1use std::collections::{HashMap, HashSet};
4use std::time::Duration;
5
6use bacnet_encoding::constructed::encode_log_multiple_record;
7use bacnet_encoding::primitives::encode_property_value;
8use bacnet_types::constructed::{
9 BACnetDeviceObjectPropertyReference, BACnetLogMultipleRecord, BACnetLogRecord, LogData,
10 LogDatum, LogValue,
11};
12use bacnet_types::enums::{
13 ErrorClass, ErrorCode, LoggingType, ObjectType, PropertyIdentifier as P,
14};
15use bacnet_types::error::Error;
16use bacnet_types::primitives::{Date, ObjectIdentifier, PropertyValue, Time};
17use bytes::BytesMut;
18use tracing::warn;
19
20use super::{LocalDevice, ObjectDatabase};
21use crate::device_reference::decode_reference;
22use crate::log_buffer::ANY_VALUE_MAX_OCTETS;
23use crate::traits::{BACnetObject, MonotonicClock};
24use alignment::{Alignment, DAY};
25
26mod alignment;
27
28const RECONCILE: Duration = Duration::from_millis(100);
31
32#[derive(PartialEq)]
33struct Configuration {
34 mode: Mode,
35 reference: PropertyValue,
38 members: Vec<Member>,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq)]
45enum Mode {
46 Polled { interval: u32 },
48 Aligned { interval: u32, offset: u32 },
53 Triggered,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq)]
60enum Member {
61 Unspecified,
65 Reference {
67 target: ObjectIdentifier,
68 property: P,
69 index: Option<u32>,
70 device: Option<ObjectIdentifier>,
72 },
73}
74
75struct Schedule {
76 configuration: Configuration,
77 last_success: Option<Duration>,
78 retry_completed: Option<Duration>,
80 alignment: Alignment,
82}
83
84impl Schedule {
85 fn new(configuration: Configuration) -> Self {
86 Self {
87 configuration,
88 last_success: None,
89 retry_completed: None,
90 alignment: Alignment::default(),
91 }
92 }
93
94 fn remaining(&self, now: Duration) -> Duration {
95 if let Some(failed) = self.retry_completed {
96 return RECONCILE.saturating_sub(now.saturating_sub(failed));
97 }
98 match (self.configuration.mode, self.last_success) {
99 (Mode::Polled { interval }, Some(success)) => {
100 hundredths(interval).saturating_sub(now.saturating_sub(success))
101 }
102 (Mode::Aligned { interval, offset }, _) => self.alignment.wait(now, interval, offset),
103 _ => Duration::ZERO,
104 }
105 }
106}
107
108fn hundredths(value: u32) -> Duration {
109 Duration::from_millis(u64::from(value) * 10)
110}
111
112#[derive(Default)]
113pub(super) struct TrendPollSchedule(HashMap<ObjectIdentifier, Schedule>);
114
115impl TrendPollSchedule {
116 pub(super) fn retire(&mut self, oid: &ObjectIdentifier) {
117 self.0.remove(oid);
118 }
119
120 pub(super) fn clear(&mut self) {
121 self.0.clear();
122 }
123}
124
125impl ObjectDatabase {
126 pub fn poll_trend_logs(&mut self) -> Duration {
179 let mut logs = self.find_by_type(ObjectType::TREND_LOG);
180 logs.extend(self.find_by_type(ObjectType::TREND_LOG_MULTIPLE));
181 let trend_logs = logs.len();
182 logs.extend(self.find_by_type(ObjectType::EVENT_LOG));
183 for oid in &logs {
186 if let Some(log) = self.get_mut(oid) {
187 log.refresh_log_window_internal();
188 }
189 }
190 logs.truncate(trend_logs);
191 let Some(monotonic) = self.monotonic_clock.clone() else {
192 return RECONCILE;
193 };
194 let mut eligible = HashSet::new();
195 let local = self.local_device();
196 for oid in logs {
197 let Some(configuration) = self.get(&oid).and_then(configuration) else {
199 continue;
200 };
201 eligible.insert(oid);
202 if self
203 .trend_poll
204 .0
205 .get(&oid)
206 .is_none_or(|entry| entry.configuration != configuration)
207 {
208 self.trend_poll.0.insert(oid, Schedule::new(configuration));
209 }
210 self.poll_one(oid, local, &*monotonic);
211 }
212 self.trend_poll.0.retain(|oid, _| eligible.contains(oid));
213 let now = monotonic();
214 let remaining = self
215 .trend_poll
216 .0
217 .values()
218 .map(|entry| entry.remaining(now))
219 .min()
220 .unwrap_or(RECONCILE)
221 .min(RECONCILE);
222 if remaining.is_zero() {
223 Duration::from_millis(1)
224 } else {
225 remaining
226 }
227 }
228
229 fn poll_one(&mut self, oid: ObjectIdentifier, local: LocalDevice, monotonic: &MonotonicClock) {
231 let now = monotonic();
232 let entry = &self.trend_poll.0[&oid];
233 let mode = entry.configuration.mode;
234 let waiting = match mode {
238 Mode::Aligned { .. } => entry
239 .retry_completed
240 .is_some_and(|failed| now.saturating_sub(failed) < RECONCILE),
241 _ => !entry.remaining(now).is_zero(),
242 };
243 if waiting {
244 return;
245 }
246 let frame = self
247 .clock_frame()
248 .filter(|frame| frame.is_valid_actual_datetime());
249 let entry = self.trend_poll.0.get_mut(&oid).unwrap();
250 let mut boundary = None;
251 if let Mode::Aligned { interval, offset } = mode {
252 let Some(frame) = frame else {
254 entry.retry_completed = Some(now);
255 return;
256 };
257 entry.retry_completed = None;
258 boundary = entry.alignment.due(now, &frame, interval, offset);
259 if boundary.is_none() {
260 return;
261 }
262 }
263 let accepted = frame.is_some_and(|frame| {
264 let values: Vec<LogValue> = self.trend_poll.0[&oid]
265 .configuration
266 .members
267 .iter()
268 .map(|member| self.acquire(local, member))
269 .collect();
270 let object = self.get_mut(&oid).unwrap();
271 let inserted = if oid.object_type() == ObjectType::TREND_LOG_MULTIPLE {
272 object.add_trend_multiple_record(BACnetLogMultipleRecord {
273 date: frame.local_date,
274 time: frame.local_time,
275 log_data: LogData::Values(values),
276 })
277 } else {
278 object.add_trend_record(BACnetLogRecord {
279 date: frame.local_date,
280 time: frame.local_time,
281 log_datum: values
283 .into_iter()
284 .next()
285 .map_or(LogDatum::NullValue, LogDatum::from),
286 status_flags: None,
287 })
288 };
289 inserted
290 .inspect_err(|error| {
291 warn!(object = %oid, %error, "trend-log record insertion failed");
292 })
293 .is_ok()
294 });
295 let completed = monotonic();
296 if accepted && mode == Mode::Triggered {
297 self.trend_poll.0.remove(&oid);
299 return;
300 }
301 let entry = self.trend_poll.0.get_mut(&oid).unwrap();
302 if !accepted {
303 entry.retry_completed = Some(completed);
304 return;
305 }
306 entry.last_success = Some(completed);
307 entry.retry_completed = None;
308 if let Some(boundary) = boundary {
309 entry.alignment.serve(boundary);
310 }
311 }
312
313 fn acquire(&self, local: LocalDevice, member: &Member) -> LogValue {
316 let Member::Reference {
317 target,
318 property,
319 index,
320 device,
321 } = *member
322 else {
323 return failure(ErrorClass::PROPERTY, ErrorCode::NO_PROPERTY_SPECIFIED);
324 };
325 if !local.is_local(device) {
326 return failure(
328 ErrorClass::PROPERTY,
329 ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED,
330 );
331 }
332 let Some(target) = self.get(&target) else {
333 return failure(ErrorClass::OBJECT, ErrorCode::UNKNOWN_OBJECT);
334 };
335 if index.is_some() && !target.is_array_property(property) {
339 return failure(ErrorClass::PROPERTY, ErrorCode::PROPERTY_IS_NOT_AN_ARRAY);
340 }
341 match target.read_property(property, index) {
342 Ok(value) => property_value_to_log_value(&value),
343 Err(Error::Protocol { class, code } | Error::Structured { class, code, .. }) => {
344 LogValue::Failure {
345 error_class: class,
346 error_code: code,
347 }
348 }
349 Err(_) => failure(ErrorClass::SERVICES, ErrorCode::OTHER),
352 }
353 }
354}
355
356fn failure(class: ErrorClass, code: ErrorCode) -> LogValue {
357 LogValue::Failure {
358 error_class: u32::from(class.to_raw()),
359 error_code: u32::from(code.to_raw()),
360 }
361}
362
363fn configuration(object: &dyn BACnetObject) -> Option<Configuration> {
365 let read = |property| object.read_property(property, None).ok();
366 let logging_type = match read(P::LOGGING_TYPE) {
367 Some(PropertyValue::Enumerated(value)) => LoggingType::from_raw(value),
368 _ => LoggingType::POLLED,
369 };
370 let mode = match logging_type {
371 LoggingType::TRIGGERED => {
373 (read(P::TRIGGER) == Some(PropertyValue::Boolean(true))).then_some(Mode::Triggered)?
374 }
375 LoggingType::POLLED => {
376 let Some(PropertyValue::Unsigned(raw)) = read(P::LOG_INTERVAL) else {
377 return None;
378 };
379 let interval = u32::try_from(raw).ok().filter(|value| *value > 0)?;
380 let offset = match read(P::INTERVAL_OFFSET) {
381 Some(PropertyValue::Unsigned(offset)) => (offset % u64::from(interval)) as u32,
382 _ => 0,
383 };
384 let aligned = read(P::ALIGN_INTERVALS) == Some(PropertyValue::Boolean(true));
385 if aligned && DAY.is_multiple_of(interval) {
389 Mode::Aligned { interval, offset }
390 } else {
391 Mode::Polled { interval }
392 }
393 }
394 _ => return None,
395 };
396 let reference = read(P::LOG_DEVICE_OBJECT_PROPERTY)?;
397 let members = if object.object_identifier().object_type() == ObjectType::TREND_LOG_MULTIPLE {
398 let PropertyValue::List(elements) = &reference else {
399 return None;
400 };
401 elements.iter().map(member).collect::<Option<Vec<_>>>()?
402 } else {
403 match member(&reference)? {
404 Member::Unspecified if mode != Mode::Triggered => return None,
407 member => vec![member],
408 }
409 };
410 if members.is_empty() && mode != Mode::Triggered {
412 return None;
413 }
414 Some(Configuration {
415 mode,
416 reference,
417 members,
418 })
419}
420
421fn member(value: &PropertyValue) -> Option<Member> {
427 let reference: BACnetDeviceObjectPropertyReference = decode_reference(value).ok()?;
428 if reference.is_unset() {
429 return Some(Member::Unspecified);
430 }
431 Some(Member::Reference {
432 target: reference.object_identifier,
433 property: P::from_raw(reference.property_identifier),
434 index: reference.property_array_index,
435 device: reference.device_identifier,
436 })
437}
438
439fn property_value_to_log_value(value: &PropertyValue) -> LogValue {
450 let value = match value {
451 PropertyValue::Real(v) => LogValue::RealValue(*v),
452 PropertyValue::Unsigned(v) => LogValue::UnsignedValue(*v),
453 PropertyValue::Signed(v) => LogValue::SignedValue(i64::from(*v)),
454 PropertyValue::Boolean(v) => LogValue::BooleanValue(*v),
455 PropertyValue::Enumerated(v) => LogValue::EnumValue(u64::from(*v)),
456 PropertyValue::BitString { unused_bits, data } => LogValue::BitstringValue {
457 unused_bits: *unused_bits,
458 data: data.clone(),
459 },
460 PropertyValue::Null => LogValue::NullValue,
461 other => {
462 let mut encoded = BytesMut::new();
463 if encode_property_value(&mut encoded, other).is_err() {
464 return failure(ErrorClass::SERVICES, ErrorCode::OTHER);
466 }
467 if encoded.len() > ANY_VALUE_MAX_OCTETS {
468 return failure(ErrorClass::PROPERTY, ErrorCode::VALUE_TOO_LONG);
469 }
470 LogValue::AnyValue(encoded.to_vec())
471 }
472 };
473 if !matches!(
474 value,
475 LogValue::BitstringValue { .. } | LogValue::AnyValue(_)
476 ) {
477 return value;
478 }
479 let probe = BACnetLogMultipleRecord {
482 date: Date {
483 year: 0,
484 month: 1,
485 day: 1,
486 day_of_week: 1,
487 },
488 time: Time {
489 hour: 0,
490 minute: 0,
491 second: 0,
492 hundredths: 0,
493 },
494 log_data: LogData::Values(vec![value.clone()]),
495 };
496 match encode_log_multiple_record(&probe, &mut BytesMut::new()) {
497 Ok(()) => value,
498 Err(_) => failure(ErrorClass::SERVICES, ErrorCode::OTHER),
499 }
500}
501
502#[cfg(test)]
503mod tests;