fix(P4): Verlaufskurven skalieren nach Daten, Diagnose ausgebaut (1.10.0) #82

Merged
thomas merged 1 commits from fix/verlaufskurven-und-aufraeumen into main 2026-09-03 17:01:50 +02:00
3 changed files with 85 additions and 63 deletions
+17
View File
@@ -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
@@ -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()
+68 -39
View File
@@ -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 -