WebUSB 用デバイスの作成

WebUSB API を最大限に活用するデバイスを構築する。

この記事では、WebUSB API を最大限に活用できるデバイスを作成する方法について説明します。API 自体の概要については、ウェブで USB デバイスにアクセスするをご覧ください。

背景

ユニバーサル シリアル バス(USB)は、周辺機器をデスクトップ コンピューティング デバイスやモバイル コンピューティング デバイスに接続するための最も一般的な物理インターフェースになっています。USB 仕様には、バスの電気特性とデバイスとの通信の一般的なモデルの定義に加えて、デバイスクラスの仕様が含まれています。これらは、デバイス メーカーが実装できる特定のデバイスクラス(ストレージ、オーディオ、ビデオ、ネットワーキングなど)の一般的なモデルです。これらのデバイスクラス仕様の利点は、オペレーティング システム ベンダーがクラス仕様に基づいて単一のドライバを実装できること(「クラスドライバ」)と、そのクラスを実装するすべてのデバイスがサポートされることです。これは、各メーカーが独自のデバイス ドライバを記述する必要があった場合と比べて大きな改善でした。

ただし、一部のデバイスは、これらの標準化されたデバイスクラスのいずれにも該当しません。メーカーは、ベンダー固有のクラスを実装しているデバイスとしてラベル付けすることもできます。この場合、OS はベンダーのドライバ パッケージに提供されている情報に基づいて、読み込むデバイス ドライバを選択します。通常は、特定のベンダー固有のプロトコルを実装することが知られているベンダー ID とプロダクト ID のセットです。

USB のもう 1 つの特徴は、デバイスが接続先のホストに複数のインターフェースを提供できることです。各インターフェースは、標準化されたクラスを実装することも、ベンダー固有のクラスを実装することもできます。オペレーティング システムがデバイスを処理する適切なドライバを選択すると、各インターフェースは異なるドライバによって要求できます。たとえば、USB ウェブカメラは通常、2 つのインターフェースを提供します。1 つは USB ビデオクラスを実装し(カメラ用)、もう 1 つは USB 音声クラスを実装します(マイク用)。オペレーティング システムは、単一の「ウェブカメラ ドライバ」を読み込むのではなく、デバイスの個別の機能を担当する独立したビデオ クラス ドライバとオーディオ クラス ドライバを読み込みます。このインターフェース クラスの構成により、柔軟性が向上します。

API の基本

標準の USB クラスの多くには、対応するウェブ API があります。たとえば、ページは getUserMedia() を使用して動画クラスのデバイスから動画をキャプチャしたり、KeyboardEvents または PointerEvents をリッスンするか、Gamepad または WebHID API を使用して、ヒューマン インターフェース(HID)クラスのデバイスから入力イベントを受信したりできます。すべてのデバイスが標準化されたクラス定義を実装しているわけではないように、すべてのデバイスが既存のウェブ プラットフォーム API に対応する機能を実装しているわけではありません。このような場合、WebUSB API は、サイトがベンダー固有のインターフェースを宣言し、ページ内から直接そのサポートを実装できるようにすることで、そのギャップを埋めることができます。

WebUSB 経由でデバイスにアクセスするための具体的な要件は、オペレーティング システムが USB デバイスを管理する方法の違いにより、プラットフォームによって若干異なります。ただし、基本的な要件として、ページが制御するインターフェースをデバイスがすでに宣言していないことが挙げられます。これは、OS ベンダーが提供する汎用クラス ドライバまたはベンダーが提供するデバイス ドライバのいずれかです。USB デバイスは複数のインターフェースを提供でき、それぞれに独自のドライバがある場合があります。そのため、一部のインターフェースをドライバが要求し、他のインターフェースをブラウザがアクセスできるようにするデバイスを構築できます。

