153 lines
6.3 KiB
Rust
153 lines
6.3 KiB
Rust
//! Owned, primitive input types for the ADR-262 bridge.
|
|
//!
|
|
//! These deliberately **mirror** the shapes RuView's sensing cycle produces
|
|
//! (the `/ws/sensing` `SensingUpdate` build site at
|
|
//! `wifi-densepose-sensing-server/src/main.rs:~5938` and the `TrustedOutput`
|
|
//! trust state surfaced via `EngineBridge` at `main.rs:~5886`) **without
|
|
//! importing** RuView's internal crates. Keeping the bridge an anti-corruption
|
|
//! layer (ADR-262 §5.4) means it takes owned primitives, not `SensingUpdate`
|
|
//! or `TrustedOutput` directly — so this crate never depends on
|
|
//! `wifi-densepose-sensing-server`.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// The CSI feature scalars RuView publishes on every `/ws/sensing` cycle.
|
|
///
|
|
/// Mirrors `FeatureInfo` (`main.rs:368-377`). All values are in RuView's own
|
|
/// units; the bridge normalizes them into `Observation.features` for fusion.
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct SensingFeatures {
|
|
/// Mean RSSI across the CSI window (dBm).
|
|
pub mean_rssi: f64,
|
|
/// CSI amplitude variance.
|
|
pub variance: f64,
|
|
/// Motion-band spectral power (drives `motion_energy`).
|
|
pub motion_band_power: f64,
|
|
/// Breathing-band spectral power (drives `breathing_band`).
|
|
pub breathing_band_power: f64,
|
|
/// Dominant frequency of the CSI window (Hz).
|
|
pub dominant_freq_hz: f64,
|
|
/// Number of change points detected in the window (drives `transient`).
|
|
pub change_points: usize,
|
|
/// Total spectral power of the window.
|
|
pub spectral_power: f64,
|
|
}
|
|
|
|
/// The RuView classification block. Mirrors `ClassificationInfo`
|
|
/// (`main.rs:379-384`).
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct SensingClass {
|
|
/// Coarse motion level label (e.g. `"none"`, `"low"`, `"high"`).
|
|
pub motion_level: String,
|
|
/// Whether a person is present.
|
|
pub presence: bool,
|
|
/// Classification confidence `0.0..=1.0`.
|
|
pub confidence: f64,
|
|
}
|
|
|
|
/// A RuView signal field — a floor-plane grid of field values. Mirrors
|
|
/// `SignalField` (`main.rs:386-390`). The bridge derives a real position from
|
|
/// the strongest field peak (like `field_localize`) and **never fabricates**
|
|
/// coordinates when this is absent.
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct SignalField {
|
|
/// Grid dimensions `[x, y, z]`.
|
|
pub grid_size: [usize; 3],
|
|
/// Row-major flattened field values; `len() == grid_size.product()`.
|
|
pub values: Vec<f64>,
|
|
}
|
|
|
|
impl SignalField {
|
|
/// Index `[x, y, z]` of the strongest field cell, or `None` if the grid is
|
|
/// empty / all-NaN. This is the honest "strongest field peak" readout that
|
|
/// `field_localize` (`field_localize.rs:16-27`) exposes — **not** calibrated
|
|
/// triangulation.
|
|
#[must_use]
|
|
pub fn peak_cell(&self) -> Option<[i32; 3]> {
|
|
let [nx, ny, nz] = self.grid_size;
|
|
if nx == 0 || ny == 0 || nz == 0 || self.values.is_empty() {
|
|
return None;
|
|
}
|
|
let mut best_idx: Option<usize> = None;
|
|
let mut best_val = f64::NEG_INFINITY;
|
|
for (i, &v) in self.values.iter().enumerate() {
|
|
if v.is_finite() && v > best_val {
|
|
best_val = v;
|
|
best_idx = Some(i);
|
|
}
|
|
}
|
|
let idx = best_idx?;
|
|
// Row-major: idx = ((x * ny) + y) * nz + z.
|
|
let z = idx % nz;
|
|
let y = (idx / nz) % ny;
|
|
let x = idx / (nz * ny);
|
|
Some([x as i32, y as i32, z as i32])
|
|
}
|
|
}
|
|
|
|
/// RuView's effective privacy class (the `effective_class` / privacy byte on
|
|
/// `TrustedOutput`).
|
|
///
|
|
/// This **mirrors** `wifi_densepose_bfld::PrivacyClass` (`bfld/lib.rs:103-116`,
|
|
/// `#[repr(u8)]`) — the four byte-level classes. The byte values are
|
|
/// **deliberately non-monotonic in information content**: `Derived = 1` carries
|
|
/// an identity embedding yet sorts *below* `Anonymous = 2`. The bridge's
|
|
/// `map_privacy` must therefore map by information content, NEVER by byte value
|
|
/// (ADR-262 §3.3 — the central correctness item).
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum RuViewPrivacyClass {
|
|
/// Byte `0` — raw CSI amplitude, local-only.
|
|
Raw,
|
|
/// Byte `1` — derived **identity** features (identity_embedding +
|
|
/// identity_risk_score), LAN-only. The dangerous one (§3.3).
|
|
Derived,
|
|
/// Byte `2` — aggregate occupancy / motion, no identity.
|
|
Anonymous,
|
|
/// Byte `3` — care/regulated: occupancy minus risk score and hash;
|
|
/// raw suppressed.
|
|
Restricted,
|
|
}
|
|
|
|
impl RuViewPrivacyClass {
|
|
/// The raw byte value used by RuView's `#[repr(u8)]` enum
|
|
/// (`bfld/lib.rs:103`). Exposed only so callers can demonstrate the
|
|
/// non-monotonicity trap in tests; the bridge never maps off this byte.
|
|
#[must_use]
|
|
pub fn raw_byte(self) -> u8 {
|
|
match self {
|
|
RuViewPrivacyClass::Raw => 0,
|
|
RuViewPrivacyClass::Derived => 1,
|
|
RuViewPrivacyClass::Anonymous => 2,
|
|
RuViewPrivacyClass::Restricted => 3,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// One sensing cycle, as a bridge input. Mirrors the join of `SensingUpdate`
|
|
/// (features + classification + signal_field) and the `TrustedOutput` trust
|
|
/// state (`trust_class`) that ADR-262 §1.2 / P1 say must be done at the bridge.
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct SensingSnapshot {
|
|
/// Capture time, nanoseconds since Unix epoch (the real `SensingUpdate`
|
|
/// timestamp, ns).
|
|
pub timestamp_ns: u64,
|
|
/// CSI feature scalars (`/ws/sensing` feature set).
|
|
pub features: SensingFeatures,
|
|
/// Classification (motion level / presence / confidence).
|
|
pub classification: SensingClass,
|
|
/// Optional signal field for a real position readout.
|
|
pub signal_field: Option<SignalField>,
|
|
/// RuView's effective privacy class (the source-of-truth, §3.3).
|
|
pub trust_class: RuViewPrivacyClass,
|
|
/// Whether the governed engine demoted this cycle (`TrustedOutput.demoted`).
|
|
/// When `true` the emitted event must be `>= P2` and raw suppressed
|
|
/// (§3.3 / §4 P2 gate (b)).
|
|
pub demoted: bool,
|
|
/// Whether this cycle's identity surface is bound to an enrolled identity
|
|
/// (RuView's `identity_bound`). Promotes `Derived` to P5 when set.
|
|
pub identity_bound: bool,
|
|
/// Stable node id (e.g. `"esp32_room_01"`).
|
|
pub node_id: String,
|
|
}
|