2012年12月26日水曜日

[Android Advent Calendar] 自作のUSBデバイスを、Androidで動かす

Android Advent Calendar 2012/12/26(表) 向けの記事です。裏は@l_b__さんです。

今年の後半にかけてAndroidのUSBホスト周りに少し詳しくなってきた、@kshojiです。Android USB MIDI Driverなどというものを作って公開しております。

USBデバイスを自分で作って、Android側でドライバを書いたら、好きなデバイスを作ってAndroidで自由に動かせて、楽しいんじゃないかなー。ということで、今回この記事を書いてみます。
TwitterのTLで尋ねてみると、USB HIDなデバイスが今時の流行っぽいので、それに乗ってみることにします。USBデバイスの自作と、そのデバイスに対応するアプリ作成の参考になれば嬉しいです。

AndroidがUSBホストに対応

Android OS 3.1からUSBホスト機能に対応して、アプリで、JavaのAPIを使ってUSBデバイスのドライバが書けるようになりました。LinuxのデバイスドライバといえばC(libusb)で書く、もしくはカーネル側をいじるという印象があるので、これはすごく画期的なことだと思います。なんといっても、Javaはとっつきやすいですから。

Android SDKにもUSBミサイルランチャを操作するサンプルが付いています。USBミサイルランチャは色々な環境でサンプルとして使われているようです。

注:必ずしもUSBデバイスであればなんでも認識する、というわけではないようです。HTC J ButterflyではUSB HIDなデバイスは認識しましたが、USB MIDIなデバイスは認識しませんでした。

AndroidでのUSBホストの使いかた

Android の USBホストの公式ドキュメントはこちらからどうぞ。

githubのリポジトリにこの記事用のコード一式を置いてみました。
いちおう、見通しがよくなるようにしているつもりです。
…と言っても、AbstractDeviceActivityクラスあたりから見ていくと、なんとなくわかるかな、程度ですが。WTFPLにしてあるので、煮るなり焼くなりお好きにどうぞ。

USBデバイスの認識

USBデバイスの認識手順としては以下の二種類があります。
  1. 事前にデバイス情報をAndroidManifest.xmlのmeta-dataに記述しておき、接続と同時にデバイスを認識する方法。
  2. UsbManagerを使って一定時間おきにデバイス情報の更新を監視して、デバイスを認識する方法。
1. の場合は、デバイスを接続・認識すると、対応するアプリを起動するためのシステムダイアログが表示されます。デバイスを認識するときにActivityが起動するため、アプリ内では1つのデバイスのみを認識することになります。
2. の場合は、アプリの起動後に、UsbManagerを用いて接続されている機器を一覧します。その後、システムダイアログを出してユーザーに問い合わせをすることでデバイスに対するアクセス権限を取得します。この一連の処理を繰り返して行なってデバイスの接続・切断状態を監視することによって、複数のデバイスを同時に接続して利用することができます。

この記事では1つのデバイスと通信することを目的として、1. の場合を説明していきます。
2. の場合は、拙作「Android USB MIDI Driver」のコードが参考になるかと思いますので、興味のある方はご参照ください。

USBデバイスの接続と情報取得のための準備

USBデバイスを接続するためには、下記のようなuses-featureタグ、meta-dataタグAndroidManifest.xmlファイルに指定しておく必要があります。

このmeta-dataタグにあるとおり、接続したいUSBクライアントの情報を別のxmlファイルに記述します。下記はUSB HIDデバイスの場合の例です。
このファイルはres/xmlに格納しておきます。

これらのXMLを設定し、Android端末にデバイスが接続されると、下図のようなシステムダイアログが表示されて、OKを押すとアプリにUsbManager.USB_DEVICE_ATTACHEDというActionのBroadcast Intentが飛んできます。このIntentの中のPercelableに念願のUsbDeviceオブジェクトが含まれています。

USBデバイス接続の論理的構造

UsbDeviceの下にはUsbInterfaceが1つ以上、UsbDeviceConnectionが1つあります。
UsbInterfaceの下にはUsbEndpointが1つ以上あります。
こんな感じです。(同じモノが2つ描かれているのは「複数個存在しえる」という意味です)。

