LTE-M Shield for Arduino の長期稼働安定化・その 2 〜ハードウェアリセットによる再起動〜

# はじめに
LTE-M Shield for Arduino を実際に利用していると、現場などへの設置に伴って長期的かつ安定的に動作させたいというモチベーションが出てきます。この観点はサンプルスケッチでも考慮されており、[もとの記事](https://zenn.dev/j3tm0t0/articles/d973184ffd4b40)では

- 通信モジュールのリセット操作を `setup()` 関数実行時に実行する
- 定期的にプログラムカウンタを `0` にしてリセット（ `setup()` 関数の再実行による Arduino 本体と通信モジュールの定期的な再起動）

といった操作によって長期稼働の安定化を図っていました。

ただ、プログラムの途中でエラーが発生すると定期的なリセット動作の前にフリーズ状態になってしまい、手動でのリセットを余儀なくされるケースがまれに発生することがあります。
事象が起きないと検証が難しく、また発生するタイミングもまちまちのため原因ははっきりしませんが、Arduino 本体の異常、もしくは通信モジュール側の異常（ハングアップ？）のどちらの可能性も想定されます。このような事象はできるだけ回避したり原因を究明したいところではありますが、一方で複数の機器やシステムと接続する以上、局所的な復旧よりもシステム全体を再起動したほうがスピーディに解決するケースは少なくありません。

ここでは、サンプルスケッチである [send_uptime_with_soracom.ino](https://github.com/soracom-labs/arduino-dragino-unified/blob/main/send_uptime_with_soracom/send_uptime_with_soracom.ino) をベースに、[ウォッチドッグタイマ](https://ja.wikipedia.org/wiki/%E3%82%A6%E3%82%A9%E3%83%83%E3%83%81%E3%83%89%E3%83%83%E3%82%B0%E3%82%BF%E3%82%A4%E3%83%9E%E3%83%BC)を利用して Arduino 本体をリセットすることでより安定的にデータ送信を実施する方法を考えます。

# 前提と注意
- この記事でいう長期稼働の安定化、安定的な動作とは「システム停止によるデータ送信の途絶を最小限にする」ことを指します。Arduino 本体や通信モジュールがリセット等の操作なく長期間動作するための方法ではありません。
- 紹介しているウォッチドッグタイマ関連操作の挿入タイミングは例示ですので、リセットまでの時間やリセットの挿入タイミング等は要件に応じて適宜変更してください。
    - たとえば設定段階でのフリーズに備える場合は `setup()` 関数内からウォッチドッグタイマを開始することも検討してください。
- ターゲットボードは Arduino UNO (ATmega328P）です。これ以外のターゲットボードやプロセッサではライブラリ関数や命令体系が異なるため、そのままでは利用できません。
    - ただし、本質的に必要な操作は共通ですので、ご利用のターゲットボードのリファレンス等を参考に実装を検討してください。

# スケッチ
スケッチ全文は[こちら](https://gist.github.com/sayacom/037261a348d6dce8451ff03a070d321e)。ベースとなる send_uptime_with_soracom.ino は[こちら](https://github.com/soracom-labs/arduino-dragino-unified/blob/main/send_uptime_with_soracom/send_uptime_with_soracom.ino)から確認いただけます。

## ウォッチドッグタイマ関連の関数
まず、Arduino (AVR) のウォッチドッグタイマを利用するためのライブラリをインポートします。

```cpp
#include <avr/wdt.h>
```

次に `loop()` 関数の先頭においてウォッチドッグタイマの設定をします。`wdt_reset()` はタイマのリセット、`wdt_enable(WDTO_8S)`はウォッチドッグタイマを開始し8秒後に発動する状態としています。開始までの時間は、秒単位ではこの他にも 1・2・４・８秒の定義があります。
いくつかのタイミングで `wdt_reset()` を呼んでいますが、このタイミングから指定時間タイマのリセット（または停止）操作がない場合はフリーズが発生したものとしてウォッチドッグタイマによるハードウェアリセットが発動するしくみになっています。処理に時間を要したり、フリーズが発生するリスクの高い処理の前後で挿入するのがポイントです。

```cpp
void loop() {
  // Setup WDT(Watch Dog Timer)
  wdt_reset();
  wdt_enable(WDTO_8S);
  ERROR_CHECK();

  long uptime_sec = millis() / 1000;

  char payload[120];
  sprintf_P(payload, PSTR("{\"uptime\":%lu}"), uptime_sec);
  CONSOLE.println(payload);

  /* connect */
  wdt_reset();
  if (!ctx.connect(ENDPOINT, 80)) {
    CONSOLE.println(F("Failed to connect endpoint."));
    ERROR_COUNT++;
    return;
  }
  ...
```

通信が完了し次の通信までの待ち状態に入る前に `wdt_disable()` を呼び、ウォッチドッグタイマを停止しています。

```cpp
  wdt_disable();
  delay(INTERVAL_MS);
```

## エラー処理
ここではプログラム的に検知可能な異常時に意図的に無限ループに突入してウォッチドッグタイマによるリセットを発動させています。
たとえば、プログラム的になんらかの処理が失敗したときに `ERROR_COUNT` をインクリメントしておいて一定回数以上のエラーが発生した場合にリセットさせる、あるいは起動から一定時間経過後にリセットをさせる…といった処理を記述しています。

```cpp
static int ERROR_COUNT = 0;
void ERROR_CHECK() {
  if (ERROR_COUNT > ERROR_COUNT_THRESHOLD) {
    CONSOLE.println("Error occurred, to be execute reset...");
    while(1); // Expect overflow WDT counter
  }
  else if (millis() > RESET_DURATION) {
    CONSOLE.println("Expired start-up time, to be execute reset...");
    while(1); // Expect overflow WDT counter
  }
}
```

# 実際の動作
実際に異常な状況を作り出してウォッチドッグタイマによる再起動が発動するかを確認します。

## 圏外などにより通信できなくなった場合
わざと LTE-M Shield for Arduino のアンテナをはずし、接続できない状態にします。この場合は `connect()` 関数の明示的なエラーを起因として `ERROR_CHECK()` 処理に入り、リセットが発生します。

```shell
15:18:05.255 -> Connecting...
15:18:07.324 -> Failed to connect endpoint.
15:18:07.359 -> {"uptime":470}
15:18:07.436 -> Connecting...
15:18:09.431 -> Failed to connect endpoint.
15:18:09.486 -> Error occurred, to be execute reset...
15:18:18.006 -> 
15:18:18.006 -> Welcome to send_uptime_with_soracom_for_long_time 1.0
```

## 通信モジュールがハングアップした場合
通信中に LTE-M Shield for Arduino のリセットボタンを押下すると通信モジュールがハングアップし、タイムアウトもしない状況になる場合があります。この例では接続処理開始からおよそ8秒後にリセットが発生していることが分かります。

```shell
15:20:49.637 -> {"uptime":87}
15:20:49.671 -> Connecting...
15:20:58.255 -> 
15:20:58.255 -> Welcome to send_uptime_with_soracom_for_long_time 1.0
```

# （おまけ）フリーズしたら BG96 のリセット端子を操作すればいいんじゃないの？
BG96 のハードウェアマニュアルによると、RESET 端子は「PWRKEY 端子（LTE-M Shield for Arduino 上の ON/OFF スイッチに相当）の操作または `AT+QPOWD` （AT コマンドによるシャットダウン）コマンドに応答しない場合に操作すること（意訳）」という記載がありました。
リセット端子は D15 ピンとシールド上の RST スイッチに接続されていますが、上記のとおり通信中に操作すると通信モジュールが応答しなくなることがあるため、極力操作しないほうがいいようです。

なお、ソフトウェア的な通信モジュールの再起動は `AT+CFUN=1,1` で実行できます。[TinyGSM](https://github.com/vshymanskyy/TinyGSM) の場合は `TinyGSM#restart()` 関数内に内包される形で実装されており、上記スケッチでは `setup()` 関数内で実行されています。

# 参考リンク
- [avr-libc の wdt.h マニュアル](https://www.nongnu.org/avr-libc/user-manual/group__avr__watchdog.html)
- [LTE-M Shield for Arduino の回路図](https://github.com/dragino/NB-IoT/blob/master/NB-IoT%20Shield/hardware/IoT%20Shield/v1.2/IoT%20Shield.v1.2%20SCH.pdf)
- [Quectel BG96](https://www.quectel.com/product/lpwa-bg96-cat-m1-nb1-egprs)
  - ハードウェアマニュアルを含む AT コマンドはページ下部からダウンロードできます（要会員登録）。