连接到不常见的 HID 设备

借助 WebHID API,网站可以访问替代的辅助键盘和非标准的游戏手柄。

François Beaufort
François Beaufort

有许多人机接口设备 (HID) 属于长尾产品,例如替代键盘或奇特的游戏手柄,这些设备太新、太旧或太不常见,系统的设备驱动程序无法访问它们。WebHID API 通过提供一种在 JavaScript 中实现设备专用逻辑的方法来解决此问题。

建议的用例

HID 设备可从人接受输入或向人提供输出。设备示例包括键盘、指控设备(鼠标、触摸屏等)和游戏手柄。借助 HID 协议,您可以在桌面设备上使用操作系统驱动程序来访问这些设备。网络平台依赖于这些驱动程序来支持 HID 设备。

当涉及替代辅助键盘(例如 Elgato Stream DeckJabra 头戴式耳机X-keys)和非标准游戏手柄支持时,无法访问不常见的 HID 设备会带来特别大的不便。专为桌面设备设计的游戏手柄通常使用 HID 来处理游戏手柄输入(按钮、操纵杆、扳机)和输出(LED、振动)。遗憾的是,游戏手柄输入和输出未得到充分标准化,并且网络浏览器通常需要针对特定设备使用自定义逻辑。这种做法不可持续,会导致对大量旧款和不常见设备的支持不佳。这还会导致浏览器依赖于特定设备行为中的怪癖。

术语

HID 包含两个基本概念:报告和报告描述符。 报告是设备与软件客户端之间交换的数据。报告描述符用于描述设备支持的数据的格式和含义。

HID(人机接口设备)是一种从人接受输入或向人提供输出的设备。它还指 HID 协议,该协议是一种用于在主机和设备之间实现双向通信的标准,旨在简化安装过程。HID 协议最初是为 USB 设备开发的,但此后已通过许多其他协议(包括蓝牙)实现。

应用和 HID 设备通过以下三种报告类型交换二进制数据:

报告类型 说明
输入报告 从设备发送到应用的数据(例如按下按钮)。
输出报告 从应用发送到设备的数据(例如,开启键盘背光的请求)。
功能报告 可在任意方向发送的数据。格式因设备而异。

报告描述符用于描述设备支持的报告的二进制格式。其结构是分层的,可以将报告分组为顶级集合中的不同集合。描述符的格式由 HID 规范定义。

HID 用途是一种数值,表示标准化的输入或输出。借助使用情况值,设备可以在其报告中描述设备的预期用途以及每个字段的用途。例如,为鼠标的左键定义一个。使用情况还会整理到使用情况页面中,以指明设备或报告的大致类别。

使用 WebHID API

功能检测

如需检查是否支持 WebHID API,请使用以下命令:

if ("hid" in navigator) {
  // The WebHID API is supported.
}

打开 HID 连接

WebHID API 是异步设计的,以防止网站界面在等待输入时阻塞。这一点非常重要,因为 HID 数据可以随时接收,因此需要有监听它的方法。

如需打开 HID 连接,请先访问 HIDDevice 对象。为此,您可以通过调用 navigator.hid.requestDevice() 提示用户选择设备,也可以从 navigator.hid.getDevices() 中选择设备,该方法会返回网站之前被授予访问权限的设备列表。

navigator.hid.requestDevice() 函数接受一个用于定义过滤条件的必需对象。这些 ID 用于匹配连接到 USB 供应商 ID (vendorId)、USB 产品 ID (productId)、用途页面值 (usagePage) 和用途值 (usage) 的任何设备。您可以从 USB ID 代码库HID 用途表文档中获取这些 ID。

此函数返回的多个 HIDDevice 对象代表同一实体设备上的多个 HID 接口。

// Filter on devices with the Nintendo Switch Joy-Con USB Vendor/Product IDs.
const filters = [
  {
    vendorId: 0x057e, // Nintendo Co., Ltd
    productId: 0x2006 // Joy-Con Left
  },
  {
    vendorId: 0x057e, // Nintendo Co., Ltd
    productId: 0x2007 // Joy-Con Right
  }
];

// Prompt user to select a Joy-Con device.
const [device] = await navigator.hid.requestDevice({ filters });
// Get all devices the user has previously granted the website access to.
const devices = await navigator.hid.getDevices();
网站上显示的 HID 设备提示的屏幕截图。
选择 Nintendo Switch Joy-Con 的用户提示。

您还可以使用 navigator.hid.requestDevice() 中的可选 exclusionFilters 键,从浏览器选择器中排除已知存在故障的某些设备。

// Request access to a device with vendor ID 0xABCD. The device must also have
// a collection with usage page Consumer (0x000C) and usage ID Consumer
// Control (0x0001). The device with product ID 0x1234 is malfunctioning.
const [device] = await navigator.hid.requestDevice({
  filters: [{ vendorId: 0xabcd, usagePage: 0x000c, usage: 0x0001 }],
  exclusionFilters: [{ vendorId: 0xabcd, productId: 0x1234 }],
});

