Skip to main content

bacnet_objects/trend/
multiple.rs

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