Skip to content

Commit d5c457a

Browse files
committed
Add CSI fingerprint DB + night mode detection
Co-Authored-By: claude-flow <ruv@ruv.net>
1 parent b2e3f27 commit d5c457a

File tree

1 file changed

+173
-1
lines changed
  • rust-port/wifi-densepose-rs/crates/wifi-densepose-pointcloud/src

1 file changed

+173
-1
lines changed

rust-port/wifi-densepose-rs/crates/wifi-densepose-pointcloud/src/csi_pipeline.rs

Lines changed: 173 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,17 @@ pub fn parse_adr018(data: &[u8]) -> Option<CsiFrame> {
7171
})
7272
}
7373

74+
// ─── CSI Fingerprint Database ──────────────────────────────────────────────
75+
76+
#[derive(Clone, Debug, serde::Serialize)]
77+
pub struct CsiFingerprint {
78+
pub name: String,
79+
pub mean_amplitudes: Vec<f32>,
80+
pub rssi_mean: f32,
81+
pub rssi_std: f32,
82+
pub samples: u32,
83+
}
84+
7485
// ─── CSI State — accumulates frames for WiFlow + vitals ─────────────────────
7586

7687
#[derive(Clone, Debug)]
@@ -101,6 +112,12 @@ pub struct CsiPipelineState {
101112
pub total_frames: u64,
102113
/// Motion detection
103114
pub motion_detected: bool,
115+
/// CSI fingerprint database for room/location identification
116+
pub fingerprints: Vec<CsiFingerprint>,
117+
/// Current identified location (name, confidence) — updated every 100 frames
118+
pub current_location: Option<(String, f32)>,
119+
/// Night mode — true when camera luminance is below threshold
120+
pub is_dark: bool,
104121
/// WiFlow model weights (loaded once)
105122
wiflow_weights: Option<WiFlowModel>,
106123
}
@@ -127,6 +144,9 @@ impl Default for CsiPipelineState {
127144
occupancy_dims: (8, 8, 4),
128145
total_frames: 0,
129146
motion_detected: false,
147+
fingerprints: Vec::new(),
148+
current_location: None,
149+
is_dark: false,
130150
wiflow_weights: load_wiflow_model(),
131151
}
132152
}
@@ -143,7 +163,7 @@ fn load_wiflow_model() -> Option<WiFlowModel> {
143163
let expanded = p.replace('~', &std::env::var("HOME").unwrap_or_default());
144164
if let Ok(data) = std::fs::read_to_string(&expanded) {
145165
if let Ok(model) = serde_json::from_str::<serde_json::Value>(&data) {
146-
if let Some(weights_b64) = model.get("weightsBase64").and_then(|v| v.as_str()) {
166+
if let Some(_weights_b64) = model.get("weightsBase64").and_then(|v| v.as_str()) {
147167
eprintln!(" WiFlow: loaded from {expanded} ({} params)",
148168
model.get("totalParams").and_then(|v| v.as_u64()).unwrap_or(0));
149169
// For now, use simplified inference — full weight parsing would go here
@@ -193,6 +213,11 @@ impl CsiPipelineState {
193213

194214
// 4. RF tomography (update occupancy grid)
195215
self.update_tomography();
216+
217+
// 5. Location fingerprint identification (every 100 frames)
218+
if self.total_frames % 100 == 0 {
219+
self.current_location = self.identify_location();
220+
}
196221
}
197222

198223
fn detect_motion(&mut self, node_id: u8) {
@@ -297,6 +322,127 @@ impl CsiPipelineState {
297322
}
298323
}
299324

325+
/// Record a CSI fingerprint for the current location/room.
326+
/// Computes mean amplitude and RSSI statistics from the last 50 frames
327+
/// across all nodes and saves as a named fingerprint.
328+
pub fn record_fingerprint(&mut self, name: &str) {
329+
// Collect last 50 frames from all nodes
330+
let mut all_amplitudes: Vec<Vec<f32>> = Vec::new();
331+
let mut rssi_values: Vec<f32> = Vec::new();
332+
333+
for history in self.node_frames.values() {
334+
for frame in history.iter().rev().take(50) {
335+
all_amplitudes.push(frame.amplitudes.clone());
336+
rssi_values.push(frame.rssi as f32);
337+
}
338+
}
339+
340+
if all_amplitudes.is_empty() {
341+
return;
342+
}
343+
344+
// Compute mean amplitude per subcarrier across all collected frames
345+
let n_sub = all_amplitudes.iter().map(|a| a.len()).max().unwrap_or(0);
346+
if n_sub == 0 {
347+
return;
348+
}
349+
let mut mean_amplitudes = vec![0.0f32; n_sub];
350+
let mut counts = vec![0u32; n_sub];
351+
for amps in &all_amplitudes {
352+
for (i, &a) in amps.iter().enumerate() {
353+
if i < n_sub {
354+
mean_amplitudes[i] += a;
355+
counts[i] += 1;
356+
}
357+
}
358+
}
359+
for i in 0..n_sub {
360+
if counts[i] > 0 {
361+
mean_amplitudes[i] /= counts[i] as f32;
362+
}
363+
}
364+
365+
// RSSI statistics
366+
let rssi_mean = rssi_values.iter().sum::<f32>() / rssi_values.len() as f32;
367+
let rssi_var = rssi_values.iter()
368+
.map(|r| (r - rssi_mean).powi(2))
369+
.sum::<f32>() / rssi_values.len() as f32;
370+
let rssi_std = rssi_var.sqrt();
371+
372+
let fingerprint = CsiFingerprint {
373+
name: name.to_string(),
374+
mean_amplitudes,
375+
rssi_mean,
376+
rssi_std,
377+
samples: all_amplitudes.len() as u32,
378+
};
379+
380+
// Replace existing fingerprint with same name, or append
381+
if let Some(existing) = self.fingerprints.iter_mut().find(|f| f.name == name) {
382+
*existing = fingerprint;
383+
} else {
384+
self.fingerprints.push(fingerprint);
385+
}
386+
}
387+
388+
/// Compare current CSI signals against saved fingerprints using cosine
389+
/// similarity. Returns (name, confidence) if the best match exceeds 0.7.
390+
pub fn identify_location(&self) -> Option<(String, f32)> {
391+
if self.fingerprints.is_empty() {
392+
return None;
393+
}
394+
395+
// Build current mean amplitude vector from last 50 frames
396+
let mut all_amplitudes: Vec<Vec<f32>> = Vec::new();
397+
for history in self.node_frames.values() {
398+
for frame in history.iter().rev().take(50) {
399+
all_amplitudes.push(frame.amplitudes.clone());
400+
}
401+
}
402+
if all_amplitudes.is_empty() {
403+
return None;
404+
}
405+
406+
let n_sub = all_amplitudes.iter().map(|a| a.len()).max().unwrap_or(0);
407+
if n_sub == 0 {
408+
return None;
409+
}
410+
let mut current = vec![0.0f32; n_sub];
411+
let mut counts = vec![0u32; n_sub];
412+
for amps in &all_amplitudes {
413+
for (i, &a) in amps.iter().enumerate() {
414+
if i < n_sub {
415+
current[i] += a;
416+
counts[i] += 1;
417+
}
418+
}
419+
}
420+
for i in 0..n_sub {
421+
if counts[i] > 0 {
422+
current[i] /= counts[i] as f32;
423+
}
424+
}
425+
426+
// Find best matching fingerprint by cosine similarity
427+
let mut best: Option<(String, f32)> = None;
428+
for fp in &self.fingerprints {
429+
let sim = cosine_similarity(&current, &fp.mean_amplitudes);
430+
if sim > 0.7 {
431+
if best.as_ref().map_or(true, |(_, s)| sim > *s) {
432+
best = Some((fp.name.clone(), sim));
433+
}
434+
}
435+
}
436+
best
437+
}
438+
439+
/// Set the ambient light level from camera frame average luminance.
440+
/// When luminance < 30 (out of 255), enables night/dark mode which
441+
/// increases CSI processing frequency and skips camera depth.
442+
pub fn set_light_level(&mut self, avg_luminance: f32) {
443+
self.is_dark = avg_luminance < 30.0;
444+
}
445+
300446
fn update_tomography(&mut self) {
301447
let (nx, ny, nz) = self.occupancy_dims;
302448
let total = nx * ny * nz;
@@ -345,6 +491,28 @@ impl CsiPipelineState {
345491
}
346492
}
347493

494+
/// Cosine similarity between two vectors. Returns 0.0 if either has zero magnitude.
495+
fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
496+
let len = a.len().min(b.len());
497+
if len == 0 {
498+
return 0.0;
499+
}
500+
let mut dot = 0.0f32;
501+
let mut mag_a = 0.0f32;
502+
let mut mag_b = 0.0f32;
503+
for i in 0..len {
504+
dot += a[i] * b[i];
505+
mag_a += a[i] * a[i];
506+
mag_b += b[i] * b[i];
507+
}
508+
let denom = mag_a.sqrt() * mag_b.sqrt();
509+
if denom < 1e-9 {
510+
0.0
511+
} else {
512+
dot / denom
513+
}
514+
}
515+
348516
// ─── UDP Receiver ───────────────────────────────────────────────────────────
349517

350518
/// Start the complete CSI pipeline — UDP receiver + processing.
@@ -392,6 +560,8 @@ pub fn get_pipeline_output(state: &Arc<Mutex<CsiPipelineState>>) -> PipelineOutp
392560
motion_detected: st.motion_detected,
393561
total_frames: st.total_frames,
394562
num_nodes: st.node_frames.len(),
563+
current_location: st.current_location.clone(),
564+
is_dark: st.is_dark,
395565
}
396566
}
397567

@@ -404,6 +574,8 @@ pub struct PipelineOutput {
404574
pub motion_detected: bool,
405575
pub total_frames: u64,
406576
pub num_nodes: usize,
577+
pub current_location: Option<(String, f32)>,
578+
pub is_dark: bool,
407579
}
408580

409581
// Serialize implementations

0 commit comments

Comments
 (0)