Bezugs-Anzeige inkl. Shot-Zusammenfassung ergänzt/erweitert

Bezugs-Anzeige inkl. Shot-Zusammenfassung ergänzt/erweitert
This commit is contained in:
raw-designs
2026-07-11 01:31:53 +02:00
parent 428f7949d4
commit e96a16c3f3
2 changed files with 366 additions and 23 deletions
+10
View File
@@ -1,4 +1,14 @@
Version 1.0.10:
- Live-Bezugsschirm (abschaltbar): Beim Start eines echten Bezugs blendet sich ein Vollbild-Overlay
weich ein — große Bezugszeit, Live-Gewicht und Flow samt Flow-Verlaufskurve der letzten 30 Sekunden
(nur bei verbundener Waage), Fortschrittsbalken Richtung Zielgewicht (Brew-by-Weight) bzw. Zielzeit
(Brew-by-Time) und Pre-Infusion-Phasenhinweis mit Restzeit. Über das X oben rechts für den laufenden
Bezug schließbar; beim nächsten Bezug erscheint er wieder. Ein/Aus-Schalter auf der Info-Seite unter
„Darstellung", die Einstellung wird P4-lokal im NVS gespeichert und überlebt Neustarts. Während
Flush und Reinigungsassistent erscheint das Overlay bewusst nicht.
- Shot-Zusammenfassung aufgewertet: Die Karte nach Bezugsende blendet jetzt weich ein/aus (Fade),
wirft einen Schlagschatten und zeigt Dauer, Endgewicht und mittleren Flow als große Werte mit
gedimmter Beschriftung statt reiner Textzeilen (Gewicht/Flow nur bei aktiver Waage).
- Pre-Infusion-Phasenanzeige auf dem Dashboard: Während eines Bezugs mit aktiver Pre-Infusion zeigt
eine Schrittleiste die Phasen „Pre-Infusion → Pause → Extraktion" an — erledigte Schritte mit Haken,
die aktive Phase hervorgehoben (Gold) inkl. Restzeit-Countdown (100-ms-Takt). Die aktive Phase kommt
+356 -23
View File
@@ -119,10 +119,27 @@ static lv_obj_t* g_legW = nullptr; static lv_obj_t* g_legD = nullptr; static lv_
static lv_obj_t* g_axHi = nullptr; static lv_obj_t* g_axMid = nullptr; static lv_obj_t* g_axLo = nullptr; // Y-Skala
#define COL_CASE lv_color_hex(0xba68c8) // Farbe Zusatzsensor im Chart (Violett; klar distinkt von Wasser-Gold/Dampf-Teal)
// Shot-Zusammenfassung (Popup nach Bezug)
static lv_obj_t* g_shotSum = nullptr; static lv_obj_t* g_shotSumBody = nullptr;
// Shot-Zusammenfassung (Karte nach Bezug; blendet weich ein/aus)
static lv_obj_t* g_shotSum = nullptr;
static lv_obj_t* g_sumCol[3] = {}; // Spalten Dauer/Gewicht/Flow (Gewicht+Flow nur mit Waage)
static lv_obj_t* g_sumVal[3] = {};
static lv_timer_t* g_shotSumTimer = nullptr;
static bool g_sumShotWas = false;
// Live-Bezugsschirm (Vollbild-Overlay waehrend eines Bezugs; abschaltbar, NVS "brewlive")
static bool g_brewLiveEnabled = true;
static lv_obj_t* g_brewScreen = nullptr;
static lv_obj_t* g_bzTimer = nullptr; static lv_obj_t* g_bzWeight = nullptr;
static lv_obj_t* g_bzFlow = nullptr; static lv_obj_t* g_bzPhase = nullptr;
static lv_obj_t* g_bzWeightBlock = nullptr; static lv_obj_t* g_bzFlowBlock = nullptr;
static lv_obj_t* g_bzChartWrap = nullptr; // Chart samt Beschriftung (ohne Waage versteckt)
static lv_obj_t* g_bzChart = nullptr; static lv_chart_series_t* g_bzSer = nullptr;
static lv_obj_t* g_bzBarRow = nullptr; // Fortschritt Richtung Ziel (BBW/BBT), sonst versteckt
static lv_obj_t* g_bzBar = nullptr; static lv_obj_t* g_bzBarLbl = nullptr;
static float g_bzFlowMax = 0.0f; // Auto-Skalierung der Flow-Achse
static bool g_brewLiveShown = false; // Overlay ist fuer den laufenden Bezug sichtbar
static bool g_brewLiveClosed = false; // Nutzer hat das Overlay fuer diesen Bezug weggetippt
static uint8_t g_bzTick = 0; // Chart nur alle 200 ms fuettern (Timer laeuft mit 100 ms)
static lv_obj_t* swBrewLive = nullptr; // Ein/Aus-Schalter auf der Info-Seite
// Kombi-Zelle Eco/Gehaeuse (wechselt sich ab, wenn beide aktiv)
static lv_obj_t* cellEco; static lv_obj_t* lblEco; static lv_obj_t* lblEcoTitle;
static bool g_ecoCasePhase = false; // false = Eco, true = Gehaeuse (bei Wechselanzeige)
@@ -1356,32 +1373,315 @@ static void chart_sample_cb(lv_timer_t*) {
}
// =====================================================================================
// Shot-Zusammenfassung (Popup, blendet nach 8 s aus oder per Tipp)
// Shot-Zusammenfassung (Karte, blendet weich ein; nach 8 s oder per Tipp weich aus)
// =====================================================================================
static void shot_sum_hide_cb(lv_timer_t*) {
if (g_shotSum) lv_obj_add_flag(g_shotSum, LV_OBJ_FLAG_HIDDEN);
if (g_shotSum) ui_fade_out_hide(g_shotSum, UI_FADE_MS);
g_shotSumTimer = nullptr;
}
static void shot_sum_tap_cb(lv_event_t*) {
if (g_shotSum) lv_obj_add_flag(g_shotSum, LV_OBJ_FLAG_HIDDEN);
if (g_shotSum) ui_fade_out_hide(g_shotSum, UI_FADE_MS);
if (g_shotSumTimer) { lv_timer_del(g_shotSumTimer); g_shotSumTimer = nullptr; }
}
// Eine Wert-Spalte der Zusammenfassung (grosser Wert, gedimmte Beschriftung darunter)
static lv_obj_t* sum_stat_col(lv_obj_t* parent, const char* caption, lv_obj_t** valOut) {
lv_obj_t* col = lv_obj_create(parent);
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, 2, 0);
lv_obj_set_flex_flow(col, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(col, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_flex_grow(col, 1);
lv_obj_clear_flag(col, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_clear_flag(col, LV_OBJ_FLAG_CLICKABLE); // Tipp faellt auf die Karte durch (schliessen)
lv_obj_t* v = lv_label_create(col);
lv_label_set_text(v, "--");
lv_obj_set_style_text_color(v, COL_TEXT, 0);
lv_obj_set_style_text_font(v, &lv_font_maven_pro_40, 0);
lv_obj_clear_flag(v, LV_OBJ_FLAG_CLICKABLE);
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_clear_flag(c, LV_OBJ_FLAG_CLICKABLE);
if (valOut) *valOut = v;
return col;
}
static void show_shot_summary(uint32_t durMs, float weight, bool haveScale) {
if (!g_shotSum) return;
float durS = durMs / 1000.0f;
String t = "Zeit: " + String(durS, 1) + " s";
char b[24];
snprintf(b, sizeof(b), "%.1f s", durS);
if (g_sumVal[0]) lv_label_set_text(g_sumVal[0], b);
if (haveScale) {
t += "\nGewicht: " + String(weight, 1) + " g";
snprintf(b, sizeof(b), "%.1f g", weight);
if (g_sumVal[1]) lv_label_set_text(g_sumVal[1], b);
float avg = (durS > 0.1f) ? weight / durS : 0.0f;
t += "\nMittl. Flow: " + String(avg, 1) + " g/s";
snprintf(b, sizeof(b), "%.1f g/s", avg);
if (g_sumVal[2]) lv_label_set_text(g_sumVal[2], b);
}
for (int i = 1; i < 3; i++) { // Gewicht/Flow nur mit Waage
if (!g_sumCol[i]) continue;
if (haveScale) lv_obj_remove_flag(g_sumCol[i], LV_OBJ_FLAG_HIDDEN);
else lv_obj_add_flag(g_sumCol[i], LV_OBJ_FLAG_HIDDEN);
}
lv_label_set_text(g_shotSumBody, t.c_str());
lv_obj_remove_flag(g_shotSum, LV_OBJ_FLAG_HIDDEN);
lv_obj_move_foreground(g_shotSum);
ui_fade_in(g_shotSum, UI_FADE_MS);
if (g_shotSumTimer) lv_timer_reset(g_shotSumTimer);
else { g_shotSumTimer = lv_timer_create(shot_sum_hide_cb, 8000, nullptr); lv_timer_set_repeat_count(g_shotSumTimer, 1); }
}
// =====================================================================================
// Live-Bezugsschirm (Vollbild-Overlay waehrend eines Bezugs)
// =====================================================================================
// Blendet sich beim Start eines echten Bezugs (kein Flush, kein Reinigungsassistent)
// weich ein: grosse Bezugszeit, Live-Gewicht + Flow samt Verlaufskurve (nur mit Waage),
// Fortschritt Richtung Zielgewicht (BBW) bzw. Zielzeit (BBT) und Pre-Infusion-Phase.
// Ueber das X fuer den laufenden Bezug schliessbar; Ein/Aus auf der Info-Seite (NVS).
// -------------------------------------------------------------------------------------
// Aktive Pre-Infusion-Phase (1..3): bevorzugt die echte S3-Statemachine (piState),
// sonst zeitbasiert aus den gelatchten Phasendauern (aeltere S3-Firmware).
static int pi_phase_now(uint32_t el) {
if (g_piStateS3 >= 1 && g_piStateS3 <= 3) return (int)g_piStateS3;
return (el < g_piDurMs) ? 1 : ((el < g_piDurMs + g_piPauseMs) ? 2 : 3);
}
static void brew_live_hide() {
if (!g_brewLiveShown) return;
g_brewLiveShown = false;
if (g_brewScreen) ui_fade_out_hide(g_brewScreen, UI_FADE_MS);
}
static void brew_live_close_cb(lv_event_t*) {
g_brewLiveClosed = true; // nur fuer den laufenden Bezug; naechster Bezug oeffnet wieder
brew_live_hide();
}
static void brew_live_show() {
if (!g_brewScreen || g_brewLiveShown) return;
bool scale = g_state.scaleEnabled && g_state.scaleConnected;
// Ohne Waage keine Gewicht-/Flow-Bloecke und keine Kurve - der Timer traegt den Schirm
if (g_bzWeightBlock) { if (scale) lv_obj_remove_flag(g_bzWeightBlock, LV_OBJ_FLAG_HIDDEN);
else lv_obj_add_flag(g_bzWeightBlock, LV_OBJ_FLAG_HIDDEN); }
if (g_bzFlowBlock) { if (scale) lv_obj_remove_flag(g_bzFlowBlock, LV_OBJ_FLAG_HIDDEN);
else lv_obj_add_flag(g_bzFlowBlock, LV_OBJ_FLAG_HIDDEN); }
if (g_bzChartWrap) { if (scale) lv_obj_remove_flag(g_bzChartWrap, LV_OBJ_FLAG_HIDDEN);
else lv_obj_add_flag(g_bzChartWrap, LV_OBJ_FLAG_HIDDEN); }
if (g_bzChart && g_bzSer) {
lv_chart_set_all_value(g_bzChart, g_bzSer, LV_CHART_POINT_NONE);
lv_chart_set_range(g_bzChart, LV_CHART_AXIS_PRIMARY_Y, 0, 200); // 0..2 g/s Startskala
}
g_bzFlowMax = 0.0f;
g_bzTick = 0;
if (g_bzPhase) lv_obj_add_flag(g_bzPhase, LV_OBJ_FLAG_HIDDEN);
if (g_bzBarRow) lv_obj_add_flag(g_bzBarRow, LV_OBJ_FLAG_HIDDEN);
g_brewLiveShown = true;
ui_fade_in(g_brewScreen, 300); // etwas fixer als UI_FADE_MS - der Bezug laeuft schon
}
// 100-ms-Takt aus dash_timers_cb: Werte, Phase, Kurve und Zielfortschritt nachziehen
static void brew_live_tick() {
if (!g_brewLiveShown) return;
char b[48];
float el = g_shotFrozenMs / 1000.0f;
snprintf(b, sizeof(b), "%.1f s", el);
label_set_if_changed(g_bzTimer, b);
bool scale = g_state.scaleEnabled && g_state.scaleConnected;
float w = g_state.weight;
if (w > -0.05f && w <= 0.0f) w = 0.0f; // kein "-0.0 g"
float f = g_state.flowRate;
if (f < 0.0f) f = 0.0f;
if (scale) {
snprintf(b, sizeof(b), "%.1f g", w);
label_set_if_changed(g_bzWeight, b);
snprintf(b, sizeof(b), "%.1f g/s", f);
label_set_if_changed(g_bzFlow, b);
// Flow-Kurve alle 200 ms fuettern (150 Punkte = 30-s-Fenster, aeltere rutschen raus)
if (g_bzChart && g_bzSer && (++g_bzTick & 1) == 0) {
lv_chart_set_next_value(g_bzChart, g_bzSer, (int32_t)(f * 100.0f + 0.5f));
if (f > g_bzFlowMax) {
g_bzFlowMax = f;
int hi = (int)(g_bzFlowMax * 125.0f); // 25 % Luft ueber dem Maximum
if (hi < 200) hi = 200; // mindestens 0..2 g/s
lv_chart_set_range(g_bzChart, LV_CHART_AXIS_PRIMARY_Y, 0, hi);
}
}
}
// Pre-Infusion-Phase (nur bei PI-Bezug)
if (g_bzPhase) {
if (g_piShotMode) {
int phase = pi_phase_now(g_shotFrozenMs);
if (phase <= 2) {
uint32_t endMs = (phase == 1) ? g_piDurMs : (g_piDurMs + g_piPauseMs);
uint32_t remainMs = (g_shotFrozenMs < endMs) ? (endMs - g_shotFrozenMs) : 0;
snprintf(b, sizeof(b), "%s - noch %.1f s",
(phase == 1) ? "Pre-Infusion" : "Pause", remainMs / 1000.0f);
} else {
snprintf(b, sizeof(b), "Extraktion");
}
label_set_if_changed(g_bzPhase, b);
lv_obj_remove_flag(g_bzPhase, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(g_bzPhase, LV_OBJ_FLAG_HIDDEN);
}
}
// Zielfortschritt: Brew-by-Weight (Gewicht) vor Brew-by-Time (Zeit), sonst versteckt
if (g_bzBarRow && g_bzBar && g_bzBarLbl) {
bool bbw = scale && g_state.bbwEnabled && g_state.bbwTarget > 0.0f;
if (bbw || (g_shotBbtMode && g_bbtTarget > 0.0f)) {
int pct;
if (bbw) {
pct = (int)(w / g_state.bbwTarget * 100.0f + 0.5f);
snprintf(b, sizeof(b), "%.1f g / %.1f g", w, g_state.bbwTarget);
} else {
pct = (int)(el / g_bbtTarget * 100.0f + 0.5f);
snprintf(b, sizeof(b), "%.1f s / %.1f s", el, g_bbtTarget);
}
if (pct < 0) pct = 0; if (pct > 100) pct = 100;
lv_bar_set_value(g_bzBar, pct, LV_ANIM_OFF);
label_set_if_changed(g_bzBarLbl, b);
lv_obj_remove_flag(g_bzBarRow, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(g_bzBarRow, LV_OBJ_FLAG_HIDDEN);
}
}
}
// Beschriftete Wert-Zeile des Live-Bezugsschirms (Caption gedimmt, Wert gross darunter)
static lv_obj_t* bz_stat_block(lv_obj_t* parent, const char* caption, lv_obj_t** valOut,
const lv_font_t* font, lv_color_t col) {
lv_obj_t* blk = lv_obj_create(parent);
lv_obj_set_size(blk, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(blk, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(blk, 0, 0);
lv_obj_set_style_pad_all(blk, 0, 0);
lv_obj_set_style_pad_row(blk, 0, 0);
lv_obj_set_flex_flow(blk, LV_FLEX_FLOW_COLUMN);
lv_obj_clear_flag(blk, LV_OBJ_FLAG_SCROLLABLE);
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_t* v = lv_label_create(blk);
lv_label_set_text(v, "--");
lv_obj_set_style_text_color(v, col, 0);
lv_obj_set_style_text_font(v, font, 0);
if (valOut) *valOut = v;
return blk;
}
static void build_brew_live_screen() {
g_brewScreen = lv_obj_create(lv_layer_top());
lv_obj_set_size(g_brewScreen, LV_PCT(100), LV_PCT(100));
lv_obj_set_style_bg_color(g_brewScreen, COL_BG, 0);
lv_obj_set_style_bg_opa(g_brewScreen, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(g_brewScreen, 0, 0);
lv_obj_set_style_radius(g_brewScreen, 0, 0);
lv_obj_set_style_pad_all(g_brewScreen, 16, 0);
lv_obj_set_style_pad_row(g_brewScreen, 10, 0);
lv_obj_set_flex_flow(g_brewScreen, LV_FLEX_FLOW_COLUMN);
lv_obj_clear_flag(g_brewScreen, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(g_brewScreen, LV_OBJ_FLAG_CLICKABLE); // schirmt die UI darunter ab
// Kopfzeile: Titel links, Schliessen-X rechts
lv_obj_t* top = lv_obj_create(g_brewScreen);
lv_obj_set_size(top, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(top, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(top, 0, 0);
lv_obj_set_style_pad_all(top, 0, 0);
lv_obj_set_flex_flow(top, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(top, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_clear_flag(top, LV_OBJ_FLAG_SCROLLABLE);
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_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);
// Hauptbereich: Wertespalte links, Flow-Kurve rechts
lv_obj_t* main = lv_obj_create(g_brewScreen);
lv_obj_set_width(main, LV_PCT(100));
lv_obj_set_flex_grow(main, 1);
lv_obj_set_style_bg_opa(main, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(main, 0, 0);
lv_obj_set_style_pad_all(main, 0, 0);
lv_obj_set_style_pad_column(main, 16, 0);
lv_obj_set_flex_flow(main, LV_FLEX_FLOW_ROW);
lv_obj_clear_flag(main, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* left = lv_obj_create(main);
lv_obj_set_size(left, 300, LV_PCT(100));
lv_obj_set_style_bg_opa(left, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(left, 0, 0);
lv_obj_set_style_pad_all(left, 0, 0);
lv_obj_set_style_pad_row(left, 10, 0);
lv_obj_set_flex_flow(left, LV_FLEX_FLOW_COLUMN);
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_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_add_flag(g_bzPhase, LV_OBJ_FLAG_HIDDEN);
g_bzChartWrap = lv_obj_create(main);
lv_obj_set_height(g_bzChartWrap, LV_PCT(100));
lv_obj_set_flex_grow(g_bzChartWrap, 1);
lv_obj_set_style_bg_opa(g_bzChartWrap, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(g_bzChartWrap, 0, 0);
lv_obj_set_style_pad_all(g_bzChartWrap, 0, 0);
lv_obj_set_style_pad_row(g_bzChartWrap, 4, 0);
lv_obj_set_flex_flow(g_bzChartWrap, LV_FLEX_FLOW_COLUMN);
lv_obj_clear_flag(g_bzChartWrap, LV_OBJ_FLAG_SCROLLABLE);
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);
g_bzChart = lv_chart_create(g_bzChartWrap);
lv_obj_set_width(g_bzChart, LV_PCT(100));
lv_obj_set_flex_grow(g_bzChart, 1);
lv_obj_set_style_bg_color(g_bzChart, COL_CARD, 0);
lv_obj_set_style_border_color(g_bzChart, COL_BORDER, 0);
lv_obj_set_style_border_width(g_bzChart, 1, 0);
lv_obj_set_style_radius(g_bzChart, 8, 0);
lv_obj_set_style_line_width(g_bzChart, 3, LV_PART_ITEMS);
lv_obj_set_style_width(g_bzChart, 0, LV_PART_INDICATOR); // keine Punkt-Marker
lv_obj_set_style_height(g_bzChart, 0, LV_PART_INDICATOR);
lv_chart_set_type(g_bzChart, LV_CHART_TYPE_LINE);
lv_chart_set_point_count(g_bzChart, 150); // 150 * 200 ms = 30 s
lv_chart_set_update_mode(g_bzChart, LV_CHART_UPDATE_MODE_SHIFT);
lv_chart_set_div_line_count(g_bzChart, 4, 0);
lv_chart_set_range(g_bzChart, LV_CHART_AXIS_PRIMARY_Y, 0, 200); // Flow * 100 (0..2 g/s Start)
g_bzSer = lv_chart_add_series(g_bzChart, COL_ACCENT, LV_CHART_AXIS_PRIMARY_Y);
lv_chart_set_all_value(g_bzChart, g_bzSer, LV_CHART_POINT_NONE);
// Fusszeile: Fortschritt Richtung Zielgewicht/Zielzeit
g_bzBarRow = lv_obj_create(g_brewScreen);
lv_obj_set_size(g_bzBarRow, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(g_bzBarRow, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(g_bzBarRow, 0, 0);
lv_obj_set_style_pad_all(g_bzBarRow, 0, 0);
lv_obj_set_style_pad_row(g_bzBarRow, 4, 0);
lv_obj_set_flex_flow(g_bzBarRow, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(g_bzBarRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_clear_flag(g_bzBarRow, LV_OBJ_FLAG_SCROLLABLE);
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);
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);
lv_obj_set_style_radius(g_bzBar, 7, 0);
lv_obj_set_style_bg_color(g_bzBar, COL_CARD2, 0);
lv_obj_set_style_radius(g_bzBar, 7, LV_PART_INDICATOR);
lv_obj_set_style_bg_color(g_bzBar, COL_ACCENT, LV_PART_INDICATOR);
lv_obj_add_flag(g_bzBarRow, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(g_brewScreen, LV_OBJ_FLAG_HIDDEN);
}
// =====================================================================================
// Brew-Control
// =====================================================================================
@@ -1919,6 +2219,12 @@ static void design_btn_cb(lv_event_t* e) {
build_temp_row_content();
ui_update(g_state); // neu aufgebaute Anzeige sofort mit den aktuellen Werten fuellen
}
// Live-Bezugsschirm ein-/ausschalten (P4-lokal in NVS gespeichert)
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(); }
if (!g_brewLiveEnabled) brew_live_hide(); // laeuft gerade ein Bezug: sofort ausblenden
}
static void build_info(lv_obj_t* p) {
section_title(p, "Darstellung");
lv_obj_t* dcard = th_card(p);
@@ -1938,6 +2244,9 @@ 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();
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);
section_title(p, "Firmware-Stände & Verbindung");
@@ -2072,9 +2381,7 @@ static void pi_bar_update() {
return;
}
uint32_t el = g_shotFrozenMs;
int phase;
if (g_piStateS3 >= 1 && g_piStateS3 <= 3) phase = (int)g_piStateS3;
else phase = (el < g_piDurMs) ? 1 : ((el < g_piDurMs + g_piPauseMs) ? 2 : 3);
int phase = pi_phase_now(el);
// Pause-Chip nur zeigen, wenn tatsaechlich eine Pause konfiguriert ist
if (g_piChip[1]) {
@@ -2251,6 +2558,8 @@ static void dash_timers_cb(lv_timer_t*) {
}
label_set_if_changed(lblFlow, buf);
}
// Live-Bezugsschirm (Werte/Kurve/Zielfortschritt im 100-ms-Takt)
brew_live_tick();
// Pre-Infusion-Phasenleiste (Sichtbarkeit/Phase/Restzeit)
pi_bar_update();
// Statuszeile (Einblenden/Rotation der konsolidierten Meldungen)
@@ -2263,9 +2572,10 @@ static void dash_timers_cb(lv_timer_t*) {
void ui_init(ProtocolClient* client) {
g_client = client;
// Gespeichertes Dashboard-Temperaturdesign laden (P4-lokal; Standard: Rundinstrumente)
// Gespeicherte Darstellungs-Einstellungen laden (P4-lokal; Design-Standard: Rundinstrumente)
{ Preferences prefs; prefs.begin("ui", false);
g_tempDesign = prefs.getUChar("tempdsgn", TD_GAUGE);
g_brewLiveEnabled = prefs.getBool("brewlive", true);
prefs.end();
if (g_tempDesign >= TD_DESIGN_COUNT) g_tempDesign = TD_GAUGE; }
@@ -2393,6 +2703,9 @@ void ui_init(ProtocolClient* client) {
// --- Reinigungsassistent-Sub-Screen (oberste Ebene, unter Tastatur/Toast/Overlays) ---
build_cleaning_screen();
// --- Live-Bezugsschirm (ueber dem Reinigungs-Screen, unter Tastatur/Standby/OTA) ---
build_brew_live_screen();
// --- Gemeinsame Tastatur (oberste Ebene) ---
g_kb = lv_keyboard_create(lv_layer_top());
lv_keyboard_set_map(g_kb, LV_KEYBOARD_MODE_TEXT_LOWER, kb_map_lc, kb_ctrl); // QWERTZ
@@ -2500,17 +2813,21 @@ void ui_init(ProtocolClient* client) {
lv_obj_set_style_text_font(g_otaBytes, &lv_font_maven_pro_18, 0);
lv_obj_add_flag(g_otaScreen, LV_OBJ_FLAG_HIDDEN);
// Shot-Zusammenfassung (zentrierte Karte, antippbar zum Schliessen)
// Shot-Zusammenfassung (zentrierte Karte mit grossen Werten, antippbar zum Schliessen)
g_shotSum = lv_obj_create(lv_layer_top());
lv_obj_set_size(g_shotSum, 360, LV_SIZE_CONTENT);
lv_obj_set_size(g_shotSum, 460, LV_SIZE_CONTENT);
lv_obj_set_style_bg_color(g_shotSum, COL_CARD, 0);
lv_obj_set_style_bg_opa(g_shotSum, LV_OPA_COVER, 0);
lv_obj_set_style_border_color(g_shotSum, COL_ACCENT, 0);
lv_obj_set_style_border_width(g_shotSum, 2, 0);
lv_obj_set_style_radius(g_shotSum, 12, 0);
lv_obj_set_style_pad_all(g_shotSum, 18, 0);
lv_obj_set_style_pad_row(g_shotSum, 8, 0);
lv_obj_set_style_radius(g_shotSum, 14, 0);
lv_obj_set_style_shadow_width(g_shotSum, 24, 0); // hebt die Karte vom Inhalt ab
lv_obj_set_style_shadow_color(g_shotSum, lv_color_hex(0x000000), 0);
lv_obj_set_style_shadow_opa(g_shotSum, LV_OPA_60, 0);
lv_obj_set_style_pad_all(g_shotSum, 20, 0);
lv_obj_set_style_pad_row(g_shotSum, 14, 0);
lv_obj_set_flex_flow(g_shotSum, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(g_shotSum, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_clear_flag(g_shotSum, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(g_shotSum, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(g_shotSum, shot_sum_tap_cb, LV_EVENT_CLICKED, nullptr);
@@ -2519,10 +2836,18 @@ void ui_init(ProtocolClient* client) {
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);
g_shotSumBody = lv_label_create(g_shotSum);
lv_label_set_text(g_shotSumBody, "");
lv_obj_set_style_text_color(g_shotSumBody, COL_TEXT, 0);
lv_obj_set_style_text_font(g_shotSumBody, &lv_font_maven_pro_18, 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);
lv_obj_set_style_border_width(sumRow, 0, 0);
lv_obj_set_style_pad_all(sumRow, 0, 0);
lv_obj_set_style_pad_column(sumRow, 10, 0);
lv_obj_set_flex_flow(sumRow, LV_FLEX_FLOW_ROW);
lv_obj_clear_flag(sumRow, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_clear_flag(sumRow, LV_OBJ_FLAG_CLICKABLE); // Tipp faellt auf die Karte durch
g_sumCol[0] = sum_stat_col(sumRow, "Dauer", &g_sumVal[0]);
g_sumCol[1] = sum_stat_col(sumRow, "Gewicht", &g_sumVal[1]);
g_sumCol[2] = sum_stat_col(sumRow, "Mittl. Flow", &g_sumVal[2]);
lv_obj_add_flag(g_shotSum, LV_OBJ_FLAG_HIDDEN);
// --- Boot-Splash / Start-Animation (oberstes Overlay) ---
@@ -2824,7 +3149,13 @@ void ui_update(const MachineState& st) {
g_piStateS3 = st.piState;
g_piDurMs = (uint32_t)(st.piDurSecs * 1000.0f);
g_piPauseMs = (uint32_t)(st.piPauseSecs * 1000.0f);
// Live-Bezugsschirm einblenden (nur echte Bezuege; nicht im Reinigungsassistenten,
// nicht im Standby und nicht, wenn der Nutzer ihn fuer diesen Bezug weggetippt hat)
if (g_brewLiveEnabled && !g_brewLiveClosed && !g_brewLiveShown &&
!st.cleaningActive && !st.standbyActive)
brew_live_show();
} else if (st.flushActive) {
brew_live_hide(); // Sicherheitsnetz: Flush zeigt nie den Bezugsschirm
g_shotStartMs = millis() - st.flushElapsedMs; // rueckwirkend koppeln (wie Bezug)
g_shotActive = true;
g_shotWasFlush = true;
@@ -2842,6 +3173,8 @@ void ui_update(const MachineState& st) {
g_shotActive = false;
g_piShotMode = false; // Phasenleiste ausblenden (pi_bar_update)
g_piStateS3 = 0;
brew_live_hide(); // Bezug vorbei -> Overlay weich ausblenden
g_brewLiveClosed = false; // Wegtipp-Sperre gilt nur fuer den laufenden Bezug
}
// Shot-Zusammenfassung beim Ende eines echten Bezugs (kein Flush, min. 3 s)
if (g_sumShotWas && !st.shotActive) {