Skip to main content

pamoja_lorawan/relay/
state.rs

1//! What a network configures on a relay, and the relay commands that configure it,
2//! TS011-1.0.1 chapter 10.
3
4use pamoja_lora::region::{ChannelPlan, Modulation, RelayChannel};
5use pamoja_lora::LinkSettings;
6
7use super::ack::{CadPeriodicity, CadToRx, XtalAccuracy};
8use super::filter::JoinFilter;
9use super::forward::WorChannel;
10use super::limits::{CounterReset, ForwardLimits, TokenBucket, UNLIMITED_DEVICE_RATE};
11use super::trusted::{TrustedDevice, TrustedDevices};
12use crate::device::EndDevice;
13use crate::mac::{relay_second_channel, MacCommand};
14
15/// The symbols a scan takes to detect a preamble, besides the relay's time to switch to
16/// receiving.
17const DETECTION_SYMBOLS: u64 = 2;
18
19/// What a relay's radio can do, which every WOR ACK it sends reports.
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
21pub struct RelaySettings {
22    /// How accurate its crystal is.
23    pub xtal_accuracy: XtalAccuracy,
24    /// How many symbols it takes from detecting a preamble to receiving.
25    pub cad_to_rx: CadToRx,
26}
27
28impl RelaySettings {
29    /// Describes a relay's radio.
30    ///
31    /// # Arguments
32    ///
33    /// * `xtal_accuracy` - how accurate its crystal is.
34    /// * `cad_to_rx` - how many symbols it takes from detecting a preamble to receiving.
35    ///
36    /// # Returns
37    ///
38    /// The settings.
39    #[must_use]
40    pub const fn new(xtal_accuracy: XtalAccuracy, cad_to_rx: CadToRx) -> RelaySettings {
41        RelaySettings {
42            xtal_accuracy,
43            cad_to_rx,
44        }
45    }
46}
47
48/// Which channels a relay scans and how often, as `RelayConfReq` sets them, TS011-1.0.1
49/// section 10.1.
50#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
51pub struct RelayConfig {
52    /// How often the relay scans each channel.
53    pub cad_periodicity: CadPeriodicity,
54    /// The default channel, one of the region's.
55    pub default_channel: RelayChannel,
56    /// A second channel, scanned half a period after the default one.
57    pub second_channel: Option<RelayChannel>,
58}
59
60impl RelayConfig {
61    /// Scans one channel.
62    ///
63    /// # Arguments
64    ///
65    /// * `cad_periodicity` - how often.
66    /// * `default_channel` - the channel.
67    ///
68    /// # Returns
69    ///
70    /// The configuration.
71    #[must_use]
72    pub const fn new(
73        cad_periodicity: CadPeriodicity,
74        default_channel: RelayChannel,
75    ) -> RelayConfig {
76        RelayConfig {
77            cad_periodicity,
78            default_channel,
79            second_channel: None,
80        }
81    }
82
83    /// The configuration a relay runs before its network sets one: the region's first
84    /// default channel, scanned once a second.
85    ///
86    /// # Arguments
87    ///
88    /// * `plan` - the region's plan.
89    ///
90    /// # Returns
91    ///
92    /// The configuration, or `None` for a region with no relay channels.
93    #[must_use]
94    pub fn region_default(plan: &ChannelPlan) -> Option<RelayConfig> {
95        Some(RelayConfig::new(
96            CadPeriodicity::Ms1000,
97            plan.relay_channel(0)?,
98        ))
99    }
100
101    /// Adds a second channel.
102    ///
103    /// # Arguments
104    ///
105    /// * `channel` - the channel.
106    ///
107    /// # Returns
108    ///
109    /// The configuration, for chaining.
110    #[must_use]
111    pub const fn with_second_channel(mut self, channel: RelayChannel) -> RelayConfig {
112        self.second_channel = Some(channel);
113        self
114    }
115
116    /// Returns one of the channels.
117    ///
118    /// # Arguments
119    ///
120    /// * `channel` - which.
121    ///
122    /// # Returns
123    ///
124    /// The channel, or `None` for a second channel the configuration does not have.
125    #[must_use]
126    pub const fn channel(&self, channel: WorChannel) -> Option<RelayChannel> {
127        match channel {
128            WorChannel::Default => Some(self.default_channel),
129            WorChannel::Second => self.second_channel,
130        }
131    }
132}
133
134/// Everything a relay keeps besides its own device: its radio, its scan, and the tables
135/// its network sets.
136#[derive(Clone, Copy, Debug, PartialEq, Eq)]
137pub(crate) struct RelayState {
138    pub(crate) settings: RelaySettings,
139    pub(crate) config: Option<RelayConfig>,
140    pub(crate) anchor_us: Option<u64>,
141    pub(crate) reloads: u64,
142    pub(crate) filter: JoinFilter,
143    pub(crate) trusted: TrustedDevices,
144    pub(crate) limits: ForwardLimits,
145}
146
147impl RelayState {
148    pub(crate) const fn new(settings: RelaySettings) -> RelayState {
149        RelayState {
150            settings,
151            config: None,
152            anchor_us: None,
153            reloads: 0,
154            filter: JoinFilter::new(),
155            trusted: TrustedDevices::new(),
156            limits: ForwardLimits::new(),
157        }
158    }
159
160    /// Runs a configuration: a stopped relay starts its scan afresh, and a running one
161    /// keeps its scan times.
162    pub(crate) fn run(&mut self, config: RelayConfig) {
163        if self.config.is_none() {
164            self.anchor_us = None;
165        }
166        self.config = Some(config);
167    }
168
169    /// Stops the scan.
170    pub(crate) fn stop(&mut self) {
171        self.config = None;
172        self.anchor_us = None;
173    }
174
175    /// Adds what the buckets earned in the hours since the scan started.
176    pub(crate) fn reload(&mut self, now_us: u64) {
177        let Some(anchor) = self.anchor_us else {
178            return;
179        };
180        let hours = now_us.saturating_sub(anchor) / super::limits::RELOAD_PERIOD_US;
181        if hours > self.reloads {
182            let earned = hours - self.reloads;
183            self.limits.reload(earned);
184            self.trusted.reload(earned);
185            self.reloads = hours;
186        }
187    }
188
189    /// Acts on a relay command and says what to answer, TS011-1.0.1 sections 10.1 to 10.6.
190    pub(crate) fn command(
191        &mut self,
192        command: &MacCommand,
193        device: &EndDevice,
194    ) -> Option<MacCommand> {
195        match *command {
196            MacCommand::RelayConfReq {
197                enabled,
198                cad_periodicity,
199                default_channel_index,
200                second_channel_index,
201                second_channel_data_rate,
202                second_channel_ack_offset,
203                second_channel_frequency_hz,
204            } => {
205                if !enabled {
206                    self.stop();
207                    return Some(MacCommand::RelayConfAns {
208                        cad_periodicity_ack: true,
209                        default_channel_index_ack: true,
210                        second_channel_index_ack: true,
211                        second_channel_data_rate_ack: true,
212                        second_channel_ack_offset_ack: true,
213                        second_channel_frequency_ack: true,
214                    });
215                }
216                let plan = device.plan();
217                let periodicity = CadPeriodicity::from_code(cad_periodicity);
218                let default_channel = plan
219                    .relay_channel(default_channel_index)
220                    .filter(|channel| channel_ok(device, channel));
221                let second_channel_index_ack = second_channel_index <= 1;
222                let (data_rate_ack, ack_offset_ack, frequency_ack) = if second_channel_index == 1 {
223                    let data_rate_ack = wor_link(plan, second_channel_data_rate).is_some();
224                    let frequency_ack = device.tunes(second_channel_frequency_hz);
225                    let ack_offset_ack = relay_second_channel(
226                        1,
227                        second_channel_data_rate,
228                        second_channel_ack_offset,
229                        second_channel_frequency_hz,
230                    )
231                    .is_some_and(|channel| device.tunes(channel.ack_frequency_hz));
232                    (data_rate_ack, ack_offset_ack, frequency_ack)
233                } else {
234                    (true, true, true)
235                };
236                let second_channel = relay_second_channel(
237                    second_channel_index,
238                    second_channel_data_rate,
239                    second_channel_ack_offset,
240                    second_channel_frequency_hz,
241                );
242                let scans_fit = match (periodicity, default_channel) {
243                    (Some(periodicity), Some(default_channel)) => {
244                        let mut config = RelayConfig::new(periodicity, default_channel);
245                        config.second_channel = second_channel;
246                        scans_fit(plan, &config, self.settings.cad_to_rx)
247                    }
248                    _ => true,
249                };
250                let answer = MacCommand::RelayConfAns {
251                    cad_periodicity_ack: periodicity.is_some() && scans_fit,
252                    default_channel_index_ack: default_channel.is_some(),
253                    second_channel_index_ack,
254                    second_channel_data_rate_ack: data_rate_ack,
255                    second_channel_ack_offset_ack: ack_offset_ack,
256                    second_channel_frequency_ack: frequency_ack,
257                };
258                if let (Some(periodicity), Some(default_channel), true) = (
259                    periodicity,
260                    default_channel,
261                    scans_fit
262                        && second_channel_index_ack
263                        && data_rate_ack
264                        && ack_offset_ack
265                        && frequency_ack,
266                ) {
267                    self.run(RelayConfig {
268                        cad_periodicity: periodicity,
269                        default_channel,
270                        second_channel,
271                    });
272                }
273                Some(answer)
274            }
275            MacCommand::FilterListReq {
276                index,
277                action,
278                eui_len,
279                ref eui,
280            } => Some(self.filter.apply(index, action, eui_len, eui)),
281            MacCommand::UpdateUplinkListReq {
282                index,
283                reload_rate,
284                bucket_size,
285                dev_addr,
286                wfcnt,
287                ref root_wor_s_key,
288            } => {
289                let bucket = TokenBucket::coded(reload_rate, bucket_size, UNLIMITED_DEVICE_RATE);
290                self.trusted.set(
291                    index,
292                    TrustedDevice::new(dev_addr, root_wor_s_key, wfcnt, bucket),
293                );
294                Some(MacCommand::UpdateUplinkListAns)
295            }
296            MacCommand::CtrlUplinkListReq { index, action } => {
297                let wfcnt = self.trusted.get(index).map(TrustedDevice::last_wfcnt);
298                if wfcnt.is_some() && action == 1 {
299                    self.trusted.remove(index);
300                }
301                Some(MacCommand::CtrlUplinkListAns {
302                    index_ack: wfcnt.is_some(),
303                    wfcnt: wfcnt.unwrap_or(0),
304                })
305            }
306            MacCommand::ConfigureFwdLimitReq {
307                reset_limit_counters,
308                join_request_reload_rate,
309                notify_reload_rate,
310                global_uplink_reload_rate,
311                overall_reload_rate,
312                join_request_bucket_size,
313                notify_bucket_size,
314                global_uplink_bucket_size,
315                overall_bucket_size,
316            } => {
317                self.limits.configure(
318                    CounterReset::from_code(reset_limit_counters),
319                    [
320                        join_request_reload_rate,
321                        notify_reload_rate,
322                        global_uplink_reload_rate,
323                        overall_reload_rate,
324                    ],
325                    [
326                        join_request_bucket_size,
327                        notify_bucket_size,
328                        global_uplink_bucket_size,
329                        overall_bucket_size,
330                    ],
331                );
332                Some(MacCommand::ConfigureFwdLimitAns)
333            }
334            _ => None,
335        }
336    }
337}
338
339/// The LoRa settings of a relay channel's data rate, numbered in the downlink table.
340pub(crate) fn wor_link(plan: &ChannelPlan, data_rate: u8) -> Option<LinkSettings> {
341    match plan.downlink_data_rate(data_rate)?.modulation {
342        Modulation::LoRa {
343            spreading_factor,
344            bandwidth_hz,
345        } => Some(LinkSettings::new(spreading_factor, bandwidth_hz)),
346        _ => None,
347    }
348}
349
350/// Whether a relay channel is LoRa and its frequencies are ones the radio tunes.
351pub(crate) fn channel_ok(device: &EndDevice, channel: &RelayChannel) -> bool {
352    wor_link(device.plan(), channel.data_rate).is_some()
353        && device.tunes(channel.wor_frequency_hz)
354        && device.tunes(channel.ack_frequency_hz)
355}
356
357/// Whether a configuration is one a relay can run: LoRa channels it tunes, and scans far
358/// enough apart that each can detect a preamble and switch to receiving before the next.
359pub(crate) fn config_ok(device: &EndDevice, config: &RelayConfig, cad_to_rx: CadToRx) -> bool {
360    channel_ok(device, &config.default_channel)
361        && config
362            .second_channel
363            .is_none_or(|channel| channel_ok(device, &channel))
364        && scans_fit(device.plan(), config, cad_to_rx)
365}
366
367/// Whether every channel's scan, detection and switch to receiving fit in the time to the
368/// next scan: a whole period, or half of one with a second channel. A channel whose data
369/// rate is not LoRa is another check's to refuse.
370fn scans_fit(plan: &ChannelPlan, config: &RelayConfig, cad_to_rx: CadToRx) -> bool {
371    let period = config.cad_periodicity.period_us();
372    let interval = if config.second_channel.is_some() {
373        period / 2
374    } else {
375        period
376    };
377    [Some(config.default_channel), config.second_channel]
378        .into_iter()
379        .flatten()
380        .all(|channel| {
381            wor_link(plan, channel.data_rate).is_none_or(|link| {
382                (DETECTION_SYMBOLS + u64::from(cad_to_rx.symbols())) * link.symbol_time_us()
383                    <= interval
384            })
385        })
386}