对串行端口执行读写操作

借助 Web Serial API,网站可以与串行设备进行通信。

François Beaufort
François Beaufort

什么是 Web Serial API?

串行端口是一种双向通信接口,可用于发送和 逐个字节接收数据。

Web Serial API 为网站提供了一种 串行设备。串行设备通过 串行端口,或通过可移动 USB 和蓝牙设备连接 模拟串行端口

换言之,Web Serial API 通过 允许网站与微控制器等串行设备通信 和 3D 打印机

此 API 也是 WebUSB 的绝佳配套 API,因为操作系统需要 与一些串行端口通信 串行 API,而不是低级别 USB API。

建议的用例

在教育、业余爱好者和工业领域,用户连接外围设备 连接到他们的计算机这些设备通常由 微控制器(通过自定义软件使用的串行连接)。部分自定义 控制这些设备的软件是采用网络技术构建的:

在某些情况下,网站会通过代理与设备通信 用户手动安装的应用。而在其他情况下,应用 通过 Electron 等框架在封装应用中提供。 而在其他情况下,用户需要执行额外步骤,例如 通过 U 盘将已编译的应用复制到设备。

在所有这些情况下,通过向 Google 合作伙伴 网站与其所控制的设备之间的通信。

当前状态

步骤 状态
1. 创建铺垫消息 完成
2. 创建规范的初始草稿 完成
3. 收集反馈和对设计进行迭代 完成
4. 源试用 完成
5. 投放 完成

使用 Web Serial API

功能检测

如需检查 Web Serial API 是否受支持,请使用以下命令:

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

打开串行端口

Web Serial API 采用异步设计。这可以防止网站界面 在等待输入时阻塞,这一点很重要,因为串行数据可以 并需要一种收听方式

如需打开串行端口,请先访问 SerialPort 对象。为此,您可以 或者通过调用以下命令,提示用户选择单个串行端口: navigator.serial.requestPort(),以响应用户手势(例如触摸) 或点击鼠标,或从 navigator.serial.getPorts() 中选择一个会返回 网站已获访问权限的串行端口列表。

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

navigator.serial.requestPort() 函数可接受一个可选的对象字面量 定义了过滤器。它们用于匹配通过 通过强制使用 USB 供应商 (usbVendorId) 和可选 USB 产品提供的 USB 设备 标识符 (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();
网站上的串行端口提示的屏幕截图
选择 BBC micro:bit 的用户提示

调用 requestPort() 会提示用户选择设备,并返回 SerialPort 对象。有了 SerialPort 对象后,调用 port.open() 会打开串行端口。baudRate字典 member 指定通过串行线路发送数据的速度。它以 每秒比特数 (bps) 单位。请查看您设备的文档,了解 正确的值,因为如果您发送和接收的所有数据都为乱码, 指定不正确。对于某些模拟串行端口的 USB 和蓝牙设备 则该值可以安全地设置为任意值,因为 。

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

您也可以在打开串行端口时指定以下任一选项。这些 选项是可选的,且具有方便的默认值

  • dataBits:每帧的数据位数(7 或 8)。
  • stopBits:帧末尾的停止位数量(1 或 2)。
  • parity:对等模式("none""even""odd")。
  • bufferSize:应创建的读写缓冲区的大小 (必须小于 16MB)。
  • flowControl:流控制模式("none""hardware")。

从串行端口读取

Web Serial API 中的输入和输出流由 Streams API 处理。

建立串行端口连接后,readablewritableSerialPort 对象返回 ReadableStreamWritableStream。这些对象将用于从 接收数据并将数据发送到 串行设备。两者都使用 Uint8Array 实例进行数据传输。

当串行设备收到新数据时,port.readable.getReader().read() 异步返回两个属性:valuedone 布尔值。如果 done 为 true,串行端口已关闭或没有更多数据传入 位置调用 port.readable.getReader() 会创建一个读取器,并将 readable 锁定为 。当 readable 处于锁定状态时,串行端口无法关闭。

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

在某些情况下(例如 缓冲区溢出、分帧错误或奇偶校验错误。系统会按照 并且可以通过在上一个循环的基础上再添加一个循环来捕获这种异常 用于检查 port.readable。这样做是可行的,因为只要错误存在, 非严重错误,系统会自动创建一个新的 ReadableStream。如果发生严重错误 发生时(例如串行设备被移除),则 port.readable 会变为 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.
  }
}

如果串行设备发回了文本,您可以用管道port.readable TextDecoderStream(如下所示)。TextDecoderStream转换流 该函数会获取所有 Uint8Array 个数据块并将其转换为字符串。

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

使用“自带缓冲区”从数据流读取数据时,您可以控制内存分配方式读取器调用 port.readable.getReader({ mode: "byob" }) 以获取 ReadableStreamBYOBReader 接口,并在调用 read() 时提供您自己的 ArrayBuffer。请注意,在 Chrome 106 或更高版本中,Web Serial API 支持此功能。

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()...
  }
}

