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, Default)]
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<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 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 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 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
94pub(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 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;