spaxel/docs/notes/simulation-testing.md
jedarden 948c966226 init: spaxel project — docs, plan, and marathon infrastructure
- 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>
2026-03-26 06:43:25 -04:00

6.2 KiB
Raw Blame History

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 sequence
  • READ_REG (0x0A) — returns chip registers including chip magic number
  • SPI_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 (38 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:

  1. Publish a firmware version via the API: POST /api/ota/publish
  2. Run a mock node client that calls GET /api/ota/check?version=0.0.1
  3. Verify it receives the update URL
  4. 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