Compare commits

..
Author SHA1 Message Date
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
raw-designsandClaude Opus 5 0ec3d89973 diag(P4): Messpunkte für den Stillstand beim Standby-Wechsel (1.9.7)
Beim Wechsel in den Standby bleibt das Display stehen: Bild bleibt auf der
alten Seite, keine Bedienung, auch der S3 weckt nicht mehr auf. Der Log
bricht nach dem letzten Bildaufbau ab, ohne Panic.

Statt zu raten, Messpunkte:
- Herzschlag alle 2 s aus dem Arduino-loop (freier Heap) und aus einem
  lv_timer im LVGL-Task (lv_mem_monitor). Bleibt nur einer aus, ist klar,
  welcher Task steht.
- ui_fade_in() der Standby-Uhr wird eingeklammert: "wird eingeblendet" /
  "eingeblendet, N ms".
- hal_backlight() wird gemessen; über 50 ms wird die Dauer gemeldet.
  Der Bridge-Schreibzugriff läuft über denselben I2C-Bus wie der Touch.

Außerdem entfernt: die LV_EVENT_INVALIDATE_AREA-Callbacks aus 1.9.5. Das
Event geht per lv_display_send_event an das Display, nicht an Objekte - die
Callbacks konnten nie feuern, ihr Ausbleiben war kein Beweis.

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:02:35 +02:00
thomas 06d9710b16 Merge pull request 'feat(P4): Eigener Menüpunkt „Darstellung“, größere Uhr im Standby (1.9.6)' (#75) from feat/darstellung-eigene-seite-groessere-uhr into main 2026-09-03 09:24:31 +02:00
raw-designsandClaude Opus 5 b3b96362c3 feat(P4): Eigener Menüpunkt "Darstellung", größere Uhr im Standby (1.9.6)
- Der Darstellungsblock wandert aus build_info() in ein eigenes build_display()
  mit neuer Seite PG_DISPLAY. Die Info-Seite zeigt nur noch Firmware-Stände und
  Verbindungsdaten.
- clock_font() liefert für WS_PANEL_7H jetzt 180/270/360 statt 120/180/240. Für
  JC_PANEL_43 bleiben die alten Stufen - 360 px passen nicht auf 480x480.
- Neue Fonts lv_font_clock_270/_360 (Maven Pro Regular, bpp 4, Range
  32,45,0x30-0x3A). bpp 3 scheidet aus: lv_font_conv erlaubt das nur mit
  Kompression, und die kostet beim Zeichnen genau das, was gerade eingespart wird.
- Alle fünf Uhr-Fonts sind deklariert; der Linker verwirft die je nach Panel
  ungenutzten. Netto wächst die WS-Firmware um 196 KB.

Beide Panel-Varianten mit arduino-cli gegengebaut (PartitionScheme=custom,
passend zur partitions.csv mit zwei 6,5-MB-Slots).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NeFMr439WcLMdebE3kU7fS
2026-09-03 09:23:50 +02:00
thomas 14f1a4789d Merge pull request 'perf(P4): Auch feste Höhen - LVGL markiert sonst das Layout als dirty (1.9.5)' (#74) from perf/feste-hoehen-und-nachweis into main 2026-09-03 07:25:04 +02:00
raw-designsandClaude Opus 5 326af4288a perf(P4): Auch feste Höhen - LVGL markiert sonst das Layout als dirty (1.9.5)
Die festen Breiten aus 1.9.4 haben nicht gereicht. lv_label_set_text ruft
lv_obj_refresh_self_size(); ist eines der beiden Maße LV_SIZE_CONTENT, gilt
das Parent-Layout als dirty und der Container wird komplett invalidiert.

- tempField(): big und setp bekommen zusätzlich eine feste Höhe aus
  lv_font_get_line_height().
- infoCell(): val ebenso, mit Platz für zwei Zeilen.
- g_clock: feste Breite/Höhe, rechtsbündig. Erklärt den Bereich
  "x 6..57, y 2..41" (Menü-Knopf), der bei jedem Minutenwechsel durch das
  Header-Relayout mit invalidiert wurde.

Zusätzlich eine gezielte Diagnose: LV_EVENT_INVALIDATE_AREA auf g_content
und der Dashboard-Seite meldet, wenn ein ganzer Container invalidiert wird -
damit ist "einzelnes Label" von "kompletter Bereich" zu unterscheiden.

Beide Panel-Varianten mit arduino-cli gegengebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 03:10:40 +02:00
thomas 2e8651bc00 Merge pull request 'perf(P4): Feste Breiten für Anzeigewerte - kein Neulayout mehr (1.9.4)' (#73) from perf/feste-breiten-kein-neulayout into main 2026-09-03 02:56:24 +02:00
raw-designsandClaude Opus 5 b13e5fdaf1 perf(P4): Feste Breiten für Anzeigewerte - kein Neulayout mehr (1.9.4)
Die Koordinaten haben den Verursacher benannt: Der invalidierte Bereich war
"x 0..1279, y 44..719" - der komplette Content-Container, bei jeder
Zustandsmeldung.

Grund: Die Wert-Labels hatten LV_SIZE_CONTENT. Ändert sich ihre Breite mit
dem Inhalt (92,4 °C -> 100,1 °C), löst LVGL ein Flex-Relayout der ganzen
Seite aus und invalidiert sie komplett. Change-checked Text allein reicht
also nicht - der Text darf beim Ändern auch die Breite nicht ändern.

tempField() und infoCell() setzen die Wert-Labels jetzt auf LV_PCT(100) mit
zentriertem Text. Optisch identisch, da die Werte ohnehin mittig standen.

Beide Panel-Varianten mit arduino-cli gegengebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 02:56:05 +02:00
thomas 671a6696ce Merge pull request 'fix(P4): Menüvorhang wieder durchscheinend, weitere Invalidierung entfernt (1.9.3)' (#72) from fix/menuevorhang-und-weitere-invalidierung into main 2026-09-03 02:36:48 +02:00
raw-designsandClaude Opus 5 288f61447d fix(P4): Menüvorhang wieder durchscheinend, weitere Invalidierung entfernt (1.9.3)
- Regression aus 1.8.7: Der deckende Drawer-Scrim verdeckte den
  Seiteninhalt, der dahinter sichtbar bleiben soll. Zurück auf LV_OPA_50.
- ready_band_update() setzte Text, Textfarbe und Hintergrundfarbe des
  Zustandsbands bei jeder Zustandsmeldung neu. Das Band spannt sich über die
  volle Breite - allein dadurch wurde ein Streifen über den ganzen Bildschirm
  ungültig. Jetzt über die change-checked Helfer.
- temp_progress_set() setzte Balkenwert und -farbe unbedingt; jetzt nur bei
  echter Änderung.
- Das WLAN-Symbol ebenso.
- Die Messung gibt zusätzlich die Koordinaten der invalidierten Bereiche aus.
  Die Fläche allein sagt nicht, welcher Teil der UI den Redraw auslöst.

Beide Panel-Varianten mit arduino-cli gegengebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 02:36:31 +02:00
thomas a91c792491 Merge pull request 'perf(P4): Nur bei echter Änderung neu zeichnen (1.9.2)' (#71) from perf/nur-bei-aenderung-neu-zeichnen into main 2026-09-03 02:17:20 +02:00
raw-designsandClaude Opus 5 e32c654b8d perf(P4): Nur bei echter Änderung neu zeichnen (1.9.2)
Die erweiterte Messung war eindeutig: "1 Bereich, 100 % der Fläche" - LVGL
zeichnete bei jeder Zustandsmeldung den kompletten Bildschirm neu, also
einmal pro Sekunde 240-300 ms lang. Kein Rendering-Tempoproblem, sondern
massiv zu viel Invalidierung.

lv_label_set_text invalidiert den Label-Bereich unbedingt, auch bei
identischem Text; lv_obj_set_style_text_color ebenso. Auf dem Dashboard
wurden pro Sekunde Werte quer über den Bildschirm gesetzt (Uhr oben,
Temperaturen Mitte, Bezugswerte unten) - LVGL joint die Bereiche, und die
Bounding-Box ist die ganze Fläche.

- Neue Helfer label_fmt_if_changed() und label_color_if_changed() neben dem
  vorhandenen label_set_if_changed().
- Alle 13 zyklischen Textzuweisungen und alle 10 Farbzuweisungen in
  ui_update darauf umgestellt.

Da sich Temperaturen selten um ein Zehntelgrad ändern und Sollwerte
praktisch nie, entfallen die meisten Neuzeichnungen ganz.

Beide Panel-Varianten mit arduino-cli gegengebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 02:17:03 +02:00
thomas 484812cb4c Merge pull request 'diag(P4): Gezeichnete Fläche messen, direktes Zeichnen zurücknehmen (1.9.1)' (#70) from diag/gezeichnete-flaeche-messen into main 2026-09-03 01:42:21 +02:00
raw-designsandClaude Opus 5 ff23e0e614 diag(P4): Gezeichnete Fläche messen, direktes Zeichnen zurücknehmen (1.9.1)
WS7_DIRECT_FB hat die Bildaufbauzeit nicht verkürzt (weiterhin 240-300 ms)
und Bildfehler erzeugt - in denselben Speicher zu zeichnen, aus dem gerade
angezeigt wird, geht ohne zweiten Framebuffer nicht sauber. Zurück auf 0,
Schalter mit Begründung erhalten.

Damit ist belegt: Der Flaschenhals liegt nicht im Kopieren, sondern im
Rendern selbst. Offen ist, ob LVGL kleine Bereiche langsam zeichnet oder
jedes Mal die volle Fläche - zwei verschiedene Ursachen.

refr_start_cb summiert deshalb jetzt die nicht zusammengefassten
inv_areas und meldet Anzahl und Flächenanteil zusammen mit der Dauer.

Beide Panel-Varianten mit arduino-cli gegengebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 01:42:05 +02:00
thomas 09b5231357 Merge pull request 'perf(P4): LVGL zeichnet direkt in den Bildspeicher (1.9.0)' (#69) from perf/direkt-in-bildspeicher-zeichnen into main 2026-09-03 01:27:32 +02:00
raw-designsandClaude Opus 5 16d8465b0c perf(P4): LVGL zeichnet direkt in den Bildspeicher (1.9.0)
Die Messung aus 1.8.8 hat es gezeigt: Jeder Bildaufbau kostete 240-300 ms,
dauerhaft, auch im Leerlauf. Die fünf Sekunden für den Aufweckdialog waren
rund zwanzig solcher Durchgänge - meine bisherigen Erklärungen (Fades,
Scrims) waren Symptome, nicht die Ursache.

Grund: zwei Durchgänge durch je 2,7 MB PSRAM pro Bild. LVGL rendert in einen
eigenen Draw-Buffer, esp_lcd_panel_draw_bitmap kopiert ihn danach komplett
in den Framebuffer.

WS7_DIRECT_FB holt den Framebuffer über esp_lcd_dpi_panel_get_frame_buffer
und übergibt ihn als LVGL-Draw-Buffer. Der IDF-Treiber erkennt das
(draw_buffer im Framebuffer-Bereich) und macht nur noch Cache-Writeback statt
Kopie; dazu LV_DISPLAY_RENDER_MODE_DIRECT, sodass nur geänderte Bereiche neu
gezeichnet werden.

Außerdem:
- Shot-Timer und Waage behalten während eines Bezugs die normale Schrift;
  der Wechsel auf den fetten 96er ist entfernt.
- Größe der Standby-Uhr einstellbar (120/180/240 px, Info-Seite,
  NVS ui/uhrgr). Wirkt sofort - sie hängt an einem einzigen Label.
- Die Knöpfe des Aufweckdialogs skalieren mit der Schriftgröße.

Beide Panel-Varianten mit arduino-cli gegengebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 01:27:14 +02:00
thomas 407d938031 Merge pull request 'diag(P4): Bildaufbau messen, Dialogknöpfe skalieren (1.8.8)' (#68) from diag/bildaufbau-messen into main 2026-09-03 01:01:36 +02:00
raw-designsandClaude Opus 5 33ce2b3ab9 diag(P4): Bildaufbau messen, Dialogknöpfe skalieren (1.8.8)
Der Aufweckdialog braucht weiterhin ~5 s. Zwei Erklärungsversuche lagen
daneben (Fade-Animationen, transparenter Scrim), also wird ab jetzt
gemessen statt geraten.

- refr_monitor_install() hängt sich über LV_EVENT_REFR_START/READY an die
  Anzeige und meldet jeden Zeichenzyklus, der 200 ms überschreitet. Im
  Normalbetrieb bleibt die Konsole still.
- Der Aufweckdialog berücksichtigt die Schriftgröße jetzt vollständig:
  Knöpfe mit font_text() statt geerbter Grundschrift, Breite 150/210/280 je
  Stufe. Bisher wirkten sie neben dem größeren Text verloren.
- Die große Standby-Uhr bleibt bewusst fest: Sie ist auf die Bildschirmhöhe
  abgestimmt.

Beide Panel-Varianten mit arduino-cli gegengebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STPpZoK3ToDhrcniVqE6MJ
2026-09-03 01:01:19 +02:00
thomas d4d4f2b9d2 Merge pull request 'fix(P4): Aufweckdialog sofort statt nach fünf Sekunden (1.8.7)' (#67) from fix/vorhaenge-deckend-und-dialogbreite into main 2026-09-03 00:47:35 +02:00
10 changed files with 48093 additions and 62 deletions
+144
View File
@@ -1,3 +1,147 @@
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.
Bleibt nur einer von beiden aus, ist klar, welcher stehen bleibt.
- Das Einblenden der Standby-Uhr meldet Anfang und Ende mit Dauer. Bleibt die zweite Meldung
aus, hängt es genau dort.
- Jeder Helligkeitswechsel, der länger als 50 ms braucht, wird mit seiner Dauer gemeldet. Der
Schreibzugriff geht über denselben Bus wie der Touch - ein Verdacht für die bekannte
Verzögerung von ein bis zwei Sekunden.
- Die Rückmeldung aus 1.9.5, die einen komplett ungültigen Inhaltsbereich melden sollte, ist
wieder entfernt. Sie konnte nie auslösen: LVGL schickt diese Meldung an das Display, nicht
an einzelne Elemente. Ihr Ausbleiben war also kein Beweis.
Version 1.9.6:
- Die Darstellungseinstellungen haben einen eigenen Menüpunkt „Darstellung" bekommen. Sie
standen bisher oben auf der Info-Seite, zwischen Sachen, mit denen sie nichts zu tun
haben. Die Info-Seite zeigt jetzt nur noch Firmware-Stände und Verbindungsdaten.
- Die Uhr im Standby ist auf dem 7-Zoll-Panel deutlich größer geworden: Die drei Stufen
liegen jetzt bei 180, 270 und 360 Pixeln statt bei 120, 180 und 240. Auf dem 4,3-Zoll-
Panel bleiben die bisherigen Stufen, dort passen größere schlicht nicht auf den Schirm.
Version 1.9.5:
- Die festen Breiten aus 1.9.4 haben nicht gereicht: Auch die HÖHE muss festliegen. LVGL
prüft beim Setzen eines Textes, ob sich die Eigengröße der Beschriftung geändert haben
könnte - ist eines der beiden Maße frei, gilt das Layout des Elternteils als neu zu
berechnen, und das macht die ganze Seite ungültig. Temperaturwerte, Sollwerte, die Werte
der unteren Zeile und die Uhrzeit in der Kopfleiste haben jetzt feste Maße in beide
Richtungen.
- Die Uhrzeit hatte denselben Effekt in der Kopfleiste: Beim Wechsel der Minute wurde die
Leiste samt Menü-Knopf neu berechnet. Das erklärt den Bereich „x 6..57, y 2..41", der in
fast jedem Durchgang auftauchte.
- Zusätzlich meldet die Firmware jetzt, wenn der Inhaltsbereich oder die Dashboard-Seite als
Ganzes ungültig gemacht wird. Damit ist zu unterscheiden, ob eine einzelne Beschriftung
den Bildaufbau auslöst oder gleich der komplette Bereich - zwei verschiedene Ursachen mit
verschiedenen Gegenmitteln.
Version 1.9.4:
- Die Messung mit Koordinaten hat den Verursacher benannt: Der ungültige Bereich war
„x 0..1279, y 44..719" - also der gesamte Inhalt unterhalb der Kopfleiste, bei jeder
Zustandsmeldung.
- Grund: Die Anzeigewerte hatten keine feste Breite. Wächst oder schrumpft eine Beschriftung
mit ihrem Inhalt - etwa von „92,4 °C" auf „100,1 °C" -, rechnet LVGL das Layout der
gesamten Seite neu und macht sie dabei komplett ungültig. Es genügt also nicht, den Text
nur bei echter Änderung zu setzen; er darf beim Ändern auch die Breite nicht verändern.
- Temperaturwerte, Sollwerte und die Werte der unteren Zeile haben jetzt eine feste Breite
mit zentriertem Text. Sichtbar ändert sich dadurch nichts - die Werte standen ohnehin
mittig.
Version 1.9.3:
- Fehlerbehebung: Bei geöffnetem Menü war der Seiteninhalt dahinter verschwunden. Der
Vorhang des Menüs war in 1.8.7 deckend gemacht worden, um Zeichenlast zu sparen - dabei
verdeckt er aber genau das, was sichtbar bleiben soll. Er ist wieder durchscheinend.
- Weitere Stellen umgestellt, die bei jeder Zustandsmeldung neu zeichnen ließen, obwohl sich
nichts geändert hatte: das Zustandsband der Darstellung „Band" (es spannt sich über die
volle Breite und machte damit allein schon einen Streifen über den ganzen Bildschirm
ungültig), die Heizleistungsbalken und das WLAN-Symbol.
- Die Messung nennt jetzt zusätzlich die Lage der neu gezeichneten Bereiche. Die Größe
allein sagt nicht, welcher Teil der Oberfläche das Neuzeichnen auslöst - die Koordinaten
schon.
Version 1.9.2:
- Ursache der Trägheit endlich gefunden. Die erweiterte Messung zeigte: „1 Bereich, 100 %
der Fläche" - LVGL zeichnete bei jeder Zustandsmeldung den kompletten Bildschirm neu, also
einmal pro Sekunde 240 bis 300 Millisekunden lang. Es war also kein Tempoproblem beim
Zeichnen, sondern es wurde viel zu viel als „muss neu gezeichnet werden" gemeldet.
- Grund: LVGL macht den Bereich einer Beschriftung ungültig, sobald ihr Text gesetzt wird -
auch dann, wenn er sich gar nicht geändert hat. Dasselbe gilt für Farbwechsel. Auf dem
Dashboard wurden im Sekundentakt Werte quer über den ganzen Bildschirm gesetzt: Uhrzeit
oben, Temperaturen in der Mitte, Bezugswerte unten. LVGL fasst benachbarte ungültige
Bereiche zu einem zusammen - und der umfasste dann die gesamte Fläche.
- Alle regelmäßig gesetzten Werte werden jetzt vorher verglichen und nur bei echter Änderung
neu geschrieben; für Farben gilt dasselbe. Da sich Temperaturen selten um ein Zehntelgrad
ändern und der Sollwert praktisch nie, entfallen die meisten dieser Neuzeichnungen.
- Das betrifft alle Bildschirmgrößen; auf den JC-Panels war es nur nie störend, weil dort ein
Bildaufbau ein Bruchteil davon kostet.
Version 1.9.1:
- Das direkte Zeichnen in den Bildspeicher aus 1.9.0 ist zurückgenommen: Es hat die
Bildaufbauzeit nicht verkürzt (weiterhin 240 bis 300 ms) und stattdessen Bildfehler
erzeugt, weil in denselben Speicher gezeichnet wird, aus dem gerade angezeigt wird.
Damit ist auch klar, dass der Flaschenhals nicht im Kopieren liegt, sondern im Zeichnen
selbst. Der Schalter WS7_DIRECT_FB bleibt samt Begründung erhalten.
- Die Messung sagt jetzt zusätzlich, WIE VIEL neu gezeichnet wurde: Anzahl der Bereiche und
ihr Anteil an der Gesamtfläche. Erst damit lässt sich unterscheiden, ob LVGL nur kleine
Bereiche anfasst - dann ist das Zeichnen an sich zu langsam - oder ob jedes Mal der ganze
Bildschirm neu entsteht, weil ihn etwas unnötig ungültig macht. Das sind zwei völlig
verschiedene Ursachen mit verschiedenen Gegenmitteln.
Version 1.9.0:
- Die Messung aus 1.8.8 hat die Ursache der Trägheit gefunden: Jeder Bildaufbau kostete
240 bis 300 Millisekunden - dauerhaft, auch im Leerlauf. Fünf Sekunden für den
Aufweckdialog waren also rund zwanzig solcher Durchgänge.
- Grund war, dass zweimal durch den Bildspeicher gearbeitet wurde: LVGL zeichnete in einen
eigenen Puffer, und dessen Inhalt wurde anschließend vollständig in den Bildspeicher
kopiert. Bei 1280x720 in 24 Bit sind das zwei Durchgänge durch je 2,7 MB.
- LVGL zeichnet jetzt direkt in den Bildspeicher. Der Treiber lässt das Kopieren dann weg,
und LVGL zeichnet nur noch die geänderten Bereiche statt jedes Mal die ganze Fläche.
Schalter WS7_DIRECT_FB in pins_config.h.
- Die Anzeigen von Shot-Timer und Waage bleiben während eines Bezugs in der normalen
Schrift; der fette Wechsel ist entfernt. Der Bereich bekommt weiterhin mehr Platz.
- Neu: Die Größe der Standby-Uhr lässt sich einstellen (Klein, Mittel, Groß - 120, 180 oder
240 Pixel), zu finden auf der Info-Seite unter Darstellung. Anders als die allgemeine
Schriftgröße wirkt sie sofort, weil sie an einem einzigen Textfeld hängt.
- Die Knöpfe des Aufweckdialogs berücksichtigen jetzt ebenfalls die eingestellte
Schriftgröße und wachsen mit.
Version 1.8.8:
- Der Aufweckdialog berücksichtigt jetzt die eingestellte Schriftgröße vollständig: Die
Knöpfe bekommen dieselbe Schrift wie der Text und wachsen mit (150 / 210 / 280 Pixel).
Bisher erbten sie die kleine Grundschrift und behielten eine feste Breite - neben dem
größeren Text wirkten sie dadurch verloren.
- Die große Standby-Uhr bleibt bewusst bei ihrer festen Größe: Sie ist bereits auf die
Bildschirmhöhe abgestimmt, eine Umschaltung würde sie nur schlechter passend machen.
- Neu: eine Zeitmessung für langsame Bildaufbauten. Überschreitet ein Zeichenzyklus
200 Millisekunden, meldet die serielle Konsole seine Dauer. Damit lässt sich feststellen,
welcher Vorgang tatsächlich Zeit kostet - die bisherigen Erklärungen für die Verzögerung
beim Aufweckdialog haben sich als falsch erwiesen, und weiteres Raten hilft nicht.
Im Normalbetrieb bleibt die Ausgabe still.
Version 1.8.7:
- Vom Fingertipp auf die Standby-Uhr bis zum Aufweckdialog vergingen rund fünf Sekunden mit
schwarzem Bildschirm. Ursache war der halbdurchsichtige Vorhang, den der Dialog über den
@@ -47,10 +47,23 @@ static bool g_bootCountCleared = false;
// --- Protokoll-Callbacks ---------------------------------------------------------------
static void onState(const MachineState& st) {
// Vermessen: Zwischen dem Umschalten und dem sichtbaren Ergebnis vergehen rund fuenf
// Sekunden. Jede Station bekommt eine Zeitmarke, damit die Luecke zu sehen ist.
static bool s_lastStandby = false;
bool wechsel = (st.standbyActive != s_lastStandby);
s_lastStandby = st.standbyActive;
uint32_t t0 = millis();
if (wechsel) esp_rom_printf("[Zustand] %u ms: Standby jetzt %s - Meldung angekommen\n",
(unsigned)t0, st.standbyActive ? "an" : "aus");
// LVGL laeuft im realen Modus in einem eigenen Task -> UI-Zugriff sperren.
hal_lock();
uint32_t t1 = millis();
ui_update(st);
uint32_t t2 = millis();
hal_unlock();
if (wechsel || (t2 - t0) > 200)
esp_rom_printf("[Zustand] %u ms: Sperre %u ms, Anzeige aktualisiert %u ms\n",
(unsigned)millis(), (unsigned)(t1 - t0), (unsigned)(t2 - t1));
// Display im Standby aus; ist "Uhrzeit im Standby anzeigen" aktiv, bleibt es an,
// aber abgedunkelt (nicht blendend bei Nacht). Helligkeiten kommen von der S3
@@ -67,11 +80,27 @@ 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) esp_rom_printf("[HAL] %u ms: Helligkeit %d %% gesetzt, brauchte %u ms\n",
(unsigned)millis(), bl, (unsigned)dt);
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();
esp_rom_printf("[Haupt] %u ms: Herzschlag, freier Speicher %u Byte\n",
(unsigned)millis(), (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" ||
@@ -332,6 +361,7 @@ 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()
+9 -3
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
@@ -569,7 +571,11 @@
LV_FONT_DECLARE(lv_font_maven_pro_48) \
LV_FONT_DECLARE(lv_font_maven_pro_bold_96) \
LV_FONT_DECLARE(lv_font_maven_pro_bold_140) \
LV_FONT_DECLARE(lv_font_clock_240)
LV_FONT_DECLARE(lv_font_clock_120) \
LV_FONT_DECLARE(lv_font_clock_180) \
LV_FONT_DECLARE(lv_font_clock_240) \
LV_FONT_DECLARE(lv_font_clock_270) \
LV_FONT_DECLARE(lv_font_clock_360)
/*Always set a default font*/
#define LV_FONT_DEFAULT &lv_font_maven_pro_14
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+27 -1
View File
@@ -496,6 +496,32 @@ static lv_display_t *display_init(esp_lcd_panel_handle_t panel_handle)
// buf1 = heap_caps_malloc(buffer_size * sizeof(lv_color_t), MALLOC_CAP_DMA);
assert(buf1);
ESP_LOGI(TAG, "LVGL buffer size: %dKB", buffer_size * sizeof(lv_color_t) / 1024);
#if WS7_DIRECT_FB
// PROJEKTAENDERUNG: LVGL zeichnet direkt in den Bildspeicher.
//
// Der Hersteller-Port legt einen eigenen Zeichenpuffer an; der fertige Inhalt wird
// danach in den Bildspeicher kopiert. Bei 1280x720 in 24 Bit sind das zwei Durchgaenge
// durch je 2,7 MB PSRAM - gemessen rund 250 bis 300 ms pro Bild, und das dauerhaft.
// Genau deshalb hing die Oberflaeche eine gute Viertelsekunde hinterher, und Vorgaenge
// mit mehreren Bildern brauchten Sekunden.
//
// Ist der Zeichenpuffer derselbe Speicher wie der Bildspeicher, laesst der
// esp_lcd-Treiber das Kopieren weg und schreibt nur den Prozessor-Cache zurueck. Dazu
// kommt LV_DISPLAY_RENDER_MODE_DIRECT: LVGL zeichnet dann nur die geaenderten Bereiche
// neu, statt jedes Mal die ganze Flaeche.
//
// Preis: Gezeichnet wird in den Speicher, der gerade angezeigt wird - bei schnellen
// Wechseln kann kurz eine Kante durchs Bild laufen. Mit nur einem Bildspeicher (den das
// Panel verlangt, siehe WS7_SINGLE_FB) ist das ohnehin nicht zu vermeiden.
void* fb0 = NULL;
if (esp_lcd_dpi_panel_get_frame_buffer(panel_handle, 1, &fb0) == ESP_OK && fb0) {
buf1 = fb0;
buffer_size = LVGL_PORT_H_RES * LVGL_PORT_V_RES;
ESP_LOGI(TAG, "LVGL zeichnet direkt in den Bildspeicher (%dKB)",
(int)(buffer_size * sizeof(lv_color_t) / 1024));
}
#endif
#endif /* LVGL_PORT_AVOID_TEAR_ENABLE */
ESP_LOGD(TAG, "Register display driver to LVGL");
@@ -511,7 +537,7 @@ static lv_display_t *display_init(esp_lcd_panel_handle_t panel_handle)
display, buf1, buf2, buffer_size * sizeof(lv_color_t),
#if LVGL_PORT_FULL_REFRESH
LV_DISPLAY_RENDER_MODE_FULL
#elif LVGL_PORT_DIRECT_MODE
#elif LVGL_PORT_DIRECT_MODE || WS7_DIRECT_FB
LV_DISPLAY_RENDER_MODE_DIRECT
#else
LV_DISPLAY_RENDER_MODE_PARTIAL
+6
View File
@@ -39,6 +39,12 @@
// nicht - siehe die ausfuehrliche Begruendung weiter unten beim Avoid-Tear-Schalter.
// Auf 0 nur, wenn das Startverhalten erneut untersucht wird.
#define WS7_SINGLE_FB 1
// LVGL zeichnet direkt in den Bildspeicher statt in einen eigenen Puffer, dessen Inhalt
// danach kopiert wird. GEPRUEFT: bringt an diesem Panel nichts (die Bildaufbauzeit blieb bei
// 240-300 ms) und erzeugt Bildfehler, weil in denselben Speicher gezeichnet wird, aus dem
// gerade angezeigt wird. Der Flaschenhals liegt also nicht im Kopieren, sondern im Zeichnen
// selbst. Bleibt als Schalter erhalten, damit der Versuch nicht erneut gemacht wird.
#define WS7_DIRECT_FB 0
#define WS7_PARALLEL_RENDER 1
#define EXAMPLE_LVGL_PORT_TASK_MAX_DELAY_MS 500 //range 2 to 2000
+291 -58
View File
@@ -18,11 +18,15 @@
#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>
#include "esp_system.h"
#include "display_hal.h"
#include "esp_rom_sys.h"
#include "lvgl_private.h"
#include <stdarg.h>
// ------------------------------------------------------------------------------------
// Schriftgroesse
@@ -124,8 +128,8 @@ static lv_obj_t* g_otaStatus = nullptr;
static lv_obj_t* g_otaBytes = nullptr;
// Navigation
enum { PG_DASH = 0, PG_CHART, PG_STATS, PG_TEMP, PG_BREW, PG_COLDEX, PG_SERVICE, PG_PROFILE, PG_LIGHT, PG_WIFI, PG_INFO, PG_COUNT };
static const char* PAGE_NAMES[PG_COUNT] = { "Dashboard", "Verlauf", "Statistik", "Temperatur", "Brew-Control", "Cold Extraction", "Reinigung/Wartung", "Profile", "Licht", "WiFi", "Info" };
enum { PG_DASH = 0, PG_CHART, PG_STATS, PG_TEMP, PG_BREW, PG_COLDEX, PG_SERVICE, PG_PROFILE, PG_LIGHT, PG_WIFI, PG_DISPLAY, PG_INFO, PG_COUNT };
static const char* PAGE_NAMES[PG_COUNT] = { "Dashboard", "Verlauf", "Statistik", "Temperatur", "Brew-Control", "Cold Extraction", "Reinigung/Wartung", "Profile", "Licht", "WiFi", "Darstellung", "Info" };
static lv_obj_t* g_content = nullptr;
static lv_obj_t* g_pages[PG_COUNT] = { nullptr };
static lv_obj_t* g_drawer = nullptr;
@@ -197,8 +201,6 @@ static lv_obj_t* g_profPickerList = nullptr; // Button-Liste im Overlay
static lv_obj_t* g_profPickerInfo = nullptr; // "Lade Profile..."-Hinweis im Overlay
static lv_obj_t* g_infoRow = nullptr; // untere Zeile; waechst waehrend eines Bezugs
static bool g_dashBrewMode = false;
static const lv_font_t* g_shotFontNormal = nullptr;
static const lv_font_t* g_bezugFontNormal = nullptr;
static lv_obj_t* cellShot; static lv_obj_t* lblShotTimer; static lv_obj_t* g_bbtBar = nullptr;
static lv_obj_t* cellSteam; static lv_obj_t* lblSteamTimer; static lv_obj_t* g_sbtBar = nullptr;
static lv_obj_t* cellBezug; static lv_obj_t* lblBezug; static lv_obj_t* g_bbwBar = nullptr;
@@ -691,6 +693,32 @@ static void stat_list_finish(int used) {
}
}
// Groesse der Standby-Uhr. Anders als die allgemeine Schriftgroesse braucht diese keinen
// Neustart: Es haengt an genau EINEM Textfeld, das sich sofort umstellen laesst.
enum { UHR_KLEIN = 0, UHR_MITTEL = 1, UHR_GROSS = 2 };
static uint8_t g_clockSize = UHR_GROSS;
static lv_obj_t* g_clockSizeBtn[3] = {};
static const lv_font_t* clock_font() {
switch (g_clockSize) {
#if JC_PANEL_TYPE == WS_PANEL_7H
// 7 Zoll, 1280x720: Hier war selbst die groesste Stufe noch zu klein.
case UHR_KLEIN: return &lv_font_clock_180;
case UHR_MITTEL: return &lv_font_clock_270;
default: return &lv_font_clock_360;
#else
// 4,3 Zoll, 480x480: Groessere Stufen passen schlicht nicht auf das Panel.
case UHR_KLEIN: return &lv_font_clock_120;
case UHR_MITTEL: return &lv_font_clock_180;
default: return &lv_font_clock_240;
#endif
}
}
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);
@@ -902,7 +930,14 @@ 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
// Vermessen: Vom Tippen bis zur sichtbaren Rueckfrage vergehen rund fuenf Sekunden.
uint32_t t0 = lv_tick_get();
esp_rom_printf("[Touch] %u ms: Tipp auf die Standby-Uhr erkannt\n", (unsigned)t0);
wake_confirm_show();
esp_rom_printf("[Touch] %u ms: Rueckfrage aufgebaut, brauchte %u ms\n",
(unsigned)lv_tick_get(), (unsigned)(lv_tick_get() - t0));
}
static void wake_cancel() { // Abbrechen -> zurueck in die Standby-Helligkeit
wake_timeout_stop();
wake_confirm_hide();
@@ -967,8 +1002,11 @@ static lv_color_t temp_state_color(float temp, float set) {
static void temp_progress_set(lv_obj_t* bar, lv_obj_t* arc, lv_obj_t* bulb, int pct, lv_color_t col) {
if (pct > 100) pct = 100; else if (pct < 0) pct = 0;
if (bar) {
lv_bar_set_value(bar, pct, LV_ANIM_OFF);
lv_obj_set_style_bg_color(bar, col, LV_PART_INDICATOR);
// Wert und Farbe nur bei echter Aenderung setzen - beides macht den Balken sonst
// bei jeder Zustandsmeldung ungueltig, obwohl sich meist nichts bewegt hat.
if (lv_bar_get_value(bar) != pct) lv_bar_set_value(bar, pct, LV_ANIM_OFF);
if (lv_color_to_u32(lv_obj_get_style_bg_color(bar, LV_PART_INDICATOR)) != lv_color_to_u32(col))
lv_obj_set_style_bg_color(bar, col, LV_PART_INDICATOR);
}
if (arc) {
lv_arc_set_value(arc, pct);
@@ -1034,6 +1072,18 @@ static lv_obj_t* tempField(lv_obj_t* parent, const char* title, lv_color_t accen
lv_label_set_text(*big, "--.-");
lv_obj_set_style_text_color(*big, COL_TEXT, 0);
lv_obj_set_style_text_font(*big, valueFont, 0);
// Feste Breite, Text zentriert. Waechst oder schrumpft eine Beschriftung mit ihrem
// Inhalt - etwa von "92,4 °C" auf "100,1 °C" -, rechnet LVGL das Flex-Layout der
// gesamten Seite neu und macht sie damit komplett ungueltig. Genau das war im
// Messprotokoll als "x 0..1279, y 44..719" zu sehen: der ganze Inhaltsbereich, bei
// jeder Zustandsmeldung. Mit fester Breite bleibt nur die Beschriftung selbst ungueltig.
lv_obj_set_width(*big, LV_PCT(100));
lv_obj_set_style_text_align(*big, LV_TEXT_ALIGN_CENTER, 0);
// Auch die HOEHE muss fest sein. LVGL prueft beim Setzen eines Textes, ob sich die
// Eigengroesse der Beschriftung geaendert haben koennte - ist eines der beiden Masse
// frei, gilt das Layout des Elternteils als neu zu berechnen, und das macht die ganze
// Seite ungueltig. Erst mit festen Massen bleibt nur die Beschriftung selbst ungueltig.
lv_obj_set_height(*big, lv_font_get_line_height(valueFont) + 6);
// Grosse Zahlen etwas enger setzen. Bei dieser Groesse wirken die Werksabstaende
// auseinandergezogen; ein paar Pixel weniger binden die Ziffern zu einer Zahl zusammen.
if (valueFont == &lv_font_maven_pro_bold_140) lv_obj_set_style_text_letter_space(*big, -3, 0);
@@ -1043,6 +1093,9 @@ static lv_obj_t* tempField(lv_obj_t* parent, const char* title, lv_color_t accen
lv_label_set_text(*setp, "Soll --.-");
lv_obj_set_style_text_color(*setp, COL_TEXT_DIM, 0);
lv_obj_set_style_text_font(*setp, font_text(), 0);
lv_obj_set_width(*setp, LV_PCT(100));
lv_obj_set_style_text_align(*setp, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_height(*setp, lv_font_get_line_height(font_text()) + 6);
*bar = lv_bar_create(c);
lv_obj_set_size(*bar, LV_PCT(92), 8);
@@ -1188,6 +1241,10 @@ static lv_obj_t* infoCell(lv_obj_t* parent, const char* title, lv_obj_t** val, l
lv_obj_set_style_text_color(*val, COL_TEXT, 0);
lv_obj_set_style_text_font(*val, font_title(), 0);
lv_obj_set_style_text_align(*val, LV_TEXT_ALIGN_CENTER, 0); // fuer mehrzeilige Werte
// Feste Masse in BEIDE Richtungen - siehe die ausfuehrliche Begruendung in tempField.
// Zwei Zeilen Platz, weil manche Werte zweizeilig sind ("32,4 g / 36,0 g").
lv_obj_set_width(*val, LV_PCT(100));
lv_obj_set_height(*val, lv_font_get_line_height(font_title()) * 2 + 6);
return card;
}
// Tipp auf die Statuszeile: Wartung -> Serviceseite, Reinigung -> Sub-Screen,
@@ -1300,10 +1357,6 @@ static void build_dashboard(lv_obj_t* p) {
lv_bar_set_range(g_bbwBar, 0, 100);
lv_bar_set_value(g_bbwBar, 0, LV_ANIM_OFF);
lv_obj_add_flag(g_bbwBar, LV_OBJ_FLAG_HIDDEN);
// Normalschriften merken, damit der Bezugsmodus sie wieder herstellen kann
g_shotFontNormal = lv_obj_get_style_text_font(lblShotTimer, LV_PART_MAIN);
g_bezugFontNormal = lv_obj_get_style_text_font(lblBezug, LV_PART_MAIN);
cellFlow = infoCell(ir, "FLOW", &lblFlow, lv_color_hex(0x64b5f6));
lv_obj_add_flag(cellFlow, LV_OBJ_FLAG_CLICKABLE); // antippbar -> Brew-Control
lv_obj_add_event_cb(cellFlow, brew_cell_cb, LV_EVENT_CLICKED, nullptr);
@@ -1363,7 +1416,7 @@ static void flush_targ() {
static void targ_w_set(float v) {
if (v < 0) v = 0; if (v > 135) v = 135;
g_targW = v;
if (lblTargW) lv_label_set_text(lblTargW, tempStr(g_targW, 0).c_str());
if (lblTargW) label_set_if_changed(lblTargW, tempStr(g_targW, 0).c_str());
if (g_arcW) lv_arc_set_value(g_arcW, (int32_t)(g_targW + 0.5f));
g_targWDirty = true;
schedule_targ_send();
@@ -1371,7 +1424,7 @@ static void targ_w_set(float v) {
static void targ_d_set(float v) {
if (v < 0) v = 0; if (v > 200) v = 200;
g_targD = v;
if (lblTargD) lv_label_set_text(lblTargD, tempStr(g_targD, 0).c_str());
if (lblTargD) label_set_if_changed(lblTargD, tempStr(g_targD, 0).c_str());
if (g_arcD) lv_arc_set_value(g_arcD, (int32_t)(g_targD + 0.5f));
g_targDDirty = true;
schedule_targ_send();
@@ -3041,6 +3094,18 @@ static void design_btn_cb(lv_event_t* e) {
// Live-Bezugsschirm ein-/ausschalten (P4-lokal in NVS gespeichert)
// Temperaturzahl einfaerben oder weiss lassen. Wirkt sofort - die Farbe wird bei jeder
// Zustandsmeldung neu gesetzt, es muss nichts neu aufgebaut werden.
// Groesse der Standby-Uhr waehlen. Wirkt sofort - es haengt an einem einzigen Textfeld.
static void clock_size_cb(lv_event_t* e) {
g_clockSize = (uint8_t)(intptr_t)lv_event_get_user_data(e);
{ Preferences prefs; prefs.begin("ui", false); prefs.putUChar("uhrgr", g_clockSize); prefs.end(); }
clock_size_apply();
for (int i = 0; i < 3; i++) if (g_clockSizeBtn[i]) {
bool an = (i == g_clockSize);
lv_obj_set_style_bg_color(g_clockSizeBtn[i], an ? COL_ACCENT : COL_CARD2, 0);
lv_obj_set_style_text_color(g_clockSizeBtn[i], an ? lv_color_hex(0x000000) : COL_TEXT, 0);
}
}
static void temp_color_sw_cb(lv_event_t* e) {
g_tempColored = sw_get((lv_obj_t*)lv_event_get_target(e));
{ Preferences prefs; prefs.begin("ui", false); prefs.putBool("tempcol", g_tempColored); prefs.end(); }
@@ -3051,8 +3116,10 @@ static void brew_live_sw_cb(lv_event_t* e) {
{ Preferences prefs; prefs.begin("ui", false); prefs.putBool("brewlive", g_brewLiveEnabled); prefs.end(); }
if (!g_brewLiveEnabled) brew_live_hide(); // laeuft gerade ein Bezug: sofort ausblenden
}
static void build_info(lv_obj_t* p) {
section_title(p, "Darstellung");
// Eigener Menuepunkt "Darstellung". Die Einstellungen sassen frueher oben auf der
// Info-Seite; dort standen sie zwischen Firmware-Staenden und Verbindungsdaten,
// mit denen sie nichts zu tun haben.
static void build_display(lv_obj_t* p) {
lv_obj_t* dcard = th_card(p);
lv_obj_set_width(dcard, LV_PCT(100));
lv_obj_set_height(dcard, LV_SIZE_CONTENT);
@@ -3083,6 +3150,20 @@ static void build_info(lv_obj_t* p) {
lv_obj_add_event_cb(fb, font_pick_cb, LV_EVENT_CLICKED, (void*)(intptr_t)i);
}
lv_obj_t* cl = lv_label_create(dcard);
lv_label_set_text(cl, "Uhrgröße im Standby");
lv_obj_set_style_text_color(cl, COL_TEXT_DIM, 0);
lv_obj_t* crow = row(dcard, nullptr);
lv_obj_set_style_pad_column(crow, 8, 0);
static const char* UHR_NAMEN[3] = { "Klein", "Mittel", "Groß" };
for (uint8_t i = 0; i < 3; i++) {
lv_obj_t* b = (i == g_clockSize) ? th_accent_button(crow, UHR_NAMEN[i])
: th_button(crow, UHR_NAMEN[i], COL_CARD2, COL_TEXT);
lv_obj_set_flex_grow(b, 1);
lv_obj_add_event_cb(b, clock_size_cb, LV_EVENT_CLICKED, (void*)(intptr_t)i);
g_clockSizeBtn[i] = b;
}
lv_obj_t* swTempCol = add_switch(dcard, "Temperaturzahl in der Zustandsfarbe");
sw_set(swTempCol, g_tempColored);
lv_obj_add_event_cb(swTempCol, temp_color_sw_cb, LV_EVENT_VALUE_CHANGED, nullptr);
@@ -3091,6 +3172,9 @@ static void build_info(lv_obj_t* p) {
sw_set(swBrewLive, g_brewLiveEnabled);
lv_obj_add_event_cb(swBrewLive, brew_live_sw_cb, LV_EVENT_VALUE_CHANGED, nullptr);
}
static void build_info(lv_obj_t* p) {
section_title(p, "Firmware-Stände & Verbindung");
lv_obj_t* card = th_card(p);
@@ -3200,10 +3284,35 @@ static void eco_case_timer_cb(lv_timer_t*) {
// =====================================================================================
// Setzt den Labeltext nur, wenn er sich aendert -> verhindert unnoetige Invalidierung
// (im Full-Refresh-Modus loest jede Aenderung ein komplettes Neuzeichnen aus).
// LVGL macht bei jedem Setzen den Bereich der Beschriftung ungueltig - auch dann, wenn sich
// der Text gar nicht geaendert hat. Auf dem Dashboard werden im Sekundentakt Werte quer ueber
// den ganzen Bildschirm gesetzt; LVGL fasst die ungueltigen Bereiche zu einem zusammen, und
// das ist dann die gesamte Flaeche. Gemessen waren das 240 bis 300 ms pro Sekunde, in denen
// die Oberflaeche mit nichts anderem beschaeftigt war. Deshalb wird ueberall dort, wo sich
// Werte nur selten aendern, vorher verglichen.
static void label_set_if_changed(lv_obj_t* lbl, const char* txt) {
if (!lbl || !txt) return;
const char* cur = lv_label_get_text(lbl);
if (!cur || strcmp(cur, txt) != 0) lv_label_set_text(lbl, txt);
}
// Wie oben, aber mit Formatangabe.
static void label_fmt_if_changed(lv_obj_t* lbl, const char* fmt, ...) {
if (!lbl) return;
char buf[96];
va_list ap; va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
label_set_if_changed(lbl, buf);
}
// Auch ein Farbwechsel macht den Bereich ungueltig. Gleiche Farbe -> nichts tun.
static void label_color_if_changed(lv_obj_t* lbl, lv_color_t col) {
if (!lbl) return;
lv_color_t cur = lv_obj_get_style_text_color(lbl, LV_PART_MAIN);
if (lv_color_to_u32(cur) != lv_color_to_u32(col))
lv_obj_set_style_text_color(lbl, col, 0);
}
// Pre-Infusion-Phasenleiste aktualisieren (100-ms-Takt aus dash_timers_cb).
// Die aktive Phase kommt bevorzugt von der S3 (piState = echte Statemachine, Quelle der
// Wahrheit; Push-Latenz max. 250 ms). Sendet eine aeltere S3-Firmware das Feld noch nicht
@@ -3412,6 +3521,102 @@ static void dash_timers_cb(lv_timer_t*) {
// sonst greifen sie auf nicht angelegte LVGL-Objekte zu.
static bool g_uiReady = false;
// ------------------------------------------------------------------------------------
// Zeitmessung fuer langsame Bildaufbauten
//
// Beim 7-Zoll-Panel dauern manche Vorgaenge spuerbar lange, und aus dem Quelltext allein
// ist nicht zu sehen, welcher Teil die Zeit frisst. LVGL meldet Anfang und Ende jedes
// Zeichenzyklus - hier wird die Dauer gemessen und nur dann ausgegeben, wenn sie eine
// Schwelle ueberschreitet. Im Normalbetrieb bleibt die Konsole still.
// ------------------------------------------------------------------------------------
#define REFR_WARN_MS 200
static uint32_t g_refrStart = 0;
static uint32_t g_refrPixels = 0;
static uint32_t g_refrAreas = 0;
static lv_area_t g_refrArea[6];
static uint8_t g_refrAreaCnt = 0;
// Beim Start eines Zeichenzyklus festhalten, WIE VIEL neu gezeichnet wird. Erst damit laesst
// sich unterscheiden, ob LVGL nur kleine Bereiche anfasst - dann ist das Zeichnen an sich zu
// langsam - oder ob jedes Mal der ganze Bildschirm neu entsteht, weil ihn etwas unnoetig
// ungueltig macht.
static void refr_start_cb(lv_event_t*) {
g_refrStart = lv_tick_get();
g_refrPixels = 0;
g_refrAreas = 0;
g_refrAreaCnt = 0;
lv_display_t* d = lv_display_get_default();
if (!d) return;
for (uint16_t i = 0; i < d->inv_p; i++) {
if (d->inv_area_joined[i]) continue;
const lv_area_t* a = &d->inv_areas[i];
g_refrPixels += (uint32_t)(a->x2 - a->x1 + 1) * (uint32_t)(a->y2 - a->y1 + 1);
if (g_refrAreaCnt < 6) g_refrArea[g_refrAreaCnt++] = *a;
g_refrAreas++;
}
}
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] %u ms: Bildaufbau %u ms, %u Bereiche, %u %% der Flaeche\n", (unsigned)lv_tick_get(),
(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.
for (uint8_t i = 0; i < g_refrAreaCnt; i++)
esp_rom_printf(" Bereich %u: x %d..%d, y %d..%d\n", (unsigned)i,
(int)g_refrArea[i].x1, (int)g_refrArea[i].x2,
(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] %u ms: Herzschlag, LVGL-Speicher frei %u von %u Byte, groesster Block %u\n",
(unsigned)lv_tick_get(), (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() {
lv_display_t* d = lv_display_get_default();
if (!d) return;
lv_display_add_event_cb(d, refr_start_cb, LV_EVENT_REFR_START, nullptr);
lv_display_add_event_cb(d, refr_ready_cb, LV_EVENT_REFR_READY, nullptr);
}
void ui_init(ProtocolClient* client) {
g_client = client;
@@ -3430,6 +3635,8 @@ void ui_init(ProtocolClient* client) {
if (g_tempDesign >= TD_DESIGN_COUNT) g_tempDesign = TD_BAND;
g_brewLiveEnabled = prefs.getBool("brewlive", true);
g_tempColored = prefs.getBool("tempcol", false);
g_clockSize = prefs.getUChar("uhrgr", UHR_GROSS);
if (g_clockSize > UHR_GROSS) g_clockSize = UHR_GROSS;
g_chartWin = prefs.getUChar("chartwin", 0);
if (g_chartWin > 3) g_chartWin = 0;
g_chartDuty = prefs.getBool("chartduty", false);
@@ -3520,6 +3727,11 @@ void ui_init(ProtocolClient* client) {
lv_obj_set_style_text_color(g_clock, COL_TEXT, 0);
lv_obj_set_style_pad_right(g_clock, 10, 0);
lv_obj_set_style_text_font(g_clock, font_text(), 0);
// Feste Masse, sonst rechnet LVGL beim Wechsel der Minute das Layout der Kopfleiste neu
// und macht sie samt Menue-Knopf ungueltig (siehe tempField).
lv_obj_set_width(g_clock, 130);
lv_obj_set_height(g_clock, lv_font_get_line_height(font_text()) + 4);
lv_obj_set_style_text_align(g_clock, LV_TEXT_ALIGN_RIGHT, 0);
g_wifiIcon = lv_label_create(header);
lv_label_set_text(g_wifiIcon, LV_SYMBOL_WIFI);
@@ -3547,22 +3759,18 @@ void ui_init(ProtocolClient* client) {
g_pages[PG_SERVICE] = make_page(g_content); build_service(g_pages[PG_SERVICE]);
g_pages[PG_WIFI] = make_page(g_content); build_wifi(g_pages[PG_WIFI]);
g_pages[PG_PROFILE] = make_page(g_content); build_profiles(g_pages[PG_PROFILE]);
g_pages[PG_DISPLAY] = make_page(g_content); build_display(g_pages[PG_DISPLAY]);
g_pages[PG_INFO] = make_page(g_content); build_info(g_pages[PG_INFO]);
// --- Schublade (Scrim + Menue-Panel) ---
g_drawer = lv_obj_create(scr);
lv_obj_set_size(g_drawer, LV_PCT(100), DISP_VER_RES - 44);
lv_obj_set_pos(g_drawer, 0, 44);
#if JC_PANEL_TYPE == WS_PANEL_7H
// Deckend statt halbdurchsichtig - aus demselben Grund wie beim Aufweckdialog: Ein
// durchscheinender Vorhang laesst LVGL die ganze Flaeche darunter mitzeichnen und
// mischen. Auf dunklem Grund sieht ein dunkles Deckend praktisch genauso aus.
lv_obj_set_style_bg_color(g_drawer, lv_color_hex(0x0d0d0d), 0);
lv_obj_set_style_bg_opa(g_drawer, LV_OPA_COVER, 0);
#else
// Bewusst durchscheinend: Der Vorhang legt sich ueber die ganze Seite, und der Inhalt
// dahinter soll sichtbar bleiben. Ein Versuch mit deckendem Vorhang (gegen die
// Zeichenlast) hat den Seiteninhalt verschwinden lassen und ist zurueckgenommen.
lv_obj_set_style_bg_color(g_drawer, lv_color_hex(0x000000), 0);
lv_obj_set_style_bg_opa(g_drawer, LV_OPA_50, 0);
#endif
lv_obj_set_style_border_width(g_drawer, 0, 0);
lv_obj_set_style_pad_all(g_drawer, 0, 0);
lv_obj_set_style_radius(g_drawer, 0, 0);
@@ -3629,7 +3837,7 @@ void ui_init(ProtocolClient* client) {
g_standbyClock = lv_label_create(g_standbyScreen);
lv_label_set_text(g_standbyClock, "--:--");
lv_obj_set_style_text_color(g_standbyClock, COL_TEXT, 0);
lv_obj_set_style_text_font(g_standbyClock, &lv_font_clock_240, 0); // grosse Uhr im Standby
lv_obj_set_style_text_font(g_standbyClock, clock_font(), 0); // Groesse einstellbar
lv_obj_center(g_standbyClock);
lv_obj_add_event_cb(g_standbyScreen, standby_screen_cb, LV_EVENT_CLICKED, nullptr); // Tippen -> Aufweck-Abfrage
lv_obj_t* standbyHint = lv_label_create(g_standbyScreen); // dezenter Hinweis am unteren Rand
@@ -3683,12 +3891,17 @@ void ui_init(ProtocolClient* client) {
lv_obj_set_flex_flow(wakeBtns, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(wakeBtns, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_clear_flag(wakeBtns, LV_OBJ_FLAG_SCROLLABLE);
// Knopfbreite und -schrift wachsen mit der Schriftgroesse mit; mit fester Breite und
// geerbter Grundschrift wirkten die Knoepfe neben dem groesseren Text verloren.
const int32_t wakeBtnW = (g_fontStep == FONT_GROSS) ? 280 : (g_fontStep == FONT_MITTEL) ? 210 : 150;
lv_obj_t* wakeNo = th_button(wakeBtns, "Abbrechen", COL_CARD2, COL_TEXT);
lv_obj_set_width(wakeNo, 150);
lv_obj_set_width(wakeNo, wakeBtnW);
lv_obj_set_style_text_font(wakeNo, font_text(), 0);
lv_obj_add_event_cb(wakeNo, wake_no_cb, LV_EVENT_CLICKED, nullptr);
g_wakeNoBtn = wakeNo;
lv_obj_t* wakeYes = th_accent_button(wakeBtns, "Aufwecken");
lv_obj_set_width(wakeYes, 150);
lv_obj_set_width(wakeYes, wakeBtnW);
lv_obj_set_style_text_font(wakeYes, font_text(), 0);
lv_obj_add_event_cb(wakeYes, wake_yes_cb, LV_EVENT_CLICKED, nullptr);
g_wakeYesLbl = lv_obj_get_child(wakeYes, 0); // Label des Knopfs (fuer "Espresso" im Abfrage-Modus)
// Zweite Zeile: direkt in die kalte Extraktion aufwecken. Ohne diesen Weg muesste man
@@ -3696,6 +3909,7 @@ void ui_init(ProtocolClient* client) {
// Nur sichtbar, wenn die S3 die Funktion freigeschaltet meldet (siehe ui_update).
g_wakeColdBtn = th_button(wakeCard, "Cold Extraction (Wasser bleibt kalt)", COL_CARD2, COL_ACCENT);
lv_obj_set_width(g_wakeColdBtn, LV_PCT(100));
lv_obj_set_style_text_font(g_wakeColdBtn, font_text(), 0);
lv_obj_add_event_cb(g_wakeColdBtn, wake_cold_cb, LV_EVENT_CLICKED, nullptr);
lv_obj_add_flag(g_wakeColdBtn, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(g_wakeConfirm, LV_OBJ_FLAG_HIDDEN);
@@ -3806,6 +4020,12 @@ void ui_init(ProtocolClient* client) {
show_page(PG_DASH);
if (g_client) g_client->sendListProfiles();
// 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;
refr_monitor_install();
lv_timer_create(ui_heartbeat_cb, 2000, nullptr);
g_uiReady = true;
}
@@ -3872,10 +4092,14 @@ static void ready_band_update(const MachineState& st) {
else if (wOk) { txt = "Wasser bereit — Dampf heizt"; bg = 0x3a2a05; fg = COL_ACCENT; }
else { txt = "Heizt auf"; bg = 0x3a2a05; fg = COL_ACCENT; }
}
lv_label_set_text(g_readyBandLbl, txt);
lv_obj_set_style_text_color(g_readyBandLbl, fg, 0);
lv_obj_set_style_bg_color(g_readyBand, lv_color_hex(bg), 0);
lv_obj_set_style_bg_opa(g_readyBand, LV_OPA_COVER, 0);
// Das Band spannt sich ueber die volle Breite. Wuerde es bei jeder Zustandsmeldung neu
// gesetzt, machte es allein schon einen Streifen ueber den ganzen Bildschirm ungueltig.
label_set_if_changed(g_readyBandLbl, txt);
label_color_if_changed(g_readyBandLbl, fg);
if (lv_color_to_u32(lv_obj_get_style_bg_color(g_readyBand, LV_PART_MAIN)) != bg) {
lv_obj_set_style_bg_color(g_readyBand, lv_color_hex(bg), 0);
lv_obj_set_style_bg_opa(g_readyBand, LV_OPA_COVER, 0);
}
}
@@ -3898,12 +4122,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);
if (lblShotTimer && g_shotFontNormal)
lv_obj_set_style_text_font(lblShotTimer, on ? &lv_font_maven_pro_bold_96 : g_shotFontNormal, 0);
if (lblBezug && g_bezugFontNormal)
lv_obj_set_style_text_font(lblBezug, on ? &lv_font_maven_pro_bold_96 : g_bezugFontNormal, 0);
// 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) {
@@ -3916,6 +4142,7 @@ static void dash_brew_mode_tick(const MachineState& st) {
}
// ------------------------------------------------------------------------------------
// Uhr läuft lokal weiter
//
@@ -3967,7 +4194,7 @@ void ui_update(const MachineState& st) {
// WLAN-Symbol
bool wifiOk = st.wifiConnected && !st.apMode;
if (g_wifiIcon) lv_obj_set_style_text_color(g_wifiIcon, wifiOk ? COL_OK : (st.apMode ? COL_WARN : COL_TEXT_MUT), 0);
label_color_if_changed(g_wifiIcon, wifiOk ? COL_OK : (st.apMode ? COL_WARN : COL_TEXT_MUT));
// Uhrzeit. Angezeigt wird die der Hauptplatine; faellt sie voruebergehend aus, zaehlt
// der P4 die zuletzt bekannte Zeit selbst weiter (siehe clock_text).
@@ -3990,8 +4217,14 @@ 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 -
@@ -4258,43 +4491,43 @@ void ui_update(const MachineState& st) {
// Dashboard - Temperaturkarten (im Fehlerfall Warnsymbol + Klartext statt Zahl, wie OLED)
if (lblTempW) {
if (st.wasserSafetyShutdown || st.wasserSensorError) {
lv_label_set_text(lblTempW, LV_SYMBOL_WARNING);
lv_obj_set_style_text_color(lblTempW, COL_DANGER, 0);
lv_label_set_text(lblSetW, st.wasserSafetyShutdown ? "Sicherheitsabsch." : "Sensorfehler!");
lv_obj_set_style_text_color(lblSetW, COL_DANGER, 0);
label_set_if_changed(lblTempW, LV_SYMBOL_WARNING);
label_color_if_changed(lblTempW, COL_DANGER);
label_set_if_changed(lblSetW, st.wasserSafetyShutdown ? "Sicherheitsabsch." : "Sensorfehler!");
label_color_if_changed(lblSetW, COL_DANGER);
temp_progress_set(barDutyW, arcTempW, bulbTempW, 0, COL_DANGER);
} else {
lv_label_set_text(lblTempW, tempStr(st.tempW, 1).c_str());
label_set_if_changed(lblTempW, tempStr(st.tempW, 1).c_str());
if (st.cxActive) {
// Cold Extraction: der Sollwert ist bedeutungslos, es wird nicht geheizt.
// Statt eines Zielbands, das nie erreicht wird, den Grund anzeigen.
lv_obj_set_style_text_color(lblTempW, temp_value_color(lv_color_hex(0x1976d2)), 0);
label_color_if_changed(lblTempW, temp_value_color(lv_color_hex(0x1976d2)));
label_set_if_changed(lblSetW, "Cold Extraction - Heizen aus");
lv_obj_set_style_text_color(lblSetW, lv_color_hex(0x1976d2), 0);
label_color_if_changed(lblSetW, lv_color_hex(0x1976d2));
temp_progress_set(barDutyW, arcTempW, bulbTempW, 0, lv_color_hex(0x1976d2));
} else {
// Ist-Wert faerbt sich weich Richtung Soll (Blau -> Gold -> Gruen im Zielband)
lv_color_t colW = temp_state_color(st.tempW, st.setW);
lv_obj_set_style_text_color(lblTempW, temp_value_color(colW), 0);
lv_label_set_text(lblSetW, ("Soll " + tempStr(st.setW, 1)).c_str());
lv_obj_set_style_text_color(lblSetW, COL_TEXT_DIM, 0);
label_color_if_changed(lblTempW, temp_value_color(colW));
label_set_if_changed(lblSetW, ("Soll " + tempStr(st.setW, 1)).c_str());
label_color_if_changed(lblSetW, COL_TEXT_DIM);
// Fortschritt = Ist/Soll in %, gedeckelt auf 100 (Ist >= Soll -> voll)
int pctW = (st.setW > 0.0f) ? (int)(st.tempW / st.setW * 100.0f + 0.5f) : 0;
temp_progress_set(barDutyW, arcTempW, bulbTempW, pctW, colW);
}
}
if (st.dampfSafetyShutdown || st.dampfSensorError) {
lv_label_set_text(lblTempD, LV_SYMBOL_WARNING);
lv_obj_set_style_text_color(lblTempD, COL_DANGER, 0);
lv_label_set_text(lblSetD, st.dampfSafetyShutdown ? "Sicherheitsabsch." : "Sensorfehler!");
lv_obj_set_style_text_color(lblSetD, COL_DANGER, 0);
label_set_if_changed(lblTempD, LV_SYMBOL_WARNING);
label_color_if_changed(lblTempD, COL_DANGER);
label_set_if_changed(lblSetD, st.dampfSafetyShutdown ? "Sicherheitsabsch." : "Sensorfehler!");
label_color_if_changed(lblSetD, COL_DANGER);
temp_progress_set(barDutyD, arcTempD, bulbTempD, 0, COL_DANGER);
} else {
lv_label_set_text(lblTempD, tempStr(st.tempD, 0).c_str());
label_set_if_changed(lblTempD, tempStr(st.tempD, 0).c_str());
lv_color_t colD = temp_state_color(st.tempD, st.setD);
lv_obj_set_style_text_color(lblTempD, temp_value_color(colD), 0);
lv_label_set_text(lblSetD, ("Soll " + tempStr(st.setD, 0)).c_str());
lv_obj_set_style_text_color(lblSetD, COL_TEXT_DIM, 0);
label_color_if_changed(lblTempD, temp_value_color(colD));
label_set_if_changed(lblSetD, ("Soll " + tempStr(st.setD, 0)).c_str());
label_color_if_changed(lblSetD, COL_TEXT_DIM);
int pctD = (st.setD > 0.0f) ? (int)(st.tempD / st.setD * 100.0f + 0.5f) : 0;
temp_progress_set(barDutyD, arcTempD, bulbTempD, pctD, colD);
}
@@ -4443,12 +4676,12 @@ void ui_update(const MachineState& st) {
float wDisp = st.weight;
if (wDisp > -0.05f && wDisp <= 0.0f) wDisp = 0.0f;
if (!st.scaleEnabled) {
lv_label_set_text(lblBezug, "--");
label_set_if_changed(lblBezug, "--");
} else if (bbwShot) {
// Waehrend des Bezugs mit Brew-by-Weight: Ist oben, Ziel darunter (zweizeilig)
lv_label_set_text_fmt(lblBezug, "%.1f g\n/ %.1f g", wDisp, shotTarget);
label_fmt_if_changed(lblBezug, "%.1f g\n/ %.1f g", wDisp, shotTarget);
} else {
lv_label_set_text_fmt(lblBezug, "%.1f g", wDisp);
label_fmt_if_changed(lblBezug, "%.1f g", wDisp);
}
// Fortschrittsbalken Gewicht/Ziel (nur waehrend BBW-Bezug)
if (g_bbwBar) {