feat(P4): Verlaufskurven mit festem Ausgangspunkt und pulsierendem Punkt (1.11.0)
Der aelteste Wert einer Kurve liegt jetzt links genau auf halber Hoehe, die Kurve waechst von dort nach oben oder unten. Der Verlauf sieht damit bei jeder Kesseltemperatur gleich aus. Beide Kurven behalten denselben Massstab. Die absolute Hoehe laesst sich dadurch nicht mehr zwischen Wasser und Dampf vergleichen, der Ausschlag aber schon. Am juengsten Messwert sitzt ein kleiner weisser Punkt mit weichem Schein, der langsam pulsiert. Beide Linien sind weiss. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wx7wu7uXXgRzp1zHUhtUa6
This commit is contained in:
co-authored by
Claude Opus 5
parent
5c64892c01
commit
c5ea697f24
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+130
-29
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user