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
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
raw-designsandClaude Opus 5 cc9ae945b1 fix(P4): Verlaufskurven skalieren nach Daten, Diagnose ausgebaut (1.10.0)
Die Sparklines im Dashboard lagen flach auf gleicher Höhe und bewegten sich
nicht, obwohl ein Kessel bei 80 °C stand und der andere bei 0.

Ursache: lv_chart_set_range() war an den Sollwert gebunden (soll-60 bis
soll+30 in Zehntelgrad). Liegt die Temperatur unter diesem Fenster - beim
Aufheizen oder bei abgeschaltetem Kessel -, klemmt LVGL jeden Punkt auf den
unteren Rand. Beide Kurven landeten dadurch auf derselben Linie.

- Neues spark_range_fit(): Ausschnitt aus min/max der vorhandenen Punkte
  (lv_chart_get_y_array), Mindestspanne DASH_SPARK_MIN_SPAN (40 = 4,0 °C),
  sonst ein Achtel Luft nach oben und unten.
- Der zuletzt gesetzte Bereich wird in user_data gemerkt, weil LVGL keinen
  Getter dafür hat; ohne den Vergleich würde set_range das Diagramm bei jeder
  Zustandsmeldung invalidieren.
- Die Charts füllen jetzt den Platz unterhalb der Zahlen (flex_grow 1,
  min_height 90) statt als 64-px-Streifen mittig zu hängen.

Aufräumen nach der abgeschlossenen Fehlersuche:
- Herzschlag, [Zustand]- und [Touch]-Messzeilen entfernt.
- refr_monitor_install() hinter WS7_PERF_MONITOR (aus).
- scroll_ohne_nachlauf() gibt LV_OBJ_FLAG_SCROLL_MOMENTUM wieder frei.

