WebSerial

现行标准 — 最后更新于

参与:
GitHub whatwg/serial (新建议题, 未解决的议题)
在 Matrix 上交流
提交记录:
GitHub whatwg/serial/commits
截至此次提交的快照
测试:
web-platform-tests serial/ (正在进行的工作)
翻译 (非规范性):
日本語
简体中文
한국어

摘要

该 WebSerial API 为网站提供了一种读写 串行设备的方式,操作通过脚本进行。这样的 API 可以连接 Web 与 物理世界,让文档能够与设备通信, 例如微控制器、3D 打印机和其他串行设备。此外, 还有一份配套的 说明 文档。

1. 扩展 Navigator 接口

[Exposed=Window, SecureContext]
partial interface Navigator {
  [SameObject] readonly attribute Serial serial;
};

1.1. serial 属性

获取时,serial 属性始终返回同一个 实例,即 Serial 对象。

2. 扩展 WorkerNavigator 接口

[Exposed=DedicatedWorker, SecureContext]
partial interface WorkerNavigator {
  [SameObject] readonly attribute Serial serial;
};

2.1. serial 属性

获取时,serial 属性始终返回同一个 实例,即 Serial 对象。

3. Serial 接口

[Exposed=(DedicatedWorker, Window), SecureContext]
interface Serial : EventTarget {
  attribute EventHandler onconnect;
  attribute EventHandler ondisconnect;
  Promise<sequence<SerialPort>> getPorts();
  [Exposed=Window] Promise<SerialPort> requestPort(optional SerialPortRequestOptions options = {});
};

3.1. requestPort() 方法

当用户首次访问站点时,该站点没有访问任何 串行设备的权限。站点必须先调用 requestPort()。 此调用 让浏览器有机会提示用户选择允许该站点 控制的设备。如果站点专为某个 始终通过 USB 连接的特定设备设计,则站点可以提供一个 筛选器,将用户可选择的设备限制为 兼容的设备。例如,为 Arduino 驱动的机器人编程的站点可以 指定如下设置,将可选端口集合限制为 USB 设备,且这些设备具有 Arduino 的 USB 供应商 ID,
const filter = { usbVendorId: 0x2341 };
const port = await navigator.serial.requestPort({ filters: [filter] });

另一方面,如果站点预期用于各种各样的 设备,或通过 USB 转串口转换器连接的设备,则可以 完全不指定筛选器,而依靠用户选择适当的设备,

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

请求用户选择端口需要向用户显示提示,因此 站点必须具有 瞬时激活,例如由用户 点击按钮触发。

<button id="connect">连接</button>
const connectButton = document.getElementById("connect");
connectButton.addEventListener('click', () => {
  try {
    const port = await navigator.serial.requestPort();
    // 继续连接到接在 |port| 上的设备。
  } catch (e) {
    // 提示已关闭,且未选择设备。
  }
});

用户可以选择不选取设备,在这种情况下,Promise 将 被拒绝,拒绝原因为 "NotFoundError" DOMException 且须由站点 处理。

该 requestPort() 方法的步骤如下:
  1. 令 promise 为 一个新的 promise。

  2. 如果 this 的 相关全局对象 的 关联的 Document 未被 允许使用 此 受策略控制的特性,其名称为 "serial",则 拒绝 promise,原因为 "SecurityError" DOMException 并返回 promise。

  3. 如果 相关全局对象(属于 this)不具有 瞬时激活,则 拒绝 promise,原因为 "SecurityError" DOMException 并返回 promise。

  4. 如果 options["filters"] 存在,则对于 每个 filter,位于 options["filters"] 中,运行 以下步骤:

    1. 如果 filter["bluetoothServiceClassId"] 存在:

      1. 如果 filter["usbVendorId"] 存在, 拒绝 promise,原因为 TypeError 并返回 promise。

      2. 如果 filter["usbProductId"] 存在, 拒绝 promise,原因为 TypeError 并返回 promise。

    2. 如果 filter["usbVendorId"] 不存在, 拒绝 promise,原因为 TypeError 并返回 promise。

      注意: 此检查实现了 以下组合规则: SerialPortFilter 不能为空,且如果 usbProductId 已指定,则 usbVendorId 必须 也被指定。

  5. 运行以下步骤,执行方式为 并行:

    1. 令 allPorts 为空的 列表。

    2. 对于每个 已向系统注册的蓝牙设备:

      1. 对于每个 BluetoothServiceUUID uuid,它受支持于该 设备:

        1. 如果 uuid 不是 被屏蔽的蓝牙 服务类 UUID:

    3. 对于每个 可用 的非蓝牙串口:

      1. 令 port 为一个 SerialPort 表示该端口。

      2. 追加 port 到 allPorts。

    4. 提示用户授予站点访问串口的权限,向用户显示 一个端口列表,其中包含 allPorts 中 匹配任意筛选器 的端口,筛选器位于 options["filters"] 中,若该成员存在;否则显示 allPorts。

    5. 如果用户未选择端口,则 将一个全局任务加入队列,在 相关全局对象(属于 this)上,使用 串口 任务源 来 拒绝 promise,原因为 "NotFoundError" DOMException 并中止这些步骤。

    6. 令 port 为一个 SerialPort 表示用户选择的端口。

    7. 将一个全局任务加入队列,在 相关全局对象(属于 this)上, 使用 串口任务源 来 解决 promise,值为 port。

  6. 返回 promise。

串口为 可用 的条件是: 它是有线串口,并且该端口 已实际连接到系统;或者它是无线串口,并且 承载该端口的无线设备已向系统注册。

3.1.1. SerialPortRequestOptions 字典

dictionary SerialPortRequestOptions {
  sequence<SerialPortFilter> filters;
  sequence<BluetoothServiceUUID> allowedBluetoothServiceClassIds;
};
filters 成员, 其 类型为 sequence<SerialPortFilter>
串口筛选器
allowedBluetoothServiceClassIds 成员, 类型为 sequence<BluetoothServiceUUID>
一个列表,包含 BluetoothServiceUUID 值,用于表示蓝牙服务 类 ID。具有自定义服务类 ID 的蓝牙端口将被排除在 向用户显示的端口列表之外,除非该服务类 ID 包含在此列表中。

3.1.2. SerialPortFilter 字典

