通过 WebHID 与 Stadia 控制器通信

闪烁的 Stadia 控制器就像标准游戏手柄一样,这意味着它的所有按钮都无法使用 Gamepad API 访问。借助 WebHID,您现在可以访问缺少的按钮。

自从 Stadia 关闭以来,许多人担心该控制器最终会成为垃圾填埋场上的无用硬件。幸运的是,Stadia 团队决定通过提供自定义固件来打开 Stadia 控制器,您可以前往 Stadia 蓝牙模式页面将固件刷写到控制器上。这会使您的 Stadia 控制器看起来像一个标准游戏手柄,您可以通过 USB 线或蓝牙进行无线连接。Stadia 蓝牙页面本身使用了 WebHIDWebUSB,这在 Project Fugu API Showcase 中值得骄傲,但这不是本文的主题。在这篇博文中,我想介绍如何通过 WebHID 与 Stadia 控制器通信。

将 Stadia 控制器用作标准游戏手柄

刷写后,控制器会对操作系统显示为标准游戏手柄。请参见以下屏幕截图,了解标准游戏手柄上的常见按钮和轴排列方式。正如 Gamepad API 规范中所定义的,标准游戏手柄的按钮数量为 0 到 16,因此共计 17 个(方向键计为 4 个按钮)。如果您在游戏手柄测试人员演示中试用 Stadia 控制器,会发现它运行起来很酷。

标准游戏手柄的架构,图中标有各个轴和按钮。

不过,如果统计 Stadia 控制器上的按钮数量,总共有 19 个。如果您在游戏手柄测试工具中系统地逐一尝试这些按钮,则会发现 AssistantCapture 按钮不起作用。即使游戏手柄规范中定义的游戏手柄 buttons 属性是开放式的,由于 Stadia 控制器显示为标准游戏手柄,因此只有按钮 0-16 会映射。您仍然可以使用其他按钮,但大多数游戏并不希望看到它们。

WebHID 有用

得益于 WebHID API,您可以响应缺失的按钮 17 和 18。如果您真正需要,甚至可以获取可通过 Gamepad API 提供的所有其他按钮和轴的数据。第一步是了解 Stadia 控制器如何向操作系统报告自身。其中一种方法是在任意随机页面上打开 Chrome 开发者工具控制台,然后从 WebHID API 请求未经过滤的设备列表。然后,您可以手动选择 Stadia 控制器以进行进一步检查。只需传递空的 filters 选项数组,即可获取未经过滤的设备列表。

const [device] = await navigator.hid.requestDevice({filters: []});

在选择器中,倒数第二个条目类似 Stadia 控制器。

WebHID API 设备选择器,其中显示了一些不相关的设备,而 Stadia 控制器位于倒数位置。

选择“Stadia Controller rev. A”设备后,将生成的 HIDDevice 对象记录到控制台。这会显示 Stadia 控制器的 productId37888,以十六进制格式表示 0x9400)和 vendorId6353,以十六进制格式表示 0x18d1)。如果您在官方 USB 供应商 ID 表中查找 vendorID,就会发现 6353 对应的是您希望的值:Google Inc.

显示记录 HIDDevice 对象的输出的 Chrome 开发者工具控制台。

上述流程的替代方法是转到网址栏中的 chrome://device-log/,按 Clear 按钮,插入 Stadia 控制器,然后按 Refresh。这与您获得的信息相同。

chrome://device-log 调试界面,显示了有关插入的 Stadia 控制器的信息。

您还可以利用 HID Explorer 工具探索与计算机相连的 HID 设备的更多详细信息。

现在,您可以使用 vendorIdproductId 这两个 ID 针对正确的 WebHID 设备正确进行过滤,以优化选择器中显示的内容。

const [stadiaController] = await navigator.hid.requestDevice({filters: [{
  vendorId: 6353,
  productId: 37888,
}]});

现在,所有不相关设备的噪音都消失了,仅显示 Stadia 控制器。

WebHID API 设备选择器,仅显示 Stadia 控制器。

接下来,通过调用 open() 方法打开 HIDDevice

await stadiaController.open();

再次记录 HIDDevice,并且 opened 标志设置为 true

Chrome 开发者工具控制台,其中显示了打开 HIDDevice 对象后记录它的输出。

