2014/04/03

Android で OCR を試してみた

Nexus5 を買いました。 実機でカメラが使えるようになったので OCR をやって見たいと思います。

Android 用の OCR ライブラリ

Google の OSS である Tesseract OCR をAndroid用に NDK でコンパイルするのがスタンダードなようです。

Tesseract OCR は C++ で記述されているので Android 向けの JavaAPI を被せた tess-two というプロジェクトを使います。

その他、NHocr と言うのが有るようですが Android 用の環境が良く分からないので今回はパスします。

tess-two プロジェクトのチェックアウトとコンパイル

tess-two は NDK の開発環境を必要とするので事前に構築してください。 -> Android NDK の環境構築

Eclipse で以下の git をクローンします。

  • https://github.com/rmtheis/tess-two.git

GITリポジトリの中から tess-two フォルダをインポートします。

プロジェクトのメニューから「Androidツール」->「Add Native Support」を選択します。

コンソールにこんなエラーが出ている場合は API Level 8 の SDK を追加するか AndroidManifest.xml と product.properties の API Level を書き換えて下さい。

JNI は「自動的にビルド」は効かないので手動でプロジェクトのメニューから「プロジェクトをビルド」を選択します。

結構長いことコンパイルが走って4種類のCPU用の .so ファイルが生成されます。

これでライブラリの準備が完了です。

サンプルプロジェクトのチェックアウトとコンパイル

インド人の Gautam Gupta さんのサンプルを使わせてもらいます。

Eclipse で以下の git をクローンしてプロジェクトをインポートします。

  • https://github.com/GautamGupta/Simple-Android-OCR.git

プロジェクトのプロパティの Android に tess-two プロジェクトをライブラリとして追加します。



SimpleAndroidOCRActivity.javaを開いてソースコードを一部修正します。

super.onCreate()の呼び出しをメソッドの最初に移動します。
※Kitkatで動かなかったため