dictionary SerialPortFilter {
  unsigned short usbVendorId;
  unsigned short usbProductId;
  BluetoothServiceUUID bluetoothServiceClassId;
};
usbVendorId 成员, 其 类型为 unsigned short
USB 供应商 ID
usbProductId 成员, 其 类型为 unsigned short
USB 产品 ID
bluetoothServiceClassId 成员, 类型为 BluetoothServiceUUID
蓝牙服务类 ID
串口 port 匹配筛选器 filter,如果以下步骤 返回 true:
  1. 令 info 为调用 port.getInfo() 的结果。

  2. 如果 filter["bluetoothServiceClassId"] 存在:

    1. 如果该串口不属于蓝牙设备,则返回 false。

    2. 如果 filter["bluetoothServiceClassId"] 等于 info["bluetoothServiceClassId"], 则返回 true。

    3. 否则,返回 false。

  3. 如果 filter["usbVendorId"] 不存在,则返回 true。

  4. 如果该串口不属于 USB 设备,则返回 false。

  5. 如果 info["usbVendorId"] 不等于 filter["usbVendorId"], 则返回 false。

  6. 如果 filter["usbProductId"] 不存在,则返回 true。

  7. 如果 info["usbProductId"] 不等于 filter["usbProductId"], 则返回 false。

  8. 否则,返回 true。

串口 port 匹配任意 筛选器,筛选器来自元素类型为 SerialPortFilter 的序列,条件是以下步骤返回 true:
  1. 对于序列中的每个 filter,运行以下子步骤:

    1. 如果 port 匹配筛选器 filter,则返回 true。

  2. 返回 false。

3.2. getPorts() 方法

如果串口由 USB 设备提供,则该设备可以连接到系统 或从系统断开连接。一旦站点具有访问端口的权限, 就可以接收这些事件,并查询已连接设备的集合, 其中包含它当前有权访问的设备。
// 页面加载时检查哪些端口可用。
document.addEventListener('DOMContentLoaded', async () => {
  let ports = await navigator.serial.getPorts();
  // 在用户界面中填入供用户选择的选项,或者
  // 自动连接到设备。
});

navigator.serial.addEventListener('connect', e => {
  // 将 |e.target| 添加到用户界面,或自动连接。
});

navigator.serial.addEventListener('disconnect', e => {
  // 从用户界面中移除 |e.target|。如果设备此前处于打开状态,
  // 也可以通过流错误观察到连接断开。
});
该 getPorts() 方法的步骤如下:
  1. 令 promise 为 一个新的 promise。

  2. 如果 this 的 相关全局对象 的 关联的 Document 未被 允许使用 此 受策略控制的特性,其名称为 "serial", 拒绝 promise,原因为 "SecurityError" DOMException 并返回 promise。

  3. 运行以下步骤,执行方式为 并行:

    1. 令 availablePorts 为一个序列,包含 可用 的串口,这些串口 已被用户允许由站点访问,此授权源于此前的一次调用, 调用的方法为 requestPort()。

    2. 令 ports 为一个序列,其中的 SerialPort 对象 表示 availablePorts 中的端口。

    3. 将一个全局任务加入队列,在 相关全局对象(属于 this)上, 使用 串口任务源 来 解决 promise,值为 ports。

  4. 返回 promise。

3.3. onconnect 属性

onconnect 是一个 事件处理器 IDL 属性,对应 connect 事件类型。

3.4. ondisconnect 属性

ondisconnect 是一个 事件处理器 IDL 属性,对应 disconnect 事件类型。

4. SerialPort 接口

[Exposed=(DedicatedWorker,Window), SecureContext]
interface SerialPort : EventTarget {
  attribute EventHandler onconnect;
  attribute EventHandler ondisconnect;
  readonly attribute boolean connected;
  readonly attribute ReadableStream? readable;
  readonly attribute WritableStream? writable;

  SerialPortInfo getInfo();

  Promise<undefined> open(SerialOptions options);
  Promise<undefined> setSignals(optional SerialOutputSignals signals = {});
  Promise<SerialInputSignals> getSignals();
  Promise<undefined> close();
  Promise<undefined> forget();
};

此接口上的方法通常异步完成,将工作排入 串口任务 源。

该 获取父级 算法,适用于 SerialPort 返回相同的 Serial 实例,该实例由 SerialPort 的 相关全局对象 的 Navigator 对象的 serial 获取器返回。

这些实例,类型为 SerialPort 创建时具有内部槽,具体描述见 下表:

内部槽 初始值 描述(非规范性)
[[state]] "closed" 跟踪以下对象的活动状态:SerialPort
[[bufferSize]] undefined 发送和接收时要缓冲的数据量
[[connected]] false 表示串口逻辑连接状态的标志
[[readable]] null 一个 ReadableStream 用于从端口接收数据
[[readFatal]] false 表示端口已遇到致命读取错误的标志
[[writable]] null 一个 WritableStream 用于向端口传输数据
[[writeFatal]] false 表示端口已遇到致命写入错误的标志
[[pendingClosePromise]] null 一个 Promise 用于等待 readable 和 writable 关闭

4.1. onconnect 属性

onconnect 是一个 事件处理器 IDL 属性,对应 connect 事件类型。

当某个串口已被用户允许由站点访问,此授权源于 此前的一次调用,调用的方法为 requestPort() 且该串口变为 逻辑上 已连接 时,运行 以下步骤:
  1. 令 port 为一个 SerialPort 表示该端口。

  2. 设置 port.[[connected]] 为 true。

  3. 触发一个事件,名称为 connect 目标为 port,并将其 bubbles 属性初始化为 true。

串口为 逻辑上已连接 的条件是:它是有线串口,并且 该端口已实际连接到系统;或者它是无线串口, 且系统与该无线设备之间存在活动连接(例如一个打开的 蓝牙 L2CAP 通道)。

4.2. ondisconnect 属性

ondisconnect 是一个 事件处理器 IDL 属性,对应 disconnect 事件类型。

当某个串口已被用户允许由站点访问,此授权源于 此前的一次调用,调用的方法为 requestPort() 且该串口不再 逻辑上 已连接 时,运行以下步骤:
  1. 令 port 为一个 SerialPort 表示该端口。

  2. 设置 port.[[connected]] 为 false。

  3. 触发一个事件,名称为 disconnect 目标为 port,并将其 bubbles 属性初始化为 true。

4.3. getInfo() 方法

