Swipe in from the side edge to change pages on the Guition (0.2.29)

Page swipes were an LVGL gesture anywhere on the screen: 50 px, fast only,
and almost always starting on a tile that lit up as pressed. On the Guition
a touch that starts within 32 px of the left or right edge and travels
40 px inward, clearly sideways, now flips one page (from the right edge
leftwards is next, from the left edge rightwards previous), at any speed
and in every rotation; a swipe that starts in the middle does nothing, so
taps and drags on tiles can no longer change the page by accident. The
whole-screen gesture is removed from the Guition profile; the CYD keeps
it. Firmware 0.2.24 for both boards, the app only reworded the setting.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Max Gramser
2026-09-14 11:40:19 +02:00
parent ea0b423fab
commit 104f20e4fa
13 changed files with 170 additions and 31 deletions
+44
View File
@@ -127,4 +127,48 @@ class TouchGuard {
Reject reject_{NONE};
};
inline TouchGuard touch_guard;
// Page navigation by swiping in from a side edge (Guition, firmware 0.2.24+). A touch that
// starts within `band` pixels of the left or right edge of the screen as the user sees it
// and travels `travel` pixels inward, clearly more sideways than up or down, flips one
// page: from the right edge leftwards is "next", from the left edge rightwards "previous".
// No speed requirement; a touch that starts in the middle never counts, so tapping and
// dragging on tiles cannot change the page by accident.
class EdgeSwipe {
public:
void configure(int width, int height, int band, int travel) {
width_ = width; height_ = height; band_ = band; travel_ = travel;
}
// Native touch coordinates plus the LVGL rotation in degrees (0, 90, 180, 270), mapped
// the way ESPHome's LVGL component rotates its pointer input.
void begin(int x, int y, int rotation = 0) {
rotation_ = rotation;
rotate(x, y);
start_x_ = x; start_y_ = y;
const int width = (rotation == 90 || rotation == 270) ? height_ : width_;
from_ = x < band_ ? 1 : x >= width - band_ ? -1 : 0; // 1: left edge, -1: right edge
done_ = false;
}
bool armed() const { return from_ != 0 && !done_; }
// +1 next page, -1 previous page, 0 nothing; fires at most once per touch.
int update(int x, int y) {
if (!armed()) return 0;
rotate(x, y);
const int dx = x - start_x_, dy = y - start_y_;
const int inward = from_ == 1 ? dx : -dx;
if (inward < travel_ || std::abs(dx) < 2 * std::abs(dy)) return 0;
done_ = true;
return from_ == 1 ? -1 : 1;
}
private:
void rotate(int &x, int &y) const {
if (rotation_ == 90) { const int tmp = y; y = width_ - x - 1; x = tmp; }
else if (rotation_ == 180) { x = width_ - x - 1; y = height_ - y - 1; }
else if (rotation_ == 270) { const int tmp = x; x = height_ - y - 1; y = tmp; }
}
int width_{480}, height_{480}, band_{32}, travel_{40}, rotation_{0};
int start_x_{0}, start_y_{0}, from_{0};
bool done_{true};
};
inline EdgeSwipe edge_swipe;
} // namespace cyd
+6 -2
View File
@@ -138,8 +138,12 @@ Klik **Opslaan & naar scherm** om je wijzigingen door te sturen.
Vanaf firmware 0.2.7 passen twintig tegels op maximaal vier pagina’s. Oudere
firmware houdt de limiet van tien totdat je bijwerkt. Via **Algemene instellingen
→ Vegen tussen pagina’s** kun je horizontaal swipen inschakelen. Sliders bedienen
alleen hun waarde; detailmenu’s en standby wisselen niet van pagina.
→ Vegen tussen pagina’s** kun je vegen inschakelen. Op de Guition (firmware
0.2.24+) veeg je dan vanaf de linker- of rechterrand naar binnen, zoals
terug-vegen op een telefoon; langzaam of snel, en een veeg die midden op het
scherm begint doet niets, zodat tikken en slepen op tegels nooit per ongeluk van
pagina wisselen. Op de CYD blijft het een snelle veeg over het scherm. Sliders
bedienen alleen hun waarde; detailmenu’s en standby wisselen niet van pagina.
Er staan zes tegels op een pagina. Bij maximaal zes verdwijnen Vorige/Volgende.
De standby-tijd is standaard tien minuten. Bij offline apparaten blokkeert het
+11
View File
@@ -177,6 +177,17 @@ staan iconen in de browser uit het midden); draai daarna `generate_packages.py`.
De substituties `MDI_GLYPH_*` zijn vervallen: `TILEn_ICON` in handmatige
profielen moet uit de set komen. Geen gewijzigde preferences of sleutels.
### Compatibiliteit 0.2.29 / firmware 0.2.24
Alleen firmware (Guition). `cyd::EdgeSwipe` (in `cyd_ui.h`, getest in
`test_cyd_ui.cpp`) vervangt op de Guition de LVGL-gesture op `home_page`: `begin()`
in `on_touch` met de LVGL-rotatie, `update()` in `on_update` voor het eerste contact;
bij een treffer `touch_guard.consume()`, `lv_indev_wait_release()` op alle indevs en
`show_tile_page`. Substituties `EDGE_SWIPE_BAND_PX`/`EDGE_SWIPE_TRAVEL_PX`;
`DISPLAY_W`/`DISPLAY_H` bepalen de rotatiemapping (ESPHome: 90° `x=y, y=W-x-1`,
270° `x=H-y-1, y=x`, 180° spiegelt beide). De CYD houdt de LVGL-gesture. De
instelling `swipe_pages` blijft de schakelaar; protocol, opslag en sleutels ongewijzigd.
### Compatibiliteit 0.2.28 / firmware 0.2.23
Alleen firmware. `cyd::TouchGuard` krijgt `configure(move_limit_px, min_press_ms)`
+36
View File
@@ -0,0 +1,36 @@
# Testresultaten app 0.2.29 / firmware 0.2.24 (2026-09-14)
Vegen vanaf de zijrand op de Guition vervangt de LVGL-gesture over het hele scherm;
de CYD houdt zijn snelle veeg. In de app alleen de omschrijving van de instelling.
## Wat veranderde
- `cyd::EdgeSwipe` in `cyd_ui.h`: een aanraking die binnen `EDGE_SWIPE_BAND_PX` (32) van de
linker- of rechterrand begint en `EDGE_SWIPE_TRAVEL_PX` (40) naar binnen aflegt, duidelijk
meer zijwaarts dan verticaal (|dx| ≥ 2·|dy|), wisselt één keer per aanraking van pagina.
Geen snelheidseis. De rotatie volgt ESPHome's LVGL-mapping (90°: x=y, y=W−x−1; 270°:
x=H−y−1, y=x; 180°: beide gespiegeld), dus "linkerrand" is altijd de rand die de gebruiker ziet.
- Guition-profiel: `configure()` in `on_boot`, `begin()` in `on_touch`, `update()` in
`on_update` voor het eerste contact; bij een treffer `touch_guard.consume()`,
`lv_indev_wait_release()` en `show_tile_page` (die de pagina begrenst). De
`LV_EVENT_GESTURE`-handler op `home_page` is verwijderd. Dezelfde blokkades als voorheen:
instelling uit, gedimd, kalibratie, open kaart.
## Geautomatiseerd
- C++ (clang++ -std=c++17 -Wall -Wextra -Werror -I.): 11/11 PASS. `test_cyd_ui` dekt:
42 px vanaf links = vorige, vanaf rechts = volgende, één keer per aanraking, start in het
midden doet niets, diagonaal telt niet en naar buiten bewegen niet, later rechtgetrokken telt
wel, en de drie rotaties.
- Python: 84 tests OK (`FIRMWARE_VERSION` 0.2.24 in app, profielen en pakketten).
`generate_packages.py --check` groen; bench-kopieën opnieuw gegenereerd.
- ESPHome 2026.6.2 compile: `easy-guition-device.yaml` SUCCESS in 41 s (RAM 31,9%, flash
18,3%), `easy-cyd-device.yaml` SUCCESS in 40 s (RAM 30,9%, flash 76,0%); geen nieuwe
waarschuwingen.
## Nog niet geverifieerd
- Fysiek vegen vanaf de rand van de Guition (frame van de wallbox, eerste GT911-meting
bij x≈0–10), inclusief een gedraaid scherm; en of de CYD-veeg onveranderd werkt na de
herbouw. De bench-borden hingen niet aan USB en reageerden niet op de API. Max werkt bij
via **Bijwerken** en kijkt in het log naar `randveeg: pagina 0 -> 1`.
+18 -12
View File
@@ -1,11 +1,15 @@
# Guition ESP32-S3-4848S040 / 4-inch EU wallbox, 480x480 IPS + GT911.
# Use guition-device.yaml; see docs/GUITION.md. CYD hardware is a separate profile.
substitutions:
SCREEN_FIRMWARE_VERSION: "0.2.23"
SCREEN_FIRMWARE_VERSION: "0.2.24"
# A tap survives a finger that drifts up to this far (480 px over 72 mm: 1 cm); further is a swipe.
TOUCH_MOVE_LIMIT_PX: "67"
# Shortest contact that counts as a tap; a resistive panel bounces, a capacitive one does not.
TOUCH_MIN_PRESS_MS: "20"
# Page swipe starts in this band along the left or right edge (about 5 mm) and needs
# this much travel inward (about 6 mm); a swipe from the middle of the screen does nothing.
EDGE_SWIPE_BAND_PX: "32"
EDGE_SWIPE_TRAVEL_PX: "40"
DYNAMIC_TILES: "false"
DASHBOARD_INBOX_INTERNAL: "true"
DEVICE_NAME: "guition-wallbox"
@@ -592,6 +596,7 @@ esphome:
then:
- lambda: |-
cyd::touch_guard.configure(${TOUCH_MOVE_LIMIT_PX}, ${TOUCH_MIN_PRESS_MS});
cyd::edge_swipe.configure(${DISPLAY_W}, ${DISPLAY_H}, ${EDGE_SWIPE_BAND_PX}, ${EDGE_SWIPE_TRAVEL_PX});
id(last_touch_ms) = millis();
id(display_dimmed) = false;
lv_obj_set_style_pad_row(id(vacuum_actions), 4, 0);
@@ -630,17 +635,6 @@ esphome:
lv_obj_add_flag(id(vacuum_overlay), LV_OBJ_FLAG_HIDDEN);
};
runtime_tiles::layout_changed = []() { id(show_tile_page).execute(); };
lv_obj_add_event_cb(id(home_page)->obj, [](lv_event_t *event) {
if(!runtime_tiles::enabled || !runtime_tiles::swipe_pages || id(display_dimmed) || id(calibration_active) ||
!id(active_entity).empty() || (runtime_tiles::detail_root && !lv_obj_has_flag(runtime_tiles::detail_root,LV_OBJ_FLAG_HIDDEN)))return;
auto *input=lv_indev_active();if(!input)return;
auto direction=lv_indev_get_gesture_dir(input);
if(direction!=LV_DIR_LEFT && direction!=LV_DIR_RIGHT)return;
cyd::touch_guard.consume();
lv_indev_wait_release(input);
id(tile_page)+=direction==LV_DIR_LEFT?1:-1;
id(show_tile_page).execute();
}, LV_EVENT_GESTURE, nullptr);
runtime_tiles::refresh = []() { if (!id(ui_refresh).is_running()) id(ui_refresh).execute(); };
runtime_tiles::bind(0, id(tile1), id(t1_title), id(t1_value), id(tile1_icon_circle), id(tile1_icon_lbl));
runtime_tiles::bind(1, id(tile2), id(t2_title), id(t2_value), id(tile2_icon_circle), id(tile2_icon_lbl));
@@ -1876,6 +1870,7 @@ touchscreen:
id(last_touch_ms) = millis();
id(touch_down) = true;
cyd::touch_guard.begin(millis(), touch.x, touch.y, touch.id);
cyd::edge_swipe.begin(touch.x, touch.y, static_cast<int>(id(screen_lvgl).get_rotation()));
ESP_LOGI("touch", "GT911 press x=%u y=%u test=%d", touch.x, touch.y, id(calibration_active));
on_update:
- lambda: |-
@@ -1883,6 +1878,17 @@ touchscreen:
// Only the contact that started the touch counts; the guard ignores other ids.
for (const auto &point : touches) {
cyd::touch_guard.update(point.x, point.y, point.id);
if (point.id != cyd::touch_guard.contact()) continue;
// Swiping in from a side edge flips the page ("Vegen tussen pagina's"); the tap
// under the finger is consumed and LVGL waits for the release.
const int step = cyd::edge_swipe.update(point.x, point.y);
if (!step || !runtime_tiles::enabled || !runtime_tiles::swipe_pages || id(display_dimmed) || id(calibration_active) ||
!id(active_entity).empty() || (runtime_tiles::detail_root && !lv_obj_has_flag(runtime_tiles::detail_root, LV_OBJ_FLAG_HIDDEN))) continue;
cyd::touch_guard.consume();
for (auto *indev = lv_indev_get_next(nullptr); indev; indev = lv_indev_get_next(indev)) lv_indev_wait_release(indev);
ESP_LOGI("touch", "randveeg: pagina %d -> %d", id(tile_page), id(tile_page) + step);
id(tile_page) += step;
id(show_tile_page).execute();
}
on_release:
- lambda: |-
+1 -1
View File
@@ -58,7 +58,7 @@
############################################################
substitutions:
SCREEN_FIRMWARE_VERSION: "0.2.23"
SCREEN_FIRMWARE_VERSION: "0.2.24"
# 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"
# Shortest contact that counts as a tap; a resistive panel bounces, a capacitive one does not.
+1 -1
View File
@@ -59,7 +59,7 @@
############################################################
substitutions:
SCREEN_FIRMWARE_VERSION: "0.2.23"
SCREEN_FIRMWARE_VERSION: "0.2.24"
# 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"
# Shortest contact that counts as a tap; a resistive panel bounces, a capacitive one does not.
+18 -12
View File
@@ -2,11 +2,15 @@
# Guition ESP32-S3-4848S040 / 4-inch EU wallbox, 480x480 IPS + GT911.
# Use guition-device.yaml; see docs/GUITION.md. CYD hardware is a separate profile.
substitutions:
SCREEN_FIRMWARE_VERSION: "0.2.23"
SCREEN_FIRMWARE_VERSION: "0.2.24"
# A tap survives a finger that drifts up to this far (480 px over 72 mm: 1 cm); further is a swipe.
TOUCH_MOVE_LIMIT_PX: "67"
# Shortest contact that counts as a tap; a resistive panel bounces, a capacitive one does not.
TOUCH_MIN_PRESS_MS: "20"
# Page swipe starts in this band along the left or right edge (about 5 mm) and needs
# this much travel inward (about 6 mm); a swipe from the middle of the screen does nothing.
EDGE_SWIPE_BAND_PX: "32"
EDGE_SWIPE_TRAVEL_PX: "40"
DYNAMIC_TILES: "true"
DASHBOARD_INBOX_INTERNAL: "false"
DEVICE_NAME: "guition-new"
@@ -596,6 +600,7 @@ esphome:
then:
- lambda: |-
cyd::touch_guard.configure(${TOUCH_MOVE_LIMIT_PX}, ${TOUCH_MIN_PRESS_MS});
cyd::edge_swipe.configure(${DISPLAY_W}, ${DISPLAY_H}, ${EDGE_SWIPE_BAND_PX}, ${EDGE_SWIPE_TRAVEL_PX});
id(last_touch_ms) = millis();
id(display_dimmed) = false;
lv_obj_set_style_pad_row(id(vacuum_actions), 4, 0);
@@ -634,17 +639,6 @@ esphome:
lv_obj_add_flag(id(vacuum_overlay), LV_OBJ_FLAG_HIDDEN);
};
runtime_tiles::layout_changed = []() { id(show_tile_page).execute(); };
lv_obj_add_event_cb(id(home_page)->obj, [](lv_event_t *event) {
if(!runtime_tiles::enabled || !runtime_tiles::swipe_pages || id(display_dimmed) || id(calibration_active) ||
!id(active_entity).empty() || (runtime_tiles::detail_root && !lv_obj_has_flag(runtime_tiles::detail_root,LV_OBJ_FLAG_HIDDEN)))return;
auto *input=lv_indev_active();if(!input)return;
auto direction=lv_indev_get_gesture_dir(input);
if(direction!=LV_DIR_LEFT && direction!=LV_DIR_RIGHT)return;
cyd::touch_guard.consume();
lv_indev_wait_release(input);
id(tile_page)+=direction==LV_DIR_LEFT?1:-1;
id(show_tile_page).execute();
}, LV_EVENT_GESTURE, nullptr);
runtime_tiles::refresh = []() { if (!id(ui_refresh).is_running()) id(ui_refresh).execute(); };
runtime_tiles::bind(0, id(tile1), id(t1_title), id(t1_value), id(tile1_icon_circle), id(tile1_icon_lbl));
runtime_tiles::bind(1, id(tile2), id(t2_title), id(t2_value), id(tile2_icon_circle), id(tile2_icon_lbl));
@@ -1807,6 +1801,7 @@ touchscreen:
id(last_touch_ms) = millis();
id(touch_down) = true;
cyd::touch_guard.begin(millis(), touch.x, touch.y, touch.id);
cyd::edge_swipe.begin(touch.x, touch.y, static_cast<int>(id(screen_lvgl).get_rotation()));
ESP_LOGI("touch", "GT911 press x=%u y=%u test=%d", touch.x, touch.y, id(calibration_active));
on_update:
- lambda: |-
@@ -1814,6 +1809,17 @@ touchscreen:
// Only the contact that started the touch counts; the guard ignores other ids.
for (const auto &point : touches) {
cyd::touch_guard.update(point.x, point.y, point.id);
if (point.id != cyd::touch_guard.contact()) continue;
// Swiping in from a side edge flips the page ("Vegen tussen pagina's"); the tap
// under the finger is consumed and LVGL waits for the release.
const int step = cyd::edge_swipe.update(point.x, point.y);
if (!step || !runtime_tiles::enabled || !runtime_tiles::swipe_pages || id(display_dimmed) || id(calibration_active) ||
!id(active_entity).empty() || (runtime_tiles::detail_root && !lv_obj_has_flag(runtime_tiles::detail_root, LV_OBJ_FLAG_HIDDEN))) continue;
cyd::touch_guard.consume();
for (auto *indev = lv_indev_get_next(nullptr); indev; indev = lv_indev_get_next(indev)) lv_indev_wait_release(indev);
ESP_LOGI("touch", "randveeg: pagina %d -> %d", id(tile_page), id(tile_page) + step);
id(tile_page) += step;
id(show_tile_page).execute();
}
on_release:
- lambda: |-
+6
View File
@@ -1,3 +1,9 @@
## 0.2.29 (firmware 0.2.24)
- **Vegen vanaf de zijrand op de Guition**: met **Vegen tussen pagina's** aan wissel je van pagina door vanaf de linker- of rechterrand naar binnen te vegen, zoals terug-vegen op een telefoon. Vanaf rechts naar links is volgende, vanaf links naar rechts vorige. Langzaam of snel maakt niet uit: de veeg begint in een band van 32 px (circa 5 mm) langs de rand en telt na 40 px (circa 6 mm) naar binnen, duidelijk meer zijwaarts dan omhoog of omlaag (`EDGE_SWIPE_BAND_PX`, `EDGE_SWIPE_TRAVEL_PX`). Werkt in alle vier de rotaties. Een veeg die midden op het scherm begint doet niets meer, zodat tikken en slepen op tegels nooit per ongeluk van pagina wisselen; de tegel onder een randveeg krijgt geen tik. Het log meldt `randveeg: pagina 0 -> 1`.
- De CYD houdt de bestaande snelle veeg over het scherm (LVGL-gesture, firmware 0.2.7+); daar verandert niets, de firmware krijgt alleen het nieuwe versienummer.
- Firmware 0.2.24 voor beide borden; de app biedt de update aan. In de app is alleen de omschrijving van de instelling aangepast.
## 0.2.28 (firmware 0.2.23)
- **Tikken die niet doorkwamen**: de firmware gooide een tik weg zodra de vinger tijdens het drukken meer dan 18 px (nog geen 3 mm) van het eerste contactpunt afweek, ver onder LVGL's eigen veegdrempel en ook met "Vegen tussen pagina's" uit. Een vinger die platter wordt of iets rolt haalde dat al. Nu geldt per bord een grens van ongeveer één centimeter (`TOUCH_MOVE_LIMIT_PX`: Guition 67 px, CYD 56 px), gemeten als afstand tot een referentiepunt dat over de eerste vier metingen (circa 60 ms) settelt in plaats van tot het allereerste punt. Eindigt de vinger op een andere tegel, dan vangt LVGL dat nog steeds op (press lost).
+1 -1
View File
@@ -16,7 +16,7 @@ WEEKDAYS = ['ma', 'di', 'wo', 'do', 'vr', 'za', 'zo']
REPO = 'https://github.com/MaxGramser/homeassistant_espscreen'
REFS = {'cyd': 'main', 'guition': 'main'}
# Firmware shipped with this app release; screens below it get an update offer.
FIRMWARE_VERSION = '0.2.23'
FIRMWARE_VERSION = '0.2.24'
ATTRS = frozenset('brightness percentage current_position current_temperature temperature current_humidity min_temp max_temp target_temp_step supported_color_modes hvac_modes hvac_action hs_color color_temp_kelvin min_color_temp_kelvin max_color_temp_kelvin fan_speed_list unit_of_measurement battery_level fan_speed volume_level is_volume_muted media_title options min max step temperature_unit supported_features device_class next_rising next_setting finishes_at duration remaining humidity wind_speed wind_speed_unit apparent_temperature fan_modes swing_modes fan_mode swing_mode'.split())
# Attributes whose boolean value the screen needs; every other bool stays behind.
BOOL_ATTRS = frozenset(['is_volume_muted'])
+1 -1
View File
@@ -89,7 +89,7 @@ const settingDefinitions = [
["clock_24h", "24-uursklok (uit = 12 uur)", "check", true],
["home_on_standby", "Na standby terug naar pagina 1", "check", false],
["rotation", "Scherm draaien (met de klok mee)", "rotation", 0],
["swipe_pages", "Vegen tussen pagina’s (firmware 0.2.7+)", "check", false],
["swipe_pages", "Vegen tussen pagina’s (Guition: vanaf de zijrand, firmware 0.2.24+; CYD: snelle veeg, 0.2.7+)", "check", false],
];
function renderSettings() {
const values = {
+1 -1
View File
@@ -1,5 +1,5 @@
name: ESP Screen Manager
version: "0.2.28"
version: "0.2.29"
slug: esp_screen_manager
description: Kies tegels voor je CYD en Guition touchscreen, zonder opnieuw flashen.
url: https://github.com/MaxGramser/homeassistant_espscreen
+26
View File
@@ -86,6 +86,32 @@ int main() {
resistive.update(50, 180); // 70 px past the settled point: a swipe
assert(!resistive.accept(1100, 1));
assert(!resistive.accept_repeat(1100, 1));
// Edge swipe (0.2.24): only a touch that starts in the side band flips a page, from the
// right edge leftwards "next", from the left edge rightwards "previous", once per touch,
// slow or fast, and never diagonally or from the middle. Rotation follows ESPHome's mapping.
cyd::EdgeSwipe edge;
edge.configure(480, 480, 32, 40);
edge.begin(6, 240);
assert(edge.armed());
assert(edge.update(30, 242) == 0); // not far enough yet
assert(edge.update(48, 245) == -1); // 42 px inward from the left edge: previous page
assert(edge.update(90, 245) == 0); // once per touch
assert(!edge.armed());
edge.begin(474, 100);
assert(edge.update(430, 104) == 1); // from the right edge: next page
edge.begin(240, 240);
assert(!edge.armed());
assert(edge.update(300, 240) == 0); // started in the middle: never a page swipe
edge.begin(6, 240);
assert(edge.update(50, 300) == 0); // diagonal: 44 px sideways against 60 down
assert(edge.update(-10, 240) == 0); // moving outward: nothing
assert(edge.update(60, 244) == -1); // straightened out later: still counts
edge.begin(240, 6, 90);
assert(edge.update(240, 60) == -1); // rotated 90: the user's left edge is the panel's top
edge.begin(240, 6, 270);
assert(edge.update(240, 60) == 1); // rotated 270: that same edge is the user's right
edge.begin(6, 240, 180);
assert(edge.update(60, 240) == 1); // upside down: the panel's left is the user's right
cyd::TouchGuard rollover;
rollover.begin(std::numeric_limits<uint32_t>::max() - 30);
assert(rollover.accept(50, 1)); // millis wraps after 49 days