94:         super.onCreate(savedInstanceState);
↓
47:     public void onCreate(Bundle savedInstanceState) {
48:         super.onCreate(savedInstanceState);

言語指定を英語から日本語に変更します。

34:     public static final String lang = "eng";
                                           ↓
34:     public static final String lang = "jpn";

tess-two のプロジェクトから日本語用の学習データをダウンロードします。

学習データファイルを assets/tessdata に配置します。

これで準備完了です。

結果

実機を繋げて実行してみます。

レシートをカメラで撮影し数秒待たされて出てきた結果がこれです。

=>

なんか惜しい感じw

所感

このままでは使えない感じですが、ボールド体のはっきりした文字は結構正確に認識しているように見えます。

アプリ側で画像処理を行ってスキャナで取り込んだような画像をライブラリに渡せればもっと精度をあげられるのではないでしょうか。


2014/04/02

Android NDK の環境構築

Android の NDK を試そうと開発環境を作ろうとしたら参考サイトのどれも古くて現状と合わないので とりあえず自分でメモ。 現状はEclipseのプラグインが全部やってくれるようです。 と言ってもこれもすぐ古くなっちゃうんだろうな。

前提条件

  • 2014年4月時点です。
  • Eclipse 4.2 の pleiades です。
  • Android-SDK は設定済みです。
  • OSは Windows7/64 です。

NDK のインストール

以下のサイトから環境に合う NDK をダウンロードして任意の場所に展開します。

この時点のバージョンは r9d でした。

Eclipse のプラグインのインストール

C/C++ の開発環境

「ヘルプ」->「新規ソフトウェアのインストール」から以下の3つをインストールする。

  • URL: http://download.eclipse.org/releases/juno/
    • ▽ プログラミング言語
      • ■ C/C++ 開発ツール
      • ■ C/C++ 開発ツール SDK
      • ■ ライブラリ API ドキュメンテーション~

NDK プラグイン

「ヘルプ」->「新規ソフトウェアのインストール」から以下の1つをインストールする。

  • URL: https://dl-ssl.google.com/android/eclipse/
    • ▽ NDK プラグイン
      • ■ Android ネイティブ開発ツール

Eclipse 再起動後、「ウィンドウ」->「設定」から「Android」->「NDK」を選んで NDK Location に展開した NDK のフォルダを設定する。

サンプルプロジェクト

新規プロジェクトで「既存コードからのAndroidプロジェクト」を選びます。

展開した NDK のフォルダの samples フォルダからプロジェクトを選択します。

プロジェクトのメニューから「Androidツール」->「Add Native Support」を選択します。

jni フォルダが認識されます。

JNI は「自動的にビルド」は効かないので手動でプロジェクトのメニューから「プロジェクトをビルド」を選択します。

正常にコンパイルされると各CPUアーキテクチャ用の .so ファイルが生成されます。

この状態で apk を作成すると .so を含んだ形で生成されます。

感想

Eclipseのプラグインが全部やってくれるのでかなり簡単です。

古い NDK の環境設定の解説にはコマンド操作が書いて有ったりしてかえって混乱するので注意してください。


2014/03/05

ViewPagerのループ

リモコンアプリを作って気がついたのだが ViewPager ってページの循環(ループ)が出来ない。

どうしても循環させたい場合は ViewPager を全部作り直す必要が有るっぽい。

このライブラリ使わせてもらえ、で話は終わってしまうのですがもう少し突っ込んでみました。

PagerAdapter だけで頑張ってみた。

ViewPager のソースを見ると子要素として持っている View は表示中のページとその左右のページの3枚のみでした。

ならば最初と最後に擬似的なページを用意してやればループしたように見せられるじゃないでしょうか。

こういうことです。

C'A B C A'
  • A と A' の View は共有します。
  • A' に遷移した時は A にジャンプします。
  • C,C' も同じです。

具体的に実装してみた物がこれです。

public class LoopPagerAdapter extends PagerAdapter {
    private static final String TAG = "LoopPagerAdapter";

    private ViewPager viewPager;
    private Page[] pages;

    public LoopPagerAdapter(ViewPager viewPager, View[] views) {
        super();
        this.viewPager = viewPager;
        this.pages = new Page[views.length+2];

        for (int i=0;i<views.length; i++) {
            pages[i+1] = new Page(views[i]);
        }
        pages[0] = new Page(views[views.length-1]);
        pages[pages.length-1] = new Page(views[0]);
        viewPager.setOnPageChangeListener(onPageChangeListener);
    }

    private OnPageChangeListener onPageChangeListener = new OnPageChangeListener() {
        @Override
        public void onPageSelected(int position) {
            if (position == 0) viewPager.setCurrentItem(getCount()-2,false);
            if (position == getCount()-1) viewPager.setCurrentItem(1,false);
        }
        @Override
        public void onPageScrollStateChanged(int state) {}
        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
    };

    @Override
    public int getCount() {
        return pages.length;
    }

    @Override
    public Object instantiateItem(ViewGroup viewGroup, int position) {
        Log.d(TAG, "instantiateItem:" + position);
        Page page = pages[position];
        View view = page.getView();
        for (int i=0;i<pages.length; i++) {
            if (pages[i].getView() == view) pages[i].setValid(false);
        }
        page.setValid(true);

        viewGroup.removeView(view);
        viewGroup.addView(view);
        return page;
    }

    @Override
    public boolean isViewFromObject(View view, Object object) {
        Page page = (Page) object;
        return page.isValid() && page.getView() == view;
    }

    @Override
    public void destroyItem(ViewGroup viewGroup, int position, Object object) {
        Page page = (Page) object;
        Log.d(TAG, "destroyItem:" + position+" : "+((TextView)page.getView()).getText());
        if (page.isValid()) {
            viewGroup.removeView(page.getView());
        }
        page.setValid(false);
    }
}

public class Page {
    private View view;
    private boolean isValid = true;

    public Page(View view) {
        this.view = view;
    }
    public View getView() {
        return view;
    }
    public boolean isValid() {
        return isValid;
    }
    public void setValid(boolean isValid) {
        this.isValid = isValid;
    }
}

A と A' は同時に存在できないのでどちらが有効なのかを管理する為に Page クラスを用意しています。

実際に動かしてみます。

ちゃんと A と C を行き来できました。
但、必ず3ページ以上必要な上、ViewPager の実装に依存するため将来動かなくなるかもしれませんw

ViewFlipper で代用

ページング用のクラスとしてもう一つ ViewFlipper が有ります。 こちらは最初からループ可能です。
但、Fragment に対応していませんし操作が Swipe でなく Fling になります。

フリックに反応して左右にページ遷移する ViewFlipper のサンプルはこうなります。

public class FlingViewFlipper extends ViewFlipper {
    private static final String TAG = "FlingViewFlipper";
    private final Animation right_in_trans_anim = createAnim(1, 0);
    private final Animation right_out_trans_anim = createAnim(0, 1);
    private final Animation left_in_trans_anim = createAnim(-1, 0);
    private final Animation left_out_trans_anim = createAnim(0, -1);
    private GestureDetector gestureDetector;

    public FlingViewFlipper(Context context, AttributeSet attrSet) {
        super(context, attrSet);
        this.gestureDetector = new GestureDetector(context, onGestureListener);
        setFlipInterval(0);
    }

    // Require delegate from Activiy.onTouchEvent()
    public boolean onTouchEvent(MotionEvent ev) {
        gestureDetector.onTouchEvent(ev);
        return false;
    }

    private OnGestureListener onGestureListener = new SimpleOnGestureListener() {
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            Log.d(TAG, "onFling:" + velocityX);
            if (velocityX < -300) {
                setOutAnimation(left_out_trans_anim);
                setInAnimation(right_in_trans_anim);
                showNext();
                return true;
            } else if (velocityX > 300) {
                setOutAnimation(right_out_trans_anim);
                setInAnimation(left_in_trans_anim);
                showPrevious();
                return true;
            }
            return false;
        }
    };

    private static Animation createAnim(float startX,float entX) {
        Animation anim = new TranslateAnimation(
            Animation.RELATIVE_TO_PARENT, startX, Animation.RELATIVE_TO_PARENT, entX,
            Animation.RELATIVE_TO_PARENT, 0, Animation.RELATIVE_TO_PARENT, 0
        );
        anim.setDuration(300);
        anim.setStartOffset(0);
        return anim;
    }
}

