diff --git a/Changelog.txt b/Changelog.txt
index aa22c0f..55c6c99 100644
--- a/Changelog.txt
+++ b/Changelog.txt
@@ -1,3 +1,34 @@
+Version 5.3.0:
+- Neu: Cold Extraction (kalte Extraktion). Die Maschine hat keinen Bypass um den Wasserkessel, kaltes Wasser
+ kommt also nur bei kaltem Kessel am Puck an. Die Funktion wird deshalb über eine Temperaturschwelle
+ freigegeben (Standard 30 °C, einstellbar 10–40 °C) und ist praktisch aus dem kalten Zustand heraus nutzbar.
+- Solange der Modus aktiv ist, heizt der Wasserkreis nicht (SSR gesperrt, Stellgröße und damit auch die
+ dutyW-Telemetrie auf 0, I-Anteil der Wasser-PID eingefroren, Feed-Forward-Boost deaktiviert). Der
+ Dampfkreis bleibt unberührt und darf weiter heizen.
+- Die Freigabe wird vor jedem Bezug neu geprüft, nicht nur beim Aktivieren: Da der Dampfkessel weiter heizt,
+ kann der Wasserkessel über die Zeit über die Schwelle steigen. In diesem Fall wird der nächste Bezug
+ abgewiesen (Meldung auf OLED, Touch-Display und im Web-Dashboard) - ein laufender Bezug wird nie abgebrochen.
+ Hysterese von 5 °C, damit die Anzeige an der Schwelle nicht flattert; Sensorfehler sperrt die Funktion.
+- Der Fluss wird über Pumpen-Pulsung reduziert (Pulsperiode 200–5000 ms, Pumpenleistung 5–100 %), davor läuft
+ eine einstellbare Vorbenetzung mit voller Pumpenleistung. Bezugsende über Zielgewicht oder Sicherheits-Timeout;
+ Brew-by-Time/Brew-by-Weight und FlowGuard sind während einer kalten Extraktion bewusst außer Kraft (FlowGuard
+ und Cold Extraction würden sonst um denselben Pumpen-Ausgang konkurrieren).
+- Pumpenschutz: Die Steuerung zählt die kumulierte Pumpen-Einschaltzeit und legt nach der eingestellten
+ Laufzeit eine Zwangspause ein (Standard 60 s Laufzeit / 20 s Pause). Zusätzlich Stillstands-Erkennung -
+ läuft die Pumpe bei aktiver Waage 15 s ohne Zulauf, wird der Bezug beendet (dichter Puck oder ausgelöster
+ Pumpen-Thermoschutz).
+- Bedienung: Parameter auf der Seite /Brew-Control, Ein-/Ausschalten über das Web-Dashboard oder das
+ UART-Touch-Display (neue Aktionen "startColdExtraction"/"stopColdExtraction"/"toggleColdExtraction").
+ Cold Extraction ist bewusst kein Profilfeld (maschinen- und situationsabhängig, wie der Feed-Forward);
+ der Modus selbst wird nicht persistiert und ist nach einem Neustart immer aus. Standby beendet ihn.
+- Statistik: Kalte Bezüge werden im Log mit einem vierten CSV-Feld "C" gekennzeichnet, zählen in allen
+ Stückzahlen mit, gehen aber NICHT in die durchschnittliche Bezugsdauer ein (Minuten statt Sekunden würden
+ den Wert verzerren). Die Info-Seite weist die Anzahl separat aus, der Verlaufs-Download hat eine neue
+ Spalte "Art". Ältere Logzeilen bleiben unverändert lesbar.
+- Neue additive State-Felder im UART-JSON (Protokoll bleibt v2): cxEnabled, cxActive, cxShot, cxAllowed,
+ cxResting, cxMaxTemp, cxTarget, cxMaxSecs, cxMessage sowie "cx" pro Eintrag in usageStats; neuer
+ statusKey "coldextraction". Ältere Display-Firmware ignoriert die Felder.
+
Version 5.2.0:
- Feed-Forward bei Bezug (Störgrößenaufschaltung) für den Wasserkessel: Während eines Bezugs wird der
Heizung sofort ein einstellbarer Leistungs-Boost (0–100 % der Fenstergröße, 0 % = aus) aufgeschaltet,
diff --git a/Doku/JC-Display_UART-Protokoll.md b/Doku/JC-Display_UART-Protokoll.md
index 18f953c..235cc2b 100644
--- a/Doku/JC-Display_UART-Protokoll.md
+++ b/Doku/JC-Display_UART-Protokoll.md
@@ -315,6 +315,23 @@ Alle gültigen `action`-Strings aus `executeDashboardAction()` (Stand v4.8.1, Ze
| `disableSteamHeat` | — | Dampf-Heizung abschalten |
| `enableSteamHeat` | — | Dampf-Heizung wieder aktivieren |
+**Cold Extraction (kalte Extraktion, ab S3 5.3.0)**
+
+| `action` | `value` | Wirkung |
+|---|---|---|
+| `startColdExtraction` | — | Modus aktivieren (Wasserkreis heizt nicht, Dampf bleibt möglich) |
+| `stopColdExtraction` | — | Modus beenden |
+| `toggleColdExtraction` | — | Modus umschalten |
+
+Alle drei Aktionen werden abgewiesen (`ok:false` + `message`), solange die Bedingungen nicht
+erfüllt sind — Hauptfall ist ein zu warmer Wasserkessel (`cxAllowed:false`). Der Modus wird
+**nicht** persistiert und ist nach jedem Neustart aus; Standby beendet ihn. Ein laufender
+Bezug wird nie abgebrochen, `stopColdExtraction` schlägt dann fehl.
+
+Die Parameter der kalten Extraktion (Schwelle, Vorbenetzung, Pulsperiode, Pumpenleistung,
+Zielgewicht, Timeout, Pumpenschutz) sind bewusst **nicht** über `saveBrew` erreichbar, sondern
+nur über die Web-Seite `/Brew-Control`. Das Display schaltet den Modus, konfiguriert ihn nicht.
+
**Persistente Toggles**
| `action` | `value` | Wirkung |
@@ -354,7 +371,28 @@ Aus `getTouchUartStatus()`. Für sprach-unabhängige UI-Logik/Icons auf dem P4 v
(statt `statusText` zu parsen):
`ready`, `error`, `standby`, `maintenance`, `tuning`, `brewing`, `eco`, `fastheatup`,
-`heating`, `ready_eco_pending`.
+`heating`, `ready_eco_pending`, `coldextraction` (ab S3 5.3.0).
+
+### Cold-Extraction-Felder im `state` (ab S3 5.3.0, additiv — Protokoll bleibt v2)
+
+| Feld | Typ | Bedeutung |
+|---|---|---|
+| `cxEnabled` | bool | Funktion in der Web-UI freigeschaltet (sonst Bedienelement ausblenden) |
+| `cxActive` | bool | Modus scharf: Wasserkreis heizt nicht |
+| `cxShot` | bool | Der laufende Bezug ist eine kalte Extraktion |
+| `cxAllowed` | bool | Freigabe **jetzt** (Wasserkessel kalt genug, kein Standby/Wartung/Tuning) |
+| `cxResting` | bool | Zwangspause des Pumpenschutzes läuft (Bezug läuft weiter) |
+| `cxMaxTemp` | float | Freigabeschwelle in °C (für eigene Hinweistexte am Display) |
+| `cxTarget` | float | Zielgewicht in g |
+| `cxMaxSecs` | float | Sicherheits-Timeout des Bezugs in s |
+| `cxMessage` | string | Sperr-/Abbruchmeldung, leer = keine (läuft nach ~6 s selbst ab) |
+
+Während `cxShot` folgt die Phase weiterhin `piState`: `1` = Vorbenetzung, `3` = Hauptbezug
+(gepulst). Eine Pause-Phase (`2`) gibt es bei der kalten Extraktion nicht.
+
+In der Antwort auf `getUsageStats` trägt jeder Eintrag in `shots[]` zusätzlich `cx` (bool) —
+kalte Bezüge zählen in `total`/`today`/`week` mit, sind aber in der durchschnittlichen
+Bezugsdauer der S3-Statistik ausgenommen.
---
diff --git a/Dual-PID.ino b/Dual-PID.ino
index 92264af..81e3f32 100644
--- a/Dual-PID.ino
+++ b/Dual-PID.ino
@@ -175,7 +175,7 @@ unsigned long lastDashboardWsPushMs = 0;
#define TOUCH_UART_BAUDRATE 230400
#define TOUCH_UART_MAX_LINE_LEN 1024
#define TOUCH_UART_PROTOCOL_VERSION 2 // v2: + Netzwerk-/PID-/Duty-/AutoTune-Telemetrie, hello-Handshake
-#define TOUCH_UART_JSON_BUFFER_SIZE 3000 // v2: groesserer Headroom fuer die zusaetzlichen Felder
+#define TOUCH_UART_JSON_BUFFER_SIZE 3400 // v2: groesserer Headroom fuer die zusaetzlichen Felder (inkl. Cold Extraction)
#define DEFAULT_FULLY_KIOSK_ENABLED 0
#define DEFAULT_FULLY_KIOSK_HOST ""
@@ -369,7 +369,7 @@ Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire);
* Firmware-Informationen
************************************************************************************/
-String version = "5.2.0";
+String version = "5.3.0";
String versionHersteller = "Thomas Müller";
String versionHerstellerMail =
"thomas@mueller.black";
@@ -696,6 +696,10 @@ struct ShotStats {
unsigned long shotsLastWeek = 0; // Kalenderwoche
unsigned long shotsLastMonth = 0; // Kalendermonat
double avgShotsPerDay = 0.0; // Durchschnitt seit erstem Log
+ // Kalte Extraktionen zaehlen in allen Stueckzahlen mit, gehen aber bewusst NICHT in
+ // die durchschnittliche Bezugsdauer ein (Minuten statt Sekunden verzerren den Wert).
+ unsigned long coldExtractionShots = 0;
+ unsigned long durationCountedShots = 0;
};
/************************************************************************************
@@ -767,6 +771,72 @@ float flowGuardLastNetWeight = NAN;
unsigned long flowGuardLastSampleMs = 0;
bool flowGuardPumpPulseActive = false;
+/************************************************************************************
+ * Cold Extraction (kalte Extraktion) - Einstellungen & Laufzeitzustand
+ ************************************************************************************
+ * Die Maschine hat keinen Bypass um den Wasserkessel: Kaltes Wasser kommt nur dann am
+ * Puck an, wenn der Wasserkessel selbst kalt ist. Deshalb wird die Funktion ueber eine
+ * Temperaturschwelle freigegeben (Default 30 C) und der Wasserkreis heizt waehrend des
+ * Modus nicht (der Dampfkreis bleibt unangetastet und darf weiter heizen).
+ * Der Fluss wird ueber Pumpen-Pulsung reduziert, der Bezug laeuft ueber Minuten.
+ ************************************************************************************/
+bool coldExtractionEnabled = false; // Funktion in der Web-UI freigeschaltet
+float coldExtractionMaxTempC = 30.0f; // Freigabeschwelle Wasserkessel
+float coldExtractionPreInfusionSeconds = 30.0f; // Vorbenetzung (Pumpe laeuft ungepulst)
+uint16_t coldExtractionPulsePeriodMs = 2000; // Pulsperiode der Pumpe im Hauptbezug
+float coldExtractionDutyPercent = 30.0f; // Pumpen-Duty im Hauptbezug
+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
+
+const bool defaultColdExtractionEnabled = false;
+const float defaultColdExtractionMaxTempC = 30.0f;
+const float defaultColdExtractionPreInfusionSeconds = 30.0f;
+const uint16_t defaultColdExtractionPulsePeriodMs = 2000;
+const float defaultColdExtractionDutyPercent = 30.0f;
+const float defaultColdExtractionTargetGrams = 60.0f;
+const float defaultColdExtractionMaxSeconds = 300.0f;
+const float defaultColdExtractionPumpMaxRunSec = 60.0f;
+const float defaultColdExtractionPumpRestSec = 20.0f;
+
+const float COLDEX_MAX_TEMP_MIN = 10.0f;
+const float COLDEX_MAX_TEMP_MAX = 40.0f;
+const float COLDEX_TEMP_RELEASE_MARGIN_C = 5.0f; // Hysterese: Sperre erst ueber Schwelle + Marge zurueck
+const float COLDEX_PREINF_SEC_MIN = 0.0f;
+const float COLDEX_PREINF_SEC_MAX = 120.0f;
+const uint16_t COLDEX_PULSE_PERIOD_MIN_MS = 200;
+const uint16_t COLDEX_PULSE_PERIOD_MAX_MS = 5000;
+const float COLDEX_DUTY_MIN = 5.0f;
+const float COLDEX_DUTY_MAX = 100.0f;
+const float COLDEX_TARGET_GRAMS_MIN = 10.0f;
+const float COLDEX_TARGET_GRAMS_MAX = 300.0f;
+const float COLDEX_MAX_SECONDS_MIN = 30.0f;
+const float COLDEX_MAX_SECONDS_MAX = 900.0f;
+const float COLDEX_PUMP_MAX_RUN_SEC_MIN = 10.0f;
+const float COLDEX_PUMP_MAX_RUN_SEC_MAX = 300.0f;
+const float COLDEX_PUMP_REST_SEC_MIN = 0.0f;
+const float COLDEX_PUMP_REST_SEC_MAX = 120.0f;
+// Stillstands-Erkennung: Pumpe laeuft, aber die Waage sieht keinen Zulauf -> Bezug beenden
+// (faengt einen ausgeloesten Pumpen-Thermoschutz und einen verstopften Puck ab).
+const unsigned long COLDEX_STALL_TIMEOUT_MS = 15000UL;
+const float COLDEX_STALL_MIN_WEIGHT_DELTA_G = 0.3f;
+
+bool coldExtractionModeActive = false; // Modus scharf: Wasserkreis heizt nicht
+bool coldExtractionShotActive = false; // der laufende Bezug ist eine kalte Extraktion
+bool coldExtractionPumpResting = false; // Zwangspause der Pumpe laeuft
+bool coldExtractionPumpOutputOn = false; // letzter an die Pumpe geschriebener Zustand
+unsigned long coldExtractionPumpRunMs = 0; // kumulierte Pumpen-Einschaltzeit seit letzter Pause
+unsigned long coldExtractionRestUntilMs = 0;
+unsigned long coldExtractionPumpSampleMs = 0;
+float coldExtractionStallLastWeight = NAN;
+unsigned long coldExtractionStallSinceMs = 0;
+bool lastShotWasColdExtraction = false; // fuer Anzeige/Log des letzten Bezugs
+// Sperrmeldung fuer OLED und Touch-Display, wenn ein kalter Bezug abgewiesen wurde
+const unsigned long COLDEX_BLOCK_MESSAGE_MS = 6000UL;
+String coldExtractionBlockMessage = "";
+unsigned long coldExtractionBlockMessageUntilMs = 0;
+
/************************************************************************************
* Standardwerte und Default-Einstellungen
************************************************************************************/
@@ -957,7 +1027,16 @@ const int EEPROM_ADDR_ACTIVE_PROFILE_NAME = 912; // 31 Bytes (char[31], nul
const int EEPROM_ADDR_ACTIVE_PROFILE_DIRTY = 943; // 1 Byte (bool) -> Ende 943
const int EEPROM_ADDR_FF_BOOST_PERCENT_WASSER = 944; // 1 Byte (uint8_t) -> Ende 944
const int EEPROM_ADDR_FF_IFREEZE_WASSER = 945; // 1 Byte (bool) -> Ende 945
-// Naechste freie Adresse: 946 (Innerhalb EEPROM_SIZE=1024)
+const int EEPROM_ADDR_COLDEX_ENABLED = 946; // 1 Byte (bool) -> Ende 946
+const int EEPROM_ADDR_COLDEX_MAX_TEMP = 947; // 4 Bytes (float) -> Ende 950
+const int EEPROM_ADDR_COLDEX_PREINF_SEC = 951; // 4 Bytes (float) -> Ende 954
+const int EEPROM_ADDR_COLDEX_PULSE_PERIOD_MS = 955; // 2 Bytes (uint16_t) -> Ende 956
+const int EEPROM_ADDR_COLDEX_DUTY = 957; // 4 Bytes (float) -> Ende 960
+const int EEPROM_ADDR_COLDEX_TARGET_GRAMS = 961; // 4 Bytes (float) -> Ende 964
+const int EEPROM_ADDR_COLDEX_MAX_SECONDS = 965; // 4 Bytes (float) -> Ende 968
+const int EEPROM_ADDR_COLDEX_PUMP_MAX_RUN = 969; // 4 Bytes (float) -> Ende 972
+const int EEPROM_ADDR_COLDEX_PUMP_REST = 973; // 4 Bytes (float) -> Ende 976
+// Naechste freie Adresse: 977 (Innerhalb EEPROM_SIZE=1024)
/************************************************************************************
* Eco-Mode Variablen
@@ -1272,6 +1351,7 @@ unsigned long savedShotDuration = 0;
// --- Zusätzliche Variablen für das Speichern des Gewichts ---
bool savedShotWasByWeight = false; // War der zu speichernde Shot ein "By-Weight"-Shot?
float savedShotFinalWeight = 0.0f; // Das finale Gewicht des zu speichernden Shots
+bool savedShotWasColdExtraction = false; // War der zu speichernde Shot eine kalte Extraktion?
/************************************************************************************
* Wartungserinnerung Variablen
@@ -2938,6 +3018,15 @@ void resetToDefaults() {
EEPROM.put(EEPROM_ADDR_FLOWGUARD_MIN_DUTY, defaultFlowGuardMinDutyPercent);
EEPROM.put(EEPROM_ADDR_STEAMBYTIME_ENABLED, defaultSteamByTimeEnabled);
EEPROM.put(EEPROM_ADDR_STEAMBYTIME_SECONDS, defaultSteamByTimeTargetSeconds);
+ EEPROM.put(EEPROM_ADDR_COLDEX_ENABLED, defaultColdExtractionEnabled);
+ EEPROM.put(EEPROM_ADDR_COLDEX_MAX_TEMP, defaultColdExtractionMaxTempC);
+ EEPROM.put(EEPROM_ADDR_COLDEX_PREINF_SEC, defaultColdExtractionPreInfusionSeconds);
+ EEPROM.put(EEPROM_ADDR_COLDEX_PULSE_PERIOD_MS, defaultColdExtractionPulsePeriodMs);
+ EEPROM.put(EEPROM_ADDR_COLDEX_DUTY, defaultColdExtractionDutyPercent);
+ EEPROM.put(EEPROM_ADDR_COLDEX_TARGET_GRAMS, defaultColdExtractionTargetGrams);
+ EEPROM.put(EEPROM_ADDR_COLDEX_MAX_SECONDS, defaultColdExtractionMaxSeconds);
+ EEPROM.put(EEPROM_ADDR_COLDEX_PUMP_MAX_RUN, defaultColdExtractionPumpMaxRunSec);
+ EEPROM.put(EEPROM_ADDR_COLDEX_PUMP_REST, defaultColdExtractionPumpRestSec);
// Piezo auf Default im EEPROM schreiben
EEPROM.put(EEPROM_ADDR_PIEZO_ENABLED, defaultPiezoEnabled);
@@ -3016,6 +3105,17 @@ void resetToDefaults() {
flowGuardLastNetWeight = NAN;
flowGuardLastSampleMs = 0;
flowGuardPumpPulseActive = false;
+ coldExtractionEnabled = defaultColdExtractionEnabled;
+ coldExtractionMaxTempC = defaultColdExtractionMaxTempC;
+ coldExtractionPreInfusionSeconds = defaultColdExtractionPreInfusionSeconds;
+ coldExtractionPulsePeriodMs = defaultColdExtractionPulsePeriodMs;
+ coldExtractionDutyPercent = defaultColdExtractionDutyPercent;
+ coldExtractionTargetGrams = defaultColdExtractionTargetGrams;
+ coldExtractionMaxSeconds = defaultColdExtractionMaxSeconds;
+ coldExtractionPumpMaxRunSec = defaultColdExtractionPumpMaxRunSec;
+ coldExtractionPumpRestSec = defaultColdExtractionPumpRestSec;
+ coldExtractionModeActive = false;
+ coldExtractionShotActive = false;
steamByTimeEnabled = defaultSteamByTimeEnabled; steamByTimeTargetSeconds = defaultSteamByTimeTargetSeconds;
// Globale Piezo Variable auf Default setzen
@@ -3786,6 +3886,14 @@ void updateFlowGuardFlowEstimate(float netWeight, unsigned long nowMs) {
}
void applyFlowGuardPumpControl(unsigned long currentShotTimeMillis, float netWeight, float targetWeightToStopAt) {
+ // Cold Extraction steuert die Pumpe exklusiv ueber die eigene Pulslogik. FlowGuard
+ // muss hier vor jedem digitalWrite(PUMP_PIN, ...) aussteigen, sonst kaempfen beide
+ // Regler um denselben Ausgang.
+ if (coldExtractionShotActive) {
+ flowGuardPumpPulseActive = false;
+ return;
+ }
+
if (currentPreInfusionState != PI_MAIN_BREW) {
flowGuardPumpPulseActive = false;
return;
@@ -3876,6 +3984,196 @@ void applyFlowGuardPumpControl(unsigned long currentShotTimeMillis, float netWei
digitalWrite(PUMP_PIN, pumpOn ? HIGH : LOW);
}
+/************************************************************************************
+ * Cold Extraction (kalte Extraktion)
+ ************************************************************************************/
+
+void resetColdExtractionRuntime() {
+ coldExtractionShotActive = false;
+ coldExtractionPumpResting = false;
+ coldExtractionPumpOutputOn = false;
+ coldExtractionPumpRunMs = 0;
+ coldExtractionRestUntilMs = 0;
+ coldExtractionPumpSampleMs = 0;
+ coldExtractionStallLastWeight = NAN;
+ coldExtractionStallSinceMs = 0;
+}
+
+// Ist der Wasserkessel kalt genug fuer eine kalte Extraktion?
+// Mit Hysterese: einmal freigegeben, bleibt die Freigabe bis Schwelle + Marge bestehen,
+// damit die Anzeige an der Schwelle nicht flattert. Sensorfehler = keine Freigabe.
+bool coldExtractionTemperatureOk() {
+ if (wasserSensorError || wasserSafetyShutdown || !isfinite(InputWasser)) {
+ return false;
+ }
+ float limit = coldExtractionMaxTempC;
+ if (coldExtractionModeActive) {
+ limit += COLDEX_TEMP_RELEASE_MARGIN_C;
+ }
+ return (InputWasser < (double)limit);
+}
+
+// Allokationsfreie Freigabepruefung - wird in den JSON-Pushes (2x/s) verwendet.
+bool coldExtractionConditionsOk() {
+ return coldExtractionEnabled &&
+ !standbyModeActive &&
+ !wartungsModusAktiv &&
+ !cleaningAssistantActive &&
+ !autoTuneWasserActive &&
+ !autoTuneDampfActive &&
+ coldExtractionTemperatureOk();
+}
+
+// Klartext-Grund, warum gerade keine kalte Extraktion moeglich ist ("" = alles frei).
+String coldExtractionBlockReason() {
+ if (!coldExtractionEnabled) {
+ return "Cold Extraction ist nicht freigeschaltet.";
+ }
+ if (standbyModeActive) {
+ return "Cold Extraction nicht moeglich: Standby aktiv.";
+ }
+ if (wartungsModusAktiv) {
+ return "Cold Extraction nicht moeglich: Wartungsmodus aktiv.";
+ }
+ if (cleaningAssistantActive) {
+ return "Cold Extraction nicht moeglich: Reinigungsassistent aktiv.";
+ }
+ if (autoTuneWasserActive || autoTuneDampfActive) {
+ return "Cold Extraction nicht moeglich: PID-Tuning laeuft.";
+ }
+ if (wasserSensorError || wasserSafetyShutdown || !isfinite(InputWasser)) {
+ return "Cold Extraction nicht moeglich: Wasser-Sensor liefert keinen gueltigen Wert.";
+ }
+ if (!coldExtractionTemperatureOk()) {
+ char buf[96];
+ snprintf(buf, sizeof(buf), "Wasserkessel zu warm (%.1f C, benoetigt < %.1f C).",
+ (double)InputWasser, (double)coldExtractionMaxTempC);
+ return String(buf);
+ }
+ return "";
+}
+
+// Modus scharf schalten bzw. beenden. Ein laufender Bezug wird nie abgebrochen.
+bool setColdExtractionMode(bool enable, String& message) {
+ if (enable == coldExtractionModeActive) {
+ message = coldExtractionModeActive ? "Cold Extraction bereits aktiv." : "Cold Extraction bereits aus.";
+ return true;
+ }
+
+ if (enable) {
+ String reason = coldExtractionBlockReason();
+ if (reason.length() > 0) {
+ message = reason;
+ return false;
+ }
+ if (shotActive || shotSoftwareActive || flushActive) {
+ message = "Cold Extraction nicht moeglich: Bezug oder Spuelen laeuft.";
+ return false;
+ }
+ coldExtractionModeActive = true;
+ resetColdExtractionRuntime();
+ message = "Cold Extraction aktiviert - Wasserkreis heizt nicht.";
+ return true;
+ }
+
+ if (shotActive || shotSoftwareActive) {
+ message = "Cold Extraction kann erst nach dem laufenden Bezug beendet werden.";
+ return false;
+ }
+ coldExtractionModeActive = false;
+ resetColdExtractionRuntime();
+ message = "Cold Extraction beendet - Wasserkreis heizt wieder.";
+ return true;
+}
+
+// Darf jetzt ein kalter Bezug starten? (Prueft pro Shot neu, nicht nur beim Aktivieren.)
+bool coldExtractionShotAllowed() {
+ return coldExtractionModeActive && coldExtractionConditionsOk();
+}
+
+// Pumpensteuerung des kalten Bezugs: Vorbenetzung ungepulst, danach Puls-Duty.
+// Zusaetzlich Pumpenschutz ueber die kumulierte Einschaltzeit (Zwangspause) - die Olab
+// hat zwar einen Thermoschutz, ein Auslösen mitten im Bezug wuerde aber den Shot
+// abbrechen und den Puck unter geschlossenem Ventil stehen lassen.
+void applyColdExtractionPumpControl(unsigned long currentShotTimeMillis) {
+ unsigned long nowMs = millis();
+
+ // Kumulierte Pumpenlaufzeit fortschreiben (nur solange die Pumpe wirklich lief).
+ if (coldExtractionPumpSampleMs != 0 && coldExtractionPumpOutputOn) {
+ coldExtractionPumpRunMs += (nowMs - coldExtractionPumpSampleMs);
+ }
+ coldExtractionPumpSampleMs = nowMs;
+
+ // Zwangspause verwalten
+ if (coldExtractionPumpResting) {
+ if ((long)(nowMs - coldExtractionRestUntilMs) >= 0) {
+ coldExtractionPumpResting = false;
+ coldExtractionPumpRunMs = 0;
+ }
+ } else if (coldExtractionPumpMaxRunSec > 0.0f &&
+ coldExtractionPumpRunMs >= (unsigned long)(coldExtractionPumpMaxRunSec * 1000.0f)) {
+ if (coldExtractionPumpRestSec > 0.0f) {
+ coldExtractionPumpResting = true;
+ coldExtractionRestUntilMs = nowMs + (unsigned long)(coldExtractionPumpRestSec * 1000.0f);
+ } else {
+ coldExtractionPumpRunMs = 0; // Pause deaktiviert -> nur Zaehler zuruecksetzen
+ }
+ }
+
+ bool pumpOn;
+ if (coldExtractionPumpResting) {
+ pumpOn = false;
+ } else if (currentPreInfusionState == PI_PRE_BREW) {
+ pumpOn = true; // Vorbenetzung: volle Pumpenleistung
+ } else {
+ float duty = coldExtractionDutyPercent / 100.0f;
+ duty = constrain(duty, COLDEX_DUTY_MIN / 100.0f, 1.0f);
+ uint16_t periodMs = coldExtractionPulsePeriodMs;
+ if (periodMs < COLDEX_PULSE_PERIOD_MIN_MS || periodMs > COLDEX_PULSE_PERIOD_MAX_MS) {
+ periodMs = defaultColdExtractionPulsePeriodMs;
+ }
+ if (duty >= 0.999f) {
+ pumpOn = true;
+ } else {
+ unsigned long onTimeMs = (unsigned long)((float)periodMs * duty);
+ if (onTimeMs < 1UL) { onTimeMs = 1UL; }
+ pumpOn = ((currentShotTimeMillis % periodMs) < onTimeMs);
+ }
+ }
+
+ coldExtractionPumpOutputOn = pumpOn;
+ digitalWrite(VALVE_PIN, HIGH);
+ digitalWrite(PUMP_PIN, pumpOn ? HIGH : LOW);
+}
+
+// Stillstands-Erkennung: laeuft die Pumpe (ohne Zwangspause) ohne Zulauf auf der Waage,
+// ist entweder der Puck dicht oder der Pumpen-Thermoschutz hat ausgeloest.
+bool coldExtractionStallDetected(float netWeight) {
+ if (!scaleEnabled || !scaleConnected || !isfinite(netWeight)) {
+ coldExtractionStallLastWeight = NAN;
+ coldExtractionStallSinceMs = 0;
+ return false;
+ }
+ if (coldExtractionPumpResting) {
+ coldExtractionStallSinceMs = 0; // Pause ist kein Stillstand
+ coldExtractionStallLastWeight = netWeight;
+ return false;
+ }
+
+ unsigned long nowMs = millis();
+ if (!isfinite(coldExtractionStallLastWeight) || coldExtractionStallSinceMs == 0) {
+ coldExtractionStallLastWeight = netWeight;
+ coldExtractionStallSinceMs = nowMs;
+ return false;
+ }
+ if ((netWeight - coldExtractionStallLastWeight) >= COLDEX_STALL_MIN_WEIGHT_DELTA_G) {
+ coldExtractionStallLastWeight = netWeight;
+ coldExtractionStallSinceMs = nowMs;
+ return false;
+ }
+ return ((nowMs - coldExtractionStallSinceMs) >= COLDEX_STALL_TIMEOUT_MS);
+}
+
/************************************************************************************
* Feed-Forward bei Bezug (Wasserkessel)
************************************************************************************/
@@ -3900,7 +4198,7 @@ void setPidWasserIntegralFrozen(bool frozen) {
// nicht im Wartungsmodus und nicht während AutoTune.
bool ffBoostWasserActiveNow() {
return ffBoostPercentWasser > 0 && (shotActive || shotSoftwareActive) &&
- !wartungsModusAktiv && !autoTuneWasserActive;
+ !wartungsModusAktiv && !autoTuneWasserActive && !coldExtractionModeActive;
}
// Effektive Einschaltzeit im aktuellen Fenster: PID-Output + Feed-Forward-Boost,
@@ -3937,11 +4235,14 @@ void stopBrewSequence(unsigned long durationMillis, bool manualStop = false, boo
}
// Statusvariablen zurücksetzen
+ bool wasColdExtraction = coldExtractionShotActive;
+ lastShotWasColdExtraction = wasColdExtraction;
shotActive = false;
shotSoftwareActive = false;
currentPreInfusionState = PI_INACTIVE;
lastShotTime = millis(); // Wichtig für Eco-Modus
resetFlowGuardRuntime();
+ resetColdExtractionRuntime(); // beendet nur den Bezug, coldExtractionModeActive bleibt
// Hardware stoppen
digitalWrite(PUMP_PIN, LOW);
@@ -3958,6 +4259,7 @@ void stopBrewSequence(unsigned long durationMillis, bool manualStop = false, boo
shotNeedsToBeSaved = true;
savedShotDuration = durationMillis;
savedShotWasByWeight = stoppedByWeight; // Kennzeichnen, ob der Bezug gewichtsbasierend war
+ savedShotWasColdExtraction = wasColdExtraction; // Kennzeichnen fuer Log/Statistik
if(stoppedByWeight) {
savedShotFinalWeight = lastShotFinalNetWeight + brewByWeightOffsetGrams; ; // Gewicht (inkl. Offset) für den Log merken
} else {
@@ -4274,6 +4576,50 @@ void setup() {
steamByTimeTargetSeconds = defaultSteamByTimeTargetSeconds;
}
+ // --- Cold Extraction ---
+ // Bei einem Update aus einer aelteren Version steht in den neuen Adressen 0xFF. Ein
+ // direktes EEPROM.get in ein bool wuerde daraus "true" machen -> als uint8_t pruefen.
+ uint8_t storedColdExtractionEnabled = 0;
+ EEPROM.get(EEPROM_ADDR_COLDEX_ENABLED, storedColdExtractionEnabled);
+ if (storedColdExtractionEnabled > 1) { storedColdExtractionEnabled = (uint8_t)(defaultColdExtractionEnabled ? 1 : 0); }
+ coldExtractionEnabled = (storedColdExtractionEnabled == 1);
+ EEPROM.get(EEPROM_ADDR_COLDEX_MAX_TEMP, coldExtractionMaxTempC);
+ EEPROM.get(EEPROM_ADDR_COLDEX_PREINF_SEC, coldExtractionPreInfusionSeconds);
+ EEPROM.get(EEPROM_ADDR_COLDEX_PULSE_PERIOD_MS, coldExtractionPulsePeriodMs);
+ EEPROM.get(EEPROM_ADDR_COLDEX_DUTY, coldExtractionDutyPercent);
+ EEPROM.get(EEPROM_ADDR_COLDEX_TARGET_GRAMS, coldExtractionTargetGrams);
+ EEPROM.get(EEPROM_ADDR_COLDEX_MAX_SECONDS, coldExtractionMaxSeconds);
+ EEPROM.get(EEPROM_ADDR_COLDEX_PUMP_MAX_RUN, coldExtractionPumpMaxRunSec);
+ EEPROM.get(EEPROM_ADDR_COLDEX_PUMP_REST, coldExtractionPumpRestSec);
+ if (!isfinite(coldExtractionMaxTempC) || coldExtractionMaxTempC < COLDEX_MAX_TEMP_MIN || coldExtractionMaxTempC > COLDEX_MAX_TEMP_MAX) {
+ coldExtractionMaxTempC = defaultColdExtractionMaxTempC;
+ }
+ if (!isfinite(coldExtractionPreInfusionSeconds) || coldExtractionPreInfusionSeconds < COLDEX_PREINF_SEC_MIN || coldExtractionPreInfusionSeconds > COLDEX_PREINF_SEC_MAX) {
+ coldExtractionPreInfusionSeconds = defaultColdExtractionPreInfusionSeconds;
+ }
+ if (coldExtractionPulsePeriodMs < COLDEX_PULSE_PERIOD_MIN_MS || coldExtractionPulsePeriodMs > COLDEX_PULSE_PERIOD_MAX_MS) {
+ coldExtractionPulsePeriodMs = defaultColdExtractionPulsePeriodMs;
+ }
+ if (!isfinite(coldExtractionDutyPercent) || coldExtractionDutyPercent < COLDEX_DUTY_MIN || coldExtractionDutyPercent > COLDEX_DUTY_MAX) {
+ coldExtractionDutyPercent = defaultColdExtractionDutyPercent;
+ }
+ if (!isfinite(coldExtractionTargetGrams) || coldExtractionTargetGrams < COLDEX_TARGET_GRAMS_MIN || coldExtractionTargetGrams > COLDEX_TARGET_GRAMS_MAX) {
+ coldExtractionTargetGrams = defaultColdExtractionTargetGrams;
+ }
+ if (!isfinite(coldExtractionMaxSeconds) || coldExtractionMaxSeconds < COLDEX_MAX_SECONDS_MIN || coldExtractionMaxSeconds > COLDEX_MAX_SECONDS_MAX) {
+ coldExtractionMaxSeconds = defaultColdExtractionMaxSeconds;
+ }
+ if (!isfinite(coldExtractionPumpMaxRunSec) || coldExtractionPumpMaxRunSec < COLDEX_PUMP_MAX_RUN_SEC_MIN || coldExtractionPumpMaxRunSec > COLDEX_PUMP_MAX_RUN_SEC_MAX) {
+ coldExtractionPumpMaxRunSec = defaultColdExtractionPumpMaxRunSec;
+ }
+ if (!isfinite(coldExtractionPumpRestSec) || coldExtractionPumpRestSec < COLDEX_PUMP_REST_SEC_MIN || coldExtractionPumpRestSec > COLDEX_PUMP_REST_SEC_MAX) {
+ coldExtractionPumpRestSec = defaultColdExtractionPumpRestSec;
+ }
+ // Der Modus selbst wird bewusst NICHT persistiert: nach einem Neustart startet die
+ // Maschine immer im normalen Heizbetrieb.
+ coldExtractionModeActive = false;
+ coldExtractionShotActive = false;
+
// Piezo-Status laden
EEPROM.get(EEPROM_ADDR_PIEZO_ENABLED, piezoEnabled);
@@ -4888,6 +5234,12 @@ void loop() {
if (wartungsModusAktiv) {
wartungsModusAktiv = false;
}
+ // Cold Extraction beim Wechsel in den Standby beenden (analog Eco/Wartung)
+ if (coldExtractionModeActive) {
+ coldExtractionModeActive = false;
+ resetColdExtractionRuntime();
+ }
+ coldExtractionBlockMessage = "";
} else if (lastStandbyModeActive) {
startupTime = currentMillis;
ecoForcedActive = false;
@@ -4931,7 +5283,10 @@ void loop() {
// Feed-Forward: I-Anteil der Wasser-PID während des Bezugs einfrieren/freigeben.
// Zentral hier verwaltet (statt an Start-/Stopp-Stellen), damit der Zustand auch
// nach Software-Shots, Einstellungsänderungen mid-shot etc. immer konsistent ist.
- setPidWasserIntegralFrozen(ffIFreezeWasser && (shotActive || shotSoftwareActive) && !wartungsModusAktiv);
+ // 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 ||
+ (ffIFreezeWasser && (shotActive || shotSoftwareActive) && !wartungsModusAktiv));
// --- Angepasste Sicherheitsueberpruefung Wasser ---
if (InputWasser < 0.0 || isnan(InputWasser)) {
@@ -4951,8 +5306,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 && (InputWasser < (SetpointWasser * 0.95));
+ bool activateBoost = boostWasserActive && !wartungsModusAktiv && !ecoModeAktiv && !autoTuneWasserActive && !coldExtractionModeActive && (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; }
}
yield();
@@ -5029,7 +5388,7 @@ void loop() {
recordResetCheckpoint(RESET_CP_LOOP_SSR);
unsigned long now = millis();
// Wasser SSR
- if (wasserSensorError || wasserSafetyShutdown || standbyModeActive || displayOtaActive) { digitalWrite(SSR_WASSER_PIN, LOW); } else {
+ if (wasserSensorError || wasserSafetyShutdown || standbyModeActive || displayOtaActive || coldExtractionModeActive) { 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));
@@ -5102,10 +5461,33 @@ void loop() {
bool stopForTime = brewByTimeEnabled && (currentShotTimeMillis >= (unsigned long)(brewByTimeTargetSeconds * 1000.0f));
bool stopForWeight = brewByWeightEnabled && scaleConnected && (netWeight >= targetWeightToStopAt);
+ // Cold Extraction hat eigene Abbruchbedingungen und ignoriert Brew-by-Time/Weight:
+ // Zielgewicht, Sicherheits-Timeout und Stillstands-Erkennung.
+ if (coldExtractionShotActive) {
+ targetWeightToStopAt = coldExtractionTargetGrams;
+ stopForTime = (currentShotTimeMillis >= (unsigned long)(coldExtractionMaxSeconds * 1000.0f));
+ stopForWeight = scaleEnabled && scaleConnected && (netWeight >= targetWeightToStopAt);
+ if (!stopForTime && !stopForWeight && coldExtractionStallDetected(netWeight)) {
+ stopForTime = true;
+ coldExtractionBlockMessage = "Cold Extraction beendet: kein Zulauf (Puck dicht oder Pumpenschutz?).";
+ coldExtractionBlockMessageUntilMs = millis() + COLDEX_BLOCK_MESSAGE_MS;
+ }
+ }
+
if (stopForTime || stopForWeight) {
stopBrewSequence(currentShotTimeMillis, false, stopForWeight); // Uebergibt die aktuelle Dauer und den Grund des Stopps
}
+ // --- Cold-Extraction-Phasen (Vorbenetzung -> Hauptbezug, keine Pause) ---
+ else if (coldExtractionShotActive) {
+ unsigned long currentPhaseTimeMillis = millis() - preInfusionPhaseStartTime;
+ if (currentPreInfusionState == PI_PRE_BREW &&
+ (currentPhaseTimeMillis >= (unsigned long)(coldExtractionPreInfusionSeconds * 1000.0f))) {
+ currentPreInfusionState = PI_MAIN_BREW;
+ preInfusionPhaseStartTime = millis();
+ }
+ }
+
// --- Pre-Infusion Zustandsmaschine (Nur ausfuehren, wenn noch nicht gestoppt wurde) ---
else if (preInfusionEnabled) {
unsigned long currentPhaseTimeMillis = millis() - preInfusionPhaseStartTime;
@@ -5125,7 +5507,11 @@ void loop() {
}
if (shotActive) {
- applyFlowGuardPumpControl(currentShotTimeMillis, netWeight, targetWeightToStopAt);
+ if (coldExtractionShotActive) {
+ applyColdExtractionPumpControl(currentShotTimeMillis);
+ } else {
+ applyFlowGuardPumpControl(currentShotTimeMillis, netWeight, targetWeightToStopAt);
+ }
}
// Kein else hier -> Entweder Stopp, oder PreInfusion-Check, oder normaler Bezug ohne PreInfusion laeuft weiter
}
@@ -5582,6 +5968,36 @@ void updateDisplay() {
return;
}
+ // Cold-Extraction-Sperrmeldung (abgewiesener kalter Bezug bzw. Abbruch wegen Stillstand)
+ if (coldExtractionBlockMessage.length() > 0) {
+ if ((long)(millis() - coldExtractionBlockMessageUntilMs) >= 0) {
+ coldExtractionBlockMessage = "";
+ } else if (!standbyModeActive) {
+ display.clearDisplay();
+ display.setTextSize(1);
+ display.setTextColor(SH110X_WHITE);
+ display.setCursor(0, 0);
+ display.println(F("Cold Extraction"));
+ display.println(F(""));
+ // Meldung auf 21 Zeichen pro Zeile umbrechen (128 px / 6 px Zeichenbreite)
+ const size_t lineLen = 21;
+ String rest = coldExtractionBlockMessage;
+ while (rest.length() > 0) {
+ if (rest.length() <= lineLen) {
+ display.println(rest);
+ break;
+ }
+ int cut = rest.lastIndexOf(' ', lineLen);
+ if (cut <= 0) { cut = lineLen; }
+ display.println(rest.substring(0, cut));
+ rest = rest.substring(cut);
+ rest.trim();
+ }
+ display.display();
+ return;
+ }
+ }
+
static bool lastStandbyDisplayActive = false;
static int lastStandbyDisplayMinute = -1;
if (standbyModeActive) {
@@ -6038,9 +6454,15 @@ if (scaleModeActive) {
display.setCursor(0, 30);
display.println(weightBuffer);
display.println("");
- if(preInfusionEnabled && currentPreInfusionState == PI_PRE_BREW) {display.println(F("Pre-Infusion ..."));}
- if(preInfusionEnabled && currentPreInfusionState == PI_PAUSE) {display.println(F("Pause ..."));}
- if(preInfusionEnabled && currentPreInfusionState == PI_MAIN_BREW) {display.println(F("Pre-Infusion beendet."));}
+ if (coldExtractionShotActive) {
+ if (coldExtractionPumpResting) { display.println(F("Cold: Pumpenpause")); }
+ else if (currentPreInfusionState == PI_PRE_BREW) { display.println(F("Cold: Vorbenetzung")); }
+ else { display.println(F("Cold Extraction ...")); }
+ } else {
+ if(preInfusionEnabled && currentPreInfusionState == PI_PRE_BREW) {display.println(F("Pre-Infusion ..."));}
+ if(preInfusionEnabled && currentPreInfusionState == PI_PAUSE) {display.println(F("Pause ..."));}
+ if(preInfusionEnabled && currentPreInfusionState == PI_MAIN_BREW) {display.println(F("Pre-Infusion beendet."));}
+ }
} else {
unsigned long elapsed = millis() - shotStartTime;
display.println(F("Shot-Timer:"));
@@ -6051,9 +6473,15 @@ if (scaleModeActive) {
display.println(buffer);
display.setTextSize(1);
display.println(F("Sekunden"));
- if(preInfusionEnabled && currentPreInfusionState == PI_PRE_BREW) {display.println(F("Pre-Infusion ..."));}
- if(preInfusionEnabled && currentPreInfusionState == PI_PAUSE) {display.println(F("Pause ..."));}
- if(preInfusionEnabled && currentPreInfusionState == PI_MAIN_BREW) {display.println(F("Pre-Infusion beendet."));}
+ if (coldExtractionShotActive) {
+ if (coldExtractionPumpResting) { display.println(F("Cold: Pumpenpause")); }
+ else if (currentPreInfusionState == PI_PRE_BREW) { display.println(F("Cold: Vorbenetzung")); }
+ else { display.println(F("Cold Extraction ...")); }
+ } else {
+ if(preInfusionEnabled && currentPreInfusionState == PI_PRE_BREW) {display.println(F("Pre-Infusion ..."));}
+ if(preInfusionEnabled && currentPreInfusionState == PI_PAUSE) {display.println(F("Pause ..."));}
+ if(preInfusionEnabled && currentPreInfusionState == PI_MAIN_BREW) {display.println(F("Pre-Infusion beendet."));}
+ }
}
fastHeatUpHeating = false;
}
@@ -6120,6 +6548,13 @@ if (scaleModeActive) {
display.println(F("Wasser: "));
if (wasserSafetyShutdown) { display.print(F("Sicherheitsabsch.")); }
else if (wasserSensorError) { display.print(F("Sensorfehler!")); }
+ else if (coldExtractionModeActive) {
+ // Cold Extraction: kein Sollwert-Vergleich sinnvoll, der Wasserkreis heizt nicht
+ display.print((int)displayedWaterTemp);
+ display.print(F(" "));
+ display.print((char)247);
+ display.print(F("C (COLD)"));
+ }
else {
display.print((int)displayedWaterTemp);
display.print(F(" / "));
@@ -6284,8 +6719,24 @@ void updateShotTimer() {
}
}
+ // Cold Extraction hat eine eigene Phasenlogik (lange Vorbenetzung, danach
+ // gepulster Hauptbezug) und laeuft unabhaengig von Pre-Infusion/FlowGuard.
+ if (coldExtractionModeActive) {
+ coldExtractionShotActive = true;
+ coldExtractionPumpResting = false;
+ coldExtractionPumpRunMs = 0;
+ coldExtractionRestUntilMs = 0;
+ coldExtractionPumpSampleMs = millis();
+ coldExtractionStallLastWeight = NAN;
+ coldExtractionStallSinceMs = 0;
+ coldExtractionPumpOutputOn = true;
+ currentPreInfusionState = (coldExtractionPreInfusionSeconds > 0.0f) ? PI_PRE_BREW : PI_MAIN_BREW;
+ preInfusionPhaseStartTime = millis();
+ digitalWrite(PUMP_PIN, HIGH);
+ digitalWrite(VALVE_PIN, HIGH);
+ }
// Pre-Infusion oder direkten Start behandeln
- if (preInfusionEnabled) {
+ else if (preInfusionEnabled) {
currentPreInfusionState = PI_PRE_BREW;
preInfusionPhaseStartTime = millis();
digitalWrite(PUMP_PIN, HIGH);
@@ -6578,7 +7029,8 @@ Steuert das automatische Beenden des Brühvorgangs basierend auf Zeit oder G
Wenn beide aktiviert sind, stoppt der Bezug, sobald die erste Bedingung (Zeit oder Gewicht) erreicht ist.
Der Bezug kann aber weiterhin manuell über den Bezugs-Schalter beendet werden.
Ebenso kann eine Pre-Infusion aktiviert und eingestellt werden.
-FlowGuard verlängert bei Bedarf die Bezugszeit bis zur eingestellten Mindest-Brühzeit, indem die Pumpe in der Hauptbezugsphase dynamisch gepulst wird.
+FlowGuard verlängert bei Bedarf die Bezugszeit bis zur eingestellten Mindest-Brühzeit, indem die Pumpe in der Hauptbezugsphase dynamisch gepulst wird.
+Cold Extraction erlaubt eine kalte Extraktion bei kaltem Wasserkessel; die Parameter dazu stehen weiter unten.
)rawliteral";
// --- Brew-By-Time (BBT) Chunks ---
@@ -6631,6 +7083,37 @@ static const char brewControlPI_Fields[] PROGMEM = R"rawliteral(
(Pumpe AN -> Pumpe AUS -> Pumpe AN)
)rawliteral";
+// --- Cold Extraction (CX) Chunks ---
+static const char brewControlCX_H3[] PROGMEM = R"rawliteral(