aranet_core/
error.rs

1//! Error types for aranet-core.
2
3use std::time::Duration;
4
5use thiserror::Error;
6
7use crate::history::HistoryParam;
8
9/// Errors that can occur when communicating with Aranet devices.
10///
11/// This enum is marked `#[non_exhaustive]` to allow adding new error variants
12/// in future versions without breaking downstream code.
13#[derive(Debug, Error)]
14#[non_exhaustive]
15pub enum Error {
16    /// Bluetooth Low Energy error.
17    #[error("Bluetooth error: {0}")]
18    Bluetooth(#[from] btleplug::Error),
19
20    /// Device not found during scan or connection.
21    #[error("Device not found: {0}")]
22    DeviceNotFound(DeviceNotFoundReason),
23
24    /// Operation attempted while not connected to device.
25    #[error("Not connected to device")]
26    NotConnected,
27
28    /// Required BLE characteristic not found on device.
29    #[error("Characteristic not found: {uuid} (searched in {service_count} services)")]
30    CharacteristicNotFound {
31        /// The UUID that was not found.
32        uuid: String,
33        /// Number of services that were searched.
34        service_count: usize,
35    },
36
37    /// Failed to parse data received from device.
38    #[error("Invalid data: {0}")]
39    InvalidData(String),
40
41    /// Invalid history data format.
42    #[error(
43        "Invalid history data: {message} (param={param:?}, expected {expected} bytes, got {actual})"
44    )]
45    InvalidHistoryData {
46        /// Description of the error.
47        message: String,
48        /// The history parameter being downloaded.
49        param: Option<HistoryParam>,
50        /// Expected data size.
51        expected: usize,
52        /// Actual data size received.
53        actual: usize,
54    },
55
56    /// Invalid reading format from sensor.
57    #[error("Invalid reading format: expected {expected} bytes, got {actual}")]
58    InvalidReadingFormat {
59        /// Expected data size.
60        expected: usize,
61        /// Actual data size received.
62        actual: usize,
63    },
64
65    /// Operation timed out.
66    #[error("Operation '{operation}' timed out after {duration:?}")]
67    Timeout {
68        /// The operation that timed out.
69        operation: String,
70        /// The timeout duration.
71        duration: Duration,
72    },
73
74    /// Operation was cancelled.
75    #[error("Operation cancelled")]
76    Cancelled,
77
78    /// I/O error.
79    #[error(transparent)]
80    Io(#[from] std::io::Error),
81
82    /// Connection failed with specific reason.
83    #[error("Connection failed: {reason}")]
84    ConnectionFailed {
85        /// The device identifier that failed to connect.
86        device_id: Option<String>,
87        /// The structured reason for the failure.
88        reason: ConnectionFailureReason,
89    },
90
91    /// Write operation failed.
92    #[error("Write failed to characteristic {uuid}: {reason}")]
93    WriteFailed {
94        /// The characteristic UUID.
95        uuid: String,
96        /// The reason for the failure.
97        reason: String,
98    },
99
100    /// Invalid configuration provided.
101    #[error("Invalid configuration: {0}")]
102    InvalidConfig(String),
103}
104
105/// Structured reasons for connection failures.
106///
107/// This enum is marked `#[non_exhaustive]` to allow adding new reasons
108/// in future versions without breaking downstream code.
109#[derive(Debug, Clone, PartialEq, Eq)]
110#[non_exhaustive]
111pub enum ConnectionFailureReason {
112    /// Bluetooth adapter not available or powered off.
113    AdapterUnavailable,
114    /// Device is out of range.
115    OutOfRange,
116    /// Device rejected the connection.
117    Rejected,
118    /// Connection attempt timed out.
119    Timeout,
120    /// Already connected to another central.
121    AlreadyConnected,
122    /// Pairing failed.
123    PairingFailed,
124    /// Generic BLE error.
125    BleError(String),
126    /// Other/unknown error.
127    Other(String),
128}
129
130impl std::fmt::Display for ConnectionFailureReason {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        match self {
133            Self::AdapterUnavailable => write!(f, "Bluetooth adapter unavailable"),
134            Self::OutOfRange => write!(f, "device out of range"),
135            Self::Rejected => write!(f, "connection rejected by device"),
136            Self::Timeout => write!(f, "connection timed out"),
137            Self::AlreadyConnected => write!(f, "device already connected"),
138            Self::PairingFailed => write!(f, "pairing failed"),
139            Self::BleError(msg) => write!(f, "BLE error: {}", msg),
140            Self::Other(msg) => write!(f, "{}", msg),
141        }
142    }
143}
144
145/// Reason why a device was not found.
146///
147/// This enum is marked `#[non_exhaustive]` to allow adding new reasons
148/// in future versions without breaking downstream code.
149#[derive(Debug, Clone)]
150#[non_exhaustive]
151pub enum DeviceNotFoundReason {
152    /// No devices found during scan.
153    NoDevicesInRange,
154    /// Device with specified name/address not found.
155    NotFound { identifier: String },
156    /// Scan timed out before finding device.
157    ScanTimeout { duration: Duration },
158    /// No Bluetooth adapter available.
159    NoAdapter,
160}
161
162impl std::fmt::Display for DeviceNotFoundReason {
163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164        match self {
165            Self::NoDevicesInRange => write!(f, "no devices in range"),
166            Self::NotFound { identifier } => write!(f, "device '{}' not found", identifier),
167            Self::ScanTimeout { duration } => write!(f, "scan timed out after {:?}", duration),
168            Self::NoAdapter => write!(f, "no Bluetooth adapter available"),
169        }
170    }
171}
172
173impl Error {
174    /// Create a device not found error for a specific identifier.
175    pub fn device_not_found(identifier: impl Into<String>) -> Self {
176        Self::DeviceNotFound(DeviceNotFoundReason::NotFound {
177            identifier: identifier.into(),
178        })
179    }
180
181    /// Create a timeout error with operation context.
182    pub fn timeout(operation: impl Into<String>, duration: Duration) -> Self {
183        Self::Timeout {
184            operation: operation.into(),
185            duration,
186        }
187    }
188
189    /// Create a characteristic not found error.
190    pub fn characteristic_not_found(uuid: impl Into<String>, service_count: usize) -> Self {
191        Self::CharacteristicNotFound {
192            uuid: uuid.into(),
193            service_count,
194        }
195    }
196
197    /// Create an invalid reading format error.
198    pub fn invalid_reading(expected: usize, actual: usize) -> Self {
199        Self::InvalidReadingFormat { expected, actual }
200    }
201
202    /// Create a configuration error.
203    pub fn invalid_config(message: impl Into<String>) -> Self {
204        Self::InvalidConfig(message.into())
205    }
206
207    /// Create a connection failure with structured reason.
208    pub fn connection_failed(device_id: Option<String>, reason: ConnectionFailureReason) -> Self {
209        Self::ConnectionFailed { device_id, reason }
210    }
211
212    /// Create a connection failure with a string reason.
213    ///
214    /// This is a convenience method that wraps the string in `ConnectionFailureReason::Other`.
215    pub fn connection_failed_str(device_id: Option<String>, reason: impl Into<String>) -> Self {
216        Self::ConnectionFailed {
217            device_id,
218            reason: ConnectionFailureReason::Other(reason.into()),
219        }
220    }
221}
222
223impl From<aranet_types::ParseError> for Error {
224    fn from(err: aranet_types::ParseError) -> Self {
225        match err {
226            aranet_types::ParseError::InsufficientBytes { expected, actual } => {
227                Error::InvalidReadingFormat { expected, actual }
228            }
229            aranet_types::ParseError::InvalidValue(msg) => Error::InvalidData(msg),
230            aranet_types::ParseError::UnknownDeviceType(byte) => {
231                Error::InvalidData(format!("Unknown device type: 0x{:02X}", byte))
232            }
233            // Handle future ParseError variants (non_exhaustive)
234            _ => Error::InvalidData(format!("Parse error: {}", err)),
235        }
236    }
237}
238
239/// Result type alias using aranet-core's Error type.
240pub type Result<T> = std::result::Result<T, Error>;
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn test_error_display() {
248        let err = Error::device_not_found("Aranet4 12345");
249        assert!(err.to_string().contains("Aranet4 12345"));
250
251        let err = Error::NotConnected;
252        assert_eq!(err.to_string(), "Not connected to device");
253
254        let err = Error::characteristic_not_found("0x2A19", 5);
255        assert!(err.to_string().contains("0x2A19"));
256        assert!(err.to_string().contains("5 services"));
257
258        let err = Error::InvalidData("bad format".to_string());
259        assert_eq!(err.to_string(), "Invalid data: bad format");
260
261        let err = Error::timeout("read_current", Duration::from_secs(10));
262        assert!(err.to_string().contains("read_current"));
263        assert!(err.to_string().contains("10s"));
264    }
265
266    #[test]
267    fn test_error_debug() {
268        let err = Error::DeviceNotFound(DeviceNotFoundReason::NoDevicesInRange);
269        let debug_str = format!("{:?}", err);
270        assert!(debug_str.contains("DeviceNotFound"));
271    }
272
273    #[test]
274    fn test_device_not_found_reasons() {
275        let err = Error::DeviceNotFound(DeviceNotFoundReason::NoAdapter);
276        assert!(err.to_string().contains("no Bluetooth adapter"));
277
278        let err = Error::DeviceNotFound(DeviceNotFoundReason::ScanTimeout {
279            duration: Duration::from_secs(30),
280        });
281        assert!(err.to_string().contains("30s"));
282    }
283
284    #[test]
285    fn test_invalid_reading_format() {
286        let err = Error::invalid_reading(13, 7);
287        assert!(err.to_string().contains("13"));
288        assert!(err.to_string().contains("7"));
289    }
290
291    #[test]
292    fn test_btleplug_error_conversion() {
293        // btleplug::Error doesn't have public constructors for most variants,
294        // but we can verify the From impl exists by checking the type compiles
295        fn _assert_from_impl<T: From<btleplug::Error>>() {}
296        _assert_from_impl::<Error>();
297    }
298
299    #[test]
300    fn test_io_error_conversion() {
301        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
302        let err: Error = io_err.into();
303        assert!(matches!(err, Error::Io(_)));
304        assert!(err.to_string().contains("file not found"));
305    }
306}