Skip to main content

pamoja_lorawan/packages/
clock.rs

1//! Application layer clock synchronization, TS003-2.0.0.
2//!
3//! A device that has no GPS and no real-time clock still needs to know the time: a Class B
4//! beacon, a multicast window and a scheduled reboot all depend on it. This package is how it
5//! finds out. The device sends [`ClockCommand::AppTimeReq`] with the time it believes it is,
6//! the server compares that against when the uplink actually arrived, and answers with
7//! [`ClockCommand::AppTimeAns`] carrying the difference in seconds. Most of the time there is
8//! nothing to say and the server stays quiet.
9//!
10//! The exchange is guarded by a four-bit token: the device raises it every time it applies a
11//! correction, and an answer whose token does not match the request is ignored, so a late
12//! answer cannot pull a corrected clock back.
13//!
14//! [`ClockSync`] holds that state for a device: it builds each request, reads what comes
15//! back, and says what the device owes its server in return.
16//!
17//! # Examples
18//!
19//! A device asks for the time and applies the correction it gets back:
20//!
21//! ```
22//! use pamoja_lorawan::packages::clock::{ClockCommand, ClockSync};
23//! use pamoja_lorawan::Direction;
24//!
25//! let mut sync = ClockSync::new();
26//! let mut frame = [0u8; 8];
27//! let len = sync.app_time_req(1_000_000, false, &mut frame)?;
28//!
29//! // The server saw the uplink 12 seconds later than the device thinks it is.
30//! let answer = ClockCommand::AppTimeAns { time_correction: 12, token: 0 };
31//! let mut down = [0u8; 8];
32//! let len = answer.encode(&mut down)?;
33//!
34//! let read = sync.heard(&down[..len])?;
35//! assert_eq!(read.correction, Some(12));
36//! assert_eq!(sync.token(), 1, "the token moves on, so a repeat is ignored");
37//! # Ok::<(), pamoja_lorawan::LorawanError>(())
38//! ```
39
40use super::{payload, write, PackageVersion};
41use crate::{Direction, LorawanError};
42
43/// The port the clock synchronization package is spoken on, section 3.
44pub const PORT: u8 = 202;
45
46/// The identifier of this package, section 3.
47pub const PACKAGE: u8 = 1;
48
49/// The version of this package implemented here, section 3.
50pub const VERSION: u8 = 2;
51
52/// The longest clock synchronization command, in bytes.
53pub const MAX_COMMAND: usize = 6;
54
55const CID_PACKAGE_VERSION: u8 = 0x00;
56const CID_APP_TIME: u8 = 0x01;
57const CID_PERIODICITY: u8 = 0x02;
58const CID_FORCE_RESYNC: u8 = 0x03;
59
60/// One command of the clock synchronization package.
61///
62/// The identifier names a different command in each direction, so reading one takes the
63/// direction the frame traveled.
64#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
65pub enum ClockCommand {
66    /// The server asking which version of this package the device implements.
67    PackageVersionReq,
68    /// The device's answer, section 3.1.
69    PackageVersionAns(PackageVersion),
70    /// The device asking for a correction, section 3.2.
71    AppTimeReq {
72        /// What the device believes the time is: seconds since the GPS epoch, modulo 2^32,
73        /// read immediately before the frame goes out.
74        device_time: u32,
75        /// Whether the server must answer even when the clock is already right.
76        ans_required: bool,
77        /// The four-bit token the answer has to carry back.
78        token: u8,
79    },
80    /// The server's correction, section 3.2.
81    AppTimeAns {
82        /// The seconds to add to the device's clock, which may be negative.
83        time_correction: i32,
84        /// The token of the request being answered.
85        token: u8,
86    },
87    /// The server setting how often the device asks, section 3.3.
88    DeviceAppTimePeriodicityReq {
89        /// The coded period: 128 * 2^period seconds, plus or minus thirty.
90        period: u8,
91    },
92    /// The device's answer, which also reports its clock, section 3.3.
93    DeviceAppTimePeriodicityAns {
94        /// Whether the device refuses a period set by the server and manages its own.
95        not_supported: bool,
96        /// What the device believes the time is.
97        device_time: u32,
98    },
99    /// The server telling the device to synchronize now, section 3.4.
100    ForceDeviceResyncCmd {
101        /// How many requests to send, at most seven; zero means the command is discarded.
102        transmissions: u8,
103    },
104}
105
106impl ClockCommand {
107    /// The identifier this command travels under.
108    ///
109    /// # Returns
110    ///
111    /// The identifier, 0x00 to 0x03.
112    #[must_use]
113    pub const fn cid(&self) -> u8 {
114        match self {
115            ClockCommand::PackageVersionReq | ClockCommand::PackageVersionAns(_) => {
116                CID_PACKAGE_VERSION
117            }
118            ClockCommand::AppTimeReq { .. } | ClockCommand::AppTimeAns { .. } => CID_APP_TIME,
119            ClockCommand::DeviceAppTimePeriodicityReq { .. }
120            | ClockCommand::DeviceAppTimePeriodicityAns { .. } => CID_PERIODICITY,
121            ClockCommand::ForceDeviceResyncCmd { .. } => CID_FORCE_RESYNC,
122        }
123    }
124
125    /// Which way this command travels.
126    ///
127    /// # Returns
128    ///
129    /// [`Direction::Uplink`] for what a device sends, [`Direction::Downlink`] for what a
130    /// server sends.
131    #[must_use]
132    pub const fn direction(&self) -> Direction {
133        match self {
134            ClockCommand::PackageVersionAns(_)
135            | ClockCommand::AppTimeReq { .. }
136            | ClockCommand::DeviceAppTimePeriodicityAns { .. } => Direction::Uplink,
137            _ => Direction::Downlink,
138        }
139    }
140
141    /// Reads one command from the front of a message.
142    ///
143    /// # Arguments
144    ///
145    /// * `direction` - which way the frame carrying it traveled.
146    /// * `bytes` - the message, from this command's identifier on.
147    ///
148    /// # Returns
149    ///
150    /// The command and how many bytes it took.
151    ///
152    /// # Errors
153    ///
154    /// [`LorawanError::MalformedFrame`] when the message ends inside the command, and
155    /// [`LorawanError::UnknownCommand`] for an identifier this package does not define.
156    ///
157    /// # Examples
158    ///
159    /// ```
160    /// use pamoja_lorawan::packages::clock::ClockCommand;
161    /// use pamoja_lorawan::Direction;
162    ///
163    /// // The server tells a device whose clock has drifted to resynchronize, twice over.
164    /// let mut message = [0u8; 4];
165    /// let resync = ClockCommand::ForceDeviceResyncCmd { transmissions: 2 };
166    /// let written = resync.encode(&mut message)?;
167    ///
168    /// let (command, taken) = ClockCommand::parse(Direction::Downlink, &message[..written])?;
169    /// assert_eq!((command, taken), (resync, written));
170    /// # Ok::<(), pamoja_lorawan::LorawanError>(())
171    /// ```
172    pub fn parse(
173        direction: Direction,
174        bytes: &[u8],
175    ) -> Result<(ClockCommand, usize), LorawanError> {
176        let cid = *bytes.first().ok_or(LorawanError::MalformedFrame)?;
177        let up = matches!(direction, Direction::Uplink);
178        match (cid, up) {
179            (CID_PACKAGE_VERSION, false) => Ok((ClockCommand::PackageVersionReq, 1)),
180            (CID_PACKAGE_VERSION, true) => {
181                let (fields, taken) = payload(bytes, 2)?;
182                Ok((
183                    ClockCommand::PackageVersionAns(PackageVersion {
184                        package: fields[0],
185                        version: fields[1],
186                    }),
187                    taken,
188                ))
189            }
190            (CID_APP_TIME, true) => {
191                let (fields, taken) = payload(bytes, 5)?;
192                Ok((
193                    ClockCommand::AppTimeReq {
194                        device_time: u32::from_le_bytes([
195                            fields[0], fields[1], fields[2], fields[3],
196                        ]),
197                        ans_required: fields[4] & 0x10 != 0,
198                        token: fields[4] & 0x0F,
199                    },
200                    taken,
201                ))
202            }
203            (CID_APP_TIME, false) => {
204                let (fields, taken) = payload(bytes, 5)?;
205                Ok((
206                    ClockCommand::AppTimeAns {
207                        time_correction: i32::from_le_bytes([
208                            fields[0], fields[1], fields[2], fields[3],
209                        ]),
210                        token: fields[4] & 0x0F,
211                    },
212                    taken,
213                ))
214            }
215            (CID_PERIODICITY, false) => {
216                let (fields, taken) = payload(bytes, 1)?;
217                Ok((
218                    ClockCommand::DeviceAppTimePeriodicityReq {
219                        period: fields[0] & 0x0F,
220                    },
221                    taken,
222                ))
223            }
224            (CID_PERIODICITY, true) => {
225                let (fields, taken) = payload(bytes, 5)?;
226                Ok((
227                    ClockCommand::DeviceAppTimePeriodicityAns {
228                        not_supported: fields[0] & 0x01 != 0,
229                        device_time: u32::from_le_bytes([
230                            fields[1], fields[2], fields[3], fields[4],
231                        ]),
232                    },
233                    taken,
234                ))
235            }
236            (CID_FORCE_RESYNC, false) => {
237                let (fields, taken) = payload(bytes, 1)?;
238                Ok((
239                    ClockCommand::ForceDeviceResyncCmd {
240                        transmissions: fields[0] & 0x07,
241                    },
242                    taken,
243                ))
244            }
245            _ => Err(LorawanError::UnknownCommand(cid)),
246        }
247    }
248
249    /// Writes this command out.
250    ///
251    /// # Arguments
252    ///
253    /// * `out` - where to write it, at least [`MAX_COMMAND`] bytes for any command.
254    ///
255    /// # Returns
256    ///
257    /// How many bytes were written.
258    ///
259    /// # Errors
260    ///
261    /// [`LorawanError::PayloadTooLong`] when the buffer is too small.
262    ///
263    /// # Examples
264    ///
265    /// ```
266    /// use pamoja_lorawan::packages::clock::ClockCommand;
267    /// use pamoja_lorawan::Direction;
268    ///
269    /// // A device reports the time it keeps and asks for the correction.
270    /// let mut out = [0u8; 8];
271    /// let request = ClockCommand::AppTimeReq { device_time: 1, ans_required: true, token: 3 };
272    /// let written = request.encode(&mut out)?;
273    /// assert_eq!(written, 6, "an identifier, four bytes of time, and one of flags");
274    ///
275    /// // The server reads the same request back.
276    /// let (heard, _) = ClockCommand::parse(Direction::Uplink, &out[..written])?;
277    /// assert_eq!(heard, request);
278    /// # Ok::<(), pamoja_lorawan::LorawanError>(())
279    /// ```
280    pub fn encode(&self, out: &mut [u8]) -> Result<usize, LorawanError> {
281        match self {
282            ClockCommand::PackageVersionReq => write(out, CID_PACKAGE_VERSION, &[]),
283            ClockCommand::PackageVersionAns(version) => write(
284                out,
285                CID_PACKAGE_VERSION,
286                &[version.package, version.version],
287            ),
288            ClockCommand::AppTimeReq {
289                device_time,
290                ans_required,
291                token,
292            } => {
293                let time = device_time.to_le_bytes();
294                let param = (u8::from(*ans_required) << 4) | (token & 0x0F);
295                write(
296                    out,
297                    CID_APP_TIME,
298                    &[time[0], time[1], time[2], time[3], param],
299                )
300            }
301            ClockCommand::AppTimeAns {
302                time_correction,
303                token,
304            } => {
305                let delta = time_correction.to_le_bytes();
306                write(
307                    out,
308                    CID_APP_TIME,
309                    &[delta[0], delta[1], delta[2], delta[3], token & 0x0F],
310                )
311            }
312            ClockCommand::DeviceAppTimePeriodicityReq { period } => {
313                write(out, CID_PERIODICITY, &[period & 0x0F])
314            }
315            ClockCommand::DeviceAppTimePeriodicityAns {
316                not_supported,
317                device_time,
318            } => {
319                let time = device_time.to_le_bytes();
320                write(
321                    out,
322                    CID_PERIODICITY,
323                    &[u8::from(*not_supported), time[0], time[1], time[2], time[3]],
324                )
325            }
326            ClockCommand::ForceDeviceResyncCmd { transmissions } => {
327                write(out, CID_FORCE_RESYNC, &[transmissions & 0x07])
328            }
329        }
330    }
331}
332
333/// Walks the commands packed into one clock synchronization message.
334///
335/// Iteration stops at an identifier this package does not define, because a command carries
336/// no length of its own and cannot be stepped over.
337pub struct ClockCommands<'a> {
338    direction: Direction,
339    bytes: &'a [u8],
340    at: usize,
341    stopped: bool,
342}
343
344impl<'a> ClockCommands<'a> {
345    /// Reads the commands in a message.
346    ///
347    /// # Arguments
348    ///
349    /// * `direction` - which way the frame carrying them traveled.
350    /// * `bytes` - the payload sent on port [`PORT`].
351    ///
352    /// # Returns
353    ///
354    /// The walk.
355    ///
356    /// # Examples
357    ///
358    /// ```
359    /// use pamoja_lorawan::packages::clock::{ClockCommand, ClockCommands};
360    /// use pamoja_lorawan::Direction;
361    ///
362    /// // One downlink asking which version of the package a device runs, and telling it to
363    /// // resynchronize once.
364    /// let sent = [
365    ///     ClockCommand::PackageVersionReq,
366    ///     ClockCommand::ForceDeviceResyncCmd { transmissions: 1 },
367    /// ];
368    /// let mut message = [0u8; 8];
369    /// let mut len = 0;
370    /// for command in &sent {
371    ///     len += command.encode(&mut message[len..]).unwrap();
372    /// }
373    ///
374    /// let read: Vec<ClockCommand> = ClockCommands::new(Direction::Downlink, &message[..len])
375    ///     .map(|command| command.unwrap())
376    ///     .collect();
377    /// assert_eq!(read, sent);
378    /// ```
379    #[must_use]
380    pub const fn new(direction: Direction, bytes: &'a [u8]) -> ClockCommands<'a> {
381        ClockCommands {
382            direction,
383            bytes,
384            at: 0,
385            stopped: false,
386        }
387    }
388
389    /// What was not read.
390    ///
391    /// # Returns
392    ///
393    /// The bytes left: empty once every command has been read, and whatever followed an
394    /// identifier this package does not define.
395    #[must_use]
396    pub fn remaining(&self) -> &'a [u8] {
397        &self.bytes[self.at.min(self.bytes.len())..]
398    }
399}
400
401impl Iterator for ClockCommands<'_> {
402    type Item = Result<ClockCommand, LorawanError>;
403
404    fn next(&mut self) -> Option<Self::Item> {
405        if self.stopped || self.at >= self.bytes.len() {
406            return None;
407        }
408        match ClockCommand::parse(self.direction, &self.bytes[self.at..]) {
409            Ok((command, taken)) => {
410                self.at += taken;
411                Some(Ok(command))
412            }
413            Err(LorawanError::UnknownCommand(_)) => {
414                self.stopped = true;
415                None
416            }
417            Err(error) => {
418                self.stopped = true;
419                Some(Err(error))
420            }
421        }
422    }
423}
424
425/// What a downlink on the clock port asked of the device.
426#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
427pub struct Heard {
428    /// The seconds to add to the device's clock, where an answer carried a correction that
429    /// matched the outstanding request.
430    pub correction: Option<i32>,
431    /// Whether the correction was the largest the field carries, so the clock is not yet
432    /// right and another request should follow, section 3.2.
433    pub more_correction: bool,
434    /// How many requests the server asked for at once, from a resynchronization command.
435    pub resync: Option<u8>,
436    /// Whether the device now owes an answer, which [`ClockSync::answer`] writes.
437    pub answer_due: bool,
438}
439
440/// The clock synchronization package running on a device, TS003-2.0.0.
441///
442/// It keeps the token that pairs an answer with its request, the period the server set, and
443/// whatever answer the device owes. It holds no clock: the caller passes the device's own
444/// time into each request and applies the correction that comes back.
445#[derive(Clone, Copy, Debug)]
446pub struct ClockSync {
447    token: u8,
448    period: u8,
449    self_managed: bool,
450    owed: Option<ClockCommand>,
451    waiting: bool,
452}
453
454impl Default for ClockSync {
455    fn default() -> ClockSync {
456        ClockSync::new()
457    }
458}
459
460impl ClockSync {
461    /// The period a device asks at until its server says otherwise, section 3.3.
462    pub const DEFAULT_PERIOD: u8 = 0;
463
464    /// Starts the package on a device, with the token at zero.
465    ///
466    /// # Returns
467    ///
468    /// The package, with nothing outstanding.
469    #[must_use]
470    pub const fn new() -> ClockSync {
471        ClockSync {
472            token: 0,
473            period: ClockSync::DEFAULT_PERIOD,
474            self_managed: false,
475            owed: None,
476            waiting: false,
477        }
478    }
479
480    /// Starts the package on a device that manages its own periodicity.
481    ///
482    /// Such a device answers a server that tries to set a period with the not-supported bit,
483    /// section 3.3.
484    ///
485    /// # Returns
486    ///
487    /// The package.
488    #[must_use]
489    pub const fn self_managed() -> ClockSync {
490        ClockSync {
491            self_managed: true,
492            ..ClockSync::new()
493        }
494    }
495
496    /// The token the next request will carry.
497    ///
498    /// # Returns
499    ///
500    /// A value 0 to 15, which rises every time a correction is applied.
501    #[must_use]
502    pub const fn token(&self) -> u8 {
503        self.token
504    }
505
506    /// How often the device should ask, as the server last set it.
507    ///
508    /// # Returns
509    ///
510    /// The seconds between requests, before the random spread of thirty seconds either way
511    /// that section 3.3 asks for.
512    #[must_use]
513    pub const fn period_s(&self) -> u32 {
514        128u32 << self.period
515    }
516
517    /// Builds the request that asks for a correction, section 3.2.
518    ///
519    /// # Arguments
520    ///
521    /// * `device_time` - what the device believes the time is, in seconds since the GPS
522    ///   epoch, read immediately before the frame goes out.
523    /// * `ans_required` - whether the server must answer even if the clock is right.
524    /// * `out` - where to write the command.
525    ///
526    /// # Returns
527    ///
528    /// How many bytes were written.
529    ///
530    /// # Errors
531    ///
532    /// [`LorawanError::PayloadTooLong`] when the buffer is too small.
533    pub fn app_time_req(
534        &mut self,
535        device_time: u32,
536        ans_required: bool,
537        out: &mut [u8],
538    ) -> Result<usize, LorawanError> {
539        self.waiting = true;
540        ClockCommand::AppTimeReq {
541            device_time,
542            ans_required,
543            token: self.token,
544        }
545        .encode(out)
546    }
547
548    /// Reads a downlink on the clock port and acts on it.
549    ///
550    /// A correction whose token does not match the outstanding request is ignored, as section
551    /// 3.2 asks, and so is any answer with no request waiting on it.
552    ///
553    /// # Arguments
554    ///
555    /// * `payload` - what arrived on port [`PORT`].
556    ///
557    /// # Returns
558    ///
559    /// The correction to apply, whether more of one is coming, and what the device now owes.
560    ///
561    /// # Errors
562    ///
563    /// The first error a command in the message raised.
564    pub fn heard(&mut self, payload: &[u8]) -> Result<Heard, LorawanError> {
565        let mut heard = Heard::default();
566        for command in ClockCommands::new(Direction::Downlink, payload) {
567            match command? {
568                ClockCommand::PackageVersionReq => {
569                    self.owed = Some(ClockCommand::PackageVersionAns(PackageVersion {
570                        package: PACKAGE,
571                        version: VERSION,
572                    }));
573                    heard.answer_due = true;
574                }
575                ClockCommand::AppTimeAns {
576                    time_correction,
577                    token,
578                } => {
579                    if self.waiting && token == self.token {
580                        self.token = (self.token + 1) % 16;
581                        self.waiting = false;
582                        heard.correction = Some(time_correction);
583                        heard.more_correction =
584                            time_correction == i32::MAX || time_correction == i32::MIN;
585                    }
586                }
587                ClockCommand::DeviceAppTimePeriodicityReq { period } => {
588                    if !self.self_managed {
589                        self.period = period;
590                    }
591                    self.owed = Some(ClockCommand::DeviceAppTimePeriodicityAns {
592                        not_supported: self.self_managed,
593                        device_time: 0,
594                    });
595                    heard.answer_due = true;
596                }
597                ClockCommand::ForceDeviceResyncCmd { transmissions } if transmissions > 0 => {
598                    heard.resync = Some(transmissions);
599                }
600                _ => {}
601            }
602        }
603        Ok(heard)
604    }
605
606    /// Writes the answer the device owes, if it owes one.
607    ///
608    /// # Arguments
609    ///
610    /// * `device_time` - what the device believes the time is, for the answer that reports
611    ///   it.
612    /// * `out` - where to write the command.
613    ///
614    /// # Returns
615    ///
616    /// How many bytes were written, and zero when nothing is owed.
617    ///
618    /// # Errors
619    ///
620    /// [`LorawanError::PayloadTooLong`] when the buffer is too small, leaving the answer
621    /// owed.
622    pub fn answer(&mut self, device_time: u32, out: &mut [u8]) -> Result<usize, LorawanError> {
623        let Some(owed) = self.owed else {
624            return Ok(0);
625        };
626        let owed = match owed {
627            ClockCommand::DeviceAppTimePeriodicityAns { not_supported, .. } => {
628                ClockCommand::DeviceAppTimePeriodicityAns {
629                    not_supported,
630                    device_time,
631                }
632            }
633            other => other,
634        };
635        let written = owed.encode(out)?;
636        self.owed = None;
637        Ok(written)
638    }
639
640    /// Whether the device owes its server an answer.
641    ///
642    /// # Returns
643    ///
644    /// `true` when [`ClockSync::answer`] would write one.
645    #[must_use]
646    pub const fn answer_due(&self) -> bool {
647        self.owed.is_some()
648    }
649}