2013/08/25

RaspberryPiでJoypadからコマンド入力

RaspberryPiのUSB接続を認識するの続き。

RaspberryPiはキーボード/モニタ無しで LAN のみ接続で使っている人が多いと思うんだけど コマンドを実行したい時に ssh で接続するのが以外に面倒。

とくに shutdown と reboot ぐらいは端末が無くても出来るようにしたい。

で、思い付いたのが joypad で入力する方法。 GPIO を使ってボタンを付ける方法も考えたのだが物理的な工作とか考えると USB Joypad の方が圧倒的にコストパフォーマンスが良い。

今回、購入したのはこれ。安っ。

joypad の入力シーケンスから任意のコマンドを実行するスクリプトは自前で作成。

入力シーケンスと言うのは ↑↓←→ABAB みたいなゲームの隠しコマンドとかで使うやつ。 単独のボタンにコマンド割り当だとうっかり実行しちゃうので。

必要なアプリが2つあるのでインストールする。

$ sudo apt-get install joystick
$ sudo apt-get install gawk
メインの AWK スクリプト。gawk専用(/home/pi/jscmd/jscmd.awk)
BEGIN {
    loadConfig(CONFIG); # -v CONFIG=file_name
    namesSize = split(config["BUTTONS"], names, /,/);
    sequence = "";
    lastModify = systime();
}

/^Event: type 1,/ {
    n = $7; sub(/,$/,"",n);
    v = $9;
    key = names[n+1];
    flag[key] = v;
    if (v) addKey();
}

/^Event: type 2,/ {
    n = $7; sub(/,$/,"",n);
    v = $9;

    if (n == 0) {
        flag["RIGHT"] = (v>0)?1:0;
        flag["LEFT"]  = (v<0)?1:0;
    } else {
        flag["DOWN"] = (v>0)?1:0;
        flag["UP"]   = (v<0)?1:0;
    }
    if (v != 0) addKey();
}

func addKey() {
    t = systime();
    if (t > lastModify+2) sequence = "";
    lastModify = t;

    keys = "";
    for (i=1; i<=namesSize; i++) {
        if (flag[names[i]]) {
            keys = sprintf("%s+%s",keys,names[i]);
        }
    }
    sub(/^[+]/,"",keys);
    sequence = sprintf("%s %s",sequence,keys);
    gsub(/[ ]+/," ",sequence);
    gsub(/^[ ]/,"",sequence);

    print ">",sequence;
    if (config[sequence]) {
        print "exec ",config[sequence];
        system(config[sequence]);
        sequence = "";
    }
}

func loadConfig(CONFIG_FILE) {
    print "--- Config --------------------------";
    while ((getline < CONFIG_FILE) > 0) {
        idx = index($0,"=");
        if (idx <= 0) continue;
        key = substr($0,0,idx-1); sub(/^[ ]+/,"",key);sub(/[ ]+$/,"",key);
        val = substr($0,idx+1); sub(/^[ ]+/,"",val);sub(/[ ]+$/,"",val);
        print key,"=",val;
        config[key] = val;
    }
    print "-------------------------------------";
}
起動スクリプト(/home/pi/jscmd/jscmd-start.sh)
#!/bin/sh -fx

BASE=`dirname $0`
JSDEV=$DEVNAME
if [ "$JSDEV" = "" ] ; then
        JSDEV=/dev/input/js0
fi

cd $BASE
jstest --event $JSDEV | gawk -v CONFIG=jscmd.cnf -f jscmd.awk &
udev設定(/etc/udev/rules.d/10-joypad.rules)
ACTION=="add",\
        SUBSYSTEMS=="usb", \
        KERNEL=="js*", \
        RUN:="/home/pi/jscmd/jscmd-start.sh", \
        OPTIONS+="last_rule"
  • 設定の反映
$ sudo service udev reload
コマンド設定ファイル(/home/pi/jscmd/jscmd.cnf)
BUTTONS=A,B,X,Y,L,R,select,start,RIGHT,LEFT,DOWN,UP

UP UP DOWN DOWN A B A B = shutdown -h now
LEFT LEFT RIGHT RIGHT A B A B = reboot
  • 最初の BUTTONS はJoypadのボタンの定義。
    • jstest コマンドで確認して「,」区切りでボタンの名前を順番に並べる。
    • 十字キーが Axes: の場合は RIGHT,LEFT,DOWN,UP を最後に追加する。
  • 3行目以降が入力シーケンスとコマンドのペア。「=」区切り。
    • 入力シーケンスはBUTTONSで定義した名前を空白区切りで記述。
    • 同時押しの場合は「A+B」のように書ける。
      • 但し、「B+A」は不可。BUTTONSでの定義順の必要がある。
    • コマンドは1行以内。
jstestの使い方

joypad は製品毎にボタンの配置が異るのでテストツールで確認する。

$ jstest /dev/input/js0
Driver version is 2.1.0.
Joystick (USB,2-axis 8-button gamepad  ) has 2 axes (X, Y)
and 8 buttons (Trigger, ThumbBtn, ThumbBtn2, TopBtn, TopBtn2, PinkieBtn, BaseBtn, BaseBtn2).
Testing ... (interrupt to exit)
Axes:  0:     0  1:     0 Buttons:  0:off  1:off  2:off  3:off  4:off  5:off  6:off  7:off



これで Joypad をUSBに差して ↑↑↓↓ABAB と入力して shutdown すれば成功です。


2013/08/11

Nexus7のカメラでQRコードを読込んでみた

