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}