Project_SylDa/docs/superpowers/plans/2026-07-28-esp32-pico-d4-port.md
eddy e5bb09e96e
All checks were successful
CI / firmware (pull_request) Successful in 1m20s
CI / android (pull_request) Successful in 13s
docs: purge remaining S3R references from historical docs and handleiding
2026-08-01 22:57:40 +02:00

13 KiB

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

git worktree add -b esp32picod4 .worktrees/esp32picod4
  • Step 2: Verify branch is active
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
[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
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
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:

#define SENSOR_PIN 38
#define RELAY_PIN  1
#define LED_PIN    46

With:

#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:

    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:

    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):

    Serial.println("=== SylDa BLE Relay ===");

With:

    Serial.println("=== Atom Lite BLE Relay ===");

Replace (line 151):

    BLEDevice::init("SylDa_Server");

With:

    BLEDevice::init("SyldaServer");

Replace (line 174):

    Serial.println("BLE: advertising started as 'SylDa_Server'");

With:

    Serial.println("BLE: advertising started as 'SyldaServer'");
  • Step 4: Verify main.cpp has no remaining S3-specific references
grep -n "S3\|Atom Voice\|46\|G38\|G1\b" .worktrees/esp32picod4/src/main.cpp

Expected: no output (no matches)

  • Step 5: Commit main.cpp
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:

BINARY_DIR="$(cd "$(dirname "$0")/.." && pwd)/.pio/build/s3lite-server"

With:

BINARY_DIR="$(cd "$(dirname "$0")/.." && pwd)/.pio/build/m5stack-atom"
  • Step 2: Commit
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:

    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
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:

        private const val ESP_VENDOR_ID = 0x303a
  • Step 2: Replace VID check in usbAttachedReceiver (line 67)

Replace:

                if (device?.vendorId == ESP_VENDOR_ID) {

With:

                if (device != null && UsbUtils.hasCdcDataInterface(device)) {
  • Step 3: Replace VID check in usbPermissionReceiver (line 85)

Replace:

                    val device = usbManager.deviceList.values.firstOrNull { it.vendorId == ESP_VENDOR_ID }

With:

                    val device = usbManager.deviceList.values.firstOrNull { UsbUtils.hasCdcDataInterface(it) }
  • Step 4: Replace VID check in usbDetachedReceiver (line 101)

Replace:

                if (device?.vendorId == ESP_VENDOR_ID) {

With:

                if (device != null && UsbUtils.hasCdcDataInterface(device)) {
  • Step 5: Replace VID check in startKeyExchange (line 208)

Replace:

        val device = usbManager.deviceList.values.firstOrNull { it.vendorId == ESP_VENDOR_ID }

With:

        val device = usbManager.deviceList.values.firstOrNull { UsbUtils.hasCdcDataInterface(it) }
  • Step 6: Replace VID check in startFlashing (line 256)

Replace:

        val device = usbManager.deviceList.values.firstOrNull { it.vendorId == ESP_VENDOR_ID }

With:

        val device = usbManager.deviceList.values.firstOrNull { UsbUtils.hasCdcDataInterface(it) }
  • Step 7: Verify no remaining ESP_VENDOR_ID references
grep -n "ESP_VENDOR_ID\|vendorId.*==" .worktrees/esp32picod4/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt

Expected: no output

  • Step 8: Commit
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
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
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 .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
ls -la .worktrees/esp32picod4/android/app/src/main/assets/firmware/boot_app0.bin

Expected: file exists

  • Step 3: Commit binary assets
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
JAVA_HOME=/home/linuxbrew/.linuxbrew/opt/openjdk@21/libexec ./gradlew assembleDebug

Expected: BUILD SUCCESSFUL

  • Step 2: Verify APK exists
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
adb devices

Expected: <ip>:5555 listed as device


Task 11: Install APK on phone

Files: N/A (app deployment)

  • Step 1: Install APK
adb install -r .worktrees/esp32picod4/android/app/build/outputs/apk/debug/app-debug.apk

Expected: Success

  • Step 2: Launch the app
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).