Androidのカメラアプリを作ってみようと思ったのだが Nexus7 にはフロントカメラしか無い。orz
QRコード・リーダーぐらいならなんとかなるかと思い手を付けてみた。

カメラの準備

普通は以下のようにするとカメラが準備できるのだが Nexus7 だとリアカメラを開こうとして失敗する。

Camera camera = Camera.open();

Nexus7 のフロントカメラを開くにはこうする。

int cameraId = 0;
Camera camera = Camera.open(cameraId);

cameraId はハードのカメラの実装状況によりまちまちなので正しくはこんなことする必要が有りそう。

Camera.CameraInfo info = new Camera.CameraInfo();
for (int id=0; id<Camera.getNumberOfCameras(); id++) {
    Camera.getCameraInfo(id, info);
    if (info.??? == ???) { // 使いたいカメラの条件式
        cameraId = id;
    }
}

AnsroidManufest.xmlにカメラのパーミッションも必要。

<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera" android:required="true"/>
<!--
<uses-feature android:name="android.hardware.camera.front" android:required="true"/>
-->

android.hardware.camera.front は無くても良いらしい...

プレビュー

SurfaceView を使う。 ググれば解説多数なので割愛。

QRコードのライブラリ

QRコードのライブラリは ZXing を使う。 google が開発しオープン化されている物。

ここから落せる。

しかし、現行の ZXing-2.2.zip にはソースしか入っていないので以下から2.3-SNAPのjarを落してきた。

  • https://oss.sonatype.org/content/repositories/snapshots/com/google/zxing/

必要なものは core-xxxxx.jar と javase-xxxxxx.jar のみ。
eclipse の Android プロジェクトの libs に入れておけば利用可能。

使い方はサンプル・コードの onPreviewFrame() 内で完結している。

注意するのはフロントカメラは左右反転しているので PlanarYUVLuminanceSource コンストラクタの最後の引数が ture になっている必要がある。

サンプル・コード

このソースだけ。リソースも参照していない。

package org.kotemaru.sample.camera;

import java.io.IOException;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.PlanarYUVLuminanceSource;
import com.google.zxing.Reader;
import com.google.zxing.Result;
import com.google.zxing.common.HybridBinarizer;

import android.app.Activity;
import android.content.res.Configuration;
import android.hardware.Camera;
import android.hardware.Camera.AutoFocusCallback;
import android.os.Bundle;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.ViewGroup;
import android.widget.Toast;

public class MainActivity extends Activity {
    private static final String TAG = "CameraSample";

    private int CAMERA_ID = 0; // for Nexus7

    private Camera camera;
    private SurfaceView surfaceView;
    private CameraListener cameraListener = new CameraListener();

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        surfaceView = new SurfaceView(this);
        setContentView(surfaceView);
    }

    @Override
    protected void onPostCreate(Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);
        SurfaceHolder holder = surfaceView.getHolder();
        holder.addCallback(cameraListener);
    }

    private int getOrientation() {
        return getResources().getConfiguration().orientation;
    }

    private class CameraListener implements
            SurfaceHolder.Callback,
            AutoFocusCallback,
            Camera.PictureCallback,
            Camera.PreviewCallback
    {

        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            camera = Camera.open(CAMERA_ID);
            try {
                camera.setPreviewDisplay(holder);
            } catch (IOException e) {
                Log.e(TAG, e.toString(), e);
            }
        }

        @Override
        public void surfaceChanged(SurfaceHolder holder, int format,
                int width, int height) {

            // カメラのプレビューサイズをViewに設定
            Camera.Parameters parameters = camera.getParameters();
            Camera.Size size = parameters.getSupportedPreviewSizes().get(0); // 0=最大サイズ 
            parameters.setPreviewSize(size.width, size.height);
            camera.setParameters(parameters);

            // 画面回転補正。
            ViewGroup.LayoutParams layoutParams = surfaceView.getLayoutParams();
            if (getOrientation() == Configuration.ORIENTATION_PORTRAIT) {
                camera.setDisplayOrientation(90);
                layoutParams.width = size.height;
                layoutParams.height = size.width;
            } else {
                camera.setDisplayOrientation(0);
                layoutParams.width = size.width;
                layoutParams.height = size.height;
            }
            surfaceView.setLayoutParams(layoutParams);

            // オートフォーカス設定。
            camera.autoFocus(cameraListener);

            camera.startPreview();
        }

        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            camera.autoFocus(null);
            camera.setPreviewCallback(null);
            camera.release();
            camera = null;
        }

        @Override
        public void onAutoFocus(boolean success, Camera camera) {
            if (success) {
                Log.d(TAG, "focus");
                // プレビューのデータ取得。
                camera.setPreviewCallback(cameraListener);
                // フルサイズ画像はTODO
                //camera.takePicture(null,null,cameraListener);
            }
        }

        @Override
        public void onPictureTaken(byte[] data, Camera camera) {
            // フルサイズ画像もやることは同じ。
            onPreviewFrame(data, camera);
        }

        @Override
        public void onPreviewFrame(byte[] data, Camera camera) {
            // 処理は1回なのでコールバック取り消し
            camera.setPreviewCallback(null);

            // 基礎データ取得
            Camera.CameraInfo info = new Camera.CameraInfo();
            Camera.getCameraInfo(CAMERA_ID, info);
            int w = camera.getParameters().getPreviewSize().width;
            int h = camera.getParameters().getPreviewSize().height;
            //int w = camera.getParameters().getPictureSize().width;
            //int h = camera.getParameters().getPictureSize().height;
            boolean isMirror = (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT);

            // プレビュー画像の型変換
            PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource(
                    data, w, h, 0, 0, w, h, isMirror);
            BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));

            // QRコード読み込み。
            Reader reader = new MultiFormatReader();
            try {
                Log.d(TAG, "decode");
                Result result = reader.decode(bitmap);
                String text = result.getText();

                Toast.makeText(MainActivity.this, text, Toast.LENGTH_LONG).show();
                Log.i(TAG, "result:" + text);
                camera.stopPreview();
                camera.autoFocus(null);
            } catch (Exception e) {
                // QRコード認識失敗でも例外発生する。
                Log.d(TAG, "decode-fail:" + e.toString());
                camera.autoFocus(cameraListener);
            }
        }
    };
}

