Skip to main content

mabi_knx/
error.rs

1//! KNX error types.
2//!
3//! This module provides comprehensive error handling for the KNX simulator,
4//! with structured error types for different failure scenarios.
5
6use std::net::SocketAddr;
7use thiserror::Error;
8
9use mabi_core::Error as CoreError;
10
11/// KNX result type.
12pub type KnxResult<T> = Result<T, KnxError>;
13
14/// KNX error types.
15#[derive(Debug, Error)]
16pub enum KnxError {
17    // ========================================================================
18    // Address Errors
19    // ========================================================================
20    /// Invalid group address format.
21    #[error("Invalid group address: {0}")]
22    InvalidGroupAddress(String),
23
24    /// Invalid individual address format.
25    #[error("Invalid individual address: {0}")]
26    InvalidIndividualAddress(String),
27
28    /// Address out of range.
29    #[error("Address out of range: {address} (valid: {valid_range})")]
30    AddressOutOfRange { address: String, valid_range: String },
31
32    // ========================================================================
33    // DPT (Datapoint Type) Errors
34    // ========================================================================
35    /// Invalid datapoint type.
36    #[error("Invalid datapoint type: {0}")]
37    InvalidDpt(String),
38
39    /// DPT encoding error.
40    #[error("DPT encoding error for {dpt}: {reason}")]
41    DptEncoding { dpt: String, reason: String },
42
43    /// DPT decoding error.
44    #[error("DPT decoding error for {dpt}: {reason}")]
45    DptDecoding { dpt: String, reason: String },
46
47    /// DPT value out of range.
48    #[error("DPT value out of range: {value} (valid: {valid_range})")]
49    DptValueOutOfRange { value: String, valid_range: String },
50
51    // ========================================================================
52    // Frame Errors
53    // ========================================================================
54    /// Frame too short.
55    #[error("Frame too short: expected at least {expected} bytes, got {actual}")]
56    FrameTooShort { expected: usize, actual: usize },
57
58    /// Invalid frame header.
59    #[error("Invalid frame header: {0}")]
60    InvalidHeader(String),
61
62    /// Invalid protocol version.
63    #[error("Invalid protocol version: expected {expected:#04x}, got {actual:#04x}")]
64    InvalidProtocolVersion { expected: u8, actual: u8 },
65
66    /// Unknown service type.
67    #[error("Unknown service type: {0:#06x}")]
68    UnknownServiceType(u16),
69
70    /// Frame length mismatch.
71    #[error("Frame length mismatch: header says {header_length}, actual is {actual_length}")]
72    FrameLengthMismatch {
73        header_length: usize,
74        actual_length: usize,
75    },
76
77    /// Invalid HPAI (Host Protocol Address Information).
78    #[error("Invalid HPAI: {0}")]
79    InvalidHpai(String),
80
81    // ========================================================================
82    // cEMI Errors
83    // ========================================================================
84    /// Unknown cEMI message code.
85    #[error("Unknown cEMI message code: {0:#04x}")]
86    UnknownMessageCode(u8),
87
88    /// Invalid cEMI frame.
89    #[error("Invalid cEMI frame: {0}")]
90    InvalidCemi(String),
91
92    /// Unknown APCI.
93    #[error("Unknown APCI: {0:#06x}")]
94    UnknownApci(u16),
95
96    // ========================================================================
97    // Connection Errors
98    // ========================================================================
99    /// Connection failed.
100    #[error("Connection failed to {address}: {reason}")]
101    ConnectionFailed { address: SocketAddr, reason: String },
102
103    /// Connection timeout.
104    #[error("Connection timeout after {timeout_ms}ms")]
105    ConnectionTimeout { timeout_ms: u64 },
106
107    /// Connection closed.
108    #[error("Connection closed: {0}")]
109    ConnectionClosed(String),
110
111    /// No more connections available.
112    #[error("No more connections available: maximum {max} reached")]
113    NoMoreConnections { max: usize },
114
115    /// Invalid channel ID.
116    #[error("Invalid channel ID: {0}")]
117    InvalidChannel(u8),
118
119    /// Sequence error.
120    #[error("Sequence error: expected {expected}, got {actual}")]
121    SequenceError { expected: u8, actual: u8 },
122
123    // ========================================================================
124    // Tunnel Errors
125    // ========================================================================
126    /// Tunnel connection error.
127    #[error("Tunnel connection error: {0}")]
128    TunnelError(String),
129
130    /// Tunnel request timeout.
131    #[error("Tunnel request timeout for channel {channel_id}")]
132    TunnelTimeout { channel_id: u8 },
133
134    /// Tunnel ACK error.
135    #[error("Tunnel ACK error: status {status:#04x}")]
136    TunnelAckError { status: u8 },
137
138    // ========================================================================
139    // Group Object Errors
140    // ========================================================================
141    /// Group object not found.
142    #[error("Group object not found: {0}")]
143    GroupObjectNotFound(String),
144
145    /// Group object write not allowed.
146    #[error("Write not allowed for group object: {0}")]
147    GroupObjectWriteNotAllowed(String),
148
149    /// Group object read not allowed.
150    #[error("Read not allowed for group object: {0}")]
151    GroupObjectReadNotAllowed(String),
152
153    // ========================================================================
154    // Server Errors
155    // ========================================================================
156    /// Server error.
157    #[error("Server error: {0}")]
158    Server(String),
159
160    /// Server not running.
161    #[error("Server not running")]
162    ServerNotRunning,
163
164    /// Server already running.
165    #[error("Server already running")]
166    ServerAlreadyRunning,
167
168    /// Bind error.
169    #[error("Failed to bind to {address}: {reason}")]
170    BindError { address: SocketAddr, reason: String },
171
172    // ========================================================================
173    // Configuration Errors
174    // ========================================================================
175    /// Configuration error.
176    #[error("Configuration error: {0}")]
177    Config(String),
178
179    /// Invalid configuration value.
180    #[error("Invalid configuration value for '{field}': {reason}")]
181    InvalidConfigValue { field: String, reason: String },
182
183    // ========================================================================
184    // Generic Errors
185    // ========================================================================
186    /// I/O error.
187    #[error("I/O error: {0}")]
188    Io(#[from] std::io::Error),
189
190    /// Core error.
191    #[error("Core error: {0}")]
192    Core(#[from] CoreError),
193
194    /// Internal error.
195    #[error("Internal error: {0}")]
196    Internal(String),
197}
198
199impl KnxError {
200    // ========================================================================
201    // Convenience constructors
202    // ========================================================================
203
204    /// Create a frame too short error.
205    pub fn frame_too_short(expected: usize, actual: usize) -> Self {
206        Self::FrameTooShort { expected, actual }
207    }
208
209    /// Create a connection failed error.
210    pub fn connection_failed(address: SocketAddr, reason: impl Into<String>) -> Self {
211        Self::ConnectionFailed {
212            address,
213            reason: reason.into(),
214        }
215    }
216
217    /// Create a DPT encoding error.
218    pub fn dpt_encoding(dpt: impl Into<String>, reason: impl Into<String>) -> Self {
219        Self::DptEncoding {
220            dpt: dpt.into(),
221            reason: reason.into(),
222        }
223    }
224
225    /// Create a DPT decoding error.
226    pub fn dpt_decoding(dpt: impl Into<String>, reason: impl Into<String>) -> Self {
227        Self::DptDecoding {
228            dpt: dpt.into(),
229            reason: reason.into(),
230        }
231    }
232
233    /// Create a sequence error.
234    pub fn sequence_error(expected: u8, actual: u8) -> Self {
235        Self::SequenceError { expected, actual }
236    }
237
238    // ========================================================================
239    // Error categorization
240    // ========================================================================
241
242    /// Check if this is a recoverable error.
243    pub fn is_recoverable(&self) -> bool {
244        matches!(
245            self,
246            Self::ConnectionTimeout { .. }
247                | Self::TunnelTimeout { .. }
248                | Self::SequenceError { .. }
249                | Self::ConnectionClosed(_)
250        )
251    }
252
253    /// Check if this is a protocol error.
254    pub fn is_protocol_error(&self) -> bool {
255        matches!(
256            self,
257            Self::FrameTooShort { .. }
258                | Self::InvalidHeader(_)
259                | Self::InvalidProtocolVersion { .. }
260                | Self::UnknownServiceType(_)
261                | Self::FrameLengthMismatch { .. }
262                | Self::UnknownMessageCode(_)
263                | Self::InvalidCemi(_)
264                | Self::UnknownApci(_)
265        )
266    }
267
268    /// Check if this is a configuration error.
269    pub fn is_config_error(&self) -> bool {
270        matches!(self, Self::Config(_) | Self::InvalidConfigValue { .. })
271    }
272}
273
274impl From<KnxError> for CoreError {
275    fn from(err: KnxError) -> Self {
276        CoreError::Protocol(err.to_string())
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn test_error_display() {
286        let err = KnxError::InvalidGroupAddress("invalid".to_string());
287        assert!(err.to_string().contains("Invalid group address"));
288    }
289
290    #[test]
291    fn test_frame_too_short() {
292        let err = KnxError::frame_too_short(10, 5);
293        assert!(err.to_string().contains("10"));
294        assert!(err.to_string().contains("5"));
295    }
296
297    #[test]
298    fn test_is_recoverable() {
299        assert!(KnxError::ConnectionTimeout { timeout_ms: 1000 }.is_recoverable());
300        assert!(!KnxError::InvalidGroupAddress("x".into()).is_recoverable());
301    }
302
303    #[test]
304    fn test_is_protocol_error() {
305        assert!(KnxError::UnknownServiceType(0x1234).is_protocol_error());
306        assert!(!KnxError::Server("test".into()).is_protocol_error());
307    }
308}