知的好奇心 for IoT

IoT関連の知的好奇心を探求するブログです

ESP8266やESP32でDeep SleepをWiFiの電波が混んでいる場所で使うときは、接続や送信でタイムアウト処理を行いDeep Sleepを実行する前にWiFiの接続を切る必要がある

市川市の川沿いの住宅街から、神田のビル群や上野のオフィス・住宅密集地に引っ越して、長らくベランダで動かしていた温湿度計のバッテリーが想定より極端に短い時間しか持たない現象に悩まされていましたが、その対処方法がやっとわかりました。

原因の特定に時間がかかった理由

原因の特定に非常に時間がかかってしまったのは、次の様な要因が重なって原因を特定し難くなっていたからです。

解決に至ったのは、次の事を行った後でした。

  • ブレーカーが落ちない部屋に引っ越す
  • IoTクラウドサービスを変える
  • テストプログラムを数日間動かす
  • さまざまなマイコンを使う

 

ブレーカーが落ちない部屋に引っ越す

ブレーカーが落ちると当然ですがWiFiルーターも落ちます。WiFiルーターは仕組み上、電源が入っても直ぐにインターネットに繋がりません。

しかも、使っていたWiFiルーターが電源が落ちた後に5GHzの電波を出さなくなることがあり、WiFiルーターの再起動を実行する事態も頻発しました。

(最後は何をしても5GHzの電波を出さなくなり、買い換えることになりました。)

WiFiルーターが動作していない時にDeep Sleepから起きてしまうと、繋がるまで接続処理を繰り返すため、その間ずっと電力を消費してしまいます。

こんな状況では原因究明どころではなく、オンライン講座にも支障を来たしてしまうため、築60年程度と思われる神田のビル群にあったシェアハウスから、築20年程度の近代的な上野のシェアハウスに引っ越しました。

 

IoTクラウドサービスを変える

部屋の温湿度をBlynkを使ってスマホで見ていたため、ベランダの温湿度もBlynkを使って見ていました。

しかし、Blynkは常時電源が入っているデバイスを前提として作られていて、Deep Sleepを使った間欠動作を考慮していません。

そのため、動作環境をシンプルにして原因を特定し易くするため、IoTクラウドサービスをAmbientに変更しました。


テストプログラムを数日間動かす

テストプログラムを作るのにも紆余曲折がありましたが、最終的にはESP8266とESP32に対応した次のプログラムに落ち着きました。

#define ESP32                           // コメントにするとESP8266用
#ifdef ESP32
  #include <WiFi.h>                     // ESP32のWiFiライブラリ
#else
  #include <ESP8266WiFi.h>              // ESP8266のWiFiライブラリ
#endif
#include <EEPROM.h>                     // EEPROMライブラリ

#define WIFI_SSID     "WiFiSSID"      // 2.4GHzのみ
#define WIFI_PASS     "WiFiのパスワード"
#define CHANNEL_ID    "12345"            // AmbientのチャネルID
#define WRITE_KEY     "1234567890abcdef" // Abmientのライトキー
#define MESURE_PERIOD 1                 // 計測間隔(分単位)
#define EEPROM_SIZE   sizeof(time_t)    // time_tのバイト数

// グローバル変数
time_t now;                             // 現在の時間(UNIXタイム)
time_t last_time;                       // 前回の計測時間(UNIXタイム)

// EEPROMから時刻を読み込む
void readEEPROM() {
  for (int i=0; i<EEPROM_SIZE; i++) {
    last_time += EEPROM.read(i) << i*8;
  }
}

// 時刻をEEPROMに書き込む
void writeEEPROM() {
  char buf[EEPROM_SIZE];
  strncpy(buf, (char *)&last_time, EEPROM_SIZE);
  for (int i=0; i<EEPROM_SIZE; i++) {
    EEPROM.write(i, buf[i]);
  }
  EEPROM.commit();
}

// WiFiの接続処理
void initWiFi() {
  Serial.print("\nConnecting to " WIFI_SSID); // ログにWiFiSSIDを表示
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);     // WiFiに接続
  int i = 1;
  while (WiFi.status() != WL_CONNECTED) { // WiFiが繋がるまでループ
    if (i > 10) {                       // 10秒のタイムアウト
      Serial.println("\n========== WiFi connecting timeout! ==========\n");
      ESP.restart();                    // リセット
    }
    delay(1000);                        // 1秒待つ
    Serial.print(".");                  // ログに「.」を表示
    i++;
  }
  Serial.println("\nWiFi connected");   // ログに「WiFi connected」を表示
}

// 時間の初期化処理
void initTime() {
  configTzTime("JST-9", "ntp.nict.jp"); // NTPの初期化
  now = 0;
  while (now < 1546268400L) {           // 現在時刻が2019-01-01 00:00:00以降になるまでループ
    delay(500);                         // 500ミリ秒待つ
    now = time(nullptr);                // 現在時刻の取得
  }
}

