feat(S3): Feed-Forward-Boost bei Bezug für den Wasserkessel (5.2.0)
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
6371b22683
commit
4f840173a9
+131
-12
@@ -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 =
|
||||
"<a href='mailto:thomas@mueller.black' class='info-link'>thomas@mueller.black</a>";
|
||||
@@ -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('>
|
||||
<label for='windowSizeWasser'>Zeitfenster (ms):</label>
|
||||
<input type='number' step='1' id='windowSizeWasser' name='windowSizeWasser' value=')rawliteral"; // Step auf 1 geändert für ms, Endet vor Window Size Wert
|
||||
|
||||
// --- Feed-Forward bei Bezug (Wasser) Sektion ---
|
||||
static const char rootChunk_FFWasser_Start[] PROGMEM = R"rawliteral('>
|
||||
<br><br> <h3>Feed-Forward bei Bezug (Wasser)</h3>
|
||||
<p style="font-size:0.9em; color: #ccc; margin-bottom:15px;">
|
||||
Während des Bezugs strömt kaltes Wasser in den Kessel. Der Boost schaltet der Heizung<br>
|
||||
sofort zusätzliche Leistung auf, statt auf den Temperaturabfall zu warten (Feed-Forward).<br>
|
||||
Richtwert: benötigte Leistung [W] = Flow [g/s] · 4,2 · Temperaturhub [K] — daraus Prozent<br>
|
||||
der Heizleistung bestimmen. <b>0 % = deaktiviert.</b> Wirkt nur während eines Bezugs,<br>
|
||||
nicht bei Spülen/Wartung. „Heizen oberhalb Setpoint verhindern“ hat weiterhin Vorrang.
|
||||
</p>
|
||||
<label for='ffBoostWasser'>Boost bei Bezug (% Heizleistung):</label>
|
||||
<input type='number' step='1' min='0' max='100' id='ffBoostWasser' name='ffBoostWasser' value=')rawliteral"; // Endet vor Boost-Prozent-Wert
|
||||
static const char rootChunk_FFWasser_IFreeze_Start[] PROGMEM = R"rawliteral('>
|
||||
<div class="toggle-switch-container">
|
||||
<span class="toggle-switch-label-text">I-Anteil während Bezug einfrieren:</span>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="ffIFreezeWasser" name="ffIFreezeWasser" value="1")rawliteral"; // Endet VOR checked
|
||||
static const char rootChunk_FFWasser_IFreeze_End[] PROGMEM = R"rawliteral(>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<small class="toggle-description">Verhindert das Aufladen des I-Anteils während des Bezugs (kein Überschwingen nach dem Shot)</small>
|
||||
)rawliteral";
|
||||
|
||||
// --- PID Dampf Sektion ---
|
||||
static const char rootChunk_PIDDampf_Start[] PROGMEM = R"rawliteral('>
|
||||
static const char rootChunk_PIDDampf_Start[] PROGMEM = R"rawliteral(
|
||||
<br><br> <h3>PID-Parameter: Dampf / Thermoblock</h3>
|
||||
<label for='kpDampf'>Kp:</label>
|
||||
<input type='number' step='0.01' id='kpDampf' name='kpDampf' value=')rawliteral"; // Endet vor Kp Dampf Wert
|
||||
@@ -8842,6 +8934,14 @@ void handlePidSettings(AsyncWebServerRequest *request) {
|
||||
response->print(buffer);
|
||||
yield();
|
||||
|
||||
response->write(rootChunk_FFWasser_Start, strlen_P(rootChunk_FFWasser_Start));
|
||||
snprintf(buffer, sizeof(buffer), "%u", (unsigned int)ffBoostPercentWasser);
|
||||
response->print(buffer);
|
||||
response->write(rootChunk_FFWasser_IFreeze_Start, strlen_P(rootChunk_FFWasser_IFreeze_Start));
|
||||
if (ffIFreezeWasser) response->print(F(" checked"));
|
||||
response->write(rootChunk_FFWasser_IFreeze_End, strlen_P(rootChunk_FFWasser_IFreeze_End));
|
||||
yield();
|
||||
|
||||
response->write(rootChunk_PIDDampf_Start, strlen_P(rootChunk_PIDDampf_Start));
|
||||
snprintf(buffer, sizeof(buffer), "%.2f", KpDampf);
|
||||
response->print(buffer);
|
||||
@@ -9067,15 +9167,34 @@ void handleSettingsUpdate(AsyncWebServerRequest *request) {
|
||||
}
|
||||
|
||||
|
||||
// Feed-Forward bei Bezug (kein Profilfeld: maschinenabhängig, nicht rezeptabhängig)
|
||||
bool ffChanged = false;
|
||||
if (request->hasArg(F("ffBoostWasser"))) {
|
||||
long val = request->arg(F("ffBoostWasser")).toInt();
|
||||
if (val >= 0 && val <= 100 && (uint8_t)val != ffBoostPercentWasser) {
|
||||
ffBoostPercentWasser = (uint8_t)val;
|
||||
EEPROM.put(EEPROM_ADDR_FF_BOOST_PERCENT_WASSER, ffBoostPercentWasser);
|
||||
ffChanged = true;
|
||||
}
|
||||
}
|
||||
bool newFfIFreeze = request->hasArg(F("ffIFreezeWasser"));
|
||||
if (newFfIFreeze != ffIFreezeWasser) {
|
||||
ffIFreezeWasser = newFfIFreeze;
|
||||
EEPROM.put(EEPROM_ADDR_FF_IFREEZE_WASSER, (uint8_t)(ffIFreezeWasser ? 1 : 0));
|
||||
ffChanged = true;
|
||||
}
|
||||
|
||||
// --- EEPROM speichern und PID aktualisieren ---
|
||||
if (valueChanged) { // Nur speichern, wenn sich mindestens ein Wert geändert hat
|
||||
if (valueChanged || ffChanged) { // Nur speichern, wenn sich mindestens ein Wert geändert hat
|
||||
EEPROM.commit();
|
||||
}
|
||||
if (valueChanged) {
|
||||
markProfileDirty(); // Setpoints/Offsets/PID/Boost/MaxTemp/Window sind Profilfelder
|
||||
}
|
||||
|
||||
// PID-Parameter an die Regler übergeben
|
||||
pidDampf.SetTunings(KpDampf, KiDampf, KdDampf);
|
||||
pidWasser.SetTunings(KpWasser, KiWasser, KdWasser);
|
||||
applyPidWasserTunings();
|
||||
|
||||
// Zurück zur Hauptseite (bereits vorhanden)
|
||||
AsyncWebServerResponse *resp = request->beginResponse(303);
|
||||
@@ -11489,7 +11608,7 @@ void applyProfileSettings(const TemperatureProfile& profileData) {
|
||||
EEPROM.commit();
|
||||
|
||||
// --- PID-Regler sofort aktualisieren ---
|
||||
pidWasser.SetTunings(KpWasser, KiWasser, KdWasser); pidDampf.SetTunings(KpDampf, KiDampf, KdDampf);
|
||||
applyPidWasserTunings(); pidDampf.SetTunings(KpDampf, KiDampf, KdDampf);
|
||||
pidWasser.SetOutputLimits(0, windowSizeWasser); pidDampf.SetOutputLimits(0, windowSizeDampf);
|
||||
// Serial.println(" PID controllers reconfigured with loaded settings.");
|
||||
|
||||
@@ -14865,7 +14984,7 @@ bool executeDashboardAction(const String& action, const String& value,
|
||||
}
|
||||
}
|
||||
if (pidNeedsUpdate) {
|
||||
pidWasser.SetTunings(KpWasser, KiWasser, KdWasser);
|
||||
applyPidWasserTunings();
|
||||
pidDampf.SetTunings(KpDampf, KiDampf, KdDampf);
|
||||
// Serial.println("PID-Parameter nach Dashboard-Aktion aktualisiert.");
|
||||
}
|
||||
@@ -15558,7 +15677,7 @@ static void buildTouchUartStateJson(char *buffer, size_t bufferSize) {
|
||||
offset += snprintf(buffer + offset, bufferSize - offset, "\"xSwitchLongAction\":%u,", (unsigned int)xSwitchLongAction);
|
||||
|
||||
// --- v2: Heizleistung (Duty in %) aus PID-Output / Fenstergroesse ---
|
||||
double dutyW = (windowSizeWasser > 0) ? (OutputWasser / (double)windowSizeWasser) * 100.0 : 0.0;
|
||||
double dutyW = (windowSizeWasser > 0) ? ((double)effectiveOutputWasserMs() / (double)windowSizeWasser) * 100.0 : 0.0; // inkl. Feed-Forward-Boost
|
||||
double dutyD = (windowSizeDampf > 0) ? (OutputDampf / (double)windowSizeDampf) * 100.0 : 0.0;
|
||||
dtostrf(dutyW, 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"dutyW\":%s,", floatBuf);
|
||||
dtostrf(dutyD, 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"dutyD\":%s,", floatBuf);
|
||||
@@ -16400,7 +16519,7 @@ static bool touchUartHandleCommandLine(const String& line) {
|
||||
}
|
||||
EEPROM.commit();
|
||||
markProfileDirty(); // PID-Werte sind Profilfelder
|
||||
if (changedW) { pidWasser.SetTunings(KpWasser, KiWasser, KdWasser); }
|
||||
if (changedW) { applyPidWasserTunings(); }
|
||||
if (changedD) { pidDampf.SetTunings(KpDampf, KiDampf, KdDampf); }
|
||||
touchUartSendAck(id, true, "PID gespeichert");
|
||||
touchUartSendState();
|
||||
|
||||
Reference in New Issue
Block a user