@@ -22,6 +22,10 @@
# include "ui.h"
# include <Preferences.h>
# include "esp_system.h"
# include "display_hal.h"
# include "esp_rom_sys.h"
# include "lvgl_private.h"
# include <stdarg.h>
// ------------------------------------------------------------------------------------
// Schriftgroesse
@@ -82,6 +86,10 @@ static void font_step_save_and_restart(uint8_t step) {
}
// Der Zaehler der Absturzsicherung darf diesen gewollten Neustart nicht mitzaehlen.
{ Preferences b ; if ( b . begin ( " bootguard " , false ) ) { b . putUShort ( " early " , 0 ) ; b . end ( ) ; } }
// Beleuchtung aus, bevor der Neustart die Bildausgabe abreissen laesst. Ohne Signal
// zeigt das Panel von sich aus ein blaues Bild - das blitzte beim Wechsel der
// Schriftgroesse kurz auf. Dunkel ist unauffaelliger als blau.
hal_backlight ( 0 ) ;
delay ( 120 ) ;
esp_restart ( ) ;
}
@@ -168,6 +176,7 @@ static void label_set_if_changed(lv_obj_t* lbl, const char* txt); // Definitio
// TD_THERMO - vertikale Thermometer-Saeulen mit Kolben und Glanzstreifen
enum TempDesign { TD_GEWICHTET = 0 , TD_VERLAUF , TD_FLAECHEN , TD_BAND , TD_DESIGN_COUNT } ;
static uint8_t g_tempDesign = TD_BAND ;
static bool g_tempColored = false ; // Temperaturzahl in der Zustandsfarbe statt weiss
# 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 ;
@@ -191,8 +200,6 @@ 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 ;
@@ -670,7 +677,9 @@ static void stat_list_set(int i, const char* dt, float d, float w, bool kalt) {
lv_label_set_text ( g_statTime [ i ] , dt ) ;
lv_label_set_text_fmt ( g_statDur [ i ] , " %.1f s " , d ) ;
if ( w > = 0.0f ) lv_label_set_text_fmt ( g_statWgt [ i ] , " %.1f g " , w ) ;
else lv_label_set_text ( g_statWgt [ i ] , " – " ) ;
// Bewusst ein einfacher Bindestrich: Der typografisch schoenere Gedankenstrich
// fehlt im Zeichensatz der Schrift und erschiene als leeres Rechteck.
else lv_label_set_text ( g_statWgt [ i ] , " - " ) ;
lv_label_set_text ( g_statCold [ i ] , kalt ? " kalt " : " " ) ;
}
@@ -683,6 +692,24 @@ static void stat_list_finish(int used) {
}
}
// Groesse der Standby-Uhr. Anders als die allgemeine Schriftgroesse braucht diese keinen
// Neustart: Es haengt an genau EINEM Textfeld, das sich sofort umstellen laesst.
enum { UHR_KLEIN = 0 , UHR_MITTEL = 1 , UHR_GROSS = 2 } ;
static uint8_t g_clockSize = UHR_GROSS ;
static lv_obj_t * g_clockSizeBtn [ 3 ] = { } ;
static const lv_font_t * clock_font ( ) {
switch ( g_clockSize ) {
case UHR_KLEIN : return & lv_font_clock_120 ;
case UHR_MITTEL : return & lv_font_clock_180 ;
default : return & lv_font_clock_240 ;
}
}
static void clock_size_apply ( ) {
if ( g_standbyClock ) lv_obj_set_style_text_font ( g_standbyClock , clock_font ( ) , 0 ) ;
}
static void scroll_ohne_nachlauf ( lv_obj_t * o ) {
# if JC_PANEL_TYPE == WS_PANEL_7H
lv_obj_clear_flag ( o , LV_OBJ_FLAG_SCROLL_MOMENTUM ) ;
@@ -772,7 +799,19 @@ static void err_icon_cb(lv_event_t*) { show_page(PG_DASH); } // Tipp auf Fehl
// Bewusst ueber das 'opa'-Style (faellt via opa_recursive auf die Kinder durch) und NICHT
// ueber 'opa_layered' - Letzteres wuerde einen Vollbild-Zwischenpuffer allozieren
// und koennte den LVGL-Speicherpool sprengen. So bleibt das Fade speicher-neutral.
// Dauer der weichen Uebergaenge (Standby-Uhr, Aufweckdialog, Bezugsuebersicht).
//
// Auf dem Waveshare-Panel: 0, also ohne Animation. Eine Deckkraft-Animation ist bei einem
// Bildspeicher in RGB888 das Teuerste, was die Oberflaeche tun kann - fuer jedes Einzelbild
// muss die gesamte Flaeche unter der Ebene neu gezeichnet und mit ihr gemischt werden. Auf
// 1280x720 dauert eine solche Blende deutlich laenger als die 450 ms, die sie dauern soll;
// spuerbar war das als Verzoegerung, bis die Standby-Uhr erschien und bis nach dem
// Fingertipp der Aufweckdialog kam. Ohne Animation erscheinen beide sofort.
# if JC_PANEL_TYPE == WS_PANEL_7H
static const uint32_t UI_FADE_MS = 0 ;
# else
static const uint32_t UI_FADE_MS = 450 ; // ruhige, unaufdringliche Dauer
# endif
static void ui_fade_exec_cb ( void * obj , int32_t v ) {
lv_obj_set_style_opa ( ( lv_obj_t * ) obj , ( lv_opa_t ) v , 0 ) ;
@@ -787,6 +826,12 @@ static void ui_fade_hide_done_cb(lv_anim_t* a) {
// Blendet 'obj' weich ein (sichtbar schalten, Opazitaet 0 -> voll).
static void ui_fade_in ( lv_obj_t * obj , uint32_t ms ) {
if ( ! obj ) return ;
if ( ms = = 0 ) { // ohne Animation: sofort sichtbar
lv_anim_delete ( obj , ui_fade_exec_cb ) ;
lv_obj_set_style_opa ( obj , LV_OPA_COVER , 0 ) ;
lv_obj_remove_flag ( obj , LV_OBJ_FLAG_HIDDEN ) ;
return ;
}
lv_anim_delete ( obj , ui_fade_exec_cb ) ; // evtl. laufende Ausblendung abbrechen
lv_obj_set_style_opa ( obj , LV_OPA_TRANSP , 0 ) ;
lv_obj_remove_flag ( obj , LV_OBJ_FLAG_HIDDEN ) ;
@@ -802,6 +847,11 @@ static void ui_fade_in(lv_obj_t* obj, uint32_t ms) {
static void ui_fade_out_hide ( lv_obj_t * obj , uint32_t ms ) {
if ( ! obj | | lv_obj_has_flag ( obj , LV_OBJ_FLAG_HIDDEN ) ) return ;
lv_anim_delete ( obj , ui_fade_exec_cb ) ;
if ( ms = = 0 ) { // ohne Animation: sofort weg
lv_obj_add_flag ( obj , LV_OBJ_FLAG_HIDDEN ) ;
lv_obj_set_style_opa ( obj , LV_OPA_COVER , 0 ) ;
return ;
}
lv_anim_t a ; lv_anim_init ( & a ) ;
lv_anim_set_var ( & a , obj ) ;
lv_anim_set_exec_cb ( & a , ui_fade_exec_cb ) ;
@@ -835,6 +885,18 @@ static void ui_fade_out_delete(lv_obj_t* obj, uint32_t ms) {
static int standby_backlight ( ) {
return g_state . standbyShowClock ? ( int ) g_state . backlightStandbyClock : 0 ;
}
// Der Aufweckdialog schliesst sich von selbst, wenn niemand entscheidet. Sonst bliebe das
// Display hell stehen, weil der Dialog auf normale Helligkeit schaltet - jemand tippt im
// Vorbeigehen versehentlich auf die Uhr, und die Maschine leuchtet die ganze Nacht.
// Zeitablauf wird wie "Abbrechen" gewertet: zurueck in die Standby-Helligkeit.
# define WAKE_DIALOG_TIMEOUT_MS 30000
static lv_timer_t * g_wakeTimeout = nullptr ;
static void wake_timeout_stop ( ) {
if ( g_wakeTimeout ) { lv_timer_delete ( g_wakeTimeout ) ; g_wakeTimeout = nullptr ; }
}
static void wake_timeout_cb ( lv_timer_t * t ) ; // weiter unten, braucht wake_cancel()
static void wake_confirm_show ( ) {
if ( ! g_wakeConfirm ) return ;
g_wakeDialogOpen = true ;
@@ -844,6 +906,9 @@ static void wake_confirm_show() {
hal_backlight ( bl ) ;
lv_obj_remove_flag ( g_wakeConfirm , LV_OBJ_FLAG_HIDDEN ) ;
lv_obj_move_foreground ( g_wakeConfirm ) ;
wake_timeout_stop ( ) ;
g_wakeTimeout = lv_timer_create ( wake_timeout_cb , WAKE_DIALOG_TIMEOUT_MS , nullptr ) ;
lv_timer_set_repeat_count ( g_wakeTimeout , 1 ) ;
}
static void wake_confirm_hide ( ) { if ( g_wakeConfirm ) lv_obj_add_flag ( g_wakeConfirm , LV_OBJ_FLAG_HIDDEN ) ; }
// Einzige Quelle der Wahrheit dafuer, ob der Aufweck-Dialog die Auswahl "Espresso oder Cold
@@ -857,12 +922,19 @@ static bool wake_choice_offered() {
( g_state . cxEnabled & & g_state . cxArmable & & g_state . cxAskOnWake ) ;
}
static void standby_screen_cb ( lv_event_t * ) { wake_confirm_show ( ) ; } // Tipp auf Standby-Uhr -> nachfragen
static void wake_no_cb ( lv_event_t * ) { // Abbrechen -> zurueck in die Standby-Helligkeit
static void wake_cancel ( ) { // Abbrechen -> zurueck in die Standby-Helligkeit
wake_timeout_stop ( ) ;
wake_confirm_hide ( ) ;
g_wakeDialogOpen = false ;
if ( g_state . standbyActive ) hal_backlight ( standby_backlight ( ) ) ;
}
static void wake_no_cb ( lv_event_t * ) { wake_cancel ( ) ; }
static void wake_timeout_cb ( lv_timer_t * ) { // niemand hat entschieden -> wie Abbrechen
g_wakeTimeout = nullptr ; // Timer laeuft nur einmal, loescht sich selbst
wake_cancel ( ) ;
}
static void wake_yes_cb ( lv_event_t * ) { // Aufwecken -> hell lassen, S3 beendet den Standby
wake_timeout_stop ( ) ;
wake_confirm_hide ( ) ;
// g_wakeDialogOpen bleibt true, bis der Standby wirklich endet (ui_update) - sonst wuerde
// das Display in der Uebergangsphase kurz auf Standby-Helligkeit abdunkeln.
@@ -878,6 +950,7 @@ static void wake_yes_cb(lv_event_t*) { // Aufwecken -> hell lassen, S3 bee
// stellen, dann den Standby beenden - sonst heizt der Wasserkreis in der Zwischenzeit an,
// und genau das soll die Funktion ja verhindern.
static void wake_cold_cb ( lv_event_t * ) {
wake_timeout_stop ( ) ;
wake_confirm_hide ( ) ;
if ( g_client ) {
g_client - > sendAction ( " startColdExtraction " ) ;
@@ -922,6 +995,18 @@ 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 ) ;
}
// Farbe der grossen Temperaturzahl.
//
// Die Zustandsfarbe steckt ohnehin schon im Balken darunter. Ob die Zahl selbst mitgefaerbt
// wird, ist deshalb Geschmackssache: Eingefaerbt ist der Zustand auch aus dem Augenwinkel
// erkennbar, weiss wirkt ruhiger und laesst die Zahl als Zahl stehen.
//
// Warnungen bleiben immer rot - das ist keine Gestaltung, sondern eine Meldung.
static lv_color_t temp_value_color ( lv_color_t zustand ) {
return g_tempColored ? zustand : COL_TEXT ;
}
// ------------------------------------------------------------------------------------
// Temperaturbereich des Dashboards - vier Darstellungen
//
@@ -1003,9 +1088,7 @@ static lv_obj_t* tempSpark(lv_obj_t* parent, lv_color_t col, lv_chart_series_t**
lv_chart_set_div_line_count ( ch , 0 , 0 ) ;
// Schwach abgesetzte Flaeche: Ohne sie waere der Bereich vor dem ersten Messwert
// vollstaendig unsichtbar - man haette gar nicht erkannt, dass dort eine Kurve gehoert.
lv_obj_set_style_bg_color ( ch , lv_color_hex ( 0x151515 ) , 0 ) ;
lv_obj_set_style_bg_opa ( ch , LV_OPA_COVER , 0 ) ;
lv_obj_set_style_radius ( ch , 6 , 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 , 4 , 0 ) ;
lv_obj_set_style_size ( ch , 0 , 0 , LV_PART_INDICATOR ) ; // keine Punktmarkierungen
@@ -1236,10 +1319,6 @@ 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 ) ;
@@ -1299,7 +1378,7 @@ static void flush_targ() {
static void targ_w_set ( float v ) {
if ( v < 0 ) v = 0 ; if ( v > 135 ) v = 135 ;
g_targW = v ;
if ( lblTargW ) lv_label_set_text ( lblTargW , tempStr ( g_targW , 0 ) . c_str ( ) ) ;
if ( lblTargW ) label_set_if_changed ( lblTargW , tempStr ( g_targW , 0 ) . c_str ( ) ) ;
if ( g_arcW ) lv_arc_set_value ( g_arcW , ( int32_t ) ( g_targW + 0.5f ) ) ;
g_targWDirty = true ;
schedule_targ_send ( ) ;
@@ -1307,7 +1386,7 @@ static void targ_w_set(float v) {
static void targ_d_set ( float v ) {
if ( v < 0 ) v = 0 ; if ( v > 200 ) v = 200 ;
g_targD = v ;
if ( lblTargD ) lv_label_set_text ( lblTargD , tempStr ( g_targD , 0 ) . c_str ( ) ) ;
if ( lblTargD ) label_set_if_changed ( lblTargD , tempStr ( g_targD , 0 ) . c_str ( ) ) ;
if ( g_arcD ) lv_arc_set_value ( g_arcD , ( int32_t ) ( g_targD + 0.5f ) ) ;
g_targDDirty = true ;
schedule_targ_send ( ) ;
@@ -2975,6 +3054,25 @@ static void design_btn_cb(lv_event_t* e) {
ui_update ( g_state ) ; // neu aufgebaute Anzeige sofort mit den aktuellen Werten fuellen
}
// Live-Bezugsschirm ein-/ausschalten (P4-lokal in NVS gespeichert)
// Temperaturzahl einfaerben oder weiss lassen. Wirkt sofort - die Farbe wird bei jeder
// Zustandsmeldung neu gesetzt, es muss nichts neu aufgebaut werden.
// Groesse der Standby-Uhr waehlen. Wirkt sofort - es haengt an einem einzigen Textfeld.
static void clock_size_cb ( lv_event_t * e ) {
g_clockSize = ( uint8_t ) ( intptr_t ) lv_event_get_user_data ( e ) ;
{ Preferences prefs ; prefs . begin ( " ui " , false ) ; prefs . putUChar ( " uhrgr " , g_clockSize ) ; prefs . end ( ) ; }
clock_size_apply ( ) ;
for ( int i = 0 ; i < 3 ; i + + ) if ( g_clockSizeBtn [ i ] ) {
bool an = ( i = = g_clockSize ) ;
lv_obj_set_style_bg_color ( g_clockSizeBtn [ i ] , an ? COL_ACCENT : COL_CARD2 , 0 ) ;
lv_obj_set_style_text_color ( g_clockSizeBtn [ i ] , an ? lv_color_hex ( 0x000000 ) : COL_TEXT , 0 ) ;
}
}
static void temp_color_sw_cb ( lv_event_t * e ) {
g_tempColored = sw_get ( ( lv_obj_t * ) lv_event_get_target ( e ) ) ;
{ Preferences prefs ; prefs . begin ( " ui " , false ) ; prefs . putBool ( " tempcol " , g_tempColored ) ; prefs . end ( ) ; }
}
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 ( ) ; }
@@ -3012,6 +3110,24 @@ static void build_info(lv_obj_t* p) {
lv_obj_add_event_cb ( fb , font_pick_cb , LV_EVENT_CLICKED , ( void * ) ( intptr_t ) i ) ;
}
lv_obj_t * cl = lv_label_create ( dcard ) ;
lv_label_set_text ( cl , " Uhrgröße im Standby " ) ;
lv_obj_set_style_text_color ( cl , COL_TEXT_DIM , 0 ) ;
lv_obj_t * crow = row ( dcard , nullptr ) ;
lv_obj_set_style_pad_column ( crow , 8 , 0 ) ;
static const char * UHR_NAMEN [ 3 ] = { " Klein " , " Mittel " , " Groß " } ;
for ( uint8_t i = 0 ; i < 3 ; i + + ) {
lv_obj_t * b = ( i = = g_clockSize ) ? th_accent_button ( crow , UHR_NAMEN [ i ] )
: th_button ( crow , UHR_NAMEN [ i ] , COL_CARD2 , COL_TEXT ) ;
lv_obj_set_flex_grow ( b , 1 ) ;
lv_obj_add_event_cb ( b , clock_size_cb , LV_EVENT_CLICKED , ( void * ) ( intptr_t ) i ) ;
g_clockSizeBtn [ i ] = b ;
}
lv_obj_t * swTempCol = add_switch ( dcard , " Temperaturzahl in der Zustandsfarbe " ) ;
sw_set ( swTempCol , g_tempColored ) ;
lv_obj_add_event_cb ( swTempCol , temp_color_sw_cb , LV_EVENT_VALUE_CHANGED , nullptr ) ;
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 ) ;
@@ -3125,10 +3241,35 @@ static void eco_case_timer_cb(lv_timer_t*) {
// =====================================================================================
// Setzt den Labeltext nur, wenn er sich aendert -> verhindert unnoetige Invalidierung
// (im Full-Refresh-Modus loest jede Aenderung ein komplettes Neuzeichnen aus).
// LVGL macht bei jedem Setzen den Bereich der Beschriftung ungueltig - auch dann, wenn sich
// der Text gar nicht geaendert hat. Auf dem Dashboard werden im Sekundentakt Werte quer ueber
// den ganzen Bildschirm gesetzt; LVGL fasst die ungueltigen Bereiche zu einem zusammen, und
// das ist dann die gesamte Flaeche. Gemessen waren das 240 bis 300 ms pro Sekunde, in denen
// die Oberflaeche mit nichts anderem beschaeftigt war. Deshalb wird ueberall dort, wo sich
// Werte nur selten aendern, vorher verglichen.
static void label_set_if_changed ( lv_obj_t * lbl , const char * txt ) {
if ( ! lbl | | ! txt ) return ;
const char * cur = lv_label_get_text ( lbl ) ;
if ( ! cur | | strcmp ( cur , txt ) ! = 0 ) lv_label_set_text ( lbl , txt ) ;
}
// Wie oben, aber mit Formatangabe.
static void label_fmt_if_changed ( lv_obj_t * lbl , const char * fmt , . . . ) {
if ( ! lbl ) return ;
char buf [ 96 ] ;
va_list ap ; va_start ( ap , fmt ) ;
vsnprintf ( buf , sizeof ( buf ) , fmt , ap ) ;
va_end ( ap ) ;
label_set_if_changed ( lbl , buf ) ;
}
// Auch ein Farbwechsel macht den Bereich ungueltig. Gleiche Farbe -> nichts tun.
static void label_color_if_changed ( lv_obj_t * lbl , lv_color_t col ) {
if ( ! lbl ) return ;
lv_color_t cur = lv_obj_get_style_text_color ( lbl , LV_PART_MAIN ) ;
if ( lv_color_to_u32 ( cur ) ! = lv_color_to_u32 ( col ) )
lv_obj_set_style_text_color ( lbl , col , 0 ) ;
}
// Pre-Infusion-Phasenleiste aktualisieren (100-ms-Takt aus dash_timers_cb).
// Die aktive Phase kommt bevorzugt von der S3 (piState = echte Statemachine, Quelle der
// Wahrheit; Push-Latenz max. 250 ms). Sendet eine aeltere S3-Firmware das Feld noch nicht
@@ -3337,6 +3478,55 @@ static void dash_timers_cb(lv_timer_t*) {
// sonst greifen sie auf nicht angelegte LVGL-Objekte zu.
static bool g_uiReady = false ;
// ------------------------------------------------------------------------------------
// Zeitmessung fuer langsame Bildaufbauten
//
// Beim 7-Zoll-Panel dauern manche Vorgaenge spuerbar lange, und aus dem Quelltext allein
// ist nicht zu sehen, welcher Teil die Zeit frisst. LVGL meldet Anfang und Ende jedes
// Zeichenzyklus - hier wird die Dauer gemessen und nur dann ausgegeben, wenn sie eine
// Schwelle ueberschreitet. Im Normalbetrieb bleibt die Konsole still.
// ------------------------------------------------------------------------------------
# define REFR_WARN_MS 200
static uint32_t g_refrStart = 0 ;
static uint32_t g_refrPixels = 0 ;
static uint32_t g_refrAreas = 0 ;
// Beim Start eines Zeichenzyklus festhalten, WIE VIEL neu gezeichnet wird. Erst damit laesst
// sich unterscheiden, ob LVGL nur kleine Bereiche anfasst - dann ist das Zeichnen an sich zu
// langsam - oder ob jedes Mal der ganze Bildschirm neu entsteht, weil ihn etwas unnoetig
// ungueltig macht.
static void refr_start_cb ( lv_event_t * ) {
g_refrStart = lv_tick_get ( ) ;
g_refrPixels = 0 ;
g_refrAreas = 0 ;
lv_display_t * d = lv_display_get_default ( ) ;
if ( ! d ) return ;
for ( uint16_t i = 0 ; i < d - > inv_p ; i + + ) {
if ( d - > inv_area_joined [ i ] ) continue ;
const lv_area_t * a = & d - > inv_areas [ i ] ;
g_refrPixels + = ( uint32_t ) ( a - > x2 - a - > x1 + 1 ) * ( uint32_t ) ( a - > y2 - a - > y1 + 1 ) ;
g_refrAreas + + ;
}
}
static void refr_ready_cb ( lv_event_t * ) {
uint32_t d = lv_tick_get ( ) - g_refrStart ;
if ( d < REFR_WARN_MS ) return ;
unsigned pct = ( unsigned ) ( ( uint64_t ) g_refrPixels * 100ULL / ( ( uint64_t ) LCD_H_RES * LCD_V_RES ) ) ;
esp_rom_printf ( " [UI] Bildaufbau %u ms, %u Bereiche, %u %% der Flaeche \n " ,
( unsigned ) d , ( unsigned ) g_refrAreas , pct ) ;
}
static void refr_monitor_install ( ) {
lv_display_t * d = lv_display_get_default ( ) ;
if ( ! d ) return ;
lv_display_add_event_cb ( d , refr_start_cb , LV_EVENT_REFR_START , nullptr ) ;
lv_display_add_event_cb ( d , refr_ready_cb , LV_EVENT_REFR_READY , nullptr ) ;
}
void ui_init ( ProtocolClient * client ) {
g_client = client ;
@@ -3354,6 +3544,9 @@ void ui_init(ProtocolClient* client) {
g_tempDesign = prefs . getUChar ( " tempdsgn " , TD_BAND ) ;
if ( g_tempDesign > = TD_DESIGN_COUNT ) g_tempDesign = TD_BAND ;
g_brewLiveEnabled = prefs . getBool ( " brewlive " , true ) ;
g_tempColored = prefs . getBool ( " tempcol " , false ) ;
g_clockSize = prefs . getUChar ( " uhrgr " , UHR_GROSS ) ;
if ( g_clockSize > UHR_GROSS ) g_clockSize = UHR_GROSS ;
g_chartWin = prefs . getUChar ( " chartwin " , 0 ) ;
if ( g_chartWin > 3 ) g_chartWin = 0 ;
g_chartDuty = prefs . getBool ( " chartduty " , false ) ;
@@ -3477,8 +3670,16 @@ void ui_init(ProtocolClient* client) {
g_drawer = lv_obj_create ( scr ) ;
lv_obj_set_size ( g_drawer , LV_PCT ( 100 ) , DISP_VER_RES - 44 ) ;
lv_obj_set_pos ( g_drawer , 0 , 44 ) ;
# if JC_PANEL_TYPE == WS_PANEL_7H
// Deckend statt halbdurchsichtig - aus demselben Grund wie beim Aufweckdialog: Ein
// durchscheinender Vorhang laesst LVGL die ganze Flaeche darunter mitzeichnen und
// mischen. Auf dunklem Grund sieht ein dunkles Deckend praktisch genauso aus.
lv_obj_set_style_bg_color ( g_drawer , lv_color_hex ( 0x0d0d0d ) , 0 ) ;
lv_obj_set_style_bg_opa ( g_drawer , LV_OPA_COVER , 0 ) ;
# else
lv_obj_set_style_bg_color ( g_drawer , lv_color_hex ( 0x000000 ) , 0 ) ;
lv_obj_set_style_bg_opa ( g_drawer , LV_OPA_50 , 0 ) ;
# endif
lv_obj_set_style_border_width ( g_drawer , 0 , 0 ) ;
lv_obj_set_style_pad_all ( g_drawer , 0 , 0 ) ;
lv_obj_set_style_radius ( g_drawer , 0 , 0 ) ;
@@ -3545,7 +3746,7 @@ void ui_init(ProtocolClient* client) {
g_standbyClock = lv_label_create ( g_standbyScreen ) ;
lv_label_set_text ( g_standbyClock , " --:-- " ) ;
lv_obj_set_style_text_color ( g_standbyClock , COL_TEXT , 0 ) ;
lv_obj_set_style_text_font ( g_standbyClock , & lv_font_clock_240 , 0 ) ; // g rosse Uhr im Standby
lv_obj_set_style_text_font ( g_standbyClock , clock_font ( ) , 0 ) ; // G roe sse einstellbar
lv_obj_center ( g_standbyClock ) ;
lv_obj_add_event_cb ( g_standbyScreen , standby_screen_cb , LV_EVENT_CLICKED , nullptr ) ; // Tippen -> Aufweck-Abfrage
lv_obj_t * standbyHint = lv_label_create ( g_standbyScreen ) ; // dezenter Hinweis am unteren Rand
@@ -3558,13 +3759,25 @@ void ui_init(ProtocolClient* client) {
g_wakeConfirm = lv_obj_create ( lv_layer_top ( ) ) ;
lv_obj_set_size ( g_wakeConfirm , LV_PCT ( 100 ) , LV_PCT ( 100 ) ) ;
lv_obj_set_style_bg_color ( g_wakeConfirm , lv_color_hex ( 0x000000 ) , 0 ) ;
# if JC_PANEL_TYPE == WS_PANEL_7H
// Deckend statt halbtransparent. Ein durchscheinender Vorhang ueber den ganzen
// Bildschirm zwingt LVGL, einen Vollbild-Zwischenpuffer anzulegen und die gesamte
// Flaeche zu mischen - bei 1280x720 in 24 Bit dauerte das rund fuenf Sekunden, in
// denen nach dem Fingertipp nur der schwarze Standby-Bildschirm zu sehen war.
// Verloren geht dabei nichts: Dahinter liegt im Standby ohnehin nur Schwarz.
lv_obj_set_style_bg_opa ( g_wakeConfirm , LV_OPA_COVER , 0 ) ;
# else
lv_obj_set_style_bg_opa ( g_wakeConfirm , LV_OPA_70 , 0 ) ;
# endif
lv_obj_set_style_border_width ( g_wakeConfirm , 0 , 0 ) ;
lv_obj_set_style_radius ( g_wakeConfirm , 0 , 0 ) ;
lv_obj_clear_flag ( g_wakeConfirm , LV_OBJ_FLAG_SCROLLABLE ) ;
lv_obj_add_flag ( g_wakeConfirm , LV_OBJ_FLAG_CLICKABLE ) ; // Scrim faengt Tipps ab
lv_obj_t * wakeCard = th_card ( g_wakeConfirm ) ;
lv_obj_set_width ( wakeCard , 420 ) ;
// Breite waechst mit der gewaehlten Schriftgroesse mit. Bei fester Breite brach der
// Text in der Stufe Gross an unguenstigen Stellen um.
lv_obj_set_width ( wakeCard , ( g_fontStep = = FONT_GROSS ) ? 900
: ( g_fontStep = = FONT_MITTEL ) ? 640 : 420 ) ;
lv_obj_set_height ( wakeCard , LV_SIZE_CONTENT ) ;
lv_obj_center ( wakeCard ) ;
lv_obj_set_flex_flow ( wakeCard , LV_FLEX_FLOW_COLUMN ) ;
@@ -3587,12 +3800,17 @@ void ui_init(ProtocolClient* client) {
lv_obj_set_flex_flow ( wakeBtns , LV_FLEX_FLOW_ROW ) ;
lv_obj_set_flex_align ( wakeBtns , LV_FLEX_ALIGN_CENTER , LV_FLEX_ALIGN_CENTER , LV_FLEX_ALIGN_CENTER ) ;
lv_obj_clear_flag ( wakeBtns , LV_OBJ_FLAG_SCROLLABLE ) ;
// Knopfbreite und -schrift wachsen mit der Schriftgroesse mit; mit fester Breite und
// geerbter Grundschrift wirkten die Knoepfe neben dem groesseren Text verloren.
const int32_t wakeBtnW = ( g_fontStep = = FONT_GROSS ) ? 280 : ( g_fontStep = = FONT_MITTEL ) ? 210 : 150 ;
lv_obj_t * wakeNo = th_button ( wakeBtns , " Abbrechen " , COL_CARD2 , COL_TEXT ) ;
lv_obj_set_width ( wakeNo , 150 ) ;
lv_obj_set_width ( wakeNo , wakeBtnW ) ;
lv_obj_set_style_text_font ( wakeNo , font_text ( ) , 0 ) ;
lv_obj_add_event_cb ( wakeNo , wake_no_cb , LV_EVENT_CLICKED , nullptr ) ;
g_wakeNoBtn = wakeNo ;
lv_obj_t * wakeYes = th_accent_button ( wakeBtns , " Aufwecken " ) ;
lv_obj_set_width ( wakeYes , 150 ) ;
lv_obj_set_width ( wakeYes , wakeBtnW ) ;
lv_obj_set_style_text_font ( wakeYes , font_text ( ) , 0 ) ;
lv_obj_add_event_cb ( wakeYes , wake_yes_cb , LV_EVENT_CLICKED , nullptr ) ;
g_wakeYesLbl = lv_obj_get_child ( wakeYes , 0 ) ; // Label des Knopfs (fuer "Espresso" im Abfrage-Modus)
// Zweite Zeile: direkt in die kalte Extraktion aufwecken. Ohne diesen Weg muesste man
@@ -3600,6 +3818,7 @@ void ui_init(ProtocolClient* client) {
// Nur sichtbar, wenn die S3 die Funktion freigeschaltet meldet (siehe ui_update).
g_wakeColdBtn = th_button ( wakeCard , " Cold Extraction (Wasser bleibt kalt) " , COL_CARD2 , COL_ACCENT ) ;
lv_obj_set_width ( g_wakeColdBtn , LV_PCT ( 100 ) ) ;
lv_obj_set_style_text_font ( g_wakeColdBtn , font_text ( ) , 0 ) ;
lv_obj_add_event_cb ( g_wakeColdBtn , wake_cold_cb , LV_EVENT_CLICKED , nullptr ) ;
lv_obj_add_flag ( g_wakeColdBtn , LV_OBJ_FLAG_HIDDEN ) ;
lv_obj_add_flag ( g_wakeConfirm , LV_OBJ_FLAG_HIDDEN ) ;
@@ -3710,6 +3929,7 @@ void ui_init(ProtocolClient* client) {
show_page ( PG_DASH ) ;
if ( g_client ) g_client - > sendListProfiles ( ) ;
refr_monitor_install ( ) ;
g_uiReady = true ;
}
@@ -3804,10 +4024,10 @@ static void dash_brew_mode(bool 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_bold_96 : g_shotFontNormal , 0 ) ;
if ( lblBezug & & g_bezugFontNormal )
lv_obj_set_style_text_font ( lblBezug , on ? & lv_font_maven_pro_bold_96 : g_bezugFontNormal , 0 ) ;
// Die Schrift von Shot-Timer und Waage bleibt unveraendert. Frueher wechselte sie
// waehrend des Bezugs auf den grossen fetten Schnitt - das wirkte gegenueber dem
// restlichen Dashboard zu laut. Der Bereich bekommt weiterhin mehr Platz, die Zahlen
// stehen dadurch freier, aber in derselben Schrift wie sonst auch.
}
static void dash_brew_mode_tick ( const MachineState & st ) {
@@ -3819,6 +4039,45 @@ static void dash_brew_mode_tick(const MachineState& st) {
dash_brew_mode ( aktiv | | ( war & & ( uint32_t ) ( jetzt - zuletzt ) < DASH_BREW_HOLD_MS ) ) ;
}
// ------------------------------------------------------------------------------------
// Uhr läuft lokal weiter
//
// Die Uhrzeit kommt von der Hauptplatine und ist leer, solange dort keine Zeitquelle
// synchronisiert ist. Bei einem erneuten Abgleich ist sie kurz weg - auf dem Display
// verschwand die Uhr dann fuer ein paar Sekunden, was gerade im Standby unruhig wirkt.
//
// Deshalb merkt sich der P4 die zuletzt empfangene Zeit und zaehlt zwischen den Meldungen
// selbst weiter. Er stellt nichts richtig: Sobald die Hauptplatine wieder eine Zeit
// schickt, gilt deren Wert. Die lokale Uhr ueberbrueckt nur die Luecke.
// ------------------------------------------------------------------------------------
static bool g_clockKnown = false ; // schon einmal eine Zeit empfangen?
static uint32_t g_clockMinute = 0 ; // zuletzt bekannte Uhrzeit in Minuten seit Mitternacht
static uint32_t g_clockTick = 0 ; // Zeitpunkt (lv_tick), zu dem sie galt
// "HH:MM" der Hauptplatine uebernehmen. Andere Formate werden nicht angetastet.
static void clock_note ( const String & hhmm ) {
if ( hhmm . length ( ) < 5 | | hhmm . charAt ( 2 ) ! = ' : ' ) return ;
int h = hhmm . substring ( 0 , 2 ) . toInt ( ) ;
int m = hhmm . substring ( 3 , 5 ) . toInt ( ) ;
if ( h < 0 | | h > 23 | | m < 0 | | m > 59 ) return ;
g_clockMinute = ( uint32_t ) ( h * 60 + m ) ;
g_clockTick = lv_tick_get ( ) ;
g_clockKnown = true ;
}
// Anzuzeigende Uhrzeit: die der Hauptplatine, solange sie eine schickt - sonst die lokal
// weitergezaehlte. Leerer Rueckgabewert heisst: noch nie eine Zeit gehabt, nichts anzeigen.
static String clock_text ( const String & vonS3 ) {
if ( vonS3 . length ( ) ) return vonS3 ;
if ( ! g_clockKnown ) return String ( ) ;
uint32_t min = g_clockMinute + ( uint32_t ) ( ( lv_tick_get ( ) - g_clockTick ) / 60000UL ) ;
char b [ 6 ] ;
snprintf ( b , sizeof ( b ) , " %02u:%02u " , ( unsigned ) ( ( min / 60 ) % 24 ) , ( unsigned ) ( min % 60 ) ) ;
return String ( b ) ;
}
void ui_update ( const MachineState & st ) {
if ( ! g_uiReady ) return ;
@@ -3835,10 +4094,13 @@ void ui_update(const MachineState& st) {
bool wifiOk = st . wifiConnected & & ! st . apMode ;
if ( g_wifiIcon ) lv_obj_set_style_text_color ( g_wifiIcon , wifiOk ? COL_OK : ( st . apMode ? COL_WARN : COL_TEXT_MUT ) , 0 ) ;
// Uhrzeit (nur wenn NTP synchronisiert)
// Uhrzeit. Angezeigt wird die der Hauptplatine; faellt sie voruebergehend aus, zaehlt
// der P4 die zuletzt bekannte Zeit selbst weiter (siehe clock_text).
clock_note ( st . timeStr ) ;
String zeit = clock_text ( st . timeStr ) ;
if ( g_clock ) {
if ( st . timeStr . length ( ) ) {
lv_label_set_text ( g_clock , st . timeStr . c_str ( ) ) ;
if ( zeit . length ( ) ) {
label_set_if_changed ( g_clock , zeit . c_str ( ) ) ;
lv_obj_remove_flag ( g_clock , LV_OBJ_FLAG_HIDDEN ) ;
} else {
lv_obj_add_flag ( g_clock , LV_OBJ_FLAG_HIDDEN ) ;
@@ -3851,13 +4113,16 @@ void ui_update(const MachineState& st) {
if ( g_standbyScreen ) {
bool shouldShow = st . standbyActive & & st . standbyShowClock ;
if ( shouldShow ) {
lv_label_set_text ( g_standbyClock , st . timeStr . length ( ) ? st . timeStr . c_str ( ) : " --:-- " ) ;
label_set_if_changed ( g_standbyClock , zeit . length ( ) ? zeit . c_str ( ) : " --:-- " ) ;
if ( ! g_standbyShown ) {
ui_fade_in ( g_standbyScreen , UI_FADE_MS ) ; // Einschlaf-Animation
g_standbyShown = true ;
} else {
lv_obj_move_foreground ( g_standbyScreen ) ;
}
// Frueher wurde die Uhr hier bei JEDER Zustandsmeldung nach vorn geholt. Das
// ordnet die Ebenen neu und laesst LVGL den gesamten Bildschirm neu zeichnen -
// einmal pro Sekunde, jedesmal 1280x720 in 24 Bit. Genau das war der Grund,
// warum sich rund um den Standby alles um ein bis zwei Sekunden verzoegerte.
// Nach vorn geholt wird die Uhr beim Einblenden, danach bleibt sie dort.
} else if ( g_standbyShown ) {
ui_fade_out_hide ( g_standbyScreen , UI_FADE_MS ) ; // Aufweck-Animation
g_standbyShown = false ;
@@ -4118,43 +4383,43 @@ void ui_update(const MachineState& st) {
// Dashboard - Temperaturkarten (im Fehlerfall Warnsymbol + Klartext statt Zahl, wie OLED)
if ( lblTempW ) {
if ( st . wasserSafetyShutdown | | st . wasserSensorError ) {
lv_label_set_text ( lblTempW , LV_SYMBOL_WARNING ) ;
lv_obj_set_style_text_color ( lblTempW , COL_DANGER , 0 ) ;
lv_label_set_text ( lblSetW , st . wasserSafetyShutdown ? " Sicherheitsabsch. " : " Sensorfehler! " ) ;
lv_obj_set_style_text_color ( lblSetW , COL_DANGER , 0 ) ;
label_set_if_changed ( lblTempW , LV_SYMBOL_WARNING ) ;
label_color_if_changed ( lblTempW , COL_DANGER ) ;
label_set_if_changed ( lblSetW , st . wasserSafetyShutdown ? " Sicherheitsabsch. " : " Sensorfehler! " ) ;
label_color_if_changed ( lblSetW , COL_DANGER ) ;
temp_progress_set ( barDutyW , arcTempW , bulbTempW , 0 , COL_DANGER ) ;
} else {
lv_label_set_text ( lblTempW , tempStr ( st . tempW , 1 ) . c_str ( ) ) ;
label_set_if_changed ( lblTempW , tempStr ( st . tempW , 1 ) . c_str ( ) ) ;
if ( st . cxActive ) {
// Cold Extraction: der Sollwert ist bedeutungslos, es wird nicht geheizt.
// Statt eines Zielbands, das nie erreicht wird, den Grund anzeigen.
lv_obj_set_style_text_color ( lblTempW , lv_color_hex ( 0x1976d2 ) , 0 ) ;
label_color_if_changed ( lblTempW , temp_value_color ( lv_color_hex ( 0x1976d2 ) ) ) ;
label_set_if_changed ( lblSetW , " Cold Extraction - Heizen aus " ) ;
lv_obj_set_style_text_color ( lblSetW , lv_color_hex ( 0x1976d2 ) , 0 ) ;
label_color_if_changed ( lblSetW , lv_color_hex ( 0x1976d2 ) ) ;
temp_progress_set ( barDutyW , arcTempW , bulbTempW , 0 , lv_color_hex ( 0x1976d2 ) ) ;
} else {
// Ist-Wert faerbt sich weich Richtung Soll (Blau -> Gold -> Gruen im Zielband)
lv_color_t colW = temp_state_color ( st . tempW , st . setW ) ;
lv_obj_set_style_text_color ( lblTempW , colW , 0 ) ;
lv_label_set_text ( lblSetW , ( " Soll " + tempStr ( st . setW , 1 ) ) . c_str ( ) ) ;
lv_obj_set_style_text_color ( lblSetW , COL_TEXT_DIM , 0 ) ;
label_color_if_changed ( lblTempW , temp_value_color ( colW ) ) ;
label_set_if_changed ( lblSetW , ( " Soll " + tempStr ( st . setW , 1 ) ) . c_str ( ) ) ;
label_color_if_changed ( lblSetW , COL_TEXT_DIM ) ;
// Fortschritt = Ist/Soll in %, gedeckelt auf 100 (Ist >= Soll -> voll)
int pctW = ( st . setW > 0.0f ) ? ( int ) ( st . tempW / st . setW * 100.0f + 0.5f ) : 0 ;
temp_progress_set ( barDutyW , arcTempW , bulbTempW , pctW , colW ) ;
}
}
if ( st . dampfSafetyShutdown | | st . dampfSensorError ) {
lv_label_set_text ( lblTempD , LV_SYMBOL_WARNING ) ;
lv_obj_set_style_text_color ( lblTempD , COL_DANGER , 0 ) ;
lv_label_set_text ( lblSetD , st . dampfSafetyShutdown ? " Sicherheitsabsch. " : " Sensorfehler! " ) ;
lv_obj_set_style_text_color ( lblSetD , COL_DANGER , 0 ) ;
label_set_if_changed ( lblTempD , LV_SYMBOL_WARNING ) ;
label_color_if_changed ( lblTempD , COL_DANGER ) ;
label_set_if_changed ( lblSetD , st . dampfSafetyShutdown ? " Sicherheitsabsch. " : " Sensorfehler! " ) ;
label_color_if_changed ( lblSetD , COL_DANGER ) ;
temp_progress_set ( barDutyD , arcTempD , bulbTempD , 0 , COL_DANGER ) ;
} else {
lv_label_set_text ( lblTempD , tempStr ( st . tempD , 0 ) . c_str ( ) ) ;
label_set_if_changed ( lblTempD , tempStr ( st . tempD , 0 ) . c_str ( ) ) ;
lv_color_t colD = temp_state_color ( st . tempD , st . setD ) ;
lv_obj_set_style_text_color ( lblTempD , colD , 0 ) ;
lv_label_set_text ( lblSetD , ( " Soll " + tempStr ( st . setD , 0 ) ) . c_str ( ) ) ;
lv_obj_set_style_text_color ( lblSetD , COL_TEXT_DIM , 0 ) ;
label_color_if_changed ( lblTempD , temp_value_color ( colD ) ) ;
label_set_if_changed ( lblSetD , ( " Soll " + tempStr ( st . setD , 0 ) ) . c_str ( ) ) ;
label_color_if_changed ( lblSetD , COL_TEXT_DIM ) ;
int pctD = ( st . setD > 0.0f ) ? ( int ) ( st . tempD / st . setD * 100.0f + 0.5f ) : 0 ;
temp_progress_set ( barDutyD , arcTempD , bulbTempD , pctD , colD ) ;
}
@@ -4303,12 +4568,12 @@ void ui_update(const MachineState& st) {
float wDisp = st . weight ;
if ( wDisp > - 0.05f & & wDisp < = 0.0f ) wDisp = 0.0f ;
if ( ! st . scaleEnabled ) {
lv_label_set_text ( lblBezug , " -- " ) ;
label_set_if_changed ( lblBezug , " -- " ) ;
} else if ( bbwShot ) {
// Waehrend des Bezugs mit Brew-by-Weight: Ist oben, Ziel darunter (zweizeilig)
lv_label_set_text_fmt ( lblBezug , " %.1f g \n / %.1f g " , wDisp , shotTarget ) ;
label_fmt_if_changed ( lblBezug , " %.1f g \n / %.1f g " , wDisp , shotTarget ) ;
} else {
lv_label_set_text_fmt ( lblBezug , " %.1f g " , wDisp ) ;
label_fmt_if_changed ( lblBezug , " %.1f g " , wDisp ) ;
}
// Fortschrittsbalken Gewicht/Ziel (nur waehrend BBW-Bezug)
if ( g_bbwBar ) {