Add Guition 480x480 wallbox UI with GT911 and stable RGB rendering

This commit is contained in:
Max Gramser
2026-09-12 10:52:59 +02:00
parent 94546cf727
commit 60438ff44a
13 changed files with 6841 additions and 9 deletions
+4
View File
@@ -32,3 +32,7 @@ __pycache__/
/calibration*.yaml.bak-*
/calibration*.yaml.tmp-*
/.DS_Store
/guition-device.yaml
/diagnostics/*.png
+8
View File
@@ -1,3 +1,11 @@
## Guition-branch
De Guition 4848S040 heeft een apart profiel `guition-4848s040.yaml` met
480×480, ST7701S RGB en GT911. Lees docs/GUITION.md. Gebruik de eigen lokale
`guition-device.yaml`; neem geen CYD-layout of XPT2046-kalibratie over.
Controleer touch met tools/verify_gt911.py en houd de CYD-regressies groen.
Configureer geen wallbox-relais als onderdeel van displayondersteuning.
# Werkinstructies voor LLM's en developers
Dit project bedient een ESP32-2432S028 met ILI9341 + XPT2046 (320×240,
+3
View File
@@ -1,3 +1,6 @@
> **Guition ESP32-S3 4-inch / 480×480:** gebruik [de Guition-handleiding](docs/GUITION.md)
> en `guition-device.yaml`. De handleiding hieronder betreft het 2,8-inch CYD.
# CYD Home Assistant-bedieningsscherm
Van een nieuwe **ESP32-2432S028 met ILI9341 + XPT2046** naar een gekalibreerd
+51
View File
@@ -0,0 +1,51 @@
"""Save a Guition LVGL diagnostic snapshot as PNG through its encrypted API."""
import argparse
import asyncio
from pathlib import Path
import re
import struct
import zlib
import yaml
from aioesphomeapi import APIClient, LogLevel
def png(rows):
pixels=bytearray()
for y in range(240):
pixels.append(0)
for x in range(240):
p=int(rows[y][x*4:x*4+4],16)
pixels.extend((((p>>11)&31)*255//31,((p>>5)&63)*255//63,(p&31)*255//31))
def chunk(name,data):
return struct.pack('>I',len(data))+name+data+struct.pack('>I',zlib.crc32(name+data)&0xffffffff)
return b'\x89PNG\r\n\x1a\n'+chunk(b'IHDR',struct.pack('>2I5B',240,240,8,2,0,0,0))+chunk(b'IDAT',zlib.compress(pixels))+chunk(b'IEND',b'')
async def main():
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('--host',default='guition-wallbox.local')
p.add_argument('--name',default='guition-wallbox')
p.add_argument('--secrets',type=Path,default=Path(__file__).resolve().parents[1]/'secrets.yaml')
p.add_argument('--output',type=Path,required=True)
args=p.parse_args()
if args.output.exists():raise ValueError('Output bestaat al; kies een andere naam.')
client=APIClient(args.host,6053,noise_psk=yaml.safe_load(args.secrets.read_text())['api_encryption_key'],expected_name=args.name)
await client.connect(login=True)
try:
_,services=await client.list_entities_services()
done=asyncio.Event();rows={};errors=[]
def log(msg):
text=msg.message.decode(errors='replace') if isinstance(msg.message,bytes) else msg.message
m=re.search(r'ROW (\d{3}) ([0-9a-f]{960})',text)
if m:rows[int(m[1])]=m[2]
if 'IMAGE FAIL' in text:errors.append(text);done.set()
if 'IMAGE COMPLETE' in text:done.set()
client.subscribe_logs(log,log_level=LogLevel.LOG_LEVEL_INFO,dump_config=False)
await client.execute_service(next(s for s in services if s.name=='ui_snapshot'),{})
await asyncio.wait_for(done.wait(),timeout=60)
if errors or set(rows)!=set(range(240)):raise ValueError(f'Onvolledig beeld: {len(rows)}/240 rijen; {errors}')
with args.output.open('xb') as f:f.write(png(rows))
print(f'PASS: LVGL-render opgeslagen in {args.output}')
finally:await client.disconnect()
if __name__=='__main__':asyncio.run(main())
+152
View File
@@ -0,0 +1,152 @@
# Guition ESP32-S3 4-inch wallbox — 480 × 480
Deze branch voegt een apart profiel toe voor de **Guition 4848S040** met
ST7701S RGB-display en **GT911 capacitieve touch**, ESP32-S3, 16 MB flash en
8 MB octal PSRAM. De naam/2mm-wandplaat beschrijft ook de behuizing; controleer
altijd de elektronica. Dit profiel configureert geen relais.
Hardwarebron: [ESPHome Guition-bordbeschrijving](https://devices.esphome.io/devices/guition-esp32-s3-4848s040/)
en het ingebouwde [MIPI RGB-model](https://esphome.io/components/display/mipi_rgb/).
De daadwerkelijk geïnstalleerde ESPHome 2026.6.2 bevat dit model.
## Bestanden
- `guition-4848s040.yaml`: neutrale hardware + 480×480-interface.
- `guition-device.example.yaml`: eigen apparaat-/tegelinstellingen.
- `guition-device.yaml`: jouw lokale profiel, buiten Git.
- `secrets.yaml`: lokale wifi/API/OTA-instellingen, buiten Git.
- `tools/verify_gt911.py`: fysieke pixelcontrole; geen resistieve ADC-kalibratie.
Het 2,8-inch CYD-profiel blijft apart. Flash dat niet naar de Guition en neem
geen `calibration.yaml` of XPT2046-correctie over.
## Interface
Zes tegels van **218 × 108 pixels** per pagina, 12px tussenruimte en een
aparte navigatiestrook. Vanaf zeven tegels verschijnt pagina twee; tot zes
verdwijnt de navigatie. Grotere letters, iconen, dimmers en kleurkeuzes; de
climatekaart heeft een grotere temperatuurweergave. Een donkerblauwe
achtergrond, gedempte uitgeschakelde tegels en lichte actieve tegels maken
de status herkenbaar. Standby begint na tien minuten zonder aanraking.
De bestaande tegelacties, climatebediening en vacuumkaart zijn behouden.
De vacuumkaart blijft gekoppeld aan tegel 6. Posities 8/10 zijn eenvoudige
actietegels; zie [TEGELS.md](TEGELS.md) voor de ondersteunde bindingen.
## Nieuwe installatie
Volg de Python-/USB-voorbereiding in [README.md](../README.md), in een verse
kopie. Maak dit profiel:
```sh
python tools/new_device.py --board guition --name wallbox-keuken --friendly-name "Wallbox keuken"
```
Dit maakt `guition-device.yaml` en nieuwe secrets; bestaande bestanden worden
niet overschreven. Vul wifi in en wijzig de tegelentiteiten. Gebruik bij
meerdere borden een aparte map per apparaat. Het huidige lokale Guition-profiel
van de eigenaar mag diens bestaande secrets gebruiken.
Controleer de seriële poort en chip voordat je uploadt. Voor het geteste bord
bleken hogere seriële snelheden bij uitlezen onbetrouwbaar. Gebruik 115200 baud:
```sh
python -m esphome config guition-device.yaml
python -m esphome compile guition-device.yaml
```
Flash via `esphome run` als de USB-verbinding betrouwbaar is. Voor expliciet
115200 baud kun je de gecombineerde factory-image gebruiken. Vervang zowel
poort als apparaatnaam in het pad door jouw waarden:
```sh
python -m esptool --chip esp32s3 --port <USB_POORT> --baud 115200 write-flash 0 .esphome/build/wallbox-keuken/.pioenvs/wallbox-keuken/firmware.factory.bin
```
De factory-image hoort bij dit S3-profiel en begint op adres **0**, anders dan
sommige klassieke ESP32-images. Bewaar een herstelkopie vóór vervanging van
bestaande firmware als die later nog nodig is. Houd binaries en logs lokaal.
## GT911 en oriëntatie testen
De GT911 geeft pixels door. Meestal is geen kalibratie nodig; een verkeerde
rotatie/transform mag niet met de CYD-affinewizard worden weggewerkt.
Met een werkende wifi/API-verbinding:
```sh
python diagnostics/control_ui.py touch_diagnostics --host wallbox-keuken.local --name wallbox-keuken
python tools/verify_gt911.py --port <USB_POORT> --output measurements-guition.json
python diagnostics/control_ui.py end_touch_diagnostics --host wallbox-keuken.local --name wallbox-keuken
```
Er verschijnen vijf kruisjes. De wizard vraagt drie afzonderlijke tikken per
punt; eerst Enter en daarna uitsluitend het genoemde kruisje aanraken. Fout
maximaal 16px, spreiding maximaal 18px. De test stuurt geen HA-acties.
Zonder wifi kun je eerst een build maken met `-s CALIBRATION_ON_BOOT true`,
die na USB-upload direct het GT911-meetscherm toont. Na controle opnieuw
bouwen/flashen zonder die override. Deze compatibiliteitsnaam opent alleen
een pixeltest; hij voert geen ADC-kalibratie uit.
Op het geteste paneel zijn beide GT911-assen gespiegeld: `TOUCH_MIRROR_X` en
`TOUCH_MIRROR_Y` staan op `true`. Standaard is `LVGL_ROTATION: "0"`. Bij een andere montage stel je de gewenste
LVGL-rotatie in en geef je dezelfde waarde aan de wizard, bijvoorbeeld
`--rotation 90`. Controleer alle hoeken; verander de GT911 `TOUCH_SWAP_XY` /
`TOUCH_MIRROR_X` / `TOUCH_MIRROR_Y` alleen als de fysieke meting dat vereist.
## HA en acceptatie
Voeg het nieuwe apparaat toe via de ESPHome-integratie (naam/IP, poort 6053,
API-sleutel uit secrets). Geef toestemming voor HA-acties en gebruik
`DIRECT_ACTIONS: "true"` zodra de juiste entiteiten gecontroleerd zijn.
```sh
python diagnostics/run_ui_test.py --host wallbox-keuken.local --name wallbox-keuken
python -m unittest discover -s tests -p 'test_*.py'
```
Doorloop ook de fysieke controles uit [ACCEPTATIE.md](ACCEPTATIE.md), met de
GT911-wizard in plaats van de XPT2046-kalibratie. Controleer beeld, juiste
aanrakingen, paginering, lange druk, climate/vacuum, echte HA-terugmelding,
standby en herstel na herstart. Een compilatie/renderproef vervangt die
fysieke controle niet. De testuitkomst van het oorspronkelijke CYD-bord
zegt niets over dit nieuwe bord.
## Gerenderde interface inspecteren
```sh
python diagnostics/capture_ui.py --host wallbox-keuken.local --name wallbox-keuken --output diagnostics/guition-home.png
```
Dit maakt alleen op verzoek een LVGL-snapshot en stuurt een 240×240-preview
via de versleutelde API. De tijdelijke buffer wordt na ongeveer 24 seconden
vrijgegeven. Het beeld controleert de renderer; het bewijst niet dat de fysieke
RGB-signalen, schermkleuren of montageoriëntatie goed zijn. De renderproef
controleert ook of zichtbare klikgebieden binnen het 480×480-scherm vallen.
## Herstelkopie van dit testbord
De oorspronkelijke gebruikte firmware inclusief bootloader, partitietabel
en NVS is lokaal bewaard als `diagnostics/guition-original-recovery.bin`.
De lengtes van de vijf applicatiesegmenten bepaalden de benodigde 4.788.224
bytes; dit is geen kopie van alle 16 MB flash. De stub controleerde de MD5
van ieder uitgelezen blok. SHA-256 van het complete herstelbestand:
```text
a656c793c422848b5ad72bc16545fb92c844b7dfba029f4e4399d22444a9e046
```
Deze kopie is privé, hoort uitsluitend bij het geteste bord en wordt niet
meegecommit of geëxporteerd. Herstellen gebeurt op adres 0, met dezelfde
115200-baudopdracht als de factory-image, maar met dit herstelbestand.
## RGB-geheugeninstellingen
De pixelklok staat op 16 MHz: op het testbord verdween daarmee het zachte
flikkeren van de 12MHz-instelling. Voor de RGB-bouncebuffer worden code en
constante data vanuit octal PSRAM uitgevoerd, met 64KB datacache en 64-byte
cachelijnen; herstel van de RGB-stroom gebeurt op VSYNC. Deze instellingen
volgen [Espressifs RGB-LCD-aanbevelingen](https://docs.espressif.com/projects/esp-idf/en/v5.5.4/esp32s3/api-reference/peripherals/lcd/rgb_lcd.html).
Controleer fysiek op incidentele glitches onder wifi-/renderbelasting; een
software-snapshot kan een verstoring van het paneelsignaal niet bewijzen.
+50
View File
@@ -0,0 +1,50 @@
# Guition hardwaretest — 12 september 2026
Getest op de aangesloten ESP32-S3 rev. 0.2, 16 MB flash en 8 MB PSRAM,
GT911 op I²C-adres 0x5D, GUITION-4848S040 RGB-display op 480×480.
## Werkende versie
ESPHome 2026.6.2 / ESP-IDF 5.5.4. De firmware rapporteert buildtijd
`2026-09-12 10:45:49 +0200`, config-hash `0xbaee760b`.
- Beide GT911-assen gespiegeld. De gebruiker zag eerst linksboven als
rechtsonder; na correctie kwamen de vijf aangeraakte regio's in de juiste
volgorde binnen: linksboven, rechtsboven, rechtsonder, linksonder, midden.
- Pixelklok 16 MHz. De gebruiker bevestigde dat het zachte flikkeren daarmee
verdween, maar meldde nog af en toe een glitch.
- Vervolgens PSRAM-uitvoering voor code/constanten, 64KB datacache met
64-byte cachelijnen en RGB-herstel op VSYNC ingeschakeld. Na deze update en
de herhaalde renderproef bevestigde de gebruiker: **“het werkt super!”**
## Uitgevoerde controles
- 23 Python-regressietests geslaagd, inclusief nieuw bordprofiel, tegelformaat,
navigatieruimte, GT911-coördinaten/rotatie en overdracht zonder lokale secrets.
- Beide C++17-testprogramma's geslaagd: eventguard/temperatuur en CYD-touchfilter.
- Volledige S3-build geslaagd. Eerste USB-upload op 115200 baud met verificatie
van de geschreven hash; vervolguploads via geauthenticeerde OTA geslaagd.
- De originele gebruikte applicatie, bootloader, partitietabel en NVS zijn
als lokale herstelkopie bewaard; zie GUITION.md voor lengte en SHA-256.
- Bootlogs bevestigen 8192KB beschikbare PSRAM, de GT911 en RGB-driver.
- **Tien paginacontroles en vijftig overlaycycli op de definitieve firmware**
geslaagd, inclusief controle dat zichtbare klikgebieden binnen 480×480 liggen.
- Tegelgebied eindigt op y=419; de volgende-knop begint op y=432. Geen overlap.
- Vrije heap bleef tijdens de definitieve renderproef rond **6,51 MB**.
- Een echte LVGL-snapshot via de versleutelde API opgehaald en visueel
gecontroleerd op tekst, kleuren en indeling. Deze screenshotfunctie bekijkt
de renderer; de fysieke signaalcontrole komt van de gebruiker.
- Een verse export uitgepakt en een neutraal Guition-profiel aangemaakt;
ESPHome-codegeneratie geslaagd zonder de persoonlijke tegelconfiguratie.
## Grenzen van deze test
De fysieke check gebruikte één tik per doel plus gebruik van de bediening;
het volledige drie-tikken-per-punt-protocol uit `verify_gt911.py` is niet op
het bord uitgevoerd. Stabiliteit is tijdens deze testsessie geobserveerd,
niet gedurende dagen. De tien-minutenstandby is behouden in de configuratie;
een nieuwe volledige standbyduurtest is niet uitgevoerd.
Home Assistant ontdekte het nieuwe apparaat. De koppeldialoog wacht nog op de
lokale API-encryptiesleutel. Werkelijke HA-acties en de terugmelding van eigen
entiteiten zijn op dit nieuwe apparaat daarom **nog niet geverifieerd**.
File diff suppressed because it is too large Load Diff
+213
View File
@@ -0,0 +1,213 @@
# Kopieer via: python tools/new_device.py --board guition --name display-keuken
# Bewerk alleen dit lokale profiel voor jouw scherm.
packages:
display: !include guition-4848s040.yaml
substitutions:
DEVICE_NAME: "guition-new"
DEVICE_FRIENDLY_NAME: "Mijn Guition"
ROOM_NAME: "Woonkamer"
TILE_COUNT: "6"
DIRECT_ACTIONS: "false" # Pas na koppelen/testen op true zetten.
AUTO_DIM_TIMEOUT: "600"
CALIBRATION_ON_BOOT: "false"
# Tegel 1
TILE1_ENTITY: "light.voorbeeld_lamp"
TILE1_STATE_ENTITY: "light.voorbeeld_lamp"
TILE1_TITLE: "Lamp"
TILE1_TYPE: "light"
TILE1_TAP_ACTION: "auto"
TILE1_TAP_SERVICE: ""
TILE1_TAP_PARAM_KEY: ""
TILE1_TAP_PARAM_VAL: ""
TILE1_LONGPRESS: "slider"
TILE1_LONGPRESS_SLIDER: "auto"
TILE1_LONGPRESS_OFF_VALUE: "0"
TILE1_LONGPRESS_ACTION: ""
TILE1_LONGPRESS_ACTION_TYPE: ""
TILE1_LONGPRESS_ACTION_SERVICE: ""
TILE1_VALUE_MODE: "auto"
TILE1_LABEL_ON: "Aan"
TILE1_LABEL_OFF: "Uit"
TILE1_ACTIVE_STATE: ""
# Tegel 2
TILE2_ENTITY: "switch.voorbeeld_stekker"
TILE2_STATE_ENTITY: "switch.voorbeeld_stekker"
TILE2_TITLE: "Stekker"
TILE2_TYPE: "switch"
TILE2_TAP_ACTION: "auto"
TILE2_TAP_SERVICE: ""
TILE2_TAP_PARAM_KEY: ""
TILE2_TAP_PARAM_VAL: ""
TILE2_LONGPRESS: "none"
TILE2_LONGPRESS_SLIDER: "auto"
TILE2_LONGPRESS_OFF_VALUE: "0"
TILE2_LONGPRESS_ACTION: ""
TILE2_LONGPRESS_ACTION_TYPE: ""
TILE2_LONGPRESS_ACTION_SERVICE: ""
TILE2_VALUE_MODE: "auto"
TILE2_LABEL_ON: "Aan"
TILE2_LABEL_OFF: "Uit"
TILE2_ACTIVE_STATE: ""
# Tegel 3
TILE3_ENTITY: "scene.voorbeeld_avond"
TILE3_STATE_ENTITY: "scene.voorbeeld_avond"
TILE3_TITLE: "Avond"
TILE3_TYPE: "scene"
TILE3_TAP_ACTION: "auto"
TILE3_TAP_SERVICE: ""
TILE3_TAP_PARAM_KEY: ""
TILE3_TAP_PARAM_VAL: ""
TILE3_LONGPRESS: "none"
TILE3_LONGPRESS_SLIDER: "auto"
TILE3_LONGPRESS_OFF_VALUE: "0"
TILE3_LONGPRESS_ACTION: ""
TILE3_LONGPRESS_ACTION_TYPE: ""
TILE3_LONGPRESS_ACTION_SERVICE: ""
TILE3_VALUE_MODE: "auto"
TILE3_LABEL_ON: "Aan"
TILE3_LABEL_OFF: "Uit"
TILE3_ACTIVE_STATE: ""
# Tegel 4
TILE4_ENTITY: "script.voorbeeld_uit"
TILE4_STATE_ENTITY: "script.voorbeeld_uit"
TILE4_TITLE: "Alles uit"
TILE4_TYPE: "script"
TILE4_TAP_ACTION: "auto"
TILE4_TAP_SERVICE: ""
TILE4_TAP_PARAM_KEY: ""
TILE4_TAP_PARAM_VAL: ""
TILE4_LONGPRESS: "none"
TILE4_LONGPRESS_SLIDER: "auto"
TILE4_LONGPRESS_OFF_VALUE: "0"
TILE4_LONGPRESS_ACTION: ""
TILE4_LONGPRESS_ACTION_TYPE: ""
TILE4_LONGPRESS_ACTION_SERVICE: ""
TILE4_VALUE_MODE: "auto"
TILE4_LABEL_ON: "Aan"
TILE4_LABEL_OFF: "Uit"
TILE4_ACTIVE_STATE: ""
# Tegel 5
TILE5_ENTITY: "climate.voorbeeld_airco"
TILE5_STATE_ENTITY: "climate.voorbeeld_airco"
TILE5_TITLE: "Airco"
TILE5_TYPE: "climate"
TILE5_TAP_ACTION: "toggle"
TILE5_TAP_SERVICE: "climate.toggle"
TILE5_TAP_PARAM_KEY: ""
TILE5_TAP_PARAM_VAL: ""
TILE5_LONGPRESS: "slider"
TILE5_LONGPRESS_SLIDER: "auto"
TILE5_LONGPRESS_OFF_VALUE: "0"
TILE5_LONGPRESS_ACTION: ""
TILE5_LONGPRESS_ACTION_TYPE: ""
TILE5_LONGPRESS_ACTION_SERVICE: ""
TILE5_VALUE_MODE: "auto"
TILE5_LABEL_ON: "Aan"
TILE5_LABEL_OFF: "Uit"
TILE5_ACTIVE_STATE: ""
# Tegel 6
TILE6_ENTITY: "vacuum.voorbeeld_robot"
TILE6_STATE_ENTITY: "vacuum.voorbeeld_robot"
TILE6_TITLE: "Stofzuiger"
TILE6_TYPE: "vacuum"
TILE6_TAP_ACTION: "custom"
TILE6_TAP_SERVICE: "vacuum.start"
TILE6_TAP_PARAM_KEY: ""
TILE6_TAP_PARAM_VAL: ""
TILE6_LONGPRESS: "vacuum"
TILE6_LONGPRESS_SLIDER: "auto"
TILE6_LONGPRESS_OFF_VALUE: "0"
TILE6_LONGPRESS_ACTION: ""
TILE6_LONGPRESS_ACTION_TYPE: ""
TILE6_LONGPRESS_ACTION_SERVICE: ""
TILE6_VALUE_MODE: "auto"
TILE6_LABEL_ON: "Aan"
TILE6_LABEL_OFF: "Uit"
TILE6_ACTIVE_STATE: ""
# Tegel 7 (verborgen zolang TILE_COUNT lager is)
TILE7_ENTITY: "light.ongebruikt_7"
TILE7_STATE_ENTITY: "light.ongebruikt_7"
TILE7_TITLE: "Tegel 7"
TILE7_TYPE: "light"
TILE7_TAP_ACTION: "auto"
TILE7_TAP_SERVICE: ""
TILE7_TAP_PARAM_KEY: ""
TILE7_TAP_PARAM_VAL: ""
TILE7_LONGPRESS: "none"
TILE7_LONGPRESS_SLIDER: "auto"
TILE7_LONGPRESS_OFF_VALUE: "0"
TILE7_LONGPRESS_ACTION: ""
TILE7_LONGPRESS_ACTION_TYPE: ""
TILE7_LONGPRESS_ACTION_SERVICE: ""
TILE7_VALUE_MODE: "auto"
TILE7_LABEL_ON: "Aan"
TILE7_LABEL_OFF: "Uit"
TILE7_ACTIVE_STATE: ""
# Tegel 8 (verborgen zolang TILE_COUNT lager is)
TILE8_ENTITY: "light.ongebruikt_8"
TILE8_STATE_ENTITY: "light.ongebruikt_8"
TILE8_TITLE: "Tegel 8"
TILE8_TYPE: "light"
TILE8_TAP_ACTION: "auto"
TILE8_TAP_SERVICE: ""
TILE8_TAP_PARAM_KEY: ""
TILE8_TAP_PARAM_VAL: ""
TILE8_LONGPRESS: "none"
TILE8_LONGPRESS_SLIDER: "auto"
TILE8_LONGPRESS_OFF_VALUE: "0"
TILE8_LONGPRESS_ACTION: ""
TILE8_LONGPRESS_ACTION_TYPE: ""
TILE8_LONGPRESS_ACTION_SERVICE: ""
TILE8_VALUE_MODE: "auto"
TILE8_LABEL_ON: "Aan"
TILE8_LABEL_OFF: "Uit"
TILE8_ACTIVE_STATE: ""
# Tegel 9 (verborgen zolang TILE_COUNT lager is)
TILE9_ENTITY: "light.ongebruikt_9"
TILE9_STATE_ENTITY: "light.ongebruikt_9"
TILE9_TITLE: "Tegel 9"
TILE9_TYPE: "light"
TILE9_TAP_ACTION: "auto"
TILE9_TAP_SERVICE: ""
TILE9_TAP_PARAM_KEY: ""
TILE9_TAP_PARAM_VAL: ""
TILE9_LONGPRESS: "none"
TILE9_LONGPRESS_SLIDER: "auto"
TILE9_LONGPRESS_OFF_VALUE: "0"
TILE9_LONGPRESS_ACTION: ""
TILE9_LONGPRESS_ACTION_TYPE: ""
TILE9_LONGPRESS_ACTION_SERVICE: ""
TILE9_VALUE_MODE: "auto"
TILE9_LABEL_ON: "Aan"
TILE9_LABEL_OFF: "Uit"
TILE9_ACTIVE_STATE: ""
# Tegel 10 (verborgen zolang TILE_COUNT lager is)
TILE10_ENTITY: "light.ongebruikt_10"
TILE10_STATE_ENTITY: "light.ongebruikt_10"
TILE10_TITLE: "Tegel 10"
TILE10_TYPE: "light"
TILE10_TAP_ACTION: "auto"
TILE10_TAP_SERVICE: ""
TILE10_TAP_PARAM_KEY: ""
TILE10_TAP_PARAM_VAL: ""
TILE10_LONGPRESS: "none"
TILE10_LONGPRESS_SLIDER: "auto"
TILE10_LONGPRESS_OFF_VALUE: "0"
TILE10_LONGPRESS_ACTION: ""
TILE10_LONGPRESS_ACTION_TYPE: ""
TILE10_LONGPRESS_ACTION_SERVICE: ""
TILE10_VALUE_MODE: "auto"
TILE10_LABEL_ON: "Aan"
TILE10_LABEL_OFF: "Uit"
TILE10_ACTIVE_STATE: ""
+57
View File
@@ -0,0 +1,57 @@
#pragma once
#include "lvgl.h"
#include "esphome/core/log.h"
// On-demand 240x240 diagnostic image of LVGL's rendered screen. Nothing is
// allocated or streamed during normal use. The RGB display signal itself
// still needs a physical visual check.
namespace guition {
inline bool check_geometry(lv_obj_t *obj) {
if (lv_obj_has_flag(obj, LV_OBJ_FLAG_HIDDEN)) return true;
bool ok = true;
lv_area_t area;
lv_obj_get_coords(obj, &area);
if (lv_obj_has_flag(obj, LV_OBJ_FLAG_CLICKABLE) &&
(area.x1 < 0 || area.y1 < 0 || area.x2 >= 480 || area.y2 >= 480)) {
ESP_LOGE("ui_test", "GEOMETRY FAIL clickable=(%d,%d)-(%d,%d)",
area.x1, area.y1, area.x2, area.y2);
ok = false;
}
for (uint32_t i = 0; i < lv_obj_get_child_count(obj); ++i)
ok = check_geometry(lv_obj_get_child(obj, i)) && ok;
return ok;
}
inline lv_draw_buf_t *snapshot = nullptr;
inline int snapshot_row = 0;
inline void snapshot_begin() {
if (snapshot) lv_draw_buf_destroy(snapshot);
snapshot = lv_snapshot_take(lv_screen_active(), LV_COLOR_FORMAT_RGB565);
snapshot_row = 0;
if (!snapshot || snapshot->header.w != 480 || snapshot->header.h != 480) {
if (snapshot) lv_draw_buf_destroy(snapshot);
snapshot = nullptr;
ESP_LOGE("ui_image", "IMAGE FAIL allocation/dimensions");
return;
}
ESP_LOGI("ui_image", "IMAGE BEGIN 240 240");
}
inline void snapshot_next_row() {
if (!snapshot) return;
const auto *row = reinterpret_cast<const uint16_t *>(
snapshot->data + snapshot_row * 2 * snapshot->header.stride);
static constexpr char hex[] = "0123456789abcdef";
char line[961];
for (int x = 0; x < 240; ++x) {
uint16_t pixel = row[x * 2];
for (int digit = 0; digit < 4; ++digit)
line[x * 4 + digit] = hex[(pixel >> (12 - digit * 4)) & 15];
}
line[960] = 0;
ESP_LOGI("ui_image", "ROW %03d %s", snapshot_row, line);
if (++snapshot_row == 240) {
lv_draw_buf_destroy(snapshot);
snapshot = nullptr;
ESP_LOGI("ui_image", "IMAGE COMPLETE");
}
}
} // namespace guition
+71
View File
@@ -0,0 +1,71 @@
"""Guition-specific geometry, hardware isolation and capacitive touch checks."""
from pathlib import Path
import re
import sys
import tempfile
import unittest
ROOT=Path(__file__).resolve().parents[1]
sys.path.insert(0,str(ROOT/'tools'))
import verify_gt911
import new_device
SOURCE=(ROOT/'guition-4848s040.yaml').read_text()
VALUES=dict(re.findall(r'^ (\w+): "([^"]*)"',SOURCE,re.M))
class GuitionTests(unittest.TestCase):
def test_hardware_is_s3_rgb_with_capacitive_touch(self):
self.assertIn('model: GUITION-4848S040',SOURCE)
self.assertIn('platform: gt911',SOURCE)
self.assertIn('mode: octal',SOURCE)
self.assertNotIn('platform: xpt2046',SOURCE)
self.assertNotIn('set_raw_correction',SOURCE)
self.assertEqual(VALUES['TOUCH_MIRROR_X'],'true')
self.assertEqual(VALUES['TOUCH_MIRROR_Y'],'true')
self.assertIn('pclk_frequency: 16MHz',SOURCE)
self.assertIn('execute_from_psram: true',SOURCE)
self.assertNotIn('id: output_red',SOURCE)
self.assertEqual(VALUES['DISPLAY_W'],'480')
self.assertEqual(VALUES['DISPLAY_H'],'480')
def test_cards_fit_with_gutters_and_reserved_navigation(self):
v=lambda k:int(VALUES[k])
rectangles=[]
for r in range(1,4):
for c in range(1,3):
x,y=v(f'GRID_COL{c}_X'),v('SCROLL_Y')+v(f'GRID_ROW{r}_Y')
w,h=v('TILE_W'),v('TILE_H')
self.assertGreaterEqual(x,16)
self.assertLessEqual(x+w,464)
self.assertLessEqual(y+h,420)
self.assertGreaterEqual(w*h,3*147*52)
for xx,yy,ww,hh in rectangles:
self.assertFalse(x<xx+ww and x+w>xx and y<yy+hh and y+h>yy)
rectangles.append((x,y,w,h))
self.assertEqual(len(rectangles),6)
self.assertIn('height: 44',SOURCE.split('id: page_next',1)[1][:200])
def test_all_tiles_guard_events_and_clip_long_titles(self):
for i in range(1,11):
self.assertEqual(SOURCE.count(f'cyd::touch_guard.accept(millis(), {i})'),2)
self.assertRegex(SOURCE,rf'id: t{i}_title\n\s+width: 130\n\s+long_mode: DOT')
self.assertEqual(VALUES['AUTO_DIM_TIMEOUT'],'600')
def test_gt911_verification_accepts_pixels_and_rejects_wrong_orientation(self):
data=dict(screen=[480,480],rotation=0,points=[dict(name=name,samples=[list(xy)]*3) for name,xy in verify_gt911.TARGETS])
self.assertEqual(len(verify_gt911.verify(data)),5)
data['rotation']=180
with self.assertRaises(ValueError):verify_gt911.verify(data)
def test_gt911_rotations(self):
self.assertEqual(verify_gt911.screen_point(20,30,90),(30,459))
self.assertEqual(verify_gt911.screen_point(20,30,270),(449,20))
def test_initializer_does_not_copy_resistive_calibration(self):
with tempfile.TemporaryDirectory() as tmp:
new_device.create(tmp,'wallbox-test','Wallbox Test','guition')
profile=(Path(tmp)/'guition-device.yaml').read_text()
self.assertIn('!include guition-4848s040.yaml',profile)
self.assertIn('DEVICE_NAME: "wallbox-test"',profile)
self.assertFalse((Path(tmp)/'calibration.yaml').exists())
self.assertTrue((Path(tmp)/'secrets.yaml').exists())
if __name__=='__main__':unittest.main()
+1 -1
View File
@@ -7,7 +7,7 @@ import zipfile
ROOT = Path(__file__).resolve().parents[1]
FILES = ['.gitignore', 'README.md', 'AGENTS.md', 'requirements.txt', 'device.example.yaml',
'calibration.example.yaml', 'secrets.yaml.example', 'cyd_ui.h', 'LICENSE',
'TILE_CONFIGURATION.md', 'CYD_STABILITY.md', 'TEST_RESULTS.md']
'guition_diagnostics.h', 'guition-4848s040.yaml', 'guition-device.example.yaml', 'TILE_CONFIGURATION.md', 'CYD_STABILITY.md', 'TEST_RESULTS.md']
FOLDERS = ['docs', 'tools', 'tests', 'components', 'diagnostics', 'fonts']
SUFFIXES = {'.py', '.cpp', '.h', '.md', '.ttf', '.txt'}
+12 -8
View File
@@ -9,18 +9,20 @@ import secrets
ROOT = Path(__file__).resolve().parents[1]
def create(directory, name, friendly_name):
def create(directory, name, friendly_name, board="cyd"):
if not re.fullmatch(r'[a-z][a-z0-9-]{0,30}', name) or name.endswith('-'):
raise ValueError('Gebruik een unieke naam van 1–31 tekens: kleine letters, cijfers en koppeltekens.')
if board not in ("cyd", "guition"):
raise ValueError("Onbekend bordprofiel")
import json
device = (ROOT / 'device.example.yaml').read_text().replace('"cyd-new"', json.dumps(name), 1)
device = device.replace('"Mijn CYD"', json.dumps(friendly_name, ensure_ascii=False), 1)
device = (ROOT / ('guition-device.example.yaml' if board == 'guition' else 'device.example.yaml')).read_text().replace('"guition-new"' if board == 'guition' else '"cyd-new"', json.dumps(name), 1)
device = device.replace('"Mijn Guition"' if board == 'guition' else '"Mijn CYD"', json.dumps(friendly_name, ensure_ascii=False), 1)
secret_text = (ROOT / 'secrets.yaml.example').read_text()
replacements = [base64.b64encode(secrets.token_bytes(32)).decode(), secrets.token_urlsafe(24), secrets.token_urlsafe(18)]
for value in replacements:
secret_text = secret_text.replace('WORDT_UNIEK_GEGENEREERD', value, 1)
files = {'device.yaml': device, 'calibration.yaml': (ROOT / 'calibration.example.yaml').read_text(),
'secrets.yaml': secret_text}
files = {'guition-device.yaml' if board == 'guition' else 'device.yaml': device, 'secrets.yaml': secret_text}
if board == 'cyd': files['calibration.yaml'] = (ROOT / 'calibration.example.yaml').read_text()
directory = Path(directory)
if not directory.is_dir():
raise ValueError('De doelmap moet al bestaan en de uitgepakte code bevatten.')
@@ -44,15 +46,17 @@ def create(directory, name, friendly_name):
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--board', choices=['cyd', 'guition'], default='cyd')
parser.add_argument('--name', required=True, help='Unieke ESPHome-naam, bijvoorbeeld display-keuken')
parser.add_argument('--friendly-name', default='Mijn CYD')
parser.add_argument('--friendly-name')
parser.add_argument('--directory', type=Path, default=ROOT)
args = parser.parse_args()
args.friendly_name = args.friendly_name or ('Mijn Guition' if args.board == 'guition' else 'Mijn CYD')
try:
for path in create(args.directory, args.name, args.friendly_name):
for path in create(args.directory, args.name, args.friendly_name, args.board):
print(f'Gemaakt: {path}')
print('Vul wifi lokaal in secrets.yaml in. Sleutels zijn uniek gegenereerd en worden niet getoond.')
print('Volg nu README.md; flash device.yaml, niet rechtstreeks de basisconfiguratie.')
print('Volg docs/GUITION.md; flash guition-device.yaml.' if args.board == 'guition' else 'Volg README.md; flash device.yaml.')
except (ValueError, OSError) as exc:
parser.exit(1, f'Fout: {exc}\n')
+83
View File
@@ -0,0 +1,83 @@
"""Guided USB pixel verification for GT911, without resistive calibration or HA actions."""
import argparse
import json
import math
from pathlib import Path
import re
import statistics
import time
from calibrate import write_file
TARGETS = [('linksboven', (24, 24)), ('rechtsboven', (455, 24)),
('rechtsonder', (455, 455)), ('linksonder', (24, 455)), ('midden', (240, 240))]
PATTERN = re.compile(r'GT911 press x=(\d+) y=(\d+) test=([01])')
def screen_point(x, y, rotation=0):
if rotation == 0: return x, y
if rotation == 90: return y, 479-x
if rotation == 180: return 479-x, 479-y
if rotation == 270: return 479-y, x
raise ValueError('Rotatie moet 0, 90, 180 of 270 zijn.')
def verify(data):
if data.get('screen') != [480,480] or len(data.get('points',[])) != 5:
raise ValueError('Verwacht vijf meetpunten op een 480x480 GT911-scherm.')
result=[]
for p,(name,target) in zip(data['points'],TARGETS):
if p.get('name') != name or len(p.get('samples',[])) != 3:
raise ValueError('Volg de vijf doelen; ieder doel vereist drie aparte tikken.')
samples=[]
for xy in p['samples']:
if len(xy)!=2 or any(type(v) is not int or not 0<=v<480 for v in xy):
raise ValueError('Touchcoördinaten vallen buiten 480x480.')
samples.append(screen_point(*xy, data.get('rotation',0)))
center=tuple(statistics.median(x[i] for x in samples) for i in (0,1))
error=math.dist(center,target)
spread=max(math.dist(x,center) for x in samples)
result.append(dict(name=name,error_px=round(error,1),spread_px=round(spread,1)))
if error>16 or spread>18:
raise ValueError(f'{name}: fout {error:.1f}px / spreiding {spread:.1f}px. Controleer rotatie en GT911-transform; geen ADC-fit gebruiken.')
return result
def main():
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--port', help='USB-poort; firmware moet het GT911-meetscherm tonen')
parser.add_argument('--output',type=Path,default=Path('measurements-guition.json'))
parser.add_argument('--input',type=Path,help='Controleer een bestaand meetbestand zonder USB')
parser.add_argument('--rotation',type=int,choices=(0,90,180,270),default=0)
args=parser.parse_args()
try:
if args.input:
data=json.loads(args.input.read_text())
else:
import serial
if not args.port: raise ValueError('--port is nodig voor een fysieke meting.')
if args.output.exists(): raise ValueError('Meetbestand bestaat al; kies een andere naam.')
data=dict(screen=[480,480],rotation=args.rotation,points=[])
with serial.Serial(args.port,115200,timeout=.2) as port:
for name,xy in TARGETS:
input(f'{name} {xy}: druk Enter en tik daarna drie keer alleen dit kruisje, met loslaten ertussen. ')
port.reset_input_buffer()
samples=[]; deadline=time.monotonic()+90; last=0
while len(samples)<3 and time.monotonic()<deadline:
m=PATTERN.search(port.readline().decode(errors='replace'))
if not m: continue
if m[3]!='1': raise ValueError('Het geïsoleerde GT911-meetscherm is niet actief.')
if time.monotonic()-last < .4:continue
samples.append([int(m[1]),int(m[2])]);last=time.monotonic()
print(f' {len(samples)}/3',flush=True)
if len(samples)!=3:raise ValueError(f'{name}: onvoldoende tikken ontvangen.')
data['points'].append(dict(name=name,samples=samples))
write_file(args.output,json.dumps(data,indent=2)+'\n')
for row in verify(data):print(row)
print('PASS: alle vijf GT911-doelen geverifieerd; geen HA-acties uitgevoerd.')
except (ValueError,OSError,TypeError,KeyError,EOFError) as exc:
parser.exit(1,f'Fout: {exc}\n')
except KeyboardInterrupt:
parser.exit(130,'Meting afgebroken.\n')
if __name__=='__main__': main()