実行結果

さすがにエミュレータでは動かないので実機で確認。

一応、QRコードを認識する事には成功した。 但し、QRコード自体をかなり拡大しないと認識できない。 小さいとカメラを非常に近づける必要があるためピントが合わないのかもしれない。

それ以前にプレビューを見るのに横からのぞき込まないといけないと言う致命的欠陥が...
(最初から分かっていた訳だがw)

技術的に可能だけど使い物にならないと言う予想通りの結論に達したのでこれで終りにしよう。



参考にさせて頂いたサイト:


2013/07/29

AndroidのPush通知(GCM)のサンプルを書いてみた

この記事の内容は古くなっています=>「GCMのサンプル再び」

Android でも Push メッセージが使えると言うので試して見ようと思ったら Google のサンプルが分り辛くて結局自分でサンプルを書いてしまった。

Push通知の仕組み

Androidはサーバ側から端末側へ通信を始める方法は無いので Push 通知できません。
Googleが取った解決方法は端末側からGoogleの専用サーバへの TCP セッションの繋ぎっぱなしです。
Push通知したいアプリは Google のサーバを経由してメッセージを送信します。

大まかな仕組みはこんな感じです。

  • (1) アプリ起動後、GCMサーバから 端末ID を取得します。
  • (2) ユーザID端末ID のペアをアプリサーバに送信してDBに保存します。
  • (3) 目的の ユーザ に向けた メッセージ をアプリサーバに送信します。
  • (4) アプリサーバは ユーザID から 端末ID を引いてGCMサーバにメッセージを転送します。
  • (5) GCMサーバは繋ぎっぱなしのセッションに メッセージ を送信します。

前提条件の注意点。

  • 端末は Google アカウントが登録されている必要があります。
  • ポート 5228、5229、5230 を使用するのでファイヤウォール等で開いている必要が有ります。

公式な文書は以下にあります。

準備

サーバ側

Googleアプリの管理コンソールにログインして 「Google Cloud Messaging for Android」サービスを有効にします。

GCMを利用するに当って必要な情報が幾つかあります。 いずれも Googleアプリの管理コンソール から取得できます。

  • API_KEY : アプリケーション・サーバがGCMサーバに接続する時の認証キーです。
    「APIAccess」の「create new server key」で生成できます。
  • SENDER_ID : 端末アプリがGCMサーバに接続する時のアプリのIDです。
    管理コンソールでは Product Number となっています。

管理コンソールに不慣れな方はこちらのサイトが詳しいです。

クライアント側

eclipse から Android SDK Manager を起動して Extras の 「GoogleCloudMessaging for Android Library」 をインストールします。

${android.sdk}/extras/google/ 配下に必要な jar とデモが入っています。

サンプル

注意: このサンプルは機能の理解を目的としている為、必要最小限の機能に絞ってあります。 Googleの作法から外れている場合も有りますので公式文書も読んで下さい。

サーバ側

サーバに必要なクラスはServlet 1つだけです。

  • 先に取得した API_KEY が必要になります。(ソースを書き換えて下さい。)
  • GCMサーバへの送信は com.google.android.gcm.server.Sender クラスを利用すれば非常に簡単です。
  • 必要な jar ファイルは以下です。
    • ${android.sdk}/extras/google/gcm/gcm-server/dist/gcm-server.jar
    • ${android.sdk}/extras/google/gcm/gcm-server/lib/*.jar
GCMServerSampleServlet.java:
package org.kotemaru.sample.gcm.server;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.google.android.gcm.server.Message;
import com.google.android.gcm.server.Result;
import com.google.android.gcm.server.Sender;


/**
 * GCMのサーバ・サンプル・サーブレット
 * - API
 * -- ?action=register&userId={ユーザID}&regId={端末ID}
 * -- ?action=unregister&userId={ユーザID}
 * -- ?action=send&userId={ユーザID}&mes={送信メッセージ}
 * 
 * 注:いろいろ端折ってます。Googleのサンプルも参照してください。
 * @author @kotemaru.org
 */

public class GCMServerSampleServlet extends HttpServlet {

    /**
     * https://code.google.com/apis/console/ で生成したAPIキー。
     */
    private static final String API_KEY = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
    private static final int RETRY_COUNT = 5;

    /**
     * ユーザIDからRegistrationIdを引くテーブル。
     * -本来はストレージに保存すべき情報。
     * -key=ユーザID: サービスの管理するID。
     * -value=RegistrationId: AndroidがGCMから取得した端末ID。
     */
    static Map<String,String> deviceMap = new HashMap<String,String>();

