From d97e4c34d53153678b829454e091c00599d2bff5 Mon Sep 17 00:00:00 2001 From: raw-designs Date: Mon, 13 Jul 2026 20:55:10 +0200 Subject: [PATCH] feat(P4): Profil-Schnellwahl im Header + Anzeige des aktiven Profils MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neuer Profil-Chip im Header (alle Seiten): zeigt das zuletzt geladene bzw. gespeicherte Profil aus den neuen S3-State-Feldern profile/profDirty (S3 ab 5.1.1), „*" = seitdem geänderte Einstellungen. Antippen öffnet ein Schnellwahl-Overlay mit allen Profilen (aktives mit Häkchen und Akzentfarbe); ein Tap lädt direkt per loadProfile. Profile-Seite markiert das aktive Profil in der Liste. Bei älterer S3-Firmware bleibt der Chip unsichtbar. Version 1.0.13, Protokoll-Doku ergänzt. Co-Authored-By: Claude Fable 5 --- Doku/JC-Display_UART-Protokoll.md | 2 + JC_Display_Firmware/Changelog.txt | 13 ++ JC_Display_Firmware/config.h | 2 +- JC_Display_Firmware/machine_state.h | 2 + JC_Display_Firmware/protocol_client.cpp | 2 + JC_Display_Firmware/ui.cpp | 154 +++++++++++++++++++++++- 6 files changed, 172 insertions(+), 3 deletions(-) diff --git a/Doku/JC-Display_UART-Protokoll.md b/Doku/JC-Display_UART-Protokoll.md index f25f862..18f953c 100644 --- a/Doku/JC-Display_UART-Protokoll.md +++ b/Doku/JC-Display_UART-Protokoll.md @@ -157,6 +157,8 @@ Erzeugt von `buildTouchUartStateJson()`. Alle Felder in einem flachen JSON-Objek | `steamHeatDisabled` | bool | Dampfheizung per Nutzer deaktiviert | | `lightOn` | bool | Beleuchtung an | | `piezoEnabled` | bool | Piezo-Summer aktiv | +| `profile` | string | zuletzt geladenes/gespeichertes Profil, leer = keines (additiv seit S3 5.1.1) | +| `profDirty` | bool | profil-relevante Einstellungen seit dem Laden geändert (additiv seit S3 5.1.1) | | `weight` | float | aktuelles Waagengewicht (UI) | | `targetWeight` | float | Ziel-Gewicht (Brew-by-Weight) | | `scaleEnabled` | bool | Waage aktiviert | diff --git a/JC_Display_Firmware/Changelog.txt b/JC_Display_Firmware/Changelog.txt index 37c9328..d3df445 100644 --- a/JC_Display_Firmware/Changelog.txt +++ b/JC_Display_Firmware/Changelog.txt @@ -1,3 +1,16 @@ +Version 1.0.13: +- Profil-Schnellwahl + Anzeige des aktiven Profils (braucht S3-Firmware ab 5.1.1): + - Neuer Profil-Chip im Header (auf allen Seiten sichtbar): zeigt das zuletzt geladene bzw. + gespeicherte Profil; ein „*" hinter dem Namen bedeutet, dass seitdem profil-relevante + Einstellungen geändert wurden. Lange Namen werden gekürzt; ohne bekanntes Profil + (oder mit älterer S3-Firmware) bleibt der Chip unsichtbar. + - Antippen des Chips öffnet die Profil-Schnellwahl: ein Overlay mit allen gespeicherten + Profilen, das aktive mit Häkchen und Akzentfarbe hervorgehoben. Ein Tap lädt das Profil + direkt (Toast bestätigt, Chip aktualisiert sich mit dem nächsten State) — zwei Berührungen + vom Dashboard zum Profilwechsel statt Umweg über die Profile-Seite. + - Profile-Seite: das aktive Profil wird in der Liste mit Häkchen-Symbol und Akzentfarbe + markiert. + Version 1.0.12: - Verlauf-Chart: Flackernde Heizleistungs-Kurven in den großen Zeitfenstern behoben. Die Dezimierung pickte pro Chart-Punkt ein Einzelsample, dessen Abtast-Raster mit jedem diff --git a/JC_Display_Firmware/config.h b/JC_Display_Firmware/config.h index f3058e7..016c105 100644 --- a/JC_Display_Firmware/config.h +++ b/JC_Display_Firmware/config.h @@ -65,7 +65,7 @@ // ------------------------------------------------------------------------------------- // Firmware // ------------------------------------------------------------------------------------- -#define DISPLAY_FW_VERSION "1.0.12" // Firmware-Stand der P4-Display-Steuerung (Info-Seite) +#define DISPLAY_FW_VERSION "1.0.13" // Firmware-Stand der P4-Display-Steuerung (Info-Seite) // ------------------------------------------------------------------------------------- // Protokoll diff --git a/JC_Display_Firmware/machine_state.h b/JC_Display_Firmware/machine_state.h index 418f665..54c4c7b 100644 --- a/JC_Display_Firmware/machine_state.h +++ b/JC_Display_Firmware/machine_state.h @@ -50,6 +50,8 @@ struct MachineState { long heatUpRemainSec = -1; // verbleibende Sekunden bis "durchgewaermt" (-1 = aus) bool lightOn = false; bool piezoEnabled = false; + String activeProfile; // zuletzt geladenes/gespeichertes Profil (leer = keines; seit S3 5.1.1) + bool profileDirty = false; // Einstellungen weichen seit dem Laden vom Profil ab // --- Waage --- float weight = 0, targetWeight = 0; diff --git a/JC_Display_Firmware/protocol_client.cpp b/JC_Display_Firmware/protocol_client.cpp index ba55193..cb22bd0 100644 --- a/JC_Display_Firmware/protocol_client.cpp +++ b/JC_Display_Firmware/protocol_client.cpp @@ -192,6 +192,8 @@ void ProtocolClient::applyState(const String& json) { s.heatUpRemainSec = doc["heatUpRemainSec"] | (long)-1; s.lightOn = doc["lightOn"] | false; s.piezoEnabled = doc["piezoEnabled"] | false; + s.activeProfile = String((const char*)(doc["profile"] | "")); // fehlt bei aelterer S3-FW -> leer + s.profileDirty = doc["profDirty"] | false; s.weight = doc["weight"] | s.weight; s.targetWeight = doc["targetWeight"] | s.targetWeight; diff --git a/JC_Display_Firmware/ui.cpp b/JC_Display_Firmware/ui.cpp index 9a939ba..143cf10 100644 --- a/JC_Display_Firmware/ui.cpp +++ b/JC_Display_Firmware/ui.cpp @@ -104,6 +104,12 @@ static lv_obj_t* bulbTempW = nullptr; static lv_obj_t* bulbTempD = nullptr; static lv_obj_t* g_designBtn[TD_DESIGN_COUNT] = {}; // Umschalter auf der Info-Seite static lv_obj_t* g_readyPill = nullptr; // "BEREIT"-Pille im Header (Kessel im Zielband) static bool g_readyState = false; // mit Hysterese, gegen Flackern an der Bandgrenze +// Profil-Chip im Header (zuletzt geladenes Profil; "*" = danach geaenderte Einstellungen). +// Antippen oeffnet die Profil-Schnellwahl (Overlay mit allen Profilen zum Direkt-Laden). +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* 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; @@ -2375,6 +2381,112 @@ static void prof_save_cb(lv_event_t*) { } static void prof_refresh_cb(lv_event_t*) { if (g_client) g_client->sendListProfiles(); } +// --- Profil-Schnellwahl: Overlay mit allen Profilen zum Direkt-Laden ------------------ +// Geoeffnet ueber den Profil-Chip im Header; ein Tap laedt das Profil sofort (die +// Bestaetigung uebernimmt der Toast + der aktualisierte Chip aus dem naechsten State). +static void prof_picker_close() { + if (!g_profPicker) return; + lv_obj_delete(g_profPicker); + g_profPicker = nullptr; + g_profPickerList = nullptr; + g_profPickerInfo = nullptr; +} +static void prof_picker_bg_cb(lv_event_t* e) { + // nur ein Tap auf die Abdunkelung selbst schliesst (Karte bubbelt nicht) + if (lv_event_get_target(e) == lv_event_get_current_target(e)) prof_picker_close(); +} +static void prof_picker_close_cb(lv_event_t*) { prof_picker_close(); } +static void prof_picker_load_cb(lv_event_t* e) { + lv_obj_t* btn = (lv_obj_t*)lv_event_get_current_target(e); + lv_obj_t* lbl = lv_obj_get_child(btn, 0); // Kind 0 = Name (pur), Kind 1 = optionales Haekchen + if (!lbl || !g_client) return; + String name = lv_label_get_text(lbl); + prof_picker_close(); + if (name == g_state.activeProfile && !g_state.profileDirty) { + showToast(("Profil ist bereits aktiv: " + name).c_str(), false); + return; + } + g_client->sendLoadProfile(name); + showToast(("Lade Profil: " + name).c_str(), false); +} +static void prof_picker_fill(const String profiles[], int count) { + if (!g_profPickerList) return; + lv_obj_clean(g_profPickerList); + if (g_profPickerInfo) { + if (count == 0) { + lv_label_set_text(g_profPickerInfo, "Keine Profile gespeichert."); + lv_obj_remove_flag(g_profPickerInfo, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(g_profPickerInfo, LV_OBJ_FLAG_HIDDEN); + } + } + for (int i = 0; i < count; i++) { + bool active = (profiles[i] == g_state.activeProfile); + lv_obj_t* b = lv_button_create(g_profPickerList); + lv_obj_set_width(b, LV_PCT(100)); + lv_obj_set_style_bg_color(b, active ? COL_ACCENT : COL_CARD2, 0); + lv_obj_set_style_radius(b, 10, 0); + lv_obj_set_style_pad_ver(b, 12, 0); + lv_obj_set_style_pad_hor(b, 14, 0); + lv_obj_set_style_shadow_width(b, 0, 0); + lv_obj_t* l = lv_label_create(b); // Kind 0: purer Profilname (prof_picker_load_cb liest ihn) + lv_label_set_text(l, profiles[i].c_str()); + lv_obj_set_style_text_color(l, active ? lv_color_hex(0x000000) : COL_TEXT, 0); + lv_obj_align(l, LV_ALIGN_LEFT_MID, 0, 0); + if (active) { + lv_obj_t* chk = lv_label_create(b); + lv_label_set_text(chk, LV_SYMBOL_OK); + lv_obj_set_style_text_color(chk, lv_color_hex(0x000000), 0); + lv_obj_align(chk, LV_ALIGN_RIGHT_MID, 0, 0); + } + lv_obj_add_event_cb(b, prof_picker_load_cb, LV_EVENT_CLICKED, nullptr); + } +} +static void prof_picker_open() { + if (g_profPicker) return; + g_profPicker = lv_obj_create(lv_screen_active()); + lv_obj_set_size(g_profPicker, LV_PCT(100), LV_PCT(100)); + lv_obj_set_pos(g_profPicker, 0, 0); + lv_obj_set_style_bg_color(g_profPicker, lv_color_hex(0x000000), 0); + lv_obj_set_style_bg_opa(g_profPicker, LV_OPA_60, 0); + lv_obj_set_style_border_width(g_profPicker, 0, 0); + lv_obj_set_style_radius(g_profPicker, 0, 0); + lv_obj_set_style_pad_all(g_profPicker, 0, 0); + lv_obj_clear_flag(g_profPicker, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_event_cb(g_profPicker, prof_picker_bg_cb, LV_EVENT_CLICKED, nullptr); + + lv_obj_t* card = th_card(g_profPicker); + lv_obj_set_size(card, 460, 400); + lv_obj_center(card); + lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(card, 8, 0); + lv_obj_clear_flag(card, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_t* hdr = row(card, nullptr); + 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_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); + + g_profPickerInfo = lv_label_create(card); + lv_label_set_text(g_profPickerInfo, "Lade Profile..."); + lv_obj_set_style_text_color(g_profPickerInfo, COL_TEXT_DIM, 0); + + g_profPickerList = lv_obj_create(card); + lv_obj_set_width(g_profPickerList, LV_PCT(100)); + lv_obj_set_flex_grow(g_profPickerList, 1); + lv_obj_set_style_bg_opa(g_profPickerList, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(g_profPickerList, 0, 0); + lv_obj_set_style_pad_all(g_profPickerList, 0, 0); + lv_obj_set_style_pad_row(g_profPickerList, 6, 0); + lv_obj_set_flex_flow(g_profPickerList, LV_FLEX_FLOW_COLUMN); + + if (g_client) g_client->sendListProfiles(); // Antwort fuellt via ui_set_profiles/prof_picker_fill +} +static void prof_chip_cb(lv_event_t*) { prof_picker_open(); } + static void build_profiles(lv_obj_t* p) { lv_obj_clear_flag(p, LV_OBJ_FLAG_SCROLLABLE); @@ -2859,6 +2971,22 @@ void ui_init(ProtocolClient* client) { lv_obj_set_flex_grow(g_hdrTitle, 1); lv_obj_set_style_text_font(g_hdrTitle, &lv_font_maven_pro_18, 0); + // Profil-Chip: zuletzt geladenes Profil ("*" = seitdem geaenderte Einstellungen). + // Antippen oeffnet die Profil-Schnellwahl. Unsichtbar, solange die S3 kein Profil meldet. + g_profChip = lv_label_create(header); + lv_label_set_text(g_profChip, ""); + lv_obj_set_style_bg_color(g_profChip, COL_CARD2, 0); + lv_obj_set_style_bg_opa(g_profChip, LV_OPA_COVER, 0); + lv_obj_set_style_text_color(g_profChip, COL_TEXT, 0); + lv_obj_set_style_radius(g_profChip, 12, 0); + lv_obj_set_style_pad_hor(g_profChip, 10, 0); + lv_obj_set_style_pad_ver(g_profChip, 3, 0); + lv_obj_set_style_margin_right(g_profChip, 10, 0); + lv_obj_add_flag(g_profChip, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_ext_click_area(g_profChip, 8); + lv_obj_add_event_cb(g_profChip, prof_chip_cb, LV_EVENT_CLICKED, nullptr); + lv_obj_add_flag(g_profChip, LV_OBJ_FLAG_HIDDEN); + g_readyPill = lv_label_create(header); // "BEREIT": Kessel im Zielband (auf allen Seiten sichtbar) lv_label_set_text(g_readyPill, "BEREIT"); lv_obj_set_style_bg_color(g_readyPill, COL_OK, 0); @@ -3437,6 +3565,24 @@ void ui_update(const MachineState& st) { else ui_fade_out_hide(g_readyPill, UI_FADE_MS); } } + // Profil-Chip im Header: Name des zuletzt geladenen Profils, "*" wenn seitdem + // profil-relevante Einstellungen geaendert wurden. Lange Namen werden gekuerzt, + // damit der Header nicht ueberlaeuft. Leer (alte S3-FW / kein Profil) -> versteckt. + if (g_profChip) { + if (st.activeProfile.length() > 0) { + String t = st.activeProfile; + if (t.length() > 18) { + int cut = 17; // nicht mitten in einem UTF-8-Mehrbyte-Zeichen (Umlaut) schneiden + while (cut > 0 && ((uint8_t)t[cut] & 0xC0) == 0x80) cut--; + t = t.substring(0, cut) + "..."; + } + if (st.profileDirty) t += " *"; + label_set_if_changed(g_profChip, t.c_str()); + lv_obj_remove_flag(g_profChip, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(g_profChip, LV_OBJ_FLAG_HIDDEN); + } + } // Timer-Synchronisation (die laufende Anzeige macht der 100-ms-Timer dash_timers_cb) // Tarier-Phase vor Brew-by-Weight-Bezug -> dash_timers_cb zeigt "Tariere..." im Shot-Timer g_scaleTaring = st.scaleTaring; @@ -3629,12 +3775,16 @@ void ui_update(const MachineState& st) { void ui_toast(const char* txt, bool error) { showToast(txt, error); } void ui_set_profiles(const String profiles[], int count) { + // Schnellwahl-Overlay (falls offen) mit derselben Antwort befuellen + prof_picker_fill(profiles, count); if (!profList) return; lv_obj_clean(profList); for (int i = 0; i < count; i++) { - lv_obj_t* b = lv_list_add_button(profList, LV_SYMBOL_FILE, profiles[i].c_str()); + // aktives Profil markieren (Haekchen + Akzentfarbe statt Datei-Symbol) + bool active = (profiles[i] == g_state.activeProfile); + lv_obj_t* b = lv_list_add_button(profList, active ? LV_SYMBOL_OK : LV_SYMBOL_FILE, profiles[i].c_str()); lv_obj_set_style_bg_color(b, COL_CARD2, 0); - lv_obj_set_style_text_color(b, COL_TEXT, 0); + lv_obj_set_style_text_color(b, active ? COL_ACCENT : COL_TEXT, 0); lv_obj_add_event_cb(b, prof_select_cb, LV_EVENT_CLICKED, nullptr); } }