HollowClock/Clock_TMC2208_RTC_ChatGPT_v3_with_magneto.ino
mxxxxb 3c7d2f41b5 v2
added auto reload of status infos
2026-01-05 09:52:29 +01:00

1109 lines
42 KiB
C++

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266HTTPUpdateServer.h>
#include <EEPROM.h>
#include <time.h>
#include <FastLED.h>
#include <ArduinoOTA.h>
// ------------------- Projektname -------------------
#define PROJECT_NAME "HollowClock"
// ------------------- Hardware -------------------
#define EN_PIN D0
#define DIR_PIN D4
#define STEP_PIN D3
#define LED_PIN D6
#define ANALOG_PIN A0
#define NUM_LEDS 60
CRGB strip[NUM_LEDS];
// ------------------- WLAN -------------------
const char* ssid = "DarkNet";
const char* password = "7DDBD0A6BC123";
// ------------------- Uhrparameter (variabel) -------------------
uint32_t stepsPerRev = 122870UL;
const uint8_t TICKS_PER_HOUR = 10;
const unsigned long STEP_TRIGGER_INTERVAL_MS = 6000;
const uint16_t NORMAL_STEP_INTERVAL_US = 10000;
const uint16_t FAST_STEP_INTERVAL_US = 300;
uint16_t currentStepInterval = NORMAL_STEP_INTERVAL_US;
// ------------------- EEPROM -------------------
const int EEPROM_SIZE = 512;
const int ADDR_CURRENT_STEP = 0;
const int ADDR_BRIGHTNESS = 20;
const int ADDR_NIGHTDIM = 24;
const int ADDR_COLOR = 28;
const int ADDR_MODE = 32;
const int ADDR_NIGHT_BRIGHTNESS = 36;
const int ADDR_NIGHT_START = 40;
const int ADDR_NIGHT_END = 44;
const int ADDR_STEPS_PER_REV = 48;
const int ADDR_RAINBOW_COLOR = 52;
const int ADDR_AUTO_SYNC_MINUTE = 56;
const int ADDR_SENSOR_TRIGGER_VALUE = 60;
const int ADDR_SENSOR_TRIGGER_MODE = 64;
const int ADDR_AUTO_SYNC_SECOND = 68; // NEU: Sekunde für Auto-Sync
// ------------------- Globale Variablen -------------------
ESP8266WebServer server(80);
ESP8266HTTPUpdateServer httpUpdater;
uint64_t currentStep = 0;
bool moving = false;
uint64_t moveStepsRemaining = 0;
uint64_t moveTargetStep = 0;
unsigned long lastStepMicros = 0;
unsigned long lastStepTrigger = 0;
bool syncMovePending = false;
unsigned long syncMoveStartTime = 0;
uint64_t syncMoveStartStep = 0;
uint8_t brightness = 128;
uint8_t nightBrightness = 20;
bool nightDim = true;
uint8_t stripColor = 96;
bool rainbowColor = false;
uint8_t nightStartHour = 22;
uint8_t nightEndHour = 6;
enum LedMode { MODE_SECOND_TICK = 0,
MODE_RUNNING,
MODE_FILL };
LedMode ledMode = MODE_SECOND_TICK;
unsigned long lastLEDTime = 0;
int lightCount = 0;
bool lightUpDown = true;
uint8_t randomRainbow = 0;
uint8_t targetBrightness = 128;
uint8_t currentBrightness = 128;
unsigned long lastBrightnessUpdate = 0;
bool forceLedRefresh = false;
char ntpTimeStr[20] = "noch keine Sync";
unsigned long lastNTPSync = 0;
const unsigned long NTP_RESYNC_INTERVAL = 6UL * 3600UL * 1000UL;
// Median Filter für Analogwert
const int MEDIAN_SAMPLES = 21;
int analogBuffer[MEDIAN_SAMPLES] = { 0 };
int analogBufferIndex = 0;
// Auto-Sync bei Sensor-Trigger
int sensorTriggerValue = 1024;
bool sensorTriggerAbove = true;
const int SENSOR_TOLERANCE = 5;
bool lastSensorTriggered = false;
unsigned long lastAutoSync = 0;
const unsigned long AUTO_SYNC_COOLDOWN = 300000;
unsigned long sensorTriggerStart = 0;
const unsigned long SENSOR_STABLE_TIME = 1000;
bool sensorEnteredLog = false;
bool cooldownLogShown = false; // NEU: Verhindert Log-Spam
uint8_t autoSyncMinute = 32;
uint8_t autoSyncSecond = 0; // NEU: Auto-Sync Sekunde
// Log-System
const int MAX_LOG_ENTRIES = 50;
String logBuffer[MAX_LOG_ENTRIES];
int logIndex = 0;
// ---------- Forward declarations ----------
bool getESP8266Time(struct tm* timeinfo);
bool timeToGermanLocal(time_t t, struct tm* out);
// ------------------- Hilfsfunktionen -------------------
void addLog(String message) {
struct tm timeinfo;
String timestamp = "";
if (getESP8266Time(&timeinfo)) {
char buf[20];
snprintf(buf, sizeof(buf), "[%02d:%02d:%02d] ", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
timestamp = String(buf);
}
logBuffer[logIndex] = timestamp + message;
logIndex = (logIndex + 1) % MAX_LOG_ENTRIES;
Serial.println(message);
}
String getLogHTML() {
String html = "";
for (int i = 0; i < MAX_LOG_ENTRIES; i++) {
int idx = (logIndex - 1 - i + MAX_LOG_ENTRIES) % MAX_LOG_ENTRIES;
if (logBuffer[idx].length() > 0) {
html += logBuffer[idx] + "\n";
}
}
return html;
}
// ------------------- Basisfunktionen -------------------
void enableDriver() {
digitalWrite(EN_PIN, LOW);
}
void disableDriver() {
digitalWrite(EN_PIN, HIGH);
}
void saveSettings() {
EEPROM.put(ADDR_BRIGHTNESS, brightness);
EEPROM.put(ADDR_NIGHTDIM, nightDim);
EEPROM.put(ADDR_COLOR, stripColor);
EEPROM.put(ADDR_MODE, ledMode);
EEPROM.put(ADDR_NIGHT_BRIGHTNESS, nightBrightness);
EEPROM.put(ADDR_NIGHT_START, nightStartHour);
EEPROM.put(ADDR_NIGHT_END, nightEndHour);
EEPROM.put(ADDR_STEPS_PER_REV, stepsPerRev);
EEPROM.put(ADDR_RAINBOW_COLOR, rainbowColor);
EEPROM.put(ADDR_AUTO_SYNC_MINUTE, autoSyncMinute);
EEPROM.put(ADDR_AUTO_SYNC_SECOND, autoSyncSecond); // NEU
EEPROM.put(ADDR_SENSOR_TRIGGER_VALUE, sensorTriggerValue);
EEPROM.put(ADDR_SENSOR_TRIGGER_MODE, sensorTriggerAbove);
EEPROM.commit();
}
void loadSettings() {
EEPROM.get(ADDR_BRIGHTNESS, brightness);
EEPROM.get(ADDR_NIGHTDIM, nightDim);
EEPROM.get(ADDR_COLOR, stripColor);
EEPROM.get(ADDR_MODE, ledMode);
EEPROM.get(ADDR_NIGHT_BRIGHTNESS, nightBrightness);
EEPROM.get(ADDR_NIGHT_START, nightStartHour);
EEPROM.get(ADDR_NIGHT_END, nightEndHour);
EEPROM.get(ADDR_STEPS_PER_REV, stepsPerRev);
EEPROM.get(ADDR_RAINBOW_COLOR, rainbowColor);
EEPROM.get(ADDR_AUTO_SYNC_MINUTE, autoSyncMinute);
EEPROM.get(ADDR_AUTO_SYNC_SECOND, autoSyncSecond); // NEU
EEPROM.get(ADDR_SENSOR_TRIGGER_VALUE, sensorTriggerValue);
EEPROM.get(ADDR_SENSOR_TRIGGER_MODE, sensorTriggerAbove);
if (brightness > 255) brightness = 128;
if (nightBrightness > 255) nightBrightness = 20;
if (ledMode > MODE_FILL) ledMode = MODE_SECOND_TICK;
if (nightStartHour > 23) nightStartHour = 22;
if (nightEndHour > 23) nightEndHour = 6;
if (stepsPerRev < 1000UL || stepsPerRev > 2000000UL) stepsPerRev = 122870UL;
if (autoSyncMinute > 59) autoSyncMinute = 32;
if (autoSyncSecond > 59) autoSyncSecond = 0; // NEU
if (sensorTriggerValue < 0 || sensorTriggerValue > 1024) sensorTriggerValue = 1024;
}
// ------------------- Zeit → Schritt-Umrechnung -------------------
uint64_t totalSteps12h() {
return (uint64_t)stepsPerRev * 12ULL;
}
uint64_t timeToStepIndex(uint8_t hour, uint8_t minute, uint8_t second) {
const uint32_t SECONDS_PER_REV = 3600UL;
uint8_t hour12 = hour % 12;
uint32_t totalSeconds = (uint32_t(hour12) * 3600UL) + (uint32_t(minute) * 60UL) + uint32_t(second);
uint64_t num = (uint64_t)totalSeconds * (uint64_t)stepsPerRev + (SECONDS_PER_REV / 2);
return (num / SECONDS_PER_REV) % totalSteps12h();
}
uint64_t calcForwardDelta(uint64_t fromStep, uint64_t toStep) {
const uint64_t TOTAL = totalSteps12h();
if (toStep >= fromStep) return toStep - fromStep;
return (TOTAL - fromStep) + toStep;
}
// ------------------- Stepper (nicht-blockierend) -------------------
void startMove(uint64_t delta, bool fast = false, bool silent = false, bool manual = false) {
if (delta == 0 || moving) return;
enableDriver();
delay(2);
digitalWrite(DIR_PIN, LOW);
currentStepInterval = fast ? FAST_STEP_INTERVAL_US : NORMAL_STEP_INTERVAL_US;
moveStepsRemaining = delta;
moveTargetStep = (currentStep + delta) % totalSteps12h();
lastStepMicros = micros();
moving = true;
if (manual) {
fill_solid(strip, NUM_LEDS, CRGB::Blue);
FastLED.show();
}
if (!silent) {
addLog("Bewegung gestartet: " + String((unsigned long)delta) + " Schritte (" + String(fast ? "schnell" : "normal") + ")");
}
}
void handleStepper() {
if (!moving) return;
unsigned long now = micros();
if ((now - lastStepMicros) >= currentStepInterval) {
lastStepMicros = now;
digitalWrite(STEP_PIN, HIGH);
delayMicroseconds(5);
digitalWrite(STEP_PIN, LOW);
currentStep++;
if (currentStep >= totalSteps12h()) currentStep -= totalSteps12h();
if (--moveStepsRemaining == 0) {
moving = false;
disableDriver();
if (syncMovePending) {
unsigned long elapsedMs = millis() - syncMoveStartTime;
unsigned long elapsedSeconds = elapsedMs / 1000;
uint64_t catchupSteps = ((uint64_t)elapsedSeconds * (uint64_t)stepsPerRev) / 3600ULL;
if (catchupSteps > 0) {
addLog("Sync abgeschlossen, fahre " + String((unsigned long)catchupSteps) + " Schritte nach (" + String(elapsedSeconds) + "s vergangen)");
syncMovePending = false;
startMove(catchupSteps, true, false, false);
return;
} else {
addLog("Sync abgeschlossen (keine Nachkorrektur nötig)");
syncMovePending = false;
}
}
if (currentStepInterval == FAST_STEP_INTERVAL_US) {
FastLED.clear(true);
forceLedRefresh = true;
}
}
}
}
void handleNormalClockMove() {
if (moving) return;
unsigned long nowMs = millis();
if (nowMs - lastStepTrigger < STEP_TRIGGER_INTERVAL_MS) return;
lastStepTrigger = nowMs;
uint64_t stepsPerTick = stepsPerRev / (60ULL * TICKS_PER_HOUR);
if (stepsPerTick == 0) stepsPerTick = 1;
currentStep += stepsPerTick;
if (currentStep >= totalSteps12h()) currentStep -= totalSteps12h();
startMove(stepsPerTick, false, true);
}
void checkAutoSync() {
if (moving || syncMovePending) return;
int analogValue = getFilteredAnalogValue();
// Prüfung abhängig vom Modus
bool sensorTriggered;
if (sensorTriggerAbove) {
sensorTriggered = (analogValue >= sensorTriggerValue - SENSOR_TOLERANCE && analogValue <= sensorTriggerValue + SENSOR_TOLERANCE);
} else {
sensorTriggered = (analogValue <= sensorTriggerValue + SENSOR_TOLERANCE && analogValue >= sensorTriggerValue - SENSOR_TOLERANCE);
}
if (sensorTriggered && !lastSensorTriggered && !sensorEnteredLog) {
sensorEnteredLog = true;
sensorTriggerStart = millis();
addLog("Sensor erreicht (Wert: " + String(analogValue) + "), warte auf Stabilität...");
}
if (sensorTriggered && lastSensorTriggered) {
unsigned long now = millis();
if (now - sensorTriggerStart >= SENSOR_STABLE_TIME) {
if (now - lastAutoSync > AUTO_SYNC_COOLDOWN) {
struct tm timeinfo;
if (getESP8266Time(&timeinfo)) {
// Berechne Zeitdifferenz zur Sync-Zeit in Sekunden
int targetTotalSec = autoSyncMinute * 60 + autoSyncSecond;
int currentTotalSec = timeinfo.tm_min * 60 + timeinfo.tm_sec;
int secDiff = currentTotalSec - targetTotalSec;
if (secDiff < 0) secDiff += 3600; // Wrap around für vorherige Stunde
// Zeitfenster: ±13 Minuten = ±780 Sekunden
if (secDiff <= 780 || secDiff >= 2820) {
uint64_t sensorPosition = timeToStepIndex(timeinfo.tm_hour, autoSyncMinute, autoSyncSecond);
uint64_t realStep = timeToStepIndex(timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
uint64_t delta = calcForwardDelta(sensorPosition, realStep);
uint64_t maxDelta = stepsPerRev / 2;
if (delta > 0 && delta < maxDelta) {
currentStep = sensorPosition;
syncMovePending = true;
syncMoveStartTime = millis();
syncMoveStartStep = currentStep;
startMove(delta, true);
lastAutoSync = now;
cooldownLogShown = false; // Reset für nächsten Cooldown
char logMsg[140];
snprintf(logMsg, sizeof(logMsg), "Auto-Sync: Sensor bei %02d:%02d, Ziel: %02d:%02d:%02d, Delta: %llu (%.1f Min)",
autoSyncMinute, autoSyncSecond, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec,
delta, (float)delta / (float)stepsPerRev * 60.0);
addLog(String(logMsg));
sensorEnteredLog = false;
} else {
addLog("Auto-Sync abgebrochen: Delta zu groß (" + String((unsigned long)delta) + " Schritte)");
sensorEnteredLog = false;
}
} else {
addLog("Auto-Sync übersprungen: Zeit " + String(timeinfo.tm_min) + ":" + String(timeinfo.tm_sec) +
" nicht im Bereich " + String(autoSyncMinute) + ":" + String(autoSyncSecond) + " ±13min");
sensorEnteredLog = false;
}
}
} else {
// Log nur einmal während Cooldown zeigen
if (!cooldownLogShown) {
addLog("Auto-Sync übersprungen: Cooldown aktiv");
cooldownLogShown = true;
}
}
sensorTriggerStart = 0;
}
}
if (!sensorTriggered && lastSensorTriggered) {
addLog("Sensor verlassen");
cooldownLogShown = false; // Reset wenn Sensor verlassen wird
}
lastSensorTriggered = sensorTriggered;
}
// ------------------- Zeit-Helper -------------------
int dayOfWeek(int y, int m, int d) {
static int t[] = { 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4 };
if (m < 3) y -= 1;
return (y + y / 4 - y / 100 + y / 400 + t[m - 1] + d) % 7;
}
int daysInMonth(int year, int month) {
static const int mdays[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
if (month == 2) {
bool leap = ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0));
return leap ? 29 : 28;
}
return mdays[month - 1];
}
int lastSundayOfMonth(int year, int month) {
int lastDay = daysInMonth(year, month);
int w = dayOfWeek(year, month, lastDay);
return lastDay - w;
}
bool isCEST_UTC(const struct tm& utc_tm) {
int y = utc_tm.tm_year + 1900;
int m = utc_tm.tm_mon + 1;
int d = utc_tm.tm_mday;
int h = utc_tm.tm_hour;
if (m > 3 && m < 10) return true;
if (m < 3 || m > 10) return false;
if (m == 3) {
int ls = lastSundayOfMonth(y, 3);
if (d > ls) return true;
if (d < ls) return false;
return (h >= 1);
}
if (m == 10) {
int ls = lastSundayOfMonth(y, 10);
if (d < ls) return true;
if (d > ls) return false;
return (h < 1);
}
return false;
}
bool getESP8266Time(struct tm* timeinfo) {
time_t now = time(nullptr);
if (now < 100000) return false;
struct tm utc_tm;
if (gmtime_r(&now, &utc_tm) == nullptr) return false;
bool dst = isCEST_UTC(utc_tm);
int32_t offset = 3600 + (dst ? 3600 : 0);
time_t local = now + offset;
struct tm* lt = gmtime(&local);
if (lt == nullptr) return false;
memcpy(timeinfo, lt, sizeof(struct tm));
return true;
}
bool timeToGermanLocal(time_t t, struct tm* out) {
struct tm utc_tm;
if (gmtime_r(&t, &utc_tm) == nullptr) return false;
bool dst = isCEST_UTC(utc_tm);
int32_t offset = 3600 + (dst ? 3600 : 0);
time_t local = t + offset;
struct tm* lt = gmtime(&local);
if (lt == nullptr) return false;
memcpy(out, lt, sizeof(struct tm));
return true;
}
// ------------------- LED -------------------
int mapLogicalToPhysical(int logicalIndex) {
if (logicalIndex % 2 == 0)
return NUM_LEDS - 1 - (logicalIndex / 2);
else
return (logicalIndex - 1) / 2;
}
void ledModeFillSimple() {
static unsigned long lastChange = 0;
static bool filling = true;
static int index = 0;
unsigned long now = millis();
if (now - lastChange > 15) {
lastChange = now;
int physicalIndex = mapLogicalToPhysical(index);
if (filling) {
int hue = rainbowColor ? (index * 4 + randomRainbow) % 255 : stripColor;
strip[physicalIndex].setHue(hue);
index++;
if (index >= NUM_LEDS) {
index = 0;
filling = false;
}
} else {
strip[physicalIndex] = CRGB::Black;
index++;
if (index >= NUM_LEDS) {
index = 0;
filling = true;
if (rainbowColor) randomRainbow = random8();
}
}
FastLED.show();
}
}
void updateLEDs() {
if (moving && currentStepInterval == FAST_STEP_INTERVAL_US) return;
struct tm timeinfo;
if (!getESP8266Time(&timeinfo)) return;
bool nightActive = nightDim && ((nightStartHour < nightEndHour && timeinfo.tm_hour >= nightStartHour && timeinfo.tm_hour < nightEndHour) || (nightStartHour > nightEndHour && (timeinfo.tm_hour >= nightStartHour || timeinfo.tm_hour < nightEndHour)));
targetBrightness = nightActive ? nightBrightness : brightness;
unsigned long now = millis();
if (now - lastBrightnessUpdate > 50) {
lastBrightnessUpdate = now;
if (currentBrightness < targetBrightness) currentBrightness++;
else if (currentBrightness > targetBrightness) currentBrightness--;
FastLED.setBrightness(currentBrightness);
}
static int lastSecondDisplay = -1;
int currentSecond = timeinfo.tm_sec;
if (ledMode == MODE_SECOND_TICK) {
if (forceLedRefresh || currentSecond != lastSecondDisplay) {
forceLedRefresh = false;
lastSecondDisplay = currentSecond;
FastLED.clear();
int index = map(currentSecond, 0, 59, 0, NUM_LEDS - 1);
int hue = rainbowColor ? (index * 4 + randomRainbow) % 255 : stripColor;
strip[mapLogicalToPhysical(index)].setHue(hue);
FastLED.show();
if (rainbowColor && currentSecond == 0) randomRainbow = random8();
}
} else if (ledMode == MODE_RUNNING) {
if (now - lastLEDTime < 50) return;
lastLEDTime = now;
int index = lightUpDown ? lightCount : NUM_LEDS - 1 - lightCount;
int hueValue = rainbowColor ? (index * 4 + randomRainbow) % 255 : stripColor;
if (lightUpDown)
strip[mapLogicalToPhysical(index)].setHue(hueValue);
else
strip[mapLogicalToPhysical(index)] = CRGB::Black;
FastLED.show();
lightCount++;
if (lightCount >= NUM_LEDS) {
lightCount = 0;
lightUpDown = !lightUpDown;
if (!lightUpDown && rainbowColor) randomRainbow = random8();
}
} else if (ledMode == MODE_FILL) {
ledModeFillSimple();
}
}
// ------------------- OTA (IDE) -------------------
void setupOTA() {
ArduinoOTA.setHostname(PROJECT_NAME);
ArduinoOTA.setPassword("espupdate");
ArduinoOTA.onStart([]() {
disableDriver();
FastLED.clear(true);
addLog("OTA Update gestartet");
});
ArduinoOTA.onEnd([]() {
addLog("OTA Update abgeschlossen");
});
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {});
ArduinoOTA.onError([](ota_error_t error) {
char buf[50];
snprintf(buf, sizeof(buf), "OTA Fehler: %u", error);
addLog(String(buf));
});
ArduinoOTA.begin();
addLog("OTA (IDE) bereit");
}
// ------------------- Web UI -------------------
String htmlHeader() {
return String("<!doctype html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width, initial-scale=1'/>") + "<title>" + String(PROJECT_NAME) + "</title>"
"<style>"
"body{font-family:Segoe UI,Arial;background:#1e1e2f;color:#eee;margin:0;display:flex;justify-content:center;}"
".wrapper{max-width:720px;width:100%;padding:2em;}"
"h1{color:#4fc3f7;margin-top:0;text-align:center}"
".card{background:#2b2b3c;padding:1em;border-radius:10px;box-shadow:0 2px 8px rgba(0,0,0,.4);margin-bottom:1.5em}"
"form{display:block}"
"button,input[type=submit]{background:#4fc3f7;color:#000;font-weight:600;border:none;border-radius:6px;padding:8px 14px;cursor:pointer;transition:.2s}"
"button:hover,input[type=submit]:hover{background:#81d4fa}"
"input,select{width:100%;padding:8px;margin-top:6px;border-radius:6px;border:1px solid #555;background:#1e1e2f;color:#fff;box-sizing:border-box}"
"label{display:block;margin-top:.8em}"
"a{color:#4fc3f7}"
"#infobox{display:none;background:#263238;border-left:4px solid #81d4fa;padding:10px;border-radius:6px;margin-bottom:1.2em;white-space:pre-wrap}"
"#infobox.show{display:block}"
"</style>"
"</head><body><div class='wrapper'><h1>"
+ String(PROJECT_NAME) + "</h1>";
}
void handleRoot() {
struct tm timeinfo;
char timeStr[16] = "??:??:??";
if (getESP8266Time(&timeinfo))
snprintf(timeStr, sizeof(timeStr), "%02d:%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
String html = htmlHeader();
html += "<div id='infobox'></div>";
html += "<div class='card'><h3>Status</h3>";
html += "<b>Aktuelle Zeit:</b> <span id='curTime'>" + String(timeStr) + "</span><br>";
html += "<b>NTP-Zeit (letzte Sync):</b> <span id='ntpTime'>--:--:--</span><br>";
html += "<b>Zeitzone:</b> Europe/Berlin (CET/CEST)<br>";
html += "<b>WLAN:</b> " + String(WiFi.status() == WL_CONNECTED ? "Verbunden" : "Getrennt") + "<br>";
html += "<b>Analogwert:</b> <span id='analogVal'>--</span><br>";
html += "<b>Auto-Sync Zeit:</b> <span id='autoSyncTime'>" + String(autoSyncMinute) + ":" + (autoSyncSecond < 10 ? "0" : "") + String(autoSyncSecond) + "</span><br>";
html += "<b>Sensor-Triggerwert:</b> <span id='sensorTrigger'>" + String(sensorTriggerValue) + " (" + String(sensorTriggerAbove ? "über" : "unter") + ")</span><br>";
html += "<b>OTA (IDE):</b> aktiv • <b>OTA (Web):</b> <a href='/update'>/update</a><br>";
html += "<b>Steps/Rev:</b> <span id='stepsPerRev'>" + String(stepsPerRev) + "</span></div>";
html += "<div class='card'><h3>System-Log</h3>";
html += "<pre id='logBox' style='max-height:200px;overflow-y:auto;background:#1e1e2f;padding:8px;border-radius:6px;margin:0'>";
html += getLogHTML();
html += "</pre></div>";
html += "<div class='card'><h3>Manuell vorwärts bewegen</h3>"
"<form onsubmit='return submitForm(this,\"/move\")'>"
"<label>Schritte (1 .. 500000):</label><input name='steps' type='number' min='1' max='500000' value='200'>"
"<label>Schnellmodus:</label><select name='fast'><option value='0'>Nein</option><option value='1' selected>Ja</option></select>"
"<input type='submit' value='Start'>"
"</form></div>";
html += "<div class='card'><h3>Zeit / NTP</h3>"
"<form onsubmit='return submitForm(this,\"/syncntp\")'>"
"<input type='submit' value='Zeit mit NTP synchronisieren'>"
"</form></div>";
html += "<div class='card'><h3>Auto-Sync Einstellungen</h3>"
"<form onsubmit='return submitForm(this,\"/setautosync\")'>"
"<label>Sensor-Zeit (MM:SS):</label>"
"<input name='ast' placeholder='z. B. 32:00' value='"
+ String(autoSyncMinute) + ":" + (autoSyncSecond < 10 ? "0" : "") + String(autoSyncSecond) + "'>"
"<label>Sensor-Triggerwert (0-1024):</label>"
"<input name='stv' type='number' min='0' max='1024' value='"
+ String(sensorTriggerValue) + "'>"
"<label>Trigger-Modus:</label>"
"<select name='stm'>"
+ String(sensorTriggerAbove ? "<option value='1' selected>Über dem Wert</option><option value='0'>Unter dem Wert</option>"
: "<option value='1'>Über dem Wert</option><option value='0' selected>Unter dem Wert</option>")
+ "</select>"
"<input type='submit' value='Speichern'>"
"</form></div>";
html += "<div class='card'><h3>Zeigerzeit setzen</h3>"
"<form onsubmit='return submitForm(this,\"/setpos\")'>"
"<label>Aktuelle Zeigerzeit (HH:MM):</label>"
"<input name='t' placeholder='z. B. 12:34' required>"
"<input type='submit' value='Zeiger auf aktuelle Zeit stellen'>"
"</form></div>";
html += "<div class='card'><h3>LED & Einstellungen</h3>"
"<form onsubmit='return submitForm(this,\"/setled\")'>"
"<label>Brightness:</label><input name='b' type='number' min='0' max='255' value='"
+ String(brightness) + "'>"
"<label>Nacht-Brightness:</label><input name='nb' type='number' min='0' max='255' value='"
+ String(nightBrightness) + "'>"
"<label>Nachtdimmen:</label><select name='n'>"
+ String(nightDim ? "<option value='1' selected>An</option><option value='0'>Aus</option>"
: "<option value='1'>An</option><option value='0' selected>Aus</option>")
+ "</select>"
"<label>Nachtdimmung Start (Stunde):</label><input name='ns' type='number' min='0' max='23' value='"
+ String(nightStartHour) + "'>"
"<label>Nachtdimmung Ende (Stunde):</label><input name='ne' type='number' min='0' max='23' value='"
+ String(nightEndHour) + "'>"
"<label>Farbe Hue:</label><input name='c' type='number' min='0' max='255' value='"
+ String(stripColor) + "'>"
"<label>Regenbogen:</label><select name='r'>"
+ String(rainbowColor ? "<option value='0'>Aus</option><option value='1' selected>An</option>"
: "<option value='0' selected>Aus</option><option value='1'>An</option>")
+ "</select>"
"<label>LED-Modus:</label><select name='m'>"
+ String(ledMode == MODE_SECOND_TICK ? "<option value='0' selected>Sekundenzeiger</option>" : "<option value='0'>Sekundenzeiger</option>") + String(ledMode == MODE_RUNNING ? "<option value='1' selected>Lauflicht</option>" : "<option value='1'>Lauflicht</option>") + String(ledMode == MODE_FILL ? "<option value='2' selected>Sekunden-Füllung</option>" : "<option value='2'>Sekunden-Füllung</option>") + "</select>"
"<label>Steps per Revolution:</label><input name='spr' type='number' min='1000' max='2000000' value='"
+ String(stepsPerRev) + "'>"
"<input type='submit' value='Speichern'>"
"</form></div>";
html +=
"<iframe name='hiddenFrame' style='display:none'></iframe>"
"<script>"
"let hideTimer;"
"function showInfo(msg){"
"const box=document.getElementById('infobox');"
"box.textContent=msg;box.classList.add('show');"
"clearTimeout(hideTimer);"
"hideTimer=setTimeout(()=>{box.classList.remove('show');},10000);"
"}"
"function submitForm(form,path){"
"form.action=path;form.target='hiddenFrame';form.method='POST';"
"const formData=new FormData(form);"
"formData.append('response','1');"
"setTimeout(()=>{"
"fetch(path,{method:'POST',body:new URLSearchParams(formData)})"
".then(r=>r.text())"
".then(t=>{showInfo(t);updateAnalog();updateLog();updateTime();updateStatus();})"
".catch(e=>showInfo('Fehler: '+e));"
"},100);"
"return true;"
"}"
"async function updateAnalog(){"
"try{const r=await fetch('/analog');document.getElementById('analogVal').innerText=await r.text();}catch(e){}"
"}"
"async function updateLog(){"
"try{const r=await fetch('/log');document.getElementById('logBox').innerHTML=await r.text();}catch(e){}"
"}"
"async function updateTime(){"
"try{const r=await fetch('/time');document.getElementById('curTime').innerText=await r.text();}catch(e){}"
"try{const r2=await fetch('/ntptime');document.getElementById('ntpTime').innerText=await r2.text();}catch(e){}"
"}"
"async function updateStatus(){"
"try{"
"const r1=await fetch('/status/autosync');document.getElementById('autoSyncTime').innerText=await r1.text();"
"const r2=await fetch('/status/trigger');document.getElementById('sensorTrigger').innerText=await r2.text();"
"const r3=await fetch('/status/stepsperrev');document.getElementById('stepsPerRev').innerText=await r3.text();"
"}catch(e){}"
"}"
"setInterval(updateAnalog,1000);"
"setInterval(updateLog,2000);"
"setInterval(updateTime,1000);"
"updateAnalog();updateLog();updateTime();"
"</script>"
"</body></html>";
server.send(200, "text/html", html);
}
// ------------------- Web-Handler -------------------
int getFilteredAnalogValue() {
analogBuffer[analogBufferIndex] = analogRead(ANALOG_PIN);
analogBufferIndex = (analogBufferIndex + 1) % MEDIAN_SAMPLES;
int sorted[MEDIAN_SAMPLES];
for (int i = 0; i < MEDIAN_SAMPLES; i++) sorted[i] = analogBuffer[i];
for (int i = 0; i < MEDIAN_SAMPLES - 1; i++) {
for (int j = 0; j < MEDIAN_SAMPLES - i - 1; j++) {
if (sorted[j] > sorted[j + 1]) {
int t = sorted[j];
sorted[j] = sorted[j + 1];
sorted[j + 1] = t;
}
}
}
return sorted[MEDIAN_SAMPLES / 2];
}
void handleAnalog() {
int val = getFilteredAnalogValue();
server.send(200, "text/plain", String(val));
}
void handleTime() {
struct tm timeinfo;
if (!getESP8266Time(&timeinfo)) {
server.send(200, "text/plain", "--:--:--");
return;
}
char timeStr[16];
snprintf(timeStr, sizeof(timeStr), "%02d:%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
server.send(200, "text/plain", String(timeStr));
}
void handleNtpTime() {
if (lastNTPSync == 0) {
server.send(200, "text/plain", "Noch kein Sync");
return;
}
time_t now = time(nullptr);
unsigned long diffSec = (millis() - lastNTPSync) / 1000;
time_t syncTime = now - diffSec;
struct tm ti;
if (!timeToGermanLocal(syncTime, &ti)) {
server.send(200, "text/plain", "Unbekannt");
return;
}
char buf[32];
snprintf(buf, sizeof(buf), "%02d:%02d:%02d", ti.tm_hour, ti.tm_min, ti.tm_sec);
server.send(200, "text/plain", String(buf));
}
void handleLog() {
server.send(200, "text/plain", getLogHTML());
}
void handleManualMove() {
bool isResponse = server.hasArg("response");
if (moving) {
if (isResponse) server.send(200, "text/plain", "⚠ Bewegung läuft bereits.");
else server.send(200, "text/html", "<html><body>OK</body></html>");
return;
}
uint32_t steps = 200;
bool fast = false;
if (server.hasArg("steps")) {
long s = server.arg("steps").toInt();
if (s < 1) s = 1;
if (s > 500000L) s = 500000L;
steps = (uint32_t)s;
}
if (server.hasArg("fast")) fast = (server.arg("fast") == "1");
startMove(steps, fast, false, true);
String msg = "Vorwärtsbewegung gestartet\nSchritte: " + String(steps) + (fast ? " (schnell)" : " (normal)");
if (isResponse) server.send(200, "text/plain", msg);
else server.send(200, "text/html", "<html><body>OK</body></html>");
}
void handleSetPos() {
bool isResponse = server.hasArg("response");
if (moving) {
if (isResponse) server.send(200, "text/plain", "⚠ Bewegung läuft bereits.");
else server.send(200, "text/html", "<html><body>OK</body></html>");
return;
}
String timestr = server.arg("t");
int hh = 0, mm = 0;
if (sscanf(timestr.c_str(), "%d:%d", &hh, &mm) != 2 || hh < 0 || hh > 23 || mm < 0 || mm > 59) {
String msg = "✗ Ungültiges Zeitformat.\nBeispiel: 12:34";
if (isResponse) server.send(200, "text/plain", msg);
else server.send(200, "text/html", "<html><body>OK</body></html>");
return;
}
currentStep = timeToStepIndex(hh, mm, 0);
struct tm timeinfo;
if (!getESP8266Time(&timeinfo)) {
String msg = "✗ Konnte aktuelle Zeit nicht lesen.\n(prüfe NTP-Sync)";
if (isResponse) server.send(200, "text/plain", msg);
else server.send(200, "text/html", "<html><body>OK</body></html>");
return;
}
uint64_t realStep = timeToStepIndex(timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
uint64_t delta = calcForwardDelta(currentStep, realStep);
syncMovePending = true;
syncMoveStartTime = millis();
syncMoveStartStep = currentStep;
startMove(delta, true);
String msg = "Zeiger wird nachgestellt\nVon: " + String(hh) + ":" + String(mm) + "\nZu: " + String(timeinfo.tm_hour) + ":" + String(timeinfo.tm_min) + ":" + String(timeinfo.tm_sec) + "\n(Vergangene Zeit wird nachgefahren)";
addLog("Manuelles Sync gestartet: " + String(hh) + ":" + String(mm) + " -> " + String(timeinfo.tm_hour) + ":" + String(timeinfo.tm_min));
if (isResponse) server.send(200, "text/plain", msg);
else server.send(200, "text/html", "<html><body>OK</body></html>");
}
void handleSetLED() {
bool isResponse = server.hasArg("response");
String msg = "";
if (server.hasArg("b")) {
brightness = constrain(server.arg("b").toInt(), 0, 255);
msg += "Brightness: " + String(brightness) + "\n";
}
if (server.hasArg("nb")) {
nightBrightness = constrain(server.arg("nb").toInt(), 0, 255);
msg += "Nacht-Brightness: " + String(nightBrightness) + "\n";
}
if (server.hasArg("n")) {
nightDim = (server.arg("n") == "1");
msg += String("Nachtdimmen: ") + (nightDim ? "An" : "Aus") + "\n";
}
if (server.hasArg("ns")) {
nightStartHour = constrain(server.arg("ns").toInt(), 0, 23);
msg += "Nacht Start: " + String(nightStartHour) + "\n";
}
if (server.hasArg("ne")) {
nightEndHour = constrain(server.arg("ne").toInt(), 0, 23);
msg += "Nacht Ende: " + String(nightEndHour) + "\n";
}
if (server.hasArg("c")) {
stripColor = constrain(server.arg("c").toInt(), 0, 255);
msg += "Hue: " + String(stripColor) + "\n";
}
if (server.hasArg("r")) {
rainbowColor = (server.arg("r") == "1");
msg += String("Regenbogen: ") + (rainbowColor ? "An" : "Aus") + "\n";
}
if (server.hasArg("m")) {
ledMode = (LedMode)constrain(server.arg("m").toInt(), 0, 2);
forceLedRefresh = true;
msg += "LED-Mode: " + String((int)ledMode) + "\n";
}
if (server.hasArg("spr")) {
uint32_t newSpr = (uint32_t)server.arg("spr").toInt();
if (newSpr < 1000UL) newSpr = 1000UL;
if (newSpr > 2000000UL) newSpr = 2000000UL;
if (newSpr != stepsPerRev) {
stepsPerRev = newSpr;
currentStep %= totalSteps12h();
msg += "Steps/Rev aktualisiert: " + String(stepsPerRev) + "\n";
}
}
saveSettings();
if (msg.length() == 0) msg = "Keine Änderungen erkannt.";
if (isResponse) server.send(200, "text/plain", msg);
else server.send(200, "text/html", "<html><body>OK</body></html>");
}
void handleSetAutoSync() {
bool isResponse = server.hasArg("response");
String msg = "";
// Parse MM:SS Format
if (server.hasArg("ast")) {
String timeStr = server.arg("ast");
int mm = 0, ss = 0;
if (sscanf(timeStr.c_str(), "%d:%d", &mm, &ss) == 2) {
if (mm >= 0 && mm <= 59 && ss >= 0 && ss <= 59) {
if (mm != autoSyncMinute || ss != autoSyncSecond) {
autoSyncMinute = mm;
autoSyncSecond = ss;
msg += "Auto-Sync Zeit: " + String(autoSyncMinute) + ":" + (autoSyncSecond < 10 ? "0" : "") + String(autoSyncSecond) + "\n";
addLog("Auto-Sync Zeit geändert auf: " + String(autoSyncMinute) + ":" + String(autoSyncSecond));
}
} else {
msg += "✗ Ungültige Zeit (0-59:0-59)\n";
}
} else {
msg += "✗ Ungültiges Format (MM:SS)\n";
}
}
if (server.hasArg("stv")) {
int newTrigger = constrain(server.arg("stv").toInt(), 0, 1024);
if (newTrigger != sensorTriggerValue) {
sensorTriggerValue = newTrigger;
msg += "Sensor-Triggerwert: " + String(sensorTriggerValue) + "\n";
addLog("Sensor-Triggerwert geändert auf: " + String(sensorTriggerValue));
}
}
if (server.hasArg("stm")) {
bool newMode = (server.arg("stm") == "1");
if (newMode != sensorTriggerAbove) {
sensorTriggerAbove = newMode;
msg += "Trigger-Modus: " + String(sensorTriggerAbove ? "Über" : "Unter") + " dem Wert\n";
addLog("Trigger-Modus geändert auf: " + String(sensorTriggerAbove ? "Über" : "Unter"));
}
}
if (msg.length() > 0) {
saveSettings();
msg += "Zeitfenster: ±13 Minuten\nToleranz: ±" + String(SENSOR_TOLERANCE);
} else {
msg = "Keine Änderungen erkannt.";
}
if (isResponse) server.send(200, "text/plain", msg);
else server.send(200, "text/html", "<html><body>OK</body></html>");
}
void handleSyncNTP() {
bool isResponse = server.hasArg("response");
syncTimeFromNTP();
String msg = "NTP-Sync ausgelöst.\nLetzte Sync: " + String(ntpTimeStr);
if (isResponse) server.send(200, "text/plain", msg);
else server.send(200, "text/html", "<html><body>OK</body></html>");
}
void handleDebugTime() {
String out = "";
time_t now = time(nullptr);
out += "time_t (raw): " + String((unsigned long)now) + "\n";
struct tm gm;
if (gmtime_r(&now, &gm) != nullptr) {
char buf[64];
snprintf(buf, sizeof(buf), "UTC: %04d-%02d-%02d %02d:%02d:%02d\n",
gm.tm_year + 1900, gm.tm_mon + 1, gm.tm_mday, gm.tm_hour, gm.tm_min, gm.tm_sec);
out += String(buf);
} else {
out += "UTC: (gmtime_r failed)\n";
}
struct tm lt;
if (timeToGermanLocal(now, &lt)) {
char buf2[64];
snprintf(buf2, sizeof(buf2), "Local (DE): %04d-%02d-%02d %02d:%02d:%02d\n",
lt.tm_year + 1900, lt.tm_mon + 1, lt.tm_mday, lt.tm_hour, lt.tm_min, lt.tm_sec);
out += String(buf2);
} else {
out += "Local (DE): (error)\n";
}
out += "lastNTPSync (ms): " + String(lastNTPSync) + " (ntpTimeStr: " + String(ntpTimeStr) + ")\n";
server.send(200, "text/plain", out);
}
void handleStatusAutoSync() {
char buf[10];
snprintf(buf, sizeof(buf), "%d:%02d", autoSyncMinute, autoSyncSecond);
server.send(200, "text/plain", String(buf));
}
void handleStatusTrigger() {
String msg = String(sensorTriggerValue) + " (" + String(sensorTriggerAbove ? "über" : "unter") + ")";
server.send(200, "text/plain", msg);
}
void handleStatusStepsPerRev() {
server.send(200, "text/plain", String(stepsPerRev));
}
// ------------------- NTP -------------------
void syncTimeFromNTP() {
if (WiFi.status() != WL_CONNECTED) {
strncpy(ntpTimeStr, "Kein WLAN", sizeof(ntpTimeStr));
addLog("NTP Sync fehlgeschlagen: Kein WLAN");
return;
}
addLog("Starte NTP-Sync (UTC holen)...");
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
const int tries = 30;
for (int i = 0; i < tries; ++i) {
time_t now = time(nullptr);
if (now > 100000) {
struct tm timeinfo;
if (getESP8266Time(&timeinfo)) {
snprintf(ntpTimeStr, sizeof(ntpTimeStr), "%02d:%02d:%02d",
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
addLog("NTP Sync erfolgreich (Europe/Berlin)");
lastNTPSync = millis();
return;
}
}
delay(500);
}
strncpy(ntpTimeStr, "Sync fehlgeschlagen", sizeof(ntpTimeStr));
addLog("NTP Sync fehlgeschlagen: Timeout");
}
// ------------------- Setup -------------------
void setup() {
Serial.begin(115200);
Serial.println();
Serial.println(PROJECT_NAME);
pinMode(EN_PIN, OUTPUT);
pinMode(DIR_PIN, OUTPUT);
pinMode(STEP_PIN, OUTPUT);
disableDriver();
digitalWrite(DIR_PIN, LOW);
FastLED.addLeds<NEOPIXEL, LED_PIN>(strip, NUM_LEDS);
FastLED.clear(true);
EEPROM.begin(EEPROM_SIZE);
loadSettings();
addLog("System gestartet");
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
addLog("Verbinde mit WLAN...");
for (int i = 0; i < 30 && WiFi.status() != WL_CONNECTED; i++) {
delay(500);
Serial.print(".");
}
Serial.println();
if (WiFi.status() == WL_CONNECTED) {
addLog("WLAN verbunden: " + WiFi.localIP().toString());
syncTimeFromNTP();
} else {
addLog("WLAN fehlgeschlagen, starte AP-Modus");
WiFi.mode(WIFI_AP);
WiFi.softAP(PROJECT_NAME);
addLog("AP gestartet: " + WiFi.softAPIP().toString());
}
setupOTA();
httpUpdater.setup(&server, "/update", "admin", "espupdate");
addLog("Web-OTA bereit unter /update");
server.on("/", HTTP_GET, handleRoot);
server.on("/analog", HTTP_GET, handleAnalog);
server.on("/time", HTTP_GET, handleTime);
server.on("/log", HTTP_GET, handleLog);
server.on("/move", HTTP_POST, handleManualMove);
server.on("/setpos", HTTP_POST, handleSetPos);
server.on("/setled", HTTP_POST, handleSetLED);
server.on("/syncntp", HTTP_POST, handleSyncNTP);
server.on("/setautosync", HTTP_POST, handleSetAutoSync);
server.on("/ntptime", HTTP_GET, handleNtpTime);
server.on("/debugtime", HTTP_GET, handleDebugTime);
server.on("/status/autosync", HTTP_GET, handleStatusAutoSync);
server.on("/status/trigger", HTTP_GET, handleStatusTrigger);
server.on("/status/stepsperrev", HTTP_GET, handleStatusStepsPerRev);
server.begin();
addLog("Webserver gestartet");
}
// ------------------- Loop -------------------
void loop() {
server.handleClient();
handleStepper();
handleNormalClockMove();
checkAutoSync();
updateLEDs();
ArduinoOTA.handle();
if (WiFi.status() == WL_CONNECTED && (millis() - lastNTPSync > NTP_RESYNC_INTERVAL))
syncTimeFromNTP();
}