Skip to main content

bacnet_server/server/
clock.rs

1use std::sync::Mutex;
2use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
3
4use bacnet_objects::clock::{ClockFrame, ClockReader};
5use bacnet_objects::database::ObjectDatabase;
6use bacnet_transport::port::TransportPort;
7use bacnet_types::error::Error;
8use bacnet_types::primitives::{Date, Time};
9
10use super::{BACnetServer, ServerConfig};
11
12const HUNDREDTHS_PER_SECOND: i128 = 100;
13const HUNDREDTHS_PER_MINUTE: i128 = 60 * HUNDREDTHS_PER_SECOND;
14const HUNDREDTHS_PER_DAY: i128 = 24 * 60 * HUNDREDTHS_PER_MINUTE;
15
16/// Validated civil-time settings for the bundled server clock.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub struct ClockConfig {
19    utc_offset_minutes: i16,
20    daylight_savings_status: bool,
21}
22
23impl ClockConfig {
24    /// Configure signed minutes west of UTC and daylight-saving status.
25    pub fn new(utc_offset_minutes: i16, daylight_savings_status: bool) -> Result<Self, Error> {
26        if !(-1440..=1440).contains(&utc_offset_minutes) || utc_offset_minutes % 15 != 0 {
27            return Err(Error::Encoding(format!(
28                "UTC offset {utc_offset_minutes} must be within -1440..=1440 minutes in 15-minute increments"
29            )));
30        }
31        Ok(Self {
32            utc_offset_minutes,
33            daylight_savings_status,
34        })
35    }
36
37    /// Return the configured signed minutes west of UTC.
38    pub fn utc_offset_minutes(self) -> i16 {
39        self.utc_offset_minutes
40    }
41
42    /// Return whether the one-hour daylight-saving adjustment is active.
43    pub fn daylight_savings_status(self) -> bool {
44        self.daylight_savings_status
45    }
46}
47
48impl<T: TransportPort + 'static> BACnetServer<T> {
49    pub(super) async fn start_with_clock_mode(
50        config: ServerConfig,
51        db: ObjectDatabase,
52        transport: T,
53        clock_config: Option<ClockConfig>,
54    ) -> Result<Self, Error> {
55        Self::start_with_clock_mode_and_bindings(config, db, transport, clock_config, Vec::new())
56            .await
57    }
58
59    /// Start with the default system-UTC Device clock.
60    pub async fn start(
61        config: ServerConfig,
62        db: ObjectDatabase,
63        transport: T,
64    ) -> Result<Self, Error> {
65        Self::start_with_clock(config, db, transport, ClockConfig::default()).await
66    }
67
68    /// Start with a validated Device clock configuration.
69    pub async fn start_with_clock(
70        config: ServerConfig,
71        db: ObjectDatabase,
72        transport: T,
73        clock_config: ClockConfig,
74    ) -> Result<Self, Error> {
75        Self::start_with_clock_mode(config, db, transport, Some(clock_config)).await
76    }
77
78    /// Start without a Device wall clock or time-synchronization execution.
79    pub async fn start_clockless(
80        config: ServerConfig,
81        db: ObjectDatabase,
82        transport: T,
83    ) -> Result<Self, Error> {
84        Self::start_with_clock_mode(config, db, transport, None).await
85    }
86}
87
88#[derive(Clone, Copy)]
89struct ClockState {
90    synchronized_utc: Option<(i128, Instant)>,
91    config: ClockConfig,
92}
93
94/// Mutable server-owned controller behind the object layer's read-only port.
95pub(super) struct ServerClock {
96    state: Mutex<ClockState>,
97}
98
99impl ServerClock {
100    pub(super) fn new(config: ClockConfig) -> Self {
101        Self {
102            state: Mutex::new(ClockState {
103                synchronized_utc: None,
104                config,
105            }),
106        }
107    }
108
109    /// Apply a fully specified local or UTC synchronization value.
110    ///
111    /// Validation and conversion finish before the controller is mutated.
112    pub(super) fn synchronize(&self, date: Date, time: Time, is_utc: bool) -> Result<(), Error> {
113        let supplied_hundredths = date_time_to_hundredths(date, time)?;
114        let config = self
115            .state
116            .lock()
117            .map_err(|_| Error::Encoding("Device clock lock poisoned".into()))?
118            .config;
119
120        let utc_hundredths = if is_utc {
121            supplied_hundredths
122        } else {
123            supplied_hundredths + i128::from(config.utc_offset_minutes) * HUNDREDTHS_PER_MINUTE
124                - if config.daylight_savings_status {
125                    60 * HUNDREDTHS_PER_MINUTE
126                } else {
127                    0
128                }
129        };
130
131        frame_from_utc_hundredths(utc_hundredths, config).ok_or_else(|| {
132            Error::Encoding("synchronized time is outside the BACnet Date range".into())
133        })?;
134
135        let mut state = self
136            .state
137            .lock()
138            .map_err(|_| Error::Encoding("Device clock lock poisoned".into()))?;
139        state.synchronized_utc = Some((utc_hundredths, crate::runtime_clock::now()));
140        Ok(())
141    }
142}
143
144impl ClockReader for ServerClock {
145    fn read_clock(&self) -> Option<ClockFrame> {
146        let state = *self.state.lock().ok()?;
147        let utc_hundredths = match state.synchronized_utc {
148            Some((anchor, monotonic_anchor)) => {
149                let elapsed = crate::runtime_clock::now().duration_since(monotonic_anchor);
150                let elapsed_hundredths = i128::try_from(elapsed.as_millis() / 10).ok()?;
151                anchor + elapsed_hundredths
152            }
153            None => system_utc_hundredths(),
154        };
155        frame_from_utc_hundredths(utc_hundredths, state.config)
156    }
157}
158
159#[cfg(test)]
160pub(crate) fn clocked_test_database() -> ObjectDatabase {
161    let mut db = ObjectDatabase::new();
162    db.set_clock_reader(Some(std::sync::Arc::new(ServerClock::new(
163        ClockConfig::default(),
164    ))));
165    db
166}
167
168fn system_utc_hundredths() -> i128 {
169    let elapsed = SystemTime::now()
170        .duration_since(UNIX_EPOCH)
171        .unwrap_or(Duration::ZERO);
172    i128::from(elapsed.as_secs()) * HUNDREDTHS_PER_SECOND + i128::from(elapsed.subsec_millis() / 10)
173}
174
175pub(super) fn date_time_to_hundredths(date: Date, time: Time) -> Result<i128, Error> {
176    let year = date
177        .actual_year()
178        .ok_or_else(|| invalid_datetime("date contains an unspecified year"))?;
179    if !(1..=12).contains(&date.month) {
180        return Err(invalid_datetime("date month must be 1..=12"));
181    }
182    let max_day = days_in_month(year, date.month);
183    if date.day == 0 || date.day > max_day {
184        return Err(invalid_datetime("date day is invalid for its month"));
185    }
186    if !(1..=7).contains(&date.day_of_week) {
187        return Err(invalid_datetime("date day-of-week must be 1..=7"));
188    }
189
190    let days = days_from_civil(i64::from(year), i64::from(date.month), i64::from(date.day));
191    let expected_day_of_week = (days + 3).rem_euclid(7) as u8 + 1;
192    if date.day_of_week != expected_day_of_week {
193        return Err(invalid_datetime("date day-of-week is inconsistent"));
194    }
195    if time.hour > 23 || time.minute > 59 || time.second > 59 || time.hundredths > 99 {
196        return Err(invalid_datetime(
197            "time must be fully specified and within its field ranges",
198        ));
199    }
200
201    let day_hundredths = i128::from(time.hour) * 60 * HUNDREDTHS_PER_MINUTE
202        + i128::from(time.minute) * HUNDREDTHS_PER_MINUTE
203        + i128::from(time.second) * HUNDREDTHS_PER_SECOND
204        + i128::from(time.hundredths);
205    Ok(i128::from(days) * HUNDREDTHS_PER_DAY + day_hundredths)
206}
207
208fn invalid_datetime(message: &str) -> Error {
209    Error::Encoding(format!("invalid time synchronization value: {message}"))
210}
211
212fn frame_from_utc_hundredths(utc_hundredths: i128, config: ClockConfig) -> Option<ClockFrame> {
213    let dst_minutes = if config.daylight_savings_status {
214        60
215    } else {
216        0
217    };
218    let local_hundredths = utc_hundredths
219        - i128::from(config.utc_offset_minutes) * HUNDREDTHS_PER_MINUTE
220        + i128::from(dst_minutes) * HUNDREDTHS_PER_MINUTE;
221    let days = local_hundredths.div_euclid(HUNDREDTHS_PER_DAY);
222    let within_day = local_hundredths.rem_euclid(HUNDREDTHS_PER_DAY);
223    let days = i64::try_from(days).ok()?;
224    let (year, month, day) = civil_from_days(days);
225    let encoded_year = year.checked_sub(1900)?;
226    if !(0..=254).contains(&encoded_year) {
227        return None;
228    }
229
230    let total_seconds = within_day / HUNDREDTHS_PER_SECOND;
231    Some(ClockFrame {
232        local_date: Date {
233            year: encoded_year as u8,
234            month: month as u8,
235            day: day as u8,
236            day_of_week: (days + 3).rem_euclid(7) as u8 + 1,
237        },
238        local_time: Time {
239            hour: (total_seconds / 3600) as u8,
240            minute: ((total_seconds % 3600) / 60) as u8,
241            second: (total_seconds % 60) as u8,
242            hundredths: (within_day % HUNDREDTHS_PER_SECOND) as u8,
243        },
244        utc_offset: config.utc_offset_minutes,
245        daylight_savings_status: config.daylight_savings_status,
246    })
247}
248
249fn days_in_month(year: u16, month: u8) -> u8 {
250    match month {
251        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
252        4 | 6 | 9 | 11 => 30,
253        2 if is_leap_year(year) => 29,
254        2 => 28,
255        _ => 0,
256    }
257}
258
259fn is_leap_year(year: u16) -> bool {
260    year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400))
261}
262
263fn days_from_civil(year: i64, month: i64, day: i64) -> i64 {
264    let year = year - i64::from(month <= 2);
265    let era = year.div_euclid(400);
266    let year_of_era = year - era * 400;
267    let month_prime = month + if month > 2 { -3 } else { 9 };
268    let day_of_year = (153 * month_prime + 2) / 5 + day - 1;
269    let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
270    era * 146_097 + day_of_era - 719_468
271}
272
273fn civil_from_days(days: i64) -> (i64, i64, i64) {
274    let shifted = days + 719_468;
275    let era = shifted.div_euclid(146_097);
276    let day_of_era = shifted - era * 146_097;
277    let year_of_era =
278        (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
279    let mut year = year_of_era + era * 400;
280    let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
281    let month_prime = (5 * day_of_year + 2) / 153;
282    let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
283    let month = month_prime + if month_prime < 10 { 3 } else { -9 };
284    year += i64::from(month <= 2);
285    (year, month, day)
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    fn date(year: u16, month: u8, day: u8, day_of_week: u8) -> Date {
293        Date {
294            year: (year - 1900) as u8,
295            month,
296            day,
297            day_of_week,
298        }
299    }
300
301    fn time(hour: u8, minute: u8, second: u8, hundredths: u8) -> Time {
302        Time {
303            hour,
304            minute,
305            second,
306            hundredths,
307        }
308    }
309
310    #[test]
311    fn clock_config_rejects_invalid_offsets() {
312        assert!(ClockConfig::new(-1440, false).is_ok());
313        assert!(ClockConfig::new(1440, true).is_ok());
314        assert!(ClockConfig::new(14, false).is_err());
315        assert!(ClockConfig::new(1441, false).is_err());
316    }
317
318    #[test]
319    fn civil_conversion_covers_offsets_dst_and_rollover() {
320        let utc = date_time_to_hundredths(date(2024, 3, 1, 5), time(0, 30, 0, 25)).unwrap();
321
322        let west = frame_from_utc_hundredths(utc, ClockConfig::new(60, false).unwrap()).unwrap();
323        assert_eq!(west.local_date, date(2024, 2, 29, 4));
324        assert_eq!(west.local_time, time(23, 30, 0, 25));
325
326        let east = frame_from_utc_hundredths(utc, ClockConfig::new(-60, false).unwrap()).unwrap();
327        assert_eq!(east.local_date, date(2024, 3, 1, 5));
328        assert_eq!(east.local_time, time(1, 30, 0, 25));
329
330        let dst = frame_from_utc_hundredths(utc, ClockConfig::new(60, true).unwrap()).unwrap();
331        assert_eq!(dst.local_date, date(2024, 3, 1, 5));
332        assert_eq!(dst.local_time, time(0, 30, 0, 25));
333    }
334
335    #[test]
336    fn synchronization_validation_rejects_wildcards_calendar_errors_and_inconsistent_days() {
337        let clock = ServerClock::new(ClockConfig::default());
338        let anchor = clock.state.lock().unwrap().synchronized_utc;
339        for invalid_date in [
340            date(2023, 2, 29, 3),
341            date(2024, 2, 29, 5),
342            Date {
343                year: Date::UNSPECIFIED,
344                month: 1,
345                day: 1,
346                day_of_week: 1,
347            },
348        ] {
349            assert!(
350                clock
351                    .synchronize(invalid_date, time(12, 0, 0, 0), true)
352                    .is_err()
353            );
354        }
355        assert!(
356            clock
357                .synchronize(
358                    date(2024, 2, 29, 4),
359                    Time {
360                        hour: Time::UNSPECIFIED,
361                        minute: 0,
362                        second: 0,
363                        hundredths: 0,
364                    },
365                    true,
366                )
367                .is_err()
368        );
369        assert!(
370            clock
371                .synchronize(date(2024, 2, 29, 4), time(24, 0, 0, 0), true)
372                .is_err()
373        );
374        assert_eq!(clock.state.lock().unwrap().synchronized_utc, anchor);
375    }
376
377    #[tokio::test(start_paused = true)]
378    async fn local_and_utc_synchronization_share_the_configured_frame() {
379        let clock = ServerClock::new(ClockConfig::new(300, true).unwrap());
380        clock
381            .synchronize(date(2024, 7, 4, 4), time(9, 15, 0, 0), false)
382            .unwrap();
383        let local = clock.read_clock().unwrap();
384        assert_eq!(local.local_date, date(2024, 7, 4, 4));
385        assert_eq!((local.local_time.hour, local.local_time.minute), (9, 15));
386
387        clock
388            .synchronize(date(2024, 7, 4, 4), time(13, 15, 0, 0), true)
389            .unwrap();
390        let utc = clock.read_clock().unwrap();
391        assert_eq!(utc.local_date, date(2024, 7, 4, 4));
392        assert_eq!((utc.local_time.hour, utc.local_time.minute), (9, 15));
393        assert_eq!(utc.utc_offset, 300);
394        assert!(utc.daylight_savings_status);
395    }
396
397    #[tokio::test(start_paused = true)]
398    async fn device_schedule_recipient_and_cov_use_the_same_frame() {
399        use bacnet_objects::database::ObjectDatabase;
400        use bacnet_objects::device::{DeviceConfig, DeviceObject};
401        use bacnet_objects::traits::BACnetObject;
402        use bacnet_types::enums::PropertyIdentifier;
403        use bacnet_types::primitives::PropertyValue;
404        use std::sync::Arc;
405
406        let clock = Arc::new(ServerClock::new(ClockConfig::new(300, true).unwrap()));
407        clock
408            .synchronize(date(2024, 7, 4, 4), time(9, 15, 0, 0), false)
409            .unwrap();
410
411        let device = DeviceObject::new(DeviceConfig::default()).unwrap();
412        let device_oid = device.object_identifier();
413        let mut db = ObjectDatabase::new();
414        db.add(Box::new(device)).unwrap();
415        db.set_clock_reader(Some(
416            Arc::clone(&clock) as Arc<dyn bacnet_objects::clock::ClockReader>
417        ));
418
419        let frame = db.clock_frame().unwrap();
420        assert_eq!(
421            db.get(&device_oid)
422                .unwrap()
423                .read_property(PropertyIdentifier::LOCAL_TIME, None)
424                .unwrap(),
425            PropertyValue::Time(frame.local_time)
426        );
427        let (today, now) = crate::schedule::schedule_instant(frame).unwrap();
428        assert_eq!(today.to_date(), frame.local_date);
429        assert_eq!(today.weekday(), 4);
430        assert_eq!(now, frame.local_time);
431        assert_eq!(
432            frame.day_of_week(),
433            Some(bacnet_types::bitstring::DaysOfWeek::THURSDAY)
434        );
435        assert_eq!(
436            crate::server::cov_clock::cov_multiple_datetime(frame),
437            (frame.local_date, frame.local_time)
438        );
439    }
440
441    #[tokio::test(start_paused = true)]
442    async fn synchronized_clock_crosses_hundredths_at_second_and_minute_edges() {
443        for (anchor, after) in [
444            (time(0, 0, 0, 99), time(0, 0, 1, 0)),
445            (time(0, 0, 59, 99), time(0, 1, 0, 0)),
446        ] {
447            let clock = ServerClock::new(ClockConfig::default());
448            clock
449                .synchronize(date(2024, 7, 4, 4), anchor, true)
450                .unwrap();
451            assert_eq!(clock.read_clock().unwrap().local_time, anchor);
452            tokio::time::advance(Duration::from_millis(9)).await;
453            assert_eq!(clock.read_clock().unwrap().local_time, anchor);
454            tokio::time::advance(Duration::from_millis(1)).await;
455            let frame = clock.read_clock().unwrap();
456            assert_eq!(frame.local_date, date(2024, 7, 4, 4));
457            assert_eq!(frame.local_time, after);
458        }
459    }
460}
461
462#[cfg(test)]
463#[path = "clock_vectors.rs"]
464mod vectors;