Dieselbe Display-Firmware bedient jetzt wahlweise das bisherige 4,3-Zoll-Panel JC4880P443C-I-W (ST7701, 480x800) oder das neue 7-Zoll-Panel JC1060P470C-I-W (JD9165, 1024x600). Umgeschaltet wird über JC_PANEL_TYPE in config.h (JC_PANEL_43 / JC_PANEL_70) oder per Compiler-Flag -DJC_PANEL_TYPE=70. Vorgabe bleibt das 4,3-Zoll-Panel; dessen Verhalten ändert sich nicht. - src/lcd/esp_lcd_jd9165.c/.h aus dem Hersteller-Paket übernommen; board_bringup.c legt je nach Panel DSI-Bus, DPI-Zeitbasis und Herstellertreiber an. - pins_config.h liefert Auflösung, LCD-Reset (4,3": GPIO5, 7,0": GPIO27) und Ausrichtung panelabhängig. Das 7-Zoll-Panel ist nativ Querformat, deshalb entfallen dort die 270-Grad-Rotation und die PPA-Beschleunigung. - lvgl_port_v9.h bezieht LVGL_PORT_H/V_RES aus pins_config.h statt fest 480/800. - Der GT911 des 7-Zöllers meldet Rohkoordinaten im Raster 800x480 statt 1024x600. Die Umrechnung hängt am process_coordinates-Hook von esp_lcd_touch, damit die Herstellerdatei esp_lcd_touch_gt911.c unverändert bleibt. - ui.cpp rechnet nicht mehr mit fest verdrahteten 480 Pixeln Höhe, sondern mit DISP_VER_RES. Layout und Schriften bleiben sonst unverändert. - Hersteller-Paket des 7-Zöllers unter JC_Display_Firmware_7zoll/ abgelegt; Werkzeuge, Archive und mitgelieferte Fremdbibliotheken per .gitignore ausgeschlossen (587 MB im Original, 21 MB im Repo). Kompiliertest ESP32-P4, beide Varianten: 4,3": 1.173.768 B Flash (37 %), 322.880 B RAM (98 %) 7,0": 1.138.752 B Flash (36 %), 322.600 B RAM (98 %) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
349 lines
13 KiB
Arduino
349 lines
13 KiB
Arduino
// =====================================================================================
|
|
// 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 <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)
|
|
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);
|
|
}
|