Skip to main content

The serial API allows websites to communicate with serial devices.


Updated

What is the serial API?

A serial port is a bidirectional communication interface that allows you to send and receive data byte by byte.

The serial API provides a way for websites to read and write to a serial device with JavaScript. Serial devices connect 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 for WebUSB as operating systems require applications to communicate with some serial ports using their native top-level serial API instead of the low-level USB API.

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

Suggested use cases

In the educational, hobby and industrial sectors, users connect peripheral devices to their computers. These devices are often controlled by microcontrollers through 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 a native agent application that users installed manually. In others, the application is delivered in a native application packaged through a framework such as Electron. And in others, the user must take 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 enhanced by providing direct communication between the website and the device it is controlling.

Actual state

Using the serial API

Enable support during the proof of origin phase

The Serial API is available on all desktop platforms (Chrome OS, Linux, macOS, and Windows) as a proof of origin on Chrome 80. The proof of origin is expected to finish just before Chrome 89 is set in February 2021 The API can also be enabled by a flag.

Origin testing allows you to test new features and provide feedback on their usability, practicality, and effectiveness to the web standards community. For more information, see the Origin testing guide for web developers. To enroll in this or any other proof of origin, visit the registration page.

Register for proof of origin

  1. Request a token by your origin.
  2. Add the token to your pages. There are two ways to do it:
    • Add a origin-trial tag to the header of each page. For example, this might look like this:
    • If you can configure your server, you can also add the token using a Origin-Trial HTTP header. The resulting response header should look like this:
      Origin-Trial: TOKEN_GOES_HERE

Enabling via chrome: // flags

To experiment with the serial API locally on all desktop platforms, without a source test token, enable the #experimental-web-platform-features flag on
chrome://flags.

Feature detection

To check if the serial API is supported, use:

if ( "serial" in navigator ) {
}

Open a serial port

The serial API is asynchronous by design. This prevents the website UI from crashing when input is expected, 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. To do this, you can ask the user to select a single serial port by calling
navigator.serial.requestPort ()or choose one of navigator.serial.getPorts ()
which returns a list of serial ports that the website has previously accessed.


const port = await navigator . serial . requestPort ( ) ;


const ports = await navigator . serial . getPorts ( ) ;

the navigator.serial.requestPort () The function takes an optional object literal that defines filters. They are used to match any serial device connected via USB to a mandatory USB provider (usbVendorId) and optional USB product identifiers (usbProductId).


const filters = [
{ usbVendorId : 0x2341 , usbProductId : 0x0043 } ,
{ usbVendorId : 0x2341 , usbProductId : 0x0001 }
] ;


const port = await navigator . serial . requestPort ( { filters } ) ;

const { usbProductId , usbVendorId } = port . getInfo ( ) ;

serial-port-prompt-1927133
User prompt to select a BBC micro: bit

Vocation 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 the serial port will open. the baudRate The 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 documentation for the correct value as all 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 can be safely set to any value as it is ignored by the emulation.


const port = await navigator . serial . requestPort ( ) ;


await port . open ( { baudRate : 9600 } ) ;

You can also specify any of the following options when opening a serial port. These options are optional and have predetermined values.

  • dataBits: The number of data bits per frame (7 or 8).
  • stopBits: The number of stop bits at the end of a frame (1 or 2).
  • parity: The parity mode (either "none", "even" or "odd").
  • bufferSize: The size of the read and write buffers to create (must be less than 16 MB).
  • flowControl: The flow control mode (either "none" or "hardware").

Read from a serial port

The input and output streams in the serial API are handled by the Streams API.

If the streams are new to you, see Streams API concepts. This article just touches the surface of streams and their management.

Once the serial port connection is established, readable and writable
properties of SerialPort object returns a ReadableStream and a
WritableStream. They will be used to receive data and send data to the serial device. They 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. Yes
done it's true, the serial port has been closed or no more data is coming in. Calling port.readable.getReader () create a reader and block readable it. While readable it is locked, the serial port cannot be closed.

const reader = port . readable . getReader ( ) ;


while ( true ) {
const { value , done } = await reader . read ( ) ;
if ( done ) {
reader . releaseLock ( ) ;
break ;
}
console . log ( value ) ;
}

Some non-fatal serial port read errors can occur 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 not fatal, a new ReadableStream it is created automatically. If a fatal error such as serial device removal occurs, port.readable becomes null.

while ( port . readable ) {
const reader = port . readable . getReader ( ) ;

try {
while ( true ) {
const { value , done } = await reader . read ( ) ;
if ( done ) {

reader . releaseLock ( ) ;
break ;
}
if ( value ) {
console . log ( value ) ;
}
}
} catch ( error ) {

}
}

If the serial device sends text back, you can pipe port.readable through a
TextDecoderStream As shown below. A TextDecoderStream is a transform current
that grabs everything Uint8Array fragments and turns them into strings.

const textDecoder = new TextDecoderStream ( ) ;
const readableStreamClosed = port . readable . pipeTo ( textDecoder . writable ) ;
const reader = textDecoder . readable . getReader ( ) ;


while ( true ) {
const { value , done } = await reader . read ( ) ;
if ( done ) {

reader . releaseLock ( ) ;
break ;
}

console . log ( value ) ;
}

Write to a serial port

To send data to a serial device, pass the data to
port.writable.getWriter (). write (). Vocation releaseLock () in
port.writable.getWriter () it is necessary for the serial port to be closed later.

const writer = port . writable . getWriter ( ) ;

const data = new Uint8Array ( [ 104 , 101 , 108 , 108 , 111 ] ) ;
await writer . write ( data ) ;


