use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::fmt::{self, Debug, Display, Formatter};
use std::ops::RangeInclusive;
use thiserror::Error;
const BASE: u16 = 0x400;
const DRIVER_BASE: u16 = 0x500;
const MAX: u16 = 0xFFF;
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, derive_more::Display)]
#[display("{self:?}")]
#[serde(transparent)]
pub struct ASCOMErrorCode(u16);
impl TryFrom<u16> for ASCOMErrorCode {
type Error = eyre::Error;
fn try_from(raw: u16) -> eyre::Result<Self> {
const RANGE: RangeInclusive<u16> = BASE..=MAX;
eyre::ensure!(
RANGE.contains(&raw),
"Error code {raw:#X} is out of valid range ({RANGE:#X?})"
);
Ok(Self(raw))
}
}
impl ASCOMErrorCode {
pub const fn new_for_driver(driver_code: u16) -> Self {
const DRIVER_MAX: u16 = MAX - DRIVER_BASE;
assert!(driver_code <= DRIVER_MAX, "Driver error code is too large");
Self(driver_code + DRIVER_BASE)
}
pub const fn as_driver_error(self) -> Result<u16, u16> {
if let Some(driver_code) = self.0.checked_sub(DRIVER_BASE) {
Ok(driver_code)
} else {
Err(self.0)
}
}
pub const fn raw(self) -> u16 {
self.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Error)]
#[error("ASCOM error {code}: {message}")]
pub struct ASCOMError {
#[serde(rename = "ErrorNumber")]
pub code: ASCOMErrorCode,
#[serde(rename = "ErrorMessage")]
pub message: Cow<'static, str>,
}
impl ASCOMError {
pub fn new(code: ASCOMErrorCode, message: impl Display) -> Self {
Self {
code,
message: format!("{message:#}").into(),
}
}
}
pub type ASCOMResult<T = ()> = Result<T, ASCOMError>;
macro_rules! ascom_error_codes {
($(#[doc = $doc:literal] $vis:vis $name:ident = $value:literal,)*) => {
impl ASCOMErrorCode {
$(
#[doc = $doc]
$vis const $name: Self = Self($value);
)*
}
impl Debug for ASCOMErrorCode {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
$(
Self::$name => f.write_str(stringify!($name)),
)*
_ => match self.as_driver_error() {
Ok(driver_code) => write!(f, "DRIVER_ERROR[{driver_code}]"),
Err(raw_code) => write!(f, "{raw_code:#X}"),
},
}
}
}
#[expect(unused)]
impl ASCOMError {
$(
#[doc = $doc]
$vis const $name: Self = Self {
code: ASCOMErrorCode::$name,
message: Cow::Borrowed(ascom_error_codes!(@msg $name $doc)),
};
)*
}
};
(@msg OK $doc:literal) => ("");
(@msg $name:ident $doc:literal) => ($doc.trim_ascii());
}
ascom_error_codes! {
pub OK = 0,
pub NOT_IMPLEMENTED = 0x400,
pub INVALID_VALUE = 0x401,
pub VALUE_NOT_SET = 0x402,
pub NOT_CONNECTED = 0x407,
pub INVALID_WHILE_PARKED = 0x408,
pub INVALID_WHILE_SLAVED = 0x409,
pub INVALID_OPERATION = 0x40B,
pub ACTION_NOT_IMPLEMENTED = 0x40C,
pub OPERATION_CANCELLED = 0x40D,
UNSPECIFIED = 0x4FF,
}
impl ASCOMError {
pub fn invalid_operation(message: impl Display) -> Self {
Self::new(ASCOMErrorCode::INVALID_OPERATION, message)
}
pub fn invalid_value(message: impl Display) -> Self {
Self::new(ASCOMErrorCode::INVALID_VALUE, message)
}
#[cfg(feature = "client")]
pub(crate) fn unspecified(message: impl Display) -> Self {
Self::new(ASCOMErrorCode::UNSPECIFIED, message)
}
}