From 76d6ee900c499082deca753b6c1ca04a2d341511 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Mon, 19 Oct 2026 20:10:21 +0200 Subject: [PATCH] Statuswoerter NTP (gruen), RTC (gelb, blinkt bei leerer Batterie) und ESP (rot), per Weboberflaeche schaltbar Co-Authored-By: Claude Sonnet 5.5 --- software/arduino/arduino.ino | 118 ++++++++++++++++++++++++++++++----- software/pio/src/main.cpp | 118 ++++++++++++++++++++++++++++++----- 2 files changed, 206 insertions(+), 30 deletions(-) diff --git a/software/arduino/arduino.ino b/software/arduino/arduino.ino index 652c532..54302ab 100644 --- a/software/arduino/arduino.ino +++ b/software/arduino/arduino.ino @@ -28,8 +28,9 @@ RTC_DS1307 rtc; #define RTC_SDA D2 #define RTC_SCL D1 bool rtcOk = false; +bool rtcBatteryEmpty = false; // RTC hat beim Start die Zeit verloren volatile bool ntpJustSynced = false; -void onTimeSet(){ ntpJustSynced = true; } +void onTimeSet(bool fromSntp){ if(fromSntp) ntpJustSynced = true; } // ================= LED ================= #define LED_PIN D4 @@ -48,7 +49,7 @@ String ntpServer = DEFAULT_NTP; String tzString = DEFAULT_TZ; // ================= SETTINGS ================= -#define SETTINGS_MAGIC 0x57554852UL // erkennt gueltige EEPROM-Daten +#define SETTINGS_MAGIC 0x57554853UL // erkennt gueltige EEPROM-Daten struct Settings { uint32_t magic; @@ -61,6 +62,8 @@ struct Settings { uint32_t bgColor; bool backgroundMode; bool nightBackgroundDisabled; + bool statusLeds; + int ntpShowSec; char ntpServer[64]; char tzString[64]; } settings; @@ -70,6 +73,8 @@ uint32_t currentColor = 0x00FFFF; uint32_t bgColor = 0x000000; bool backgroundMode = false; bool nightBackgroundDisabled = false; +bool statusLeds = true; // Statuswoerter NTP/RTC/ESP anzeigen +int ntpShowSec = 10; // Mindestdauer der gruenen NTP-Anzeige bool forceInstant = false; bool updateNeeded = true; int brightness = 120; @@ -99,6 +104,8 @@ void saveSettings(){ settings.bgColor = bgColor; settings.backgroundMode = backgroundMode; settings.nightBackgroundDisabled = nightBackgroundDisabled; + settings.statusLeds = statusLeds; + settings.ntpShowSec = ntpShowSec; strncpy(settings.ntpServer, ntpServer.c_str(), sizeof(settings.ntpServer) - 1); settings.ntpServer[sizeof(settings.ntpServer) - 1] = '\0'; strncpy(settings.tzString, tzString.c_str(), sizeof(settings.tzString) - 1); @@ -120,6 +127,8 @@ void loadSettings(){ bgColor = settings.bgColor & 0xFFFFFF; backgroundMode = settings.backgroundMode; nightBackgroundDisabled = settings.nightBackgroundDisabled; + statusLeds = settings.statusLeds; + ntpShowSec = constrain(settings.ntpShowSec, 1, 60); settings.ntpServer[sizeof(settings.ntpServer) - 1] = '\0'; settings.tzString[sizeof(settings.tzString) - 1] = '\0'; ntpServer = String(settings.ntpServer); @@ -135,8 +144,15 @@ void loadSettings(){ #define NTP_GUELTIG_MS 7200000UL // NTP-Sync gilt 2 h als aktuell unsigned long lastRtcReadMs = 0; unsigned long lastNtpMs = 0; // 0 = noch kein NTP-Sync seit Start +bool ntpShowing = false; // NTP-Wort leuchtet gerade +unsigned long ntpShowUntil = 0; -void readRtcToSystem(){ +bool ntpAktuell(){ + return lastNtpMs != 0 && millis() - lastNtpMs < NTP_GUELTIG_MS; +} + +// Liest die RTC; liefert false, wenn sie keine gueltige Zeit hat (Batterie leer). +bool readRtcToSystem(){ #ifdef RTC_VORHANDEN lastRtcReadMs = millis(); #if defined(RTC_MODUL_DS3231) @@ -149,8 +165,10 @@ void readRtcToSystem(){ struct timeval tv = { (time_t)n.unixtime(), 0 }; settimeofday(&tv, nullptr); ntpJustSynced = false; // der eigene Aufruf ist keine NTP-Zeit + return true; } #endif + return false; } void initRtc(){ @@ -158,27 +176,72 @@ void initRtc(){ Wire.begin(RTC_SDA, RTC_SCL); if(!rtc.begin()) return; rtcOk = true; - readRtcToSystem(); + // Nach dem Nachstellen per NTP ist der Batteriestatus nicht mehr erkennbar, + // daher wird er nur beim Start bestimmt. + rtcBatteryEmpty = !readRtcToSystem(); #endif } // Nach jeder NTP-Synchronisation die RTC nachstellen; ohne aktuellen NTP-Sync // stuendlich die Systemzeit aus der RTC auffrischen. -void updateRtc(){ -#ifdef RTC_VORHANDEN - if(!rtcOk) return; +void updateTimeSources(){ if(ntpJustSynced){ ntpJustSynced = false; if(timeSynced()){ lastNtpMs = millis() ? millis() : 1; - rtc.adjust(DateTime((uint32_t)time(nullptr))); + ntpShowing = true; + ntpShowUntil = millis() + (unsigned long)ntpShowSec * 1000UL; +#ifdef RTC_VORHANDEN + if(rtcOk) rtc.adjust(DateTime((uint32_t)time(nullptr))); +#endif } } - bool ntpAktuell = lastNtpMs != 0 && millis() - lastNtpMs < NTP_GUELTIG_MS; - if(!ntpAktuell && millis() - lastRtcReadMs >= RTC_INTERVAL_MS) readRtcToSystem(); + if(ntpShowing && (long)(millis() - ntpShowUntil) >= 0) ntpShowing = false; +#ifdef RTC_VORHANDEN + if(rtcOk && !rtcBatteryEmpty && !ntpAktuell() && millis() - lastRtcReadMs >= RTC_INTERVAL_MS) + readRtcToSystem(); #endif } +// ================= STATUS-WOERTER ================= +// NTP (gruen): waehrend/kurz nach dem NTP-Abgleich +// RTC (gelb): RTC vorhanden, aber kein aktueller NTP-Abgleich; bei leerer Batterie +// blinkt das Wort im 10-s-Takt +// ESP (rot): weder gueltige RTC noch NTP +#define STATUS_GRUEN 0x00FF00 +#define STATUS_GELB 0xFFFF00 +#define STATUS_ROT 0xFF0000 +struct StatusFarben { uint32_t ntp, rtc, esp; }; +StatusFarben statusAktuell = {0, 0, 0}; + +StatusFarben berechneStatus(){ + StatusFarben st = {0, 0, 0}; + if(!statusLeds) return st; + bool ntpOk = ntpAktuell(); + bool rtcGut = rtcOk && !rtcBatteryEmpty; + if(ntpShowing) st.ntp = STATUS_GRUEN; + if(rtcOk){ + if(rtcBatteryEmpty) st.rtc = ((millis() / 10000UL) & 1) ? 0 : STATUS_GELB; + else if(!ntpOk) st.rtc = STATUS_GELB; + } + if(!rtcGut && !ntpOk) st.esp = STATUS_ROT; + return st; +} + +// Aenderung der Statusanzeige erkennen (loest ein Neuzeichnen aus) +bool statusGeaendert(){ + StatusFarben st = berechneStatus(); + bool changed = st.ntp != statusAktuell.ntp || st.rtc != statusAktuell.rtc || st.esp != statusAktuell.esp; + statusAktuell = st; + return changed; +} + +void applyStatus(){ + for(int i=34;i<=36;i++) if(statusAktuell.ntp) targetBuffer[i] = statusAktuell.ntp; + for(int i=56;i<=58;i++) if(statusAktuell.rtc) targetBuffer[i] = statusAktuell.rtc; + for(int i=67;i<=69;i++) if(statusAktuell.esp) targetBuffer[i] = statusAktuell.esp; +} + // ================= HELFER ================= bool isNightTime(struct tm *t){ if(!autoNight) return false; @@ -348,10 +411,21 @@ void showTimeWords(int h, int m){ else if(hh == 7) word_SIEBEN(); else if(hh == 8) word_ACHT(); else if(hh == 9) word_NEUN(); else if(hh == 10) word_ZEHN_H(); else if(hh == 11) word_ELF(); else word_ZWOELF(); + applyStatus(); + if(forceInstant){ showInstant(); forceInstant = false; } else { fadeToTargetSmooth(20, 10); } } +// Solange noch keine Zeit bekannt ist, nur Hintergrund und Statuswoerter zeigen +void showStatusOnly(){ + for(int i=0;i Hintergrund in Nacht aus +
+ Status-Anzeige (NTP grün, RTC gelb, ESP rot)

