SORACOM Air for LoRaWAN で GPS トラッカー (Grove GPS 編)

審査員として参加した [道東×IoTハッカソン](https://connpass.com/event/79932/) の空き時間で LoRaWAN を使った GPS トラッカーを作ってみたというお話しです。

アプリケーションは先日アップデートされた SORACOM Harvest の [マップ機能](https://blog.soracom.jp/blog/2018/04/26/harvest-map-support/) を活用しています

![lorawan+gps/device](https://docs.google.com/drawings/d/e/2PACX-1vTJl-N5L4CFJlzyF8o4r2dKsBI_JpN_01Ja-hpiMPptofR_lBm_2dUcewHiiv623esuNiydqyVtr_pv/pub?w=617&h=465)

![lorawan+gps/map](https://docs.google.com/drawings/d/e/2PACX-1vRLEm08beEqqCtwg2KVAQ03SIsbB-gu8yVUA4-N-I9-HfAzSISCFXnWDDYusTGnh7o-kQQ8nCL5OuLZ/pub?w=916&h=687)

# 解説

## ハードウェアと費用

| 品        | 用途     |  金額(円、税込み概算)  |
|-----------|----------|-------:|
| Arduino UNO R3 | マイコン | 3500 |
| [Grove ベースシールド V2](http://wiki.seeedstudio.com/Base_Shield_V2/) | GPS 取り付け | 1200 |
| [Grove GPS](http://wiki.seeedstudio.com/Grove-GPS/) | GPS | 4200 |
| [LoRa 開発シールド (AL-050)](https://soracom.jp/products/lora/al-050/) | LoRaWAN 通信モデム | 8000 |
|  合計 | |  **16900** | 

### Grove ベースシールド V2

Arduino UNO R3 のピンヘッダを Grove コネクタシステムに変換するシールドです。(ピン互換なマイコンボードなら使えるハズです（未確認）)

Analog, Digital, UART, I2C が Grove コネクタになっています。

![Grove ベースシールド V2](https://raw.githubusercontent.com/SeeedDocument/Base_Shield_V2/master/img/Base_Shield_v2-1.png)

今回 GPS モジュールは D6 に取り付けています。
[仕様](http://wiki.seeedstudio.com/Grove-GPS/)では UART ですが [SoftwareSerial](https://www.arduino.cc/en/Reference/SoftwareSerial) を利用することで digital ポートを UART として使えるようにしています。
通信速度が 9600bps 程度であれば SoftwareSerial で大丈夫です。

ちなみに D2 に取り付けると LoRa 開発シールドの LoRa モデムと干渉する関係でまともに動きません。ご注意ください。
（LoRa モデムの初期化に失敗します）

## ソフトウェア (ファームウェア)

### ペイロード設計

ペイロードのフォーマットは素直にバイトストリーム(バイナリ)にしました。

| 緯度 | 経度 |
|----|----|
| float 4 byte | float 4 byte |

[Arduino UNO R3 のバイトオーダーはリトルエンディアン](https://forum.arduino.cc/index.php?topic=429183.0) です。

### コード (スケッチ)

ペイロードのフォーマット設計が決まったらコードを書いていきます。

```c++:lora_gps.ino
#include <SoftwareSerial.h>
SoftwareSerial gpsport(6, 7);
#include "TinyGPS++.h"
TinyGPSPlus gps;
#include <lorawan_client.h>
LoRaWANClient client;

typedef union
{
 float number;
 uint8_t bytes[4];
} FLOATUNION_t;

void setup() {
  Serial.begin(9600);
  gpsport.begin(9600);
  if(! client.connect())
  {
    Serial.println(" failed to connect. Halt...");
    for(;;){};
  }
}

void loop()
{
  byte payload[8];
  FLOATUNION_t lat;
  FLOATUNION_t lng;
  uint8_t payload_offset; 
  
  gpsport.listen();
  while (gpsport.available() > 0)
    gps.encode(gpsport.read());
  if (gps.location.isValid())
  {
    payload_offset = 0;
    lat.number = gps.location.lat();
    Serial.println(lat.number, 6);
    for (int i = 0; i < 4; i++)
      payload[i + payload_offset] = lat.bytes[i];
      
    payload_offset = 4;
    lng.number = gps.location.lng();
    Serial.println(lng.number, 6);
    for (int i = 0; i < 4; i++)
      payload[i + payload_offset] = lng.bytes[i];

    for (int i = 0; i < sizeof(payload); i++)
      Serial.print(payload[i], HEX);
    Serial.println();

    client.sendBinary(payload, sizeof(payload), 2);
    delay(6000);
  } else {
    Serial.println("INVALID");
  }
}
```

# 解説

ポイントは下記の通りです。

* GPS との UART 通信に SoftwareSerial を利用
* GPS から出力される [NMEA 形式](https://ja.wikipedia.org/wiki/NMEA_0183) のデコードに [TinyGPS++](http://arduiniana.org/libraries/tinygpsplus/) を利用
* float 型から byte 型への変換に [union](https://wwws.kobe-c.ac.jp/deguchi/c/union.html) を利用

## GPS との UART 通信に SoftwareSerial を利用

D6 ポートに接続した GPS モジュールと通信するための宣言です。
Grove ベースシールドの D6 は [仕様](http://wiki.seeedstudio.com/Base_Shield_V2/#hardware-overview) を見ると D6 と D7 に接続されていますので、それをそれぞれ TX と RX に割り当てるように宣言しています。

```c++
#include <SoftwareSerial.h>
SoftwareSerial gpsport(6, 7);
```

`setup()` 内で通信速度を決定した後 `loop()` 内に実装しますが `gpsport.read()`  の直前で `gpsport.listen()` を忘れないようにしてください。

```c++:loop()内
  gpsport.listen();
  while (gpsport.available() > 0)
    gps.encode(gpsport.read());
```

SoftwareSerial は同時に複数のポートを開くことができません。 実は LoRa 開発シールドも SoftwareSerial 使っているため、そこから制御を得るために `read()` 前に `listen()` を発行する必要があります。

ちなみに LoRa 開発シールドのライブラリでも `listen()` しています。

## TinyGPS++ の利用

TinyGPS++ のポイントは `#encode()` 関数です。うまくデコードできれば `location#isValid()` が `true` になるので、あとは `location` オブジェクトの中を参照してください。

```c++:loop()内
  gpsport.listen();
  while (gpsport.available() > 0)
    gps.encode(gpsport.read());
  if (gps.location.isValid()) {
    float v = gps.location.lat();
  }
```

## float 型から byte 型への変換

union を利用して 1 バイトずつ切り出せるようにします。

```
typedef union
{
 float number;
 uint8_t bytes[4];
} FLOATUNION_t;
```

以上の宣言をすると下記のように使うことができます。

```c++
FLOATUNION_t lat;
lat.number = 123.11; /* float 型を代入 */
Serial.println(lat.bytes[0], HEX); /* 変数内の1バイト目を取得 */
```

これを byte 型へ愚直に押し込めるといった方法になります。

```c++
    lat.number = gps.location.lat();
    for (int i = 0; i < 4; i++)
      payload[i] = lat.bytes[i];

    lng.number = gps.location.lng();
    for (int i = 0; i < 4; i++)
      payload[i + 4] = lng.bytes[i]; /* payload への代入は、位置をずらさないと上書きになっちゃうよ */
```

最後は `client.sendBinary(payload, sizeof(payload));` として送信します。

## SORACOM Air for LoRaWAN / Sigfox のバイナリパーサー機能

ペイロードが下記のようになっているので、それを SORACOM 上で展開するように [バイナリパーサー機能](https://blog.soracom.jp/blog/2017/07/05/binary-parser/) を設定します

| 緯度 | 経度 |
|----|----|
| float 4 byte | float 4 byte |

これに対するフォーマットは下記の通りです。

`lat::float:32:little-endian lng::float:32:little-endian`
※Arduino UNO R3 のバイトオーダーはリトルエンディアンなので、末尾に `little-endian` を指定しています。というか、指定できるのかよ！

これで SORACOM からは下記の通りに出力されます。

```json
{
  "lat": 35.1111111,
  "lng": 139.2222222
}
```

![lorawan+gps/binaryparser](https://docs.google.com/drawings/d/e/2PACX-1vRP3ohVHGQaS7sDxWUCX7ITpEDZ14l3iEd1IO4MbAHVYiUntVgUwRwVxuB6M6MGkCLotctnXWc-3PVo/pub?w=681&h=439)

#### SORACOM Harvest へのデータ送信

先日リリースされた SORACOM Harvest の新機能 [位置情報データのサポート](https://blog.soracom.jp/blog/2018/04/26/harvest-map-support/) によると

> "lat (または latitude)“, "lon (または longitude, lng, long)” を含むデータを SORACOM Harvest に POST してださい。

ということですので、先ほどのバイナリパーサー機能のキーで位置情報がレンダリングされるわけです。

## ソフトウェア実装における補足事項

### ペイロード設計の背景

LoRaWAN は 1 回のデータ送信に 11 バイト使えます。このサイズに対して緯度、経度の情報を小数点込みで文字列として送る方法としては

* `35.xx139.yy` と小数点の精度を減らす
* `1xxxx2yyyy` と北緯35度もしくは東経139度からの相対値＋小数点の精度を減らす

こんな方法が考えられますが、いづれにおいても「精度が悪い」「国内限定」といった制約があります。

### GPS の小数点における精度

GPS の小数点以下の精度については [携帯のGPS精度の計算](https://naokiwatanabe.blogspot.jp/2015/03/gps.html) や [GPSログの桁数を考えてみました](http://sunao-mita.pgw.jp/blog/index.php/2015/11/26/gps-logger/) が詳しく解説されていますが、まとめると下表のようになります

| 小数点 | 精度 |
|---------|----|
| 第 0 位 | 約 100 km |
| 第 1 位 | 約  10 km |
| 第 2 位 | 約   1 km |
| 第 3 位 | 約 100 m |
| 第 4 位 | 約  10 m |
| 第 5 位 | 約   1 m | 
| 第 6 位 | 約  10 cm |
| 第 7 位 | 約   1 cm |

用途にもよりますが、 小数点第 5 位は欲しいものです。

### ペイロードサイズ削減のアイデア

小数点部に対して必要な精度を整数化して送り、受け取り側で精度を戻すといった考え方もあります。
例えば `35.xxxxx` であれば 100,000 を乗算して `3,5xx,xxx` にした後に 100,000 の除算をするという方法です
今回の例では結局 4 バイト消費する long 型になるため採用を見送りましたが、計算結果が unsigned integer 型 (2バイト) に収まるのであれば有効な方法ですので、覚えておいて損は無いと思います。

ちなみに SORACOM Air for LoRaWAN (Sigfox) では、後述するバイナリパーサー機能上で四則演算ができるため、デバイスからは unsigned integer で送りつつ、バイナリパーサー機能で除算できます。
そのため、クラウド側で除算する必要が無いので便利かなと。

# あとがき

空き時間にやったワリには、壮大な解説ブログになってしまいました。
ちゃんと審査はしたよ！

EoT