该 getInfo() 方法的步骤如下:
  1. 令 info 为空的 有序映射。

  2. 如果该端口属于 USB 设备,则执行以下步骤:

    1. 设置 info["usbVendorId"] 为供应商 ID,该 ID 来自该 设备。

    2. 设置 info["usbProductId"] 为产品 ID,该 ID 来自该 设备。

  3. 如果该端口是蓝牙设备上的服务,则执行以下步骤:

    1. 设置 info["bluetoothServiceClassId"] 为服务 类 UUID,该 UUID 属于该蓝牙服务。

  4. 返回 info。

4.3.1. SerialPortInfo 字典

dictionary SerialPortInfo {
  unsigned short usbVendorId;
  unsigned short usbProductId;
  BluetoothServiceUUID bluetoothServiceClassId;
};
usbVendorId 成员, 类型为 unsigned short
如果该端口属于 USB 设备,则该成员为 16 位供应商 ID, 来自该设备。否则为 undefined。
usbProductId 成员, 类型为 unsigned short
如果该端口属于 USB 设备,则该成员为 16 位产品 ID,来自该设备。否则为 undefined。
bluetoothServiceClassId 成员, 类型为 BluetoothServiceUUID
如果该端口是蓝牙设备上的服务,则该成员为一个 BluetoothServiceUUID 包含服务类 UUID。否则,它 将为 undefined。

4.4. open() 方法

在通过串口通信之前,必须先打开串口。打开端口 允许站点指定必要的参数,以控制数据如何 发送和接收。开发者需要查阅 所连接设备的文档,以确定适当的参数。
await port.open({ baudRate: /* 选择波特率 */ });

当 open() 完成后,就可以访问 readable 和 writable 属性,以获取 ReadableStream 和 WritableStream 实例,用于接收数据,数据来自 已连接的设备,也用于向该设备发送数据。

该 open() 方法的步骤如下:
  1. 令 promise 为 一个新的 promise。

  2. 如果 this.[[state]] 不是 "closed",则拒绝 promise 原因为 "InvalidStateError" DOMException 并返回 promise。

  3. 如果 options["baudRate"] 为 0,则拒绝 promise,原因为 TypeError 并返回 promise。

  4. 如果 options["dataBits"] 既不是 7 也不是 8,则拒绝 promise 原因为 TypeError 并返回 promise。

  5. 如果 options["stopBits"] 既不是 1 也不是 2,则拒绝 promise 原因为 TypeError 并返回 promise。

  6. 如果 options["bufferSize"] 为 0,则拒绝 promise,原因为 TypeError 并返回 promise。

  7. 可选地,如果 options["bufferSize"] 大于 实现所能支持的大小,则拒绝 promise,原因为 TypeError 并 返回 promise。

  8. 设置 this.[[state]] 为 "opening"。

  9. 执行以下步骤,执行方式为 并行。

    1. 调用操作系统打开串口,使用连接 参数(或其默认值),这些参数在 options 中指定。

    2. 如果此操作因任何原因失败,则 将一个全局任务加入队列,在 相关全局对象(属于 this)上,使用 串口 任务源 来 拒绝 promise,原因为 "NetworkError" DOMException 并 中止这些步骤。

    3. 设置 this.[[state]] 为 "opened"。

    4. 设置 this.[[bufferSize]] 为 options["bufferSize"]。

    5. 将一个全局任务加入队列,在 相关全局对象(属于 this)上, 使用 串口任务源 来 解决 promise,值为 undefined。

  10. 返回 promise。

4.4.1. SerialOptions 字典

dictionary SerialOptions {
  required [EnforceRange] unsigned long baudRate;
  [EnforceRange] octet dataBits = 8;
  [EnforceRange] octet stopBits = 1;
  ParityType parity = "none";
  [EnforceRange] unsigned long bufferSize = 255;
  FlowControlType flowControl = "none";
};
baudRate 成员, 类型为 unsigned long
一个非零正值,表示建立串行 通信时应使用的波特率。

注意: baudRate 是唯一的 必需 成员,位于此 字典中。虽然其他连接参数具有常见的默认值, 但开发者应认真考虑并查阅 打算连接的设备的文档,以确定正确的 值。虽然某些值很常见,但并不存在标准波特率。 要求提供此参数可以减少因本规范任意选择 默认值而造成混淆的可能性。

dataBits 成员, 类型为 octet,默认为 8
每帧的数据位数。为 7 或 8。
stopBits 成员, 类型为 octet,默认为 1
帧末尾的停止位数。为 1 或 2。
parity 成员, 类型为 ParityType,默认为 "none"
奇偶校验模式。
bufferSize 成员, 类型为 unsigned long,默认为 255
一个非零正值,表示读写缓冲区的大小, 这些缓冲区应被创建。
flowControl 成员, 类型为 FlowControlType,默认为 "none"
流控制模式。
4.4.1.1. ParityType 枚举
enum ParityType {
  "none",
  "even",
  "odd"
};
none
每个数据字均不发送奇偶校验位。
even
数据字加上奇偶校验位后满足偶校验。
odd
数据字加上奇偶校验位后满足奇校验。
4.4.1.2. FlowControlType 枚举
enum FlowControlType {
  "none",
  "hardware"
};
none
不启用流控制。
hardware
启用使用 RTS 和 CTS 信号的硬件流控制。

4.5. connected 属性

该 connected 获取器的步骤如下:
  1. 返回 this.[[connected]]。

4.6. readable 属性

从串口接收数据的应用程序通常使用嵌套的 两层循环,如下所示,
while (port.readable) {
  const reader = port.readable.getReader();
  try {
    while (true) {
      const { value, done } = await reader.read();
      if (done) {
        // |reader| 已被取消。
        break;
      }
      // 对 |value| 执行某些操作...
    }
  } catch (error) {
    // 处理 |error|...
  } finally {
    reader.releaseLock();
  }
}

内层循环将从端口读取数据块,直到遇到 错误,此时将执行 "catch" 块中的代码。 外层循环通过打开新的读取器,处理可恢复的错误, 例如奇偶校验失败。致命错误将导致 readable 随之 变为 null,并使循环结束。

只要串口保持打开,它就能持续产生数据,而 每个数据块所包含的数据量(数据块由 read() 返回)基本上是任意的,取决于 它被调用的时机。由设备及与其通信的代码 决定完整消息的构成。例如, 设备可能使用 ASCII 格式的文本与主机通信, 每条消息以换行符(或序列 "\r\n")结尾。一条管线 由 TransformStream 组成, 可用于自动将 Uint8Array 数据块(由 readable 提供)转换为 DOMString 字符串, 每个字符串包含一整行文本。

class LineBreakTransformer {
  constructor() {
    this.container = '';
  }

