Help us understand the problem. What is going on with this article?

WebUSBことはじめ

この記事はMuroran Institute of Technology Advent Calendar 2018の14日目の記事になりました。

はじめに

皆さん、WebUSB使ってますか1?WebUSBは標準化もされたばかりで、まだまだ知見も溜まってきていないところではありますが、Webの世界を物理デバイスから拡張できる可能性を秘めたAPIです。ちょっと使い方が難しいAPIではありますが、これが使えると世界がぐっと広がりますのでこの記事を読んでぜひご自身でいろんな工作を楽しんでみてください!

執筆環境

この記事は以下の環境で書かれています。WebUSBはOS標準のドライバが間に挟まると途端に動かなくなったりします。また、セキュリティ上接続が拒否されるデバイスもあります2。動作が確認されていない環境で動かすのは、場合によってはかなり難易度が高くなることに注意してください。また、USBデバイスの例として国内で普及しているNFCリーダーであるPaSoRi RC-S380を使用します。

  • macOS Mojave 10.14.2
  • Google Chrome 713
  • PaSoRi RC-S3804

WebUSBでデバイスと接続する

ではいよいよWebUSBを使ってデバイスに接続してみましょう。Google Chromeを起動して、JavaScriptコンソールを用意してください。

VendorIDとProductIDの特定

WebUSBでUSBデバイスに接続を要求するにはnavigator.usb.requestDevice({'filters': []})を叩けば良いのですが、そのままだと目的としていないデバイスまで一覧に表示されてしまいます。試しにやってみましょう。コンソールに以下のコードを打ち込むと、接続できるデバイスの一覧がポップアップ画面に表示されます。目的とするRC-S380以外にも、キーボードやトラックパッドまで表示されてしまっています。ユーザーが意図しないデバイスを選択しないためにも、フィルター条件としてVendorIDとProductIDを指定しておきます。

WebUSBの接続要求
// Promiseが返ってくるためawaitします
await navigator.usb.requestDevice({'filters': []})

フィルターなし状態でデバイスに接続しようとした場合

VendorIDとProductIDを特定するのは簡単で、Linuxの場合lsusbコマンドを、macOSの場合system_profiler SPUSBDataTypeコマンドを利用すれば、特定することができます。macOSでの例を以下に示します。

RC-S80のVendorIDとProductIDの特定
$ system_profiler SPUSBDataType
USB:

    USB 3.0 Bus:

      Host Controller Driver: AppleUSBXHCISPT

      ~中略~

        RC-S380/S:

          Product ID: 0x06c1
          Vendor ID: 0x054c  (Sony Corporation)
          Version: 1.11
          Serial Number: 0563527
          Speed: Up to 12 Mb/sec
          Manufacturer: SONY
          Location ID: 0x14200000 / 7
          Current Available (mA): 500
          Extra Operating Current (mA): 0

VendorIDとProductIDが特定できたら、今度はフィルターを設定してから叩いてみます。今度こそ目的のデバイスだけを表示することができるようになりました。

フィルターを設定してUSB接続要求を実施
const vendor_id = 0x054c
const product_id = 0x06c1
await navigator.usb.requestDevice(
  {
    'filters': [
      {'vendorId': vendor_id, 'product_id': product_id}
    ]
  }
)

フィルターを指定してデバイスに接続しようとした場合

デバイスの初期化

デバイスの接続要求をしたら、次はデバイスを開いて初期化する作業が続きます。USBデバイスの初期化には、ConfigurationInterfaceを調べる必要があります。まずは以下のコードをコンソールから叩いて、この2つの値を読み出します。以下のコードを入力してみましょう。device.configurationsを呼ぶと、そのUSBデバイスで利用可能なConfigurationの一覧がUSBConfigurationオブジェクトの配列で返ってきます。

