hive_btle/mesh/
manager.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//! Mesh Manager
17//!
18//! Manages the mesh topology, connections, and provides parent failover.
19
20#[cfg(not(feature = "std"))]
21use alloc::{boxed::Box, collections::BTreeMap, vec::Vec};
22#[cfg(feature = "std")]
23use std::collections::HashMap;
24
25use core::sync::atomic::{AtomicUsize, Ordering};
26
27#[cfg(feature = "std")]
28use std::sync::RwLock;
29
30use crate::discovery::HiveBeacon;
31use crate::error::{BleError, Result};
32use crate::{HierarchyLevel, NodeId};
33
34use super::topology::{
35    ConnectionState, DisconnectReason, MeshTopology, ParentCandidate, PeerInfo, PeerRole,
36    TopologyConfig, TopologyEvent,
37};
38
39/// Callback type for topology events
40pub type TopologyCallback = Box<dyn Fn(&TopologyEvent) + Send + Sync>;
41
42/// Mesh manager state
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44pub enum ManagerState {
45    /// Not started
46    #[default]
47    Stopped,
48    /// Starting up
49    Starting,
50    /// Running and managing topology
51    Running,
52    /// In parent failover mode
53    Failover,
54    /// Stopping
55    Stopping,
56}
57
58/// Manages the BLE mesh topology
59///
60/// Responsible for:
61/// - Tracking parent/child/peer connections
62/// - Parent selection and failover
63/// - Connection lifecycle management
64/// - Publishing topology events
65#[cfg(feature = "std")]
66pub struct MeshManager {
67    /// Our node ID
68    node_id: NodeId,
69    /// Our hierarchy level
70    my_level: HierarchyLevel,
71    /// Configuration
72    config: TopologyConfig,
73    /// Current topology state
74    topology: RwLock<MeshTopology>,
75    /// Connected peer info
76    peers: RwLock<HashMap<NodeId, PeerInfo>>,
77    /// Parent candidates from beacons
78    candidates: RwLock<Vec<ParentCandidate>>,
79    /// Current state
80    state: RwLock<ManagerState>,
81    /// Event callbacks
82    callbacks: RwLock<Vec<TopologyCallback>>,
83    /// Monotonic time in milliseconds (for testing without system time)
84    /// Using AtomicUsize for 32-bit platform compatibility (ESP32)
85    current_time_ms: AtomicUsize,
86}
87
88#[cfg(feature = "std")]
89impl MeshManager {
90    /// Create a new mesh manager
91    pub fn new(node_id: NodeId, my_level: HierarchyLevel, config: TopologyConfig) -> Self {
92        let topology = MeshTopology::new(my_level, config.max_children, config.max_connections);
93
94        Self {
95            node_id,
96            my_level,
97            config,
98            topology: RwLock::new(topology),
99            peers: RwLock::new(HashMap::new()),
100            candidates: RwLock::new(Vec::new()),
101            state: RwLock::new(ManagerState::Stopped),
102            callbacks: RwLock::new(Vec::new()),
103            current_time_ms: AtomicUsize::new(0),
104        }
105    }
106
107    /// Get our node ID
108    pub fn node_id(&self) -> &NodeId {
109        &self.node_id
110    }
111
112    /// Get our hierarchy level
113    pub fn my_level(&self) -> HierarchyLevel {
114        self.my_level
115    }
116
117    /// Get current state
118    pub fn state(&self) -> ManagerState {
119        *self.state.read().unwrap()
120    }
121
122    /// Start the mesh manager
123    pub fn start(&self) -> Result<()> {
124        let mut state = self.state.write().unwrap();
125        match *state {
126            ManagerState::Stopped => {
127                *state = ManagerState::Running;
128                Ok(())
129            }
130            _ => Err(BleError::InvalidState("Already started".into())),
131        }
132    }
133
134    /// Stop the mesh manager
135    pub fn stop(&self) -> Result<()> {
136        let mut state = self.state.write().unwrap();
137        *state = ManagerState::Stopped;
138
139        // Clear topology
140        let mut topology = self.topology.write().unwrap();
141        topology.parent = None;
142        topology.children.clear();
143        topology.peers.clear();
144
145        // Clear peers
146        self.peers.write().unwrap().clear();
147
148        // Clear candidates
149        self.candidates.write().unwrap().clear();
150
151        Ok(())
152    }
153
154    /// Register a callback for topology events
155    pub fn on_topology_event(&self, callback: TopologyCallback) {
156        self.callbacks.write().unwrap().push(callback);
157    }
158
159    /// Emit a topology event to all listeners
160    fn emit_event(&self, event: TopologyEvent) {
161        let callbacks = self.callbacks.read().unwrap();
162        for callback in callbacks.iter() {
163            callback(&event);
164        }
165    }
166
167    /// Set the current time (for testing or embedded without RTC)
168    /// Note: Uses usize internally for 32-bit platform compatibility
169    pub fn set_time_ms(&self, time_ms: u64) {
170        self.current_time_ms
171            .store(time_ms as usize, Ordering::SeqCst);
172    }
173
174    /// Get the current time
175    /// Note: Returns u64 but internally stored as usize for 32-bit compatibility
176    pub fn time_ms(&self) -> u64 {
177        self.current_time_ms.load(Ordering::SeqCst) as u64
178    }
179
180    /// Get a snapshot of the current topology
181    pub fn topology(&self) -> MeshTopology {
182        self.topology.read().unwrap().clone()
183    }
184
185    /// Check if we have a parent
186    pub fn has_parent(&self) -> bool {
187        self.topology.read().unwrap().has_parent()
188    }
189
190    /// Get our parent's node ID
191    pub fn parent(&self) -> Option<NodeId> {
192        self.topology.read().unwrap().parent
193    }
194
195    /// Get list of children
196    pub fn children(&self) -> Vec<NodeId> {
197        self.topology.read().unwrap().children.clone()
198    }
199
200    /// Get number of children
201    pub fn child_count(&self) -> usize {
202        self.topology.read().unwrap().children.len()
203    }
204
205    /// Check if we can accept more children
206    pub fn can_accept_child(&self) -> bool {
207        self.topology.read().unwrap().can_accept_child()
208    }
209
210    /// Get all connected peer IDs
211    pub fn connected_peers(&self) -> Vec<NodeId> {
212        self.topology.read().unwrap().all_connected()
213    }
214
215    /// Get peer info for a node
216    pub fn get_peer_info(&self, node_id: &NodeId) -> Option<PeerInfo> {
217        self.peers.read().unwrap().get(node_id).cloned()
218    }
219
220    /// Process a beacon from a discovered node
221    ///
222    /// This updates our list of potential parents
223    pub fn process_beacon(&self, beacon: &HiveBeacon, rssi: i8) {
224        // Only consider nodes at higher hierarchy levels as parent candidates
225        if beacon.hierarchy_level > self.my_level {
226            let candidate = ParentCandidate {
227                node_id: beacon.node_id,
228                level: beacon.hierarchy_level,
229                rssi,
230                age_ms: 0,
231                failure_count: self
232                    .peers
233                    .read()
234                    .unwrap()
235                    .get(&beacon.node_id)
236                    .map(|p| p.failure_count)
237                    .unwrap_or(0),
238            };
239
240            let mut candidates = self.candidates.write().unwrap();
241
242            // Update existing or add new
243            if let Some(existing) = candidates.iter_mut().find(|c| c.node_id == beacon.node_id) {
244                existing.rssi = rssi;
245                existing.age_ms = 0;
246                existing.level = beacon.hierarchy_level;
247            } else {
248                candidates.push(candidate);
249            }
250        }
251    }
252
253    /// Select best parent from candidates
254    ///
255    /// Returns the best candidate based on RSSI, age, and failure history
256    pub fn select_best_parent(&self) -> Option<ParentCandidate> {
257        let candidates = self.candidates.read().unwrap();
258
259        candidates
260            .iter()
261            .filter(|c| {
262                c.rssi >= self.config.min_parent_rssi
263                    && c.age_ms <= self.config.max_beacon_age_ms
264                    && c.failure_count < self.config.max_failures
265            })
266            .max_by_key(|c| c.score(self.my_level))
267            .cloned()
268    }
269
270    /// Connect to a node as our parent
271    pub fn connect_parent(&self, node_id: NodeId, level: HierarchyLevel, rssi: i8) -> Result<()> {
272        let mut topology = self.topology.write().unwrap();
273
274        if topology.has_parent() {
275            return Err(BleError::InvalidState("Already have a parent".into()));
276        }
277
278        if !topology.set_parent(node_id) {
279            return Err(BleError::ConnectionFailed(
280                "Cannot accept connection".into(),
281            ));
282        }
283
284        // Add peer info
285        let mut peer_info = PeerInfo::new(node_id, PeerRole::Parent, level);
286        peer_info.state = ConnectionState::Connected;
287        peer_info.rssi = Some(rssi);
288        peer_info.connected_at = Some(self.time_ms());
289        peer_info.last_seen_ms = self.time_ms();
290
291        self.peers.write().unwrap().insert(node_id, peer_info);
292
293        // Emit event
294        drop(topology); // Release lock before emitting
295        self.emit_event(TopologyEvent::ParentConnected {
296            node_id,
297            level,
298            rssi: Some(rssi),
299        });
300
301        self.emit_topology_changed();
302        Ok(())
303    }
304
305    /// Disconnect from our parent
306    pub fn disconnect_parent(&self, reason: DisconnectReason) -> Option<NodeId> {
307        let old_parent = {
308            let mut topology = self.topology.write().unwrap();
309            topology.clear_parent()
310        };
311
312        if let Some(ref parent_id) = old_parent {
313            self.peers.write().unwrap().remove(parent_id);
314
315            self.emit_event(TopologyEvent::ParentDisconnected {
316                node_id: *parent_id,
317                reason,
318            });
319            self.emit_topology_changed();
320        }
321
322        old_parent
323    }
324
325    /// Accept a child connection
326    pub fn accept_child(&self, node_id: NodeId, level: HierarchyLevel) -> Result<()> {
327        let mut topology = self.topology.write().unwrap();
328
329        if !topology.add_child(node_id) {
330            return Err(BleError::ConnectionFailed("Cannot accept child".into()));
331        }
332
333        // Add peer info
334        let mut peer_info = PeerInfo::new(node_id, PeerRole::Child, level);
335        peer_info.state = ConnectionState::Connected;
336        peer_info.connected_at = Some(self.time_ms());
337        peer_info.last_seen_ms = self.time_ms();
338
339        self.peers.write().unwrap().insert(node_id, peer_info);
340
341        // Emit event
342        drop(topology);
343        self.emit_event(TopologyEvent::ChildConnected { node_id, level });
344
345        self.emit_topology_changed();
346        Ok(())
347    }
348
349    /// Remove a child
350    pub fn remove_child(&self, node_id: &NodeId, reason: DisconnectReason) -> bool {
351        let removed = {
352            let mut topology = self.topology.write().unwrap();
353            topology.remove_child(node_id)
354        };
355
356        if removed {
357            self.peers.write().unwrap().remove(node_id);
358
359            self.emit_event(TopologyEvent::ChildDisconnected {
360                node_id: *node_id,
361                reason,
362            });
363            self.emit_topology_changed();
364        }
365
366        removed
367    }
368
369    /// Start parent failover process
370    pub fn start_failover(&self) -> Result<()> {
371        let mut state = self.state.write().unwrap();
372        if *state != ManagerState::Running {
373            return Err(BleError::InvalidState("Not running".into()));
374        }
375
376        let old_parent = self.disconnect_parent(DisconnectReason::LinkLoss);
377
378        if let Some(old_parent_id) = old_parent {
379            *state = ManagerState::Failover;
380            drop(state);
381
382            self.emit_event(TopologyEvent::ParentFailoverStarted {
383                old_parent: old_parent_id,
384            });
385        }
386
387        Ok(())
388    }
389
390    /// Complete failover by connecting to new parent
391    pub fn complete_failover(
392        &self,
393        new_parent: Option<(NodeId, HierarchyLevel, i8)>,
394    ) -> Result<()> {
395        let old_parent = {
396            // Get old parent from candidates list (it was stored there)
397            self.candidates
398                .read()
399                .unwrap()
400                .first()
401                .map(|c| c.node_id)
402                .unwrap_or_else(|| NodeId::new(0))
403        };
404
405        if let Some((node_id, level, rssi)) = new_parent {
406            self.connect_parent(node_id, level, rssi)?;
407
408            let mut state = self.state.write().unwrap();
409            *state = ManagerState::Running;
410            drop(state);
411
412            self.emit_event(TopologyEvent::ParentFailoverCompleted {
413                old_parent,
414                new_parent: Some(node_id),
415            });
416        } else {
417            let mut state = self.state.write().unwrap();
418            *state = ManagerState::Running;
419            drop(state);
420
421            self.emit_event(TopologyEvent::ParentFailoverCompleted {
422                old_parent,
423                new_parent: None,
424            });
425        }
426
427        Ok(())
428    }
429
430    /// Update RSSI for a connected peer
431    pub fn update_rssi(&self, node_id: &NodeId, rssi: i8) {
432        let mut peers = self.peers.write().unwrap();
433        if let Some(peer) = peers.get_mut(node_id) {
434            peer.update_rssi(rssi);
435            peer.last_seen_ms = self.time_ms();
436        }
437        drop(peers);
438
439        self.emit_event(TopologyEvent::ConnectionQualityChanged {
440            node_id: *node_id,
441            rssi,
442        });
443    }
444
445    /// Record a connection failure for a node
446    pub fn record_failure(&self, node_id: &NodeId) {
447        let mut peers = self.peers.write().unwrap();
448        if let Some(peer) = peers.get_mut(node_id) {
449            peer.record_failure();
450        }
451
452        // Also update candidate failure count
453        let mut candidates = self.candidates.write().unwrap();
454        if let Some(candidate) = candidates.iter_mut().find(|c| &c.node_id == node_id) {
455            candidate.failure_count = candidate.failure_count.saturating_add(1);
456        }
457    }
458
459    /// Age all candidates (call periodically)
460    pub fn age_candidates(&self, elapsed_ms: u64) {
461        let mut candidates = self.candidates.write().unwrap();
462        for candidate in candidates.iter_mut() {
463            candidate.age_ms = candidate.age_ms.saturating_add(elapsed_ms);
464        }
465
466        // Remove candidates that are too old
467        candidates.retain(|c| c.age_ms <= self.config.max_beacon_age_ms * 2);
468    }
469
470    /// Check if we should switch parents (better option available)
471    pub fn should_switch_parent(&self) -> Option<ParentCandidate> {
472        let topology = self.topology.read().unwrap();
473        let current_parent = topology.parent?;
474        drop(topology);
475
476        let peers = self.peers.read().unwrap();
477        let current_rssi = peers.get(&current_parent)?.rssi?;
478        drop(peers);
479
480        // Find best alternative
481        let best = self.select_best_parent()?;
482
483        // Only switch if significantly better (hysteresis)
484        if best.rssi > current_rssi + self.config.rssi_hysteresis as i8 {
485            Some(best)
486        } else {
487            None
488        }
489    }
490
491    /// Helper to emit topology changed event
492    fn emit_topology_changed(&self) {
493        let topology = self.topology.read().unwrap();
494        self.emit_event(TopologyEvent::TopologyChanged {
495            child_count: topology.children.len(),
496            peer_count: topology.peers.len(),
497            has_parent: topology.has_parent(),
498        });
499    }
500}
501
502#[cfg(test)]
503mod tests {
504    use super::*;
505
506    fn create_manager() -> MeshManager {
507        MeshManager::new(
508            NodeId::new(0x1234),
509            HierarchyLevel::Platform,
510            TopologyConfig::default(),
511        )
512    }
513
514    #[test]
515    fn test_manager_creation() {
516        let manager = create_manager();
517        assert_eq!(manager.node_id().as_u32(), 0x1234);
518        assert_eq!(manager.my_level(), HierarchyLevel::Platform);
519        assert_eq!(manager.state(), ManagerState::Stopped);
520    }
521
522    #[test]
523    fn test_start_stop() {
524        let manager = create_manager();
525
526        assert!(manager.start().is_ok());
527        assert_eq!(manager.state(), ManagerState::Running);
528
529        assert!(manager.stop().is_ok());
530        assert_eq!(manager.state(), ManagerState::Stopped);
531    }
532
533    #[test]
534    fn test_connect_parent() {
535        let manager = create_manager();
536        manager.start().unwrap();
537
538        let parent_id = NodeId::new(0x5678);
539        assert!(manager
540            .connect_parent(parent_id, HierarchyLevel::Squad, -50)
541            .is_ok());
542
543        assert!(manager.has_parent());
544        assert_eq!(manager.parent(), Some(parent_id));
545
546        // Can't connect another parent
547        assert!(manager
548            .connect_parent(NodeId::new(0x9999), HierarchyLevel::Squad, -50)
549            .is_err());
550    }
551
552    #[test]
553    fn test_disconnect_parent() {
554        let manager = create_manager();
555        manager.start().unwrap();
556
557        let parent_id = NodeId::new(0x5678);
558        manager
559            .connect_parent(parent_id, HierarchyLevel::Squad, -50)
560            .unwrap();
561
562        let old = manager.disconnect_parent(DisconnectReason::Requested);
563        assert_eq!(old, Some(parent_id));
564        assert!(!manager.has_parent());
565    }
566
567    #[test]
568    fn test_accept_child() {
569        let manager = MeshManager::new(
570            NodeId::new(0x1234),
571            HierarchyLevel::Squad,
572            TopologyConfig::default(),
573        );
574        manager.start().unwrap();
575
576        let child_id = NodeId::new(0x0001);
577        assert!(manager
578            .accept_child(child_id, HierarchyLevel::Platform)
579            .is_ok());
580
581        assert_eq!(manager.child_count(), 1);
582        assert_eq!(manager.children(), vec![child_id]);
583    }
584
585    #[test]
586    fn test_max_children() {
587        let config = TopologyConfig {
588            max_children: 2,
589            ..Default::default()
590        };
591
592        let manager = MeshManager::new(NodeId::new(0x1234), HierarchyLevel::Squad, config);
593        manager.start().unwrap();
594
595        assert!(manager
596            .accept_child(NodeId::new(0x0001), HierarchyLevel::Platform)
597            .is_ok());
598        assert!(manager
599            .accept_child(NodeId::new(0x0002), HierarchyLevel::Platform)
600            .is_ok());
601        assert!(manager
602            .accept_child(NodeId::new(0x0003), HierarchyLevel::Platform)
603            .is_err());
604    }
605
606    #[test]
607    fn test_process_beacon() {
608        let manager = create_manager();
609        manager.start().unwrap();
610
611        let beacon = HiveBeacon {
612            node_id: NodeId::new(0x5678),
613            hierarchy_level: HierarchyLevel::Squad,
614            version: 1,
615            seq_num: 1,
616            capabilities: 0,
617            battery_percent: 100,
618            geohash: 0,
619        };
620
621        manager.process_beacon(&beacon, -50);
622
623        let best = manager.select_best_parent();
624        assert!(best.is_some());
625        assert_eq!(best.unwrap().node_id.as_u32(), 0x5678);
626    }
627
628    #[test]
629    fn test_select_best_parent_rssi() {
630        let manager = create_manager();
631        manager.start().unwrap();
632
633        // Add two candidates
634        let beacon1 = HiveBeacon {
635            node_id: NodeId::new(0x1111),
636            hierarchy_level: HierarchyLevel::Squad,
637            version: 1,
638            seq_num: 1,
639            capabilities: 0,
640            battery_percent: 100,
641            geohash: 0,
642        };
643
644        let beacon2 = HiveBeacon {
645            node_id: NodeId::new(0x2222),
646            hierarchy_level: HierarchyLevel::Squad,
647            version: 1,
648            seq_num: 1,
649            capabilities: 0,
650            battery_percent: 100,
651            geohash: 0,
652        };
653
654        manager.process_beacon(&beacon1, -70);
655        manager.process_beacon(&beacon2, -50); // Better RSSI
656
657        let best = manager.select_best_parent().unwrap();
658        assert_eq!(best.node_id.as_u32(), 0x2222);
659    }
660
661    #[test]
662    fn test_failover() {
663        let manager = create_manager();
664        manager.start().unwrap();
665
666        let parent_id = NodeId::new(0x5678);
667        manager
668            .connect_parent(parent_id, HierarchyLevel::Squad, -50)
669            .unwrap();
670
671        // Start failover
672        assert!(manager.start_failover().is_ok());
673        assert_eq!(manager.state(), ManagerState::Failover);
674        assert!(!manager.has_parent());
675
676        // Complete without new parent
677        assert!(manager.complete_failover(None).is_ok());
678        assert_eq!(manager.state(), ManagerState::Running);
679    }
680
681    #[test]
682    fn test_event_callback() {
683        use std::sync::atomic::{AtomicBool, Ordering};
684        use std::sync::Arc;
685
686        let manager = create_manager();
687        manager.start().unwrap();
688
689        let called = Arc::new(AtomicBool::new(false));
690        let called_clone = called.clone();
691
692        manager.on_topology_event(Box::new(move |event| {
693            if matches!(event, TopologyEvent::ParentConnected { .. }) {
694                called_clone.store(true, Ordering::SeqCst);
695            }
696        }));
697
698        manager
699            .connect_parent(NodeId::new(0x5678), HierarchyLevel::Squad, -50)
700            .unwrap();
701
702        assert!(called.load(Ordering::SeqCst));
703    }
704
705    #[test]
706    fn test_update_rssi() {
707        let manager = create_manager();
708        manager.start().unwrap();
709
710        let parent_id = NodeId::new(0x5678);
711        manager
712            .connect_parent(parent_id, HierarchyLevel::Squad, -50)
713            .unwrap();
714
715        manager.update_rssi(&parent_id, -60);
716
717        let info = manager.get_peer_info(&parent_id).unwrap();
718        assert_eq!(info.rssi, Some(-60));
719    }
720
721    #[test]
722    fn test_age_candidates() {
723        let manager = create_manager();
724        manager.start().unwrap();
725
726        let beacon = HiveBeacon {
727            node_id: NodeId::new(0x5678),
728            hierarchy_level: HierarchyLevel::Squad,
729            version: 1,
730            seq_num: 1,
731            capabilities: 0,
732            battery_percent: 100,
733            geohash: 0,
734        };
735
736        manager.process_beacon(&beacon, -50);
737
738        // Age past the threshold
739        manager.age_candidates(25_000);
740
741        // Should be removed
742        let best = manager.select_best_parent();
743        assert!(best.is_none());
744    }
745}