一般的ではない HID デバイスへの接続

WebHID API を使用すると、ウェブサイトは代替の補助キーボードや珍しいゲームパッドにアクセスできます。

François Beaufort
François Beaufort

代替キーボードや珍しいゲームパッドなど、ヒューマン インターフェース デバイス(HID)のロングテールには、新しすぎる、古すぎる、システムのデバイス ドライバではアクセスできないほど珍しいものがあります。WebHID API は、JavaScript でデバイス固有のロジックを実装する方法を提供することで、この問題を解決します。

推奨されるユースケース

HID デバイスは、人間から入力を受け取ったり、人間に出力を提供したりします。デバイスの例としては、キーボード、ポインティング デバイス(マウス、タッチスクリーンなど)、ゲームパッドなどがあります。HID プロトコルにより、オペレーティング システム ドライバを使用してデスクトップ パソコンでこれらのデバイスにアクセスできます。ウェブ プラットフォームは、これらのドライバを使用して HID デバイスをサポートします。

一般的でない HID デバイスにアクセスできないことは、代替の補助キーボード(Elgato Stream DeckJabra ヘッドセットX キーなど)や、珍しいゲームパッドに対応している場合に特に苦労します。パソコン用に設計されたゲームパッドは、ゲームパッドの入力(ボタン、ジョイスティック、トリガー)と出力(LED、ランブル)に HID を使用することがよくあります。残念ながら、ゲームパッドの入出力は十分に標準化されておらず、ウェブブラウザでは、特定のデバイス向けのカスタム ロジックが必要になることがよくあります。これは持続不可能であり、古いデバイスや一般的でないデバイスのロングテールに対するサポートが不十分になります。また、ブラウザが特定のデバイスの動作の特性に依存します。

用語

HID は、レポートとレポート記述子という 2 つの基本的な概念で構成されています。レポートは、デバイスとソフトウェア クライアントの間で交換されるデータです。レポート記述子は、デバイスがサポートするデータの形式と意味を表します。

HID(ヒューマン インターフェース デバイス)は、人間から入力を受け取る、または人間に出力を提供するデバイスの一種です。また、インストール手順を簡素化するように設計されたホストとデバイス間の双方向通信の標準である HID プロトコルを指します。HID プロトコルはもともと USB デバイス用に開発されましたが、Bluetooth など他の多くのプロトコルで実装されています。

アプリケーションと HID デバイスは、次の 3 種類のレポートを通じてバイナリデータを交換します。

レポートタイプ 説明
入力レポート デバイスからアプリに送信されるデータ(ボタンの押下など)
出力レポート アプリケーションからデバイスに送信されるデータ(キーボード バックライトをオンにするリクエストなど)。
機能レポート どちらの方向にも送信できるデータです。形式はデバイスによって異なります。

レポート記述子は、デバイスがサポートするレポートのバイナリ形式を記述します。構造は階層的であり、トップレベルのコレクション内でレポートを個別のコレクションとしてグループ化できます。記述子の形式は、HID 仕様によって定義されます。

HID 使用状況は、標準化された入力または出力を表す数値です。用途の値によって、デバイスはデバイスの用途とレポートの各フィールドの目的を説明することができます。たとえば、マウスの左ボタンに対して 1 つを定義します。使用状況は使用状況ページにも整理され、デバイスまたはレポートの大まかなカテゴリを示します。

WebHID API の使用

機能検出

WebHID API がサポートされているかどうかを確認するには、次のコマンドを使用します。

if ("hid" in navigator) {
  // The WebHID API is supported.
}

HID 接続を開く

WebHID API は、入力を待機しているときにウェブサイトの UI がブロックされないように、設計上非同期になっています。HID データはいつでも受信でき、それをリッスンする方法が必要になるため、これは重要です。

HID 接続を開くには、まず HIDDevice オブジェクトにアクセスします。そのためには、navigator.hid.requestDevice() を呼び出してデバイスを選択するようユーザーに促すか、以前にウェブサイトへのアクセスが許可されているデバイスのリストを返す navigator.hid.getDevices() からデバイスを選択します。