感想

PageViewer はちょと実装が複雑過ぎる気がしました。
PagerAdapter がどう呼ばれるのか PageViewer のソースを見ないと理解が難しいです。

どうしても Fragment でページを管理したいと言う要求がなければ普通に ViewFlipper を使うのがおすすめです。


2014/03/01

USB赤外線リモコン アプリ

Nexus7からUSB赤外線リモコンを操る(前編)「(中編)」 「(後編)」 を元に簡単なリモコンアプリを作りました。

使い方

デバイスの準備

専用のデバイスが必要です。
Nexus7からUSB赤外線リモコンを操る(前編)」 を参照してください。

起動

アプリをインストールした状態でデバイスを接続するとダイアログが表示されるので「OK」をタップするとアプリが自動的に起動します。

赤外線の登録

アプリが起動したらメニューの「登録モード on/off」を選択して登録モードにします。

登録モードはボタンの枠が水色になります。
この状態で登録したいボタンをタップします。

ダイアログが出るのでデバイスに向けてリモコンを操作してください。

正常に登録できるとダイアログが消えるので続けて他のボタンを登録します。

赤外線の送信

もう一度、メニューの「登録モード on/off」を選択して登録モードを解除し各ボタンをタップすれば登録した赤外線が送信されます。

リモコンの選択

横方向にスワイプするとリモコンを選択できます。

登録データの保存と復元

メニューの「登録データ保存」/「登録データ復元」を選択すると登録データの保存と復元が行えます。

現在のところ保存先は Android/data/org.kotemaru.android.irrc/IrData.db に固定です。
ファイル形式は Sqlite です。

リモコン画面のカスタマイズ

現状ではリモコン画面のカスタマイズは Android の開発環境が無いとできません。
SVN からプロジェクトを落として Eclipse で開いてください。

リモコン画面は HTML で記述されています。 テンプレートとなる assets/remocon/1.TV.html を同じフォルダに別名をつけてコピーしてください。 自動的に新しいリモコンとして追加されます。

HTMLに id 属性の付いた <button> を配置すればそのまま登録可能なリモコンのボタンになります。 <button> の id 属性は DB 上のキーとなるので HTMLページ内で一意でなければなりません。

<title> タグはリモコン選択時にアプリタイトルとして表示されるので適当な物を指定して下さい。

アイコンについて

ボタンのアイコンは こちら からお借りしました。 300種類くらい有るので適当な物が見つかると思います。

雑感

