Skip to main content

bacnet_objects/
log_buffer.rs

1//! Shared resident storage and identity projection for BACnet log objects.
2
3use std::collections::VecDeque;
4
5use bacnet_encoding::constructed::{
6    encode_event_log_record, encode_log_multiple_record, encode_log_record,
7};
8use bacnet_types::bitstring::LogStatus;
9use bacnet_types::constructed::{
10    BACnetEventLogRecord, BACnetLogMultipleRecord, BACnetLogRecord, EventLogDatum, LogData,
11    LogDatum,
12};
13use bacnet_types::enums::{ErrorClass, ErrorCode, PropertyIdentifier as P};
14use bacnet_types::error::Error;
15use bacnet_types::primitives::{Date, Time};
16use bytes::BytesMut;
17
18use crate::property_metadata::{
19    PropertyConformance::RequiredRead, PropertyMetadata, PropertyWriteCapability::ReadOnly,
20};
21
22// Shared conformance rows only. LOG_BUFFER is a present, read-only BACnetLIST
23// that ReadProperty refuses; ReadRange pages it through `LogBufferRecords`.
24pub(crate) const BUFFER_SIZE_METADATA: PropertyMetadata =
25    PropertyMetadata::new(P::BUFFER_SIZE, RequiredRead, None, ReadOnly);
26pub(crate) const LOG_BUFFER_METADATA: PropertyMetadata =
27    PropertyMetadata::new(P::LOG_BUFFER, RequiredRead, None, ReadOnly);
28pub(crate) const TOTAL_RECORD_COUNT_METADATA: PropertyMetadata =
29    PropertyMetadata::new(P::TOTAL_RECORD_COUNT, RequiredRead, None, ReadOnly);
30
31/// The answer a log object gives a ReadProperty of its Log_Buffer.
32///
33/// Clauses 12.25.14, 12.27.13, 12.30.19 and 12.64.10 open a log buffer to
34/// ReadRange (and, for an Audit Log, AuditLogQuery) only, so a property read
35/// names the property as present but not readable this way
36/// (Clause 15.5.1.3.1).
37pub(crate) fn log_buffer_read_denied() -> Error {
38    Error::Protocol {
39        class: ErrorClass::PROPERTY.to_raw() as u32,
40        code: ErrorCode::READ_ACCESS_DENIED.to_raw() as u32,
41    }
42}
43
44/// A log object's Log_Buffer as ReadRange pages it: the resident records,
45/// oldest first, each framed on demand as its Clause 21 record production.
46///
47/// The records align element for element with
48/// [`crate::traits::BACnetObject::log_record_identities_internal`]. A page
49/// encodes only the records it visits, so a narrow window over a full log
50/// stays cheap.
51pub trait LogBufferRecords {
52    /// The number of resident records.
53    fn record_count(&self) -> usize;
54
55    /// Append the record at `index` (0 is the oldest) to `buf`.
56    ///
57    /// Encoding cannot fail: the built-in logs refuse a record that would not
58    /// encode when it is added, and an implementation must keep that promise
59    /// too. Panics when `index` is not below
60    /// [`record_count`](Self::record_count).
61    fn encode_record(&self, index: usize, buf: &mut BytesMut);
62}
63
64/// The largest encoded value, in octets, that the trend pollers log as an
65/// any-value. A larger value (a long string, a big array) is logged as a
66/// PROPERTY / VALUE_TOO_LONG failure instead, so that one Trend Log record
67/// stays small enough for a ReadRange page on a 480-octet APDU.
68pub const ANY_VALUE_MAX_OCTETS: usize = 256;
69
70/// Stable object-owned identity for one resident log record.
71///
72/// Identity views returned by [`crate::traits::BACnetObject::log_record_identities_internal`]
73/// are ordered oldest-to-newest and align element-for-element with the owning
74/// object's resident records and `LOG_BUFFER` projection. Sequence numbers are
75/// always nonzero. Event and Trend logs count in Unsigned32 and wrap from
76/// `u32::MAX` to 1; Audit Log counts in Unsigned64 and wraps from `u64::MAX`.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct LogRecordIdentity {
79    sequence_number: u64,
80    date: Date,
81    time: Time,
82}
83
84impl LogRecordIdentity {
85    /// Construct an identity, rejecting zero as an invalid record sequence.
86    pub fn new(sequence_number: u64, date: Date, time: Time) -> Option<Self> {
87        (sequence_number != 0).then_some(Self {
88            sequence_number,
89            date,
90            time,
91        })
92    }
93
94    /// Return this record's nonzero sequence number.
95    pub fn sequence_number(&self) -> u64 {
96        self.sequence_number
97    }
98
99    /// Return the date owned by this record.
100    pub fn date(&self) -> Date {
101        self.date
102    }
103
104    /// Return the time owned by this record.
105    pub fn time(&self) -> Time {
106        self.time
107    }
108}
109
110/// A record a [`LogRecordBuffer`] can hold: it carries its own timestamp,
111/// the shared lifecycle can build the log-status record of its family, and
112/// it encodes as its family's Clause 21 production.
113pub(crate) trait ResidentLogRecord: Clone {
114    /// The local date and time the record was acquired.
115    fn timestamp(&self) -> (Date, Time);
116    /// A log-status record carrying `status`.
117    fn log_status(date: Date, time: Time, status: LogStatus) -> Self;
118    /// Append the record's wire form to `buf`, leaving it unchanged on error.
119    fn encode(&self, buf: &mut BytesMut) -> Result<(), Error>;
120}
121
122impl ResidentLogRecord for BACnetLogRecord {
123    fn timestamp(&self) -> (Date, Time) {
124        (self.date, self.time)
125    }
126
127    fn log_status(date: Date, time: Time, status: LogStatus) -> Self {
128        Self {
129            date,
130            time,
131            log_datum: LogDatum::LogStatus(status),
132            status_flags: None,
133        }
134    }
135
136    fn encode(&self, buf: &mut BytesMut) -> Result<(), Error> {
137        encode_log_record(self, buf)
138    }
139}
140
141impl ResidentLogRecord for BACnetEventLogRecord {
142    fn timestamp(&self) -> (Date, Time) {
143        (self.date, self.time)
144    }
145
146    fn log_status(date: Date, time: Time, status: LogStatus) -> Self {
147        Self {
148            date,
149            time,
150            log_datum: EventLogDatum::LogStatus(status),
151        }
152    }
153
154    fn encode(&self, buf: &mut BytesMut) -> Result<(), Error> {
155        encode_event_log_record(self, buf)
156    }
157}
158
159impl ResidentLogRecord for BACnetLogMultipleRecord {
160    fn timestamp(&self) -> (Date, Time) {
161        (self.date, self.time)
162    }
163
164    fn log_status(date: Date, time: Time, status: LogStatus) -> Self {
165        Self {
166            date,
167            time,
168            log_data: LogData::LogStatus(status),
169        }
170    }
171
172    fn encode(&self, buf: &mut BytesMut) -> Result<(), Error> {
173        encode_log_multiple_record(self, buf)
174    }
175}
176
177#[derive(Clone)]
178pub(crate) struct LogRecordBuffer<R = BACnetLogRecord> {
179    capacity: u32,
180    records: VecDeque<R>,
181    total_record_count: u32,
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185pub(crate) enum OrdinaryAdmission {
186    IgnoredDisabled,
187    Inserted,
188    CountOnly,
189    StopBeforeFull,
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193pub(crate) enum ForcedAdmission {
194    Inserted,
195    CountOnly,
196}
197
198impl<R: ResidentLogRecord> LogRecordBuffer<R> {
199    pub(crate) fn new(capacity: u32) -> Self {
200        Self {
201            capacity,
202            records: VecDeque::new(),
203            total_record_count: 0,
204        }
205    }
206
207    pub(crate) fn admit_ordinary(
208        &mut self,
209        record: R,
210        enabled: bool,
211        stop_when_full: bool,
212    ) -> OrdinaryAdmission {
213        if !enabled {
214            return OrdinaryAdmission::IgnoredDisabled;
215        }
216        if stop_when_full && self.next_record_would_fill() {
217            return OrdinaryAdmission::StopBeforeFull;
218        }
219
220        match self.insert_counted(record) {
221            ForcedAdmission::Inserted => OrdinaryAdmission::Inserted,
222            ForcedAdmission::CountOnly => OrdinaryAdmission::CountOnly,
223        }
224    }
225
226    pub(crate) fn insert_forced(&mut self, record: R) -> ForcedAdmission {
227        self.insert_counted(record)
228    }
229
230    pub(crate) fn is_full(&self) -> bool {
231        self.records.len() >= self.capacity as usize
232    }
233
234    pub(crate) fn next_record_would_fill_positive_capacity(&self) -> bool {
235        self.capacity > 0 && self.next_record_would_fill()
236    }
237
238    pub(crate) fn records(&self) -> &VecDeque<R> {
239        &self.records
240    }
241
242    pub(crate) fn total_record_count(&self) -> u32 {
243        self.total_record_count
244    }
245
246    pub(crate) fn capacity(&self) -> u32 {
247        self.capacity
248    }
249
250    pub(crate) fn clear(&mut self) {
251        self.records.clear();
252    }
253
254    pub(crate) fn identities(&self) -> Vec<LogRecordIdentity> {
255        if self.records.is_empty() {
256            return Vec::new();
257        }
258
259        debug_assert_ne!(self.total_record_count, 0);
260        let mut sequence_number = self.total_record_count;
261        for _ in 1..self.records.len() {
262            sequence_number = previous_sequence(sequence_number);
263        }
264
265        self.records
266            .iter()
267            .map(|record| {
268                let (date, time) = record.timestamp();
269                let identity = LogRecordIdentity {
270                    sequence_number: u64::from(sequence_number),
271                    date,
272                    time,
273                };
274                sequence_number = next_sequence(sequence_number);
275                identity
276            })
277            .collect()
278    }
279
280    fn next_record_would_fill(&self) -> bool {
281        self.capacity == 0 || self.records.len().saturating_add(1) >= self.capacity as usize
282    }
283
284    fn insert_counted(&mut self, record: R) -> ForcedAdmission {
285        self.total_record_count = next_sequence(self.total_record_count);
286        if self.capacity == 0 {
287            return ForcedAdmission::CountOnly;
288        }
289        if self.is_full() {
290            self.records.pop_front();
291        }
292        self.records.push_back(record);
293        ForcedAdmission::Inserted
294    }
295
296    #[cfg(test)]
297    pub(crate) fn set_total_record_count_for_test(&mut self, total_record_count: u32) {
298        debug_assert!(self.records.is_empty());
299        self.total_record_count = total_record_count;
300    }
301}
302
303impl<R: ResidentLogRecord> LogBufferRecords for LogRecordBuffer<R> {
304    fn record_count(&self) -> usize {
305        self.records.len()
306    }
307
308    fn encode_record(&self, index: usize, buf: &mut BytesMut) {
309        // `LogLifecycle::try_add_ordinary` refuses a record that would not
310        // encode, the lifecycle's own status records always encode, and a
311        // resident record is never changed.
312        self.records[index]
313            .encode(buf)
314            .expect("every resident log record encodes");
315    }
316}
317
318fn next_sequence(sequence_number: u32) -> u32 {
319    if sequence_number == u32::MAX {
320        1
321    } else {
322        sequence_number + 1
323    }
324}
325
326fn previous_sequence(sequence_number: u32) -> u32 {
327    if sequence_number == 1 {
328        u32::MAX
329    } else {
330        sequence_number - 1
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337    use bacnet_types::constructed::{BACnetLogRecord, LogDatum};
338    use bacnet_types::primitives::{Date, Time};
339
340    fn record(hour: u8) -> BACnetLogRecord {
341        BACnetLogRecord {
342            date: Date {
343                year: 126,
344                month: 8,
345                day: 31,
346                day_of_week: 1,
347            },
348            time: Time {
349                hour,
350                minute: 0,
351                second: 0,
352                hundredths: 0,
353            },
354            log_datum: LogDatum::UnsignedValue(hour as u64),
355            status_flags: None,
356        }
357    }
358
359    #[test]
360    fn log_buffer_assigns_one_and_wraps_max_without_zero() {
361        let mut buffer = LogRecordBuffer::new(2);
362        assert_eq!(buffer.total_record_count(), 0);
363        assert!(LogRecordIdentity::new(0, record(0).date, record(0).time).is_none());
364
365        assert_eq!(
366            buffer.admit_ordinary(record(1), true, false),
367            OrdinaryAdmission::Inserted
368        );
369        assert_eq!(buffer.identities()[0].sequence_number(), 1);
370
371        buffer.clear();
372        buffer.set_total_record_count_for_test(u32::MAX);
373        assert_eq!(
374            buffer.admit_ordinary(record(2), true, false),
375            OrdinaryAdmission::Inserted
376        );
377        assert_eq!(buffer.total_record_count(), 1);
378        assert_eq!(buffer.identities()[0].sequence_number(), 1);
379    }
380
381    #[test]
382    fn log_buffer_rejections_do_not_consume_sequence() {
383        let mut buffer = LogRecordBuffer::new(1);
384        assert_eq!(
385            buffer.admit_ordinary(record(1), false, false),
386            OrdinaryAdmission::IgnoredDisabled
387        );
388        assert_eq!(buffer.total_record_count(), 0);
389
390        assert_eq!(
391            buffer.admit_ordinary(record(2), true, true),
392            OrdinaryAdmission::StopBeforeFull
393        );
394        assert_eq!(buffer.total_record_count(), 0);
395        assert!(buffer.identities().is_empty());
396    }
397
398    #[test]
399    fn log_buffer_zero_capacity_counts_without_retaining() {
400        let mut ring = LogRecordBuffer::new(0);
401        assert_eq!(
402            ring.admit_ordinary(record(1), true, false),
403            OrdinaryAdmission::CountOnly
404        );
405        assert_eq!(
406            ring.admit_ordinary(record(2), true, false),
407            OrdinaryAdmission::CountOnly
408        );
409        assert!(ring.records().is_empty());
410        assert_eq!(ring.total_record_count(), 2);
411
412        let mut stop_when_full = LogRecordBuffer::new(0);
413        assert_eq!(
414            stop_when_full.admit_ordinary(record(1), true, true),
415            OrdinaryAdmission::StopBeforeFull
416        );
417        assert!(stop_when_full.records().is_empty());
418        assert_eq!(stop_when_full.total_record_count(), 0);
419    }
420
421    #[test]
422    fn log_buffer_fifo_eviction_keeps_survivor_identity_and_alignment() {
423        let mut buffer = LogRecordBuffer::new(2);
424        assert_eq!(
425            buffer.admit_ordinary(record(1), true, false),
426            OrdinaryAdmission::Inserted
427        );
428        assert_eq!(
429            buffer.admit_ordinary(record(2), true, false),
430            OrdinaryAdmission::Inserted
431        );
432        let survivor = buffer.identities()[1];
433        assert_eq!(
434            buffer.admit_ordinary(record(3), true, false),
435            OrdinaryAdmission::Inserted
436        );
437
438        let identities = buffer.identities();
439        assert_eq!(
440            identities,
441            vec![
442                survivor,
443                LogRecordIdentity::new(3, record(3).date, record(3).time).unwrap()
444            ]
445        );
446        assert_eq!(identities[0].sequence_number(), 2);
447        assert_ne!(identities[0].sequence_number(), 1);
448        for (record, identity) in buffer.records().iter().zip(&identities) {
449            assert_eq!(identity.date(), record.date);
450            assert_eq!(identity.time(), record.time);
451        }
452    }
453
454    #[test]
455    fn log_buffer_wrap_and_eviction_keep_modular_fifo_alignment() {
456        let mut buffer = LogRecordBuffer::new(3);
457        buffer.set_total_record_count_for_test(u32::MAX - 1);
458        assert_eq!(
459            buffer.admit_ordinary(record(1), true, false),
460            OrdinaryAdmission::Inserted
461        );
462        assert_eq!(
463            buffer.admit_ordinary(record(2), true, false),
464            OrdinaryAdmission::Inserted
465        );
466        assert_eq!(
467            buffer.admit_ordinary(record(3), true, false),
468            OrdinaryAdmission::Inserted
469        );
470        assert_eq!(
471            buffer
472                .identities()
473                .iter()
474                .map(LogRecordIdentity::sequence_number)
475                .collect::<Vec<_>>(),
476            vec![u64::from(u32::MAX), 1, 2]
477        );
478
479        assert_eq!(
480            buffer.admit_ordinary(record(4), true, false),
481            OrdinaryAdmission::Inserted
482        );
483        assert_eq!(
484            buffer
485                .identities()
486                .iter()
487                .map(LogRecordIdentity::sequence_number)
488                .collect::<Vec<_>>(),
489            vec![1, 2, 3]
490        );
491        assert_eq!(
492            buffer
493                .records()
494                .iter()
495                .map(|record| record.time.hour)
496                .collect::<Vec<_>>(),
497            vec![2, 3, 4]
498        );
499    }
500
501    #[test]
502    fn log_buffer_clear_preserves_counter_and_constructor_resets_it() {
503        let mut buffer = LogRecordBuffer::new(2);
504        assert_eq!(
505            buffer.admit_ordinary(record(1), true, false),
506            OrdinaryAdmission::Inserted
507        );
508        assert_eq!(
509            buffer.admit_ordinary(record(2), true, false),
510            OrdinaryAdmission::Inserted
511        );
512        buffer.clear();
513
514        assert!(buffer.records().is_empty());
515        assert!(buffer.identities().is_empty());
516        assert_eq!(buffer.total_record_count(), 2);
517        assert_eq!(
518            buffer.admit_ordinary(record(3), true, false),
519            OrdinaryAdmission::Inserted
520        );
521        assert_eq!(buffer.identities()[0].sequence_number(), 3);
522        assert_eq!(
523            LogRecordBuffer::<BACnetLogRecord>::new(2).total_record_count(),
524            0
525        );
526    }
527
528    #[test]
529    fn log_buffer_duplicate_timestamps_keep_distinct_sequences() {
530        let mut buffer = LogRecordBuffer::new(2);
531        let duplicate = record(4);
532        assert_eq!(
533            buffer.admit_ordinary(duplicate.clone(), true, false),
534            OrdinaryAdmission::Inserted
535        );
536        assert_eq!(
537            buffer.admit_ordinary(duplicate, true, false),
538            OrdinaryAdmission::Inserted
539        );
540
541        let identities = buffer.identities();
542        assert_eq!(identities[0].date(), identities[1].date());
543        assert_eq!(identities[0].time(), identities[1].time());
544        assert_eq!(identities[0].sequence_number(), 1);
545        assert_eq!(identities[1].sequence_number(), 2);
546    }
547
548    #[test]
549    fn log_buffer_clone_restore_preserves_payloads_and_derived_identities() {
550        let mut buffer = LogRecordBuffer::new(2);
551        assert_eq!(
552            buffer.admit_ordinary(record(1), true, false),
553            OrdinaryAdmission::Inserted
554        );
555        assert_eq!(
556            buffer.admit_ordinary(record(2), true, false),
557            OrdinaryAdmission::Inserted
558        );
559        let records = buffer.records().clone();
560        let identities = buffer.identities();
561        let total = buffer.total_record_count();
562
563        let snapshot = buffer.clone();
564        buffer.clear();
565        buffer = snapshot;
566
567        assert_eq!(buffer.records(), &records);
568        assert_eq!(buffer.identities(), identities);
569        assert_eq!(buffer.total_record_count(), total);
570    }
571}