feat(P4): Profil-Schnellwahl im Header + Anzeige des aktiven Profils

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 <noreply@anthropic.com>
This commit is contained in:
raw-designs
2026-07-13 20:55:10 +02:00
co-authored by Claude Fable 5
parent 00a4c80b13
commit d97e4c34d5
6 changed files with 172 additions and 3 deletions
+2
View File
@@ -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 |
+13
View File
@@ -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
+1 -1
View File
@@ -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
+2
View File
@@ -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;
+2
View File
@@ -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;
+152 -2
View File
@@ -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);
}
}