Skip to main content

hdds_micro/transport/mesh/
neighbor.rs

1// SPDX-License-Identifier: Apache-2.0 OR MIT
2// Copyright (c) 2025-2026 naskel.com
3
4//! Neighbor table for mesh routing
5
6/// Information about a neighboring node
7#[derive(Debug, Clone, Copy, Default)]
8pub struct Neighbor {
9    /// Node ID
10    pub node_id: u8,
11    /// Last RSSI value (-dBm, higher = better)
12    pub rssi: i16,
13    /// Last seen tick
14    pub last_seen: u32,
15    /// Messages received from this neighbor
16    pub rx_count: u16,
17    /// Valid flag
18    valid: bool,
19}
20
21impl Neighbor {
22    /// Create a new neighbor entry
23    pub fn new(node_id: u8, rssi: i16, tick: u32) -> Self {
24        Self {
25            node_id,
26            rssi,
27            last_seen: tick,
28            rx_count: 1,
29            valid: true,
30        }
31    }
32
33    /// Update neighbor info
34    pub fn update(&mut self, rssi: i16, tick: u32) {
35        // Exponential moving average for RSSI
36        self.rssi = ((self.rssi as i32 * 7 + rssi as i32) / 8) as i16;
37        self.last_seen = tick;
38        self.rx_count = self.rx_count.saturating_add(1);
39    }
40
41    /// Check if neighbor is valid
42    pub fn is_valid(&self) -> bool {
43        self.valid
44    }
45
46    /// Calculate link quality (0-100)
47    pub fn link_quality(&self) -> u8 {
48        // Map RSSI to quality
49        // -50 dBm = excellent (100)
50        // -100 dBm = poor (0)
51        let rssi = self.rssi.clamp(-100, -50);
52        ((rssi + 100) * 2).min(100) as u8
53    }
54}
55
56/// Fixed-size neighbor table
57pub struct NeighborTable<const N: usize> {
58    /// Neighbor entries
59    entries: [Neighbor; N],
60    /// Current tick
61    tick: u32,
62    /// Neighbor lifetime in ticks
63    lifetime: u32,
64}
65
66impl<const N: usize> NeighborTable<N> {
67    /// Create a new neighbor table
68    pub fn new(lifetime: u32) -> Self {
69        Self {
70            entries: [Neighbor::default(); N],
71            tick: 0,
72            lifetime,
73        }
74    }
75
76    /// Advance the tick counter
77    pub fn tick(&mut self) {
78        self.tick = self.tick.wrapping_add(1);
79    }
80
81    /// Update or add a neighbor
82    pub fn update(&mut self, node_id: u8, rssi: i16) {
83        // First, try to find existing entry
84        for entry in &mut self.entries {
85            if entry.valid && entry.node_id == node_id {
86                entry.update(rssi, self.tick);
87                return;
88            }
89        }
90
91        // Not found, try to add new entry
92        // First, try to find an invalid or expired slot
93        for entry in &mut self.entries {
94            if !entry.valid {
95                *entry = Neighbor::new(node_id, rssi, self.tick);
96                return;
97            }
98
99            let age = self.tick.wrapping_sub(entry.last_seen);
100            if age > self.lifetime {
101                *entry = Neighbor::new(node_id, rssi, self.tick);
102                return;
103            }
104        }
105
106        // Table full, replace weakest link
107        let mut worst_idx = 0;
108        let mut worst_quality = 255u8;
109
110        for (i, entry) in self.entries.iter().enumerate() {
111            if entry.valid {
112                let quality = entry.link_quality();
113                if quality < worst_quality {
114                    worst_quality = quality;
115                    worst_idx = i;
116                }
117            }
118        }
119
120        // Only replace if new neighbor has better signal
121        let new_quality = Neighbor::new(node_id, rssi, self.tick).link_quality();
122        if new_quality > worst_quality {
123            self.entries[worst_idx] = Neighbor::new(node_id, rssi, self.tick);
124        }
125    }
126
127    /// Get neighbor by node ID
128    pub fn get(&self, node_id: u8) -> Option<&Neighbor> {
129        for entry in &self.entries {
130            if entry.valid && entry.node_id == node_id {
131                let age = self.tick.wrapping_sub(entry.last_seen);
132                if age <= self.lifetime {
133                    return Some(entry);
134                }
135            }
136        }
137        None
138    }
139
140    /// Get best neighbor (highest link quality)
141    pub fn best_neighbor(&self) -> Option<&Neighbor> {
142        let mut best: Option<&Neighbor> = None;
143        let mut best_quality = 0u8;
144
145        for entry in &self.entries {
146            if entry.valid {
147                let age = self.tick.wrapping_sub(entry.last_seen);
148                if age <= self.lifetime {
149                    let quality = entry.link_quality();
150                    if quality > best_quality {
151                        best_quality = quality;
152                        best = Some(entry);
153                    }
154                }
155            }
156        }
157
158        best
159    }
160
161    /// Get all valid neighbors
162    pub fn iter(&self) -> impl Iterator<Item = &Neighbor> {
163        let tick = self.tick;
164        let lifetime = self.lifetime;
165
166        self.entries
167            .iter()
168            .filter(move |e| e.valid && tick.wrapping_sub(e.last_seen) <= lifetime)
169    }
170
171    /// Get number of valid neighbors
172    pub fn len(&self) -> usize {
173        self.iter().count()
174    }
175
176    /// Check if table is empty
177    pub fn is_empty(&self) -> bool {
178        self.len() == 0
179    }
180
181    /// Clear all entries
182    pub fn clear(&mut self) {
183        for entry in &mut self.entries {
184            entry.valid = false;
185        }
186    }
187
188    /// Expire old entries
189    pub fn expire_old(&mut self) {
190        for entry in &mut self.entries {
191            if entry.valid {
192                let age = self.tick.wrapping_sub(entry.last_seen);
193                if age > self.lifetime {
194                    entry.valid = false;
195                }
196            }
197        }
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn test_neighbor_creation() {
207        let neighbor = Neighbor::new(1, -70, 0);
208        assert_eq!(neighbor.node_id, 1);
209        assert_eq!(neighbor.rssi, -70);
210        assert!(neighbor.is_valid());
211    }
212
213    #[test]
214    fn test_neighbor_link_quality() {
215        // Excellent signal
216        let n1 = Neighbor::new(1, -50, 0);
217        assert_eq!(n1.link_quality(), 100);
218
219        // Good signal
220        let n2 = Neighbor::new(2, -70, 0);
221        assert_eq!(n2.link_quality(), 60);
222
223        // Poor signal
224        let n3 = Neighbor::new(3, -95, 0);
225        assert_eq!(n3.link_quality(), 10);
226
227        // Very poor (clamped)
228        let n4 = Neighbor::new(4, -120, 0);
229        assert_eq!(n4.link_quality(), 0);
230    }
231
232    #[test]
233    fn test_neighbor_table_basic() {
234        let mut table: NeighborTable<8> = NeighborTable::new(100);
235
236        table.update(1, -70);
237        table.update(2, -80);
238
239        assert_eq!(table.len(), 2);
240        assert!(table.get(1).is_some());
241        assert!(table.get(2).is_some());
242        assert!(table.get(3).is_none());
243    }
244
245    #[test]
246    fn test_neighbor_table_update() {
247        let mut table: NeighborTable<8> = NeighborTable::new(100);
248
249        table.update(1, -70);
250        assert_eq!(table.get(1).unwrap().rx_count, 1);
251
252        table.update(1, -65);
253        let neighbor = table.get(1).unwrap();
254        assert_eq!(neighbor.rx_count, 2);
255        // RSSI should be smoothed
256        assert!(neighbor.rssi > -70 && neighbor.rssi < -65);
257    }
258
259    #[test]
260    fn test_neighbor_table_expiry() {
261        let mut table: NeighborTable<8> = NeighborTable::new(10);
262
263        table.update(1, -70);
264        assert_eq!(table.len(), 1);
265
266        // Advance time past lifetime
267        for _ in 0..15 {
268            table.tick();
269        }
270
271        // Should be expired
272        assert!(table.get(1).is_none());
273        assert_eq!(table.len(), 0);
274    }
275
276    #[test]
277    fn test_neighbor_table_best() {
278        let mut table: NeighborTable<8> = NeighborTable::new(100);
279
280        table.update(1, -90); // Poor
281        table.update(2, -60); // Good
282        table.update(3, -80); // Medium
283
284        let best = table.best_neighbor().unwrap();
285        assert_eq!(best.node_id, 2);
286    }
287
288    #[test]
289    fn test_neighbor_table_full() {
290        let mut table: NeighborTable<4> = NeighborTable::new(1000);
291
292        // Fill table with weak signals
293        table.update(1, -95);
294        table.update(2, -95);
295        table.update(3, -95);
296        table.update(4, -95);
297        assert_eq!(table.len(), 4);
298
299        // Add stronger signal - should replace weakest
300        table.update(5, -60);
301        assert_eq!(table.len(), 4);
302        assert!(table.get(5).is_some());
303    }
304
305    #[test]
306    fn test_neighbor_table_clear() {
307        let mut table: NeighborTable<8> = NeighborTable::new(100);
308
309        table.update(1, -70);
310        table.update(2, -80);
311        assert_eq!(table.len(), 2);
312
313        table.clear();
314        assert_eq!(table.len(), 0);
315        assert!(table.is_empty());
316    }
317}