Wio LTE & SORACOM で富士登山をトラッキング

Wio LTE と SORACOM を使って富士登山をトラッキングしてきました。
取り留めのない内容になってしまい申し訳ないです。コンテキスト毎に後日再編集します。

## 目的

富士登山は3回目です。
前回(2年前)は OpenBlocks IoT BX1 に SORACOM Air SIM を挿し、 TI SensorTag CC2541 で温湿度や気圧を取っていましたが、ブログを書くことを失念するという大失態をしまして、今度こそは！というのがきっかけです。

[ダッシュボード作成・共有サービスの SORACOM Lagoon](https://soracom.jp/services/lagoon) では、このように可視化されました。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/22754/bd5e5b69-873f-a9fb-df88-e7a8d5fef2ba.png)

# 準備編

## 構成

![fuji/structure](https://docs.google.com/drawings/d/e/2PACX-1vT3FausnErCfV27WmaF8vpK28hCs3-iHju29-DZ6bkBcZ5kOdC08jzJ2M7NPjnSYnrukL3B45gdlzOw/pub?w=1014&h=655)

* [Grove Barometer](http://wiki.seeedstudio.com/Grove-Barometer_Sensor-BME280/)
    * 取得データ: 温度、湿度、気圧
    * BME280 が使われてて比較的高精度。I2C で使える便利なやつ。
* [Grove GPS](http://wiki.seeedstudio.com/Grove-GPS/)
    * 取得データ: 緯度経度、高度、タイムスタンプ
    * 本当はみちびきが使えてバックアップ電池を搭載している [AE-GYSFDMAXB](http://akizukidenshi.com/catalog/g/gK-09991/) にしたかったんだけど、5V なんで諦めた
        * Wio LTE が 3.3V。[回路図](http://akizukidenshi.com/download/ds/akizuki/AE-GPS_manual_d1.pdf)を見ると3.3V LDO 挟んで3.3Vにしてるから、その後に給電できるようにすればいいんだけど面倒だった。
* [Grove Gas O2](http://wiki.seeedstudio.com/Grove-Gas_Sensor-O2/)
    * [とあるツイート](#tweet)をきっかけに探してみたらあった！が、今回いちばん厄介だった！
* [Grove Button](http://wiki.seeedstudio.com/Grove-Button/)
    * 基本動作は3分に一回センシング＆送信としていますが、デバッグ用にボタンでも送信できるようにするため取り付け。
* [Wio LTE用ケース SORACOM-UGロゴ入りスペシャルバージョン](https://make.dmm.com/item/927565/)
    * 本当は屋内用らしいですが、屋外で使ってしまいました。
* [SORACOM Air for セルラー](https://soracom.jp/services/air/cellular)
    * IoT 向け 3G/LTE データ通信サービス (要するに SIM)
* [SORACOM Harvest](https://soracom.jp/services/harvest)
    * データ収集・蓄積サービス
* [SORACOM Lagoon](https://soracom.jp/services/lagoon)
    * ダッシュボード作成・共有サービス

<h3 id="tweet">とあるツイート</h3>

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">標高を上げていくなら酸素濃度とか取れたら面白いかなと思ったんですが、センサーが無さそうでした…。</p>&mdash; Yuki (@yukisama00) <a href="https://twitter.com/yukisama00/status/1027197151653195776?ref_src=twsrc%5Etfw">2018年8月8日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## 製作

### パッケージング

最終的には以下のようになりました。

![fuji/package](https://docs.google.com/drawings/d/e/2PACX-1vSJ2IovoT_qXHzKOQ8tsjvIcHzsOlxrgiY3SDBmOsrW0WWcNUWVGS-BXaLl1_kbpOtZwfWeirsQVFFq/pub?w=664&h=501)

O2 Gas センサーの取り付けに苦戦しました。
センサー部はかなり大きめの部品がついています。一方ケース側が想定しているのは横に寝かせて取り付ける形であり、横にはできないため平置きする必要がありました。平置にするとしても底面にO2センサー本体を取り付けた後の端子が突き出すことで取り付けが不可能でした。

対策はリューターでケース底面に穴を掘ったうえ、Wio LTE を支える柱部分も一部削り、平置きできるようにしました。

![fuji/mount O2 gas](https://docs.google.com/drawings/d/e/2PACX-1vS09xKTVt-Mf6oO23tOrYlxc25gJ28FfOBvF23H55qAC_DSRS9eFOnyiYGxNKSxv0Is_Us7t3-Y6Y4v/pub?w=566&h=454)

### コード

コード全体は [Gist](https://gist.github.com/ma2shita/8089f4e418e322ff47a1454b3bb53ceb) に掲載しました。(MITライセンスです)
ここではポイントを解説します。

#### データフォーマット

SORACOM Harvest には JSON で送信しています。

```json
{
  "perfome_counter": 132,
  "event_src": "loop",
  "tmpr_c": 29.82999992,
  "humd_percent": 7,
  "pres_hpa": 646.73,
  "geo_src": "GPS",
  "lat": 35.360715,
  "lng": 138.7273217,
  "alt_m": 3772.2,
  "timestamp": 1534566736,
  "o2_percent": 18.04949951
}
```

#### payload の JSON 化に ArduinoJson を使用

JSON 文字列の生成に `sprintf()` はちょっとスマートでないため [ArduinoJson](https://github.com/bblanchon/ArduinoJson) を利用しています。
※ 5系を使っています

ArduinoJson.h を `#include` する前に [`ARDUINOJSON_USE_DOUBLE`](https://arduinojson.org/v5/api/config/use_double/) を `1` にしています。
デフォルトは 0 で、この状態だと小数部が 1~3 桁で落とされます。温度や酸素濃度ならこの程度でもいいのですが GPS の座標データの桁が落ちるのは困るためです。この定数を 1 にすることで高精度な桁を入れることが可能になるので宣言しています。

```c++
#define ARDUINOJSON_USE_DOUBLE 1 /* for high precision float */
#include <ArduinoJson.h> /* 5.13.2 (don't use 6.x) */
StaticJsonBuffer<1024> jsonBuffer;
```

宣言が済んでしまえば直感的な I/F で JSON を組み立てることができます。

```c++
void main_line(const char *event_src) {
  JsonObject& json = jsonBuffer.createObject();
  json["event_src"] = event_src;
```

組み立てた JSON を文字列にするのは `printTo()` メソッドです。これで `payload` には JSON 文字列が格納されます。
このままですとグローバル変数として宣言した `jsonBuffer` を使いまわすことになってしまうため `clear()` もしておくとトラブル回避できます。

```c++
  char payload[1024];
  json.printTo(payload, sizeof(payload));
  jsonBuffer.clear();
  // SerialUSB.println(payload);
```

#### SORACOM への送信部分を「毎度接続＆切断」

モデムは消費電力が大きい部品です。そのため、モデムを如何に寝かせておくかが省電力化の第一歩となります。
今回はデータ送信毎に毎度 ON/OFF をするようにしました。これによる消費電力への影響は後述しています。

```c++
void send_to_soracom_with_ondemand(const char *payload) {
  const char *host = "http://harvest.soracom.io";
  Wio.PowerSupplyLTE(true);
  delay(500);
  if (!Wio.TurnOnOrReset()) { SerialUSB.println("TurnOnOrReset Error"); goto comm_error; } 
  if (!Wio.Activate("soracom.io", "sora", "sora")) { SerialUSB.println("Activate Error"); goto comm_error; }
  int status;
  if (!Wio.HttpPost(host, payload, &status)) { SerialUSB.println("### ERROR! ###"); goto comm_error; }
  SerialUSB.println(status);
  Wio.Deactivate();
comm_error:
  Wio.PowerSupplyLTE(false);
}
```

送信の成否を問わず `Wio.PowerSupplyLTE(false)` として必ずモデムの電源を OFF しています。

#### TinyGPS++ でのタイムスタンプ取得

[TinyGPS++](http://arduiniana.org/libraries/tinygpsplus/) は UART から出力された GPS 文字列をパースするのに強力です。
TinyGPS++ で解析された日時情報を UNIX time にするためには `struct tm` と `mktime()` を利用するのが手っ取り早いです。

```c++
  struct tm t;
  t.tm_year = TinyGPS.date.year() - 1900;
  t.tm_mon  = TinyGPS.date.month() - 1;
  t.tm_mday = TinyGPS.date.day();
  t.tm_hour = TinyGPS.time.hour();
  t.tm_min  = TinyGPS.time.minute();
  t.tm_sec  = TinyGPS.time.second();
  t.tm_isdst= -1;
  time_t epoch = mktime(&t);
```

#### Wio LTE における `analogRead()`

Wio LTE における analogRead() では注意点が２つあります

**1 つめ; `pinMode()` を使用する**

Wio LTE では `analogRead()` したい PIN には `pinMode()` を設定する必要があります。
その際、第 2 引数には `INPUT_ANALOG` を指定します。

```c++
pinMode(_O2_SENSOR_PIN, INPUT_ANALOG);
```

これで `_O2_SENSOR_PIN` を `analogRead()` で読むことができるようになります。

ちなみに Arduino UNO R3 では `analogRead()` するピンに対してのセットアップは不要です。(というか `pinMode()` を設定してしまうと digital PIN になってしまう)

**2 つめ; 12bit-ADC と電圧**

Wio LTE は 12bit (=4096) です。電圧は 3.3V 固定になります。
そのため、例えば `map()` を使って電圧(mV)を求める場合は下記のようになります。

```c++
long mV = map(analogRead(_O2_SENSOR_PIN), 0, 4095, 0, 3300);
```

ちなみに Arduino UNO R3 (5V) の場合、ADC (A/D 変換) の分解能は 10bit (=1024) です。
電圧が 5V のセンサーの場合は `map(analogRead(PIN), 0, 1023, 0, 5000)` となります。

#### `attachInterrupt()` による割込み処理 (の実装ミス)

3分に1度のセンシング＆送信とした実装であるため、動作確認用にボタンによる割込みでセンシング＆送信を実行する実装を試みました。

具体的には `attachInterrupt()` で `pin_interrupt()` 登録し、ボタンの RISING ステート時に `main_line()` を実行する実装ですが、これは完全に実装ミスでした。

`main_line()` 内において [`delay()` 等の動作しない関数](http://www.musashinodenpa.com/arduino/ref/index.php?f=0&pos=3069)を使用していたため、動作が不定となったことが原因です。
（不定＝動いたり、動かなかったり）

登山当日の最終チェックでボタンによるセンシング＆送信ができないことが発覚したため、出発直前に `pin_interrupt()` 内の `main_line()` 呼び出し部をコメントアウトにて機能を OFF にして回避しました。

正しい実装は `pin_interrupt()` 内で global 変数の `loop_counter` の操作のみ行い、 `main_line()` の実行自体は `loop()` に任せるべきでした。

**改修ポイント**

```diff
  void pin_interrupt() {
-   main_line("pin_interrupt");
-   loop_counter = 0;
+   loop_counter = _EXEC_INTERVAL; /* 値を溢れさせることにより (続く) */
  }
```

```c++
void loop() {
  if (loop_counter > _EXEC_INTERVAL - 1) { /* (続き) 強制的にここが true になる */
    main_line("loop");
    loop_counter = 0;
  }
  loop_counter++;
  delay(_LOOP_INTERVAL);
}
```

### 消費電力

計測には [CT-2](https://qiita.com/ma2shita/items/cb024692995178a6295e) を使用しました。

3分に1度のセンシング＆データ送信とし、毎度 モデムON→ネットワーク接続→送信→モデムOFF としています。( `void send_to_soracom_with_ondemand(const char *payload)` )

データ送信時は最大で 300mA となりますが、送信に要する時間は 15秒程度です。
それ以外の待機時間においては平均で 90mAh くらい、トータルとしても 95mAh には達しない消費電力量でした。

6200mAh なモバイルバッテリーで実測したところ、約 70時間でしたので合ってるかと思います。

### センサーの計測値を検証可能に

BME280 における温湿度は検定済み機器で検証できるし、そこから計測された気圧も「まぁあってるかなー」と推測できそう。GPS も「今の位置」をレンダリングすれば検証できるんだけど、問題は O2 濃度。

空気中に 約21% 程含まれているはずの酸素ですが、今回のセンサーでは 18.5% 前後を計測しており、 [酸素濃度と人体への影響](http://mh.rgr.jp/memo/mv0124.htm) によると、"安全の限界＝連続換気が必要" という値だそうです。
でもこれ、私の自宅での計測値です。ウチってそんなにヤバいのか。。。？とも思いましたが、なんせ検証する方法が手元に無いため、校正のしようがありませんでした。

出力電圧をオシロで計測すると 1.82V 前後。 [サンプルコード](http://wiki.seeedstudio.com/Grove-Gas_Sensor-O2/)による算出は合ってるっぽいのです。
[O2センサー自体のデータシート](https://github.com/SeeedDocument/Grove_Gas_Sensor_O2/raw/master/resources/ME2-O2-D20%200-25%25%20Manual%20%28ver1.2%29.pdf) はありますが、Grove Gas O2 sensorとしてのデータシートが見当たらず、算出式が合っているのかの検証も厳しい状態でした。

ここからの学びは、センサーは別の手法で検証可能(校正可能)である事が重要という事です。誤ったデータを出発点としたデータ活用はしばし悲劇を生みますが、センサーも同様です。
ただしここで諦めるわけではなく、例えば「普段の状況に対しての差分」として相対評価を行うといった事には使えそうですが、やはり **実地で計測したデータを基に活用する** ことは避けることができませんので、注意したいものです。

# 実運用

登山直後はリュックサックのサイドポケットに入れておいたのですが、直射日光を拾ってしまい温度が高めに計測されています。その後、日が当たらないポケットに移動したのですが、リュックサック自体の温度が上昇し、その温度を拾ってしまい、気温として取得することができませんでした。

![fuji / deploy](https://docs.google.com/drawings/d/e/2PACX-1vQ4pYQcjFiGLCiAn5Xq5nWc1esqUudh86u71e49IwmzNJCuziAJ-m6-UBLq5-ZVnlyFnt9nN_UuHLh7/pub?w=752&h=529)

その他のデータは概ね良好な計測結果を出してくれたと思います。

# SORACOM Harvest 内のデータ

[SORACOM Harvest](https://soracom.jp/services/harvest) は標準ですとデータの保存期間が40日となっています。（8/22現在、問い合わせベースですが延長できます)

そのためデータを最終的に永続化する必要がありますが、ここは [soracom-cli](https://dev.soracom.io/jp/tools/soracom_cli) にてデータを取得しました

```console
$ soracom data get --imsi ${IMSI} \
  --from $(date -d '2018/8/18 5:20:00 +900' '+%s000') \
  --limit $(echo 60/3*10 | bc)
```

GNU coreutils の date コマンドは日付を UNIX epoch 形式に変換できるので、それを利用して *from* の引数にしています。 from は [ミリ秒精度の指定](https://dev.soracom.io/jp/docs/api/#!/DataEntry/getDataEntries) です。
取得件数の計算は bc コマンドです。3分に1度の取得を10時間分という意味ですね。

例えば Amazon S3 に転送するなら[AWS CLI とパイプでつなげる](https://dev.classmethod.jp/cloud/aws/aws-cli-s3-streaming/)という方法もあります。

```console
$ soracom data get --imsi ${IMSI} \
  --from $(date -d '2018/8/18 5:20:00 +900' '+%s000') \
  --limit $(echo 60/3*10 | bc) | \
  aws s3 cp - s3://yout-bucket/fuji2018-sensing.json
```

今回取得したデータは [Google Drive で公開](https://drive.google.com/file/d/1kts8pDlt9VcWzMO0CdyM_tIb7b0_J6cp/view?usp=sharing) しています。

最近リリースされた [SORACOM Harvest の Publish API](https://blog.soracom.jp/blog/2018/07/04/inventory-update/) を使えばデータの再現も可能です。

# 費用

| 物品 | 金額 (円) |
|------|------:|
| [Wio LTE JP Version](https://soracom.jp/products/wio_lte/) | 9800 |
| [Grove Barometer](https://www.chip1stop.com/view/dispDetail/DispDetail?partId=SEED-0000478) | 1880 | 
| [Grove GPS](https://www.switch-science.com/catalog/1179/) | 4179 |
| [Grove Gas O2 Sensor](https://www.robotshop.com/jp/ja/grove-o-gas-sensor.html) | 6876 |
| [Grove Button](https://www.switch-science.com/catalog/801/) | 280 |
| [Wio LTE ケース SORACOM UG スペシャル edition](https://make.dmm.com/item/927565/) | 6696 |
| [SORACOM Air for セルラー 日本向け SIM (nanoサイズ)](https://www.amazon.co.jp/dp/B07C3JRR3X) | 1260 |

計: 30971 円
（いくつかの部品や SORACOM Air SIM は [Grove IoT スターターキット for SORACOM](https://soracom.jp/products/kit/grovestarter_kit_lte/) の中身を流用しています）

これに加えて SORACOM 費用が下記の通りでした。

| サービス | 金額 |
|----------|----:|
| SORACOM Air for セルラー 基本料 | 20 円 (2日分) |
| SORACOM Air for セルラー データ通信 | 約 0.043 円※ |
| [SORACOM Harvest](https://soracom.jp/services/harvest/) | 10 円 (2日分) |
| [SORACOM Lagoon](https://soracom.jp/services/lagoon/) | 980 円/月 (ただし利用開始月とその翌月は無料) |

※ データ通信の内訳: 速度クラス s1.fast

| カテゴリ | 通信量 (bytes) | 通信料 (円 (少数第4位 四捨五入) |
|---------|--------:|-------:|
| Upload | 53,556 | 0.014 |
| Download | 32,769 | 0.029 |

※ すべての金額は税抜き、8/15現在です

# まとめ

まだ書けていないこと

* コードの変遷
    * いきなり最終系に行ったわけではなく、それの試行錯誤の歴史
* SORACOM Lagoon でのダッシュボードの作り方と共有について
* センサーの値に対する考察
* ゴールベース志向の PoC

## あとがき

疲れた。

EoT