4 Commits
4 changed files with 444 additions and 60 deletions

No files matched your search

+4
View File
@@ -35,6 +35,9 @@ Am Anfang den ESP anlöten:
## Flashen ## Flashen
**Board und RTC wählen:** PlatformIO: Umgebung `<board>_<rtc>` mit Board `d1_mini` oder `nodemcu` und RTC `ds3231`, `ds1307` oder `keine` (z. B. `pio run -e nodemcu_ds3231 -t upload`). Arduino IDE: Board unter Werkzeuge wählen und oben in `arduino.ino` `RTC_MODUL_DS3231`, `RTC_MODUL_DS1307` oder `RTC_MODUL_KEINE` definieren (Standard: DS3231). Die RTC hängt an D2 (SDA) / D1 (SCL); der DS1307 braucht 5 V.
1. Dieses Repo clonen (`git clone https://github.com/c3e/wordclock.git`) 1. Dieses Repo clonen (`git clone https://github.com/c3e/wordclock.git`)
2. Die Arduino IDE installieren (https://www.arduino.cc/en/Main/Software) 2. Die Arduino IDE installieren (https://www.arduino.cc/en/Main/Software)
3. Die Datei `wordclock/wordclock.ino` in der Arduino IDE öffnen 3. Die Datei `wordclock/wordclock.ino` in der Arduino IDE öffnen
@@ -45,6 +48,7 @@ Am Anfang den ESP anlöten:
8. Folgende installieren: 8. Folgende installieren:
* FastLED * FastLED
* WiFiManager * WiFiManager
* RTClib (Adafruit)
9. Folgende Zip-Dateien herunterladen: 9. Folgende Zip-Dateien herunterladen:
* https://github.com/PaulStoffregen/Time/archive/master.zip * https://github.com/PaulStoffregen/Time/archive/master.zip
* https://github.com/JChristensen/Timezone/archive/master.zip * https://github.com/JChristensen/Timezone/archive/master.zip
+203 -29
View File
@@ -5,10 +5,40 @@
#include <EEPROM.h> #include <EEPROM.h>
#include <time.h> #include <time.h>
#include <WiFiManager.h> #include <WiFiManager.h>
#include <Wire.h>
#include <sys/time.h>
// ================= RTC =================
// RTC-Modul waehlen (vor dem Kompilieren): RTC_MODUL_DS3231, RTC_MODUL_DS1307 oder RTC_MODUL_KEINE.
// PlatformIO: ueber die Umgebung (siehe platformio.ini). Arduino IDE: hier eintragen.
// Das Board (D1 mini / NodeMCU) waehlt man in der IDE bzw. ebenfalls ueber die Umgebung;
// die Pinnamen D1/D2/D4 sind bei beiden identisch.
#if !defined(RTC_MODUL_DS3231) && !defined(RTC_MODUL_DS1307) && !defined(RTC_MODUL_KEINE)
#define RTC_MODUL_DS3231
#endif
#if defined(RTC_MODUL_DS3231) || defined(RTC_MODUL_DS1307)
#include <RTClib.h>
#define RTC_VORHANDEN
#endif
#if defined(RTC_MODUL_DS3231)
RTC_DS3231 rtc;
#elif defined(RTC_MODUL_DS1307)
RTC_DS1307 rtc;
#endif
#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(bool fromSntp){ if(fromSntp) ntpJustSynced = true; }
// ================= LED ================= // ================= LED =================
#define LED_PIN D4 #define LED_PIN D4
#define NUMPIXELS 110 // Neue Aufteilung (cad/openscad/gemeinsam.scad): je Wort ein LED-Streifen mit einer
// LED pro Buchstabe, Zickzack durch die Zeilen. Zeile 0 laeuft links->rechts, Zeile 1
// rechts->links usw. Die Fuellbuchstaben gibt es nicht, nur die Woerter (inkl. NTP,
// API, RTC, ESP). Minutenpunkte sind abgeschaltet.
#define NUMPIXELS 104
Adafruit_NeoPixel pixels(NUMPIXELS, LED_PIN, NEO_GRB + NEO_KHZ800); Adafruit_NeoPixel pixels(NUMPIXELS, LED_PIN, NEO_GRB + NEO_KHZ800);
// ================= SERVER & NTP ================= // ================= SERVER & NTP =================
@@ -19,7 +49,7 @@ String ntpServer = DEFAULT_NTP;
String tzString = DEFAULT_TZ; String tzString = DEFAULT_TZ;
// ================= SETTINGS ================= // ================= SETTINGS =================
#define SETTINGS_MAGIC 0x57554852UL // erkennt gueltige EEPROM-Daten #define SETTINGS_MAGIC 0x57554853UL // erkennt gueltige EEPROM-Daten
struct Settings { struct Settings {
uint32_t magic; uint32_t magic;
@@ -32,6 +62,8 @@ struct Settings {
uint32_t bgColor; uint32_t bgColor;
bool backgroundMode; bool backgroundMode;
bool nightBackgroundDisabled; bool nightBackgroundDisabled;
bool statusLeds;
int ntpShowSec;
char ntpServer[64]; char ntpServer[64];
char tzString[64]; char tzString[64];
} settings; } settings;
@@ -41,6 +73,8 @@ uint32_t currentColor = 0x00FFFF;
uint32_t bgColor = 0x000000; uint32_t bgColor = 0x000000;
bool backgroundMode = false; bool backgroundMode = false;
bool nightBackgroundDisabled = false; bool nightBackgroundDisabled = false;
bool statusLeds = true; // Statuswoerter NTP/RTC/ESP anzeigen
int ntpShowSec = 10; // Mindestdauer der gruenen NTP-Anzeige
bool forceInstant = false; bool forceInstant = false;
bool updateNeeded = true; bool updateNeeded = true;
int brightness = 120; int brightness = 120;
@@ -70,6 +104,8 @@ void saveSettings(){
settings.bgColor = bgColor; settings.bgColor = bgColor;
settings.backgroundMode = backgroundMode; settings.backgroundMode = backgroundMode;
settings.nightBackgroundDisabled = nightBackgroundDisabled; settings.nightBackgroundDisabled = nightBackgroundDisabled;
settings.statusLeds = statusLeds;
settings.ntpShowSec = ntpShowSec;
strncpy(settings.ntpServer, ntpServer.c_str(), sizeof(settings.ntpServer) - 1); strncpy(settings.ntpServer, ntpServer.c_str(), sizeof(settings.ntpServer) - 1);
settings.ntpServer[sizeof(settings.ntpServer) - 1] = '\0'; settings.ntpServer[sizeof(settings.ntpServer) - 1] = '\0';
strncpy(settings.tzString, tzString.c_str(), sizeof(settings.tzString) - 1); strncpy(settings.tzString, tzString.c_str(), sizeof(settings.tzString) - 1);
@@ -91,6 +127,8 @@ void loadSettings(){
bgColor = settings.bgColor & 0xFFFFFF; bgColor = settings.bgColor & 0xFFFFFF;
backgroundMode = settings.backgroundMode; backgroundMode = settings.backgroundMode;
nightBackgroundDisabled = settings.nightBackgroundDisabled; nightBackgroundDisabled = settings.nightBackgroundDisabled;
statusLeds = settings.statusLeds;
ntpShowSec = constrain(settings.ntpShowSec, 1, 60);
settings.ntpServer[sizeof(settings.ntpServer) - 1] = '\0'; settings.ntpServer[sizeof(settings.ntpServer) - 1] = '\0';
settings.tzString[sizeof(settings.tzString) - 1] = '\0'; settings.tzString[sizeof(settings.tzString) - 1] = '\0';
ntpServer = String(settings.ntpServer); ntpServer = String(settings.ntpServer);
@@ -101,6 +139,109 @@ void loadSettings(){
configTime(tzString.c_str(), ntpServer.c_str()); configTime(tzString.c_str(), ntpServer.c_str());
} }
// Zeit aus der RTC uebernehmen. Die RTC laeuft in UTC.
#define RTC_INTERVAL_MS 3600000UL // ohne NTP: RTC stuendlich neu einlesen
#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;
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)
bool valid = !rtc.lostPower();
#else
bool valid = rtc.isrunning();
#endif
DateTime n = rtc.now();
if(valid && n.year() >= 2024){
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(){
#ifdef RTC_VORHANDEN
Wire.begin(RTC_SDA, RTC_SCL);
if(!rtc.begin()) return;
rtcOk = true;
// 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 updateTimeSources(){
if(ntpJustSynced){
ntpJustSynced = false;
if(timeSynced()){
lastNtpMs = millis() ? millis() : 1;
ntpShowing = true;
ntpShowUntil = millis() + (unsigned long)ntpShowSec * 1000UL;
#ifdef RTC_VORHANDEN
if(rtcOk) rtc.adjust(DateTime((uint32_t)time(nullptr)));
#endif
}
}
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 ================= // ================= HELFER =================
bool isNightTime(struct tm *t){ bool isNightTime(struct tm *t){
if(!autoNight) return false; if(!autoNight) return false;
@@ -181,29 +322,34 @@ void fadeToTargetSmooth(int steps = 30, int delayMs = 15){
} }
// ================= WORTUHR LOGIK ================= // ================= WORTUHR LOGIK =================
void word_ES(){ setRange(0,1); } // LED-Indizes in Kettenreihenfolge (Zeilen im Zickzack, Woerter je Zeile in Laufrichtung)
void word_IST(){ setRange(3,5); } void word_ES(){ setRange(0,1); } // Zeile 0 (links->rechts)
void word_FUENF_M(){ setRange(7,10); } void word_IST(){ setRange(2,4); }
void word_ZEHN_M(){ setRange(18,21); } void word_FUENF_M(){ setRange(5,8); }
void word_ZWANZIG(){ setRange(11,17); } void word_ZEHN_M(){ setRange(9,12); } // Zeile 1 (rechts->links)
void word_VIERTEL(){ setRange(22,28); } void word_ZWANZIG(){ setRange(13,19); }
void word_VOR(){ setRange(30,32); } void word_VIERTEL(){ setRange(20,26); } // Zeile 2
void word_NACH(){ setRange(40,43); } void word_VOR(){ setRange(27,29); }
void word_HALB(){ setRange(33,36); } void word_NACH(){ setRange(30,33); } // Zeile 3 (rechts->links)
void word_EINS(){ setRange(62,65); } // NTP 34-36
void word_EIN(){ setRange(62,64); } void word_HALB(){ setRange(37,40); }
void word_ZWEI(){ setRange(55,58); } void word_NEUN(){ setRange(41,44); } // Zeile 4
void word_DREI(){ setRange(66,69); } // API 45-47
void word_VIER(){ setRange(73,76); } void word_FUENF_H(){ setRange(48,51); }
void word_FUENF_H(){ setRange(51,54); } void word_EINS(){ setRange(52,55); } // Zeile 5 (rechts->links): S, EIN, RTC, ZWEI
void word_SECHS(){ setRange(83,87); } void word_EIN(){ setRange(53,55); }
void word_SIEBEN(){ setRange(88,93); } // RTC 56-58
void word_ACHT(){ setRange(77,80); } void word_ZWEI(){ setRange(59,62); }
void word_NEUN(){ setRange(44,47); } void word_DREI(){ setRange(63,66); } // Zeile 6
void word_ZEHN_H(){ setRange(106,109); } // ESP 67-69
void word_ELF(){ setRange(103,105); } void word_VIER(){ setRange(70,73); }
void word_ZWOELF(){ setRange(94,98); } void word_SECHS(){ setRange(74,78); } // Zeile 7 (rechts->links)
void word_UHR(){ setRange(99,101); } void word_ACHT(){ setRange(79,82); }
void word_SIEBEN(){ setRange(83,88); } // Zeile 8
void word_ZWOELF(){ setRange(89,93); }
void word_ZEHN_H(){ setRange(94,97); } // Zeile 9 (rechts->links)
void word_ELF(){ setRange(98,100); }
void word_UHR(){ setRange(101,103); }
String getTimeText(int h, int m){ String getTimeText(int h, int m){
int m5 = (m/5)*5; int m5 = (m/5)*5;
@@ -265,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 == 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(); else if(hh == 10) word_ZEHN_H(); else if(hh == 11) word_ELF(); else word_ZWOELF();
applyStatus();
if(forceInstant){ showInstant(); forceInstant = false; } if(forceInstant){ showInstant(); forceInstant = false; }
else { fadeToTargetSmooth(20, 10); } else { fadeToTargetSmooth(20, 10); }
} }
// Solange noch keine Zeit bekannt ist, nur Hintergrund und Statuswoerter zeigen
void showStatusOnly(){
for(int i=0;i<NUMPIXELS;i++) currentBuffer[i] = targetBuffer[i];
uint32_t activeBg = backgroundMode ? bgColor : 0;
for(int i=0;i<NUMPIXELS;i++) targetBuffer[i] = activeBg;
applyStatus();
showInstant();
}
// ================= WEB HANDLERS ================= // ================= WEB HANDLERS =================
String jsonEscape(const String &s){ String jsonEscape(const String &s){
String out; String out;
@@ -313,7 +470,9 @@ void handleStatus(){
json += "\"nightStart\":" + String(nightStart) + ","; json += "\"nightStart\":" + String(nightStart) + ",";
json += "\"nightEnd\":" + String(nightEnd) + ","; json += "\"nightEnd\":" + String(nightEnd) + ",";
json += "\"bg\":" + String(backgroundMode ? 1 : 0) + ","; json += "\"bg\":" + String(backgroundMode ? 1 : 0) + ",";
json += "\"nightBgOff\":" + String(nightBackgroundDisabled ? 1 : 0); json += "\"nightBgOff\":" + String(nightBackgroundDisabled ? 1 : 0) + ",";
json += "\"statusLeds\":" + String(statusLeds ? 1 : 0) + ",";
json += "\"ntpShowSec\":" + String(ntpShowSec);
json += "}"; json += "}";
server.send(200, "application/json", json); server.send(200, "application/json", json);
} }
@@ -351,6 +510,10 @@ static const char INDEX_HTML[] PROGMEM = R"rawliteral(
<input type='range' id='nbr' min='0' max='255' oninput="f('/nightBrightness?val='+this.value)"> <input type='range' id='nbr' min='0' max='255' oninput="f('/nightBrightness?val='+this.value)">
<input type='checkbox' id='nightBgOff' onchange="f('/nightBgOff?val='+(this.checked?1:0))"> Hintergrund in Nacht aus <input type='checkbox' id='nightBgOff' onchange="f('/nightBgOff?val='+(this.checked?1:0))"> Hintergrund in Nacht aus
</div> </div>
<div class='card'>
<input type='checkbox' id='sl' onchange="f('/statusLeds?val='+(this.checked?1:0))"> <b>Status-Anzeige</b> (NTP gr&uuml;n, RTC gelb, ESP rot)<br><br>
NTP-Anzeige mind. <input type='number' id='nsec' min='1' max='60' style='width:50px' oninput="if(this.value!=='')f('/ntpShowSec?val='+this.value)"> Sekunden
</div>
<div class='card'> <div class='card'>
<label>NTP Server:</label> <label>NTP Server:</label>
<input type='text' id='ntpS' onchange="f('/setNTP?val='+encodeURIComponent(this.value))"> <input type='text' id='ntpS' onchange="f('/setNTP?val='+encodeURIComponent(this.value))">
@@ -367,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;}}; 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('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('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...';})} }).catch(()=>{document.getElementById('status').innerText='Verbindung unterbrochen...';})}
setInterval(u, 2500); u(); setInterval(u, 2500); u();
</script></body></html> </script></body></html>
@@ -397,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 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 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 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 handleBG(){ int v; if(!argInRange(0,1,v)) return; backgroundMode=v; forceInstant=true; updateNeeded=true; sendOK(); }
void handleNTP(){ void handleNTP(){
@@ -422,7 +587,9 @@ void setup(){
pixels.clear(); pixels.clear();
pixels.show(); pixels.show();
settimeofday_cb(onTimeSet);
loadSettings(); loadSettings();
initRtc();
WiFiManager wm; WiFiManager wm;
wm.autoConnect("Wortuhr"); wm.autoConnect("Wortuhr");
@@ -438,6 +605,8 @@ void setup(){
server.on("/nightBrightness", handleNightBrightness); server.on("/nightBrightness", handleNightBrightness);
server.on("/nightBgOff", handleNightBgOff); server.on("/nightBgOff", handleNightBgOff);
server.on("/bg", handleBG); server.on("/bg", handleBG);
server.on("/statusLeds", handleStatusLeds);
server.on("/ntpShowSec", handleNtpShowSec);
server.on("/setNTP", handleNTP); server.on("/setNTP", handleNTP);
server.on("/setTZ", handleTZ); server.on("/setTZ", handleTZ);
server.on("/save", handleSave); server.on("/save", handleSave);
@@ -446,9 +615,14 @@ void setup(){
void loop(){ void loop(){
server.handleClient(); server.handleClient();
updateTimeSources();
if(statusGeaendert()){ forceInstant = true; updateNeeded = true; }
// Erst anzeigen, wenn die Zeit per NTP synchronisiert ist // Ohne bekannte Zeit nur die Statuswoerter anzeigen
if(!timeSynced()) return; if(!timeSynced()){
if(updateNeeded){ updateNeeded = false; forceInstant = false; showStatusOnly(); }
return;
}
time_t now = time(nullptr); time_t now = time(nullptr);
struct tm *t = localtime(&now); struct tm *t = localtime(&now);
+34 -2
View File
@@ -1,8 +1,40 @@
[env:d1_mini] ; Umgebung = Board + RTC-Modul waehlen, z.B.: pio run -e nodemcu_ds3231 -t upload
; Board: d1_mini | nodemcu RTC: ds3231 | ds1307 | keine
[common]
platform = espressif8266 platform = espressif8266
board = d1_mini
framework = arduino framework = arduino
monitor_speed = 115200 monitor_speed = 115200
lib_deps = lib_deps =
adafruit/Adafruit NeoPixel adafruit/Adafruit NeoPixel
tzapu/WiFiManager tzapu/WiFiManager
adafruit/RTClib
[board_d1_mini]
board = d1_mini
[board_nodemcu]
board = nodemcuv2
[env:d1_mini_ds3231]
extends = common, board_d1_mini
build_flags = -DRTC_MODUL_DS3231
[env:d1_mini_ds1307]
extends = common, board_d1_mini
build_flags = -DRTC_MODUL_DS1307
[env:d1_mini_keine]
extends = common, board_d1_mini
build_flags = -DRTC_MODUL_KEINE
[env:nodemcu_ds3231]
extends = common, board_nodemcu
build_flags = -DRTC_MODUL_DS3231
[env:nodemcu_ds1307]
extends = common, board_nodemcu
build_flags = -DRTC_MODUL_DS1307
[env:nodemcu_keine]
extends = common, board_nodemcu
build_flags = -DRTC_MODUL_KEINE
+203 -29
View File
@@ -5,10 +5,40 @@
#include <EEPROM.h> #include <EEPROM.h>
#include <time.h> #include <time.h>
#include <WiFiManager.h> #include <WiFiManager.h>
#include <Wire.h>
#include <sys/time.h>
// ================= RTC =================
// RTC-Modul waehlen (vor dem Kompilieren): RTC_MODUL_DS3231, RTC_MODUL_DS1307 oder RTC_MODUL_KEINE.
// PlatformIO: ueber die Umgebung (siehe platformio.ini). Arduino IDE: hier eintragen.
// Das Board (D1 mini / NodeMCU) waehlt man in der IDE bzw. ebenfalls ueber die Umgebung;
// die Pinnamen D1/D2/D4 sind bei beiden identisch.
#if !defined(RTC_MODUL_DS3231) && !defined(RTC_MODUL_DS1307) && !defined(RTC_MODUL_KEINE)
#define RTC_MODUL_DS3231
#endif
#if defined(RTC_MODUL_DS3231) || defined(RTC_MODUL_DS1307)
#include <RTClib.h>
#define RTC_VORHANDEN
#endif
#if defined(RTC_MODUL_DS3231)
RTC_DS3231 rtc;
#elif defined(RTC_MODUL_DS1307)
RTC_DS1307 rtc;
#endif
#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(bool fromSntp){ if(fromSntp) ntpJustSynced = true; }
// ================= LED ================= // ================= LED =================
#define LED_PIN D4 #define LED_PIN D4
#define NUMPIXELS 110 // Neue Aufteilung (cad/openscad/gemeinsam.scad): je Wort ein LED-Streifen mit einer
// LED pro Buchstabe, Zickzack durch die Zeilen. Zeile 0 laeuft links->rechts, Zeile 1
// rechts->links usw. Die Fuellbuchstaben gibt es nicht, nur die Woerter (inkl. NTP,
// API, RTC, ESP). Minutenpunkte sind abgeschaltet.
#define NUMPIXELS 104
Adafruit_NeoPixel pixels(NUMPIXELS, LED_PIN, NEO_GRB + NEO_KHZ800); Adafruit_NeoPixel pixels(NUMPIXELS, LED_PIN, NEO_GRB + NEO_KHZ800);
// ================= SERVER & NTP ================= // ================= SERVER & NTP =================
@@ -19,7 +49,7 @@ String ntpServer = DEFAULT_NTP;
String tzString = DEFAULT_TZ; String tzString = DEFAULT_TZ;
// ================= SETTINGS ================= // ================= SETTINGS =================
#define SETTINGS_MAGIC 0x57554852UL // erkennt gueltige EEPROM-Daten #define SETTINGS_MAGIC 0x57554853UL // erkennt gueltige EEPROM-Daten
struct Settings { struct Settings {
uint32_t magic; uint32_t magic;
@@ -32,6 +62,8 @@ struct Settings {
uint32_t bgColor; uint32_t bgColor;
bool backgroundMode; bool backgroundMode;
bool nightBackgroundDisabled; bool nightBackgroundDisabled;
bool statusLeds;
int ntpShowSec;
char ntpServer[64]; char ntpServer[64];
char tzString[64]; char tzString[64];
} settings; } settings;
@@ -41,6 +73,8 @@ uint32_t currentColor = 0x00FFFF;
uint32_t bgColor = 0x000000; uint32_t bgColor = 0x000000;
bool backgroundMode = false; bool backgroundMode = false;
bool nightBackgroundDisabled = false; bool nightBackgroundDisabled = false;
bool statusLeds = true; // Statuswoerter NTP/RTC/ESP anzeigen
int ntpShowSec = 10; // Mindestdauer der gruenen NTP-Anzeige
bool forceInstant = false; bool forceInstant = false;
bool updateNeeded = true; bool updateNeeded = true;
int brightness = 120; int brightness = 120;
@@ -70,6 +104,8 @@ void saveSettings(){
settings.bgColor = bgColor; settings.bgColor = bgColor;
settings.backgroundMode = backgroundMode; settings.backgroundMode = backgroundMode;
settings.nightBackgroundDisabled = nightBackgroundDisabled; settings.nightBackgroundDisabled = nightBackgroundDisabled;
settings.statusLeds = statusLeds;
settings.ntpShowSec = ntpShowSec;
strncpy(settings.ntpServer, ntpServer.c_str(), sizeof(settings.ntpServer) - 1); strncpy(settings.ntpServer, ntpServer.c_str(), sizeof(settings.ntpServer) - 1);
settings.ntpServer[sizeof(settings.ntpServer) - 1] = '\0'; settings.ntpServer[sizeof(settings.ntpServer) - 1] = '\0';
strncpy(settings.tzString, tzString.c_str(), sizeof(settings.tzString) - 1); strncpy(settings.tzString, tzString.c_str(), sizeof(settings.tzString) - 1);
@@ -91,6 +127,8 @@ void loadSettings(){
bgColor = settings.bgColor & 0xFFFFFF; bgColor = settings.bgColor & 0xFFFFFF;
backgroundMode = settings.backgroundMode; backgroundMode = settings.backgroundMode;
nightBackgroundDisabled = settings.nightBackgroundDisabled; nightBackgroundDisabled = settings.nightBackgroundDisabled;
statusLeds = settings.statusLeds;
ntpShowSec = constrain(settings.ntpShowSec, 1, 60);
settings.ntpServer[sizeof(settings.ntpServer) - 1] = '\0'; settings.ntpServer[sizeof(settings.ntpServer) - 1] = '\0';
settings.tzString[sizeof(settings.tzString) - 1] = '\0'; settings.tzString[sizeof(settings.tzString) - 1] = '\0';
ntpServer = String(settings.ntpServer); ntpServer = String(settings.ntpServer);
@@ -101,6 +139,109 @@ void loadSettings(){
configTime(tzString.c_str(), ntpServer.c_str()); configTime(tzString.c_str(), ntpServer.c_str());
} }
// Zeit aus der RTC uebernehmen. Die RTC laeuft in UTC.
#define RTC_INTERVAL_MS 3600000UL // ohne NTP: RTC stuendlich neu einlesen
#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;
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)
bool valid = !rtc.lostPower();
#else
bool valid = rtc.isrunning();
#endif
DateTime n = rtc.now();
if(valid && n.year() >= 2024){
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(){
#ifdef RTC_VORHANDEN
Wire.begin(RTC_SDA, RTC_SCL);
if(!rtc.begin()) return;
rtcOk = true;
// 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 updateTimeSources(){
if(ntpJustSynced){
ntpJustSynced = false;
if(timeSynced()){
lastNtpMs = millis() ? millis() : 1;
ntpShowing = true;
ntpShowUntil = millis() + (unsigned long)ntpShowSec * 1000UL;
#ifdef RTC_VORHANDEN
if(rtcOk) rtc.adjust(DateTime((uint32_t)time(nullptr)));
#endif
}
}
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 ================= // ================= HELFER =================
bool isNightTime(struct tm *t){ bool isNightTime(struct tm *t){
if(!autoNight) return false; if(!autoNight) return false;
@@ -181,29 +322,34 @@ void fadeToTargetSmooth(int steps = 30, int delayMs = 15){
} }
// ================= WORTUHR LOGIK ================= // ================= WORTUHR LOGIK =================
void word_ES(){ setRange(0,1); } // LED-Indizes in Kettenreihenfolge (Zeilen im Zickzack, Woerter je Zeile in Laufrichtung)
void word_IST(){ setRange(3,5); } void word_ES(){ setRange(0,1); } // Zeile 0 (links->rechts)
void word_FUENF_M(){ setRange(7,10); } void word_IST(){ setRange(2,4); }
void word_ZEHN_M(){ setRange(18,21); } void word_FUENF_M(){ setRange(5,8); }
void word_ZWANZIG(){ setRange(11,17); } void word_ZEHN_M(){ setRange(9,12); } // Zeile 1 (rechts->links)
void word_VIERTEL(){ setRange(22,28); } void word_ZWANZIG(){ setRange(13,19); }
void word_VOR(){ setRange(30,32); } void word_VIERTEL(){ setRange(20,26); } // Zeile 2
void word_NACH(){ setRange(40,43); } void word_VOR(){ setRange(27,29); }
void word_HALB(){ setRange(33,36); } void word_NACH(){ setRange(30,33); } // Zeile 3 (rechts->links)
void word_EINS(){ setRange(62,65); } // NTP 34-36
void word_EIN(){ setRange(62,64); } void word_HALB(){ setRange(37,40); }
void word_ZWEI(){ setRange(55,58); } void word_NEUN(){ setRange(41,44); } // Zeile 4
void word_DREI(){ setRange(66,69); } // API 45-47
void word_VIER(){ setRange(73,76); } void word_FUENF_H(){ setRange(48,51); }
void word_FUENF_H(){ setRange(51,54); } void word_EINS(){ setRange(52,55); } // Zeile 5 (rechts->links): S, EIN, RTC, ZWEI
void word_SECHS(){ setRange(83,87); } void word_EIN(){ setRange(53,55); }
void word_SIEBEN(){ setRange(88,93); } // RTC 56-58
void word_ACHT(){ setRange(77,80); } void word_ZWEI(){ setRange(59,62); }
void word_NEUN(){ setRange(44,47); } void word_DREI(){ setRange(63,66); } // Zeile 6
void word_ZEHN_H(){ setRange(106,109); } // ESP 67-69
void word_ELF(){ setRange(103,105); } void word_VIER(){ setRange(70,73); }
void word_ZWOELF(){ setRange(94,98); } void word_SECHS(){ setRange(74,78); } // Zeile 7 (rechts->links)
void word_UHR(){ setRange(99,101); } void word_ACHT(){ setRange(79,82); }
void word_SIEBEN(){ setRange(83,88); } // Zeile 8
void word_ZWOELF(){ setRange(89,93); }
void word_ZEHN_H(){ setRange(94,97); } // Zeile 9 (rechts->links)
void word_ELF(){ setRange(98,100); }
void word_UHR(){ setRange(101,103); }
String getTimeText(int h, int m){ String getTimeText(int h, int m){
int m5 = (m/5)*5; int m5 = (m/5)*5;
@@ -265,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 == 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(); else if(hh == 10) word_ZEHN_H(); else if(hh == 11) word_ELF(); else word_ZWOELF();
applyStatus();
if(forceInstant){ showInstant(); forceInstant = false; } if(forceInstant){ showInstant(); forceInstant = false; }
else { fadeToTargetSmooth(20, 10); } else { fadeToTargetSmooth(20, 10); }
} }
// Solange noch keine Zeit bekannt ist, nur Hintergrund und Statuswoerter zeigen
void showStatusOnly(){
for(int i=0;i<NUMPIXELS;i++) currentBuffer[i] = targetBuffer[i];
uint32_t activeBg = backgroundMode ? bgColor : 0;
for(int i=0;i<NUMPIXELS;i++) targetBuffer[i] = activeBg;
applyStatus();
showInstant();
}
// ================= WEB HANDLERS ================= // ================= WEB HANDLERS =================
String jsonEscape(const String &s){ String jsonEscape(const String &s){
String out; String out;
@@ -313,7 +470,9 @@ void handleStatus(){
json += "\"nightStart\":" + String(nightStart) + ","; json += "\"nightStart\":" + String(nightStart) + ",";
json += "\"nightEnd\":" + String(nightEnd) + ","; json += "\"nightEnd\":" + String(nightEnd) + ",";
json += "\"bg\":" + String(backgroundMode ? 1 : 0) + ","; json += "\"bg\":" + String(backgroundMode ? 1 : 0) + ",";
json += "\"nightBgOff\":" + String(nightBackgroundDisabled ? 1 : 0); json += "\"nightBgOff\":" + String(nightBackgroundDisabled ? 1 : 0) + ",";
json += "\"statusLeds\":" + String(statusLeds ? 1 : 0) + ",";
json += "\"ntpShowSec\":" + String(ntpShowSec);
json += "}"; json += "}";
server.send(200, "application/json", json); server.send(200, "application/json", json);
} }
@@ -351,6 +510,10 @@ static const char INDEX_HTML[] PROGMEM = R"rawliteral(
<input type='range' id='nbr' min='0' max='255' oninput="f('/nightBrightness?val='+this.value)"> <input type='range' id='nbr' min='0' max='255' oninput="f('/nightBrightness?val='+this.value)">
<input type='checkbox' id='nightBgOff' onchange="f('/nightBgOff?val='+(this.checked?1:0))"> Hintergrund in Nacht aus <input type='checkbox' id='nightBgOff' onchange="f('/nightBgOff?val='+(this.checked?1:0))"> Hintergrund in Nacht aus
</div> </div>
<div class='card'>
<input type='checkbox' id='sl' onchange="f('/statusLeds?val='+(this.checked?1:0))"> <b>Status-Anzeige</b> (NTP gr&uuml;n, RTC gelb, ESP rot)<br><br>
NTP-Anzeige mind. <input type='number' id='nsec' min='1' max='60' style='width:50px' oninput="if(this.value!=='')f('/ntpShowSec?val='+this.value)"> Sekunden
</div>
<div class='card'> <div class='card'>
<label>NTP Server:</label> <label>NTP Server:</label>
<input type='text' id='ntpS' onchange="f('/setNTP?val='+encodeURIComponent(this.value))"> <input type='text' id='ntpS' onchange="f('/setNTP?val='+encodeURIComponent(this.value))">
@@ -367,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;}}; 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('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('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...';})} }).catch(()=>{document.getElementById('status').innerText='Verbindung unterbrochen...';})}
setInterval(u, 2500); u(); setInterval(u, 2500); u();
</script></body></html> </script></body></html>
@@ -397,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 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 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 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 handleBG(){ int v; if(!argInRange(0,1,v)) return; backgroundMode=v; forceInstant=true; updateNeeded=true; sendOK(); }
void handleNTP(){ void handleNTP(){
@@ -422,7 +587,9 @@ void setup(){
pixels.clear(); pixels.clear();
pixels.show(); pixels.show();
settimeofday_cb(onTimeSet);
loadSettings(); loadSettings();
initRtc();
WiFiManager wm; WiFiManager wm;
wm.autoConnect("Wortuhr"); wm.autoConnect("Wortuhr");
@@ -438,6 +605,8 @@ void setup(){
server.on("/nightBrightness", handleNightBrightness); server.on("/nightBrightness", handleNightBrightness);
server.on("/nightBgOff", handleNightBgOff); server.on("/nightBgOff", handleNightBgOff);
server.on("/bg", handleBG); server.on("/bg", handleBG);
server.on("/statusLeds", handleStatusLeds);
server.on("/ntpShowSec", handleNtpShowSec);
server.on("/setNTP", handleNTP); server.on("/setNTP", handleNTP);
server.on("/setTZ", handleTZ); server.on("/setTZ", handleTZ);
server.on("/save", handleSave); server.on("/save", handleSave);
@@ -446,9 +615,14 @@ void setup(){
void loop(){ void loop(){
server.handleClient(); server.handleClient();
updateTimeSources();
if(statusGeaendert()){ forceInstant = true; updateNeeded = true; }
// Erst anzeigen, wenn die Zeit per NTP synchronisiert ist // Ohne bekannte Zeit nur die Statuswoerter anzeigen
if(!timeSynced()) return; if(!timeSynced()){
if(updateNeeded){ updateNeeded = false; forceInstant = false; showStatusOnly(); }
return;
}
time_t now = time(nullptr); time_t now = time(nullptr);
struct tm *t = localtime(&now); struct tm *t = localtime(&now);