WebHID APIでJoy Conを使ってみる

ハッカソンのネタ探しの時に見つけた。
HIDとは、Human Interface Deviceの略で人間からの入力を受け取ったり、コンピュータから人間に情報を出力したりするデバイスのことらしい。
例)キーボードやマウス、ゲームパッドなど
USBには、機種の種類を表すデバイスクラスとしてHIDクラスというものがあり、製品ごとに個別のドライバソフトを用意してなくても利用できる。
Bluetoothにも、HIDプロファイルというものがある。
WebHIDは、このHIDをブラウザ上で使えるAPIで対応しているブラウザは少ないが、夢が広がりそう。

なんとWiiリモコンやJoy Conはbluetooth接続ができ、HIDデバイスとして認識されるためWebHIDを使うと色々できるみたい。
今回は、Joy Conを使っていく。
Joy Con、実は結構リッチなコントローラーで
HD振動
加速度センサー
ジャイロスコープ
NFCリーダー
IRカメラ
など要素が盛り込まれている

加速度センサーを使いたい。
まずは、この方のリバースエンジニアリングのレポートを読んだり、MDNの例を読み込む

流れとしては、
requestDevice でアクセスの許可
↓
openで接続確立
↓
sendreportで色々設定
↓
inputreportでデータが送られてきた時にイベント発火

MDNのexampleを動かしてみる
index.html
<body>
<h1>
webhid Joy Con
</h1>
<Button id="leftButton">Joy Con L</Button>
<Button id="rightButton">Joy Con R</Button>
<Button id="check">Check</Button>
<Button id="disconnect">disconnect</Button>
<script src="script.js"></script>
<h1>加速度</h1>
<p id="value">neko</p>
</body>
script.js
const leftFilter = [
{
vendorId: 0x057e,
productId: 0x2006,
},
];
const rightFilter = [
{
vendorId: 0x057e,
productId: 0x2007,
},
];
let leftJoycon, rightJoycon;
document.getElementById("leftButton").addEventListener('click', async () => {
let devices = await navigator.hid.requestDevice({ filters: leftFilter });
leftJoycon = devices[0];
if (leftJoycon) {
await leftJoycon.open();
leftJoycon.addEventListener('inputreport', event => {
console.log("Left Joy-Con Input Report:", event);
console.log("Data:", event.data);
});
}
});
document.getElementById("rightButton").addEventListener('click', async () => {
let devices = await navigator.hid.requestDevice({ filters: rightFilter });
rightJoycon = devices[0];
if (rightJoycon) {
await rightJoycon.open();
rightJoycon.addEventListener('inputreport', event => {
console.log("Right Joy-Con Input Report:", event);
console.log("Data:", event.data);
});
const enableVibrationData = [1, 0, 1, 64, 64, 0, 1, 64, 64, 0x48, 0x01];
await rightJoycon.sendReport(0x01, new Uint8Array(enableVibrationData));
const rumbleData = [ /* ... */];
await rightJoycon.sendReport(0x10, new Uint8Array(rumbleData));
}
});
document.getElementById("check").addEventListener('click', async () => {
console.log(leftJoycon, rightJoycon);
});
document.getElementById("disconnect").addEventListener('click', async () => {
await rightJoycon.close();
await leftJoycon.close();
});
sendReportでデータ送る時、
sendReport(OUTPUT, new Uint8Array([1, 0, 1, 64, 64, 0, 1, 64, 64, subcommand, arg]))
でいけるみたい。
上記の例だと、振動を有効化するためのレポートを送っている
const enableVibrationData = [1, 0, 1, 64, 64, 0, 1, 64, 64, 0x48, 0x01];
await rightJoycon.sendReport(0x01, new Uint8Array(enableVibrationData));
OUTPUT 0x01
Rumble and subcommand.
Subcommand 0x48: Enable vibration
One argument of x00 Disable or x01 Enable.
enableにするために0x01をargにつけて送る
振動のデータを送る
const rumbleData = [ /* ... */];
await rightJoycon.sendReport(0x10, new Uint8Array(rumbleData));
OUTPUT 0x10
Rumble only. See OUTPUT 0x01 and "Rumble data" below.

加速度センサーを使うためには、
subcommand 0x40に 0x01送ったあと、input reportモードにして、INPUT 0x30を送るとよさそう
Subcommand 0x40: Enable IMU (6-Axis sensor)
One argument of x00 Disable or x01 Enable.
OUTPUT 0x01
Rumble and subcommand.
Subcommand 0x03: Set input report mode
One argument:
INPUT 0x30
Standard full mode - input reports with IMU data instead of subcommand replies. Pushes current state @60Hz, or @120Hz if Pro Controller.
addEventListener('inputreport', event
これだと、ボタンが押された時しかreportが来なかったため oninputeventに変更
script.js
document.getElementById("leftButton").addEventListener('click', async () => {
let devices = await navigator.hid.requestDevice({ filters: leftFilter });
leftJoycon = devices[0];
if (leftJoycon) {
await leftJoycon.open();
const enableIMU = [1, 0, 64, 64, 0, 1, 64, 64, 0x40, 0x01]
leftJoycon.sendReport(0x01, new Uint8Array(enableIMU));
await sleep(200);
const standardFullMode = [1, 0, 64, 64, 0, 1, 64, 64, 0x03, 0x30]
leftJoycon.sendReport(0x01, new Uint8Array(standardFullMode));
leftJoycon.oninputreport = (event) => {
const { data, device, reportId } = event;
console.log("Left Joy-Con Input Report:", event);
console.log("Data:", event.data);
}
}
});
あと、このデータを使いやすい形に変換してあげたら良さそう。
また、madgwickフィルタを使えば6軸のセンサでいい感じに姿勢推定ができるみたい。