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::vec::Vec;
50
51use bacnet_types::constructed::{BACnetEventParameter, ChangeOfValueCriteria};
52use bacnet_types::error::{DecodeError, DecodingKind, EncodeError, 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::into_encode_refusal)?;
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::into_encode_refusal)?;
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(
205                EncodeError::new("tag [{}] cannot use an opaque constructed body")
206                    .within("BACnetEventParameter")
207                    .with_value((*tag).into())
208                    .into(),
209            );
210        }
211        BACnetEventParameter::Opaque { tag, data } => {
212            validate_tlv_sequence(data, "opaque event parameters")
213                .map_err(Error::into_encode_refusal)?;
214            // Preserved alternative: its captured bytes are the complete
215            // SEQUENCE body, re-emitted under its own opening/closing pair.
216            tags::encode_opening_tag(buf, *tag);
217            buf.extend_from_slice(data);
218            tags::encode_closing_tag(buf, *tag);
219        }
220    }
221    Ok(())
222}
223
224/// Decode one framed [`BACnetEventParameter`] CHOICE alternative at `offset`.
225///
226/// Returns the value and the offset past its encoding. Unmodeled (but valid)
227/// alternatives are preserved as [`BACnetEventParameter::Opaque`]; the
228/// omitted/deprecated/reserved tags 6, 7, 12, 19 are rejected.
229pub fn decode_event_parameter(
230    data: &[u8],
231    offset: usize,
232) -> Result<(BACnetEventParameter, usize), Error> {
233    use BACnetEventParameter as EP;
234
235    // Pre-framing EventEnrollment writes use [255] as an internal wrapper.
236    // Keep this shipped compatibility form local so the generic tag parser
237    // can reject ASHRAE-reserved tag number 255 everywhere else.
238    if data.get(offset..offset.saturating_add(2)) == Some(&[0xFE, 0xFF]) {
239        let body_start = offset + 2;
240        // The wrapper needs its closing pair after the opening one.
241        if data.len() < body_start + 2 {
242            return Err(Error::buffer_too_short(body_start + 2, data.len()));
243        }
244        let body_end = data.len() - 2;
245        if data.get(body_end..) != Some(&[0xFF, 0xFF]) {
246            return Err(Error::decoding(
247                offset,
248                "BACnetEventParameter: missing legacy closing tag",
249            ));
250        }
251        return Ok((
252            EP::Opaque {
253                tag: u8::MAX,
254                data: data[body_start..body_end].to_vec(),
255            },
256            data.len(),
257        ));
258    }
259
260    let (tag, pos) = tags::decode_tag(data, offset)?;
261    let what = "BACnetEventParameter";
262
263    if tag.class == tags::TagClass::Application && tag.number == tags::app_tag::OCTET_STRING {
264        let (octets, end) = contents(data, pos, tag.length)?;
265        return Ok((
266            EP::Opaque {
267                tag: u8::MAX,
268                data: octets.to_vec(),
269            },
270            end,
271        ));
272    }
273
274    if tag.class != tags::TagClass::Context {
275        return Err(Error::decoding(
276            offset,
277            "BACnetEventParameter: expected a context tag",
278        ));
279    }
280
281    if RESERVED_EVENT_TAGS.contains(&tag.number) {
282        return Err(DecodeError::new(
283            DecodingKind::InvalidEncoding,
284            offset,
285            "BACnetEventParameter: context tag [{}] is omitted/deprecated/reserved in Clause 21",
286        )
287        .with_value(tag.number.into())
288        .into());
289    }
290
291    // `none [20] NULL` is the only primitive alternative in this CHOICE.
292    if !tag.is_opening && !tag.is_closing {
293        if tag.number == 20 && tag.length == 0 {
294            return Ok((
295                EP::Opaque {
296                    tag: 20,
297                    data: Vec::new(),
298                },
299                pos,
300            ));
301        }
302        return Err(Error::decoding(
303            offset,
304            "BACnetEventParameter: invalid primitive alternative",
305        ));
306    }
307
308    if !tag.is_opening {
309        return Err(Error::decoding(
310            offset,
311            "BACnetEventParameter: unexpected closing tag",
312        ));
313    }
314    if tag.number == 20 {
315        return Err(Error::decoding(
316            offset,
317            "BACnetEventParameter: none [20] must use primitive NULL form",
318        ));
319    }
320
321    let value = match tag.number {
322        0 => {
323            let what = "BACnetEventParameter change-of-bitstring";
324            let (time_delay, pos) = decode_ctx_unsigned::<u32>(data, pos, 0, what)?;
325            let (bitmask, pos) = decode_ctx_bit_string(data, pos, 1, what)?;
326            let mut pos = expect_opening(data, pos, 2, what)?;
327            let mut list_of_values = Vec::new();
328            while !next_is_closing(data, pos, 2)? {
329                if list_of_values.len() >= MAX_FRAMED_ITEMS {
330                    return Err(Error::overflow(
331                        pos,
332                        "change-of-bitstring: list-of-bitstring-values exceeds limit",
333                    ));
334                }
335                let (bits, p) = decode_app_bit_string(data, pos, what)?;
336                list_of_values.push(bits);
337                pos = p;
338            }
339            pos = expect_closing(data, pos, 2, what)?;
340            pos = expect_closing(data, pos, 0, what)?;
341            (
342                EP::ChangeOfBitstring {
343                    time_delay,
344                    bitmask,
345                    list_of_values,
346                },
347                pos,
348            )
349        }
350        1 => {
351            let what = "BACnetEventParameter change-of-state";
352            let (time_delay, pos) = decode_ctx_unsigned::<u32>(data, pos, 0, what)?;
353            let mut pos = expect_opening(data, pos, 1, what)?;
354            let mut list_of_values = Vec::new();
355            while !next_is_closing(data, pos, 1)? {
356                if list_of_values.len() >= MAX_FRAMED_ITEMS {
357                    return Err(Error::overflow(
358                        pos,
359                        "change-of-state: list-of-values exceeds limit",
360                    ));
361                }
362                let (state, p) = decode_property_state(data, pos)?;
363                list_of_values.push(state);
364                pos = p;
365            }
366            pos = expect_closing(data, pos, 1, what)?;
367            pos = expect_closing(data, pos, 1, what)?;
368            (
369                EP::ChangeOfState {
370                    time_delay,
371                    list_of_values,
372                },
373                pos,
374            )
375        }
376        2 => {
377            let what = "BACnetEventParameter change-of-value";
378            let (time_delay, pos) = decode_ctx_unsigned::<u32>(data, pos, 0, what)?;
379            let mut pos = expect_opening(data, pos, 1, what)?;
380            let (inner, _) = tags::decode_tag(data, pos)?;
381            let criteria = if inner.is_context(0) {
382                let (bits, p) = decode_ctx_bit_string(data, pos, 0, what)?;
383                pos = p;
384                ChangeOfValueCriteria::Bitmask {
385                    unused_bits: bits.0,
386                    data: bits.1,
387                }
388            } else if inner.is_context(1) {
389                let (increment, p) = decode_ctx_real(data, pos, 1, what)?;
390                pos = p;
391                ChangeOfValueCriteria::ReferencedPropertyIncrement(increment)
392            } else {
393                return Err(DecodeError::new(
394                    DecodingKind::InvalidEncoding,
395                    pos,
396                    "change-of-value: unexpected cov-criteria tag [{}]",
397                )
398                .with_value(inner.number.into())
399                .into());
400            };
401            pos = expect_closing(data, pos, 1, what)?;
402            pos = expect_closing(data, pos, 2, what)?;
403            (
404                EP::ChangeOfValue {
405                    time_delay,
406                    criteria,
407                },
408                pos,
409            )
410        }
411        4 => {
412            let what = "BACnetEventParameter floating-limit";
413            let (time_delay, pos) = decode_ctx_unsigned::<u32>(data, pos, 0, what)?;
414            let pos = expect_opening(data, pos, 1, what)?;
415            let (setpoint_reference, pos) = decode_dopr_body(data, pos, what)?;
416            let pos = expect_closing(data, pos, 1, what)?;
417            let (low_diff_limit, pos) = decode_ctx_real(data, pos, 2, what)?;
418            let (high_diff_limit, pos) = decode_ctx_real(data, pos, 3, what)?;
419            let (deadband, pos) = decode_ctx_real(data, pos, 4, what)?;
420            let pos = expect_closing(data, pos, 4, what)?;
421            (
422                EP::FloatingLimit {
423                    time_delay,
424                    setpoint_reference,
425                    low_diff_limit,
426                    high_diff_limit,
427                    deadband,
428                },
429                pos,
430            )
431        }
432        5 => {
433            let what = "BACnetEventParameter out-of-range";
434            let (time_delay, pos) = decode_ctx_unsigned::<u32>(data, pos, 0, what)?;
435            let (low_limit, pos) = decode_ctx_real(data, pos, 1, what)?;
436            let (high_limit, pos) = decode_ctx_real(data, pos, 2, what)?;
437            let (deadband, pos) = decode_ctx_real(data, pos, 3, what)?;
438            let pos = expect_closing(data, pos, 5, what)?;
439            (
440                EP::OutOfRange {
441                    time_delay,
442                    low_limit,
443                    high_limit,
444                    deadband,
445                },
446                pos,
447            )
448        }
449        9 => {
450            let what = "BACnetEventParameter extended";
451            let (vendor_id, pos) = decode_ctx_unsigned::<u16>(data, pos, 0, what)?;
452            let (extended_event_type, pos) = decode_ctx_unsigned::<u32>(data, pos, 1, what)?;
453            let (raw_params, pos) = decode_ctx_constructed(data, pos, 2, what)?;
454            validate_extended_parameters(raw_params, "extended")?;
455            let pos = expect_closing(data, pos, 9, what)?;
456            (
457                EP::Extended {
458                    vendor_id,
459                    extended_event_type,
460                    parameters: raw_params.to_vec(),
461                },
462                pos,
463            )
464        }
465        n => {
466            // Unmodeled constructed alternative: preserve the SEQUENCE body
467            // verbatim after validating its nested tag structure.
468            let (body, p) = tags::extract_context_value(data, pos, n)?;
469            validate_tlv_sequence(body, "opaque event parameters")?;
470            (
471                EP::Opaque {
472                    tag: n,
473                    data: body.to_vec(),
474                },
475                p,
476            )
477        }
478    };
479    let (value, end) = value;
480    validate_tlv_sequence(&data[offset..end], what)?;
481    Ok((value, end))
482}