M5StickCで時計を作る（テーマはWiFi、deep_sleep、RTC）

手元にM5StickC購入時に付いてきたバンドもあるし、時計を作ってみよう！と思い立ちました。
#構想（実現したいこと）
・正確な時間を簡単に初期設定したい。→ WiFiでタイムサーバから時刻を取得する。
・１日はバッテリーを持たせたい。→ 使わない間はスリープさせる。
・外に持ち出して使いたい。→ スリープ復帰時は内部の時計（RTC）から時刻を取得して表示する。
#参考にさせていただいた情報
M5StickCのRTCをNTPサーバーからセットする（たなかまさゆきさん [@tnkmasayuki](https://twitter.com/tnkmasayuki "@tnkmasayuki")）
　https://lang-ship.com/blog/work/m5stickc-rtc-ntp/
M5Displayクラスの使い方
　https://lang-ship.com/reference/unofficial/M5StickC/Tips/M5Display/
スリープについて
　https://lang-ship.com/reference/unofficial/M5StickC/System/Sleep/
　https://lang-ship.com/reference/unofficial/M5StickC/System/Sleep/Deep/EXT0/
#動作中の画面
至ってシンプルな佇まい。ポイントは液晶っぽい「7セグ風フォント」でしょうか？！
![IMG_0760.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/677344/1ffcbcde-0d51-f376-9dd7-3439340d8a00.jpeg)

#スケッチ
※このスケッチはM5StickC用です。（M5StickC Plusでは動作しません。）

```c:Local_Clock4.ino
#include <M5StickC.h>
#include <WiFi.h>
#include <time.h>

// wifiの設定
const char* ssid     = "xxxxxxxxxx"; // WiFiのSSID
const char* password = "xxxxxxxxxx"; // WiFiのパスワード

// 時計の設定
const char* ntpServer = "ntp.jst.mfeed.ad.jp"; // NTPサーバー
const long  gmtOffset_sec = 9 * 3600;          // 時差９時間
const int   daylightOffset_sec = 0;            // サマータイム設定なし

RTC_TimeTypeDef RTC_TimeStruct; // 時刻
RTC_DateTypeDef RTC_DateStruct; // 日付

unsigned long setuptime; // スリープ開始判定用（ミリ秒）
int sMin = 0; // 画面書き換え判定用（分）

void printLocalTime() {
  static const char *wd[7] = {"Sun","Mon","Tue","Wed","Thr","Fri","Sat"};
  M5.Rtc.GetTime(&RTC_TimeStruct); // 時刻の取り出し
  M5.Rtc.GetData(&RTC_DateStruct); // 日付の取り出し

  if (sMin == RTC_TimeStruct.Minutes) {
    M5.Lcd.fillRect(140,10,20,10,BLACK); // 秒の表示エリアだけ書き換え
  } else {
    M5.Lcd.fillScreen(BLACK); // 「分」が変わったら画面全体を書き換え
  }
  
  M5.Lcd.setTextColor(GREEN);
  M5.Lcd.setCursor(0, 10, 7);  //x,y,font 7:48ピクセル7セグ風フォント

  M5.Lcd.printf("%02d:%02d", RTC_TimeStruct.Hours, RTC_TimeStruct.Minutes); // 時分を表示

  M5.Lcd.setTextFont(1); // 1:Adafruit 8ピクセルASCIIフォント
  M5.Lcd.printf(":%02d\n",RTC_TimeStruct.Seconds); // 秒を表示

  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setCursor(25, 65, 1);  //x,y,font 1:Adafruit 8ピクセルASCIIフォント
  M5.Lcd.printf("Date:%04d.%02d.%02d %s\n", RTC_DateStruct.Year, RTC_DateStruct.Month, RTC_DateStruct.Date, wd[RTC_DateStruct.WeekDay]);

  sMin = RTC_TimeStruct.Minutes; // 「分」を保存
}

void setup() {
  M5.begin();
  M5.Lcd.setRotation(1);
  M5.Axp.ScreenBreath(9);
  M5.Lcd.fillScreen(BLACK);

  // 電源オン時は、WiFiでNTPサーバーから時刻を取得
  esp_sleep_wakeup_cause_t wakeup_reason;
  wakeup_reason = esp_sleep_get_wakeup_cause(); // スリープからの復帰理由を取得
  
  switch(wakeup_reason){
    case ESP_SLEEP_WAKEUP_EXT0      :
    Serial.printf("外部割り込み(RTC_IO)で起動\n"); break; // スリープ復帰時はWiFiで時刻取得しない
    
    default                         : 
    Serial.printf("スリープ以外からの起動\n"); // 電源オン時は、WiFiでNTPサーバーから時刻取得
    
    // connect to WiFi
    M5.Lcd.setCursor(0, 0, 1);
    M5.Lcd.setTextColor(WHITE);
    M5.Lcd.setTextSize(1);
    M5.Lcd.print("WiFi connecting");
    
    WiFi.begin(ssid, password);
    
    while (WiFi.status() != WL_CONNECTED) { // １秒おきに接続状態を確認
      delay(1000);
      M5.Lcd.print(".");
    }
    M5.Lcd.println("\nconnected");
    
    // Set ntp time to local
    configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
    
    // Get local time
    struct tm timeInfo;
    if (getLocalTime(&timeInfo)) {
      M5.Lcd.print("NTP : ");
      M5.Lcd.println(ntpServer);
      
      // Set RTC time
      RTC_TimeTypeDef TimeStruct;
      TimeStruct.Hours   = timeInfo.tm_hour;
      TimeStruct.Minutes = timeInfo.tm_min;
      TimeStruct.Seconds = timeInfo.tm_sec;
      M5.Rtc.SetTime(&TimeStruct);
      
      RTC_DateTypeDef DateStruct;
      DateStruct.WeekDay = timeInfo.tm_wday;
      DateStruct.Month = timeInfo.tm_mon + 1;
      DateStruct.Date = timeInfo.tm_mday;
      DateStruct.Year = timeInfo.tm_year + 1900;
      M5.Rtc.SetData(&DateStruct);
    }

    // disconnect WiFi as it's no longer needed
    WiFi.disconnect(true);
    WiFi.mode(WIFI_OFF);
    
    M5.Lcd.println("WiFi disconnected");
    delay(1000);

    break; // switch 終わり
  }
  setuptime = millis(); // 起動からの経過時間（ミリ秒）を保存
}

void loop() {
  M5.update();
  printLocalTime();
  delay(980); // 0.98秒待ち
  
  // GPIO37がLOWになったら(M5StickCのA(HOME)ボタンが押されたら)スリープから復帰
  pinMode(GPIO_NUM_37, INPUT_PULLUP);
  esp_sleep_enable_ext0_wakeup(GPIO_NUM_37, LOW);
  
  // 表示開始から１分経過後にディープスリープスタート
  if(millis() > setuptime + 1000 * 60){
    M5.Axp.SetSleep(); //Display OFF
    esp_deep_sleep_start();
  }
}
```

<details><summary>2020.8.30追記　スケッチを一部変更しました。旧スケッチはこのセクションを展開すると参照できます。
　（変更箇所）
　・画面の書き換え間隔
　・スリープの判定にmillis()を使用</summary><div>

```c:Local_Clock2.ino
#include <M5StickC.h>
#include <WiFi.h>
#include <time.h>

// wifiの設定
const char* ssid     = "xxxxxxxxxx"; // WiFiのSSID
const char* password = "xxxxxxxxxx"; // WiFiのパスワード

// 時計の設定
const char* ntpServer = "ntp.jst.mfeed.ad.jp"; // NTPサーバー
const long  gmtOffset_sec = 9 * 3600;          // 時差９時間
const int   daylightOffset_sec = 0;            // サマータイム設定なし

RTC_TimeTypeDef RTC_TimeStruct; // 時刻
RTC_DateTypeDef RTC_DateStruct; // 日付

int timeCnt = 0; // スリープ判定用カウンター

void printLocalTime() {
  static const char *wd[7] = {"Sun","Mon","Tue","Wed","Thr","Fri","Sat"};
  M5.Rtc.GetTime(&RTC_TimeStruct); // 時刻の取り出し
  M5.Rtc.GetData(&RTC_DateStruct); // 日付の取り出し

  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextColor(GREEN);
  M5.Lcd.setCursor(0, 10, 7);  //x,y,font 7:48ピクセル7セグ風フォント

  M5.Lcd.printf("%02d:%02d", RTC_TimeStruct.Hours, RTC_TimeStruct.Minutes); // 時分を表示
  
  M5.Lcd.setTextFont(1); // 1:Adafruit 8ピクセルASCIIフォント
  M5.Lcd.printf(":%02d\n",RTC_TimeStruct.Seconds); // 秒を表示

  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setCursor(25, 65, 1);  //x,y,font 1:Adafruit 8ピクセルASCIIフォント
  M5.Lcd.printf("Date:%04d.%02d.%02d %s\n", RTC_DateStruct.Year, RTC_DateStruct.Month, RTC_DateStruct.Date, wd[RTC_DateStruct.WeekDay]);
}

void setup() {
  M5.begin();
  M5.Lcd.setRotation(1);
  M5.Axp.ScreenBreath(9);
  M5.Lcd.fillScreen(BLACK);

  // 電源オン時は、WiFiでNTPサーバーから時刻を取得
  esp_sleep_wakeup_cause_t wakeup_reason;
  wakeup_reason = esp_sleep_get_wakeup_cause(); // スリープからの復帰理由を取得
  
  switch(wakeup_reason){
    case ESP_SLEEP_WAKEUP_EXT0      :
    Serial.printf("外部割り込み(RTC_IO)で起動\n"); break; // スリープ復帰時はWiFiで時刻取得しない
    
    default                         : 
    Serial.printf("スリープ以外からの起動\n"); // 電源オン時は、WiFiでNTPサーバーから時刻取得
    
    // connect to WiFi
    M5.Lcd.setCursor(0, 0, 1);
    M5.Lcd.setTextColor(WHITE);
    M5.Lcd.setTextSize(1);
    M5.Lcd.print("WiFi connecting");
    
    WiFi.begin(ssid, password);
    
    while (WiFi.status() != WL_CONNECTED) { // １秒おきに接続状態を確認
      delay(1000);
      M5.Lcd.print(".");
    }
    M5.Lcd.println("\nconnected");
    
    // Set ntp time to local
    configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
    
    // Get local time
    struct tm timeInfo;
    if (getLocalTime(&timeInfo)) {
      M5.Lcd.print("NTP : ");
      M5.Lcd.println(ntpServer);
      
      // Set RTC time
      RTC_TimeTypeDef TimeStruct;
      TimeStruct.Hours   = timeInfo.tm_hour;
      TimeStruct.Minutes = timeInfo.tm_min;
      TimeStruct.Seconds = timeInfo.tm_sec;
      M5.Rtc.SetTime(&TimeStruct);
      
      RTC_DateTypeDef DateStruct;
      DateStruct.WeekDay = timeInfo.tm_wday;
      DateStruct.Month = timeInfo.tm_mon + 1;
      DateStruct.Date = timeInfo.tm_mday;
      DateStruct.Year = timeInfo.tm_year + 1900;
      M5.Rtc.SetData(&DateStruct);
    }

    // disconnect WiFi as it's no longer needed
    WiFi.disconnect(true);
    WiFi.mode(WIFI_OFF);
    
    M5.Lcd.println("WiFi disconnected");
    delay(1000);

    break; // switch 終わり
  }
}

void loop() {
  printLocalTime();
  
  delay(500); // 0.5秒待ち
  timeCnt++;  // スリープ判定用のカウンターに1加算
  
  // GPIO37がLOWになったら(M5StickCのA(HOME)ボタンが押されたら)スリープから復帰
  pinMode(GPIO_NUM_37, INPUT_PULLUP);
  esp_sleep_enable_ext0_wakeup(GPIO_NUM_37, LOW);

  // １分経過後にディープスリープスタート
  if(timeCnt > 120){
    timeCnt = 0;
    M5.Axp.SetSleep(); //Display OFF
    esp_deep_sleep_start();
  }
}
```
</div></details>

<details><summary>2020.9.6追記　スケッチを一部変更しました。旧スケッチはこのセクションを展開すると参照できます。
　（変更箇所）
　・画面の書き換え間隔（分が変わったときのみ画面全体を書き換え）</summary><div>

```c:Local_Clock3.ino
#include <M5StickC.h>
#include <WiFi.h>
#include <time.h>

// wifiの設定
const char* ssid     = "xxxxxxxxxx"; // WiFiのSSID
const char* password = "xxxxxxxxxx"; // WiFiのパスワード

// 時計の設定
const char* ntpServer = "ntp.jst.mfeed.ad.jp"; // NTPサーバー
const long  gmtOffset_sec = 9 * 3600;          // 時差９時間
const int   daylightOffset_sec = 0;            // サマータイム設定なし

RTC_TimeTypeDef RTC_TimeStruct; // 時刻
RTC_DateTypeDef RTC_DateStruct; // 日付

unsigned long setuptime; // スリープ開始判定用

void printLocalTime() {
  static const char *wd[7] = {"Sun","Mon","Tue","Wed","Thr","Fri","Sat"};
  M5.Rtc.GetTime(&RTC_TimeStruct); // 時刻の取り出し
  M5.Rtc.GetData(&RTC_DateStruct); // 日付の取り出し

  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextColor(GREEN);
  M5.Lcd.setCursor(0, 10, 7);  //x,y,font 7:48ピクセル7セグ風フォント

  M5.Lcd.printf("%02d:%02d", RTC_TimeStruct.Hours, RTC_TimeStruct.Minutes); // 時分を表示

  M5.Lcd.setTextFont(1); // 1:Adafruit 8ピクセルASCIIフォント
  M5.Lcd.printf(":%02d\n",RTC_TimeStruct.Seconds); // 秒を表示

  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setCursor(25, 65, 1);  //x,y,font 1:Adafruit 8ピクセルASCIIフォント
  M5.Lcd.printf("Date:%04d.%02d.%02d %s\n", RTC_DateStruct.Year, RTC_DateStruct.Month, RTC_DateStruct.Date, wd[RTC_DateStruct.WeekDay]);
}

void setup() {
  M5.begin();
  M5.Lcd.setRotation(1);
  M5.Axp.ScreenBreath(9);
  M5.Lcd.fillScreen(BLACK);

  // 電源オン時は、WiFiでNTPサーバーから時刻を取得
  esp_sleep_wakeup_cause_t wakeup_reason;
  wakeup_reason = esp_sleep_get_wakeup_cause(); // スリープからの復帰理由を取得
  
  switch(wakeup_reason){
    case ESP_SLEEP_WAKEUP_EXT0      :
    Serial.printf("外部割り込み(RTC_IO)で起動\n"); break; // スリープ復帰時はWiFiで時刻取得しない
    
    default                         : 
    Serial.printf("スリープ以外からの起動\n"); // 電源オン時は、WiFiでNTPサーバーから時刻取得
    
    // connect to WiFi
    M5.Lcd.setCursor(0, 0, 1);
    M5.Lcd.setTextColor(WHITE);
    M5.Lcd.setTextSize(1);
    M5.Lcd.print("WiFi connecting");
    
    WiFi.begin(ssid, password);
    
    while (WiFi.status() != WL_CONNECTED) { // １秒おきに接続状態を確認
      delay(1000);
      M5.Lcd.print(".");
    }
    M5.Lcd.println("\nconnected");
    
    // Set ntp time to local
    configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
    
    // Get local time
    struct tm timeInfo;
    if (getLocalTime(&timeInfo)) {
      M5.Lcd.print("NTP : ");
      M5.Lcd.println(ntpServer);
      
      // Set RTC time
      RTC_TimeTypeDef TimeStruct;
      TimeStruct.Hours   = timeInfo.tm_hour;
      TimeStruct.Minutes = timeInfo.tm_min;
      TimeStruct.Seconds = timeInfo.tm_sec;
      M5.Rtc.SetTime(&TimeStruct);
      
      RTC_DateTypeDef DateStruct;
      DateStruct.WeekDay = timeInfo.tm_wday;
      DateStruct.Month = timeInfo.tm_mon + 1;
      DateStruct.Date = timeInfo.tm_mday;
      DateStruct.Year = timeInfo.tm_year + 1900;
      M5.Rtc.SetData(&DateStruct);
    }

    // disconnect WiFi as it's no longer needed
    WiFi.disconnect(true);
    WiFi.mode(WIFI_OFF);
    
    M5.Lcd.println("WiFi disconnected");
    delay(1000);

    break; // switch 終わり
  }
  setuptime = millis(); // 起動からの経過時間（ミリ秒）を保存
}

void loop() {
  M5.update();
  printLocalTime();
  delay(980); // 0.98秒待ち
  
  // GPIO37がLOWになったら(M5StickCのA(HOME)ボタンが押されたら)スリープから復帰
  pinMode(GPIO_NUM_37, INPUT_PULLUP);
  esp_sleep_enable_ext0_wakeup(GPIO_NUM_37, LOW);
  
  // 表示開始から１分経過後にディープスリープスタート
  if(millis() > setuptime + 1000 * 60){
    M5.Axp.SetSleep(); //Display OFF
    esp_deep_sleep_start();
  }
}
```
</div></details>
・2020.11.15追記　スケッチを一部変更しました。（不要な秒表示の書き換え行を削除しました。）

#所感
NTPサーバーからの時刻取得は意外とすんなりできたのですが、苦労したのはスリープ復帰時の時刻取得です。スリープ復帰時はsetupから再スタートになるため、WiFi接続しようとします。これでは外出時に困るため、電源オン時はWiFi接続、スリープ復帰時は何もしないという判定を入れました。

この判定はうまく処理されるのですが、今度はスリープ復帰時に日本時間が表示されない（９時間の時差がない時刻が表示される？）という状態になりました。

RTC（Real Time Clock）に時刻を保存する必要があるのでは？どうやったら保存＆取得できるのか？色々調べて見つけたのが、たなかまさゆきさんのブログでした。本当に助かりました。

と、いうわけで今回は時計スケッチの作成を通して、WiFi接続、スリープ、RTCについて理解を深めることができました。