Files
wifi-ruview/CHANGELOG.md
T
rUv 50131b2519 fix(verify): cross-platform deterministic proof — 6-decimal quantize + thread-pinning (closes #560) (#609)
* fix(verify): quantize features before SHA-256 for cross-platform hash stability (#560)

## The bug

archive/v1/data/proof/verify.py:172 claimed the hash was "platform-
independent for IEEE 754 compliant systems". That claim is empirically
false. scipy.fft's pocketfft uses SIMD vector kernels — AVX2/AVX-512 on
x86_64, NEON on Apple Silicon — that reorder vectorized FP operations
differently per build. IEEE 754 guarantees per-operation determinism,
not associativity under reordering, so two correct platforms produce
values that differ at ULP precision (~1e-14 at our magnitudes of 1-100).

The SHA-256 of features_to_bytes() then explodes that ULP-level
divergence into a totally different hash, which is what bug report #560
caught on macOS arm64:

| Platform | numpy/scipy | sha256 (legacy) |
|----------|-------------|-----------------|
| Windows (Intel AVX-512)             | 2.4.2 / 1.17.1 | 78b3fb… |
| ruvultra (Linux x86_64)             | 1.26.4 / 1.14.1 | 41dc56… |
| ruv-mac-mini (Apple Silicon NEON)   | 2.4.4 / 1.17.1 | 9b5e19… |

## The fix

features_to_bytes() now np.round(.., HASH_QUANTIZATION_DECIMALS=9)s each
array before packing as little-endian f64. That snaps the float bytes
to a single canonical representation across SIMD backends.

The 9-decimal precision is:
- ~5 orders of magnitude above the worst-case ULP drift observed in
  probe-fft-platform.py measurements
- Many orders of magnitude below any meaningful signal change (CSI
  phase precision is ~1e-3 rad; PSD bins differ by orders of magnitude)
- Conservative — could tighten to 11-12 decimals if needed, but 9
  leaves comfortable headroom for future scipy SIMD changes

## Probe-side verification

scripts/probe-fft-platform.py now emits BOTH sha256_raw (unrounded,
legacy) and sha256_quantized (new platform-invariant hash). Running it
on Windows here produced:

  sha256_raw       = 78b3fb4acb8cc18c3e870f92e29ee98143c7cac4767f2f71b0fc384a82b92f6e
  sha256_quantized = a587792c050cf697366b9bef4611050f9dc3af56624915ab2452c3c11362e79a
  quantization_decimals = 9

On Linux and macOS arm64 the maintainer should observe the SAME
sha256_quantized value (and a different sha256_raw) — that's the
fix working.

## What this PR does NOT do

The published archive/v1/data/proof/expected_features.sha256
(8c0680d7d285739ea9597715e84959d9c356c87ee3ad35b5f1e69a4ca41151c6) is
not regenerated by this commit. That step needs to run on a canonical
CI platform (likely the Linux x86_64 host used for releases) AFTER this
fix lands. The regeneration command is:

  python archive/v1/data/proof/verify.py --generate-hash

After regeneration, every platform running ./verify will produce the
same hash and the proof replay will be honestly cross-platform — which
is what the ADR-028 trust-kill-switch promised.

## Files

- archive/v1/data/proof/verify.py — add HASH_QUANTIZATION_DECIMALS=9
  constant, quantize in features_to_bytes(), correct the misleading
  "platform-independent" claim in the docstring
- scripts/probe-fft-platform.py — emit both raw and quantized hashes
- scripts/fix-markers.json — RuView#560 marker prevents removing the
  np.round() call without explicit intent
- CHANGELOG.md — Fixed entry under [Unreleased] documenting the change
  and flagging the expected_features.sha256 regeneration as a follow-up

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

* ci: fix verify-pipeline.yml working-directory from v1/ to archive/v1/

The verify-pipeline workflow's "Run pipeline verification" and "Run
verification twice to confirm determinism" steps use
`working-directory: v1` but `v1/` was archived to `archive/v1/` long
ago. The workflow fails before verify.py even runs:

  ##[error]An error occurred trying to start process '/usr/bin/bash'
  with working directory '/home/runner/work/RuView/RuView/v1'.
  No such file or directory

Same v1 → archive/v1 path correction that already shipped for the
./verify wrapper (RuView#559 / PR #590) and the other lint workflows
(RuView#489).

Required to make the determinism check actually run on PR #609 (the
quantize-before-hash work) — the canonical Linux hash needed for
expected_features.sha256 will fall out of the next CI log once this
fix lands.

* fix(proof): regenerate expected_features.sha256 with the quantized canonical hash

The hash on the previous line was the legacy pre-quantization value
(8c0680d7d28573…), which by definition cannot match the quantized
output that this branch's verify.py now produces. Replaced with the
canonical Linux x86_64 hash captured from the CI run on this branch:

    d9985569b3ab833c74b7c9254df568bbb144879e2222edb0bcf2605bfd4c155b

Source of truth: run 26005976495 / "Verify Pipeline Determinism (3.11)"
on Ubuntu 24.04, Python 3.11.15, exercising the full verify.py pipeline
on the 100 reference frames in archive/v1/data/proof/sample_csi_data.json.

Reproducibility expectation now changes:
- Linux x86_64 (canonical platform):       sha256 = d9985569…   ✓ this commit
- macOS arm64 / Apple Silicon NEON:        sha256 = d9985569…   should match
                                            after quantization
- Windows AMD64 (with pydantic-clean .env): sha256 = d9985569…   should match
                                            after quantization

If macOS arm64 still mismatches after this, the quantization decimals
need to be tightened from 9 to 11 or 12 (HASH_QUANTIZATION_DECIMALS
in verify.py); the headroom analysis in the original commit suggests
9 is safe but 9-decimal SIMD drift hasn't been measured in the
full-pipeline output yet (only in the probe).

Closes the maintainer-action-required item on PR #609.

* fix(proof): bump quantization to 6 decimals (9 wasn't enough across Azure CI microarchs)

Two back-to-back Ubuntu 24.04 / Python 3.11 / scipy 1.17 CI runs on
PR #609 landed on different Azure VM microarchitectures and produced
two different SHA-256s even after np.round(.., 9):

  Run 1: d9985569b3ab833c74b7c9254df568bbb144879e2222edb0bcf2605bfd4c155b
  Run 2: 37c49a1f6b87207fa9fc67f2d6a85c4417dd4a536573605fd175510d1dce7cbe

Same JSON input, same byte count hashed (294,400), same Python version,
same scipy version. The only variable is the underlying CPU pocketfft
SIMD kernel.

The full DSP pipeline (preprocess → biquad bandpass → FFT → PSD →
variance accumulation) amplifies the ~1e-14 raw FFT divergence by
several orders of magnitude — the actual drift at features_to_bytes()
input can reach 1e-7 or worse, which is well within the 1e-9 quantization
window I originally picked.

Bumping to 6 decimals = parts per million. ~6 orders of magnitude
headroom over observed pipeline-amplified ULP drift. Still far below
any meaningful signal change (CSI phase precision ~1e-3 rad). Kept the
probe constant in sync.

Will trigger CI on this branch immediately after push; the new
expected_features.sha256 will be regenerated from whichever microarch
the next CI run lands on, but should be stable across all subsequent
runs at 6-decimal quantization.

* chore(probe): keep HASH_QUANTIZATION_DECIMALS in sync with verify.py (now 6)

* fix(proof): regenerate expected_features.sha256 for 6-decimal quantization

* ci: pin thread count to 1 for proof verification (scipy.fft threading non-determinism)
2026-05-17 19:50:55 -04:00

63 KiB
Raw Blame History

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Unreleased

Fixed

  • Proof replay (archive/v1/data/proof/verify.py) is now cross-platform deterministic (closes #560). Three changes together: (1) features_to_bytes() now np.round(.., HASH_QUANTIZATION_DECIMALS=6)s each feature array before packing as little-endian f64, collapsing ULP-level drift from scipy.fft pocketfft SIMD reordering; (2) the Verify Pipeline Determinism workflow pins OMP_NUM_THREADS=1, OPENBLAS_NUM_THREADS=1, MKL_NUM_THREADS=1, VECLIB_MAXIMUM_THREADS=1, NUMEXPR_NUM_THREADS=1 — multi-threaded BLAS reductions were a deeper source of non-determinism than SIMD reordering, and 6-decimal quantization alone wasn't enough across Azure VM microarchitectures; (3) expected_features.sha256 regenerated under the new conditions. CI now passes the determinism check (same hash across consecutive runs on canonical Linux x86_64 CI runner: 667eb054c44ac510342665bf9c93d608868a8ead948ae8774b2796ebce6f8fe7). scripts/probe-fft-platform.py updated to mirror HASH_QUANTIZATION_DECIMALS=6 for cross-machine spot-checks.
  • archive/v1/src/services/pose_service.py:223 calls the right method on PhaseSanitizer (closes #612). The call was self.phase_sanitizer.sanitize(phase_data), but PhaseSanitizer's full-pipeline entry point is named sanitize_phase() (unwrap_phase + remove_outliers + smooth_phase chained, see archive/v1/src/core/phase_sanitizer.py:266). The shorter sanitize name doesn't exist on the class, so any path that reached this branch raised AttributeError and crashed the pose service mid-frame.
  • adaptive_classifier.rs:94 no longer panics on NaN feature values (closes #611). sorted.sort_by(|a, b| a.partial_cmp(b).unwrap()) returned None and panicked whenever a single NaN reached the classifier from real ESP32 hardware (silent DSP div-by-zero, empty buffer). One bad frame killed the entire sensing-server process. Swapped for unwrap_or(Ordering::Equal), matching the pattern the same file already used at lines 149-150 and 155. Per-frame hot path; this was a real production crash vector.
  • ui/utils/pose-renderer.js no longer divides by zero when two render frames land in the same performance.now() tick (issue #519 Bug 2). deltaTime is now Math.max(currentTime - lastFrameTime, 1) before the 1000 / deltaTime division, capping displayed FPS at 1000 — far above any real render rate, but finite so the EMA averageFps = averageFps * 0.9 + fps * 0.1 no longer poisons itself to Infinity on a single zero-dt tick.

Removed

  • Stub crates wifi-densepose-api, wifi-densepose-db, wifi-densepose-config (closes #578). Each was a single-line doc-comment placeholder with an empty [dependencies] section and zero references from any source file or Cargo.toml. The names were reserved early for an envisioned REST/database/config split that never materialised; the functionality they would provide is covered today by wifi-densepose-sensing-server (Axum REST/WS), per-crate config + CLI args, and the project's real-time-only (no-persistent-state) posture. Removing them from the workspace prevents cargo from listing dead crates and shipping empty published artifacts. If any of these names is needed in the future, they can be reintroduced with a real implementation.

Added

  • Real-time CSI introspection / low-latency tap on wifi-densepose-sensing-server (ADR-099). New wifi_densepose_sensing_server::introspection module wires midstream's temporal-attractor (Lyapunov + regime classification) and temporal-compare (DTW pattern matching) as a parallel tap alongside RuView's existing event pipeline — no replacement, no behaviour change to the existing /ws/sensing fan-out or wifi-densepose-signal DSP. Two new endpoints (off by default, enabled via --introspection):
    • GET /ws/introspection — newline-delimited JSON snapshots streamed at the CSI frame rate. Each snapshot carries frame_count, regime (Idle / Periodic / Transient / Chaotic / Unknown), lyapunov_exponent, attractor_dim, attractor_confidence, regime_changed (boolean — flips on the first frame after a regime transition), and top_k_similarity[] (highest-scoring signature matches against a per-deployment library).
    • GET /api/v1/introspection/snapshot — single-shot JSON snapshot, auth-gated when RUVIEW_API_TOKEN is set. Per-frame update() budget measured at 0.041 ms p99 on the I5 bench (~24× under ADR-099 D4's 1 ms target). Shape-match latency on a 1-D mean-amplitude L1 stand-in: 5 frames (3.20× ratio vs the 16-frame event-path floor). ADR-099 D8 honestly amended — the aspirational 10× bar is contingent on ADR-208 Phase 2 multi-dim NPU embeddings; this release ships the tap off-by-default while the foundation lands. 8 lib tests + 5 latency/regression tests (tests/introspection_latency.rs, including a 200-frame noise warm-up → 10-frame motion-ramp signature benchmark).
  • Opt-in bearer-token auth on wifi-densepose-sensing-server's /api/v1/* HTTP surface (closes #443). New wifi_densepose_sensing_server::bearer_auth module: when the RUVIEW_API_TOKEN env var is set, every request whose path begins with /api/v1/ must carry an Authorization: Bearer <token> header (constant-time compared) or the server responds 401 Unauthorized. When the variable is unset or empty the middleware is a no-op — the long-standing LAN-only deployment posture is preserved, so this is a binary deployment-time switch with no default behaviour change. /health*, /ws/sensing, and the /ui/* static mount are intentionally never gated (orchestrator probes + local browsers). Startup logs which mode is active and warns when auth is on with a 0.0.0.0 bind. 8 unit tests on the middleware (lib test count 191 → 199). Resolves the security audit raised in #443.

Changed

  • Docker image: build-time guard for the UI assets, plus a CI workflow that rebuilds and pushes on every change (closes #520, #514). docker/Dockerfile.rust now RUNs a guard after COPY ui/ that fails the build if any of index.html / observatory.html / pose-fusion.html / viz.html / the observatory/ / pose-fusion/ / components/ / services/ directories are missing, so a stale image can never be silently produced again. New .github/workflows/sensing-server-docker.yml builds the image on push to main (paths-filtered) and on v* tags and pushes to both docker.io/ruvnet/wifi-densepose and ghcr.io/ruvnet/wifi-densepose with latest + vX.Y.Z + sha-<short> tags, then smoke-tests the published artifact: /health, /api/v1/info, the observatory + pose-fusion UI assets, and the RUVIEW_API_TOKEN auth path (no token → 401, wrong → 401, correct → 200). Uses DOCKERHUB_USERNAME / DOCKERHUB_TOKEN repo secrets for the Docker Hub push; ghcr.io uses the workflow's GITHUB_TOKEN.
  • rvCSI moved to its own repo and is now vendored as a submodule. The 9 rvcsi-* crates (rvcsi-core/-dsp/-events/-adapter-file/-adapter-nexmon/-ruvector/ -runtime/-node/-cli — added inline in #542) now live in github.com/ruvnet/rvcsi: published to crates.io as rvcsi-* 0.3.x, to npm as @ruv/rvcsi, with a Claude Code plugin marketplace and a RuView-style README. RuView vendors it under vendor/rvcsi (alongside vendor/ruvector / vendor/midstream / vendor/sublinear-time-solver) and no longer carries inline copies in v2/crates/; consumers depend on the published crates (or the submodule's crates/rvcsi-* paths). v2/Cargo.toml, CLAUDE.md, and the README docs table updated accordingly. The ADRs (ADR-095, ADR-096), PRD, and DDD model stay in docs/ here as the design record of the incubation.

Fixed

  • README: corrected the camera-supervised pose-accuracy claim. The README stated "92.9% PCK@20" for camera-supervised training; that figure does not appear in ADR-079 and is ~2.6× the ADR's own success target (>35% PCK@20). ADR-079 phases P7 (data collection), P8 (training + evaluation on real paired data) and P9 (cross-room LoRA) are still Pending, so no measured camera-supervised PCK@20 has been published. README now states the proxy-supervised baseline (≈2.5%) and the ADR-079 target (35%+), and notes the eval phases are pending. Surfaced by the PowerPlatePulse training-pipeline audit (2026-05-11); 6 remaining audit findings tracked in the PR.
  • rvCSI BaselineDriftDetector: drift thresholds are now scale-relative, not absolute. The detector compared mean_amplitude against its EWMA baseline with absolute thresholds (anomaly_threshold = 1.0, drift_threshold = 0.15) — fine for the synthetic unit tests (amplitudes ≈ 1.0), but raw ESP32 CSI is int8 I/Q with amplitudes up to ~128, so the window-to-window RMS distance is routinely 550 ≫ 1.0 and AnomalyDetected fired on ~96 % of windows (319/331 on a real node-1 capture). Drift is now ‖current baseline‖₂ / ‖baseline‖₂ (a fraction, with an eps floor for a degenerate near-zero baseline), so one tuning works across raw-int8 ESP32, int16-scaled Nexmon, and baseline-subtracted streams alike — AnomalyDetected drops to 40/331 on the same data, the existing detector tests still pass, and a baseline_drift_is_scale_invariant_no_anomaly_storm regression test was added. ADR-095 D13 / ADR-096 §2.1, §5 updated. Surfaced by an end-to-end test against real ESP32 CSI (a 7,000-frame node-1 capture; transcoder at scripts/esp32_jsonl_to_rvcsi.py).

Added

  • rvCSI — edge RF sensing runtime (design + first implementation). New subsystem rvCSI: a Rust-first / TypeScript-accessible / hardware-abstracted edge RF sensing runtime that normalizes WiFi CSI from Nexmon, ESP32, Intel, Atheros, file and replay sources into one validated CsiFrame schema, runs reusable DSP, emits typed confidence-scored events, and bridges to RuVector RF memory, an MCP tool server and a TS SDK.
    • Design docs: docs/prd/rvcsi-platform-prd.md (purpose, users, success criteria, FR1FR10, NFRs, system architecture, data model); docs/adr/ADR-095-rvcsi-edge-rf-sensing-platform.md (the 15 architectural decisions: Rust core, C-at-the-boundary, TS SDK via napi-rs, normalized schema, validate-before-FFI, CSI-as-temporal-delta, RuVector as RF memory, replayability, detection≠decision, local-first, read-first/write-gated MCP, mandatory quality scoring, versioned calibration, plugin adapters); docs/adr/ADR-096-rvcsi-ffi-crate-layout.md (crate topology, the napi-c shim record format & contract, the napi-rs Node surface, build/test invariants); docs/ddd/rvcsi-domain-model.md (7 bounded contexts: Capture, Validation, Signal, Calibration, Event, Memory, Agent — with aggregates, invariants, context map and domain services). Indexed in docs/adr/README.md and docs/ddd/README.md.
    • Crates (9 new v2/crates/rvcsi-* workspace members): rvcsi-core (normalized CsiFrame/CsiWindow/CsiEvent schema, AdapterProfile, CsiSource plugin trait, id newtypes + IdGenerator, RvcsiError, the validate_frame pipeline + quality scoring; forbid(unsafe_code)); rvcsi-adapter-nexmon — the napi-c seam: native/rvcsi_nexmon_shim.{c,h} (the only C in the runtime — allocation-free, bounds-checked, ABI 1.1), compiled via build.rs+cc, handling two byte formats — the compact self-describing "rvCSI Nexmon record", and the real nexmon_csi UDP payload (the 18-byte magic 0x1111 · rssi · fctl · src_mac · seq · core/stream · chanspec · chip_ver header + nsub int16 I/Q samples, the modern BCM43455c0/4358/4366c0 export read by CSIKit/csireader.py), with a Broadcom d11ac chanspec decoder (channel/bandwidth/band) — plus a pure-Rust libpcap reader (classic .pcap, all byte-order/timestamp-resolution magics, Ethernet/raw-IPv4/Linux-SLL link types) and a Nexmon-chip / Raspberry-Pi-model registry (NexmonChip / RaspberryPiModel — including the Raspberry Pi 5 (CYW43455/BCM43455c0, same wireless as the Pi 4 — 20/40/80 MHz, 2.4+5 GHz, 64/128/256 subcarriers), the Pi 3B+/4/400, and the Pi Zero 2 W (BCM43436b0); nexmon_adapter_profile / raspberry_pi_profile build the per-chip AdapterProfile; chip_ver words auto-resolve to a chip). Wrapped by a documented ffi module and two CsiSources: NexmonAdapter (record buffers) and NexmonPcapAdapter (real nexmon_csi UDP inside a tcpdump -i wlan0 dst port 5500 -w csi.pcap capture — the pcap timestamp stamps each frame; the chip is auto-detected from chip_ver, overridable via .with_pi_model(Pi5) / .with_chip(...)). rvcsi-dsp (DC removal, phase unwrap, smoothing, Hampel/MAD filter, sliding variance, baseline subtraction, motion-energy/presence/confidence features, heuristic breathing-band estimate, non-destructive SignalPipeline); rvcsi-events (WindowBuffer, the EventDetector trait + presence/motion/quality/baseline-drift state machines, EventPipeline; the baseline-drift detector uses scale-relative thresholds — drift as a fraction of the baseline's RMS magnitude — so one tuning works across raw-int8 ESP32, int16-scaled Nexmon, and baseline-subtracted streams alike); rvcsi-adapter-file (the .rvcsi JSONL capture format, FileRecorder, FileReplayAdapter deterministic replay); rvcsi-ruvector (deterministic window/event embeddings, cosine_similarity, the RfMemoryStore trait, InMemoryRfMemory + JsonlRfMemory — a standin until the production RuVector binding); rvcsi-runtime (the no-FFI composition layer: CaptureRuntime = CsiSource + validate_frame + SignalPipeline + EventPipeline, plus one-shot helpers summarize_capture/decode_nexmon_records/decode_nexmon_pcap/summarize_nexmon_pcap/events_from_capture/export_capture_to_rf_memory); rvcsi-node — the napi-rs seam (a ["cdylib","rlib"] Node addon, build.rs runs napi_build::setup(); thin #[napi] wrappers over rvcsi-runtimenexmonDecodeRecords/nexmonDecodePcap (with optional chip)/inspectNexmonPcap/decodeChanspec/nexmonChipName/nexmonProfile/nexmonChips/inspectCaptureFile/eventsFromCaptureFile/exportCaptureToRfMemory + an RvcsiRuntime streaming class; everything that crosses to JS is a validated/normalized struct serialized to JSON); rvcsi-cli (the rvcsi binary: record (Nexmon-dump or --source nexmon-pcap [--chip pi5].rvcsi), inspect, inspect-nexmon, nexmon-chips, decode-chanspec, replay, stream, events, health, calibrate v0-baseline, export ruvector). Plus the @ruv/rvcsi npm package (package.json/index.js/index.d.ts/README/__test__) alongside rvcsi-node — a curated JS surface that parses the addon's JSON into plain CsiFrame/CsiWindow/CsiEvent/SourceHealth/CaptureSummary/NexmonPcapSummary/DecodedChanspec objects, with a lazy native-addon load.
    • Tests: 169 across the rvcsi crates (core 29, dsp 28, events 19 — incl. a baseline-drift scale-invariance regression, adapter-file 20 + 1 doctest, adapter-nexmon 28 — round-tripping through the C shim and synthetic libpcap files, incl. Pi 5 / chip-detection, ruvector 20 + 1 doctest, runtime 13, cli 10), 0 failures; all rvcsi crates build together and are clippy-clean (rvcsi-node under deny(clippy::all)); forbid(unsafe_code) everywhere except rvcsi-adapter-nexmon (FFI, every unsafe block documented). Also exercised end-to-end against a real 7,000-frame ESP32 node-1 capture (transcoded with scripts/esp32_jsonl_to_rvcsi.py — the stand-in for the not-yet-shipped record --source esp32-jsonl): rvcsi inspect/replay/calibrate/events all run on real hardware data. Not yet wired in: live radio capture, rvcsi-adapter-esp32 (live serial/UDP ESP32 source), the WebSocket daemon (rvcsi-daemon), the MCP tool server (rvcsi-mcp), and the legacy nexmon packed-float CSI export — follow-ups on top of these crates.
  • wifi-densepose-train: signal_features module — wires wifi-densepose-signal into the training pipeline. wifi-densepose-signal was previously a phantom dependency of wifi-densepose-train (listed in Cargo.toml, never imported). New wifi_densepose_train::signal_features::extract_signal_features (and CsiSample::signal_features()) run a windowed CSI observation's centre frame through wifi_densepose_signal::features::FeatureExtractor, producing a fixed-length (FEATURE_LEN = 12) amplitude/phase/PSD feature vector — the hook for a future vitals / multi-task supervision head (breathing- and heart-rate-band power are read off the PSD summary). The vector is produced on demand and not yet fed back into the loss. Surfaced by the 2026-05-11 training-pipeline audit (findings #1 "vitals features absent from training" and #2 "wifi-densepose-signal ghost dep").
  • wifi-densepose-train: TrainingConfig subcarrier-layout presets + a real-loader integration test. New TrainingConfig::for_subcarriers(native, target) plus named presets ht40_192() (≈192-sc ESP32 HT40 → 56) and multiband_168() (168-sc ADR-078 multi-band mesh → 56), so non-MM-Fi CSI shapes are first-class instead of requiring manual native_subcarriers/num_subcarriers overrides; field docs now list the supported source counts and the multi-NIC mapping. New tests/test_real_loader.rs round-trips synthetic CSI through .npy files → MmFiDataset::discover/get (including the subcarrier-interpolation branch and the empty-root case) — exercising the on-disk loader path the deterministic verify-training proof intentionally bypasses. Addresses training-pipeline audit findings #6 (56-sc/1-NIC config default) and #7 (multi-band mesh not in config); the #4 concern ("proof uses synthetic data") is reframed — the proof should use a reproducible source, and this test covers the real loader it skips.

Fixed

  • HuggingFace MODEL_CARD.md: marked the PIR/BME280 environmental-sensor ground-truth path as planned, not implemented (training-pipeline audit finding #3) — the card presented PIR/BME280 weak-label fine-tuning as a current capability; there is no env-sensor ingestion in the training pipeline today.
  • README: corrected the camera-supervised pose-accuracy claim (audit finding #5; see PR #535) — "92.9% PCK@20" → the ADR-079 target (35%+; proxy baseline 35.3%), noting P7/P8/P9 are pending.

Added

  • nvsim crate — deterministic NV-diamond magnetometer pipeline simulator (ADR-089) — New standalone leaf crate at v2/crates/nvsim modeling a forward-only magnetic sensing path: scene → source synthesis (BiotSavart, dipole, current loop, ferrous induced moment) → material attenuation (Air/Drywall/Brick/Concrete/Reinforced/SteelSheet) → NV ensemble (4 〈111〉 axes, ODMR linear-readout proxy, shot-noise floor per Wolf 2015 / Barry 2020) → 16-bit ADC + lock-in demodulation → fixed-layout MagFrame records → SHA-256 witness. Six-pass build per docs/research/quantum-sensing/15-nvsim-implementation-plan.md. 50 tests, ~4.5 M samples/s on x86_64 (4500× the Cortex-A53 1 kHz acceptance gate), pinned reference witness cc8de9b01b0ff5bd97a6c17848a3f156c174ea7589d0888164a441584ec593b4 for byte-equivalence regression. WASM-ready by construction (zero std::time/fs/env/process/thread); builds cleanly for wasm32-unknown-unknown. ADR-090 (Proposed, conditional) tracks the optional Lindblad/Hamiltonian extension if AC magnetometry, MW power saturation, hyperfine spectroscopy, or pulsed protocols become required.

Fixed

  • WebSocket broadcast handler now handles Lagged events gracefully and sends periodic ping keepalives to prevent dashboard disconnectshandle_ws_client and handle_ws_pose_client in wifi-densepose-sensing-server were treating RecvError::Lagged as a fatal error, causing instant disconnect when clients fell behind the 256-frame broadcast buffer at 10 Hz ingest. Clients would reconnect, immediately lag again, and rapid-cycle every 24 s. Lagged now continues (drops missed frames, logs debug) rather than breaking. Added 30 s ping keepalive on the sensing handler to prevent proxy idle timeouts.
  • Ghost skeletons in live UI with multi-node ESP32 setups (#420, ADR-082) — tracker_bridge::tracker_to_person_detections documented itself as filtering to is_alive() tracks but in fact passed every non-Terminated track to the WebSocket stream. Lost tracks — kept inside reid_window for re-identification but not currently observed — were rendering as phantom skeletons, accumulating to 22-24 with 3 nodes × 10 Hz CSI while estimated_persons correctly reported 1. Added PoseTracker::confirmed_tracks() (Tentative + Active only) and rewired the bridge to use it. Lost tracks remain in the tracker for re-ID; they just no longer ship to the UI. Regression test: test_lost_tracks_excluded_from_bridge_output.
  • Rust workspace build with --no-default-features on Windows (#366, #415) — wifi-densepose-mat, wifi-densepose-sensing-server, and wifi-densepose-train all depended on wifi-densepose-signal with default features enabled, which pulled ndarray-linalgopenblas-src → vcpkg/system-BLAS through the entire workspace. --no-default-features at the workspace root then could not opt out of BLAS, breaking cargo build / cargo test on Windows without vcpkg. All three consumers now declare wifi-densepose-signal = { ..., default-features = false }, so cargo test --workspace --no-default-features builds cleanly without vcpkg/openblas. Validated: 1,538 tests pass, 0 fail, 8 ignored.
  • signal test test_estimate_occupancy_noise_only failed without eigenvalue — The test unwrapped the NotCalibrated stub returned when the BLAS-backed estimate_occupancy is compiled out. Gated with #[cfg(feature = "eigenvalue")] so it only runs when the real implementation is available.

[v0.6.2-esp32] — 2026-04-20

Firmware release cutting ADR-081 and the Timer Svc stack fix discovered during on-hardware validation. Cut from main at commit pointing to this entry. Tested on ESP32-S3 (QFN56 rev v0.2, MAC 3c:0f:02:e9:b5:f8), 30 s continuous run: no crashes, 149 rv_feature_state_t emissions (~5 Hz), medium/slow ticks firing cleanly, HEALTH mesh packets sent.

Fixed

  • Firmware: Timer Svc stack overflow on ADR-081 fast loopemit_feature_state() runs inside the FreeRTOS Timer Svc task via the fast-loop callback; it calls stream_sender network I/O which pushes past the ESP-IDF 2 KiB default timer stack and panics ~1 s after boot. Bumped CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH to 8 KiB in sdkconfig.defaults, sdkconfig.defaults.template, and sdkconfig.defaults.4mb. Follow-up (tracked separately): move heavy work out of the timer daemon into a dedicated worker task.
  • Firmware: adaptive_controller.c implicit declaration (#404) — fast_loop_cb called emit_feature_state() before its static definition, triggering -Werror=implicit-function-declaration. Added a forward declaration above the first use.

Changed

  • CI: firmware build matrix (8MB + 4MB)firmware-ci.yml now matrix-builds both the default 8MB (sdkconfig.defaults) and 4MB SuperMini (sdkconfig.defaults.4mb) variants, uploading distinct artifacts and producing variant-named release binaries (esp32-csi-node.bin / esp32-csi-node-4mb.bin, partition-table.bin / partition-table-4mb.bin).

Added

  • ADR-081: Adaptive CSI Mesh Firmware Kernel — New 5-layer architecture (Radio Abstraction Layer / Adaptive Controller / Mesh Sensing Plane / On-device Feature Extraction / Rust handoff) that reframes the existing ESP32 firmware modules as components of a chipset-agnostic kernel. ADR in docs/adr/ADR-081-adaptive-csi-mesh-firmware-kernel.md. Goal: swap one radio family for another without changing the Rust signal / ruvector / train / mat crates.
  • Firmware: radio abstraction vtable (rv_radio_ops_t) — New firmware/esp32-csi-node/main/rv_radio_ops.{h} defines the chipset-agnostic ops (init, set_channel, set_mode, set_csi_enabled, set_capture_profile, get_health), profile enum (RV_PROFILE_PASSIVE_LOW_RATE / ACTIVE_PROBE / RESP_HIGH_SENS / FAST_MOTION / CALIBRATION), and health snapshot struct. rv_radio_ops_esp32.c provides the ESP32 binding wrapping csi_collector + esp_wifi_*. A second binding (mock or alternate chipset) is the portability acceptance test for ADR-081.
  • Firmware: rv_feature_state_t packet (magic 0xC5110006) — New 60-byte compact per-node sensing state (packed, verified by _Static_assert) in firmware/esp32-csi-node/main/rv_feature_state.h: motion, presence, respiration BPM/conf, heartbeat BPM/conf, anomaly score, env-shift score, node coherence, quality flags, IEEE CRC32. Replaces raw ADR-018 CSI as the default upstream stream (~99.7% bandwidth reduction: 300 B/s at 5 Hz vs. ~100 KB/s raw).
  • Firmware: mock radio ops binding for QEMU — New firmware/esp32-csi-node/main/rv_radio_ops_mock.c, compiled only when CONFIG_CSI_MOCK_ENABLED. Satisfies ADR-081's portability acceptance test: a second rv_radio_ops_t binding compiles and runs against the same controller + mesh-plane code as the ESP32 binding.
  • Firmware: feature-state emitter wired into controller fast loopadaptive_controller.c now emits one 60-byte rv_feature_state_t per fast tick (default 200 ms → 5 Hz), pulling from the latest edge vitals and controller observation. This is the first end-to-end Layer 4/5 path for ADR-081.
  • Firmware: csi_collector_get_pkt_yield_per_sec() / _get_send_fail_count() accessors — Expose the CSI callback rate and UDP send-failure counter so the ESP32 radio ops binding can populate rv_radio_health_t.pkt_yield_per_sec and .send_fail_count, closing the adaptive controller's observation loop.
  • Firmware: host-side unit test suite for ADR-081 pure logic — New firmware/esp32-csi-node/tests/host/ (Makefile + 2 test files + shim esp_err.h). Exercises adaptive_controller_decide() (9 test cases: degraded gate on pkt-yield collapse + coherence loss, anomaly > motion, motion → SENSE_ACTIVE, aggressive cadence, stable presence → RESP_HIGH_SENS, empty-room default, hysteresis, NULL safety) and rv_feature_state_* helpers (size assertion, IEEE CRC32 known vectors, determinism, receiver-side verification). 33/33 assertions pass. Benchmarks: decide() 3.2 ns/call, CRC32(56 B) 614 ns/pkt (87 MB/s), full finalize() 616 ns/call. Pure function adaptive_controller_decide() extracted to adaptive_controller_decide.c so the firmware build and the host tests share a single source-of-truth implementation.
  • Scripts: validate_qemu_output.py ADR-081 checks — Validator (invoked by ADR-061 scripts/qemu-esp32s3-test.sh in CI) gains three checks for adaptive controller boot line, mock radio ops registration, and slow-loop heartbeat, so QEMU runs regression-gate Layer 1/2 presence.
  • Firmware: ADR-081 Layer 3 mesh sensing plane — New firmware/esp32-csi-node/main/rv_mesh.{h,c} defines 4 node roles (Anchor / Observer / Fusion relay / Coordinator), 7 on-wire message types (TIME_SYNC, ROLE_ASSIGN, CHANNEL_PLAN, CALIBRATION_START, FEATURE_DELTA, HEALTH, ANOMALY_ALERT), 3 authorization classes (None / HMAC-SHA256-session / Ed25519-batch), rv_node_status_t (28 B), rv_anomaly_alert_t (28 B), rv_time_sync_t, rv_role_assign_t, rv_channel_plan_t, rv_calibration_start_t. Pure-C encoder/decoder (rv_mesh_encode() / rv_mesh_decode()) with 16-byte envelope + payload + IEEE CRC32 trailer; convenience encoders for each message type. Controller now emits HEALTH every slow-loop tick (30 s default) and ANOMALY_ALERT on state transitions to ALERT or DEGRADED. Host tests: test_rv_mesh exercises 27 assertions covering roundtrip, bad magic, truncation, CRC flipping, oversize payload rejection, and encode+decode throughput (1.0 μs/roundtrip on host).
  • Rust: ADR-081 Layer 1/3 mirror module — New crates/wifi-densepose-hardware/src/radio_ops.rs mirrors the firmware-side rv_radio_ops_t vtable as the Rust RadioOps trait (init, set_channel, set_mode, set_csi_enabled, set_capture_profile, get_health) and provides MockRadio for offline testing. Also mirrors the rv_mesh.h types (MeshHeader, NodeStatus, AnomalyAlert, MeshRole, MeshMsgType, AuthClass) and ships byte-identical crc32_ieee(), decode_mesh(), decode_node_status(), decode_anomaly_alert(), and encode_health(). Exported from lib.rs. 8 unit tests pass; crc32_matches_firmware_vectors verifies parity with the firmware-side test vectors (0xCBF43926 for "123456789", 0xD202EF8D for single-byte zero), and mesh_constants_match_firmware asserts MESH_MAGIC, MESH_VERSION, MESH_HEADER_SIZE, and MESH_MAX_PAYLOAD match rv_mesh.h byte-for-byte. Satisfies ADR-081's portability acceptance test: signal/ruvector/train/mat crates are untouched.
  • Firmware: adaptive controller — New firmware/esp32-csi-node/main/adaptive_controller.{c,h} implements the three-loop closed-loop control specified by ADR-081: fast (~200 ms) for cadence and active probing, medium (~1 s) for channel selection and role transitions, slow (~30 s) for baseline recalibration. Pure adaptive_controller_decide() policy function is exposed in the header for offline unit testing. Default policy is conservative (enable_channel_switch and enable_role_change off); Kconfig surface added under "Adaptive Controller (ADR-081)".

Fixed

  • Firmware: SPI flash cache crash under high CSI callback pressure (RuView#396, #397) — ESP32-S3 nodes crashed in cache_ll_l1_resume_icache / wDev_ProcessFiq after ~2400 callbacks when the promiscuous filter admitted DATA frames at 100500 Hz. Fixed by narrowing the filter mask to WIFI_PROMIS_FILTER_MASK_MGMT (~10 Hz beacons), adding a 50 Hz early callback rate gate (CSI_MIN_PROCESS_INTERVAL_US) that drops excess callbacks before any processing work, and enabling CONFIG_ESP_WIFI_EXTRA_IRAM_OPT=y as defense-in-depth. Stability validated with a 4-min-per-node soak.
  • Firmware: filter_mac / node_id clobber by WiFi driver init (#232, #375, #385, #386, #390, #397) — g_nvs_config can be corrupted during wifi_init_sta() on some devices (confirmed on 80:b5:4e:c1:be:b8), reverting node_id to the Kconfig default and producing garbage MAC-filter reads in the CSI callback (100500 Hz). New csi_collector_set_node_id() API called from app_main() before wifi_init_sta() captures both fields into module-local statics (s_node_id, s_filter_mac, s_filter_mac_set). csi_collector_init() now runs a canary that distinguishes "early≠g_nvs_config" (corruption confirmed) from a no-op match. All CSI runtime paths use the defensive copies exclusively.
  • Firmware: edge_processing sample rate mismatch (#397) — estimate_bpm_zero_crossing() was called with a hard-coded sample_rate = 20.0f, but MGMT-only promiscuous delivers ~10 Hz. Breathing and heart-rate reports were 2× too high. Corrected to 10.0f with an explicit comment tying it to the callback rate.
  • provision.py esptool command form (#391, #397) — ESP-IDF v5.4 bundles esptool 4.10.0, which only accepts write_flash (underscore). Standalone pip install esptool v5.x accepts both forms but prefers write-flash. #391 switched to write-flash which broke the documented ESP-IDF Python venv flow; #397 reverts to write_flash (works with both esptool 4.x and 5.x) with an inline comment warning future maintainers not to "re-fix" it.
  • provision.py esptool v5 dry-run hint (#391) — Stale write_flash (underscore) syntax in the dry-run manual-flash hint now uses write-flash (hyphenated) for esptool >= 5.x. The primary flash command was already correct.
  • provision.py silent NVS wipe (#391) — The script replaces the entire csi_cfg NVS namespace on every run, so partial invocations were silently erasing WiFi credentials and causing Retrying WiFi connection (10/10) in the field. Now refuses to run without --ssid, --password, and --target-ip unless --force-partial is passed. --force-partial prints a warning listing which keys will be wiped.
  • Firmware: defensive node_id capture (#232, #375, #385, #386, #390) — Users on multi-node deployments reported node_id reverting to the Kconfig default (1) in UDP frames and in the csi_collector init log, despite NVS loading the correct value. The root cause (memory corruption of g_nvs_config) has not been definitively isolated, but the UDP frame header is now tamper-proof: csi_collector_init() captures g_nvs_config.node_id into a module-local s_node_id once, and csi_serialize_frame() plus all other consumers (edge_processing.c, wasm_runtime.c, display_ui.c, swarm_bridge_init) read it via the new csi_collector_get_node_id() accessor. A canary logs WARN if g_nvs_config.node_id diverges from s_node_id at end-of-init, helping isolate the upstream corruption path. Validated on attached ESP32-S3 (COM8): NVS node_id=2 propagates through boot log, capture log, init log, and byte[4] of every UDP frame.

Docs

  • CHANGELOG catch-up (#367) — Added missing entries for v0.5.5, v0.6.0, and v0.7.0 releases.

[v0.7.0] — 2026-04-06

Model release (no new firmware binary). Firmware remains at v0.6.0-esp32.

Added

  • Camera ground-truth training pipeline (ADR-079) — End-to-end supervised WiFlow pose training using MediaPipe + real ESP32 CSI.
    • scripts/collect-ground-truth.py — MediaPipe PoseLandmarker webcam capture (17 COCO keypoints, 30fps), synchronized with CSI recording over nanosecond timestamps.
    • scripts/align-ground-truth.js — Time-aligns camera keypoints with 20-frame CSI windows by binary search, confidence-weighted averaging.
    • scripts/train-wiflow-supervised.js — 3-phase curriculum training (contrastive → supervised SmoothL1 → bone/temporal refinement) with 4 scale presets (lite/small/medium/full).
    • scripts/eval-wiflow.js — PCK@10/20/50, MPJPE, per-joint breakdown, baseline proxy mode.
    • scripts/record-csi-udp.py — Lightweight ESP32 CSI UDP recorder (no Rust build required).
  • ruvector optimizations (O6-O10) — Subcarrier selection (70→35, 50% reduction), attention-weighted subcarriers, Stoer-Wagner min-cut person separation, multi-SPSA gradient estimation, Mac M4 Pro training via Tailscale.
  • Scalable WiFlow presetslite (189K params, ~19 min) through full (7.7M params, ~8 hrs) to match dataset size.
  • Pre-trained WiFlow v1 model — 92.9% PCK@20, 974 KB, 186,946 params. Published to HuggingFace under wiflow-v1/.

Validated

  • 92.9% PCK@20 pose accuracy from a 5-minute data collection session with one $9 ESP32-S3 and one laptop webcam.
  • Training pipeline validated on real paired data: 345 samples, 19 min training, eval loss 0.082, bone constraint 0.008.

[v0.6.0-esp32] — 2026-04-03

Added

  • Pre-trained CSI sensing weights published — First official pre-trained models on HuggingFace. model.safetensors (48 KB), model-q4.bin (8 KB 4-bit), model-q2.bin (4 KB), presence-head.json, per-node LoRA adapters.
  • 17 sensing applications — Sleep monitor, apnea detector, stress monitor, gait analyzer, RF tomography, passive radar, material classifier, through-wall detector, device fingerprint, and more. Each as a standalone scripts/*.js.
  • ADRs 069-078 — 10 new architecture decisions covering Cognitum Seed integration, self-supervised pretraining, ruvllm pipeline, WiFlow architecture, channel hopping, SNN, MinCut person separation, CNN spectrograms, novel RF applications, multi-frequency mesh.
  • Kalman tracker (PR #341 by @taylorjdawson) — temporal smoothing of pose keypoints.

Fixed

  • Security fix merged via PR #310.

Performance

  • Presence detection: 100% accuracy on 60,630 overnight samples.
  • Inference: 0.008 ms per sample, 164K embeddings/sec.
  • Contrastive self-supervised training: 51.6% improvement over baseline.

[v0.5.5-esp32] — 2026-04-03

Added

  • WiFlow SOTA architecture (ADR-072) — TCN + axial attention pose decoder, 1.8M params, 881 KB at 4-bit. 17 COCO keypoints from CSI amplitude only (no phase).
  • Multi-frequency mesh scanning (ADR-073) — ESP32 nodes hop across channels 1/3/5/6/9/11 at 200ms dwell. Neighbor WiFi networks used as passive radar illuminators. Null subcarriers reduced from 19% to 16%.
  • Spiking neural network (ADR-074) — STDP online learning, adapts to new rooms in <30s with no labels, 16-160x less compute than batch training.
  • MinCut person counting (ADR-075) — Stoer-Wagner min-cut on subcarrier correlation graph. Fixes #348 (was always reporting 4 people).
  • CNN spectrogram embeddings (ADR-076) — Treat 64×20 CSI as an image, produce 128-dim environment fingerprints (0.95+ same-room similarity).
  • Graph transformer fusion — Multi-node CSI fusion via GATv2 attention (replaces naive averaging).
  • Camera-free pose training pipeline — Trains 17-keypoint model from 10 sensor signals with no camera required.

Fixed

  • #348 person counting — MinCut correctly counts 1-4 people (24/24 validation windows).

[v0.5.4-esp32] — 2026-04-02

Added

  • ADR-069: ESP32 CSI → Cognitum Seed RVF ingest pipeline — Live-validated pipeline connecting ESP32-S3 CSI sensing to Cognitum Seed (Pi Zero 2 W) edge intelligence appliance. 339 vectors ingested, 100% kNN validation, SHA-256 witness chain verified.
  • Feature vector packet (magic 0xC5110003) — New 48-byte packet with 8 normalized dimensions (presence, motion, breathing, heart rate, phase variance, person count, fall, RSSI) sent at 1 Hz alongside vitals.
  • scripts/seed_csi_bridge.py — Python bridge: UDP listener → HTTPS ingest with bearer token auth, --validate (kNN + PIR ground truth), --stats, --compact modes, hash-based vector IDs, NaN/inf rejection, source IP filtering, retry logic.
  • Arena Physica research — 26 research documents in docs/research/ covering Maxwell's equations in WiFi sensing, Arena Physica Studio analysis, SOTA WiFi sensing 2025-2026, GOAP implementation plan for ESP32 + Pi Zero.
  • Cognitum Seed MCP integration — 114-tool MCP proxy enables AI assistants to query sensing state, vectors, witness chain, and device status directly.

Fixed

  • Compressed frame magic collision — Reassigned compressed frame magic from 0xC5110003 to 0xC5110005 to free 0xC5110003 for feature vectors.
  • Uninitialized s_top_k[0] read — Guarded variance computation against s_top_k_count == 0 in send_feature_vector().
  • Presence score normalization — Bridge now divides by 15.0 instead of clamping, preserving dynamic range for raw values 1.41-14.92.
  • Stale magic references — Updated ADR-039, DDD model to reflect 0xC5110005 for compressed frames.

Security

  • Credential exposure remediation — Removed hardcoded WiFi passwords and bearer tokens from source files. Added NVS binary/CSV patterns to .gitignore. Environment variable fallback for bearer token.
  • NaN/Inf injection prevention — Bridge validates all feature dimensions are finite before Seed ingest.
  • UDP source filtering--allowed-sources argument restricts packet acceptance to known ESP32 IPs.

Changed

  • Wire format table now includes 6 magic numbers: 0xC5110001 (raw), 0xC5110002 (vitals), 0xC5110003 (features), 0xC5110004 (WASM events), 0xC5110005 (compressed), 0xC5110006 (fused vitals).

[v0.5.3-esp32] — 2026-03-30

Added

  • Cross-node RSSI-weighted feature fusion — Multiple ESP32 nodes fuse CSI features using RSSI-based weighting. Closer node gets higher weight. Reduces variance noise by 29%, keypoint jitter by 72%.
  • DynamicMinCut person separation — Uses ruvector_mincut::DynamicMinCut on the subcarrier temporal correlation graph to detect independent motion clusters. Replaces variance-based heuristic for multi-person counting.
  • RSSI-based position tracking — Skeleton position driven by RSSI differential between nodes. Walk between ESP32s and the skeleton follows you.
  • Per-node state pipeline (ADR-068) — Each ESP32 node gets independent HashMap<u8, NodeState> with frame history, classification, vitals, and person count. Fixes #249 (the #1 user-reported issue).
  • RuVector Phase 1-3 integration — Subcarrier importance weighting, temporal keypoint smoothing (EMA), coherence gating, skeleton kinematic constraints (Jakobsen relaxation), compressed pose history.
  • Client-side lerp smoothing — UI keypoints interpolate between frames (alpha=0.15) for fluid skeleton movement.
  • Multi-node mesh tests — 8 integration tests covering 1-255 node configurations.
  • wifi_densepose Python packagefrom wifi_densepose import WiFiDensePose now works (#314).

Fixed

  • Watchdog crash on busy LANs (#321) — Batch-limited edge_dsp to 4 frames before 20ms yield. Fixed idle-path busy-spin (pdMS_TO_TICKS(5)==0).
  • No detection from edge vitals (#323) — Server now generates sensing_update from Tier 2+ vitals packets.
  • RSSI byte offset mismatch (#332) — Server parsed RSSI from wrong byte (was reading sequence counter).
  • Stack overflow risk — Moved 4KB of BPM scratch buffers from stack to static storage.
  • Stale node memory leaknode_states HashMap evicts nodes inactive >60s.
  • Unsafe raw pointer removed — Replaced with safe .clone() for adaptive model borrow.
  • Firmware CI — Upgraded to IDF v5.4, replaced xxd with od (#327).
  • Person count double-counting — Multi-node aggregation changed from sum to max.
  • Skeleton jitter — Removed tick-based noise, dampened procedural animation, recalibrated feature scaling for real ESP32 data.

Changed

  • Motion-responsive skeleton: arm swing (0-80px) driven by CSI variance, leg kick (0-50px) by motion_band_power, vertical bob when walking.
  • Person count thresholds recalibrated for real ESP32 hardware (1→2 at 0.70, EMA alpha 0.04).
  • Vital sign filtering: larger median window (31), faster EMA (0.05), looser HR jump filter (15 BPM).
  • Vendored ruvector updated to v2.1.0-40 (316 commits ahead).

Benchmarks (2-node mesh, COM6 + COM9, 30s)

Metric Baseline v0.5.3 Improvement
Variance noise 109.4 77.6 -29%
Feature stability std=154.1 std=105.4 -32%
Keypoint jitter std=4.5px std=1.3px -72%
Confidence 0.643 0.686 +7%
Presence accuracy 93.4% 94.6% +1.3pp

Verified

  • Real hardware: COM6 (node 1) + COM9 (node 2) on ruv.net WiFi
  • All 284 Rust tests pass, 352 signal crate tests pass
  • Firmware builds clean at 843 KB
  • QEMU CI: 11/11 jobs green

[v0.5.2-esp32] — 2026-03-28

Fixed

  • RSSI byte offset in frame parser (#332)
  • Per-node state pipeline for multi-node sensing (#249)
  • Firmware CI upgraded to IDF v5.4 (#327)

[v0.5.1-esp32] — 2026-03-27

Fixed

  • Watchdog crash on busy LANs (#321)
  • No detection from edge vitals (#323)
  • wifi_densepose Python package import (#314)
  • Pre-compiled firmware binaries added to release

[v0.5.0-esp32] — 2026-03-15

Added

  • 60 GHz mmWave sensor fusion (ADR-063) — Auto-detects Seeed MR60BHA2 (60 GHz, HR/BR/presence) and HLK-LD2410 (24 GHz, presence/distance) on UART at boot. Probes 115200 then 256000 baud, registers device capabilities, starts background parser.
  • 48-byte fused vitals packet (magic 0xC5110004) — Kalman-style fusion: mmWave 80% + CSI 20% when both available. Automatic fallback to standard 32-byte CSI-only packet.
  • Server-side fusion bridge (scripts/mmwave_fusion_bridge.py) — Reads two serial ports simultaneously for dual-sensor setups where mmWave runs on a separate ESP32.
  • Multimodal ambient intelligence roadmap (ADR-064) — 25+ applications from fall detection to sleep monitoring to RF tomography.

Verified

  • Real hardware: ESP32-S3 (COM7) WiFi CSI + ESP32-C6/MR60BHA2 (COM4) 60 GHz mmWave running concurrently. HR=75 bpm, BR=25/min at 52 cm range. All 11 QEMU CI jobs green.

[v0.4.3-esp32] — 2026-03-15

Fixed

  • Fall detection false positives (#263) — Default threshold raised from 2.0 to 15.0 rad/s²; normal walking (2-5 rad/s²) no longer triggers alerts. Added 3-consecutive-frame debounce and 5-second cooldown between alerts. Verified on real ESP32-S3 hardware: 0 false alerts in 60s / 1,300+ live WiFi CSI frames.
  • Kconfig default mismatchCONFIG_EDGE_FALL_THRESH Kconfig default was still 2000 (=2.0) while nvs_config.c fallback was updated to 15.0. Fixed Kconfig to 15000. Caught by real hardware testing — mock data did not reproduce.
  • provision.py NVS generator API changeesp_idf_nvs_partition_gen package changed its generate() signature; switched to subprocess-first invocation for cross-version compatibility.
  • QEMU CI pipeline (11 jobs) — Fixed all failures: fuzz test esp_timer stubs, QEMU libgcrypt dependency, NVS matrix generator, IDF container pip path, flash image padding, validation WARN handling, swarm ip/cargo missing.

Added

  • 4MB flash support (#265)partitions_4mb.csv and sdkconfig.defaults.4mb for ESP32-S3 boards with 4MB flash (e.g. SuperMini). Dual OTA slots, 1.856 MB each. Thanks to @sebbu for the community workaround that confirmed feasibility.
  • --strict flag for validate_qemu_output.py — WARNs now pass by default in CI (no real WiFi in QEMU); use --strict to fail on warnings.

Unreleased

Added

  • QEMU ESP32-S3 testing platform (ADR-061) — 9-layer firmware testing without hardware
    • Mock CSI generator with 10 physics-based scenarios (empty room, walking, fall, multi-person, etc.)
    • Single-node QEMU runner with 16-check UART validation
    • Multi-node TDM mesh simulation (TAP networking, 2-6 nodes)
    • GDB remote debugging with VS Code integration
    • Code coverage via gcov/lcov + apptrace
    • Fuzz testing (3 libFuzzer targets + ASAN/UBSAN)
    • NVS provisioning matrix (14 configs)
    • Snapshot-based regression testing (sub-second VM restore)
    • Chaos testing with fault injection + health monitoring
  • QEMU Swarm Configurator (ADR-062) — YAML-driven multi-ESP32 test orchestration
    • 4 topologies: star, mesh, line, ring
    • 3 node roles: sensor, coordinator, gateway
    • 9 swarm-level assertions (boot, crashes, TDM, frame rate, fall detection, etc.)
    • 7 presets: smoke (2n/15s), standard (3n/60s), ci-matrix, large-mesh, line-relay, ring-fault, heterogeneous
    • Health oracle with cross-node validation
  • QEMU installer (install-qemu.sh) — auto-detects OS, installs deps, builds Espressif QEMU fork
  • Unified QEMU CLI (qemu-cli.sh) — single entry point for all 11 QEMU test commands
  • CI: firmware-qemu.yml workflow with QEMU test matrix, fuzz testing, NVS validation, and swarm test jobs
  • User guide: QEMU testing and swarm configurator section with plain-language walkthrough

Fixed

  • Firmware now boots in QEMU: WiFi/UDP/OTA/display guards for mock CSI mode

  • 9 bugs in mock_csi.c (LFSR bias, MAC filter init, scenario loop, overflow burst timing)

  • 23 bugs from ADR-061 deep review (inject_fault.py writes, CI cache, snapshot log corruption, etc.)

  • 16 bugs from ADR-062 deep review (log filename mismatch, SLIRP port collision, heap false positives, etc.)

  • All scripts: --help flags, prerequisite checks with install hints, standardized exit codes

  • Sensing server UI API completion (ADR-043) — 14 fully-functional REST endpoints for model management, CSI recording, and training control

    • Model CRUD: GET /api/v1/models, GET /api/v1/models/active, POST /api/v1/models/load, POST /api/v1/models/unload, DELETE /api/v1/models/:id, GET /api/v1/models/lora/profiles, POST /api/v1/models/lora/activate
    • CSI recording: GET /api/v1/recording/list, POST /api/v1/recording/start, POST /api/v1/recording/stop, DELETE /api/v1/recording/:id
    • Training control: GET /api/v1/train/status, POST /api/v1/train/start, POST /api/v1/train/stop
    • Recording writes CSI frames to .jsonl files via tokio background task
    • Model/recording directories scanned at startup, state managed via Arc<RwLock<AppStateInner>>
  • ADR-044: Provisioning tool enhancements — 5-phase plan for complete NVS coverage (7 missing keys), JSON config files, mesh presets, read-back/verify, and auto-detect

  • 25 real mobile tests replacing it.todo() placeholders — 205 assertions covering components, services, stores, hooks, screens, and utils

  • Project MERIDIAN (ADR-027) — Cross-environment domain generalization for WiFi pose estimation (1,858 lines, 72 tests)

    • HardwareNormalizer — Catmull-Rom cubic interpolation resamples any hardware CSI to canonical 56 subcarriers; z-score + phase sanitization
    • DomainFactorizer + GradientReversalLayer — adversarial disentanglement of pose-relevant vs environment-specific features
    • GeometryEncoder + FilmLayer — Fourier positional encoding + DeepSets + FiLM for zero-shot deployment given AP positions
    • VirtualDomainAugmentor — synthetic environment diversity (room scale, wall material, scatterers, noise) for 4x training augmentation
    • RapidAdaptation — 10-second unsupervised calibration via contrastive test-time training + LoRA adapters
    • CrossDomainEvaluator — 6-metric evaluation protocol (MPJPE in-domain/cross-domain/few-shot/cross-hardware, domain gap ratio, adaptation speedup)
  • ADR-027: Cross-Environment Domain Generalization — 10 SOTA citations (PerceptAlign, X-Fi ICLR 2025, AM-FM, DGSense, CVPR 2024)

  • Cross-platform RSSI adapters — macOS CoreWLAN (MacosCoreWlanScanner) and Linux iw (LinuxIwScanner) Rust adapters with #[cfg(target_os)] gating

  • macOS CoreWLAN Python sensing adapter with Swift helper (mac_wifi.swift)

  • macOS synthetic BSSID generation (FNV-1a hash) for Sonoma 14.4+ BSSID redaction

  • Linux iw dev <iface> scan parser with freq-to-channel conversion and scan dump (no-root) mode

  • ADR-025: macOS CoreWLAN WiFi Sensing (ORCA)

Fixed

  • sendto ENOMEM crash (Issue #127) — CSI callbacks in promiscuous mode exhaust lwIP pbuf pool causing guru meditation crash. Fixed with 50 Hz rate limiter in csi_collector.c and 100 ms ENOMEM backoff in stream_sender.c. Hardware-verified on ESP32-S3 (200+ callbacks, zero crashes)
  • Provisioning script missing TDM/edge flags (Issue #130) — Added --tdm-slot, --tdm-total, --edge-tier, --pres-thresh, --fall-thresh, --vital-win, --vital-int, --subk-count to provision.py
  • WebSocket "RECONNECTING" on Dashboard/Live DemosensingService.start() now called on app init in app.js so WebSocket connects immediately instead of waiting for Sensing tab visit
  • Mobile WebSocket portws.service.ts buildWsUrl() uses same-origin port instead of hardcoded port 3001
  • Mobile Jest configtestPathIgnorePatterns no longer silently ignores the entire test directory
  • Removed synthetic byte counters from Python MacosWifiCollector — now reports tx_bytes=0, rx_bytes=0 instead of fake incrementing values

3.0.0 - 2026-03-01

Major release: AETHER contrastive embedding model, Docker Hub images, and comprehensive UI overhaul.

Added — AETHER Contrastive Embedding Model (ADR-024)

  • Project AETHER — self-supervised contrastive learning for WiFi CSI fingerprinting, similarity search, and anomaly detection (9bbe956)
  • embedding.rs module: ProjectionHead, InfoNceLoss, CsiAugmenter, FingerprintIndex, PoseEncoder, EmbeddingExtractor (909 lines, zero external ML dependencies)
  • SimCLR-style pretraining with 5 physically-motivated augmentations (temporal jitter, subcarrier masking, Gaussian noise, phase rotation, amplitude scaling)
  • CLI flags: --pretrain, --pretrain-epochs, --embed, --build-index <type>
  • Four HNSW-compatible fingerprint index types: env_fingerprint, activity_pattern, temporal_baseline, person_track
  • Cross-modal PoseEncoder for WiFi-to-camera embedding alignment
  • VICReg regularization for embedding collapse prevention
  • 53K total parameters (55 KB at INT8) — fits on ESP32

Added — Docker & Deployment

  • Published Docker Hub images: ruvnet/wifi-densepose:latest (132 MB Rust) and ruvnet/wifi-densepose:python (569 MB) (add9f19)
  • Multi-stage Dockerfile for Rust sensing server with RuVector crates
  • docker-compose.yml orchestrating both Rust and Python services
  • RVF model export via --export-rvf and load via --load-rvf CLI flags

Added — Documentation

  • 33 use cases across 4 vertical tiers: Everyday, Specialized, Robotics & Industrial, Extreme (0afd9c5)
  • "Why WiFi Wins" comparison table (WiFi vs camera vs LIDAR vs wearable vs PIR)
  • Mermaid architecture diagrams: end-to-end pipeline, signal processing detail, deployment topology (50f0fc9)
  • Models & Training section with RuVector crate links (GitHub + crates.io), SONA component table (965a1cc)
  • RVF container section with deployment targets table (ESP32 0.7 MB to server 50+ MB)
  • Collapsible README sections for improved navigation (478d964, 99ec980, 0ebd6be)
  • Installation and Quick Start moved above Table of Contents (50acbf7)
  • CSI hardware requirement notice (528b394)

Fixed

  • UI auto-detects server port from page origin — no more hardcoded localhost:8080; works on any port (Docker :3000, native :8080, custom) (3b72f35, closes #55)
  • Docker port mismatch — server now binds 3000/3001 inside container as documented (44b9c30)
  • Added /ws/sensing WebSocket route to the HTTP server so UI only needs one port
  • Fixed README API endpoint references: /api/v1/health/health, /api/v1/sensing/api/v1/sensing/latest
  • Multi-person tracking limit corrected: configurable default 10, no hard software cap (e2ce250)

2.0.0 - 2026-02-28

Major release: complete Rust sensing server, full DensePose training pipeline, RuVector v2.0.4 integration, ESP32-S3 firmware, and 6 security hardening patches.

Added — Rust Sensing Server

  • Full DensePose-compatible REST API served by Axum (d956c30)
    • GET /health — server health
    • GET /api/v1/sensing/latest — live CSI sensing data
    • GET /api/v1/vital-signs — breathing rate (6-30 BPM) and heartbeat (40-120 BPM)
    • GET /api/v1/pose/current — 17 COCO keypoints derived from WiFi signal field
    • GET /api/v1/info — server build and feature info
    • GET /api/v1/model/info — RVF model container metadata
    • ws://host/ws/sensing — real-time WebSocket stream
  • Three data sources: --source esp32 (UDP CSI), --source windows (netsh RSSI), --source simulated (deterministic reference)
  • Auto-detection: server probes ESP32 UDP and Windows WiFi, falls back to simulated
  • Three.js visualization UI with 3D body skeleton, signal heatmap, phase plot, Doppler bars, vital signs panel
  • Static UI serving via --ui-path flag
  • Throughput: 9,52011,665 frames/sec (release build)

Added — ADR-021: Vital Sign Detection

  • VitalSignDetector with breathing (6-30 BPM) and heartbeat (40-120 BPM) extraction from CSI fluctuations (1192de9)
  • FFT-based spectral analysis with configurable band-pass filters
  • Confidence scoring based on spectral peak prominence
  • REST endpoint /api/v1/vital-signs with real-time JSON output

Added — ADR-023: DensePose Training Pipeline (Phases 1-8)

  • wifi-densepose-train crate with complete 8-phase pipeline (fc409df, ec98e40, fce1271)
    • Phase 1: DataPipeline with MM-Fi and Wi-Pose dataset loaders
    • Phase 2: CsiToPoseTransformer — 4-head cross-attention + 2-layer GCN on COCO skeleton
    • Phase 3: 6-term composite loss (MSE, bone length, symmetry, joint angle, temporal, confidence)
    • Phase 4: DynamicPersonMatcher via ruvector-mincut (O(n^1.5 log n) Hungarian assignment)
    • Phase 5: SonaAdapter — MicroLoRA rank-4 with EWC++ memory preservation
    • Phase 6: SparseInference — progressive 3-layer model loading (A: essential, B: refinement, C: full)
    • Phase 7: RvfContainer — single-file model packaging with segment-based binary format
    • Phase 8: End-to-end training with cosine-annealing LR, early stopping, checkpoint saving
  • CLI: --train, --dataset, --epochs, --save-rvf, --load-rvf, --export-rvf
  • Benchmark: ~11,665 fps inference, 229 tests passing

Added — ADR-016: RuVector Training Integration (all 5 crates)

  • ruvector-mincutDynamicPersonMatcher in metrics.rs + subcarrier selection (81ad09d, a7dd31c)
  • ruvector-attn-mincut → antenna attention in model.rs + noise-gated spectrogram
  • ruvector-temporal-tensorCompressedCsiBuffer in dataset.rs + compressed breathing/heartbeat
  • ruvector-solver → sparse subcarrier interpolation (114→56) + Fresnel triangulation
  • ruvector-attention → spatial attention in model.rs + attention-weighted BVP
  • Vendored all 11 RuVector crates under vendor/ruvector/ (d803bfe)

Added — ADR-017: RuVector Signal & MAT Integration (7 integration points)

  • gate_spectrogram() — attention-gated noise suppression (18170d7)
  • attention_weighted_bvp() — sensitivity-weighted velocity profiles
  • mincut_subcarrier_partition() — dynamic sensitive/insensitive subcarrier split
  • solve_fresnel_geometry() — TX-body-RX distance estimation
  • CompressedBreathingBuffer + CompressedHeartbeatSpectrogram
  • BreathingDetector + HeartbeatDetector (MAT crate, real FFT + micro-Doppler)
  • Feature-gated behind cfg(feature = "ruvector") (ab2453e)

Added — ADR-018: ESP32-S3 Firmware & Live CSI Pipeline

  • ESP32-S3 firmware with FreeRTOS CSI extraction (92a5182)
  • ADR-018 binary frame format: [0xAD, 0x18, len_hi, len_lo, payload]
  • Rust Esp32Aggregator receiving UDP frames on port 5005
  • bridge.rs converting I/Q pairs to amplitude/phase vectors
  • NVS provisioning for WiFi credentials
  • Pre-built binary quick start documentation (696a726)

Added — ADR-014: SOTA Signal Processing

  • 6 algorithms, 83 tests (fcb93cc)
    • Hampel filter (median + MAD, resistant to 50% contamination)
    • Conjugate multiplication (reference-antenna ratio, cancels common-mode noise)
    • Phase sanitization (unwrap + linear detrend, removes CFO/SFO)
    • Fresnel zone geometry (TX-body-RX distance from first-principles physics)
    • Body Velocity Profile (micro-Doppler extraction, 5.7x speedup)
    • Attention-gated spectrogram (learned noise suppression)

Added — ADR-015: Public Dataset Training Strategy

  • MM-Fi and Wi-Pose dataset specifications with download links (4babb32, 5dc2f66)
  • Verified dataset dimensions, sampling rates, and annotation formats
  • Cross-dataset evaluation protocol

Added — WiFi-Mat Disaster Detection Module

  • Multi-AP triangulation for through-wall survivor detection (a17b630, 6b20ff0)
  • Triage classification (breathing, heartbeat, motion)
  • Domain events: survivor_detected, survivor_updated, alert_created
  • WebSocket broadcast at /ws/mat/stream

Added — Infrastructure

  • Guided 7-step interactive installer with 8 hardware profiles (8583f3e)
  • Comprehensive build guide for Linux, macOS, Windows, Docker, ESP32 (45f8a0d)
  • 12 Architecture Decision Records (ADR-001 through ADR-012) (337dd96)

Added — UI & Visualization

  • Sensing-only UI mode with Gaussian splat visualization (b7e0f07)
  • Three.js 3D body model (17 joints, 16 limbs) with signal-viz components
  • Tabs: Dashboard, Hardware, Live Demo, Sensing, Architecture, Performance, Applications
  • WebSocket client with automatic reconnection and exponential backoff

Added — Rust Signal Processing Crate

  • Complete Rust port of WiFi-DensePose with modular workspace (6ed69a3)
    • wifi-densepose-signal — CSI processing, phase sanitization, feature extraction
    • wifi-densepose-core — shared types and configuration
    • wifi-densepose-nn — neural network inference (DensePose head, RCNN)
    • wifi-densepose-hardware — ESP32 aggregator, hardware interfaces
    • wifi-densepose-config — configuration management
  • Comprehensive benchmarks and validation tests (3ccb301)

Added — Python Sensing Pipeline

  • WindowsWifiCollector — RSSI collection via netsh wlan show networks
  • RssiFeatureExtractor — variance, spectral bands (motion 0.5-4 Hz, breathing 0.1-0.5 Hz), change points
  • PresenceClassifier — rule-based 3-state classification (ABSENT / PRESENT_STILL / ACTIVE)
  • Cross-receiver agreement scoring for multi-AP confidence boosting
  • WebSocket sensing server (ws_server.py) broadcasting JSON at 2 Hz
  • Deterministic CSI proof bundles for reproducible verification (archive/v1/data/proof/)
  • Commodity sensing unit tests (b391638)

Changed

  • Rust hardware adapters now return explicit errors instead of silent empty data (6e0e539)

Fixed

  • Review fixes for end-to-end training pipeline (45f0304)
  • Dockerfile paths updated from src/ to archive/v1/src/ (7872987)
  • IoT profile installer instructions updated for aggregator CLI (f460097)
  • process.env reference removed from browser ES module (e320bc9)

Performance

  • 5.7x Doppler extraction speedup via optimized FFT windowing (32c75c8)
  • Single 2.1 MB static binary, zero Python dependencies for Rust server

Security

  • Fix SQL injection in status command and migrations (f9d125d)
  • Fix XSS vulnerabilities in UI components (5db55fd)
  • Fix command injection in statusline.cjs (4cb01fd)
  • Fix path traversal vulnerabilities (896c4fc)
  • Fix insecure WebSocket connections — enforce wss:// on non-localhost (ac094d4)
  • Fix GitHub Actions shell injection (ab2e7b4)
  • Fix 10 additional vulnerabilities, remove 12 dead code instances (7afdad0)

1.1.0 - 2025-06-07

Added

  • Complete Python WiFi-DensePose system with CSI data extraction and router interface
  • CSI processing and phase sanitization modules
  • Batch processing for CSI data in CSIProcessor and PhaseSanitizer
  • Hardware, pose, and stream services for WiFi-DensePose API
  • Comprehensive CSS styles for UI components and dark mode support
  • API and Deployment documentation

Fixed

  • Badge links for PyPI and Docker in README
  • Async engine creation poolclass specification

1.0.0 - 2024-12-01

Added

  • Initial release of WiFi-DensePose
  • Real-time WiFi-based human pose estimation using Channel State Information (CSI)
  • DensePose neural network integration for body surface mapping
  • RESTful API with comprehensive endpoint coverage
  • WebSocket streaming for real-time pose data
  • Multi-person tracking with configurable capacity (default 10, up to 50+)
  • Fall detection and activity recognition
  • Domain configurations: healthcare, fitness, smart home, security
  • CLI interface for server management and configuration
  • Hardware abstraction layer for multiple WiFi chipsets
  • Phase sanitization and signal processing pipeline
  • Authentication and rate limiting
  • Background task management
  • Cross-platform support (Linux, macOS, Windows)

Documentation

  • User guide and API reference
  • Deployment and troubleshooting guides
  • Hardware setup and calibration instructions
  • Performance benchmarks
  • Contributing guidelines