    public void doGet(HttpServletRequest req, HttpServletResponse res) 
            throws IOException {

        System.out.println("=> "+req.getQueryString());

        String action         = req.getParameter("action");
        String registrationId = req.getParameter("regId");
        String userId         = req.getParameter("userId");
        String msg            = req.getParameter("msg");

        if ("register".equals(action)) {
            // 端末登録、Androidから呼ばれる。
            deviceMap.put(userId, registrationId);

        } else if ("unregister".equals(action)) {
            // 端末登録解除、Androidから呼ばれる。
            deviceMap.remove(userId);

        } else if ("send".equals(action)) {
            // メッセージ送信。任意の送信アプリから呼ばれる。

            registrationId = deviceMap.get(userId);
            Sender sender = new Sender(API_KEY);
            Message message = new Message.Builder().addData("msg", msg).build();
            Result result = sender.send(message, registrationId, RETRY_COUNT);

            res.setContentType("text/plain");
            res.getWriter().println("Result="+result);
        } else if ("sendAll".equals(action)) {
            // TODO: 省略。googleのサンプル参照。
        } else {
            res.setStatus(500);
        }
    }
}


クライアント側

サーバに必要なクラスは MainActivity と GCMBaseIntentServiceの実装クラスの2つです。

  • 先に取得した SENDER_ID が必要になります。(ソースを書き換えて下さい。)
  • アプリサーバへの送信は通常の HttpClient で行います。
    (URLはソースを書き換えて下さい。)
  • 受け取ったPush通知はログとトーストに出力しています。
  • AndroidManifest はサンプルからパッケージ名を変更しただけです。
  • 必要な jar ファイルは以下です。
    • ${android.sdk}/extras/google/gcm/gcm-client/dist/gcm-client.jar
MainActivity.java:
package org.kotemaru.sample.gcm.client;

import android.app.Activity;
import android.os.Bundle;

import com.google.android.gcm.GCMRegistrar;

/**
 * クライアントアプリ本体。
 * @author @kotemaru.org
 */
public class MainActivity extends Activity {
    /**
     * https://code.google.com/apis/console/のProject Number。
     */
    public static final String SENDER_ID = "nnnnnnnnnnn";

    /**
     * アプリサーバーのURL。
     */
    public static final String SERVER_URL = "http://192.168.0.3:8888/gcmserversample";
    /**
     * アプリのユーザID。本来はログイン中のユーザとかになるはず。
     */
    public static final String USER_ID = "TarouYamada";


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final String regId = GCMRegistrar.getRegistrationId(this);
        if (regId.equals("")) {
            // GCMへ端末登録。登録後、GCMIntentService.onRegistered()が呼ばれる。
            GCMRegistrar.register(this, SENDER_ID);
        } else {
            // 登録済みの場合、ここではアプリに登録しなおしているが
            // Googleのサンプルでは unregister して register しなおしている。
            String uri = SERVER_URL+"?action=register"
                    +"&userId="+USER_ID
                    +"&regId="+regId;
            Util.doGetAsync(uri);
        }
    }

    @Override
    protected void onDestroy() {
        GCMRegistrar.onDestroy(this);
        super.onDestroy();
    }

}
GCMIntentService.java:

注意事項:クラス名は「GCMIntentService」に固定です。 クラス名が異なるとレシーバがサービスを起動できなくなりハマります。

package org.kotemaru.sample.gcm.client;

import static org.kotemaru.sample.gcm.client.MainActivity.*;

import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.util.Log;
import android.widget.Toast;

import com.google.android.gcm.GCMBaseIntentService;
import com.google.android.gcm.GCMRegistrar;

/**
 * Push通知受け取りサービス。
 * @author @kotemaru.org
 */
public class GCMIntentService extends GCMBaseIntentService {

    private static final String TAG = "GCMIntentService";

    private Handler toaster;

    public GCMIntentService() {
        super(SENDER_ID);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        toaster = new Handler();
    }

    @Override
    protected void onRegistered(Context context, String registrationId) {
        Log.i(TAG, "onRegistered: regId = " + registrationId);
        // GCMから発行された端末IDをアプリサーバに登録する。
        String uri = SERVER_URL + "?action=register"
                + "&userId=" + USER_ID
                + "&regId=" + registrationId;
        Util.doGet(uri);
    }

    @Override
    protected void onMessage(Context context, Intent intent) {
        // アプリサーバから送信されたPushメッセージの受信。
        // Message.data が Intent.extra になるらしい。
        CharSequence msg = intent.getCharSequenceExtra("msg");
        Log.i(TAG, "onMessage: msg = " + msg);
        toast("Push message: " + msg);
    }



    @Override
    protected void onUnregistered(Context context, String registrationId) {
        Log.i(TAG, "onUnregistered: regId = " + registrationId);
        if (GCMRegistrar.isRegisteredOnServer(context)) {
            String uri = SERVER_URL + "?action=unregister"
                    + "&userId=" + USER_ID;
            Util.doGet(uri);
        } else {
            Log.i(TAG, "onUnregistered: ignore");
        }
    }

    @Override
    protected void onDeletedMessages(Context context, int total) {
        Log.i(TAG, "onDeletedMessages total="+total);
        toast("onDeletedMessages: " + total);
    }

    @Override
    public void onError(Context context, String errorId) {
        Log.i(TAG, "onError: " + errorId);
        toast("onError: " + errorId);
    }

    @Override
    protected boolean onRecoverableError(Context context, String errorId) {
        Log.i(TAG, "onRecoverableError: " + errorId);
        toast("onRecoverableError: " + errorId);
        return super.onRecoverableError(context, errorId);
    }


