Compare commits

..
Author SHA1 Message Date
raw-designsandClaude Opus 5 49bdc686c4 fix(S3): Auswahl beim Aufwecken auch nach Kaltstart + Diagnose der Bedingungen (5.6.1)
Die Abfrage hängt an fünf Bedingungen gleichzeitig (freigeschaltet, Abfrage
eingeschaltet, Display verbunden, Modus gerade aus, kein Wartungsmodus/Reinigung/
Tuning). Fehlt eine, passiert schlicht nichts, und von außen war nicht erkennbar
welche. Die Seite /Cold-Extraction zeigt sie jetzt live mit Haken bzw. Kreuz an,
dazu ob das Auswahlfenster gerade offen ist.

Zusätzlich eine echte Lücke geschlossen: Die Abfrage kam nur beim Standby-Übergang.
Wer die Maschine am Netzschalter einschaltet, durchläuft nie einen solchen Übergang
— dieser Weg war nicht abgedeckt. Jetzt öffnet das Fenster einmal pro Laufzeit auch
nach einem Kaltstart, sobald sich das Display gemeldet hat. Startet die Maschine in
den Standby hinein, bleibt es beim Standby-Übergang.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 02:33:35 +02:00
thomas 8dcebaa22f Merge pull request 'feat: Espresso/Cold-Auswahl bei jedem Aufwecken, auch per Schalter (S3 5.6.0 / P4 1.4.0)' (#22) from feat/cold-extraction-auswahl-nach-aufwecken into main 2026-08-12 01:47:09 +02:00
raw-designsandClaude Opus 5 7c1e485722 feat: Espresso/Cold-Auswahl bei jedem Aufwecken, auch per Schalter (S3 5.6.0 / P4 1.4.0)
Die Abfrage war eine reine Vorab-Abfrage des Displays und erschien nur beim
Antippen des Standby-Bildschirms. Beim Aufwecken über den Schalter ist der Standby
aber schon beendet, bevor das Display überhaupt fragen könnte — dieser Weg war nie
abgedeckt.

Die Steuerung hält jetzt nach dem Aufwecken ein Auswahlfenster offen (20 s): Der
Wasserkreis heizt in dieser Zeit nicht, das Display blendet die Auswahl von sich aus
ein und zeigt einen Countdown. Ohne Entscheidung wird danach normal geheizt, es geht
also nichts verloren, wenn niemand hinsieht.

Das Fenster öffnet nur mit angeschlossenem Display (sonst könnte niemand wählen),
bei freigeschalteter Funktion und aktiver Einstellung "Abfrage beim Aufwecken", und
nur wenn der Modus nicht ohnehin schon läuft. Es schließt bei Entscheidung,
Bezug/Spülen oder erneutem Standby.

Neue Aktion wakeEspresso (beendet Standby falls aktiv und schließt das Fenster) —
das Display sendet sie statt deactivateStandby, sobald die Auswahl relevant ist,
sonst würde direkt nach dem Aufwecken nochmal gefragt. Neue State-Felder
cxWakeChoice und cxWakeChoiceSec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 01:46:47 +02:00
thomas 7d14f94a72 Merge pull request 'feat: Abfrage Espresso oder Cold Extraction beim Aufwecken (S3 5.5.0 / P4 1.3.0)' (#21) from feat/cold-extraction-abfrage-beim-aufwecken into main 2026-08-12 01:02:58 +02:00
raw-designsandClaude Opus 5 11307d255b feat: Abfrage Espresso oder Cold Extraction beim Aufwecken (S3 5.5.0 / P4 1.3.0)
Der Aufweck-Dialog am Touch-Display wird zur Auswahl: "Womit aufwecken?" mit
"Espresso" (heizt normal auf) und "Cold Extraction (Wasser bleibt kalt)". Damit
lässt sich bei jedem Aufwecken neu entscheiden, ohne vorher die Web-UI zu bedienen.
Bei der kalten Variante wird der Modus scharf geschaltet, bevor der Standby endet —
der Wasserkreis heizt also gar nicht erst an.

Abschaltbar über die neue Einstellung "Abfrage beim Aufwecken" (EEPROM 982), zu
finden auf /Cold-Extraction und auf der Cold-Extraction-Seite des Displays. Ist sie
aus, erscheint wieder der bisherige Dialog. Ohne angeschlossenes Display oder bei
nicht freigeschalteter Funktion hat sie keine Wirkung. Neues Feld cxAskOnWake im
State und in saveBrew.

Dazu: Die Meldungstexte der kalten Extraktion sind jetzt mit echten Umlauten
geschrieben ("nicht möglich" statt "nicht moeglich"). Für das OLED übersetzt die
neue Hilfsfunktion oledUmlauts() sie beim Rendern nach CP437 — dort hätte UTF-8
sonst zwei Fehlzeichen je Umlaut ergeben, weshalb die Texte ursprünglich
transliteriert waren. Web-UI und Touch-Display stellen UTF-8 direkt dar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 01:02:37 +02:00
thomas f1a8d8c175 Merge pull request 'fix: Zielgewicht bei Cold Extraction ueberall korrekt anzeigen (S3 5.4.1 / P4 1.2.1)' (#20) from fix/cold-extraction-zielgewicht-anzeige into main 2026-08-12 00:52:07 +02:00
8 changed files with 337 additions and 38 deletions
+41
View File
@@ -1,3 +1,44 @@
Version 5.6.1:
- Diagnose: Die Seite /Cold-Extraction zeigt jetzt live, welche Voraussetzungen der Abfrage beim
Aufwecken erfüllt sind (freigeschaltet, Abfrage eingeschaltet, Touch-Display verbunden, Modus
gerade aus, kein Wartungsmodus/Reinigung/Tuning) und ob das Auswahlfenster gerade offen ist.
Hintergrund: Die Abfrage hängt an fünf Bedingungen gleichzeitig - fehlt eine, passiert einfach
nichts, und es war von außen nicht erkennbar welche.
- Die Abfrage kommt jetzt auch nach einem Kaltstart, nicht nur nach dem Standby. Wer die Maschine
am Netzschalter einschaltet, durchläuft nie einen Standby-Übergang; dieser Weg war deshalb bisher
nicht abgedeckt. Startet die Maschine in den Standby hinein, bleibt es beim Standby-Übergang.
Version 5.6.0:
- Die Abfrage „Espresso oder Cold Extraction" kommt jetzt bei JEDEM Weg aus dem Standby, also auch beim
Aufwecken über den Schalter. Bisher war sie eine reine Vorab-Abfrage des Touch-Displays und erschien nur
beim Antippen des Standby-Bildschirms - beim Schalter ist der Standby aber schon beendet, bevor das
Display überhaupt fragen könnte. Dieser Weg war damit nie abgedeckt.
- Dafür hält die Steuerung nach dem Aufwecken ein Auswahlfenster offen (20 s): Der Wasserkreis heizt in
dieser Zeit nicht, das Display zeigt die Auswahl mit Countdown. Ohne Entscheidung wird anschließend
normal geheizt - es geht also nichts verloren, wenn niemand hinsieht.
- Das Fenster öffnet nur mit angeschlossenem Touch-Display (ohne Display könnte niemand wählen), bei
freigeschalteter Funktion und aktivierter Einstellung „Abfrage beim Aufwecken", und nur wenn der
Cold-Extraction-Modus nicht ohnehin schon aktiv ist. Es schließt sofort bei einer Entscheidung, bei
Bezug/Spülen oder erneutem Standby.
- Neue Aktion „wakeEspresso": beendet den Standby (falls noch aktiv) und schließt das Auswahlfenster.
Das Display sendet sie statt „deactivateStandby", sobald die Auswahl relevant ist - sonst würde nach
dem Aufwecken über den Dialog gleich nochmal gefragt.
- Neue State-Felder „cxWakeChoice" (Fenster offen) und „cxWakeChoiceSec" (Restsekunden).
Version 5.5.0:
- Neu: Beim Aufwecken aus dem Standby fragt das Touch-Display, ob normal geheizt (Espresso) oder kalt
extrahiert werden soll. Damit lässt sich das bei jedem Aufwecken neu entscheiden, ohne vorher die
Web-UI zu bedienen. Bei „Cold Extraction" wird der Modus scharf geschaltet, BEVOR der Standby endet -
der Wasserkreis heizt also gar nicht erst an.
- Abschaltbar über die neue Einstellung „Abfrage beim Aufwecken" auf /Cold-Extraction bzw. auf der
Cold-Extraction-Seite des Touch-Displays. Ohne angeschlossenes Display oder bei nicht freigeschalteter
Funktion hat sie keine Wirkung; ist sie aus, bleibt der bisherige Dialog („Aufwecken" / „Abbrechen").
- Neues UART-Feld „cxAskOnWake" im State und als Feld in saveBrew.
- Die Meldungstexte der kalten Extraktion sind jetzt mit echten Umlauten geschrieben („nicht möglich"
statt „nicht moeglich"). Für das OLED werden sie beim Rendern nach CP437 übersetzt (neue Hilfsfunktion
oledUmlauts) - dort hätte UTF-8 sonst zwei Fehlzeichen je Umlaut ergeben. Web-UI und Touch-Display
stellen UTF-8 ohnehin direkt dar.
Version 5.4.1:
- Bug-Fix: Während einer kalten Extraktion stand als Zielgewicht überall noch der Brew-by-Weight-Wert
des normalen Espresso-Bezugs. Betroffen waren OLED, Web-Dashboard und Touch-Display, weil alle drei
+5 -1
View File
@@ -322,6 +322,7 @@ Alle gültigen `action`-Strings aus `executeDashboardAction()` (Stand v4.8.1, Ze
| `startColdExtraction` | — | Modus aktivieren (Wasserkreis heizt nicht, Dampf bleibt möglich) |
| `stopColdExtraction` | — | Modus beenden |
| `toggleColdExtraction` | — | Modus umschalten |
| `wakeEspresso` | — | Standby beenden (falls aktiv) und das Auswahlfenster schließen, also „normal heizen" wählen (ab S3 5.6.0) |
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
@@ -343,7 +344,7 @@ Benutzer, warum die Maschine nicht heizt. Ein laufender Bezug wird nie abgebroch
Die Parameter der kalten Extraktion sind ab S3 5.4.0 über `saveBrew` einstellbar (zusätzlich
zur Web-Seite `/Cold-Extraction`). Zulässige Felder: `cxEnabled` (bool), `cxMaxTemp`,
`cxPreInf`, `cxPreInfDuty`, `cxPulseMs`, `cxDuty`, `cxTarget`, `cxMaxSecs`, `cxPumpMaxRun`,
`cxPumpRest`. Es werden nur die tatsächlich gesendeten Felder übernommen; Grenzwerte prüft
`cxPumpRest`, `cxAskOnWake` (bool). Es werden nur die tatsächlich gesendeten Felder übernommen; Grenzwerte prüft
die S3. Die aktuellen Werte stehen alle im `state`, damit das Display seine Eingabefelder
vorbelegen kann.
@@ -406,6 +407,9 @@ Aus `getTouchUartStatus()`. Für sprach-unabhängige UI-Logik/Icons auf dem P4 v
| `cxPulseMs` | uint16 | Pulsperiode in ms (ab S3 5.4.0) |
| `cxPumpMaxRun` | float | Pumpenschutz: max. kumulierte Laufzeit in s (ab S3 5.4.0) |
| `cxPumpRest` | float | Pumpenschutz: Zwangspause in s (ab S3 5.4.0) |
| `cxAskOnWake` | bool | Beim Aufwecken aus dem Standby nach Espresso/Cold Extraction fragen (ab S3 5.5.0) |
| `cxWakeChoice` | bool | Auswahlfenster nach dem Aufwecken offen — Wasserkreis heizt solange nicht (ab S3 5.6.0) |
| `cxWakeChoiceSec` | long | Restsekunden der Bedenkzeit |
| `cxMaxSecs` | float | Sicherheits-Timeout des Bezugs in s |
| `cxBlock` | uint8 | Sperrgrund (siehe unten), `0` = frei |
| `cxNotice` | uint8 | transiente Meldung, `0` = keine (läuft nach ~6 s selbst ab) |
+192 -20
View File
@@ -369,7 +369,7 @@ Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire);
* Firmware-Informationen
************************************************************************************/
String version = "5.4.1";
String version = "5.6.1";
String versionHersteller = "Thomas M&uuml;ller";
String versionHerstellerMail =
"<a href='mailto:thomas@mueller.black' class='info-link'>thomas@mueller.black</a>";
@@ -790,6 +790,19 @@ float coldExtractionTargetGrams = 60.0f; // Zielgewicht (ca. 1:3 auf 18 g
float coldExtractionMaxSeconds = 300.0f; // Sicherheits-Timeout des Bezugs
float coldExtractionPumpMaxRunSec = 60.0f; // kumulierte Pumpenlaufzeit bis zur Zwangspause
float coldExtractionPumpRestSec = 20.0f; // Dauer der Zwangspause
// Beim Aufwecken aus dem Standby am Touch-Display fragen, ob normal geheizt oder kalt
// extrahiert werden soll. Nur wirksam bei angeschlossenem UART-Display und freigeschalteter
// Funktion; abschaltbar, wenn man den Zwischenschritt nicht mehr will.
bool coldExtractionAskOnWake = true;
// Auswahlfenster nach dem Aufwecken: Der Dialog am Display ist eine Vorab-Abfrage und kann
// den Schalter-Weg nicht abdecken - dort ist der Standby schon beendet, bevor das Display
// fragen koennte. Deshalb haelt die Steuerung den Wasserkreis nach dem Aufwecken kurz aus
// und wartet auf die Entscheidung; laeuft das Fenster ab, wird normal geheizt.
bool coldExtractionWakeChoicePending = false;
unsigned long coldExtractionWakeChoiceUntilMs = 0;
unsigned long coldExtractionWakeDecisionMs = 0; // Zeitpunkt der letzten expliziten Wahl
const unsigned long COLDEX_WAKE_CHOICE_MS = 20000UL; // Bedenkzeit
const unsigned long COLDEX_WAKE_DECISION_GRACE_MS = 5000UL; // frisch entschieden -> kein Fenster
const bool defaultColdExtractionEnabled = false;
const float defaultColdExtractionMaxTempC = 30.0f;
@@ -808,6 +821,7 @@ const float defaultColdExtractionTargetGrams = 60.0f;
const float defaultColdExtractionMaxSeconds = 300.0f;
const float defaultColdExtractionPumpMaxRunSec = 60.0f;
const float defaultColdExtractionPumpRestSec = 20.0f;
const bool defaultColdExtractionAskOnWake = true;
const float COLDEX_MAX_TEMP_MIN = 10.0f;
const float COLDEX_MAX_TEMP_MAX = 40.0f;
@@ -1073,7 +1087,8 @@ const int EEPROM_ADDR_COLDEX_PUMP_MAX_RUN = 969; // 4 Bytes (float) -> Ende
const int EEPROM_ADDR_COLDEX_PUMP_REST = 973; // 4 Bytes (float) -> Ende 976
const int EEPROM_ADDR_COLDEX_MODE_ACTIVE = 977; // 1 Byte (uint8_t) -> Ende 977
const int EEPROM_ADDR_COLDEX_PREINF_DUTY = 978; // 4 Bytes (float) -> Ende 981
// Naechste freie Adresse: 982 (Innerhalb EEPROM_SIZE=1024)
const int EEPROM_ADDR_COLDEX_ASK_ON_WAKE = 982; // 1 Byte (uint8_t) -> Ende 982
// Naechste freie Adresse: 983 (Innerhalb EEPROM_SIZE=1024)
/************************************************************************************
* Eco-Mode Variablen
@@ -3067,6 +3082,7 @@ void resetToDefaults() {
EEPROM.put(EEPROM_ADDR_COLDEX_PUMP_MAX_RUN, defaultColdExtractionPumpMaxRunSec);
EEPROM.put(EEPROM_ADDR_COLDEX_PUMP_REST, defaultColdExtractionPumpRestSec);
EEPROM.put(EEPROM_ADDR_COLDEX_MODE_ACTIVE, (uint8_t)0); // Werksreset: Modus immer aus
EEPROM.put(EEPROM_ADDR_COLDEX_ASK_ON_WAKE, (uint8_t)(defaultColdExtractionAskOnWake ? 1 : 0));
// Piezo auf Default im EEPROM schreiben
EEPROM.put(EEPROM_ADDR_PIEZO_ENABLED, defaultPiezoEnabled);
@@ -3155,6 +3171,7 @@ void resetToDefaults() {
coldExtractionMaxSeconds = defaultColdExtractionMaxSeconds;
coldExtractionPumpMaxRunSec = defaultColdExtractionPumpMaxRunSec;
coldExtractionPumpRestSec = defaultColdExtractionPumpRestSec;
coldExtractionAskOnWake = defaultColdExtractionAskOnWake;
coldExtractionModeActive = false;
coldExtractionShotActive = false;
steamByTimeEnabled = defaultSteamByTimeEnabled; steamByTimeTargetSeconds = defaultSteamByTimeTargetSeconds;
@@ -4060,6 +4077,34 @@ bool coldExtractionTemperatureOk() {
return (InputWasser < (double)limit);
}
// UTF-8-Umlaute in die Codepage der OLED-Schrift (CP437) uebersetzen. Meldungstexte werden
// mit echten Umlauten gehalten (Web-UI und Touch-Display koennen UTF-8); das OLED wuerde
// daraus zwei Fehlzeichen machen - vgl. "Startverz\x94gerung" an anderer Stelle.
String oledUmlauts(const String& in) {
String out;
out.reserve(in.length());
for (size_t i = 0; i < in.length(); i++) {
uint8_t c = (uint8_t)in[i];
if (c == 0xC3 && (i + 1) < in.length()) {
uint8_t d = (uint8_t)in[i + 1];
char mapped = 0;
switch (d) {
case 0xA4: mapped = (char)0x84; break; // ae
case 0xB6: mapped = (char)0x94; break; // oe
case 0xBC: mapped = (char)0x81; break; // ue
case 0x84: mapped = (char)0x8E; break; // Ae
case 0x96: mapped = (char)0x99; break; // Oe
case 0x9C: mapped = (char)0x9A; break; // Ue
case 0x9F: mapped = (char)0xE1; break; // ss
default: break;
}
if (mapped) { out += mapped; i++; continue; }
}
out += (char)c;
}
return out;
}
// Zielgewicht des aktuellen bzw. naechsten Bezugs. Bei aktiver Cold Extraction ist das
// coldExtractionTargetGrams - brewByWeightTargetGrams gilt dann nicht, weil die kalte
// Extraktion Brew-by-Weight ersetzt. Alle Anzeigen (OLED, Web, Display) muessen diesen
@@ -4105,23 +4150,23 @@ String coldExtractionBlockReason() {
return "Cold Extraction ist nicht freigeschaltet.";
}
if (standbyModeActive) {
return "Cold Extraction nicht moeglich: Standby aktiv.";
return "Cold Extraction nicht möglich: Standby aktiv.";
}
if (wartungsModusAktiv) {
return "Cold Extraction nicht moeglich: Wartungsmodus aktiv.";
return "Cold Extraction nicht möglich: Wartungsmodus aktiv.";
}
if (cleaningAssistantActive) {
return "Cold Extraction nicht moeglich: Reinigungsassistent aktiv.";
return "Cold Extraction nicht möglich: Reinigungsassistent aktiv.";
}
if (autoTuneWasserActive || autoTuneDampfActive) {
return "Cold Extraction nicht moeglich: PID-Tuning laeuft.";
return "Cold Extraction nicht möglich: PID-Tuning läuft.";
}
if (wasserSensorError || wasserSafetyShutdown || !isfinite(InputWasser)) {
return "Cold Extraction nicht moeglich: Wasser-Sensor liefert keinen gueltigen Wert.";
return "Cold Extraction nicht möglich: Wasser-Sensor liefert keinen gültigen Wert.";
}
if (!coldExtractionTemperatureOk()) {
char buf[96];
snprintf(buf, sizeof(buf), "Wasserkessel zu warm (%.1f C, benoetigt < %.1f C).",
snprintf(buf, sizeof(buf), "Wasserkessel zu warm (%.1f °C, benötigt < %.1f °C).",
(double)InputWasser, (double)coldExtractionMaxTempC);
return String(buf);
}
@@ -4142,7 +4187,7 @@ bool setColdExtractionMode(bool enable, String& message) {
return false;
}
if (shotActive || shotSoftwareActive || flushActive) {
message = "Cold Extraction nicht moeglich: Bezug oder Spuelen laeuft.";
message = "Cold Extraction nicht möglich: Bezug oder Spülen läuft.";
return false;
}
coldExtractionModeActive = true;
@@ -4154,7 +4199,7 @@ bool setColdExtractionMode(bool enable, String& message) {
} 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.";
message += " Der Kessel ist noch zu warm - er kühlt jetzt ab, Bezüge sind bis dahin gesperrt.";
}
if (!coldExtractionScaleUsable()) {
// Ohne Waage faellt die Dosierung auf die maximale Bezugsdauer zurueck - das
@@ -4204,13 +4249,13 @@ String coldExtractionArmBlockReason() {
return "Cold Extraction ist nicht freigeschaltet.";
}
if (wartungsModusAktiv) {
return "Cold Extraction nicht moeglich: Wartungsmodus aktiv.";
return "Cold Extraction nicht möglich: Wartungsmodus aktiv.";
}
if (cleaningAssistantActive) {
return "Cold Extraction nicht moeglich: Reinigungsassistent aktiv.";
return "Cold Extraction nicht möglich: Reinigungsassistent aktiv.";
}
if (autoTuneWasserActive || autoTuneDampfActive) {
return "Cold Extraction nicht moeglich: PID-Tuning laeuft.";
return "Cold Extraction nicht möglich: PID-Tuning läuft.";
}
return "";
}
@@ -4753,6 +4798,11 @@ void setup() {
// Der Modus ist bewusst persistent: Wer nur kalte Bezuege machen will, soll die
// Maschine nicht nach jedem Standby/Neustart erst aufheizen lassen, um sie danach
// wieder abkuehlen zu muessen. Nur bei entzogener Freischaltung faellt er weg.
uint8_t storedColdExtractionAsk = 0;
EEPROM.get(EEPROM_ADDR_COLDEX_ASK_ON_WAKE, storedColdExtractionAsk);
if (storedColdExtractionAsk > 1) { storedColdExtractionAsk = (uint8_t)(defaultColdExtractionAskOnWake ? 1 : 0); }
coldExtractionAskOnWake = (storedColdExtractionAsk == 1);
uint8_t storedColdExtractionMode = 0;
EEPROM.get(EEPROM_ADDR_COLDEX_MODE_ACTIVE, storedColdExtractionMode);
if (storedColdExtractionMode > 1) { storedColdExtractionMode = 0; }
@@ -5395,6 +5445,39 @@ void loop() {
lastShotTime = millis();
resetOffsetCompensationReferences();
applySteamHeatDisabledOnStartupWakeIfConfigured();
// Auswahlfenster oeffnen: gilt fuer JEDEN Weg aus dem Standby (Schalter, Touch, Web,
// Timer). Nur sinnvoll mit angeschlossenem Display - ohne das koennte niemand waehlen,
// und die Maschine wuerde nur unnoetig spaeter heizen.
if (coldExtractionEnabled && coldExtractionAskOnWake && touchUartClientActive &&
!coldExtractionModeActive && coldExtractionArmable() &&
(coldExtractionWakeDecisionMs == 0 ||
(currentMillis - coldExtractionWakeDecisionMs) > COLDEX_WAKE_DECISION_GRACE_MS)) {
coldExtractionWakeChoicePending = true;
coldExtractionWakeChoiceUntilMs = currentMillis + COLDEX_WAKE_CHOICE_MS;
}
}
// Gleiche Auswahl nach einem Kaltstart: Wer die Maschine am Netzschalter einschaltet,
// kommt nie durch den Standby-Uebergang - ohne diesen Zweig gaebe es dort keine Abfrage.
// Einmal pro Laufzeit, sobald sich das Display gemeldet hat.
static bool coldExtractionBootChoiceDone = false;
if (!coldExtractionBootChoiceDone && !standbyModeActive && touchUartClientActive &&
coldExtractionEnabled && coldExtractionAskOnWake && !coldExtractionModeActive &&
coldExtractionArmable()) {
coldExtractionBootChoiceDone = true;
coldExtractionWakeChoicePending = true;
coldExtractionWakeChoiceUntilMs = currentMillis + COLDEX_WAKE_CHOICE_MS;
} else if (!coldExtractionBootChoiceDone && touchUartClientActive && standbyModeActive) {
// Start in den Standby hinein: Der Standby-Uebergang uebernimmt die Abfrage.
coldExtractionBootChoiceDone = true;
}
// Auswahlfenster beenden: Zeit abgelaufen, Standby, oder es wird ohnehin schon bezogen
if (coldExtractionWakeChoicePending) {
if (standbyModeActive || coldExtractionModeActive || shotActive || shotSoftwareActive ||
flushActive || !coldExtractionEnabled ||
(long)(currentMillis - coldExtractionWakeChoiceUntilMs) >= 0) {
coldExtractionWakeChoicePending = false;
}
}
if (standbyModeActive != lastStandbyModeActive) {
syncFullyKioskWithStandby(standbyModeActive);
@@ -5426,7 +5509,7 @@ void loop() {
// nach Software-Shots, Einstellungsänderungen mid-shot etc. immer konsistent ist.
// Bei Cold Extraction zusaetzlich einfrieren: der Wasserkreis heizt minutenlang nicht,
// ohne Freeze wuerde der I-Anteil aufladen und nach dem Modus stark ueberschwingen.
setPidWasserIntegralFrozen(coldExtractionModeActive ||
setPidWasserIntegralFrozen(coldExtractionModeActive || coldExtractionWakeChoicePending ||
(ffIFreezeWasser && (shotActive || shotSoftwareActive) && !wartungsModusAktiv));
// --- Angepasste Sicherheitsueberpruefung Wasser ---
@@ -5447,12 +5530,12 @@ void loop() {
} else {
if (wasserSensorError) Serial.println("INFO: Wassertemperatursensor wieder OK."); wasserSensorError = false;
if (wasserSafetyShutdown) Serial.println("INFO: Wassertemperatur wieder im sicheren Bereich."); wasserSafetyShutdown = false;
bool activateBoost = boostWasserActive && !wartungsModusAktiv && !ecoModeAktiv && !autoTuneWasserActive && !coldExtractionModeActive && (InputWasser < (SetpointWasser * 0.95));
bool activateBoost = boostWasserActive && !wartungsModusAktiv && !ecoModeAktiv && !autoTuneWasserActive && !coldExtractionModeActive && !coldExtractionWakeChoicePending && (InputWasser < (SetpointWasser * 0.95));
if (activateBoost) { OutputWasser = windowSizeWasser; } else if (!autoTuneWasserActive) { pidWasser.Compute(); }
// Cold Extraction: Stellgroesse hart auf 0 - der Wasserkreis heizt nicht. Die PID
// laeuft weiter (D-Anteil/lastInput bleiben aktuell), nur der Output wird verworfen,
// damit auch die dutyW-Telemetrie fuer das Display die Wahrheit zeigt.
if (coldExtractionModeActive) { OutputWasser = 0; }
if (coldExtractionModeActive || coldExtractionWakeChoicePending) { OutputWasser = 0; }
}
yield();
@@ -5529,7 +5612,7 @@ void loop() {
recordResetCheckpoint(RESET_CP_LOOP_SSR);
unsigned long now = millis();
// Wasser SSR
if (wasserSensorError || wasserSafetyShutdown || standbyModeActive || displayOtaActive || coldExtractionModeActive) { digitalWrite(SSR_WASSER_PIN, LOW); } else {
if (wasserSensorError || wasserSafetyShutdown || standbyModeActive || displayOtaActive || coldExtractionModeActive || coldExtractionWakeChoicePending) { digitalWrite(SSR_WASSER_PIN, LOW); } else {
if (now - windowStartTimeWasser > windowSizeWasser) { windowStartTimeWasser = now; }
unsigned long effOutWasser = effectiveOutputWasserMs(); // PID-Output + Feed-Forward-Boost bei Bezug
bool pidWantsHeat = (effOutWasser > 0) && (now < (windowStartTimeWasser + effOutWasser));
@@ -6124,7 +6207,9 @@ void updateDisplay() {
display.println(F(""));
// Meldung auf 21 Zeichen pro Zeile umbrechen (128 px / 6 px Zeichenbreite)
const size_t lineLen = 21;
String rest = coldExtractionBlockMessage;
// Umlaute in die CP437-Codepage der OLED-Schrift wandeln; danach ist jeder Umlaut
// wieder ein Byte, der Zeilenumbruch unten zaehlt also richtig.
String rest = oledUmlauts(coldExtractionBlockMessage);
while (rest.length() > 0) {
if (rest.length() <= lineLen) {
display.println(rest);
@@ -7262,6 +7347,15 @@ static const char brewControlCX_Fields[] PROGMEM = R"rawliteral(
<small class="toggle-description-input">(Oberhalb dieser Temperatur bleibt der Modus zwar schaltbar, Bez&uuml;ge sind aber gesperrt.)</small>
{CX_STATUS_MSG}
<h3>Abfrage beim Aufwecken</h3>
<div class="toggle-switch-container"><span class="toggle-switch-label-text">Am Touch-Display fragen:</span>
<label class="toggle-switch"><input type="checkbox" id="cxAskOnWake" name="cxAskOnWake" value="1"{CX_ASK_CHECKED}>
<span class="toggle-slider"></span></label></div>
<small class="toggle-description-input">(Beim Aufwecken aus dem Standby fragt das Touch-Display, ob normal geheizt oder kalt
extrahiert werden soll - so entscheidet man das bei jedem Aufwecken neu, ohne vorher die Web-UI zu bedienen.
Ohne angeschlossenes Touch-Display oder bei nicht freigeschalteter Funktion hat die Einstellung keine Wirkung.)</small>
{CX_ASK_DIAG}
<h3>Vorbenetzung</h3>
<label for='cxPreInf'>Dauer (Sekunden):</label>
<input type='number' min='0.0' max='120.0' step='1.0' id='cxPreInf' name='cxPreInf' value='{CX_PREINF}' required>
@@ -7489,6 +7583,8 @@ struct BrewControlUpdate {
float cxPumpMaxRun = 0.0f;
bool hasCXPumpRest = false;
float cxPumpRest = 0.0f;
bool hasCXAskOnWake = false;
bool cxAskOnWake = false;
};
bool applyBrewControlUpdate(const BrewControlUpdate& update) {
@@ -7686,6 +7782,11 @@ bool applyBrewControlUpdate(const BrewControlUpdate& update) {
coldExtractionConfigChanged = true;
}
}
if (update.hasCXAskOnWake && update.cxAskOnWake != coldExtractionAskOnWake) {
coldExtractionAskOnWake = update.cxAskOnWake;
EEPROM.put(EEPROM_ADDR_COLDEX_ASK_ON_WAKE, (uint8_t)(coldExtractionAskOnWake ? 1 : 0));
coldExtractionConfigChanged = true;
}
// "changed" umfasst hier nur noch profil-relevante Felder (BBT/BBW/PI/SBT);
// FlowGuard und Cold Extraction sind kein Bestandteil von TemperatureProfile und
@@ -7741,6 +7842,28 @@ void handleColdExtraction(AsyncWebServerRequest *request) {
cxFieldsHtml.replace("{CX_PUMP_MAX_RUN}", buffer);
snprintf(buffer, sizeof(buffer), "%.1f", coldExtractionPumpRestSec);
cxFieldsHtml.replace("{CX_PUMP_REST}", buffer);
cxFieldsHtml.replace("{CX_ASK_CHECKED}", coldExtractionAskOnWake ? " checked" : "");
// Live-Status der Bedingungen: Die Abfrage haengt an mehreren Voraussetzungen gleichzeitig.
// Fehlt eine, passiert schlicht nichts - ohne diese Anzeige ist nicht erkennbar, welche.
{
String diag = "<br><small><b>Voraussetzungen der Abfrage:</b><br>";
auto flag = [](bool ok, const String& text) {
return String("<span style='color:") + (ok ? "#66CC66" : "#FFCC00") + ";'>" +
(ok ? "&#10004; " : "&#10008; ") + text + "</span><br>";
};
diag += flag(coldExtractionEnabled, "Funktion freigeschaltet");
diag += flag(coldExtractionAskOnWake, "Abfrage beim Aufwecken eingeschaltet");
diag += flag(touchUartClientActive, "Touch-Display verbunden");
diag += flag(!coldExtractionModeActive, "Cold-Extraction-Modus derzeit aus (sonst gibt es nichts zu fragen)");
diag += flag(coldExtractionArmable(), "Kein Wartungsmodus / Reinigung / PID-Tuning");
if (coldExtractionWakeChoicePending) {
long remainMs = (long)(coldExtractionWakeChoiceUntilMs - millis());
long remainSec = (remainMs > 0) ? ((remainMs + 999) / 1000) : 0;
diag += "<b style='color:#66CC66;'>Auswahlfenster ist gerade offen (noch " + String(remainSec) + " s).</b><br>";
}
diag += "</small>";
cxFieldsHtml.replace("{CX_ASK_DIAG}", diag);
}
if (coldExtractionEnabled) {
String cxStatus;
@@ -7791,6 +7914,8 @@ void handleSaveColdExtraction(AsyncWebServerRequest *request) {
if (update.hasCXPumpMaxRun) { update.cxPumpMaxRun = request->arg("cxPumpMaxRun").toFloat(); }
update.hasCXPumpRest = request->hasArg("cxPumpRest");
if (update.hasCXPumpRest) { update.cxPumpRest = request->arg("cxPumpRest").toFloat(); }
update.hasCXAskOnWake = true; // Checkbox: fehlt im POST = "aus"
update.cxAskOnWake = request->hasArg("cxAskOnWake");
applyBrewControlUpdate(update);
@@ -15597,6 +15722,29 @@ bool executeDashboardAction(const String& action, const String& value,
success = true;
}
}
// Gegenstueck zu startColdExtraction beim Aufwecken: "normal heizen" bewusst waehlen.
// Beendet den Standby, falls noch aktiv, und schliesst das Auswahlfenster - sonst
// wuerde nach dem Aufwecken noch einmal gefragt.
else if (action == "wakeEspresso") {
coldExtractionWakeDecisionMs = millis();
coldExtractionWakeChoicePending = false;
if (standbyModeActive) {
standbyModeActive = false;
startupTime = millis();
ecoForcedActive = false;
steamDelayOverridden = false;
EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
ecoModeAktiv = false;
ecoModeActivatedTime = 0;
lastShotTime = millis();
applyLightOutput();
message = "Espresso: Standby aufgehoben, Kessel heizt auf.";
} else {
message = "Espresso: Kessel heizt auf.";
}
success = true;
}
else if (action == "activateStandby") {
if (standbyModeActive) {
message = "Standby ist bereits aktiv.";
@@ -15726,6 +15874,9 @@ bool executeDashboardAction(const String& action, const String& value,
} else {
target = !coldExtractionModeActive;
}
// Jede bewusste Wahl schliesst das Auswahlfenster nach dem Aufwecken
coldExtractionWakeDecisionMs = millis();
coldExtractionWakeChoicePending = false;
success = setColdExtractionMode(target, message);
if (!success) {
coldExtractionBlockMessage = message;
@@ -15747,7 +15898,7 @@ bool executeDashboardAction(const String& action, const String& value,
// Freigabe wird pro Bezug neu geprueft (der Dampfkreis darf weiter heizen und
// kann den Wasserkessel ueber die Zeit ueber die Schwelle bringen).
message = coldExtractionBlockReason();
if (message.length() == 0) { message = "Cold Extraction derzeit nicht moeglich."; }
if (message.length() == 0) { message = "Cold Extraction derzeit nicht möglich."; }
coldExtractionBlockMessageUntilMs = millis() + COLDEX_BLOCK_MESSAGE_MS;
coldExtractionBlockMessage = message;
coldExtractionNotice = COLDEX_NOTICE_START_REJECTED;
@@ -16228,6 +16379,9 @@ void buildDashboardJson(char *buffer, size_t bufferSize) {
}
}
statusKey = "brewing";
} else if (coldExtractionWakeChoicePending) {
statusText = "Auswahl: Espresso oder Cold Extraction?";
statusKey = "coldextraction";
} else if (coldExtractionModeActive) {
statusText = coldExtractionScaleUsable()
? "Cold Extraction bereit (Wasserkreis heizt nicht)"
@@ -16479,6 +16633,9 @@ static void getTouchUartStatus(String& statusText, String& statusKey) {
statusText = "Bezug aktiv...";
}
statusKey = "brewing";
} else if (coldExtractionWakeChoicePending) {
statusText = "Auswahl: Espresso oder Cold Extraction?";
statusKey = "coldextraction";
} else if (coldExtractionModeActive) {
statusText = coldExtractionScaleUsable() ? "Cold Extraction bereit"
: "Cold Extraction bereit (Zeitsteuerung)";
@@ -16679,6 +16836,17 @@ static void buildTouchUartStateJson(char *buffer, size_t bufferSize) {
// 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");
offset += snprintf(buffer + offset, bufferSize - offset, "\"cxAskOnWake\":%s,", coldExtractionAskOnWake ? "true" : "false");
// Offenes Auswahlfenster nach dem Aufwecken (+ Restsekunden fuer den Countdown am Display)
offset += snprintf(buffer + offset, bufferSize - offset, "\"cxWakeChoice\":%s,", coldExtractionWakeChoicePending ? "true" : "false");
{
long cxWakeSec = 0;
if (coldExtractionWakeChoicePending) {
long remainMs = (long)(coldExtractionWakeChoiceUntilMs - millis());
cxWakeSec = (remainMs > 0) ? (long)((remainMs + 999) / 1000) : 0;
}
offset += snprintf(buffer + offset, bufferSize - offset, "\"cxWakeChoiceSec\":%ld,", cxWakeSec);
}
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);
dtostrf(coldExtractionPreInfusionSeconds, 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"cxPreInf\":%s,", floatBuf);
@@ -17410,13 +17578,17 @@ static bool touchUartHandleCommandLine(const String& line) {
update.hasCXPumpRest = true;
update.cxPumpRest = floatVal;
}
if (jsonGetBoolValue(line, "cxAskOnWake", boolVal)) {
update.hasCXAskOnWake = true;
update.cxAskOnWake = boolVal;
}
if (!update.hasBBTEnabled && !update.hasBBTSecs && !update.hasBBWEnabled && !update.hasBBWTarget &&
!update.hasBBWOffset && !update.hasPIEnabled && !update.hasPIDurSecs && !update.hasPIPauseSecs &&
!update.hasSBTEnabled && !update.hasSBTSecs &&
!update.hasCXEnabled && !update.hasCXMaxTemp && !update.hasCXPreInf && !update.hasCXPreInfDuty &&
!update.hasCXPulseMs && !update.hasCXDuty && !update.hasCXTarget && !update.hasCXMaxSecs &&
!update.hasCXPumpMaxRun && !update.hasCXPumpRest) {
!update.hasCXPumpMaxRun && !update.hasCXPumpRest && !update.hasCXAskOnWake) {
touchUartSendError(id, "Keine Brew-Felder");
return false;
}
+20
View File
@@ -1,3 +1,23 @@
Version 1.4.0:
- Die Auswahl „Espresso oder Cold Extraction" erscheint jetzt auch beim Aufwecken über den Schalter
(braucht S3-Firmware ab 5.6.0). Meldet die S3 ein offenes Auswahlfenster (cxWakeChoice), blendet das
Display den Dialog von sich aus ein - ohne Antippen - und zeigt die verbleibende Bedenkzeit als
Countdown. „Abbrechen" entfällt in diesem Fall, weil es nichts abzubrechen gibt: Läuft die Zeit ab,
heizt die Maschine normal auf.
- Der Espresso-Knopf sendet „wakeEspresso" statt „deactivateStandby", sobald die Auswahl relevant ist.
Damit wird die Entscheidung auf der S3 vermerkt und nicht direkt danach erneut gefragt.
- Der Cold-Extraction-Knopf beendet den Standby nur noch, wenn er überhaupt aktiv ist - im Auswahlfenster
nach dem Aufwecken schaltet er nur den Modus.
Version 1.3.0:
- Der Aufweck-Dialog wird zur Auswahl: „Womit aufwecken?" mit den Knöpfen „Espresso" (heizt normal auf)
und „Cold Extraction (Wasser bleibt kalt)". Braucht S3-Firmware ab 5.5.0.
Bei „Cold Extraction" wird erst der Modus scharf geschaltet und dann der Standby beendet, damit der
Wasserkreis nicht zwischendurch anheizt.
- Steuerbar über die neue Einstellung „Beim Aufwecken fragen" (Cold-Extraction-Seite, Abschnitt
Grundeinstellung; identisch zur Web-UI). Ist sie aus, erscheint wieder der bisherige Dialog mit
„Aufwecken" und „Abbrechen" - der Zwischenschritt entfällt dann komplett.
Version 1.2.1:
- Bug-Fix: Während einer kalten Extraktion zeigten Waage-Kachel und Live-Bezugsschirm das Zielgewicht
des normalen Espresso-Bezugs (bbwTarget) statt des Ziels der kalten Extraktion (cxTarget). Außerdem
+1 -1
View File
@@ -65,7 +65,7 @@
// -------------------------------------------------------------------------------------
// Firmware
// -------------------------------------------------------------------------------------
#define DISPLAY_FW_VERSION "1.2.1" // Firmware-Stand der P4-Display-Steuerung (Info-Seite)
#define DISPLAY_FW_VERSION "1.4.0" // Firmware-Stand der P4-Display-Steuerung (Info-Seite)
// -------------------------------------------------------------------------------------
// Protokoll
+3
View File
@@ -84,6 +84,9 @@ struct MachineState {
uint16_t cxPulseMs = 0; // Pulsperiode in ms
float cxPumpMaxRun = 0; // Pumpenschutz: max. kumulierte Laufzeit in s
float cxPumpRest = 0; // Pumpenschutz: Zwangspause in s
bool cxAskOnWake = false; // Beim Aufwecken aus dem Standby fragen (seit S3 5.5.0)
bool cxWakeChoice = false; // Auswahlfenster nach dem Aufwecken offen (seit S3 5.6.0)
long cxWakeChoiceSec = 0; // Restsekunden der Bedenkzeit
float cxMaxSecs = 0; // Sicherheits-Timeout des Bezugs in s
// Sperrgrund: 0=frei, 1=nicht freigeschaltet, 2=Standby, 3=Wartung, 4=Reinigungsassistent,
// 5=PID-Tuning, 6=Sensorfehler, 7=Wasserkessel zu warm
+3
View File
@@ -236,6 +236,9 @@ void ProtocolClient::applyState(const String& json) {
s.cxPulseMs = doc["cxPulseMs"] | s.cxPulseMs;
s.cxPumpMaxRun = doc["cxPumpMaxRun"] | s.cxPumpMaxRun;
s.cxPumpRest = doc["cxPumpRest"] | s.cxPumpRest;
s.cxAskOnWake = doc["cxAskOnWake"] | false;
s.cxWakeChoice = doc["cxWakeChoice"] | false;
s.cxWakeChoiceSec = doc["cxWakeChoiceSec"] | (long)0;
s.cxMaxSecs = doc["cxMaxSecs"] | s.cxMaxSecs;
s.cxBlock = doc["cxBlock"] | (uint8_t)0;
s.cxNotice = doc["cxNotice"] | (uint8_t)0;
+72 -16
View File
@@ -38,7 +38,10 @@ 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 lv_obj_t* g_wakeColdBtn = nullptr; // "Cold Extraction" im Aufweck-Dialog
static lv_obj_t* g_wakeMsg = nullptr; // Text des Aufweck-Dialogs (wechselt bei Abfrage-Modus)
static lv_obj_t* g_wakeYesLbl = nullptr; // Beschriftung des Bestaetigen-Knopfs
static lv_obj_t* g_wakeNoBtn = nullptr; // "Abbrechen" (entfaellt beim Auswahlfenster nach dem Aufwecken)
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)
@@ -258,6 +261,7 @@ static lv_obj_t* taCxDuty = nullptr; static lv_obj_t* taCxTarget = nullptr;
static lv_obj_t* taCxMaxSecs = nullptr; static lv_obj_t* taCxPumpMaxRun = nullptr;
static lv_obj_t* taCxPumpRest = nullptr;
static lv_obj_t* swCxEnabled = nullptr; // Freischaltung (entspricht dem Web-Toggle)
static lv_obj_t* swCxAskOnWake = nullptr; // Abfrage beim Aufwecken aus dem Standby
static lv_obj_t* lblCxPageHint = nullptr;
static uint32_t g_cxEditMs = 0; // waehrend der Eingabe die Felder nicht ueberschreiben
@@ -604,7 +608,12 @@ static void wake_yes_cb(lv_event_t*) { // Aufwecken -> hell lassen, S3 bee
wake_confirm_hide();
// g_wakeDialogOpen bleibt true, bis der Standby wirklich endet (ui_update) - sonst wuerde
// das Display in der Uebergangsphase kurz auf Standby-Helligkeit abdunkeln.
if (g_client) { g_client->sendAction("deactivateStandby"); showToast("Maschine wird aufgeweckt...", false); }
if (!g_client) return;
// "wakeEspresso" beendet den Standby UND schliesst das Auswahlfenster der S3. Ohne das
// wuerde nach dem Aufwecken noch einmal gefragt bzw. der Wasserkreis bliebe kurz aus.
const bool choiceRelevant = g_state.cxWakeChoice || (g_state.cxEnabled && g_state.cxAskOnWake);
g_client->sendAction(choiceRelevant ? "wakeEspresso" : "deactivateStandby");
showToast(g_state.standbyActive ? "Maschine wird aufgeweckt..." : "Espresso: Kessel heizt auf.", 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,
@@ -613,8 +622,8 @@ 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);
if (g_state.standbyActive) g_client->sendAction("deactivateStandby");
showToast("Cold Extraction aktiviert.", false);
}
}
bool ui_wake_dialog_open() { return g_wakeDialogOpen; } // fuer die Helligkeitssteuerung in onState
@@ -2076,7 +2085,8 @@ static void send_save_coldex(lv_event_t*) {
f += "\"cxTarget\":"; f += String(ta_f(taCxTarget), 1); f += ",";
f += "\"cxMaxSecs\":"; f += String(ta_f(taCxMaxSecs), 1); f += ",";
f += "\"cxPumpMaxRun\":"; f += String(ta_f(taCxPumpMaxRun), 1); f += ",";
f += "\"cxPumpRest\":"; f += String(ta_f(taCxPumpRest), 1);
f += "\"cxPumpRest\":"; f += String(ta_f(taCxPumpRest), 1); f += ",";
f += "\"cxAskOnWake\":"; f += boolStr(sw_get(swCxAskOnWake));
g_client->sendCommand("saveBrew", f);
g_cxEditMs = millis();
showToast("Speichere Cold Extraction...", false);
@@ -2096,6 +2106,7 @@ static void build_coldex(lv_obj_t* p) {
lv_obj_t* cBase = group_card(p, "Grundeinstellung");
swCxEnabled = add_switch(cBase, "Funktion freigeschaltet");
taCxMaxTemp = add_num(cBase, "Freigabe bis (°C)");
swCxAskOnWake = add_switch(cBase, "Beim Aufwecken fragen");
lv_obj_t* cPre = group_card(p, "Vorbenetzung",
"Deutlich schwächer als der Hauptbezug: Kaltes Mehl quillt nicht, volle Leistung legt "
@@ -2132,6 +2143,7 @@ static void build_coldex(lv_obj_t* p) {
static void populate_coldex(const MachineState& st) {
if (!taCxMaxTemp) return;
sw_set(swCxEnabled, st.cxEnabled);
sw_set(swCxAskOnWake, st.cxAskOnWake);
ta_setf(taCxMaxTemp, st.cxMaxTemp, 1);
ta_setf(taCxPreInf, st.cxPreInfSec, 0);
ta_setf(taCxPreInfDuty, st.cxPreInfDuty, 0);
@@ -3324,12 +3336,12 @@ void ui_init(ProtocolClient* client) {
lv_obj_set_flex_align(wakeCard, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_row(wakeCard, 18, 0);
lv_obj_clear_flag(wakeCard, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* wakeMsg = lv_label_create(wakeCard);
lv_obj_set_width(wakeMsg, LV_PCT(100));
lv_obj_set_style_text_align(wakeMsg, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_style_text_color(wakeMsg, COL_TEXT, 0);
lv_obj_set_style_text_font(wakeMsg, &lv_font_maven_pro_18, 0);
lv_label_set_text(wakeMsg, "Die Maschine befindet sich im Standby.\nAus dem Standby aufwecken?");
g_wakeMsg = lv_label_create(wakeCard);
lv_obj_set_width(g_wakeMsg, LV_PCT(100));
lv_obj_set_style_text_align(g_wakeMsg, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_style_text_color(g_wakeMsg, COL_TEXT, 0);
lv_obj_set_style_text_font(g_wakeMsg, &lv_font_maven_pro_18, 0);
lv_label_set_text(g_wakeMsg, "Die Maschine befindet sich im Standby.\nAus dem Standby aufwecken?");
lv_obj_t* wakeBtns = lv_obj_create(wakeCard);
lv_obj_set_width(wakeBtns, LV_PCT(100));
lv_obj_set_height(wakeBtns, LV_SIZE_CONTENT);
@@ -3343,13 +3355,15 @@ void ui_init(ProtocolClient* client) {
lv_obj_t* wakeNo = th_button(wakeBtns, "Abbrechen", COL_CARD2, COL_TEXT);
lv_obj_set_width(wakeNo, 150);
lv_obj_add_event_cb(wakeNo, wake_no_cb, LV_EVENT_CLICKED, nullptr);
g_wakeNoBtn = wakeNo;
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);
g_wakeYesLbl = lv_obj_get_child(wakeYes, 0); // Label des Knopfs (fuer "Espresso" im Abfrage-Modus)
// 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);
g_wakeColdBtn = th_button(wakeCard, "Cold Extraction (Wasser bleibt kalt)", 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);
@@ -3548,7 +3562,9 @@ void ui_update(const MachineState& st) {
}
// Aufweck-Ablauf: nach beendetem Standby Dialog schliessen und Helligkeits-Flag loeschen
// (auch wenn der Dialog schon ausgeblendet ist, weil "Aufwecken" das Flag bewusst haelt).
if (!st.standbyActive) {
// Bei offenem Auswahlfenster (cxWakeChoice) bleibt der Dialog stehen, obwohl der Standby
// schon beendet ist - genau dafuer ist er da.
if (!st.standbyActive && !st.cxWakeChoice) {
g_wakeDialogOpen = false;
if (g_wakeConfirm && !lv_obj_has_flag(g_wakeConfirm, LV_OBJ_FLAG_HIDDEN))
lv_obj_add_flag(g_wakeConfirm, LV_OBJ_FLAG_HIDDEN);
@@ -3580,9 +3596,49 @@ void ui_update(const MachineState& st) {
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);
// Abfrage-Modus: Der Aufweck-Dialog wird zur Auswahl "Espresso oder Cold Extraction".
// Abschaltbar ueber die Einstellung cxAskOnWake, damit man sich den Zwischenschritt
// sparen kann, wenn gerade keine kalten Bezuege anstehen.
{
// Zwei Faelle: Vorab-Abfrage beim Antippen im Standby (askMode) und das von der S3
// geoeffnete Auswahlfenster NACH dem Aufwecken (choiceOpen) - letzteres deckt auch
// das Aufwecken per Schalter ab, wo es keine Vorab-Abfrage geben kann.
const bool choiceOpen = st.cxWakeChoice;
const bool askMode = choiceOpen ||
(st.cxEnabled && st.cxArmable && !st.cxActive && st.cxAskOnWake);
if (g_wakeColdBtn) {
if (askMode) lv_obj_remove_flag(g_wakeColdBtn, LV_OBJ_FLAG_HIDDEN);
else lv_obj_add_flag(g_wakeColdBtn, LV_OBJ_FLAG_HIDDEN);
}
// Im Auswahlfenster gibt es nichts abzubrechen - laeuft die Zeit ab, wird geheizt.
if (g_wakeNoBtn) {
if (choiceOpen) lv_obj_add_flag(g_wakeNoBtn, LV_OBJ_FLAG_HIDDEN);
else lv_obj_remove_flag(g_wakeNoBtn, LV_OBJ_FLAG_HIDDEN);
}
if (g_wakeMsg) {
if (choiceOpen) {
char b[128];
snprintf(b, sizeof(b),
"Womit weiter?\nEspresso heizt auf, Cold Extraction bleibt kalt.\n"
"Ohne Auswahl wird in %ld s geheizt.", st.cxWakeChoiceSec);
label_set_if_changed(g_wakeMsg, b);
} else {
label_set_if_changed(g_wakeMsg, askMode
? "Die Maschine befindet sich im Standby.\nWomit aufwecken?"
: "Die Maschine befindet sich im Standby.\nAus dem Standby aufwecken?");
}
}
if (g_wakeYesLbl) {
label_set_if_changed(g_wakeYesLbl, askMode ? "Espresso" : "Aufwecken");
}
// Auswahlfenster von der S3 -> Dialog von sich aus zeigen (kein Antippen noetig)
if (choiceOpen && g_wakeConfirm && lv_obj_has_flag(g_wakeConfirm, LV_OBJ_FLAG_HIDDEN)) {
wake_confirm_show();
} else if (!choiceOpen && !st.standbyActive && g_wakeConfirm &&
!lv_obj_has_flag(g_wakeConfirm, LV_OBJ_FLAG_HIDDEN)) {
wake_confirm_hide(); // Fenster abgelaufen oder entschieden
g_wakeDialogOpen = false;
}
}
if (st.cxNotice == 2) {
status_set(ST_COLDEX, true, LV_SYMBOL_WARNING " Cold Extraction beendet: kein Zulauf",