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:
co-authored by
Claude Opus 5
parent
e0c6784ecf
commit
4f9f686d81
@@ -0,0 +1,367 @@
|
||||
// =====================================================================================
|
||||
// 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);
|
||||
}
|
||||
Reference in New Issue
Block a user