Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddfd31dddf | ||
|
|
1959c49d9c | ||
|
|
1d5d16f998 | ||
|
|
a4ed0197e7 | ||
|
|
7296a0d2f6 | ||
|
|
c5ea697f24 | ||
|
|
5c64892c01 | ||
|
|
1febc0b092 | ||
|
|
a84f902f75 | ||
|
|
bcbeea2a17 | ||
|
|
1fe96b70aa | ||
|
|
43517a2c48 | ||
|
|
6a867e54fd |
@@ -1,3 +1,68 @@
|
||||
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:
|
||||
- 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
|
||||
|
||||
@@ -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.2" // Firmware-Stand der P4-Display-Steuerung (Info-Seite)
|
||||
|
||||
// -------------------------------------------------------------------------------------
|
||||
// Protokoll
|
||||
|
||||
+264
-44
@@ -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 };
|
||||
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
|
||||
// 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
|
||||
// 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 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_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 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_readyBandLbl = nullptr;
|
||||
static lv_obj_t* g_tempRow = nullptr; // Container; Inhalt wird je Design (neu) aufgebaut
|
||||
@@ -1119,47 +1134,175 @@ 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;
|
||||
|
||||
static void spark_scan(lv_obj_t* ch, lv_chart_series_t* ser, int32_t* mn, int32_t* mx) {
|
||||
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);
|
||||
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];
|
||||
// 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];
|
||||
}
|
||||
|
||||
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;
|
||||
// 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 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;
|
||||
// Der Punkt bleibt vollstaendig innerhalb der Kurvenflaeche. Saesse seine Mitte genau
|
||||
// auf dem letzten Messwert - also auf der rechten Kante -, laege seine rechte Haelfte
|
||||
// ausserhalb und wuerde abgeschnitten. Der Schein darf weiterhin hinausreichen.
|
||||
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 - r; g_dotCyW = y; }
|
||||
else { g_dotCxD = w - r; 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
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
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
|
||||
@@ -1178,9 +1321,35 @@ static lv_obj_t* tempSpark(lv_obj_t* parent, lv_color_t col, lv_chart_series_t**
|
||||
lv_obj_set_style_pad_all(ch, 4, 0);
|
||||
lv_obj_set_style_size(ch, 0, 0, LV_PART_INDICATOR); // keine Punktmarkierungen
|
||||
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);
|
||||
*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
|
||||
// und zeichnet deshalb nichts - beim Umschalten der Darstellung saehe man bis zur
|
||||
// ersten Sekundenprobe eine leere Flaeche.
|
||||
@@ -1199,7 +1368,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);
|
||||
@@ -1224,10 +1395,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;
|
||||
}
|
||||
@@ -1727,14 +1902,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. Beide teilen sich
|
||||
// eine Skala, siehe spark_ranges_fit().
|
||||
if (g_sparkSerW && g_sparkW)
|
||||
lv_chart_set_next_value(g_sparkW, g_sparkSerW, (int32_t)(g_state.tempW * 10.0f));
|
||||
if (g_sparkSerD && g_sparkD)
|
||||
lv_chart_set_next_value(g_sparkD, g_sparkSerD, (int32_t)(g_state.tempD * 10.0f));
|
||||
spark_ranges_fit(); // gemeinsame Skala, damit die Hoehen vergleichbar bleiben
|
||||
}
|
||||
static void build_chart(lv_obj_t* p) {
|
||||
lv_obj_clear_flag(p, LV_OBJ_FLAG_SCROLLABLE);
|
||||
@@ -1958,12 +2125,64 @@ static void chart_redraw() {
|
||||
}
|
||||
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*) {
|
||||
// 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);
|
||||
|
||||
// In die Kurven geschrieben wird nicht hier, sondern viermal je Sekunde im
|
||||
// Schritt-Timer - dazwischen liegen die Stuetzpunkte des Kosinusbogens.
|
||||
g_probeW = probeW;
|
||||
g_probeD = probeD;
|
||||
g_probeGueltig = true;
|
||||
|
||||
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)));
|
||||
@@ -4055,6 +4274,7 @@ void ui_init(ProtocolClient* client) {
|
||||
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(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);
|
||||
if (g_client) g_client->sendListProfiles();
|
||||
|
||||
Reference in New Issue
Block a user