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#[cfg(feature = "std")]
33use super::encrypted_beacon::{BeaconKey, EncryptedBeacon};
34
35#[cfg(feature = "std")]
37const DEFAULT_DEVICE_TIMEOUT_MS: u64 = 30_000;
38
39#[cfg(feature = "std")]
41const DEDUP_INTERVAL_MS: u64 = 500;
42
43#[derive(Debug, Clone)]
45pub struct TrackedDevice {
46 pub beacon: HiveBeacon,
48 pub address: String,
50 pub rssi: i8,
52 pub rssi_history: Vec<i8>,
54 pub first_seen_ms: u64,
56 pub last_seen_ms: u64,
58 pub estimated_distance: Option<f32>,
60 pub connectable: bool,
62}
63
64impl TrackedDevice {
65 #[cfg(feature = "std")]
67 fn new(
68 beacon: HiveBeacon,
69 address: String,
70 rssi: i8,
71 connectable: bool,
72 current_time_ms: u64,
73 ) -> Self {
74 Self {
75 beacon,
76 address,
77 rssi,
78 rssi_history: vec![rssi],
79 first_seen_ms: current_time_ms,
80 last_seen_ms: current_time_ms,
81 estimated_distance: None,
82 connectable,
83 }
84 }
85
86 #[cfg(feature = "std")]
88 fn update(&mut self, beacon: HiveBeacon, rssi: i8, connectable: bool, current_time_ms: u64) {
89 self.beacon = beacon;
90 self.rssi = rssi;
91 self.last_seen_ms = current_time_ms;
92 self.connectable = connectable;
93
94 self.rssi_history.push(rssi);
96 if self.rssi_history.len() > 10 {
97 self.rssi_history.remove(0);
98 }
99 }
100
101 pub fn average_rssi(&self) -> i8 {
103 if self.rssi_history.is_empty() {
104 return self.rssi;
105 }
106 let sum: i32 = self.rssi_history.iter().map(|&r| r as i32).sum();
107 (sum / self.rssi_history.len() as i32) as i8
108 }
109
110 pub fn is_stale(&self, timeout_ms: u64, current_time_ms: u64) -> bool {
112 current_time_ms.saturating_sub(self.last_seen_ms) > timeout_ms
113 }
114
115 pub fn time_tracked_ms(&self, current_time_ms: u64) -> u64 {
117 current_time_ms.saturating_sub(self.first_seen_ms)
118 }
119}
120
121#[derive(Debug, Clone, Default)]
123pub struct ScanFilter {
124 pub hive_only: bool,
126 pub min_hierarchy_level: Option<HierarchyLevel>,
128 pub required_capabilities: Option<u16>,
130 pub excluded_capabilities: Option<u16>,
132 pub min_rssi: Option<i8>,
134 pub max_distance: Option<f32>,
136 pub connectable_only: bool,
138}
139
140impl ScanFilter {
141 pub fn hive_nodes() -> Self {
143 Self {
144 hive_only: true,
145 ..Default::default()
146 }
147 }
148
149 pub fn potential_parents(our_level: HierarchyLevel) -> Self {
151 Self {
152 hive_only: true,
153 min_hierarchy_level: Some(our_level),
154 connectable_only: true,
155 ..Default::default()
156 }
157 }
158
159 pub fn matches(&self, adv: &ParsedAdvertisement) -> bool {
161 if self.hive_only && !adv.is_hive_device() {
163 return false;
164 }
165
166 if let Some(min_rssi) = self.min_rssi {
168 if adv.rssi < min_rssi {
169 return false;
170 }
171 }
172
173 if let Some(max_distance) = self.max_distance {
175 if let Some(distance) = adv.estimated_distance_meters() {
176 if distance > max_distance {
177 return false;
178 }
179 }
180 }
181
182 if self.connectable_only && !adv.connectable {
184 return false;
185 }
186
187 if let Some(ref beacon) = adv.beacon {
189 if let Some(min_level) = self.min_hierarchy_level {
191 if beacon.hierarchy_level < min_level {
192 return false;
193 }
194 }
195
196 if let Some(required) = self.required_capabilities {
198 if beacon.capabilities & required != required {
199 return false;
200 }
201 }
202
203 if let Some(excluded) = self.excluded_capabilities {
205 if beacon.capabilities & excluded != 0 {
206 return false;
207 }
208 }
209 }
210
211 true
212 }
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217pub enum ScannerState {
218 Idle,
220 Scanning,
222 Paused,
224}
225
226#[cfg(feature = "std")]
232pub struct Scanner {
233 #[allow(dead_code)]
235 config: DiscoveryConfig,
236 state: ScannerState,
238 devices: HashMap<NodeId, TrackedDevice>,
240 address_map: HashMap<String, NodeId>,
242 filter: ScanFilter,
244 device_timeout_ms: u64,
246 last_processed: HashMap<NodeId, u64>,
248 current_time_ms: u64,
250 beacon_key: Option<BeaconKey>,
252 mesh_id_bytes: Option<[u8; 4]>,
254}
255
256#[cfg(feature = "std")]
257impl Scanner {
258 pub fn new(config: DiscoveryConfig) -> Self {
260 Self {
261 config,
262 state: ScannerState::Idle,
263 devices: HashMap::new(),
264 address_map: HashMap::new(),
265 filter: ScanFilter::default(),
266 device_timeout_ms: DEFAULT_DEVICE_TIMEOUT_MS,
267 last_processed: HashMap::new(),
268 current_time_ms: 0,
269 beacon_key: None,
270 mesh_id_bytes: None,
271 }
272 }
273
274 pub fn set_time_ms(&mut self, time_ms: u64) {
276 self.current_time_ms = time_ms;
277 }
278
279 pub fn set_filter(&mut self, filter: ScanFilter) {
281 self.filter = filter;
282 }
283
284 pub fn set_device_timeout_ms(&mut self, timeout_ms: u64) {
286 self.device_timeout_ms = timeout_ms;
287 }
288
289 pub fn set_beacon_key(&mut self, key: BeaconKey, mesh_id_bytes: [u8; 4]) {
298 self.beacon_key = Some(key);
299 self.mesh_id_bytes = Some(mesh_id_bytes);
300 }
301
302 pub fn clear_beacon_key(&mut self) {
304 self.beacon_key = None;
305 self.mesh_id_bytes = None;
306 }
307
308 pub fn can_decrypt_beacons(&self) -> bool {
310 self.beacon_key.is_some() && self.mesh_id_bytes.is_some()
311 }
312
313 pub fn state(&self) -> ScannerState {
315 self.state
316 }
317
318 pub fn start(&mut self) {
320 self.state = ScannerState::Scanning;
321 }
322
323 pub fn pause(&mut self) {
325 self.state = ScannerState::Paused;
326 }
327
328 pub fn stop(&mut self) {
330 self.state = ScannerState::Idle;
331 }
332
333 pub fn process_advertisement(&mut self, adv: ParsedAdvertisement) -> bool {
342 if !self.filter.matches(&adv) {
344 return false;
345 }
346
347 let (beacon, node_id) = if let Some(ref b) = adv.beacon {
349 (b.clone(), b.node_id)
351 } else if let Some(ref encrypted_data) = adv.encrypted_service_data {
352 match self.try_decrypt_beacon(encrypted_data) {
354 Some((decrypted_beacon, _mesh_id)) => {
355 let node_id = decrypted_beacon.node_id;
356 (decrypted_beacon, node_id)
357 }
358 None => return false, }
360 } else {
361 return false; };
363
364 if let Some(&last) = self.last_processed.get(&node_id) {
366 if self.current_time_ms.saturating_sub(last) < DEDUP_INTERVAL_MS {
367 return false;
368 }
369 }
370 self.last_processed.insert(node_id, self.current_time_ms);
371
372 let is_new = !self.devices.contains_key(&node_id);
374
375 if let Some(device) = self.devices.get_mut(&node_id) {
376 device.update(beacon, adv.rssi, adv.connectable, self.current_time_ms);
378 } else {
379 let device = TrackedDevice::new(
381 beacon,
382 adv.address.clone(),
383 adv.rssi,
384 adv.connectable,
385 self.current_time_ms,
386 );
387 self.devices.insert(node_id, device);
388 self.address_map.insert(adv.address, node_id);
389 }
390
391 is_new
392 }
393
394 fn try_decrypt_beacon(&self, encrypted_data: &[u8]) -> Option<(HiveBeacon, [u8; 4])> {
398 let key = self.beacon_key.as_ref()?;
399 let expected_mesh_id = self.mesh_id_bytes?;
400
401 let (encrypted_beacon, mesh_id) = EncryptedBeacon::decrypt(encrypted_data, key)?;
403
404 if mesh_id != expected_mesh_id {
406 return None;
407 }
408
409 let beacon = HiveBeacon {
411 version: 1,
412 capabilities: encrypted_beacon.capabilities,
413 node_id: encrypted_beacon.node_id,
414 hierarchy_level: HierarchyLevel::from(encrypted_beacon.hierarchy_level),
415 geohash: 0, battery_percent: encrypted_beacon.battery_percent,
417 seq_num: 0, };
419
420 Some((beacon, mesh_id))
421 }
422
423 pub fn get_device(&self, node_id: &NodeId) -> Option<&TrackedDevice> {
425 self.devices.get(node_id)
426 }
427
428 pub fn get_node_id_for_address(&self, address: &str) -> Option<&NodeId> {
430 self.address_map.get(address)
431 }
432
433 pub fn devices(&self) -> impl Iterator<Item = &TrackedDevice> {
435 self.devices.values()
436 }
437
438 pub fn devices_by_rssi(&self) -> Vec<&TrackedDevice> {
440 let mut devices: Vec<_> = self.devices.values().collect();
441 devices.sort_by(|a, b| b.rssi.cmp(&a.rssi));
442 devices
443 }
444
445 pub fn devices_by_hierarchy(&self) -> Vec<&TrackedDevice> {
447 let mut devices: Vec<_> = self.devices.values().collect();
448 devices.sort_by(|a, b| b.beacon.hierarchy_level.cmp(&a.beacon.hierarchy_level));
449 devices
450 }
451
452 pub fn device_count(&self) -> usize {
454 self.devices.len()
455 }
456
457 pub fn remove_stale(&mut self) -> usize {
461 let timeout = self.device_timeout_ms;
462 let current_time = self.current_time_ms;
463 let stale: Vec<NodeId> = self
464 .devices
465 .iter()
466 .filter(|(_, d)| d.is_stale(timeout, current_time))
467 .map(|(id, _)| *id)
468 .collect();
469
470 let count = stale.len();
471 for node_id in stale {
472 if let Some(device) = self.devices.remove(&node_id) {
473 self.address_map.remove(&device.address);
474 self.last_processed.remove(&node_id);
475 }
476 }
477
478 count
479 }
480
481 pub fn clear(&mut self) {
483 self.devices.clear();
484 self.address_map.clear();
485 self.last_processed.clear();
486 }
487
488 pub fn find_best_parent(&self, our_level: HierarchyLevel) -> Option<&TrackedDevice> {
492 self.devices
493 .values()
494 .filter(|d| {
495 d.beacon.hierarchy_level > our_level && d.connectable && !d.beacon.is_lite_node()
496 })
497 .max_by(|a, b| {
498 match a.beacon.hierarchy_level.cmp(&b.beacon.hierarchy_level) {
500 core::cmp::Ordering::Equal => {
501 a.average_rssi().cmp(&b.average_rssi())
503 }
504 other => other,
505 }
506 })
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513
514 fn make_adv(node_id: u32, rssi: i8, level: HierarchyLevel) -> ParsedAdvertisement {
515 let beacon = HiveBeacon::new(NodeId::new(node_id))
516 .with_hierarchy_level(level)
517 .with_battery(80);
518
519 ParsedAdvertisement {
520 address: format!("00:11:22:33:44:{:02X}", node_id as u8),
521 rssi,
522 beacon: Some(beacon),
523 encrypted_service_data: None,
524 local_name: Some(format!("HIVE-{:08X}", node_id)),
525 tx_power: Some(0),
526 connectable: true,
527 }
528 }
529
530 #[test]
531 fn test_scanner_process_advertisement() {
532 let config = DiscoveryConfig::default();
533 let mut scanner = Scanner::new(config);
534 scanner.set_time_ms(1000);
535
536 let adv = make_adv(0x12345678, -60, HierarchyLevel::Platform);
537 assert!(scanner.process_advertisement(adv));
538 assert_eq!(scanner.device_count(), 1);
539
540 scanner.set_time_ms(1100);
542 let adv2 = make_adv(0x12345678, -65, HierarchyLevel::Platform);
543 assert!(!scanner.process_advertisement(adv2));
544 assert_eq!(scanner.device_count(), 1);
545 }
546
547 #[test]
548 fn test_scan_filter_hive_only() {
549 let filter = ScanFilter::hive_nodes();
550
551 let hive_adv = make_adv(0x12345678, -60, HierarchyLevel::Platform);
552 assert!(filter.matches(&hive_adv));
553
554 let non_hive = ParsedAdvertisement {
555 address: "AA:BB:CC:DD:EE:FF".to_string(),
556 rssi: -50,
557 beacon: None,
558 encrypted_service_data: None,
559 local_name: Some("Other Device".to_string()),
560 tx_power: None,
561 connectable: true,
562 };
563 assert!(!filter.matches(&non_hive));
564 }
565
566 #[test]
567 fn test_scan_filter_rssi() {
568 let filter = ScanFilter {
569 hive_only: true,
570 min_rssi: Some(-70),
571 ..Default::default()
572 };
573
574 let strong = make_adv(0x11111111, -60, HierarchyLevel::Platform);
575 assert!(filter.matches(&strong));
576
577 let weak = make_adv(0x22222222, -80, HierarchyLevel::Platform);
578 assert!(!filter.matches(&weak));
579 }
580
581 #[test]
582 fn test_find_best_parent() {
583 let config = DiscoveryConfig::default();
584 let mut scanner = Scanner::new(config);
585 scanner.set_time_ms(0);
586
587 let squad = make_adv(0x11111111, -60, HierarchyLevel::Squad);
589 scanner.process_advertisement(squad);
590
591 scanner.set_time_ms(501); let platoon = make_adv(0x22222222, -70, HierarchyLevel::Platoon);
594 scanner.process_advertisement(platoon);
595
596 let parent = scanner.find_best_parent(HierarchyLevel::Platform);
598 assert!(parent.is_some());
599 assert_eq!(
601 parent.unwrap().beacon.hierarchy_level,
602 HierarchyLevel::Platoon
603 );
604 }
605
606 #[test]
607 fn test_devices_by_rssi() {
608 let config = DiscoveryConfig::default();
609 let mut scanner = Scanner::new(config);
610 scanner.set_time_ms(0);
611
612 scanner.process_advertisement(make_adv(0x11111111, -80, HierarchyLevel::Platform));
613 scanner.set_time_ms(501);
614 scanner.process_advertisement(make_adv(0x22222222, -50, HierarchyLevel::Platform));
615 scanner.set_time_ms(1002);
616 scanner.process_advertisement(make_adv(0x33333333, -70, HierarchyLevel::Platform));
617
618 let sorted = scanner.devices_by_rssi();
619 assert_eq!(sorted.len(), 3);
620 assert_eq!(sorted[0].rssi, -50); assert_eq!(sorted[1].rssi, -70);
622 assert_eq!(sorted[2].rssi, -80);
623 }
624
625 #[test]
626 fn test_remove_stale() {
627 let config = DiscoveryConfig::default();
628 let mut scanner = Scanner::new(config);
629 scanner.set_time_ms(0);
630
631 scanner.process_advertisement(make_adv(0x11111111, -60, HierarchyLevel::Platform));
632 assert_eq!(scanner.device_count(), 1);
633
634 scanner.set_time_ms(35_000);
636 let removed = scanner.remove_stale();
637 assert_eq!(removed, 1);
638 assert_eq!(scanner.device_count(), 0);
639 }
640
641 #[test]
642 fn test_encrypted_beacon_scanning() {
643 use crate::discovery::{mesh_id_to_bytes, EncryptedBeacon as EB};
644
645 let config = DiscoveryConfig::default();
646 let mut scanner = Scanner::new(config);
647 scanner.set_time_ms(0);
648
649 let beacon_key = BeaconKey::from_base(&[0x42; 32]);
650 let mesh_id_bytes = mesh_id_to_bytes("TEST-MESH");
651 let node_id = NodeId::new(0x12345678);
652
653 scanner.set_beacon_key(beacon_key.clone(), mesh_id_bytes);
655 assert!(scanner.can_decrypt_beacons());
656
657 let encrypted_beacon = EB::new(node_id, 0x0F00, u8::from(HierarchyLevel::Squad), 85);
659 let encrypted_data = encrypted_beacon.encrypt(&beacon_key, &mesh_id_bytes);
660
661 let adv = ParsedAdvertisement {
663 address: "00:11:22:33:44:55".to_string(),
664 rssi: -60,
665 beacon: None,
666 encrypted_service_data: Some(encrypted_data),
667 local_name: Some("HIVE".to_string()),
668 tx_power: None,
669 connectable: true,
670 };
671
672 assert!(scanner.process_advertisement(adv));
674 assert_eq!(scanner.device_count(), 1);
675
676 let device = scanner.get_device(&node_id).unwrap();
678 assert_eq!(device.beacon.node_id, node_id);
679 assert_eq!(device.beacon.capabilities, 0x0F00);
680 assert_eq!(device.beacon.hierarchy_level, HierarchyLevel::Squad);
681 assert_eq!(device.beacon.battery_percent, 85);
682 }
683
684 #[test]
685 fn test_encrypted_beacon_wrong_mesh_rejected() {
686 use crate::discovery::{mesh_id_to_bytes, EncryptedBeacon as EB};
687
688 let config = DiscoveryConfig::default();
689 let mut scanner = Scanner::new(config);
690 scanner.set_time_ms(0);
691
692 let beacon_key = BeaconKey::from_base(&[0x42; 32]);
693 let our_mesh_id = mesh_id_to_bytes("OUR-MESH");
694 let other_mesh_id = mesh_id_to_bytes("OTHER-MESH");
695 let node_id = NodeId::new(0x12345678);
696
697 scanner.set_beacon_key(beacon_key.clone(), our_mesh_id);
699
700 let encrypted_beacon = EB::new(node_id, 0x0F00, u8::from(HierarchyLevel::Squad), 85);
702 let encrypted_data = encrypted_beacon.encrypt(&beacon_key, &other_mesh_id);
703
704 let adv = ParsedAdvertisement {
705 address: "00:11:22:33:44:55".to_string(),
706 rssi: -60,
707 beacon: None,
708 encrypted_service_data: Some(encrypted_data),
709 local_name: Some("HIVE".to_string()),
710 tx_power: None,
711 connectable: true,
712 };
713
714 assert!(!scanner.process_advertisement(adv));
716 assert_eq!(scanner.device_count(), 0);
717 }
718}