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 {
31        address: String,
32        valid_range: String,
33    },
34
35    // ========================================================================
36    // DPT (Datapoint Type) Errors
37    // ========================================================================
38    /// Invalid datapoint type.
39    #[error("Invalid datapoint type: {0}")]
40    InvalidDpt(String),
41
42    /// DPT encoding error.
43    #[error("DPT encoding error for {dpt}: {reason}")]
44    DptEncoding { dpt: String, reason: String },
45
46    /// DPT decoding error.
47    #[error("DPT decoding error for {dpt}: {reason}")]
48    DptDecoding { dpt: String, reason: String },
49
50    /// DPT value out of range.
51    #[error("DPT value out of range: {value} (valid: {valid_range})")]
52    DptValueOutOfRange { value: String, valid_range: String },
53
54    // ========================================================================
55    // Frame Errors
56    // ========================================================================
57    /// Frame too short.
58    #[error("Frame too short: expected at least {expected} bytes, got {actual}")]
59    FrameTooShort { expected: usize, actual: usize },
60
61    /// Invalid frame header.
62    #[error("Invalid frame header: {0}")]
63    InvalidHeader(String),
64
65    /// Invalid protocol version.
66    #[error("Invalid protocol version: expected {expected:#04x}, got {actual:#04x}")]
67    InvalidProtocolVersion { expected: u8, actual: u8 },
68
69    /// Unknown service type.
70    #[error("Unknown service type: {0:#06x}")]
71    UnknownServiceType(u16),
72
73    /// Frame length mismatch.
74    #[error("Frame length mismatch: header says {header_length}, actual is {actual_length}")]
75    FrameLengthMismatch {
76        header_length: usize,
77        actual_length: usize,
78    },
79
80    /// Invalid HPAI (Host Protocol Address Information).
81    #[error("Invalid HPAI: {0}")]
82    InvalidHpai(String),
83
84    // ========================================================================
85    // cEMI Errors
86    // ========================================================================
87    /// Unknown cEMI message code.
88    #[error("Unknown cEMI message code: {0:#04x}")]
89    UnknownMessageCode(u8),
90
91    /// Invalid cEMI frame.
92    #[error("Invalid cEMI frame: {0}")]
93    InvalidCemi(String),
94
95    /// Unknown APCI.
96    #[error("Unknown APCI: {0:#06x}")]
97    UnknownApci(u16),
98
99    // ========================================================================
100    // Connection Errors
101    // ========================================================================
102    /// Connection failed.
103    #[error("Connection failed to {address}: {reason}")]
104    ConnectionFailed { address: SocketAddr, reason: String },
105
106    /// Connection timeout.
107    #[error("Connection timeout after {timeout_ms}ms")]
108    ConnectionTimeout { timeout_ms: u64 },
109
110    /// Connection closed.
111    #[error("Connection closed: {0}")]
112    ConnectionClosed(String),
113
114    /// No more connections available.
115    #[error("No more connections available: maximum {max} reached")]
116    NoMoreConnections { max: usize },
117
118    /// Invalid channel ID.
119    #[error("Invalid channel ID: {0}")]
120    InvalidChannel(u8),
121
122    /// Sequence error.
123    #[error("Sequence error: expected {expected}, got {actual}")]
124    SequenceError { expected: u8, actual: u8 },
125
126    /// Duplicate frame detected.
127    #[error("Duplicate frame: sequence {sequence}, expected {expected}")]
128    DuplicateFrame { sequence: u8, expected: u8 },
129
130    /// Out-of-order frame detected.
131    #[error("Out-of-order frame: sequence {sequence}, expected {expected}, distance {distance}")]
132    OutOfOrderFrame {
133        sequence: u8,
134        expected: u8,
135        distance: u8,
136    },
137
138    /// Fatal sequence desync (knxd: seqno >= rno + 5).
139    #[error("Fatal sequence desync: sequence {sequence}, expected {expected}, distance {distance} — tunnel restart required")]
140    FatalDesync {
141        sequence: u8,
142        expected: u8,
143        distance: u8,
144    },
145
146    /// Send error threshold exceeded.
147    #[error("Send error threshold exceeded: {consecutive_errors} consecutive errors (threshold: {threshold})")]
148    SendErrorThresholdExceeded {
149        consecutive_errors: u32,
150        threshold: u32,
151    },
152
153    // ========================================================================
154    // Tunnel Errors
155    // ========================================================================
156    /// Tunnel connection error.
157    #[error("Tunnel connection error: {0}")]
158    TunnelError(String),
159
160    /// Tunnel request timeout.
161    #[error("Tunnel request timeout for channel {channel_id}")]
162    TunnelTimeout { channel_id: u8 },
163
164    /// Tunnel ACK error.
165    #[error("Tunnel ACK error: status {status:#04x}")]
166    TunnelAckError { status: u8 },
167
168    /// Tunnel ACK timeout after retries.
169    #[error("Tunnel ACK timeout: channel {channel_id}, sequence {sequence}, attempts {attempts}")]
170    AckTimeout {
171        channel_id: u8,
172        sequence: u8,
173        attempts: u8,
174    },
175
176    /// L_Data.con confirmation failure (Ctrl1 bit 0 = 1).
177    #[error("L_Data.con NACK: bus delivery failed for channel {channel_id}")]
178    ConfirmationNack { channel_id: u8 },
179
180    /// L_Data.con confirmation timeout.
181    #[error("L_Data.con timeout: channel {channel_id}, sequence {sequence}")]
182    ConfirmationTimeout { channel_id: u8, sequence: u8 },
183
184    /// Tunnel state transition error.
185    #[error("Invalid tunnel state transition: {from} -> {to}")]
186    InvalidStateTransition { from: String, to: String },
187
188    /// Channel ID mismatch.
189    #[error("Channel ID mismatch: expected {expected}, got {actual}")]
190    ChannelMismatch { expected: u8, actual: u8 },
191
192    // ========================================================================
193    // Flow Control Errors
194    // ========================================================================
195    /// Frame dropped by flow control filter.
196    #[error("Flow control: frame dropped — {reason}")]
197    FlowControlDrop { reason: String },
198
199    /// Frame queued by flow control (not an error, informational).
200    #[error("Flow control: frame queued for channel {channel_id}")]
201    FlowControlQueued { channel_id: u8 },
202
203    /// Circuit breaker is open, dropping frames.
204    #[error("Circuit breaker open: {consecutive_failures} consecutive failures (threshold: {threshold})")]
205    CircuitBreakerOpen {
206        consecutive_failures: u32,
207        threshold: u32,
208    },
209
210    /// PaceFilter delay exceeded maximum allowed.
211    #[error("Pace filter: delay {delay_ms}ms exceeds maximum {max_delay_ms}ms")]
212    PaceFilterDelayExceeded { delay_ms: u64, max_delay_ms: u64 },
213
214    // ========================================================================
215    // Group Object Errors
216    // ========================================================================
217    /// Group object not found.
218    #[error("Group object not found: {0}")]
219    GroupObjectNotFound(String),
220
221    /// Group object write not allowed.
222    #[error("Write not allowed for group object: {0}")]
223    GroupObjectWriteNotAllowed(String),
224
225    /// Group object read not allowed.
226    #[error("Read not allowed for group object: {0}")]
227    GroupObjectReadNotAllowed(String),
228
229    // ========================================================================
230    // Server Errors
231    // ========================================================================
232    /// Server error.
233    #[error("Server error: {0}")]
234    Server(String),
235
236    /// Server not running.
237    #[error("Server not running")]
238    ServerNotRunning,
239
240    /// Server already running.
241    #[error("Server already running")]
242    ServerAlreadyRunning,
243
244    /// Bind error.
245    #[error("Failed to bind to {address}: {reason}")]
246    BindError { address: SocketAddr, reason: String },
247
248    // ========================================================================
249    // Configuration Errors
250    // ========================================================================
251    /// Configuration error.
252    #[error("Configuration error: {0}")]
253    Config(String),
254
255    /// Invalid configuration value.
256    #[error("Invalid configuration value for '{field}': {reason}")]
257    InvalidConfigValue { field: String, reason: String },
258
259    // ========================================================================
260    // Generic Errors
261    // ========================================================================
262    /// I/O error.
263    #[error("I/O error: {0}")]
264    Io(#[from] std::io::Error),
265
266    /// Core error.
267    #[error("Core error: {0}")]
268    Core(#[from] CoreError),
269
270    /// Internal error.
271    #[error("Internal error: {0}")]
272    Internal(String),
273}
274
275impl KnxError {
276    // ========================================================================
277    // Convenience constructors
278    // ========================================================================
279
280    /// Create a frame too short error.
281    pub fn frame_too_short(expected: usize, actual: usize) -> Self {
282        Self::FrameTooShort { expected, actual }
283    }
284
285    /// Create a connection failed error.
286    pub fn connection_failed(address: SocketAddr, reason: impl Into<String>) -> Self {
287        Self::ConnectionFailed {
288            address,
289            reason: reason.into(),
290        }
291    }
292
293    /// Create a DPT encoding error.
294    pub fn dpt_encoding(dpt: impl Into<String>, reason: impl Into<String>) -> Self {
295        Self::DptEncoding {
296            dpt: dpt.into(),
297            reason: reason.into(),
298        }
299    }
300
301    /// Create a DPT decoding error.
302    pub fn dpt_decoding(dpt: impl Into<String>, reason: impl Into<String>) -> Self {
303        Self::DptDecoding {
304            dpt: dpt.into(),
305            reason: reason.into(),
306        }
307    }
308
309    /// Create a sequence error.
310    pub fn sequence_error(expected: u8, actual: u8) -> Self {
311        Self::SequenceError { expected, actual }
312    }
313
314    // ========================================================================
315    // Error categorization
316    // ========================================================================
317
318    /// Check if this is a recoverable error.
319    pub fn is_recoverable(&self) -> bool {
320        matches!(
321            self,
322            Self::ConnectionTimeout { .. }
323                | Self::TunnelTimeout { .. }
324                | Self::SequenceError { .. }
325                | Self::ConnectionClosed(_)
326                | Self::DuplicateFrame { .. }
327                | Self::OutOfOrderFrame { .. }
328                | Self::AckTimeout { .. }
329                | Self::ConfirmationNack { .. }
330                | Self::ConfirmationTimeout { .. }
331                | Self::FlowControlDrop { .. }
332                | Self::FlowControlQueued { .. }
333                | Self::PaceFilterDelayExceeded { .. }
334        )
335    }
336
337    /// Check if this is a flow control related error.
338    pub fn is_flow_control_error(&self) -> bool {
339        matches!(
340            self,
341            Self::FlowControlDrop { .. }
342                | Self::FlowControlQueued { .. }
343                | Self::CircuitBreakerOpen { .. }
344                | Self::PaceFilterDelayExceeded { .. }
345        )
346    }
347
348    /// Check if this requires tunnel restart.
349    pub fn requires_tunnel_restart(&self) -> bool {
350        matches!(
351            self,
352            Self::FatalDesync { .. } | Self::SendErrorThresholdExceeded { .. }
353        )
354    }
355
356    /// Check if this is a protocol error.
357    pub fn is_protocol_error(&self) -> bool {
358        matches!(
359            self,
360            Self::FrameTooShort { .. }
361                | Self::InvalidHeader(_)
362                | Self::InvalidProtocolVersion { .. }
363                | Self::UnknownServiceType(_)
364                | Self::FrameLengthMismatch { .. }
365                | Self::UnknownMessageCode(_)
366                | Self::InvalidCemi(_)
367                | Self::UnknownApci(_)
368        )
369    }
370
371    /// Check if this is a configuration error.
372    pub fn is_config_error(&self) -> bool {
373        matches!(self, Self::Config(_) | Self::InvalidConfigValue { .. })
374    }
375}
376
377impl From<KnxError> for CoreError {
378    fn from(err: KnxError) -> Self {
379        CoreError::Protocol(err.to_string())
380    }
381}
382
383#[cfg(test)]
384mod tests {
385    use super::*;
386
387    #[test]
388    fn test_error_display() {
389        let err = KnxError::InvalidGroupAddress("invalid".to_string());
390        assert!(err.to_string().contains("Invalid group address"));
391    }
392
393    #[test]
394    fn test_frame_too_short() {
395        let err = KnxError::frame_too_short(10, 5);
396        assert!(err.to_string().contains("10"));
397        assert!(err.to_string().contains("5"));
398    }
399
400    #[test]
401    fn test_is_recoverable() {
402        assert!(KnxError::ConnectionTimeout { timeout_ms: 1000 }.is_recoverable());
403        assert!(!KnxError::InvalidGroupAddress("x".into()).is_recoverable());
404    }
405
406    #[test]
407    fn test_is_protocol_error() {
408        assert!(KnxError::UnknownServiceType(0x1234).is_protocol_error());
409        assert!(!KnxError::Server("test".into()).is_protocol_error());
410    }
411}