ウェブ上の USB デバイスへのアクセス

WebUSB API により、USB をウェブに持ち込むことで、より安全で使いやすくなります。

François Beaufort
François Beaufort

簡潔に「USB」と言えば、キーボード、マウス、オーディオ、ビデオ、ストレージ デバイスをすぐに思い浮かべるでしょう。確かにそうですが、他の種類の Universal Serial Bus(USB)デバイスもあります。

このような標準化されていない USB デバイスを利用する場合は、ハードウェア ベンダーがプラットフォーム固有のドライバと SDK を記述する必要があります。残念なことに、これまでは、このプラットフォーム固有のコードが原因で、ウェブでこれらのデバイスが使用できなくなっていました。これが、USB デバイス サービスをウェブに公開する方法を提供するために WebUSB API が作成された理由の一つです。ハードウェア メーカーは、この API を使用して、自社のデバイス用にクロス プラットフォーム JavaScript SDK を構築できます。

しかし、最も重要なのは、USB をウェブに導入することで安全で使いやすいものになることです。

WebUSB API で想定される動作を見てみましょう。

  1. USB デバイスを購入します。
  2. パソコンに接続します。通知がすぐに表示され、そのデバイスのリンク先となるウェブサイトが示されます。
  3. この通知をクリックすると、ウェブサイトが完成し、利用できるようになりました。
  4. クリックして接続すると、Chrome に USB デバイス選択ツールが表示され、デバイスを選択できます。

成功です。

WebUSB API を使用しないと、この手順はどのようになるでしょうか。

  1. プラットフォーム固有のアプリケーションをインストールする。
  2. 自分のオペレーティング システムでサポートされている場合は、正しいファイルをダウンロードしたことを確認します。
  3. ものをインストールします。運が良ければ、インターネットからドライバやアプリのインストールについて警告する OS の恐ろしいプロンプトやポップアップは表示されなくなります。運が悪ければ、インストールしたドライバやアプリケーションが故障してパソコンに損害を与えます。(ウェブは、不具合のあるウェブサイトを含むように作られています)。
  4. この機能を一度だけ使用する場合、コードは削除するまでパソコンに保存されたままになります。(ウェブの場合、未使用のスペースは最終的に再利用されます)。

始める前に

この記事は、USB の仕組みに関する基本的な知識があることを前提としています。そうでない場合は、NutShell の USB を読むことをおすすめします。USB の背景情報については、公式の USB 仕様をご覧ください。

WebUSB API は Chrome 61 で利用できます。

オリジン トライアルで利用可能

Google では、実際に WebUSB API を使用しているデベロッパーから可能な限り多くのフィードバックをいただくため、この機能をオリジン トライアルとして Chrome 54 と Chrome 57 で追加しました。

最新のトライアルは 2017 年 9 月に終了しました。

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

HTTPS のみ

この機能により、安全なコンテキストでのみ動作します。つまり、TLS を念頭に置いて構築する必要があります。

必要なユーザー操作

セキュリティ対策として、navigator.usb.requestDevice() はタップやマウスクリックなどのユーザー操作でのみ呼び出すことができます。

権限ポリシー

権限ポリシーとは、デベロッパーがさまざまなブラウザ機能や API を選択的に有効または無効にできるメカニズムです。これは、HTTP ヘッダーや iframe の「allow」属性で定義できます。

Navigator オブジェクトで usb 属性を公開するかどうか、つまり WebUSB を許可するかどうかを制御する権限ポリシーを定義できます。

WebUSB が許可されていないヘッダー ポリシーの例を次に示します。

Feature-Policy: fullscreen "*"; usb "none"; payment "self" https://payment.example.com

USB が許可されているコンテナ ポリシーの別の例を次に示します。

<iframe allowpaymentrequest allow="usb; fullscreen"></iframe>

コーディングを始める

WebUSB API は JavaScript の Promise に大きく依存しています。まだよく理解していない場合は、こちらの Promise のチュートリアルをご覧ください。さらに、() => {} は単純に ECMAScript 2015 のアロー関数です。

USB デバイスへのアクセス

navigator.usb.requestDevice() を使用して接続された 1 つの USB デバイスを選択するようにユーザーに促すか、navigator.usb.getDevices() を呼び出して、ウェブサイトがアクセスを許可されているすべての接続済み USB デバイスのリストを取得します。

