審査員として参加した 道東×IoTハッカソン の空き時間で LoRaWAN を使った GPS トラッカーを作ってみたというお話しです。
アプリケーションは先日アップデートされた SORACOM Harvest の マップ機能 を活用しています
解説
ハードウェアと費用
品 | 用途 | 金額(円、税込み概算) |
---|---|---|
Arduino UNO R3 | マイコン | 3500 |
Grove ベースシールド V2 | GPS 取り付け | 1200 |
Grove GPS | GPS | 4200 |
LoRa 開発シールド (AL-050) | LoRaWAN 通信モデム | 8000 |
合計 | 16900 |
Grove ベースシールド V2
Arduino UNO R3 のピンヘッダを Grove コネクタシステムに変換するシールドです。(ピン互換なマイコンボードなら使えるハズです(未確認))
Analog, Digital, UART, I2C が Grove コネクタになっています。
今回 GPS モジュールは D6 に取り付けています。
仕様では UART ですが SoftwareSerial を利用することで digital ポートを UART として使えるようにしています。
通信速度が 9600bps 程度であれば SoftwareSerial で大丈夫です。
ちなみに D2 に取り付けると LoRa 開発シールドの LoRa モデムと干渉する関係でまともに動きません。ご注意ください。
(LoRa モデムの初期化に失敗します)
ソフトウェア (ファームウェア)
ペイロード設計
ペイロードのフォーマットは素直にバイトストリーム(バイナリ)にしました。
緯度 | 経度 |
---|---|
float 4 byte | float 4 byte |
Arduino UNO R3 のバイトオーダーはリトルエンディアン です。
コード (スケッチ)
ペイロードのフォーマット設計が決まったらコードを書いていきます。
#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 形式 のデコードに TinyGPS++ を利用
- float 型から byte 型への変換に union を利用
GPS との UART 通信に SoftwareSerial を利用
D6 ポートに接続した GPS モジュールと通信するための宣言です。
Grove ベースシールドの D6 は 仕様 を見ると D6 と D7 に接続されていますので、それをそれぞれ TX と RX に割り当てるように宣言しています。
#include <SoftwareSerial.h>
SoftwareSerial gpsport(6, 7);
setup()
内で通信速度を決定した後 loop()
内に実装しますが gpsport.read()
の直前で gpsport.listen()
を忘れないようにしてください。
gpsport.listen();
while (gpsport.available() > 0)
gps.encode(gpsport.read());
SoftwareSerial は同時に複数のポートを開くことができません。 実は LoRa 開発シールドも SoftwareSerial 使っているため、そこから制御を得るために read()
前に listen()
を発行する必要があります。
ちなみに LoRa 開発シールドのライブラリでも listen()
しています。
TinyGPS++ の利用
TinyGPS++ のポイントは #encode()
関数です。うまくデコードできれば location#isValid()
が true
になるので、あとは location
オブジェクトの中を参照してください。
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;
以上の宣言をすると下記のように使うことができます。
FLOATUNION_t lat;
lat.number = 123.11; /* float 型を代入 */
Serial.println(lat.bytes[0], HEX); /* 変数内の1バイト目を取得 */
これを byte 型へ愚直に押し込めるといった方法になります。
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 上で展開するように バイナリパーサー機能 を設定します
緯度 | 経度 |
---|---|
float 4 byte | float 4 byte |
これに対するフォーマットは下記の通りです。
lat::float:32:little-endian lng::float:32:little-endian
※Arduino UNO R3 のバイトオーダーはリトルエンディアンなので、末尾に little-endian
を指定しています。というか、指定できるのかよ!
これで SORACOM からは下記の通りに出力されます。
{
"lat": 35.1111111,
"lng": 139.2222222
}
SORACOM Harvest へのデータ送信
先日リリースされた SORACOM Harvest の新機能 位置情報データのサポート によると
"lat (または latitude)“, "lon (または longitude, lng, long)” を含むデータを SORACOM Harvest に POST してださい。
ということですので、先ほどのバイナリパーサー機能のキーで位置情報がレンダリングされるわけです。
ソフトウェア実装における補足事項
ペイロード設計の背景
LoRaWAN は 1 回のデータ送信に 11 バイト使えます。このサイズに対して緯度、経度の情報を小数点込みで文字列として送る方法としては
-
35.xx139.yy
と小数点の精度を減らす -
1xxxx2yyyy
と北緯35度もしくは東経139度からの相対値+小数点の精度を減らす
こんな方法が考えられますが、いづれにおいても「精度が悪い」「国内限定」といった制約があります。
GPS の小数点における精度
GPS の小数点以下の精度については 携帯のGPS精度の計算 や GPSログの桁数を考えてみました が詳しく解説されていますが、まとめると下表のようになります
小数点 | 精度 |
---|---|
第 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