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