使用するUsbInterface/UsbEndpointの検索

UsbEndpointオブジェクトには、このEndpointが実際にどういう通信をするのかという情報が格納されています。「入力用・出力用」などの通信の方向(Direction)、割り込みを使うのか、それともバルク転送を使うのかというった通信方式(Type)などです。

Endpointの種類(Type)としては、以下のものが定義されています。
  • USB_ENDPOINT_XFER_CONTROL (endpoint zero:ホストからデバイスの設定・パラメータを変更するためのEndpoint)
  • USB_ENDPOINT_XFER_ISOC (isochronous endpoint:一定時間内に一定量の通信を終わらせるよう保証・制御された転送)
  • USB_ENDPOINT_XFER_BULK (bulk endpoint:大容量データの一括転送)
  • USB_ENDPOINT_XFER_INT (interrupt endpoint:割り込みを使用した転送、小容量データの一定周期ごとの転送)
注:これらのうち、Androidでは、USB_ENDPOINT_XFER_ISOC な Endpointは非対応となっています。カメラ(USB Video Class)もisochronousな通信をするため、ドライバを作ることが出来ませんでした。

これら、Direction・Typeなどの情報から、実際にどのUsbInterface/UsbEndpointを使ってデバイスと通信するのかを決定します。

USBデバイスとの通信の準備

