1#[cfg(not(feature = "std"))]
6use alloc::{string::String, vec::Vec};
7#[cfg(feature = "std")]
8use std::collections::HashMap;
9
10#[cfg(feature = "std")]
11use crate::config::DiscoveryConfig;
12use crate::HierarchyLevel;
13#[cfg(feature = "std")]
14use crate::NodeId;
15
16use super::beacon::{HiveBeacon, ParsedAdvertisement};
17
18#[cfg(feature = "std")]
20const DEFAULT_DEVICE_TIMEOUT_MS: u64 = 30_000;
21
22#[cfg(feature = "std")]
24const DEDUP_INTERVAL_MS: u64 = 500;
25
26#[derive(Debug, Clone)]
28pub struct TrackedDevice {
29 pub beacon: HiveBeacon,
31 pub address: String,
33 pub rssi: i8,
35 pub rssi_history: Vec<i8>,
37 pub first_seen_ms: u64,
39 pub last_seen_ms: u64,
41 pub estimated_distance: Option<f32>,
43 pub connectable: bool,
45}
46
47impl TrackedDevice {
48 #[cfg(feature = "std")]
50 fn new(
51 beacon: HiveBeacon,
52 address: String,
53 rssi: i8,
54 connectable: bool,
55 current_time_ms: u64,
56 ) -> Self {
57 Self {
58 beacon,
59 address,
60 rssi,
61 rssi_history: vec![rssi],
62 first_seen_ms: current_time_ms,
63 last_seen_ms: current_time_ms,
64 estimated_distance: None,
65 connectable,
66 }
67 }
68
69 #[cfg(feature = "std")]
71 fn update(&mut self, beacon: HiveBeacon, rssi: i8, connectable: bool, current_time_ms: u64) {
72 self.beacon = beacon;
73 self.rssi = rssi;
74 self.last_seen_ms = current_time_ms;
75 self.connectable = connectable;
76
77 self.rssi_history.push(rssi);
79 if self.rssi_history.len() > 10 {
80 self.rssi_history.remove(0);
81 }
82 }
83
84 pub fn average_rssi(&self) -> i8 {
86 if self.rssi_history.is_empty() {
87 return self.rssi;
88 }
89 let sum: i32 = self.rssi_history.iter().map(|&r| r as i32).sum();
90 (sum / self.rssi_history.len() as i32) as i8
91 }
92
93 pub fn is_stale(&self, timeout_ms: u64, current_time_ms: u64) -> bool {
95 current_time_ms.saturating_sub(self.last_seen_ms) > timeout_ms
96 }
97
98 pub fn time_tracked_ms(&self, current_time_ms: u64) -> u64 {
100 current_time_ms.saturating_sub(self.first_seen_ms)
101 }
102}
103
104#[derive(Debug, Clone, Default)]
106pub struct ScanFilter {
107 pub hive_only: bool,
109 pub min_hierarchy_level: Option<HierarchyLevel>,
111 pub required_capabilities: Option<u16>,
113 pub excluded_capabilities: Option<u16>,
115 pub min_rssi: Option<i8>,
117 pub max_distance: Option<f32>,
119 pub connectable_only: bool,
121}
122
123impl ScanFilter {
124 pub fn hive_nodes() -> Self {
126 Self {
127 hive_only: true,
128 ..Default::default()
129 }
130 }
131
132 pub fn potential_parents(our_level: HierarchyLevel) -> Self {
134 Self {
135 hive_only: true,
136 min_hierarchy_level: Some(our_level),
137 connectable_only: true,
138 ..Default::default()
139 }
140 }
141
142 pub fn matches(&self, adv: &ParsedAdvertisement) -> bool {
144 if self.hive_only && !adv.is_hive_device() {
146 return false;
147 }
148
149 if let Some(min_rssi) = self.min_rssi {
151 if adv.rssi < min_rssi {
152 return false;
153 }
154 }
155
156 if let Some(max_distance) = self.max_distance {
158 if let Some(distance) = adv.estimated_distance_meters() {
159 if distance > max_distance {
160 return false;
161 }
162 }
163 }
164
165 if self.connectable_only && !adv.connectable {
167 return false;
168 }
169
170 if let Some(ref beacon) = adv.beacon {
172 if let Some(min_level) = self.min_hierarchy_level {
174 if beacon.hierarchy_level < min_level {
175 return false;
176 }
177 }
178
179 if let Some(required) = self.required_capabilities {
181 if beacon.capabilities & required != required {
182 return false;
183 }
184 }
185
186 if let Some(excluded) = self.excluded_capabilities {
188 if beacon.capabilities & excluded != 0 {
189 return false;
190 }
191 }
192 }
193
194 true
195 }
196}
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200pub enum ScannerState {
201 Idle,
203 Scanning,
205 Paused,
207}
208
209#[cfg(feature = "std")]
215pub struct Scanner {
216 #[allow(dead_code)]
218 config: DiscoveryConfig,
219 state: ScannerState,
221 devices: HashMap<NodeId, TrackedDevice>,
223 address_map: HashMap<String, NodeId>,
225 filter: ScanFilter,
227 device_timeout_ms: u64,
229 last_processed: HashMap<NodeId, u64>,
231 current_time_ms: u64,
233}
234
235#[cfg(feature = "std")]
236impl Scanner {
237 pub fn new(config: DiscoveryConfig) -> Self {
239 Self {
240 config,
241 state: ScannerState::Idle,
242 devices: HashMap::new(),
243 address_map: HashMap::new(),
244 filter: ScanFilter::default(),
245 device_timeout_ms: DEFAULT_DEVICE_TIMEOUT_MS,
246 last_processed: HashMap::new(),
247 current_time_ms: 0,
248 }
249 }
250
251 pub fn set_time_ms(&mut self, time_ms: u64) {
253 self.current_time_ms = time_ms;
254 }
255
256 pub fn set_filter(&mut self, filter: ScanFilter) {
258 self.filter = filter;
259 }
260
261 pub fn set_device_timeout_ms(&mut self, timeout_ms: u64) {
263 self.device_timeout_ms = timeout_ms;
264 }
265
266 pub fn state(&self) -> ScannerState {
268 self.state
269 }
270
271 pub fn start(&mut self) {
273 self.state = ScannerState::Scanning;
274 }
275
276 pub fn pause(&mut self) {
278 self.state = ScannerState::Paused;
279 }
280
281 pub fn stop(&mut self) {
283 self.state = ScannerState::Idle;
284 }
285
286 pub fn process_advertisement(&mut self, adv: ParsedAdvertisement) -> bool {
290 if !self.filter.matches(&adv) {
292 return false;
293 }
294
295 let (beacon, node_id) = match adv.beacon {
297 Some(ref b) => (b.clone(), b.node_id),
298 None => return false, };
300
301 if let Some(&last) = self.last_processed.get(&node_id) {
303 if self.current_time_ms.saturating_sub(last) < DEDUP_INTERVAL_MS {
304 return false;
305 }
306 }
307 self.last_processed.insert(node_id, self.current_time_ms);
308
309 let is_new = !self.devices.contains_key(&node_id);
311
312 if let Some(device) = self.devices.get_mut(&node_id) {
313 device.update(beacon, adv.rssi, adv.connectable, self.current_time_ms);
315 } else {
316 let device = TrackedDevice::new(
318 beacon,
319 adv.address.clone(),
320 adv.rssi,
321 adv.connectable,
322 self.current_time_ms,
323 );
324 self.devices.insert(node_id, device);
325 self.address_map.insert(adv.address, node_id);
326 }
327
328 is_new
329 }
330
331 pub fn get_device(&self, node_id: &NodeId) -> Option<&TrackedDevice> {
333 self.devices.get(node_id)
334 }
335
336 pub fn get_node_id_for_address(&self, address: &str) -> Option<&NodeId> {
338 self.address_map.get(address)
339 }
340
341 pub fn devices(&self) -> impl Iterator<Item = &TrackedDevice> {
343 self.devices.values()
344 }
345
346 pub fn devices_by_rssi(&self) -> Vec<&TrackedDevice> {
348 let mut devices: Vec<_> = self.devices.values().collect();
349 devices.sort_by(|a, b| b.rssi.cmp(&a.rssi));
350 devices
351 }
352
353 pub fn devices_by_hierarchy(&self) -> Vec<&TrackedDevice> {
355 let mut devices: Vec<_> = self.devices.values().collect();
356 devices.sort_by(|a, b| b.beacon.hierarchy_level.cmp(&a.beacon.hierarchy_level));
357 devices
358 }
359
360 pub fn device_count(&self) -> usize {
362 self.devices.len()
363 }
364
365 pub fn remove_stale(&mut self) -> usize {
369 let timeout = self.device_timeout_ms;
370 let current_time = self.current_time_ms;
371 let stale: Vec<NodeId> = self
372 .devices
373 .iter()
374 .filter(|(_, d)| d.is_stale(timeout, current_time))
375 .map(|(id, _)| *id)
376 .collect();
377
378 let count = stale.len();
379 for node_id in stale {
380 if let Some(device) = self.devices.remove(&node_id) {
381 self.address_map.remove(&device.address);
382 self.last_processed.remove(&node_id);
383 }
384 }
385
386 count
387 }
388
389 pub fn clear(&mut self) {
391 self.devices.clear();
392 self.address_map.clear();
393 self.last_processed.clear();
394 }
395
396 pub fn find_best_parent(&self, our_level: HierarchyLevel) -> Option<&TrackedDevice> {
400 self.devices
401 .values()
402 .filter(|d| {
403 d.beacon.hierarchy_level > our_level && d.connectable && !d.beacon.is_lite_node()
404 })
405 .max_by(|a, b| {
406 match a.beacon.hierarchy_level.cmp(&b.beacon.hierarchy_level) {
408 core::cmp::Ordering::Equal => {
409 a.average_rssi().cmp(&b.average_rssi())
411 }
412 other => other,
413 }
414 })
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421
422 fn make_adv(node_id: u32, rssi: i8, level: HierarchyLevel) -> ParsedAdvertisement {
423 let beacon = HiveBeacon::new(NodeId::new(node_id))
424 .with_hierarchy_level(level)
425 .with_battery(80);
426
427 ParsedAdvertisement {
428 address: format!("00:11:22:33:44:{:02X}", node_id as u8),
429 rssi,
430 beacon: Some(beacon),
431 local_name: Some(format!("HIVE-{:08X}", node_id)),
432 tx_power: Some(0),
433 connectable: true,
434 }
435 }
436
437 #[test]
438 fn test_scanner_process_advertisement() {
439 let config = DiscoveryConfig::default();
440 let mut scanner = Scanner::new(config);
441 scanner.set_time_ms(1000);
442
443 let adv = make_adv(0x12345678, -60, HierarchyLevel::Platform);
444 assert!(scanner.process_advertisement(adv));
445 assert_eq!(scanner.device_count(), 1);
446
447 scanner.set_time_ms(1100);
449 let adv2 = make_adv(0x12345678, -65, HierarchyLevel::Platform);
450 assert!(!scanner.process_advertisement(adv2));
451 assert_eq!(scanner.device_count(), 1);
452 }
453
454 #[test]
455 fn test_scan_filter_hive_only() {
456 let filter = ScanFilter::hive_nodes();
457
458 let hive_adv = make_adv(0x12345678, -60, HierarchyLevel::Platform);
459 assert!(filter.matches(&hive_adv));
460
461 let non_hive = ParsedAdvertisement {
462 address: "AA:BB:CC:DD:EE:FF".to_string(),
463 rssi: -50,
464 beacon: None,
465 local_name: Some("Other Device".to_string()),
466 tx_power: None,
467 connectable: true,
468 };
469 assert!(!filter.matches(&non_hive));
470 }
471
472 #[test]
473 fn test_scan_filter_rssi() {
474 let filter = ScanFilter {
475 hive_only: true,
476 min_rssi: Some(-70),
477 ..Default::default()
478 };
479
480 let strong = make_adv(0x11111111, -60, HierarchyLevel::Platform);
481 assert!(filter.matches(&strong));
482
483 let weak = make_adv(0x22222222, -80, HierarchyLevel::Platform);
484 assert!(!filter.matches(&weak));
485 }
486
487 #[test]
488 fn test_find_best_parent() {
489 let config = DiscoveryConfig::default();
490 let mut scanner = Scanner::new(config);
491 scanner.set_time_ms(0);
492
493 let squad = make_adv(0x11111111, -60, HierarchyLevel::Squad);
495 scanner.process_advertisement(squad);
496
497 scanner.set_time_ms(501); let platoon = make_adv(0x22222222, -70, HierarchyLevel::Platoon);
500 scanner.process_advertisement(platoon);
501
502 let parent = scanner.find_best_parent(HierarchyLevel::Platform);
504 assert!(parent.is_some());
505 assert_eq!(
507 parent.unwrap().beacon.hierarchy_level,
508 HierarchyLevel::Platoon
509 );
510 }
511
512 #[test]
513 fn test_devices_by_rssi() {
514 let config = DiscoveryConfig::default();
515 let mut scanner = Scanner::new(config);
516 scanner.set_time_ms(0);
517
518 scanner.process_advertisement(make_adv(0x11111111, -80, HierarchyLevel::Platform));
519 scanner.set_time_ms(501);
520 scanner.process_advertisement(make_adv(0x22222222, -50, HierarchyLevel::Platform));
521 scanner.set_time_ms(1002);
522 scanner.process_advertisement(make_adv(0x33333333, -70, HierarchyLevel::Platform));
523
524 let sorted = scanner.devices_by_rssi();
525 assert_eq!(sorted.len(), 3);
526 assert_eq!(sorted[0].rssi, -50); assert_eq!(sorted[1].rssi, -70);
528 assert_eq!(sorted[2].rssi, -80);
529 }
530
531 #[test]
532 fn test_remove_stale() {
533 let config = DiscoveryConfig::default();
534 let mut scanner = Scanner::new(config);
535 scanner.set_time_ms(0);
536
537 scanner.process_advertisement(make_adv(0x11111111, -60, HierarchyLevel::Platform));
538 assert_eq!(scanner.device_count(), 1);
539
540 scanner.set_time_ms(35_000);
542 let removed = scanner.remove_stale();
543 assert_eq!(removed, 1);
544 assert_eq!(scanner.device_count(), 0);
545 }
546}