// Ambientへの送信処理
void send2Ambient() {
  String strT = "25.5";                 // 温度(ダミー値)
  String strH = "62.2";                 // 湿度(ダミー値)
  String strP = "1001";                 // 気圧(ダミー値)
  WiFiClient client;                    // WiFi接続用オブジェクト

  client.setTimeout(10000);             // 10秒のコネクションタイムアウト
  if (client.connect("ambidata.io", 80)) { // Ambientのサーバーに接続
    // Ambientに送信するメッセージを作成
    char msg[128] = "{\"writeKey\":\"" WRITE_KEY "\"";
    strcat(msg, ",\"d1\":");
    strcat(msg, strT.c_str());
    strcat(msg, ",\"d2\":");
    strcat(msg, strH.c_str());
    strcat(msg, ",\"d3\":");
    strcat(msg, strP.c_str());
    strcat(msg, "}\r\n");
    int length = strlen(msg);

    // POSTリクエストの送信
    client.print("POST /api/v2/channels/" CHANNEL_ID "/data HTTP/1.1\r\n");
    client.print("Host: ambidata.io\r\n");
    client.print("Content-Length: ");
    client.print(length);
    client.print("\r\nContent-Type: application/json\r\n\r\n");
    client.print(msg);

    // レスポンスの取得
    int i = 0;
    while (!client.available()) {
      delay(100);
      if (++i > 100) {                  // 10秒のレスポンスタイムアウト
        client.stop();
        Serial.println("\n========== Ambient response timeout! ==========\n");
        WiFi.disconnect(true);          // WiFiの切断
        ESP.restart();                  // リセット
      }
    }
    Serial.println(client.readStringUntil('\n')); // レスポンスのログ出力
    client.stop();
  } else {
    Serial.println("\n========== Ambient connection timeout! ==========\n");
    WiFi.disconnect(true);              // WiFiの切断
    ESP.restart();                      // リセット
  }
}

void setup() {
#ifdef ESP32
  Serial.begin(115200);                 // シリアルモニタの通信速度を設定(ESP32)
#else 
  Serial.begin(74880);                  // シリアルモニタの通信速度を設定(ESP8266)
#endif
  delay(10);
  EEPROM.begin(EEPROM_SIZE);            // EEPROMの利用
  initWiFi();                           // WiFiの接続処理
  initTime();                           // 時間の初期化処理

  // ディープスリープから起きる時間の計算
  readEEPROM();                         // EEPROMから前回の時刻を読み込む
  if (last_time > now || now > last_time + MESURE_PERIOD * 2 * 60) { // EEPROMの初期化条件
    last_time = now - MESURE_PERIOD * 60; // 前回の時刻に今の時刻と計測間隔との差をセット(初期化)
  }
  time_t wake_time = last_time + MESURE_PERIOD * 60; // 起きる時刻の計算
  if (now + 1 >= wake_time) {           // 現在時刻が起きる時刻より大きいとき(1秒マージン)
    send2Ambient();                     // Ambientへの送信処理
    last_time = wake_time;              // 前回の時刻を更新
    writeEEPROM();                      // EEPROMに前回の時刻を書き込む
  } else {
    Serial.println("It's early.");      // 起きる時刻が早かったとき
  }

  // ディープスリープ処理
  now = time(nullptr);                  // 現在時刻の更新
  time_t  diff = last_time + MESURE_PERIOD * 60 - now;  // 次の起きる時刻と現在時刻の差を計算
  Serial.printf("Go to sleep for %d seconds.\n", diff); // ログに差を出力
  WiFi.disconnect(true);                // WiFiの切断
  ESP.deepSleep(diff * 1000 * 1000);    // Deep Sleepを実行(マイクロ秒)
}

void loop() {
}

プログラムの特徴は後で説明します。

 

さまざまなマイコンを使う

問題をややこしくしたのがESP8266から変えたESP32搭載のM5 ATOM Liteが不調だったことです。

シリアルポートが頻繁につながらなくなる現象が発生したり、WiFiの接続性が極端に悪くなったりするときがあり、使用しているPCがMacだったことも災いして状況がつかめなくなっていました。

以下に挙げる手持ちのマイコンでもテストプログラムを動かしてみることで、たまたま使用していたM5 ATOM Liteが不調なんだということが最終的にはわかりましたが、わかるまで何日もかかる事態となりました。

 

テストプログラムの特徴

接続や送信でタイムアウト処理を追加

このテストプログラムの1番の特徴はWiFiの接続処理やAmbientへの送信処理にタイムアウト処理を追加して、タイムアウトが発生した時にマイコンをリセットするようにしたことです。

