连接到不常见的 HID 设备

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

François Beaufort
François Beaufort

有大量人机接口设备 (HID)(例如替代键盘或异型游戏手柄)过于新颖、过于陈旧或过于罕见,以至于系统设备驱动程序无法访问。WebHID API 通过提供一种在 JavaScript 中实现设备专用逻辑的方式来解决此问题。

建议的应用场景

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

在涉及替代的辅助键盘(例如 Elgato Stream DeckJabra 耳机X-keys)和非标准的游戏手柄支持时,无法访问非标准的 HID 设备尤其令人头疼。专为桌面设备设计的游戏手柄通常使用 HID 来处理游戏手柄输入(按钮、操纵杆、扳机)和输出(LED、振动)。遗憾的是,游戏手柄的输入和输出并未实现标准化,因此 Web 浏览器通常需要针对特定设备采用自定义逻辑。这种做法不可持续,会导致对大量旧版和不常见设备的支持不佳。它还会导致浏览器依赖于特定设备的行为怪癖。

术语

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() 函数接受一个定义过滤器的必需对象。这些值用于匹配通过 USB 供应商标识符 (vendorId)、USB 产品标识符 (productId)、用法页面值 (usagePage) 和用法值 (usage) 连接的任何设备。您可以从 USB ID 存储库HID 用途表文档中获取这些值。

此函数返回的多个 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]}.`);
});

请参阅 webhid-joycon-button 演示。

发送输出报告

如需向 HID 设备发送输出报告,请将与输出报告关联的 8 位报告 ID (reportId) 和字节作为 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));

请参阅 webhid-joycon-rumble 演示。

发送和接收功能报告

功能报告是唯一一种可以双向传输的 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);
}

请参阅 webhid-apple-keyboard-backlight 演示。

如需从 HID 设备接收功能报告,请将与功能报告关联的 8 位报告 ID (reportId) 传递给 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 演示。快去看看吧!

安全和隐私设置

规范作者在设计和实现 WebHID API 时,遵循了控制对强大的 Web 平台功能的访问权限中定义的核心原则,包括用户控制、透明度和人体工程学。使用此 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ß 拍摄,黑色和银色笔记本电脑照片由 Athul Cyriac Ajay 在 Unsplash 上拍摄。