Learn Measure Blog Live About
Old modems, routers, network equipment. Serial, phone, audio, ethernet connectors.

Read and write from a serial port

Read and write from a serial port

The Serial API allows websites to communicate with serial devices.

Updated
Appears in: Devices

Web apps should be able to do anything iOS/Android/desktop apps can. The Capabilities project, of which Serial API is only a part, aims to do just that. To learn about other capabilities and to keep up with their progress, follow Unlocking new capabilities for the web.

What is the Serial API?

A serial port is a bidirectional communication interface that allows sending and receiving data byte by byte.

The 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 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.

This article reflects the Serial API as implemented in Chrome 86 and later. Some property names have changed from previous versions.

Suggested use cases

In the educational, hobbyist, and industrial sectors, users connect peripheral devices to their computers. These devices are often controlled by microcontrollers via 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 In Progress
3. Gather feedback & iterate on design In Progress
4. Origin trial In Progress
5. Launch Not started

Using the Serial API

Enabling via chrome://flags

To experiment with the Serial API locally on all desktop platforms, without an origin trial token, enable the #experimental-web-platform-features flag in chrome://flags.

Enabling support during the origin trial phase

The Serial API is available on all desktop platforms (Chrome OS, Linux, macOS, and Windows) as an origin trial in Chrome 80. The origin trial is expected to end just before Chrome 89 moves to stable in February 2021. The API can also be enabled using a flag.

Origin trials allow you to try new features and give feedback on their usability, practicality, and effectiveness to the web standards community. For more information, see the Origin Trials Guide for Web Developers. To sign up for this or another origin trial, visit the registration page.

Register for the origin trial

  1. Request a token for your origin.
  2. Add the token to your pages. There are two ways to do that:
    • Add an origin-trial <meta> tag to the head of each page. For example, this may look something like:
      <meta http-equiv="origin-trial" content="TOKEN_GOES_HERE">
    • If you can configure your server, you can also add the token using an Origin-Trial HTTP header. The resulting response header should look something like:
      Origin-Trial: TOKEN_GOES_HERE

Feature detection

To check if the Serial API is supported, use:

if ("serial" in navigator) {
// The Serial API is supported.
}

Open a serial port

The 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(), or pick one from navigator.serial.getPorts() which returns a list of serial ports the website has been granted access to previously.

// 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();
Screenshot of a serial port prompt on a website
User prompt for selecting a BBC micro:bit

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 Serial API are handled by the Streams API.

If streams are new to you, check out Streams API concepts. This article barely scratches the surface of streams and stream handling.

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);
}

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.

const reader = port.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 Uint8Array.
console.log(value);
}

// Force reader.read() to resolve immediately and subsequently
// call reader.releaseLock() in the loop example above.
await reader.cancel();
await port.close();

Closing a serial port is more complicated when using transform streams (like TextDecoderStream and TextEncoderStream). 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.

navigator.serial.addEventListener("connect", (event) => {
// TODO: Automatically open event.port or warn user a port is available.
});

navigator.serial.addEventListener("disconnect", (event) => {
// TODO: Remove |event.port| from the UI.
// If the serial port was opened, a stream error would be observed as well.
});

Handle signals

After establishing the serial port connection, you can explicitly query and set signals exposed by the serial port for device detection and flow control. These signals are defined as boolean values. For example, some devices such as Arduino will enter a programming mode if the Data Terminal Ready (DTR) signal is toggled.

Setting output signals and getting input signals are respectively done by calling port.setSignals() and port.getSignals(). See usage examples below.

// Turn off Serial Break signal.
await port.setSignals({ break: false });

// Turn on Data Terminal Ready (DTR) signal.
await port.setSignals({ dataTerminalReady: true });

// Turn off Request To Send (RTS) signal.
await port.setSignals({ requestToSend: false });
const signals = await port.getSignals();
console.log(`Clear To Send: ${signals.clearToSend}`);
console.log(`Data Carrier Detect: ${signals.dataCarrierDetect}`);
console.log(`Data Set Ready: ${signals.dataSetReady}`);
console.log(`Ring Indicator: ${signals.ringIndicator}`);

Transforming streams

