Web Serial API: Connect Microcontrollers with JS
The Web Serial API provides a direct bridge between web applications and serial hardware devices, such as microcontrollers, without requiring native companion software or intermediate local servers. By exposing standard serial communication interfaces through modern browser APIs, developers can discover, connect, read from, and write to devices like Arduino, ESP32, and Raspberry Pi Pico directly from client-side JavaScript. This article explains how the API establishes this hardware-to-browser connection, its underlying security model, and the core implementation steps required for bidirectional data transfer.
The Core Communication Model
Traditional web browsers operate in an isolated sandbox with no
direct access to underlying operating system hardware. The Web Serial
API safely bridges this gap by exposing the operating system’s serial
ports via the navigator.serial interface. When a web page
requests access, the browser acts as an intermediary, translating
JavaScript streams into raw serial byte streams managed by the operating
system’s serial drivers.
Security and User Permissions
Hardware access requires strict security measures. The Web Serial API
enforces two fundamental security mechanisms: 1. Secure
Contexts: The API is only accessible over HTTPS or
localhost. 2. Explicit User Consent: A
website cannot automatically scan for or connect to serial devices.
Connection initiation requires a transient user activation (such as a
button click) calling navigator.serial.requestPort(). This
opens a native browser prompt allowing the user to select the specific
connected microcontroller.
Requesting and Opening a Port
To establish communication, JavaScript prompts the user to select a device, optionally filtering by USB vendor and product IDs. Once selected, the port is opened with defined serial transmission parameters:
// Request device selection
const port = await navigator.serial.requestPort();
// Open the connection with specific serial parameters
await port.open({
baudRate: 115200,
dataBits: 8,
stopBits: 1,
parity: "none"
});Reading Data from the Microcontroller
Data transfer relies on the standard Streams API. Incoming bytes are
handled through port.readable. To process text data, the
raw byte stream can be piped through a
TextDecoderStream:
const textDecoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
const reader = textDecoder.readable.getReader();
// Read incoming serial data in a loop
while (true) {
const { value, done } = await reader.read();
if (done) {
reader.releaseLock();
break;
}
console.log("Received from microcontroller:", value);
}Writing Data to the Microcontroller
Sending commands or payloads to the device follows an equivalent
streaming pattern using port.writable. A
TextEncoderStream converts JavaScript strings into raw byte
buffers sent over the wire:
const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
const writer = textEncoder.writable.getWriter();
// Send a command to the device
await writer.write("LED_ON\n");Disconnecting and Port Cleanup
To avoid locking the serial interface on the host operating system, connections must be cleanly terminated. Closing a connection involves canceling active stream readers and writers before closing the port object itself:
await reader.cancel();
await readableStreamClosed.catch(() => {});
await writer.close();
await writableStreamClosed;
await port.close();By leveraging standard serial communication protocols and modern web stream architectures, the Web Serial API transforms the browser into an interactive environment for hardware control, firmware updates, and real-time telemetry display.