1pub mod alarms;
21mod clean;
22mod commands;
23mod constructed;
24mod data;
25mod event_parameter;
26mod fields;
27pub mod log_records;
28mod logged;
29pub mod notification;
30mod primitive;
31mod recipients;
32mod tagged;
33mod units;
34mod wire;
35
36pub use clean::{MAX_NAME_CHARS, clean_name, clean_text};
37pub(crate) use constructed::timestamp_text;
38pub use data::{Datum, Fields, PLAIN_ELEMENTS};
39pub use logged::{LogColumns, LoggedProperty, MAX_STATE_TEXT_OBJECTS};
40pub use wire::format_property_value;
41
42use bacnet_encoding::constructed::{ConstructedElement, ElementShape, property_element};
43use bacnet_encoding::primitives::{decode_application_value, decode_priority_date_time};
44use bacnet_types::enums::{
45 EngineeringUnits, ErrorClass, ErrorCode, ObjectType, PropertyIdentifier,
46};
47use bacnet_types::error::{Error, RemoteError};
48use bacnet_types::primitives::{Date, PropertyValue, Time};
49
50use units::unit_symbol;
51
52pub type RawRow = (PropertyIdentifier, Result<Vec<u8>, RowError>);
55
56#[derive(Debug, Clone, PartialEq, Eq)]
59#[non_exhaustive]
60pub enum RowError {
61 Remote(RemoteError),
64 NoValue,
67 Other(String),
69}
70
71impl RowError {
72 pub fn class_code(&self) -> Option<(ErrorClass, ErrorCode)> {
74 match self {
75 Self::Remote(RemoteError::Error { class, code }) => Some((*class, *code)),
76 _ => None,
77 }
78 }
79}
80
81impl From<Error> for RowError {
82 fn from(error: Error) -> Self {
86 match error.remote() {
87 Some(remote) => Self::Remote(remote),
88 None => Self::Other(error.to_string()),
89 }
90 }
91}
92
93impl std::error::Error for RowError {}
94
95impl std::fmt::Display for RowError {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 match self {
101 Self::Remote(RemoteError::Error { class, code }) => write!(f, "{class}:{code}"),
102 Self::Remote(remote) => write!(f, "{remote}"),
103 Self::NoValue => f.write_str("no value"),
104 Self::Other(text) => f.write_str(text),
105 }
106 }
107}
108
109const MAX_LIST_SHOWN: usize = 8;
111
112pub fn decode_all(data: &[u8]) -> Option<Vec<PropertyValue>> {
115 let mut offset = 0;
116 let mut values = Vec::new();
117 while offset < data.len() {
118 let (value, next) = decode_application_value(data, offset).ok()?;
119 values.push(value);
120 offset = next;
121 }
122 Some(values)
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub enum Floats {
128 Display,
130 Exact,
132}
133
134#[derive(Debug, Clone, Default, PartialEq, Eq)]
136pub struct StateTexts {
137 pub active: Option<String>,
139 pub inactive: Option<String>,
141 pub states: Vec<String>,
143}
144
145impl StateTexts {
146 pub fn properties(object_type: ObjectType) -> &'static [PropertyIdentifier] {
149 if is_binary(object_type) {
150 &[
151 PropertyIdentifier::ACTIVE_TEXT,
152 PropertyIdentifier::INACTIVE_TEXT,
153 ]
154 } else if is_multistate(object_type) {
155 &[PropertyIdentifier::STATE_TEXT]
156 } else {
157 &[]
158 }
159 }
160
161 pub fn name(object_type: ObjectType, property: PropertyIdentifier, index: Option<u32>) -> bool {
167 (is_binary(object_type) || is_multistate(object_type))
168 && holds_state(property)
169 && index != Some(0)
170 }
171
172 pub fn cleaned(&self) -> Self {
176 Self {
177 active: self.active.as_deref().map(clean_name),
178 inactive: self.inactive.as_deref().map(clean_name),
179 states: self.states.iter().map(|text| clean_name(text)).collect(),
180 }
181 }
182
183 pub fn from_rows(rows: &[RawRow]) -> Self {
187 let values = |property| {
188 rows.iter()
189 .find(|(p, _)| *p == property)
190 .and_then(|(_, value)| value.as_deref().ok())
191 .and_then(decode_all)
192 .unwrap_or_default()
193 .into_iter()
194 .map(|value| match value {
197 PropertyValue::CharacterString(text) => text,
198 _ => String::new(),
199 })
200 };
201 let text = |property| values(property).next().filter(|text| !text.is_empty());
202 Self {
203 active: text(PropertyIdentifier::ACTIVE_TEXT),
204 inactive: text(PropertyIdentifier::INACTIVE_TEXT),
205 states: values(PropertyIdentifier::STATE_TEXT).collect(),
206 }
207 }
208}
209
210pub(crate) fn is_binary(object_type: ObjectType) -> bool {
212 [
213 ObjectType::BINARY_INPUT,
214 ObjectType::BINARY_OUTPUT,
215 ObjectType::BINARY_VALUE,
216 ]
217 .contains(&object_type)
218}
219
220pub(crate) fn is_multistate(object_type: ObjectType) -> bool {
222 [
223 ObjectType::MULTI_STATE_INPUT,
224 ObjectType::MULTI_STATE_OUTPUT,
225 ObjectType::MULTI_STATE_VALUE,
226 ]
227 .contains(&object_type)
228}
229
230pub(crate) fn is_date_time_value(object_type: ObjectType) -> bool {
233 [
234 ObjectType::DATETIME_VALUE,
235 ObjectType::DATETIMEPATTERN_VALUE,
236 ]
237 .contains(&object_type)
238}
239
240pub(crate) fn holds_state(property: PropertyIdentifier) -> bool {
242 [
243 PropertyIdentifier::PRESENT_VALUE,
244 PropertyIdentifier::FEEDBACK_VALUE,
245 PropertyIdentifier::RELINQUISH_DEFAULT,
246 PropertyIdentifier::PRIORITY_ARRAY,
247 PropertyIdentifier::ALARM_VALUE,
248 PropertyIdentifier::ALARM_VALUES,
249 PropertyIdentifier::FAULT_VALUES,
250 ]
251 .contains(&property)
252}
253
254#[derive(Debug, Clone, Copy)]
256pub struct ValueFormat<'a> {
257 object_type: ObjectType,
258 texts: &'a StateTexts,
259 floats: Floats,
260 max_shown: usize,
262}
263
264impl<'a> ValueFormat<'a> {
265 pub fn new(object_type: ObjectType, texts: &'a StateTexts, floats: Floats) -> Self {
267 Self {
268 object_type,
269 texts,
270 floats,
271 max_shown: MAX_LIST_SHOWN,
272 }
273 }
274
275 pub fn whole_lists(self) -> Self {
278 Self {
279 max_shown: usize::MAX,
280 ..self
281 }
282 }
283
284 pub fn for_object<'b>(
287 &self,
288 object_type: ObjectType,
289 texts: &'b StateTexts,
290 ) -> ValueFormat<'b> {
291 ValueFormat {
292 object_type,
293 texts,
294 floats: self.floats,
295 max_shown: self.max_shown,
296 }
297 }
298
299 pub fn real(&self, value: f32) -> String {
301 primitive::text(
302 PropertyIdentifier::PRESENT_VALUE,
303 &PropertyValue::Real(value),
304 self.floats,
305 )
306 }
307
308 pub fn constructed_items(
312 &self,
313 property: PropertyIdentifier,
314 data: &[u8],
315 ) -> Option<Vec<String>> {
316 let (element, _) = property_element(self.object_type, property)?;
317 constructed::elements(self, element, data, 0)
318 }
319
320 pub fn value(&self, property: PropertyIdentifier, data: &[u8]) -> String {
324 self.value_at(property, None, data)
325 }
326
327 fn element_at(
332 &self,
333 property: PropertyIdentifier,
334 index: Option<u32>,
335 ) -> Option<(ConstructedElement, ElementShape, usize)> {
336 let (element, shape) = property_element(self.object_type, property)?;
337 match index {
338 None => Some((element, shape, 0)),
339 Some(0) => None,
340 Some(index) => Some((
341 element,
342 ElementShape::Single,
343 usize::try_from(index - 1).ok()?,
344 )),
345 }
346 }
347
348 fn elements_at(
351 &self,
352 property: PropertyIdentifier,
353 index: Option<u32>,
354 data: &[u8],
355 ) -> Option<(ConstructedElement, ElementShape, Vec<String>)> {
356 let (element, shape, first) = self.element_at(property, index)?;
357 let texts = constructed::elements(self, element, data, first)?;
358 Some((element, shape, texts))
359 }
360
361 pub fn value_at(
364 &self,
365 property: PropertyIdentifier,
366 index: Option<u32>,
367 data: &[u8],
368 ) -> String {
369 if let Some((element, shape, texts)) = self.elements_at(property, index, data) {
370 return match (shape, texts.as_slice()) {
371 (ElementShape::Single, [one]) => one.clone(),
372 _ => self.list(texts, constructed::separator(element)),
373 };
374 }
375 let Some(values) = decode_all(data) else {
376 return primitive::raw(data);
377 };
378 match values.as_slice() {
379 [] => "(empty)".into(),
380 [one] if index == Some(0) => primitive::text(property, one, self.floats),
382 [one] => self.element(property, one),
383 [PropertyValue::Date(date), PropertyValue::Time(time)] => {
384 primitive::date_time(date, time)
385 }
386 [PropertyValue::Date(start), PropertyValue::Date(end)] => {
387 primitive::date_range(start, end)
388 }
389 many => self.list(
390 many.iter()
391 .map(|value| self.element(property, value))
392 .collect(),
393 ", ",
394 ),
395 }
396 }
397
398 pub fn with_units(
401 &self,
402 property: PropertyIdentifier,
403 data: &[u8],
404 units: Option<EngineeringUnits>,
405 ) -> String {
406 let text = self.value(property, data);
407 let numeric = matches!(
408 decode_all(data).as_deref(),
409 Some([PropertyValue::Real(_)
410 | PropertyValue::Double(_)
411 | PropertyValue::Unsigned(_)
412 | PropertyValue::Signed(_)])
413 );
414 match units {
415 Some(units) if numeric && units != EngineeringUnits::NO_UNITS => {
416 format!("{text} {}", unit_symbol(units))
417 }
418 _ => text,
419 }
420 }
421
422 pub fn plain(&self, property: PropertyIdentifier, data: &[u8]) -> String {
425 match decode_all(data).as_deref() {
426 Some([PropertyValue::CharacterString(text)]) => text.clone(),
427 _ => self.value(property, data),
428 }
429 }
430
431 pub fn element(&self, property: PropertyIdentifier, value: &PropertyValue) -> String {
433 if holds_state(property)
434 && let Some(state) = self.state(value)
435 {
436 return state;
437 }
438 if let Some((date, time)) = self.priority_date_time(property, value) {
439 return primitive::date_time(&date, &time);
440 }
441 primitive::text(property, value, self.floats)
442 }
443
444 fn priority_date_time(
448 &self,
449 property: PropertyIdentifier,
450 value: &PropertyValue,
451 ) -> Option<(Date, Time)> {
452 let PropertyValue::ApplicationData(octets) = value else {
453 return None;
454 };
455 if !is_date_time_value(self.object_type) || property != PropertyIdentifier::PRIORITY_ARRAY {
456 return None;
457 }
458 match decode_priority_date_time(octets, 0) {
459 Ok((date_time, end)) if end == octets.len() => Some(date_time),
460 _ => None,
461 }
462 }
463
464 fn state(&self, value: &PropertyValue) -> Option<String> {
466 let named = |name: String, text: Option<&String>| match text {
467 Some(text) if !text.is_empty() => format!("{name} ({text})"),
468 _ => name,
469 };
470 match value {
471 PropertyValue::Enumerated(raw @ (0 | 1)) if is_binary(self.object_type) => {
472 Some(if *raw == 1 {
473 named("active".into(), self.texts.active.as_ref())
474 } else {
475 named("inactive".into(), self.texts.inactive.as_ref())
476 })
477 }
478 PropertyValue::Unsigned(state) if is_multistate(self.object_type) => {
479 let text = usize::try_from(*state)
480 .ok()
481 .and_then(|state| state.checked_sub(1))
482 .and_then(|index| self.texts.states.get(index));
483 Some(named(state.to_string(), text))
484 }
485 _ => None,
486 }
487 }
488}
489
490impl ValueFormat<'_> {
491 fn list(&self, texts: Vec<String>, separator: &str) -> String {
493 let count = texts.len();
494 let shown: Vec<String> = texts.into_iter().take(self.max_shown).collect();
495 match count.saturating_sub(self.max_shown) {
496 0 => format!("[{}]", shown.join(separator)),
497 more => format!(
498 "[{}{separator}+{more} more] ({count} items)",
499 shown.join(separator)
500 ),
501 }
502 }
503}
504
505pub fn raw_text(data: &[u8]) -> String {
507 primitive::raw(data)
508}
509
510pub fn object_name(rows: &[RawRow]) -> Option<String> {
513 let (_, value) = rows
514 .iter()
515 .find(|(p, _)| *p == PropertyIdentifier::OBJECT_NAME)?;
516 name_text(value.as_deref().ok()?)
517}
518
519pub fn name_text(data: &[u8]) -> Option<String> {
522 match decode_all(data)?.as_slice() {
523 [PropertyValue::CharacterString(text)] => Some(clean_name(text)),
524 _ => None,
525 }
526}
527
528pub fn units(rows: &[RawRow]) -> Option<EngineeringUnits> {
530 let (_, value) = rows.iter().find(|(p, _)| *p == PropertyIdentifier::UNITS)?;
531 match decode_all(value.as_deref().ok()?)?.as_slice() {
532 [PropertyValue::Enumerated(raw)] => Some(EngineeringUnits::from_raw(*raw)),
533 _ => None,
534 }
535}
536
537#[cfg(test)]
538mod tests;
539
540#[cfg(test)]
541mod render_tests;