When you receive data from the serial device, you won't necessarily get all of the data at once. It may be arbitrarily chunked. For more information, see Streams API concepts.

To deal with this, you can use some built-in transform streams such as TextDecoderStream or create your own transform stream which allows you to parse the incoming stream and return parsed data. The transform stream sits between the serial device and the read loop that is consuming the stream. It can apply an arbitrary transform before the data is consumed. Think of it like an assembly line: as a widget comes down the line, each step in the line modifies the widget, so that by the time it gets to its final destination, it's a fully functioning widget.

Photo of an aeroplane factory
World War II Castle Bromwich Aeroplane Factory

For example, consider how to create a transform stream class that consumes a stream and chunks it based on line breaks. Its transform() method is called every time new data is received by the stream. It can either enqueue the data or save it for later. The flush() method is called when the stream is closed, and it handles any data that hasn't been processed yet.

To use the transform stream class, you need to pipe an incoming stream through it. In the third code example under Read from a serial port, the original input stream was only piped through a TextDecoderStream, so we need to call pipeThrough() to pipe it through our new LineBreakTransformer.

class LineBreakTransformer {
constructor() {
// A container for holding stream data until a new line.
this.chunks = "";
}

transform(chunk, controller) {
// Append new chunks to existing chunks.
this.chunks += chunk;
// For each line breaks in chunks, send the parsed lines out.
const lines = this.chunks.split("\r\n");
this.chunks = lines.pop();
lines.forEach((line) => controller.enqueue(line));
}

flush(controller) {
// When the stream is closed, flush any remaining chunks out.
controller.enqueue(this.chunks);
}
}
const textDecoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
const reader = textDecoder.readable
.pipeThrough(new TransformStream(new LineBreakTransformer()))
.getReader();

For debugging serial device communication issues, use the tee() method of port.readable to split the streams going to or from the serial device. The two streams created can be consumed independently and this allows you to print one to the console for inspection.

const [appReadable, devReadable] = port.readable.tee();

// You may want to update UI with incoming data from appReadable
// and log incoming data in JS console for inspection from devReadable.

Dev Tips

Debugging the Serial API in Chrome is easy with the internal page, chrome://device-log where you can see all serial device related events in one single place.

Screenshot of the internal page for debugging the Serial API.
Internal page in Chrome for debugging the Serial API.

The internal page supports debugging the Serial API in Chrome 87 and later.

Codelab

In the Google Developer codelab, you'll use the Serial API to interact with a BBC micro:bit board to show images on its 5x5 LED matrix.

Polyfill

On Android, support for USB-based serial ports is possible using the WebUSB API and the Serial API polyfill. This polyfill is limited to hardware and platforms where the device is accessible via the WebUSB API because it has not been claimed by a built-in device driver.

Security and privacy

The spec authors have designed and implemented the Serial API using the core principles defined in Controlling Access to Powerful Web Platform Features, including user control, transparency, and ergonomics. The ability to use this API is primarily gated by a permission model that grants access to only a single serial device at a time. In response to a user prompt, the user must take active steps to select a particular serial device.

To understand the security tradeoffs, check out the security and privacy sections of the Serial API Explainer.

Feedback

The Chrome team would love to hear about your thoughts and experiences with the Serial API.

Tell us about the API design

Is there something about the API that doesn't work as expected? Or are there missing methods or properties that you need to implement your idea?

File a spec issue on the Serial API GitHub repo or add your thoughts to an existing issue.

Report a problem with the implementation

Did you find a bug with Chrome's implementation? Or is the implementation different from the spec?

File a bug at https://new.crbug.com. Be sure to include as much detail as you can, provide simple instructions for reproducing the bug, and have Components set to Blink>Serial. Glitch works great for sharing quick and easy repros.

Show support

Are you planning to use the Serial API? Your public support helps the Chrome team prioritize features and shows other browser vendors how critical it is to support them.

Send a Tweet to @ChromiumDev with the hashtag #SerialAPI and let us know where and how you're using it.

Helpful links

Demos:

Acknowledgements

Thanks to Reilly Grant and Joe Medley for their reviews of this article. Aeroplane factory photo by Birmingham Museums Trust on Unsplash.

Last updated: Improve article