ascom_alpaca/
errors.rs

1use serde::{Deserialize, Serialize};
2use std::borrow::Cow;
3use std::fmt::{self, Debug, Display, Formatter};
4use std::ops::RangeInclusive;
5use thiserror::Error;
6
7/// The starting value for error numbers.
8const BASE: u16 = 0x400;
9/// The starting value for driver-specific error numbers.
10const DRIVER_BASE: u16 = 0x500;
11/// The maximum value for error numbers.
12const MAX: u16 = 0xFFF;
13
14/// Alpaca representation of an ASCOM error code.
15#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, derive_more::Display)]
16#[display("{self:?}")]
17#[serde(transparent)]
18pub struct ASCOMErrorCode(u16);
19
20impl TryFrom<u16> for ASCOMErrorCode {
21    type Error = eyre::Error;
22
23    /// Convert a raw error code into an `ASCOMErrorCode` if it's in the valid range.
24    fn try_from(raw: u16) -> eyre::Result<Self> {
25        const RANGE: RangeInclusive<u16> = BASE..=MAX;
26
27        eyre::ensure!(
28            RANGE.contains(&raw),
29            "Error code {raw:#X} is out of valid range ({RANGE:#X?})"
30        );
31        Ok(Self(raw))
32    }
33}
34
35impl ASCOMErrorCode {
36    /// Generate ASCOM error code from a zero-based driver error code.
37    ///
38    /// Will panic if the driver error code is larger than the maximum allowed (2815).
39    ///
40    /// You'll typically want to define an enum for your driver errors and use this in a single
41    /// place - in the [`From`] conversion from your driver error type to the [`ASCOMError`].
42    ///
43    /// # Example
44    ///
45    /// ```
46    /// use ascom_alpaca::{ASCOMError, ASCOMErrorCode};
47    /// use thiserror::Error;
48    ///
49    /// #[derive(Debug, Error)]
50    /// pub enum MyDriverError {
51    ///     #[error("Port communication error: {0}")]
52    ///     PortError(#[from] std::io::Error),
53    ///     #[error("Initialization error: {0}")]
54    ///     InitializationError(String),
55    /// }
56    ///
57    /// // this allows you to then use `my_driver.method()?` when implementing Alpaca traits
58    /// // and it will convert your driver error to an ASCOM error automatically
59    /// impl From<MyDriverError> for ASCOMError {
60    ///     fn from(error: MyDriverError) -> Self {
61    ///         ASCOMError::new(
62    ///             ASCOMErrorCode::new_for_driver(match error {
63    ///                 MyDriverError::PortError(_) => 0,
64    ///                 MyDriverError::InitializationError(_) => 1,
65    ///             }),
66    ///             error,
67    ///         )
68    ///     }
69    /// }
70    /// ```
71    pub const fn new_for_driver(driver_code: u16) -> Self {
72        const DRIVER_MAX: u16 = MAX - DRIVER_BASE;
73
74        assert!(driver_code <= DRIVER_MAX, "Driver error code is too large");
75
76        Self(driver_code + DRIVER_BASE)
77    }
78
79    /// Get the driver-specific error code.
80    ///
81    /// Returns `Ok` with `0`-based driver error code if this is a driver error.
82    /// Returns `Err` with raw error code if not a driver error.
83    pub const fn as_driver_error(self) -> Result<u16, u16> {
84        if let Some(driver_code) = self.0.checked_sub(DRIVER_BASE) {
85            Ok(driver_code)
86        } else {
87            Err(self.0)
88        }
89    }
90
91    /// Get the raw error code.
92    pub const fn raw(self) -> u16 {
93        self.0
94    }
95}
96
97/// ASCOM error.
98#[derive(Debug, Clone, Serialize, Deserialize, Error)]
99#[error("ASCOM error {code}: {message}")]
100pub struct ASCOMError {
101    /// Error number.
102    #[serde(rename = "ErrorNumber")]
103    pub code: ASCOMErrorCode,
104    /// Error message.
105    #[serde(rename = "ErrorMessage")]
106    pub message: Cow<'static, str>,
107}
108
109impl ASCOMError {
110    /// Create a new `ASCOMError` from given error code and a message.
111    pub fn new(code: ASCOMErrorCode, message: impl Display) -> Self {
112        Self {
113            code,
114            message: message.to_string().into(),
115        }
116    }
117}
118
119/// Result type for ASCOM methods.
120pub type ASCOMResult<T = ()> = Result<T, ASCOMError>;
121
122macro_rules! ascom_error_codes {
123    ($(#[doc = $doc:literal] $vis:vis $name:ident = $value:literal,)*) => {
124        impl ASCOMErrorCode {
125            $(
126                #[doc = $doc]
127                $vis const $name: Self = Self($value);
128            )*
129        }
130
131        impl Debug for ASCOMErrorCode {
132            fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
133                match *self {
134                    $(
135                        Self::$name => f.write_str(stringify!($name)),
136                    )*
137                    _ => match self.as_driver_error() {
138                        Ok(driver_code) => write!(f, "DRIVER_ERROR[{driver_code}]"),
139                        Err(raw_code) => write!(f, "{raw_code:#X}"),
140                    },
141                }
142            }
143        }
144
145        #[expect(unused)]
146        impl ASCOMError {
147            $(
148                #[doc = $doc]
149                $vis const $name: Self = Self {
150                    code: ASCOMErrorCode::$name,
151                    message: Cow::Borrowed(ascom_error_codes!(@msg $name $doc)),
152                };
153            )*
154        }
155    };
156
157    (@msg OK $doc:literal) => ("");
158    (@msg $name:ident $doc:literal) => ($doc.trim_ascii());
159}
160
161ascom_error_codes! {
162    /// Success.
163    pub OK = 0,
164
165    // Well-known Alpaca error codes as per the specification.
166
167    /// Property or method not implemented.
168    pub NOT_IMPLEMENTED = 0x400,
169    /// Invalid value.
170    pub INVALID_VALUE = 0x401,
171    /// A value has not been set.
172    pub VALUE_NOT_SET = 0x402,
173    /// The communications channel is not connected.
174    pub NOT_CONNECTED = 0x407,
175    /// The attempted operation is invalid because the mount is currently in a Parked state.
176    pub INVALID_WHILE_PARKED = 0x408,
177    /// The attempted operation is invalid because the mount is currently in a Slaved state.
178    pub INVALID_WHILE_SLAVED = 0x409,
179    /// The requested operation can not be undertaken at this time.
180    pub INVALID_OPERATION = 0x40B,
181    /// The requested action is not implemented in this driver.
182    pub ACTION_NOT_IMPLEMENTED = 0x40C,
183    /// In-progress asynchronous operation has been cancelled.
184    pub OPERATION_CANCELLED = 0x40D,
185
186    // Extra codes for internal use only.
187
188    /// Reserved 'catch-all' error code (0x4FF) used when nothing else was specified.
189    UNSPECIFIED = 0x4FF,
190}
191
192impl ASCOMError {
193    /// Create a new "invalid operation" error with the specified message.
194    pub fn invalid_operation(message: impl Display) -> Self {
195        Self::new(ASCOMErrorCode::INVALID_OPERATION, message)
196    }
197
198    /// Create a new "invalid value" error with the specified message.
199    pub fn invalid_value(message: impl Display) -> Self {
200        Self::new(ASCOMErrorCode::INVALID_VALUE, message)
201    }
202
203    /// Create a new error with unspecified error code and the given message.
204    #[cfg(feature = "client")]
205    pub(crate) fn unspecified(message: impl Display) -> Self {
206        Self::new(ASCOMErrorCode::UNSPECIFIED, message)
207    }
208}