#include #include #include #include #include #include #include // ------------------- 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("") + "" + String(PROJECT_NAME) + "" "" "

" + String(PROJECT_NAME) + "

"; } 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 += "
"; html += "

Status

"; html += "Aktuelle Zeit: " + String(timeStr) + "
"; html += "NTP-Zeit (letzte Sync): --:--:--
"; html += "Zeitzone: Europe/Berlin (CET/CEST)
"; html += "WLAN: " + String(WiFi.status() == WL_CONNECTED ? "Verbunden" : "Getrennt") + "
"; html += "Analogwert: --
"; html += "Auto-Sync Zeit: " + String(autoSyncMinute) + ":" + (autoSyncSecond < 10 ? "0" : "") + String(autoSyncSecond) + "
"; html += "Sensor-Triggerwert: " + String(sensorTriggerValue) + " (" + String(sensorTriggerAbove ? "über" : "unter") + ")
"; html += "OTA (IDE): aktiv • OTA (Web): /update
"; html += "Steps/Rev: " + String(stepsPerRev) + "
"; html += "

System-Log

"; html += "
";
  html += getLogHTML();
  html += "
"; html += "

Manuell vorwärts bewegen

" "
" "" "" "" "
"; html += "

Zeit / NTP

" "
" "" "
"; html += "

Auto-Sync Einstellungen

" "
" "" "" "" "" "" "" "" "
"; html += "

Zeigerzeit setzen

" "
" "" "" "" "
"; html += "

LED & Einstellungen

" "
" "" "" "" "" "" "" "" "" "" "" "
"; 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", "OK"); 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", "OK"); } 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", "OK"); 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", "OK"); 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", "OK"); 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", "OK"); } 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", "OK"); } 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", "OK"); } 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", "OK"); } 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, <)) { 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(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(); }