Skip to main content

pamoja_radios/
duty.rs

1//! A duty-cycle guard for a radio.
2//!
3//! A regional duty-cycle limit caps the share of time a node may transmit, and
4//! [`LinkSettings::min_off_time_us`] turns that limit into the silence a transmission
5//! of a given length owes. [`DutyCycle`] keeps the account against a clock the caller
6//! supplies, so the same guard works over a microcontroller's hardware timer and a
7//! host's monotonic clock.
8
9use pamoja_lora::LinkSettings;
10
11/// The silence a radio owes after its transmissions under a duty-cycle limit.
12///
13/// Record each transmission with [`transmitted`](DutyCycle::transmitted), then ask
14/// [`ready`](DutyCycle::ready) or [`wait_us`](DutyCycle::wait_us) before the next. A
15/// limit of zero forbids transmitting, and the guard never becomes ready.
16///
17/// # Examples
18///
19/// ```
20/// use pamoja_lora::LinkSettings;
21/// use pamoja_radios::duty::DutyCycle;
22///
23/// // SF12 at 125 kHz under a 1% limit.
24/// let link = LinkSettings::new(12, 125_000);
25/// let mut guard = DutyCycle::new(10);
26/// assert!(guard.ready(0));
27///
28/// // A ten-byte reading sent at time zero holds the channel for 991,232 us, and owes
29/// // ninety-nine times that in silence.
30/// assert_eq!(guard.transmitted(0, &link, 10), 991_232);
31/// assert_eq!(guard.wait_us(0), 99_123_200);
32/// assert!(!guard.ready(99_000_000));
33/// assert!(guard.ready(99_123_200));
34/// ```
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub struct DutyCycle {
37    permille: u32,
38    earliest_us: u64,
39}
40
41impl DutyCycle {
42    /// Creates a guard for a duty-cycle limit, ready to transmit at once.
43    ///
44    /// # Arguments
45    ///
46    /// * `permille` - the limit in parts per thousand, so `10` is 1%; `0` forbids
47    ///   transmitting and `1000` or more imposes no silence.
48    ///
49    /// # Returns
50    ///
51    /// The guard.
52    pub const fn new(permille: u32) -> Self {
53        Self {
54            permille,
55            earliest_us: if permille == 0 { u64::MAX } else { 0 },
56        }
57    }
58
59    /// Returns the limit the guard enforces.
60    ///
61    /// # Returns
62    ///
63    /// The limit in parts per thousand.
64    pub fn permille(&self) -> u32 {
65        self.permille
66    }
67
68    /// Returns the earliest time the next transmission may start.
69    ///
70    /// # Returns
71    ///
72    /// A time in microseconds on the caller's clock, or [`u64::MAX`] when the limit
73    /// forbids transmitting.
74    pub fn earliest_us(&self) -> u64 {
75        self.earliest_us
76    }
77
78    /// Returns how long the radio must still stay silent.
79    ///
80    /// # Arguments
81    ///
82    /// * `now_us` - the current time in microseconds on the caller's clock.
83    ///
84    /// # Returns
85    ///
86    /// The remaining silence in microseconds, zero when a transmission may start.
87    pub fn wait_us(&self, now_us: u64) -> u64 {
88        self.earliest_us.saturating_sub(now_us)
89    }
90
91    /// Reports whether a transmission may start now.
92    ///
93    /// # Arguments
94    ///
95    /// * `now_us` - the current time in microseconds on the caller's clock.
96    ///
97    /// # Returns
98    ///
99    /// `true` once the silence the last transmission owed has passed.
100    pub fn ready(&self, now_us: u64) -> bool {
101        self.permille != 0 && now_us >= self.earliest_us
102    }
103
104    /// Records a transmission and the silence it owes.
105    ///
106    /// # Arguments
107    ///
108    /// * `started_us` - when the transmission started, in microseconds on the caller's
109    ///   clock.
110    /// * `link` - the settings the frame was sent with.
111    /// * `payload_len` - the payload length in bytes.
112    ///
113    /// # Returns
114    ///
115    /// The frame's time on air in microseconds.
116    pub fn transmitted(&mut self, started_us: u64, link: &LinkSettings, payload_len: usize) -> u64 {
117        let airtime = link.airtime_us(payload_len);
118        if self.permille == 0 {
119            return airtime;
120        }
121        let off_time = link.min_off_time_us(payload_len, self.permille.min(1000));
122        self.earliest_us = started_us.saturating_add(airtime).saturating_add(off_time);
123        airtime
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn a_new_guard_is_ready_at_once() {
133        assert!(DutyCycle::new(10).ready(0));
134        assert_eq!(DutyCycle::new(10).wait_us(123), 0);
135    }
136
137    #[test]
138    fn a_transmission_owes_its_off_time_after_its_airtime() {
139        let link = LinkSettings::new(7, 125_000);
140        let mut guard = DutyCycle::new(10);
141        let airtime = guard.transmitted(5_000, &link, 20);
142        assert_eq!(guard.earliest_us(), 5_000 + airtime + airtime * 99);
143        assert!(!guard.ready(5_000 + airtime * 100 - 1));
144        assert!(guard.ready(5_000 + airtime * 100));
145    }
146
147    #[test]
148    fn a_zero_limit_never_becomes_ready() {
149        let link = LinkSettings::new(7, 125_000);
150        let mut guard = DutyCycle::new(0);
151        assert!(!guard.ready(0));
152        guard.transmitted(0, &link, 10);
153        assert!(!guard.ready(u64::MAX));
154        assert_eq!(guard.wait_us(0), u64::MAX);
155    }
156
157    #[test]
158    fn a_limit_of_a_thousand_or_more_owes_no_silence() {
159        let link = LinkSettings::new(7, 125_000);
160        for permille in [1_000, 1_500] {
161            let mut guard = DutyCycle::new(permille);
162            let airtime = guard.transmitted(0, &link, 10);
163            assert!(guard.ready(airtime));
164        }
165    }
166}