diff --git a/Changelog.txt b/Changelog.txt
index 1699cff..a606486 100644
--- a/Changelog.txt
+++ b/Changelog.txt
@@ -1,3 +1,17 @@
+Version 5.3.6:
+- Cold Extraction: Die Vorbenetzung läuft nicht mehr mit voller Pumpenleistung, sondern gepulst mit
+ eigener, niedriger Leistung (neue Einstellung „Pumpenleistung Vorbenetzung", Standard 10 %).
+ Messung an der Maschine: Nach 7 s Vorbenetzung kommen heiß 1-2 Tropfen, kalt bei voller Leistung
+ rund 10 g dünne Brühe ohne Crema - etwa zehnfacher Durchfluss trotz dreifacher Viskosität.
+ Ursache ist nicht die Viskosität, sondern das Kaffeebett: Kaltes Mehl quillt nicht, ist schlecht
+ benetzbar, es wandert kein Feinanteil und es lösen sich kaum Feststoffe, die das Bett verdichten.
+ Volle Leistung auf den trockenen kalten Puck legt deshalb sofort Kanäle an, die sich - anders als
+ bei heißem Bezug - nie wieder schließen; der restliche Bezug läuft daran vorbei statt zu extrahieren.
+- Standard der Pumpenleistung im Hauptbezug von 30 % auf 20 % gesenkt (gleicher Grund: das kalte Bett
+ ist erheblich durchlässiger als ein heißes). Bestehende Installationen behalten ihren Wert.
+- Der Erklärtext auf /Brew-Control benennt den Zusammenhang jetzt und weist darauf hin, dass die
+ Vorbenetzung deutlich schwächer eingestellt sein sollte als der Hauptbezug.
+
Version 5.3.5:
- Cold Extraction lässt sich jetzt auch im Standby und bei noch warmem Kessel scharf schalten.
Bisher galt „Standby aktiv" und „Kessel zu warm" auch fürs Aktivieren - das war falsch herum
diff --git a/Dual-PID.ino b/Dual-PID.ino
index 6eedfdb..bc7d8c7 100644
--- a/Dual-PID.ino
+++ b/Dual-PID.ino
@@ -369,7 +369,7 @@ Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire);
* Firmware-Informationen
************************************************************************************/
-String version = "5.3.5";
+String version = "5.3.6";
String versionHersteller = "Thomas Müller";
String versionHerstellerMail =
"thomas@mueller.black";
@@ -782,9 +782,10 @@ bool flowGuardPumpPulseActive = false;
************************************************************************************/
bool coldExtractionEnabled = false; // Funktion in der Web-UI freigeschaltet
float coldExtractionMaxTempC = 30.0f; // Freigabeschwelle Wasserkessel
-float coldExtractionPreInfusionSeconds = 10.0f; // Vorbenetzung (Pumpe laeuft ungepulst)
+float coldExtractionPreInfusionSeconds = 10.0f; // Vorbenetzung (gepulst, eigene Leistung)
+float coldExtractionPreInfDutyPercent = 10.0f; // Pumpenleistung waehrend der Vorbenetzung
uint16_t coldExtractionPulsePeriodMs = 2000; // Pulsperiode der Pumpe im Hauptbezug
-float coldExtractionDutyPercent = 30.0f; // Pumpen-Duty im Hauptbezug
+float coldExtractionDutyPercent = 20.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
@@ -792,13 +793,17 @@ float coldExtractionPumpRestSec = 20.0f; // Dauer der Zwangspause
const bool defaultColdExtractionEnabled = false;
const float defaultColdExtractionMaxTempC = 30.0f;
-// 10 s statt der urspruenglichen 30 s: Die Pumpe laeuft in dieser Phase ungepulst auf
-// voller Leistung. Heiss kommt der erste Tropfen nach ~7 s, kalt (rund dreifache
-// Viskositaet) entsprechend spaeter - 30 s waeren laengst voller Bezug bei vollem Druck
-// gewesen, und die Haelfte des Pumpen-Laufzeitbudgets waere vor dem Hauptbezug verbraucht.
const float defaultColdExtractionPreInfusionSeconds = 10.0f;
+// Die Vorbenetzung laeuft bewusst SEHR sanft. Messung an der Maschine: heiss kommen nach
+// 7 s Vorbenetzung 1-2 Tropfen, kalt bei voller Pumpenleistung rund 10 g duenne Bruehe -
+// etwa zehnfacher Durchfluss trotz dreifacher Viskositaet. Kaltes Mehl quillt nicht und ist
+// schlecht benetzbar; volle Leistung auf den trockenen Puck legt sofort Kanaele an, die sich
+// - anders als heiss - nie wieder zudruecken. Der restliche Bezug laeuft dann daran vorbei.
+const float defaultColdExtractionPreInfDutyPercent = 10.0f;
const uint16_t defaultColdExtractionPulsePeriodMs = 2000;
-const float defaultColdExtractionDutyPercent = 30.0f;
+// 20 % statt 30 %: Das kalte Kaffeebett ist deutlich durchlaessiger als ein heisses
+// (kein Quellen, keine Feinanteil-Wanderung, keine geloesten Feststoffe, die es verdichten).
+const float defaultColdExtractionDutyPercent = 20.0f;
const float defaultColdExtractionTargetGrams = 60.0f;
const float defaultColdExtractionMaxSeconds = 300.0f;
const float defaultColdExtractionPumpMaxRunSec = 60.0f;
@@ -1067,7 +1072,8 @@ const int EEPROM_ADDR_COLDEX_MAX_SECONDS = 965; // 4 Bytes (float) -> Ende
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
const int EEPROM_ADDR_COLDEX_MODE_ACTIVE = 977; // 1 Byte (uint8_t) -> Ende 977
-// Naechste freie Adresse: 978 (Innerhalb EEPROM_SIZE=1024)
+const int EEPROM_ADDR_COLDEX_PREINF_DUTY = 978; // 4 Bytes (float) -> Ende 981
+// Naechste freie Adresse: 982 (Innerhalb EEPROM_SIZE=1024)
/************************************************************************************
* Eco-Mode Variablen
@@ -3054,6 +3060,7 @@ void resetToDefaults() {
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_PREINF_DUTY, defaultColdExtractionPreInfDutyPercent);
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);
@@ -3142,6 +3149,7 @@ void resetToDefaults() {
coldExtractionPreInfusionSeconds = defaultColdExtractionPreInfusionSeconds;
coldExtractionPulsePeriodMs = defaultColdExtractionPulsePeriodMs;
coldExtractionDutyPercent = defaultColdExtractionDutyPercent;
+ coldExtractionPreInfDutyPercent = defaultColdExtractionPreInfDutyPercent;
coldExtractionTargetGrams = defaultColdExtractionTargetGrams;
coldExtractionMaxSeconds = defaultColdExtractionMaxSeconds;
coldExtractionPumpMaxRunSec = defaultColdExtractionPumpMaxRunSec;
@@ -4230,10 +4238,12 @@ void applyColdExtractionPumpControl(unsigned long currentShotTimeMillis) {
bool pumpOn;
if (coldExtractionPumpResting) {
pumpOn = false;
- } else if (currentPreInfusionState == PI_PRE_BREW) {
- pumpOn = true; // Vorbenetzung: volle Pumpenleistung
} else {
- float duty = coldExtractionDutyPercent / 100.0f;
+ // Vorbenetzung und Hauptbezug pulsen beide - nur mit unterschiedlicher Leistung.
+ // Volle Leistung auf den trockenen kalten Puck legt Kanaele an, die sich mangels
+ // Quellung nie wieder schliessen (siehe Kommentar bei den Defaults).
+ float duty = ((currentPreInfusionState == PI_PRE_BREW) ? coldExtractionPreInfDutyPercent
+ : 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) {
@@ -4698,6 +4708,7 @@ void setup() {
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_PREINF_DUTY, coldExtractionPreInfDutyPercent);
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);
@@ -4714,6 +4725,10 @@ void setup() {
if (!isfinite(coldExtractionDutyPercent) || coldExtractionDutyPercent < COLDEX_DUTY_MIN || coldExtractionDutyPercent > COLDEX_DUTY_MAX) {
coldExtractionDutyPercent = defaultColdExtractionDutyPercent;
}
+ // Beim Update aus <= 5.3.5 steht hier 0xFFFFFFFF (NaN) -> Default greift automatisch
+ if (!isfinite(coldExtractionPreInfDutyPercent) || coldExtractionPreInfDutyPercent < COLDEX_DUTY_MIN || coldExtractionPreInfDutyPercent > COLDEX_DUTY_MAX) {
+ coldExtractionPreInfDutyPercent = defaultColdExtractionPreInfDutyPercent;
+ }
if (!isfinite(coldExtractionTargetGrams) || coldExtractionTargetGrams < COLDEX_TARGET_GRAMS_MIN || coldExtractionTargetGrams > COLDEX_TARGET_GRAMS_MAX) {
coldExtractionTargetGrams = defaultColdExtractionTargetGrams;
}
@@ -7207,11 +7222,13 @@ static const char brewControlCX_ToggleContainerEnd[] PROGMEM = R"rawliteral(>Freigabe bis Wassertemperatur (°C):
-
+
+
+
-
+
@@ -7224,9 +7241,12 @@ static const char brewControlCX_Fields[] PROGMEM = R"rawliteral(
(Die Maschine hat keinen Bypass um den Wasserkessel: Kaltes Wasser kommt nur bei kaltem
Kessel am Puck an. Deshalb ist die kalte Extraktion nur unterhalb der Freigabetemperatur möglich, praktisch also
aus dem kalten Zustand heraus. Solange der Modus aktiv ist, heizt der Wasserkreis nicht - der Dampfkreis bleibt
- unberührt und darf weiter heizen. Während der Vorbenetzung läuft die Pumpe durchgehend auf voller
- Leistung (kein Pulsen) - sinnvoll ist eine Dauer knapp unterhalb des ersten Tropfens; gepulst wird erst im
- Hauptbezug. Die Pumpenlaufzeit wird kumuliert gezählt; nach der eingestellten
+ unberührt und darf weiter heizen. Vorbenetzung und Hauptbezug pulsen beide, nur mit unterschiedlicher
+ Leistung: Kaltes Kaffeemehl quillt nicht und ist schlecht benetzbar - volle Leistung auf den trockenen Puck
+ legt sofort Kanäle an, die sich (anders als bei heißem Bezug) nie wieder schließen, und der
+ restliche Bezug läuft daran vorbei. Deshalb die Vorbenetzung deutlich schwächer einstellen als den
+ Hauptbezug. Das kalte Bett ist insgesamt viel durchlässiger als ein heißes, niedrige Werte sind
+ also normal. Die Pumpenlaufzeit wird kumuliert gezählt; nach der eingestellten
Laufzeit legt die Steuerung eine Zwangspause ein. Der Modus selbst wird auf dem Dashboard bzw. am Touch-Display
ein- und ausgeschaltet und bleibt über Standby und Neustart hinweg aktiv, bis er dort wieder ausgeschaltet
wird - solange heizt der Wasserkreis nicht.)
@@ -7362,6 +7382,8 @@ void handleBrewControl(AsyncWebServerRequest *request) {
cxFieldsHtml.replace("{CX_PULSE_MS}", buffer);
snprintf(buffer, sizeof(buffer), "%.1f", coldExtractionDutyPercent);
cxFieldsHtml.replace("{CX_DUTY}", buffer);
+ snprintf(buffer, sizeof(buffer), "%.1f", coldExtractionPreInfDutyPercent);
+ cxFieldsHtml.replace("{CX_PREINF_DUTY}", buffer);
snprintf(buffer, sizeof(buffer), "%.1f", coldExtractionTargetGrams);
cxFieldsHtml.replace("{CX_TARGET}", buffer);
snprintf(buffer, sizeof(buffer), "%.1f", coldExtractionMaxSeconds);
@@ -7460,6 +7482,8 @@ struct BrewControlUpdate {
uint16_t cxPulseMs = 0;
bool hasCXDuty = false;
float cxDuty = 0.0f;
+ bool hasCXPreInfDuty = false;
+ float cxPreInfDuty = 0.0f;
bool hasCXTarget = false;
float cxTarget = 0.0f;
bool hasCXMaxSecs = false;
@@ -7625,6 +7649,14 @@ bool applyBrewControlUpdate(const BrewControlUpdate& update) {
coldExtractionConfigChanged = true;
}
}
+ if (update.hasCXPreInfDuty) {
+ float v = update.cxPreInfDuty;
+ if (isfinite(v) && v >= COLDEX_DUTY_MIN && v <= COLDEX_DUTY_MAX && fabs(v - coldExtractionPreInfDutyPercent) > 0.01f) {
+ coldExtractionPreInfDutyPercent = v;
+ EEPROM.put(EEPROM_ADDR_COLDEX_PREINF_DUTY, coldExtractionPreInfDutyPercent);
+ coldExtractionConfigChanged = true;
+ }
+ }
if (update.hasCXTarget) {
float v = update.cxTarget;
if (isfinite(v) && v >= COLDEX_TARGET_GRAMS_MIN && v <= COLDEX_TARGET_GRAMS_MAX && fabs(v - coldExtractionTargetGrams) > 0.01f) {
@@ -7715,6 +7747,8 @@ void handleSaveBrewControl(AsyncWebServerRequest *request) {
if (update.hasCXPulseMs) { update.cxPulseMs = (uint16_t)request->arg("cxPulseMs").toInt(); }
update.hasCXDuty = request->hasArg("cxDuty");
if (update.hasCXDuty) { update.cxDuty = request->arg("cxDuty").toFloat(); }
+ update.hasCXPreInfDuty = request->hasArg("cxPreInfDuty");
+ if (update.hasCXPreInfDuty) { update.cxPreInfDuty = request->arg("cxPreInfDuty").toFloat(); }
update.hasCXTarget = request->hasArg("cxTarget");
if (update.hasCXTarget) { update.cxTarget = request->arg("cxTarget").toFloat(); }
update.hasCXMaxSecs = request->hasArg("cxMaxSecs");