  transform(chunk, controller) {
    this.container += chunk;
    const lines = this.container.split('\r\n');
    this.container = lines.pop();
    lines.forEach(line => controller.enqueue(line));
  }

  flush(controller) {
    controller.enqueue(this.container);
  }
}

const decoder = new TextDecoderStream();
const streamClosed = port.readable.pipeTo(decoder.writable);
const lineReader = decoder.readable
    .pipeThrough(new TransformStream(new LineBreakTransformer()))
    .getReader();

与 示例 7 一样,管道链不能 仅使用 pipeThrough() 构建。必须使用 pipeTo(), 将第一个 TransformStream 连接到 SerialPort 以便能够 在想要关闭端口时等待管道链关闭。

lineReader.cancel();
await streamClosed;
await port.close();

编码消息边界的其他方式包括:在每条消息前加上 其长度,或等待规定的时长后再发送下一条 消息。实现一个 TransformStream 来处理这些类型的消息 边界,留给读者作为练习。

虽然 read() 方法是异步的, 不会阻塞执行,但在使用 async/await 语法的代码中,看起来可能 像是会阻塞。在这种情况下,实现超时机制可能有所帮助, 以便在一段时间内未收到数据时,允许代码继续 执行。下面的示例使用 releaseLock() 方法中断对 read() 的调用,在 计时器到期后进行。此操作不会关闭流,因此在 超时后收到的任何数据,仍可在稍后调用 getReader() 再次获取读取器后读取。

async function readWithTimeout(port, timeout) {
  const reader = port.readable.getReader();
  const timer = setTimeout(() => {
    reader.releaseLock();
  }, timeout);
  const result = await reader.read();
  clearTimeout(timer);
  reader.releaseLock();
  return result;
}

此特性(属于 releaseLock()) 添加于 whatwg/streams#1168 且直到最近才由浏览器实现。

该 readable 获取器的步骤如下:
  1. 如果 this.[[readable]] 不是 null,则返回 this.[[readable]]。

  2. 如果 this.[[state]] 不是 "opened",则返回 null。

  3. 如果 this.[[readFatal]] 为 true,则返回 null。

  4. 令 stream 为一个 新建的 ReadableStream。

  5. 令 pullAlgorithm 为以下步骤:

    1. 令 desiredSize 为 期望的 填充至高水位标记的大小,针对 this.[[readable]]。

    2. 如果 this.[[readable]] 的 当前 BYOB 请求视图 非空,则将 desiredSize 设置为 this.[[readable]] 的 当前 BYOB 请求 视图 的 字节长度。

    3. 令 promise 为 一个 新的 promise。

    4. 运行以下步骤,执行方式为 并行:

      1. 调用操作系统读取至多 desiredSize 个字节,来源为 该端口,将结果放入 字节序列 bytes。

        注意: this.[[state]] 变为 "forgotten" 时,可以 按照 the port was disconnected 的情况处理。

      2. 将一个全局任务加入队列,在 相关全局对象(属于 this)上,使用 串口 任务源 来运行以下 步骤:

        1. 如果未遇到错误,则:

          1. 如果 this.[[readable]] 的 当前 BYOB 请求视图 非空, 则 写入 bytes 到 this.[[readable]] 的 当前 BYOB 请求视图,并设置 view 为 this.[[readable]] 的 当前 BYOB 请求视图。

          2. 否则,将 view 设置为以下操作的结果: 创建 一个 Uint8Array 使用 bytes,在 this 的 相关 Realm 中。

          3. 入队 view 到 this.[[readable]]。

          4. 解决 promise,值为 undefined。

        2. 如果遇到缓冲区溢出情况,则调用 报错,作用于 this.[[readable]] 原因为 "BufferOverrunError" DOMException 并 调用以下步骤,以 处理关闭 可读流。

        3. 如果遇到中断状态,则调用 报错,作用于 this.[[readable]] 原因为 "BreakError" DOMException 并调用 以下步骤,以 处理关闭 可读流。

        4. 如果遇到帧错误,则调用 报错,作用于 this.[[readable]] 原因为 "FramingError" DOMException 并调用 以下步骤,以 处理关闭 可读流。

        5. 如果遇到奇偶校验错误,则调用 报错,作用于 this.[[readable]] 原因为 "ParityError" DOMException 并调用 以下步骤,以 处理关闭 可读流。

        6. 如果遇到操作系统错误,则调用 报错,作用于 this.[[readable]] 原因为 "UnknownError" DOMException 并调用以下步骤 以 处理关闭 可读流。

        7. 如果 the port was disconnected,则运行以下步骤:

          1. 设置 this.[[readFatal]] 为 true,

          2. 调用 报错,作用于 this.[[readable]] 原因为 "NetworkError" DOMException。

          3. 调用以下步骤,以 处理 关闭可读流。

    5. 返回 promise。

  6. 令 cancelAlgorithm 为以下步骤:

    1. 令 promise 为 一个 新的 promise。

    2. 运行以下步骤,执行方式为 并行。

      1. 调用操作系统,丢弃该端口的所有软件 和硬件接收缓冲区中的内容。

      2. 将一个全局任务加入队列,在 相关全局对象(属于 this)上,使用 串口 任务源 来运行以下 步骤:

        1. 调用以下步骤,以 处理关闭 可读流。

        2. 解决 promise,值为 undefined。

    3. 返回 promise。

  7. 设置并提供字节读取 支持 stream,其中 pullAlgorithm 设置 为 pullAlgorithm,cancelAlgorithm 设置为 cancelAlgorithm,并且 highWaterMark 设置 为 this.[[bufferSize]]。

  8. 设置 this.[[readable]] 为 stream。

  9. 返回 stream。

要 处理 关闭可读流,执行以下步骤:
  1. 设置 this.[[readable]] 为 null。

  2. 如果 this.[[writable]] 为 null,并且 this.[[pendingClosePromise]] 不是 null,则 解决 this.[[pendingClosePromise]] 值为 undefined。

4.7. writable 属性

要向端口写入单个数据块,可以使用一个 WritableStreamDefaultWriter 根据需要创建和释放。此 示例使用 TextEncoder 将一个 DOMString 编码为所需的 Uint8Array 以进行传输。
const encoder = new TextEncoder();
const writer = port.writable.getWriter();
await writer.write(encoder.encode("PING"));
writer.releaseLock();

