Android端末に外部センサーをつけてみた

※この記事は2019年１2月に公開した記事になります。

[ひとり開発 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/solo-developer)、24日目の記事です。

少し前にAndroidに外部センサーをどうにかしてつけられないか？と思い、色々試行錯誤したらできちゃいました。
せっかくQiitaのアカウントもあるので今回はそれについて書きます。

#ざっくりと作ったものを紹介
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">中華Androidカーナビにセンサーが無かったので取り付けた <a href="https://t.co/Ja0VMIAEK7">pic.twitter.com/Ja0VMIAEK7</a></p>&mdash; 辰零 (@Ryo_TATSUREI) <a href="https://twitter.com/Ryo_TATSUREI/status/1176503212368486400?ref_src=twsrc%5Etfw">September 24, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

動画のようにセンサーを接続したArduinoからAndroidへUSBシリアルでデータを渡し、XposedモジュールでSensorManagerに割り込ませる処理を行っています。
センサーはBNO055を使用。

動画ではmicro:bitを使っていますが後でadafruit Trinket M0に変えてます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258005/9c8a20cf-2d5d-ba13-872f-36e9be1b3937.jpeg" width="500">

#作り方
大まかな作り方は

1.センサーとArduinoを繋ぐ！
2.Arduinoにプログラムを流し込む！（キャリブレーション値も取る！）
3.XposedモジュールをAndroidにインストール！
4.ArduinoとAndroidを繋ぐ！
5.動く！！！

以下詳細です。

