/************************************************************************************ * Dual-PID Controller Firmware * Version 2.5.2 * * Dieses Programm steuert Wasser- und Dampftemperatur über zwei unabhängige PID-Regler * mithilfe eines MAX6675-Thermo-Elements (Dampf) und eines NTC-Sensors (Wasser). * Zusätzlich werden diverse Funktionen wie Eco-Modus, Profile, Fast-Heat-Up und * WLAN-Funktionen für eine Weboberfläche bereitgestellt. * * Compiler- und Hardware-Umgebung: * - ESP8266: Wemos D1 mini, 4MB * - Bibliotheken: Wire, Adafruit_GFX, Adafruit_SH110X, EEPROM, PID_v1, PID_AutoTune_v0, * max6675, ESP8266WiFi, ESP8266WebServer ************************************************************************************/ #include #include #include #include #include #include #include "max6675.h" #include #include /************************************************************************************ * 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.5.7"; 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 = true; // 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 ************************************************************************************/ double SetpointDampf, InputDampf, OutputDampf; double SetpointWasser, InputWasser, OutputWasser; double OffsetDampf = 0.0, OffsetWasser = 0.0; // PID-Variablen (Regelungsparameter) double KpDampf = 3.0, KiDampf = 0.1, KdDampf = 0.5; double KpWasser = 5.0, KiWasser = 0.2, KdWasser = 1.0; // Instanziierung der PID-Regler 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 /************************************************************************************ * 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_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 --- (NEU) 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 ---Adressen für Sicherheitslimits 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 /************************************************************************************ * 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 = 25.0; const double defaultTuningNoiseWasser = 1.0; const double defaultTuningStartValueWasser = 95.0; const unsigned int defaultTuningLookBackWasser = 60; // Parameter für Dampf (NEU) double tuningStepDampf; double tuningNoiseDampf; double tuningStartValueDampf; unsigned int tuningLookBackDampf; const double defaultTuningStepDampf = 15.0; // Höher wegen mehr Leistung const double defaultTuningNoiseDampf = 1.0; // Thermoblock könnte "rauschen" const double defaultTuningStartValueDampf = 95.0; // Höherer Startpunkt im Dampfbereich const unsigned int defaultTuningLookBackDampf = 30; // Evtl. längere Beobachtung nötig 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; bool delayDisplayUpdate = false; /************************************************************************************ * 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 runtimeSaveInterval = 60000; // Speichern alle 60s /************************************************************************************ * 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; /************************************************************************************ * 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) ************************************************************************************/ static const char commonStyle[] PROGMEM = R"rawliteral( )rawliteral"; /************************************************************************************ * Gemeinsame Navigation (PROGMEM) ************************************************************************************/ static const char commonNav[] PROGMEM = R"rawliteral( )rawliteral"; /************************************************************************************ * WiFi-Konfigurationsstrukturen und Funktionen ************************************************************************************/ 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); }; // Liest die WLAN-Konfiguration aus dem EEPROM bool loadWiFiConfig(WiFiConfig& config) { char magic[5] = { 0 }; EEPROM.get(EEPROM_ADDR_WIFI_CONFIG_MAGIC, magic); // 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; } // Laden der gespeicherten Daten EEPROM.get(EEPROM_ADDR_WIFI_CONFIG_DATA, config); return true; } // 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); EEPROM.commit(); } // Aktiviert den Access Point Modus und zeigt IP an void startAPMode() { WiFi.softAP(F("Dual PID-Controller")); Serial.print(F("AP-Mode IP: ")); Serial.println(WiFi.softAPIP()); } // Webserver-Handler: WLAN-Konfigurations-Seite void handleWiFiConfig() { static const char wifiConfigForm[] PROGMEM = R"rawliteral( WLAN-Konfiguration )rawliteral"; static const char wifiConfigForm2[] PROGMEM = R"rawliteral( )rawliteral"; static const char wifiConfigForm3[] PROGMEM = R"rawliteral(

WLAN-Konfiguration

WLAN-Einstellungen

IP-Einstellungen

AP-Modus

