1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use thiserror::Error;

/// Alpaca representation of an ASCOM error code.
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ASCOMErrorCode(u16);

impl TryFrom<u16> for ASCOMErrorCode {
    type Error = eyre::Error;

    /// Convert a raw error code into an `ASCOMErrorCode` if it's in the valid range.
    fn try_from(raw: u16) -> eyre::Result<Self> {
        let range = BASE..=MAX;
        eyre::ensure!(
            range.contains(&raw),
            "Error code {raw:#X} is out of valid range ({range:#X?})",
            raw = raw,
            range = range,
        );
        Ok(Self(raw))
    }
}

/// The starting value for error numbers.
const BASE: u16 = 0x400;
/// The starting value for driver-specific error numbers.
const DRIVER_BASE: u16 = 0x500;
/// The maximum value for error numbers.
const MAX: u16 = 0xFFF;

impl ASCOMErrorCode {
    /// Generate ASCOM error code from a zero-based driver error code.
    ///
    /// Will panic if the driver error code is larger than the maximum allowed (2815).
    ///
    /// You'll typically want to define an enum for your driver errors and use this in a single
    /// place - in the [`From`] conversion from your driver error type to the [`ASCOMError`].
    ///
    /// # Example
    ///
    /// ```
    /// use ascom_alpaca::{ASCOMError, ASCOMErrorCode};
    /// use thiserror::Error;
    ///
    /// #[derive(Debug, Error)]
    /// pub enum MyDriverError {
    ///     #[error("Port communication error: {0}")]
    ///     PortError(#[from] std::io::Error),
    ///     #[error("Initialization error: {0}")]
    ///     InitializationError(String),
    /// }
    ///
    /// // this allows you to then use `my_driver.method()?` when implementing Alpaca traits
    /// // and it will convert your driver error to an ASCOM error automatically
    /// impl From<MyDriverError> for ASCOMError {
    ///     fn from(error: MyDriverError) -> Self {
    ///         ASCOMError::new(
    ///             ASCOMErrorCode::new_for_driver(match error {
    ///                 MyDriverError::PortError(_) => 0,
    ///                 MyDriverError::InitializationError(_) => 1,
    ///             }),
    ///             error,
    ///         )
    ///     }
    /// }
    /// ```
    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)
    }

    /// Get the driver-specific error code.
    ///
    /// Returns `Ok` with `0`-based driver error code if this is a driver error.
    /// Returns `Err` with raw error code if not a driver error.
    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)
        }
    }

    /// Get the raw error code.
    pub const fn raw(self) -> u16 {
        self.0
    }
}

/// ASCOM error.
#[derive(Debug, Clone, Serialize, Deserialize, Error)]
#[error("ASCOM error {code}: {message}")]
pub struct ASCOMError {
    /// Error number.
    #[serde(rename = "ErrorNumber")]
    pub code: ASCOMErrorCode,
    /// Error message.
    #[serde(rename = "ErrorMessage")]
    pub message: Cow<'static, str>,
}

impl ASCOMError {
    /// Create a new `ASCOMError` from given error code and a message.
    pub fn new(code: ASCOMErrorCode, message: impl std::fmt::Display) -> Self {
        Self {
            code,
            message: message.to_string().into(),
        }
    }
}

/// Result type for ASCOM methods.
pub type ASCOMResult<T = ()> = Result<T, ASCOMError>;

macro_rules! ascom_error_codes {
    ($(#[doc = $doc:literal] $name:ident = $value:literal,)*) => {
        impl ASCOMErrorCode {
            $(
                #[doc = $doc]
                pub const $name: Self = Self($value);
            )*
        }

        impl std::fmt::Debug for ASCOMErrorCode {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                match *self {
                    $(
                        Self::$name => write!(f, "{}", stringify!($name)),
                    )*
                    _ => match self.as_driver_error() {
                        Ok(driver_code) => write!(f, "DRIVER_ERROR[{driver_code}]"),
                        Err(raw_code) => write!(f, "{raw_code:#X}"),
                    },
                }
            }
        }

        impl std::fmt::Display for ASCOMErrorCode {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                std::fmt::Debug::fmt(self, f)
            }
        }

        impl ASCOMError {
            $(
                #[doc = $doc]
                pub const $name: Self = Self {
                    code: ASCOMErrorCode::$name,
                    message: Cow::Borrowed($doc),
                };
            )*
        }
    };
}

ascom_error_codes! {
    #[doc = ""]
    OK = 0,
    #[doc = "The requested action is not implemented in this driver"]
    ACTION_NOT_IMPLEMENTED = 0x40C,
    #[doc = "The requested operation can not be undertaken at this time"]
    INVALID_OPERATION = 0x40B,
    #[doc = "Invalid value"]
    INVALID_VALUE = 0x401,
    #[doc = "The attempted operation is invalid because the mount is currently in a Parked state"]
    INVALID_WHILE_PARKED = 0x408,
    #[doc = "The attempted operation is invalid because the mount is currently in a Slaved state"]
    INVALID_WHILE_SLAVED = 0x409,
    #[doc = "The communications channel is not connected"]
    NOT_CONNECTED = 0x407,
    #[doc = "Property or method not implemented"]
    NOT_IMPLEMENTED = 0x400,
    #[doc = "The requested item is not present in the ASCOM cache"]
    NOT_IN_CACHE = 0x40D,
    #[doc = "Settings error"]
    SETTINGS = 0x40A,
    #[doc = "Unspecified error"]
    UNSPECIFIED = 0x4FF,
    #[doc = "A value has not been set"]
    VALUE_NOT_SET = 0x402,
}

impl ASCOMError {
    /// Create a new "invalid operation" error with the specified message.
    pub fn invalid_operation(message: impl std::fmt::Display) -> Self {
        Self::new(ASCOMErrorCode::INVALID_OPERATION, message)
    }

    /// Create a new "invalid value" error with the specified message.
    pub fn invalid_value(message: impl std::fmt::Display) -> Self {
        Self::new(ASCOMErrorCode::INVALID_VALUE, message)
    }

    /// Create a new error with unspecified error code and the given message.
    pub fn unspecified(message: impl std::fmt::Display) -> Self {
        Self::new(ASCOMErrorCode::UNSPECIFIED, message)
    }
}