WebHID を使ってブラウザ上で Joy-Con のジャイロの値を取得する

WebHID を使ってブラウザ上で Joy-Con のジャイロの値を取得する

はじめに

第二のドワンゴ Advent Calendar 2019の23日目の記事です。再入社エントリも公開したのでもしよかったら見てください。
Google Chrome 78 で WebHID API が experimental features として追加されたので試しに Joy-Con を接続してみる話です。
Joy-ConGamePad API を使用することでブラウザ上からボタンやスティックの情報を取得することができるのですが、残念ながらジャイロの取得には対応していません。
WebHID API を使用すれば Joy-Con のすべての機能にアクセスできるので、ブラウザ上でジャイロや IR センサーなどの情報を利用することができるはずです。

WebHID を有効にする

WebHID はまだ experimental なので、使用するためには chrome://flags/#enable-experimental-web-platform-features で有効にする必要があります。
これで navigator に hid プロパティが追加されるようになります。

WebHID を使用して Bluetooth 端末とブラウザを接続する

WebHID を使用して端末と接続するには navigator.hid.requestDevice を使用します。
ただし、この function はユーザーアクションなしに呼び出すことができないため、ボタンのクリックイベントなどを通して呼び出す必要があります。

document.querySelector("#start-button").addEventListener("click", async () => {
  const device = await navigator.hid.requestDevice({ filters: [] });
});

このコードを実行すると、以下のようなダイアログが現れます。
f:id:thiry:20191220192720p:plain
ここで選択したデバイスHIDDevice インターフェースとして device 変数に格納されます。
requestDevice に引数として渡している filters に条件を追加すると、該当するデバイスのみをダイアログに表示することができます。

document.querySelector("#start-button").addEventListener("click", async () => {
  const device = await navigator.hid.requestDevice({ 
    filters: [
      {
        vendorId: 0x057e, // Nintendo vendor ID
        productId: 0x2007 // joy-con R
      }
    ]
  });
});

これでデバイスの接続が完了しました。

Joy-Con から送られてきたデータをコンソールに出力する

いつもの addEventListener 経由で行います。

device.addEventListener("inputreport", (event) => {
  console.log(event.data);
});

Joy-Con にコマンドを送信する

EventListener の設定は終わりましたが、Joy-Con にコマンドを送ってデータを要求しないと何も console にアウトプットされません。
コマンドの送信は HIDDevice#sendReport 経由で行います。インターフェースは以下の通り

interface HIDDevice : EventTarget {
    Promise<void> sendReport(octet reportId, BufferSource data);
};

コマンドを送る方法が分かったので、https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering にまとまっている Joy-Con の制御データをリバースエンジニアリングした情報を使用して実際にコマンドを送信してみます。 Bluetooth HID Informationを読んでいくと OUTPUT 0x01 で指定したサブコマンドを実行することができることが分かります。
サブコマンドには色々なコマンドがありますが、Subcommand 0x03: Set input report modeを使用すると Joy-Con のボタンやスティックの入力データが Joy-Con から送信されるようになるようです。
コマンドとサブコマンドが分かったところで、OUTPUT 0x01 にある C 言語で書かれたサンプルや、Chromium の GamePad API における Joy-Con サポートの実装をもとに WebHID API 経由 で HID に送るデータを作っていきます。

let globalPacketNumber = 0x00; // 0x0 ~ 0xf でループするパケットナンバー.
const createQuery = (subCommand, subCommandArguments) => {
    const query = new Array(48).fill(0x00);
    query[0] = globalPacketNumber % 0x10; // 0x0 ~ 0xf になるように % 0x10 する
    query[1] = 0x00;
    query[2] = 0x01;
    query[5] = 0x00;
    query[6] = 0x01;
    query[9] = subCommand;
    query[10] = subCommandArguments;

    globalPacketNumber++;

    return Uint8Array.from(query);
};

await device.sendReport(0x01, createQuery(0x03, 0x3f));// 第2引数はサブコマンドの args. 0x3f = ボタンやスティックを操作するとボタンの各種情報を送信するようになる

これで Joy-Con のボタンを押すと console に Joy-Con の情報が表示されるようになったはずです。

Joy-Con からジャイロの情報を取得する

Joy-Conジャイロセンサーを有効にしないとデータを送ってこないので設定を有効にします
例によってドキュメントを読んでいくとSubcommand 0x40: Enable IMU (6-Axis sensor)が該当するコマンドであることが分かります。

await device.sendReport(0x01, createQuery(0x40, 0x01)); // 0x01 = enable, 0x00 = disable

これでジャイロが有効になりました。
さて、次に Input Report Mode を変更してジャイロの値をリアルタイムで取得できるようにします。

await device.sendReport(0x01, createQuery(0x03, 0x30)); // 0x30 = 60Hz Standard full mode.

注意点として、ジャイロの有効化を行ってから実際にジャイロセンサーの値が Input Report Mode に含まれるまでに若干のラグがあります。
Input Report Mode を有効にする前に 0.5 秒程度待つと良いでしょう。

const delay = (delayMs) => new Promise(resolve => setTimeout(resolve, delayMs));
// ジャイロセンサー有効化
await device.sendReport(0x01, createQuery(0x40, 0x01));
// 0.5秒待つ
await delay(500);
// Input Report Mode を 60Hz Standard full mode に変更
await device.sendReport(0x01, createQuery(0x03, 0x30));

まとめ

これでブラウザ上から Joy-Conジャイロセンサーの値を取得できるようになりました。ジャイロの値を使ったアプリケーションを作るところまでたどり着きたかったのですが時間切れでした。
ここまでの実装は https://github.com/Thiry1/joycon-webhid にアップしておくので、興味を持った方は覗いてみてください。