From e96a16c3f390b3d2cec519bc3e10f89c8241c135 Mon Sep 17 00:00:00 2001 From: raw-designs Date: Sat, 11 Jul 2026 01:31:53 +0200 Subject: [PATCH] =?UTF-8?q?Bezugs-Anzeige=20inkl.=20Shot-Zusammenfassung?= =?UTF-8?q?=20erg=C3=A4nzt/erweitert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bezugs-Anzeige inkl. Shot-Zusammenfassung ergänzt/erweitert --- JC_Display_Firmware/Changelog.txt | 10 + JC_Display_Firmware/ui.cpp | 379 ++++++++++++++++++++++++++++-- 2 files changed, 366 insertions(+), 23 deletions(-) diff --git a/JC_Display_Firmware/Changelog.txt b/JC_Display_Firmware/Changelog.txt index 90364c1..cfab9b7 100644 --- a/JC_Display_Firmware/Changelog.txt +++ b/JC_Display_Firmware/Changelog.txt @@ -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 diff --git a/JC_Display_Firmware/ui.cpp b/JC_Display_Firmware/ui.cpp index 17a41d6..94fcbe4 100644 --- a/JC_Display_Firmware/ui.cpp +++ b/JC_Display_Firmware/ui.cpp @@ -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) {