Compare commits

...
Author SHA1 Message Date
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
raw-designsandClaude Opus 5 e5c61d42d4 refactor(P4): Schriftgröße auf die Info-Seite verschieben (1.7.3)
Die Info-Seite hat bereits einen Abschnitt "Darstellung" mit der Wahl der
Temperaturanzeige und dem Live-Bezugsschirm. Die Schriftgröße gehört dort
hinein - sie ist eine Einstellung dieses Displays, keine Wartungsaufgabe.

- Die eigene Karte auf der Wartungsseite entfällt; diese hat damit wieder
  vier Karten in zwei Spalten.
- Die Auswahl selbst ist unverändert: drei Stufen, Neustart danach.
- Sie gilt für alle Panels, nicht nur den 7-Zöller.

Beide Panel-Varianten mit arduino-cli gegengebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LFRMCnijNEA7obGAq5Jz9v
2026-09-02 18:09:21 +02:00
thomas d77b0795ea Merge pull request 'feat(P4): Schriftgröße einstellbar (1.7.2)' (#58) from feat/schriftgroesse-einstellbar into main 2026-09-02 18:02:40 +02:00
raw-designsandClaude Opus 5 7ebffdab8f feat(P4): Schriftgröße einstellbar (1.7.2)
Klein, Mittel oder Groß - Karte "Darstellung" auf der Seite Reinigung &
Wartung.

LVGL-Schriften sind fest einkompilierte Bilddaten und existieren nur in den
Größen, die im Projekt liegen (14/18/28/40/48). Stufenloses Skalieren gibt
es also nicht. Stattdessen drei Rollen - font_small(), font_text(),
font_title() -, die je nach Einstellung eine Stufe höher greifen; die 45
festen Schriftverweise in ui.cpp sind darauf umgestellt.

- Die großen Anzeigen (Temperatur 40/48, Uhr) bleiben fest: Sie sind auf die
  Bildschirmgröße abgestimmt.
- Die Grundschrift wird am Bildschirmobjekt gesetzt, damit alles ohne eigene
  Schrift sie erbt.
- Die Einstellung liegt lokal im P4 (NVS "anzeige"), nicht auf der S3.
- Nach der Auswahl folgt ein Neustart; der Frühstart-Zähler der
  Absturzsicherung wird vorher zurückgesetzt.

Offen: In der Stufe Groß brauchen die Karten mehr Platz - ob alle Seiten
dann noch vollständig auf den Bildschirm passen, zeigt erst das Gerät.

Beide Panel-Varianten mit arduino-cli gegengebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 18:02:23 +02:00
thomas b73fc798c3 Merge pull request 'feat(P4): Cold Extraction und Wartung mehrspaltig (1.7.1)' (#57) from feat/coldex-und-wartung-mehrspaltig into main 2026-09-02 17:52:32 +02:00
5 changed files with 6356 additions and 336 deletions
+56
View File
@@ -1,3 +1,59 @@
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
eine Einstellung dieses Displays, keine Wartungsaufgabe.
- Die Seite Reinigung & Wartung hat damit wieder vier Karten in zwei Spalten.
- Die Auswahl selbst ist unverändert: drei Stufen, Neustart nach der Auswahl.
- Sie steht auf allen Panels zur Verfügung, nicht nur auf dem 7-Zöller.
Version 1.7.2:
- Die Schriftgröße lässt sich jetzt einstellen: Klein, Mittel oder Groß, zu finden auf der
Seite Reinigung & Wartung in der neuen Karte „Darstellung".
- LVGL-Schriften sind fest einkompilierte Bilddaten; es gibt sie nur in den Größen, die im
Projekt liegen (14, 18, 28, 40 und 48 Pixel). Stufenlos skalieren lässt sich also nichts.
Stattdessen gibt es drei Rollen - klein, normal, hervorgehoben -, die je nach Einstellung
eine Stufe höher greifen.
- Die großen Anzeigen für Temperatur und Uhrzeit bleiben unverändert: Sie sind bereits auf
die Bildschirmgröße abgestimmt und sollen nicht mitwachsen.
- Die Einstellung liegt lokal im Display, nicht auf der Hauptplatine - sie betrifft nur
dieses Gerät. Nach der Auswahl startet der P4 neu, weil die Schriften in bereits
angelegten Bedienelementen stecken und sich nicht nachträglich umhängen lassen.
- Zu erwarten: In der Stufe Groß brauchen die Karten mehr Platz. Ob alle Seiten dann noch
vollständig auf den Bildschirm passen, zeigt erst das Gerät.
Version 1.7.1:
- Cold Extraction ist auf dem 7-Zoll-Panel dreispaltig: oben die drei Karten mit
Erklaerungstext (Modus, Vorbenetzung, Bezugsende), darunter die knappen (Grundeinstellung,
+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_96) \
LV_FONT_DECLARE(lv_font_maven_pro_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
+371 -336
View File
@@ -20,6 +20,71 @@
#include <Preferences.h> // Dashboard-Design P4-lokal merken (NVS)
#include <esp_heap_caps.h> // Verlaufs-Historie im PSRAM allozieren (interner RAM ist knapp)
#include "ui.h"
#include <Preferences.h>
#include "esp_system.h"
// ------------------------------------------------------------------------------------
// Schriftgroesse
//
// LVGL-Schriften sind fest einkompilierte Bilddaten, es gibt sie also nur in den Groessen,
// die im Projekt liegen: 14, 18, 28, 40 und 48 Pixel. Stufenlos skalieren laesst sich
// nichts. Umschalten zwischen ihnen dagegen schon - deshalb drei Rollen statt fester
// Groessen, die je nach Einstellung eine Stufe hoeher greifen.
//
// Die grossen Anzeigen (Temperatur, Uhr) bleiben unveraendert: Sie sind bereits auf die
// Bildschirmgroesse abgestimmt und sollen nicht mitwachsen.
//
// Die Einstellung liegt lokal im P4, nicht auf der Hauptplatine - sie betrifft nur dieses
// Display. Sie wirkt beim naechsten Start: Die Schriften stecken in Hunderten bereits
// angelegter Bedienelemente, die sich nicht nachtraeglich umhaengen lassen, ohne die
// gesamte Oberflaeche neu aufzubauen.
// ------------------------------------------------------------------------------------
enum { FONT_KLEIN = 0, FONT_MITTEL = 1, FONT_GROSS = 2 };
static uint8_t g_fontStep = FONT_KLEIN;
static const lv_font_t* font_small() {
switch (g_fontStep) {
case FONT_GROSS: return &lv_font_maven_pro_28;
case FONT_MITTEL: return &lv_font_maven_pro_18;
default: return &lv_font_maven_pro_14;
}
}
static const lv_font_t* font_text() {
switch (g_fontStep) {
case FONT_GROSS: return &lv_font_maven_pro_40;
case FONT_MITTEL: return &lv_font_maven_pro_28;
default: return &lv_font_maven_pro_18;
}
}
static const lv_font_t* font_title() {
switch (g_fontStep) {
case FONT_GROSS: return &lv_font_maven_pro_48;
case FONT_MITTEL: return &lv_font_maven_pro_40;
default: return &lv_font_maven_pro_28;
}
}
static void font_step_load() {
Preferences p;
if (p.begin("anzeige", true)) {
g_fontStep = (uint8_t)p.getUChar("schrift", FONT_KLEIN);
p.end();
}
if (g_fontStep > FONT_GROSS) g_fontStep = FONT_KLEIN;
}
static void font_step_save_and_restart(uint8_t step) {
if (step > FONT_GROSS) return;
Preferences p;
if (p.begin("anzeige", false)) {
p.putUChar("schrift", step);
p.end();
}
// Der Zaehler der Absturzsicherung darf diesen gewollten Neustart nicht mitzaehlen.
{ Preferences b; if (b.begin("bootguard", false)) { b.putUShort("early", 0); b.end(); } }
delay(120);
esp_restart();
}
#include "theme.h"
#include "config.h"
#include "display_hal.h" // hal_backlight() - Helligkeit fuer den Aufweck-Dialog
@@ -101,8 +166,15 @@ 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;
#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;
@@ -117,6 +189,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;
@@ -426,7 +502,7 @@ static lv_obj_t* group_card(lv_obj_t* parent, const char* title, const char* sub
lv_obj_t* t = lv_label_create(c);
lv_label_set_text(t, title);
lv_obj_set_style_text_color(t, COL_ACCENT, 0);
lv_obj_set_style_text_font(t, &lv_font_maven_pro_18, 0);
lv_obj_set_style_text_font(t, font_text(), 0);
if (subtitle) {
lv_obj_t* s = lv_label_create(c);
lv_label_set_long_mode(s, LV_LABEL_LONG_WRAP);
@@ -460,6 +536,8 @@ static void add_save_button(lv_obj_t* parent, const char* txt, lv_event_cb_t cb)
// Nur fuer das Waveshare-Panel: Die JC-Panels haben das Tempoproblem nicht, ihr
// Bediengefuehl soll unveraendert bleiben.
// ------------------------------------------------------------------------------------
// Mehrspaltige Seiten fuer das grosse Panel
//
@@ -731,7 +809,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);
@@ -739,298 +817,91 @@ 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);
// ------------------------------------------------------------------------------------
// 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.
// ------------------------------------------------------------------------------------
// 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);
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, &lv_font_maven_pro_18, 0);
*big = lv_label_create(card);
lv_obj_set_style_text_font(t, font_small(), 0);
*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_48, 0);
*setp = lv_label_create(card);
lv_obj_set_style_text_font(*big, valueFont, 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, &lv_font_maven_pro_18, 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_text_font(*setp, font_text(), 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_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_border_width(*bar, 0, 0);
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, 6, 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;
}
// 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
// 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);
// 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_label_set_text(t, title);
lv_obj_set_style_text_color(t, accent, 0);
lv_obj_set_style_text_font(t, &lv_font_maven_pro_14, 0);
lv_obj_align(t, LV_ALIGN_CENTER, 0, -48);
*big = lv_label_create(face);
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, &lv_font_maven_pro_14, 0);
lv_obj_align(*setp, LV_ALIGN_CENTER, 0, 62);
// 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) {
lv_obj_t* ch = lv_chart_create(parent);
lv_obj_set_size(ch, LV_PCT(96), 72);
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);
lv_obj_set_style_bg_opa(ch, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(ch, 0, 0);
lv_obj_set_style_pad_all(ch, 0, 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);
return ch;
}
// 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, &lv_font_maven_pro_18, 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_label_set_text(*setp, "Soll --.-");
lv_obj_set_style_text_color(*setp, COL_TEXT_DIM, 0);
lv_obj_set_style_text_font(*setp, &lv_font_maven_pro_18, 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, &lv_font_maven_pro_18, 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, &lv_font_maven_pro_18, 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);
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_bar_set_range(*bar, 0, 100);
lv_bar_set_value(*bar, 0, LV_ANIM_OFF);
}
// 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);
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, &lv_font_maven_pro_18, 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, &lv_font_maven_pro_18, 0);
}
// 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);
@@ -1038,31 +909,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_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_96, false, 0);
g_sparkW = tempSpark(cw, COL_OK, &g_sparkSerW);
lv_obj_t* cd = tempField(g_tempRow, "DAMPF", teal, &lblTempD, &lblSetD, &barDutyD,
1, &lv_font_maven_pro_96, false, 0);
g_sparkD = tempSpark(cd, COL_ACCENT, &g_sparkSerD);
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_140, true, 0x0d0d0d);
tempField(g_tempRow, "DAMPF", teal, &lblTempD, &lblSetD, &barDutyD,
1, &lv_font_maven_pro_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_96, false, 0);
tempField(inner, "DAMPF", teal, &lblTempD, &lblSetD, &barDutyD,
1, &lv_font_maven_pro_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) {
@@ -1080,7 +1001,7 @@ static lv_obj_t* infoCell(lv_obj_t* parent, const char* title, lv_obj_t** val, l
*val = lv_label_create(card);
lv_label_set_text(*val, "--");
lv_obj_set_style_text_color(*val, COL_TEXT, 0);
lv_obj_set_style_text_font(*val, &lv_font_maven_pro_28, 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
return card;
}
@@ -1158,6 +1079,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);
@@ -1193,6 +1115,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);
@@ -1288,7 +1214,7 @@ static lv_obj_t* build_temp_arc(lv_obj_t* parent, const char* title, lv_color_t
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, &lv_font_maven_pro_18, 0);
lv_obj_set_style_text_font(t, font_text(), 0);
lv_obj_t* arc = lv_arc_create(card);
lv_obj_set_size(arc, 180, 180);
@@ -1465,7 +1391,7 @@ static void build_light(lv_obj_t* p) {
lv_obj_set_size(btnLight, 320, 150);
lv_obj_add_event_cb(btnLight, light_cb, LV_EVENT_CLICKED, nullptr);
lv_obj_t* l = lv_obj_get_child(btnLight, 0);
if (l) lv_obj_set_style_text_font(l, &lv_font_maven_pro_28, 0);
if (l) lv_obj_set_style_text_font(l, font_title(), 0);
}
// =====================================================================================
@@ -1511,6 +1437,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);
@@ -1531,13 +1471,13 @@ static void build_chart(lv_obj_t* p) {
g_chartWinBtn[i] = th_button(btns, winNames[i], COL_CARD2, COL_TEXT);
lv_obj_set_style_pad_hor(g_chartWinBtn[i], 10, 0);
lv_obj_set_style_pad_ver(g_chartWinBtn[i], 5, 0);
lv_obj_set_style_text_font(lv_obj_get_child(g_chartWinBtn[i], 0), &lv_font_maven_pro_14, 0);
lv_obj_set_style_text_font(lv_obj_get_child(g_chartWinBtn[i], 0), font_small(), 0);
lv_obj_add_event_cb(g_chartWinBtn[i], chart_win_btn_cb, LV_EVENT_CLICKED, (void*)(intptr_t)i);
}
g_chartDutyBtn = th_button(btns, "Leistung", COL_CARD2, COL_TEXT);
lv_obj_set_style_pad_hor(g_chartDutyBtn, 10, 0);
lv_obj_set_style_pad_ver(g_chartDutyBtn, 5, 0);
lv_obj_set_style_text_font(lv_obj_get_child(g_chartDutyBtn, 0), &lv_font_maven_pro_14, 0);
lv_obj_set_style_text_font(lv_obj_get_child(g_chartDutyBtn, 0), font_small(), 0);
lv_obj_add_event_cb(g_chartDutyBtn, chart_duty_btn_cb, LV_EVENT_CLICKED, nullptr);
chart_win_btns_refresh();
@@ -1569,7 +1509,7 @@ static void build_chart(lv_obj_t* p) {
g_tuneCardTitle = lv_label_create(tcol);
lv_label_set_text(g_tuneCardTitle, "PID-Tuning läuft...");
lv_obj_set_style_text_color(g_tuneCardTitle, COL_WARN, 0);
lv_obj_set_style_text_font(g_tuneCardTitle, &lv_font_maven_pro_18, 0);
lv_obj_set_style_text_font(g_tuneCardTitle, font_text(), 0);
g_tuneCardBody = lv_label_create(tcol);
lv_label_set_text(g_tuneCardBody, "--");
lv_obj_set_style_text_color(g_tuneCardBody, COL_TEXT, 0);
@@ -1626,7 +1566,7 @@ static void build_chart(lv_obj_t* p) {
lv_obj_t* ax[3] = { g_axHi, g_axMid, g_axLo };
for (int i = 0; i < 3; i++) {
lv_obj_set_style_text_color(ax[i], COL_TEXT_DIM, 0);
lv_obj_set_style_text_font(ax[i], &lv_font_maven_pro_14, 0);
lv_obj_set_style_text_font(ax[i], font_small(), 0);
lv_label_set_text(ax[i], "--");
}
@@ -1818,7 +1758,7 @@ static lv_obj_t* sum_stat_col(lv_obj_t* parent, const char* caption, lv_obj_t**
lv_obj_t* c = lv_label_create(col);
lv_label_set_text(c, caption);
lv_obj_set_style_text_color(c, COL_TEXT_DIM, 0);
lv_obj_set_style_text_font(c, &lv_font_maven_pro_14, 0);
lv_obj_set_style_text_font(c, font_small(), 0);
lv_obj_clear_flag(c, LV_OBJ_FLAG_CLICKABLE);
if (valOut) *valOut = v;
return col;
@@ -1980,7 +1920,7 @@ static lv_obj_t* bz_stat_block(lv_obj_t* parent, const char* caption, lv_obj_t**
lv_obj_t* c = lv_label_create(blk);
lv_label_set_text(c, caption);
lv_obj_set_style_text_color(c, COL_TEXT_MUT, 0);
lv_obj_set_style_text_font(c, &lv_font_maven_pro_14, 0);
lv_obj_set_style_text_font(c, font_small(), 0);
lv_obj_t* v = lv_label_create(blk);
lv_label_set_text(v, "--");
lv_obj_set_style_text_color(v, col, 0);
@@ -2013,7 +1953,7 @@ static void build_brew_live_screen() {
lv_obj_t* title = lv_label_create(top);
lv_label_set_text(title, "Bezug läuft");
lv_obj_set_style_text_color(title, COL_ACCENT, 0);
lv_obj_set_style_text_font(title, &lv_font_maven_pro_28, 0);
lv_obj_set_style_text_font(title, font_title(), 0);
lv_obj_t* closeBtn = th_button(top, LV_SYMBOL_CLOSE, COL_CARD2, COL_TEXT);
lv_obj_set_size(closeBtn, 56, 40);
lv_obj_add_event_cb(closeBtn, brew_live_close_cb, LV_EVENT_CLICKED, nullptr);
@@ -2039,11 +1979,11 @@ static void build_brew_live_screen() {
lv_obj_clear_flag(left, LV_OBJ_FLAG_SCROLLABLE);
bz_stat_block(left, "ZEIT", &g_bzTimer, &lv_font_maven_pro_48, COL_ACCENT);
g_bzWeightBlock = bz_stat_block(left, "GEWICHT", &g_bzWeight, &lv_font_maven_pro_48, COL_TEXT);
g_bzFlowBlock = bz_stat_block(left, "FLOW", &g_bzFlow, &lv_font_maven_pro_28, COL_TEXT);
g_bzFlowBlock = bz_stat_block(left, "FLOW", &g_bzFlow, font_title(), COL_TEXT);
g_bzPhase = lv_label_create(left);
lv_label_set_text(g_bzPhase, "");
lv_obj_set_style_text_color(g_bzPhase, COL_ACCENT, 0);
lv_obj_set_style_text_font(g_bzPhase, &lv_font_maven_pro_18, 0);
lv_obj_set_style_text_font(g_bzPhase, font_text(), 0);
lv_obj_add_flag(g_bzPhase, LV_OBJ_FLAG_HIDDEN);
g_bzChartWrap = lv_obj_create(main);
@@ -2058,7 +1998,7 @@ static void build_brew_live_screen() {
lv_obj_t* chartCap = lv_label_create(g_bzChartWrap);
lv_label_set_text(chartCap, "Flow-Verlauf (letzte 30 s)");
lv_obj_set_style_text_color(chartCap, COL_TEXT_MUT, 0);
lv_obj_set_style_text_font(chartCap, &lv_font_maven_pro_14, 0);
lv_obj_set_style_text_font(chartCap, font_small(), 0);
g_bzChart = lv_chart_create(g_bzChartWrap);
lv_obj_set_width(g_bzChart, LV_PCT(100));
lv_obj_set_flex_grow(g_bzChart, 1);
@@ -2090,7 +2030,7 @@ static void build_brew_live_screen() {
g_bzBarLbl = lv_label_create(g_bzBarRow);
lv_label_set_text(g_bzBarLbl, "");
lv_obj_set_style_text_color(g_bzBarLbl, COL_TEXT_DIM, 0);
lv_obj_set_style_text_font(g_bzBarLbl, &lv_font_maven_pro_18, 0);
lv_obj_set_style_text_font(g_bzBarLbl, font_text(), 0);
g_bzBar = lv_bar_create(g_bzBarRow);
lv_obj_set_size(g_bzBar, LV_PCT(100), 14);
lv_bar_set_range(g_bzBar, 0, 100);
@@ -2302,7 +2242,7 @@ static lv_obj_t* cln_stepper(lv_obj_t* parent, const char* label, lv_event_cb_t
lv_obj_t* val = lv_label_create(grp);
lv_label_set_text(val, "--");
lv_obj_set_style_text_color(val, COL_TEXT, 0);
lv_obj_set_style_text_font(val, &lv_font_maven_pro_28, 0);
lv_obj_set_style_text_font(val, font_title(), 0);
lv_obj_t* bp = th_button(grp, "+", COL_CARD2, COL_TEXT);
lv_obj_set_size(bp, 64, 56);
lv_obj_add_event_cb(bp, plus, LV_EVENT_CLICKED, nullptr);
@@ -2384,7 +2324,7 @@ static void build_cleaning_screen() {
lv_obj_t* title = lv_label_create(hdr);
lv_label_set_text(title, "Reinigungsassistent");
lv_obj_set_style_text_color(title, COL_ACCENT, 0);
lv_obj_set_style_text_font(title, &lv_font_maven_pro_18, 0);
lv_obj_set_style_text_font(title, font_text(), 0);
// --- Konfigurationsbereich (sichtbar, wenn inaktiv) ---
g_clnCfg = lv_obj_create(g_clnScreen);
@@ -2429,7 +2369,7 @@ static void build_cleaning_screen() {
lv_obj_set_width(g_clnProgLbl, LV_PCT(100));
lv_label_set_text(g_clnProgLbl, "Reinigung läuft...");
lv_obj_set_style_text_color(g_clnProgLbl, COL_TEXT, 0);
lv_obj_set_style_text_font(g_clnProgLbl, &lv_font_maven_pro_18, 0);
lv_obj_set_style_text_font(g_clnProgLbl, font_text(), 0);
lv_obj_t* stopBtn = th_button(g_clnProgress, LV_SYMBOL_STOP " Reinigung abbrechen", COL_DANGER, COL_TEXT);
lv_obj_set_width(stopBtn, LV_PCT(100));
@@ -2473,6 +2413,12 @@ static void send_reset_maint(lv_event_t*) {
g_client->sendCommand("saveService", "\"resetMaintenanceCounter\":true");
showToast("Wartungszähler zurückgesetzt", false);
}
// Schriftgroesse waehlen. Speichert und startet neu - die Schriften stecken in bereits
// angelegten Bedienelementen und lassen sich nicht nachtraeglich umhaengen.
static void font_pick_cb(lv_event_t* e) {
font_step_save_and_restart((uint8_t)(intptr_t)lv_event_get_user_data(e));
}
static void build_service(lv_obj_t* p) {
#if JC_PANEL_TYPE == WS_PANEL_7H
// Vier Karten in zwei Spalten. Frueher standen hier lose Zeilen unter blossen
@@ -2511,6 +2457,7 @@ static void build_service(lv_obj_t* p) {
lv_obj_set_width(cb, LV_PCT(100));
lv_obj_add_event_cb(cb, cln_open, LV_EVENT_CLICKED, nullptr);
page_grid(p, PAGE_COLS_2, PAGE_ROWS_2C);
grid_at(cMaint, 0, 0); grid_at(cFlush, 1, 0);
grid_at(cLight, 0, 1); grid_at(cCln, 1, 1);
@@ -2799,7 +2746,7 @@ static void prof_picker_open() {
lv_obj_t* t = lv_label_create(hdr);
lv_label_set_text(t, "Profil laden");
lv_obj_set_style_text_color(t, COL_ACCENT, 0);
lv_obj_set_style_text_font(t, &lv_font_maven_pro_18, 0);
lv_obj_set_style_text_font(t, font_text(), 0);
lv_obj_t* x = th_button(hdr, LV_SYMBOL_CLOSE, COL_CARD2, COL_TEXT);
lv_obj_add_event_cb(x, prof_picker_close_cb, LV_EVENT_CLICKED, nullptr);
@@ -2879,7 +2826,7 @@ static void info_block(lv_obj_t* card, const char* title, lv_obj_t** valOut, con
lv_obj_t* t = lv_label_create(card);
lv_label_set_text(t, title);
lv_obj_set_style_text_color(t, COL_ACCENT, 0);
lv_obj_set_style_text_font(t, &lv_font_maven_pro_18, 0);
lv_obj_set_style_text_font(t, font_text(), 0);
lv_obj_t* v = lv_label_create(card);
lv_obj_set_width(v, LV_PCT(100));
lv_label_set_text(v, initial);
@@ -2887,7 +2834,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;
@@ -2931,6 +2878,19 @@ static void build_info(lv_obj_t* p) {
lv_obj_add_event_cb(g_designBtn[i], design_btn_cb, LV_EVENT_CLICKED, (void*)(intptr_t)i);
}
design_btns_refresh();
lv_obj_t* fl = lv_label_create(dcard);
lv_label_set_text(fl, "Schriftgröße (Neustart des Displays)");
lv_obj_set_style_text_color(fl, COL_TEXT_DIM, 0);
lv_obj_t* frow = row(dcard, nullptr);
lv_obj_set_style_pad_column(frow, 8, 0);
static const char* FONT_NAMEN[3] = { "Klein", "Mittel", "Groß" };
for (uint8_t i = 0; i < 3; i++) {
lv_obj_t* fb = (i == g_fontStep) ? th_accent_button(frow, FONT_NAMEN[i])
: th_button(frow, FONT_NAMEN[i], COL_CARD2, COL_TEXT);
lv_obj_set_flex_grow(fb, 1);
lv_obj_add_event_cb(fb, font_pick_cb, LV_EVENT_CLICKED, (void*)(intptr_t)i);
}
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);
@@ -3275,16 +3235,23 @@ 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_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
lv_obj_t* scr = lv_screen_active();
lv_obj_set_style_bg_color(scr, COL_BG, 0);
// Grundschrift am Bildschirm: Alles, was keine eigene Schrift setzt, erbt sie von hier.
lv_obj_set_style_text_font(scr, font_small(), 0);
lv_obj_clear_flag(scr, LV_OBJ_FLAG_SCROLLABLE);
// --- Kopfleiste ---
@@ -3314,7 +3281,7 @@ void ui_init(ProtocolClient* client) {
lv_obj_set_style_text_color(g_hdrTitle, COL_TEXT, 0);
lv_obj_set_style_pad_left(g_hdrTitle, 10, 0);
lv_obj_set_flex_grow(g_hdrTitle, 1);
lv_obj_set_style_text_font(g_hdrTitle, &lv_font_maven_pro_18, 0);
lv_obj_set_style_text_font(g_hdrTitle, font_text(), 0);
// Profil-Chip: zuletzt geladenes Profil ("*" = seitdem geaenderte Einstellungen).
// Antippen oeffnet die Profil-Schnellwahl. Unsichtbar, solange die S3 kein Profil meldet.
@@ -3362,7 +3329,7 @@ void ui_init(ProtocolClient* client) {
lv_label_set_text(g_clock, "");
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, &lv_font_maven_pro_18, 0);
lv_obj_set_style_text_font(g_clock, font_text(), 0);
g_wifiIcon = lv_label_create(header);
lv_label_set_text(g_wifiIcon, LV_SYMBOL_WIFI);
@@ -3494,7 +3461,7 @@ void ui_init(ProtocolClient* client) {
lv_obj_set_width(g_wakeMsg, LV_PCT(100));
lv_obj_set_style_text_align(g_wakeMsg, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_style_text_color(g_wakeMsg, COL_TEXT, 0);
lv_obj_set_style_text_font(g_wakeMsg, &lv_font_maven_pro_18, 0);
lv_obj_set_style_text_font(g_wakeMsg, font_text(), 0);
lv_label_set_text(g_wakeMsg, "Die Maschine befindet sich im Standby.\nAus dem Standby aufwecken?");
lv_obj_t* wakeBtns = lv_obj_create(wakeCard);
lv_obj_set_width(wakeBtns, LV_PCT(100));
@@ -3538,15 +3505,15 @@ void ui_init(ProtocolClient* client) {
lv_obj_t* otaTitle = lv_label_create(g_otaScreen);
lv_label_set_text(otaTitle, "Display-Firmware-Update");
lv_obj_set_style_text_color(otaTitle, COL_ACCENT, 0);
lv_obj_set_style_text_font(otaTitle, &lv_font_maven_pro_28, 0);
lv_obj_set_style_text_font(otaTitle, font_title(), 0);
g_otaStatus = lv_label_create(g_otaScreen);
lv_label_set_text(g_otaStatus, "Bitte warten...");
lv_obj_set_style_text_color(g_otaStatus, COL_TEXT, 0);
lv_obj_set_style_text_font(g_otaStatus, &lv_font_maven_pro_18, 0);
lv_obj_set_style_text_font(g_otaStatus, font_text(), 0);
g_otaBytes = lv_label_create(g_otaScreen);
lv_label_set_text(g_otaBytes, "");
lv_obj_set_style_text_color(g_otaBytes, COL_TEXT_DIM, 0);
lv_obj_set_style_text_font(g_otaBytes, &lv_font_maven_pro_18, 0);
lv_obj_set_style_text_font(g_otaBytes, font_text(), 0);
lv_obj_add_flag(g_otaScreen, LV_OBJ_FLAG_HIDDEN);
// Shot-Zusammenfassung (zentrierte Karte mit grossen Werten, antippbar zum Schliessen)
@@ -3571,7 +3538,7 @@ void ui_init(ProtocolClient* client) {
lv_obj_t* sumTitle = lv_label_create(g_shotSum);
lv_label_set_text(sumTitle, "Bezug beendet");
lv_obj_set_style_text_color(sumTitle, COL_ACCENT, 0);
lv_obj_set_style_text_font(sumTitle, &lv_font_maven_pro_28, 0);
lv_obj_set_style_text_font(sumTitle, font_title(), 0);
lv_obj_t* sumRow = lv_obj_create(g_shotSum);
lv_obj_set_size(sumRow, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(sumRow, LV_OPA_TRANSP, 0);
@@ -3608,7 +3575,7 @@ void ui_init(ProtocolClient* client) {
lv_obj_t* splashSub = lv_label_create(g_splash);
lv_label_set_text(splashSub, "Siebträgersteuerung");
lv_obj_set_style_text_color(splashSub, COL_TEXT_DIM, 0);
lv_obj_set_style_text_font(splashSub, &lv_font_maven_pro_18, 0);
lv_obj_set_style_text_font(splashSub, font_text(), 0);
lv_obj_t* splashSpin = lv_spinner_create(g_splash);
lv_spinner_set_anim_params(splashSpin, 1000, 60);
lv_obj_set_size(splashSpin, 48, 48);
@@ -3618,7 +3585,7 @@ void ui_init(ProtocolClient* client) {
lv_obj_t* splashVer = lv_label_create(g_splash);
lv_label_set_text(splashVer, "Firmware " DISPLAY_FW_VERSION);
lv_obj_set_style_text_color(splashVer, COL_TEXT_MUT, 0);
lv_obj_set_style_text_font(splashVer, &lv_font_maven_pro_14, 0);
lv_obj_set_style_text_font(splashVer, font_small(), 0);
g_splashActive = true;
ui_fade_in(g_splash, UI_FADE_MS); // weiches Erscheinen beim Boot
@@ -3674,6 +3641,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_96 : g_shotFontNormal, 0);
if (lblBezug && g_bezugFontNormal)
lv_obj_set_style_text_font(lblBezug, on ? &lv_font_maven_pro_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;
@@ -4013,6 +4044,10 @@ void ui_update(const MachineState& st) {
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