Skip to main content

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}