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

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

François Beaufort
François Beaufort

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

おすすめのユースケース

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

一般的ではない HID デバイスにアクセスできないことは、代替の補助キーボード(Elgato Stream DeckJabra ヘッドセットX-keys など)や特殊なゲームパッドのサポートに関して特に問題となります。デスクトップ向けに設計されたゲームパッドは、ゲームパッドの入力(ボタン、ジョイスティック、トリガー)と出力(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.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" イベントをリッスンして、受信した入力レポートを処理できます。これらのイベントには、DataView オブジェクト(data)としての HID データ、それが属する HID デバイス(device)、入力レポートに関連付けられた 8 ビットのレポート ID(reportId)が含まれます。

赤と青の Nintendo Switch の写真。
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]}.`);
});

Pen webhid-joycon-button のデモをご覧ください。

出力レポートを送信する

出力レポートを 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));

webhid-joycon-rumble のデモをご覧ください。

機能レポートの送信と受信

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

黒と銀色のノートパソコンの写真。
ノートパソコンのキーボード

HID デバイスに機能レポートを送信するには、機能レポートに関連付けられた 8 ビットのレポート ID(reportId)とバイトを 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);
}

Pen の webhid-apple-keyboard-backlight デモをご覧ください。

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 での HID のデバッグは、内部ページ about://device-log を使用すると簡単です。このページでは、HID デバイスと USB デバイスに関連するすべてのイベントを 1 か所で確認できます。

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} を追加することもできます。userplugdev グループのメンバーであることを確認します。その後、デバイスを再接続するだけです。

ブラウザ サポート

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 に設定してください。

サポートを表示

WebHID API を使用する予定はありますか?公開サポートは、Chrome チームが機能の優先順位を決定するのに役立ち、他のブラウザ ベンダーにサポートの重要性を示すことができます。

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

関連情報

謝辞

この記事のレビューをしてくれた Matt ReynoldsJoe Medley に感謝します。赤と青の Nintendo Switch の写真は Sara Kurfeß 氏、黒と銀のノートパソコンの写真は Athul Cyriac Ajay 氏が Unsplash に投稿したものです。