Skip to main content

bacnet_encoding/constructed/
object_property_reference.rs

1//! `BACnetObjectPropertyReference` and its `BACnetSetpointReference` wrapper
2//! (ASHRAE 135-2020 Clause 21) — wire codecs for the object-reference
3//! properties of the Loop (Clause 12.17) and Pulse Converter (Clause 12.23)
4//! objects.
5//!
6//! ```text
7//! BACnetObjectPropertyReference ::= SEQUENCE {
8//!     object-identifier    [0] BACnetObjectIdentifier,
9//!     property-identifier  [1] BACnetPropertyIdentifier,
10//!     property-array-index [2] Unsigned OPTIONAL  -- used only with array datatype
11//! }
12//!
13//! BACnetSetpointReference ::= SEQUENCE {
14//!     setpoint-reference [0] BACnetObjectPropertyReference OPTIONAL
15//! }
16//! ```
17//!
18//! Every member is context-tagged, so a property *write* carries a reference
19//! as primitive context tags [0]/[1] (plus optional [2]) concatenated on the
20//! wire; the `Setpoint_Reference` property nests those members in the
21//! opening/closing tag 0 frame of `BACnetSetpointReference`. Unlike
22//! `BACnetDeviceObjectPropertyReference` there is NO device member in this
23//! production — these references name objects in the local device only — so
24//! a device-qualifying [3] element is rejected on decode rather than being
25//! silently absorbed (the tranche-J [`super::decode_dopr_body`] codec accepts
26//! [3]; this codec narrows it).
27
28use bacnet_types::constructed::BACnetObjectPropertyReference;
29use bacnet_types::error::Error;
30use bytes::BytesMut;
31
32use crate::primitives;
33use crate::tags;
34
35use super::decode_dopr_body;
36
37const WHAT: &str = "BACnetObjectPropertyReference";
38
39/// Encode the bare `BACnetObjectPropertyReference` member sequence:
40/// context-tagged [0]/[1] plus [2] when the reference is indexed.
41pub fn encode_object_property_reference(buf: &mut BytesMut, r: &BACnetObjectPropertyReference) {
42    primitives::encode_ctx_object_id(buf, 0, &r.object_identifier);
43    primitives::encode_ctx_unsigned(buf, 1, r.property_identifier as u64);
44    if let Some(index) = r.property_array_index {
45        primitives::encode_ctx_unsigned(buf, 2, index as u64);
46    }
47}
48
49/// Encode the `BACnetSetpointReference` form: the reference inside an
50/// opening/closing context tag 0 frame (the production's
51/// `setpoint-reference [0]` member).
52pub fn encode_setpoint_reference(buf: &mut BytesMut, r: &BACnetObjectPropertyReference) {
53    tags::encode_opening_tag(buf, 0);
54    encode_object_property_reference(buf, r);
55    tags::encode_closing_tag(buf, 0);
56}
57
58/// Decode a whole property-value payload as the bare
59/// `BACnetObjectPropertyReference` member sequence.
60///
61/// Full consumption is required and the production is narrowed relative to
62/// the shared DOPR body codec: a device-qualifying member `[3]` is not part
63/// of `BACnetObjectPropertyReference` (the Loop/Pulse Converter references
64/// are local-device only), so it is rejected — as is any other content
65/// trailing the [0]/[1]/[2] members.
66pub fn decode_object_property_reference(
67    data: &[u8],
68) -> Result<BACnetObjectPropertyReference, Error> {
69    let (dopr, end) = decode_dopr_body(data, 0, WHAT)?;
70    if end != data.len() {
71        return Err(Error::decoding(
72            end,
73            format!("{WHAT}: trailing content after the reference members"),
74        ));
75    }
76    if dopr.device_identifier.is_some() {
77        return Err(Error::decoding(
78            0,
79            format!("{WHAT}: [3] device-identifier is not part of this production"),
80        ));
81    }
82    Ok(BACnetObjectPropertyReference {
83        object_identifier: dopr.object_identifier,
84        property_identifier: dopr.property_identifier,
85        property_array_index: dopr.property_array_index,
86    })
87}
88
89/// Decode a whole property-value payload as the `BACnetSetpointReference`
90/// form: an opening/closing context tag 0 frame whose content is the bare
91/// reference, decoded with [`decode_object_property_reference`]'s strictness.
92///
93/// The production's member is `OPTIONAL`: an empty frame (`0x0E 0x0F`) is a
94/// syntactically valid encoding of the ABSENT alternative (Clause 12.17,
95/// `Setpoint_Reference`: "The absence of a reference indicates that the
96/// setpoint for this control loop is fixed and is contained in the Setpoint
97/// property") and yields `None` — not an encoding error.
98pub fn decode_setpoint_reference(
99    data: &[u8],
100) -> Result<Option<BACnetObjectPropertyReference>, Error> {
101    let what = "BACnetSetpointReference";
102    let (tag, pos) = tags::decode_tag(data, 0)?;
103    if !tag.is_opening_tag(0) {
104        return Err(Error::decoding(
105            0,
106            format!("{what}: expected opening tag [0]"),
107        ));
108    }
109    let (inner, after) = tags::extract_context_value(data, pos, 0)?;
110    if after != data.len() {
111        return Err(Error::decoding(
112            after,
113            format!("{what}: trailing content after the closing tag"),
114        ));
115    }
116    if inner.is_empty() {
117        return Ok(None); // setpoint-reference [0] absent (OPTIONAL member)
118    }
119    decode_object_property_reference(inner).map(Some)
120}
121
122#[cfg(test)]
123mod tests;