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 個)を備えています(十字キーは 4 個のボタンとしてカウントされます)。ゲームパッド テスターのデモで Stadia コントローラを試すと、問題なく動作することがわかります。

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

ただし、Stadia コントローラのボタンの数は 19 個です。ゲームパッド テスターでボタンを 1 つずつ試すと、アシスタントボタンとキャプチャボタンが機能しないことがわかります。ゲームパッド仕様で定義されているゲームパッド buttons 属性がオープンエンドの場合でも、Stadia コントローラは標準のゲームパッドとして表示されるため、ボタン 0 ~ 16 のみがマッピングされます。他のボタンは引き続き使用できますが、ほとんどのゲームでは存在しないものと見なされます。

WebHID の活用

WebHID API を使用すると、不足しているボタン 17 と 18 に話しかけることができます。必要に応じて、Gamepad API ですでに利用可能な他のすべてのボタンと軸に関するデータも取得できます。最初のステップは、Stadia コントローラがオペレーティング システムに自身を報告する方法を確認することです。たとえば、任意のページで Chrome DevTools コンソールを開き、WebHID API からフィルタされていないデバイスのリストをリクエストします。その後、詳細な検査のために Stadia コントローラを手動で選択します。空の filters オプション 配列を渡すだけで、フィルタされていないデバイスのリストを取得できます。

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

選択ツールでは、2 番目のエントリが Stadia コントローラのように見えます。

無関係なデバイスがいくつか表示された WebHID API デバイス選択ツール。Stadia コントローラが最後から 2 番目の位置に表示されています。

[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/ に移動し、[消去] ボタンを押して Stadia コントローラを接続し、[更新] を押します。同じ情報が提供されます。

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

別の方法として、HID Explorer ツールを使用する方法もあります。このツールを使用すると、パソコンに接続されている HID デバイスの詳細情報を確認できます。

これらの 2 つの ID(vendorIdproductId)を使用して、正しい 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);
});

コントローラでアシスタント ボタンを押して離すと、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);

入力レポートのイベントデータを再度ログに記録すると、状況が明確になり、「アシスタント ボタンの押下」イベントと「アシスタント ボタンの離脱」イベントを解読できるようになります。最初の整数(両方のイベントで 8)はボタンの押下に関連しているようで、2 番目の整数(20)は [アシスタント] ボタンが押されたかどうかに関連しているようです。

HIDInputReportEvent ごとにログに記録される Uint8Array オブジェクトが表示された Chrome DevTools コンソール。

アシスタント ボタンではなくキャプチャ ボタンを押すと、2 番目の整数値が、ボタンを押すと 1 から 0 に切り替わり、ボタンを離すと 0 から 1 に切り替わります。これにより、不足している 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 コントローラは、私が作成したデモで確認できます。この記事のスニペットを基に構築されたソースコードもぜひご覧ください。わかりやすくするため、ABXY ボタン(Gamepad API で制御)と、アシスタントボタンとキャプチャボタン(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 さんが確認しました。