Fix what a full test round of today's releases found: a ticking single clock, a switch that works in standby, no taps from a swipe (0.2.78)

A test and code-analysis round of 0.2.72 to 0.2.76 (suites at every tag, sanitizers, ESP32 and GitHub builds, host
harnesses on both boards, the editor in a browser, best practice checked against ESPHome, LVGL, Home Assistant and
Vue) found these, fixed here with Max's choices for the product questions:

- Firmware 0.2.65: the second hand of a single analog clock moves again; on a page that is one full on/off tile the
  waking push also switches it; on the Guition a flick is never a tap and a finger that slides off a tile doesn't tap
  it; Previous/Next react to a firm press; the same Go to page tile may sit on several pages; a layout the screen has
  no memory for is refused instead of crashing; 64-bit redraw mask for 48 tiles; Montserrat 14 no longer built.
- App: Save refuses a layout too long for one message or too big for eight pages; an offline screen keeps its
  firmware version from the device registry; HA refusals reach the editor; 128 KB requests; changelog out of the
  live stream; cached hashed assets; tini as init and a clean stop; changelog parsing without asterisks or crashes.
- Editor: clicking the open screen keeps unsaved edits, edits during a save stay unsaved, a Page row and phone
  dragging to other pages, repeated page tiles, limits from the server, HA's error messages, a type-checked build.
