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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct ClockConfig {
19 utc_offset_minutes: i16,
20 daylight_savings_status: bool,
21}
22
23impl ClockConfig {
24 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 pub fn utc_offset_minutes(self) -> i16 {
39 self.utc_offset_minutes
40 }
41
42 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 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 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 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
103pub(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 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}