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

WebUSB API は、USB をウェブに移行することで、USB をより安全かつ簡単に使用できるようにします。

François Beaufort
François Beaufort

「USB」と簡単に言ったら、キーボード、マウス、オーディオ機器、ビデオ機器、ストレージ デバイスをすぐに思い浮かべるでしょう。ご指摘のとおりですが、他の種類のユニバーサル シリアル バス(USB)デバイスもあります。

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

最も重要なのは、USB をウェブに移行することで、USB をより安全かつ簡単に使用できるようにすることです。

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

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

完了です。

WebUSB API がなければ、この手順はどのようになるのでしょうか。

  1. プラットフォーム固有のアプリケーションをインストールします。
  2. オペレーティング システムでサポートされている場合でも、正しいものをダウンロードしたことを確認します。
  3. デバイスを設置します。運が良ければ、インターネットからドライバやアプリケーションをインストールすることに関する恐ろしい OS プロンプトやポップアップは表示されません。運が悪いと、インストールしたドライバやアプリケーションが誤動作し、パソコンに損傷を与える可能性があります。(ウェブは、機能しないウェブサイトを含むように構築されています)。
  4. この機能を 1 回だけ使用すると、コードは削除を検討するまでパソコンに保存されたままになります。(ウェブでは、未使用のスペースは最終的に再利用されます)。

始める前に

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

WebUSB API を使用可能にしました(Chrome 61)。

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

現場で WebUSB API を使用するデベロッパーからできるだけ多くのフィードバックを得るため、この機能は以前、Chrome 54 と Chrome 57 にオリジン トライアルとして追加されました。

最新の試験運用は 2017 年 9 月に正常に終了しました。

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

HTTPS のみ

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

ユーザー ジェスチャーが必要

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

権限に関するポリシー

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

usb 属性を Navigator オブジェクトに公開するかどうか、つまり 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 をよくご存じない場合は、こちらの優れた Promise のチュートリアルをご覧ください。さらに、() => {} は単に ECMAScript 2015 の Arrow 関数です。

USB デバイスにアクセスする

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

navigator.usb.requestDevice() 関数は、filters を定義する必須の JavaScript オブジェクトを受け取ります。これらのフィルタは、指定したベンダー ID(vendorId)と、必要に応じてプロダクト ID(productId)を持つ USB デバイスを照合するために使用されます。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、デバイスで可能な構成の数など、デバイスに関する基本的な情報と重要な情報が含まれています。基本的には、デバイス 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 つの処理を行います。

  • このデバイスは、Chrome でランディング ページ URL を読み取るための WebUSB デバイスとして機能します。
  • 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 オブジェクトでプロミスが満たされます。

USB に精通している方には、すべておなじみのものです。

もっと必要です

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

  • USB デバイスへの構成パラメータまたはコマンド パラメータの送受信に使用される CONTROL 転送は、controlTransferIn(setup, length)controlTransferOut(setup, data) で処理されます。
  • 少量のタイムセンシティブ データに使用される INTERRUPT 転送は、transferIn(endpointNumber, length)transferOut(endpointNumber, data) を使用した BULK 転送と同じ方法で処理されます。
  • 動画や音声などのデータのストリームに使用される 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 に感謝いたします。