连接到不常用的 HID 设备

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

François Beaufort
François Beaufort

发布时间:2020 年 9 月 15 日

Browser Support

  • Chrome: 89.
  • Edge: 89.
  • Firefox: not supported.
  • Safari: not supported.

Source

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

建议的应用场景

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

在涉及替代的辅助键盘(例如 Elgato Stream DeckJabra 耳机X-keys)和非标准游戏手柄支持时,无法访问非标准 HID 设备尤其令人头疼。专为桌面设备设计的游戏手柄通常使用 HID 来处理游戏手柄输入(按钮、操纵杆、扳机)和输出(LED、振动)。

遗憾的是,游戏手柄输入和输出并未实现标准化,因此 Web 浏览器通常需要针对特定设备采用自定义逻辑。这种做法不可持续,会导致对大量旧版和不常见设备的支持不佳。它还会导致浏览器依赖于特定设备的行为怪异之处。

术语

人机接口设备 (HID) 可以接收人类的输入或向人类提供输出。 HID 协议是一种用于在主机和设备之间进行双向通信的标准协议,旨在简化安装过程。

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

应用和 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();
用于选择 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();

接收输入报告

Nintendo Switch Joy-Con。

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

接着前面的示例,此代码可帮助您检测用户在 Joy-Con 右侧设备上按下了哪个按钮,以便您在家中进行尝试。

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]}.`);
});

请参阅 CodePen 上的演示

发送输出报告

如需向 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.
const rumbleData = [ /* ... */ ];
await device.sendReport(0x10, new Uint8Array(rumbleData));

请参阅 CodePen 上的演示

发送和接收功能报告

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

请参阅 CodePen 上的演示

如需从 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.
}

开发者提示

用于调试 HID 的内部网页。

使用内部网页 about://device-log 在 Chrome 中调试 HID,您可以在一个位置查看所有 HID 和 USB 设备相关事件。

您可以查看 HID 浏览器,将 HID 设备信息转储为人类可读的格式。它会为每个 HID 用途建立从用途值到名称的映射。

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

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

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

演示

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

实用链接

致谢

感谢 Matt ReynoldsJoe Medley 的评价。