Beide Panel-Varianten mit arduino-cli gegengebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019G95njDFkedFqXiz5sEaJ2
2026-09-03 17:01:19 +02:00
thomas 696707a9a4 Merge pull request 'fix(P4): Hintergrundbeleuchtung kommt sofort - Bridge braucht zwei Latches (1.9.12)' (#81) from fix/helligkeit-doppelte-uebernahme into main 2026-09-03 15:46:17 +02:00
raw-designsandClaude Opus 5 6bf046801c fix(P4): Hintergrundbeleuchtung kommt sofort - Bridge braucht zwei Latches (1.9.12)
Der Nutzer hat es im Gegenlicht gesehen: Das Bild steht nach 446 ms fertig
da, nur die Beleuchtung kommt Sekunden später. Die gesamte Suche im
Zeichenablauf lief damit am falschen Ort - die Messungen dort waren alle
unauffällig, weil dort auch nichts war.

Der Referenz-Sketch (dsi-test.ino, enableBacklight) setzt 0xAB/0xAA zweimal
mit delay(100) dazwischen, ausdrücklich "sicherheitshalber Helligkeit noch
einmal übernehmen". jc_backlight_set() schrieb die Sequenz nur einmal.

- 0xAB/0xAA werden jetzt wiederholt, Pause über WS7_BACKLIGHT_LATCH_MS (20).
- 0xAD (Freigabe) nur noch beim ersten Aufruf statt bei jeder Änderung; der
  Referenzcode setzt sie ebenfalls nur einmal, und sie erneut zu schreiben
  stößt die Helligkeitsstufe der Bridge neu an.
- jc_backlight_set() meldet seine Dauer selbst; die doppelte Messung im .ino
  entfällt.

Beide Panel-Varianten mit arduino-cli gegengebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 15:45:50 +02:00
thomas cd1617fdce Merge pull request 'fix(P4): Endlose Neustartschleife nach Kaltstart (1.9.11)' (#80) from fix/kaltstart-neustartschleife into main 2026-09-03 15:09:03 +02:00
raw-designsandClaude Opus 5 5df6c20171 fix(P4): Endlose Neustartschleife nach Kaltstart (1.9.11)
Log zeigt zweimal hintereinander "rst:0x1 (POWERON)" und
"[Panel] Startgrund: 1", danach nichts mehr - Beleuchtung an, kein Bild.

Ursache: Der gewollte einmalige Neustart nach Kaltstart (WS7_COLD_BOOT_RESTART)
prüfte nur esp_reset_reason() == ESP_RST_POWERON. Der Kommentar nahm an, der
zweite Durchlauf melde einen Software-Reset. Tatsächlich meldet der Chip auch
nach esp_restart() wieder POWERON, die Bedingung greift erneut - Endlosschleife.
Der bootguard konnte nicht eingreifen, weil derselbe Zweig "early" jedes Mal
auf 0 zurücksetzt.

- Zusätzliches NVS-Flag "kaltneu" in bootguard: wird vor esp_restart() gesetzt
  und beim zweiten Durchlauf erkannt und gelöscht. Unabhängig vom gemeldeten
  Startgrund, damit die Schleife strukturell ausgeschlossen ist.
- Die Meldung lief über DBG_SERIAL (USB-CDC) und war im Monitor an der UART
  nicht zu sehen - jetzt esp_rom_printf wie die übrigen Panel-Meldungen.

Beide Panel-Varianten mit arduino-cli gegengebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NeFMr439WcLMdebE3kU7fS
2026-09-03 15:08:40 +02:00
thomas 7d7893cf08 Merge pull request 'diag(P4): Touch-Rohwerte vermessen, einheitliche Zeitbasis (1.9.10)' (#79) from diag/touch-rohwerte-und-zeitbasis into main 2026-09-03 13:52:10 +02:00
raw-designsandClaude Opus 5 db707ffd8c diag(P4): Touch-Rohwerte vermessen, einheitliche Zeitbasis (1.9.10)
Auswertung des letzten Logs (lv_tick lief 2728 ms hinter millis):
Tipp erkannt 49428, Rückfrage aufgebaut 49430, Bildaufbau fertig 49746.
Also 318 ms vom erkannten Tipp bis zum fertigen Bild - die gemessene Kette
ist schnell. Der Knopfdruck im Dialog liegt bei 55638 (Bereich 645..856,
374..426), die Antwort des S3 bei 59303, also 937 ms Rundlauf.

Die Verzögerung liegt damit VOR der Erkennung. Deshalb:
- ws7_touch_get_xy() meldet den ersten verworfenen Rohwert nach Ruhe mit
  Koordinaten und zählt, wie viele bis zur akzeptierten Berührung anfallen.
- Alle Messzeilen auf millis() statt lv_tick_get(); die beiden Uhren
  auseinanderzurechnen hat das Lesen unnötig erschwert.

Beide Panel-Varianten mit arduino-cli gegengebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NeFMr439WcLMdebE3kU7fS
2026-09-03 13:51:48 +02:00
thomas 9eb386095d Merge pull request 'fix(P4): Kachelhöhe beim Bezug halten, Messzeilen mit Zeitmarke (1.9.9)' (#78) from fix/kachelhoehe-und-zeitmarken into main 2026-09-03 13:38:36 +02:00
raw-designsandClaude Opus 5 7fec5d9fb5 fix(P4): Kachelhöhe beim Bezug halten, Messzeilen mit Zeitmarke (1.9.9)
- dash_brew_mode() verteilt die flex_grow-Werte nicht mehr um. Die untere
  Zeile bekam während des Bezugs 3 statt 2 und die Temperaturzeile 1 statt 3;
  die kleinen Kacheln wurden dadurch enorm hoch und das Layout sprang.
- Die Messzeilen aus dem .ino liefen über Serial und damit über USB-CDC,
  während der Monitor an der UART hängt - sie waren nie zu sehen. Jetzt
  durchgängig esp_rom_printf wie die Panel-Meldungen.
- Alle Messzeilen tragen eine Zeitmarke in ms; ohne die sind die Lücken
  nicht zu erkennen.
- onState() misst Ankunft der Zustandsmeldung, Wartezeit auf hal_lock() und
  Dauer von ui_update(). standby_screen_cb() klammert wake_confirm_show()
  ein. Das deckt beide Richtungen der gemeldeten Fünf-Sekunden-Verzögerung ab.

Das Einblenden der Standby-Uhr scheidet als Ursache aus: laut Messung 2 ms.

Beide Panel-Varianten mit arduino-cli gegengebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NeFMr439WcLMdebE3kU7fS
2026-09-03 13:38:14 +02:00
thomas 32280a7ea3 Merge pull request 'fix(P4): Glyphenpuffer ins PSRAM - behebt Stillstand bei großer Standby-Uhr (1.9.8)' (#77) from fix/glyphenpuffer-ins-psram into main 2026-09-03 13:17:30 +02:00
raw-designsandClaude Opus 5 5988668026 fix(P4): Glyphenpuffer ins PSRAM - behebt Stillstand bei großer Standby-Uhr (1.9.8)
Ursache gefunden. lv_draw_label.c legt pro Glyphe einen A8-Draw-Buf an:
box_w * LV_ROUND_UP(box_h, 32) Byte. Größte Ziffer des 360er-Fonts ist
190x247 -> 190*256 = 48 KB. Mit LV_DRAW_SW_DRAW_UNIT_CNT 2 sind zwei davon
gleichzeitig offen, also ~97 KB. Der LVGL-Pool (LV_MEM_SIZE 256 KB) hatte
laut Herzschlag nur 98068 Byte am Stück frei - es reichte knapp nicht.

LV_USE_ASSERT_MALLOC ist 1 und LV_ASSERT_HANDLER war "while(1);". Bei
fehlgeschlagener Allokation hält der LVGL-Task also stumm an: kein Panic,
kein Backtrace, Log bricht einfach ab. Genau das beobachtete Bild.

- font_buf_malloc/-free über lv_draw_buf_get_font_handlers(): Puffer ab
  16 KB kommen per heap_caps_malloc aus dem PSRAM, kleinere weiter aus dem
  LVGL-Pool (schneller; sonst würde jeder Text langsamer). Freigabe
  unterscheidet über esp_ptr_external_ram().
- LV_ASSERT_HANDLER gibt jetzt eine Meldung aus, bevor er anhält.

Der interne RAM bleibt unangetastet - LV_MEM_SIZE zu erhöhen war keine
Option, der statische Speicher liegt bereits bei ~98 %.

Beide Panel-Varianten mit arduino-cli gegengebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NeFMr439WcLMdebE3kU7fS
2026-09-03 13:17:06 +02:00
thomas 4f2700d59a Merge pull request 'diag(P4): Messpunkte für den Stillstand beim Standby-Wechsel (1.9.7)' (#76) from diag/standby-stillstand-vermessen into main 2026-09-03 13:02:57 +02:00
7 changed files with 596 additions and 87 deletions
+156
View File
@@ -1,3 +1,159 @@
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:
- 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
der Kessel schon nahe am Ziel ist. Heizte er noch auf oder war er ganz aus, lag jeder
Messwert unterhalb des Fensters und wurde auf den unteren Rand geklemmt: Beide Kurven lagen
flach auf derselben Höhe und bewegten sich nicht, obwohl der eine Kessel bei 80 Grad stand
und der andere bei 0. Der Ausschnitt richtet sich jetzt nach den tatsächlich vorhandenen
Werten, mit einer Mindestspanne von vier Grad, damit ein ruhig geregelter Kessel nicht
jedes Zehntelgrad zum vollen Ausschlag aufbläht.
- Die Kurven nehmen jetzt den Platz unterhalb der Zahlen ein statt als schmaler Streifen in
der Mitte zu hängen: Sie beginnen weiter unten auf der Karte und sind deutlich höher.
- Der Nachlauf beim Wischen ist zurück. Er war abgeschaltet, weil jedes Einzelbild während
des Schwungs den ganzen Bildschirm neu zeichnete; seit die Anzeigewerte feste Maße haben,
fällt das weg.
- Die Messzeilen der Fehlersuche sind wieder entfernt. Die Messung des Bildaufbaus bleibt
erhalten, liegt aber hinter dem Schalter WS7_PERF_MONITOR und ist aus.
Version 1.9.12:
- Die Hintergrundbeleuchtung kommt beim Wechsel in den und aus dem Standby jetzt sofort.
Bisher stand das Bild bereits fertig da - im Gegenlicht war es zu sehen -, blieb aber
mehrere Sekunden dunkel. Das war die ganze gesuchte Verzögerung.
- Ursache: Die Bridge übernimmt eine neue Helligkeit nicht zuverlässig beim ersten Mal. Der
Beispielcode von Waveshare setzt sie deshalb zweimal, mit einer Pause dazwischen,
ausdrücklich „sicherheitshalber". Genau diese Wiederholung fehlte.
- Die Panel-Freigabe wird nur noch beim ersten Mal gesetzt. Sie erneut zu setzen, während die
Beleuchtung läuft, stößt die Helligkeitsstufe der Bridge neu an; der Beispielcode setzt sie
ebenfalls nur ein einziges Mal.
- Jeder Helligkeitswechsel meldet jetzt seine Dauer. Bleibt die Beleuchtung wieder hinterher,
lässt sich die Pause zwischen den beiden Übernahmen über WS7_BACKLIGHT_LATCH_MS erhöhen.
Version 1.9.11:
- Endlose Startschleife behoben: Beleuchtung an, nie ein Bild. Der P4 startet sich nach dem
Einschalten absichtlich einmal neu, weil das Panel sonst schwarz bleibt. Die Absicherung
dagegen verließ sich darauf, dass der zweite Start kein Kaltstart mehr ist - der Chip
meldet aber auch nach dem selbst ausgelösten Neustart wieder einen Kaltstart. Damit griff
die Bedingung erneut, endlos.
- Der Neustart wird jetzt vermerkt und beim zweiten Durchlauf erkannt, unabhängig davon, was
der Chip als Startgrund meldet. Beim nächsten echten Einschalten erfolgt wieder genau ein
Neustart.
- Die Meldung über diesen Neustart lief bisher über die USB-Verbindung und war im seriellen
Monitor nicht zu sehen - deshalb war von der Schleife nichts zu erkennen. Sie läuft jetzt
denselben Weg wie die übrigen Panel-Meldungen.
Version 1.9.10:
- Alle Messzeilen laufen jetzt auf derselben Uhr. Die des Zeichenablaufs startete später als
die des Hauptablaufs; die Zeiten ließen sich nur mit Umrechnung vergleichen.
- Der Touch meldet jetzt, wenn er Rohwerte verwirft, weil sie außerhalb des gültigen
Bereichs liegen, und nennt bei jeder erkannten Berührung, wie viele vorher verworfen
wurden. Eine Berührung, deren Rohwerte lange verworfen werden, sieht von außen genauso aus
wie ein Display, das nicht reagiert - das war bisher nicht zu unterscheiden.
Version 1.9.9:
- Die kleinen Kacheln unten im Dashboard behalten beim Bezug ihre Höhe. Bisher bekam die
untere Zeile während des Bezugs deutlich mehr Platz - die Kacheln wurden enorm hoch und
das Dashboard sprang bei jedem Bezugsbeginn sichtbar um.
- Die Messzeilen gingen bisher teils über die USB-Verbindung hinaus und waren im seriellen
Monitor gar nicht zu sehen - die Messung der Helligkeit lief damit ins Leere. Alle
Messzeilen laufen jetzt über denselben Weg wie die Panel-Meldungen.
- Jede Messzeile trägt jetzt eine Zeitmarke. Nur so sind die Lücken zu sehen, in denen die
fünf Sekunden vergehen.
- Der Wechsel in den und aus dem Standby wird an jeder Station vermessen: Ankunft der
Meldung, Warten auf die Anzeigesperre, Dauer der Aktualisierung. Ebenso der Weg vom Tippen
auf die Standby-Uhr bis zur fertig aufgebauten Rückfrage.
Version 1.9.8:
- Der Stillstand beim Wechsel in den Standby ist gefunden und behoben. LVGL legt für JEDE
Textglyphe einen eigenen Zwischenpuffer an - bei der großen Uhr rund 48 KB pro Ziffer, mit
zwei parallel arbeitenden Zeicheneinheiten fast 100 KB gleichzeitig. Der interne
LVGL-Speicher hatte nur rund 98 KB am Stück frei; es reichte knapp nicht. Schlägt die
Anforderung fehl, hält LVGL per Zusicherung in einer Endlosschleife an - kein Absturz,
keine Meldung, das Display steht einfach. Deshalb brach der Mitschnitt ohne Fehler ab.
- Große Glyphenpuffer liegen jetzt im externen Speicher (PSRAM, 32 MB frei). Kleine bleiben
im internen, der ist schneller - sonst würde jeder Text langsamer.
- Hält LVGL künftig wegen einer verletzten Zusicherung an, sagt es das jetzt, statt stumm
stehen zu bleiben.
Version 1.9.7:
- Messpunkte für den Stillstand beim Wechsel in den Standby: Beide Abläufe - der Haupt- und
der Zeichenablauf - melden jetzt alle zwei Sekunden einen Herzschlag mit Speicherstand.
+24 -22
View File
@@ -67,25 +67,11 @@ static void onState(const MachineState& st) {
}
if (bl < 0) bl = 0; if (bl > 100) bl = 100;
if (bl != s_lastBacklight) {
// Vermessen: Rund um jeden Helligkeitswechsel hing die Bedienung bisher ein bis
// zwei Sekunden. Der Schreibzugriff geht ueber denselben I2C-Bus wie der Touch.
uint32_t t0 = millis();
hal_backlight(bl);
uint32_t dt = millis() - t0;
if (dt > 50) Serial.printf("[HAL] Helligkeit %d %% gesetzt, %u ms\n", bl, (unsigned)dt);
hal_backlight(bl); // meldet seine Dauer selbst, siehe jc_backlight_set()
s_lastBacklight = bl;
}
}
// Herzschlag aus dem Haupt-Ablauf. Zusammen mit dem Herzschlag aus dem Zeichen-Ablauf
// laesst sich unterscheiden, welcher der beiden stehen bleibt, wenn nichts mehr geht.
static void main_heartbeat() {
static uint32_t s_last = 0;
if (millis() - s_last < 2000) return;
s_last = millis();
Serial.printf("[Haupt] Herzschlag, freier Speicher %u Byte\n", (unsigned)ESP.getFreeHeap());
}
static void onAck(uint32_t id, bool ok, const String& message) {
// Automatische/Heartbeat-Acks nicht als Toast zeigen (pong, Handshake, Listen-Refresh).
bool routine = ok && (message == "pong" || message == "hello" ||
@@ -321,20 +307,37 @@ void setup() {
// nicht (siehe WS7_DSI_RESTART).
//
// Deshalb startet der P4 sich nach einem Kaltstart genau einmal selbst neu, nachdem
// der erste Hochlauf durch ist. Beim zweiten Durchlauf ist der Startgrund kein
// Kaltstart mehr, die Bedingung greift also nicht erneut - eine Schleife ist
// ausgeschlossen. Kostet rund drei Sekunden, aber nur beim Einschalten.
// der erste Hochlauf durch ist. Kostet rund drei Sekunden, aber nur beim Einschalten.
//
// Frueher stuetzte sich die Absicherung dagegen allein auf den Startgrund: Beim zweiten
// Durchlauf sei es kein Kaltstart mehr, eine Schleife also ausgeschlossen. Das stimmt
// nicht - der Chip meldet auch nach dem selbst ausgeloesten Neustart wieder einen
// Kaltstart, und dann startet er endlos neu. Genau das ist aufgetreten: Beleuchtung an,
// nie ein Bild. Deshalb wird jetzt zusaetzlich vermerkt, dass der Neustart schon
// stattgefunden hat; darauf ist Verlass, unabhaengig vom gemeldeten Startgrund.
#if (JC_PANEL_TYPE == WS_PANEL_7H) && WS7_COLD_BOOT_RESTART
if (esp_reset_reason() == ESP_RST_POWERON) {
Preferences p;
p.begin("bootguard", false);
bool schonNeugestartet = p.getUChar("kaltneu", 0) != 0;
if (schonNeugestartet) {
// Zweiter Durchlauf: Vermerk loeschen, damit beim naechsten echten Einschalten
// wieder genau ein Neustart erfolgt.
p.putUChar("kaltneu", 0);
p.end();
esp_rom_printf("[Panel] Neustart bereits erfolgt - Hochlauf wird fortgesetzt\n");
} else {
p.putUChar("kaltneu", 1);
// Der Fruehstart-Zaehler wurde oben bereits erhoeht. Diesen gewollten Neustart
// darf er nicht mitzaehlen, sonst laeuft die Absturzsicherung nach ein paar
// Einschaltvorgaengen in den Update-Modus.
{ Preferences p; p.begin("bootguard", false); p.putUShort("early", 0); p.end(); }
DBG_SERIAL.println(F("[Panel] Kaltstart - einmaliger Neustart fuer das Displaypanel"));
DBG_SERIAL.flush();
p.putUShort("early", 0);
p.end();
esp_rom_printf("[Panel] Kaltstart - einmaliger Neustart fuer das Displaypanel\n");
delay(50);
esp_restart();
}
}
#endif
g_client.begin(); // UART starten, hello folgt automatisch in loop()
@@ -346,7 +349,6 @@ static unsigned long s_lastDbgMs = 0;
#endif
void loop() {
main_heartbeat();
g_client.loop(); // UART lesen, hello/heartbeat, state -> UI
hal_loop(); // realer Modus: leer; Stub-Modus: lv_timer_handler()
+39 -2
View File
@@ -235,9 +235,34 @@ void jc_backlight_set(int percent)
if (percent > 100) percent = 100;
if (percent < 0) percent = 0;
uint8_t brightness = (uint8_t)((255 * percent) / 100);
uint8_t wert = (uint8_t)(255 - brightness);
int64_t t0 = esp_timer_get_time();
// Die Bridge uebernimmt eine neue Helligkeit nicht zuverlaessig beim ersten Mal. Der
// Beispielcode von Waveshare setzt sie deshalb zweimal, mit einer Pause dazwischen,
// ausdruecklich "sicherheitshalber". Uns fehlte diese Wiederholung: Das Bild stand
// dann fertig da, blieb aber dunkel, bis die Uebernahme irgendwann doch griff - beim
// Ein- und Ausschalten des Standby waren das mehrere Sekunden.
//
// Die Freigabe (0xAD) bleibt beim ersten Aufruf, danach nicht mehr: Sie erneut zu
// setzen, waehrend die Beleuchtung laeuft, stoesst die Helligkeitsstufe der Bridge neu
// an - der Beispielcode setzt sie darum ebenfalls nur ein einziges Mal.
static bool freigegeben = false;
if (!freigegeben) {
ws7_bridge_write(0xAD, 0x01); // Panel-/Backlight-Freigabe
ws7_bridge_write(0xAB, (uint8_t)(255 - brightness));
vTaskDelay(pdMS_TO_TICKS(20)); // wie im Beispielcode: Bridge setzen lassen
freigegeben = true;
}
ws7_bridge_write(0xAB, wert);
ws7_bridge_write(0xAA, 0x01); // Helligkeit uebernehmen
vTaskDelay(pdMS_TO_TICKS(WS7_BACKLIGHT_LATCH_MS));
ws7_bridge_write(0xAB, wert); // Wiederholung, siehe oben
ws7_bridge_write(0xAA, 0x01);
esp_rom_printf("[HAL] %u ms: Helligkeit %d %% gesetzt, brauchte %u ms\n",
(unsigned)(esp_timer_get_time() / 1000), percent,
(unsigned)((esp_timer_get_time() - t0) / 1000));
}
// =====================================================================================
@@ -310,14 +335,26 @@ static bool ws7_touch_get_xy(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y
bool got = s_touch_get_xy_orig(tp, x, y, strength, point_num, max_point_num);
int64_t now = esp_timer_get_time();
// Unplausible Rohwerte gelten als Aussetzer, nicht als Beruehrung.
// Unplausible Rohwerte gelten als Aussetzer, nicht als Beruehrung. Wie viele davon
// anfallen, war bisher nirgends zu sehen - eine Beruehrung, deren Rohwerte lange
// verworfen werden, sieht von aussen wie ein Display aus, das nicht reagiert.
static uint32_t s_verworfen = 0;
if (got && *point_num > 0 &&
(x[0] > WS7_TOUCH_RAW_X_MAX || y[0] > WS7_TOUCH_RAW_Y_MAX)) {
if (s_verworfen == 0)
esp_rom_printf("[Touch] %u ms: Rohwert verworfen (x %u, y %u liegen ausserhalb)\n",
(unsigned)(now / 1000), (unsigned)x[0], (unsigned)y[0]);
s_verworfen++;
got = false;
*point_num = 0;
}
if (got && *point_num > 0) {
if (!s_touch_down)
esp_rom_printf("[Touch] %u ms: Beruehrung erkannt bei x %u, y %u; %u Rohwerte vorher verworfen\n",
(unsigned)(now / 1000), (unsigned)x[0], (unsigned)y[0],
(unsigned)s_verworfen);
s_verworfen = 0;
s_touch_last_x = x[0];
s_touch_last_y = y[0];
s_touch_last_us = now;
+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.3" // Firmware-Stand der P4-Display-Steuerung (Info-Seite)
// -------------------------------------------------------------------------------------
// Protokoll
+4 -2
View File
@@ -374,8 +374,10 @@
#define LV_USE_ASSERT_OBJ 0 /*Check the object's type and existence (e.g. not deleted). (Slow)*/
/*Add a custom handler when assert happens e.g. to restart the MCU*/
#define LV_ASSERT_HANDLER_INCLUDE <stdint.h>
#define LV_ASSERT_HANDLER while(1); /*Halt by default*/
#define LV_ASSERT_HANDLER_INCLUDE <esp_rom_sys.h>
// Frueher hielt LVGL hier stumm in einer Endlosschleife an - das Display stand, ohne dass
// irgendwo etwas davon zu sehen war. Jetzt sagt es wenigstens, dass es angehalten hat.
#define LV_ASSERT_HANDLER esp_rom_printf("[LVGL] Zusicherung verletzt - Zeichenablauf haelt an\n"); while(1);
/*-------------
* Debug
+8
View File
@@ -182,6 +182,14 @@
// auf I2C antwortet. Meldet sich 0x14 beim Start nicht, diesen Wert erhoehen.
#define WS7_TOUCH_POWER_MS 120
// Pause zwischen den beiden Helligkeitsuebernahmen der Bridge. Sie uebernimmt eine neue
// Helligkeit nicht zuverlaessig beim ersten Mal; der Beispielcode von Waveshare setzt sie
// darum zweimal. Ohne die Wiederholung stand das Bild fertig da, blieb aber dunkel - beim
// Wechsel in den und aus dem Standby mehrere Sekunden lang. Der Beispielcode wartet 100 ms;
// weniger genuegt hier, weil die Wiederholung ohnehin folgt. Bleibt die Beleuchtung wieder
// hinterher, diesen Wert erhoehen.
#define WS7_BACKLIGHT_LATCH_MS 20
// Prueffarbbalken statt Oberflaeche anzeigen. Sie entstehen im DSI-Baustein selbst und
// benutzen weder Bildspeicher noch LVGL. Damit laesst sich trennen, ob die Anzeigekette
// bis zum Panel arbeitet (Balken sichtbar) oder nicht (schwarz). Nur zur Fehlersuche.
+358 -54
View File
@@ -18,6 +18,7 @@
#include <Arduino.h>
#include <ArduinoJson.h>
#include <Preferences.h> // Dashboard-Design P4-lokal merken (NVS)
#include <esp_memory_utils.h> // esp_ptr_external_ram(): welcher Speicher einen Puffer traegt
#include <esp_heap_caps.h> // Verlaufs-Historie im PSRAM allozieren (interner RAM ist knapp)
#include "ui.h"
#include <Preferences.h>
@@ -177,11 +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
@@ -718,13 +737,10 @@ static void clock_size_apply() {
if (g_standbyClock) lv_obj_set_style_text_font(g_standbyClock, clock_font(), 0);
}
static void scroll_ohne_nachlauf(lv_obj_t* o) {
#if JC_PANEL_TYPE == WS_PANEL_7H
lv_obj_clear_flag(o, LV_OBJ_FLAG_SCROLL_MOMENTUM);
#else
(void)o;
#endif
}
// Der Nachlauf beim Wischen war auf dem Waveshare-Panel eine Zeit lang abgeschaltet, weil
// jedes Einzelbild waehrend des Schwungs den ganzen Schirm neu zeichnete. Seit die
// Anzeigewerte feste Masse haben, faellt das weg - das Menue laeuft mit Nachlauf fluessig.
static void scroll_ohne_nachlauf(lv_obj_t* o) { (void)o; }
static lv_obj_t* make_page(lv_obj_t* parent) {
lv_obj_t* p = lv_obj_create(parent);
@@ -929,7 +945,9 @@ static bool wake_choice_offered() {
return g_state.cxWakeChoice ||
(g_state.cxEnabled && g_state.cxArmable && g_state.cxAskOnWake);
}
static void standby_screen_cb(lv_event_t*) { wake_confirm_show(); } // Tipp auf Standby-Uhr -> nachfragen
static void standby_screen_cb(lv_event_t*) { // Tipp auf Standby-Uhr -> nachfragen
wake_confirm_show();
}
static void wake_cancel() { // Abbrechen -> zurueck in die Standby-Helligkeit
wake_timeout_stop();
wake_confirm_hide();
@@ -1105,10 +1123,195 @@ 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.
// Gemeinsamer Ausschnitt fuer BEIDE Verlaufskurven.
//
// 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 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
// 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 (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);
lv_obj_set_size(ch, LV_PCT(96), 64);
// 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
// mehr Hoehe - Temperaturaenderungen sind so ueberhaupt erst zu erkennen.
lv_obj_set_width(ch, LV_PCT(96));
lv_obj_set_height(ch, LV_PCT(100));
lv_obj_set_flex_grow(ch, 1);
lv_obj_set_style_min_height(ch, 90, 0);
lv_chart_set_type(ch, LV_CHART_TYPE_LINE);
lv_chart_set_point_count(ch, DASH_SPARK_POINTS);
lv_chart_set_div_line_count(ch, 0, 0);
@@ -1117,18 +1320,47 @@ 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_border_width(ch, 0, 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_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);
// Ausschnitt und 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.
int32_t sollT = (int32_t)(soll * 10.0f);
lv_chart_set_range(ch, LV_CHART_AXIS_PRIMARY_Y, sollT - 60, sollT + 30);
// 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.
(void)soll;
lv_chart_set_all_value(ch, *ser, (int32_t)(jetzt * 10.0f));
return ch;
return ch; // Ausschnitt setzt spark_ranges_fit(), sobald beide Kurven stehen
}
// Temperaturbereich gemaess aktiver Darstellung (neu) aufbauen - auch beim Umschalten
@@ -1141,6 +1373,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);
@@ -1165,10 +1400,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;
}
@@ -1667,20 +1907,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
// haengt am Sollwert, damit die Abweichung sichtbar wird und nicht in der Skala
// untergeht - eine feste Skala von 0 bis 130 Grad zeigte nur eine gerade Linie.
if (g_sparkSerW && g_sparkW) {
int32_t soll = (int32_t)(g_state.setW * 10.0f);
lv_chart_set_range(g_sparkW, LV_CHART_AXIS_PRIMARY_Y, soll - 60, soll + 30);
lv_chart_set_next_value(g_sparkW, g_sparkSerW, (int32_t)(g_state.tempW * 10.0f));
}
if (g_sparkSerD && g_sparkD) {
int32_t soll = (int32_t)(g_state.setD * 10.0f);
lv_chart_set_range(g_sparkD, LV_CHART_AXIS_PRIMARY_Y, soll - 120, soll + 60);
lv_chart_set_next_value(g_sparkD, g_sparkSerD, (int32_t)(g_state.tempD * 10.0f));
}
}
static void build_chart(lv_obj_t* p) {
lv_obj_clear_flag(p, LV_OBJ_FLAG_SCROLLABLE);
@@ -1904,12 +2130,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)));
@@ -3555,7 +3833,7 @@ static void refr_ready_cb(lv_event_t*) {
uint32_t d = lv_tick_get() - g_refrStart;
if (d < REFR_WARN_MS) return;
unsigned pct = (unsigned)((uint64_t)g_refrPixels * 100ULL / ((uint64_t)LCD_H_RES * LCD_V_RES));
esp_rom_printf("[UI] Bildaufbau %u ms, %u Bereiche, %u %% der Flaeche\n",
esp_rom_printf("[UI] %u ms: Bildaufbau %u ms, %u Bereiche, %u %% der Flaeche\n", (unsigned)millis(),
(unsigned)d, (unsigned)g_refrAreas, pct);
// Lage der Bereiche mit ausgeben. Erst daran ist zu erkennen, WELCHER Teil der
// Oberflaeche das Neuzeichnen ausloest - die Groesse allein sagt das nicht.
@@ -3565,10 +3843,34 @@ static void refr_ready_cb(lv_event_t*) {
(int)g_refrArea[i].y1, (int)g_refrArea[i].y2);
}
static void ui_heartbeat_cb(lv_timer_t*) {
lv_mem_monitor_t m; lv_mem_monitor(&m);
esp_rom_printf("[UI] Herzschlag, LVGL-Speicher frei %u von %u Byte, groesster Block %u\n",
(unsigned)m.free_size, (unsigned)m.total_size, (unsigned)m.free_biggest_size);
// LVGL legt fuer JEDE Textglyphe einen eigenen Zwischenpuffer an - Breite mal Hoehe, ein
// Byte je Bildpunkt. Bei der grossen Standby-Uhr sind das 190 x 256 = rund 48 KB pro Ziffer,
// und mit zwei parallel arbeitenden Zeicheneinheiten fast 100 KB gleichzeitig. Der interne
// LVGL-Speicher fasst nur 245 KB, wovon rund 98 KB am Stueck frei sind - es reichte knapp
// nicht. Schlaegt die Anforderung fehl, haelt LVGL per Zusicherung in einer Endlosschleife
// an: kein Absturz, keine Meldung, das Display steht einfach. Genau das war der Stillstand
// beim Wechsel in den Standby.
//
// Diese Puffer wandern daher in den externen Speicher (PSRAM, 32 MB frei). Kleine Glyphen
// bleiben im internen Speicher, weil der schneller ist und Text sonst ueberall langsamer
// wuerde; nur die grossen gehen nach aussen.
#define FONT_BUF_EXTERN_AB 16384 // ab dieser Groesse in den externen Speicher
static void* font_buf_malloc(size_t size, lv_color_format_t cf) {
LV_UNUSED(cf);
size += LV_DRAW_BUF_ALIGN - 1;
if (size >= FONT_BUF_EXTERN_AB) {
void* p = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (p) return p;
}
return lv_malloc(size);
}
static void font_buf_free(void* buf) {
// Der externe Speicher liegt ausserhalb des LVGL-Bereichs; daran ist zu erkennen,
// welcher der beiden Wege den Puffer vergeben hat.
if (esp_ptr_external_ram(buf)) heap_caps_free(buf);
else lv_free(buf);
}
static void refr_monitor_install() {
@@ -3977,12 +4279,18 @@ 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();
refr_monitor_install();
lv_timer_create(ui_heartbeat_cb, 2000, nullptr);
// Glyphenpuffer umlenken, bevor der erste Text gezeichnet wird.
lv_draw_buf_handlers_t* fh = lv_draw_buf_get_font_handlers();
fh->buf_malloc_cb = font_buf_malloc;
fh->buf_free_cb = font_buf_free;
#if WS7_PERF_MONITOR
refr_monitor_install(); // nur zum Einmessen, siehe WS7_PERF_MONITOR
#endif
g_uiReady = true;
}
@@ -4079,12 +4387,14 @@ static void dash_brew_mode(bool on) {
if (on == g_dashBrewMode) return; // nur bei echtem Wechsel neu anordnen
g_dashBrewMode = on;
if (!g_tempRow || !g_infoRow) return;
lv_obj_set_flex_grow(g_tempRow, on ? 1 : 3);
lv_obj_set_flex_grow(g_infoRow, on ? 3 : 2);
// Die Schrift von Shot-Timer und Waage bleibt unveraendert. Frueher wechselte sie
// waehrend des Bezugs auf den grossen fetten Schnitt - das wirkte gegenueber dem
// restlichen Dashboard zu laut. Der Bereich bekommt weiterhin mehr Platz, die Zahlen
// stehen dadurch freier, aber in derselben Schrift wie sonst auch.
// Die Zeilenhoehen bleiben unveraendert. Frueher bekam die untere Zeile waehrend des
// Bezugs deutlich mehr Platz - die kleinen Kacheln wurden dadurch enorm hoch und das
// Dashboard sprang bei jedem Bezugsbeginn sichtbar um. Der Bezug hebt sich jetzt allein
// ueber seine Inhalte hervor, nicht ueber die Groesse.
(void)on;
// Die Schrift von Shot-Timer und Waage bleibt ebenfalls unveraendert. Frueher wechselte
// sie waehrend des Bezugs auf den grossen fetten Schnitt - das wirkte gegenueber dem
// restlichen Dashboard zu laut.
}
static void dash_brew_mode_tick(const MachineState& st) {
@@ -4172,14 +4482,8 @@ void ui_update(const MachineState& st) {
if (shouldShow) {
label_set_if_changed(g_standbyClock, zeit.length() ? zeit.c_str() : "--:--");
if (!g_standbyShown) {
// Vermessen: Das Einblenden der Uhr ist der Moment, in dem das Display
// zuletzt stehen geblieben ist. Bleibt die zweite Zeile aus, haengt es hier.
esp_rom_printf("[UI] Standby-Uhr wird eingeblendet\n");
uint32_t t0 = lv_tick_get();
ui_fade_in(g_standbyScreen, UI_FADE_MS); // Einschlaf-Animation
g_standbyShown = true;
esp_rom_printf("[UI] Standby-Uhr eingeblendet, %u ms\n",
(unsigned)(lv_tick_get() - t0));
}
// Frueher wurde die Uhr hier bei JEDER Zustandsmeldung nach vorn geholt. Das
// ordnet die Ebenen neu und laesst LVGL den gesamten Bildschirm neu zeichnen -