本当は、リピート機能やリモコン画面の登録機能も付けたかったのですが需要が不明なのでここまでとしました。 このデバイスを使っている人は基本開発者だと思うで後は好きにしてください。(^^;

Android アプリとしては WebView の JavaScript から USBデバイスの制御をしているので中々面白いものになっていると思います。


2014/02/23

Nexus7からUSB赤外線リモコンを操る(後編)

Nexus7からUSB赤外線リモコンを操る(前編)「(中編)」の続きです。 先に前/中編をみてください。

赤外線リモコンキットのプロトコル

フォーラムにもなぜかプロトコルについての資料がありません。 ファームウェアのソース公開されているので自分で調べろ(or決めろ)って事でしょうか。

仕方ないのでこちらのサイトを参考にファームのソースからプロトコルを調べました。

基本形

パケットは64バイトの固定長です。

要求パケットの1バイト目にコマンドのコードが入り、 応答パケットの1バイト目に同じコマンドのコードが入って戻ってきます。

パケットの2バイト目以降が要求パラメータまたは応答の戻り値となっています。

応答にエラーコードと言うものは無いようなので Timeout で検出と思われます。

家電のリモコンから赤外線データの受信

リモコンから赤外線データを受信するにはデバイスを受信モードにしデータを取得してから受信モードを終了します。

操作パケットデータ(64byte固定)
受信モード開始
(1)  要求->0x53,0x01,0xff…0xff
(2)  応答<-0x53,0x00,0x00…0x00
データ取得(繰り返す)
(3)  要求->0x52,0xff,0xff…0xff
(4)  応答<-0x52,0xXX,0xXX…0xXX
受信モード終了
(5)  要求->0x53,0x00,0xff…0xff
(6)  応答<-0x53,0x00,0x00…0x00

リモコンからまだ赤外線データを受け取っていない場合は (4) の応答の2バイト目が 0x00 となる為、データが取得できるまで (3),(4) を繰り返します。

デバイスから赤外線データの送信

取得したデータを送信します。

(4) で受け取ったデータの1バイト目を送信コマンド(0x61)に差し替えて要求するだけです。
投げっぱなしで応答は有りません。

操作パケットデータ(64byte固定)
データ送信
(7)  要求->0x61,0xXX,0xXX…0xXX

プロトコルの実装

家電のリモコンから赤外線データの受信

応答が非同期となるのでリスナインターフェースを用意します。 後は、パケットを作って非同期タスクに投げるだけです。

public interface IrrcResponseListener {
    public void onIrrcResponse(byte[] data);
}

public void startReceiveIR(IrrcResponseListener listener) {
    byte[] buff = initBuffer(new byte[PACKET_SIZE], (byte) 0xff);
    buff[0] = RECEIVE_IR_MODE_CMD;
    buff[1] = 1;
    new RequestAsyncTask(listener).request(buff, true, false);
}

public void endReceiveIR(IrrcResponseListener listener) {
    byte[] buff = initBuffer(new byte[PACKET_SIZE], (byte) 0xff);
    buff[0] = RECEIVE_IR_MODE_CMD;
    buff[1] = 0;
    new RequestAsyncTask(listener).request(buff, true, false);
}

public void getReveiveIRData(IrrcResponseListener listener) {
    byte[] buff = initBuffer(new byte[PACKET_SIZE], (byte) 0xff);
    buff[0] = RECEIVE_IR_DATA_CMD;
    new RequestAsyncTask(listener).request(buff, true, true);
}

デバイスから赤外線データの送信

パケットを作って非同期タスクに投げるだけです。

public void sendData(byte[] buff) {
    buff[0] = SEND_IR_CMD;
    new RequestAsyncTask(null).request(buff, false, false);
}

非同期タスク

応答有無、リトライ有無の指定にしたがってプロトコルにそった送受信を行っているだけです。
異常系やキャンセル処理への考慮は不十分です。
#APIが混乱しているのは仕様ですw

private class RequestAsyncTask extends AsyncTask<byte[], Void, byte[]> {
    private IrrcResponseListener listener;
    private boolean withResponse = false;
    private boolean withRetry = false;

    public RequestAsyncTask(IrrcResponseListener listener) {
        this.listener = listener;
    }

    public void request(byte[] buff, boolean withResponse, boolean withRetry) {
        this.withResponse = withResponse;
        this.withRetry = withRetry;
        execute(buff);
    }

    @Override
    protected byte[] doInBackground(byte[]... args) {
        Log.d(TAG, "RequestAsyncTask start");
        try {
            byte[] reqData = args[0];
            byte[] resData = null;
            boolean isRetry = false;
            do {
                doRequest(reqData);
                if (withResponse) {
                    resData = doResponse();
                    if (resData[0] != reqData[0]) {
                        Log.e(TAG, "Bad resposne code " + resData[0]);
                        return null;
                    }
                    if (withRetry && resData[1] == 0x00) {
                        sleep(500);
                        isRetry = true;
                    } else {
                        isRetry = false;
                    }
                }
            } while (isRetry);
            return resData;
        } catch (Throwable t) {
            Log.e(TAG, t.getMessage(), t);
            return null;
        }
    }

    @Override
    protected void onPostExecute(byte[] result) {
        if (listener != null) {
            listener.onIrrcResponse(result);
        }
    }
    private void doRequest(byte[] buff) throws IOException {
        …省略(中編参照)
    }
    private byte[] doResponse() throws IOException {
        …省略(中編参照)
    }
}

動かしてみる

受信と送信のボタン2つだけの Activity を作って動かしてみました。

Nexus7に繋げるのですがここで一つ問題が。
赤外線リモコンキットのコネクタは Mini-USB なので micro-USB と直結しようとするとレアなケーブルが必要で手持ちに有りませんでした。

結果こんな事に(笑)

それはそれとして、
アプリの受信ボタンをタップしてからデバイスの受光部分に向けてリモコンを操作します。
電源ボタンを押してみました。

デバイスを家電機器に向けてアプリの送信ボタンをタップすると無事、家電の電源が入って実験成功です。

 

まとめ

これで Nexus7 から赤外線リモコンキットを操作することが可能になりました。

基本的に Android から USBデバイスを操作するのは同じ流れで行けると思うのですが やはり OSが一部デバイスをアプリに使わせてくれないのは致命的な問題のような気がします。 普通のUSBデバイスはファームの書き換えなんてさせてくれませんから。

追々、リモコンアプリを作って行きたいのですが ボタン配置のカスマイズをできるようにしないといけないので以外に難しそうです。

ソース全体は以下のSVNを参照して下さい。


2014/02/22

Nexus7からUSB赤外線リモコンを操る(中編)

Nexus7からUSB赤外線リモコンを操る(前編)」の続きです。先に前編をみてください。

以前 USB 接続をやった時は PC がホストで Android がデバイスだったけれど今回は Android がホストになるのでやること多いです。

基本的なやり方はいつもの勝手に翻訳さんのサイトを参照しました。

但し、割と内容が薄く書いて無い注意事項が結構あります。

マニフェスト

マニフェストはドキュメント通りです。

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="org.kotemaru.android.irrc" android:versionCode="1" android:versionName="1.0" >
    <uses-sdk android:minSdkVersion="12" />
    <permission android:name="android.hardware.usb.host" ></permission>
    <uses-feature android:name="android.hardware.usb.host" android:required="true" />

    <application
        android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" >
        <activity
            android:name="org.kotemaru.android.irrc.MainActivity"
            android:configChanges="orientation|screenSize"
            android:label="@string/app_name" >
            <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_DEVICE_ATTACHED" />
            </intent-filter>
            <meta-data
                android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
                android:resource="@xml/device_filter" />
        </activity>
    </application>
</manifest>

USB_DEVICE_ATTACHED で Activity を起動する設定にすると 既に Activity が起動していても USB_DEVICE_ATTACHED で Activity が onCreate() から再起動されます。

LAUNCHER からも起動できるようにして有るとちょっとややこしい感じになります。

res/xml/device_filter.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <usb-device vendor-id="8938" product-id="30" />
</resources>

デバイスを特定する情報を記述します。 注意事項は値の記法が10進数なことです。 最初、16進数で書いてはまりました。

USBデバイスの構造

USBデバイスの構造は少し複雑なので整理します。
1つのUSBデバイスは複数のインターフェース(機能)を持ちます。通常は1つです。
各インターフェースは入出力の Endpoint を複数持ちます。通常は1つか2つです。

赤外線リモコンキットは4つのインタフェースを持ち以下のような構造になっていました。
但し、データ通信意外は前編のファームウェアによりダミーになっています。

  • UsbDevice : 赤外線リモコンキット
    • UsbInterface[0] : キーボード
      • UsbEndpoint[0] : IN(INTERRAPT)
      • UsbEndpoint[1] : OUT(INTERRAPT)
    • UsbInterface[1] : マウス
      • UsbEndpoint[0] : IN(INTERRAPT)
    • UsbInterface[2] : ボリュームコントローラ
      • UsbEndpoint[0] : IN(INTERRAPT)
    • UsbInterface[3] : データ通信
      • UsbEndpoint[0] : IN(INTERRAPT)
      • UsbEndpoint[1] : OUT(INTERRAPT)

UsbEndpoint のタイプは CONTROL,ISOC,BULK,INTERRAPT の4種類あります。 通常アプリが使用するのは BLUK か INTERRAPT で赤外線リモコンキットは IN/OUT ともに INTERRAPT(非同期) で通信します。

詳細は以下のサイトが詳しいです。

デバイスの認識

起動直後の処理

public static IrrcUsbDriver init(MainActivity activity, String permissionName) {
    IrrcUsbDriver driver = new IrrcUsbDriver(activity, permissionName);
    // USB_DEVICE_ATTACHEDから起動された場合は intent がデバイスを持っている。
    UsbDevice device = activity.getIntent().getParcelableExtra(UsbManager.EXTRA_DEVICE);
    if (device == null) {
        // LAUNCHER からの起動の場合は接続済デバイス一覧から検索する。
        device = findDevice(driver.usbManager, VENDER_ID, PRODUCT_ID);
    }
    /*
     * USB_DEVICE_ATTACHED で起動するように AndroidManifest.xml を記述すると
     * USB_DEVICE_ATTACHED で必ず onCreate() が呼ばれるので Activity から設定した Receiver は呼ばれない。
     * 従って、ここで onAttach() を呼ぶ。
     */
    driver.onAttach(device);
    return driver;
}

USB_DEVICE_ATTACHED から Activity が起動された場合は intent がデバイスを持っているでそのままデバイス認識の処理に入ります。

LAUNCHER から起動された場合は自前でデバイス一覧から検索します。 それでも見つからない場合の処理はアプリしだいです。

デバイス認識の処理

public String onAttach(UsbDevice device) {
    Log.d(TAG, "onAttach:" + device);
    usbDevice = device;
    if (usbDevice == null) {
        Log.e(TAG, "Not found USB Device.");
        return "Not found USB Device.";
    }
    if (usbManager.hasPermission(usbDevice)) {
        return onStart(usbDevice);
    } else {
        // デバイスの利用許可をユーザに求める。
        // 結果は UsbReceiver.onReceive()にコールバック。
        usbManager.requestPermission(usbDevice, permissionIntent);
    }
    return null;
}

アプリがUSBデバイスを使用するにはユーザの許可が要ります。 許可が無い場合はユーザに許可を求めるリクエストを投げて Receiver で受け取ります。

許可があればデバイスとの接続を開始します。

デバイス接続の処理

public String onStart(UsbDevice device) {
    Log.d(TAG, "onStart:" + device);
    if (! device.equals(usbDevice)) {
        return "No device attach.";
    }
    if (! usbManager.hasPermission(usbDevice)) {
        return "No device permission.";
    }

    usbConnection = usbManager.openDevice(usbDevice);
    // TODO:インターフェースの検出は端折ってます。
    UsbInterface usbIf = usbDevice.getInterface(INTERFACE_INDEX);

    // EndPointの検索。分かってる場合は直接取り出しても良い。
    for (int i = 0; i < usbIf.getEndpointCount(); i++) {
        UsbEndpoint ep = usbIf.getEndpoint(i);
        Log.d(TAG, "tye=" + ep.getType());
        if (ep.getType() == UsbConstants.USB_ENDPOINT_XFER_INT) {
            if (ep.getDirection() == UsbConstants.USB_DIR_IN) {
                endpointIn = ep;
            } else if (ep.getDirection() == UsbConstants.USB_DIR_OUT) {
                endpointOut = ep;
            }
        }
    }
    if (endpointIn == null || endpointOut == null) {
        Log.e(TAG, "Device has not IN/OUT Endpoint.");
        return "Device has not IN/OUT Endpoint.";
    }
    // デバイスの確保
    usbConnection.claimInterface(usbIf, true);
    isReady = true;
    return null;
}

この辺りはドキュメント通りです。
Endpoint のタイプは分かっていますがあえてチェックしています。

デバイスの終了処理

public String onDetach(UsbDevice device) {
    Log.d(TAG, "onDetach:" + device);

    if (!device.equals(usbDevice)) {
        Log.d(TAG, "onDetach: Other device.");
        return "Other device";
    }

    if (usbConnection != null) {
        UsbInterface usbIf = usbDevice.getInterface(INTERFACE_INDEX);
        usbConnection.releaseInterface(usbIf);
        usbConnection.close();
    }
    usbConnection = null;
    usbDevice = null;
    isReady = false;
    return null;
}

ここもドキュメント通りです。注意事項はありません。

レシーバ

レシーバの登録

public static UsbReceiver init(MainActivity activity, Driver driver, String permissionName) {
    UsbReceiver receiver = new UsbReceiver(activity, driver, permissionName);
    IntentFilter filter = new IntentFilter();
    filter.addAction(permissionName);  // USBデバイスの利用許可の通知を受ける。
    filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
    activity.registerReceiver(receiver, filter);
    return receiver;
}

デバイスの利用許可とDETACHEDを受け取ります。

レシーバの処理

@Override
public void onReceive(Context context, Intent intent) {
    String action = intent.getAction();
    Log.d(TAG,"onReceive:"+action);
    UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
    if (permissionName.equals(action)) {
        String errorMeg = driver.onStart(device);
        if (errorMeg != null) {
            activity.errorDialog(errorMeg);
        }
    } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
        if (driver.onDetach(device) == null) {
            activity.finish();
        }
    }
}

