市川市の川沿いの住宅街から、神田のビル群や上野のオフィス・住宅密集地に引っ越して、長らくベランダで動かしていた温湿度計のバッテリーが想定より極端に短い時間しか持たない現象に悩まされていましたが、その対処方法がやっとわかりました。
原因の特定に時間がかかった理由
原因の特定に非常に時間がかかってしまったのは、次の様な要因が重なって原因を特定し難くなっていたからです。
解決に至ったのは、次の事を行った後でした。
ブレーカーが落ちない部屋に引っ越す
ブレーカーが落ちると当然ですが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 "WiFiのSSID" // 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); // ログにWiFiのSSIDを表示 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が不調なんだということが最終的にはわかりましたが、わかるまで何日もかかる事態となりました。
- ESP32-DevKitC
- M5StickC
- M5 ATOM Matrix
- M5 ATOM Lite(他に持っていたもの)
- AE-ESP-WROOM02-DEV (ESP8266)
- Wio Node (ESP8266)
テストプログラムの特徴
接続や送信でタイムアウト処理を追加
このテストプログラムの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週間ぐらいテストプログラムを実行させた結果、タイムアウトが発生するのは平日の特定の時間に集中していることが判明しています。
日曜は全くタイムアウトが発生しないんです。
ただ、今回発生していた問題は非常に厄介でわかりずらいため、無用なトラブルに悩まされるのがいやであれば対応しておいた方がいいと思います。