ESP8266を電池で使う

1時間ごとにDeepSleepから復帰する


ESP8266には3つのスリープモードがあります。
スリープモードの詳細はこちらで 紹介されています。
DeepSleep中は微弱電流で動き続けますが、DeepSleep中のタイマーは1時間(60分)で2分程度の誤差(2/60=3.33%) が発生します。
DeepSleep中のタイマーの精度はこ ちらで紹介していま す。
従って、DeepSleepのSleep時間に3600秒を指定しても、1時間後に復帰することができません。
そこで、NTPを併用して毎時0分にDeepSleepから復帰する方法を紹介します。
正確には、毎時59分(ごろ)にDeepSleepから一旦復帰し、NTPで残り時間を確認してから再度DeepSleepします。

こんな感じで動作します。時間は仮の時間です。
@12時10分に電源ON。12時59分10秒までSleep
ASleepから復帰。NTPで時間を確認。タイマー誤差により12時57分10秒なので、12時59分10秒までSleep
BSleepから復帰。NTPで時間を確認。タイマー誤差により12時59分05秒なので、13時00分00秒までSleep
CSleepから復帰。MQTTでデータを送信。13時59分10秒までSleep
D以降、Aから繰り返し

RTCのユーザエリアは以下の用途で使っています。
・rtcData.clientMode:0ならば次回起動時はNTPクライアント、1ならば次回起動時にはMQTTクライアントを実行する。
・rtcData.nextHour:次にMQTTを実行する時間
・rtcData.ntpCount:NTP接続した回数
・rtcData.sleepCount:DeepSleepした回数
・rtcData.sleepSec[10]:DeepSleepした秒数

使用したスケッチは以下のスケッチです。
MQTTサーバーはローカルなサーバーを使いました。
/*
 RTC User Memoryを使用するDeelSleepモードのテスト
 毎時0分に起動する
 50秒前に仮起動しNTPで時間を求めたのち、再度Sleep時間を決定する。
*/
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <WiFiUdp.h>
#include <TimeLib.h> // https://github.com/PaulStoffregen/Time

#define TARGET_MIN 0  // DeepSleep復帰時刻(分)
#define TARGET_SEC 0  // DeepSleep復帰時刻(秒)

// Update these with values suitable for your network.
const char* ssid = "アクセスポイントのSSID";
const char* password = "アクセスポイントのパスワード";
const char* mqtt_server = "MQTTブローカーのIPアドレス";

WiFiClient espClient;
PubSubClient client(espClient);

//RTC memory(512Byte)の定義
struct {
  uint8_t clientMode;
  uint8_t nextHour;
  uint8_t ntpCount;
  uint8_t sleepCount;
  uint16_t sleepSec[10];
  uint8_t data[488];
} rtcData;

// UDPローカルポート番号
unsigned int localPort = 2390;
// NTPサーバー
char timeServer[] = "ntp.nict.jp";
// NTPパケットバッファサイズ
const int NTP_PACKET_SIZE= 48;
// NTP受信用パケットバッファ
byte packetBuffer[NTP_PACKET_SIZE];

// Udpクラス
WiFiUDP udp;

// AP接続までに必要な時間(Mill Sec)
unsigned long diffMillis;

// Print RTC memory
void printMemory() {
  Serial.print("clientMode=");
  Serial.print(rtcData.clientMode);
  Serial.print(" nextHour=");
  Serial.print(rtcData.nextHour);
  Serial.print(" ntpCount=");
  Serial.print(rtcData.ntpCount);
  Serial.println();
}

// Connect AP
void setup_wifi() {
  // We start by connecting to a WiFi network
  //Serial.println();
  //Serial.println();
  Serial.print("Wait for WiFi...");
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
}

