spaxel/mothership/cmd/sim
jedarden d048987d61
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
feat(ingestion): persist hello-announced node position in fleet registry (bf-24xp)
spaxel-sim computed good corner/perimeter geometry in createVirtualNodes but
the hello message sent on WebSocket connect did not include position, so the
mothership never learned the sim node's location and the fleet registry/DB row
was left at the nodes-table schema default (0,0,1) — co-located with every
other node.

Both sim CLIs (cmd/sim and mothership/cmd/sim) now announce pos_x/pos_y/pos_z
in the hello handshake, sourced from createVirtualNodes. HelloMessage gains
PosX/PosY/PosZ as *float64 (not plain float64) so an absent position is
distinguishable from a genuine (0,0,0): a real ESP32 omits these in hello
(its position is user-placed in the dashboard), whereas spaxel-sim announces
its computed geometry.

The connect/register path (server.HandleNodeWS -> FleetNotifier.OnNodeConnected
-> fleet.Manager) now carries the announced position through. The Manager
persists it via registry.SetNodePosition only when all three axes are present,
so a nil/partial announcement preserves any existing user-placed position
rather than clobbering it. The position then flows onward to the fusion engine
through the bf-3p6g connect/register sink (ForwardNodePosition ->
nodePositionSink -> fusionEngine.SetNodePosition), so a freshly connected sim
node is seeded at its announced location instead of (0,0,1).

The FleetNotifier interface change (OnNodeConnected gains three *float64
params) is propagated to every implementer — Manager (persists), FleetHealer
and SelfHealManager (read geometry from the registry, ignore), the guided-
troubleshoot notifier (tracks connect/disconnect only), and the cmd/mothership
multiFleetNotifier fan-out.

Tests: existing OnNodeConnected call sites updated to the new signature.
TestManagerOnNodeConnectedPersistsHelloPosition asserts a node connecting with
an announced position yields a registry row whose pos differs from (0,0,1) and
matches the sent coordinates, and that the same coordinates reach the fusion
engine. TestManagerOnNodeConnectedWithoutHelloPositionPreservesExisting
asserts the real-ESP32 case (nil announce) and partial-announce both leave an
existing user-placed position untouched.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 18:42:34 -04:00
..
generator.go style(mothership): run go fmt to format all Go code 2026-05-24 15:30:02 -04:00
main.go feat(ingestion): persist hello-announced node position in fleet registry (bf-24xp) 2026-07-03 18:42:34 -04:00
main.go.bak test: implement acceptance scenario integration tests (AS-1 through AS-6) 2026-05-05 05:45:15 -04:00
main_test.go style(mothership): run go fmt to format all Go code 2026-05-24 15:30:02 -04:00
Makefile feat: implement CSI simulator CLI with GDOP overlay 2026-04-09 12:31:13 -04:00
README.md feat: implement CSI simulator CLI with GDOP overlay 2026-04-09 12:31:13 -04:00
scenario.go style(mothership): run go fmt to format all Go code 2026-05-24 15:30:02 -04:00
verify.go style(mothership): run go fmt to format all Go code 2026-05-24 15:30:02 -04:00
walker.go fix(sim): correct default WS endpoint and remove dead walker code 2026-04-24 21:42:14 -04:00

Spaxel CSI Simulator

A Go CLI tool for testing the Spaxel mothership without ESP32 hardware. The simulator opens WebSocket connections as virtual nodes and sends synthetic CSI binary frames.

Building

cd mothership
go build -o spaxel-sim ./cmd/sim

Usage

spaxel-sim [flags]

Flags

Flag Type Default Description
--mothership string ws://localhost:8080/ws/node Mothership WebSocket URL
--token string "" Node authentication token (X-Spaxel-Token header)
--nodes int 4 Number of virtual nodes to simulate
--walkers int 1 Number of walking persons to simulate
--rate int 20 CSI packet rate in Hz per node
--duration duration 30s Simulation duration (0 = run forever)
--ble bool false Also send simulated BLE advertisements
--seed int64 42 Random seed for reproducible runs
--width float64 6.0 Space width in meters
--depth float64 5.0 Space depth in meters
--height float64 2.5 Space height in meters
--space string "" Space dimensions as "WxDxH" (overrides --width/--depth/--height)
--show-frame-rate bool true Show per-second frame counts to stdout
--verbose bool false Enable verbose logging

