Skip to main content

pamoja_routing/
router.rs

1//! The routing table and the per-packet forwarding decision.
2
3/// A learned route to a destination: the neighbor to send through, and the cost.
4#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5pub struct Route {
6    dst: u32,
7    next_hop: u32,
8    cost: u16,
9}
10
11impl Route {
12    /// Returns the destination node this route reaches.
13    ///
14    /// # Returns
15    ///
16    /// The destination address.
17    pub fn dst(&self) -> u32 {
18        self.dst
19    }
20
21    /// Returns the neighbor to send through to reach the destination.
22    ///
23    /// # Returns
24    ///
25    /// The next-hop address.
26    pub fn next_hop(&self) -> u32 {
27        self.next_hop
28    }
29
30    /// Returns the cost of this route, in whatever metric the caller reports (hop count,
31    /// summed link cost, or another).
32    ///
33    /// # Returns
34    ///
35    /// The route cost; lower is better.
36    pub fn cost(&self) -> u16 {
37        self.cost
38    }
39}
40
41/// What to do with a packet bound for a given destination.
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub enum Forward {
44    /// The packet is for this node; hand it to the application.
45    Deliver,
46    /// A route is known; unicast the packet to this next hop.
47    Relay(u32),
48    /// No route is known; fall back to flooding the packet.
49    Flood,
50}
51
52/// A fixed-size routing table for one node.
53///
54/// The table holds up to `N` routes, learned from the traffic the node hears. It keeps the
55/// cheapest route it knows to each destination, and when full it gives up the most
56/// expensive route to make room for a cheaper one, so its limited memory holds the routes
57/// most worth keeping.
58///
59/// # Examples
60///
61/// ```
62/// use pamoja_routing::{Forward, Router};
63///
64/// // A soil probe, node 10, reaches the gateway, node 11, through a relay, node 12,
65/// // three hops out.
66/// let (probe, gateway, relay) = (10, 11, 12);
67/// let mut router: Router<8> = Router::new(probe);
68/// router.observe(gateway, relay, 3);
69/// assert_eq!(router.next_hop(gateway), Some(relay));
70///
71/// // A packet addressed to the probe itself is delivered, not forwarded.
72/// assert_eq!(router.forward(probe), Forward::Deliver);
73/// ```
74#[derive(Clone, Copy, Debug)]
75pub struct Router<const N: usize> {
76    me: u32,
77    routes: [Option<Route>; N],
78}
79
80impl<const N: usize> Router<N> {
81    /// Creates an empty router for the node at `me`.
82    ///
83    /// # Arguments
84    ///
85    /// * `me` - this node's address.
86    ///
87    /// # Returns
88    ///
89    /// A router holding no routes.
90    pub const fn new(me: u32) -> Self {
91        Router {
92            me,
93            routes: [None; N],
94        }
95    }
96
97    /// Returns this node's address.
98    ///
99    /// # Returns
100    ///
101    /// The address the router was created with.
102    pub fn address(&self) -> u32 {
103        self.me
104    }
105
106    /// Returns how many routes this table can hold.
107    ///
108    /// # Returns
109    ///
110    /// `N`, the size fixed in the type.
111    pub const fn capacity(&self) -> usize {
112        N
113    }
114
115    /// Lists the routes the table holds.
116    ///
117    /// # Returns
118    ///
119    /// Each route once, in the order the table holds them. A new route takes the first
120    /// free slot and one that displaces another takes that slot, so the order is the
121    /// table's own, not sorted by destination or cost.
122    ///
123    /// # Examples
124    ///
125    /// ```
126    /// use pamoja_routing::Router;
127    ///
128    /// // A weather mast, node 1, has heard a rain gauge and a wind vane.
129    /// let (mast, rain_gauge, wind_vane, relay) = (1, 20, 21, 5);
130    /// let mut router: Router<4> = Router::new(mast);
131    /// router.observe(rain_gauge, relay, 2);
132    /// router.observe(wind_vane, wind_vane, 1);
133    ///
134    /// let held: Vec<(u32, u32, u16)> = router
135    ///     .routes()
136    ///     .map(|route| (route.dst(), route.next_hop(), route.cost()))
137    ///     .collect();
138    /// assert_eq!(held, [(rain_gauge, relay, 2), (wind_vane, wind_vane, 1)]);
139    /// ```
140    pub fn routes(&self) -> impl Iterator<Item = Route> + '_ {
141        self.routes.iter().flatten().copied()
142    }
143
144    /// Learns the way to a node from a packet heard from it.
145    ///
146    /// A packet that originated at `origin` and reached this node via the neighbor `via`
147    /// proves `via` is a way back to `origin` at the reported `cost`. The router adopts the
148    /// route if it is cheaper than what it knows, or if it refreshes the cost of the route
149    /// it is already using, and ignores a route to itself.
150    ///
151    /// # Arguments
152    ///
153    /// * `origin` - the node the packet came from, the destination this route reaches.
154    /// * `via` - the neighbor the packet arrived through, the next hop for this route.
155    /// * `cost` - the cost the packet reports for reaching `origin` through `via`.
156    ///
157    /// # Returns
158    ///
159    /// `true` if the table changed (a route was added, redirected, or recosted), `false`
160    /// if the observation taught it nothing new.
161    pub fn observe(&mut self, origin: u32, via: u32, cost: u16) -> bool {
162        observe_into(&mut self.routes, self.me, origin, via, cost)
163    }
164
165    /// Returns the next hop to reach a destination, if a route is known.
166    ///
167    /// # Arguments
168    ///
169    /// * `dst` - the destination to reach.
170    ///
171    /// # Returns
172    ///
173    /// The next-hop address, or [`None`] if no route is known.
174    pub fn next_hop(&self, dst: u32) -> Option<u32> {
175        self.route(dst).map(|route| route.next_hop)
176    }
177
178    /// Returns the cost of the known route to a destination, if any.
179    ///
180    /// # Arguments
181    ///
182    /// * `dst` - the destination to reach.
183    ///
184    /// # Returns
185    ///
186    /// The route cost, or [`None`] if no route is known.
187    pub fn cost(&self, dst: u32) -> Option<u16> {
188        self.route(dst).map(|route| route.cost)
189    }
190
191    /// Returns the known route to a destination, if any.
192    ///
193    /// # Arguments
194    ///
195    /// * `dst` - the destination to reach.
196    ///
197    /// # Returns
198    ///
199    /// The [`Route`], or [`None`] if no route is known.
200    pub fn route(&self, dst: u32) -> Option<Route> {
201        route_in(&self.routes, dst)
202    }
203
204    /// Decides what to do with a packet bound for a destination.
205    ///
206    /// # Arguments
207    ///
208    /// * `dst` - the packet's destination.
209    ///
210    /// # Returns
211    ///
212    /// [`Forward::Deliver`] if the packet is for this node, [`Forward::Relay`] with the
213    /// next hop if a route is known, or [`Forward::Flood`] otherwise.
214    pub fn forward(&self, dst: u32) -> Forward {
215        forward_in(&self.routes, self.me, dst)
216    }
217
218    /// Forgets the route to a destination, if one is held.
219    ///
220    /// # Arguments
221    ///
222    /// * `dst` - the destination whose route to drop.
223    pub fn forget(&mut self, dst: u32) {
224        forget_in(&mut self.routes, dst)
225    }
226
227    /// Returns how many routes the table currently holds.
228    ///
229    /// # Returns
230    ///
231    /// The number of routes.
232    pub fn len(&self) -> usize {
233        self.routes.iter().filter(|slot| slot.is_some()).count()
234    }
235
236    /// Reports whether the table holds no routes.
237    ///
238    /// # Returns
239    ///
240    /// `true` if no routes are held.
241    pub fn is_empty(&self) -> bool {
242        self.routes.iter().all(Option::is_none)
243    }
244}
245
246// Learns a route into a slot slice. Split out so the fixed-size and runtime-sized tables
247// share one implementation rather than two that can drift.
248fn observe_into(routes: &mut [Option<Route>], me: u32, origin: u32, via: u32, cost: u16) -> bool {
249    if origin == me {
250        return false;
251    }
252    if let Some(index) = index_of_in(routes, origin) {
253        let route = routes[index]
254            .as_mut()
255            .expect("index_of_in points at a route");
256        if cost < route.cost || via == route.next_hop {
257            let changed = route.next_hop != via || route.cost != cost;
258            route.next_hop = via;
259            route.cost = cost;
260            return changed;
261        }
262        return false;
263    }
264
265    let new = Route {
266        dst: origin,
267        next_hop: via,
268        cost,
269    };
270    if let Some(empty) = routes.iter().position(Option::is_none) {
271        routes[empty] = Some(new);
272        return true;
273    }
274
275    // The table is full; replace the costliest route if this one is cheaper. A capacity of
276    // zero leaves nothing to replace, so the observation is dropped.
277    if let Some((worst, worst_cost)) = routes
278        .iter()
279        .enumerate()
280        .filter_map(|(i, slot)| slot.as_ref().map(|route| (i, route.cost)))
281        .max_by_key(|&(_, cost)| cost)
282    {
283        if cost < worst_cost {
284            routes[worst] = Some(new);
285            return true;
286        }
287    }
288    false
289}
290
291// The slot index of the route to `dst`, if one is held.
292fn index_of_in(routes: &[Option<Route>], dst: u32) -> Option<usize> {
293    routes
294        .iter()
295        .position(|slot| slot.as_ref().is_some_and(|route| route.dst == dst))
296}
297
298// The route to `dst`, if one is held.
299fn route_in(routes: &[Option<Route>], dst: u32) -> Option<Route> {
300    index_of_in(routes, dst).map(|index| routes[index].expect("index_of_in points at a route"))
301}
302
303// Decides what to do with a packet bound for `dst`.
304fn forward_in(routes: &[Option<Route>], me: u32, dst: u32) -> Forward {
305    if dst == me {
306        return Forward::Deliver;
307    }
308    match route_in(routes, dst) {
309        Some(route) => Forward::Relay(route.next_hop),
310        None => Forward::Flood,
311    }
312}
313
314// Drops the route to `dst`, if one is held.
315fn forget_in(routes: &mut [Option<Route>], dst: u32) {
316    if let Some(index) = index_of_in(routes, dst) {
317        routes[index] = None;
318    }
319}
320
321/// A routing table whose size is chosen when it is built, rather than at compile time.
322///
323/// [`Router`] fixes its capacity in the type, which suits a microcontroller that knows its
324/// own limits. A gateway, or any caller reaching this through a language binding, does not
325/// know the size until it runs, and a const generic cannot cross a foreign function
326/// boundary at all. This is the same table with its slots on the heap, so both share one
327/// implementation and answer identically.
328///
329/// Requires the `alloc` feature.
330///
331/// # Examples
332///
333/// ```
334/// use pamoja_routing::{DynamicRouter, Forward};
335///
336/// // A gateway sizes its table for the mesh it is actually serving.
337/// let (gateway, pump, relay) = (1, 9, 5);
338/// let mut router = DynamicRouter::new(gateway, 512);
339/// router.observe(pump, relay, 2);
340/// assert_eq!(router.forward(pump), Forward::Relay(relay));
341/// assert_eq!(router.capacity(), 512);
342/// ```
343#[cfg(any(feature = "alloc", test))]
344#[derive(Clone, Debug)]
345pub struct DynamicRouter {
346    me: u32,
347    routes: alloc::vec::Vec<Option<Route>>,
348}
349
350#[cfg(any(feature = "alloc", test))]
351impl DynamicRouter {
352    /// Creates an empty router holding up to `capacity` routes.
353    ///
354    /// # Arguments
355    ///
356    /// * `me` - this node's address.
357    /// * `capacity` - how many routes to make room for. A capacity of zero is allowed and
358    ///   makes every unknown destination flood, which is the behavior with no table.
359    ///
360    /// # Returns
361    ///
362    /// A router holding no routes.
363    pub fn new(me: u32, capacity: usize) -> Self {
364        DynamicRouter {
365            me,
366            routes: alloc::vec![None; capacity],
367        }
368    }
369
370    /// Returns this node's address.
371    ///
372    /// # Returns
373    ///
374    /// The address the router was created with.
375    pub fn address(&self) -> u32 {
376        self.me
377    }
378
379    /// Returns how many routes this table can hold.
380    ///
381    /// # Returns
382    ///
383    /// The capacity it was created with.
384    pub fn capacity(&self) -> usize {
385        self.routes.len()
386    }
387
388    /// Lists the routes the table holds.
389    ///
390    /// # Returns
391    ///
392    /// Each route once, in the order the table holds them, which is the order
393    /// [`Router::routes`] gives for the same history.
394    pub fn routes(&self) -> impl Iterator<Item = Route> + '_ {
395        self.routes.iter().flatten().copied()
396    }
397
398    /// Learns the way to a node from a packet heard from it.
399    ///
400    /// # Arguments
401    ///
402    /// * `origin` - the node the packet came from, the destination this route reaches.
403    /// * `via` - the neighbor the packet arrived through, the next hop for this route.
404    /// * `cost` - the cost the packet reports for reaching `origin` through `via`.
405    ///
406    /// # Returns
407    ///
408    /// `true` if the table changed, `false` if the observation taught it nothing new.
409    pub fn observe(&mut self, origin: u32, via: u32, cost: u16) -> bool {
410        observe_into(&mut self.routes, self.me, origin, via, cost)
411    }
412
413    /// Returns the next hop to reach a destination, if a route is known.
414    ///
415    /// # Arguments
416    ///
417    /// * `dst` - the destination to reach.
418    ///
419    /// # Returns
420    ///
421    /// The next-hop address, or [`None`] if no route is known.
422    pub fn next_hop(&self, dst: u32) -> Option<u32> {
423        route_in(&self.routes, dst).map(|route| route.next_hop)
424    }
425
426    /// Returns the cost of the known route to a destination, if any.
427    ///
428    /// # Arguments
429    ///
430    /// * `dst` - the destination to reach.
431    ///
432    /// # Returns
433    ///
434    /// The route cost, or [`None`] if no route is known.
435    pub fn cost(&self, dst: u32) -> Option<u16> {
436        route_in(&self.routes, dst).map(|route| route.cost)
437    }
438
439    /// Returns the known route to a destination, if any.
440    ///
441    /// # Arguments
442    ///
443    /// * `dst` - the destination to reach.
444    ///
445    /// # Returns
446    ///
447    /// The [`Route`], or [`None`] if no route is known.
448    pub fn route(&self, dst: u32) -> Option<Route> {
449        route_in(&self.routes, dst)
450    }
451
452    /// Decides what to do with a packet bound for a destination.
453    ///
454    /// # Arguments
455    ///
456    /// * `dst` - the packet's destination.
457    ///
458    /// # Returns
459    ///
460    /// [`Forward::Deliver`] if the packet is for this node, [`Forward::Relay`] with the
461    /// next hop if a route is known, or [`Forward::Flood`] otherwise.
462    pub fn forward(&self, dst: u32) -> Forward {
463        forward_in(&self.routes, self.me, dst)
464    }
465
466    /// Forgets the route to a destination, if one is held.
467    ///
468    /// # Arguments
469    ///
470    /// * `dst` - the destination whose route to drop.
471    pub fn forget(&mut self, dst: u32) {
472        forget_in(&mut self.routes, dst)
473    }
474
475    /// Returns how many routes the table currently holds.
476    ///
477    /// # Returns
478    ///
479    /// The number of routes.
480    pub fn len(&self) -> usize {
481        self.routes.iter().filter(|slot| slot.is_some()).count()
482    }
483
484    /// Reports whether the table holds no routes.
485    ///
486    /// # Returns
487    ///
488    /// `true` if no routes are held.
489    pub fn is_empty(&self) -> bool {
490        self.routes.iter().all(Option::is_none)
491    }
492}
493
494#[cfg(test)]
495mod tests {
496    use super::*;
497
498    #[test]
499    fn a_learned_route_is_used() {
500        let mut router: Router<8> = Router::new(1);
501        assert!(router.observe(9, 5, 2));
502        assert_eq!(router.next_hop(9), Some(5));
503        assert_eq!(router.cost(9), Some(2));
504        assert_eq!(router.forward(9), Forward::Relay(5));
505    }
506
507    #[test]
508    fn a_packet_for_this_node_is_delivered() {
509        let router: Router<8> = Router::new(1);
510        assert_eq!(router.forward(1), Forward::Deliver);
511    }
512
513    #[test]
514    fn an_unknown_destination_floods() {
515        let router: Router<8> = Router::new(1);
516        assert_eq!(router.forward(42), Forward::Flood);
517    }
518
519    #[test]
520    fn a_cheaper_route_replaces_a_costlier_one() {
521        let mut router: Router<8> = Router::new(1);
522        router.observe(9, 5, 4);
523        assert!(router.observe(9, 7, 1));
524        assert_eq!(router.next_hop(9), Some(7));
525        assert_eq!(router.cost(9), Some(1));
526    }
527
528    #[test]
529    fn a_costlier_route_is_ignored() {
530        let mut router: Router<8> = Router::new(1);
531        router.observe(9, 7, 1);
532        assert!(!router.observe(9, 5, 4));
533        assert_eq!(router.next_hop(9), Some(7));
534    }
535
536    #[test]
537    fn the_current_next_hop_can_refresh_its_cost() {
538        let mut router: Router<8> = Router::new(1);
539        router.observe(9, 7, 1);
540        // The same neighbor now reports a higher cost; we trust our current path.
541        assert!(router.observe(9, 7, 3));
542        assert_eq!(router.cost(9), Some(3));
543    }
544
545    #[test]
546    fn we_never_route_to_ourselves() {
547        let mut router: Router<8> = Router::new(1);
548        assert!(!router.observe(1, 5, 1));
549        assert_eq!(router.route(1), None);
550    }
551
552    #[test]
553    fn a_full_table_evicts_its_costliest_route_for_a_cheaper_one() {
554        let mut router: Router<2> = Router::new(1);
555        router.observe(10, 2, 5);
556        router.observe(11, 3, 8); // the costliest
557        assert_eq!(router.len(), 2);
558
559        // A cheaper route than the costliest evicts it.
560        assert!(router.observe(12, 4, 2));
561        assert_eq!(router.next_hop(11), None); // evicted
562        assert_eq!(router.next_hop(10), Some(2)); // kept
563        assert_eq!(router.next_hop(12), Some(4)); // added
564    }
565
566    #[test]
567    fn a_full_table_keeps_its_routes_against_a_costlier_one() {
568        let mut router: Router<2> = Router::new(1);
569        router.observe(10, 2, 5);
570        router.observe(11, 3, 8);
571        // A new route costlier than everything held is not worth a slot.
572        assert!(!router.observe(12, 4, 9));
573        assert_eq!(router.next_hop(12), None);
574        assert_eq!(router.len(), 2);
575    }
576
577    #[test]
578    fn forgetting_a_route_drops_it() {
579        let mut router: Router<8> = Router::new(1);
580        router.observe(9, 5, 2);
581        router.forget(9);
582        assert_eq!(router.route(9), None);
583        assert!(router.is_empty());
584    }
585
586    #[test]
587    fn an_empty_router_reports_empty() {
588        let router: Router<8> = Router::new(1);
589        assert!(router.is_empty());
590        assert_eq!(router.len(), 0);
591    }
592
593    #[test]
594    fn a_zero_capacity_router_never_learns_but_does_not_panic() {
595        let mut router: Router<0> = Router::new(1);
596        assert!(!router.observe(9, 5, 2));
597        assert_eq!(router.next_hop(9), None);
598        assert_eq!(router.forward(9), Forward::Flood);
599        assert!(router.is_empty());
600    }
601
602    #[test]
603    fn a_runtime_sized_table_decides_the_same_way() {
604        let mut fixed: Router<8> = Router::new(1);
605        let mut dynamic = DynamicRouter::new(1, 8);
606        for (origin, via, cost) in [(9u32, 5u32, 4u16), (9, 7, 1), (10, 5, 3), (1, 2, 1)] {
607            assert_eq!(
608                fixed.observe(origin, via, cost),
609                dynamic.observe(origin, via, cost),
610                "the two tables learn identically"
611            );
612        }
613        for dst in [1u32, 9, 10, 42] {
614            assert_eq!(fixed.forward(dst), dynamic.forward(dst));
615            assert_eq!(fixed.route(dst), dynamic.route(dst));
616        }
617        assert_eq!(fixed.len(), dynamic.len());
618        assert!(fixed.routes().eq(dynamic.routes()));
619
620        fixed.forget(9);
621        dynamic.forget(9);
622        assert_eq!(fixed.forward(9), dynamic.forward(9));
623        assert_eq!(fixed.len(), dynamic.len());
624        assert!(fixed.routes().eq(dynamic.routes()));
625    }
626
627    #[test]
628    fn a_fixed_table_reports_the_size_in_its_type() {
629        let router: Router<6> = Router::new(1);
630        assert_eq!(router.capacity(), 6);
631    }
632
633    #[test]
634    fn the_routes_held_are_listed_in_the_order_the_table_holds_them() {
635        let mut router: Router<4> = Router::new(1);
636        router.observe(9, 5, 2);
637        router.observe(10, 5, 3);
638        router.observe(11, 7, 1);
639        router.forget(10);
640        router.observe(12, 3, 4);
641        let held: Vec<u32> = router.routes().map(|route| route.dst()).collect();
642        assert_eq!(held, [9, 12, 11], "a new route takes the first free slot");
643    }
644
645    #[test]
646    fn a_displacing_route_takes_the_slot_of_the_route_it_displaced() {
647        let mut router: Router<2> = Router::new(1);
648        router.observe(10, 5, 3);
649        router.observe(32, 3, 5);
650        assert!(router.observe(11, 7, 2));
651        let held: Vec<(u32, u32, u16)> = router
652            .routes()
653            .map(|route| (route.dst(), route.next_hop(), route.cost()))
654            .collect();
655        assert_eq!(held, [(10, 5, 3), (11, 7, 2)]);
656    }
657
658    #[cfg(any(target_pointer_width = "32", target_pointer_width = "64"))]
659    #[test]
660    fn a_route_slot_costs_sixteen_bytes() {
661        assert_eq!(core::mem::size_of::<Router<64>>(), 4 + 64 * 16);
662    }
663
664    #[test]
665    fn a_runtime_sized_table_fills_to_the_size_it_was_given() {
666        let mut router = DynamicRouter::new(1, 3);
667        assert_eq!(router.capacity(), 3);
668        assert!(router.is_empty());
669        for node in 0..10u32 {
670            router.observe(node + 0x100, 0x05, 4);
671        }
672        assert_eq!(router.len(), 3, "it holds no more than it was sized for");
673    }
674
675    #[test]
676    fn a_table_with_no_room_floods_everything() {
677        let mut router = DynamicRouter::new(1, 0);
678        assert!(!router.observe(9, 5, 2), "there is nowhere to put a route");
679        assert_eq!(router.forward(9), Forward::Flood);
680        assert_eq!(
681            router.forward(1),
682            Forward::Deliver,
683            "a local packet still arrives"
684        );
685    }
686}