Web Serial API 端口权限与生命周期
通过用户操作请求 Web Serial,区分权限与端口就绪状态,限制数据流,处理断开并保留安全备用路径。
BotBrowser Team
Web Serial 允许页面请求用户选择的串行端口,但选择器结果不等于已连接、可信设备,也不能证明设备归谁所有。BotBrowser 可以在受控 context 中验证页面自有的权限和备用分支,但不会制造实体串行设备、绕过浏览器授予操作系统权限,也不能证明硬件 I/O。应明确约束:由用户操作发起,只打开支持的端口,限制每条数据帧,断开时取消并关闭 streams,并让任务在没有串行硬件时仍可用。
TL;DR
navigator.serial.requestPort({ filters: [{ usbVendorId: 0x2341 }] })需要安全上下文和短暂的用户激活。- 选择的
SerialPort是来源范围内的能力,不证明端口已打开、连接、安全或协议兼容。 - 使用审查后的
port.open({ baudRate: 115200 }),并在改变状态前校验每条解码帧。 connect和disconnect是浏览器观察到的变化;取消 readers、释放 locks、关闭端口并保留草稿。- 保存
granted、unsupported、invalid-frame或disconnected,不保存序列号、原始缓冲区或推断身份。
目录
- 在使用时请求
- 区分权限与端口就绪
- 限制可读和可写 streams
- 协调 connect 与 disconnect
- 最小化元数据并保留备用路径
- 用决策表分类结果
- BotBrowser 的能力与限制
- 实践结论
在使用时请求
Web Serial 仅限安全上下文,requestPort() 应在用户操作期间运行。检测 navigator.serial 只代表方法存在,不会打开选择器;iframe 策略、受管浏览器、平台或操作系统都可能拒绝请求。
async function connectSerial() {
if (!window.isSecureContext || !navigator.serial) return showSerialState('unsupported');
setSerialState('requesting');
try {
const port = await navigator.serial.requestPort({
filters: [{ usbVendorId: 0x2341 }],
});
await openSelectedPort(port);
} catch (error) {
showSerialState(error.name === 'NotFoundError' ? 'cancelled' : 'denied');
}
}
connectButton.addEventListener('click', connectSerial);
不要在加载时打开选择器,也不要在取消后循环请求。保留草稿,等待新的明确操作。WebUSB 设备访问:权限、接口与隐私区分授权和会话就绪;WebHID 设备权限与隐私专门讨论 HID 报告生命周期;浏览器权限指纹说明权限状态不等于授权或身份。
区分权限与端口就绪
requestPort() 返回浏览器选择的端口,但它可能已断开、使用错误 baudRate 或运行不同协议。把选择、打开、读取、写入和关闭视为不同状态,并记录支持的 baudRate、dataBits、stopBits、parity 与 flowControl。配置错误可能产生看似合理但无效的字节,因此要在 decoder 后校验协议。
navigator.serial.getPorts() 不是完整硬件清单。重新连接应由用户操作触发并重新检查配置。Web Serial 规范与 MDN Web Serial API描述接口,但不证明身份、地点或实体结果。
限制可读和可写 streams
port.readable 与 port.writable 是异步 streams。使用最大长度、分隔符或长度字段;协议有 checksum 时校验,并为不完整帧设置超时。
const MAX_FRAME_BYTES = 128;
async function readSerialFrames(port) {
const reader = port.readable.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
if (value.byteLength > MAX_FRAME_BYTES) {
showSerialState('invalid-frame');
continue;
}
acceptBoundedFrame(value);
}
} finally {
reader.releaseLock();
}
}
不要记录每个字节,也不要把任意帧交给命令解释器。将错误分类为 invalid-length、invalid-checksum 或 timeout。writer.write() 成功只证明浏览器接受了字节,不证明硬件产生效果;关闭前先释放 writer 并取消 reader。
const serialOutcome = { state: 'fallback', rawIdentifiersRetained: false };
showSerialState(serialOutcome.state);
协调 connect 与 disconnect
connect 表示浏览器观察到端口出现,disconnect 表示观察到端口移除;两者都不说明原因,也不允许静默请求。断开时先让任务终止,再取消 reader、释放 writer、尽力执行 port.close()、移除 listeners、保留数据并显示重连按钮。不要自动重连或重发上一条命令。
使用 idle、requesting、selected、opening、opened、reading、invalid-frame、disconnected、cancelled 和 fallback 状态。端口回来后重新执行配置与 handshake,不要把 serialNumber 或 getInfo() 当成身份。
最小化元数据并保留备用路径
把浏览器权限、串行会话、协议同意和远程上传分开。本地读取不授权服务器上传。运行记录通常只需 unsupported、denied、cancelled、opened、invalid-frame、disconnected 或 fallback、时间和测试环境;避免 getInfo()、USB 标识符、原始缓冲区和长期历史。保留手动输入、文件导入、模拟值或静态配置。
用决策表分类结果
| 观察阶段 | 能证明什么 | 安全下一步 |
|---|---|---|
| API 缺失、不安全上下文或策略阻止 | 当前无法启动 Web Serial | 保留无串行任务并说明限制 |
| 选择器取消或权限拒绝 | 本次没有可用授权 | 保留草稿,等待明确操作 |
端口已选但 open() 拒绝 | 没有可用会话 | 显示配置、关闭并不扩大访问 |
| reader 报告无效帧 | 输入未通过协议 | 丢弃、分类并继续任务 |
disconnect 事件 | 浏览器观察到端口移除 | 取消 streams、关闭、保留数据并让用户重试 |
| 有效且受限的帧 | 页面接受一个值 | 只报告该值,不推断实体效果 |
BotBrowser 的能力与限制
BotBrowser 受控 context 可以验证连接按钮、能力检测、选择器结果、帧校验、断开清理和无串行备用路径。高级能力文档描述了可检查的浏览器能力。它不提供实体串行外设、不授予系统权限、不保证端口,也不证明字节改变了硬件。BotBrowser 可以验证 navigator.serial 的可见状态和备用路径,但不会验证真实设备的电气操作。
{
"api": "navigator.serial",
"permission": "denied",
"portState": "fallback",
"framesAccepted": 0,
"rawIdentifiersRetained": false,
"physicalDevice": "not-asserted"
}
改变浏览器、策略、权限、配置、模拟帧或断开条件后重跑 fixture。比较第一个可见状态差异,不要承诺通用硬件支持。
来源
实践结论
使用明确操作,区分权限、打开和 streams,校验每条帧,并在断开后关闭前取消 readers。让任务在没有串行硬件时仍完整可用,记录失败阶段而不是端口元数据。浏览器、策略、驱动、固件或权限变化后重跑 fixture;页面成功不代表实体硬件行为。
本文属于指南: Web 平台能力面:一致性复核地图