wifi-densepose/docs/adr/ADR-102-nbvi-subcarrier-sel...

5.4 KiB
Raw Blame History

ADR-102 — NBVI Subcarrier Selection (server-side)

Status: Accepted Date: 2026-05-17 Scope: v2/crates/wifi-densepose-sensing-server/src/main.rs (AmpState.nbvi_*, nbvi_select_top_k).

Context

Each ESP32-S3 CSI frame carries 56 active subcarriers on the HT20 20 MHz channel. The amplitudes per subcarrier have very different SNR depending on frequency-selective fading: in the operator's deployment subcarriers k=6..11 and k=22..26 sit at CV ≈ 6 % when the room is empty, while subcarriers k=38..43 (middle of the band, near the LTF nulls) sit at CV ≈ 11 % — pure channel noise, no information about the room.

ADR-101's classifier computes broadband-mean CV. Averaging over all 56 subcarriers means the noisy ones drag the baseline CV up to 5-7 %. That blunted the motion gates and we had to push them up to 10-22 %, losing sensitivity to subtle motion.

Decisions

D1 — Port Francesco Pace's NBVI to the server (not the FW)

Formula (ESPectre, GPLv3):

NBVI(k) = α · (σ_k / μ_k²) + (1 - α) · (σ_k / μ_k),     α = 0.5
  • σ_k / μ_k² — penalises weak subcarriers (a quiet bin with mean ≈ 0 gets and is filtered out).
  • σ_k / μ_k — standard coefficient of variation; rewards stability.
  • α = 0.5 — empirically balanced (per Pace's α-sweep tests).

Where: in the server, not in FW. Pros: trivial to retune per deployment, no flash cycle, single source of truth across two FW variants we ship (runbot_csi_node and esp32s3_csi_capture). Cons: we lose the ability to only emit selected subcarriers (would save UDP bandwidth) — but at ~25 fps × 56 × 2 bytes = 2.8 KB/s per node, bandwidth isn't a concern.

D2 — Top-K with K = 12

Selected at server boot once nbvi_history has 90+ samples; then re-selected every NBVI_REFRESH_TICKS = 200 calls (~5 s of combined classifier ticks). The selected indices live in AmpState.nbvi_selected.

K=12 matches ESPectre's default. Smaller K = less averaging smoothing; larger K = drags in worse subcarriers.

D3 — Dead-zone gate at 25 % of median mean

Before NBVI scoring, drop any subcarrier whose mean amplitude is below 0.25 × median(all subcarrier means). Guard tones (FW reports amp[0] = 0 for DC), edge bins, and dead frequencies are excluded so they can't "win" with σ/μ² → ∞.

D4 — ESPectre Step 1: quiet-window finder

Naive NBVI ranking over the entire history is biased if a body walked through during the calibration buffer. ADR-102 v2 adds the quiet-window finder from Pace's Step 1:

  1. Slide an AMP_SHORT_WIN=90-sample window across nbvi_history with stride AMP_SHORT_WIN/3 = 30.
  2. For each window, compute the CV of its per-frame broadband mean.
  3. The window with the lowest CV is "quietest".
  4. Per-subcarrier mean and std for NBVI scoring use only that window.

If history is smaller than one window, the whole buffer is used. Stride 30 (overlap of 60) keeps wall-clock cost trivial for 600 frames.

D5 — mean_for_baseline uses FULL broadband, not NBVI

NBVI top-K re-selects between server restarts (different "quietest" window may give different ranking). That made the persisted baseline value incomparable across restarts (see ADR-103 D1). Fix: ADR-101 classifier keeps a parallel short_full ring buffer of FULL broadband means (all non-zero subcarriers, no NBVI filter). When ADR-103's persistent override is active, the baseline-drop check compares full-broadband short window to full-broadband baseline. NBVI subset is still used for CV (motion sensitivity is what NBVI shines at — full broadband mean is just the integral level).

Files Touched

v2/crates/wifi-densepose-sensing-server/src/main.rs
  - struct AmpState
  - nbvi_select_top_k()
  - amp_presence_override() (broadband_mean computation)

Verified Acceptance (operator's deployment, 2026-05-17)

Idle empty-room CV, sensing-server with 2 pps housekeeping ping:

Full 56 subc NBVI top-12
node 1 (rssi -53 dBm) ~5.0 % 3.1 %
node 2 (rssi -67 dBm) ~7.0 % 3.9 %

Reduction 38-44 %. The lower baseline let ADR-101 gates be tightened from 15 % / 30 % down to 10 % / 22 % for moving/active without raising the false-positive rate — subtler motions like waving while sitting near a sensor now trigger.

Open Items

  • Step 3 FP-rate validation — closed in ADR-104 D4 (commit 6212b17e). K ∈ {6,8,10,12,16,20} sweep, smallest-FP wins; ties broken by smallest total-NBVI score.
  • Persist NBVI selectionAMP_BASELINE_OVERRIDE (ADR-103) persists baseline scalar but not the chosen subcarrier indices. After server restart NBVI re-ranks from scratch; in deployments where the channel changes over hours we'd want to re-rank anyway, so for now this is correct, not an open item.
  • FW boot-time NBVI freeze — ESPectre's Pace freezes NBVI for the lifetime of the boot. Trade-off vs our adaptive rolling refresh. Worth exploring if FP rate is a problem in real homes.

References