写入较大的数据块时,允许端口施加背 压可能很重要,以免串行发送器在发送 应用程序生成的数据时落后太多。该 write() 方法返回一个 Promise 其 在数据写入后解决。虽然在 发送缓冲区中保留一些数据对于维持良好的吞吐量很重要,但等待此 Promise 解决后再生成过多的数据块,是一种良好实践,可以 避免过度缓冲。

该 writable 获取器的步骤如下:
  1. 如果 this.[[writable]] 不是 null,则返回 this.[[writable]]。

  2. 如果 this.[[state]] 不是 "opened",则返回 null。

  3. 如果 this.[[writeFatal]] 为 true,则返回 null。

  4. 令 stream 为一个 新建的 WritableStream。

  5. 令 signal 为 stream 的 信号。

  6. 令 writeAlgorithm 为以下步骤,给定 chunk:

    1. 令 promise 为 一个 新的 promise。

    2. 断言:signal 未 中止。

    3. 如果 chunk 无法 转换为 IDL 值,其 类型为 BufferSource, 则拒绝 promise,原因为 TypeError 并返回 promise。否则,将转换结果保存在 source 中。

    4. 获取缓冲区源的副本,缓冲区源为 source,并将结果保存在 bytes 中。

    5. 并行 运行以下步骤:

      1. 调用操作系统将 bytes 写入端口。 或者,存储此数据块以便后续合并。

        注意: 操作系统 可以在满足以下条件时从此操作返回: bytes 已排入传输队列,而不必等到它 已传输完毕。

        注意: this.[[state]] 变为 "forgotten" 时,可以 按照 the port was disconnected 的情况处理。

      2. 将一个全局任务加入队列,在 相关全局对象(属于 this)上,使用 串口 任务源 来运行以下 步骤:

        1. 如果数据块已成功写入,或已存储以便后续 合并,则 解决 promise,值为 undefined。

          注意: [STREAMS] 规定, writeAlgorithm 仅会 在以下 Promise 解决后被调用,该 promise 由此前的一次调用 此算法返回。为提高效率,实现 可以解决此 Promise 提前进行,以便 将多个等待中的数据块(位于 WritableStream 的 内部队列中)合并为对操作系统的单次请求。

        2. 如果遇到操作系统错误,则 拒绝 promise,原因为 "UnknownError" DOMException。

        3. 如果 the port was disconnected,则运行以下步骤:

          1. 设置 this.[[writeFatal]] 为 true。

          2. 拒绝 promise,原因为 "NetworkError" DOMException。

          3. 调用以下步骤,以 处理 关闭可写流。

        4. 如果 signal 已 中止,则 拒绝 promise 原因为 signal 的 中止原因。

    6. 返回 promise。

  7. 令 abortAlgorithm 为以下步骤:

    1. 令 promise 为 一个 新的 promise。

    2. 运行以下步骤,执行方式为 并行。

      1. 调用操作系统,丢弃该端口的所有软件 和硬件发送缓冲区中的内容。

      2. 将一个全局任务加入队列,在 相关全局对象(属于 this)上,使用 串口 任务源 来运行以下 步骤:

        1. 调用以下步骤,以 处理关闭 可写流。

        2. 解决 promise,值为 undefined。

    3. 返回 promise。

  8. 令 closeAlgorithm 为以下步骤:

    1. 令 promise 为 一个 新的 promise。

    2. 运行以下步骤,执行方式为 并行。

      1. 调用操作系统,刷新该端口的所有软件 和硬件发送缓冲区中的内容。

      2. 将一个全局任务加入队列,在 相关全局对象(属于 this)上,使用 串口 任务源 来运行以下 步骤:

        1. 调用以下步骤,以 处理关闭 可写流。

        2. 如果 signal 已 中止,则 拒绝 promise 原因为 signal 的 中止原因。

        3. 否则,解决 promise,值为 undefined。

    3. 返回 promise。

  9. 设置 stream,其中 writeAlgorithm 设置为 writeAlgorithm, abortAlgorithm 设置为 abortAlgorithm, closeAlgorithm 设置为 closeAlgorithm, highWaterMark 设置为 this.[[bufferSize]], 并且 sizeAlgorithm 设置为按字节计数的 大小 算法。

  10. 添加 以下中止步骤到 signal:

    1. 使任何调用操作系统向端口写入数据的操作 尽快返回,无论已经写入了多少数据。

  11. 设置 this.[[writable]] 为 stream。

  12. 返回 stream。

要 处理 关闭可写流,执行以下步骤:
  1. 设置 this.[[writable]] 为 null。

  2. 如果 this.[[readable]] 为 null,并且 this.[[pendingClosePromise]] 不是 null,则 解决 this.[[pendingClosePromise]] 值为 undefined。

4.8. setSignals() 方法

串口包含许多用于设备检测和 流控制的附加信号,可以显式查询和设置这些信号。例如, 为某些微控制器编程时,首先需要进入“编程” 模式,方式是切换“数据终端就绪”(或 DTR)信号。
await port.setSignals({ dataTerminalReady: false });
await new Promise(resolve => setTimeout(resolve, 200));
await port.setSignals({ dataTerminalReady: true });
该 setSignals() 方法的步骤如下:
  1. 令 promise 为 一个新的 promise。

  2. 如果 this.[[state]] 不是 "opened",则拒绝 promise 原因为 "InvalidStateError" DOMException 并返回 promise。

  3. 如果 signals 的所有指定成员均不存在,则拒绝 promise,原因为 TypeError 并返回 promise。

  4. 执行以下步骤,执行方式为 并行:

    注意: 理想情况下,指定于 signals 中的更改应以原子方式应用, 但 POSIX 和 Windows API 均不支持这一点,而用户 代理将使用这些 API 来实现这些步骤。因此,下面给出的顺序 很可能会被应用程序依赖。

    1. 如果 signals["dataTerminalReady"] 存在, 则调用操作系统将信号置为有效(若为 true)或无效(若为 false),该信号是串口上的“数据终端就绪”或“DTR”信号。

    2. 如果 signals["requestToSend"] 存在,则调用 操作系统将信号置为有效(若为 true)或无效(若为 false),该信号是串口上的“请求发送”或“RTS”信号。

    3. 如果 signals["break"] 存在,则调用 操作系统将信号置为有效(若为 true)或无效(若为 false), 该信号是串口上的“中断”信号。

      注意: “中断”信号 通常作为带内信号实现, 通过将发送线路保持在“标记”电压来实现,因此只要 该信号保持有效,就会阻止数据传输。

    4. 如果操作系统无法更改这些 信号中任意一个的状态,无论原因如何,则 将一个全局任务加入队列,在 相关全局对象(属于 this)上,使用 串口 任务源 来拒绝 promise,原因为 "NetworkError" DOMException。

    5. 将一个全局任务加入队列,在 相关全局对象(属于 this)上, 使用 串口任务源 来 解决 promise,值为 undefined。

  5. 返回 promise。

