借助 WebHID API,网站可以访问替代的辅助键盘和非标准的游戏手柄。
有许多人机接口设备 (HID) 属于长尾产品,例如替代键盘或奇特的游戏手柄,这些设备太新、太旧或太不常见,系统的设备驱动程序无法访问它们。WebHID API 通过提供一种在 JavaScript 中实现设备专用逻辑的方法来解决此问题。
建议的用例
HID 设备可从人接受输入或向人提供输出。设备示例包括键盘、指控设备(鼠标、触摸屏等)和游戏手柄。借助 HID 协议,您可以在桌面设备上使用操作系统驱动程序来访问这些设备。网络平台依赖于这些驱动程序来支持 HID 设备。
当涉及替代辅助键盘(例如 Elgato Stream Deck、Jabra 头戴式耳机、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();
您还可以使用 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
)。
继续使用上一个示例,以下代码展示了如何检测用户在 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 设备信息转换为人类可读的格式,请查看 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}
,以获得更具体的规则。确保您的 user
是 plugdev
群组的成员。然后,只需重新连接设备即可。
浏览器支持
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 Reynolds 和 Joe Medley 对本文的审核。 红色和蓝色的 Nintendo Switch 照片由 Sara Kurfeß 拍摄,黑色和银色的笔记本电脑照片由 Unsplash 上的 Athul Cyriac Ajay 拍摄。