たとえば、ハイエンドの USB キーボードは、オペレーティング システムの入力サブシステムによって要求される HID クラス インターフェースと、WebUSB で使用可能で、構成ツールで使用できるベンダー固有のインターフェースを提供できます。このツールはメーカーのウェブサイトで提供され、ユーザーはプラットフォーム固有のソフトウェアをインストールしなくても、マクロキーや照明効果など、デバイスの動作の一部を変更できます。このようなデバイスの構成記述子は次のようになります。

フィールド 説明
構成記述子
0x09 bLength この記述子のサイズ
0x02 bDescriptorType 構成記述子
0x0039 wTotalLength この記述子シリーズの合計長
0x02 bNumInterfaces インターフェースの数
0x01 bConfigurationValue 構成 1
0x00 iConfiguration 構成名(なし)
0b1010000 bmAttributes リモート ウェイクアップ機能付きの電池駆動デバイス
0x32 bMaxPower 最大電力は 2 mA 単位で表されます
インターフェース記述子
0x09 bLength この記述子のサイズ
0x04 bDescriptorType インターフェース記述子
0x00 bInterfaceNumber インターフェース 0
0x00 bAlternateSetting 代替設定 0(デフォルト)
0x01 bNumEndpoints 1 個のエンドポイント
0x03 bInterfaceClass HID インターフェース クラス
0x01 bInterfaceSubClass ブート インターフェースのサブクラス
0x01 bInterfaceProtocol キーボード
0x00 iInterface インターフェース名(なし)
HID 記述子
0x09 bLength この記述子のサイズ
0x21 bDescriptorType HID 記述子
0x0101 bcdHID HID バージョン 1.1
0x00 bCountryCode ハードウェアの対象国
0x01 bNumDescriptors 続く HID クラス記述子の数
0x22 bDescriptorType レポート記述子のタイプ
0x003F wDescriptorLength レポート記述子の合計長
エンドポイント記述子
0x07 bLength この記述子のサイズ
0x05 bDescriptorType エンドポイント記述子
0b10000001 bEndpointAddress エンドポイント 1(インド)
0b00000011 bmAttributes 応答を中断する
0x0008 wMaxPacketSize 8 バイト パケット
0x0A bInterval 10 ミリ秒間隔
インターフェース記述子
0x09 bLength この記述子のサイズ
0x04 bDescriptorType インターフェース記述子
0x01 bInterfaceNumber インターフェース 1
0x00 bAlternateSetting 代替設定 0(デフォルト)
0x02 bNumEndpoints 2 つのエンドポイント
0xFF bInterfaceClass ベンダー固有のインターフェース クラス
0x00 bInterfaceSubClass
0x00 bInterfaceProtocol
0x00 iInterface インターフェース名(なし)
エンドポイント記述子
0x07 bLength この記述子のサイズ
0x05 bDescriptorType エンドポイント記述子
0b10000010 bEndpointAddress エンドポイント 1(インド)
0b00000010 bmAttributes 一括
0x0040 wMaxPacketSize 64 バイトのパケット
0x00 bInterval 一括エンドポイントの場合はなし
エンドポイント記述子
0x07 bLength この記述子のサイズ
0x05 bDescriptorType エンドポイント記述子
0b00000011 bEndpointAddress エンドポイント 3(OUT)
0b00000010 bmAttributes 一括
0x0040 wMaxPacketSize 64 バイトのパケット
0x00 bInterval 一括エンドポイントの場合はなし

構成記述子は、複数の記述子が連結されたものです。各フィールドは bLength フィールドと bDescriptorType フィールドで始まるため、識別できます。最初のインターフェースは、関連付けられた HID 記述子と、入力イベントをオペレーティング システムに配信するために使用される単一のエンドポイントを持つ HID インターフェースです。2 つ目のインターフェースはベンダー固有のインターフェースで、2 つのエンドポイントがあり、デバイスにコマンドを送信してレスポンスを受信するために使用できます。

WebUSB 記述子

