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

フラッシュされた Stadia コントローラは標準のゲームパッドのように動作します。つまり、Gamepad API を使用してアクセスできないボタンもあります。WebHID を使用すると、見つからなかったボタンにアクセスできるようになりました。

Stadia のサービス終了後、多くの人がコントローラが埋め立て地の無用の長物になることを恐れていました。幸いなことに、Stadia チームは、Stadia Bluetooth モードのページにアクセスしてコントローラに書き込めるカスタム ファームウェアを提供することで、Stadia コントローラをオープンにすることを決定しました。これにより、Stadia コントローラが標準のゲームパッドとして認識され、USB ケーブルまたは Bluetooth 経由でワイヤレスに接続できるようになります。Stadia Bluetooth ページは Project Fugu API Showcase で紹介されています。このページ自体は WebHIDWebUSB を使用していますが、この記事では取り上げません。この投稿では、WebHID を介して Stadia コントローラと通信する方法について説明します。

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

フラッシュ後、コントローラはオペレーティング システムに標準のゲームパッドとして認識されます。標準的なゲームパッドの一般的なボタンと軸の配置については、次のスクリーンショットをご覧ください。Gamepad API 仕様で定義されているように、標準のゲームパッドには 0 ~ 16 のボタン(合計 17 個)があります(方向パッドは 4 個のボタンとしてカウントされます)。ゲームパッド テスターのデモで Stadia コントローラを試すと、問題なく動作することがわかります。

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

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

WebHID の活用

WebHID API により、欠落しているボタン 17 と 18 に対応できます。必要に応じて、Gamepad API で利用可能な他のすべてのボタンと軸に関するデータを取得することもできます。まず、Stadia コントローラがオペレーティング システムにどのようにレポートされるかを確認します。その方法の 1 つは、任意のページで Chrome DevTools Console を開き、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 を検索すると、6353Google Inc. にマッピングされていることがわかります。

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

上記のフローの代替手段として、URL バーで chrome://device-log/ に移動し、[クリア] ボタンを押して、Stadia コントローラを接続し、[更新] を押す方法もあります。これにより、同じ情報が提供されます。

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

もう 1 つの代替手段として、HID Explorer ツールを使用する方法があります。このツールを使用すると、パソコンに接続されている HID デバイスの詳細をさらに詳しく調べることができます。

これらの 2 つの ID(vendorIdproductId)を使用して、適切な 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 DevTools コンソールに、HIDDevice オブジェクトを開いた後のログ出力が表示されている。

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

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

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

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

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 コンソール。

アシスタント ボタンではなく、キャプチャ ボタンを押すと、ボタンを押したときに 1 から 0 に切り替わる 2 番目の整数が表示されます。これにより、欠落している 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 デバイスを使用するには、初回に一度だけピッカーの操作を行う必要がありますが、次回以降の接続では、既知のデバイスに再接続できます。そのためには、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 の生データが表示されるため、コントローラのすべてのボタンと軸を把握できます。

Gamepad API で制御される A、B、X、Y ボタンと、WebHID API で制御されるアシスタント ボタンとキャプチャ ボタンを示す Stadia コントローラ デモアプリ。

まとめ

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

謝辞

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