+ NTP-Anzeige mind. Sekunden +
@@ -450,7 +530,7 @@ static const char INDEX_HTML[] PROGMEM = R"rawliteral( const s=(i,v,c=false)=>{const e=document.getElementById(i);if(e&&document.activeElement!==e){if(c)e.checked=(v==1);else e.value=v;}}; s('col',d.color);s('br',d.brightness);s('night',d.night,true);s('nS',d.nightStart);s('nE',d.nightEnd); s('nbr',d.nightBrightness);s('bg',d.bg,true);s('bgcol',d.bgColor);s('ntpS',d.ntpServer);s('tzS',d.tzString); - s('nightBgOff',d.nightBgOff,true); + s('nightBgOff',d.nightBgOff,true);s('sl',d.statusLeds,true);s('nsec',d.ntpShowSec); }).catch(()=>{document.getElementById('status').innerText='Verbindung unterbrochen...';})} setInterval(u, 2500); u(); @@ -480,6 +560,8 @@ void handleNightStart(){ int v; if(!argInRange(0,23,v)) return; nightStart=v; fo void handleNightEnd(){ int v; if(!argInRange(0,23,v)) return; nightEnd=v; forceInstant=true; updateNeeded=true; sendOK(); } void handleNightBrightness(){ int v; if(!argInRange(0,255,v)) return; nightBrightness=v; forceInstant=true; updateNeeded=true; sendOK(); } void handleNightBgOff(){ int v; if(!argInRange(0,1,v)) return; nightBackgroundDisabled=v; forceInstant=true; updateNeeded=true; sendOK(); } +void handleStatusLeds(){ int v; if(!argInRange(0,1,v)) return; statusLeds=v; forceInstant=true; updateNeeded=true; sendOK(); } +void handleNtpShowSec(){ int v; if(!argInRange(1,60,v)) return; ntpShowSec=v; sendOK(); } void handleBG(){ int v; if(!argInRange(0,1,v)) return; backgroundMode=v; forceInstant=true; updateNeeded=true; sendOK(); } void handleNTP(){ @@ -523,6 +605,8 @@ void setup(){ server.on("/nightBrightness", handleNightBrightness); server.on("/nightBgOff", handleNightBgOff); server.on("/bg", handleBG); + server.on("/statusLeds", handleStatusLeds); + server.on("/ntpShowSec", handleNtpShowSec); server.on("/setNTP", handleNTP); server.on("/setTZ", handleTZ); server.on("/save", handleSave); @@ -531,10 +615,14 @@ void setup(){ void loop(){ server.handleClient(); - updateRtc(); + updateTimeSources(); + if(statusGeaendert()){ forceInstant = true; updateNeeded = true; } - // Erst anzeigen, wenn die Zeit per NTP synchronisiert ist - if(!timeSynced()) return; + // Ohne bekannte Zeit nur die Statuswoerter anzeigen + if(!timeSynced()){ + if(updateNeeded){ updateNeeded = false; forceInstant = false; showStatusOnly(); } + return; + } time_t now = time(nullptr); struct tm *t = localtime(&now); diff --git a/software/pio/src/main.cpp b/software/pio/src/main.cpp index 652c532..54302ab 100644 --- a/software/pio/src/main.cpp +++ b/software/pio/src/main.cpp @@ -28,8 +28,9 @@ RTC_DS1307 rtc; #define RTC_SDA D2 #define RTC_SCL D1 bool rtcOk = false; +bool rtcBatteryEmpty = false; // RTC hat beim Start die Zeit verloren volatile bool ntpJustSynced = false; -void onTimeSet(){ ntpJustSynced = true; } +void onTimeSet(bool fromSntp){ if(fromSntp) ntpJustSynced = true; } // ================= LED ================= #define LED_PIN D4 @@ -48,7 +49,7 @@ String ntpServer = DEFAULT_NTP; String tzString = DEFAULT_TZ; // ================= SETTINGS ================= -#define SETTINGS_MAGIC 0x57554852UL // erkennt gueltige EEPROM-Daten +#define SETTINGS_MAGIC 0x57554853UL // erkennt gueltige EEPROM-Daten struct Settings { uint32_t magic; @@ -61,6 +62,8 @@ struct Settings { uint32_t bgColor; bool backgroundMode; bool nightBackgroundDisabled; + bool statusLeds; + int ntpShowSec; char ntpServer[64]; char tzString[64]; } settings; @@ -70,6 +73,8 @@ uint32_t currentColor = 0x00FFFF; uint32_t bgColor = 0x000000; bool backgroundMode = false; bool nightBackgroundDisabled = false; +bool statusLeds = true; // Statuswoerter NTP/RTC/ESP anzeigen +int ntpShowSec = 10; // Mindestdauer der gruenen NTP-Anzeige bool forceInstant = false; bool updateNeeded = true; int brightness = 120; @@ -99,6 +104,8 @@ void saveSettings(){ settings.bgColor = bgColor; settings.backgroundMode = backgroundMode; settings.nightBackgroundDisabled = nightBackgroundDisabled; + settings.statusLeds = statusLeds; + settings.ntpShowSec = ntpShowSec; strncpy(settings.ntpServer, ntpServer.c_str(), sizeof(settings.ntpServer) - 1); settings.ntpServer[sizeof(settings.ntpServer) - 1] = '\0'; strncpy(settings.tzString, tzString.c_str(), sizeof(settings.tzString) - 1); @@ -120,6 +127,8 @@ void loadSettings(){ bgColor = settings.bgColor & 0xFFFFFF; backgroundMode = settings.backgroundMode; nightBackgroundDisabled = settings.nightBackgroundDisabled; + statusLeds = settings.statusLeds; + ntpShowSec = constrain(settings.ntpShowSec, 1, 60); settings.ntpServer[sizeof(settings.ntpServer) - 1] = '\0'; settings.tzString[sizeof(settings.tzString) - 1] = '\0'; ntpServer = String(settings.ntpServer); @@ -135,8 +144,15 @@ void loadSettings(){ #define NTP_GUELTIG_MS 7200000UL // NTP-Sync gilt 2 h als aktuell unsigned long lastRtcReadMs = 0; unsigned long lastNtpMs = 0; // 0 = noch kein NTP-Sync seit Start +bool ntpShowing = false; // NTP-Wort leuchtet gerade +unsigned long ntpShowUntil = 0; -void readRtcToSystem(){ +bool ntpAktuell(){ + return lastNtpMs != 0 && millis() - lastNtpMs < NTP_GUELTIG_MS; +} + +// Liest die RTC; liefert false, wenn sie keine gueltige Zeit hat (Batterie leer). +bool readRtcToSystem(){ #ifdef RTC_VORHANDEN lastRtcReadMs = millis(); #if defined(RTC_MODUL_DS3231) @@ -149,8 +165,10 @@ void readRtcToSystem(){ struct timeval tv = { (time_t)n.unixtime(), 0 }; settimeofday(&tv, nullptr); ntpJustSynced = false; // der eigene Aufruf ist keine NTP-Zeit + return true; } #endif + return false; } void initRtc(){ @@ -158,27 +176,72 @@ void initRtc(){ Wire.begin(RTC_SDA, RTC_SCL); if(!rtc.begin()) return; rtcOk = true; - readRtcToSystem(); + // Nach dem Nachstellen per NTP ist der Batteriestatus nicht mehr erkennbar, + // daher wird er nur beim Start bestimmt. + rtcBatteryEmpty = !readRtcToSystem(); #endif } // Nach jeder NTP-Synchronisation die RTC nachstellen; ohne aktuellen NTP-Sync // stuendlich die Systemzeit aus der RTC auffrischen. -void updateRtc(){ -#ifdef RTC_VORHANDEN - if(!rtcOk) return; +void updateTimeSources(){ if(ntpJustSynced){ ntpJustSynced = false; if(timeSynced()){ lastNtpMs = millis() ? millis() : 1; - rtc.adjust(DateTime((uint32_t)time(nullptr))); + ntpShowing = true; + ntpShowUntil = millis() + (unsigned long)ntpShowSec * 1000UL; +#ifdef RTC_VORHANDEN + if(rtcOk) rtc.adjust(DateTime((uint32_t)time(nullptr))); +#endif } } - bool ntpAktuell = lastNtpMs != 0 && millis() - lastNtpMs < NTP_GUELTIG_MS; - if(!ntpAktuell && millis() - lastRtcReadMs >= RTC_INTERVAL_MS) readRtcToSystem(); + if(ntpShowing && (long)(millis() - ntpShowUntil) >= 0) ntpShowing = false; +#ifdef RTC_VORHANDEN + if(rtcOk && !rtcBatteryEmpty && !ntpAktuell() && millis() - lastRtcReadMs >= RTC_INTERVAL_MS) + readRtcToSystem(); #endif } +// ================= STATUS-WOERTER ================= +// NTP (gruen): waehrend/kurz nach dem NTP-Abgleich +// RTC (gelb): RTC vorhanden, aber kein aktueller NTP-Abgleich; bei leerer Batterie +// blinkt das Wort im 10-s-Takt +// ESP (rot): weder gueltige RTC noch NTP +#define STATUS_GRUEN 0x00FF00 +#define STATUS_GELB 0xFFFF00 +#define STATUS_ROT 0xFF0000 +struct StatusFarben { uint32_t ntp, rtc, esp; }; +StatusFarben statusAktuell = {0, 0, 0}; + +StatusFarben berechneStatus(){ + StatusFarben st = {0, 0, 0}; + if(!statusLeds) return st; + bool ntpOk = ntpAktuell(); + bool rtcGut = rtcOk && !rtcBatteryEmpty; + if(ntpShowing) st.ntp = STATUS_GRUEN; + if(rtcOk){ + if(rtcBatteryEmpty) st.rtc = ((millis() / 10000UL) & 1) ? 0 : STATUS_GELB; + else if(!ntpOk) st.rtc = STATUS_GELB; + } + if(!rtcGut && !ntpOk) st.esp = STATUS_ROT; + return st; +} + +// Aenderung der Statusanzeige erkennen (loest ein Neuzeichnen aus) +bool statusGeaendert(){ + StatusFarben st = berechneStatus(); + bool changed = st.ntp != statusAktuell.ntp || st.rtc != statusAktuell.rtc || st.esp != statusAktuell.esp; + statusAktuell = st; + return changed; +} + +void applyStatus(){ + for(int i=34;i<=36;i++) if(statusAktuell.ntp) targetBuffer[i] = statusAktuell.ntp; + for(int i=56;i<=58;i++) if(statusAktuell.rtc) targetBuffer[i] = statusAktuell.rtc; + for(int i=67;i<=69;i++) if(statusAktuell.esp) targetBuffer[i] = statusAktuell.esp; +} + // ================= HELFER ================= bool isNightTime(struct tm *t){ if(!autoNight) return false; @@ -348,10 +411,21 @@ void showTimeWords(int h, int m){ else if(hh == 7) word_SIEBEN(); else if(hh == 8) word_ACHT(); else if(hh == 9) word_NEUN(); else if(hh == 10) word_ZEHN_H(); else if(hh == 11) word_ELF(); else word_ZWOELF(); + applyStatus(); + if(forceInstant){ showInstant(); forceInstant = false; } else { fadeToTargetSmooth(20, 10); } } +// Solange noch keine Zeit bekannt ist, nur Hintergrund und Statuswoerter zeigen +void showStatusOnly(){ + for(int i=0;i Hintergrund in Nacht aus
+
+ Status-Anzeige (NTP grün, RTC gelb, ESP rot)