#必要なもの
・adafruit Trinket M0
・BNO055(9軸センサー）
・Xposed導入済みのAndroid
・その他ハンダゴテとかそういう道具

#接続
![Untitled Sketch 2_ブレッドボード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258005/ff709ecf-6070-5272-6392-a88380e68b7e.png)

上の画像のようにちゃちゃっと配線！
プルアップ抵抗は必要に応じて付けてください。

#コード(Arduino)
まずはキャリブレーション値を取らないといけないので下記のコードを書き込んでください。
※BNO055のライブラリが必要です。先に入れてください。[リンク](https://github.com/arduino-libraries/BNO055)

<details><summary>キャリブレーション用コード</summary><div>

~~~C

#include <BNO055_support.h>
#include <Wire.h>

struct bno055_t myBNO;
unsigned char accelCalibStatus = 0;
unsigned char magCalibStatus = 0;
unsigned char gyroCalibStatus = 0;
unsigned char sysCalibStatus = 0; 

unsigned long lastTime = 0;

BNO055_S16 accOffsetX = 0;
BNO055_S16 accOffsetY = 0;
BNO055_S16 accOffsetZ = 0;
BNO055_S16 gyroOffsetX = 0;
BNO055_S16 gyroOffsetY = 0;
BNO055_S16 gyroOffsetZ = 0;
BNO055_S16 magOffsetX = 0;
BNO055_S16 magOffsetY = 0;
BNO055_S16 magOffsetZ = 0;

boolean flgCalib = false;
int calCount = 0;

void setup()
{
  lastTime = millis() + 1000;
  //Initialize I2C communication
  Wire.begin();

  delay(500);

  BNO_Init(&myBNO);

  bno055_set_operation_mode(OPERATION_MODE_NDOF);

  delay(1);

  Serial.begin(115200);

  Serial.println();
  Serial.println();
  Serial.println();
  Serial.println();
}

void loop() //This code is looped forever
{
  if ((millis() - lastTime) >= 500)
  {
    lastTime = millis();
    if (!flgCalib)
    {
      Serial.print("Time Stamp: ");
      Serial.println(lastTime);

      bno055_get_accelcalib_status(&accelCalibStatus);
      Serial.print("Accelerometer Calibration Status: ");
      Serial.println(accelCalibStatus);

      bno055_get_magcalib_status(&magCalibStatus);
      Serial.print("Magnetometer Calibration Status: ");
      Serial.println(magCalibStatus);

      bno055_get_gyrocalib_status(&gyroCalibStatus);
      Serial.print("Gyroscope Calibration Status: ");
      Serial.println(gyroCalibStatus);

      bno055_get_syscalib_status(&sysCalibStatus);
      Serial.print("System Calibration Status: ");
      Serial.println(sysCalibStatus);

      Serial.println(); //To separate between packets

      calCount++;

      if (accelCalibStatus == 3 && magCalibStatus == 3 && gyroCalibStatus == 3)
      {
        flgCalib = true;
        //delay(1000);

        if (calCount > 10)
        {
          software_reset();
        }
      }
    }else{
      showCalibData();
    }
  }

}

void showCalibData()
{

  bno055_read_accel_offset_x_axis(&accOffsetX);
  bno055_read_accel_offset_y_axis(&accOffsetY);
  bno055_read_accel_offset_z_axis(&accOffsetZ);

  bno055_read_gyro_offset_x_axis(&gyroOffsetX);
  bno055_read_gyro_offset_y_axis(&gyroOffsetY);
  bno055_read_gyro_offset_z_axis(&gyroOffsetZ);

  bno055_read_mag_offset_x_axis(&magOffsetX);
  bno055_read_mag_offset_y_axis(&magOffsetY);
  bno055_read_mag_offset_z_axis(&magOffsetZ);

  Serial.print("AccCalibration");
  Serial.println();
  Serial.print("X:");
  Serial.println(accOffsetX);
  Serial.print("Y:");
  Serial.println(accOffsetY);
  Serial.print("Z:");
  Serial.println(accOffsetZ);

  Serial.print("GyroCalibration");
  Serial.println();
  Serial.print("X:");
  Serial.println(gyroOffsetX);
  Serial.print("Y:");
  Serial.println(gyroOffsetY);
  Serial.print("Z:");
  Serial.println(gyroOffsetZ);

  Serial.print("MagCalibration");
  Serial.println();
  Serial.print("X:");
  Serial.println(magOffsetX);
  Serial.print("Y:");
  Serial.println(magOffsetY);
  Serial.print("Z:");
  Serial.println(magOffsetZ);
}

void software_reset()
{
  SCB->AIRCR = ((0x5FA << SCB_AIRCR_VECTKEY_Pos) | SCB_AIRCR_SYSRESETREQ_Msk);
}
~~~
</div></details>

Tera Termなんかで接続するとキャリブレーション状況が出力されます。

キャリブレーションのやり方は公式が動画を出してるので<a href="https://www.youtube.com/watch?v=Bw0WuAyGsnY">こちら</a>を参考にしてください。
<iframe width="560" height="315" src="https://www.youtube.com/embed/Bw0WuAyGsnY" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

キャリブレーション後にArduinoを再起動している理由は、
私が購入したgy-bno055だとキャリブレーション値がうまく読み込めないためです。
原因不明ですが再起動させるとうまくいくのでしています。

次にXposedモジュールと通信する用のコードを書き込みます。

※76～87行でオフセット値をセットしてます。
　ここの部分はさっき取得したキャリブレーション値に書き換えてください。

<details><summary>Xposedモジュールと通信する用のコード</summary><div>

~~~C
#include "BNO055_support.h"
#include <Wire.h>

struct bno055_t myBNO;
struct bno055_accel accData;
struct bno055_gyro gyroData;
struct bno055_mag magData;
struct bno055_euler eulerData;
struct bno055_quaternion quatData;
struct bno055_linear_accel linearAccData;
struct bno055_gravity gravityData;

BNO055_S16 magOffsetX = 0;
BNO055_S16 magOffsetY = 0;
BNO055_S16 magOffsetZ = 0;

unsigned long lastTime = 0;

int ledStatus = 0;
int ledCount = 0;

void setup()
{
  Serial.begin(115200);
  lastTime = millis() + 1000;
  pinMode(PIN_LED, OUTPUT);

  Wire.begin();

  delay(500);

  BNO_Init(&myBNO); 

  bno055_write_accel_offset_x_axis(10);
  bno055_write_accel_offset_y_axis(-51);
  bno055_write_accel_offset_z_axis(8);

  bno055_write_gyro_offset_x_axis(-1);
  bno055_write_gyro_offset_y_axis(-3);
  bno055_write_gyro_offset_z_axis(1);

  bno055_write_mag_offset_x_axis(114);
  bno055_write_mag_offset_y_axis(598);
  bno055_write_mag_offset_z_axis(57);

  bno055_set_operation_mode(OPERATION_MODE_NDOF);

  bno055_set_accel_range(ACCEL_RANGE_8G);
  bno055_set_gyro_range(GYRO_RANGE_500rps);

  delay(1);

}

void loop()
{
  if ((millis() - lastTime) >= 10)
  {
    lastTime = millis();
    byte sendData[39];

    bno055_read_accel_xyz(&accData);
    bno055_read_gyro_xyz(&gyroData);
    bno055_read_mag_xyz(&magData);
    bno055_read_linear_accel_xyz(&linearAccData);
    bno055_read_gravity_xyz(&gravityData);
    bno055_read_quaternion_wxyz(&quatData);

    byte d[2];
    convertBytes(accData.x, d);
    sendData[0] = d[0];
    sendData[1] = d[1];
    convertBytes(accData.y, d);
    sendData[2] = d[0];
    sendData[3] = d[1];
    convertBytes(accData.z, d);
    sendData[4] = d[0];
    sendData[5] = d[1];
    convertBytes(gyroData.x, d);
    sendData[6] = d[0];
    sendData[7] = d[1];
    convertBytes(gyroData.y, d);
    sendData[8] = d[0];
    sendData[9] = d[1];
    convertBytes(gyroData.z, d);
    sendData[10] = d[0];
    sendData[11] = d[1];
    convertBytes(magData.x, d);
    sendData[12] = d[0];
    sendData[13] = d[1];
    convertBytes(magData.y, d);
    sendData[14] = d[0];
    sendData[15] = d[1];
    convertBytes(magData.z, d);
    sendData[16] = d[0];
    sendData[17] = d[1];
    convertBytes(linearAccData.x, d);
    sendData[18] = d[0];
    sendData[19] = d[1];
    convertBytes(linearAccData.y, d);
    sendData[20] = d[0];
    sendData[21] = d[1];
    convertBytes(linearAccData.z, d);
    sendData[22] = d[0];
    sendData[23] = d[1];
    convertBytes(gravityData.x, d);
    sendData[24] = d[0];
    sendData[25] = d[1];
    convertBytes(gravityData.y, d);
    sendData[26] = d[0];
    sendData[27] = d[1];
    convertBytes(gravityData.z, d);
    sendData[28] = d[0];
    sendData[29] = d[1];
    convertBytes(quatData.x, d);
    sendData[30] = d[0];
    sendData[31] = d[1];
    convertBytes(quatData.y, d);
    sendData[32] = d[0];
    sendData[33] = d[1];
    convertBytes(quatData.z, d);
    sendData[34] = d[0];
    sendData[35] = d[1];
    convertBytes(quatData.w, d);
    sendData[36] = d[0];
    sendData[37] = d[1];

    uint32_t chksum = 0;
    for (int i = 0; i < 38; i++)
    {
      chksum += sendData[i];
    }
    sendData[38] = chksum & 0xFF;

    Serial.write(sendData, 39);

    ledCount++;
    if (ledCount > 2)
    {
      ledStatus = !ledStatus;
      digitalWrite(PIN_LED, ledStatus);
      ledCount = 0;
    }
  }
}

void convertBytes(int num, byte data[2])
{
  data[0] = (num & 0xFF00) >> 8;
  data[1] = num & 0xFF;
}

void software_reset()
{
  SCB->AIRCR = ((0x5FA << SCB_AIRCR_VECTKEY_Pos) | SCB_AIRCR_SYSRESETREQ_Msk);
}
~~~
</div></details>

Arduino側はこれでOK

次はAndroid側になります。

#コード(Android)
githubにプロジェクトごと上げたのでこれをビルドしてインストールすればOK（なはず）
[Xposedモジュール](https://github.com/ryotn/SensorInjectionXposedModule)

あとはXposedでモジュールを有効化し、ArduinoとAndroidをUSBで接続してください。

<2020/10/03 追記>
※Androidの仕様でアプリがUSBデバイスへアクセスする際にユーザからの許可が必要になります。
SystemUI.apkを改造することによって回避することが可能です。
（常用する場合はやったほうがいい）
https://stackoverflow.com/questions/13726583/bypass-android-usb-host-permission-confirmation-dialog/30563253#30563253

#作ったきっかけ
使っているAndroidナビにセンサー類が一切入っておらず、Y!カーナビで「運転力診断」やトンネル内の自車位置特定ができませんでした。
Xposedならどうにかして外部センサーの情報をアプリに渡せないか？と思い検索したところGyroEmuなるものを発見。
このモジュールはジャイロセンサーは無いけど加速度センサー、地磁気センサーはある端末でジャイロをソフトウェア的に再現し、SensorManagerに割り込ませるものです。
この仕組みを上手く使えば作れそう・・・と思い今回作りました。
（なので今回作ったXposedモジュールはGyroEmuのフォークとして上げてます）

#それでちゃんと動くの・・・？
Y!カーナビで試した感じではそれなりに動いているっぽいです（？）
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Yカーナビで何度か運転力診断試したけどそれっぽい評価が出てるから多分大丈夫 <a href="https://t.co/MLhjhPxq2g">pic.twitter.com/MLhjhPxq2g</a></p>&mdash; 辰零 (@Ryo_TATSUREI) <a href="https://twitter.com/Ryo_TATSUREI/status/1176503651356962816?ref_src=twsrc%5Etfw">September 24, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

トンネル内でも自車位置特定がそれなりにできてるっぽいので多分大丈夫

他のアプリではわかりません（笑

<2020/10/03 追記>
このあと数ヶ月ほど使っていましたがどうやらちゃんと動いているか怪しい感じでした。
Y!カーナビの運転力診断の得点も変に高い値が出ますし、トンネル内での自車位置特定もNEO-M8Uによるものかもしれません・・・
それとアプリの作りがいまいちなためか、ちょくちょく落ちるので今は使っていません。
（Y!カーナビの運転力診断もなくなっちゃったので・・・）

#おわりに
Qiitaアカウントを取ってからいつか自分も記事を書こうと思ってかなり月日が立ってしまいました・・・。
せっかくだしAdvent Calendarに登録しよう！と思って枠を取ったものの、全然書いておらず二日前に急いで書いた次第です。
内容も結構ニッチな感じではありますが普段からこういった自分がほしいアプリなどちまちま作っています。
またそのうち新しいアプリを作ったら記事を書きたいと思います。