WebHID で Stadia コントローラに話しかける

フラッシュされた Stadia コントローラは標準のゲームパッドのように動作します。つまり、すべてのボタンが Gamepad API を使用してアクセスできるわけではありません。WebHID を使用することで、欠落しているボタンにアクセスできるようになりました。

Stadia のサービス終了以降、多くの人がコントローラが廃棄物処理場で使い捨てのハードウェアになるのではないかと懸念していました。幸いなことに、Stadia コントローラは、Stadia Bluetooth モードページに移動してコントローラでフラッシュできるカスタム ファームウェアを提供することで、Stadia コントローラを公開することにしました。これにより、Stadia コントローラが標準のゲームパッドのように表示され、USB ケーブルまたは Bluetooth 経由でワイヤレスで接続できます。Project Fugu API Showcase で紹介された Stadia Bluetooth ページ自体も WebHIDWebUSB を使用していますが、この記事のトピックは扱いません。この投稿では、WebHID 経由で Stadia コントローラと通信する方法について説明します。

標準のゲームパッドとしての Stadia コントローラ

フラッシュすると、コントローラは標準のゲームパッドとしてオペレーティング システムに表示されます。次のスクリーンショットは、標準のゲームパッドでの一般的なボタンと軸の配置を示しています。Gamepad API の仕様で定義されているように、標準のゲームパッドには 0 ~ 16 のボタンがあるため、合計 17 個です(D-pad は 4 つのボタンとしてカウントされます)。ゲームパッド テスターのデモで Stadia コントローラを試すと、チャームのように機能することがわかります。

さまざまな軸とボタンがラベル付けされた標準的なゲームパッドのスキーマ。

しかし、Stadia コントローラのボタンの数を数えると、19 個あります。ゲームパッド テスターで体系的に試すと、[アシスタント] ボタンと [キャプチャ] ボタンが機能しないことがわかります。ゲームパッドの仕様で定義されているゲームパッドの buttons 属性がオープンエンドであっても、Stadia コントローラは標準のゲームパッドとして表示されるため、ボタン 0 ~ 16 のみがマッピングされます。他のボタンも使用可能ですが、ほとんどのゲームではそうしたボタンの存在は想定されていません。

WebHID が問題を解決

WebHID API により、欠落しているボタン 17 と 18 について話すことができます。必要に応じて、Gamepad API を介してすでに利用可能な他のすべてのボタンと軸に関するデータを取得することもできます。まず、Stadia コントローラがオペレーティング システムに自身を報告する方法を確認します。そのための 1 つの方法は、ランダムなページで Chrome DevTools コンソールを開き、WebHID API にフィルタされていないデバイスのリストをリクエストすることです。その後、詳しく調べるために Stadia コントローラを手動で選択します。空の filters オプション配列を渡すだけで、フィルタされていないデバイスのリストを取得できます。

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

選択ツールで、最後から 2 番目のエントリが Stadia コントローラのようになります。

関連性のないデバイスが表示された WebHID API のデバイス選択ツールと、最後から 2 番目の位置に Stadia コントローラ。

「Stadia Controller rev. A」デバイスを選択したら、結果の HIDDevice オブジェクトをコンソールに記録します。これにより、Stadia コントローラの productId37888、16 進数で 0x9400)と vendorId6353、16 進数で 0x18d1)が表示されます。公式の USB ベンダー ID の表vendorID を調べると、6353 が適切な Google Inc. にマッピングされていることがわかります。

HIDDevice オブジェクトのロギングの出力が表示されている Chrome DevTools コンソール。

上記のフローの代わりに、URL バーの chrome://device-log/ に移動して [Clear] ボタンを押し、Stadia コントローラを接続して [Refresh] を押すこともできます。同じ情報が表示されます。

接続された Stadia コントローラに関する情報が表示されている chrome://device-log デバッグ インターフェース。

また、HID Explorer ツールを使用して、パソコンに接続されている HID デバイスの詳細を調べる方法もあります。

