@@ -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