Compare commits

...
Author SHA1 Message Date
raw-designsandClaude Opus 5 cf57e11ccb fix(P4): Übergänge ohne Animation, kein blaues Aufblitzen, Temperaturfarbe wählbar (1.8.4)
- Standby-Uhr und Aufweckdialog kamen spürbar verzögert. Ursache waren die
  Fade-Animationen: Eine Deckkraft-Animation ist bei einem Bildspeicher in
  RGB888 das Teuerste überhaupt, weil pro Einzelbild die ganze Fläche unter
  der Ebene neu gezeichnet und gemischt werden muss. UI_FADE_MS ist für
  WS_PANEL_7H auf 0; ui_fade_in/ui_fade_out_hide zeigen bzw. verstecken dann
  direkt. Die JC-Panels behalten die Animation.
- Beim Wechsel der Schriftgröße blitzte Blau auf. Das ist das Panel selbst -
  ohne Videosignal zeigt es Blau, und der nötige Neustart reißt das Signal
  ab. hal_backlight(0) vor esp_restart().
- Neue Einstellung "Temperaturzahl in der Zustandsfarbe" (Info-Seite,
  Abschnitt Darstellung, NVS ui/tempcol, Vorgabe aus). Aus bleibt die Zahl
  weiß; die Zustandsfarbe steckt dann allein im Balken. Warnungen bleiben
  immer rot.

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 00:03:58 +02:00
thomas 6d345d2145 Merge pull request 'fix(P4): Fehlendes °C, unsichtbarer Verlauf, fette Zahlen (1.8.3)' (#63) from fix/dashboard-schrift-und-verlauf into main 2026-09-02 20:12:52 +02:00
raw-designsandClaude Opus 5 a410da6e72 fix(P4): Fehlendes °C, unsichtbarer Verlauf, fette Zahlen (1.8.3)
Drei Punkte am neuen Dashboard:

- Hinter der Temperatur stand ein leeres Rechteck statt des C. Die großen
  Schriften waren auf Ziffern, Komma, Punkt, Minus und Gradzeichen
  beschränkt; C fehlte. Jetzt enthalten, ebenso g und s für die Anzeige
  während eines Bezugs.
- Bei "Verlauf" war keine Kurve zu sehen: Ein frisches lv_chart hat lauter
  LV_CHART_POINT_NONE und zeichnet nichts, und ohne Hintergrund war der
  Bereich unsichtbar, bis nach einer Minute Messwerte zusammenkamen. Die
  Kurve startet jetzt beim aktuellen Wert (lv_chart_set_all_value), Bereich
  und Fläche sind sofort gesetzt.
- Die großen Zahlen stehen im fetten Schnitt und mit engerem Zeichenabstand,
  wie im Entwurf. Erzeugt aus MavenPro-VariableFont bei wght=700 via
  fontTools varLib.instancer; dieselbe Schriftart, nur kräftiger. Die
  Dateien heißen entsprechend lv_font_maven_pro_bold_96/_140.

Beide Panel-Varianten mit arduino-cli gegengebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 20:12:32 +02:00
thomas f0a214d712 Merge pull request 'feat(P4): Letzte Bezüge als Tabelle statt Textblock (1.8.2)' (#62) from feat/statistik-tabelle into main 2026-09-02 19:52:14 +02:00
raw-designsandClaude Opus 5 21f273aae4 feat(P4): Letzte Bezüge als Tabelle statt Textblock (1.8.2)
Die Liste war ein einziges mehrzeiliges Label: Zeitpunkt, Dauer und Gewicht
durch Leerzeichen getrennt aneinandergereiht. Weil Zahlen unterschiedlich
breit sind, stand nichts untereinander - zwei Bezüge ließen sich nicht
vergleichen, ohne jede Zeile einzeln zu lesen.

Jetzt eine Tabelle mit festen Spalten (Zeitpunkt / Dauer / Gewicht / kalt),
Ziffern gleicher Breite, Kopfzeile und leicht abgesetzten wechselnden
Zeilen statt Trennlinien. Ohne Waage bezogen bleibt die Gewichtsspalte leer
statt eine Null vorzutäuschen.

Die Zeilen werden einmal angelegt und danach nur gefüllt oder ausgeblendet -
kein Neuanlegen bei jeder Aktualisierung.

Beide Panel-Varianten mit arduino-cli gegengebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 19:51:57 +02:00
thomas e42d2b3de2 Merge pull request 'feat(P4): Bezugsanzeige für alle vier Darstellungen (1.8.1)' (#61) from feat/bezugsanzeige into main 2026-09-02 19:41:07 +02:00
raw-designsandClaude Opus 5 785f64331d feat(P4): Bezugsanzeige für alle vier Darstellungen (1.8.1)
Sobald ein Bezug oder Dampfbezug läuft, schrumpft der Temperaturbereich
(flex-grow 3 -> 1) und die untere Zeile wächst (2 -> 3); Shot-Timer und
Gewicht wechseln auf 96 px. Während eines Bezugs zählen Zeit und Gewicht,
die Kesseltemperaturen sind dann Nebensache.

Keine zweite Ansicht: Die Seite verschiebt nur das Gewicht zwischen ihren
beiden Bereichen. Nichts doppelt zu pflegen, nichts springt an eine andere
Stelle.

Der Wechsel hält nach dem Bezug noch vier Sekunden an - ohne dieses
Nachhalten würde die Seite bei jeder kurzen Unterbrechung hin- und
herspringen.

Beide Panel-Varianten mit arduino-cli gegengebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 19:40:51 +02:00
thomas 40aabe48bc Merge pull request 'feat(P4): Vier neue Dashboard-Darstellungen (1.8.0)' (#60) from feat/dashboard-vier-darstellungen into main 2026-09-02 19:30:11 +02:00
raw-designsandClaude Opus 5 ca3a9c1264 feat(P4): Vier neue Dashboard-Darstellungen (1.8.0)
Ersetzt die bisherigen fünf (Klassisch, Instrumente, Minimal, Soft-3D,
Thermometer) durch vier, die nicht nur den Temperaturbereich anders zeichnen,
sondern die Fläche unterschiedlich nutzen:

- Gewichtet: Wasser mit flex-grow 2, Dampf mit 1.
- Verlauf: lv_chart-Sparkline je Kessel, gespeist aus der bestehenden
  Sekundenabtastung; Ausschnitt am Sollwert ausgerichtet.
- Flächen: ohne Kartenkanten, zwei Felder mit leicht verschiedenem Grund.
- Band: Zustandsband über volle Breite (bereit / heizt / Bezug / Cold
  Extraction / Standby). Neue Vorgabe.

Zwei neue Schriftgrade (96 und 140 px), erzeugt mit lv_font_conv aus
MavenPro-Regular.ttf, beschränkt auf Ziffern, Komma, Punkt, Minus und
Gradzeichen - die größte Textschrift hatte 48 px und reichte für die großen
Zahlen nicht.

Ein gespeicherter Wert außerhalb des neuen Bereichs fällt auf die Vorgabe
zurück. Die fünf alten Builder sind entfernt.

Beide Panel-Varianten mit arduino-cli gegengebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 19:29:54 +02:00
thomas bcd9ecf0c8 Merge pull request 'refactor(P4): Schriftgröße auf die Info-Seite verschieben (1.7.3)' (#59) from refactor/schriftgroesse-auf-info-seite into main 2026-09-02 18:09:36 +02:00
5 changed files with 8978 additions and 326 deletions
+77
View File
@@ -1,3 +1,80 @@
Version 1.8.4:
- Standby-Uhr und Aufweckdialog erscheinen jetzt sofort statt mit spürbarer Verzögerung.
Ursache waren die weichen Ein- und Ausblendungen: Eine Deckkraft-Animation ist bei einem
Bildspeicher in RGB888 das Teuerste, was die Oberfläche tun kann, weil für jedes Einzelbild
die gesamte Fläche unter der Ebene neu gezeichnet und mit ihr gemischt werden muss. Auf
1280x720 dauerte eine solche Blende deutlich länger als die 450 ms, die vorgesehen waren.
Auf dem Waveshare-Panel laufen die Übergänge deshalb ohne Animation; die JC-Panels
behalten sie.
- Beim Wechsel der Schriftgröße blitzte kurz ein blaues Bild auf. Das kommt nicht von der
Firmware: Das Panel zeigt von sich aus Blau, sobald kein Videosignal anliegt - und beim
nötigen Neustart reißt das Signal kurz ab. Die Beleuchtung wird jetzt vorher abgeschaltet;
dunkel ist unauffälliger als blau.
- Neue Einstellung unter Darstellung: „Temperaturzahl in der Zustandsfarbe". Aus bleibt die
Zahl weiß, die Zustandsfarbe steckt dann allein im Balken darunter - ruhiger, und die Zahl
bleibt eine Zahl. Ein bedeutet, sie färbt sich mit, wie bisher. Vorgabe ist aus.
Warnungen bleiben in jedem Fall rot; das ist keine Gestaltung, sondern eine Meldung.
Version 1.8.3:
- Fehlerbehebung: Bei allen Darstellungen stand hinter der Temperatur ein leeres Rechteck
statt des C. Die beiden großen Schriften waren auf Ziffern, Komma, Punkt, Minus und
Gradzeichen beschränkt - das C fehlte darin schlicht. Es ist jetzt enthalten, ebenso g
und s für die Gewichts- und Zeitanzeige während eines Bezugs.
- Fehlerbehebung: Bei der Darstellung „Verlauf" war keine Kurve zu sehen. Ein frisch
angelegtes Diagramm enthält lauter „kein Wert"-Punkte und zeichnet deshalb nichts; ohne
Hintergrund war der Bereich vollständig unsichtbar, bis nach einer Minute genug Messwerte
zusammengekommen wären. Die Kurve startet jetzt beim aktuellen Messwert, und ihre Fläche
ist leicht abgesetzt.
- Die großen Zahlen des Dashboards stehen jetzt im fetten Schnitt und etwas enger, wie im
Entwurf. Erzeugt aus der variablen Maven-Pro-Datei mit Gewicht 700 - dieselbe Schriftart
wie bisher, nur kräftiger. Die übrige Oberfläche bleibt unverändert.
Version 1.8.2:
- Die Liste der letzten Bezüge auf der Statistikseite ist jetzt eine Tabelle mit festen
Spalten: Zeitpunkt, Dauer, Gewicht und gegebenenfalls die Kennzeichnung „kalt".
- Vorher war es ein einziges mehrzeiliges Textfeld, in dem die Werte durch Leerzeichen
getrennt aneinandergereiht standen. Weil Zahlen unterschiedlich breit sind, stand nichts
untereinander - zwei Bezüge ließen sich nicht vergleichen, ohne jede Zeile einzeln zu
lesen. Genau das sah unruhig aus.
- Ziffern haben jetzt gleiche Breite, jede zweite Zeile ist leicht abgesetzt, und eine
Kopfzeile benennt die Spalten. Wurde ohne Waage bezogen, bleibt die Gewichtsspalte leer,
statt eine Null vorzutäuschen.
- Die Zeilen werden einmal angelegt und danach nur gefüllt oder ausgeblendet, statt bei
jeder Aktualisierung neu zu entstehen.
Version 1.8.1:
- Die Bezugsanzeige ist da, und zwar für alle vier Darstellungen gemeinsam: Sobald ein
Bezug oder Dampfbezug läuft, schrumpft der Temperaturbereich und die untere Zeile wächst.
Shot-Timer und Gewicht werden dabei auf 96 Pixel vergrößert - während eines Bezugs zählen
Zeit und Gewicht, die Kesseltemperaturen sind dann Nebensache.
- Es gibt keine zweite Ansicht: Die Seite verschiebt nur das Gewicht zwischen ihren beiden
Bereichen. Damit bleibt alles an seinem Platz, und es gibt nichts doppelt zu pflegen.
- Der Wechsel hält nach dem Bezug noch vier Sekunden an. Ohne dieses Nachhalten würde die
Seite bei jeder kurzen Unterbrechung hin- und herspringen - das wäre unruhiger als gar
kein Wechsel.
Version 1.8.0:
- Das Dashboard hat vier neue Darstellungen, die die bisherigen fünf ersetzen. Sie sind
keine Varianten desselben Bildes, sondern vier Antworten auf die Frage, was die große
Fläche des 7-Zöllers leisten soll:
* Gewichtet - Wasser bekommt zwei Drittel der Fläche, Dampf ein Drittel. Beim Espresso
zählt die Wassertemperatur; die Asymmetrie ist die Aussage.
* Verlauf - unter jeder Temperatur eine flache Kurve der letzten Minute. Die eigentliche
Frage an eine PID-Regelung ist nicht „wie warm", sondern „hält sie" - und das kann eine
Zahl grundsätzlich nicht zeigen. Der Ausschnitt hängt am Sollwert, damit die Abweichung
sichtbar wird statt in der Skala unterzugehen.
* Flächen - keine Kartenkanten, zwei Felder mit einem Hauch Helligkeitsunterschied.
* Band - ein farbiges Band über die volle Breite sagt den Zustand, bevor eine Zahl
gelesen wird: bereit, heizt, Bezug, Cold Extraction oder Standby. Neue Vorgabe.
- Dafür gibt es zwei neue Schriftgrade (96 und 140 Pixel). Sie enthalten nur Ziffern,
Komma, Punkt, Minus und Gradzeichen - mit vollem Zeichensatz wären sie um ein Vielfaches
größer, ohne etwas beizutragen. Die größte Textschrift hatte bisher 48 Pixel, was für die
großen Zahlen nicht reichte.
- Ältere Stände haben in den Einstellungen noch die Nummer einer der früheren Darstellungen
gespeichert; liegt sie außerhalb, fällt sie auf die Vorgabe zurück.
- Der Aufbau der Bezugsanzeige - untere Zeile wechselt während eines Bezugs auf große
Timer- und Gewichtsanzeige - folgt als nächster Schritt.
Version 1.7.3:
- Die Schriftgröße steht jetzt auf der Info-Seite im Abschnitt „Darstellung", zusammen mit
der Wahl der Temperaturanzeige und dem Live-Bezugsschirm. Dort gehört sie hin: Es ist
+2
View File
@@ -567,6 +567,8 @@
LV_FONT_DECLARE(lv_font_maven_pro_28) \
LV_FONT_DECLARE(lv_font_maven_pro_40) \
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)
/*Always set a default font*/
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+430 -326
View File
@@ -22,6 +22,7 @@
#include "ui.h"
#include <Preferences.h>
#include "esp_system.h"
#include "display_hal.h"
// ------------------------------------------------------------------------------------
// Schriftgroesse
@@ -82,6 +83,10 @@ static void font_step_save_and_restart(uint8_t step) {
}
// Der Zaehler der Absturzsicherung darf diesen gewollten Neustart nicht mitzaehlen.
{ Preferences b; if (b.begin("bootguard", false)) { b.putUShort("early", 0); b.end(); } }
// Beleuchtung aus, bevor der Neustart die Bildausgabe abreissen laesst. Ohne Signal
// zeigt das Panel von sich aus ein blaues Bild - das blitzte beim Wechsel der
// Schriftgroesse kurz auf. Dunkel ist unauffaelliger als blau.
hal_backlight(0);
delay(120);
esp_restart();
}
@@ -166,8 +171,16 @@ static void label_set_if_changed(lv_obj_t* lbl, const char* txt); // Definitio
// TD_MINIMAL - rahmenlos, nur grosse Werte mit duenner Ist/Soll-Linie
// TD_NEO - Soft-3D/Neumorph: Flaeche wirkt aus dem Hintergrund herausgedrueckt
// TD_THERMO - vertikale Thermometer-Saeulen mit Kolben und Glanzstreifen
enum TempDesign { TD_CARDS = 0, TD_GAUGE, TD_MINIMAL, TD_NEO, TD_THERMO, TD_DESIGN_COUNT };
static uint8_t g_tempDesign = TD_GAUGE;
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
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_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
static lv_obj_t* lblTempW; static lv_obj_t* lblSetW; static lv_obj_t* barDutyW; // barDuty*: lv_bar (Karten/Minimal/Neo/Thermo-Saeule)
static lv_obj_t* lblTempD; static lv_obj_t* lblSetD; static lv_obj_t* barDutyD;
@@ -182,6 +195,10 @@ static lv_obj_t* g_profChip = nullptr;
static lv_obj_t* g_profPicker = nullptr; // Schnellwahl-Overlay (Vollbild-Abdunkelung)
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;
@@ -348,7 +365,6 @@ static String g_selProfile;
// Statistik (Nutzungsverlauf)
static lv_obj_t* g_statSummary = nullptr; // Aggregate (Heute/Woche/Gesamt)
static lv_obj_t* g_statList = nullptr; // Liste der letzten Einzelbezuege
// =====================================================================================
// Hilfen
@@ -567,6 +583,112 @@ static void grid_at(lv_obj_t* c, uint8_t col, uint8_t row, uint8_t col_span = 1)
}
#endif
// ------------------------------------------------------------------------------------
// Liste der letzten Bezuege
//
// Frueher war das EIN mehrzeiliges Textfeld: Zeitpunkt, Dauer und Gewicht standen in einer
// Zeile aneinandergereiht, durch Leerzeichen getrennt. Weil die Zahlen unterschiedlich
// breit sind, stand nichts untereinander - man konnte zwei Bezuege nicht vergleichen, ohne
// jede Zeile einzeln zu lesen.
//
// Jetzt eine Tabelle mit festen Spalten und Ziffern gleicher Breite. Die Zeilen werden
// einmal angelegt und danach nur noch gefuellt oder ausgeblendet - eine Statistikseite,
// die bei jeder Aktualisierung Dutzende Objekte neu anlegt, waere unnoetig teuer.
// ------------------------------------------------------------------------------------
#define STAT_ROW_COUNT 12
static lv_obj_t* g_statRow[STAT_ROW_COUNT] = {};
static lv_obj_t* g_statTime[STAT_ROW_COUNT] = {};
static lv_obj_t* g_statDur[STAT_ROW_COUNT] = {};
static lv_obj_t* g_statWgt[STAT_ROW_COUNT] = {};
static lv_obj_t* g_statCold[STAT_ROW_COUNT] = {};
static lv_obj_t* g_statEmpty = nullptr;
static lv_obj_t* stat_cell(lv_obj_t* row, int32_t width, bool rechts, lv_color_t col) {
lv_obj_t* l = lv_label_create(row);
if (width > 0) lv_obj_set_width(l, width);
else lv_obj_set_flex_grow(l, 1);
lv_obj_set_style_text_color(l, col, 0);
lv_obj_set_style_text_align(l, rechts ? LV_TEXT_ALIGN_RIGHT : LV_TEXT_ALIGN_LEFT, 0);
lv_label_set_text(l, "");
return l;
}
static void stat_list_build(lv_obj_t* parent) {
lv_obj_t* card = th_card(parent);
lv_obj_set_width(card, LV_PCT(100));
lv_obj_set_height(card, LV_SIZE_CONTENT);
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(card, 0, 0);
lv_obj_clear_flag(card, LV_OBJ_FLAG_SCROLLABLE);
// Kopfzeile - benennt die Spalten, damit die Zahlen ohne Einheit auskommen
lv_obj_t* head = lv_obj_create(card);
lv_obj_set_width(head, LV_PCT(100));
lv_obj_set_height(head, LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(head, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(head, 0, 0);
lv_obj_set_style_pad_all(head, 4, 0);
lv_obj_set_style_pad_column(head, 10, 0);
lv_obj_set_flex_flow(head, LV_FLEX_FLOW_ROW);
lv_obj_clear_flag(head, LV_OBJ_FLAG_SCROLLABLE);
stat_cell(head, 0, false, COL_TEXT_MUT);
lv_label_set_text(lv_obj_get_child(head, 0), "Zeitpunkt");
lv_label_set_text(stat_cell(head, 96, true, COL_TEXT_MUT), "Dauer");
lv_label_set_text(stat_cell(head, 96, true, COL_TEXT_MUT), "Gewicht");
lv_label_set_text(stat_cell(head, 58, true, COL_TEXT_MUT), "");
for (int i = 0; i < STAT_ROW_COUNT; i++) {
lv_obj_t* r = lv_obj_create(card);
lv_obj_set_width(r, LV_PCT(100));
lv_obj_set_height(r, LV_SIZE_CONTENT);
// Jede zweite Zeile leicht abgesetzt - fuehrt das Auge ueber die Zeile, ohne dass
// dafuer Trennlinien noetig waeren.
lv_obj_set_style_bg_color(r, lv_color_hex(0x242424), 0);
lv_obj_set_style_bg_opa(r, (i % 2) ? LV_OPA_COVER : LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(r, 0, 0);
lv_obj_set_style_radius(r, 5, 0);
lv_obj_set_style_pad_all(r, 5, 0);
lv_obj_set_style_pad_column(r, 10, 0);
lv_obj_set_flex_flow(r, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(r, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_clear_flag(r, LV_OBJ_FLAG_SCROLLABLE);
g_statTime[i] = stat_cell(r, 0, false, COL_TEXT_DIM);
g_statDur[i] = stat_cell(r, 96, true, COL_TEXT);
g_statWgt[i] = stat_cell(r, 96, true, COL_TEXT);
g_statCold[i] = stat_cell(r, 58, true, lv_color_hex(0x80cbc4));
g_statRow[i] = r;
lv_obj_add_flag(r, LV_OBJ_FLAG_HIDDEN);
}
g_statEmpty = lv_label_create(card);
lv_obj_set_width(g_statEmpty, LV_PCT(100));
lv_label_set_text(g_statEmpty, "Noch keine Bezüge aufgezeichnet.");
lv_obj_set_style_text_color(g_statEmpty, COL_TEXT_MUT, 0);
lv_obj_set_style_pad_all(g_statEmpty, 5, 0);
}
// Eine Zeile fuellen. 'w' unter 0 bedeutet: ohne Waage bezogen, dann bleibt die Spalte leer
// statt eine Null vorzutaeuschen.
static void stat_list_set(int i, const char* dt, float d, float w, bool kalt) {
if (i < 0 || i >= STAT_ROW_COUNT || !g_statRow[i]) return;
lv_obj_remove_flag(g_statRow[i], LV_OBJ_FLAG_HIDDEN);
lv_label_set_text(g_statTime[i], dt);
lv_label_set_text_fmt(g_statDur[i], "%.1f s", d);
if (w >= 0.0f) lv_label_set_text_fmt(g_statWgt[i], "%.1f g", w);
else lv_label_set_text(g_statWgt[i], "");
lv_label_set_text(g_statCold[i], kalt ? "kalt" : "");
}
static void stat_list_finish(int used) {
for (int i = used; i < STAT_ROW_COUNT; i++)
if (g_statRow[i]) lv_obj_add_flag(g_statRow[i], LV_OBJ_FLAG_HIDDEN);
if (g_statEmpty) {
if (used > 0) lv_obj_add_flag(g_statEmpty, LV_OBJ_FLAG_HIDDEN);
else lv_obj_remove_flag(g_statEmpty, LV_OBJ_FLAG_HIDDEN);
}
}
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);
@@ -656,7 +778,19 @@ static void err_icon_cb(lv_event_t*) { show_page(PG_DASH); } // Tipp auf Fehl
// Bewusst ueber das 'opa'-Style (faellt via opa_recursive auf die Kinder durch) und NICHT
// ueber 'opa_layered' - Letzteres wuerde einen Vollbild-Zwischenpuffer allozieren
// und koennte den LVGL-Speicherpool sprengen. So bleibt das Fade speicher-neutral.
// Dauer der weichen Uebergaenge (Standby-Uhr, Aufweckdialog, Bezugsuebersicht).
//
// Auf dem Waveshare-Panel: 0, also ohne Animation. Eine Deckkraft-Animation ist bei einem
// Bildspeicher in RGB888 das Teuerste, was die Oberflaeche tun kann - fuer jedes Einzelbild
// muss die gesamte Flaeche unter der Ebene neu gezeichnet und mit ihr gemischt werden. Auf
// 1280x720 dauert eine solche Blende deutlich laenger als die 450 ms, die sie dauern soll;
// spuerbar war das als Verzoegerung, bis die Standby-Uhr erschien und bis nach dem
// Fingertipp der Aufweckdialog kam. Ohne Animation erscheinen beide sofort.
#if JC_PANEL_TYPE == WS_PANEL_7H
static const uint32_t UI_FADE_MS = 0;
#else
static const uint32_t UI_FADE_MS = 450; // ruhige, unaufdringliche Dauer
#endif
static void ui_fade_exec_cb(void* obj, int32_t v) {
lv_obj_set_style_opa((lv_obj_t*)obj, (lv_opa_t)v, 0);
@@ -671,6 +805,12 @@ static void ui_fade_hide_done_cb(lv_anim_t* a) {
// Blendet 'obj' weich ein (sichtbar schalten, Opazitaet 0 -> voll).
static void ui_fade_in(lv_obj_t* obj, uint32_t ms) {
if (!obj) return;
if (ms == 0) { // ohne Animation: sofort sichtbar
lv_anim_delete(obj, ui_fade_exec_cb);
lv_obj_set_style_opa(obj, LV_OPA_COVER, 0);
lv_obj_remove_flag(obj, LV_OBJ_FLAG_HIDDEN);
return;
}
lv_anim_delete(obj, ui_fade_exec_cb); // evtl. laufende Ausblendung abbrechen
lv_obj_set_style_opa(obj, LV_OPA_TRANSP, 0);
lv_obj_remove_flag(obj, LV_OBJ_FLAG_HIDDEN);
@@ -686,6 +826,11 @@ static void ui_fade_in(lv_obj_t* obj, uint32_t ms) {
static void ui_fade_out_hide(lv_obj_t* obj, uint32_t ms) {
if (!obj || lv_obj_has_flag(obj, LV_OBJ_FLAG_HIDDEN)) return;
lv_anim_delete(obj, ui_fade_exec_cb);
if (ms == 0) { // ohne Animation: sofort weg
lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN);
lv_obj_set_style_opa(obj, LV_OPA_COVER, 0);
return;
}
lv_anim_t a; lv_anim_init(&a);
lv_anim_set_var(&a, obj);
lv_anim_set_exec_cb(&a, ui_fade_exec_cb);
@@ -798,7 +943,7 @@ static void temp_progress_set(lv_obj_t* bar, lv_obj_t* arc, lv_obj_t* bulb, int
if (pct > 100) pct = 100; else if (pct < 0) pct = 0;
if (bar) {
lv_bar_set_value(bar, pct, LV_ANIM_OFF);
if (g_tempDesign != TD_CARDS) lv_obj_set_style_bg_color(bar, col, LV_PART_INDICATOR);
lv_obj_set_style_bg_color(bar, col, LV_PART_INDICATOR);
}
if (arc) {
lv_arc_set_value(arc, pct);
@@ -806,298 +951,119 @@ static void temp_progress_set(lv_obj_t* bar, lv_obj_t* arc, lv_obj_t* bulb, int
}
if (bulb) lv_obj_set_style_bg_color(bulb, col, 0);
}
static void tempCard(lv_obj_t* parent, const char* title, lv_color_t accent,
lv_obj_t** big, lv_obj_t** setp, lv_obj_t** bar) {
lv_obj_t* card = th_card(parent);
lv_obj_set_size(card, LV_PCT(49), LV_PCT(100));
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(card, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_clear_flag(card, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(card, LV_OBJ_FLAG_CLICKABLE); // antippbar -> Temperatur-Seite
lv_obj_add_event_cb(card, temp_card_cb, LV_EVENT_CLICKED, nullptr);
lv_obj_t* t = lv_label_create(card);
lv_label_set_text(t, title);
lv_obj_set_style_text_color(t, accent, 0);
lv_obj_set_style_text_font(t, font_text(), 0);
*big = lv_label_create(card);
lv_label_set_text(*big, "--.-");
lv_obj_set_style_text_color(*big, COL_TEXT, 0);
lv_obj_set_style_text_font(*big, &lv_font_maven_pro_48, 0);
*setp = lv_label_create(card);
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);
*bar = lv_bar_create(card);
lv_obj_set_size(*bar, LV_PCT(92), 12);
lv_obj_set_style_bg_color(*bar, lv_color_hex(0x3a3a3a), 0); // sichtbarer Track (auch bei 0 %)
lv_obj_set_style_bg_opa(*bar, LV_OPA_COVER, 0);
lv_obj_set_style_border_color(*bar, COL_BORDER, 0);
lv_obj_set_style_border_width(*bar, 1, 0);
lv_obj_set_style_radius(*bar, 6, 0);
lv_obj_set_style_bg_color(*bar, accent, LV_PART_INDICATOR);
lv_obj_set_style_radius(*bar, 6, LV_PART_INDICATOR);
lv_bar_set_range(*bar, 0, 100);
lv_bar_set_value(*bar, 0, LV_ANIM_OFF);
// Farbe der grossen Temperaturzahl.
//
// Die Zustandsfarbe steckt ohnehin schon im Balken darunter. Ob die Zahl selbst mitgefaerbt
// wird, ist deshalb Geschmackssache: Eingefaerbt ist der Zustand auch aus dem Augenwinkel
// erkennbar, weiss wirkt ruhiger und laesst die Zahl als Zahl stehen.
//
// Warnungen bleiben immer rot - das ist keine Gestaltung, sondern eine Meldung.
static lv_color_t temp_value_color(lv_color_t zustand) {
return g_tempColored ? zustand : COL_TEXT;
}
// Rundinstrument im Manometer-Look (Design TD_GAUGE): freistehend ohne Kartenrahmen.
// Leichte 3D-Wirkung ueber einen "Metall"-Bezel (vertikaler Verlauf hell -> dunkel) mit
// weichem Schlagschatten und ein leicht aufgehelltes Zifferblatt darunter. Der 270-Grad-
// Bogen zeigt Ist/Soll in % und faerbt sich mit der Zustandsfarbe des Ist-Werts.
static void tempGauge(lv_obj_t* parent, const char* title, lv_color_t accent,
lv_obj_t** big, lv_obj_t** setp, lv_obj_t** arc) {
lv_obj_t* holder = lv_obj_create(parent);
lv_obj_set_size(holder, LV_PCT(49), LV_PCT(100));
lv_obj_set_style_bg_opa(holder, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(holder, 0, 0);
lv_obj_set_style_pad_all(holder, 0, 0);
lv_obj_clear_flag(holder, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(holder, LV_OBJ_FLAG_CLICKABLE); // antippbar -> Temperatur-Seite
lv_obj_add_event_cb(holder, temp_card_cb, LV_EVENT_CLICKED, nullptr);
// Bezel-Ring: Verlauf von hell (oben) nach dunkel (unten) + Schatten = Metall-Optik
lv_obj_t* bezel = lv_obj_create(holder);
lv_obj_set_size(bezel, 188, 188);
lv_obj_center(bezel);
lv_obj_set_style_radius(bezel, LV_RADIUS_CIRCLE, 0);
lv_obj_set_style_bg_color(bezel, lv_color_hex(0x5c5c5c), 0);
lv_obj_set_style_bg_grad_color(bezel, lv_color_hex(0x141414), 0);
lv_obj_set_style_bg_grad_dir(bezel, LV_GRAD_DIR_VER, 0);
lv_obj_set_style_border_width(bezel, 0, 0);
lv_obj_set_style_pad_all(bezel, 0, 0);
lv_obj_set_style_shadow_width(bezel, 16, 0);
lv_obj_set_style_shadow_offset_y(bezel, 5, 0);
lv_obj_set_style_shadow_color(bezel, lv_color_hex(0x000000), 0);
lv_obj_set_style_shadow_opa(bezel, LV_OPA_60, 0);
lv_obj_clear_flag(bezel, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_clear_flag(bezel, LV_OBJ_FLAG_CLICKABLE); // Tipp faellt zum Holder durch
// ------------------------------------------------------------------------------------
// Temperaturbereich des Dashboards - vier Darstellungen
//
// Gemeinsam ist allen: zwei Kessel, die Zahl ist das Erste, was man sieht. Unterschiedlich
// ist, was die Flaeche zusaetzlich leistet.
//
// Die grossen Zahlen brauchen Schriftgrade, die es vorher nicht gab (96 und 140 Pixel).
// Diese beiden Schriften enthalten nur Ziffern, Komma, Punkt, Minus und Gradzeichen - mit
// vollem Zeichensatz waeren sie um ein Vielfaches groesser, ohne etwas beizutragen.
// ------------------------------------------------------------------------------------
// Zifferblatt: leicht "vertieft" (Verlauf dunkel oben -> etwas heller unten)
lv_obj_t* face = lv_obj_create(bezel);
lv_obj_set_size(face, 174, 174);
lv_obj_center(face);
lv_obj_set_style_radius(face, LV_RADIUS_CIRCLE, 0);
lv_obj_set_style_bg_color(face, lv_color_hex(0x0c0c0c), 0);
lv_obj_set_style_bg_grad_color(face, lv_color_hex(0x232323), 0);
lv_obj_set_style_bg_grad_dir(face, LV_GRAD_DIR_VER, 0);
lv_obj_set_style_border_width(face, 0, 0);
lv_obj_set_style_pad_all(face, 0, 0);
lv_obj_clear_flag(face, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_clear_flag(face, LV_OBJ_FLAG_CLICKABLE);
// Ein Kesselfeld. 'grow' verteilt die Breite zwischen den beiden, 'valueFont' bestimmt die
// Groesse der Zahl, 'flat' laesst den Kartenrahmen weg und faerbt stattdessen die Flaeche.
static lv_obj_t* tempField(lv_obj_t* parent, const char* title, lv_color_t accent,
lv_obj_t** big, lv_obj_t** setp, lv_obj_t** bar,
uint8_t grow, const lv_font_t* valueFont,
bool flat, uint32_t flatBg) {
lv_obj_t* c;
if (flat) {
c = lv_obj_create(parent);
lv_obj_set_style_bg_color(c, lv_color_hex(flatBg), 0);
lv_obj_set_style_bg_opa(c, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(c, 0, 0);
lv_obj_set_style_radius(c, 0, 0);
lv_obj_set_style_pad_all(c, 14, 0);
} else {
c = th_card(parent);
lv_obj_set_style_pad_all(c, 12, 0);
}
lv_obj_set_height(c, LV_PCT(100));
lv_obj_set_flex_grow(c, grow);
lv_obj_set_flex_flow(c, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(c, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_clear_flag(c, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(c, LV_OBJ_FLAG_CLICKABLE); // antippbar -> Temperatur-Seite
lv_obj_add_event_cb(c, temp_card_cb, LV_EVENT_CLICKED, nullptr);
// 270-Grad-Bogen (Luecke unten): reine Anzeige, kein Griff, nicht bedienbar
*arc = lv_arc_create(face);
lv_obj_set_size(*arc, 158, 158);
lv_obj_center(*arc);
lv_arc_set_rotation(*arc, 135);
lv_arc_set_bg_angles(*arc, 0, 270);
lv_arc_set_range(*arc, 0, 100);
lv_arc_set_value(*arc, 0);
lv_obj_remove_style(*arc, nullptr, LV_PART_KNOB);
lv_obj_clear_flag(*arc, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_style_arc_width(*arc, 12, LV_PART_MAIN);
lv_obj_set_style_arc_color(*arc, lv_color_hex(0x2e2e2e), LV_PART_MAIN);
lv_obj_set_style_arc_width(*arc, 12, LV_PART_INDICATOR);
lv_obj_set_style_arc_color(*arc, accent, LV_PART_INDICATOR);
lv_obj_set_style_arc_rounded(*arc, true, LV_PART_INDICATOR);
lv_obj_t* t = lv_label_create(face);
lv_obj_t* t = lv_label_create(c);
lv_label_set_text(t, title);
lv_obj_set_style_text_color(t, accent, 0);
lv_obj_set_style_text_font(t, font_small(), 0);
lv_obj_align(t, LV_ALIGN_CENTER, 0, -48);
*big = lv_label_create(face);
*big = lv_label_create(c);
lv_label_set_text(*big, "--.-");
lv_obj_set_style_text_color(*big, COL_TEXT, 0);
lv_obj_set_style_text_font(*big, &lv_font_maven_pro_40, 0);
lv_obj_align(*big, LV_ALIGN_CENTER, 0, -2);
// Soll-Wert sitzt in der Bogen-Luecke unten (dort ist die volle Breite frei)
*setp = lv_label_create(face);
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_small(), 0);
lv_obj_align(*setp, LV_ALIGN_CENTER, 0, 62);
}
// Minimal-Design (TD_MINIMAL): komplett rahmenlos - nur grosser Ist-Wert in der
// Zustandsfarbe, darunter eine duenne Ist/Soll-Linie und der Soll-Wert. Sehr ruhig.
static void tempMinimal(lv_obj_t* parent, const char* title, lv_color_t accent,
lv_obj_t** big, lv_obj_t** setp, lv_obj_t** bar) {
lv_obj_t* holder = lv_obj_create(parent);
lv_obj_set_size(holder, LV_PCT(49), LV_PCT(100));
lv_obj_set_style_bg_opa(holder, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(holder, 0, 0);
lv_obj_set_style_pad_all(holder, 0, 0);
lv_obj_set_style_pad_row(holder, 10, 0);
lv_obj_set_flex_flow(holder, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(holder, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_clear_flag(holder, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(holder, LV_OBJ_FLAG_CLICKABLE); // antippbar -> Temperatur-Seite
lv_obj_add_event_cb(holder, temp_card_cb, LV_EVENT_CLICKED, nullptr);
lv_obj_t* t = lv_label_create(holder);
lv_label_set_text(t, title);
lv_obj_set_style_text_color(t, accent, 0);
lv_obj_set_style_text_font(t, font_text(), 0);
*big = lv_label_create(holder);
lv_label_set_text(*big, "--.-");
lv_obj_set_style_text_color(*big, COL_TEXT, 0);
lv_obj_set_style_text_font(*big, &lv_font_maven_pro_48, 0);
*bar = lv_bar_create(holder); // duenne Ist/Soll-Linie
lv_obj_set_size(*bar, LV_PCT(64), 4);
lv_obj_set_style_bg_color(*bar, lv_color_hex(0x2a2a2a), 0);
lv_obj_set_style_bg_opa(*bar, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(*bar, 0, 0);
lv_obj_set_style_radius(*bar, 2, 0);
lv_obj_set_style_bg_color(*bar, accent, LV_PART_INDICATOR); // folgt spaeter der Zustandsfarbe
lv_obj_set_style_radius(*bar, 2, LV_PART_INDICATOR);
lv_bar_set_range(*bar, 0, 100);
lv_bar_set_value(*bar, 0, LV_ANIM_OFF);
*setp = lv_label_create(holder);
lv_obj_set_style_text_font(*big, valueFont, 0);
// 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);
else if (valueFont == &lv_font_maven_pro_bold_96) lv_obj_set_style_text_letter_space(*big, -2, 0);
*setp = lv_label_create(c);
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);
}
// Soft-3D/Neumorph (TD_NEO): Flaeche fast im Hintergrundton, wirkt herausgedrueckt.
// LVGL kann nur einen Schatten je Objekt, daher zwei verschachtelte Objekte:
// aussen dunkler Schatten unten rechts, innen heller "Lichtschein" oben links.
static void tempNeo(lv_obj_t* parent, const char* title, lv_color_t accent,
lv_obj_t** big, lv_obj_t** setp, lv_obj_t** bar) {
lv_obj_t* shell = lv_obj_create(parent);
lv_obj_set_size(shell, LV_PCT(48), LV_PCT(100));
lv_obj_set_style_radius(shell, 20, 0);
lv_obj_set_style_bg_color(shell, lv_color_hex(0x171717), 0);
lv_obj_set_style_bg_opa(shell, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(shell, 0, 0);
lv_obj_set_style_pad_all(shell, 0, 0);
lv_obj_set_style_shadow_width(shell, 14, 0);
lv_obj_set_style_shadow_offset_x(shell, 6, 0);
lv_obj_set_style_shadow_offset_y(shell, 6, 0);
lv_obj_set_style_shadow_color(shell, lv_color_hex(0x000000), 0);
lv_obj_set_style_shadow_opa(shell, LV_OPA_70, 0);
lv_obj_clear_flag(shell, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(shell, LV_OBJ_FLAG_CLICKABLE); // antippbar -> Temperatur-Seite
lv_obj_add_event_cb(shell, temp_card_cb, LV_EVENT_CLICKED, nullptr);
lv_obj_t* inner = lv_obj_create(shell);
lv_obj_set_size(inner, LV_PCT(100), LV_PCT(100));
lv_obj_set_style_radius(inner, 20, 0);
lv_obj_set_style_bg_color(inner, lv_color_hex(0x1e1e1e), 0); // leicht gewoelbt: oben etwas heller
lv_obj_set_style_bg_grad_color(inner, lv_color_hex(0x121212), 0);
lv_obj_set_style_bg_grad_dir(inner, LV_GRAD_DIR_VER, 0);
lv_obj_set_style_border_width(inner, 0, 0);
lv_obj_set_style_shadow_width(inner, 14, 0);
lv_obj_set_style_shadow_offset_x(inner, -6, 0);
lv_obj_set_style_shadow_offset_y(inner, -6, 0);
lv_obj_set_style_shadow_color(inner, lv_color_hex(0x3c3c3c), 0);
lv_obj_set_style_shadow_opa(inner, LV_OPA_50, 0);
lv_obj_set_style_pad_row(inner, 8, 0);
lv_obj_set_flex_flow(inner, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(inner, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_clear_flag(inner, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_clear_flag(inner, LV_OBJ_FLAG_CLICKABLE); // Tipp faellt zur Shell durch
lv_obj_t* t = lv_label_create(inner);
lv_label_set_text(t, title);
lv_obj_set_style_text_color(t, accent, 0);
lv_obj_set_style_text_font(t, font_text(), 0);
*big = lv_label_create(inner);
lv_label_set_text(*big, "--.-");
lv_obj_set_style_text_color(*big, COL_TEXT, 0);
lv_obj_set_style_text_font(*big, &lv_font_maven_pro_48, 0);
*setp = lv_label_create(inner);
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);
*bar = lv_bar_create(inner); // "eingelassener" Balken: Track dunkler als Flaeche
lv_obj_set_size(*bar, LV_PCT(78), 10);
lv_obj_set_style_bg_color(*bar, lv_color_hex(0x0a0a0a), 0);
*bar = lv_bar_create(c);
lv_obj_set_size(*bar, LV_PCT(92), 8);
lv_obj_set_style_bg_color(*bar, lv_color_hex(0x2c2c2c), 0);
lv_obj_set_style_bg_opa(*bar, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(*bar, 0, 0);
lv_obj_set_style_radius(*bar, 5, 0);
lv_obj_set_style_bg_color(*bar, accent, LV_PART_INDICATOR); // folgt spaeter der Zustandsfarbe
lv_obj_set_style_radius(*bar, 5, LV_PART_INDICATOR);
lv_obj_set_style_radius(*bar, 4, 0);
lv_obj_set_style_bg_color(*bar, accent, LV_PART_INDICATOR);
lv_obj_set_style_radius(*bar, 4, LV_PART_INDICATOR);
lv_bar_set_range(*bar, 0, 100);
lv_bar_set_value(*bar, 0, LV_ANIM_OFF);
return c;
}
// Thermometer-Saeulen (TD_THERMO): vertikale Saeule (lv_bar waechst bei Hochformat von
// unten), Glanzstreifen als Glas-Effekt, runder Kolben unten - alles rahmenlos.
static void tempThermo(lv_obj_t* parent, const char* title, lv_color_t accent,
lv_obj_t** big, lv_obj_t** setp, lv_obj_t** bar, lv_obj_t** bulb) {
lv_obj_t* holder = lv_obj_create(parent);
lv_obj_set_size(holder, LV_PCT(49), LV_PCT(100));
lv_obj_set_style_bg_opa(holder, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(holder, 0, 0);
lv_obj_set_style_pad_all(holder, 0, 0);
lv_obj_set_style_pad_column(holder, 14, 0);
lv_obj_set_flex_flow(holder, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(holder, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_clear_flag(holder, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(holder, LV_OBJ_FLAG_CLICKABLE); // antippbar -> Temperatur-Seite
lv_obj_add_event_cb(holder, temp_card_cb, LV_EVENT_CLICKED, nullptr);
lv_obj_t* th = lv_obj_create(holder); // Saeule + Kolben (feste Groesse)
lv_obj_set_size(th, 48, 190);
lv_obj_set_style_bg_opa(th, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(th, 0, 0);
lv_obj_set_style_pad_all(th, 0, 0);
lv_obj_clear_flag(th, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_clear_flag(th, LV_OBJ_FLAG_CLICKABLE);
*bar = lv_bar_create(th); // hochkant -> fuellt sich von unten
lv_obj_set_size(*bar, 20, 158);
lv_obj_align(*bar, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_set_style_bg_color(*bar, lv_color_hex(0x262626), 0);
lv_obj_set_style_bg_opa(*bar, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(*bar, 0, 0);
lv_obj_set_style_radius(*bar, 10, 0);
lv_obj_set_style_bg_color(*bar, accent, LV_PART_INDICATOR); // folgt spaeter der Zustandsfarbe
lv_obj_set_style_radius(*bar, 10, LV_PART_INDICATOR);
lv_bar_set_range(*bar, 0, 100);
lv_bar_set_value(*bar, 0, LV_ANIM_OFF);
lv_obj_t* gloss = lv_obj_create(th); // Glanzstreifen (Glas-Effekt)
lv_obj_set_size(gloss, 4, 138);
lv_obj_align(gloss, LV_ALIGN_TOP_MID, -5, 8);
lv_obj_set_style_bg_color(gloss, lv_color_hex(0xffffff), 0);
lv_obj_set_style_bg_opa(gloss, LV_OPA_20, 0);
lv_obj_set_style_border_width(gloss, 0, 0);
lv_obj_set_style_radius(gloss, 2, 0);
lv_obj_clear_flag(gloss, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_clear_flag(gloss, LV_OBJ_FLAG_CLICKABLE);
*bulb = lv_obj_create(th); // Kolben (ueberlappt die Saeule leicht)
lv_obj_set_size(*bulb, 36, 36);
lv_obj_align(*bulb, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_obj_set_style_radius(*bulb, LV_RADIUS_CIRCLE, 0);
lv_obj_set_style_bg_color(*bulb, accent, 0); // folgt spaeter der Zustandsfarbe
lv_obj_set_style_bg_opa(*bulb, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(*bulb, 0, 0);
lv_obj_set_style_shadow_width(*bulb, 10, 0);
lv_obj_set_style_shadow_offset_y(*bulb, 3, 0);
lv_obj_set_style_shadow_color(*bulb, lv_color_hex(0x000000), 0);
lv_obj_set_style_shadow_opa(*bulb, LV_OPA_40, 0);
lv_obj_clear_flag(*bulb, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_clear_flag(*bulb, LV_OBJ_FLAG_CLICKABLE);
// 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.
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* ch = lv_chart_create(parent);
lv_obj_set_size(ch, LV_PCT(96), 64);
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);
// Schwach abgesetzte Flaeche: Ohne sie waere der Bereich vor dem ersten Messwert
// vollstaendig unsichtbar - man haette gar nicht erkannt, dass dort eine Kurve gehoert.
lv_obj_set_style_bg_color(ch, lv_color_hex(0x151515), 0);
lv_obj_set_style_bg_opa(ch, LV_OPA_COVER, 0);
lv_obj_set_style_radius(ch, 6, 0);
lv_obj_set_style_border_width(ch, 0, 0);
lv_obj_set_style_pad_all(ch, 4, 0);
lv_obj_set_style_size(ch, 0, 0, LV_PART_INDICATOR); // keine Punktmarkierungen
lv_obj_set_style_line_width(ch, 3, LV_PART_ITEMS);
lv_obj_clear_flag(ch, LV_OBJ_FLAG_SCROLLABLE);
*ser = lv_chart_add_series(ch, col, LV_CHART_AXIS_PRIMARY_Y);
lv_obj_t* col = lv_obj_create(holder); // Textspalte neben der Saeule
lv_obj_set_size(col, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(col, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(col, 0, 0);
lv_obj_set_style_pad_all(col, 0, 0);
lv_obj_set_style_pad_row(col, 6, 0);
lv_obj_set_flex_flow(col, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(col, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER);
lv_obj_clear_flag(col, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_clear_flag(col, LV_OBJ_FLAG_CLICKABLE);
lv_obj_t* t = lv_label_create(col);
lv_label_set_text(t, title);
lv_obj_set_style_text_color(t, accent, 0);
lv_obj_set_style_text_font(t, font_text(), 0);
*big = lv_label_create(col);
lv_label_set_text(*big, "--.-");
lv_obj_set_style_text_color(*big, COL_TEXT, 0);
lv_obj_set_style_text_font(*big, &lv_font_maven_pro_48, 0);
*setp = lv_label_create(col);
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);
// 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);
lv_chart_set_all_value(ch, *ser, (int32_t)(jetzt * 10.0f));
return ch;
}
// Temperaturzeile gemaess aktivem Design (neu) befuellen - auch beim Umschalten zur Laufzeit
// Temperaturbereich gemaess aktiver Darstellung (neu) aufbauen - auch beim Umschalten
static void build_temp_row_content() {
if (!g_tempRow) return;
lv_obj_clean(g_tempRow);
@@ -1105,31 +1071,81 @@ static void build_temp_row_content() {
lblTempD = lblSetD = barDutyD = nullptr;
arcTempW = arcTempD = nullptr;
bulbTempW = bulbTempD = nullptr;
// Nur Soft-3D braucht Innenabstand, damit die weichen Schatten nicht am Rand abgeschnitten werden
lv_obj_set_style_pad_hor(g_tempRow, (g_tempDesign == TD_NEO) ? 12 : 0, 0);
lv_obj_set_style_pad_ver(g_tempRow, (g_tempDesign == TD_NEO) ? 12 : 0, 0);
g_sparkW = g_sparkD = nullptr;
g_sparkSerW = g_sparkSerD = nullptr;
g_readyBand = g_readyBandLbl = nullptr;
const lv_color_t teal = lv_color_hex(0x80cbc4);
lv_obj_set_style_pad_all(g_tempRow, 0, 0);
lv_obj_set_style_pad_column(g_tempRow, 12, 0);
lv_obj_set_style_pad_row(g_tempRow, 10, 0);
lv_obj_set_flex_flow(g_tempRow, LV_FLEX_FLOW_ROW);
switch (g_tempDesign) {
case TD_GAUGE:
tempGauge(g_tempRow, "WASSER", COL_ACCENT, &lblTempW, &lblSetW, &arcTempW);
tempGauge(g_tempRow, "DAMPF", teal, &lblTempD, &lblSetD, &arcTempD);
break;
case TD_MINIMAL:
tempMinimal(g_tempRow, "WASSER", COL_ACCENT, &lblTempW, &lblSetW, &barDutyW);
tempMinimal(g_tempRow, "DAMPF", teal, &lblTempD, &lblSetD, &barDutyD);
break;
case TD_NEO:
tempNeo(g_tempRow, "WASSER", COL_ACCENT, &lblTempW, &lblSetW, &barDutyW);
tempNeo(g_tempRow, "DAMPF", teal, &lblTempD, &lblSetD, &barDutyD);
break;
case TD_THERMO:
tempThermo(g_tempRow, "WASSER", COL_ACCENT, &lblTempW, &lblSetW, &barDutyW, &bulbTempW);
tempThermo(g_tempRow, "DAMPF", teal, &lblTempD, &lblSetD, &barDutyD, &bulbTempD);
break;
default:
tempCard(g_tempRow, "WASSER", COL_ACCENT, &lblTempW, &lblSetW, &barDutyW);
tempCard(g_tempRow, "DAMPF", teal, &lblTempD, &lblSetD, &barDutyD);
break;
case TD_GEWICHTET: {
// Beim Espresso zaehlt die Wassertemperatur; der Dampfkessel ist Nebenwert. Also
// zwei Drittel Flaeche fuer Wasser, ein Drittel fuer Dampf - die Asymmetrie ist die
// Aussage, nicht ein Versehen.
tempField(g_tempRow, "WASSER", COL_ACCENT, &lblTempW, &lblSetW, &barDutyW,
2, &lv_font_maven_pro_bold_140, false, 0);
tempField(g_tempRow, "DAMPF", teal, &lblTempD, &lblSetD, &barDutyD,
1, &lv_font_maven_pro_48, false, 0);
break;
}
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);
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);
break;
}
case TD_FLAECHEN: {
// Keine Kartenkanten: Der Bereich ist in zwei Felder geteilt, getrennt nur durch
// einen Hauch Helligkeitsunterschied.
tempField(g_tempRow, "WASSER", COL_ACCENT, &lblTempW, &lblSetW, &barDutyW,
1, &lv_font_maven_pro_bold_140, true, 0x0d0d0d);
tempField(g_tempRow, "DAMPF", teal, &lblTempD, &lblSetD, &barDutyD,
1, &lv_font_maven_pro_bold_140, true, 0x151515);
lv_obj_set_style_pad_column(g_tempRow, 0, 0); // Felder stossen direkt aneinander
break;
}
default: { // TD_BAND
// Ein farbiges Band ueber die volle Breite sagt den Zustand, bevor man eine Zahl
// liest. Deshalb hier eine Spalte: Band oben, darunter die beiden Kessel.
lv_obj_set_flex_flow(g_tempRow, LV_FLEX_FLOW_COLUMN);
g_readyBand = lv_obj_create(g_tempRow);
lv_obj_set_width(g_readyBand, LV_PCT(100));
lv_obj_set_height(g_readyBand, 34);
lv_obj_set_style_radius(g_readyBand, 8, 0);
lv_obj_set_style_border_width(g_readyBand, 0, 0);
lv_obj_set_style_pad_all(g_readyBand, 0, 0);
lv_obj_clear_flag(g_readyBand, LV_OBJ_FLAG_SCROLLABLE);
g_readyBandLbl = lv_label_create(g_readyBand);
lv_obj_set_style_text_font(g_readyBandLbl, font_text(), 0);
lv_label_set_text(g_readyBandLbl, "");
lv_obj_center(g_readyBandLbl);
lv_obj_t* inner = lv_obj_create(g_tempRow);
lv_obj_set_width(inner, LV_PCT(100));
lv_obj_set_flex_grow(inner, 1);
lv_obj_set_style_bg_opa(inner, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(inner, 0, 0);
lv_obj_set_style_pad_all(inner, 0, 0);
lv_obj_set_style_pad_column(inner, 12, 0);
lv_obj_set_flex_flow(inner, LV_FLEX_FLOW_ROW);
lv_obj_clear_flag(inner, LV_OBJ_FLAG_SCROLLABLE);
tempField(inner, "WASSER", COL_ACCENT, &lblTempW, &lblSetW, &barDutyW,
1, &lv_font_maven_pro_bold_96, false, 0);
tempField(inner, "DAMPF", teal, &lblTempD, &lblSetD, &barDutyD,
1, &lv_font_maven_pro_bold_96, false, 0);
break;
}
}
}
static lv_obj_t* infoCell(lv_obj_t* parent, const char* title, lv_obj_t** val, lv_color_t accent, lv_obj_t** titleOut = nullptr) {
@@ -1225,6 +1241,7 @@ static void build_dashboard(lv_obj_t* p) {
// Info-Zeile: Timer / Bezug / Eco-Countdown / Gehaeuse (teilen sich die Breite)
lv_obj_t* ir = lv_obj_create(p);
g_infoRow = ir;
lv_obj_set_width(ir, LV_PCT(100));
lv_obj_set_flex_grow(ir, 2);
lv_obj_set_style_bg_opa(ir, LV_OPA_TRANSP, 0); lv_obj_set_style_border_width(ir, 0, 0);
@@ -1260,6 +1277,10 @@ 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);
@@ -1578,6 +1599,20 @@ 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);
@@ -2961,7 +2996,7 @@ static void info_block(lv_obj_t* card, const char* title, lv_obj_t** valOut, con
if (valOut) *valOut = v;
}
// --- Darstellung: Dashboard-Temperaturdesign umschalten (P4-lokal, in NVS gespeichert) ---
static const char* TEMP_DESIGN_NAMES[TD_DESIGN_COUNT] = { "Klassisch", "Instrumente", "Minimal", "Soft-3D", "Thermometer" };
static const char* TEMP_DESIGN_NAMES[TD_DESIGN_COUNT] = { "Gewichtet", "Verlauf", "Flächen", "Band" };
static void design_btns_refresh() {
for (int i = 0; i < TD_DESIGN_COUNT; i++) {
if (!g_designBtn[i]) continue;
@@ -2981,6 +3016,13 @@ static void design_btn_cb(lv_event_t* e) {
ui_update(g_state); // neu aufgebaute Anzeige sofort mit den aktuellen Werten fuellen
}
// 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.
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(); }
}
static void brew_live_sw_cb(lv_event_t* e) {
g_brewLiveEnabled = sw_get((lv_obj_t*)lv_event_get_target(e));
{ Preferences prefs; prefs.begin("ui", false); prefs.putBool("brewlive", g_brewLiveEnabled); prefs.end(); }
@@ -3018,6 +3060,10 @@ 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* 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);
swBrewLive = add_switch(dcard, "Live-Bezugsschirm (Vollbild während des Bezugs)");
sw_set(swBrewLive, g_brewLiveEnabled);
lv_obj_add_event_cb(swBrewLive, brew_live_sw_cb, LV_EVENT_VALUE_CHANGED, nullptr);
@@ -3049,11 +3095,12 @@ static void build_info(lv_obj_t* p) {
static void stats_request() {
if (!g_client) return;
if (g_statSummary) label_set_if_changed(g_statSummary, "Lade Statistik...");
if (g_statList) label_set_if_changed(g_statList, "");
stat_list_finish(0);
g_client->sendGetUsageStats();
}
static void stats_refresh_cb(lv_event_t*) { stats_request(); }
static void build_stats(lv_obj_t* p) {
lv_obj_set_scroll_dir(p, LV_DIR_VER);
lv_obj_set_scrollbar_mode(p, LV_SCROLLBAR_MODE_AUTO);
@@ -3078,15 +3125,7 @@ static void build_stats(lv_obj_t* p) {
// Letzte Einzelbezuege
section_title(p, "Letzte Bezüge");
lv_obj_t* lc = th_card(p);
lv_obj_set_width(lc, LV_PCT(100));
lv_obj_set_height(lc, LV_SIZE_CONTENT);
lv_obj_set_flex_flow(lc, LV_FLEX_FLOW_COLUMN);
lv_obj_clear_flag(lc, LV_OBJ_FLAG_SCROLLABLE);
g_statList = lv_label_create(lc);
lv_obj_set_width(g_statList, LV_PCT(100));
lv_label_set_text(g_statList, "");
lv_obj_set_style_text_color(g_statList, COL_TEXT_DIM, 0);
stat_list_build(p);
}
// =====================================================================================
@@ -3362,13 +3401,17 @@ void ui_init(ProtocolClient* client) {
}
{ Preferences prefs; prefs.begin("ui", false);
g_tempDesign = prefs.getUChar("tempdsgn", TD_GAUGE);
// Vorgabe ist jetzt TD_BAND. Aeltere Staende haben hier Werte der frueheren fuenf
// Darstellungen stehen - liegt der Wert ausserhalb, faellt er auf die Vorgabe zurueck.
g_tempDesign = prefs.getUChar("tempdsgn", TD_BAND);
if (g_tempDesign >= TD_DESIGN_COUNT) g_tempDesign = TD_BAND;
g_brewLiveEnabled = prefs.getBool("brewlive", true);
g_tempColored = prefs.getBool("tempcol", false);
g_chartWin = prefs.getUChar("chartwin", 0);
if (g_chartWin > 3) g_chartWin = 0;
g_chartDuty = prefs.getBool("chartduty", false);
prefs.end();
if (g_tempDesign >= TD_DESIGN_COUNT) g_tempDesign = TD_GAUGE; }
}
font_step_load(); // gewaehlte Schriftgroesse, bevor irgendetwas angelegt wird
@@ -3765,6 +3808,70 @@ static void appendTuneLive(String& out, const char* kreis, uint32_t elapsedMs,
// =====================================================================================
// Live-Aktualisierung
// =====================================================================================
// Zustandsband der Darstellung TD_BAND. Es beantwortet die haeufigste Frage - ist sie
// bereit? - ohne dass eine Zahl gelesen werden muss.
static void ready_band_update(const MachineState& st) {
if (!g_readyBand || !g_readyBandLbl) return;
const char* txt;
uint32_t bg;
lv_color_t fg;
if (st.standbyActive) {
txt = "Standby"; bg = 0x1e1e1e; fg = COL_TEXT_MUT;
} else if (st.shotActive) {
txt = "Bezug läuft"; bg = 0x3a2a05; fg = COL_ACCENT;
} else if (st.cxActive) {
txt = "Cold Extraction"; bg = 0x0d2b33; fg = lv_color_hex(0x80cbc4);
} else {
bool wOk = fabsf(st.tempW - st.setW) <= 1.0f;
bool dOk = st.steamHeatDisabled || fabsf(st.tempD - st.setD) <= 3.0f;
if (wOk && dOk) { txt = "Bereit — beide Kessel auf Temperatur"; bg = 0x11301a; fg = COL_OK; }
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);
}
// ------------------------------------------------------------------------------------
// Bezugsanzeige: die untere Zeile wechselt ihren Zustand
//
// Im Ruhezustand ist der Temperaturbereich das Wichtigste - die haeufigste Frage lautet
// "ist sie bereit?". Waehrend eines Bezugs kehrt sich das um: Dann zaehlen Zeit und
// Gewicht, und die Kesseltemperaturen sind Nebensache. Statt zweier getrennter Ansichten
// verschiebt die Seite nur das Gewicht zwischen ihren beiden Bereichen und vergroessert
// die beiden Zahlen, auf die es dann ankommt.
//
// Der Wechsel haelt nach dem Bezug noch kurz an. Ohne dieses Nachhalten wuerde die Seite
// bei jeder kurzen Unterbrechung hin- und herspringen - das waere unruhiger als gar kein
// Wechsel.
// ------------------------------------------------------------------------------------
#define DASH_BREW_HOLD_MS 4000
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);
}
static void dash_brew_mode_tick(const MachineState& st) {
static bool war = false; // schon einmal ein Bezug gesehen?
static uint32_t zuletzt = 0;
bool aktiv = st.shotActive || st.steamCircuitActive;
uint32_t jetzt = lv_tick_get();
if (aktiv) { war = true; zuletzt = jetzt; }
dash_brew_mode(aktiv || (war && (uint32_t)(jetzt - zuletzt) < DASH_BREW_HOLD_MS));
}
void ui_update(const MachineState& st) {
if (!g_uiReady) return;
@@ -4074,14 +4181,14 @@ void ui_update(const MachineState& st) {
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, lv_color_hex(0x1976d2), 0);
lv_obj_set_style_text_color(lblTempW, temp_value_color(lv_color_hex(0x1976d2)), 0);
label_set_if_changed(lblSetW, "Cold Extraction - Heizen aus");
lv_obj_set_style_text_color(lblSetW, lv_color_hex(0x1976d2), 0);
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, colW, 0);
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);
// Fortschritt = Ist/Soll in %, gedeckelt auf 100 (Ist >= Soll -> voll)
@@ -4098,12 +4205,16 @@ void ui_update(const MachineState& st) {
} else {
lv_label_set_text(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, colD, 0);
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);
int pctD = (st.setD > 0.0f) ? (int)(st.tempD / st.setD * 100.0f + 0.5f) : 0;
temp_progress_set(barDutyD, arcTempD, bulbTempD, pctD, colD);
}
ready_band_update(st);
dash_brew_mode_tick(st);
if (false) {
}
}
// "BEREIT"-Pille im Header: Wasser im Zielband (und Dampf, sofern er heizt), kein
// Fehler/Standby/Eco und kein laufender Aufheiz-Countdown. Mit Hysterese gegen
@@ -4480,13 +4591,13 @@ void ui_set_usage_stats(const String& json) {
JsonDocument doc;
if (deserializeJson(doc, json)) {
label_set_if_changed(g_statSummary, "Statistik konnte nicht gelesen werden.");
if (g_statList) label_set_if_changed(g_statList, "");
stat_list_finish(0);
return;
}
bool avail = doc["available"] | false;
if (!avail) {
label_set_if_changed(g_statSummary, "Noch keine Nutzungsdaten vorhanden.");
if (g_statList) label_set_if_changed(g_statList, "");
stat_list_finish(0);
return;
}
@@ -4495,8 +4606,7 @@ void ui_set_usage_stats(const String& json) {
long week = doc["week"] | 0;
bool partial = doc["partial"] | false;
// Liste + Mittelwert der angezeigten Bezuege aufbauen
String l;
// Mittelwert der angezeigten Bezuege; die Liste fuellt stat_list_set()
int n = 0;
float durSum = 0.0f;
int nDur = 0; // Anzahl der Bezuege, die in den Mittelwert eingehen (ohne kalte)
@@ -4505,10 +4615,7 @@ void ui_set_usage_stats(const String& json) {
float d = v["d"] | 0.0f;
float w = v["w"] | -1.0f;
bool cold = v["cx"] | false; // kalte Extraktion (fehlt bei S3-FW < 5.3.0)
if (n > 0) l += "\n";
l += String(dt) + " " + String(d, 1) + " s";
if (w >= 0.0f) l += " " + String(w, 1) + " g";
if (cold) l += " (kalt)";
stat_list_set(n, dt, d, w, cold);
// Minutenlange kalte Bezuege wuerden den Mittelwert unbrauchbar machen -> raus
// (gleiche Regel wie in der S3-Statistik)
if (!cold) { durSum += d; nDur++; }
@@ -4526,10 +4633,7 @@ void ui_set_usage_stats(const String& json) {
}
label_set_if_changed(g_statSummary, s.c_str());
if (g_statList) {
if (n == 0) l = "Keine Einzelbezüge vorhanden.";
label_set_if_changed(g_statList, l.c_str());
}
stat_list_finish(n);
}
void ui_set_wifi_networks(const String& json) {