1use std::net::SocketAddr;
7use thiserror::Error;
8
9use mabi_core::Error as CoreError;
10
11pub type KnxResult<T> = Result<T, KnxError>;
13
14#[derive(Debug, Error)]
16pub enum KnxError {
17 #[error("Invalid group address: {0}")]
22 InvalidGroupAddress(String),
23
24 #[error("Invalid individual address: {0}")]
26 InvalidIndividualAddress(String),
27
28 #[error("Address out of range: {address} (valid: {valid_range})")]
30 AddressOutOfRange {
31 address: String,
32 valid_range: String,
33 },
34
35 #[error("Invalid datapoint type: {0}")]
40 InvalidDpt(String),
41
42 #[error("DPT encoding error for {dpt}: {reason}")]
44 DptEncoding { dpt: String, reason: String },
45
46 #[error("DPT decoding error for {dpt}: {reason}")]
48 DptDecoding { dpt: String, reason: String },
49
50 #[error("DPT value out of range: {value} (valid: {valid_range})")]
52 DptValueOutOfRange { value: String, valid_range: String },
53
54 #[error("Frame too short: expected at least {expected} bytes, got {actual}")]
59 FrameTooShort { expected: usize, actual: usize },
60
61 #[error("Invalid frame header: {0}")]
63 InvalidHeader(String),
64
65 #[error("Invalid protocol version: expected {expected:#04x}, got {actual:#04x}")]
67 InvalidProtocolVersion { expected: u8, actual: u8 },
68
69 #[error("Unknown service type: {0:#06x}")]
71 UnknownServiceType(u16),
72
73 #[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 #[error("Invalid HPAI: {0}")]
82 InvalidHpai(String),
83
84 #[error("Unknown cEMI message code: {0:#04x}")]
89 UnknownMessageCode(u8),
90
91 #[error("Invalid cEMI frame: {0}")]
93 InvalidCemi(String),
94
95 #[error("Unknown APCI: {0:#06x}")]
97 UnknownApci(u16),
98
99 #[error("Connection failed to {address}: {reason}")]
104 ConnectionFailed { address: SocketAddr, reason: String },
105
106 #[error("Connection timeout after {timeout_ms}ms")]
108 ConnectionTimeout { timeout_ms: u64 },
109
110 #[error("Connection closed: {0}")]
112 ConnectionClosed(String),
113
114 #[error("No more connections available: maximum {max} reached")]
116 NoMoreConnections { max: usize },
117
118 #[error("Invalid channel ID: {0}")]
120 InvalidChannel(u8),
121
122 #[error("Sequence error: expected {expected}, got {actual}")]
124 SequenceError { expected: u8, actual: u8 },
125
126 #[error("Duplicate frame: sequence {sequence}, expected {expected}")]
128 DuplicateFrame { sequence: u8, expected: u8 },
129
130 #[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 #[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 #[error("Send error threshold exceeded: {consecutive_errors} consecutive errors (threshold: {threshold})")]
148 SendErrorThresholdExceeded {
149 consecutive_errors: u32,
150 threshold: u32,
151 },
152
153 #[error("Tunnel connection error: {0}")]
158 TunnelError(String),
159
160 #[error("Tunnel request timeout for channel {channel_id}")]
162 TunnelTimeout { channel_id: u8 },
163
164 #[error("Tunnel ACK error: status {status:#04x}")]
166 TunnelAckError { status: u8 },
167
168 #[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 #[error("L_Data.con NACK: bus delivery failed for channel {channel_id}")]
178 ConfirmationNack { channel_id: u8 },
179
180 #[error("L_Data.con timeout: channel {channel_id}, sequence {sequence}")]
182 ConfirmationTimeout { channel_id: u8, sequence: u8 },
183
184 #[error("Invalid tunnel state transition: {from} -> {to}")]
186 InvalidStateTransition { from: String, to: String },
187
188 #[error("Channel ID mismatch: expected {expected}, got {actual}")]
190 ChannelMismatch { expected: u8, actual: u8 },
191
192 #[error("Flow control: frame dropped — {reason}")]
197 FlowControlDrop { reason: String },
198
199 #[error("Flow control: frame queued for channel {channel_id}")]
201 FlowControlQueued { channel_id: u8 },
202
203 #[error("Circuit breaker open: {consecutive_failures} consecutive failures (threshold: {threshold})")]
205 CircuitBreakerOpen {
206 consecutive_failures: u32,
207 threshold: u32,
208 },
209
210 #[error("Pace filter: delay {delay_ms}ms exceeds maximum {max_delay_ms}ms")]
212 PaceFilterDelayExceeded { delay_ms: u64, max_delay_ms: u64 },
213
214 #[error("Group object not found: {0}")]
219 GroupObjectNotFound(String),
220
221 #[error("Write not allowed for group object: {0}")]
223 GroupObjectWriteNotAllowed(String),
224
225 #[error("Read not allowed for group object: {0}")]
227 GroupObjectReadNotAllowed(String),
228
229 #[error("Server error: {0}")]
234 Server(String),
235
236 #[error("Server not running")]
238 ServerNotRunning,
239
240 #[error("Server already running")]
242 ServerAlreadyRunning,
243
244 #[error("Failed to bind to {address}: {reason}")]
246 BindError { address: SocketAddr, reason: String },
247
248 #[error("Configuration error: {0}")]
253 Config(String),
254
255 #[error("Invalid configuration value for '{field}': {reason}")]
257 InvalidConfigValue { field: String, reason: String },
258
259 #[error("I/O error: {0}")]
264 Io(#[from] std::io::Error),
265
266 #[error("Core error: {0}")]
268 Core(#[from] CoreError),
269
270 #[error("Internal error: {0}")]
272 Internal(String),
273}
274
275impl KnxError {
276 pub fn frame_too_short(expected: usize, actual: usize) -> Self {
282 Self::FrameTooShort { expected, actual }
283 }
284
285 pub fn connection_failed(address: SocketAddr, reason: impl Into<String>) -> Self {
287 Self::ConnectionFailed {
288 address,
289 reason: reason.into(),
290 }
291 }
292
293 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 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 pub fn sequence_error(expected: u8, actual: u8) -> Self {
311 Self::SequenceError { expected, actual }
312 }
313
314 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 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 pub fn requires_tunnel_restart(&self) -> bool {
350 matches!(
351 self,
352 Self::FatalDesync { .. } | Self::SendErrorThresholdExceeded { .. }
353 )
354 }
355
356 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 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}