ESP32のWiFiおよびtzapu/WiFiManagerにおいて回復処理を試行錯誤した

# 1. はじめに
　WiFiのSSID/Keyを設定する手段としてtzapu/WiFiManager[^11]を利用しています。正常時は特に問題はなく快適です。ここでは、回復処理についていくつか試行錯誤した内容を記載しています。
 M5StickC, M5StickC Plus, M5Atomの以下の環境で試行錯誤しました。

- Arduino-IDE
> 1.8.19 (Widnows11)
- Boards Manager:
>M5Stack official version 2.0.4
- Library Manager:
>M5StickC 0.2.8
M5StickC Plus 0.0.8
M5Atom 0.1.0
WiFiManager 2.0.12-beta

WiFi: Wireless Fidelity
SSID: Service Set IDentifier
[^11]: [tzapu/WiFiManager](https://github.com/tzapu/WiFiManager)

# 2. WiFiManagerのインスタンス宣言
　WiFiManagerをnon blocking modeで使用する場合、WiFiManagerのインスタンスをグローバルで宣言する必要がある[^21]と説明にあります。WiFiManagerは、Configuration Portalを起動してこれが完了すると自身も終了する模様です。Configration Portalを起動することなくWiFi接続できた場合、WiFiManagerは動作しつづける模様です。これらのことは、WiFi接続状態が変化した際にWiFiManagerがメッセージを出すかどうかで確認できます。
　一方、blocking modeで使用する場合は、WiFiManagerのインスタンスを必ずしもグローバルで宣言する必要はありません。**setup()内でWiFiManagerを完結させたい場合、setup()内でインスタンス宣言する**ほうが望ましいと言えます。setup()が終了した時点でWiFiManagerを破棄することができます。

- non blocking mode: 処理を、他の処理と並行して行う
- blocking mode: 処理が終わるまで、次の処理に移らない
- Configuration Portal: 一時的にWiFiのアクセスポイントとなり、SSIS/Keyを設定するWebページを公開する。

[^21]: [tzapu/WiFiManager/README.md](https://github.com/tzapu/WiFiManager/blob/master/README.md)

# 3. 一時的なWiFi接続断からの回復

　以下は、arduino-esp32のWiFiType.h[^31]からwl_status_tの定義を抜き出しました。

``` WiFiType.h
typedef enum {
    WL_NO_SHIELD        = 255,   // for compatibility with WiFi Shield library
    WL_IDLE_STATUS      = 0,
    WL_NO_SSID_AVAIL    = 1,
    WL_SCAN_COMPLETED   = 2,
    WL_CONNECTED        = 3,
    WL_CONNECT_FAILED   = 4,
    WL_CONNECTION_LOST  = 5,
    WL_DISCONNECTED     = 6
} wl_status_t;
```

　WiFi接続状態でアクセスポイントである無線ルーターの電源を抜くと、以下の様にステータスが変化しました。

**WL_CONNECTED → WL_CONECTION_LOST → WL_NO_SSID_AVAIL**

　この状態で無線ルーターの電源を投入すると、以下の様にステータスが変化しWiFi接続が回復しました。

**WL_NO_SSID_AVAIL → WL_IDLE_STATUS → WL_CONNECTED**

　以上から、WL_NO_SSID_AVAIL状態については、SSID/Keyが有効である限り、WiFiの回復機能が有効です。

[^31]: [espressif/arduino-esp32/libraries/WiFi/src/WiFiType.h](https://github.com/espressif/arduino-esp32/blob/master/libraries/WiFi/src/WiFiType.h)

# 4. WiFi切断状態からの回復

　WL_DISCONNECTED状態からのWiFi接続には、WiFi.reconnect()を実行する必要があります。WiFi.reconnect()は、EEPROMに保存されているSSID/Keyを使用します。保存されていない場合、WiFi.reconnect()はfalseを返します。以下は、arduino-esp32のWiFiSTA.cpp[^41]からの抜粋です。reconnect()のなかでesp_wifi_disconnect()とesp_wifi_connect()が呼ばれています。各々、WiFi.disconnect()とWiFi.begin()で使用されている機能です。

``` WiFiSTA.cpp
/**
 * will force a disconnect and then start reconnecting to AP
 * @return true when successful
 */
bool WiFiSTAClass::reconnect()
{
    if(WiFi.getMode() & WIFI_MODE_STA) {
        if(esp_wifi_disconnect() == ESP_OK) {
            return esp_wifi_connect() == ESP_OK;
        }
    }
    return false;
}
```

EEPROM: Electrically Erasable Programmable Read-Only Memory

[^41]: [espressif/arduino-esp32/libraries/WiFi/src/WiFiSTA.cpp](https://github.com/espressif/arduino-esp32/blob/master/libraries/WiFi/src/WiFiSTA.cpp)

## WiFiManagerでのWiFi接続失敗
　WiFiManagerの一連の処理でWiFi接続ができない状況としては、経験的に以下が想定できます。

- EEPROMに保存されたSSID/Keyで、アクセスポイントの都合などで一時的に接続できなかった。このため、立ち上がってきたconfiguration portalを無視した
- EEPROMにSSID/Keyが保存されていないか、古いなどそのままでは接続できない。このためconfiguration portalでSSID/Keyを設定しようとしたが失敗した

　WiFiManagerでWiFi接続に失敗した場合、WL_DISCONNECTEDまたはWL_CONNECT_FAIL状態となります。WL_DISCONNECTED状態に対しては以下の回復処理が考えられます。

1. WiFi.reconnect()を実行し、EEPROMに保存されたSSID/Keyでの接続を試みる。
1. WiFi.reconnect()に失敗した場合、システムをリブートする。

　リブートによりシステムの不安定要素が減り、confuguration portalによるSSID/Keyの再設定も可能になります。WL_CONNECT_FAILの場合、詳細原因は未追及ながらリブートによる解消が期待できます。

## localtimeの影響
　現地時間localtimeは、システム時計の値に基づきますが、これが**2017年以降でないとWifi.reconnect()を実行してもWL_DISCONNECTED状態から抜け出せない**という不可解な現象に悩まされています。localtimeを取得するgetLocalTime()は、2016年以前の場合Falseを返す仕様であり関連性がありそうですが、詳細を突き詰めるには至っていません。
　対策としては以下を考えました。

1. システム起動時にRTCから現在時刻を設定する
2. RTCがないかまたはRTCが有効でない場合、システム起動時に2017年以降の仮の時刻を設定する

上記2.の場合、localtimeが正しくないですがgetLocalTime()はtrueを返します。localtimeの有効性は別途管理する必要があります。以下は、esp32-hal-time.c[^42]からのgetLocalTime()の抜粋です。

```esp32-hal-time.c
bool getLocalTime(struct tm * info, uint32_t ms)
{
    uint32_t start = millis();
    time_t now;
    while((millis()-start) <= ms) {
        time(&now);
        localtime_r(&now, info);
        if(info->tm_year > (2016 - 1900)){
            return true;
        }
        delay(10);
    }
    return false;
}
```

[^42]: [espressif/arduino-esp32/cores/esp32/esp32-hal-time.c](https://github.com/espressif/arduino-esp32/blob/master/cores/esp32/esp32-hal-time.c)


# 5. 実装例
　電波の届かないところにある電波時計の時刻を合わるため、NTPに同期した時刻に基づいて疑似的にJJY信号を発信する装置[^51]を作成しました。WiFi, NTP, およびRTCを合わせて[^52]試行錯誤して作成した処理の流れは以下です。

![wifi_ntp_rtc.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378410/423b46b5-a804-6e29-94a0-39c0dbb9eb95.jpeg)

1. RTCの時刻が有効の場合はシステムに設定する(localtime有効)。有効でない場合は仮の時刻(2017年以降)を設定する
1. WiFiに接続する。WiFiManagerをblocking modeで使用し、処理終了後は破棄する
1. WiFiの状態にかかわらず、NTPを起動する
1. 処理ループにおいて、WiFiがWL_CONNECT_FAILの場合、原因解消を期待してリブートを行う
1. WL_DISCONNECTEDの場合はWiFi.reconnectを行う。3回実行して回復しない場合はリブートする
1. NTPの時刻同期が成功(localtime有効)したらRTCを更新する
1. localtimeが有効な場合のみ、JJY信号を送信する

[^51]: [標準電波 JJY もどきを M5StickC / M5Atom の Ticker で生成する](https://qiita.com/BotanicFields/items/a78c80f947388caf0d36)
[^52]: [ESP32 において NTP の時刻同期を捕まえて RTC を更新する](https://qiita.com/BotanicFields/items/f1e28af5a63e4ccf7023)

# 6. おわりに
　不可解な現象についてソースを参照しながら原因を突き止めることが重要ですが、時間がかかってしまいます。間違いや改善など皆様からアドバイスを頂ければ幸いです。