Compare commits

...
Author SHA1 Message Date
thomas 7296a0d2f6 Merge pull request 'feat(P4): Verlaufskurven mit festem Ausgangspunkt und pulsierendem Punkt (1.11.0)' (#87) from feat/verlaufskurven-anker-und-pulspunkt into main 2026-09-04 11:39:50 +02:00
raw-designsandClaude Opus 5 c5ea697f24 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
2026-09-04 11:39:29 +02:00
thomas 5c64892c01 Merge pull request 'fix(P4): Delle nach dem Standby per Median statt Sperrfrist (1.10.4)' (#86) from fix/temperaturdelle-median into main 2026-09-04 10:22:05 +02:00
raw-designsandClaude Opus 5 1febc0b092 fix(P4): Delle nach dem Standby per Median statt Sperrfrist (1.10.4)
Die Massnahme aus 1.10.3 verlaengerte die Delle, statt sie zu entfernen.
Sie hielt beim Umschalten des Standby-Merkers den zuletzt gemeldeten Wert
fest - zu diesem Zeitpunkt lag die gestoerte Probe aber bereits an.
Festgehalten wurde also ausgerechnet die Delle, und das sekundenlang.

Die Verlaufskurven tragen jetzt den mittleren Wert der letzten fuenf
Proben ein (Median). Ein bis zwei verirrte Proben verschwinden damit
vollstaendig, unabhaengig vom Zeitpunkt. Echte Aenderungen kommen
unverfaelscht durch, nur zwei Sekunden spaeter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011gwqJf2DcqHFQpoY3XkDHX
2026-09-04 10:21:44 +02:00
thomas a84f902f75 Merge pull request 'fix(P4): Temperaturdelle in den Verlaufskurven nach dem Standby (1.10.3)' (#85) from fix/temperaturdelle-nach-standby into main 2026-09-04 09:16:50 +02:00
raw-designsandClaude Opus 5 bcbeea2a17 fix(P4): Temperaturdelle in den Verlaufskurven nach dem Standby (1.10.3)
Direkt nach dem Aufwachen aus dem Standby zog die Wasserkurve eine kurze
Delle nach unten. Ursache ist die Messung, nicht der Kessel: In genau
diesem Moment schalten die Heizungen, und der Wassersensor haengt als NTC
am ADC. Ein Kessel kann binnen einer Sekunde nicht um Grade einbrechen.

Die Verlaufskurven halten deshalb rund um jeden Standby-Wechsel sechs
Sekunden lang ihren letzten Wert, statt die gestoerte Probe einzutragen.
Das gilt fuer die kleinen Kurven im Dashboard und fuer die grosse
Verlaufsseite. Die angezeigten Temperaturzahlen bleiben unveraendert.

Ausserdem: DISPLAY_FW_VERSION stand seit dem Ruecknehmen von 1.6.11 auf
1.6.10 fest und wurde bei keinem der folgenden Staende mitgezogen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fe8KxS47iPmuByJPs5RvEw
2026-09-04 09:16:18 +02:00
thomas 1fe96b70aa Merge pull request 'fix(P4): Dashboard-Sparklines wurden nie fortgeschrieben (1.10.2)' (#84) from fix/verlaufskurven-werden-nie-fortgeschrieben into main 2026-09-04 01:42:57 +02:00
raw-designsandClaude Opus 5 43517a2c48 fix(P4): Dashboard-Sparklines wurden nie fortgeschrieben (1.10.2)
Die Linien blieben identisch und unbewegt - kein Skalierungsproblem, wie
1.10.0 und 1.10.1 angenommen hatten.

lv_chart_set_next_value() für g_sparkW/g_sparkD stand am Ende von
chart_duty_btn_cb(), dem Callback des Heizleistungs-Umschalters auf der
Verlauf-SEITE. Die Sparklines wuchsen also nur, wenn jemand genau diesen
Knopf drückte. Ansonsten zeigten sie unverändert, was lv_chart_set_all_value()
beim Aufbau der Darstellung eingetragen hatte.

Der Block sitzt jetzt in chart_sample_cb() (1-s-Timer), und zwar vor dessen
"if (!g_chart || !g_hist) return" - die Dashboard-Kurven sollen nicht davon
abhängen, ob die Verlauf-Seite samt großem Diagramm aufgebaut ist.

Die gemeinsame Skala aus 1.10.1 bleibt richtig, konnte bisher aber nichts
ausrichten, weil nie neue Werte ankamen.

Beide Panel-Varianten mit arduino-cli gegengebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012gBXGt5M22ns5YfNJRaiMu
2026-09-04 01:42:35 +02:00
thomas 6a867e54fd Merge pull request 'fix(P4): Verlaufskurven mit gemeinsamer Skala - Höhen wieder vergleichbar (1.10.1)' (#83) from fix/verlauf-gemeinsame-skala into main 2026-09-03 17:23:29 +02:00
raw-designsandClaude Opus 5 edfa2d4711 fix(P4): Verlaufskurven mit gemeinsamer Skala - Höhen wieder vergleichbar (1.10.1)
Nachbesserung zu 1.10.0. Dort bekam jede Kurve ihren eigenen Ausschnitt aus
ihren eigenen min/max. Bei zwei flachen Kurven (Wasser 70 °C, Dampf 0 °C)
greift beidesmal die Mindestspanne, jede Linie sitzt mittig in ihrem Fenster -
und beide liegen auf identischer Höhe. Die Skalierung war zu clever: Bewegung
wurde sichtbar, aber die Höhe verlor jede Bedeutung.

- spark_range_fit(ch, ser) ersetzt durch spark_ranges_fit(): scannt beide
  Serien, bildet einen gemeinsamen Bereich und setzt ihn auf beide Charts.
- Der gemerkte Bereich liegt jetzt in zwei Datei-Statics statt in user_data
  der Charts; das nimmt auch das nicht freigegebene lv_malloc bei jedem
  Neuaufbau der Temperaturzeile weg.
- build_temp_row_content() setzt den Merker beim Aufbau zurück und ruft
  spark_ranges_fit() erst, wenn beide Kurven stehen.

Beide Panel-Varianten mit arduino-cli gegengebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ZJ2iUTbm7Zz5DTPkwgJyC
2026-09-03 17:22:28 +02:00
thomas fc0a756127 Merge pull request 'fix(P4): Verlaufskurven skalieren nach Daten, Diagnose ausgebaut (1.10.0)' (#82) from fix/verlaufskurven-und-aufraeumen into main 2026-09-03 17:01:50 +02:00
3 changed files with 260 additions and 51 deletions
+55
View File
@@ -1,3 +1,58 @@
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
Zeitpunkt lag die gestörte Probe aber bereits an. Festgehalten wurde also ausgerechnet
die Delle, und das sekundenlang.
- Statt einer Sperrfrist tragen die Verlaufskurven jetzt den mittleren Wert der letzten
fünf Proben ein (Median). Eine oder zwei verirrte Proben verschwinden damit vollständig,
unabhängig davon, wann sie auftreten - beim Standby-Ende ebenso wie beim Bezugsstart.
- Echte Temperaturänderungen kommen unverfälscht durch, nur zwei Sekunden später. Das ist
die Verzögerung, um die es hier ging.
- Die angezeigten Temperaturzahlen bleiben weiterhin ungefiltert.
Version 1.10.3:
- Direkt nach dem Aufwachen aus dem Standby zog die Wasserkurve eine kurze Delle nach
unten. Ursache ist die Messung, nicht der Kessel: In genau diesem Moment schalten die
Heizungen, und der Wassersensor hängt als NTC am ADC. Ein Kessel kann binnen einer
Sekunde nicht um Grade einbrechen.
- Die Verlaufskurven halten deshalb rund um jeden Standby-Wechsel sechs Sekunden lang
ihren letzten Wert, statt die gestörte Probe einzutragen. Das gilt für die kleinen
Kurven im Dashboard und für die große Verlaufsseite.
- Die angezeigten Temperaturzahlen bleiben unverändert: Dort steht weiter, was gemessen
wird.
- Die Versionsanzeige stand seit dem Zurücknehmen von 1.6.11 auf 1.6.10 fest und wurde bei
keinem der folgenden Stände mitgezogen. Sie zeigt jetzt wieder den echten Stand.
Version 1.10.2:
- Die Verlaufskurven im Dashboard wurden überhaupt nicht fortgeschrieben. Der Code dafür
stand am Ende der Umschaltung „Heizleistung anzeigen" auf der Verlauf-Seite - also im
Callback eines Knopfes. Die Kurven wuchsen nur weiter, wenn jemand genau diesen Knopf
drückte. Sonst zeigten sie unverändert den Wert, der beim Aufbau der Darstellung gerade
anlag: zwei flache, gleich hohe Linien.
- Die Fortschreibung sitzt jetzt im sekündlichen Abtaster, dort, wo auch der große Verlauf
seine Messwerte nimmt. Damit bewegen sich die Kurven, und die gemeinsame Skala aus 1.10.1
kann ihre Arbeit überhaupt erst tun.
Version 1.10.1:
- Die beiden Verlaufskurven teilen sich jetzt eine gemeinsame Skala. In 1.10.0 zoomte jede
Kurve auf ihre eigenen Werte - dadurch saß jede mittig in ihrem eigenen Fenster, und
Wasser bei 70 Grad lag genauso hoch wie Dampf bei 0. Die Höhe sagte nichts mehr aus.
- Die Skala richtet sich nach den Werten beider Kessel zusammen. Der wärmere Kessel liegt
damit sichtbar höher, und die Skala bleibt so eng, wie es die Daten zulassen: Stehen beide
dicht beieinander, greift die Mindestspanne von vier Grad und schon Zehntelgrade sind zu
erkennen.
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
+1 -1
View File
@@ -126,7 +126,7 @@
// -------------------------------------------------------------------------------------
// Firmware
// -------------------------------------------------------------------------------------
#define DISPLAY_FW_VERSION "1.6.10" // Firmware-Stand der P4-Display-Steuerung (Info-Seite)
#define DISPLAY_FW_VERSION "1.11.0" // Firmware-Stand der P4-Display-Steuerung (Info-Seite)
// -------------------------------------------------------------------------------------
// Protokoll
+204 -50
View File
@@ -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
@@ -1108,51 +1115,162 @@ 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.
// Gemeinsamer Ausschnitt fuer BEIDE Verlaufskurven.
//
// 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;
// Zwei Anlaeufe waren vorher daneben. Erst hing der Ausschnitt am Sollwert - dann lag beim
// Aufheizen jeder Messwert unterhalb des Fensters und wurde auf den unteren Rand geklemmt.
// Danach zoomte jede Kurve auf ihre eigenen Werte - dann sass jede mittig in ihrem eigenen
// Fenster, und Wasser bei 70 Grad lag genauso hoch wie Dampf bei 0. Die Hoehe sagte nichts.
//
// Jetzt teilen sich beide Kurven eine Skala, die sich nach den Werten beider zusammen
// 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_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
// ADC, einmal je Regelzyklus abgetastet - ein Kessel kann binnen einer Sekunde nicht um
// Grade einbrechen, die Delle ist also eine Stoerung der Messung, kein Vorgang an der
// Maschine.
//
// In die Kurven geht deshalb nicht die frische Probe, sondern der mittlere Wert der
// letzten fuenf (Median). Eine oder zwei verirrte Proben verschwinden damit vollstaendig,
// waehrend echte Temperaturaenderungen unverfaelscht durchkommen - nur eben zwei Sekunden
// spaeter. Genau diese kleine Verzoegerung ist der Preis.
//
// Ein erster Anlauf hielt stattdessen beim Standby-Wechsel den zuletzt gemeldeten Wert
// fest. Das machte es schlimmer: Beim Umschalten des Merkers lag die gestoerte Probe
// bereits an, gehalten wurde also ausgerechnet die Delle - und zwar sekundenlang.
// Die angezeigten Zahlen bleiben unangetastet - was gemessen wird, steht dort weiter drin.
#define KURVEN_MEDIAN 5
static float g_medW[KURVEN_MEDIAN];
static float g_medD[KURVEN_MEDIAN];
static uint8_t g_medPos = 0;
static uint8_t g_medFill = 0;
// Mittlerer Wert von hoechstens fuenf Proben. Bei so wenigen Werten ist Sortieren durch
// Einfuegen das Einfachste, was die Aufgabe loest.
static float median_von(const float* quelle, uint8_t anzahl) {
float f[KURVEN_MEDIAN];
for (uint8_t i = 0; i < anzahl; i++) {
float v = quelle[i];
int8_t j = (int8_t)i - 1;
while (j >= 0 && f[j] > v) { f[j + 1] = f[j]; j--; }
f[j + 1] = v;
}
return f[anzahl / 2];
}
// 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);
int32_t mn = INT32_MAX, mx = INT32_MIN;
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];
}
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;
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 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.
// 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);
// Abfragen laesst sich der gesetzte Ausschnitt bei LVGL nicht, also wird er gemerkt.
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);
}
merk[0] = mn; merk[1] = mx;
lv_chart_set_range(ch, LV_CHART_AXIS_PRIMARY_Y, mn, mx);
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
@@ -1174,13 +1292,30 @@ 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.
(void)soll;
lv_chart_set_all_value(ch, *ser, (int32_t)(jetzt * 10.0f));
spark_range_fit(ch, *ser);
return ch;
return ch; // Ausschnitt setzt spark_ranges_fit(), sobald beide Kurven stehen
}
// Temperaturbereich gemaess aktiver Darstellung (neu) aufbauen - auch beim Umschalten
@@ -1193,6 +1328,9 @@ static void build_temp_row_content() {
bulbTempW = bulbTempD = nullptr;
g_sparkW = g_sparkD = nullptr;
g_sparkSerW = g_sparkSerD = nullptr;
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);
@@ -1217,10 +1355,15 @@ 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;
}
@@ -1719,17 +1862,6 @@ static void chart_duty_btn_cb(lv_event_t*) {
else lv_obj_add_flag(g_legDuty, LV_OBJ_FLAG_HIDDEN);
}
chart_redraw();
// Verlaufskurven des Dashboards (Darstellung "Verlauf") mitfuehren. Der Ausschnitt
// richtet sich nach den vorhandenen Werten, siehe spark_range_fit().
if (g_sparkSerW && g_sparkW) {
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) {
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) {
lv_obj_clear_flag(p, LV_OBJ_FLAG_SCROLLABLE);
@@ -1954,11 +2086,33 @@ static void chart_redraw() {
lv_chart_refresh(g_chart);
}
static void chart_sample_cb(lv_timer_t*) {
// Verlaufskurven des Dashboards (Darstellung "Verlauf") fortschreiben. Das stand
// frueher am Ende von chart_duty_btn_cb - also im Callback eines Knopfes auf der
// Verlauf-SEITE. Die Kurven wurden dadurch nur fortgeschrieben, wenn jemand genau
// diesen Knopf drueckte; sonst zeigten sie unveraendert den Wert, der beim Aufbau
// der Darstellung gerade anlag. Beide Linien lagen deshalb flach und identisch.
//
// Bewusst vor dem Ausstieg unten: Die Kurven im Dashboard haengen nicht daran, ob
// die Verlauf-Seite mit ihrem grossen Diagramm schon aufgebaut ist.
// Frische Probe einreihen, in die Kurven geht der mittlere der letzten fuenf Werte.
g_medW[g_medPos] = g_state.tempW;
g_medD[g_medPos] = g_state.tempD;
g_medPos = (uint8_t)((g_medPos + 1) % KURVEN_MEDIAN);
if (g_medFill < KURVEN_MEDIAN) g_medFill++;
const float probeW = median_von(g_medW, g_medFill);
const float probeD = median_von(g_medD, g_medFill);
if (g_sparkSerW && g_sparkW)
lv_chart_set_next_value(g_sparkW, g_sparkSerW, (int32_t)(probeW * 10.0f));
if (g_sparkSerD && g_sparkD)
lv_chart_set_next_value(g_sparkD, g_sparkSerD, (int32_t)(probeD * 10.0f));
spark_ranges_fit(); // gemeinsame Skala, damit die Hoehen vergleichbar bleiben
if (!g_chart || !g_hist) return;
// 1-s-Probe in die 60-min-Historie (Ringpuffer); Anzeige zeichnet chart_redraw
bool caseOn = g_state.caseSensorEnabled && g_state.hasCaseTemp;
g_hist->w[g_histPos] = (int16_t)(g_state.tempW * 10.0f + 0.5f);
g_hist->d[g_histPos] = (int16_t)(g_state.tempD * 10.0f + 0.5f);
g_hist->w[g_histPos] = (int16_t)(probeW * 10.0f + 0.5f);
g_hist->d[g_histPos] = (int16_t)(probeD * 10.0f + 0.5f);
g_hist->c[g_histPos] = caseOn ? (int16_t)(g_state.caseTemp * 10.0f + 0.5f) : CHART_HIST_NONE;
float dw = g_state.dutyW, dd = g_state.dutyD;
g_hist->dutyW[g_histPos] = (uint8_t)(dw < 0.0f ? 0 : (dw > 100.0f ? 100 : (int)(dw + 0.5f)));