diff --git a/P4_Display_Firmware/Changelog.txt b/P4_Display_Firmware/Changelog.txt index ca475df..90c4581 100644 --- a/P4_Display_Firmware/Changelog.txt +++ b/P4_Display_Firmware/Changelog.txt @@ -1,3 +1,14 @@ +Version 1.11.0: +- Die Verlaufskurven im Dashboard beginnen jetzt immer am selben Punkt: Der älteste Wert + liegt links genau auf halber Höhe, die Kurve wächst von dort nach oben oder unten. Der + Verlauf sieht dadurch bei jeder Kesseltemperatur gleich aus. +- Beide Kurven behalten denselben Maßstab. Die absolute Höhe lässt sich damit nicht mehr + zwischen Wasser und Dampf vergleichen - der Ausschlag aber schon: Wer weiter ausschlägt, + hat sich auch stärker verändert. +- Am jüngsten Messwert sitzt ein kleiner weißer Punkt mit weichem Schein, der langsam + pulsiert. +- Beide Linien sind jetzt weiß. Die Farbe der Karte steckt ohnehin in der Überschrift. + Version 1.10.4: - Die Maßnahme aus 1.10.3 machte die Delle nach dem Standby länger statt kleiner. Sie hielt beim Umschalten des Standby-Merkers den zuletzt gemeldeten Wert fest - zu diesem diff --git a/P4_Display_Firmware/config.h b/P4_Display_Firmware/config.h index 5a86062..5b4e3fe 100644 --- a/P4_Display_Firmware/config.h +++ b/P4_Display_Firmware/config.h @@ -126,7 +126,7 @@ // ------------------------------------------------------------------------------------- // Firmware // ------------------------------------------------------------------------------------- -#define DISPLAY_FW_VERSION "1.10.4" // Firmware-Stand der P4-Display-Steuerung (Info-Seite) +#define DISPLAY_FW_VERSION "1.11.0" // Firmware-Stand der P4-Display-Steuerung (Info-Seite) // ------------------------------------------------------------------------------------- // Protokoll diff --git a/P4_Display_Firmware/ui.cpp b/P4_Display_Firmware/ui.cpp index a472f46..ae22a09 100644 --- a/P4_Display_Firmware/ui.cpp +++ b/P4_Display_Firmware/ui.cpp @@ -182,10 +182,17 @@ static bool g_tempColored = false; // Temperaturzahl in der Zustandsfarbe // 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 +// Punkt am juengsten Messwert. Er pulsiert leicht und traegt einen weichen Schein - das +// ist die einzige Stelle der Oberflaeche, an der etwas dauerhaft in Bewegung ist, und +// deshalb bleibt er klein und langsam. +#define SPARK_DOT_SIZE 12 +#define SPARK_DOT_PULS_MS 1100 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; static lv_chart_series_t* g_sparkSerD = nullptr; +static lv_obj_t* g_sparkDotW = nullptr; // pulsierender Punkt am juengsten Messwert +static lv_obj_t* g_sparkDotD = nullptr; static lv_obj_t* g_readyBand = nullptr; // Zustandsband der Darstellung TD_BAND static lv_obj_t* g_readyBandLbl = nullptr; static lv_obj_t* g_tempRow = nullptr; // Container; Inhalt wird je Design (neu) aufgebaut @@ -1119,8 +1126,10 @@ static lv_obj_t* tempField(lv_obj_t* parent, const char* title, lv_color_t accen // richtet. Damit ist die Hoehe wieder vergleichbar - der waermere Kessel liegt sichtbar // hoeher -, und die Skala bleibt so eng, wie es die Daten zulassen: Stehen beide Kessel // dicht beieinander, greift die Mindestspanne und schon Zehntelgrade sind zu sehen. -static int32_t g_sparkMin = INT32_MIN; // zuletzt gesetzter Ausschnitt, fuer beide gleich -static int32_t g_sparkMax = INT32_MIN; +static int32_t g_sparkMinW = INT32_MIN; // zuletzt gesetzter Ausschnitt je Kurve +static int32_t g_sparkMaxW = INT32_MIN; +static int32_t g_sparkMinD = INT32_MIN; +static int32_t g_sparkMaxD = INT32_MIN; // Kurz nach einem Standby-Wechsel sackt die Wassertemperatur fuer einen Sekundenbruchteil // ab. In genau diesem Moment schalten die Heizungen, und der Wassersensor ist ein NTC am @@ -1156,44 +1165,112 @@ static float median_von(const float* quelle, uint8_t anzahl) { return f[anzahl / 2]; } -static void spark_scan(lv_obj_t* ch, lv_chart_series_t* ser, int32_t* mn, int32_t* mx) { - if (!ch || !ser) return; +// Ausschnitt einer Kurve: Der aelteste Punkt - links am Rand - liegt genau auf halber +// Hoehe, und die Kurve waechst von dort nach oben oder unten. Das ist der ruhige Eindruck, +// den ein Verlauf haben soll: Er beginnt immer an derselben Stelle, egal bei welcher +// Temperatur der Kessel gerade steht. +// +// Beide Kurven bekommen denselben Massstab (gleiche Grad je Bildpunkt). Die absolute Hoehe +// laesst sich damit zwar nicht mehr vergleichen - dafuer aber der Ausschlag: Eine Kurve, +// die doppelt so weit ausschlaegt, hat sich auch doppelt so stark veraendert. +// +// Rueckgabe: falsch, wenn die Kurve noch keinen brauchbaren Wert hat. +static bool spark_anker(lv_obj_t* ch, lv_chart_series_t* ser, int32_t* anker, int32_t* abweichung) { + if (!ch || !ser) return false; int32_t* pts = lv_chart_get_y_array(ch, ser); - if (!pts) return; + if (!pts) return false; uint32_t n = lv_chart_get_point_count(ch); + if (n == 0) return false; + uint32_t erster = lv_chart_get_x_start_point(ch, ser) % n; // aeltester Punkt im Ring + int32_t a = pts[erster]; + if (a == LV_CHART_POINT_NONE) return false; + int32_t weit = 0; 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]; + int32_t d = pts[i] - a; + if (d < 0) d = -d; + if (d > weit) weit = d; } + *anker = a; + *abweichung = weit; + return true; +} + +// Punkt am juengsten Messwert an seine Stelle setzen (rechter Rand, Hoehe nach Wert). +static void spark_dot_place(lv_obj_t* ch, lv_chart_series_t* ser, lv_obj_t* dot, + int32_t mn, int32_t mx) { + if (!ch || !ser || !dot) return; + if (mx <= mn) return; + int32_t* pts = lv_chart_get_y_array(ch, ser); + uint32_t n = lv_chart_get_point_count(ch); + if (!pts || n == 0) return; + uint32_t letzter = (lv_chart_get_x_start_point(ch, ser) + n - 1) % n; // juengster Punkt + int32_t v = pts[letzter]; + if (v == LV_CHART_POINT_NONE) { lv_obj_add_flag(dot, LV_OBJ_FLAG_HIDDEN); return; } + int32_t w = lv_obj_get_content_width(ch); + int32_t h = lv_obj_get_content_height(ch); + if (w <= 0 || h <= 0) return; + int32_t r = SPARK_DOT_SIZE / 2; + int32_t y = h - (int32_t)(((int64_t)(v - mn) * h) / (mx - mn)); + if (y < r) y = r; + if (y > h - r) y = h - r; + lv_obj_align(dot, LV_ALIGN_TOP_LEFT, w - r, y - r); // Mitte auf den letzten Punkt + lv_obj_remove_flag(dot, LV_OBJ_FLAG_HIDDEN); } static void spark_ranges_fit() { - int32_t mn = INT32_MAX, mx = INT32_MIN; - spark_scan(g_sparkW, g_sparkSerW, &mn, &mx); - spark_scan(g_sparkD, g_sparkSerD, &mn, &mx); - if (mn > mx) return; // noch kein einziger Wert - if (mx - mn < DASH_SPARK_MIN_SPAN) { // beide dicht beieinander: 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; - } + int32_t aW = 0, aD = 0, dW = 0, dD = 0; + bool okW = spark_anker(g_sparkW, g_sparkSerW, &aW, &dW); + bool okD = spark_anker(g_sparkD, g_sparkSerD, &aD, &dD); + if (!okW && !okD) return; // noch kein einziger Wert + + int32_t halb = (okW && dW > dD) ? dW : dD; // gemeinsamer Massstab: der groessere Ausschlag + if (!okW) halb = dD; + if (!okD) halb = dW; + halb += halb / 8; // etwas Luft, damit die Kurve nicht den Rand kuesst + if (halb < DASH_SPARK_MIN_SPAN / 2) halb = DASH_SPARK_MIN_SPAN / 2; + // 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. // Abfragen laesst sich der gesetzte Ausschnitt bei LVGL nicht, also wird er gemerkt. - if (mn == g_sparkMin && mx == g_sparkMax) return; - g_sparkMin = mn; - g_sparkMax = mx; - if (g_sparkW) lv_chart_set_range(g_sparkW, LV_CHART_AXIS_PRIMARY_Y, mn, mx); - if (g_sparkD) lv_chart_set_range(g_sparkD, LV_CHART_AXIS_PRIMARY_Y, mn, mx); + if (okW) { + int32_t mn = aW - halb, mx = aW + halb; + if (mn != g_sparkMinW || mx != g_sparkMaxW) { + g_sparkMinW = mn; g_sparkMaxW = mx; + lv_chart_set_range(g_sparkW, LV_CHART_AXIS_PRIMARY_Y, mn, mx); + } + spark_dot_place(g_sparkW, g_sparkSerW, g_sparkDotW, g_sparkMinW, g_sparkMaxW); + } + if (okD) { + int32_t mn = aD - halb, mx = aD + halb; + if (mn != g_sparkMinD || mx != g_sparkMaxD) { + g_sparkMinD = mn; g_sparkMaxD = mx; + lv_chart_set_range(g_sparkD, LV_CHART_AXIS_PRIMARY_Y, mn, mx); + } + spark_dot_place(g_sparkD, g_sparkSerD, g_sparkDotD, g_sparkMinD, g_sparkMaxD); + } +} + +// Weiches Pulsieren des Punktes: Nur der Schein atmet, der Punkt selbst bleibt ruhig. +static void spark_dot_puls_cb(void* var, int32_t v) { + lv_obj_t* d = (lv_obj_t*)var; + lv_obj_set_style_shadow_width(d, 6 + v / 5, 0); // 6 .. 26 + lv_obj_set_style_shadow_opa(d, (lv_opa_t)(50 + v), 0); // 50 .. 150 +} +static void spark_dot_puls(lv_obj_t* dot) { + lv_anim_t a; lv_anim_init(&a); + lv_anim_set_var(&a, dot); + lv_anim_set_exec_cb(&a, spark_dot_puls_cb); + lv_anim_set_values(&a, 0, 100); + lv_anim_set_duration(&a, SPARK_DOT_PULS_MS); + lv_anim_set_playback_duration(&a, SPARK_DOT_PULS_MS); + lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); + lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out); + lv_anim_start(&a); } 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** dot, float jetzt, float soll) { lv_obj_t* ch = lv_chart_create(parent); // 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 @@ -1215,6 +1292,24 @@ 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); + // Punkt am juengsten Messwert, mit weichem Schein. Er haengt als eigenes Element im + // Diagramm; gesetzt wird seine Stelle in spark_dot_place, sobald der Ausschnitt steht. + *dot = lv_obj_create(ch); + lv_obj_remove_style_all(*dot); + lv_obj_set_size(*dot, SPARK_DOT_SIZE, SPARK_DOT_SIZE); + lv_obj_set_style_radius(*dot, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_bg_color(*dot, lv_color_white(), 0); + lv_obj_set_style_bg_opa(*dot, LV_OPA_COVER, 0); + lv_obj_set_style_shadow_color(*dot, lv_color_white(), 0); + lv_obj_set_style_shadow_width(*dot, 10, 0); + lv_obj_set_style_shadow_opa(*dot, 90, 0); + lv_obj_remove_flag(*dot, LV_OBJ_FLAG_CLICKABLE); + lv_obj_remove_flag(*dot, LV_OBJ_FLAG_SCROLLABLE); + // Bis die erste Stelle berechnet ist, bleibt er verborgen - sonst blitzte er kurz + // in der linken oberen Ecke auf. + lv_obj_add_flag(*dot, LV_OBJ_FLAG_HIDDEN); + spark_dot_puls(*dot); + // 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. @@ -1233,7 +1328,9 @@ static void build_temp_row_content() { bulbTempW = bulbTempD = nullptr; g_sparkW = g_sparkD = nullptr; g_sparkSerW = g_sparkSerD = nullptr; - g_sparkMin = g_sparkMax = INT32_MIN; // gemerkter Ausschnitt gilt fuer die alten Kurven + g_sparkDotW = g_sparkDotD = nullptr; + g_sparkMinW = g_sparkMaxW = INT32_MIN; // gemerkter Ausschnitt gilt fuer die alten Kurven + g_sparkMinD = g_sparkMaxD = INT32_MIN; g_readyBand = g_readyBandLbl = nullptr; const lv_color_t teal = lv_color_hex(0x80cbc4); @@ -1258,10 +1355,14 @@ static void build_temp_row_content() { case TD_VERLAUF: { lv_obj_t* cw = tempField(g_tempRow, "WASSER", COL_ACCENT, &lblTempW, &lblSetW, &barDutyW, 1, &lv_font_maven_pro_bold_96, false, 0); - g_sparkW = tempSpark(cw, COL_OK, &g_sparkSerW, g_state.tempW, g_state.setW); + // Beide Linien weiss: Die Farbe der Karte steckt schon in der Ueberschrift, und + // zwei verschiedenfarbige Linien nebeneinander wirken unruhiger als eine Sprache. + g_sparkW = tempSpark(cw, lv_color_white(), &g_sparkSerW, &g_sparkDotW, + g_state.tempW, g_state.setW); lv_obj_t* cd = tempField(g_tempRow, "DAMPF", teal, &lblTempD, &lblSetD, &barDutyD, 1, &lv_font_maven_pro_bold_96, false, 0); - g_sparkD = tempSpark(cd, COL_ACCENT, &g_sparkSerD, g_state.tempD, g_state.setD); + g_sparkD = tempSpark(cd, lv_color_white(), &g_sparkSerD, &g_sparkDotD, + g_state.tempD, g_state.setD); spark_ranges_fit(); break; }