2026年10月9日金曜日

M5StickC Nixie tube Clock with NTC

M5StickC Nixie tube Clockで紹介した時計ですが、起動時にパソコンと同期する設定となっていたため再起動すると時間設定が出来ず表示がめちゃくちゃになります。これでは不便ですので、WiFi接続時はNTCと同期し、接続できないときはRTCの設定日時を使用するように変更しました。


いつもの様にろくに調べないで見切り発車したため、最初の記述は電源オンオフで+9時間されて時間が起動するたびにずれて行きました。原因を調べたところ、M5Unified.hはUTCを記述することが基本(推奨)となっているようで、日本時間で設定していると読みだした際に+9時間してしまったようです。さらっと書いていますが、これが判るまで結構時間が掛かりました。この対策で基本に則ってRTCにはUTCで記載するようにし、表示時にヘルパー関数を使用してJSTで表示するようにしています。


なお、確認用にボタンBを押すとUTC(RTC)とJSTを表示するようにしています(確認用)。ついでにボタンBが上になるように画面向きの設定を変更しています。

// M5StickC Nixie tube Clock: 2026.09.18
#include <M5Unified.h>
#include "vfd_18x34.h"
#include "vfd_35x67.h"
#include "wifi_setting.h"
#include <WiFi.h>
#include <time.h>

// NTP設定
const char* ntpServer1 = "0.pool.ntp.org";
const char* ntpServer2 = "1.pool.ntp.org";
const char* ntpServer3 = "2.pool.ntp.org";
const char* gmtOffset_sec = "JST-9"; 

int mode_ = 3; // 3:2Lines 2: 2Lines(YYMM), 1:1Line

const uint8_t* n[] =  { 
  vfd_18x34_0, vfd_18x34_1, vfd_18x34_2, vfd_18x34_3, vfd_18x34_4,
  vfd_18x34_5, vfd_18x34_6, vfd_18x34_7, vfd_18x34_8, vfd_18x34_9
  }; // vfd font 18x34
 
 const uint32_t ns[] =  { 
  sizeof(vfd_18x34_0), sizeof(vfd_18x34_1), sizeof(vfd_18x34_2), sizeof(vfd_18x34_3), sizeof(vfd_18x34_4),
  sizeof(vfd_18x34_5), sizeof(vfd_18x34_6), sizeof(vfd_18x34_7), sizeof(vfd_18x34_8), sizeof(vfd_18x34_9)
  }; // vfd font 18x34

const uint8_t* m[] =  { 
  vfd_35x67_0, vfd_35x67_1, vfd_35x67_2, vfd_35x67_3, vfd_35x67_4,
  vfd_35x67_5, vfd_35x67_6, vfd_35x67_7, vfd_35x67_8, vfd_35x67_9
  }; // vfd font 35x67

const uint32_t ms[] =  { 
  sizeof(vfd_35x67_0), sizeof(vfd_35x67_1), sizeof(vfd_35x67_2), sizeof(vfd_35x67_3), sizeof(vfd_35x67_4),
  sizeof(vfd_35x67_5), sizeof(vfd_35x67_6), sizeof(vfd_35x67_7), sizeof(vfd_35x67_8), sizeof(vfd_35x67_9)
  }; // vfd font 35x67

// 時間管理用の変数
unsigned long lastClockUpdate = 0;
unsigned long lastButtonCheck = 0;

// フェードエフェクト管理用の変数
bool isFading = false;
unsigned long lastFadeStepTime = 0;
int fadeStep = 0;
const int fadeBrightnessSequence[] = {120, 130, 140, 150, 160, 170, 180, 190, 200, 190, 180, 170, 160, 150, 140, 130, 120, 200};
const int FADE_STEPS_COUNT = 18;

// UTCからJSTのtm構造体に変換する共通ヘルパー関数
struct tm getJstTime() {
  auto dt = M5.Rtc.getDateTime();
  struct tm tm_utc;
  tm_utc.tm_year = dt.date.year - 1900;
  tm_utc.tm_mon  = dt.date.month - 1;
  tm_utc.tm_mday = dt.date.date;
  tm_utc.tm_hour = dt.time.hours;
  tm_utc.tm_min  = dt.time.minutes;
  tm_utc.tm_sec  = dt.time.seconds;
  tm_utc.tm_isdst = 0;

  time_t t_utc = mktime(&tm_utc);
  time_t t_jst = t_utc + (9 * 3600); // +9時間
  return *localtime(&t_jst);
}

// 関数のプロトタイプ宣言
void vfd_0_line();
void vfd_1_line();
void vfd_2_line();
void vfd_3_line();
void handleFade();
void triggerFade();