具体的には次の3箇所にタイムアウト処理を追加しました。

  • WiFi接続時
  • Ambientのサーバー接続時
  • Ambientからのレスポンス受信時

タイムアウト時間は全て10秒で設定しています。

WiFiの接続であれば数秒、サーバーの接続やレスポンス受信は1秒もかからないで完了するのが普通で、ずっと待てば完了するというものではないため余裕を持った時間として10秒にしています。

 AmbientにはESP8266とESP32用にライブラリが用意されていますが、ライブラリを使用するとタイムアウト処理が行えないばかりか、データが正常に送られたかどうかが判断できないため使用していません。

サーバーへのPOSTリクエスト処理はライブラリのコードを参考にして書いています。

また、マイコンをリセットする命令にはESP8266とESP32で共通のESP.restert()を使っています。

 

Deep Sleep実行前にWiFi接続を切る処理を追加

Deep Sleep実行前と、Ambientへの送信処理でタイムアウトが発生してマイコンをリセットする前にWiFi.disconnect(true)を実行してWiFiの接続を切るようにしています。

この処理は地味に重要で、この処理を入れていないとWiFi接続に問題が起きだしたときに同じWiFiルーターに接続している他の機器を落とすことがあります。

具体的にはログに次の様なメッセージが物凄い数吐き出されます。

E (245) wifi:Set status to INIT

このメッセージ表示された時刻で部屋の温湿度を測っていたM5StickCがフリーズしていた他、ガイガーカウンターを動かしているESP8266がリセットしていました。

(両方ともBlynkを使用しているため、デバイスがオフラインやオンラインになった時刻がわかります。)

数回、この現象が起きたため関連があるものと思われます。

 

このエラーメッセージのことには言及していませんが、ESP32の開発元のespressifのドキュメントにも、次のようにDeep Sleepを実行する前にWiFiの接続を切るように指示する記述がありました。

WiFi/BT and sleep modes

In deep sleep and light sleep modes, wireless peripherals are powered down. Before entering deep
sleep or light sleep modes, applications must disable WiFi and BT using appropriate calls 
(esp_bluedroid_disable(), esp_bt_controller_disable(), esp_wifi_stop()). WiFi and BT
connections will not be maintained in deep sleep or light sleep, even if these functions are not
called.


前回実行時刻の保存用にEEPROMを使用

ESP32には変数の前にRTC_DATA_ATTRと書くとDeep Sleep後もその変数の値が保持される便利機能があります。

しかし、ESP8266にはこの機能がなく、ESP.restart()を実行したり電源を切ると消えてしまうため、ESP8266とESP32両方で使える不揮発性メモリのEEPROMに前回実行時刻を保存するようにしています。

 

このコードを書いている時に判明したのですが、なんとESP32はtime_tを32bit(4byte)で使っていて、2038年問題に対応できていないんです。

対してESP8266は64bit(8byte)だったんです!

ただ、これはmacOS Big Surで開発版の3.0.0-devを使っていたためと判明しました。

VMware Fusionで動かしているWindows 10でArduino core for ESP8266 2.7.4でコンパイルしたら32bit(4byte)でした。

time_tのサイズと使用したボードパッケージのバージョン

  • 32bit(4byte) : Arduino core for the ESP32 1.0.6
  • 32bit(4byte) : Arduino core for ESP8266 2.7.4
  • 64bit(8byte) : Arduino core for ESP8266 3.0.0-dev

 

ESP8266とESP32のソースコードの一元化

インクルードするヘッダーファイルがESP8266とESP32で違っているくらいで、他のコードはDeep Sleepを実行するESP.deepSleep()も含めて両方で動くことがわかったため、ソースコードを一元化しました。

ESP8266とは違い、ESP.xxxxx()の何が使えるかはESP32にドキュメントが存在しないためわからないのがESP32をArduinoで使うときの問題ですね。

(ESP32の公式ドキュメントはESP-IDF用のものしかなく、ESP32のGitHubにはドキュメントリンクがありません。)

 

そもそものWiFiの混雑による影響について

ビル群にあった神田のシェアハウスから上野のシェアハウスに移りましたが、周りはオフィスと住宅の密集地でWiFiの混み具合は神田といい勝負という感じです。

WiFiの混み具合はWiFi AnalyzerというAndroidアプリを使って調べています。

 

ここで紹介しているタイムアウト処理付きのプログラムは、そもそも一戸建ての住宅や田舎の環境では必要がないものです。

実際のべ1週間ぐらいテストプログラムを実行させた結果、タイムアウトが発生するのは平日の特定の時間に集中していることが判明しています。

日曜は全くタイムアウトが発生しないんです。

ただ、今回発生していた問題は非常に厄介でわかりずらいため、無用なトラブルに悩まされるのがいやであれば対応しておいた方がいいと思います。