Web Serial API

This article is mainly for translation:https://web.dev/serial/This blog post is for learning how to use the API and developing requirements for the project. Follow me to learn more aboutWeb Serial APIStep pit experience sharing, video sharing version:https://www.bilibili.com/video/BV1N54y1L7Wz/.

What is the Web Serial API?

A serial port is a two-way communication interface that allows bytes to send and receive data.

The Web Serial API provides a way for Web sites to read and write to Serial devices using JavaScript. Serial devices can be connected through serial ports on the user’s system or via removable USB and Bluetooth devices that simulate serial ports.

In other words, the Web Serial API connects the Web to the physical world by allowing websites to communicate with Serial devices, such as microcontrollers and 3D printers.

This API is also a good companion to WebUSB, because operating systems require applications to communicate with some serial ports using their high-level serial API rather than the low-level USB API.

Suggest use cases

In the educational, amateur, and industrial sectors, users connect peripherals to their computers. These devices are usually controlled by microcontrollers through serial connections used by custom software. Some of the custom software that controls these devices is built using networking technology:

  • Arduino Create
  • Betaflight Configurator
  • Espruino Web IDE
  • Microsoft MakeCode

In some cases, websites communicate with devices through proxy applications that users install manually. In other cases, applications are delivered as packaged applications through a framework such as Electron. In other cases, users need to perform additional steps, such as copying the compiled application to the device via a USB flash drive.

In all of these cases, the user experience will be improved by providing direct communication between the site and the devices it controls.

How do I use the Web Serial API

Feature detection

Check whether your browser supports the Web Serial API:

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

Open the serial port

The Web Serial API is asynchronous by design. This prevents the site UI from blocking while waiting for input, which is important because serial data can be received at any time and you need a way to listen for it. To open a SerialPort, first access a SerialPort object. To do this, you can call the navigator. Serial. RequestPort () to prompt the user to select a serial port, or from the navigator. Serial. GetPorts () a selection, this method returns a previously granted the site list of serial port access.

// Prompts the user to select a serial port.
const port = await navigator.serial.requestPort();
Copy the code
// Get all serial ports that the user previously granted access to the site.
const ports = await navigator.serial.getPorts();
Copy the code

The requestPort () function takes an optional object literal that defines the filter. They are used to match any serial device connected over USB with a mandatory USB vendor (usbVendorId) and optional USB Product identifier (usbProductId).

// Filter device with Arduino Uno USB supplier/product ID.
const filters = [
  { usbVendorId: 0x2341.usbProductId: 0x0043 },
  { usbVendorId: 0x2341.usbProductId: 0x0001}];// Prompts the user to select an Arduino Uno device.
const port = await navigator.serial.requestPort({ filters });

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

The call to 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 SerialPort. The baudRate dictionary member specifies the speed at which data is sent over the serial line. It is expressed in bits per second (BPS). Check your device’s documentation for correct values, as all data sent and received will be garbled if this is specified incorrectly. For some USB and Bluetooth devices that simulate a serial port, this value can safely be set to any value because it will be ignored by the simulation.

// Prompts the user to select a serial port
const port = await navigator.serial.requestPort();

// Wait for the serial port to open
await port.open({ baudRate: 9600 });
Copy the code

You can also specify any of the following options when opening the serial port. These options are optional and have convenient defaults.

  • dataBits: Data bits per frame (7 or 8).
  • stopBits: The number of stop bits (1 or 2) at the end of a frame.
  • parity: verification mode, which can be None, even, or odd.
  • bufferSize: The size of the read/write buffer that should be created (must be less than 16MB).
  • flowControl: Flow control mode (None or Hardware).

Reading from the serial port

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

After the SerialPort connection is established, the readable and writable properties of the SerialPort object return a ReadableStream and a WritableStream. These will be used to receive data from and send data to the serial device. Both use the Uint8Array instance for data transfer.

When new data arrives from the serial device, port.readable.getreader ().read() asynchronously returns two properties :value and a done Boolean value. If done is true, the serial port is closed, or there is no more data input. Call port.readable.getreader () to create a reader and lock it as readable. The serial port cannot be closed when readable is locked.

const reader = port.readable.getReader();

// Listen for data from serial devices
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);
}
Copy the code

Non-fatal serial port read errors such as buffer overflows, frame errors, or parity errors may occur under certain circumstances. These are thrown as exceptions and can be caught by adding another loop on top of the one that checked port.readable. This works because as long as the error is non-fatal, a new ReadableStream is created automatically. Port if a fatal error occurs, such as a serial device being deleted. The readable becomes zero.

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 errors.}}Copy the code

If the serial device sends text back, you can pipe the port. It can be read via TextDecoderStream, as shown below. TextDecoderStream is a transform stream that grabs all of the Uint8Array blocks and converts them to strings.

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

// Listen for data from serial devices.
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);
}
Copy the code

Write a serial port

To send data to a serial device, pass the data to port.writable.getwriter ().write(). ReleaseLock () is called on port.writable.getwriter () to close the serial port 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();
Copy the code

TextEncoderStream piped to the port sends text to the device. Port.writable is shown below.

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

const writer = textEncoder.writable.getWriter();

await writer.write("hello");
Copy the code

Close a serial port

