bacnet-encoding 0.13.0

BACnet ASN.1/BER encoding, APDU and NPDU codecs per ASHRAE 135-2020
Documentation
//! `BACnetFaultParameter` full ASN.1 framing.
//!
//! `BACnetFaultParameter` (ASHRAE 135-2020 Clause 21) selects one fault
//! algorithm by context tag and carries its settings; [`FaultParameters`]
//! models every alternative. `none` is a primitive NULL. Every other
//! alternative is a constructed value holding context-tagged members, all
//! required:
//!
//! | Tag | Alternative | Member tag | Member | Type |
//! |---|---|---|---|---|
//! | `[0]` | `none` | | | NULL, no members |
//! | `[1]` | `fault-characterstring` | `[0]` | `list-of-fault-values` | list of CharacterString |
//! | `[2]` | `fault-extended` | `[0]` | `vendor-id` | Unsigned16 |
//! | | | `[1]` | `extended-fault-type` | Unsigned |
//! | | | `[2]` | `parameters` | list of extended values, see below |
//! | `[3]` | `fault-life-safety` | `[0]` | `list-of-fault-values` | list of life-safety states |
//! | | | `[1]` | `mode-property-reference` | `BACnetDeviceObjectPropertyReference` |
//! | `[4]` | `fault-state` | `[0]` | `list-of-fault-values` | list of `BACnetPropertyStates` |
//! | `[5]` | `fault-status-flags` | `[0]` | `status-flags-reference` | device property reference |
//! | `[6]` | `fault-out-of-range` | `[0]` | `min-normal-value` | numeric choice, see below |
//! | | | `[1]` | `max-normal-value` | the same four-way choice |
//! | `[7]` | `fault-listed` | `[0]` | `fault-list-reference` | device property reference |
//!
//! Each "device property reference" is a `BACnetDeviceObjectPropertyReference`,
//! and the life-safety list holds `BACnetLifeSafetyState` values. Each extended
//! `parameters` entry is an application-tagged primitive or a `[0]`-framed
//! `BACnetDeviceObjectPropertyReference`.
//!
//! The min/max inner CHOICE alternatives are untagged — discovered by their
//! APPLICATION tag (REAL=4, Unsigned=2, Double=5, INTEGER=3). The Rust type
//! stores `f64`, so encode always selects the `Double` alternative; decode
//! accepts all four.

#[cfg(not(feature = "std"))]
use alloc::{string::ToString, vec::Vec};

use bacnet_types::constructed::FaultParameters;
use bacnet_types::enums::LifeSafetyState;
use bacnet_types::error::Error;
use bytes::BytesMut;

use crate::primitives;
use crate::tags::{self, TagClass};

use super::tagged::{
    contents, decode_app_character_string, decode_app_enumerated, decode_ctx_constructed,
    decode_ctx_unsigned, expect_closing, expect_opening, next_is_closing,
};
use super::{
    decode_dopr_body, decode_property_state, encode_dopr_body, encode_property_state,
    validate_extended_parameters, MAX_FRAMED_ITEMS,
};

/// Encode a [`FaultParameters`] as its full CHOICE framing.
pub fn encode_fault_parameters(buf: &mut BytesMut, value: &FaultParameters) -> Result<(), Error> {
    let mut encoded = BytesMut::new();
    encode_fault_parameters_into(&mut encoded, value)?;
    super::validate_tlv_sequence(&encoded, "FaultParameters")
        .map_err(|error| Error::Encoding(error.to_string()))?;
    buf.extend_from_slice(&encoded);
    Ok(())
}