设备处于打开状态时,通过附加事件监听器来监听传入的 inputreport 事件。

stadiaController.addEventListener('inputreport', (e) => {
  console.log(e);
});

当您按住控制器上的 Assistant 按钮后,控制台会记录两个事件。您可以将这些事件视为“按下 Google 助理按钮”和“按下 Google 助理按钮”事件。除了 timeStamp 之外,两个事件乍一看并没有什么区别。

Chrome 开发者工具控制台,其中显示了正在记录的 HIDInputReportEvent 对象。

HIDInputReportEvent 接口的 reportId 属性会针对此报告返回单字节标识前缀,如果 HID 接口不使用报告 ID,则返回 0。在本例中是 3。Secret 位于 data 属性中,表示为大小为 10 的 DataViewDataView 提供了一个低级接口,用于在二进制文件 ArrayBuffer 中读取和写入多种数字类型。若要从此表示法中更清晰地理解内容,则可从 ArrayBuffer 中创建一个 Uint8Array,这样您就可以看到各个 8 位无符号整数。

const data = new Uint8Array(event.data.buffer);

当您再次记录输入报告事件数据时,事情就会变得更有意义,“Google 助理按钮按下”和“Google 助理按钮释放”事件开始变得可以解读。第一个整数(两个事件中的 8)似乎与按下按钮有关,第二个整数(20)似乎与是否按下 Google 助理按钮有关。

Chrome 开发者工具控制台,其中显示了为每个 HIDInputReportEvent 记录的 Uint8Array 对象。

Capture 按钮而不是 Assistant 按钮后,您会看到,在释放按钮后,第二个整数会从 1 切换为 0。这样一来,您可以编写一个非常简单的“驱动程序”,以便充分利用缺少的两个按钮。

stadia.addEventListener('inputreport', (event) => {
  if (!e.reportId === 3) {
    return;
  }
  const data = new Uint8Array(event.data.buffer);
  if (data[0] === 8) {
    if (data[1] === 1) {
      hidButtons[1].classList.add('highlight');
    } else if (data[1] === 2) {
      hidButtons[0].classList.add('highlight');
    } else if (data[1] === 3) {
      hidButtons[0].classList.add('highlight');
      hidButtons[1].classList.add('highlight');
    } else {
      hidButtons[0].classList.remove('highlight');
      hidButtons[1].classList.remove('highlight');
    }
  }
});

通过像这样的逆向工程方法,你可以依次按下按钮,逐个轴地确定如何使用 WebHID 与 Stadia 控制器通信。掌握了窍门之后,剩下的工作几乎都是机械的整数映射。

现在还缺少一点,那就是 Gamepad API 提供的流畅连接体验。出于安全考虑,为了使用 Stadia 控制器等 WebHID 设备,您始终需要查看一次初始选择器体验,但日后建立连接时,您可以重新连接到已知设备。为此,请调用 getDevices() 方法。

let stadiaController;
const [device] = await navigator.hid.getDevices();
if (device && device.vendorId === 6353 && device.productId === 37888) {
  stadiaController = device;
}

演示

在我构建的演示中,您可以看到由 Gamepad API 和 WebHID API 共同控制的 Stadia 控制器。请务必查看源代码,它基于本文中的代码段构建而成。为简单起见,我只显示了 ABXY 按钮(由 Gamepad API 控制),以及 AssistantCapture 按钮(由 WebHID API 控制)。控制器图片下方是原始 WebHID 数据,方便您大致了解控制器上的所有按钮和轴。

https://stadia-controller-webhid-gamepad.glitch.me/ 上的演示版应用显示了由 Gamepad API 控制的 A、B、X 和 Y 按钮,以及由 WebHID API 控制的 Google 助理和“摄趣”按钮。

总结

得益于新固件,Stadia 控制器现在可用作包含 17 个按钮的标准游戏手柄,在大多数情况下,这足以控制常见的网页游戏。无论出于什么原因,您需要从控制器上所有 19 个按钮中获取的数据,WebHID 可让您访问低级别输入报告,您可以通过对报告逐一进行逆向工程来解读此类报告。如果您在读完这篇文章后碰巧编写了一个完整的 WebHID 驱动程序,请务必与我联系,我很乐意在此处链接到您的项目。祝您使用 WebHID 一切顺利!

致谢

这篇文章由 François Beaufort 发表。