Let the page buttons keep up with the finger: Next, Next, Next reaches page 4 (0.2.72)

The Previous and Next bars ran their tap through the tile guard, whose 600 ms
same-tile window (so one contact never fires a tile twice) dropped the second
and third tap of a quick Next, Next, Next. They now use accept_repeat, the
-/+ keys' rule since 0.2.25: every clean tap counts, only a contact within
150 ms of the last accepted tap on the same button is a bounce. The page fill
already dropped a fill under way when the next page was asked for. Firmware
0.2.61; test_cyd_ui.cpp covers the page buttons; host harness and check builds
in docs/TEST_RESULTS_0272.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Max Gramser
2026-09-18 07:29:23 +02:00
parent 2cb347964d
commit 9f5694b9eb
11 changed files with 124 additions and 16 deletions
+3 -2
View File
@@ -73,8 +73,9 @@ class TouchGuard {
if (moved_) { reject_ = MOVED; return false; } if (moved_) { reject_ = MOVED; return false; }
return accept_slider(now, tile); return accept_slider(now, tile);
} }
// For -/+ keys: every clean tap counts, even the third within a second, so a // For -/+ keys and the page buttons: every clean tap counts, even the third within a second,
// setpoint moves several steps in one go. Only bounce (same key within `gap`) is dropped. // so a setpoint moves several steps in one go and Next, Next, Next reaches page 4 without
// waiting for each page to draw. Only bounce (same key within `gap`) is dropped.
bool accept_repeat(uint32_t now, int tile, uint32_t gap = 150) { bool accept_repeat(uint32_t now, int tile, uint32_t gap = 150) {
if (moved_) { reject_ = MOVED; return false; } if (moved_) { reject_ = MOVED; return false; }
if (accepted_) { reject_ = USED; return false; } if (accepted_) { reject_ = USED; return false; }
+15
View File
@@ -176,6 +176,21 @@ icons sit off-center in the browser); run `generate_packages.py` afterward.
The `MDI_GLYPH_*` substitutions are retired: `TILEn_ICON` in manual The `MDI_GLYPH_*` substitutions are retired: `TILEn_ICON` in manual
profiles must come from the set. No changed preferences or keys. profiles must come from the set. No changed preferences or keys.
### Compatibility 0.2.72 / firmware 0.2.61
Firmware only (both board profiles; `cyd_ui.h` gains a comment); the app raises `FIRMWARE_VERSION`. No protocol,
storage or editor change, and an older app drives this firmware as before.
- The `< Previous` and `Next >` bars on `home_page` call `cyd::touch_guard.accept_repeat(millis(), 11 / 12)` instead
of `accept()`. `accept()` keeps a 600 ms window per tile so one contact can never fire a tile's action twice; on the
page bar that window dropped the second and third tap of Next, Next, Next, so page 4 took three waits of about
half a second. `accept_repeat`, the -/+ keys' rule since 0.2.25, still drops a contact shorter than 40 ms (20 ms on
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.
- 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.
### Compatibility 0.2.71 / firmware 0.2.60 ### Compatibility 0.2.71 / firmware 0.2.60
Firmware only (`runtime_model.h`, `runtime_tiles.h`); the app raises `FIRMWARE_VERSION`. No protocol, storage or Firmware only (`runtime_model.h`, `runtime_tiles.h`); the app raises `FIRMWARE_VERSION`. No protocol, storage or
+76
View File
@@ -0,0 +1,76 @@
# Test results app 0.2.72 / firmware 0.2.61 (2026-09-18)
Max: "als ik snel naar pagina 4 wil, kan ik niet snel doorklikken, dan moet je steeds wachten totdat de pagina geladen
is". Next, Next, Next on the page bar took three waits of about half a second each. This release lets the page buttons
keep up with the finger. See CHANGELOG 0.2.72 and the compatibility note in docs/RELEASING.md. Built on 0.2.71
(`2cb3479`).
## What happened before
- The `< Previous` and `Next >` bars ran their tap through `cyd::touch_guard.accept()`, the guard of every tile: it
drops a second tap on the same tile within 600 ms as a bounce, so one contact can never switch a light twice. On
the page bar that window dropped the second and third tap of Next, Next, Next. The page itself was never the
problem: since 0.2.42 a page switch places the new page in the next frame and fills its cards two per refresh, and
a new switch drops a fill still under way.
- The -/+ keys of the setpoint pill had the same complaint in 0.2.25 and got `accept_repeat()`: every clean tap
counts, only a contact within 150 ms of the last one on the same key is a bounce. The page buttons now use it.
## Automated
- C++ (`clang++ -std=c++17 -Wall -Wextra -Werror -I.`): 17/17 PASS in a clean export of the committed tree plus this
round's changes. `tests/test_cyd_ui.cpp` now holds the page buttons: three Next taps 250 ms apart all count, the
old `accept()` still refuses within its window, and Previous right after Next counts.
- Python (`.venv-portal`): 386 tests OK, the same set as 0.2.71 (firmware only; the app raises `FIRMWARE_VERSION`).
- `generate_packages.py --check` green.
- The working tree also carried another session's uncommitted `runtime_model.h` / `runtime_tiles.h` work (the
48-tile round, 0.2.73); every build and test below ran from a clean export with only this round's changes.
## Firmware on the Mac (host builds, driven over the ESPHome API)
`pager_host.py` (scratchpad, after `wake_sleep_host.py`): the living room layout of the demo home on 8099 (17 tiles,
four pages), a `pager_taps` action that taps the page bar from an LVGL timer (`count` taps `gap_ms` apart, each a
90 ms contact ending in `LV_EVENT_SHORT_CLICKED` on the button, as the touch layer produces it), an `old_rule` mode
that taps the way firmware 0.2.60 did, and a probe that logs the page, the page number label and the fill state.
**Guition 12/12, CYD 12/12** (the first run judged the bounce and old-rule bursts by the spacing asked for, and
the host's LVGL loop stalls about a second on the first frame of a burst, macOS throttling the SDL window, so
two taps meant to be 80 ms apart landed 1040 ms apart; the checks now judge every tap against the spacing that
really happened, and the second run passed on both boards).
| Check | Guition | CYD |
|---|---|---|
| The living room layout shows four pages, page 1 drawn | pass | pass |
| Next, Next, Next 200 ms apart lands on page 4 (4 / 4) | pass | pass |
| Every one of those taps moved a page: 2, 3, 4, none refused | pass | pass |
| Page 4 finishes drawing afterwards | pass | pass |
| Two more Next taps on the last page change nothing | pass | pass |
| Previous, Previous, Previous 200 ms apart lands on page 1 | pass | pass |
| Three Next taps 160 ms apart (six a second) land on page 4 | pass | pass |
| Three Previous taps 160 ms apart land on page 1 | pass | pass |
| Bounce: a Next within 150 ms of the last accepted tap is refused, a slower one counts and moves a page | pass | pass |
| The old rule (0.2.60): a Next within 600 ms of the last accepted tap was refused | pass | pass |
| Back on page 1 afterwards | pass | pass |
| No errors in the log | pass | pass |
The bounce burst on the Guition landed at 98, 94 and 81 ms and was accepted, refused, accepted, refused, as the
rule says (the third tap came 192 ms after the first accepted one); the CYD's at 84, 84 and 96 ms the same way. The
old-rule burst on the Guition landed at 208, 202 and 203 ms and was accepted, refused, refused, accepted: that is
what Max felt, one page per 600 ms, however fast he tapped.
## Builds (ESPHome 2026.6.2)
Check profiles with the generated packages and local components; no errors or compiler warnings.
| Profile | RAM | Flash | vs 0.2.71 |
|---|---|---|---|
| Guition | 25.7% (84,372 B) | 21.0% (1,708,943 B) | same RAM, -24 B flash |
| CYD | 24.0% (78,796 B) | 79.2% (1,453,647 B) | same RAM, -36 B flash |
The check builds carry a device name of their own (`check-<board>-f9`) and placeholder Wi-Fi, API and OTA values, so
the sizes differ from the bench profiles by the length of those strings; the code is the same.
## Not tested
- Real hardware: neither bench screen ran this firmware before the release. Max's own screens get it through Update;
the tap that started this is the one to try there, on the Guition and on the CYD.
- A resistive panel's lift-off bounce on the CYD is covered by the 40 ms minimum contact and the 150 ms gap, the same
net the -/+ keys have had since 0.2.25 (physically tested then), not by a new measurement.
+3 -3
View File
@@ -2,7 +2,7 @@
# Source profile for packages/guition.yaml (tools/generate_packages.py); see docs/GUITION.md. # Source profile for packages/guition.yaml (tools/generate_packages.py); see docs/GUITION.md.
# A screen gets its tiles from the ESP Screen Manager add-on while it runs. The CYD is its own profile. # A screen gets its tiles from the ESP Screen Manager add-on while it runs. The CYD is its own profile.
substitutions: substitutions:
SCREEN_FIRMWARE_VERSION: "0.2.60" SCREEN_FIRMWARE_VERSION: "0.2.61"
# 0: no drift limit; LVGL decides, releasing inside the tile is a tap, leaving it is not # 0: no drift limit; LVGL decides, releasing inside the tile is a tap, leaving it is not
# (like iOS). The CYD keeps a limit because its resistive panel jumps. # (like iOS). The CYD keeps a limit because its resistive panel jumps.
TOUCH_MOVE_LIMIT_PX: "0" TOUCH_MOVE_LIMIT_PX: "0"
@@ -2438,7 +2438,7 @@ lvgl:
on_short_click: on_short_click:
- if: - if:
condition: condition:
lambda: 'return cyd::touch_guard.accept(millis(), 11);' lambda: 'return cyd::touch_guard.accept_repeat(millis(), 11);'
then: then:
- lambda: 'id(tile_page) = std::max(0, id(tile_page) - 1);' - lambda: 'id(tile_page) = std::max(0, id(tile_page) - 1);'
- script.execute: show_tile_page - script.execute: show_tile_page
@@ -2479,7 +2479,7 @@ lvgl:
on_short_click: on_short_click:
- if: - if:
condition: condition:
lambda: 'return cyd::touch_guard.accept(millis(), 12);' lambda: 'return cyd::touch_guard.accept_repeat(millis(), 12);'
then: then:
- lambda: 'id(tile_page) += 1;' - lambda: 'id(tile_page) += 1;'
- script.execute: show_tile_page - script.execute: show_tile_page
+3 -3
View File
@@ -17,7 +17,7 @@
############################################################ ############################################################
substitutions: substitutions:
SCREEN_FIRMWARE_VERSION: "0.2.60" SCREEN_FIRMWARE_VERSION: "0.2.61"
# A tap survives a finger that drifts up to this far (320 px over 58 mm: 1 cm); further is a swipe. # A tap survives a finger that drifts up to this far (320 px over 58 mm: 1 cm); further is a swipe.
TOUCH_MOVE_LIMIT_PX: "56" TOUCH_MOVE_LIMIT_PX: "56"
# Shortest contact that counts as a tap; a resistive panel bounces, a capacitive one does not. # Shortest contact that counts as a tap; a resistive panel bounces, a capacitive one does not.
@@ -2351,7 +2351,7 @@ lvgl:
on_short_click: on_short_click:
- if: - if:
condition: condition:
lambda: 'return cyd::touch_guard.accept(millis(), 11);' lambda: 'return cyd::touch_guard.accept_repeat(millis(), 11);'
then: then:
- lambda: 'id(tile_page) = std::max(0, id(tile_page) - 1);' - lambda: 'id(tile_page) = std::max(0, id(tile_page) - 1);'
- script.execute: show_tile_page - script.execute: show_tile_page
@@ -2392,7 +2392,7 @@ lvgl:
on_short_click: on_short_click:
- if: - if:
condition: condition:
lambda: 'return cyd::touch_guard.accept(millis(), 12);' lambda: 'return cyd::touch_guard.accept_repeat(millis(), 12);'
then: then:
- lambda: 'id(tile_page) += 1;' - lambda: 'id(tile_page) += 1;'
- script.execute: show_tile_page - script.execute: show_tile_page
+3 -3
View File
@@ -18,7 +18,7 @@
############################################################ ############################################################
substitutions: substitutions:
SCREEN_FIRMWARE_VERSION: "0.2.60" SCREEN_FIRMWARE_VERSION: "0.2.61"
# A tap survives a finger that drifts up to this far (320 px over 58 mm: 1 cm); further is a swipe. # A tap survives a finger that drifts up to this far (320 px over 58 mm: 1 cm); further is a swipe.
TOUCH_MOVE_LIMIT_PX: "56" TOUCH_MOVE_LIMIT_PX: "56"
# Shortest contact that counts as a tap; a resistive panel bounces, a capacitive one does not. # Shortest contact that counts as a tap; a resistive panel bounces, a capacitive one does not.
@@ -2340,7 +2340,7 @@ lvgl:
on_short_click: on_short_click:
- if: - if:
condition: condition:
lambda: 'return cyd::touch_guard.accept(millis(), 11);' lambda: 'return cyd::touch_guard.accept_repeat(millis(), 11);'
then: then:
- lambda: 'id(tile_page) = std::max(0, id(tile_page) - 1);' - lambda: 'id(tile_page) = std::max(0, id(tile_page) - 1);'
- script.execute: show_tile_page - script.execute: show_tile_page
@@ -2381,7 +2381,7 @@ lvgl:
on_short_click: on_short_click:
- if: - if:
condition: condition:
lambda: 'return cyd::touch_guard.accept(millis(), 12);' lambda: 'return cyd::touch_guard.accept_repeat(millis(), 12);'
then: then:
- lambda: 'id(tile_page) += 1;' - lambda: 'id(tile_page) += 1;'
- script.execute: show_tile_page - script.execute: show_tile_page
+3 -3
View File
@@ -3,7 +3,7 @@
# Source profile for packages/guition.yaml (tools/generate_packages.py); see docs/GUITION.md. # Source profile for packages/guition.yaml (tools/generate_packages.py); see docs/GUITION.md.
# A screen gets its tiles from the ESP Screen Manager add-on while it runs. The CYD is its own profile. # A screen gets its tiles from the ESP Screen Manager add-on while it runs. The CYD is its own profile.
substitutions: substitutions:
SCREEN_FIRMWARE_VERSION: "0.2.60" SCREEN_FIRMWARE_VERSION: "0.2.61"
# 0: no drift limit; LVGL decides, releasing inside the tile is a tap, leaving it is not # 0: no drift limit; LVGL decides, releasing inside the tile is a tap, leaving it is not
# (like iOS). The CYD keeps a limit because its resistive panel jumps. # (like iOS). The CYD keeps a limit because its resistive panel jumps.
TOUCH_MOVE_LIMIT_PX: "0" TOUCH_MOVE_LIMIT_PX: "0"
@@ -2427,7 +2427,7 @@ lvgl:
on_short_click: on_short_click:
- if: - if:
condition: condition:
lambda: 'return cyd::touch_guard.accept(millis(), 11);' lambda: 'return cyd::touch_guard.accept_repeat(millis(), 11);'
then: then:
- lambda: 'id(tile_page) = std::max(0, id(tile_page) - 1);' - lambda: 'id(tile_page) = std::max(0, id(tile_page) - 1);'
- script.execute: show_tile_page - script.execute: show_tile_page
@@ -2468,7 +2468,7 @@ lvgl:
on_short_click: on_short_click:
- if: - if:
condition: condition:
lambda: 'return cyd::touch_guard.accept(millis(), 12);' lambda: 'return cyd::touch_guard.accept_repeat(millis(), 12);'
then: then:
- lambda: 'id(tile_page) += 1;' - lambda: 'id(tile_page) += 1;'
- script.execute: show_tile_page - script.execute: show_tile_page
+8
View File
@@ -1,3 +1,11 @@
## 0.2.72 (firmware 0.2.61)
Next, Next, Next: the page buttons keep up with your finger.
- **Tap through the pages as fast as you like.** Tap Next three times in a row and you are on page 4, whether or not the pages in between had finished drawing. Before, a second tap on the same button within 600 ms was dropped as a bounce, the rule that keeps a light from switching twice on one tap, so every page made you wait before the next tap counted. The page buttons now follow the rule of the -/+ keys: every clean tap counts, and only a contact within 150 ms of the last one on the same button is taken for a bounce.
- **Previous does the same**, and tapping past the first or last page still changes nothing.
- Needs firmware 0.2.61: press **Update** on the screen. Includes everything from 0.2.71.
## 0.2.71 (firmware 0.2.60) ## 0.2.71 (firmware 0.2.60)
A small slider stays where you leave it while the lights fade towards it. A small slider stays where you leave it while the lights fade towards it.
+1 -1
View File
@@ -20,7 +20,7 @@ WEEKDAYS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
REPO = 'https://github.com/MaxGramser/homeassistant_espscreen' REPO = 'https://github.com/MaxGramser/homeassistant_espscreen'
REFS = {'cyd': 'main', 'guition': 'main'} REFS = {'cyd': 'main', 'guition': 'main'}
# Firmware shipped with this app release; screens below it get an update offer. # Firmware shipped with this app release; screens below it get an update offer.
FIRMWARE_VERSION = '0.2.60' FIRMWARE_VERSION = '0.2.61'
# The Auto standby switch a screen offers Home Assistant automations. # The Auto standby switch a screen offers Home Assistant automations.
AUTO_STANDBY_MIN_FIRMWARE = '0.2.41' AUTO_STANDBY_MIN_FIRMWARE = '0.2.41'
# The settings page the screen opens itself, and the screen.settings tile that opens it. # The settings page the screen opens itself, and the screen.settings tile that opens it.
+1 -1
View File
@@ -1,5 +1,5 @@
name: ESP Screen Manager name: ESP Screen Manager
version: "0.2.71" version: "0.2.72"
slug: esp_screen_manager slug: esp_screen_manager
description: Choose tiles for your CYD and Guition touchscreen, without reflashing. description: Choose tiles for your CYD and Guition touchscreen, without reflashing.
url: https://github.com/MaxGramser/homeassistant_espscreen url: https://github.com/MaxGramser/homeassistant_espscreen
+8
View File
@@ -21,6 +21,14 @@ int main() {
r.begin(1300); assert(!r.accept_repeat(1340, 7)); // 80 ms after the last: bounce r.begin(1300); assert(!r.accept_repeat(1340, 7)); // 80 ms after the last: bounce
r.begin(1300); r.update(40, 0); assert(!r.accept_repeat(1500, 7)); // a swipe is never a step r.begin(1300); r.update(40, 0); assert(!r.accept_repeat(1500, 7)); // a swipe is never a step
r.begin(1600); assert(r.accept_repeat(1660, 8)); // the other key right away r.begin(1600); assert(r.accept_repeat(1660, 8)); // the other key right away
// Page buttons (0.2.72): Next, Next, Next at a finger's pace all count, so page 4 is three taps away
// while the pages are still drawing; accept() would have dropped the second and third (600 ms).
cyd::TouchGuard pager;
pager.begin(2000); assert(pager.accept_repeat(2080, 12));
pager.begin(2250); assert(pager.accept_repeat(2330, 12)); // 250 ms after the last
pager.begin(2500); assert(pager.accept_repeat(2580, 12)); // and again
pager.begin(2600); assert(!pager.accept(2680, 12)); // the old rule: bounce window
pager.begin(2700); assert(pager.accept_repeat(2780, 11)); // Previous right after Next
cyd::TouchGuard g; cyd::TouchGuard g;
g.begin(100); g.begin(100);
assert(!g.accept(120, 1)); // resistive noise pulse assert(!g.accept(120, 1)); // resistive noise pulse