#include #include #include #include #include #include #include // ================= LED ================= #define LED_PIN D4 #define NUMPIXELS 110 Adafruit_NeoPixel pixels(NUMPIXELS, LED_PIN, NEO_GRB + NEO_KHZ800); // ================= SERVER & NTP ================= ESP8266WebServer server(80); const char DEFAULT_NTP[] = "de.pool.ntp.org"; const char DEFAULT_TZ[] = "CET-1CEST,M3.5.0/02,M10.5.0/03"; String ntpServer = DEFAULT_NTP; String tzString = DEFAULT_TZ; // ================= SETTINGS ================= #define SETTINGS_MAGIC 0x57554852UL // erkennt gueltige EEPROM-Daten struct Settings { uint32_t magic; int brightness; int nightBrightness; bool autoNight; int nightStart; int nightEnd; uint32_t color; uint32_t bgColor; bool backgroundMode; bool nightBackgroundDisabled; char ntpServer[64]; char tzString[64]; } settings; // ================= VAR ================= uint32_t currentColor = 0x00FFFF; uint32_t bgColor = 0x000000; bool backgroundMode = false; bool nightBackgroundDisabled = false; bool forceInstant = false; bool updateNeeded = true; int brightness = 120; int nightBrightness = 10; bool autoNight = true; int nightStart = 22; int nightEnd = 6; uint32_t targetBuffer[NUMPIXELS]; // Soll-Farben (ohne Helligkeit/Gamma) uint32_t currentBuffer[NUMPIXELS]; // zuletzt angezeigte Soll-Farben int lastMinute = -1; int lastH = -1; // ================= ZEIT ================= bool timeSynced(){ return time(nullptr) > 100000; } // ================= EEPROM ================= void saveSettings(){ settings.magic = SETTINGS_MAGIC; settings.brightness = brightness; settings.nightBrightness = nightBrightness; settings.autoNight = autoNight; settings.nightStart = nightStart; settings.nightEnd = nightEnd; settings.color = currentColor; settings.bgColor = bgColor; settings.backgroundMode = backgroundMode; settings.nightBackgroundDisabled = nightBackgroundDisabled; 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); settings.tzString[sizeof(settings.tzString) - 1] = '\0'; EEPROM.put(0, settings); EEPROM.commit(); } void loadSettings(){ EEPROM.begin(512); EEPROM.get(0, settings); if(settings.magic == SETTINGS_MAGIC){ brightness = constrain(settings.brightness, 0, 255); nightBrightness = constrain(settings.nightBrightness, 0, 255); autoNight = settings.autoNight; nightStart = constrain(settings.nightStart, 0, 23); nightEnd = constrain(settings.nightEnd, 0, 23); currentColor = settings.color & 0xFFFFFF; bgColor = settings.bgColor & 0xFFFFFF; backgroundMode = settings.backgroundMode; nightBackgroundDisabled = settings.nightBackgroundDisabled; settings.ntpServer[sizeof(settings.ntpServer) - 1] = '\0'; settings.tzString[sizeof(settings.tzString) - 1] = '\0'; ntpServer = String(settings.ntpServer); tzString = String(settings.tzString); if(ntpServer.length() == 0) ntpServer = DEFAULT_NTP; if(tzString.length() == 0) tzString = DEFAULT_TZ; } configTime(tzString.c_str(), ntpServer.c_str()); } // ================= HELFER ================= bool isNightTime(struct tm *t){ if(!autoNight) return false; int h = t->tm_hour; if(nightStart > nightEnd){ return (h >= nightStart || h < nightEnd); } else { return (h >= nightStart && h < nightEnd); } } void setRange(int a,int b){ for(int i=a;i<=b;i++) targetBuffer[i]=currentColor; } uint32_t blend(uint32_t c1, uint32_t c2, float t){ uint8_t r1=(c1>>16)&255, g1=(c1>>8)&255, b1=c1&255; uint8_t r2=(c2>>16)&255, g2=(c2>>8)&255, b2=c2&255; return pixels.Color((uint8_t)(r1+(r2-r1)*t + 0.5f), (uint8_t)(g1+(g2-g1)*t + 0.5f), (uint8_t)(b1+(b2-b1)*t + 0.5f)); } uint8_t gamma8(uint8_t x){ float f = (float)x / 255.0f; return (uint8_t)(f * f * 255.0f + 0.5f); } // Skaliert einen Farbkanal mit der Helligkeit und korrigiert Gamma. // Ein aktiver Kanal bleibt bei Helligkeit > 0 mindestens auf 1, damit er nicht erlischt. uint8_t scaleChannel(uint8_t c, float factor){ if(c == 0 || factor <= 0) return 0; uint8_t v = gamma8((uint8_t)(c * factor)); return v ? v : 1; } uint32_t applyGammaBrightness(uint32_t c, float brightnessFactor){ return pixels.Color(scaleChannel((c >> 16) & 255, brightnessFactor), scaleChannel((c >> 8) & 255, brightnessFactor), scaleChannel(c & 255, brightnessFactor)); } int getBrightness(struct tm *t){ return isNightTime(t) ? nightBrightness : brightness; } void showInstant(){ time_t now = time(nullptr); struct tm *t = localtime(&now); float b = getBrightness(t) / 255.0f; for(int i=0;i= 25) h++; const char* arr[] = {"Zwölf","Eins","Zwei","Drei","Vier","Fünf","Sechs","Sieben","Acht","Neun","Zehn","Elf"}; String hName = arr[h%12]; String t = "Es ist "; switch(m5){ case 0: t += (h%12 == 1) ? String("ein") : hName; t += " Uhr"; break; case 5: t += "fünf nach " + hName; break; case 10: t += "zehn nach " + hName; break; case 15: t += "viertel nach " + hName; break; case 20: t += "zwanzig nach " + hName; break; case 25: t += "fünf vor halb " + hName; break; case 30: t += "halb " + hName; break; case 35: t += "fünf nach halb " + hName; break; case 40: t += "zwanzig vor " + hName; break; case 45: t += "viertel vor " + hName; break; case 50: t += "zehn vor " + hName; break; case 55: t += "fünf vor " + hName; break; } return t; } void showTimeWords(int h, int m){ time_t now = time(nullptr); struct tm *t = localtime(&now); // Startpunkt des Fades ist die zuletzt angezeigte Soll-Farbe (ohne Helligkeit/Gamma) for(int i=0;i= 25) h++; switch(m5){ case 0: word_UHR(); break; case 5: word_FUENF_M(); word_NACH(); break; case 10: word_ZEHN_M(); word_NACH(); break; case 15: word_VIERTEL(); word_NACH(); break; case 20: word_ZWANZIG(); word_NACH(); break; case 25: word_FUENF_M(); word_VOR(); word_HALB(); break; case 30: word_HALB(); break; case 35: word_FUENF_M(); word_NACH(); word_HALB(); break; case 40: word_ZWANZIG(); word_VOR(); break; case 45: word_VIERTEL(); word_VOR(); break; case 50: word_ZEHN_M(); word_VOR(); break; case 55: word_FUENF_M(); word_VOR(); break; } int hh = h % 12; if(hh == 1 && full) word_EIN(); else if(hh == 1) word_EINS(); else if(hh == 2) word_ZWEI(); else if(hh == 3) word_DREI(); else if(hh == 4) word_VIER(); else if(hh == 5) word_FUENF_H(); else if(hh == 6) word_SECHS(); 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(); if(forceInstant){ showInstant(); forceInstant = false; } else { fadeToTargetSmooth(20, 10); } } // ================= WEB HANDLERS ================= String jsonEscape(const String &s){ String out; for(unsigned int i = 0; i < s.length(); i++){ char c = s[i]; if(c == '"' || c == '\\'){ out += '\\'; out += c; } else if((uint8_t)c < 0x20) out += ' '; else out += c; } return out; } void sendOK(){ server.send(200, "text/plain", "OK"); } // Liest den Parameter "val" als Zahl; sendet 400 und liefert false bei ungueltigem Wert. bool argInRange(int lo, int hi, int &out){ if(!server.hasArg("val")){ server.send(400, "text/plain", "Fehlender Wert"); return false; } int v = server.arg("val").toInt(); if(v < lo || v > hi){ server.send(400, "text/plain", "Ungueltiger Wert"); return false; } out = v; return true; } void handleStatus(){ time_t now = time(nullptr); struct tm *t = localtime(&now); char hexCol[10], hexBg[10]; snprintf(hexCol, sizeof(hexCol), "#%06X", (unsigned int)currentColor); snprintf(hexBg, sizeof(hexBg), "#%06X", (unsigned int)bgColor); String ntpStat = timeSynced() ? "OK" : "SYNC..."; String json = "{"; json += "\"txt\":\"" + (timeSynced() ? jsonEscape(getTimeText(t->tm_hour, t->tm_min)) : String("Warte auf Zeit...")) + "\","; json += "\"ip\":\"" + WiFi.localIP().toString() + "\","; json += "\"ntp\":\"" + ntpStat + "\","; json += "\"ntpServer\":\"" + jsonEscape(ntpServer) + "\","; json += "\"tzString\":\"" + jsonEscape(tzString) + "\","; json += "\"brightness\":" + String(brightness) + ","; json += "\"nightBrightness\":" + String(nightBrightness) + ","; json += "\"color\":\"" + String(hexCol) + "\","; json += "\"bgColor\":\"" + String(hexBg) + "\","; json += "\"night\":" + String(autoNight ? 1 : 0) + ","; json += "\"nightStart\":" + String(nightStart) + ","; json += "\"nightEnd\":" + String(nightEnd) + ","; json += "\"bg\":" + String(backgroundMode ? 1 : 0) + ","; json += "\"nightBgOff\":" + String(nightBackgroundDisabled ? 1 : 0); json += "}"; server.send(200, "application/json", json); } static const char INDEX_HTML[] PROGMEM = R"rawliteral( Wortuhr
Lade...
Verbinde...
Hintergrund

