一般的ではない 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 プロトコルは、ホストとデバイス間の双方向通信の標準で、インストール手順を簡素化するように設計されたものです。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 の写真。
Nintendo Switch Joy-Con デバイス

前の例に続き、次のコードは、ユーザーが Joy-Con Right デバイスで押したボタンを検出して、自宅で試せるようにする方法を示しています。

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 デバイスに機能レポートを送信するには、機能レポート(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 デバイスから機能レポートを受信するには、機能レポートに関連付けられた 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 グループのメンバーであることを確認します。その後、デバイスを再接続します。

ブラウザ サポート

Chrome 89 では、WebHID API をすべてのデスクトップ プラットフォーム(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)。