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    /// Duplicate frame detected.
124    #[error("Duplicate frame: sequence {sequence}, expected {expected}")]
125    DuplicateFrame { sequence: u8, expected: u8 },
126
127    /// Out-of-order frame detected.
128    #[error("Out-of-order frame: sequence {sequence}, expected {expected}, distance {distance}")]
129    OutOfOrderFrame {
130        sequence: u8,
131        expected: u8,
132        distance: u8,
133    },
134
135    /// Fatal sequence desync (knxd: seqno >= rno + 5).
136    #[error("Fatal sequence desync: sequence {sequence}, expected {expected}, distance {distance} — tunnel restart required")]
137    FatalDesync {
138        sequence: u8,
139        expected: u8,
140        distance: u8,
141    },
142
143    /// Send error threshold exceeded.
144    #[error("Send error threshold exceeded: {consecutive_errors} consecutive errors (threshold: {threshold})")]
145    SendErrorThresholdExceeded {
146        consecutive_errors: u32,
147        threshold: u32,
148    },
149
150    // ========================================================================
151    // Tunnel Errors
152    // ========================================================================
153    /// Tunnel connection error.
154    #[error("Tunnel connection error: {0}")]
155    TunnelError(String),
156
157    /// Tunnel request timeout.
158    #[error("Tunnel request timeout for channel {channel_id}")]
159    TunnelTimeout { channel_id: u8 },
160
161    /// Tunnel ACK error.
162    #[error("Tunnel ACK error: status {status:#04x}")]
163    TunnelAckError { status: u8 },
164
165    /// Tunnel ACK timeout after retries.
166    #[error("Tunnel ACK timeout: channel {channel_id}, sequence {sequence}, attempts {attempts}")]
167    AckTimeout {
168        channel_id: u8,
169        sequence: u8,
170        attempts: u8,
171    },
172
173    /// L_Data.con confirmation failure (Ctrl1 bit 0 = 1).
174    #[error("L_Data.con NACK: bus delivery failed for channel {channel_id}")]
175    ConfirmationNack { channel_id: u8 },
176
177    /// L_Data.con confirmation timeout.
178    #[error("L_Data.con timeout: channel {channel_id}, sequence {sequence}")]
179    ConfirmationTimeout { channel_id: u8, sequence: u8 },
180
181    /// Tunnel state transition error.
182    #[error("Invalid tunnel state transition: {from} -> {to}")]
183    InvalidStateTransition { from: String, to: String },
184
185    /// Channel ID mismatch.
186    #[error("Channel ID mismatch: expected {expected}, got {actual}")]
187    ChannelMismatch { expected: u8, actual: u8 },
188
189    // ========================================================================
190    // Flow Control Errors
191    // ========================================================================
192    /// Frame dropped by flow control filter.
193    #[error("Flow control: frame dropped — {reason}")]
194    FlowControlDrop { reason: String },
195
196    /// Frame queued by flow control (not an error, informational).
197    #[error("Flow control: frame queued for channel {channel_id}")]
198    FlowControlQueued { channel_id: u8 },
199
200    /// Circuit breaker is open, dropping frames.
201    #[error("Circuit breaker open: {consecutive_failures} consecutive failures (threshold: {threshold})")]
202    CircuitBreakerOpen {
203        consecutive_failures: u32,
204        threshold: u32,
205    },
206
207    /// PaceFilter delay exceeded maximum allowed.
208    #[error("Pace filter: delay {delay_ms}ms exceeds maximum {max_delay_ms}ms")]
209    PaceFilterDelayExceeded { delay_ms: u64, max_delay_ms: u64 },
210
211    // ========================================================================
212    // Group Object Errors
213    // ========================================================================
214    /// Group object not found.
215    #[error("Group object not found: {0}")]
216    GroupObjectNotFound(String),
217
218    /// Group object write not allowed.
219    #[error("Write not allowed for group object: {0}")]
220    GroupObjectWriteNotAllowed(String),
221
222    /// Group object read not allowed.
223    #[error("Read not allowed for group object: {0}")]
224    GroupObjectReadNotAllowed(String),
225
226    // ========================================================================
227    // Server Errors
228    // ========================================================================
229    /// Server error.
230    #[error("Server error: {0}")]
231    Server(String),
232
233    /// Server not running.
234    #[error("Server not running")]
235    ServerNotRunning,
236
237    /// Server already running.
238    #[error("Server already running")]
239    ServerAlreadyRunning,
240
241    /// Bind error.
242    #[error("Failed to bind to {address}: {reason}")]
243    BindError { address: SocketAddr, reason: String },
244
245    // ========================================================================
246    // Configuration Errors
247    // ========================================================================
248    /// Configuration error.
249    #[error("Configuration error: {0}")]
250    Config(String),
251
252    /// Invalid configuration value.
253    #[error("Invalid configuration value for '{field}': {reason}")]
254    InvalidConfigValue { field: String, reason: String },
255
256    // ========================================================================
257    // Generic Errors
258    // ========================================================================
259    /// I/O error.
260    #[error("I/O error: {0}")]
261    Io(#[from] std::io::Error),
262
263    /// Core error.
264    #[error("Core error: {0}")]
265    Core(#[from] CoreError),
266
267    /// Internal error.
268    #[error("Internal error: {0}")]
269    Internal(String),
270}
271
272impl KnxError {
273    // ========================================================================
274    // Convenience constructors
275    // ========================================================================
276
277    /// Create a frame too short error.
278    pub fn frame_too_short(expected: usize, actual: usize) -> Self {
279        Self::FrameTooShort { expected, actual }
280    }
281
282    /// Create a connection failed error.
283    pub fn connection_failed(address: SocketAddr, reason: impl Into<String>) -> Self {
284        Self::ConnectionFailed {
285            address,
286            reason: reason.into(),
287        }
288    }
289
290    /// Create a DPT encoding error.
291    pub fn dpt_encoding(dpt: impl Into<String>, reason: impl Into<String>) -> Self {
292        Self::DptEncoding {
293            dpt: dpt.into(),
294            reason: reason.into(),
295        }
296    }
297
298    /// Create a DPT decoding error.
299    pub fn dpt_decoding(dpt: impl Into<String>, reason: impl Into<String>) -> Self {
300        Self::DptDecoding {
301            dpt: dpt.into(),
302            reason: reason.into(),
303        }
304    }
305
306    /// Create a sequence error.
307    pub fn sequence_error(expected: u8, actual: u8) -> Self {
308        Self::SequenceError { expected, actual }
309    }
310
311    // ========================================================================
312    // Error categorization
313    // ========================================================================
314
315    /// Check if this is a recoverable error.
316    pub fn is_recoverable(&self) -> bool {
317        matches!(
318            self,
319            Self::ConnectionTimeout { .. }
320                | Self::TunnelTimeout { .. }
321                | Self::SequenceError { .. }
322                | Self::ConnectionClosed(_)
323                | Self::DuplicateFrame { .. }
324                | Self::OutOfOrderFrame { .. }
325                | Self::AckTimeout { .. }
326                | Self::ConfirmationNack { .. }
327                | Self::ConfirmationTimeout { .. }
328                | Self::FlowControlDrop { .. }
329                | Self::FlowControlQueued { .. }
330                | Self::PaceFilterDelayExceeded { .. }
331        )
332    }
333
334    /// Check if this is a flow control related error.
335    pub fn is_flow_control_error(&self) -> bool {
336        matches!(
337            self,
338            Self::FlowControlDrop { .. }
339                | Self::FlowControlQueued { .. }
340                | Self::CircuitBreakerOpen { .. }
341                | Self::PaceFilterDelayExceeded { .. }
342        )
343    }
344
345    /// Check if this requires tunnel restart.
346    pub fn requires_tunnel_restart(&self) -> bool {
347        matches!(
348            self,
349            Self::FatalDesync { .. }
350                | Self::SendErrorThresholdExceeded { .. }
351        )
352    }
353
354    /// Check if this is a protocol error.
355    pub fn is_protocol_error(&self) -> bool {
356        matches!(
357            self,
358            Self::FrameTooShort { .. }
359                | Self::InvalidHeader(_)
360                | Self::InvalidProtocolVersion { .. }
361                | Self::UnknownServiceType(_)
362                | Self::FrameLengthMismatch { .. }
363                | Self::UnknownMessageCode(_)
364                | Self::InvalidCemi(_)
365                | Self::UnknownApci(_)
366        )
367    }
368
369    /// Check if this is a configuration error.
370    pub fn is_config_error(&self) -> bool {
371        matches!(self, Self::Config(_) | Self::InvalidConfigValue { .. })
372    }
373}
374
375impl From<KnxError> for CoreError {
376    fn from(err: KnxError) -> Self {
377        CoreError::Protocol(err.to_string())
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384
385    #[test]
386    fn test_error_display() {
387        let err = KnxError::InvalidGroupAddress("invalid".to_string());
388        assert!(err.to_string().contains("Invalid group address"));
389    }
390
391    #[test]
392    fn test_frame_too_short() {
393        let err = KnxError::frame_too_short(10, 5);
394        assert!(err.to_string().contains("10"));
395        assert!(err.to_string().contains("5"));
396    }
397
398    #[test]
399    fn test_is_recoverable() {
400        assert!(KnxError::ConnectionTimeout { timeout_ms: 1000 }.is_recoverable());
401        assert!(!KnxError::InvalidGroupAddress("x".into()).is_recoverable());
402    }
403
404    #[test]
405    fn test_is_protocol_error() {
406        assert!(KnxError::UnknownServiceType(0x1234).is_protocol_error());
407        assert!(!KnxError::Server("test".into()).is_protocol_error());
408    }
409}