ESP-13を使ってみる

1日1回決まった時間にDeepSleepから復帰する


前回は毎時0分にDeepSleepから復帰する(正確には MQTTで通信する)方法を紹介しま した。
そこで、今回は1日1回、決まった時間にDeepSleepから復帰する(正確にはMQTTで通信する)方法を紹介します。
但し、DeepSleepの最大Sleep時間は約70分なので、実際は何回かSleepから復帰、即Sleepを繰り返し、
目的の時間に近づいたらNTPを使って現在時刻を確認してSleep時間を補正します。

使用したスケッチは以下のスケッチです。
スケッチの先頭のコメントの様に動作します。
DeepSleep復帰時間は13:20にしましたが、特に意味はありません。

RTCのユーザエリアは以下の用途で使っています。
・先頭の4バイト:CRCとして使用し初回起動か、DeepSleepからの復帰かの判断に使います。
・4バイト目:Sleepから復帰した時の動作モード
・5バイト目:目的の時間(今回は13)
・6バイト目:目的の時間(今回は20)
・7バイト目:NTPで現在時間を確認した回数
・8バイト目以降:NTPで現在時刻を確認した時分

MQTTサーバーはローカルなサーバーを使いました。
/*
 RTC User Memoryを使用するDeelSleepモードのテスト
 1日1回、決まった時間にMQTTで通信する
 rtcData.data[0]を以下のように使用する
  =0:MQTTクライアントモード
  =1:NTPクライアントモード
  <1:単純Sleepモード(単純Sleepする回数)

  例
                                                      Sleep時間 
  10:00 電源ON NTPで時間を確認 rtcData.data[0]=0->2   70分
  11:08 復 帰                   rtcData.data[0]=2->1   70分
  12:16 復帰   NTPで時間を確認 rtcData.data[0]=1->1   64分
  13:19 復帰   NTPで時間を確認 rtcData.data[0]=1->0   1分
  13:20 復帰   MQTT送信        rtcData.data[0]=0->20  70分
  14:28 復 帰                   rtcData.data[0]=20->19 70分
  15:36 復 帰                   rtcData.data[0]=19->18 70分
  16:44 復 帰                   rtcData.data[0]=18->17 70分
  17:52 復 帰                   rtcData.data[0]=17->16 70分
  19:00 復 帰                   rtcData.data[0]=16->15 70分
  20:08 復 帰                   rtcData.data[0]=15->14 70分
  21:16 復 帰                   rtcData.data[0]=14->13 70分
  22:24 復 帰                   rtcData.data[0]=13->12 70分
  23:32 復 帰                   rtcData.data[0]=12->11 70分
   0:40 復 帰                   rtcData.data[0]=11->10 70分
   1:48 復 帰                   rtcData.data[0]=10->9  70分
   2:56 復 帰                   rtcData.data[0]=9->8   70分
   4:04 復 帰                   rtcData.data[0]=8->7   70分
   5:12 復 帰                   rtcData.data[0]=7->6   70分
   6:20 復 帰                   rtcData.data[0]=6->5   70分
   7:28 復 帰                   rtcData.data[0]=5->4   70分
   8:36 復 帰                   rtcData.data[0]=4->3   70分
   9:44 復 帰                   rtcData.data[0]=3->2   70分
  10:52 復 帰                   rtcData.data[0]=2->1   70分
  12:00 復帰   NTPで時間を確認 rtcData.data[0]=1->1   70分
  13:19 復帰   NTPで時間を確認 rtcData.data[0]=1->0    1分
  13:20 復帰   MQTT送信        rtcData.data[0]=0->20  70分
*/
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <WiFiUdp.h>
#include <TimeLib.h> // https://github.com/PaulStoffregen/Time
#include <Ticker.h>

#define TARGET_HOR 13 // DeepSleep復帰時刻(時) 特に意味はない
#define TARGET_MIN 20 // DeepSleep復帰時刻(分) 特に意味はない
#define TARGET_SEC 0  // DeepSleep復帰時刻(秒)
//#define BUILTIN_LED 13

// Update these with values suitable for your network.
const char* ssid = "aterm-e625c0-g";
const char* password = "05ecd1dcd39c6";
const char* mqtt_server = "192.168.10.40";
//const char* mqtt_server = "broker.hivemq.com";

WiFiClient espClient;
PubSubClient client(espClient);
Ticker ticker1;

char topic[50];
char msg[50];

//RTC memory(512Byte)の定義
struct {
  uint32_t crc32; // CRC
  uint8_t data[508]; // User Data
} rtcData;

// UDPローカルポート番号
unsigned int localPort = 2390;
// NTPタイムサーバIPアドレス(ntp.nict.jp NTP server)
IPAddress timeServer(133, 243, 238, 164);
// NTPパケットバッファサイズ
const int NTP_PACKET_SIZE= 48;
// NTP送受信用パケットバッファ
byte packetBuffer[NTP_PACKET_SIZE];
// 最後にパケットを送信した時間(ミリ秒)
unsigned long lastSendPacketTime = 0;