Verwendung im Access Point-Modus (AP).
Erreichbarkeit unter IP-Adresse: 192.168.4.1

)rawliteral"; WiFiConfig currentConfig; char magic[5] = { 0 }; EEPROM.get(EEPROM_ADDR_WIFI_CONFIG_MAGIC, magic); bool configValid = (strncmp(magic, storageMagicValue, 4) == 0); // Wenn configValid, lädt die gespeicherten Daten, sonst Standardwerte if (configValid) { EEPROM.get(EEPROM_ADDR_WIFI_CONFIG_DATA, currentConfig); } 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); } // HTML zusammenbauen String html = FPSTR(wifiConfigForm); html += FPSTR(commonStyle); html += FPSTR(wifiConfigForm2); html += FPSTR(commonNav); html += FPSTR(wifiConfigForm3); html.replace("{SSID}", currentConfig.ssid); // Im Demo-Modus wird das Passwort durch Sternchen ersetzt if(!demoModus) { html.replace("{PASSWORD}", currentConfig.password); } else { html.replace("{PASSWORD}", "*** *** *** ***"); } html.replace("{IP}", currentConfig.staticIP.toString()); html.replace("{GATEWAY}", currentConfig.gateway.toString()); html.replace("{SUBNET}", currentConfig.subnet.toString()); html.replace("{DHCP_CHECKED}", currentConfig.useStaticIP ? "" : "checked"); html.replace("{STATIC_CHECKED}", currentConfig.useStaticIP ? "checked" : ""); html.replace("{STATIC_DISPLAY}", currentConfig.useStaticIP ? "block" : "none"); server.send(200, F("text/html"), html); } // Handler zum Speichern der WLAN-Konfiguration 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)); strncpy(newConfig.password, server.arg(F("password")).c_str(), sizeof(newConfig.password)); 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"))); } // Im Demo-Modus wird nichts gespeichert, sonst schon if(!demoModus) { saveWiFiConfig(newConfig); } 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); html += FPSTR(saveConfigHtml3); server.send(200, F("text/html"), html); delay(5000); ESP.restart(); } // Handler, um den AP-Modus per Knopfdruck zu erzwingen void handleForceAPMode() { // Leer-Konfiguration speichern, damit beim nächsten Start AP aktiv wird WiFiConfig emptyConfig; 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); html += FPSTR(forceAPHtml3); server.send(200, F("text/html"), html); delay(1000); ESP.restart(); } // Prüfung der WiFi-Verbindung (Reconnect falls getrennt) 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 void drawwifiSymbol(int16_t x, int16_t y) { display.drawBitmap(x, y, wifiSymbol, 13, 7, SH110X_WHITE); } /************************************************************************************ * Handler zum Rücksetzen der Betriebszeit ************************************************************************************/ void handleResetRuntime() { totalRuntime = 0; EEPROM.put(EEPROM_ADDR_RUNTIME, totalRuntime); EEPROM.commit(); server.sendHeader(F("Location"), F("/info")); server.send(303); } /************************************************************************************ * Handler zum Rücksetzen des Shotzählers ************************************************************************************/ void handleResetShots() { shotCounter = 0; EEPROM.put(EEPROM_ADDR_SHOTCOUNTER, shotCounter); EEPROM.commit(); server.sendHeader(F("Location"), F("/info")); server.send(303); } /************************************************************************************ * Variable für Systemstartzeit (relevant für die Dampf-Verzögerung) ************************************************************************************/ unsigned long startupTime; /************************************************************************************ * setup(): Initialisierung des Systems ************************************************************************************/ void setup() { Serial.begin(115200); EEPROM.begin(1024); pinMode(SSR_DAMPF_PIN, OUTPUT); pinMode(SSR_WASSER_PIN, OUTPUT); pinMode(SHOT_TIMER_PIN, INPUT_PULLUP); // 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; EEPROM.put(EEPROM_ADDR_MAGICVALUE, storageMagicValue); EEPROM.commit(); } // 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); } else { SetpointDampf = defaultSetpointDampf; EEPROM.put(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf); SetpointWasser = defaultSetpointWasser; EEPROM.put(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser); OffsetDampf = defaultOffsetDampf; EEPROM.put(EEPROM_ADDR_OFFSET_DAMPF, OffsetDampf); OffsetWasser = defaultOffsetWasser; EEPROM.put(EEPROM_ADDR_OFFSET_WASSER, OffsetWasser); KpDampf = defaultKpDampf; EEPROM.put(EEPROM_ADDR_KP_DAMPF, KpDampf); KiDampf = defaultKiDampf; EEPROM.put(EEPROM_ADDR_KI_DAMPF, KiDampf); KdDampf = defaultKdDampf; EEPROM.put(EEPROM_ADDR_KD_DAMPF, KdDampf); KpWasser = defaultKpWasser; EEPROM.put(EEPROM_ADDR_KP_WASSER, KpWasser); KiWasser = defaultKiWasser; EEPROM.put(EEPROM_ADDR_KI_WASSER, KiWasser); KdWasser = defaultKdWasser; EEPROM.put(EEPROM_ADDR_KD_WASSER, KdWasser); ecoModeMinutes = defaultEcoModeMinutes; EEPROM.put(EEPROM_ADDR_ECOMODE_MINUTES, ecoModeMinutes); ecoModeTempWasser = defaultEcoModeTempWasser; EEPROM.put(EEPROM_ADDR_ECOMODE_TEMP_WASSER, ecoModeTempWasser); ecoModeTempDampf = defaultEcoModeTempDampf; EEPROM.put(EEPROM_ADDR_ECOMODE_TEMP_DAMPF, ecoModeTempDampf); strcpy(infoHersteller, defaultInfoHersteller.c_str()); EEPROM.put(EEPROM_ADDR_INFO_HERSTELLER, infoHersteller); strcpy(infoModell, defaultInfoModell.c_str()); EEPROM.put(EEPROM_ADDR_INFO_MODELL, infoModell); strcpy(infoZusatz, defaultInfoZusatz.c_str()); EEPROM.put(EEPROM_ADDR_INFO_ZUSATZ, infoZusatz); dynamicEcoActive = false; EEPROM.put(EEPROM_ADDR_DYNAMIC_ECO_MODE, dynamicEcoActive); fastHeatUpAktiv = false; EEPROM.put(EEPROM_ADDR_FASTHEATUP_DATA, fastHeatUpAktiv); dampfVerzoegerung = defaultDampfVerzoegerung; EEPROM.put(EEPROM_ADDR_STEAM_DELAY, dampfVerzoegerung); 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); // --- Bestehende Tuning-Parameter als Wasser-Defaults speichern --- tuningStepWasser = defaultTuningStepWasser; // Name geändert tuningNoiseWasser = defaultTuningNoiseWasser; // Name geändert tuningStartValueWasser = defaultTuningStartValueWasser; // Name geändert tuningLookBackWasser = defaultTuningLookBackWasser; // Name geändert EEPROM.put(EEPROM_ADDR_TUNING_STEP_WASSER, tuningStepWasser); EEPROM.put(EEPROM_ADDR_TUNING_NOISE_WASSER, tuningNoiseWasser); EEPROM.put(EEPROM_ADDR_TUNING_STARTVALUE_WASSER, tuningStartValueWasser); EEPROM.put(EEPROM_ADDR_TUNING_LOOKBACK_WASSER, tuningLookBackWasser); // --- NEU: Dampf-Tuning-Parameter als Defaults speichern --- tuningStepDampf = defaultTuningStepDampf; tuningNoiseDampf = defaultTuningNoiseDampf; tuningStartValueDampf = defaultTuningStartValueDampf; tuningLookBackDampf = defaultTuningLookBackDampf; EEPROM.put(EEPROM_ADDR_TUNING_STEP_DAMPF, tuningStepDampf); EEPROM.put(EEPROM_ADDR_TUNING_NOISE_DAMPF, tuningNoiseDampf); EEPROM.put(EEPROM_ADDR_TUNING_STARTVALUE_DAMPF, tuningStartValueDampf); EEPROM.put(EEPROM_ADDR_TUNING_LOOKBACK_DAMPF, tuningLookBackDampf); // NEU: Default-Sicherheitslimits speichern (nutzt die initialisierten globalen Variablen) EEPROM.put(EEPROM_ADDR_MAX_TEMP_WASSER, maxTempWasser); EEPROM.put(EEPROM_ADDR_MAX_TEMP_DAMPF, maxTempDampf); EEPROM.commit(); } // WiFi-Einstellungen laden und ggf. herstellen WiFiConfig wifiConfig; bool hasWiFiConfig = loadWiFiConfig(wifiConfig); if (hasWiFiConfig) { WiFi.mode(WIFI_STA); if (wifiConfig.useStaticIP) { WiFi.config(wifiConfig.staticIP, wifiConfig.gateway, wifiConfig.subnet); } 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!")); } else { Serial.println(F("\nKonnte keine Verbindung herstellen!")); startAPMode(); } } else { startAPMode(); } // Betriebszeit und Shotcounter aus EEPROM laden EEPROM.get(EEPROM_ADDR_RUNTIME, totalRuntime); if (totalRuntime == ULONG_MAX) { totalRuntime = 0; } EEPROM.get(EEPROM_ADDR_SHOTCOUNTER, shotCounter); if (shotCounter == ULONG_MAX) { shotCounter = 0; EEPROM.put(EEPROM_ADDR_SHOTCOUNTER, shotCounter); } 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")); display.display(); delay(delayInit1); // PID-Regler aktivieren und Parameter setzen pidDampf.SetMode(AUTOMATIC); pidWasser.SetMode(AUTOMATIC); pidDampf.SetTunings(KpDampf, KiDampf, KdDampf); pidWasser.SetTunings(KpWasser, KiWasser, KdWasser); // Anzeige zur WiFi-Verbindung if (WiFi.status() != WL_CONNECTED) { 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("WiFi-Verbindung:")); display.println(F("AP-Modus gestartet")); display.println(WiFi.softAPIP()); } else { 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("Webserver gestartet.")); display.println(F("IP-Adresse:")); display.println(WiFi.localIP()); } display.display(); delay(delayInit2); // Routen für Webserver server.on("/", handleRoot); server.on("/info", handleInfo); server.on("/eco", handleEco); server.on("/fast-heat-up", handleFastHeatUp); server.on("/updateFast-Heat-Up-Settings", handleFastHeatUpSettings); server.on("/updateSettings", handleUpdate); server.on("/updateInfoSettings", handleInfoUpdate); 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); 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); 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); 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

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

