feat(P4): Cold Extraction am Touch-Display + RX-Zeilenlimit-Fix (1.1.0 / S3 5.3.2)

Display-Seite der kalten Extraktion: Schalter unter "Modi" auf der Temperatur-Seite
mit Erklärzeile, eigener Slot in der Statuszeile direkt hinter der
Sicherheitsmeldung, "Cold Extraction - Heizen aus" in der Wasser-Kachel statt eines
unerreichbaren Sollwerts, unterdrückter Aufheiz-Countdown, "Vorbenetzung" als erste
Phase des kalten Bezugs, Parameter-Anzeige auf der Brew-Seite und "(kalt)"-Markierung
in der Statistik (kalte Bezüge zählen nicht in die mittlere Dauer).

Bug-Fix P4: PROTO_RX_LINE_MAX lag bei 2048 Bytes, während die State-Zeile der S3
bereits rund 2,1 KB erreicht. Mit langem Status-, Profil- oder SSID-Text lag sie
darüber — dann wurde die ganze Zeile verworfen und das Display fror auf dem letzten
Stand ein, ohne dass die Verbindung als tot erkannt wurde. Limit jetzt 4096 Bytes;
der Puffer bleibt bei 2 KB reserviert, weil der interne RAM knapp ist und eine
Arduino-String nicht ins PSRAM alloziert werden kann.

