1use std::time::Duration;
4
5use thiserror::Error;
6
7use crate::history::HistoryParam;
8
9#[derive(Debug, Error)]
14#[non_exhaustive]
15pub enum Error {
16 #[error("Bluetooth error: {0}")]
18 Bluetooth(#[from] btleplug::Error),
19
20 #[error("Device not found: {0}")]
22 DeviceNotFound(DeviceNotFoundReason),
23
24 #[error("Not connected to device")]
26 NotConnected,
27
28 #[error("Characteristic not found: {uuid} (searched in {service_count} services)")]
30 CharacteristicNotFound {
31 uuid: String,
33 service_count: usize,
35 },
36
37 #[error("Invalid data: {0}")]
39 InvalidData(String),
40
41 #[error(
43 "Invalid history data: {message} (param={param:?}, expected {expected} bytes, got {actual})"
44 )]
45 InvalidHistoryData {
46 message: String,
48 param: Option<HistoryParam>,
50 expected: usize,
52 actual: usize,
54 },
55
56 #[error("Invalid reading format: expected {expected} bytes, got {actual}")]
58 InvalidReadingFormat {
59 expected: usize,
61 actual: usize,
63 },
64
65 #[error("Operation '{operation}' timed out after {duration:?}")]
67 Timeout {
68 operation: String,
70 duration: Duration,
72 },
73
74 #[error("Operation cancelled")]
76 Cancelled,
77
78 #[error(transparent)]
80 Io(#[from] std::io::Error),
81
82 #[error("Connection failed: {reason}")]
84 ConnectionFailed {
85 device_id: Option<String>,
87 reason: ConnectionFailureReason,
89 },
90
91 #[error("Write failed to characteristic {uuid}: {reason}")]
93 WriteFailed {
94 uuid: String,
96 reason: String,
98 },
99
100 #[error("Invalid configuration: {0}")]
102 InvalidConfig(String),
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
110#[non_exhaustive]
111pub enum ConnectionFailureReason {
112 AdapterUnavailable,
114 OutOfRange,
116 Rejected,
118 Timeout,
120 AlreadyConnected,
122 PairingFailed,
124 BleError(String),
126 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#[derive(Debug, Clone)]
150#[non_exhaustive]
151pub enum DeviceNotFoundReason {
152 NoDevicesInRange,
154 NotFound { identifier: String },
156 ScanTimeout { duration: Duration },
158 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 pub fn device_not_found(identifier: impl Into<String>) -> Self {
176 Self::DeviceNotFound(DeviceNotFoundReason::NotFound {
177 identifier: identifier.into(),
178 })
179 }
180
181 pub fn timeout(operation: impl Into<String>, duration: Duration) -> Self {
183 Self::Timeout {
184 operation: operation.into(),
185 duration,
186 }
187 }
188
189 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 pub fn invalid_reading(expected: usize, actual: usize) -> Self {
199 Self::InvalidReadingFormat { expected, actual }
200 }
201
202 pub fn invalid_config(message: impl Into<String>) -> Self {
204 Self::InvalidConfig(message.into())
205 }
206
207 pub fn connection_failed(device_id: Option<String>, reason: ConnectionFailureReason) -> Self {
209 Self::ConnectionFailed { device_id, reason }
210 }
211
212 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 _ => Error::InvalidData(format!("Parse error: {}", err)),
235 }
236 }
237}
238
239pub 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 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}