4.8.1. SerialOutputSignals 字典

dictionary SerialOutputSignals {
  boolean dataTerminalReady;
  boolean requestToSend;
  boolean break;
};
dataTerminalReady, 类型为 boolean
数据终端就绪 (DTR)
requestToSend, 其 类型为 boolean
请求发送 (RTS)
break, 类型为 boolean
中断

4.9. getSignals() 方法

该 getSignals() 方法的步骤如下:
  1. 令 promise 为 一个新的 promise。

  2. 如果 this.[[state]] 不是 "opened",则拒绝 promise 原因为 "InvalidStateError" DOMException 并返回 promise。

  3. 执行以下步骤,执行方式为 并行:

    1. 向操作系统查询控制信号的状态,这些信号 可能由连接到串口的设备置为有效。

    2. 如果操作系统无法确定这些信号的状态, 无论原因如何,则 将一个全局任务加入队列,在 相关全局对象(属于 this)上,使用 串口 任务源 来拒绝 promise,原因为 "NetworkError" DOMException 并中止这些 步骤。

    3. 令 dataCarrierDetect 为 true,如果“数据载波检测”或 “DCD” 信号已被设备置为有效;否则为 false。

    4. 令 clearToSend 为 true,如果“允许发送”或“CTS”信号 已 被设备置为有效;否则为 false。

    5. 令 ringIndicator 为 true,如果“振铃指示”或“RI”信号 已 被设备置为有效;否则为 false。

    6. 令 dataSetReady 为 true,如果“数据设备就绪”或“DSR”信号 已 被设备置为有效;否则为 false。

    7. 令 signals 为 有序 映射 «[ "dataCarrierDetect" → dataCarrierDetect, "clearToSend" → clearToSend, "ringIndicator" → ringIndicator, "dataSetReady" → dataSetReady ]»。

    8. 将一个全局任务加入队列,在 相关全局对象(属于 this)上, 使用 串口任务源 来 解决 promise,值为 signals。

  4. 返回 promise。

4.9.1. SerialInputSignals 字典

dictionary SerialInputSignals {
  required boolean dataCarrierDetect;
  required boolean clearToSend;
  required boolean ringIndicator;
  required boolean dataSetReady;
};
dataCarrierDetect 成员, 类型为 boolean
数据载波检测 (DCD)
clearToSend 成员, 其 类型为 boolean
允许发送 (CTS)
ringIndicator 成员, 类型为 boolean
振铃指示 (RI)
dataSetReady 成员, 类型为 boolean
数据设备就绪 (DSR)

4.10. close() 方法

不再需要与端口通信时,可以关闭端口, 并由系统释放相关资源。

调用 port.close() 会隐式调用 port.readable.cancel() 和 port.writable.abort() 以清除 任何已缓冲的数据。如果应用程序已调用 port.readable.getReader() 或 port.writable.getWriter() 则流处于 锁定状态,端口无法关闭。这迫使开发者决定如何 处理任何正在进行的读取或写入操作。例如,为了 确保在端口关闭之前所有已缓冲的数据都已传输, 应用程序必须等待 Promise 其返回自 writer.close()。

const encoder = new TextEncoder();
const writer = port.writable.getWriter();
writer.write(encoder.encode("A long message that will take..."));
await writer.close();
await port.close();

要丢弃任何尚未发送的数据,应用程序可以改为调用 writer.abort()。

如果一个 TransformStream 正通过管道连接到 port.writable 那么 等待此 Promise 其返回自 writer.close() 解决,是不够的。 应用程序必须等待管道链关闭,方式是等待 Promise 其返回自 pipeTo() 解决。

const encoder = new TextEncoderStream();
const writableStreamClosed = encoder.readable.pipeTo(port.writable);
const writer = encoder.writable.getWriter();
writer.write("A long message that will take...");
writer.close();
await writableStreamClosed;
await port.close();

如果正使用循环从端口读取数据块,如 示例 4 所示,则必须先退出循环,再 调用 port.close()。

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| 已被取消。
          break;
        }
        // 对 |value| 执行某些操作...
      }
    } catch (error) {
      // 处理 |error|...
    } finally {
      reader.releaseLock();
    }
  }

  await port.close();
}

const closed = readUntilClosed();

// 一段时间之后...
keepReading = false;
reader.cancel();
await closed;

调用 reader.cancel() 会使对 reader.read() 的调用立即返回,退出 内层循环,并调用 reader.releaseLock()。 随后,外层循环 退出,因为 keepReading 已设置为 false,而随着流 解除锁定,port.close() 就可以成功完成。

虽然也可以调用 port.close() 立即进行, 在等待此 Promise 解决后,其返回自 reader.cancel() 但更好的做法是将 对 port.close() 的调用作为 readUntilClosed() 的最后一步, 这样,当遇到致命错误,且 port.readable 变为 null 时,端口也会关闭。

该 close() 方法的步骤如下:
  1. 令 promise 为 一个新的 promise。

  2. 如果 this.[[state]] 不是 "opened",则拒绝 promise 原因为 "InvalidStateError" DOMException 并返回 promise。

  3. 令 cancelPromise 为调用 取消 的结果,作用于 this.[[readable]] 或者为 一个已解决的 promise,其值为 undefined,如果 this.[[readable]] 为 null。

  4. 令 abortPromise 为调用 中止 的结果,作用于 this.[[writable]] 或者为 一个已解决的 promise,其值为 undefined,如果 this.[[writable]] 为 null。

  5. 令 pendingClosePromise 为 一个新的 promise。

  6. 如果 this.[[readable]] 和 this.[[writable]] 均为 null,则 解决 pendingClosePromise,值为 undefined。

  7. 设置 this.[[pendingClosePromise]] 为 pendingClosePromise。

  8. 令 combinedPromise 为以下操作的结果:获取一个等待所有 promise 的 promise 使用 «cancelPromise, abortPromise, pendingClosePromise»。

  9. 设置 this.[[state]] 为 "closing"。

  10. 响应 combinedPromise。

  11. 返回 promise。

4.11. forget() 方法