S3 5.3.2: Sperrgrund und transiente Meldung gehen als Codes (cxBlock/cxNotice) statt
als Klartext ans Display, dazu cxPreInf für die Phasenleiste. Der bis zu 110 Byte
lange Freitext hätte die State-Zeile über das Zeilenlimit gedrückt; die Texte rendert
jetzt das Display, analog zur Trennung statusKey/statusText. Auf dem OLED und in den
ack-Antworten bleibt der Klartext unverändert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
raw-designs
2026-08-10 23:56:17 +02:00
co-authored by Claude Opus 5
parent 57b7969f8b
commit e8edc8eb30
8 changed files with 324 additions and 21 deletions
+58 -8
View File
@@ -369,7 +369,7 @@ Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire);
* Firmware-Informationen
************************************************************************************/
String version = "5.3.1";
String version = "5.3.2";
String versionHersteller = "Thomas M&uuml;ller";
String versionHerstellerMail =
"<a href='mailto:thomas@mueller.black' class='info-link'>thomas@mueller.black</a>";
@@ -832,11 +832,36 @@ 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
// Sperrmeldung fuer das OLED, wenn ein kalter Bezug abgewiesen bzw. abgebrochen wurde.
// Der Klartext bleibt S3-intern; ans Touch-Display gehen nur die Codes unten, damit die
// State-Zeile kurz bleibt (der P4 hat ein Zeilenlimit) und das Display seine eigenen
// Texte rendern kann - dieselbe Trennung wie bei statusKey/statusText.
const unsigned long COLDEX_BLOCK_MESSAGE_MS = 6000UL;
String coldExtractionBlockMessage = "";
unsigned long coldExtractionBlockMessageUntilMs = 0;
// Sperrgrund (cxBlock): 0=frei, 1=nicht freigeschaltet, 2=Standby, 3=Wartung,
// 4=Reinigungsassistent, 5=PID-Tuning, 6=Sensorfehler, 7=Wasserkessel zu warm
enum ColdExtractionBlock : uint8_t {
COLDEX_BLOCK_NONE = 0,
COLDEX_BLOCK_NOT_ENABLED = 1,
COLDEX_BLOCK_STANDBY = 2,
COLDEX_BLOCK_MAINTENANCE = 3,
COLDEX_BLOCK_CLEANING = 4,
COLDEX_BLOCK_TUNING = 5,
COLDEX_BLOCK_SENSOR = 6,
COLDEX_BLOCK_TOO_WARM = 7
};
// Transiente Meldung (cxNotice): 0=keine, 1=Start abgewiesen, 2=Bezug wegen fehlenden
// Zulaufs beendet, 3=Umschalten waehrend eines Bezugs abgelehnt
enum ColdExtractionNotice : uint8_t {
COLDEX_NOTICE_NONE = 0,
COLDEX_NOTICE_START_REJECTED = 1,
COLDEX_NOTICE_NO_FLOW = 2,
COLDEX_NOTICE_BUSY = 3
};
uint8_t coldExtractionNotice = COLDEX_NOTICE_NONE;
/************************************************************************************
* Standardwerte und Default-Einstellungen
************************************************************************************/
@@ -4032,6 +4057,19 @@ bool coldExtractionConditionsOk() {
coldExtractionTemperatureOk();
}
// Sperrgrund als Code fuer das Touch-Display (0 = frei). Gleiche Reihenfolge wie
// coldExtractionBlockReason(), damit Text und Code immer dieselbe Ursache nennen.
uint8_t coldExtractionBlockCode() {
if (!coldExtractionEnabled) return COLDEX_BLOCK_NOT_ENABLED;
if (standbyModeActive) return COLDEX_BLOCK_STANDBY;
if (wartungsModusAktiv) return COLDEX_BLOCK_MAINTENANCE;
if (cleaningAssistantActive) return COLDEX_BLOCK_CLEANING;
if (autoTuneWasserActive || autoTuneDampfActive) return COLDEX_BLOCK_TUNING;
if (wasserSensorError || wasserSafetyShutdown || !isfinite(InputWasser)) return COLDEX_BLOCK_SENSOR;
if (!coldExtractionTemperatureOk()) return COLDEX_BLOCK_TOO_WARM;
return COLDEX_BLOCK_NONE;
}
// Klartext-Grund, warum gerade keine kalte Extraktion moeglich ist ("" = alles frei).
String coldExtractionBlockReason() {
if (!coldExtractionEnabled) {
@@ -5253,6 +5291,7 @@ void loop() {
// kalten Bezug wieder abkuehlen zu muessen. Nur die Laufzeitdaten werden verworfen.
resetColdExtractionRuntime();
coldExtractionBlockMessage = "";
coldExtractionNotice = COLDEX_NOTICE_NONE;
} else if (lastStandbyModeActive) {
startupTime = currentMillis;
ecoForcedActive = false;
@@ -5484,6 +5523,7 @@ void loop() {
stopForTime = true;
coldExtractionBlockMessage = "Cold Extraction beendet: kein Zulauf (Puck dicht oder Pumpenschutz?).";
coldExtractionBlockMessageUntilMs = millis() + COLDEX_BLOCK_MESSAGE_MS;
coldExtractionNotice = COLDEX_NOTICE_NO_FLOW;
}
}
@@ -5985,6 +6025,7 @@ void updateDisplay() {
if (coldExtractionBlockMessage.length() > 0) {
if ((long)(millis() - coldExtractionBlockMessageUntilMs) >= 0) {
coldExtractionBlockMessage = "";
coldExtractionNotice = COLDEX_NOTICE_NONE;
} else if (!standbyModeActive) {
display.clearDisplay();
display.setTextSize(1);
@@ -15491,6 +15532,8 @@ bool executeDashboardAction(const String& action, const String& value,
if (!success) {
coldExtractionBlockMessage = message;
coldExtractionBlockMessageUntilMs = millis() + COLDEX_BLOCK_MESSAGE_MS;
coldExtractionNotice = (shotActive || shotSoftwareActive) ? COLDEX_NOTICE_BUSY
: COLDEX_NOTICE_START_REJECTED;
}
} else if (action == "startShot") {
if (standbyModeActive) {
@@ -15509,6 +15552,7 @@ bool executeDashboardAction(const String& action, const String& value,
if (message.length() == 0) { message = "Cold Extraction derzeit nicht moeglich."; }
coldExtractionBlockMessageUntilMs = millis() + COLDEX_BLOCK_MESSAGE_MS;
coldExtractionBlockMessage = message;
coldExtractionNotice = COLDEX_NOTICE_START_REJECTED;
beepShort();
success = false;
} else {
@@ -16432,15 +16476,21 @@ static void buildTouchUartStateJson(char *buffer, size_t bufferSize) {
offset += snprintf(buffer + offset, bufferSize - offset, "\"cxResting\":%s,", coldExtractionPumpResting ? "true" : "false");
dtostrf(coldExtractionMaxTempC, 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"cxMaxTemp\":%s,", floatBuf);
dtostrf(coldExtractionTargetGrams, 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"cxTarget\":%s,", floatBuf);
dtostrf(coldExtractionPreInfusionSeconds, 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"cxPreInf\":%s,", floatBuf);
dtostrf(coldExtractionMaxSeconds, 5, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"cxMaxSecs\":%s,", floatBuf);
// Sperrmeldung nach abgewiesenem Start (leer = keine); laeuft nach COLDEX_BLOCK_MESSAGE_MS ab.
// Die Texte stammen ausschliesslich aus dieser Firmware und enthalten keine JSON-Sonderzeichen.
// Sperrgrund + transiente Meldung als Codes (kein Freitext - die State-Zeile muss kurz
// bleiben, der P4 begrenzt eingehende Zeilen; Texte rendert das Display selbst).
offset += snprintf(buffer + offset, bufferSize - offset, "\"cxBlock\":%u,", (unsigned int)coldExtractionBlockCode());
{
const char* cxMsg = "";
if (coldExtractionBlockMessage.length() > 0 && (long)(millis() - coldExtractionBlockMessageUntilMs) < 0) {
cxMsg = coldExtractionBlockMessage.c_str();
uint8_t notice = COLDEX_NOTICE_NONE;
if (coldExtractionNotice != COLDEX_NOTICE_NONE) {
if ((long)(millis() - coldExtractionBlockMessageUntilMs) < 0) {
notice = coldExtractionNotice;
} else {
coldExtractionNotice = COLDEX_NOTICE_NONE;
}
}
offset += snprintf(buffer + offset, bufferSize - offset, "\"cxMessage\":\"%s\",", cxMsg);
offset += snprintf(buffer + offset, bufferSize - offset, "\"cxNotice\":%u,", (unsigned int)notice);
}
dtostrf(brewByWeightTargetGrams, 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"bbwTarget\":%s,", floatBuf);
dtostrf(brewByWeightOffsetGrams, 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"bbwOffset\":%s,", floatBuf);