通信をする前には、UsbDeviceConnection宛に「いまからこのUsbInterfaceを使うよ」と通知します(UsbDeviceConnection#claimInterface)。通知しなくても使えるAndroid端末もありますが、 例えばGalaxy Tab 10.1ではこれを呼んでいないと通信できませんでした。

具体的な通信の実装方法については、USBデバイス側の実装を示した後に記述します。

USBデバイスの接続解除

事前にActivityのonCreate内などでBroadcastReceiverを継承したクラスを登録しておきます。

サンプルプロジェクトだと、UsbDeviceDetachedReceiverで、内訳はこんな感じです。

USBデバイスの接続解除の際にはUsbManager.ACTION_USB_DEVICE_DETACHEDというインテントが飛んできます。これをUsbDeviceDetachedReceiverが受信し、
OnDeviceDetachedListener(ここではActivityが実装している)に通知をするという流れになります。

接続解除する前にアプリが閉じられた場合、bradcastReceiverが残ったままになるので、ActivityのonDestroyでその登録を解除しておきます。

USBデバイスを作る

mbedを使う

今回は「mbed」でサクっと作ります。もちろん、AVRのV-USBでも、PICでもいいとは思いますが、試作するときに手っ取り早いのはmbedです。

mbedは「高速にプロトタイピングが出来るマイコン」として設計されています。
  • web上で動くコンパイラが使えるので、開発環境のセットアップが必要ありません。(オフラインな開発環境もあるようです)。どこででも、サイトにログインすれば書いたコードにアクセスできます。
  • 公式ライブラリや、コミュニティ有志によるライブラリを簡単にインポートできる仕組みがあります。
  • マイコン側は、USBやEthernet、Serial、I2C、CANなど、様々な機能を搭載しています。Arduinoのように「なにか特別なことしようと思ったら拡張ボード(値段がそこそこお高い)を買ってね」というのがありません。(もちろん、物理的なUSBポートやEthernetポートとの接続は必要ですが、それらの部品は比較的安いです)。

詳しくは公式サイトnxpfanさんの翻訳記事、「mbedを始めましょう!」などが参考になります。

今回用意するもの:mbed(青いのでも、黄色いのでも)、Webブラウザの動く環境、ソルダーレスブレッドボード、ブレッドボード用配線(適量)、半固定抵抗(適当な値のものでOKです:1個)
上記写真の左側に見えるUSB miniBポートに接続すると、PCからはマスストレージデバイスとして認識されます。そのルートディレクトリに、コンパイラでビルドしてダウンロードしたバイナリファイルを置きます。一番最後に置かれたファイルの内容を実行するので、(容量が足りていれば)古いファイルがあっても消す必要はありません。

写真で接続されているUSB miniBポートは、mbedがプログラムで動かしているUSBクライアントのポートです。この接続については、mbedのHandbook「USBDevice」の図が参考になります。

mbedの右下脇の青いツマミっぽいのが半固定抵抗(ボリューム)で、0Vから3.3Vまでの電圧を作成して、それをアナログ入力できるIOポート「p15」に接続しています。

余談:黄色いmbedは省電力なので、PCからの供給電力のみでUSBデバイスを動作させることができます。青いmbedの場合は、デバイスの消費電力が100mAを超えるようで、USBホストからの給電のみでは動きませんでした(写真左側に見えるUSBポートにも、USBケーブルを接続する必要がありました)。bMaxPowerあたりを弄ればいけそうな気もしたのですが、うまく動かず…。

今回作ったUSBデバイスの処理内容

ホスト→デバイスとデバイス→ホストのインタラクションを行うための、以下のような処理を実装してみました。
  • Androidからデータを送信して、搭載されているLED4つを点灯させます。
  • 「p15」に接続したボリュームの値を読んで、Androidに送信します。
実装内容はごくごく簡単なもので、USB HIDライブラリのサンプルコードに毛を生やした程度のものです。わりと短いので、コード全体を貼り付けておきます。わずか30行ちょっとでできました。

AndroidとUSBデバイスとの通信

通信の時には、UsbDeviceConnection宛に「使いたいUsbEndpoint」と通信するデータを指定します。

以下は出力用通信の例です(OutputDevice.javaより抜粋)。EndpointはgetMaxPacketSize()の戻り値のサイズ単位で通信をしますので、長いデータサイズを送る場合はそのサイズごとに分割したり、工夫します。
今回のデバイスのEndpoint Typeはバルク転送(USB_ENDPOINT_XFER_BULK)を使っているので、deviceConnection.bulkTransfer()を呼んでいます。コントロール転送の場合は、deviceConnection.controlTransfer()を使います。

入力用のEndpointの場合も、同じメソッドを使用します。
入力データがあるかどうかを定期的にチェックする必要があるので、別スレッドを作成してぐるぐる回します。別スレッドなので、コンストラクタで渡されるHandlerを使ってデータの受け渡しをしています。
stopFlagで、アプリの終了時(onDestroyのタイミング)に止められるようにしています。

まとめ

…というわけで、あとはAndroidアプリ側に適当にButtonやらSeekBarをレイアウトして、OnなんとかListenerで適宜USBデバイスとの通信メソッドを呼んでやれば、「Androidアプリのボタンを押すとmbedのLEDが点灯/消灯、mbed側のボリュームをいじるとAndroidアプリのSeekBarが動く」というアプリの完成です。

物理的なつまみをいじって動かす、ブロック崩しとかPONGとか用の、独自ゲームコントローラなんかはすぐ作れちゃいそうです。しかもUSB HIDであれば、マウスやキーボードと同等なデバイスなので、ほぼ全てのUSB Host対応端末で動くはずです。

ホスト側:Android

AndroidのUSBホストの実装は、libusbと同じような感覚で記述できます。少なくともUSB周りの知識は要るとは思いますが、今回のサンプルプロジェクトを参考にすれば、似たようなものはそこそこ簡単に作れるかと思います。
今回はデバイス側も作ってしまいましたが、既に市販されて流通しているPC用などのUSBデバイスが動かせる、というところも魅力的です。ADKだと、デバイスからドライバまで一通り作らないといけませんからね(もちろん、それはそれで技術的には楽しいです)。

クライアント側:mbed

USBクライアントライブラリは非常にシンプルに記述できるように作られているので、デバイス作成入門の最初の一歩として、すごく楽に開発ができました。
今回は半固定抵抗の値を表示してみましたが、この部分を電圧で値を取得できるセンサーに変えるだけで、センサーの値 を取得できる USBデバイスになります。
mbedは、このようにUSBクライアント・デバイスも簡単に作れますし、他にもいろいろと遊べる機能が満載でお勧めです。この年末年始や冬休みに遊んでみてはどうでしょうか。

おしまい

簡潔にしようと思いつつも、長々と書いてしまいました。最後までお読み頂きありがとうございました。
明日は、表が@kyusyukeigoさん、裏が@tmk_betaさんです。