Android で USB 通信を行う為の API が有ったので試してみた。

USB の API は ADK(Accessory Development Kit)と呼ばれている物。

Android がホストになる場合とUSB機器になる場合があるが今回試したのは後者の方で Android が PC の USBデバイスとして認識されるケース。

Android側

API の選択

ADK は android.hardware.usb と com.android.future.usb の2つがある。 この2つは基本的に同じなのだけど後者は Android/2.3.4 向けの互換用。

違いは基本インスタンスの取得方法だけ。

android.hardware.usb:

UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);
UsbAccessory accessory = (UsbAccessory) intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY);

com.android.future.usb:

UsbManager manager = UsbManager.getInstance(this);
UsbAccessory accessory = UsbManager.getAccessory(intent);

今回は android.hardware.usb を使用する。

AndroidManufest.xml の設定

USB 関連の設定を追加して置く。

AndroidManufest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="org.kotemaru.android.usbsample" android:versionCode="1"
    android:versionName="1.0">

    <uses-sdk android:minSdkVersion="12" android:targetSdkVersion="17" />

    <application android:allowBackup="true" android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" android:theme="@style/AppTheme">

        <uses-feature android:name="android.hardware.usb.accessory" />

        <activity android:name="org.kotemaru.android.usbsample.MainActivity"
            android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />

                <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" />
            </intent-filter>
        </activity>
    </application>

</manifest>

res/xml/accessory_filter.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <usb-accessory
        manufacturer="kotemaru.org"
        model="AdkSample"
        version="1.0" />
</resources>

ソース

MainAcrivity.java:

  • UsbReceiverとUsbDriverを呼んでいる所以外は普通のActivity。
package org.kotemaru.android.usbsample;

import java.io.IOException;

import android.os.Bundle;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Intent;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;

public class MainActivity extends Activity {

    private static final String ACTION_USB_PERMISSION = "org.kotemaru.android.usbsample.USB_PERMISSION";

    private UsbDriver usbDriver;
    private UsbReceiver usbReceiver;
    private EditText editText1;
    private EditText editText2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        usbDriver = new UsbDriver(this);
        usbReceiver = UsbReceiver.init(this, ACTION_USB_PERMISSION, usbDriver);
        
        editText1 = (EditText) findViewById(R.id.editText1);
        editText2 = (EditText) findViewById(R.id.editText2);

        Button sendBtn = (Button) findViewById(R.id.sendBtn);
        sendBtn.setOnClickListener(new OnClickListener(){
            @Override public void onClick(View v) {
                String msg = editText1.getText().toString();
                try {
                    usbDriver.send(msg);
                    String rmsg = usbDriver.receive();
                    editText2.setText(rmsg);
                } catch (IOException e) {
                    errorDialog(e.getMessage());
                }
            }
        });
        Button resetBtn = (Button) findViewById(R.id.resetBtn);
        resetBtn.setOnClickListener(new OnClickListener(){
            @Override public void onClick(View v) {
                restart();
            }
        });
    }

    @Override
    public void onResume() {
        super.onResume();
        usbReceiver.resume();
    }

    @Override
    public void onPause() {
        super.onPause();
        usbReceiver.close();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        usbReceiver.destroy();
    }

    protected void restart() {
        Intent intent = this.getIntent();
        this.finish();
        this.startActivity(intent);
    }
    public void errorDialog(String message) {
        AlertDialog.Builder dialog = new AlertDialog.Builder(this);
        dialog.setTitle("Error!");
        dialog.setMessage(message);
        dialog.show();
    }
}

UsbReceiver.java:

  • USB の ATTACH/DETTACH イベントを受け取る為の Receiver。
  • pause/resume の処理が有るため若干ややこしくなっている.
package org.kotemaru.android.usbsample;

import android.app.Activity;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.usb.UsbAccessory;
import android.hardware.usb.UsbManager;
import android.util.Log;

public class UsbReceiver extends BroadcastReceiver {
    private static final String TAG = "UsbReceiver";

    private Activity activity;
    private Driver driver;
    private final String action_usb_permission;

    private UsbManager usbManager;
    private UsbAccessory activeAccessory;

    private PendingIntent permissionIntent;
    private boolean permissionRequestPending = false;

    // I/O処理を分離する為のインターフェース。
    public interface Driver {
        public void openAccessory(UsbAccessory accessory);
        public void closeAccessory(UsbAccessory accessory);
    }

    public static UsbReceiver init(Activity activity, String permissionName, Driver driver) {
        UsbReceiver receiver = new UsbReceiver(activity, permissionName, driver);

        /* receiver */
        IntentFilter filter = new IntentFilter();
        filter.addAction(permissionName);
        filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED);
        activity.registerReceiver(receiver, filter);