以下示例展示了如何从 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`.
}

以下示例说明了如何从串行端口读取特定数量的数据:

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

写入串行端口

要将数据发送到串行设备,请将数据传递给 port.writable.getWriter().write()。正在拨打 releaseLock() 必须使用 port.writable.getWriter() 才能稍后关闭串行端口。

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

通过传送到 port.writableTextEncoderStream 将文本发送到设备 如下所示。

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

const writer = textEncoder.writable.getWriter();

await writer.write("hello");

关闭串行端口

port.close() 会在其 readablewritable 成员的情况下关闭串行端口 已解锁,这表示releaseLock()已针对各自的 读者和作者。

await port.close();

不过,在使用循环从串行设备持续读取数据时, port.readable 将始终处于锁定状态,直到遇到错误。在本课中, 在这种情况下,调用 reader.cancel() 会强制 reader.read() 解析 { value: undefined, done: true },因此允许 来调用 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;
});

使用转换流时,关闭串行端口会更加复杂。像以前一样调用 reader.cancel()。 然后调用 writer.close()port.close()。这会将错误传播到 将转换流传输到底层串行端口。由于错误传播 不会立即发生,您需要使用 readableStreamClosed,并且 先前创建的 writableStreamClosed promise,用于检测 port.readable 何时 和 port.writable 已解锁。取消 reader 会导致 视频流将被中止因此您必须捕获并忽略所产生的错误。

// 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();

监听连接和断开连接

如果串行端口由 USB 设备提供,则可以连接该设备 或与系统断开连接。当网站被授权 访问串行端口时,它应该监控 connectdisconnect 事件。

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

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

处理信号

建立串行端口连接后,您可以明确查询和设置 用于设备检测和流控制的串行端口公开的信号。这些 定义为布尔值例如,Arduino 等一些设备 如果数据终端就绪 (DTR) 信号处于 已切换。

设置输出信号和获取输入信号分别由以下操作完成: 调用 port.setSignals()port.getSignals()。请参阅下面的用法示例。

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

转换流

从串行设备接收数据时,不一定会获得 一次性处理数据它可能会被任意分块。如需了解详情,请参阅 Streams API 概念

要解决此问题,您可以使用一些内置的转换流, TextDecoderStream 或创建自己的转换流,以便执行以下操作: 解析传入的流并返回解析后的数据。转换流位于 串行设备和使用数据流的读取循环之间。它可以 在使用数据之前应用任意转换。你可以把它想象成 汇编行:随着一个 widget 逐行,行中的每个步骤都会修改 这样,在它到达最终目的地时 可正常运行的 widget。

<ph type="x-smartling-placeholder">
</ph> 飞机工厂的照片
第二次世界大战布罗米奇城堡飞机工厂

例如,考虑如何创建使用 然后根据换行将其分块调用其 transform() 方法 自动发送提醒。它可以将数据加入队列 保存下来供日后使用当音频流关闭时,系统会调用 flush() 方法, 它会处理尚未处理的所有数据。

要使用转换流类,您需要将传入的流通过管道 。在第三个代码示例从串行端口读取下, 原始输入流只通过 TextDecoderStream 传递, 需要调用 pipeThrough() 来通过新的 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();

如需调试串行设备通信问题,请使用 tee() 方法 port.readable,用于拆分传入或传出串行设备的串流。两者 你可以单独使用创建的视频流 控制台进行检查。

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.

撤消对串行端口的访问权限

网站可以清理不再使用的串行端口的访问权限 有兴趣通过对 SerialPort 实例调用 forget() 来保留数据。对于 例如,对于在共享计算机上使用的教育 Web 应用,该应用具有许多 累积的大量用户生成权限会导致 用户体验。

// Voluntarily revoke access to this serial port.
await port.forget();

由于 Chrome 103 或更高版本支持 forget(),因此请检查此功能是否 支持以下各项:

if ("serial" in navigator && "forget" in SerialPort.prototype) {
  // forget() is supported.
}

开发提示

您可以通过内部网页轻松地在 Chrome 中调试 Web Serial API, about://device-log,您可以在一个集中查看所有串行设备相关事件 一个位置

<ph type="x-smartling-placeholder">
</ph> 用于调试 Web Serial API 的内部页面的屏幕截图。
Chrome 中用于调试 Web Serial API 的内部页面。

Codelab

Google Developers Codelab 中,您将使用 Web Serial API 与 BBC micro:bit 开发板在其 5x5 LED 矩阵上显示图像。

浏览器支持

Web Serial API 适用于所有桌面平台(ChromeOS、Linux、macOS 和 Windows)。

聚酯纤维

在 Android 上,可以使用 WebUSB API 来支持基于 USB 的串行端口 和 Serial API polyfill。此 polyfill 仅适用于硬件和 哪些平台的设备可通过 WebUSB API 进行访问(因为设备尚未 已被内置设备驱动程序声明。

安全和隐私设置

规范作者通过 控制对强大的网络平台功能的访问权限中定义的原则, 包括用户控制、透明度和人体工程学。使用 API 主要由权限模型控制,该模型仅授予对单个 API 的访问权限 同时支持串行设备为了响应用户提示,用户必须主动 选择特定串行设备的步骤。

如需了解安全性方面的权衡,请参阅安全隐私权 部分。

反馈

Chrome 团队非常希望了解您对 Web Serial API。

向我们介绍 API 设计

API 是否存在无法按预期运行的地方?或者,在那里 缺少实现想法所需的方法或属性?

Web Serial API GitHub 代码库上提交规范问题,或添加 对现有问题的想法

报告实现存在的问题

您在 Chrome 的实现过程中是否发现了错误?还是 与规范不同?

访问 https://new.crbug.com 提交 bug。务必尽可能多添加一些 提供重现错误的简单说明 组件设置为 Blink>SerialGlitch 非常适用于以下情况: 轻松快速的重现问题

表示支持

您打算使用 Web Serial API 吗?您的公开支持对 Chrome 很有帮助 团队确定各项功能的优先级,并向其他浏览器供应商展示 支持他们。

使用 # 标签向 @ChromiumDev 发送推文 #SerialAPI 并告诉我们您使用它的地点和方式。

实用链接

演示

致谢

感谢 Reilly GrantJoe Medley 对本文的评价。 飞机工厂照片,由伯明翰博物馆信托基金会Unsplash 网站上提供。