/************************************************************************************ * Dual-PID Controller Firmware (Zeitproportional) * Version (Siehe unten im Code) * * Dieses Programm steuert Wasser- und Dampftemperatur über zwei unabhängige PID-Regler * mit zeitproportionaler Ansteuerung der SSRs. * Sensoren: MAX6675 (Dampf), NTC (Wasser). * Funktionen: Eco-Modus, Profile, Fast-Heat-Up, WLAN-Weboberfläche, AutoTune, * Wartungserinnerung, Werkseinstellungen, Hostname-Konfiguration. * * Compiler- und Hardware-Umgebung: * - ESP8266: Wemos D1 mini, 4MB * - Bibliotheken: Wire, Adafruit_GFX, Adafruit_SH110X, EEPROM, PID_v1, PID_AutoTune_v0, * max6675, ESP8266WiFi, ESP8266WebServer, ESP8266mDNS ************************************************************************************/ #include #include #include #include #include #include #include "max6675.h" #include #include #include // NEU: Für Hostname #include // Für isnan() /************************************************************************************ * Hardware-Pinbelegung ************************************************************************************/ #define SSR_WASSER_PIN D2 // SSR für Wasser-Heizung #define SSR_DAMPF_PIN D1 // SSR für Dampf-Heizung #define SHOT_TIMER_PIN D8 // Input für Shot-Timer #define OLED_SDA D3 // SDA-Leitung für OLED #define OLED_SCK D4 // SCK-Leitung für OLED #define ANALOG_NTC_PIN A0 // Analoger Eingang für den NTC-Sensor // Display-Objekt (Adafruit SH1106G) Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire); /************************************************************************************ * MAX6675 (Dampf-Temperatursensor) ************************************************************************************/ #define MAX6675_SCK D7 // Serial Clock #define MAX6675_CS D6 // Chip Select #define MAX6675_SO D5 // Serial Out MAX6675 thermocouple(MAX6675_SCK, MAX6675_CS, MAX6675_SO); /************************************************************************************ * Firmware-Informationen ************************************************************************************/ String version = "2.7.2"; // Versionsnummer erhöht String versionHersteller = "Thomas Müller"; String versionHerstellerMail = "" "thomas@mueller.black"; String versionHerstellerWeb = "" "https://raw-designs.de/ | " "" "https://mueller.black"; // Demo-Modus: Hier können bestimmte Funktionen (WLAN-Anpassungen etc.) eingeschränkt werden. bool demoModus = false; // Dieses Kennwort muss in einer Firmware-Binärdatei vorhanden sein, damit ein Update gültig ist. static const char FIRMWARE_PASSWORD[] = "FWKennwort123"; static bool passwordFound = false; static int passMatchPos = 0; const int passLength = sizeof(FIRMWARE_PASSWORD) - 1; /************************************************************************************ * Geräte-Infos ************************************************************************************/ char infoHersteller[50]; char infoModell[50]; char infoZusatz[50]; /************************************************************************************ * NTC-Sensor-Parameter (Wasser) * - A0 wird verwendet, um die Temperatur über einen Spannungsteiler zu messen ************************************************************************************/ const double V_SUPPLY = 3.3; // Versorgungsspannung const double ADC_REF = 3.3; // ADC-Referenz const double R_FIXED = 3000.0; // Festwiderstand 3 kΩ // Daten des NTC const double T0 = 298.15; // Referenztemperatur in Kelvin (25°C) const double R0 = 50000.0; // NTC-Widerstand bei 25°C const double beta = 3950.0; // Beta-Wert /************************************************************************************ * Temperatur-Profil-Struktur * - Speichert Wasser- und Dampf-Setpoint, Eco-Einstellungen und Profilname ************************************************************************************/ struct TemperatureProfile { double setpointWasser; double setpointDampf; int ecoModeMinutes; bool dynamicEcoActive; int dampfVerzoegerung; char profileName[20]; }; /************************************************************************************ * PID-Regler-Konfiguration & Zeitproportionale Steuerung ************************************************************************************/ // PID Input/Output/Setpoint Variablen double SetpointDampf, InputDampf, OutputDampf; double SetpointWasser, InputWasser, OutputWasser; double OffsetDampf = 0.0, OffsetWasser = 0.0; // PID-Konstanten (Regelungsparameter) double KpDampf = 3.0, KiDampf = 0.1, KdDampf = 0.5; double KpWasser = 5.0, KiWasser = 0.2, KdWasser = 1.0; // --- NEU: Zeitproportionale Steuerung --- unsigned long windowSizeWasser = 2000; // Fenstergröße für Wasser-SSR in ms (2 Sekunden) unsigned long windowSizeDampf = 2000; // Fenstergröße für Dampf-SSR in ms (2 Sekunden) unsigned long windowStartTimeWasser; // Startzeit des aktuellen Fensters für Wasser unsigned long windowStartTimeDampf; // Startzeit des aktuellen Fensters für Dampf // PID-Regler Instanzen // WICHTIG: Die Output-Grenzen werden in setup() auf die Fenstergröße gesetzt! PID pidDampf(&InputDampf, &OutputDampf, &SetpointDampf, KpDampf, KiDampf, KdDampf, DIRECT); PID pidWasser(&InputWasser, &OutputWasser, &SetpointWasser, KpWasser, KiWasser, KdWasser, DIRECT); // Variablen für Sicherheitsabschaltung double maxTempWasser = 110.0; // Standardwert, wie bisher hardcoded double maxTempDampf = 180.0; // Standardwert, wie bisher hardcoded /************************************************************************************ * Standardwerte und Default-Einstellungen ************************************************************************************/ double defaultSetpointDampf = 165.0; double defaultSetpointWasser = 93.0; double defaultOffsetDampf = 0.0; double defaultOffsetWasser = 0.0; double defaultKpDampf = 3.0; double defaultKiDampf = 0.1; double defaultKdDampf = 0.5; double defaultKpWasser = 5.0; double defaultKiWasser = 0.2; double defaultKdWasser = 1.0; int defaultEcoModeMinutes = 0; int defaultEcoModeTempWasser = 60; int defaultEcoModeTempDampf = 60; String defaultInfoHersteller = ""; String defaultInfoModell = ""; String defaultInfoZusatz = ""; int defaultDampfVerzoegerung = 0; int dampfVerzoegerung = 0; // Dampfverzögerung in Minuten // NEU: Standardwerte für Wartung und Hostname int defaultMaintenanceInterval = 0; // 0 = Deaktiviert String defaultHostname = "Dual-PID-Controller"; /************************************************************************************ * Webserver und zugehörige Objekte ************************************************************************************/ ESP8266WebServer server(80); // Webserver auf Port 80 // MagicValue zum Erkennen bereits gespeicherter EEPROM-Daten const char storageMagicValue[5] = "MGVE"; /************************************************************************************ * EEPROM Adressen für das Speichern/Laden von Werten ************************************************************************************/ const int EEPROM_SIZE = 1024; // Gesamtgröße des verwendeten EEPROM-Speichers /************************************************************************************ * EEPROM Adressen für das Speichern/Laden von Werten ************************************************************************************/ const int EEPROM_ADDR_MAGICVALUE = 0; // 5 Bytes (char[5]) const int EEPROM_ADDR_SETPOINT_DAMPF = 5; // 8 Bytes (double) const int EEPROM_ADDR_SETPOINT_WASSER = 13; // 8 Bytes const int EEPROM_ADDR_OFFSET_DAMPF = 21; // 8 Bytes const int EEPROM_ADDR_OFFSET_WASSER = 29; // 8 Bytes const int EEPROM_ADDR_KP_DAMPF = 37; // 8 Bytes const int EEPROM_ADDR_KI_DAMPF = 45; // 8 Bytes const int EEPROM_ADDR_KD_DAMPF = 53; // 8 Bytes const int EEPROM_ADDR_KP_WASSER = 61; // 8 Bytes const int EEPROM_ADDR_KI_WASSER = 69; // 8 Bytes const int EEPROM_ADDR_KD_WASSER = 77; // 8 Bytes const int EEPROM_ADDR_ECOMODE_MINUTES = 85; // 4 Bytes (int) const int EEPROM_ADDR_ECOMODE_TEMP_WASSER = 89; // 4 Bytes const int EEPROM_ADDR_ECOMODE_TEMP_DAMPF = 93; // 4 Bytes const int EEPROM_ADDR_INFO_HERSTELLER = 97; // 50 Bytes (char[50]) const int EEPROM_ADDR_INFO_MODELL = 147; // 50 Bytes const int EEPROM_ADDR_INFO_ZUSATZ = 197; // 50 Bytes const int EEPROM_ADDR_FASTHEATUP_DATA = 247; // 1 Byte (bool) const int EEPROM_ADDR_RUNTIME = 248; // 4 Bytes (unsigned long) const int EEPROM_ADDR_SHOTCOUNTER = 252; // 4 Bytes (unsigned long) const int EEPROM_ADDR_DYNAMIC_ECO_MODE = 256; // 1 Byte (bool) const int EEPROM_ADDR_PROFILE_1 = 257; // 50 Bytes für Profil 1 (struct TemperatureProfile) const int EEPROM_ADDR_PROFILE_2 = 307; // 50 Bytes für Profil 2 (struct TemperatureProfile) // --- AutoTune Parameter Wasser --- const int EEPROM_ADDR_TUNING_STEP_WASSER = 357; // 8 Bytes (double) const int EEPROM_ADDR_TUNING_NOISE_WASSER = 365; // 8 Bytes (double) const int EEPROM_ADDR_TUNING_STARTVALUE_WASSER = 373; // 8 Bytes (double) const int EEPROM_ADDR_TUNING_LOOKBACK_WASSER = 381; // 4 Bytes (unsigned int) -> Ende 384 // --- WiFi Konfiguration --- const int EEPROM_ADDR_WIFI_CONFIG_MAGIC = 385; // 5 Bytes (char[5]) const int EEPROM_ADDR_WIFI_CONFIG_DATA = 390; // 113 Bytes (struct WiFiConfig) -> Ende ca. 503 // --- Sonstige Einstellungen --- const int EEPROM_ADDR_STEAM_DELAY = 527; // 4 Bytes (int) -> Ende 530 // --- AutoTune Parameter Dampf --- const int EEPROM_ADDR_TUNING_STEP_DAMPF = 531; // 8 Bytes (double) const int EEPROM_ADDR_TUNING_NOISE_DAMPF = 539; // 8 Bytes (double) const int EEPROM_ADDR_TUNING_STARTVALUE_DAMPF = 547; // 8 Bytes (double) const int EEPROM_ADDR_TUNING_LOOKBACK_DAMPF = 555; // 4 Bytes (unsigned int) -> Ende 558 // --- Adressen für Übertemperatursicherung / Sicherheitsabschaltung --- const int EEPROM_ADDR_MAX_TEMP_WASSER = 559; // 8 Bytes (double) -> Ende 566 const int EEPROM_ADDR_MAX_TEMP_DAMPF = 567; // 8 Bytes (double) -> Ende 574 // --- NEUE ADRESSEN --- const int EEPROM_ADDR_MAINTENANCE_INTERVAL = 575; // 4 Bytes (int) -> Ende 578 const int EEPROM_ADDR_MAINTENANCE_INTERVAL_COUNTER = 579; // 4 Bytes (int) -> Ende 578 const int EEPROM_ADDR_HOSTNAME = 583; // 33 Bytes (char[33]) -> Ende 611 /************************************************************************************ * Eco-Mode Variablen ************************************************************************************/ unsigned long lastShotTime = 0; // Wann wurde zuletzt ein Shot beendet int ecoModeMinutes = 0; // Zeit (Minuten) bis Eco-Modus bool ecoModeAktiv = 0; // Flag, ob Eco läuft int ecoModeTempWasser = 50; // Zieltemp Wasser im Eco int ecoModeTempDampf = 50; // Zieltemp Dampf im Eco bool dynamicEcoActive = false; unsigned long ecoModeActivatedTime = 0; /************************************************************************************ * Fast-Heat-Up Variablen ************************************************************************************/ bool fastHeatUpAktiv = 0; // Ist Fast-Heat-Up aktiviert? bool fastHeatUpHeating = 0; // Wird gerade hochgeheizt (Wasser >130)? int fastHeatUpSetpoint = 130; /************************************************************************************ * Sicherheits- und Fehlerstatus ************************************************************************************/ bool wasserSensorError = false; // Flag für Fehler am Wassertemperatursensor bool dampfSensorError = false; // Flag für Fehler am Dampftemperatursensor /************************************************************************************ * Auto-Tune-Einstellungen ************************************************************************************/ // Parameter für Wasser double tuningStepWasser; double tuningNoiseWasser; double tuningStartValueWasser; unsigned int tuningLookBackWasser; const double defaultTuningStepWasser = 500.0; // Angepasst für Zeitproportional (Output 0-2000) const double defaultTuningNoiseWasser = 1.0; const double defaultTuningStartValueWasser = 1000.0; // Mittlerer Output als Startwert const unsigned int defaultTuningLookBackWasser = 60; // Parameter für Dampf double tuningStepDampf; double tuningNoiseDampf; double tuningStartValueDampf; unsigned int tuningLookBackDampf; const double defaultTuningStepDampf = 300.0; // Angepasst für Zeitproportional const double defaultTuningNoiseDampf = 1.0; const double defaultTuningStartValueDampf = 1000.0; // Mittlerer Output als Startwert const unsigned int defaultTuningLookBackDampf = 30; PID_ATune* autoTuneWasser; PID_ATune* autoTuneDampf; bool autoTuneWasserActive = false; bool autoTuneDampfActive = false; /************************************************************************************ * Shot-Timer Variablen ************************************************************************************/ unsigned long shotStartTime = 0; unsigned long shotEndTime = 0; bool shotActive = false; unsigned long lastShotDurationMillis = 0; // Speichert die Dauer des letzten Shots in ms /************************************************************************************ * Shot-Zähler und Betriebszeit ************************************************************************************/ unsigned long shotCounter = 0; // Shots > 20 Sekunden unsigned long totalRuntime = 0; // Gesamt-Betriebszeit in Sekunden unsigned long lastRuntimeSave = 0; // Wann zuletzt gespeichert const unsigned long initialRuntimeSaveDelay = 10 * 60 * 1000UL; // 10 Minuten const unsigned long subsequentRuntimeSaveInterval = 5 * 60 * 1000UL; // 5 Minuten bool initialRuntimeSaveDone = false; /************************************************************************************ * NEU: Wartungserinnerung Variablen ************************************************************************************/ int maintenanceInterval = 0; // Intervall in Shots (0 = deaktiviert) unsigned long maintenanceIntervalCounter = 0; // Zähler für Wartungsintervall bool displayMaintenanceMessage = false; // Flag, ob Meldung aktiv angezeigt wird unsigned long maintenanceMessageStartTime = 0; // Startzeit der Meldungsanzeige /************************************************************************************ * Verzögerungen für Anzeigen ************************************************************************************/ int delayInit1 = 1500; // Display-Bild am Anfang int delayInit2 = 3000; // Warte für Anzeige unsigned long previousMillis = 0; // für Intervall z.B. 250ms const unsigned long interval = 250; // PID-Berechnungsintervall /************************************************************************************ * NEU: Hostname Variable ************************************************************************************/ char hostname[33] = "Dual-PID-Controller"; // Max 32 Zeichen + Nullterminator /************************************************************************************ * WiFi Signal Bitmap (Display-Anzeige) ************************************************************************************/ static const unsigned char PROGMEM wifiSymbol[] = { 0x1f, 0xc0, 0x20, 0x20, 0x4f, 0x90, 0x90, 0x48, 0x27, 0x20, 0x08, 0x80, 0x02, 0x00 }; /************************************************************************************ * Gemeinsame CSS-Styles (PROGMEM) - Unverändert ************************************************************************************/ static const char commonStyle[] PROGMEM = R"rawliteral( )rawliteral"; /************************************************************************************ * Gemeinsame Navigation (PROGMEM) - URLs angepasst ************************************************************************************/ static const char commonNav[] PROGMEM = R"rawliteral( )rawliteral"; /************************************************************************************ * WiFi-Konfigurationsstrukturen und Funktionen - Hostname hinzugefügt ************************************************************************************/ struct WiFiConfig { char ssid[32] = ""; char password[64] = ""; bool useStaticIP = false; IPAddress staticIP = IPAddress(192, 168, 4, 1); IPAddress gateway = IPAddress(192, 168, 4, 1); IPAddress subnet = IPAddress(255, 255, 255, 0); IPAddress dns = IPAddress(8, 8, 8, 8); // NEU: Hostname in WiFi-Config (obwohl global gespeichert, hier für Vollständigkeit) // Wird nicht direkt hier gespeichert, sondern über globale Variable `hostname` }; // Liest die WLAN-Konfiguration aus dem EEPROM // Gibt true zurück, wenn eine gültige Konfiguration (SSID) geladen wurde bool loadWiFiConfig(WiFiConfig& config) { char magic[5] = { 0 }; EEPROM.get(EEPROM_ADDR_WIFI_CONFIG_MAGIC, magic); // Lade Hostname (immer, auch wenn keine WiFi-Config da ist) EEPROM.get(EEPROM_ADDR_HOSTNAME, hostname); hostname[sizeof(hostname)-1] = '\0'; // Nullterminierung sicherstellen // Wenn Hostname leer ist (z.B. nach Reset), Default setzen if (strlen(hostname) == 0) { strncpy(hostname, defaultHostname.c_str(), sizeof(hostname) - 1); hostname[sizeof(hostname) - 1] = '\0'; EEPROM.put(EEPROM_ADDR_HOSTNAME, hostname); // Default im EEPROM speichern EEPROM.commit(); } // Falls MagicValue nicht übereinstimmt, Standardwerte setzen if (strncmp(magic, storageMagicValue, 4) != 0) { strcpy(config.ssid, ""); strcpy(config.password, ""); config.useStaticIP = false; config.staticIP = IPAddress(192, 168, 4, 1); config.gateway = IPAddress(192, 168, 4, 1); config.subnet = IPAddress(255, 255, 255, 0); config.dns = IPAddress(8, 8, 8, 8); return false; // Keine gültige Konfiguration gefunden } // Laden der gespeicherten Daten EEPROM.get(EEPROM_ADDR_WIFI_CONFIG_DATA, config); // Nullterminierung sicherstellen (wichtig!) config.ssid[sizeof(config.ssid)-1] = '\0'; config.password[sizeof(config.password)-1] = '\0'; // Prüfen, ob SSID leer ist (kann nach AP-Modus-Erzwingung passieren) return (strlen(config.ssid) > 0); // True, wenn SSID nicht leer ist } // Schreibt die WLAN-Konfiguration ins EEPROM void saveWiFiConfig(const WiFiConfig& config) { EEPROM.put(EEPROM_ADDR_WIFI_CONFIG_MAGIC, storageMagicValue); EEPROM.put(EEPROM_ADDR_WIFI_CONFIG_DATA, config); // Hostname wird separat gespeichert (in handleSaveWiFiConfig) EEPROM.commit(); } // Aktiviert den Access Point Modus und zeigt IP an void startAPMode() { WiFi.hostname(hostname); // Hostname auch für AP setzen WiFi.softAP(hostname); // AP-Name = Hostname Serial.print(F("AP-Mode IP: ")); Serial.println(WiFi.softAPIP()); } /************************************************************************************ * Webserver-Handler: WLAN-Konfigurations-Seite - Hostname hinzugefügt ************************************************************************************/ // === PROGMEM Teile definieren (angepasst für Signalstärke & Hostname) === static const char wifiPageChunk1[] PROGMEM = R"rawliteral( WLAN-Konfiguration )rawliteral"; static const char wifiPageChunk2[] PROGMEM = R"rawliteral()rawliteral"; // Head Ende // Teil 3a: Body bis VOR Signalstärke-Wert static const char wifiPageChunk3_a[] PROGMEM = R"rawliteral(