デバイス使用許可が来たらデバイスの接続開始処理を呼びます。
DETACHED で Activity を終わらせていますがアプリの仕様しだいです。

データの送受信

USB_ENDPOINT_XFER_INT(非同期) の通信は UI スレッドでは行えません。 必ず、AsyncTask か Thread で行います。

非同期パケット送信

    private void doRequest(byte[] buff) throws IOException {
        Log.d(TAG, "request:" + dump(buff));

        ByteBuffer buffer = ByteBuffer.allocate(buff.length);
        UsbRequest request = new UsbRequest();
        buffer.put(buff);

        request.initialize(usbConnection, endpointOut);
        request.queue(buffer, buff.length);

        UsbRequest finishReq;
        while ((finishReq = usbConnection.requestWait()) != request) {
            if (finishReq == null) throw new IOException("Request failed.");
            sleep(100);
        }
    }

非同期パケット送信の方法はこれでほぼ定形だと思います。

requestWait() の戻り値は同時に走っている他の UsbRequest の場合もあるのでループでチェックします。 null は何らかのエラーが有ったとき返るようです。

非同期パケット受信

    private byte[] doResponse() throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(endpointIn.getMaxPacketSize());
        buffer.clear();
        UsbRequest request = new UsbRequest();
        request.initialize(usbConnection, endpointIn);
        request.queue(buffer, endpointIn.getMaxPacketSize());

        UsbRequest finishReq;
        while ((finishReq = usbConnection.requestWait()) != request) {
            if (finishReq == null) throw new IOException("Request failed.");
            sleep(100);
        }

        // Note: OSバージョンにより flip() の必要性が異なる気がする...
        if (buffer.remaining() == 0) buffer.flip();

        byte[] buff = new byte[buffer.remaining()];
        buffer.get(buff);
        Log.d(TAG, "response:" + dump(buff));
        return buff;
    }

