1#[cfg(not(feature = "std"))]
8use alloc::{boxed::Box, format, string::String, vec::Vec};
9#[cfg(feature = "std")]
10use std::time::Duration;
11
12use crate::constructed::BACnetObjectPropertyReference;
13use crate::data_link::DataLink;
14use crate::enums::{ErrorClass, ErrorCode, PropertyIdentifier, RejectReason};
15
16fn format_protocol_error(class: u32, code: u32) -> String {
17 let class_name = ErrorClass::ALL_NAMED
18 .iter()
19 .find(|(_, v)| v.to_raw() as u32 == class)
20 .map(|(n, _)| n.to_lowercase().replace('_', "-"));
21 let code_name = ErrorCode::ALL_NAMED
22 .iter()
23 .find(|(_, v)| v.to_raw() as u32 == code)
24 .map(|(n, _)| n.to_lowercase().replace('_', "-"));
25
26 match (class_name, code_name) {
27 (Some(cn), Some(co)) => format!("BACnet error: {cn} / {co}"),
28 (Some(cn), None) => format!("BACnet error: {cn} / code={code}"),
29 (None, Some(co)) => format!("BACnet error: class={class} / {co}"),
30 (None, None) => format!("BACnet error: class={class} / code={code}"),
31 }
32}
33
34#[derive(Debug, thiserror::Error)]
36pub enum Error {
37 #[error("{}", format_protocol_error(*.class, *.code))]
39 Protocol {
40 class: u32,
42 code: u32,
44 },
45
46 #[error("{}, {detail}", format_protocol_error(*.class, *.code))]
53 Structured {
54 class: u32,
56 code: u32,
58 detail: Box<ErrorDetail>,
61 },
62
63 #[error("BACnet reject: reason={reason}")]
65 Reject {
66 reason: u8,
68 },
69
70 #[error("BACnet abort: reason={reason}")]
72 Abort {
73 reason: u8,
75 },
76
77 #[error("message is too long for routed path to DNET {dnet}")]
79 RoutedPathTooLong {
80 dnet: u16,
82 },
83
84 #[error("routed path state capacity of {capacity} entries is exhausted")]
86 RoutedPathCapacityExceeded {
87 capacity: usize,
89 },
90
91 #[error("operation requires {required}; this transport is {actual}")]
95 UnsupportedTransport {
96 required: DataLink,
98 actual: DataLink,
100 },
101
102 #[error("encoding error: {0}")]
104 Encoding(String),
105
106 #[error("decoding error at offset {offset}: {message}")]
108 Decoding {
109 offset: usize,
111 kind: DecodingKind,
113 message: String,
115 },
116
117 #[cfg(feature = "std")]
119 #[error("transport error: {0}")]
120 Transport(#[from] std::io::Error),
121
122 #[cfg(feature = "std")]
124 #[error("request timed out after {0:?}")]
125 Timeout(Duration),
126
127 #[error("segmentation error: {0}")]
129 Segmentation(String),
130
131 #[error("buffer too short: need {need} bytes, have {have}")]
133 BufferTooShort {
134 need: usize,
136 have: usize,
138 },
139
140 #[error("value out of range: {0}")]
142 OutOfRange(String),
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
149pub enum DecodingKind {
150 InvalidEncoding,
155 OutOfRange,
158 Overflow,
161 InvalidTag,
166 Missing,
169 Trailing,
172 Unsupported,
176}
177
178impl DecodingKind {
179 pub fn reject_reason(self) -> RejectReason {
192 match self {
193 Self::InvalidEncoding => RejectReason::INVALID_DATA_ENCODING,
194 Self::OutOfRange => RejectReason::PARAMETER_OUT_OF_RANGE,
195 Self::Overflow => RejectReason::BUFFER_OVERFLOW,
196 Self::InvalidTag => RejectReason::INVALID_TAG,
197 Self::Missing => RejectReason::MISSING_REQUIRED_PARAMETER,
198 Self::Trailing => RejectReason::TOO_MANY_ARGUMENTS,
199 Self::Unsupported => RejectReason::OTHER,
200 }
201 }
202}
203
204#[derive(Debug, Clone, PartialEq, Eq)]
207pub enum ErrorDetail {
208 FirstFailedElementNumber(u32),
216 FirstFailedWriteAttempt(BACnetObjectPropertyReference),
219 FirstFailedSubscription(BACnetObjectPropertyReference),
224 PrivateTransfer {
227 vendor_id: u32,
229 service_number: u32,
231 error_parameters: Option<Vec<u8>>,
234 },
235 VtSessionIdentifiers(Vec<u8>),
239}
240
241impl core::fmt::Display for ErrorDetail {
242 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
243 fn reference(
244 f: &mut core::fmt::Formatter<'_>,
245 what: &str,
246 reference: &BACnetObjectPropertyReference,
247 ) -> core::fmt::Result {
248 write!(
249 f,
250 "{what} {} {}",
251 reference.object_identifier,
252 PropertyIdentifier::from_raw(reference.property_identifier)
253 )?;
254 match reference.property_array_index {
255 Some(index) => write!(f, "[{index}]"),
256 None => Ok(()),
257 }
258 }
259 match self {
260 Self::FirstFailedElementNumber(number) => write!(f, "first failed element {number}"),
261 Self::FirstFailedWriteAttempt(attempt) => {
262 reference(f, "first failed write attempt", attempt)
263 }
264 Self::FirstFailedSubscription(subscription) => {
265 reference(f, "first failed subscription", subscription)
266 }
267 Self::PrivateTransfer {
268 vendor_id,
269 service_number,
270 error_parameters,
271 } => {
272 write!(f, "vendor {vendor_id} service {service_number}")?;
273 match error_parameters {
274 Some(parameters) => {
275 write!(f, ", {} octets of error parameters", parameters.len())
276 }
277 None => Ok(()),
278 }
279 }
280 Self::VtSessionIdentifiers(sessions) => {
281 f.write_str("VT sessions not closed:")?;
282 for session in sessions {
283 write!(f, " {session}")?;
284 }
285 Ok(())
286 }
287 }
288 }
289}
290
291pub type Result<T> = core::result::Result<T, Error>;
293
294impl Error {
295 pub fn protocol(class: u32, code: u32, detail: Option<ErrorDetail>) -> Self {
298 match detail {
299 Some(detail) => Self::Structured {
300 class,
301 code,
302 detail: Box::new(detail),
303 },
304 None => Self::Protocol { class, code },
305 }
306 }
307
308 pub fn decoding(offset: usize, message: impl Into<String>) -> Self {
311 Self::decoding_kind(DecodingKind::InvalidEncoding, offset, message)
312 }
313
314 pub fn decoding_kind(kind: DecodingKind, offset: usize, message: impl Into<String>) -> Self {
316 Self::Decoding {
317 offset,
318 kind,
319 message: message.into(),
320 }
321 }
322
323 pub fn invalid_tag(offset: usize, message: impl Into<String>) -> Self {
326 Self::decoding_kind(DecodingKind::InvalidTag, offset, message)
327 }
328
329 pub fn missing(offset: usize, message: impl Into<String>) -> Self {
332 Self::decoding_kind(DecodingKind::Missing, offset, message)
333 }
334
335 pub fn trailing(offset: usize, message: impl Into<String>) -> Self {
338 Self::decoding_kind(DecodingKind::Trailing, offset, message)
339 }
340
341 pub fn out_of_range(offset: usize, message: impl Into<String>) -> Self {
344 Self::decoding_kind(DecodingKind::OutOfRange, offset, message)
345 }
346
347 pub fn overflow(offset: usize, message: impl Into<String>) -> Self {
350 Self::decoding_kind(DecodingKind::Overflow, offset, message)
351 }
352
353 pub fn reject_reason(&self) -> Option<RejectReason> {
362 match self {
363 Self::Reject { reason } => Some(RejectReason::from_raw(*reason)),
364 Self::Decoding { kind, .. } => Some(kind.reject_reason()),
365 Self::BufferTooShort { .. } => Some(RejectReason::MISSING_REQUIRED_PARAMETER),
366 _ => None,
367 }
368 }
369
370 pub fn into_request_reject(self) -> Self {
379 match self.reject_reason() {
380 Some(reason) => Self::Reject {
381 reason: reason.to_raw(),
382 },
383 None => self,
384 }
385 }
386
387 pub fn buffer_too_short(need: usize, have: usize) -> Self {
389 Self::BufferTooShort { need, have }
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396
397 #[test]
398 fn protocol_error_display() {
399 let err = Error::Protocol { class: 2, code: 31 };
400 assert!(err.to_string().contains("property"));
401 assert!(err.to_string().contains("unknown-object"));
402
403 let err2 = Error::Protocol {
405 class: 999,
406 code: 999,
407 };
408 assert!(err2.to_string().contains("class=999"));
409 assert!(err2.to_string().contains("code=999"));
410 }
411
412 #[test]
413 fn structured_error_display_names_the_detail() {
414 use crate::enums::ObjectType;
415 use crate::primitives::ObjectIdentifier;
416
417 let reference = |index| BACnetObjectPropertyReference {
418 object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_VALUE, 7).unwrap(),
419 property_identifier: PropertyIdentifier::PRESENT_VALUE.to_raw(),
420 property_array_index: index,
421 };
422 let private_transfer = |error_parameters| ErrorDetail::PrivateTransfer {
423 vendor_id: 555,
424 service_number: 7,
425 error_parameters,
426 };
427 for (class, code, detail, expected) in [
428 (
429 5,
430 81,
431 ErrorDetail::FirstFailedElementNumber(2),
432 "BACnet error: services / list-element-not-found, first failed element 2",
433 ),
434 (
435 2,
436 40,
437 ErrorDetail::FirstFailedWriteAttempt(reference(Some(3))),
438 "BACnet error: property / write-access-denied, \
439 first failed write attempt ANALOG_VALUE,7 PRESENT_VALUE[3]",
440 ),
441 (
442 2,
443 44,
444 ErrorDetail::FirstFailedSubscription(reference(None)),
445 "BACnet error: property / not-cov-property, \
446 first failed subscription ANALOG_VALUE,7 PRESENT_VALUE",
447 ),
448 (
449 5,
450 0,
451 private_transfer(Some(vec![0x21, 1])),
452 "BACnet error: services / other, vendor 555 service 7, \
453 2 octets of error parameters",
454 ),
455 (
456 5,
457 0,
458 private_transfer(None),
459 "BACnet error: services / other, vendor 555 service 7",
460 ),
461 (
462 5,
463 39,
464 ErrorDetail::VtSessionIdentifiers(vec![1, 4]),
465 "BACnet error: services / vt-session-termination-failure, \
466 VT sessions not closed: 1 4",
467 ),
468 ] {
469 assert_eq!(
470 Error::protocol(class, code, Some(detail)).to_string(),
471 expected
472 );
473 }
474 assert!(matches!(
475 Error::protocol(2, 31, None),
476 Error::Protocol { class: 2, code: 31 }
477 ));
478 }
479
480 #[test]
481 fn decoding_error_display() {
482 let err = Error::decoding(42, "unexpected tag");
483 assert!(err.to_string().contains("offset 42"));
484 assert!(err.to_string().contains("unexpected tag"));
485 }
486
487 #[test]
488 fn syntax_faults_name_their_reject_reasons() {
489 for (error, reason) in [
490 (
491 Error::decoding(1, "bad"),
492 RejectReason::INVALID_DATA_ENCODING,
493 ),
494 (
495 Error::out_of_range(1, "wide"),
496 RejectReason::PARAMETER_OUT_OF_RANGE,
497 ),
498 (Error::overflow(1, "many"), RejectReason::BUFFER_OVERFLOW),
499 (
500 Error::decoding_kind(DecodingKind::Unsupported, 1, "DBCS"),
501 RejectReason::OTHER,
502 ),
503 (Error::invalid_tag(1, "tag"), RejectReason::INVALID_TAG),
504 (
505 Error::missing(1, "gone"),
506 RejectReason::MISSING_REQUIRED_PARAMETER,
507 ),
508 (Error::trailing(1, "more"), RejectReason::TOO_MANY_ARGUMENTS),
509 (
510 Error::buffer_too_short(9, 4),
511 RejectReason::MISSING_REQUIRED_PARAMETER,
512 ),
513 (
514 Error::Reject { reason: 8 },
515 RejectReason::UNDEFINED_ENUMERATION,
516 ),
517 ] {
518 assert_eq!(error.reject_reason(), Some(reason), "{error}");
519 }
520 for error in [
521 Error::Protocol { class: 5, code: 0 },
522 Error::Encoding("x".into()),
523 Error::OutOfRange("x".into()),
524 Error::Abort { reason: 1 },
525 ] {
526 assert_eq!(error.reject_reason(), None, "{error}");
527 }
528 assert!(matches!(
530 Error::trailing(3, "more").into_request_reject(),
531 Error::Reject { reason } if reason == RejectReason::TOO_MANY_ARGUMENTS.to_raw()
532 ));
533 assert!(matches!(
534 Error::OutOfRange("x".into()).into_request_reject(),
535 Error::OutOfRange(_)
536 ));
537 assert_eq!(
539 Error::trailing(3, "two trailing byte(s)").to_string(),
540 "decoding error at offset 3: two trailing byte(s)"
541 );
542 }
543
544 #[test]
545 fn buffer_too_short_display() {
546 let err = Error::buffer_too_short(10, 3);
547 assert!(err.to_string().contains("need 10"));
548 assert!(err.to_string().contains("have 3"));
549 }
550
551 #[cfg(feature = "std")]
552 #[test]
553 fn timeout_error_display() {
554 let err = Error::Timeout(Duration::from_secs(3));
555 assert!(err.to_string().contains("3s"));
556 }
557
558 #[test]
559 fn routed_path_too_long_display_preserves_dnet() {
560 let err = Error::RoutedPathTooLong { dnet: 1234 };
561 assert!(err.to_string().contains("1234"));
562 }
563
564 #[test]
565 fn routed_path_capacity_display_preserves_bound() {
566 let err = Error::RoutedPathCapacityExceeded { capacity: 256 };
567 assert!(err.to_string().contains("256"));
568 }
569
570 #[test]
571 fn unsupported_transport_display_names_both_data_links() {
572 let err = Error::UnsupportedTransport {
573 required: DataLink::Bip,
574 actual: DataLink::Mstp,
575 };
576 assert_eq!(
577 err.to_string(),
578 "operation requires BACnet/IP; this transport is MS/TP"
579 );
580 }
581}