Compare commits

...
Author SHA1 Message Date
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
3 changed files with 105 additions and 15 deletions
+19
View File
@@ -1,3 +1,22 @@
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
+1 -1
View File
@@ -126,7 +126,7 @@
// -------------------------------------------------------------------------------------
// Firmware
// -------------------------------------------------------------------------------------
#define DISPLAY_FW_VERSION "1.11.0" // Firmware-Stand der P4-Display-Steuerung (Info-Seite)
#define DISPLAY_FW_VERSION "1.11.2" // Firmware-Stand der P4-Display-Steuerung (Info-Seite)
// -------------------------------------------------------------------------------------
// Protokoll
+85 -14
View File
@@ -178,14 +178,20 @@ 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 12
#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;
@@ -193,6 +199,8 @@ 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
@@ -1196,6 +1204,8 @@ static bool spark_anker(lv_obj_t* ch, lv_chart_series_t* ser, int32_t* anker, in
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) {
@@ -1210,14 +1220,23 @@ static void spark_dot_place(lv_obj_t* ch, lv_chart_series_t* ser, lv_obj_t* dot,
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;
lv_obj_align(dot, LV_ALIGN_TOP_LEFT, w - r, y - r); // Mitte auf den letzten Punkt
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);
@@ -1251,11 +1270,24 @@ static void spark_ranges_fit() {
}
}
// Weiches Pulsieren des Punktes: Nur der Schein atmet, der Punkt selbst bleibt ruhig.
static void spark_dot_puls_cb(void* var, int32_t v) {
// 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;
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
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);
@@ -1289,6 +1321,13 @@ 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);
@@ -1301,8 +1340,9 @@ static lv_obj_t* tempSpark(lv_obj_t* parent, lv_color_t col, lv_chart_series_t**
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_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
@@ -2085,6 +2125,36 @@ 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
@@ -2102,11 +2172,11 @@ static void chart_sample_cb(lv_timer_t*) {
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
// 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
@@ -4204,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();