fn encode_fault_parameters_into(buf: &mut BytesMut, value: &FaultParameters) -> Result<(), Error> {
    use FaultParameters as F;
    match value {
        F::FaultNone => {
            // none [0] NULL — primitive context tag, no contents.
            tags::encode_tag(buf, 0, TagClass::Context, 0);
        }
        F::FaultCharacterString { fault_values } => {
            tags::encode_opening_tag(buf, 1);
            tags::encode_opening_tag(buf, 0);
            for s in fault_values {
                primitives::encode_app_character_string(buf, s)?;
            }
            tags::encode_closing_tag(buf, 0);
            tags::encode_closing_tag(buf, 1);
        }
        F::FaultExtended {
            vendor_id,
            extended_fault_type,
            parameters,
        } => {
            validate_extended_parameters(parameters, "fault-extended")
                .map_err(|error| Error::Encoding(error.to_string()))?;
            tags::encode_opening_tag(buf, 2);
            primitives::encode_ctx_unsigned(buf, 0, *vendor_id as u64);
            primitives::encode_ctx_unsigned(buf, 1, *extended_fault_type as u64);
            tags::encode_opening_tag(buf, 2);
            buf.extend_from_slice(parameters);
            tags::encode_closing_tag(buf, 2);
            tags::encode_closing_tag(buf, 2);
        }
        F::FaultLifeSafety {
            fault_values,
            mode_property_reference,
        } => {
            tags::encode_opening_tag(buf, 3);
            tags::encode_opening_tag(buf, 0);
            for v in fault_values {
                primitives::encode_app_enumerated(buf, v.to_raw());
            }
            tags::encode_closing_tag(buf, 0);
            // mode-property-reference [1] BACnetDeviceObjectPropertyReference
            tags::encode_opening_tag(buf, 1);
            encode_dopr_body(buf, mode_property_reference);
            tags::encode_closing_tag(buf, 1);
            tags::encode_closing_tag(buf, 3);
        }
        F::FaultState { fault_values } => {
            let mut encoded_states = BytesMut::new();
            for state in fault_values {
                encode_property_state(&mut encoded_states, state)?;
            }
            tags::encode_opening_tag(buf, 4);
            tags::encode_opening_tag(buf, 0);
            buf.extend_from_slice(&encoded_states);
            tags::encode_closing_tag(buf, 0);
            tags::encode_closing_tag(buf, 4);
        }
        F::FaultStatusFlags { reference } => {
            tags::encode_opening_tag(buf, 5);
            tags::encode_opening_tag(buf, 0);
            encode_dopr_body(buf, reference);
            tags::encode_closing_tag(buf, 0);
            tags::encode_closing_tag(buf, 5);
        }
        F::FaultOutOfRange {
            min_normal,
            max_normal,
        } => {
            tags::encode_opening_tag(buf, 6);
            // min/max-normal-value [n] CHOICE — explicitly tagged around the
            // application-tagged alternative; the model stores f64, so the
            // `double` alternative encodes it losslessly.
            tags::encode_opening_tag(buf, 0);
            primitives::encode_app_double(buf, *min_normal);
            tags::encode_closing_tag(buf, 0);
            tags::encode_opening_tag(buf, 1);
            primitives::encode_app_double(buf, *max_normal);
            tags::encode_closing_tag(buf, 1);
            tags::encode_closing_tag(buf, 6);
        }
        F::FaultListed { reference } => {
            tags::encode_opening_tag(buf, 7);
            tags::encode_opening_tag(buf, 0);
            encode_dopr_body(buf, reference);
            tags::encode_closing_tag(buf, 0);
            tags::encode_closing_tag(buf, 7);
        }
    }
    Ok(())
}

/// Decode one application-tagged REAL/Unsigned/Double/INTEGER as f64 — the
/// `fault-out-of-range` min/max inner CHOICE alternatives.
fn decode_fault_normal_value(data: &[u8], offset: usize) -> Result<(f64, usize), Error> {
    let (t, pos) = tags::decode_tag(data, offset)?;
    if t.class != TagClass::Application || t.is_opening || t.is_closing {
        return Err(Error::decoding(
            offset,
            "fault-out-of-range: min/max-normal-value must be application-tagged",
        ));
    }
    let what = "fault-out-of-range min/max-normal-value";
    if t.number == tags::app_tag::BOOLEAN {
        return Err(Error::decoding(
            offset,
            format!("{what}: BOOLEAN is not a valid alternative"),
        ));
    }
    let (content, end) = contents(data, pos, t.length)?;
    let value = match t.number {
        tags::app_tag::REAL => primitives::decode_real(content)? as f64,
        tags::app_tag::UNSIGNED => primitives::decode_unsigned(content)? as f64,
        tags::app_tag::DOUBLE => primitives::decode_double(content)?,
        tags::app_tag::SIGNED => primitives::decode_signed(content)? as f64,
        other => {
            return Err(Error::decoding(
                offset,
                format!("{what}: unexpected application tag {other}"),
            ))
        }
    };
    Ok((value, end))
}

