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