Close cards through one path so the page swipe is never left blocked (0.2.32)

Read live on a Guition: every edge swipe was recognised but refused by the
"card open" check, because the light, climate and vacuum cards cleared
their overlay on close (X, tap beside the card, wake from standby) without
forgetting active_entity. One opened card blocked swiping until the next
layout change; the old whole-screen gesture and the CYD had the same gate.
Both boards now close cards through one script, close_cards, which hides
every overlay and clears that state; wake_display uses it too.

The Guition edge swipe now follows LVGL's own pointer events
(lv_indev_add_event_cb: PRESSED begins, PRESSING flips the page and calls
lv_indev_wait_release from inside LVGL) instead of ESPHome's touchscreen
on_update, so the point arrives already rotated. The direction rule is 45
degrees instead of twice as horizontal, and the log now says why a swipe
was refused or how far an unfinished one got. Firmware 0.2.27 for both
boards; the app only moves its target version.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Max Gramser
2026-09-14 13:09:53 +02:00
parent 95cdfa30cf
commit b9e1dfbd95
11 changed files with 248 additions and 109 deletions
+16 -21
View File
@@ -128,46 +128,41 @@ class TouchGuard {
}; };
inline TouchGuard touch_guard; inline TouchGuard touch_guard;
// Page navigation by swiping in from a side edge (Guition, firmware 0.2.24+). A touch that // Page navigation by swiping in from a side edge (Guition, firmware 0.2.24+). Fed from
// starts within `band` pixels of the left or right edge of the screen as the user sees it // LVGL's own pointer events (PRESSED/PRESSING), so the coordinates are already rotated the
// and travels `travel` pixels inward, clearly more sideways than up or down, flips one // way the user sees the screen. A touch that starts within `band` pixels of the left or
// right edge and travels `travel` pixels inward, more sideways than up or down, flips one
// page: from the right edge leftwards is "next", from the left edge rightwards "previous". // 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 // No speed requirement; a touch that starts in the middle never counts, so tapping and
// dragging on tiles cannot change the page by accident. // dragging on tiles cannot change the page by accident.
class EdgeSwipe { class EdgeSwipe {
public: public:
void configure(int width, int height, int band, int travel) { void configure(int band, int travel) { band_ = band; travel_ = travel; }
width_ = width; height_ = height; band_ = band; travel_ = travel; // `width` is the screen width as the user sees it (LVGL's horizontal resolution).
} void begin(int x, int y, int width) {
// 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; 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 from_ = x < band_ ? 1 : x >= width - band_ ? -1 : 0; // 1: left edge, -1: right edge
done_ = false; done_ = false;
inward_ = sideways_ = 0;
} }
bool armed() const { return from_ != 0 && !done_; } bool armed() const { return from_ != 0 && !done_; }
// +1 next page, -1 previous page, 0 nothing; fires at most once per touch. // +1 next page, -1 previous page, 0 nothing; fires at most once per touch.
int update(int x, int y) { int update(int x, int y) {
if (!armed()) return 0; if (!armed()) return 0;
rotate(x, y);
const int dx = x - start_x_, dy = y - start_y_; const int dx = x - start_x_, dy = y - start_y_;
const int inward = from_ == 1 ? dx : -dx; const int inward = from_ == 1 ? dx : -dx;
if (inward < travel_ || std::abs(dx) < 2 * std::abs(dy)) return 0; inward_ = std::max(inward_, inward);
sideways_ = std::max(sideways_, std::abs(dy));
if (inward < travel_ || std::abs(dx) < std::abs(dy)) return 0;
done_ = true; done_ = true;
return from_ == 1 ? -1 : 1; return from_ == 1 ? -1 : 1;
} }
// How far the finger got, for the log when an edge touch ends without a page flip.
int inward() const { return inward_; }
int sideways() const { return sideways_; }
private: private:
void rotate(int &x, int &y) const { int band_{32}, travel_{40};
if (rotation_ == 90) { const int tmp = y; y = width_ - x - 1; x = tmp; } int start_x_{0}, start_y_{0}, from_{0}, inward_{0}, sideways_{0};
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}; bool done_{true};
}; };
inline EdgeSwipe edge_swipe; inline EdgeSwipe edge_swipe;
+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 De substituties `MDI_GLYPH_*` zijn vervallen: `TILEn_ICON` in handmatige
profielen moet uit de set komen. Geen gewijzigde preferences of sleutels. profielen moet uit de set komen. Geen gewijzigde preferences of sleutels.
### Compatibiliteit 0.2.32 / firmware 0.2.27
Alleen firmware. `cyd::EdgeSwipe` werkt op LVGL-pointercoördinaten: `configure(band, travel)`,
`begin(x, y, width)` met `lv_display_get_horizontal_resolution()`, `update()` met de 45°-regel,
plus `inward()`/`sideways()` voor het log. De Guition registreert één `lv_indev_add_event_cb`
(PRESSED/PRESSING/RELEASED) in `on_boot`; de touchscreen-triggers voeden alleen nog de
`TouchGuard`. Beide bordprofielen krijgen het script `close_cards` (roept
`runtime_tiles::dismiss()` aan, anders overlays verbergen en `active_entity` wissen); de
kruisjes en achtergrondtikken van de kaarten en `wake_display` gebruiken het. Protocol, opslag,
preferences en sleutels ongewijzigd.
### Compatibiliteit 0.2.31 / firmware 0.2.26 ### Compatibiliteit 0.2.31 / firmware 0.2.26
Opslagversie en tegelprotocol blijven 1. `tiles[].slot` is additief: de absolute plek Opslagversie en tegelprotocol blijven 1. `tiles[].slot` is additief: de absolute plek
+48
View File
@@ -0,0 +1,48 @@
# Testresultaten app 0.2.32 / firmware 0.2.27 (2026-09-14)
Alleen firmware: de randveeg die op een echte Guition (Studio 1, firmware 0.2.24) nooit
pakte, plus de veeg op LVGL's eigen pointer-events. De app verandert alleen de doelversie.
## Live diagnose op Studio 1 (ESPHome-log via de native API)
- Elke veeg begon netjes in de band (`GT911 press x=0…13` links, `x=470…478` rechts), maar er
volgde nooit `randveeg`. Instelling stond aan (add-on: `swipe_pages: true`, 10 tegels,
rotatie 0), `ui_state` meldde niet gedimd, geen kalibratie, geen runtime-kaart.
- Na `preview_runtime_card -1` (roept `runtime_tiles::dismiss()` aan) werkte elke veeg
meteen: `randveeg: pagina 0 -> 1`. Oorzaak: de kruisjes en achtergrondtikken van de
licht-, klimaat- en stofzuigerkaart en het wakker worden uit standby verbergen de kaart
zonder `active_entity` te wissen; de veeg-guard leest die en meldde stil "kaart open".
Dezelfde gate zat in de oude LVGL-gesture en in het CYD-profiel.
- Daarna: ongeveer één op de twee eerste vegen na een paginawissel vuurde niet, de
tweede wel (bijvoorbeeld 12:57:28 en :29 vanaf rechts zonder wissel, :29 wel). De
0.2.24-firmware logde daarvoor niets; 0.2.27 meldt nu `randveeg niet gevuurd: N px naar
binnen, M px verticaal` en de richtingseis is 45° in plaats van "twee keer zo horizontaal".
- Eén tik werd terecht geweigerd: `tik op script.studio_1_startup_sequence genegeerd:
verplaatst 73 px (grens 67)`; de grens op de Guition is een open vraag aan Max.
## Wat veranderde
- `cyd::EdgeSwipe` op LVGL-pointercoördinaten (`begin(x, y, width)` met de horizontale
resolutie van LVGL, dus rotatie is al verwerkt), 45°-regel, `inward()`/`sideways()`.
- Guition: één `lv_indev_add_event_cb` in `on_boot` (PRESSED → begin, PRESSING → update en
paginawissel met `lv_indev_wait_release`, RELEASED/PRESS_LOST → logregel als de veeg
niet vuurde); geblokkeerde vegen loggen de reden. De touchscreen-triggers voeden alleen
nog de tap-guard.
- Beide borden: script `close_cards` (via `runtime_tiles::dismiss()`, anders overlays
verbergen en `active_entity` wissen) voor de zes sluitknoppen en `wake_display`.
## Geautomatiseerd
- C++ (clang++ -std=c++17 -Wall -Wextra -Werror -I.): 11/11 PASS; `test_cyd_ui` dekt de
nieuwe `EdgeSwipe`-API, de 45°-regel, de logwaarden en een smallere (gedraaide) breedte.
- Python: 91 tests OK (`FIRMWARE_VERSION` 0.2.27 in app, profielen en pakketten).
`generate_packages.py --check` groen.
- ESPHome 2026.6.2 compile (verse worktree, geen cache): `easy-guition-device.yaml` SUCCESS in
112 s (RAM 32,0%, flash 18,3%), `easy-cyd-device.yaml` SUCCESS in 86 s (RAM 31,0%, flash
76,1%); geen nieuwe waarschuwingen.
## Nog te controleren op Studio 1
- Werkt de randveeg direct na een update zonder eerst een kaart te openen en te sluiten, en
pakt de eerste veeg na een paginawissel nu ook; het log (`touch`) laat het zien.
- Na het openen en sluiten van een lichtkaart (kruisje, tik ernaast, standby) blijft vegen werken.
+53 -24
View File
@@ -1,7 +1,7 @@
# Guition ESP32-S3-4848S040 / 4-inch EU wallbox, 480x480 IPS + GT911. # 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. # Use guition-device.yaml; see docs/GUITION.md. CYD hardware is a separate profile.
substitutions: substitutions:
SCREEN_FIRMWARE_VERSION: "0.2.26" SCREEN_FIRMWARE_VERSION: "0.2.27"
# A tap survives a finger that drifts up to this far (480 px over 72 mm: 1 cm); further is a swipe. # 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" TOUCH_MOVE_LIMIT_PX: "67"
# 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.
@@ -596,7 +596,38 @@ esphome:
then: then:
- lambda: |- - lambda: |-
cyd::touch_guard.configure(${TOUCH_MOVE_LIMIT_PX}, ${TOUCH_MIN_PRESS_MS}); 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}); cyd::edge_swipe.configure(${EDGE_SWIPE_BAND_PX}, ${EDGE_SWIPE_TRAVEL_PX});
// Edge swipe on LVGL's own pointer events: the point is already rotated, the callback
// runs inside LVGL, and lv_indev_wait_release keeps the tile under the finger quiet.
if (auto *pointer = lv_indev_get_next(nullptr)) {
lv_indev_add_event_cb(pointer, [](lv_event_t *e) {
auto *indev = static_cast<lv_indev_t *>(lv_event_get_current_target(e));
const auto code = lv_event_get_code(e);
lv_point_t p;
lv_indev_get_point(indev, &p);
if (code == LV_EVENT_PRESSED) {
cyd::edge_swipe.begin(p.x, p.y, lv_display_get_horizontal_resolution(lv_display_get_default()));
return;
}
if (code == LV_EVENT_RELEASED || code == LV_EVENT_PRESS_LOST) {
if (cyd::edge_swipe.armed() && cyd::edge_swipe.inward() > 0)
ESP_LOGI("touch", "randveeg niet gevuurd: %d px naar binnen, %d px verticaal", cyd::edge_swipe.inward(), cyd::edge_swipe.sideways());
return;
}
if (code != LV_EVENT_PRESSING) return;
const int step = cyd::edge_swipe.update(p.x, p.y);
if (!step) return;
const char *blocked = !runtime_tiles::enabled ? "geen runtime-tegels" : !runtime_tiles::swipe_pages ? "instelling uit"
: id(display_dimmed) ? "scherm gedimd" : id(calibration_active) ? "kalibratie" : !id(active_entity).empty() ? "kaart open"
: (runtime_tiles::detail_root && !lv_obj_has_flag(runtime_tiles::detail_root, LV_OBJ_FLAG_HIDDEN)) ? "detailkaart open" : nullptr;
if (blocked) { ESP_LOGI("touch", "randveeg genegeerd: %s", blocked); return; }
cyd::touch_guard.consume();
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();
}, LV_EVENT_ALL, nullptr);
}
id(last_touch_ms) = millis(); id(last_touch_ms) = millis();
id(display_dimmed) = false; id(display_dimmed) = false;
lv_obj_set_style_pad_row(id(vacuum_actions), 4, 0); lv_obj_set_style_pad_row(id(vacuum_actions), 4, 0);
@@ -1870,7 +1901,6 @@ touchscreen:
id(last_touch_ms) = millis(); id(last_touch_ms) = millis();
id(touch_down) = true; id(touch_down) = true;
cyd::touch_guard.begin(millis(), touch.x, touch.y, touch.id); 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)); ESP_LOGI("touch", "GT911 press x=%u y=%u test=%d", touch.x, touch.y, id(calibration_active));
on_update: on_update:
- lambda: |- - lambda: |-
@@ -1878,17 +1908,6 @@ touchscreen:
// Only the contact that started the touch counts; the guard ignores other ids. // Only the contact that started the touch counts; the guard ignores other ids.
for (const auto &point : touches) { for (const auto &point : touches) {
cyd::touch_guard.update(point.x, point.y, point.id); 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: on_release:
- lambda: |- - lambda: |-
@@ -3472,7 +3491,7 @@ lvgl:
scrollable: false scrollable: false
clickable: true clickable: true
on_click: on_click:
- lvgl.widget.hide: brightness_overlay - script.execute: close_cards
- obj: - obj:
id: brightness_overlay_dim id: brightness_overlay_dim
@@ -3813,7 +3832,7 @@ lvgl:
clickable: true clickable: true
scrollable: false scrollable: false
on_click: on_click:
- lvgl.widget.hide: color_detail_overlay - script.execute: close_cards
widgets: widgets:
- label: - label:
align: CENTER align: CENTER
@@ -3846,7 +3865,7 @@ lvgl:
clickable: true clickable: true
scrollable: false scrollable: false
on_click: on_click:
- lvgl.widget.hide: color_detail_overlay - script.execute: close_cards
widgets: widgets:
- label: - label:
align: CENTER align: CENTER
@@ -3912,7 +3931,7 @@ lvgl:
clickable: true clickable: true
scrollable: false scrollable: false
on_click: on_click:
- lvgl.widget.hide: climate_detail_overlay - script.execute: close_cards
widgets: widgets:
- label: - label:
align: CENTER align: CENTER
@@ -4769,7 +4788,7 @@ lvgl:
clickable: true clickable: true
scrollable: false scrollable: false
on_click: on_click:
- lvgl.widget.hide: vacuum_overlay - script.execute: close_cards
widgets: widgets:
- label: - label:
align: CENTER align: CENTER
@@ -4792,7 +4811,7 @@ lvgl:
clickable: true clickable: true
scrollable: false scrollable: false
on_click: on_click:
- lvgl.widget.hide: vacuum_overlay - script.execute: close_cards
widgets: widgets:
- label: - label:
align: CENTER align: CENTER
@@ -5040,6 +5059,19 @@ script:
else lv_obj_add_flag(id(lbl_time), LV_OBJ_FLAG_HIDDEN); else lv_obj_add_flag(id(lbl_time), LV_OBJ_FLAG_HIDDEN);
if (!id(ui_refresh).is_running()) id(ui_refresh).execute(); if (!id(ui_refresh).is_running()) id(ui_refresh).execute();
- id: close_cards
then:
- lambda: |-
// Runtime cards and the legacy overlays close through one path; the swipe guard
// reads active_entity, so a hidden card must never leave it behind.
if (runtime_tiles::dismiss) { runtime_tiles::dismiss(); return; }
id(active_entity).clear();
- lvgl.widget.hide: brightness_overlay
- lvgl.widget.hide: color_detail_overlay
- lvgl.widget.hide: climate_detail_overlay
- lvgl.widget.hide: climate_mode_overlay
- lvgl.widget.hide: vacuum_overlay
- id: wake_display - id: wake_display
mode: restart mode: restart
then: then:
@@ -5050,10 +5082,7 @@ script:
// als je het scherm aanraakt en op licht staat te wachten. // als je het scherm aanraakt en op licht staat te wachten.
id(set_backlight).execute(screen_settings::current.brightness, 80); id(set_backlight).execute(screen_settings::current.brightness, 80);
- lvgl.widget.hide: dim_wake_overlay - lvgl.widget.hide: dim_wake_overlay
- lvgl.widget.hide: color_detail_overlay - script.execute: close_cards
- lvgl.widget.hide: climate_detail_overlay
- lvgl.widget.hide: climate_mode_overlay
- lvgl.widget.hide: vacuum_overlay
# One owner for the backlight level: the LEDC fader steps the duty itself, so # One owner for the backlight level: the LEDC fader steps the duty itself, so
# LVGL redraws cannot make the transition stutter. A new call interrupts the # LVGL redraws cannot make the transition stutter. A new call interrupts the
+21 -11
View File
@@ -58,7 +58,7 @@
############################################################ ############################################################
substitutions: substitutions:
SCREEN_FIRMWARE_VERSION: "0.2.26" SCREEN_FIRMWARE_VERSION: "0.2.27"
# 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.
@@ -3608,7 +3608,7 @@ lvgl:
scrollable: false scrollable: false
clickable: true clickable: true
on_click: on_click:
- lvgl.widget.hide: brightness_overlay - script.execute: close_cards
- obj: - obj:
id: brightness_overlay_dim id: brightness_overlay_dim
@@ -3949,7 +3949,7 @@ lvgl:
clickable: true clickable: true
scrollable: false scrollable: false
on_click: on_click:
- lvgl.widget.hide: color_detail_overlay - script.execute: close_cards
widgets: widgets:
- label: - label:
align: CENTER align: CENTER
@@ -3982,7 +3982,7 @@ lvgl:
clickable: true clickable: true
scrollable: false scrollable: false
on_click: on_click:
- lvgl.widget.hide: color_detail_overlay - script.execute: close_cards
widgets: widgets:
- label: - label:
align: CENTER align: CENTER
@@ -4048,7 +4048,7 @@ lvgl:
clickable: true clickable: true
scrollable: false scrollable: false
on_click: on_click:
- lvgl.widget.hide: climate_detail_overlay - script.execute: close_cards
widgets: widgets:
- label: - label:
align: CENTER align: CENTER
@@ -4905,7 +4905,7 @@ lvgl:
clickable: true clickable: true
scrollable: false scrollable: false
on_click: on_click:
- lvgl.widget.hide: vacuum_overlay - script.execute: close_cards
widgets: widgets:
- label: - label:
align: CENTER align: CENTER
@@ -4928,7 +4928,7 @@ lvgl:
clickable: true clickable: true
scrollable: false scrollable: false
on_click: on_click:
- lvgl.widget.hide: vacuum_overlay - script.execute: close_cards
widgets: widgets:
- label: - label:
align: CENTER align: CENTER
@@ -5156,6 +5156,19 @@ script:
else lv_obj_add_flag(id(lbl_time), LV_OBJ_FLAG_HIDDEN); else lv_obj_add_flag(id(lbl_time), LV_OBJ_FLAG_HIDDEN);
if (!id(ui_refresh).is_running()) id(ui_refresh).execute(); if (!id(ui_refresh).is_running()) id(ui_refresh).execute();
- id: close_cards
then:
- lambda: |-
// Runtime cards and the legacy overlays close through one path; the swipe guard
// reads active_entity, so a hidden card must never leave it behind.
if (runtime_tiles::dismiss) { runtime_tiles::dismiss(); return; }
id(active_entity).clear();
- lvgl.widget.hide: brightness_overlay
- lvgl.widget.hide: color_detail_overlay
- lvgl.widget.hide: climate_detail_overlay
- lvgl.widget.hide: climate_mode_overlay
- lvgl.widget.hide: vacuum_overlay
- id: wake_display - id: wake_display
mode: restart mode: restart
then: then:
@@ -5169,10 +5182,7 @@ script:
transition_length: 80ms transition_length: 80ms
brightness: !lambda "return screen_settings::current.brightness / 100.0f;" brightness: !lambda "return screen_settings::current.brightness / 100.0f;"
- lvgl.widget.hide: dim_wake_overlay - lvgl.widget.hide: dim_wake_overlay
- lvgl.widget.hide: color_detail_overlay - script.execute: close_cards
- lvgl.widget.hide: climate_detail_overlay
- lvgl.widget.hide: climate_mode_overlay
- lvgl.widget.hide: vacuum_overlay
- id: dim_display - id: dim_display
mode: restart mode: restart
+21 -11
View File
@@ -59,7 +59,7 @@
############################################################ ############################################################
substitutions: substitutions:
SCREEN_FIRMWARE_VERSION: "0.2.26" SCREEN_FIRMWARE_VERSION: "0.2.27"
# 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.
@@ -3539,7 +3539,7 @@ lvgl:
scrollable: false scrollable: false
clickable: true clickable: true
on_click: on_click:
- lvgl.widget.hide: brightness_overlay - script.execute: close_cards
- obj: - obj:
id: brightness_overlay_dim id: brightness_overlay_dim
@@ -3880,7 +3880,7 @@ lvgl:
clickable: true clickable: true
scrollable: false scrollable: false
on_click: on_click:
- lvgl.widget.hide: color_detail_overlay - script.execute: close_cards
widgets: widgets:
- label: - label:
align: CENTER align: CENTER
@@ -3913,7 +3913,7 @@ lvgl:
clickable: true clickable: true
scrollable: false scrollable: false
on_click: on_click:
- lvgl.widget.hide: color_detail_overlay - script.execute: close_cards
widgets: widgets:
- label: - label:
align: CENTER align: CENTER
@@ -3979,7 +3979,7 @@ lvgl:
clickable: true clickable: true
scrollable: false scrollable: false
on_click: on_click:
- lvgl.widget.hide: climate_detail_overlay - script.execute: close_cards
widgets: widgets:
- label: - label:
align: CENTER align: CENTER
@@ -4836,7 +4836,7 @@ lvgl:
clickable: true clickable: true
scrollable: false scrollable: false
on_click: on_click:
- lvgl.widget.hide: vacuum_overlay - script.execute: close_cards
widgets: widgets:
- label: - label:
align: CENTER align: CENTER
@@ -4859,7 +4859,7 @@ lvgl:
clickable: true clickable: true
scrollable: false scrollable: false
on_click: on_click:
- lvgl.widget.hide: vacuum_overlay - script.execute: close_cards
widgets: widgets:
- label: - label:
align: CENTER align: CENTER
@@ -5087,6 +5087,19 @@ script:
else lv_obj_add_flag(id(lbl_time), LV_OBJ_FLAG_HIDDEN); else lv_obj_add_flag(id(lbl_time), LV_OBJ_FLAG_HIDDEN);
if (!id(ui_refresh).is_running()) id(ui_refresh).execute(); if (!id(ui_refresh).is_running()) id(ui_refresh).execute();
- id: close_cards
then:
- lambda: |-
// Runtime cards and the legacy overlays close through one path; the swipe guard
// reads active_entity, so a hidden card must never leave it behind.
if (runtime_tiles::dismiss) { runtime_tiles::dismiss(); return; }
id(active_entity).clear();
- lvgl.widget.hide: brightness_overlay
- lvgl.widget.hide: color_detail_overlay
- lvgl.widget.hide: climate_detail_overlay
- lvgl.widget.hide: climate_mode_overlay
- lvgl.widget.hide: vacuum_overlay
- id: wake_display - id: wake_display
mode: restart mode: restart
then: then:
@@ -5100,10 +5113,7 @@ script:
transition_length: 80ms transition_length: 80ms
brightness: !lambda "return screen_settings::current.brightness / 100.0f;" brightness: !lambda "return screen_settings::current.brightness / 100.0f;"
- lvgl.widget.hide: dim_wake_overlay - lvgl.widget.hide: dim_wake_overlay
- lvgl.widget.hide: color_detail_overlay - script.execute: close_cards
- lvgl.widget.hide: climate_detail_overlay
- lvgl.widget.hide: climate_mode_overlay
- lvgl.widget.hide: vacuum_overlay
- id: dim_display - id: dim_display
mode: restart mode: restart
+53 -24
View File
@@ -2,7 +2,7 @@
# Guition ESP32-S3-4848S040 / 4-inch EU wallbox, 480x480 IPS + GT911. # 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. # Use guition-device.yaml; see docs/GUITION.md. CYD hardware is a separate profile.
substitutions: substitutions:
SCREEN_FIRMWARE_VERSION: "0.2.26" SCREEN_FIRMWARE_VERSION: "0.2.27"
# A tap survives a finger that drifts up to this far (480 px over 72 mm: 1 cm); further is a swipe. # 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" TOUCH_MOVE_LIMIT_PX: "67"
# 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.
@@ -600,7 +600,38 @@ esphome:
then: then:
- lambda: |- - lambda: |-
cyd::touch_guard.configure(${TOUCH_MOVE_LIMIT_PX}, ${TOUCH_MIN_PRESS_MS}); 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}); cyd::edge_swipe.configure(${EDGE_SWIPE_BAND_PX}, ${EDGE_SWIPE_TRAVEL_PX});
// Edge swipe on LVGL's own pointer events: the point is already rotated, the callback
// runs inside LVGL, and lv_indev_wait_release keeps the tile under the finger quiet.
if (auto *pointer = lv_indev_get_next(nullptr)) {
lv_indev_add_event_cb(pointer, [](lv_event_t *e) {
auto *indev = static_cast<lv_indev_t *>(lv_event_get_current_target(e));
const auto code = lv_event_get_code(e);
lv_point_t p;
lv_indev_get_point(indev, &p);
if (code == LV_EVENT_PRESSED) {
cyd::edge_swipe.begin(p.x, p.y, lv_display_get_horizontal_resolution(lv_display_get_default()));
return;
}
if (code == LV_EVENT_RELEASED || code == LV_EVENT_PRESS_LOST) {
if (cyd::edge_swipe.armed() && cyd::edge_swipe.inward() > 0)
ESP_LOGI("touch", "randveeg niet gevuurd: %d px naar binnen, %d px verticaal", cyd::edge_swipe.inward(), cyd::edge_swipe.sideways());
return;
}
if (code != LV_EVENT_PRESSING) return;
const int step = cyd::edge_swipe.update(p.x, p.y);
if (!step) return;
const char *blocked = !runtime_tiles::enabled ? "geen runtime-tegels" : !runtime_tiles::swipe_pages ? "instelling uit"
: id(display_dimmed) ? "scherm gedimd" : id(calibration_active) ? "kalibratie" : !id(active_entity).empty() ? "kaart open"
: (runtime_tiles::detail_root && !lv_obj_has_flag(runtime_tiles::detail_root, LV_OBJ_FLAG_HIDDEN)) ? "detailkaart open" : nullptr;
if (blocked) { ESP_LOGI("touch", "randveeg genegeerd: %s", blocked); return; }
cyd::touch_guard.consume();
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();
}, LV_EVENT_ALL, nullptr);
}
id(last_touch_ms) = millis(); id(last_touch_ms) = millis();
id(display_dimmed) = false; id(display_dimmed) = false;
lv_obj_set_style_pad_row(id(vacuum_actions), 4, 0); lv_obj_set_style_pad_row(id(vacuum_actions), 4, 0);
@@ -1801,7 +1832,6 @@ touchscreen:
id(last_touch_ms) = millis(); id(last_touch_ms) = millis();
id(touch_down) = true; id(touch_down) = true;
cyd::touch_guard.begin(millis(), touch.x, touch.y, touch.id); 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)); ESP_LOGI("touch", "GT911 press x=%u y=%u test=%d", touch.x, touch.y, id(calibration_active));
on_update: on_update:
- lambda: |- - lambda: |-
@@ -1809,17 +1839,6 @@ touchscreen:
// Only the contact that started the touch counts; the guard ignores other ids. // Only the contact that started the touch counts; the guard ignores other ids.
for (const auto &point : touches) { for (const auto &point : touches) {
cyd::touch_guard.update(point.x, point.y, point.id); 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: on_release:
- lambda: |- - lambda: |-
@@ -3403,7 +3422,7 @@ lvgl:
scrollable: false scrollable: false
clickable: true clickable: true
on_click: on_click:
- lvgl.widget.hide: brightness_overlay - script.execute: close_cards
- obj: - obj:
id: brightness_overlay_dim id: brightness_overlay_dim
@@ -3744,7 +3763,7 @@ lvgl:
clickable: true clickable: true
scrollable: false scrollable: false
on_click: on_click:
- lvgl.widget.hide: color_detail_overlay - script.execute: close_cards
widgets: widgets:
- label: - label:
align: CENTER align: CENTER
@@ -3777,7 +3796,7 @@ lvgl:
clickable: true clickable: true
scrollable: false scrollable: false
on_click: on_click:
- lvgl.widget.hide: color_detail_overlay - script.execute: close_cards
widgets: widgets:
- label: - label:
align: CENTER align: CENTER
@@ -3843,7 +3862,7 @@ lvgl:
clickable: true clickable: true
scrollable: false scrollable: false
on_click: on_click:
- lvgl.widget.hide: climate_detail_overlay - script.execute: close_cards
widgets: widgets:
- label: - label:
align: CENTER align: CENTER
@@ -4700,7 +4719,7 @@ lvgl:
clickable: true clickable: true
scrollable: false scrollable: false
on_click: on_click:
- lvgl.widget.hide: vacuum_overlay - script.execute: close_cards
widgets: widgets:
- label: - label:
align: CENTER align: CENTER
@@ -4723,7 +4742,7 @@ lvgl:
clickable: true clickable: true
scrollable: false scrollable: false
on_click: on_click:
- lvgl.widget.hide: vacuum_overlay - script.execute: close_cards
widgets: widgets:
- label: - label:
align: CENTER align: CENTER
@@ -4971,6 +4990,19 @@ script:
else lv_obj_add_flag(id(lbl_time), LV_OBJ_FLAG_HIDDEN); else lv_obj_add_flag(id(lbl_time), LV_OBJ_FLAG_HIDDEN);
if (!id(ui_refresh).is_running()) id(ui_refresh).execute(); if (!id(ui_refresh).is_running()) id(ui_refresh).execute();
- id: close_cards
then:
- lambda: |-
// Runtime cards and the legacy overlays close through one path; the swipe guard
// reads active_entity, so a hidden card must never leave it behind.
if (runtime_tiles::dismiss) { runtime_tiles::dismiss(); return; }
id(active_entity).clear();
- lvgl.widget.hide: brightness_overlay
- lvgl.widget.hide: color_detail_overlay
- lvgl.widget.hide: climate_detail_overlay
- lvgl.widget.hide: climate_mode_overlay
- lvgl.widget.hide: vacuum_overlay
- id: wake_display - id: wake_display
mode: restart mode: restart
then: then:
@@ -4981,10 +5013,7 @@ script:
// als je het scherm aanraakt en op licht staat te wachten. // als je het scherm aanraakt en op licht staat te wachten.
id(set_backlight).execute(screen_settings::current.brightness, 80); id(set_backlight).execute(screen_settings::current.brightness, 80);
- lvgl.widget.hide: dim_wake_overlay - lvgl.widget.hide: dim_wake_overlay
- lvgl.widget.hide: color_detail_overlay - script.execute: close_cards
- lvgl.widget.hide: climate_detail_overlay
- lvgl.widget.hide: climate_mode_overlay
- lvgl.widget.hide: vacuum_overlay
# One owner for the backlight level: the LEDC fader steps the duty itself, so # One owner for the backlight level: the LEDC fader steps the duty itself, so
# LVGL redraws cannot make the transition stutter. A new call interrupts the # LVGL redraws cannot make the transition stutter. A new call interrupts the
+7
View File
@@ -1,3 +1,10 @@
## 0.2.32 (firmware 0.2.27)
- **Randveeg die nooit pakte, opgelost.** Live meegelezen op een Guition: de veeg werd herkend maar geblokkeerd door de voorwaarde "kaart open". De lichtkaart, klimaatkaart en stofzuigerkaart wisten bij sluiten (kruisje, tik naast de kaart) en bij wakker worden uit standby de interne "actieve kaart" niet, dus na één keer een kaart openen bleef vegen stil geblokkeerd tot de indeling veranderde. Dat gold ook voor de oude veeg over het hele scherm en voor de CYD. Beide borden sluiten kaarten nu via één script (`close_cards`) dat alles verbergt én die toestand wist; wakker worden gebruikt hetzelfde pad.
- **Randveeg op LVGL's eigen manier.** De Guition volgt de veeg nu via de pointer-events van LVGL 9 (`lv_indev_add_event_cb`, PRESSED/PRESSING) in plaats van ESPHome's touchscreen-`on_update`: de coördinaten komen al gedraaid van LVGL (geen eigen rotatiemapping meer), en `lv_indev_wait_release` wordt vanuit LVGL zelf aangeroepen. De richtingseis is versoepeld van "twee keer zo horizontaal" naar "meer zijwaarts dan verticaal" (45°), zodat een schuine duimveeg vanaf rechts ook in één keer pakt.
- **Log zegt waarom.** Een herkende maar geblokkeerde randveeg meldt de reden (`randveeg genegeerd: kaart open`), en een randveeg die zonder paginawissel eindigt meldt hoe ver de vinger kwam (`randveeg niet gevuurd: 28 px naar binnen, 35 px verticaal`), naast het bestaande `randveeg: pagina 0 -> 1`.
- Firmware 0.2.27 voor beide borden; de app biedt de update aan. Alleen de doelversie verandert in de app.
## 0.2.31 (firmware 0.2.26) ## 0.2.31 (firmware 0.2.26)
- **Vrij slepen, met lege plekken**: de editor werkt met vaste plekken in plaats van een volgorde die steeds wordt aangeschoven. Elke tegel heeft zijn eigen plek (kolom, rij, pagina) en die verandert alleen als jij hem versleept. Lege plekken zijn gewoon lege plekken: naast een enkele tegel, midden op een pagina, waar je wilt; ze blijven staan zodat je kunt ordenen. Sleep een tegel op een lege plek en hij staat daar. Sleep hem op een andere tegel en die twee wisselen: de ander neemt de plek die vrijkwam, of anders de dichtstbijzijnde vrije plek. Alle overige tegels blijven waar ze staan. Tijdens het slepen laat het voorbeeld al zien waar alles terechtkomt; loslaten bevestigt precies dat, loslaten buiten het voorbeeld verandert niets. - **Vrij slepen, met lege plekken**: de editor werkt met vaste plekken in plaats van een volgorde die steeds wordt aangeschoven. Elke tegel heeft zijn eigen plek (kolom, rij, pagina) en die verandert alleen als jij hem versleept. Lege plekken zijn gewoon lege plekken: naast een enkele tegel, midden op een pagina, waar je wilt; ze blijven staan zodat je kunt ordenen. Sleep een tegel op een lege plek en hij staat daar. Sleep hem op een andere tegel en die twee wisselen: de ander neemt de plek die vrijkwam, of anders de dichtstbijzijnde vrije plek. Alle overige tegels blijven waar ze staan. Tijdens het slepen laat het voorbeeld al zien waar alles terechtkomt; loslaten bevestigt precies dat, loslaten buiten het voorbeeld verandert niets.
+1 -1
View File
@@ -16,7 +16,7 @@ WEEKDAYS = ['ma', 'di', 'wo', 'do', 'vr', 'za', 'zo']
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.26' FIRMWARE_VERSION = '0.2.27'
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()) 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. # Attributes whose boolean value the screen needs; every other bool stays behind.
BOOL_ATTRS = frozenset(['is_volume_muted']) BOOL_ATTRS = frozenset(['is_volume_muted'])
+1 -1
View File
@@ -1,5 +1,5 @@
name: ESP Screen Manager name: ESP Screen Manager
version: "0.2.31" version: "0.2.32"
slug: esp_screen_manager slug: esp_screen_manager
description: Kies tegels voor je CYD en Guition touchscreen, zonder opnieuw flashen. description: Kies tegels voor je CYD en Guition touchscreen, zonder opnieuw flashen.
url: https://github.com/MaxGramser/homeassistant_espscreen url: https://github.com/MaxGramser/homeassistant_espscreen
+16 -16
View File
@@ -86,32 +86,32 @@ int main() {
resistive.update(50, 180); // 70 px past the settled point: a swipe resistive.update(50, 180); // 70 px past the settled point: a swipe
assert(!resistive.accept(1100, 1)); assert(!resistive.accept(1100, 1));
assert(!resistive.accept_repeat(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 // Edge swipe (0.2.24, LVGL pointer coordinates since 0.2.27): only a touch that starts in
// right edge leftwards "next", from the left edge rightwards "previous", once per touch, // the side band flips a page, from the right edge leftwards "next", from the left edge
// slow or fast, and never diagonally or from the middle. Rotation follows ESPHome's mapping. // rightwards "previous", once per touch, slow or fast, more sideways than vertical, never
// from the middle. The width follows the rotated screen.
cyd::EdgeSwipe edge; cyd::EdgeSwipe edge;
edge.configure(480, 480, 32, 40); edge.configure(32, 40);
edge.begin(6, 240); edge.begin(6, 240, 480);
assert(edge.armed()); assert(edge.armed());
assert(edge.update(30, 242) == 0); // not far enough yet 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(48, 245) == -1); // 42 px inward from the left edge: previous page
assert(edge.update(90, 245) == 0); // once per touch assert(edge.update(90, 245) == 0); // once per touch
assert(!edge.armed()); assert(!edge.armed());
edge.begin(474, 100); edge.begin(474, 100, 480);
assert(edge.update(430, 104) == 1); // from the right edge: next page assert(edge.update(430, 104) == 1); // from the right edge: next page
edge.begin(240, 240); edge.begin(240, 240, 480);
assert(!edge.armed()); assert(!edge.armed());
assert(edge.update(300, 240) == 0); // started in the middle: never a page swipe assert(edge.update(300, 240) == 0); // started in the middle: never a page swipe
edge.begin(6, 240); edge.begin(6, 240, 480);
assert(edge.update(50, 300) == 0); // diagonal: 44 px sideways against 60 down assert(edge.update(50, 300) == 0); // steeper than 45 degrees: 44 px sideways against 60 down
assert(edge.update(-10, 240) == 0); // moving outward: nothing assert(edge.update(-10, 240) == 0); // moving outward: nothing
assert(edge.update(60, 244) == -1); // straightened out later: still counts assert(edge.update(60, 270) == -1); // 54 sideways against 30 down: a slanted thumb swipe counts
edge.begin(240, 6, 90); edge.begin(6, 240, 480);
assert(edge.update(240, 60) == -1); // rotated 90: the user's left edge is the panel's top edge.update(20, 250);
edge.begin(240, 6, 270); assert(edge.inward() == 14 && edge.sideways() == 10); // what the log reports for a swipe that ended early
assert(edge.update(240, 60) == 1); // rotated 270: that same edge is the user's right edge.begin(300, 240, 320);
edge.begin(6, 240, 180); assert(edge.update(250, 240) == 1); // a 320 px wide (rotated or smaller) screen: right band from 288
assert(edge.update(60, 240) == 1); // upside down: the panel's left is the user's right
cyd::TouchGuard rollover; cyd::TouchGuard rollover;
rollover.begin(std::numeric_limits<uint32_t>::max() - 30); rollover.begin(std::numeric_limits<uint32_t>::max() - 30);
assert(rollover.accept(50, 1)); // millis wraps after 49 days assert(rollover.accept(50, 1)); // millis wraps after 49 days