feat(signal): ADR-135 empty-room baseline calibration#838
Merged
Conversation
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>
Adds a `--min-frames N` flag to `wifi-densepose calibrate` that overrides the ADR-135 tier minimum (default 600 frames at 20 Hz for HT20). Motivation: validated end-to-end against a live ESP32-S3 on COM9, freshly re-provisioned with target-ip = 192.168.1.50 (this host). The firmware emits CSI at roughly 0.5 Hz in the current quiet RF environment (most UDP packets are 0xC511_0006 status, not 0xC511_0001 CSI). Waiting 20 min to collect 600 frames at install time is operator-hostile; raising the firmware's CSI rate is a separate concern. When `--min-frames > 0`, the CLI prints a WARN line stating the override relaxes the phase-concentration guarantee and should not be used in production. ADR-135 defaults are preserved unchanged. Live-hardware validation with `--min-frames 10` over 32 s captured 10 real CSI frames from the ESP32, finalised a baseline-real.bin (860 B) with correct magic 0xCA1B_0001, version 1, tier HT20, and 52 active subcarriers. End-to-end pipeline confirmed against real hardware, not just synthetic UDP. Co-Authored-By: claude-flow <ruv@ruv.net>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Operator-initiated empty-room baseline calibration per ADR-135. Records 30 s of stationary CSI, emits per-subcarrier baseline (Welford amplitude + von Mises phase), and provides deviation z-scores to gate downstream stages. Plugs into multistatic coherence gating, motion detection, and CIR (ADR-134) as a reference-subtracted input.
What's in this PR
src/ruvsense/calibration.rs(core impl, Welford + circular phase)tests/calibration_synthetic.rs(stationary + perturbation, per tier)tests/calibration_drift.rs(recalibration trigger at z>4 over 900 frames)tests/calibration_roundtrip.rs(bytes round-trip, magic, version)benches/calibration_bench.rs(5 ops × 4 tiers = 20 combos)src/bin/calibration_proof_runner.rs(witness)wifi-densepose-cli/src/calibrate.rs(UDP listener + banner)scripts/synth-csi-udp.py(HW-loop emitter)Algorithm
(count, mean, M2)— inline recurrence identical tofield_model::WelfordStats::update, packed intoSubcarrierStatsso each frame is a single index pass.sin(θ)andcos(θ)sums per subcarrier → mean =atan2(Σsin, Σcos), dispersion =1 − √(Σsin² + Σcos²) / N(von Mises 1−R̄).amplitude_z_median > 2.0 || phase_drift_median > π/6).Hardware-in-loop validation
Full 600-frame capture exercised end-to-end via
scripts/synth-csi-udp.py→wifi-densepose calibrate:Binary file header verified: bytes 0–3 =
01 00 1B CA= LE0xCA1B_0001✓, version 1, tier HT20.Live ESP32 capture from COM9 is operator follow-up. Requires re-running
firmware/esp32-csi-node/provision.pywith the host's current LAN IP so the device's UDP target matches. Synthetic emitter is also useful long-term for CI integration tests of the CLI binary.Cross-platform witness fix carried forward
Same lesson from PR #837: never quantise floats at high precision before hashing. Uses u16 LE at 1e-2/1e-4/1e-3 for the per-subcarrier
amp_mean / amp_variance / phase_mean / phase_dispersion, in natural subcarrier index order, no sort. Hash:d6bce07e….Test plan
cargo test -p wifi-densepose-signal --no-default-features— 382 lib + 32 calibration integration + 9 CIR integration passcargo bench -p wifi-densepose-signal --no-default-features --bench calibration_bench --no-run— 20 bench combinations compilebash scripts/verify-calibration-proof.sh→ VERDICT: PASScargo check -p wifi-densepose-cli --no-default-features— greenFirmware
No new firmware needed. Same as ADR-134 — calibration runs host-side in
wifi-densepose-signal, ingesting the existing 0xC511_0001 CSI frame format the ESP32-S3 already emits at v0.7.0-esp32 (ADR-110). Drift-triggered auto-recalibration is a P2 follow-up.Known limitations
🤖 Generated with claude-flow