The Web Serial API allows websites to communicate with serial devices.
What is the Web Serial API?
A serial port is a bidirectional communication interface that allows sending and receiving data byte by byte.
The Web Serial API provides a way for websites to read from and write to a serial device with JavaScript. Serial devices are connected either through a serial port on the user's system or through removable USB and Bluetooth devices that emulate a serial port.
In other words, the Web Serial API bridges the web and the physical world by allowing websites to communicate with serial devices, such as microcontrollers and 3D printers.
This API is also a great companion to WebUSB as operating systems require applications to communicate with some serial ports using their higher-level serial API rather than the low-level USB API.
Suggested use cases
In the educational, hobbyist, and industrial sectors, users connect peripheral devices to their computers. These devices are often controlled by microcontrollers with a serial connection used by custom software. Some custom software to control these devices is built with web technology:
In some cases, websites communicate with the device through an agent application that users installed manually. In others, the application is delivered in a packaged application through a framework such as Electron. And in others, the user is required to perform an additional step such as copying a compiled application to the device via a USB flash drive.
In all these cases, the user experience will be improved by providing direct communication between the website and the device that it is controlling.
Current status
| Step | Status |
|---|---|
| 1. Create explainer | Complete |
| 2. Create initial draft of specification | Complete |
| 3. Gather feedback & iterate on design | Complete |
| 4. Origin trial | Complete |
| 5. Launch | Complete |
Using the Web Serial API
Feature detection
To check if the Web Serial API is supported, use:
if ("serial" in navigator) {
// The Web Serial API is supported.
}
Open a serial port
The Web Serial API is asynchronous by design. This prevents the website UI from blocking when awaiting input, which is important because serial data can be received at any time, requiring a way to listen to it.
To open a serial port, first access a SerialPort object. For this, you can
either prompt the user to select a single serial port by calling
navigator.serial.requestPort() in response to a user gesture such as touch
or mouse click, or pick one from navigator.serial.getPorts() which returns
a list of serial ports the website has been granted access to.
document.querySelector('button').addEventListener('click', async () => {
// Prompt user to select any serial port.
const port = await navigator.serial.requestPort();
});
// Get all serial ports the user has previously granted the website access to.
const ports = await navigator.serial.getPorts();
The navigator.serial.requestPort() function takes an optional object literal
that defines filters. Those are used to match any serial device connected over
USB with a mandatory USB vendor (usbVendorId) and optional USB product
identifiers (usbProductId).
// Filter on devices with the Arduino Uno USB Vendor/Product IDs.
const filters = [
{ usbVendorId: 0x2341, usbProductId: 0x0043 },
{ usbVendorId: 0x2341, usbProductId: 0x0001 }
];
// Prompt user to select an Arduino Uno device.
const port = await navigator.serial.requestPort({ filters });
const { usbProductId, usbVendorId } = port.getInfo();
Calling requestPort() prompts the user to select a device and returns a
SerialPort object. Once you have a SerialPort object, calling port.open()
with the desired baud rate will open the serial port. The baudRate dictionary
member specifies how fast data is sent over a serial line. It is expressed in
units of bits-per-second (bps). Check your device's documentation for the
correct value as all the data you send and receive will be gibberish if this is
specified incorrectly. For some USB and Bluetooth devices that emulate a serial
port this value may be safely set to any value as it is ignored by the
emulation.
// Prompt user to select any serial port.
const port = await navigator.serial.requestPort();
// Wait for the serial port to open.
await port.open({ baudRate: 9600 });
You can also specify any of the options below when opening a serial port. These options are optional and have convenient default values.
dataBits: The number of data bits per frame (either 7 or 8).stopBits: The number of stop bits at the end of a frame (either 1 or 2).parity: The parity mode (either"none","even"or"odd").bufferSize: The size of the read and write buffers that should be created (must be less than 16MB).flowControl: The flow control mode (either"none"or"hardware").
Read from a serial port
Input and output streams in the Web Serial API are handled by the Streams API.
After the serial port connection is established, the readable and writable
properties from the SerialPort object return a ReadableStream and a
WritableStream. Those will be used to receive data from and send data to the
serial device. Both use Uint8Array instances for data transfer.
When new data arrives from the serial device, port.readable.getReader().read()
returns two properties asynchronously: the value and a done boolean. If
done is true, the serial port has been closed or there is no more data coming
in. Calling port.readable.getReader() creates a reader and locks readable to
it. While readable is locked, the serial port can't be closed.
const reader = port.readable.getReader();
// Listen to data coming from the serial device.
while (true) {
const { value, done } = await reader.read();
if (done) {
// Allow the serial port to be closed later.
reader.releaseLock();
break;
}
// value is a Uint8Array.
console.log(value);
}
Some non-fatal serial port read errors can happen under some conditions such as
buffer overflow, framing errors, or parity errors. Those are thrown as
exceptions and can be caught by adding another loop on top of the previous one
that checks port.readable. This works because as long as the errors are
non-fatal, a new ReadableStream is created automatically. If a fatal error
occurs, such as the serial device being removed, then port.readable becomes
null.
while (port.readable) {
const reader = port.readable.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) {
// Allow the serial port to be closed later.
reader.releaseLock();
break;
}
if (value) {
console.log(value);
}
}
} catch (error) {
// TODO: Handle non-fatal read error.
}
}
If the serial device sends text back, you can pipe port.readable through a
TextDecoderStream as shown below. A TextDecoderStream is a transform stream
that grabs all Uint8Array chunks and converts them to strings.
const textDecoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
const reader = textDecoder.readable.getReader();
// Listen to data coming from the serial device.
while (true) {
const { value, done } = await reader.read();
if (done) {
// Allow the serial port to be closed later.
reader.releaseLock();
break;
}
// value is a string.
console.log(value);
}
You can take control of how memory is allocated when you read from the stream using a "Bring Your Own Buffer" reader. Call port.readable.getReader({ mode: "byob" }) to get the ReadableStreamBYOBReader interface and provide your own ArrayBuffer when calling read(). Note that the Web Serial API supports this feature in Chrome 106 or later.
try {
const reader = port.readable.getReader({ mode: "byob" });
// Call reader.read() to read data into a buffer...
} catch (error) {
if (error instanceof TypeError) {
// BYOB readers are not supported.
// Fallback to port.readable.getReader()...
}
}
Here's an example of how to reuse the buffer out of value.buffer:
const bufferSize = 1024; // 1kB
let buffer = new ArrayBuffer(bufferSize);
// Set `bufferSize` on open() to at least the size of the buffer.
await port.open({ baudRate: 9600, bufferSize });
const reader = port.readable.getReader({ mode: "byob" });
while (true) {
const { value, done } = await reader.read(new Uint8Array(buffer));
if (done) {
break;
}
buffer = value.buffer;
// Handle `value`.
}
Here's another example of how to read a specific amount of data from a serial port:
async function readInto(reader, buffer) {
let offset = 0;
while (offset < buffer.byteLength) {
const { value, done } = await reader.read(
new Uint8Array(buffer, offset)
);
if (done) {
break;
}
buffer = value.buffer;
offset += value.byteLength;
}
return buffer;
}
const reader = port.readable.getReader({ mode: "byob" });
let buffer = new ArrayBuffer(512);
// Read the first 512 bytes.
buffer = await readInto(reader, buffer);
// Then read the next 512 bytes.
buffer = await readInto(reader, buffer);
Write to a serial port
To send data to a serial device, pass data to
port.writable.getWriter().write(). Calling releaseLock() on
port.writable.getWriter() is required for the serial port to be closed later.
const writer = port.writable.getWriter();
const data = new Uint8Array([104, 101, 108, 108, 111]); // hello
await writer.write(data);
// Allow the serial port to be closed later.
writer.releaseLock();
Send text to the device through a TextEncoderStream piped to port.writable
as shown below.
const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
const writer = textEncoder.writable.getWriter();
await writer.write("hello");
Close a serial port
port.close() closes the serial port if its readable and writable members
are unlocked, meaning releaseLock() has been called for their respective
reader and writer.
await port.close();
However, when continuously reading data from a serial device using a loop,
port.readable will always be locked until it encounters an error. In this
case, calling reader.cancel() will force reader.read() to resolve
immediately with { value: undefined, done: true } and therefore allowing the
loop to call reader.releaseLock().
// Without transform streams.
let keepReading = true;
let reader;
async function readUntilClosed() {
while (port.readable && keepReading) {
reader = port.readable.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) {
// reader.cancel() has been called.
break;
}
// value is a Uint8Array.
console.log(value);
}
} catch (error) {
// Handle error...
} finally {
// Allow the serial port to be closed later.
reader.releaseLock();
}
}
await port.close();
}
const closedPromise = readUntilClosed();
document.querySelector('button').addEventListener('click', async () => {
// User clicked a button to close the serial port.
keepReading = false;
// Force reader.read() to resolve immediately and subsequently
// call reader.releaseLock() in the loop example above.
reader.cancel();
await closedPromise;
});
Closing a serial port is more complicated when using transform streams. Call reader.cancel() as before.
Then call writer.close() and port.close(). This propagates errors through
the transform streams to the underlying serial port. Because error propagation
doesn't happen immediately, you need to use the readableStreamClosed and
writableStreamClosed promises created earlier to detect when port.readable
and port.writable have been unlocked. Cancelling the reader causes the
stream to be aborted; this is why you must catch and ignore the resulting error.
// With transform streams.
const textDecoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
const reader = textDecoder.readable.getReader();
// Listen to data coming from the serial device.
while (true) {
const { value, done } = await reader.read();
if (done) {
reader.releaseLock();
break;
}
// value is a string.
console.log(value);
}
const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
reader.cancel();
await readableStreamClosed.catch(() => { /* Ignore the error */ });
writer.close();
await writableStreamClosed;
await port.close();
Listen to connection and disconnection
If a serial port is provided by a USB device then that device may be connected
or disconnected from the system. When the website has been granted permission to
access a serial port, it should monitor the connect and disconnect events.