navigator.hid.requestDevice() 関数は、フィルタを定義する必須オブジェクトを受け取ります。これらは、USB ベンダー ID(vendorId)、USB 製品 ID(productId)、使用状況ページの値(usagePage)、使用状況の値(usage)に接続されたデバイスを照合するために使用されます。この情報は、USB ID リポジトリHID 使用状況表のドキュメントから取得できます。

この関数から返される複数の HIDDevice オブジェクトは、同じ物理デバイス上の複数の HID インターフェースを表します。

// Filter on devices with the Nintendo Switch Joy-Con USB Vendor/Product IDs.
const filters = [
  {
    vendorId: 0x057e, // Nintendo Co., Ltd
    productId: 0x2006 // Joy-Con Left
  },
  {
    vendorId: 0x057e, // Nintendo Co., Ltd
    productId: 0x2007 // Joy-Con Right
  }
];

// Prompt user to select a Joy-Con device.
const [device] = await navigator.hid.requestDevice({ filters });
// Get all devices the user has previously granted the website access to.
const devices = await navigator.hid.getDevices();
ウェブサイトに表示された HID デバイスのメッセージのスクリーンショット。
Nintendo Switch Joy-Con を選択するためのユーザー プロンプト。

navigator.hid.requestDevice() でオプションの exclusionFilters キーを使用して、誤動作などが確認されている一部のデバイスをブラウザ選択ツールから除外することもできます。

// Request access to a device with vendor ID 0xABCD. The device must also have
// a collection with usage page Consumer (0x000C) and usage ID Consumer
// Control (0x0001). The device with product ID 0x1234 is malfunctioning.
const [device] = await navigator.hid.requestDevice({
  filters: [{ vendorId: 0xabcd, usagePage: 0x000c, usage: 0x0001 }],
  exclusionFilters: [{ vendorId: 0xabcd, productId: 0x1234 }],
});

HIDDevice オブジェクトには、デバイス識別用の USB ベンダー ID とプロダクト ID が含まれます。その collections 属性は、デバイスのレポート形式の階層的な説明で初期化されます。

for (let collection of device.collections) {
  // An HID collection includes usage, usage page, reports, and subcollections.
  console.log(`Usage: ${collection.usage}`);
  console.log(`Usage page: ${collection.usagePage}`);

  for (let inputReport of collection.inputReports) {
    console.log(`Input report: ${inputReport.reportId}`);
    // Loop through inputReport.items
  }

  for (let outputReport of collection.outputReports) {
    console.log(`Output report: ${outputReport.reportId}`);
    // Loop through outputReport.items
  }

  for (let featureReport of collection.featureReports) {
    console.log(`Feature report: ${featureReport.reportId}`);
    // Loop through featureReport.items
  }

  // Loop through subcollections with collection.children
}

HIDDevice デバイスはデフォルトで「閉じた」状態で返されます。データを送受信するには、open() を呼び出して開く必要があります。

// Wait for the HID connection to open before sending/receiving data.
await device.open();

入力レポートの受信

HID 接続が確立されたら、デバイスから "inputreport" イベントをリッスンすることで、受信入力レポートを処理できます。これらのイベントには、HID データを DataView オブジェクト(data)、それが属する HID デバイス(device)、入力レポートに関連付けられた 8 ビットのレポート ID(reportId)として含まれます。

赤と青の任天堂のスイッチの写真。
Nintendo Switch Joy-Con デバイス。

前の例について、以下のコードは、ユーザーが Joy-Con 右デバイスでどのボタンを押したかを検出する方法を示しています。これにより、自宅で試すことができます。

device.addEventListener("inputreport", event => {
  const { data, device, reportId } = event;

  // Handle only the Joy-Con Right device and a specific report ID.
  if (device.productId !== 0x2007 && reportId !== 0x3f) return;

  const value = data.getUint8(0);
  if (value === 0) return;

  const someButtons = { 1: "A", 2: "X", 4: "B", 8: "Y" };
  console.log(`User pressed button ${someButtons[value]}.`);
});

出力レポートを送信する