// Connect MQTT server
void server_connect() {
  char clientid[20];

  sprintf(clientid,"ESP8266-%06x",ESP.getChipId());
  Serial.print("clientid=");
  Serial.println(clientid);
  // Loop until we're reconnected
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect
    if (client.connect(clientid)) {
      Serial.println("connected");
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

// send an NTP request to the time server at the given address
void sendNTPpacket(char * address)
{
  Serial.println("sending NTP packet...");
  // set all bytes in the buffer to 0
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  // Initialize values needed to form NTP request
  // (see URL above for details on the packets)
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  // 8 bytes of zero for Root Delay & Root Dispersion
  packetBuffer[12]  = 49;
  packetBuffer[13]  = 0x4E;
  packetBuffer[14]  = 49;
  packetBuffer[15]  = 52;

  // all NTP fields have been given values, now
  // you can send a packet requesting a timestamp:
  udp.beginPacket(address, 123); //NTP requests are to port 123
  udp.write(packetBuffer, NTP_PACKET_SIZE);
  udp.endPacket();
}

/*
  * 指定日時のUnixTimeを求める
  */
time_t getTime(int hr,int min,int sec,int dy, int mnth, int yr){
  tmElements_t tm;
  // year can be given as full four digit year or two digts (2010 or 10 for 2010);
  //it is converted to years since 1970
  if( yr > 99)
    yr = yr - 1970;
  else
    yr += 30;
  tm.Year = yr;
  tm.Month = mnth;
  tm.Day = dy;
  tm.Hour = hr;
  tm.Minute = min;
  tm.Second = sec;
  return makeTime(tm);
}

/*
  * UnixTimeをYY/MM/DD hh:mm:ssで表示する
  */
void showTime(char * title, time_t timet) {
  Serial.print(title);
  Serial.print(":");
  Serial.print(year(timet));
  Serial.print("/");
  Serial.print(month(timet));
  Serial.print("/");
  Serial.print(day(timet));
  Serial.print(" ");
  Serial.print(hour(timet));
  Serial.print(":");
  Serial.print(minute(timet));
  Serial.print(":");
  Serial.println(second(timet));
}

/*
  * 現在日時をYY/MM/DD hh:mm:ssで表示する
  */
void showNow(char * title) {
  Serial.print(title);
  Serial.print(":");
  Serial.print(year());
  Serial.print("/");
  Serial.print(month());
  Serial.print("/");
  Serial.print(day());
  Serial.print(" ");
  Serial.print(hour());
  Serial.print(":");
  Serial.print(minute());
  Serial.print(":");
  Serial.println(second());
}

/*
  *  指定時間(HH:MM:SS)までのSleep秒を求める
  *  jst: 現在日時(日本時間のUNIXTIME)
  *  hh : 指定時間(時)
  *  mm : 指定時間(分)
  *  ss : 指定時間(秒)
*/
long getSleepSecond(time_t jst, int hh, int mm, int ss) {
  time_t time_tg;
  time_t time_tn;
  long diff;

  // 現在日時を設定
  //Serial.print("jst=");
  //Serial.print(jst);
  setTime(jst);
  // 指定日時(当日)のUnixTime
  time_tg = getTime(hh,mm,ss,day(),month(),year());
  //Serial.print(" time_tg=");
  //Serial.print(time_tg);
  // 指定日時(翌日)のUnixTime
  time_tn = time_tg + 60*60*24;
  //Serial.print(" time_tn=");
  //Serial.println(time_tn);
  diff=time_tg-jst;
  if (diff < 0) {
    diff=time_tn-jst;
  }
  showTime("Now",jst);
  showTime("Target",time_tg);
  //Serial.print("diff=");
  //Serial.println(diff);
  showTime("Result",jst+diff);
  return diff;
}

/*
  *  次のMM:SSまでのSleep秒を求める
  *  jst: 現在日時(日本時間のUNIXTIME)
  *  mm : 指定時間(分)
  *  ss : 指定時間(秒)
*/
long getSleepSecond2(time_t jst, int mm, int ss) {
  time_t time_tg;
  time_t time_tn;
  long diff;

  // 現在日時を設定
  //Serial.print("jst=");
  //Serial.println(jst);
  setTime(jst);
  // 1時間後のUnixTime
  time_tg = getTime(hour()+1,mm,ss,day(),month(),year());
  diff=time_tg-jst;
  showTime("Now",jst);
  showTime("Target",time_tg);
  //Serial.print("diff=");
  //Serial.println(diff);
  showTime("Result",jst+diff);
  return diff;
}

void setup() {
  unsigned long startMillis = millis();
  delay(100);
  Serial.begin(115200);
  Serial.println();
  Serial.print("ESP.getResetReason()=");
  Serial.println(ESP.getResetReason());
  String resetReason = ESP.getResetReason();

  /*
  enum rst_reason {
  REANSON_DEFAULT_RST = 0, // ノーマルスタート。電源オンなど。
  REANSON_WDT_RST = 1, // ハードウェアウォッチドッグによるリセット
  REANSON_EXCEPTION_RST = 2, // 例外によるリセット。GPIO状態は変化しない
  REANSON_SOFT_WDT_RST = 3, // ソフトウェアウォッチドッグによるリセット。GPIO状態は変化しない
  REANSON_SOFT_RESTART = 4, // ソフトウェアによるリセット。GPIO状態は変化しない
  REANSON_DEEP_SLEEP_AWAKE= 5, // ディープスリープ復帰
  REANSON_EXT_SYS_RST = 6, // 外部要因(RSTピン)によるリセット。
  };
  */

  rst_info *prst = ESP.getResetInfoPtr();
  /*
  struct rst_info{
      uint32 reason;
      uint32 exccause;
      uint32 epc1;
      uint32 epc2;
      uint32 epc3;
      uint32 excvaddr;
      uint32 depc;
  };
  */
  Serial.print("reset reason=");
  Serial.println(prst->reason);

  // RTC memoryからデータを読み込む
  if (ESP.rtcUserMemoryRead(0, (uint32_t*) &rtcData, sizeof(rtcData))) {
    Serial.println("rtcUserMemoryRead Success");
    if (prst->reason != 5) { // Not REANSON_DEEP_SLEEP_AWAKE
      rtcData.clientMode = 0;
      rtcData.nextHour = 0;
      rtcData.ntpCount = 0;
      rtcData.sleepCount = 0;
    }
    printMemory();
  } else {
    Serial.println("rtcUserMemoryRead Fail");
  }

  setup_wifi();
  unsigned long connectedMillis = millis();
  Serial.print("startMillis=");
  Serial.print(startMillis);
  Serial.print(" conectedMillis=");
  Serial.print(connectedMillis);
 
  // 接続までに必要な時間(Mill Sec)
  diffMillis = connectedMillis-startMillis;
  Serial.print(" diffMillis=");
  Serial.println(diffMillis);

  // NTPクライアントモード
  if (rtcData.clientMode == 0) {
    Serial.println("Starting NTP Client");
    udp.begin(localPort);
    // 時刻リクエストを送信
    sendNTPpacket(timeServer);
    rtcData.ntpCount++; // NTP接続回数

  // MQTTクライアントモード
  } else {
    //setup_wifi();
    Serial.println("Starting MQTT Client");
    client.setServer(mqtt_server, 1883);
    server_connect();

    //
    // ここに何か処理を書く(例えばセンサーからのデータ読み込み)
    //

    char topic[50];
    char msg[50];
    char sleepSec[64];
    memset(sleepSec, 0, sizeof(sleepSec));
    for (int i=0;i<rtcData.sleepCount;i++) {
      Serial.print("rtcData.sleepSec=");
      Serial.println(rtcData.sleepSec[i]);
      char wk[10];
      sprintf(wk, "%d ", rtcData.sleepSec[i]);
      strcat(sleepSec, wk);
    }
    Serial.print("sleepSec=");
    Serial.println(sleepSec);
   
    sprintf(topic,"DeepSleep/ESP8266-%06x",ESP.getChipId());
    sprintf(msg,"%02d:%02d %d %s", rtcData.nextHour,TARGET_MIN,rtcData.ntpCount,sleepSec);
    Serial.println(msg);
    client.publish(topic, msg);
    client.disconnect();
    WiFi.disconnect();

    rtcData.clientMode = 0; // 次回起動はNTPモード
    rtcData.nextHour = 0;
    rtcData.ntpCount = 0;
    rtcData.sleepCount = 1;
    rtcData.sleepSec[0] = 3600;
   
    // RTC memoryにデータを書き込む
    if (ESP.rtcUserMemoryWrite(0, (uint32_t*) &rtcData, sizeof(rtcData))) {
      Serial.println("rtcUserMemoryWrite Success");
      printMemory();
    } else {
      Serial.println("rtcUserMemoryWrite Fail");     
    }

    // 次回は60分後に起動する(誤差で実際はもっと早くWakeUpする)
    //long sleepSec = (60 * 60);
    uint32_t sleepUsec = 60 * 60 * 1000 * 1000;
   
    // DEEP SLEEPモード突入
    Serial.println("DEEP SLEEP START!!");
    ESP.deepSleep(sleepUsec , WAKE_RF_DEFAULT);
    delay(1000);
  }
}

void loop()
{
  // NTPサーバからのパケット受信
  int cb = udp.parsePacket();
  if (cb) {
    Serial.print("packet received, length=");
    Serial.println(cb);
    // バッファに受信データを読み込む
    udp.read(packetBuffer, NTP_PACKET_SIZE); // read the packet into the buffer
    // Wifiはもう使わない
    WiFi.disconnect();

    // 時刻情報はパケットの40バイト目からはじまる4バイトのデータ
    unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
    unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);

    // NTPタイムスタンプは64ビットの符号無し固定小数点数(整数部32ビット、小数部32ビット)
    // 1900年1月1日0時との相対的な差を秒単位で表している
    // 小数部は切り捨てて、秒を求めている
    unsigned long secsSince1900 = highWord << 16 | lowWord;
    Serial.print("Seconds since Jan 1 1900 = " );
    Serial.println(secsSince1900);

    // NTPタイムスタンプをUNIXタイムに変換する
    // UNITタイムは1970年1月1日0時からはじまる
    // 1900年から1970年の70年を秒で表すと2208988800秒になる
    const unsigned long seventyYears = 2208988800UL;
    // NTPタイムスタンプから70年分の秒を引くとUNIXタイムが得られる
    time_t epoch = secsSince1900 - seventyYears;
    Serial.print("Unix time = ");
    Serial.println(epoch);
    // 日本時間のUNIXタイム
    time_t jst = epoch + (9 * 60 * 60);
   
    // 目的の時間までのSleep時間を求める
    long sleepSec = getSleepSecond2(jst, TARGET_MIN,TARGET_SEC);
    Serial.print("sleepSec = ");
    Serial.println(sleepSec);

    long sleepMill;
    // 目的の時間まで60秒以下
    if (sleepSec < 60) {
      int diffSec = diffMillis / 1000;
      sleepSec = sleepSec - diffSec;
      sleepMill = sleepSec * 1000;
      rtcData.clientMode = 1; // 次回起動はMQTTモード
      rtcData.nextHour = hour()+1;

    // 目的の時間まで60秒以上
    } else {
      sleepSec = sleepSec - 50;
      sleepMill = sleepSec * 1000;
      rtcData.clientMode = 0; // 次回起動はNTPモード
      rtcData.nextHour = hour()+1;
    }

    rtcData.sleepSec[rtcData.sleepCount] = sleepSec;
    rtcData.sleepCount = rtcData.sleepCount + 1;

    // RTC memoryにデータを書き込む
    if (ESP.rtcUserMemoryWrite(0, (uint32_t*) &rtcData, sizeof(rtcData))) {
      Serial.println("rtcUserMemoryWrite Success");
      printMemory();
    } else {
      Serial.println("rtcUserMemoryWrite Fail");     
    }
       
    // DEEP SLEEPモード突入
    Serial.print("sleepSec = ");
    Serial.print(sleepSec);
    Serial.print(" sleepMill = ");
    Serial.println(sleepMill);
    showTime("Next Wake Up",jst+sleepSec);

    uint32_t sleepUsec = sleepMill * 1000;
    Serial.println("DEEP SLEEP START!!");
    ESP.deepSleep(sleepUsec , WAKE_RF_DEFAULT);
    delay(1000);
  }
}

mosquittoクライアントを使って受信した日時とMQTTのメッセージを確認してみました。
結果は以下の通り、毎時0分にDeepSleepから復帰して、MQTTでデータを送信しています。
起動直後はSleep間隔が分からないので、NTPサーバーに3回接続しています。
それ以降は、NTPサーバーに2回接続していることも分かります。
3600秒でDeepSleepしても、186秒も前にSleepから復帰することが有ることも分かります。
また、3600秒のDeepSleepから復帰する時間がバラバラなことも分かります。


続く...