ConfigurationとInterfaceの確認
// デバイスの接続要求
// ポップアップが出るので接続するデバイスを選択すること
const device = await navigator.usb.requestDevice(
  {
    'filters': [
      {'vendorId': vendor_id, 'product_id': product_id}
    ]
  }
)
// Configurationsの表示
device.configurations

USBConfigurationオブジェクトが返ってきた様子

今回はUSBConfigurationオブジェクトが1つしか存在しないので、そこに記載されている情報を元に初期化していきます。USBConfigurationオブジェクトのconfigurationValueフィールドに、Configuration番号が数値で記載されているのでこれをメモしておきます。また、USBInterfaceオブジェクトの配列形式で、interfacesフィールドにこのUSB機器が受け付けるインターフェース情報が格納されています。これも今回ひとつしか存在していませんので、interfaceNumberフィールドに格納されている数値をメモしておきます。

ここでついでに、USB通信の出口と入口を確認しておきます。USBInterfaceオブジェクトのalternateフィールドの中にUSBAlternateInterfaceが格納されています。さらにその中にあるendpointsフィールドにUSBEndpointオブジェクトが配列形式で格納されています。この中にあるinoutendpointNumberを控えておきます。inがデバイスからデータを受け取るエンドポイントで、outがデバイスにデータを送るエンドポイントです。

USB通信の出入り口の確認

最後に、次のようにデバイスを初期化します。

デバイスの初期化
// いずれもPromiseが返るので、awaitします
await device.open()
await device.selectConfiguration(1)
await device.claimInterface(0)

Let's USB通信

Send ACK!!!

デバイスの初期化も終わり、デバイスと接続できたのでいよいよUSBデバイスと通信をします。ではまずUSBパケットを組み立てましょう。Uint8Arrayオブジェクトにデータを詰め込んで送信します。まず例として、RC-S380にACKパケットを送ってみます。RC-S380の送信エンドポイントは2だったので、transferOutメソッドを使ってそこに向けてデータを出力します。うまく行くとUSBOutTransferResultオブジェクトが返ってきます。

ACKパケットの送信
const ack_packet = Uint8Array.of(0x00, 0x00, 0xff, 0x00, 0xff, 0x00)
await device.transferOut(2, ack_packet)

スクリーンショット 2018-12-20 14.12.43.png

RC-S380のパケットを組み立てる

ACKを送ったらRC-S380に対してCommandType1に設定する命令を送信します。CommandTypeを設定する命令は0x2aで、引数に0x01を取ればいいだけなのですが、そのまま0x2a 0x01を送っても何の反応もありません。仕様に従って専用のパケットを組み立てる必要があります。

まずコマンドヘッダである0xd6をコマンドの先頭にくっつける必要があります。従って送信するコマンドは0xd6 0x2a 0x01となります。さらに、この末尾に2バイト分のチェックサムをリトルエンディアンでくっつけます。チェックサムは以下の計算式で定義されています。

\mbox{checksum} = \left(256 - \sum\left(\mbox{コマンド}\right)\right) \bmod 256

今回の場合はチェックサムが255となりますので、16進数化してリトルエンディアンにすると0xff 0x00となります。これをくっつけてコマンドは0xd6 0x2a 0x01 0xff 0x00となります。

\left( 256 - (\mbox{0xd6} + \mbox{0x2a} + \mbox{0x01}) \right) \bmod 256 = 255

これで終わりかと思いきや、まだまだ続きがあります。次はパケット自体のヘッダを組み立てる必要があります。ヘッダの仕様は以下の通りです。

オフセット 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08
0x00 0x00 0xff 0xff 0xff 0xff コマンド サイズ チェックサム

コマンドサイズはコマンドヘッダを含み、コマンドのチェックサムを含まない部分で定義されていますので、今回は3となります。これをリトルエンディアンでくっつけます。従って今回の場合0x03 0x00となります。チェックサムはコマンドサイズのチェックサムとなっています。計算方法は先ほどと同じです。0x03 0x00の和から計算すると0xfdとなるのでこれをくっつけます。

