Skip to main content

bacnet_encoding/constructed/
log_record.rs

1//! Trend Log log records (Clause 12.25.14), framed as Clause 21 gives
2//! BACnetLogRecord.
3//!
4//! A record is two constructed fields and an optional third. Context tag 0
5//! wraps the timestamp, an application Date then an application Time.
6//! Context tag 1 wraps the log datum, whose alternatives take context tags 0
7//! to 10 in [`LogDatum`] declaration order: the three-bit log status, the
8//! seven plain datatypes and the clock change as primitives, failure as a
9//! constructed pair of application Enumerated class and code, and any-value
10//! as a constructed wrapper around the value's own tagged encoding. A
11//! primitive context 2 holding the four-bit Status_Flags may follow.
12
13use super::log_fields::{
14    check_bit_string, decode_any_value, decode_failure, decode_integer, decode_log_status,
15    decode_timestamp, encode_any_value, encode_ctx_integer, encode_failure, encode_log_status,
16    encode_timestamp,
17};
18use super::tagged::{contents, decode_ctx_constructed, expect_end};
19use crate::{primitives, tags};
20use bacnet_types::bitstring::status_flags_from_bacnet;
21use bacnet_types::constructed::{BACnetLogRecord, LogDatum};
22use bacnet_types::error::Error;
23use bacnet_types::primitives::StatusFlags;
24use bytes::BytesMut;
25
26const RECORD: &str = "BACnetLogRecord";
27const TIMESTAMP: &str = "BACnetLogRecord timestamp";
28const DATUM: &str = "BACnetLogRecord log-datum";
29const FAILURE_FIELD: &str = "BACnetLogRecord failure";
30
31const LOG_STATUS: u8 = 0;
32const BOOLEAN: u8 = 1;
33const REAL: u8 = 2;
34const ENUMERATED: u8 = 3;
35const UNSIGNED: u8 = 4;
36const INTEGER: u8 = 5;
37const BIT_STRING: u8 = 6;
38const NULL: u8 = 7;
39const FAILURE: u8 = 8;
40const TIME_CHANGE: u8 = 9;
41const ANY: u8 = 10;
42
43/// The record field after the datum.
44const STATUS_FLAGS: u8 = 2;
45
46/// Encode one Trend Log record.
47///
48/// Fails, leaving `buf` unchanged, for a bit string whose unused-bit count
49/// is above 7 (or nonzero with no data) or an any-value whose bytes aren't
50/// complete tagged values.
51pub fn encode_log_record(record: &BACnetLogRecord, buf: &mut BytesMut) -> Result<(), Error> {
52    let mut out = BytesMut::new();
53    encode_timestamp(&mut out, &record.date, &record.time);
54    tags::encode_opening_tag(&mut out, 1);
55    match &record.log_datum {
56        LogDatum::LogStatus(status) => encode_log_status(&mut out, LOG_STATUS, *status),
57        LogDatum::BooleanValue(value) => primitives::encode_ctx_boolean(&mut out, BOOLEAN, *value),
58        LogDatum::RealValue(value) => primitives::encode_ctx_real(&mut out, REAL, *value),
59        // An ENUMERATED's contents are encoded as an Unsigned's are.
60        LogDatum::EnumValue(value) => primitives::encode_ctx_unsigned(&mut out, ENUMERATED, *value),
61        LogDatum::UnsignedValue(value) => {
62            primitives::encode_ctx_unsigned(&mut out, UNSIGNED, *value)
63        }
64        LogDatum::SignedValue(value) => encode_ctx_integer(&mut out, INTEGER, *value),
65        LogDatum::BitstringValue { unused_bits, data } => {
66            check_bit_string(
67                *unused_bits,
68                data,
69                "bitstring-value has {} unused bits over {} octets",
70            )?;
71            primitives::encode_ctx_bit_string(&mut out, BIT_STRING, *unused_bits, data);
72        }
73        LogDatum::NullValue => tags::encode_tag(&mut out, NULL, tags::TagClass::Context, 0),
74        LogDatum::Failure {
75            error_class,
76            error_code,
77        } => encode_failure(&mut out, FAILURE, *error_class, *error_code),
78        LogDatum::TimeChange(seconds) => {
79            primitives::encode_ctx_real(&mut out, TIME_CHANGE, *seconds)
80        }
81        LogDatum::AnyValue(bytes) => encode_any_value(&mut out, ANY, bytes)?,
82    }
83    tags::encode_closing_tag(&mut out, 1);
84    if let Some(flags) = record.status_flags {
85        // StatusFlags keeps in-alarm as its highest of four bits, so shifting
86        // puts it in the top bit of the octet, where the wire wants bit 0.
87        let octet = (flags & StatusFlags::all()).bits() << 4;
88        primitives::encode_ctx_bit_string(&mut out, STATUS_FLAGS, 4, &[octet]);
89    }
90    buf.extend_from_slice(&out);
91    Ok(())
92}
93
94/// Decode one Trend Log record starting at `offset`, returning it and the
95/// offset just past it.
96///
97/// Records sit back to back in a ReadRange item list, so the optional status
98/// flags are read only when the very next tag is the primitive context 2;
99/// anything else is left for the caller as the start of what follows.
100pub fn decode_log_record(data: &[u8], offset: usize) -> Result<(BACnetLogRecord, usize), Error> {
101    let (date, time, datum_start) = decode_timestamp(data, offset, TIMESTAMP)?;
102    let (body, mut end) = decode_ctx_constructed(data, datum_start, 1, DATUM)?;
103    let log_datum = decode_datum(body, datum_start)?;
104    let mut status_flags = None;
105    if end < data.len() {
106        let (tag, start) = tags::decode_tag(data, end)?;
107        if tag.is_context(STATUS_FLAGS) {
108            let (octets, next) = contents(data, start, tag.length)?;
109            status_flags = match octets {
110                [4, bits] if bits & 0x0f == 0 => Some(status_flags_from_bacnet(&[*bits])),
111                _ => {
112                    return Err(Error::decoding(
113                        end,
114                        "BACnetLogRecord status-flags must be a four-bit BitString",
115                    ))
116                }
117            };
118            end = next;
119        }
120    }
121    Ok((
122        BACnetLogRecord {
123            date,
124            time,
125            log_datum,
126            status_flags,
127        },
128        end,
129    ))
130}
131
132fn decode_datum(data: &[u8], offset: usize) -> Result<LogDatum, Error> {
133    let (tag, start) = tags::decode_tag(data, 0)?;
134    let (datum, end) = if tag.is_opening_tag(FAILURE) {
135        let (body, end) = tags::extract_context_value(data, start, FAILURE)?;
136        let (error_class, error_code) = decode_failure(body, offset, FAILURE_FIELD)?;
137        let datum = LogDatum::Failure {
138            error_class,
139            error_code,
140        };
141        (datum, end)
142    } else if tag.is_opening_tag(ANY) {
143        let (body, end) = tags::extract_context_value(data, start, ANY)?;
144        (LogDatum::AnyValue(decode_any_value(body)?), end)
145    } else if tag.class == tags::TagClass::Context && !tag.is_opening && !tag.is_closing {
146        let (octets, end) = contents(data, start, tag.length)?;
147        (decode_primitive(tag.number, octets, offset)?, end)
148    } else {
149        return Err(Error::decoding(offset, "log-datum has an unknown tag"));
150    };
151    expect_end(data, end, offset, DATUM)?;
152    Ok(datum)
153}
154
155fn decode_primitive(number: u8, contents: &[u8], offset: usize) -> Result<LogDatum, Error> {
156    Ok(match number {
157        LOG_STATUS => LogDatum::LogStatus(decode_log_status(contents, offset, RECORD)?),
158        BOOLEAN => match contents {
159            [0] => LogDatum::BooleanValue(false),
160            [1] => LogDatum::BooleanValue(true),
161            _ => return Err(Error::decoding(offset, "boolean-value must be one octet")),
162        },
163        REAL => LogDatum::RealValue(primitives::decode_real(contents)?),
164        ENUMERATED => LogDatum::EnumValue(primitives::decode_unsigned(contents)?),
165        UNSIGNED => LogDatum::UnsignedValue(primitives::decode_unsigned(contents)?),
166        INTEGER => LogDatum::SignedValue(decode_integer(contents, offset)?),
167        BIT_STRING => {
168            let (unused_bits, data) = primitives::decode_bit_string(contents)?;
169            LogDatum::BitstringValue { unused_bits, data }
170        }
171        NULL if contents.is_empty() => LogDatum::NullValue,
172        TIME_CHANGE => LogDatum::TimeChange(primitives::decode_real(contents)?),
173        _ => return Err(Error::decoding(offset, "log-datum has an unknown tag")),
174    })
175}