Port.close () closes the serial port if its readable and writable are unlocked, which means releaseLock() has been called for its respective read-write members.

await port.close();
Copy the code

However, when a loop is used to read data continuously from a serial device, port Readable will remain locked until an error is encountered. In this case, calling reader.cancel() forces reader.read() to be immediately resolved to {value: undefined, done: true}, allowing a loop to call reader.releaselock ().

// There is no transform stream.
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() was 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() = > {// The user clicks the button to close the serial port.
  keepReading = false;
  // Forces reader.read() to parse immediately and subsequently
  Call reader.releaselock () in the loop example above.
  reader.cancel();
  await closedPromise;
});
Copy the code

Closing the serial port is more complicated when using conversion streams such as TextDecoderStream and TextTenCoderStream. Call reader.cancel() as before. Then call writer.close() and port.close(). This propagates the error to the underlying serial port via the conversion stream. Because error propagation does not occur immediately, you need to use the readableStreamClosed and writableStreamClosed Promise you created earlier to detect when the port is. Readable and port. Writable unlocked. Canceling the reader causes the stream to abort; This is why you must catch and ignore the resulting errors.

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

// Listen for data from serial devices.
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();
Copy the code

Listen to connect and disconnect

If a serial port is provided by a USB device, the device can be connected or disconnected from the system. When a Web site is granted access to a serial port, it should monitor connection and disconnect events.

navigator.serial.addEventListener("connect".(event) = > {
  // TODO:Auto open event. Target or warning user port available.
});

navigator.serial.addEventListener("disconnect".(event) = > {
  // TODO: Remove |event.target| from the UI.
  // If the serial port is open, a stream error is also observed.
});
Copy the code

Prior to Chrome 89, connect and disconnect events triggered a custom SerialConnectionEvent object, and the affected SerialPort interface was available as a port property. You might want to use the event. The port | | event. The event target. Target to handle the transformation.

Processing the signal

After a serial port is connected, you can explicitly query and set the signals exposed through the serial port for device detection and traffic control. These signals are defined as Boolean values. For example, when the Data Terminal Ready (DTR) signal is switched, some devices (such as Arduino) enter programming mode.

Setting up output signals and getting input signals are done by calling port.setsignals () and port.getsignals (), respectively. See usage examples below.

// Turn off the serial interrupt signal.
await port.setSignals({ break: false });

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

// Turn off the send request (RTS) signal.
await port.setSignals({ requestToSend: false });
Copy the code
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}`);
Copy the code

Change the flow

When you receive data from a serial device, you don’t have to get it all at once. It can be grouped arbitrarily. For more information, see Flow API concepts.

To solve this problem, you can use some built-in conversion streams such as TextDecoderStream or create your own conversion stream, which allows you to parse the incoming stream and return parsed data. The conversion stream lies between the serial device and the read loop that uses the stream. It can apply any transformation before using the data. Think of it like an assembly line: as a widget runs along the assembly line, each step along the assembly line modifies the widget so that by the time it reaches its final destination, it is a fully functional widget.

For example, consider how to create a transformation flow class that uses streams and groups them based on newlines. Each time the stream receives new data, its transform() method is called. It can either queue data or save it for later use. The flush() method is called when the stream is closed, which processes any unprocessed data.

To use the transform flow class, you need to pipe in a stream. In the third code example reading from the serial port, the raw input stream is only transmitted through the TextDecoderStream pipe, so we need to call Pipe Through () to transfer it through the new LineBreakTransformer pipe.

class LineBreakTransformer {
  constructor() {
    // A container to store stream data until a new row appears.
    this.chunks = "";
  }

  transform(chunk, controller) {
    // Append the new block to the existing block.
    this.chunks += chunk;
    // For each row segment, the parsed row is sent.
    const lines = this.chunks.split("\r\n");
    this.chunks = lines.pop();
    lines.forEach((line) = > controller.enqueue(line));
  }

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

For debugging serial device communication problems, use the tee() method of port. Port. Readable, which is used to separate streams to and from serial devices. The two streams created can be used independently, allowing you to print one to the console for inspection.

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

// You may want to update the UI with understandable data
// Log the incoming data to the JS console for inspection from devReadable.
Copy the code

Dev tips

inChromeIn the debuggingWeb Serial APIIt’s easy. There’s an internal page,Chrome://device-logYou can see all serial device-related events in one place.

Codelab

In the Google Developer code lab, you will use the Web Serial API to interact with the BBC Micro: Bit board to display images on its 5×5 LED matrix.

Browser support

The Web Serial API is available on all desktop platforms of Chrome 89 (Chrome OS, Linux, macOS, and Windows).

Polyfill

On Android, you can use the WebUSB API and the Serial API Polyfill to support USB-based serial ports. This putty is limited to hardware and platform access to the device through the WebUSB API, as it is not declared by the built-in device driver.

Security and privacy

The authors of the specification designed and implemented the Web Serial API using core principles that control access to powerful Web platform features, including user control, transparency, and ergonomics. The ability to use this API is primarily determined by a permission model that grants access to a single serial device at a time. In response to user prompts, the user must take active steps to select a particular serial device.

To understand the security tradeoffs, check out the security and privacy section of the Web Serial API interpreter.