Skip to main content

bacnet_encoding/constructed/
log_multiple_record.rs

1//! Trend Log Multiple log records (Clause 12.30.19), framed as Clause 21
2//! gives BACnetLogMultipleRecord.
3//!
4//! A record is two constructed fields. Context tag 0 wraps the timestamp, an
5//! application Date then an application Time. Context tag 1 wraps the log
6//! data, one of three alternatives: a primitive context 0 holding the
7//! three-bit log status, a constructed context 1 holding one entry per
8//! logged member, or a primitive context 2 holding the clock change as a
9//! REAL. Inside the member list each entry has its own context tag, 0 to 8
10//! in [`LogValue`] declaration order: the seven plain datatypes as
11//! primitives, then failure as a constructed pair of application
12//! Enumerated class and code, then any-value as a constructed wrapper
13//! around the value's own tagged encoding.
14
15#[cfg(not(feature = "std"))]
16use alloc::vec::Vec;
17
18use super::log_fields::{
19    check_bit_string, decode_any_value, decode_failure, decode_integer, decode_log_status,
20    decode_timestamp, encode_any_value, encode_ctx_integer, encode_failure, encode_log_status,
21    encode_timestamp,
22};
23use super::tagged::{contents, decode_ctx_constructed, expect_end};
24use crate::{primitives, tags};
25use bacnet_types::constructed::{BACnetLogMultipleRecord, LogData, LogValue};
26use bacnet_types::error::Error;
27use bytes::BytesMut;
28
29const RECORD: &str = "BACnetLogMultipleRecord";
30const TIMESTAMP: &str = "BACnetLogMultipleRecord timestamp";
31const LOG_DATA: &str = "BACnetLogMultipleRecord log-data";
32const FAILURE_FIELD: &str = "BACnetLogMultipleRecord failure";
33
34const BOOLEAN: u8 = 0;
35const REAL: u8 = 1;
36const ENUMERATED: u8 = 2;
37const UNSIGNED: u8 = 3;
38const INTEGER: u8 = 4;
39const BIT_STRING: u8 = 5;
40const NULL: u8 = 6;
41const FAILURE: u8 = 7;
42const ANY: u8 = 8;
43
44/// Encode one Trend Log Multiple record.
45///
46/// Fails, leaving `buf` unchanged, for a bit string whose unused-bit count
47/// is above 7 (or nonzero with no data) or an any-value whose bytes aren't
48/// complete tagged values.
49pub fn encode_log_multiple_record(
50    record: &BACnetLogMultipleRecord,
51    buf: &mut BytesMut,
52) -> Result<(), Error> {
53    let mut out = BytesMut::new();
54    encode_timestamp(&mut out, &record.date, &record.time);
55    tags::encode_opening_tag(&mut out, 1);
56    match &record.log_data {
57        LogData::LogStatus(status) => encode_log_status(&mut out, 0, *status),
58        LogData::Values(values) => {
59            tags::encode_opening_tag(&mut out, 1);
60            for value in values {
61                encode_value(value, &mut out)?;
62            }
63            tags::encode_closing_tag(&mut out, 1);
64        }
65        LogData::TimeChange(seconds) => primitives::encode_ctx_real(&mut out, 2, *seconds),
66    }
67    tags::encode_closing_tag(&mut out, 1);
68    buf.extend_from_slice(&out);
69    Ok(())
70}
71
72fn encode_value(value: &LogValue, buf: &mut BytesMut) -> Result<(), Error> {
73    match value {
74        LogValue::BooleanValue(value) => primitives::encode_ctx_boolean(buf, BOOLEAN, *value),
75        LogValue::RealValue(value) => primitives::encode_ctx_real(buf, REAL, *value),
76        LogValue::EnumValue(value) => primitives::encode_ctx_unsigned(buf, ENUMERATED, *value),
77        LogValue::UnsignedValue(value) => primitives::encode_ctx_unsigned(buf, UNSIGNED, *value),
78        LogValue::SignedValue(value) => encode_ctx_integer(buf, INTEGER, *value),
79        LogValue::BitstringValue { unused_bits, data } => {
80            check_bit_string(*unused_bits, data, "bitstring-value")?;
81            primitives::encode_ctx_bit_string(buf, BIT_STRING, *unused_bits, data);
82        }
83        LogValue::NullValue => tags::encode_tag(buf, NULL, tags::TagClass::Context, 0),
84        LogValue::Failure {
85            error_class,
86            error_code,
87        } => encode_failure(buf, FAILURE, *error_class, *error_code),
88        LogValue::AnyValue(bytes) => encode_any_value(buf, ANY, bytes)?,
89    }
90    Ok(())
91}
92
93/// Decode one Trend Log Multiple record starting at `offset`, returning it
94/// and the offset just past it.
95pub fn decode_log_multiple_record(
96    data: &[u8],
97    offset: usize,
98) -> Result<(BACnetLogMultipleRecord, usize), Error> {
99    let (date, time, data_start) = decode_timestamp(data, offset, TIMESTAMP)?;
100    let (body, end) = decode_ctx_constructed(data, data_start, 1, LOG_DATA)?;
101    let log_data = decode_log_data(body, data_start)?;
102    Ok((
103        BACnetLogMultipleRecord {
104            date,
105            time,
106            log_data,
107        },
108        end,
109    ))
110}
111
112fn decode_log_data(data: &[u8], offset: usize) -> Result<LogData, Error> {
113    let (tag, start) = tags::decode_tag(data, 0)?;
114    let (log_data, end) = if tag.is_context(0) {
115        let (octets, end) = contents(data, start, tag.length)?;
116        (
117            LogData::LogStatus(decode_log_status(octets, offset, RECORD)?),
118            end,
119        )
120    } else if tag.is_opening_tag(1) {
121        let (list, end) = tags::extract_context_value(data, start, 1)?;
122        (LogData::Values(decode_values(list, offset)?), end)
123    } else if tag.is_context(2) {
124        let (octets, end) = contents(data, start, tag.length)?;
125        (LogData::TimeChange(primitives::decode_real(octets)?), end)
126    } else {
127        return Err(Error::decoding(
128            offset,
129            "log-data expected context [0], constructed [1], or context [2]",
130        ));
131    };
132    expect_end(data, end, offset, LOG_DATA)?;
133    Ok(log_data)
134}
135
136fn decode_values(data: &[u8], offset: usize) -> Result<Vec<LogValue>, Error> {
137    let mut values = Vec::new();
138    let mut pos = 0;
139    while pos < data.len() {
140        let (tag, start) = tags::decode_tag(data, pos)?;
141        let (value, end) = if tag.is_opening_tag(FAILURE) {
142            let (body, end) = tags::extract_context_value(data, start, FAILURE)?;
143            let (error_class, error_code) = decode_failure(body, offset, FAILURE_FIELD)?;
144            let value = LogValue::Failure {
145                error_class,
146                error_code,
147            };
148            (value, end)
149        } else if tag.is_opening_tag(ANY) {
150            let (body, end) = tags::extract_context_value(data, start, ANY)?;
151            (LogValue::AnyValue(decode_any_value(body)?), end)
152        } else if tag.class == tags::TagClass::Context && !tag.is_opening && !tag.is_closing {
153            let (octets, end) = contents(data, start, tag.length)?;
154            (decode_primitive(tag.number, octets, offset)?, end)
155        } else {
156            return Err(Error::decoding(offset, "log-data entry has an unknown tag"));
157        };
158        values.push(value);
159        pos = end;
160    }
161    Ok(values)
162}
163
164fn decode_primitive(number: u8, contents: &[u8], offset: usize) -> Result<LogValue, Error> {
165    Ok(match number {
166        BOOLEAN => match contents {
167            [0] => LogValue::BooleanValue(false),
168            [1] => LogValue::BooleanValue(true),
169            _ => return Err(Error::decoding(offset, "boolean-value must be one octet")),
170        },
171        REAL => LogValue::RealValue(primitives::decode_real(contents)?),
172        ENUMERATED => LogValue::EnumValue(primitives::decode_unsigned(contents)?),
173        UNSIGNED => LogValue::UnsignedValue(primitives::decode_unsigned(contents)?),
174        INTEGER => LogValue::SignedValue(decode_integer(contents, offset)?),
175        BIT_STRING => {
176            let (unused_bits, data) = primitives::decode_bit_string(contents)?;
177            LogValue::BitstringValue { unused_bits, data }
178        }
179        NULL if contents.is_empty() => LogValue::NullValue,
180        _ => return Err(Error::decoding(offset, "log-data entry has an unknown tag")),
181    })
182}