Files
Dual-PID/P4_Display_Firmware/P4_Display_Firmware.ino
T
raw-designsandClaude Opus 5 5df6c20171 fix(P4): Endlose Neustartschleife nach Kaltstart (1.9.11)
Log zeigt zweimal hintereinander "rst:0x1 (POWERON)" und
"[Panel] Startgrund: 1", danach nichts mehr - Beleuchtung an, kein Bild.

Ursache: Der gewollte einmalige Neustart nach Kaltstart (WS7_COLD_BOOT_RESTART)
prüfte nur esp_reset_reason() == ESP_RST_POWERON. Der Kommentar nahm an, der
zweite Durchlauf melde einen Software-Reset. Tatsächlich meldet der Chip auch
nach esp_restart() wieder POWERON, die Bedingung greift erneut - Endlosschleife.
Der bootguard konnte nicht eingreifen, weil derselbe Zweig "early" jedes Mal
auf 0 zurücksetzt.

- Zusätzliches NVS-Flag "kaltneu" in bootguard: wird vor esp_restart() gesetzt
  und beim zweiten Durchlauf erkannt und gelöscht. Unabhängig vom gemeldeten
  Startgrund, damit die Schleife strukturell ausgeschlossen ist.
- Die Meldung lief über DBG_SERIAL (USB-CDC) und war im Monitor an der UART
  nicht zu sehen - jetzt esp_rom_printf wie die übrigen Panel-Meldungen.

Beide Panel-Varianten mit arduino-cli gegengebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NeFMr439WcLMdebE3kU7fS
2026-09-03 15:08:40 +02:00

422 lines
17 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) {
// Vermessen: Zwischen dem Umschalten und dem sichtbaren Ergebnis vergehen rund fuenf
// Sekunden. Jede Station bekommt eine Zeitmarke, damit die Luecke zu sehen ist.
static bool s_lastStandby = false;
bool wechsel = (st.standbyActive != s_lastStandby);
s_lastStandby = st.standbyActive;
uint32_t t0 = millis();
if (wechsel) esp_rom_printf("[Zustand] %u ms: Standby jetzt %s - Meldung angekommen\n",
(unsigned)t0, st.standbyActive ? "an" : "aus");
// LVGL laeuft im realen Modus in einem eigenen Task -> UI-Zugriff sperren.
hal_lock();
uint32_t t1 = millis();
ui_update(st);
uint32_t t2 = millis();
hal_unlock();
if (wechsel || (t2 - t0) > 200)
esp_rom_printf("[Zustand] %u ms: Sperre %u ms, Anzeige aktualisiert %u ms\n",
(unsigned)millis(), (unsigned)(t1 - t0), (unsigned)(t2 - t1));
// 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) esp_rom_printf("[HAL] %u ms: Helligkeit %d %% gesetzt, brauchte %u ms\n",
(unsigned)millis(), 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();
esp_rom_printf("[Haupt] %u ms: Herzschlag, freier Speicher %u Byte\n",
(unsigned)millis(), (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. Kostet rund drei Sekunden, aber nur beim Einschalten.
//
// Frueher stuetzte sich die Absicherung dagegen allein auf den Startgrund: Beim zweiten
// Durchlauf sei es kein Kaltstart mehr, eine Schleife also ausgeschlossen. Das stimmt
// nicht - der Chip meldet auch nach dem selbst ausgeloesten Neustart wieder einen
// Kaltstart, und dann startet er endlos neu. Genau das ist aufgetreten: Beleuchtung an,
// nie ein Bild. Deshalb wird jetzt zusaetzlich vermerkt, dass der Neustart schon
// stattgefunden hat; darauf ist Verlass, unabhaengig vom gemeldeten Startgrund.
#if (JC_PANEL_TYPE == WS_PANEL_7H) && WS7_COLD_BOOT_RESTART
if (esp_reset_reason() == ESP_RST_POWERON) {
Preferences p;
p.begin("bootguard", false);
bool schonNeugestartet = p.getUChar("kaltneu", 0) != 0;
if (schonNeugestartet) {
// Zweiter Durchlauf: Vermerk loeschen, damit beim naechsten echten Einschalten
// wieder genau ein Neustart erfolgt.
p.putUChar("kaltneu", 0);
p.end();
esp_rom_printf("[Panel] Neustart bereits erfolgt - Hochlauf wird fortgesetzt\n");
} else {
p.putUChar("kaltneu", 1);
// 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.
p.putUShort("early", 0);
p.end();
esp_rom_printf("[Panel] Kaltstart - einmaliger Neustart fuer das Displaypanel\n");
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);
}