// ===================================================================================== // JC_Display_Firmware.ino // Touch-Display-Firmware (ESP32-P4) fuer die Dual-PID-Siebtraegersteuerung // ===================================================================================== // // Architektur (siehe Doku/JC-Display_UART-Protokoll.md): // Hauptplatine (ESP32-S3) --UART 230400 8N1--> dieses Display (ESP32-P4) // Die S3 bleibt Quelle der Wahrheit + Webinterface. Dieses Display ist ein // reiner UART-Client (Touch-Bedienung + Anzeige). // // Module: // config.h - Pins/UART/Protokoll // machine_state.h - Spiegel des 'state'-Objekts // protocol_client.* - UART-Protokoll (hello/state/ack/profiles/...) // ui.* - LVGL-Dashboard (hardwareunabhaengig) // display_hal.* - Panel/Touch/LVGL-Glue (einziger board-spezifischer Teil) // // Benoetigte Bibliotheken: LVGL 9.x, ArduinoJson 7.x // Board: "ESP32P4 Dev Module" (Arduino-ESP32 >= 3.1 mit P4-Support) // ===================================================================================== #include "config.h" #include "machine_state.h" #include "protocol_client.h" #include "ui.h" #include "display_hal.h" #include #include #include #include "mbedtls/base64.h" static MachineState g_state; static ProtocolClient g_client(g_state); // --- Boot-Loop-Schutz / UART-Update-Modus --------------------------------------------- // Zaehlt Fruehstarts in NVS (ueberlebt jeden Reset/Power-Off zuverlaessig). Erreicht der // Zaehler die Schwelle - durch eine Absturzschleife ODER durch mehrfaches schnelles // Reset-Druecken (manuelle Geste) - startet der P4 headless (ohne LVGL/Panel, dem // haeufigsten Absturzgrund) und haelt nur UART + OTA offen, sodass der S3 die P4-Firmware // jederzeit neu flashen kann. In loop() wird der Zaehler nach stabiler Laufzeit auf 0 // gesetzt. Siehe enterUpdateMode(). static const uint16_t P4_UPDATE_MODE_THRESHOLD = 4; // schnelle Fruehstarts in Folge -> Update-Modus static const uint32_t P4_STABLE_UPTIME_MS = 30000UL; // so lange stabil -> Zaehler zuruecksetzen static bool g_updateModeActive = false; static bool g_bootCountCleared = false; // --- Protokoll-Callbacks --------------------------------------------------------------- static void onState(const MachineState& st) { // LVGL laeuft im realen Modus in einem eigenen Task -> UI-Zugriff sperren. hal_lock(); ui_update(st); hal_unlock(); // Display im Standby aus; ist "Uhrzeit im Standby anzeigen" aktiv, bleibt es an, // aber abgedunkelt (nicht blendend bei Nacht). Helligkeiten kommen von der S3 // (frei einstellbar: aktiv + Standby-Uhr). Nur bei Aenderung schalten, um // unnoetige LEDC-Schreibzugriffe zu vermeiden. static int s_lastBacklight = -1; int bl; if (st.standbyActive) { // Aufweck-Dialog offen -> normale Helligkeit, damit die Rueckfrage lesbar ist. if (ui_wake_dialog_open()) bl = (int)st.backlightActive; else bl = st.standbyShowClock ? (int)st.backlightStandbyClock : 0; } else { bl = (int)st.backlightActive; } if (bl < 0) bl = 0; if (bl > 100) bl = 100; if (bl != s_lastBacklight) { hal_backlight(bl); s_lastBacklight = bl; } } static void onAck(uint32_t id, bool ok, const String& message) { // Automatische/Heartbeat-Acks nicht als Toast zeigen (pong, Handshake, Listen-Refresh). bool routine = ok && (message == "pong" || message == "hello" || message == "profile gesendet" || message == "state gesendet" || message == "usageStats gesendet"); if (!routine) { hal_lock(); ui_toast(message.c_str(), !ok); hal_unlock(); } #ifdef DBG_SERIAL DBG_SERIAL.printf("[ack] id=%lu ok=%d msg=%s\n", (unsigned long)id, ok ? 1 : 0, message.c_str()); #endif } static void onProfiles(const String profiles[], int count) { #ifdef DBG_SERIAL DBG_SERIAL.printf("[profiles] %d Profil(e)\n", count); for (int i = 0; i < count; i++) DBG_SERIAL.printf(" - %s\n", profiles[i].c_str()); #endif // LVGL laeuft im realen Modus in einem eigenen Task -> UI-Zugriff sperren. hal_lock(); ui_set_profiles(profiles, count); hal_unlock(); } static void onProfileDetails(const String& json) { #ifdef DBG_SERIAL DBG_SERIAL.print("[profileDetails] "); DBG_SERIAL.println(json); #endif // LVGL laeuft im realen Modus in einem eigenen Task -> UI-Zugriff sperren. hal_lock(); ui_set_profile_details(json); hal_unlock(); } static void onWifiNetworks(const String& json) { #ifdef DBG_SERIAL DBG_SERIAL.print("[wifiNetworks] "); DBG_SERIAL.println(json); #endif hal_lock(); ui_set_wifi_networks(json); hal_unlock(); } static void onUsageStats(const String& json) { #ifdef DBG_SERIAL DBG_SERIAL.print("[usageStats] "); DBG_SERIAL.println(json); #endif // LVGL laeuft im realen Modus in einem eigenen Task -> UI-Zugriff sperren. hal_lock(); ui_set_usage_stats(json); hal_unlock(); } // --- OTA-Update der Display-Firmware ueber die UART (von der S3 gestreamt) --- static uint32_t g_otaReceived = 0; static void otaSendAck(long seq, bool ok, const char* err = nullptr) { String line = "{\"type\":\"otaAck\",\"seq\":" + String(seq) + ",\"ok\":"; line += (ok ? "true" : "false"); if (err) { line += ",\"error\":\""; line += err; line += "\""; } line += "}"; DISPLAY_UART_PORT.println(line); } static void onOta(const String& json) { JsonDocument doc; if (deserializeJson(doc, json)) return; const char* type = doc["type"] | ""; if (strcmp(type, "otaBegin") == 0) { g_otaReceived = 0; bool ok = Update.begin(UPDATE_SIZE_UNKNOWN); // schreibt in den inaktiven OTA-Slot hal_lock(); ui_ota_begin(); hal_unlock(); otaSendAck(-1, ok, ok ? nullptr : "begin"); return; } if (strcmp(type, "otaData") == 0) { long seq = doc["seq"] | -1; const char* b64 = doc["data"] | ""; static uint8_t buf[1200]; size_t outlen = 0; if (mbedtls_base64_decode(buf, sizeof(buf), &outlen, (const unsigned char*)b64, strlen(b64)) != 0) { otaSendAck(seq, false, "b64"); return; } if (Update.write(buf, outlen) != outlen) { Update.abort(); hal_lock(); ui_ota_fail("Schreibfehler"); hal_unlock(); otaSendAck(seq, false, "write"); return; } g_otaReceived += outlen; hal_lock(); ui_ota_progress(g_otaReceived); hal_unlock(); otaSendAck(seq, true); return; } if (strcmp(type, "otaEnd") == 0) { bool ok = Update.end(true); otaSendAck(-2, ok, ok ? nullptr : "end"); if (ok) { hal_lock(); ui_ota_done(); hal_unlock(); DISPLAY_UART_PORT.flush(); delay(1500); ESP.restart(); } else { hal_lock(); ui_ota_fail("Pruefsumme/Abschluss"); hal_unlock(); } return; } if (strcmp(type, "otaAbort") == 0) { Update.abort(); hal_lock(); ui_ota_fail("Abgebrochen"); hal_unlock(); otaSendAck(-3, true); return; } } // --- UART-Update-Modus (headless): OTA-Empfang OHNE LVGL/UI ---------------------------- // Wie onOta(), aber ohne hal_lock()/ui_ota_*-Aufrufe, da im Update-Modus das Display // bewusst nicht initialisiert ist. static void onOtaSafe(const String& json) { JsonDocument doc; if (deserializeJson(doc, json)) return; const char* type = doc["type"] | ""; if (strcmp(type, "otaBegin") == 0) { g_otaReceived = 0; bool ok = Update.begin(UPDATE_SIZE_UNKNOWN); otaSendAck(-1, ok, ok ? nullptr : "begin"); return; } if (strcmp(type, "otaData") == 0) { long seq = doc["seq"] | -1; const char* b64 = doc["data"] | ""; static uint8_t buf[1200]; size_t outlen = 0; if (mbedtls_base64_decode(buf, sizeof(buf), &outlen, (const unsigned char*)b64, strlen(b64)) != 0) { otaSendAck(seq, false, "b64"); return; } if (Update.write(buf, outlen) != outlen) { Update.abort(); otaSendAck(seq, false, "write"); return; } g_otaReceived += outlen; otaSendAck(seq, true); return; } if (strcmp(type, "otaEnd") == 0) { bool ok = Update.end(true); otaSendAck(-2, ok, ok ? nullptr : "end"); if (ok) { DISPLAY_UART_PORT.flush(); delay(1500); ESP.restart(); } return; } if (strcmp(type, "otaAbort") == 0) { Update.abort(); otaSendAck(-3, true); return; } } // Update-Modus: KEIN LVGL/Panel-Bringup (haeufigster Absturzgrund uebersprungen). Nur // UART + OTA-Empfang offen, damit der S3 die P4-Firmware neu flashen kann. Kehrt NIE zurueck. static void enterUpdateMode() { g_updateModeActive = true; DBG_SERIAL.println(F("\n*** P4 UPDATE-MODUS: mehrere Startfehler erkannt -> nur UART + OTA ***")); DBG_SERIAL.println(F("Display bleibt dunkel. Firmware ueber die S3-Weboberflaeche neu flashen.")); // Zaehler sofort auf 0 -> ein einzelner Reset fuehrt zurueck in den Normalbetrieb. // Eine echte Absturzschleife baut ihn danach einfach wieder bis zur Schwelle auf. { Preferences p; p.begin("bootguard", false); p.putUShort("early", 0); p.end(); } g_client.onOta(onOtaSafe); // OTA-Empfang ohne UI g_client.begin(); // UART starten; hello/ping in loop() -> S3 erkennt den P4 while (true) { g_client.loop(); // UART lesen, hello/heartbeat, OTA empfangen delay(2); } } // --- Setup / Loop ---------------------------------------------------------------------- void setup() { DBG_SERIAL.begin(DBG_BAUD); delay(50); #if JC_PANEL_TYPE == JC_PANEL_70 DBG_SERIAL.println(F("\nJC1060P470C-I-W (7,0 Zoll) Display-Firmware startet...")); #else DBG_SERIAL.println(F("\nJC4880P443C-I-W (4,3 Zoll) Display-Firmware startet...")); #endif // --- Boot-Loop-Schutz: Fruehstart-Zaehler in NVS erhoehen; Schwelle -> Update-Modus --- { Preferences bootPrefs; bootPrefs.begin("bootguard", false); uint16_t earlyBoots = (uint16_t)(bootPrefs.getUShort("early", 0) + 1); bootPrefs.putUShort("early", earlyBoots); bootPrefs.end(); DBG_SERIAL.printf("[bootguard] earlyBoots=%u/%u\n", earlyBoots, P4_UPDATE_MODE_THRESHOLD); if (earlyBoots >= P4_UPDATE_MODE_THRESHOLD) { enterUpdateMode(); // kehrt nie zurueck } } g_client.onState(onState); g_client.onAck(onAck); g_client.onProfiles(onProfiles); g_client.onProfileDetails(onProfileDetails); g_client.onUsageStats(onUsageStats); g_client.onWifiNetworks(onWifiNetworks); g_client.onOta(onOta); hal_init(&g_client); // LVGL + Panel/Touch + Dashboard (ruft ui_init intern auf) g_client.begin(); // UART starten, hello folgt automatisch in loop() DBG_SERIAL.println(F("Bereit. Warte auf Verbindung zur Hauptplatine...")); } #if SHOW_DEBUG_OVERLAY static unsigned long s_lastDbgMs = 0; #endif void loop() { g_client.loop(); // UART lesen, hello/heartbeat, state -> UI hal_loop(); // realer Modus: leer; Stub-Modus: lv_timer_handler() // Boot-Loop-Schutz: laeuft die Firmware lange genug stabil, gilt sie als gesund // -> Fruehstart-Zaehler in NVS auf 0 (einmalig). Verhindert Fehlalarm. if (!g_bootCountCleared && millis() > P4_STABLE_UPTIME_MS) { g_bootCountCleared = true; Preferences p; p.begin("bootguard", false); p.putUShort("early", 0); p.end(); DBG_SERIAL.println(F("[bootguard] stabile Laufzeit -> Zaehler zurueckgesetzt")); } // --- On-Screen-Diagnose (alle 500 ms, unabhaengig vom Link-Status) --- #if SHOW_DEBUG_OVERLAY unsigned long now = millis(); if (now - s_lastDbgMs > 500) { s_lastDbgMs = now; String dbg = ""; #if UART_SELFTEST_INTERNAL_LOOPBACK dbg += ">> INTERNER LOOPBACK-SELBSTTEST <<\n"; #endif dbg += "LINK: " + String(g_client.linkUp() ? "UP" : "down"); dbg += " TXpin50 RXpin51 @230400\n"; dbg += "TX hello:" + String(g_client.helloCount); dbg += " cmd:" + String(g_client.txCmdCount); dbg += " RX byte:" + String(g_client.rxByteCount); dbg += " line:" + String(g_client.rxLineCount); dbg += " err:" + String(g_client.parseErrorCount) + "\n"; dbg += "RXlast: "; dbg += (g_client.lastRxLine.length() ? g_client.lastRxLine.substring(0, 110) : "(noch nichts empfangen)"); hal_lock(); ui_set_debug(dbg.c_str()); hal_unlock(); } #endif delay(2); }