この vendorIdproductId の 2 つの ID を使用して、適切な WebHID デバイスを正しくフィルタリングすることで、選択ツールに表示されるものを絞り込むことができます。

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

これで、関係のないデバイスからのノイズがなくなり、Stadia コントローラのみが表示されるようになりました。

Stadia コントローラのみが表示された WebHID API のデバイス選択ツール。

次に、open() メソッドを呼び出して HIDDevice を開きます。

await stadiaController.open();

もう一度 HIDDevice をログに記録すると、opened フラグが true に設定されます。

HIDDevice オブジェクトを開いた後、そのオブジェクトのログの出力が表示されている Chrome DevTools コンソール。

デバイスを開いた状態で、イベント リスナーをアタッチして inputreport イベントの受信をリッスンします。

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

コントローラの [Assistant] ボタンを押して離すと、2 つのイベントがコンソールに記録されます。これらは、「アシスタント ボタンの押下」および「アシスタントのボタンアップ」イベントと考えることができます。timeStamp を除けば、この 2 つのイベントは一見見分けがつきません。

ログに記録されている HIDInputReportEvent オブジェクトを示す Chrome DevTools コンソール。

HIDInputReportEvent インターフェースの reportId プロパティは、このレポートの 1 バイトの識別接頭辞を返します。HID インターフェースがレポート ID を使用しない場合は 0 を返します。この場合は 3 です。シークレットは data プロパティにあり、サイズ 10 の DataView として表されます。DataView は、バイナリ ArrayBuffer の複数の数値型を読み書きするための低レベルのインターフェースを提供します。この表現をわかりやすくするには、ArrayBuffer から Uint8Array を作成し、個々の 8 ビット符号なし整数を確認します。

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

その後、入力レポート イベントデータをもう一度ログに記録すると、状況が理解しやすくなり、「アシスタント ボタンの押下」と「アシスタントのボタンの上」のイベントが解読可能になり始めます。1 つ目の整数(両方のイベントの 8)はボタンの押下に関連しているように見え、2 つ目の整数(20)はアシスタント ボタンが押されているかどうかに関連しているようです。

Chrome DevTools コンソール。HIDInputReportEvent ごとに Uint8Array オブジェクトが記録されています。

[Assistant] ボタンではなく [Capture] ボタンを押すと、2 つ目の整数が、ボタンを押すと 1 から 0 に切り替わります。これにより、不足している 2 つのボタンを利用できる、非常にシンプルな「ドライバ」を作成できます。

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 デバイスを操作するには、常に最初の選択ツールを 1 回行う必要がありますが、その後の接続では既知のデバイスに再接続できます。そのためには、getDevices() メソッドを呼び出します。

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

デモ

私が作成したデモでは、Gamepad API と WebHID API で共同で制御する Stadia コントローラを確認できます。ソースコードをご確認ください。ソースコードは、この記事のスニペットを基に構築されています。わかりやすくするため、[A]、[B]、[X]、[Y] ボタン(Gamepad API で制御)と、[Assistant]、[Capture] ボタン(WebHID API で制御)のみを表示します。コントローラの画像の下には WebHID の生データが表示されるため、コントローラのすべてのボタンと軸の状況を把握できます。

https://stadia-controller-webhid-gamepad.glitch.me/ のデモアプリ。A、B、X、Y のボタンが Gamepad API によって制御され、アシスタントとキャプチャのボタンが WebHID API によって制御されている。

まとめ

新しいファームウェアにより、Stadia コントローラは 17 個のボタンを備えた標準のゲームパッドとして使用できるようになりました。ほとんどの場合、このコントローラは一般的なウェブゲームを操作するのに十分な量です。なんらかの理由でコントローラの 19 個のボタンすべてからデータが必要な場合、WebHID を使用すると、レベルの低い入力レポートにアクセスして、1 つずつリバース エンジニアリングすることで解読できます。この記事をお読みになったうえで、完全な WebHID ドライバを作成された場合は、ご連絡ください。お客様のプロジェクトをこちらにリンクいたします。WebHID をぜひご活用ください。

謝辞

この記事は François Beaufort によってレビューされました。