出力レポートを HID デバイスに送信するには、出力レポート(reportId)に関連付けられている 8 ビットのレポート ID と、バイトを BufferSourcedata)として device.sendReport() に渡します。レポートが送信されると、返された Promise が解決されます。HID デバイスがレポート ID を使用しない場合は、reportId を 0 に設定します。

以下の例は Joy-Con デバイスに適用され、出力レポートで振動する方法を示しています。

// First, send a command to enable vibration.
// Magical bytes come from https://github.com/mzyy94/joycon-toolweb
const enableVibrationData = [1, 0, 1, 64, 64, 0, 1, 64, 64, 0x48, 0x01];
await device.sendReport(0x01, new Uint8Array(enableVibrationData));

// Then, send a command to make the Joy-Con device rumble.
// Actual bytes are available in the sample below.
const rumbleData = [ /* ... */ ];
await device.sendReport(0x10, new Uint8Array(rumbleData));

機能レポートの送受信

機能レポートは、両方向に移動することができる唯一の HID データレポートです。これにより、HID デバイスとアプリケーションが標準化されていない HID データを交換できるようになります。入力レポートや出力レポートとは異なり、特徴レポートはアプリで定期的に受信または送信されることはありません。

黒とシルバーのノートパソコンの写真。
ノートパソコンのキーボード

機能レポートを HID デバイスに送信するには、機能レポート(reportId)に関連付けられている 8 ビットのレポート ID と、バイトを BufferSourcedata)として device.sendFeatureReport() に渡します。レポートが送信されると、返された Promise は解決されます。HID デバイスがレポート ID を使用しない場合は、reportId を 0 に設定します。

次の例は、機能レポートの使用方法を示しています。ここでは、Apple キーボード バックライト デバイスをリクエストし、デバイスを開いて点滅させる方法を示しています。

const waitFor = duration => new Promise(r => setTimeout(r, duration));

// Prompt user to select an Apple Keyboard Backlight device.
const [device] = await navigator.hid.requestDevice({
  filters: [{ vendorId: 0x05ac, usage: 0x0f, usagePage: 0xff00 }]
});

// Wait for the HID connection to open.
await device.open();

// Blink!
const reportId = 1;
for (let i = 0; i < 10; i++) {
  // Turn off
  await device.sendFeatureReport(reportId, Uint32Array.from([0, 0]));
  await waitFor(100);
  // Turn on
  await device.sendFeatureReport(reportId, Uint32Array.from([512, 0]));
  await waitFor(100);
}

HID デバイスから機能レポートを受信するには、機能レポート(reportId)に関連付けられた 8 ビットのレポート ID を device.receiveFeatureReport() に渡します。返された Promise は、機能レポートのコンテンツを含む DataView オブジェクトで解決されます。HID デバイスがレポート ID を使用しない場合は、reportId を 0 に設定します。

// Request feature report.
const dataView = await device.receiveFeatureReport(/* reportId= */ 1);

// Read feature report contents with dataView.getInt8(), getUint8(), etc...

接続と接続解除を聞く

HID デバイスへのアクセスが許可されているウェブサイトは、"connect" イベントと "disconnect" イベントをリッスンすることで、接続イベントと切断イベントをアクティブに受信できます。

navigator.hid.addEventListener("connect", event => {
  // Automatically open event.device or warn user a device is available.
});

navigator.hid.addEventListener("disconnect", event => {
  // Remove |event.device| from the UI.
});

HID デバイスへのアクセス権を取り消す

ウェブサイトは、HIDDevice インスタンスで forget() を呼び出すことで、保持する必要がなくなった HID デバイスにアクセスするための権限をクリーンアップできます。たとえば、多数のデバイスを備えた共有コンピュータで使用される教育用ウェブ アプリケーションの場合、ユーザー生成の権限が大量に蓄積されると、ユーザー エクスペリエンスが低下します。

単一の HIDDevice インスタンスで forget() を呼び出すと、同じ物理デバイス上のすべての HID インターフェースへのアクセスが取り消されます。

// Voluntarily revoke access to this HID device.
await device.forget();

forget() は Chrome 100 以降で利用できるため、この機能がサポートされているかどうかを以下で確認してください。

