use std::time::Duration;
use thiserror::Error;
use crate::history::HistoryParam;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Error {
#[error("Bluetooth error: {0}")]
Bluetooth(#[from] btleplug::Error),
#[error("Device not found: {0}")]
DeviceNotFound(DeviceNotFoundReason),
#[error("Not connected to device")]
NotConnected,
#[error("Characteristic not found: {uuid} (searched in {service_count} services)")]
CharacteristicNotFound {
uuid: String,
service_count: usize,
},
#[error("Unsupported: {0}")]
Unsupported(String),
#[error("Invalid data: {0}")]
InvalidData(String),
#[error(
"Invalid history data: {message} (param={param:?}, expected {expected} bytes, got {actual})"
)]
InvalidHistoryData {
message: String,
param: Option<HistoryParam>,
expected: usize,
actual: usize,
},
#[error("Invalid reading format: expected {expected} bytes, got {actual}")]
InvalidReadingFormat {
expected: usize,
actual: usize,
},
#[error("Operation '{operation}' timed out after {duration:?}")]
Timeout {
operation: String,
duration: Duration,
},
#[error("Operation cancelled")]
Cancelled,
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("Connection failed: {reason}")]
ConnectionFailed {
device_id: Option<String>,
reason: ConnectionFailureReason,
},
#[error("Write failed to characteristic {uuid}: {reason}")]
WriteFailed {
uuid: String,
reason: String,
},
#[error("Invalid configuration: {0}")]
InvalidConfig(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ConnectionFailureReason {
AdapterUnavailable,
OutOfRange,
Rejected,
Timeout,
AlreadyConnected,
PairingFailed,
BleError(String),
Other(String),
}
impl std::fmt::Display for ConnectionFailureReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AdapterUnavailable => write!(f, "Bluetooth adapter unavailable"),
Self::OutOfRange => write!(f, "device out of range"),
Self::Rejected => write!(f, "connection rejected by device"),
Self::Timeout => write!(f, "connection timed out"),
Self::AlreadyConnected => write!(f, "device already connected"),
Self::PairingFailed => write!(f, "pairing failed"),
Self::BleError(msg) => write!(f, "BLE error: {}", msg),
Self::Other(msg) => write!(f, "{}", msg),
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum DeviceNotFoundReason {
NoDevicesInRange,
NotFound { identifier: String },
ScanTimeout { duration: Duration },
NoAdapter,
}
impl std::fmt::Display for DeviceNotFoundReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoDevicesInRange => write!(f, "no devices in range"),
Self::NotFound { identifier } => write!(f, "device '{}' not found", identifier),
Self::ScanTimeout { duration } => write!(f, "scan timed out after {:?}", duration),
Self::NoAdapter => write!(f, "no Bluetooth adapter available"),
}
}
}
impl Error {
pub fn device_not_found(identifier: impl Into<String>) -> Self {
Self::DeviceNotFound(DeviceNotFoundReason::NotFound {
identifier: identifier.into(),
})
}
pub fn timeout(operation: impl Into<String>, duration: Duration) -> Self {
Self::Timeout {
operation: operation.into(),
duration,
}
}
pub fn characteristic_not_found(uuid: impl Into<String>, service_count: usize) -> Self {
Self::CharacteristicNotFound {
uuid: uuid.into(),
service_count,
}
}
pub fn invalid_reading(expected: usize, actual: usize) -> Self {
Self::InvalidReadingFormat { expected, actual }
}
pub fn invalid_config(message: impl Into<String>) -> Self {
Self::InvalidConfig(message.into())
}
pub fn connection_failed(device_id: Option<String>, reason: ConnectionFailureReason) -> Self {
Self::ConnectionFailed { device_id, reason }
}
pub fn connection_failed_str(device_id: Option<String>, reason: impl Into<String>) -> Self {
Self::ConnectionFailed {
device_id,
reason: ConnectionFailureReason::Other(reason.into()),
}
}
}
impl From<aranet_types::ParseError> for Error {
fn from(err: aranet_types::ParseError) -> Self {
match err {
aranet_types::ParseError::InsufficientBytes { expected, actual } => {
Error::InvalidReadingFormat { expected, actual }
}
aranet_types::ParseError::InvalidValue(msg) => Error::InvalidData(msg),
aranet_types::ParseError::UnknownDeviceType(byte) => {
Error::InvalidData(format!("Unknown device type: 0x{:02X}", byte))
}
_ => Error::InvalidData(format!("Parse error: {}", err)),
}
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display() {
let err = Error::device_not_found("Aranet4 12345");
assert!(err.to_string().contains("Aranet4 12345"));
let err = Error::NotConnected;
assert_eq!(err.to_string(), "Not connected to device");
let err = Error::characteristic_not_found("0x2A19", 5);
assert!(err.to_string().contains("0x2A19"));
assert!(err.to_string().contains("5 services"));
let err = Error::InvalidData("bad format".to_string());
assert_eq!(err.to_string(), "Invalid data: bad format");
let err = Error::timeout("read_current", Duration::from_secs(10));
assert!(err.to_string().contains("read_current"));
assert!(err.to_string().contains("10s"));
}
#[test]
fn test_error_debug() {
let err = Error::DeviceNotFound(DeviceNotFoundReason::NoDevicesInRange);
let debug_str = format!("{:?}", err);
assert!(debug_str.contains("DeviceNotFound"));
}
#[test]
fn test_device_not_found_reasons() {
let err = Error::DeviceNotFound(DeviceNotFoundReason::NoAdapter);
assert!(err.to_string().contains("no Bluetooth adapter"));
let err = Error::DeviceNotFound(DeviceNotFoundReason::ScanTimeout {
duration: Duration::from_secs(30),
});
assert!(err.to_string().contains("30s"));
}
#[test]
fn test_invalid_reading_format() {
let err = Error::invalid_reading(13, 7);
assert!(err.to_string().contains("13"));
assert!(err.to_string().contains("7"));
}
#[test]
fn test_btleplug_error_conversion() {
fn _assert_from_impl<T: From<btleplug::Error>>() {}
_assert_from_impl::<Error>();
}
#[test]
fn test_io_error_conversion() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let err: Error = io_err.into();
assert!(matches!(err, Error::Io(_)));
assert!(err.to_string().contains("file not found"));
}
}