hive_btle/mesh/
routing.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//! Message routing for HIVE-BTLE mesh
17//!
18//! Handles routing messages through the mesh topology, including:
19//! - Upward routing to parent
20//! - Downward routing to children
21//! - Broadcast to all connected peers
22
23#[cfg(not(feature = "std"))]
24use alloc::{vec, vec::Vec};
25
26use crate::{HierarchyLevel, NodeId};
27
28use super::topology::MeshTopology;
29
30/// Message routing direction
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum RouteDirection {
33    /// Route upward toward the root (parent direction)
34    Upward,
35    /// Route downward toward leaves (children direction)
36    Downward,
37    /// Route to all connected peers
38    Broadcast,
39    /// Route to a specific node
40    Targeted(u32), // NodeId as u32 for Copy
41}
42
43/// A routing decision for a message
44#[derive(Debug, Clone)]
45pub struct RouteDecision {
46    /// Next hop(s) for the message
47    pub next_hops: Vec<NodeId>,
48    /// Whether to keep a local copy
49    pub local_copy: bool,
50    /// Whether routing succeeded
51    pub routed: bool,
52    /// Reason if routing failed
53    pub failure_reason: Option<RouteFailure>,
54}
55
56impl RouteDecision {
57    /// Create a successful route decision
58    pub fn success(next_hops: Vec<NodeId>, local_copy: bool) -> Self {
59        Self {
60            next_hops,
61            local_copy,
62            routed: true,
63            failure_reason: None,
64        }
65    }
66
67    /// Create a failed route decision
68    pub fn failed(reason: RouteFailure) -> Self {
69        Self {
70            next_hops: Vec::new(),
71            local_copy: false,
72            routed: false,
73            failure_reason: Some(reason),
74        }
75    }
76
77    /// Route for local processing only
78    pub fn local_only() -> Self {
79        Self {
80            next_hops: Vec::new(),
81            local_copy: true,
82            routed: true,
83            failure_reason: None,
84        }
85    }
86}
87
88/// Reason for routing failure
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum RouteFailure {
91    /// No parent available for upward routing
92    NoParent,
93    /// No children for downward routing
94    NoChildren,
95    /// Target node not found in topology
96    TargetNotFound,
97    /// No peers connected
98    NoPeers,
99    /// TTL expired
100    TtlExpired,
101    /// Message too large
102    MessageTooLarge,
103}
104
105/// Router for mesh messages
106///
107/// Determines where to send messages based on the current topology.
108#[derive(Debug, Clone)]
109pub struct MeshRouter {
110    /// Our node ID
111    pub node_id: NodeId,
112    /// Our hierarchy level
113    pub my_level: HierarchyLevel,
114}
115
116impl MeshRouter {
117    /// Create a new router
118    pub fn new(node_id: NodeId, my_level: HierarchyLevel) -> Self {
119        Self { node_id, my_level }
120    }
121
122    /// Route a message based on direction
123    pub fn route(&self, direction: RouteDirection, topology: &MeshTopology) -> RouteDecision {
124        match direction {
125            RouteDirection::Upward => self.route_upward(topology),
126            RouteDirection::Downward => self.route_downward(topology),
127            RouteDirection::Broadcast => self.route_broadcast(topology),
128            RouteDirection::Targeted(target_id) => {
129                self.route_targeted(&NodeId::new(target_id), topology)
130            }
131        }
132    }
133
134    /// Route upward to parent
135    fn route_upward(&self, topology: &MeshTopology) -> RouteDecision {
136        match &topology.parent {
137            Some(parent_id) => RouteDecision::success(vec![*parent_id], true),
138            None => RouteDecision::failed(RouteFailure::NoParent),
139        }
140    }
141
142    /// Route downward to children
143    fn route_downward(&self, topology: &MeshTopology) -> RouteDecision {
144        if topology.children.is_empty() {
145            RouteDecision::failed(RouteFailure::NoChildren)
146        } else {
147            RouteDecision::success(topology.children.clone(), true)
148        }
149    }
150
151    /// Route to all connected peers (broadcast)
152    fn route_broadcast(&self, topology: &MeshTopology) -> RouteDecision {
153        let all = topology.all_connected();
154        if all.is_empty() {
155            RouteDecision::failed(RouteFailure::NoPeers)
156        } else {
157            RouteDecision::success(all, true)
158        }
159    }
160
161    /// Route to a specific target node
162    fn route_targeted(&self, target: &NodeId, topology: &MeshTopology) -> RouteDecision {
163        // Check if target is directly connected
164        if topology.is_connected(target) {
165            return RouteDecision::success(vec![*target], false);
166        }
167
168        // Not directly connected - need to route through topology
169        // For now, use simple strategy:
170        // - If we have a parent, route upward (parent has broader view)
171        // - Otherwise, route to all children (flood downward)
172
173        if let Some(ref parent) = topology.parent {
174            // Route to parent, it will figure out where to send
175            RouteDecision::success(vec![*parent], false)
176        } else if !topology.children.is_empty() {
177            // Flood to all children
178            RouteDecision::success(topology.children.clone(), false)
179        } else {
180            RouteDecision::failed(RouteFailure::TargetNotFound)
181        }
182    }
183
184    /// Determine routing for a received message
185    ///
186    /// Given the source and destination, decide what to do with a message.
187    pub fn handle_received(
188        &self,
189        source: &NodeId,
190        destination: Option<&NodeId>,
191        direction: RouteDirection,
192        topology: &MeshTopology,
193    ) -> RouteDecision {
194        // Check if we are the destination
195        if let Some(dest) = destination {
196            if dest == &self.node_id {
197                return RouteDecision::local_only();
198            }
199        }
200
201        // Determine role of source (may be useful for advanced routing)
202        let _source_role = topology.get_role(source);
203
204        match direction {
205            RouteDirection::Upward => {
206                // Message going upward - forward to parent
207                self.route_upward(topology)
208            }
209            RouteDirection::Downward => {
210                // Message going downward - forward to children
211                // Exclude the source if it's one of our children
212                let mut children = topology.children.clone();
213                children.retain(|c| c != source);
214                if children.is_empty() {
215                    RouteDecision::local_only()
216                } else {
217                    RouteDecision::success(children, true)
218                }
219            }
220            RouteDirection::Broadcast => {
221                // Broadcast - forward to all except source
222                let mut all = topology.all_connected();
223                all.retain(|n| n != source);
224                RouteDecision::success(all, true)
225            }
226            RouteDirection::Targeted(target_id) => {
227                let target = NodeId::new(target_id);
228                if target == self.node_id {
229                    RouteDecision::local_only()
230                } else {
231                    self.route_targeted(&target, topology)
232                }
233            }
234        }
235    }
236
237    /// Get the best route for aggregation (data flowing upward)
238    ///
239    /// For HIVE-Lite nodes, this is always the parent.
240    pub fn aggregation_route(&self, topology: &MeshTopology) -> Option<NodeId> {
241        topology.parent
242    }
243
244    /// Get routes for dissemination (data flowing downward)
245    ///
246    /// Returns all children that should receive the data.
247    pub fn dissemination_routes(&self, topology: &MeshTopology) -> Vec<NodeId> {
248        topology.children.clone()
249    }
250}
251
252/// Message hop tracking for loop prevention
253#[derive(Debug, Clone)]
254pub struct HopTracker {
255    /// Maximum allowed hops
256    pub max_hops: u8,
257    /// Nodes this message has visited
258    pub visited: Vec<NodeId>,
259}
260
261impl HopTracker {
262    /// Create a new hop tracker
263    pub fn new(max_hops: u8) -> Self {
264        Self {
265            max_hops,
266            visited: Vec::new(),
267        }
268    }
269
270    /// Record visiting a node
271    pub fn visit(&mut self, node_id: NodeId) -> bool {
272        if self.visited.contains(&node_id) {
273            return false; // Loop detected
274        }
275        if self.visited.len() >= self.max_hops as usize {
276            return false; // TTL expired
277        }
278        self.visited.push(node_id);
279        true
280    }
281
282    /// Check if we've visited a node
283    pub fn has_visited(&self, node_id: &NodeId) -> bool {
284        self.visited.contains(node_id)
285    }
286
287    /// Get remaining hops
288    pub fn remaining_hops(&self) -> u8 {
289        self.max_hops.saturating_sub(self.visited.len() as u8)
290    }
291
292    /// Check if TTL is expired
293    pub fn is_expired(&self) -> bool {
294        self.visited.len() >= self.max_hops as usize
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301
302    fn create_topology_with_parent() -> MeshTopology {
303        let mut topology = MeshTopology::new(HierarchyLevel::Platform, 3, 7);
304        topology.set_parent(NodeId::new(0x1000));
305        topology.add_child(NodeId::new(0x0001));
306        topology.add_child(NodeId::new(0x0002));
307        topology
308    }
309
310    #[test]
311    fn test_route_upward() {
312        let router = MeshRouter::new(NodeId::new(0x1234), HierarchyLevel::Platform);
313        let topology = create_topology_with_parent();
314
315        let decision = router.route(RouteDirection::Upward, &topology);
316        assert!(decision.routed);
317        assert_eq!(decision.next_hops.len(), 1);
318        assert_eq!(decision.next_hops[0].as_u32(), 0x1000);
319    }
320
321    #[test]
322    fn test_route_upward_no_parent() {
323        let router = MeshRouter::new(NodeId::new(0x1234), HierarchyLevel::Platform);
324        let topology = MeshTopology::new(HierarchyLevel::Platform, 3, 7);
325
326        let decision = router.route(RouteDirection::Upward, &topology);
327        assert!(!decision.routed);
328        assert_eq!(decision.failure_reason, Some(RouteFailure::NoParent));
329    }
330
331    #[test]
332    fn test_route_downward() {
333        let router = MeshRouter::new(NodeId::new(0x1234), HierarchyLevel::Squad);
334        let topology = create_topology_with_parent();
335
336        let decision = router.route(RouteDirection::Downward, &topology);
337        assert!(decision.routed);
338        assert_eq!(decision.next_hops.len(), 2);
339    }
340
341    #[test]
342    fn test_route_broadcast() {
343        let router = MeshRouter::new(NodeId::new(0x1234), HierarchyLevel::Platform);
344        let topology = create_topology_with_parent();
345
346        let decision = router.route(RouteDirection::Broadcast, &topology);
347        assert!(decision.routed);
348        assert_eq!(decision.next_hops.len(), 3); // Parent + 2 children
349    }
350
351    #[test]
352    fn test_route_targeted_direct() {
353        let router = MeshRouter::new(NodeId::new(0x1234), HierarchyLevel::Platform);
354        let topology = create_topology_with_parent();
355
356        // Target is a child (directly connected)
357        let decision = router.route(RouteDirection::Targeted(0x0001), &topology);
358        assert!(decision.routed);
359        assert_eq!(decision.next_hops.len(), 1);
360        assert_eq!(decision.next_hops[0].as_u32(), 0x0001);
361    }
362
363    #[test]
364    fn test_route_targeted_via_parent() {
365        let router = MeshRouter::new(NodeId::new(0x1234), HierarchyLevel::Platform);
366        let topology = create_topology_with_parent();
367
368        // Target is not directly connected - should route to parent
369        let decision = router.route(RouteDirection::Targeted(0x9999), &topology);
370        assert!(decision.routed);
371        assert_eq!(decision.next_hops.len(), 1);
372        assert_eq!(decision.next_hops[0].as_u32(), 0x1000); // Parent
373    }
374
375    #[test]
376    fn test_handle_received_for_us() {
377        let router = MeshRouter::new(NodeId::new(0x1234), HierarchyLevel::Platform);
378        let topology = create_topology_with_parent();
379
380        let decision = router.handle_received(
381            &NodeId::new(0x1000),
382            Some(&NodeId::new(0x1234)), // Destination is us
383            RouteDirection::Downward,
384            &topology,
385        );
386
387        assert!(decision.local_copy);
388        assert!(decision.next_hops.is_empty());
389    }
390
391    #[test]
392    fn test_hop_tracker() {
393        let mut tracker = HopTracker::new(3);
394
395        assert!(tracker.visit(NodeId::new(0x0001)));
396        assert!(tracker.visit(NodeId::new(0x0002)));
397        assert!(tracker.visit(NodeId::new(0x0003)));
398        assert!(!tracker.visit(NodeId::new(0x0004))); // TTL expired
399
400        assert!(tracker.has_visited(&NodeId::new(0x0001)));
401        assert!(!tracker.has_visited(&NodeId::new(0x0004)));
402    }
403
404    #[test]
405    fn test_hop_tracker_loop_detection() {
406        let mut tracker = HopTracker::new(10);
407
408        assert!(tracker.visit(NodeId::new(0x0001)));
409        assert!(tracker.visit(NodeId::new(0x0002)));
410        assert!(!tracker.visit(NodeId::new(0x0001))); // Loop detected
411    }
412
413    #[test]
414    fn test_aggregation_route() {
415        let router = MeshRouter::new(NodeId::new(0x1234), HierarchyLevel::Platform);
416        let topology = create_topology_with_parent();
417
418        let route = router.aggregation_route(&topology);
419        assert_eq!(route, Some(NodeId::new(0x1000)));
420    }
421
422    #[test]
423    fn test_dissemination_routes() {
424        let router = MeshRouter::new(NodeId::new(0x1234), HierarchyLevel::Squad);
425        let topology = create_topology_with_parent();
426
427        let routes = router.dissemination_routes(&topology);
428        assert_eq!(routes.len(), 2);
429    }
430}