Fix antenna-regime presence over-read: de-saturate motion_score + correct CSI header offsets#923
Fix antenna-regime presence over-read: de-saturate motion_score + correct CSI header offsets#923williammalone wants to merge 1 commit into
Conversation
…rect CSI header offsets After external IPEX antennas were added to the ESP32-S3 mesh nodes, a confirmed-empty room read "present" indefinitely. Two root-cause bugs: 1. motion_score saturation. `variance_motion` and `mbp_motion` used fixed divisors (/10, /25) calibrated for the antenna-less regime. Antennas raised amplitudes ~5x and these amplitude^2 energies ~30x, pinning both terms at the 1.0 clamp — so raw_motion could not fall near the presence floor and the adaptive baseline subtraction in smooth_and_classify was defeated. Normalize both by signal power (mean_amp^2) — the same dimensionless sqrt-of-power-ratio form already used by temporal_motion_score — making motion_score amplitude-scale-invariant. This fixes the single shared extract_features_from_frame used by BOTH the aggregate and the per-node paths, so room-level presence benefits too. (csi.rs carries the identical change in its dead mirror copy to keep the two in sync.) 2. parse_esp32_frame header offsets were 2 bytes early vs the firmware layout (csi_collector.c csi_serialize_frame: seq @ [12..16), rssi @ [16], noise_floor @ [17]). rssi was decoded from sequence-counter byte 2 — which stays 0 for the first 65,536 frames — yielding an impossible rssi=0 dBm that zeroed the RSSI fusion weights and the SNR-based signal_quality. The I/Q payload at byte 20 was already correct (CSI_HEADER_SIZE == 20), so amplitude-derived features were unaffected. Adds regression tests asserting motion_score is amplitude-scale-invariant and that a quiet high-amplitude signal does not saturate. Full binary suite green (103 tests). Validated live on the 2-node mesh: RSSI now reports real values (-28..-74 dBm, was 0) and an empty room now produces genuine low-motion frames. A residual over-read remains (real multi-subcarrier CSI reads elevated even when empty) — that intrinsic empty-vs- still ambiguity needs a learned reference (adaptive classifier retrain), tracked separately. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Reviewed — both fixes are correct. 👍 CSI header offsets: verified against the firmware serializer on
The old parser read motion_score de-saturation: normalizing Heads-up — needs a rebase (my fault). GitHub now shows this LGTM to merge once rebased. |
Summary
Fixes a presence over-read that appeared on an ESP32-S3 CSI mesh after external IPEX antennas were added: a confirmed-empty room read "present" indefinitely. Two independent root-cause bugs in
wifi-densepose-sensing-server:1.
motion_scoresaturation (presence path never migrated off fixed divisors)variance_motionandmbp_motioninextract_features_from_frameused hardcoded divisors (/10,/25) tuned for the antenna-less regime. Antennas raised amplitudes ~5× and these amplitude² energies ~30×, pinning both terms at the1.0clamp. A saturated term carries no information, soraw_motioncould never fall near the presence floor and the adaptive baseline subtraction insmooth_and_classifywas defeated.Fix: normalize both by signal power (
mean_amp²) — the same dimensionlesssqrt-of-power-ratio form already used bytemporal_motion_scorein the same function — makingmotion_scoreamplitude-scale-invariant. The dominanttemporal_motion_scoreterm already did this, which is why only the other two terms saturated. This fixes the single sharedextract_features_from_frameused by both the aggregate and per-node classification paths.2.
parse_esp32_frameheader offsets off by 2 bytes vs firmwareThe parser read
sequence/rssi/noise_floortwo bytes early relative to the firmware layout (firmware/esp32-csi-node/main/csi_collector.c→csi_serialize_frame:seq @ [12..16),rssi @ [16],noise_floor @ [17]).rssiwas decoded from sequence-counter byte 2 — which stays0for the first 65,536 frames — yielding an impossiblerssi = 0 dBmthat zeroed the RSSI fusion weights and the SNR-basedsignal_quality. The I/Q payload at byte 20 (CSI_HEADER_SIZE) was already correct, so amplitude-derived features were unaffected.Tests
motion_scoreis amplitude-scale-invariant (1× vs 5×), and a quiet high-amplitude signal does not saturate while a moving one scores higher (dynamic range preserved).Validation (live, 2-node mesh, real CSI)
rssi=0sentinel is gone.mbp ~4), impossible before — the ceiling clamp previously made every frame look like ~180.Known residual (tracked separately)
Genuine multi-subcarrier CSI still reads elevated (
mbp ~180) in an empty room — intrinsic antenna multipath noise where empty-but-noisy and motionless-person overlap. This needs a learned empty-room reference (adaptive-classifier retrain on de-saturated features), not threshold math. These two fixes are the necessary prerequisite for that work.🤖 Generated with Claude Code