Struktur aufgeräumt: SOFTWARE -> software, KiCad V2 nach board/kicad, alte KiCAD-Versionen entfernt

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
lukarioandClaude Sonnet 5.5 committed 2026-10-17 10:13:46 +02:00
1 parent 251d3a20fb
commit f70d007811
16 files changed
-29165

No files matched your search

+467
View File
@@ -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);
}
}