Make page swipes smooth and give lights a new colour card (0.2.42)
Page switches show the new page as empty cards at once and draw two cards per LVGL refresh, so touch is polled between the steps; on the Guition the longest loop gap during a swipe dropped from 276 to 74 ms (docs/SWIPE_PROFILE.md). Custom parts and control panels are reused instead of rebuilt, state updates and ticks redraw only their own card, clocks redraw once a minute, and a swipe past the last page draws nothing. Slider handles sit inside the fill (grey when the card is off), the busy sheet covers the whole card, and the light colour card is redesigned with rounded tracks and colour knobs. Firmware 0.2.36; SWIPE_PROFILE builds carry a per-swipe profiler for measurements. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -176,6 +176,36 @@ 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.42 / firmware 0.2.36
|
||||
|
||||
Firmware only: storage version, tile protocol, preferences and keys are unchanged. What moved
|
||||
inside `runtime_tiles.h` (measurements in docs/SWIPE_PROFILE.md):
|
||||
|
||||
- `render()` is split into `render_slot()`. A page switch (`show_page` to another page) places
|
||||
the page, shows every card as a skeleton (a sheet in the card colour over its content area,
|
||||
`Widgets::veil`) and draws two cards per LVGL refresh (`fill_cards`, `FILL_STEP_CARDS`,
|
||||
`LV_EVENT_REFR_READY`, 40 ms fallback); a new switch cancels the fill. `apply_page` (same page:
|
||||
keepalives, re-packing) still draws everything at once. `check_tile_geometry` finishes a fill
|
||||
still under way and logs `page fill still under way at the check`.
|
||||
- `refresh_tile(index)`, `refresh_header_only()` and `refresh_all()` mark what the next
|
||||
`render()` draws; a plain `refresh()` from the board YAML draws every card, as before.
|
||||
- A slot hides custom parts and control panels instead of deleting them and reuses them for the
|
||||
same kind (`hide_extra`, `hide_panel`); `end_extra`/`end_panel` still delete on a kind change.
|
||||
- Guarded style setters `set_color`/`set_number`; card content sizes from the styles
|
||||
(`tile_width`, `content_width`, `content_height`) instead of `lv_obj_update_layout()`.
|
||||
- Card sliders: `slider_handle()` narrows the knob and moves it back into the fill and sets the
|
||||
range to `-below..1000`, so 0 still shows one round end; `slider_event` keeps values at 0 or
|
||||
above, `commit_slider` clamps as before. Off cards get a grey fill.
|
||||
- The busy sheet is sized from the requested card width and kept as the tile's last child.
|
||||
- `show_page` ignores a request past the first or last page when that page is already shown.
|
||||
- `tick()` redraws clock cards when the minute changes.
|
||||
- `light_controls::setup(parent, font, width, height, icon_font)`: the icon font is new and
|
||||
optional; both board profiles pass `materialdesign_icons_mini` (the glyphs palette,
|
||||
thermometer and lightbulb are already in the tile icon set). Rows, sliders, `open()` and
|
||||
`self_test()` keep their behaviour; the brightness range starts below 1 for the same round end.
|
||||
- `swipe_profile.h` and the `swipe_test` message exist only with `-DSWIPE_PROFILE=1`; release
|
||||
builds and packages never set it.
|
||||
|
||||
### Compatibility 0.2.41 / firmware 0.2.35
|
||||
|
||||
Storage version and tile protocol stay 1. Firmware 0.2.35 only raises
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# Page swipe profile (2026-09-15, app 0.2.42 / firmware 0.2.36)
|
||||
|
||||
Why a page swipe on the Guition blocked the main loop for 300+ ms, what changed, and how to
|
||||
measure it again. ESPHome 2026.6.2, LVGL 9.5, bench boards on USB.
|
||||
|
||||
## How to measure
|
||||
|
||||
- Build with `-DSWIPE_PROFILE=1` in `esphome: platformio_options: build_flags` (the bench package
|
||||
only; release builds never carry it). Without the define every hook in
|
||||
`components/smart_display/swipe_profile.h` is an empty inline.
|
||||
- One INFO line per page switch, tag `swipe_prof`, times in ms from the swipe (`show_page`):
|
||||
`skel` CPU of the swipe pass, `fill` CPU of the later fill steps (`steps`), `first` swipe to
|
||||
the first frame on the glass, `done` swipe to the frame that completes the page, `gap` the
|
||||
longest main-loop gap until then (touch is polled once per loop), `frames` per LVGL refresh
|
||||
`+start:layout/render/flush:flush calls:pixels`, `slots` CPU per card, `parts` CPU per step
|
||||
of the passes. The first swipe also logs the object count (Guition: 251 on the screen).
|
||||
- Render and flush come from `lv_display_add_event_cb` (`LV_EVENT_RENDER_START/READY`,
|
||||
`LV_EVENT_FLUSH_START/FINISH`); the loop gap from an `lv_timer` that runs at the start of
|
||||
every `lv_timer_handler` call. ESPHome's "took a long time" warning only fires on a new
|
||||
maximum, so it is not the metric.
|
||||
- Without a finger: the diagnostic build accepts
|
||||
`{"v":1,"op":"swipe_test","n":12,"ms":1400}` through the `screen_message` action (page switch
|
||||
every `ms`; `"back": 150` swipes straight back, `"cards": 3` changes the fill step). It runs
|
||||
inside `lv_timer_handler` instead of the touch loop; both land in the same loop iteration.
|
||||
|
||||
Layout on the bench Guition (the owner's): 10 tiles on 2 pages. Page 1: climate, analog clock
|
||||
without background, two scripts, wide light with a mini slider. Page 2: script, vacuum, two
|
||||
scripts, wide light with brightness controls.
|
||||
|
||||
## Results (median / p90, ms)
|
||||
|
||||
| Build | n | first | done | gap | skel CPU | fill CPU | frame 1 render | frame 1 flush | worst frame | frames |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| 1 baseline (0.2.35) | 33 | 80 / 84 | 400 / 412 | 276 / 283 | 26 / 29 | 92 / 209 | 51 / 53 | 26 / 27 | 95 / 100 | 2 / 2 |
|
||||
| 2 custom parts and panels kept | 12 | 84 / 86 | 285 / 302 | 163 / 170 | 29 / 30 | 91 / 100 | 49 / 52 | 25 / 26 | 94 / 100 | 2 / 2 |
|
||||
| 3 fill in steps, grey skeleton | 30 | 100 / 107 | 237 / 253 | 125 / 138 | 30 / 36 | 33 / 40 | 61 / 66 | 24 / 25 | 69 / 72 | 3 / 3 |
|
||||
| 7 final (0.2.36) | 12 | 52 / 54 | 244 / 250 | 74 / 75 | 12 / 13 | 45 / 46 | 38 / 40 | 19 / 21 | 40 / 42 | 4 / 4 |
|
||||
| 7 final, CYD | 12 | 83 / 84 | 250 / 256 | 109 / 110 | 9 / 9 | 20 / 20 | 67 / 68 | 29 / 30 | 74 / 75 | 3 / 3 |
|
||||
|
||||
Builds 1–3 are touch swipes by the owner (build 3 includes taps on the page buttons, which
|
||||
run inside `lv_timer_handler` and show about 30 ms more gap); build 7 is `swipe_test`.
|
||||
`worst frame` is layout plus render of the slowest refresh. The CYD had no baseline run in
|
||||
this round; its log once showed `lvgl took a long time (397 ms)` before.
|
||||
|
||||
CPU per card in the content pass, median, baseline → final: to page 1 `11.8, 105.0, 44.8,
|
||||
12.1, 31.6` → `4.9, 10.1, 3.4, 3.5, 20.3`; to page 2 `12.0, 13.8, 12.9, 12.1, 30.8` →
|
||||
`4.9, 7.8, 3.4, 3.1, 21.4`.
|
||||
|
||||
## What the numbers showed
|
||||
|
||||
- The stall was mostly our own content pass, not LVGL's drawing: 207 ms CPU on page 1 before
|
||||
a single pixel, then an 80 ms frame. The clock card's 18 parts were deleted when the slot
|
||||
showed another card and created again on the way back (105 ms); the next card paid the
|
||||
layout of those new objects (45 ms); the wide light rebuilt its control panel (30 ms).
|
||||
- Every LVGL mutation costs about 0.3 ms on the Guition (objects and styles live in PSRAM,
|
||||
16 KB instruction cache, 32 KB data cache), and `lv_obj_update_layout()` walks the whole
|
||||
screen. A `lv_obj_set_style_*` with the value the object already has still refreshes and
|
||||
invalidates it.
|
||||
- Flush is about a third of a frame (19–26 ms per ~110–150k px, including LVGL's RGB565 byte
|
||||
swap), rendering the rest.
|
||||
|
||||
## What changed
|
||||
|
||||
1. **Fill in steps** (task step 2). `render()` is split into `render_slot()`. The swipe pass
|
||||
places the page (page number, card widths) and shows every card as a skeleton; each LVGL
|
||||
refresh after that draws the next two cards (`fill_cards`, one step per `LV_EVENT_REFR_READY`,
|
||||
40 ms fallback when a step changed no pixel). A new swipe cancels the fill. `render()` leaves
|
||||
waiting cards to the fill; state messages, commands and ticks now draw only their own card
|
||||
(`refresh_tile`), the top bar only the bar (`refresh_header_only`); a refresh from the board
|
||||
YAML (minute tick, time sync) still draws every card.
|
||||
2. **One cheap skeleton** (step 3). The skeleton is the empty card: contents hidden under a
|
||||
sheet in the card's colour over the content area (plain rectangle, no corners, border or
|
||||
layer). The card itself is not restyled and restored any more. First frame 80 → 52 ms.
|
||||
3. **Less work per card.** Custom parts and control panels are hidden instead of deleted when
|
||||
a slot shows another card and reused when the same kind comes back. Colours and opacities go
|
||||
through guarded setters (`set_color`, `set_number`) that skip unchanged values. Card content
|
||||
sizes come from the styles, so no layout pass per card. Swiping past the first or last page
|
||||
draws nothing. Clock cards redraw when the minute changes instead of every second.
|
||||
4. **Draw buffer** (step 4): not changed. Frames flush 1–10 areas of at most ~20k px each,
|
||||
below the 25 % buffer (57.6k px), so a larger buffer would not merge them. Direct mode into
|
||||
the RGB framebuffer would need glue around ESPHome's `lvgl`/`st7701s` components, and the
|
||||
owner prefers staying on ESPHome's and LVGL's defaults. Dropping LVGL's byte swap
|
||||
(`byte_order: little_endian` with a 16-pin `data_pins` list) was not tried for the same
|
||||
reason (colour risk on every Guition). `LV_OBJ_STYLE_CACHE=1` was built but not measured
|
||||
and is not enabled.
|
||||
5. **Styles** (step 5): the guarded setters above. Not done: widget `opa` on the disabled
|
||||
state of panel keys and detail buttons (only while a command is under way; replacing it
|
||||
with `bg_opa`/`text_opa` does not look identical for checked keys), pre-mixed graph and sun
|
||||
fills (no graph on the bench layout), a separate measurement of rounded corner masks.
|
||||
|
||||
Tried and dropped: a fade-in of each card under a fading sheet (150–180 ms). Completion went to
|
||||
390–550 ms with 50–100 ms frames (about 10–12 fps) because fading cards redraw completely every
|
||||
frame; the owner preferred speed.
|
||||
|
||||
## Remaining
|
||||
|
||||
- CYD: the longest gap is the skeleton frame itself over SPI (51.6k px, 67 ms render incl.
|
||||
29 ms flush), about 110 ms.
|
||||
- A wide card that alternates between a mini slider and a control panel in the same slot still
|
||||
costs about 20 ms CPU per switch (real geometry and font changes).
|
||||
@@ -0,0 +1,49 @@
|
||||
# Test results app 0.2.42 / firmware 0.2.36 (2026-09-15)
|
||||
|
||||
Smooth page swipes, slider handles inside the fill, the busy sheet over the whole card and a new
|
||||
colour card for lights. See CHANGELOG 0.2.42, RELEASING "Compatibility 0.2.42" and
|
||||
docs/SWIPE_PROFILE.md for the measurements.
|
||||
|
||||
## Automated
|
||||
|
||||
- Python (`.venv-portal`): 139 tests OK.
|
||||
- C++ (clang++ -std=c++17 -Wall -Wextra -Werror -I.): 13/13 PASS.
|
||||
- `generate_packages.py --check` green; `node --check app.js` clean.
|
||||
- ESPHome 2026.6.2 compile SUCCESS, one after another per board (manual profile first, then Easy
|
||||
Setup, so the upload took the Easy build): `guition-device.yaml` (RAM 32.6%, flash 19.7%),
|
||||
`easy-guition-device.yaml` (RAM 32.5%, flash 18.7%), `device.yaml` (RAM 31.6%, flash 82.3%),
|
||||
`easy-cyd-device.yaml` (RAM 31.5%, flash 77.5%). Packages carry `SCREEN_FIRMWARE_VERSION` 0.2.36.
|
||||
|
||||
## Bench boards (USB)
|
||||
|
||||
- Both Easy Setup builds uploaded over USB; `device_info` reports project 0.2.36, compiled
|
||||
2026-09-15 10:54:46 (Guition) and 10:54:48 (CYD), matching the build logs.
|
||||
- `diagnostics/run_ui_test.py` on both release builds: 10/10 `page_check=PASS`, 50 overlay render
|
||||
cycles, no failures, no `page fill still under way` line. The same passed on the diagnostic
|
||||
builds before.
|
||||
- Swipe measurements with `-DSWIPE_PROFILE=1` builds (same code plus the profiler), 12 page
|
||||
switches each through `swipe_test`, median / p90:
|
||||
- Guition (owner's layout, 10 tiles, 2 pages): first frame 52 / 54 ms, page complete
|
||||
244 / 250 ms, longest loop gap 74 / 75 ms (baseline 276 / 283 ms), slowest frame 40 / 42 ms.
|
||||
- CYD (8 tiles, 2 pages): first frame 83 / 84 ms, page complete 250 / 256 ms, longest loop gap
|
||||
109 / 110 ms (the skeleton frame over SPI).
|
||||
- Earlier builds of this round were swiped by hand by the owner on the Guition (33 swipes for the
|
||||
baseline, 30 on the step fill); a fade variant was tried and dropped at the owner's request.
|
||||
|
||||
## Renders (ESPHome host + SDL, both boards, firmware code of this release)
|
||||
|
||||
- Light cards at 1, 3, 5, 60, 90 and 100 %: the white handle sits inside the fill on mini sliders
|
||||
and control sliders, 1 % shows one round end with the handle inside.
|
||||
- Busy sheet on a wide card with brightness controls: covers the whole card, spinner centred.
|
||||
- Colour card at both ends and in the middle of every track (knobs whole, not cut off), and for a
|
||||
light with only colour temperature or only colour (two cards each). The close and done
|
||||
buttons are unchanged; the demo preview hides them.
|
||||
|
||||
## Not tested / for the owner
|
||||
|
||||
- The final release build by hand on the glass (the owner swiped the earlier builds of this round).
|
||||
- The grey slider end of a light that is off on a real light (render path only through the palette
|
||||
code; the demo lights were on).
|
||||
- The colour card on a real colour light with Home Assistant commands (renders use the demo
|
||||
preview without actions).
|
||||
- The update from 0.2.35 to 0.2.36 through the app.
|
||||
Reference in New Issue
Block a user