" // --- HIER BEGINNEN DIE ÄNDERUNGEN --- "

Parameter Wasser / Kessel

" // Überschrift für Wasser "" "" // ID, Name, Placeholder angepasst "" "" // ID, Name, Placeholder angepasst "" "" // ID, Name, Placeholder angepasst "" "" // ID, Name, Placeholder angepasst "

Parameter Dampf / Thermoblock

" // Überschrift für Dampf "" "" // NEUE Felder "" "" // NEUE Felder "" "" // NEUE Felder "" "" // NEUE Felder // --- HIER ENDEN DIE ÄNDERUNGEN --- "\n" "
\n" "


" "
" "

Erklärung der Parameter:

\n" "

Step:
\n" "Dieser Wert bestimmt, wie stark der Ausgangswert während des AutoTune-Vorgangs verändert wird. Mit anderen Worten, er gibt die Größe des Impulses an, der in das System eingespeist wird, um Oszillationen zu erzeugen. Ein zu hoher Wert kann zu übermaßigen Ausschlägen führen, während ein zu niedriger Wert möglicherweise nicht genug Dynamik erzeugt.

\n" "

Noise:
\n" "Dieser Parameter definiert einen Toleranzbereich (Noise-Band) für die gemessenen Eingangswerte. Kleine Schwankungen, die unterhalb dieses Wertes liegen, werden als Rauschen betrachtet und ignoriert. Das hilft, die Auswirkungen von Messrauschen zu minimieren und sicherzustellen, dass nur signifikante Änderungen in der Systemantwort zur Bestimmung der PID-Parameter herangezogen werden.