/// Decode one framed [`FaultParameters`] CHOICE alternative at `offset`.
///
/// Returns the value and the offset past its encoding.
pub fn decode_fault_parameters(
    data: &[u8],
    offset: usize,
) -> Result<(FaultParameters, usize), Error> {
    use FaultParameters as F;
    let (tag, pos) = tags::decode_tag(data, offset)?;
    let what = "BACnetFaultParameter";

    // none [0] NULL — primitive context tag, no contents.
    if tag.is_context(0) {
        if tag.length != 0 {
            return Err(Error::decoding(
                offset,
                "BACnetFaultParameter: none [0] NULL must have no contents",
            ));
        }
        return Ok((F::FaultNone, pos));
    }
    if !tag.is_opening {
        return Err(Error::decoding(
            offset,
            "BACnetFaultParameter: expected an opening context tag or [0] NULL",
        ));
    }

    let value = match tag.number {
        1 => {
            let mut pos = expect_opening(data, pos, 0, what)?;
            let mut fault_values = Vec::new();
            while !next_is_closing(data, pos, 0)? {
                if fault_values.len() >= MAX_FRAMED_ITEMS {
                    return Err(Error::overflow(
                        pos,
                        "fault-characterstring: list-of-fault-values exceeds limit",
                    ));
                }
                let (s, p) = decode_app_character_string(data, pos, what)?;
                fault_values.push(s);
                pos = p;
            }
            pos = expect_closing(data, pos, 0, what)?;
            pos = expect_closing(data, pos, 1, what)?;
            (F::FaultCharacterString { fault_values }, pos)
        }
        2 => {
            let what = "BACnetFaultParameter fault-extended";
            let (vendor_id, pos) = decode_ctx_unsigned::<u16>(data, pos, 0, what)?;
            let (extended_fault_type, pos) = decode_ctx_unsigned::<u32>(data, pos, 1, what)?;
            let (raw_params, pos) = decode_ctx_constructed(data, pos, 2, what)?;
            validate_extended_parameters(raw_params, "fault-extended")?;
            let pos = expect_closing(data, pos, 2, what)?;
            (
                F::FaultExtended {
                    vendor_id,
                    extended_fault_type,
                    parameters: raw_params.to_vec(),
                },
                pos,
            )
        }
        3 => {
            let mut pos = expect_opening(data, pos, 0, what)?;
            let mut fault_values = Vec::new();
            while !next_is_closing(data, pos, 0)? {
                if fault_values.len() >= MAX_FRAMED_ITEMS {
                    return Err(Error::overflow(
                        pos,
                        "fault-life-safety: list-of-fault-values exceeds limit",
                    ));
                }
                let (v, p) = decode_app_enumerated::<u32>(data, pos, what)?;
                fault_values.push(LifeSafetyState::from_raw(v));
                pos = p;
            }
            pos = expect_closing(data, pos, 0, what)?;
            pos = expect_opening(data, pos, 1, what)?;
            let (mode_property_reference, p) = decode_dopr_body(data, pos, what)?;
            pos = p;
            pos = expect_closing(data, pos, 1, what)?;
            pos = expect_closing(data, pos, 3, what)?;
            (
                F::FaultLifeSafety {
                    fault_values,
                    mode_property_reference,
                },
                pos,
            )
        }
        4 => {
            let mut pos = expect_opening(data, pos, 0, what)?;
            let mut fault_values = Vec::new();
            while !next_is_closing(data, pos, 0)? {
                if fault_values.len() >= MAX_FRAMED_ITEMS {
                    return Err(Error::overflow(
                        pos,
                        "fault-state: list-of-fault-values exceeds limit",
                    ));
                }
                let (state, p) = decode_property_state(data, pos)?;
                fault_values.push(state);
                pos = p;
            }
            pos = expect_closing(data, pos, 0, what)?;
            pos = expect_closing(data, pos, 4, what)?;
            (F::FaultState { fault_values }, pos)
        }
        5 => {
            let mut pos = expect_opening(data, pos, 0, what)?;
            let (reference, p) = decode_dopr_body(data, pos, what)?;
            pos = p;
            pos = expect_closing(data, pos, 0, what)?;
            pos = expect_closing(data, pos, 5, what)?;
            (F::FaultStatusFlags { reference }, pos)
        }
        6 => {
            let mut pos = expect_opening(data, pos, 0, what)?;
            let (min_normal, p) = decode_fault_normal_value(data, pos)?;
            pos = p;
            pos = expect_closing(data, pos, 0, what)?;
            pos = expect_opening(data, pos, 1, what)?;
            let (max_normal, p) = decode_fault_normal_value(data, pos)?;
            pos = p;
            pos = expect_closing(data, pos, 1, what)?;
            pos = expect_closing(data, pos, 6, what)?;
            (
                F::FaultOutOfRange {
                    min_normal,
                    max_normal,
                },
                pos,
            )
        }
        7 => {
            let mut pos = expect_opening(data, pos, 0, what)?;
            let (reference, p) = decode_dopr_body(data, pos, what)?;
            pos = p;
            pos = expect_closing(data, pos, 0, what)?;
            pos = expect_closing(data, pos, 7, what)?;
            (F::FaultListed { reference }, pos)
        }
        n => {
            return Err(Error::decoding(
                offset,
                format!("BACnetFaultParameter: unknown CHOICE tag [{n}]"),
            ));
        }
    };
    let (value, end) = value;
    super::validate_tlv_sequence(&data[offset..end], what)?;
    Ok((value, end))
}