1084 lines
41 KiB
C++
1084 lines
41 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> " + String(autoSyncMinute) + ":" + String(autoSyncSecond) + "<br>";
|
|
html += "<b>Sensor-Triggerwert:</b> " + String(sensorTriggerValue) + " (" + String(sensorTriggerAbove ? "über" : "unter") + ")<br>";
|
|
html += "<b>OTA (IDE):</b> aktiv • <b>OTA (Web):</b> <a href='/update'>/update</a><br>";
|
|
html += "<b>Steps/Rev:</b> " + String(stepsPerRev) + "</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();})"
|
|
".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){}"
|
|
"}"
|
|
"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, <)) {
|
|
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);
|
|
}
|
|
|
|
// ------------------- 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.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();
|
|
}
|