Skip to main content

bacnet_ops/format/
data.rs

1//! Values as typed data, for JSON: numbers as numbers, enumerations with
2//! their number and name, bit strings with their bits and set names, and
3//! constructed elements as the text the formatter writes for them.
4//!
5//! The shape is a contract of the CLI's JSON output (see the CLI
6//! reference, `docs/CLI.md`, "Value JSON"); names are kebab case, as in the
7//! text.
8
9use bacnet_encoding::constructed::{ConstructedElement, ElementShape};
10use bacnet_types::enums::{
11    PropertyIdentifier, ResolvedBits, ResolvedEnum, resolve_bits, resolve_enum,
12};
13use bacnet_types::primitives::PropertyValue;
14use serde::Serialize;
15
16use super::primitive::{date_text, hex, plain_bits, time_text};
17use super::{ValueFormat, constructed, decode_all, event_parameter, holds_state, is_binary};
18use crate::label::kebab;
19
20/// A property value as typed data. Serialized untagged, so each form is
21/// told apart by its JSON type and keys.
22#[derive(Debug, Clone, PartialEq, Serialize)]
23#[serde(untagged)]
24#[non_exhaustive]
25pub enum Datum {
26    /// NULL, as `null`.
27    Null,
28    /// A BOOLEAN, as `true` or `false`.
29    Boolean(bool),
30    /// An Unsigned, as a number.
31    Unsigned(u64),
32    /// A Signed, as a number.
33    Signed(i64),
34    /// A REAL, as a number.
35    Real(f32),
36    /// A Double, as a number.
37    Double(f64),
38    /// A REAL or Double that is not a number or infinite, which JSON
39    /// numbers can't hold: `{"float": "NaN"}`.
40    NonFinite {
41        /// `NaN`, `inf` or `-inf`.
42        float: String,
43    },
44    /// A CharacterString, as a string.
45    Text(String),
46    /// An OCTET STRING: `{"octets": "0a0b"}`, lowercase hex.
47    Octets {
48        /// The octets as hex.
49        octets: String,
50    },
51    /// An ENUMERATED: `{"value": 2, "name": "offnormal"}`, without a name
52    /// when the property's enumeration isn't known.
53    Enumerated {
54        /// The number on the wire.
55        value: u32,
56        /// Its name in kebab case.
57        #[serde(skip_serializing_if = "Option::is_none")]
58        name: Option<String>,
59    },
60    /// A BIT STRING: `{"bits": "0100", "names": ["fault"]}`, first bit
61    /// first. `names` lists the set bits by name, and is left out when the
62    /// property's bits have no names.
63    Bits {
64        /// One digit a bit.
65        bits: String,
66        /// The set bits' names in kebab case.
67        #[serde(skip_serializing_if = "Option::is_none")]
68        names: Option<Vec<String>>,
69    },
70    /// A Date: `{"date": "2026-10-19"}`, in the text's form.
71    Date {
72        /// The date as the text writes it.
73        date: String,
74    },
75    /// A Time: `{"time": "07:00"}`, in the text's form.
76    Time {
77        /// The time as the text writes it.
78        time: String,
79    },
80    /// A BACnetObjectIdentifier: `{"type": "analog-input", "type_number":
81    /// 0, "instance": 1}`.
82    Object {
83        /// The object type in kebab case.
84        #[serde(rename = "type")]
85        object_type: String,
86        /// The object type's number.
87        type_number: u32,
88        /// The instance number.
89        instance: u32,
90    },
91    /// Several values, or the elements of a list or array, as an array.
92    List(Vec<Datum>),
93    /// One constructed element (a timestamp, a schedule day, a recipient):
94    /// `{"element": "daily-schedule", "text": "Mon 07:00 = active"}`. The
95    /// element names its kind; the text is for display, not for parsing.
96    /// An element decoded field by field (an Event_Parameters value, an
97    /// event notification's values) adds them under `fields`.
98    Constructed {
99        /// The element's kind in kebab case, `destination`.
100        element: String,
101        /// The element as the text writes it.
102        text: String,
103        /// Its fields by name, when the element is decoded field by field.
104        #[serde(skip_serializing_if = "Option::is_none")]
105        fields: Option<Fields>,
106    },
107    /// Octets that don't decode: `{"raw": "0e0f"}`.
108    Raw {
109        /// The octets as hex.
110        raw: String,
111    },
112}
113
114/// The named fields of a [`Datum::Constructed`], in their wire order;
115/// JSON writes them as an object keyed by name in snake case,
116/// `{"time_delay": 30, "low_limit": 10}`.
117#[derive(Debug, Clone, Default, PartialEq)]
118pub struct Fields(Vec<(String, Datum)>);
119
120impl Fields {
121    /// Fields from `(name, value)` pairs, in order.
122    pub fn new(fields: Vec<(String, Datum)>) -> Self {
123        Self(fields)
124    }
125
126    /// The value of the field `name`.
127    pub fn get(&self, name: &str) -> Option<&Datum> {
128        self.0
129            .iter()
130            .find(|(key, _)| key == name)
131            .map(|(_, value)| value)
132    }
133
134    /// The fields in order, each its name and value.
135    pub fn iter(&self) -> impl Iterator<Item = (&str, &Datum)> {
136        self.0.iter().map(|(key, value)| (key.as_str(), value))
137    }
138
139    /// How many fields there are.
140    pub fn len(&self) -> usize {
141        self.0.len()
142    }
143
144    /// Whether there are none.
145    pub fn is_empty(&self) -> bool {
146        self.0.is_empty()
147    }
148}
149
150/// Constructed elements whose text the formatter writes from numbers,
151/// names, times and dates only: references, timestamps, addresses, scales
152/// and the like. Any other kind may quote a character string.
153pub const PLAIN_ELEMENTS: &[&str] = &[
154    "destination",
155    "port-permission",
156    "read-access-specification",
157    "device-object-reference",
158    "authentication-factor-format",
159    "stage-limit-value",
160    "device-object-property-reference",
161    "object-property-reference",
162    "recipient",
163    "calendar-entry",
164    "date-range",
165    "date-time",
166    "time-stamp",
167    "cov-subscription",
168    "value-source",
169    "scale",
170    "prescale",
171    "address-binding",
172    "shed-level",
173];
174
175impl Datum {
176    /// Whether its text may quote a character string a device wrote: a
177    /// string, or a constructed element whose kind isn't in
178    /// [`PLAIN_ELEMENTS`] or whose fields may. A front end shows such text
179    /// as data, never as its own words.
180    pub fn may_hold_device_text(&self) -> bool {
181        match self {
182            Self::Text(_) => true,
183            Self::List(items) => items.iter().any(Self::may_hold_device_text),
184            Self::Constructed {
185                element, fields, ..
186            } => {
187                !PLAIN_ELEMENTS.contains(&element.as_str())
188                    || fields
189                        .iter()
190                        .flat_map(Fields::iter)
191                        .any(|(_, datum)| datum.may_hold_device_text())
192            }
193            // The formatter's own digits, names, dates and hex.
194            Self::Null
195            | Self::Boolean(_)
196            | Self::Unsigned(_)
197            | Self::Signed(_)
198            | Self::Real(_)
199            | Self::Double(_)
200            | Self::NonFinite { .. }
201            | Self::Octets { .. }
202            | Self::Enumerated { .. }
203            | Self::Bits { .. }
204            | Self::Date { .. }
205            | Self::Time { .. }
206            | Self::Object { .. }
207            | Self::Raw { .. } => false,
208        }
209    }
210}
211
212impl Serialize for Fields {
213    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
214        use serde::ser::SerializeMap;
215        let mut map = serializer.serialize_map(Some(self.0.len()))?;
216        for (key, value) in &self.0 {
217            map.serialize_entry(key, value)?;
218        }
219        map.end()
220    }
221}
222
223impl ValueFormat<'_> {
224    /// A property's value as typed data, decoded as [`Self::value`] decodes
225    /// it: a constructed value element by element, and otherwise as
226    /// application values, one a datum and several a list.
227    pub fn data(&self, property: PropertyIdentifier, data: &[u8]) -> Datum {
228        self.data_at(property, None, data)
229    }
230
231    /// [`Self::data`] of a read at an array index, as
232    /// [`Self::value_at`] reads it.
233    pub fn data_at(&self, property: PropertyIdentifier, index: Option<u32>, data: &[u8]) -> Datum {
234        if let Some((ConstructedElement::EventParameter, shape, _)) =
235            self.element_at(property, index)
236            && let Some(mut items) = event_parameter::data(self, data)
237        {
238            return match shape {
239                ElementShape::Single if items.len() == 1 => items.remove(0),
240                _ => Datum::List(items),
241            };
242        }
243        if let Some((element, shape, texts)) = self.elements_at(property, index, data) {
244            let kind = constructed::element_name(element);
245            let mut items: Vec<Datum> = texts
246                .into_iter()
247                .map(|text| Datum::Constructed {
248                    element: kind.into(),
249                    text,
250                    fields: None,
251                })
252                .collect();
253            return match shape {
254                ElementShape::Single if items.len() == 1 => items.remove(0),
255                _ => Datum::List(items),
256            };
257        }
258        let Some(values) = decode_all(data) else {
259            return Datum::Raw { raw: hex(data, "") };
260        };
261        match values.as_slice() {
262            [one] => self.element_data(property, one),
263            many => Datum::List(
264                many.iter()
265                    .map(|value| self.element_data(property, value))
266                    .collect(),
267            ),
268        }
269    }
270
271    /// One application value of `property` as typed data.
272    pub fn element_data(&self, property: PropertyIdentifier, value: &PropertyValue) -> Datum {
273        match value {
274            PropertyValue::Null => Datum::Null,
275            PropertyValue::Boolean(b) => Datum::Boolean(*b),
276            PropertyValue::Unsigned(n) => Datum::Unsigned(*n),
277            PropertyValue::Signed(n) => Datum::Signed(i64::from(*n)),
278            PropertyValue::Real(v) if v.is_finite() => Datum::Real(*v),
279            PropertyValue::Double(v) if v.is_finite() => Datum::Double(*v),
280            PropertyValue::Real(v) => non_finite(f64::from(*v)),
281            PropertyValue::Double(v) => non_finite(*v),
282            PropertyValue::CharacterString(text) => Datum::Text(text.clone()),
283            PropertyValue::OctetString(bytes) => Datum::Octets {
284                octets: hex(bytes, ""),
285            },
286            PropertyValue::Enumerated(raw) => Datum::Enumerated {
287                value: *raw,
288                name: self.enum_name(property, value),
289            },
290            PropertyValue::BitString { .. } => Datum::Bits {
291                bits: bit_digits(value),
292                names: bit_names(property, value),
293            },
294            PropertyValue::Date(date) => Datum::Date {
295                date: date_text(date),
296            },
297            PropertyValue::Time(time) => Datum::Time {
298                time: time_text(time),
299            },
300            PropertyValue::ObjectIdentifier(object) => Datum::Object {
301                object_type: kebab(object.object_type()),
302                type_number: object.object_type().to_raw(),
303                instance: object.instance_number(),
304            },
305            PropertyValue::List(items) => Datum::List(
306                items
307                    .iter()
308                    .map(|item| self.element_data(property, item))
309                    .collect(),
310            ),
311            PropertyValue::ApplicationData(bytes) => match self.priority_date_time(property, value)
312            {
313                // A date-time slot reads as Present_Value's date and time.
314                Some((date, time)) => Datum::List(vec![
315                    Datum::Date {
316                        date: date_text(&date),
317                    },
318                    Datum::Time {
319                        time: time_text(&time),
320                    },
321                ]),
322                None => Datum::Raw {
323                    raw: hex(bytes, ""),
324                },
325            },
326        }
327    }
328
329    /// The name of an enumerated value of `property`: a binary object's
330    /// state, or the property's own enumeration.
331    fn enum_name(&self, property: PropertyIdentifier, value: &PropertyValue) -> Option<String> {
332        if let PropertyValue::Enumerated(raw @ (0 | 1)) = value
333            && holds_state(property)
334            && is_binary(self.object_type)
335        {
336            return Some(if *raw == 1 { "active" } else { "inactive" }.into());
337        }
338        match resolve_enum(property, value)? {
339            ResolvedEnum::Unknown(_) => None,
340            // A number outside the enumeration's names prints as itself.
341            named => Some(kebab(named)).filter(|name| name.parse::<u64>().is_err()),
342        }
343    }
344}
345
346fn non_finite(value: f64) -> Datum {
347    Datum::NonFinite {
348        float: value.to_string(),
349    }
350}
351
352/// A bit string's digits, first bit first: `0100`.
353fn bit_digits(value: &PropertyValue) -> String {
354    let text = plain_bits(value);
355    text.trim_start_matches("bits(")
356        .trim_end_matches(')')
357        .to_string()
358}
359
360/// The names of the set bits, when `property` names its bits.
361fn bit_names(property: PropertyIdentifier, value: &PropertyValue) -> Option<Vec<String>> {
362    fn names<'a>(flags: impl Iterator<Item = (&'a str, impl Sized)>) -> Vec<String> {
363        flags.map(|(name, _)| kebab(name)).collect()
364    }
365    Some(match resolve_bits(property, value)? {
366        ResolvedBits::StatusFlags(flags) => names(flags.iter_names()),
367        ResolvedBits::EventTransitionBits(bits) => names(bits.iter_names()),
368        ResolvedBits::LimitEnable(bits) => names(bits.iter_names()),
369        ResolvedBits::ServicesSupported(services) => services.iter().map(kebab).collect(),
370        ResolvedBits::ObjectTypesSupported(types) => types.iter().map(kebab).collect(),
371        ResolvedBits::Unknown { .. } => return None,
372    })
373}