+ NTP-Anzeige mind. Sekunden +
@@ -450,7 +530,7 @@ static const char INDEX_HTML[] PROGMEM = R"rawliteral( const s=(i,v,c=false)=>{const e=document.getElementById(i);if(e&&document.activeElement!==e){if(c)e.checked=(v==1);else e.value=v;}}; s('col',d.color);s('br',d.brightness);s('night',d.night,true);s('nS',d.nightStart);s('nE',d.nightEnd); s('nbr',d.nightBrightness);s('bg',d.bg,true);s('bgcol',d.bgColor);s('ntpS',d.ntpServer);s('tzS',d.tzString); - s('nightBgOff',d.nightBgOff,true); + s('nightBgOff',d.nightBgOff,true);s('sl',d.statusLeds,true);s('nsec',d.ntpShowSec); }).catch(()=>{document.getElementById('status').innerText='Verbindung unterbrochen...';})} setInterval(u, 2500); u(); @@ -480,6 +560,8 @@ void handleNightStart(){ int v; if(!argInRange(0,23,v)) return; nightStart=v; fo void handleNightEnd(){ int v; if(!argInRange(0,23,v)) return; nightEnd=v; forceInstant=true; updateNeeded=true; sendOK(); } void handleNightBrightness(){ int v; if(!argInRange(0,255,v)) return; nightBrightness=v; forceInstant=true; updateNeeded=true; sendOK(); } void handleNightBgOff(){ int v; if(!argInRange(0,1,v)) return; nightBackgroundDisabled=v; forceInstant=true; updateNeeded=true; sendOK(); } +void handleStatusLeds(){ int v; if(!argInRange(0,1,v)) return; statusLeds=v; forceInstant=true; updateNeeded=true; sendOK(); } +void handleNtpShowSec(){ int v; if(!argInRange(1,60,v)) return; ntpShowSec=v; sendOK(); } void handleBG(){ int v; if(!argInRange(0,1,v)) return; backgroundMode=v; forceInstant=true; updateNeeded=true; sendOK(); } void handleNTP(){ @@ -523,6 +605,8 @@ void setup(){ server.on("/nightBrightness", handleNightBrightness); server.on("/nightBgOff", handleNightBgOff); server.on("/bg", handleBG); + server.on("/statusLeds", handleStatusLeds); + server.on("/ntpShowSec", handleNtpShowSec); server.on("/setNTP", handleNTP); server.on("/setTZ", handleTZ); server.on("/save", handleSave); @@ -531,10 +615,14 @@ void setup(){ void loop(){ server.handleClient(); - updateRtc(); + updateTimeSources(); + if(statusGeaendert()){ forceInstant = true; updateNeeded = true; } - // Erst anzeigen, wenn die Zeit per NTP synchronisiert ist - if(!timeSynced()) return; + // Ohne bekannte Zeit nur die Statuswoerter anzeigen + if(!timeSynced()){ + if(updateNeeded){ updateNeeded = false; forceInstant = false; showStatusOnly(); } + return; + } time_t now = time(nullptr); struct tm *t = localtime(&now);