From 4f840173a94bc261424bac1c9d038939bdc67748 Mon Sep 17 00:00:00 2001 From: raw-designs Date: Sat, 18 Jul 2026 04:09:13 +0200 Subject: [PATCH] =?UTF-8?q?feat(S3):=20Feed-Forward-Boost=20bei=20Bezug=20?= =?UTF-8?q?f=C3=BCr=20den=20Wasserkessel=20(5.2.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Während eines Bezugs strömt kaltes Wasser in den Kessel; die PID reagiert erst auf den verzögert gemessenen Temperaturabfall. Der neue Feed-Forward schaltet der Heizung sofort einen einstellbaren Leistungs-Boost auf (0-100 % der Fenstergröße, 0 % = aus), die PID regelt nur den Restfehler. - Zustandslose Aufschaltung in der SSR-Fensterlogik (effectiveOutputWasserMs), gedeckelt auf die Fenstergröße; Duty-Telemetrie (dutyW) zeigt den Boost mit - Optional: I-Anteil der Wasser-PID während des Bezugs einfrieren (zentral in der PID-Schleife verwaltet, applyPidWasserTunings-Helper) - Nur bei echten Bezügen (Hardware/Software-Shot), nicht bei Spülen, Wartungsmodus oder AutoTune; Sicherheitslogik unverändert - Web-UI: neue Sektion „Feed-Forward bei Bezug (Wasser)“ auf /PID; EEPROM 944/945, bewusst kein Profilfeld (maschinenabhängig) Co-Authored-By: Claude Fable 5 --- Changelog.txt | 14 +++++ Dual-PID.ino | 143 +++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 145 insertions(+), 12 deletions(-) diff --git a/Changelog.txt b/Changelog.txt index b171e5c..aa22c0f 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,3 +1,17 @@ +Version 5.2.0: +- Feed-Forward bei Bezug (Störgrößenaufschaltung) für den Wasserkessel: Während eines Bezugs wird der + Heizung sofort ein einstellbarer Leistungs-Boost (0–100 % der Fenstergröße, 0 % = aus) aufgeschaltet, + statt auf den vom Sensor gemeldeten Temperaturabfall zu warten. Die PID regelt nur noch den Restfehler; + der Temperatur-Dip während des Shots wird deutlich kleiner. Einstellung auf der /PID-Seite + („Feed-Forward bei Bezug"), gespeichert im EEPROM (bewusst kein Profilfeld, da maschinenabhängig). +- Optional: I-Anteil der Wasser-PID während des Bezugs einfrieren. Verhindert das Aufladen des + Integral-Anteils während des Shots und damit das Überschwingen nach Bezugsende. +- Der Boost wirkt nur bei echten Bezügen (Hardware-Schalter oder Software-Start), nicht bei + Spülen/Wartungsmodus/AutoTune. Sicherheitslogik unverändert: Sensorfehler, Übertemperatur-Abschaltung + und „Heizen oberhalb Setpoint verhindern" haben weiterhin Vorrang; Deckelung auf die Fenstergröße. +- Die Heizleistungs-Telemetrie (dutyW, u. a. für das Verlaufs-Chart des Touch-Displays) zeigt den + Boost mit an - damit ist die Wirkung direkt im Chart kalibrierbar. + Version 5.1.2: - Profil-Seite (Web-UI): Die neue „Aktives Profil"-Anzeige stand linksbündig außerhalb des Karten-Layouts. Sie nutzt jetzt den status-message-Stil der Seite (zentriert, max. 600 px, diff --git a/Dual-PID.ino b/Dual-PID.ino index 29981a8..92264af 100644 --- a/Dual-PID.ino +++ b/Dual-PID.ino @@ -369,7 +369,7 @@ Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire); * Firmware-Informationen ************************************************************************************/ -String version = "5.1.2"; +String version = "5.2.0"; String versionHersteller = "Thomas Müller"; String versionHerstellerMail = "thomas@mueller.black"; @@ -656,6 +656,17 @@ unsigned long windowStartTimeDampf; // Startzeit des aktuellen Fensters für D PID pidDampf(&InputDampf, &OutputDampf, &SetpointDampf, KpDampf, KiDampf, KdDampf, DIRECT); PID pidWasser(&InputWasser, &OutputWasser, &SetpointWasser, KpWasser, KiWasser, KdWasser, DIRECT); +// Feed-Forward (Störgrößenaufschaltung) bei Bezug: der Bezug ist eine bekannte Störung +// (kaltes Wasser strömt in den Kessel). Statt auf den Temperaturabfall zu warten, wird +// der Heizleistung während des Bezugs sofort ein fester Boost aufgeschaltet; die PID +// regelt nur noch den Restfehler. Optional wird der I-Anteil während des Bezugs +// eingefroren, damit er sich nicht auflädt (verhindert Überschwingen nach dem Shot). +uint8_t ffBoostPercentWasser = 0; // Zusatz-Heizleistung während Bezug in % der Fenstergröße (0 = deaktiviert) +bool ffIFreezeWasser = false; // I-Anteil der Wasser-PID während des Bezugs einfrieren +bool ffPidIFrozenWasser = false; // Laufzeitzustand: Ki ist aktuell auf 0 gesetzt +const uint8_t defaultFfBoostPercentWasser = 0; +const bool defaultFfIFreezeWasser = false; + // Variablen für Sicherheitsabschaltung double maxTempWasser = 110.0; // Standardwert, wie bisher hardcoded double maxTempDampf = 180.0; // Standardwert, wie bisher hardcoded @@ -944,7 +955,9 @@ const int EEPROM_ADDR_POWERON_STANDBY = 910; // 1 Byte (bool) -> Ende 9 const int EEPROM_ADDR_DISPLAY_TYPE = 911; // 1 Byte (uint8_t) -> Ende 911 const int EEPROM_ADDR_ACTIVE_PROFILE_NAME = 912; // 31 Bytes (char[31], nullterminiert) -> Ende 942 const int EEPROM_ADDR_ACTIVE_PROFILE_DIRTY = 943; // 1 Byte (bool) -> Ende 943 -// Naechste freie Adresse: 944 (Innerhalb EEPROM_SIZE=1024) +const int EEPROM_ADDR_FF_BOOST_PERCENT_WASSER = 944; // 1 Byte (uint8_t) -> Ende 944 +const int EEPROM_ADDR_FF_IFREEZE_WASSER = 945; // 1 Byte (bool) -> Ende 945 +// Naechste freie Adresse: 946 (Innerhalb EEPROM_SIZE=1024) /************************************************************************************ * Eco-Mode Variablen @@ -2871,6 +2884,8 @@ void resetToDefaults() { EEPROM.put(EEPROM_ADDR_BOOST_DAMPF_ACTIVE, defaultBoostDampfActive); EEPROM.put(EEPROM_ADDR_PREVENTHEAT_WASSER, defaultPreventHeatAboveSetpointWasser); EEPROM.put(EEPROM_ADDR_PREVENTHEAT_DAMPF, defaultPreventHeatAboveSetpointDampf); + EEPROM.put(EEPROM_ADDR_FF_BOOST_PERCENT_WASSER, defaultFfBoostPercentWasser); + EEPROM.put(EEPROM_ADDR_FF_IFREEZE_WASSER, (uint8_t)(defaultFfIFreezeWasser ? 1 : 0)); EEPROM.put(EEPROM_ADDR_MAX_TEMP_WASSER, maxTempWasser); // Sollte ggf. eigene Defaults haben EEPROM.put(EEPROM_ADDR_MAX_TEMP_DAMPF, maxTempDampf); // Sollte ggf. eigene Defaults haben @@ -3861,6 +3876,46 @@ void applyFlowGuardPumpControl(unsigned long currentShotTimeMillis, float netWei digitalWrite(PUMP_PIN, pumpOn ? HIGH : LOW); } +/************************************************************************************ + * Feed-Forward bei Bezug (Wasserkessel) + ************************************************************************************/ + +// Setzt die Wasser-PID-Tunings und berücksichtigt dabei einen eingefrorenen I-Anteil. +// Alle Stellen, die die Wasser-Tunings anwenden, müssen diese Funktion nutzen, damit +// der Freeze-Zustand bei Änderungen während eines Bezugs erhalten bleibt. +void applyPidWasserTunings() { + pidWasser.SetTunings(KpWasser, ffPidIFrozenWasser ? 0.0 : KiWasser, KdWasser); +} + +// Friert den I-Anteil der Wasser-PID ein bzw. gibt ihn wieder frei (nur bei Wechsel). +// Bei Ki=0 bleibt die aufsummierte I-Komponente in der PID-Library erhalten und wird +// nach dem Auftauen unverändert weiterverwendet. +void setPidWasserIntegralFrozen(bool frozen) { + if (frozen == ffPidIFrozenWasser) return; + ffPidIFrozenWasser = frozen; + applyPidWasserTunings(); +} + +// Feed-Forward nur während eines echten Bezugs (Hardware- oder Software-Shot), +// nicht im Wartungsmodus und nicht während AutoTune. +bool ffBoostWasserActiveNow() { + return ffBoostPercentWasser > 0 && (shotActive || shotSoftwareActive) && + !wartungsModusAktiv && !autoTuneWasserActive; +} + +// Effektive Einschaltzeit im aktuellen Fenster: PID-Output + Feed-Forward-Boost, +// gedeckelt auf die Fenstergröße. Zustandslos, damit SSR-Ansteuerung und +// Duty-Telemetrie (Chart!) denselben Wert sehen. +unsigned long effectiveOutputWasserMs() { + double out = OutputWasser; + if (ffBoostWasserActiveNow()) { + out += ((double)ffBoostPercentWasser / 100.0) * (double)windowSizeWasser; + } + if (out > (double)windowSizeWasser) out = (double)windowSizeWasser; + if (out < 0.0) out = 0.0; + return (unsigned long)out; +} + /************************************************************************************ * Stoppt den Brühvorgang (Pumpe/Ventil) und verarbeitet Shot-Daten. * Wird von updateShotTimer() oder aus loop() (BrewByTime/Weight) aufgerufen. @@ -4178,6 +4233,13 @@ void setup() { EEPROM.get(EEPROM_ADDR_BOOST_DAMPF_ACTIVE, boostDampfActive); EEPROM.get(EEPROM_ADDR_PREVENTHEAT_WASSER, preventHeatAboveSetpointWasser); EEPROM.get(EEPROM_ADDR_PREVENTHEAT_DAMPF, preventHeatAboveSetpointDampf); + uint8_t storedFfBoostPercent = defaultFfBoostPercentWasser; + EEPROM.get(EEPROM_ADDR_FF_BOOST_PERCENT_WASSER, storedFfBoostPercent); + if (storedFfBoostPercent > 100) storedFfBoostPercent = defaultFfBoostPercentWasser; // u. a. unprogrammierte 0xFF + ffBoostPercentWasser = storedFfBoostPercent; + uint8_t storedFfIFreeze = 0; + EEPROM.get(EEPROM_ADDR_FF_IFREEZE_WASSER, storedFfIFreeze); + ffIFreezeWasser = (storedFfIFreeze == 1); EEPROM.get(EEPROM_ADDR_WINDOWSIZE_WASSER, windowSizeWasser); EEPROM.get(EEPROM_ADDR_WINDOWSIZE_DAMPF, windowSizeDampf); uint8_t storedOffsetCompensationEnabled = 0; @@ -4504,7 +4566,7 @@ void setup() { } pidWasser.SetOutputLimits(0, windowSizeWasser); pidDampf.SetOutputLimits(0, windowSizeDampf); - pidWasser.SetTunings(KpWasser, KiWasser, KdWasser); + applyPidWasserTunings(); pidDampf.SetTunings(KpDampf, KiDampf, KdDampf); pidWasser.SetMode(AUTOMATIC); pidDampf.SetMode(AUTOMATIC); @@ -4866,6 +4928,11 @@ void loop() { } yield(); + // Feed-Forward: I-Anteil der Wasser-PID während des Bezugs einfrieren/freigeben. + // Zentral hier verwaltet (statt an Start-/Stopp-Stellen), damit der Zustand auch + // nach Software-Shots, Einstellungsänderungen mid-shot etc. immer konsistent ist. + setPidWasserIntegralFrozen(ffIFreezeWasser && (shotActive || shotSoftwareActive) && !wartungsModusAktiv); + // --- Angepasste Sicherheitsueberpruefung Wasser --- if (InputWasser < 0.0 || isnan(InputWasser)) { wasserSensorError = true; @@ -4964,7 +5031,8 @@ void loop() { // Wasser SSR if (wasserSensorError || wasserSafetyShutdown || standbyModeActive || displayOtaActive) { digitalWrite(SSR_WASSER_PIN, LOW); } else { if (now - windowStartTimeWasser > windowSizeWasser) { windowStartTimeWasser = now; } - bool pidWantsHeat = (OutputWasser > 0) && (now < (windowStartTimeWasser + (unsigned long)OutputWasser)); + unsigned long effOutWasser = effectiveOutputWasserMs(); // PID-Output + Feed-Forward-Boost bei Bezug + bool pidWantsHeat = (effOutWasser > 0) && (now < (windowStartTimeWasser + effOutWasser)); bool preventOverheatActive = (preventHeatAboveSetpointWasser && (InputWasser > SetpointWasser)) || (ecoModeAktiv && (InputWasser > SetpointWasser)); if (pidWantsHeat && !preventOverheatActive) { digitalWrite(SSR_WASSER_PIN, HIGH); } else { digitalWrite(SSR_WASSER_PIN, LOW); } } @@ -6406,7 +6474,7 @@ void handleAutoTune(AsyncWebServerRequest *request) { KdWasser = newKd; // WICHTIG: PID-Tunings sofort anwenden - pidWasser.SetTunings(KpWasser, KiWasser, KdWasser); + applyPidWasserTunings(); // Neue Werte im EEPROM speichern EEPROM.put(EEPROM_ADDR_KP_WASSER, KpWasser); @@ -8717,8 +8785,32 @@ static const char rootChunk_PIDWasser_Window[] PROGMEM = R"rawliteral('> +

Feed-Forward bei Bezug (Wasser)

+

+ Während des Bezugs strömt kaltes Wasser in den Kessel. Der Boost schaltet der Heizung
+ sofort zusätzliche Leistung auf, statt auf den Temperaturabfall zu warten (Feed-Forward).
+ Richtwert: benötigte Leistung [W] = Flow [g/s] · 4,2 · Temperaturhub [K] — daraus Prozent
+ der Heizleistung bestimmen. 0 % = deaktiviert. Wirkt nur während eines Bezugs,
+ nicht bei Spülen/Wartung. „Heizen oberhalb Setpoint verhindern“ hat weiterhin Vorrang. +

+ + +
+ I-Anteil während Bezug einfrieren: + +
+ Verhindert das Aufladen des I-Anteils während des Bezugs (kein Überschwingen nach dem Shot) +)rawliteral"; + // --- PID Dampf Sektion --- -static const char rootChunk_PIDDampf_Start[] PROGMEM = R"rawliteral('> +static const char rootChunk_PIDDampf_Start[] PROGMEM = R"rawliteral(

PID-Parameter: Dampf / Thermoblock