diff --git a/Changelog.txt b/Changelog.txt index 913bcfa..1699cff 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,3 +1,19 @@ +Version 5.3.5: +- Cold Extraction lässt sich jetzt auch im Standby und bei noch warmem Kessel scharf schalten. + Bisher galt „Standby aktiv" und „Kessel zu warm" auch fürs Aktivieren - das war falsch herum + gedacht: Der Modus schaltet die Heizung AB, man kam aber erst an den Schalter, nachdem die + Maschine aufgeweckt war und bereits zu heizen begonnen hatte. Genau der Ablauf, den die + Funktion vermeiden soll. +- Scharfschalten und Beziehen sind jetzt zwei getrennte Bedingungen: + - Scharfschalten (neues State-Feld „cxArmable"): blockiert nur durch fehlende Freischaltung, + Wartungsmodus, Reinigungsassistent, PID-Tuning oder einen laufenden Bezug/Spülvorgang. + - Bezug (unverändert „cxAllowed"): zusätzlich kein Standby, Kessel unter der Freigabeschwelle, + gültiger Sensorwert. + Ist der Modus scharf, der Bezug aber noch gesperrt, sagen Web-UI und Display das jetzt + ausdrücklich („Modus aktiv, Bezug noch gesperrt: …"). +- Wird bei warmem Kessel aktiviert, geht die Wasserheizung sofort aus und der Kessel kühlt ab; + die Rückmeldung weist darauf hin, dass Bezüge bis zum Unterschreiten der Schwelle gesperrt sind. + Version 5.3.4: - Cold Extraction weist jetzt deutlich darauf hin, wenn keine Waage aktiv/verbunden ist. Ohne Waage greifen weder das Zielgewicht noch die Stillstands-Erkennung - der Bezug endet dann erst nach der diff --git a/Doku/JC-Display_UART-Protokoll.md b/Doku/JC-Display_UART-Protokoll.md index 27dea7b..2ef8f4b 100644 --- a/Doku/JC-Display_UART-Protokoll.md +++ b/Doku/JC-Display_UART-Protokoll.md @@ -323,8 +323,17 @@ Alle gültigen `action`-Strings aus `executeDashboardAction()` (Stand v4.8.1, Ze | `stopColdExtraction` | — | Modus beenden | | `toggleColdExtraction` | — | Modus umschalten | -Alle drei Aktionen werden abgewiesen (`ok:false` + `message`), solange die Bedingungen nicht -erfüllt sind — Hauptfall ist ein zu warmer Wasserkessel (`cxAllowed:false`). Der Modus ist +Für das Bedienelement zählt `cxArmable`, **nicht** `cxAllowed`: Der Modus schaltet die +Wasserheizung ab und muss deshalb auch im Standby und bei noch warmem Kessel scharf zu +stellen sein — sonst heizt die Maschine nach dem Aufwecken an, bevor man an den Schalter +kommt. `cxAllowed` gilt nur für den einzelnen Bezug; ist der Modus scharf und `cxAllowed` +false, sollte die UI „Modus aktiv, Bezug noch gesperrt" zeigen. + +Ein Aufwecken direkt in die kalte Extraktion sendet `startColdExtraction` **vor** +`deactivateStandby` — in dieser Reihenfolge, damit der Wasserkreis gar nicht erst anheizt. + +Die Aktionen werden abgewiesen (`ok:false` + `message`), solange die Bedingungen nicht +erfüllt sind. Der Modus ist **persistent** (EEPROM) und überlebt Standby und Neustart; er endet nur durch explizites Ausschalten, durch Entzug der Freischaltung auf `/Brew-Control` oder durch einen Werksreset. Das P4-UI sollte `cxActive` deshalb dauerhaft sichtbar spiegeln — sonst wundert sich der @@ -383,7 +392,8 @@ Aus `getTouchUartStatus()`. Für sprach-unabhängige UI-Logik/Icons auf dem P4 v | `cxEnabled` | bool | Funktion in der Web-UI freigeschaltet (sonst Bedienelement ausblenden) | | `cxActive` | bool | Modus scharf: Wasserkreis heizt nicht | | `cxShot` | bool | Der laufende Bezug ist eine kalte Extraktion | -| `cxAllowed` | bool | Freigabe **jetzt** (Wasserkessel kalt genug, kein Standby/Wartung/Tuning) | +| `cxAllowed` | bool | **Bezug** jetzt möglich (Wasserkessel kalt genug, kein Standby/Wartung/Tuning) | +| `cxArmable` | bool | **Modus umschaltbar** — auch im Standby und bei warmem Kessel (ab S3 5.3.5) | | `cxResting` | bool | Zwangspause des Pumpenschutzes läuft (Bezug läuft weiter) | | `cxMaxTemp` | float | Freigabeschwelle in °C (für eigene Hinweistexte am Display) | | `cxTarget` | float | Zielgewicht in g | diff --git a/Dual-PID.ino b/Dual-PID.ino index 058d94d..6eedfdb 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.3.4"; +String version = "5.3.5"; String versionHersteller = "Thomas Müller"; String versionHerstellerMail = "thomas@mueller.black"; @@ -4119,7 +4119,7 @@ bool setColdExtractionMode(bool enable, String& message) { } if (enable) { - String reason = coldExtractionBlockReason(); + String reason = coldExtractionArmBlockReason(); if (reason.length() > 0) { message = reason; return false; @@ -4132,6 +4132,13 @@ bool setColdExtractionMode(bool enable, String& message) { resetColdExtractionRuntime(); persistColdExtractionMode(); message = "Cold Extraction aktiviert - Wasserkreis heizt nicht (bleibt auch nach Standby/Neustart aktiv)."; + if (standbyModeActive) { + message += " Nach dem Aufwecken bleibt der Wasserkreis aus."; + } else if (!coldExtractionTemperatureOk()) { + // Scharfschalten bei warmem Kessel ist erlaubt (Heizung geht sofort aus), ein + // Bezug aber erst, wenn der Kessel unter die Schwelle gefallen ist. + message += " Der Kessel ist noch zu warm - er kuehlt jetzt ab, Bezuege sind bis dahin gesperrt."; + } if (!coldExtractionScaleUsable()) { // Ohne Waage faellt die Dosierung auf die maximale Bezugsdauer zurueck - das // muss der Benutzer wissen, sonst wundert er sich ueber das Ergebnis. @@ -4160,6 +4167,37 @@ bool coldExtractionShotAllowed() { return coldExtractionModeActive && coldExtractionConditionsOk(); } +/************************************************************************************ + * Scharfschalten vs. Beziehen - bewusst zwei verschiedene Bedingungen + ************************************************************************************ + * Der Modus schaltet die Wasserheizung AB. Das an Standby oder an die Kesseltemperatur + * zu koppeln waere falsch herum: Im Standby kaeme man gar nicht an den Schalter, und + * nach dem Aufwecken heizt die Maschine sofort los - man muesste erst aufheizen lassen, + * um danach auf das Abkuehlen zu warten. Genau der Fall, den die Funktion vermeiden soll. + * Deshalb: Scharfschalten ist im Standby und bei warmem Kessel erlaubt; ueber den + * einzelnen Bezug entscheidet weiterhin coldExtractionShotAllowed(). + ************************************************************************************/ +bool coldExtractionArmable() { + return coldExtractionEnabled && !wartungsModusAktiv && !cleaningAssistantActive && + !autoTuneWasserActive && !autoTuneDampfActive; +} + +String coldExtractionArmBlockReason() { + if (!coldExtractionEnabled) { + return "Cold Extraction ist nicht freigeschaltet."; + } + if (wartungsModusAktiv) { + return "Cold Extraction nicht moeglich: Wartungsmodus aktiv."; + } + if (cleaningAssistantActive) { + return "Cold Extraction nicht moeglich: Reinigungsassistent aktiv."; + } + if (autoTuneWasserActive || autoTuneDampfActive) { + return "Cold Extraction nicht moeglich: PID-Tuning laeuft."; + } + return ""; +} + // Pumpensteuerung des kalten Bezugs: Vorbenetzung ungepulst, danach Puls-Duty. // Zusaetzlich Pumpenschutz ueber die kumulierte Einschaltzeit (Zwangspause) - die Olab // hat zwar einen Thermoschutz, ein Auslösen mitten im Bezug wuerde aber den Shot @@ -15011,15 +15049,21 @@ static const char dashboardJavaScript[] PROGMEM = R"rawliteral( if (coldexRow) { coldexRow.style.display = (data.cxEnabled === true) ? '' : 'none'; } if (coldexToggleEl) { updateToggleState(coldexToggleEl, data.cxActive); - coldexToggleEl.disabled = (data.cxEnabled !== true) || (data.cxActive !== true && data.cxAllowed !== true); + // Umschalten haengt an cxArmable, NICHT an cxAllowed: Der Modus muss auch im + // Standby und bei noch warmem Kessel scharf zu stellen sein. + coldexToggleEl.disabled = (data.cxEnabled !== true) || (data.cxArmable !== true); } if (coldexHint) { let hintText = ''; - if (data.cxEnabled === true && data.cxAllowed !== true && data.cxActive !== true) { - if (typeof data.tempW === 'number' && typeof data.cxMaxTemp === 'number' && data.tempW >= data.cxMaxTemp) { - hintText = 'Wasserkessel zu warm (' + data.tempW.toFixed(1) + ' °C, benötigt < ' + data.cxMaxTemp.toFixed(1) + ' °C).'; + if (data.cxEnabled === true && data.cxAllowed !== true) { + // Gilt auch bei aktivem Modus: dann ist er scharf, aber der Bezug noch gesperrt + const prefix = (data.cxActive === true) ? 'Modus aktiv, Bezug noch gesperrt: ' : ''; + if (data.standbyActive === true) { + hintText = prefix + 'Standby aktiv - Bezug erst nach dem Aufwecken.'; + } else if (typeof data.tempW === 'number' && typeof data.cxMaxTemp === 'number' && data.tempW >= data.cxMaxTemp) { + hintText = prefix + 'Wasserkessel zu warm (' + data.tempW.toFixed(1) + ' °C, benötigt < ' + data.cxMaxTemp.toFixed(1) + ' °C).'; } else { - hintText = 'Cold Extraction derzeit nicht möglich.'; + hintText = prefix + 'Bezug derzeit nicht möglich.'; } } // Waagen-Hinweis gilt zusätzlich und unabhängig von der Temperatur-Freigabe @@ -16191,6 +16235,8 @@ void buildDashboardJson(char *buffer, size_t bufferSize) { offset += snprintf(buffer + offset, bufferSize - offset, "\"cxEnabled\":%s,", coldExtractionEnabled ? "true" : "false"); offset += snprintf(buffer + offset, bufferSize - offset, "\"cxActive\":%s,", coldExtractionModeActive ? "true" : "false"); offset += snprintf(buffer + offset, bufferSize - offset, "\"cxAllowed\":%s,", coldExtractionConditionsOk() ? "true" : "false"); + // cxArmable = Modus umschaltbar (auch im Standby und bei warmem Kessel); cxAllowed = Bezug moeglich + offset += snprintf(buffer + offset, bufferSize - offset, "\"cxArmable\":%s,", coldExtractionArmable() ? "true" : "false"); dtostrf(coldExtractionMaxTempC, 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"cxMaxTemp\":%s,", floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"scaleModeActive\":%s,", scaleModeActive ? "true" : "false"); dtostrf(brewByTimeTargetSeconds, 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"bbtSecs\":%s,", floatBuf); @@ -16517,6 +16563,8 @@ static void buildTouchUartStateJson(char *buffer, size_t bufferSize) { offset += snprintf(buffer + offset, bufferSize - offset, "\"cxActive\":%s,", coldExtractionModeActive ? "true" : "false"); offset += snprintf(buffer + offset, bufferSize - offset, "\"cxShot\":%s,", coldExtractionShotActive ? "true" : "false"); offset += snprintf(buffer + offset, bufferSize - offset, "\"cxAllowed\":%s,", coldExtractionConditionsOk() ? "true" : "false"); + // cxArmable = Modus umschaltbar (auch im Standby und bei warmem Kessel); cxAllowed = Bezug moeglich + offset += snprintf(buffer + offset, bufferSize - offset, "\"cxArmable\":%s,", coldExtractionArmable() ? "true" : "false"); offset += snprintf(buffer + offset, bufferSize - offset, "\"cxResting\":%s,", coldExtractionPumpResting ? "true" : "false"); dtostrf(coldExtractionMaxTempC, 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"cxMaxTemp\":%s,", floatBuf); dtostrf(coldExtractionTargetGrams, 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"cxTarget\":%s,", floatBuf); diff --git a/JC_Display_Firmware/Changelog.txt b/JC_Display_Firmware/Changelog.txt index d7c4d93..33aa0e8 100644 --- a/JC_Display_Firmware/Changelog.txt +++ b/JC_Display_Firmware/Changelog.txt @@ -1,3 +1,15 @@ +Version 1.1.3: +- Aufweck-Dialog hat einen dritten Knopf: „Aufwecken mit Cold Extraction" (braucht S3 ab 5.3.5). + Er stellt den Modus scharf und beendet erst danach den Standby - in dieser Reihenfolge, damit + der Wasserkreis gar nicht erst anheizt. Ohne diesen Weg musste man normal aufwecken (Maschine + heizt an) und den Modus danach umschalten. Der Knopf erscheint nur, wenn die Funktion + freigeschaltet und umschaltbar ist und der Modus nicht ohnehin schon läuft. +- Der Schalter „Cold Extraction" auf der Temperatur-Seite hängt jetzt am neuen State-Feld + cxArmable statt an cxAllowed: umschaltbar auch im Standby und bei warmem Kessel, gesperrt nur + während eines laufenden kalten Bezugs. Bei älterer S3-Firmware fällt das Feld auf cxEnabled + zurück, der Schalter bleibt also bedienbar. +- Erklärzeile im Standby: „Nach dem Aufwecken bleibt der Wasserkreis aus." + Version 1.1.2: - Cold Extraction: Fehlt eine nutzbare Waage, weist das Display jetzt dauerhaft darauf hin. Ohne Waage greifen weder Zielgewicht noch Stillstands-Erkennung, der Bezug endet erst nach der diff --git a/JC_Display_Firmware/config.h b/JC_Display_Firmware/config.h index 1ae7412..5a36f36 100644 --- a/JC_Display_Firmware/config.h +++ b/JC_Display_Firmware/config.h @@ -65,7 +65,7 @@ // ------------------------------------------------------------------------------------- // Firmware // ------------------------------------------------------------------------------------- -#define DISPLAY_FW_VERSION "1.1.2" // Firmware-Stand der P4-Display-Steuerung (Info-Seite) +#define DISPLAY_FW_VERSION "1.1.3" // Firmware-Stand der P4-Display-Steuerung (Info-Seite) // ------------------------------------------------------------------------------------- // Protokoll diff --git a/JC_Display_Firmware/machine_state.h b/JC_Display_Firmware/machine_state.h index ec10384..d6e10aa 100644 --- a/JC_Display_Firmware/machine_state.h +++ b/JC_Display_Firmware/machine_state.h @@ -72,7 +72,8 @@ struct MachineState { bool cxEnabled = false; // Funktion auf der S3 freigeschaltet (sonst Bedienelement ausblenden) bool cxActive = false; // Modus scharf: Wasserkreis heizt nicht (persistent, seit S3 5.3.1) bool cxShot = false; // der laufende Bezug ist eine kalte Extraktion - bool cxAllowed = false; // Freigabe jetzt (Wasserkessel kalt genug, kein Standby/Wartung/Tuning) + bool cxAllowed = false; // Bezug jetzt moeglich (Wasserkessel kalt genug, kein Standby/Wartung/Tuning) + bool cxArmable = false; // Modus umschaltbar - auch im Standby und bei warmem Kessel (seit S3 5.3.5) bool cxResting = false; // Zwangspause des Pumpenschutzes laeuft (Bezug laeuft weiter) float cxMaxTemp = 0; // Freigabeschwelle in C float cxTarget = 0; // Zielgewicht in g diff --git a/JC_Display_Firmware/protocol_client.cpp b/JC_Display_Firmware/protocol_client.cpp index ee58043..7bed323 100644 --- a/JC_Display_Firmware/protocol_client.cpp +++ b/JC_Display_Firmware/protocol_client.cpp @@ -225,6 +225,8 @@ void ProtocolClient::applyState(const String& json) { s.cxActive = doc["cxActive"] | false; s.cxShot = doc["cxShot"] | false; s.cxAllowed = doc["cxAllowed"] | false; + // Fehlt bei S3-FW < 5.3.5 -> auf cxEnabled zurueckfallen, sonst waere der Schalter tot + s.cxArmable = doc["cxArmable"] | (doc["cxEnabled"] | false); s.cxResting = doc["cxResting"] | false; s.cxMaxTemp = doc["cxMaxTemp"] | s.cxMaxTemp; s.cxTarget = doc["cxTarget"] | s.cxTarget; diff --git a/JC_Display_Firmware/ui.cpp b/JC_Display_Firmware/ui.cpp index 7af9225..77f0fc5 100644 --- a/JC_Display_Firmware/ui.cpp +++ b/JC_Display_Firmware/ui.cpp @@ -38,6 +38,7 @@ static lv_timer_t* g_toastTimer = nullptr; static lv_obj_t* g_standbyScreen = nullptr; // Vollbild-Overlay im Standby (Uhr) static lv_obj_t* g_standbyClock = nullptr; static lv_obj_t* g_wakeConfirm = nullptr; // Bestaetigungsdialog "Aus dem Standby aufwecken?" +static lv_obj_t* g_wakeColdBtn = nullptr; // "Aufwecken mit Cold Extraction" (nur wenn freigeschaltet) static bool g_wakeDialogOpen = false; // Dialog sichtbar -> Display hell schalten (lesbar) static bool g_standbyShown = false; // Standby-Uhr-Overlay zuletzt als sichtbar geschaltet (fuer Ein-/Ausblend-Uebergang) @@ -572,6 +573,17 @@ static void wake_yes_cb(lv_event_t*) { // Aufwecken -> hell lassen, S3 bee // das Display in der Uebergangsphase kurz auf Standby-Helligkeit abdunkeln. if (g_client) { g_client->sendAction("deactivateStandby"); showToast("Maschine wird aufgeweckt...", false); } } +// Aufwecken direkt in die kalte Extraktion. Reihenfolge ist wichtig: erst den Modus scharf +// stellen, dann den Standby beenden - sonst heizt der Wasserkreis in der Zwischenzeit an, +// und genau das soll die Funktion ja verhindern. +static void wake_cold_cb(lv_event_t*) { + wake_confirm_hide(); + if (g_client) { + g_client->sendAction("startColdExtraction"); + g_client->sendAction("deactivateStandby"); + showToast("Aufwecken mit Cold Extraction...", false); + } +} bool ui_wake_dialog_open() { return g_wakeDialogOpen; } // fuer die Helligkeitssteuerung in onState // ===================================================================================== @@ -3224,6 +3236,13 @@ void ui_init(ProtocolClient* client) { lv_obj_t* wakeYes = th_accent_button(wakeBtns, "Aufwecken"); lv_obj_set_width(wakeYes, 150); lv_obj_add_event_cb(wakeYes, wake_yes_cb, LV_EVENT_CLICKED, nullptr); + // Zweite Zeile: direkt in die kalte Extraktion aufwecken. Ohne diesen Weg muesste man + // erst normal aufwecken (Maschine heizt an) und den Modus danach umschalten. + // Nur sichtbar, wenn die S3 die Funktion freigeschaltet meldet (siehe ui_update). + g_wakeColdBtn = th_button(wakeCard, "Aufwecken mit Cold Extraction", COL_CARD2, COL_ACCENT); + lv_obj_set_width(g_wakeColdBtn, LV_PCT(100)); + lv_obj_add_event_cb(g_wakeColdBtn, wake_cold_cb, LV_EVENT_CLICKED, nullptr); + lv_obj_add_flag(g_wakeColdBtn, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(g_wakeConfirm, LV_OBJ_FLAG_HIDDEN); // OTA-Overlay (Vollbild, waehrend Firmware-Update) @@ -3449,6 +3468,12 @@ void ui_update(const MachineState& st) { // Ohne nutzbare Waage laeuft die kalte Extraktion rein zeitgesteuert (Zielgewicht und // Stillstands-Erkennung greifen nicht) - das wird dauerhaft mit angezeigt. const bool cxScaleOk = st.scaleEnabled && st.scaleConnected; + // "Aufwecken mit Cold Extraction" nur anbieten, wenn die Funktion freigeschaltet und + // umschaltbar ist und der Modus nicht ohnehin schon laeuft. + if (g_wakeColdBtn) { + if (st.cxEnabled && st.cxArmable && !st.cxActive) lv_obj_remove_flag(g_wakeColdBtn, LV_OBJ_FLAG_HIDDEN); + else lv_obj_add_flag(g_wakeColdBtn, LV_OBJ_FLAG_HIDDEN); + } if (st.cxNotice == 2) { status_set(ST_COLDEX, true, LV_SYMBOL_WARNING " Cold Extraction beendet: kein Zulauf", COL_WARN, lv_color_hex(0x000000)); @@ -3851,8 +3876,9 @@ void ui_update(const MachineState& st) { if (lblColdExHint) lv_obj_add_flag(lblColdExHint, LV_OBJ_FLAG_HIDDEN); } sw_set(swColdEx, st.cxActive); - // Einschalten nur bei Freigabe; Ausschalten immer - ausser waehrend eines kalten Bezugs - bool usable = st.cxEnabled && (st.cxActive ? !st.cxShot : st.cxAllowed); + // Umschalten haengt an cxArmable, NICHT an cxAllowed: Der Modus muss auch im Standby + // und bei warmem Kessel scharf zu stellen sein. Waehrend eines kalten Bezugs gesperrt. + bool usable = st.cxEnabled && st.cxArmable && !(st.cxActive && st.cxShot); if (usable) lv_obj_remove_state(swColdEx, LV_STATE_DISABLED); else lv_obj_add_state(swColdEx, LV_STATE_DISABLED); if (lblColdExHint && st.cxEnabled) { @@ -3860,7 +3886,9 @@ void ui_update(const MachineState& st) { String hint; if (st.cxActive) { hint = "Wasserkreis heizt nicht (bleibt über Standby und Neustart aktiv). "; - if (st.cxAllowed) { + if (st.standbyActive) { + hint += "Nach dem Aufwecken bleibt der Wasserkreis aus."; + } else if (st.cxAllowed) { char b[96]; if (scaleOk) { snprintf(b, sizeof(b), "Bezug regulär starten - Ziel %.0f g, max %.0f s.",