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//! `BACnetObjectPropertyReference` names one property of an object, and can
7//! narrow it to a single element when the property is an array:
8//!
9//! | Field | Tag | Type | Optional |
10//! |---|---|---|---|
11//! | `object-identifier` | `[0]` | `BACnetObjectIdentifier` | no |
12//! | `property-identifier` | `[1]` | `BACnetPropertyIdentifier` | no |
13//! | `property-array-index` | `[2]` | Unsigned | yes; only meaningful for array properties |
14//!
15//! `BACnetSetpointReference` wraps at most one such reference:
16//!
17//! | Field | Tag | Type | Optional |
18//! |---|---|---|---|
19//! | `setpoint-reference` | `[0]` | `BACnetObjectPropertyReference` | yes |
20//!
21//! Every member is context-tagged, so a property value carries a reference
22//! as primitive context tags \[0\]/\[1\] (plus optional \[2\]) concatenated on
23//! the wire; the `Setpoint_Reference` property nests those members in the
24//! opening/closing tag 0 frame of `BACnetSetpointReference`, and leaves the
25//! frame out entirely when it holds no reference (an absent optional member
26//! has no encoding, Clause 20.2.16). Unlike
27//! `BACnetDeviceObjectPropertyReference` there is NO device member in this
28//! production — these references name objects in the local device only — so
29//! a device-qualifying \[3\] element is rejected on decode rather than being
30//! silently absorbed (the tranche-J `decode_dopr_body` codec accepts
31//! \[3\]; this codec narrows it).
32
33use bacnet_types::constructed::BACnetObjectPropertyReference;
34use bacnet_types::error::Error;
35use bytes::BytesMut;
36
37use crate::primitives;
38use crate::tags;
39
40use super::tagged::{
41    decode_ctx_constructed, decode_ctx_object_id, decode_ctx_unsigned, decode_optional_ctx,
42    expect_end, next_is_context,
43};
44
45const WHAT: &str = "BACnetObjectPropertyReference";
46const SETPOINT: &str = "BACnetSetpointReference";
47
48/// Encode the bare `BACnetObjectPropertyReference` member sequence:
49/// context-tagged \[0\]/\[1\] plus \[2\] when the reference is indexed.
50pub fn encode_object_property_reference(buf: &mut BytesMut, r: &BACnetObjectPropertyReference) {
51    primitives::encode_ctx_object_id(buf, 0, &r.object_identifier);
52    primitives::encode_ctx_unsigned(buf, 1, r.property_identifier as u64);
53    if let Some(index) = r.property_array_index {
54        primitives::encode_ctx_unsigned(buf, 2, index as u64);
55    }
56}
57
58/// Encode the `BACnetSetpointReference` form: the reference inside an
59/// opening/closing context tag 0 frame (the production's
60/// `setpoint-reference [0]` member).
61pub fn encode_setpoint_reference(buf: &mut BytesMut, r: &BACnetObjectPropertyReference) {
62    tags::encode_opening_tag(buf, 0);
63    encode_object_property_reference(buf, r);
64    tags::encode_closing_tag(buf, 0);
65}
66
67/// Decode the bare `BACnetObjectPropertyReference` members starting at
68/// `offset`, \[0\], \[1\] and the optional \[2\]; returns the reference
69/// and the offset just past its last member.
70///
71/// The production is narrowed relative to the shared DOPR body codec: a
72/// device-qualifying \[3\] right after the members is not part of it (the
73/// Loop/Pulse Converter references are local-device only), so it is refused.
74/// Anything else that follows is the caller's to judge.
75pub fn decode_object_property_reference_at(
76    data: &[u8],
77    offset: usize,
78) -> Result<(BACnetObjectPropertyReference, usize), Error> {
79    let (object_identifier, offset) = decode_ctx_object_id(data, offset, 0, WHAT)?;
80    let (property_identifier, offset) = decode_ctx_unsigned::<u32>(data, offset, 1, WHAT)?;
81    let (property_array_index, offset) =
82        decode_optional_ctx(data, offset, 2, WHAT, decode_ctx_unsigned::<u32>)?;
83    if next_is_context(data, offset, 3)? {
84        return Err(Error::decoding(
85            offset,
86            format!("{WHAT}: [3] device-identifier is not part of this production"),
87        ));
88    }
89    Ok((
90        BACnetObjectPropertyReference {
91            object_identifier,
92            property_identifier,
93            property_array_index,
94        },
95        offset,
96    ))
97}
98
99/// Decode a whole property-value payload as the bare
100/// `BACnetObjectPropertyReference` member sequence: the members, read by
101/// [`decode_object_property_reference_at`], must use all of `data`.
102pub fn decode_object_property_reference(
103    data: &[u8],
104) -> Result<BACnetObjectPropertyReference, Error> {
105    let (reference, end) = decode_object_property_reference_at(data, 0)?;
106    expect_end(data, end, end, WHAT)?;
107    Ok(reference)
108}
109
110/// Decode the `BACnetSetpointReference` frame at `offset` holding a
111/// reference: opening context tag 0, the bare members, which must fill the
112/// frame, and the closing tag. Returns the reference and the offset just
113/// past the closing tag; what follows is the caller's to judge.
114///
115/// A frame with nothing inside (`0x0E 0x0F`) is the member present but
116/// holding no object or property, which this refuses like any other
117/// incomplete reference. The value without a reference has no frame at all;
118/// see [`decode_setpoint_reference`].
119pub fn decode_setpoint_reference_at(
120    data: &[u8],
121    offset: usize,
122) -> Result<(BACnetObjectPropertyReference, usize), Error> {
123    let (inner, after) = decode_ctx_constructed(data, offset, 0, SETPOINT)?;
124    let (reference, end) = decode_object_property_reference_at(inner, 0)?;
125    expect_end(inner, end, offset, SETPOINT)?;
126    Ok((reference, after))
127}
128
129/// Decode a whole property-value payload as the `BACnetSetpointReference`
130/// form, the frame read by [`decode_setpoint_reference_at`], which must use
131/// all of `data`.
132///
133/// The production's one member is optional, and an absent member has no
134/// encoding (Clause 20.2.16), so an empty payload is the value without a
135/// reference and yields `None`: Clause 12.17.16 then takes the setpoint from
136/// the Loop's Setpoint property.
137pub fn decode_setpoint_reference(
138    data: &[u8],
139) -> Result<Option<BACnetObjectPropertyReference>, Error> {
140    if data.is_empty() {
141        return Ok(None);
142    }
143    let (reference, after) = decode_setpoint_reference_at(data, 0)?;
144    expect_end(data, after, after, SETPOINT)?;
145    Ok(Some(reference))
146}
147
148#[cfg(test)]
149mod tests;