Skip to main content

bacnet_objects/
log_reporting.rs

1//! BUFFER_READY intrinsic reporting for the log objects (Clause 13.3.7).
2//!
3//! A Trend Log, Event Log or Trend Log Multiple that reports intrinsically
4//! tells the recipients of its Notification Class each time
5//! Notification_Threshold more records have been collected since its last
6//! report, so they can fetch the new ones with ReadRange. The three tables
7//! (12-29, 12-31 and 12-35) give each log the same event rows, and the
8//! crate's `BufferReadyReporting` keeps them for any of the three: it runs
9//! the algorithm on the log's Total_Record_Count, its pMonitoredValue, and
10//! serves and takes the rows itself. [`BufferReadyReport`] carries one
11//! report's counts to the server.
12//!
13//! Event_State never leaves NORMAL. Each report is a NORMAL to NORMAL
14//! transition, so it stamps the TO_NORMAL slots of Event_Time_Stamps,
15//! Event_Message_Texts and Acked_Transitions, and Event_Enable's TO_NORMAL
16//! flag decides whether it goes out.
17//!
18//! An object adopts it in three steps: hold one and lend it to its
19//! `LogLifecycle`, which restarts Records_Since_Notification at a purge;
20//! offer each read and write to its `read` and `write`; and invoke
21//! `impl_buffer_ready_reporting!` with the field and the log buffer.
22
23use bacnet_types::bitstring::EventTransitionBits;
24use bacnet_types::constructed::BACnetObjectPropertyReference;
25use bacnet_types::enums::{
26    ErrorClass, ErrorCode, EventState, EventType, NotifyType, PropertyIdentifier as P,
27};
28use bacnet_types::error::Error;
29use bacnet_types::primitives::{BACnetTimeStamp, PropertyValue};
30
31use crate::common;
32use crate::event::history::{EventHistory, EventTransitionState};
33use crate::event::options::REPORTING_OPTION_METADATA as OPTIONS;
34use crate::event::{
35    EnrollmentSummaryCapability, EventStateChange, EventTransitionCommit,
36    EventTransitionCommitError, TransitionOutcome,
37};
38use crate::property_metadata::{
39    PropertyConformance::Optional,
40    PropertyMetadata,
41    PropertyPresenceCondition::{IntrinsicReportingOptional, IntrinsicReportingRequired},
42    PropertyWriteCapability::{Always, ReadOnly},
43};
44
45/// The counts one BUFFER_READY report carries (Clause 13.3.7), as the
46/// server projects them into the notification's event values.
47#[doc(hidden)]
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct BufferReadyReport {
50    /// Previous_Notification: Last_Notify_Record before this report, the
51    /// Total_Record_Count at the report before or when detection started.
52    pub previous_notification: u32,
53    /// Current_Notification: the Total_Record_Count this report was made at.
54    pub current_notification: u32,
55}
56
57/// A row footnote 3 of Table 12-31 (4 of Tables 12-29 and 12-35) requires
58/// of a log that reports intrinsically.
59const fn required(
60    property: P,
61    write: crate::property_metadata::PropertyWriteCapability,
62) -> PropertyMetadata {
63    PropertyMetadata::new(property, Optional, Some(IntrinsicReportingRequired), write)
64}
65
66/// A row footnote 5 of Table 12-31 (7 of 12-29, 6 of 12-35) only lets such
67/// a log have.
68const fn permitted(
69    property: P,
70    write: crate::property_metadata::PropertyWriteCapability,
71) -> PropertyMetadata {
72    PropertyMetadata::new(property, Optional, Some(IntrinsicReportingOptional), write)
73}
74
75/// The event rows a log that reports intrinsically adds, in the order the
76/// three log tables list them: the ones the tables require of a log with
77/// intrinsic reporting, and Event_Message_Texts, Event_Message_Texts_Config
78/// and the Event_Algorithm_Inhibit pair, which they only permit (#1329).
79pub(crate) const BUFFER_READY_METADATA: [PropertyMetadata; 13] = [
80    required(P::NOTIFICATION_THRESHOLD, Always),
81    required(P::RECORDS_SINCE_NOTIFICATION, ReadOnly),
82    required(P::LAST_NOTIFY_RECORD, ReadOnly),
83    required(P::NOTIFICATION_CLASS, Always),
84    required(P::EVENT_ENABLE, Always),
85    required(P::ACKED_TRANSITIONS, ReadOnly),
86    required(P::NOTIFY_TYPE, Always),
87    required(P::EVENT_TIME_STAMPS, ReadOnly),
88    permitted(P::EVENT_MESSAGE_TEXTS, ReadOnly),
89    OPTIONS[0],
90    required(P::EVENT_DETECTION_ENABLE, Always),
91    OPTIONS[1],
92    OPTIONS[2],
93];
94
95/// The records collected from Total_Record_Count `from` to `to`. The count
96/// wraps from 2^32 - 1 to 1, never passing zero, so a smaller `to` has gone
97/// round once: the second condition of the algorithm (Clause 13.3.7).
98pub(crate) fn records_between(from: u32, to: u32) -> u32 {
99    if to >= from {
100        to - from
101    } else {
102        // At most 2^32 - 2: `to` is at least 1 once the count has wrapped.
103        (u64::from(to) + u64::from(u32::MAX) - u64::from(from)) as u32
104    }
105}
106
107/// The event state, configuration and history of one log reporting
108/// BUFFER_READY.
109#[derive(Debug, Clone)]
110pub(crate) struct BufferReadyReporting {
111    /// Notification_Threshold, pThreshold: the records that make a report.
112    /// Zero, the default, makes none.
113    notification_threshold: u32,
114    /// Last_Notify_Record, pPreviousCount: Total_Record_Count at the last
115    /// report, or when detection started.
116    last_notify_record: u32,
117    /// Total_Record_Count when Records_Since_Notification last restarted:
118    /// at the last report, purge or start of detection.
119    count_restarted_at: u32,
120    notification_class: u32,
121    event_enable: EventTransitionBits,
122    notify_type: NotifyType,
123    /// Always NORMAL: the algorithm knows no other state.
124    event_state: EventState,
125    acked_transitions: EventTransitionBits,
126    history: EventHistory,
127    /// Event_Detection_Enable: FALSE suspends the algorithm (Clause 13.2.2.1).
128    event_detection_enable: bool,
129    /// The counts of the last committed report, for its event values.
130    report: Option<BufferReadyReport>,
131}
132
133impl Default for BufferReadyReporting {
134    /// Detection on with no threshold, so nothing is reported until one is
135    /// set; Notification_Class 0, every transition enabled and acknowledged,
136    /// and reports sent as events rather than alarms.
137    fn default() -> Self {
138        Self {
139            notification_threshold: 0,
140            last_notify_record: 0,
141            count_restarted_at: 0,
142            notification_class: 0,
143            event_enable: EventTransitionBits::all(),
144            notify_type: NotifyType::EVENT,
145            event_state: EventState::NORMAL,
146            acked_transitions: EventTransitionBits::all(),
147            history: EventHistory::default(),
148            event_detection_enable: true,
149            report: None,
150        }
151    }
152}
153
154impl BufferReadyReporting {
155    /// Event_State as served: always NORMAL.
156    pub(crate) fn event_state(&self) -> EventState {
157        self.event_state
158    }
159
160    pub(crate) fn set_notification_threshold(&mut self, threshold: u32) {
161        self.notification_threshold = threshold;
162    }
163
164    pub(crate) fn set_notification_class(&mut self, class: u32) {
165        self.notification_class = class;
166    }
167
168    pub(crate) fn set_event_enable(&mut self, enable: EventTransitionBits) {
169        self.event_enable = enable;
170    }
171
172    pub(crate) fn set_notify_type(&mut self, notify_type: NotifyType) {
173        self.notify_type = notify_type;
174    }
175
176    /// Restart Records_Since_Notification at Total_Record_Count `total`, as a
177    /// purge does before it records BUFFER_PURGED (Clause 12.27.14). The
178    /// algorithm's own count, from Last_Notify_Record, goes on.
179    pub(crate) fn restart_count(&mut self, total: u32) {
180        self.count_restarted_at = total;
181    }
182
183    /// Serve one of the event rows for a log whose Total_Record_Count is
184    /// `total`, or `None` for any other property.
185    pub(crate) fn read(
186        &self,
187        property: P,
188        array_index: Option<u32>,
189        total: u32,
190    ) -> Option<Result<PropertyValue, Error>> {
191        let unsigned = |value: u32| Some(Ok(PropertyValue::Unsigned(u64::from(value))));
192        let transitions = |bits: EventTransitionBits| {
193            Some(Ok(PropertyValue::BitString {
194                unused_bits: 5,
195                data: vec![bits.to_bacnet()],
196            }))
197        };
198        match property {
199            P::EVENT_STATE => Some(Ok(PropertyValue::Enumerated(self.event_state.to_raw()))),
200            P::EVENT_DETECTION_ENABLE => {
201                Some(Ok(PropertyValue::Boolean(self.event_detection_enable)))
202            }
203            P::NOTIFICATION_THRESHOLD => unsigned(self.notification_threshold),
204            P::RECORDS_SINCE_NOTIFICATION => {
205                unsigned(records_between(self.count_restarted_at, total))
206            }
207            P::LAST_NOTIFY_RECORD => unsigned(self.last_notify_record),
208            P::NOTIFICATION_CLASS => unsigned(self.notification_class),
209            P::EVENT_ENABLE => transitions(self.event_enable),
210            P::ACKED_TRANSITIONS => transitions(self.acked_transitions),
211            P::NOTIFY_TYPE => Some(Ok(PropertyValue::Enumerated(self.notify_type.to_raw()))),
212            _ => self.history.read(property, array_index),
213        }
214    }
215
216    /// Take a network write of one of the writable event rows for a log
217    /// whose Total_Record_Count is `total`, or `None` for any other property.
218    /// Acked_Transitions is refused with WRITE_ACCESS_DENIED, as on every
219    /// intrinsic-reporting object; the other read-only rows are left to the
220    /// object's metadata. Event_Message_Texts_Config and the
221    /// Event_Algorithm_Inhibit pair go to the history's
222    /// [`ReportingOptions`](crate::event::options::ReportingOptions).
223    ///
224    /// Event_Detection_Enable FALSE puts Event_State, Acked_Transitions,
225    /// Event_Time_Stamps and Event_Message_Texts back to their starting
226    /// values (Clause 12.27.26). TRUE again starts the algorithm afresh,
227    /// counting from the current Total_Record_Count.
228    pub(crate) fn write(
229        &mut self,
230        property: P,
231        array_index: Option<u32>,
232        value: &PropertyValue,
233        total: u32,
234    ) -> Option<Result<(), Error>> {
235        if let Some(result) =
236            self.history
237                .write(property, array_index, value, self.event_detection_enable)
238        {
239            return Some(result);
240        }
241        let result = match property {
242            P::EVENT_DETECTION_ENABLE => boolean(value).map(|enable| {
243                if !enable {
244                    self.event_state = EventState::NORMAL;
245                    self.acked_transitions = EventTransitionBits::all();
246                    self.history.reset();
247                } else if !self.event_detection_enable {
248                    self.last_notify_record = total;
249                    self.count_restarted_at = total;
250                }
251                self.event_detection_enable = enable;
252            }),
253            P::NOTIFICATION_THRESHOLD => {
254                unsigned32(value).map(|threshold| self.notification_threshold = threshold)
255            }
256            P::NOTIFICATION_CLASS => unsigned32(value).map(|class| self.notification_class = class),
257            P::EVENT_ENABLE => match value {
258                PropertyValue::BitString { unused_bits, data } => {
259                    common::check_fixed_width_bit_string(*unused_bits, data, 3).map(|byte| {
260                        self.event_enable = EventTransitionBits::from_bacnet(&[byte]);
261                    })
262                }
263                _ => Err(common::invalid_data_type_error()),
264            },
265            P::NOTIFY_TYPE => match *value {
266                PropertyValue::Enumerated(raw) => {
267                    let notify_type = NotifyType::from_raw(raw);
268                    if NotifyType::ALL_NAMED.iter().any(|&(_, n)| n == notify_type) {
269                        self.notify_type = notify_type;
270                        Ok(())
271                    } else {
272                        Err(common::value_out_of_range_error())
273                    }
274                }
275                _ => Err(common::invalid_data_type_error()),
276            },
277            P::ACKED_TRANSITIONS => Err(common::write_access_denied_error()),
278            _ => return None,
279        };
280        Some(result)
281    }
282
283    /// The algorithm (Clause 13.3.7) for a log whose Total_Record_Count is
284    /// `total`: a NORMAL to NORMAL transition once Notification_Threshold or
285    /// more records have been collected since Last_Notify_Record, counting
286    /// across the wrap. No threshold, detection off, or a TRUE
287    /// Event_Algorithm_Inhibit, which stops a NORMAL state's re-entry
288    /// (Clause 13.2.2.1), proposes nothing; the records keep counting, so a
289    /// report falls due once the inhibit clears. Proposing changes nothing;
290    /// the commit does.
291    pub(crate) fn propose(&self, total: u32) -> Option<TransitionOutcome> {
292        let due = self.event_detection_enable
293            && !self.history.options.inhibited()
294            && self.notification_threshold > 0
295            && records_between(self.last_notify_record, total) >= self.notification_threshold;
296        due.then(|| TransitionOutcome {
297            change: EventStateChange {
298                from: EventState::NORMAL,
299                to: EventState::NORMAL,
300            },
301            event_type: EventType::BUFFER_READY,
302            distribute: self.event_enable.contains(EventTransitionBits::TO_NORMAL),
303        })
304    }
305
306    /// Commit one report made at Total_Record_Count `total` through the
307    /// shared kernel, then move Last_Notify_Record to `total` and restart
308    /// Records_Since_Notification. Any target other than NORMAL is refused
309    /// as unsupported: the algorithm has no other state.
310    pub(crate) fn commit(
311        &mut self,
312        commit: EventTransitionCommit,
313        total: u32,
314    ) -> Result<(), EventTransitionCommitError> {
315        if commit.change.to != EventState::NORMAL {
316            return Err(EventTransitionCommitError::Unsupported);
317        }
318        EventTransitionState::new(
319            &mut self.event_state,
320            &mut self.acked_transitions,
321            &mut self.history,
322        )
323        .commit(commit)?;
324        self.report = Some(BufferReadyReport {
325            previous_notification: self.last_notify_record,
326            current_notification: total,
327        });
328        self.last_notify_record = total;
329        self.count_restarted_at = total;
330        Ok(())
331    }
332
333    /// The counts of the last committed report.
334    pub(crate) fn report(&self) -> Option<BufferReadyReport> {
335        self.report
336    }
337
338    /// Acknowledge the latest report stamped `timestamp`; with detection off
339    /// there is nothing to acknowledge (NO_ALARM_CONFIGURED).
340    pub(crate) fn acknowledge(
341        &mut self,
342        event_state: EventState,
343        timestamp: &BACnetTimeStamp,
344    ) -> Result<Option<EventStateChange>, Error> {
345        if !self.event_detection_enable {
346            return Err(common::protocol_error(
347                ErrorClass::OBJECT,
348                ErrorCode::NO_ALARM_CONFIGURED,
349            ));
350        }
351        self.history.acknowledge_correlated_detailed(
352            &mut self.acked_transitions,
353            event_state,
354            timestamp,
355        )
356    }
357
358    /// The property Event_Algorithm_Inhibit follows, if any.
359    pub(crate) fn inhibit_reference(&self) -> Option<BACnetObjectPropertyReference> {
360        self.history.options.inhibit_reference().cloned()
361    }
362
363    /// Take the referenced value as Event_Algorithm_Inhibit; returns whether
364    /// it changed.
365    pub(crate) fn follow_inhibit(&mut self, inhibit: bool) -> bool {
366        self.history.options.follow(inhibit)
367    }
368
369    /// What GetEnrollmentSummary reports for the log.
370    pub(crate) fn enrollment_summary(&self) -> EnrollmentSummaryCapability {
371        EnrollmentSummaryCapability {
372            event_type: EventType::BUFFER_READY,
373            last_transition: self.history.last_transition(),
374        }
375    }
376}
377
378fn boolean(value: &PropertyValue) -> Result<bool, Error> {
379    match *value {
380        PropertyValue::Boolean(value) => Ok(value),
381        _ => Err(common::invalid_data_type_error()),
382    }
383}
384
385fn unsigned32(value: &PropertyValue) -> Result<u32, Error> {
386    match *value {
387        PropertyValue::Unsigned(value) => common::u64_to_u32(value),
388        _ => Err(common::invalid_data_type_error()),
389    }
390}
391
392/// Implement the `BACnetObject` intrinsic-reporting hooks of a log that
393/// holds a [`BufferReadyReporting`] in `$reporting` and its records in the
394/// [`LogRecordBuffer`](crate::log_buffer::LogRecordBuffer) `$buffer`.
395macro_rules! impl_buffer_ready_reporting {
396    ($reporting:ident, $buffer:ident) => {
397        fn enrollment_summary_capability_internal(
398            &self,
399        ) -> Option<$crate::event::EnrollmentSummaryCapability> {
400            Some(self.$reporting.enrollment_summary())
401        }
402
403        fn evaluate_intrinsic_reporting(&mut self) -> Option<$crate::event::TransitionOutcome> {
404            self.$reporting.propose(self.$buffer.total_record_count())
405        }
406
407        fn tick_intrinsic_reporting(&mut self) -> Option<$crate::event::TransitionOutcome> {
408            self.$reporting.propose(self.$buffer.total_record_count())
409        }
410
411        fn commit_event_transition_internal(
412            &mut self,
413            commit: $crate::event::EventTransitionCommit,
414        ) -> Result<(), $crate::event::EventTransitionCommitError> {
415            let total = self.$buffer.total_record_count();
416            self.$reporting.commit(commit, total)
417        }
418
419        fn acknowledge_alarm_correlated_internal(
420            &mut self,
421            event_state: bacnet_types::enums::EventState,
422            timestamp: &bacnet_types::primitives::BACnetTimeStamp,
423        ) -> Result<(), bacnet_types::error::Error> {
424            self.$reporting
425                .acknowledge(event_state, timestamp)
426                .map(|_| ())
427        }
428
429        fn acknowledge_alarm_correlated_detailed_internal(
430            &mut self,
431            event_state: bacnet_types::enums::EventState,
432            timestamp: &bacnet_types::primitives::BACnetTimeStamp,
433        ) -> Result<Option<$crate::event::EventStateChange>, bacnet_types::error::Error> {
434            self.$reporting.acknowledge(event_state, timestamp)
435        }
436
437        fn buffer_ready_report_internal(&self) -> Option<$crate::log_reporting::BufferReadyReport> {
438            self.$reporting.report()
439        }
440
441        fn event_algorithm_inhibit_reference_internal(
442            &self,
443        ) -> Option<bacnet_types::constructed::BACnetObjectPropertyReference> {
444            self.$reporting.inhibit_reference()
445        }
446
447        fn follow_event_algorithm_inhibit_internal(&mut self, inhibit: bool) -> bool {
448            self.$reporting.follow_inhibit(inhibit)
449        }
450    };
451}
452
453pub(crate) use impl_buffer_ready_reporting;
454
455#[cfg(test)]
456#[path = "log_reporting_tests.rs"]
457mod tests;