【ESP8266】長時間deepsleepするとモバイルバッテリーがOFFになっちゃう件をどうにかする

#はじめに
ESP8266のdeepsleepモードは優秀すぎて、モバイルバッテリーで運用しようとすると消費電流が少なすぎて負荷無しと判断され、電源が切れちゃう問題があります。

かといって、負荷無しと判断されないよう、単にdeepsleepの時間を短縮すると、無駄にバッテリーが浪費されてしまいます。

この問題をお手軽に解決する方法を考えてみます。

#ここでの解決策
モバイルバッテリーを使うこと、余計なハードウェアを使用しないこと、を条件とすると、deepsleepの時間を短縮することは避けられません。

しかし、deepsleepから目覚めた後の処理を工夫すれば、電力の浪費は避けられそうです。

具体的には、目覚めた後に毎回処理をせず、何回かに1回だけ処理をすればよさそうです。

普通に毎回送信する場合の消費電力のイメージはこうです。

![power1.png](https://qiita-image-store.s3.amazonaws.com/0/104360/aaed748b-5cb7-edaa-9f7d-5754ca1524e5.png)

何回かに1回だけ送信処理する場合の消費電力はこうなります。

![power2.png](https://qiita-image-store.s3.amazonaws.com/0/104360/608df3d9-2122-ec99-94da-5f847175ae20.png)

WiFiアクセスの処理をする場合はネゴから通信終了まで数秒間フルパワーで電気食っちゃいますが、起きてすぐ寝るだけならそんなに食わないという戦略です。

#目覚めたのは何回目？
ESP8266の内蔵メモリは、deepsleepの時に電源を落とされてしまうので、前回の値を覚えておくことができません。つまり、目覚めたのが何回目かを記憶するために、普通の変数を使うことができません。

しかし、内蔵RTCモジュールに512バイトのメモリがあり、そこはdeepsleep中でも維持されます。

RTCメモリへの書き込みは
`ESP.rtcUserMemoryWrite(offset, &data, sizeof(data))`
で、
RTCメモリからの読込は
`ESP.rtcUserMemoryRead(offset, &data, sizeof(data))`
です。

ここに目覚めた回数のカウンタ値を入れます。

#deepsleepからの目覚めか、電源入ったのか
deepsleepからの復帰時も、電源投入時と同じく`setup()`から始まります。
電源投入時のRTCメモリの内容は不定なので、RTCメモリからは「一番最初の起動」を知ることは出来ません。そのため、RTCメモリ以外から起動理由を調べる必要があります。

起動理由は `ESP.getResetReason()` で調べることができます。

戻り値は文字列で、deepsleepからの復帰時は `"Deep-Sleep Wake"` が返ってきます。

#ソースコード例
・deepsleepは100秒毎
・メイン処理は6回ごと(6*100=600で、10分毎)

ここでのメインの処理は http://192.168.0.1/ にアクセスするだけです。
実用する場合は、センサを読んでどっかに投げるような処理になるでしょう。

```cpp:deepsleeptest.ino
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>

// ディープスリープ時間(マイクロ秒)
// モバイルバッテリーがOFFになる時間よりちょい短めに設定する
const uint32_t DEEP_SLEEP_uS = 1000*1000*100;

// 何回起きたら実際に送信処理するか、の値
const uint32_t SEND_INTERVAL = 6;

const char* ssid     = "your-ssid";
const char* password = "your-password";

// メインのお仕事
void mainJob() {
  Serial.print("[WiFi] connecting");
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    // WiFiアクセスポイントが死んでると、その間バッテリを浪費してしまうので、
    // リトライオーバーで諦める処理を入れたほうがいいかも
  }
  Serial.println(".");

  HTTPClient http;
  Serial.println("[HTTP] begin...");
  http.begin("http://192.168.0.1/");

  Serial.println("[HTTP] GET...");
  // start connection and send HTTP header
  int httpCode = http.GET();

  // httpCode will be negative on error
  if(httpCode > 0) {
    // HTTP header has been send and Server response header has been handled
    Serial.printf("[HTTP] GET... code: %d\r\n", httpCode);

    // file found at server
    if(httpCode == HTTP_CODE_OK) {
        String payload = http.getString();
        Serial.println(payload);
    }
  } else {
    Serial.printf("[HTTP] GET... failed, error: %s\r\n", http.errorToString(httpCode).c_str());
  }

  http.end();
}

void setup() {
  Serial.begin(74880);  // 起動時に合わせて74880bpsにする
  
  String resetReason = ESP.getResetReason();
  Serial.println(resetReason);
  
  uint32_t wakeupCounter;
  if ( resetReason == "Deep-Sleep Wake" ) {
    // deepsleepからの復帰だったら
    // RTCメモリからwakeupCounterを読み出して、＋１
    ESP.rtcUserMemoryRead(0, &wakeupCounter, sizeof(wakeupCounter));
    wakeupCounter++;
  }
  else {
    // 電源ONからの起動だったら、wakeupCounterを初期化（1発目は即実行）
    wakeupCounter = SEND_INTERVAL;
  }
  Serial.print("wakeupCounter : ");
  Serial.println(wakeupCounter);

  if ( SEND_INTERVAL <= wakeupCounter ) {
    // カウンタが規定値以上 → カウンタクリアして、仕事する
    wakeupCounter=0;
    mainJob();
  }
  else {
    // モバイルバッテリーがOFFにならないようにするために起きただけ
    // （機種によっては、電流を流したフラグを確実に立てさせるため、delay()を入れたほうがいいかも？）
  }

  // RTCメモリにwakeupCounterを書き込む
  ESP.rtcUserMemoryWrite(0, &wakeupCounter, sizeof(wakeupCounter));

  // 寝る
  ESP.deepSleep(DEEP_SLEEP_uS, WAKE_RF_DEFAULT);
}

void loop() {
  // deepSleep呼んだ後も少し実行されちゃうけど、
  // ここの空ループでやり過ごす。
}
```

#おわりに

IoTは電源確保がネックになりがちです。

この記事の方法は最善ではないかもしれませんが、お手軽なので電子工作の入口の作品には使いやすいと思います。