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
This commit is contained in:
raw-designs
2026-09-01 03:30:42 +02:00
co-authored by Claude Opus 5
parent e0c6784ecf
commit 4f9f686d81
4805 changed files with 26 additions and 26 deletions
+620
View File
@@ -0,0 +1,620 @@
# 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).*