From cc9ae945b1ad995faedab3f3b8baabe902984499 Mon Sep 17 00:00:00 2001 From: raw-designs Date: Thu, 3 Sep 2026 17:01:19 +0200 Subject: [PATCH] fix(P4): Verlaufskurven skalieren nach Daten, Diagnose ausgebaut (1.10.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Sparklines im Dashboard lagen flach auf gleicher Höhe und bewegten sich nicht, obwohl ein Kessel bei 80 °C stand und der andere bei 0. Ursache: lv_chart_set_range() war an den Sollwert gebunden (soll-60 bis soll+30 in Zehntelgrad). Liegt die Temperatur unter diesem Fenster - beim Aufheizen oder bei abgeschaltetem Kessel -, klemmt LVGL jeden Punkt auf den unteren Rand. Beide Kurven landeten dadurch auf derselben Linie. - Neues spark_range_fit(): Ausschnitt aus min/max der vorhandenen Punkte (lv_chart_get_y_array), Mindestspanne DASH_SPARK_MIN_SPAN (40 = 4,0 °C), sonst ein Achtel Luft nach oben und unten. - Der zuletzt gesetzte Bereich wird in user_data gemerkt, weil LVGL keinen Getter dafür hat; ohne den Vergleich würde set_range das Diagramm bei jeder Zustandsmeldung invalidieren. - Die Charts füllen jetzt den Platz unterhalb der Zahlen (flex_grow 1, min_height 90) statt als 64-px-Streifen mittig zu hängen. Aufräumen nach der abgeschlossenen Fehlersuche: - Herzschlag, [Zustand]- und [Touch]-Messzeilen entfernt. - refr_monitor_install() hinter WS7_PERF_MONITOR (aus). - scroll_ohne_nachlauf() gibt LV_OBJ_FLAG_SCROLL_MOMENTUM wieder frei. Beide Panel-Varianten mit arduino-cli gegengebaut. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019G95njDFkedFqXiz5sEaJ2 --- P4_Display_Firmware/Changelog.txt | 17 ++++ P4_Display_Firmware/P4_Display_Firmware.ino | 24 ----- P4_Display_Firmware/ui.cpp | 107 +++++++++++++------- 3 files changed, 85 insertions(+), 63 deletions(-) diff --git a/P4_Display_Firmware/Changelog.txt b/P4_Display_Firmware/Changelog.txt index c15fe0d..6a082c9 100644 --- a/P4_Display_Firmware/Changelog.txt +++ b/P4_Display_Firmware/Changelog.txt @@ -1,3 +1,20 @@ +Version 1.10.0: +- Die Verlaufskurven im Dashboard zeigen jetzt wirklich einen Verlauf. Ihr Ausschnitt hing + bisher am Sollwert - sechs Grad darunter bis drei darüber. Das funktioniert nur, solange + der Kessel schon nahe am Ziel ist. Heizte er noch auf oder war er ganz aus, lag jeder + Messwert unterhalb des Fensters und wurde auf den unteren Rand geklemmt: Beide Kurven lagen + flach auf derselben Höhe und bewegten sich nicht, obwohl der eine Kessel bei 80 Grad stand + und der andere bei 0. Der Ausschnitt richtet sich jetzt nach den tatsächlich vorhandenen + Werten, mit einer Mindestspanne von vier Grad, damit ein ruhig geregelter Kessel nicht + jedes Zehntelgrad zum vollen Ausschlag aufbläht. +- Die Kurven nehmen jetzt den Platz unterhalb der Zahlen ein statt als schmaler Streifen in + der Mitte zu hängen: Sie beginnen weiter unten auf der Karte und sind deutlich höher. +- Der Nachlauf beim Wischen ist zurück. Er war abgeschaltet, weil jedes Einzelbild während + des Schwungs den ganzen Bildschirm neu zeichnete; seit die Anzeigewerte feste Maße haben, + fällt das weg. +- Die Messzeilen der Fehlersuche sind wieder entfernt. Die Messung des Bildaufbaus bleibt + erhalten, liegt aber hinter dem Schalter WS7_PERF_MONITOR und ist aus. + Version 1.9.12: - Die Hintergrundbeleuchtung kommt beim Wechsel in den und aus dem Standby jetzt sofort. Bisher stand das Bild bereits fertig da - im Gegenlicht war es zu sehen -, blieb aber diff --git a/P4_Display_Firmware/P4_Display_Firmware.ino b/P4_Display_Firmware/P4_Display_Firmware.ino index f0aeffe..f0ec04d 100644 --- a/P4_Display_Firmware/P4_Display_Firmware.ino +++ b/P4_Display_Firmware/P4_Display_Firmware.ino @@ -47,23 +47,10 @@ 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 @@ -85,16 +72,6 @@ static void onState(const MachineState& st) { } } -// 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" || @@ -372,7 +349,6 @@ 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() diff --git a/P4_Display_Firmware/ui.cpp b/P4_Display_Firmware/ui.cpp index 67a8dad..4ae5186 100644 --- a/P4_Display_Firmware/ui.cpp +++ b/P4_Display_Firmware/ui.cpp @@ -179,6 +179,9 @@ enum TempDesign { TD_GEWICHTET = 0, TD_VERLAUF, TD_FLAECHEN, TD_BAND, TD_DESIGN_ static uint8_t g_tempDesign = TD_BAND; static bool g_tempColored = false; // Temperaturzahl in der Zustandsfarbe statt weiss #define DASH_SPARK_POINTS 60 // eine Minute Verlauf, ein Punkt je Sekunde +// Kleinster Ausschnitt der Verlaufskurve in Zehntelgrad. Ohne Untergrenze blaeht ein +// ruhig geregelter Kessel jedes Zehntelgrad zum vollen Ausschlag auf. +#define DASH_SPARK_MIN_SPAN 40 // 4,0 Grad static lv_obj_t* g_sparkW = nullptr; // Verlaufskurven der Darstellung TD_VERLAUF static lv_obj_t* g_sparkD = nullptr; static lv_chart_series_t* g_sparkSerW = nullptr; @@ -719,13 +722,10 @@ static void clock_size_apply() { if (g_standbyClock) lv_obj_set_style_text_font(g_standbyClock, clock_font(), 0); } -static void scroll_ohne_nachlauf(lv_obj_t* o) { -#if JC_PANEL_TYPE == WS_PANEL_7H - lv_obj_clear_flag(o, LV_OBJ_FLAG_SCROLL_MOMENTUM); -#else - (void)o; -#endif -} +// Der Nachlauf beim Wischen war auf dem Waveshare-Panel eine Zeit lang abgeschaltet, weil +// jedes Einzelbild waehrend des Schwungs den ganzen Schirm neu zeichnete. Seit die +// Anzeigewerte feste Masse haben, faellt das weg - das Menue laeuft mit Nachlauf fluessig. +static void scroll_ohne_nachlauf(lv_obj_t* o) { (void)o; } static lv_obj_t* make_page(lv_obj_t* parent) { lv_obj_t* p = lv_obj_create(parent); @@ -931,12 +931,7 @@ static bool wake_choice_offered() { (g_state.cxEnabled && g_state.cxArmable && g_state.cxAskOnWake); } static void standby_screen_cb(lv_event_t*) { // Tipp auf Standby-Uhr -> nachfragen - // Vermessen: Vom Tippen bis zur sichtbaren Rueckfrage vergehen rund fuenf Sekunden. - uint32_t t0 = millis(); - esp_rom_printf("[Touch] %u ms: Tipp auf die Standby-Uhr erkannt\n", (unsigned)t0); wake_confirm_show(); - esp_rom_printf("[Touch] %u ms: Rueckfrage aufgebaut, brauchte %u ms\n", - (unsigned)millis(), (unsigned)(millis() - t0)); } static void wake_cancel() { // Abbrechen -> zurueck in die Standby-Helligkeit wake_timeout_stop(); @@ -1113,10 +1108,59 @@ static lv_obj_t* tempField(lv_obj_t* parent, const char* title, lv_color_t accen // Flache Verlaufskurve unter einer Temperatur. Zeigt, ob die Regelung haelt - das ist die // eigentliche Frage an eine PID-Regelung, und eine Zahl kann sie grundsaetzlich nicht // beantworten. Gespeist wird sie aus derselben Sekundenabtastung wie die Verlaufsseite. +// Ausschnitt der Verlaufskurve an die tatsaechlich vorhandenen Werte anpassen. +// +// Frueher spannte er sich um den SOLLWERT: sechs Grad darunter bis drei darueber. Das +// funktioniert nur, solange der Kessel schon nahe am Ziel ist. Heizt er noch auf oder ist +// er ganz aus, liegt jeder Messwert unterhalb des Fensters und wird auf den unteren Rand +// geklemmt - beide Kurven lagen dann flach auf derselben Hoehe und bewegten sich nicht, +// obwohl der eine Kessel bei 80 Grad stand und der andere bei 0. +static void spark_range_fit(lv_obj_t* ch, lv_chart_series_t* ser) { + if (!ch || !ser) return; + int32_t* pts = lv_chart_get_y_array(ch, ser); + if (!pts) return; + uint32_t n = lv_chart_get_point_count(ch); + int32_t mn = INT32_MAX, mx = INT32_MIN; + for (uint32_t i = 0; i < n; i++) { + if (pts[i] == LV_CHART_POINT_NONE) continue; + if (pts[i] < mn) mn = pts[i]; + if (pts[i] > mx) mx = pts[i]; + } + if (mn > mx) return; // noch kein einziger Wert + if (mx - mn < DASH_SPARK_MIN_SPAN) { // ruhige Regelung: Kurve mittig halten + int32_t mitte = (mn + mx) / 2; + mn = mitte - DASH_SPARK_MIN_SPAN / 2; + mx = mitte + DASH_SPARK_MIN_SPAN / 2; + } else { // sonst etwas Luft nach oben und unten + int32_t luft = (mx - mn) / 8; + mn -= luft; + mx += luft; + } + // Nur bei echter Aenderung setzen - lv_chart_set_range macht das Diagramm sonst bei + // jeder Zustandsmeldung ungueltig, auch wenn sich der Ausschnitt gar nicht ruehrt. + // Den zuletzt gesetzten Ausschnitt merkt sich die Kurve selbst; abfragen kann man ihn + // bei LVGL nicht. + int32_t* merk = (int32_t*)lv_obj_get_user_data(ch); + if (merk && merk[0] == mn && merk[1] == mx) return; + if (!merk) { + merk = (int32_t*)lv_malloc(2 * sizeof(int32_t)); + if (!merk) { lv_chart_set_range(ch, LV_CHART_AXIS_PRIMARY_Y, mn, mx); return; } + lv_obj_set_user_data(ch, merk); + } + merk[0] = mn; merk[1] = mx; + lv_chart_set_range(ch, LV_CHART_AXIS_PRIMARY_Y, mn, mx); +} + static lv_obj_t* tempSpark(lv_obj_t* parent, lv_color_t col, lv_chart_series_t** ser, float jetzt, float soll) { lv_obj_t* ch = lv_chart_create(parent); - lv_obj_set_size(ch, LV_PCT(96), 64); + // Die Kurve nimmt den Platz unterhalb der Zahlen ein, statt als schmaler Streifen in + // der Mitte zu haengen. Sie beginnt damit weiter unten auf der Karte und hat deutlich + // mehr Hoehe - Temperaturaenderungen sind so ueberhaupt erst zu erkennen. + lv_obj_set_width(ch, LV_PCT(96)); + lv_obj_set_height(ch, LV_PCT(100)); + lv_obj_set_flex_grow(ch, 1); + lv_obj_set_style_min_height(ch, 90, 0); lv_chart_set_type(ch, LV_CHART_TYPE_LINE); lv_chart_set_point_count(ch, DASH_SPARK_POINTS); lv_chart_set_div_line_count(ch, 0, 0); @@ -1130,12 +1174,12 @@ static lv_obj_t* tempSpark(lv_obj_t* parent, lv_color_t col, lv_chart_series_t** lv_obj_clear_flag(ch, LV_OBJ_FLAG_SCROLLABLE); *ser = lv_chart_add_series(ch, col, LV_CHART_AXIS_PRIMARY_Y); - // Ausschnitt und Startwert sofort setzen. Ein frisch angelegtes Diagramm hat lauter - // "kein Wert"-Punkte und zeichnet deshalb nichts - beim Umschalten der Darstellung - // saehe man bis zur ersten Sekundenprobe eine leere Flaeche. - int32_t sollT = (int32_t)(soll * 10.0f); - lv_chart_set_range(ch, LV_CHART_AXIS_PRIMARY_Y, sollT - 60, sollT + 30); + // Startwert sofort setzen. Ein frisch angelegtes Diagramm hat lauter "kein Wert"-Punkte + // und zeichnet deshalb nichts - beim Umschalten der Darstellung saehe man bis zur + // ersten Sekundenprobe eine leere Flaeche. + (void)soll; lv_chart_set_all_value(ch, *ser, (int32_t)(jetzt * 10.0f)); + spark_range_fit(ch, *ser); return ch; } @@ -1677,17 +1721,14 @@ static void chart_duty_btn_cb(lv_event_t*) { chart_redraw(); // Verlaufskurven des Dashboards (Darstellung "Verlauf") mitfuehren. Der Ausschnitt - // haengt am Sollwert, damit die Abweichung sichtbar wird und nicht in der Skala - // untergeht - eine feste Skala von 0 bis 130 Grad zeigte nur eine gerade Linie. + // richtet sich nach den vorhandenen Werten, siehe spark_range_fit(). if (g_sparkSerW && g_sparkW) { - int32_t soll = (int32_t)(g_state.setW * 10.0f); - lv_chart_set_range(g_sparkW, LV_CHART_AXIS_PRIMARY_Y, soll - 60, soll + 30); lv_chart_set_next_value(g_sparkW, g_sparkSerW, (int32_t)(g_state.tempW * 10.0f)); + spark_range_fit(g_sparkW, g_sparkSerW); } if (g_sparkSerD && g_sparkD) { - int32_t soll = (int32_t)(g_state.setD * 10.0f); - lv_chart_set_range(g_sparkD, LV_CHART_AXIS_PRIMARY_Y, soll - 120, soll + 60); lv_chart_set_next_value(g_sparkD, g_sparkSerD, (int32_t)(g_state.tempD * 10.0f)); + spark_range_fit(g_sparkD, g_sparkSerD); } } static void build_chart(lv_obj_t* p) { @@ -3573,13 +3614,6 @@ static void refr_ready_cb(lv_event_t*) { (int)g_refrArea[i].y1, (int)g_refrArea[i].y2); } -static void ui_heartbeat_cb(lv_timer_t*) { - lv_mem_monitor_t m; lv_mem_monitor(&m); - esp_rom_printf("[UI] %u ms: Herzschlag, LVGL-Speicher frei %u von %u Byte, groesster Block %u\n", - (unsigned)millis(), (unsigned)m.free_size, (unsigned)m.total_size, - (unsigned)m.free_biggest_size); -} - // LVGL legt fuer JEDE Textglyphe einen eigenen Zwischenpuffer an - Breite mal Hoehe, ein // Byte je Bildpunkt. Bei der grossen Standby-Uhr sind das 190 x 256 = rund 48 KB pro Ziffer, // und mit zwei parallel arbeitenden Zeicheneinheiten fast 100 KB gleichzeitig. Der interne @@ -4024,8 +4058,9 @@ void ui_init(ProtocolClient* client) { lv_draw_buf_handlers_t* fh = lv_draw_buf_get_font_handlers(); fh->buf_malloc_cb = font_buf_malloc; fh->buf_free_cb = font_buf_free; - refr_monitor_install(); - lv_timer_create(ui_heartbeat_cb, 2000, nullptr); +#if WS7_PERF_MONITOR + refr_monitor_install(); // nur zum Einmessen, siehe WS7_PERF_MONITOR +#endif g_uiReady = true; } @@ -4217,14 +4252,8 @@ void ui_update(const MachineState& st) { if (shouldShow) { label_set_if_changed(g_standbyClock, zeit.length() ? zeit.c_str() : "--:--"); if (!g_standbyShown) { - // Vermessen: Das Einblenden der Uhr ist der Moment, in dem das Display - // zuletzt stehen geblieben ist. Bleibt die zweite Zeile aus, haengt es hier. - esp_rom_printf("[UI] Standby-Uhr wird eingeblendet\n"); - uint32_t t0 = millis(); ui_fade_in(g_standbyScreen, UI_FADE_MS); // Einschlaf-Animation g_standbyShown = true; - esp_rom_printf("[UI] Standby-Uhr eingeblendet, %u ms\n", - (unsigned)(millis() - t0)); } // Frueher wurde die Uhr hier bei JEDER Zustandsmeldung nach vorn geholt. Das // ordnet die Ebenen neu und laesst LVGL den gesamten Bildschirm neu zeichnen - -- 2.54.0