Files
Dual-PID/P4_Display_Firmware/P4_Display_Firmware.ino
raw-designsandClaude Opus 5 8db8e166a2 fix(P4): Nach Kaltstart einmal selbst neu starten (1.6.22)
Der DSI-Ab- und Wiederaufbau aus 1.6.21 hat nicht geholfen, sondern das
Bild um einen Schritt verschlechtert: Einschalten - gar nichts, erster
Reset - nur Beleuchtung, zweiter Reset - Bild. Ein Neuaufbau im laufenden
Betrieb ist einem echten Chip-Reset also nicht gleichwertig.
WS7_DSI_RESTART steht wieder auf 0, der Schalter bleibt dokumentiert.

Stattdessen der Weg, den die Messungen stützen: Nach einem Kaltstart
startet der P4 sich genau einmal selbst neu, sobald der erste Hochlauf
durch ist. Ein Reset hat in jedem Versuch zuverlässig geholfen.

- Bedingung ist esp_reset_reason() == ESP_RST_POWERON, beim zweiten
  Durchlauf greift sie nicht mehr - keine Schleife möglich.
- Der Frühstart-Zähler der Absturzsicherung wird vorher zurückgesetzt,
  damit der gewollte Neustart nicht in den Update-Modus führt.
- Schalter WS7_COLD_BOOT_RESTART, kostet ~3 s beim Einschalten.

Das ist eine Umgehung, keine Erklärung: Warum das Panel den ersten Anlauf
nicht annimmt, bleibt offen - der Hochlauf meldet durchgehend Erfolg.

Beide Panel-Varianten mit arduino-cli gegengebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P7JsiDJ1QbNFLDz5chf8Jt
2026-09-02 02:44:06 +02:00

375 lines
14 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) {
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)
// --- 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() {
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);
}