# WebUSB API

> navigator.usb.requestDevice() 让页面配对没有类驱动的 USB 设备：过滤器、USBDevice 的传输方法、规范定义的每个异常，以及可运行示例。

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](https://github.com/mozilla/standards-positions/issues/100)）；Safari 没有实现；Android WebView 暴露 `navigator.usb` 但所有调用都会失败（[crbug.com/41441927](https://crbug.com/41441927)）。Chrome 70 起在专用 worker 中可用，Chrome 118 起在扩展的 service worker 中可用（BCD `api.USB.worker_support`）。

## 语法

```js
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`。

:::observed
Chrome 的提示字符串来自 [`usb.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/webusb/usb.cc) 与 [`usb_device.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/webusb/usb_device.cc)（chromium.googlesource.com）：手势之外调用 `requestDevice()` 以 `SecurityError: Must be handling a user gesture to show a permission request.` 拒绝；取消选择器以 `NotFoundError: No device selected.` 拒绝；只带 `productId` 的过滤器抛出 `TypeError: A filter containing a productId must also contain a vendorId.`；`open()` 之前调用 `claimInterface()` 以 `InvalidStateError: The device must be opened first.` 拒绝；声明 HID 或大容量存储接口以 `SecurityError: The requested interface implements a protected class.` 拒绝；声明失败（操作系统驱动仍占着接口，Windows 上没装 WinUSB 时很常见）以 `NetworkError: Unable to claim interface.` 拒绝。
:::

## 示例

每个示例都先检查 `navigator.usb`，并给出没有它时的分支。只有 `requestDevice()` 需要点击，之后的传输可以在任何后续代码中运行。

### 按厂商 ID 配对设备，带复用路径与回退

`getDevices()` 返回此前已授权的设备，所以选择器只在首次访问出现。没有该 API 时，函数返回可供界面展示的原因，例如指向桌面浏览器或原生工具的链接。

```js
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` 的情况。

### 声明接口并完成一次批量往返

找到第一个厂商自定义接口（`interfaceClass` 为 0xFF），声明它，定位批量端点，交换一个数据包。此处的 `SecurityError` 表示接口属于受保护类，WebUSB 不会交出；`NetworkError` 表示操作系统驱动仍占着它。

```js
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()` 即可恢复。

### 设备拔出时断开连接

已授权设备通过 `navigator.usb` 的 `disconnect` 事件通告移除。把 `event.device` 与正在使用的设备比较，避免无关硬件重置界面。

```js
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](/zh/reference/capabilities/web-serial/)，面向使用串口协议的设备
- [WebHID API](/zh/reference/capabilities/web-hid/)，面向 WebUSB 拒绝交出的 HID 类
- [Web Bluetooth API](/zh/reference/capabilities/web-bluetooth/)
- [WebUSB API: requestDevice() method](https://usb.spec.whatwg.org/#dom-usb-requestdevice)（whatwg.org）
- [WebUSB API: open() method](https://usb.spec.whatwg.org/#dom-usbdevice-open)（whatwg.org）
- [Access USB devices on the web](https://developer.chrome.com/docs/capabilities/usb)（developer.chrome.com）
- [Mozilla standards position: WebUSB](https://github.com/mozilla/standards-positions/issues/100)（github.com）
- [Chromium bug 41441927: WebUSB in Android WebView](https://crbug.com/41441927)（crbug.com）