WLAN-Konfiguration

Netzwerk

)rawliteral"; // Endet vor dem Wert // Teil 3b: NACH Signalstärke-Wert bis VOR Hostname Value static const char wifiPageChunk3_b[] PROGMEM = R"rawliteral(

IP-Einstellungen

)rawliteral"; // Beginnt mit '', endet mit display:%s'> static const char wifiPageChunk6[] PROGMEM = R"rawliteral(

AP-Modus

Verwendung im Access Point-Modus (AP).
Erreichbarkeit unter IP-Adresse: 192.168.4.1
)rawliteral"; // Beginnt mit '', Rest bis Ende // --- Hilfsfunktion zur Umrechnung von RSSI in Prozent (Approximation) --- int calculateSignalPercentage(int rssi) { int quality = 0; if (rssi <= -100) { quality = 0; } else if (rssi >= -50) { // Guter Empfang quality = 100; } else { // Lineare Skalierung zwischen -100dBm (0%) und -50dBm (100%) quality = 2 * (rssi + 100); } return quality; } void handleWiFiConfig() { Serial.println(F("Executing handleWiFiConfig (v4: Hostname added)")); // EEPROM Lese-Logik (inkl. Hostname aus globaler Variable) WiFiConfig currentConfig; char magic[5] = { 0 }; EEPROM.get(EEPROM_ADDR_WIFI_CONFIG_MAGIC, magic); bool configValid = (strncmp(magic, storageMagicValue, 4) == 0); if (configValid) { EEPROM.get(EEPROM_ADDR_WIFI_CONFIG_DATA, currentConfig); currentConfig.ssid[sizeof(currentConfig.ssid)-1] = '\0'; currentConfig.password[sizeof(currentConfig.password)-1] = '\0'; } else { strcpy(currentConfig.ssid, ""); strcpy(currentConfig.password, ""); currentConfig.useStaticIP = false; currentConfig.staticIP = IPAddress(192, 168, 4, 1); currentConfig.gateway = IPAddress(192, 168, 4, 1); currentConfig.subnet = IPAddress(255, 255, 255, 0); } // Hostname wird aus globaler Variable `hostname` gelesen (wurde in loadWiFiConfig geladen) Serial.println(F("EEPROM logic done.")); // --- Signalstärke ermitteln --- char signalBuffer[50]; // Puffer für den Anzeigetext if (WiFi.getMode() == WIFI_STA && WiFi.status() == WL_CONNECTED) { long rssi = WiFi.RSSI(); // Signalstärke in dBm holen int percentage = calculateSignalPercentage(rssi); // Text formatieren (z.B. "-65 dBm (70%)") snprintf(signalBuffer, sizeof(signalBuffer), "%d dBm (%d%%)", (int)rssi, percentage); Serial.print(F("WiFi Signal: ")); Serial.println(signalBuffer); } else if (WiFi.getMode() == WIFI_AP) { strcpy(signalBuffer, "AP-Modus (keine Messung)"); Serial.println(F("WiFi Signal: AP Mode")); } else { strcpy(signalBuffer, "Nicht verbunden"); Serial.println(F("WiFi Signal: Not Connected")); } // --- Ende Signalstärke --- // Temporäre Puffer für Senden char formatBuffer[256]; // Für snprintf_P char ipBuffer[16]; // ---- Beginn des Sendens ---- server.setContentLength(CONTENT_LENGTH_UNKNOWN); server.send(200, F("text/html"), ""); Serial.println("Sent header"); // Chunk 1: HTML-Start server.sendContent_P(wifiPageChunk1); // Chunk 1.5: Common Style (global) server.sendContent_P(commonStyle); // Chunk 2: Head Ende server.sendContent_P(wifiPageChunk2); // Chunk 2.5: Common Nav (global, URLs angepasst) server.sendContent_P(commonNav); // Chunk 3a: Body bis VOR Signalwert server.sendContent_P(wifiPageChunk3_a); // Endet mit // Chunk 3.1: Signalwert senden server.sendContent(signalBuffer); // Sendet den formatierten Signal-String // Chunk 3b: NACH Signalwert bis VOR Hostname Value server.sendContent_P(wifiPageChunk3_b); // Endet mit value=' // Chunk 3.2: Hostname senden server.sendContent(hostname); // Sendet den aktuellen Hostnamen // Chunk 3c: NACH Hostname bis VOR SSID Value server.sendContent_P(wifiPageChunk3_c); // Beginnt mit ', endet mit value=' Serial.println("Sent initial chunks incl. Signal Strength & Hostname"); // Chunk 4: SSID einfügen (Handle Empty) if (strlen(currentConfig.ssid) == 0) { server.sendContent(F("SSID eingeben")); } else { server.sendContent(currentConfig.ssid); } // Chunk 5: Nach SSID bis vor Passwort server.sendContent_P(wifiPageChunk4); // Beginnt mit ', endet mit value=' // Chunk 6: Passwort einfügen (Handle Empty) if (!demoModus) { if (strlen(currentConfig.password) == 0) { server.sendContent(F("Passwort eingeben")); } else { server.sendContent(currentConfig.password); } } else { server.sendContent(F("*** *** *** ***")); } Serial.println("Sent SSID and Password chunks"); // Chunk 7: Radiobuttons und Div-Start (formatiert) snprintf_P(formatBuffer, sizeof(formatBuffer), wifiPageChunk5_format, currentConfig.useStaticIP ? "" : "checked", currentConfig.useStaticIP ? "checked" : "", currentConfig.useStaticIP ? "block" : "none" ); server.sendContent(formatBuffer); Serial.println("Sent combined Chunk 7 (IP Select Radios + Div Start)"); // === IP Felder (wie zuvor) === server.sendContent_P(wifiPageChunk6); snprintf(ipBuffer, sizeof(ipBuffer), "%d.%d.%d.%d", currentConfig.staticIP[0], currentConfig.staticIP[1], currentConfig.staticIP[2], currentConfig.staticIP[3]); server.sendContent(ipBuffer); server.sendContent_P(wifiPageChunk7); snprintf(ipBuffer, sizeof(ipBuffer), "%d.%d.%d.%d", currentConfig.gateway[0], currentConfig.gateway[1], currentConfig.gateway[2], currentConfig.gateway[3]); server.sendContent(ipBuffer); server.sendContent_P(wifiPageChunk8); snprintf(ipBuffer, sizeof(ipBuffer), "%d.%d.%d.%d", currentConfig.subnet[0], currentConfig.subnet[1], currentConfig.subnet[2], currentConfig.subnet[3]); server.sendContent(ipBuffer); Serial.println("Sent IP fields"); // Chunk 9: Rest der Seite server.sendContent_P(wifiPageChunk9); Serial.println("Sent Rest of Page"); // Letzter Chunk zum Abschließen server.sendContent(F("")); Serial.println("Sent final empty chunk"); Serial.println(F("Finished sending handleWiFiConfig (v4 Final)")); // ---- Ende des Sendens ---- } // Handler zum Speichern der WLAN-Konfiguration - Hostname hinzugefügt void handleSaveWiFiConfig() { if (!server.hasArg(F("ssid"))) { server.send(500, F("text/plain"), F("Fehler: SSID nicht gefunden")); return; } WiFiConfig newConfig; strncpy(newConfig.ssid, server.arg(F("ssid")).c_str(), sizeof(newConfig.ssid) - 1); newConfig.ssid[sizeof(newConfig.ssid) - 1] = '\0'; // Sicherstellen der Nullterminierung strncpy(newConfig.password, server.arg(F("password")).c_str(), sizeof(newConfig.password) - 1); newConfig.password[sizeof(newConfig.password) - 1] = '\0'; // Sicherstellen der Nullterminierung newConfig.useStaticIP = (server.arg(F("ipType")) == "static"); if (newConfig.useStaticIP) { newConfig.staticIP.fromString(server.arg(F("ip"))); newConfig.gateway.fromString(server.arg(F("gateway"))); newConfig.subnet.fromString(server.arg(F("subnet"))); // DNS wird hier nicht gespeichert, Standard bleibt } // NEU: Hostname speichern if (server.hasArg(F("hostname"))) { strncpy(hostname, server.arg(F("hostname")).c_str(), sizeof(hostname) - 1); hostname[sizeof(hostname) - 1] = '\0'; // Sicherstellen der Nullterminierung // Validierung (optional, z.B. auf erlaubte Zeichen prüfen) if (strlen(hostname) == 0) { // Wenn leer, Default verwenden strncpy(hostname, defaultHostname.c_str(), sizeof(hostname) - 1); hostname[sizeof(hostname) - 1] = '\0'; } EEPROM.put(EEPROM_ADDR_HOSTNAME, hostname); // Hostname ins EEPROM schreiben } // Im Demo-Modus wird nichts gespeichert, sonst schon if(!demoModus) { saveWiFiConfig(newConfig); // Speichert SSID, PW, IP-Config // EEPROM.commit() wird in saveWiFiConfig gemacht (inkl. Hostname-Speicherung) } static const char saveConfigHtml[] PROGMEM = R"rawliteral( Einstellungen gespeichert )rawliteral"; static const char saveConfigHtml2[] PROGMEM = R"rawliteral( )rawliteral"; static const char saveConfigHtml3[] PROGMEM = R"rawliteral(

Einstellungen gespeichert

Die Einstellungen wurden gespeichert. Neustart ...

)rawliteral"; String html = FPSTR(saveConfigHtml); html += FPSTR(commonStyle); html += FPSTR(saveConfigHtml2); html += FPSTR(commonNav); // URLs angepasst html += FPSTR(saveConfigHtml3); server.send(200, F("text/html"), html); delay(5000); ESP.restart(); } // Handler, um den AP-Modus per Knopfdruck zu erzwingen - Unverändert void handleForceAPMode() { // Leer-Konfiguration speichern, damit beim nächsten Start AP aktiv wird WiFiConfig emptyConfig; // Wichtig: Hostname NICHT löschen! if(!demoModus) { saveWiFiConfig(emptyConfig); } static const char forceAPHtml[] PROGMEM = R"rawliteral( AP-Modus aktivieren )rawliteral"; static const char forceAPHtml2[] PROGMEM = R"rawliteral( )rawliteral"; static const char forceAPHtml3[] PROGMEM = R"rawliteral(

AP-Modus wird aktiviert...

Die Einstellungen wurden gespeichert. Neustart im AP-Modus ...

)rawliteral"; String html = FPSTR(forceAPHtml); html += FPSTR(commonStyle); html += FPSTR(forceAPHtml2); html += FPSTR(commonNav); // URLs angepasst html += FPSTR(forceAPHtml3); server.send(200, F("text/html"), html); delay(1000); ESP.restart(); } // Prüfung der WiFi-Verbindung (Reconnect falls getrennt) - Unverändert void checkWifiConnection() { if (WiFi.status() != WL_CONNECTED) { WiFi.reconnect(); if (WiFi.waitForConnectResult() != WL_CONNECTED) { Serial.println(F("WiFi-Verbindung kann nicht hergestellt werden!")); } } } // WiFi-Symbol aufs Display zeichnen - Unverändert void drawwifiSymbol(int16_t x, int16_t y) { display.drawBitmap(x, y, wifiSymbol, 13, 7, SH110X_WHITE); } /************************************************************************************ * Handler zum Rücksetzen der Betriebszeit - URL angepasst ************************************************************************************/ void handleResetRuntime() { totalRuntime = 0; EEPROM.put(EEPROM_ADDR_RUNTIME, totalRuntime); EEPROM.commit(); server.sendHeader(F("Location"), F("/Info")); // URL angepasst server.send(303); } /************************************************************************************ * Handler zum Rücksetzen des Shotzählers - URL angepasst ************************************************************************************/ void handleResetShots() { shotCounter = 0; EEPROM.put(EEPROM_ADDR_SHOTCOUNTER, shotCounter); EEPROM.commit(); server.sendHeader(F("Location"), F("/Info")); // URL angepasst server.send(303); } /************************************************************************************ * NEU: Handler zum Rücksetzen des Wartungszählers (über Shot-Zähler) ************************************************************************************/ void handleResetMaintenance() { maintenanceIntervalCounter = 0; // Wartungszähler-Reset auf 0 EEPROM.put(EEPROM_ADDR_MAINTENANCE_INTERVAL_COUNTER, maintenanceIntervalCounter); EEPROM.commit(); displayMaintenanceMessage = false; // Meldung ggf. ausblenden Serial.println("Wartungszähler zurückgesetzt."); server.sendHeader(F("Location"), F("/Info")); // URL angepasst server.send(303); } /************************************************************************************ * Handler für den Export der EEPROM-Einstellungen - Unverändert ************************************************************************************/ void handleExportSettings() { Serial.println("Anfrage zum Export der EEPROM-Einstellungen empfangen."); // Header für Dateidownload setzen server.sendHeader("Content-Disposition", "attachment; filename=\"dual_pid_settings.bin\""); server.sendHeader("Content-Type", "application/octet-stream"); server.setContentLength(EEPROM_SIZE); // Wichtig: Korrekte Größe senden // EEPROM-Inhalt byteweise senden // Puffergröße klein halten, um Speicher zu sparen const size_t bufferSize = 64; byte buffer[bufferSize]; size_t bytesSent = 0; // Start des Sendens (Status 200 OK) server.send(200, "application/octet-stream", ""); // EEPROM lesen und in Chunks senden for (int i = 0; i < EEPROM_SIZE; i += bufferSize) { size_t currentChunkSize = (EEPROM_SIZE - i < bufferSize) ? (EEPROM_SIZE - i) : bufferSize; for (size_t j = 0; j < currentChunkSize; j++) { buffer[j] = EEPROM.read(i + j); } server.sendContent((const char*)buffer, currentChunkSize); bytesSent += currentChunkSize; yield(); // Wichtig für Netzwerkstabilität bei längeren Operationen } Serial.printf("EEPROM-Export abgeschlossen. %d Bytes gesendet.\n", bytesSent); // Kein server.stop() hier, da die Verbindung automatisch geschlossen wird. } /************************************************************************************ * Handler für den Import der EEPROM-Einstellungen (Upload-Verarbeitung) - Unverändert ************************************************************************************/ // Globale Variablen bleiben gleich static int eepromWriteAddress = 0; static bool importSuccessful = false; static String importStatusMessage = ""; void handleImportUpload() { HTTPUpload& upload = server.upload(); // Demo-Modus Prüfung (Bleibt bestehen) if (demoModus) { if (upload.status == UPLOAD_FILE_START) { Serial.println("FEHLER: EEPROM-Import im Demo-Modus nicht erlaubt!"); importStatusMessage = F("Import im Demo-Modus nicht erlaubt!"); importSuccessful = false; } return; } switch (upload.status) { case UPLOAD_FILE_START: Serial.printf("EEPROM Import gestartet: %s\n", upload.filename.c_str()); // Optional: Debug-Ausgabe kann bleiben, um zu sehen, was gemeldet wird Serial.printf("Info: Gemeldete upload.contentLength (kann Overhead enthalten): %u\n", upload.contentLength); Serial.printf("Info: Erwartete Nutzdaten-Größe (EEPROM_SIZE): %d\n", EEPROM_SIZE); eepromWriteAddress = 0; // Zähler zurücksetzen importSuccessful = false; // Status zurücksetzen importStatusMessage = ""; // Nachricht zurücksetzen // !!!!! KEINE GRÖSSENPRÜFUNG MEHR HIER !!!!! Serial.println("Starte Empfang der Upload-Daten..."); break; // Wichtig: break bleibt case UPLOAD_FILE_WRITE: // Prüfen, ob bereits ein Fehler aufgetreten ist (z.B. durch zu viele Daten im vorherigen Chunk) if (importStatusMessage.length() > 0) { Serial.println("Ignoriere weitere Schreibdaten wegen vorherigem Fehler."); yield(); break; // Beende die Verarbeitung für diesen Status } // Schreibe die empfangenen Bytes, aber schütze vor Überlauf // Serial.printf("Schreibe %d Bytes nach EEPROM Adresse %d\n", upload.currentSize, eepromWriteAddress); for (size_t i = 0; i < upload.currentSize; i++) { // Prüfen, ob das Schreiben das EEPROM_SIZE Limit überschreiten würde if (eepromWriteAddress < EEPROM_SIZE) { EEPROM.write(eepromWriteAddress, upload.buf[i]); eepromWriteAddress++; } else { // Es kommen MEHR Daten als erwartet! Serial.printf("FEHLER: Mehr als %d Bytes empfangen! Adresse: %d. Breche ab.\n", EEPROM_SIZE, eepromWriteAddress); importStatusMessage = F("Importfehler: Empfangene Datei zu groß!"); // 'ß' für ß // Breche die Verarbeitung dieses Chunks und zukünftiger Chunks ab goto end_write_processing; // Springe aus der Schleife und dem Case } } end_write_processing:; // Sprungziel für goto yield(); break; case UPLOAD_FILE_END: Serial.println("EEPROM Import Upload-Ende erreicht."); Serial.printf("Insgesamt verarbeitete Bytes: %d\n", eepromWriteAddress); // Wichtige Info // FINALE GRÖSSENPRÜFUNG und Fehlerprüfung // 1. Gab es einen Fehler während des Schreibens (z.B. Datei zu groß)? if (importStatusMessage.length() > 0) { Serial.println("Import nicht erfolgreich aufgrund vorheriger Fehler (z.B. Datei zu groß)."); importSuccessful = false; // Sicherstellen, dass Flag false ist } // 2. Wurden exakt EEPROM_SIZE Bytes verarbeitet? else if (eepromWriteAddress == EEPROM_SIZE) { Serial.println("Korrekte Anzahl Bytes empfangen. EEPROM Commit wird durchgeführt..."); if (EEPROM.commit()) { Serial.println("EEPROM Commit erfolgreich."); importSuccessful = true; importStatusMessage = F("Einstellungen erfolgreich importiert! Neustart wird durchgeführt..."); // 'ü' für ü } else { Serial.println("FEHLER: EEPROM Commit fehlgeschlagen!"); importSuccessful = false; importStatusMessage = F("Import fehlgeschlagen: EEPROM Commit Fehler!"); } } // 3. Wurden weniger oder mehr Bytes verarbeitet (und kein Fehler vorher gesetzt)? else { Serial.printf("FEHLER: Upload beendet, aber falsche Anzahl Bytes verarbeitet! Verarbeitet: %d, Erwartet: %d\n", eepromWriteAddress, EEPROM_SIZE); importSuccessful = false; importStatusMessage = F("Import fehlgeschlagen: Falsche Dateigröße!"); // 'ö', 'ß' } // Bereinigung für den nächsten Upload eepromWriteAddress = 0; break; case UPLOAD_FILE_ABORTED: Serial.println("EEPROM Import vom Client abgebrochen."); importSuccessful = false; // Nur Nachricht setzen, wenn nicht schon ein Fehler vorliegt if (importStatusMessage.length() == 0) { importStatusMessage = F("Import abgebrochen!"); } eepromWriteAddress = 0; break; default: Serial.printf("Unbekannter Upload-Status: %d\n", upload.status); break; } } /************************************************************************************ * Handler für die Import-Seite (nach dem Upload) - Unverändert ************************************************************************************/ void handleImportSettings() { // Verwende die Statusvariablen, die von handleImportUpload gesetzt wurden // Diese HTML-Teile werden im Browser angezeigt -> Umlaute ersetzen static const char importDoneHtmlStart[] PROGMEM = R"rawliteral( Einstellungen Import )rawliteral"; static const char importDoneHtmlHeadEnd[] PROGMEM = R"rawliteral()rawliteral"; static const char importDoneHtmlBody[] PROGMEM = R"rawliteral(

