ウェブ上の 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. デバイスを設置します。運が良ければ、インターネットからドライバやアプリケーションをインストールする際の警告メッセージやポップアップは表示されません。運が悪いと、インストールしたドライバやアプリケーションが誤動作し、パソコンに損傷を与える可能性があります。(ウェブは、機能しないウェブサイトを含むように構築されています)。
  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 のチュートリアルをご覧ください。もう 1 つ、() => {} は単なる ECMAScript 2015 のアロー関数です。

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

navigator.usb.requestDevice() を使用して接続されている 1 つの USB デバイスを選択するようユーザーにプロンプトを表示するか、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 ポート経由で簡単に通信する方法を説明します。https://github.com/webusb/arduino の手順に沿って、スケッチを WebUSB 対応にします。

心配はいりません。この記事の後半で、以下に記載するすべての 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() で排他的アクセスをリクエストする必要があります。最後に、device.controlTransferOut() を呼び出して、WebUSB Serial API を介して通信するための適切なコマンドで Arduino デバイスを設定する必要があります。

そこから、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 転送と同じ方法で処理されます。
  • 動画や音声などのデータのストリーミングに使用される同期転送は、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 に感謝いたします。