#include #include #include #include #include #include #include "max6675.h" #include #include /* Hardware definitions */ #define SSR_WASSER_PIN D1 #define SSR_DAMPF_PIN D2 #define SHOT_TIMER_PIN D8 #define OLED_SDA D3 #define OLED_SCK D4 #define ANALOG_NTC_PIN A0 // Display setup Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire); // MAX6675 setup #define MAX6675_SCK D5 #define MAX6675_CS D6 #define MAX6675_SO D7 MAX6675 thermocouple(MAX6675_SCK, MAX6675_CS, MAX6675_SO); // Firmware-Version (wird auch auf dem Display angezeigt) String version = "2.4.1"; String versionHersteller = "Thomas Müller"; String versionHerstellerMail = "thomas@mueller.black"; String versionHerstellerWeb = "https://raw-designs.de/ | https://mueller.black/"; // Geräteinfo char infoHersteller[50]; char infoModell[50]; char infoZusatz[50]; // Profile struct TemperatureProfile { double setpointWasser; double setpointDampf; int ecoModeMinutes; bool dynamicEcoActive; char profileName[20]; }; // PID variables double SetpointDampf, InputDampf, OutputDampf; double SetpointWasser, InputWasser, OutputWasser; double OffsetDampf = 0.0, OffsetWasser = 0.0; double KpDampf = 2.0, KiDampf = 5.0, KdDampf = 1.0; double KpWasser = 2.0, KiWasser = 5.0, KdWasser = 1.0; PID pidDampf(&InputDampf, &OutputDampf, &SetpointDampf, KpDampf, KiDampf, KdDampf, DIRECT); PID pidWasser(&InputWasser, &OutputWasser, &SetpointWasser, KpWasser, KiWasser, KdWasser, DIRECT); // Standardwerte definieren double defaultSetpointDampf = 165.0; double defaultSetpointWasser = 93.0; double defaultOffsetDampf = 0.0; double defaultOffsetWasser = 0.0; double defaultKpDampf = 0.0; double defaultKiDampf = 1.0; double defaultKdDampf = 0.5; double defaultKpWasser = 12.0; double defaultKiWasser = 0.0; double defaultKdWasser = 2.5; int defaultEcoModeMinutes = 0; int defaultEcoModeTempWasser = 60; int defaultEcoModeTempDampf = 60; String defaultInfoHersteller = ""; String defaultInfoModell = ""; String defaultInfoZusatz = ""; // Web server setup ESP8266WebServer server(80); // MagicValue const char storageMagicValue[5] = "MGVE"; //------------------------------ // EEPROM Adressen als Konstanten 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 const int EEPROM_ADDR_DYNAMIC_ECO_MODE = 256; // 1 Byte (bool) const int EEPROM_ADDR_PROFILE_1 = 257; // 50 Bytes für Profile 1 const int EEPROM_ADDR_PROFILE_2 = 307; // 50 Bytes für Profile 2 const int EEPROM_ADDR_WIFI_CONFIG_MAGIC = 357; // 5 Bytes const int EEPROM_ADDR_WIFI_CONFIG_DATA = 362; // sizeof(WiFiConfig) ≈ 165 Bytes + Puffer //------------------------------ // Eco-Mode unsigned long lastShotTime = 0; int ecoModeMinutes = 0; bool ecoModeAktiv = 0; int ecoModeTempWasser = 50; int ecoModeTempDampf = 50; bool dynamicEcoActive = false; unsigned long ecoModeActivatedTime = 0; // Fast-Heat-Up bool fastHeatUpAktiv = 0; bool fastHeatUpHeating = 0; int fastHeatUpSetpoint = 130; // AutoTune-Einstellungen double tuningStep = 50; // Schrittweite für Output double tuningNoise = 1; // Toleranz für Änderungen double tuningStartValue = 50; // Startwert für AutoTune unsigned int tuningLookBack = 20; // Anzahl der Lookback-Zyklen PID_ATune* autoTuneWasser; PID_ATune* autoTuneDampf; bool autoTuneWasserActive = false; bool autoTuneDampfActive = false; // Shot timer unsigned long shotStartTime = 0; unsigned long shotEndTime = 0; bool shotActive = false; bool delayDisplayUpdate = false; // Shot-Zähler const int runtimeAddress = 500; // 4 Bytes (unsigned long) const int shotCounterAddress = 504; // 4 Bytes (unsigned long) → für große Zahlen unsigned long shotCounter = 0; // Mit unsigned long für große Zahlen // Betriebszeit unsigned long totalRuntime = 0; unsigned long lastRuntimeSave = 0; // Zeitpunkt des letzten Speicherns const unsigned long runtimeSaveInterval = 60000; // 1 Minute // Delays für Anzeige beim Start int delayInit1 = 1500; int delayInit2 = 3000; // Delays für den Loop, um SSRs korrekt zu schalten und Sensoren zu lesen unsigned long previousMillis = 0; // vorheriger Zeitpunkt const unsigned long interval = 250; // Intervall in Millisekunden // WiFi Signal Bitmap static const unsigned char PROGMEM wifiSymbol[] = { 0x1f, 0xc0, 0x20, 0x20, 0x4f, 0x90, 0x90, 0x48, 0x27, 0x20, 0x08, 0x80, 0x02, 0x00 }; // Gemeinsame CSS-Styles als PROGMEM static const char commonStyle[] PROGMEM = R"rawliteral( )rawliteral"; // Gemeinsame Navigation als PROGMEM static const char commonNav[] PROGMEM = R"rawliteral( )rawliteral"; // WiFi 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); }; bool loadWiFiConfig(WiFiConfig& config) { char magic[5] = { 0 }; EEPROM.get(EEPROM_ADDR_WIFI_CONFIG_MAGIC, magic); if (strncmp(magic, storageMagicValue, 4) != 0) { // Setze Standardwerte 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; } // Lese Daten ab der KORREKTEN Adresse EEPROM.get(EEPROM_ADDR_WIFI_CONFIG_DATA, config); return true; } void saveWiFiConfig(const WiFiConfig& config) { EEPROM.put(EEPROM_ADDR_WIFI_CONFIG_MAGIC, storageMagicValue); EEPROM.put(EEPROM_ADDR_WIFI_CONFIG_DATA, config); EEPROM.commit(); } void startAPMode() { WiFi.softAP(F("Dual PID-Controller")); Serial.print(F("AP-Mode IP: ")); Serial.println(WiFi.softAPIP()); } void handleWiFiConfig() { Serial.print("Groesse der WiFiConfig-Struktur (Bytes): "); Serial.println(sizeof(WiFiConfig)); 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); 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); currentConfig.dns = IPAddress(8, 8, 8, 8); } String html = FPSTR(wifiConfigForm); html += FPSTR(commonStyle); html += FPSTR(wifiConfigForm2); html += FPSTR(commonNav); html += FPSTR(wifiConfigForm3); html.replace("{SSID}", currentConfig.ssid); html.replace("{PASSWORD}", currentConfig.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); } 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"))); } 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(); } void handleForceAPMode() { // Leere WLAN-Konfiguration abspeichern WiFiConfig emptyConfig; 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(); } void checkWifiConnection() { if (WiFi.status() != WL_CONNECTED) { WiFi.reconnect(); if (WiFi.waitForConnectResult() != WL_CONNECTED) { Serial.println(F("WiFi-Verbindung kann nicht hergestellt werden!")); } } } void drawwifiSymbol(int16_t x, int16_t y) { display.drawBitmap(x, y, wifiSymbol, 13, 7, SH110X_WHITE); } void handleResetRuntime() { totalRuntime = 0; EEPROM.put(EEPROM_ADDR_RUNTIME, totalRuntime); EEPROM.commit(); server.sendHeader(F("Location"), F("/info")); server.send(303); } void handleResetShots() { shotCounter = 0; EEPROM.put(EEPROM_ADDR_SHOTCOUNTER, shotCounter); EEPROM.commit(); server.sendHeader(F("Location"), F("/info")); server.send(303); } // ------------------------------------------------------ // Festes Kennwort, das im Binärfile enthalten sein muss - Für FW-Update static const char FIRMWARE_PASSWORD[] = "FWKennwort123"; static bool passwordFound = false; static int passMatchPos = 0; const int passLength = sizeof(FIRMWARE_PASSWORD) - 1; // ------------------------------------------------------ void setup() { Serial.begin(115200); EEPROM.begin(1024); pinMode(SSR_DAMPF_PIN, OUTPUT); pinMode(SSR_WASSER_PIN, OUTPUT); pinMode(SHOT_TIMER_PIN, INPUT_PULLUP); // Magic Value aus EEPROM lesen 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(); } // Gelesene Werte prüfen, ggf. Standardwerte setzen EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf); if (!magicValueVorhanden || SetpointDampf > 200) { SetpointDampf = defaultSetpointDampf; EEPROM.put(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf); } EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser); if (!magicValueVorhanden || SetpointWasser > 200) { SetpointWasser = defaultSetpointWasser; EEPROM.put(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser); } EEPROM.get(EEPROM_ADDR_OFFSET_DAMPF, OffsetDampf); if (!magicValueVorhanden) { OffsetDampf = defaultOffsetDampf; EEPROM.put(EEPROM_ADDR_OFFSET_DAMPF, OffsetDampf); } EEPROM.get(EEPROM_ADDR_OFFSET_WASSER, OffsetWasser); if (!magicValueVorhanden) { OffsetWasser = defaultOffsetWasser; EEPROM.put(EEPROM_ADDR_OFFSET_WASSER, OffsetWasser); } EEPROM.get(EEPROM_ADDR_KP_DAMPF, KpDampf); if (!magicValueVorhanden) { KpDampf = defaultKpDampf; EEPROM.put(EEPROM_ADDR_KP_DAMPF, KpDampf); } EEPROM.get(EEPROM_ADDR_KI_DAMPF, KiDampf); if (!magicValueVorhanden) { KiDampf = defaultKiDampf; EEPROM.put(EEPROM_ADDR_KI_DAMPF, KiDampf); } EEPROM.get(EEPROM_ADDR_KD_DAMPF, KdDampf); if (!magicValueVorhanden) { KdDampf = defaultKdDampf; EEPROM.put(EEPROM_ADDR_KD_DAMPF, KdDampf); } EEPROM.get(EEPROM_ADDR_KP_WASSER, KpWasser); if (!magicValueVorhanden) { KpWasser = defaultKpWasser; EEPROM.put(EEPROM_ADDR_KP_WASSER, KpWasser); } EEPROM.get(EEPROM_ADDR_KI_WASSER, KiWasser); if (!magicValueVorhanden) { KiWasser = defaultKiWasser; EEPROM.put(EEPROM_ADDR_KI_WASSER, KiWasser); } EEPROM.get(EEPROM_ADDR_KD_WASSER, KdWasser); if (!magicValueVorhanden) { KdWasser = defaultKdWasser; EEPROM.put(EEPROM_ADDR_KD_WASSER, KdWasser); } EEPROM.get(EEPROM_ADDR_ECOMODE_MINUTES, ecoModeMinutes); if (!magicValueVorhanden || ecoModeMinutes < 0) { ecoModeMinutes = defaultEcoModeMinutes; EEPROM.put(EEPROM_ADDR_ECOMODE_MINUTES, ecoModeMinutes); } EEPROM.get(EEPROM_ADDR_ECOMODE_TEMP_WASSER, ecoModeTempWasser); if (!magicValueVorhanden || ecoModeTempWasser < 0) { ecoModeTempWasser = defaultEcoModeTempWasser; EEPROM.put(EEPROM_ADDR_ECOMODE_TEMP_WASSER, ecoModeTempWasser); } EEPROM.get(EEPROM_ADDR_ECOMODE_TEMP_DAMPF, ecoModeTempDampf); if (!magicValueVorhanden || ecoModeTempDampf < 0) { ecoModeTempDampf = defaultEcoModeTempDampf; EEPROM.put(EEPROM_ADDR_ECOMODE_TEMP_DAMPF, ecoModeTempDampf); } EEPROM.get(EEPROM_ADDR_INFO_HERSTELLER, infoHersteller); if (!magicValueVorhanden || strlen(infoHersteller) == 0) { strcpy(infoHersteller, defaultInfoHersteller.c_str()); EEPROM.put(EEPROM_ADDR_INFO_HERSTELLER, infoHersteller); } EEPROM.get(EEPROM_ADDR_INFO_MODELL, infoModell); if (!magicValueVorhanden || strlen(infoModell) == 0) { strcpy(infoModell, defaultInfoModell.c_str()); EEPROM.put(EEPROM_ADDR_INFO_MODELL, infoModell); } EEPROM.get(EEPROM_ADDR_INFO_ZUSATZ, infoZusatz); if (!magicValueVorhanden || strlen(infoZusatz) == 0) { strcpy(infoZusatz, defaultInfoZusatz.c_str()); EEPROM.put(EEPROM_ADDR_INFO_ZUSATZ, infoZusatz); } if (!magicValueVorhanden) { dynamicEcoActive = false; EEPROM.put(EEPROM_ADDR_DYNAMIC_ECO_MODE, dynamicEcoActive); } else { EEPROM.get(EEPROM_ADDR_DYNAMIC_ECO_MODE, dynamicEcoActive); } if (!magicValueVorhanden) { fastHeatUpAktiv = false; EEPROM.put(EEPROM_ADDR_FASTHEATUP_DATA, fastHeatUpAktiv); } else { EEPROM.get(EEPROM_ADDR_FASTHEATUP_DATA, fastHeatUpAktiv); } if (!magicValueVorhanden) { // Standardwerte initialisieren, wenn kein gültiger Magic-Value vorliegt: TemperatureProfile defaultProfile; defaultProfile.setpointWasser = defaultSetpointWasser; defaultProfile.setpointDampf = defaultSetpointDampf; defaultProfile.ecoModeMinutes = defaultEcoModeMinutes; defaultProfile.dynamicEcoActive = false; // Kopiere "Unbenannt" in das Namensfeld; strncpy kopiert maximal sizeof(profileName) Bytes. strncpy(defaultProfile.profileName, "Unbenannt", sizeof(defaultProfile.profileName)); // Sicherstellen, dass das Feld nullterminiert ist defaultProfile.profileName[sizeof(defaultProfile.profileName) - 1] = '\0'; // Beide Profile in den EEPROM schreiben EEPROM.put(EEPROM_ADDR_PROFILE_1, defaultProfile); EEPROM.put(EEPROM_ADDR_PROFILE_2, defaultProfile); EEPROM.commit(); } 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(); } 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); } // Werte schreiben EEPROM.commit(); if (fastHeatUpAktiv) { fastHeatUpHeating = true; } 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); pidDampf.SetMode(AUTOMATIC); pidWasser.SetMode(AUTOMATIC); pidDampf.SetTunings(KpDampf, KiDampf, KdDampf); pidWasser.SetTunings(KpWasser, KiWasser, KdWasser); 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); 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); 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); }); 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); }); 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.
" "
\n" "