Einstellungen Import

{status_message}

)rawliteral"; String htmlResponse = FPSTR(importDoneHtmlStart); htmlResponse += FPSTR(commonStyle); // Annahme: commonStyle enthält keine Umlaute/ß für Browserausgabe oder diese sind bereits korrekt kodiert htmlResponse += FPSTR(importDoneHtmlHeadEnd); htmlResponse += FPSTR(commonNav); // URLs angepasst String body = FPSTR(importDoneHtmlBody); body.replace(F("{status_message}"), importStatusMessage); // Nachricht einfügen (Umlaute wurden bereits in handleImportUpload ersetzt) htmlResponse += body; server.send(200, F("text/html"), htmlResponse); // Wenn erfolgreich, Neustart nach kurzer Verzögerung if (importSuccessful) { Serial.println("Neustart in 5 Sekunden nach EEPROM-Import..."); delay(5000); ESP.restart(); } } /************************************************************************************ * NEU: Funktion zum Zurücksetzen auf Werkseinstellungen ************************************************************************************/ void resetToDefaults() { Serial.println("Zurücksetzen auf Werkseinstellungen..."); // PID & Offset EEPROM.put(EEPROM_ADDR_SETPOINT_DAMPF, defaultSetpointDampf); EEPROM.put(EEPROM_ADDR_SETPOINT_WASSER, defaultSetpointWasser); EEPROM.put(EEPROM_ADDR_OFFSET_DAMPF, defaultOffsetDampf); EEPROM.put(EEPROM_ADDR_OFFSET_WASSER, defaultOffsetWasser); EEPROM.put(EEPROM_ADDR_KP_DAMPF, defaultKpDampf); EEPROM.put(EEPROM_ADDR_KI_DAMPF, defaultKiDampf); EEPROM.put(EEPROM_ADDR_KD_DAMPF, defaultKdDampf); EEPROM.put(EEPROM_ADDR_KP_WASSER, defaultKpWasser); EEPROM.put(EEPROM_ADDR_KI_WASSER, defaultKiWasser); EEPROM.put(EEPROM_ADDR_KD_WASSER, defaultKdWasser); // Eco Modus EEPROM.put(EEPROM_ADDR_ECOMODE_MINUTES, defaultEcoModeMinutes); EEPROM.put(EEPROM_ADDR_ECOMODE_TEMP_WASSER, defaultEcoModeTempWasser); EEPROM.put(EEPROM_ADDR_ECOMODE_TEMP_DAMPF, defaultEcoModeTempDampf); EEPROM.put(EEPROM_ADDR_DYNAMIC_ECO_MODE, false); // Default für Dynamic Eco EEPROM.put(EEPROM_ADDR_STEAM_DELAY, defaultDampfVerzoegerung); // Geräteinfo char tempInfo[50]; strncpy(tempInfo, defaultInfoHersteller.c_str(), sizeof(tempInfo) - 1); tempInfo[sizeof(tempInfo) - 1] = '\0'; EEPROM.put(EEPROM_ADDR_INFO_HERSTELLER, tempInfo); strncpy(tempInfo, defaultInfoModell.c_str(), sizeof(tempInfo) - 1); tempInfo[sizeof(tempInfo) - 1] = '\0'; EEPROM.put(EEPROM_ADDR_INFO_MODELL, tempInfo); strncpy(tempInfo, defaultInfoZusatz.c_str(), sizeof(tempInfo) - 1); tempInfo[sizeof(tempInfo) - 1] = '\0'; EEPROM.put(EEPROM_ADDR_INFO_ZUSATZ, tempInfo); // Fast Heat Up EEPROM.put(EEPROM_ADDR_FASTHEATUP_DATA, false); // Default für Fast Heat Up // Profile (mit Default-Werten überschreiben) TemperatureProfile defaultProfile; defaultProfile.setpointWasser = defaultSetpointWasser; defaultProfile.setpointDampf = defaultSetpointDampf; defaultProfile.ecoModeMinutes = defaultEcoModeMinutes; defaultProfile.dynamicEcoActive = false; defaultProfile.dampfVerzoegerung = defaultDampfVerzoegerung; strncpy(defaultProfile.profileName, "Unbenannt", sizeof(defaultProfile.profileName)); defaultProfile.profileName[sizeof(defaultProfile.profileName) - 1] = '\0'; EEPROM.put(EEPROM_ADDR_PROFILE_1, defaultProfile); EEPROM.put(EEPROM_ADDR_PROFILE_2, defaultProfile); // AutoTune Parameter EEPROM.put(EEPROM_ADDR_TUNING_STEP_WASSER, defaultTuningStepWasser); EEPROM.put(EEPROM_ADDR_TUNING_NOISE_WASSER, defaultTuningNoiseWasser); EEPROM.put(EEPROM_ADDR_TUNING_STARTVALUE_WASSER, defaultTuningStartValueWasser); EEPROM.put(EEPROM_ADDR_TUNING_LOOKBACK_WASSER, defaultTuningLookBackWasser); EEPROM.put(EEPROM_ADDR_TUNING_STEP_DAMPF, defaultTuningStepDampf); EEPROM.put(EEPROM_ADDR_TUNING_NOISE_DAMPF, defaultTuningNoiseDampf); EEPROM.put(EEPROM_ADDR_TUNING_STARTVALUE_DAMPF, defaultTuningStartValueDampf); EEPROM.put(EEPROM_ADDR_TUNING_LOOKBACK_DAMPF, defaultTuningLookBackDampf); // Sicherheitslimits (verwenden die globalen Variablen, die beim Start initialisiert wurden) EEPROM.put(EEPROM_ADDR_MAX_TEMP_WASSER, maxTempWasser); // Diese sollten eigentlich auch Defaults haben EEPROM.put(EEPROM_ADDR_MAX_TEMP_DAMPF, maxTempDampf); // TODO: Eigene Defaults definieren? // WiFi Konfiguration löschen (erzwingt AP-Modus beim nächsten Start) char emptyMagic[5] = {0}; EEPROM.put(EEPROM_ADDR_WIFI_CONFIG_MAGIC, emptyMagic); // Hostname zurücksetzen strncpy(hostname, defaultHostname.c_str(), sizeof(hostname) - 1); hostname[sizeof(hostname) - 1] = '\0'; EEPROM.put(EEPROM_ADDR_HOSTNAME, hostname); // Wartungsintervall zurücksetzen EEPROM.put(EEPROM_ADDR_MAINTENANCE_INTERVAL_COUNTER, 0UL); EEPROM.put(EEPROM_ADDR_MAINTENANCE_INTERVAL, defaultMaintenanceInterval); // Zähler zurücksetzen (optional, aber sinnvoll) EEPROM.put(EEPROM_ADDR_RUNTIME, 0UL); EEPROM.put(EEPROM_ADDR_SHOTCOUNTER, 0UL); // Magic Value für allgemeine Einstellungen setzen (wichtig!) EEPROM.put(EEPROM_ADDR_MAGICVALUE, storageMagicValue); // Alle Änderungen ins EEPROM schreiben if (EEPROM.commit()) { Serial.println("Werkseinstellungen erfolgreich ins EEPROM geschrieben."); } else { Serial.println("FEHLER: Schreiben der Werkseinstellungen ins EEPROM fehlgeschlagen!"); } } /************************************************************************************ * NEU: Handler für das Zurücksetzen auf Werkseinstellungen ************************************************************************************/ void handleResetDefaults() { if (!demoModus) { resetToDefaults(); // Führt das eigentliche Reset durch } else { Serial.println("Werkseinstellungen im Demo-Modus nicht möglich."); // Optional: Fehlermeldung an den Client senden server.send(403, F("text/plain"), F("Reset im Demo-Modus nicht erlaubt.")); return; } // Bestätigungsseite senden und Neustart auslösen static const char resetDoneHtmlStart[] PROGMEM = R"rawliteral( Werkseinstellungen )rawliteral"; static const char resetDoneHtmlHeadEnd[] PROGMEM = R"rawliteral()rawliteral"; static const char resetDoneHtmlBody[] PROGMEM = R"rawliteral(

Werkseinstellungen

Alle Einstellungen wurden auf die Werkseinstellungen zurückgesetzt. Das Gerät wird neu gestartet...

)rawliteral"; String htmlResponse = FPSTR(resetDoneHtmlStart); htmlResponse += FPSTR(commonStyle); htmlResponse += FPSTR(resetDoneHtmlHeadEnd); htmlResponse += FPSTR(commonNav); // URLs angepasst htmlResponse += FPSTR(resetDoneHtmlBody); server.send(200, F("text/html"), htmlResponse); delay(5000); // Kurz warten, damit der Browser die Seite anzeigen kann ESP.restart(); } /************************************************************************************ * Variable für Systemstartzeit (relevant für die Dampf-Verzögerung) - Unverändert ************************************************************************************/ unsigned long startupTime; /************************************************************************************ * setup(): Initialisierung des Systems - Angepasst für neue EEPROM-Werte, Hostname, URLs ************************************************************************************/ void setup() { Serial.begin(115200); EEPROM.begin(EEPROM_SIZE); // Größe angepasst pinMode(SSR_DAMPF_PIN, OUTPUT); pinMode(SSR_WASSER_PIN, OUTPUT); pinMode(SHOT_TIMER_PIN, INPUT_PULLUP); // Initialisiere SSRs als AUS digitalWrite(SSR_DAMPF_PIN, LOW); digitalWrite(SSR_WASSER_PIN, LOW); // Prüfen, ob MagicValue bereits im EEPROM liegt char storedMagicValue[5] = { 0 }; bool magicValueVorhanden = false; EEPROM.get(EEPROM_ADDR_MAGICVALUE, storedMagicValue); if (strncmp(storedMagicValue, storageMagicValue, 4) == 0) { magicValueVorhanden = true; } else { magicValueVorhanden = false; // Hier noch KEINE Defaults schreiben, das passiert unten gebündelt // Nur Magic Value setzen, falls er fehlt // EEPROM.put(EEPROM_ADDR_MAGICVALUE, storageMagicValue); // EEPROM.commit(); // Commit erfolgt später } // Werte aus dem EEPROM laden, wenn vorhanden, sonst mit Default-Werten initialisieren if (magicValueVorhanden) { EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf); EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser); EEPROM.get(EEPROM_ADDR_OFFSET_DAMPF, OffsetDampf); EEPROM.get(EEPROM_ADDR_OFFSET_WASSER, OffsetWasser); EEPROM.get(EEPROM_ADDR_KP_DAMPF, KpDampf); EEPROM.get(EEPROM_ADDR_KI_DAMPF, KiDampf); EEPROM.get(EEPROM_ADDR_KD_DAMPF, KdDampf); EEPROM.get(EEPROM_ADDR_KP_WASSER, KpWasser); EEPROM.get(EEPROM_ADDR_KI_WASSER, KiWasser); EEPROM.get(EEPROM_ADDR_KD_WASSER, KdWasser); EEPROM.get(EEPROM_ADDR_ECOMODE_MINUTES, ecoModeMinutes); EEPROM.get(EEPROM_ADDR_ECOMODE_TEMP_WASSER, ecoModeTempWasser); EEPROM.get(EEPROM_ADDR_ECOMODE_TEMP_DAMPF, ecoModeTempDampf); EEPROM.get(EEPROM_ADDR_INFO_HERSTELLER, infoHersteller); EEPROM.get(EEPROM_ADDR_INFO_MODELL, infoModell); EEPROM.get(EEPROM_ADDR_INFO_ZUSATZ, infoZusatz); EEPROM.get(EEPROM_ADDR_DYNAMIC_ECO_MODE, dynamicEcoActive); EEPROM.get(EEPROM_ADDR_FASTHEATUP_DATA, fastHeatUpAktiv); EEPROM.get(EEPROM_ADDR_STEAM_DELAY, dampfVerzoegerung); // --- Bestehende Tuning-Parameter laden (jetzt Wasser) --- EEPROM.get(EEPROM_ADDR_TUNING_STEP_WASSER, tuningStepWasser); // Name geändert EEPROM.get(EEPROM_ADDR_TUNING_NOISE_WASSER, tuningNoiseWasser); // Name geändert EEPROM.get(EEPROM_ADDR_TUNING_STARTVALUE_WASSER, tuningStartValueWasser); // Name geändert EEPROM.get(EEPROM_ADDR_TUNING_LOOKBACK_WASSER, tuningLookBackWasser); // Name geändert // --- NEU: Dampf-Tuning-Parameter laden --- EEPROM.get(EEPROM_ADDR_TUNING_STEP_DAMPF, tuningStepDampf); EEPROM.get(EEPROM_ADDR_TUNING_NOISE_DAMPF, tuningNoiseDampf); EEPROM.get(EEPROM_ADDR_TUNING_STARTVALUE_DAMPF, tuningStartValueDampf); EEPROM.get(EEPROM_ADDR_TUNING_LOOKBACK_DAMPF, tuningLookBackDampf); // NEU: Sicherheitslimits laden EEPROM.get(EEPROM_ADDR_MAX_TEMP_WASSER, maxTempWasser); EEPROM.get(EEPROM_ADDR_MAX_TEMP_DAMPF, maxTempDampf); // NEU: Wartungsintervall laden EEPROM.get(EEPROM_ADDR_MAINTENANCE_INTERVAL, maintenanceInterval); // NEU: Hostname wird in loadWiFiConfig geladen // Sicherstellen, dass Strings nullterminiert sind infoHersteller[sizeof(infoHersteller)-1] = '\0'; infoModell[sizeof(infoModell)-1] = '\0'; infoZusatz[sizeof(infoZusatz)-1] = '\0'; } else // Magic Value fehlt -> ALLES auf Default setzen { Serial.println("Magic Value nicht gefunden. Setze alle EEPROM-Werte auf Defaults."); // Setze globale Variablen auf Defaults SetpointDampf = defaultSetpointDampf; SetpointWasser = defaultSetpointWasser; OffsetDampf = defaultOffsetDampf; OffsetWasser = defaultOffsetWasser; KpDampf = defaultKpDampf; KiDampf = defaultKiDampf; KdDampf = defaultKdDampf; KpWasser = defaultKpWasser; KiWasser = defaultKiWasser; KdWasser = defaultKdWasser; ecoModeMinutes = defaultEcoModeMinutes; ecoModeTempWasser = defaultEcoModeTempWasser; ecoModeTempDampf = defaultEcoModeTempDampf; strncpy(infoHersteller, defaultInfoHersteller.c_str(), sizeof(infoHersteller)-1); infoHersteller[sizeof(infoHersteller)-1] = '\0'; strncpy(infoModell, defaultInfoModell.c_str(), sizeof(infoModell)-1); infoModell[sizeof(infoModell)-1] = '\0'; strncpy(infoZusatz, defaultInfoZusatz.c_str(), sizeof(infoZusatz)-1); infoZusatz[sizeof(infoZusatz)-1] = '\0'; dynamicEcoActive = false; fastHeatUpAktiv = false; dampfVerzoegerung = defaultDampfVerzoegerung; tuningStepWasser = defaultTuningStepWasser; tuningNoiseWasser = defaultTuningNoiseWasser; tuningStartValueWasser = defaultTuningStartValueWasser; tuningLookBackWasser = defaultTuningLookBackWasser; tuningStepDampf = defaultTuningStepDampf; tuningNoiseDampf = defaultTuningNoiseDampf; tuningStartValueDampf = defaultTuningStartValueDampf; tuningLookBackDampf = defaultTuningLookBackDampf; // maxTempWasser/Dampf behalten ihre initialen Codedefaults maintenanceInterval = defaultMaintenanceInterval; maintenanceIntervalCounter = 0; strncpy(hostname, defaultHostname.c_str(), sizeof(hostname)-1); hostname[sizeof(hostname)-1] = '\0'; // Rufe resetToDefaults auf, um EEPROM zu schreiben resetToDefaults(); // Schreibt alle Defaults und committet } // WiFi-Einstellungen laden und ggf. herstellen WiFiConfig wifiConfig; bool hasWiFiConfig = loadWiFiConfig(wifiConfig); // Lädt auch Hostname WiFi.hostname(hostname); // Hostname setzen VOR begin/softAP if (hasWiFiConfig) { WiFi.mode(WIFI_STA); if (wifiConfig.useStaticIP) { WiFi.config(wifiConfig.staticIP, wifiConfig.gateway, wifiConfig.subnet); } Serial.printf("Verbinde mit SSID: %s, Hostname: %s\n", wifiConfig.ssid, hostname); WiFi.begin(wifiConfig.ssid, wifiConfig.password); int retries = 0; while (WiFi.status() != WL_CONNECTED && retries < 20) { delay(500); Serial.print(F(".")); retries++; } if (WiFi.status() == WL_CONNECTED) { Serial.println(F("\nVerbunden!")); Serial.print(F("IP-Adresse: ")); Serial.println(WiFi.localIP()); // mDNS starten if (MDNS.begin(hostname)) { Serial.println("mDNS Responder gestartet"); MDNS.addService("http", "tcp", 80); // Service für Webserver anmelden } else { Serial.println("Fehler beim Starten des mDNS Responders!"); } } else { Serial.println(F("\nKonnte keine Verbindung herstellen! Starte AP-Modus...")); startAPMode(); } } else { Serial.println("Keine WLAN-Konfiguration gefunden. Starte AP-Modus..."); startAPMode(); } // Wartungszähler, Betriebszeit und Shotcounter aus EEPROM laden EEPROM.get(EEPROM_ADDR_RUNTIME, totalRuntime); if (totalRuntime == ULONG_MAX || totalRuntime > (86400UL * 365 * 10)) { // Plausibilitätscheck (z.B. > 10 Jahre) Serial.println("Ungültige Betriebszeit im EEPROM, setze auf 0."); totalRuntime = 0; EEPROM.put(EEPROM_ADDR_RUNTIME, totalRuntime); // Korrigierten Wert speichern EEPROM.commit(); } EEPROM.get(EEPROM_ADDR_SHOTCOUNTER, shotCounter); if (shotCounter == ULONG_MAX || shotCounter > 1000000UL) // Plausibilitätscheck { Serial.println("Ungültiger Shotcounter im EEPROM, setze auf 0."); shotCounter = 0; EEPROM.put(EEPROM_ADDR_SHOTCOUNTER, shotCounter); EEPROM.commit(); } EEPROM.get(EEPROM_ADDR_MAINTENANCE_INTERVAL_COUNTER, maintenanceIntervalCounter); if (maintenanceIntervalCounter == ULONG_MAX || maintenanceIntervalCounter > 1000000UL) // Plausibilitätscheck { Serial.println("Ungültiger Wartungszähler im EEPROM, setze auf 0."); maintenanceIntervalCounter = 0; EEPROM.put(EEPROM_ADDR_MAINTENANCE_INTERVAL_COUNTER, maintenanceIntervalCounter); EEPROM.commit(); } if (fastHeatUpAktiv) { fastHeatUpHeating = true; } // OLED-Initialisierung Wire.begin(OLED_SDA, OLED_SCK); if (!display.begin(0x3C, true)) { Serial.println(F("Display konnte nicht initialisiert werden!")); } display.clearDisplay(); display.setCursor(0, 0); display.setTextColor(SH110X_WHITE); display.setTextSize(1); display.println(infoHersteller); display.println(infoModell); display.println(infoZusatz); display.println(""); display.println(F("Dual-PID-Controller")); display.print(F("Version: ")); display.println(version); display.println(F("von Thomas M\201ller")); // \201 ist ü für manche Displays display.display(); delay(delayInit1); // --- PID-Regler Konfiguration --- // Setze Output-Grenzen auf die Fenstergröße für zeitproportionale Steuerung pidWasser.SetOutputLimits(0, windowSizeWasser); pidDampf.SetOutputLimits(0, windowSizeDampf); // Setze PID-Parameter pidDampf.SetTunings(KpDampf, KiDampf, KdDampf); pidWasser.SetTunings(KpWasser, KiWasser, KdWasser); // Setze PID-Modus auf AUTOMATIC pidDampf.SetMode(AUTOMATIC); pidWasser.SetMode(AUTOMATIC); // Initialisiere Startzeiten für die Zeitfenster windowStartTimeWasser = millis(); windowStartTimeDampf = millis(); // Anzeige zur WiFi-Verbindung display.clearDisplay(); display.setCursor(0, 0); display.setTextColor(SH110X_WHITE); display.setTextSize(1); display.println(infoHersteller); display.println(infoModell); display.println(infoZusatz); display.println(""); if (WiFi.status() != WL_CONNECTED) { display.println(F("WiFi: AP-Modus")); display.print(F("SSID: ")); display.println(hostname); display.print(F("IP: ")); display.println(WiFi.softAPIP()); } else { display.println(F("Webserver gestartet")); display.print(F("IP: ")); display.println(WiFi.localIP()); display.print(F("Host: ")); display.print(hostname); display.println(".local"); } display.display(); delay(delayInit2); // Routen für Webserver (URLs angepasst) server.on("/", handleRoot); server.on("/Info", handleInfo); // URL geändert server.on("/ECO", handleEco); // URL geändert server.on("/Fast-Heat-Up", handleFastHeatUp); // URL geändert server.on("/updateFast-Heat-Up-Settings", handleFastHeatUpSettings); server.on("/updateSettings", handleUpdate); server.on("/updateInfoSettings", handleInfoUpdate); server.on("/updateMaintenanceSettings", handleMaintenanceUpdate); server.on("/updateEcoSettings", handleEcoUpdate); // AutoTune-Route (Wasser) server.on("/autoTuneWasser", []() { String page = F("\n\n\nAutoTune Wasser\n"); page += FPSTR(commonStyle); page += F("\n"); page += FPSTR(commonNav); // URLs angepasst page += F("\n