void setup(void){ 
  auto cfg = M5.config();
  M5.begin(cfg);

  // 画面の向きを設定 (1: 横向きなど)
  M5.Display.setRotation(1);
  M5.Display.fillScreen(TFT_BLACK);
  M5.Display.setBrightness(200);
  M5.Display.setTextSize(1);

  // Wi-Fi 接続
  WiFi.begin(ssid, password);
  M5.Display.print("Connecting to WiFi");

  // 最大5秒間、Wi-Fi接続を待つ
  int timeout = 0;
  while (WiFi.status() != WL_CONNECTED && timeout < 20) {
    delay(500);
    M5.Display.print(".");
    timeout++;
  }
  Serial.println();

  // Sync NTC when connecting Wi-Fi
  if (WiFi.status() == WL_CONNECTED) {
    M5.Display.println("WiFi Connected.");
    // Get time data from NTP
    M5.Display.println("WiFi Connected!\nSyncing NTP...");

    //  時差を設定してNTPから時刻を取得
    configTzTime(gmtOffset_sec, ntpServer1, ntpServer2, ntpServer3);

    // NTPから正しい現在時刻が届くまで最大15秒間ループして待つ
    struct tm timeinfo;
    int retry = 0;
    while (!getLocalTime(&timeinfo, 1000) && retry < 30) {
      delay(500);
      M5.Display.print(".");
      retry++;
    }

    if (retry <= 30) {
      time_t t = time(nullptr) + 1;  // Advance one second
      while (t > time(nullptr))   ;  // Synchronization in seconds
      M5.Rtc.setDateTime(gmtime(&t));

      delay(1000);
      M5.Display.println("RTC updated via NTP.");
    } else {
      M5.Display.println("\nNTP Error! Rebooting...");
      delay(3000);
      ESP.restart(); // 再起動してやり直す
    }

    WiFi.disconnect(true);
    WiFi.mode(WIFI_OFF);
  } else {
    Serial.println("WiFi not available. Skip NTP sync.");
  }
  delay(2000);
  M5.Display.clear();
}

void loop(void) { 
  M5.update();
  unsigned long currentMillis = millis();

  // 1. ボタン入力の監視(200msごとにサクサク判定)
  if (currentMillis - lastButtonCheck >= 200) {
    if (M5.BtnA.isPressed()) {
      lastButtonCheck = currentMillis;
      if (mode_ == 3)      { mode_ = 1; }
      else if (mode_ == 2) { mode_ = 3; }
      else if (mode_ == 1) { mode_ = 2; }
      else if (mode_ == 0) { mode_ = 1; }
      M5.Display.fillScreen(TFT_BLACK); // モード変更時に画面クリア
    } else if (M5.BtnB.isPressed()) {
      lastButtonCheck = currentMillis;
      mode_ = 0;
      M5.Display.fillScreen(TFT_BLACK); // モード変更時に画面クリア
    }
  }

  // 2. 時計の定期描画(500msごと)
  if (currentMillis - lastClockUpdate >= 500) {
    lastClockUpdate = currentMillis;
    
    if (mode_ == 3)      { vfd_3_line(); } // hh:mm:ss
    else if (mode_ == 2) { vfd_2_line(); } // y:m:d hh:mm:ss
    else if (mode_ == 1) { vfd_1_line(); } // mm:ss
    else if (mode_ == 0) { vfd_0_line(); } // text
  }

  // 3. フェードエフェクトの進行管理(バックグラウンド処理)
  if (isFading) {
    handleFade();
  }
}

void vfd_3_line(){
  // 時刻を保持する構造体
  struct tm jst = getJstTime();
  int h1 = int(jst.tm_hour/ 10 );
  int h2 = int(jst.tm_hour- h1*10 );
  int i1 = int(jst.tm_min / 10 );
  int i2 = int(jst.tm_min - i1*10 );
  int s1 = int(jst.tm_sec / 10 );
  int s2 = int(jst.tm_sec - s1*10 );

  M5.Display.drawPng (m[h1], ms[h1],   3,  0, 35, 67);
  M5.Display.drawPng (m[h2], ms[h2],  41,  0, 35, 67);
  M5.Display.drawPixel( 79, 22, ORANGE); M5.Display.drawPixel( 79, 48, ORANGE); 
  M5.Display.drawPixel( 79, 21, YELLOW); M5.Display.drawPixel( 79, 47, YELLOW); 
  M5.Display.drawPng (m[i1], ms[i1],  83,  0, 35, 67);
  M5.Display.drawPng (m[i2], ms[i2], 121,  0, 35, 67);
  M5.Display.drawPng (n[s1], ns[s1], 120, 45, 18, 34);
  M5.Display.drawPng (n[s2], ns[s2], 140, 45, 18, 34);

  // フェード中でない、かつ00秒のときだけ1回だけ起動する
  if (s1 == 0 && s2 == 0 && !isFading) { triggerFade(); }
}