Automatisches PID-Tuning für Wasser

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

Automatisches PID-Tuning für Dampf

\n" "\n" "
\n" "\n"); server.send(200, F("text/html"), page); }); server.on("/charts", handleCharts); 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); }); 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" "\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); }); 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) { 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; } } } 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(); }); 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(); }); server.on("/wifi-config", handleWiFiConfig); server.on("/saveWiFiConfig", handleSaveWiFiConfig); server.on("/forceAPMode", handleForceAPMode); server.on("/resetRuntime", handleResetRuntime); server.on("/resetShots", handleResetShots); // Routen für die Profile server.on("/profiles", handleProfiles); server.on("/saveProfile1", handleSaveProfile1); server.on("/loadProfile1", handleLoadProfile1); server.on("/saveProfile2", handleSaveProfile2); server.on("/loadProfile2", handleLoadProfile2); server.begin(); Serial.println(F("Webserver gestartet")); } void loop() { unsigned long currentMillis = millis(); if (currentMillis - previousMillis >= interval) { previousMillis = currentMillis; // Beispielhafter Temperatur-Input InputDampf = round(thermocouple.readCelsius() + OffsetDampf); InputWasser = round((analogRead(ANALOG_NTC_PIN) * (3.3 / 1023.0)) + OffsetWasser); handleAutoTune(); if (!autoTuneWasserActive) pidWasser.Compute(); if (!autoTuneDampfActive) pidDampf.Compute(); digitalWrite(SSR_DAMPF_PIN, (int)OutputDampf); digitalWrite(SSR_WASSER_PIN, (int)OutputWasser); } updateShotTimer(); 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 { ecoModeAktiv = false; ecoModeActivatedTime = 0; EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser); EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf); } } } updateDisplay(); server.handleClient(); static unsigned long lastRuntimeSecond = 0; if (millis() - lastRuntimeSecond >= 1000) { totalRuntime++; lastRuntimeSecond = millis(); } if (millis() - lastRuntimeSave >= 60000) { EEPROM.put(EEPROM_ADDR_RUNTIME, totalRuntime); EEPROM.commit(); lastRuntimeSave = millis(); checkWifiConnection(); } } void updateDisplay() { if (!autoTuneWasserActive && !autoTuneDampfActive) { if (delayDisplayUpdate && (millis() - shotEndTime < 2000)) { return; } else { delayDisplayUpdate = false; } display.clearDisplay(); if (WiFi.status() == WL_CONNECTED) {drawwifiSymbol(113, 0);} display.setCursor(0, 0); display.setTextColor(SH110X_WHITE); display.setTextSize(1); if (fastHeatUpHeating && !shotActive && ((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")); } else { if (shotActive) { unsigned long elapsed = millis() - shotStartTime; display.println(F("Shot-Timer:")); display.println(""); display.setTextSize(4); display.println(elapsed / 1000.0, 1); display.setTextSize(1); display.println(F("Sekunden")); fastHeatUpHeating = false; } else { display.println(F("Temperaturen:")); display.println(""); 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")); if (ecoModeAktiv && dynamicEcoActive) { display.print(F(" (ECO+)")); } if (ecoModeAktiv && !dynamicEcoActive) { display.print(F(" (ECO)")); } display.println(""); display.println(""); display.println(F("Dampf: ")); 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+)")); } if (ecoModeAktiv && !dynamicEcoActive) { display.print(F(" (ECO)")); } } } display.display(); } } void updateShotTimer() { if (digitalRead(SHOT_TIMER_PIN) == HIGH) { if (!shotActive) { shotActive = true; shotStartTime = millis(); } } else { if (shotActive) { unsigned long shotDuration = millis() - shotStartTime; 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; } } } void startAutoTuneWasser() { if(ecoModeAktiv) { ecoModeAktiv = false; EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser); } // Eco-Modus deaktivieren und Temp. zurücksetzen autoTuneWasser = new PID_ATune(&InputWasser, &OutputWasser); autoTuneWasser->SetOutputStep(tuningStep); autoTuneWasser->SetControlType(1); autoTuneWasser->SetNoiseBand(tuningNoise); autoTuneWasser->SetLookbackSec(tuningLookBack); OutputWasser = tuningStartValue; autoTuneWasserActive = true; } void startAutoTuneDampf() { if(ecoModeAktiv) { ecoModeAktiv = false; EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf); } // Eco-Modus deaktivieren und Temp. zurücksetzen autoTuneDampf = new PID_ATune(&InputDampf, &OutputDampf); autoTuneDampf->SetOutputStep(tuningStep); autoTuneDampf->SetControlType(1); autoTuneDampf->SetNoiseBand(tuningNoise); autoTuneDampf->SetLookbackSec(tuningLookBack); OutputDampf = tuningStartValue; autoTuneDampfActive = true; } void stopAutoTuneWasser() { if (autoTuneWasserActive) { autoTuneWasserActive = false; delete autoTuneWasser; autoTuneWasser = nullptr; OutputWasser = 0; } } void stopAutoTuneDampf() { if (autoTuneDampfActive) { autoTuneDampfActive = false; delete autoTuneDampf; autoTuneDampf = nullptr; OutputDampf = 0; } } void handleAutoTune() { if (autoTuneWasserActive) { 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(); autoTuneWasserActive = false; delete autoTuneWasser; } else { 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 verwenden!")); display.println(F("Anzeige erlischt,")); display.println(F("sobald der Vorgang")); display.println(F("abgeschlossen ist!")); display.display(); } } if (autoTuneDampfActive) { 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(); autoTuneDampfActive = false; delete autoTuneDampf; } else { 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 verwenden!")); display.println(F("Anzeige erlischt,")); display.println(F("sobald der Vorgang")); display.println(F("abgeschlossen ist!")); display.display(); } } } 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}
Boot-Version: {BOOT_VERSION}
CPU-Takt: {CPU_MHZ} MHz
Sketch-Größe: {SKETCH_SIZE} Bytes
Freier Sketch-Speicher: {FREE_SKETCH} Bytes
)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); 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)); html.replace(F("{FREE_HEAP}"), String(ESP.getFreeHeap())); html.replace(F("{SDK_VERSION}"), String(ESP.getSdkVersion())); html.replace(F("{BOOT_VERSION}"), String(ESP.getBootVersion())); html.replace(F("{CPU_MHZ}"), String(ESP.getCpuFreqMHz())); html.replace(F("{SKETCH_SIZE}"), String(ESP.getSketchSize())); html.replace(F("{FREE_SKETCH}"), String(ESP.getFreeSketchSpace())); server.send(200, F("text/html"), html); } 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.
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 zubereiten zu können. 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.


)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("")); server.send(200, F("text/html"), html); } 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.
)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); } 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); } 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