PID-Tuning

\n"); if (!autoTuneWasserActive && !autoTuneDampfActive) { startAutoTuneWasser(); page += F("
\n" "

AutoTune gestartet: Wasser-PID

\n" "
\n" "\n" "
\n"); } else { page += F("
\n" "

AutoTune ist bereits aktiv!

\n" "
\n" "\n" "
\n"); } page += F("\n"); server.send(200, F("text/html"), page); }); // AutoTune-Route (Dampf) server.on("/autoTuneDampf", []() { String page = F("\n\n\nAutoTune Dampf\n"); page += FPSTR(commonStyle); page += F("\n"); page += FPSTR(commonNav); // URLs angepasst page += F("\n

PID-Tuning

\n"); if (!autoTuneWasserActive && !autoTuneDampfActive) { startAutoTuneDampf(); page += F("
\n" "

AutoTune gestartet: Dampf-PID

\n" "
\n" "\n" "
\n"); } else { page += F("
\n" "

AutoTune ist bereits aktiv!

\n" "
\n" "\n" "
\n"); } page += F("\n"); server.send(200, F("text/html"), page); }); // Hauptseite PID-Tuning (Info, Starten etc.) server.on("/PID-Tuning", HTTP_GET, []() { String page = F("\n\n\nPID-Tuning\n"); page += FPSTR(commonStyle); page += F("\n"); page += FPSTR(commonNav); // URLs angepasst page += F("\n"); if (autoTuneWasserActive || autoTuneDampfActive) { page += F("
\n" "

Laufendes PID-Tuning abbrechen?

\n" "\n" "

\n"); } page += F("

PID-Tuning

\n" "
Das automatische PID-Tuning dient dazu, die optimalen Parameter für den PID-Regler (Proportional-, Integral- und Differentialanteil) selbständig zu ermitteln. Dabei analysiert das System mithilfe von Algorithmen das Regelverhalten (z. B. Reaktion auf einen Testimpuls) und passt Kp, Ki und Kd so an, dass gewünschte Kriterien wie kurze Einschwingzeit, geringe Überschwingung und stabile Regelung erreicht werden" "

" "Ein AutoTune-Vorgang kann durchaus 10-20 Minuten in Anspruch nehmen!" "

" "WICHTIG:
" "Vor dem Start des PID-Tunings sollte die Maschine die gewünschte Betriebstemperatur erreicht haben und stabil sein.
" "Andernfalls könnten die automatisch ermittelten PID-Werte für diesen Temperaturbereich ungenau oder nicht optimal sein." "

" "
" "
\n" "

Automatisches PID-Tuning für Wasser

\n" "\n" "
\n" "
\n" "

Automatisches PID-Tuning für Dampf

\n" "\n" "
\n"); // Form für AutoTune-Parameter page += F("
\n" "

AutoTune-Parameter (Zeitproportional)

\n" // Titel angepasst "Hier können die AutoTune-Parameter geändert werden.
Änderungen sollten mit Vorsicht durchgeführt werden!

" "

Parameter Wasser / Kessel

" "" // Beschreibung angepasst "" "" // Einheit hinzugefügt "" "" // Beschreibung angepasst "" "" "" "

Parameter Dampf / Thermoblock

" "" // Beschreibung angepasst "" "" // Einheit hinzugefügt "" "" // Beschreibung angepasst "" "" "" "\n" "
\n" "


" "
" "

Erklärung der Parameter (Zeitproportional):

\n" // Titel angepasst "

Step:
\n" "Dieser Wert bestimmt, wie stark der Ausgangswert (innerhalb des Bereichs 0 bis 2000ms) während des AutoTune-Vorgangs verändert wird. Er gibt die Größe des Impulses an, der die Einschaltdauer des SSRs beeinflusst.

\n" "

Noise:
\n" "Dieser Parameter definiert einen Toleranzbereich (Noise-Band) für die gemessenen Eingangstemperaturen (°C). Kleine Schwankungen unterhalb dieses Wertes werden ignoriert.

\n" "

StartValue:
\n" "Hier wird der Anfangswert für den Ausgang des Reglers (0 bis 2000ms) festgelegt, wenn der AutoTune-Vorgang startet. Dieser Wert bestimmt die initiale Einschaltdauer des SSRs.

\n" "

LookBack:
\n" "Dieser Parameter legt fest, über welchen Zeitraum (in Sekunden) der Algorithmus die vergangene Systemantwort analysiert, um die Schwingungsperiode zu messen.

