bacnet_objects/trend/mod.rs
1//! TrendLog (type 20) and TrendLogMultiple (type 27) objects per ASHRAE 135-2020.
2
3use std::borrow::Cow;
4use std::collections::VecDeque;
5use std::sync::Arc;
6
7use bacnet_types::bitstring::EventTransitionBits;
8use bacnet_types::constructed::{BACnetDeviceObjectPropertyReference, BACnetLogRecord};
9use bacnet_types::enums::{
10 ErrorClass, ErrorCode, LoggingType, NotifyType, ObjectType, PropertyIdentifier, Reliability,
11};
12use bacnet_types::error::Error;
13use bacnet_types::primitives::{Date, ObjectIdentifier, PropertyValue, StatusFlags, Time};
14
15use crate::clock::ClockReader;
16use crate::common::{self, read_property_list_property};
17use crate::log_buffer::{
18 log_buffer_read_denied, LogBufferRecords, LogRecordBuffer, LogRecordIdentity,
19};
20use crate::log_lifecycle::LogLifecycle;
21use crate::log_reporting::{impl_buffer_ready_reporting, BufferReadyReporting};
22use crate::log_window::LogWindow;
23use crate::traits::BACnetObject;
24use acquisition::{Acquisition, Rules};
25
26mod acquisition;
27mod metadata;
28mod multiple;
29mod multiple_metadata;
30mod references;
31
32pub use acquisition::DEFAULT_LOG_INTERVAL;
33pub use multiple::TrendLogMultipleObject;
34pub use references::MAX_LOG_DEVICE_OBJECT_PROPERTIES;
35
36/// BACnet TrendLog object (type 20, Clause 12.25).
37///
38/// Ring buffer of timestamped values of the one property
39/// Log_DeviceObjectProperty names. The database's trend poller samples it
40/// every Log_Interval while Logging_Type is POLLED, at clock-aligned times
41/// when Align_Intervals asks for them, and once for each Trigger while it is
42/// TRIGGERED; the application may also call `add_record()`. Records are kept
43/// only while Enable is TRUE and the local time lies between Start_Time and
44/// Stop_Time. COV logging isn't carried out, so Logging_Type refuses it.
45pub struct TrendLogObject {
46 oid: ObjectIdentifier,
47 name: String,
48 description: String,
49 log_enable: bool,
50 stop_when_full: bool,
51 buffer_size: u32,
52 log_buffer: LogRecordBuffer,
53 reliability: Reliability,
54 log_device_object_property: Option<BACnetDeviceObjectPropertyReference>,
55 acquisition: Acquisition,
56 window: LogWindow,
57 reporting: BufferReadyReporting,
58 clock: Option<Arc<dyn ClockReader>>,
59}
60
61impl TrendLogObject {
62 /// Create a new Trend Log object with logging enabled; `buffer_size` is the record capacity.
63 pub fn new(instance: u32, name: impl Into<String>, buffer_size: u32) -> Result<Self, Error> {
64 let oid = ObjectIdentifier::new(ObjectType::TREND_LOG, instance)?;
65 Ok(Self {
66 oid,
67 name: name.into(),
68 description: String::new(),
69 log_enable: true,
70 stop_when_full: false,
71 buffer_size,
72 log_buffer: LogRecordBuffer::new(buffer_size),
73 reliability: Reliability::NO_FAULT_DETECTED,
74 log_device_object_property: None,
75 acquisition: Acquisition::new(Rules::TrendLog),
76 window: LogWindow::default(),
77 reporting: BufferReadyReporting::default(),
78 clock: None,
79 })
80 }
81
82 /// Add a BACnetLogRecord to the trend log buffer.
83 ///
84 /// Success does not guarantee a resident ordinary record: disabled logging
85 /// and a record outside the Start_Time / Stop_Time window are ignored,
86 /// zero-capacity logging may only count, and a stop-before-full
87 /// transition records status instead. Missing/invalid status clocks fail
88 /// atomically with DEVICE / OPERATIONAL_PROBLEM. A successful call serves
89 /// a pending Trigger, which reads FALSE again, even when the record is
90 /// ignored (Enable FALSE, or outside the window): the acquisition was
91 /// made.
92 pub fn add_record(&mut self, record: BACnetLogRecord) -> Result<(), Error> {
93 self.lifecycle().try_add_ordinary(record)?;
94 self.acquisition.acquired();
95 Ok(())
96 }
97
98 /// Get the current buffer contents.
99 pub fn records(&self) -> &VecDeque<BACnetLogRecord> {
100 self.log_buffer.records()
101 }
102
103 /// Total_Record_Count: the records collected since the log was created
104 /// or restored, going from 2^32 - 1 on to 1. The newest record in
105 /// [`records`](Self::records) carries this number. Save the two to
106 /// [restore](Self::restore_log_buffer) the log later.
107 pub fn total_record_count(&self) -> u32 {
108 self.log_buffer.total_record_count()
109 }
110
111 /// Restore the log buffer, as a device restarting with saved records
112 /// does, or seed Total_Record_Count with no records, as a test that
113 /// needs the count near its wrap does (#1537).
114 ///
115 /// `records` become the resident records, oldest first. The newest is
116 /// numbered `total_record_count` and each one before it one less, going
117 /// from 1 back to 2^32 - 1, so a count below the number of records is a
118 /// count that has wrapped. The records are kept as given, whatever
119 /// Enable and the Start_Time / Stop_Time window say, and nothing is
120 /// recorded for the restore. BUFFER_READY counts from the restored
121 /// count, as when detection starts.
122 ///
123 /// Fails with [`Error::OutOfRange`], or a record's encoding error,
124 /// leaving the log unchanged, when there are more records than
125 /// Buffer_Size, when records come with a count of zero, when Stop_When_Full
126 /// and Enable are both TRUE and the records fill the buffer (such a log
127 /// stops before its last slot is taken), or when a record would not
128 /// encode.
129 ///
130 /// Restore a log before adding it to an `ObjectDatabase`. A running
131 /// server reaches its objects only through `BACnetObject`, which offers
132 /// no restore on purpose: renumbering a log that peers are reading would
133 /// change what their sequence numbers mean, with no BUFFER_PURGED record
134 /// to tell them, and the BUFFER_READY reports already sent would name
135 /// counts that no longer hold.
136 ///
137 /// A device restoring its saved records after a restart then calls
138 /// [`record_interruption`](Self::record_interruption) with the time it
139 /// came back: Clause 12.25.14 gives a log that status when a power
140 /// failure or reset broke its collection, so readers know samples may
141 /// be missing. That record counts toward Total_Record_Count like any
142 /// other, numbered one past the restored count.
143 pub fn restore_log_buffer(
144 &mut self,
145 total_record_count: u32,
146 records: impl IntoIterator<Item = BACnetLogRecord>,
147 ) -> Result<(), Error> {
148 self.lifecycle()
149 .restore(total_record_count, records.into_iter().collect())
150 }
151
152 /// Append a LOG_INTERRUPTED status record stamped `date` and `time`, as
153 /// a log restored after a restart does (#1537); see
154 /// [`restore_log_buffer`](Self::restore_log_buffer).
155 ///
156 /// The record goes in whatever Enable and the window say, counts toward
157 /// Total_Record_Count, and pushes out the oldest record of a full
158 /// buffer. It also carries LOG_DISABLED while collection is off; when it
159 /// fills a Stop_When_Full buffer it does so and turns Enable FALSE, as
160 /// the log's own status records do. Without a clock bound, as before the
161 /// log is added to a database, the time has to come from the caller: a
162 /// `date` and `time` that aren't an actual moment (every field given,
163 /// the weekday matching the date) fail with [`Error::OutOfRange`] and
164 /// change nothing.
165 pub fn record_interruption(&mut self, date: Date, time: Time) -> Result<(), Error> {
166 self.lifecycle().record_interruption((date, time))
167 }
168
169 /// Clear the buffer.
170 pub fn clear(&mut self) {
171 self.log_buffer.clear();
172 }
173
174 /// Set the description string.
175 pub fn set_description(&mut self, desc: impl Into<String>) {
176 self.description = desc.into();
177 }
178
179 /// Set Log_DeviceObjectProperty, the property the log samples, as local
180 /// configuration: the log buffer is left as it is.
181 ///
182 /// A reference may name another device; the poller then logs a failure
183 /// for each sample instead of reading it. `None`, or a reference whose
184 /// object or Device is at the reserved instance 4194303, leaves the log
185 /// without one (#1417). A Device member that isn't a Device identifier
186 /// fails with PROPERTY / VALUE_OUT_OF_RANGE and changes nothing.
187 pub fn set_log_device_object_property(
188 &mut self,
189 reference: Option<BACnetDeviceObjectPropertyReference>,
190 ) -> Result<(), Error> {
191 if let Some(reference) = &reference {
192 crate::device_reference::check_device_member(reference.device_identifier)?;
193 }
194 self.log_device_object_property = reference.and_then(crate::device_reference::set_or_unset);
195 Ok(())
196 }
197
198 /// A client's write of Log_DeviceObjectProperty (#1234): one reference,
199 /// the unset form clearing it (#1417). See [`references::check_written`]
200 /// for the refusals; Null is no reference, so INVALID_DATA_TYPE. A new
201 /// value purges the buffer, leaving a BUFFER_PURGED status record (Clause
202 /// 12.25.8); without a valid clock the purge fails with DEVICE /
203 /// OPERATIONAL_PROBLEM and nothing changes. Writing the value already held
204 /// changes nothing.
205 fn write_log_device_object_property(&mut self, value: PropertyValue) -> Result<(), Error> {
206 let reference = crate::device_reference::decode_reference(&value)?;
207 references::check_written(&reference)?;
208 let reference = crate::device_reference::set_or_unset(reference);
209 if reference != self.log_device_object_property {
210 self.lifecycle().purge()?;
211 self.log_device_object_property = reference;
212 }
213 Ok(())
214 }
215
216 /// Set Logging_Type, as a client's write does: POLLED or TRIGGERED. This
217 /// device has no COV acquisition yet (#1480), so COV, like any other
218 /// value, is PROPERTY / OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED (Clause
219 /// 12.25.26) and changes nothing. POLLED with a zero Log_Interval sets
220 /// [`DEFAULT_LOG_INTERVAL`]; TRIGGERED sets Log_Interval to zero.
221 pub fn set_logging_type(&mut self, logging_type: LoggingType) -> Result<(), Error> {
222 self.acquisition.set_logging_type(logging_type)
223 }
224
225 /// Set Log_Interval in hundredths of a second, as a client's write does.
226 /// While Logging_Type is TRIGGERED it is read-only and this is PROPERTY /
227 /// WRITE_ACCESS_DENIED. A POLLED log's nonzero interval set to zero would
228 /// switch it to COV logging (Clause 12.25.9), which is refused with
229 /// PROPERTY / OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED, as COV itself is.
230 pub fn set_log_interval(&mut self, hundredths: u32) -> Result<(), Error> {
231 self.acquisition.set_log_interval(hundredths)
232 }
233
234 /// Set Start_Time, the local date and time from which records are kept,
235 /// as local configuration: nothing is recorded for the change itself,
236 /// but the log notes at once where the window stands, so a client's
237 /// write that then opens or shuts it is recorded. Every field
238 /// unspecified leaves the start open. Any other value has to name an
239 /// actual date and time, or it is PROPERTY / VALUE_OUT_OF_RANGE: the
240 /// weekday may stay unspecified, and unspecified seconds or hundredths
241 /// count as zero.
242 pub fn set_start_time(&mut self, date: Date, time: Time) -> Result<(), Error> {
243 self.lifecycle()
244 .configure_window(PropertyIdentifier::START_TIME, (date, time))
245 }
246
247 /// Set Stop_Time, the local date and time from which records are no
248 /// longer kept, under the same rules as
249 /// [`set_start_time`](Self::set_start_time).
250 pub fn set_stop_time(&mut self, date: Date, time: Time) -> Result<(), Error> {
251 self.lifecycle()
252 .configure_window(PropertyIdentifier::STOP_TIME, (date, time))
253 }
254
255 /// Set Align_Intervals: whether a POLLED log acquires at clock-aligned
256 /// times.
257 pub fn set_align_intervals(&mut self, align: bool) {
258 self.acquisition.set_align_intervals(align);
259 }
260
261 /// Set Interval_Offset, the delay after each aligned boundary, in
262 /// hundredths; it applies modulo Log_Interval.
263 pub fn set_interval_offset(&mut self, hundredths: u32) {
264 self.acquisition.set_interval_offset(hundredths);
265 }
266
267 /// Ask for one acquisition, as a local process writing Trigger TRUE does.
268 /// Only a TRIGGERED log takes it; any other is PROPERTY /
269 /// NOT_CONFIGURED_FOR_TRIGGERED_LOGGING.
270 pub fn trigger(&mut self) -> Result<(), Error> {
271 self.acquisition.trigger()
272 }
273
274 /// Set Notification_Threshold, the number of records that makes a
275 /// BUFFER_READY report; zero, the default, makes none.
276 pub fn set_notification_threshold(&mut self, threshold: u32) {
277 self.reporting.set_notification_threshold(threshold);
278 }
279
280 /// Set Notification_Class: the number of the Notification Class whose
281 /// recipients get the log's reports (0 by default).
282 pub fn set_notification_class(&mut self, class: u32) {
283 self.reporting.set_notification_class(class);
284 }
285
286 /// Set Event_Enable; a report goes out only while its TO_NORMAL flag is
287 /// set, as it is by default.
288 pub fn set_event_enable(&mut self, enable: EventTransitionBits) {
289 self.reporting.set_event_enable(enable);
290 }
291
292 /// Set Notify_Type, sent with each report (EVENT by default).
293 pub fn set_notify_type(&mut self, notify_type: NotifyType) {
294 self.reporting.set_notify_type(notify_type);
295 }
296
297 fn lifecycle(&mut self) -> LogLifecycle<'_, BACnetLogRecord> {
298 LogLifecycle::new(
299 &mut self.log_buffer,
300 &mut self.log_enable,
301 &mut self.stop_when_full,
302 self.clock.as_ref(),
303 &mut self.window,
304 &mut self.reporting,
305 )
306 }
307}
308
309impl BACnetObject for TrendLogObject {
310 fn object_identifier(&self) -> ObjectIdentifier {
311 self.oid
312 }
313
314 fn object_name(&self) -> &str {
315 &self.name
316 }
317
318 fn read_property(
319 &self,
320 property: PropertyIdentifier,
321 array_index: Option<u32>,
322 ) -> Result<PropertyValue, Error> {
323 // Logging_Type, Log_Interval, Align_Intervals, Interval_Offset and
324 // Trigger, then Start_Time and Stop_Time.
325 if let Some(value) = self
326 .acquisition
327 .read(property)
328 .or_else(|| self.window.read(property))
329 {
330 return Ok(value);
331 }
332 let total = self.log_buffer.total_record_count();
333 if let Some(result) = self.reporting.read(property, array_index, total) {
334 return result;
335 }
336 match property {
337 p if p == PropertyIdentifier::OBJECT_IDENTIFIER => {
338 Ok(PropertyValue::ObjectIdentifier(self.oid))
339 }
340 p if p == PropertyIdentifier::OBJECT_NAME => {
341 Ok(PropertyValue::CharacterString(self.name.clone()))
342 }
343 p if p == PropertyIdentifier::DESCRIPTION => {
344 Ok(PropertyValue::CharacterString(self.description.clone()))
345 }
346 p if p == PropertyIdentifier::OBJECT_TYPE => {
347 Ok(PropertyValue::Enumerated(ObjectType::TREND_LOG.to_raw()))
348 }
349 p if p == PropertyIdentifier::LOG_ENABLE => Ok(PropertyValue::Boolean(self.log_enable)),
350 p if p == PropertyIdentifier::STOP_WHEN_FULL => {
351 Ok(PropertyValue::Boolean(self.stop_when_full))
352 }
353 p if p == PropertyIdentifier::BUFFER_SIZE => {
354 Ok(PropertyValue::Unsigned(self.buffer_size as u64))
355 }
356 p if p == PropertyIdentifier::RECORD_COUNT => {
357 Ok(PropertyValue::Unsigned(self.records().len() as u64))
358 }
359 p if p == PropertyIdentifier::TOTAL_RECORD_COUNT => {
360 Ok(PropertyValue::Unsigned(u64::from(total)))
361 }
362 // Clause 12.25.30 lets only IN_ALARM (from Event_State) and FAULT
363 // (from Reliability) move on a Trend Log; OVERRIDDEN and
364 // OUT_OF_SERVICE are always FALSE. Table 12-29 has no
365 // Out_Of_Service property (#985).
366 p if p == PropertyIdentifier::STATUS_FLAGS => Ok(common::compute_status_flags(
367 StatusFlags::empty(),
368 self.reliability,
369 false,
370 self.reporting.event_state(),
371 )),
372 p if p == PropertyIdentifier::RELIABILITY => {
373 Ok(PropertyValue::Enumerated(self.reliability.to_raw()))
374 }
375 // ReadRange pages it through `log_buffer_internal`.
376 p if p == PropertyIdentifier::LOG_BUFFER => Err(log_buffer_read_denied()),
377 // The Clause 21 encoding; while no reference is set, the empty
378 // element a Trend Log Multiple grows by (#1417).
379 p if p == PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY => {
380 Ok(crate::device_reference::optional_reference_value(
381 self.log_device_object_property.as_ref(),
382 ObjectType::ANALOG_INPUT,
383 ))
384 }
385 p if p == PropertyIdentifier::PROPERTY_LIST => {
386 read_property_list_property(&self.property_list(), array_index)
387 }
388 _ => Err(Error::Protocol {
389 class: ErrorClass::PROPERTY.to_raw() as u32,
390 code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
391 }),
392 }
393 }
394
395 fn write_property(
396 &mut self,
397 property: PropertyIdentifier,
398 array_index: Option<u32>,
399 value: PropertyValue,
400 _priority: Option<u8>,
401 ) -> Result<(), Error> {
402 if property == PropertyIdentifier::LOG_ENABLE {
403 if let PropertyValue::Boolean(v) = value {
404 return self.lifecycle().write_enable(v);
405 }
406 return Err(Error::Protocol {
407 class: ErrorClass::PROPERTY.to_raw() as u32,
408 code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
409 });
410 }
411 if let Some(result) = self.acquisition.write(property, &value) {
412 return result;
413 }
414 if let Some(result) = self.window.write(property, &value) {
415 // A change that opens or closes the window is recorded at once.
416 result?;
417 self.lifecycle().refresh_window();
418 return Ok(());
419 }
420 if property == PropertyIdentifier::STOP_WHEN_FULL {
421 if let PropertyValue::Boolean(v) = value {
422 return self.lifecycle().write_stop_when_full(v);
423 }
424 return Err(Error::Protocol {
425 class: ErrorClass::PROPERTY.to_raw() as u32,
426 code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
427 });
428 }
429 if property == PropertyIdentifier::RECORD_COUNT {
430 if let PropertyValue::Unsigned(0) = value {
431 return self.lifecycle().purge();
432 }
433 return Err(Error::Protocol {
434 class: ErrorClass::PROPERTY.to_raw() as u32,
435 code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
436 });
437 }
438 // Clause 12.25 Table 12-29 lists Reliability as plain O with no
439 // writability footnote, and unlike the intrinsic-reporting objects the
440 // Trend Log Reliability_Evaluation_Inhibit paragraph requires
441 // NO_FAULT_DETECTED while evaluation is inhibited, without an exception
442 // for a client-supplied Reliability value while Out_Of_Service is TRUE.
443 // Nothing in Clause 12.25 grants a
444 // network client this property: the log owns it (logging status and
445 // fault indication), so every write is refused.
446 if property == PropertyIdentifier::RELIABILITY {
447 return Err(Error::Protocol {
448 class: ErrorClass::PROPERTY.to_raw() as u32,
449 code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
450 });
451 }
452 if property == PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY {
453 return self.write_log_device_object_property(value);
454 }
455 let total = self.log_buffer.total_record_count();
456 if let Some(result) = self.reporting.write(property, array_index, &value, total) {
457 return result;
458 }
459 if let Some(result) = common::write_description(&mut self.description, property, &value) {
460 return result;
461 }
462 Err(crate::common::unhandled_write_error(
463 self.property_metadata().as_ref(),
464 property,
465 array_index,
466 ))
467 }
468
469 fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
470 metadata::for_object(self)
471 }
472
473 fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
474 crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
475 }
476
477 fn bind_clock_internal(&mut self, clock: Option<Arc<dyn ClockReader>>) {
478 self.clock = clock;
479 }
480
481 fn log_record_identities_internal(&self) -> Option<Vec<LogRecordIdentity>> {
482 Some(self.log_buffer.identities())
483 }
484
485 fn log_buffer_internal(&self) -> Option<&dyn LogBufferRecords> {
486 Some(&self.log_buffer)
487 }
488
489 fn add_trend_record(&mut self, record: BACnetLogRecord) -> Result<(), Error> {
490 self.add_record(record)
491 }
492
493 fn refresh_log_window_internal(&mut self) -> bool {
494 self.lifecycle().refresh_window()
495 }
496
497 impl_buffer_ready_reporting!(reporting, log_buffer);
498}
499
500#[cfg(test)]
501mod log_record_tests;
502
503#[cfg(test)]
504mod multiple_options_tests;
505
506#[cfg(test)]
507mod options_tests;
508
509#[cfg(test)]
510mod reference_tests;
511
512#[cfg(test)]
513mod tests;