Aller au contenu

ADR-0061: The board and the screen size are a compile-time choice (tab5_carte), one package per board, the size written once

Page en anglais

Cette page n'existe qu'en anglais.

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; tab5 by default through Jinja, like tab5_ecran), picks Tab5/paquets/carte-<carte>.yaml at the place tab5_hardware and tab5_ecran had in the packages: block. A board package carries the hardware and the geometry:
    • carte-tab5.yaml loads geometrie-tab5.yaml, tab5-hardware.yaml and the screen revision ecran-*.yaml, unchanged;
    • carte-waveshare-7b.yaml holds the whole hardware of the Waveshare (pins from the compiled trial essais/waveshare-p4-7b.yaml of the feat/essais-ecrans-waveshare branch) and loads geometrie-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_h from modal_card_h); the C++ gets it through -DTAB5_ECRAN_L, -DTAB5_ECRAN_H, -DTAB5_ROTATION build flags in both configurations (tab5-ha-hmi.yaml, tab5-rendu-host.yaml), with the Tab5's values as defaults in tab5_geometrie.h for the PC tests; kCarteL/kCarteH derive from kEcranL/kEcranH, and the pinball returns to kRotationRepos. tests/test_geometrie_partagee.py checks 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_b in tab5-styles.yaml, and the theme fonts generated by tools/gen_themes.py): size: ${ (N * (carte_police_echelle | float)) | round | int }, which gives N on 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.yaml turns them into template entities with !extend/!remove (no pin, optimistic switches, update_interval: never sensors), removes the RTC and its 24 h interval (which gets an id, tab5_rtc_demarrage, for that) and keeps the time-zone restore. The IMU is chosen in the root file: tab5-imu.yaml on 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 from tab5-hardware.yaml into the Waveshare package (logger, OTA, backlight output, media player…) can drift; tests/test_carte_7b.py keeps them equal.
  • The off-device render follows tab5_carte (geometrie-<carte>.yaml in tab5-rendu-host.yaml, snapshot panel at the board's size). rendu-host.yml gets 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-7b in esphome-tab5.yml, same triggers as build, 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 id of the RTC interval — checked by comparing the configurations resolved by esphome config before and after, substitutions aside. The config_hash changes anyway: ESPHome hashes the substitutions block, which gains the carte_* keys.
  • The project name (axellum.tab5-ha-hmi), esphome: name and friendly_name do 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 by kMiseEnPageTab5 (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 lists Tab5/ecran/carte_7b_version.h, whose static_assert on ESPHOME_VERSION_CODE stops 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.h and as tokens of tab5-ui-tokens.yaml, kept equal per board by tests/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 by disposer()), 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(); YAML onglets_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-7b compares 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 action rendu_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 / 1280 and y × H / 720, rounded to the nearest pixel in integers ((v × L + 640) // 1280). Same proportions, same grid, smaller. Each value is a token of tab5-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 the kAcc… constants of tab5_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.py checks 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 by carte_police_echelle in 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.py allows 1 pixel there, and still requires the ink inside its frame).
  • Fonts: mdi_font_70 (top buttons, HA-mode cards, plant row) and mdi_font_alert (weather warnings in the tiles) follow carte_police_echelle like 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 follow carte_police_echelle too (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's meteo_cell_width, meteo_cell_height and meteo_cell_y are no longer read (the tiles follow the board) and leave the template, which now holds only comments (esphome config accepts a user_entities.yaml without 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 with largeur_carte(), hauteur_carte() and taille_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++ constexpr per 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 by carte_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 by echelle_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 of roboto_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_CONTENT on 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 with lv_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.