Skip to main content

bacnet_types/
error.rs

1//! BACnet error types.
2//!
3//! Provides the top-level [`Error`] type used throughout the library,
4//! covering protocol errors, encoding/decoding failures, transport issues,
5//! and timeouts.
6
7#[cfg(not(feature = "std"))]
8use alloc::string::String;
9#[cfg(feature = "std")]
10use std::time::Duration;
11
12use crate::enums::{ErrorClass, ErrorCode};
13
14fn format_protocol_error(class: u32, code: u32) -> String {
15    let class_name = ErrorClass::ALL_NAMED
16        .iter()
17        .find(|(_, v)| v.to_raw() as u32 == class)
18        .map(|(n, _)| n.to_lowercase().replace('_', "-"));
19    let code_name = ErrorCode::ALL_NAMED
20        .iter()
21        .find(|(_, v)| v.to_raw() as u32 == code)
22        .map(|(n, _)| n.to_lowercase().replace('_', "-"));
23
24    match (class_name, code_name) {
25        (Some(cn), Some(co)) => format!("BACnet error: {cn} / {co}"),
26        (Some(cn), None) => format!("BACnet error: {cn} / code={code}"),
27        (None, Some(co)) => format!("BACnet error: class={class} / {co}"),
28        (None, None) => format!("BACnet error: class={class} / code={code}"),
29    }
30}
31
32/// Top-level error type for the BACnet library.
33#[derive(Debug, thiserror::Error)]
34pub enum Error {
35    /// BACnet protocol error response (Clause 20.1.7).
36    #[error("{}", format_protocol_error(*.class, *.code))]
37    Protocol {
38        /// Error class value.
39        class: u32,
40        /// Error code value.
41        code: u32,
42    },
43
44    /// BACnet reject PDU (Clause 20.1.5).
45    #[error("BACnet reject: reason={reason}")]
46    Reject {
47        /// Reject reason value.
48        reason: u8,
49    },
50
51    /// BACnet abort PDU (Clause 20.1.6).
52    #[error("BACnet abort: reason={reason}")]
53    Abort {
54        /// Abort reason value.
55        reason: u8,
56    },
57
58    /// Error encoding a PDU.
59    #[error("encoding error: {0}")]
60    Encoding(String),
61
62    /// Error decoding received data.
63    #[error("decoding error at offset {offset}: {message}")]
64    Decoding {
65        /// Byte offset where the error occurred.
66        offset: usize,
67        /// Description of what went wrong.
68        message: String,
69    },
70
71    /// Transport-level I/O error.
72    #[cfg(feature = "std")]
73    #[error("transport error: {0}")]
74    Transport(#[from] std::io::Error),
75
76    /// Request timed out.
77    #[cfg(feature = "std")]
78    #[error("request timed out after {0:?}")]
79    Timeout(Duration),
80
81    /// Segmentation assembly error.
82    #[error("segmentation error: {0}")]
83    Segmentation(String),
84
85    /// Buffer too short for the expected data.
86    #[error("buffer too short: need {need} bytes, have {have}")]
87    BufferTooShort {
88        /// Minimum bytes needed.
89        need: usize,
90        /// Bytes actually available.
91        have: usize,
92    },
93
94    /// Invalid tag encountered during decode.
95    #[error("invalid tag: {0}")]
96    InvalidTag(String),
97
98    /// Value out of valid range.
99    #[error("value out of range: {0}")]
100    OutOfRange(String),
101}
102
103/// Convenience alias for `Result<T, Error>`.
104pub type Result<T> = core::result::Result<T, Error>;
105
106impl Error {
107    /// Create a decoding error at the given byte offset.
108    pub fn decoding(offset: usize, message: impl Into<String>) -> Self {
109        Self::Decoding {
110            offset,
111            message: message.into(),
112        }
113    }
114
115    /// Create a buffer-too-short error.
116    pub fn buffer_too_short(need: usize, have: usize) -> Self {
117        Self::BufferTooShort { need, have }
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn protocol_error_display() {
127        let err = Error::Protocol { class: 2, code: 31 };
128        assert!(err.to_string().contains("property"));
129        assert!(err.to_string().contains("unknown-object"));
130
131        // Unknown class/code falls back to numeric
132        let err2 = Error::Protocol {
133            class: 999,
134            code: 999,
135        };
136        assert!(err2.to_string().contains("class=999"));
137        assert!(err2.to_string().contains("code=999"));
138    }
139
140    #[test]
141    fn decoding_error_display() {
142        let err = Error::decoding(42, "unexpected tag");
143        assert!(err.to_string().contains("offset 42"));
144        assert!(err.to_string().contains("unexpected tag"));
145    }
146
147    #[test]
148    fn buffer_too_short_display() {
149        let err = Error::buffer_too_short(10, 3);
150        assert!(err.to_string().contains("need 10"));
151        assert!(err.to_string().contains("have 3"));
152    }
153
154    #[cfg(feature = "std")]
155    #[test]
156    fn timeout_error_display() {
157        let err = Error::Timeout(Duration::from_secs(3));
158        assert!(err.to_string().contains("3s"));
159    }
160}