Skip to main content

bacnet_encoding/constructed/
event_parameter.rs

1//! `BACnetEventParameter` full ASN.1 framing.
2//!
3//! `BACnetEventParameter` (ASHRAE 135-2020 Clause 21) selects one event
4//! algorithm by context tag and carries that algorithm's settings. Each
5//! alternative that [`BACnetEventParameter`] models is a constructed value
6//! under its own tag, holding context-tagged members that are all required:
7//!
8//! | Tag | Alternative | Member tag | Member | Type |
9//! |---|---|---|---|---|
10//! | `[0]` | `change-of-bitstring` | `[0]` | `time-delay` | Unsigned |
11//! | | | `[1]` | `bitmask` | bit string |
12//! | | | `[2]` | `list-of-bitstring-values` | list of bit strings |
13//! | `[1]` | `change-of-state` | `[0]` | `time-delay` | Unsigned |
14//! | | | `[1]` | `list-of-values` | list of `BACnetPropertyStates` |
15//! | `[2]` | `change-of-value` | `[0]` | `time-delay` | Unsigned |
16//! | | | `[1]` | `cov-criteria` | `[0]` bit string or `[1]` REAL, see below |
17//! | `[4]` | `floating-limit` | `[0]` | `time-delay` | Unsigned |
18//! | | | `[1]` | `setpoint-reference` | `BACnetDeviceObjectPropertyReference` |
19//! | | | `[2]` | `low-diff-limit` | REAL |
20//! | | | `[3]` | `high-diff-limit` | REAL |
21//! | | | `[4]` | `deadband` | REAL |
22//! | `[5]` | `out-of-range` | `[0]` | `time-delay` | Unsigned |
23//! | | | `[1]` | `low-limit` | REAL |
24//! | | | `[2]` | `high-limit` | REAL |
25//! | | | `[3]` | `deadband` | REAL |
26//! | `[9]` | `extended` | `[0]` | `vendor-id` | Unsigned16 |
27//! | | | `[1]` | `extended-event-type` | Unsigned |
28//! | | | `[2]` | `parameters` | list of extended values, see below |
29//!
30//! `cov-criteria` is either `[0]` `bitmask` (a bit string) or `[1]`
31//! `referenced-property-increment` (a REAL). Each extended `parameters` entry
32//! is an application-tagged primitive or a `[0]`-framed
33//! `BACnetDeviceObjectPropertyReference`.
34//!
35//! Four tags carry no alternative here. Tag 6 is skipped because it would
36//! mirror the proprietary complex-event-type notification alternative, and
37//! tag 19 is skipped because change-of-reliability has no event parameters.
38//! Tag 7 is deprecated and tag 12 is reserved.
39//!
40//! Alternatives with no Rust model (`command-failure [3]`,
41//! `change-of-life-safety [8]`, `buffer-ready [10]`, `unsigned-range [11]`,
42//! `access-event [13]`, `*-out-of-range [14..=16]`, `change-of-characterstring
43//! [17]`, `change-of-status-flags [18]`, `none [20] NULL`,
44//! `change-of-discrete-value [21]`, `change-of-timer [22]`, vendor tags) are
45//! preserved byte-for-byte through [`BACnetEventParameter::Opaque`]. The
46//! omitted/deprecated/reserved slots 6, 7, 12 and 19 are rejected on decode.
47
48use bacnet_types::constructed::{BACnetEventParameter, ChangeOfValueCriteria};
49use bacnet_types::error::Error;
50use bytes::BytesMut;
51
52use crate::primitives;
53use crate::tags;
54
55use super::tagged::{
56    contents, decode_app_bit_string, decode_ctx_bit_string, decode_ctx_constructed,
57    decode_ctx_real, decode_ctx_unsigned, expect_closing, expect_opening, next_is_closing,
58};
59use super::{
60    decode_dopr_body, decode_property_state, encode_dopr_body, encode_property_state,
61    validate_extended_parameters, validate_tlv_sequence, MAX_FRAMED_ITEMS,
62};
63
64/// Context tags the Clause 21 production omits, deprecates, or reserves.
65/// Decoding one means the peer is speaking something other than the 135-2020
66/// production, so these are hard errors rather than Opaque shadows.
67const RESERVED_EVENT_TAGS: [u8; 4] = [6, 7, 12, 19];
68const MODELED_EVENT_TAGS: [u8; 6] = [0, 1, 2, 4, 5, 9];
69
70/// Encode a [`BACnetEventParameter`] as its full CHOICE framing.
71///
72/// Validation includes the complete framing depth. Errors leave `buf`
73/// unchanged.
74pub fn encode_event_parameter(
75    buf: &mut BytesMut,
76    value: &BACnetEventParameter,
77) -> Result<(), Error> {
78    let mut encoded = BytesMut::new();
79    encode_event_parameter_into(&mut encoded, value)?;
80    validate_tlv_sequence(&encoded, "BACnetEventParameter")
81        .map_err(|error| Error::Encoding(error.to_string()))?;
82    buf.extend_from_slice(&encoded);
83    Ok(())
84}
85
86fn encode_event_parameter_into(
87    buf: &mut BytesMut,
88    value: &BACnetEventParameter,
89) -> Result<(), Error> {
90    match value {
91        BACnetEventParameter::ChangeOfBitstring {
92            time_delay,
93            bitmask,
94            list_of_values,
95        } => {
96            tags::encode_opening_tag(buf, 0);
97            primitives::encode_ctx_unsigned(buf, 0, *time_delay as u64);
98            primitives::encode_ctx_bit_string(buf, 1, bitmask.0, &bitmask.1);
99            tags::encode_opening_tag(buf, 2);
100            for (unused_bits, data) in list_of_values {
101                primitives::encode_app_bit_string(buf, *unused_bits, data);
102            }
103            tags::encode_closing_tag(buf, 2);
104            tags::encode_closing_tag(buf, 0);
105        }
106        BACnetEventParameter::ChangeOfState {
107            time_delay,
108            list_of_values,
109        } => {
110            let mut encoded_states = BytesMut::new();
111            for state in list_of_values {
112                encode_property_state(&mut encoded_states, state)?;
113            }
114            tags::encode_opening_tag(buf, 1);
115            primitives::encode_ctx_unsigned(buf, 0, *time_delay as u64);
116            tags::encode_opening_tag(buf, 1);
117            buf.extend_from_slice(&encoded_states);
118            tags::encode_closing_tag(buf, 1);
119            tags::encode_closing_tag(buf, 1);
120        }
121        BACnetEventParameter::ChangeOfValue {
122            time_delay,
123            criteria,
124        } => {
125            tags::encode_opening_tag(buf, 2);
126            primitives::encode_ctx_unsigned(buf, 0, *time_delay as u64);
127            // cov-criteria [1] CHOICE — a tagged CHOICE is always explicitly
128            // tagged: opening/closing [1] around the alternative's own tag.
129            tags::encode_opening_tag(buf, 1);
130            match criteria {
131                ChangeOfValueCriteria::Bitmask { unused_bits, data } => {
132                    primitives::encode_ctx_bit_string(buf, 0, *unused_bits, data);
133                }
134                ChangeOfValueCriteria::ReferencedPropertyIncrement(increment) => {
135                    primitives::encode_ctx_real(buf, 1, *increment);
136                }
137            }
138            tags::encode_closing_tag(buf, 1);
139            tags::encode_closing_tag(buf, 2);
140        }
141        BACnetEventParameter::FloatingLimit {
142            time_delay,
143            setpoint_reference,
144            low_diff_limit,
145            high_diff_limit,
146            deadband,
147        } => {
148            tags::encode_opening_tag(buf, 4);
149            primitives::encode_ctx_unsigned(buf, 0, *time_delay as u64);
150            // setpoint-reference [1] BACnetDeviceObjectPropertyReference (SEQUENCE)
151            tags::encode_opening_tag(buf, 1);
152            encode_dopr_body(buf, setpoint_reference);
153            tags::encode_closing_tag(buf, 1);
154            primitives::encode_ctx_real(buf, 2, *low_diff_limit);
155            primitives::encode_ctx_real(buf, 3, *high_diff_limit);
156            primitives::encode_ctx_real(buf, 4, *deadband);
157            tags::encode_closing_tag(buf, 4);
158        }
159        BACnetEventParameter::OutOfRange {
160            time_delay,
161            low_limit,
162            high_limit,
163            deadband,
164        } => {
165            tags::encode_opening_tag(buf, 5);
166            primitives::encode_ctx_unsigned(buf, 0, *time_delay as u64);
167            primitives::encode_ctx_real(buf, 1, *low_limit);
168            primitives::encode_ctx_real(buf, 2, *high_limit);
169            primitives::encode_ctx_real(buf, 3, *deadband);
170            tags::encode_closing_tag(buf, 5);
171        }
172        BACnetEventParameter::Extended {
173            vendor_id,
174            extended_event_type,
175            parameters,
176        } => {
177            validate_extended_parameters(parameters, "extended")
178                .map_err(|error| Error::Encoding(error.to_string()))?;
179            tags::encode_opening_tag(buf, 9);
180            primitives::encode_ctx_unsigned(buf, 0, *vendor_id as u64);
181            primitives::encode_ctx_unsigned(buf, 1, *extended_event_type as u64);
182            // parameters [2] SEQUENCE OF CHOICE — held as raw pre-encoded items.
183            tags::encode_opening_tag(buf, 2);
184            buf.extend_from_slice(parameters);
185            tags::encode_closing_tag(buf, 2);
186            tags::encode_closing_tag(buf, 9);
187        }
188        BACnetEventParameter::Opaque { tag, data } if *tag == u8::MAX => {
189            // Legacy pre-framing EventEnrollment values used an Octet String.
190            primitives::encode_app_octet_string(buf, data);
191        }
192        BACnetEventParameter::Opaque { tag: 20, data } if data.is_empty() => {
193            // Clause 21 defines none [20] as context NULL, not an empty SEQUENCE.
194            tags::encode_tag(buf, 20, tags::TagClass::Context, 0);
195        }
196        BACnetEventParameter::Opaque { tag, .. }
197            if *tag == 20
198                || MODELED_EVENT_TAGS.contains(tag)
199                || RESERVED_EVENT_TAGS.contains(tag) =>
200        {
201            return Err(Error::Encoding(format!(
202                "BACnetEventParameter: tag [{tag}] cannot use an opaque constructed body"
203            )));
204        }
205        BACnetEventParameter::Opaque { tag, data } => {
206            validate_tlv_sequence(data, "opaque event parameters")
207                .map_err(|error| Error::Encoding(error.to_string()))?;
208            // Preserved alternative: its captured bytes are the complete
209            // SEQUENCE body, re-emitted under its own opening/closing pair.
210            tags::encode_opening_tag(buf, *tag);
211            buf.extend_from_slice(data);
212            tags::encode_closing_tag(buf, *tag);
213        }
214    }
215    Ok(())
216}
217
218/// Decode one framed [`BACnetEventParameter`] CHOICE alternative at `offset`.
219///
220/// Returns the value and the offset past its encoding. Unmodeled (but valid)
221/// alternatives are preserved as [`BACnetEventParameter::Opaque`]; the
222/// omitted/deprecated/reserved tags 6, 7, 12, 19 are rejected.
223pub fn decode_event_parameter(
224    data: &[u8],
225    offset: usize,
226) -> Result<(BACnetEventParameter, usize), Error> {
227    use BACnetEventParameter as EP;
228
229    // Pre-framing EventEnrollment writes use [255] as an internal wrapper.
230    // Keep this shipped compatibility form local so the generic tag parser
231    // can reject ASHRAE-reserved tag number 255 everywhere else.
232    if data.get(offset..offset.saturating_add(2)) == Some(&[0xFE, 0xFF]) {
233        let body_start = offset + 2;
234        // The wrapper needs its closing pair after the opening one.
235        if data.len() < body_start + 2 {
236            return Err(Error::buffer_too_short(body_start + 2, data.len()));
237        }
238        let body_end = data.len() - 2;
239        if data.get(body_end..) != Some(&[0xFF, 0xFF]) {
240            return Err(Error::decoding(
241                offset,
242                "BACnetEventParameter: missing legacy closing tag",
243            ));
244        }
245        return Ok((
246            EP::Opaque {
247                tag: u8::MAX,
248                data: data[body_start..body_end].to_vec(),
249            },
250            data.len(),
251        ));
252    }
253
254    let (tag, pos) = tags::decode_tag(data, offset)?;
255    let what = "BACnetEventParameter";
256
257    if tag.class == tags::TagClass::Application && tag.number == tags::app_tag::OCTET_STRING {
258        let (octets, end) = contents(data, pos, tag.length)?;
259        return Ok((
260            EP::Opaque {
261                tag: u8::MAX,
262                data: octets.to_vec(),
263            },
264            end,
265        ));
266    }
267
268    if tag.class != tags::TagClass::Context {
269        return Err(Error::decoding(
270            offset,
271            "BACnetEventParameter: expected a context tag",
272        ));
273    }
274
275    if RESERVED_EVENT_TAGS.contains(&tag.number) {
276        return Err(Error::decoding(
277            offset,
278            format!(
279                "BACnetEventParameter: context tag [{}] is omitted/deprecated/reserved in Clause 21",
280                tag.number
281            ),
282        ));
283    }
284
285    // `none [20] NULL` is the only primitive alternative in this CHOICE.
286    if !tag.is_opening && !tag.is_closing {
287        if tag.number == 20 && tag.length == 0 {
288            return Ok((
289                EP::Opaque {
290                    tag: 20,
291                    data: Vec::new(),
292                },
293                pos,
294            ));
295        }
296        return Err(Error::decoding(
297            offset,
298            "BACnetEventParameter: invalid primitive alternative",
299        ));
300    }
301
302    if !tag.is_opening {
303        return Err(Error::decoding(
304            offset,
305            "BACnetEventParameter: unexpected closing tag",
306        ));
307    }
308    if tag.number == 20 {
309        return Err(Error::decoding(
310            offset,
311            "BACnetEventParameter: none [20] must use primitive NULL form",
312        ));
313    }
314
315    let value = match tag.number {
316        0 => {
317            let what = "BACnetEventParameter change-of-bitstring";
318            let (time_delay, pos) = decode_ctx_unsigned::<u32>(data, pos, 0, what)?;
319            let (bitmask, pos) = decode_ctx_bit_string(data, pos, 1, what)?;
320            let mut pos = expect_opening(data, pos, 2, what)?;
321            let mut list_of_values = Vec::new();
322            while !next_is_closing(data, pos, 2)? {
323                if list_of_values.len() >= MAX_FRAMED_ITEMS {
324                    return Err(Error::overflow(
325                        pos,
326                        "change-of-bitstring: list-of-bitstring-values exceeds limit",
327                    ));
328                }
329                let (bits, p) = decode_app_bit_string(data, pos, what)?;
330                list_of_values.push(bits);
331                pos = p;
332            }
333            pos = expect_closing(data, pos, 2, what)?;
334            pos = expect_closing(data, pos, 0, what)?;
335            (
336                EP::ChangeOfBitstring {
337                    time_delay,
338                    bitmask,
339                    list_of_values,
340                },
341                pos,
342            )
343        }
344        1 => {
345            let what = "BACnetEventParameter change-of-state";
346            let (time_delay, pos) = decode_ctx_unsigned::<u32>(data, pos, 0, what)?;
347            let mut pos = expect_opening(data, pos, 1, what)?;
348            let mut list_of_values = Vec::new();
349            while !next_is_closing(data, pos, 1)? {
350                if list_of_values.len() >= MAX_FRAMED_ITEMS {
351                    return Err(Error::overflow(
352                        pos,
353                        "change-of-state: list-of-values exceeds limit",
354                    ));
355                }
356                let (state, p) = decode_property_state(data, pos)?;
357                list_of_values.push(state);
358                pos = p;
359            }
360            pos = expect_closing(data, pos, 1, what)?;
361            pos = expect_closing(data, pos, 1, what)?;
362            (
363                EP::ChangeOfState {
364                    time_delay,
365                    list_of_values,
366                },
367                pos,
368            )
369        }
370        2 => {
371            let what = "BACnetEventParameter change-of-value";
372            let (time_delay, pos) = decode_ctx_unsigned::<u32>(data, pos, 0, what)?;
373            let mut pos = expect_opening(data, pos, 1, what)?;
374            let (inner, _) = tags::decode_tag(data, pos)?;
375            let criteria = if inner.is_context(0) {
376                let (bits, p) = decode_ctx_bit_string(data, pos, 0, what)?;
377                pos = p;
378                ChangeOfValueCriteria::Bitmask {
379                    unused_bits: bits.0,
380                    data: bits.1,
381                }
382            } else if inner.is_context(1) {
383                let (increment, p) = decode_ctx_real(data, pos, 1, what)?;
384                pos = p;
385                ChangeOfValueCriteria::ReferencedPropertyIncrement(increment)
386            } else {
387                return Err(Error::decoding(
388                    pos,
389                    format!(
390                        "change-of-value: unexpected cov-criteria tag [{}]",
391                        inner.number
392                    ),
393                ));
394            };
395            pos = expect_closing(data, pos, 1, what)?;
396            pos = expect_closing(data, pos, 2, what)?;
397            (
398                EP::ChangeOfValue {
399                    time_delay,
400                    criteria,
401                },
402                pos,
403            )
404        }
405        4 => {
406            let what = "BACnetEventParameter floating-limit";
407            let (time_delay, pos) = decode_ctx_unsigned::<u32>(data, pos, 0, what)?;
408            let pos = expect_opening(data, pos, 1, what)?;
409            let (setpoint_reference, pos) = decode_dopr_body(data, pos, what)?;
410            let pos = expect_closing(data, pos, 1, what)?;
411            let (low_diff_limit, pos) = decode_ctx_real(data, pos, 2, what)?;
412            let (high_diff_limit, pos) = decode_ctx_real(data, pos, 3, what)?;
413            let (deadband, pos) = decode_ctx_real(data, pos, 4, what)?;
414            let pos = expect_closing(data, pos, 4, what)?;
415            (
416                EP::FloatingLimit {
417                    time_delay,
418                    setpoint_reference,
419                    low_diff_limit,
420                    high_diff_limit,
421                    deadband,
422                },
423                pos,
424            )
425        }
426        5 => {
427            let what = "BACnetEventParameter out-of-range";
428            let (time_delay, pos) = decode_ctx_unsigned::<u32>(data, pos, 0, what)?;
429            let (low_limit, pos) = decode_ctx_real(data, pos, 1, what)?;
430            let (high_limit, pos) = decode_ctx_real(data, pos, 2, what)?;
431            let (deadband, pos) = decode_ctx_real(data, pos, 3, what)?;
432            let pos = expect_closing(data, pos, 5, what)?;
433            (
434                EP::OutOfRange {
435                    time_delay,
436                    low_limit,
437                    high_limit,
438                    deadband,
439                },
440                pos,
441            )
442        }
443        9 => {
444            let what = "BACnetEventParameter extended";
445            let (vendor_id, pos) = decode_ctx_unsigned::<u16>(data, pos, 0, what)?;
446            let (extended_event_type, pos) = decode_ctx_unsigned::<u32>(data, pos, 1, what)?;
447            let (raw_params, pos) = decode_ctx_constructed(data, pos, 2, what)?;
448            validate_extended_parameters(raw_params, "extended")?;
449            let pos = expect_closing(data, pos, 9, what)?;
450            (
451                EP::Extended {
452                    vendor_id,
453                    extended_event_type,
454                    parameters: raw_params.to_vec(),
455                },
456                pos,
457            )
458        }
459        n => {
460            // Unmodeled constructed alternative: preserve the SEQUENCE body
461            // verbatim after validating its nested tag structure.
462            let (body, p) = tags::extract_context_value(data, pos, n)?;
463            validate_tlv_sequence(body, "opaque event parameters")?;
464            (
465                EP::Opaque {
466                    tag: n,
467                    data: body.to_vec(),
468                },
469                p,
470            )
471        }
472    };
473    let (value, end) = value;
474    validate_tlv_sequence(&data[offset..end], what)?;
475    Ok((value, end))
476}