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 { address: String, valid_range: String },
31
32 #[error("Invalid datapoint type: {0}")]
37 InvalidDpt(String),
38
39 #[error("DPT encoding error for {dpt}: {reason}")]
41 DptEncoding { dpt: String, reason: String },
42
43 #[error("DPT decoding error for {dpt}: {reason}")]
45 DptDecoding { dpt: String, reason: String },
46
47 #[error("DPT value out of range: {value} (valid: {valid_range})")]
49 DptValueOutOfRange { value: String, valid_range: String },
50
51 #[error("Frame too short: expected at least {expected} bytes, got {actual}")]
56 FrameTooShort { expected: usize, actual: usize },
57
58 #[error("Invalid frame header: {0}")]
60 InvalidHeader(String),
61
62 #[error("Invalid protocol version: expected {expected:#04x}, got {actual:#04x}")]
64 InvalidProtocolVersion { expected: u8, actual: u8 },
65
66 #[error("Unknown service type: {0:#06x}")]
68 UnknownServiceType(u16),
69
70 #[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 #[error("Invalid HPAI: {0}")]
79 InvalidHpai(String),
80
81 #[error("Unknown cEMI message code: {0:#04x}")]
86 UnknownMessageCode(u8),
87
88 #[error("Invalid cEMI frame: {0}")]
90 InvalidCemi(String),
91
92 #[error("Unknown APCI: {0:#06x}")]
94 UnknownApci(u16),
95
96 #[error("Connection failed to {address}: {reason}")]
101 ConnectionFailed { address: SocketAddr, reason: String },
102
103 #[error("Connection timeout after {timeout_ms}ms")]
105 ConnectionTimeout { timeout_ms: u64 },
106
107 #[error("Connection closed: {0}")]
109 ConnectionClosed(String),
110
111 #[error("No more connections available: maximum {max} reached")]
113 NoMoreConnections { max: usize },
114
115 #[error("Invalid channel ID: {0}")]
117 InvalidChannel(u8),
118
119 #[error("Sequence error: expected {expected}, got {actual}")]
121 SequenceError { expected: u8, actual: u8 },
122
123 #[error("Duplicate frame: sequence {sequence}, expected {expected}")]
125 DuplicateFrame { sequence: u8, expected: u8 },
126
127 #[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 #[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 #[error("Send error threshold exceeded: {consecutive_errors} consecutive errors (threshold: {threshold})")]
145 SendErrorThresholdExceeded {
146 consecutive_errors: u32,
147 threshold: u32,
148 },
149
150 #[error("Tunnel connection error: {0}")]
155 TunnelError(String),
156
157 #[error("Tunnel request timeout for channel {channel_id}")]
159 TunnelTimeout { channel_id: u8 },
160
161 #[error("Tunnel ACK error: status {status:#04x}")]
163 TunnelAckError { status: u8 },
164
165 #[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 #[error("L_Data.con NACK: bus delivery failed for channel {channel_id}")]
175 ConfirmationNack { channel_id: u8 },
176
177 #[error("L_Data.con timeout: channel {channel_id}, sequence {sequence}")]
179 ConfirmationTimeout { channel_id: u8, sequence: u8 },
180
181 #[error("Invalid tunnel state transition: {from} -> {to}")]
183 InvalidStateTransition { from: String, to: String },
184
185 #[error("Channel ID mismatch: expected {expected}, got {actual}")]
187 ChannelMismatch { expected: u8, actual: u8 },
188
189 #[error("Flow control: frame dropped — {reason}")]
194 FlowControlDrop { reason: String },
195
196 #[error("Flow control: frame queued for channel {channel_id}")]
198 FlowControlQueued { channel_id: u8 },
199
200 #[error("Circuit breaker open: {consecutive_failures} consecutive failures (threshold: {threshold})")]
202 CircuitBreakerOpen {
203 consecutive_failures: u32,
204 threshold: u32,
205 },
206
207 #[error("Pace filter: delay {delay_ms}ms exceeds maximum {max_delay_ms}ms")]
209 PaceFilterDelayExceeded { delay_ms: u64, max_delay_ms: u64 },
210
211 #[error("Group object not found: {0}")]
216 GroupObjectNotFound(String),
217
218 #[error("Write not allowed for group object: {0}")]
220 GroupObjectWriteNotAllowed(String),
221
222 #[error("Read not allowed for group object: {0}")]
224 GroupObjectReadNotAllowed(String),
225
226 #[error("Server error: {0}")]
231 Server(String),
232
233 #[error("Server not running")]
235 ServerNotRunning,
236
237 #[error("Server already running")]
239 ServerAlreadyRunning,
240
241 #[error("Failed to bind to {address}: {reason}")]
243 BindError { address: SocketAddr, reason: String },
244
245 #[error("Configuration error: {0}")]
250 Config(String),
251
252 #[error("Invalid configuration value for '{field}': {reason}")]
254 InvalidConfigValue { field: String, reason: String },
255
256 #[error("I/O error: {0}")]
261 Io(#[from] std::io::Error),
262
263 #[error("Core error: {0}")]
265 Core(#[from] CoreError),
266
267 #[error("Internal error: {0}")]
269 Internal(String),
270}
271
272impl KnxError {
273 pub fn frame_too_short(expected: usize, actual: usize) -> Self {
279 Self::FrameTooShort { expected, actual }
280 }
281
282 pub fn connection_failed(address: SocketAddr, reason: impl Into<String>) -> Self {
284 Self::ConnectionFailed {
285 address,
286 reason: reason.into(),
287 }
288 }
289
290 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 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 pub fn sequence_error(expected: u8, actual: u8) -> Self {
308 Self::SequenceError { expected, actual }
309 }
310
311 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 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 pub fn requires_tunnel_restart(&self) -> bool {
347 matches!(
348 self,
349 Self::FatalDesync { .. }
350 | Self::SendErrorThresholdExceeded { .. }
351 )
352 }
353
354 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 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}