Files
Dual-PID/Doku/JC-Display_UART-Protokoll.md
T
raw-designsandClaude Opus 5 e8edc8eb30 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>
2026-08-10 23:56:17 +02:00

593 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# JC4880P443C-I-W Touch-Display — UART-Protokoll & Architektur
Status: Entwurf v1.0 der Doku · Firmware-Protokollversion: **1** (vorhanden, abgeschaltet) ·
Ziel-Protokollversion: **2** (geplante Erweiterungen)
Diese Datei beschreibt die Anbindung des Touch-Displays **JC4880P443C-I-W** an die
Siebträger-Steuerung (`Dual_PID_FastHeatUp.ino`). Sie dokumentiert den **bereits
vorhandenen** UART-Code, bewertet ihn und legt die geplanten Erweiterungen fest.
Es wird **noch kein Code geändert** — diese Doku ist die Grundlage dafür.
---
## 1. Architektur-Überblick
Bewusste Designentscheidung (siehe Projektbesprechung):
- Das **Webinterface bleibt auf der Hauptplatine** (ESP32-S3). Es bleibt unverändert
die zentrale Steuerungs- und Konnektivitätsinstanz.
- Das **JC-Display ist ein vollwertiges Touch-Bediengerät**, das über UART angebunden
ist und denselben Funktionsumfang bietet wie das Webinterface — aber **kein** eigenes
WLAN/Webserver betreibt.
- Das **OLED (SH1106G) bleibt optional** und wird wie bisher über ein **Compile-Flag**
gewählt (`ENABLE_DISPLAY`). Es ist unabhängig vom JC-Display.
```
+----------------------------+ UART (GPIO43/44) +---------------------------+
| Hauptplatine ESP32-S3 | <----------------------------------> | JC4880P443C-I-W |
| Dual_PID_FastHeatUp.ino | JSON-Zeilen, 230400 Baud, 8N1 | ESP32-P4 (UI/LVGL) |
| | | + ESP32-C6 (WLAN/BT)* |
| - PID-Regelung (Wasser/ | S3 -> P4: state / ack / error | |
| Dampf), SSRs, Sensoren | P4 -> S3: op-Kommandos | - Touch-Bedienung |
| - Waage, Profile, EEPROM | | - Statusanzeige |
| - WLAN + Webinterface | | - Einstellungen |
| - OLED (optional) | | ST7701 / GT911 / ES8311 |
+----------------------------+ +---------------------------+
* Der ESP32-C6 auf dem JC-Display (WLAN/BT) wird in diesem Projekt NICHT für die
Maschinensteuerung benötigt. WLAN/Webinterface laufen weiterhin auf der S3.
```
### Rollenverteilung
| Aufgabe | S3 (Hauptplatine) | P4 (JC-Display) |
|---|---|---|
| Echtzeit-Regelung (PID, SSR, Sicherheit) | **ja** | nein |
| Sensoren (NTC, MAX6675, Waage) | **ja** | nein |
| Persistenz (EEPROM, FFat-Profile) | **ja** | nein |
| WLAN + Webinterface | **ja** | nein |
| Touch-UI / Bedienoberfläche | nein | **ja** |
| OLED-Statusanzeige (optional) | **ja** (Compile-Flag) | — |
| "Quelle der Wahrheit" für den Zustand | **ja** | spiegelt nur |
**Wichtig:** Die S3 bleibt die alleinige Quelle der Wahrheit. Das P4-Display hält keinen
eigenen, abweichenden Zustand, sondern stellt den von der S3 gepushten `state` dar und
schickt Bedienwünsche als Kommandos. So bleiben Web-UI, OLED und Touch-Display konsistent.
---
## 2. Hardware-Anbindung
| Parameter | Wert | Quelle im Code |
|---|---|---|
| UART-Instanz | `Serial1` | `HardwareSerial& touchUart = Serial1;` |
| TX (S3 -> P4) | **GPIO43** | `#define TOUCH_UART_TX_PIN 43` |
| RX (S3 <- P4) | **GPIO44** | `#define TOUCH_UART_RX_PIN 44` |
| Baudrate | 230400 | `#define TOUCH_UART_BAUDRATE 230400` |
| Format | 8N1 | `touchUart.begin(..., SERIAL_8N1, RX, TX)` |
| Logikpegel | 3,3 V | ESP32-S3 / ESP32-P4 beide 3,3 V — **keine Pegelwandlung nötig** |
### Verkabelung (gekreuzt)
```
S3 GPIO43 (TX) -----> P4 RX
S3 GPIO44 (RX) <----- P4 TX
S3 GND ------ P4 GND (gemeinsame Masse zwingend)
```
> **Hinweis (bewusst akzeptiert):** GPIO43/44 sind beim ESP32-S3 zugleich die Standard-
> Pins der seriellen Debug-Konsole (UART0). Solange das JC-Display dort angeschlossen ist,
> steht die klassische serielle Debug-Ausgabe an diesen Pins nicht parallel zur Verfügung.
> Debug/Logging erfolgt dann über USB-CDC bzw. das Webinterface. Diese Pins wurden
> ausdrücklich beibehalten, weil sie auf der Platine herausgeführt sind.
---
## 3. Transport & Framing
- **Zeilenbasiert:** Eine Nachricht = eine kompakte JSON-Zeile, abgeschlossen mit `\n`.
`\r` wird empfangsseitig ignoriert.
- **Max. Zeilenlänge (RX):** `TOUCH_UART_MAX_LINE_LEN` = 1024 Byte. Längere Zeilen werden
verworfen und mit einem `error` (`"Zeile zu lang"`) quittiert (Overflow-Schutz).
- **State-Puffer (TX):** `TOUCH_UART_JSON_BUFFER_SIZE` = 2400 Byte (`snprintf`-basiert).
- **Encoding:** ASCII/UTF-8. Strings werden escaped (`jsonEscapeForUart`,
`escapedStatusText` etc.): `\`, `"`, `\n`, `\r`, `\t`.
- **Kein Binär-Frame, kein CRC in v1.** (CRC/Handshake siehe v2-Erweiterungen.)
### Nachrichtenrichtungen
| Richtung | `type`/`op` | Bedeutung |
|---|---|---|
| **S3 -> P4** | `type:"state"` | Vollständiger Zustands-Push (Telemetrie) |
| **S3 -> P4** | `type:"ack"` | Bestätigung eines Kommandos (`ok:true/false`, `message`) |
| **S3 -> P4** | `type:"error"` | Fehler (Parsing, unbekannter op, Overflow) |
| **S3 -> P4** | `type:"profiles"` | Liste der Profilnamen (Antwort auf `listProfiles`) |
| **S3 -> P4** | `type:"usageStats"` | Nutzungsstatistik (Antwort auf `getUsageStats`) |
| **P4 -> S3** | `op:"..."` | Kommando vom Display (siehe §6) |
Jedes Kommando trägt eine optionale **`id`** (uint32), die in `ack`/`error` zurückgespiegelt
wird (Request/Response-Korrelation).
---
## 4. Push-Verhalten (S3 -> P4)
Implementiert in `touchUartTick()`:
- **Client-Aktivierung:** Erst nachdem das Display ein Kommando gesendet hat, gilt es als
aktiv (`touchUartClientActive`). Ohne aktiven Client wird **nicht** gepusht.
- **Client-Timeout:** `TOUCH_UART_CLIENT_TIMEOUT_MS` = 15000 ms ohne Kommando -> inaktiv.
-> Das Display muss regelmäßig `ping` (oder ein beliebiges Kommando) senden (Heartbeat).
- **Adaptives Intervall:**
- Leerlauf: `TOUCH_UART_INTERVAL_IDLE_MS` = 1000 ms
- Aktiv (Bezug/Flush/Burst): `TOUCH_UART_INTERVAL_ACTIVE_MS` = 250 ms
- **Force-Push** bei Zustandswechseln: Bezug, Flush, Dampfkreis (= Dampfbezug/Dampf-Flush),
Tarier-Phase, Sensorfehler/Sicherheitsabschaltung, Reinigungsassistent, Standby (Details in §5).
- **Burst (250 ms)** zusätzlich während: Bezug, Flush, Dampfkreis, Tarier-Phase, Reinigungsassistent.
- **Burst** nach Bezugsende: `TOUCH_UART_SHOT_END_GRACE_MS` = 500 ms Nachlauf.
**Empfehlung P4-Seite:** alle ~5 s `ping` senden (Heartbeat), nach jedem Bedienvorgang
zusätzlich `getState` ist nicht nötig (die S3 pusht nach jedem Kommando automatisch `state`).
---
## 5. Telemetrie — `state`-Objekt (Protokoll v1, IST-Zustand)
Erzeugt von `buildTouchUartStateJson()`. Alle Felder in einem flachen JSON-Objekt.
| Feld | Typ | Bedeutung |
|---|---|---|
| `type` | string | immer `"state"` |
| `protocolVersion` | int | aktuell `1` |
| `firmwareVersion` | string | z. B. `"4.8.1"` |
| `tempW` | float | Ist-Temperatur Wasser (angezeigt) |
| `setW` | float | Soll-Temperatur Wasser |
| `tempD` | float | Ist-Temperatur Dampf (angezeigt) |
| `setD` | float | Soll-Temperatur Dampf |
| `statusText` | string | Klartext-Status (DE), z. B. `"Bezug aktiv..."` |
| `statusKey` | string | maschinenlesbarer Statusschlüssel (siehe §7) |
| `shotActive` | bool | Bezug läuft |
| `shotElapsedMs` | uint | vergangene Bezugszeit in ms |
| `steamCircuitActive` | bool | Dampf-/Wasserkreis aktiv |
| `steamFlushActive` | bool | Steam-Flush aktiv |
| `flushActive` | bool | Flush aktiv |
| `ecoActive` | bool | Eco-Modus aktiv |
| `standbyActive` | bool | Standby aktiv |
| `maintenanceActive` | bool | Wartungsmodus aktiv |
| `steamHeatDisabled` | bool | Dampfheizung per Nutzer deaktiviert |
| `lightOn` | bool | Beleuchtung an |
| `piezoEnabled` | bool | Piezo-Summer aktiv |
| `profile` | string | zuletzt geladenes/gespeichertes Profil, leer = keines (additiv seit S3 5.1.1) |
| `profDirty` | bool | profil-relevante Einstellungen seit dem Laden geändert (additiv seit S3 5.1.1) |
| `weight` | float | aktuelles Waagengewicht (UI) |
| `targetWeight` | float | Ziel-Gewicht (Brew-by-Weight) |
| `scaleEnabled` | bool | Waage aktiviert |
| `scaleType` | uint | Waagentyp |
| `scaleConnected` | bool | Waage verbunden |
| `hx711CalFactor` | float | HX711-Kalibrierfaktor |
| `hx711DisplaySmoothing` | bool | HX711-Anzeigeglättung |
| `piEnabled` | bool | Pre-Infusion aktiv |
| `bbtEnabled` | bool | Brew-by-Time aktiv |
| `bbwEnabled` | bool | Brew-by-Weight aktiv |
| `sbtEnabled` | bool | Steam-by-Time aktiv |
| `bbtSecs` | float | Brew-by-Time Zielzeit (s) |
| `sbtSecs` | float | Steam-by-Time Zielzeit (s) |
| `piDurSecs` | float | Pre-Infusion Dauer (s) |
| `piPauseSecs` | float | Pre-Infusion Pause (s) |
| `bbwTarget` | float | Brew-by-Weight Zielgewicht (g) |
| `bbwOffset` | float | Brew-by-Weight Offset (g) |
| `caseSensorEnabled` | bool | Gehäusesensor aktiv |
| `caseTempDashboard` | bool | Gehäusetemp. im Dashboard sichtbar (effektiv) |
| `caseTempDashboardSetting` | bool | Einstellung Dashboard-Anzeige |
| `caseTempDisplaySetting` | bool | Einstellung Display-Anzeige |
| `caseSensorType` | uint | Typ des Gehäusesensors |
| `caseOffset` | float | Offset Gehäusesensor |
| `caseTemp` | float\|null | Gehäusetemperatur (`null` bei Fehler/inaktiv) |
| `maintenanceInterval` | int | Wartungsintervall |
| `maintenanceIntervalCounter` | uint | Wartungszähler |
| `flushDurationSeconds` | uint | Flush-Dauer (s) |
| `steamFlushDurationSeconds` | uint | Steam-Flush-Dauer (s) |
| `xSwitchAction` | uint | X-Switch Kurz-Aktion |
| `xSwitchLongAction` | uint | X-Switch Lang-Aktion |
**v2-Felder (seit Phase A.1 implementiert):**
| Feld | Typ | Bedeutung |
|---|---|---|
| `dutyW` | float | Heizleistung Wasser in % (`OutputWasser / windowSizeWasser * 100`) |
| `dutyD` | float | Heizleistung Dampf in % |
| `kpW` / `kiW` / `kdW` | float | PID-Parameter Wasser (read-only) |
| `kpD` / `kiD` / `kdD` | float | PID-Parameter Dampf (read-only) |
| `autoTuneW` | bool | AutoTune Wasser läuft |
| `autoTuneD` | bool | AutoTune Dampf läuft |
| `autoTuneWStatus` | uint8 | Ergebnis/Grund letzter Wasser-Lauf: 0=Idle, 1=läuft, 2=Erfolg, 3=Failsafe (>9 Peaks, evtl. ungenau), 4=degeneriert (keine Schwingung, Werte verworfen), 5=Sensorfehler, 6=Sicherheitsabschaltung, 7=manuell abgebrochen |
| `autoTuneDStatus` | uint8 | Ergebnis/Grund letzter Dampf-Lauf (Codes wie `autoTuneWStatus`) |
| `wifiConnected` | bool | STA verbunden **oder** AP-Modus aktiv |
| `apMode` | bool | Access-Point-Modus aktiv |
| `ip` | string | aktuelle IP (STA-IP, sonst softAP-IP) |
| `ssid` | string | verbundene SSID bzw. AP-SSID |
| `rssi` | int | WLAN-Signalstärke (dBm), `0` wenn nicht STA-verbunden |
> `protocolVersion` ist mit diesen Feldern auf **2** angehoben.
**v2.1-Felder (Timer-Kopplung, Sicherheit, Reinigungsassistent):**
| Feld | Typ | Bedeutung |
|---|---|---|
| `steamElapsedMs` | uint | vergangene Dampfbezugszeit in ms (rückwirkende Timer-Kopplung, analog `shotElapsedMs`) |
| `flushElapsedMs` | uint | vergangene Wasser-Flush-Zeit in ms (rückwirkende Timer-Kopplung) |
| `scaleTaring` | bool | Tarier-Phase vor einem Brew-by-Weight-Bezug läuft (HX711); Display zeigt „Tariere…" |
| `wasserSensorError` | bool | Wassersensor liefert ungültigen Wert |
| `wasserSafetyShutdown` | bool | Wassertemperatur über Sicherheitslimit → Heizung aus |
| `dampfSensorError` | bool | Dampfsensor liefert ungültigen Wert |
| `dampfSafetyShutdown` | bool | Dampftemperatur über Sicherheitslimit → Heizung aus |
| `cleaningActive` | bool | Reinigungsassistent läuft |
| `cleaningWaiting` | bool | Reinigungsassistent heizt noch auf 93 °C vor |
| `cleaningBrewPhase` | bool | aktuelle Phase: `true` = Bezug, `false` = Pause |
| `cleaningCycle` | uint | aktueller Zyklus |
| `cleaningCycles` | uint | Gesamtzahl der Zyklen |
| `cleaningBrewSeconds` | uint | Bezugsdauer je Zyklus (s) |
| `cleaningPauseSeconds` | uint | Pausendauer je Zyklus (s) |
| `cleaningPhaseRemainSec` | int | verbleibende Sekunden der aktuellen Phase |
> **Force-Push** zusätzlich bei: Dampfkreis start/stop (deckt Dampfbezug **und** Dampf-Flush ab),
> Tarier-Phase start/stop, Sensorfehler/Sicherheitsabschaltung (Flanke), Reinigungsassistent start/stop,
> Standby-Wechsel. **Burst (250 ms)** zusätzlich während: Dampfkreis, Tarier-Phase, Reinigungsassistent.
---
## 6. Kommando-Referenz (Protokoll v1, IST-Zustand)
Verarbeitet in `touchUartHandleCommandLine()`. Alle Kommandos: `{"op":"...","id":<n>,...}`.
Antwort jeweils `ack`/`error`; bei zustandsändernden Ops folgt automatisch ein `state`-Push.
| `op` | Zusatzfelder | Wirkung | Antwort |
|---|---|---|---|
| `ping` | — | Heartbeat | `ack` `"pong"` |
| `getState` | — | erzwingt sofortigen State-Push | `state` + `ack` |
| `listProfiles` | — | Profilnamen anfordern | `profiles` + `ack` |
| `getUsageStats` | — | Nutzungsstatistik anfordern | `usageStats` + `ack` |
| `action` | `action` (string), `value` (opt.) | ruft `executeDashboardAction()` auf — **dieselbe** Aktions-Engine wie das Web-Dashboard | `ack` + `state` |
| `loadProfile` | `profile` (string) | Profil laden | `ack` + `state` |
| `saveProfile` | `profile` (string) | aktuelle Einstellungen als Profil speichern | `ack` (+ `state`) |
| `saveBrew` | `bbtEnabled`,`bbtSecs`,`bbwEnabled`,`bbwTarget`,`bbwOffset`,`piEnabled`,`piDurSecs`,`piPauseSecs`,`sbtEnabled`,`sbtSecs` (alle optional) | `applyBrewControlUpdate()` | `ack` + `state` |
| `saveService` | `piezoEnabled`,`maintenanceInterval`,`flushDurationSeconds`,`steamFlushDurationSeconds`,`resetMaintenanceCounter` | `applyServiceSettingsUpdate()` | `ack` + `state` |
| `saveSensor` | `section` (`case`/`scale`/`xswitch`) + jeweilige Felder (`caseSensorEnabled`, `scaleEnabled`, `scaleType`, `hx711CalFactor`, `xSwitchAction`, …) | `applySensorSettingsUpdate()` | `ack` + `state` |
| `startCleaning` | `cycles`,`brewSeconds`,`pauseSeconds` (alle optional, sonst aktuelle Werte) | setzt die Parameter und startet den Reinigungsassistenten (Prüfung via `canStartCleaningAssistant()`; bei Ablehnung `ok:false` mit Begründung) | `ack` + `state` |
| `stopCleaning` | — | bricht den laufenden Reinigungsassistenten ab | `ack` + `state` |
### Antwort-Envelopes
```jsonc
// ack
{"type":"ack","id":42,"ok":true,"message":"Brew gespeichert"}
// error
{"type":"error","id":42,"message":"op fehlt"}
// profiles
{"type":"profiles","id":7,"profiles":["Espresso","Lungo","Reinigung"]}
```
### Die wichtigste Erweiterungsstelle: `action`
`executeDashboardAction(action, value, success, message, settingsChanged, pidNeedsUpdate)`
ist **die gemeinsame Aktions-Engine** von Web-Dashboard und Touch-Display. Jede Aktion, die
das Webinterface auslöst (Standby, Eco, Licht, Flush, Bezug-Steuerung, Setpoints,
Reinigungsassistent, …), ist damit ohne Mehraufwand auch übers Display verfügbar.
### 6.1 Vollständige `action`-Referenz
Alle gültigen `action`-Strings aus `executeDashboardAction()` (Stand v4.8.1, Zeilen
~1339913863). Aufruf vom Display als `{"op":"action","id":<n>,"action":"<name>","value":"<v>"}`.
`value` ist immer ein **String**; die Engine konvertiert (`toFloat`/`toInt`, bzw. Vergleich
`== "true"`).
**Temperatur-Sollwerte**
| `action` | `value` | Wirkung |
|---|---|---|
| `setTempW` | float `0..135` | Wasser-Sollwert (°C), persistiert in EEPROM |
| `setTempD` | float `0..200` | Dampf-Sollwert (°C), persistiert in EEPROM |
| `setTempDisplayMode` | int (`CLASSIC..RAILS`) | Temperatur-Anzeigemodus im Dashboard |
**Betriebsmodi**
| `action` | `value` | Wirkung |
|---|---|---|
| `startEco` | — | Eco-Modus aktivieren |
| `stopEco` | — | Eco-Modus für diesen Zyklus beenden |
| `activateStandby` | — | Standby aktivieren (Software) |
| `deactivateStandby` | — | Standby aufheben (Software) |
| `activateStandbyLegacy` | — | Standby-Override (schalterbasiert) reaktivieren |
| `deactivateStandbyLegacy` | — | Standby per WebUI aufheben (nur bei aktivem Schalter) |
| `toggleMaintenance` | `"true"`/`"false"` | Wartungsmodus ein/aus (temporär) |
**Bezug / Dampf / Spülen**
| `action` | `value` | Wirkung |
|---|---|---|
| `startShot` | — | Bezug starten (Software) |
| `stopShot` | — | Bezug beenden |
| `startSteam` | — | Dampfbezug starten |
| `stopSteam` | — | Dampfbezug / Dampf-Flush beenden |
| `flush` | — | Spülen starten (Dauer = `flushDurationSeconds`) |
| `steamFlush` | — | Dampf-Flush starten (Dauer = `steamFlushDurationSeconds`) |
| `overrideSteamDelay` | — | Dampfverzögerung temporär überspringen |
| `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 ist
**persistent** (EEPROM) und überlebt Standby und Neustart; er endet nur durch explizites
Ausschalten, durch Entzug der Freischaltung auf `/Brew-Control` oder durch einen Werksreset.
Das P4-UI sollte `cxActive` deshalb dauerhaft sichtbar spiegeln — sonst wundert sich der
Benutzer, warum die Maschine nicht heizt. 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 |
|---|---|---|
| `toggleBoostW` | `"true"`/`"false"` | Boost Wasser (EEPROM) |
| `toggleBoostD` | `"true"`/`"false"` | Boost Dampf (EEPROM) |
| `togglePI` | `"true"`/`"false"` | Pre-Infusion (EEPROM) |
| `toggleBBT` | `"true"`/`"false"` | Brew-by-Time (EEPROM) |
| `toggleBBW` | `"true"`/`"false"` | Brew-by-Weight (EEPROM) |
| `toggleLight` | `"true"`/`"false"` | Beleuchtung (SSR + EEPROM) |
**Waage**
| `action` | `value` | Wirkung |
|---|---|---|
| `tareScale` | — | Tarieren (nur I2C/HX711; ESP-NOW nicht über UI) |
| `toggleScaleMode` | `"true"`/`"false"` | Waage-Anzeige-/Wiegemodus (temporär) |
**Profil**
| `action` | `value` | Wirkung |
|---|---|---|
| `loadProfile` | Profilname | Profil laden (identisch zu `op:"loadProfile"`) |
> **Sperre bei Reinigungsassistent:** Solange `cleaningAssistantActive`, weisen
> `startEco`, `toggleMaintenance`, `startShot`, `stopShot`, `startSteam`, `stopSteam`,
> `flush`, `steamFlush` mit `success:false` und entsprechender Meldung ab. Das Display
> sollte diese Aktionen dann ausgegraut darstellen (Status `maintenance`).
> **Unbekannte Aktion:** liefert `message:"Unbekannte Aktion."` und `success:false`.
---
## 7. Status-Schlüssel (`statusKey`)
Aus `getTouchUartStatus()`. Für sprach-unabhängige UI-Logik/Icons auf dem P4 verwenden
(statt `statusText` zu parsen):
`ready`, `error`, `standby`, `maintenance`, `tuning`, `brewing`, `eco`, `fastheatup`,
`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 |
| `cxPreInf` | float | Vorbenetzung in s (für die Phasenleiste des kalten Bezugs) |
| `cxMaxSecs` | float | Sicherheits-Timeout des Bezugs in s |
| `cxBlock` | uint8 | Sperrgrund (siehe unten), `0` = frei |
| `cxNotice` | uint8 | transiente Meldung, `0` = keine (läuft nach ~6 s selbst ab) |
`cxBlock`: `0` frei, `1` nicht freigeschaltet, `2` Standby, `3` Wartungsmodus,
`4` Reinigungsassistent, `5` PID-Tuning, `6` Sensorfehler, `7` Wasserkessel zu warm.
`cxNotice`: `0` keine, `1` Start abgewiesen (Grund steht in `cxBlock`), `2` Bezug wegen
fehlenden Zulaufs beendet, `3` Umschalten während eines Bezugs abgelehnt.
Bewusst **Codes statt Klartext**: Die State-Zeile muss unter dem Zeilenlimit des P4 bleiben
(`PROTO_RX_LINE_MAX`; sie liegt real bereits bei ~2,1 KB), und die Texte gehören ins Display —
dieselbe Trennung wie bei `statusKey`/`statusText`.
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.
---
## 8. Geplante Erweiterungen — Protokoll v2
Für ein vollwertiges 480×800-Touch-UI fehlen v1 noch einige Daten/Funktionen. Geplant:
### 8.1 Neue Telemetrie-Felder im `state`
- **[erledigt A.1] Netzwerk:** `wifiConnected`, `apMode`, `ssid`, `ip`, `rssi`.
- **[erledigt A.1] PID:** aktuelle Kp/Ki/Kd Wasser & Dampf (read-only).
- **[erledigt A.1] AutoTune:** `autoTuneW` / `autoTuneD` (laufend ja/nein).
- **[erledigt] AutoTune-Ergebnis:** `autoTuneWStatus` / `autoTuneDStatus` (Erfolg/Abbruch + Grund; siehe §8.1-Tabelle). P4-Dashboard blendet das Ergebnis nach Lauf-Ende ~30 s als Banner ein; S3-Web `/PID-Tuning` zeigt es persistent.
- **[erledigt A.1] Heizleistung:** Duty Wasser & Dampf (`dutyW`/`dutyD`, für Balkenanzeige).
- **[offen] Flow-Rate:** `flowRate` (g/s) als **Live-Zahl** (kein Chart) — bei Bedarf später.
- **[offen] AutoTune-Fortschritt:** numerischer Fortschritt (Peak-Count) — derzeit nur Lauf-Flag + Ergebnis-/Abbruchstatus.
> **Kein Chart am Display** (Entscheidung): Es wird **keine** Verlaufs-/Historiendaten-
> Übertragung benötigt. Damit entfällt der aufwändigste Teil (großer Ringpuffer, Chunking).
### 8.2 Neue Kommandos (`op`)
| `op` | Status | Zweck |
|---|---|---|
| `hello` | **erledigt** | Handshake: P4 meldet sich, S3 antwortet mit `type:"hello"` + `firmwareVersion`, `protocolVersion`, `capabilities[]`, danach `state` + `ack`. |
| `getProfileDetails` | **erledigt** | Profil-Inhalt (`profile`) → Antwort `type:"profileDetails"` mit allen Feldern (Setpoints, Offsets, Kp/Ki/Kd, Boost, Window, Eco, Brew, Piezo, FastHeatUp). |
| `setPid` | **erledigt** | Felder `kpW/kiW/kdW/kpD/kiD/kdD` (alle optional) → EEPROM + Live-`SetTunings`. |
| `startAutotune` | **erledigt** | `target`:`water`\|`steam` (auch `wasser`/`dampf`). Spiegelt `handleAutoTune*` (Guard: nur wenn keins läuft, Wartungsmodus wird zurückgesetzt). |
| `stopAutotune` | **erledigt** | Bricht laufendes AutoTune Wasser **und** Dampf ab. |
| `scanWifi` | **erledigt** | Startet einen **asynchronen** WLAN-Scan. Sofort `ack`; wenn fertig, pusht die S3 `type:"wifiNetworks"` (Array `{ssid,rssi,enc}`, max. 20). Timeout 15 s. |
| `setWifi` | **erledigt** | Felder `ssid` (Pflicht) + `password` (optional, offenes Netz = leer). Speichert in `WiFiConfig` (EEPROM) und ruft `WiFi.begin(...)` **nicht-blockierend** auf; vorhandene IP-Einstellungen bleiben erhalten. Fortschritt über die `wifi*`-Felder im `state`. |
| `getUsageStats` | **erledigt** | Nutzungsstatistik anfordern → Antwort `type:"usageStats"`. Aggregierte Kennzahlen aus `calculateShotStatistics()` (`today/week/month`, `yesterday/lastWeek/lastMonth`, `total`, `avgDur`, `avgPerDay`) plus `shots[]` = **letzte ~12** Einzelbezüge (`dt`, `d`=Dauer s, `w`=Gewicht g bzw. `-1`). Es wird nur das **Dateiende** von `/Nutzungsstatistik.csv` gelesen, nie die ganze Datei (P4-Heap schonen). |
| `getNetworkInfo` | entfällt | WLAN-Details sind bereits im `state` enthalten — eigener Op unnötig. |
> Antwort auf `scanWifi` (Beispiel, eigene Push-Nachricht sobald der Scan fertig ist):
> ```jsonc
> {"type":"wifiNetworks","networks":[
> {"ssid":"MeinWLAN","rssi":-52,"enc":true},
> {"ssid":"Gast","rssi":-71,"enc":false}
> ]}
> ```
> Antwort auf `getUsageStats` (Beispiel):
> ```jsonc
> {"type":"usageStats","id":7,"available":true,"total":340,
> "today":3,"week":18,"month":72,"yesterday":5,"lastWeek":21,"lastMonth":80,
> "avgDur":27.4,"avgPerDay":4.9,
> "shots":[
> {"dt":"02.07. 08:14","d":27.5,"w":36.5},
> {"dt":"02.07. 07:41","d":25.0,"w":-1}
> ]}
> ```
> Antwort auf `hello` (Beispiel):
> ```jsonc
> {"type":"hello","protocolVersion":2,"firmwareVersion":"4.8.1",
> "capabilities":["state","action","listProfiles","loadProfile","saveProfile","saveBrew","saveService","saveSensor","setPid","startAutotune","stopAutotune","getProfileDetails","scanWifi","setWifi","getUsageStats"]}
> ```
>
> Anzeige am P4: eigener Menüpunkt **„Statistik"** (`ui.cpp`, `PG_STATS`). Beim Öffnen der
> Seite (und über einen „Aktualisieren"-Button) sendet der P4 `getUsageStats` und stellt die
> Aggregate (Heute/Woche/Monat inkl. Vorperiode, Gesamt, mittlere Dauer, mittl. Bezüge/Tag)
> sowie die letzten ~12 Einzelbezüge dar.
### 8.3 Robustheit / Transport
- **Handshake + Capabilities** statt impliziter Aktivierung -> P4 kann sich an die
tatsächliche Firmware-Version anpassen (Vorwärts-/Rückwärtskompatibilität).
- **Optionale Prüfsumme** (z. B. CRC16 als Feld `crc`) gegen SSR-/EMV-Störungen auf der
Maschine. Empfänger verwirft Zeilen mit falscher Prüfsumme.
- **Puffergrößen:** Ohne Chart bleiben die Nutzdaten klein. `getProfileDetails` (ein
`TemperatureProfile`) passt voraussichtlich in die bestehenden Limits (Zeile 1024 B,
State 2400 B). Chunking ist damit **nicht** erforderlich; Limit nur bei Bedarf leicht anheben.
### 8.4 Versionierung
- `protocolVersion` wird auf **2** angehoben, **sobald** ein nicht abwärtskompatibles Feld
Pflicht wird. Reine *Ergänzungen* (neue optionale Felder/Ops) bleiben v1-kompatibel —
der P4 ignoriert Unbekanntes.
- Der P4 prüft beim `hello` die `protocolVersion` der S3 und schaltet Features entsprechend
frei/aus.
---
## 9. OLED-Koexistenz (Compile-Flag)
- OLED bleibt über `ENABLE_DISPLAY` (Compile-Flag) wählbar — unverändert.
- Das JC-Display wird über `TOUCH_UART_ENABLED` (Compile-Flag) aktiviert.
- **Geplante Änderung:** beide Flags strikt **unabhängig** halten, damit alle Kombinationen
baubar sind:
| `ENABLE_DISPLAY` | `TOUCH_UART_ENABLED` | Ergebnis |
|---|---|---|
| 0 | 0 | weder OLED noch Touch (Headless, nur Web) |
| 1 | 0 | nur OLED (heutiger Standard) |
| 0 | 1 | nur JC-Touch-Display |
| 1 | 1 | OLED **und** JC-Touch parallel |
> Aktuell ist sicherzustellen, dass kein UART-Code versehentlich von `ENABLE_DISPLAY`
> abhängt und keine I2C-/Pin-Ressource doppelt belegt wird (OLED nutzt I2C GPIO17/18,
> Touch nutzt UART GPIO43/44 — **kein Konflikt**).
---
## 10. Sprachen / UI-Texte
- **Die App ist aktuell reines Deutsch** — es existiert keine i18n-/Sprachumschalt-
Infrastruktur (geprüft: S3-Firmware und Web-Skripte haben nur fest verdrahtetes Deutsch,
HTML durchgängig `lang="de"`). Damit ist **kein** `getStrings`/`setLanguage` nötig.
- Der P4 hält seine **deutschen** UI-Texte lokal (statische Strings im Display-Projekt).
- `statusKey` (sprachneutral, §7) bleibt trotzdem die saubere Grundlage für die UI-Logik
(Icons/Zustände), unabhängig vom angezeigten `statusText`.
- Projekt-Hinweis: Sollte später eine zweite Anwendungssprache eingeführt werden, ist sie
laut Projektregel überall zu ergänzen — dann auch in den P4-Stringtabellen.
---
## 11. Bewertung des Bestands (Keep / Change / Delete)
**Behalten (1:1):** Protokoll-Framing, Push-Engine (`touchUartTick`), RX-Parser
(`touchUartRxTick`), Kommando-Dispatch (`touchUartHandleCommandLine`), JSON-Helfer
(`jsonGetStringValue/UInt/Bool/Float/RawValue`, `jsonDecodeString`, `jsonEscapeForUart`),
ACK/Error-Envelopes, Reset-Checkpoints (`RESET_CP_TOUCH_UART_RX/TX`).
**Ändern/Erweitern:** Flag-Entkopplung (§9), Telemetrie-/Op-Erweiterungen (§8),
Puffergrößen/Chunking, optionaler CRC + Handshake, `action`-Stringliste dokumentieren.
**Löschen:** Nichts.
**Fully-Kiosk bleibt erhalten** (Entscheidung) — als eigenständige, optionale Funktion
neben dem JC-Display. Lastbewertung der S3:
- Einziger Aufruf `sendFullyKioskLoadUrl()` wird **nur bei Standby-Zustandswechsel**
getriggert (`if (standbyModeActive != lastStandbyModeActive)`), **nicht periodisch**
(wenige Male pro Tag).
- Blockierender HTTP-GET, aber mit **kurzem Timeout** (`timeoutMs`, Default 700 ms);
bei `enabled=0` sofortiger Rückkehr ohne Netzwerkzugriff.
- Übergänge passieren im Standby, also **außerhalb eines Bezugs** -> kein Einfluss auf die
Regelung. **Keine relevante Dauerlast, keine Leistungsgrenze.** Koexistiert problemlos
mit dem UART-Display.
---
## 12. Implementierungsplan
### Phase A — S3-Seite (Hauptplatine, `Dual_PID_FastHeatUp.ino`)
1. **[erledigt]** `action`-Stringliste aus `executeDashboardAction()` dokumentiert (§6.1).
2. **[erledigt A.1]** Flag-Entkopplung von `ENABLE_DISPLAY` dokumentiert/abgesichert (§9);
Defines kommentiert, `TOUCH_UART_PROTOCOL_VERSION` -> 2, Puffer -> 3000.
3. **[erledigt A.1]** v2-Telemetriefelder ergänzt: Netzwerk, PID, Duty, AutoTune-Status (§8.1).
4. **[erledigt A.1]** Op `hello` implementiert (§8.2).
5. **[erledigt]** Weitere Ops: `getProfileDetails`, `setPid`, `startAutotune/stopAutotune` (§8.2).
6. **[offen, optional]** CRC + ggf. Puffer-Feinjustage (§8.3) — bewusst nicht als toter,
ungetesteter Code eingebaut; bei Bedarf später.
7. **[offen, durch dich]** `TOUCH_UART_ENABLED` auf 1 setzen + Build verifizieren, **sobald**
das JC-Display physisch an Serial1 (GPIO43/44) hängt. Bewusst noch **0**, damit die
serielle Debug-Konsole (GPIO43/44) im aktuellen Betrieb erhalten bleibt.
### Phase B — P4-Seite (JC-Display, Projekt `JC_Display_Firmware/`)
Grundgerüst angelegt (Arduino-ESP32 P4 + LVGL 9 + ArduinoJson 7). Aufteilung:
`config.h`, `machine_state.h`, `protocol_client.*`, `ui.*`, `display_hal.*`, `.ino`, `README.md`.
1. **[erledigt]** UART-Client: Zeilen-RX/TX, JSON-Parsing, `state` -> UI-Modell,
`hello`-Handshake, Heartbeat, Link-Timeout (`protocol_client.*`).
2. **[erledigt]** Dashboard: Temps/Soll/Duty, Status-Banner (nach `statusKey`),
Verbindungs-/WLAN-Anzeige, Bezugs-Timer, Bedien-Buttons -> Kommandos (`ui.*`).
3. **[erledigt]** Senderoutinen für `setPid`, `startAutotune/stopAutotune`,
`getProfileDetails` im Client vorhanden.
4. **[erledigt]** Board-Bringup auf Basis der **Hersteller-Portierung** verdrahtet
(`display_hal.cpp`, Schalter `JC_USE_REAL_PANEL`): MIPI-DSI/ST7701, GT911 (I²C SDA=7/SCL=8),
Backlight GPIO23, LCD-Reset GPIO5; LVGL-Port mit eigenem Task + Lock. Stub-Modus bleibt für
Schreibtisch-Tests. Vendor-BSP-Dateien sind in den Sketch zu kopieren (siehe Projekt-README).
5. **[festgelegt]** UART-Pins am JC-Display: **P4 GPIO50 (TX) / GPIO51 (RX)** vom Expand-IO-Header
JP1 (Schaltplan `4_USB&IO.png`). Gegenseite S3 unverändert GPIO43/44.
6. **[offen]** Weitere Screens: Einstellungen (Brew/Service/Sensor), Profil-Auswahl/-Editor,
PID/AutoTune-Bedienung; Ausrichtung Hochformat (Rotation 0 in `pins_config.h`).
### Entschieden
- **Sprachen:** App ist Deutsch-only, keine i18n -> P4 hält deutsche Strings lokal (§10).
- **Chart am Display:** nicht benötigt -> kein `getChartData`/Chunking (§8).
- **Fully-Kiosk:** bleibt als optionale Funktion erhalten; keine relevante S3-Last (§11).
---
*Letzter Abgleich mit Code: `Dual_PID_FastHeatUp.ino` v4.8.1 — Abschnitt „Dashboard Code
Block" (`buildTouchUartStateJson`, `touchUartHandleCommandLine`, `touchUartTick`,
`touchUartRxTick`) sowie Defines `TOUCH_UART_*` (Zeilen ~155184).*