pamoja_lorawan/relay/timing.rs
1//! How long a wake-on-radio preamble must be and when it goes out, TS011-1.0.1 section 5.2
2//! and appendix 1.
3//!
4//! A relay wakes once a [`CadPeriodicity`] and needs to hear at least 6 preamble symbols and
5//! its [`CadToRx`] symbols after it wakes. An end device that does not know when that is
6//! sends a preamble spanning a whole period. Once a WOR ACK has told it when the relay
7//! scanned, it predicts the next scan and sends only as much preamble as the two crystals
8//! could have drifted since.
9
10use super::ack::{CadPeriodicity, CadToRx, StateSync, XtalAccuracy};
11
12/// The shortest WOR preamble, in symbols, TS011-1.0.1 appendix 1.
13pub const MIN_WOR_PREAMBLE_SYMBOLS: u16 = 8;
14
15/// The symbols a relay's radio needs to demodulate, besides its time to switch from
16/// detecting to receiving.
17const DEMODULATION_SYMBOLS: u64 = 6;
18
19/// The preamble length of an end device that does not know when the relay scans,
20/// TS011-1.0.1 section 5.2.
21///
22/// `floor(CADPeriodicity / TSYMB) + 1 + 6 + CadToRx`. An end device that has never heard
23/// from the relay assumes a second between scans and eight symbols to receive.
24///
25/// # Arguments
26///
27/// * `cad_periodicity` - how often the relay scans.
28/// * `symbol_us` - the symbol time of the WOR frame's data rate, in microseconds.
29/// * `cad_to_rx` - how long the relay takes to start receiving.
30///
31/// # Returns
32///
33/// The preamble length in symbols, capped at what a radio's sixteen-bit preamble register
34/// holds.
35///
36/// # Examples
37///
38/// ```
39/// use pamoja_lorawan::relay::{unsynchronized_preamble_symbols, CadPeriodicity, CadToRx};
40///
41/// // TS011-1.0.1 appendix 1: SF10 at 125 kHz, 8.192 ms a symbol, a relay scanning every
42/// // 500 ms and taking 4 symbols to receive.
43/// assert_eq!(
44/// unsynchronized_preamble_symbols(CadPeriodicity::Ms500, 8_192, CadToRx::Symbols4),
45/// 72
46/// );
47/// ```
48#[must_use]
49pub fn unsynchronized_preamble_symbols(
50 cad_periodicity: CadPeriodicity,
51 symbol_us: u64,
52 cad_to_rx: CadToRx,
53) -> u16 {
54 preamble(cad_periodicity.period_us(), symbol_us, cad_to_rx)
55}
56
57/// The milliseconds a relay reports from the start of its scan to the end of the WOR
58/// preamble, rounded up, TS011-1.0.1 appendix 1.
59///
60/// An end device takes `TREF = TLAST + PreambleLength * TSYMB - TOffset`, the end of its
61/// preamble less the offset, to be when the relay scanned, so the offset has to end where
62/// the preamble does. A relay finds that from when the frame finished arriving, less the
63/// time its sync word and payload take, which [`LinkSettings::airtime_us`] gives with no
64/// preamble symbols. Semtech LoRa Basics Modem's relay does the same. Appendix 1 writes the
65/// offset as `TEND - TSCAN - TOA(WOR) + 16.25 * TSYMB`, a fixed allowance that lands on
66/// the end of the preamble for no WOR frame's actual time on air, so a device would aim
67/// symbols early.
68///
69/// [`LinkSettings::airtime_us`]: pamoja_lora::LinkSettings::airtime_us
70///
71/// # Arguments
72///
73/// * `scan_start_us` - when the scan that detected the frame started.
74/// * `preamble_end_us` - when the frame's preamble ended.
75///
76/// # Returns
77///
78/// The offset in milliseconds, or `None` for a preamble that ended before the scan started
79/// or more than the eleven bits a WOR ACK carries after it.
80///
81/// # Examples
82///
83/// ```
84/// use pamoja_lora::LinkSettings;
85/// use pamoja_lorawan::relay::{t_offset_ms, WOR_UPLINK_LEN};
86///
87/// // TS011-1.0.1 appendix 1: an SF10 scan at 87 654 ms, and a preamble ending 891.584 ms
88/// // later.
89/// assert_eq!(t_offset_ms(87_654_000, 88_545_584), Some(892));
90///
91/// // A WOR frame that finished arriving at 88 734 ms at SF10 ended its preamble 264.192 ms
92/// // earlier: 4.25 symbols of sync word and 28 of payload.
93/// let sync_and_payload = LinkSettings::new(10, 125_000).with_preamble(0).airtime_us(WOR_UPLINK_LEN);
94/// assert_eq!(sync_and_payload, 264_192);
95/// assert_eq!(t_offset_ms(87_654_000, 88_734_000 - sync_and_payload), Some(816));
96/// ```
97#[must_use]
98pub fn t_offset_ms(scan_start_us: u64, preamble_end_us: u64) -> Option<u16> {
99 let micros = preamble_end_us.checked_sub(scan_start_us)?;
100 u16::try_from(micros.div_ceil(1000))
101 .ok()
102 .filter(|&offset| offset <= StateSync::MAX_T_OFFSET_MS)
103}
104
105/// When a synchronized end device's next WOR frame goes out, and with how long a preamble.
106#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
107pub struct WorSlot {
108 /// When to start sending, in microseconds.
109 pub start_us: u64,
110 /// The preamble length in symbols.
111 pub preamble_symbols: u16,
112}
113
114/// What an end device knows of a relay's scans once a WOR ACK has arrived, TS011-1.0.1
115/// section 3.9 and appendix 1.
116#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
117pub struct Synchronization {
118 /// When the relay scanned the channel the acknowledged frame went out on, `TREF`, in the
119 /// device's microseconds.
120 pub reference_us: u64,
121 /// How often the relay scans.
122 pub cad_periodicity: CadPeriodicity,
123 /// How accurate its crystal is.
124 pub relay_xtal: XtalAccuracy,
125 /// How long it takes to start receiving.
126 pub cad_to_rx: CadToRx,
127}
128
129impl Synchronization {
130 /// Works out when the relay scanned from the WOR ACK that answered a frame.
131 ///
132 /// `TREF = TLAST + PreambleLength * TSYMB - TOFFSET`.
133 ///
134 /// # Arguments
135 ///
136 /// * `wor_start_us` - when the acknowledged WOR frame started going out, `TLAST`.
137 /// * `preamble_symbols` - its preamble length.
138 /// * `symbol_us` - its symbol time.
139 /// * `state` - what the acknowledgment said.
140 ///
141 /// # Returns
142 ///
143 /// The synchronization, referenced to the channel that frame used.
144 #[must_use]
145 pub fn from_ack(
146 wor_start_us: u64,
147 preamble_symbols: u16,
148 symbol_us: u64,
149 state: &StateSync,
150 ) -> Synchronization {
151 let preamble_us = u64::from(preamble_symbols) * symbol_us;
152 Synchronization {
153 reference_us: (wor_start_us + preamble_us)
154 .saturating_sub(u64::from(state.t_offset_ms) * 1000),
155 cad_periodicity: state.cad_periodicity,
156 relay_xtal: state.xtal_accuracy,
157 cad_to_rx: state.cad_to_rx,
158 }
159 }
160
161 /// Picks the next scan to aim a WOR frame at, TS011-1.0.1 appendix 1.
162 ///
163 /// `TNEXT = N * CADPeriodicity + TREF` with `N = ceil((TNOW - TREF) / CADPeriodicity)`,
164 /// `DriftError = (RelayXtalAccuracy + EndDeviceXtalAccuracy) * (TNEXT - TREF) / 10^6`,
165 /// `TSTART = TNEXT - DriftError / 2`, moved one period on while that is already past, and
166 /// `PreambleLength = max(8, floor(DriftError / TSYMB) + 1 + 6 + CadToRx)`.
167 ///
168 /// # Arguments
169 ///
170 /// * `now_us` - the time, `TNOW`.
171 /// * `device_xtal_ppm` - the end device's own crystal accuracy.
172 /// * `symbol_us` - the symbol time of the WOR frame's data rate.
173 /// * `other_channel` - whether the frame goes out on the relay's other channel, which it
174 /// scans half a period apart.
175 ///
176 /// # Returns
177 ///
178 /// When to send and how long a preamble, or `None` once the drift exceeds a whole
179 /// period, when the device is no longer synchronized and falls back to
180 /// [`unsynchronized_preamble_symbols`].
181 ///
182 /// # Examples
183 ///
184 /// ```
185 /// use pamoja_lorawan::relay::{
186 /// CadPeriodicity, CadToRx, Forward, StateSync, Synchronization, XtalAccuracy,
187 /// };
188 ///
189 /// // TS011-1.0.1 appendix 1: a 133-symbol SF10 frame sent at 1 234 ms, and an
190 /// // acknowledgment reporting 892 ms from a 30 ppm relay scanning every 500 ms.
191 /// let state = StateSync {
192 /// cad_to_rx: CadToRx::Symbols4,
193 /// forward: Forward::Available,
194 /// relay_data_rate: 3,
195 /// xtal_accuracy: XtalAccuracy::Ppm30,
196 /// cad_periodicity: CadPeriodicity::Ms500,
197 /// t_offset_ms: 892,
198 /// };
199 /// let sync = Synchronization::from_ack(1_234_000, 133, 8_192, &state);
200 /// assert_eq!(sync.reference_us, 1_431_536);
201 ///
202 /// // A minute later, from a 20 ppm device, the drift is 3 ms.
203 /// let slot = sync.next_wor(61_000_000, 20, 8_192, false).expect("still synchronized");
204 /// assert_eq!(slot.start_us, 61_430_036);
205 /// assert_eq!(slot.preamble_symbols, 11);
206 ///
207 /// // Ten hours on, the drift passes a whole period.
208 /// assert_eq!(sync.next_wor(36_000_000_000, 20, 8_192, false), None);
209 /// ```
210 #[must_use]
211 pub fn next_wor(
212 &self,
213 now_us: u64,
214 device_xtal_ppm: u32,
215 symbol_us: u64,
216 other_channel: bool,
217 ) -> Option<WorSlot> {
218 let period = self.cad_periodicity.period_us();
219 let reference = if other_channel {
220 self.reference_us + period / 2
221 } else {
222 self.reference_us
223 };
224 let ppm = u64::from(self.relay_xtal.ppm()) + u64::from(device_xtal_ppm);
225 let elapsed = now_us.saturating_sub(reference);
226 let mut next = reference + elapsed.div_ceil(period) * period;
227 loop {
228 let drift = ppm.checked_mul(next - reference)? / 1_000_000;
229 if drift > period {
230 return None;
231 }
232 let start = next - drift / 2;
233 if start >= now_us {
234 return Some(WorSlot {
235 start_us: start,
236 preamble_symbols: preamble(drift, symbol_us, self.cad_to_rx)
237 .max(MIN_WOR_PREAMBLE_SYMBOLS),
238 });
239 }
240 next += period;
241 }
242 }
243}
244
245/// `floor(span / TSYMB) + 1 + 6 + CadToRx`, capped at sixteen bits.
246fn preamble(span_us: u64, symbol_us: u64, cad_to_rx: CadToRx) -> u16 {
247 let whole = span_us.checked_div(symbol_us).unwrap_or(u64::MAX);
248 let symbols = whole
249 .saturating_add(1 + DEMODULATION_SYMBOLS)
250 .saturating_add(u64::from(cad_to_rx.symbols()));
251 u16::try_from(symbols).unwrap_or(u16::MAX)
252}