WebUSB はファームウェアを変更しなくても多くのデバイスで動作しますが、追加機能を有効にするには、WebUSB のサポートを示す特定の記述子でデバイスをマークする必要があります。たとえば、デバイスが電源に接続されているときにブラウザがユーザーを誘導するランディング ページの URL を指定できます。

Chrome の WebUSB 通知のスクリーンショット
WebUSB 通知。

バイナリ デバイス オブジェクト ストア(BOS)は USB 3.0 で導入されたコンセプトですが、バージョン 2.1 の一部として USB 2.0 デバイスにもバックポートされています。WebUSB のサポートを宣言するには、まず BOS 記述子に次のプラットフォーム機能記述子を追加します。

フィールド 説明
バイナリ デバイスのオブジェクト ストア記述子
0x05 bLength この記述子のサイズ
0x0F bDescriptorType バイナリ デバイスのオブジェクト ストア記述子
0x001D wTotalLength この記述子シリーズの合計長
0x01 bNumDeviceCaps BOS 内のデバイス機能記述子の数
WebUSB プラットフォームの機能記述子
0x18 bLength この記述子のサイズ
0x10 bDescriptorType デバイスの機能記述子
0x05 bDevCapabilityType プラットフォームの機能記述子
0x00 bReserved
{0x38, 0xB6, 0x08, 0x34, 0xA9, 0x09, 0xA0, 0x47, 0x8B, 0xFD, 0xA0, 0x76, 0x88, 0x15, 0xB6, 0x65} PlatformCapablityUUID リトル エンディアン形式の WebUSB プラットフォーム機能記述子 GUID
0x0100 bcdVersion WebUSB 記述子 バージョン 1.0
0x01 bVendorCode WebUSB の bRequest 値
0x01 iLandingPage ランディング ページの URL

プラットフォーム機能 UUID は、デバイスに関する基本情報を提供する WebUSB プラットフォーム機能記述子としてこれを識別します。ブラウザがデバイスの詳細情報を取得するには、bVendorCode 値を使用してデバイスに追加のリクエストを送信します。現在指定されているリクエストは、URL 記述子を返す GET_URL のみです。これらは文字列記述子に似ていますが、URL を最小のバイト数でエンコードするように設計されています。"https://google.com" の URL 記述子は次のようになります。

フィールド 説明
URL 記述子
0x0D bLength この記述子のサイズ
0x03 bDescriptorType URL 記述子
0x01 bScheme https://
"google.com" URL UTF-8 でエンコードされた URL コンテンツ

デバイスを初めて接続すると、ブラウザは次の標準 GET_DESCRIPTOR コントロール転送を発行して BOS 記述子を読み取ります。

bmRequestType bRequest wValue wIndex wLength データ(レスポンス)
0b10000000 0x06 0x0F00 0x0000 * BOS 記述子

このリクエストは通常 2 回行われます。最初のリクエストでは、ホストが大きな転送を commit せずに wTotalLength フィールドの値を検出できるように十分な大きさの wLength が使用され、2 回目のリクエストでは、完全な記述子の長さが判明したときに行われます。

WebUSB プラットフォーム キャパビティ記述子の iLandingPage フィールドがゼロ以外の値に設定されている場合、ブラウザは、bRequest をプラットフォーム キャパビティ記述子の bVendorCode 値に設定し、wValueiLandingPage 値に設定してコントロール転送を発行することで、WebUSB 固有の GET_URL リクエストを実行します。GET_URL のリクエスト コード(0x02)は wIndex に格納します。

bmRequestType bRequest wValue wIndex wLength データ(レスポンス)
0b11000000 0x01 0x0001 0x0002 * URL 記述子

このリクエストは、最初に読み取られる記述子の長さをプローブするために、2 回発行される場合があります。

プラットフォーム固有の考慮事項

WebUSB API は USB デバイスにアクセスするための一貫したインターフェースを提供しようとしていますが、デベロッパーは、デバイスにアクセスするためのウェブブラウザの要件など、アプリに課せられる要件に注意する必要があります。

