Compare commits

...
Author SHA1 Message Date
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
raw-designsandClaude Opus 5 334896b9cf fix: Zielgewicht bei Cold Extraction überall korrekt anzeigen (S3 5.4.1 / P4 1.2.1)
Während einer kalten Extraktion stand als Zielgewicht überall noch der
Brew-by-Weight-Wert des normalen Espresso-Bezugs. OLED, Web-Dashboard und
Touch-Display griffen alle direkt auf brewByWeightTargetGrams zu — die kalte
Extraktion ersetzt Brew-by-Weight aber und stoppt nach coldExtractionTargetGrams.

Neue Hilfsfunktion activeTargetWeightGrams() liefert das Ziel des aktuellen bzw.
nächsten Bezugs. Das UART-Feld targetWeight führt jetzt diesen Wert; bbwTarget
bleibt unverändert die Roh-Einstellung, damit die Brew-Control-Seite weiter das
Richtige anzeigt.

Zusätzlich hing die Gewichtsanzeige selbst an "Brew-by-Weight aktiv" — das ist bei
kalter Extraktion typischerweise aus, obwohl ein Ziel existiert. OLED, Waage-Kachel
und Live-Bezugsschirm zeigen Gewicht und Fortschritt jetzt auch beim kalten Bezug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 00:51:48 +02:00
thomas e8b1832fc1 Merge pull request 'feat: eigene Cold-Extraction-Seite in Web-UI und Display (S3 5.4.0 / P4 1.2.0)' (#19) from feat/cold-extraction-eigene-seite into main 2026-08-12 00:09:49 +02:00
8 changed files with 187 additions and 40 deletions
+24
View File
@@ -1,3 +1,27 @@
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
direkt brewByWeightTargetGrams anzeigten - die kalte Extraktion ersetzt Brew-by-Weight aber und stoppt
nach coldExtractionTargetGrams. Neue Hilfsfunktion activeTargetWeightGrams() liefert jetzt das Ziel
des aktuellen bzw. nächsten Bezugs; das UART-Feld "targetWeight" führt diesen Wert (bbwTarget bleibt
unverändert die Roh-Einstellung).
- Auf dem OLED wird während eines kalten Bezugs jetzt überhaupt Gewicht angezeigt: Die Anzeige hing an
„Brew-by-Weight aktiv", das bei kalter Extraktion typischerweise aus ist - trotz vorhandenem Ziel.
Version 5.4.0: Version 5.4.0:
- Cold Extraction hat eine eigene Seite: /Cold-Extraction, eigener Eintrag im Menü. Auf /Brew-Control - Cold Extraction hat eine eigene Seite: /Cold-Extraction, eigener Eintrag im Menü. Auf /Brew-Control
steht nur noch ein Verweis darauf - die Seite war mit dem angehängten Block unübersichtlich geworden. steht nur noch ein Verweis darauf - die Seite war mit dem angehängten Block unübersichtlich geworden.
+2 -1
View File
@@ -343,7 +343,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 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`, zur Web-Seite `/Cold-Extraction`). Zulässige Felder: `cxEnabled` (bool), `cxMaxTemp`,
`cxPreInf`, `cxPreInfDuty`, `cxPulseMs`, `cxDuty`, `cxTarget`, `cxMaxSecs`, `cxPumpMaxRun`, `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 die S3. Die aktuellen Werte stehen alle im `state`, damit das Display seine Eingabefelder
vorbelegen kann. vorbelegen kann.
@@ -406,6 +406,7 @@ Aus `getTouchUartStatus()`. Für sprach-unabhängige UI-Logik/Icons auf dem P4 v
| `cxPulseMs` | uint16 | Pulsperiode in ms (ab S3 5.4.0) | | `cxPulseMs` | uint16 | Pulsperiode in ms (ab S3 5.4.0) |
| `cxPumpMaxRun` | float | Pumpenschutz: max. kumulierte Laufzeit in s (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) | | `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) |
| `cxMaxSecs` | float | Sicherheits-Timeout des Bezugs in s | | `cxMaxSecs` | float | Sicherheits-Timeout des Bezugs in s |
| `cxBlock` | uint8 | Sperrgrund (siehe unten), `0` = frei | | `cxBlock` | uint8 | Sperrgrund (siehe unten), `0` = frei |
| `cxNotice` | uint8 | transiente Meldung, `0` = keine (läuft nach ~6 s selbst ab) | | `cxNotice` | uint8 | transiente Meldung, `0` = keine (läuft nach ~6 s selbst ab) |
+96 -20
View File
@@ -369,7 +369,7 @@ Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire);
* Firmware-Informationen * Firmware-Informationen
************************************************************************************/ ************************************************************************************/
String version = "5.4.0"; String version = "5.5.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>";
@@ -790,6 +790,10 @@ float coldExtractionTargetGrams = 60.0f; // Zielgewicht (ca. 1:3 auf 18 g
float coldExtractionMaxSeconds = 300.0f; // Sicherheits-Timeout des Bezugs float coldExtractionMaxSeconds = 300.0f; // Sicherheits-Timeout des Bezugs
float coldExtractionPumpMaxRunSec = 60.0f; // kumulierte Pumpenlaufzeit bis zur Zwangspause float coldExtractionPumpMaxRunSec = 60.0f; // kumulierte Pumpenlaufzeit bis zur Zwangspause
float coldExtractionPumpRestSec = 20.0f; // Dauer der 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;
const bool defaultColdExtractionEnabled = false; const bool defaultColdExtractionEnabled = false;
const float defaultColdExtractionMaxTempC = 30.0f; const float defaultColdExtractionMaxTempC = 30.0f;
@@ -808,6 +812,7 @@ const float defaultColdExtractionTargetGrams = 60.0f;
const float defaultColdExtractionMaxSeconds = 300.0f; const float defaultColdExtractionMaxSeconds = 300.0f;
const float defaultColdExtractionPumpMaxRunSec = 60.0f; const float defaultColdExtractionPumpMaxRunSec = 60.0f;
const float defaultColdExtractionPumpRestSec = 20.0f; const float defaultColdExtractionPumpRestSec = 20.0f;
const bool defaultColdExtractionAskOnWake = true;
const float COLDEX_MAX_TEMP_MIN = 10.0f; const float COLDEX_MAX_TEMP_MIN = 10.0f;
const float COLDEX_MAX_TEMP_MAX = 40.0f; const float COLDEX_MAX_TEMP_MAX = 40.0f;
@@ -1073,7 +1078,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_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_MODE_ACTIVE = 977; // 1 Byte (uint8_t) -> Ende 977
const int EEPROM_ADDR_COLDEX_PREINF_DUTY = 978; // 4 Bytes (float) -> Ende 981 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 * Eco-Mode Variablen
@@ -3067,6 +3073,7 @@ void resetToDefaults() {
EEPROM.put(EEPROM_ADDR_COLDEX_PUMP_MAX_RUN, defaultColdExtractionPumpMaxRunSec); EEPROM.put(EEPROM_ADDR_COLDEX_PUMP_MAX_RUN, defaultColdExtractionPumpMaxRunSec);
EEPROM.put(EEPROM_ADDR_COLDEX_PUMP_REST, defaultColdExtractionPumpRestSec); 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_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 // Piezo auf Default im EEPROM schreiben
EEPROM.put(EEPROM_ADDR_PIEZO_ENABLED, defaultPiezoEnabled); EEPROM.put(EEPROM_ADDR_PIEZO_ENABLED, defaultPiezoEnabled);
@@ -3155,6 +3162,7 @@ void resetToDefaults() {
coldExtractionMaxSeconds = defaultColdExtractionMaxSeconds; coldExtractionMaxSeconds = defaultColdExtractionMaxSeconds;
coldExtractionPumpMaxRunSec = defaultColdExtractionPumpMaxRunSec; coldExtractionPumpMaxRunSec = defaultColdExtractionPumpMaxRunSec;
coldExtractionPumpRestSec = defaultColdExtractionPumpRestSec; coldExtractionPumpRestSec = defaultColdExtractionPumpRestSec;
coldExtractionAskOnWake = defaultColdExtractionAskOnWake;
coldExtractionModeActive = false; coldExtractionModeActive = false;
coldExtractionShotActive = false; coldExtractionShotActive = false;
steamByTimeEnabled = defaultSteamByTimeEnabled; steamByTimeTargetSeconds = defaultSteamByTimeTargetSeconds; steamByTimeEnabled = defaultSteamByTimeEnabled; steamByTimeTargetSeconds = defaultSteamByTimeTargetSeconds;
@@ -4060,6 +4068,42 @@ bool coldExtractionTemperatureOk() {
return (InputWasser < (double)limit); 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
// Wert nehmen, sonst steht dort das Espresso-Ziel waehrend eines kalten Bezugs.
float activeTargetWeightGrams() {
return coldExtractionModeActive ? coldExtractionTargetGrams : brewByWeightTargetGrams;
}
// Steht eine nutzbare Waage bereit? Ohne sie laeuft die kalte Extraktion rein zeitgesteuert: // Steht eine nutzbare Waage bereit? Ohne sie laeuft die kalte Extraktion rein zeitgesteuert:
// Zielgewicht und Stillstands-Erkennung greifen nicht, es bleibt nur die maximale Bezugsdauer. // Zielgewicht und Stillstands-Erkennung greifen nicht, es bleibt nur die maximale Bezugsdauer.
// Bewusst kein Sperrgrund - ein kurz abgemeldeter HX711 soll die Funktion nicht blockieren. // Bewusst kein Sperrgrund - ein kurz abgemeldeter HX711 soll die Funktion nicht blockieren.
@@ -4097,23 +4141,23 @@ String coldExtractionBlockReason() {
return "Cold Extraction ist nicht freigeschaltet."; return "Cold Extraction ist nicht freigeschaltet.";
} }
if (standbyModeActive) { if (standbyModeActive) {
return "Cold Extraction nicht moeglich: Standby aktiv."; return "Cold Extraction nicht möglich: Standby aktiv.";
} }
if (wartungsModusAktiv) { if (wartungsModusAktiv) {
return "Cold Extraction nicht moeglich: Wartungsmodus aktiv."; return "Cold Extraction nicht möglich: Wartungsmodus aktiv.";
} }
if (cleaningAssistantActive) { if (cleaningAssistantActive) {
return "Cold Extraction nicht moeglich: Reinigungsassistent aktiv."; return "Cold Extraction nicht möglich: Reinigungsassistent aktiv.";
} }
if (autoTuneWasserActive || autoTuneDampfActive) { 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)) { 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()) { if (!coldExtractionTemperatureOk()) {
char buf[96]; 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); (double)InputWasser, (double)coldExtractionMaxTempC);
return String(buf); return String(buf);
} }
@@ -4134,7 +4178,7 @@ bool setColdExtractionMode(bool enable, String& message) {
return false; return false;
} }
if (shotActive || shotSoftwareActive || flushActive) { 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; return false;
} }
coldExtractionModeActive = true; coldExtractionModeActive = true;
@@ -4146,7 +4190,7 @@ bool setColdExtractionMode(bool enable, String& message) {
} else if (!coldExtractionTemperatureOk()) { } else if (!coldExtractionTemperatureOk()) {
// Scharfschalten bei warmem Kessel ist erlaubt (Heizung geht sofort aus), ein // Scharfschalten bei warmem Kessel ist erlaubt (Heizung geht sofort aus), ein
// Bezug aber erst, wenn der Kessel unter die Schwelle gefallen ist. // 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()) { if (!coldExtractionScaleUsable()) {
// Ohne Waage faellt die Dosierung auf die maximale Bezugsdauer zurueck - das // Ohne Waage faellt die Dosierung auf die maximale Bezugsdauer zurueck - das
@@ -4196,13 +4240,13 @@ String coldExtractionArmBlockReason() {
return "Cold Extraction ist nicht freigeschaltet."; return "Cold Extraction ist nicht freigeschaltet.";
} }
if (wartungsModusAktiv) { if (wartungsModusAktiv) {
return "Cold Extraction nicht moeglich: Wartungsmodus aktiv."; return "Cold Extraction nicht möglich: Wartungsmodus aktiv.";
} }
if (cleaningAssistantActive) { if (cleaningAssistantActive) {
return "Cold Extraction nicht moeglich: Reinigungsassistent aktiv."; return "Cold Extraction nicht möglich: Reinigungsassistent aktiv.";
} }
if (autoTuneWasserActive || autoTuneDampfActive) { if (autoTuneWasserActive || autoTuneDampfActive) {
return "Cold Extraction nicht moeglich: PID-Tuning laeuft."; return "Cold Extraction nicht möglich: PID-Tuning läuft.";
} }
return ""; return "";
} }
@@ -4745,6 +4789,11 @@ void setup() {
// Der Modus ist bewusst persistent: Wer nur kalte Bezuege machen will, soll die // 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 // Maschine nicht nach jedem Standby/Neustart erst aufheizen lassen, um sie danach
// wieder abkuehlen zu muessen. Nur bei entzogener Freischaltung faellt er weg. // 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; uint8_t storedColdExtractionMode = 0;
EEPROM.get(EEPROM_ADDR_COLDEX_MODE_ACTIVE, storedColdExtractionMode); EEPROM.get(EEPROM_ADDR_COLDEX_MODE_ACTIVE, storedColdExtractionMode);
if (storedColdExtractionMode > 1) { storedColdExtractionMode = 0; } if (storedColdExtractionMode > 1) { storedColdExtractionMode = 0; }
@@ -6116,7 +6165,9 @@ void updateDisplay() {
display.println(F("")); display.println(F(""));
// Meldung auf 21 Zeichen pro Zeile umbrechen (128 px / 6 px Zeichenbreite) // Meldung auf 21 Zeichen pro Zeile umbrechen (128 px / 6 px Zeichenbreite)
const size_t lineLen = 21; 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) { while (rest.length() > 0) {
if (rest.length() <= lineLen) { if (rest.length() <= lineLen) {
display.println(rest); display.println(rest);
@@ -6566,7 +6617,9 @@ if (scaleModeActive) {
// Wenn die Zustandsmaschine nicht aktiv ist, zeige den laufenden Shot oder die Temperaturen // Wenn die Zustandsmaschine nicht aktiv ist, zeige den laufenden Shot oder die Temperaturen
else if (shotActive) { else if (shotActive) {
// Priorität: Shot läuft *aktuell*? // Priorität: Shot läuft *aktuell*?
if (brewByWeightEnabled && scaleConnected) { // Auch ein kalter Bezug hat ein Gewichtsziel - unabhaengig von Brew-by-Weight,
// das die Cold Extraction ersetzt.
if ((brewByWeightEnabled || coldExtractionShotActive) && scaleConnected) {
display.setTextSize(1); display.setTextSize(1);
display.setCursor(0, 0); display.setCursor(0, 0);
display.println(F("Bezug aktiv:")); display.println(F("Bezug aktiv:"));
@@ -6583,7 +6636,7 @@ if (scaleModeActive) {
netWeight = 0.0; netWeight = 0.0;
} }
char weightBuffer[15]; char weightBuffer[15];
snprintf(weightBuffer, sizeof(weightBuffer), "%.1f/%.1fg", netWeight, brewByWeightTargetGrams); snprintf(weightBuffer, sizeof(weightBuffer), "%.1f/%.1fg", netWeight, activeTargetWeightGrams());
// --- ENDE ÄNDERUNG --- // --- ENDE ÄNDERUNG ---
display.setCursor(0, 30); display.setCursor(0, 30);
@@ -7252,6 +7305,14 @@ 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> <small class="toggle-description-input">(Oberhalb dieser Temperatur bleibt der Modus zwar schaltbar, Bez&uuml;ge sind aber gesperrt.)</small>
{CX_STATUS_MSG} {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>
<h3>Vorbenetzung</h3> <h3>Vorbenetzung</h3>
<label for='cxPreInf'>Dauer (Sekunden):</label> <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> <input type='number' min='0.0' max='120.0' step='1.0' id='cxPreInf' name='cxPreInf' value='{CX_PREINF}' required>
@@ -7479,6 +7540,8 @@ struct BrewControlUpdate {
float cxPumpMaxRun = 0.0f; float cxPumpMaxRun = 0.0f;
bool hasCXPumpRest = false; bool hasCXPumpRest = false;
float cxPumpRest = 0.0f; float cxPumpRest = 0.0f;
bool hasCXAskOnWake = false;
bool cxAskOnWake = false;
}; };
bool applyBrewControlUpdate(const BrewControlUpdate& update) { bool applyBrewControlUpdate(const BrewControlUpdate& update) {
@@ -7676,6 +7739,11 @@ bool applyBrewControlUpdate(const BrewControlUpdate& update) {
coldExtractionConfigChanged = true; 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); // "changed" umfasst hier nur noch profil-relevante Felder (BBT/BBW/PI/SBT);
// FlowGuard und Cold Extraction sind kein Bestandteil von TemperatureProfile und // FlowGuard und Cold Extraction sind kein Bestandteil von TemperatureProfile und
@@ -7731,6 +7799,7 @@ void handleColdExtraction(AsyncWebServerRequest *request) {
cxFieldsHtml.replace("{CX_PUMP_MAX_RUN}", buffer); cxFieldsHtml.replace("{CX_PUMP_MAX_RUN}", buffer);
snprintf(buffer, sizeof(buffer), "%.1f", coldExtractionPumpRestSec); snprintf(buffer, sizeof(buffer), "%.1f", coldExtractionPumpRestSec);
cxFieldsHtml.replace("{CX_PUMP_REST}", buffer); cxFieldsHtml.replace("{CX_PUMP_REST}", buffer);
cxFieldsHtml.replace("{CX_ASK_CHECKED}", coldExtractionAskOnWake ? " checked" : "");
if (coldExtractionEnabled) { if (coldExtractionEnabled) {
String cxStatus; String cxStatus;
@@ -7781,6 +7850,8 @@ void handleSaveColdExtraction(AsyncWebServerRequest *request) {
if (update.hasCXPumpMaxRun) { update.cxPumpMaxRun = request->arg("cxPumpMaxRun").toFloat(); } if (update.hasCXPumpMaxRun) { update.cxPumpMaxRun = request->arg("cxPumpMaxRun").toFloat(); }
update.hasCXPumpRest = request->hasArg("cxPumpRest"); update.hasCXPumpRest = request->hasArg("cxPumpRest");
if (update.hasCXPumpRest) { update.cxPumpRest = request->arg("cxPumpRest").toFloat(); } if (update.hasCXPumpRest) { update.cxPumpRest = request->arg("cxPumpRest").toFloat(); }
update.hasCXAskOnWake = true; // Checkbox: fehlt im POST = "aus"
update.cxAskOnWake = request->hasArg("cxAskOnWake");
applyBrewControlUpdate(update); applyBrewControlUpdate(update);
@@ -15737,7 +15808,7 @@ bool executeDashboardAction(const String& action, const String& value,
// Freigabe wird pro Bezug neu geprueft (der Dampfkreis darf weiter heizen und // Freigabe wird pro Bezug neu geprueft (der Dampfkreis darf weiter heizen und
// kann den Wasserkessel ueber die Zeit ueber die Schwelle bringen). // kann den Wasserkessel ueber die Zeit ueber die Schwelle bringen).
message = coldExtractionBlockReason(); 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; coldExtractionBlockMessageUntilMs = millis() + COLDEX_BLOCK_MESSAGE_MS;
coldExtractionBlockMessage = message; coldExtractionBlockMessage = message;
coldExtractionNotice = COLDEX_NOTICE_START_REJECTED; coldExtractionNotice = COLDEX_NOTICE_START_REJECTED;
@@ -16325,7 +16396,7 @@ void buildDashboardJson(char *buffer, size_t bufferSize) {
// Plattformdaten // Plattformdaten
dtostrf(getWeightReadingForUi(), 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"weight\":%s,", floatBuf); dtostrf(getWeightReadingForUi(), 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"weight\":%s,", floatBuf);
dtostrf(brewByWeightTargetGrams, 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"targetWeight\":%s,", floatBuf); dtostrf(activeTargetWeightGrams(), 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"targetWeight\":%s,", floatBuf); // bei Cold Extraction das kalte Ziel
offset += snprintf(buffer + offset, bufferSize - offset, "\"scaleEnabled\":%s,", scaleEnabled ? "true" : "false"); offset += snprintf(buffer + offset, bufferSize - offset, "\"scaleEnabled\":%s,", scaleEnabled ? "true" : "false");
offset += snprintf(buffer + offset, bufferSize - offset, "\"scaleType\":%u,", (unsigned int)scaleType); offset += snprintf(buffer + offset, bufferSize - offset, "\"scaleType\":%u,", (unsigned int)scaleType);
offset += snprintf(buffer + offset, bufferSize - offset, "\"scaleConnected\":%s,", scaleConnected ? "true" : "false"); offset += snprintf(buffer + offset, bufferSize - offset, "\"scaleConnected\":%s,", scaleConnected ? "true" : "false");
@@ -16610,7 +16681,7 @@ static void buildTouchUartStateJson(char *buffer, size_t bufferSize) {
} }
dtostrf(getWeightReadingForUi(), 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"weight\":%s,", floatBuf); dtostrf(getWeightReadingForUi(), 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"weight\":%s,", floatBuf);
dtostrf(brewByWeightTargetGrams, 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"targetWeight\":%s,", floatBuf); dtostrf(activeTargetWeightGrams(), 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"targetWeight\":%s,", floatBuf); // bei Cold Extraction das kalte Ziel
// Endgewicht des letzten Bezugs inkl. Offset (identisch zur OLED: Netto beim Stopp + Offset). // Endgewicht des letzten Bezugs inkl. Offset (identisch zur OLED: Netto beim Stopp + Offset).
// Nur bei gewichtsbasiertem Bezug aussagekraeftig -> lastShotByWeight signalisiert das dem P4. // Nur bei gewichtsbasiertem Bezug aussagekraeftig -> lastShotByWeight signalisiert das dem P4.
dtostrf(lastShotFinalNetWeight + brewByWeightOffsetGrams, 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"lastShotWeight\":%s,", floatBuf); dtostrf(lastShotFinalNetWeight + brewByWeightOffsetGrams, 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"lastShotWeight\":%s,", floatBuf);
@@ -16669,6 +16740,7 @@ static void buildTouchUartStateJson(char *buffer, size_t bufferSize) {
// cxArmable = Modus umschaltbar (auch im Standby und bei warmem Kessel); cxAllowed = Bezug moeglich // 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, "\"cxArmable\":%s,", coldExtractionArmable() ? "true" : "false");
offset += snprintf(buffer + offset, bufferSize - offset, "\"cxResting\":%s,", coldExtractionPumpResting ? "true" : "false"); offset += snprintf(buffer + offset, bufferSize - offset, "\"cxResting\":%s,", coldExtractionPumpResting ? "true" : "false");
offset += snprintf(buffer + offset, bufferSize - offset, "\"cxAskOnWake\":%s,", coldExtractionAskOnWake ? "true" : "false");
dtostrf(coldExtractionMaxTempC, 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"cxMaxTemp\":%s,", floatBuf); 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(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); dtostrf(coldExtractionPreInfusionSeconds, 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"cxPreInf\":%s,", floatBuf);
@@ -17400,13 +17472,17 @@ static bool touchUartHandleCommandLine(const String& line) {
update.hasCXPumpRest = true; update.hasCXPumpRest = true;
update.cxPumpRest = floatVal; update.cxPumpRest = floatVal;
} }
if (jsonGetBoolValue(line, "cxAskOnWake", boolVal)) {
update.hasCXAskOnWake = true;
update.cxAskOnWake = boolVal;
}
if (!update.hasBBTEnabled && !update.hasBBTSecs && !update.hasBBWEnabled && !update.hasBBWTarget && if (!update.hasBBTEnabled && !update.hasBBTSecs && !update.hasBBWEnabled && !update.hasBBWTarget &&
!update.hasBBWOffset && !update.hasPIEnabled && !update.hasPIDurSecs && !update.hasPIPauseSecs && !update.hasBBWOffset && !update.hasPIEnabled && !update.hasPIDurSecs && !update.hasPIPauseSecs &&
!update.hasSBTEnabled && !update.hasSBTSecs && !update.hasSBTEnabled && !update.hasSBTSecs &&
!update.hasCXEnabled && !update.hasCXMaxTemp && !update.hasCXPreInf && !update.hasCXPreInfDuty && !update.hasCXEnabled && !update.hasCXMaxTemp && !update.hasCXPreInf && !update.hasCXPreInfDuty &&
!update.hasCXPulseMs && !update.hasCXDuty && !update.hasCXTarget && !update.hasCXMaxSecs && !update.hasCXPulseMs && !update.hasCXDuty && !update.hasCXTarget && !update.hasCXMaxSecs &&
!update.hasCXPumpMaxRun && !update.hasCXPumpRest) { !update.hasCXPumpMaxRun && !update.hasCXPumpRest && !update.hasCXAskOnWake) {
touchUartSendError(id, "Keine Brew-Felder"); touchUartSendError(id, "Keine Brew-Felder");
return false; return false;
} }
+16
View File
@@ -1,3 +1,19 @@
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
erschienen Gewichtsanzeige und Fortschrittsbalken nur, wenn Brew-by-Weight aktiv war - bei kalter
Extraktion ist das typischerweise aus, obwohl ein Ziel existiert. Beide Anzeigen nutzen jetzt
cxTarget, sobald cxShot gesetzt ist.
Version 1.2.0: Version 1.2.0:
- Neue Seite „Cold Extraction" mit eigenem Menüeintrag (braucht S3-Firmware ab 5.4.0). Dort stehen jetzt - Neue Seite „Cold Extraction" mit eigenem Menüeintrag (braucht S3-Firmware ab 5.4.0). Dort stehen jetzt
alle Parameter der kalten Extraktion und lassen sich direkt am Display ändern: Freigabetemperatur, alle Parameter der kalten Extraktion und lassen sich direkt am Display ändern: Freigabetemperatur,
+1 -1
View File
@@ -65,7 +65,7 @@
// ------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------
// Firmware // Firmware
// ------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------
#define DISPLAY_FW_VERSION "1.2.0" // Firmware-Stand der P4-Display-Steuerung (Info-Seite) #define DISPLAY_FW_VERSION "1.3.0" // Firmware-Stand der P4-Display-Steuerung (Info-Seite)
// ------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------
// Protokoll // Protokoll
+1
View File
@@ -84,6 +84,7 @@ struct MachineState {
uint16_t cxPulseMs = 0; // Pulsperiode in ms uint16_t cxPulseMs = 0; // Pulsperiode in ms
float cxPumpMaxRun = 0; // Pumpenschutz: max. kumulierte Laufzeit in s float cxPumpMaxRun = 0; // Pumpenschutz: max. kumulierte Laufzeit in s
float cxPumpRest = 0; // Pumpenschutz: Zwangspause in s float cxPumpRest = 0; // Pumpenschutz: Zwangspause in s
bool cxAskOnWake = false; // Beim Aufwecken aus dem Standby fragen (seit S3 5.5.0)
float cxMaxSecs = 0; // Sicherheits-Timeout des Bezugs in s float cxMaxSecs = 0; // Sicherheits-Timeout des Bezugs in s
// Sperrgrund: 0=frei, 1=nicht freigeschaltet, 2=Standby, 3=Wartung, 4=Reinigungsassistent, // Sperrgrund: 0=frei, 1=nicht freigeschaltet, 2=Standby, 3=Wartung, 4=Reinigungsassistent,
// 5=PID-Tuning, 6=Sensorfehler, 7=Wasserkessel zu warm // 5=PID-Tuning, 6=Sensorfehler, 7=Wasserkessel zu warm
+1
View File
@@ -236,6 +236,7 @@ void ProtocolClient::applyState(const String& json) {
s.cxPulseMs = doc["cxPulseMs"] | s.cxPulseMs; s.cxPulseMs = doc["cxPulseMs"] | s.cxPulseMs;
s.cxPumpMaxRun = doc["cxPumpMaxRun"] | s.cxPumpMaxRun; s.cxPumpMaxRun = doc["cxPumpMaxRun"] | s.cxPumpMaxRun;
s.cxPumpRest = doc["cxPumpRest"] | s.cxPumpRest; s.cxPumpRest = doc["cxPumpRest"] | s.cxPumpRest;
s.cxAskOnWake = doc["cxAskOnWake"] | false;
s.cxMaxSecs = doc["cxMaxSecs"] | s.cxMaxSecs; s.cxMaxSecs = doc["cxMaxSecs"] | s.cxMaxSecs;
s.cxBlock = doc["cxBlock"] | (uint8_t)0; s.cxBlock = doc["cxBlock"] | (uint8_t)0;
s.cxNotice = doc["cxNotice"] | (uint8_t)0; s.cxNotice = doc["cxNotice"] | (uint8_t)0;
+46 -18
View File
@@ -38,7 +38,9 @@ static lv_timer_t* g_toastTimer = nullptr;
static lv_obj_t* g_standbyScreen = nullptr; // Vollbild-Overlay im Standby (Uhr) static lv_obj_t* g_standbyScreen = nullptr; // Vollbild-Overlay im Standby (Uhr)
static lv_obj_t* g_standbyClock = nullptr; static lv_obj_t* g_standbyClock = nullptr;
static lv_obj_t* g_wakeConfirm = nullptr; // Bestaetigungsdialog "Aus dem Standby aufwecken?" 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 bool g_wakeDialogOpen = false; // Dialog sichtbar -> Display hell schalten (lesbar) 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) static bool g_standbyShown = false; // Standby-Uhr-Overlay zuletzt als sichtbar geschaltet (fuer Ein-/Ausblend-Uebergang)
@@ -258,6 +260,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* taCxMaxSecs = nullptr; static lv_obj_t* taCxPumpMaxRun = nullptr;
static lv_obj_t* taCxPumpRest = nullptr; static lv_obj_t* taCxPumpRest = nullptr;
static lv_obj_t* swCxEnabled = nullptr; // Freischaltung (entspricht dem Web-Toggle) 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 lv_obj_t* lblCxPageHint = nullptr;
static uint32_t g_cxEditMs = 0; // waehrend der Eingabe die Felder nicht ueberschreiben static uint32_t g_cxEditMs = 0; // waehrend der Eingabe die Felder nicht ueberschreiben
@@ -1858,12 +1861,16 @@ static void brew_live_tick() {
} }
// Zielfortschritt: Brew-by-Weight (Gewicht) vor Brew-by-Time (Zeit), sonst versteckt // Zielfortschritt: Brew-by-Weight (Gewicht) vor Brew-by-Time (Zeit), sonst versteckt
if (g_bzBarRow && g_bzBar && g_bzBarLbl) { if (g_bzBarRow && g_bzBar && g_bzBarLbl) {
bool bbw = scale && g_state.bbwEnabled && g_state.bbwTarget > 0.0f; // Ein kalter Bezug hat sein eigenes Ziel (cxTarget) und ersetzt Brew-by-Weight -
// sonst stuende hier das Espresso-Ziel.
const bool cxShotNow = g_state.cxShot && g_state.cxTarget > 0.0f;
const float wTarget = cxShotNow ? g_state.cxTarget : g_state.bbwTarget;
bool bbw = scale && (cxShotNow || (g_state.bbwEnabled && g_state.bbwTarget > 0.0f));
if (bbw || (g_shotBbtMode && g_bbtTarget > 0.0f)) { if (bbw || (g_shotBbtMode && g_bbtTarget > 0.0f)) {
int pct; int pct;
if (bbw) { if (bbw) {
pct = (int)(w / g_state.bbwTarget * 100.0f + 0.5f); pct = (int)(w / wTarget * 100.0f + 0.5f);
snprintf(b, sizeof(b), "%.1f g / %.1f g", w, g_state.bbwTarget); snprintf(b, sizeof(b), "%.1f g / %.1f g", w, wTarget);
} else { } else {
pct = (int)(el / g_bbtTarget * 100.0f + 0.5f); pct = (int)(el / g_bbtTarget * 100.0f + 0.5f);
snprintf(b, sizeof(b), "%.1f s / %.1f s", el, g_bbtTarget); snprintf(b, sizeof(b), "%.1f s / %.1f s", el, g_bbtTarget);
@@ -2072,7 +2079,8 @@ static void send_save_coldex(lv_event_t*) {
f += "\"cxTarget\":"; f += String(ta_f(taCxTarget), 1); f += ","; f += "\"cxTarget\":"; f += String(ta_f(taCxTarget), 1); f += ",";
f += "\"cxMaxSecs\":"; f += String(ta_f(taCxMaxSecs), 1); f += ","; f += "\"cxMaxSecs\":"; f += String(ta_f(taCxMaxSecs), 1); f += ",";
f += "\"cxPumpMaxRun\":"; f += String(ta_f(taCxPumpMaxRun), 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_client->sendCommand("saveBrew", f);
g_cxEditMs = millis(); g_cxEditMs = millis();
showToast("Speichere Cold Extraction...", false); showToast("Speichere Cold Extraction...", false);
@@ -2092,6 +2100,7 @@ static void build_coldex(lv_obj_t* p) {
lv_obj_t* cBase = group_card(p, "Grundeinstellung"); lv_obj_t* cBase = group_card(p, "Grundeinstellung");
swCxEnabled = add_switch(cBase, "Funktion freigeschaltet"); swCxEnabled = add_switch(cBase, "Funktion freigeschaltet");
taCxMaxTemp = add_num(cBase, "Freigabe bis (°C)"); taCxMaxTemp = add_num(cBase, "Freigabe bis (°C)");
swCxAskOnWake = add_switch(cBase, "Beim Aufwecken fragen");
lv_obj_t* cPre = group_card(p, "Vorbenetzung", lv_obj_t* cPre = group_card(p, "Vorbenetzung",
"Deutlich schwächer als der Hauptbezug: Kaltes Mehl quillt nicht, volle Leistung legt " "Deutlich schwächer als der Hauptbezug: Kaltes Mehl quillt nicht, volle Leistung legt "
@@ -2128,6 +2137,7 @@ static void build_coldex(lv_obj_t* p) {
static void populate_coldex(const MachineState& st) { static void populate_coldex(const MachineState& st) {
if (!taCxMaxTemp) return; if (!taCxMaxTemp) return;
sw_set(swCxEnabled, st.cxEnabled); sw_set(swCxEnabled, st.cxEnabled);
sw_set(swCxAskOnWake, st.cxAskOnWake);
ta_setf(taCxMaxTemp, st.cxMaxTemp, 1); ta_setf(taCxMaxTemp, st.cxMaxTemp, 1);
ta_setf(taCxPreInf, st.cxPreInfSec, 0); ta_setf(taCxPreInf, st.cxPreInfSec, 0);
ta_setf(taCxPreInfDuty, st.cxPreInfDuty, 0); ta_setf(taCxPreInfDuty, st.cxPreInfDuty, 0);
@@ -3320,12 +3330,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_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_set_style_pad_row(wakeCard, 18, 0);
lv_obj_clear_flag(wakeCard, LV_OBJ_FLAG_SCROLLABLE); lv_obj_clear_flag(wakeCard, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* wakeMsg = lv_label_create(wakeCard); g_wakeMsg = lv_label_create(wakeCard);
lv_obj_set_width(wakeMsg, LV_PCT(100)); lv_obj_set_width(g_wakeMsg, LV_PCT(100));
lv_obj_set_style_text_align(wakeMsg, LV_TEXT_ALIGN_CENTER, 0); lv_obj_set_style_text_align(g_wakeMsg, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_style_text_color(wakeMsg, COL_TEXT, 0); lv_obj_set_style_text_color(g_wakeMsg, COL_TEXT, 0);
lv_obj_set_style_text_font(wakeMsg, &lv_font_maven_pro_18, 0); lv_obj_set_style_text_font(g_wakeMsg, &lv_font_maven_pro_18, 0);
lv_label_set_text(wakeMsg, "Die Maschine befindet sich im Standby.\nAus dem Standby aufwecken?"); 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_t* wakeBtns = lv_obj_create(wakeCard);
lv_obj_set_width(wakeBtns, LV_PCT(100)); lv_obj_set_width(wakeBtns, LV_PCT(100));
lv_obj_set_height(wakeBtns, LV_SIZE_CONTENT); lv_obj_set_height(wakeBtns, LV_SIZE_CONTENT);
@@ -3342,10 +3352,11 @@ void ui_init(ProtocolClient* client) {
lv_obj_t* wakeYes = th_accent_button(wakeBtns, "Aufwecken"); lv_obj_t* wakeYes = th_accent_button(wakeBtns, "Aufwecken");
lv_obj_set_width(wakeYes, 150); lv_obj_set_width(wakeYes, 150);
lv_obj_add_event_cb(wakeYes, wake_yes_cb, LV_EVENT_CLICKED, nullptr); 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 // Zweite Zeile: direkt in die kalte Extraktion aufwecken. Ohne diesen Weg muesste man
// erst normal aufwecken (Maschine heizt an) und den Modus danach umschalten. // erst normal aufwecken (Maschine heizt an) und den Modus danach umschalten.
// Nur sichtbar, wenn die S3 die Funktion freigeschaltet meldet (siehe ui_update). // 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_set_width(g_wakeColdBtn, LV_PCT(100));
lv_obj_add_event_cb(g_wakeColdBtn, wake_cold_cb, LV_EVENT_CLICKED, nullptr); 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_wakeColdBtn, LV_OBJ_FLAG_HIDDEN);
@@ -3576,9 +3587,23 @@ void ui_update(const MachineState& st) {
const bool cxScaleOk = st.scaleEnabled && st.scaleConnected; const bool cxScaleOk = st.scaleEnabled && st.scaleConnected;
// "Aufwecken mit Cold Extraction" nur anbieten, wenn die Funktion freigeschaltet und // "Aufwecken mit Cold Extraction" nur anbieten, wenn die Funktion freigeschaltet und
// umschaltbar ist und der Modus nicht ohnehin schon laeuft. // umschaltbar ist und der Modus nicht ohnehin schon laeuft.
if (g_wakeColdBtn) { // Abfrage-Modus: Der Aufweck-Dialog wird zur Auswahl "Espresso oder Cold Extraction".
if (st.cxEnabled && st.cxArmable && !st.cxActive) lv_obj_remove_flag(g_wakeColdBtn, LV_OBJ_FLAG_HIDDEN); // Abschaltbar ueber die Einstellung cxAskOnWake, damit man sich den Zwischenschritt
else lv_obj_add_flag(g_wakeColdBtn, LV_OBJ_FLAG_HIDDEN); // sparen kann, wenn gerade keine kalten Bezuege anstehen.
{
const bool askMode = 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);
}
if (g_wakeMsg) {
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");
}
} }
if (st.cxNotice == 2) { if (st.cxNotice == 2) {
status_set(ST_COLDEX, true, LV_SYMBOL_WARNING " Cold Extraction beendet: kein Zulauf", status_set(ST_COLDEX, true, LV_SYMBOL_WARNING " Cold Extraction beendet: kein Zulauf",
@@ -3915,7 +3940,10 @@ void ui_update(const MachineState& st) {
else lv_obj_add_flag(cellBezug, LV_OBJ_FLAG_HIDDEN); else lv_obj_add_flag(cellBezug, LV_OBJ_FLAG_HIDDEN);
} }
if (lblBezug) { if (lblBezug) {
bool bbwShot = st.shotActive && st.bbwEnabled && st.bbwTarget > 0.0f; // Bei kaltem Bezug zaehlt cxTarget, nicht das Brew-by-Weight-Ziel
const bool cxShotNow = st.cxShot && st.cxTarget > 0.0f;
const float shotTarget = cxShotNow ? st.cxTarget : st.bbwTarget;
bool bbwShot = st.shotActive && (cxShotNow || (st.bbwEnabled && st.bbwTarget > 0.0f));
// Winzige negative Messwerte (z.B. -0.04 g) oder negatives Float-Null wuerden von // Winzige negative Messwerte (z.B. -0.04 g) oder negatives Float-Null wuerden von
// "%.1f" als "-0.0" dargestellt -> auf 0.0 normalisieren (kein fuehrendes Minus). // "%.1f" als "-0.0" dargestellt -> auf 0.0 normalisieren (kein fuehrendes Minus).
float wDisp = st.weight; float wDisp = st.weight;
@@ -3924,14 +3952,14 @@ void ui_update(const MachineState& st) {
lv_label_set_text(lblBezug, "--"); lv_label_set_text(lblBezug, "--");
} else if (bbwShot) { } else if (bbwShot) {
// Waehrend des Bezugs mit Brew-by-Weight: Ist oben, Ziel darunter (zweizeilig) // Waehrend des Bezugs mit Brew-by-Weight: Ist oben, Ziel darunter (zweizeilig)
lv_label_set_text_fmt(lblBezug, "%.1f g\n/ %.1f g", wDisp, st.bbwTarget); lv_label_set_text_fmt(lblBezug, "%.1f g\n/ %.1f g", wDisp, shotTarget);
} else { } else {
lv_label_set_text_fmt(lblBezug, "%.1f g", wDisp); lv_label_set_text_fmt(lblBezug, "%.1f g", wDisp);
} }
// Fortschrittsbalken Gewicht/Ziel (nur waehrend BBW-Bezug) // Fortschrittsbalken Gewicht/Ziel (nur waehrend BBW-Bezug)
if (g_bbwBar) { if (g_bbwBar) {
if (bbwShot) { if (bbwShot) {
int pct = (int)(st.weight / st.bbwTarget * 100.0f + 0.5f); int pct = (int)(st.weight / shotTarget * 100.0f + 0.5f);
if (pct < 0) pct = 0; if (pct > 100) pct = 100; if (pct < 0) pct = 0; if (pct > 100) pct = 100;
lv_bar_set_value(g_bbwBar, pct, LV_ANIM_OFF); lv_bar_set_value(g_bbwBar, pct, LV_ANIM_OFF);
lv_obj_remove_flag(g_bbwBar, LV_OBJ_FLAG_HIDDEN); lv_obj_remove_flag(g_bbwBar, LV_OBJ_FLAG_HIDDEN);