\n" "
" "\n"); // --- Placeholders ersetzen --- page.replace("{WINDOW_SIZE_WASSER}", String(windowSizeWasser)); // NEU page.replace("{WINDOW_SIZE_DAMPF}", String(windowSizeDampf)); // NEU page.replace("{TUNING_STEP_WASSER}", String(tuningStepWasser, 1)); page.replace("{TUNING_NOISE_WASSER}", String(tuningNoiseWasser, 1)); page.replace("{TUNING_STARTVALUE_WASSER}", String(tuningStartValueWasser, 1)); page.replace("{TUNING_LOOKBACK_WASSER}", String(tuningLookBackWasser)); page.replace("{TUNING_STEP_DAMPF}", String(tuningStepDampf, 1)); page.replace("{TUNING_NOISE_DAMPF}", String(tuningNoiseDampf, 1)); page.replace("{TUNING_STARTVALUE_DAMPF}", String(tuningStartValueDampf, 1)); page.replace("{TUNING_LOOKBACK_DAMPF}", String(tuningLookBackDampf)); server.send(200, F("text/html"), page); }); // Speichern der AutoTune-Einstellungen server.on("/updateAutotuneSettings", HTTP_POST, []() { bool changed = false; // Flag, um zu prüfen, ob EEPROM.commit() nötig ist // --- Wasser Parameter --- if (server.hasArg(F("tuningStepWasser"))) { tuningStepWasser = server.arg(F("tuningStepWasser")).toFloat(); EEPROM.put(EEPROM_ADDR_TUNING_STEP_WASSER, tuningStepWasser); changed = true; } if (server.hasArg(F("tuningNoiseWasser"))) { tuningNoiseWasser = server.arg(F("tuningNoiseWasser")).toFloat(); EEPROM.put(EEPROM_ADDR_TUNING_NOISE_WASSER, tuningNoiseWasser); changed = true; } if (server.hasArg(F("tuningStartValueWasser"))) { tuningStartValueWasser = server.arg(F("tuningStartValueWasser")).toFloat(); EEPROM.put(EEPROM_ADDR_TUNING_STARTVALUE_WASSER, tuningStartValueWasser); changed = true; } if (server.hasArg(F("tuningLookBackWasser"))) { tuningLookBackWasser = server.arg(F("tuningLookBackWasser")).toInt(); EEPROM.put(EEPROM_ADDR_TUNING_LOOKBACK_WASSER, tuningLookBackWasser); changed = true; } // --- Dampf Parameter (NEU) --- if (server.hasArg(F("tuningStepDampf"))) { tuningStepDampf = server.arg(F("tuningStepDampf")).toFloat(); EEPROM.put(EEPROM_ADDR_TUNING_STEP_DAMPF, tuningStepDampf); changed = true; } if (server.hasArg(F("tuningNoiseDampf"))) { tuningNoiseDampf = server.arg(F("tuningNoiseDampf")).toFloat(); EEPROM.put(EEPROM_ADDR_TUNING_NOISE_DAMPF, tuningNoiseDampf); changed = true; } if (server.hasArg(F("tuningStartValueDampf"))) { tuningStartValueDampf = server.arg(F("tuningStartValueDampf")).toFloat(); EEPROM.put(EEPROM_ADDR_TUNING_STARTVALUE_DAMPF, tuningStartValueDampf); changed = true; } if (server.hasArg(F("tuningLookBackDampf"))) { tuningLookBackDampf = server.arg(F("tuningLookBackDampf")).toInt(); EEPROM.put(EEPROM_ADDR_TUNING_LOOKBACK_DAMPF, tuningLookBackDampf); changed = true; } // Nur speichern, wenn sich etwas geändert hat if (changed) { EEPROM.commit(); } server.sendHeader(F("Location"), F("/PID-Tuning")); // URL bleibt gleich server.send(303); }); // Chart-Anzeige-Handler - URL angepasst server.on("/Charts", handleCharts); // URL geändert // JSON-Daten für Temperaturwerte - Unverändert server.on("/data", []() { String json = F("{"); json += F("\"wasser\":") + String(InputWasser) + F(","); json += F("\"setpointwasser\":") + String(SetpointWasser) + F(","); json += F("\"dampf\":") + String(InputDampf) + F(","); json += F("\"setpointdampf\":") + String(SetpointDampf); json += F("}"); server.send(200, F("application/json"), json); }); // Firmware-Update-Seite - URL angepasst, Reset-Button hinzugefügt server.on("/Firmware", HTTP_GET, []() { // URL geändert String page = F("\n\n\nFirmware & Einstellungen\n"); // Titel angepasst page += FPSTR(commonStyle); page += F("\n"); page += FPSTR(commonNav); // URLs angepasst // ----- HIER BEGINNT DER GROSSE F() BLOCK ----- page += F("\n" "

Firmware & Einstellungen

\n" // Titel angepasst // --- Firmware Info Sektion (unverändert) --- "
\n" "

Firmware-Info

\n" "Firmware-Version: {FIRMWAREVERSION}
\n" "Hersteller: {SWHERSTELLER}
\n" "E-Mail: {SWHERSTELLERMAIL}
\n" "Website: {SWHERSTELLERWEBSITE}
\n" "
\n" // --- Firmware Update Sektion (unverändert) --- "
\n" "

Firmware-Update

\n" "Das Firmware-Update kann durch den Upload einer .bin-Datei durchgeführt werden.
\n" "Nach dem Update wird ein automatischer Neustart durchgeführt.
\n" "Sollte der Neustart nicht erfolgen, so kann dieser auch durch kurzzeitiges Trennen der Stromversorgung erfolgen.
\n" "
\n" "\n" // accept hinzugefügt "
" "\n" "
\n" // --- NEUE SEKTION: Einstellungen Import/Export --- // Export "
\n" "

Einstellungen exportieren

" "Sicherung aller aktuellen Einstellungen (gesamter EEPROM-Inhalt) in einer Datei.
\n" "Einstellungen Exportieren (.bin)\n" "
\n" // Import "
\n" "

Einstellungen importieren

" "Alle Einstellungen aus einer zuvor exportierten .bin-Datei wiederherstellen.

\n" "ACHTUNG:
" "Dieser Vorgang überschreibt ALLE aktuellen Einstellungen im EEPROM!
\n" "Die Datei muss exakt {EEPROM_SIZE_BYTES} Bytes groß sein.
\n" "Nach dem Import wird das Gerät automatisch neu gestartet.
\n" "
\n" "\n" // accept und required hinzugefügt "
\n" "\n" "
\n" // --- ENDE IMPORT/EXPORT SEKTION --- // --- NEU: Werkseinstellungen --- "
\n" // Bestätigungsdialog hinzugefügt "

Werkseinstellungen

\n" "Setzt ALLE Einstellungen (PID, Eco, Profile, WiFi, etc.) auf die Standardwerte zurück.