可以主动撤销对某个串口的访问权限,该权限此前 由用户授予。
// 请求一个串口。
const port = await navigator.serial.requestPort();

// 稍后... 撤销对该串口的访问权限。
await port.forget();
该 forget() 方法的步骤如下:
  1. 如果用户代理无法执行此操作(例如权限由 管理员策略授予),则返回 一个已解决的 promise,其值为 undefined。

  2. 运行以下步骤,执行方式为 并行:

    1. 设置 this.[[state]] 为 "forgetting"。

    2. 移除 this,从系统上的串口序列中,这些串口 已被用户允许由站点访问,此授权源于此前的一次调用, 调用的方法为 requestPort()。

    3. 设置 this.[[state]] 为 "forgotten"。

    4. 将一个全局任务加入队列,在 相关全局对象(属于 this)上, 使用 串口任务源 来 解决 promise,值为 undefined。

  3. 返回 promise。

5. 屏蔽列表

本规范依赖一个屏蔽列表文件,位于 https://github.com/whatwg/serial 仓库中,用于限制网站可以访问的端口集合。

以下操作的结果:解析蓝牙服务类 ID 屏蔽列表,列表位于 URL url,是一个 列表,其元素为 UUID 值,用于表示自定义 服务 ID。

该 串行端口配置文件服务类 ID 是一个 BluetoothServiceUUID 其值为 "00001101-0000-1000-8000-00805f9b34fb"。

一个 BluetoothServiceUUID serviceUuid 是 被屏蔽的蓝牙服务 类 UUID,如果以下步骤返回 true:
  1. 令 uuid 为调用以下方法的结果: BluetoothUUID.getService() 使用 serviceUuid。

  2. 令 blocklist 为以下操作的结果:解析蓝牙服务 类 ID 屏蔽列表,列表位于 https://github.com/whatwg/serial/blob/main/bluetooth-service-blocklist.txt。

  3. 如果 blocklist 包含 uuid,则返回 true。

  4. 如果 uuid 是 串行端口配置文件服务类 ID, 则返回 false。

  5. 如果 uuid 以下列内容结尾:"-0000-1000-8000-00805f9b34fb", 则返回 true。

  6. 否则,返回 false。

6. 集成

6.1. 权限策略

本规范定义了一项特性,控制是否可以使用所暴露的方法, 这些方法由 serial 属性暴露,该属性位于 Navigator 对象上。

此特性的特性名称为 "serial"。

该 默认允许列表 对于此特性而言为 'self'。

7. 安全注意事项

本节为非规范性内容。

此 API 带来的安全风险类似于 [WEBBLUETOOTH] 和 [WEBUSB],因此 从这些 API 中获得的经验同样适用于此处。主要威胁包括:

缓解所有这些攻击的主要措施是 requestPort() 模式,它要求用户交互,并且每次仅支持授予访问 单个设备的权限。这可以防止无须用户操作的攻击,因为站点无法 枚举所有已连接的设备,以确定是否存在易受攻击的设备, 而必须主动告知用户它希望获得访问权限。 实现还可以提供视觉指示,表明站点当前 正在与设备通信,并提供控件,以便在任何 时间撤销该权限。

本规范要求站点在 安全上下文 中提供, 以防止基于网络的攻击者注入恶意代码。 这确保用户在作出权限 决策时看到的站点身份是准确的。本规范还要求顶层文档 通过 [PERMISSIONS-POLICY] 主动启用,才能允许跨源 iframe 使用此 API。与 [CSP3] 结合使用时,这些机制能够提供保护, 抵御恶意代码注入攻击。

剩余的风险是通过钓鱼 攻击利用已连接的设备,该攻击诱使用户授予恶意站点访问 设备的权限。这些攻击既可以利用设备按设计提供的 功能,也可以在设备上安装恶意固件,进而攻击 主机计算机。主机软件可能易受攻击,因为它 未正确验证来自已连接设备的输入。这一 领域的安全研究促使软件供应商将已连接的设备视为 不可信。

没有任何机制能够完全防止此类攻击,因为 从页面发送到设备的数据是不透明的字节序列。试图 阻止发送某种特定类型的数据,很可能会遇到变通手段, 这些手段来自仍然希望向其设备发送此类 数据的设备制造商。

用户代理可以实现额外的机制,以控制对设备的访问:

这些实现:[WEBBLUETOOTH] 和 [WEBUSB],已经试验过 这些缓解措施,但其有效性存在局限。首先, 很难界定设备是否可被利用。例如,此 API 将 允许站点向微控制器开发板上传固件。这是 此 API 的一个关键用例,因为这些设备在教育和 业余爱好者市场中很常见。这些开发板没有实现固件签名验证, 因此很容易被转变为恶意设备。这些开发板显然 可被利用,但不应被屏蔽。

此外,维护易受攻击设备的列表对于 USB 和 蓝牙很有效,因为这些协议定义了用于收集设备 元数据的带外机制。因此,即使此类设备 向主机呈现为虚拟串口,也能轻松识别其品牌和型号。然而, 还存在通用的 USB 转串口或蓝牙转串口适配器,以及具有“真实” 串口的系统,这些串口使用 DB-25、DE-9 或 RJ-45 连接器。对于这些端口,没有 可读取的元数据来确定连接到 端口的设备的身份,因此无法阻止对这些设备的访问。

8. 隐私注意事项

本节为非规范性内容。

串口和串行设备包含两类敏感信息。当 端口是 USB 或蓝牙设备时,存在一些标识符,例如供应商和 产品 ID(用于标识品牌和型号),以及序列号或 MAC 地址。串行设备本身也可能具有自己的标识符,该标识符 可以通过经由串口发送的命令获取。设备还可能存储 其他私人信息,这些信息可能具有识别性,也可能不具有。

为了管理设备权限,实现很可能会存储设备 标识符,例如 USB 供应商 ID、产品 ID 和序列号,存储于其用户 偏好设置文件中,用作稳定的设备标识符,这些设备是用户已 授予站点访问权限的设备。这些标识符不会直接与站点共享,并且会 在权限被撤销或站点数据被整体清除时删除。

页面获得访问权限后可以向设备发送命令,通过这些命令,页面 也可能访问其他任何敏感信息,这些信息存储于 设备中。基于 § 7 安全注意事项 中提到的原因,尝试阻止页面访问此信息既不切实际, 也 不可取。

