Compare commits

...
Author SHA1 Message Date
thomas 6964e65002 Merge pull request 'fix(P4): Punkt sitzt wieder auf dem letzten Messwert (1.11.3)' (#90) from fix/punkt-auf-letztem-messwert into main 2026-09-04 12:51:17 +02:00
raw-designsandClaude Opus 5 8933c6f115 fix(P4): Punkt sitzt wieder auf dem letzten Messwert (1.11.3)
Um den Beschnitt an der rechten Kante zu vermeiden, war der Punkt um eine
halbe Punktbreite nach innen gerueckt - die Linie schaute dadurch rechts
unter ihm hervor.

Statt den Punkt zu verschieben, laesst die Kurvenflaeche jetzt rechts
einen schmalen Rand frei. Der Platz gehoert noch zum Diagramm, wird aber
nicht mehr bezeichnet: Die Linie endet frueher, der Punkt sitzt genau auf
ihrem letzten Wert und wird nicht abgeschnitten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wx7wu7uXXgRzp1zHUhtUa6
2026-09-04 12:50:56 +02:00
thomas ddfd31dddf Merge pull request 'feat(P4): Signalpunkt am Kurvenende, kleiner und ohne Beschnitt (1.11.2)' (#89) from feat/signalpunkt-am-kurvenende into main 2026-09-04 12:21:56 +02:00
raw-designsandClaude Opus 5 1959c49d9c feat(P4): Signalpunkt am Kurvenende, kleiner und ohne Beschnitt (1.11.2)
Der Punkt sass mittig auf dem letzten Messwert und damit auf der rechten
Kante der Kurvenflaeche - seine rechte Haelfte lag ausserhalb und wurde
abgeschnitten. Er sitzt jetzt vollstaendig innerhalb; der Schein darf
weiterhin darueber hinausreichen.

Der Punkt ist auf etwa zwei Drittel verkleinert. Das Pulsieren ist neu
choreografiert: Der Punkt selbst atmet leicht, der Schein macht das
Gegenteil - zieht der Punkt sich zusammen, geht der Schein auf.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wx7wu7uXXgRzp1zHUhtUa6
2026-09-04 12:21:36 +02:00
thomas 1d5d16f998 Merge pull request 'fix(P4): Punkt am Kurvenende sichtbar, weiche Linienführung (1.11.1)' (#88) from fix/kurvenpunkt-und-weiche-linien into main 2026-09-04 12:06:46 +02:00
raw-designsandClaude Opus 5 a4ed0197e7 fix(P4): Punkt am Kurvenende sichtbar, weiche Linienfuehrung (1.11.1)
Das Diagramm beschnitt seine Kinder an der eigenen Kante: Der Punkt am
rechten Ende war angeschnitten, und vom Schein war nichts zu sehen -
damit blieb auch das Pulsieren unsichtbar. Punkt und Schein duerfen jetzt
darueber hinausreichen; der Schein ist kraeftiger, der Punkt etwas
groesser.

Die Kurven verlaufen jetzt weich statt kantig. Gemessen wird weiterhin
einmal je Sekunde, gezeichnet viermal so fein: Zwischen zwei Messwerten
liegen vier Stuetzpunkte auf einem Kosinusbogen, der an den Messpunkten
waagerecht beginnt und endet. Linienenden und -ecken sind abgerundet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wx7wu7uXXgRzp1zHUhtUa6
2026-09-04 12:06:24 +02:00
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
3 changed files with 303 additions and 63 deletions
+48
View File
@@ -1,3 +1,51 @@
Version 1.11.3:
- Der Punkt sitzt wieder genau auf dem letzten Messwert, statt eine halbe Punktbreite davor.
Die Linie schaut damit rechts nicht mehr unter ihm hervor. Damit trotzdem nichts
abgeschnitten wird, lässt die Kurvenfläche rechts einen schmalen Rand frei: Der Platz
gehört noch zum Diagramm, wird aber nicht mehr bezeichnet.
Version 1.11.2:
- Der Punkt am Kurvenende sitzt jetzt vollständig innerhalb der Kurvenfläche, statt mittig
auf dem letzten Messwert und damit halb außerhalb. Rechts wird nichts mehr abgeschnitten.
Der Schein darf weiterhin darüber hinausreichen.
- Der Punkt ist auf etwa zwei Drittel verkleinert.
- Neues Pulsieren als Signalpunkt: Der Punkt selbst atmet leicht, und der Schein macht das
Gegenteil. Zieht der Punkt sich zusammen, geht der Schein auf; wird der Punkt wieder
größer, zieht der Schein sich zurück.
Version 1.11.1:
- Der Punkt am rechten Ende der Verlaufskurven war angeschnitten, und von seinem Schein war
nichts zu sehen: Das Diagramm beschnitt beides an seiner eigenen Kante. Punkt und Schein
dürfen jetzt darüber hinausreichen, das Pulsieren ist damit sichtbar. Der Schein wurde
zusätzlich kräftiger und der Punkt eine Spur größer.
- Die Linien verlaufen jetzt weich statt kantig. Gemessen wird weiterhin einmal je Sekunde,
gezeichnet wird viermal so fein: Zwischen zwei Messwerten liegen vier Stützpunkte auf
einem Kosinusbogen, der an den Messpunkten waagerecht beginnt und endet. Dadurch
entstehen dort keine Knicke mehr. Linienenden und -ecken sind zusätzlich abgerundet.
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: Version 1.10.3:
- Direkt nach dem Aufwachen aus dem Standby zog die Wasserkurve eine kurze Delle nach - 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 unten. Ursache ist die Messung, nicht der Kessel: In genau diesem Moment schalten die
+1 -1
View File
@@ -126,7 +126,7 @@
// ------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------
// Firmware // Firmware
// ------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------
#define DISPLAY_FW_VERSION "1.10.3" // Firmware-Stand der P4-Display-Steuerung (Info-Seite) #define DISPLAY_FW_VERSION "1.11.3" // Firmware-Stand der P4-Display-Steuerung (Info-Seite)
// ------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------
// Protokoll // Protokoll
+254 -62
View File
@@ -178,14 +178,29 @@ static void label_set_if_changed(lv_obj_t* lbl, const char* txt); // Definitio
enum TempDesign { TD_GEWICHTET = 0, TD_VERLAUF, TD_FLAECHEN, TD_BAND, TD_DESIGN_COUNT }; enum TempDesign { TD_GEWICHTET = 0, TD_VERLAUF, TD_FLAECHEN, TD_BAND, TD_DESIGN_COUNT };
static uint8_t g_tempDesign = TD_BAND; static uint8_t g_tempDesign = TD_BAND;
static bool g_tempColored = false; // Temperaturzahl in der Zustandsfarbe statt weiss static bool g_tempColored = false; // Temperaturzahl in der Zustandsfarbe statt weiss
#define DASH_SPARK_POINTS 60 // eine Minute Verlauf, ein Punkt je Sekunde // Eine Minute Verlauf. Gemessen wird einmal je Sekunde, gezeichnet wird viermal so fein:
// Zwischen zwei Messwerten liegen vier Stuetzpunkte auf einem Kosinusbogen. Aus dem
// Streckenzug mit harten Ecken wird damit eine weich geschwungene Linie.
#define SPARK_TEILUNG 4
#define DASH_SPARK_STEP_MS 250
#define DASH_SPARK_POINTS (60 * SPARK_TEILUNG)
// Kleinster Ausschnitt der Verlaufskurve in Zehntelgrad. Ohne Untergrenze blaeht ein // Kleinster Ausschnitt der Verlaufskurve in Zehntelgrad. Ohne Untergrenze blaeht ein
// ruhig geregelter Kessel jedes Zehntelgrad zum vollen Ausschlag auf. // ruhig geregelter Kessel jedes Zehntelgrad zum vollen Ausschlag auf.
#define DASH_SPARK_MIN_SPAN 40 // 4,0 Grad #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 10 // groesster Durchmesser des Punktes
#define SPARK_DOT_MIN 7 // kleinster - dazwischen atmet er
#define SPARK_DOT_PULS_MS 1100
static lv_obj_t* g_sparkW = nullptr; // Verlaufskurven der Darstellung TD_VERLAUF static lv_obj_t* g_sparkW = nullptr; // Verlaufskurven der Darstellung TD_VERLAUF
static lv_obj_t* g_sparkD = nullptr; static lv_obj_t* g_sparkD = nullptr;
static lv_chart_series_t* g_sparkSerW = nullptr; static lv_chart_series_t* g_sparkSerW = nullptr;
static lv_chart_series_t* g_sparkSerD = 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 int32_t g_dotCxW = 0, g_dotCyW = 0; // Mitte des Punktes in der Kurvenflaeche
static int32_t g_dotCxD = 0, g_dotCyD = 0;
static lv_obj_t* g_readyBand = nullptr; // Zustandsband der Darstellung TD_BAND static lv_obj_t* g_readyBand = nullptr; // Zustandsband der Darstellung TD_BAND
static lv_obj_t* g_readyBandLbl = nullptr; static lv_obj_t* g_readyBandLbl = nullptr;
static lv_obj_t* g_tempRow = nullptr; // Container; Inhalt wird je Design (neu) aufgebaut static lv_obj_t* g_tempRow = nullptr; // Container; Inhalt wird je Design (neu) aufgebaut
@@ -1119,58 +1134,176 @@ 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 // 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 // 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. // 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_sparkMinW = INT32_MIN; // zuletzt gesetzter Ausschnitt je Kurve
static int32_t g_sparkMax = INT32_MIN; 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 // 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 // ab. In genau diesem Moment schalten die Heizungen, und der Wassersensor ist ein NTC am
// ADC - ein Kessel kann binnen einer Sekunde nicht um Grade einbrechen, die Delle ist also // ADC, einmal je Regelzyklus abgetastet - ein Kessel kann binnen einer Sekunde nicht um
// eine Stoerung der Messung, kein Vorgang an der Maschine. Fuer die Dauer des Uebergangs // Grade einbrechen, die Delle ist also eine Stoerung der Messung, kein Vorgang an der
// schreiben die Verlaufskurven deshalb den letzten Wert von vor dem Wechsel weiter. Die // Maschine.
// angezeigten Zahlen bleiben unangetastet - was gemessen wird, steht dort weiter drin. //
#define KURVEN_SPERRE_MS 6000 // In die Kurven geht deshalb nicht die frische Probe, sondern der mittlere Wert der
static uint32_t g_kurvenSperreBis = 0; // millis(); bis dahin Werte halten // letzten fuenf (Median). Eine oder zwei verirrte Proben verschwinden damit vollstaendig,
static float g_haltW = 0.0f; // waehrend echte Temperaturaenderungen unverfaelscht durchkommen - nur eben zwei Sekunden
static float g_haltD = 0.0f; // 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;
static void spark_scan(lv_obj_t* ch, lv_chart_series_t* ser, int32_t* mn, int32_t* mx) { // Mittlerer Wert von hoechstens fuenf Proben. Bei so wenigen Werten ist Sortieren durch
if (!ch || !ser) return; // Einfuegen das Einfachste, was die Aufgabe loest.
int32_t* pts = lv_chart_get_y_array(ch, ser); static float median_von(const float* quelle, uint8_t anzahl) {
if (!pts) return; float f[KURVEN_MEDIAN];
uint32_t n = lv_chart_get_point_count(ch); for (uint8_t i = 0; i < anzahl; i++) {
for (uint32_t i = 0; i < n; i++) { float v = quelle[i];
if (pts[i] == LV_CHART_POINT_NONE) continue; int8_t j = (int8_t)i - 1;
if (pts[i] < *mn) *mn = pts[i]; while (j >= 0 && f[j] > v) { f[j + 1] = f[j]; j--; }
if (pts[i] > *mx) *mx = pts[i]; f[j + 1] = v;
} }
return f[anzahl / 2];
} }
static void spark_ranges_fit() { // Ausschnitt einer Kurve: Der aelteste Punkt - links am Rand - liegt genau auf halber
int32_t mn = INT32_MAX, mx = INT32_MIN; // Hoehe, und die Kurve waechst von dort nach oben oder unten. Das ist der ruhige Eindruck,
spark_scan(g_sparkW, g_sparkSerW, &mn, &mx); // den ein Verlauf haben soll: Er beginnt immer an derselben Stelle, egal bei welcher
spark_scan(g_sparkD, g_sparkSerD, &mn, &mx); // Temperatur der Kessel gerade steht.
if (mn > mx) return; // noch kein einziger Wert //
if (mx - mn < DASH_SPARK_MIN_SPAN) { // beide dicht beieinander: mittig halten // Beide Kurven bekommen denselben Massstab (gleiche Grad je Bildpunkt). Die absolute Hoehe
int32_t mitte = (mn + mx) / 2; // laesst sich damit zwar nicht mehr vergleichen - dafuer aber der Ausschlag: Eine Kurve,
mn = mitte - DASH_SPARK_MIN_SPAN / 2; // die doppelt so weit ausschlaegt, hat sich auch doppelt so stark veraendert.
mx = mitte + DASH_SPARK_MIN_SPAN / 2; //
} else { // sonst etwas Luft nach oben und unten // Rueckgabe: falsch, wenn die Kurve noch keinen brauchbaren Wert hat.
int32_t luft = (mx - mn) / 8; static bool spark_anker(lv_obj_t* ch, lv_chart_series_t* ser, int32_t* anker, int32_t* abweichung) {
mn -= luft; if (!ch || !ser) return false;
mx += luft; int32_t* pts = lv_chart_get_y_array(ch, ser);
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;
int32_t d = pts[i] - a;
if (d < 0) d = -d;
if (d > weit) weit = d;
} }
*anker = a;
*abweichung = weit;
return true;
}
static void spark_dot_apply(lv_obj_t* d, int32_t groesse); // weiter unten
// 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;
// Die Mitte des Punktes sitzt genau auf dem letzten Messwert, also am rechten Rand der
// Kurvenflaeche. Damit seine rechte Haelfte trotzdem nicht abgeschnitten wird, laesst
// das Diagramm rechts einen Rand frei (siehe tempSpark) - dort steht der Punkt, ohne
// dass die Linie unter ihm hervorschaut.
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;
if (dot == g_sparkDotW) { g_dotCxW = w; g_dotCyW = y; }
else { g_dotCxD = w; g_dotCyD = y; }
spark_dot_apply(dot, lv_obj_get_width(dot));
lv_obj_remove_flag(dot, LV_OBJ_FLAG_HIDDEN);
}
// Gefilterte Sekundenprobe, vom Abtaster gesetzt und vom Schritt-Timer abgeholt.
static float g_probeW = 0.0f, g_probeD = 0.0f;
static bool g_probeGueltig = false;
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 // 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. // jeder Zustandsmeldung ungueltig, auch wenn sich der Ausschnitt gar nicht ruehrt.
// Abfragen laesst sich der gesetzte Ausschnitt bei LVGL nicht, also wird er gemerkt. // Abfragen laesst sich der gesetzte Ausschnitt bei LVGL nicht, also wird er gemerkt.
if (mn == g_sparkMin && mx == g_sparkMax) return; if (okW) {
g_sparkMin = mn; int32_t mn = aW - halb, mx = aW + halb;
g_sparkMax = mx; if (mn != g_sparkMinW || mx != g_sparkMaxW) {
if (g_sparkW) lv_chart_set_range(g_sparkW, LV_CHART_AXIS_PRIMARY_Y, mn, mx); g_sparkMinW = mn; g_sparkMaxW = mx;
if (g_sparkD) lv_chart_set_range(g_sparkD, LV_CHART_AXIS_PRIMARY_Y, mn, 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);
}
}
// Punkt in seiner aktuellen Groesse an seine Stelle setzen. Die Mitte ist gemerkt, denn
// beim Atmen aendert sich die Groesse - ohne Nachfuehren wanderte der Punkt davon.
static void spark_dot_apply(lv_obj_t* d, int32_t groesse) {
if (!d) return;
const int32_t cx = (d == g_sparkDotW) ? g_dotCxW : g_dotCxD;
const int32_t cy = (d == g_sparkDotW) ? g_dotCyW : g_dotCyD;
lv_obj_set_size(d, groesse, groesse);
lv_obj_align(d, LV_ALIGN_TOP_LEFT, cx - groesse / 2, cy - groesse / 2);
}
// Signalpunkt: Der Punkt selbst atmet leicht, und der Schein macht genau das Gegenteil.
// Zieht der Punkt sich zusammen, geht der Schein auf; wird der Punkt wieder gross, zieht
// der Schein sich zurueck. Das wirkt wie ein Signal, das ausgesendet wird.
static void spark_dot_puls_cb(void* var, int32_t v) { // v laeuft 0 .. 100
lv_obj_t* d = (lv_obj_t*)var;
spark_dot_apply(d, SPARK_DOT_MIN + ((SPARK_DOT_SIZE - SPARK_DOT_MIN) * v) / 100);
lv_obj_set_style_shadow_width(d, 30 - (22 * v) / 100, 0); // 30 .. 8
lv_obj_set_style_shadow_opa(d, (lv_opa_t)(200 - (150 * v) / 100), 0); // 200 .. 50
}
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, 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); lv_obj_t* ch = lv_chart_create(parent);
// Die Kurve nimmt den Platz unterhalb der Zahlen ein, statt als schmaler Streifen in // 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 // der Mitte zu haengen. Sie beginnt damit weiter unten auf der Karte und hat deutlich
@@ -1187,11 +1320,41 @@ static lv_obj_t* tempSpark(lv_obj_t* parent, lv_color_t col, lv_chart_series_t**
lv_obj_set_style_bg_opa(ch, LV_OPA_TRANSP, 0); lv_obj_set_style_bg_opa(ch, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(ch, 0, 0); lv_obj_set_style_border_width(ch, 0, 0);
lv_obj_set_style_pad_all(ch, 4, 0); lv_obj_set_style_pad_all(ch, 4, 0);
// Rechts etwas mehr Rand: Dort steht der Punkt auf dem letzten Messwert. Der Platz
// gehoert noch zum Diagramm, wird aber nicht mehr bezeichnet - deshalb ragt weder die
// Linie unter dem Punkt hervor noch wird der Punkt an der Kante beschnitten.
lv_obj_set_style_pad_right(ch, SPARK_DOT_SIZE / 2 + 3, 0);
lv_obj_set_style_size(ch, 0, 0, LV_PART_INDICATOR); // keine Punktmarkierungen lv_obj_set_style_size(ch, 0, 0, LV_PART_INDICATOR); // keine Punktmarkierungen
lv_obj_set_style_line_width(ch, 3, LV_PART_ITEMS); lv_obj_set_style_line_width(ch, 3, LV_PART_ITEMS);
// Runde Enden und Ecken: An den Stuetzpunkten sitzt damit kein spitzer Winkel.
lv_obj_set_style_line_rounded(ch, true, LV_PART_ITEMS);
// Der Punkt am juengsten Messwert sitzt am rechten Rand, sein Schein reicht darueber
// hinaus. Ohne diese Freigabe schneidet das Diagramm beides ab - der Punkt war
// angeschnitten, und vom Schein war ueberhaupt nichts zu sehen.
lv_obj_add_flag(ch, LV_OBJ_FLAG_OVERFLOW_VISIBLE);
lv_obj_add_flag(parent, LV_OBJ_FLAG_OVERFLOW_VISIBLE); // dasselbe fuer die Karte
lv_obj_clear_flag(ch, LV_OBJ_FLAG_SCROLLABLE); lv_obj_clear_flag(ch, LV_OBJ_FLAG_SCROLLABLE);
*ser = lv_chart_add_series(ch, col, LV_CHART_AXIS_PRIMARY_Y); *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, 18, 0);
lv_obj_set_style_shadow_opa(*dot, 130, 0);
lv_obj_set_style_shadow_spread(*dot, 1, 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 // Startwert sofort setzen. Ein frisch angelegtes Diagramm hat lauter "kein Wert"-Punkte
// und zeichnet deshalb nichts - beim Umschalten der Darstellung saehe man bis zur // und zeichnet deshalb nichts - beim Umschalten der Darstellung saehe man bis zur
// ersten Sekundenprobe eine leere Flaeche. // ersten Sekundenprobe eine leere Flaeche.
@@ -1210,7 +1373,9 @@ static void build_temp_row_content() {
bulbTempW = bulbTempD = nullptr; bulbTempW = bulbTempD = nullptr;
g_sparkW = g_sparkD = nullptr; g_sparkW = g_sparkD = nullptr;
g_sparkSerW = g_sparkSerD = 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; g_readyBand = g_readyBandLbl = nullptr;
const lv_color_t teal = lv_color_hex(0x80cbc4); const lv_color_t teal = lv_color_hex(0x80cbc4);
@@ -1235,10 +1400,14 @@ static void build_temp_row_content() {
case TD_VERLAUF: { case TD_VERLAUF: {
lv_obj_t* cw = tempField(g_tempRow, "WASSER", COL_ACCENT, &lblTempW, &lblSetW, &barDutyW, lv_obj_t* cw = tempField(g_tempRow, "WASSER", COL_ACCENT, &lblTempW, &lblSetW, &barDutyW,
1, &lv_font_maven_pro_bold_96, false, 0); 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, lv_obj_t* cd = tempField(g_tempRow, "DAMPF", teal, &lblTempD, &lblSetD, &barDutyD,
1, &lv_font_maven_pro_bold_96, false, 0); 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(); spark_ranges_fit();
break; break;
} }
@@ -1961,6 +2130,36 @@ static void chart_redraw() {
} }
lv_chart_refresh(g_chart); lv_chart_refresh(g_chart);
} }
// Vier Stuetzpunkte je Messwert. Der Weg von der vorigen zur neuen Probe wird nicht
// geradlinig genommen, sondern auf einem Kosinusbogen: Er beginnt und endet waagerecht,
// weshalb an den Messpunkten kein Knick entsteht. Genau das macht den runden Eindruck.
static void spark_step_cb(lv_timer_t*) {
if (!g_probeGueltig) return;
if (!g_sparkW && !g_sparkD) return; // andere Darstellung: nichts zu tun
static float vonW = 0.0f, zielW = 0.0f, vonD = 0.0f, zielD = 0.0f;
static bool beginn = true;
static uint8_t schritt = SPARK_TEILUNG;
if (beginn) { vonW = zielW = g_probeW; vonD = zielD = g_probeD; beginn = false; }
if (schritt >= SPARK_TEILUNG) { // neue Probe uebernehmen
vonW = zielW; zielW = g_probeW;
vonD = zielD; zielD = g_probeD;
schritt = 0;
}
schritt++;
const float t = (float)schritt / (float)SPARK_TEILUNG;
const float f = 0.5f - 0.5f * cosf(3.14159265f * t);
if (g_sparkSerW && g_sparkW)
lv_chart_set_next_value(g_sparkW, g_sparkSerW,
(int32_t)((vonW + (zielW - vonW) * f) * 10.0f));
if (g_sparkSerD && g_sparkD)
lv_chart_set_next_value(g_sparkD, g_sparkSerD,
(int32_t)((vonD + (zielD - vonD) * f) * 10.0f));
spark_ranges_fit(); // gemeinsamer Massstab, Punkt am juengsten Wert nachfuehren
}
static void chart_sample_cb(lv_timer_t*) { static void chart_sample_cb(lv_timer_t*) {
// Verlaufskurven des Dashboards (Darstellung "Verlauf") fortschreiben. Das stand // Verlaufskurven des Dashboards (Darstellung "Verlauf") fortschreiben. Das stand
// frueher am Ende von chart_duty_btn_cb - also im Callback eines Knopfes auf der // frueher am Ende von chart_duty_btn_cb - also im Callback eines Knopfes auf der
@@ -1970,17 +2169,19 @@ static void chart_sample_cb(lv_timer_t*) {
// //
// Bewusst vor dem Ausstieg unten: Die Kurven im Dashboard haengen nicht daran, ob // Bewusst vor dem Ausstieg unten: Die Kurven im Dashboard haengen nicht daran, ob
// die Verlauf-Seite mit ihrem grossen Diagramm schon aufgebaut ist. // die Verlauf-Seite mit ihrem grossen Diagramm schon aufgebaut ist.
// Sperrfrist nach einem Standby-Wechsel: letzten Wert halten statt der frischen Probe. // Frische Probe einreihen, in die Kurven geht der mittlere der letzten fuenf Werte.
bool sperre = g_kurvenSperreBis != 0 && (int32_t)(millis() - g_kurvenSperreBis) < 0; g_medW[g_medPos] = g_state.tempW;
if (!sperre) { g_haltW = g_state.tempW; g_haltD = g_state.tempD; } g_medD[g_medPos] = g_state.tempD;
const float probeW = sperre ? g_haltW : g_state.tempW; g_medPos = (uint8_t)((g_medPos + 1) % KURVEN_MEDIAN);
const float probeD = sperre ? g_haltD : g_state.tempD; 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) // In die Kurven geschrieben wird nicht hier, sondern viermal je Sekunde im
lv_chart_set_next_value(g_sparkW, g_sparkSerW, (int32_t)(probeW * 10.0f)); // Schritt-Timer - dazwischen liegen die Stuetzpunkte des Kosinusbogens.
if (g_sparkSerD && g_sparkD) g_probeW = probeW;
lv_chart_set_next_value(g_sparkD, g_sparkSerD, (int32_t)(probeD * 10.0f)); g_probeD = probeD;
spark_ranges_fit(); // gemeinsame Skala, damit die Hoehen vergleichbar bleiben g_probeGueltig = true;
if (!g_chart || !g_hist) return; if (!g_chart || !g_hist) return;
// 1-s-Probe in die 60-min-Historie (Ringpuffer); Anzeige zeichnet chart_redraw // 1-s-Probe in die 60-min-Historie (Ringpuffer); Anzeige zeichnet chart_redraw
@@ -4078,6 +4279,7 @@ void ui_init(ProtocolClient* client) {
lv_timer_create(dash_timers_cb, 100, nullptr); // Timer-Anzeige fortlaufend aktualisieren lv_timer_create(dash_timers_cb, 100, nullptr); // Timer-Anzeige fortlaufend aktualisieren
lv_timer_create(eco_case_timer_cb, 5000, nullptr); // Eco/Gehaeuse alle 5 s wechseln lv_timer_create(eco_case_timer_cb, 5000, nullptr); // Eco/Gehaeuse alle 5 s wechseln
lv_timer_create(chart_sample_cb, 1000, nullptr); // Verlauf jede Sekunde abtasten lv_timer_create(chart_sample_cb, 1000, nullptr); // Verlauf jede Sekunde abtasten
lv_timer_create(spark_step_cb, DASH_SPARK_STEP_MS, nullptr); // Kurven weich fortschreiben
show_page(PG_DASH); show_page(PG_DASH);
if (g_client) g_client->sendListProfiles(); if (g_client) g_client->sendListProfiles();
@@ -4275,16 +4477,6 @@ void ui_update(const MachineState& st) {
// Standby-Uhr-Overlay (Backlight wird in onState gesteuert). Uebergaenge werden weich // Standby-Uhr-Overlay (Backlight wird in onState gesteuert). Uebergaenge werden weich
// gefadet: Einschlafen blendet die Uhr ein, Aufwecken blendet sie aus. Nur bei echtem // gefadet: Einschlafen blendet die Uhr ein, Aufwecken blendet sie aus. Nur bei echtem
// Statuswechsel animieren (g_standbyShown), sonst nur Uhrzeit/Vordergrund aktualisieren. // Statuswechsel animieren (g_standbyShown), sonst nur Uhrzeit/Vordergrund aktualisieren.
// Standby-Wechsel merken: die Verlaufskurven halten danach kurz ihren letzten Wert,
// damit die Messstoerung beim Schalten der Heizungen keine Delle in die Linie zieht.
static bool letzterStandby = false;
if (st.standbyActive != letzterStandby) {
letzterStandby = st.standbyActive;
g_haltW = st.tempW;
g_haltD = st.tempD;
g_kurvenSperreBis = millis() + KURVEN_SPERRE_MS;
}
if (g_standbyScreen) { if (g_standbyScreen) {
bool shouldShow = st.standbyActive && st.standbyShowClock; bool shouldShow = st.standbyActive && st.standbyShowClock;
if (shouldShow) { if (shouldShow) {