Adds the three firmware host-test modules required by the Testing Strategy as a
plain gcc harness under firmware/test/ — NOT idf.py --target linux. That path was
rejected (docs/notes/firmware-host-test-approach.md, bf-21t): firmware/main
cannot host-link because csi.c pulls in esp_wifi.h and provision.c pulls in
driver/uart.h, and the single `main` component REQUIRES esp_wifi/bt/driver,
which have no linux build — so even nvs_migration.c (hostable in isolation) is
unhostable as part of the component. The harness therefore tests dependency-free
logic extractions and binary-format/wire contracts instead of linking the
firmware source.
- test_nvs_migration.c: fresh-install init to v1, no-downgrade guard, forward
migration loop dispatch (v→v+1 at index v−1), and the concrete v1→v2 step
(rename ms_ip→mothership_ip, default ntp_server), driven against an in-memory
NVS store. Mirrors nvs_migration.c decision-for-decision.
- test_csi_frame.c: 24-byte header field round-trip, explicit little-endian
timestamp byte order, signed-RSSI (uint8_t) reinterpretation, I/Q payload
copy, n_sub=0 header-only probe, and the ingestion-side validation rules
(too-short / payload-mismatch / n_sub>128 / bad channel). Mirrors the
websocket.c encoder contract (offset/byte for offset/byte).
- test_serial_prov.c: provisioning JSON parser + NVS-mapping mirror of
provision.c (all four protocol branches + every field mapping), shipping a
bounded recursive-descent JSON decoder as the fuzz target. The fuzz pass
(4000 random byte streams, a tricky-input corpus, 500 deep-nesting cases)
proves the parser never crashes and the protocol always answers a single
well-formed {"ok":...} line on any UART input.
- Makefile: gcc build/run recipe that globs every test_*.c + test_runner.c.
CI wiring: the Dockerfile firmware-builder stage now runs `make -C test test`
before the expensive ESP-IDF build, so a logic/format-contract regression fails
the image build fast. .gitignore + .dockerignore exclude the regenerable
host_tests binary.
docs/plan/plan.md Testing Strategy updated from the idf.py description to the
gcc harness (matching the decision record).
28 tests, all passing. go test ./... and go vet ./... unchanged (firmware-only).
Co-Authored-By: Claude <noreply@anthropic.com>
|
||
|---|---|---|
| .beads | ||
| .github/workflows | ||
| .marathon | ||
| cmd/sim | ||
| dashboard | ||
| docs | ||
| firmware | ||
| mothership | ||
| test/acceptance | ||
| tests/e2e | ||
| .dockerignore | ||
| .gitignore | ||
| .golangci.yml | ||
| .needle-predispatch-sha | ||
| .needle.yaml | ||
| API_IMPLEMENTATION_STATUS.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| fix_ble_handlers.py | ||
| go.work | ||
| PROGRESS.md | ||
| README.md | ||
| VERSION | ||
Spaxel
WiFi CSI-based indoor positioning for self-hosted homes.
Spaxel detects and localizes people in a home using WiFi Channel State Information — no cameras, no microphones, no cloud. A single Docker container (the "mothership") runs on a home server and manages a fleet of ESP32-S3 nodes that capture and stream CSI over WiFi. The mothership fuses CSI from all links to detect presence, follow motion, and — with enough nodes — estimate 2D/3D position, rendered on a Three.js floor-plan dashboard.
Everything runs locally on hardware you own. There is no cloud relay, no account, and no RF beyond the 2.4 GHz WiFi already in your home.
What it can realistically do
Based on physics and the research in docs/research/ (see docs/research/06-accuracy-and-limits.md):
- Presence detection — reliably, with 2+ nodes on opposite sides of a space
- Approximate 2D position — ±0.5–1.0 m with 4+ nodes
- Motion / trajectory tracking — follows moving people
- Rough person count — distinguishes 1 vs. 2+ (degrades at 3+)
- Rough Z-axis — ±1–2 m with mixed-height node placement (enables fall detection)
- Stationary-person detection — via breathing micro-motion (0.1–0.5 Hz)
Not achievable with 2.4 GHz CSI: sub-10 cm accuracy, skeletal pose, reliable 5+ person tracking.
Privacy by design
Spaxel is CSI-only — it never captures camera images or audio. Detection is local: data stays on the mothership, there is no cloud relay or remote access, and no user accounts are required (a single PIN protects the dashboard). See the Non-Goals section of docs/plan/plan.md.
Repository layout
Spaxel is a Go workspace of three modules, plus ESP32 firmware and a static frontend:
| Path | Description |
|---|---|
mothership/ |
Go backend — ingestion, signal pipeline, localizer, fleet manager, REST/WebSocket API, dashboard server (github.com/spaxel/mothership) |
cmd/sim/ |
spaxel-sim — CSI/node simulator CLI for hardware-free development and integration tests (github.com/spaxel/sim) |
test/acceptance/ |
Acceptance-test module (AS-1 … AS-7), driven by the simulator |
firmware/ |
ESP-IDF (C) firmware for the ESP32-S3 node fleet |
dashboard/ |
Vanilla JS + Three.js single-page UI (see dashboard/README.md) |
docs/ |
Plan, notes, and research (see Documentation below) |
Dockerfile, docker-compose.yml |
Single-container packaging |
PROGRESS.md, VERSION |
Implementation status and current version |
Quickstart
The mothership ships as a single container, published as ronaldraygun/spaxel. The bundled docker-compose.yml builds from source by default and exposes one port (8080).
git clone https://github.com/jedarden/spaxel.git
cd spaxel
docker compose up -d # builds the mothership image, host networking
# …or skip the build and use the published image:
# docker pull ronaldraygun/spaxel
Then open http://<server-ip>:8080, set a dashboard PIN, and use Add Node (Chrome/Edge Web Serial) to provision an ESP32-S3 over USB. The node discovers the mothership via mDNS and begins streaming CSI — zero manual IP configuration.
network_mode: hostis required for mDNS multicast to reach ESP32 nodes on your LAN. If host networking isn't available, setSPAXEL_MDNS_ENABLED=falseand provision nodes with a manual mothership IP (seedocs/notes/mdns-override.md).
Key environment variables
| Variable | Default | Purpose |
|---|---|---|
SPAXEL_BIND_ADDR |
0.0.0.0:8080 |
Listen address (set to 127.0.0.1:8080 behind a local reverse proxy) |
SPAXEL_DATA_DIR |
/data |
Persistent storage: SQLite, baselines, floor plans, CSI replay buffer, firmware |
SPAXEL_MQTT_BROKER |
(unset) | Optional MQTT broker URL for Home Assistant integration (e.g. mqtt://homeassistant.local:1883) |
TZ |
UTC |
Timezone for diurnal baselines, briefings, and quiet hours (IANA name) |
SPAXEL_MDNS_ENABLED |
true |
Disable when not using host networking |
The full list is in the Deployment section of docs/plan/plan.md.
Building & developing
# Mothership backend
cd mothership && go test ./... && go vet ./...
# CSI / node simulator
go build -o spaxel-sim ./cmd/sim
# Hardware-free acceptance suite (no ESP32 needed)
cd test/acceptance && go test ./...
# Dashboard unit + accessibility tests
cd dashboard && npm test && npm run test:a11y
Firmware is built with ESP-IDF 5.2.x — see the Firmware Build System section of the plan.
Documentation
docs/plan/plan.md— the complete design: architecture, components, schema, deployment, phasesdocs/notes/— implementation notes (recovery mechanisms, mDNS override, simulation testing, UX)docs/research/— CSI fundamentals, physics, algorithms, accuracy limits, prior-art papersdashboard/README.md— dashboard test setup (Jest, axe-core + Playwright)PROGRESS.md— phase-by-phase implementation status
Spaxel is self-hosted, CSI-only, and cloud-free by design.