Beim Wechsel in den Standby bleibt das Display stehen: Bild bleibt auf der alten Seite, keine Bedienung, auch der S3 weckt nicht mehr auf. Der Log bricht nach dem letzten Bildaufbau ab, ohne Panic. Statt zu raten, Messpunkte: - Herzschlag alle 2 s aus dem Arduino-loop (freier Heap) und aus einem lv_timer im LVGL-Task (lv_mem_monitor). Bleibt nur einer aus, ist klar, welcher Task steht. - ui_fade_in() der Standby-Uhr wird eingeklammert: "wird eingeblendet" / "eingeblendet, N ms". - hal_backlight() wird gemessen; über 50 ms wird die Dauer gemeldet. Der Bridge-Schreibzugriff läuft über denselben I2C-Bus wie der Touch. Außerdem entfernt: die LV_EVENT_INVALIDATE_AREA-Callbacks aus 1.9.5. Das Event geht per lv_display_send_event an das Display, nicht an Objekte - die Callbacks konnten nie feuern, ihr Ausbleiben war kein Beweis. Beide Panel-Varianten mit arduino-cli gegengebaut. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NeFMr439WcLMdebE3kU7fS
390 lines
15 KiB
Arduino
390 lines
15 KiB
Arduino
// =====================================================================================
|
|
// P4_Display_Firmware.ino
|
|
// Touch-Display-Firmware (ESP32-P4) fuer die Dual-PID-Siebtraegersteuerung
|
|
// =====================================================================================
|
|
//
|
|
// Architektur (siehe Doku/P4-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 "pins_config.h"
|
|
#include <ArduinoJson.h>
|
|
#include <Update.h>
|
|
#include <Preferences.h>
|
|
#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) {
|
|
// Vermessen: Rund um jeden Helligkeitswechsel hing die Bedienung bisher ein bis
|
|
// zwei Sekunden. Der Schreibzugriff geht ueber denselben I2C-Bus wie der Touch.
|
|
uint32_t t0 = millis();
|
|
hal_backlight(bl);
|
|
uint32_t dt = millis() - t0;
|
|
if (dt > 50) Serial.printf("[HAL] Helligkeit %d %% gesetzt, %u ms\n", bl, (unsigned)dt);
|
|
s_lastBacklight = bl;
|
|
}
|
|
}
|
|
|
|
// Herzschlag aus dem Haupt-Ablauf. Zusammen mit dem Herzschlag aus dem Zeichen-Ablauf
|
|
// laesst sich unterscheiden, welcher der beiden stehen bleibt, wenn nichts mehr geht.
|
|
static void main_heartbeat() {
|
|
static uint32_t s_last = 0;
|
|
if (millis() - s_last < 2000) return;
|
|
s_last = millis();
|
|
Serial.printf("[Haupt] Herzschlag, freier Speicher %u Byte\n", (unsigned)ESP.getFreeHeap());
|
|
}
|
|
|
|
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)
|
|
|
|
// --- Waveshare-Panel: einmaliger Neustart nach dem Einschalten ---
|
|
//
|
|
// Das Panel kommt nach dem Anlegen der Versorgung im ersten Anlauf nicht zuverlaessig
|
|
// hoch: Die Beleuchtung geht an, ein Bild bleibt aus - obwohl jeder Schritt des
|
|
// Hochlaufs Erfolg meldet. Ein Reset behebt es zuverlaessig, und zwar nur ein echter
|
|
// Chip-Reset; die DSI-Strecke im laufenden Betrieb ab- und wieder aufzubauen genuegt
|
|
// nicht (siehe WS7_DSI_RESTART).
|
|
//
|
|
// Deshalb startet der P4 sich nach einem Kaltstart genau einmal selbst neu, nachdem
|
|
// der erste Hochlauf durch ist. Beim zweiten Durchlauf ist der Startgrund kein
|
|
// Kaltstart mehr, die Bedingung greift also nicht erneut - eine Schleife ist
|
|
// ausgeschlossen. Kostet rund drei Sekunden, aber nur beim Einschalten.
|
|
#if (JC_PANEL_TYPE == WS_PANEL_7H) && WS7_COLD_BOOT_RESTART
|
|
if (esp_reset_reason() == ESP_RST_POWERON) {
|
|
// Der Fruehstart-Zaehler wurde oben bereits erhoeht. Diesen gewollten Neustart
|
|
// darf er nicht mitzaehlen, sonst laeuft die Absturzsicherung nach ein paar
|
|
// Einschaltvorgaengen in den Update-Modus.
|
|
{ Preferences p; p.begin("bootguard", false); p.putUShort("early", 0); p.end(); }
|
|
DBG_SERIAL.println(F("[Panel] Kaltstart - einmaliger Neustart fuer das Displaypanel"));
|
|
DBG_SERIAL.flush();
|
|
delay(50);
|
|
esp_restart();
|
|
}
|
|
#endif
|
|
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() {
|
|
main_heartbeat();
|
|
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);
|
|
}
|