        return receiver;
    }

    public UsbReceiver(Activity activity, String permissionName, Driver driver) {
        super();
        this.activity = activity;
        this.action_usb_permission = permissionName;
        this.driver = driver;

        this.usbManager = (UsbManager) activity.getSystemService(Context.USB_SERVICE);
        this.permissionIntent =
                PendingIntent.getBroadcast(activity, 0, new Intent(permissionName), 0);
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (action_usb_permission.equals(action)) {
            open(intent);
        } else if (UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)) {
            close(intent);
        }
    }

    private synchronized void open(Intent intent) {
        UsbAccessory accessory = (UsbAccessory) intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY);
        if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
            driver.openAccessory(accessory);
            activeAccessory = accessory;
        } else {
            Log.d(TAG, "permission denied for accessory " + accessory);
        }
        permissionRequestPending = false;
    }

    private synchronized void close(Intent intent) {
        UsbAccessory accessory = (UsbAccessory) intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY);
        if (accessory != null && accessory.equals(activeAccessory)) {
            close();
        }
    }

    public void resume() {
        UsbAccessory[] accessories = usbManager.getAccessoryList();
        UsbAccessory accessory = (accessories == null ? null : accessories[0]);
        if (accessory != null) {
            if (usbManager.hasPermission(accessory)) {
                driver.openAccessory(accessory);
            } else {
                synchronized (this) {
                    if (!permissionRequestPending) {
                        usbManager.requestPermission(accessory, permissionIntent);
                        permissionRequestPending = true;
                    }
                }
            }
        } else {
            Log.d(TAG, "accessory is null");
        }
    }

    public synchronized void close() {
        if (activeAccessory != null) {
            driver.closeAccessory(activeAccessory);
            activeAccessory = null;
        }
    }
    public void destroy() {
        activity.unregisterReceiver(this);
    }
}

UsbDriver.java:

  • USBへのI/O処理。
  • USB接続はFileDescriptor扱いなのでほぼ普通の java.io と変わらない。
package org.kotemaru.android.usbsample;

import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

import android.content.Context;
import android.hardware.usb.UsbAccessory;
import android.hardware.usb.UsbManager;
import android.os.ParcelFileDescriptor;
import android.util.Log;

public class UsbDriver implements UsbReceiver.Driver {
    private static final String TAG = "UsbDriver";

    private UsbManager usbManager;

    private ParcelFileDescriptor fileDescriptor;
    private FileInputStream usbIn;
    private FileOutputStream usbOut;

    public UsbDriver(MainActivity activity) {
        this.usbManager = (UsbManager) activity.getSystemService(Context.USB_SERVICE);
    }

    @Override
    public void openAccessory(UsbAccessory accessory) {
        fileDescriptor = usbManager.openAccessory(accessory);

        if (fileDescriptor != null) {
            FileDescriptor fd = fileDescriptor.getFileDescriptor();
            usbIn = new FileInputStream(fd);
            usbOut = new FileOutputStream(fd);
        } else {
            Log.d(TAG, "accessory open fail");
        }
    }

    @Override
    public void closeAccessory(UsbAccessory accessory) {
        try {
            if (fileDescriptor != null) {
                fileDescriptor.close();
            }
        } catch (IOException e) {
            // ignore.
        } finally {
            fileDescriptor = null;
        }
    }

    public void send(String msg) throws IOException {
        usbOut.write(msg.getBytes("UTF-8"));
        usbOut.flush();
    }
    public String receive() throws IOException {
        byte[] buff = new byte[1024];
        int len = usbIn.read(buff);
        return new String(buff,0,len,"UTF-8");
    }
}

PC 側

PC 側は Ubuntu,FreeBSD,RaspberryPi で接続を確認している。 libusb を使用するので Windows でも繋がるはず。

libusb の準備

linux系の場合は libusb-1.0 をインストールする。FreeBSD はOS組込なので不要。

$ sudo apt-get install libusb-1.0

libusb で ADK のプロトコルを実装したライブラリ AOA をこちらからお借りしました。 (若干修正が入っています。)

ソース

受け取った文字列を大文字に変換して送り返すだけです。 言語は C++ です。

AdkEcho.cpp:

/**
Android USB connection sample.
@Author kotemru.org
@Licence apache/2.0
*/

#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <signal.h>
#include <string.h>

#include "AOA/AOA.h"

// USB Connector opend.
AOA acc("kotemaru.org",
        "AdkSample",
        "Sample for ADK",
        "1.0",
        "http://blog.kotemaru.org/androidUSBSample",
        "000000000000001") ;

/**
 * Disconnect USB innterrupt aborted.
 */
void signal_callback_handler(int signum)
{
    fprintf(stderr, "\ninterrupt %d\n",signum);
    acc.disconnect();
    exit(0);
}

static void error(char *msg, int rc) {
    fprintf(stderr,"Error(%d,%s): %s\n",rc,strerror(errno),msg);
    acc.disconnect();
    exit(0);
}

int main(int argc, char *argv[])
{
    signal(SIGINT, signal_callback_handler);
    signal(SIGTERM, signal_callback_handler);

    unsigned char buff[1024];

    acc.connect(100);
    // Echo back.
    while (1) {
        int len = acc.read(buff, sizeof(buff), 1000000);
        if (len < 0) error("acc.read",len);
        buff[len+1] = '\0';
        printf("USB>%s\n", buff);
        for (int i=0; i<len; i++) buff[i] = buff[i] - 0x20;
        acc.write(buff, len, 1000);
    }
}

実行結果

先に Android 側アプリを起動して置きます。

USBをPCに接続してから PC 側のプログラムを起動します。 (※root権限が必要です。)

$ sudo ./AdkEcho
VID:18D1, PID:2D01 Class:00
already in accessory mode.
bNumInterfaces: 2
bNumEndpoints: 2
 bEndpointAddress: 81, bmAttributes:02
 bEndpointAddress: 02, bmAttributes:02
VID:18D1, PID:2D01

Android 側にダイアログが出るので応答します。

接続したら英小文字の文書を入力して「Send」をタップします

英大文字が帰ってきたら成功です。

所感

正直、ネットワークで無くUSBでPCと繋げたいケースと言うのが余り思い付かない。

今のところ、RaspberryPiのTTY端末くらい。

何か面白いアイデアが有ったら教えて下さい。

ソースのSVNは以下に有ります。

  • https://kotemaru.googlecode.com/svn/trunk/androidUSBSample