hive_btle/mesh/
topology.rs

1//! Mesh topology tracking and events
2//!
3//! Tracks the mesh topology including parent/child/peer relationships
4//! and publishes events when the topology changes.
5
6#[cfg(not(feature = "std"))]
7use alloc::vec::Vec;
8
9use crate::{HierarchyLevel, NodeId};
10
11/// State of a connection to a peer
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub enum ConnectionState {
14    /// Not connected
15    #[default]
16    Disconnected,
17    /// Connection in progress
18    Connecting,
19    /// Connected and active
20    Connected,
21    /// Disconnecting
22    Disconnecting,
23}
24
25/// Role of a peer in the mesh topology
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum PeerRole {
28    /// Our parent (we are a child of this node)
29    Parent,
30    /// Our child (this node is a child of us)
31    Child,
32    /// Peer at the same level (sibling)
33    Peer,
34}
35
36/// Information about a peer in the mesh
37#[derive(Debug, Clone)]
38pub struct PeerInfo {
39    /// Node ID of the peer
40    pub node_id: NodeId,
41    /// Role in the topology
42    pub role: PeerRole,
43    /// Connection state
44    pub state: ConnectionState,
45    /// Hierarchy level of the peer
46    pub hierarchy_level: HierarchyLevel,
47    /// Last known RSSI
48    pub rssi: Option<i8>,
49    /// Time since connection established
50    pub connected_at: Option<u64>,
51    /// Number of messages received
52    pub messages_received: u32,
53    /// Number of messages sent
54    pub messages_sent: u32,
55    /// Number of connection failures
56    pub failure_count: u8,
57    /// Last seen timestamp (monotonic ms)
58    pub last_seen_ms: u64,
59}
60
61impl PeerInfo {
62    /// Create new peer info
63    pub fn new(node_id: NodeId, role: PeerRole, hierarchy_level: HierarchyLevel) -> Self {
64        Self {
65            node_id,
66            role,
67            state: ConnectionState::Disconnected,
68            hierarchy_level,
69            rssi: None,
70            connected_at: None,
71            messages_received: 0,
72            messages_sent: 0,
73            failure_count: 0,
74            last_seen_ms: 0,
75        }
76    }
77
78    /// Check if this peer is connected
79    pub fn is_connected(&self) -> bool {
80        self.state == ConnectionState::Connected
81    }
82
83    /// Update RSSI value
84    pub fn update_rssi(&mut self, rssi: i8) {
85        self.rssi = Some(rssi);
86    }
87
88    /// Record a connection failure
89    pub fn record_failure(&mut self) {
90        self.failure_count = self.failure_count.saturating_add(1);
91    }
92
93    /// Reset failure count on successful connection
94    pub fn reset_failures(&mut self) {
95        self.failure_count = 0;
96    }
97}
98
99/// Current mesh topology state
100#[derive(Debug, Clone, Default)]
101pub struct MeshTopology {
102    /// Our parent node (if we have one)
103    pub parent: Option<NodeId>,
104    /// Our children nodes
105    pub children: Vec<NodeId>,
106    /// Peer nodes at our level
107    pub peers: Vec<NodeId>,
108    /// Our hierarchy level
109    pub my_level: HierarchyLevel,
110    /// Maximum children we can accept
111    pub max_children: u8,
112    /// Maximum total connections
113    pub max_connections: u8,
114}
115
116impl MeshTopology {
117    /// Create a new mesh topology
118    pub fn new(my_level: HierarchyLevel, max_children: u8, max_connections: u8) -> Self {
119        Self {
120            parent: None,
121            children: Vec::new(),
122            peers: Vec::new(),
123            my_level,
124            max_children,
125            max_connections,
126        }
127    }
128
129    /// Get total number of connections
130    pub fn connection_count(&self) -> usize {
131        let parent_count = if self.parent.is_some() { 1 } else { 0 };
132        parent_count + self.children.len() + self.peers.len()
133    }
134
135    /// Check if we can accept more connections
136    pub fn can_accept_connection(&self) -> bool {
137        self.connection_count() < self.max_connections as usize
138    }
139
140    /// Check if we can accept more children
141    pub fn can_accept_child(&self) -> bool {
142        self.children.len() < self.max_children as usize && self.can_accept_connection()
143    }
144
145    /// Check if we have a parent
146    pub fn has_parent(&self) -> bool {
147        self.parent.is_some()
148    }
149
150    /// Add a parent
151    pub fn set_parent(&mut self, node_id: NodeId) -> bool {
152        if self.parent.is_some() {
153            return false;
154        }
155        if !self.can_accept_connection() {
156            return false;
157        }
158        self.parent = Some(node_id);
159        true
160    }
161
162    /// Remove parent
163    pub fn clear_parent(&mut self) -> Option<NodeId> {
164        self.parent.take()
165    }
166
167    /// Add a child
168    pub fn add_child(&mut self, node_id: NodeId) -> bool {
169        if !self.can_accept_child() {
170            return false;
171        }
172        if self.children.contains(&node_id) {
173            return false;
174        }
175        self.children.push(node_id);
176        true
177    }
178
179    /// Remove a child
180    pub fn remove_child(&mut self, node_id: &NodeId) -> bool {
181        if let Some(pos) = self.children.iter().position(|n| n == node_id) {
182            self.children.remove(pos);
183            true
184        } else {
185            false
186        }
187    }
188
189    /// Add a peer
190    pub fn add_peer(&mut self, node_id: NodeId) -> bool {
191        if !self.can_accept_connection() {
192            return false;
193        }
194        if self.peers.contains(&node_id) {
195            return false;
196        }
197        self.peers.push(node_id);
198        true
199    }
200
201    /// Remove a peer
202    pub fn remove_peer(&mut self, node_id: &NodeId) -> bool {
203        if let Some(pos) = self.peers.iter().position(|n| n == node_id) {
204            self.peers.remove(pos);
205            true
206        } else {
207            false
208        }
209    }
210
211    /// Get all connected node IDs
212    pub fn all_connected(&self) -> Vec<NodeId> {
213        let mut nodes = Vec::with_capacity(self.connection_count());
214        if let Some(ref parent) = self.parent {
215            nodes.push(*parent);
216        }
217        nodes.extend(self.children.iter().cloned());
218        nodes.extend(self.peers.iter().cloned());
219        nodes
220    }
221
222    /// Check if a node is connected in any role
223    pub fn is_connected(&self, node_id: &NodeId) -> bool {
224        self.parent.as_ref() == Some(node_id)
225            || self.children.contains(node_id)
226            || self.peers.contains(node_id)
227    }
228
229    /// Get the role of a connected node
230    pub fn get_role(&self, node_id: &NodeId) -> Option<PeerRole> {
231        if self.parent.as_ref() == Some(node_id) {
232            Some(PeerRole::Parent)
233        } else if self.children.contains(node_id) {
234            Some(PeerRole::Child)
235        } else if self.peers.contains(node_id) {
236            Some(PeerRole::Peer)
237        } else {
238            None
239        }
240    }
241}
242
243/// Events that occur when the mesh topology changes
244#[derive(Debug, Clone)]
245pub enum TopologyEvent {
246    /// Connected to a parent node
247    ParentConnected {
248        /// The parent's node ID
249        node_id: NodeId,
250        /// Parent's hierarchy level
251        level: HierarchyLevel,
252        /// Signal strength
253        rssi: Option<i8>,
254    },
255    /// Disconnected from parent
256    ParentDisconnected {
257        /// The parent's node ID
258        node_id: NodeId,
259        /// Reason for disconnect
260        reason: DisconnectReason,
261    },
262    /// A child connected to us
263    ChildConnected {
264        /// The child's node ID
265        node_id: NodeId,
266        /// Child's hierarchy level
267        level: HierarchyLevel,
268    },
269    /// A child disconnected
270    ChildDisconnected {
271        /// The child's node ID
272        node_id: NodeId,
273        /// Reason for disconnect
274        reason: DisconnectReason,
275    },
276    /// A peer connected
277    PeerConnected {
278        /// The peer's node ID
279        node_id: NodeId,
280    },
281    /// A peer disconnected
282    PeerDisconnected {
283        /// The peer's node ID
284        node_id: NodeId,
285        /// Reason for disconnect
286        reason: DisconnectReason,
287    },
288    /// Topology changed (general notification)
289    TopologyChanged {
290        /// Number of children
291        child_count: usize,
292        /// Number of peers
293        peer_count: usize,
294        /// Have parent
295        has_parent: bool,
296    },
297    /// Parent failover started
298    ParentFailoverStarted {
299        /// Previous parent
300        old_parent: NodeId,
301    },
302    /// Parent failover completed
303    ParentFailoverCompleted {
304        /// Previous parent
305        old_parent: NodeId,
306        /// New parent (if found)
307        new_parent: Option<NodeId>,
308    },
309    /// Connection quality changed
310    ConnectionQualityChanged {
311        /// Node ID
312        node_id: NodeId,
313        /// New RSSI
314        rssi: i8,
315    },
316}
317
318/// Reason for a disconnection
319#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
320pub enum DisconnectReason {
321    /// Normal disconnect requested
322    Requested,
323    /// Connection timed out
324    Timeout,
325    /// Remote device disconnected
326    RemoteDisconnect,
327    /// Connection supervision timeout
328    SupervisionTimeout,
329    /// Link loss
330    LinkLoss,
331    /// Local device error
332    LocalError,
333    /// Unknown reason
334    #[default]
335    Unknown,
336}
337
338/// Candidate for parent selection
339#[derive(Debug, Clone)]
340pub struct ParentCandidate {
341    /// Node ID
342    pub node_id: NodeId,
343    /// Hierarchy level
344    pub level: HierarchyLevel,
345    /// Signal strength
346    pub rssi: i8,
347    /// Time since last beacon (ms)
348    pub age_ms: u64,
349    /// Previous failure count
350    pub failure_count: u8,
351}
352
353impl ParentCandidate {
354    /// Calculate a score for this candidate (higher = better)
355    ///
356    /// Factors:
357    /// - RSSI (signal strength) - primary factor
358    /// - Age (freshness of beacon)
359    /// - Failure history
360    /// - Hierarchy level preference
361    pub fn score(&self, my_level: HierarchyLevel) -> i32 {
362        let mut score = 0i32;
363
364        // RSSI contributes -100 to 0 (typical range is -100 to -30 dBm)
365        // Scale to give strong signals a big boost
366        score += (self.rssi as i32 + 100) * 2; // 0-140 range
367
368        // Prefer fresh beacons (within last 5 seconds)
369        if self.age_ms < 1000 {
370            score += 20;
371        } else if self.age_ms < 3000 {
372            score += 10;
373        } else if self.age_ms < 5000 {
374            score += 5;
375        }
376        // Old beacons get no bonus
377
378        // Penalize previous failures
379        score -= (self.failure_count as i32) * 15;
380
381        // Prefer parents at the level above us
382        let ideal_level = match my_level {
383            HierarchyLevel::Platform => HierarchyLevel::Squad,
384            HierarchyLevel::Squad => HierarchyLevel::Platoon,
385            HierarchyLevel::Platoon => HierarchyLevel::Company,
386            HierarchyLevel::Company => HierarchyLevel::Company, // Company has no parent
387        };
388
389        if self.level == ideal_level {
390            score += 30;
391        } else if self.level > my_level {
392            score += 15;
393        }
394        // Same level or lower gets no hierarchy bonus
395
396        score
397    }
398}
399
400/// Configuration for topology management
401#[derive(Debug, Clone)]
402pub struct TopologyConfig {
403    /// Maximum children to accept
404    pub max_children: u8,
405    /// Maximum total connections
406    pub max_connections: u8,
407    /// Minimum RSSI to consider for parent (-100 to 0 dBm)
408    pub min_parent_rssi: i8,
409    /// Maximum beacon age to consider (ms)
410    pub max_beacon_age_ms: u64,
411    /// Parent supervision timeout (ms)
412    pub parent_timeout_ms: u64,
413    /// Connection attempt timeout (ms)
414    pub connect_timeout_ms: u64,
415    /// Maximum connection failures before blacklisting
416    pub max_failures: u8,
417    /// Failover delay after parent loss (ms)
418    pub failover_delay_ms: u64,
419    /// RSSI hysteresis for switching parents (dB)
420    pub rssi_hysteresis: u8,
421}
422
423impl Default for TopologyConfig {
424    fn default() -> Self {
425        Self {
426            max_children: 3,
427            max_connections: 7,
428            min_parent_rssi: -85,
429            max_beacon_age_ms: 10_000,
430            parent_timeout_ms: 5_000,
431            connect_timeout_ms: 10_000,
432            max_failures: 3,
433            failover_delay_ms: 1_000,
434            rssi_hysteresis: 6,
435        }
436    }
437}
438
439#[cfg(test)]
440mod tests {
441    use super::*;
442
443    #[test]
444    fn test_mesh_topology_new() {
445        let topology = MeshTopology::new(HierarchyLevel::Squad, 3, 7);
446        assert_eq!(topology.my_level, HierarchyLevel::Squad);
447        assert_eq!(topology.max_children, 3);
448        assert_eq!(topology.max_connections, 7);
449        assert!(topology.parent.is_none());
450        assert!(topology.children.is_empty());
451    }
452
453    #[test]
454    fn test_set_parent() {
455        let mut topology = MeshTopology::new(HierarchyLevel::Platform, 3, 7);
456        let parent_id = NodeId::new(0x1234);
457
458        assert!(topology.set_parent(parent_id));
459        assert_eq!(topology.parent, Some(parent_id));
460        assert_eq!(topology.connection_count(), 1);
461
462        // Can't set another parent
463        assert!(!topology.set_parent(NodeId::new(0x5678)));
464    }
465
466    #[test]
467    fn test_add_children() {
468        let mut topology = MeshTopology::new(HierarchyLevel::Squad, 2, 7);
469
470        assert!(topology.add_child(NodeId::new(0x1111)));
471        assert!(topology.add_child(NodeId::new(0x2222)));
472        // Max children reached
473        assert!(!topology.add_child(NodeId::new(0x3333)));
474
475        assert_eq!(topology.children.len(), 2);
476    }
477
478    #[test]
479    fn test_connection_limit() {
480        let mut topology = MeshTopology::new(HierarchyLevel::Squad, 5, 3);
481
482        assert!(topology.set_parent(NodeId::new(0x0001)));
483        assert!(topology.add_child(NodeId::new(0x0002)));
484        assert!(topology.add_peer(NodeId::new(0x0003)));
485        // Max connections reached
486        assert!(!topology.add_child(NodeId::new(0x0004)));
487        assert!(!topology.add_peer(NodeId::new(0x0005)));
488
489        assert_eq!(topology.connection_count(), 3);
490    }
491
492    #[test]
493    fn test_remove_child() {
494        let mut topology = MeshTopology::new(HierarchyLevel::Squad, 3, 7);
495        let child_id = NodeId::new(0x1111);
496
497        topology.add_child(child_id);
498        assert!(topology.remove_child(&child_id));
499        assert!(!topology.remove_child(&child_id)); // Already removed
500        assert!(topology.children.is_empty());
501    }
502
503    #[test]
504    fn test_get_role() {
505        let mut topology = MeshTopology::new(HierarchyLevel::Squad, 3, 7);
506        let parent_id = NodeId::new(0x0001);
507        let child_id = NodeId::new(0x0002);
508        let peer_id = NodeId::new(0x0003);
509        let unknown_id = NodeId::new(0x9999);
510
511        topology.set_parent(parent_id);
512        topology.add_child(child_id);
513        topology.add_peer(peer_id);
514
515        assert_eq!(topology.get_role(&parent_id), Some(PeerRole::Parent));
516        assert_eq!(topology.get_role(&child_id), Some(PeerRole::Child));
517        assert_eq!(topology.get_role(&peer_id), Some(PeerRole::Peer));
518        assert_eq!(topology.get_role(&unknown_id), None);
519    }
520
521    #[test]
522    fn test_all_connected() {
523        let mut topology = MeshTopology::new(HierarchyLevel::Squad, 3, 7);
524        topology.set_parent(NodeId::new(0x0001));
525        topology.add_child(NodeId::new(0x0002));
526        topology.add_peer(NodeId::new(0x0003));
527
528        let all = topology.all_connected();
529        assert_eq!(all.len(), 3);
530    }
531
532    #[test]
533    fn test_parent_candidate_score() {
534        let candidate = ParentCandidate {
535            node_id: NodeId::new(0x1234),
536            level: HierarchyLevel::Squad,
537            rssi: -50,
538            age_ms: 500,
539            failure_count: 0,
540        };
541
542        // Platform looking for Squad parent
543        let score = candidate.score(HierarchyLevel::Platform);
544        // RSSI: (-50 + 100) * 2 = 100
545        // Age: +20 (< 1000ms)
546        // Failures: 0
547        // Hierarchy: +30 (ideal level)
548        assert_eq!(score, 150);
549    }
550
551    #[test]
552    fn test_parent_candidate_score_with_failures() {
553        let candidate = ParentCandidate {
554            node_id: NodeId::new(0x1234),
555            level: HierarchyLevel::Squad,
556            rssi: -50,
557            age_ms: 500,
558            failure_count: 2,
559        };
560
561        let score = candidate.score(HierarchyLevel::Platform);
562        // Base: 150 - (2 * 15) = 120
563        assert_eq!(score, 120);
564    }
565
566    #[test]
567    fn test_peer_info() {
568        let mut peer = PeerInfo::new(
569            NodeId::new(0x1234),
570            PeerRole::Child,
571            HierarchyLevel::Platform,
572        );
573
574        assert!(!peer.is_connected());
575        assert_eq!(peer.failure_count, 0);
576
577        peer.record_failure();
578        peer.record_failure();
579        assert_eq!(peer.failure_count, 2);
580
581        peer.reset_failures();
582        assert_eq!(peer.failure_count, 0);
583    }
584
585    #[test]
586    fn test_topology_config_default() {
587        let config = TopologyConfig::default();
588        assert_eq!(config.max_children, 3);
589        assert_eq!(config.max_connections, 7);
590        assert_eq!(config.min_parent_rssi, -85);
591    }
592}