非同期パケット受信もこれでほぼ定形だと思います。

謎なのは エミュレータ(4.0.4) では buffer.flip() は必要無かったのですが Nexus7(4.4.2) では必要になりました。 赤外線リモコンキットはパケットが固定長なのでこのコードで どちらも動作しますがデバイスによっては OSバージョンのチェックが必要かもしれません。

感想

通信できるようになるまで作法と言うか手順が多いですね。
通信自体も非同期だと一手間かかります。
Android の USBホスト実装は基本的にこの形に乗りそうです。

ソース全体は以下のSVNを参照して下さい。

いよいよリモコンを操ります。
Nexus7からUSB赤外線リモコンを操る(後編)」に続きます。


2014/02/21

Nexus7からUSB赤外線リモコンを操る(前編)

スマホを汎用の赤外線リモコンにしたいと言う需要は結構有そうに思うのだがどうだろう。
少なくとも私は以前から欲しかった。

で密林さんで見つけたのがこれ

FRISKのケースに入ると言うUSB接続のマルチリモコンのキットだ。
安いのでキットの方をポチったら翌々日に到着。

部品数が少ないのでハンダごて使える人なら簡単に作れます。

完成品:

PCに メーカのサイト から落としてきたアプリを入れて動作確認するとサクッと動きました。

