chore: remove research docs from git tracking, add to .gitignore

This commit is contained in:
eddy 2026-07-29 23:53:26 +02:00
commit 15498d025a
14 changed files with 9 additions and 2778 deletions

10
.gitignore vendored
View file

@ -31,4 +31,12 @@ Thumbs.db
*~
# Git worktrees
.worktrees/
.worktrees/
# Research/documentation (not part of the project code)
appstore.md
codereview.md
DC3-24V*.md
DC3-24V*.pdf
esplibalternatief.md
docs/

View file

@ -1,45 +0,0 @@
DC3-24V Capacitieve Touch Switch Sensor Module Momentary/Vergrendeling 10A Regelbare Stroom Elektronische Component 137 verkocht €1,06 46% korting €1,97 Meer dan 3 stuks, 1% extra kortingo ff Pr ij s is inclusief btw | Douanerechten worden berekend b ij het afrekenen ; Extra 2% korting €2,00 korting op €15,00 AI-overzicht van item Disclaimer: deze inhoud is door AI gegenereerd en vertegenwoordigt niet de mening van de verkoper. Het platform en de verkopers aanvaarden geen enkele juridische aansprakel ij kheid in dit verband. DC3-24V capaciteve touch sensor module Deze DC3-24V capacitive touch switch sensor module is ontworpen voor gebruik in elektronische systemen met een spanning van 3 tot 24 V. Ideaal voor automatiseringstoepassingen waar betrouwbare bediening nodig is. Momentary en vergrendelingsmodus De module ondersteunt zowel momentary als vergrendelingsmodus , waardoor flexibele bediening mogel ij k is. Perfect voor toepassingen waar een t ij del ij ke of bl ij vende activering vereist is. Maximaal 10 A stroomregeling Met een maximale stroomcapaciteit van 10 A kan de module zware elektrische belastingen aan. Geschikt voor het schakelen van motoren, verlichting of andere elektrische apparaten in automatiseringssystemen. CE-gecerti fi ceerd en veilig gebruik De module is CE-gecerti fi ceerd , wat aangeeft dat deze voldoet aan Europese veiligheids- en prestatie-eisen. Zorgt voor betrouwbare en veilige werking in diverse omgevingen. Compacte afmetingen en licht gewicht Met een verpakking van 2 x 2 x 1 cm en een gewicht van 0,011 kg is de module compact en licht. Ideaal voor ruimtebeperkte installaties of mobiele systemen. Meer pr ij sinformatie Beoordeling Gerelateerde items Bundel : 3-24V Bek ij k Meer 5 V-24 V 3A d … € 3 , 23 €7,18 -55% 4.8 €2 korting op €15 Dc 3V-15V Ca … € 3 , 27 5 Gratis levering vanaf €1 DC 5 V-30 V T … € 1 , 51 4.6 Bestverkopend op A DC 5 V-30 V T … € 1 , 46 5 Gratis levering vanaf €1 TTP223B Digit … € 2 , 27 €4,54 -50% 120 verkocht €2 korting op €15 ad. Download de AliExpress- app Hi, Eddy Account hc12 433mhz NL / EUR … Winkelwagentje
Speci fi caties Overzicht report Elk €3 07 ≥ 2 stuks Beste pr ij s voor verg Gratis levering vanaf €1 Elk €2 16 ≥ 3 stuks Gebruik anderen Hoogwaardige chemicaliën Geen Communicatiemethode andere Camera uitgerust Nee Modelnummer DC3-24V Compatibiliteit anderen Merknaam occkic Oorsprong Cn (Oorsprong) Staat van montage B ij na klaar Speci fi catie CN-stekker Itemgidsen en documenten Gebruikershandleiding Bek ij ken Kenmerken 1.Brede spanningscompatibiliteit 3-24V, ultralaag standby-stroomverbruik slechts 2μA 2.10A krachtige aandr ij ving, stabiele controle voor krachtige apparaten 3.Dubbele t ij del ij ke/vergrendelingsmodi, flexibele aanpassing aan toepassingsbehoeften 4.Niet-metalen paneeldetratiedetectie, nauwkeurige herkenning door glas/kunststof 5.MOSFET-ontwerp van industriële kwaliteit, sterk anti-interferentievermogen en lange levensduur Productbeschr ij ving Deze module maakt gebruik van de JR223B capacitieve aanraaksensorchip + SI4164 krachtige MOSFET-uitgang, die 3-24V brede ingangsspanningsregeling ondersteunt.De statische stroom van de module is ≤2μA wanneer de schakelaar is uitgeschakeld, en de schakelaar kan een maximale stroom van maximaal 10A regelen.Vaak gebruikt voor aanraakdetectiecontrole op niet-metalen materialen zoals plastic, glas, rubber, acrylpanelen of droge houten oppervlakken.Toepassingen z ij n onder meer aanraakschakelaars, LED-lichtmodi fi caties, auto-aanpassing, ontsnappingsruimtespellen en andere aanraakgestuurde producten. Parameter Bedr ij fsspanning: DC 3-24V Maximale regelbare stroom: 10A Stroomverbruik in stand-by: ≤2 μA Aanraakdetectieafstand: ≤8 mm (kan enigszins variëren afhankel ij k van het materiaal van het afdekpaneel) Voorzorgsmaatregelen: 1.Deze module werkt binnen een spanningsbereik van DC 3-24V.Zorg ervoor dat de voedingsspanning binnen dit bereik valt; overmatige spanning kan de module beschadigen. 2.Bevestig de module aan de binnenkant van het paneel met dubbelz ij dige tape.Zorg ervoor dat er geen luchtspleet bestaat tussen de aanraakmodule en het paneel, aangezien dit de aanraakgevoeligheid en detectieafstand kan beïnvloeden. 3.Wanneer het op glazen spiegels wordt aangebracht, isoleer het verzilverde aanraakgevoelige gebied van de non- touch coating.4.Verm ij d het verminderen van de capaciteit op punt A om de gevoeligheid te verhogen.Minimaliseer in Klantenrecensies Speci fi caties Overzicht Winkel Dit vindt u misschien ook leuk Download de AliExpress- app Hi, Eddy Account NL / EUR … Winkelwagentje
plaats daarvan de paneeldikte en openingen tussen schakelaars en panelen.Hoewel dit de gevoeligheid kan verbeteren, vergroot het ook de kans op valse triggers. 5.Volg strikt het referentieschakelschema voor bedrading. Pakket omvat Module*1 Dubbelz ij dige tape*1 Download de AliExpress- app Hi, Eddy Account NL / EUR … Winkelwagentje
Download de AliExpress- app Hi, Eddy Account NL / EUR … Winkelwagentje
Download de AliExpress- app Hi, Eddy Account NL / EUR … Winkelwagentje
Download de AliExpress- app Hi, Eddy Account NL / EUR … Winkelwagentje
Download de AliExpress- app Hi, Eddy Account NL / EUR … Winkelwagentje
Download de AliExpress- app Hi, Eddy Account NL / EUR … Winkelwagentje
Download de AliExpress- app Hi, Eddy Account NL / EUR … Winkelwagentje
Download de AliExpress- app Hi, Eddy Account NL / EUR … Winkelwagentje
Download de AliExpress- app Hi, Eddy Account NL / EUR … Winkelwagentje
Download de AliExpress- app Hi, Eddy Account NL / EUR … Winkelwagentje
Download de AliExpress- app Hi, Eddy Account NL / EUR … Winkelwagentje
Download de AliExpress- app Hi, Eddy Account NL / EUR … Winkelwagentje
Download de AliExpress- app Hi, Eddy Account NL / EUR … Winkelwagentje
Download de AliExpress- app Hi, Eddy Account NL / EUR … Winkelwagentje
Download de AliExpress- app Hi, Eddy Account NL / EUR … Winkelwagentje
Download de AliExpress- app Hi, Eddy Account NL / EUR … Winkelwagentje
Download de AliExpress- app Hi, Eddy Account NL / EUR … Winkelwagentje
Download de AliExpress- app Hi, Eddy Account NL / EUR … Winkelwagentje
Informatie over b ij komende kosten Voor items die van buiten de Europese Unie worden geleverd, kunt u onderworpen z ij n aan extra kosten voor btw en, indien van toepassing, douanerechten in uw land. Als AliExpress wettel ij k verplicht is om btw te innen, ziet u de pr ij s inclusief btw b ij het afrekenen. Neem voor meer informatie over deze kosten contact op met de belastingdienst en douaneautoriteiten in uw land. Verkocht door Tools-Electronics Store, verwerkt door Tools-Electronics Store Informatie over productconformiteit Disclaimer Disclaimer: Afbeeldingen en productinformatie op deze pagina kunnen automatisch door AI z ij n vertaald. Deze vertalingen worden uitsluitend ter gemak aangeboden en worden geleverd zoals ze z ij n, en kunnen onnauwkeurigheden of inconsistenties bevatten. Tools-Electronics Store 95.9% Positieve feedback | 14K Volgers Aanbevolen uit Tools-Electronics Store Download de AliExpress- app Hi, Eddy Account NL / EUR … Winkelwagentje
Dit vindt u misschien ook leuk VRX-75DF 7,2 GH … € 87 , 99 €84,54 bespaard 3 × €29,33 zonder rente Android Mobiele Telefoon The … € 77 , 69 €143,87 -45% 1 3 × €25,89 zonder rente RX8500-SPI FPV Video-ontva … € 74 , 39 €137,76 -46% 3 × €24,79 zonder rente RX8200 6G-8.2G … € 66 , 99 €69,72 bespaard 3 × €22,33 zonder rente RX7264 FPV-zende € 72 , 99 €137,72 - 3 × €24,33 zonder re Elektrisch Gereedschap … € 4 , 99 €9,94 -49% 4.9 500+ verkocht €2 korting op €15 Gratis levering vanaf €10 Mini Touch Sensor Dimbare Sc … € 1 , 62 €3,12 -48% 5 69 verkocht €2 korting op €15 DC3V-30V Touch Key S … € 2 , 84 5 42 verkocht Gratis levering vanaf €10 12V Capacitieve Aanraa … € 3 , 24 4.8 128 verkocht Gratis levering vanaf €10 Beste pr ij s voor vergel ij kbare 1 € 1 , 99 4.9 Invoerrechten verrek €2,28 bespaard Bundeldeals Universele elektrocardiograaf- … € 1 , 69 5 399 verkocht Dual Button Touch Mod … € 2 , 9 €4,23 -31% 5 28 verkocht €2 korting op €15 Elk €2,76, ≥ 2 stuks DC3V-30V Touch … € 0 , 82 5 600+ verkocht €2 korting op €15 €0,82 bespaard Hi-Link HLK-LD24 … € 0 , 64 5 107 verkocht €2 korting op €15 €0,02 bespaard Contactloos Capacit € 5 , 59 €7,87 -28% 5 104 ver 5 stks Piëzo-elektrische … € 1 , 71 ( €0,34 / stuk ) 4.6 150 verkocht Gratis levering vanaf €10 20/50/100 STKS/PART … € 2 , 35 4.9 1.000+ verkocht Bestverkopend op AliExpress Gratis levering vanaf €10 3/4 Draad Capacitieve … € 3 , 03 5 70 verkocht Gratis levering vanaf €10 30 stks TCRT5000 L ij n Track S … € 9 , 89 1 verkocht A € 3 , 69 €8,02 -53% 4.8 1.000 €2 korting op €15 Elk €3,5, ≥ 2 stuks ad. ad. ad. ad. Download de AliExpress- app Hi, Eddy Account NL / EUR … Winkelwagentje
Help Helpcentrum , Geschillen en meldingen , Retour-en Restitutiebeleid , Melden overtredingen van intellectuele-eigendomsrechten , DSA/OSA-informatie , Integriteitsnal Klachteningang zonder inloggen , Retourbeleid Meertalige AliExpress-sites Pусский , Portuguese , Spanish , French , German , Italian , Dutch , Turkish , Japanese , Korean , Thai , Arabic , Hebrew , Polish Browse by Category Alles populaire , Product , Promotie , Lage pr ij s , Uitstekende Waarde , Beoordelingen , Wiki , Video Alibaba Group Alibaba Group Website , AliExpress , Alimama , Fliggy , Alibaba Cloud , Alibaba International , AliTelecom , DingTalk , Juhuasuan , Taobao Marketplace , Tmall , Taobao Global , AliOS , 1688 Mensen zochten ook naar Dit product behoort tot Home Intellectual Property Protection - Privacy Policy - Cookievoorkeuren - Overzicht - Terms of Use - Informatie voor EU-Consumenten - Overeenkomst voor transac Algemene Voorwaarden voor Consumenten in de EU/EEA/UK - User Information Legal Enquiry Guide © 2010-2025 AliExpress.com. All rights reserved. Quectel Lc29Haa Da Ea Ba Bs … € 14 , 79 4.9 136 verkocht Touch Key Switch Module Aan … € 0 , 69 €0,78 -12% 5 17 verkocht Aanpasbaar Aanraakschakelaar Cap … € 1 , 72 4.8 313 verkocht Gratis levering vanaf €10 Beste pr ij s voor vergel ij kbare Moderne beukenhoute … € 1 , 91 ( €0,64 / stuk ) 4.8 900+ verkocht Gratis levering vanaf €10 € 1 , 28 ( €0,46 / stuk 4.9 5.000 €2 korting op €15 Elk €1,22, ≥ 3 stuks ad. ad. Download de AliExpress- app Hi, Eddy Account NL / EUR … Winkelwagentje

View file

@ -1,205 +0,0 @@
# App Store Publicatie — Sylvia Touch Relay
## Licentie-overzicht Dependencies
| Dependency | Versie | Licentie | Copyleft? | FOSS? |
|---|---|---|---|---|
| `io.github.dkaukov:esp32-flash-lib` | 1.1.17 | **GPL-3.0** | Ja (strong) | Ja |
| `com.eclipsesource.minimal-json:minimal-json` | 0.9.5 | MIT | Nee | Ja |
| `org.slf4j:slf4j-nop` | 2.0.16 | MIT | Nee | Ja |
| `androidx.security:security-crypto` | 1.1.0-alpha06 | Apache 2.0 | Nee | Ja |
| `androidx.core:core-ktx` | 1.12.0 | Apache 2.0 | Nee | Ja |
| `androidx.appcompat:appcompat` | 1.6.1 | Apache 2.0 | Nee | Ja |
### GPL-3.0 Copyleft Implicatie
`esp32-flash-lib` is GPL-3.0. Een app die ertoe linkt (compile-time dependency) moet zelf ook GPL-3.0 (of compatibele licentie) zijn. Dit betekent:
- App **moet** onder GPL-3.0 worden uitgebracht
- Source code moet beschikbaar blijven (GitHub volstaat)
- Geen proprietary SDKs linken (niet van toepassing)
- Voor andere stores (Amazon, Samsung) is dit geen belemmering
### Actie: GPL-3.0 toepassen ✅ Voltooid
1. ✅ `LICENSE` file toevoegen in repo root (volledige GPL-3.0 tekst)
2. ✅ Source file headers toevoegen: `// SPDX-License-Identifier: GPL-3.0-or-later` in alle 6 Kotlin bestanden
3. ✅ `README.md` licentiesectie bijwerken
---
## Google Play Store
### Vereisten (2026)
| Vereiste | Status | Opmerking |
|---|---|---|
| Ontwikkelaarsaccount | ❌ Nog aanmaken | $25 eenmalig |
| Overheids-ID verificatie | ❌ Nog doen | Paspoort/rijbewijs + selfie |
| AAB formaat (geen APK) | ❌ Release build nodig | Nu alleen debug |
| Release signing key | ❌ Aanmaken | `keytool` of Play App Signing |
| targetSdk ≥ 35 | ❌ Nu 34 | Verplicht; ≥ 36 vanaf 31 aug 2026 |
| App icoon (mipmap) | ❌ Ontbreekt | Geen mipmap resources aanwezig |
| Privacy Policy URL | ❌ Nodig | Website of GitHub Pages |
| Data Safety formulier | ❌ Invullen | In Play Console |
| IARC content rating | ❌ Invullen | Vragenlijst in Play Console |
| App signing | ⚠️ Keuze | Play App Signing (aanbevolen) of eigen key |
| `minifyEnabled true` | ⚠️ Aanbevolen | Proguard/R8 rules ontbreken nog |
| `ACCESS_FINE_LOCATION` | ⚠️ Mogelijk overbodig | Sinds Android 12 niet meer nodig voor BLE scan |
### Closed Testing Verplichting
Nieuwe persoonlijke accounts moeten **closed testing** doorlopen:
- Minimaal **12 testers**
- Minimaal **14 opeenvolgende dagen** testen
- Testers moeten de app actief gebruiken
- Na goedkeuring: pas dan kan de app publiek
### Publicatie Stappenplan
1. Ontwikkelaarsaccount aanmaken + ID verificatie
2. `targetSdk` upgraden naar 35 (codereview #7.2)
3. App icoon ontwerpen + mipmap resources toevoegen
4. Release build configureren (signing, minify, proguard)
5. Privacy Policy pagina maken (GitHub Pages of statische site)
6. AAB build maken: `./gradlew bundleRelease`
7. Closed testing opzetten met 12 testers
8. Na 14 dagen: productie-release indienen
---
## Alternatieve App Stores
### Vergelijking
| Store | Kosten | Review tijd | GMS nodig? | FOSS verplicht? | Relevante beperking |
|---|---|---|---|---|---|
| **Samsung Galaxy Store** | Gratis | Weken | Nee | Nee | 16KB page size verplicht voor native libs |
| **Amazon Appstore** | $0 | ~1 dag | Nee | Nee | Laagste drempel, grote userbase (Fire TV/tablets) |
| **F-Droid** | $0 | 2-3 weken | Nee | **Ja** | Bouwt zelf vanuit source; hele codebase + deps moeten FOSS |
| **Huawei AppGallery** | Gratis | Dagen | Nee | Nee | ID verificatie verplicht |
| **Aptoide** | $0 | Uren | Nee | Nee | Minimale review, minder vertrouwd |
### F-Droid Specifiek
- **Compatibel**: alle deps zijn FOSS (GPL-3.0, MIT, Apache 2.0)
- **Vereist**: app moet open-source licentie hebben → GPL-3.0 (verplicht door esp32-flash-lib copyleft)
- **Proces**: F-Droid bouwt de app zelf vanuit source (geen APK/AAB upload)
- **Aanvraag**: merge request in [fdroiddata](https://gitlab.com/fdroid/fdroiddata) repo met build recipe
- **Voordelen**: geen ontwikkelaarskosten, geen account nodig, vertrouwd door FOSS-gemeenschap
- **Nadelen**: kleine userbase, geen automatische updates buiten F-Droid client
### Amazon Appstore Specifiek
- **Laagste drempel**: gratis, snelle review (~1 dag), geen ID verificatie
- **Bereik**: Fire TV, Fire tablets, Kindle — niche maar relevant voor IoT-apps
- **Formaat**: APK of AAB
- **Aanmelden**: [developer.amazon.com](https://developer.amazon.com)
### Samsung Galaxy Store Specifiek
- **Bereik**: alleen Samsung Galaxy toestellen (groot marktaandeel, maar exclusief)
- **Niet-Samsung**: Galaxy Store kan officieel alleen op Samsung toestellen; sideloading APK op andere merken werkt onbetrouwbaar (ROM-afhankelijkheden, craches, geen updates)
- **16KB page size**: als de app native libraries bevat, moeten deze met 16KB page alignment worden gebuild. Onze app heeft geen native libs, dus geen probleem.
- **Aanmelden**: [seller.samsungapps.com](https://seller.samsungapps.com)
### Aptoide
- **Niet zelf-hosted**: jouw store draait binnen Aptoide's infrastructuur, niet op eigen server
- **Gebruikers moeten eerst Aptoide client installeren** voordat ze jouw store kunnen vinden
- **Data breach**: 2020 — 20M gebruikersgegevens gelekt
- **Review**: minimaal (uren), maar minder vertrouwd bij gebruikers
- **Geschikt voor**: grote distributie, niet voor 1-2 apps → overkill voor ons
---
## Eigen App Store (Self-Hosted)
Technisch is een eigen app store simpel: APK hosten op een webserver, gebruiker downloadt direct. Android staat installatie uit onbekende bronnen toe. Er zijn geen Google-licenties of -toestemmingen nodig om een app store te runnen.
### Praktische Overwegingen
- Gebruiker moet "Installatie uit onbekende bronnen" inschakelen; Google Play Protect waarschuwt
- Geen automatische updates tenzij je zelf een update-check in de app bouwt
- **Android Developer Verificatie (vanaf 2027 globaal)**: Google vereist identity verification + package name registratie via Android Developer Console voor installatie op gecertificeerde apparaten. Dit geldt voor óók sideloading. De verificatie is gratis (geen $25 Play-account nodig), maar vereist legitimatiebewijs.
### Self-Hosted Oplossingen Vergelijking
| Aspect | Selfdroid | Aptoide | Simpele webpagina |
|---|---|---|---|
| **Zelf-hosted** | ✅ Ja | ❌ Nee (Aptoide's ecosysteem) | ✅ Ja |
| **Installatie voor gebruiker** | URL in browser → download APK | Eerst Aptoide client, dan store zoeken | URL → download APK |
| **Licentie** | BSD 3-clause | Propriëtair | NVT |
| **Setup** | Python/Flask + nginx + uWSGI | Account op aptoide.com | Statische HTML |
| **Admin paneel** | ✅ Met wachtwoordbeveiliging | Via Aptoide dashboard | Handmatig upload |
| **APK metadata extractie** | ✅ Automatisch (naam, versie, icoon) | ✅ Automatisch | Nee |
| **Android companion app** | Gepland, nooit gebouwd | ✅ Aptoide client | Nee |
| **Data breach history** | Nee | Ja (2020, 20M gebruikers) | Nee |
| **Geschikt voor 1 app** | ✅ Perfect | ❌ Overkill | ✅ Simpelst |
### Aanrader: Selfdroid
**Selfdroid** ([vitlabuda/selfdroid-web-app](https://github.com/vitlabuda/selfdroid-web-app)) — self-hosted Android app store, Python/Flask, BSD-licentie.
Voordelen:
- Admin uploadt APK, metadata automatisch geëxtraheerd
- Gebruikers openen URL op telefoon → downloaden APK direct vanuit browser
- Geen extra app installatie nodig voor gebruikers
- Lichtgewicht: Python 3.7+, Flask, nginx + uWSGI
- Kan op bestaande server draaien
- BSD-licentie compatibel met onze GPL-3.0 app
Deploy stappen:
1. Clone repo, configureer `Settings.py` (admin wachtwoord)
2. `./prepare.sh` voor Python venv + dependencies
3. nginx vhost + uWSGI config (meegeleverd in repo)
4. Upload APK via admin paneel
5. Deel URL met gebruikers
Alternatief voor maximale eenvoud: APK op GitHub Releases hosten + simpele downloadpagina (GitHub Pages). Geen server nodig, maar geen auto-metadata of admin paneel.
---
## Multi-Channel Distributiestrategie
We publiceren parallel via meerdere kanalen. Geen exclusiviteit — Android staat meerdere distributiekanalen naast elkaar toe.
| Kanaal | Bereik | Kosten | Beheer | Prioriteit |
|---|---|---|---|---|
| **F-Droid** | FOSS-gebruikers wereldwijd | Gratis | MR in fdroiddata, F-Droid bouwt zelf | 1e |
| **Samsung Galaxy Store** | Samsung-gebruikers | Gratis | Upload AAB, Samsung review | 2e |
| **Eigen store (Selfdroid)** | Iedereen met directe link | Hosting alleen | APK upload via admin paneel | 3e |
| **Google Play Store** | Grootste bereik | $25 + ID verificatie | Veel vereisten, closed testing | 4e |
### Volgorde
1. **F-Droid** — geen kosten, F-Droid bouwt zelf vanuit source, vertrouwd kanaal
2. **Samsung Galaxy Store** — gratis, bereikt Samsung-gebruikers die geen F-Droid hebben
3. **Selfdroid (eigen store)** — APK distributie voor iedereen buiten F-Droid/Samsung, geen goedkeuringsproces
4. **Google Play Store** — grootste bereik maar veel vereisten (account, closed testing, targetSdk 35)
### Benodigde stappen vóór publicatie (alle kanalen)
| Stap | F-Droid | Galaxy Store | Eigen store | Google Play |
|---|---|---|---|---|
| GPL-3.0 licentie | ✅ Gedaan | — | — | — |
| Release build (signed) | F-Droid bouwt zelf | ✅ Nodig | ✅ Nodig | ✅ Nodig (AAB) |
| App icoon (mipmap) | ✅ Nodig | ✅ Nodig | — | ✅ Nodig |
| targetSdk ≥ 35 | — | — | — | ✅ Verplicht |
| Privacy Policy URL | — | — | — | ✅ Verplicht |
| Developer verificatie | — | — | 2027 | ✅ Automatisch |
| Proguard rules | Aanbevolen | Aanbevolen | — | Aanbevolen |
---
## Openstaande Codereview Punten die Publicatie Blokkeren
| # | Prioriteit | Bevinding | Relevantie voor publicatie |
|---|---|---|---|
| 7.2 | LAAG | compileSdk/targetSdk → 35 | **Blokkeert Google Play** |
| 4.5 | LAAG | Proguard rules | **Aanbevolen** voor release build |
| 6.2 | LAAG | Dark mode | Gebruikerservaring |
| — | — | App icoon (mipmap) | **Verplicht** voor alle stores |
| — | — | Release build signing | **Verplicht** voor alle stores |
| — | — | Privacy Policy URL | **Verplicht** voor Google Play |

View file

@ -1,21 +0,0 @@
#### 5.3 Broakkoppel exception swallowing (LAAG) — **OPGELOST**
**Bestand:** `SettingsActivity.kt:191-193`, `478`
```kotlin
try { unregisterReceiver(usbAttachedReceiver) } catch (e: Exception) {
Log.w(TAG, "Failed to unregister usbAttachedReceiver", e)
}
try { unregisterReceiver(usbPermissionReceiver) } catch (e: Exception) {
Log.w(TAG, "Failed to unregister usbPermissionReceiver", e)
}
try { unregisterReceiver(usbDetachedReceiver) } catch (e: Exception) {
Log.w(TAG, "Failed to unregister usbDetachedReceiver", e)
}
try { connection.close() } catch (e: Exception) {
Log.w(TAG, "Failed to close USB connection", e)
}
```
**Origineel:** Lege catch-blokken die exceptions.swallowen zonder logging.
**Opgelost:** Alle catches loggen nu een warning met de volledige exception ( inclusief stack trace) voor betere diagnose.

View file

@ -1,192 +0,0 @@
# ESP32-PICO-D4 (M5Stack Atom) Port Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Create a sidebranch for ESP32-PICO-D4 board variant (M5Stack Atom) with touch sensor on G33 (open-drain, needs pullup) and relay on G32, then compile, bundle with Android app, and deploy to phone via wireless debugging.
**Architecture:**
- Create sidebranch `esp32picod4` using git worktree or branch
- Update `platformio.ini` to use `m5stack-atom` board (ESP32-PICO-D4)
- Update pin definitions in `src/main.cpp`: `SENSOR_PIN` to 33, `RELAY_PIN` to 32, `LED_PIN` to 27 (RGB LED on Atom)
- Recompile firmware with PlatformIO
- Copy firmware binaries to Android app assets via `tools/copy_firmware.sh`
- Build Android APK and install via ADB wireless
- Verify connection and operation
**Tech Stack:** C++/Arduino, PlatformIO, Android/Kotlin/Gradle, ADB (wireless debugging)
---
### Task 1: Create sidebranch using git worktree
**Files:**
- Create: `.worktrees/esp32picod4` (git worktree directory)
- [ ] **Step 1: Create and switch to sidebranch in worktree**
```bash
git worktree add -b esp32picod4 .worktrees/esp32picod4
```
- [ ] **Step 2: Verify branch is active**
Run: `git branch --show-current`
Expected: `esp32picod4`
- [ ] **Step 3: Commit** (optional for now; we commit after changes)
### Task 2: Update PlatformIO configuration for PICO-D4 (M5Stack Atom)
**Files:**
- Modify: `platformio.ini:3`
- [ ] **Step 1: Change board to m5stack-atom**
```ini
board = m5stack-atom
```
Note: The M5Stack Atom (Lite/Matrix) uses ESP32-PICO-D4. The board ID in PlatformIO is `m5stack-atom`.
- [ ] **Step 2: Verify board exists in PlatformIO**
Run: `pio boards | grep m5stack-atom`
Expected: line containing `m5stack-atom`
### Task 3: Update pin definitions for M5Stack Atom
**Files:**
- Modify: `src/main.cpp:7-9`
- [ ] **Step 1: Change SENSOR_PIN to 33, RELAY_PIN to 32**
```cpp
#define SENSOR_PIN 33 // G33, touch sensor (open-drain, needs pullup)
#define RELAY_PIN 32 // G32, relay output
// LED_PIN left unchanged for now (46 from original)
```
**Important:** G33 is configured with `INPUT_PULLUP` in setup (line 136) — that remains correct for open-drain sensor. No change needed there.
### Task 4: Compile firmware for new board
**Files:** N/A (build artifacts)
- [ ] **Step 1: Clean previous build and compile**
Run: `pio run --environment m5stack-atom --target clean && pio run --environment m5stack-atom`
Expected: Build succeeds, output binaries in `.pio/build/m5stack-atom/`
- [ ] **Step 2: Verify build output files exist**
Run: `ls .pio/build/m5stack-atom/firmware*.bin .pio/build/m5stack-atom/bootloader*.bin .pio/build/m5stack-atom/partitions*.bin .pio/build/m5stack-atom/boot_app0*.bin`
Expected: 4 files: `firmware.bin`, `bootloader.bin`, `partitions.bin`, `boot_app0.bin`
### Task 5: Copy firmware binaries to Android app assets
**Files:**
- Modify/Create: `android/app/src/main/assets/firmware/` (destination)
- [ ] **Step 1: Run copy script**
```bash
bash tools/copy_firmware.sh
```
- [ ] **Step 2: Verify files copied**
Run: `ls android/app/src/main/assets/firmware/`
Expected: `bootloader.bin`, `partitions.bin`, `firmware.bin`, `boot_app0.bin`
- [ ] **Step 3: Commit changes**
```bash
git add platformio.ini src/main.cpp
git commit -m "feat: port to M5Stack Atom ESP32-PICO-D4 (touch=G33, relay=G32, LED=G27)"
```
### Task 6: Build Android APK and install via wireless ADB
**Files:**
- Build output: `android/app/build/outputs/apk/debug/app-debug.apk`
- [ ] **Step 1: Build the Android app**
```bash
cd android
JAVA_HOME=/home/linuxbrew/.linuxbrew/opt/openjdk@21/libexec ./gradlew assembleDebug
```
Expected: APK built at `app/build/outputs/apk/debug/app-debug.apk`
- [ ] **Step 2: Verify APK exists**
Run: `ls android/app/build/outputs/apk/debug/app-debug.apk`
Expected: file exists
- [ ] **Step 3: Get phone IP and check ADB wireless connection**
We'll use the adb-wireless skill to connect to the Android phone. First check if already connected:
Run: `adb devices`
Expected: If device listed as `<ip>:5555`, we're connected. If not, we need to connect.
- [ ] **Step 4: If not connected, connect via ADB wireless** (using adb-wireless skill)
- [ ] **Step 5: Install APK on phone**
Run: `adb -s <device_ip>:5555 install -r android/app/build/outputs/apk/debug/app-debug.apk`
Expected: Success output
### Task 7: Deploy firmware to ESP32 via Android app
**Files:** N/A (device action)
- [ ] **Step 1: Launch app on phone** (manual or via ADB)
```bash
adb -s <device_ip>:5555 shell am start -n com.eddy.atomrelay/.MainActivity
```
- [ ] **Step 2: Put ESP32 in download mode** (manual: hold reset ~2s until green LED)
- [ ] **Step 3: Connect ESP32 via USB-OTG to phone** (manual)
- [ ] **Step 4: Use app to flash firmware** (manual: Settings → Firmware Flashen)
- [ ] **Step 5: Wait for flash completion and disconnect**
### Task 8: Provision HMAC key via app
**Files:** N/A (device action)
- [ ] **Step 1: With ESP32 connected via USB (not in download mode), open app**
- [ ] **Step 2: Go to Settings → Sleutel Uitwisselen**
- [ ] **Step 3: App reads or generates key and provisions ESP**
- [ ] **Step 4: Verify toast "Beveiligingssleutel gelezen"**
### Task 9: Test BLE connection and relay control
**Files:** N/A (manual verification)
- [ ] **Step 1: Ensure phone and ESP32 are powered and within range**
- [ ] **Step 2: Open app → auto-scan should find "AtomS3R-Relay"**
- [ ] **Step 3: Verify status shows "Connected"**
- [ ] **Step 4: Hold HOLD button → relay should activate (verify with multimeter or relay click)**
- [ ] **Step 5: Release → relay deactivates**
- [ ] **Step 6: Touch sensor on G33 should also control relay** (manual test)
---
## Self-Review
**1. Spec coverage:**
- Sidebranch `esp32picod4`: Task 1 ✓
- Compile for ESP32-PICO-D4 (M5Stack Atom): Task 2 ✓
- Relay on G32: Task 3 ✓
- Touch sensor on G33 with pullup: Task 3 ✓
- LED on G27 (Atom's RGB LED): Task 3 ✓
- Bundle binary with Android app: Task 5 ✓
- Deploy to Android phone with wireless debugging: Task 6 ✓
- Wireless debugging setup: Task 6 (adb-wireless) ✓
**2. Placeholder scan:** No placeholders — all steps contain explicit commands/paths.
**3. Type consistency:** Pin numbers consistent: 33 (SENSOR_PIN), 32 (RELAY_PIN), 27 (LED_PIN).

View file

@ -1,499 +0,0 @@
# Code Review Fixes Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Fix all Critical, Important, and selected Minor issues from the code review to make the Android app production-ready.
**Architecture:** Incremental fixes to existing files. New `UsbUtils.kt` utility object to eliminate code duplication. No architectural changes — just bug fixes, lifecycle hardening, and cleanup.
**Tech Stack:** Kotlin, Android SDK API 24-34, USB Host API, BLE API, esp32-flash-lib
---
## File Structure
| File | Action | Responsibility |
|---|---|---|
| `UsbUtils.kt` | **Create** | Shared USB utility functions (findCdcDataInterface, findBulkEndpoint) |
| `MainActivity.kt` | **Modify** | Fix receiver flag, handler cleanup, onPause scan stop, boolean state |
| `SettingsActivity.kt` | **Modify** | Thread lifecycle check, use UsbUtils |
| `EspFlasher.kt` | **Modify** | try/finally connection cleanup, progress mapping, use UsbUtils |
| `UsbSerialTransport.kt` | **Modify** | Throw IOException on read error |
| `AndroidManifest.xml` | **Modify** | allowBackup=false |
| `activity_settings.xml` | **Modify** | Fix download-modus text |
---
### Task 1: Create UsbUtils utility object
**Files:**
- Create: `android/app/src/main/java/com/eddy/atomrelay/UsbUtils.kt`
- [ ] **Step 1: Create UsbUtils.kt**
```kotlin
package com.eddy.atomrelay
import android.hardware.usb.UsbConstants
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbEndpoint
import android.hardware.usb.UsbInterface
object UsbUtils {
private const val CDC_DATA_INTERFACE_CLASS = 0x0A
fun findCdcDataInterface(device: UsbDevice): UsbInterface? {
for (i in 0 until device.interfaceCount) {
val intf = device.getInterface(i)
if (intf.interfaceClass == CDC_DATA_INTERFACE_CLASS) {
return intf
}
}
return null
}
fun findBulkEndpoint(intf: UsbInterface, direction: Int): UsbEndpoint? {
for (i in 0 until intf.endpointCount) {
val ep = intf.getEndpoint(i)
if (ep.type == UsbConstants.USB_ENDPOINT_XFER_BULK && ep.direction == direction) {
return ep
}
}
return null
}
}
```
- [ ] **Step 2: Build to verify compilation**
Run: `JAVA_HOME=/home/linuxbrew/.linuxbrew/Cellar/openjdk@21/21.0.12/libexec ./gradlew assembleDebug`
Expected: BUILD SUCCESSFUL
- [ ] **Step 3: Commit**
```bash
git add android/app/src/main/java/com/eddy/atomrelay/UsbUtils.kt
git commit -m "Add UsbUtils utility object for shared USB functions"
```
---
### Task 2: Fix UsbSerialTransport.read() — throw IOException on error
**Files:**
- Modify: `android/app/src/main/java/com/eddy/atomrelay/UsbSerialTransport.kt:15-18`
- [ ] **Step 1: Replace read method**
Old:
```kotlin
override fun read(buffer: ByteArray, length: Int): Int {
val n = connection.bulkTransfer(epIn, buffer, length, 5000)
if (n < 0) return 0
return n
}
```
New:
```kotlin
override fun read(buffer: ByteArray, length: Int): Int {
val n = connection.bulkTransfer(epIn, buffer, length, 5000)
if (n < 0) throw IOException("USB read failed")
return n
}
```
- [ ] **Step 2: Build to verify**
Run: `JAVA_HOME=/home/linuxbrew/.linuxbrew/Cellar/openjdk@21/21.0.12/libexec ./gradlew assembleDebug`
Expected: BUILD SUCCESSFUL
- [ ] **Step 3: Commit**
```bash
git add android/app/src/main/java/com/eddy/atomrelay/UsbSerialTransport.kt
git commit -m "Fix UsbSerialTransport.read() to throw IOException on error"
```
---
### Task 3: Fix EspFlasher — connection leak + progress mapping + UsbUtils
**Files:**
- Modify: `android/app/src/main/java/com/eddy/atomrelay/EspFlasher.kt`
- [ ] **Step 1: Replace doFlash with try/finally and progress mapping**
Replace the entire `doFlash` method and the `progressCallback` with:
```kotlin
private fun doFlash(usbManager: UsbManager, device: UsbDevice, callback: FlashCallback) {
Thread.sleep(500)
val connection = usbManager.openDevice(device)
?: throw IOException("Kan USB-apparaat niet openen")
try {
val intf = UsbUtils.findCdcDataInterface(device)
?: throw IOException("Geen CDC data interface gevonden")
if (!connection.claimInterface(intf, true)) {
throw IOException("Kan USB interface niet claimen")
}
val epOut = UsbUtils.findBulkEndpoint(intf, UsbConstants.USB_DIR_OUT)
?: throw IOException("Geen USB OUT endpoint gevonden")
val epIn = UsbUtils.findBulkEndpoint(intf, UsbConstants.USB_DIR_IN)
?: throw IOException("Geen USB IN endpoint gevonden")
postProgress(callback, 0, "Verbinden met ESP bootloader...")
val transport = UsbSerialTransport(connection, epOut, epIn)
val assets = context.assets
val bootloader = assets.open("firmware/bootloader.bin").readBytes()
val partitions = assets.open("firmware/partitions.bin").readBytes()
val bootApp0 = assets.open("firmware/boot_app0.bin").readBytes()
val firmware = assets.open("firmware/firmware.bin").readBytes()
val totalSize = bootloader.size + partitions.size + bootApp0.size + firmware.size
data class FlashStep(val name: String, val address: Int, val data: ByteArray, val baseProgress: Int, val range: Int, val compress: Boolean)
val steps = listOf(
FlashStep("Bootloader", 0x0, bootloader, 30, 10, false),
FlashStep("Partitie tabel", 0x8000, partitions, 40, 10, false),
FlashStep("Boot app0", 0xe000, bootApp0, 50, 10, false),
FlashStep("Firmware", 0x10000, firmware, 60, 35, true)
)
postProgress(callback, 5, "Sync met bootloader...")
val api = EspFlasherApi.connect(transport)
.withCallBack(object : ProgressCallback {
override fun onProgress(pct: Float) {
Log.d(TAG, "Lib progress: $pct%")
}
override fun onInfo(value: String) {
Log.d(TAG, "Info: $value")
}
})
postProgress(callback, 10, "Chip detecteren...")
val detected = api.chipDetect()
val chipId = detected.getChipId()
Log.i(TAG, "Detected chip: $chipId")
postProgress(callback, 15, "Chip: ${chipId.readableName}")
postProgress(callback, 20, "Stub laden...")
val stub = detected.loadStub()
postProgress(callback, 25, "Flash wissen...")
stub.eraseFlash()
for (step in steps) {
val sizeStr = if (step.data.size > 1024) "${step.data.size / 1024}KB" else "${step.data.size} bytes"
postProgress(callback, step.baseProgress, "${step.name} schrijven ($sizeStr)...")
val writeStub = if (step.compress) stub.withCompression(true) else stub.withCompression(false)
writeStub.writeFlash(step.address, step.data)
val written = steps.takeWhile { it !== step }.sumOf { it.data.size } + step.data.size
val overallProgress = step.baseProgress + (step.range * step.data.size / totalSize).coerceAtLeast(1) + step.range
postProgress(callback, minOf(step.baseProgress + step.range, 95), "${step.name} geschreven")
}
postProgress(callback, 95, "ESP resetten...")
stub.reset()
postProgress(callback, 100, "Firmware geflashed!")
handler.post { callback.onComplete() }
} finally {
connection.close()
}
}
```
- [ ] **Step 2: Remove the now-duplicated private methods**
Delete `findCdcDataInterface` and `findBulkEndpoint` from EspFlasher.kt (they are now in UsbUtils).
- [ ] **Step 3: Build to verify**
Run: `JAVA_HOME=/home/linuxbrew/.linuxbrew/Cellar/openjdk@21/21.0.12/libexec ./gradlew assembleDebug`
Expected: BUILD SUCCESSFUL
- [ ] **Step 4: Commit**
```bash
git add android/app/src/main/java/com/eddy/atomrelay/EspFlasher.kt
git commit -m "Fix EspFlasher: connection leak, progress mapping, use UsbUtils"
```
---
### Task 4: Fix SettingsActivity — UsbUtils + thread lifecycle
**Files:**
- Modify: `android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt`
- [ ] **Step 1: Replace findCdcDataInterface and findBulkEndpoint calls with UsbUtils**
In `readKeyFromEsp`, replace:
```kotlin
val intf = findCdcDataInterface(device)
```
with:
```kotlin
val intf = UsbUtils.findCdcDataInterface(device)
```
Replace:
```kotlin
val epOut = findBulkEndpoint(intf, UsbConstants.USB_DIR_OUT)
```
with:
```kotlin
val epOut = UsbUtils.findBulkEndpoint(intf, UsbConstants.USB_DIR_OUT)
```
Replace:
```kotlin
val epIn = findBulkEndpoint(intf, UsbConstants.USB_DIR_IN)
```
with:
```kotlin
val epIn = UsbUtils.findBulkEndpoint(intf, UsbConstants.USB_DIR_IN)
```
- [ ] **Step 2: Delete the duplicated private methods**
Delete `findCdcDataInterface` and `findBulkEndpoint` from SettingsActivity.kt.
- [ ] **Step 3: Add lifecycle check in thread callback**
In `doKeyExchange`, replace the `Thread` block:
```kotlin
Thread {
val result = readKeyFromEsp(usbManager, device)
handler.post {
```
with:
```kotlin
Thread {
val result = readKeyFromEsp(usbManager, device)
if (isFinishing || isDestroyed) return@Thread
handler.post {
```
- [ ] **Step 4: Build to verify**
Run: `JAVA_HOME=/home/linuxbrew/.linuxbrew/Cellar/openjdk@21/21.0.12/libexec ./gradlew assembleDebug`
Expected: BUILD SUCCESSFUL
- [ ] **Step 5: Commit**
```bash
git add android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt
git commit -m "Fix SettingsActivity: use UsbUtils, add lifecycle guard"
```
---
### Task 5: Fix MainActivity — receiver flag, handler cleanup, onPause, boolean state
**Files:**
- Modify: `android/app/src/main/java/com/eddy/atomrelay/MainActivity.kt`
- [ ] **Step 1: Fix registerReceiver flag**
In `onCreate`, replace:
```kotlin
registerReceiver(bondReceiver, IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED))
```
with:
```kotlin
registerReceiver(bondReceiver, IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED), Context.RECEIVER_NOT_EXPORTED)
```
- [ ] **Step 2: Add SCAN_TIMEOUT_MS constant**
In companion object, add:
```kotlin
private const val SCAN_TIMEOUT_MS = 10000L
```
- [ ] **Step 3: Add boolean for scan timeout state**
Add field:
```kotlin
private var scanTimedOut = false
```
- [ ] **Step 4: Fix stopScan to use boolean**
Replace `stopScan`:
```kotlin
@SuppressLint("MissingPermission")
private fun stopScan() {
if (!isScanning) return
isScanning = false
val adapter = BluetoothAdapter.getDefaultAdapter()
adapter?.bluetoothLeScanner?.stopScan(scanCallback)
if (statusText.text == "Scanning...") {
statusText.text = "Device not found"
}
}
```
with:
```kotlin
@SuppressLint("MissingPermission")
private fun stopScan() {
if (!isScanning) return
isScanning = false
val adapter = BluetoothAdapter.getDefaultAdapter()
adapter?.bluetoothLeScanner?.stopScan(scanCallback)
if (scanTimedOut) {
statusText.text = "Device not found"
}
}
```
- [ ] **Step 5: Fix startScan to use constant and boolean**
In `startScan`, replace:
```kotlin
handler.postDelayed({
if (isScanning) stopScan()
}, 10000)
```
with:
```kotlin
scanTimedOut = true
handler.postDelayed({
if (isScanning) stopScan()
}, SCAN_TIMEOUT_MS)
```
And add `scanTimedOut = false` after `isScanning = true`:
```kotlin
isScanning = true
scanTimedOut = false
statusText.text = "Scanning..."
```
- [ ] **Step 6: Add onPause**
Add after `onResume`:
```kotlin
override fun onPause() {
super.onPause()
stopScan()
}
```
- [ ] **Step 7: Fix onDestroy — add handler cleanup**
Replace `onDestroy`:
```kotlin
@SuppressLint("UnspecifiedRegisterReceiverFlag")
override fun onDestroy() {
super.onDestroy()
stopScan()
bluetoothGatt?.close()
unregisterReceiver(bondReceiver)
}
```
with:
```kotlin
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacksAndMessages(null)
stopScan()
bluetoothGatt?.close()
try { unregisterReceiver(bondReceiver) } catch (e: Exception) {}
}
```
- [ ] **Step 8: Build to verify**
Run: `JAVA_HOME=/home/linuxbrew/.linuxbrew/Cellar/openjdk@21/21.0.12/libexec ./gradlew assembleDebug`
Expected: BUILD SUCCESSFUL
- [ ] **Step 9: Commit**
```bash
git add android/app/src/main/java/com/eddy/atomrelay/MainActivity.kt
git commit -m "Fix MainActivity: receiver flag, handler cleanup, onPause, boolean state"
```
---
### Task 6: Fix AndroidManifest — allowBackup=false
**Files:**
- Modify: `android/app/src/main/AndroidManifest.xml:14`
- [ ] **Step 1: Change allowBackup**
Replace:
```xml
android:allowBackup="true"
```
with:
```xml
android:allowBackup="false"
```
- [ ] **Step 2: Commit**
```bash
git add android/app/src/main/AndroidManifest.xml
git commit -m "Disable allowBackup to protect HMAC key from extraction"
```
---
### Task 7: Fix activity_settings.xml — download-modus text
**Files:**
- Modify: `android/app/src/main/res/layout/activity_settings.xml:31`
- [ ] **Step 1: Fix text**
Replace:
```xml
android:text="Sluit de ESP aan op USB en zet de ESP in download-modus (houd reset 2s in tot de groene LED knippert). Tap dan op 'Firmware Flashen'."
```
with:
```xml
android:text="Zet de ESP in download-modus (houd reset 2s in tot de groene LED aangaat) en tik op 'Firmware Flashen'."
```
- [ ] **Step 2: Commit**
```bash
git add android/app/src/main/res/layout/activity_settings.xml
git commit -m "Fix download-modus instruction text in settings layout"
```
---
### Task 8: Final build and install
- [ ] **Step 1: Build**
Run: `JAVA_HOME=/home/linuxbrew/.linuxbrew/Cellar/openjdk@21/21.0.12/libexec ./gradlew assembleDebug`
Expected: BUILD SUCCESSFUL
- [ ] **Step 2: Install**
Run: `adb install -r android/app/build/outputs/apk/debug/app-debug.apk`
Expected: Success
- [ ] **Step 3: Commit plan document**
```bash
git add docs/superpowers/plans/2026-07-26-code-review-fixes.md
git commit -m "Add code review fixes implementation plan"
```

View file

@ -1,499 +0,0 @@
# ESP32-PICO-D4 (M5Stack Atom Lite) Port — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Port the Sylvia Touch Relay project from ESP32-S3-Pico to ESP32-PICO-D4 (Atom Lite) with relay on G32, touch on G33 (pullup), SK6812 LED on G27, CDC-class USB detection in the Android app, and deploy via ADB wireless.
**Architecture:** Create side branch `esp32picod4` via git worktree. Replace platformio.ini S3 config with `m5stack-atom` board + FastLED library. Update pin definitions and LED code in main.cpp. Replace Android USB VID filter with CDC-ACM class filter. No DTR/RTS changes needed — the esp32-flash-lib handles it correctly.
**Tech Stack:** C++/Arduino (FastLED), PlatformIO (espressif32@6.7.0), Android/Kotlin (AGP 8.5.2, Gradle 8.14), ADB (wireless debugging)
---
### Task 1: Create sidebranch using git worktree
**Files:**
- Create: `.worktrees/esp32picod4/` (git worktree directory)
- [ ] **Step 1: Create and switch to sidebranch in worktree**
```bash
git worktree add -b esp32picod4 .worktrees/esp32picod4
```
- [ ] **Step 2: Verify branch is active**
```bash
git -C .worktrees/esp32picod4 branch --show-current
```
Expected: `esp32picod4`
---
### Task 2: Update PlatformIO configuration for PICO-D4
**Files:**
- Modify: `platformio.ini`
**Worktree path:** `.worktrees/esp32picod4/platformio.ini`
- [ ] **Step 1: Replace entire platformio.ini content**
```ini
[env:m5stack-atom]
platform = espressif32@6.7.0
board = m5stack-atom
framework = arduino
build_flags =
-DESP32
-DCORE_DEBUG_LEVEL=5
lib_deps =
fastled/FastLED@^3.7.0
upload_port = /dev/ttyUSB0
upload_speed = 115200
```
- [ ] **Step 2: Verify m5stack-atom board exists in PlatformIO**
```bash
pio boards --json-output 2>/dev/null | python3 -c "import json,sys; boards=[b['id'] for b in json.load(sys.stdin) if 'm5stack-atom' in b['id']]; print('\n'.join(boards) if boards else 'NOT FOUND')"
```
Expected: `m5stack-atom` (or similar variant)
- [ ] **Step 3: Commit platformio.ini**
```bash
git -C .worktrees/esp32picod4 add platformio.ini
git -C .worktrees/esp32picod4 commit -m "feat: switch platformio config to m5stack-atom (ESP32-PICO-D4)"
```
---
### Task 3: Update firmware for Atom Lite (pins, LED, BLE name)
**Files:**
- Modify: `src/main.cpp`
**Worktree path:** `.worktrees/esp32picod4/src/main.cpp`
- [ ] **Step 1: Replace pin definitions (lines 10-12)**
Replace:
```cpp
#define SENSOR_PIN 38
#define RELAY_PIN 1
#define LED_PIN 46
```
With:
```cpp
#include <FastLED.h>
#define SENSOR_PIN 33
#define RELAY_PIN 32
#define LED_PIN 27
#define NUM_LEDS 1
CRGB leds[NUM_LEDS];
```
- [ ] **Step 2: Replace setup() LED init and blink code (lines 139-149)**
Replace:
```cpp
pinMode(SENSOR_PIN, INPUT_PULLUP);
pinMode(RELAY_PIN, OUTPUT);
pinMode(LED_PIN, OUTPUT);
for (int i = 0; i < 3; i++) {
digitalWrite(LED_PIN, HIGH);
delay(200);
digitalWrite(LED_PIN, LOW);
delay(200);
}
Serial.println("LED blink done, starting BLE...");
```
With:
```cpp
pinMode(SENSOR_PIN, INPUT_PULLUP);
pinMode(RELAY_PIN, OUTPUT);
FastLED.addLeds<SK6812, LED_PIN, GRB>(leds, NUM_LEDS);
FastLED.setBrightness(32);
for (int i = 0; i < 3; i++) {
leds[0] = CRGB::Green;
FastLED.show();
delay(200);
leds[0] = CRGB::Black;
FastLED.show();
delay(200);
}
Serial.println("LED blink done, starting BLE...");
```
- [ ] **Step 3: Replace Serial banner and BLE device name**
Replace (line 125):
```cpp
Serial.println("=== Atom VoiceS3R BLE Relay ===");
```
With:
```cpp
Serial.println("=== Atom Lite BLE Relay ===");
```
Replace (line 151):
```cpp
BLEDevice::init("AtomS3R-Relay");
```
With:
```cpp
BLEDevice::init("SyldaServer");
```
Replace (line 174):
```cpp
Serial.println("BLE: advertising started as 'AtomS3R-Relay'");
```
With:
```cpp
Serial.println("BLE: advertising started as 'SyldaServer'");
```
- [ ] **Step 4: Verify main.cpp has no remaining S3-specific or AtomS3R references**
```bash
grep -n "S3R\|S3\|Atom Voice\|46\|G38\|G1\b" .worktrees/esp32picod4/src/main.cpp
```
Expected: no output (no matches)
- [ ] **Step 5: Commit main.cpp**
```bash
git -C .worktrees/esp32picod4 add src/main.cpp
git -C .worktrees/esp32picod4 commit -m "feat: port firmware to Atom Lite (G32 relais, G33 touch, G27 SK6812 LED, SyldaServer BLE)"
```
---
### Task 4: Update copy_firmware.sh for new environment name
**Files:**
- Modify: `tools/copy_firmware.sh`
**Worktree path:** `.worktrees/esp32picod4/tools/copy_firmware.sh`
- [ ] **Step 1: Change environment directory name (line 4)**
Replace:
```bash
BINARY_DIR="$(cd "$(dirname "$0")/.." && pwd)/.pio/build/m5stack-atoms3r"
```
With:
```bash
BINARY_DIR="$(cd "$(dirname "$0")/.." && pwd)/.pio/build/m5stack-atom"
```
- [ ] **Step 2: Commit**
```bash
git -C .worktrees/esp32picod4 add tools/copy_firmware.sh
git -C .worktrees/esp32picod4 commit -m "chore: update copy_firmware.sh for m5stack-atom environment"
```
---
### Task 5: Add CDC-class USB detection to UsbUtils.kt
**Files:**
- Modify: `android/app/src/main/java/com/eddy/atomrelay/UsbUtils.kt`
**Worktree path:** `.worktrees/esp32picod4/android/app/src/main/java/com/eddy/atomrelay/UsbUtils.kt`
- [ ] **Step 1: Add hasCdcDataInterface function**
After the `findBulkEndpoint` function (before the closing `}` of `object UsbUtils`), add:
```kotlin
fun hasCdcDataInterface(device: UsbDevice): Boolean {
for (i in 0 until device.interfaceCount) {
if (device.getInterface(i).interfaceClass == CDC_DATA_INTERFACE_CLASS) {
return true
}
}
return false
}
```
- [ ] **Step 2: Commit**
```bash
git -C .worktrees/esp32picod4 add android/app/src/main/java/com/eddy/atomrelay/UsbUtils.kt
git -C .worktrees/esp32picod4 commit -m "feat: add CDC-class USB detection for universal bridge support"
```
---
### Task 6: Replace VID filter with CDC-class filter in SettingsActivity.kt
**Files:**
- Modify: `android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt`
**Worktree path:** `.worktrees/esp32picod4/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt`
- [ ] **Step 1: Remove ESP_VENDOR_ID constant (line 39)**
Delete:
```kotlin
private const val ESP_VENDOR_ID = 0x303a
```
- [ ] **Step 2: Replace VID check in usbAttachedReceiver (line 67)**
Replace:
```kotlin
if (device?.vendorId == ESP_VENDOR_ID) {
```
With:
```kotlin
if (device != null && UsbUtils.hasCdcDataInterface(device)) {
```
- [ ] **Step 3: Replace VID check in usbPermissionReceiver (line 85)**
Replace:
```kotlin
val device = usbManager.deviceList.values.firstOrNull { it.vendorId == ESP_VENDOR_ID }
```
With:
```kotlin
val device = usbManager.deviceList.values.firstOrNull { UsbUtils.hasCdcDataInterface(it) }
```
- [ ] **Step 4: Replace VID check in usbDetachedReceiver (line 101)**
Replace:
```kotlin
if (device?.vendorId == ESP_VENDOR_ID) {
```
With:
```kotlin
if (device != null && UsbUtils.hasCdcDataInterface(device)) {
```
- [ ] **Step 5: Replace VID check in startKeyExchange (line 208)**
Replace:
```kotlin
val device = usbManager.deviceList.values.firstOrNull { it.vendorId == ESP_VENDOR_ID }
```
With:
```kotlin
val device = usbManager.deviceList.values.firstOrNull { UsbUtils.hasCdcDataInterface(it) }
```
- [ ] **Step 6: Replace VID check in startFlashing (line 256)**
Replace:
```kotlin
val device = usbManager.deviceList.values.firstOrNull { it.vendorId == ESP_VENDOR_ID }
```
With:
```kotlin
val device = usbManager.deviceList.values.firstOrNull { UsbUtils.hasCdcDataInterface(it) }
```
- [ ] **Step 7: Verify no remaining ESP_VENDOR_ID references**
```bash
grep -n "ESP_VENDOR_ID\|vendorId.*==" .worktrees/esp32picod4/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt
```
Expected: no output
- [ ] **Step 8: Commit**
```bash
git -C .worktrees/esp32picod4 add android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt
git -C .worktrees/esp32picod4 commit -m "feat: replace USB VID filter with CDC-ACM class detection"
```
---
### Task 7: Compile firmware
**Files:** N/A (build artifacts in `.pio/build/m5stack-atom/`)
**Work tree path:** `.worktrees/esp32picod4/`
- [ ] **Step 1: Clean and compile**
```bash
pio run -d .worktrees/esp32picod4 -e m5stack-atom -t clean
pio run -d .worktrees/esp32picod4 -e m5stack-atom
```
Expected: `SUCCESS` at end of output
- [ ] **Step 2: Verify build output files**
```bash
ls -la .worktrees/esp32picod4/.pio/build/m5stack-atom/firmware.bin \
.worktrees/esp32picod4/.pio/build/m5stack-atom/bootloader.bin \
.worktrees/esp32picod4/.pio/build/m5stack-atom/partitions.bin
```
Expected: all 3 files exist with non-zero size
---
### Task 8: Copy firmware binaries to Android app assets
**Files:**
- Modify: `android/app/src/main/assets/firmware/` (destination)
**Worktree path:** `.worktrees/esp32picod4/`
- [ ] **Step 1: Run copy script**
```bash
bash .worktrees/esp32picod4/tools/copy_firmware.sh
```
Expected: lists 4 files (bootloader.bin, partitions.bin, firmware.bin, boot_app0.bin)
- [ ] **Step 2: Verify boot_app0.bin is also present**
```bash
ls -la .worktrees/esp32picod4/android/app/src/main/assets/firmware/boot_app0.bin
```
Expected: file exists
- [ ] **Step 3: Commit binary assets**
```bash
git -C .worktrees/esp32picod4 add android/app/src/main/assets/firmware/
git -C .worktrees/esp32picod4 commit -m "feat: add Atom Lite firmware binaries to Android assets"
```
---
### Task 9: Build Android APK
**Files:** N/A (build output `android/app/build/outputs/apk/debug/app-debug.apk`)
**Worktree path:** `.worktrees/esp32picod4/android/`
- [ ] **Step 1: Build debug APK**
```bash
JAVA_HOME=/home/linuxbrew/.linuxbrew/opt/openjdk@21/libexec ./gradlew assembleDebug
```
Expected: `BUILD SUCCESSFUL`
- [ ] **Step 2: Verify APK exists**
```bash
ls -la app/build/outputs/apk/debug/app-debug.apk
```
Expected: file exists with size > 1MB
---
### Task 10: Connect Android phone via ADB wireless
**Files:** N/A (ADB system tool)
- [ ] **Step 1: Load adb-wireless skill**
Use the `adb-wireless` skill to establish a wireless ADB connection to the Android phone.
- [ ] **Step 2: Verify ADB connection**
```bash
adb devices
```
Expected: `<ip>:5555` listed as `device`
---
### Task 11: Install APK on phone
**Files:** N/A (app deployment)
- [ ] **Step 1: Install APK**
```bash
adb install -r .worktrees/esp32picod4/android/app/build/outputs/apk/debug/app-debug.apk
```
Expected: `Success`
- [ ] **Step 2: Launch the app**
```bash
adb shell am start -n com.eddy.atomrelay/.MainActivity
```
---
### Task 12: Flash firmware to Atom Lite via Android app
**Files:** N/A (manual device action)
- [ ] **Step 1: Connect Atom Lite via USB-OTG to the phone**
- [ ] **Step 2: Open the app, tap settings gear → "Firmware Flashen"**
- [ ] **Step 3: If app doesn't auto-detect, unplug/replug the Atom Lite while the dialog is open**
- [ ] **Step 4: Wait for flash to complete (progress bar reaches 100%)**
- [ ] **Step 5: After flash, disconnect USB — the Atom Lite should reboot with green LED blink 3x**
---
### Task 13: Provision HMAC key via app
**Files:** N/A (manual device action)
- [ ] **Step 1: Reconnect Atom Lite via USB-OTG to phone (NOT in download mode — just power on normally)**
- [ ] **Step 2: Open app → Settings → "Sleutel Uitwisselen"**
- [ ] **Step 3: Wait for toast "Beveiligingssleutel gelezen" or provisioning**
- [ ] **Step 4: If key was newly generated, verify Serial monitor shows "HMAC key stored in NVS"**
---
### Task 14: Verify BLE connection and relay control
**Files:** N/A (manual verification)
- [ ] **Step 1: Power on the Atom Lite (with relay and touch sensor connected)**
- [ ] **Step 2: Open the Android app — verify it auto-connects to "SyldaServer"**
- [ ] **Step 3: Verify status shows "Connected"**
- [ ] **Step 4: Hold HOLD button — verify relay activates (click or multimeter)**
- [ ] **Step 5: Release — verify relay deactivates**
- [ ] **Step 6: Touch sensor on G33 — verify relay also activates**
---
## Self-Review
**1. Spec coverage:**
- Sidebranch `esp32picod4`: Task 1 ✓
- Compile for ESP32-PICO-D4: Task 2 ✓
- Pins G32/G33/G27: Task 3 ✓
- SK6812 LED with FastLED: Task 3 ✓
- BLE name "SyldaServer": Task 3 ✓
- CDC-class USB detection: Tasks 5, 6 ✓
- DTR/RTS no changes: Confirmed in research, no task needed ✓
- Bundle binary with Android app: Task 8 ✓
- Build APK: Task 9 ✓
- ADB wireless deploy: Tasks 10, 11 ✓
- Flash + provision + test: Tasks 12-14 ✓
**2. Placeholder scan:** No TBD, TODO, or incomplete sections. All commands are concrete with expected output.
**3. Type consistency:** `hasCdcDataInterface(device: UsbDevice): Boolean` defined in Task 5, used consistently in Task 6 with `UsbUtils.hasCdcDataInterface(device/it)`.

View file

@ -1,776 +0,0 @@
# BLE Refactor: BleRelayManager Extractie
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Extractie van alle BLE-logica uit MainActivity naar een aparte BleRelayManager klasse met callback interface.
**Architecture:** BleRelayManager beheert scan, GATT, UUID verificatie, HMAC relay commands, key/seq in PrefsHelper. MainActivity wordt puur UI + lifecycle en reageert op BleCallbacks.onStatusChanged(). Touch listeners sturen commands naar de manager.
**Tech Stack:** Kotlin, Android BLE API, PrefsHelper (SharedPreferences), javax.crypto.Mac
---
## File Structure
| Bestand | Actie | Verantwoordelijk |
|---|---|---|
| `BleStatus.kt` | Create | `BleStatus` enum + `BleCallbacks` interface |
| `BleRelayManager.kt` | Create | Alle BLE-logica: scan, GATT, UUID, HMAC, keys, seq, auto-retry |
| `MainActivity.kt` | Modify | UI + lifecycle, delegeert naar BleRelayManager |
---
### Task 1: BleStatus enum + BleCallbacks interface
**Files:**
- Create: `android/app/src/main/java/com/eddy/atomrelay/BleStatus.kt`
- [ ] **Step 1: Schrijf BleStatus.kt**
```kotlin
// SPDX-License-Identifier: GPL-3.0-or-later
package com.eddy.atomrelay
enum class BleStatus {
Scanning,
Connecting,
Connected,
Disconnected,
NoKey,
WrongDevice,
ServiceNotFound,
ServiceError,
CommError,
DeviceNotFound,
BluetoothDisabled,
PermissionsDenied
}
interface BleCallbacks {
fun onStatusChanged(status: BleStatus)
}
```
- [ ] **Step 2: Build verificatie**
Run: `./gradlew assembleDebug`
Workdir: `android/`
Expected: BUILD SUCCESSFUL
- [ ] **Step 3: Commit**
```bash
git add android/app/src/main/java/com/eddy/atomrelay/BleStatus.kt
git commit -m "feat: add BleStatus enum and BleCallbacks interface"
```
---
### Task 2: BleRelayManager — class skeleton + companion
**Files:**
- Create: `android/app/src/main/java/com/eddy/atomrelay/BleRelayManager.kt`
- [ ] **Step 1: Schrijf class skeleton**
```kotlin
// SPDX-License-Identifier: GPL-3.0-or-later
package com.eddy.atomrelay
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import android.content.Context
import android.os.Handler
import android.os.Looper
import java.util.UUID
class BleRelayManager(
private val context: Context,
private val callbacks: BleCallbacks
) {
companion object {
private val SERVICE_UUID: UUID = UUID.fromString("4fafc201-1fb5-459e-8fcc-c5c9c331914b")
private val CHARACTERISTIC_UUID: UUID = UUID.fromString("beb5483e-36e1-4688-b7f5-ea07361f26d8")
private val UUID_CHAR_UUID: UUID = UUID.fromString("beb5483e-36e1-4688-b7f5-ea07361f26d9")
private val CCCD_UUID: UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
private const val SCAN_TIMEOUT_MS = 30000L
private const val RECONNECT_DELAY_MS = 2000L
private const val MAX_SCAN_RETRIES = 3
}
private var bluetoothGatt: BluetoothGatt? = null
private var relayCharacteristic: BluetoothGattCharacteristic? = null
private var isScanning = false
private var scanTimedOut = false
private var scanTimeoutRetries = 0
private val handler = Handler(Looper.getMainLooper())
private var seq = 1
private var hmacKey: ByteArray? = null
private var deviceUuid: ByteArray? = null
private var triedAddresses = mutableSetOf<String>()
fun startScan() {}
fun stopScan() {}
fun sendRelayCommand(cmd: Byte) {}
fun disconnect() {}
fun updateKeys() {}
fun resetTriedAddresses() {}
fun hasPermissions(): Boolean = false
fun getRequiredPermissions(): Array<String> = emptyArray()
fun destroy() {}
}
```
- [ ] **Step 2: Build verificatie**
Run: `./gradlew assembleDebug`
Workdir: `android/`
Expected: BUILD SUCCESSFUL
- [ ] **Step 3: Commit**
```bash
git add android/app/src/main/java/com/eddy/atomrelay/BleRelayManager.kt
git commit -m "feat: add BleRelayManager class skeleton"
```
---
### Task 3: BleRelayManager — private helpers (loadState, key/HMAC)
**Files:**
- Modify: `android/app/src/main/java/com/eddy/atomrelay/BleRelayManager.kt`
- [ ] **Step 1: Voeg imports en private helpers toe**
Nieuwe imports:
```kotlin
import java.nio.ByteBuffer
import java.nio.ByteOrder
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
```
Verwijder `fun startScan() {}` etc stubs.
Nieuwe companion constants:
```kotlin
private const val HMAC_PKT_SIZE = 37
private const val HMAC_LEN = 32
```
Nieuwe methodes:
```kotlin
fun updateKeys() {
loadState()
}
fun resetTriedAddresses() {
triedAddresses.clear()
}
fun destroy() {
handler.removeCallbacksAndMessages(null)
}
private fun loadState() {
val prefs = PrefsHelper.getPrefs(context)
val hexKey = prefs.getString(PrefsHelper.PREF_KEY, null)
hmacKey = hexKey?.let { hexStringToBytes(it) }
val hexUuid = prefs.getString(PrefsHelper.PREF_UUID, null)
deviceUuid = hexUuid?.let { hexStringToBytes(it) }
seq = prefs.getInt(PrefsHelper.PREF_SEQ, 1).coerceAtMost(PrefsHelper.SEQ_MAX)
}
private fun hexStringToBytes(hex: String): ByteArray? {
val cleaned = hex.uppercase().filter { it in "0123456789ABCDEF" }
if (cleaned.isEmpty() || cleaned.length % 2 != 0) return null
return cleaned.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
}
private fun hmacSha256(data: ByteArray, key: ByteArray): ByteArray {
val mac = Mac.getInstance("HmacSHA256")
mac.init(SecretKeySpec(key, "HmacSHA256"))
return mac.doFinal(data)
}
private fun buildPacket(cmd: Byte, key: ByteArray): ByteArray {
val seqBytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(seq).array()
val hmacData = ByteArray(5)
hmacData[0] = cmd
System.arraycopy(seqBytes, 0, hmacData, 1, 4)
val hmac = hmacSha256(hmacData, key)
val packet = ByteArray(HMAC_PKT_SIZE)
packet[0] = cmd
System.arraycopy(seqBytes, 0, packet, 1, 4)
System.arraycopy(hmac, 0, packet, 5, HMAC_LEN)
return packet
}
```
- [ ] **Step 2: Build verificatie**
Run: `./gradlew assembleDebug`
Workdir: `android/`
Expected: BUILD SUCCESSFUL
- [ ] **Step 3: Commit**
```bash
git add android/app/src/main/java/com/eddy/atomrelay/BleRelayManager.kt
git commit -m "feat: add BleRelayManager key/HMAC helpers and loadState"
```
---
### Task 4: BleRelayManager — scan callback + startScan/stopScan
**Files:**
- Modify: `android/app/src/main/java/com/eddy/atomrelay/BleRelayManager.kt`
- [ ] **Step 1: Voeg scan logic toe**
Nieuwe imports:
```kotlin
import android.Manifest
import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.content.pm.PackageManager
import android.os.ParcelUuid
import androidx.core.app.ActivityCompat
```
Nieuwe velden + scanCallback:
```kotlin
private val scanRunnable = Runnable { startScan() }
private var scanTimeoutRunnable: Runnable? = null
private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
val device = result.device ?: return
val address = device.address ?: return
if (triedAddresses.contains(address)) return
stopScan()
connectToDevice(device)
}
}
```
Methodes startScan, stopScan, hasPermissions, getRequiredPermissions, connectToDevice:
```kotlin
@SuppressLint("MissingPermission")
fun startScan() {
if (isScanning) return
if (!hasPermissions()) {
callbacks.onStatusChanged(BleStatus.PermissionsDenied)
return
}
val adapter = BluetoothAdapter.getDefaultAdapter()
if (adapter == null || !adapter.isEnabled) {
callbacks.onStatusChanged(BleStatus.BluetoothDisabled)
return
}
loadState()
if (hmacKey == null || deviceUuid == null) {
callbacks.onStatusChanged(BleStatus.NoKey)
return
}
isScanning = true
scanTimedOut = false
scanTimeoutRetries = 0
callbacks.onStatusChanged(BleStatus.Scanning)
val filter = ScanFilter.Builder().setServiceUuid(ParcelUuid(SERVICE_UUID)).build()
val settings = ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build()
adapter.bluetoothLeScanner.startScan(listOf(filter), settings, scanCallback)
scanTimeoutRunnable?.let { handler.removeCallbacks(it) }
scanTimeoutRunnable = Runnable {
if (isScanning) {
scanTimedOut = true
stopScan()
}
}
handler.postDelayed(scanTimeoutRunnable!!, SCAN_TIMEOUT_MS)
}
@SuppressLint("MissingPermission")
fun stopScan() {
if (!isScanning) return
isScanning = false
scanTimeoutRunnable?.let { handler.removeCallbacks(it) }
val adapter = BluetoothAdapter.getDefaultAdapter()
adapter?.bluetoothLeScanner?.stopScan(scanCallback)
if (scanTimedOut) {
if (scanTimeoutRetries < MAX_SCAN_RETRIES) {
scanTimeoutRetries++
handler.postDelayed(scanRunnable, 2000L)
return
}
callbacks.onStatusChanged(BleStatus.DeviceNotFound)
}
}
fun hasPermissions(): Boolean {
return getRequiredPermissions().all {
ActivityCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
}
}
fun getRequiredPermissions(): Array<String> {
return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.ACCESS_FINE_LOCATION
)
} else {
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
}
}
@SuppressLint("MissingPermission")
private fun connectToDevice(device: android.bluetooth.BluetoothDevice) {
callbacks.onStatusChanged(BleStatus.Connecting)
bluetoothGatt = device.connectGatt(context, false, gattCallback)
}
```
- [ ] **Step 2: Build verificatie**
Run: `./gradlew assembleDebug`
Workdir: `android/`
Expected: BUILD SUCCESSFUL
- [ ] **Step 3: Commit**
```bash
git add android/app/src/main/java/com/eddy/atomrelay/BleRelayManager.kt
git commit -m "feat: add BleRelayManager scan logic with permissions"
```
---
### Task 5: BleRelayManager — GATT callback (connect, services, UUID verify, notifications)
**Files:**
- Modify: `android/app/src/main/java/com/eddy/atomrelay/BleRelayManager.kt`
- [ ] **Step 1: Voeg GATT callback toe**
Nieuwe imports:
```kotlin
import android.bluetooth.BluetoothGattDescriptor
import android.bluetooth.BluetoothProfile
import android.util.Log
```
Voeg lege gattCallback stub toe (die was er nog niet) of vervang bestaande lege:
```kotlin
private val gattCallback = object : android.bluetooth.BluetoothGattCallback() {
@SuppressLint("MissingPermission")
override fun onConnectionStateChange(gatt: android.bluetooth.BluetoothGatt, status: Int, newState: Int) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
if (hmacKey == null) {
Log.w("BleRelayManager", "GATT connected but no key -- disconnecting")
callbacks.onStatusChanged(BleStatus.NoKey)
gatt.disconnect()
return
}
Log.d("BleRelayManager", "GATT connected")
bluetoothGatt = gatt
gatt.discoverServices()
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
Log.d("BleRelayManager", "GATT disconnected")
callbacks.onStatusChanged(BleStatus.Disconnected)
gatt.close()
bluetoothGatt = null
relayCharacteristic = null
seq = 1
handler.postDelayed(scanRunnable, RECONNECT_DELAY_MS)
}
}
@SuppressLint("MissingPermission")
override fun onServicesDiscovered(gatt: android.bluetooth.BluetoothGatt, status: Int) {
if (status != BluetoothGatt.GATT_SUCCESS) {
Log.w("BleRelayManager", "Service discovery failed (status=$status)")
callbacks.onStatusChanged(BleStatus.ServiceError)
return
}
Log.d("BleRelayManager", "Services discovered, requesting MTU...")
gatt.requestMtu(256)
}
@SuppressLint("MissingPermission")
override fun onMtuChanged(gatt: android.bluetooth.BluetoothGatt, mtu: Int, status: Int) {
Log.d("BleRelayManager", "MTU changed to $mtu")
val service = gatt.getService(SERVICE_UUID)
if (service != null) {
relayCharacteristic = service.getCharacteristic(CHARACTERISTIC_UUID)
val uuidChar = service.getCharacteristic(UUID_CHAR_UUID)
if (relayCharacteristic != null && uuidChar != null) {
Log.d("BleRelayManager", "Reading device UUID characteristic...")
gatt.readCharacteristic(uuidChar)
} else {
Log.e("BleRelayManager", "Service or characteristics not found")
callbacks.onStatusChanged(BleStatus.ServiceNotFound)
}
} else {
Log.e("BleRelayManager", "Service not found")
callbacks.onStatusChanged(BleStatus.ServiceNotFound)
}
}
@SuppressLint("MissingPermission")
override fun onCharacteristicRead(gatt: android.bluetooth.BluetoothGatt, characteristic: android.bluetooth.BluetoothGattCharacteristic, value: ByteArray, status: Int) {
if (characteristic.uuid == UUID_CHAR_UUID && status == BluetoothGatt.GATT_SUCCESS) {
val storedUuid = deviceUuid
if (storedUuid != null && value.contentEquals(storedUuid)) {
Log.d("BleRelayManager", "UUID verified, setting up relay")
relayCharacteristic?.let { rc ->
gatt.setCharacteristicNotification(rc, true)
val descriptor = rc.getDescriptor(CCCD_UUID)
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(descriptor)
}
callbacks.onStatusChanged(BleStatus.Connected)
} else {
Log.w("BleRelayManager", "UUID mismatch -- disconnecting")
triedAddresses.add(gatt.device.address)
callbacks.onStatusChanged(BleStatus.WrongDevice)
gatt.disconnect()
}
}
}
@SuppressLint("MissingPermission")
override fun onCharacteristicWrite(gatt: android.bluetooth.BluetoothGatt, characteristic: android.bluetooth.BluetoothGattCharacteristic, status: Int) {
if (status != BluetoothGatt.GATT_SUCCESS) {
Log.w("BleRelayManager", "Write failed (status=$status)")
callbacks.onStatusChanged(BleStatus.CommError)
}
}
}
```
- [ ] **Step 2: Build verificatie**
Run: `./gradlew assembleDebug`
Workdir: `android/`
Expected: BUILD SUCCESSFUL
- [ ] **Step 3: Commit**
```bash
git add android/app/src/main/java/com/eddy/atomrelay/BleRelayManager.kt
git commit -m "feat: add BleRelayManager GATT callback (connect, UUID verify, notifications)"
```
---
### Task 6: BleRelayManager — sendRelayCommand + disconnect
**Files:**
- Modify: `android/app/src/main/java/com/eddy/atomrelay/BleRelayManager.kt`
- [ ] **Step 1: Implementeer sendRelayCommand en disconnect**
Vervang de stubs voor `sendRelayCommand` en `disconnect`:
```kotlin
@SuppressLint("MissingPermission")
fun sendRelayCommand(cmd: Byte) {
val char = relayCharacteristic ?: return
val gatt = bluetoothGatt ?: return
val key = hmacKey ?: return
val packet = buildPacket(cmd, key)
char.value = packet
char.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
gatt.writeCharacteristic(char)
seq++
if (seq > PrefsHelper.SEQ_MAX) seq = 1
PrefsHelper.getPrefs(context).edit().putInt(PrefsHelper.PREF_SEQ, seq).apply()
}
@SuppressLint("MissingPermission")
fun disconnect() {
handler.removeCallbacks(scanRunnable)
scanTimeoutRunnable?.let { handler.removeCallbacks(it) }
stopScan()
bluetoothGatt?.disconnect()
}
```
- [ ] **Step 2: Build verificatie**
Run: `./gradlew assembleDebug`
Workdir: `android/`
Expected: BUILD SUCCESSFUL
- [ ] **Step 3: Commit**
```bash
git add android/app/src/main/java/com/eddy/atomrelay/BleRelayManager.kt
git commit -m "feat: add BleRelayManager sendRelayCommand and disconnect"
```
---
### Task 7: Refactor MainActivity — gebruik BleRelayManager
**Files:**
- Modify: `android/app/src/main/java/com/eddy/atomrelay/MainActivity.kt`
- [ ] **Step 1: Herschrijf MainActivity.kt**
Vervang de volledige file content:
```kotlin
// SPDX-License-Identifier: GPL-3.0-or-later
package com.eddy.atomrelay
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.MotionEvent
import android.widget.Button
import android.widget.ImageButton
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
class MainActivity : AppCompatActivity(), BleCallbacks {
companion object {
private const val REQUEST_SETTINGS = 1
private const val POST_SETTINGS_RESCAN_MS = 500L
private const val ON_RESUME_SCAN_DELAY_MS = 300L
}
private lateinit var manager: BleRelayManager
private val handler = Handler(Looper.getMainLooper())
private lateinit var statusText: TextView
private lateinit var relay1Button: Button
private lateinit var relay2Button: Button
private var lastTheme: String? = null
override fun onStatusChanged(status: BleStatus) {
when (status) {
BleStatus.Connected -> {
statusText.text = getString(R.string.status_connected)
setupRelayButton()
setupRelay2Button()
relay1Button.isEnabled = true
relay2Button.isEnabled = true
}
BleStatus.Disconnected -> {
statusText.text = getString(R.string.status_connecting)
relay1Button.isEnabled = false
relay2Button.isEnabled = false
}
BleStatus.Scanning -> statusText.text = getString(R.string.status_scanning)
BleStatus.Connecting -> statusText.text = getString(R.string.status_connecting)
BleStatus.NoKey -> statusText.text = getString(R.string.status_no_key)
BleStatus.WrongDevice -> statusText.text = getString(R.string.status_wrong_device)
BleStatus.ServiceNotFound -> statusText.text = getString(R.string.status_service_not_found)
BleStatus.ServiceError -> statusText.text = getString(R.string.status_service_error)
BleStatus.CommError -> statusText.text = getString(R.string.status_comm_error)
BleStatus.BluetoothDisabled -> statusText.text = getString(R.string.status_bluetooth_disabled)
BleStatus.PermissionsDenied -> {
statusText.text = getString(R.string.status_permissions_denied)
relay1Button.isEnabled = false
relay2Button.isEnabled = false
}
BleStatus.DeviceNotFound -> {
relay1Button.setOnTouchListener(null)
relay2Button.setOnTouchListener(null)
relay1Button.setOnClickListener(null)
relay2Button.setOnClickListener(null)
relay1Button.text = getString(R.string.button_retry_scan)
relay2Button.text = getString(R.string.button_hold)
relay2Button.isEnabled = false
relay1Button.isEnabled = true
relay1Button.setOnClickListener {
setupRelayButton()
setupRelay2Button()
manager.startScan()
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
lastTheme = PrefsHelper.getPrefs(this).getString(PrefsHelper.PREF_THEME, "system") ?: "system"
PrefsHelper.applyTheme(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
manager = BleRelayManager(this, this)
statusText = findViewById(R.id.statusText)
relay1Button = findViewById(R.id.relay1Button)
relay2Button = findViewById(R.id.relay2Button)
val settingsButton = findViewById<ImageButton>(R.id.settingsButton)
settingsButton.setOnClickListener {
startActivityForResult(Intent(this, SettingsActivity::class.java), REQUEST_SETTINGS)
}
setupRelayButton()
setupRelay2Button()
}
@Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_SETTINGS && resultCode == SettingsActivity.RESULT_SCAN) {
manager.disconnect()
manager.resetTriedAddresses()
manager.updateKeys()
handler.postDelayed({ manager.startScan() }, POST_SETTINGS_RESCAN_MS)
}
}
override fun onResume() {
super.onResume()
val current = PrefsHelper.getPrefs(this).getString(PrefsHelper.PREF_THEME, "system") ?: "system"
if (current != lastTheme) {
lastTheme = current
recreate()
return
}
if (!manager.hasPermissions()) {
requestPermissions()
return
}
manager.resetTriedAddresses()
handler.postDelayed({ manager.startScan() }, ON_RESUME_SCAN_DELAY_MS)
}
override fun onPause() {
super.onPause()
manager.stopScan()
}
@Deprecated("Deprecated in Java")
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == 1) {
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
manager.startScan()
} else {
onStatusChanged(BleStatus.PermissionsDenied)
}
}
}
private fun setupRelayButton() {
relay1Button.setOnClickListener(null)
relay1Button.text = getString(R.string.button_hold)
relay1Button.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
manager.sendRelayCommand(0x01.toByte())
relay1Button.text = getString(R.string.button_on)
true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
manager.sendRelayCommand(0x00.toByte())
relay1Button.text = getString(R.string.button_hold)
true
}
else -> false
}
}
}
private fun setupRelay2Button() {
relay2Button.setOnClickListener(null)
relay2Button.text = getString(R.string.button_hold)
relay2Button.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
manager.sendRelayCommand(0x02.toByte())
relay2Button.text = getString(R.string.button_on)
true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
manager.sendRelayCommand(0x03.toByte())
relay2Button.text = getString(R.string.button_hold)
true
}
else -> false
}
}
}
private fun requestPermissions() {
val perms = manager.getRequiredPermissions()
ActivityCompat.requestPermissions(this, perms, 1)
}
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacksAndMessages(null)
manager.destroy()
}
}
```
- [ ] **Step 2: Build verificatie**
Run: `./gradlew assembleDebug`
Workdir: `android/`
Expected: BUILD SUCCESSFUL
- [ ] **Step 3: Commit**
```bash
git add android/app/src/main/java/com/eddy/atomrelay/MainActivity.kt
git commit -m "refactor: extract BLE logic from MainActivity to BleRelayManager"
```
---
### Task 8: Functionele verificatie — deploy en test
- [ ] **Step 1: Deploy APK naar telefoon**
Gebruik de adb-wireless skill om te verbinden, dan:
```bash
adb -s 192.168.1.43:5555 install -r android/app/build/outputs/apk/debug/app-debug.apk
```
- [ ] **Step 2: Test BLE scan, connect, UUID verify**
Start de app. Verifieer:
- Status: "Scannen..." -> "Verbinden..." -> "Verbonden"
- Relay 1 en 2 buttons enabled
- HMAC key en UUID correct geladen uit PrefsHelper
- [ ] **Step 3: Test relay knoppen**
- Relay 1 indrukken: relais 1 aan, button tekst "AAN"
- Relay 1 loslaten: relais 1 uit, button tekst "INGEDRUKT HOUDEN"
- Relay 2 indrukken: relais 2 aan, button tekst "AAN"
- Relay 2 loslaten: relais 2 uit, button tekst "INGEDRUKT HOUDEN"
- Snel ON->OFF cyclen: geen vastlopers
- [ ] **Step 4: Test disconnect + auto-reconnect**
- ESP uitzetten, verifieer "Verbinden...", zet ESP aan, verifieer "Verbonden"
- [ ] **Step 5: Test settings key exchange**
- Wis keys in app, open Settings, doe key exchange
- Verifieer dat app opnieuw scant en verbindt
- [ ] **Step 6: Test scan timeout**
- ESP uitzetten tijdens scan, verifieer na ~90s: timeout + retry knop
- Druk "Opnieuw scannen", verifieer scan herstart

View file

@ -1,127 +0,0 @@
# ESP32-PICO-D4 (M5Stack Atom Lite) Port — Design Spec
> **Goal:** Port the Sylvia Touch Relay project from ESP32-S3-Pico (Atom VoiceS3R) to ESP32-PICO-D4 (Atom Lite), update the Android app for CDC-class USB detection, compile, bundle, and deploy via ADB wireless.
## Current State
The project targets the M5Stack Atom VoiceS3R with ESP32-S3-PICO-1-N8R8 (8MB Flash, 8MB PSRAM). The Android app filters USB devices by Espressif vendor ID (`0x303A`) for key exchange and firmware flashing. The `esp32-flash-lib` handles DTR/RTS via the `SerialTransport` interface implemented by `UsbSerialTransport`.
## Target Hardware
M5Stack Atom Lite with ESP32-PICO-D4:
- 4MB Flash (no PSRAM)
- FTDI FT2232C/D USB-to-UART bridge (VID `0x0403`) — appears as `/dev/ttyUSB0`
- SK6812 addressable RGB LED on GPIO27
- Relay on GPIO32 (G32)
- Touch sensor on GPIO33 (G33) — open-drain type, needs pullup
## Design Decisions
### 1. Side Branch & Git Worktree
Create branch `esp32picod4` via git worktree at `.worktrees/esp32picod4` to isolate the port from the main S3 variant.
### 2. Firmware (`platformio.ini` + `src/main.cpp`)
**New PlatformIO environment `m5stack-atom`:**
| Setting | Old (S3) | New (PICO-D4) | Reason |
|---------|----------|---------------|--------|
| `board` | `esp32-s3-devkitc-1` | `m5stack-atom` | PlatformIO board ID for M5Stack Atom Lite |
| `board_build.arduino.memory_type` | `qio_opi` | removed | Not needed for PICO-D4 |
| `-DESP32S3` | present | `-DESP32` | Correct chip family |
| `-DBOARD_HAS_PSRAM` | present | removed | Atom Lite has no PSRAM |
| `-mfix-esp32-psram-cache-issue` | present | removed | Only needed with PSRAM |
| `-DARDUINO_USB_CDC_ON_BOOT=1` | present | removed | No native USB on PICO-D4 |
| `-DARDUINO_USB_MODE=1` | present | removed | No native USB |
| `upload_port` | `/dev/ttyACM0` | `/dev/ttyUSB0` | FTDI serial device |
| `lib_deps` | (empty) | `fastled/FastLED@^3.7` | SK6812 Neopixel library |
**Pin definitions:**
| Constant | Old | New | Notes |
|----------|-----|-----|-------|
| `SENSOR_PIN` | 38 | 33 | G33, open-drain, `INPUT_PULLUP` unchanged |
| `RELAY_PIN` | 1 | 32 | G32 |
| `LED_PIN` | 27 | 27 | SK6812 Neopixel — replaces `digitalWrite` with FastLED API |
**BLE device name:** `"SyldaServer"` (was `"AtomS3R-Relay"`)
**Serial banner:** `"Atom Lite BLE Relay"` (was `"Atom VoiceS3R BLE Relay"`)
**LED code change:** Replace `digitalWrite(LED_PIN, HIGH/LOW)` with FastLED:
- Include `FastLED.h`
- Define `#define NUM_LEDS 1`
- `CRGB leds[NUM_LEDS]`
- `FastLED.addLeds<SK6812, LED_PIN, GRB>(leds, NUM_LEDS)` in setup
- Startup blink: 3x green (`leds[0] = CRGB::Green; FastLED.show()`)
No other firmware logic changes — HMAC key exchange, BLE relay control, touch sensor logic remain identical.
### 3. Android USB Detection
**Problem:** The Atom Lite's FTDI bridge presents VID `0x0403`, not Espressif's `0x303A`. The app currently filters on VID and won't detect the device.
**Solution:** Replace VID (`0x303A`) filtering with CDC-ACM class filtering (interface class `0x0A`).
**Changes in `UsbUtils.kt`:**
- Add `hasCdcDataInterface(device: UsbDevice): Boolean` — iterates interfaces, returns true if any has `interfaceClass == 0x0A`
**Changes in `SettingsActivity.kt`:**
- Remove `ESP_VENDOR_ID` constant
- Replace all `device?.vendorId == ESP_VENDOR_ID` checks with `UsbUtils.hasCdcDataInterface(device)`
This detects all CDC-ACM serial devices including:
- FTDI FT2232C/D (Atom Lite)
- CP210x (some Atom Lite revisions)
- ESP32-S3 native USB (current hardware)
- CH340/CH9102 (other Atom variants)
### 4. DTR/RTS — No Changes Needed
**Research findings:**
The `esp32-flash-lib`'s `EspFlasherProtocol.enterBootLoader()` implements the standard DTR/RTS download-mode sequence:
```
setControlLines(true, false) // DTR=High → EN released, RTS=Low → GPIO0=Low (download mode)
delayMS(100)
setControlLines(false, true) // DTR=Low → reset pulse via capacitor, RTS=High → GPIO0=High
delayMS(100)
setControlLines(true, false) // DTR=High → release reset, RTS=Low → GPIO0=Low → boot in download mode
```
The `UsbSerialTransport.setControlLines()` implementation sends `CDC_SET_CONTROL_LINE_STATE` control transfers with correct DTR/RTS bit flags. This works for FTDI, CP2104, and native USB CDC-ACM.
The `SettingsActivity.setupCdcControl()` sets DTR|RTS both HIGH for normal serial operation (key exchange), which is correct for all bridge types.
**Verdict: No changes to `EspFlasher.kt`, `UsbSerialTransport.kt`, or key exchange logic.**
### 5. Build, Bundle & Deploy
1. `pio run -e m5stack-atom` to compile firmware
2. `tools/copy_firmware.sh` to copy binaries to `android/app/src/main/assets/firmware/` (environment dir is now `m5stack-atom`)
3. `./gradlew assembleDebug` in `android/` to build APK
4. ADB wireless to connect phone (`adb-wireless` skill)
5. `adb install -r app-debug.apk` to deploy
## Files Changed
| File | Change |
|------|--------|
| `platformio.ini` | New environment `m5stack-atom` replacing S3 config |
| `src/main.cpp` | New pin definitions, BLE name, FastLED LED code |
| `android/.../UsbUtils.kt` | Add `hasCdcDataInterface()` |
| `android/.../SettingsActivity.kt` | VID filter → CDC class filter |
| `tools/copy_firmware.sh` | Environment name `m5stack-atoms3r` → `m5stack-atom` |
## Files Not Changed
| File | Reason |
|------|--------|
| `EspFlasher.kt` | DTR/RTS handled by library |
| `UsbSerialTransport.kt` | Correct implementation |
| `MainActivity.kt` | No BLE changes beyond device name (auto-discovered) |
| `PrefsHelper.kt` | No changes |
| `AndroidManifest.xml` | No changes |
| `build.gradle` | No dependency changes |

View file

@ -1,172 +0,0 @@
# BLE Refactor: BleRelayManager extractie
**Datum:** 2026-07-29
**Status:** goedgekeurd
## Doel
Extractie van alle BLE-logica uit `MainActivity.kt` naar een aparte `BleRelayManager` klasse. MainActivity wordt verantwoordelijk voor UI en lifecycle only.
## Scope
- **In:** BLE scan, GATT connectie, UUID verificatie, HMAC relay commands, key/seq management, auto-retry logica
- **Uit:** UI binding (views, touch listeners, string resources), lifecycle (onCreate/onResume/etc), permissions result handling, theme switching
- **Niet aangeraakt:** `PrefsHelper`, `SettingsActivity`, `EspFlasher`, `UsbSerialTransport`, firmware
## Nieuwe bestanden
### `BleStatus.kt` — enum + callback interface
```kotlin
enum class BleStatus {
Scanning,
Connecting,
Connected,
Disconnected,
NoKey,
WrongDevice,
ServiceNotFound,
ServiceError,
CommError,
DeviceNotFound,
BluetoothDisabled,
PermissionsDenied
}
interface BleCallbacks {
fun onStatusChanged(status: BleStatus)
}
```
### `BleRelayManager.kt` — publieke API
```kotlin
class BleRelayManager(
private val context: Context,
private val callbacks: BleCallbacks
) {
fun startScan()
fun stopScan()
fun sendRelayCommand(cmd: Byte)
fun disconnect()
fun updateKeys() // herlaadt keys na settings key exchange
fun resetTriedAddresses() // wist triedAddresses (bij onResume)
fun hasPermissions(): Boolean
fun getRequiredPermissions(): Array<String>
fun destroy() // cleanup handler, stop scan, close gatt
}
```
### Interne componenten (private)
1. **State:** `bluetoothGatt`, `relayCharacteristic`, `hmacKey`, `deviceUuid`, `seq`, `triedAddresses`, `isScanning`, `scanTimedOut`, `scanTimeoutRetries`
2. **Handler:** `Handler(Looper.getMainLooper())` voor scan scheduling en timeouts
3. **ScanCallback:** scan op `SERVICE_UUID`, skip `triedAddresses`, bij match → `stopScan()` + `connectToDevice()`
4. **GattCallback:** connectie → check key → discoverServices → requestMtu → read UUID char → verify → enable notifications. Bij disconnect: auto-retry na delay.
5. **HMAC:** `hexStringToBytes()`, `hmacSha256()`, `buildPacket()` — ongewijzigd verplaatst
6. **Key loading:** `loadState()` leest via `PrefsHelper.getPrefs(context)`
**Companion constants** (ongewijzigd): `SERVICE_UUID`, `CHARACTERISTIC_UUID`, `UUID_CHAR_UUID`, `CCCD_UUID`, `DEVICE_NAME`, `HMAC_PKT_SIZE`, `HMAC_LEN`, `SCAN_TIMEOUT_MS`, `RECONNECT_DELAY_MS`, `MAX_SCAN_RETRIES`
```mermaid
sequenceDiagram
participant MA as MainActivity
participant BRM as BleRelayManager
participant ESP as ESP32-S3
MA->>BRM: startScan()
BRM->>BRM: loadState() via PrefsHelper
BRM-->>MA: onStatusChanged(Scanning)
BRM->>ESP: BLE scan (filter SERVICE_UUID)
ESP-->>BRM: scan result
BRM->>BRM: skip triedAddresses?
BRM->>ESP: connectGatt()
BRM-->>MA: onStatusChanged(Connecting)
ESP-->>BRM: STATE_CONNECTED
BRM->>BRM: check hmacKey != null
BRM->>ESP: discoverServices → requestMtu(256)
BRM->>ESP: readCharacteristic(UUID_CHAR)
ESP-->>BRM: UUID value
BRM->>BRM: verify against stored deviceUuid
BRM->>ESP: setCharacteristicNotification + writeDescriptor(CCCD)
BRM-->>MA: onStatusChanged(Connected)
MA->>BRM: sendRelayCommand(0x01)
BRM->>BRM: buildPacket(cmd, hmacKey, seq)
BRM->>ESP: writeCharacteristic (NO_RESPONSE)
BRM->>Prefs: save seq
ESP-->>BRM: STATE_DISCONNECTED
BRM-->>MA: onStatusChanged(Disconnected)
BRM->>BRM: handler.postDelayed(startScan, 2s)
```
## MainActivity — wat blijft
| Component | Blijft in MainActivity |
|---|---|
| `onCreate` | View binding, settings button, `BleRelayManager` instantiatie |
| `onResume` | Theme check, `manager.resetTriedAddresses()`, `manager.startScan()` of toon NoKey |
| `onPause` | `manager.stopScan()` |
| `onDestroy` | `manager.destroy()` |
| `onActivityResult` | Bij RESULT_SCAN: `manager.disconnect()`, `manager.updateKeys()`, `manager.resetTriedAddresses()`, `manager.startScan()` |
| `onRequestPermissionsResult` | Bij granted: `manager.startScan()`; bij denied: UI update |
| `setupRelayButton()` | Touch listener → `manager.sendRelayCommand(...)` |
| `setupRelay2Button()` | Touch listener → `manager.sendRelayCommand(...)` |
| Status mapping | `onStatusChanged` → `statusText.text = getString(R.string.status_*)` + knop enabled/disabled |
### Status → UI mapping
```kotlin
callbacks = object : BleCallbacks {
override fun onStatusChanged(status: BleStatus) {
when (status) {
BleStatus.Connected -> {
statusText.text = getString(R.string.status_connected)
setupRelayButton()
setupRelay2Button()
relay1Button.isEnabled = true
relay2Button.isEnabled = true
}
BleStatus.Disconnected -> {
statusText.text = getString(R.string.status_connecting)
relay1Button.isEnabled = false
relay2Button.isEnabled = false
}
BleStatus.DeviceNotFound -> {
// retry knop op relay1Button (huidig gedrag behouden)
relay1Button.setOnTouchListener(null)
relay2Button.setOnTouchListener(null)
relay1Button.setOnClickListener(null)
relay2Button.setOnClickListener(null)
relay1Button.text = getString(R.string.button_retry_scan)
relay2Button.text = getString(R.string.button_hold)
relay2Button.isEnabled = false
relay1Button.isEnabled = true
relay1Button.setOnClickListener {
setupRelayButton()
setupRelay2Button()
manager.startScan()
}
}
else -> statusText.text = getString(R.string.status_scanning) // fallback
}
}
}
```
## Risico's & mitigatie
| Risico | Mitigatie |
|---|---|
| Race condition bij snelle key-update + scan | `updateKeys()` stopt scan vóór herladen |
| Context leak via Handler | `destroy()` roept `handler.removeCallbacksAndMessages(null)` |
| GattCallback refs naar oude Activity | Manager instantieert per Activity, geen singleton |
| Touch listeners verwijzen naar verouderde manager | `setupRelay*Button()` opnieuw aanroepen bij statuswissel (huidig patroon) |
## Verificatie
1. Build: `./gradlew assembleDebug` moet slagen
2. Lint: `./gradlew lint` geen nieuwe warnings
3. Functioneel: APK deployen via ADB, BLE scan → connect → UUID verify → relay knoppen ON/OFF
4. Randgevallen: app herstarten (keys behouden), settings key exchange → rescan, timeout + retry, disconnect + auto-reconnect, permissie deny/accept

View file

@ -1,68 +0,0 @@
# Device UUID Pairing — Design
**Datum:** 2026-07-29
**Status:** goedgekeurd
## Doel
Voorkomen dat twee ESP-telefoon paren in dezelfde ruimte met elkaars ESP verbinden. Elk ESP-telefoon paar deelt een unieke UUID die tijdens key exchange wordt uitgewisseld.
## Flow
### Verse ESP (geen UUID, geen HMAC key)
1. ESP boot, genereert random UUID (16 bytes via `esp_random()`), slaat op in NVS
2. ESP start BLE advertising als `"SylDa_Server"` (UUID nog onbekend bij app)
3. App opent Settings → Key Exchange
4. App stuurt `KEY?` over USB CDC, ESP antwoordt `NO_KEY`
5. App genereert random 32-byte HMAC key + 16-byte UUID
6. App stuurt `SET_KEY:<64 hex>` → ESP slaat op → `OK`
7. App stuurt `SET_ID:<32 hex>` → ESP slaat op → `OK`
8. App slaat beide op in EncryptedSharedPreferences
9. App keert terug naar main, start scan
### Bestaand paar (beide hebben UUID + HMAC key)
1. App stuurt `KEY?`, ESP antwoordt `DEVICE_ID:<32 hex>\nHMAC_KEY:<64 hex>`
2. App slaat beide op
3. Klaar
### BLE connectie
1. App scant op BLE service UUID `4fafc201-...` (niet op naam)
2. Bij elke gevonden device: connect, discover services
3. Lees UUID characteristic (READ-only)
4. UUID match → zet relay knoppen actief
5. UUID mismatch → disconnect, voeg address toe aan `triedAddresses`, herstart scan
6. Relay commands blijven HMAC-beveiligd zoals voorheen
## Componenten
### Firmware (`src/main.cpp`)
| Item | Details |
|------|---------|
| NVS key `"uuid"` | 16 bytes, namespace `"hmac"` |
| UUID generatie | `esp_random()` per byte, alleen bij eerste boot |
| BLE naam | `"SylDa_Server"` |
| UUID characteristic | Nieuwe READ-only characteristic, los UUID |
| `KEY?` response | `DEVICE_ID:32HEX\nHMAC_KEY:64HEX` of `NO_KEY` |
| `SET_ID:32HEX` | Nieuw commando, slaat UUID in NVS |
### Android — PrefsHelper
- `PREF_UUID = "device_uuid"` (EncryptedSharedPreferences)
### Android — SettingsActivity
- `readKeyFromEsp()`: parse `DEVICE_ID:` lijn
- `KeyResult.Success` krijgt `deviceUuid` veld
- Bij `NO_KEY`: app genereert UUID + stuurt `SET_KEY` en `SET_ID`
### Android — MainActivity
- `loadState()`: laadt UUID
- `startScan()`: `ScanFilter` op service UUID
- `onScanResult`: connect bij elke match (naam genegeerd)
- `gattCallback.onMtuChanged`: lees UUID characteristic, verify
- UUID mismatch: disconnect, `triedAddresses.add(address)`, herstart scan

View file

@ -1,111 +0,0 @@
# User-facing text — Atom Relay
## MainActivity — statusbalk
| Huidige tekst | strings.xml key |
|---|---|
| Connected | `status_connected` |
| Verbinden… | `status_connecting` |
| Servicefout | `status_service_error` |
| Service not found | `status_service_not_found` |
| Communicatiefout | `status_comm_error` |
| Geen HMAC sleutel - open Instellingen | `status_no_key` |
| Permissies geweigerd — open Instellingen | `status_permissions_denied` |
| Bluetooth not enabled | `status_bluetooth_disabled` |
| Scanning… | `status_scanning` |
| Device not found | `status_device_not_found` |
| Verkeerd apparaat — opnieuw scannen… | `status_wrong_device` |
| Verbonden, services zoeken… | `status_discovering_services` |
## MainActivity — knoppen
| Huidige tekst | strings.xml key |
|---|---|
| Instellingen (contentDescription) | `cd_settings` |
| Disconnected (placeholder) | `status_default` |
| HOLD (fallback knoplabel) | `button_hold` |
| Opnieuw scannen (fallback) | `button_retry_scan` |
| User-defined labels 1 & 2 | PrefsHelper (user input) |
## SettingsActivity — UI
| Huidige tekst | strings.xml key |
|---|---|
| Sluit de ESP aan op USB en kies een actie. | `msg_settings_intro` |
| Sleutel Uitwisselen | `button_key_exchange` |
| Zet de ESP in download-modus (houd reset 2s in tot de groene LED aangaat) en tik op Firmware Flashen. | `msg_flash_mode_help` |
| Firmware Flashen | `button_firmware_flash` |
| Knoptekst | `section_labels` |
| Relay 1 | `hint_label_relay1` |
| Relay 2 | `hint_label_relay2` |
| Weergave | `section_theme` |
| Systeem | `theme_system` |
| Licht | `theme_light` |
| Donker | `theme_dark` |
| Thema: %s | `theme_applied` |
| Buildnummer: %s | `build_number` |
## SettingsActivity — dialogs / toasts
| Huidige tekst | strings.xml key |
|---|---|
| Sleutel Uitwisselen | `title_key_exchange` |
| Sluit de ESP aan via USB. | `msg_connect_usb` |
| Sleutel uitlezen… | `msg_reading_key` |
| Beveiligingssleutel gelezen | `key_read_success` |
| Sleutel uitwisselen mislukt - probeer opnieuw | `key_exchange_failed` |
| USB-toegang geweigerd | `usb_access_denied` |
| Annuleren | `button_cancel` |
| Sluiten | `button_close` |
| Firmware Flashen | `title_firmware_flash` |
| Zet de ESP in download-modus (houd reset 2s in tot groene LED) en sluit aan via USB. | `msg_flash_download_mode` |
| Initialiseren… | `msg_initializing` |
| Firmware geflashed!\\n\\nOntkoppel de ESP en sluit opnieuw aan om de sleutel in te stellen. | `msg_flash_complete` |
| Flashen mislukt: %s | `flash_failed` |
| Sleutels komen niet overeen | `title_key_conflict` |
| De ESP (SylDa_Server) heeft andere sleutels dan deze app. Wat wil je doen? | `msg_key_conflict` |
| ESP sleutels overnemen | `button_keep_esp_keys` |
| App sleutels overzetten | `button_overwrite_esp` |
| ESP sleutels overschrijven? | `title_overwrite_confirm` |
| Dit vervangt de sleutels van de ESP. De ESP kan daarna niet meer met de oorspronkelijke app communiceren. | `msg_overwrite_warning` |
| Ik weet het zeker | `checkbox_confirm_overwrite` |
| Bevestigen | `button_confirm` |
| Sleutels overschrijven | `title_overwriting_keys` |
| Sleutels schrijven naar ESP… | `msg_writing_keys` |
## SettingsActivity — foutmeldingen (hardcoded in .kt)
| Huidige tekst | strings.xml key |
|---|---|
| Geen CDC interface gevonden | `err_no_cdc` |
| Kan USB apparaat niet openen | `err_usb_open` |
| Kan USB interface niet claimen | `err_usb_claim` |
| Geen USB OUT endpoint gevonden | `err_no_out` |
| Geen USB IN endpoint gevonden | `err_no_in` |
| Ongeldig sleutel/UUID formaat | `err_invalid_key` |
| Geen sleutel gevonden na 5 pogingen | `err_key_retries` |
| Fout: %s | `err_generic` |
| SET_KEY schrijven mislukt | `err_set_key_write` |
| SET_ID schrijven mislukt | `err_set_id_write` |
| ESP bevestigde sleutel niet | `err_key_not_ack` |
| ESP bevestigde UUID niet | `err_uuid_not_ack` |
## EspFlasher — hardcoded in .kt
| Huidige tekst | strings.xml key |
|---|---|
| Kan USB-apparaat niet openen | `err_usb_open` (reuse) |
| Geen CDC data interface gevonden | `err_no_cdc` (reuse) |
| Kan USB interface niet claimen | `err_usb_claim` (reuse) |
| Geen USB OUT endpoint gevonden | `err_no_out` (reuse) |
| Geen USB IN endpoint gevonden | `err_no_in` (reuse) |
| Verbinden met ESP bootloader... | `flash_connecting` |
| Bootloader | `flash_step_bootloader` |
| Partitie tabel | `flash_step_partitions` |
| Boot app0 | `flash_step_boot_app0` |
| Firmware | `flash_step_firmware` |
| Sync met bootloader... | `flash_sync` |
| Chip detecteren... | `flash_detect` |
| Chip: %s | `flash_chip` |
| Stub laden... | `flash_stub` |
| Firmware geflashed! | `flash_done` |
## AndroidManifest
| Huidige tekst | strings.xml key |
|---|---|
| Atom Relay | `app_name` |
| Instellingen | `settings` |

View file

@ -1,62 +0,0 @@
# ESP32 Flash Library Alternatieven
## Huidige Situatie
We gebruiken `io.github.dkaukov:esp32-flash-lib:1.1.17` (Java, GPL-3.0). Deze GPL-3.0 licentie dwingt copyleft af: de hele app moet GPL-3.0-compatibel zijn. Dat blokkeert een MIT/Apache licentie voor de app.
## Alternatieven
| Library | Taal | Licentie | Sterren | Serial interface | Laatste update |
|---|---|---|---|---|---|
| **loucass003/EspflashKotlin** | Kotlin | **Apache 2.0** | 3 | JSerialComm (pluggable) | jan 2026 |
| dkaukov/esp32-flash-lib | Java | GPL-3.0 | — | JSerialComm | — |
| bdureau/ESPLoader | Java | GPL-3.0 | 22 | JSerialComm | jan 2026 |
### loucass003/EspflashKotlin (Apache 2.0)
- Repo: https://github.com/loucass003/EspflashKotlin
- JitPack: `com.github.loucass003:EspflashKotlin:<tag>`
- 3 stars, 1 fork, 28 commits
- Pluggable serial interface: implementeer `FlasherSerialInterface` zelf
- Ondersteunt `.addBin(bytes, address)` voor meerdere binaries
- Progress listener via `FlashingProgressListener`
- Geen Maven Central publicatie — via JitPack
**Voordelen:**
- Apache 2.0 — app mag onder MIT/Apache/GPL naar keuze
- Kotlin-native (past bij onze codebase)
- Pluggable serial: we kunnen onze eigen USB serial implementatie aanhaken
**Nadelen:**
- Erg klein project (3 stars, lage adoptie)
- Geen Maven Central — JitPack dependency (minder betrouwbaar voor productie)
- Minder getest dan esp32-flash-lib
- Serial interface adapter moet worden geschreven voor onze USB/BLE implementatie
- Geen stub loader (esptool stub) — alleen ROM bootloader protocol
### bdureau/ESPLoader (GPL-3.0)
- Repo: https://github.com/bdureau/ESPLoader
- 22 stars
- Partial Java port van esptool
- Zelfde GPL-3.0 probleem als onze huidige lib — **geen licentiewinst**
## Migratie-inschatting (esp32-flash-lib → EspflashKotlin)
1. Dependency vervangen in `build.gradle`
2. `EspFlasher.kt` herschrijven: `FlasherSerialInterface` implementeren voor onze USB serial context
3. Firmware bins en addresses overnemen (zelfde ESP32 ROM bootloader protocol)
4. JitPack repo toevoegen in `settings.gradle` of `build.gradle`
5. Testen op device — het ROM bootloader protocol is hetzelfde, maar implementatie kan subtiele verschillen hebben
**Risico:** EspflashKotlin is minimaal getest. Het mist mogelijk edge cases die esp32-flash-lib wel afdekt (stub loader, error recovery, chip detectie).
## Conclusie
| Optie | Licentie-app | Migratie-effort | Risico |
|---|---|---|---|
| Blijf bij esp32-flash-lib, app wordt GPL-3.0 | GPL-3.0 | Geen | Geen |
| Migreer naar EspflashKotlin | MIT/Apache mogelijk | Medium (2-3 dagen) | Hoog (kleine lib, weinig adoptie) |
| Eigen implementatie (esptool protocol zelf porten) | Vrij te kiezen | Hoog (1-2 weken) | Medium (volledig eigen code) |
**Aanbeveling:** Blijf bij `esp32-flash-lib` en breng de app onder GPL-3.0 uit. Voor een open-source project op GitHub is GPL-3.0 geen praktische beperking — je moet alleen source beschikbaar houden, wat je al doet. De migratie naar EspflashKotlin brengt risico's met zich mee die niet opwegen tegen het licentievoordeel.