最後にパケットヘッダとコマンドをくっつけて、送信してみましょう。

SetCommandType1の送信
const set_command_type_packet = Uint8Array.of(0x00, 0x00, 0xff, 0xff, 0xff, 0x03, 0x00, 0xfd, 0xd6, 0x2a, 0x01, 0xff, 0x00)
await device.transferOut(2, set_command_type_packet)

パケットの受信

ではSetCommandTypeがうまく行ったか確認するため、USBデバイスから送り返されてくるACKを受信します。RC-S380ではコマンドを送った後まずACKが送り返されてきて、そのあとコマンドに対する返り値が送り返される仕様になっています。そのため、コマンド送信後には必ず2回データを受信する必要があります5

受信にはさっきと逆のtransferInメソッドを使います。受信エンドポイントは1だったので、そこからデータを受信しましょう。なおデータを受信する際には受信バイト数を指定する必要があります。nfcpyのソースコードによると、RC-S380の最大受信バイト数は290バイトらしいので、その数を利用します。必要な分だけを受信すれば良さそうにも思いますが、データ長を事前に知る方法を今のところ見つけていないので、最大受信バイト数分一気に受信することにします。USBTransferResultオブジェクトが返ってくるので、dataフィールドに入っているDataViewオブジェクトのbufferフィールドを読んで、0x00 0x00 0xff 0x00 0xff 0x00が返ってきていればOKです。

ACKパケットの受信
await device.transferIn(1, 290)

ACKパケットの受信

続いてSetCommandTypeに対する応答パケットを読み取ります。同じく最大受信バイト数を指定してデータを受信しましょう。おそらく以下のようなデータが返ってくると思います。応答パケットの内訳は送信パケットと同じなので、先頭8バイトがパケットヘッダ、その次が応答ヘッダ、最後の2バイトがチェックサムとなっているので、残った0x2b 0x00が正味の応答パケットとなります6。これで一通りWebUSBを用いてUSB機器と通信することができました。ちなみにこの後NFCリーダーを使ってカードと通信するにはもっともっと手順を踏む必要がありますが、この記事からは逸脱するのでこれ以上は触れません。

SetCommandTypeに対する応答パケットの受信
await device.transferIn(1, 290)

SetCommandTypeに対する応答パケットの受信

0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0a 0x0b 0x0c
0x00 0x00 0xff 0xff 0xff 0x03 0x00 0xfd 0xd7 0x2b 0x00 0xfe 0x00

おわりに

ここまでWebUSBを使ってJavaScriptからUSB機器を扱う方法を見てきました。結局のところ、USBの生パケットを組み立てたりパースしたりしながら送受信するということで、やっていることは低レイヤな感じになります。USBでつながっているものはだいたい制御できるので、ブラウザからArduinoとかをいじったりすることもできますし、USBメモリを読み込んでいる先人もいたりします。

皆さんもぜひUSBパケットと戯れて、JavaScript用の様々なUSBドライバーの開発にいそしんでみてはいかがでしょうか。それではまた。


  1. 私は色々あってかなり使い込んでます... 

  2. blink-devのMLに詳細がありますが、オーディオ機器やスマートカードリーダー、ワイヤレス機器などはWebUSBからの接続がブロックされています。RC-S380はスマートカードリーダーに当たるような気がしますが、今のところなぜかブロックされていません。 

  3. 今のところWebUSBはChromeやOperaなどChromiumをベースにしたブラウザでしか動きません。 

  4. PaSoRi RC-S380もドライバが挟まると動かなくなります。とは言ってもWindows用のドライバしか存在しないので、macOSでは普通に接続できます。Linuxではrootなしでアクセスできるようにする設定が必要です。 

  5. なおACKパケットに対しては何も送り返してきません。transferInしても次に何か出力があるまで無反応になります。 

  6. この応答パケットの意味は仕様書を読んでいないのでよくわかってないです... 

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away