    private void toast(final String msg) {
        toaster.post(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(GCMIntentService.this, msg, Toast.LENGTH_LONG).show();
            }
        });
    }

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

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

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <permission
        android:name="org.kotemaru.sample.gcm.client.permission.C2D_MESSAGE"
        android:protectionLevel="signature" />
    <uses-permission
        android:name="org.kotemaru.sample.gcm.client.permission.C2D_MESSAGE" />

    <uses-permission
        android:name="com.google.android.c2dm.permission.RECEIVE" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="org.kotemaru.sample.gcm.client.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <receiver
            android:name="com.google.android.gcm.GCMBroadcastReceiver"
            android:permission="com.google.android.c2dm.permission.SEND" >
            <intent-filter>
                <!-- Receives the actual messages. -->
                <action android:name="com.google.android.c2dm.intent.RECEIVE" />
                <!-- Receives the registration id. -->
                <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
                <category android:name="org.kotemaru.sample.gcm.client" />
            </intent-filter>
        </receiver>
        <service android:name=".GCMIntentService" />

    </application>

</manifest>

実行結果

サーバを起動してからクライアントを起動します。

PCから以下の URI をブラウザで開きます。

http://アプリサーバ/gcmserversample?action=send&userId=ユーザID&msg=Hello%20Android!

Androidでトーストが無事表示されました。

  

ダウンロード

このサンプルの eclipse プロジェクトは SVN から落せます。

クライアント:
サーバ:

サーバはGAE用の環境です。