Examples

Basic simulation (4 nodes, 1 walker, 30 seconds):

spaxel-sim --mothership ws://localhost:8080/ws/node --nodes 4 --walkers 1 --duration 30s

With authentication:

spaxel-sim --mothership ws://localhost:8080/ws/node --token <your-token> --nodes 4

Custom space dimensions:

spaxel-sim --space "10x8x3.0" --nodes 6 --walkers 2

With BLE advertisements:

spaxel-sim --ble --walkers 2

Reproducible run (fixed seed):

spaxel-sim --seed 12345 --duration 60s

Run indefinitely (for manual testing):

spaxel-sim --duration 0

CSI Frame Format

The simulator generates valid CSI binary frames matching the Spaxel specification:

Header (24 bytes):
  node_mac:     6 bytes  — source node MAC
  peer_mac:     6 bytes  — transmitting peer MAC
  timestamp_us: 8 bytes  — uint64, microseconds since node boot
  rssi:         1 byte   — int8, dBm
  noise_floor:  1 byte   — int8, dBm
  channel:      1 byte   — uint8, WiFi channel
  n_sub:        1 byte   — uint8, subcarrier count

Payload (n_sub × 2 bytes):
  Per subcarrier: int8 I, int8 Q

Simulation Model

Virtual Nodes

  • Positioned at corners and edges of the defined space
  • Mixed heights (high at 80% of room height, low at 30%)
  • MAC addresses: AA:BB:CC:DD:E0:00 through AA:BB:CC:DD:E0:0N

Walkers

  • Start at center of room (average person height: 1.7m)
  • Random walk with Gaussian velocity updates
  • Bounce off walls
  • BLE address: 11:22:33:44:55:00 through 11:22:33:44:55:0N

Signal Model

  • Path loss: Free space path loss model (PL(d) = PL₀ + 10·n·log₁₀(d/d₀))
    • PL₀ = 40 dB at d₀ = 1m
    • n = 2.0 (free space)
  • Fresnel modulation: Amplitude increases when walker is in Fresnel zones
    • Zone 1: maximum modulation
    • Zone 5+: no modulation
  • Noise: Gaussian noise added to I/Q values

Integration Testing

The simulator is designed for integration testing:

# Start mothership
docker run -d -p 8080:8080 --name spaxel-test ghcr.io/spaxel/spaxel:latest

# Run simulator for 30 seconds
spaxel-sim --mothership ws://localhost:8080/ws/node --nodes 4 --walkers 1 --duration 30s

# Check blob count (should be > 0 if detection is working)
curl -s http://localhost:8080/api/blobs | jq '. | length > 0'

Output

The simulator logs:

  • Connection status
  • Per-second frame rates (if --show-frame-rate)
  • Final statistics on completion
  • Errors (authentication failures, WebSocket errors, etc.)

Example output:

[INFO] CSI Simulator starting
[INFO] Configuration: nodes=4, walkers=1, rate=20 Hz, duration=30s
[INFO] Space: 6.0x5.0x2.5 m
[INFO] Connecting to: ws://localhost:8080/ws/node
[INFO] Node AA:BB:CC:DD:E0:00 connected to mothership
[STATS] Node AA:BB:CC:DD:E0:00: 20 frames/s
[STATS] Node AA:BB:CC:DD:E0:00: 20 frames/s
[INFO] Simulation completed successfully
[STATS] Node AA:BB:CC:DD:E0:00: sent 600 frames

Authentication

When the mothership requires authentication (SPAXEL_INSTALL_SECRET is set), you must provide a valid node token. Generate a token for a simulated node using the mothership's provisioning API or derive it manually:

# Get token from mothership provisioning endpoint
curl -s http://localhost:8080/api/provision -d '{"mac":"AA:BB:CC:DD:E0:00"}'

Error Handling

The simulator exits with non-zero status on:

  • WebSocket connection failure
  • Authentication rejection (HTTP 401)
  • Mothership rejection ({type:"reject"} message)

Testing

Run the test suite:

cd mothership
go test ./cmd/sim/...

Tests cover:

  • Space dimension parsing
  • MAC address conversion
  • CSI frame structure validation
  • Fresnel zone modulation
  • RSSI calculation
  • Walker position updates