\n" "

StartValue:
\n" "Hier wird der Anfangswert für den Ausgang des Reglers festgelegt, wenn der AutoTune-Vorgang startet. Dieser Wert sollte idealerweise im linear arbeitenden Bereich des Systems liegen, da von diesem Ausgangspunkt aus der tuningStep angewendet wird. Er beeinflusst also, in welchem Bereich die anschließenden Impulse liegen.

\n" "

LookBack:
\n" "Dieser Parameter legt fest, über welchen Zeitraum (in Sekunden) der Algorithmus die vergangene Systemantwort analysiert. Er bestimmt quasi das „Fenster“, über das die Schwingungsperiode und damit die Dynamik des Systems gemessen wird. Ein zu kurzes Fenster könnte zu einer ungenauen Erfassung der Oszillation führen, während ein zu langes Fenster unnötig viel Zeit benötigt.

\n" "
" "\n"); // --- Placeholders ersetzen --- page.replace("{TUNING_STEP_WASSER}", String(tuningStepWasser, 2)); // Angepasst page.replace("{TUNING_NOISE_WASSER}", String(tuningNoiseWasser, 2)); // Angepasst page.replace("{TUNING_STARTVALUE_WASSER}", String(tuningStartValueWasser, 2)); // Angepasst page.replace("{TUNING_LOOKBACK_WASSER}", String(tuningLookBackWasser)); // Angepasst page.replace("{TUNING_STEP_DAMPF}", String(tuningStepDampf, 2)); // NEU page.replace("{TUNING_NOISE_DAMPF}", String(tuningNoiseDampf, 2)); // NEU page.replace("{TUNING_STARTVALUE_DAMPF}", String(tuningStartValueDampf, 2)); // NEU page.replace("{TUNING_LOOKBACK_DAMPF}", String(tuningLookBackDampf)); // NEU 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")); server.send(303); }); // Chart-Anzeige-Handler server.on("/charts", handleCharts); // JSON-Daten für Temperaturwerte 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 server.on("/updateFirmware", HTTP_GET, []() { String page = F("\n\n\nFirmware-Update\n"); page += FPSTR(commonStyle); page += F("\n"); page += FPSTR(commonNav); page += F("\n" "

Firmware

\n" "
\n" "

Firmware-Info

\n" "Firmware-Version: {FIRMWAREVERSION}
\n" "Hersteller: {SWHERSTELLER}
\n" "E-Mail: {SWHERSTELLERMAIL}
\n" "Website: {SWHERSTELLERWEBSITE}
\n" "
\n" "
\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" "
" "\n" "
\n" "\n"); page.replace(F("{FIRMWAREVERSION}"), version); page.replace(F("{SWHERSTELLER}"), versionHersteller); page.replace(F("{SWHERSTELLERMAIL}"), versionHerstellerMail); page.replace(F("{SWHERSTELLERWEBSITE}"), versionHerstellerWeb); server.send(200, F("text/html"), page); }); // Firmware-Upload-Handler 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}