- tools/check.sh + GitHub Actions + a CYD flash budget; docs match 48 tiles and the new editor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Max Gramser
2026-09-18 17:25:23 +02:00
parent c09e4e333f
commit 1a6308aba7
80 changed files with 2623 additions and 320 deletions
+78 -9
View File
@@ -23,16 +23,42 @@
The editor is the Vue app in `web/`: after a change under `web/src`, run
`cd web && npm ci && npm test && npm run check && npm run build` and commit
`screen_manager/app/static` with it (that folder is the build output; never edit it by hand).
2. Run all Python tests with aiohttp installed, all `tests/*.cpp`, the
generator with `--check`, and compile both Easy Setup profiles. Do that
sequentially: profiles with the same `DEVICE_NAME` share one build folder, and a
parallel build can make an upload pick the wrong `firmware.bin`. Check that no
secrets are in Git.
2. Run `tools/check.sh` (with `PYTHON=.venv-portal/bin/python` on a development machine). It runs all
Python tests with aiohttp, PyYAML, Pillow and fontTools installed, every `tests/*.cpp` with
`clang++ -std=c++17 -Wall -Wextra -Werror -I.`, both generators with `--check`, and the editor's
`npm ci`, `npm test`, `npm run check` and `npm run build`, and fails when that fresh build differs from the
`screen_manager/app/static` in Git (committed or staged). For a firmware change, `tools/check.sh --firmware`
compiles both board profiles with placeholder secrets from a temporary folder (never the real `secrets.yaml`) and
applies the flash budget below; `--all` does both. CI (`.github/workflows/ci.yml`) runs the same script on every
push and pull request to main. Compile sequentially: profiles with the same `DEVICE_NAME` share one build folder,
and a parallel build can make an upload pick the wrong `firmware.bin` (the check builds are called `check-cyd` and
`check-guition` and build under `.esphome/check`, apart from the bench profiles). Check that no secrets are in Git.
**Flash budget of the CYD** (Max's rule, app 0.2.78). The CYD has 4 MB of flash and two update slots of
1,835,008 bytes; the Guition's 16 MB leave it far from any limit. Measure the CYD on the build users get: the YAML
`core.installation_yaml()` writes has the Wi-Fi fallback access point (`wifi: ap:`) and `captive_portal:`, about
84 KB more than a profile without them. The board profiles carry both, so `tools/check.sh --firmware` measures
that shape; it reads the slot from the build's `partitions.csv` (`app0`, `ota_0`) and the image from
`firmware.ota.bin`, and `--baseline <bytes>` prints the delta against the last release. The check-profile tables
in earlier test results left both out and read about 84 KB low (0.2.72: 1,453,647 bytes, 79.2 %, where the
user-shaped build was 1,538,032 bytes, 83.8 %). Build with the add-on's pinned ESPHome (`screen_manager/Dockerfile`)
and, when the ESPHome Device Builder ships a newer ESPHome, with that one too (`ESPHOME=<its esphome command>`),
because users build their updates there.
| CYD image, share of 1,835,008 bytes | Rule |
|---|---|
| up to 90 % | normal |
| 90-93 % | tight: every release states its flash delta; a delta over 8 KB needs a matching saving or Max's OK |
| 93-95 % | only fixes ship |
| over 95 % | never: that keeps about 90 KB for ESPHome upgrades and users' own overrides |
3. Test app start, saving, restarting/updating with existing layouts,
reconnecting to HA, and an ESP restart. Test a new card on real
hardware. A good build doesn't replace physical touch acceptance.
4. Bump the app version and firmware project version; write the CHANGELOG and concrete
test results. Only publish compatible changes directly to main.
test results. Only publish compatible changes directly to main. `tests/test_release_lint.py`
(part of `tools/check.sh`) holds `config.yaml`'s version, the first CHANGELOG heading and its
firmware against `FIRMWARE_VERSION`, keeps the CHANGELOG headings unique and newest first, and checks
that every `fonts/...` file the packages fetch from GitHub is in the tree.
5. Commit and push main (the only release branch). Create an immutable tag
`screens-vX.Y.Z` from the same commit. Test the remote YAML in an empty folder:
all components/fonts must be fetchable via GitHub.
@@ -52,7 +78,7 @@ For backend tests on a development machine:
```sh
python3 -m venv .venv-portal
.venv-portal/bin/pip install aiohttp PyYAML
.venv-portal/bin/pip install aiohttp PyYAML Pillow fonttools
.venv-portal/bin/python -m unittest discover -s tests
```
@@ -189,6 +215,34 @@ icons sit off-center in the browser); run `generate_packages.py` afterward.
The `MDI_GLYPH_*` substitutions are retired: `TILEn_ICON` in manual
profiles must come from the set. No changed preferences or keys.
### Compatibility 0.2.78 / firmware 0.2.65
Repeated navigation tiles: the same `screen.page_<n>` may sit on several pages of one screen. Firmware 0.2.65 accepts
repeats of `screen.page_*` only (`Model::set_layout`, `page_entity()`); every Home Assistant entity still appears once.
The app sends a repeat only to 0.2.65+ (`PAGE_TILE_REPEAT_MIN_FIRMWARE` in `min_firmware`); an older screen gets
"Install screen firmware 0.2.65 or newer first". Tile events from Claude in HA pick a copy by `slot`/`page` (move:
`from_slot`/`from_page`); without one they act on the first copy in slot order, and the answer names its page and slot.
- Firmware: `dirty_tiles` is 64 bits (`tile_bit`, `mark_tile`, `static_assert(MAX_TILES <= 64)`). `set_layout` refuses a
layout whose tile list does not fit the largest free block (`tile_room`, ESP32 only) and reports it through
`Model::refusal`; `TileAllocator` logs and aborts on a null block instead of writing through it. The second hand
moves on `analog` and `calendar` dials while part 18 is an `lv_line` (`lv_obj_check_type`). Guition: an
`LV_EVENT_GESTURE` handler on `home_page` consumes the contact (no page flip; LVGL needs speed and 50 px), and
`runtime_tiles::event` drops a tap or hold released outside the tile (`lv_obj_hit_test`); LVGL's `PRESS_LOCK` stays so
a finger that slides on to a slider never drags it. `dim_wake_overlay` calls `runtime_tiles::wake_tap()` after
`wake_display`: on a page that is one full tile whose tap is `<domain>.toggle`, the waking push also taps it. Page
bars (tile pages and the settings page) react to `on_click`/`LV_EVENT_CLICKED`. `lvgl: default_font` is a Roboto font
of each board, so ESPHome no longer builds Montserrat 14 (CYD -13.9 KB).
- App: `Manager.save` encodes the real layout message and refuses one over 4096 bytes; `core.packed_slots` refuses tiles
that don't fit eight pages. `discover_screens` adds `firmware_known`: the "Screen firmware" sensor when it holds a
strict X.Y.Z, else the device registry's `sw_version` in the form `X.Y.Z (ESPHome …)`. Every screen in the inventory
carries `firmware_known`, `tile_limit`, `full_page` and `page_tiles_repeat`; the changelog moved from the update
summary (and the live stream) to the full inventory's top-level `changelog`. HA refusals answer 400 and an unreachable
HA 503, both JSON `{"error"}`. `client_max_size` 128 KB. Hashed `/assets/` files get `private, max-age=31536000,
immutable`; the page and `/api` keep `no-store`. `config.yaml` drops `init: false` (tini is PID 1 again) and the server
uses aiohttp's `handle_signals`.
- Editor: `npm run build` type-checks first (`vue-tsc --noEmit`), `devEngines` pins Node. Storage unchanged.
### Compatibility 0.2.74 / firmware 0.2.62
Forty-eight tiles per screen, a tile over the whole page (`size: full`) and navigation tiles (`screen.page_1` to
@@ -196,6 +250,11 @@ Forty-eight tiles per screen, a tile over the whole page (`size: full`) and navi
gets "Install screen firmware 0.2.62 or newer first". Older apps keep working with this firmware: they never send
`full` or a page tile, and the firmware still packs `wide` as before.
Downgrade: an app older than 0.2.74 stops at startup on a stored layout with more than 20 tiles or a page tile
(`validate_layout` raises "Choose at most 20 tiles." or "This entity isn't supported." even with `stored=True`);
`screens.json` stays untouched. Before going back, trim the layout to 20 normal tiles, or restore the app backup (the
Supervisor restores the version and its data together).
- Firmware: `MAX_TILES` is `MAX_SLOTS` (48). `Model::tiles` is a `std::vector` sized to the layout (a `TileAllocator`
over ESPHome's `RAMAllocator`, so a Guition keeps the list in PSRAM; the CYD's falls back to its heap); nothing may
index `tiles` beyond `count`. A `Tile` has `full` next to `wide` (a full tile is also wide) and `cells()`; `pack()`
@@ -227,6 +286,9 @@ storage or editor change, and an older app drives this firmware as before.
the Guition), a contact that moved, a contact that already counted, and a tap within 150 ms of the last accepted
tap on the same button (bounce). `show_page` already drops a fill still under way when the next page is asked for,
so nothing else changed. `tests/test_cyd_ui.cpp` covers the page buttons.
- On the CYD a lift-off bounce is stopped first by the XPT2046 filter (`components/xpt2046/touchscreen/touch_filter.h`:
three matching samples start a contact, two empty samples release it, one empty sample is ignored); the 40 ms
minimum and the 150 ms same-button gap of `accept_repeat` are backup nets behind it (noted 2026-09-18).
- The settings page's own pager (`settings_screen.h`) never used the guard and is unchanged. Tile taps, cards, keys,
chips and sliders keep `accept()` and its window.
@@ -1138,8 +1200,15 @@ rotation mapping, `update()`, `end()`); the indev callback is gone. Otherwise un
### Compatibility 0.2.33 / firmware 0.2.28
Firmware only. `TouchGuard::configure(0, ...)` disables the movement limit
(`moved_` is then never set; LVGL's press-lost decides whether a tap goes through). The
Guition profile sets `TOUCH_MOVE_LIMIT_PX` to 0, the CYD keeps 56. Otherwise unchanged.
(`moved_` is then never set). The Guition profile sets `TOUCH_MOVE_LIMIT_PX` to 0, the
CYD keeps 56. Otherwise unchanged.
Correction (2026-09-18): this note said LVGL's press-lost then decides whether a tap goes
through. It doesn't: a tile keeps LVGL's `LV_OBJ_FLAG_PRESS_LOCK` (on by default), so a
finger that slides off never sends `LV_EVENT_PRESS_LOST`, and on the Guition a release
outside the tile still counted as a tap. From firmware 0.2.65 a tile without a movement limit
checks it itself (`runtime_tiles::event`): a tap or hold whose finger lets go outside the tile
does nothing. PRESS_LOCK stays on.
### Compatibility 0.2.32 / firmware 0.2.27