void vfd_2_line(){
  struct tm jst = getJstTime();
  int yearFull = jst.tm_year + 1900;
  int monthFull = jst.tm_mon + 1;
  int y1 = int( yearFull   / 1000 );
  int y2 = int((yearFull  - y1*1000 ) / 100 );
  int y3 = int((yearFull  - y1*1000 - y2*100 ) / 10 );
  int y4 = int( yearFull  - y1*1000 - y2*100 - y3*10 );
  int j1 = int(monthFull  / 10);
  int j2 = int(monthFull  - j1*10 );
  int d1 = int(jst.tm_mday   / 10 );
  int d2 = int(jst.tm_mday   - d1*10 );
  int h1 = int(jst.tm_hour  / 10) ;
  int h2 = int(jst.tm_hour  - h1*10 );
  int i1 = int(jst.tm_min / 10 );
  int i2 = int(jst.tm_min - i1*10 );
  int s1 = int(jst.tm_sec / 10 );
  int s2 = int(jst.tm_sec - s1*10 );

    // 日時表示
  M5.Display.drawPng (n[y1], ns[y1],   0,  0, 18, 34);
  M5.Display.drawPng (n[y2], ns[y2],  19,  0, 18, 34); 
  M5.Display.drawPng (n[y3], ns[y3],  38,  0, 18, 34);
  M5.Display.drawPng (n[y4], ns[y4],  57,  0, 18, 34);
  M5.Display.drawPixel(  77, 13, ORANGE); M5.Display.drawPixel(  77, 23, ORANGE); 
  M5.Display.drawPng (n[j1], ns[j1],  80,  0, 18, 34);
  M5.Display.drawPng (n[j2], ns[j2], 100,  0, 18, 34);
  M5.Display.drawPixel( 118, 13, ORANGE); M5.Display.drawPixel( 119, 23, ORANGE); 
  M5.Display.drawPng (n[d1], ns[d1], 120,  0, 18, 34);
  M5.Display.drawPng (n[d2], ns[d2], 140,  0, 18, 34);

  // 時間表示
  M5.Display.drawPng (n[h1], ns[h1],   0, 40, 18, 34);
  M5.Display.drawPng (n[h2], ns[h2],  20, 40, 18, 34); 
  M5.Display.drawPixel(  48, 54, ORANGE); M5.Display.drawPixel(  48, 64, ORANGE); 
  M5.Display.drawPng (n[i1], ns[i1],  60, 40, 18, 34);
  M5.Display.drawPng (n[i2], ns[i2],  80, 40, 18, 34);
  M5.Display.drawPixel( 108, 54, ORANGE); M5.Display.drawPixel( 108, 64, ORANGE); 
  M5.Display.drawPng (n[s1], ns[s1], 120, 40, 18, 34);
  M5.Display.drawPng (n[s2], ns[s2], 140, 40, 18, 34);

  // フェード中でない、かつ00秒のときだけ1回だけ起動する
  if (s1 == 0 && s2 == 0 && !isFading) { triggerFade(); }
}

void vfd_1_line(){
  struct tm jst = getJstTime();
  int i1 = int(jst.tm_min / 10 );
  int i2 = int(jst.tm_min - i1*10 );
  int s1 = int(jst.tm_sec / 10 );
  int s2 = int(jst.tm_sec - s1*10 );

  // 時間表示
  M5.Display.drawPng (m[i1], ms[i1],   2,  6, 35, 67);
  M5.Display.drawPng (m[i2], ms[i2],  41,  6, 35, 67);
  M5.Display.drawPixel( 79, 28, ORANGE); M5.Display.drawPixel( 79, 54, ORANGE); 
  M5.Display.drawPixel( 79, 27, YELLOW); M5.Display.drawPixel( 79, 53, YELLOW); 
  M5.Display.drawPng (m[s1], ms[s1],  83,  6, 35, 67);
  M5.Display.drawPng (m[s2], ms[s2], 121,  6, 35, 67);

 // フェード中でない、かつ00秒のときだけ1回だけ起動する
 if (s1 == 0 && s2 == 0 && !isFading) { triggerFade(); }
}

void vfd_0_line(){
    M5.Display.setCursor(0, 10);
    M5.Display.setTextColor(WHITE, BLACK);

    // RTCから生データ(UTC)を取得して表示
    auto dt = M5.Rtc.getDateTime();
    M5.Display.printf("UTC : %04d/%02d/%02d %02d:%02d:%02d\r\n", 
                      dt.date.year, dt.date.month, dt.date.date, 
                      dt.time.hours, dt.time.minutes, dt.time.seconds);

    // 計算したJST(日本時間)を表示
    struct tm jst = getJstTime();
    M5.Display.printf("JST : %04d/%02d/%02d %02d:%02d:%02d\r\n", 
                      jst.tm_year + 1900, jst.tm_mon + 1, jst.tm_mday, 
                      jst.tm_hour, jst.tm_min, jst.tm_sec);
}

// フェードを開始させるフラグ制御
void triggerFade() {
  if (!isFading) {
    isFading = true;
    fadeStep = 0;
    lastFadeStepTime = millis();
  }
}

// delayを使わずにバックグラウンドで輝度をじわじわ変化させる処理
void handleFade() {
  unsigned long currentMillis = millis();
  // 25msごとに輝度を1段階変化させる
  if (currentMillis - lastFadeStepTime >= 25) {
    lastFadeStepTime = currentMillis;
    
    M5.Display.setBrightness(fadeBrightnessSequence[fadeStep]);
    fadeStep++;
    
    // すべてのステップが終了したらエフェクトを終了
    if (fadeStep >= FADE_STEPS_COUNT) {
      isFading = false;
    }
  }
}

0 件のコメント:

コメントを投稿