umsh_core/
route_slack.rs

1//! How many optional flood hops a send along an established route carries
2//! past the route's end.
3//!
4//! A learned route narrows a send's flood budget to what the route costs.
5//! The slack on top of that keeps delivery working when the far end of the
6//! path has changed since the route was learned—most often because the
7//! destination moved. It is spent only where something suggests that might
8//! have happened: the destination is known to be mobile, or the link nearest
9//! it is unmeasured or weak.
10//!
11//! The route's *last hop* is that link: between the destination and the
12//! first node that heard it. For a direct route it is this node's own
13//! reception of the destination; for a source route it is how the repeater
14//! named by the route's last hint heard the destination, as reported in the
15//! trace-signal entry that repeater wrote.
16
17use crate::options::TraceSignalEntry;
18
19/// The slack the policy grants, in flood hops, when it grants any.
20///
21/// Source-route hints must be consumed before this allowance can be used.
22/// Originating sends still respect their flood ceiling, including disabled
23/// flooding.
24pub const ESTABLISHED_ROUTE_EXTRA_FLOOD_HOPS: u8 = 1;
25
26/// A destination heard directly above this RSSI has enough link budget
27/// that the reverse direction can be trusted without a repeater backstop.
28pub const STRONG_DIRECT_RSSI_DBM: i16 = -75;
29
30/// A last hop measured at or below this RSSI is weak enough that the
31/// destination may already be out of that repeater's reach.
32pub const WEAK_LAST_HOP_RSSI_DBM: i16 = -96;
33
34/// What a sender knows about the far end of the route it is about to use.
35#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
36pub struct RouteEvidence {
37    /// The route was chosen by the application rather than learned.
38    pub hand_set: bool,
39    /// The route reaches the destination without a repeater.
40    pub direct: bool,
41    /// How the route's last hop was measured, if it was. A point-to-point
42    /// entry marks a hop with no radio in it.
43    pub last_hop: Option<TraceSignalEntry>,
44    /// The destination advertises itself as mobile.
45    pub peer_mobile: bool,
46    /// The frame being acknowledged is a retransmission of one this node
47    /// has already acknowledged: whatever went back the first time did not
48    /// arrive, however good the link toward this node looks.
49    pub peer_retransmitted: bool,
50}
51
52/// The flood hops a send along an established route carries past the
53/// route's end: [`ESTABLISHED_ROUTE_EXTRA_FLOOD_HOPS`] or none.
54///
55/// A route chosen by hand gets none: the application said where the frame
56/// goes. An acknowledgment of a retransmission gets the slack, since the
57/// last one did not get through. A destination heard directly and
58/// strongly, or over a link with no radio in it, gets none. Otherwise a
59/// mobile destination, an unmeasured last hop, or a weak one each earn the
60/// slack.
61pub fn established_route_slack(evidence: RouteEvidence) -> u8 {
62    if evidence.hand_set {
63        return 0;
64    }
65    if evidence.peer_retransmitted {
66        return ESTABLISHED_ROUTE_EXTRA_FLOOD_HOPS;
67    }
68    // `None`: never measured. `Some(None)`: a point-to-point hop.
69    let last_hop_rssi = evidence.last_hop.map(TraceSignalEntry::rssi_dbm);
70    let strong_direct = evidence.direct
71        && match last_hop_rssi {
72            Some(None) => true,
73            Some(Some(rssi)) => rssi > STRONG_DIRECT_RSSI_DBM,
74            None => false,
75        };
76    if strong_direct {
77        return 0;
78    }
79    if evidence.peer_mobile {
80        return ESTABLISHED_ROUTE_EXTRA_FLOOD_HOPS;
81    }
82    match last_hop_rssi {
83        None => ESTABLISHED_ROUTE_EXTRA_FLOOD_HOPS,
84        Some(Some(rssi)) if rssi <= WEAK_LAST_HOP_RSSI_DBM => ESTABLISHED_ROUTE_EXTRA_FLOOD_HOPS,
85        Some(_) => 0,
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    const SLACK: u8 = ESTABLISHED_ROUTE_EXTRA_FLOOD_HOPS;
94
95    fn air(rssi_dbm: i16) -> Option<TraceSignalEntry> {
96        Some(TraceSignalEntry::new(rssi_dbm, 0))
97    }
98
99    fn direct(last_hop: Option<TraceSignalEntry>) -> RouteEvidence {
100        RouteEvidence {
101            direct: true,
102            last_hop,
103            ..RouteEvidence::default()
104        }
105    }
106
107    fn routed(last_hop: Option<TraceSignalEntry>) -> RouteEvidence {
108        RouteEvidence {
109            last_hop,
110            ..RouteEvidence::default()
111        }
112    }
113
114    #[test]
115    fn a_route_chosen_by_hand_never_gets_slack() {
116        for evidence in [direct(air(-120)), routed(None), routed(air(-120))] {
117            let evidence = RouteEvidence {
118                hand_set: true,
119                peer_mobile: true,
120                ..evidence
121            };
122            assert_eq!(established_route_slack(evidence), 0);
123        }
124    }
125
126    #[test]
127    fn an_ack_for_a_retransmission_gets_slack_however_strong_the_link() {
128        for evidence in [
129            direct(air(-30)),
130            direct(Some(TraceSignalEntry::POINT_TO_POINT)),
131            routed(air(-40)),
132        ] {
133            let evidence = RouteEvidence {
134                peer_retransmitted: true,
135                ..evidence
136            };
137            assert_eq!(established_route_slack(evidence), SLACK, "{evidence:?}");
138        }
139        let hand_set = RouteEvidence {
140            hand_set: true,
141            peer_retransmitted: true,
142            ..direct(air(-30))
143        };
144        assert_eq!(established_route_slack(hand_set), 0);
145    }
146
147    #[test]
148    fn a_strong_or_bridged_direct_peer_gets_none_even_when_mobile() {
149        for last_hop in [air(-74), air(-30), Some(TraceSignalEntry::POINT_TO_POINT)] {
150            let evidence = RouteEvidence {
151                peer_mobile: true,
152                ..direct(last_hop)
153            };
154            assert_eq!(established_route_slack(evidence), 0, "{last_hop:?}");
155        }
156    }
157
158    #[test]
159    fn a_direct_peer_below_the_strong_threshold_falls_through_to_the_last_hop_rules() {
160        assert_eq!(established_route_slack(direct(air(-75))), 0);
161        assert_eq!(established_route_slack(direct(air(-95))), 0);
162        assert_eq!(established_route_slack(direct(air(-96))), SLACK);
163        let mobile = RouteEvidence {
164            peer_mobile: true,
165            ..direct(air(-75))
166        };
167        assert_eq!(established_route_slack(mobile), SLACK);
168    }
169
170    #[test]
171    fn an_unmeasured_last_hop_gets_slack() {
172        assert_eq!(established_route_slack(direct(None)), SLACK);
173        assert_eq!(established_route_slack(routed(None)), SLACK);
174    }
175
176    #[test]
177    fn a_mobile_destination_gets_slack_past_any_routed_last_hop() {
178        for last_hop in [air(-40), Some(TraceSignalEntry::POINT_TO_POINT)] {
179            let evidence = RouteEvidence {
180                peer_mobile: true,
181                ..routed(last_hop)
182            };
183            assert_eq!(established_route_slack(evidence), SLACK);
184        }
185    }
186
187    #[test]
188    fn only_a_weak_routed_last_hop_gets_slack() {
189        assert_eq!(established_route_slack(routed(air(-95))), 0);
190        assert_eq!(established_route_slack(routed(air(-96))), SLACK);
191        assert_eq!(established_route_slack(routed(air(-120))), SLACK);
192        assert_eq!(
193            established_route_slack(routed(Some(TraceSignalEntry::POINT_TO_POINT))),
194            0
195        );
196    }
197}