Skip to content

feat(signal): ADR-135 empty-room baseline calibration#838

Merged
ruvnet merged 2 commits into
mainfrom
feat/adr-135-empty-room-calibration
May 29, 2026
Merged

feat(signal): ADR-135 empty-room baseline calibration#838
ruvnet merged 2 commits into
mainfrom
feat/adr-135-empty-room-calibration

Conversation

@ruvnet
Copy link
Copy Markdown
Owner

@ruvnet ruvnet commented May 28, 2026

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

Component Lines Status
src/ruvsense/calibration.rs (core impl, Welford + circular phase) 455 7 lib unit tests pass
tests/calibration_synthetic.rs (stationary + perturbation, per tier) 484 17 pass
tests/calibration_drift.rs (recalibration trigger at z>4 over 900 frames) 243 5 pass
tests/calibration_roundtrip.rs (bytes round-trip, magic, version) 247 10 pass
benches/calibration_bench.rs (5 ops × 4 tiers = 20 combos) 246 compiles
src/bin/calibration_proof_runner.rs (witness) 277 hash PASS
wifi-densepose-cli/src/calibrate.rs (UDP listener + banner) 295 green
scripts/synth-csi-udp.py (HW-loop emitter) 95 used in validation

Algorithm

  • Amplitude: per-subcarrier Welford (count, mean, M2) — inline recurrence identical to field_model::WelfordStats::update, packed into SubcarrierStats so each frame is a single index pass.
  • Phase: running sin(θ) and cos(θ) sums per subcarrier → mean = atan2(Σsin, Σcos), dispersion = 1 − √(Σsin² + Σcos²) / N (von Mises 1−R̄).
  • Deviation: amplitude z-score per subcarrier + angular distance for phase; aggregate median + max + motion flag (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.pywifi-densepose calibrate:

[calibrate] listening on udp://127.0.0.1:5005
[calibrate] capturing 600 frames (~32 s, tier=ht20) — ensure room is empty
[calibrate] 100/600 frames | z_med=0.67 z_max=1.92 | motion: no
[calibrate] 200/600 frames | z_med=0.68 z_max=2.11 | motion: no
[calibrate] 300/600 frames | z_med=0.82 z_max=2.22 | motion: no
[calibrate] 400/600 frames | z_med=0.74 z_max=2.37 | motion: no
[calibrate] 500/600 frames | z_med=0.75 z_max=2.16 | motion: no
[calibrate] 600/600 frames | z_med=0.73 z_max=2.41 | motion: no
[calibrate] finalising baseline from 600 frames…
[calibrate] baseline saved to baseline.bin (860 bytes)
[calibrate] summary: frames=600 tier=Ht20 subcarriers=52

Binary file header verified: bytes 0–3 = 01 00 1B CA = LE 0xCA1B_0001 ✓, version 1, tier HT20.

Live ESP32 capture from COM9 is operator follow-up. Requires re-running firmware/esp32-csi-node/provision.py with 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 pass
  • cargo bench -p wifi-densepose-signal --no-default-features --bench calibration_bench --no-run — 20 bench combinations compile
  • bash scripts/verify-calibration-proof.sh → VERDICT: PASS
  • cargo check -p wifi-densepose-cli --no-default-features — green
  • End-to-end CLI binary path validated via synthetic UDP emitter (600 frames, baseline.bin written, header correct)
  • CI: new "ADR-135 calibration witness proof (determinism guard)" step under Rust Workspace Tests

Firmware

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

  • Hardware-in-loop validation used synthetic UDP rather than live ESP32 — re-provisioning the firmware's target-IP is operator follow-up.
  • Drift-triggered auto-recalibration window (900 frames, z>4 sustained) is implemented in the deviation math but the trigger emission/event-bus integration is P2.

🤖 Generated with claude-flow

ruvnet added 2 commits May 28, 2026 18:57
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>
@ruvnet ruvnet merged commit 36db13a into main May 29, 2026
35 of 38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant