umsh_mac/
observations.rs

1//! What the radio has heard from whom, independent of who it was for.
2//!
3//! Route learning records the way to a *peer*; this records the last
4//! reception from a *transmitter*. The two differ for exactly the traffic
5//! that makes a repeater useful: a frame forwarded past this node teaches
6//! nothing about a peer, and a frame overheard on the air never reaches the
7//! host at all, but both prove a neighbor was on the air and how well it was
8//! heard. That is what a
9//! [Peer Repeaters Response](../../docs/protocol/src/mac-commands.md) reports
10//! about the hops it names.
11
12use umsh_core::RouterHint;
13use umsh_hal::Snr;
14
15/// How many transmitters the table remembers.
16///
17/// A neighborhood larger than this is one where the least recently heard
18/// entries are the ones worth losing, and the whole table has to fit a
19/// single response page's worth of answers anyway.
20pub const MAX_TRANSMITTER_OBSERVATIONS: usize = 16;
21
22/// The most recent reception from one transmitter.
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub struct TransmitterObservation {
25    /// All a trace or source route reveals about a hop, and so all this
26    /// table can key on.
27    pub hint: RouterHint,
28    /// Received signal strength of the most recent reception, in dBm.
29    pub rssi_dbm: i16,
30    /// Signal-to-noise ratio of the most recent reception.
31    pub snr: Snr,
32    /// When that reception was, on the monotonic clock.
33    pub last_seen_ms: u64,
34}
35
36/// A bounded, least-recently-heard table of transmitter observations.
37#[derive(Clone, Debug, Default)]
38pub struct TransmitterObservations {
39    entries: heapless::Vec<TransmitterObservation, MAX_TRANSMITTER_OBSERVATIONS>,
40}
41
42impl TransmitterObservations {
43    pub const fn new() -> Self {
44        Self {
45            entries: heapless::Vec::new(),
46        }
47    }
48
49    /// Record a reception from `hint`, replacing whatever was known before.
50    ///
51    /// Only the latest reception is kept: a peer-repeater entry reports the
52    /// most recent measurement, and an average across a moving neighbor
53    /// would describe a link that no longer exists.
54    pub fn observe(&mut self, hint: RouterHint, rssi_dbm: i16, snr: Snr, now_ms: u64) {
55        if let Some(entry) = self.entries.iter_mut().find(|entry| entry.hint == hint) {
56            entry.rssi_dbm = rssi_dbm;
57            entry.snr = snr;
58            entry.last_seen_ms = now_ms;
59            return;
60        }
61        let observation = TransmitterObservation {
62            hint,
63            rssi_dbm,
64            snr,
65            last_seen_ms: now_ms,
66        };
67        if self.entries.push(observation).is_ok() {
68            return;
69        }
70        // Full: the least recently heard transmitter is the one whose
71        // absence says the least.
72        let Some(oldest) = self
73            .entries
74            .iter_mut()
75            .min_by_key(|entry| entry.last_seen_ms)
76        else {
77            return;
78        };
79        *oldest = observation;
80    }
81
82    pub fn iter(&self) -> impl Iterator<Item = &TransmitterObservation> {
83        self.entries.iter()
84    }
85
86    pub fn len(&self) -> usize {
87        self.entries.len()
88    }
89
90    pub fn is_empty(&self) -> bool {
91        self.entries.is_empty()
92    }
93
94    /// The observation for one transmitter, if it is still held.
95    pub fn get(&self, hint: &RouterHint) -> Option<&TransmitterObservation> {
96        self.entries.iter().find(|entry| &entry.hint == hint)
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    fn hint(seed: u8) -> RouterHint {
105        RouterHint([seed, seed])
106    }
107
108    #[test]
109    fn a_repeat_reception_replaces_what_was_known_rather_than_adding_to_it() {
110        let mut table = TransmitterObservations::new();
111        table.observe(hint(1), -100, Snr::from_decibels(-9), 1_000);
112        table.observe(hint(1), -70, Snr::from_decibels(6), 2_000);
113        assert_eq!(table.len(), 1);
114        let entry = table.get(&hint(1)).unwrap();
115        assert_eq!(entry.rssi_dbm, -70);
116        assert_eq!(entry.snr, Snr::from_decibels(6));
117        assert_eq!(entry.last_seen_ms, 2_000);
118    }
119
120    #[test]
121    fn a_full_table_drops_the_least_recently_heard_transmitter() {
122        let mut table = TransmitterObservations::new();
123        for seed in 0..MAX_TRANSMITTER_OBSERVATIONS as u8 {
124            table.observe(
125                hint(seed),
126                -90,
127                Snr::from_decibels(0),
128                1_000 + u64::from(seed),
129            );
130        }
131        // Refresh the oldest so a plain insertion-order eviction would pick
132        // the wrong one.
133        table.observe(hint(0), -80, Snr::from_decibels(1), 9_000);
134        table.observe(hint(200), -95, Snr::from_decibels(-2), 10_000);
135
136        assert_eq!(table.len(), MAX_TRANSMITTER_OBSERVATIONS);
137        assert!(table.get(&hint(200)).is_some());
138        assert!(
139            table.get(&hint(0)).is_some(),
140            "refreshed, so not the oldest"
141        );
142        assert!(table.get(&hint(1)).is_none(), "least recently heard");
143    }
144}