Commit graph

10 commits

Author SHA1 Message Date
jedarden
0aa6046f82 feat: implement firmware LED blink handler for identify command
- Add led_init() call during firmware initialization in app_main()
- Add led_stop_blink() call on WebSocket disconnect
- LED GPIO configurable via CONFIG_SPAXEL_LED_GPIO (default GPIO8)
- Blink runs in FreeRTOS task at ~5Hz (100ms on/100ms off)
- Any running blink is cancelled when new identify message received

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 07:01:42 -04:00
jedarden
a42c5e7ea1 feat: wire NTP client into firmware build and initialization
- Add ntp.c to CMakeLists.txt SRCS so it's compiled and linked
- Load ntp_server from NVS in load_nvs_config() (default: pool.ntp.org)
- Add ntp_server field to spaxel_state_t
- Initialize NTP after WiFi connects with 10s sync timeout, WARN on failure
- Re-sync NTP after WiFi reconnect (WIFI_LOST state)
- Start periodic 10-minute resync timer via esp_timer
- Add ntp_synced boolean to health JSON message
- Handle ntp_server field in downstream config message
- Fix periodic resync callback to properly stop/restart SNTP

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 13:19:07 -04:00
jedarden
e44dd345f6 feat: implement comprehensive /healthz endpoint
Add complete health check implementation for Docker HEALTHCHECK and
Traefik health routing with:

Response fields:
- status: "ok" or "degraded"
- uptime_s: seconds since mothership boot
- version: mothership version string
- nodes_online: count of connected nodes
- db: "ok" or "failing" (SELECT 1 with 100ms timeout)
- load_level: 0-3 from load shedding state
- reason: human-readable explanation (only when degraded)

HTTP status codes:
- 200 for healthy (status="ok")
- 503 for degraded (status="degraded")

Degraded conditions:
- Database unreachable
- Load level 3 sustained for >60 seconds
- No nodes connected after 5 minutes uptime

Docker HEALTHCHECK updated to verify status="ok" response.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 11:09:36 -04:00
jedarden
391ed884e4 feat: implement NVS schema migration on boot
Implement versioned NVS key migration on ESP32-S3 firmware so
OTA-updated firmware gracefully handles NVS written by older versions.

- Add nvs_migration.c/h with migration framework
- On boot, read schema_ver from NVS; initialize to 1 if missing
- Run migrations sequentially if schema_ver < COMPILED_NVS_VERSION
- Each migration commits after each write for durability
- Log all migration steps to UART for debugging
- Example migration v1→v2: rename 'ms_ip' to 'mothership_ip',
  add 'ntp_server' with default 'pool.ntp.org'
- Migration failure leaves NVS in consistent state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 10:27:38 -04:00
jedarden
ee1caf6ed8 feat: dual-partition OTA rollback validation with 60s role timeout
Update partition layout to 4MB factory/ota_0/ota_1 partitions for larger
firmware images. Add a 60-second validation timer that starts after the
hello message is sent on boot from an OTA partition. The partition is only
marked valid (via esp_ota_mark_app_valid_cancel_rollback) after receiving
a role message from the mothership. If the timer expires without role
receipt, the partition stays unconfirmed and the bootloader rolls back to
the previous firmware on next reset.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 01:43:29 -04:00
jedarden
d2e5b4d4a0 feat: implement passive radar auto-detection of router AP
Automatically detect the home router as a passive radar TX source,
eliminating need for a dedicated active TX node.

Firmware changes:
- During hello message, include ap_bssid and ap_channel from esp_wifi_sta_get_ap_info()

Mothership changes:
- On hello: extract ap_bssid; if >=80% of nodes report same BSSID create virtual node entry with virtual=1
- OUI lookup: embedded IEEE OUI registry as Go map compiled via go:embed; display router brand
- Detect AP BSSID change (router reboot/replacement) and emit system alert
- SQLite nodes table: add virtual BOOL, node_type TEXT, ap_bssid TEXT, ap_channel INT columns

Dashboard changes:
- Show "I detected your router (ASUS). Place it on the floor plan..." notification
- Render virtual AP nodes with distinct router icon in 3D view
- Drag-to-place virtual node (distinct router icon) in 3D editor

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 14:03:03 -04:00
jedarden
f76ab62698 feat(firmware): OTA SHA-256 verification and captive portal URL decoding
- Add git-based version header generation for firmware builds
- Implement SHA-256 hash verification for OTA downloads with mbedtls
- Add URL decoding for captive portal form parsing (spaces, special chars)
- Add mbedtls dependency for SHA-256 verification

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 19:56:11 -04:00
jedarden
bcfd1e3f16 feat(protocol): full bidirectional node protocol over WebSocket
- firmware: wire on-device motion hints to websocket_send_motion_hint()
  with 1s rate-limit; csi_set_rate() now writes to g_state.packet_rate
- firmware: websocket_send_motion_hint() sends variance + MAC + timestamp
  to mothership so rate controller ramps ahead of server-side detection
- mothership: RateController.OnMotionHint() preemptively ramps adjacent
  nodes via SetAdjacentNodesFn callback (topology-aware burst propagation)
- mothership: idle timeout extended to 30s; variance_threshold=0 in active
  mode (server handles detection), DefaultVarianceThreshold=1.0 in idle
- mothership: SendRoleToMAC() exposes dynamic role changes post-connect
- mothership: SendOTAToMAC() enables pushing firmware updates to nodes
- mothership: OTA status events are now logged with state and progress %

Protocol is backward-compatible: binary CSI frames work as in Phase 1;
JSON control messages are additive on the same single WebSocket per node.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 23:14:19 -04:00
jedarden
fb691904c6 feat(dashboard): per-link motion presence indicator with amplitude time series
- Dashboard hub broadcasts motion state changes immediately on transition
  (idle↔motion) via BroadcastMotionState; periodic state snapshots include
  motion_states for new client init
- Per-link presence badge (green CLEAR / red MOTION) rendered in link list
  alongside global presence indicator in status bar
- Amplitude mean time-series chart (60 s rolling window) for selected link,
  line segments colored by motion state at each sample
- Fix: links created from JSON link_active/state events now initialize
  ampHistory and lastAmpSample so time-series accumulates from first frame

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 22:55:33 -04:00
jedarden
f178496c83 feat(firmware): ESP32-S3 firmware skeleton with CSI/BLE/WebSocket
- State machine: 7-state lifecycle (BOOT→WIFI→DISCOVERY→CONNECTED,
  degraded modes WIFI_LOST/MOTHERSHIP_UNAVAILABLE/CAPTIVE_PORTAL)
- WiFi: STA connection with exponential backoff, mDNS discovery with
  cached IP fallback, captive portal AP for provisioning
- WebSocket: bidirectional comms, binary CSI frames (24B header + I/Q),
  JSON hello/health/ble/ota_status upstream, role/config/ota/reboot
  downstream, OTA task with progress reporting
- CSI: promiscuous mode capture, queue-based processing, passive BSSID
  filter, on-device variance tracking (Welford's) for motion hints
- BLE: passive scanning on Core 0, 60-device cache, 5s reporting
- NVS: 15-key schema with versioning, role/rate persistence

Complete: ESP32 firmware skeleton, passive radar, BLE scanning
Remaining: Dashboard skeleton, Docker packaging (Phase 1 items 5-6)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 07:20:55 -04:00