ADR-0061: The board and the screen size are a compile-time choice (tab5_carte), one package per board, the size written once¶
Status: Accepted (2026-10-12, lot 1 of the Waveshare 1024×600 port; the Waveshare board compiles but has never run on a board when written). Date: 2026-10-12
Context¶
Axel ordered a Waveshare ESP32-P4-WIFI6-Touch-LCD-7B (1024×600, landscape panel, ESP32-P4 revision 3, 32 MB flash). The firmware assumed one board everywhere: the Tab5's hardware in tab5-hardware.yaml and its screen revision in ecran-*.yaml, 1280/720 written in the tokens, in tab5_geometrie.h and in the C++ that derives from it, rotation: 270 in LVGL and in the pinball's return to landscape, the font sizes in pixels.
The goal of this first lot: make the board and its screen size a parameter without changing a pixel or a behaviour on the Tab5, and have the Waveshare firmware compile completely. Adapting the interface to 1024×600 is a later lot.
Decision¶
- One substitution,
tab5_carte(Tab5/user_entities.yaml;tab5by default through Jinja, liketab5_ecran), picksTab5/paquets/carte-<carte>.yamlat the placetab5_hardwareandtab5_ecranhad in thepackages:block. A board package carries the hardware and the geometry:carte-tab5.yamlloadsgeometrie-tab5.yaml,tab5-hardware.yamland the screen revisionecran-*.yaml, unchanged;carte-waveshare-7b.yamlholds the whole hardware of the Waveshare (pins from the compiled trialessais/waveshare-p4-7b.yamlof thefeat/essais-ecrans-wavesharebranch) and loadsgeometrie-waveshare-7b.yaml.
- The geometry of a board is six substitutions in
geometrie-<carte>.yaml: screen size in landscape (carte_ecran_l,carte_ecran_h), panel size as the display driver sees it (carte_dalle_l,carte_dalle_h), LVGL rotation (carte_rotation: 270 on the Tab5, whose panel is portrait; 0 on the Waveshare when written, 180 since the board arrived, see On the board) and the scale of the large fonts (carte_police_echelle: 1, and 0.82 on the Waveshare, a little under 600 / 720 ≈ 0.83). - The size is written once. The tokens derive from it (
modal_card_w: ${ carte_ecran_l | int - 30 },cal_grid_hfrommodal_card_h); the C++ gets it through-DTAB5_ECRAN_L,-DTAB5_ECRAN_H,-DTAB5_ROTATIONbuild flags in both configurations (tab5-ha-hmi.yaml,tab5-rendu-host.yaml), with the Tab5's values as defaults intab5_geometrie.hfor the PC tests;kCarteL/kCarteHderive fromkEcranL/kEcranH, and the pinball returns tokRotationRepos.tests/test_geometrie_partagee.pychecks the C++ against the tokens for every board. The layout literals (positions and sizes of the widgets) are deliberately left alone. - Large fonts follow the board (
roboto_32_b,roboto_45_b,roboto_55_b,roboto_130_bintab5-styles.yaml, and the theme fonts generated bytools/gen_themes.py):size: ${ (N * (carte_police_echelle | float)) | round | int }, which givesNon the Tab5. - The hardware the Waveshare lacks keeps its ids, as stubs. The other packages (diagnostics, battery, energy saving, settings) reference the Tab5's power switches, battery gauge, headphone detect and RTC. Rather than make each of them conditional,
carte-waveshare-7b.yamlturns them into template entities with!extend/!remove(no pin, optimistic switches,update_interval: neversensors), removes the RTC and its 24 h interval (which gets anid,tab5_rtc_demarrage, for that) and keeps the time-zone restore. The IMU is chosen in the root file:tab5-imu.yamlon the Tab5,tab5-imu-absente.yaml(the « Tap-to-Wake » switch only, without a name) elsewhere. The least intrusive option: the Tab5's packages do not change. The price: the blocks copied fromtab5-hardware.yamlinto the Waveshare package (logger, OTA, backlight output, media player…) can drift;tests/test_carte_7b.pykeeps them equal. - The off-device render follows
tab5_carte(geometrie-<carte>.yamlintab5-rendu-host.yaml, snapshot panel at the board's size).rendu-host.ymlgets a second job,rendu-7b, which renders every screen at 1024×600 into a separate artifact (rendu-1024x600), informative only: the « identical » comparison stays the Tab5's, and the scripts keep the Tab5's coordinates. - CI compiles the Waveshare (
build-carte-7binesphome-tab5.yml, same triggers asbuild, not a required check).
Consequences¶
- On the Tab5 the resolved configuration is the same as before, except for the three build flags (equal to the header's defaults) and the
idof the RTC interval — checked by comparing the configurations resolved byesphome configbefore and after, substitutions aside. Theconfig_hashchanges anyway: ESPHome hashes the substitutions block, which gains thecarte_*keys. - The project name (
axellum.tab5-ha-hmi),esphome: nameandfriendly_namedo not change with the board: a Waveshare compiled with the defaults would announce itself as a Tab5. - The Waveshare's interface is not adapted: popups and the home screen overflow at 1024×600 until a later lot replaces the layout literals. A compile-time layout check that only holds at the Tab5's size (the five rows of the House popup fit in the card,
tab5_maison.cpp) is guarded bykMiseEnPageTab5(tab5_geometrie.h, true on the Tab5): it stays strict there and lets the Waveshare compile. - Not verified when written: nothing ran on a Waveshare (display, touch, audio, Wi-Fi co-processor); the pins come from a trial configuration that was compiled, never run.
- The Waveshare needs ESPHome 2026.9.1 or later (2026.9.0 aborts while starting the display of a P4 v3, esphome#18984; found on the board on 2026-10-14). A
min_version:in the board package would be overridden by the root file's (2026.9.0, the Tab5's floor, read as is by CI) when packages are merged, so the board package listsTab5/ecran/carte_7b_version.h, whosestatic_assertonESPHOME_VERSION_CODEstops the build of the Waveshare under 2026.9.1.
Second lot: what the popups share follows the card (2026-10-12)¶
- What every popup shares derives from the modal card, in
tab5_geometrie.hand as tokens oftab5-ui-tokens.yaml, kept equal per board bytests/test_geometrie_partagee.py: the body (kCorpsW/kCorpsH,corps_w/corps_h), the graph area of a full-width card (kGraphiqueL), two half cards side by side (kDemiL/kDemiX), the end of the header tabs (kOngletsFin), the calendar columns (kCalCol*), the rows of a room in the Lights and Shutters popups (kPieceLigne*, at most the Tab5's 92-pixel pitch), the centre card's panels (kPanneauCentralL). The House popup's rows share the height left under their headers (104 pixels at most, set bydisposer()), and its compile-time check now holds on every board. - Header tabs keep the Tab5's layout and are scaled from the ×: places and widths written for the Tab5 (x 318…1148, 200 pixels; the Alarm clock from 196) are brought to a narrower card by the ratio of the two zones, measured from the × (
onglet_x(),onglet_echelle(); YAMLonglets_fin,onglets_zone). The ratio is 1 on the Tab5, so nothing moves there. - Each popup's own layout is not touched (positions inside its cards, heights of its rows, the Music card): later lots. Neither are the home screen and the games. Two exceptions, where the smaller card made two elements overlap on the 7B: the alarm clock's two rollers are placed from the middle of their card (the « : » is centred), and the climate's room temperature keeps its Tab5 height (below its card on the 7B, as in the first lot, instead of over the − / + buttons).
- The off-device render measures instead of guessing: every capture publishes the screen shown (the « Écran courant » name) and the number of drawn objects that LVGL clips (
Tab5/rendu/rendu_mesure.h, render only);bilan-7bcompares the 7B to the Tab5 (screens opened / expected, clipped objects per popup) in its job summary. The render robot replays the Tab5's gestures brought to the board's screen and opens through navigation what the 7B's home screen hides (tools/rendu/vers_carte.py, render actionrendu_ecran); on the Tab5 the steps are unchanged. - On the Tab5 the resolved configuration is identical to the first lot's (
esphome config, substitutions aside).
Third lot: the home screen is the Tab5's, brought to the card's screen (2026-10-13)¶
- One rule for the whole home screen: every position and size the Tab5 draws at 1280×720 is brought to the card's screen,
x × L / 1280andy × H / 720, rounded to the nearest pixel in integers ((v × L + 640) // 1280). Same proportions, same grid, smaller. Each value is a token oftab5-ui-tokens.yaml(acc_*for the grid, the top buttons, the left zone, the « Ok Nabu » frame, the row under the clock, the − / + tile, the centre card, the bottom tiles and the HA-mode cards;hor_*for the clock), never a literal in the widget. The C++ that places home-screen objects uses the same formula (acc_x(),acc_y()and thekAcc…constants oftab5_geometrie.h: the centre card, the row's width, the HA-mode cards, the low anchor of a climate's wheel, the compact player).tests/test_geometrie_partagee.pychecks every token against the C++ for every board, checks that each token gives back the Tab5's old value (test_accueil_du_tab5_au_pixel), and checks the grid's invariants on every board (columns and clock side by side within the screen, five tiles centred, the centre card between the frame and the tiles). - The clock follows its font, not the screen: its digits use
roboto_130_b, already scaled bycarte_police_echellein the first lot. Its inner layout (digit frames, « : », date, the cut between the three touch zones) is the Tab5's scaled by the same factor (police_pct,-DTAB5_POLICE_PCT,police_px()), then centred in the clock tile (hor_dx,hor_dy).theme_polices()places every theme's rollers with the same rule; on the Tab5 the factor is 100 % and the offsets 0. Values are scaled from the Tab5's rounded pixels, not recomputed from the font's units: on another board a frame can be 1 pixel wider than a digit's advance (tests/test_horloge.pyallows 1 pixel there, and still requires the ink inside its frame). - Fonts:
mdi_font_70(top buttons, HA-mode cards, plant row) andmdi_font_alert(weather warnings in the tiles) followcarte_police_echellelike the text fonts.mdi_font_45,mdi_font_56(shared with the games' selector) and the weather fonts keep their size. Amended on 2026-10-18: the icons of the two temperatures above the − / + tile and the − / + glyphs move to two small fonts of their own that followcarte_police_echelletoo (mdi_font_temp, 45 px on the Tab5;mdi_font_pas, 56 px): at 45 px on the 7B, the living-room value's « ° » ended 4 px before the greenhouse icon, and the − nearly filled its 40 px button. - The render robot touches the 7B's home screen at its real places (
tools/rendu/vers_carte.py): a home-screen point of the Tab5 is brought to the card's screen with the same formula, so the top buttons, the − / + tile and the bottom tiles are really touched; the navigation replacement of the second lot no longer fires on the home screen (kept as a safety net). Tab5/user_entities.yaml'smeteo_cell_width,meteo_cell_heightandmeteo_cell_yare no longer read (the tiles follow the board) and leave the template, which now holds only comments (esphome configaccepts auser_entities.yamlwithout any key, checked on both configurations).- On the Tab5 the resolved configuration is identical to the second lot's except for one build flag,
-DTAB5_POLICE_PCT=100(the header's default). - Not done here: each popup's own layout and the games (later lots).
Fourth lot: each popup's own layout follows the card (2026-10-13)¶
Developed in parallel with the third lot (the home screen); games stay out of scope.
- Same layout, smaller: every literal of a popup's own layout is the Tab5's value brought to the body of the card in the expression itself, rounded down — a width or an x
${ 386 * (corps_w | int) // 1202 }, a height or a y${ 456 * (corps_h | int) // 598 }, a square the smaller of the two, a negative${ 0 - … }. The C++ does the same withlargeur_carte(),hauteur_carte()andtaille_carte()(tab5_geometrie.h). On the Tab5 every ratio is 1: the resolved configuration is identical (esphome config, substitutions aside, checked after each family) and the C++ constants keep their values (asserted by the tests). - Families, one commit each: Shutters and Lights, Climate, Settings and the system console, Temperature and Weather, Alarm clock, Energy, TV remote, Cameras, Plants, Voice assistant, Device. Each has a test that resolves its YAML and its C++ for every board, checks that both give the same numbers and that everything fits in the card, and pins the Tab5's values (
tests/test_*_popup.py,test_energie.py,test_historique.py,test_telecommandes.py);constantes_cpp()(tests/commun.py) evaluates the C++constexprper board. - What does not shrink is placed from what does:
roboto_22, the MDI fonts and the weather fonts keep their size on every board; the bold Roboto sizes are scaled bycarte_police_echelle. Line heights come from the font's metrics (ligne_roboto_130_b,ligne_roboto_45_b,ligne_roboto_32_b, tokens): the Alarm clock's rollers are as tall as their rows, and what sits under a roller is placed from its bottom. - Departures from "same layout, smaller", where a fixed-size text no longer fitted:
- Plants: five cards of 178 pixels on the 7B; the metric rows' margin gives way (14 → 5 pixels) and their text moves from x 50 to 38 so that « Température » (128 pixels in
roboto_22) fits beside its icon — only when the card is too narrow (pot_ligne_marge,pot_texte_x). - Cameras: the picture is asked from Home Assistant in eighths of 1920 × 1080 (HA's scaling steps), the largest that fits the body with the name, time and dots under it: 4/8 (960 × 540) on the Tab5, 3/8 (720 × 405) on the 7B (
cameras_huitiemes,kImageHuitiemes); the mosaic's cells follow (356 × 198 on the 7B, thumbnails asked at 360 × 202 and scaled byechelle_couvrir()). - Voice assistant: the settings panel and the answer card end 8 pixels above the action bar (
corps_h - 8 -its height); the « Your request » card keeps its 104 pixels (two lines ofroboto_22). - TV remote: the bottom row keeps its 102 pixels; the pad is a square of the smaller ratio and the commands column takes what is left.
- After a look at the 7B renders, a fixed-size word is placed from its fixed-size icon rather than scaled with the card (TV remote keys and On / Off, « Settings » of the voice assistant, the console's Flash value; « Marche / Arrêt », 142 pixels, then wraps onto two lines inside its key on the 7B, its width
SIZE_CONTENTon the Tab5 as before), or shifted / dropped only when it would not fit: Boost / Eco icon and word move left together (0 on the Tab5, 17 pixels on the 7B), the Alarm clock's hint under the rollers loses its 22-pixel margin (one line on the 7B), the Cameras time keeps its 360 pixels for « Touch a picture to enlarge it », and a weather column of 61 pixels shows a rain amount without « mm » and the hour instead of « Tomorrow » when they do not fit (measured withlv_text_get_size()). - Not verified when written: nothing ran on a Waveshare. The hardware JPEG decoder with 405-pixel-high pictures and the speed of the scaled thumbnails are unknown; the 120-pixel weather icon sits in a 119-pixel box on the 7B.
On the board (2026-10-14)¶
The board arrived on 2026-10-14 and has run the firmware since (the author's daily screen). Two settings written before it came were wrong and are fixed in carte-waveshare-7b.yaml and geometrie-waveshare-7b.yaml: the panel is mounted upside down (carte_rotation: "180", the GT911 mirrored in X and Y, its reset on GPIO 23, as in the public configurations that run on this board), and the ESP32-C6's reset is active high like the Tab5's. The PSRAM at speed: 200MHz, already in the board package, proved necessary: the first trial firmware, without it, had about 30 display underruns a second. The ESPHome floor of this board, 2026.9.1, is in the Consequences above. What happened on the board, in order, and what is not verified yet: Waveshare ESP32-P4 7B.
Since 2026-10-19 (ADR-0069), a board mounted the other way round no longer needs a build with carte_rotation: "0": its « Sens de l'écran » setting (« Retourné ») adds the 180° while running, and the published firmware fits both mountings.