macOS

macOS では特別な設定は必要ありません。WebUSB を使用するウェブサイトは、デバイスに接続し、カーネル ドライバまたは別のアプリケーションによって要求されていないインターフェースを要求できます。

Linux

Linux は macOS に似ていますが、ほとんどのディストリビューションでは、デフォルトで USB デバイスを開く権限を持つユーザー アカウントが設定されていません。udev というシステム デーモンが、デバイスにアクセスできるユーザーとグループを割り当てる役割を担っています。このようなルールでは、指定されたベンダー ID と製品 ID に一致するデバイスの所有権が、周辺機器にアクセスできるユーザーの共通グループである plugdev グループに割り当てられます。

SUBSYSTEM=="usb", ATTR{idVendor}=="XXXX", ATTR{idProduct}=="XXXX", GROUP="plugdev"

XXXX は、デバイスのベンダー ID とプロダクト ID の 16 進数に置き換えます。たとえば、ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e11" は Nexus One スマートフォンに対応します。正しく認識されるようにするには、通常の「0x」接頭辞を付けて、すべて小文字で記述する必要があります。デバイスの ID を確認するには、コマンドライン ツール lsusb を実行します。

このルールは /etc/udev/rules.d ディレクトリのファイルに配置し、デバイスが接続されるとすぐに有効になります。udev を再起動する必要はありません。

Android

Android プラットフォームは Linux をベースとしていますが、システム構成を変更する必要はありません。デフォルトでは、オペレーティング システムにドライバが組み込まれていないデバイスは、ブラウザで使用できます。ただし、デバイスに接続する際にユーザーが追加の手順を行う必要があることに、デベロッパーは注意する必要があります。ユーザーが requestDevice() の呼び出しに応答してデバイスを選択すると、Chrome にデバイスへのアクセスを許可するかどうかを尋ねるメッセージが Android に表示されます。このプロンプトは、デバイスへの接続権限がすでに付与されているウェブサイトにユーザーが戻り、ウェブサイトが open() を呼び出す場合にも再び表示されます。

また、デフォルトで含まれるドライバが少ないため、デスクトップ Linux よりも Android でアクセスできるデバイスが増えます。たとえば、Android SDK にはシリアル デバイスと通信するための API がないため、USB-シリアル アダプタで一般的に実装されている USB CDC-ACM クラスが含まれていません。

ChromeOS

ChromeOS も Linux をベースとしており、システム構成の変更も必要ありません。permission_broker サービスは USB デバイスへのアクセスを制御し、未使用のインターフェースが 1 つ以上ある限り、ブラウザが USB デバイスにアクセスできるようにします。

Windows

Windows ドライバモデルには追加の要件があります。上記のプラットフォームとは異なり、ドライバが読み込まれていない場合でも、ユーザー アプリケーションから USB デバイスを開く機能はデフォルトではありません。代わりに、アプリケーションがデバイスにアクセスするために使用するインターフェースを提供するために、特別なドライバ(WinUSB)を読み込む必要があります。これは、システムにインストールされているカスタム ドライバ情報ファイル(INF)を使用するか、デバイス ファームウェアを変更して、列挙中に Microsoft OS 互換性記述子を提供する方法で行うことができます。

ドライバ情報ファイル(INF)

ドライバ情報ファイルは、デバイスを初めて検出したときに Windows に何をすべきかを指示します。ユーザーのシステムには WinUSB ドライバがすでに含まれているため、必要なのは、INF ファイルでベンダー ID とプロダクト ID をこの新しいインストール ルールに関連付けることだけです。以下のファイルは基本的な例です。.inf 拡張子のファイルに保存し、「X」でマークされたセクションを変更してから、右クリックしてコンテキスト メニューから [インストール] を選択します。

