Skip to main content

bacnet_encoding/constructed/
fault_parameter.rs

1//! `BACnetFaultParameter` full ASN.1 framing.
2//!
3//! Production (ASHRAE 135-2020 Clause 21), modeled by [`FaultParameters`]:
4//!
5//! ```text
6//! BACnetFaultParameter ::= CHOICE {
7//!     none                 [0] NULL,
8//!     fault-characterstring [1] SEQUENCE { list-of-fault-values [0] SEQUENCE OF CharacterString },
9//!     fault-extended       [2] SEQUENCE { vendor-id [0] Unsigned16,
10//!                                         extended-fault-type [1] Unsigned,
11//!                                         parameters [2] SEQUENCE OF CHOICE { ... } },
12//!     fault-life-safety    [3] SEQUENCE { list-of-fault-values [0] SEQUENCE OF BACnetLifeSafetyState,
13//!                                         mode-property-reference [1] BACnetDeviceObjectPropertyReference },
14//!     fault-state          [4] SEQUENCE { list-of-fault-values [0] SEQUENCE OF BACnetPropertyStates },
15//!     fault-status-flags   [5] SEQUENCE { status-flags-reference [0] BACnetDeviceObjectPropertyReference },
16//!     fault-out-of-range   [6] SEQUENCE { min-normal-value [0] CHOICE { real REAL, unsigned Unsigned,
17//!                                                                         double Double, integer INTEGER },
18//!                                         max-normal-value [1] CHOICE { ... same ... } },
19//!     fault-listed         [7] SEQUENCE { fault-list-reference [0] BACnetDeviceObjectPropertyReference } }
20//! ```
21//!
22//! The min/max inner CHOICE alternatives are untagged — discovered by their
23//! APPLICATION tag (REAL=4, Unsigned=2, Double=5, INTEGER=3). The Rust type
24//! stores `f64`, so encode always selects the `Double` alternative; decode
25//! accepts all four.
26
27use bacnet_types::constructed::FaultParameters;
28use bacnet_types::error::Error;
29use bytes::BytesMut;
30
31use crate::primitives;
32use crate::tags::{self, TagClass};
33
34use super::{
35    decode_app_character_string, decode_app_enumerated, decode_ctx_unsigned, decode_dopr_body,
36    decode_property_state, encode_dopr_body, encode_property_state, expect_closing, expect_opening,
37    validate_extended_parameters, MAX_FRAMED_ITEMS,
38};
39
40/// Encode a [`FaultParameters`] as its full CHOICE framing.
41pub fn encode_fault_parameters(buf: &mut BytesMut, value: &FaultParameters) -> Result<(), Error> {
42    let mut encoded = BytesMut::new();
43    encode_fault_parameters_into(&mut encoded, value)?;
44    super::validate_tlv_sequence(&encoded, "FaultParameters")
45        .map_err(|error| Error::Encoding(error.to_string()))?;
46    buf.extend_from_slice(&encoded);
47    Ok(())
48}
49
50fn encode_fault_parameters_into(buf: &mut BytesMut, value: &FaultParameters) -> Result<(), Error> {
51    use FaultParameters as F;
52    match value {
53        F::FaultNone => {
54            // none [0] NULL — primitive context tag, no contents.
55            tags::encode_tag(buf, 0, TagClass::Context, 0);
56        }
57        F::FaultCharacterString { fault_values } => {
58            tags::encode_opening_tag(buf, 1);
59            tags::encode_opening_tag(buf, 0);
60            for s in fault_values {
61                primitives::encode_app_character_string(buf, s)?;
62            }
63            tags::encode_closing_tag(buf, 0);
64            tags::encode_closing_tag(buf, 1);
65        }
66        F::FaultExtended {
67            vendor_id,
68            extended_fault_type,
69            parameters,
70        } => {
71            validate_extended_parameters(parameters, "fault-extended")
72                .map_err(|error| Error::Encoding(error.to_string()))?;
73            tags::encode_opening_tag(buf, 2);
74            primitives::encode_ctx_unsigned(buf, 0, *vendor_id as u64);
75            primitives::encode_ctx_unsigned(buf, 1, *extended_fault_type as u64);
76            tags::encode_opening_tag(buf, 2);
77            buf.extend_from_slice(parameters);
78            tags::encode_closing_tag(buf, 2);
79            tags::encode_closing_tag(buf, 2);
80        }
81        F::FaultLifeSafety {
82            fault_values,
83            mode_for_reference,
84        } => {
85            tags::encode_opening_tag(buf, 3);
86            tags::encode_opening_tag(buf, 0);
87            for v in fault_values {
88                primitives::encode_app_enumerated(buf, *v);
89            }
90            tags::encode_closing_tag(buf, 0);
91            // mode-property-reference [1] BACnetDeviceObjectPropertyReference
92            tags::encode_opening_tag(buf, 1);
93            encode_dopr_body(buf, mode_for_reference);
94            tags::encode_closing_tag(buf, 1);
95            tags::encode_closing_tag(buf, 3);
96        }
97        F::FaultState { fault_values } => {
98            let mut encoded_states = BytesMut::new();
99            for state in fault_values {
100                encode_property_state(&mut encoded_states, state)?;
101            }
102            tags::encode_opening_tag(buf, 4);
103            tags::encode_opening_tag(buf, 0);
104            buf.extend_from_slice(&encoded_states);
105            tags::encode_closing_tag(buf, 0);
106            tags::encode_closing_tag(buf, 4);
107        }
108        F::FaultStatusFlags { reference } => {
109            tags::encode_opening_tag(buf, 5);
110            tags::encode_opening_tag(buf, 0);
111            encode_dopr_body(buf, reference);
112            tags::encode_closing_tag(buf, 0);
113            tags::encode_closing_tag(buf, 5);
114        }
115        F::FaultOutOfRange {
116            min_normal,
117            max_normal,
118        } => {
119            tags::encode_opening_tag(buf, 6);
120            // min/max-normal-value [n] CHOICE — explicitly tagged around the
121            // application-tagged alternative; the model stores f64, so the
122            // `double` alternative encodes it losslessly.
123            tags::encode_opening_tag(buf, 0);
124            primitives::encode_app_double(buf, *min_normal);
125            tags::encode_closing_tag(buf, 0);
126            tags::encode_opening_tag(buf, 1);
127            primitives::encode_app_double(buf, *max_normal);
128            tags::encode_closing_tag(buf, 1);
129            tags::encode_closing_tag(buf, 6);
130        }
131        F::FaultListed { reference } => {
132            tags::encode_opening_tag(buf, 7);
133            tags::encode_opening_tag(buf, 0);
134            encode_dopr_body(buf, reference);
135            tags::encode_closing_tag(buf, 0);
136            tags::encode_closing_tag(buf, 7);
137        }
138    }
139    Ok(())
140}
141
142/// Decode one application-tagged REAL/Unsigned/Double/INTEGER as f64 — the
143/// `fault-out-of-range` min/max inner CHOICE alternatives.
144fn decode_fault_normal_value(data: &[u8], offset: usize) -> Result<(f64, usize), Error> {
145    let (t, pos) = tags::decode_tag(data, offset)?;
146    if t.class != TagClass::Application || t.is_opening || t.is_closing {
147        return Err(Error::decoding(
148            offset,
149            "fault-out-of-range: min/max-normal-value must be application-tagged",
150        ));
151    }
152    let what = "fault-out-of-range min/max-normal-value";
153    if t.number == tags::app_tag::BOOLEAN {
154        return Err(Error::decoding(
155            offset,
156            format!("{what}: BOOLEAN is not a valid alternative"),
157        ));
158    }
159    let end = pos
160        .checked_add(t.length as usize)
161        .ok_or_else(|| Error::decoding(pos, format!("{what}: length overflow")))?;
162    if end > data.len() {
163        return Err(Error::buffer_too_short(end, data.len()));
164    }
165    let content = &data[pos..end];
166    let value = match t.number {
167        tags::app_tag::REAL => primitives::decode_real(content)? as f64,
168        tags::app_tag::UNSIGNED => primitives::decode_unsigned(content)? as f64,
169        tags::app_tag::DOUBLE => primitives::decode_double(content)?,
170        tags::app_tag::SIGNED => primitives::decode_signed(content)? as f64,
171        other => {
172            return Err(Error::decoding(
173                offset,
174                format!("{what}: unexpected application tag {other}"),
175            ))
176        }
177    };
178    Ok((value, end))
179}
180
181/// Decode one framed [`FaultParameters`] CHOICE alternative at `offset`.
182///
183/// Returns the value and the offset past its encoding.
184pub fn decode_fault_parameters(
185    data: &[u8],
186    offset: usize,
187) -> Result<(FaultParameters, usize), Error> {
188    use FaultParameters as F;
189    let (tag, pos) = tags::decode_tag(data, offset)?;
190    let what = "BACnetFaultParameter";
191
192    // none [0] NULL — primitive context tag, no contents.
193    if tag.is_context(0) {
194        if tag.length != 0 {
195            return Err(Error::decoding(
196                offset,
197                "BACnetFaultParameter: none [0] NULL must have no contents",
198            ));
199        }
200        return Ok((F::FaultNone, pos));
201    }
202    if !tag.is_opening {
203        return Err(Error::decoding(
204            offset,
205            "BACnetFaultParameter: expected an opening context tag or [0] NULL",
206        ));
207    }
208
209    let value = match tag.number {
210        1 => {
211            let mut pos = expect_opening(data, pos, 0, what)?;
212            let mut fault_values = Vec::new();
213            loop {
214                let (peek, _) = tags::decode_tag(data, pos)?;
215                if peek.is_closing_tag(0) {
216                    break;
217                }
218                if fault_values.len() >= MAX_FRAMED_ITEMS {
219                    return Err(Error::decoding(
220                        pos,
221                        "fault-characterstring: list-of-fault-values exceeds limit",
222                    ));
223                }
224                let (s, p) = decode_app_character_string(data, pos, what)?;
225                fault_values.push(s);
226                pos = p;
227            }
228            pos = expect_closing(data, pos, 0, what)?;
229            pos = expect_closing(data, pos, 1, what)?;
230            (F::FaultCharacterString { fault_values }, pos)
231        }
232        2 => {
233            let mut pos = pos;
234            let (raw, p) = decode_ctx_unsigned(data, pos, 0, what)?;
235            let vendor_id = u16::try_from(raw)
236                .map_err(|_| Error::decoding(pos, "fault-extended: vendor-id exceeds u16"))?;
237            pos = p;
238            let (raw, p) = decode_ctx_unsigned(data, pos, 1, what)?;
239            let extended_fault_type = u32::try_from(raw).map_err(|_| {
240                Error::decoding(pos, "fault-extended: extended-fault-type exceeds u32")
241            })?;
242            pos = p;
243            pos = expect_opening(data, pos, 2, what)?;
244            let (raw_params, p) = tags::extract_context_value(data, pos, 2)?;
245            validate_extended_parameters(raw_params, "fault-extended")?;
246            pos = p;
247            pos = expect_closing(data, pos, 2, what)?;
248            (
249                F::FaultExtended {
250                    vendor_id,
251                    extended_fault_type,
252                    parameters: raw_params.to_vec(),
253                },
254                pos,
255            )
256        }
257        3 => {
258            let mut pos = expect_opening(data, pos, 0, what)?;
259            let mut fault_values = Vec::new();
260            loop {
261                let (peek, _) = tags::decode_tag(data, pos)?;
262                if peek.is_closing_tag(0) {
263                    break;
264                }
265                if fault_values.len() >= MAX_FRAMED_ITEMS {
266                    return Err(Error::decoding(
267                        pos,
268                        "fault-life-safety: list-of-fault-values exceeds limit",
269                    ));
270                }
271                let (v, p) = decode_app_enumerated(data, pos, what)?;
272                fault_values.push(v);
273                pos = p;
274            }
275            pos = expect_closing(data, pos, 0, what)?;
276            pos = expect_opening(data, pos, 1, what)?;
277            let (mode_for_reference, p) = decode_dopr_body(data, pos, what)?;
278            pos = p;
279            pos = expect_closing(data, pos, 1, what)?;
280            pos = expect_closing(data, pos, 3, what)?;
281            (
282                F::FaultLifeSafety {
283                    fault_values,
284                    mode_for_reference,
285                },
286                pos,
287            )
288        }
289        4 => {
290            let mut pos = expect_opening(data, pos, 0, what)?;
291            let mut fault_values = Vec::new();
292            loop {
293                let (peek, _) = tags::decode_tag(data, pos)?;
294                if peek.is_closing_tag(0) {
295                    break;
296                }
297                if fault_values.len() >= MAX_FRAMED_ITEMS {
298                    return Err(Error::decoding(
299                        pos,
300                        "fault-state: list-of-fault-values exceeds limit",
301                    ));
302                }
303                let (state, p) = decode_property_state(data, pos)?;
304                fault_values.push(state);
305                pos = p;
306            }
307            pos = expect_closing(data, pos, 0, what)?;
308            pos = expect_closing(data, pos, 4, what)?;
309            (F::FaultState { fault_values }, pos)
310        }
311        5 => {
312            let mut pos = expect_opening(data, pos, 0, what)?;
313            let (reference, p) = decode_dopr_body(data, pos, what)?;
314            pos = p;
315            pos = expect_closing(data, pos, 0, what)?;
316            pos = expect_closing(data, pos, 5, what)?;
317            (F::FaultStatusFlags { reference }, pos)
318        }
319        6 => {
320            let mut pos = expect_opening(data, pos, 0, what)?;
321            let (min_normal, p) = decode_fault_normal_value(data, pos)?;
322            pos = p;
323            pos = expect_closing(data, pos, 0, what)?;
324            pos = expect_opening(data, pos, 1, what)?;
325            let (max_normal, p) = decode_fault_normal_value(data, pos)?;
326            pos = p;
327            pos = expect_closing(data, pos, 1, what)?;
328            pos = expect_closing(data, pos, 6, what)?;
329            (
330                F::FaultOutOfRange {
331                    min_normal,
332                    max_normal,
333                },
334                pos,
335            )
336        }
337        7 => {
338            let mut pos = expect_opening(data, pos, 0, what)?;
339            let (reference, p) = decode_dopr_body(data, pos, what)?;
340            pos = p;
341            pos = expect_closing(data, pos, 0, what)?;
342            pos = expect_closing(data, pos, 7, what)?;
343            (F::FaultListed { reference }, pos)
344        }
345        n => {
346            return Err(Error::decoding(
347                offset,
348                format!("BACnetFaultParameter: unknown CHOICE tag [{n}]"),
349            ));
350        }
351    };
352    let (value, end) = value;
353    super::validate_tlv_sequence(&data[offset..end], what)?;
354    Ok((value, end))
355}