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('>
+
+ 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.
+