MacとNexus OneでAndroid Oepn Accessoryを試してみる
先日サンフランシスコで行われたGoogle I/OのKeynoteでは様々な発表がありましたが、その中で、僕が特に面白いと思ったのが「Android Open Accessory」です。
簡単に言うと、Androidデバイスに接続するUSB周辺機器を、誰でも簡単に作ることができる仕組みです。
Google I/Oではこの仕組みを試すための開発ボード「ADK」が無償で配られたり、日本のメーカーからも発売されたりしていますが、電子工作ということで敷居が高いと感じている人も多いんじゃないかと思います。
しかし、Android Open Accessoryが適用できるのは、別に電子工作に限った話ではありません。PCとAndroid端末さえあれば、今すぐに試してみることができます。というわけで、早速デモを作ってみたので簡単に解説していきます。
用意するもの
今回作るものの概要
Mac PCにUSBで接続されたNexus One上のAndroidアプリと、Mac PC上の実行ファイルとの間で、USBポートを介してデータの通信を行います。
ただ単にデータ通信するだけでは面白くないので、KeyNoteのデモのように、端末の動きをゲームに反映させてみます。
アーキテクチャは以下の図のような感じで、Androidの傾きセンサーやボタンクリックを検出して、その値をUSBを介して通信し、最終的にJavaScriptで作ったゲームに端末の傾きの値を反映させたり、アプリのボタンをクリックするとボールが追加されるようにします。
JavaScriptのゲームはBox2DJSのデモをちょこっと改造した力学演算系のゲームです。
ソース全体
github: https://github.com/thorikawa/android-openaccessory-with-pc-sample
にアップロードしています。
アプリ側のソース
android-app-usbフォルダがアプリ側のソースとなっています。
基本的には、USB Accessory | Android Developersに記載されている内容に準拠します。
AndroidManifestの記述
USB周辺機器(今回はPC)が端末に接続されたイベントを捕捉できるように、AndroidManifestにIntent-Filterを記述します。
<activity android:name=".Top" android:label="@string/app_name" <intent-filter> <action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" /> </intent-filter> <meta-data android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" android:resource="@xml/accessory_filter" /> </activity>
meta-dataのresourceにxmlファイルを指定していますが、これは別途作成します。
filtering用のxmlファイル作成
/res/xml以下に、上記Intent-Filterのmeta-dataに指定したaccessory_filter.xmlを作成します。
<?xml version="1.0" encoding="utf-8"?> <resources> <usb-accessory manufacturer="Poly's Factory" model="Android Oepn Accessory Demo" version="1.0" /> </resources>
ここで指定するManufacturer・Model・Versionによって、Activityが扱えるUSB Accessoryがフィルタリングされます。後で解説するPC側のプログラムでも、同じ値を指定します。
Activity作成
Activity側では、接続されたUSB Accessoryと通信するために、2通りの方法でUSB Accessoryオブジェクトを取得できます。
1.Intentから取得する場合
USB機器接続時のIntentからActivityが呼び出された場合、IntentからUSB Accessoryoオブジェクトを取得することができます。
Intent intent = getIntent();
if (UsbManager.ACTION_USB_ACCESSORY_ATTACHED.equals(intent.getAction())) {
mUsbAccessory = UsbManager.getAccessory(intent);
}
2.任意のタイミングで取得する場合
USB機器接続後に、任意のタイミングでUSB Accessoryオブジェクトを取得することもできます。
mUsbManager = UsbManager.getInstance(this); UsbAccessory[] accessoryList = mUsbManager.getAccessoryList(); for (UsbAccessory usbAccessory : accessoryList) { // 条件に合致するusbAccessoryを選択 .... }
USB Accessory取得後はUsbManager.openAccessory()で、ParcelFileDescriptorを取得することで、普通のファイルストリームと同じように扱うことができます。
ParcelFileDescriptor pfd = mUsbManager.openAccessory(mUsbAccessory); FileDescriptor fd = pfd.getFileDescriptor(); FileInputStream fis = new FileInputStream(fd); FileOutputStream fos = new FileOutputStream(fd); fos.write(....);
実際のデモプログラムの中では、傾きセンサーで取得された値と、ボール追加ボタンが押されたかどうかを、バイト列にしてFileOutputStreamに出力しています。
PC側のソース
pc-usb.cがPC側のソースになっています。libusbとリンクする必要があるので "gcc -lusb-1.0 pc-usb.c" などでビルドして下さい。
PC側では、Android Open Accessory専用のプロトコルである、Android accessory protocolで通信を行う必要があります。
プロトコルの内容は、Android Open Accessory Development Kit | Android Developersを見ればわかりますが、Using Android in Industrial Automation » Turn your Linux computer into a huge Android USB Accessoryの記事で、Cとlibusbを使ったサンプルソースを公開している方がいらっしゃるので、ここではそれを参考にしてプログラムを書いてみます。
Android端末との接続
まず、USBで繋がっているAndroid端末の接続ハンドルを取得します。
#define VID 0x18D1 #define PID 0x4E12 libusb_init(NULL); libusb_device_handle* handle = libusb_open_device_with_vid_pid(NULL, VID, PID);
ここでVendorIDとProductIDを指定して、ハンドルをオープンしていますが、このIDは機種ごとに異なり、また同じ端末でも、USBデバッグモードとそうでない時で異なるので注意してください。
上記のIDはNexus Oneのデバッグモードで指定するIDです。
VendorID・ProductIDは、Mac OSであれば
/Developer/Applications/Utilities/USB Prober.app/Contents/MacOS/USB Prober
などを使って簡単に調べることができます。
Accessory Modeのset up
次に、Android端末はそのままではAccessoryと通信を行うモードになっていないので、モードを変更します。
Attempt to start the device in accessory modeに記載されている通信を、libusbを介して行います。
手順は、(1)端末がAndroid accessory protocolに対応しているかどうかの判定 (2)Accessory Identificationの送信 (3)端末をAccessory modeに変更 です。
(1)端末がAndroid accessory protocolに対応しているかどうかの判定
libusb_control_transfer(handle,0xC0,51,0,0,ioBuffer,2,0);
ioBufferにサポートするaccessory protocolのバージョンが返り、この値が0以外ならば端末がaccessory protocolをサポートしていると判断します。(現在サポートしている場合のバージョンは"1"のみ存在する)
(2)Accessory Identificationの送信
libusb_control_transfer(handle,0x40,52,0,0,(char*)manufacturer,strlen(manufacturer),0); libusb_control_transfer(handle,0x40,52,0,1,(char*)modelName,strlen(modelName)+1,0); libusb_control_transfer(handle,0x40,52,0,2,(char*)description,strlen(description)+1,0); libusb_control_transfer(handle,0x40,52,0,3,(char*)version,strlen(version)+1,0); libusb_control_transfer(handle,0x40,52,0,4,(char*)uri,strlen(uri)+1,0); libusb_control_transfer(handle,0x40,52,0,5,(char*)serialNumber,strlen(serialNumber)+1,0);
この命令によって、USB機器を識別するためのManifactorer・Model・Versionなどの情報が端末に送信されます。accessory_filter.xmlに記載されたフィルタリング情報は、ここで送信された値と一致するかで判定します。
(3)端末をAccessory modeに変更
libusb_control_transfer(handle,0x40,53,0,0,NULL,0,0);
Accessory Modeでのデータ送受信
以上で端末がAccessory Modeに変更されました。
このタイミングで端末のProduct IDが変更されるので
libusb_close(handle);
によって、Accessory Mode変更前の接続ハンドルをクローズし、
#define ACCESSORY_PID 0x2D01 handle = libusb_open_device_with_vid_pid(NULL, VID, ACCESSORY_PID)
によって、新しいAccessory ModeのPIDで接続ハンドルを作り直します。
ここまで行えば、後はlibusb_bulk_transfer関数を通じて、端末とバイトストリームをやりとりすることができます。
#define IN 0x83 #define OUT 0x03 #define BUFFER 1024 unsigned char buffer[BUFFER]; static int transferred; while (libusb_bulk_transfer(handle, IN, buffer, BUFFER, &transferred, 0) == 0) { ... }
ここで定数INとOUTは、それぞれAndroid側のUSBインターフェースを識別する値で、これも端末ごとに異なっています。今回はNexusOneの値を採用しています。
他の端末の場合、ProductIDと同じようにUSB Proberなどで確認してください。
PC側プログラムではここで読み込んだ値を、JavaScriptからも読み込めるように、一旦テキストファイルに書きだしています。
JavaScriptゲーム側
box2d-with-usbフォルダ以下が、ブラウザで実行するJavaScriptゲームのソースになっています。
詳細は省きますが、PC側プログラムがテキストファイルに書きだした取得された傾きセンサーの値と、ボールが追加されたかどうかのフラグを参照して、それに応じた力学アニメーションを行うようになっています。