\n" "ACHTUNG:
" "Dieser Vorgang kann nicht rückgängig gemacht werden!
\n" "Das Gerät wird nach dem Zurücksetzen neu gestartet (ggf. im AP-Modus, wenn keine WiFi-Daten hinterlegt waren).\n" "
\n" "\n" // Roter Button "
\n" // --- ENDE WERKSEINSTELLUNGEN --- "\n"); // ----- HIER ENDET DER GROSSE F() BLOCK ----- // --- Platzhalter ersetzen (NACHDEM der F()-Block hinzugefügt wurde) --- page.replace(F("{FIRMWAREVERSION}"), version); page.replace(F("{SWHERSTELLER}"), versionHersteller); page.replace(F("{SWHERSTELLERMAIL}"), versionHerstellerMail); page.replace(F("{SWHERSTELLERWEBSITE}"), versionHerstellerWeb); page.replace(F("{EEPROM_SIZE_BYTES}"), String(EEPROM_SIZE)); server.send(200, F("text/html"), page); }); // Firmware-Upload-Handler - Unverändert server.on("/update", HTTP_POST, []() { static const char updateDone[] PROGMEM = R"rawliteral( Firmware-Update )rawliteral"; static const char updateDone2[] PROGMEM = R"rawliteral( )rawliteral"; static const char updateDone3[] PROGMEM = R"rawliteral(

{status_message}

Weiterleitung zur Startseite in 30 Sekunden ...

)rawliteral"; String htmlResponse = FPSTR(updateDone); htmlResponse += FPSTR(commonStyle); htmlResponse += FPSTR(updateDone2); htmlResponse += FPSTR(commonNav); // URLs angepasst htmlResponse += FPSTR(updateDone3); String statusMessage; if (!passwordFound) { statusMessage = F("Update abgebrochen! Die Firmware konnte nicht validiert werden."); } else { if (Update.hasError()) { statusMessage = F("Update fehlgeschlagen!"); } else { statusMessage = F("Update erfolgreich!"); } } htmlResponse.replace(F("{status_message}"), statusMessage); server.send(200, F("text/html"), htmlResponse); delay(1000); ESP.restart(); }, []() { HTTPUpload& upload = server.upload(); if (upload.status == UPLOAD_FILE_START) { Serial.printf("Update gestartet: %s\n", upload.filename.c_str()); passwordFound = false; passMatchPos = 0; if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) { Update.printError(Serial); } } else if (upload.status == UPLOAD_FILE_WRITE) { // Prüfen auf Firmware-Passwort for (size_t i = 0; i < upload.currentSize; i++) { char c = (char)upload.buf[i]; if (c == FIRMWARE_PASSWORD[passMatchPos]) { passMatchPos++; if (passMatchPos == passLength) { passwordFound = true; } } else { if (c == FIRMWARE_PASSWORD[0]) { passMatchPos = 1; } else { passMatchPos = 0; } } } // Schreibvorgang if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { Update.printError(Serial); } } else if (upload.status == UPLOAD_FILE_END) { if (!passwordFound) { Serial.println("Kennwort nicht gefunden -> Abbruch!"); Update.end(false); } else { if (Update.end(true)) { Serial.printf("Update erfolgreich: %u Bytes\n", upload.totalSize); } else { Update.printError(Serial); } } } else if (upload.status == UPLOAD_FILE_ABORTED) { Update.end(); Serial.println(F("Update abgebrochen.")); } yield(); }); // Handler zum Abbrechen des PID-AutoTune - Unverändert server.on("/abbruch-pid-tuning", HTTP_POST, []() { static const char abortTuning[] PROGMEM = R"rawliteral( Info )rawliteral"; static const char abortTuning2[] PROGMEM = R"rawliteral( )rawliteral"; static const char abortTuning3[] PROGMEM = R"rawliteral(

PID-Tuning Abbruch

Das PID-Tuning wurde erfolgreich abgebrochen.

Der Normalbetrieb wird nun fortgesetzt.

)rawliteral"; String html = FPSTR(abortTuning); html += FPSTR(commonStyle); html += FPSTR(abortTuning2); html += FPSTR(commonNav); // URLs angepasst html += FPSTR(abortTuning3); server.send(200, F("text/html"), html); stopAutoTuneWasser(); stopAutoTuneDampf(); }); // Routen für WLAN-Einstellungen - URL angepasst server.on("/Netzwerk", handleWiFiConfig); // URL geändert server.on("/saveWiFiConfig", handleSaveWiFiConfig); server.on("/forceAPMode", handleForceAPMode); // Routen zum Zurücksetzen von Betriebszeit und ShotCounter - URL angepasst server.on("/resetRuntime", handleResetRuntime); server.on("/resetShots", handleResetShots); // NEU: Route zum Zurücksetzen des Wartungszählers server.on("/resetMaintenance", handleResetMaintenance); // Routen für Profile - URL angepasst server.on("/Profile", handleProfiles); // URL geändert server.on("/saveProfile1", handleSaveProfile1); server.on("/loadProfile1", handleLoadProfile1); server.on("/saveProfile2", handleSaveProfile2); server.on("/loadProfile2", handleLoadProfile2); // Routen für EEPROM Import/Export - Unverändert server.on("/exportSettings", HTTP_GET, handleExportSettings); server.on("/importSettings", HTTP_POST, handleImportSettings, handleImportUpload); // NEU: Route für Werkseinstellungen server.on("/resetDefaults", HTTP_POST, handleResetDefaults); // Webserver starten server.begin(); Serial.println(F("Webserver gestartet")); startupTime = millis(); } /************************************************************************************ * loop(): Hauptschleife - mDNS Update hinzugefügt ************************************************************************************/ void loop() { unsigned long currentMillis = millis(); // --- Temperaturmessung und PID-Berechnung (im Intervall) --- if (currentMillis - previousMillis >= interval) { previousMillis = currentMillis; // Temperaturmessung InputDampf = round(thermocouple.readCelsius() + OffsetDampf); InputWasser = round(readNTCTemperature() + OffsetWasser); // --- Sicherheitsüberprüfung Wasser --- if (InputWasser < 0.0 || InputWasser > maxTempWasser || isnan(InputWasser)) // Auch NaN prüfen { wasserSensorError = true; OutputWasser = 0; // PID-Output auf 0 setzen Serial.println("WARNUNG: Wassertemperatur ungültig oder außerhalb des Bereichs!"); } else { wasserSensorError = false; // PID nur berechnen, wenn kein Fehler und kein AutoTune aktiv if (!autoTuneWasserActive) { pidWasser.Compute(); } // AutoTune hat seine eigene Logik zum Setzen von OutputWasser } // --- Sicherheitsüberprüfung Dampf --- if (isnan(InputDampf) || InputDampf < 0.0 || InputDampf > maxTempDampf) { dampfSensorError = true; OutputDampf = 0; // PID-Output auf 0 setzen Serial.println("WARNUNG: Dampftemperatur ungültig oder außerhalb des Bereichs!"); } else { dampfSensorError = false; // PID nur berechnen, wenn kein Fehler, kein AutoTune und keine Startverzögerung aktiv if (!autoTuneDampfActive) { // Dampf-Startverzögerung prüfen if (dampfVerzoegerung > 0 && (millis() - startupTime < (unsigned long)dampfVerzoegerung * 60000UL)) { // Während der Verzögerung: Heizen verhindern OutputDampf = 0; // Sicherstellen, dass PID Output 0 ist } else { // Normalbetrieb nach Verzögerung und ohne Sensorfehler pidDampf.Compute(); } } // AutoTune hat seine eigene Logik zum Setzen von OutputDampf } // AutoTune-Logik (wird jetzt nur noch Output setzen, SSR-Steuerung unten) handleAutoTune(); } // Ende des Temperaturmessungs-/PID-Berechnungsintervalls // --- Zeitproportionale SSR-Ansteuerung (wird kontinuierlich geprüft) --- unsigned long now = millis(); // --- Wasser SSR --- if (wasserSensorError) { digitalWrite(SSR_WASSER_PIN, LOW); // Sicherheit: Bei Fehler immer AUS } else { // Fenster neu starten, wenn Zeit abgelaufen if (now - windowStartTimeWasser > windowSizeWasser) { windowStartTimeWasser += windowSizeWasser; } // SSR einschalten, wenn aktuelle Zeit innerhalb der ON-Phase des Fensters liegt // OutputWasser ist jetzt der ON-Zeitraum in ms (0 bis windowSizeWasser) if (OutputWasser > 0 && (now < (windowStartTimeWasser + (unsigned long)OutputWasser))) { digitalWrite(SSR_WASSER_PIN, HIGH); } else { digitalWrite(SSR_WASSER_PIN, LOW); } } // --- Dampf SSR --- if (dampfSensorError) { digitalWrite(SSR_DAMPF_PIN, LOW); // Sicherheit: Bei Fehler immer AUS } else { // Fenster neu starten, wenn Zeit abgelaufen if (now - windowStartTimeDampf > windowSizeDampf) { windowStartTimeDampf += windowSizeDampf; } // SSR einschalten, wenn aktuelle Zeit innerhalb der ON-Phase des Fensters liegt // OutputDampf ist jetzt der ON-Zeitraum in ms (0 bis windowSizeDampf) // Beachte: OutputDampf kann durch die Startverzögerung oder Fehler auf 0 gesetzt sein if (OutputDampf > 0 && (now < (windowStartTimeDampf + (unsigned long)OutputDampf))) { digitalWrite(SSR_DAMPF_PIN, HIGH); } else { digitalWrite(SSR_DAMPF_PIN, LOW); } } // Shot-Timer verarbeiten (unverändert) updateShotTimer(); // Eco-Modus (unverändert - Sicherheitschecks haben Vorrang vor Eco-Sollwerten) if (!autoTuneWasserActive && !autoTuneDampfActive) { if (ecoModeMinutes > 0 && !shotActive) { unsigned long currentTimeEco = millis(); if (currentTimeEco - lastShotTime > (ecoModeMinutes * 60UL * 1000UL)) { if (!ecoModeAktiv) { ecoModeActivatedTime = currentTimeEco; } ecoModeAktiv = true; if (dynamicEcoActive) { int minutesPassed = (currentTimeEco - ecoModeActivatedTime) / (60UL * 1000UL); SetpointWasser = (ecoModeTempWasser - minutesPassed > 0 ? ecoModeTempWasser - minutesPassed : 0); SetpointDampf = (ecoModeTempDampf - minutesPassed > 0 ? ecoModeTempDampf - minutesPassed : 0); } else { SetpointWasser = ecoModeTempWasser; SetpointDampf = ecoModeTempDampf; } } else { // Wenn Eco-Zeit noch nicht erreicht ODER Shot aktiv war, normale Sollwerte laden // Nur laden, wenn Eco gerade deaktiviert wird, um ständiges EEPROM-Lesen zu vermeiden if (ecoModeAktiv) { EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser); EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf); } ecoModeAktiv = false; ecoModeActivatedTime = 0; } } else if (ecoModeAktiv) { // Sicherstellen, dass Eco deaktiviert wird, wenn ecoModeMinutes=0 gesetzt wird o.ä. ecoModeAktiv = false; ecoModeActivatedTime = 0; EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser); EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf); } } // Display regelmäßig updaten (Logik für Wartungsmeldung hinzugefügt) updateDisplay(); server.handleClient(); // Webserver (unverändert) MDNS.update(); // NEU: mDNS-Anfragen bearbeiten // Betriebszeit zählen (unverändert) static unsigned long lastRuntimeSecond = 0; if (millis() - lastRuntimeSecond >= 1000) { totalRuntime++; lastRuntimeSecond = millis(); } // Speichern der Betriebszeit (NEUE LOGIK) und WiFi-Check - Unverändert if (!demoModus) // Bedingung hinzugefügt: Nur speichern, wenn KEIN Demo-Modus aktiv ist { unsigned long currentMillisLoop = millis(); // Aktuelle Zeit für diese Prüfung holen unsigned long intervalToUse; // Bestimme, welches Intervall verwendet werden soll if (!initialRuntimeSaveDone) { // Wenn das erste Speichern noch nicht erfolgt ist, nutze die 10-Minuten-Verzögerung intervalToUse = initialRuntimeSaveDelay; } else { // Wenn das erste Speichern bereits erfolgt ist, nutze das 5-Minuten-Intervall intervalToUse = subsequentRuntimeSaveInterval; } // Prüfe, ob das ermittelte Intervall seit dem letzten Speichern abgelaufen ist if (currentMillisLoop - lastRuntimeSave >= intervalToUse) { EEPROM.put(EEPROM_ADDR_RUNTIME, totalRuntime); // Schreibe die aktuelle Betriebszeit ins EEPROM EEPROM.commit(); // Führe das Schreiben physisch durch lastRuntimeSave = currentMillisLoop; // Aktualisiere den Zeitpunkt des letzten Speicherns // Wenn dies das erste Speichern war, setze das Flag if (!initialRuntimeSaveDone) { initialRuntimeSaveDone = true; Serial.println("Betriebszeit: Erstes Speichern nach 10 Minuten erfolgt."); // Optional: Log-Ausgabe } else { Serial.println("Betriebszeit: Regelmäßiges Speichern (alle 5 Minuten) erfolgt."); // Optional: Log-Ausgabe } // Den WiFi-Check können wir auch hier durchführen, da er nun seltener aufgerufen wird if (WiFi.getMode() == WIFI_STA) { checkWifiConnection(); } } } } /************************************************************************************ * Liest die Temperatur vom NTC-Sensor (Wasser) über den Spannungsteiler (ADC) - Unverändert ************************************************************************************/ double readNTCTemperature() { int adcValue = analogRead(A0); // Prüfen auf ungültige ADC-Werte (können bei Problemen auftreten) if (adcValue <= 0 || adcValue >= 1023) // 1023 ist max. Wert, kann auf Kurzschluss hindeuten { Serial.print("WARNUNG: Ungültiger ADC-Wert vom NTC-Sensor: "); Serial.println(adcValue); return NAN; // Not-a-Number als Fehlerwert zurückgeben } double sensorVoltage = adcValue * (ADC_REF / 1023.0); // Vermeide Division durch Null, falls sensorVoltage == V_SUPPLY if (sensorVoltage >= V_SUPPLY) { Serial.println("WARNUNG: NTC-Spannung entspricht Versorgungsspannung, R_ntc wäre unendlich."); return NAN; // Fehler } // Vermeide Division durch Null, falls sensorVoltage == 0 (bereits oben abgefangen, aber zur Sicherheit) if (sensorVoltage <= 0) { Serial.println("WARNUNG: NTC-Spannung ist 0 oder negativ."); return NAN; // Fehler } double R_ntc = R_FIXED * ((V_SUPPLY / sensorVoltage) - 1.0); // Prüfen auf negativen oder sehr kleinen Widerstandswert if (R_ntc <= 0) { Serial.print("WARNUNG: Berechneter NTC-Widerstand ungültig: "); Serial.println(R_ntc); return NAN; // Fehler } // Temperaturberechnung über die Beta-Gleichung // log() erwartet einen positiven Wert double log_R_R0 = log(R_ntc / R0); double temp_inv_K = (1.0 / T0) + (1.0 / beta) * log_R_R0; // Prüfen auf Division durch Null bei der Invertierung if (temp_inv_K == 0) { Serial.println("WARNUNG: Kehrwert der Kelvin-Temperatur ist 0."); return NAN; // Fehler } double temperatureK = 1.0 / temp_inv_K; double temperatureC = temperatureK - 273.15; // Optional: Plausibilitätsprüfung des Ergebnisses // if (temperatureC < -50 || temperatureC > 200) { // Beispielgrenzen // Serial.print("WARNUNG: Berechnete NTC-Temperatur unplausibel: "); // Serial.println(temperatureC); // return NAN; // } return temperatureC; } /************************************************************************************ * Aktualisiert das Display (Temperaturanzeige, Shot-Timer, Wartung etc.) - Angepasst ************************************************************************************/ /************************************************************************************ * Aktualisiert das Display (Temperaturanzeige, Shot-Timer, Wartung etc.) - Angepasst ************************************************************************************/ /************************************************************************************ * Aktualisiert das Display (Temperaturanzeige, Shot-Timer, Wartung etc.) - Angepasst * Priorität: PID Tuning > Fast Heat Up > Aktiver Shot > Letzte Shot-Zeit > Wartung > Temperaturen ************************************************************************************/ void updateDisplay() { // 1. Höchste Priorität: PID Tuning aktiv? -> Eigene Anzeige, dann Ende. if (autoTuneWasserActive || autoTuneDampfActive) { display.clearDisplay(); display.setCursor(0, 0); display.setTextSize(1); display.setTextColor(SH110X_WHITE); display.println(F("PID-Tuning aktiv:")); display.println(autoTuneWasserActive ? F("Wasser-PID") : F("Dampf-PID")); display.println(""); display.println(F("Bitte Maschine nicht")); display.println(F("verwenden!")); display.display(); return; // Funktion hier beenden } // --- Keine Tuning-Anzeige --- display.clearDisplay(); // WiFi Symbol (immer anzeigen, wenn verbunden) if (WiFi.status() == WL_CONNECTED) { drawwifiSymbol(113, 0); } display.setCursor(0, 0); display.setTextColor(SH110X_WHITE); display.setTextSize(1); // 2. Priorität: Fast-Heat-Up aktiv und relevant? if (fastHeatUpHeating && !shotActive && !wasserSensorError && ((int)InputWasser < fastHeatUpSetpoint)) { SetpointWasser = fastHeatUpSetpoint; display.println(F("Fast-Heat-Up aktiv:")); display.println(""); display.println(F("20 Sek. Flushen,")); display.println(F("wenn Temp. erreicht!")); display.println(F(" ")); display.println(F("Wasser: ")); display.print((int)InputWasser); display.print(F(" / ")); display.print((int)SetpointWasser); display.print(F(" ")); display.print((char)247); display.print(F("C")); } // 3. Kein Fast-Heat-Up: Was soll sonst angezeigt werden? else { // 3a. Priorität: Shot läuft *aktuell*? if (shotActive) { unsigned long elapsed = millis() - shotStartTime; display.println(F("Shot-Timer:")); display.println(""); display.setTextSize(3); display.println(elapsed / 1000.0, 1); display.setTextSize(1); display.println(F("Sekunden")); fastHeatUpHeating = false; // Laufender Shot beendet F-H-U Anzeige } // 3b. Priorität: Letzte Shot-Zeit anzeigen (für 2 Sekunden)? else if (shotEndTime > 0 && millis() < shotEndTime + 2000) { display.println(F("Bezugszeit:")); display.println(""); display.setTextSize(3); display.println(lastShotDurationMillis / 1000.0, 1); // Gespeicherte Dauer display.setTextSize(1); display.println(F("Sekunden")); } // 3c. Priorität: Wartungserinnerung anzeigen (für 6 Sekunden nach Trigger) // Diese Bedingung wird nur erreicht, wenn kein Shot aktiv ist UND die letzte Shot-Zeit nicht angezeigt wird. else if (displayMaintenanceMessage && millis() < maintenanceMessageStartTime + 6000) { display.println(F("Erinnerung:")); display.println(F("Reinigung /")); display.println(F("Wartung!")); } // 3d. Standardanzeige: Temperaturen (wenn nichts anderes zutrifft) else { // Optional: Flag für Wartungsanzeige nach 3s zurücksetzen (obwohl es besser ist, es manuell zu tun) // if (displayMaintenanceMessage && millis() >= maintenanceMessageStartTime + 3000) { // // Hier nichts tun, Flag bleibt true bis zum Reset, nur die Anzeige stoppt. // } // Optional: shotEndTime zurücksetzen, falls gewünscht // if (shotEndTime > 0) { shotEndTime = 0; } display.println(F("Temperaturen:")); display.println(""); // Wasser Temperatur / Fehleranzeige display.println(F("Wasser: ")); if (wasserSensorError) { display.print(F("Sensorfehler!")); } else { display.print((int)InputWasser); display.print(F(" / ")); display.print((int)SetpointWasser); display.print(F(" ")); display.print((char)247); display.print(F("C")); if (ecoModeAktiv && dynamicEcoActive) display.print(F(" (ECO+)")); else if (ecoModeAktiv && !dynamicEcoActive) display.print(F(" (ECO)")); } display.println(""); display.println(""); // Dampf Temperatur / Fehleranzeige display.println(F("Dampf: ")); if (dampfSensorError) { display.print(F("Sensorfehler!")); } else { if (!(dampfVerzoegerung > 0 && (millis() - startupTime < (unsigned long)dampfVerzoegerung * 60000UL))) { display.print((int)InputDampf); display.print(F(" / ")); display.print((int)SetpointDampf); display.print(F(" ")); display.print((char)247); display.print(F("C")); if (ecoModeAktiv && dynamicEcoActive) display.print(F(" (ECO+)")); else if (ecoModeAktiv && !dynamicEcoActive) display.print(F(" (ECO)")); } else { display.print(F("Startverz\x94gerung")); } } } // Ende Standardanzeige (Temperaturen) } // Ende "Kein Fast-Heat-Up" display.display(); // Display *immer* am Ende aktualisieren, egal was angezeigt wurde } /************************************************************************************ * Verwaltet den Shot-Timer (wenn z.B. der Nutzer den Bezug aktiviert) - Wartungscheck hinzugefügt ************************************************************************************/ void updateShotTimer() { if (digitalRead(SHOT_TIMER_PIN) == HIGH) // Annahme: HIGH = Bezug aktiv { // Shot beginnt if (!shotActive) { shotActive = true; shotStartTime = millis(); lastShotDurationMillis = 0; // Dauer für neuen Shot zurücksetzen shotEndTime = 0; // Endzeit für neuen Shot zurücksetzen displayMaintenanceMessage = false; // Sicherstellen, dass alte Meldung weg ist } } else // Annahme: LOW = Bezug inaktiv { // Shot endet if (shotActive) { unsigned long shotDuration = millis() - shotStartTime; lastShotDurationMillis = shotDuration; // *** NEU: Letzte Dauer speichern *** shotEndTime = millis(); // *** NEU: Endzeitpunkt speichern *** // nur Shots > 20 Sekunden zählen if (shotDuration > 20000) { shotCounter++; maintenanceIntervalCounter++; if (!demoModus) { // Nur speichern, wenn kein Demo-Modus EEPROM.put(EEPROM_ADDR_SHOTCOUNTER, shotCounter); EEPROM.put(EEPROM_ADDR_MAINTENANCE_INTERVAL_COUNTER, maintenanceIntervalCounter); EEPROM.commit(); } Serial.print(F("Shots: ")); Serial.println(shotCounter); // Prüfen, ob Wartungsintervall erreicht ist if (maintenanceInterval > 0 && maintenanceIntervalCounter >= maintenanceInterval) { displayMaintenanceMessage = true; // Flag für Anzeige setzen maintenanceMessageStartTime = millis(); // Startzeit für Anzeige merken Serial.println("Wartungsintervall erreicht!"); } } shotActive = false; // Shot als beendet markieren lastShotTime = millis(); // Wichtig für Eco-Modus (bleibt wie es ist) // shotEndTime und delayDisplayUpdate werden nicht mehr benötigt (bzw. shotEndTime hat neue Bedeutung) } } } /************************************************************************************ * Startet AutoTune für Wasser-PID (Angepasst für Zeitproportional) ************************************************************************************/ void startAutoTuneWasser() { // Eco-Modus deaktivieren, falls aktiv if(ecoModeAktiv) { ecoModeAktiv = false; // Ursprünglichen Setpoint wiederherstellen (aus EEPROM oder Profil) EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser); // Optional: Auch Dampf-Setpoint wiederherstellen, falls Eco beide beeinflusst EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf); } // Stelle sicher, dass der PID im Automatikmodus ist bevor Tuning beginnt pidWasser.SetMode(AUTOMATIC); // Sicherstellen, dass die Output-Grenzen korrekt gesetzt sind pidWasser.SetOutputLimits(0, windowSizeWasser); autoTuneWasser = new PID_ATune(&InputWasser, &OutputWasser); // Verwende die Wasser-spezifischen Parameter autoTuneWasser->SetOutputStep(tuningStepWasser); // Angepasst autoTuneWasser->SetControlType(1); // DIRECT control (heating) autoTuneWasser->SetNoiseBand(tuningNoiseWasser); // Angepasst autoTuneWasser->SetLookbackSec((int)tuningLookBackWasser); // Angepasst (Cast zu int für Funktion) // Setze den Startwert für den Output // WICHTIG: AutoTune manipuliert OutputWasser direkt. Die zeitproportionale // Logik in loop() wird diesen Wert verwenden, um den SSR zu steuern. OutputWasser = tuningStartValueWasser; // Angepasst // KEIN direktes digitalWrite mehr hier! Die loop() übernimmt das. autoTuneWasserActive = true; Serial.println("Starte AutoTune für Wasser (Zeitproportional)..."); } /************************************************************************************ * Startet AutoTune für Dampf-PID (Angepasst für Zeitproportional) ************************************************************************************/ void startAutoTuneDampf() { // Eco-Modus deaktivieren, falls aktiv if(ecoModeAktiv) { ecoModeAktiv = false; // Ursprünglichen Setpoint wiederherstellen (aus EEPROM oder Profil) EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf); // Optional: Auch Wasser-Setpoint wiederherstellen, falls Eco beide beeinflusst EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser); } // Stelle sicher, dass der PID im Automatikmodus ist bevor Tuning beginnt pidDampf.SetMode(AUTOMATIC); // Sicherstellen, dass die Output-Grenzen korrekt gesetzt sind pidDampf.SetOutputLimits(0, windowSizeDampf); autoTuneDampf = new PID_ATune(&InputDampf, &OutputDampf); // Verwende die Dampf-spezifischen Parameter autoTuneDampf->SetOutputStep(tuningStepDampf); // Angepasst autoTuneDampf->SetControlType(1); // DIRECT control (heating) autoTuneDampf->SetNoiseBand(tuningNoiseDampf); // Angepasst autoTuneDampf->SetLookbackSec((int)tuningLookBackDampf); // Angepasst (Cast zu int für Funktion) // Setze den Startwert für den Output // WICHTIG: AutoTune manipuliert OutputDampf direkt. Die zeitproportionale // Logik in loop() wird diesen Wert verwenden, um den SSR zu steuern. OutputDampf = tuningStartValueDampf; // Angepasst // KEIN direktes digitalWrite mehr hier! Die loop() übernimmt das. autoTuneDampfActive = true; Serial.println("Starte AutoTune für Dampf (Zeitproportional)..."); } /************************************************************************************ * Stoppt AutoTune für Wasser - Unverändert ************************************************************************************/ void stopAutoTuneWasser() { if (autoTuneWasserActive) { autoTuneWasserActive = false; delete autoTuneWasser; autoTuneWasser = nullptr; // Setze PID zurück in den Normalbetrieb (ggf. Output auf 0) pidWasser.SetMode(AUTOMATIC); // Sicherstellen, dass PID wieder normal läuft OutputWasser = 0; // Optional: Output zurücksetzen // Stelle sicher, dass SSR ausgeschaltet ist digitalWrite(SSR_WASSER_PIN, LOW); Serial.println("AutoTune Wasser gestoppt."); } } /************************************************************************************ * Stoppt AutoTune für Dampf - Unverändert ************************************************************************************/ void stopAutoTuneDampf() { if (autoTuneDampfActive) { autoTuneDampfActive = false; delete autoTuneDampf; autoTuneDampf = nullptr; // Setze PID zurück in den Normalbetrieb (ggf. Output auf 0) pidDampf.SetMode(AUTOMATIC); // Sicherstellen, dass PID wieder normal läuft OutputDampf = 0; // Optional: Output zurücksetzen // Stelle sicher, dass SSR ausgeschaltet ist digitalWrite(SSR_DAMPF_PIN, LOW); Serial.println("AutoTune Dampf gestoppt."); } } /************************************************************************************ * Überwacht die AutoTune-Prozesse (Dampf/Wasser) - Display-Logik ausgelagert ************************************************************************************/ void handleAutoTune() { // AutoTune Wasser if (autoTuneWasserActive) { // Prüfen auf Sensorfehler während des Tunings if (wasserSensorError) { Serial.println("FEHLER: Wassertemperatursensor während AutoTune ausgefallen! Breche Tuning ab."); stopAutoTuneWasser(); // Ruft Funktion auf, die Flag zurücksetzt und Speicher freigibt // Optional: Fehlermeldung auf Display ausgeben (wird jetzt in updateDisplay gemacht) // display.clearDisplay(); ... // delay(3000); // Nachricht kurz anzeigen return; // Funktion für diesen Durchlauf verlassen } // Wenn Runtime() 1 zurückgibt, ist der AutoTune-Prozess abgeschlossen // AutoTune->Runtime() berechnet und SETZT OutputWasser! if (autoTuneWasser->Runtime() == 1) { KpWasser = autoTuneWasser->GetKp(); KiWasser = autoTuneWasser->GetKi(); KdWasser = autoTuneWasser->GetKd(); // WICHTIG: PID-Tunings sofort anwenden pidWasser.SetTunings(KpWasser, KiWasser, KdWasser); // Speichern im EEPROM EEPROM.put(EEPROM_ADDR_KP_WASSER, KpWasser); EEPROM.put(EEPROM_ADDR_KI_WASSER, KiWasser); EEPROM.put(EEPROM_ADDR_KD_WASSER, KdWasser); EEPROM.commit(); Serial.println("AutoTune Wasser erfolgreich abgeschlossen."); Serial.printf("Neue Werte: Kp=%.2f, Ki=%.2f, Kd=%.2f\n", KpWasser, KiWasser, KdWasser); stopAutoTuneWasser(); // Korrekt beenden } // Display wird in updateDisplay() gehandhabt } // AutoTune Dampf if (autoTuneDampfActive) { // Prüfen auf Sensorfehler während des Tunings if (dampfSensorError) { Serial.println("FEHLER: Dampftemperatursensor während AutoTune ausgefallen! Breche Tuning ab."); stopAutoTuneDampf(); // Ruft Funktion auf, die Flag zurücksetzt und Speicher freigibt // Optional: Fehlermeldung auf Display ausgeben (wird jetzt in updateDisplay gemacht) return; // Funktion für diesen Durchlauf verlassen } // Wenn Runtime() 1 zurückgibt, ist der AutoTune-Prozess abgeschlossen // AutoTune->Runtime() berechnet und SETZT OutputDampf! if (autoTuneDampf->Runtime() == 1) { KpDampf = autoTuneDampf->GetKp(); KiDampf = autoTuneDampf->GetKi(); KdDampf = autoTuneDampf->GetKd(); // WICHTIG: PID-Tunings sofort anwenden pidDampf.SetTunings(KpDampf, KiDampf, KdDampf); // Speichern im EEPROM EEPROM.put(EEPROM_ADDR_KP_DAMPF, KpDampf); EEPROM.put(EEPROM_ADDR_KI_DAMPF, KiDampf); EEPROM.put(EEPROM_ADDR_KD_DAMPF, KdDampf); EEPROM.commit(); Serial.println("AutoTune Dampf erfolgreich abgeschlossen."); Serial.printf("Neue Werte: Kp=%.2f, Ki=%.2f, Kd=%.2f\n", KpDampf, KiDampf, KdDampf); stopAutoTuneDampf(); // Korrekt beenden } // Display wird in updateDisplay() gehandhabt } } /************************************************************************************ * Handler für die Info-Seite - URL angepasst, Wartungsintervall hinzugefügt ************************************************************************************/ void handleInfo() // URL: /Info { static const char infoHtml[] PROGMEM = R"rawliteral( Info )rawliteral"; static const char infoHtml2[] PROGMEM = R"rawliteral( )rawliteral"; static const char infoHtml3[] PROGMEM = R"rawliteral(

Info

Geräteinfo

Die Geräteinformationen werden beim Start der Maschine, bzw. PID-Controllers im Display angezeigt.

Reinigung & Wartung

Zeigt nach der eingestellten Anzahl von Bezügen eine Reinigungs-/ Wartungserinnerung im Display an.

Wartungszähler zurücksetzen

Aktueller Zählerstand: {MAINTENANCECOUNTER}

Setzt den Zähler für die Reinigungs- & Wartungserinnerung zurück.
Dies ist nach der erfolgreichen Durchführung notwendig,
damit die Meldung nicht nach jedem Bezug erneut scheint.

Betriebszeit:

{RUNTIME}

Shots: (Bezüge über 20 Sekunden)

{SHOT_COUNT}

Systeminfo

Freier Heap: {FREE_HEAP} Bytes
SDK-Version: {SDK_VERSION}
Core-Version: {CORE_VERSION}
Boot-Version: {BOOT_VERSION}
Chip-ID: {CHIP_ID}
CPU-Takt: {CPU_MHZ} MHz
Letzter Reset-Grund: {RESET_REASON}

Sketch-Größe: {SKETCH_SIZE} Bytes
Freier Sketch-Speicher: {FREE_SKETCH} Bytes
Flash-Größe (Chip): {FLASH_SIZE} Bytes

MAC-Adresse: {MAC_ADDRESS}
WLAN-Signalstärke (RSSI): {WIFI_RSSI} dBm
Hostname: {HOSTNAME}.local
)rawliteral"; unsigned long totalSeconds = totalRuntime; unsigned long days = totalSeconds / 86400; unsigned long hours = (totalSeconds % 86400) / 3600; unsigned long minutes = (totalSeconds % 3600) / 60; String runtimeStr = String(days) + F(" Tag(e), ") + String(hours) + F(" Stunde(n) und ") + String(minutes) + F(" Minuten"); String html = FPSTR(infoHtml); html += FPSTR(commonStyle); html += FPSTR(infoHtml2); html += FPSTR(commonNav); // URLs angepasst String body = FPSTR(infoHtml3); // Temporäre Variable für Ersetzungen body.replace(F("{HERSTELLER}"), String(infoHersteller)); body.replace(F("{MODELL}"), String(infoModell)); body.replace(F("{ZUSATZ}"), String(infoZusatz)); body.replace(F("{RUNTIME}"), runtimeStr); body.replace(F("{SHOT_COUNT}"), String(shotCounter)); body.replace(F("{MAINTENANCECOUNTER}"), String(maintenanceIntervalCounter)); body.replace(F("{MAINTENANCE_INTERVAL}"), String(maintenanceInterval)); // NEU // Systeminfo-Ersetzungen body.replace(F("{FREE_HEAP}"), String(ESP.getFreeHeap())); body.replace(F("{SDK_VERSION}"), String(ESP.getSdkVersion())); body.replace(F("{CORE_VERSION}"), ESP.getCoreVersion()); // NEU - Ist schon String body.replace(F("{BOOT_VERSION}"), String(ESP.getBootVersion())); body.replace(F("{CHIP_ID}"), String(ESP.getChipId(), HEX)); // NEU - ChipID oft als HEX angezeigt body.replace(F("{CPU_MHZ}"), String(ESP.getCpuFreqMHz())); body.replace(F("{RESET_REASON}"), ESP.getResetReason()); // NEU - Ist schon String body.replace(F("{SKETCH_SIZE}"), String(ESP.getSketchSize())); body.replace(F("{FREE_SKETCH}"), String(ESP.getFreeSketchSpace())); body.replace(F("{FLASH_SIZE}"), String(ESP.getFlashChipSize())); // NEU body.replace(F("{MAC_ADDRESS}"), WiFi.macAddress()); // NEU - Ist schon String // RSSI nur anzeigen, wenn verbunden, sonst "N/A" String rssi_str = F("N/A"); if (WiFi.status() == WL_CONNECTED) { rssi_str = String(WiFi.RSSI()); } body.replace(F("{WIFI_RSSI}"), rssi_str); // NEU body.replace(F("{HOSTNAME}"), String(hostname)); // NEU html += body; // Füge den modifizierten Body zum Gesamt-HTML hinzu server.send(200, F("text/html"), html); } /************************************************************************************ * Handler für die Eco-Modus-Konfiguration - URL angepasst ************************************************************************************/ void handleEco() // URL: /ECO { static const char ecoHtml[] PROGMEM = R"rawliteral( Eco-Modus )rawliteral"; static const char ecoHtml2[] PROGMEM = R"rawliteral( )rawliteral"; static const char ecoHtml3[] PROGMEM = R"rawliteral(

Eco-Modus

Eco-Modus

Der Eco-Modus senkt die voreingestellten Temperaturen nach der angegebenen Zeit auf die Eco-Temperaturen ab.

Dynamischer Eco-Modus

Der dynamische Eco-Modus verhindert, dass die Maschine bis in die Unendlichkeit eine gewisse Temperatur aufrecht erhält.
Er senkt die Temperatur pro Minute um ein weiteres Grad ab, bis letzlich das Heizen vollständig beendet wird.
Der dynamische Eco-Modus ist nur in Kombination mit dem Eco-Modus nutzbar!

Beispiel für eine praktische Anwendung:
Peter hat an seiner Maschine den Eco-Modus auf 45 Minuten gestellt, mit Eco-Temperatur von 60 Grad. Nun kommt ihm etwas dazwischen und er kommt erst bei Minute 60 an die Maschine, um sich einen Espresso zuzubereiten. Die Maschine ist nun allerdings schon auf 60 Grad herabgekühlt und er muss erneut das Aufheizen abwarten ...
Markus passiert das gleiche, doch er hat den dynamischen Eco-Modus mit einer initialen Eco-Temperatur von 95 Grad aktiviert - Seine Maschine ist nun zumindest noch bei 80 Grad.
Markus muss zwar auch warten, kann jedoch noch vor Peter einen Espresso trinken.

Verzögerung für Dampf

ACHTUNG:
Um Konflikte zu vermeiden, sollte der Wert geringer sein, als der des Eco-Modus, falls dieser aktiviert ist!
)rawliteral"; String html = FPSTR(ecoHtml); html += FPSTR(commonStyle); html += FPSTR(ecoHtml2); html += FPSTR(commonNav); // URLs angepasst html += FPSTR(ecoHtml3); html.replace(F("{ECO_MODE}"), String(ecoModeMinutes)); html.replace(F("{ECO_MODE_TEMP_WASSER}"), String(ecoModeTempWasser)); html.replace(F("{ECO_MODE_TEMP_DAMPF}"), String(ecoModeTempDampf)); html.replace(F("{DYNAMIC_ECO_MODE_CHECKBOX}"), dynamicEcoActive ? F("checked") : F("")); html.replace(F("{DAMPF_DELAY}"), String(dampfVerzoegerung)); server.send(200, F("text/html"), html); } /************************************************************************************ * Handler für die Fast-Heat-Up Seite - URL angepasst ************************************************************************************/ void handleFastHeatUp() // URL: /Fast-Heat-Up { static const char fhuHtml[] PROGMEM = R"rawliteral( Fast-Heat-Up-Modus )rawliteral"; static const char fhuHtml2[] PROGMEM = R"rawliteral( )rawliteral"; static const char fhuHtml3[] PROGMEM = R"rawliteral(

Fast-Heat-Up-Modus

Fast-Heat-Up-Modus

Der Fast-Heat-Up-Modus ermöglicht es, die Maschine noch schneller aufzuheizen.
Der Kessel wird beim Start auf 130 Grad Celsius erhitzt.
Nachdem die Temperatur erreicht ist, muss ein Flush von ca. 20 Sekunden durchgeführt werden.

ACHTUNG:
Bitte beachten, dass auf der Seite PID-Einstellung eventuell die max. Wassertemperatur der Sicherheitsabschaltung angepasst werden muss!
)rawliteral"; String html = FPSTR(fhuHtml); html += FPSTR(commonStyle); html += FPSTR(fhuHtml2); html += FPSTR(commonNav); // URLs angepasst html += FPSTR(fhuHtml3); html.replace(F("{FASTHEATUP_MODE_CHECKBOX}"), fastHeatUpAktiv ? F("checked") : F("")); server.send(200, F("text/html"), html); } /************************************************************************************ * Handler zum Speichern der Fast-Heat-Up-Einstellung - URL angepasst ************************************************************************************/ void handleFastHeatUpSettings() { if (server.hasArg(F("fastHeatUpAktiv"))) { fastHeatUpAktiv = true; } else { fastHeatUpAktiv = false; } EEPROM.put(EEPROM_ADDR_FASTHEATUP_DATA, fastHeatUpAktiv); EEPROM.commit(); server.sendHeader(F("Location"), F("/Fast-Heat-Up")); // URL angepasst server.send(303); } /************************************************************************************ * Handler für Root (PID-Einstellungen) - Unverändert ************************************************************************************/ void handleRoot() { static const char rootHtml[] PROGMEM = R"rawliteral( PID-Einstellung )rawliteral"; static const char rootHtml2[] PROGMEM = R"rawliteral( )rawliteral"; static const char rootHtml3[] PROGMEM = R"rawliteral(

PID-Einstellung

{SENSOR_ERROR_MESSAGES}

Temperatur & Offset: Wasser / Kessel


Temperatur & Offset: Dampf / Thermoblock


PID-Parameter: Wasser / Kessel


PID-Parameter: Dampf / Thermoblock


Sicherheitsabschaltung

Die Sicherheitsabschaltung bietet eine softwareseitige Übertemperatursicherung,
ersetzt jedoch keine hardwareseitige Lösung durch z.B. Bimetall Temperaturschalter!
Bei der Verwendung von Fast-Heat-Up muss der Wert für die Maximaltemperatur angepasst werden!

)rawliteral"; String html = FPSTR(rootHtml); html += FPSTR(commonStyle); html += FPSTR(rootHtml2); html += FPSTR(commonNav); // URLs angepasst html += FPSTR(rootHtml3); html.replace(F("{MAX_TEMP_WASSER}"), String(maxTempWasser, 1)); // Mit einer Nachkommastelle html.replace(F("{MAX_TEMP_DAMPF}"), String(maxTempDampf, 1)); // Mit einer Nachkommastelle html.replace(F("{WASSER}"), String((int)SetpointWasser)); html.replace(F("{OFFSET_WASSER}"), String(OffsetWasser, 1)); html.replace(F("{DAMPF}"), String((int)SetpointDampf)); html.replace(F("{OFFSET_DAMPF}"), String(OffsetDampf, 1)); html.replace(F("{KP_WASSER}"), String(KpWasser, 1)); html.replace(F("{KI_WASSER}"), String(KiWasser, 1)); html.replace(F("{KD_WASSER}"), String(KdWasser, 1)); html.replace(F("{KP_DAMPF}"), String(KpDampf, 1)); html.replace(F("{KI_DAMPF}"), String(KiDampf, 1)); html.replace(F("{KD_DAMPF}"), String(KdDampf, 1)); // --- NEU: Dynamische Fehlermeldungen generieren --- String errorMessageHtml = ""; // Standardmäßig leer // Extern deklarierte Flags `wasserSensorError` und `dampfSensorError` verwenden if (wasserSensorError && dampfSensorError) { if (!demoModus) { errorMessageHtml = "
Die Daten der Temperatursensoren sind derzeit nicht verfügbar!
Bitte Temperatur-Sensoren prüfen!
"; } else { errorMessageHtml = "
Demo-Betrieb: Es sind keine Temperatur-Daten verfügbar, bzw. keine Sensoren angeschlossen
"; } } else if (wasserSensorError) { errorMessageHtml = "
Temperaturwert für Wasser nicht verfügbar
"; } else if (dampfSensorError) { errorMessageHtml = "
Temperaturwert für Dampf nicht verfügbar
"; } // Platzhalter im HTML durch die generierte Fehlermeldung ersetzen (oder durch Leerstring, falls kein Fehler) html.replace(F("{SENSOR_ERROR_MESSAGES}"), errorMessageHtml); server.send(200, F("text/html"), html); } /************************************************************************************ * Schreibt die Werte in den EEPROM (PID/Eco) - Unverändert ************************************************************************************/ void handleUpdate() { if (server.hasArg(F("maxTempWasser"))) maxTempWasser = server.arg(F("maxTempWasser")).toFloat(); if (server.hasArg(F("maxTempDampf"))) maxTempDampf = server.arg(F("maxTempDampf")).toFloat(); if (server.hasArg(F("wasser"))) SetpointWasser = server.arg(F("wasser")).toFloat(); if (server.hasArg(F("dampf"))) SetpointDampf = server.arg(F("dampf")).toFloat(); if (server.hasArg(F("offsetWasser"))) OffsetWasser = server.arg(F("offsetWasser")).toFloat(); if (server.hasArg(F("offsetDampf"))) OffsetDampf = server.arg(F("offsetDampf")).toFloat(); if (server.hasArg(F("kpWasser"))) KpWasser = server.arg(F("kpWasser")).toFloat(); if (server.hasArg(F("kiWasser"))) KiWasser = server.arg(F("kiWasser")).toFloat(); if (server.hasArg(F("kdWasser"))) KdWasser = server.arg(F("kdWasser")).toFloat(); if (server.hasArg(F("kpDampf"))) KpDampf = server.arg(F("kpDampf")).toFloat(); if (server.hasArg(F("kiDampf"))) KiDampf = server.arg(F("kiDampf")).toFloat(); if (server.hasArg(F("kdDampf"))) KdDampf = server.arg(F("kdDampf")).toFloat(); EEPROM.put(EEPROM_ADDR_MAX_TEMP_WASSER, maxTempWasser); EEPROM.put(EEPROM_ADDR_MAX_TEMP_DAMPF, maxTempDampf); EEPROM.put(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf); EEPROM.put(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser); EEPROM.put(EEPROM_ADDR_OFFSET_DAMPF, OffsetDampf); EEPROM.put(EEPROM_ADDR_OFFSET_WASSER, OffsetWasser); EEPROM.put(EEPROM_ADDR_KP_DAMPF, KpDampf); EEPROM.put(EEPROM_ADDR_KI_DAMPF, KiDampf); EEPROM.put(EEPROM_ADDR_KD_DAMPF, KdDampf); EEPROM.put(EEPROM_ADDR_KP_WASSER, KpWasser); EEPROM.put(EEPROM_ADDR_KI_WASSER, KiWasser); EEPROM.put(EEPROM_ADDR_KD_WASSER, KdWasser); EEPROM.commit(); pidDampf.SetTunings(KpDampf, KiDampf, KdDampf); pidWasser.SetTunings(KpWasser, KiWasser, KdWasser); server.sendHeader(F("Location"), F("/")); // URL bleibt Root server.send(303); } /************************************************************************************ * Speichert Eco-Einstellungen - URL angepasst ************************************************************************************/ void handleEcoUpdate() { if (server.hasArg(F("ecoMode"))) ecoModeMinutes = server.arg(F("ecoMode")).toInt(); if (server.hasArg(F("ecoModeTempWasser"))) ecoModeTempWasser = server.arg(F("ecoModeTempWasser")).toInt(); if (server.hasArg(F("ecoModeTempDampf"))) ecoModeTempDampf = server.arg(F("ecoModeTempDampf")).toInt(); if (server.hasArg(F("dynamicEcoMode"))) { dynamicEcoActive = true; } else { dynamicEcoActive = false; } if (server.hasArg(F("dampfDelay"))) { dampfVerzoegerung = server.arg(F("dampfDelay")).toInt(); } EEPROM.put(EEPROM_ADDR_ECOMODE_MINUTES, ecoModeMinutes); EEPROM.put(EEPROM_ADDR_ECOMODE_TEMP_WASSER, ecoModeTempWasser); EEPROM.put(EEPROM_ADDR_ECOMODE_TEMP_DAMPF, ecoModeTempDampf); EEPROM.put(EEPROM_ADDR_DYNAMIC_ECO_MODE, dynamicEcoActive); EEPROM.put(EEPROM_ADDR_STEAM_DELAY, dampfVerzoegerung); EEPROM.commit(); // Wenn Eco deaktiviert, Setpoints zurückladen if (ecoModeMinutes == 0) { ecoModeAktiv = false; ecoModeActivatedTime = 0; EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser); EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf); } server.sendHeader(F("Location"), F("/ECO")); // URL angepasst server.send(303); } /************************************************************************************ * Speichert Geräte-Infos ************************************************************************************/ void handleInfoUpdate() { bool changed = false; if (server.hasArg(F("hersteller"))) { strncpy(infoHersteller, server.arg(F("hersteller")).c_str(), sizeof(infoHersteller)-1); infoHersteller[sizeof(infoHersteller)-1] = '\0'; EEPROM.put(EEPROM_ADDR_INFO_HERSTELLER, infoHersteller); changed = true; } if (server.hasArg(F("modell"))) { strncpy(infoModell, server.arg(F("modell")).c_str(), sizeof(infoModell)-1); infoModell[sizeof(infoModell)-1] = '\0'; EEPROM.put(EEPROM_ADDR_INFO_MODELL, infoModell); changed = true; } if (server.hasArg(F("zusatz"))) { strncpy(infoZusatz, server.arg(F("zusatz")).c_str(), sizeof(infoZusatz)-1); infoZusatz[sizeof(infoZusatz)-1] = '\0'; EEPROM.put(EEPROM_ADDR_INFO_ZUSATZ, infoZusatz); changed = true; } if (changed) { EEPROM.commit(); } server.sendHeader(F("Location"), F("/Info")); // URL angepasst server.send(303); } /************************************************************************************ * Speichert das Wartungsintervall ************************************************************************************/ void handleMaintenanceUpdate() { bool changed = false; // Wartungsintervall speichern if (server.hasArg(F("maintenanceInterval"))) { int newInterval = server.arg(F("maintenanceInterval")).toInt(); if (newInterval < 0) newInterval = 0; // Sicherstellen, dass nicht negativ if (newInterval != maintenanceInterval) { maintenanceInterval = newInterval; EEPROM.put(EEPROM_ADDR_MAINTENANCE_INTERVAL, maintenanceInterval); changed = true; Serial.printf("Wartungsintervall gesetzt auf: %d Shots\n", maintenanceInterval); } } if (changed) { EEPROM.commit(); } server.sendHeader(F("Location"), F("/Info")); // URL angepasst server.send(303); } /************************************************************************************ * Handler für Temperatur-Charts (Live) - URL angepasst ************************************************************************************/ void handleCharts() { // HTML Teil 1: Head, Chart.js CDN, CSS-Styles für die Chart-Seite static const char chartsHtml[] PROGMEM = R"rawliteral( Live-Temperaturverlauf )rawliteral"; // HTML Teil 2: Schließendes Head-Tag static const char chartsHtml2[] PROGMEM = R"rawliteral( )rawliteral"; // HTML Teil 3: Body, Navigation, Chart-Container und JavaScript static const char chartsHtml3[] PROGMEM = R"rawliteral(

Live-Temperaturverlauf

{SENSOR_ERROR_MESSAGES}
)rawliteral"; // --- HTML zusammensetzen --- String html = FPSTR(chartsHtml); // Teil 1 (Head + CSS Start) html += FPSTR(commonStyle); // Deine allgemeinen Styles hier einfügen html += FPSTR(chartsHtml2); // Teil 2 (Head Ende) html += FPSTR(commonNav); // Deine Navigation hier einfügen html += FPSTR(chartsHtml3); // Teil 3 (Body + JS inkl. Platzhalter) // --- NEU: Dynamische Fehlermeldungen generieren --- String errorMessageHtml = ""; // Standardmäßig leer // Extern deklarierte Flags `wasserSensorError` und `dampfSensorError` verwenden if (wasserSensorError && dampfSensorError) { if (!demoModus) { errorMessageHtml = "
Die Daten der Temperatursensoren sind derzeit nicht verfügbar!
Bitte Temperatur-Sensoren prüfen!
"; } else { errorMessageHtml = "
Demo-Betrieb: Es sind keine Temperatur-Daten verfügbar, bzw. keine Sensoren angeschlossen
"; } } else if (wasserSensorError) { errorMessageHtml = "
Temperaturwert für Wasser nicht verfügbar
"; } else if (dampfSensorError) { errorMessageHtml = "
Temperaturwert für Dampf nicht verfügbar
"; } // Platzhalter im HTML durch die generierte Fehlermeldung ersetzen (oder durch Leerstring, falls kein Fehler) html.replace(F("{SENSOR_ERROR_MESSAGES}"), errorMessageHtml); // --- Senden der Seite --- server.send(200, F("text/html"), html); } /************************************************************************************ * Profil-Funktionen (Speichern/Laden von Profil 1 und 2) - URL angepasst ************************************************************************************/ void handleProfiles() // URL: /Profile { TemperatureProfile p1; TemperatureProfile p2; EEPROM.get(EEPROM_ADDR_PROFILE_1, p1); EEPROM.get(EEPROM_ADDR_PROFILE_2, p2); // Sicherstellen, dass Namen nullterminiert sind p1.profileName[sizeof(p1.profileName)-1] = '\0'; p2.profileName[sizeof(p2.profileName)-1] = '\0'; static const char profilesHtml[] PROGMEM = R"rawliteral( Profile )rawliteral"; static const char profilesHtml2[] PROGMEM = R"rawliteral( )rawliteral"; static const char profilesHtml3[] PROGMEM = R"rawliteral(

Temperaturprofile

Es können zwei unabhängige Profile angelegt oder geladen werden.

In den Profilen werden gespeichert:
- Setpoint Wasser
- Setpoint Dampf
- Eco-Modus-Einstellungen
- Aufheizen für Dampf verzögern
- Profilname

Profil 1 (Name: {PROFILE1NAME})

Profil 1 laden

Temperatureinstellungen aus Profil 1 laden.

Profil 2 (Name: {PROFILE2NAME})

Profil 2 laden

Temperatureinstellungen aus Profil 2 laden
)rawliteral"; String page = FPSTR(profilesHtml); page += FPSTR(commonStyle); page += FPSTR(profilesHtml2); page += FPSTR(commonNav); // URLs angepasst String htmlBody = FPSTR(profilesHtml3); htmlBody.replace("{PROFILE1NAME}", String(p1.profileName)); htmlBody.replace("{PROFILE2NAME}", String(p2.profileName)); page += htmlBody; server.send(200, F("text/html"), page); } // Speichert aktuelles Setup in Profil 1 - URL angepasst void handleSaveProfile1() { TemperatureProfile p; EEPROM.get(EEPROM_ADDR_PROFILE_1, p); if (server.hasArg("profileName")) { strncpy(p.profileName, server.arg("profileName").c_str(), sizeof(p.profileName)-1); p.profileName[sizeof(p.profileName)-1] = '\0'; // Nullterminierung } // Wir verwenden die Werte aus dem EEPROM, damit nicht fälschlicherweise ECO-Werte genommen werden EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, p.setpointWasser); // Korrigiert: Lade Wert aus EEPROM EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, p.setpointDampf); // Korrigiert: Lade Wert aus EEPROM p.ecoModeMinutes = ecoModeMinutes; p.dynamicEcoActive = dynamicEcoActive; p.dampfVerzoegerung = dampfVerzoegerung; EEPROM.put(EEPROM_ADDR_PROFILE_1, p); EEPROM.commit(); server.sendHeader(F("Location"), F("/Profile")); // URL angepasst server.send(303); } // Lädt Profil 1 in die aktuellen Parameter - URL angepasst void handleLoadProfile1() { TemperatureProfile p; EEPROM.get(EEPROM_ADDR_PROFILE_1, p); SetpointWasser = p.setpointWasser; SetpointDampf = p.setpointDampf; ecoModeMinutes = p.ecoModeMinutes; dynamicEcoActive = p.dynamicEcoActive; dampfVerzoegerung = p.dampfVerzoegerung; EEPROM.put(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser); EEPROM.put(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf); EEPROM.put(EEPROM_ADDR_ECOMODE_MINUTES, ecoModeMinutes); EEPROM.put(EEPROM_ADDR_DYNAMIC_ECO_MODE, dynamicEcoActive); EEPROM.put(EEPROM_ADDR_STEAM_DELAY, dampfVerzoegerung); EEPROM.commit(); // PID-Parameter neu setzen, falls sie sich durch das Profil geändert haben // (Obwohl Profile aktuell keine PID-Parameter speichern, ist dies gute Praxis) // pidDampf.SetTunings(KpDampf, KiDampf, KdDampf); // Wenn Profile Kp/Ki/Kd enthalten würden // pidWasser.SetTunings(KpWasser, KiWasser, KdWasser); // Eco-Status zurücksetzen, falls nötig ecoModeAktiv = false; ecoModeActivatedTime = 0; server.sendHeader(F("Location"), F("/Profile")); // URL angepasst server.send(303); } // Speichert aktuelles Setup in Profil 2 - URL angepasst void handleSaveProfile2() { TemperatureProfile p; EEPROM.get(EEPROM_ADDR_PROFILE_2, p); if (server.hasArg("profileName")) { strncpy(p.profileName, server.arg("profileName").c_str(), sizeof(p.profileName)-1); p.profileName[sizeof(p.profileName)-1] = '\0'; // Nullterminierung } EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, p.setpointWasser); // Korrigiert EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, p.setpointDampf); // Korrigiert p.ecoModeMinutes = ecoModeMinutes; p.dynamicEcoActive = dynamicEcoActive; p.dampfVerzoegerung = dampfVerzoegerung; EEPROM.put(EEPROM_ADDR_PROFILE_2, p); EEPROM.commit(); server.sendHeader(F("Location"), F("/Profile")); // URL angepasst server.send(303); } // Lädt Profil 2 in die aktuellen Parameter - URL angepasst void handleLoadProfile2() { TemperatureProfile p; EEPROM.get(EEPROM_ADDR_PROFILE_2, p); SetpointWasser = p.setpointWasser; SetpointDampf = p.setpointDampf; ecoModeMinutes = p.ecoModeMinutes; dynamicEcoActive = p.dynamicEcoActive; dampfVerzoegerung = p.dampfVerzoegerung; EEPROM.put(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser); EEPROM.put(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf); EEPROM.put(EEPROM_ADDR_ECOMODE_MINUTES, ecoModeMinutes); EEPROM.put(EEPROM_ADDR_DYNAMIC_ECO_MODE, dynamicEcoActive); EEPROM.put(EEPROM_ADDR_STEAM_DELAY, dampfVerzoegerung); EEPROM.commit(); // PID-Parameter neu setzen, falls sie sich durch das Profil geändert haben // pidDampf.SetTunings(KpDampf, KiDampf, KdDampf); // pidWasser.SetTunings(KpWasser, KiWasser, KdWasser); // Eco-Status zurücksetzen, falls nötig ecoModeAktiv = false; ecoModeActivatedTime = 0; server.sendHeader(F("Location"), F("/Profile")); // URL angepasst server.send(303); }