SORACOMで作るなんちゃってコネクテッドカー！

# はじめに

コネクテッドカーってあるじゃないですか。
IoTの代表例としてよく挙げられて、最近はいろんな車に車両情報や走行情報のモニタリングなどの機能がついてきてます。
でも僕の車は2003年モデルで最新車種には程遠く、全然コネクトしてくれないわけです。
困っちゃいますよね。

で、突如思いついたんですが、僕には仕事で身についた２つのスキルがあるんですよね。
１つはSORACOMをはじめとするIoTの知識と開発・運用経験、もう１つはCANやOBD2といった車載ネットワークに関する知識と実装経験です。
これを組み合わせればやたら古い僕の車でもコネクテッドカーにできるんじゃね？だって片方はコネクテッドで片方はカーだもの。できないわけがない。

ということでいろいろやって僕の車はコネクテッドカーになりました。

![lagoon.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/20cb5563-d1db-21e8-b11d-9cc94527232b.png)

まだまだ雑ではありますが、こんな感じで車両情報や走行情報が可視化でき、ログに残っていきます。

初期費用15000円くらい、ランニングコストは月500円くらいです。わりといい線いってるのではないでしょうか。
どんな形で実現したかを書いていきます。

# 構成

全体像は以下のようになります。
![スクリーンショット 2020-08-30 0.47.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/8ffae9f3-c21a-f8d7-49b5-43f433bfaa5c.png)

各要素を簡単に説明します。

- 車: モニタリング/ロギングの対象
- ELM327: 車両情報を取得する規格に対応するためのICとBluetoothなどの通信インタフェースが載ったアダプタ
- M5Stack: 画面およびWifi、Bluetoothなどがつき、セルラー通信にも対応できるマイコンボード
- SORACOM Orbit: デバイスとクラウド間でデータを変換できるサービス
- SORACOM Harvest: デバイスからのデータを保存するサービス
- SORACOM Lagoon: Harvestに保存されたデータを自由度の高いダッシュボードで可視化するサービス

どうでしょうか。並べるだけでコネクテッドカーができそうな気がしてきましたね。
それでは具体的に見ていきましょう。

## 車

最近の車は電子制御が当たり前なのですが、その中心となるのがECU(Electronic Control Unit)です。昔はEngine Control Unitって言ってた気がするのですが、呼び方変わったみたいですね。

ECUには通信バス経由でいろいろなセンサーや制御系統が接続されており、センサーの値をもとに様々な制御を行っています。通信バスにはCAN(Controller Area Network)やLIN(Local Interconnect Network)、MOST(Media Oriented Systems Transport)、FlexRayなどがあります。１つしか使ってないわけではなくて、用途とコストで使い分けていますが、メインはCANです。

ECUにはいろんなセンサーがつながってますし、どんな走りをしているのかはECUが一番知ってるので、こいつから情報を引き出せると便利そうですよね。でも内部のネットワークにそのまま繋ぐと危ない(外部からエンジンなどのコントロールができてしまう)ですし、車内ネットワーク内で扱うデータの形式やIDは各車各様で、メンテナンスにて個別対応するの大変すぎるだろ、ということで、メンテナンスのためにECUにアクセスして情報を取得する方法が規格化されました。これがOBD(On-board diagnostics)です。実際はOBDの初版はあんまり共通に使えるものではなかったので、現在はその次のバージョンのOBD2が使われています。

車の中にこういうコネクタついてませんか？下の写真は僕の車のハンドルの下あたりについてたものです。これがOBD2のコネクタで、ここからECUにアクセスできます。

![ODB.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/432414c5-1079-01a6-608d-82ff4e1e3527.jpeg)