writer . releaseLock ( ) ;

Send text to the device through a TextEncoderStream channeled 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 () close the serial port if your readable and writable The members are unlocked, sense releaseLock () It has been summoned by its respective reader and writer.

await port . close ( ) ;

However, when continuously reading data from a serial device using a loop,
port.readable it will always be blocked until it encounters an error. In this case, calling reader.cancel () force to reader.read () to solve immediately with {value: undefined, done: true} and thus allowing the loop to call reader.releaseLock ().



const reader = port . readable . getReader ( ) ;


while ( true ) {
const { value , done } = await reader . read ( ) ;
if ( done ) {
reader . releaseLock ( ) ;
break ;
}

console . log ( value ) ;
}



await reader . cancel ( ) ;
await port . close ( ) ;

Closing a serial port is more complicated when using it transform streams (I like it
TextDecoderStream and TextEncoderStream). Call reader.cancel () like before. Then call writer.close () and port.close (). This propagates the errors through the transform streams to the underlying serial port. Because error propagation does not happen immediately, you should use the readableStreamClosed and
writableStreamClosed previously created promises to detect when port.readable
and port.writable have been unlocked. Cancel the reader causes the sequence to be aborted; that's why you have to catch and ignore the resulting error.



const textDecoder = new TextDecoderStream ( ) ;
const readableStreamClosed = port . readable . pipeTo ( textDecoder . writable ) ;
const reader = textDecoder . readable . getReader ( ) ;


while ( true ) {
const { value , done } = await reader . read ( ) ;
if ( done ) {
reader . releaseLock ( ) ;
break ;
}

console . log ( value ) ;
}

const textEncoder = new TextEncoderStream ( ) ;
const writableStreamClosed = textEncoder . readable . pipeTo ( port . writable ) ;

reader . cancel ( ) ;
await readableStreamClosed . catch ( ( ) => { } ) ;

writer . close ( ) ;
await writableStreamClosed ;

await port . close ( ) ;

Hear the connection and disconnection

If a USB device provides a serial port, then that device can be connected to 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 ) => {
} ) ;

navigator . serial . addEventListener ( "disconnect" , ( event ) => {
} ) ;

Handle signals

After establishing the serial port connection, you can explicitly query and configure the signals exposed by the serial port for device discovery and flow control. These signals are defined as Boolean values. For example, some devices like Arduino will go into a programming mode if the Data Terminal Ready (DTR) signal is activated.

Adjustment exit signs and get input signals are made respectively by calling port.setSignals () and port.getSignals (). See the usage examples below.


await port . setSignals ( { break : false } ) ;


await port . setSignals ( { dataTerminalReady : true } ) ;


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 will not necessarily get all the data at once. It can be arbitrarily fragmented. For more information, see
Streams API concepts.

To deal with this, you can use some built-in transform streams, like
TextDecoderStream or create your own transformation flow that allows you to analyze the incoming flow and return analyzed data. The transform stream is between the serial device and the read loop that is consuming the stream. You can apply an arbitrary transformation before the data is consumed. Think of it like an assembly line - as a widget moves down the line, each step on the line modifies the widget so that when it reaches its final destination, it is a fully functioning widget.

airplane-factory-2628261
Castle Bromwich WWII Aircraft Factory

For example, consider how to create a transform stream class that consumes a stream and chunks it based on line breaks. Their transform () the method is called each time the flow receives new data. You can queue the data or save it for later. the flush () The method is called when the stream is closed and handles any data that has not yet been processed.

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

class LineBreakTransformer {
constructor ( ) {
this . chunks = "" ;
}

transform ( chunk , controller ) {
this . chunks + = chunk ;
const lines = this . chunks . split ( "rn" ) ;
this . chunks = lines . pop ( ) ;
lines . forEach ( ( line ) => controller . enqueue ( line ) ) ;
}

flush ( controller ) {
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 ( ) ;

To debug serial device communication problems, use the tee() method of
port.readable to split the transmissions going to or from the serial device. The two created flows can be consumed independently and this allows you to print one on the console for inspection.

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

Codelab

At Google Developer Code Lab, you will use the serial API to interact with a
BBC micro: bit board to display images on its 5 × 5 LED matrix.

Polyfill

On Android, USB-based serial ports can be supported through the WebUSB API and Polyfill API series. This polyfill is limited to hardware and platforms where the device can be accessed via the WebUSB API because it has not been claimed by a built-in device driver.

Security and privacy

The specification authors have designed and implemented the serial API using the basic principles defined in Control access to powerful features of the web platform, including user control, transparency and ergonomics. The ability to use this API is primarily controlled by a permissions model that grants access to only one serial device at a time. In response to a user request, the user must take active steps to select a particular serial device.

To understand the security tradeoffs, see the security and privacy
Serial API Explainer sections.

Feedback

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

Tell us about the API design

Is there something in the API that is not working as expected? Or are you missing any methods or properties you need to implement your idea?

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

Report a problem with the deployment

Found a bug with the Chrome implementation? Or is the implementation different from the specification?

File a bug in https://new.crbug.com. Be sure to include as much detail as you can, provide simple instructions for reproducing the bug, and
Components adjusted to Blink> Serial. Failure works great for quick and easy sharing of reps.

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 important it is to support them.

Send a tweet to @Cromodev with the hashtag
#SerialAPI

and let us know where and how you are using it.

Helpful Links

Population:

Thanks

Thanks to Reilly Scholarship and Joe medley for their reviews of this article. Aircraft factory photo by Birmingham Museums Trust in Unsplash.


error: Attention: Protected content.