dnp3/app/
types.rs

1use std::time::{Duration, SystemTime};
2
3use crate::app::measurement::DoubleBit;
4use crate::app::variations::Variation;
5use crate::app::QualifierCode;
6
7use chrono::{DateTime, SecondsFormat, TimeZone, Utc};
8use scursor::{WriteCursor, WriteError};
9
10/// Wrapper around a u64 count of milliseconds since Unix epoch UTC
11#[derive(Copy, Clone, Debug, PartialEq, Eq)]
12pub struct Timestamp {
13    value: u64,
14}
15
16impl Timestamp {
17    /// Maximum allowed DNP3 timestamp value (48-bits)
18    pub const MAX_VALUE: u64 = 0x0000_FFFF_FFFF_FFFF;
19    pub(crate) const OUT_OF_RANGE: &'static str = "<out of range>";
20
21    /// Create a timestamp from a count of milliseconds since epoch
22    pub const fn new(value: u64) -> Self {
23        Self {
24            value: value & Self::MAX_VALUE,
25        }
26    }
27
28    /// Minimum valid timestamp
29    pub const fn min() -> Self {
30        Self::zero()
31    }
32
33    /// Timestamp value of zero corresponding to the epoch
34    pub const fn zero() -> Self {
35        Self::new(0)
36    }
37
38    /// Maximum valid timestamp
39    pub fn max() -> Self {
40        Self::new(Self::MAX_VALUE)
41    }
42
43    /// Attempt to create a Timestamp from a SystemTime
44    pub fn try_from_system_time(system_time: SystemTime) -> Option<Timestamp> {
45        Some(Timestamp::new(
46            u64::try_from(
47                system_time
48                    .duration_since(SystemTime::UNIX_EPOCH)
49                    .ok()?
50                    .as_millis(),
51            )
52            .ok()?,
53        ))
54    }
55
56    /// Attempt to create a `DateTime<Utc>` from a Timestamp
57    pub fn to_datetime_utc(self) -> Option<DateTime<Utc>> {
58        Utc.timestamp_millis_opt(self.value as i64).single()
59    }
60
61    /// Retrieve the raw u64 value
62    pub fn raw_value(&self) -> u64 {
63        self.value
64    }
65
66    pub(crate) fn write(self, cursor: &mut WriteCursor) -> Result<(), WriteError> {
67        cursor.write_u48_le(self.value)
68    }
69
70    pub(crate) fn read(cursor: &mut scursor::ReadCursor) -> Result<Self, scursor::ReadError> {
71        Ok(Self {
72            value: cursor.read_u48_le()?,
73        })
74    }
75
76    pub(crate) fn checked_add(self, x: Duration) -> Option<Timestamp> {
77        // safe from overflow since self.value cannot possibly be larger than MAX
78        let max_add = Self::MAX_VALUE - self.value;
79        let millis = x.as_millis();
80        if millis > max_add as u128 {
81            return None;
82        }
83        Some(Timestamp::new(self.value + millis as u64))
84    }
85}
86
87impl std::fmt::Display for Timestamp {
88    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
89        match self.to_datetime_utc() {
90            Some(x) => write!(f, "{}", x.to_rfc3339_opts(SecondsFormat::Millis, true)),
91            None => f.write_str(Timestamp::OUT_OF_RANGE),
92        }
93    }
94}
95
96pub(crate) struct BitPair {
97    pub(crate) high: bool,
98    pub(crate) low: bool,
99}
100
101impl BitPair {
102    pub(crate) fn new(high: bool, low: bool) -> Self {
103        Self { high, low }
104    }
105}
106
107impl DoubleBit {
108    pub(crate) fn from(high: bool, low: bool) -> Self {
109        match (high, low) {
110            (false, false) => DoubleBit::Intermediate,
111            (false, true) => DoubleBit::DeterminedOff,
112            (true, false) => DoubleBit::DeterminedOn,
113            (true, true) => DoubleBit::Indeterminate,
114        }
115    }
116
117    pub(crate) fn to_bit_pair(self) -> BitPair {
118        match self {
119            DoubleBit::Intermediate => BitPair::new(false, false),
120            DoubleBit::DeterminedOff => BitPair::new(false, true),
121            DoubleBit::DeterminedOn => BitPair::new(true, false),
122            DoubleBit::Indeterminate => BitPair::new(true, true),
123        }
124    }
125
126    pub(crate) fn to_byte(self) -> u8 {
127        match self {
128            DoubleBit::Intermediate => 0b00,
129            DoubleBit::DeterminedOff => 0b01,
130            DoubleBit::DeterminedOn => 0b10,
131            DoubleBit::Indeterminate => 0b11,
132        }
133    }
134}
135
136impl std::fmt::Display for DoubleBit {
137    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
138        write!(f, "{self:?}")
139    }
140}
141
142impl std::fmt::Display for Variation {
143    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
144        let (g, v) = self.to_group_and_var();
145        write!(f, "g{g}v{v}")
146    }
147}
148
149impl QualifierCode {
150    pub(crate) fn description(self) -> &'static str {
151        match self {
152            QualifierCode::AllObjects => "all objects",
153            QualifierCode::Range8 => "1-byte start/stop",
154            QualifierCode::Range16 => "2-byte start/stop",
155            QualifierCode::Count8 => "1-byte count of objects",
156            QualifierCode::Count16 => "2-byte count of objects",
157            QualifierCode::CountAndPrefix8 => "1-byte count of objects",
158            QualifierCode::CountAndPrefix16 => "2-byte count of objects",
159            QualifierCode::FreeFormat16 => "2-byte free format",
160        }
161    }
162}
163
164#[cfg(test)]
165mod test {
166    use super::*;
167
168    #[test]
169    fn conversion_from_timestamp_to_datetime_utc_cannot_overflow() {
170        let timestamp = Timestamp::new(std::u64::MAX);
171        timestamp.to_datetime_utc();
172    }
173
174    #[test]
175    fn timestamp_display_formatting_works_as_expected() {
176        assert_eq!(format!("{}", Timestamp::min()), "1970-01-01T00:00:00.000Z");
177        assert_eq!(
178            format!("{}", Timestamp::max()),
179            "+10889-08-02T05:31:50.655Z"
180        );
181    }
182}