Android との接続

いきなりはまります。
接続してアプリから UsbManager.getDeviceList() としても何も取得できません。 調べると OS レベルでは認識しているようなのですがアプリから見えないようです。

「UsbManager.getDeviceList empty」でググると一杯出てくるのですがどうも マウス、キーボード、USBメモリはアプリには見せないようになっているらしいです。
仕様として明確になっている記述は見つかりません。
機種によって動く動かないの記事がみられるので USBホスト機能自体ベンダー依存なのかもしれません。

この赤外線リモコンキットはキーボードとマウスのインターフェースを持っているため OSに誤認されデバイス全体が隠蔽されてしまっているようです。

ファームウェアの書き換え

このキットはファームウェアのソースが公開されていてフォーラムからDLできます。 そこでファームウェアを書き換えてキーボード、マウスの機能を無効化する事にします。

フォーラムからファームウェア書換えツールをDLして修正したファームウェアに書き換えます。

ファームウェア書き換え手順

  1. デバイスを外して 赤いスイッチを BOOT側 にします。
  2. デバイスを接続して書換えツール(HIDBootLoader.exe) を起動します。
  3. 「Open hex file」ボタンを押してAndroid用修正ファームウェア(RemoconServant-for-android.hex)を選択します。
  4. 「Program/Verify」ボタンを押して Complete と表示されれば書き換え終了です。
  5. デバイスを外して 赤いスイッチを元に戻します。

