一般的ではない 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" イベントをリッスンして、受信した入力レポートを処理できます。これらのイベントには、HID データが DataView オブジェクト(data)、それが属する 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]}.`);
});

出力レポートを送信する

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

HID デバイスから機能レポートを受信するには、機能レポートに関連付けられた 8 ビットのレポート ID(reportId)を 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 デバイスへのアクセス権をクリーンアップできます。たとえば、多くのデバイスで共有されるコンピュータで使用される教育用ウェブ アプリケーションの場合、ユーザー作成の権限が大量に蓄積されると、ユーザー エクスペリエンスが低下します。

1 つの 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 に設定します。Glitch は、簡単な再現手順をすばやく共有するのに適しています。

クリエイターを応援する

WebHID API を使用する予定はありますか?一般公開されている機能へのサポートは、Chrome チームが機能の優先順位を決める際に役立ちます。また、他のブラウザ ベンダーに、その機能のサポートがどれほど重要であるかを示します。

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

関連情報

謝辞

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