Sie werden in 30 Sekunden zur Startseite weitergeleitet...

)rawliteral"; String htmlResponse = FPSTR(updateDone); htmlResponse += FPSTR(commonStyle); htmlResponse += FPSTR(updateDone2); htmlResponse += FPSTR(commonNav); 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 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); html += FPSTR(abortTuning3); server.send(200, F("text/html"), html); stopAutoTuneWasser(); stopAutoTuneDampf(); }); // Routen für WLAN-Einstellungen server.on("/netzwerk", handleWiFiConfig); server.on("/saveWiFiConfig", handleSaveWiFiConfig); server.on("/forceAPMode", handleForceAPMode); // Routen zum Zurücksetzen von Betriebszeit und ShotCounter server.on("/resetRuntime", handleResetRuntime); server.on("/resetShots", handleResetShots); // Routen für Profile server.on("/profiles", handleProfiles); server.on("/saveProfile1", handleSaveProfile1); server.on("/loadProfile1", handleLoadProfile1); server.on("/saveProfile2", handleSaveProfile2); server.on("/loadProfile2", handleLoadProfile2); // Webserver starten server.begin(); Serial.println(F("Webserver gestartet")); startupTime = millis(); } /************************************************************************************ * loop(): Hauptschleife ************************************************************************************/ void loop() { unsigned long currentMillis = millis(); // Intervall für Messung/SSR-Schaltung if (currentMillis - previousMillis >= interval) { previousMillis = currentMillis; // Temperaturmessung InputDampf = round(thermocouple.readCelsius() + OffsetDampf); InputWasser = round(readNTCTemperature() + OffsetWasser); // --- Sicherheitsüberprüfung Wasser --- // Prüft, ob der Messwert im gültigen Bereich (0°C - Sicherheitsabschaltungs-Wert) liegt. // Beinhaltet auch die Prüfung auf den Fehlerwert -273.15 aus readNTCTemperature. if (InputWasser < 0.0 || InputWasser > maxTempWasser) { wasserSensorError = true; // Fehlerflag setzen digitalWrite(SSR_WASSER_PIN, LOW); // Heizung SOFORT ausschalten OutputWasser = 0; // PID-Output zur Sicherheit auf 0 setzen Serial.println("WARNUNG: Wassertemperatur außerhalb des gültigen Bereichs <0°C oder >maxTempWasser!"); } else { wasserSensorError = false; // Kein Fehler, Flag zurücksetzen // PID nur berechnen und SSR schalten, wenn kein Fehler und kein AutoTune aktiv if (!autoTuneWasserActive) { pidWasser.Compute(); digitalWrite(SSR_WASSER_PIN, (int)OutputWasser); } // AutoTune hat seine eigene Logik zum Setzen von OutputWasser, SSR wird dort indirekt gesteuert } // --- Sicherheitsüberprüfung Dampf --- // Prüft, ob der Messwert gültig ist (kein NaN) und im Bereich >0°C und (oft schon in Arduino.h enthalten) if (isnan(InputDampf) || InputDampf < 0.0 || InputDampf > maxTempDampf) { dampfSensorError = true; // Fehlerflag setzen digitalWrite(SSR_DAMPF_PIN, LOW); // Heizung SOFORT ausschalten OutputDampf = 0; // PID-Output zur Sicherheit auf 0 setzen Serial.println("WARNUNG: Dampftemperatur ungültig oder außerhalb des Bereichs <0°C oder >maxTempDampf!"); } else { dampfSensorError = false; // Kein Fehler, Flag zurücksetzen // PID nur berechnen und SSR schalten, wenn kein Fehler, kein AutoTune und keine Startverzögerung aktiv if (!autoTuneDampfActive) { // Dampf-Startverzögerung prüfen (innerhalb der 'kein Fehler'-Logik) if (dampfVerzoegerung > 0 && (millis() - startupTime < (unsigned long)dampfVerzoegerung * 60000UL)) { // Während der Verzögerung: Heizen verhindern, auch wenn Sensor OK ist // SetpointDampf = 0; // Setpoint temporär ändern ist nicht ideal, besser direkt SSR steuern digitalWrite(SSR_DAMPF_PIN, LOW); // Heizung aus während Verzögerung OutputDampf = 0; // Sicherstellen, dass PID Output 0 ist } else { // Normalbetrieb nach Verzögerung und ohne Sensorfehler pidDampf.Compute(); digitalWrite(SSR_DAMPF_PIN, (int)OutputDampf); } } // AutoTune hat seine eigene Logik zum Setzen von OutputDampf } // AutoTune-Logik (unverändert - beachte: AutoTune stoppt nicht automatisch bei Sensorfehler!) handleAutoTune(); // Die SSR-digitalWrites sind jetzt innerhalb der Sicherheits-if/else-Blöcke } // 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 (unverändert im Aufruf) updateDisplay(); server.handleClient(); // Webserver (unverändert) // Betriebszeit zählen (unverändert) static unsigned long lastRuntimeSecond = 0; if (millis() - lastRuntimeSecond >= 1000) { totalRuntime++; lastRuntimeSecond = millis(); } // Speichern und WiFi-Check (unverändert) if (millis() - lastRuntimeSave >= runtimeSaveInterval) // 60000 ms = 1 Minute { EEPROM.put(EEPROM_ADDR_RUNTIME, totalRuntime); // Optional: Nur committen, wenn sich Wert geändert hat (spart Schreibzyklen) // EEPROM.commit(); // Commit kann auch seltener erfolgen, wenn nötig lastRuntimeSave = millis(); // Prüfe WiFi Verbindung nur wenn nicht im AP Modus if (WiFi.getMode() == WIFI_STA) { checkWifiConnection(); } // Commit hier, da ShotCounter etc. auch gespeichert werden könnten EEPROM.commit(); } } /************************************************************************************ * Liest die Temperatur vom NTC-Sensor (Wasser) über den Spannungsteiler (ADC) ************************************************************************************/ double readNTCTemperature() { int adcValue = analogRead(A0); if (adcValue <= 0) { return -273.15; // Fehler- fallback } double sensorVoltage = adcValue * (ADC_REF / 1023.0); double R_ntc = R_FIXED * ((V_SUPPLY / sensorVoltage) - 1.0); // Temperaturberechnung über die Beta-Gleichung double temperatureK = 1.0 / ((1.0 / T0) + (1.0 / beta) * log(R_ntc / R0)); return temperatureK - 273.15; } /************************************************************************************ * Aktualisiert das Display (Temperaturanzeige, Shot-Timer etc.) ************************************************************************************/ void updateDisplay() { // Wenn PID-Tuning läuft, wird das Display woanders beschrieben. if (!autoTuneWasserActive && !autoTuneDampfActive) { // Warte 2 Sekunden nach Shot-Ende, bevor die Anzeige zurückgeht if (delayDisplayUpdate && (millis() - shotEndTime < 2000)) { return; } else { delayDisplayUpdate = false; } display.clearDisplay(); if (WiFi.status() == WL_CONNECTED) { drawwifiSymbol(113, 0); // Position anpassen, falls nötig } display.setCursor(0, 0); display.setTextColor(SH110X_WHITE); display.setTextSize(1); // Fast-Heat-Up Anzeige // Zeige F-H-U nur an, wenn *kein* Sensorfehler vorliegt if (fastHeatUpHeating && !shotActive && !wasserSensorError && ((int)InputWasser < fastHeatUpSetpoint)) { SetpointWasser = fastHeatUpSetpoint; // Setpoint setzen (Sicherheit prüft Ist-Wert!) 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); // Zeige Zieltemp für F-H-U display.print(F(" ")); display.print((char)247); // Grad-Symbol display.print(F("C")); } else { // Anzeige, wenn Shot läuft if (shotActive) { unsigned long elapsed = millis() - shotStartTime; display.println(F("Shot-Timer:")); display.println(""); display.setTextSize(3); // Größere Schrift für Timer display.println(elapsed / 1000.0, 1); // Zeit in Sekunden mit einer Nachkommastelle display.setTextSize(1); // Zurück zur normalen Schriftgröße display.println(F("Sekunden")); fastHeatUpHeating = false; // Shot unterbricht FastHeatUp Anzeige/Logik } else // Normalanzeige (kein Shot, kein Fast-Heat-Up) { display.println(F("Temperaturen:")); display.println(""); // Wasser Temperatur / Fehleranzeige display.println(F("Wasser: ")); // Text geändert für Konsistenz if (wasserSensorError) { display.print(F("Sensorfehler!")); } else { display.print((int)InputWasser); display.print(F(" / ")); display.print((int)SetpointWasser); // Zeige aktuellen Sollwert display.print(F(" ")); display.print((char)247); // Grad-Symbol display.print(F("C")); // ECO Status nur anzeigen, wenn kein Fehler vorliegt if (ecoModeAktiv && dynamicEcoActive) display.print(F(" (ECO+)")); else if (ecoModeAktiv && !dynamicEcoActive) display.print(F(" (ECO)")); } display.println(""); // Leerzeile display.println(""); // Zusätzliche Leerzeile für Abstand // Dampf Temperatur / Fehleranzeige display.println(F("Dampf: ")); // Text geändert für Konsistenz if (dampfSensorError) { display.print(F("Sensorfehler!")); } else { // Anzeige nur, wenn keine Startverzögerung aktiv ist ODER Verzögerung abgelaufen if (!(dampfVerzoegerung > 0 && (millis() - startupTime < (unsigned long)dampfVerzoegerung * 60000UL))) { display.print((int)InputDampf); display.print(F(" / ")); display.print((int)SetpointDampf); // Zeige aktuellen Sollwert display.print(F(" ")); display.print((char)247); // Grad-Symbol display.print(F("C")); // ECO Status nur anzeigen, wenn kein Fehler vorliegt if (ecoModeAktiv && dynamicEcoActive) display.print(F(" (ECO+)")); else if (ecoModeAktiv && !dynamicEcoActive) display.print(F(" (ECO)")); } else { display.print(F("Startverz\x94gerung")); // Anzeige während der Verzögerung } } // Keine zusätzliche Leerzeile am Ende nötig } } display.display(); // Display aktualisieren } // Wenn AutoTune aktiv ist, wird das Display in handleAutoTune() beschrieben (unverändert) } /************************************************************************************ * Verwaltet den Shot-Timer (wenn z.B. der Nutzer den Bezug aktiviert) ************************************************************************************/ void updateShotTimer() { if (digitalRead(SHOT_TIMER_PIN) == HIGH) { // Shot beginnt if (!shotActive) { shotActive = true; shotStartTime = millis(); } } else { // Shot endet if (shotActive) { unsigned long shotDuration = millis() - shotStartTime; // nur Shots > 20 Sekunden zählen if (shotDuration > 20000) { shotCounter++; EEPROM.put(EEPROM_ADDR_SHOTCOUNTER, shotCounter); EEPROM.commit(); Serial.print(F("Shots: ")); Serial.println(shotCounter); } shotActive = false; lastShotTime = millis(); shotEndTime = millis(); delayDisplayUpdate = true; } } } /************************************************************************************ * Startet AutoTune für Wasser-PID ************************************************************************************/ 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); 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 OutputWasser = tuningStartValueWasser; // Angepasst // Wichtig: Den SSR Pin direkt mit dem Startwert ansteuern, bevor AutoTune übernimmt digitalWrite(SSR_WASSER_PIN, (int)OutputWasser); autoTuneWasserActive = true; Serial.println("Starte AutoTune für Wasser..."); } /************************************************************************************ * Startet AutoTune für Dampf-PID ************************************************************************************/ 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 // (Setpoint wird hier nicht geändert, da Tuning oft bei Betriebstemperatur startet) pidDampf.SetMode(AUTOMATIC); 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 OutputDampf = tuningStartValueDampf; // Angepasst // Wichtig: Den SSR Pin direkt mit dem Startwert ansteuern, bevor AutoTune übernimmt digitalWrite(SSR_DAMPF_PIN, (int)OutputDampf); autoTuneDampfActive = true; Serial.println("Starte AutoTune für Dampf..."); } /************************************************************************************ * Stoppt AutoTune für Wasser ************************************************************************************/ void stopAutoTuneWasser() { if (autoTuneWasserActive) { autoTuneWasserActive = false; delete autoTuneWasser; autoTuneWasser = nullptr; OutputWasser = 0; } } /************************************************************************************ * Stoppt AutoTune für Dampf ************************************************************************************/ void stopAutoTuneDampf() { if (autoTuneDampfActive) { autoTuneDampfActive = false; delete autoTuneDampf; autoTuneDampf = nullptr; OutputDampf = 0; } } /************************************************************************************ * Überwacht die AutoTune-Prozesse (Dampf/Wasser) und schreibt gefundene Werte ins EEPROM ************************************************************************************/ void handleAutoTune() { // AutoTune Wasser if (autoTuneWasserActive) { // NEU: 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 display.clearDisplay(); display.setCursor(0, 0); display.println(F("PID-Tuning Fehler:")); display.println(F("Wasser-Sensor!")); display.display(); delay(3000); // Nachricht kurz anzeigen return; // Funktion für diesen Durchlauf verlassen } // Wenn Runtime() 1 zurückgibt, ist der AutoTune-Prozess abgeschlossen // (Original-Logik, jetzt nur ausgeführt, wenn kein Sensorfehler vorliegt) if (autoTuneWasser->Runtime() == 1) { KpWasser = autoTuneWasser->GetKp(); KiWasser = autoTuneWasser->GetKi(); KdWasser = autoTuneWasser->GetKd(); pidWasser.SetTunings(KpWasser, KiWasser, KdWasser); 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 } else { // Anzeige während AutoTune (unverändert) display.clearDisplay(); display.setCursor(0, 0); display.println(F("PID-Tuning aktiv:")); display.println(F("Wasser-PID")); display.println(""); display.println(F("Bitte Maschine nicht")); display.println(F("verwenden!")); // Textumbruch für Lesbarkeit display.display(); } } // AutoTune Dampf if (autoTuneDampfActive) { // NEU: 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 display.clearDisplay(); display.setCursor(0, 0); display.println(F("PID-Tuning Fehler:")); display.println(F("Dampf-Sensor!")); display.display(); delay(3000); // Nachricht kurz anzeigen return; // Funktion für diesen Durchlauf verlassen } // Wenn Runtime() 1 zurückgibt, ist der AutoTune-Prozess abgeschlossen // (Original-Logik, jetzt nur ausgeführt, wenn kein Sensorfehler vorliegt) if (autoTuneDampf->Runtime() == 1) { KpDampf = autoTuneDampf->GetKp(); KiDampf = autoTuneDampf->GetKi(); KdDampf = autoTuneDampf->GetKd(); pidDampf.SetTunings(KpDampf, KiDampf, KdDampf); 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 } else { // Anzeige während AutoTune (unverändert) display.clearDisplay(); display.setCursor(0, 0); display.println(F("PID-Tuning aktiv:")); display.println(F("Dampf-PID")); display.println(""); display.println(F("Bitte Maschine nicht")); display.println(F("verwenden!")); display.display(); } } } /************************************************************************************ * Handler für die Info-Seite ************************************************************************************/ void handleInfo() { 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.

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
)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); html += FPSTR(infoHtml3); // <-- Stelle sicher, dass hier das angepasste HTML verwendet wird html.replace(F("{HERSTELLER}"), String(infoHersteller)); html.replace(F("{MODELL}"), String(infoModell)); html.replace(F("{ZUSATZ}"), String(infoZusatz)); html.replace(F("{RUNTIME}"), runtimeStr); html.replace(F("{SHOT_COUNT}"), String(shotCounter)); // Systeminfo-Ersetzungen html.replace(F("{FREE_HEAP}"), String(ESP.getFreeHeap())); html.replace(F("{SDK_VERSION}"), String(ESP.getSdkVersion())); html.replace(F("{CORE_VERSION}"), ESP.getCoreVersion()); // NEU - Ist schon String html.replace(F("{BOOT_VERSION}"), String(ESP.getBootVersion())); html.replace(F("{CHIP_ID}"), String(ESP.getChipId(), HEX)); // NEU - ChipID oft als HEX angezeigt html.replace(F("{CPU_MHZ}"), String(ESP.getCpuFreqMHz())); html.replace(F("{RESET_REASON}"), ESP.getResetReason()); // NEU - Ist schon String html.replace(F("{SKETCH_SIZE}"), String(ESP.getSketchSize())); html.replace(F("{FREE_SKETCH}"), String(ESP.getFreeSketchSpace())); html.replace(F("{FLASH_SIZE}"), String(ESP.getFlashChipSize())); // NEU html.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()); } html.replace(F("{WIFI_RSSI}"), rssi_str); // NEU server.send(200, F("text/html"), html); } /************************************************************************************ * Handler für die Eco-Modus-Konfiguration ************************************************************************************/ void handleEco() { 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); 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 ************************************************************************************/ void handleFastHeatUp() { 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); 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 ************************************************************************************/ 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")); server.send(303); } /************************************************************************************ * Handler für Root (PID-Einstellungen) ************************************************************************************/ 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

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); 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)); server.send(200, F("text/html"), html); } /************************************************************************************ * Schreibt die Werte in den EEPROM (PID/Eco) ************************************************************************************/ 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("/")); server.send(303); } /************************************************************************************ * Speichert Eco-Einstellungen ************************************************************************************/ 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")); server.send(303); } /************************************************************************************ * Speichert Geräte-Infos ************************************************************************************/ void handleInfoUpdate() { if (server.hasArg(F("hersteller"))) { strncpy(infoHersteller, server.arg(F("hersteller")).c_str(), sizeof(infoHersteller)); } if (server.hasArg(F("modell"))) { strncpy(infoModell, server.arg(F("modell")).c_str(), sizeof(infoModell)); } if (server.hasArg(F("zusatz"))) { strncpy(infoZusatz, server.arg(F("zusatz")).c_str(), sizeof(infoZusatz)); } EEPROM.put(EEPROM_ADDR_INFO_HERSTELLER, infoHersteller); EEPROM.put(EEPROM_ADDR_INFO_MODELL, infoModell); EEPROM.put(EEPROM_ADDR_INFO_ZUSATZ, infoZusatz); EEPROM.commit(); server.sendHeader(F("Location"), F("/info")); server.send(303); } /************************************************************************************ * Handler für Temperatur-Charts (Live) - Modernisierte Version mit Fehlermeldungen ************************************************************************************/ 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) { errorMessageHtml = "
Die Temperaturwerte sind aktuell nicht verfügbar
"; } 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) ************************************************************************************/ void handleProfiles() { TemperatureProfile p1; TemperatureProfile p2; EEPROM.get(EEPROM_ADDR_PROFILE_1, p1); EEPROM.get(EEPROM_ADDR_PROFILE_2, p2); 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); 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 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)); } // Wir verwenden die Werte aus dem EEPROM, damit nicht fälschlicherweise ECO-Werte genommen werden p.setpointWasser = EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser); p.setpointDampf = EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf); p.ecoModeMinutes = ecoModeMinutes; p.dynamicEcoActive = dynamicEcoActive; p.dampfVerzoegerung = dampfVerzoegerung; EEPROM.put(EEPROM_ADDR_PROFILE_1, p); EEPROM.commit(); server.sendHeader(F("Location"), F("/profiles")); server.send(303); } // Lädt Profil 1 in die aktuellen Parameter 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(); server.sendHeader(F("Location"), F("/profiles")); server.send(303); } // Speichert aktuelles Setup in Profil 2 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)); } p.setpointWasser = EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser); p.setpointDampf = EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf); p.ecoModeMinutes = ecoModeMinutes; p.dynamicEcoActive = dynamicEcoActive; p.dampfVerzoegerung = dampfVerzoegerung; EEPROM.put(EEPROM_ADDR_PROFILE_2, p); EEPROM.commit(); server.sendHeader(F("Location"), F("/profiles")); server.send(303); } // Lädt Profil 2 in die aktuellen Parameter 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(); server.sendHeader(F("Location"), F("/profiles")); server.send(303); }