2011年12月8日木曜日

Android Hello ADK つくった!

DemoKit を動かしてみたよエントリや、Arduino で ADK 自作したよエントリはあるのに、Android ADK 用の Android アプリのプログラミングについて初心者向けに書かれたエントリがぜーんぜんなかったので書くことにしました。

ちなみにわたくし、電子工作ブランク10年くらいです。 10年前にブレッドボード上にケーブルが空中回廊みたいになった電卓を作って以来です。

今回は USB Accessory モードの方についてです。
公式ドキュメントはこちら
http://developer.android.com/guide/topics/usb/accessory.html
私のブログでの和訳はこちら
http://y-anz-m.blogspot.com/2011/05/usb-accessory.html

さて、とりあえず開発環境の準備。
DemoKit 動かしてみたよエントリがたくさんあるのでその辺りみればだいたい書いてると思う。
ちなみに公式ドキュメントはこちら。
Android Open Accessory Development Kit | Android Developers

これの Getting Started with the ADK を見ながら(私のブログでの和訳はこちら http://y-anz-m.blogspot.com/2011/05/android-open-accessory-development-kit.html

  • Arduino Software : ADK は Arduino がベースになっているので、ADK で動かすプログラムは Arduino で書く。そのための IDE。コーディングは持ちろん、デバッグやファームインストールもこれで行う。
  • CapSence library : 人間の静電容量を感知するためのライブラリ。DemoKit の Android シールドにあるタッチセンサーの為に必要。これいれないとタッチセンサーを触っても DemoKit アプリ側で反応しない。
  • The ADK package : ADK board 用のファームウェアおよび ADK board とシールド用の hardware design files。

をまずはとってきて、ドキュメントの手順通りにセットアップする。

で、DemoKit を動かす。


さて、ここまではできるよ。手順通りにやればいいだけだからね。
でもさ、DemoKit 取り外して、アクセサリに LED 1個つなげて光らせるだけでも自分だけでやれって言われると、
はて、どうしたら、、、?ってなるのですよ。

とりあえず DemoKit のコード見るよねー。
アクセサリ側はまだいいんだけど、Android アプリ側がすごく複雑。。。
とりあえず LED を on/off するだけの最小構成が知りたいのに操作できる項目が多い(DemoKit では入力としてリレー、3色LEDx3。出力としてジョイスティック、ボタン、タッチセンサー、温度、湿度がある)のでレイアウトも複雑だし、クラスも多い。。。。

やはりここは HelloWorld ならぬ HelloADK が必要なのでは。

ということでつくりましたよっと。



1. Android app

まずは Android アプリ側から。

注意点: Build Target は 2.3.3 (API Level 10) の Google APIs にする。

USB Accessory モードはもともと USB Host と同じく Android 3.0 からサポートされたものなのですが、Android 2.3.4 にバックポートされています。このバックポートを使うには 2.3.3 の Google APIs を指定する必要があります!



LED を on / off するだけなんでレイアウトはトグルボタン1個でいいよね。
ADK に接続してるかどうかのステータスも出すようにしよう。

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:gravity="center" android:orientation="vertical" > <TextView android:id="@+id/status" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="10dip" /> <ToggleButton android:id="@+id/toggleBtn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="10dip" /> <TextView android:id="@+id/ledState" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="10dip" /> </LinearLayout>

では肝心の Activity 側。まずは UsbManager のインスタンスを取得します。
Add-on library (つまり 2.3.4 にバックポートされたライブラリ)を使っている場合は
UsbManager manager = UsbManager.getInstance(this);
3.0 以降のプラットフォーム標準の API を使う場合は
UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);

流れ的には

onCreate()
 ・UsbManager の取得
 ・オレオレIntent action でパーミッション依頼用の
  PendingIntent 作成
 ・オレオレIntent action とアクセサリが外されたときの
  BroadcastReceiver 登録
 ・画面のセットアップ

onResume()
 ・USBAccessory の一覧を取得して、権限チェック。
  あればアクセサリとの接続を開く
  (UsbAccessory からファイルデスクリプタを取得し
   ファイルデスクリプタから入出力用の Stream を取得。
   入力を受け取るためのスレッドを開始)
  、なければパーミッションを依頼

onPause()
 ・アクセサリとの接続閉じる