ファームウェア修正内容

USBのdescriptor定義をいじってキーボード、マウスと認識されないようにしただけです。

自分でビルドしたい人は以下の修正パッチを当てて -D__FOR_ANDROID でコンパイルして下さい。

ビルドツールが見つけ辛いのでリンクを張って置きます。

ファームウェア書き換え結果

ファームウェア書き換え後、Android に接続するとちゃんとアプリから認識できました。

リモコン アプリ

感想

さらっと書いてますが、ここまでめっちゃ大変でした。

アプリから認識されないUSBデバイスの種類が曖昧だし、 ファームウェアのコンパイラはチップメーカのサイトから消えてるし。

ともあれやっとこれで Android のプログラムに入れます。

Nexus7からUSB赤外線リモコンを操る(中編)」に続きます。


2014/02/17

AndroidのWebViewをPCのChromeでデバッグ

WebView 上で Web アプリを開発する場合に JavaScript のデバッグが大変そうだなと思いどうすれば良いのか調べてみた。

結論から言うと PC の Chrome に ADB プラグインをインストールすると リモートで Chrome のデバッガが使える事が分かった。

しかも WebView がデバックできるようになったのが 4.4(KitKat) からと言うタイムリーさ。

インストール

以下の Chrome ウェブストアからプラグインをインストールすれば終わりです。
(なぜかウェブストアの検索では出てこないのでリンクを直接叩く必要があります。)

「+無料」のボタンをクリックでインストール開始します。

エミュレータの準備

WebView のデバッグは 4.4(KitKat) からなので andrid-sdk の SDK Manager を起動して 4.4.2(Level-19) 以上の開発環境を一式落とします。

次に AVD Manager を起動して 4.4.2(Level-19) 以上のエミュレータイメージを作成して起動します。

Target: の項目が 4.4.2(Level-19) 以上になっていれば他の項目は何でも良いです。

試してませんが 4.4 の実機があればそちらでもデバッグ可能なようです。

WebViewアプリの準備

アプリのどこかに以下のコードを入れてリモートデバッグを有効にします。

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    WebView.setWebContentsDebuggingEnabled(true);
}

API Level-19 以前には存在しないメソッドなのでバージョンチェックが必要です。 クラスメソッドなので WebView インスタンスは必要ありません。

コンパイルしなおしてエミュレータで実行しておけば準備完了です。

デバッガの起動

PC の Chrome を起動するとドロイド君のアイコンが有るのでクリックして 「View Inspection Targets」を選択します。

デバッグ可能なブラウザの一覧が表示されるので「inspect」をクリックします。

別ウインドウでデバッガが起動するので後は通常の Chrome と同じようにデバッグできます。

ブレークポイントで止めて見たところ。

所感

JavaScript のデバッグにおいては Chrome のデバッガは圧倒的に使いやすいのでこれは大変ナイスです。

これで Android におけるWebハイブリッドアプリを開発する為の環境はほぼ揃った用に思えます。


プロフィール
20年勤めた会社がリーマンショックで消滅、紆余曲折を経て現在はフリーランスのSE。 失業をきっかけにこのブログを始める。

サイト内検索

登録
RSS/2.0

カテゴリ

最近の投稿【android】

リンク

アーカイブ