Commit Graph

16 Commits

Author SHA1 Message Date
rUv 5789351b78
fix(esp32): add connected-STA self-ping CSI traffic source (#954) (#985)
The ESP32 CSI engine only produces CSI for received OFDM frames (L-LTF/
HT-LTF). On a quiet network — or on a display-enabled build where the
#893 MGMT->MGMT+DATA promiscuous upgrade is skipped (has_display=true) —
the only CSI-eligible frames are sparse beacons (often non-OFDM DSSS),
so wifi_csi_callback can starve to yield=0pps -> DEGRADED -> motion=0
(#521, #954).

Fix (additive): pin a ~50 Hz OFDM unicast floor by pinging the STA's own
DHCP gateway. The router's ICMP echo replies are OFDM frames destined to
this station and drive the CSI engine regardless of promiscuous filter
state or ambient traffic. Mirrors Espressif's esp-csi csi_recv_router
reference. Promiscuous capture (#396/#893) is left fully intact so
multistatic/multi-node sensing still hears other stations' frames.

Reconciles PR #955 (which removed promiscuous entirely and conflicted
with the already-shipped #893 DATA-capture path) into an additive change
on current main.

Verified on ESP32-S3 (N16R8, COM8), ESP-IDF v5.4:
  Promiscuous mode enabled (MGMT-only, RuView#396)
  self-ping started -> 192.168.1.1 @50Hz (CSI OFDM source, fix #521/#954)
  CSI cb #1: len=128 rssi=-40 ch=5
  adaptive_ctrl: state=6 yield=13-19pps motion=1.00 presence>0  (SENSE_ACTIVE)
DEGRADED cleared; CSI yield stable ~15 pps over 60 s.

Co-authored-by: Meraj <merajmehrabi@gmail.com>
2026-06-09 14:43:12 +02:00
ruv 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.
2026-06-02 09:57:19 +02:00
rUv 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.
2026-05-23 15:34:48 -04:00
Chaitanya Tata cee414f3c0
firmware/esp32-csi-node: IDF 6 build, HE CSI config, unicore DSP, provision chip detect (#522)
* firmware/esp32-csi-node: fix IDF 6 build (PSA SHA-256, explicit REQUIRES)

- rvf_parser: use psa_hash_* / psa_hash_compute; mbedTLS 4 has no public
  mbedtls/sha256.h on the IDF include path.
- main/CMakeLists: declare REQUIRES for WiFi, netif, HTTP, OTA, drivers, lwip,
  mbedtls per ESP-IDF v6 component dependency checks; optional wasm3 when
  CONFIG_WASM_ENABLE.

Signed-off-by: Chaitanya Tata <chaitanya@dotstarconsulting.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* firmware/esp32-csi-node: fix CSI config for Wi-Fi 6 (ESP32-C6)

When CONFIG_SOC_WIFI_HE_SUPPORT is set, wifi_csi_config_t is the
wifi_csi_acquire_config_t bitfield layout. The legacy bool fields
(lltf_en, htltf_en, ...) only apply to ESP32-S3-class targets.

Initialize acquire fields for HE targets; add MAC v3-only members when
CONFIG_SOC_WIFI_MAC_VERSION_NUM >= 3.

Verified: idf.py build for esp32c6 and esp32s3 (ESP-IDF v6.1).

Signed-off-by: Chaitanya Tata <chaitanya@dotstarconsulting.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* firmware/esp32-csi-node: pin edge DSP task for unicore (ESP32-C6)

edge_processing_init used xTaskCreatePinnedToCore(..., core 1). ESP32-C6
runs FreeRTOS unicore (portNUM_PROCESSORS == 1), so core 1 trips the
xTaskCreatePinnedToCore range assert right after CSI init.

Use core 1 only when SMP is available; otherwise pin to core 0.

Signed-off-by: Chaitanya Tata <chaitanya@dotstarconsulting.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* firmware/esp32-csi-node: provision NVS with chip auto-detect

provision.py always passed --chip esp32s3 to esptool, so flashing NVS on
ESP32-C6 failed. Default --chip to auto (esptool v5) and add an explicit
--chip override. Use write-flash instead of deprecated write_flash.

Signed-off-by: Chaitanya Tata <chaitanya@dotstarconsulting.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Signed-off-by: Chaitanya Tata <chaitanya@dotstarconsulting.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-17 18:00:40 -04:00
ruv 3f55c95b34 fix(esp32): disable WiFi modem sleep so CSI capture isn't starved (#521)
csi_collector_init() never called esp_wifi_set_ps(), leaving the radio on
the ESP-IDF STA default WIFI_PS_MIN_MODEM. The modem then sleeps between
DTIM beacons; combined with the MGMT-only promiscuous filter (#396) the
CSI callback is starved and the per-second yield collapses toward 0 pps,
which is what users on a clean multi-node setup were seeing
(motion=0.00 presence=0.00 yield=0pps).

Force WIFI_PS_NONE before enabling promiscuous mode — the textbook
requirement for reliable CSI capture (every ESP-IDF CSI example does it).
New boot line: "csi_collector: WiFi modem sleep disabled (WIFI_PS_NONE)
for CSI capture". Battery duty-cycling is unaffected: power_mgmt_init()
runs after this and re-enables modem sleep when provision.py is given
--duty-cycle <100.

Builds clean for esp32s3 (idf.py build, 48% flash free).

Closes #521

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-11 10:47:48 -04:00
rUv 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>
2026-04-28 08:41:49 -04:00
rUv 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
8dfb031. No source changes.

* Fix implicit decl of emit_feature_state in adaptive_controller

fast_loop_cb calls emit_feature_state() at line 224, but the static
definition is at line 256. GCC treats the implicit declaration as
non-static, then the real static definition conflicts, and
-Werror=all promotes both to hard build errors.

Add a forward declaration above the first use. Unblocks ESP32-S3
firmware build and all QEMU matrix jobs.

Co-Authored-By: claude-flow <ruv@ruv.net>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-20 10:38:23 -04:00
ruv 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>
2026-04-15 13:47:34 -04:00
ruv 8a84748a83 fix(firmware): use NVS node_id instead of Kconfig constant (#279)
CONFIG_CSI_NODE_ID (compile-time, always 1) was hardcoded in 6
places: CSI frame serialization, compressed frames, vitals packets,
WASM output packets, and display UI. NVS provisioning wrote the
correct node_id but it was never used at runtime.

Fixed all occurrences to use g_nvs_config.node_id:
- csi_collector.c: frame header + log message
- edge_processing.c: compressed frame + vitals packet
- wasm_runtime.c: WASM output packet
- display_ui.c: system info display

This means --node-id 0/1/2 provisioning now actually works for
multi-node mesh deployments.

Closes #279

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-16 15:12:45 -04:00
rUv d793c1f49f
feat(firmware): --channel and --filter-mac provisioning (ADR-060)
- provision.py: add --channel (CSI channel override) and --filter-mac
  (AA:BB:CC:DD:EE:FF format) arguments with validation
- nvs_config: add csi_channel, filter_mac[6], filter_mac_set fields;
  read from NVS on boot
- csi_collector: auto-detect AP channel when no NVS override is set;
  filter CSI frames by source MAC when filter_mac is configured
- ADR-060 documents the design and rationale

Fixes #247, fixes #229
2026-03-13 08:27:08 -04:00
ruv 952f27a1ce fix(firmware): enable CSI in sdkconfig and add build guard (ADR-057)
The committed sdkconfig had CONFIG_ESP_WIFI_CSI_ENABLED disabled, causing
all builds to crash at runtime with "CSI not enabled in menuconfig".
Root cause: sdkconfig.defaults.template existed but ESP-IDF only reads
sdkconfig.defaults (no .template suffix).

Fixes:
- Add sdkconfig.defaults with CONFIG_ESP_WIFI_CSI_ENABLED=y
- Add #error compile guard in csi_collector.c to prevent recurrence
- Fix NVS encryption default (requires eFuse, breaks clean builds)

Verified: Docker build + flash to ESP32-S3 + CSI callbacks confirmed.

Closes #241
Relates to #223, #238, #234, #210, #190

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-12 13:49:20 -04:00
rUv ce171696b2
fix: rate-limit CSI sends and add ENOMEM backoff to prevent crash (#132)
The CSI callback fires for every WiFi frame in promiscuous mode
(100-500+ fps). Each call invoked sendto() synchronously, exhausting
lwIP packet buffers (errno 12 = ENOMEM). The rapid-fire failures
cascaded into a LoadProhibited guru meditation crash.

Two fixes:

1. csi_collector.c: Rate-limit UDP sends to 50 Hz (20ms interval).
   CSI frames arriving between sends are dropped — the sensing
   pipeline only needs 20-50 Hz.

2. stream_sender.c: When sendto fails with ENOMEM, suppress further
   sends for 100ms to let lwIP reclaim buffers. Logs the backoff
   event and resumes automatically.

Closes #127
2026-03-03 16:00:40 -05:00
ruv 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>
2026-03-02 23:53:25 -05:00
rUv 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>
2026-03-02 17:08:27 -05:00
ruv 303871275b feat: ADR-029/031 TDM sensing protocol, channel hopping, and NVS config
Implement the hardware and firmware portions of RuvSense (ADR-029) and
RuView (ADR-031) for multistatic WiFi sensing:

Rust (wifi-densepose-hardware):
- TdmSchedule: uniform slot assignments with configurable cycle period,
  guard intervals, and processing window (default 4-node 20 Hz)
- TdmCoordinator: manages sensing cycles, tracks per-slot completion,
  cumulative clock drift compensation (±10 ppm over 50 ms = 0.5 us)
- SyncBeacon: 16-byte wire format for cycle synchronization with
  drift correction offsets
- TdmSlotCompleted event for aggregator notification
- 18 unit tests + 4 doctests, all passing

Firmware (C, ESP32):
- Channel-hop table in csi_collector.c (s_hop_channels, configurable
  via csi_collector_set_hop_table)
- Timer-driven channel hopping via esp_timer at dwell intervals
- NDP frame injection stub via esp_wifi_80211_tx()
- Backward-compatible: hop_count=1 disables hopping entirely
- NVS config extension: hop_count, chan_list, dwell_ms, tdm_slot,
  tdm_node_count with bounds validation and Kconfig fallback defaults

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-01 21:33:48 -05:00
rUv 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>
2026-02-28 13:22:04 -05:00