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