/ src / agg.rs
agg.rs
  1  use std::collections::{HashMap, HashSet, VecDeque};
  2  use crate::decode::FrameSummary;
  3  use crate::util::MacAddr;
  4  
  5  #[derive(Debug, Clone)]
  6  pub struct ApState {
  7      pub bssid: MacAddr,
  8      pub ssid: Option<String>,
  9      pub channel: Option<u16>,
 10      pub channel_mhz: Option<u16>,
 11      pub last_rssi_dbm: Option<i8>,
 12      pub last_seen_ns: i128,
 13      pub total_frames: u64,
 14      pub beacon_frames: u64,
 15      pub probe_req_frames: u64,
 16      pub probe_resp_frames: u64,
 17  }
 18  
 19  impl ApState {
 20      pub fn new(bssid: MacAddr, ts_ns: i128) -> Self {
 21          Self {
 22              bssid,
 23              ssid: None,
 24              channel: None,
 25              channel_mhz: None,
 26              last_rssi_dbm: None,
 27              last_seen_ns: ts_ns,
 28              total_frames: 0,
 29              beacon_frames: 0,
 30              probe_req_frames: 0,
 31              probe_resp_frames: 0,
 32          }
 33      }
 34  
 35      pub fn update(&mut self, f: &FrameSummary) {
 36          self.last_seen_ns = f.ts_ns;
 37          self.total_frames += 1;
 38          if let Some(rssi) = f.rssi_dbm { self.last_rssi_dbm = Some(rssi); }
 39          if let Some(ch) = f.channel { self.channel = Some(ch); }
 40          if let Some(mhz) = f.channel_mhz { self.channel_mhz = Some(mhz); }
 41          if let Some(ssid) = &f.ssid {
 42              // Keep last non-empty SSID; empty string is "hidden".
 43              if !ssid.is_empty() {
 44                  self.ssid = Some(ssid.clone());
 45              }
 46          }
 47  
 48          if f.dot11_type == Some(0) {
 49              match f.dot11_subtype {
 50                  Some(8) => self.beacon_frames += 1,
 51                  Some(4) => self.probe_req_frames += 1,
 52                  Some(5) => self.probe_resp_frames += 1,
 53                  _ => {}
 54              }
 55          }
 56      }
 57  }
 58  
 59  #[derive(Default)]
 60  pub struct Aggregator {
 61      aps: HashMap<MacAddr, ApState>,
 62      pub timeline: VecDeque<FrameSummary>,
 63      pub max_timeline: usize,
 64      max_aps: usize,
 65  }
 66  
 67  impl Aggregator {
 68      pub fn new(max_timeline: usize, max_aps: usize) -> Self {
 69          Self {
 70              aps: HashMap::new(),
 71              timeline: VecDeque::new(),
 72              max_timeline,
 73              max_aps,
 74          }
 75      }
 76  
 77      pub fn ingest(&mut self, f: FrameSummary) {
 78          if let Some(bssid) = f.bssid {
 79              let entry = self.aps.entry(bssid).or_insert_with(|| ApState::new(bssid, f.ts_ns));
 80              entry.update(&f);
 81              if self.max_aps > 0 && self.aps.len() > self.max_aps {
 82                  self.prune_aps();
 83              }
 84          }
 85          if self.max_timeline == 0 {
 86              return;
 87          }
 88          if self.timeline.len() == self.max_timeline {
 89              self.timeline.pop_front();
 90          }
 91          self.timeline.push_back(f);
 92      }
 93  
 94      pub fn ap_states_sorted(&self) -> Vec<&ApState> {
 95          let mut v: Vec<&ApState> = self.aps.values().collect();
 96          v.sort_by_key(|a| std::cmp::Reverse(a.last_seen_ns));
 97          v
 98      }
 99  
100      fn prune_aps(&mut self) {
101          let mut entries: Vec<(MacAddr, i128)> = self
102              .aps
103              .iter()
104              .map(|(bssid, ap)| (*bssid, ap.last_seen_ns))
105              .collect();
106          entries.sort_by_key(|entry| std::cmp::Reverse(entry.1));
107          let keep: HashSet<MacAddr> = entries
108              .into_iter()
109              .take(self.max_aps)
110              .map(|entry| entry.0)
111              .collect();
112          self.aps.retain(|bssid, _| keep.contains(bssid));
113      }
114  }