Interface (LVGL)¶
Pourquoi LVGL natif¶
L'alternative à une interface LVGL native serait un dashboard web tournant dans un navigateur embarqué dans le firmware. Cette approche a des coûts réels sur un microcontrôleur :
- La pile WebSocket ou HTTP entre en compétition avec le flux audio pour les sockets
- Le moteur de layout du navigateur re-rend à chaque mise à jour, déclenchant des repeintures complètes
- Si le réseau est instable, l'écran affiche un spinner de chargement ou une page blanche
LVGL est compilé dans le firmware. La mise en page de l'écran est calculée une fois au boot. Les mises à jour sont chirurgicales : quand une valeur de température change, seul le label pour cette valeur est invalidé et redessiné. Le reste de l'écran est inchangé. Sur le cœur RISC-V à 360 MHz, avec le framebuffer dans 32 Mo de PSRAM, une valeur modifiée se redessine en moins de 10 ms ; l'écran entier prend 133 ms (mesuré sur la tablette, firmware 3.2.0, 28/09/2026).
Un effet de bord appréciable : si le Wi-Fi tombe ou que Home Assistant redémarre, le dernier état connu reste affiché exactement comme il était.
Icônes : polices vectorielles au lieu d'images¶
Les anciens panneaux tactiles (Nextion, setups TFT_eSPI) utilisent typiquement des fichiers d'images bitmap pour les icônes. Chaque icône est un .png ou .jpg embarqué en flash, décodé à l'exécution.
Ce projet utilise Material Design Icons rendu comme police TTF (materialdesignicons-webfont.ttf). Le moteur de polices de LVGL rend les glyphes depuis le fichier de police à la taille spécifiée dans le style.
La différence en pratique : - Une icône bitmap soleil à 45 px pèse ~8 KB en PNG, décodée en 8 KB en RAM à l'exécution - Le même icône comme glyphe MDI à 45 px coûte ~200 octets dans le fichier de police, rendu à la volée par le moteur de polices - Le scaling est gratuit : le même glyphe peut se rendre à 24 px, 45 px ou 72 px sans assets séparés
Une seconde police d'icônes (IconeMeteo.ttf) couvre les symboles météo spécifiques absents du set MDI.
Note anti-aliasing : les polices sont configurées à bpp: 1 (1 bit par pixel) plutôt que les 4 bpp par défaut. Ça divise par deux l'empreinte RAM de la police et réduit significativement le coût CPU du rendu à chaque frame. À 45 px sur un affichage 1280 × 720, la différence visuelle entre 1 bpp et 4 bpp est négligeable à la distance de visionnage normale.
Système de styles global¶
LVGL permet deux approches pour styliser les widgets :
- Styles inline — définir les propriétés directement sur chaque objet
- Objets style — créer un
lv_style_tnommé, y définir les propriétés une fois, et l'attacher à plusieurs objets
Ce projet utilise exclusivement l'approche 2, via tab5-styles.yaml. Tous les objets style sont déclarés globalement et attachés par nom dans tab5-lvgl.yaml.
Pourquoi ça compte sur un ESP32-P4 avec 768 KB de SRAM interne : - Chaque propriété de style inline crée un enregistrement de style local dans la structure de données interne du widget - Avec 80+ widgets, c'est 80+ allocations de style séparées, fragmentées en PSRAM - Un objet style partagé, attaché à 10 widgets similaires, utilise 1 allocation partagée par les 10
En pratique, le passage des styles inline aux styles globaux a libéré environ 40 KB de PSRAM dans ce projet.
Thèmes. Chaque style partagé lit la palette active (UIColor) par une lambda, et les vingt et un thèmes changent la palette, les formes (rayons, bordures, ombres) et trois polices d'affichage pendant que l'écran tourne (ADR-0029). C'est pourquoi un widget ne porte jamais de couleur à lui : ESPHome la figerait à la compilation et aucun thème ne pourrait la changer.
Couleur dynamique¶
LVGL supporte des tags de couleur inline dans les chaînes de texte des labels :
"#FF6B35 27°C# Salon"
LVGL parse le tag de couleur hex et rend "27°C" en orange, puis "Salon" dans la couleur par défaut du label. Aucun code supplémentaire requis.
C'est utilisé pour les horaires de la carte centrale, les postes du matin dans leur couleur. Les couleurs sont choisies sur l'appareil, dans la palette du thème actif, et le texte est reconstruit au changement de thème : une couleur écrite dans un texte n'est pas repeinte par un style (un blanc écrit là rendait les horaires invisibles dans les thèmes clairs, corrigé avant la 3.6.0).
Grisage des éléments inactifs¶
Quand un bouton ou contrôle est indisponible, l'approche commune est de le cacher avec LV_OBJ_FLAG_HIDDEN. Ça fonctionne mais laisse un trou dans la mise en page.
Ce projet utilise plutôt l'opacité de recolorisation :
lv_obj_set_style_img_recolor(obj, lv_color_hex(0x808080), LV_PART_MAIN);
lv_obj_set_style_img_recolor_opa(obj, 180, LV_PART_MAIN);
Ça superpose une teinte grise sur l'élément sans le cacher. La mise en page reste symétrique, et le retour visuel communique clairement "inactif" sans perturber la géométrie de l'écran.
Contraintes CPU à garder en tête¶
Quelques patterns LVGL qui semblent anodins mais sont coûteux sur un microcontrôleur :
Coins arrondis sur objets transparents. Si un objet a bg_opa: 0 et radius > 0, LVGL calcule quand même le masque de clipping pour les coins arrondis à chaque fois que le widget est redessiné, pour rien. Correction : mettre radius: 0 sur les conteneurs transparents.
Padding inutilisé. Toute valeur de padding non nulle fait inclure le widget dans les passes de détection de collision. Les conteneurs purement structurels devraient avoir pad_all: 0 et border_width: 0.
Invalidations fréquentes plein écran. Appeler lv_obj_invalidate(lv_scr_act()) force une repeinte complète au prochain tick. Le bon pattern est d'appeler lv_obj_invalidate(widget_spécifique) pour que seule la zone modifiée soit redessinée.