バッファになんでも無制限に溜めるのですぐ未定義動作するぼくです。

弊学科には学科特製の移動ロボットを用いた課題解決型実験があるのですが、そのメモ。
手持ちに機体そのものの画像がないのでそれは後々。

注記

ここで出て来るらrpi、ずにゃん、らずぴっぴ、又はらずぱいはRaspberry Pi 1 Model Bのことをさす。

なにつくっとんねん

弊学科の移動ロボット、一応ジャイロセンサが付いているらしいがうまく使えない(らしい)。
そこで、Android端末の傾き検出機能を用いて、機体の回転角を出してやろうという意図で下記のようなシステムを作成した。

-------------------------------------          -------------------------------     ---------------------------------
|Android                         |          |rpi : ArchLinux ARM        |     |i2c slave : hardware         |
|dynamic ip(wlan0)                |          |static ip 192.168.81.0(wlan0)|     |in: button,ultrasonic sensor   |
|magnetic field,accelerometer sensor| wireless |dynamic ip (eth0)            | i2c |out: DC motor                |
|REST client                     |    <->   |REST server,i2c master      | <-> |i2c slave address : ref fig.pi |
-------------------------------------          -------------------------------     ---------------------------------
図pi i2cdetectの結果
図pi i2cdetectの結果

Android側構成

Android側はSensorsで傾き検出するコードとRetrofit+RxAndroidなRESTクライアントを書いておしまい。
後々githubリポジトリは公開しようかなと思ってるけど、とりあえず現時点のコードを一部抜粋。

public class MainActivity extends Activity {
    private SensorManager mSensorManager;
    private Sensor mAccelerometer, mMagneticField;
    private SensorEventListener mSensorListener;

    private ApiService service = ApiUtils.build().create(ApiService.class);

    private RotationData rotationData = new RotationData();

    @Override
    protected void onCreate(Bundle savedInstanceState) {

    /**略 **/

    mSensorListener = new SensorEventListener() {
            @Override
            public void onSensorChanged(SensorEvent event) {
                //精度が悪いときは捨てる
                if (event.accuracy <= SensorManager.SENSOR_STATUS_UNRELIABLE) return;
                //取得
                switch (event.sensor.getType()) {
                    case Sensor.TYPE_MAGNETIC_FIELD:
                        rotationData.setGeomagnetic(event.values.clone());
                        break;
                    case Sensor.TYPE_ACCELEROMETER:
                        rotationData.setAcceleration(event.values.clone());
                        break;
                }
                if (rotationData.getGeomagnetic() != null && rotationData.getAcceleration() != null) {
                    float[] rotationMatrix = new float[9],
                            remappedRotationMatrix = new float[9];
                    SensorManager.getRotationMatrix(rotationMatrix, null, rotationData.getAcceleration(), rotationData.getGeomagnetic());
                    SensorManager.remapCoordinateSystem(rotationMatrix,
                            SensorManager.AXIS_X,
                            SensorManager.AXIS_Y,
                            remappedRotationMatrix);
                    SensorManager.getOrientation(remappedRotationMatrix, rotationData.orientations);

                    /** UIに値をセットするコード (略)**/

                    //データ送信
                    callSendDataApi(rotationData);
                }
            }

            @Override
            public void onAccuracyChanged(Sensor sensor, int accuracy) {
            }
        };
    }

    private void callSendDataApi(RotationData rData) {
        RequestRotationData requestRotationData = new RequestRotationData();
        requestRotationData.setRotationData(rData);

        service.postRotationData(requestRotationData)
                .subscribeOn(Schedulers.io())
                .observeOn(Schedulers.trampoline())
                .subscribe(new Observer<Void>() {
                               @Override
                               public void onSubscribe(@NonNull Disposable disposable) {
                               }

                               @Override
                               public void onNext(@NonNull Void aVoid) {
                                   Log.i(MainActivity.class.getName(), "SUCCESS");
                               }

                               @Override
                               public void onError(@NonNull Throwable throwable) {
                                   Log.i(MainActivity.class.getName(), "FAILED: " + Arrays.toString(throwable.getStackTrace()));
                               }

                               @Override
                               public void onComplete() {
                               }
                           }
                );
    }

    /** 略 **/
}

rotationMatrix

加速度センサより得られる重力加速度ベクトルと地磁気センサから得られる地磁気ベクトルを用いて回転行列 rotationMatrix が作れるそうですが、この様子をうまく座標平面に出した図が欲しい….線形代数センスのNASA
この辺が詳しそうです。

次の行でこれを remapCoordinateSystem を用いてWorld座標系の回転行列を取得します。これを利用して、World座標系における絶対座標的な 方位角 を取得します( getOrientation )。
方位角に関してはここが図付きでわかりやすいかもです。

最後に得られる orientation は、

  • orientation[0] 方位(Z軸周り) 北向きが0
  • orientation[1] 仰角(X軸周り) 端末長辺の水平方向が0
  • orientation[2] ロール角(Y軸周り) 端末短辺の水平方向が0

となっています。
今回は端末をロボットの床面に垂直に立てて使用することを想定しているので、Z軸方向の方位角を出力の回転角としてセットしています。

callSendDataApi

RxAndroidとRetrofit2を用いたREST ClientのAPI呼び出し部分です。

まずはModelとRequest(?この辺まだよくわかってない)を用意します。Kotlinで書いていれば data class 化して行数減らせそうな要素ですね。

class RotationData {

    @Expose
    public float[] orientations;
    @Expose(serialize = false, deserialize = false)
    private float[] acceleration;
    @Expose(serialize = false, deserialize = false)
    private float[] geomagnetic;
    @Expose
    private float rotation;
    
    /** setter ,getter ,constructor (省略) **/
}
public class RequestRotationData {

    @Expose
    RotationData rotationData;

    public void setRotationData(RotationData rotationData) {
        this.rotationData = rotationData;
    }

}

@Expose はgson変換するときに使う(らしい)です。

次はAPIを宣言してあげます。

public interface ApiService {

    @POST("rotation")
    io.reactivex.Observable<Void> postRotationData(@Body RequestRotationData rotationData);

}
public class ApiUtils {

    public static Retrofit build(){
        return new Retrofit.Builder()
                .baseUrl("http://192.168.81.1:4545/")
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .build();
    }

}

Util classをつくってやることで使い回しやすくなります。

MainActivity側で postRotationData を呼ぶとObservableを返すので、そこからsubscribeする。
以下のメソッドは実行スレッドを引数で指定する。

  • subscribeOn :処理を実行する側
  • observeOn :処理を受け取る側

実行スレッドに関してはここを参考にした。

コールバックは次のとおり。

  • onSubscribe :元のObservableに加工できる。 準備に使えそう。
  • onNext :処理の成功時に呼ばれる。結果が引数にくる。
  • onError :処理の失敗時に呼ばれる。Exceptionが引数にくる。
  • onComplete :処理の成否に関わらず最後に呼ばれる。

unSubscribeをどこかで呼ぶべきなので、その辺は後の実装で追加予定。

Rpi側構成

学科で使われるらずぴっぴのOSがDebian7.0 wheezy(glibcが2.13ぐらいのやつ)とらずぴっぴ本体と相まって非常に古いので、自前のSDカードでArch Linuxを用いた環境を構築した。

長くなりそうなので続きは別記事で。

てきとうにかいてたら常体と敬体がまざった….