if ("hid" in navigator && "forget" in HIDDevice.prototype) {
  // forget() is supported.
}

開発のヒント

Chrome の内部ページ about://device-log を使用すると、HID デバイスと USB デバイスに関連するすべてのイベントを 1 か所で確認できます。これにより、Chrome での HID のデバッグを簡単に行えます。

HID をデバッグするための内部ページのスクリーンショット。
HID をデバッグするための Chrome の内部ページ

HID デバイス情報を人が読める形式でダンプするには、HID エクスプローラを使用します。使用状況の値から各 HID 使用状況の名前にマッピングされます。

ほとんどの Linux システムでは、HID デバイスはデフォルトで読み取り専用権限にマッピングされています。Chrome で HID デバイスを開けるようにするには、新しい udev ルールを追加する必要があります。/etc/udev/rules.d/50-yourdevicename.rules に次の内容のファイルを作成します。

KERNEL=="hidraw*", ATTRS{idVendor}=="[yourdevicevendor]", MODE="0664", GROUP="plugdev"

上記の行で、デバイスが Nintendo Switch の Joy-Con などの場合、[yourdevicevendor]057e です。ATTRS{idProduct} を追加して、より具体的なルールを構成することもできます。user が「plugdev」グループのメンバーであることを確認してください。その後、デバイスを再接続します。

ブラウザ サポート

WebHID API は、Chrome 89 のすべてのデスクトップ プラットフォーム(ChromeOS、Linux、macOS、Windows)で利用できます。

デモ

WebHID のデモについては、web.dev/hid-examples をご覧ください。見てみよう!

セキュリティとプライバシー

仕様作成者は、強力なウェブ プラットフォーム機能へのアクセスの制御で定義されている基本原則(ユーザー制御、透明性、エルゴノミクスなど)を使用して WebHID API を設計、実装しています。この API を使用できるかどうかは、主に一度に 1 つの HID デバイスのみへのアクセスを許可する権限モデルによって制限されます。ユーザー プロンプトに応じて、ユーザーは特定の HID デバイスを選択する必要があります。

セキュリティのトレードオフについては、WebHID 仕様のセキュリティとプライバシーに関する考慮事項のセクションをご覧ください。

さらに、Chrome は各トップレベル コレクションの使用状況を検査し、トップレベル コレクションに保護された用途(汎用キーボード、マウスなど)がある場合、ウェブサイトはそのコレクションで定義されたレポートを送受信できなくなります。保護されている用途の完全なリストは一般公開されています。

なお、セキュリティに注意を要する HID デバイス(より強力な認証に使用される FIDO HID デバイスなど)も Chrome でブロックされます。USB ブロックリストHID ブロックリストのファイルをご覧ください。

フィードバック

Chrome チームでは、WebHID API に関するご意見やご感想をお待ちしております。

API の設計についてお聞かせください

API に想定外の動作はありますか?あるいは、アイデアを実装する必要があるメソッドやプロパティが不足しているのでしょうか。

WebHID API GitHub リポジトリで仕様の問題を提出するか、既存の問題に意見を追加します。

実装に関する問題を報告する

Chrome の実装にバグが見つかりましたか?それとも 実装が仕様と異なっているか?

WebHID のバグを報告する方法をご確認ください。できる限り詳細な情報を記載し、バグを再現するための簡単な手順を記載し、[コンポーネント] を Blink>HID に設定します。Glitch は、すばやく簡単に再現する場合に適しています。

応援する

WebHID API を使用する予定はありますか?一般公開のサポートにより、Chrome チームが機能に優先順位を付け、他のブラウザ ベンダーがそれらの機能をサポートすることがいかに重要であるかを示します。

ハッシュタグ #WebHID を使用して @ChromiumDev 宛てにツイートを送信し、どこでどのように使用しているかをお知らせください。

関連情報

謝辞

この記事をレビューしてくれた Matt ReynoldsJoe Medley に感謝します。 赤と青の Nintendo Switch の写真(Sara Kurfeates)、黒と銀のノートパソコン(Athul Cyriac Ajay)(Unsplash)