diff --git a/Changelog.txt b/Changelog.txt index c177b4c..0c9dd75 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,3 +1,15 @@ +Version 5.1.1: +- 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 + Profil-Auswahl sowie auf der Profil-Seite (aktives Profil zusätzlich mit grünem Punkt in der Liste). +- „Geändert"-Erkennung: Werden nach dem Laden profil-relevante Einstellungen geändert (Solltemperaturen, + PID-Werte inkl. AutoTune-Ergebnis und AutoTune-Parameter, Boost, Eco, Dampfverzögerung, Fast-Heat-Up, + Brew-Control, Piezo), erscheint hinter dem Profilnamen „✱ (geändert)". Reine Anzeige-/Systemeinstellungen + (Anzeigemodus, Aufheizzeit, Licht, FlowGuard, Waage/Sensoren) lösen das bewusst nicht aus. +- Profil löschen setzt die Anzeige zurück, wenn das gelöschte Profil das aktive war; Werksreset ebenso. +- Neue additive State-Felder "profile" und "profDirty" im UART-JSON für das Touch-Display (Protokoll bleibt + kompatibel, ältere Display-Firmware ignoriert die Felder) - Grundlage für die Profil-Schnellwahl am Display. + Version 5.1.0: - Display-Auswahl in die Web-UI (/Sensoren) verlegt: Ohne Display / OLED (SH1106, I2C) / UART-Touch-Display. Der Compile-Schalter ENABLE_DISPLAY entfällt, die Einstellung wird im EEPROM gespeichert und ohne Neustart diff --git a/Dual-PID.ino b/Dual-PID.ino index 3df341b..ebe727a 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.0"; +String version = "5.1.1"; String versionHersteller = "Thomas Müller"; String versionHerstellerMail = "thomas@mueller.black"; @@ -942,7 +942,9 @@ const int EEPROM_ADDR_BACKLIGHT_ACTIVE_PERCENT = 908; // 1 Byte (uint8_t) const int EEPROM_ADDR_BACKLIGHT_STANDBY_CLOCK_PERCENT = 909; // 1 Byte (uint8_t) -> Ende 909 const int EEPROM_ADDR_POWERON_STANDBY = 910; // 1 Byte (bool) -> Ende 910 const int EEPROM_ADDR_DISPLAY_TYPE = 911; // 1 Byte (uint8_t) -> Ende 911 -// Naechste freie Adresse: 912 (Innerhalb EEPROM_SIZE=1024) +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) /************************************************************************************ * Eco-Mode Variablen @@ -1312,6 +1314,45 @@ char hostname[33] = "Dual-PID"; // Max 32 Zeichen + Nullterminator String profileStatusMessage = ""; // für Nachrichten auf der Profil-Seite String standbyTimerStatusMessage = ""; // fuer Nachrichten auf der Timer-Seite +// Zuletzt geladenes/gespeichertes Profil (leer = keines). Wird im EEPROM gehalten, +// damit die Anzeige einen Neustart überlebt. activeProfileDirty markiert, dass nach +// dem Laden profil-relevante Einstellungen geändert wurden (Anzeige "geändert"). +String activeProfileName = ""; +bool activeProfileDirty = false; + +// Aktives Profil + Dirty-Flag ins EEPROM schreiben (char[31], nullterminiert) +void persistActiveProfile() { + char nameBuf[31]; + memset(nameBuf, 0, sizeof(nameBuf)); + strncpy(nameBuf, activeProfileName.c_str(), sizeof(nameBuf) - 1); + for (size_t i = 0; i < sizeof(nameBuf); i++) { + EEPROM.write(EEPROM_ADDR_ACTIVE_PROFILE_NAME + i, (uint8_t)nameBuf[i]); + } + EEPROM.write(EEPROM_ADDR_ACTIVE_PROFILE_DIRTY, activeProfileDirty ? 1 : 0); + EEPROM.commit(); +} + +void setActiveProfile(const String& name) { + activeProfileName = name; + activeProfileDirty = false; + persistActiveProfile(); +} + +void clearActiveProfile() { + activeProfileName = ""; + activeProfileDirty = false; + persistActiveProfile(); +} + +// Nach einer profil-relevanten Einstellungsänderung aufrufen: markiert das aktive +// Profil als "geändert". Ohne aktives Profil oder bei bereits gesetztem Flag ein No-Op. +void markProfileDirty() { + if (activeProfileName.length() == 0 || activeProfileDirty) return; + activeProfileDirty = true; + EEPROM.write(EEPROM_ADDR_ACTIVE_PROFILE_DIRTY, 1); + EEPROM.commit(); +} + /************************************************************************************ * WiFi Signal Bitmap (Display-Anzeige) ************************************************************************************/ @@ -2925,6 +2966,9 @@ void resetToDefaults() { // Alle Änderungen ins EEPROM schreiben EEPROM.commit(); + // Werksreset -> kein Profil mehr aktiv + clearActiveProfile(); + // WICHTIG: Globale Variablen auch auf Default setzen SetpointWasser = defaultSetpointWasser; SetpointDampf = defaultSetpointDampf; OffsetWasser = defaultOffsetWasser; OffsetDampf = defaultOffsetDampf; @@ -4264,6 +4308,28 @@ void setup() { storedDisplayType = DEFAULT_DISPLAY_TYPE; // u. a. unprogrammierte 0xFF } displayType = storedDisplayType; + // Zuletzt geladenes Profil aus dem EEPROM holen. Unprogrammierter Bereich (0xFF) + // oder nicht druckbare Zeichen -> kein aktives Profil. + { + char nameBuf[31]; + for (size_t i = 0; i < sizeof(nameBuf); i++) { + nameBuf[i] = (char)EEPROM.read(EEPROM_ADDR_ACTIVE_PROFILE_NAME + i); + } + nameBuf[sizeof(nameBuf) - 1] = '\0'; + bool valid = true; + for (size_t i = 0; nameBuf[i] != '\0'; i++) { + // Steuerzeichen oder 0xFF (unprogrammiertes EEPROM) -> ungueltig. + // Bytes >= 0x80 bleiben erlaubt (UTF-8-Umlaute im Anzeigenamen). + if ((uint8_t)nameBuf[i] < 0x20 || (uint8_t)nameBuf[i] == 0xFF) { valid = false; break; } + } + if (valid && nameBuf[0] != '\0') { + activeProfileName = String(nameBuf); + activeProfileDirty = (EEPROM.read(EEPROM_ADDR_ACTIVE_PROFILE_DIRTY) == 1); + } else { + activeProfileName = ""; + activeProfileDirty = false; + } + } uint8_t storedScaleEnabled = 0; uint8_t storedScaleType = defaultScaleType; EEPROM.get(EEPROM_ADDR_SCALE_ENABLED, storedScaleEnabled); @@ -6353,6 +6419,7 @@ void handleAutoTune(AsyncWebServerRequest *request) { autoTuneWasserResultKi = (float)KiWasser; autoTuneWasserResultKd = (float)KdWasser; autoTuneWasserStatus = (peaks > 9) ? AT_FAILSAFE_PEAKS : AT_SUCCESS; + markProfileDirty(); // PID-Werte weichen jetzt vom geladenen Profil ab } // Serial.println("AutoTune Wasser erfolgreich abgeschlossen."); @@ -6411,6 +6478,7 @@ void handleAutoTune(AsyncWebServerRequest *request) { autoTuneDampfResultKi = (float)KiDampf; autoTuneDampfResultKd = (float)KdDampf; autoTuneDampfStatus = (peaks > 9) ? AT_FAILSAFE_PEAKS : AT_SUCCESS; + markProfileDirty(); // PID-Werte weichen jetzt vom geladenen Profil ab } // Serial.println("AutoTune Dampf erfolgreich abgeschlossen."); @@ -6707,7 +6775,6 @@ bool applyBrewControlUpdate(const BrewControlUpdate& update) { if (update.hasFlowGuardEnabled && update.flowGuardEnabled != flowGuardEnabled) { flowGuardEnabled = update.flowGuardEnabled; EEPROM.put(EEPROM_ADDR_FLOWGUARD_ENABLED, flowGuardEnabled); - changed = true; flowGuardConfigChanged = true; } if (update.hasFlowGuardMinSecs) { @@ -6715,7 +6782,6 @@ bool applyBrewControlUpdate(const BrewControlUpdate& update) { if (newFlowGuardMinSecs >= FLOW_GUARD_MIN_SECONDS_MIN && newFlowGuardMinSecs <= FLOW_GUARD_MIN_SECONDS_MAX && fabs(newFlowGuardMinSecs - flowGuardMinBrewSeconds) > 0.01f) { flowGuardMinBrewSeconds = newFlowGuardMinSecs; EEPROM.put(EEPROM_ADDR_FLOWGUARD_MIN_SECONDS, flowGuardMinBrewSeconds); - changed = true; flowGuardConfigChanged = true; } } @@ -6724,7 +6790,6 @@ bool applyBrewControlUpdate(const BrewControlUpdate& update) { if (newPulseMs >= FLOW_GUARD_PULSE_PERIOD_MIN_MS && newPulseMs <= FLOW_GUARD_PULSE_PERIOD_MAX_MS && newPulseMs != flowGuardPulsePeriodMs) { flowGuardPulsePeriodMs = newPulseMs; EEPROM.put(EEPROM_ADDR_FLOWGUARD_PULSE_PERIOD_MS, flowGuardPulsePeriodMs); - changed = true; flowGuardConfigChanged = true; } } @@ -6733,7 +6798,6 @@ bool applyBrewControlUpdate(const BrewControlUpdate& update) { if (newFlowGuardMinDuty >= FLOW_GUARD_MIN_DUTY_MIN && newFlowGuardMinDuty <= FLOW_GUARD_MIN_DUTY_MAX && fabs(newFlowGuardMinDuty - flowGuardMinDutyPercent) > 0.01f) { flowGuardMinDutyPercent = newFlowGuardMinDuty; EEPROM.put(EEPROM_ADDR_FLOWGUARD_MIN_DUTY, flowGuardMinDutyPercent); - changed = true; flowGuardConfigChanged = true; } } @@ -6778,10 +6842,16 @@ bool applyBrewControlUpdate(const BrewControlUpdate& update) { } } + // "changed" umfasst hier nur noch profil-relevante Felder (BBT/BBW/PI/SBT); + // FlowGuard ist kein Bestandteil von TemperatureProfile und laeuft ueber + // flowGuardConfigChanged separat -> markProfileDirty() bleibt praezise. if (changed) { + markProfileDirty(); + } + if (changed || flowGuardConfigChanged) { EEPROM.commit(); } - return changed; + return changed || flowGuardConfigChanged; } void handleSaveBrewControl(AsyncWebServerRequest *request) { @@ -8507,6 +8577,7 @@ void handleFastHeatUpSettings(AsyncWebServerRequest *request) { EEPROM.put(EEPROM_ADDR_FASTHEATUP_DATA, fastHeatUpAktiv); EEPROM.commit(); + markProfileDirty(); // fastHeatUpAktiv ist Profilfeld AsyncWebServerResponse *resp = request->beginResponse(303); resp->addHeader(F("Location"), F("/Fast-Heat-Up")); @@ -8999,6 +9070,7 @@ void handleSettingsUpdate(AsyncWebServerRequest *request) { // --- EEPROM speichern und PID aktualisieren --- if (valueChanged) { // Nur speichern, wenn sich mindestens ein Wert geändert hat EEPROM.commit(); + markProfileDirty(); // Setpoints/Offsets/PID/Boost/MaxTemp/Window sind Profilfelder } // PID-Parameter an die Regler übergeben @@ -9386,6 +9458,7 @@ void handleUpdateAutotuneSettings(AsyncWebServerRequest *request) { if (changed) { EEPROM.commit(); + markProfileDirty(); // AutoTune-Parameter sind Profilfelder } AsyncWebServerResponse *response = request->beginResponse(303); @@ -9733,6 +9806,8 @@ void handleEcoUpdate(AsyncWebServerRequest *request) { EEPROM.put(EEPROM_ADDR_BACKLIGHT_ACTIVE_PERCENT, backlightActivePercent); EEPROM.put(EEPROM_ADDR_BACKLIGHT_STANDBY_CLOCK_PERCENT, backlightStandbyClockPercent); EEPROM.commit(); + // Eco/Dampfverzoegerung/Wake-Verhalten sind Profilfelder -> Profil als geaendert markieren + markProfileDirty(); // Wenn Eco deaktiviert, Setpoints zurückladen if (ecoModeMinutes == 0 && !ecoSwitchActive) { @@ -9796,6 +9871,7 @@ void handleUpdatePiezoSettings(AsyncWebServerRequest *request) { if (changed) { EEPROM.commit(); + markProfileDirty(); // piezoEnabled ist Teil des Profils } AsyncWebServerResponse *resp = request->beginResponse(303); @@ -11271,6 +11347,8 @@ bool saveProfile(const String& profileName, const TemperatureProfile& profileDat if (bytesWritten == sizeof(TemperatureProfile)) { // Serial.println(" Profile saved successfully (Binary Format)."); + // Gespeicherter Ist-Zustand entspricht ab jetzt diesem Profil -> als aktiv merken + setActiveProfile(displayName); return true; } else { // Serial.printf(" ERROR: Failed to write complete profile data! Bytes written: %d, Expected: %d\n", bytesWritten, sizeof(TemperatureProfile)); @@ -11418,6 +11496,9 @@ void applyProfileSettings(const TemperatureProfile& profileData) { // Eco-Status zurücksetzen ecoModeAktiv = false; ecoModeActivatedTime = 0; // Serial.println(" Eco mode status reset."); + + // Erfolgreich angewendet -> als aktives Profil merken (setzt Dirty-Flag zurück) + setActiveProfile(normalizeProfileDisplayName(String(profileData.profileName))); } @@ -11480,6 +11561,11 @@ bool deleteProfile(const String& profileName) { if (LittleFS.exists(filePath)) { if (LittleFS.remove(filePath)) { // Serial.println(" Profile deleted successfully."); + // War das geloeschte Profil das aktive -> Anzeige zuruecksetzen + if (activeProfileName.length() > 0 && + sanitizeProfileName(activeProfileName) == sanitizedName) { + clearActiveProfile(); + } return true; } else { // Serial.println(" ERROR: Failed to delete profile file."); @@ -11745,6 +11831,19 @@ void handleProfilesPage(AsyncWebServerRequest *request) { profileStatusMessage = ""; // Nachricht entfernen, nachdem sie angezeigt wurde } + // Aktives Profil anzeigen (zuletzt geladen/gespeichert; Stern = danach geändert) + { + String activeDiv = "
Aktives Profil: "; + if (activeProfileName.length() > 0) { + activeDiv += htmlEscape(activeProfileName); + if (activeProfileDirty) { activeDiv += " ✱ (geändert)"; } + } else { + activeDiv += "—"; + } + activeDiv += "
"; + response->print(activeDiv); + } + // Profilliste generieren response->print(FPSTR(profilesPageListStart)); //