1#[cfg(not(feature = "std"))]
21use alloc::{string::String, vec::Vec};
22#[cfg(feature = "std")]
23use std::collections::HashMap;
24
25#[cfg(feature = "std")]
26use crate::config::DiscoveryConfig;
27use crate::HierarchyLevel;
28#[cfg(feature = "std")]
29use crate::NodeId;
30
31use super::beacon::{HiveBeacon, ParsedAdvertisement};
32
33#[cfg(feature = "std")]
35const DEFAULT_DEVICE_TIMEOUT_MS: u64 = 30_000;
36
37#[cfg(feature = "std")]
39const DEDUP_INTERVAL_MS: u64 = 500;
40
41#[derive(Debug, Clone)]
43pub struct TrackedDevice {
44 pub beacon: HiveBeacon,
46 pub address: String,
48 pub rssi: i8,
50 pub rssi_history: Vec<i8>,
52 pub first_seen_ms: u64,
54 pub last_seen_ms: u64,
56 pub estimated_distance: Option<f32>,
58 pub connectable: bool,
60}
61
62impl TrackedDevice {
63 #[cfg(feature = "std")]
65 fn new(
66 beacon: HiveBeacon,
67 address: String,
68 rssi: i8,
69 connectable: bool,
70 current_time_ms: u64,
71 ) -> Self {
72 Self {
73 beacon,
74 address,
75 rssi,
76 rssi_history: vec![rssi],
77 first_seen_ms: current_time_ms,
78 last_seen_ms: current_time_ms,
79 estimated_distance: None,
80 connectable,
81 }
82 }
83
84 #[cfg(feature = "std")]
86 fn update(&mut self, beacon: HiveBeacon, rssi: i8, connectable: bool, current_time_ms: u64) {
87 self.beacon = beacon;
88 self.rssi = rssi;
89 self.last_seen_ms = current_time_ms;
90 self.connectable = connectable;
91
92 self.rssi_history.push(rssi);
94 if self.rssi_history.len() > 10 {
95 self.rssi_history.remove(0);
96 }
97 }
98
99 pub fn average_rssi(&self) -> i8 {
101 if self.rssi_history.is_empty() {
102 return self.rssi;
103 }
104 let sum: i32 = self.rssi_history.iter().map(|&r| r as i32).sum();
105 (sum / self.rssi_history.len() as i32) as i8
106 }
107
108 pub fn is_stale(&self, timeout_ms: u64, current_time_ms: u64) -> bool {
110 current_time_ms.saturating_sub(self.last_seen_ms) > timeout_ms
111 }
112
113 pub fn time_tracked_ms(&self, current_time_ms: u64) -> u64 {
115 current_time_ms.saturating_sub(self.first_seen_ms)
116 }
117}
118
119#[derive(Debug, Clone, Default)]
121pub struct ScanFilter {
122 pub hive_only: bool,
124 pub min_hierarchy_level: Option<HierarchyLevel>,
126 pub required_capabilities: Option<u16>,
128 pub excluded_capabilities: Option<u16>,
130 pub min_rssi: Option<i8>,
132 pub max_distance: Option<f32>,
134 pub connectable_only: bool,
136}
137
138impl ScanFilter {
139 pub fn hive_nodes() -> Self {
141 Self {
142 hive_only: true,
143 ..Default::default()
144 }
145 }
146
147 pub fn potential_parents(our_level: HierarchyLevel) -> Self {
149 Self {
150 hive_only: true,
151 min_hierarchy_level: Some(our_level),
152 connectable_only: true,
153 ..Default::default()
154 }
155 }
156
157 pub fn matches(&self, adv: &ParsedAdvertisement) -> bool {
159 if self.hive_only && !adv.is_hive_device() {
161 return false;
162 }
163
164 if let Some(min_rssi) = self.min_rssi {
166 if adv.rssi < min_rssi {
167 return false;
168 }
169 }
170
171 if let Some(max_distance) = self.max_distance {
173 if let Some(distance) = adv.estimated_distance_meters() {
174 if distance > max_distance {
175 return false;
176 }
177 }
178 }
179
180 if self.connectable_only && !adv.connectable {
182 return false;
183 }
184
185 if let Some(ref beacon) = adv.beacon {
187 if let Some(min_level) = self.min_hierarchy_level {
189 if beacon.hierarchy_level < min_level {
190 return false;
191 }
192 }
193
194 if let Some(required) = self.required_capabilities {
196 if beacon.capabilities & required != required {
197 return false;
198 }
199 }
200
201 if let Some(excluded) = self.excluded_capabilities {
203 if beacon.capabilities & excluded != 0 {
204 return false;
205 }
206 }
207 }
208
209 true
210 }
211}
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
215pub enum ScannerState {
216 Idle,
218 Scanning,
220 Paused,
222}
223
224#[cfg(feature = "std")]
230pub struct Scanner {
231 #[allow(dead_code)]
233 config: DiscoveryConfig,
234 state: ScannerState,
236 devices: HashMap<NodeId, TrackedDevice>,
238 address_map: HashMap<String, NodeId>,
240 filter: ScanFilter,
242 device_timeout_ms: u64,
244 last_processed: HashMap<NodeId, u64>,
246 current_time_ms: u64,
248}
249
250#[cfg(feature = "std")]
251impl Scanner {
252 pub fn new(config: DiscoveryConfig) -> Self {
254 Self {
255 config,
256 state: ScannerState::Idle,
257 devices: HashMap::new(),
258 address_map: HashMap::new(),
259 filter: ScanFilter::default(),
260 device_timeout_ms: DEFAULT_DEVICE_TIMEOUT_MS,
261 last_processed: HashMap::new(),
262 current_time_ms: 0,
263 }
264 }
265
266 pub fn set_time_ms(&mut self, time_ms: u64) {
268 self.current_time_ms = time_ms;
269 }
270
271 pub fn set_filter(&mut self, filter: ScanFilter) {
273 self.filter = filter;
274 }
275
276 pub fn set_device_timeout_ms(&mut self, timeout_ms: u64) {
278 self.device_timeout_ms = timeout_ms;
279 }
280
281 pub fn state(&self) -> ScannerState {
283 self.state
284 }
285
286 pub fn start(&mut self) {
288 self.state = ScannerState::Scanning;
289 }
290
291 pub fn pause(&mut self) {
293 self.state = ScannerState::Paused;
294 }
295
296 pub fn stop(&mut self) {
298 self.state = ScannerState::Idle;
299 }
300
301 pub fn process_advertisement(&mut self, adv: ParsedAdvertisement) -> bool {
305 if !self.filter.matches(&adv) {
307 return false;
308 }
309
310 let (beacon, node_id) = match adv.beacon {
312 Some(ref b) => (b.clone(), b.node_id),
313 None => return false, };
315
316 if let Some(&last) = self.last_processed.get(&node_id) {
318 if self.current_time_ms.saturating_sub(last) < DEDUP_INTERVAL_MS {
319 return false;
320 }
321 }
322 self.last_processed.insert(node_id, self.current_time_ms);
323
324 let is_new = !self.devices.contains_key(&node_id);
326
327 if let Some(device) = self.devices.get_mut(&node_id) {
328 device.update(beacon, adv.rssi, adv.connectable, self.current_time_ms);
330 } else {
331 let device = TrackedDevice::new(
333 beacon,
334 adv.address.clone(),
335 adv.rssi,
336 adv.connectable,
337 self.current_time_ms,
338 );
339 self.devices.insert(node_id, device);
340 self.address_map.insert(adv.address, node_id);
341 }
342
343 is_new
344 }
345
346 pub fn get_device(&self, node_id: &NodeId) -> Option<&TrackedDevice> {
348 self.devices.get(node_id)
349 }
350
351 pub fn get_node_id_for_address(&self, address: &str) -> Option<&NodeId> {
353 self.address_map.get(address)
354 }
355
356 pub fn devices(&self) -> impl Iterator<Item = &TrackedDevice> {
358 self.devices.values()
359 }
360
361 pub fn devices_by_rssi(&self) -> Vec<&TrackedDevice> {
363 let mut devices: Vec<_> = self.devices.values().collect();
364 devices.sort_by(|a, b| b.rssi.cmp(&a.rssi));
365 devices
366 }
367
368 pub fn devices_by_hierarchy(&self) -> Vec<&TrackedDevice> {
370 let mut devices: Vec<_> = self.devices.values().collect();
371 devices.sort_by(|a, b| b.beacon.hierarchy_level.cmp(&a.beacon.hierarchy_level));
372 devices
373 }
374
375 pub fn device_count(&self) -> usize {
377 self.devices.len()
378 }
379
380 pub fn remove_stale(&mut self) -> usize {
384 let timeout = self.device_timeout_ms;
385 let current_time = self.current_time_ms;
386 let stale: Vec<NodeId> = self
387 .devices
388 .iter()
389 .filter(|(_, d)| d.is_stale(timeout, current_time))
390 .map(|(id, _)| *id)
391 .collect();
392
393 let count = stale.len();
394 for node_id in stale {
395 if let Some(device) = self.devices.remove(&node_id) {
396 self.address_map.remove(&device.address);
397 self.last_processed.remove(&node_id);
398 }
399 }
400
401 count
402 }
403
404 pub fn clear(&mut self) {
406 self.devices.clear();
407 self.address_map.clear();
408 self.last_processed.clear();
409 }
410
411 pub fn find_best_parent(&self, our_level: HierarchyLevel) -> Option<&TrackedDevice> {
415 self.devices
416 .values()
417 .filter(|d| {
418 d.beacon.hierarchy_level > our_level && d.connectable && !d.beacon.is_lite_node()
419 })
420 .max_by(|a, b| {
421 match a.beacon.hierarchy_level.cmp(&b.beacon.hierarchy_level) {
423 core::cmp::Ordering::Equal => {
424 a.average_rssi().cmp(&b.average_rssi())
426 }
427 other => other,
428 }
429 })
430 }
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436
437 fn make_adv(node_id: u32, rssi: i8, level: HierarchyLevel) -> ParsedAdvertisement {
438 let beacon = HiveBeacon::new(NodeId::new(node_id))
439 .with_hierarchy_level(level)
440 .with_battery(80);
441
442 ParsedAdvertisement {
443 address: format!("00:11:22:33:44:{:02X}", node_id as u8),
444 rssi,
445 beacon: Some(beacon),
446 local_name: Some(format!("HIVE-{:08X}", node_id)),
447 tx_power: Some(0),
448 connectable: true,
449 }
450 }
451
452 #[test]
453 fn test_scanner_process_advertisement() {
454 let config = DiscoveryConfig::default();
455 let mut scanner = Scanner::new(config);
456 scanner.set_time_ms(1000);
457
458 let adv = make_adv(0x12345678, -60, HierarchyLevel::Platform);
459 assert!(scanner.process_advertisement(adv));
460 assert_eq!(scanner.device_count(), 1);
461
462 scanner.set_time_ms(1100);
464 let adv2 = make_adv(0x12345678, -65, HierarchyLevel::Platform);
465 assert!(!scanner.process_advertisement(adv2));
466 assert_eq!(scanner.device_count(), 1);
467 }
468
469 #[test]
470 fn test_scan_filter_hive_only() {
471 let filter = ScanFilter::hive_nodes();
472
473 let hive_adv = make_adv(0x12345678, -60, HierarchyLevel::Platform);
474 assert!(filter.matches(&hive_adv));
475
476 let non_hive = ParsedAdvertisement {
477 address: "AA:BB:CC:DD:EE:FF".to_string(),
478 rssi: -50,
479 beacon: None,
480 local_name: Some("Other Device".to_string()),
481 tx_power: None,
482 connectable: true,
483 };
484 assert!(!filter.matches(&non_hive));
485 }
486
487 #[test]
488 fn test_scan_filter_rssi() {
489 let filter = ScanFilter {
490 hive_only: true,
491 min_rssi: Some(-70),
492 ..Default::default()
493 };
494
495 let strong = make_adv(0x11111111, -60, HierarchyLevel::Platform);
496 assert!(filter.matches(&strong));
497
498 let weak = make_adv(0x22222222, -80, HierarchyLevel::Platform);
499 assert!(!filter.matches(&weak));
500 }
501
502 #[test]
503 fn test_find_best_parent() {
504 let config = DiscoveryConfig::default();
505 let mut scanner = Scanner::new(config);
506 scanner.set_time_ms(0);
507
508 let squad = make_adv(0x11111111, -60, HierarchyLevel::Squad);
510 scanner.process_advertisement(squad);
511
512 scanner.set_time_ms(501); let platoon = make_adv(0x22222222, -70, HierarchyLevel::Platoon);
515 scanner.process_advertisement(platoon);
516
517 let parent = scanner.find_best_parent(HierarchyLevel::Platform);
519 assert!(parent.is_some());
520 assert_eq!(
522 parent.unwrap().beacon.hierarchy_level,
523 HierarchyLevel::Platoon
524 );
525 }
526
527 #[test]
528 fn test_devices_by_rssi() {
529 let config = DiscoveryConfig::default();
530 let mut scanner = Scanner::new(config);
531 scanner.set_time_ms(0);
532
533 scanner.process_advertisement(make_adv(0x11111111, -80, HierarchyLevel::Platform));
534 scanner.set_time_ms(501);
535 scanner.process_advertisement(make_adv(0x22222222, -50, HierarchyLevel::Platform));
536 scanner.set_time_ms(1002);
537 scanner.process_advertisement(make_adv(0x33333333, -70, HierarchyLevel::Platform));
538
539 let sorted = scanner.devices_by_rssi();
540 assert_eq!(sorted.len(), 3);
541 assert_eq!(sorted[0].rssi, -50); assert_eq!(sorted[1].rssi, -70);
543 assert_eq!(sorted[2].rssi, -80);
544 }
545
546 #[test]
547 fn test_remove_stale() {
548 let config = DiscoveryConfig::default();
549 let mut scanner = Scanner::new(config);
550 scanner.set_time_ms(0);
551
552 scanner.process_advertisement(make_adv(0x11111111, -60, HierarchyLevel::Platform));
553 assert_eq!(scanner.device_count(), 1);
554
555 scanner.set_time_ms(35_000);
557 let removed = scanner.remove_stale();
558 assert_eq!(removed, 1);
559 assert_eq!(scanner.device_count(), 0);
560 }
561}