HIDDevice 对象包含用于设备识别的 USB 供应商和产品标识符。其 collections 属性使用设备报告格式的分层描述进行初始化。

for (let collection of device.collections) {
  // An HID collection includes usage, usage page, reports, and subcollections.
  console.log(`Usage: ${collection.usage}`);
  console.log(`Usage page: ${collection.usagePage}`);

  for (let inputReport of collection.inputReports) {
    console.log(`Input report: ${inputReport.reportId}`);
    // Loop through inputReport.items
  }

  for (let outputReport of collection.outputReports) {
    console.log(`Output report: ${outputReport.reportId}`);
    // Loop through outputReport.items
  }

  for (let featureReport of collection.featureReports) {
    console.log(`Feature report: ${featureReport.reportId}`);
    // Loop through featureReport.items
  }

  // Loop through subcollections with collection.children
}

HIDDevice 设备默认返回为“关闭”状态,必须先通过调用 open() 将其打开,然后才能发送或接收数据。

// Wait for the HID connection to open before sending/receiving data.
await device.open();

接收输入报告

HID 连接建立后,您可以通过监听设备上的 "inputreport" 事件来处理传入的输入报告。这些事件包含 HID 数据(作为 DataView 对象 [data])、其所属的 HID 设备 (device) 以及与输入报告关联的 8 位报告 ID (reportId)。

红色和蓝色的任天堂 Switch 照片。
Nintendo Switch Joy-Con 设备。

继续使用上一个示例,以下代码展示了如何检测用户在 Joy-Con Right 设备上按下了哪个按钮,以便您在家中试用。

device.addEventListener("inputreport", event => {
  const { data, device, reportId } = event;

  // Handle only the Joy-Con Right device and a specific report ID.
  if (device.productId !== 0x2007 && reportId !== 0x3f) return;

  const value = data.getUint8(0);
  if (value === 0) return;

  const someButtons = { 1: "A", 2: "X", 4: "B", 8: "Y" };
  console.log(`User pressed button ${someButtons[value]}.`);
});

发送输出报告

如需向 HID 设备发送输出报告,请将与输出报告 (reportId) 关联的 8 位报告 ID 和字节作为 BufferSource (data) 传递给 device.sendReport()。报告发送后,返回的 promise 会解析。如果 HID 设备不使用报告 ID,请将 reportId 设置为 0。

以下示例适用于 Joy-Con 设备,展示了如何使用输出报告使其振动。

// First, send a command to enable vibration.
// Magical bytes come from https://github.com/mzyy94/joycon-toolweb
const enableVibrationData = [1, 0, 1, 64, 64, 0, 1, 64, 64, 0x48, 0x01];
await device.sendReport(0x01, new Uint8Array(enableVibrationData));

// Then, send a command to make the Joy-Con device rumble.
// Actual bytes are available in the sample below.
const rumbleData = [ /* ... */ ];
await device.sendReport(0x10, new Uint8Array(rumbleData));

发送和接收功能报告

功能报告是唯一可在两个方向传输的 HID 数据报告。它们允许 HID 设备和应用交换非标准化 HID 数据。与输入和输出报告不同,应用不会定期接收或发送功能报告。

黑色和银色笔记本电脑的照片。
笔记本电脑键盘

如需向 HID 设备发送功能报告,请将与功能报告关联的 8 位报告 ID (reportId) 和字节作为 BufferSource (data) 传递给 device.sendFeatureReport()。报告发送后,返回的 promise 会解析。如果 HID 设备不使用报告 ID,请将 reportId 设置为 0。

以下示例展示了如何使用功能报告,其中介绍了如何请求 Apple 键盘背光设备、打开该设备并使其闪烁。

const waitFor = duration => new Promise(r => setTimeout(r, duration));

// Prompt user to select an Apple Keyboard Backlight device.
const [device] = await navigator.hid.requestDevice({
  filters: [{ vendorId: 0x05ac, usage: 0x0f, usagePage: 0xff00 }]
});

// Wait for the HID connection to open.
await device.open();

// Blink!
const reportId = 1;
for (let i = 0; i < 10; i++) {
  // Turn off
  await device.sendFeatureReport(reportId, Uint32Array.from([0, 0]));
  await waitFor(100);
  // Turn on
  await device.sendFeatureReport(reportId, Uint32Array.from([512, 0]));
  await waitFor(100);
}

如需从 HID 设备接收功能报告,请将与功能报告 (reportId) 关联的 8 位报告 ID 传递给 device.receiveFeatureReport()。返回的 promise 会解析为包含地图项报告内容的 DataView 对象。如果 HID 设备不使用报告 ID,请将 reportId 设为 0。