[Version]
Signature   = "$Windows NT$"
Class       = USBDevice
ClassGUID   = {88BAE032-5A81-49f0-BC3D-A4FF138216D6}
Provider    = %ManufacturerName%
CatalogFile = WinUSBInstallation.cat
DriverVer   = 09/04/2012,13.54.20.543

; ========== Manufacturer/Models sections ===========

[Manufacturer]
%ManufacturerName% = Standard,NTx86,NTia64,NTamd64

[Standard.NTx86]
%USB\MyCustomDevice.DeviceDesc% = USB_Install,USB\VID_XXXX&PID_XXXX

[Standard.NTia64]
%USB\MyCustomDevice.DeviceDesc% = USB_Install,USB\VID_XXXX&PID_XXXX

[Standard.NTamd64]
%USB\MyCustomDevice.DeviceDesc% = USB_Install,USB\VID_XXXX&PID_XXXX

; ========== Class definition ===========

[ClassInstall32]
AddReg = ClassInstall_AddReg

[ClassInstall_AddReg]
HKR,,,,%ClassName%
HKR,,NoInstallClass,,1
HKR,,IconPath,%REG_MULTI_SZ%,"%systemroot%\system32\setupapi.dll,-20"
HKR,,LowerLogoVersion,,5.2

; =================== Installation ===================

[USB_Install]
Include = winusb.inf
Needs   = WINUSB.NT

[USB_Install.Services]
Include = winusb.inf
Needs   = WINUSB.NT.Services

[USB_Install.HW]
AddReg = Dev_AddReg

[Dev_AddReg]
HKR,,DeviceInterfaceGUIDs,0x10000,"{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}"

; =================== Strings ===================

[Strings]
ManufacturerName              = "Your Company Name Here"
ClassName                     = "Your Company Devices"
USB\MyCustomDevice.DeviceDesc = "Your Device Name Here"

[Dev_AddReg] セクションでは、デバイスの DeviceInterfaceGUID のセットを構成します。アプリケーションが Windows API を介してデバイス インターフェースを見つけて接続するには、すべてのデバイス インターフェースに GUID が必要です。New-Guid PowerShell コマンドレットまたはオンライン ツールを使用して、ランダムな GUID を生成します。

開発目的では、Zadig ツールを使用して、USB インターフェース用に読み込まれたドライバを WinUSB ドライバに簡単に置き換えることができます。

Microsoft OS 互換性記述子

上記の INF ファイル方式は、すべてのユーザーのマシンを事前に構成する必要があるため、煩雑です。Windows 8.1 以降では、カスタム USB 記述子を使用する方法もあります。これらの記述子は、デバイスが最初に接続されたときに Windows オペレーティング システムに情報を提供します。通常、この情報は INF ファイルに含まれます。

WebUSB 記述子を設定したら、Microsoft の OS 互換性記述子も簡単に追加できます。まず、この追加のプラットフォーム機能記述子を使用して BOS 記述子を拡張します。これを考慮して、wTotalLengthbNumDeviceCaps を更新してください。

フィールド 説明
Microsoft OS 2.0 プラットフォームの機能記述子
0x1C bLength この記述子のサイズ
0x10 bDescriptorType デバイスの機能記述子
0x05 bDevCapabilityType プラットフォームの機能記述子
0x00 bReserved
{0xDF, 0x60, 0xDD, 0xD8, 0x89, 0x45, 0xC7, 0x4C, 0x9C, 0xD2, 0x65, 0x9D, 0x9E, 0x64, 0x8A, 0x9F} PlatformCapablityUUID リトル エンディアン形式の Microsoft OS 2.0 プラットフォーム互換性記述子 GUID
0x06030000 dwWindowsVersion 対応している最小の Windows バージョン(Windows 8.1)
0x00B2 wMSOSDescriptorSetTotalLength 記述子セットの合計長
0x02 bMS_VendorCode 追加の Microsoft 記述子を取得するための bRequest 値
0x00 bAltEnumCode デバイスが代替の列挙に対応していない

