Skip to main content

hdds_micro/transport/mesh/
router.rs

1// SPDX-License-Identifier: Apache-2.0 OR MIT
2// Copyright (c) 2025-2026 naskel.com
3
4//! Mesh router and relay decision logic
5
6use super::header::MeshHeader;
7use super::neighbor::NeighborTable;
8use super::seen::SeenCache;
9use super::{MeshStats, DEFAULT_TTL, MAX_TTL};
10
11/// Mesh routing configuration
12#[derive(Debug, Clone, Copy)]
13pub struct MeshConfig {
14    /// Default TTL for new messages
15    pub default_ttl: u8,
16    /// Whether to relay messages
17    pub relay_enabled: bool,
18    /// Whether to relay broadcast messages
19    pub relay_broadcast: bool,
20    /// Minimum RSSI to consider for relay (-dBm)
21    pub min_relay_rssi: i16,
22    /// Neighbor table lifetime (ticks)
23    pub neighbor_lifetime: u32,
24    /// Seen cache lifetime (ticks)
25    pub seen_lifetime: u32,
26}
27
28impl Default for MeshConfig {
29    fn default() -> Self {
30        Self {
31            default_ttl: DEFAULT_TTL,
32            relay_enabled: true,
33            relay_broadcast: true,
34            min_relay_rssi: -100,   // Accept all
35            neighbor_lifetime: 300, // 5 minutes at 1 tick/sec
36            seen_lifetime: 60,      // 1 minute
37        }
38    }
39}
40
41impl MeshConfig {
42    /// Create config for a relay node
43    pub fn relay_node() -> Self {
44        Self {
45            relay_enabled: true,
46            relay_broadcast: true,
47            ..Default::default()
48        }
49    }
50
51    /// Create config for an endpoint (no relay)
52    pub fn endpoint() -> Self {
53        Self {
54            relay_enabled: false,
55            relay_broadcast: false,
56            ..Default::default()
57        }
58    }
59
60    /// Create config for a gateway
61    pub fn gateway() -> Self {
62        Self {
63            default_ttl: MAX_TTL,
64            relay_enabled: true,
65            relay_broadcast: true,
66            ..Default::default()
67        }
68    }
69}
70
71/// Decision about what to do with a received message
72#[derive(Debug, Clone)]
73pub enum RelayDecision {
74    /// Deliver to local application
75    Deliver,
76    /// Relay to network (with updated header)
77    Relay(MeshHeader),
78    /// Drop (duplicate or TTL expired)
79    Drop,
80}
81
82/// Mesh router
83pub struct MeshRouter<const NEIGHBORS: usize, const SEEN: usize> {
84    /// Our node ID
85    node_id: u8,
86    /// Configuration
87    config: MeshConfig,
88    /// Neighbor table
89    neighbors: NeighborTable<NEIGHBORS>,
90    /// Seen message cache
91    seen: SeenCache<SEEN>,
92    /// Statistics
93    stats: MeshStats,
94}
95
96impl<const NEIGHBORS: usize, const SEEN: usize> MeshRouter<NEIGHBORS, SEEN> {
97    /// Create a new mesh router
98    pub fn new(node_id: u8, config: MeshConfig) -> Self {
99        Self {
100            node_id,
101            config,
102            neighbors: NeighborTable::new(config.neighbor_lifetime),
103            seen: SeenCache::new(config.seen_lifetime),
104            stats: MeshStats::default(),
105        }
106    }
107
108    /// Get configuration
109    pub fn config(&self) -> &MeshConfig {
110        &self.config
111    }
112
113    /// Get neighbor table
114    pub fn neighbors(&self) -> &NeighborTable<NEIGHBORS> {
115        &self.neighbors
116    }
117
118    /// Get statistics
119    pub fn stats(&self) -> MeshStats {
120        self.stats
121    }
122
123    /// Advance time (call periodically, e.g., once per second)
124    pub fn tick(&mut self) {
125        self.neighbors.tick();
126        self.seen.tick();
127        self.neighbors.expire_old();
128    }
129
130    /// Process a received message and decide what to do
131    pub fn process_received(&mut self, header: &MeshHeader, rssi: Option<i16>) -> RelayDecision {
132        // Update neighbor info if RSSI available
133        if let Some(rssi) = rssi {
134            self.neighbors.update(header.src, rssi);
135        }
136
137        // Check if we've seen this message before
138        let msg_id = header.message_id();
139        if self.seen.check_and_mark(msg_id) {
140            self.stats.rx_duplicate += 1;
141            return RelayDecision::Drop;
142        }
143
144        // Check if message is for us
145        let is_for_us = header.dst == self.node_id || header.dst == 0xFF;
146
147        // Check if we should relay
148        let should_relay = self.should_relay(header, rssi);
149
150        if should_relay {
151            if let Some(relay_header) = header.for_relay() {
152                self.stats.tx_relayed += 1;
153
154                if is_for_us {
155                    self.stats.rx_delivered += 1;
156                }
157
158                return RelayDecision::Relay(relay_header);
159            }
160
161            // TTL expired
162            self.stats.rx_ttl_expired += 1;
163
164            if is_for_us {
165                self.stats.rx_delivered += 1;
166                return RelayDecision::Deliver;
167            }
168
169            return RelayDecision::Drop;
170        }
171
172        if is_for_us {
173            self.stats.rx_delivered += 1;
174            RelayDecision::Deliver
175        } else {
176            RelayDecision::Drop
177        }
178    }
179
180    /// Determine if we should relay a message
181    fn should_relay(&self, header: &MeshHeader, rssi: Option<i16>) -> bool {
182        // Don't relay our own messages
183        if header.src == self.node_id {
184            return false;
185        }
186
187        // Don't relay unicast messages addressed to us
188        if header.dst == self.node_id {
189            return false;
190        }
191
192        // Check if relay is enabled
193        if !self.config.relay_enabled {
194            return false;
195        }
196
197        // Check broadcast relay setting
198        if header.is_broadcast() && !self.config.relay_broadcast {
199            return false;
200        }
201
202        // Check RSSI threshold
203        if let Some(rssi) = rssi {
204            if rssi < self.config.min_relay_rssi {
205                return false;
206            }
207        }
208
209        // Check TTL
210        if header.ttl == 0 {
211            return false;
212        }
213
214        true
215    }
216
217    /// Record that we originated a message
218    pub fn record_originated(&mut self, header: &MeshHeader) {
219        self.seen.mark_seen(header.message_id());
220        self.stats.tx_originated += 1;
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn test_router_creation() {
230        let router: MeshRouter<8, 32> = MeshRouter::new(1, MeshConfig::default());
231        assert_eq!(router.config().default_ttl, DEFAULT_TTL);
232    }
233
234    #[test]
235    fn test_relay_decision_deliver() {
236        let mut router: MeshRouter<8, 32> = MeshRouter::new(1, MeshConfig::endpoint());
237
238        // Message addressed to us
239        let header = MeshHeader::new(2, 1, 100, 3);
240        let decision = router.process_received(&header, Some(-70));
241
242        match decision {
243            RelayDecision::Deliver => {}
244            _ => panic!("Expected Deliver"),
245        }
246    }
247
248    #[test]
249    fn test_relay_decision_broadcast() {
250        let mut router: MeshRouter<8, 32> = MeshRouter::new(1, MeshConfig::relay_node());
251
252        // Broadcast message
253        let header = MeshHeader::broadcast(2, 100, 3);
254        let decision = router.process_received(&header, Some(-70));
255
256        match decision {
257            RelayDecision::Relay(new_header) => {
258                assert_eq!(new_header.ttl, 2); // Decremented
259                assert_eq!(new_header.hop_count, 1); // Incremented
260            }
261            _ => panic!("Expected Relay"),
262        }
263    }
264
265    #[test]
266    fn test_relay_decision_duplicate() {
267        let mut router: MeshRouter<8, 32> = MeshRouter::new(1, MeshConfig::relay_node());
268
269        let header = MeshHeader::broadcast(2, 100, 3);
270
271        // First time - should relay
272        let decision1 = router.process_received(&header, Some(-70));
273        assert!(matches!(decision1, RelayDecision::Relay(_)));
274
275        // Second time - duplicate
276        let decision2 = router.process_received(&header, Some(-70));
277        assert!(matches!(decision2, RelayDecision::Drop));
278
279        assert_eq!(router.stats().rx_duplicate, 1);
280    }
281
282    #[test]
283    fn test_relay_decision_ttl_expired() {
284        let mut router: MeshRouter<8, 32> = MeshRouter::new(1, MeshConfig::relay_node());
285
286        // Message with TTL=0
287        let header = MeshHeader::new(2, 3, 100, 0);
288        let decision = router.process_received(&header, Some(-70));
289
290        // Should not relay, not for us -> drop
291        assert!(matches!(decision, RelayDecision::Drop));
292    }
293
294    #[test]
295    fn test_relay_disabled() {
296        let mut router: MeshRouter<8, 32> = MeshRouter::new(1, MeshConfig::endpoint());
297
298        // Broadcast from another node
299        let header = MeshHeader::broadcast(2, 100, 3);
300        let decision = router.process_received(&header, Some(-70));
301
302        // Should deliver (broadcast) but not relay
303        match decision {
304            RelayDecision::Deliver => {}
305            _ => panic!("Expected Deliver (not Relay)"),
306        }
307    }
308
309    #[test]
310    fn test_own_message_not_relayed() {
311        let mut router: MeshRouter<8, 32> = MeshRouter::new(1, MeshConfig::relay_node());
312
313        // Message from ourselves
314        let header = MeshHeader::broadcast(1, 100, 3);
315        let decision = router.process_received(&header, Some(-70));
316
317        // Should deliver (broadcast to us) but not relay
318        match decision {
319            RelayDecision::Deliver => {}
320            _ => panic!("Expected Deliver"),
321        }
322    }
323
324    #[test]
325    fn test_rssi_threshold() {
326        let config = MeshConfig {
327            min_relay_rssi: -80,
328            relay_enabled: true,
329            relay_broadcast: true,
330            ..Default::default()
331        };
332        let mut router: MeshRouter<8, 32> = MeshRouter::new(1, config);
333
334        // Strong signal - should relay
335        let header1 = MeshHeader::broadcast(2, 100, 3);
336        let decision1 = router.process_received(&header1, Some(-70));
337        assert!(matches!(decision1, RelayDecision::Relay(_)));
338
339        // Weak signal - should not relay (but deliver since broadcast)
340        let header2 = MeshHeader::broadcast(3, 101, 3);
341        let decision2 = router.process_received(&header2, Some(-90));
342        assert!(matches!(decision2, RelayDecision::Deliver));
343    }
344
345    #[test]
346    fn test_neighbor_tracking() {
347        let mut router: MeshRouter<8, 32> = MeshRouter::new(1, MeshConfig::relay_node());
348
349        // Receive messages from multiple sources
350        let h1 = MeshHeader::broadcast(2, 100, 3);
351        let h2 = MeshHeader::broadcast(3, 101, 3);
352        let h3 = MeshHeader::broadcast(4, 102, 3);
353
354        router.process_received(&h1, Some(-70));
355        router.process_received(&h2, Some(-80));
356        router.process_received(&h3, Some(-60));
357
358        assert_eq!(router.neighbors().len(), 3);
359
360        let best = router.neighbors().best_neighbor().unwrap();
361        assert_eq!(best.node_id, 4); // Strongest signal
362    }
363
364    #[test]
365    fn test_stats_tracking() {
366        let mut router: MeshRouter<8, 32> = MeshRouter::new(1, MeshConfig::relay_node());
367
368        // Relayed message
369        let h1 = MeshHeader::broadcast(2, 100, 3);
370        router.process_received(&h1, Some(-70));
371
372        // Duplicate
373        router.process_received(&h1, Some(-70));
374
375        // For us only
376        let h2 = MeshHeader::new(3, 1, 101, 3);
377        router.process_received(&h2, Some(-70));
378
379        let stats = router.stats();
380        assert_eq!(stats.tx_relayed, 1);
381        assert_eq!(stats.rx_duplicate, 1);
382        assert_eq!(stats.rx_delivered, 2); // broadcast + unicast
383    }
384
385    #[test]
386    fn test_config_presets() {
387        let relay = MeshConfig::relay_node();
388        assert!(relay.relay_enabled);
389        assert!(relay.relay_broadcast);
390
391        let endpoint = MeshConfig::endpoint();
392        assert!(!endpoint.relay_enabled);
393
394        let gateway = MeshConfig::gateway();
395        assert_eq!(gateway.default_ttl, MAX_TTL);
396    }
397}