Nachtmodus

Von: Uhr Bis: Uhr

Hintergrund in Nacht aus
)rawliteral"; void handleRoot(){ server.send_P(200, "text/html", INDEX_HTML); } void handleColor(){ if(server.hasArg("c")){ currentColor = strtoul(server.arg("c").c_str(), NULL, 16) & 0xFFFFFF; forceInstant = true; updateNeeded = true; } sendOK(); } void handleBGColor(){ if(server.hasArg("c")){ bgColor = strtoul(server.arg("c").c_str(), NULL, 16) & 0xFFFFFF; forceInstant = true; updateNeeded = true; } sendOK(); } void handleBrightness(){ int v; if(!argInRange(0,255,v)) return; brightness=v; forceInstant=true; updateNeeded=true; sendOK(); } void handleNight(){ int v; if(!argInRange(0,1,v)) return; autoNight=v; forceInstant=true; updateNeeded=true; sendOK(); } void handleNightStart(){ int v; if(!argInRange(0,23,v)) return; nightStart=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 handleNightBgOff(){ int v; if(!argInRange(0,1,v)) return; nightBackgroundDisabled=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(){ String v = server.arg("val"); v.trim(); if(v.length() == 0 || v.length() >= sizeof(settings.ntpServer)){ server.send(400, "text/plain", "Ungueltiger Wert"); return; } ntpServer = v; configTime(tzString.c_str(), ntpServer.c_str()); sendOK(); } void handleTZ(){ String v = server.arg("val"); v.trim(); if(v.length() == 0 || v.length() >= sizeof(settings.tzString)){ server.send(400, "text/plain", "Ungueltiger Wert"); return; } tzString = v; configTime(tzString.c_str(), ntpServer.c_str()); sendOK(); } void handleSave(){ saveSettings(); sendOK(); } // ================= SETUP & LOOP ================= void setup(){ Serial.begin(115200); pixels.begin(); pixels.clear(); pixels.show(); loadSettings(); WiFiManager wm; wm.autoConnect("Wortuhr"); server.on("/", handleRoot); server.on("/status", handleStatus); server.on("/color", handleColor); server.on("/bgcolor", handleBGColor); server.on("/brightness", handleBrightness); server.on("/night", handleNight); server.on("/nightStart", handleNightStart); server.on("/nightEnd", handleNightEnd); server.on("/nightBrightness", handleNightBrightness); server.on("/nightBgOff", handleNightBgOff); server.on("/bg", handleBG); server.on("/setNTP", handleNTP); server.on("/setTZ", handleTZ); server.on("/save", handleSave); server.begin(); } void loop(){ server.handleClient(); // Erst anzeigen, wenn die Zeit per NTP synchronisiert ist if(!timeSynced()) return; time_t now = time(nullptr); struct tm *t = localtime(&now); int currentM5 = (t->tm_min / 5) * 5; if(currentM5 != lastMinute || t->tm_hour != lastH){ lastMinute = currentM5; lastH = t->tm_hour; updateNeeded = true; } if(updateNeeded){ updateNeeded = false; showTimeWords(t->tm_hour, t->tm_min); } }