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}