Zieltemperatur und Offset




PID Wasser




PID Dampf



)rawliteral"; String html = FPSTR(rootHtml); html += FPSTR(commonStyle); html += FPSTR(rootHtml2); html += FPSTR(commonNav); html += FPSTR(rootHtml3); 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); } void handleUpdate() { 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(); 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(); 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.put(EEPROM_ADDR_ECOMODE_MINUTES, ecoModeMinutes); EEPROM.put(EEPROM_ADDR_ECOMODE_TEMP_WASSER, ecoModeTempWasser); EEPROM.put(EEPROM_ADDR_ECOMODE_TEMP_DAMPF, ecoModeTempDampf); EEPROM.commit(); pidDampf.SetTunings(KpDampf, KiDampf, KdDampf); pidWasser.SetTunings(KpWasser, KiWasser, KdWasser); server.sendHeader(F("Location"), F("/")); server.send(303); } 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; } 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.commit(); server.sendHeader(F("Location"), F("/eco")); server.send(303); } 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); } void handleCharts() { static const char chartsHtml[] PROGMEM = R"rawliteral( Live-Temperaturverlauf )rawliteral"; static const char chartsHtml2[] PROGMEM = R"rawliteral( )rawliteral"; static const char chartsHtml3[] PROGMEM = R"rawliteral(

Live-Temperaturverlauf

)rawliteral"; String html = FPSTR(chartsHtml); html += FPSTR(commonStyle); html += FPSTR(chartsHtml2); html += FPSTR(commonNav); html += FPSTR(chartsHtml3); server.send(200, F("text/html"), html); } void handleProfiles() { // Vor dem Erzeugen des HTML die aktuellen Profile aus dem EEPROM lesen TemperatureProfile p1; TemperatureProfile p2; EEPROM.get(EEPROM_ADDR_PROFILE_1, p1); EEPROM.get(EEPROM_ADDR_PROFILE_2, p2); // HTML-Erzeugung static const char profilesHtml[] PROGMEM = R"rawliteral( Profile )rawliteral"; static const char profilesHtml2[] PROGMEM = R"rawliteral( )rawliteral"; // Wir bauen eine gemeinsame Seite mit den Daten beider Profile. // Für jeden "Speichern"-Button kann man den Namen anpassen. // "profileName1" und "profileName2" - so kann man sie unabhängig speichern. 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
- Profilname

