Skip to main content

bacnet_objects/trend/
multiple.rs

1//! TrendLogMultiple (type 27, Clause 12.30).
2
3use std::borrow::Cow;
4use std::collections::VecDeque;
5use std::sync::Arc;
6
7use bacnet_types::bitstring::EventTransitionBits;
8use bacnet_types::constructed::{BACnetDeviceObjectPropertyReference, BACnetLogMultipleRecord};
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 super::acquisition::{Acquisition, Rules};
16use super::multiple_metadata;
17use super::references::{self, MAX_LOG_DEVICE_OBJECT_PROPERTIES};
18use crate::clock::ClockReader;
19use crate::common::{self, read_property_list_property};
20use crate::device_reference;
21use crate::log_buffer::{
22    log_buffer_read_denied, LogBufferRecords, LogRecordBuffer, LogRecordIdentity,
23};
24use crate::log_lifecycle::LogLifecycle;
25use crate::log_persistence::store::{DurableLog, LogStore};
26use crate::log_reporting::{impl_buffer_ready_reporting, BufferReadyReporting};
27use crate::log_window::LogWindow;
28use crate::traits::BACnetObject;
29
30/// BACnet TrendLogMultiple object (type 27).
31///
32/// Multi-channel trending. Unlike TrendLog, which monitors a single property,
33/// TrendLogMultiple monitors a list of device-object-property references and
34/// logs one value per reference in each record. The database's trend poller
35/// samples every reference every Log_Interval while Logging_Type is POLLED,
36/// at clock-aligned times when Align_Intervals asks for them, and once for
37/// each Trigger while it is TRIGGERED. Records are kept only while Enable is
38/// TRUE and the local time lies between Start_Time and Stop_Time.
39pub struct TrendLogMultipleObject {
40    oid: ObjectIdentifier,
41    name: String,
42    description: String,
43    log_enable: bool,
44    stop_when_full: bool,
45    buffer_size: u32,
46    log_buffer: LogRecordBuffer<BACnetLogMultipleRecord>,
47    log_device_object_property: Vec<BACnetDeviceObjectPropertyReference>,
48    acquisition: Acquisition,
49    window: LogWindow,
50    reporting: BufferReadyReporting,
51    reliability: Reliability,
52    clock: Option<Arc<dyn ClockReader>>,
53    /// The storage the log commits to, when built with `with_persistence`.
54    store: Option<LogStore<BACnetLogMultipleRecord>>,
55}
56
57impl TrendLogMultipleObject {
58    /// Build a log with logging enabled that holds at most `buffer_size`
59    /// records.
60    pub fn new(instance: u32, name: impl Into<String>, buffer_size: u32) -> Result<Self, Error> {
61        let oid = ObjectIdentifier::new(ObjectType::TREND_LOG_MULTIPLE, instance)?;
62        Ok(Self {
63            oid,
64            name: name.into(),
65            description: String::new(),
66            log_enable: true,
67            stop_when_full: false,
68            buffer_size,
69            log_buffer: LogRecordBuffer::new(buffer_size),
70            log_device_object_property: Vec::new(),
71            acquisition: Acquisition::new(Rules::TrendLogMultiple),
72            window: LogWindow::default(),
73            reporting: BufferReadyReporting::default(),
74            reliability: Reliability::NO_FAULT_DETECTED,
75            clock: None,
76            store: None,
77        })
78    }
79
80    /// Add a record to the log buffer.
81    ///
82    /// Success does not guarantee a resident ordinary record: disabled logging
83    /// and a record outside the Start_Time / Stop_Time window are ignored,
84    /// zero-capacity logging may only count, and a stop-before-full
85    /// transition records status instead. Missing/invalid status clocks fail
86    /// atomically with DEVICE / OPERATIONAL_PROBLEM. A successful call serves
87    /// a pending Trigger, which reads FALSE again, even when the record is
88    /// ignored (Enable FALSE, or outside the window): the acquisition was
89    /// made.
90    pub fn add_record(&mut self, record: BACnetLogMultipleRecord) -> Result<(), Error> {
91        self.settle_interruption(Some((record.date, record.time)));
92        self.lifecycle().try_add_ordinary(record)?;
93        self.acquisition.acquired();
94        self.commit_if_counted();
95        Ok(())
96    }
97
98    /// Append a reference to Log_DeviceObjectProperty as local configuration:
99    /// the log buffer is left as it is.
100    ///
101    /// A reference may name another device; the poller then logs a failure
102    /// in its slot of each record instead of reading it. A Device member that
103    /// isn't a Device identifier fails with PROPERTY / VALUE_OUT_OF_RANGE, and
104    /// a reference past [`MAX_LOG_DEVICE_OBJECT_PROPERTIES`] with RESOURCES /
105    /// NO_SPACE_TO_WRITE_PROPERTY; either leaves the array unchanged.
106    pub fn add_property_reference(
107        &mut self,
108        reference: BACnetDeviceObjectPropertyReference,
109    ) -> Result<(), Error> {
110        device_reference::check_device_member(reference.device_identifier)?;
111        if self.log_device_object_property.len() >= MAX_LOG_DEVICE_OBJECT_PROPERTIES {
112            return Err(references::no_space_error());
113        }
114        self.log_device_object_property.push(reference);
115        Ok(())
116    }
117
118    /// A client's write of Log_DeviceObjectProperty (#1234).
119    ///
120    /// A whole write replaces the array, at any length up to
121    /// [`MAX_LOG_DEVICE_OBJECT_PROPERTIES`]; an indexed write replaces one
122    /// element. Writing index 0 resizes the array (Clause 12.1.5.1): a smaller
123    /// Unsigned drops the trailing elements, a larger one appends empty
124    /// elements, one past the cap is RESOURCES / NO_SPACE_TO_WRITE_PROPERTY and
125    /// another datatype PROPERTY / INVALID_DATA_TYPE. Each written reference
126    /// goes through [`references::check_written`], where an element naming
127    /// instance 4194303 is an empty one. A new value purges the buffer,
128    /// leaving a BUFFER_PURGED status record, which is the first of the two
129    /// actions Clause 12.30.11 offers; without a valid clock the purge fails
130    /// with DEVICE / OPERATIONAL_PROBLEM and nothing changes. Writing the value
131    /// already held changes nothing.
132    fn write_log_device_object_property(
133        &mut self,
134        array_index: Option<u32>,
135        value: PropertyValue,
136    ) -> Result<(), Error> {
137        let mut candidate = self.log_device_object_property.clone();
138        match array_index {
139            None => {
140                let written = device_reference::decode_references(&value)?;
141                if written.len() > MAX_LOG_DEVICE_OBJECT_PROPERTIES {
142                    return Err(references::no_space_error());
143                }
144                for reference in &written {
145                    references::check_written(reference)?;
146                }
147                candidate = written;
148            }
149            Some(0) => {
150                let PropertyValue::Unsigned(size) = value else {
151                    return Err(common::invalid_data_type_error());
152                };
153                let size = usize::try_from(size)
154                    .ok()
155                    .filter(|size| *size <= MAX_LOG_DEVICE_OBJECT_PROPERTIES)
156                    .ok_or_else(references::no_space_error)?;
157                candidate.resize_with(size, references::empty_element);
158            }
159            Some(index) => {
160                let slot = usize::try_from(index - 1)
161                    .ok()
162                    .and_then(|slot| candidate.get_mut(slot))
163                    .ok_or_else(common::invalid_array_index_error)?;
164                let reference = device_reference::decode_reference(&value)?;
165                references::check_written(&reference)?;
166                *slot = reference;
167            }
168        }
169        if candidate != self.log_device_object_property {
170            self.lifecycle().purge()?;
171            self.log_device_object_property = candidate;
172        }
173        Ok(())
174    }
175
176    /// Get the current buffer contents.
177    pub fn records(&self) -> &VecDeque<BACnetLogMultipleRecord> {
178        self.log_buffer.records()
179    }
180
181    /// Total_Record_Count, as
182    /// [`TrendLogObject::total_record_count`](super::TrendLogObject::total_record_count)
183    /// serves it.
184    pub fn total_record_count(&self) -> u32 {
185        self.log_buffer.total_record_count()
186    }
187
188    /// Restore the log buffer, or seed Total_Record_Count with no records,
189    /// before the log is added to an `ObjectDatabase`, under the rules of
190    /// [`TrendLogObject::restore_log_buffer`](super::TrendLogObject::restore_log_buffer)
191    /// (#1537). Records aren't checked against Log_DeviceObjectProperty,
192    /// which may have changed since they were taken. After a restart, follow
193    /// it with [`record_interruption`](Self::record_interruption), which
194    /// counts toward Total_Record_Count.
195    pub fn restore_log_buffer(
196        &mut self,
197        total_record_count: u32,
198        records: impl IntoIterator<Item = BACnetLogMultipleRecord>,
199    ) -> Result<(), Error> {
200        self.lifecycle()
201            .restore(total_record_count, records.into_iter().collect())
202    }
203
204    /// Append a LOG_INTERRUPTED status record, as
205    /// [`TrendLogObject::record_interruption`](super::TrendLogObject::record_interruption)
206    /// does, after restoring the log on a restart (#1537). A log reopened
207    /// from storage appends this record itself; calling this first takes its
208    /// place.
209    pub fn record_interruption(&mut self, date: Date, time: Time) -> Result<(), Error> {
210        self.lifecycle().record_interruption((date, time))?;
211        if let Some(store) = &mut self.store {
212            store.interruption_recorded();
213        }
214        self.commit_if_counted();
215        Ok(())
216    }
217
218    /// Clear the buffer.
219    pub fn clear(&mut self) {
220        self.log_buffer.clear();
221    }
222
223    /// Set the description string.
224    pub fn set_description(&mut self, desc: impl Into<String>) {
225        self.description = desc.into();
226    }
227
228    /// Set Logging_Type, as a client's write does: POLLED or TRIGGERED. COV
229    /// logging isn't allowed for this object type (Clause 12.30.12), so COV,
230    /// like any other value, is PROPERTY / VALUE_OUT_OF_RANGE and changes
231    /// nothing. POLLED with a zero Log_Interval sets
232    /// [`DEFAULT_LOG_INTERVAL`](super::DEFAULT_LOG_INTERVAL); TRIGGERED sets
233    /// Log_Interval to zero.
234    pub fn set_logging_type(&mut self, logging_type: LoggingType) -> Result<(), Error> {
235        self.acquisition.set_logging_type(logging_type)
236    }
237
238    /// Set Log_Interval in hundredths of a second. While Logging_Type is
239    /// TRIGGERED it is read-only and this is PROPERTY / WRITE_ACCESS_DENIED.
240    pub fn set_log_interval(&mut self, hundredths: u32) -> Result<(), Error> {
241        self.acquisition.set_log_interval(hundredths)
242    }
243
244    /// Set Start_Time, the local date and time from which records are kept,
245    /// as local configuration: nothing is recorded for the change itself,
246    /// but the log notes at once where the window stands, so a client's
247    /// write that then opens or shuts it is recorded. Every field
248    /// unspecified leaves the start open. Any other value has to name an
249    /// actual date and time, or it is PROPERTY / VALUE_OUT_OF_RANGE: the
250    /// weekday may stay unspecified, and unspecified seconds or hundredths
251    /// count as zero.
252    pub fn set_start_time(&mut self, date: Date, time: Time) -> Result<(), Error> {
253        self.lifecycle()
254            .configure_window(PropertyIdentifier::START_TIME, (date, time))
255    }
256
257    /// Set Stop_Time, the local date and time from which records are no
258    /// longer kept, under the same rules as
259    /// [`set_start_time`](Self::set_start_time).
260    pub fn set_stop_time(&mut self, date: Date, time: Time) -> Result<(), Error> {
261        self.lifecycle()
262            .configure_window(PropertyIdentifier::STOP_TIME, (date, time))
263    }
264
265    /// Set Align_Intervals: whether a POLLED log acquires at clock-aligned
266    /// times.
267    pub fn set_align_intervals(&mut self, align: bool) {
268        self.acquisition.set_align_intervals(align);
269    }
270
271    /// Set Interval_Offset, the delay after each aligned boundary, in
272    /// hundredths; it applies modulo Log_Interval.
273    pub fn set_interval_offset(&mut self, hundredths: u32) {
274        self.acquisition.set_interval_offset(hundredths);
275    }
276
277    /// Ask for one acquisition, as a local process writing Trigger TRUE does.
278    /// Only a TRIGGERED log takes it; any other is PROPERTY /
279    /// NOT_CONFIGURED_FOR_TRIGGERED_LOGGING.
280    pub fn trigger(&mut self) -> Result<(), Error> {
281        self.acquisition.trigger()
282    }
283
284    pub(super) fn logging_type(&self) -> LoggingType {
285        self.acquisition.logging_type()
286    }
287
288    /// Set Notification_Threshold, the number of records that makes a
289    /// BUFFER_READY report; zero, the default, makes none.
290    pub fn set_notification_threshold(&mut self, threshold: u32) {
291        self.reporting.set_notification_threshold(threshold);
292    }
293
294    /// Set Notification_Class: the number of the Notification Class whose
295    /// recipients get the log's reports (0 by default).
296    pub fn set_notification_class(&mut self, class: u32) {
297        self.reporting.set_notification_class(class);
298    }
299
300    /// Set Event_Enable; a report goes out only while its TO_NORMAL flag is
301    /// set, as it is by default.
302    pub fn set_event_enable(&mut self, enable: EventTransitionBits) {
303        self.reporting.set_event_enable(enable);
304    }
305
306    /// Set Notify_Type, sent with each report (EVENT by default).
307    pub fn set_notify_type(&mut self, notify_type: NotifyType) {
308        self.reporting.set_notify_type(notify_type);
309    }
310
311    fn lifecycle(&mut self) -> LogLifecycle<'_, BACnetLogMultipleRecord> {
312        LogLifecycle::new(
313            &mut self.log_buffer,
314            &mut self.log_enable,
315            &mut self.stop_when_full,
316            self.clock.as_ref(),
317            &mut self.window,
318            &mut self.reporting,
319        )
320    }
321}
322
323impl BACnetObject for TrendLogMultipleObject {
324    fn object_identifier(&self) -> ObjectIdentifier {
325        self.oid
326    }
327
328    fn object_name(&self) -> &str {
329        &self.name
330    }
331
332    fn read_property(
333        &self,
334        property: PropertyIdentifier,
335        array_index: Option<u32>,
336    ) -> Result<PropertyValue, Error> {
337        // Logging_Type, Log_Interval, Align_Intervals, Interval_Offset and
338        // Trigger, then Start_Time and Stop_Time.
339        if let Some(value) = self
340            .acquisition
341            .read(property)
342            .or_else(|| self.window.read(property))
343        {
344            return Ok(value);
345        }
346        let total = self.log_buffer.total_record_count();
347        if let Some(result) = self.reporting.read(property, array_index, total) {
348            return result;
349        }
350        match property {
351            p if p == PropertyIdentifier::OBJECT_IDENTIFIER => {
352                Ok(PropertyValue::ObjectIdentifier(self.oid))
353            }
354            p if p == PropertyIdentifier::OBJECT_NAME => {
355                Ok(PropertyValue::CharacterString(self.name.clone()))
356            }
357            p if p == PropertyIdentifier::DESCRIPTION => {
358                Ok(PropertyValue::CharacterString(self.description.clone()))
359            }
360            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
361                ObjectType::TREND_LOG_MULTIPLE.to_raw(),
362            )),
363            p if p == PropertyIdentifier::LOG_ENABLE => Ok(PropertyValue::Boolean(self.log_enable)),
364            p if p == PropertyIdentifier::STOP_WHEN_FULL => {
365                Ok(PropertyValue::Boolean(self.stop_when_full))
366            }
367            p if p == PropertyIdentifier::BUFFER_SIZE => {
368                Ok(PropertyValue::Unsigned(self.buffer_size as u64))
369            }
370            p if p == PropertyIdentifier::RECORD_COUNT => {
371                Ok(PropertyValue::Unsigned(self.records().len() as u64))
372            }
373            p if p == PropertyIdentifier::TOTAL_RECORD_COUNT => {
374                Ok(PropertyValue::Unsigned(u64::from(total)))
375            }
376            // Clause 12.30.5 lets only IN_ALARM (from Event_State) and FAULT
377            // (from Reliability) move on a Trend Log Multiple; OVERRIDDEN and
378            // OUT_OF_SERVICE are always FALSE. Table 12-35 has no
379            // Out_Of_Service property (#985).
380            p if p == PropertyIdentifier::STATUS_FLAGS => Ok(common::compute_status_flags(
381                StatusFlags::empty(),
382                self.reliability,
383                false,
384                self.reporting.event_state(),
385            )),
386            p if p == PropertyIdentifier::RELIABILITY => {
387                Ok(PropertyValue::Enumerated(self.reliability.to_raw()))
388            }
389            // ReadRange pages it through `log_buffer_internal`.
390            p if p == PropertyIdentifier::LOG_BUFFER => Err(log_buffer_read_denied()),
391            // A BACnetARRAY: one Clause 21 encoding per element (#1234).
392            p if p == PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY => common::read_array(
393                device_reference::reference_elements(&self.log_device_object_property),
394                array_index,
395            ),
396            p if p == PropertyIdentifier::PROPERTY_LIST => {
397                read_property_list_property(&self.property_list(), array_index)
398            }
399            _ => Err(Error::Protocol {
400                class: ErrorClass::PROPERTY,
401                code: ErrorCode::UNKNOWN_PROPERTY,
402            }),
403        }
404    }
405
406    fn write_property(
407        &mut self,
408        property: PropertyIdentifier,
409        array_index: Option<u32>,
410        value: PropertyValue,
411        _priority: Option<u8>,
412    ) -> Result<(), Error> {
413        if property == PropertyIdentifier::LOG_ENABLE {
414            if let PropertyValue::Boolean(v) = value {
415                return self.lifecycle().write_enable(v);
416            }
417            return Err(Error::Protocol {
418                class: ErrorClass::PROPERTY,
419                code: ErrorCode::INVALID_DATA_TYPE,
420            });
421        }
422        if let Some(result) = self.acquisition.write(property, &value) {
423            return result;
424        }
425        if let Some(result) = self.window.write(property, &value) {
426            // A change that opens or closes the window is recorded at once.
427            result?;
428            self.lifecycle().refresh_window();
429            return Ok(());
430        }
431        if property == PropertyIdentifier::STOP_WHEN_FULL {
432            if let PropertyValue::Boolean(v) = value {
433                return self.lifecycle().write_stop_when_full(v);
434            }
435            return Err(Error::Protocol {
436                class: ErrorClass::PROPERTY,
437                code: ErrorCode::INVALID_DATA_TYPE,
438            });
439        }
440        if property == PropertyIdentifier::RECORD_COUNT {
441            if let PropertyValue::Unsigned(0) = value {
442                return self.lifecycle().purge();
443            }
444            return Err(Error::Protocol {
445                class: ErrorClass::PROPERTY,
446                code: ErrorCode::INVALID_DATA_TYPE,
447            });
448        }
449        if property == PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY {
450            return self.write_log_device_object_property(array_index, value);
451        }
452        let total = self.log_buffer.total_record_count();
453        if let Some(result) = self.reporting.write(property, array_index, &value, total) {
454            return result;
455        }
456        if let Some(result) = common::write_description(&mut self.description, property, &value) {
457            return result;
458        }
459        Err(crate::common::unhandled_write_error(
460            self.property_metadata().as_ref(),
461            property,
462            array_index,
463        ))
464    }
465
466    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
467        multiple_metadata::for_object(self)
468    }
469
470    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
471        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
472    }
473
474    fn bind_clock_internal(&mut self, clock: Option<Arc<dyn ClockReader>>) {
475        self.clock = clock;
476        self.settle_interruption(None);
477    }
478
479    fn durable_writes_internal(&mut self) -> Option<&mut dyn crate::durable::DurableWrites> {
480        self.store.is_some().then_some(self as _)
481    }
482
483    fn advance_monotonic_time_internal(&mut self, now: std::time::Duration) -> bool {
484        self.commit_if_interval(now);
485        false
486    }
487
488    fn log_record_identities_internal(&self) -> Option<Vec<LogRecordIdentity>> {
489        Some(self.log_buffer.identities())
490    }
491
492    fn log_buffer_internal(&self) -> Option<&dyn LogBufferRecords> {
493        Some(&self.log_buffer)
494    }
495
496    fn add_trend_multiple_record(&mut self, record: BACnetLogMultipleRecord) -> Result<(), Error> {
497        self.add_record(record)
498    }
499
500    fn refresh_log_window_internal(&mut self) -> bool {
501        self.settle_interruption(None);
502        let recorded = self.lifecycle().refresh_window();
503        self.commit_if_counted();
504        recorded
505    }
506
507    impl_buffer_ready_reporting!(reporting, log_buffer);
508}
509
510#[path = "multiple_durable.rs"]
511mod durable;