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}