Previously the ready signal was sent exactly once at window open, so the
browser had to have the serial port open at that exact millisecond after
reboot. Now the firmware broadcasts SPAXEL READY every second for the
full provisioning window (2 min fresh / 15 s reprov), giving the host
ample time to open the port and catch the handshake.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add optional mothership IP override to the provisioning flow so nodes
on networks where mDNS is blocked (enterprise WiFi, mesh, VLANs) can
connect on first boot without manual intervention.
- Add ms_ip field to provisioning Payload and request structs
- Firmware writes ms_ip to both NVS_KEY_MS_IP and NVS_KEY_MS_IP_PROV
- Discovery prefers provisioned IP on first attempt, falls back to
mDNS, then cached IP
- Web Serial wizard adds Mothership IP field in Network Troubleshooting
- Auto-populates IP when browser accesses dashboard by IP address
- Document when/how to use the override in docs/notes/mdns-override.md
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the hardcoded flash_mb:16 with a runtime query so the mothership
receives the correct physical flash size regardless of the hardware variant.
On the 4MB XMC board this will now report 4; on a 16MB board it reports 16.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SSID input: add autocorrect=off, autocapitalize=none, spellcheck=false to
prevent mobile browsers from silently altering SSIDs with special chars
- Password input: same attrs for consistency
- Firmware: accept WPA/WPA2 mixed mode (WIFI_AUTH_WPA_WPA2_PSK) so networks
with special characters in the password connect regardless of WPA version
- Firmware: detect open networks (empty password) and use WIFI_AUTH_OPEN so
passwordless networks are not rejected by the auth threshold
JSON encoding path (JSON.stringify → TextEncoderStream → cJSON) already
handles all characters correctly; these changes prevent browser-side mangling
and firmware-side connection rejection.
- Show real-time status messages and a collapsible log panel during WiFi provisioning
- Thread addProvLog/setProvStatus callbacks through provisionAndSend and sendPayloadOverSerial
- Log every stage: mothership fetch, payload assembly, port open retries, serial send/response
- All log lines also go to browser console.log/warn/error
- Firmware: extend provisioning window from 10s to 120s for fresh boards (15s for re-provisioning)
- Firmware: include MAC address in SPAXEL READY message for display
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mdns was moved out of the built-in ESP-IDF tree in v5.x and must be
declared via idf_component.yml for the component manager to resolve it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
led.c was missing from the build, causing undefined reference errors
for led_init, led_stop_blink, and led_blink_identify at link time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>