Serial Simplicity
Connecting Microcontrollers to the Web with p5.js and WebSerial
As embedded engineers, we’re used to working with oscilloscopes, terminal windows, and IDEs. But what if I told you that you could create a fully interactive serial interface—complete with data visualization and custom controls—right inside your browser, using JavaScript?
That’s exactly what I set out to explore using the p5.js creative coding library and its companion p5.WebSerial
extension. The result is a lightweight, browser-based dashboard that can connect to your microcontroller over USB, read sensor values, and visualize them—all without installing any desktop software.
Let’s take a closer look at how it works.
Why WebSerial?
Traditionally, reading serial data from a microcontroller like an Arduino or ESP32 involves connecting to a COM port using a terminal program or writing a custom Python or Processing script. The Web Serial API changes the game by allowing websites (with permission) to communicate directly with serial devices.
This opens up exciting opportunities for rapid prototyping, testing, and even end-user applications—right in the browser.
The Tools
p5.js: A JavaScript library for creative coding and rapid UI development.
p5.WebSerial: An add-on that wraps the Web Serial API with an easy-to-use interface.
Modern browser: Chrome or Edge (WebSerial is not yet supported in Firefox or Safari).
What the Code Does
Here’s a quick rundown of what this sample project enables:
Draws a 400×300 canvas using p5.js.
Provides a "Choose Serial Port" button for the user to select their connected microcontroller.
Opens and manages the serial connection using Web Serial.
Reads incoming data (e.g., sensor values) and displays them on-screen in real-time.
Adds a "Disconnect" button to safely close the connection.
Core Functions:
makePortButton()
: Creates the UI for selecting a serial port.serialEvent()
: Called automatically when new serial data arrives.makeDisconnectButton()
: Lets the user close the connection gracefully.draw()
: Continuously updates the canvas with the latest sensor reading.
This allows for seamless interaction with any device that outputs serial data—such as temperature sensors, light sensors, or custom-built instruments.
How the Data Flows
Once the serial port is selected, the browser opens a connection and begins listening for incoming lines of data. Here’s what the serialEvent handler looks like:
function serialEvent() {
inData = serial.readLine();
if (inData != null) {
inData = trim(inData);
vals = int(splitTokens(inData, ","));
if (vals.length >= 1) {
console.log(vals[0]); // Print first sensor value
}
}
}
The data is parsed, logged to the console, and shown on the canvas. You can expand this to plot data over time, control actuators, or trigger events based on thresholds.
Why It Matters
This approach simplifies how we prototype and share microcontroller projects. Imagine sending someone a link instead of source code, and letting them interact with your hardware through a sleek browser UI. Whether you're debugging sensors in the lab or demonstrating a project to a client, the immediacy and accessibility of WebSerial are transformative.
Limitations & Considerations
Browser compatibility: Only Chrome and Edge currently support Web Serial.
Security model: Web Serial requires user permission to access ports—good for safety, but not suited for fully autonomous applications.
Performance: Suitable for low- to moderate-speed data (e.g., 9600–115200 baud). Not ideal for high-speed binary streams.
Get Started
You can clone or fork the project from my GitHub repository [insert repo link if applicable], and adapt it to your own microcontroller projects. For best results, ensure your device outputs clean, newline-terminated ASCII strings.
Final Thoughts
This is just the beginning. As browser-based hardware interfaces become more common, I believe we’ll see more tools that blur the line between engineering and creative coding. Whether you're building a sensor dashboard, a hardware demo, or a smart art installation, p5.js and WebSerial offer a new, elegant way to connect code and circuits.
Sometimes the best interface isn’t an app—it’s a webpage.
Here is the entire sketch.js, followed by the .ino file for the Arduino Uno Rev4 Wifi. Or grab it from GitHub.
// variable to hold an instance of the p5.webserial library:
const serial = new p5.WebSerial();
// HTML button objects:
let portButton;
let disconnectButton;
let inData; // for incoming serial data
let outByte = 0;
let vals = [];
function setup() {
createCanvas(400, 300);
if (!navigator.serial) {
alert("WebSerial is not supported in this browser. Try Chrome or MS Edge.");
}
navigator.serial.addEventListener("connect", portConnect);
navigator.serial.addEventListener("disconnect", portDisconnect);
makePortButton();
// Setup event handlers
serial.on("noport", makePortButton);
serial.on("portavailable", openPort);
serial.on("requesterror", portError);
serial.on("data", serialEvent);
serial.on("close", handleClose);
}
function draw() {
background(0);
fill(255);
text("sensor value: " + inData, 30, 50);
}
// Create the "Choose Port" button
function makePortButton() {
if (!portButton) {
portButton = createButton("Choose Serial Port");
portButton.position(10, 10);
portButton.mousePressed(async () => {
try {
// Always inside the button click handler:
await serial.requestPort(); // Opens browser port chooser
await serial.open(); // Tries to open selected port
console.log("Serial port opened");
makeDisconnectButton(); // Show disconnect button
portButton.hide(); // Hide choose button
} catch (err) {
console.error("Serial connection failed:", err);
alert("Failed to open serial port. Is it already in use?");
}
});
} else {
portButton.show();
}
}
// Create the "Disconnect" button
function makeDisconnectButton() {
if (!disconnectButton) {
disconnectButton = createButton("Disconnect");
disconnectButton.position(160, 10);
disconnectButton.mousePressed(async () => {
try {
// Try to close using p5.WebSerial method
await serial.close();
// Extra check: forcibly close the underlying Web Serial port if still connected
if (serial._port && serial._port.readable) {
await serial._port.close();
console.log("Underlying Web Serial port forcibly closed.");
}
handleClose();
} catch (err) {
console.error("Error while disconnecting:", err);
}
});
} else {
disconnectButton.show();
}
}
// Called when a port is selected and ready to open
function openPort() {
serial.open().then(() => {
console.log("Port opened successfully");
if (portButton) portButton.hide();
makeDisconnectButton(); // show disconnect button
});
}
// Called when serial port is closed
function handleClose() {
if (portButton) portButton.show();
if (disconnectButton) disconnectButton.hide();
}
// Handle serial port errors:
function portError(err) {
alert("Serial port error: " + err);
}
// Serial data received:
function serialEvent() {
inData = serial.readLine();
if (inData != null) {
inData = trim(inData);
vals = int(splitTokens(inData, ","));
if (vals.length >= 1) {
value1 = vals[0];
console.log(value1);
}
}
}
function portConnect() {
console.log("Port connected");
serial.getPorts();
}
function portDisconnect() {
console.log("Port disconnected");
serial.close();
}
function handleClose() {
console.log("Serial port closed.");
if (portButton) portButton.show();
if (disconnectButton) disconnectButton.hide();
inData = ""; // Clear last data
}
And the Arduino sketch:
void setup() {
Serial.begin(9600); // Start serial communication
}
void loop() {
int sensorValue = analogRead(A5); // Read from analog pin A5
Serial.println(sensorValue); // Send to serial port
delay(50); // Slow down output a bit (20 readings/second)
}