fix(sensing-server): don't let edge-vitals sentinel RSSI poison mean_rssi (#1882)

The ESP32 edge-vitals packet (0xC511_0002) carries a raw i8 RSSI byte with
no "valid" flag. When the edge pipeline hasn't sampled a real reading yet,
it has been observed sending near-zero sentinel values (-1, -2 dBm) that
are physically implausible for a WiFi link. These were pushed straight
into the node's rssi_history and the room's fused mean_rssi, producing a
one-tick reading of ~-2 dBm on node 3 while its own CSI path simultaneously
showed a real ~-50 dBm signal.

Add an is_plausible_rssi() guard (mirroring the existing
update_csi_fps_ema() plausibility-filter pattern) so an implausible RSSI
falls back to the node's last known-good reading instead of overwriting it.
This commit is contained in:
rUv authored and GitHub committed 2026-09-09 20:10:06 +00:00
1 parent bd6f38e2df
commit 00a42d7cda
1 file changed
+58 -5
@@ -819,6 +819,51 @@ mod debounce_room_classification_tests {
}
}
/// Upper plausibility ceiling (dBm) for a real 2.4/5 GHz WiFi RSSI reading.
///
/// A received WiFi signal is always attenuated by free-space path loss and
/// receiver noise floor; no deployed node has ever measured better than
/// roughly -20 dBm. The ESP32 edge-vitals packet (magic 0xC511_0002,
/// ADR-039) carries a raw `i8` RSSI byte with no "valid" flag, and has been
/// observed sending near-zero sentinel values (e.g. -1, -2 dBm) when the
/// edge pipeline hasn't sampled a real reading yet. Left unguarded, that
/// sentinel overwrote the node's `rssi_history` and the room's fused
/// `mean_rssi` for the tick it arrived on (found 2026-09-09: node 3
/// reporting -2.0 dBm while simultaneously showing a real ~-50 dBm CSI
/// reading on the same node).
pub(crate) const MAX_PLAUSIBLE_RSSI_DBM: i8 = -10;
/// Returns `true` if `rssi_dbm` is a physically plausible WiFi RSSI
/// reading (see [`MAX_PLAUSIBLE_RSSI_DBM`]).
pub(crate) fn is_plausible_rssi(rssi_dbm: i8) -> bool {
rssi_dbm <= MAX_PLAUSIBLE_RSSI_DBM
}
#[cfg(test)]
mod rssi_plausibility_tests {
use super::{is_plausible_rssi, MAX_PLAUSIBLE_RSSI_DBM};
#[test]
fn realistic_readings_are_plausible() {
assert!(is_plausible_rssi(-42));
assert!(is_plausible_rssi(-53));
assert!(is_plausible_rssi(-90));
}
#[test]
fn near_zero_sentinel_values_are_rejected() {
assert!(!is_plausible_rssi(-1));
assert!(!is_plausible_rssi(-2));
assert!(!is_plausible_rssi(0));
}
#[test]
fn boundary_is_inclusive() {
assert!(is_plausible_rssi(MAX_PLAUSIBLE_RSSI_DBM));
assert!(!is_plausible_rssi(MAX_PLAUSIBLE_RSSI_DBM + 1));
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SignalField {
grid_size: [usize; 3],
@@ -8889,10 +8934,18 @@ async fn udp_receiver_task(
warn!(name: semconv::EVENT_RUVIEW_FALL_DETECTED, { "ruview.node.id" = node_id }, "fall detected by node {node_id}");
}
ns.edge_vitals = Some(vitals.clone());
ns.rssi_history.push_back(vitals.rssi as f64);
if ns.rssi_history.len() > 60 {
ns.rssi_history.pop_front();
}
// An implausible RSSI (e.g. -1/-2 dBm, see
// `is_plausible_rssi`) is a firmware sentinel, not a
// measurement — don't let it clobber the node's history.
let mean_rssi_dbm = if is_plausible_rssi(vitals.rssi) {
ns.rssi_history.push_back(vitals.rssi as f64);
if ns.rssi_history.len() > 60 {
ns.rssi_history.pop_front();
}
vitals.rssi as f64
} else {
ns.rssi_history.back().copied().unwrap_or(0.0)
};
// Store per-node person count from edge vitals.
let node_est = if vitals.presence {
@@ -9010,7 +9063,7 @@ async fn udp_receiver_task(
);
let features = FeatureInfo {
mean_rssi: vitals.rssi as f64,
mean_rssi: mean_rssi_dbm,
variance: vitals.motion_energy as f64,
motion_band_power: vitals.motion_energy as f64,
breathing_band_power: if vitals.presence { 0.5 } else { 0.0 },