wifi-densepose/docs/adr/ADR-112-multi-ap-signal-fie...

6.7 KiB
Raw Blame History

ADR-112 — Multi-AP signal_field via MultistaticFuser

Status: Accepted Date: 2026-05-17 Scope: v2/crates/wifi-densepose-sensing-server/src/main.rs (signal_field_from_multistatic, two ESP32 vitals call sites). Closes the "Real signal_field via multistatic fusion" Open Item in ADR-105.

Context

ADR-105 D6 stripped the synthetic signal_field paint and left a 20×20 zero grid in its place. The honesty contract was: never emit visual positional output without a physically grounded source. A real multistatic fuser (MultistaticFuser in wifi-densepose-signal) is already wired into the server via multistatic_bridge::fuse_or_fallback and consumed by compute_person_score_from_amplitudes — but its output didn't feed the signal_field heatmap.

This ADR consumes that fusion output to produce a coverage × activity spatial map when ≥ 2 ESP32 nodes are simultaneously active.

What the new map honestly is (and isn't)

  • Is: a 20×20 floor-plane heatmap where each cell value = Σ over active nodes of global_activity · exp(-d²/2σ²), with d the Euclidean distance from the cell to that node's configured position, σ a fixed radius, and global_activity = cv²(fused_amplitude) · cross_node_coherence. Both factors live in [0, 1]; their product gates the field on simultaneous CSI modulation AND inter-node agreement.
  • Is not: a person-location estimate. Commodity ESP32s have no phase-coherent ranging (no UWB, no two-way ranging); any "target position" would be fabrication. The map shows where the active sensors' coverage zones overlap when they collectively see modulation. That's a real, derivable quantity. A "where is the person" claim is not, and is deliberately withheld.

Decisions

D1 — signal_field_from_multistatic(fuser, node_states) -> SignalField

New function in main.rs. Re-runs multistatic_bridge::fuse_or_fallback (cheap — attention-weighted mean across O(N_nodes × N_subcarriers)), discards the count-fallback path, and proceeds only when:

  • fused.active_nodes >= 2, AND
  • fused.node_positions non-empty, AND
  • fused.fused_amplitude non-empty, AND
  • global_activity > 1e-3 (everything below is rounding noise).

Otherwise returns the same zero-filled grid generate_signal_field produces. This preserves ADR-105's contract on single-sensor deployments and degenerate fusion failures.

D2 — Render constants

  • Grid 20 × 1 × 20 (matches the existing SignalField shape and the UI's heatmap consumer).
  • ROOM_EXTENT_M = 3.0 m (half-width of the square the grid spans — 6 m × 6 m floor). Matches the typical "operator room" dimension and the placement of the two physical sensors.
  • SIGMA_M = ROOM_EXTENT_M / 4.0 = 0.75 m for the isotropic Gaussian. Borrowed from Pace's ESPectre heuristic (his code uses ~room/4 for a similar overlap-rendering pass).
  • (grid_x, grid_y) → (x, z) projection — the WiFi sensors live in 3D position space [x, y, z] where y is height, but the heatmap is a floor-plan view, so we ignore y and use (x, z).

D3 — cv² × coherence as the activity scalar

Two factors so that EITHER a quiet channel (low cv²) OR disagreeing sensors (low coherence) collapses the field to zeros. This means:

  • Empty room (low cv²) → blank map. Truthful.
  • One sensor saw a transient (high cv² for one node, low coherence across nodes) → blank map. Truthful — no multistatic signal.
  • All sensors see synchronized modulation → bright map. Truthful — there really is something in the shared coverage.

The product is bounded in [0, 1]; we clamp each cell to [0, 1] post-sum because two overlapping gaussians can sum to > 1 in their shared region.

D4 — Call-site contract: prefer multistatic, else zero

Both ESP32 vitals paths build the field as:

let multi = signal_field_from_multistatic(&s.multistatic_fuser, &s.node_states);
if multi.values.iter().any(|&v| v > 0.0) { multi } else { /* zero */ }

A multi that is all-zero — either because < 2 nodes are active or because the activity threshold wasn't met — gets discarded and the existing generate_signal_field zero is emitted. This keeps the output identical to today's behavior when the multistatic path can't produce signal, so no consumer is surprised.

The Windows WiFi / multi-BSSID paths (windows_wifi_task) are not touched: they have no per-node spatial positions, so the multistatic approach doesn't apply and they keep their zero grid.

Trade-offs

  • Node positions must be configured. The --node-positions CLI flag (SENSING_NODE_POSITIONS env) is the source of truth. If unset, multistatic_fuser has empty positions, so this ADR silently degrades to zero output — no user-visible regression.
  • Coverage map ≠ target map. Operators looking at the heatmap will be tempted to read it as "the person is here." Mitigation: the field is brightest at the nodes themselves, not between them, so the visual signature is "sensor coverage glow," not "blob in the middle of the room." A future ADR (e.g. ADR-115, RF tomography or RSSI MUSIC) could replace this with a real localizer; this ADR is the honest baseline that holds until then.
  • σ is fixed. A room-sized parameter should arguably scale with the inter-node distance, but until we have more than two sensors in one deployment that's premature parameter sprawl. The ROOM_EXTENT_M / SIGMA_M constants are intentionally hard-coded in one place to be easy to find and tune.

Files Touched

v2/crates/wifi-densepose-sensing-server/src/main.rs
  - signal_field_from_multistatic (D1, D2, D3)
  - two vitals-path call sites adopt the prefer-multistatic-else-zero
    contract (D4)

docs/adr/ADR-112-multi-ap-signal-field.md  (this)
docs/adr/ADR-105-no-synthetic-data-in-production-runtime.md
  - close "Real signal_field via multistatic fusion" Open Item

Verified Acceptance

  • cargo build --release -p wifi-densepose-sensing-server clean.
  • cargo test --release -p wifi-densepose-sensing-server --no-default-features — 313 tests pass (no regressions).
  • With one sensor active, signal_field.values are all zero — matches ADR-105 behaviour.
  • With two sensors active and a person moving in shared coverage, the field is non-zero with bright cells overlapping at each sensor's footprint and tapering between them.

References

  • ADR-105 D6 — the "no synthetic signal_field" honesty contract.
  • wifi_densepose_signal::ruvsense::multistatic::MultistaticFuser — the upstream attention-weighted fuser this ADR consumes.
  • multistatic_bridge::fuse_or_fallback — the existing call path this ADR reuses.
  • Francesco Pace, How I Turned My Wi-Fi Into a Motion Sensor — Part 2, "Multi-AP heatmap" — the σ ≈ room/4 heuristic source.