// Request feature report.
const dataView = await device.receiveFeatureReport(/* reportId= */ 1);

// Read feature report contents with dataView.getInt8(), getUint8(), etc...

监听连接和断开连接

当网站获得访问 HID 设备的权限后,它可以通过监听 "connect""disconnect" 事件主动接收连接和断开连接事件。

navigator.hid.addEventListener("connect", event => {
  // Automatically open event.device or warn user a device is available.
});

navigator.hid.addEventListener("disconnect", event => {
  // Remove |event.device| from the UI.
});

撤消对 HID 设备的访问权限

网站可以通过对 HIDDevice 实例调用 forget(),清理对不再感兴趣保留的 HID 设备的访问权限。例如,对于在有许多设备共用的计算机上使用的教育类 Web 应用,大量累积的用户生成的权限会导致用户体验不佳。

对单个 HIDDevice 实例调用 forget() 会撤消对同一物理设备上所有 HID 接口的访问权限。

// Voluntarily revoke access to this HID device.
await device.forget();

由于 forget() 在 Chrome 100 或更高版本中提供,因此请通过以下方式检查设备是否支持此功能:

if ("hid" in navigator && "forget" in HIDDevice.prototype) {
  // forget() is supported.
}

开发者提示

借助内部页面 about://device-log,您可以轻松在 Chrome 中调试 HID。您可以在一个位置查看所有与 HID 和 USB 设备相关的事件。

用于调试 HID 的内部页面的屏幕截图。
Chrome 中的内部页面,用于调试 HID。

如需将 HID 设备信息转换为人类可读的格式,请查看 HID 浏览器。它用于将每个 HID 用法从用法值映射到名称。

在大多数 Linux 系统上,HID 设备默认映射为只读权限。如需允许 Chrome 打开 HID 设备,您需要添加新的 udev 规则。在 /etc/udev/rules.d/50-yourdevicename.rules 下创建一个文件,其中包含以下内容:

KERNEL=="hidraw*", ATTRS{idVendor}=="[yourdevicevendor]", MODE="0664", GROUP="plugdev"

例如,如果您的设备是 Nintendo Switch Joy-Con,则上述代码行中的 [yourdevicevendor]057e。您还可以添加 ATTRS{idProduct},以获得更具体的规则。确保您的 userplugdev 群组的成员。然后,只需重新连接设备即可。

浏览器支持

WebHID API 在 Chrome 89 中适用于所有桌面平台(ChromeOS、Linux、macOS 和 Windows)。

演示

web.dev/hid-examples 列出了一些 WebHID 演示。快去看看!

安全和隐私设置

规范作者使用控制对强大 Web 平台功能的访问中定义的核心原则(包括用户控制、透明度和人体工学)设计和实现了 WebHID API。能否使用此 API 主要取决于权限模型,该模型一次只能授予对单个 HID 设备的访问权限。在响应用户提示时,用户必须采取积极措施来选择特定的 HID 设备。

如需了解安全权衡,请参阅 WebHID 规范的安全和隐私注意事项部分。

此外,Chrome 还会检查每个顶级集合的使用情况,如果某个顶级集合的使用情况受保护(例如通用键盘、鼠标),则相应网站将无法发送和接收该集合中定义的任何报告。受保护用例的完整列表公开提供

请注意,Chrome 中也屏蔽了对安全性敏感的 HID 设备(例如用于更强身份验证的 FIDO HID 设备)。请参阅 USB 屏蔽名单HID 屏蔽名单文件。

反馈

Chrome 团队非常期待您分享对 WebHID API 的看法和使用体验。

请向我们说明 API 设计

API 是否存在未按预期运行的情况?或者,您是否缺少实现想法所需的方法或属性?

WebHID API GitHub 代码库中提交规范问题,或在现有问题中添加您的想法。

报告实现方面的问题

您是否发现了 Chrome 实现中的 bug?还是实现与规范不同?

请参阅如何提交 WebHID bug。请务必尽可能提供详细信息,提供有关重现 bug 的简单说明,并将组件设置为 Blink>HID故障非常适合分享快速简便的重现步骤。

表达支持

您是否打算使用 WebHID API?您的公开支持有助于 Chrome 团队确定功能的优先级,并向其他浏览器供应商表明支持这些功能的重要性。

使用 #WebHID 标签向 @ChromiumDev 发送推文,告诉我们您在哪里以及如何使用该功能。

实用链接

致谢

感谢 Matt ReynoldsJoe Medley 对本文的审核。 红色和蓝色的 Nintendo Switch 照片由 Sara Kurfeß 拍摄,黑色和银色的笔记本电脑照片由 Unsplash 上的 Athul Cyriac Ajay 拍摄。