この辺りの車載ネットワークや診断系の説明し出すとキリがないので、参考資料としてとりあえずWikipediaを上げておきます。
[Wikipedia オン・ボード・ダイアグノーシス](https://ja.wikipedia.org/wiki/%E3%82%AA%E3%83%B3%E3%83%BB%E3%83%9C%E3%83%BC%E3%83%89%E3%83%BB%E3%83%80%E3%82%A4%E3%82%A2%E3%82%B0%E3%83%8E%E3%83%BC%E3%82%B7%E3%82%B9)

また車載ネットワークの世界では有名なVectorという会社が初心者用の説明資料を用意してくれているので、これを読むとさわりの部分はわかるのではないかと思います。
[はじめての診断](https://assets.vector.com/cms/content/know-how/VJ/PDF/For_Beginners_Diagnostic_Communication.pdf)

## ELM327

OBD2を使うと、ECUからエンジン回転数や水温、バッテリ電圧や燃料の残量など、様々な情報を取得することができます。ですが、OBD2となっても、ECUと直接やりとりをする方式はいくつかあり、ツールを車両に合わせて変更しなければなりません。

以下は[Wikipedia](https://ja.wikipedia.org/wiki/%E3%82%AA%E3%83%B3%E3%83%BB%E3%83%9C%E3%83%BC%E3%83%89%E3%83%BB%E3%83%80%E3%82%A4%E3%82%A2%E3%82%B0%E3%83%8E%E3%83%BC%E3%82%B7%E3%82%B9)からの抜粋です。

![スクリーンショット 2020-08-30 13.05.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/98484a0e-4935-dbe9-2885-075b4a02407d.png)

その状況も最近は改善されていて、ほぼCANでの通信になっています。アメリカでは診断系にはCANを使うことが義務付けられており、日本やヨーロッパも同じようになってきています。そのため、概ね2010年以降の車であれば、CANで接続が可能です。ということは僕の車はダメってことですね。

車に合わせてツールを作るのは大変なのですが、OBD2の通信を仲介してくれるICに[ELM327](https://www.elmelectronics.com/ic/elm327/)があります。こちらはELM Electronics社のICで、[こちらの対応表](https://www.elmelectronics.com/products/ics/obd/)を見ても大半のプロトコルには対応できていることがわかります。基本的にはこれを使えば、ECUと直接通信しなくていいのですね。

このICはOBD2をRS232Cという(昔のPCでよく使われていた)シリアル通信に変換してくれるのですが、このICとBluetoothやWifiなどのインタフェースを載せて、車両の情報をスマホアプリでモニタリングできるアダプタが多数出回っており、アマゾンなどで普通に購入できます。

https://www.amazon.co.jp/gp/bestsellers/automotive/2565871051/ref=zg_b_bs_2565871051_1

2020/8現在では[こちらのAmTakeのアダプタ](https://www.amazon.co.jp/OBD2%E3%82%B9%E3%82%AD%E3%83%A3%E3%83%B3%E3%83%84%E3%83%BC%E3%83%AB-BLUETOOTH-%E8%87%AA%E5%8B%95%E8%BB%8A%E3%82%B9%E3%82%AD%E3%83%A3%E3%83%8A%E3%83%BC-bluetooth%E4%BB%95%E6%A7%98-%EF%BC%8AiPhone%E3%81%AF%E5%AF%BE%E5%BF%9C%E3%81%A7%E3%81%8D%E3%81%BE%E3%81%9B%E3%82%93/dp/B07RNHZS44/)が一位みたいですね。まずはこちらを購入しました。

Bluetoothまでついて1500円以下とか謎の安さですね。あまり安いのは中のELMのICが偽物だったりして、本来使えるはずのコマンドが使えなかったりするとのことで、いろいろ試してみる必要があるみたいです。またV1.5とV2.1が出回っており、新しい方ということでV2.1を選ぶとCAN専用で古い車には対応できないらしいので、古い車に対応する際にはV1.5を選ぶと良いでしょう。

購入できたら、スマホアプリで動作確認をします。アダプタの取説で紹介されているのはTorqueというアプリです。無料版のLiteと有料版のProがあります。
https://play.google.com/store/apps/details?id=org.prowl.torquefree&hl=ja

車のOBD2コネクタにアダプタを挿します。
![ELM327.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/7907dc39-35e6-3509-b099-6e5df23ab9f9.jpeg)

注意点として、OBD2のコネクタには電源が常時入っています。そのため、エンジンがかかっていなかったり、キーが入ってない状態でも動き続けてバッテリを消費し続けます。バッテリが上がらないよう注意しましょう。もうちょっと価格高めのアダプタには、自動的にパワーオフする機能付きのものもあるみたいなので、実際使うにはそっちの方がいいかもしれませんね。

アダプタを動かすとBluetoothでペアリングできるようになりますので、スマホから「OBDII」という名前の機器を探してペアリングします。（名前はアダプタによって違うかも）その後、アプリから「OBDII」を選ぶと通信できるようになります。

ただし古い車はそのままでは対応できないものもあり、その場合はカスタム設定が必要になります。私の車の場合は、このような設定が必要でした。（インターネットで車種を探すと出てきました。こういうのどうやって調べてるんですかね）

![Screenshot_20200830_133158_org.prowl.torque.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/d7ff5b85-1f34-3bce-fa03-7d4579e2dc02.jpeg)

なお、このカスタム設定は有料版じゃないとできませんでした。

また、[OBD Info-san!](https://play.google.com/store/apps/details?id=ph.jpn.ganchi.obdinfosan_trial&hl=ja)というアプリもあり、こちらのトライアル版は短時間しか使えませんが、接続確認はできましたので、つながるかどうか確認する場合はこちらの方がいいかもしれません。

どちらのアプリでも、接続すると車速やエンジン回転数、エンジンの水温など、ECUからの情報を画面に表示してくれます。スマホのGPSや加速度センサの情報なども併用されているようですね。

ここまでで、ELM327を使ってBluetooth通信でECUとやりとりし、車両情報を持ってくることができました。これだけでも結構面白いのですが、コネクテッドカーにするにはこの情報をインターネットにアップロードする必要がありますね。スマホアプリでやってもいいのですが、わざわざアプリを立ち上げたりするのは面倒なので、自動的にやってくれるアイテムが欲しいところです。

## M5Stack

今回は[M5Stack](https://www.switch-science.com/catalog/3647/)を使います。M5Stackは画面とボタンを備えた人気の高いマイコンボードで、モジュールを重ねていくことで拡張できるようになっています。標準でBluetoothが使用でき、拡張モジュールで3Gのセルラー通信もできるようになることから、今回の用途にはうってつけですね。電源もUSBなので、車のシガーソケットからこんな感じで簡単にとることができます。

![M5Stack.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/9c53c035-5afb-721b-9a2f-c56c054ffda5.jpeg)

M5Stackにはマイコンのプログラムを書き込むことができますが、今回は以下の4つが必要になります。

- Bluetoothで接続、送受信するプログラム
- 3Gで接続、送受信するプログラム
- ELM327を設定するプログラム
- ECUからデータを取得するプログラム

---

まずBluetoothの接続、送受信のプログラムです。調べたところBluetoothの通信はSPP(Serial Port Profile)のようです。BLEではないのですね。（余談ですが、BluetoothのアダプタがiOS非対応になっている原因は多分これです。iOSとSPPで通信するためには、Appleと[MFi](https://developer.apple.com/jp/programs/mfi/)という契約をしないといけないんですよね）

古くからあるBluetoothの規格なのですが、BLE専用のBluetoothだと対応してなかったりします。ちょっと心配でしたがちゃんとB5StackはSPPにも対応しており、公式のライブラリもありました。別途導入しなくても最初から使える状態です。

https://github.com/espressif/arduino-esp32/tree/master/libraries/BluetoothSerial

接続する側と、接続される側両方のサンプルがありますが、今回は接続する側を使えば良いでしょう。

https://github.com/espressif/arduino-esp32/blob/master/libraries/BluetoothSerial/examples/SerialToSerialBTM/SerialToSerialBTM.ino

このサンプルはBluetoothの名前で接続する方法で書かれています。今回のアダプタだと「OBDII」でアクセスできるのですが、なんとサンプルに入っている名前が「OBDII」なんですね。これはいけそうな感じ。ただ、名前指定の自動接続だと間違えて他の人のアダプタに繋いでしまうかもしれないので、可能であればBluetoothアドレス指定での接続の方がよいでしょう。接続できたらシリアル通信ができるようになるので、あとは普通のシリアル通信として読み書きすれば良いです。

---

次に3Gに接続し、送受信するためのプログラムですが、こちらはSORACOMが説明してくれているのでそれを使えば良いでしょう。

https://dev.soracom.io/jp/start/m5stack/#step3

あとで説明しますが、今回はSORACOM Orbitを使いたいため、通信先はUnified Endpointにします。M5StackでUDPを使うのは面倒なので、TCPの23080に対して通信すると良いでしょう。(HTTPでも良いがヘッダーを作ったりするのがやや面倒)

---

次にELM327の設定ですが、これはなかなか厄介です。ELM327は基本的にはELM327に対するATコマンドで設定します。その方法を記載したデータシートはこちらからダウンロードできます。

https://www.elmelectronics.com/wp-content/uploads/2016/07/ELM327DS.pdf

94ページの英語のデータシートで、そこそこのボリュームです。ハードウェア的なピン配置やタイミングなどもあるので、通信として見ないといけないのは8ページから始まる「Communicating with the ELM327」です。また10ページからATコマンドリストとその説明がありますので、これを見ながらATコマンドを送って設定します。例えば以下のようなコマンドを送ります。

```
ATZ    # ELM327をリセットする
ATSP4  # ECUとの通信プロトコルをKWPに設定する
ATIB96 # ECUとの通信ボーレートを9600bpsにする
```

一部車種に合わせる必要のあるコマンドもあることがわかりますね。

ちゃんとしたプロダクトを作るにはちゃんと読んで作る必要がありますが、今回はなんちゃってなので簡略化して、動作確認したAndroidアプリと同じコマンドを送るようにします。Androidアプリは単にSPPで接続してコマンドを送っているだけなので、PCなどのBluetoothが受信できる機器であれば同じように受信できます。受信したコマンドをログに残しつつそのままELM327に流せば、動作に必要なコマンドがわかります。PCが通信のプロキシをする形ですね。あまり褒められた調査方法ではないのですが、動作するアプリが手元にある状況を最大限活用させてもらいました。（ハードウェアがちょっとわかる人なら、ELMのRS232Cの信号をPCに繋いで端末ソフトで表示するのもありです）

コマンドがわかったら、Bluetoothで接続したあと、応答を確認しながらコマンドを送っていきましょう。改行コードは<CR>で、応答は最後「>」で終わります。これは次のコマンドを送っていいよ、というプロンプトですね。ここまで受信できたら次のコマンドを送り、送るコマンドが無くなったら初期化終了です。この部分は車種によって変わるので、マイコンのプログラムに埋め込むのではなく、SORACOM Air メタデータタグに入れて受信して使うと良いでしょう。

---

最後にECUとの通信です。これはATコマンドではなく、16進テキスト形式のバイナリコマンドです。この形式のコマンドはECUに転送されるようになっています。

このコマンドについては、30ページからのOBD Commands、Talking to the Vehicleの章にて説明されています。基本的には送信は以下の2バイトです。

１バイト目 - モード
2バイト目 - PID(Parameter ID)

どのようなモード、各モードにどのようなIDがあるかは規格に記載されていますが、以下のページにわかりやすくまとめられています。

https://en.wikipedia.org/wiki/OBD-II_PIDs

ページに`Before 2002, J1979 referred to these services as "modes"`と書かれているように、serviceとmodeは同じものです。最新の規格だとserviceなのですが、ELM327のデータシートはmodeと書かれているので、以下modeで統一します。

例えば、車速はmode=01、PID=0Dなので、

```
010D<CR>
```

と要求します。するとECUは以下のような応答を返します。

```
410D3C<CR><CR>>
```

先頭の1バイト目がモード、2バイト目がPIDであることは変わりません。1バイト目に40が加算されているのは、レスポンスであることを表しています。続くバイトが応答です。PIDの内容を確認すると、

![スクリーンショット 2020-08-30 18.47.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/0b9d6fa5-74a8-b3bd-843f-1193da405f91.png)

となっており、応答のバイト数は1バイトで、数字はそのまま使うので(Aは応答の1バイト目)車速は60(3C)km/hであることがわかります。

さて、お気づきの通り、PIDごとに戻りのバイト数やその数値をどのように扱うかは個別に定義されています。これをマイコンで対応させるの大変ですよね。プログラムは複雑になるし、取得する値の種類が増えるたびにバージョンアップが必要になります。そんなのやってられません。そのため、PIDの解釈はこのあと説明するSORACOM Orbitにまかせ、マイコンは応答文字列をそのままクラウドに送ることとします。

どのPIDの値を取得するかは車種や用途によってかわりますので、こちらもSORACOMのタグから取得できるようにすると良いでしょう。

以上を踏まえてM5Stackのコードは以下のようになりました。(C++で書ければもっとスマートになるはずですが、まだ書けないのでCで書いてます。。)

```c
#include <M5Stack.h>
#include "BluetoothSerial.h"
#define TINY_GSM_MODEM_UBLOX
#include <TinyGsmClient.h>
TinyGsm modem(Serial2); /* 3G board modem */
TinyGsmClient ctx(modem);

char initializeCommands[1024];
char fetchCommands[1024];

BluetoothSerial SerialBT;

void setup() {
  Serial.begin(115200);
  M5.begin();
  M5.Lcd.clear(BLACK);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.println(F("M5Stack + 3G Module"));

  M5.Lcd.print(F("modem.restart()"));
  Serial2.begin(115200, SERIAL_8N1, 16, 17);
  modem.restart();
  M5.Lcd.println(F("done"));

  M5.Lcd.print(F("getModemInfo:"));
  String modemInfo = modem.getModemInfo();
  M5.Lcd.println(modemInfo);

  M5.Lcd.print(F("waitForNetwork()"));
  while (!modem.waitForNetwork()) M5.Lcd.print(".");
  M5.Lcd.println(F("Ok"));

  M5.Lcd.print(F("gprsConnect(soracom.io)"));
  modem.gprsConnect("soracom.io", "sora", "sora");
  M5.Lcd.println(F("done"));

  M5.Lcd.print(F("isNetworkConnected()"));
  while (!modem.isNetworkConnected()) M5.Lcd.print(".");
  M5.Lcd.println(F("Ok"));

  M5.Lcd.print(F("My IP addr: "));
  IPAddress ipaddr = modem.localIP();
  M5.Lcd.println(ipaddr);

  char btTarget[16];
  getHttp("metadata.soracom.io", "/v1/subscriber.tags.target", btTarget, sizeof(btTarget));
  Serial.println(btTarget);
  String name = btTarget;

  getHttp("metadata.soracom.io", "/v1/subscriber.tags.init", initializeCommands, sizeof(initializeCommands));
  Serial.println(initializeCommands);

  getHttp("metadata.soracom.io", "/v1/subscriber.tags.fetch", fetchCommands, sizeof(fetchCommands));
  Serial.println(fetchCommands);

  SerialBT.begin("M5Stack", true); 
  
  M5.Lcd.println("Bluetooth connecting");
  bool connected = SerialBT.connect(name);
  
  if(connected) {
    Serial.println("Connected Succesfully!");
    M5.Lcd.println("Connected Succesfully!");
  } else {
    while(!SerialBT.connected(10000)) {
      Serial.println("Failed to connect. Make sure remote device is available and in range, then restart app."); 
    }
  }

  char *p = initializeCommands;
  while (true){
    char command[64];
    memset(command, 0, sizeof(command));
    char *p1 = strchr(p, (int)',');
    if (p1 != NULL){
      memcpy(command, p, p1 - p);
      command[p1 - p] = 0;
    } else {
      strcpy(command, p);
    }
    strcat(command, "\r");

    M5.Lcd.println(command);

    SerialBT.write((const uint8_t *)command, strlen(command));
    char recvBuf[1024];
    int len = readUntil(recvBuf, sizeof(recvBuf), '>');
    Serial.println(recvBuf);
    M5.Lcd.println(recvBuf);

    if (p1 == NULL){
      break;
    } else {
      p = p1 + 1;
    }
  }
}

void loop() {
  M5.update();

  M5.Lcd.clear(BLACK);
  M5.Lcd.setCursor(0, 0);

  char sendBuf[1024];
  memset(sendBuf, 0, sizeof(sendBuf));

  char *p = fetchCommands;
  while (true){
    char command[64];
    memset(command, 0, sizeof(command));
    char *p1 = strchr(p, (int)',');
    if (p1 != NULL){
      memcpy(command, p, p1 - p);
      command[p1 - p] = 0;
    } else {
      strcpy(command, p);
    }
    strcat(command, "\r");

    M5.Lcd.println(command);
    SerialBT.write((const uint8_t *)command, strlen(command));
    char btBuf[128];
    int len = readUntil(btBuf, sizeof(btBuf), '>');
    Serial.println(btBuf);
    M5.Lcd.println(btBuf);
    char *delimiter = strchr(btBuf, (int)'\r');
    if (delimiter != NULL) {
      *delimiter = 0;
    }
    if (p != fetchCommands){
      strcat(sendBuf, ",");
    }
    strcat(sendBuf, btBuf);

    if (p1 == NULL){
      break;
    } else {
      p = p1 + 1;
    }
  }

  if (!ctx.connect("uni.soracom.io", 23080)) {
    Serial.println(F("Connect failed."));
    return;
  }

  ctx.print(sendBuf);
  M5.Lcd.println("sent");

  char soracomBuf[128];
  memset(soracomBuf, 0, sizeof(soracomBuf));
  ctx.readBytes(soracomBuf, sizeof(soracomBuf));
  ctx.stop();
  M5.Lcd.println(soracomBuf);

  delay(5000);
}

size_t getHttp(char *host, char *path, char *recvBuf, int len){
  memset(recvBuf, 0, len);
  if (!ctx.connect(host, 80)) {
    Serial.println(F("Connect failed."));
    return -1;
  }

  ctx.print("GET ");
  ctx.print(path);
  ctx.println(" HTTP/1.0");
  ctx.print("Host: ");
  ctx.println(host);
  ctx.println();

  while (ctx.connected()) {
    String line = ctx.readStringUntil('\n');
    if (line == "\r") {
      Serial.println("headers received.");
      break;
    }
  }
  int recvLen = ctx.readBytes(recvBuf, len);
  ctx.stop();

  if (recvBuf[recvLen - 1] == '\r' || recvBuf[recvLen - 1] == '\n'){
    recvBuf[recvLen - 1] = 0;
    recvLen--;
  }

  return recvLen;
}

size_t readUntil(char *buf, size_t len, char delimiter){
  int i = 0;
  memset(buf, 0, len);
  while (true) {
    int chr = SerialBT.read();
    if (chr < 0){
      continue;
    }
    buf[i] = (char)chr;
    i++;
    if (chr == delimiter){
      return i;
    }
    if (i == len) {
      return i;
    }
  }
  return -1;
}
```

セルラーの方はそうでもないけど、Bluetoothは接続できないこともあったり、また途中で通信切れたりした時の処理がなかったり、安定性に難があるため、あとで改善します。多分。
M5Stackについては以上です。

## SORACOM Orbit

[SORACOM Orbit](https://soracom.jp/services/orbit/)は2020年の7月に発表された新サービスです。今回の用途でなぜSORACOM Orbitを使いたいのかを簡単に説明します。

IoT設計の基本的な考え方としては、デバイスファームウェアの変更は最小限になるようにしたいものです。クラウドのバージョンアップに比べてデバイスのバージョンアップは大変です。対象となるデバイスは多く、接続や電源状態も不安定で、失敗した際の切り戻しがとても難しいという事情によります。クラウドのように失敗したら立て直す、というわけにはいかないんですね。

それを考えると、PIDをマイコンで解釈させて、解釈後の値を送信する、という方式だと、定義が追加されるたびにファームウェア変更が必要になるため、やりたくありません。

そこでマイコン側はデータに手をつけずそのままクラウドに送信して、クラウドでデータを加工する、という方式をとることになります。この場合、データ加工のプログラムが書けるサーバーやFaaSに送る場合はまだよいのですが、送ったデータをそのまま保存や利用するSaaSを利用することが難しくなってしまう、という課題があります。SORACOMで言えばSORACOM HarvestやSORACOM Funnelのようなサービスとの連携ですね。この場合はいったんSORACOM Funk + AWS Lambdaでデータを処理して、改めてSaaSにデータを送る、というデータフローになります。デバイスに比べると相当マシですがまだ面倒であり、またSORACOMのSIM認証が使いにくいというデメリットもあります。

このような場合、以前はバイナリパーサーというサービスを使っていました。(今も使えるサービスです)これはデータをバイナリ値として解釈し、簡単な変換をしてからJSONで出力する、というサービスですが、固定のデータフォーマットに対する変換であるため、今回のように先頭データによって以降のデータの解釈が変わる、というタイプのデータには使えません。

そこでSORACOM Orbitです。SORACOM Orbitはデバイスとクラウドの間に存在し、デバイスからクラウド、クラウドからデバイスへ送られるデータを自分で書いたプログラムで加工することができます。そのため、今回のようなデータ形式であっても十分対応することができます。

また、SaaSに限らず連携先に届く前にデータを加工できるので、データの加工部分を一箇所にまとめることができます。加工方法が同じであれば、SORACOM Harvestに連携する際も、SORACOM Beamで自社サーバーに連携する際も、SORACOM FunnelでAWSやAzureなどのSaaSに連携する際も、SORACOM FunkでFaaSに連携する際も、同じプログラムで連携することができます。(ちょっとした差分であれば、メタデータと連携することで吸収できます)「OBD2の仕様に基づいたデータ変換」といった再利用性の高いプログラムであれば、様々なところで使いまわせそうですよね。この部分も良いところだと思います。

SORACOM Orbitのプログラムは言語を問わず、WASM(WebAssembly)形式にビルドできるものであれば使用可能です。サービスで使用するのはビルド済みのWASMモジュールです。今回は使い慣れているので、やはりC言語で記述します。(Rustとかやってみたい気もします)

```c
#include <emscripten.h>
#include "soracom/orbit.h"

#include "nlohmann/json.hpp"
using nlohmann::json;

int32_t uplink_body();
uint8_t HexStringToChar(char *buf);

extern "C" {

EMSCRIPTEN_KEEPALIVE
int32_t uplink() {
    return uplink_body();
}

}

int32_t uplink_body() {
    const char* buf = NULL;
    size_t siz = 0;
    int32_t err = soracom_get_input_buffer_as_string(&buf, &siz);
    if (err < 0) {
        return err;
    }

    char input[siz + 1];
    memcpy(input, buf, siz);
    input[siz] = 0;
    soracom_release_input_buffer(buf);
    json j;

    char *p = input;
    while (true){
        char *p1 = strchr(p, (int)',');
        if (p1 != NULL){
            *p1 = 0;
        }

        uint8_t firstByte = HexStringToChar(p);
        uint8_t mode = firstByte & 0x0F;
        uint8_t pid = HexStringToChar(p + 2);

        // https://en.wikipedia.org/wiki/OBD-II_PIDs
        if (mode == 1){
            // Show current data
            if (pid == 5){
                // Engine coolant temperature
                // Length: 1
                // A - 40
                j["EngineCoolantTemperature"] = HexStringToChar(p + 4) - 40;
            } else if (pid == 12) {
                // Engine speed
                // Length: 2
                // 256A + B / 4
                uint16_t speed = ((uint16_t)HexStringToChar(p + 4) * 256 + (uint16_t)HexStringToChar(p + 6)) / 4;
                j["EngineSpeed"] = speed;
            } else if (pid == 13) {
                // Vehicle speed
                // Length: 1
                // A
                j["VehicleSpeed"] = HexStringToChar(p + 4);
            }
        }

        if (p1 == NULL){
            break;
        } else {
            p = p1 + 1;
        }
    }

    std::string output = j.dump();
    soracom_set_json_output(output.c_str(), output.size());

    return 0;
}

uint8_t HexStringToChar(char *buf){
    uint8_t ret = 0;
    for (int i = 0; i < 2; i++){
        if (buf[i] >= '0' && buf[i] <= '9'){
            ret = ret * 16 + (buf[i] - '0');
        } else if (buf[i] >= 'a' && buf[i] <= 'f'){
            ret = ret * 16 + (buf[i] - 'a') + 10;
        } else if (buf[i] >= 'A' && buf[i] <= 'F'){
            ret = ret * 16 + (buf[i] - 'A') + 10;
        } else {
            // Error
            return 0;
        }
    }

    return ret;
}
```

とりあえず、車速、エンジン回転数、エンジン水温を解釈できるようにしました。このプログラムをWASM形式にビルドして、SORACOM Orbitにアップロードします。ビルド環境の提供やビルド、アップロードの方法などは開発者ガイドに記載がありますので、こちらを参照ください。

https://dev.soracom.io/jp/orbit/what-is-orbit/

アップロードしたら、M5Stackで使用するSIMのグループにSORACOM Orbitの設定をすれば、アップロードしたら加工済みのデータがクラウドに送られるようになります。

## SORACOM Harvest、SORACOM Lagoon

最後にSORACOM HarvestとSORACOM Lagoonです。

[SORACOM Harvest](https://soracom.jp/services/harvest/)はデバイスからアップロードされたデータを保存するサービスです。また、保存されたデータをテキストやシンプルなグラフで確認することができます。

また、データアップロード元のデバイス認証にSIMを使っています。その回線で接続されていること自体が正しいデバイスであることを保証しているので、デバイス側に認証情報を持ち、認証処理をする必要がなくなるのですね。必要な設定は、使うか使わないかだけの、素晴らしくシンプルなサービスです。

![スクリーンショット 2020-08-30 19.57.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/2550ed1f-6538-efa0-074b-25d215ce6aad.png)

IoTデバイスのデータをアップロードして保存してとりあえず見れるようにするのに、これ以上簡単にできるサービスはちょっと思いつきません。

SORACOM Harvestを強化するのがダッシュボード作成・共有サービスの[SORACOM Lagoon](https://soracom.jp/services/lagoon/)です。

SORACOM Harvestでの可視化はシンプルなものですが、SORACOM Lagoonではグラフや値表示、メーター表示や地図表示など、様々な形式のパネルを好きなように定義したダッシュボードを作成することができます。また作成したダッシュボードを他の人と共有することもできます。こちらは公式ページからの引用です。

![スクリーンショット 2020-08-30 20.01.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/c68f1681-eb3a-085b-cfb2-19238e7ba4e2.png)

SORACOM HarvestにアップロードされたデータをLagoonで可視化するのは、手っ取り早くデータをいい感じに可視化するいい方法です。可視化の部分を自分で作るのはとても時間がかかるので、とりあえず人に見せるときにはこういう出来合いのものをまずは使ったほうがいいでしょう。

また、届いたデータに条件を設定し、条件に合致したら通知をすることもできます。例えば車速が100km/hを超えたらLINEに警告が届く、といった使い方ができます。

長くなりましたが、構成の説明は以上です。

# コスト

コストは初期コストとランニングコストに分かれます。(調べて書いてはいますが、公式の資料と相違がある場合は当然公式の資料が正しいので、実際使用する前には公式資料をご参照ください)

初期コスト

| 項目     |  価格 |
|----------|------:|
| [M5Stack Basic 3G 拡張ボード セット](https://soracom.jp/products/kit/3g_module_m5stack_set/) | 12,800円 |
| [特定地域向け IoT SIM (plan-D)](https://soracom.jp/products/sim/plan-d/)　| 852円 |
| [ELM327 OBD2 スキャンツール ](https://www.amazon.co.jp/OBD2%E3%82%B9%E3%82%AD%E3%83%A3%E3%83%B3%E3%83%84%E3%83%BC%E3%83%AB-BLUETOOTH-%E8%87%AA%E5%8B%95%E8%BB%8A%E3%82%B9%E3%82%AD%E3%83%A3%E3%83%8A%E3%83%BC-bluetooth%E4%BB%95%E6%A7%98-%EF%BC%8AiPhone%E3%81%AF%E5%AF%BE%E5%BF%9C%E3%81%A7%E3%81%8D%E3%81%BE%E3%81%9B%E3%82%93/dp/B07RNHZS44) | 1,459円 |
| 合計 | 15,111円 |

特にELM327 OBD2 スキャンツールは現時点の価格であり、今後変動したり商品がなくなる可能性は十分あります。ご了承ください。
おおよそ1万5千円といったところですね。

ランニングコスト

| 項目     |  価格 |
|----------|------:|
| SIM 基本料金 | 10円/日<br>310円/月 |
| SORACOM Orbit 基本使用料 | 20円/月<br>無料利用枠:1SIM分 |
| SORACOM Orbit リクエスト料金 | 50円/1万回<br>無料利用枠:1万回分 |
| SORACOM Harvest | 5円/日<br>155円/月<br>無料利用枠:31日分 |
| SORACOM Lagoon | Freeプラン: 無料 <br> Makerプラン: 980円/月 <br> Proプラン: 4980円/月 |

１人1SIMで使用している分にはSIM基本料金以外は無料利用枠に入ります。Orbitの使用料は使用状況によりますが、ずっと運転しているわけでなければ月1万回には入るだろうという考えです。(5秒に1回1時間で720回)

Lagoon以外のサービス料はかかるとすると、550円/月程度の費用です。年間で6600円程度。すごく高いというわけではないですが、無視できるほど安い、というわけでもないですね。下げるためには、データ量を減らして基本料金の安いSIMにする、データを複数回まとめて送る、利用しない日はHarvestをOFFにする、などが考えられます。
コストダウンの方法をいろいろ考えるのも面白そうなので、何か思いついたらブログにします。

# おわりに

今回古い車を自分のスキルでコネクテッドカーにしよう、という思いつきで始まりまして、最初は無茶なんじゃないかと思いましたが、やってみると案外簡単にできてしまいました。既製品のデバイスやサービス、ライブラリなどが揃っていて、組み合わせていくだけでサービスになる、というのが分かりますね。いい時代になったものです。

一方今回は車両や走行データの保存、可視化というところまではできましたが、本当に価値を産むのはこのデータの活用というところなのだと思いました。例えば運転状況から運転のクセを指摘するであるとか、車両に故障の兆しがあれば本人やメンテナンス業者に連絡がいく、といったところですね。実際のコネクテッドカーではその辺りのサービスも含めて考えられているのでしょう。今後はメーカーもデータ活用ビジネスみたいな考えていかないといけないので大変ですね。。

新サービスのSORACOM Orbitは今回の要件にピッタリはまってすごくよかったです。Orbitに限らずSORACOMサービスはデバイスに負担をかけずに様々なサービスとの連携を簡単にしてくれるので、SORACOMを試してみたことのない人はぜひ試してみていただければと思います。

以上です。