# WebHID API

> navigator.hid.requestDevice() 配对一台人机接口设备，HIDDevice 负责收发输入、输出与特征报告。本页列出过滤器成员、每个异常，以及带回退分支的示例。

`navigator.hid.requestDevice()` 弹出一个由浏览器托管的选择器，列出已连接且匹配所传过滤器的人机接口设备（HID），并以用户授权的那些 `HIDDevice` 对象兑现。设备打开后，输入报告以 `inputreport` 事件送达，输出报告与特征报告以字节缓冲区发送，PWA 因此能驱动宏键盘、游戏手柄、条码扫描器，或任何通过 USB 或蓝牙讲 HID 协议的实验仪器。

支持仅限桌面 Chromium：Windows、macOS、Linux 与 ChromeOS 上的 Chrome 89 和 Edge 89 实现了 `HID`、`HIDDevice`、`requestDevice()` 与 `getDevices()`；Chrome 100 加入 `HIDDevice.forget()`，Chrome 131 在专用 worker 中暴露 `navigator.hid`（BCD `api.HID`、`api.HID.worker_support`）。Android 版 Chrome、Android WebView、Firefox 与 Safari 均无支持，WebKit 已提交正式的「反对」立场（[standards-positions #510](https://github.com/WebKit/standards-positions/issues/510)）。

## 语法

```js
navigator.hid.requestDevice(options)
navigator.hid.getDevices()

device.open()
device.close()
device.forget()
device.sendReport(reportId, data)
device.sendFeatureReport(reportId, data)
device.receiveFeatureReport(reportId)
```

`requestDevice()` 与 `getDevices()` 返回 `Promise<sequence<HIDDevice>>`；用户关闭选择器时 `requestDevice()` 以空序列兑现而不是拒绝。`open()`、`close()`、`forget()`、`sendReport()` 与 `sendFeatureReport()` 返回 `Promise<undefined>`；`receiveFeatureReport()` 返回 `Promise<DataView>`，其首字节可能是报告 ID。`navigator.hid` 标注为 `[SecureContext]`，`requestDevice()` 还要求在带瞬时用户激活的 `Window` 全局对象中调用。每个 `HIDDevice` 暴露 `opened`、`vendorId`、`productId`、`productName` 与 `collections`，并触发 `inputreport`（`HIDInputReportEvent`，带 `device`、`reportId`、`data`）；`navigator.hid` 对已授权设备触发 `connect` 与 `disconnect`（`HIDConnectionEvent`，带 `device`）。

## 参数

`requestDevice()` 接受一个 `HIDDeviceRequestOptions` 字典。

| 成员 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `filters` | `sequence<HIDDeviceFilter>` | 是 | 匹配任一过滤器的设备会被列出；`[]` 列出浏览器未封禁的全部已连接 HID 设备。 |
| `exclusionFilters` | `sequence<HIDDeviceFilter>` | 否 | 匹配其中任一项的设备即使匹配 `filters` 也会被隐藏。给出时必须非空。 |

`HIDDeviceFilter` 的每个成员都是可选的，但只有当 `productId` 与 `vendorId` 同时出现、`usage` 与 `usagePage` 同时出现时过滤器才有效。

| 成员 | 类型 | 说明 |
|---|---|---|
| `vendorId` | `unsigned long` | USB-IF 厂商 ID，例如 Logitech 为 `0x046d`。 |
| `productId` | `unsigned short` | 产品 ID；需要同时给出 `vendorId`。 |
| `usagePage` | `unsigned short` | 顶层集合的 HID usage page，例如 `0x0001` 为 Generic Desktop，`0xff00` 及以上为厂商自定义页。 |
| `usage` | `unsigned short` | `usagePage` 内的 usage ID；需要同时给出 `usagePage`。 |

`sendReport()`、`sendFeatureReport()` 与 `receiveFeatureReport()` 接受 `reportId`（`octet`，接口不使用报告 ID 时为 `0`）；两个发送方法还接受 `BufferSource` 类型的 `data`，即不含 ID 字节的报告载荷。

## 异常

出现下列情况之一时，`requestDevice()` 与 `getDevices()` 在任何选择器出现之前即拒绝。

| 异常 | 条件 |
|---|---|
| `SecurityError` | 文档不被允许使用 `hid` Permissions Policy 特性，或（仅 `requestDevice()`）调用时没有瞬时用户激活。 |
| `NotSupportedError` | `requestDevice()` 在非 `Window` 的全局对象（如 worker）中被调用。 |
| `TypeError` | `filters` 或 `exclusionFilters` 中有无效过滤器（只有 `productId` 没有 `vendorId`，或只有 `usage` 没有 `usagePage`），或 `exclusionFilters` 给出但为空。 |

`HIDDevice` 各方法的拒绝条件如下。

| 方法 | 异常 | 条件 |
|---|---|---|
| `open()` | `InvalidStateError` | 设备状态不是 `"closed"`（已打开、正在打开或已遗忘）。 |
| `open()` | `NetworkError` | 操作系统拒绝打开设备。 |
| `close()` | 无 | 正常兑现；任何挂起的 `sendReport()` 或特征报告 Promise 以 `AbortError` 拒绝。 |
| `forget()` | `InvalidStateError` | 设备已处于 `"forgotten"` 或 `"forgetting"` 状态。 |
| `sendReport()`、`sendFeatureReport()`、`receiveFeatureReport()` | `InvalidStateError` | 设备不处于 `"opened"` 状态。 |
| 同上三个 | `TypeError` | 接口使用报告 ID 却传了 `0`，或接口不使用报告 ID 却传了非零值。 |
| 同上三个 | `NotAllowedError` | 该报告在浏览器的封禁列表上（例如受保护的键盘或 FIDO 集合上的报告）。 |
| 同上三个 | `NetworkError` | 操作系统写入或读取报告失败。 |

被封禁的输入报告不会引发任何异常：浏览器只是从不为它触发 `inputreport`。

:::observed
在 Chrome 中于用户激活处理函数之外调用 `navigator.hid.requestDevice({ filters: [] })`，以 `SecurityError: Must be handling a user gesture to show a permission request.` 拒绝；对从未打开的设备调用 `device.sendReport()`，以 `InvalidStateError: The device must be opened first.` 拒绝；第二次 `device.open()` 以 `InvalidStateError: The device is already open.` 拒绝；`requestDevice({ filters: [], exclusionFilters: [] })` 抛出 `TypeError: Failed to execute 'requestDevice' on 'HID': 'exclusionFilters', if present, must contain at least one filter.`。这些字符串定义在 Chromium 的 [`hid.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/hid/hid.cc) 与 [`hid_device.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/hid/hid_device.cc)（chromium.googlesource.com）。
:::

## 示例

每个示例都先检查 `navigator.hid`，并说明在该属性不存在的 Firefox、Safari 或 Android 上页面怎么做。

### 配对厂商专用设备并打开它

按厂商 ID 过滤，选择器只列出该厂商的设备；取第一台授权设备并打开。调用必须放在 `click` 处理函数内。没有 WebHID 时，处理函数改为提示用户使用厂商的桌面工具。

```js
const LOGITECH = 0x046d;

document.querySelector('#pair').addEventListener('click', async () => {
  const note = document.querySelector('#hid-note');
  if (!('hid' in navigator)) {
    note.textContent = '此浏览器无法与 HID 设备通信，请用厂商工具进行配置。';
    return;
  }

  const [device] = await navigator.hid.requestDevice({ filters: [{ vendorId: LOGITECH }] });
  if (!device) return; // 选择器被关闭：返回空数组，不是错误

  if (!device.opened) await device.open();
  note.textContent = `已连接 ${device.productName}（${device.vendorId.toString(16)}:${device.productId.toString(16)}）`;
});
```

用户关闭选择器时 `requestDevice()` 以空数组兑现，所以解构后判断 `device` 就能覆盖取消的情况，不需要 `try`/`catch`。

### 读取自定义控制器的输入报告

`open()` 之后，设备发送的每份报告都以 `inputreport` 事件到达，`data` 是载荷的 `DataView`。下面的处理函数把一份两字节的厂商报告解码为旋钮位置和按键位图；布局来自设备的报告描述符，运行时可通过 `device.collections` 查看。

```js
function listenToDial(device, onChange) {
  if (!('hid' in navigator)) return () => {};

  const handler = (event) => {
    if (event.reportId !== 0x01) return;
    const position = event.data.getUint8(0);
    const buttons = event.data.getUint8(1);
    onChange({ position, pressed: (buttons & 0x01) !== 0 });
  };
  device.addEventListener('inputreport', handler);
  return () => device.removeEventListener('inputreport', handler);
}
```

`event.data` 不含报告 ID 字节；它单独作为 `event.reportId` 提供，接口不给报告编号的设备上该值为 `0`。

### 页面加载时恢复已授权的设备

授权在刷新后仍然有效，所以 `getDevices()` 无需弹窗也无需手势就能返回此前授权的设备。配合 `connect` 与 `disconnect` 事件，可以在页面打开期间重新打开被拔掉又插回的设备。

```js
async function restoreDevices(onDevice, onGone) {
  if (!('hid' in navigator)) return;

  for (const device of await navigator.hid.getDevices()) {
    await device.open();
    onDevice(device);
  }
  navigator.hid.addEventListener('connect', async ({ device }) => {
    await device.open();
    onDevice(device);
  });
  navigator.hid.addEventListener('disconnect', ({ device }) => onGone(device));
}
```

`forget()` 可以从脚本撤销授权，所以「移除设备」按钮不必把用户送去站点设置页就能清掉它。

## 另请参阅

- [WebUSB API](/zh/reference/capabilities/web-usb/)，用于非 HID 类的 USB 接口
- [Web Serial API](/zh/reference/capabilities/web-serial/)
- [Web Bluetooth API](/zh/reference/capabilities/web-bluetooth/)，用于以 GATT 而非 HID 通信的蓝牙低功耗设备
- [WebHID: requestDevice() method](https://hid.spec.whatwg.org/#dom-hid-requestdevice)（hid.spec.whatwg.org）
- [WebHID: HIDDevice open() method](https://hid.spec.whatwg.org/#dom-hiddevice-open)（hid.spec.whatwg.org）
- [WebKit standards position: WebHID](https://github.com/WebKit/standards-positions/issues/510)（github.com）
- [Connecting to uncommon HID devices](https://developer.chrome.com/docs/capabilities/hid)（developer.chrome.com）