// Udpクラス
WiFiUDP udp;

// Calculate CRC
uint32_t calculateCRC32(const uint8_t *data, size_t length)
{
  uint32_t crc = 0xffffffff;
  while (length--) {
    uint8_t c = *data++;
    for (uint32_t i = 0x80; i > 0; i >>= 1) {
      bool bit = crc & 0x80000000;
      if (c & i) {
        bit = !bit;
      }
      crc <<= 1;
      if (bit) {
        crc ^= 0x04c11db7;
      }
    }
  }
  return crc;
}

// Print RTC memory
void printMemory(int sz) {
  char buf[3];
//  for (int i = 0; i < sizeof(rtcData); i++) {
  for (int i = 0; i < sz; i++) {
    sprintf(buf, "%02X", rtcData.data[i]);
    Serial.print(buf);
    if ((i + 1) % 32 == 0) {
      Serial.println();
    }
    else {
      Serial.print(" ");
    }
  }
  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());
}

// Receive MQTT topic
void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
  }
  Serial.println();
}

// 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");
      // ... and resubscribe
      client.subscribe("inTopic");
    } 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
unsigned long sendNTPpacket(IPAddress& 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);
//    showNow("Now");
    // 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 flush_led() {
  static bool flag = true;
  if (flag) digitalWrite(BUILTIN_LED,LOW);
  if (!flag) digitalWrite(BUILTIN_LED,HIGH);
  flag=!flag;
}

void setup() {
  long sleep;
 
  delay(1000);
  Serial.begin(9600);
  Serial.println();
  Serial.println();
  Serial.println();
  Serial.println("WakeUp");
 
  // RTC memoryからデータを読み込む
  if (ESP.rtcUserMemoryRead(0, (uint32_t*) &rtcData, sizeof(rtcData))) {
    Serial.println("Read: ");
    printMemory(10);
  // 読み込んだデータでCRC32を計算する
    uint32_t crcOfData = calculateCRC32(((uint8_t*) &rtcData) + 4, sizeof(rtcData) - 4);
    Serial.print("CRC32 of data: ");
    Serial.println(crcOfData, HEX);
    Serial.print("CRC32 read from RTC: ");
    Serial.println(rtcData.crc32, HEX);
    // CRC32が一致しない
    if (crcOfData != rtcData.crc32) {
      Serial.println("CRC32 in RTC memory doesn't match CRC32 of data. Data is probably invalid!");
      for (int i = 0; i < sizeof(rtcData); i++) {
        rtcData.data[i] = 0;
      }
      rtcData.data[0] = 1; // 最初はNTPで時間を確認
      rtcData.data[1] = TARGET_HOR;
      rtcData.data[2] = TARGET_MIN;
      rtcData.data[3] = 0;
    }
    // CRC32は一致したが目的時間が一致しない
    else if (rtcData.data[1] != TARGET_HOR || rtcData.data[2] != TARGET_MIN) {
      Serial.println("CRC32 check ok, but target time doesn't match!");
      for (int i = 0; i < sizeof(rtcData.data); i++) {
        rtcData.data[i] = 0;
      }
      rtcData.data[0] = 1; // 最初はNTPで時間を確認
      rtcData.data[1] = TARGET_HOR;
      rtcData.data[2] = TARGET_MIN;
      rtcData.data[3] = 0;
    }
    // CRC32が一致したのでDeelSleepからの復帰
    else {
      Serial.println("CRC32 check ok, data is probably valid.");
      Serial.println("Return from DeeSleep.");
    }
  }


  // NTPクライントモード
  if (rtcData.data[0] == 1) {
    Serial.println("Starting UDP Client");
    pinMode(BUILTIN_LED,OUTPUT);
    ticker1.attach_ms(50, flush_led);
    setup_wifi();
 //   ticker1.detach();
    udp.begin(localPort);
  // 時刻リクエストを送信
    sendNTPpacket(timeServer);

  // MQTTクライアントモード
  } else if (rtcData.data[0] == 0) {
    Serial.println("Starting MQTT Client");
    setup_wifi();
    client.setServer(mqtt_server, 1883);
    client.setCallback(callback);
    server_connect();
    client.loop();
  //
  // ここに何か処理を書く(例えばセンサーからのデータ読み込み)
  //
    sprintf(topic,"Daily/ESP8266-%06x",ESP.getChipId());
    sprintf(msg,"%02d %02d:%02d %d",
    rtcData.data[0],rtcData.data[1],rtcData.data[2],rtcData.data[3]);
    char wk[10];
    for(int i=0;i<rtcData.data[3];i++) {
      memset(wk,0,sizeof(wk));
      sprintf(wk," %02d:%02d",rtcData.data[i*2+4],rtcData.data[i*2+5]);
      strcat(msg,wk);
    }

    Serial.print("MQTT topic=");
    Serial.println(topic);
    Serial.print("MQTT msg=");
    Serial.println(msg);
    client.publish(topic, msg);
    client.disconnect();
    WiFi.disconnect();

    rtcData.data[0] = 20; // 次回起動は単純Sleepモード 単純Sleepする回数(20*70=1400分=23時間20分)
    rtcData.data[3] = 0;

    // CRC32を再計算
    rtcData.crc32 = calculateCRC32(((uint8_t*) &rtcData) + 4, sizeof(rtcData) - 4);
    // RTC memoryにデータを書き込む
    if (ESP.rtcUserMemoryWrite(0, (uint32_t*) &rtcData, sizeof(rtcData))) {
      Serial.println("Write: ");
      printMemory(10);
    }

    // 次回は70分後に起動する(DeepSleepの最大Sleep時間は約71分)
    // DEEP SLEEPモード突入命令
    Serial.println("DEEP SLEEP START!!");
    sleep = (70 * 60);
    ESP.deepSleep(sleep * 1000 * 1000 , WAKE_RF_DEFAULT);
    delay(1000);

  // 単純Sleepモード
  } else {
    Serial.println("Sleep Again");
    rtcData.data[0]--;
 
    // CRC32を再計算
    rtcData.crc32 = calculateCRC32(((uint8_t*) &rtcData) + 4, sizeof(rtcData) - 4);
    // RTC memoryにデータを書き込む
    if (ESP.rtcUserMemoryWrite(0, (uint32_t*) &rtcData, sizeof(rtcData))) {
      Serial.println("Write: ");
      printMemory(10);
    }

    // DEEP SLEEPモード突入命令
    Serial.println("DEEP SLEEP START!!");
    sleep = (70 * 60); // 再び70分のSleep
    ESP.deepSleep(sleep * 1000 * 1000 , WAKE_RF_DEFAULT);
    delay(1000);
  }
}

