Files
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

368 lines
16 KiB
C++

// =====================================================================================
// protocol_client.cpp - Implementierung des UART-Protokoll-Clients (P4-Seite)
// =====================================================================================
#include "protocol_client.h"
#include <ArduinoJson.h> // Bibliothek: "ArduinoJson" (v7) ueber den Library Manager
#if UART_SELFTEST_INTERNAL_LOOPBACK
#include "driver/uart.h" // fuer uart_set_loop_back()
#endif
// -------------------------------------------------------------------------------------
// Setup / Hauptschleife
// -------------------------------------------------------------------------------------
void ProtocolClient::begin() {
// Groesserer RX-Puffer: OTA-Chunks (~1.4 KB) duerfen nicht ueberlaufen (Default 256).
DISPLAY_UART_PORT.setRxBufferSize(2048);
DISPLAY_UART_PORT.begin(DISPLAY_UART_BAUDRATE, SERIAL_8N1,
DISPLAY_UART_RX_PIN, DISPLAY_UART_TX_PIN);
// Bewusst NICHT auf PROTO_RX_LINE_MAX (4 KB) reserviert: Der interne RAM ist knapp
// (statisch ~98 %), und eine Arduino-String kann nicht ins PSRAM alloziert werden.
// 2 KB deckt die reale State-Zeile (~2,1 KB) fast vollstaendig ab; der Rest waechst
// in kleinen Schritten nach. Das Limit dient nur als Obergrenze gegen Muell/Overflow.
_rxBuf.reserve(2048);
_lastHelloMs = 0;
_lastHeartbeatMs = 0;
#if UART_SELFTEST_INTERNAL_LOOPBACK
// DIAGNOSE: TX intern auf RX legen. UART_NUM_1 muss zu DISPLAY_UART_PORT (Serial1) passen.
uart_set_loop_back(UART_NUM_1, true);
#endif
}
void ProtocolClient::loop() {
// --- Eingehende Bytes zeilenweise einsammeln ---
while (DISPLAY_UART_PORT.available() > 0) {
char c = (char)DISPLAY_UART_PORT.read();
rxByteCount++;
if (c == '\r') continue;
if (c == '\n') {
if (_rxOverflow) {
_rxBuf = "";
_rxOverflow = false;
} else {
String line = _rxBuf;
_rxBuf = "";
line.trim();
if (line.length() > 0) { rxLineCount++; handleLine(line); }
}
continue;
}
if (_rxOverflow) continue;
if (_rxBuf.length() >= PROTO_RX_LINE_MAX) { _rxOverflow = true; continue; }
_rxBuf += c;
}
maintainLink();
}
// -------------------------------------------------------------------------------------
// Verbindungspflege: hello-Handshake, Heartbeat, Timeout
// -------------------------------------------------------------------------------------
void ProtocolClient::maintainLink() {
unsigned long now = millis();
// Link-Timeout: zu lange nichts empfangen -> Verbindung als tot markieren
if (_state.linkUp && (now - _state.lastRxMs > PROTO_LINK_TIMEOUT_MS)) {
_state.linkUp = false;
}
if (!_state.linkUp) {
// Solange keine Verbindung: regelmaessig hello senden
if (now - _lastHelloMs >= PROTO_HELLO_RETRY_MS) {
_lastHelloMs = now;
sendHello();
}
return;
}
// Verbunden: Heartbeat senden, damit die S3 uns als aktiven Client behaelt
if (now - _lastHeartbeatMs >= PROTO_HEARTBEAT_MS) {
_lastHeartbeatMs = now;
sendPing();
}
}
// -------------------------------------------------------------------------------------
// Empfangene Zeile verarbeiten
// -------------------------------------------------------------------------------------
void ProtocolClient::handleLine(const String& line) {
_state.lastRxMs = millis();
lastRxLine = line;
JsonDocument doc;
DeserializationError err = deserializeJson(doc, line);
if (err) {
parseErrorCount++;
#ifdef DBG_SERIAL
DBG_SERIAL.print(F("[proto] JSON-Fehler: "));
DBG_SERIAL.println(err.c_str());
#endif
return;
}
const char* type = doc["type"] | "";
// OTA-Nachrichten (otaBegin/otaData/otaEnd/otaAbort) separat behandeln.
if (strncmp(type, "ota", 3) == 0) {
if (_otaCb) _otaCb(line);
return;
}
if (strcmp(type, "state") == 0) {
applyState(line);
_state.linkUp = true; // jeder gueltige state bestaetigt die Verbindung
if (_stateCb) _stateCb(_state);
} else if (strcmp(type, "hello") == 0) {
_state.protocolVersion = doc["protocolVersion"] | 0;
_state.firmwareVersion = String((const char*)(doc["firmwareVersion"] | ""));
_state.linkUp = true;
#ifdef DBG_SERIAL
DBG_SERIAL.printf("[proto] hello: FW=%s, proto=%u\n",
_state.firmwareVersion.c_str(), _state.protocolVersion);
if (_state.protocolVersion != PROTO_EXPECTED_VERSION) {
DBG_SERIAL.printf("[proto] WARNUNG: Protokollversion %u != erwartet %u\n",
_state.protocolVersion, (unsigned)PROTO_EXPECTED_VERSION);
}
#endif
} else if (strcmp(type, "ack") == 0) {
// S3 sendet das Erfolgs-Flag als "success"; "ok" als Fallback (aeltere Versionen).
bool ackOk = doc["success"] | (doc["ok"] | false);
if (_ackCb) _ackCb(doc["id"] | 0, ackOk,
String((const char*)(doc["message"] | "")));
} else if (strcmp(type, "error") == 0) {
if (_ackCb) _ackCb(doc["id"] | 0, false,
String((const char*)(doc["message"] | "")));
} else if (strcmp(type, "profiles") == 0) {
applyProfiles(line);
} else if (strcmp(type, "profileDetails") == 0) {
if (_profileDetailsCb) _profileDetailsCb(line);
} else if (strcmp(type, "usageStats") == 0) {
if (_usageStatsCb) _usageStatsCb(line);
} else if (strcmp(type, "wifiNetworks") == 0) {
if (_wifiNetworksCb) _wifiNetworksCb(line);
}
}
// -------------------------------------------------------------------------------------
// state -> MachineState
// -------------------------------------------------------------------------------------
void ProtocolClient::applyState(const String& json) {
JsonDocument doc;
if (deserializeJson(doc, json)) return;
MachineState& s = _state;
s.protocolVersion = doc["protocolVersion"] | s.protocolVersion;
s.firmwareVersion = String((const char*)(doc["firmwareVersion"] | s.firmwareVersion.c_str()));
s.tempW = doc["tempW"] | s.tempW;
s.setW = doc["setW"] | s.setW;
s.tempD = doc["tempD"] | s.tempD;
s.setD = doc["setD"] | s.setD;
s.statusText = String((const char*)(doc["statusText"] | ""));
s.statusKey = String((const char*)(doc["statusKey"] | ""));
s.wasserSensorError = doc["wasserSensorError"] | false;
s.wasserSafetyShutdown = doc["wasserSafetyShutdown"] | false;
s.dampfSensorError = doc["dampfSensorError"] | false;
s.dampfSafetyShutdown = doc["dampfSafetyShutdown"] | false;
s.shotActive = doc["shotActive"] | false;
s.shotElapsedMs = doc["shotElapsedMs"] | 0;
s.scaleTaring = doc["scaleTaring"] | false;
s.steamCircuitActive = doc["steamCircuitActive"] | false;
s.steamElapsedMs = doc["steamElapsedMs"] | 0;
s.steamFlushActive = doc["steamFlushActive"] | false;
s.steamFlushElapsedMs= doc["steamFlushElapsedMs"]| 0;
s.flushActive = doc["flushActive"] | false;
s.flushElapsedMs = doc["flushElapsedMs"] | 0;
s.ecoActive = doc["ecoActive"] | false;
s.ecoCountdownSec = doc["ecoCountdownSec"] | (long)-1;
s.ecoShowInfo = doc["ecoShowInfo"] | false;
s.standbyActive = doc["standbyActive"] | false;
s.standbyShowClock = doc["standbyShowClock"] | false;
s.backlightActive = doc["backlightActive"] | s.backlightActive;
s.backlightStandbyClock = doc["backlightStandbyClock"] | s.backlightStandbyClock;
s.maintenanceActive = doc["maintenanceActive"] | false;
s.steamHeatDisabled = doc["steamHeatDisabled"] | false;
s.steamDelayActive = doc["steamDelayActive"] | false;
s.steamDelayRemainSec= doc["steamDelayRemainSec"]| (long)0;
s.heatUpMinutes = doc["heatUpMinutes"] | 0;
s.heatUpRemainSec = doc["heatUpRemainSec"] | (long)-1;
s.lightOn = doc["lightOn"] | false;
s.piezoEnabled = doc["piezoEnabled"] | false;
s.activeProfile = String((const char*)(doc["profile"] | "")); // fehlt bei aelterer S3-FW -> leer
s.profileDirty = doc["profDirty"] | false;
s.weight = doc["weight"] | s.weight;
s.targetWeight = doc["targetWeight"] | s.targetWeight;
s.lastShotWeight = doc["lastShotWeight"] | s.lastShotWeight;
s.lastShotByWeight = doc["lastShotByWeight"] | false;
s.flowRate = doc["flowRate"] | 0.0f;
s.scaleEnabled = doc["scaleEnabled"] | false;
s.scaleType = doc["scaleType"] | 0;
s.scaleConnected= doc["scaleConnected"]| false;
s.piEnabled = doc["piEnabled"] | false;
s.bbtEnabled = doc["bbtEnabled"] | false;
s.bbwEnabled = doc["bbwEnabled"] | false;
s.sbtEnabled = doc["sbtEnabled"] | false;
s.bbtSecs = doc["bbtSecs"] | s.bbtSecs;
s.sbtSecs = doc["sbtSecs"] | s.sbtSecs;
s.piDurSecs = doc["piDurSecs"] | s.piDurSecs;
s.piPauseSecs= doc["piPauseSecs"]| s.piPauseSecs;
s.piState = doc["piState"] | (uint8_t)0; // fehlt bei aelterer S3-FW -> 0 (UI leitet dann zeitbasiert ab)
s.bbwTarget = doc["bbwTarget"] | s.bbwTarget;
s.bbwOffset = doc["bbwOffset"] | s.bbwOffset;
// Cold Extraction (fehlt bei S3-FW < 5.3.0 -> cxEnabled bleibt false, UI blendet alles aus)
s.cxEnabled = doc["cxEnabled"] | false;
s.cxActive = doc["cxActive"] | false;
s.cxShot = doc["cxShot"] | false;
s.cxAllowed = doc["cxAllowed"] | false;
// Fehlt bei S3-FW < 5.3.5 -> auf cxEnabled zurueckfallen, sonst waere der Schalter tot
s.cxArmable = doc["cxArmable"] | (doc["cxEnabled"] | false);
s.cxResting = doc["cxResting"] | false;
s.cxMaxTemp = doc["cxMaxTemp"] | s.cxMaxTemp;
s.cxTarget = doc["cxTarget"] | s.cxTarget;
s.cxPreInfSec= doc["cxPreInf"] | s.cxPreInfSec;
s.cxPreInfDuty = doc["cxPreInfDuty"] | s.cxPreInfDuty;
s.cxDuty = doc["cxDuty"] | s.cxDuty;
s.cxPulseMs = doc["cxPulseMs"] | s.cxPulseMs;
s.cxPumpMaxRun = doc["cxPumpMaxRun"] | s.cxPumpMaxRun;
s.cxPumpRest = doc["cxPumpRest"] | s.cxPumpRest;
s.cxAskOnWake = doc["cxAskOnWake"] | false;
s.cxWakeChoice = doc["cxWakeChoice"] | false;
s.cxWakeChoiceSec = doc["cxWakeChoiceSec"] | (long)0;
s.cxMaxSecs = doc["cxMaxSecs"] | s.cxMaxSecs;
s.cxBlock = doc["cxBlock"] | (uint8_t)0;
s.cxNotice = doc["cxNotice"] | (uint8_t)0;
s.caseSensorEnabled = doc["caseSensorEnabled"] | false;
s.caseTempDashboard = doc["caseTempDashboard"] | false;
s.caseSensorType = doc["caseSensorType"] | 0;
if (doc["caseTemp"].isNull()) {
s.hasCaseTemp = false;
} else {
s.hasCaseTemp = true;
s.caseTemp = doc["caseTemp"] | 0.0f;
}
s.maintenanceInterval = doc["maintenanceInterval"] | 0;
s.maintenanceIntervalCounter = doc["maintenanceIntervalCounter"] | 0;
s.flushDurationSeconds = doc["flushDurationSeconds"] | 0;
s.steamFlushDurationSeconds = doc["steamFlushDurationSeconds"] | 0;
s.cleaningActive = doc["cleaningActive"] | false;
s.cleaningWaiting = doc["cleaningWaiting"] | false;
s.cleaningBrewPhase = doc["cleaningBrewPhase"] | false;
s.cleaningCycle = doc["cleaningCycle"] | 0;
s.cleaningCycles = doc["cleaningCycles"] | 0;
s.cleaningBrewSeconds = doc["cleaningBrewSeconds"] | 0;
s.cleaningPauseSeconds = doc["cleaningPauseSeconds"] | 0;
s.cleaningPhaseRemainSec = doc["cleaningPhaseRemainSec"] | (long)0;
// v2
s.dutyW = doc["dutyW"] | 0.0f;
s.dutyD = doc["dutyD"] | 0.0f;
s.kpW = doc["kpW"] | 0.0f; s.kiW = doc["kiW"] | 0.0f; s.kdW = doc["kdW"] | 0.0f;
s.kpD = doc["kpD"] | 0.0f; s.kiD = doc["kiD"] | 0.0f; s.kdD = doc["kdD"] | 0.0f;
s.autoTuneW = doc["autoTuneW"] | false;
s.autoTuneD = doc["autoTuneD"] | false;
s.autoTuneWStatus = doc["autoTuneWStatus"] | s.autoTuneWStatus;
s.autoTuneDStatus = doc["autoTuneDStatus"] | s.autoTuneDStatus;
s.wifiConnected = doc["wifiConnected"] | false;
s.apMode = doc["apMode"] | false;
s.ip = String((const char*)(doc["ip"] | ""));
s.ssid = String((const char*)(doc["ssid"] | ""));
s.rssi = doc["rssi"] | 0;
s.timeStr = String((const char*)(doc["time"] | ""));
}
// -------------------------------------------------------------------------------------
// profiles -> Callback (Array von Namen)
// -------------------------------------------------------------------------------------
void ProtocolClient::applyProfiles(const String& json) {
if (!_profilesCb) return;
JsonDocument doc;
if (deserializeJson(doc, json)) return;
JsonArray arr = doc["profiles"].as<JsonArray>();
static const int MAX_PROFILES = 32;
String names[MAX_PROFILES];
int count = 0;
for (JsonVariant v : arr) {
if (count >= MAX_PROFILES) break;
names[count++] = String((const char*)(v | ""));
}
_profilesCb(names, count);
}
// -------------------------------------------------------------------------------------
// Senden
// -------------------------------------------------------------------------------------
uint32_t ProtocolClient::sendCommand(const String& op, const String& extraFields) {
uint32_t id = _nextId++;
String msg = "{\"op\":\"" + op + "\",\"id\":" + String(id);
if (extraFields.length() > 0) {
msg += ",";
msg += extraFields;
}
msg += "}";
DISPLAY_UART_PORT.println(msg);
txCmdCount++;
return id;
}
uint32_t ProtocolClient::sendHello() { helloCount++; return sendCommand("hello", "\"firmwareVersion\":\"" DISPLAY_FW_VERSION "\""); }
uint32_t ProtocolClient::sendPing() { return sendCommand("ping", "\"firmwareVersion\":\"" DISPLAY_FW_VERSION "\""); }
uint32_t ProtocolClient::sendGetState() { return sendCommand("getState"); }
uint32_t ProtocolClient::sendListProfiles() { return sendCommand("listProfiles"); }
uint32_t ProtocolClient::sendGetUsageStats(){ return sendCommand("getUsageStats"); }
uint32_t ProtocolClient::sendLoadProfile(const String& profile) {
return sendCommand("loadProfile", "\"profile\":\"" + profile + "\"");
}
uint32_t ProtocolClient::sendGetProfileDetails(const String& profile) {
return sendCommand("getProfileDetails", "\"profile\":\"" + profile + "\"");
}
uint32_t ProtocolClient::sendAction(const String& action, const String& value) {
String fields = "\"action\":\"" + action + "\"";
if (value.length() > 0) fields += ",\"value\":\"" + value + "\"";
return sendCommand("action", fields);
}
uint32_t ProtocolClient::sendSetPid(const String& fieldsJson) {
return sendCommand("setPid", fieldsJson);
}
uint32_t ProtocolClient::sendStartAutotune(const String& target) {
return sendCommand("startAutotune", "\"target\":\"" + target + "\"");
}
uint32_t ProtocolClient::sendStopAutotune() {
return sendCommand("stopAutotune");
}
// Minimal-Escaping fuer JSON-Strings (Backslash + Anfuehrungszeichen).
static String jsonEsc(const String& s) {
String o; o.reserve(s.length() + 4);
for (size_t i = 0; i < s.length(); i++) {
char c = s[i];
if (c == '\\' || c == '"') { o += '\\'; o += c; }
else if (c == '\n' || c == '\r') { /* weglassen */ }
else o += c;
}
return o;
}
uint32_t ProtocolClient::sendScanWifi() {
return sendCommand("scanWifi");
}
uint32_t ProtocolClient::sendSetWifi(const String& ssid, const String& password) {
String f = "\"ssid\":\"" + jsonEsc(ssid) + "\",\"password\":\"" + jsonEsc(password) + "\"";
return sendCommand("setWifi", f);
}