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