久しぶりにUSBホストシールドで遊んでみました。
USBで制御できるミサイルランチャー(カメラ付き)を手に入れたので、これをArduinoから動かしてみます。
必要なもの
- Arduino (Duemilanove / Uno。 Megaでも大丈夫かな?)
- USBホストシールド
- USBミサイルランチャー
- ACアダプタ 9V〜12Vくらい
ライブラリのインストール
準備として、ホストシールドのライブラリをダウンロードしてインストールする必要があります。
下記GitHubではUSBホストシールドライブラリ2.0が公開されています。2.0では、Arduino IDE 1.0対応、USBハブ対応などが行われています。
GitHub - felis/USB_Host_Shield_2.0: Revision 2.0 of USB Host Library for Arduino.
USBホストシールド2.0との差分の対策
このライブラリはUSB Host Shield 2.0という新しいバージョンのシールド用になっています。
いま現在国内で入手しやすいと思われる、スイッチサイエンスなどで販売されているSparkFunバージョンと比べると、USBホストのリセットの仕方が変わっています*1。そのため、SparkFunバージョンでこのライブラリを動作させる際には、少し細工が必要です。
具体的には、ArduinoのRESETピンと、デジタル7ピン(古いシールド*2の場合は8ピン)とを、ジャンパワイヤなどで接続しておきます。これで、Arduinoのリセット時にUSBホストもリセットされるようになります。*3
上記は(古いシールドなので)8ピンをRESETに接続したところ。
他の注意点として、USB電源だけではUSB機器への供給電圧が不足するため、9〜12V程度のACアダプタをArduinoに接続しておかないと、動作が不安定になって機器が認識されなかったりするので注意が必要です。
ミサイルランチャーのプロトコルを解析
USBを使ってミサイルランチャーをコントロールするためには、どんなプロトコルで制御されているのかを知る必要があります。といってもデータシートがあるわけではないので、USBパケットをキャプチャして解析する必要があります。
とりあえず、ベンダーIDをプロダクトIDを調べます。下記はMacにつないで"システム情報"でUSBを見たところ。このミサイルランチャーの場合、USBハブが内蔵されており、そこにカメラとミサイル制御デバイスが接続されている、という構成になっていました。ミサイルの方のベンダーIDは0x2123, プロダクトIDは0x1010です。
USBキャプチャはWindowsマシンで行いました。まずはフリーのUSBスニファー「Snoopy Pro」を導入しておきます。また、メーカーページからダウンロードしたWindows版制御ソフトをインストールし、動かせるようにしておきます。
Snoopy Proを起動するとUSBデバイス一覧が表示されるので、先ほど調べたベンダーID&プロダクトIDを持つデバイスを探し、右クリックしてInstall and Restartを選ぶと、USBLogウィンドウが開き、キャプチャが始まります。
この状態で、制御ソフトのボタンを上下左右や発射ボタンをクリックして、制御パケットをキャプチャします。このとき長くボタンを押すと大量のパケットがキャプチャされてしまって解析の手間が増えるので、短く一瞬だけクリックするのがポイント。
実際に上下左右、発射ボタンを押した時のキャプチャ結果はこんな感じでした。(右に出てるのは制御ソフト。動作音や発射警報音が鳴ったりと結構凝ってますw)
out down と表示されているのが、PCからUSBへの出力パケットです。その下のout upはUSBからの応答です。FunctionにCLASS_INTERFACEと表示されていますが、これはミサイルランチャーがHID(Human Interface Device)という規格に準拠しているためです。HIDでは、GetReport / SetReport というリクエストでデバイスとの入力 / 出力を行います。上のキャプチャはいずれも、SetReportをしていることを表しています。その右のData部分が、実際に送受信されている内容です。
Data部分を見ていくと、動き始める時に 「02 ** 00 00 00 00 00 00」(**は下なら01、上なら02、左なら04、右なら08、発射なら10)、止まる時に「02 20 00 00 00 00 00 00」というデータが送られていることが分かりました。Arduinoからでも、たぶんこれと同じものを送ってやりさえすれば動くのではないかと想像できます。簡単ですね。実際にはこの他にも動作状況を取得するためのパケット(GetReport)が流れているのですが、とりあえずは無視しておきます。
次に、左ボタンを押下した時のデータを詳細表示してみます。
SetReportリクエストを送るためには、SetupPacketのうちの4つめと3つめ、report_type / report_id という番号も必要になります。今回はそれぞれ、report_type=2, report_id=0でした。
Arduinoからミサイルランチャーを動かしてみる
さて、だいたいプロトコルの見当がついたら、Arduinoで実際に動かしてみます。一般のデバイスに対応するにはライブラリのcdc*.h, cdc*.cppを複製してインターフェースを実装することになるのですが、今回はHIDに準拠しているので、ライブラリ標準で用意されているHIDUniversalというクラスで取り扱えるため、かなり簡単になります。
実際のスケッチはこんな感じ。
#include <Usb.h> #include <usbhub.h> #include <hid.h> #include <hiduniversal.h> USB Usb; // USBホストシールド制御用 USBHub Hub(&Usb); // 使用するハブの数だけ定義しておく USBHub Hub2(&Usb); HIDUniversal Hid(&Usb); // HIDデバイス void setup() { Serial.begin(115200); Serial.println("Start"); // USB初期化 if (Usb.Init() == -1) Serial.println("OSC did not start."); delay(200); } void loop() { Usb.Task(); // 定期的に呼び出す必要あり // シリアルから文字を受信 if (Serial.available()) { byte c = Serial.read(); if (c == 'd') c = 0x01; // DOWN else if (c == 'u') c = 0x02; // UP else if (c == 'l') c = 0x04; // LEFT else if (c == 'r') c = 0x08; // RIGHT else if (c == 'm') c = 0x10; // LAUNCH MISSILE! else if (c == 's') c = 0x20; // STOP else return; byte cmd[8] = {2, c, 0, 0, 0, 0, 0, 0}; // 送信するコマンド(Data) // 引数1,2には0を指定。以降、report_type=2, report_id=0 でcmdをSetReport送信 Hid.SetReport(0, 0, 2, 0, sizeof(cmd), cmd); } }
ミサイルランチャーに内蔵のUSBハブでHIDが接続されているため、USBHubクラスのインスタンスを少なくとも1つ定義しておく必要があります(1つ定義すると1段分のハブを認識できるようになる)。あとはHIDUniversalクラスのインスタンスを1つ定義します。そうしておいて、setup()でUsb.Init()を呼び、loop()でUsb.Task()を何回か呼んでいると、自動的にHIDデバイスを検索してHidインスタンスを通じて操作できるようにしてくれます。
このスケッチをArduinoに書き込んで、ミサイルランチャーとUSBホストシールドを接続し、シリアルから l/r/d/u/s/m のいずれかの文字を送ると、それぞれ左回転/右回転/下/上/停止/ミサイル発射 の動作をしてくれました。素直なデバイスです。^^;
上下左右は限界に達するか停止コマンドを送るまで回り続けます。いっぽうミサイル発射時は、1発発射したあたりで勝手に止まってくれるので、停止信号などは送らなくても大丈夫でした。
なお、上記スケッチをビルドすると30KB近くになって、ほぼArduinoのフラッシュを使い切ってしまいますが、この容量の約半分はHIDUniversalクラスが表示するデバイス情報メッセージのせいです。ライブラリのhiduniversal.cppの232行目あたりにある部分を下記のように無効化すると、あっという間に15KBほどサイズが小さくなります。
USBTRACE("HU configured\r\n"); #if 0 { HexDumper<USBReadParser, uint16_t, uint16_t> Hex; ReportDescParser Rpt; if (rcode = GetReportDescr(0, &Hex)) goto FailGetReportDescr; if (rcode = GetReportDescr(0, &Rpt)) goto FailGetReportDescr; } #endif
ドア開閉をセンサで検知して発射するようにしたら、侵入者を撃退(?)するデバイスができるかも。(いやそれだと主にミサイル食らうのは自分か…。)
あと内蔵のカメラが使えたりするとさらに面白いんですが、さすがに処理能力的に厳しいかな。。
追記
ルンバに載せて移動砲台にしてみました。
http://d.hatena.ne.jp/NeoCat/20120220/1329699693 →最後の動画