Profil 1 (Name: {PROFILE1NAME})



Aktuelle Werte in Profil 1 speichern.

Profil 1 laden

Temperatureinstellungen aus Profil 1 laden.

Profil 2 (Name: {PROFILE2NAME})



Aktuelle Werte in Profil 2 speichern.

Profil 2 laden

Temperatureinstellungen aus Profil 2 laden

)rawliteral"; String page = FPSTR(profilesHtml); page += FPSTR(commonStyle); page += FPSTR(profilesHtml2); page += FPSTR(commonNav); // Im dritten Teil füllen wir die Platzhalter {PROFILE1NAME} und {PROFILE2NAME}. 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); } 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)); } p.setpointWasser = EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser); // Werte aus EEPROM übernehmen, damit fälschlicherweise Werte aus dem laufenden Eco-Modus übernommen werden! p.setpointDampf = EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf); // Werte aus EEPROM übernehmen, damit fälschlicherweise Werte aus dem laufenden Eco-Modus übernommen werden! p.ecoModeMinutes = ecoModeMinutes; p.dynamicEcoActive = dynamicEcoActive; p.dampfVerzoegerung = dampfVerzoegerung; // NEU: Dampf-Verzögerung speichern EEPROM.put(EEPROM_ADDR_PROFILE_1, p); EEPROM.commit(); server.sendHeader(F("Location"), F("/profiles")); server.send(303); } 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; // NEU: Dampf-Verzögerung laden 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); } 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); // Werte aus EEPROM übernehmen, damit fälschlicherweise Werte aus dem laufenden Eco-Modus übernommen werden! p.setpointDampf = EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf); // Werte aus EEPROM übernehmen, damit fälschlicherweise Werte aus dem laufenden Eco-Modus übernommen werden! p.ecoModeMinutes = ecoModeMinutes; p.dynamicEcoActive = dynamicEcoActive; p.dampfVerzoegerung = dampfVerzoegerung; // NEU: Dampf-Verzögerung speichern EEPROM.put(EEPROM_ADDR_PROFILE_2, p); EEPROM.commit(); server.sendHeader(F("Location"), F("/profiles")); server.send(303); } 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; // NEU: Dampf-Verzögerung laden 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); }