Two ingest bugs caused real ESP32-C6 HE20 CSI to be silently discarded or
never received — the "real data silently lost" failure class. Each fix is
pinned by a test that fails on the old code.
#1009 §1b — HE20 baseline recorder trimmed 256->242 bins by sequential index.
ESP-IDF v5.5.2 delivers all 256 FFT bins for an HE20 frame, but
CalibrationConfig::he20() carried num_active: 242, so the recorder (no HE20
tone map — extract_first_stream takes the first num_active columns
sequentially) kept bins 0..242 = the lower guard band + DC, NOT the 242 active
tones, silently corrupting the empty-room baseline. Now num_active: 256 records
every delivered bin, aligned 1:1 with the live deviation() path. The exact-242
tone map stays only in cir.rs (HE20_ACTIVE), where the Phi sensing matrix needs
it. HE20 synthetic/bench fixtures updated to feed 256-bin frames.
#1009 §1a/§1c — u8->u16 n_subcarriers truncation, regression-pinned.
The ADR-018 wire format carries n_subcarriers as u16 LE at bytes 6-7; a 256-bin
HE20 frame (byte6=0x00) read as one byte decodes to 0 subcarriers -> every
frame skipped. The CLI parser and the sensing-server parse_esp32_frame were
already corrected to u16 under #1005/ADR-110; added regression tests that fail
on the old single-byte read so the truncation cannot silently return.
#1004 — --source auto latched on simulate forever, never binding UDP :5005.
A one-shot boot probe resolved the source once; with no CSI flowing at boot
(the normal firmware/server startup race) it served simulated poses for the
whole process and ignored real CSI arriving seconds later (the prior #937 fix
hard-exited instead — equally wrong). New plan_source() state machine: in auto
mode ALWAYS bind the UDP receiver and serve simulated only until the first real
frame, then udp_receiver_task promotes source -> esp32 (mirroring the existing
esp32 -> esp32:offline reversion). simulated_data_task self-suspends once
promoted. Explicit --source simulated stays a hard, UDP-free offline override.
Validation: 3-crate tests 1118 passed / 0 failed; workspace 3166 passed /
0 failed; Python proof VERDICT: PASS (bit-exact, unaffected). cir.rs untouched.
Co-Authored-By: claude-flow <ruv@ruv.net>
The cir_pipeline end-to-end test was gated on the same dominant_tap_ratio
floor; the windowed-ratio fix resolves it. All 6 ADR-134 P2 CIR tests
(cir_synthetic 5 + cir_pipeline 1) now pass. signal+cir: 472 pass / 0 fail.
Co-Authored-By: claude-flow <ruv@ruv.net>
Found the principled fix for the rms-delay-spread inflation (superseding my
prior 'needs ISTA work' note): the spurious ~15-20% tap at ~bin 150 is an
ALIAS of the near-zero dominant tap — the ISTA delay grid is circular (Φ is
DFT-like), so bins >= G/2 are non-causal negative delays. Computing the delay
spread over only the causal half [0, G/2) drops rms from 389ns to 65ns (true
value), cleanly and robustly (no fragile magnitude threshold). Un-ignores
should_produce_positive_rms_delay_spread.
ADR-134 P2 cir_synthetic now FULLY resolved: all 5 previously-ignored tests
pass via two physics-justified fixes (windowed dominant-ratio for super-
resolution leakage + causal-window rms for circular-grid aliasing). signal+cir:
471 pass / 0 fail / 0 ignored in cir_synthetic.
Co-Authored-By: claude-flow <ruv@ruv.net>
Diagnosed the one still-ignored CIR test: ISTA emits a spurious ~15-20%-of-
dominant tap at an implausible far delay (~bin 150 / ~3us) that inflates
rms_delay_spread to ~390ns (vs ~53ns true). It sits too close to the real
weakest tap (~30% of dominant) for a safe magnitude cutoff, so the proper fix
is ISTA recovery-quality work (grid de-aliasing / far-tap suppression), not a
band-aid threshold. Sharpened the #[ignore] note accordingly. signal+cir:
470 pass / 0 fail.
Co-Authored-By: claude-flow <ruv@ruv.net>
The CIR estimator's dominant_tap_ratio measured a single grid bin, but on the
3x super-resolved ISTA grid a single physical tap leaks across ~3 adjacent
bins — so the ratio under-counted the dominant tap and sat far below the
per-tier floors (HT20 0.158<0.30, HT40 0.133<0.35, HE20 0.102<0.40), forcing
the 3-tap recovery + 40MHz-ToF tests to be #[ignore]d.
Fix (data-backed via a lambda sweep): (1) compute dominant_tap_ratio over a
+/-1-bin window around the peak — the physical tap's true footprint; (2) tune
L1 lambda for sparse multipath (HT20 .05->.08, HT40 .03->.08, HE20 .03->.18).
Result: ratios 0.367/0.406/0.474, comfortably above floors with all 3 taps
preserved. Un-ignores should_recover_3tap_channel_{ht20,ht40,he20} and
should_return_tof_at_40mhz. signal crate: 470 pass / 0 fail; change isolated
to CIR (no external consumers). The rms-delay-spread test stays ignored with a
re-scoped note (far-tap robustness is separate remaining work).
Co-Authored-By: claude-flow <ruv@ruv.net>
Operator-initiated calibration that records 30 s of stationary CSI,
emits a per-subcarrier baseline (amplitude mean+variance via Welford,
phase via circular sin/cos sums with von Mises dispersion), and gates
downstream stages on a deviation z-score. Plugs into multistatic
coherence gating, motion/presence detection, and the new ADR-134 CIR
estimator as a reference-subtracted input.
API surface (under wifi_densepose_signal):
CalibrationConfig::{ht20, ht40, he20, he40}
CalibrationRecorder { record(), finalize(), frames_recorded() }
BaselineCalibration {
subcarriers: Vec<SubcarrierBaseline>,
deviation(&CsiFrame), subtract_in_place(&mut CsiFrame),
to_bytes(), from_bytes()
}
CalibrationDeviationScore { amplitude_z_median, amplitude_z_max,
phase_drift_median, motion_flagged }
CalibrationError { SubcarrierMismatch, TierMismatch,
InsufficientFrames, VersionMismatch, TruncatedBuffer }
Binary baseline format: magic 0xCA1B_0001 + u8 version=1 + u8 tier +
captured_at_unix_s (i64) + frame_count (u64) + num_subcarriers (u32) +
[SubcarrierBaseline; N] as 16 bytes each (amp_mean, amp_variance,
phase_mean, phase_dispersion as f32 LE). Hand-written serialisation so
the format is stable across Rust toolchain versions without serde drift.
CLI: new `wifi-densepose calibrate` subcommand binds a UDP listener
(0xC511_0001 frames), streams them through CalibrationRecorder, prints
a real-time z-score banner per ADR-135 §risk 1 (operator-may-be-moving),
aborts on sustained high deviation, and writes the binary baseline to
disk. Local UDP packet parser duplicated from sensing-server (per ADR
discussion — avoids cross-crate API churn).
Witness: cross-platform-deterministic SHA-256 over the per-subcarrier
quantised baseline profile (u16 LE at 1e-2/1e-4/1e-3, no sort) using
the lesson learnt from the CIR PR #837 libm-jitter fix. Hash:
d6bce07ecb1648e6936561df44bf4a3bfc17bb0ba5f692646b2301d105b52f67
CI guard: new "ADR-135 calibration witness proof (determinism guard)"
step under the Rust Workspace Tests job, adjacent to the existing
ADR-134 CIR guard. Regressions are unambiguously attributable.
Hardware-in-loop validation: full 600-frame capture exercised via the
new scripts/synth-csi-udp.py emitter targeting 127.0.0.1:5005. The CLI
binary received 600 frames at 20 Hz, z_med stable at ~0.7, motion
correctly NOT flagged, finalised baseline written to baseline.bin (860
bytes) with correct magic + version + timestamp in the header. Live
ESP32 capture from COM9 is operator follow-up — requires provisioning
the firmware's UDP target IP to match the host running the CLI.
Test results (cargo test -p wifi-densepose-signal --no-default-features):
lib: 382 pass / 0 fail / 1 ignored
calibration_synthetic: 17 pass / 0 fail
calibration_drift: 5 pass / 0 fail
calibration_roundtrip: 10 pass / 0 fail
cir_*: 9 pass + 6 documented P2 ignores
doctest: 10 pass
Bench: 20 Criterion combinations registered
(recorder_record / recorder_finalize / deviation / record_600 /
to_bytes across HT20/HT40/HE20/HE40 tiers).
Witness: bash scripts/verify-calibration-proof.sh → VERDICT: PASS
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(signal): ADR-134 — CSI→CIR via ISTA + NeumannSolver warm-start
End-to-end first-class Channel Impulse Response estimation in the Rust
workspace. Bridges CSI (frequency domain) to CIR (delay domain) so
multistatic coherence gating, NLOS/LOS classification, and (at HT40+)
ToF ranging become tractable in `wifi-densepose-signal`.
Algorithm: ISTA L1 sparse recovery over a normalized DFT sub-matrix
sensing operator Φ ∈ ℂ^(K×G) with G = 3K (3× super-resolution). The
Tikhonov-regularised warm start re-uses `ruvector_solver::neumann::
NeumannSolver` — same call pattern as `fresnel.rs:280` and
`train/subcarrier.rs:225` — so no new crate dependencies.
Tiers supported: HT20 / HT40 / HE20 (Tier A-HE, C6) / HE40. The C6
HE-LTF tier is the preferred Tier A target whenever an 11ax AP is in
range; firmware substrate already shipped at v0.7.0-esp32 per ADR-110.
Measured performance (release, single CirEstimator shared across 12
links): HT20 2.72 ms / HE20 3.20 ms / HT40 13.43 ms / HE40 9.71 ms per
estimate(). HT20 12-link multistatic 17.7 ms — fits the 50 ms RuvSense
cycle; HT40 12-link 74 ms exceeds it and is flagged in ADR-134 §2.7 as
requiring Rayon parallelism or G=2K super-res reduction.
Measured Φ conditioning: κ(Φ) ≈ 1.00 identically across all tiers.
ADR-134 §2.3 was corrected — the C6 advantage is statistical SNR gain
(√(242/52) ≈ 2.16×) from more independent measurements, not improved
conditioning.
Witness: bit-deterministic SHA-256 over CirEstimator output on the
synthetic ADR-028 reference signal (100 frames, top-5 taps, 1e-6
quantization). Hash committed to expected_cir_features.sha256;
verify-cir-proof.sh wires the check into the existing witness bundle.
CI: cargo test --features cir + verify-cir-proof.sh added as separate
steps under the Rust Workspace Tests job; regressions are unambiguously
attributable.
Files:
- ADR + WITNESS-LOG-028 row 34 + CLAUDE.md module count (14 → 15)
- src/ruvsense/cir.rs (~540 LOC) + lib.rs re-exports + multistatic.rs
wire-up (reversible via `use_cir_gate=false`)
- 3 integration tests + Criterion bench + 3 deterministic fixtures
- cir_proof_runner binary + sha256 + verify-cir-proof.sh
Test rate: 395 pass / 6 ignored (P2 ISTA hyperparameter tuning; see
#[ignore] reasons) / 0 fail. cargo check clean; verify-cir-proof.sh
VERDICT: PASS.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(signal): make CIR witness cross-platform-deterministic
The first witness (Windows-generated hash 89704bfd…) failed on Linux CI
with a different hash (b36741bf…). Root cause: hashing `re`/`im` parts of
top-5 taps at 1e-6 precision is too tight against libm differences in
sin/cos/sqrt across glibc, MSVC, and Apple-clang. The previous
"top-5 sorted by magnitude" form also suffered from rank instability when
taps are near-tied — libm jitter could shuffle the ordering even when the
algorithm is unchanged.
New canonical form: full per-tap quantised-magnitude profile in natural
index order, no sort.
- 156 taps × 2 bytes (u16 le) per frame = 312 bytes/frame.
- Quantisation 1e-2 — robust to ~1e-3 float drift while still tripping
on real algorithmic changes (e.g., a 10× lambda shift moves magnitudes
by >1e-2).
- No top-K selection — eliminates the unstable magnitude-sort step.
Regenerated expected_cir_features.sha256 — new hash 120bd7b1…
If the next CI run still mismatches, the cause is structural (rustfft SIMD
code path selection or NeumannSolver internal ordering), not magnitudes,
and the witness needs further coarsening or to be made platform-tagged.
Co-Authored-By: claude-flow <ruv@ruv.net>
The Rust port lived two directories deep (rust-port/wifi-densepose-rs/)
without any sibling under rust-port/ that warranted the extra level.
Move the whole workspace up to v2/ to match v1/ (Python) at the same
depth and shorten every cd / build command across the repo.
git mv preserves history for all tracked files. 60 files updated for
path references (CI workflows, ADRs, docs, scripts, READMEs, internal
.claude-flow state). Two manual fixes for relative-cd paths in
CLAUDE.md and ADR-043 that became wrong after the depth change
(cd ../.. → cd ..).
Validated:
- cargo check --workspace --no-default-features → clean (after target/
nuke; the gitignored target/ was carried by the OS rename and had
hard-coded old paths in build scripts)
- cargo test --workspace --no-default-features → 1,539 passed, 0 failed,
8 ignored (same totals as pre-rename)
- ESP32-S3 on COM7 → still streaming live CSI (cb #40300, RSSI -64 dBm)
After-merge follow-up: contributors should `rm -rf v2/target` once and
let cargo regenerate from the new path.