WebSerial: How to communicate with a microcontroller from the browser
I want to talk a bit about the WebSerial API. You have likely seen websites that allow you to flash microcontrollers directly from a web page. I first encountered this with the Micro:bit educational kit. A child can build a Scratch algorithm and, with a single click, upload the firmware to the board straight from the browser.
This is incredibly convenient (as long as you have an internet connection):
No need to install specialized software (IDEs, drivers, or command-line utilities).
No complex environment setups or configurations.
Minimal permission issues (though Linux users might still need occasional adjustments).
Essentially, you only need a single HTML + JavaScript page to handle all the logic. It works almost everywhere on Chromium-based browsers (Google Chrome, Microsoft Edge, Opera, Vivaldi, Brave). Interestingly, on Chrome for Android, this method provides access to Bluetooth/BLE serial, though I couldn’t get it to work with USB CDC devices.
It is also worth noting that support is missing in Firefox and Safari. Their developers officially consider this technology potentially dangerous because it grants web pages direct access to the computer’s hardware.
What we need to get started:
Hosting with HTTPS support. This is a critical security requirement. The WebSerial API (like most modern Web APIs) works exclusively in a secure context. If your site runs over plain HTTP, the browser will simply block access to the ports. You can also use a local HTTP server (i.e., http://localhost), which is considered secure by default.
A microcontroller with USB CDC support. Your device must be able to act as a virtual COM port. In my case, I’ll be using an RP2040 module, which is perfect for this task since CDC support is built-in at both the hardware and standard library levels.
RP2040 Hello World
Let’s start with a classic example to verify the connection between the hardware and the browser. When creating a project in the SDK, it is crucial to enable the Console over USB option. This allows you to redirect the standard I/O (stdio) from the hardware UART to a virtual COM port via USB. Once enabled, standard functions like printf, fread, and fwrite will work directly with this interface.
In your CMakeLists.txt file, this is controlled by the following lines:
pico_enable_stdio_usb(project_name 1) pico_enable_stdio_uart(project_name 0)
Firmware Code:
#include "pico/stdlib.h"
int main()
{
stdio_init_all();
while (true) {
printf("Hello, world %d!\n",
to_ms_since_boot(get_absolute_time())
);
sleep_ms(1000);
}
}Once a second, the microcontroller will send a test string to the USB port. Our next task is to “catch” it on the webpage side.
Web Section
In JavaScript, the first thing you need to do is check for WebSerial support. This is done as follows:
if (!('serial' in navigator)) {
alert('Web Serial API not supported in this browser.');
return;
}
}The next step is connecting to and opening the port. Since WebSerial
API methods return Promises, they must be called using
the await operator inside asynchronous functions:
port = await navigator.serial.requestPort();
await port.open({ baudRate: 115200 });To terminate the session, the port.close() method is
used.
To receive data, you need to obtain a Reader object. Since the data stream is binary, the reader returns buffers (Uint8Array). If you are expecting text, this data needs to be decoded, for example, using a TextDecoder:
const textDecoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) break;
console.log(textDecoder.decode(value));
}Full Page Example
Let’s create a simple page with two buttons: Connect and Disconnect. All received data will be decoded into text and printed to the browser console.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSerial Simple Connect</title>
</head>
<body>
<button id="connectBtn">Connect</button>
<button id="disconnectBtn">Disconnect</button>
<script>
var port = null;
var reader = null;
document.getElementById('connectBtn').onclick = async () => {
if (!('serial' in navigator)) {
alert('Web Serial API not supported in this browser.');
return;
}
port = await navigator.serial.requestPort();
await port.open({ baudRate: 115200 });
console.log('Connected to serial device');
reader = port.readable.getReader();
const textDecoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) break;
console.log(textDecoder.decode(value));
}
};
document.getElementById('disconnectBtn').onclick = async () => {
console.log('Disconnecting from serial device');
if (reader) {
await reader.cancel();
reader = null;
}
if (port) {
await port.close();
port = null;
}
};
</script>
</body>
</html>When you click the Connect button, a device selection dialog appears:
In the list, we see ttyACM0 — that is my RP2040 Pico.
After connecting, a stream of messages appears in the console:
It is worth noting that data over a serial port can arrive in chunks. As seen in the screenshot, a single line might be split across several separate buffers. Developers must handle these cases manually (for example, by buffering and concatenating strings until a newline character is received).
Conclusions
WebSerial API is an incredibly exciting technology. It allows you to instantly visualize data from a microcontroller on a large computer screen or even control hardware without installing any specialized software. Where to apply WebSerial:
Quick Dashboard Creation: Perfect for displaying sensor graphs or debug logs in a user-friendly web interface.
Firmware Updates: Excellent for service pages where a user can update their device directly through the browser.
Cross-platform Tools: Your tools will work on Windows, macOS, and Linux without needing to rewrite code for each OS.


Comments
Post a Comment