Add Guition 480x480 wallbox UI with GT911 and stable RGB rendering
This commit is contained in:
@@ -32,3 +32,7 @@ __pycache__/
|
||||
/calibration*.yaml.bak-*
|
||||
/calibration*.yaml.tmp-*
|
||||
/.DS_Store
|
||||
|
||||
/guition-device.yaml
|
||||
|
||||
/diagnostics/*.png
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
@@ -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
@@ -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: ""
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
@@ -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')
|
||||
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user