实现应让用户完全控制 站点可以访问哪些设备,并且不应在没有用户交互的情况下授予设备访问权限。这正是 该 requestPort() 方法的意图。这样可以防止站点 静默枚举所有已连接的设备并从中收集数据。这 类似于文件选择器用户界面。站点并不了解文件系统,只能了解 用户选择的文件或目录。实现 可以在站点使用这些权限时通知用户,方式是在 标签页或地址栏中显示指示图标。

提供“隐私”或“无痕”浏览模式的实现应 确保用户常规配置中的权限不会延续到此类 会话中,并且在此会话中授予的权限不会在 会话结束后持久保存。实现在此类会话中授予访问 设备的权限时,可以警告用户,因为这类似于手动输入具有识别性的信息, 前面提到的设备标识符以及通过 与设备通信可获得的其他唯一属性,都可能被用来跨会话识别 用户。

如果用户不了解 授予设备访问权限如何打破 Web 安全模型中传统的 隔离边界,就可能对本 API 授予的能力感到意外。安全用户界面和文档 应说明,授予站点访问设备的权限可能使站点获得完整 控制权,控制该设备及其中包含的任何数据。

致谢

感谢 Anatol Ulrich, Chris Mumford, Clément Menard, Domenic Denicola, Dominique Hazael-Massieux, Florian Loitsch, Florian Scholz, Francis Gulotta, François Beaufort, Jack Hsieh, Keavon Chambers, Kenneth Rohde Christiansen, Marcos Cáceres, Matt Reynolds, Jesse Melhuish, Michael Kohler, Ms2ger, Rick Waldron, Sankha Narayan Guria, Simon Pieters, Suz Hinton, Travis Leithead, 以及 Vincent Scheib 的出色贡献。

本标准的作者是 Reilly Grant (Google, reillyg@google.com)。

知识产权

版权所有 © WHATWG (Apple, Google, Mozilla, Microsoft)。本作品依据 知识共享署名 4.0 国际许可协议 授权。对于其中被纳入源代码的部分, 源代码中的这些部分则依据 BSD 3 条款许可证 授权。

索引

术语,定义于本 规范

术语,定义于 参考文献

参考文献

规范性参考文献

[DOM]
Anne van Kesteren. DOM 标准. 现行标准。 URL: https://dom.spec.whatwg.org/
[HTML]
Anne van Kesteren; 等. HTML 标准. 现行标准。URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra 标准. 现行标准。URL: https://infra.spec.whatwg.org/
[PERMISSIONS-POLICY]
Ian Clelland; Ari Chivukula. 权限 策略. URL: https://w3c.github.io/webappsec-permissions-policy/
[STREAMS]
Adam Rice; 等. 流标准. 现行 标准。URL: https://streams.spec.whatwg.org/
[WEBBLUETOOTH]
Reilly Grant. WebBluetooth 标准. 现行 标准。URL: https://bluetooth.spec.whatwg.org/
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL 标准. 现行 标准。URL: https://webidl.spec.whatwg.org/

非规范性参考文献

[CSP3]
Mike West; Antonio Sartori. 内容安全策略 第 3 级. URL: https://w3c.github.io/webappsec-csp/
[WEBUSB]
Reilly Grant. WebUSB 标准. 现行标准。URL: https://usb.spec.whatwg.org/

IDL 索引

[Exposed=Window, SecureContext]
partial interface Navigator {
  [SameObject] readonly attribute Serial serial;
};

[Exposed=DedicatedWorker, SecureContext]
partial interface WorkerNavigator {
  [SameObject] readonly attribute Serial serial;
};

[Exposed=(DedicatedWorker, Window), SecureContext]
interface Serial : EventTarget {
  attribute EventHandler onconnect;
  attribute EventHandler ondisconnect;
  Promise<sequence<SerialPort>> getPorts();
  [Exposed=Window] Promise<SerialPort> requestPort(optional SerialPortRequestOptions options = {});
};

dictionary SerialPortRequestOptions {
  sequence<SerialPortFilter> filters;
  sequence<BluetoothServiceUUID> allowedBluetoothServiceClassIds;
};

dictionary SerialPortFilter {
  unsigned short usbVendorId;
  unsigned short usbProductId;
  BluetoothServiceUUID bluetoothServiceClassId;
};

[Exposed=(DedicatedWorker,Window), SecureContext]
interface SerialPort : EventTarget {
  attribute EventHandler onconnect;
  attribute EventHandler ondisconnect;
  readonly attribute boolean connected;
  readonly attribute ReadableStream? readable;
  readonly attribute WritableStream? writable;

  SerialPortInfo getInfo();

  Promise<undefined> open(SerialOptions options);
  Promise<undefined> setSignals(optional SerialOutputSignals signals = {});
  Promise<SerialInputSignals> getSignals();
  Promise<undefined> close();
  Promise<undefined> forget();
};

dictionary SerialPortInfo {
  unsigned short usbVendorId;
  unsigned short usbProductId;
  BluetoothServiceUUID bluetoothServiceClassId;
};

dictionary SerialOptions {
  required [EnforceRange] unsigned long baudRate;
  [EnforceRange] octet dataBits = 8;
  [EnforceRange] octet stopBits = 1;
  ParityType parity = "none";
  [EnforceRange] unsigned long bufferSize = 255;
  FlowControlType flowControl = "none";
};

enum ParityType {
  "none",
  "even",
  "odd"
};

enum FlowControlType {
  "none",
  "hardware"
};

dictionary SerialOutputSignals {
  boolean dataTerminalReady;
  boolean requestToSend;
  boolean break;
};

dictionary SerialInputSignals {
  required boolean dataCarrierDetect;
  required boolean clearToSend;
  required boolean ringIndicator;
  required boolean dataSetReady;
};

⚠MDN

Navigator/serial

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

Serial/getPorts

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

Serial/requestPort

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

Serial

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

SerialPort/close

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

SerialPort/connect_event

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

SerialPort/connect_event

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

SerialPort/disconnect_event

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

SerialPort/disconnect_event

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

SerialPort/forget

In only one current engine.

FirefoxNoneSafariNoneChrome103+
Opera?Edge103+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

SerialPort/getInfo

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

SerialPort/getSignals

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

SerialPort/open

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

SerialPort/readable

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

SerialPort/setSignals

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

SerialPort/writable

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

SerialPort

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

WorkerNavigator/serial

In only one current engine.

FirefoxNoneSafariNoneChrome89+
OperaNoneEdge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

Headers/Permissions-Policy/serial

In only one current engine.

FirefoxNoneSafariNoneChrome89+
Opera?Edge89+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?