Software bereinigt: getrennte Projekte für Arduino IDE und PlatformIO
Fehlerkorrekturen u.a.: ZEHN-Wort (LED 18-21), Fade-Flackern, Nacht-Helligkeit, EEPROM-Erkennung per Magic-Wert, String-Terminierung, Warten auf NTP-Sync, Validierung der Web-Eingaben. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1c79c9f5ef
commit
251d3a20fb
7 files changed
+945
-1064
No files matched your search
@@ -0,0 +1,2 @@
|
|||||||
|
# SSH-Schluessel nie committen
|
||||||
|
.ssh/
|
||||||
@@ -0,0 +1,467 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
#include <ESP8266WebServer.h>
|
||||||
|
#include <Adafruit_NeoPixel.h>
|
||||||
|
#include <EEPROM.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <WiFiManager.h>
|
||||||
|
|
||||||
|
// ================= 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<NUMPIXELS;i++){
|
||||||
|
pixels.setPixelColor(i, applyGammaBrightness(targetBuffer[i], b));
|
||||||
|
}
|
||||||
|
pixels.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
void fadeToTargetSmooth(int steps = 30, int delayMs = 15){
|
||||||
|
time_t now = time(nullptr);
|
||||||
|
struct tm *t = localtime(&now);
|
||||||
|
float bTarget = getBrightness(t) / 255.0f;
|
||||||
|
|
||||||
|
// Im Nachtmodus faden wir langsamer (mehr Schritte), um das Springen zu kaschieren
|
||||||
|
if(isNightTime(t)){
|
||||||
|
steps = 60;
|
||||||
|
delayMs = 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int s = 0; s <= steps; s++){
|
||||||
|
float tBlend = (float)s / steps;
|
||||||
|
for(int i = 0; i < NUMPIXELS; i++){
|
||||||
|
uint32_t c = blend(currentBuffer[i], targetBuffer[i], tBlend);
|
||||||
|
pixels.setPixelColor(i, applyGammaBrightness(c, bTarget));
|
||||||
|
}
|
||||||
|
pixels.show();
|
||||||
|
yield(); // Verhindert WDT-Reset beim ESP8266
|
||||||
|
delay(delayMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= WORTUHR LOGIK =================
|
||||||
|
void word_ES(){ setRange(0,1); }
|
||||||
|
void word_IST(){ setRange(3,5); }
|
||||||
|
void word_FUENF_M(){ setRange(7,10); }
|
||||||
|
void word_ZEHN_M(){ setRange(18,21); }
|
||||||
|
void word_ZWANZIG(){ setRange(11,17); }
|
||||||
|
void word_VIERTEL(){ setRange(22,28); }
|
||||||
|
void word_VOR(){ setRange(30,32); }
|
||||||
|
void word_NACH(){ setRange(40,43); }
|
||||||
|
void word_HALB(){ setRange(33,36); }
|
||||||
|
void word_EINS(){ setRange(62,65); }
|
||||||
|
void word_EIN(){ setRange(62,64); }
|
||||||
|
void word_ZWEI(){ setRange(55,58); }
|
||||||
|
void word_DREI(){ setRange(66,69); }
|
||||||
|
void word_VIER(){ setRange(73,76); }
|
||||||
|
void word_FUENF_H(){ setRange(51,54); }
|
||||||
|
void word_SECHS(){ setRange(83,87); }
|
||||||
|
void word_SIEBEN(){ setRange(88,93); }
|
||||||
|
void word_ACHT(){ setRange(77,80); }
|
||||||
|
void word_NEUN(){ setRange(44,47); }
|
||||||
|
void word_ZEHN_H(){ setRange(106,109); }
|
||||||
|
void word_ELF(){ setRange(103,105); }
|
||||||
|
void word_ZWOELF(){ setRange(94,98); }
|
||||||
|
void word_UHR(){ setRange(99,101); }
|
||||||
|
|
||||||
|
String getTimeText(int h, int m){
|
||||||
|
int m5 = (m/5)*5;
|
||||||
|
if(m5 >= 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<NUMPIXELS;i++) currentBuffer[i] = targetBuffer[i];
|
||||||
|
|
||||||
|
// Hintergrund mit Nacht-Aus-Option
|
||||||
|
uint32_t activeBg = 0;
|
||||||
|
if(backgroundMode && !(nightBackgroundDisabled && isNightTime(t))){
|
||||||
|
activeBg = bgColor;
|
||||||
|
}
|
||||||
|
for(int i=0;i<NUMPIXELS;i++) targetBuffer[i] = activeBg;
|
||||||
|
|
||||||
|
word_ES(); word_IST();
|
||||||
|
int m5 = (m/5)*5; bool full = (m5 == 0); if(m5 >= 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(
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width, initial-scale=1'>
|
||||||
|
<title>Wortuhr</title>
|
||||||
|
<style>
|
||||||
|
body{font-family:Arial,sans-serif; background:#fff; margin:0; color:#333; display:flex; flex-direction:column; align-items:center}
|
||||||
|
.card{width:90%; max-width:400px; margin:10px; padding:15px; border-radius:12px; background:#f2f2f2; box-shadow: 0 2px 5px rgba(0,0,0,0.1); box-sizing:border-box}
|
||||||
|
.big{font-size:26px; text-align:center; font-weight:bold; color:#0055ff}
|
||||||
|
#status{font-size:0.85em; color:#666; text-align:center}
|
||||||
|
input[type='range'], input[type='color'], input[type='text']{width:100%; margin-bottom:10px; box-sizing:border-box}
|
||||||
|
button{width:100%; padding:12px; border-radius:8px; background:#0055ff; color:#fff; border:none; cursor:pointer}
|
||||||
|
label{font-weight:bold; display:block; margin-bottom:5px}
|
||||||
|
</style></head><body>
|
||||||
|
<div class='card big' id='txt'>Lade...</div>
|
||||||
|
<div class='card' id='status'>Verbinde...</div>
|
||||||
|
<div class='card'>
|
||||||
|
<label>Farbe Worte</label>
|
||||||
|
<input type='color' id='col' onchange="f('/color?c='+this.value.substring(1))">
|
||||||
|
<label>Helligkeit</label>
|
||||||
|
<input type='range' id='br' min='0' max='255' oninput="f('/brightness?val='+this.value)">
|
||||||
|
</div>
|
||||||
|
<div class='card'>
|
||||||
|
<input type='checkbox' id='bg' onchange="f('/bg?val='+(this.checked?1:0))"> <b>Hintergrund</b><br><br>
|
||||||
|
<input type='color' id='bgcol' onchange="f('/bgcolor?c='+this.value.substring(1))">
|
||||||
|
</div>
|
||||||
|
<div class='card'>
|
||||||
|
<input type='checkbox' id='night' onchange="f('/night?val='+(this.checked?1:0))"> <b>Nachtmodus</b><br><br>
|
||||||
|
Von: <input type='number' id='nS' min='0' max='23' style='width:50px' oninput="n('/nightStart',this.value)"> Uhr
|
||||||
|
Bis: <input type='number' id='nE' min='0' max='23' style='width:50px' oninput="n('/nightEnd',this.value)"> Uhr<br><br>
|
||||||
|
<label>Nacht-Helligkeit</label>
|
||||||
|
<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
|
||||||
|
</div>
|
||||||
|
<div class='card'>
|
||||||
|
<label>NTP Server:</label>
|
||||||
|
<input type='text' id='ntpS' onchange="f('/setNTP?val='+encodeURIComponent(this.value))">
|
||||||
|
<label>Zeitzone (POSIX TZ):</label>
|
||||||
|
<input type='text' id='tzS' onchange="f('/setTZ?val='+encodeURIComponent(this.value))">
|
||||||
|
</div>
|
||||||
|
<div class='card'><button onclick="f('/save').then(()=>alert('Gespeichert!'))">EEPROM Speichern</button></div>
|
||||||
|
<script>
|
||||||
|
function f(u){return fetch(u)}
|
||||||
|
function n(u,v){ if(v==='') return; f(u+'?val='+Math.min(23,Math.max(0,parseInt(v)||0))); }
|
||||||
|
function u(){fetch('/status').then(r=>r.json()).then(d=>{
|
||||||
|
document.getElementById('txt').innerText=d.txt;
|
||||||
|
document.getElementById('status').innerText="IP: "+d.ip+" | NTP: "+d.ntp;
|
||||||
|
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);
|
||||||
|
}).catch(()=>{document.getElementById('status').innerText='Verbindung unterbrochen...';})}
|
||||||
|
setInterval(u, 2500); u();
|
||||||
|
</script></body></html>
|
||||||
|
)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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,686 +0,0 @@
|
|||||||
#include <ESP8266WiFi.h>
|
|
||||||
#include <ESP8266WebServer.h>
|
|
||||||
#include <Adafruit_NeoPixel.h>
|
|
||||||
#include <EEPROM.h>
|
|
||||||
#include <time.h>
|
|
||||||
#include <WiFiManager.h>
|
|
||||||
|
|
||||||
|
|
||||||
// ================= LED =================
|
|
||||||
#define PIN D4
|
|
||||||
#define NUMPIXELS 110
|
|
||||||
Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
|
|
||||||
|
|
||||||
// ================= SERVER =================
|
|
||||||
ESP8266WebServer server(80);
|
|
||||||
|
|
||||||
// ================= NTP =================
|
|
||||||
String ntpServer = "de.pool.ntp.org";
|
|
||||||
String tzString = "CET-1CEST,M3.5.0/02,M10.5.0/03";
|
|
||||||
|
|
||||||
// ================= SETTINGS =================
|
|
||||||
struct Settings {
|
|
||||||
int brightness;
|
|
||||||
int nightBrightness;
|
|
||||||
bool autoNight;
|
|
||||||
int nightStart;
|
|
||||||
int nightEnd;
|
|
||||||
uint32_t color;
|
|
||||||
uint32_t bgColor;
|
|
||||||
bool backgroundMode;
|
|
||||||
char ntpServer[64];
|
|
||||||
char tzString[64];
|
|
||||||
} settings;
|
|
||||||
|
|
||||||
// ================= VAR =================
|
|
||||||
uint32_t currentColor = 0x00FFFF;
|
|
||||||
uint32_t lastColor = 0x00FFFF;
|
|
||||||
uint32_t bgColor = 0x000000;
|
|
||||||
bool backgroundMode = 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];
|
|
||||||
uint32_t currentBuffer[NUMPIXELS];
|
|
||||||
|
|
||||||
int lastMinute = -1;
|
|
||||||
|
|
||||||
// ================= EEPROM =================
|
|
||||||
void saveSettings(){
|
|
||||||
settings.brightness = brightness;
|
|
||||||
settings.nightBrightness = nightBrightness;
|
|
||||||
settings.autoNight = autoNight;
|
|
||||||
settings.nightStart = nightStart;
|
|
||||||
settings.nightEnd = nightEnd;
|
|
||||||
settings.color = currentColor;
|
|
||||||
settings.bgColor = bgColor;
|
|
||||||
settings.backgroundMode = backgroundMode;
|
|
||||||
strncpy(settings.ntpServer, ntpServer.c_str(), sizeof(settings.ntpServer));
|
|
||||||
strncpy(settings.tzString, tzString.c_str(), sizeof(settings.tzString));
|
|
||||||
|
|
||||||
EEPROM.put(0, settings);
|
|
||||||
EEPROM.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void loadSettings(){
|
|
||||||
EEPROM.begin(512);
|
|
||||||
EEPROM.get(0, settings);
|
|
||||||
|
|
||||||
// Prüfen, ob EEPROM leer ist (über Helligkeit)
|
|
||||||
if(settings.brightness == 0 || settings.brightness == 255){
|
|
||||||
// Standardwerte setzen
|
|
||||||
brightness = 120;
|
|
||||||
nightBrightness = 10;
|
|
||||||
autoNight = true;
|
|
||||||
nightStart = 22;
|
|
||||||
nightEnd = 6;
|
|
||||||
currentColor = 0x00FFFF;
|
|
||||||
bgColor = 0x000000;
|
|
||||||
backgroundMode = false;
|
|
||||||
ntpServer = "de.pool.ntp.org";
|
|
||||||
tzString = "CET-1CEST,M3.5.0/02,M10.5.0/03";
|
|
||||||
} else {
|
|
||||||
brightness = settings.brightness;
|
|
||||||
nightBrightness = settings.nightBrightness;
|
|
||||||
autoNight = settings.autoNight;
|
|
||||||
nightStart = settings.nightStart;
|
|
||||||
nightEnd = settings.nightEnd;
|
|
||||||
currentColor = settings.color;
|
|
||||||
bgColor = settings.bgColor;
|
|
||||||
backgroundMode = settings.backgroundMode;
|
|
||||||
ntpServer = String(settings.ntpServer);
|
|
||||||
tzString = String(settings.tzString);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Zeitkonfiguration sofort anwenden
|
|
||||||
configTime(tzString.c_str(), ntpServer.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ================= HELFER =================
|
|
||||||
void clearTarget(){
|
|
||||||
for(int i=0;i<NUMPIXELS;i++) targetBuffer[i]=0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setRange(int a,int b){
|
|
||||||
for(int i=a;i<=b;i++) targetBuffer[i]=currentColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setRangeColor(int a,int b, uint32_t col){
|
|
||||||
for(int i=a;i<=b;i++) targetBuffer[i] = col;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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(
|
|
||||||
r1 + (r2 - r1) * t,
|
|
||||||
g1 + (g2 - g1) * t,
|
|
||||||
b1 + (b2 - b1) * t
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t applyBrightness(uint32_t c, float brightnessFactor){
|
|
||||||
uint8_t r = ((c >> 16) & 255) * brightnessFactor;
|
|
||||||
uint8_t g = ((c >> 8) & 255) * brightnessFactor;
|
|
||||||
uint8_t b = (c & 255) * brightnessFactor;
|
|
||||||
|
|
||||||
return pixels.Color(r,g,b);
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t gamma8(uint8_t x){
|
|
||||||
if(x<5) return 0;
|
|
||||||
float f = x / 255.0;
|
|
||||||
f = pow(f, 2.2); // Gamma 2.2
|
|
||||||
return (uint8_t)(f * 255);
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t applyGammaBrightness(uint32_t c, float brightnessFactor){
|
|
||||||
|
|
||||||
uint8_t r = ((c >> 16) & 255) * brightnessFactor;
|
|
||||||
uint8_t g = ((c >> 8) & 255) * brightnessFactor;
|
|
||||||
uint8_t b = (c & 255) * brightnessFactor;
|
|
||||||
|
|
||||||
// 🔥 Gamma Korrektur
|
|
||||||
r = gamma8(r);
|
|
||||||
g = gamma8(g);
|
|
||||||
b = gamma8(b);
|
|
||||||
|
|
||||||
return pixels.Color(r,g,b);
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateDisplayNow(){
|
|
||||||
time_t now = time(nullptr);
|
|
||||||
struct tm *t = localtime(&now);
|
|
||||||
if(t){
|
|
||||||
showTimeWords(t->tm_hour, t->tm_min);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void showInstant(){
|
|
||||||
|
|
||||||
time_t now = time(nullptr);
|
|
||||||
struct tm *t = localtime(&now);
|
|
||||||
|
|
||||||
float b = getBrightness(t) / 255.0;
|
|
||||||
|
|
||||||
for(int i=0;i<NUMPIXELS;i++){
|
|
||||||
uint32_t c = applyGammaBrightness(targetBuffer[i], b);
|
|
||||||
pixels.setPixelColor(i, c);
|
|
||||||
}
|
|
||||||
|
|
||||||
pixels.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= WORTUHR =================
|
|
||||||
|
|
||||||
// ES IST
|
|
||||||
void word_ES(){ setRange(0,1); }
|
|
||||||
void word_IST(){ setRange(3,5); }
|
|
||||||
|
|
||||||
// Minuten
|
|
||||||
void word_FUENF_M(){ setRange(7,10); }
|
|
||||||
void word_ZEHN_M(){ setRange(18,20); }
|
|
||||||
void word_ZWANZIG(){ setRange(11,17); }
|
|
||||||
void word_VIERTEL(){ setRange(22,28); }
|
|
||||||
void word_VOR(){ setRange(30,32); }
|
|
||||||
void word_NACH(){ setRange(40,43); }
|
|
||||||
void word_HALB(){ setRange(33,36); }
|
|
||||||
|
|
||||||
// Stunden
|
|
||||||
void word_EINS(){ setRange(62,65); }
|
|
||||||
void word_EIN(){ setRange(62,64); }
|
|
||||||
void word_ZWEI(){ setRange(55,58); }
|
|
||||||
void word_DREI(){ setRange(66,69); }
|
|
||||||
void word_VIER(){ setRange(73,76); }
|
|
||||||
void word_FUENF_H(){ setRange(51,54); }
|
|
||||||
void word_SECHS(){ setRange(83,87); }
|
|
||||||
void word_SIEBEN(){ setRange(88,93); }
|
|
||||||
void word_ACHT(){ setRange(77,80); }
|
|
||||||
void word_NEUN(){ setRange(44,47); }
|
|
||||||
void word_ZEHN_H(){ setRange(106,109); }
|
|
||||||
void word_ELF(){ setRange(103,105); }
|
|
||||||
void word_ZWOELF(){ setRange(94,98); }
|
|
||||||
|
|
||||||
// UHR
|
|
||||||
void word_UHR(){ setRange(99,101); }
|
|
||||||
|
|
||||||
// ================= TEXT =================
|
|
||||||
String hourToText(int h){
|
|
||||||
String arr[]={"Zwölf","Eins","Zwei","Drei","Vier","Fünf","Sechs","Sieben","Acht","Neun","Zehn","Elf"};
|
|
||||||
return arr[h%12];
|
|
||||||
}
|
|
||||||
|
|
||||||
String hourText(int h,bool full){
|
|
||||||
int hh = h % 12;
|
|
||||||
|
|
||||||
if(hh == 1){
|
|
||||||
return full ? "ein" : "eins";
|
|
||||||
}
|
|
||||||
return hourToText(h);
|
|
||||||
}
|
|
||||||
|
|
||||||
String getTimeText(int h,int m){
|
|
||||||
int m5=(m/5)*5;
|
|
||||||
if(m5>=25) h++;
|
|
||||||
|
|
||||||
String t="Es ist ";
|
|
||||||
|
|
||||||
switch(m5){
|
|
||||||
case 0: t+= hourText(h, true)+" Uhr"; break;
|
|
||||||
case 5: t+="fünf nach "+hourToText(h); break;
|
|
||||||
case 10: t+="zehn nach "+hourToText(h); break;
|
|
||||||
case 15: t+="viertel nach "+hourToText(h); break;
|
|
||||||
case 20: t+="zwanzig nach "+hourToText(h); break;
|
|
||||||
case 25: t+="fünf vor halb "+hourToText(h); break;
|
|
||||||
case 30: t+="halb "+hourToText(h); break;
|
|
||||||
case 35: t+="fünf nach halb "+hourToText(h); break;
|
|
||||||
case 40: t+="zwanzig vor "+hourToText(h); break;
|
|
||||||
case 45: t+="viertel vor "+hourToText(h); break;
|
|
||||||
case 50: t+="zehn vor "+hourToText(h); break;
|
|
||||||
case 55: t+="fünf vor "+hourToText(h); break;
|
|
||||||
}
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= NACHTMODUS =================
|
|
||||||
int getBrightness(struct tm *t){
|
|
||||||
if(!autoNight) return brightness;
|
|
||||||
|
|
||||||
int h=t->tm_hour;
|
|
||||||
if(nightStart > nightEnd){
|
|
||||||
if(h >= nightStart || h < nightEnd) return nightBrightness;
|
|
||||||
} else {
|
|
||||||
if(h >= nightStart && h < nightEnd) return nightBrightness;
|
|
||||||
}
|
|
||||||
return brightness;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void fadeToTargetSmooth(int steps = 10, int delayMs = 5){
|
|
||||||
|
|
||||||
time_t now = time(nullptr);
|
|
||||||
struct tm *t = localtime(&now);
|
|
||||||
|
|
||||||
float bTarget = getBrightness(t) / 255.0;
|
|
||||||
|
|
||||||
for(int s = 0; s <= steps; s++){
|
|
||||||
float tBlend = (float)s / steps;
|
|
||||||
|
|
||||||
for(int i=0;i<NUMPIXELS;i++){
|
|
||||||
|
|
||||||
uint32_t c = blend(currentBuffer[i], targetBuffer[i], tBlend);
|
|
||||||
|
|
||||||
uint32_t finalColor = applyGammaBrightness(c, bTarget);
|
|
||||||
|
|
||||||
pixels.setPixelColor(i, finalColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
pixels.show();
|
|
||||||
delay(delayMs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ================= ANZEIGE =================
|
|
||||||
void showTimeWords(int h, int m){
|
|
||||||
|
|
||||||
// 🔥 aktuellen Zustand als Start für Fade holen
|
|
||||||
for(int i=0;i<NUMPIXELS;i++){
|
|
||||||
currentBuffer[i] = pixels.getPixelColor(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🔥 Zielbuffer komplett neu aufbauen
|
|
||||||
for(int i=0;i<NUMPIXELS;i++){
|
|
||||||
if(backgroundMode){
|
|
||||||
targetBuffer[i] = bgColor; // frei wählbarer Hintergrund
|
|
||||||
} else {
|
|
||||||
targetBuffer[i] = 0; // aus
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grundtext
|
|
||||||
word_ES();
|
|
||||||
word_IST();
|
|
||||||
|
|
||||||
int m5 = (m/5)*5;
|
|
||||||
bool full = (m5 == 0);
|
|
||||||
|
|
||||||
if(m5 >= 25) h++;
|
|
||||||
|
|
||||||
// Minuten
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stunden
|
|
||||||
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();
|
|
||||||
|
|
||||||
// 🔥 jetzt alles weich darstellen (inkl. Gamma & Brightness)
|
|
||||||
if(forceInstant){
|
|
||||||
showInstant();
|
|
||||||
forceInstant = false;
|
|
||||||
}else{
|
|
||||||
fadeToTargetSmooth(10,5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ================= WEB =================
|
|
||||||
void handleNTP() {
|
|
||||||
if (server.hasArg("val")) {
|
|
||||||
ntpServer = server.arg("val");
|
|
||||||
// Konfiguration sofort mit neuem Server aktualisieren
|
|
||||||
configTime(tzString.c_str(), ntpServer.c_str());
|
|
||||||
Serial.println("NTP Server geändert: " + ntpServer);
|
|
||||||
}
|
|
||||||
server.send(200, "text/plain", "OK");
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleTZ() {
|
|
||||||
if (server.hasArg("val")) {
|
|
||||||
tzString = server.arg("val");
|
|
||||||
// Konfiguration sofort mit neuer Zeitzone aktualisieren
|
|
||||||
configTime(tzString.c_str(), ntpServer.c_str());
|
|
||||||
Serial.println("Zeitzone geändert: " + tzString);
|
|
||||||
}
|
|
||||||
server.send(200, "text/plain", "OK");
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleColor(){
|
|
||||||
if(server.hasArg("c")){
|
|
||||||
lastColor=currentColor;
|
|
||||||
currentColor=strtoul(server.arg("c").c_str(),NULL,16);
|
|
||||||
forceInstant = true;
|
|
||||||
updateDisplayNow();
|
|
||||||
|
|
||||||
}
|
|
||||||
server.send(200,"text/plain","OK");
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleBGColor(){
|
|
||||||
if(server.hasArg("c")){
|
|
||||||
bgColor=strtoul(server.arg("c").c_str(),NULL,16);
|
|
||||||
forceInstant = true;
|
|
||||||
updateNeeded = true;
|
|
||||||
}
|
|
||||||
server.send(200,"text/plain","OK");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void handleBrightness(){
|
|
||||||
brightness=server.arg("val").toInt();
|
|
||||||
forceInstant = true;
|
|
||||||
updateNeeded = true;
|
|
||||||
server.send(200,"text/plain","OK");
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleNight(){
|
|
||||||
autoNight=server.arg("val").toInt();
|
|
||||||
forceInstant = true;
|
|
||||||
updateNeeded = true;
|
|
||||||
server.send(200,"text/plain","OK");
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleBG(){
|
|
||||||
backgroundMode=server.arg("val").toInt();
|
|
||||||
forceInstant = true;
|
|
||||||
updateNeeded = true;
|
|
||||||
server.send(200,"text/plain","OK");
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleSave(){
|
|
||||||
saveSettings();
|
|
||||||
server.send(200,"text/plain","saved");
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleNightStart(){
|
|
||||||
int val = server.arg("val").toInt();
|
|
||||||
if(val >= 0 && val <= 23) {
|
|
||||||
nightStart = val;
|
|
||||||
forceInstant = true;
|
|
||||||
updateNeeded = true;
|
|
||||||
server.send(200, "text/plain", "OK");
|
|
||||||
} else {
|
|
||||||
server.send(400, "text/plain", "Ungueltiger Wert");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleNightEnd(){
|
|
||||||
int val = server.arg("val").toInt();
|
|
||||||
|
|
||||||
if(val >= 0 && val <= 23) {
|
|
||||||
nightEnd = val;
|
|
||||||
forceInstant = true;
|
|
||||||
updateNeeded = true;
|
|
||||||
server.send(200, "text/plain", "OK");
|
|
||||||
} else {
|
|
||||||
server.send(400, "text/plain", "Ungueltiger Wert");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleNightBrightness(){
|
|
||||||
nightBrightness = server.arg("val").toInt();
|
|
||||||
forceInstant = true;
|
|
||||||
updateNeeded = true;
|
|
||||||
server.send(200, "text/plain", "OK");
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleStatus() {
|
|
||||||
time_t now = time(nullptr);
|
|
||||||
struct tm *t = localtime(&now);
|
|
||||||
char hexCol[10];
|
|
||||||
snprintf(hexCol, sizeof(hexCol), "#%06X", currentColor);
|
|
||||||
char hexBg[10];
|
|
||||||
snprintf(hexBg, sizeof(hexBg), "#%06X",bgColor);
|
|
||||||
|
|
||||||
String ntpStat = (now > 100000) ? "OK" : "SYNC...";
|
|
||||||
Serial.println(ntpStat);
|
|
||||||
String json = "{";
|
|
||||||
json += "\"txt\":\"" + getTimeText(t->tm_hour, t->tm_min) + "\",";
|
|
||||||
json += "\"ip\":\"" + WiFi.localIP().toString() + "\",";
|
|
||||||
json += "\"ntp\":\"" + ntpStat + "\",";
|
|
||||||
json += "\"ntpServer\":\"" + ntpServer + "\",";
|
|
||||||
json += "\"tzString\":\"" + 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); // Hier KEIN Komma am Ende
|
|
||||||
json += "}";
|
|
||||||
|
|
||||||
server.send(200, "application/json", json);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= HTML =================
|
|
||||||
void handleRoot(){
|
|
||||||
String html = R"rawliteral(
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset='UTF-8'>
|
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1'>
|
|
||||||
<style>
|
|
||||||
body{font-family:Arial,sans-serif; background:#fff; margin:0; color:#333; display:flex; flex-direction:column; align-items:center}
|
|
||||||
.card{width:90%; max-width:400px; margin:10px; padding:15px; border-radius:12px; background:#f2f2f2; box-shadow: 0 2px 5px rgba(0,0,0,0.1)}
|
|
||||||
.big{font-size:26px; text-align:center; font-weight:bold; color:#0055ff}
|
|
||||||
#status{font-size:0.85em; color:#666; text-align:center}
|
|
||||||
.grid{display:flex; gap:10px; align-items:center; margin-top:10px}
|
|
||||||
.sub-label{font-size:0.85em; margin-top:10px; display:block; color:#555}
|
|
||||||
input[type='number']{width:55px; padding:8px; border-radius:6px; border:1px solid #ccc; text-align:center}
|
|
||||||
input[type='range'], input[type='color']{width:100%; cursor:pointer}
|
|
||||||
button{width:100%; padding:12px; border-radius:8px; border:none; background:#0055ff; color:#fff; font-weight:bold; cursor:pointer}
|
|
||||||
label{font-weight:bold; display:block; margin-bottom:5px}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div class='card big' id='txt'>--:--</div>
|
|
||||||
<div class='card' id='status'>Verbinde...</div>
|
|
||||||
|
|
||||||
<div class='card'>
|
|
||||||
<label>Wort-LED Farbe</label>
|
|
||||||
<input type='color' id='col' onchange='c(this.value)'>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class='card'>
|
|
||||||
<label>Standard Helligkeit</label>
|
|
||||||
<input type='range' id='br' min='0' max='255' oninput='b(this.value)'>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class='card'>
|
|
||||||
<input type='checkbox' id='night' onchange='n(this.checked)'> <b>Nachtmodus (Auto)</b>
|
|
||||||
|
|
||||||
<span class='sub-label'>Zeitraum:</span>
|
|
||||||
<div class='grid'>
|
|
||||||
Von: <input type='number' id='nS' min='0' max='23' oninput='ns(this.value)' autocomplete='off'> Uhr
|
|
||||||
Bis: <input type='number' id='nE' min='0' max='23' oninput='ne(this.value)' autocomplete='off'> Uhr
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span class='sub-label'>Nacht-Helligkeit:</span>
|
|
||||||
<input type='range' id='nbr' min='0' max='255' oninput='nb(this.value)'>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class='card'>
|
|
||||||
<input type='checkbox' id='bg' onchange='bgf(this.checked)'> <b>Hintergrund</b>
|
|
||||||
<span class='sub-label'>Farbe</span>
|
|
||||||
<input type='color' id='bgcol' onchange='bgc(this.value)'>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class='card'>
|
|
||||||
<label>Zeit-Quellen</label>
|
|
||||||
<span class='sub-label'>NTP Server:</span>
|
|
||||||
<input type='text' id='ntpS' onchange='setNTP(this.value)' style='width:100%'>
|
|
||||||
|
|
||||||
<span class='sub-label'>Zeitzone (POSIX TZ):</span>
|
|
||||||
<input type='text' id='tzS' onchange='setTZ(this.value)' style='width:100%'>
|
|
||||||
<a href='https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv' target='_blank' style='font-size:0.7em'>TZ-Datenbank Hilfe</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class='card'>
|
|
||||||
<button onclick='save()'>Einstellungen im EEPROM speichern</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function c(v){fetch('/color?c='+v.substring(1))}
|
|
||||||
function bgc(v){fetch('/bgcolor?c='+v.substring(1))}
|
|
||||||
function b(v){fetch('/brightness?val='+v)}
|
|
||||||
function n(v){fetch('/night?val='+(v?1:0))}
|
|
||||||
function nb(v){fetch('/nightBrightness?val='+v)} // NEU: Nacht-Helligkeit
|
|
||||||
function bgf(v){fetch('/bg?val='+(v?1:0))}
|
|
||||||
function save(){fetch('/save').then(()=>alert('Gespeichert!'))}
|
|
||||||
function setNTP(v){ fetch('/setNTP?val=' + encodeURIComponent(v)); }
|
|
||||||
function setTZ(v){ fetch('/setTZ?val=' + encodeURIComponent(v)); }
|
|
||||||
function setNTP(v){ fetch('/setNTP?val=' + encodeURIComponent(v)); }
|
|
||||||
function setTZ(v){ fetch('/setTZ?val=' + encodeURIComponent(v)); }
|
|
||||||
|
|
||||||
function ns(v){
|
|
||||||
if(v === "") return; // Warten, bis eine Zahl drin steht
|
|
||||||
let val = parseInt(v);
|
|
||||||
if(val < 0) val = 0; if(val > 23) val = 23;
|
|
||||||
fetch('/nightStart?val=' + val);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ne(v){
|
|
||||||
if(v === "") return;
|
|
||||||
let val = parseInt(v);
|
|
||||||
if(val < 0) val = 0; if(val > 23) val = 23;
|
|
||||||
fetch('/nightEnd?val=' + val);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function update(){
|
|
||||||
fetch('/status').then(r=>r.json()).then(d=>{
|
|
||||||
// Hilfsfunktion zum sicheren Setzen von Werten
|
|
||||||
const setVal = (id, val, isCheck = false) => {
|
|
||||||
const el = document.getElementById(id);
|
|
||||||
if(!el) return; // Falls Element nicht existiert, überspringen
|
|
||||||
if(document.activeElement === el) return; // Falls User gerade tippt, überspringen
|
|
||||||
|
|
||||||
if(isCheck) el.checked = (val == 1);
|
|
||||||
else el.value = val;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Texte immer aktualisieren
|
|
||||||
const txt = document.getElementById('txt');
|
|
||||||
const stat = document.getElementById('status');
|
|
||||||
if(txt) txt.innerHTML = d.txt;
|
|
||||||
if(stat) stat.innerHTML = "IP: " + d.ip + " | NTP: " + d.ntp;
|
|
||||||
|
|
||||||
// Alle Eingabefelder sicher aktualisieren
|
|
||||||
setVal('col', d.color);
|
|
||||||
setVal('bgcol', d.bgColor);
|
|
||||||
setVal('br', d.brightness);
|
|
||||||
setVal('nbr', d.nightBrightness);
|
|
||||||
setVal('nS', d.nightStart);
|
|
||||||
setVal('nE', d.nightEnd);
|
|
||||||
setVal('ntpS', d.ntpServer);
|
|
||||||
setVal('tzS', d.tzString);
|
|
||||||
|
|
||||||
// Checkboxen
|
|
||||||
setVal('night', d.night, true);
|
|
||||||
setVal('bg', d.bg, true);
|
|
||||||
|
|
||||||
}).catch(err => {
|
|
||||||
console.error("Update-Fehler:", err);
|
|
||||||
const stat = document.getElementById('status');
|
|
||||||
if(stat) stat.innerHTML = "Verbindung unterbrochen...";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setInterval(update, 2000);
|
|
||||||
update();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
)rawliteral";
|
|
||||||
server.send(200, "text/html", html);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ================= SETUP =================
|
|
||||||
void setup(){
|
|
||||||
Serial.begin(115200);
|
|
||||||
|
|
||||||
WiFiManager wm;
|
|
||||||
wm.autoConnect("Wortuhr");
|
|
||||||
|
|
||||||
configTime(tzString.c_str(), ntpServer.c_str());
|
|
||||||
for(int i=0;i<NUMPIXELS;i++){
|
|
||||||
currentBuffer[i] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pixels.begin();
|
|
||||||
loadSettings();
|
|
||||||
|
|
||||||
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("/bg",handleBG);
|
|
||||||
server.on("/save",handleSave);
|
|
||||||
server.on("/setNTP", handleNTP);
|
|
||||||
server.on("/setTZ", handleTZ);
|
|
||||||
|
|
||||||
server.begin();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= LOOP =================
|
|
||||||
void loop(){
|
|
||||||
server.handleClient();
|
|
||||||
|
|
||||||
time_t now=time(nullptr);
|
|
||||||
struct tm *t=localtime(&now);
|
|
||||||
|
|
||||||
if(t && t->tm_min !=lastMinute){
|
|
||||||
lastMinute=t->tm_min;
|
|
||||||
updateNeeded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(updateNeeded){
|
|
||||||
showTimeWords(t->tm_hour,t->tm_min);
|
|
||||||
updateNeeded = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.pio/
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[env:d1_mini]
|
||||||
|
platform = espressif8266
|
||||||
|
board = d1_mini
|
||||||
|
framework = arduino
|
||||||
|
monitor_speed = 115200
|
||||||
|
lib_deps =
|
||||||
|
adafruit/Adafruit NeoPixel
|
||||||
|
tzapu/WiFiManager
|
||||||
@@ -0,0 +1,467 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
#include <ESP8266WebServer.h>
|
||||||
|
#include <Adafruit_NeoPixel.h>
|
||||||
|
#include <EEPROM.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <WiFiManager.h>
|
||||||
|
|
||||||
|
// ================= 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<NUMPIXELS;i++){
|
||||||
|
pixels.setPixelColor(i, applyGammaBrightness(targetBuffer[i], b));
|
||||||
|
}
|
||||||
|
pixels.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
void fadeToTargetSmooth(int steps = 30, int delayMs = 15){
|
||||||
|
time_t now = time(nullptr);
|
||||||
|
struct tm *t = localtime(&now);
|
||||||
|
float bTarget = getBrightness(t) / 255.0f;
|
||||||
|
|
||||||
|
// Im Nachtmodus faden wir langsamer (mehr Schritte), um das Springen zu kaschieren
|
||||||
|
if(isNightTime(t)){
|
||||||
|
steps = 60;
|
||||||
|
delayMs = 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int s = 0; s <= steps; s++){
|
||||||
|
float tBlend = (float)s / steps;
|
||||||
|
for(int i = 0; i < NUMPIXELS; i++){
|
||||||
|
uint32_t c = blend(currentBuffer[i], targetBuffer[i], tBlend);
|
||||||
|
pixels.setPixelColor(i, applyGammaBrightness(c, bTarget));
|
||||||
|
}
|
||||||
|
pixels.show();
|
||||||
|
yield(); // Verhindert WDT-Reset beim ESP8266
|
||||||
|
delay(delayMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= WORTUHR LOGIK =================
|
||||||
|
void word_ES(){ setRange(0,1); }
|
||||||
|
void word_IST(){ setRange(3,5); }
|
||||||
|
void word_FUENF_M(){ setRange(7,10); }
|
||||||
|
void word_ZEHN_M(){ setRange(18,21); }
|
||||||
|
void word_ZWANZIG(){ setRange(11,17); }
|
||||||
|
void word_VIERTEL(){ setRange(22,28); }
|
||||||
|
void word_VOR(){ setRange(30,32); }
|
||||||
|
void word_NACH(){ setRange(40,43); }
|
||||||
|
void word_HALB(){ setRange(33,36); }
|
||||||
|
void word_EINS(){ setRange(62,65); }
|
||||||
|
void word_EIN(){ setRange(62,64); }
|
||||||
|
void word_ZWEI(){ setRange(55,58); }
|
||||||
|
void word_DREI(){ setRange(66,69); }
|
||||||
|
void word_VIER(){ setRange(73,76); }
|
||||||
|
void word_FUENF_H(){ setRange(51,54); }
|
||||||
|
void word_SECHS(){ setRange(83,87); }
|
||||||
|
void word_SIEBEN(){ setRange(88,93); }
|
||||||
|
void word_ACHT(){ setRange(77,80); }
|
||||||
|
void word_NEUN(){ setRange(44,47); }
|
||||||
|
void word_ZEHN_H(){ setRange(106,109); }
|
||||||
|
void word_ELF(){ setRange(103,105); }
|
||||||
|
void word_ZWOELF(){ setRange(94,98); }
|
||||||
|
void word_UHR(){ setRange(99,101); }
|
||||||
|
|
||||||
|
String getTimeText(int h, int m){
|
||||||
|
int m5 = (m/5)*5;
|
||||||
|
if(m5 >= 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<NUMPIXELS;i++) currentBuffer[i] = targetBuffer[i];
|
||||||
|
|
||||||
|
// Hintergrund mit Nacht-Aus-Option
|
||||||
|
uint32_t activeBg = 0;
|
||||||
|
if(backgroundMode && !(nightBackgroundDisabled && isNightTime(t))){
|
||||||
|
activeBg = bgColor;
|
||||||
|
}
|
||||||
|
for(int i=0;i<NUMPIXELS;i++) targetBuffer[i] = activeBg;
|
||||||
|
|
||||||
|
word_ES(); word_IST();
|
||||||
|
int m5 = (m/5)*5; bool full = (m5 == 0); if(m5 >= 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(
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width, initial-scale=1'>
|
||||||
|
<title>Wortuhr</title>
|
||||||
|
<style>
|
||||||
|
body{font-family:Arial,sans-serif; background:#fff; margin:0; color:#333; display:flex; flex-direction:column; align-items:center}
|
||||||
|
.card{width:90%; max-width:400px; margin:10px; padding:15px; border-radius:12px; background:#f2f2f2; box-shadow: 0 2px 5px rgba(0,0,0,0.1); box-sizing:border-box}
|
||||||
|
.big{font-size:26px; text-align:center; font-weight:bold; color:#0055ff}
|
||||||
|
#status{font-size:0.85em; color:#666; text-align:center}
|
||||||
|
input[type='range'], input[type='color'], input[type='text']{width:100%; margin-bottom:10px; box-sizing:border-box}
|
||||||
|
button{width:100%; padding:12px; border-radius:8px; background:#0055ff; color:#fff; border:none; cursor:pointer}
|
||||||
|
label{font-weight:bold; display:block; margin-bottom:5px}
|
||||||
|
</style></head><body>
|
||||||
|
<div class='card big' id='txt'>Lade...</div>
|
||||||
|
<div class='card' id='status'>Verbinde...</div>
|
||||||
|
<div class='card'>
|
||||||
|
<label>Farbe Worte</label>
|
||||||
|
<input type='color' id='col' onchange="f('/color?c='+this.value.substring(1))">
|
||||||
|
<label>Helligkeit</label>
|
||||||
|
<input type='range' id='br' min='0' max='255' oninput="f('/brightness?val='+this.value)">
|
||||||
|
</div>
|
||||||
|
<div class='card'>
|
||||||
|
<input type='checkbox' id='bg' onchange="f('/bg?val='+(this.checked?1:0))"> <b>Hintergrund</b><br><br>
|
||||||
|
<input type='color' id='bgcol' onchange="f('/bgcolor?c='+this.value.substring(1))">
|
||||||
|
</div>
|
||||||
|
<div class='card'>
|
||||||
|
<input type='checkbox' id='night' onchange="f('/night?val='+(this.checked?1:0))"> <b>Nachtmodus</b><br><br>
|
||||||
|
Von: <input type='number' id='nS' min='0' max='23' style='width:50px' oninput="n('/nightStart',this.value)"> Uhr
|
||||||
|
Bis: <input type='number' id='nE' min='0' max='23' style='width:50px' oninput="n('/nightEnd',this.value)"> Uhr<br><br>
|
||||||
|
<label>Nacht-Helligkeit</label>
|
||||||
|
<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
|
||||||
|
</div>
|
||||||
|
<div class='card'>
|
||||||
|
<label>NTP Server:</label>
|
||||||
|
<input type='text' id='ntpS' onchange="f('/setNTP?val='+encodeURIComponent(this.value))">
|
||||||
|
<label>Zeitzone (POSIX TZ):</label>
|
||||||
|
<input type='text' id='tzS' onchange="f('/setTZ?val='+encodeURIComponent(this.value))">
|
||||||
|
</div>
|
||||||
|
<div class='card'><button onclick="f('/save').then(()=>alert('Gespeichert!'))">EEPROM Speichern</button></div>
|
||||||
|
<script>
|
||||||
|
function f(u){return fetch(u)}
|
||||||
|
function n(u,v){ if(v==='') return; f(u+'?val='+Math.min(23,Math.max(0,parseInt(v)||0))); }
|
||||||
|
function u(){fetch('/status').then(r=>r.json()).then(d=>{
|
||||||
|
document.getElementById('txt').innerText=d.txt;
|
||||||
|
document.getElementById('status').innerText="IP: "+d.ip+" | NTP: "+d.ntp;
|
||||||
|
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);
|
||||||
|
}).catch(()=>{document.getElementById('status').innerText='Verbindung unterbrochen...';})}
|
||||||
|
setInterval(u, 2500); u();
|
||||||
|
</script></body></html>
|
||||||
|
)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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,378 +0,0 @@
|
|||||||
#include <ESP8266WiFi.h>
|
|
||||||
#include <ESP8266WebServer.h>
|
|
||||||
#include <Adafruit_NeoPixel.h>
|
|
||||||
#include <EEPROM.h>
|
|
||||||
#include <time.h>
|
|
||||||
#include <WiFiManager.h>
|
|
||||||
|
|
||||||
// ================= LED =================
|
|
||||||
#define PIN D4
|
|
||||||
#define NUMPIXELS 110
|
|
||||||
Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
|
|
||||||
|
|
||||||
// ================= SERVER & NTP =================
|
|
||||||
ESP8266WebServer server(80);
|
|
||||||
String ntpServer = "de.pool.ntp.org";
|
|
||||||
String tzString = "CET-1CEST,M3.5.0/02,M10.5.0/03";
|
|
||||||
|
|
||||||
// ================= SETTINGS =================
|
|
||||||
struct Settings {
|
|
||||||
int brightness;
|
|
||||||
int nightBrightness;
|
|
||||||
bool autoNight;
|
|
||||||
int nightStart;
|
|
||||||
int nightEnd;
|
|
||||||
uint32_t color;
|
|
||||||
uint32_t bgColor;
|
|
||||||
bool backgroundMode;
|
|
||||||
char ntpServer[64];
|
|
||||||
char tzString[64];
|
|
||||||
bool nightBackgroundDisabled; // Neu
|
|
||||||
} settings;
|
|
||||||
|
|
||||||
// ================= VAR =================
|
|
||||||
uint32_t currentColor = 0x00FFFF;
|
|
||||||
uint32_t lastColor = 0x00FFFF;
|
|
||||||
uint32_t bgColor = 0x000000;
|
|
||||||
bool backgroundMode = false;
|
|
||||||
bool nightBackgroundDisabled = false; // Neu
|
|
||||||
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];
|
|
||||||
uint32_t currentBuffer[NUMPIXELS];
|
|
||||||
int lastMinute = -1;
|
|
||||||
int lastH = -1;
|
|
||||||
|
|
||||||
// ================= EEPROM =================
|
|
||||||
void saveSettings(){
|
|
||||||
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; // Neu
|
|
||||||
strncpy(settings.ntpServer, ntpServer.c_str(), sizeof(settings.ntpServer));
|
|
||||||
strncpy(settings.tzString, tzString.c_str(), sizeof(settings.tzString));
|
|
||||||
EEPROM.put(0, settings);
|
|
||||||
EEPROM.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
void loadSettings(){
|
|
||||||
EEPROM.begin(512);
|
|
||||||
EEPROM.get(0, settings);
|
|
||||||
if(settings.brightness == 0 || settings.brightness == 255){
|
|
||||||
brightness = 120;
|
|
||||||
nightBrightness = 10;
|
|
||||||
autoNight = true;
|
|
||||||
nightStart = 22;
|
|
||||||
nightEnd = 6;
|
|
||||||
currentColor = 0x00FFFF;
|
|
||||||
bgColor = 0x000000;
|
|
||||||
backgroundMode = false;
|
|
||||||
nightBackgroundDisabled = false;
|
|
||||||
ntpServer = "de.pool.ntp.org";
|
|
||||||
tzString = "CET-1CEST,M3.5.0/02,M10.5.0/03";
|
|
||||||
} else {
|
|
||||||
brightness = settings.brightness;
|
|
||||||
nightBrightness = settings.nightBrightness;
|
|
||||||
autoNight = settings.autoNight;
|
|
||||||
nightStart = settings.nightStart;
|
|
||||||
nightEnd = settings.nightEnd;
|
|
||||||
currentColor = settings.color;
|
|
||||||
bgColor = settings.bgColor;
|
|
||||||
backgroundMode = settings.backgroundMode;
|
|
||||||
nightBackgroundDisabled = settings.nightBackgroundDisabled;
|
|
||||||
ntpServer = String(settings.ntpServer);
|
|
||||||
tzString = String(settings.tzString);
|
|
||||||
}
|
|
||||||
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(r1+(r2-r1)*t, g1+(g2-g1)*t, b1+(b2-b1)*t);
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t gamma8(uint8_t x) {
|
|
||||||
if (x == 0) return 0;
|
|
||||||
// Verwende einen leicht angepassten Exponenten für den Nachtmodus
|
|
||||||
float f = (float)x / 255.0;
|
|
||||||
return (uint8_t)(pow(f, 2.0) * 255.0 + 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t applyGammaBrightness(uint32_t c, float brightnessFactor) {
|
|
||||||
// Wenn der Faktor extrem klein ist, erzwingen wir eine Mindesthelligkeit für aktive LEDs
|
|
||||||
if (brightnessFactor < 0.01 && brightnessFactor > 0) brightnessFactor = 0.01;
|
|
||||||
|
|
||||||
uint8_t r = (uint8_t)(((c >> 16) & 255) * brightnessFactor);
|
|
||||||
uint8_t g = (uint8_t)(((c >> 8) & 255) * brightnessFactor);
|
|
||||||
uint8_t b = (uint8_t)((c & 255) * brightnessFactor);
|
|
||||||
|
|
||||||
return pixels.Color(gamma8(r), gamma8(g), gamma8(b));
|
|
||||||
}
|
|
||||||
|
|
||||||
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.0;
|
|
||||||
for(int i=0;i<NUMPIXELS;i++){
|
|
||||||
pixels.setPixelColor(i, applyGammaBrightness(targetBuffer[i], b));
|
|
||||||
}
|
|
||||||
pixels.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
void fadeToTargetSmooth(int steps = 30, int delayMs = 15) {
|
|
||||||
time_t now = time(nullptr);
|
|
||||||
struct tm *t = localtime(&now);
|
|
||||||
float bTarget = getBrightness(t) / 255.0;
|
|
||||||
|
|
||||||
// Im Nachtmodus faden wir langsamer (mehr Schritte), um das Springen zu kaschieren
|
|
||||||
if (isNightTime(t)) {
|
|
||||||
steps = 60;
|
|
||||||
delayMs = 25;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int s = 0; s <= steps; s++) {
|
|
||||||
float tBlend = (float)s / steps;
|
|
||||||
for (int i = 0; i < NUMPIXELS; i++) {
|
|
||||||
uint32_t c = blend(currentBuffer[i], targetBuffer[i], tBlend);
|
|
||||||
pixels.setPixelColor(i, applyGammaBrightness(c, bTarget));
|
|
||||||
}
|
|
||||||
pixels.show();
|
|
||||||
yield(); // Verhindert WDT-Reset beim ESP8266
|
|
||||||
delay(delayMs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= WORTUHR LOGIK =================
|
|
||||||
void word_ES(){ setRange(0,1); } void word_IST(){ setRange(3,5); }
|
|
||||||
void word_FUENF_M(){ setRange(7,10); } void word_ZEHN_M(){ setRange(18,20); }
|
|
||||||
void word_ZWANZIG(){ setRange(11,17); } void word_VIERTEL(){ setRange(22,28); }
|
|
||||||
void word_VOR(){ setRange(30,32); } void word_NACH(){ setRange(40,43); }
|
|
||||||
void word_HALB(){ setRange(33,36); } void word_EINS(){ setRange(62,65); }
|
|
||||||
void word_EIN(){ setRange(62,64); } void word_ZWEI(){ setRange(55,58); }
|
|
||||||
void word_DREI(){ setRange(66,69); } void word_VIER(){ setRange(73,76); }
|
|
||||||
void word_FUENF_H(){ setRange(51,54); } void word_SECHS(){ setRange(83,87); }
|
|
||||||
void word_SIEBEN(){ setRange(88,93); } void word_ACHT(){ setRange(77,80); }
|
|
||||||
void word_NEUN(){ setRange(44,47); } void word_ZEHN_H(){ setRange(106,109); }
|
|
||||||
void word_ELF(){ setRange(103,105); } void word_ZWOELF(){ setRange(94,98); }
|
|
||||||
void word_UHR(){ setRange(99,101); }
|
|
||||||
|
|
||||||
String getTimeText(int h, int m){
|
|
||||||
int m5=(m/5)*5; if(m5>=25) h++;
|
|
||||||
String arr[]={"Zwölf","Eins","Zwei","Drei","Vier","Fünf","Sechs","Sieben","Acht","Neun","Zehn","Elf"};
|
|
||||||
String hT = arr[h%12]; if(m5==0 && h%12==1) hT = "ein";
|
|
||||||
String t = "Es ist ";
|
|
||||||
switch(m5){
|
|
||||||
case 0: t+= hT + " Uhr"; break;
|
|
||||||
case 5: t+= "fünf nach " + arr[h%12]; break;
|
|
||||||
case 10: t+= "zehn nach " + arr[h%12]; break;
|
|
||||||
case 15: t+= "viertel nach " + arr[h%12]; break;
|
|
||||||
case 20: t+= "zwanzig nach " + arr[h%12]; break;
|
|
||||||
case 25: t+= "fünf vor halb " + arr[h%12]; break;
|
|
||||||
case 30: t+= "halb " + arr[h%12]; break;
|
|
||||||
case 35: t+= "fünf nach halb " + arr[h%12]; break;
|
|
||||||
case 40: t+= "zwanzig vor " + arr[h%12]; break;
|
|
||||||
case 45: t+= "viertel vor " + arr[h%12]; break;
|
|
||||||
case 50: t+= "zehn vor " + arr[h%12]; break;
|
|
||||||
case 55: t+= "fünf vor " + arr[h%12]; break;
|
|
||||||
}
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
void showTimeWords(int h, int m){
|
|
||||||
time_t now = time(nullptr);
|
|
||||||
struct tm *t = localtime(&now);
|
|
||||||
|
|
||||||
for(int i=0;i<NUMPIXELS;i++) currentBuffer[i] = pixels.getPixelColor(i);
|
|
||||||
|
|
||||||
// HG Logik mit Nacht-Aus-Option
|
|
||||||
uint32_t activeBg = 0;
|
|
||||||
if(backgroundMode) {
|
|
||||||
if(nightBackgroundDisabled && isNightTime(t)) {
|
|
||||||
activeBg = 0;
|
|
||||||
} else {
|
|
||||||
activeBg = bgColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for(int i=0;i<NUMPIXELS;i++) targetBuffer[i] = activeBg;
|
|
||||||
|
|
||||||
word_ES(); word_IST();
|
|
||||||
int m5 = (m/5)*5; bool full = (m5 == 0); if(m5 >= 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 =================
|
|
||||||
void handleStatus() {
|
|
||||||
time_t now = time(nullptr);
|
|
||||||
struct tm *t = localtime(&now);
|
|
||||||
char hexCol[10], hexBg[10];
|
|
||||||
snprintf(hexCol, sizeof(hexCol), "#%06X", currentColor);
|
|
||||||
snprintf(hexBg, sizeof(hexBg), "#%06X", bgColor);
|
|
||||||
String ntpStat = (now > 100000) ? "OK" : "SYNC...";
|
|
||||||
String json = "{";
|
|
||||||
json += "\"txt\":\"" + getTimeText(t->tm_hour, t->tm_min) + "\",";
|
|
||||||
json += "\"ip\":\"" + WiFi.localIP().toString() + "\",";
|
|
||||||
json += "\"ntp\":\"" + ntpStat + "\",";
|
|
||||||
json += "\"ntpServer\":\"" + ntpServer + "\",";
|
|
||||||
json += "\"tzString\":\"" + 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleRoot(){
|
|
||||||
String html = R"rawliteral(
|
|
||||||
<html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width, initial-scale=1'>
|
|
||||||
<style>
|
|
||||||
body{font-family:Arial,sans-serif; background:#fff; margin:0; color:#333; display:flex; flex-direction:column; align-items:center}
|
|
||||||
.card{width:90%; max-width:400px; margin:10px; padding:15px; border-radius:12px; background:#f2f2f2; box-shadow: 0 2px 5px rgba(0,0,0,0.1)}
|
|
||||||
.big{font-size:26px; text-align:center; font-weight:bold; color:#0055ff}
|
|
||||||
input[type='range'], input[type='color'], input[type='text']{width:100%; margin-bottom:10px}
|
|
||||||
button{width:100%; padding:12px; border-radius:8px; background:#0055ff; color:#fff; border:none; cursor:pointer}
|
|
||||||
label{font-weight:bold; display:block; margin-bottom:5px}
|
|
||||||
</style></head><body>
|
|
||||||
<div class='card big' id='txt'>Lade...</div>
|
|
||||||
<div class='card' id='status'>Verbinde...</div>
|
|
||||||
<div class='card'><label>Farbe Worte</label><div> </div><input type='color' id='col' onchange="f('/color?c='+this.value.substring(1))">
|
|
||||||
<label>Helligkeit</label><input type='range' id='br' min='0' max='255' oninput="f('/brightness?val='+this.value)"></div>
|
|
||||||
<div class='card'><input type='checkbox' id='bg' onchange="f('/bg?val='+(this.checked?1:0))"> <b>Hintergrund</b><br><div> </div>
|
|
||||||
<input type='color' id='bgcol' onchange="f('/bgcolor?c='+this.value.substring(1))"></div>
|
|
||||||
<div class='card'><input type='checkbox' id='night' onchange="f('/night?val='+(this.checked?1:0))"> <b>Nachtmodus</b><br> <div> </div>
|
|
||||||
Von: <input type='number' id='nS' style='width:50px' oninput="f('/nightStart?val='+this.value)"> Uhr
|
|
||||||
Bis: <input type='number' id='nE' style='width:50px' oninput="f('/nightEnd?val='+this.value)"> Uhr<br><div> </div>
|
|
||||||
<label>Nacht-Helligkeit</label><input type='range' id='nbr' min='0' max='255' oninput="f('/nightBrightness?val='+this.value)">
|
|
||||||
<br><input type='checkbox' id='nightBgOff' onchange="f('/nightBgOff?val='+(this.checked?1:0))"> HG in Nacht aus</div>
|
|
||||||
|
|
||||||
<div class='card'><label>NTP Server:</label><input type='text' id='ntpS' onchange="f('/setNTP?val='+this.value)">
|
|
||||||
<label>Zeitzone:</label><input type='text' id='tzS' onchange="f('/setTZ?val='+this.value)"></div>
|
|
||||||
<div class='card'><button onclick="f('/save');alert('Gespeichert!')">EEPROM Speichern</button></div>
|
|
||||||
<script>
|
|
||||||
function f(u){fetch(u)}
|
|
||||||
function u(){fetch('/status').then(r=>r.json()).then(d=>{
|
|
||||||
document.getElementById('txt').innerText=d.txt;
|
|
||||||
document.getElementById('status').innerText="IP: "+d.ip+" | NTP: "+d.ntp;
|
|
||||||
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);
|
|
||||||
})}
|
|
||||||
setInterval(u, 2500); u();
|
|
||||||
</script></body></html>)rawliteral";
|
|
||||||
server.send(200, "text/html", html);
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleColor(){ if(server.hasArg("c")){ currentColor=strtoul(server.arg("c").c_str(),NULL,16); forceInstant=true; showTimeWords(lastH, lastMinute); } server.send(200); }
|
|
||||||
void handleBGColor(){ if(server.hasArg("c")){ bgColor=strtoul(server.arg("c").c_str(),NULL,16); forceInstant=true; updateNeeded=true; } server.send(200); }
|
|
||||||
void handleBrightness(){ brightness=server.arg("val").toInt(); forceInstant=true; updateNeeded=true; server.send(200); }
|
|
||||||
void handleNight(){ autoNight=server.arg("val").toInt(); forceInstant=true; updateNeeded=true; server.send(200); }
|
|
||||||
void handleNightStart(){ nightStart=server.arg("val").toInt(); forceInstant=true; updateNeeded=true; server.send(200); }
|
|
||||||
void handleNightEnd(){ nightEnd=server.arg("val").toInt(); forceInstant=true; updateNeeded=true; server.send(200); }
|
|
||||||
void handleNightBrightness(){ nightBrightness=server.arg("val").toInt(); forceInstant=true; updateNeeded=true; server.send(200); }
|
|
||||||
void handleNightBgOff(){ nightBackgroundDisabled=server.arg("val").toInt(); forceInstant=true; updateNeeded=true; server.send(200); }
|
|
||||||
void handleBG(){ backgroundMode=server.arg("val").toInt(); forceInstant=true; updateNeeded=true; server.send(200); }
|
|
||||||
void handleNTP(){ ntpServer=server.arg("val"); configTime(tzString.c_str(), ntpServer.c_str()); server.send(200); }
|
|
||||||
void handleTZ(){ tzString=server.arg("val"); configTime(tzString.c_str(), ntpServer.c_str()); server.send(200); }
|
|
||||||
void handleSave(){ saveSettings(); server.send(200); }
|
|
||||||
|
|
||||||
// ================= SETUP & LOOP =================
|
|
||||||
void setup(){
|
|
||||||
Serial.begin(115200);
|
|
||||||
WiFiManager wm; wm.autoConnect("Wortuhr");
|
|
||||||
loadSettings();
|
|
||||||
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();
|
|
||||||
pixels.begin();
|
|
||||||
}
|
|
||||||
|
|
||||||
void loop(){
|
|
||||||
server.handleClient();
|
|
||||||
|
|
||||||
time_t now = time(nullptr);
|
|
||||||
struct tm *t = localtime(&now);
|
|
||||||
|
|
||||||
if(t) {
|
|
||||||
int currentM5 = (t->tm_min / 5) * 5;
|
|
||||||
if(currentM5 != lastMinute || t->tm_hour != lastH){
|
|
||||||
lastMinute = currentM5;
|
|
||||||
lastH = t->tm_hour;
|
|
||||||
updateNeeded = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(updateNeeded){
|
|
||||||
showTimeWords(lastH, (t ? t->tm_min : 0));
|
|
||||||
updateNeeded = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user