- WiFi CSI-based indoor positioning system for self-hosted home environments - docs/plan/plan.md: full 9-phase implementation plan (65 gaps closed by analysis) - docs/research/: CSI fundamentals, physics, algorithms, signal processing, mesh topology, accuracy limits, literature - docs/notes/: recovery mechanisms, simulation testing, UX visualization - .marathon/instruction.md: per-iteration marathon instructions with detailed commit format - .marathon/start.sh: GLM-5 tmux launcher via ZAI proxy Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6.2 KiB
Simulated ESP32 Installation Testing
The goal is to be able to develop and test the full installation flow — web installer, provisioning, OTA — without requiring a physical ESP32-S3 connected via USB.
1. Simulating the Web Installer (ESP Web Tools)
ESP Web Tools uses the browser's Web Serial API, which only activates on actual serial port connections. There is no official simulation mode. Three approaches:
Option A: Virtual Serial Port (Linux)
Create a virtual serial port pair using socat:
socat -d -d pty,raw,echo=0 pty,raw,echo=0
# Creates: /dev/pts/3 <---> /dev/pts/4
Then write a Python script that mimics the ESP32 bootloader's ROM command protocol on one end of the pty. The installer's esptool-js library communicates using the same SLIP-framed protocol as esptool.py.
Key bootloader commands to emulate:
SYNC(0x08) — ROM sync, 36-byte sequenceREAD_REG(0x0A) — returns chip registers including chip magic numberSPI_FLASH_MD5— used for verification
The chip magic number determines the detected chip family:
CHIP_MAGIC = {
0x6F51306F: "ESP32-S3", # Returns this to identify as S3
0x00F01D83: "ESP32",
0x000007C6: "ESP32-S2",
0x6921506F: "ESP32-C3",
}
Reference: esptool-js source at github.com/espressif/esptool-js.
Option B: Chrome DevTools Protocol Mock
Use Playwright or Puppeteer to intercept Web Serial API calls and mock responses. More complex but allows end-to-end testing of the web UI without any serial hardware:
// In Playwright test:
await page.exposeFunction('__mockSerial', async (command) => {
if (command === 'sync') return { success: true, chipFamily: 'ESP32-S3' };
if (command === 'flash') return { success: true };
});
This approach tests the UI logic (chip detection display, progress bars, error states) independently of hardware.
Option C: Physical Target on Development Machine
The simplest approach: keep one physical ESP32-S3 permanently connected to the development machine via USB. Use it as the integration test target. Flash, verify detection works, then proceed to automated testing for everything above the hardware layer.
Recommended for Spaxel: Option C for hardware-level validation, Option B for CI pipeline testing of the UI.
2. Simulating the Provisioning Portal
The captive portal (SoftAP + HTTP server on the ESP32) can be tested by running the HTML form served by provisioning.c directly in a browser, pointing form submissions at a local Go server that mocks the NVS save:
# Run a mock provisioning endpoint:
go run ./cmd/mock-prov -port 80
# Open in browser: http://localhost/
# Submit form → check NVS keys were received correctly
The form HTML in provisioning.c is self-contained and renders in any browser. Test:
- Valid SSID + IP → 200 + "Rebooting" message
- Empty SSID → 400 Bad Request
- Very long strings → truncation handling
3. Simulating CSI Data (Node-Level)
For testing the mothership pipeline without physical nodes, write a CSI packet generator that produces valid UDP packets in Spaxel's wire format:
// tools/csi-sim/main.go
package main
import (
"math"
"math/rand"
"net"
"time"
"encoding/binary"
)
func main() {
conn, _ := net.Dial("udp", "localhost:4210")
txMAC := []byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x01}
rxMAC := []byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x02}
for seq := uint32(0); ; seq++ {
pkt := buildCSIPacket(txMAC, rxMAC, seq, simulateHuman(seq))
conn.Write(pkt)
time.Sleep(10 * time.Millisecond) // 100 Hz
}
}
// simulateHuman generates amplitude perturbation pattern for a person
// walking across the Fresnel zone between tx and rx.
func simulateHuman(tick uint32) []float64 {
amp := make([]float64, 52)
t := float64(tick) * 0.01 // seconds
personX := 2.5 + 1.5*math.Sin(0.3*t) // oscillates across room
for k := range amp {
// Static background
amp[k] = 30.0 + float64(k)*0.1 + rand.NormFloat64()*2.0
// Person perturbation: Fresnel zone crossing produces cosine variation
amp[k] += 8.0 * math.Cos(2*math.Pi*personX/0.125 + float64(k)*0.2)
}
return amp
}
Scenarios to simulate:
- Empty room: baseline noise only
- Walking person: sinusoidal Fresnel zone crossing
- Stationary person: static offset above baseline (3–8 dB per affected link)
- Two people: superposition of two independent perturbation patterns
- Breathing: 0.25 Hz amplitude modulation on top of static offset
4. End-to-End CI Pipeline
For automated testing in CI (no hardware available):
# .github/workflows/test.yml
jobs:
test-server:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Start mothership stack
run: docker-compose up -d broker spaxel
- name: Run CSI simulator
run: go run ./tools/csi-sim -duration 10s -nodes 4 -scenario walking
- name: Assert blobs detected
run: |
sleep 5 # let positioning engine accumulate
curl http://localhost/api/blobs | jq '.[] | select(.confidence > 0.5)' | wc -l | grep -v '^0$'
test-firmware-build:
runs-on: ubuntu-latest
container: espressif/idf:v5.3
steps:
- uses: actions/checkout@v4
- name: Build firmware
working-directory: node/
run: idf.py build
- name: Verify binary exists
run: test -f node/build/spaxel-node.bin
5. Simulating OTA
Test the full OTA cycle without flashing real hardware:
- Publish a firmware version via the API:
POST /api/ota/publish - Run a mock node client that calls
GET /api/ota/check?version=0.0.1 - Verify it receives the update URL
- Verify the binary is downloadable:
GET /api/ota/firmware/0.0.2/firmware.bin
# Publish test firmware
curl -X POST http://localhost/api/ota/publish \
-F "version=0.0.2" \
-F "firmware=@node/build/spaxel-node.bin"
# Check OTA as a node running 0.0.1
curl "http://localhost/api/ota/check?version=0.0.1"
# Expected: {"version":"0.0.2","url":"/api/ota/firmware/0.0.2/firmware.bin"}
# Check OTA as a node already on latest
curl "http://localhost/api/ota/check?version=0.0.2"
# Expected: 204 No Content