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}