能力 · API
WebUSB API
发布于
WebUSB 把没有被操作系统类驱动接管的 USB 设备(开发板、烧录器、自制仪器)交给页面:navigator.usb.requestDevice() 弹出选择器,返回的 USBDevice 依次打开、选配置、声明接口,然后执行控制、批量、中断或同步传输。规范由 WHATWG 发布,WICG 地址会重定向过去。
Chrome 61 与 Edge 79 在桌面端和 Android 上实现了该 API,Samsung Internet 镜像 Chrome(BCD api.USB)。Firefox 没有实现,Mozilla 对 WebUSB 的标准立场为反对(mozilla/standards-positions#100);Safari 没有实现;Android WebView 暴露 navigator.usb 但所有调用都会失败(crbug.com/41441927)。Chrome 70 起在专用 worker 中可用,Chrome 118 起在扩展的 service worker 中可用(BCD api.USB.worker_support)。
navigator.usb.requestDevice(options)navigator.usb.getDevices()
device.open()device.selectConfiguration(configurationValue)device.claimInterface(interfaceNumber)device.selectAlternateInterface(interfaceNumber, alternateSetting)device.controlTransferIn(setup, length)device.controlTransferOut(setup, data)device.transferIn(endpointNumber, length)device.transferOut(endpointNumber, data)device.isochronousTransferIn(endpointNumber, packetLengths)device.isochronousTransferOut(endpointNumber, data, packetLengths)device.clearHalt(direction, endpointNumber)device.releaseInterface(interfaceNumber)device.reset()device.close()device.forget()每个方法都返回 Promise。requestDevice() 以一个 USBDevice 兑现,且必须在瞬时用户激活内调用;getDevices() 以本源已获授权且仍连着的设备兑现。输入传输以 USBInTransferResult(data 为 DataView,status 为 "ok"、"stall" 或 "babble")兑现;输出传输以 USBOutTransferResult(bytesWritten、status)兑现。navigator.usb 会为已授权设备触发 connect 与 disconnect 事件。所有接口都是 [SecureContext]。
requestDevice(options) 接受 USBDeviceRequestOptions 字典,其 filters 成员必填(空数组表示列出全部设备)。控制传输接受 USBControlTransferParameters 字典。
| 字典 | 成员 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
USBDeviceRequestOptions |
filters |
sequence<USBDeviceFilter> |
是 | 设备只要满足任一过滤器中所有已给出的成员即匹配。 |
USBDeviceRequestOptions |
exclusionFilters |
sequence<USBDeviceFilter> |
否 | 匹配其中任一项的设备不出现在选择器中(Chrome 117)。 |
USBDeviceFilter |
vendorId |
unsigned short |
否 | USB 厂商 ID。 |
USBDeviceFilter |
productId |
unsigned short |
否 | 产品 ID;只有与 vendorId 同时给出才有效。 |
USBDeviceFilter |
classCode |
octet |
否 | 设备或接口类。 |
USBDeviceFilter |
subclassCode |
octet |
否 | 需要 classCode。 |
USBDeviceFilter |
protocolCode |
octet |
否 | 需要 subclassCode。 |
USBDeviceFilter |
serialNumber |
DOMString |
否 | 精确匹配的序列号字符串。 |
USBControlTransferParameters |
requestType |
USBRequestType |
是 | "standard"、"class" 或 "vendor"。 |
USBControlTransferParameters |
recipient |
USBRecipient |
是 | "device"、"interface"、"endpoint" 或 "other"。 |
USBControlTransferParameters |
request |
octet |
是 | setup 包的 bRequest。 |
USBControlTransferParameters |
value |
unsigned short |
是 | wValue。 |
USBControlTransferParameters |
index |
unsigned short |
是 | wIndex;接收方为 "interface" 或 "endpoint" 时,低字节必须指向已声明的接口或其端点。 |
USBDevice 以只读方式暴露描述符树:configurations(USBConfiguration 数组),每项含 interfaces(USBInterface),每个接口含 alternates(USBAlternateInterface),其中列出 endpoints(USBEndpoint,有 endpointNumber、direction、type、packetSize)。vendorId、productId、manufacturerName、productName、serialNumber 与 opened 是属性。
| 方法 | 异常 | 条件 |
|---|---|---|
requestDevice() |
TypeError |
过滤器或排除过滤器无效:有 productId 没 vendorId、有 subclassCode 没 classCode、有 protocolCode 没 subclassCode。 |
requestDevice() |
SecurityError |
没有瞬时激活,或文档被禁用了 usb Permissions Policy 特性。 |
requestDevice() |
NotFoundError |
用户关闭了选择器,或没有已连接设备匹配过滤器。 |
任一 USBDevice 方法 |
NotFoundError |
设备已不再连接。 |
selectConfiguration()、claimInterface()、selectAlternateInterface()、clearHalt()、各传输 |
NotFoundError |
配置值、接口号、备用设置或端点不存在。 |
selectConfiguration()、claimInterface()、各传输 |
InvalidStateError |
设备未打开、未选择配置,或(传输与 selectAlternateInterface())接口未声明。 |
claimInterface() |
SecurityError |
接口属于受保护类(例如 HID 或大容量存储)且上下文不是无限制的。 |
transferIn()、transferOut() |
InvalidAccessError |
端点不是 "bulk" 或 "interrupt"。 |
isochronousTransferIn()、isochronousTransferOut() |
InvalidAccessError |
端点不是 "isochronous"。 |
open()、selectConfiguration()、claimInterface()、releaseInterface()、selectAlternateInterface()、clearHalt()、reset()、各传输 |
NetworkError |
平台操作或传输因 stall、babble 之外的原因失败。 |
| 进行中的传输 | AbortError |
被 close()、releaseInterface() 或 selectAlternateInterface() 中止。 |
规范还维护一份黑名单(按 idVendor、idProduct、bcdDevice 匹配),其中的设备直接不出现在选择器里,而不是以异常拒绝。Chromium 另外会在传输超时时给出 TimeoutError,在同步传输的包长度与缓冲区不符时给出 DataError,在端点号大于 15 时给出 IndexSizeError。
每个示例都先检查 navigator.usb,并给出没有它时的分支。只有 requestDevice() 需要点击,之后的传输可以在任何后续代码中运行。
按厂商 ID 配对设备,带复用路径与回退
Section titled “按厂商 ID 配对设备,带复用路径与回退”getDevices() 返回此前已授权的设备,所以选择器只在首次访问出现。没有该 API 时,函数返回可供界面展示的原因,例如指向桌面浏览器或原生工具的链接。
async function pair(vendorId) { if (!navigator.usb) { return { device: null, reason: '当前浏览器不提供 WebUSB。' }; }
const granted = await navigator.usb.getDevices(); let device = granted.find((d) => d.vendorId === vendorId);
if (!device) { try { device = await navigator.usb.requestDevice({ filters: [{ vendorId }] }); } catch (err) { if (err.name === 'NotFoundError') return { device: null, reason: '未选择设备。' }; throw err; } }
await device.open(); if (device.configuration === null) await device.selectConfiguration(1); return { device, reason: null };}多数操作系统在 open() 之后已经设好 device.configuration;显式的 selectConfiguration(1) 覆盖它为 null 的情况。
声明接口并完成一次批量往返
Section titled “声明接口并完成一次批量往返”找到第一个厂商自定义接口(interfaceClass 为 0xFF),声明它,定位批量端点,交换一个数据包。此处的 SecurityError 表示接口属于受保护类,WebUSB 不会交出;NetworkError 表示操作系统驱动仍占着它。
async function roundTrip(device, payload) { const iface = device.configuration.interfaces.find((i) => i.alternates.some((a) => a.interfaceClass === 0xff), ); if (!iface) throw new Error('该设备没有厂商自定义接口。');
await device.claimInterface(iface.interfaceNumber); const alt = iface.alternates[0]; const outEp = alt.endpoints.find((e) => e.direction === 'out' && e.type === 'bulk'); const inEp = alt.endpoints.find((e) => e.direction === 'in' && e.type === 'bulk');
const sent = await device.transferOut(outEp.endpointNumber, payload); if (sent.status === 'stall') await device.clearHalt('out', outEp.endpointNumber);
const reply = await device.transferIn(inEp.endpointNumber, inEp.packetSize); await device.releaseInterface(iface.interfaceNumber); return reply.status === 'ok' ? new Uint8Array(reply.data.buffer) : null;}"stall" 状态不是异常:设备暂停了端点,重试前按文档调用 clearHalt() 即可恢复。
设备拔出时断开连接
Section titled “设备拔出时断开连接”已授权设备通过 navigator.usb 的 disconnect 事件通告移除。把 event.device 与正在使用的设备比较,避免无关硬件重置界面。
function watch(device, onLost) { if (!navigator.usb) return () => {}; const handler = (event) => { if (event.device === device) onLost(); }; navigator.usb.addEventListener('disconnect', handler); return () => navigator.usb.removeEventListener('disconnect', handler);}断开时仍在进行的传输会以 NotFoundError 拒绝,所以 onLost 也是丢弃未决 Promise 队列的地方。
- Web Serial API,面向使用串口协议的设备
- WebHID API,面向 WebUSB 拒绝交出的 HID 类
- Web Bluetooth API
- WebUSB API: requestDevice() method(whatwg.org)
- WebUSB API: open() method(whatwg.org)
- Access USB devices on the web(developer.chrome.com)
- Mozilla standards position: WebUSB(github.com)
- Chromium bug 41441927: WebUSB in Android WebView(crbug.com)
规范
| 规范 | 状态 |
|---|---|
| WebUSB API | WICG 草案 |
| WebUSB API: requestDevice() method | WHATWG 现行标准 |
| WebUSB API: claimInterface() method | WHATWG 现行标准 |
| WebUSB API: blocklist | WHATWG 现行标准 |
- 图例
- 支持
- 部分支持
- 需开启标志
- 不支持
- 未知
| 浏览器 / 平台 | 支持 | 版本 | 置信度 | 来源 | 备注 |
|---|---|---|---|---|---|
| Chrome (Desktop) | 支持 | 61 | 高 | 来源 | — |
| Chrome (Android) | 支持 | 61 | 高 | 来源 | 1 |
| Edge (Desktop) | 支持 | 79 | 高 | 来源 | 2 |
| Firefox (Desktop) | 不支持 | — | 高 | 来源 | 3 |
| Firefox (Android) | 不支持 | — | 高 | 来源 | 45 |
| Safari (macOS) | 不支持 | — | 高 | 来源 | 6 |
| Safari (iOS) | 不支持 | — | 高 | 来源 | 78 |
| Samsung Internet | 支持 | 8.0 | 高 | 来源 | 9 |
| WebView (Android) | 不支持 | — | 高 | 来源 | 10 |
| Opera | 支持 | 48 | 高 | 来源 | — |
- 由 browser-compat-data 镜像自 Chrome 的数据推导。
- 由 browser-compat-data 镜像自 Chrome 的数据推导。
- browser-compat-data 未记录 Firefox 的支持。
- browser-compat-data 未记录 Firefox for Android 的支持。
- 由 browser-compat-data 镜像自 Firefox 的数据推导。
- browser-compat-data 未记录 Safari 的支持。
- browser-compat-data 未记录 iOS 版 Safari 的支持。
- 由 browser-compat-data 镜像自 Safari 的数据推导。
- 由 browser-compat-data 镜像自 Chrome Android 的数据推导。
- browser-compat-data 未记录 WebView Android 的支持。