Skip to main content

pamoja_lorawan/relay/
limits.rs

1//! The token buckets that bound what a relay forwards, TS011-1.0.1 section 8.8.
2
3use crate::mac::RELAY_BUCKET_MULTIPLIER;
4
5/// How often every bucket earns its reload rate, in microseconds: an hour.
6pub const RELOAD_PERIOD_US: u64 = 3_600_000_000;
7
8/// The reload rate a trusted end device's bucket is coded with to mean no limit,
9/// TS011-1.0.1 table 54.
10pub const UNLIMITED_DEVICE_RATE: u8 = 63;
11
12/// The reload rate a relay-wide bucket is coded with to mean no limit, TS011-1.0.1 table 64.
13pub const UNLIMITED_RELAY_RATE: u8 = 127;
14
15/// A token bucket: it earns a number of tokens an hour up to its size, and a relay spends
16/// one on each message it lets through.
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
18pub struct TokenBucket {
19    reload_rate: Option<u8>,
20    size: u16,
21    tokens: u16,
22}
23
24impl TokenBucket {
25    /// A bucket that earns `reload_rate` tokens an hour and holds `size`, starting full.
26    ///
27    /// # Arguments
28    ///
29    /// * `reload_rate` - tokens earned an hour.
30    /// * `size` - the most tokens it holds.
31    ///
32    /// # Returns
33    ///
34    /// The bucket.
35    #[must_use]
36    pub const fn new(reload_rate: u8, size: u16) -> TokenBucket {
37        TokenBucket {
38            reload_rate: Some(reload_rate),
39            size,
40            tokens: size,
41        }
42    }
43
44    /// A bucket as a relay command codes it, starting full.
45    ///
46    /// # Arguments
47    ///
48    /// * `reload_rate` - tokens earned an hour, or `unlimited` for no limit.
49    /// * `bucket_size` - the coded size multiplier, TS011-1.0.1 table 55; only its low two
50    ///   bits are read.
51    /// * `unlimited` - the rate that means no limit: [`UNLIMITED_DEVICE_RATE`] or
52    ///   [`UNLIMITED_RELAY_RATE`].
53    ///
54    /// # Returns
55    ///
56    /// The bucket.
57    #[must_use]
58    pub const fn coded(reload_rate: u8, bucket_size: u8, unlimited: u8) -> TokenBucket {
59        if reload_rate >= unlimited {
60            return TokenBucket::unlimited();
61        }
62        let multiplier = RELAY_BUCKET_MULTIPLIER[(bucket_size & 0x03) as usize];
63        TokenBucket::new(reload_rate, reload_rate as u16 * multiplier)
64    }
65
66    /// A bucket that never runs out.
67    ///
68    /// # Returns
69    ///
70    /// The bucket.
71    #[must_use]
72    pub const fn unlimited() -> TokenBucket {
73        TokenBucket {
74            reload_rate: None,
75            size: 0,
76            tokens: 0,
77        }
78    }
79
80    /// Returns how many tokens the bucket earns an hour.
81    ///
82    /// # Returns
83    ///
84    /// The rate, or `None` for no limit.
85    #[must_use]
86    pub const fn reload_rate(&self) -> Option<u8> {
87        self.reload_rate
88    }
89
90    /// Returns the most tokens the bucket holds.
91    ///
92    /// # Returns
93    ///
94    /// The size, 0 for a bucket with no limit.
95    #[must_use]
96    pub const fn size(&self) -> u16 {
97        self.size
98    }
99
100    /// Returns the tokens the bucket holds now.
101    ///
102    /// # Returns
103    ///
104    /// The count, 0 for a bucket with no limit.
105    #[must_use]
106    pub const fn tokens(&self) -> u16 {
107        self.tokens
108    }
109
110    /// Reports whether a message may pass.
111    ///
112    /// # Returns
113    ///
114    /// `true` for a bucket with no limit or a token left.
115    #[must_use]
116    pub const fn has_token(&self) -> bool {
117        self.reload_rate.is_none() || self.tokens > 0
118    }
119
120    /// Reports whether the bucket is empty and never refills, which a relay reports as
121    /// forwarding disabled.
122    ///
123    /// # Returns
124    ///
125    /// `true` for an empty bucket that earns nothing.
126    #[must_use]
127    pub const fn disabled(&self) -> bool {
128        matches!(self.reload_rate, Some(0)) && self.tokens == 0
129    }
130
131    /// Spends a token.
132    ///
133    /// # Returns
134    ///
135    /// `false`, spending nothing, when the bucket is empty.
136    pub fn take(&mut self) -> bool {
137        match self.reload_rate {
138            None => true,
139            Some(_) if self.tokens > 0 => {
140                self.tokens -= 1;
141                true
142            }
143            Some(_) => false,
144        }
145    }
146
147    /// Adds what the bucket earns over some hours, keeping no more than its size.
148    ///
149    /// # Arguments
150    ///
151    /// * `hours` - the whole hours since the last reload.
152    pub fn reload(&mut self, hours: u64) {
153        if let Some(rate) = self.reload_rate {
154            let earned = u64::from(rate).saturating_mul(hours);
155            let tokens = u64::from(self.tokens).saturating_add(earned);
156            self.tokens = tokens.min(u64::from(self.size)) as u16;
157        }
158    }
159
160    /// Sets the tokens, keeping no more than the bucket's size.
161    ///
162    /// # Arguments
163    ///
164    /// * `tokens` - the count.
165    pub fn set_tokens(&mut self, tokens: u16) {
166        if self.reload_rate.is_some() {
167            self.tokens = tokens.min(self.size);
168        }
169    }
170
171    /// Sets the tokens to one hour's reload, as a relay does when it starts scanning.
172    pub fn refill_one_hour(&mut self) {
173        if let Some(rate) = self.reload_rate {
174            self.set_tokens(u16::from(rate));
175        }
176    }
177}
178
179/// What `ConfigureFwdLimitReq` does to the token counters, TS011-1.0.1 table 63.
180#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
181pub enum CounterReset {
182    /// Empty every bucket.
183    Empty,
184    /// Set each to its reload rate.
185    ReloadRate,
186    /// Fill each.
187    Full,
188    /// Leave the counters, trimmed to any smaller size. A bucket that had no limit starts
189    /// full.
190    Keep,
191}
192
193impl CounterReset {
194    /// Reads a coded value, 0 through 3.
195    ///
196    /// # Arguments
197    ///
198    /// * `code` - the value; only its low two bits are read.
199    ///
200    /// # Returns
201    ///
202    /// The reset.
203    #[must_use]
204    pub const fn from_code(code: u8) -> CounterReset {
205        match code & 0x03 {
206            0 => CounterReset::Empty,
207            1 => CounterReset::ReloadRate,
208            2 => CounterReset::Full,
209            _ => CounterReset::Keep,
210        }
211    }
212
213    /// Returns the value a `ConfigureFwdLimitReq` carries.
214    ///
215    /// # Returns
216    ///
217    /// The code, 0 through 3.
218    #[must_use]
219    pub const fn code(self) -> u8 {
220        self as u8
221    }
222
223    fn apply(self, bucket: &mut TokenBucket, previous: u16) {
224        match self {
225            CounterReset::Empty => bucket.set_tokens(0),
226            CounterReset::ReloadRate => bucket.refill_one_hour(),
227            CounterReset::Full => bucket.set_tokens(bucket.size()),
228            CounterReset::Keep => bucket.set_tokens(previous),
229        }
230    }
231}
232
233/// A relay's four relay-wide limits, TS011-1.0.1 tables 23 and 24.
234///
235/// A join request spends a token of `join_request` and `overall`, an uplink from a trusted
236/// end device one of its own bucket, `global_uplink` and `overall`, and a notification of an
237/// unknown end device one of `notify` and `overall`.
238///
239/// # Examples
240///
241/// ```
242/// use pamoja_lorawan::relay::ForwardLimits;
243///
244/// let mut limits = ForwardLimits::new();
245/// assert_eq!(limits.join_request.reload_rate(), Some(4));
246/// assert_eq!(limits.join_request.size(), 8);
247///
248/// // A relay starts with an hour's worth of tokens, so a fifth join request waits.
249/// for _ in 0..4 {
250///     assert!(limits.join_request.take());
251/// }
252/// assert!(!limits.join_request.has_token());
253///
254/// // An hour on, four more arrive.
255/// limits.reload(1);
256/// assert_eq!(limits.join_request.tokens(), 4);
257/// ```
258#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
259pub struct ForwardLimits {
260    /// Join requests forwarded.
261    pub join_request: TokenBucket,
262    /// Notifications of end devices the relay does not trust.
263    pub notify: TokenBucket,
264    /// Uplinks forwarded for every trusted end device together.
265    pub global_uplink: TokenBucket,
266    /// Everything the relay sends on behalf of others.
267    pub overall: TokenBucket,
268}
269
270impl ForwardLimits {
271    /// The limits of TS011-1.0.1 table 23, each holding one hour's reload.
272    ///
273    /// # Returns
274    ///
275    /// Four join requests and four notifications an hour, each bucket twice that; and eight
276    /// uplinks and eight messages overall an hour, each bucket sixteen.
277    #[must_use]
278    pub const fn new() -> ForwardLimits {
279        ForwardLimits {
280            join_request: starting(4, 8),
281            notify: starting(4, 8),
282            global_uplink: starting(8, 16),
283            overall: starting(8, 16),
284        }
285    }
286
287    /// Takes a `ConfigureFwdLimitReq`, TS011-1.0.1 section 10.6.
288    ///
289    /// # Arguments
290    ///
291    /// * `reset` - what happens to the counters.
292    /// * `reload_rates` - the coded rates, join request, notify, global uplink and overall,
293    ///   127 meaning no limit.
294    /// * `bucket_sizes` - the coded size multipliers in the same order, table 55.
295    pub fn configure(&mut self, reset: CounterReset, reload_rates: [u8; 4], bucket_sizes: [u8; 4]) {
296        for (at, bucket) in [
297            &mut self.join_request,
298            &mut self.notify,
299            &mut self.global_uplink,
300            &mut self.overall,
301        ]
302        .into_iter()
303        .enumerate()
304        {
305            let previous = if bucket.reload_rate().is_none() {
306                u16::MAX
307            } else {
308                bucket.tokens()
309            };
310            *bucket = TokenBucket::coded(reload_rates[at], bucket_sizes[at], UNLIMITED_RELAY_RATE);
311            reset.apply(bucket, previous);
312        }
313    }
314
315    /// Adds what every bucket earns over some hours.
316    ///
317    /// # Arguments
318    ///
319    /// * `hours` - the whole hours since the last reload.
320    pub fn reload(&mut self, hours: u64) {
321        self.join_request.reload(hours);
322        self.notify.reload(hours);
323        self.global_uplink.reload(hours);
324        self.overall.reload(hours);
325    }
326
327    /// Sets every bucket to one hour's reload, as TS011-1.0.1 section 8.8 has a relay do
328    /// when it starts scanning.
329    pub fn restart(&mut self) {
330        self.join_request.refill_one_hour();
331        self.notify.refill_one_hour();
332        self.global_uplink.refill_one_hour();
333        self.overall.refill_one_hour();
334    }
335}
336
337impl Default for ForwardLimits {
338    fn default() -> ForwardLimits {
339        ForwardLimits::new()
340    }
341}
342
343const fn starting(reload_rate: u8, size: u16) -> TokenBucket {
344    TokenBucket {
345        reload_rate: Some(reload_rate),
346        size,
347        tokens: reload_rate as u16,
348    }
349}