void loop()
{
  long sleep;
  long sleep2;


  // 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

    // 時刻情報はパケットの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);
    // 日本時間のUNITタイム
    time_t jst = epoch + (9 * 60 * 60);
   
    // 目的の時間までのSleep時間を求める
    sleep=getSleepSecond(jst,TARGET_HOR,TARGET_MIN,TARGET_SEC);
    sleep2 = sleep;
    WiFi.disconnect();

    Serial.print("sleep2 = ");
    Serial.println(sleep2);
    if (sleep2 <= 600) {
      sleep = sleep2;
      rtcData.data[0] = 0; // 次回起動はMQTTモード
    } else if (sleep2 < 8400) {
      sleep = sleep2;
      rtcData.data[0] = 1; // 次回起動はNTPモード
    } else {
      sleep = (70 * 60); // 70分のSleep
//      rtcData.data[0] = (sleep2/sleep) + 1; // 単純Sleepする回数
      rtcData.data[0] = (sleep2/sleep); // 単純Sleepする回数
    }
    Serial.print("rtcData.data[0] = ");
    Serial.println(rtcData.data[0]);
    rtcData.data[3]++; // NTP確認回数
    int i = rtcData.data[3];
    rtcData.data[i*2+2] = hour();
    rtcData.data[i*2+3] = minute();

    // CRC32を再計算
    rtcData.crc32 = calculateCRC32(((uint8_t*) &rtcData) + 4, sizeof(rtcData) - 4);
    // RTC memoryにデータを書き込む
    if (ESP.rtcUserMemoryWrite(0, (uint32_t*) &rtcData, sizeof(rtcData))) {
      Serial.println("Write: ");
      printMemory(10);
    }
    Serial.print("sleep = ");
    Serial.println(sleep);
    showTime("Next Wake Up",jst+sleep);
   
    // DEEP SLEEPモード突入命令
    Serial.println("DEEP SLEEP START!!");
    ESP.deepSleep(sleep * 1000 * 1000 , WAKE_RF_DEFAULT);
    delay(1000);
  }
}

OrangePi-PCでMQTTのSubscriberを作成し、受信した日時とMQTTのメッセージをファイルに記録するようにしました。
結果は以下の通り、13時20分にDeepSleepから復帰して、MQTTでデータを送信しています。
Wifiアクセスポイントの接続に大体7秒ぐらいかかるので、MQTTサーバーに接続するのは少し遅れた時間となります。
[ ]内の意味は
[00 起動予定時刻 NTPにアクセスした回数 NTPにアクセスした時刻]
です。

起動直後は現在時刻が分からないので、起動直後にNTPで現在時刻を確認していることが分かります。
その後は1日2回、現在時刻を確認しています。
rtcData.data[0] = 20; // 次回起動は単純Sleepモード 単純Sleepする回数(20*70=1400分=23時間20分)

rtcData.data[0] = 21; // 次回起動は単純Sleepモード 単純Sleepする回数(21*70=1470分=24時間30分)
に変えれば、1日1回の時刻確認で済みます。