onDestroy()
 ・onCreate() で登録した BroadcastReceiver 解除

・View が操作されたら出力用ストリームにバイト列書き込む。
・入力受け取り用スレッドでバイト列受け取ったらUIスレッドに渡してViewに反映

こんな感じかな。 あとは、コメントみてくれ。

public class MainActivity extends Activity implements Runnable { private static final String TAG = "HelloLED"; private static final String ACTION_USB_PERMISSION = "com.uphyca.android.app.helloled.action.USB_PERMISSION"; private PendingIntent mPermissionIntent; private boolean mPermissionRequestPending; private UsbManager mUsbManager; private UsbAccessory mAccessory; ParcelFileDescriptor mFileDescriptor; FileInputStream mInputStream; FileOutputStream mOutputStream; private ToggleButton mToggleButton; private TextView mLedStatusView; private TextView mStatusView; private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (ACTION_USB_PERMISSION.equals(action)) { synchronized (this) { // Intent からアクセサリを取得 UsbAccessory accessory = UsbManager.getAccessory(intent); // パーミッションがあるかチェック if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { // 接続を開く openAccessory(accessory); } else { Log.d(TAG, "permission denied for accessory " + accessory); } mPermissionRequestPending = false; } } else if (UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)) { // Intent からアクセサリを取得 UsbAccessory accessory = UsbManager.getAccessory(intent); if (accessory != null && accessory.equals(mAccessory)) { // 接続を閉じる closeAccessory(); } } } }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // UsbManager のインスタンスを取得 mUsbManager = UsbManager.getInstance(this); // オレオレパーミッション用 Broadcast Intent mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0); // オレオレパーミッション Intent とアクセサリが取り外されたときの Intent を登録 IntentFilter filter = new IntentFilter(); filter.addAction(ACTION_USB_PERMISSION); filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED); registerReceiver(mUsbReceiver, filter); setContentView(R.layout.main); mToggleButton = (ToggleButton) findViewById(R.id.toggleBtn); mLedStatusView = (TextView) findViewById(R.id.ledState); mStatusView = (TextView) findViewById(R.id.status); mToggleButton.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { byte command = 0x1; byte value = (byte) (isChecked ? 0x1 : 0x0); sendCommand(command, value); } }); enableControls(false); } @Override public void onResume() { super.onResume(); if (mInputStream != null && mOutputStream != null) { return; } // USB Accessory の一覧を取得 UsbAccessory[] accessories = mUsbManager.getAccessoryList(); UsbAccessory accessory = (accessories == null ? null : accessories[0]); if (accessory != null) { // Accessory にアクセスする権限があるかチェック if (mUsbManager.hasPermission(accessory)) { // 接続を開く openAccessory(accessory); } else { synchronized (mUsbReceiver) { if (!mPermissionRequestPending) { // パーミッションを依頼 mUsbManager.requestPermission(accessory, mPermissionIntent); mPermissionRequestPending = true; } } } } else { Log.d(TAG, "mAccessory is null"); } } @Override public void onPause() { super.onPause(); closeAccessory(); } @Override public void onDestroy() { unregisterReceiver(mUsbReceiver); super.onDestroy(); } private void openAccessory(UsbAccessory accessory) { // アクセサリにアクセスするためのファイルディスクリプタを取得 mFileDescriptor = mUsbManager.openAccessory(accessory); if (mFileDescriptor != null) { mAccessory = accessory; FileDescriptor fd = mFileDescriptor.getFileDescriptor(); // 入出力用のストリームを確保 mInputStream = new FileInputStream(fd); mOutputStream = new FileOutputStream(fd); // この中でアクセサリとやりとりする Thread thread = new Thread(null, this, "DemoKit"); thread.start(); Log.d(TAG, "accessory opened"); enableControls(true); } else { Log.d(TAG, "accessory open fail"); } } private void closeAccessory() { enableControls(false); try { if (mFileDescriptor != null) { mFileDescriptor.close(); } } catch (IOException e) { } finally { mFileDescriptor = null; mAccessory = null; } } private void enableControls(boolean enable) { if (enable) { mStatusView.setText("connected"); } else { mStatusView.setText("not connected"); } mToggleButton.setEnabled(enable); } private static final int MESSAGE_LED = 1; private class LedMsg { private byte on; public LedMsg(byte on) { this.on = on; } public boolean isOn() { if(on == 0x1) return true; else return false; } } // ここでアクセサリと通信する @Override public void run() { int ret = 0; byte[] buffer = new byte[16384]; int i; // アクセサリ -> アプリ while (ret >= 0) { try { ret = mInputStream.read(buffer); } catch (IOException e) { break; } i = 0; while (i < ret) { int len = ret - i; switch (buffer[i]) { case 0x1: // 2byte のオレオレプロトコル // 0x1 0x0 や 0x1 0x1 など if (len >= 2) { Message m = Message.obtain(mHandler, MESSAGE_LED); m.obj = new LedMsg(buffer[i + 1]); mHandler.sendMessage(m); } i += 2; break; default: Log.d(TAG, "unknown msg: " + buffer[i]); i = len; break; } } } } // UI スレッドで画面上の表示を変更 private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MESSAGE_LED: LedMsg o = (LedMsg) msg.obj; handleLedMessage(o); break; } } }; private void handleLedMessage(LedMsg l) { if(l.isOn()) { mLedStatusView.setText("ON"); } else { mLedStatusView.setText("OFF"); } } // アプリ -> アクセサリ public void sendCommand(byte command, byte value) { byte[] buffer = new byte[2]; if(value != 0x1 && value != 0x0) value = 0x0; // 2byte のオレオレプロトコル // 0x1 0x0 や 0x1 0x1 buffer[0] = command; buffer[1] = value; if (mOutputStream != null) { try { mOutputStream.write(buffer); } catch (IOException e) { Log.e(TAG, "write failed", e); } } } }

つぎにアクセサリをフィルターするための XML ファイルを作成
manufacturer と model と version はアクセサリ側のコードの aac() の引数をあわせる。

res/xml/accessory_filter.xml


最後の AndroidManifest.xml にいろいろ宣言
・<uses-library android:name="com.android.future.usb.accessory" />
・android.hardware.usb.action.USB_ACCESSORY_ATTACHED の intentfilter
・android.hardware.usb.action.USB_ACCESSORY_ATTACHED の meta-data

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.uphyca.android.helloadk" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="10" /> <application android:icon="@drawable/ic_launcher" android:label="@string/app_name" > <uses-library android:name="com.android.future.usb.accessory" /> <activity android:label="@string/app_name" android:launchMode="singleInstance" android:name=".MainActivity" android:taskAffinity="" > <intent-filter > <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <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> </application> </manifest>

これでアプリ側は完了。



2. アクセサリ側

アプリ側で定義したオレオレプロトコルに沿って LED 用のデジタル出力ピンのレベルの HIGH/LOW を切り替えるだけ。

#include <Wire.h> #include <Max3421e.h> #include <Usb.h> #include <AndroidAccessory.h> #define LED 24 AndroidAccessory acc("uPhyca", "HelloADK", "DemoKit Arduino Board", "1.0", "http://www.android.com", "0000000012345678"); void setup(); void loop(); void init_leds() { // 24 の DIGITAL を LED 用の出力にする pinMode(LED, OUTPUT); } void setup() { Serial.begin(115200); Serial.print("\r\nStart"); init_leds(); acc.powerOn(); } void loop() { byte msg[2]; byte led; if (acc.isConnected()) { int len = acc.read(msg, sizeof(msg), 1); if (len > 0) { if (msg[0] == 0x1) { if(msg[1] == 0x1) { digitalWrite(LED, HIGH); msg[0] = 0x1; msg[1] = 0x1; acc.write(msg, 2); } else { digitalWrite(LED, LOW); msg[0] = 0x1; msg[1] = 0x2; acc.write(msg, 2); } } } } else { digitalWrite(LED, LOW); } delay(10); }

DIGITAL の 24 を LED のアノードにつないで、カソードを抵抗につないで、抵抗を GND につないでと。



最初に接続を許可するか聞かれるので OK する。



トグルボタンで LED の ON /OFF できたー。





GitHub にもおいといたよ!
yanzm/HelloADK - GitHub - http://goo.gl/bhG1t



1 件のコメント:

  1. DemoKitを見てもなかなか理解できなかったのですが、こちらの記事を参考にさせて頂き初心者の私でも形になったものが作れました!ありがとうございました!!
    http://www.youtube.com/watch?v=bZR-oVh_rOI

    返信削除