navigator.usb.requestDevice() 関数は、filters を定義する必須の JavaScript オブジェクトを受け取ります。これらのフィルタは、USB デバイスを特定のベンダー ID(vendorId)と、オプションでプロダクト ID(productId)と照合するために使用されます。classCodeprotocolCodeserialNumbersubclassCode キーもここで定義できます。

Chrome での USB デバイスのユーザー プロンプトのスクリーンショット
USB デバイスのユーザー プロンプト。

たとえば、オリジンを許可するように構成された、接続された Arduino デバイスにアクセスする方法は、次のとおりです。

navigator.usb.requestDevice({ filters: [{ vendorId: 0x2341 }] })
.then(device => {
  console.log(device.productName);      // "Arduino Micro"
  console.log(device.manufacturerName); // "Arduino LLC"
})
.catch(error => { console.error(error); });

質問する前に、私はこの 0x2341 の 16 進数を魔法のように思いついたわけではありません。こちらの USB ID のリストで「Arduino」を検索しました。

上記のフルフィルメント済み Promise で返される USB device には、サポートされている USB バージョン、最大パケットサイズ、ベンダー ID、プロダクト ID、デバイスが持つ可能な設定の数など、デバイスに関する基本的かつ重要な情報が含まれています。基本的に、デバイス USB 記述子のすべてのフィールドが含まれます。

// Get all connected USB devices the website has been granted access to.
navigator.usb.getDevices().then(devices => {
  devices.forEach(device => {
    console.log(device.productName);      // "Arduino Micro"
    console.log(device.manufacturerName); // "Arduino LLC"
  });
})

なお、USB デバイスで WebUSB のサポートがアナウンスされ、ランディング ページ URL が定義されている場合は、USB デバイスが接続されたときに Chrome に永続的な通知が表示されます。この通知をクリックすると、ランディング ページが開きます。

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

Arduino USB ボードに話しかける

では、WebUSB 互換の Arduino ボードから USB ポートを介して通信することがどれほど簡単かを見てみましょう。スケッチを WebUSB で有効にする手順については、https://github.com/webusb/arduino をご覧ください。

WebUSB デバイスのメソッドについては、この記事の後半で説明します。

let device;

navigator.usb.requestDevice({ filters: [{ vendorId: 0x2341 }] })
.then(selectedDevice => {
    device = selectedDevice;
    return device.open(); // Begin a session.
  })
.then(() => device.selectConfiguration(1)) // Select configuration #1 for the device.
.then(() => device.claimInterface(2)) // Request exclusive control over interface #2.
.then(() => device.controlTransferOut({
    requestType: 'class',
    recipient: 'interface',
    request: 0x22,
    value: 0x01,
    index: 0x02})) // Ready to receive data
.then(() => device.transferIn(5, 64)) // Waiting for 64 bytes of data from endpoint #5.
.then(result => {
  const decoder = new TextDecoder();
  console.log('Received: ' + decoder.decode(result.data));
})
.catch(error => { console.error(error); });

私が使用している WebUSB ライブラリは、標準の USB シリアル プロトコルに基づく 1 つのサンプル プロトコルを実装しているだけであり、メーカーは任意のエンドポイント セットとタイプを作成できます。制御転送は、小規模な構成コマンドがバスの優先度を持ち、構造が明確に定義されているため、特に適しています。

これが Arduino ボードにアップロードされたスケッチです。

// Third-party WebUSB Arduino library
#include <WebUSB.h>

WebUSB WebUSBSerial(1 /* https:// */, "webusb.github.io/arduino/demos");

#define Serial WebUSBSerial

void setup() {
  Serial.begin(9600);
  while (!Serial) {
    ; // Wait for serial port to connect.
  }
  Serial.write("WebUSB FTW!");
  Serial.flush();
}

void loop() {
  // Nothing here for now.
}

上記のサンプルコードで使用されているサードパーティの WebUSB Arduino ライブラリは、基本的に次の 2 つのことを行います。

  • デバイスは WebUSB デバイスとして機能し、Chrome でランディング ページ URL を読み取ることができます。
  • WebUSB Serial API を公開しています。この API を使用してデフォルトの API をオーバーライドできます。

JavaScript コードをもう一度確認します。ユーザーが選択した device を取得すると、device.open() はプラットフォーム固有のすべての手順を実行して、USB デバイスとのセッションを開始します。次に、device.selectConfiguration() を使用して、利用可能な USB 構成を選択する必要があります。構成では、デバイスへの電力供給方法、最大消費電力、インターフェース数を指定します。インターフェースについては、device.claimInterface() を使用した排他的アクセスもリクエストする必要があります。これは、インターフェースが要求されている場合に、インターフェースまたは関連するエンドポイントにのみデータを転送できるためです。最後に、WebUSB Serial API を介して通信できるように、適切なコマンドで Arduino デバイスをセットアップするために、device.controlTransferOut() を呼び出す必要があります。

