mirror of
https://github.com/ruvnet/RuView.git
synced 2026-10-23 15:13:03 -05:00
dependabot/github_actions/actions/github-script-9
26
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
33a9e90896 |
Merge pull request #1902 from ruvnet/feat/firmware-usb-onboarding-c6
firmware: USB onboarding on main + C6 console RX fix (0.8.12) |
||
|
|
130afabae8 |
feat(firmware): add secure macOS USB onboarding
(cherry picked from commit 3373e79f38afef3a1430feeb78ae034e78777c66) |
||
|
|
500cfe4787 |
feat(firmware): persistent on-node log and coredump-to-flash (#1878)
* feat(firmware): add the 16MB flash profile the C6 fleet runs on The 4MB and display layouts have 1.875MB and 2MB OTA slots, sized for 4MB and 8MB parts. A 16MB C6 (DevKitC-1 N16) has no layout in the tree at all, so building for one meant hand-editing sdkconfig -- and a smaller table written over a deployed 16MB node relocates NVS and destroys its provisioning, which is why this belongs in the repo rather than in a workstation's untracked sdkconfig. Two 4MB OTA slots, plus a 64K coredump partition and an 8000K FAT volume for on-node storage. Rollback is enabled so an OTA'd image that cannot confirm itself is reverted by the bootloader. Layered, not inherited: sdkconfig.defaults.esp32c6 keeps its 4MB layout so the common 4MB devkit is never silently repartitioned. Co-Authored-By: claude-flow <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01PVWMiHQifoYXL7uL3bphrZ * feat(firmware): persistent on-node log and coredump-to-flash A remote fault currently leaves no post-mortem. main.c decodes the reset reason at boot and logs it to the CONSOLE -- which is exactly what a power cycle destroys, and a power cycle is how a wedged node gets recovered. The flash was already provisioned for this and the features were simply off: partitions_16mb.csv has reserved a 64K `coredump` partition and an 8000K `storage` FAT volume since the 16MB layout was written, neither had ever been mounted or written, and the build defaulted to CONFIG_ESP_COREDUMP_ENABLE_TO_NONE. - node_log.c/h: a 128 B-record ring on the storage volume. Boot records (reset reason + the previous session's final uptime + a health snapshot), periodic health samples, and event records. Ordering is boot_id + uptime_s because these boards have no RTC and nothing can be timestamped absolutely. Counters are stored RAW and differenced at read time, so a cadence change never invalidates old records. Retention, not wear, binds: 128 B every 300 s is ~208 days in 8 MB, and the write rate is bounded explicitly. - ap_rssi_dbm, the association link from esp_wifi_sta_get_ap_info. The adaptive controller's rssi_median_dbm is a CSI statistic, not the uplink, so a node with a bad uplink currently shows no signal for it anywhere. - CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH, complementary rather than a substitute: a coredump catches a CRASH, the boot record catches a HANG followed by a watchdog reset. node_log_init failing is non-fatal by design -- the module disables itself and every entry point becomes a no-op. A logging volume that will not mount must never take the fleet down with it. csi_collector gains two gate accessors. This build selects its frame gate at compile time, so both return CSI_GATE_NOT_CONFIGURABLE rather than a plausible number: the log keeps a field of fixed width and meaning either way, and an explicit "not applicable" is stored instead of something a reader would later reason from as a real setting. Built with ESP-IDF v5.4 for esp32c6 using defaults + .esp32c6 + .16mb. Verified from the resulting artifacts: 16MB table with coredump and storage present, COREDUMP_ENABLE_TO_FLASH set, rollback enabled, image 0x10abd0 bytes in a 4M slot. NOT flashed to hardware from this branch -- the equivalent code was exercised on a node-3 pilot, but that is not evidence for this build. Co-Authored-By: claude-flow <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01PVWMiHQifoYXL7uL3bphrZ * docs(adr-362): reading the on-node log is on-demand, never streamed The log engine lands with no consumer, so the access pattern was open. Records the decision to pull a node's log only when a human asks -- after a hang or a crash, or while troubleshooting that node -- and to keep nothing server-side. The reasoning that decided it: the ring already survives a power cycle on the node (~208 days of retention), so a server copy duplicates a durability property that is already met; and airtime is this fleet's binding constraint, measured at 91% consumed by two slow nodes, so a periodic drain across nine boards would spend exactly the resource whose exhaustion you would open the log to investigate. An instrument must not perturb what it measures. Leaves the decode-where question open on purpose, with the argument for a raw proxy stated: decoding server-side defines the record layout in both C and Rust, and the pilot already paid once for a host/firmware disagreement about a field's meaning. Co-Authored-By: claude-flow <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01PVWMiHQifoYXL7uL3bphrZ --------- Co-authored-by: Joe <joe@GitDev.hedgehog> Co-authored-by: claude-flow <ruv@ruv.net> |
||
|
|
a4d49f76b9 | feat(firmware): add XIAO C6 antenna selection | ||
|
|
944b7bea77 |
fix(firmware): retry WiFi forever; add an uplink watchdog
A node that lost WiFi could give up permanently. The disconnect handler counted retries in s_retry_num, which is only ever cleared on a successful IP_EVENT_STA_GOT_IP, so it accumulated across the node's entire uptime. After ten cumulative disconnects it set WIFI_FAIL_BIT and never called esp_wifi_connect() again. No backoff reset, no periodic retry, no reboot. Only a power cycle cleared it. MEASURED 2026-08-30: the AP serving the fleet took a firmware update and rebooted at ~03:38. All three wall-mounted nodes stopped delivering within three minutes of each other (nodes 0 and 1 at 03:37:03, to the second) and none recovered for 5.5-7.5 hours, until each was physically unplugged. CSI capture ran the whole time -- node 0's sync sequence advanced 16600 -> 46340 at a steady ~30/s while it was invisible to the server -- because capture is driver-level and needs no IP. The boards looked alive on a bench and dead to the aggregator. Retry is now unbounded with 500 ms -> 30 s exponential backoff, scheduled on an esp_timer one-shot so esp_wifi_connect() is never reached through a delay inside the event handler. Two things that were conflated are now separate: WIFI_BOOT_WAIT_ATTEMPTS only releases app_main from its startup wait so the rest of the node can boot; nothing stops the node retrying. The uplink watchdog covers the failures we have not diagnosed: no successful send for CONFIG_UPLINK_WATCHDOG_TIMEOUT_S restarts the node. The signal is a successful sendto(), which on UDP means the stack accepted the datagram, NOT that the server received it -- so stopping the aggregator cannot reboot the fleet. It arms on the first successful send rather than at boot, so a node powered up while the AP is down does not reboot-loop; the unbounded reconnect owns that case. The timeout is 1200 s, sized from a 112-minute soak rather than a guess: 30 observed silences across two nodes, median 225 s, one 510 s outlier that looked like two cycles running together. An earlier 120 s would have rebooted healthy nodes every few minutes, and even 600 s left only 90 s of margin over an event already seen once. Do not lower it below ~900 s while the ~225 s stalls are unexplained; a needless reboot re-runs ESP-NOW leader election and discards mesh time-sync state. Also guards thermal.c's apply_tx_power behind CONFIG_THERMAL_THROTTLE so a monitor-only build (the default, and what is on the wall) stops warning about an unused function. Kconfig gains CSI_GATE_MESH_ALIGNED here alongside the watchdog options; its implementation lands in a later commit and the option is inert until then. Co-Authored-By: claude-flow <ruv@ruv.net> (cherry picked from commit bb6d954de68fd4d542e191fd5e82c753f67a9874) |
||
|
|
5419bff2a8 |
feat(firmware): thermal monitoring, radio throttling, reset reason, heap health
The C6 carries a temperature sensor this firmware has never read. Tolerable on a bench board; not once nine nodes are sealed in printed enclosures on a wall with the radio transmitting continuously. What is protected is the ENCLOSURE, not the silicon. The part is rated far past anything it sees here, but PLA softens at its glass transition (~55-60 C) and sags under its own weight over weeks -- a failure that never announces itself. Thresholds are set against DIE temperature with that offset in mind: ~72 C on the die in a small vented box is roughly a 50-55 C enclosure. Setting the trip at 55 would throttle a perfectly healthy node. Throttling reduces WiFi transmit power in 3 dB steps, the dominant heat source and the one that degrades gracefully -- links weaken but keep working, whereas cutting capture rate would blind the node. Recovery has 8 C of hysteresis because oscillating around the trip point would put a visible RSSI step on every link the node has, repeatedly. `thermal_tx_dbm()` and `thermal_state()` are exported for a specific reason: a throttled node's RSSI falls at both ends, which is indistinguishable from an obstruction in the link diagnostics. Anything reading link strength must be able to tell a hot node from a wall. Also records two things nothing tracked before: - **Reset reason at boot.** A node that reboots tells you almost nothing; one that reboots with ESP_RST_BROWNOUT tells you its supply sagged, which on shared outlets is a wiring problem rather than a firmware one. Three nodes dropping together on 2026-08-28 would have been answered by this one line. - **Free and minimum-free heap** in the periodic log. A slow leak and an overheat look identical after ten days unless the minimum has been visibly walking downward the whole time. Kconfig-gated (default y on C6), thresholds configurable. Builds clean at 0xfe990, 45% partition free. Co-Authored-By: claude-flow <ruv@ruv.net> (cherry picked from commit 898295fba7b3873b37e85a5ffc0ec87fe609aa23) |
||
|
|
5780c239e4 | security: repair scanning and close stale alert sources | ||
|
|
6f0114c488 |
fix(firmware): accept WPA/WPA2-mixed routers; warn about compact-board thermal risk
Lower the STA auth threshold from WPA2_PSK to WPA_PSK so routers running WPA/WPA2-mixed compatibility mode aren't rejected with WIFI_REASON_NO_AP_FOUND_IN_AUTHMODE_THRESHOLD (reason=211), and log the disconnect reason/rssi instead of retrying blind. Also document the thermal risk of running this firmware's continuous-radio, tier-2 DSP pipeline on coin-sized clone boards (ESP32-S3-Zero, SuperMini) with minimal PCB copper and budget regulators, after a field report of three such boards failing to power on following a normal session. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
4bf88e1283 |
feat(firmware): gate LED gamma viz behind CONFIG_LED_GAMMA_VIZ (ADR-183 follow-up) (#1129)
The 40 Hz gamma flicker is now Kconfig-gated (default y, unchanged behaviour). Set CONFIG_LED_GAMMA_VIZ=n for a dark, lower-power boot (the LED is simply cleared) — important for photosensitive deployments, no source edit needed. The colormap saturation point is now operator-tunable via CONFIG_LED_MOTION_FULLSCALE_MILLI (default 250 = 0.25). Build + flash confirmed on ESP32-S3 N16R8 (COM8): default y still arms the gamma timer, CSI flows. ADR-183 updated (gate moved from follow-up to done). |
||
|
|
a4c2935a2f |
feat(firmware): onboard LED 40 Hz gamma stimulus + CSI-motion colour (ADR-183) (#1127)
* chore(deps): bump ruv-neural submodule — ColorMap no_std for ESP32 Points to ruvnet/ruv-neural#3 (c9638fa): ruv-neural-viz::ColorMap now builds no_std, so it can run on the ESP32. Unblocks driving the onboard WS2812 from the viridis/cool-warm colormap. Co-Authored-By: claude-flow <ruv@ruv.net> * feat(firmware): onboard LED as 40 Hz gamma stimulus, colour from live CSI motion (ADR-183) The S3 onboard WS2812 (GPIO 48, #962) now runs a GENUS-style 40 Hz gamma square wave (12.5 ms on/off, 50% duty). The ON-phase colour is live CSI motion (edge motion_energy) mapped through a 60-step viridis LUT generated from ruv-neural-viz::ColorMap::viridis() — still=purple, moving=yellow. Uses the now-no_std ColorMap (ruvnet/ruv-neural#3 / #1126). Hardware- confirmed on ESP32-S3 N16R8 (COM8): boot log shows the timer armed, CSI keeps flowing (27-38 pps). Honesty + photosensitivity notes + a Kconfig-gate follow-up are in ADR-183. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
898c536eac |
fix(firmware): capture DATA frames on display-less boards — #893/#866/#897
The pre-built binaries set a MGMT-only promiscuous filter (WIFI_PROMIS_FILTER_MASK_MGMT) as the #396 workaround — DATA-frame interrupt load races the QSPI display's SPI traffic against the SPI-flash cache and crashes Core 0 in wDev_ProcessFiq. But MGMT-only fires the CSI callback only on sparse management frames, so on the common DISPLAY-LESS boards (DevKitC-1, T7-S3, N8R8) CSI yield collapses to 0 pps under real traffic (#521) — the node looks dead despite being on the network, which is the root cause of most "can't reproduce / it's fake" reports (#804/#37). A board with no AMOLED panel has no QSPI/SPI-flash contention, so it can safely capture DATA frames. After the boot-time display probe runs: - display present -> keep MGMT-only (preserve #396 crash protection) - no display -> upgrade filter to MGMT|DATA (restore CSI yield) Implementation (runtime-gated, no boot reorder): - display_task.c: s_display_active flag + display_is_active() accessor, set true only when the panel is detected and the display task starts. - csi_collector.c: csi_collector_enable_data_capture() re-sets the promiscuous filter to MGMT|DATA. - main.c: after display_task_start(), if !display_is_active() (or display support not compiled in), upgrade the filter. Build-verified on BOTH targets: esp32c6 (headless path) and esp32s3 (display path, display_task.c compiled) — Project build complete, RC 0. Needs on-hardware confirmation that yield recovers and no #396 crash. |
||
|
|
00a234eda8 |
ADR-110: ESP32-C6 firmware extension (#764)
Closes the firmware-side ADR-110 design at v0.7.0-esp32 after a 38-iter /loop SOTA sprint. Headline (bench, COM9+COM12 ESP32-C6): - 99.56% cross-board RX, 104.1 µs smoothed offset stdev (≤100 µs §2.4 target met) - 3.95× EMA suppression, 1.4 ppm crystal skew preserved 4 firmware releases: v0.6.7 / v0.6.8 / v0.6.9 / v0.7.0-esp32. 42 ADR-110 unit tests, 1761 v2 workspace tests, full Firmware CI + QEMU green. |
||
|
|
fa28318bae | fix(led): disable onboard WS2812 LED during CSI collection (#273) | ||
|
|
f06d0c6ab5 |
fix(firmware): SPI cache crash fix + node_id/filter_mac defensive copies + esptool v5 (rebased #397)
* fix(firmware): move defensive node_id capture before wifi_init_sta()
The original defensive copy in csi_collector_init() (line 172 of main.c)
runs AFTER wifi_init_sta() (line 147), which on some ESP32-S3 devices
corrupts g_nvs_config.node_id back to the Kconfig default of 1.
Reproduced on device 80:b5:4e:c1:be:b8 (ESP32-S3 QFN56 rev v0.2):
- NVS provisioned with node_id=5
- Release firmware (no fix): seed receives node_id=1 (clobbered)
- This patch: seed receives node_id=5 (correct)
Changes:
- Add csi_collector_set_node_id() called from main.c immediately
after nvs_config_load(), before wifi_init_sta() runs
- csi_collector_init() now detects and logs the clobber if early
capture disagrees with current g_nvs_config value
- Fallback path preserved: if set_node_id() is never called,
init() still captures from g_nvs_config (backwards compatible)
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(firmware): defensive copy of filter_mac to prevent callback crash
The CSI callback reads g_nvs_config.filter_mac_set and filter_mac on
every invocation (100-500 Hz). If wifi_init_sta() corrupts g_nvs_config
(same root cause as the node_id clobber), the callback reads garbage
from the struct, leading to Core 0 LoadProhibited panic after ~2400
callbacks (~70 seconds of operation).
Extends the early-capture pattern from the node_id fix to also copy
filter_mac_set and filter_mac into module-local statics before WiFi
init runs. Adds canary logging to detect filter_mac corruption.
Observed on device 80:b5:4e:c1:be:b8 via serial:
CSI cb #2400 → Guru Meditation Error: Core 0 panic'ed (LoadProhibited)
→ TG0WDT_SYS_RST → reboot → crash again at ~2900 callbacks
Refs #232 #375 #385 #386 #390
Co-Authored-By: Ruflo & AQE
* fix(firmware): MGMT-only promiscuous filter to prevent SPI cache crash
The WiFi driver's wDev_ProcessFiq interrupt handler crashes with
LoadProhibited in cache_ll_l1_resume_icache when promiscuous mode
captures MGMT+DATA frames (100-500 interrupts/sec). The high interrupt
rate races with SPI flash cache operations, corrupting cache state.
Changes:
- Promiscuous filter: MGMT+DATA → MGMT-only (~10 Hz beacons)
- CSI config: disable htltf_en and stbc_htltf2_en (LLTF-only)
LLTF provides 64 subcarriers (HT20) — sufficient for presence,
breathing, and fall detection. The 10 Hz beacon rate eliminates
the SPI flash cache contention that caused the crash.
Verified on device 80:b5:4e:c1:be:b8:
- Before: LoadProhibited crash at ~1600-2400 callbacks (every ~70s)
- After: 2700+ callbacks over 4.7 minutes, zero crashes
Backtrace decode confirmed crash in ESP-IDF closed-source WiFi blob:
_xt_lowint1 → wDev_ProcessFiq → spi_flash_restore_cache
→ cache_ll_l1_resume_icache → EXCVADDR=0x00000004 (NULL deref)
Co-Authored-By: Ruflo & AQE
* fix(provision): write-flash → write_flash for esptool v5 compat
esptool v5+ rejects hyphenated subcommands. The provision script
used 'write-flash' which fails with "invalid choice". Changed to
'write_flash' (underscore) which works with both old and new esptool.
Co-Authored-By: Ruflo & AQE
* fix(firmware): 50 Hz callback rate gate + sdkconfig extra IRAM opt
- Add early rate gate in wifi_csi_callback at 50 Hz (defense-in-depth,
does not prevent crash alone but reduces callback execution time)
- Add null-data injection timer infrastructure (disabled — TX adds
interrupt pressure that triggers the SPI cache crash, RuView#396)
- sdkconfig.defaults: add CONFIG_ESP_WIFI_EXTRA_IRAM_OPT=y
- sdkconfig.defaults: document SPIRAM XIP attempt (crashes differently)
Co-Authored-By: Ruflo & AQE
* fix(firmware): address PR #397 review feedback
Applies @ruvnet's five review requests on PR #397 (RuView#397 comment
4289417527):
1. **Inline comment on `provision.py` `write_flash`** — ESP-IDF v5.4
bundles esptool 4.10.0 (underscore-only). #391's hyphen swap broke
the documented venv flow; kept the underscore form and added a
three-line comment warning future maintainers not to "re-fix" it.
2. **Correct `edge_processing.c` sample_rate** (blocking) — changed
hard-coded `20.0f` → `10.0f` at line 718 so
`estimate_bpm_zero_crossing()` matches the MGMT-only CSI rate.
Without this, breathing and heart-rate reports were 2× the true
value. Added a comment tying the constant to the callback rate gate.
3. **Removed disabled probe-injection infrastructure** — dropped the
forward declaration, the `CSI_PROBE_INTERVAL_MS` define, six static
variables (`s_probe_timer`, `s_probe_tx_count`, `s_probe_tx_fail`,
`s_ap_bssid`, `s_ap_bssid_known`), and three functions
(`csi_send_probe_request`, `probe_timer_cb`,
`csi_collector_start_probe_timer`). None were reachable.
`csi_inject_ndp_frame()` reverted to the original ADR-029 stub.
Can be revived from this commit's parent if needed.
4. **Cleaned `sdkconfig.defaults`** — removed the SPIRAM prose and
commented-out `# CONFIG_SPIRAM is not set` line. Kept only the live
`CONFIG_ESP_WIFI_EXTRA_IRAM_OPT=y` with a concise rationale.
5. **Bumped firmware version 0.6.1 → 0.6.2** and added four
`[Unreleased]` CHANGELOG entries covering the SPI cache crash fix,
the `filter_mac` / `node_id` clobber defense, the sample-rate
correction, and the `write_flash` command-form revert.
Net: +39 / -128 across six files.
Validation in this devcontainer:
- Static sanity on modified C files: braces balance (csi_collector.c
59/59; edge_processing.c 96/96), zero dangling references to removed
probe-injection symbols.
- Rust workspace tests and Python proof not executed here — cargo not
installed and pip blocked by PEP 668. Deferring hardware build +
flash + miniterm verification to @ruvnet's COM7 per his offer in
the review comment.
Co-Authored-By: claude-flow <ruv@ruv.net>
---------
Co-authored-by: Dragan Spiridonov <spiridonovdragan@gmail.com>
|
||
|
|
5a7f431b0e |
ADR-081: Implement 5-layer adaptive CSI mesh firmware kernel (#404)
* ADR-081: adaptive CSI mesh firmware kernel + scaffolding
Introduces a 5-layer firmware kernel that reframes the existing ESP32
modules as components of a chipset-agnostic architecture and authorizes
adaptive control + a compact feature-state stream as the default upstream.
Layers:
L1 Radio Abstraction Layer — rv_radio_ops_t vtable + ESP32 binding
L2 Adaptive Controller — fast/medium/slow loops (200ms/1s/30s)
L3 Mesh Sensing Plane — anchor/observer/relay/coordinator (spec)
L4 On-device Feature Extr. — rv_feature_state_t (magic 0xC5110006)
L5 Rust handoff — feature_state default; debug raw gated
Files:
docs/adr/ADR-081-adaptive-csi-mesh-firmware-kernel.md (new)
firmware/esp32-csi-node/main/rv_radio_ops.h (new)
firmware/esp32-csi-node/main/rv_radio_ops_esp32.c (new)
firmware/esp32-csi-node/main/rv_feature_state.{h,c} (new)
firmware/esp32-csi-node/main/adaptive_controller.{h,c} (new)
firmware/esp32-csi-node/main/main.c (wire L1+L2)
firmware/esp32-csi-node/main/CMakeLists.txt (add 4 sources)
firmware/esp32-csi-node/main/Kconfig.projbuild (controller knobs)
CHANGELOG.md (Unreleased)
Default policy is conservative: enable_channel_switch and
enable_role_change are off, so behavior matches today's firmware
unless an operator opts in via menuconfig. The pure
adaptive_controller_decide() is exposed for offline unit tests.
Reuses (does not rewrite): csi_collector, edge_processing (ADR-039),
swarm_bridge (ADR-066), secure_tdm (ADR-032), wasm_runtime (ADR-040).
* ADR-081: implement Layers 1/2/4 end-to-end + host tests + QEMU hooks
Turns the ADR-081 scaffolding into a working adaptive CSI mesh kernel:
Layer 1 radio abstraction has an ESP32 binding and a mock binding; Layer 2
adaptive controller runs on FreeRTOS timers; Layer 4 feature-state packet
is emitted at 5 Hz by default, replacing raw ADR-018 CSI as the default
upstream.
New files:
firmware/esp32-csi-node/main/adaptive_controller_decide.c (pure policy)
firmware/esp32-csi-node/main/rv_radio_ops_mock.c (QEMU binding)
firmware/esp32-csi-node/tests/host/Makefile (host tests)
firmware/esp32-csi-node/tests/host/test_adaptive_controller.c
firmware/esp32-csi-node/tests/host/test_rv_feature_state.c
firmware/esp32-csi-node/tests/host/esp_err.h (shim)
firmware/esp32-csi-node/tests/host/.gitignore
Modified:
adaptive_controller.c — includes pure decide.c; emit_feature_state()
wired into fast loop (200 ms = 5 Hz)
rv_radio_ops_esp32.c — get_health() fills pkt_yield + send_fail
csi_collector.{c,h} — pkt_yield/send_fail accessors (ADR-081 L1)
rv_feature_state.h — packed size corrected to 60 bytes
(was incorrectly 80 in initial commit)
main.c — mock binding registered under mock CSI
CMakeLists.txt — rv_radio_ops_mock.c under CSI_MOCK_ENABLED
scripts/validate_qemu_output.py — 3 new ADR-081 checks (17/18/19)
docs/adr/ADR-081-*.md — status → Accepted (partial);
implementation-status matrix; measured
benchmarks (decide 3.2 ns, CRC32 614 ns);
bandwidth 300 B/s @ 5 Hz (99.7% vs raw);
verification section
CHANGELOG.md — artifact-level entries
Tests (host, gcc -O2 -std=c11):
test_adaptive_controller: 18/18 pass, decide() = 3.2 ns/call
test_rv_feature_state: 15/15 pass, CRC32(56 B) = 614 ns/pkt, 87 MB/s
sizeof(rv_feature_state_t) == 60 asserted
IEEE CRC32 known vectors verified
Deferred (tracked in ADR-081 roadmap Phase 3/4):
Layer 3 mesh-plane message types, role-assignment FSM, Rust-side mirror
trait in crates/wifi-densepose-hardware/src/radio_ops.rs.
* ADR-081: Layer 3 mesh plane + Rust mirror trait — all 5 layers landed
Fully implements the remaining deferred pieces of the adaptive CSI mesh
firmware kernel. All 5 layers (Radio Abstraction, Adaptive Controller,
Mesh Sensing Plane, On-device Feature Extraction, Rust handoff) are
now implemented and host-tested end-to-end.
Layer 3 — Mesh Sensing Plane (firmware/esp32-csi-node/main/rv_mesh.{h,c}):
* 4 node roles: Unassigned / Anchor / Observer / FusionRelay / Coordinator
* 7 message types: TIME_SYNC, ROLE_ASSIGN, CHANNEL_PLAN,
CALIBRATION_START, FEATURE_DELTA, HEALTH, ANOMALY_ALERT
* 3 auth classes: None / HMAC-SHA256-session / Ed25519-batch
* Payload types: rv_node_status_t (28 B), rv_anomaly_alert_t (28 B),
rv_time_sync_t (16 B), rv_role_assign_t (16 B),
rv_channel_plan_t (24 B), rv_calibration_start_t (20 B)
* 16-byte envelope + payload + IEEE CRC32 trailer
* Pure rv_mesh_encode()/rv_mesh_decode() plus typed convenience encoders
* rv_mesh_send_health() + rv_mesh_send_anomaly() helpers
Controller wiring (adaptive_controller.c):
* Slow loop (30 s default) now emits HEALTH
* apply_decision() emits ANOMALY_ALERT on transitions to ALERT /
DEGRADED
* Role + mesh epoch tracked in module state; epoch bumps on role
change
Layer 5 — Rust mirror (crates/wifi-densepose-hardware/src/radio_ops.rs):
* RadioOps trait mirrors rv_radio_ops_t vtable
* MockRadio backend for offline tests
* MeshHeader / NodeStatus / AnomalyAlert types mirror rv_mesh.h
* Byte-identical IEEE CRC32 (poly 0xEDB88320) verified against
firmware test vectors (0xCBF43926 for "123456789")
* decode_mesh / decode_node_status / decode_anomaly_alert / encode_health
* 8 unit tests, including mesh_constants_match_firmware which asserts
MESH_MAGIC/VERSION/HEADER_SIZE/MAX_PAYLOAD match rv_mesh.h
byte-for-byte
* Exported from lib.rs
* signal/ruvector/train/mat crates untouched — satisfies ADR-081
portability acceptance test
Tests (all passing):
test_adaptive_controller: 18/18 (C, decide() 3.2 ns/call)
test_rv_feature_state: 15/15 (C, CRC32 87 MB/s)
test_rv_mesh: 27/27 (C, roundtrip 1.0 µs)
radio_ops::tests (Rust): 8/8
--- total: 68/68 assertions green ---
Docs:
* ADR-081 status flipped to Accepted
* Implementation-status matrix updated; L3 + Rust mirror both
marked Implemented
* Benchmarks table extended with rv_mesh encode+decode roundtrip
* Verification section updated with cargo test invocation
* CHANGELOG: two new entries for L3 mesh plane + Rust mirror
Remaining follow-ups (Phase 3.5 polish, not blocking):
* Mesh RX path (UDP listener + dispatch) on the firmware
* Ed25519 signing for CHANNEL_PLAN / CALIBRATION_START
* Hardware validation on COM7
* Add test_rv_mesh to host-test .gitignore
Fixes an untracked-file warning from the repo stop-hook: the compiled
binary was built by make but the .gitignore update was missed in
|
||
|
|
425f0e6aac |
fix(firmware): defensive node_id capture prevents runtime clobber (#390)
Users on multi-node ESP32 deployments have been reporting for months
that their provisioned `node_id` reverts to the Kconfig default of `1`
in UDP frames and the `csi_collector` init log, despite boot showing:
nvs_config: NVS override: node_id=4
main: ESP32-S3 CSI Node (ADR-018) - Node ID: 4
csi_collector: CSI collection initialized (node_id=1, channel=11)
See #232, #375, #385, #386, #390. The root memory-corruption path for
the `g_nvs_config.node_id` byte has not been definitively isolated
(does not reproduce on my attached ESP32-S3 running current source
and the v0.6.0 release binary), but the UDP frame header can be made
tamper-proof regardless:
1. `csi_collector_init()` now captures `g_nvs_config.node_id` into a
module-local `static uint8_t s_node_id` at init time.
2. `csi_serialize_frame()` reads `buf[4]` from `s_node_id`, not from
the global - so any later corruption of `g_nvs_config` cannot
affect outgoing CSI frames.
3. All other consumers (`edge_processing.c` x3, `wasm_runtime.c`,
`display_ui.c`, `main.c swarm_bridge_init`) now go through a new
`csi_collector_get_node_id()` accessor instead of reading the
global directly.
4. A canary at end-of-init logs `WARN` if `g_nvs_config.node_id`
already diverges from the captured value - this will pinpoint
the corruption path if it happens on a user's device.
Hardware validation on attached ESP32-S3 (COM8):
- NVS loads node_id=2
- Boot log: `main: ... Node ID: 2`
- NEW log: `csi_collector: Captured node_id=2 at init (defensive
copy for #232/#375/#385/#390)`
- Init log: `csi_collector: CSI collection initialized (node_id=2)`
- UDP frame byte[4] = 2 (verified via socket sniffer, 15/15 packets)
This is defense in depth - it shields the UDP frame from whatever
upstream bug is clobbering the struct. When a user hits the original
bug, the canary WARN will help isolate the root cause.
Refs #232 #375 #385 #386 #390
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||
|
|
b5e924cd72 |
fix: embed firmware version from version.txt, log at boot (#354)
- Add version.txt (0.6.0) read by CMakeLists.txt so esp_app_get_description()->version matches the release tag - Log firmware version on boot: "v0.6.0 — Node ID: X" - Remove stale Kconfig help text (said default 2.0, actual is 15.0) Fixes the version mismatch reported in #354 where flashing v0.5.3 binaries showed v0.4.3 because PROJECT_VER was never set. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
9d70d621da |
feat: ADR-073 enable multi-frequency channel hopping from NVS
- main.c: call csi_collector_set_hop_table() at boot when hop_count > 1 - provision.py: add --hop-channels and --hop-dwell flags, write chan_list blob and dwell_ms to NVS matching firmware's expected format - Validated: Node 1 hopping ch 1/6/11, Node 2 hopping ch 3/5/9, 200ms dwell, null subcarriers reduced from 19% to 16% Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
2b8a7cc458 |
feat: happiness scoring pipeline + ESP32 swarm with Cognitum Seed (#285)
* feat: happiness scoring pipeline with ESP32 swarm + Cognitum Seed coordinator ADR-065: Hotel guest happiness scoring from WiFi CSI physiological proxies. ADR-066: ESP32 swarm with Cognitum Seed as coordinator for multi-zone analytics. Firmware: - swarm_bridge.c/h: FreeRTOS task on Core 0, HTTP client with Bearer auth, registers with Seed, sends heartbeats (30s) and happiness vectors (5s) - nvs_config: seed_url, seed_token, zone_name, swarm intervals - provision.py: --seed-url, --seed-token, --zone CLI args - esp32-hello-world: capability discovery firmware for 4MB ESP32-S3 variant WASM edge modules: - exo_happiness_score.rs: 8-dim happiness vector from gait speed, stride regularity, movement fluidity, breathing calm, posture, dwell time (events 690-694, 11 tests, ESP32-optimized buffers + event decimation) - ghost_hunter.rs standalone binary: 5.7 KB WASM, feature-gated default pipeline RuView Live: - --mode happiness dashboard with bar visualization - --seed flag for Cognitum Seed bridge (urllib, background POST) - HappinessScorer + SeedBridge classes (stdlib only, no deps) Examples: - seed_query.py: CLI tool (status, search, witness, monitor, report) - provision_swarm.sh: batch provisioning for multi-node deployment - happiness_vector_schema.json: 8-dim vector format documentation Verified live: ESP32 on COM5 (4MB flash) registered with Seed at 10.1.10.236, vectors flowing, witness chain growing (epoch 455, chain 1108). Co-Authored-By: claude-flow <ruv@ruv.net> * ci: raise firmware binary size gate to 1100 KB for HTTP client stack The swarm bridge (ADR-066) adds esp_http_client for Seed communication, which pulls in the HTTP/TLS stack (~150 KB). Binary grew from ~978 KB to ~1077 KB. Raise the gate from 950 KB to 1100 KB. Still fits comfortably in both 4MB (1856 KB OTA slot, 43% free) and 8MB flash variants. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
66e2fa0835 |
feat: ADR-063/064 mmWave sensor fusion + multimodal ambient intelligence (#269)
* docs: ADR-063 mmWave sensor fusion with WiFi CSI 60 GHz mmWave radar (Seeed MR60BHA2, HLK-LD2410/LD2450) fusion with WiFi CSI for dual-confirm fall detection, clinical-grade vitals, and self-calibrating CSI pipeline. Covers auto-detection, 6 supported sensors, Kalman fusion, extended 48-byte vitals packet, RuVector/RuvSense integration points, and 6-phase implementation plan. Based on live hardware capture from ESP32-C6 + MR60BHA2 on COM4. Co-Authored-By: claude-flow <ruv@ruv.net> * feat(firmware): ADR-063 mmWave sensor fusion — full implementation Phase 1-2 of ADR-063: mmwave_sensor.c/h: - MR60BHA2 UART parser (60 GHz: HR, BR, presence, distance) - LD2410 UART parser (24 GHz: presence, distance) - Auto-detection: probes UART for known frame headers at boot - Mock generator for QEMU testing (synthetic HR 72±2, BR 16±1) - Capability flag registration per sensor type edge_processing.c/h: - 48-byte fused vitals packet (magic 0xC5110004) - Kalman-style fusion: mmWave 80% + CSI 20% when both available - Automatic fallback to CSI-only 32-byte packet when no mmWave - Dual presence flag (Bit3 = mmwave_present) main.c: - mmwave_sensor_init() called at boot with auto-detect - Status logged in startup banner Fuzz stubs updated for mmwave_sensor API. Build verified: QEMU mock build passes. Co-Authored-By: claude-flow <ruv@ruv.net> * fix(firmware): correct MR60BHA2 + LD2410 UART protocols (ADR-063) MR60BHA2: SOF=0x01 (not 0x5359), XOR+NOT checksums on header and data, frame types 0x0A14 (BR), 0x0A15 (HR), 0x0A16 (distance), 0x0F09 (presence). Based on Seeed Arduino library research. LD2410: 256000 baud (not 115200), 0xAA report head marker, target state byte at offset 2 (after data_type + head_marker). Auto-detect: probes MR60 at 115200 first, then LD2410 at 256000. Sets final baud rate after detection. Co-Authored-By: claude-flow <ruv@ruv.net> * feat: ADR-063 Phase 6 server-side mmWave + CSI fusion bridge Python script reads both serial ports simultaneously: - COM4 (ESP32-C6 + MR60BHA2): parses ESPHome debug output for HR, BR, presence, distance - COM7 (ESP32-S3): reads CSI edge processing frames Kalman-style fusion: mmWave 80% + CSI 20% for vitals, OR gate for presence. Verified on real hardware: mmWave HR=75bpm, BR=25/min at 52cm range, CSI frames flowing concurrently. Both sensors live for 30 seconds. Co-Authored-By: claude-flow <ruv@ruv.net> * docs: ADR-064 multimodal ambient intelligence roadmap 25+ applications across 4 tiers from practical to exotic: - Tier 1 (build now): zero-FP fall detection, sleep monitoring, occupancy HVAC, baby breathing, bathroom safety - Tier 2 (research): gait analysis, stress detection, gesture control, respiratory screening, multi-room activity - Tier 3 (frontier): cardiac arrhythmia, RF tomography, sign language, cognitive load, swarm sensing - Tier 4 (exotic): emotion contagion, lucid dreaming, plant monitoring, pet behavior Priority matrix with effort estimates. All P0-P1 items work with existing hardware (ESP32-S3 + MR60BHA2 + BH1750). Co-Authored-By: claude-flow <ruv@ruv.net> * fix(ci): add ESP_ERR_NOT_FOUND to fuzz stubs mmwave_sensor stub returns ESP_ERR_NOT_FOUND which wasn't defined in the minimal esp_stubs.h for host-based fuzz testing. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
523be943b0 |
feat: QEMU ESP32-S3 testing platform + swarm configurator (ADR-061/062) (#260)
9-layer QEMU testing platform (ADR-061) and YAML-driven swarm configurator (ADR-062) for ESP32-S3 firmware testing without hardware. 12 commits, 56 files, +9,500 lines. Tested on Windows with Espressif QEMU 9.0.0 — firmware boots, mock CSI generates frames, 14/16 validation checks pass. 39 bugs found and fixed across 2 deep code reviews. Closes #259 Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
8b57a6f64c |
docs: update README with ADR-045–048, Observatory, adaptive classifier, AMOLED display
- Update ADR count from 44 to 48 - Add adaptive classifier (ADR-048) to Intelligence features - Add Observatory visualization (ADR-047) and AMOLED display (ADR-045) to Deployment features - Update screenshot to v2-screen.png - Add ADR-045 (AMOLED), ADR-046 (Android TV), ADR-047 (Observatory), DDD deployment model - Add AMOLED display firmware (display_hal, display_task, display_ui, LVGL config) - Add Observatory UI (13 Three.js modules, CSS, HTML entry point) - Add trained adaptive model JSON - Update .gitignore for managed_components, recordings, .swarm Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
4b1005524e |
feat: complete vendor repos, add edge intelligence and WASM modules
- Add 154 missing vendor files (gitignore was filtering them) - vendor/midstream: 564 files (was 561) - vendor/sublinear-time-solver: 1190 files (was 1039) - Add ESP32 edge processing (ADR-039): presence, vitals, fall detection - Add WASM programmable sensing (ADR-040/041) with wasm3 runtime - Add firmware CI workflow (.github/workflows/firmware-ci.yml) - Add wifi-densepose-wasm-edge crate for edge WASM modules - Update sensing server, provision.py, UI components Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
915943cef4 |
feat: ESP32 CSI MAC address filtering with NVS/Kconfig support (#101)
* feat: add MAC address filter for ESP32 CSI collection In multi-AP environments, CSI frames from different access points get mixed together, corrupting the sensing signal. Add transmitter MAC filtering so only frames from a specified AP are processed. Implementation: - csi_collector: filter in wifi_csi_callback by comparing info->mac against configured MAC; log transmitter MAC in periodic debug output - csi_collector_set_filter_mac(): runtime API to enable/disable filter - Kconfig: CSI_FILTER_MAC option (format "AA:BB:CC:DD:EE:FF") - NVS: "filter_mac" 6-byte blob overrides Kconfig at runtime - nvs_config: parse Kconfig MAC string at boot, load NVS override - main: apply filter from config after csi_collector_init() When no filter is configured (default), behavior is unchanged — all transmitter MACs are accepted for backward compatibility. Fixes #98 Co-Authored-By: claude-flow <ruv@ruv.net> * chore: add CLAUDE.local.md to .gitignore Local machine configuration (ESP-IDF paths, COM port, build instructions) should not be committed to the repository. Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
7872987ee6 |
fix(docker): Update Dockerfile paths from src/ to v1/src/
The source code was moved to v1/src/ but the Dockerfile still referenced src/ directly, causing build failures. Updated all COPY paths, uvicorn module paths, test paths, and bandit scan paths. Also added missing v1/__init__.py for Python module resolution. Fixes #33 Co-Authored-By: claude-flow <ruv@ruv.net> |
||
|
|
92a5182dc3 |
feat(adr-018): ESP32-S3 firmware, Rust aggregator, and live CSI pipeline
Complete end-to-end WiFi CSI capture pipeline verified on real hardware: - ESP32-S3 firmware: WiFi STA + promiscuous mode CSI collection, ADR-018 binary serialization, UDP streaming at ~20 Hz - Rust aggregator CLI binary (clap): receives UDP frames, parses with Esp32CsiParser, prints per-frame summary (node, seq, rssi, amp) - UDP aggregator module with per-node sequence tracking and drop detection - CsiFrame bridge to detection pipeline (amplitude/phase/SNR conversion) - Python ESP32 binary parser with UDP reader - Presence detection confirmed: motion score 10/10 from live CSI variance Hardware verified: ESP32-S3-DevKitC-1 (CP2102, MAC 3C:0F:02:EC:C2:28), Docker ESP-IDF v5.2 build, esptool 5.1.0 flash, 20 Rust + 6 Python tests pass. Co-Authored-By: claude-flow <ruv@ruv.net> |