canopen_rs/time.rs
1//! Time stamp object (TIME) — the network time-of-day (CiA 301 §7.2.6).
2//!
3//! A single node (the TIME producer) broadcasts the absolute time on the
4//! default COB-ID `0x100` ([`TIME_COB_ID`]). The payload is a six-byte
5//! `TIME_OF_DAY`: milliseconds since midnight and days since 1 January 1984.
6//!
7//! ```
8//! use canopen_rs::time::TimeOfDay;
9//!
10//! let t = TimeOfDay::new(0x0123_4567, 0x1234);
11//! let frame = t.encode();
12//! assert_eq!(frame, [0x67, 0x45, 0x23, 0x01, 0x34, 0x12]);
13//! assert_eq!(TimeOfDay::decode(&frame).unwrap(), t);
14//! ```
15
16use crate::{Error, Result};
17
18/// The default TIME COB-ID (`0x100`), configurable via object `0x1012`.
19pub const TIME_COB_ID: u16 = 0x100;
20
21/// The number of milliseconds in a day — the exclusive upper bound of a valid
22/// [`TimeOfDay::milliseconds`].
23pub const MS_PER_DAY: u32 = 86_400_000;
24
25/// A CANopen `TIME_OF_DAY`: milliseconds after midnight and days since the
26/// CANopen epoch (1 January 1984).
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct TimeOfDay {
29 /// Milliseconds after midnight. The wire field is 28 bits, so only the low
30 /// 28 bits are transmitted (values up to [`MS_PER_DAY`]).
31 pub milliseconds: u32,
32 /// Days since 1 January 1984.
33 pub days: u16,
34}
35
36impl TimeOfDay {
37 /// Construct a time of day.
38 pub const fn new(milliseconds: u32, days: u16) -> Self {
39 Self { milliseconds, days }
40 }
41
42 /// Encode into the six-byte TIME data field: the 28-bit millisecond count
43 /// little-endian in bytes 0..4 (top four bits reserved, zero) and the day
44 /// count little-endian in bytes 4..6.
45 pub fn encode(&self) -> [u8; 6] {
46 let ms = self.milliseconds & 0x0FFF_FFFF;
47 let mut frame = [0u8; 6];
48 frame[0..4].copy_from_slice(&ms.to_le_bytes());
49 frame[4..6].copy_from_slice(&self.days.to_le_bytes());
50 frame
51 }
52
53 /// Decode a TIME data field. Requires at least six bytes; the reserved top
54 /// four bits of the millisecond field are masked off.
55 pub fn decode(data: &[u8]) -> Result<Self> {
56 if data.len() < 6 {
57 return Err(Error::BadLength);
58 }
59 Ok(Self {
60 milliseconds: u32::from_le_bytes([data[0], data[1], data[2], data[3]]) & 0x0FFF_FFFF,
61 days: u16::from_le_bytes([data[4], data[5]]),
62 })
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 #[test]
71 fn cob_id_is_default() {
72 assert_eq!(TIME_COB_ID, 0x100);
73 }
74
75 #[test]
76 fn encode_decode_roundtrips() {
77 let t = TimeOfDay::new(45_000_000, 15_600); // ~12:30, a plausible day count
78 assert_eq!(TimeOfDay::decode(&t.encode()).unwrap(), t);
79 }
80
81 #[test]
82 fn masks_reserved_millisecond_bits() {
83 // Bits above 28 are reserved and must not survive an encode/decode.
84 let t = TimeOfDay::new(0xF000_0001, 1);
85 assert_eq!(t.encode()[..4], [0x01, 0x00, 0x00, 0x00]);
86 assert_eq!(TimeOfDay::decode(&t.encode()).unwrap().milliseconds, 1);
87 }
88
89 #[test]
90 fn decode_rejects_short_frame() {
91 assert_eq!(TimeOfDay::decode(&[0; 5]), Err(Error::BadLength));
92 }
93}