Add stable CYD firmware, USB calibration wizard and complete setup guide
This commit is contained in:
+22
@@ -10,3 +10,25 @@ testdevice.yaml
|
||||
|
||||
# Handmatige backups van de config
|
||||
*.yaml.bak*
|
||||
|
||||
# Local rollback binaries and diagnostic captures
|
||||
/diagnostics/*.bin
|
||||
/diagnostics/*.log
|
||||
|
||||
# Python caches from local ESPHome components
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Per-owner onboarding files and share bundles
|
||||
/device.yaml
|
||||
/calibration.yaml
|
||||
/calibration.proposed.yaml
|
||||
/measurements*.json
|
||||
/dist/
|
||||
/.venv/
|
||||
|
||||
# Local editor state and measurement/temporary outputs
|
||||
/.idea/
|
||||
/calibration*.yaml.bak-*
|
||||
/calibration*.yaml.tmp-*
|
||||
/.DS_Store
|
||||
|
||||
Generated
-10
@@ -1,10 +0,0 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Ignored default folder with query files
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
Generated
-8
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
Generated
-8
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/esphome-cyd-display.iml" filepath="$PROJECT_DIR$/.idea/esphome-cyd-display.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
Generated
-19
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MessDetectorOptionsConfiguration">
|
||||
<option name="transferred" value="true" />
|
||||
</component>
|
||||
<component name="PHPCSFixerOptionsConfiguration">
|
||||
<option name="transferred" value="true" />
|
||||
</component>
|
||||
<component name="PHPCodeSnifferOptionsConfiguration">
|
||||
<option name="highlightLevel" value="WARNING" />
|
||||
<option name="transferred" value="true" />
|
||||
</component>
|
||||
<component name="PhpStanOptionsConfiguration">
|
||||
<option name="transferred" value="true" />
|
||||
</component>
|
||||
<component name="PsalmOptionsConfiguration">
|
||||
<option name="transferred" value="true" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,48 @@
|
||||
# Werkinstructies voor LLM's en developers
|
||||
|
||||
Dit project bedient een ESP32-2432S028 met ILI9341 + XPT2046 (320×240,
|
||||
LVGL 90°). Lees README.md en docs/ voordat je installeert. De eigenaar kan
|
||||
fysiek tikken; een agent kan dat niet vervangen door softwarecoördinaten.
|
||||
|
||||
## Een nieuw scherm installeren
|
||||
|
||||
1. Identificeer USB-poort en bordvariant. Gebruik ESPHome 2026.6.2 met Python
|
||||
3.11–3.14. Controleer eerst of er lokale configuratie bestaat.
|
||||
2. Maak in een verse kopie met `tools/new_device.py` een eigen `device.yaml`,
|
||||
`calibration.yaml`, `secrets.yaml`. Overschrijf nooit een werkend profiel.
|
||||
Laat de eigenaar wifi lokaal invullen. Toon geen sleutels in logs/chat.
|
||||
3. Flash `device.yaml` met de CLI-substitutie `CALIBRATION_ON_BOOT=true`.
|
||||
Het geïsoleerde scherm heeft vijf kruisjes en werkt zonder HA.
|
||||
4. Volg docs/CALIBREREN.md: geleide USB-capture, fit, flash, nieuwe capture,
|
||||
onafhankelijke verify. Vraag fysieke tikken per doel. Wacht op bevestiging
|
||||
dat het meetscherm werkelijk zichtbaar is; een geslaagde build is geen flash.
|
||||
5. Flash zonder die override. Koppel de eigen HA via de ESPHome-integratie.
|
||||
Lees echte entity-ID's en ondersteunde attributen; verzin geen entiteiten.
|
||||
6. Configureer alle gebruikte tegels volgens docs/TEGELS.md. De vacuumkaart
|
||||
zit op positie 6. Posities 8/10 hebben geen volledige slider/climatebinding.
|
||||
Test geen echte apparaat-acties zonder toestemming van de eigenaar.
|
||||
7. Doorloop docs/ACCEPTATIE.md en rapporteer werkelijk uitgevoerde tests,
|
||||
beperkingen en de geobserveerde stabiliteitsduur.
|
||||
|
||||
## Code en regressies
|
||||
|
||||
- Houd basishardware en UI in `home-like-2432s028.yaml`; eigen gegevens horen
|
||||
in de genegeerde lokale profielen. `home-like.yaml` is een ander oud profiel.
|
||||
- Behoud vaste pagina's, verborgen navigatie bij maximaal zes tegels en
|
||||
minimaal 600 seconden standaard-standby. Geen vrij scrollen herintroduceren
|
||||
zonder touch-/navigatieregressies fysiek te testen.
|
||||
- Behoud touchfilter en eventguard vóór acties; kalibratie verwerkt gefilterde
|
||||
fysieke ADC-waarden. Zet de affine correctie niet dubbel in driver en UI.
|
||||
- Kalibratiewizard veronderstelt swap_xy=false, mirror_x=true, mirror_y=false
|
||||
en LVGL 90°. Een gewijzigde oriëntatie vereist ook nieuwe projectie/tests.
|
||||
- Run Python-tests, beide C++-tests en ESPHome-validatie/build bij codewijzigingen.
|
||||
Firmwaretests en hardwareacceptatie zijn verschillende controles.
|
||||
- `diagnostics/run_ui_test.py` rendert zonder HA-acties; raak tijdens die test
|
||||
het scherm niet aan. Gebruik `--name` voor de verwachte apparaatidentiteit.
|
||||
- Deel via `tools/export_bundle.py` of Git. Stage geen secrets, metingen,
|
||||
binaries, logs, buildcache of lokale apparaatprofielen.
|
||||
- Geen automatische firmware-upload naar een willekeurige aangesloten poort.
|
||||
Bij meerdere borden eerst de bedoelde poort vaststellen.
|
||||
|
||||
Historische diagnose is achtergrond, geen bewijs dat een nieuw paneel goed
|
||||
werkt. Maak tijdens onboarding geen claims over niet-uitgevoerde fysieke tests.
|
||||
@@ -0,0 +1,120 @@
|
||||
> Historisch onderzoek aan één paneel. Voor een nieuw scherm: [README.md](README.md)
|
||||
> en [de vijfpuntskalibratie](docs/CALIBREREN.md); neem geen meetwaarden over.
|
||||
|
||||
# ESP32-2432S028: stabiele bediening
|
||||
|
||||
Gebruik **home-like-2432s028.yaml** voor de aangesloten 2,8-inch CYD met
|
||||
ILI9341 en XPT2046. `home-like.yaml` hoort bij het andere ST7789V/GT911-bord.
|
||||
De wifi-, API- en OTA-geheimen blijven in `secrets.yaml`.
|
||||
|
||||
## Bediening
|
||||
|
||||
- Stel `TILE_COUNT` in op het aantal gebruikte tegels (1–10). Bij maximaal
|
||||
zes tegels verdwijnen beide paginaknoppen en de pagina-aanduiding; hogere
|
||||
tegelnummers zijn verborgen.
|
||||
- Pagina 1 bevat tegels 1–6; pagina 2 bevat 7–10. Gebruik de knoppen
|
||||
**Vorige** en **Volgende** onderaan. Slepen is uitgeschakeld.
|
||||
- Kort tikken voert de bestaande tegelactie uit. Lang indrukken opent de
|
||||
bestaande licht-, climate- of vacuumkaart, of de ingestelde lange actie.
|
||||
- Standby begint na tien minuten zonder aanraking (`AUTO_DIM_TIMEOUT: "600"`).
|
||||
- De eerste tik op het gedimde scherm maakt het scherm wakker.
|
||||
- Contacten korter dan 60 ms worden genegeerd. Eén aanraking kan maximaal
|
||||
één tegelactie geven; een verplaatsing groter dan 18 pixels annuleert de tik; dezelfde tegel wordt 600 ms tegen contactdender beschermd.
|
||||
- De thermostaat verstuurt de afgeronde, begrensde temperatuur die hij toont.
|
||||
Een lokale temperatuur- of moduskeuze krijgt drie seconden om met HA te
|
||||
synchroniseren. Een ingedrukte bediening voorkomt automatisch dimmen.
|
||||
- Pippa gebruikt de echte `vacuum.s8`-status, zodat docken, schoonmaken,
|
||||
pauzeren en terugkeren onderscheiden worden. Ontbrekende HA-statussen
|
||||
worden als `Niet beschikbaar` getoond.
|
||||
|
||||
## Technische keuzes
|
||||
|
||||
ESPHome 2026.6.2 met ESP-IDF; er zijn geen Arduino-componenten nodig.
|
||||
De displaybus draait op 40 MHz, zoals ESPHome's standaard CYD-profiel.
|
||||
XPT2046 wordt elke 20 ms uitgelezen zonder GPIO36-interrupt. De drukdrempel
|
||||
is 800, na een verdachte hoekmeting bij druk 422. Een lokale XPT2046-driver
|
||||
vereist drie consistente ADC-samples voor een nieuwe aanraking, negeert losse
|
||||
uitschieters en wacht twee drukloze samples af voor loslaten. Dit voegt circa
|
||||
40–60 ms toe bij aanraken en voorkomt dat een onstabiele eerste meting meteen
|
||||
als een andere knop wordt geïnterpreteerd. De driver staat onder `components/`. De geïnstalleerde
|
||||
interruptdriver probeert op die input-only pin een niet-beschikbare interne
|
||||
pull-up te activeren; de oude firmware logde hiervoor een GPIO-fout.
|
||||
|
||||
LVGL krijgt 12% tekenbuffer. Geen scrollanimaties en geen extra flitspuls na
|
||||
elke tik. Tegelachtergronden zijn ondoorzichtig. Updates binnen 100 ms worden
|
||||
samengevoegd; ongewijzigde tegellabels en kleuren worden niet opnieuw gezet.
|
||||
De UI-initialisatie draait pas nadat LVGL is gestart. De klok ververst direct
|
||||
bij tijdsynchronisatie. Roboto staat lokaal in `fonts/`, inclusief OFL-licentie.
|
||||
|
||||
De actieve indeling is 320×240. De oude, uitgecommentarieerde portretpresets
|
||||
zijn nog geen aangepaste paginalay-outs: reserveer bij een andere oriëntatie
|
||||
ook onderaan 34 pixels voor de navigatie en controleer de tegelposities.
|
||||
|
||||
## Bouwen en flashen
|
||||
|
||||
```sh
|
||||
esphome compile home-like-2432s028.yaml
|
||||
esphome upload home-like-2432s028.yaml --device /dev/cu.usbserial-130
|
||||
esphome logs home-like-2432s028.yaml --device /dev/cu.usbserial-130
|
||||
```
|
||||
|
||||
Sluit een lopende seriële loglezer voordat je flasht. De vorige configuratie
|
||||
staat lokaal in `diagnostics/before-2432s028.yaml.bak`; de bijbehorende vorige
|
||||
factorybinary in `diagnostics/before-2432s028.factory.bin`. Deze lokale backups
|
||||
worden niet in Git opgenomen. Teruggaan kan door de backup als configuratie
|
||||
te herstellen en opnieuw te bouwen/flashen; herstel de oude Arduino-toolchain
|
||||
als je de oude configuratie bouwt.
|
||||
|
||||
## Tests
|
||||
|
||||
```sh
|
||||
clang++ -std=c++17 -Wall -Wextra -Werror tests/test_cyd_ui.cpp -o /tmp/test_cyd_ui
|
||||
/tmp/test_cyd_ui
|
||||
python3 tests/test_layout.py
|
||||
clang++ -std=c++17 -Wall -Wextra -Werror tests/test_touch_filter.cpp -o /tmp/test_touch_filter
|
||||
/tmp/test_touch_filter
|
||||
```
|
||||
|
||||
De C++-test controleert contactdender, korte ruispulsen, opeenvolgende tegels,
|
||||
herhaling binnen één aanraking, `millis()`-overflow en temperatuurafronding.
|
||||
De indelingstest controleert overlap, schermgrenzen, ruimte voor navigatie
|
||||
en het filter vóór beide handlers van alle tien tegels.
|
||||
|
||||
Met de Python-omgeving van ESPHome (met aioesphomeapi, PyYAML en pyserial):
|
||||
|
||||
```sh
|
||||
python diagnostics/capture_serial.py --reset --seconds 120 > diagnostics/runtime.log
|
||||
python diagnostics/run_ui_test.py --host cyd-2432s028.local
|
||||
```
|
||||
|
||||
De API-test leest de encryptiesleutel intern uit `secrets.yaml` en drukt deze
|
||||
niet af. Hij wisselt tien keer van pagina en tekent de vijf overlays in elke
|
||||
ronde. Hij roept geen Home Assistant-acties aan. Raak het scherm tijdens
|
||||
deze circa 11 seconden niet aan. Dit is een teken-/geheugentest; hij vervangt
|
||||
geen fysieke toets van de touchkalibratie of een test van HA-service-effecten.
|
||||
|
||||
Diagnostiek logt iedere 30 seconden vrije heap en grootste vrije geheugenblok.
|
||||
`DEBUG` is tijdelijk nuttig voor touch- en verbindingsdiagnose. Voor dagelijks
|
||||
gebruik kan `logger.level` op `INFO` zodra de fysieke bediening goed is.
|
||||
|
||||
## Touchdiagnose
|
||||
|
||||
`python diagnostics/control_ui.py touch_diagnostics` opent een apart scherm
|
||||
met vier kruizen. Tijdens deze meting kunnen aanrakingen geen tegelactie
|
||||
uitvoeren. Tik nauwkeurig achtereenvolgens linksboven, rechtsboven,
|
||||
rechtsonder en linksonder. De ruwe en genormaliseerde coördinaten staan in
|
||||
de seriële logs met tag `touch`. Sluit af met
|
||||
`python diagnostics/control_ui.py end_touch_diagnostics`. De kruizen staan
|
||||
op (20,20), (299,20), (299,219) en (20,219) voor landschap 320×240.
|
||||
|
||||
De huidige kalibratie (X 225–3800, Y 274–3609) is afgeleid uit de drie
|
||||
herhaalde metingen rechtsboven en linksonder op dit fysieke bord. De eerdere
|
||||
metingen linksboven waren onstabiel en zijn bewust niet gebruikt om het
|
||||
hele scherm naar een verkeerde positie te verschuiven.
|
||||
|
||||
Daarbovenop corrigeert `set_raw_correction(...)` in `on_boot` de gemeten
|
||||
scheefstand. De fit gebruikt de gefilterde hoekmedianen (830,556), (544,3461),
|
||||
(3410,3391) en (3492,372). De vier medianen vallen na correctie binnen vijf
|
||||
pixels van hun meetdoel in de regressietest. Dit zijn bord-specifieke
|
||||
coëfficiënten; neem ze niet blind over op een ander paneel. De `raw=`-logs
|
||||
blijven de fysieke, gefilterde ADC-metingen tonen.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Adrian Kuehlewind
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,221 @@
|
||||
# CYD Home Assistant-bedieningsscherm
|
||||
|
||||
Van een nieuwe **ESP32-2432S028 met ILI9341 + XPT2046** naar een gekalibreerd
|
||||
Home Assistant-scherm, via USB. Met lichtbediening, scènes/scripts, climate,
|
||||
vacuum, maximaal tien tegels en vaste pagina's. Bij maximaal zes tegels
|
||||
verdwijnt de paginering. Standby begint standaard na tien minuten.
|
||||
|
||||
**Nieuw hier? Begin hieronder.** Je hoeft geen Home Assistant-token te maken
|
||||
of een ESPHome-add-on te installeren om via de computer te flashen.
|
||||
|
||||
## Met een LLM of developer werken
|
||||
|
||||
Geef de uitgepakte map en deze opdracht:
|
||||
|
||||
> Lees eerst AGENTS.md en README.md. Begeleid mij bij een nieuwe installatie
|
||||
> op een ESP32-2432S028 die via USB is aangesloten, met mijn eigen Home
|
||||
> Assistant. Controleer het bord, de seriële poort en de Python-omgeving.
|
||||
> Maak een lokaal device.yaml-profiel; gebruik geen bestaande persoonlijke
|
||||
> entiteiten of paneelkalibratie. Doorloop USB-kalibratie, HA-koppeling,
|
||||
> tegelconfiguratie en de acceptatietest. Vraag mij om fysieke tikken wanneer
|
||||
> nodig. Houd sleutels en wachtwoorden in lokale bestanden en rapporteer
|
||||
> welke controles echt zijn uitgevoerd.
|
||||
|
||||
De begeleider kan de code en logs lezen, maar kan het fysieke scherm niet
|
||||
vanzelf zien. Jij controleert het beeld en voert de gevraagde tikken uit.
|
||||
|
||||
## 1. Benodigdheden controleren
|
||||
|
||||
- Het bedoelde bord: ESP32-2432S028, 320×240, **ILI9341-display en resistieve
|
||||
XPT2046-touch**. Er bestaan borden met bijna dezelfde naam en een andere
|
||||
schermcontroller. Deze handleiding garandeert die varianten niet.
|
||||
- Een USB-**datakabel** en een computer met macOS, Linux of Windows.
|
||||
- Python **3.11–3.14**, internet voor de eerste build en enkele GB vrije ruimte.
|
||||
De versiegrens komt uit het geteste ESPHome-pakket; gebruik geen Python 3.15.
|
||||
- Een 2,4GHz-wifinetwerk; Home Assistant moet het bord via het netwerk kunnen
|
||||
bereiken. Houd het wifiwachtwoord lokaal beschikbaar.
|
||||
- Toegang tot Home Assistant om entiteiten te kiezen en ESPHome toe te voegen.
|
||||
|
||||
Gebruik bij voorkeur één aangesloten ESP-bord tegelijk. Noteer de variant en
|
||||
het MAC-adres uit de boot-/uploadlogs. `home-like.yaml` en `buttons.yaml` uit
|
||||
het oorspronkelijke project zijn **niet** de startersconfiguratie voor dit bord.
|
||||
|
||||
## 2. De werkomgeving installeren
|
||||
|
||||
Kloon [de repository](https://github.com/MaxGramser/homeassistant_espscreen)
|
||||
of pak de starter-ZIP uit. Open een terminal **in de codefolder**. Alle opdrachten hieronder
|
||||
worden vanuit die folder uitgevoerd.
|
||||
|
||||
macOS/Linux:
|
||||
|
||||
```sh
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Windows PowerShell:
|
||||
|
||||
```powershell
|
||||
py -3.13 -m venv .venv
|
||||
.\.venv\Scripts\Activate.ps1
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Als PowerShell activeren blokkeert, gebruik rechtstreeks
|
||||
`.\.venv\Scripts\python.exe` in plaats van `python`; een systeemwijziging is
|
||||
niet nodig. Controleer daarna:
|
||||
|
||||
```sh
|
||||
python --version
|
||||
python -m esphome version
|
||||
python -m serial.tools.list_ports
|
||||
```
|
||||
|
||||
Verwacht **ESPHome 2026.6.2**. Deze repo bevat een aangepaste touchdriver die
|
||||
met deze versie is getest. Upgrade niet stilzwijgend tijdens een installatie.
|
||||
Zie ook de [officiële ESPHome-installatiehandleiding](https://esphome.io/install/).
|
||||
|
||||
Noteer de poort: bijvoorbeeld `/dev/cu.usbserial-130` op macOS,
|
||||
`/dev/ttyUSB0` op Linux of `COM3` op Windows. Verderop staat `<USB_POORT>`;
|
||||
vervang dat steeds door de werkelijke poort, zonder punthaken.
|
||||
|
||||
## 3. Een eigen apparaatprofiel maken
|
||||
|
||||
Kies een unieke naam, bijvoorbeeld `display-keuken`:
|
||||
|
||||
```sh
|
||||
python tools/new_device.py --name display-keuken --friendly-name "Display keuken"
|
||||
```
|
||||
|
||||
Dit maakt drie lokale bestanden en overschrijft niets:
|
||||
|
||||
| Bestand | Wat je erin aanpast |
|
||||
|---|---|
|
||||
| `device.yaml` | Apparaatnaam, ruimte, tegels en gedrag |
|
||||
| `calibration.yaml` | Kalibratie van dit ene fysieke paneel; de wizard schrijft dit |
|
||||
| `secrets.yaml` | Wifi, API-encryptiesleutel, OTA- en fallbackwachtwoord |
|
||||
|
||||
De sleutels worden uniek gegenereerd. Vul in `secrets.yaml` alleen jouw
|
||||
`wifi_ssid` en `wifi_password` in. Laat de gegenereerde sleutels staan en
|
||||
bewaar dit bestand bij je eigen configuratie. Deel het niet mee in de ZIP.
|
||||
De API-encryptiesleutel heb je later nodig bij de HA-koppeling; kopieer die
|
||||
lokaal uit het bestand, niet via een openbare chat.
|
||||
|
||||
Bestaan deze bestanden al? Werk in een nieuwe kopie van de map voor een nieuw
|
||||
scherm. Wis de sleutels/kalibratie van een werkend scherm niet.
|
||||
|
||||
`device.yaml` importeert de basis en jouw kalibratie. Het overschrijft ook
|
||||
**alle verborgen tegelposities**, zodat die geen entiteiten van iemand anders
|
||||
blijven volgen. Begin met de zes voorbeeldtegels. `DIRECT_ACTIONS: "false"`
|
||||
voorkomt directe HA-aansturing tijdens de eerste installatie. Maak in deze
|
||||
fase nog geen HA-automations die op de actie-sensor reageren.
|
||||
|
||||
## 4. Eerste USB-flash en kalibratie
|
||||
|
||||
Controleer eerst de configuratie (ESPHome schermt secrets standaard af):
|
||||
|
||||
```sh
|
||||
python -m esphome config device.yaml
|
||||
```
|
||||
|
||||
Flash de kalibratiemodus, die zonder HA-verbinding werkt:
|
||||
|
||||
```sh
|
||||
python -m esphome -s CALIBRATION_ON_BOOT true run device.yaml --device <USB_POORT>
|
||||
```
|
||||
|
||||
De eerste build kan meerdere minuten duren. Na upload verschijnen vijf
|
||||
kruisjes op een donker scherm. `run` blijft logs tonen: stop **alleen de
|
||||
loglezer met Ctrl+C** voordat je de kalibratiewizard start. Het bord blijft aan.
|
||||
|
||||
Volg nu [docs/CALIBREREN.md](docs/CALIBREREN.md): per doel worden drie tikken
|
||||
verzameld, de vier hoeken bepalen de correctie en het midden is een
|
||||
onafhankelijke controle. Daarna flash je de uitkomst en meet je opnieuw.
|
||||
**Kopieer nooit de meetwaarden van een ander paneel.**
|
||||
|
||||
## 5. Home Assistant koppelen
|
||||
|
||||
Na een geslaagde kalibratie flash je de normale modus:
|
||||
|
||||
```sh
|
||||
python -m esphome run device.yaml --device <USB_POORT>
|
||||
```
|
||||
|
||||
Controleer in de logs de wifi-IP, stabiele boot en apparaatnaam. Ctrl+C sluit
|
||||
de loglezer. Het scherm moet nu de normale tegelpagina tonen.
|
||||
|
||||
1. Open in HA **Instellingen → Apparaten & diensten**.
|
||||
2. Kies het ontdekte ESPHome-apparaat, of **Integratie toevoegen → ESPHome**.
|
||||
3. Gebruik `display-keuken.local` of het IP-adres uit de logs; API-poort **6053**.
|
||||
4. Vul de `api_encryption_key` uit je lokale `secrets.yaml` in als daarom wordt gevraagd.
|
||||
5. Controleer of de apparaatnaam klopt en de status online wordt.
|
||||
6. Open de opties/configuratie van deze ESPHome-integratie en schakel
|
||||
**Allow the device to perform Home Assistant actions** in.
|
||||
|
||||
De precieze vertaling/plaats van de optie verschilt per HA-versie. Zie de
|
||||
[officiële integratiehandleiding](https://www.home-assistant.io/integrations/esphome).
|
||||
De HA-integratie verbindt met het bord; de ESPHome Device Builder/add-on is
|
||||
optioneel en is een andere functie.
|
||||
|
||||
## 6. Eigen tegels instellen
|
||||
|
||||
Volg [docs/TEGELS.md](docs/TEGELS.md). Verzamel de echte entity-ID's in HA,
|
||||
controleer ondersteunde attributen/acties en pas `device.yaml` aan. Gebruik
|
||||
korte titels. Als je alles hebt gecontroleerd:
|
||||
|
||||
```yaml
|
||||
substitutions:
|
||||
DIRECT_ACTIONS: "true"
|
||||
TILE_COUNT: "6"
|
||||
```
|
||||
|
||||
Dit zijn wijzigingen in de **bestaande** substitutions-map, geen tweede map
|
||||
onderaan het bestand. Bouw en flash opnieuw met `run device.yaml`. De tegels
|
||||
volgen daarna de werkelijke HA-status. Een actieve API-verbinding alleen
|
||||
bewijst nog niet dat een actie door HA is toegestaan.
|
||||
|
||||
## 7. Opleveren en testen
|
||||
|
||||
Voer [docs/ACCEPTATIE.md](docs/ACCEPTATIE.md) uit. Dat omvat een onafhankelijke
|
||||
touchmeting, beide pagina's indien aanwezig, lange druk, sliders, climate,
|
||||
vacuum, werkelijke HA-status en de juiste actie-effecten. De ingebouwde
|
||||
renderproef stuurt zelf geen HA-apparaten aan:
|
||||
|
||||
```sh
|
||||
python diagnostics/run_ui_test.py --host display-keuken.local --name display-keuken
|
||||
```
|
||||
|
||||
Bewaar bij deze persoon: `device.yaml`, `calibration.yaml`, `secrets.yaml`, de
|
||||
meetbestanden, de gebruikte ESPHome-versie en een kort opleverrapport.
|
||||
Voor later bijwerken kun je USB blijven gebruiken of OTA gebruiken:
|
||||
|
||||
```sh
|
||||
python -m esphome run device.yaml --device display-keuken.local
|
||||
```
|
||||
|
||||
Verander naam/API-sleutel niet zonder ook de HA-koppeling bij te werken.
|
||||
Bij storing: [docs/PROBLEMEN.md](docs/PROBLEMEN.md).
|
||||
|
||||
## Deze code aan de volgende persoon geven
|
||||
|
||||
Gebruik de exporteur in plaats van je hele werkmap te kopiëren:
|
||||
|
||||
```sh
|
||||
python tools/export_bundle.py --output dist/cyd-starter.zip
|
||||
```
|
||||
|
||||
De ZIP bevat de code, lokale fonts, licenties, handleidingen en neutrale
|
||||
voorbeelden. De basisdefaults worden voor de ontvanger geneutraliseerd.
|
||||
Geen `secrets.yaml`, eigen `device.yaml`, metingen, logs, `.git`, `.esphome`,
|
||||
Python-omgeving of firmwarebackups. De exporteur weigert een bestaande ZIP
|
||||
te overschrijven; kies bij een nieuwe versie een andere bestandsnaam.
|
||||
|
||||
De originele basis is van
|
||||
[akuehlewind/ESPHome-touch-display-mount](https://github.com/akuehlewind/ESPHome-touch-display-mount).
|
||||
Project-, ESPHome-driver- en fontlicenties staan respectievelijk in `LICENSE`,
|
||||
`components/xpt2046/LICENSE` en `fonts/`. Historische borddiagnose staat in
|
||||
[CYD_STABILITY.md](CYD_STABILITY.md); volg voor een **nieuwe** installatie de
|
||||
handleidingen hierboven.
|
||||
@@ -0,0 +1,68 @@
|
||||
# Testresultaten ESP32-2432S028 — 12 september 2026
|
||||
|
||||
Getest op de via USB aangesloten `cyd-2432s028`, ESP32 rev3.1, ESPHome
|
||||
2026.6.2 / ESP-IDF. De firmware meldt compilatietijd
|
||||
`2026-09-12 09:54:02 +0200` en is via USB geschreven en geverifieerd.
|
||||
|
||||
## Geslaagd
|
||||
|
||||
- C++-regressietests voor ruis, contactdender, verspringende aanrakingen,
|
||||
echte sleepbewegingen, temperatuurafronding en `millis()`-overflow.
|
||||
- Vijf indelings-/configuratietests voor schermgrenzen, overlap, modale
|
||||
lagen, voorwaardelijke paginering en guards vóór alle tegelhandlers.
|
||||
- ESPHome-validatie met `TILE_COUNT: 6`; navigatie is in de code aan
|
||||
`count > 6` gekoppeld en tegels boven het ingestelde aantal zijn verborgen.
|
||||
- Volledige ESP-IDF-build en USB-upload met verificatie van de geschreven data.
|
||||
- Verbinding met de bestaande Home Assistant-installatie en ontvangst van
|
||||
entiteitsstatussen.
|
||||
- **10 paginacontroles en 50 overlay-cycli op het echte apparaat.**
|
||||
Render-startteller steeg van 38 naar 101 tussen de eerste en
|
||||
laatste paginacontrole: de test controleert ook werkelijke tekenactiviteit.
|
||||
- Runtime-geometrie: tegelgebied `(0,40)–(319,203)`, knop Volgende
|
||||
`(211,206)–(310,237)`. De gebieden overlappen niet.
|
||||
- Vrije heap tijdens de tien testmetingen: 86,416–139,696 bytes
|
||||
volgens `esp_get_free_heap_size()`. Geen oplopend geheugenverlies of reset
|
||||
in deze korte tekenproef.
|
||||
|
||||
## Fysieke touchdiagnose
|
||||
|
||||
De gebruiker tikte herhaaldelijk de vier schermhoeken aan. De oorspronkelijke
|
||||
meting sprong linksboven soms meer dan honderd pixels weg. Na de driverfilter
|
||||
bleven de samples in herkenbare hoekgroepen. De overblijvende scheefstand is
|
||||
met een affine correctie op de hoekmedianen gecorrigeerd; de regressietest
|
||||
plaatst die medianen binnen vijf pixels van het doel. De bronwaarden en
|
||||
coëfficiënten staan in de configuratie, tests en CYD_STABILITY.md.
|
||||
|
||||
De gebruiker bevestigde na de laatste flash dat de juiste knoppen reageren
|
||||
en de bediening nu goed werkt.
|
||||
|
||||
## Grenzen
|
||||
|
||||
Dit is geen langdurige duurproef. Home Assistant-acties zijn niet automatisch
|
||||
uitgevoerd tijdens de schermtest. De gebruiker heeft de fysieke bediening na de laatste kalibratiecorrectie
|
||||
bevestigd: "Ja, dit werkt nu goed". De tien minuten
|
||||
standby zijn ingesteld; er is niet tien minuten gewacht om de volledige
|
||||
idle-timeout af te laten lopen.
|
||||
|
||||
Lokale ruwe logs staan in `diagnostics/acceptance-ui-test.log` en
|
||||
`diagnostics/acceptance-runtime.log` en worden niet in Git opgenomen.
|
||||
|
||||
## Overdraagbare installatie — aanvullende softwarecontrole
|
||||
|
||||
Op 12 september 2026 is de nieuwe onboarding getest:
|
||||
|
||||
- 17 Python-tests slagen: layout, kalibratie-fit, onafhankelijke verificatie,
|
||||
foutieve/ruisende metingen, unieke secrets, niet overschrijven en export.
|
||||
- Beide C++17-regressieprogramma's slagen (touchfilter en UI-guard).
|
||||
- Starter-ZIP uitgepakt in een verse tijdelijke map; nieuw profiel met eigen
|
||||
gegenereerde sleutels aangemaakt. Volledige ESPHome 2026.6.2 / ESP-IDF-build
|
||||
met kalibratie-opstartmodus geslaagd.
|
||||
- Gegenereerde C++ gecontroleerd op identiteitcorrectie voor het nieuwe bord.
|
||||
Het aparte lokale profiel behoudt de eerder gemeten zes correctiecoëfficiënten.
|
||||
- Git-index en bestaande historie gecontroleerd op de lokale credentials;
|
||||
geen overeenkomsten. Lokale profielen en secrets zijn uitgesloten.
|
||||
|
||||
De vijfpuntswizard is met synthetische meetgegevens getest. Hij is in deze
|
||||
ronde niet opnieuw fysiek uitgevoerd of naar het bestaande bord geflasht;
|
||||
voor ieder nieuw paneel blijft de fysieke acceptatie verplicht. De bovenstaande
|
||||
oudere hardwaretest betreft de eerder geïnstalleerde firmware.
|
||||
@@ -1,5 +1,13 @@
|
||||
> Nieuwe ESP32-2432S028-installatie: begin bij [README.md](README.md) en
|
||||
> [de actuele tegelhandleiding](docs/TEGELS.md). Hieronder staat ook legacygedrag.
|
||||
|
||||
# Tile Configuration Reference
|
||||
|
||||
For the ESP32-2432S028 profile, use `home-like-2432s028.yaml` and
|
||||
[CYD_STABILITY.md](CYD_STABILITY.md). That profile uses fixed pages instead of
|
||||
scrolling, hides pagination when `TILE_COUNT <= 6`, and dims after ten minutes.
|
||||
The scrolling description below applies to `home-like.yaml`.
|
||||
|
||||
This document explains every substitution in `home-like.yaml` and provides copy-paste examples for all supported entity types.
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Identiteit: startpunt voor een NIEUW paneel, geen gemeten kalibratie.
|
||||
# tools/calibrate.py schrijft een gemeten versie van dit bestand.
|
||||
substitutions:
|
||||
TOUCH_CAL_X_MIN: "280"
|
||||
TOUCH_CAL_X_MAX: "3860"
|
||||
TOUCH_CAL_Y_MIN: "340"
|
||||
TOUCH_CAL_Y_MAX: "3860"
|
||||
TOUCH_AFFINE_XX: "1.000000000"
|
||||
TOUCH_AFFINE_XY: "0.000000000"
|
||||
TOUCH_AFFINE_XC: "0.000000000"
|
||||
TOUCH_AFFINE_YX: "0.000000000"
|
||||
TOUCH_AFFINE_YY: "1.000000000"
|
||||
TOUCH_AFFINE_YC: "0.000000000"
|
||||
@@ -0,0 +1,709 @@
|
||||
# ESPHome License
|
||||
|
||||
Copyright (c) 2019 ESPHome
|
||||
|
||||
The ESPHome License is made up of two base licenses: MIT and the GNU GENERAL PUBLIC LICENSE.
|
||||
The C++/runtime codebase of the ESPHome project (file extensions .c, .cpp, .h, .hpp, .tcc, .ino) are
|
||||
published under the GPLv3 license. The python codebase and all other parts of this codebase are
|
||||
published under the MIT license.
|
||||
|
||||
Both MIT and GPLv3 licenses are attached to this document.
|
||||
|
||||
## MIT License
|
||||
|
||||
Copyright (c) 2019 ESPHome
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
## GPLv3 License
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
@@ -0,0 +1,21 @@
|
||||
# XPT2046 input stabilization
|
||||
|
||||
Based on ESPHome 2026.6.2's `esphome/components/xpt2046` (license alongside).
|
||||
The sample handoff in `touchscreen/xpt2046.cpp` and filter/calibration members
|
||||
in `touchscreen/xpt2046.h` differ from upstream. `touch_filter.h` is local.
|
||||
|
||||
The installed CYD produced widely separated coordinates at first contact.
|
||||
The filter requires three samples within 100 ADC units before reporting a
|
||||
new contact, ignores isolated large jumps, and requires two absent-pressure
|
||||
samples before reporting release. Normal continuous drags use a small
|
||||
moving average; large movements are accepted after three stable samples.
|
||||
|
||||
Use polling (20 ms in the device configuration), not an IRQ-only setup: the
|
||||
filter needs follow-up samples while confirming a contact or release.
|
||||
Run `tests/test_touch_filter.cpp` when changing the filter or updating ESPHome.
|
||||
|
||||
An optional affine raw-coordinate correction defaults to the identity.
|
||||
The device profile sets its six coefficients at boot, fitted to median
|
||||
filtered corner readings from this particular panel. Raw diagnostic getters
|
||||
still expose the original filtered ADC values, allowing recalibration without
|
||||
mistaking corrected coordinates for physical measurements.
|
||||
@@ -0,0 +1,5 @@
|
||||
import esphome.config_validation as cv
|
||||
|
||||
CONFIG_SCHEMA = cv.invalid(
|
||||
"This component sould now be used as platform of the Touchscreen component."
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import spi, touchscreen
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_THRESHOLD
|
||||
|
||||
CODEOWNERS = ["@numo68", "@nielsnl68"]
|
||||
DEPENDENCIES = ["spi"]
|
||||
|
||||
XPT2046_ns = cg.esphome_ns.namespace("xpt2046")
|
||||
XPT2046Component = XPT2046_ns.class_(
|
||||
"XPT2046Component",
|
||||
touchscreen.Touchscreen,
|
||||
spi.SPIDevice,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
touchscreen.touchscreen_schema(calibration_required=True)
|
||||
.extend(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(XPT2046Component),
|
||||
cv.Optional(CONF_INTERRUPT_PIN): cv.All(
|
||||
pins.internal_gpio_input_pin_schema
|
||||
),
|
||||
cv.Optional(CONF_THRESHOLD, default=400): cv.int_range(min=0, max=4095),
|
||||
},
|
||||
)
|
||||
)
|
||||
.extend(spi.spi_device_schema()),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await spi.register_spi_device(var, config)
|
||||
await touchscreen.register_touchscreen(var, config)
|
||||
|
||||
cg.add(var.set_threshold(config[CONF_THRESHOLD]))
|
||||
|
||||
if CONF_INTERRUPT_PIN in config:
|
||||
pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN])
|
||||
cg.add(var.set_irq_pin(pin))
|
||||
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
#include <cstdlib>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::xpt2046 {
|
||||
// Optional affine calibration corrects skew as well as scale/offset.
|
||||
// Identity by default; coefficients belong to an individual physical panel.
|
||||
class RawCorrection {
|
||||
public:
|
||||
void set(float xx, float xy, float xc, float yx, float yy, float yc) {
|
||||
xx_ = xx; xy_ = xy; xc_ = xc; yx_ = yx; yy_ = yy; yc_ = yc;
|
||||
}
|
||||
int16_t x(int raw_x, int raw_y) const { return bound(xx_ * raw_x + xy_ * raw_y + xc_); }
|
||||
int16_t y(int raw_x, int raw_y) const { return bound(yx_ * raw_x + yy_ * raw_y + yc_); }
|
||||
private:
|
||||
static int16_t bound(float v) { return static_cast<int16_t>(std::clamp(std::lround(v), 0L, 4095L)); }
|
||||
float xx_{1}, xy_{0}, xc_{0}, yx_{0}, yy_{1}, yc_{0};
|
||||
};
|
||||
|
||||
// XPT2046 ADC values can be wildly wrong as a finger first contacts/releases
|
||||
// the resistive sheet. Do not expose a lone sample as a new LVGL target.
|
||||
class TouchFilter {
|
||||
public:
|
||||
enum Result { HOLD, POSITION, RELEASE };
|
||||
Result sample(bool pressed, int16_t raw_x, int16_t raw_y) {
|
||||
if (!pressed) {
|
||||
candidate_count_ = 0;
|
||||
if (active_ && ++release_count_ < 2) return HOLD;
|
||||
active_ = false;
|
||||
release_count_ = 0;
|
||||
return RELEASE;
|
||||
}
|
||||
release_count_ = 0;
|
||||
if (active_ && near(raw_x, raw_y, x_, y_, 300)) {
|
||||
candidate_count_ = 0;
|
||||
x_ = (x_ + raw_x) / 2;
|
||||
y_ = (y_ + raw_y) / 2;
|
||||
return POSITION;
|
||||
}
|
||||
// Initial contact or a sudden jump: require three consistent samples.
|
||||
if (!candidate_count_ || !near(raw_x, raw_y, candidate_x_, candidate_y_, 100)) {
|
||||
candidate_x_ = raw_x;
|
||||
candidate_y_ = raw_y;
|
||||
candidate_count_ = 1;
|
||||
return HOLD;
|
||||
}
|
||||
candidate_x_ = (candidate_x_ * candidate_count_ + raw_x) / (candidate_count_ + 1);
|
||||
candidate_y_ = (candidate_y_ * candidate_count_ + raw_y) / (candidate_count_ + 1);
|
||||
if (++candidate_count_ < 3) return HOLD;
|
||||
x_ = candidate_x_;
|
||||
y_ = candidate_y_;
|
||||
active_ = true;
|
||||
candidate_count_ = 0;
|
||||
return POSITION;
|
||||
}
|
||||
int16_t x() const { return x_; }
|
||||
int16_t y() const { return y_; }
|
||||
private:
|
||||
static bool near(int x1, int y1, int x2, int y2, int distance) {
|
||||
return std::abs(x1 - x2) <= distance && std::abs(y1 - y2) <= distance;
|
||||
}
|
||||
int16_t x_{0}, y_{0}, candidate_x_{0}, candidate_y_{0};
|
||||
uint8_t candidate_count_{0}, release_count_{0};
|
||||
bool active_{false};
|
||||
};
|
||||
} // namespace esphome::xpt2046
|
||||
@@ -0,0 +1,117 @@
|
||||
#include "xpt2046.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace esphome::xpt2046 {
|
||||
|
||||
static const char *const TAG = "xpt2046";
|
||||
|
||||
void XPT2046Component::setup() {
|
||||
if (this->irq_pin_ != nullptr) {
|
||||
// The pin reports a touch with a falling edge. Unfortunately the pin goes also changes state
|
||||
// while the channels are read and wiring it as an interrupt is not straightforward and would
|
||||
// need careful masking. A GPIO poll is cheap so we'll just use that.
|
||||
|
||||
this->irq_pin_->setup(); // INPUT
|
||||
this->irq_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
|
||||
this->irq_pin_->setup();
|
||||
this->attach_interrupt_(this->irq_pin_, gpio::INTERRUPT_FALLING_EDGE);
|
||||
}
|
||||
this->spi_setup();
|
||||
this->read_adc_(0xD0); // ADC powerdown, enable PENIRQ pin
|
||||
}
|
||||
|
||||
void XPT2046Component::update_touches() {
|
||||
int16_t data[6], x_raw, y_raw, z_raw;
|
||||
bool touch = false;
|
||||
|
||||
enable();
|
||||
|
||||
int16_t touch_pressure_1 = this->read_adc_(0xB1 /* touch_pressure_1 */);
|
||||
int16_t touch_pressure_2 = this->read_adc_(0xC1 /* touch_pressure_2 */);
|
||||
z_raw = touch_pressure_1 + 0xfff - touch_pressure_2;
|
||||
ESP_LOGVV(TAG, "Touchscreen Update z = %d", z_raw);
|
||||
touch = (z_raw >= this->threshold_);
|
||||
if (touch) {
|
||||
read_adc_(0xD1 /* X */); // dummy Y measure, 1st is always noisy
|
||||
data[0] = this->read_adc_(0x91 /* Y */);
|
||||
data[1] = this->read_adc_(0xD1 /* X */); // make 3 x-y measurements
|
||||
data[2] = this->read_adc_(0x91 /* Y */);
|
||||
data[3] = this->read_adc_(0xD1 /* X */);
|
||||
data[4] = this->read_adc_(0x91 /* Y */);
|
||||
}
|
||||
|
||||
data[5] = this->read_adc_(0xD0 /* X */); // Last X touch power down
|
||||
|
||||
disable();
|
||||
|
||||
if (touch) {
|
||||
x_raw = best_two_avg(data[1], data[3], data[5]);
|
||||
y_raw = best_two_avg(data[0], data[2], data[4]);
|
||||
|
||||
ESP_LOGD(TAG, "Touchscreen Update [%d, %d], z = %d", x_raw, y_raw, z_raw);
|
||||
|
||||
if (this->filter_.sample(true, x_raw, y_raw) == TouchFilter::POSITION) {
|
||||
this->add_raw_touch_position_(0,
|
||||
this->correction_.x(this->filter_.x(), this->filter_.y()),
|
||||
this->correction_.y(this->filter_.x(), this->filter_.y()), z_raw);
|
||||
} else {
|
||||
this->skip_update_ = true;
|
||||
}
|
||||
} else if (this->filter_.sample(false, 0, 0) == TouchFilter::HOLD) {
|
||||
this->skip_update_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void XPT2046Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"XPT2046:\n"
|
||||
" X min: %d\n"
|
||||
" X max: %d\n"
|
||||
" Y min: %d\n"
|
||||
" Y max: %d\n"
|
||||
" Swap X/Y: %s\n"
|
||||
" Invert X: %s\n"
|
||||
" Invert Y: %s\n"
|
||||
" threshold: %d",
|
||||
this->x_raw_min_, this->x_raw_max_, this->y_raw_min_, this->y_raw_max_, YESNO(this->swap_x_y_),
|
||||
YESNO(this->invert_x_), YESNO(this->invert_y_), this->threshold_);
|
||||
LOG_PIN(" IRQ Pin: ", this->irq_pin_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
// float XPT2046Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
int16_t XPT2046Component::best_two_avg(int16_t value1, int16_t value2, int16_t value3) {
|
||||
int16_t delta_a, delta_b, delta_c;
|
||||
int16_t reta = 0;
|
||||
|
||||
delta_a = (value1 > value2) ? value1 - value2 : value2 - value1;
|
||||
delta_b = (value1 > value3) ? value1 - value3 : value3 - value1;
|
||||
delta_c = (value3 > value2) ? value3 - value2 : value2 - value3;
|
||||
|
||||
if (delta_a <= delta_b && delta_a <= delta_c) {
|
||||
reta = (value1 + value2) >> 1;
|
||||
} else if (delta_b <= delta_a && delta_b <= delta_c) {
|
||||
reta = (value1 + value3) >> 1;
|
||||
} else {
|
||||
reta = (value2 + value3) >> 1;
|
||||
}
|
||||
|
||||
return reta;
|
||||
}
|
||||
|
||||
int16_t XPT2046Component::read_adc_(uint8_t ctrl) { // NOLINT
|
||||
uint8_t data[2];
|
||||
|
||||
this->write_byte(ctrl);
|
||||
delay(1);
|
||||
data[0] = this->read_byte();
|
||||
data[1] = this->read_byte();
|
||||
|
||||
return ((data[0] << 8) | data[1]) >> 3;
|
||||
}
|
||||
|
||||
} // namespace esphome::xpt2046
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
#include "touch_filter.h"
|
||||
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/components/touchscreen/touchscreen.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::xpt2046 {
|
||||
|
||||
using namespace touchscreen;
|
||||
|
||||
class XPT2046Component : public Touchscreen,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
|
||||
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_2MHZ> {
|
||||
public:
|
||||
void set_raw_correction(float xx, float xy, float xc, float yx, float yy, float yc) {
|
||||
correction_.set(xx, xy, xc, yx, yy, yc);
|
||||
}
|
||||
int16_t filtered_raw_x() const { return filter_.x(); }
|
||||
int16_t filtered_raw_y() const { return filter_.y(); }
|
||||
/// Set the threshold for the touch detection.
|
||||
void set_threshold(int16_t threshold) { this->threshold_ = threshold; }
|
||||
/// Set the pin used to detect the touch.
|
||||
void set_irq_pin(InternalGPIOPin *pin) { this->irq_pin_ = pin; }
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
// float get_setup_priority() const override;
|
||||
|
||||
protected:
|
||||
TouchFilter filter_;
|
||||
RawCorrection correction_;
|
||||
static int16_t best_two_avg(int16_t value1, int16_t value2, int16_t value3);
|
||||
|
||||
int16_t read_adc_(uint8_t ctrl);
|
||||
|
||||
void update_touches() override;
|
||||
|
||||
int16_t threshold_;
|
||||
|
||||
InternalGPIOPin *irq_pin_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace esphome::xpt2046
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
namespace cyd {
|
||||
// Work in tenths of a degree and always clamp AFTER rounding.
|
||||
inline int quantize_temperature(int value, int minimum, int maximum, int step) {
|
||||
step = std::max(1, step);
|
||||
value = std::clamp(value, minimum, maximum);
|
||||
const int snapped = minimum + static_cast<int>(std::lround(
|
||||
static_cast<double>(value - minimum) / step)) * step;
|
||||
return std::clamp(snapped, minimum, maximum);
|
||||
}
|
||||
|
||||
class TouchGuard {
|
||||
public:
|
||||
void begin(uint32_t now, int x = 0, int y = 0) {
|
||||
started_ = now;
|
||||
accepted_ = false;
|
||||
moved_ = false;
|
||||
start_x_ = x;
|
||||
start_y_ = y;
|
||||
}
|
||||
void update(int x, int y) {
|
||||
// A contact that jumps to another target must never become a tile tap.
|
||||
if (std::abs(x - start_x_) > 18 || std::abs(y - start_y_) > 18) moved_ = true;
|
||||
}
|
||||
bool accept(uint32_t now, int tile) {
|
||||
if (accepted_ || moved_ || now - started_ < 60) return false;
|
||||
if (has_previous_ && tile == previous_tile_ && now - previous_ < 600) return false;
|
||||
accepted_ = true;
|
||||
has_previous_ = true;
|
||||
previous_tile_ = tile;
|
||||
previous_ = now;
|
||||
return true;
|
||||
}
|
||||
private:
|
||||
uint32_t started_{0}, previous_{0};
|
||||
int previous_tile_{-1};
|
||||
int start_x_{0}, start_y_{0};
|
||||
bool moved_{false};
|
||||
bool accepted_{false}, has_previous_{false};
|
||||
};
|
||||
inline TouchGuard touch_guard;
|
||||
} // namespace cyd
|
||||
@@ -0,0 +1,215 @@
|
||||
# Kopieer via: python tools/new_device.py --name display-keuken
|
||||
# Bewerk alleen dit lokale profiel en calibration.yaml voor jouw scherm.
|
||||
packages:
|
||||
display: !include home-like-2432s028.yaml
|
||||
calibration: !include calibration.yaml
|
||||
|
||||
substitutions:
|
||||
DEVICE_NAME: "cyd-new"
|
||||
DEVICE_FRIENDLY_NAME: "Mijn CYD"
|
||||
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,19 @@
|
||||
"""Capture ESP32 boot/runtime diagnostics. Requires pyserial (in ESPHome's venv)."""
|
||||
import argparse
|
||||
import time
|
||||
import serial
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--port', default='/dev/cu.usbserial-130')
|
||||
parser.add_argument('--seconds', type=int, default=90)
|
||||
parser.add_argument('--reset', action='store_true')
|
||||
args = parser.parse_args()
|
||||
with serial.Serial(args.port, 115200, timeout=0.2) as port:
|
||||
if args.reset:
|
||||
port.dtr = False
|
||||
port.rts = True
|
||||
time.sleep(0.1)
|
||||
port.rts = False
|
||||
end = time.monotonic() + args.seconds
|
||||
while time.monotonic() < end:
|
||||
print(port.read(4096).decode('utf-8', errors='replace'), end='', flush=True)
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Open/close the isolated touch diagnostic screen via encrypted ESPHome API."""
|
||||
import argparse
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
from aioesphomeapi import APIClient, LogLevel
|
||||
|
||||
async def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('action', choices=['touch_diagnostics', 'end_touch_diagnostics', 'ui_state'])
|
||||
parser.add_argument('--host', default='cyd-2432s028.local')
|
||||
parser.add_argument('--name', default='cyd-2432s028', help='Verwachte DEVICE_NAME')
|
||||
parser.add_argument('--secrets', type=Path, default=Path(__file__).resolve().parents[1] / 'secrets.yaml')
|
||||
args = parser.parse_args()
|
||||
secret = yaml.safe_load(args.secrets.read_text())
|
||||
client = APIClient(args.host, 6053, noise_psk=secret['api_encryption_key'], expected_name=args.name, client_info='CYD touch diagnostics')
|
||||
await client.connect(login=True)
|
||||
try:
|
||||
def log(msg):
|
||||
text = msg.message.decode(errors='replace') if isinstance(msg.message, bytes) else msg.message
|
||||
if 'ui_diag' in text:
|
||||
print(text, flush=True)
|
||||
client.subscribe_logs(log, log_level=LogLevel.LOG_LEVEL_DEBUG, dump_config=False)
|
||||
_, services = await client.list_entities_services()
|
||||
await client.execute_service(next(s for s in services if s.name == args.action), {})
|
||||
await asyncio.sleep(0.5)
|
||||
print(args.action + ': sent')
|
||||
finally:
|
||||
await client.disconnect()
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Run the firmware's render test through its encrypted API; no HA actions."""
|
||||
import argparse
|
||||
import asyncio
|
||||
import re
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
from aioesphomeapi import APIClient, LogLevel
|
||||
|
||||
async def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--host', default='cyd-2432s028.local')
|
||||
parser.add_argument('--name', default='cyd-2432s028', help='Verwachte DEVICE_NAME')
|
||||
parser.add_argument('--secrets', type=Path, default=Path(__file__).resolve().parents[1] / 'secrets.yaml')
|
||||
args = parser.parse_args()
|
||||
secrets = yaml.safe_load(args.secrets.read_text())
|
||||
client = APIClient(args.host, 6053, noise_psk=secrets['api_encryption_key'], client_info='CYD UI diagnostics', expected_name=args.name)
|
||||
await client.connect(login=True)
|
||||
try:
|
||||
info = await client.device_info()
|
||||
print(f'Connected: {info.name}, ESPHome {info.esphome_version}, compiled {info.compilation_time}', flush=True)
|
||||
_, services = await client.list_entities_services()
|
||||
test = next(s for s in services if s.name == 'ui_self_test')
|
||||
done = asyncio.Event()
|
||||
failures = []
|
||||
passes = []
|
||||
frames = []
|
||||
def log(msg):
|
||||
text = msg.message.decode(errors='replace') if isinstance(msg.message, bytes) else msg.message
|
||||
if any(tag in text for tag in ('ui_test', 'UI_TEST', '[health', 'took a long')):
|
||||
print(text, flush=True)
|
||||
if 'page2=PASS' in text or 'page_check=PASS' in text:
|
||||
passes.append(text)
|
||||
match = re.search(r'frames=(\d+)', text)
|
||||
if match: frames.append(int(match[1]))
|
||||
if 'FAIL' in text: failures.append(text)
|
||||
if 'UI_TEST COMPLETE' in text: done.set()
|
||||
client.subscribe_logs(log, log_level=LogLevel.LOG_LEVEL_DEBUG, dump_config=False)
|
||||
await client.execute_service(test, {})
|
||||
await asyncio.wait_for(done.wait(), timeout=45)
|
||||
assert len(passes) == 10, f'Expected 10 page checks, got {len(passes)}'
|
||||
assert not failures, failures
|
||||
assert len(frames) == 10 and frames[-1] - frames[0] >= 40, f'Render callbacks stalled: {frames}'
|
||||
print('PASS: 10 page checks and 50 overlay render cycles completed', flush=True)
|
||||
finally:
|
||||
await client.disconnect()
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,81 @@
|
||||
# Acceptatietest en oplevering
|
||||
|
||||
Noteer datum, bordvariant/MAC, Git-commit, ESPHome-versie, apparaatnaam en de
|
||||
namen van de twee meetbestanden. Neem geen wachtwoorden in het rapport op.
|
||||
|
||||
## Zonder echte HA-acties
|
||||
|
||||
1. Kalibreer met [CALIBREREN.md](CALIBREREN.md); flash de uitkomst en voer een
|
||||
nieuwe, onafhankelijke meting met `verify` uit. Een geslaagde fit alleen
|
||||
bewijst niet dat de firmwarecoördinaten kloppen.
|
||||
2. Flash de normale modus. Controleer dat het meetscherm verdwijnt.
|
||||
3. Controleer elk zichtbaar vak en de randen: geen andere tegel mag reageren.
|
||||
Tik rustig en snel achter elkaar, maar verwacht geen herhaling binnen de
|
||||
bewuste debounceperiode. Een veeg mag geen tegelactie achteraf veroorzaken.
|
||||
4. Bij 1–6 tegels: geen paginaknop. Bij 7–10: herhaal minstens twintig keer
|
||||
Volgende → tegel → Vorige. De onderste tegel mag niet reageren op de
|
||||
paginaknop. Open/sluit overlays; een tik daarin mag niet doorlekken.
|
||||
5. Draai de ingebouwde renderproef via de versleutelde API:
|
||||
|
||||
```sh
|
||||
python diagnostics/run_ui_test.py --host display-keuken.local --name display-keuken
|
||||
```
|
||||
|
||||
Vereist tien geslaagde paginacontroles, vijftig overlaycycli en voortgang
|
||||
van echte draw-callbacks. Dit test geen fysieke touch of HA-acties.
|
||||
6. Laat het bord minstens 15 minuten aan. Controleer dat het niet herstart,
|
||||
dat standby pas na tien minuten zonder aanraking begint en dat de eerste
|
||||
tik het scherm wekt zonder een apparaat te schakelen.
|
||||
|
||||
## Met Home Assistant
|
||||
|
||||
Schakel directe acties pas nu in, flash opnieuw en kies veilige testapparaten.
|
||||
Controleer elke geconfigureerde tegel met zijn echte HA-entiteit:
|
||||
|
||||
- Een tik voert precies één bedoelde actie uit; de terugmelding klopt.
|
||||
- Veranderen vanuit HA wordt ook op het scherm zichtbaar.
|
||||
- Lichtslider/kleur, climate-setpoint en ondersteunde modi werken waar gebruikt.
|
||||
- Vacuumacties en snelheden werken waar ondersteund. Noteer ontbrekende functies.
|
||||
- Na stroomonderbreking komen wifi en HA terug; naam, kalibratie en tegels blijven.
|
||||
- Na een korte wifi-/HA-onderbreking herstelt de verbinding zonder USB-flash.
|
||||
|
||||
Bewaar logs alleen lokaal. Controleer op resets, watchdogmeldingen, herhaalde
|
||||
connectiefouten en opvallend trage componenten. Een korte renderproef bewijst
|
||||
geen stabiliteit over dagen; noteer de werkelijk geobserveerde duur.
|
||||
|
||||
## Geautomatiseerde ontwikkelchecks
|
||||
|
||||
Vanuit de repo, met de Python-omgeving actief:
|
||||
|
||||
```sh
|
||||
python -m unittest discover -s tests -p 'test_*.py'
|
||||
c++ -std=c++17 -Wall -Wextra -pedantic tests/test_cyd_ui.cpp -o /tmp/test_cyd_ui
|
||||
/tmp/test_cyd_ui
|
||||
c++ -std=c++17 -Wall -Wextra -pedantic tests/test_touch_filter.cpp -o /tmp/test_touch_filter
|
||||
/tmp/test_touch_filter
|
||||
python -m esphome config device.yaml
|
||||
python -m esphome compile device.yaml
|
||||
```
|
||||
|
||||
De C++-commando's hierboven zijn voor macOS/Linux met een compiler. Op Windows
|
||||
kan de developer een beschikbare C++17-toolchain gebruiken. Flash alleen het
|
||||
gecontroleerde profiel naar het geïdentificeerde bord.
|
||||
|
||||
## Opleverrapport
|
||||
|
||||
```text
|
||||
Bord / MAC:
|
||||
Git-commit / ESPHome-versie:
|
||||
Apparaatnaam / HA-entiteiten:
|
||||
Kalibratiemeting + onafhankelijke verificatie:
|
||||
Renderproef:
|
||||
Fysieke paginering/touch/standby:
|
||||
Werkelijke HA-acties:
|
||||
Geobserveerde stabiele duur:
|
||||
Beperkingen / nog niet getest:
|
||||
Lokale profielbestanden veilig bewaard:
|
||||
```
|
||||
|
||||
Geef de eigenaar de drie lokale YAML-bestanden en de meetbestanden via een
|
||||
passend privé-kanaal. Gebruik voor een andere eigenaar de schone repository
|
||||
of starter-ZIP, zodat die eigen sleutels en eigen paneelkalibratie krijgt.
|
||||
@@ -0,0 +1,127 @@
|
||||
# Een nieuw paneel kalibreren
|
||||
|
||||
Dit protocol is voor **320×240, LVGL-rotatie 90°, swap_xy=false,
|
||||
mirror_x=true, mirror_y=false**. Andere oriëntaties, GT911-touch en andere
|
||||
controllers vereisen een apart profiel. Verander rotatie of mirroring niet
|
||||
halverwege de meting.
|
||||
|
||||
De filter in deze repo stabiliseert de XPT2046-metingen. Kalibratie corrigeert
|
||||
vervolgens offset, schaal en scheefstand. Kalibratie repareert geen defect
|
||||
paneel, losse verbinding, instabiele voeding of willekeurige uitschieters.
|
||||
|
||||
## Voorbereiden
|
||||
|
||||
1. Gebruik `device.yaml` van `tools/new_device.py`, met een eigen
|
||||
`calibration.yaml` die begint als identiteit. De persoonlijke correctie
|
||||
in het historische basisprofiel mag niet worden overgenomen.
|
||||
2. Controleer de datakabel en de USB-poort met `python -m serial.tools.list_ports`.
|
||||
3. Flash vanaf de reporoot:
|
||||
|
||||
```sh
|
||||
python -m esphome -s CALIBRATION_ON_BOOT true run device.yaml --device <USB_POORT>
|
||||
```
|
||||
|
||||
4. Controleer fysiek het **donkere scherm met vijf witte + tekens**.
|
||||
Het blijft aan en de tegelacties zijn hier niet toegankelijk.
|
||||
5. Stop de ESPHome-loglezer met Ctrl+C. Laat het bord en de USB-kabel zitten.
|
||||
Slechts één programma mag tegelijk de seriële poort gebruiken.
|
||||
|
||||
Wifi/HA zijn voor deze USB-route niet nodig. Als het bord al op wifi staat,
|
||||
kan het scherm ook via de versleutelde API worden geopend:
|
||||
|
||||
```sh
|
||||
python diagnostics/control_ui.py touch_diagnostics --host display-keuken.local --name display-keuken
|
||||
```
|
||||
|
||||
Dat verandert de startmodus niet. Gebruik voor een nieuwe installatie de
|
||||
USB-route hierboven, zodat de kalibratiemodus ook na een reset actief blijft.
|
||||
|
||||
## Meting A: gegevens verzamelen
|
||||
|
||||
```sh
|
||||
python tools/calibrate.py capture --port <USB_POORT> --device-name display-keuken --output measurements-before.json
|
||||
```
|
||||
|
||||
De wizard begeleidt **één doel tegelijk**:
|
||||
|
||||
1. Lees in de terminal welk kruisje aan de beurt is.
|
||||
2. Druk op Enter en tik daarna alleen dat kruisje **drie keer**.
|
||||
3. Houd elke tik circa een halve seconde vast en laat tussendoor volledig los.
|
||||
4. Wacht op de volgende terminalprompt. Ga niet zelf alvast naar een andere hoek.
|
||||
|
||||
Volgorde: linksboven, rechtsboven, rechtsonder, linksonder, midden.
|
||||
Gebruik een geschikte stompe stylus of een kleine, bewuste vingertik; geen
|
||||
scherpe punt. De doelen staan op `(20,20)`, `(299,20)`, `(299,219)`, `(20,219)`
|
||||
en `(160,120)`.
|
||||
|
||||
De wizard leest alleen touchlogs met `calibration=1`. Hij flasht niets, maakt
|
||||
geen netwerkverbinding en stuurt geen HA-acties. Een onvolledige meting wordt
|
||||
niet als geldig bestand opgeslagen. Bij een verkeerde tik: Ctrl+C en opnieuw
|
||||
beginnen. Gebruik een nieuwe bestandsnaam als een meting al bestaat.
|
||||
|
||||
## Een correctie berekenen
|
||||
|
||||
```sh
|
||||
python tools/calibrate.py fit measurements-before.json --output calibration.yaml --replace
|
||||
```
|
||||
|
||||
Het script:
|
||||
|
||||
- neemt per hoek de mediaan van de drie ruwe ADC-metingen;
|
||||
- bepaalt een affine correctie uit de vier hoeken;
|
||||
- controleert het **niet voor de fit gebruikte middenpunt**;
|
||||
- weigert te grote fouten (>12 px), spreiding (>18 px) en onbruikbare meetgeometrie;
|
||||
- bewaart de bestaande `calibration.yaml` als een gedateerde `.bak` voordat
|
||||
een nieuwe, complete versie wordt geplaatst.
|
||||
|
||||
De output bevat alleen de vier basisgrenzen en zes affine coëfficiënten in
|
||||
een ESPHome-substitutions-map. `device.yaml` importeert deze als calibration-
|
||||
package. Verander de coëfficiënten niet op gevoel en voeg geen tweede correctie
|
||||
in de driver toe. De `raw=`-logs bevatten de gefilterde **fysieke ADC-waarden**,
|
||||
ook als al een correctie is ingesteld; opnieuw kalibreren telt dus niet dubbel op.
|
||||
|
||||
Een geslaagde fit op bestaande data is nog geen geslaagde fysieke test.
|
||||
|
||||
## Meting B: de geflashte uitkomst onafhankelijk testen
|
||||
|
||||
Flash de correctie, nog steeds met het meetscherm:
|
||||
|
||||
```sh
|
||||
python -m esphome -s CALIBRATION_ON_BOOT true run device.yaml --device <USB_POORT>
|
||||
```
|
||||
|
||||
Sluit de loglezer met Ctrl+C en verzamel **nieuwe tikken**:
|
||||
|
||||
```sh
|
||||
python tools/calibrate.py capture --port <USB_POORT> --device-name display-keuken --output measurements-after.json
|
||||
python tools/calibrate.py verify measurements-after.json
|
||||
```
|
||||
|
||||
`verify` gebruikt de echte `native=`-coördinaten van de firmware, draait die
|
||||
naar de schermcoördinaten en vergelijkt ze met de doelen. Hij voert **geen
|
||||
nieuwe fit** uit. Alle vijf punten moeten slagen. Gebruik niet per ongeluk
|
||||
het bestand van vóór de flash voor deze controle.
|
||||
|
||||
Lukt dit niet? Bekijk welk punt faalt, controleer de volgorde/druk, meet opnieuw
|
||||
en lees [PROBLEMEN.md](PROBLEMEN.md). Verhoog niet alleen de tolerantie om de
|
||||
test groen te krijgen. Rapporteer een resterende hardwareafwijking eerlijk.
|
||||
|
||||
## Terug naar normale bediening
|
||||
|
||||
```sh
|
||||
python -m esphome run device.yaml --device <USB_POORT>
|
||||
```
|
||||
|
||||
Hier is **geen** `-s CALIBRATION_ON_BOOT true` opgegeven. In `device.yaml` blijft
|
||||
de instelling `false`. Controleer de werkelijke tegels en navigatie volgens
|
||||
[ACCEPTATIE.md](ACCEPTATIE.md).
|
||||
|
||||
Als de kalibratiemodus uitsluitend via de API was geopend, kun je hem sluiten:
|
||||
|
||||
```sh
|
||||
python diagnostics/control_ui.py end_touch_diagnostics --host display-keuken.local --name display-keuken
|
||||
```
|
||||
|
||||
Bewaar beide meetbestanden en `calibration.yaml` bij dit fysieke scherm.
|
||||
Na vervangen van het paneel opnieuw beginnen; geef alleen de neutrale
|
||||
starterbestanden aan een volgende eigenaar.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Problemen oplossen
|
||||
|
||||
| Symptoom | Eerst controleren |
|
||||
|---|---|
|
||||
| Geen USB-poort | Datakabel, andere USB-poort, bordvoeding; vergelijk `python -m serial.tools.list_ports` voor/na aansluiten. Installeer zo nodig de driver van de USB-chipfabrikant. |
|
||||
| Permission denied op Linux | Toegang tot de seriële devicegroep (vaak `dialout`); opnieuw inloggen na een groepswijziging. |
|
||||
| Poort bezet | Stop ESPHome-logs, IDE-monitor en andere seriële lezers. Eén lezer tegelijk. |
|
||||
| Upload blijft verbinden | Controleer juiste bord/poort. Houd BOOT ingedrukt tijdens verbinden en laat daarna los als het bord dat vereist. |
|
||||
| Build faalt | Python 3.11–3.14 en ESPHome 2026.6.2; volledige uitgepakte map, lokale fonts/components aanwezig, internet en schijfruimte. Bewaar eerste echte fout uit de buildlog. |
|
||||
| Wit/zwart of verkeerd gedraaid beeld | Controleer ILI9341 + XPT2046-variant. Niet willekeurig de pinnen, rotatie of SPI-instellingen van een ander CYD-model kopiëren. |
|
||||
| Geen meetscherm | De upload moet klaar zijn, niet alleen de build. Flash met `-s CALIBRATION_ON_BOOT true`. Zoek in USB-logs `USB calibration ready`; normaal opstarten toont geen kruisjes. |
|
||||
| Geen metingen | Vijf kruisjes zichtbaar, andere loglezer dicht, juiste poort. Druk eerst Enter in de wizard en tik daarna drie keer hetzelfde gevraagde punt, met loslaten ertussen. |
|
||||
| Tik springt naar andere plek | Controleer onafhankelijke `verify`, eigen `calibration.yaml` en vaste oriëntatie. Niet dezelfde tikken gebruiken om zowel te fitten als te verifiëren. |
|
||||
| Fit meldt spreiding/fout | Meet opnieuw volgens de prompts. Instabiele ADC-metingen niet oplossen door de tolerantie te verhogen; controleer voeding, kabel en touchhardware. |
|
||||
| Dubbele HA-actie | Controleer zowel touchlogs als HA-automations. Gebruik niet tegelijk directe acties én een automation op de actie-sensor voor dezelfde handeling. |
|
||||
| Wifi verbindt niet | 2,4GHz, juiste lokale secrets, netwerkbereik. USB-kalibratie heeft HA niet nodig. |
|
||||
| HA ziet het bord niet | Handmatig IP uit logs gebruiken; poort 6053 bereikbaar, juiste encryptiesleutel, geen gastnetwerkisolatie. |
|
||||
| Status werkt maar actie niet | HA-optie voor toegestane apparaat-acties, `DIRECT_ACTIONS: "true"`, echte entity-ID en ondersteunde actie controleren. |
|
||||
| Vacuum/climate deels bruikbaar | Ondersteunde modi/attributen verschillen per integratie; zie TEGELS.md en test dezelfde actie eerst in HA. |
|
||||
| Verkeerde paginering | `TILE_COUNT` is 1–10; tot zes verborgen knoppen, vanaf zeven twee pagina's. Opnieuw flashen na wijzigen. |
|
||||
| Vastloper/reset | USB-log bewaren, resetreden/voeding controleren en renderproef draaien. Noteer handeling en tijdstip. |
|
||||
|
||||
Voor een USB-log (stop andere lezers):
|
||||
|
||||
```sh
|
||||
python diagnostics/capture_serial.py --port <USB_POORT> --seconds 120
|
||||
```
|
||||
|
||||
De bestaande `CYD_STABILITY.md` en `TEST_RESULTS.md` beschrijven eerder
|
||||
onderzoek aan één specifiek bord. Hun meetwaarden zijn geen kalibratie voor
|
||||
een nieuw bord. Begin bij [README.md](../README.md), behoud een werkende
|
||||
lokale configuratie en verander één oorzaak tegelijk.
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
# Tegels instellen voor jouw Home Assistant
|
||||
|
||||
Bewerk `device.yaml`, binnen de bestaande `substitutions:`. De basiscode hoef
|
||||
je voor de onderstaande voorbeelden niet aan te passen. Elke wijziging vraagt
|
||||
opnieuw bouwen en flashen. Doorloop eerst de [installatie](../README.md).
|
||||
|
||||
## Entity-ID's verzamelen
|
||||
|
||||
Open in HA **Ontwikkelaarstools → Statussen**. Zoek de gewenste entiteit en
|
||||
kopieer de volledige ID, bijvoorbeeld `light.keuken`. Controleer status en
|
||||
attributen. De schermtitel is vrij te kiezen; de entity-ID moet exact kloppen.
|
||||
Test onder **Ontwikkelaarstools → Acties** de gewenste actie met die entiteit.
|
||||
Een actie kan het echte apparaat bedienen: kies een geschikte test.
|
||||
Zie [HA-acties](https://www.home-assistant.io/docs/scripts/perform-actions/).
|
||||
|
||||
Maak eerst een lijst met positie, titel, entity-ID, tikactie en lange-drukactie.
|
||||
Gebruik bestaande HA-scripts/scènes voor meerdere apparaten of complexe logica.
|
||||
|
||||
## Posities en grenzen
|
||||
|
||||
| Posities | Bedoeld gebruik in deze firmware |
|
||||
|---|---|
|
||||
| 1–6 | Hoofdtegels; light, switch, scene, script, fan, cover en climate |
|
||||
| 6 | Ook de vacuumkaart; de kaart is specifiek met tegel 6 verbonden |
|
||||
| 7 en 9 | Extra lichttegels met helderheid/kleurbediening |
|
||||
| 8 en 10 | Extra status-/actietegels; geen volledige numerieke/climatekaart |
|
||||
|
||||
`TILE_COUNT: "1"` tot `"10"` bepaalt hoeveel **opeenvolgende** posities zichtbaar
|
||||
zijn. Tot en met zes zijn Vorige/Volgende verborgen; vanaf zeven staat 1–6 op
|
||||
pagina één en de rest op pagina twee. Vrij scrollen is vervangen door vaste
|
||||
pagina's. Laat ongebruikte posities op de neutrale voorbeeldentiteiten staan.
|
||||
Verander geen rastermaten om tegels toe te voegen.
|
||||
|
||||
## Belangrijkste instellingen
|
||||
|
||||
| Instelling (`N` = tegelnummer) | Betekenis |
|
||||
|---|---|
|
||||
| `TILEN_ENTITY` | Entiteit waarop de actie wordt uitgevoerd |
|
||||
| `TILEN_STATE_ENTITY` | Entiteit voor status/attributen; meestal dezelfde |
|
||||
| `TILEN_TITLE` | Korte titel, liefst maximaal circa 12 tekens |
|
||||
| `TILEN_TYPE` | Domein, bijvoorbeeld `light`, `scene`, `climate` |
|
||||
| `TILEN_TAP_ACTION` | `auto`, `toggle` of `custom` voor onderstaande voorbeelden |
|
||||
| `TILEN_TAP_SERVICE` | Expliciete HA-actie bij custom, bijvoorbeeld `script.turn_on` |
|
||||
| `TILEN_LONGPRESS` | `none`, `slider` of (alleen tegel 6) `vacuum` |
|
||||
| `TILEN_LONGPRESS_SLIDER` | `auto`; kiest passende regeling op basis van type |
|
||||
| `TILEN_LABEL_ON/OFF` | Tekst voor eenvoudige aan/uitstatus |
|
||||
|
||||
De voorbeeldconfig vult ook de overige velden in. Laat die staan tenzij je
|
||||
ze bewust gebruikt. `STATE_ENTITY` kan bij een scène/script een echte lamp of
|
||||
schakelaar volgen; een scène heeft zelf geen betrouwbare aan/uitstatus.
|
||||
|
||||
## Voorbeelden
|
||||
|
||||
Wijzig deze sleutels in de bestaande map; plak niet nogmaals `substitutions:`.
|
||||
|
||||
Dimbare lamp op positie 1 (tik schakelt, lang indrukken opent de regeling):
|
||||
|
||||
```yaml
|
||||
TILE1_ENTITY: "light.keuken"
|
||||
TILE1_STATE_ENTITY: "light.keuken"
|
||||
TILE1_TITLE: "Keuken"
|
||||
TILE1_TYPE: "light"
|
||||
TILE1_TAP_ACTION: "auto"
|
||||
TILE1_LONGPRESS: "slider"
|
||||
```
|
||||
|
||||
Een niet-dimbare lamp/schakelaar krijgt `LONGPRESS: "none"`. Kleurbediening
|
||||
vereist dat de lamp de betreffende kleurmodus ondersteunt.
|
||||
|
||||
Scène op positie 3:
|
||||
|
||||
```yaml
|
||||
TILE3_ENTITY: "scene.avond"
|
||||
TILE3_STATE_ENTITY: "light.keuken"
|
||||
TILE3_TITLE: "Avond"
|
||||
TILE3_TYPE: "scene"
|
||||
TILE3_TAP_ACTION: "auto"
|
||||
TILE3_LONGPRESS: "none"
|
||||
```
|
||||
|
||||
Script op positie 4:
|
||||
|
||||
```yaml
|
||||
TILE4_ENTITY: "script.alles_uit"
|
||||
TILE4_STATE_ENTITY: "script.alles_uit"
|
||||
TILE4_TITLE: "Alles uit"
|
||||
TILE4_TYPE: "script"
|
||||
TILE4_TAP_ACTION: "auto"
|
||||
TILE4_LONGPRESS: "none"
|
||||
```
|
||||
|
||||
Airco op positie 5 (lang indrukken opent climate):
|
||||
|
||||
```yaml
|
||||
TILE5_ENTITY: "climate.woonkamer"
|
||||
TILE5_STATE_ENTITY: "climate.woonkamer"
|
||||
TILE5_TITLE: "Airco"
|
||||
TILE5_TYPE: "climate"
|
||||
TILE5_TAP_ACTION: "toggle"
|
||||
TILE5_TAP_SERVICE: "climate.toggle"
|
||||
TILE5_LONGPRESS: "slider"
|
||||
```
|
||||
|
||||
Controleer `min_temp`, `max_temp`, `target_temp_step`, `hvac_modes` en de
|
||||
ondersteunde fan-/swingmodes in HA. Deze kaart is primair voor een **enkel
|
||||
setpoint**, niet een thermostaat die uitsluitend een temperatuurinterval
|
||||
gebruikt. Test de modi van jouw integratie afzonderlijk; een andere airco
|
||||
kan andere mogelijkheden hebben.
|
||||
|
||||
Robotstofzuiger op positie 6 (tik start; lang indrukken opent de kaart):
|
||||
|
||||
```yaml
|
||||
TILE6_ENTITY: "vacuum.robot"
|
||||
TILE6_STATE_ENTITY: "vacuum.robot"
|
||||
TILE6_TITLE: "Stofzuiger"
|
||||
TILE6_TYPE: "vacuum"
|
||||
TILE6_TAP_ACTION: "custom"
|
||||
TILE6_TAP_SERVICE: "vacuum.start"
|
||||
TILE6_LONGPRESS: "vacuum"
|
||||
```
|
||||
|
||||
De vacuumkaart gebruikt start/pauze/stop/terugkeren/lokaliseren en de
|
||||
snelheidsnamen `quiet`, `balanced`, `turbo`, `max`. Vergelijk deze met de
|
||||
ondersteunde acties en `fan_speed_list` in HA. De snelheidsknoppen moeten in
|
||||
de basiscode aangepast worden als jouw integratie andere namen verwacht.
|
||||
Dit is geen universele kaart voor ieder robotmodel.
|
||||
|
||||
## Acties inschakelen en controleren
|
||||
|
||||
Zet na kalibratie en HA-koppeling `DIRECT_ACTIONS: "true"`. Geef in de
|
||||
ESPHome-integratie toestemming voor Home Assistant-acties. Gebruik geen
|
||||
HA-automation die dezelfde schermactie daarnaast nogmaals uitvoert; dat kan
|
||||
een dubbele toggle veroorzaken. Bij `DIRECT_ACTIONS: "false"` kan de
|
||||
actie-sensor nog gebeurtenissen publiceren voor eigen automations.
|
||||
|
||||
Flash `device.yaml` en test één tegel tegelijk: juiste status, juiste fysieke
|
||||
actie, terugmelding vanuit HA en lange druk. Controleer ook dat externe
|
||||
wijzigingen in HA op het scherm verschijnen. Voer daarna de
|
||||
[acceptatietest](ACCEPTATIE.md) uit.
|
||||
|
||||
Extra iconen vereisen een glyph in de `materialdesign_icons`-fontlijst. De
|
||||
bundel bevat de fonts lokaal. De oudere uitgebreide
|
||||
[TILE_CONFIGURATION.md](../TILE_CONFIGURATION.md) beschrijft ook het oude
|
||||
scrollprofiel; gebruik die niet als hardware-/layoutinstructie voor dit bord.
|
||||
@@ -0,0 +1,20 @@
|
||||
Pictogrammers Free License
|
||||
--------------------------
|
||||
|
||||
This icon collection is released as free, open source, and GPL friendly by
|
||||
the [Pictogrammers](http://pictogrammers.com/) icon group. You may use it
|
||||
for commercial projects, open source projects, or anything really.
|
||||
|
||||
# Icons: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
|
||||
Some of the icons are redistributed under the Apache 2.0 license. All other
|
||||
icons are either redistributed under their respective licenses or are
|
||||
distributed under the Apache 2.0 license.
|
||||
|
||||
# Fonts: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
|
||||
All web and desktop fonts are distributed under the Apache 2.0 license. Web
|
||||
and desktop fonts contain some icons that are redistributed under the Apache
|
||||
2.0 license. All other icons are either redistributed under their respective
|
||||
licenses or are distributed under the Apache 2.0 license.
|
||||
|
||||
# Code: MIT (https://opensource.org/licenses/MIT)
|
||||
The MIT license applies to all non-font and non-icon files.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,93 @@
|
||||
Copyright 2011 The Roboto Project Authors (https://github.com/googlefonts/roboto-classic)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
# Tested toolchain; do not silently upgrade during commissioning.
|
||||
esphome==2026.6.2
|
||||
@@ -0,0 +1,6 @@
|
||||
# tools/new_device.py maakt unieke sleutels; bewerk wifi daarna lokaal.
|
||||
wifi_ssid: "VUL_JE_2_4_GHZ_WIFI_IN"
|
||||
wifi_password: "VUL_JE_WIFI_WACHTWOORD_IN"
|
||||
api_encryption_key: "WORDT_UNIEK_GEGENEREERD"
|
||||
ota_password: "WORDT_UNIEK_GEGENEREERD"
|
||||
ap_password: "WORDT_UNIEK_GEGENEREERD"
|
||||
@@ -0,0 +1,35 @@
|
||||
#include "../cyd_ui.h"
|
||||
#include <cassert>
|
||||
#include <limits>
|
||||
int main() {
|
||||
using cyd::quantize_temperature;
|
||||
assert(quantize_temperature(223, 160, 300, 5) == 225);
|
||||
assert(quantize_temperature(221, 160, 300, 5) == 220);
|
||||
assert(quantize_temperature(-1, 160, 300, 5) == 160);
|
||||
assert(quantize_temperature(999, 160, 299, 5) == 299);
|
||||
assert(quantize_temperature(201, 160, 300, 0) == 201);
|
||||
cyd::TouchGuard g;
|
||||
g.begin(100);
|
||||
assert(!g.accept(120, 1)); // resistive noise pulse
|
||||
assert(g.accept(180, 1));
|
||||
assert(!g.accept(800, 1)); // same physical gesture cannot fire twice
|
||||
g.begin(200);
|
||||
assert(!g.accept(280, 1)); // contact bounce cannot repeat the action
|
||||
g.begin(300);
|
||||
assert(g.accept(380, 2)); // another tile is still responsive
|
||||
g.begin(1000);
|
||||
assert(g.accept(1080, 2));
|
||||
cyd::TouchGuard moving;
|
||||
moving.begin(100, 50, 50);
|
||||
moving.update(52, 48);
|
||||
assert(moving.accept(180, 1)); // small resistive jitter remains usable
|
||||
moving.begin(1000, 50, 50);
|
||||
moving.update(50, 80);
|
||||
moving.update(50, 50);
|
||||
assert(!moving.accept(1200, 2)); // excursion stays cancelled, even on return
|
||||
cyd::TouchGuard rollover;
|
||||
rollover.begin(std::numeric_limits<uint32_t>::max() - 30);
|
||||
assert(rollover.accept(50, 1)); // millis wraps after 49 days
|
||||
rollover.begin(60);
|
||||
assert(!rollover.accept(130, 1));
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Regression checks for the 320x240 page geometry and event guards."""
|
||||
from pathlib import Path
|
||||
import re
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE = (ROOT / 'home-like-2432s028.yaml').read_text()
|
||||
VALUES = dict(re.findall(r'^ (\w+): "([^"]*)"', SOURCE, re.M))
|
||||
|
||||
class LayoutTests(unittest.TestCase):
|
||||
def test_visible_rows_fit_viewport_and_leave_navigation_clear(self):
|
||||
v = lambda k: int(VALUES[k])
|
||||
rectangles = []
|
||||
for row in range(1, 4):
|
||||
for col in range(1, 3):
|
||||
x, y = v(f'GRID_COL{col}_X'), v('SCROLL_Y') + v(f'GRID_ROW{row}_Y')
|
||||
w, h = v('TILE_W'), v('TILE_H')
|
||||
self.assertGreaterEqual(x, 0)
|
||||
self.assertLessEqual(x + w, v('DISPLAY_W'))
|
||||
self.assertLessEqual(y + h, v('SCROLL_Y') + v('SCROLL_H'))
|
||||
self.assertLessEqual(y + h, v('DISPLAY_H') - 34)
|
||||
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))
|
||||
|
||||
def test_each_tile_filters_both_events_before_side_effects(self):
|
||||
for n in range(1, 11):
|
||||
start = SOURCE.index(f' id: tile{n}\n')
|
||||
next_obj = re.search(r'^ - obj:', SOURCE[start:], re.M)
|
||||
end = start + next_obj.start() if next_obj else len(SOURCE)
|
||||
block = SOURCE[start:end]
|
||||
for event in ('short_click', 'long_press'):
|
||||
match = re.search(r'on_' + event + r':\s+- if:\s+condition:\s+lambda: (.*?)\n', block)
|
||||
self.assertIsNotNone(match, (n, event))
|
||||
self.assertIn(f'cyd::touch_guard.accept(millis(), {n})', match[1])
|
||||
|
||||
def test_navigation_is_above_grid_but_below_modal_overlays(self):
|
||||
grid = SOURCE.index(' id: tile_scroll')
|
||||
nav = SOURCE.index(' id: page_prev')
|
||||
modal = SOURCE.index(' id: brightness_overlay')
|
||||
self.assertLess(grid, nav)
|
||||
self.assertLess(nav, modal)
|
||||
|
||||
def test_pagination_is_conditional_and_excess_tiles_are_hidden(self):
|
||||
block = SOURCE.split(' - id: show_tile_page\n', 1)[1].split(' - id: wake_display', 1)[0]
|
||||
self.assertIn('const bool paginated = count > 6;', block)
|
||||
self.assertIn('i < count &&', block)
|
||||
self.assertIn('if (!paginated) id(tile_page) = 0;', block)
|
||||
self.assertIn('id(page_prev), id(page_next), id(page_number)', block)
|
||||
self.assertIn('else lv_obj_add_flag(control, LV_OBJ_FLAG_HIDDEN);', block)
|
||||
|
||||
def test_self_test_cannot_call_a_home_assistant_action(self):
|
||||
block = SOURCE.split(' - id: ui_self_test\n', 1)[1].split(' - id: show_tile_page', 1)[0]
|
||||
self.assertNotIn('homeassistant.action:', block)
|
||||
self.assertNotIn('do_tile_action', block)
|
||||
self.assertNotIn('climate_target_commit', block)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,126 @@
|
||||
"""Offline regression tests for installation, calibration and safe export."""
|
||||
import copy
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import zipfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / 'tools'))
|
||||
import calibrate
|
||||
import new_device
|
||||
import export_bundle
|
||||
|
||||
|
||||
def measurements():
|
||||
points = []
|
||||
for name, (x, y) in calibrate.TARGETS:
|
||||
rx = round(280 + y * 3580 / 240)
|
||||
ry = round(340 + x * 3520 / 320)
|
||||
samples = [dict(raw_x=rx+d, raw_y=ry+d, native_x=239-y,
|
||||
native_y=x, pressure=1500) for d in (-2, 0, 2)]
|
||||
points.append(dict(name=name, screen=[x, y], samples=samples))
|
||||
return dict(schema=1, screen=[320, 240], points=points)
|
||||
|
||||
|
||||
class CalibrationTests(unittest.TestCase):
|
||||
def test_identity_fit_and_independent_verification(self):
|
||||
coefficients, report = calibrate.fit(measurements())
|
||||
self.assertAlmostEqual(coefficients[0], 1, places=3)
|
||||
self.assertAlmostEqual(coefficients[4], 1, places=3)
|
||||
self.assertTrue(all(p['error_px'] < 1 for p in report))
|
||||
self.assertEqual(len(calibrate.verify(measurements())), 5)
|
||||
|
||||
def test_affine_skew_is_corrected(self):
|
||||
data = measurements()
|
||||
for point in data['points']:
|
||||
for s in point['samples']:
|
||||
x, y = s['raw_x'], s['raw_y']
|
||||
s['raw_x'] = round(.9*x - .03*y + 190)
|
||||
s['raw_y'] = round(.04*x + .95*y + 50)
|
||||
_, report = calibrate.fit(data)
|
||||
self.assertTrue(all(p['error_px'] < 1 for p in report))
|
||||
|
||||
def test_center_is_not_used_to_hide_bad_fit(self):
|
||||
data = measurements()
|
||||
for s in data['points'][4]['samples']: s['raw_x'] += 600
|
||||
with self.assertRaisesRegex(ValueError, 'midden'): calibrate.fit(data)
|
||||
|
||||
def test_jitter_rejected(self):
|
||||
data = measurements()
|
||||
data['points'][0]['samples'][0]['raw_x'] += 700
|
||||
with self.assertRaises(ValueError): calibrate.fit(data)
|
||||
|
||||
def test_repeated_corner_rejected(self):
|
||||
data = measurements()
|
||||
for p in data['points']: p['samples'] = copy.deepcopy(data['points'][0]['samples'])
|
||||
with self.assertRaises(ValueError): calibrate.fit(data)
|
||||
|
||||
def test_wrong_firmware_coordinates_rejected(self):
|
||||
data = measurements()
|
||||
for s in data['points'][0]['samples']: s['native_y'] += 40
|
||||
with self.assertRaises(ValueError): calibrate.verify(data)
|
||||
|
||||
def test_capture_requires_isolated_mode(self):
|
||||
line = 'press native=20,30 raw=500,600 pressure=1500 calibration='
|
||||
self.assertEqual(calibrate.parse_touch(line+'1')['raw_x'], 500)
|
||||
with self.assertRaises(ValueError): calibrate.parse_touch(line+'0')
|
||||
self.assertIsNone(calibrate.parse_touch('unrelated log'))
|
||||
|
||||
def test_wrong_screen_or_order_rejected(self):
|
||||
for field in ('screen', 'order'):
|
||||
data = measurements()
|
||||
if field == 'screen': data['screen'] = [240,320]
|
||||
else: data['points'].reverse()
|
||||
with self.assertRaises(ValueError): calibrate.fit(data)
|
||||
|
||||
def test_output_requires_explicit_replace_and_preserves_backup(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp)/'calibration.yaml'
|
||||
calibrate.write_file(path, 'old')
|
||||
with self.assertRaises(ValueError): calibrate.write_file(path, 'new')
|
||||
calibrate.write_file(path, 'new', replace=True)
|
||||
self.assertEqual(path.read_text(), 'new')
|
||||
self.assertEqual(next(Path(tmp).glob('*.bak-*')).read_text(), 'old')
|
||||
|
||||
|
||||
class InstallationTests(unittest.TestCase):
|
||||
def test_unique_secrets_and_no_overwrite(self):
|
||||
with tempfile.TemporaryDirectory() as a, tempfile.TemporaryDirectory() as b:
|
||||
new_device.create(a, 'display-a', 'Display A')
|
||||
new_device.create(b, 'display-b', 'Display B')
|
||||
original = (Path(a)/'secrets.yaml').read_text()
|
||||
self.assertNotEqual(original, (Path(b)/'secrets.yaml').read_text())
|
||||
self.assertNotIn('WORDT_UNIEK_GEGENEREERD', original)
|
||||
with self.assertRaises(ValueError): new_device.create(a, 'new', 'New')
|
||||
self.assertEqual(original, (Path(a)/'secrets.yaml').read_text())
|
||||
|
||||
def test_invalid_device_name(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
for name in ('../oops', 'UPPER', 'has space', 'trailing-'):
|
||||
with self.assertRaises(ValueError): new_device.create(tmp, name, 'Test')
|
||||
self.assertEqual(list(Path(tmp).iterdir()), [])
|
||||
|
||||
def test_export_is_neutral_and_has_required_sources(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
output = Path(tmp)/'starter.zip'
|
||||
export_bundle.export(ROOT, output)
|
||||
with zipfile.ZipFile(output) as archive:
|
||||
names = archive.namelist()
|
||||
for forbidden in ('secrets.yaml', 'device.yaml', 'calibration.yaml', '.git/', '.esphome/', '.log', '.bin', '.json'):
|
||||
self.assertFalse(any(('/'+forbidden in n if forbidden.endswith('/') else n.endswith(forbidden) if forbidden.startswith('.') else n.endswith('/'+forbidden)) for n in names), forbidden)
|
||||
self.assertIn('cyd-display/.gitignore', names)
|
||||
base = archive.read('cyd-display/home-like-2432s028.yaml').decode()
|
||||
values = dict(re.findall(r'^ (\w+): "([^"]*)"', base, re.M))
|
||||
self.assertEqual(values['TOUCH_AFFINE_XX'], '1.000000000')
|
||||
self.assertEqual(values['DIRECT_ACTIONS'], 'false')
|
||||
for i in range(1,11):
|
||||
self.assertRegex(values[f'TILE{i}_ENTITY'], r'\.(voorbeeld_|ongebruikt_)')
|
||||
archive.extractall(tmp)
|
||||
new_device.create(Path(tmp)/'cyd-display', 'fresh-board', 'Fresh board')
|
||||
with self.assertRaises(FileExistsError): export_bundle.export(ROOT, output)
|
||||
|
||||
|
||||
if __name__ == '__main__': unittest.main()
|
||||
@@ -0,0 +1,43 @@
|
||||
#include "../components/xpt2046/touchscreen/touch_filter.h"
|
||||
#include <cassert>
|
||||
int main() {
|
||||
using F = esphome::xpt2046::TouchFilter;
|
||||
esphome::xpt2046::RawCorrection correction;
|
||||
assert(correction.x(100, 200) == 100 && correction.y(100, 200) == 200);
|
||||
correction.set(1.073915801f, 0.065912977f, -345.080450614f, 0.044372873f, 0.983997898f, -69.394322384f);
|
||||
const int corners[][4] = {{830,556,20,20}, {544,3461,299,20},
|
||||
{3410,3391,299,219}, {3492,372,20,219}};
|
||||
for (const auto &p : corners) {
|
||||
const double sx = (correction.y(p[0], p[1]) - 274) * 320.0 / 3335;
|
||||
const double sy = (correction.x(p[0], p[1]) - 225) * 240.0 / 3575;
|
||||
assert(std::abs(sx - p[2]) < 5 && std::abs(sy - p[3]) < 5);
|
||||
}
|
||||
assert(correction.x(-30000, -30000) == 0);
|
||||
assert(correction.y(30000, 30000) == 4095);
|
||||
F f;
|
||||
// Measured bad top-left first samples must not reach LVGL immediately.
|
||||
assert(f.sample(true, 2067, 1873) == F::HOLD);
|
||||
assert(f.sample(true, 535, 500) == F::HOLD);
|
||||
assert(f.sample(true, 550, 510) == F::HOLD);
|
||||
assert(f.sample(true, 530, 495) == F::POSITION);
|
||||
assert(f.x() >= 530 && f.x() <= 550);
|
||||
assert(f.y() >= 495 && f.y() <= 510);
|
||||
// A far-away sample cannot move the pointer to another button.
|
||||
assert(f.sample(true, 3000, 3000) == F::HOLD);
|
||||
assert(f.sample(true, 540, 500) == F::POSITION);
|
||||
assert(f.x() < 600);
|
||||
// One dropped pressure sample doesn't create a second press/release.
|
||||
assert(f.sample(false, 0, 0) == F::HOLD);
|
||||
assert(f.sample(true, 550, 500) == F::POSITION);
|
||||
// A deliberate drag still works, including a large consistent movement.
|
||||
assert(f.sample(true, 700, 650) == F::POSITION);
|
||||
assert(f.sample(true, 2000, 2000) == F::HOLD);
|
||||
assert(f.sample(true, 2010, 2005) == F::HOLD);
|
||||
assert(f.sample(true, 1990, 2000) == F::POSITION);
|
||||
assert(f.x() > 1900);
|
||||
assert(f.sample(false, 0, 0) == F::HOLD);
|
||||
assert(f.sample(false, 0, 0) == F::RELEASE);
|
||||
// A lone ghost pulse must never start a gesture.
|
||||
assert(f.sample(true, 3900, 100) == F::HOLD);
|
||||
assert(f.sample(false, 0, 0) == F::RELEASE);
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
"""USB calibration: guided capture, affine fit with center hold-out, verification.
|
||||
|
||||
No network or Home Assistant access. The device must already run the firmware
|
||||
with CALIBRATION_ON_BOOT=true (or have its isolated diagnostic page opened).
|
||||
"""
|
||||
import argparse
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import statistics
|
||||
import time
|
||||
|
||||
TARGETS = [('linksboven', (20, 20)), ('rechtsboven', (299, 20)),
|
||||
('rechtsonder', (299, 219)), ('linksonder', (20, 219)), ('midden', (160, 120))]
|
||||
BOUNDS = (280, 3860, 340, 3860)
|
||||
PATTERN = re.compile(r'press native=(\d+),(\d+) raw=(\d+),(\d+) pressure=(\d+) calibration=([01])')
|
||||
|
||||
|
||||
def parse_touch(line):
|
||||
match = PATTERN.search(line)
|
||||
if not match:
|
||||
return None
|
||||
nx, ny, rx, ry, pressure, isolated = map(int, match.groups())
|
||||
if not isolated:
|
||||
raise ValueError('Geen geïsoleerd meetscherm: start met CALIBRATION_ON_BOOT=true. Geen data opgeslagen.')
|
||||
return dict(native_x=nx, native_y=ny, raw_x=rx, raw_y=ry, pressure=pressure)
|
||||
|
||||
|
||||
def write_file(path, contents, replace=False):
|
||||
path = Path(path)
|
||||
if path.exists():
|
||||
if not replace:
|
||||
raise ValueError(f'{path} bestaat al; kies een andere naam of expliciet --replace.')
|
||||
backup = path.with_name(path.name + '.bak-' + datetime.now().strftime('%Y%m%d-%H%M%S-%f'))
|
||||
backup.write_bytes(path.read_bytes())
|
||||
# Write completely before replacing an existing configuration.
|
||||
temporary = path.with_name(path.name + '.tmp-' + str(os.getpid()))
|
||||
try:
|
||||
with temporary.open('x', encoding='utf-8') as handle:
|
||||
handle.write(contents)
|
||||
os.replace(temporary, path)
|
||||
finally:
|
||||
temporary.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def validate(data):
|
||||
if data.get('schema') != 1 or data.get('screen') != [320, 240]:
|
||||
raise ValueError('Verwacht schema 1 en landschap 320×240; andere oriëntaties zijn niet ondersteund.')
|
||||
points = data.get('points', [])
|
||||
if len(points) != len(TARGETS):
|
||||
raise ValueError('Alle vier hoeken én het onafhankelijke middenpunt zijn nodig.')
|
||||
for point, (name, xy) in zip(points, TARGETS):
|
||||
if point.get('name') != name or point.get('screen') != list(xy):
|
||||
raise ValueError('Meetpunten/volgorde komen niet overeen met de wizard.')
|
||||
if len(point.get('samples', [])) < 3:
|
||||
raise ValueError(f'{name}: minimaal drie afzonderlijke tikken nodig.')
|
||||
for sample in point['samples']:
|
||||
for key, maximum in [('raw_x', 4095), ('raw_y', 4095), ('native_x', 239), ('native_y', 319), ('pressure', 8190)]:
|
||||
value = sample.get(key)
|
||||
if type(value) is not int or not 0 <= value <= maximum:
|
||||
raise ValueError(f'{name}: ongeldige {key}.')
|
||||
return points
|
||||
|
||||
|
||||
def median_raw(point):
|
||||
return tuple(statistics.median(s[key] for s in point['samples']) for key in ('raw_x', 'raw_y'))
|
||||
|
||||
|
||||
def least_squares(rows, values):
|
||||
matrix = [[sum(row[i] * row[j] for row in rows) for j in range(3)] +
|
||||
[sum(row[i] * value for row, value in zip(rows, values))] for i in range(3)]
|
||||
for column in range(3):
|
||||
pivot = max(range(column, 3), key=lambda row: abs(matrix[row][column]))
|
||||
matrix[column], matrix[pivot] = matrix[pivot], matrix[column]
|
||||
scale = matrix[column][column]
|
||||
if abs(scale) < 1e-9:
|
||||
raise ValueError('Meetpunten zijn niet onafhankelijk; waarschijnlijk dezelfde hoek meermaals gemeten.')
|
||||
matrix[column] = [v / scale for v in matrix[column]]
|
||||
for row in range(3):
|
||||
if row != column:
|
||||
factor = matrix[row][column]
|
||||
matrix[row] = [a - factor * b for a, b in zip(matrix[row], matrix[column])]
|
||||
return [matrix[i][3] for i in range(3)]
|
||||
|
||||
|
||||
def project(coefficients, rx, ry):
|
||||
xx, xy, xc, yx, yy, yc = coefficients
|
||||
xmin, xmax, ymin, ymax = BOUNDS
|
||||
return ((yx * rx + yy * ry + yc - ymin) * 320 / (ymax - ymin),
|
||||
(xx * rx + xy * ry + xc - xmin) * 240 / (xmax - xmin))
|
||||
|
||||
|
||||
def fit(data, tolerance=12):
|
||||
points = validate(data)
|
||||
medians = [median_raw(p) for p in points]
|
||||
# Scale inputs to avoid poorly conditioned normal equations on 12-bit ADC data.
|
||||
rows = [(x / 4095, y / 4095, 1) for x, y in medians[:4]]
|
||||
xmin, xmax, ymin, ymax = BOUNDS
|
||||
values_x = [xmin + p['screen'][1] * (xmax - xmin) / 240 for p in points[:4]]
|
||||
values_y = [ymin + p['screen'][0] * (ymax - ymin) / 320 for p in points[:4]]
|
||||
cx, cy = least_squares(rows, values_x), least_squares(rows, values_y)
|
||||
coefficients = [cx[0] / 4095, cx[1] / 4095, cx[2], cy[0] / 4095, cy[1] / 4095, cy[2]]
|
||||
if not all(math.isfinite(v) for v in coefficients):
|
||||
raise ValueError('Ongeldige kalibratiecoëfficiënten.')
|
||||
# Reject captures that amplify small sensor jitter into huge screen movements.
|
||||
if max(abs(v) for v in (coefficients[0], coefficients[1], coefficients[3], coefficients[4])) > 4:
|
||||
raise ValueError('Te klein meetbereik; tik de echte schermhoeken aan.')
|
||||
report = []
|
||||
for point, median in zip(points, medians):
|
||||
predicted = project(coefficients, *median)
|
||||
error = math.dist(predicted, point['screen'])
|
||||
spread = max(math.dist(project(coefficients, s['raw_x'], s['raw_y']), predicted) for s in point['samples'])
|
||||
report.append(dict(name=point['name'], median=list(median), error_px=round(error, 2), spread_px=round(spread, 2)))
|
||||
if error > tolerance or spread > 18:
|
||||
raise ValueError(f"{point['name']}: fout {error:.1f}px, spreiding {spread:.1f}px. "
|
||||
'Meet opnieuw; een instabiel paneel niet wegkalibreren.')
|
||||
return coefficients, report
|
||||
|
||||
|
||||
def calibration_yaml(coefficients):
|
||||
names = ['TOUCH_AFFINE_XX', 'TOUCH_AFFINE_XY', 'TOUCH_AFFINE_XC', 'TOUCH_AFFINE_YX', 'TOUCH_AFFINE_YY', 'TOUCH_AFFINE_YC']
|
||||
lines = ['# Gemeten met tools/calibrate.py; uitsluitend voor dit fysieke paneel.', 'substitutions:']
|
||||
for name, value in zip(['TOUCH_CAL_X_MIN', 'TOUCH_CAL_X_MAX', 'TOUCH_CAL_Y_MIN', 'TOUCH_CAL_Y_MAX'], BOUNDS):
|
||||
lines.append(f' {name}: "{value}"')
|
||||
lines += [f' {name}: "{value:.9f}"' for name, value in zip(names, coefficients)]
|
||||
return '\n'.join(lines) + '\n'
|
||||
|
||||
|
||||
def verify(data, tolerance=12):
|
||||
results = []
|
||||
for point in validate(data):
|
||||
measured = [(s['native_y'], 239 - s['native_x']) for s in point['samples']]
|
||||
center = tuple(statistics.median(p[i] for p in measured) for i in (0, 1))
|
||||
error = math.dist(center, point['screen'])
|
||||
spread = max(math.dist(p, center) for p in measured)
|
||||
results.append(dict(name=point['name'], error_px=round(error, 2), spread_px=round(spread, 2)))
|
||||
if error > tolerance or spread > 18:
|
||||
raise ValueError(f"{point['name']}: echte firmwarecoördinaten wijken {error:.1f}px af (spreiding {spread:.1f}px).")
|
||||
return results
|
||||
|
||||
|
||||
def capture(args):
|
||||
import serial
|
||||
if args.output.exists():
|
||||
raise ValueError('Meetbestand bestaat al; kies een nieuwe naam.')
|
||||
data = dict(schema=1, device_name=args.device_name, screen=[320, 240],
|
||||
created_at=datetime.now(timezone.utc).isoformat(), points=[])
|
||||
print('Alleen voor 320×240 / LVGL 90° / swap_xy=false / mirror_x=true / mirror_y=false.')
|
||||
print('Het scherm moet de VIJF kruisjes tonen. Sluit andere seriële loglezers af.')
|
||||
with serial.Serial(args.port, 115200, timeout=0.2) as port:
|
||||
for name, xy in TARGETS:
|
||||
input(f'\nVolgende punt: {name} {xy}. Druk ENTER en tik daarna alleen dit kruisje {args.samples} keer: ')
|
||||
port.reset_input_buffer()
|
||||
samples = []
|
||||
deadline = time.monotonic() + args.timeout
|
||||
last = -math.inf
|
||||
while len(samples) < args.samples and time.monotonic() < deadline:
|
||||
sample = parse_touch(port.readline().decode('utf-8', errors='replace'))
|
||||
if sample and time.monotonic() - last >= 0.4:
|
||||
samples.append(sample)
|
||||
last = time.monotonic()
|
||||
print(f' {name}: {len(samples)}/{args.samples}', flush=True)
|
||||
if len(samples) != args.samples:
|
||||
raise ValueError(f'{name}: te weinig geldige tikken. Controleer de firmware, het meetscherm en USB-poort.')
|
||||
data['points'].append(dict(name=name, screen=list(xy), samples=samples))
|
||||
write_file(args.output, json.dumps(data, indent=2) + '\n')
|
||||
print(f'Meting opgeslagen: {args.output}. Het script heeft niets geflasht of aangestuurd.')
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
commands = parser.add_subparsers(dest='command', required=True)
|
||||
c = commands.add_parser('capture', help='Lees geleide tikken via USB; firmware moet al het meetscherm tonen.')
|
||||
c.add_argument('--port', required=True)
|
||||
c.add_argument('--output', type=Path, required=True)
|
||||
c.add_argument('--device-name', default='onbekend')
|
||||
c.add_argument('--samples', type=int, choices=range(3, 10), default=3)
|
||||
c.add_argument('--timeout', type=float, default=60)
|
||||
for name in ('fit', 'verify'):
|
||||
sub = commands.add_parser(name)
|
||||
sub.add_argument('input', type=Path)
|
||||
sub.add_argument('--tolerance', type=float, default=12)
|
||||
if name == 'fit':
|
||||
sub.add_argument('--output', type=Path, default=Path('calibration.proposed.yaml'))
|
||||
sub.add_argument('--replace', action='store_true')
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
if args.command == 'capture':
|
||||
if args.timeout <= 0: raise ValueError('Timeout moet positief zijn.')
|
||||
capture(args)
|
||||
else:
|
||||
if args.tolerance <= 0 or not math.isfinite(args.tolerance): raise ValueError('Tolerantie moet eindig en positief zijn.')
|
||||
data = json.loads(args.input.read_text())
|
||||
if args.command == 'fit':
|
||||
coefficients, report = fit(data, args.tolerance)
|
||||
write_file(args.output, calibration_yaml(coefficients), args.replace)
|
||||
print(f'Kalibratie geschreven: {args.output}. Het middenpunt is onafhankelijk gecontroleerd.')
|
||||
else:
|
||||
report = verify(data, args.tolerance)
|
||||
print('PASS: onafhankelijke fysieke controle van de geflashte coördinaten.')
|
||||
for item in report:
|
||||
print(f"{item['name']}: fout={item['error_px']}px, spreiding={item['spread_px']}px")
|
||||
except (ValueError, OSError, KeyError, TypeError, EOFError) as exc:
|
||||
parser.exit(1, f'Fout: {exc}\n')
|
||||
except KeyboardInterrupt:
|
||||
parser.exit(130, 'Afgebroken; geen gedeeltelijke meting opgeslagen.\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Export an allowlisted, neutral starter ZIP (never copies local secrets/logs)."""
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import re
|
||||
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']
|
||||
FOLDERS = ['docs', 'tools', 'tests', 'components', 'diagnostics', 'fonts']
|
||||
SUFFIXES = {'.py', '.cpp', '.h', '.md', '.ttf', '.txt'}
|
||||
|
||||
|
||||
def neutral_base(root):
|
||||
defaults = '\n'.join((root / name).read_text() for name in ['device.example.yaml', 'calibration.example.yaml'])
|
||||
replacements = dict(re.findall(r'^ (\w+): ("[^"\n]*")', defaults, re.M))
|
||||
base = (root / 'home-like-2432s028.yaml').read_text()
|
||||
def replace(match):
|
||||
key = match[1]
|
||||
return f' {key}: {replacements.get(key, match[2])}'
|
||||
return re.sub(r'^ (\w+): ("[^"\n]*")', replace, base, flags=re.M)
|
||||
|
||||
|
||||
def export(root, output):
|
||||
root, output = Path(root).resolve(), Path(output)
|
||||
paths = [root / name for name in FILES]
|
||||
for folder in FOLDERS:
|
||||
for path in (root / folder).rglob('*'):
|
||||
if path.is_file() and '__pycache__' not in path.parts and (path.suffix in SUFFIXES or path.name == 'LICENSE'):
|
||||
paths.append(path)
|
||||
for path in paths:
|
||||
if path.is_symlink() or not path.resolve().is_relative_to(root):
|
||||
raise ValueError(f'Geen bestanden buiten de bronmap of symlinks toegestaan: {path}')
|
||||
if not path.is_file():
|
||||
raise ValueError(f'Ontbrekend bundelbestand: {path}')
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
# Exclusive mode refuses to replace a previously delivered bundle.
|
||||
with zipfile.ZipFile(output, 'x', zipfile.ZIP_DEFLATED) as archive:
|
||||
archive.writestr('cyd-display/home-like-2432s028.yaml', neutral_base(root))
|
||||
for path in sorted(set(paths)):
|
||||
archive.write(path, 'cyd-display/' + path.relative_to(root).as_posix())
|
||||
return output
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--output', type=Path, default=ROOT / 'dist' / 'cyd-starter.zip')
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
print(f'Deel deze ZIP: {export(ROOT, args.output)}')
|
||||
print('Geen lokale secrets, device.yaml, kalibratiemetingen, logs of firmwarebackups opgenomen.')
|
||||
except (OSError, ValueError) as exc:
|
||||
parser.exit(1, f'Fout: {exc}\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Create local device, calibration and secrets files without overwriting any."""
|
||||
import argparse
|
||||
import base64
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import secrets
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def create(directory, name, friendly_name):
|
||||
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.')
|
||||
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)
|
||||
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}
|
||||
directory = Path(directory)
|
||||
if not directory.is_dir():
|
||||
raise ValueError('De doelmap moet al bestaan en de uitgepakte code bevatten.')
|
||||
for name in files:
|
||||
if (directory / name).exists():
|
||||
raise ValueError(f'{directory / name} bestaat al; niets overschreven. Gebruik een nieuwe kopie van de map.')
|
||||
created = []
|
||||
try:
|
||||
for name, contents in files.items():
|
||||
path = directory / name
|
||||
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
||||
created.append(path)
|
||||
with os.fdopen(fd, 'w', encoding='utf-8') as handle:
|
||||
handle.write(contents)
|
||||
except Exception:
|
||||
for path in created:
|
||||
path.unlink(missing_ok=True)
|
||||
raise
|
||||
return created
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--name', required=True, help='Unieke ESPHome-naam, bijvoorbeeld display-keuken')
|
||||
parser.add_argument('--friendly-name', default='Mijn CYD')
|
||||
parser.add_argument('--directory', type=Path, default=ROOT)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
for path in create(args.directory, args.name, args.friendly_name):
|
||||
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.')
|
||||
except (ValueError, OSError) as exc:
|
||||
parser.exit(1, f'Fout: {exc}\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user