hive_btle/discovery/
scanner.rs

1// Copyright (c) 2025-2026 (r)evolve - Revolve Team LLC
2// SPDX-License-Identifier: Apache-2.0
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! BLE Scanner for discovering HIVE nodes
17//!
18//! Provides filtering, deduplication, and tracking of discovered HIVE beacons.
19
20#[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/// Default timeout for considering a device "stale" (ms)
36#[cfg(feature = "std")]
37const DEFAULT_DEVICE_TIMEOUT_MS: u64 = 30_000;
38
39/// Minimum interval between processing duplicate beacons from same node (ms)
40#[cfg(feature = "std")]
41const DEDUP_INTERVAL_MS: u64 = 500;
42
43/// Tracked device state
44#[derive(Debug, Clone)]
45pub struct TrackedDevice {
46    /// Last received beacon
47    pub beacon: HiveBeacon,
48    /// Device address
49    pub address: String,
50    /// Last RSSI reading
51    pub rssi: i8,
52    /// RSSI history for averaging (last N readings)
53    pub rssi_history: Vec<i8>,
54    /// When first discovered (monotonic ms timestamp)
55    pub first_seen_ms: u64,
56    /// When last beacon received (monotonic ms timestamp)
57    pub last_seen_ms: u64,
58    /// Estimated distance in meters
59    pub estimated_distance: Option<f32>,
60    /// Whether this device is currently connectable
61    pub connectable: bool,
62}
63
64impl TrackedDevice {
65    /// Create a new tracked device
66    #[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    /// Update with new beacon data
87    #[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        // Keep last 10 RSSI readings for averaging
95        self.rssi_history.push(rssi);
96        if self.rssi_history.len() > 10 {
97            self.rssi_history.remove(0);
98        }
99    }
100
101    /// Get average RSSI
102    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    /// Check if this device is stale (not seen recently)
111    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    /// Get time since first discovery in milliseconds
116    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/// Filter criteria for scanning
122#[derive(Debug, Clone, Default)]
123pub struct ScanFilter {
124    /// Only include HIVE nodes
125    pub hive_only: bool,
126    /// Only include nodes at or above this hierarchy level
127    pub min_hierarchy_level: Option<HierarchyLevel>,
128    /// Only include nodes with these capabilities (bitmask)
129    pub required_capabilities: Option<u16>,
130    /// Exclude nodes with these capabilities
131    pub excluded_capabilities: Option<u16>,
132    /// Minimum RSSI threshold (exclude weaker signals)
133    pub min_rssi: Option<i8>,
134    /// Maximum estimated distance in meters
135    pub max_distance: Option<f32>,
136    /// Only include connectable devices
137    pub connectable_only: bool,
138}
139
140impl ScanFilter {
141    /// Create a filter for HIVE nodes only
142    pub fn hive_nodes() -> Self {
143        Self {
144            hive_only: true,
145            ..Default::default()
146        }
147    }
148
149    /// Create a filter for potential parents (nodes above our level)
150    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    /// Check if a parsed advertisement passes this filter
160    pub fn matches(&self, adv: &ParsedAdvertisement) -> bool {
161        // HIVE-only filter
162        if self.hive_only && !adv.is_hive_device() {
163            return false;
164        }
165
166        // RSSI filter
167        if let Some(min_rssi) = self.min_rssi {
168            if adv.rssi < min_rssi {
169                return false;
170            }
171        }
172
173        // Distance filter
174        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        // Connectable filter
183        if self.connectable_only && !adv.connectable {
184            return false;
185        }
186
187        // Beacon-specific filters
188        if let Some(ref beacon) = adv.beacon {
189            // Hierarchy level filter
190            if let Some(min_level) = self.min_hierarchy_level {
191                if beacon.hierarchy_level < min_level {
192                    return false;
193                }
194            }
195
196            // Required capabilities
197            if let Some(required) = self.required_capabilities {
198                if beacon.capabilities & required != required {
199                    return false;
200                }
201            }
202
203            // Excluded capabilities
204            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/// Scanner state machine
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217pub enum ScannerState {
218    /// Not scanning
219    Idle,
220    /// Actively scanning
221    Scanning,
222    /// Paused (e.g., during connection)
223    Paused,
224}
225
226/// BLE Scanner for discovering HIVE nodes
227///
228/// Handles beacon reception, filtering, deduplication, and device tracking.
229///
230/// Note: This type requires the `std` feature for full functionality.
231#[cfg(feature = "std")]
232pub struct Scanner {
233    /// Scanner configuration (will be used for PHY/power management)
234    #[allow(dead_code)]
235    config: DiscoveryConfig,
236    /// Current state
237    state: ScannerState,
238    /// Tracked devices by node ID
239    devices: HashMap<NodeId, TrackedDevice>,
240    /// Address to node ID mapping (for devices without parsed beacon)
241    address_map: HashMap<String, NodeId>,
242    /// Filter criteria
243    filter: ScanFilter,
244    /// Device timeout (ms)
245    device_timeout_ms: u64,
246    /// Last dedup timestamps per node (ms)
247    last_processed: HashMap<NodeId, u64>,
248    /// Current time (monotonic ms, set externally)
249    current_time_ms: u64,
250    /// Beacon key for decrypting encrypted beacons (optional)
251    beacon_key: Option<BeaconKey>,
252    /// Expected mesh ID bytes for filtering decrypted beacons
253    mesh_id_bytes: Option<[u8; 4]>,
254}
255
256#[cfg(feature = "std")]
257impl Scanner {
258    /// Create a new scanner with default settings
259    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    /// Set the current time (call periodically from platform)
275    pub fn set_time_ms(&mut self, time_ms: u64) {
276        self.current_time_ms = time_ms;
277    }
278
279    /// Set the scan filter
280    pub fn set_filter(&mut self, filter: ScanFilter) {
281        self.filter = filter;
282    }
283
284    /// Set device timeout in milliseconds
285    pub fn set_device_timeout_ms(&mut self, timeout_ms: u64) {
286        self.device_timeout_ms = timeout_ms;
287    }
288
289    /// Configure beacon key for decrypting encrypted advertisements
290    ///
291    /// # Arguments
292    /// * `key` - Beacon encryption key from mesh genesis
293    /// * `mesh_id_bytes` - Expected 4-byte mesh identifier for filtering
294    ///
295    /// When configured, the scanner will attempt to decrypt encrypted beacons
296    /// and only accept those from the specified mesh.
297    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    /// Clear beacon key (stop accepting encrypted beacons)
303    pub fn clear_beacon_key(&mut self) {
304        self.beacon_key = None;
305        self.mesh_id_bytes = None;
306    }
307
308    /// Check if this scanner can decrypt encrypted beacons
309    pub fn can_decrypt_beacons(&self) -> bool {
310        self.beacon_key.is_some() && self.mesh_id_bytes.is_some()
311    }
312
313    /// Get current state
314    pub fn state(&self) -> ScannerState {
315        self.state
316    }
317
318    /// Start scanning
319    pub fn start(&mut self) {
320        self.state = ScannerState::Scanning;
321    }
322
323    /// Pause scanning
324    pub fn pause(&mut self) {
325        self.state = ScannerState::Paused;
326    }
327
328    /// Stop scanning
329    pub fn stop(&mut self) {
330        self.state = ScannerState::Idle;
331    }
332
333    /// Process a received advertisement
334    ///
335    /// Returns true if this is a new or updated device that passes the filter.
336    ///
337    /// Handles both plaintext and encrypted beacons:
338    /// - Plaintext beacons are processed directly from `adv.beacon`
339    /// - Encrypted beacons (in `adv.encrypted_service_data`) are decrypted if a
340    ///   beacon key is configured
341    pub fn process_advertisement(&mut self, adv: ParsedAdvertisement) -> bool {
342        // Apply filter
343        if !self.filter.matches(&adv) {
344            return false;
345        }
346
347        // Extract beacon and node ID - try plaintext first, then encrypted
348        let (beacon, node_id) = if let Some(ref b) = adv.beacon {
349            // Plaintext beacon
350            (b.clone(), b.node_id)
351        } else if let Some(ref encrypted_data) = adv.encrypted_service_data {
352            // Try to decrypt encrypted beacon
353            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, // Decryption failed (wrong mesh or no key)
359            }
360        } else {
361            return false; // No beacon = not a HIVE device
362        };
363
364        // Check deduplication
365        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        // Update or create tracked device
373        let is_new = !self.devices.contains_key(&node_id);
374
375        if let Some(device) = self.devices.get_mut(&node_id) {
376            // Update existing device
377            device.update(beacon, adv.rssi, adv.connectable, self.current_time_ms);
378        } else {
379            // New device
380            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    /// Attempt to decrypt an encrypted beacon
395    ///
396    /// Returns the decrypted beacon and mesh_id if successful.
397    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        // Try to decrypt
402        let (encrypted_beacon, mesh_id) = EncryptedBeacon::decrypt(encrypted_data, key)?;
403
404        // Check mesh ID matches (ensures this is from our mesh)
405        if mesh_id != expected_mesh_id {
406            return None;
407        }
408
409        // Convert EncryptedBeacon to HiveBeacon
410        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, // Not included in encrypted beacon
416            battery_percent: encrypted_beacon.battery_percent,
417            seq_num: 0, // Not included in encrypted beacon
418        };
419
420        Some((beacon, mesh_id))
421    }
422
423    /// Get a tracked device by node ID
424    pub fn get_device(&self, node_id: &NodeId) -> Option<&TrackedDevice> {
425        self.devices.get(node_id)
426    }
427
428    /// Get node ID for an address
429    pub fn get_node_id_for_address(&self, address: &str) -> Option<&NodeId> {
430        self.address_map.get(address)
431    }
432
433    /// Get all tracked devices
434    pub fn devices(&self) -> impl Iterator<Item = &TrackedDevice> {
435        self.devices.values()
436    }
437
438    /// Get devices sorted by RSSI (strongest first)
439    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    /// Get devices sorted by hierarchy level (highest first)
446    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    /// Get count of tracked devices
453    pub fn device_count(&self) -> usize {
454        self.devices.len()
455    }
456
457    /// Remove stale devices
458    ///
459    /// Returns the number of devices removed.
460    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    /// Clear all tracked devices
482    pub fn clear(&mut self) {
483        self.devices.clear();
484        self.address_map.clear();
485        self.last_processed.clear();
486    }
487
488    /// Find the best parent candidate
489    ///
490    /// Selects based on hierarchy level (prefer higher) and RSSI (prefer stronger).
491    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                // First compare hierarchy level
499                match a.beacon.hierarchy_level.cmp(&b.beacon.hierarchy_level) {
500                    core::cmp::Ordering::Equal => {
501                        // Then compare RSSI
502                        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        // Duplicate within dedup interval should be ignored
541        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        // Add a squad leader
588        let squad = make_adv(0x11111111, -60, HierarchyLevel::Squad);
589        scanner.process_advertisement(squad);
590
591        // Add a platoon leader (higher hierarchy)
592        scanner.set_time_ms(501); // Avoid dedup
593        let platoon = make_adv(0x22222222, -70, HierarchyLevel::Platoon);
594        scanner.process_advertisement(platoon);
595
596        // Find parent for platform node
597        let parent = scanner.find_best_parent(HierarchyLevel::Platform);
598        assert!(parent.is_some());
599        // Should prefer platoon (higher hierarchy) despite weaker signal
600        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); // Strongest first
621        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        // Fast forward past timeout
635        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        // Configure scanner for encrypted beacons
654        scanner.set_beacon_key(beacon_key.clone(), mesh_id_bytes);
655        assert!(scanner.can_decrypt_beacons());
656
657        // Create an encrypted beacon
658        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        // Create advertisement with encrypted data
662        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        // Process advertisement
673        assert!(scanner.process_advertisement(adv));
674        assert_eq!(scanner.device_count(), 1);
675
676        // Verify decrypted device
677        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        // Configure scanner for our mesh
698        scanner.set_beacon_key(beacon_key.clone(), our_mesh_id);
699
700        // Create encrypted beacon for a different mesh
701        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        // Should be rejected - wrong mesh
715        assert!(!scanner.process_advertisement(adv));
716        assert_eq!(scanner.device_count(), 0);
717    }
718}