WebUSB デスクリプタと同様に、これらのデスクリプタに関連する制御転送で使用される bRequest 値を選択する必要があります。この例では 0x02 を選択しました。wIndex に配置される 0x07 は、デバイスから Microsoft OS 2.0 記述子セットを取得するコマンドです。

bmRequestType bRequest wValue wIndex wLength データ(レスポンス)
0b11000000 0x02 0x0000 0x0007 * MS OS 2.0 記述子セット

USB デバイスには複数の機能があるため、記述子セットの最初の部分には、続くプロパティが関連付けられている機能を記述します。次の例では、複合デバイスのインターフェース 1 を構成します。記述子には、このインターフェースに関する 2 つの重要な情報が OS に提供されます。互換 ID 記述子は、このデバイスが WinUSB ドライバと互換性があることを Windows に伝えます。レジストリ プロパティ記述子は、上記の INF の例の [Dev_AddReg] セクションと同様に機能し、この関数にデバイス インターフェース GUID を割り当てるようにレジストリ プロパティを設定します。

フィールド 説明
Microsoft OS 2.0 記述子セット ヘッダー
0x000A wLength この記述子のサイズ
0x0000 wDescriptorType 記述子セット ヘッダー記述子
0x06030000 dwWindowsVersion 対応している最小の Windows バージョン(Windows 8.1)
0x00B2 wTotalLength 記述子セットの合計長
Microsoft OS 2.0 構成サブセット ヘッダー
0x0008 wLength この記述子のサイズ
0x0001 wDescriptorType 構成サブセット ヘッダーの説明。
0x00 bConfigurationValue 構成 1 に適用されます(通常は 1 からインデックス付けされる構成ですが、0 からインデックス付けされます)。
0x00 bReserved 0 に設定する必要があります
0x00A8 wTotalLength このヘッダーを含むサブセットの合計長
Microsoft OS 2.0 関数サブセット ヘッダー
0x0008 wLength この記述子のサイズ
0x0002 wDescriptorType 関数サブセット ヘッダー記述子
0x01 bFirstInterface 関数の最初のインターフェース
0x00 bReserved 0 に設定する必要があります
0x00A0 wSubsetLength このヘッダーを含むサブセットの合計長
Microsoft OS 2.0 互換 ID 記述子
0x0014 wLength この記述子のサイズ
0x0003 wDescriptorType 互換性のある ID 記述子
"WINUSB\0\0" CompatibileID 8 バイトにパディングされた ASCII 文字列
"\0\0\0\0\0\0\0\0" SubCompatibleID 8 バイトにパディングされた ASCII 文字列
Microsoft OS 2.0 レジストリ プロパティ記述子
0x0084 wLength この記述子のサイズ
0x0004 wDescriptorType レジストリ プロパティ記述子
0x0007 wPropertyDataType REG_MULTI_SZ
0x002A wPropertyNameLength プロパティ名の長さ
"DeviceInterfaceGUIDs\0" PropertyName UTF-16LE でエンコードされた null 終端子を含むプロパティ名
0x0050 wPropertyDataLength プロパティ値の長さ
"{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}\0\0" PropertyData UTF-16LE でエンコードされた GUID と 2 つの null 終端子

Windows は、この情報をデバイスにクエリするのは 1 回だけです。デバイスが有効な記述子で応答しなかった場合、次回デバイスが接続されたときに再度確認されることはありません。Microsoft は、デバイスの列挙時に作成されるレジストリ エントリを説明するUSB デバイス レジストリ エントリのリストを提供しています。テストでは、デバイス用に作成されたエントリを削除して、Windows が記述子の再読み取りを試行するようにします。

詳細については、これらの記述子の使用方法に関する Microsoft のブログ投稿をご覧ください。

WebUSB 記述子と Microsoft OS 記述子の両方を含む WebUSB 対応デバイスを実装するコードの例は、次のプロジェクトにあります。