Skip to main content

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}