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::{format, 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    /// A router reported that the active message was too long for a routed path.
59    #[error("message is too long for routed path to DNET {dnet}")]
60    RoutedPathTooLong {
61        /// Destination network rejected by the router.
62        dnet: u16,
63    },
64
65    /// The client cannot safely allocate state for another routed path.
66    #[error("routed path state capacity of {capacity} entries is exhausted")]
67    RoutedPathCapacityExceeded {
68        /// Maximum number of immediate-router/DNET path entries retained.
69        capacity: usize,
70    },
71
72    /// Error encoding a PDU.
73    #[error("encoding error: {0}")]
74    Encoding(String),
75
76    /// Error decoding received data.
77    #[error("decoding error at offset {offset}: {message}")]
78    Decoding {
79        /// Byte offset where the error occurred.
80        offset: usize,
81        /// Description of what went wrong.
82        message: String,
83    },
84
85    /// Transport-level I/O error.
86    #[cfg(feature = "std")]
87    #[error("transport error: {0}")]
88    Transport(#[from] std::io::Error),
89
90    /// Request timed out.
91    #[cfg(feature = "std")]
92    #[error("request timed out after {0:?}")]
93    Timeout(Duration),
94
95    /// Segmentation assembly error.
96    #[error("segmentation error: {0}")]
97    Segmentation(String),
98
99    /// Buffer too short for the expected data.
100    #[error("buffer too short: need {need} bytes, have {have}")]
101    BufferTooShort {
102        /// Minimum bytes needed.
103        need: usize,
104        /// Bytes actually available.
105        have: usize,
106    },
107
108    /// Invalid tag encountered during decode.
109    #[error("invalid tag: {0}")]
110    InvalidTag(String),
111
112    /// Value out of valid range.
113    #[error("value out of range: {0}")]
114    OutOfRange(String),
115}
116
117/// Convenience alias for `Result<T, Error>`.
118pub type Result<T> = core::result::Result<T, Error>;
119
120impl Error {
121    /// Create a decoding error at the given byte offset.
122    pub fn decoding(offset: usize, message: impl Into<String>) -> Self {
123        Self::Decoding {
124            offset,
125            message: message.into(),
126        }
127    }
128
129    /// Create a buffer-too-short error.
130    pub fn buffer_too_short(need: usize, have: usize) -> Self {
131        Self::BufferTooShort { need, have }
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn protocol_error_display() {
141        let err = Error::Protocol { class: 2, code: 31 };
142        assert!(err.to_string().contains("property"));
143        assert!(err.to_string().contains("unknown-object"));
144
145        // Unknown class/code falls back to numeric
146        let err2 = Error::Protocol {
147            class: 999,
148            code: 999,
149        };
150        assert!(err2.to_string().contains("class=999"));
151        assert!(err2.to_string().contains("code=999"));
152    }
153
154    #[test]
155    fn decoding_error_display() {
156        let err = Error::decoding(42, "unexpected tag");
157        assert!(err.to_string().contains("offset 42"));
158        assert!(err.to_string().contains("unexpected tag"));
159    }
160
161    #[test]
162    fn buffer_too_short_display() {
163        let err = Error::buffer_too_short(10, 3);
164        assert!(err.to_string().contains("need 10"));
165        assert!(err.to_string().contains("have 3"));
166    }
167
168    #[cfg(feature = "std")]
169    #[test]
170    fn timeout_error_display() {
171        let err = Error::Timeout(Duration::from_secs(3));
172        assert!(err.to_string().contains("3s"));
173    }
174
175    #[test]
176    fn routed_path_too_long_display_preserves_dnet() {
177        let err = Error::RoutedPathTooLong { dnet: 1234 };
178        assert!(err.to_string().contains("1234"));
179    }
180
181    #[test]
182    fn routed_path_capacity_display_preserves_bound() {
183        let err = Error::RoutedPathCapacityExceeded { capacity: 256 };
184        assert!(err.to_string().contains("256"));
185    }
186}