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









