Files
Dual-PID/Doku/P4-Display_UART-Protokoll.md
T
raw-designsandClaude Opus 5 4f9f686d81 refactor: Display-Firmware neutral als P4_Display_Firmware führen
Der Ordner hieß JC_Display_Firmware, bedient seit 1.6.0 aber auch das
Waveshare 7inch DSI LCD (H) am ESP32-P4-Pico. Der Name führte in die Irre.

- JC_Display_Firmware/ -> P4_Display_Firmware/ (samt Sketch, den Arduino
  gleichnamig zum Ordner verlangt)
- Doku/JC-Display_UART-Protokoll.md -> Doku/P4-Display_UART-Protokoll.md,
  Verweise und Titel angepasst
- Verweise in CLAUDE.md, README und Quelltextköpfen nachgezogen

Nur Namen und Pfade, keine Logikänderung. Der Vendor-Ordner
JC_Display_Firmware_7zoll/ behält seinen Namen, ebenso die Bezeichner
JC_PANEL_TYPE/jc_board_bringup im Quelltext.

Enthält außerdem die bereits im Arbeitsverzeichnis liegende, noch nicht
committete Ergänzung des Git-Workflows in CLAUDE.md (PRs über die Gitea-API
statt der hängenden tea-CLI).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018QoLDQeUh1dCQz7yYZVb4Y
2026-09-01 03:30:42 +02:00

621 lines
34 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.
# P4-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 **P4-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 P4-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 P4-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 (P4-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 P4-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 |
| `wakeEspresso` | — | Standby beenden (falls aktiv) und das Auswahlfenster schließen, also „normal heizen" wählen (ab S3 5.6.0) |
Für das Bedienelement zählt `cxArmable`, **nicht** `cxAllowed`: Der Modus schaltet die
Wasserheizung ab und muss deshalb auch im Standby und bei noch warmem Kessel scharf zu
stellen sein — sonst heizt die Maschine nach dem Aufwecken an, bevor man an den Schalter
kommt. `cxAllowed` gilt nur für den einzelnen Bezug; ist der Modus scharf und `cxAllowed`
false, sollte die UI „Modus aktiv, Bezug noch gesperrt" zeigen.
Ein Aufwecken direkt in die kalte Extraktion sendet `startColdExtraction` **vor**
`deactivateStandby` — in dieser Reihenfolge, damit der Wasserkreis gar nicht erst anheizt.
Die Aktionen werden abgewiesen (`ok:false` + `message`), solange die Bedingungen nicht
erfüllt sind. 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 sind ab S3 5.4.0 über `saveBrew` einstellbar (zusätzlich
zur Web-Seite `/Cold-Extraction`). Zulässige Felder: `cxEnabled` (bool), `cxMaxTemp`,
`cxPreInf`, `cxPreInfDuty`, `cxPulseMs`, `cxDuty`, `cxTarget`, `cxMaxSecs`, `cxPumpMaxRun`,
`cxPumpRest`, `cxAskOnWake` (bool). Es werden nur die tatsächlich gesendeten Felder übernommen; Grenzwerte prüft
die S3. Die aktuellen Werte stehen alle im `state`, damit das Display seine Eingabefelder
vorbelegen kann.
**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 | **Bezug** jetzt möglich (Wasserkessel kalt genug, kein Standby/Wartung/Tuning) |
| `cxArmable` | bool | **Modus umschaltbar** — auch im Standby und bei warmem Kessel (ab S3 5.3.5) |
| `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 (auch für die Phasenleiste des kalten Bezugs) |
| `cxPreInfDuty` | float | Pumpenleistung Vorbenetzung in % (ab S3 5.4.0) |
| `cxDuty` | float | Pumpenleistung Hauptbezug in % (ab S3 5.4.0) |
| `cxPulseMs` | uint16 | Pulsperiode in ms (ab S3 5.4.0) |
| `cxPumpMaxRun` | float | Pumpenschutz: max. kumulierte Laufzeit in s (ab S3 5.4.0) |
| `cxPumpRest` | float | Pumpenschutz: Zwangspause in s (ab S3 5.4.0) |
| `cxAskOnWake` | bool | Beim Aufwecken aus dem Standby nach Espresso/Cold Extraction fragen (ab S3 5.5.0) |
| `cxWakeChoice` | bool | Auswahlfenster nach dem Aufwecken offen — Wasserkreis heizt solange nicht (ab S3 5.6.0) |
| `cxWakeChoiceSec` | long | Restsekunden der Bedenkzeit |
| `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, `4` ohne Waage
aktiviert (ab S3 5.3.4).
**Ohne Waage** (`scaleEnabled`/`scaleConnected` false) läuft die kalte Extraktion rein
zeitgesteuert: `cxTarget` ist wirkungslos und die Stillstands-Erkennung ist inaktiv, es
bleibt `cxMaxSecs` als Ende. Das ist Absicht (ein kurz abgemeldeter HX711 soll die Funktion
nicht blockieren) und **kein** Sperrgrund — das Display sollte es aber dauerhaft anzeigen.
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 P4-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 P4-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 P4-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 (P4-Display, Projekt `P4_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 P4-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).*