この記事はKernel/VM探検隊のAdvent Calendar向けの記事です。
去年は別のブログで書いたんだけど、今年はAndroidネタなのでこっちで書きます。
KernelともVMともあまり関係無い気がするけど、AndroidはDalvikで動いているのでVMってことで(^^)
Android 3.1から Android Open AccessoryというUSBケーブルを使ってAndroidとArduinoボードを接続するAPIが搭載された。
PC並の処理能力を持つAndroid携帯をUSB Hostにして、ArduinoをUSB Clientにするのが当然だと思うんだが
Android携帯をClientにしてArduino側をホストにするという、逆転の発想でAndroid 2.3.4へのバックポートが実現している。
Android端末でUSBホストを実現するとなると、カーネルレベルでOTGに対応しなければならず結構面倒なんだが、Clientで動作するんだからそういう手間も要らない。
さて、これで何ができるかというと、ArduinoでできることをほとんどすべてAndroid側から制御することができる。
ArduinoではAVRやPICをつかって、PWM制御やDigital入出力、Analog入力などが行える。
つまり、Arduinoにサーボモータをつけて動かしたり、いろんなセンサをつけて、値を取り出すことができる。
例として使われているArduino MEGA 2560あたりを使うとPWMで12系統使えるので、かなりの数のサーボを制御することができる。
(電力の供給は別途考えないといけないけど)
つまりArduinoのボードとAndroid端末を組み込んだロボットなんかも作れたりする。
Android端末であれば、3GやWifiが使えるので、遠隔操作だってできてしまう。
ということで、極簡単にプログラムの構造を説明する。
Arduino側
Arduinoの場合は、CPUがAVRであるArduino純正の環境とMicrochipが作成したPIC24で動作する環境がある
それぞれ別のソースがあるのだが、AVR版の方が見やすいのでそちらの方をベースに話を進める。
AVR版のソースはArduino言語と呼ばれるC++のサブセットにArduino拡張を加えた言語を使用する。
といっても文法はCにクラスが追加された程度でなのでCだと思って読めばそれほど苦労はしない。
Arduinoのプログラムはスケッチと呼ばれ、setupとloopという関数が必須で、起動すると一度だけsetupが実行され、その後継続的にloopが呼ばれるという形式になる。
呼び出し処理はファームウェアで行われるので、特別なことは考えなくても良い様になっている。
割り込み処理も登録できるが、それほどクリティカルでない処理ならloopの中でポーリングする方が楽に処理できる。
当然だが、他の処理を止めない為にloopで実行する処理は最小限にとどめ、時間のかかる処理は分割して行う等の工夫は必要となるが、今回の場合Arduino側で複雑な処理をする必要はないので気にしない。
loopで行うべき処理は、
・USBの入力をみて、コマンドが来ていれば処理する。
・ボタンやセンサーをチェックして、値が変わっていたら、USBに出力する。
という極単純なものになる。
Android側
Android側では、Arduinoボードが接続されたら自動的にアプリの起動が行われる。
起動されたアプリケーションでは、接続されているデバイスを確認し、入出力ストリームを開き、ストリームへの入力を監視するだけの処理になる。
ストリームへの入力を検出したらコマンドを解釈して画面を更新し、画面操作を認識したら出力ストリームにコマンドを送る。
というArduino側の処理と対応した処理を行うだけで十分。
実際、RT-ADK&ADSというArduinoボードを使用して、Android 2.3.4または3.1以降のAndroid携帯を接続することで、たとえば、ジョイスティックを使って地図をスクロールしたりする程度のアプリなら簡単に作れる。
ということで、最後にAndroid側のソースを掲載して、終わりにしたい。
Arduino側はGoogle提供のdemokitのソースをそのまま使用する。
というわけで、簡単に作れて、結構面白いことができそうなので、どなたか何か作りませんか?
package com.example.adkmap;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.ParcelFileDescriptor;
// import android.widget.TextView;
import com.android.future.usb.UsbAccessory;
import com.android.future.usb.UsbManager;
import com.google.android.maps.MapActivity;
import com.google.android.maps.MapView;
public class ADKMapActivity extends MapActivity implements Runnable {
private static final byte COMMAND_JOYSTICK = 6;
private static final byte COMMAND_BUTTON = 1;
protected static final int MESSAGE_JOY = 1;
private static final int MESSAGE_BUTTON = 2;
private static final int BUTTON1 = 0;
private static final int BUTTON3 = 2;
private static final int ON = 1;
private MapView mMapView;
private UsbManager mUsbManager;
private UsbAccessory mAccessory;
private ParcelFileDescriptor mFileDescriptor;
private FileInputStream mInputStream;
private FileOutputStream mOutputStream;
// private TextView tv;
private static final String ACTION_USB_PERMISSION = “com.example.adkmap.action.USB_PERMISSION”;
private boolean mPermissionRequestPending=false;
private PendingIntent mPermissionIntent;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// tv = (TextView)findViewById(R.id.textView1);
// マップにズームコントローラを表示されるためにMapViewを取得する
mMapView = (MapView)findViewById(R.id.mapview);
// ズームコントローラを表示する
mMapView.setBuiltInZoomControls(true);
// USBアクセサリの許諾をとるためのrequestPermissionで使用するPendingIntent
mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(
ACTION_USB_PERMISSION), 0);
// USBアクセサリを取り外したときのインテントを受け取るためのフィルター
IntentFilter filter = new IntentFilter(UsbManager.ACTION_USB_ACCESSORY_DETACHED);
// permissionRequestの結果を受け取るためのフィルター
filter.addAction(ACTION_USB_PERMISSION);
registerReceiver(mUsbReceiver, filter);
}
@Override
protected void onPause() {
super.onPause();
// tv.setText(“onPause”);
// onPauseではアクセサリをクローズする
closeAccessory();
}
@Override
protected void onResume() {
super.onResume();
// tv.setText(“onResume”);
// UsbManagerクラスのインスタンスを取得
mUsbManager = UsbManager.getInstance(this);
UsbAccessory accessory = null;
// 現在接続されているアクセサリのリストを取得する
UsbAccessory[] accessories = mUsbManager.getAccessoryList();
if(accessories != null && accessories.length == 1){
// アクセサリが一つだけ接続されていることを確認する。
accessory = accessories[0];
if (mUsbManager.hasPermission(accessory)) {
// 既にアクセサリの使用許可を持っていれば openAccessoryを実行する
openAccessory(accessory);
} else {
// アクセサリの使用許可を持っていなければrequestPermissionメソッドを実行する
synchronized (mUsbReceiver) {
if (!mPermissionRequestPending) {
mUsbManager.requestPermission(accessory,
mPermissionIntent);
mPermissionRequestPending = true;
}
}
}
}
}
@Override
protected boolean isRouteDisplayed() {
return false;
}
private void openAccessory(UsbAccessory accessory) {
// ファイルディスクリプタの取得
mFileDescriptor = mUsbManager.openAccessory(accessory);
if (mFileDescriptor != null) {
// mAccessoryに保存
mAccessory = accessory;
FileDescriptor fd = mFileDescriptor.getFileDescriptor();
// InputStream/OutputStremのオープン
mInputStream = new FileInputStream(fd);
mOutputStream = new FileOutputStream(fd);
// 通信スレッドの起動
Thread thread = new Thread(this);
thread.start();
} else {
}
}
@Override
public void run() {
int len = 0; // 読み出したデータのサイズ
byte[] buffer = new byte[16384]; // バッファ
int cur; // 現在処理しているデータの位置
while (len >= 0) {
try {
len = mInputStream.read(buffer);// InputStreamからデータを読み出し
} catch (IOException e) {
break;
}
cur = 0;
while (cur < len) {
int remain = len – cur;
switch (buffer[cur]) {
case COMMAND_JOYSTICK:
// コマンド番号がJOYSTICKに一致した場合
if (remain >= 3) {
// 残りのバイト数が3バイト以上あればメッセージを送信する
Message m = Message.obtain(mHandler, MESSAGE_JOY);
m.obj = new JoyMsg(buffer[cur + 1], buffer[cur + 2]);
mHandler.sendMessage(m);
}
// データ位置を3バイト進める
cur += 3;
break;
case COMMAND_BUTTON:
// コマンド番号がボタンに一致した場合
if(remain >= 3){
// 残りのバイト数が三バイト以上なら、ボタンのメッセージを送信する
Message m = Message.obtain(mHandler, MESSAGE_BUTTON);
m.obj = new ButtonMsg(buffer[cur+1],buffer[cur+2]);
mHandler.sendMessage(m);
}
cur += 3;
break;
default:
// JOYSTICKコマンド以外は無視して読み飛ばす
cur += 3;
break;
}
}
}
}
Handler mHandler = new Handler() {
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_JOY:
// ジョイスティックのメッセージだったら、handleJoyMessageを呼び出す
JoyMsg j = (JoyMsg) msg.obj;
handleJoyMessage(j);
break;
case MESSAGE_BUTTON:
// ボタンのメッセージだったら、handleButtonMessageを呼び出す
ButtonMsg b = (ButtonMsg)msg.obj;
handleButtonMessage(b);
break;
}
}
};
// ジョイスティックの情報を管理するクラス
protected class JoyMsg {
private int x;
private int y;
// コンストラクタ
public JoyMsg(int x, int y) {
this.x = x;
this.y = y;
}
// アクセサ
public int getX() {
return x;
}
public int getY() {
return y;
}
}
// ボタンの情報を管理するクラス
protected class ButtonMsg {
private int button;
private int state;
public ButtonMsg(int b, int s){
button = b;
state = s;
}
public int getButton(){
return button;
}
public int getState(){
return state;
}
}
protected void handleJoyMessage(JoyMsg j) {
int x = j.getX();
int y = j.getY();
// ジョイスティックは全く触っていない状態でもある程度の値を返すので±10以上動いた場合にのみスクロールを実行する。
if(x>10 || x < -10 || y > 10 || y < -10){
mMapView.getController().scrollBy(j.getX(), j.getY());
}
}
protected void handleButtonMessage(ButtonMsg b){
int button = b.getButton();
int state = b.getState();
switch(button){
case BUTTON1:
if(state == ON){
mMapView.getController().zoomOut();
}
break;
case BUTTON3:
if(state == ON){
mMapView.getController().zoomIn();
}
break;
}
}
// Broadcast Intentを受信するレシーバー
private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// intentからActionを取り出す
String action = intent.getAction();
if (ACTION_USB_PERMISSION.equals(action)) {
// permissionRequestの結果のIntentの場合
synchronized (this) {
// インテントからアクセサリを取り出す
UsbAccessory accessory = UsbManager.getAccessory(intent);
if (intent.getBooleanExtra(
UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
// 使用が許可されていれば、openAccessoryを実行
openAccessory(accessory);
} else {
// 使用が許可されていなければ何もしない
// tv.setText(“Permission Denied”);
}
mPermissionRequestPending = false;
}
}else if (UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)) {
// ActionがACTION_USB_ACCESSORY_DETACHEDだった場合
// intentからAccessoryを取得する
UsbAccessory accessory = UsbManager.getAccessory(intent);
if (accessory != null && accessory.equals(mAccessory)) {
// 現在使用しているアクセサリに一致する場合はcloseAccessoryを呼び出す
closeAccessory();
}
}
}
};
// アクセサリとの接続をクローズする
private void closeAccessory() {
try {
if (mFileDescriptor != null) {
mFileDescriptor.close();
}
} catch (IOException e) {
} finally {
mFileDescriptor = null;
mAccessory = null;
}
}
}