この記事はMuroran Institute of Technology Advent Calendar 2018の14日目の記事になりました。
はじめに
皆さん、WebUSB使ってますか1?WebUSBは標準化もされたばかりで、まだまだ知見も溜まってきていないところではありますが、Webの世界を物理デバイスから拡張できる可能性を秘めたAPIです。ちょっと使い方が難しいAPIではありますが、これが使えると世界がぐっと広がりますのでこの記事を読んでぜひご自身でいろんな工作を楽しんでみてください!
執筆環境
この記事は以下の環境で書かれています。WebUSBはOS標準のドライバが間に挟まると途端に動かなくなったりします。また、セキュリティ上接続が拒否されるデバイスもあります2。動作が確認されていない環境で動かすのは、場合によってはかなり難易度が高くなることに注意してください。また、USBデバイスの例として国内で普及しているNFCリーダーであるPaSoRi RC-S380
を使用します。
WebUSBでデバイスと接続する
ではいよいよWebUSBを使ってデバイスに接続してみましょう。Google Chromeを起動して、JavaScriptコンソールを用意してください。
VendorIDとProductIDの特定
WebUSBでUSBデバイスに接続を要求するにはnavigator.usb.requestDevice({'filters': []})
を叩けば良いのですが、そのままだと目的としていないデバイスまで一覧に表示されてしまいます。試しにやってみましょう。コンソールに以下のコードを打ち込むと、接続できるデバイスの一覧がポップアップ画面に表示されます。目的とするRC-S380以外にも、キーボードやトラックパッドまで表示されてしまっています。ユーザーが意図しないデバイスを選択しないためにも、フィルター条件としてVendorIDとProductIDを指定しておきます。
// Promiseが返ってくるためawaitします
await navigator.usb.requestDevice({'filters': []})
VendorIDとProductIDを特定するのは簡単で、Linuxの場合lsusb
コマンドを、macOSの場合system_profiler SPUSBDataType
コマンドを利用すれば、特定することができます。macOSでの例を以下に示します。
$ 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が特定できたら、今度はフィルターを設定してから叩いてみます。今度こそ目的のデバイスだけを表示することができるようになりました。
const vendor_id = 0x054c
const product_id = 0x06c1
await navigator.usb.requestDevice(
{
'filters': [
{'vendorId': vendor_id, 'product_id': product_id}
]
}
)
デバイスの初期化
デバイスの接続要求をしたら、次はデバイスを開いて初期化する作業が続きます。USBデバイスの初期化には、Configuration
とInterface
を調べる必要があります。まずは以下のコードをコンソールから叩いて、この2つの値を読み出します。以下のコードを入力してみましょう。device.configurations
を呼ぶと、そのUSBデバイスで利用可能なConfigurationの一覧がUSBConfiguration
オブジェクトの配列で返ってきます。
// デバイスの接続要求
// ポップアップが出るので接続するデバイスを選択すること
const device = await navigator.usb.requestDevice(
{
'filters': [
{'vendorId': vendor_id, 'product_id': product_id}
]
}
)
// Configurationsの表示
device.configurations
今回はUSBConfiguration
オブジェクトが1つしか存在しないので、そこに記載されている情報を元に初期化していきます。USBConfiguration
オブジェクトのconfigurationValue
フィールドに、Configuration番号が数値で記載されているのでこれをメモしておきます。また、USBInterface
オブジェクトの配列形式で、interfaces
フィールドにこのUSB機器が受け付けるインターフェース情報が格納されています。これも今回ひとつしか存在していませんので、interfaceNumber
フィールドに格納されている数値をメモしておきます。
ここでついでに、USB通信の出口と入口を確認しておきます。USBInterface
オブジェクトのalternate
フィールドの中にUSBAlternateInterface
が格納されています。さらにその中にあるendpoints
フィールドにUSBEndpoint
オブジェクトが配列形式で格納されています。この中にあるin
とout
のendpointNumber
を控えておきます。in
がデバイスからデータを受け取るエンドポイントで、out
がデバイスにデータを送るエンドポイントです。
最後に、次のようにデバイスを初期化します。
// いずれも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
オブジェクトが返ってきます。
const ack_packet = Uint8Array.of(0x00, 0x00, 0xff, 0x00, 0xff, 0x00)
await device.transferOut(2, ack_packet)
RC-S380のパケットを組み立てる
ACKを送ったらRC-S380に対してCommandType
を1
に設定する命令を送信します。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 |
---|---|---|---|---|---|---|---|---|
値 | 0x00 | 0x00 | 0xff | 0xff | 0xff | [コマンド | サイズ] | チェックサム |
コマンドサイズはコマンドヘッダを含み、コマンドのチェックサムを含まない部分で定義されていますので、今回は3
となります。これをリトルエンディアンでくっつけます。従って今回の場合0x03 0x00
となります。チェックサムはコマンドサイズのチェックサムとなっています。計算方法は先ほどと同じです。0x03 0x00
の和から計算すると0xfd
となるのでこれをくっつけます。
最後にパケットヘッダとコマンドをくっつけて、送信してみましょう。
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です。
await device.transferIn(1, 290)
続いてSetCommandType
に対する応答パケットを読み取ります。同じく最大受信バイト数を指定してデータを受信しましょう。おそらく以下のようなデータが返ってくると思います。応答パケットの内訳は送信パケットと同じなので、先頭8バイトがパケットヘッダ、その次が応答ヘッダ、最後の2バイトがチェックサムとなっているので、残った0x2b 0x00
が正味の応答パケットとなります6。これで一通りWebUSBを用いてUSB機器と通信することができました。ちなみにこの後NFCリーダーを使ってカードと通信するにはもっともっと手順を踏む必要がありますが、この記事からは逸脱するのでこれ以上は触れません。
await device.transferIn(1, 290)
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ドライバーの開発にいそしんでみてはいかがでしょうか。それではまた。
-
私は色々あってかなり使い込んでます... ↩
-
blink-devのMLに詳細がありますが、オーディオ機器やスマートカードリーダー、ワイヤレス機器などはWebUSBからの接続がブロックされています。
RC-S380
はスマートカードリーダーに当たるような気がしますが、今のところなぜかブロックされていません。 ↩ -
今のところWebUSBはChromeやOperaなどChromiumをベースにしたブラウザでしか動きません。 ↩
-
PaSoRi RC-S380もドライバが挟まると動かなくなります。とは言ってもWindows用のドライバしか存在しないので、macOSでは普通に接続できます。Linuxではrootなしでアクセスできるようにする設定が必要です。 ↩
-
なおACKパケットに対しては何も送り返してきません。
transferIn
しても次に何か出力があるまで無反応になります。 ↩ -
この応答パケットの意味は仕様書を読んでいないのでよくわかってないです... ↩