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::{DecodeError, DecodingKind, Error};
35use bytes::BytesMut;
36
37pub use super::reference_sinks::{
38 encode_object_property_reference_to_sink, encode_setpoint_reference_to_sink,
39};
40
41use super::tagged::{
42 decode_ctx_constructed, decode_ctx_object_id, decode_ctx_unsigned, decode_optional_ctx,
43 expect_end, next_is_context,
44};
45
46const WHAT: &str = "BACnetObjectPropertyReference";
47const SETPOINT: &str = "BACnetSetpointReference";
48
49/// Encode the bare `BACnetObjectPropertyReference` member sequence:
50/// context-tagged \[0\]/\[1\] plus \[2\] when the reference is indexed.
51pub fn encode_object_property_reference(buf: &mut BytesMut, r: &BACnetObjectPropertyReference) {
52 encode_object_property_reference_to_sink(buf, r).expect("BytesMut accepts each reference");
53}
54
55/// Encode the `BACnetSetpointReference` form: the reference inside an
56/// opening/closing context tag 0 frame (the production's
57/// `setpoint-reference [0]` member).
58pub fn encode_setpoint_reference(buf: &mut BytesMut, r: &BACnetObjectPropertyReference) {
59 encode_setpoint_reference_to_sink(buf, r).expect("BytesMut accepts each reference");
60}
61
62/// Decode the bare `BACnetObjectPropertyReference` members starting at
63/// `offset`, \[0\], \[1\] and the optional \[2\]; returns the reference
64/// and the offset just past its last member.
65///
66/// The production is narrowed relative to the shared DOPR body codec: a
67/// device-qualifying \[3\] right after the members is not part of it (the
68/// Loop/Pulse Converter references are local-device only), so it is refused.
69/// Anything else that follows is the caller's to judge.
70pub fn decode_object_property_reference_at(
71 data: &[u8],
72 offset: usize,
73) -> Result<(BACnetObjectPropertyReference, usize), Error> {
74 let (object_identifier, offset) = decode_ctx_object_id(data, offset, 0, WHAT)?;
75 let (property_identifier, offset) = decode_ctx_unsigned::<u32>(data, offset, 1, WHAT)?;
76 let (property_array_index, offset) =
77 decode_optional_ctx(data, offset, 2, WHAT, decode_ctx_unsigned::<u32>)?;
78 if next_is_context(data, offset, 3)? {
79 return Err(DecodeError::new(
80 DecodingKind::InvalidEncoding,
81 offset,
82 "[3] device-identifier is not part of this production",
83 )
84 .within(WHAT)
85 .into());
86 }
87 Ok((
88 BACnetObjectPropertyReference {
89 object_identifier,
90 property_identifier,
91 property_array_index,
92 },
93 offset,
94 ))
95}
96
97/// Decode a whole property-value payload as the bare
98/// `BACnetObjectPropertyReference` member sequence: the members, read by
99/// [`decode_object_property_reference_at`], must use all of `data`.
100pub fn decode_object_property_reference(
101 data: &[u8],
102) -> Result<BACnetObjectPropertyReference, Error> {
103 let (reference, end) = decode_object_property_reference_at(data, 0)?;
104 expect_end(data, end, end, WHAT)?;
105 Ok(reference)
106}
107
108/// Decode the `BACnetSetpointReference` frame at `offset` holding a
109/// reference: opening context tag 0, the bare members, which must fill the
110/// frame, and the closing tag. Returns the reference and the offset just
111/// past the closing tag; what follows is the caller's to judge.
112///
113/// A frame with nothing inside (`0x0E 0x0F`) is the member present but
114/// holding no object or property, which this refuses like any other
115/// incomplete reference. The value without a reference has no frame at all;
116/// see [`decode_setpoint_reference`].
117pub fn decode_setpoint_reference_at(
118 data: &[u8],
119 offset: usize,
120) -> Result<(BACnetObjectPropertyReference, usize), Error> {
121 let (inner, after) = decode_ctx_constructed(data, offset, 0, SETPOINT)?;
122 let (reference, end) = decode_object_property_reference_at(inner, 0)?;
123 expect_end(inner, end, offset, SETPOINT)?;
124 Ok((reference, after))
125}
126
127/// Decode a whole property-value payload as the `BACnetSetpointReference`
128/// form, the frame read by [`decode_setpoint_reference_at`], which must use
129/// all of `data`.
130///
131/// The production's one member is optional, and an absent member has no
132/// encoding (Clause 20.2.16), so an empty payload is the value without a
133/// reference and yields `None`: Clause 12.17.16 then takes the setpoint from
134/// the Loop's Setpoint property.
135pub fn decode_setpoint_reference(
136 data: &[u8],
137) -> Result<Option<BACnetObjectPropertyReference>, Error> {
138 if data.is_empty() {
139 return Ok(None);
140 }
141 let (reference, after) = decode_setpoint_reference_at(data, 0)?;
142 expect_end(data, after, after, SETPOINT)?;
143 Ok(Some(reference))
144}
145
146#[cfg(test)]
147mod tests;