连接到不常见的 HID 设备

WebHID API 可让网站访问替代的辅助键盘和独特的游戏手柄。

François Beaufort
François Beaufort

有一长串的人机接口设备 (HID)(如备用键盘或奇异游戏手柄)过于新、太旧或太不常见,以至于系统的设备驱动程序无法访问。WebHID API 提供了一种在 JavaScript 中实现设备专用逻辑的方法,来解决此问题。

建议的用例

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

当涉及到备用的辅助键盘(例如 Elgato Stream DeckJabra 耳机X 键)和支持异国情调的游戏手柄时,无法访问不常见的 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() 函数接受一个用于定义过滤条件的必需对象。这些参数可用于匹配连接了 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 设备提示的屏幕截图。
选择任天堂 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" 事件来处理传入的输入报告。这些事件包含作为 DataView 对象 (data) 的 HID 数据、它所属的 HID 设备 (device) 以及与输入报告关联的 8 位报告 ID (reportId)。

红色和蓝色任天堂切换照片。
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 设备,请将与输出报告关联的 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));

发送和接收功能报告

特征报告是唯一可以双向传输的 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 设备接收功能报告,请将与功能报告关联的 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 群组的成员。然后重新连接设备即可。

浏览器支持

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

样本歌曲

如需查看一些 WebHID 演示,请访问 web.dev/hid-examples。去看看吧!

安全和隐私设置

规范作者根据控制对强大 Web 平台功能的访问权限中定义的核心原则(包括用户控制、透明度和工效学设计)设计和实现了 WebHID API。能否使用该 API 主要受权限模型控制,该权限模型一次仅授予对单个 HID 设备的访问权限。为响应用户提示,用户必须主动选择特定的 HID 设备。

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

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

请注意,对安全性要求较高的 HID 设备(例如用于更严格的身份验证的 FIDO HID 设备)也会在 Chrome 中被屏蔽。请参阅 USB 屏蔽名单HID 屏蔽名单文件。

反馈

Chrome 团队希望了解您对 WebHID API 的想法和体验。

向我们介绍 API 设计

是否存在 API 无法正常运行的问题?或者,您是否需要缺少一些方法或属性来实现您的想法?

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

报告实施方面的问题

您是否发现了 Chrome 实现方面的错误?或者实现方式是否不同于规范?

请查看如何提交 WebHID 错误。请务必提供尽可能多的详细信息,提供重现 bug 的简单说明,并将组件设置为 Blink>HIDGlitch 非常适合用于快速轻松地分享重现的视频。

表达支持

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

请使用 # 标签 #WebHID@ChromiumDev 发送一条推文,告诉我们您使用该产品的位置和方式。

实用链接

致谢

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