- 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>
- 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>
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>
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>
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>
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>
- 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>
- 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>
- 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>