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()
方法的步骤如下:
-
令 promise 为 一个新的 promise。
-
如果 this 的 相关全局对象 的 关联的 Document 未被 允许使用 此 受策略控制的特性,其名称为 "serial",则 拒绝 promise,原因为 "
SecurityError"DOMException并返回 promise。 -
如果 相关全局对象(属于 this)不具有 瞬时激活,则 拒绝 promise,原因为 "
SecurityError"DOMException并返回 promise。 -
如果 options["
filters"] 存在,则对于 每个 filter,位于 options["filters"] 中,运行 以下步骤:-
如果 filter["
bluetoothServiceClassId"] 存在:-
如果 filter["
usbVendorId"] 存在, 拒绝 promise,原因为TypeError并返回 promise。 -
如果 filter["
usbProductId"] 存在, 拒绝 promise,原因为TypeError并返回 promise。
-
-
如果 filter["
usbVendorId"] 不存在, 拒绝 promise,原因为TypeError并返回 promise。注意: 此检查实现了 以下组合规则:
SerialPortFilter不能为空,且如果usbProductId已指定,则usbVendorId必须 也被指定。
-
-
运行以下步骤,执行方式为 并行:
-
令 allPorts 为空的 列表。
-
对于每个 已向系统注册的蓝牙设备:
-
对于每个
BluetoothServiceUUIDuuid,它受支持于该 设备:-
如果 uuid 不是 被屏蔽的蓝牙 服务类 UUID:
-
如果 uuid 等于 串行 端口配置文件服务类 ID,或者
-
options["
allowedBluetoothServiceClassIds"] 存在且 包含 uuid:-
令 port 为一个
SerialPort表示该服务, 该服务位于蓝牙设备上。 -
追加 port 到 allPorts。
-
-
-
-
-
-
令 port 为一个
SerialPort表示该端口。 -
追加 port 到 allPorts。
-
-
提示用户授予站点访问串口的权限,向用户显示 一个端口列表,其中包含 allPorts 中 匹配任意筛选器 的端口,筛选器位于 options["
filters"] 中,若该成员存在;否则显示 allPorts。 -
如果用户未选择端口,则 将一个全局任务加入队列,在 相关全局对象(属于 this)上,使用 串口 任务源 来 拒绝 promise,原因为 "
NotFoundError"DOMException并中止这些步骤。 -
令 port 为一个
SerialPort表示用户选择的端口。 -
将一个全局任务加入队列,在 相关全局对象(属于 this)上, 使用 串口任务源 来 解决 promise,值为 port。
-
-
返回 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
true:
-
令 info 为调用 port.
getInfo()的结果。 -
如果 filter["
bluetoothServiceClassId"] 存在:-
如果该串口不属于蓝牙设备,则返回
false。 -
如果 filter["
bluetoothServiceClassId"] 等于 info["bluetoothServiceClassId"], 则返回true。 -
否则,返回
false。
-
-
如果 filter["
usbVendorId"] 不存在,则返回true。 -
如果该串口不属于 USB 设备,则返回
false。 -
如果 info["
usbVendorId"] 不等于 filter["usbVendorId"], 则返回false。 -
如果 filter["
usbProductId"] 不存在,则返回true。 -
如果 info["
usbProductId"] 不等于 filter["usbProductId"], 则返回false。 -
否则,返回
true。
SerialPortFilter
的序列,条件是以下步骤返回 true:
-
对于序列中的每个 filter,运行以下子步骤:
-
如果 port 匹配筛选器 filter,则返回
true。
-
-
返回
false。
3.2. getPorts() 方法
// 页面加载时检查哪些端口可用。 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()
方法的步骤如下:
-
令 promise 为 一个新的 promise。
-
如果 this 的 相关全局对象 的 关联的 Document 未被 允许使用 此 受策略控制的特性,其名称为
"serial", 拒绝 promise,原因为 "SecurityError"DOMException并返回 promise。 -
运行以下步骤,执行方式为 并行:
-
令 availablePorts 为一个序列,包含 可用 的串口,这些串口 已被用户允许由站点访问,此授权源于此前的一次调用, 调用的方法为
requestPort()。 -
令 ports 为一个序列,其中的
SerialPort对象 表示 availablePorts 中的端口。 -
将一个全局任务加入队列,在 相关全局对象(属于 this)上, 使用 串口任务源 来 解决 promise,值为 ports。
-
-
返回 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()
且该串口变为 逻辑上
已连接 时,运行
以下步骤:
-
令 port 为一个
SerialPort表示该端口。 -
设置 port.
[[connected]]为true。
串口为 逻辑上已连接 的条件是:它是有线串口,并且 该端口已实际连接到系统;或者它是无线串口, 且系统与该无线设备之间存在活动连接(例如一个打开的 蓝牙 L2CAP 通道)。
4.2. ondisconnect 属性
ondisconnect
是一个 事件处理器 IDL 属性,对应
disconnect
事件类型。
requestPort()
且该串口不再 逻辑上
已连接 时,运行以下步骤:
-
令 port 为一个
SerialPort表示该端口。 -
设置 port.
[[connected]]为false。 -
触发一个事件,名称为
disconnect目标为 port,并将其bubbles属性初始化为true。
4.3. getInfo() 方法
getInfo()
方法的步骤如下:
-
令 info 为空的 有序映射。
-
如果该端口属于 USB 设备,则执行以下步骤:
-
设置 info["
usbVendorId"] 为供应商 ID,该 ID 来自该 设备。 -
设置 info["
usbProductId"] 为产品 ID,该 ID 来自该 设备。
-
-
如果该端口是蓝牙设备上的服务,则执行以下步骤:
-
设置 info["
bluetoothServiceClassId"] 为服务 类 UUID,该 UUID 属于该蓝牙服务。
-
-
返回 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()
方法的步骤如下:
-
令 promise 为 一个新的 promise。
-
如果 this.
[[state]]不是"closed",则拒绝 promise 原因为 "InvalidStateError"DOMException并返回 promise。 -
如果 options["
baudRate"] 为 0,则拒绝 promise,原因为TypeError并返回 promise。 -
如果 options["
dataBits"] 既不是 7 也不是 8,则拒绝 promise 原因为TypeError并返回 promise。 -
如果 options["
stopBits"] 既不是 1 也不是 2,则拒绝 promise 原因为TypeError并返回 promise。 -
如果 options["
bufferSize"] 为 0,则拒绝 promise,原因为TypeError并返回 promise。 -
可选地,如果 options["
bufferSize"] 大于 实现所能支持的大小,则拒绝 promise,原因为TypeError并 返回 promise。 -
执行以下步骤,执行方式为 并行。
-
调用操作系统打开串口,使用连接 参数(或其默认值),这些参数在 options 中指定。
-
如果此操作因任何原因失败,则 将一个全局任务加入队列,在 相关全局对象(属于 this)上,使用 串口 任务源 来 拒绝 promise,原因为 "
NetworkError"DOMException并 中止这些步骤。 -
设置 this.
[[bufferSize]]为 options["bufferSize"]。 -
将一个全局任务加入队列,在 相关全局对象(属于 this)上, 使用 串口任务源 来 解决 promise,值为
undefined。
-
-
返回 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
获取器的步骤如下:
-
返回 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
获取器的步骤如下:
-
如果 this.
[[readable]]不是null,则返回 this.[[readable]]。 -
如果 this.
[[readFatal]]为true,则返回null。 -
令 stream 为一个 新建的
ReadableStream。 -
令 pullAlgorithm 为以下步骤:
-
令 desiredSize 为 期望的 填充至高水位标记的大小,针对 this.
[[readable]]。 -
如果 this.
[[readable]]的 当前 BYOB 请求视图 非空,则将 desiredSize 设置为 this.[[readable]]的 当前 BYOB 请求 视图 的 字节长度。 -
令 promise 为 一个 新的 promise。
-
运行以下步骤,执行方式为 并行:
-
调用操作系统读取至多 desiredSize 个字节,来源为 该端口,将结果放入 字节序列 bytes。
注意: this.
[[state]]变为"forgotten"时,可以 按照 the port was disconnected 的情况处理。 -
将一个全局任务加入队列,在 相关全局对象(属于 this)上,使用 串口 任务源 来运行以下 步骤:
-
如果未遇到错误,则:
-
如果 this.
[[readable]]的 当前 BYOB 请求视图 非空, 则 写入 bytes 到 this.[[readable]]的 当前 BYOB 请求视图,并设置 view 为 this.[[readable]]的 当前 BYOB 请求视图。 -
否则,将 view 设置为以下操作的结果: 创建 一个
Uint8Array使用 bytes,在 this 的 相关 Realm 中。 -
入队 view 到 this.
[[readable]]。 -
解决 promise,值为
undefined。
-
-
如果遇到缓冲区溢出情况,则调用 报错,作用于 this.
[[readable]]原因为 "BufferOverrunError"DOMException并 调用以下步骤,以 处理关闭 可读流。 -
如果遇到中断状态,则调用 报错,作用于 this.
[[readable]]原因为 "BreakError"DOMException并调用 以下步骤,以 处理关闭 可读流。 -
如果遇到帧错误,则调用 报错,作用于 this.
[[readable]]原因为 "FramingError"DOMException并调用 以下步骤,以 处理关闭 可读流。 -
如果遇到奇偶校验错误,则调用 报错,作用于 this.
[[readable]]原因为 "ParityError"DOMException并调用 以下步骤,以 处理关闭 可读流。 -
如果遇到操作系统错误,则调用 报错,作用于 this.
[[readable]]原因为 "UnknownError"DOMException并调用以下步骤 以 处理关闭 可读流。 -
如果 the port was disconnected,则运行以下步骤:
-
设置 this.
[[readFatal]]为true, -
调用 报错,作用于 this.
[[readable]]原因为 "NetworkError"DOMException。 -
调用以下步骤,以 处理 关闭可读流。
-
-
-
-
返回 promise。
-
-
令 cancelAlgorithm 为以下步骤:
-
令 promise 为 一个 新的 promise。
-
运行以下步骤,执行方式为 并行。
-
返回 promise。
-
-
设置并提供字节读取 支持 stream,其中 pullAlgorithm 设置 为 pullAlgorithm,cancelAlgorithm 设置为 cancelAlgorithm,并且 highWaterMark 设置 为 this.
[[bufferSize]]。 -
设置 this.
[[readable]]为 stream。 -
返回 stream。
-
设置 this.
[[readable]]为null。 -
如果 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
获取器的步骤如下:
-
如果 this.
[[writable]]不是null,则返回 this.[[writable]]。 -
如果 this.
[[writeFatal]]为true,则返回null。 -
令 stream 为一个 新建的
WritableStream。 -
令 signal 为 stream 的 信号。
-
令 writeAlgorithm 为以下步骤,给定 chunk:
-
令 promise 为 一个 新的 promise。
-
断言:signal 未 中止。
-
如果 chunk 无法 转换为 IDL 值,其 类型为
BufferSource, 则拒绝 promise,原因为TypeError并返回 promise。否则,将转换结果保存在 source 中。 -
获取缓冲区源的副本,缓冲区源为 source,并将结果保存在 bytes 中。
-
并行 运行以下步骤:
-
调用操作系统将 bytes 写入端口。 或者,存储此数据块以便后续合并。
注意: 操作系统 可以在满足以下条件时从此操作返回: bytes 已排入传输队列,而不必等到它 已传输完毕。
注意: this.
[[state]]变为"forgotten"时,可以 按照 the port was disconnected 的情况处理。 -
将一个全局任务加入队列,在 相关全局对象(属于 this)上,使用 串口 任务源 来运行以下 步骤:
-
如果数据块已成功写入,或已存储以便后续 合并,则 解决 promise,值为
undefined。注意: [STREAMS] 规定, writeAlgorithm 仅会 在以下
Promise解决后被调用,该 promise 由此前的一次调用 此算法返回。为提高效率,实现 可以解决此Promise提前进行,以便 将多个等待中的数据块(位于WritableStream的 内部队列中)合并为对操作系统的单次请求。 -
如果遇到操作系统错误,则 拒绝 promise,原因为 "
UnknownError"DOMException。 -
如果 the port was disconnected,则运行以下步骤:
-
设置 this.
[[writeFatal]]为true。 -
拒绝 promise,原因为 "
NetworkError"DOMException。 -
调用以下步骤,以 处理 关闭可写流。
-
-
-
-
返回 promise。
-
-
令 abortAlgorithm 为以下步骤:
-
令 promise 为 一个 新的 promise。
-
运行以下步骤,执行方式为 并行。
-
返回 promise。
-
-
令 closeAlgorithm 为以下步骤:
-
设置 stream,其中 writeAlgorithm 设置为 writeAlgorithm, abortAlgorithm 设置为 abortAlgorithm, closeAlgorithm 设置为 closeAlgorithm, highWaterMark 设置为 this.
[[bufferSize]], 并且 sizeAlgorithm 设置为按字节计数的 大小 算法。 -
添加 以下中止步骤到 signal:
-
使任何调用操作系统向端口写入数据的操作 尽快返回,无论已经写入了多少数据。
-
-
设置 this.
[[writable]]为 stream。 -
返回 stream。
-
设置 this.
[[writable]]为null。 -
如果 this.
[[readable]]为null,并且 this.[[pendingClosePromise]]不是null,则 解决 this.[[pendingClosePromise]]值为undefined。
4.8. setSignals() 方法
await port. setSignals({ dataTerminalReady: false }); await new Promise( resolve=> setTimeout( resolve, 200 )); await port. setSignals({ dataTerminalReady: true });
setSignals()
方法的步骤如下:
-
令 promise 为 一个新的 promise。
-
如果 this.
[[state]]不是"opened",则拒绝 promise 原因为 "InvalidStateError"DOMException并返回 promise。 -
如果 signals 的所有指定成员均不存在,则拒绝 promise,原因为
TypeError并返回 promise。 -
执行以下步骤,执行方式为 并行:
注意: 理想情况下,指定于 signals 中的更改应以原子方式应用, 但 POSIX 和 Windows API 均不支持这一点,而用户 代理将使用这些 API 来实现这些步骤。因此,下面给出的顺序 很可能会被应用程序依赖。
-
如果 signals["
dataTerminalReady"] 存在, 则调用操作系统将信号置为有效(若为true)或无效(若为false),该信号是串口上的“数据终端就绪”或“DTR”信号。 -
如果 signals["
requestToSend"] 存在,则调用 操作系统将信号置为有效(若为true)或无效(若为false),该信号是串口上的“请求发送”或“RTS”信号。 -
如果 signals["
break"] 存在,则调用 操作系统将信号置为有效(若为true)或无效(若为false), 该信号是串口上的“中断”信号。注意: “中断”信号 通常作为带内信号实现, 通过将发送线路保持在“标记”电压来实现,因此只要 该信号保持有效,就会阻止数据传输。
-
如果操作系统无法更改这些 信号中任意一个的状态,无论原因如何,则 将一个全局任务加入队列,在 相关全局对象(属于 this)上,使用 串口 任务源 来拒绝 promise,原因为 "
NetworkError"DOMException。 -
将一个全局任务加入队列,在 相关全局对象(属于 this)上, 使用 串口任务源 来 解决 promise,值为
undefined。
-
-
返回 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()
方法的步骤如下:
-
令 promise 为 一个新的 promise。
-
如果 this.
[[state]]不是"opened",则拒绝 promise 原因为 "InvalidStateError"DOMException并返回 promise。 -
执行以下步骤,执行方式为 并行:
-
向操作系统查询控制信号的状态,这些信号 可能由连接到串口的设备置为有效。
-
如果操作系统无法确定这些信号的状态, 无论原因如何,则 将一个全局任务加入队列,在 相关全局对象(属于 this)上,使用 串口 任务源 来拒绝 promise,原因为 "
NetworkError"DOMException并中止这些 步骤。 -
令 dataCarrierDetect 为
true,如果“数据载波检测”或 “DCD” 信号已被设备置为有效;否则为false。 -
令 clearToSend 为
true,如果“允许发送”或“CTS”信号 已 被设备置为有效;否则为false。 -
令 ringIndicator 为
true,如果“振铃指示”或“RI”信号 已 被设备置为有效;否则为false。 -
令 dataSetReady 为
true,如果“数据设备就绪”或“DSR”信号 已 被设备置为有效;否则为false。 -
令 signals 为 有序 映射 «[ "
dataCarrierDetect" → dataCarrierDetect, "clearToSend" → clearToSend, "ringIndicator" → ringIndicator, "dataSetReady" → dataSetReady ]»。 -
将一个全局任务加入队列,在 相关全局对象(属于 this)上, 使用 串口任务源 来 解决 promise,值为 signals。
-
-
返回 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()
方法的步骤如下:
-
令 promise 为 一个新的 promise。
-
如果 this.
[[state]]不是"opened",则拒绝 promise 原因为 "InvalidStateError"DOMException并返回 promise。 -
令 cancelPromise 为调用 取消 的结果,作用于 this.
[[readable]]或者为 一个已解决的 promise,其值为undefined,如果 this.[[readable]]为null。 -
令 abortPromise 为调用 中止 的结果,作用于 this.
[[writable]]或者为 一个已解决的 promise,其值为undefined,如果 this.[[writable]]为null。 -
令 pendingClosePromise 为 一个新的 promise。
-
如果 this.
[[readable]]和 this.[[writable]]均为null,则 解决 pendingClosePromise,值为undefined。 -
设置 this.
[[pendingClosePromise]]为 pendingClosePromise。 -
令 combinedPromise 为以下操作的结果:获取一个等待所有 promise 的 promise 使用 «cancelPromise, abortPromise, pendingClosePromise»。
-
响应 combinedPromise。
-
如果 combinedPromise 已兑现,则:
-
运行以下步骤,执行方式为 并行:
-
调用操作系统关闭串口,并释放 任何相关资源。
-
设置 this.
[[readFatal]]和 this.[[writeFatal]]为false。 -
设置 this.
[[pendingClosePromise]]为null。 -
将一个全局任务加入队列,在 相关全局对象(属于 this)上,使用 串口任务源 来 解决 promise,值为
undefined。
-
-
-
如果 combinedPromise 被拒绝,原因为 r,则:
-
设置 this.
[[pendingClosePromise]]为null。 -
将一个全局任务加入队列,在 相关全局对象(属于 this)上,使用 串口任务源 来 拒绝 promise,原因为 r。
-
-
-
返回 promise。
4.11. forget() 方法
// 请求一个串口。 const port= await navigator. serial. requestPort(); // 稍后... 撤销对该串口的访问权限。 await port. forget();
forget()
方法的步骤如下:
-
如果用户代理无法执行此操作(例如权限由 管理员策略授予),则返回 一个已解决的 promise,其值为
undefined。 -
运行以下步骤,执行方式为 并行:
-
返回 promise。
5. 屏蔽列表
本规范依赖一个屏蔽列表文件,位于 https://github.com/whatwg/serial 仓库中,用于限制网站可以访问的端口集合。
以下操作的结果:解析蓝牙服务类 ID
屏蔽列表,列表位于
URL url,是一个 列表,其元素为 UUID
值,用于表示自定义
服务 ID。
该 串行端口配置文件服务类 ID 是一个
BluetoothServiceUUID
其值为 "00001101-0000-1000-8000-00805f9b34fb"。
BluetoothServiceUUID
serviceUuid 是 被屏蔽的蓝牙服务
类 UUID,如果以下步骤返回 true:
-
令 uuid 为调用以下方法的结果:
BluetoothUUID.getService()使用 serviceUuid。 -
令 blocklist 为以下操作的结果:解析蓝牙服务 类 ID 屏蔽列表,列表位于 https://github.com/whatwg/serial/blob/main/bluetooth-service-blocklist.txt。
-
如果 blocklist 包含 uuid,则返回
true。 -
如果 uuid 是 串行端口配置文件服务类 ID, 则返回
false。 -
如果 uuid 以下列内容结尾:"
-0000-1000-8000-00805f9b34fb", 则返回true。 -
否则,返回
false。
6. 集成
6.1. 权限策略
本规范定义了一项特性,控制是否可以使用所暴露的方法,
这些方法由 serial
属性暴露,该属性位于 Navigator
对象上。
此特性的特性名称为 "serial"。
该 默认允许列表 对于此特性而言为
'self'。
7. 安全注意事项
本节为非规范性内容。
此 API 带来的安全风险类似于 [WEBBLUETOOTH] 和 [WEBUSB],因此 从这些 API 中获得的经验同样适用于此处。主要威胁包括:
-
恶意站点欺骗用户,使用户授予其访问 设备的权限,并利用设备的预期功能达到恶意目的。例如, 机器人造成物理损害。
-
恶意站点欺骗用户,使用户授予其访问 设备的权限,随后将自己的固件安装到设备中,以修改 设备的预期功能来达到恶意目的,或攻击 设备所连接的主机。例如,在其他 与设备通信的主机软件中触发缓冲区溢出。
-
恶意代码被注入已获准访问 设备的可信站点,并执行上述任意行为。例如,在线固件更新 工具遭到入侵,转而分发恶意固件。
-
攻击者说服用户将恶意设备连接到其 系统,该设备与恶意或遭到利用的站点串通,建立 基于 Web 的通道,以与攻击者通信。
缓解所有这些攻击的主要措施是 requestPort()
模式,它要求用户交互,并且每次仅支持授予访问
单个设备的权限。这可以防止无须用户操作的攻击,因为站点无法
枚举所有已连接的设备,以确定是否存在易受攻击的设备,
而必须主动告知用户它希望获得访问权限。
实现还可以提供视觉指示,表明站点当前
正在与设备通信,并提供控件,以便在任何
时间撤销该权限。
本规范要求站点在 安全上下文 中提供, 以防止基于网络的攻击者注入恶意代码。 这确保用户在作出权限 决策时看到的站点身份是准确的。本规范还要求顶层文档 通过 [PERMISSIONS-POLICY] 主动启用,才能允许跨源 iframe 使用此 API。与 [CSP3] 结合使用时,这些机制能够提供保护, 抵御恶意代码注入攻击。
剩余的风险是通过钓鱼 攻击利用已连接的设备,该攻击诱使用户授予恶意站点访问 设备的权限。这些攻击既可以利用设备按设计提供的 功能,也可以在设备上安装恶意固件,进而攻击 主机计算机。主机软件可能易受攻击,因为它 未正确验证来自已连接设备的输入。这一 领域的安全研究促使软件供应商将已连接的设备视为 不可信。
没有任何机制能够完全防止此类攻击,因为 从页面发送到设备的数据是不透明的字节序列。试图 阻止发送某种特定类型的数据,很可能会遇到变通手段, 这些手段来自仍然希望向其设备发送此类 数据的设备制造商。
用户代理可以实现额外的机制,以控制对设备的访问:
-
一种设置,阻止站点调用
requestPort()除非 该站点已被添加到显式允许列表中。系统管理员可以在其管理的全部设备上应用此设置, 使用企业策略控制功能。这些控制功能可以允许管理员 自动授予选定站点访问特定设备的权限,而不允许访问 其他设备。
-
可以随用户代理部署一份设备 ID 列表,包含已知可被利用的 硬件。对列出的设备的连接将被 阻止。实现可以使用其自动更新或实验 管理系统,动态部署此列表的更新,以阻止 正在进行的攻击。
这些实现:[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 条款许可证 授权。