そこから、device.transferIn() はデバイスに対して一括転送を実行し、ホストが一括データを受信する準備ができたことを通知します。その後、適切に解析する必要がある DataView data を含む result オブジェクトによって Promise が処理されます。

USB になじみのある方であれば、どれもおなじみのものです。

もっと必要です

WebUSB API を使用すると、以下のすべての USB 転送/エンドポイント タイプを操作できます。

  • USB デバイスに対して構成パラメータまたはコマンド パラメータを送受信するために使用される CONTROL 転送は、controlTransferIn(setup, length)controlTransferOut(setup, data) で処理されます。
  • 少量の時間センシティブ データに使用される INTERRUPT 転送は、transferIn(endpointNumber, length)transferOut(endpointNumber, data) を使用した一括転送と同じ方法で処理されます。
  • 動画や音声などのデータ ストリームに使用される ISOCHRONOUS 転送は、isochronousTransferIn(endpointNumber, packetLengths)isochronousTransferOut(endpointNumber, data, packetLengths) で処理されます。
  • 時間的制約のない大量のデータを信頼性の高い方法で転送するために使用される一括転送は、transferIn(endpointNumber, length)transferOut(endpointNumber, data) で処理されます。

また、Mike Tsao の WebLight プロジェクトも参考になります。WebUSB API 用に設計された USB 制御 LED デバイス(ここでは Arduino は使用していません)を構築する基礎的なサンプルです。ハードウェア、ソフトウェア、ファームウェアがあります

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

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

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

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

if ("usb" in navigator && "forget" in USBDevice.prototype) {
  // forget() is supported.
}

転送サイズの制限

一部のオペレーティング システムでは、保留中の USB トランザクションに含めることができるデータの量に上限があります。データを小さなトランザクションに分割し、一度に少数のトランザクションのみを送信することで、このような制限を回避できます。また、使用するメモリ量も削減され、転送が完了した進行状況をアプリから報告できるようになります。

エンドポイントに送信された複数の転送は常に順番に実行されるため、USB 転送間のレイテンシを回避するためにキューに登録された複数のチャンクを送信して、スループットを改善できます。チャンクが完全に送信されるたびに、以下のヘルパー関数の例に記載されているように、追加のデータを提供する必要があることがコードに通知されます。

const BULK_TRANSFER_SIZE = 16 * 1024; // 16KB
const MAX_NUMBER_TRANSFERS = 3;

async function sendRawPayload(device, endpointNumber, data) {
  let i = 0;
  let pendingTransfers = [];
  let remainingBytes = data.byteLength;
  while (remainingBytes > 0) {
    const chunk = data.subarray(
      i * BULK_TRANSFER_SIZE,
      (i + 1) * BULK_TRANSFER_SIZE
    );
    // If we've reached max number of transfers, let's wait.
    if (pendingTransfers.length == MAX_NUMBER_TRANSFERS) {
      await pendingTransfers.shift();
    }
    // Submit transfers that will be executed in order.
    pendingTransfers.push(device.transferOut(endpointNumber, chunk));
    remainingBytes -= chunk.byteLength;
    i++;
  }
  // And wait for last remaining transfers to complete.
  await Promise.all(pendingTransfers);
}

ヒント

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

Chrome で WebUSB をデバッグするデバイスのログページのスクリーンショット
WebUSB API をデバッグするための Chrome のデバイスログ ページ

内部ページ about://usb-internals も便利です。この内部ページを使用すると、仮想 WebUSB デバイスの接続と接続解除をシミュレートできます。これは、実際のハードウェアを使用せずに UI をテストする場合に便利です。

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

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

SUBSYSTEM=="usb", ATTR{idVendor}=="[yourdevicevendor]", MODE="0664", GROUP="plugdev"

ここで、デバイスが Arduino の場合、[yourdevicevendor]2341 です。ATTR{idProduct} を追加して、より具体的なルールを作成することもできます。userplugdev グループのメンバーであることを確認します。その後、デバイスを再接続してください。

リソース

ハッシュタグ #WebUSB を使用して @ChromiumDev 宛てにツイートを送信し、使用場所と使用方法をお知らせください。

謝辞

この記事をレビューしてくれた Joe Medley に感謝します。