No description
Find a file
jedarden 7757149440 fix(bf-39mli): co-locate Alice CSI blob with triangulated point so matcher clears f_distance
The BLE identity matcher never attached Alice's triangulated device to a CSI
blob (matchConf stayed below MinMatchConfidence=0.60). Two compounding root
causes, both verified via [BLEDBG] instrumentation (SPAXEL_BLE_DEBUG):

1. RSSI round-trip inconsistency (cmd/sim/main.go): the sim emitted RSSI under
   a different log-distance path-loss model (-50-20log10(d)) than the matcher
   inverts (internal/ble: RefRSSI=-65, PathLossExp=2.5 -> rssiToDistance at
   identity.go:376). rssiToDistance then systematically underestimated true
   distance (~0.22x), triangulating every device too close to the node cluster
   and off its CSI blob. The BLE key was also "rssi" instead of "rssi_dbm",
   which ingestion maps to, leaving RSSIdBm at 0 and collapsing triangulation.
   Now emit under the matcher's own model constants and the "rssi_dbm" key, so
   rssiToDistance inverts RSSI back to the true node->walker distance.

2. Axis convention (identity.go): horizontal blob distance was computed over
   (X,Z), but Z is the height axis -- triangulated Z is pinned to the anchor
   mounting plane while the CSI blob centroid sits at person height (~1m), so
   (X,Z) misread that vertical gap as horizontal distance and suppressed the
   distance gate. Use the (X,Y) floor plane, matching createBLEOnlyTracks and
   the sim/fusion runtime convention (X,Y floor; Z up).

The [BLEDBG] instrumentation (bleDebug/SPAXEL_BLE_DEBUG gate + log lines) is
retained as live matcher diagnostics.

Verified (HEAD, uncommitted, independently re-run):
- SPAXEL_BLE_DEBUG=1 scripts/run-sim-ble-match.sh -> PASS:
  blob_id=7 conf=0.7139527093753523, [BLEDBG] assign hDist=0.61m matchConf=0.714
  (gate 0.60), reject_in_sim_log=0, peak_blobs=2.
- scripts/run-sim-local.sh -> PASS: peak_blob_count=2, reject_in_sim_log=0,
  sim --verify PASSED, 4 nodes online, fps 363 (no base-blob-path regression).
- go vet ./mothership/... + go build ./mothership/... + cmd/sim -> clean.

Co-Authored-By: Claude <noreply@anthropic.com>
Bead-Id: bf-39mli
2026-07-08 13:59:41 -04:00
.beads docs(bf-f0jee): checkpoint close — IO-6 tracking verified, bead closed on redispatch 2026-07-07 12:48:27 -04:00
.marathon chore(marathon): GLM-4.7 launcher; origin->Forgejo (mirrors to GitHub) 2026-05-24 09:37:25 -04:00
cmd/sim feat(sim): provision real per-node HMAC token by default (bf-4mle6) 2026-07-07 15:16:30 -04:00
dashboard fix(bf-56uk): make blob-identity TypeScript verification compile-clean 2026-07-06 19:32:31 -04:00
docs docs(bf-29wyl): confirm token-supply path dead — header-only, unread 2026-07-07 06:55:56 -04:00
firmware test(runner): document C11 7.13.2.1 setjmp clobber audit at call site (bf-31rd) 2026-07-04 04:29:59 -04:00
mothership fix(bf-39mli): co-locate Alice CSI blob with triangulated point so matcher clears f_distance 2026-07-08 13:59:41 -04:00
notes docs(bf-14wx5): document BLE identity fixture + verifiable run-sim-ble-fixture.sh 2026-07-08 13:59:41 -04:00
scripts docs(bf-14wx5): document BLE identity fixture + verifiable run-sim-ble-fixture.sh 2026-07-08 13:59:41 -04:00
test/acceptance test(acceptance): add IO-1 and IO-2 acceptance tests 2026-05-24 12:45:41 -04:00
tests/e2e feat: complete anomaly detection & security mode dashboard UI 2026-04-07 15:50:25 -04:00
.dockerignore feat(deploy): Docker packaging with multi-stage build and compose orchestration 2026-03-26 07:46:15 -04:00
.gitignore chore(firmware): gitignore abandoned idf.py --target linux test_apps residue 2026-07-03 13:28:21 -04:00
.golangci.yml fix(lint): resolve golangci-lint v2 failures blocking CI 2026-06-03 23:34:50 -04:00
.needle-predispatch-sha docs(bf-34lwt): close — root-cause re-verified at HEAD 402bb7b 2026-07-07 12:28:10 -04:00
.needle.yaml feat: trigger CI build and verify deployment 2026-04-06 09:54:08 -04:00
API_IMPLEMENTATION_STATUS.md docs: verify REST API implementation completeness 2026-04-09 18:18:42 -04:00
blob_observation.sh test(bf-51fq): add manual /api/blobs observation harness for runtime blob tracking 2026-07-08 00:58:20 -04:00
docker-compose.yml feat(deploy): Docker packaging with multi-stage build and compose orchestration 2026-03-26 07:46:15 -04:00
Dockerfile test: add firmware host tests for nvs/csi/serial_prov + wire gcc harness into CI 2026-07-03 13:21:26 -04:00
fix_ble_handlers.py feat: implement spatial quick actions with follow camera 2026-04-09 22:54:36 -04:00
go.work simulator: build and update spaxel-sim CLI tool 2026-05-06 00:26:51 -04:00
PROGRESS.md docs(bf-3hji): re-verify spaxel-sim streams CSI — no reject, frames/s=240, 4 nodes online 2026-07-07 16:01:15 -04:00
README.md docs(ci): add accessibility quality gate documentation 2026-07-06 00:32:03 -04:00
VERSION fix(provision): re-broadcast SPAXEL READY every 1s during window 2026-06-05 18:23:25 -04:00

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.51.0 m with 4+ nodes
  • Motion / trajectory tracking — follows moving people
  • Rough person count — distinguishes 1 vs. 2+ (degrades at 3+)
  • Rough Z-axis — ±12 m with mixed-height node placement (enables fall detection)
  • Stationary-person detection — via breathing micro-motion (0.10.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: host is required for mDNS multicast to reach ESP32 nodes on your LAN. If host networking isn't available, set SPAXEL_MDNS_ENABLED=false and provision nodes with a manual mothership IP (see docs/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 (CI quality gate)
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


Spaxel is self-hosted, CSI-only, and cloud-free by design.