Multi-node calibration silently produces false occupancy
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 94.5k
- Forks
- 12.5k
- Avg merge
- 21h 27m
- Merged PRs (30d)
- 43
Description
Multi-node calibration silently produces false occupancy
Repo: ruvnet/RuView (sensing-server) · Severity: High · Measured: 2026-09-14/15, 3–4 ESP32-C6 nodes
Symptom
A room calibrated with four nodes reports an occupant in an empty room, in every frame.
| capture | bound nodes | eigenvalues | residual threshold | empty room | occupied |
|---|---|---|---|---|---|
| 1 | 4 | 0 | 19.96 | present 12/12 | — |
| 2 | 4 | 0 | 18.90 | present 1162/1162 | — |
| 3 | 1 (node 13) | 5 | 3.65 | absent 604/604 | present 616/616 |
Root cause
maybe_feed_calibration_frame admits every bound radio into the field model:
// The field model remains single-link, but every radio in the room
// binding may contribute bounded observations on the frozen grid.
The model is single-link: one baseline, one set of amplitude offsets. Averaging several
radios' offsets into it flattens the eigenstructure — hence baseline_eigenvalue_count: 0
— leaving only a scalar energy threshold, inflated by the cross-radio spread (19.96 vs 3.65).
person_count_at (main.rs:2232) already documents the scoring half:
// A single-link model may score only the source node it was calibrated
// against. Applying one node's baseline to a different radio creates
// deterministic false occupancy from hardware-specific amplitude offsets.
let bound_source_node_id = (self.calibration_source_node_ids.len() == 1) …
…but it only engages when exactly one node is bound. With more, it falls through to the
shared mixed history — so the guard never fires in the case its own comment describes.
calibration_start accepts a multi-node binding the model cannot honour.
Fix
Restrict the baseline write, not the admission: only the grid-bound radio calls
maybe_feed_calibration; every bound radio still registers as a contributor and is scored
from the bound radio's history. This mirrors the narrowing bootstrap_baseline::store
already applies (vec![binding.source_node_id] as the frozen model source).
Caution for reviewers: gating the whole feed path instead deadlocks the capture —
calibration_source_nodes_missing requires every bound node to contribute before finalize.
Measured: 43,120 frames over 51 minutes stuck in collecting, missing=[12,13,14], no
error surfaced. The accompanying PR carries a regression test for exactly this.
Fixed in #1936.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with calibration_start and maybe_feed_calibration_frame, then read person_count_at in main.rs around line 2232 and compare the source narrowing in bootstrap_baseline::store. The accompanying PR contains the regression test; done means multi-node calibration keeps all bound radios as contributors while writing the single-link baseline only from the grid-bound radio and avoids false occupancy.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- backend, embedded-iot
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 20/100