コンパイル前に以下の手順が必要です。

  1. プロジェクトのメニューから「プロパティ」->「Google」->「App Engine」 で App Engine の設定。
  2. lib/*.jar を war/WEB-INF/lib/ にコピー。
  3. プロジェクトのメニューから「実行」->「実行の構成」で GAE の引数 に -a 0.0.0.0 を追加。

所感

何億台あるか分からない Android が同時に接続しても大丈夫なサーバを用意できるのはさすが Google と言った所でしょうか。

最初はデモのソースに余計な物が多くて訳が分からんかったのですが整理したら割とシンプルになりました。

スリープ時の挙動とか電池の消費具合とかまだ色々しらべる必要が有りそうだけど できる事の幅が広がるので何か面白い使い道を考えたい所。

終り。


2013/07/21

RaspberryPiのUSB接続を認識する

RaspberryPi で USB デバイスの抜き差しに応じてアプリの起動/終了をしたかったのでその調査メモ。

Linux では USB 制御は udev と言う仕掛けを使うらしい。

/etc/udev/rules.d/ に目的のデバイスの *.rules ファイルを記述すればOK。
rules ファイルは先頭が数字になっているので多分その順番で処理されるものと思われる。
rules の書き方は man udev でマニュアルが見れるが正直分かり辛い。

とりあえずサンプルをググりながら手持ちのJoyStickを認識する設定を書いてみた。

ルールファイル

/etc/udev/rules/10-joystick-test.rules:

ACTION=="add",\
        SUBSYSTEMS=="usb", \
        ATTRS{idVendor}=="0d9d", \
        ATTRS{idProduct}=="3011", \
        KERNEL=="js*", \
        RUN:="/tmp/test1.sh start", \
        OPTIONS+="last_rule"

ACTION=="remove",\
        SUBSYSTEMS=="usb", \
        ENV{ID_VENDOR_ID}=="0d9d", \
        ENV{ID_MODEL_ID}=="3011", \
        KERNEL=="js*", \
        RUN:="/tmp/test1.sh stop", \
        OPTIONS+="last_rule"
  • 1つの定義は1行で記述する。改行は「\」でエスケープできる。
  • 比較演算子が条件、代入演算子が処理と考えれば良い。
  • ACTION: "add"が接続、"remove"が切断。
  • SUBSYSTEMS: USB の場合は固定。
  • ATTRS: udevadm info コマンドで取れる属性値。
  • ENV: udevadm monitor コマンドで取れる環境変数値。
  • KERNEL: /dev/ 以下の名前と思えば良い。*はワイルドカード。
  • RUN: 実行スクリプト
  • OPTIONS: 多分無くても良い。サンプルに書いてあったのでそのまま。
  • add と remove で ATTRS と ENV の違いが有ることに注意。


udevadm info コマンド

以下のコマンドでデバイスの情報を色々表示してくれる。

$ sudo udevadm info -a -p $(udevadm info -q path -n /dev/input/js0)

但し、2013年6月時点のOSでは表示後ハングアップしてしまう。
Vendor/Productは dmesg でも見れるので無理に使う必要は無い。

udevadm monitor コマンド

起動して置いて USB を抜き差しすると接続ログとデバイス情報が吐き出される。

$ udevadm monitor
monitor will print the received events for:
UDEV - the event which udev sends out after rule processing
KERNEL - the kernel uevent

UDEV  [258008.515110] add      /devices/platform/bcm2708_usb/usb1/1-1/1-1.2/1-1.2.1/1-1.2.1:1.0/input/input18/js0 (input)
ACTION=add
DEVLINKS=/dev/input/by-id/usb-HuiJia_4Axes_16Key_GamePad-joystick /dev/input/by-path/platform-bcm2708_usb-usb-0:1.2.1:1.0-joystick
DEVNAME=/dev/input/js0
DEVPATH=/devices/platform/bcm2708_usb/usb1/1-1/1-1.2/1-1.2.1/1-1.2.1:1.0/input/input18/js0
ID_BUS=usb
ID_INPUT=1
ID_INPUT_JOYSTICK=1
ID_MODEL=4Axes_16Key_GamePad
ID_MODEL_ENC=4Axes\x2016Key\x20GamePad
ID_MODEL_ID=3011
ID_PATH=platform-bcm2708_usb-usb-0:1.2.1:1.0
ID_PATH_TAG=platform-bcm2708_usb-usb-0_1_2_1_1_0
ID_REVISION=4b24
ID_SERIAL=HuiJia_4Axes_16Key_GamePad
ID_TYPE=hid
ID_USB_DRIVER=usbhid
ID_USB_INTERFACES=:030000:
ID_USB_INTERFACE_NUM=00
ID_VENDOR=HuiJia
ID_VENDOR_ENC=HuiJia\x20
ID_VENDOR_ID=0d9d
MAJOR=13
MINOR=0
SEQNUM=1031
SUBSYSTEM=input
TAGS=:udev-acl:
UDEV_LOG=3
USEC_INITIALIZED=258008048772


実行スクリプト

/tmp/test1.sh:

#!/bin/bash
echo `date` $0 $1 $DEVNAME >> /tmp/test.log
  • 環境変数 DEVNAME はデバイス名が入っている。
  • その他に設定される環境変数はスクリプト内で env を実行すれば分かる。
実行結果

/tmp/test.log:

Sun Jul 21 18:46:35 JST 2013 /tmp/test1.sh start /dev/input/js0
Sun Jul 21 18:46:38 JST 2013 /tmp/test1.sh stop /dev/input/js0

ちゃんと呼ばれている。

後は JoyStick の操作を読み取って任意の処理を行うスクリプトを起動させれば良い。

RaspberryPiはshutdownボタンとか無いのが以外に不便なのだがこれで実現できる。 -> RaspberryPiでJoypadからコマンド入力


2013/07/15

RaspberryPiをDNSにする

前から自宅LAN用のDNSが欲しかったんだけど24時間稼働というハードルが以外に高く 実現できないでいた。

RaspberryPiにちょうど良いのでDNSにしてみた。

デフォルトでは入っていないようなのでインストール。

$ sudo apt-get install bind

しかし、この後困った。/etc/named.conf が無いのだ。

色々ググってやっと分かった。Debian系は /etc/bind/ なのね。
しかも named.conf が幾つかに分割されていて独自のお作法が有る様子...

良く分かんないけど適当にやったら動いたからメモっとく。

/etc/bind/named.conf.options:

acl localnet {
        192.168.0.0/24;
        127.0.0.1;
};

options {
        directory "/var/cache/bind";
        allow-query { localnet; };
        allow-transfer { none; };
        forwarders { 192.168.0.1; };      # 家のルータはNS持ってるので
        forward only;

        dnssec-validation auto;
        auth-nxdomain no;    # conform to RFC1035
        listen-on-v6 { any; };         
};

PS. forwardersが効いていなかったので修正

        allow-transfer { localnet; };
        dnssec-validation no;

/etc/bind/named.conf.local:

zone "myhome.jp" {
        type master;
        file "/etc/bind/myhome.jp.zone";
};

zone "0.168.192.in-addr.arpa" {
        type master;
        file "/etc/bind/myhome.jp.rev";
};

/etc/bind/myhome.jp.zone:

$TTL    86400
@       IN      SOA     ns.myhome.jp. root.myhome.jp. (
                        2013063001      ; Serial
                        3600            ; Refresh
                        900             ; Retry
                        604800          ; Expire
                        86400 ) ; Negative Cache TTL
;
@       IN      NS      ns.myhome.jp.   ; this host
@       IN      A       127.0.0.1
@       IN      AAAA    ::1
ns      IN      A       192.168.0.7     ; this host
rpi     IN      A       192.168.0.7     ; this host
router  IN      A       192.168.0.1     ;

/etc/bind/myhome.jp.rev:

$TTL    86400
@       IN      SOA     ns.myhome.jp. root.myhome.jp. (
                        2013063001      ; Serial
                        3600            ; Refresh
                        900             ; Retry
                        604800          ; Expire
                        3600 )  ; Negative Cache TTL
;
@       IN      NS      ns.myhome.jp.
7       IN      PTR     rpi.myhome.jp.
1       IN      PTR     router.myhome.jp.

で bind 再起動。

$ sudo service bind9 restart

参照側PCでDNSを設定して確認する。

$ sudo vi /etc/resolve.conf
domain myhome.jp
nameserver      192.168.0.7
$ nslookup rpi
Server:         192.168.0.7
Address:        192.168.0.7#53

Name:   rpi.myhome.jp
Address: 192.168.0.7

$ nslookup 192.168.0.7
Server:         192.168.0.7
Address:        192.168.0.7#53

7.0.168.192.in-addr.arpa        name = rpi.myhome.jp.

問題無さげ。地味に嬉しい。


2013/07/11

RaspberryPiのパーティション

RaspberryPiを購入してみたの続き。

とりあずOSを入れただけなのでその他の設定したメモです。

固定IPの設定

DHCPは不便なので固定IPを振ります。

$ sudo vi /etc/network/interfaces   #以下の修正を入れる
#iface eth0 inet dhcp
iface eth0 inet static
address 192.168.0.xxx
netmask 255.255.255.0
gateway 192.168.0.1

パーティションの切り直し

img ファイルは2Gの状態なのでSDカードサイズの16Gまで使えるようにします。
単純に最大まで使うだけならば raspi-config コマンドの expand_rootfs から行えます。

しかし、書込回数に上限のあるSDカードで /var がルートパーティションに有るのは嫌なので手動で /var 専用のパーティションを切る事にしました。

-> と思ったのですが今時のフラッシュメモリはウェアレベリング機能が入っていて同じセルに書き込まないらしいです。 なので意味無いのですがせっかくなのでパーティション分け方法のメモとして残します。

  • ルートパーティションを10Gに拡張
$ sudo fdisk /dev/mmcblk0

Command (m for help): p
        Device Boot      Start         End      Blocks   Id  System
/dev/mmcblk0p1            8192      122879       57344    c  W95 FAT32 (LBA)
/dev/mmcblk0p2          122880     XXXXXXX     XXXXXXX   83  Linux

Command (m for help): d
Partition number (1-4): 2
Partition 2 is deleted

Command (m for help): n
Partition type:
   p   primary (1 primary, 0 extended, 3 free)
   e   extended
Select (default p): p
Partition number (1-4, default 2): 2
First sector (2048-xxxxxxx, default 2048): 122880   ←元と同じStart
Last sector, +sectors or +size{K,M,G} (8192-xxxxxxx, default xxxxxxx): +10G
Using default value 10485760

Command (m for help): p
        Device Boot      Start         End      Blocks   Id  System
/dev/mmcblk0p1            8192      122879       57344    c  W95 FAT32 (LBA)
/dev/mmcblk0p2          122880    21094399    10485760   83  Linux

Command (m for help): w

$ sudo reboot
  • ルートパーテションのファイルシステムを再構築します。
$ sudo resize2fs /dev/mmcblk0p2
$ df
Filesystem     1K-blocks    Used Available Use% Mounted on
rootfs          10321208 1462516   8421024  15% /
/dev/root       10321208 1462516   8421024  15% /
devtmpfs          216132       0    216132   0% /dev
tmpfs              44880     232     44648   1% /run
tmpfs               5120       0      5120   0% /run/lock
tmpfs              89740       0     89740   0% /run/shm
/dev/mmcblk0p1     57288   18960     38328  34% /boot


  • 拡張領域に/var用の1Gのパーティションを切ります。
$ sudo fdisk /dev/mmcblk0
Command (m for help): p
        Device Boot      Start         End      Blocks   Id  System
/dev/mmcblk0p1            8192      122879       57344    c  W95 FAT32 (LBA)
/dev/mmcblk0p2          122880    21094399    10485760   83  Linux

Command (m for help): n
Partition type:
   p   primary (2 primary, 0 extended, 2 free)
   e   extended
Select (default p): e
Partition number (1-4, default 3): 
Using default value 3
First sector (2048-30881791, default 2048): 21094400    ←End+1
Last sector, +sectors or +size{K,M,G} (21094400-30881791, default 30881791):
Using default value 30881791

Command (m for help): p
        Device Boot      Start         End      Blocks   Id  System
/dev/mmcblk0p1            8192      122879       57344    c  W95 FAT32 (LBA)
/dev/mmcblk0p2          122880    21094399    10485760   83  Linux
/dev/mmcblk0p3        21094400    30881791     4893696    5  Extended

Command (m for help): n
Partition type:
   p   primary (2 primary, 1 extended, 1 free)
   l   logical (numbered from 5)
Select (default p): l
Adding logical partition 5
First sector (21096448-30881791, default 21096448): 
Using default value 21096448
Last sector, +sectors or +size{K,M,G} (21096448-30881791, default 30881791): +1G

Command (m for help): p
        Device Boot      Start         End      Blocks   Id  System
/dev/mmcblk0p1            8192      122879       57344    c  W95 FAT32 (LBA)
/dev/mmcblk0p2          122880    21094399    10485760   83  Linux
/dev/mmcblk0p3        21094400    30881791     4893696    5  Extended
/dev/mmcblk0p5        21096448    23193599     1048576   83  Linux

Command (m for help): w

$ sudo reboot
  • 作成したパーティションに現在の /var の内容をコピーして次回からマウントされるようにします。
$ sudo mkfs -t ext4 /dev/mmcblk0p5
$ sudo mount /dev/mmcblk0p5 /mnt
$ sudo apt-get install rsync    #デフォルトでは入ってませんでした
$ sudo rsync -av /var/ /mnt
$ sudo vi /etc/fstab  #以下の一行追加
/dev/mmcblk0p5  /var            ext4    defaults,noatime  0       1
$ sudo reboot
$ df
Filesystem     1K-blocks    Used Available Use% Mounted on
   :
/dev/mmcblk0p5   1032088  675428    304232  69% /var


拡張領域に余裕を残してあるのはこの /var に書き込め無くなったら 別のパーティションを切って引っ越す作戦です。

RAMディスクを使う作戦も有るのですが /var 全部はカバーできないしメモリが減るのでこの方法でしばらく試して見ようと思います。



これでやっと落ちついて作業ができます。


2013/07/04

RaspberryPiを購入してみた

気になっていたRaspberry-Piを買ってみた。

購入先は正規代理店の RSコンポーネンツ株式会社 から。

ここはアマゾンより安いのでお勧めだが個人の場合、幾つか注意事項がある。

  • 個人でも「通常のRSとのお取り引き」を選んでユーザ登録を行う。
  • 土日祝日の配送は行わないので配送先に平日受け取れる場所を設定する。
  • 支払いは「クレジット」か「代引」とする。

基本的に法人相手の取り引きを行う会社だが例外的に個人でも受け付けていると形。

で発注すると翌日には届いちゃう。

専用ケースも購入したが保管用のケースが付いてくるのでこれに穴空けしても充分な感じ。

その他に買った物。

SDカードは新調したほうが良いと思うがその他は手持ちがあれば不要。
セルフ電源付きのUSBハブはUSB-HDDとか繋げる時には必要になるので無ければ買って置いた方が良いと思います。

インストール

ググれば沢山出て来ますが一応メモります。

以下のサイトからOS(201X-xx-xx-wheezy-raspbian.zip)をDLします。

展開して中の img ファイルをSDカードに書き込みます。

$ sudo dd if=201X-xx-xx-wheezy-raspbian.img of=/dev/XXX bs=10240000

書き込み先のデバイス名はOS依存です。FreeBSDでUSB接続の場合は /dev/da0 とかです。 Windowsの場合は専用ツールが必要だったりします。適当にググってください。

OSを書き込んだSDカードを挿入し電源を繋げれば起動します。
LANケーブルを差して置けば DHCP で sshd が起動するので ssh で接続します。

$ ssh pi@192.168.0.x
Password: raspberry

ログインできればセットアップ終了です。

動画再生

動画プレーヤーが最初からOSに入っているのでYoutubeから落してきたmp4を再生してみます。

$ omxplayer PentaAsibuto.mp4

Full-HD(1920x1080p/30fps) の動画ファイルをモニタの都合で 1280x720p 再生しています。
この時のCPU使用率は15%程度で、コマ落ち等全くありませんでした。

とりあえず準備が整いました。
大変なのはここからですが...


2013/06/29

Nexus7を外部モニタ化 (2)

Nexus7を外部モニタ化した時に邪魔だったVNCクライアントのナビゲーションバー消せないか調べてみた。

結論から言うとAndroid/4.1で追加されたAPIを使えばアプリ単位で消せる。
OS全体で消そうとするとroot権が必要になる。

前回試したアプリの中で androidVNC がソース公開されていたのでこれに 自前のパッチを当てて試したところ 800x1280 の完全な全画面モードでVNCを使う事ができた。

終了。

じゃあ、野良ビルドでも公開しようかなと思ったらここで問題が。
apkを作ってアプリを実機にインストールしようても 「×アプリはインストールされません。」のメッセージが出る。

eclipseからデバッグモードでのインストールは問題無い。

うーん、一度同じアプリをGooglePlayからインストールしているから 正規の証明書が残ってるって事だろうか?
これ以上の情報が無く対処しようがないので野良ビルドの公開は諦めとします。

=> 出来ました Nexus7を外部モニタ化 (3)

...

せっかくなのでビルド手順はメモして置きます。

androidVNCのSVNからプロジェクトを2つチェックアウトします。

  • http://android-vnc-viewer.googlecode.com/svn/trunk/eclipse_projects/
    • androidVNC
    • ZoomerWithKeys

trunkの日付が 2011年になっているのでこのプロジェクトは活動停止中っぽいです。

ビルドには sqlitegen というプラグインが必要です。 (専用プラグインのような気がします。)
以下のページの案内にしたがってインストールしてください。

以下のパッチを当てます。

Index: AndroidManifest.xml
===================================================================
--- AndroidManifest.xml (revision 204)
+++ AndroidManifest.xml (working copy)
@@ -1,14 +1,14 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.androidVNC" android:installLocation="auto" android:versionCode="13" android:versionName="0.5.0">
+    package="android.androidVNC" android:installLocation="auto" android:versionCode="14" android:versionName="0.5.0">
     <application android:icon="@drawable/icon" android:label="androidVNC" android:debuggable="false">
-       <activity android:label="@string/app_name" android:name="androidVNC" android:screenOrientation="landscape">
+       <activity android:label="@string/app_name" android:name="androidVNC" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN"></action>
                <category android:name="android.intent.category.LAUNCHER"></category>
            </intent-filter>
        </activity>
-       <activity android:screenOrientation="landscape" android:configChanges="orientation|keyboardHidden" android:name="VncCanvasActivity">
+       <activity  android:configChanges="orientation|keyboardHidden" android:name="VncCanvasActivity">
            <intent-filter>
                <action android:name="android.intent.action.VIEW"></action>
            </intent-filter>
Index: src/android/androidVNC/VncCanvasActivity.java
===================================================================
--- src/android/androidVNC/VncCanvasActivity.java   (revision 204)
+++ src/android/androidVNC/VncCanvasActivity.java   (working copy)
@@ -39,6 +39,7 @@
 import android.graphics.PointF;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.Handler;
 import android.os.SystemClock;
 import android.util.Log;
 import android.view.KeyEvent;
@@ -607,6 +608,23 @@

        vncCanvas = (VncCanvas) findViewById(R.id.vnc_canvas);
        zoomer = (ZoomControls) findViewById(R.id.zoomer);
+       
+       vncCanvas.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener(){
+           @Override
+           public void onSystemUiVisibilityChange(int visibility) {
+               if (visibility == View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) return;
+               
+               Handler handler = new Handler();
+               handler.postDelayed(new Runnable() {
+                  @Override
+                  public void run() {
+                       vncCanvas.setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
+                   }
+               }, 3000);
+           }
+       });
+       vncCanvas.setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
+

        vncCanvas.initializeVncCanvas(connection, new Runnable() {
            public void run() {

プロジェクトの「プロパティ」から「Android」を選んでターゲットを 4.2.2 にします。

これでビルドできるはずです。
尚、「自動でビルド」のチェックは何故か必須です。


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

サイト内検索

登録
RSS/2.0

カテゴリ

最近の投稿

リンク

アーカイブ