Compare commits

...
Author SHA1 Message Date
raw-designsandClaude Fable 5 4f840173a9 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>
2026-07-18 04:09:13 +02:00
thomas 6371b22683 Merge pull request 'fix(S3): „Aktives Profil"-Anzeige auf der Profil-Seite ins Karten-Layout eingefügt' (#9) from fix/profilseite-aktiv-anzeige-layout into main 2026-07-14 00:15:30 +02:00
raw-designsandClaude Fable 5 de2d0635b2 fix(S3): „Aktives Profil"-Anzeige auf der Profil-Seite ins Karten-Layout eingefügt
Die Zeile stand linksbündig ohne max-width außerhalb der zentrierten
Seitenstruktur. Jetzt im status-message-Stil der Seite (zentriert,
max. 600 px, dezenter neutraler Rahmen), Profilname fett. Version 5.1.2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 00:14:53 +02:00
thomas 801a659b0c Merge pull request 'feat(P4): Profil-Schnellwahl im Header + Anzeige des aktiven Profils' (#8) from feat/profil-schnellwahl-p4 into main 2026-07-13 20:55:47 +02:00
2 changed files with 156 additions and 15 deletions
+19
View File
@@ -1,3 +1,22 @@
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 (0100 % 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,
dezenter Rahmen) und fügt sich damit in das bestehende Layout ein; Profilname zusätzlich fett.
Version 5.1.1: Version 5.1.1:
- Aktives Profil wird jetzt geführt und angezeigt: Die Steuerung merkt sich das zuletzt geladene bzw. - Aktives Profil wird jetzt geführt und angezeigt: Die Steuerung merkt sich das zuletzt geladene bzw.
gespeicherte Profil dauerhaft im EEPROM (überlebt Neustarts). Web-UI: Anzeige auf dem Dashboard über der gespeicherte Profil dauerhaft im EEPROM (überlebt Neustarts). Web-UI: Anzeige auf dem Dashboard über der
+137 -15
View File
@@ -369,7 +369,7 @@ Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire);
* Firmware-Informationen * Firmware-Informationen
************************************************************************************/ ************************************************************************************/
String version = "5.1.1"; String version = "5.2.0";
String versionHersteller = "Thomas M&uuml;ller"; String versionHersteller = "Thomas M&uuml;ller";
String versionHerstellerMail = String versionHerstellerMail =
"<a href='mailto:thomas@mueller.black' class='info-link'>thomas@mueller.black</a>"; "<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 pidDampf(&InputDampf, &OutputDampf, &SetpointDampf, KpDampf, KiDampf, KdDampf, DIRECT);
PID pidWasser(&InputWasser, &OutputWasser, &SetpointWasser, KpWasser, KiWasser, KdWasser, 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 // Variablen für Sicherheitsabschaltung
double maxTempWasser = 110.0; // Standardwert, wie bisher hardcoded double maxTempWasser = 110.0; // Standardwert, wie bisher hardcoded
double maxTempDampf = 180.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_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_NAME = 912; // 31 Bytes (char[31], nullterminiert) -> Ende 942
const int EEPROM_ADDR_ACTIVE_PROFILE_DIRTY = 943; // 1 Byte (bool) -> Ende 943 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 * Eco-Mode Variablen
@@ -2871,6 +2884,8 @@ void resetToDefaults() {
EEPROM.put(EEPROM_ADDR_BOOST_DAMPF_ACTIVE, defaultBoostDampfActive); EEPROM.put(EEPROM_ADDR_BOOST_DAMPF_ACTIVE, defaultBoostDampfActive);
EEPROM.put(EEPROM_ADDR_PREVENTHEAT_WASSER, defaultPreventHeatAboveSetpointWasser); EEPROM.put(EEPROM_ADDR_PREVENTHEAT_WASSER, defaultPreventHeatAboveSetpointWasser);
EEPROM.put(EEPROM_ADDR_PREVENTHEAT_DAMPF, defaultPreventHeatAboveSetpointDampf); 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_WASSER, maxTempWasser); // Sollte ggf. eigene Defaults haben
EEPROM.put(EEPROM_ADDR_MAX_TEMP_DAMPF, maxTempDampf); // 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); 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. * Stoppt den Brühvorgang (Pumpe/Ventil) und verarbeitet Shot-Daten.
* Wird von updateShotTimer() oder aus loop() (BrewByTime/Weight) aufgerufen. * 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_BOOST_DAMPF_ACTIVE, boostDampfActive);
EEPROM.get(EEPROM_ADDR_PREVENTHEAT_WASSER, preventHeatAboveSetpointWasser); EEPROM.get(EEPROM_ADDR_PREVENTHEAT_WASSER, preventHeatAboveSetpointWasser);
EEPROM.get(EEPROM_ADDR_PREVENTHEAT_DAMPF, preventHeatAboveSetpointDampf); 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_WASSER, windowSizeWasser);
EEPROM.get(EEPROM_ADDR_WINDOWSIZE_DAMPF, windowSizeDampf); EEPROM.get(EEPROM_ADDR_WINDOWSIZE_DAMPF, windowSizeDampf);
uint8_t storedOffsetCompensationEnabled = 0; uint8_t storedOffsetCompensationEnabled = 0;
@@ -4504,7 +4566,7 @@ void setup() {
} }
pidWasser.SetOutputLimits(0, windowSizeWasser); pidWasser.SetOutputLimits(0, windowSizeWasser);
pidDampf.SetOutputLimits(0, windowSizeDampf); pidDampf.SetOutputLimits(0, windowSizeDampf);
pidWasser.SetTunings(KpWasser, KiWasser, KdWasser); applyPidWasserTunings();
pidDampf.SetTunings(KpDampf, KiDampf, KdDampf); pidDampf.SetTunings(KpDampf, KiDampf, KdDampf);
pidWasser.SetMode(AUTOMATIC); pidWasser.SetMode(AUTOMATIC);
pidDampf.SetMode(AUTOMATIC); pidDampf.SetMode(AUTOMATIC);
@@ -4866,6 +4928,11 @@ void loop() {
} }
yield(); 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 --- // --- Angepasste Sicherheitsueberpruefung Wasser ---
if (InputWasser < 0.0 || isnan(InputWasser)) { if (InputWasser < 0.0 || isnan(InputWasser)) {
wasserSensorError = true; wasserSensorError = true;
@@ -4964,7 +5031,8 @@ void loop() {
// Wasser SSR // Wasser SSR
if (wasserSensorError || wasserSafetyShutdown || standbyModeActive || displayOtaActive) { digitalWrite(SSR_WASSER_PIN, LOW); } else { if (wasserSensorError || wasserSafetyShutdown || standbyModeActive || displayOtaActive) { digitalWrite(SSR_WASSER_PIN, LOW); } else {
if (now - windowStartTimeWasser > windowSizeWasser) { windowStartTimeWasser = now; } 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)); bool preventOverheatActive = (preventHeatAboveSetpointWasser && (InputWasser > SetpointWasser)) || (ecoModeAktiv && (InputWasser > SetpointWasser));
if (pidWantsHeat && !preventOverheatActive) { digitalWrite(SSR_WASSER_PIN, HIGH); } else { digitalWrite(SSR_WASSER_PIN, LOW); } if (pidWantsHeat && !preventOverheatActive) { digitalWrite(SSR_WASSER_PIN, HIGH); } else { digitalWrite(SSR_WASSER_PIN, LOW); }
} }
@@ -6406,7 +6474,7 @@ void handleAutoTune(AsyncWebServerRequest *request) {
KdWasser = newKd; KdWasser = newKd;
// WICHTIG: PID-Tunings sofort anwenden // WICHTIG: PID-Tunings sofort anwenden
pidWasser.SetTunings(KpWasser, KiWasser, KdWasser); applyPidWasserTunings();
// Neue Werte im EEPROM speichern // Neue Werte im EEPROM speichern
EEPROM.put(EEPROM_ADDR_KP_WASSER, KpWasser); 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> <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 <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&auml;hrend des Bezugs str&ouml;mt kaltes Wasser in den Kessel. Der Boost schaltet der Heizung<br>
sofort zus&auml;tzliche Leistung auf, statt auf den Temperaturabfall zu warten (Feed-Forward).<br>
Richtwert: ben&ouml;tigte Leistung [W] = Flow [g/s] &middot; 4,2 &middot; Temperaturhub [K] &mdash; daraus Prozent<br>
der Heizleistung bestimmen. <b>0&nbsp;% = deaktiviert.</b> Wirkt nur w&auml;hrend eines Bezugs,<br>
nicht bei Sp&uuml;len/Wartung. &bdquo;Heizen oberhalb Setpoint verhindern&ldquo; 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&auml;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&auml;hrend des Bezugs (kein &Uuml;berschwingen nach dem Shot)</small>
)rawliteral";
// --- PID Dampf Sektion --- // --- 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> <br><br> <h3>PID-Parameter: Dampf / Thermoblock</h3>
<label for='kpDampf'>Kp:</label> <label for='kpDampf'>Kp:</label>
<input type='number' step='0.01' id='kpDampf' name='kpDampf' value=')rawliteral"; // Endet vor Kp Dampf Wert <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); response->print(buffer);
yield(); 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)); response->write(rootChunk_PIDDampf_Start, strlen_P(rootChunk_PIDDampf_Start));
snprintf(buffer, sizeof(buffer), "%.2f", KpDampf); snprintf(buffer, sizeof(buffer), "%.2f", KpDampf);
response->print(buffer); 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 --- // --- 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(); EEPROM.commit();
}
if (valueChanged) {
markProfileDirty(); // Setpoints/Offsets/PID/Boost/MaxTemp/Window sind Profilfelder markProfileDirty(); // Setpoints/Offsets/PID/Boost/MaxTemp/Window sind Profilfelder
} }
// PID-Parameter an die Regler übergeben // PID-Parameter an die Regler übergeben
pidDampf.SetTunings(KpDampf, KiDampf, KdDampf); pidDampf.SetTunings(KpDampf, KiDampf, KdDampf);
pidWasser.SetTunings(KpWasser, KiWasser, KdWasser); applyPidWasserTunings();
// Zurück zur Hauptseite (bereits vorhanden) // Zurück zur Hauptseite (bereits vorhanden)
AsyncWebServerResponse *resp = request->beginResponse(303); AsyncWebServerResponse *resp = request->beginResponse(303);
@@ -11489,7 +11608,7 @@ void applyProfileSettings(const TemperatureProfile& profileData) {
EEPROM.commit(); EEPROM.commit();
// --- PID-Regler sofort aktualisieren --- // --- 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); pidWasser.SetOutputLimits(0, windowSizeWasser); pidDampf.SetOutputLimits(0, windowSizeDampf);
// Serial.println(" PID controllers reconfigured with loaded settings."); // Serial.println(" PID controllers reconfigured with loaded settings.");
@@ -11831,14 +11950,17 @@ void handleProfilesPage(AsyncWebServerRequest *request) {
profileStatusMessage = ""; // Nachricht entfernen, nachdem sie angezeigt wurde profileStatusMessage = ""; // Nachricht entfernen, nachdem sie angezeigt wurde
} }
// Aktives Profil anzeigen (zuletzt geladen/gespeichert; Stern = danach geändert) // Aktives Profil anzeigen (zuletzt geladen/gespeichert; Stern = danach geändert).
// Im status-message-Stil der Seite: zentriert, max-width 600px - fuegt sich damit in
// das Karten-Layout ein statt linksbuendig aus der Reihe zu tanzen.
{ {
String activeDiv = "<div style=\"margin-bottom:12px;opacity:0.85;\">Aktives Profil: "; String activeDiv = "<div class=\"status-message\" style=\"background-color: rgba(255,255,255,0.06); border-color: rgba(255,255,255,0.15);\">Aktives Profil: <b>";
if (activeProfileName.length() > 0) { if (activeProfileName.length() > 0) {
activeDiv += htmlEscape(activeProfileName); activeDiv += htmlEscape(activeProfileName);
activeDiv += "</b>";
if (activeProfileDirty) { activeDiv += " &#10033; (ge&auml;ndert)"; } if (activeProfileDirty) { activeDiv += " &#10033; (ge&auml;ndert)"; }
} else { } else {
activeDiv += "&mdash;"; activeDiv += "&mdash;</b>";
} }
activeDiv += "</div>"; activeDiv += "</div>";
response->print(activeDiv); response->print(activeDiv);
@@ -14862,7 +14984,7 @@ bool executeDashboardAction(const String& action, const String& value,
} }
} }
if (pidNeedsUpdate) { if (pidNeedsUpdate) {
pidWasser.SetTunings(KpWasser, KiWasser, KdWasser); applyPidWasserTunings();
pidDampf.SetTunings(KpDampf, KiDampf, KdDampf); pidDampf.SetTunings(KpDampf, KiDampf, KdDampf);
// Serial.println("PID-Parameter nach Dashboard-Aktion aktualisiert."); // Serial.println("PID-Parameter nach Dashboard-Aktion aktualisiert.");
} }
@@ -15555,7 +15677,7 @@ static void buildTouchUartStateJson(char *buffer, size_t bufferSize) {
offset += snprintf(buffer + offset, bufferSize - offset, "\"xSwitchLongAction\":%u,", (unsigned int)xSwitchLongAction); offset += snprintf(buffer + offset, bufferSize - offset, "\"xSwitchLongAction\":%u,", (unsigned int)xSwitchLongAction);
// --- v2: Heizleistung (Duty in %) aus PID-Output / Fenstergroesse --- // --- 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; 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(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); dtostrf(dutyD, 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"dutyD\":%s,", floatBuf);
@@ -16397,7 +16519,7 @@ static bool touchUartHandleCommandLine(const String& line) {
} }
EEPROM.commit(); EEPROM.commit();
markProfileDirty(); // PID-Werte sind Profilfelder markProfileDirty(); // PID-Werte sind Profilfelder
if (changedW) { pidWasser.SetTunings(KpWasser, KiWasser, KdWasser); } if (changedW) { applyPidWasserTunings(); }
if (changedD) { pidDampf.SetTunings(KpDampf, KiDampf, KdDampf); } if (changedD) { pidDampf.SetTunings(KpDampf, KiDampf, KdDampf); }
touchUartSendAck(id, true, "PID gespeichert"); touchUartSendAck(id, true, "PID gespeichert");
touchUartSendState(); touchUartSendState();