11use serde:: { Deserialize , Serialize } ;
22use std:: borrow:: Cow ;
3- use std:: fmt:: { self , Debug , Display , Formatter } ;
3+ use std:: fmt:: { self , Debug , Display , Formatter , UpperHex } ;
44use std:: ops:: RangeInclusive ;
55use thiserror:: Error ;
66
@@ -11,24 +11,44 @@ const DRIVER_BASE: u16 = 0x500;
1111/// The maximum value for error numbers.
1212const MAX : u16 = 0xFFF ;
1313
14+ /// The valid range of error numbers.
15+ const RANGE : RangeInclusive < u16 > = BASE ..=MAX ;
16+
17+ fn invalid_error_code ( raw : impl UpperHex ) -> eyre:: Error {
18+ eyre:: eyre!( "Error code {raw:#X} is out of valid range ({RANGE:#X?})" )
19+ }
20+
1421/// Alpaca representation of an ASCOM error code.
1522#[ derive( Clone , Copy , PartialEq , Eq , Serialize , Deserialize , derive_more:: Display ) ]
1623#[ display( "{self:?}" ) ]
1724#[ serde( transparent) ]
1825pub struct ASCOMErrorCode ( u16 ) ;
1926
27+ impl TryFrom < i32 > for ASCOMErrorCode {
28+ type Error = eyre:: Error ;
29+
30+ /// Convert a raw error code into an `ASCOMErrorCode` if it's in the valid range.
31+ fn try_from ( raw : i32 ) -> eyre:: Result < Self > {
32+ if let Ok ( raw) = u16:: try_from ( raw)
33+ && RANGE . contains ( & raw )
34+ {
35+ Ok ( Self ( raw) )
36+ } else {
37+ Err ( invalid_error_code ( raw) )
38+ }
39+ }
40+ }
41+
2042impl TryFrom < u16 > for ASCOMErrorCode {
2143 type Error = eyre:: Error ;
2244
2345 /// Convert a raw error code into an `ASCOMErrorCode` if it's in the valid range.
2446 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) )
47+ if RANGE . contains ( & raw ) {
48+ Ok ( Self ( raw) )
49+ } else {
50+ Err ( invalid_error_code ( raw) )
51+ }
3252 }
3353}
3454
@@ -95,7 +115,7 @@ impl ASCOMErrorCode {
95115}
96116
97117/// ASCOM error.
98- #[ derive( Debug , Clone , Serialize , Deserialize , Error ) ]
118+ #[ derive( Debug , Clone , Serialize , Error ) ]
99119#[ error( "ASCOM error {code}: {message}" ) ]
100120pub struct ASCOMError {
101121 /// Error number.
@@ -106,6 +126,33 @@ pub struct ASCOMError {
106126 pub message : Cow < ' static , str > ,
107127}
108128
129+ impl < ' de > Deserialize < ' de > for ASCOMError {
130+ fn deserialize < D : serde:: Deserializer < ' de > > ( deserializer : D ) -> Result < Self , D :: Error > {
131+ #[ derive( Deserialize ) ]
132+ #[ serde( rename_all = "PascalCase" ) ]
133+ struct Repr {
134+ error_number : i32 ,
135+ error_message : Cow < ' static , str > ,
136+ }
137+
138+ let Repr {
139+ error_number,
140+ error_message,
141+ } = Repr :: deserialize ( deserializer) ?;
142+
143+ // A zero error number is success; anything else goes through the shared out-of-range
144+ // handling so the JSON and `imagebytes` paths agree (issue #22).
145+ Ok ( if error_number == 0 {
146+ Self {
147+ code : ASCOMErrorCode :: OK ,
148+ message : error_message,
149+ }
150+ } else {
151+ Self :: new_with_unbounded_code ( error_number, & error_message)
152+ } )
153+ }
154+ }
155+
109156impl ASCOMError {
110157 /// Create a new `ASCOMError` from given error code and a message.
111158 pub fn new ( code : ASCOMErrorCode , message : impl Display ) -> Self {
@@ -114,6 +161,23 @@ impl ASCOMError {
114161 message : format ! ( "{message:#}" ) . into ( ) ,
115162 }
116163 }
164+
165+ /// Create an `ASCOMError` from a raw, non-zero error number that may fall outside Alpaca's
166+ /// valid range.
167+ ///
168+ /// Drivers occasionally report HRESULT-style codes (e.g. `-2147024882`) that don't fit.
169+ ///
170+ /// Instead of failing hard, which would lose the original error message (see issue #22),
171+ /// we surface those as `UNSPECIFIED` with the range error prepended to the main one.
172+ pub ( crate ) fn new_with_unbounded_code ( raw_code : i32 , message : & str ) -> Self {
173+ match ASCOMErrorCode :: try_from ( raw_code) {
174+ Ok ( code) => Self :: new ( code, message) ,
175+ Err ( err) => Self :: new (
176+ ASCOMErrorCode :: UNSPECIFIED ,
177+ format_args ! ( "{err}: {message}" ) ,
178+ ) ,
179+ }
180+ }
117181}
118182
119183/// Result type for ASCOM methods.
@@ -186,7 +250,7 @@ ascom_error_codes! {
186250 // Extra codes for internal use only.
187251
188252 /// Reserved 'catch-all' error code (0x4FF) used when nothing else was specified.
189- UNSPECIFIED = 0x4FF ,
253+ pub ( crate ) UNSPECIFIED = 0x4FF ,
190254}
191255
192256impl ASCOMError {
@@ -206,3 +270,47 @@ impl ASCOMError {
206270 Self :: new ( ASCOMErrorCode :: UNSPECIFIED , message)
207271 }
208272}
273+
274+ #[ cfg( test) ]
275+ mod tests {
276+ use super :: { ASCOMError , ASCOMErrorCode } ;
277+ use std:: assert_matches;
278+
279+ // Devices sometimes return HRESULT-style error numbers that don't fit `u16`. These must
280+ // deserialize as `UNSPECIFIED` (keeping the message) rather than failing the response (#22).
281+ #[ test]
282+ fn deserializes_out_of_range_error_number_as_unspecified ( ) -> eyre:: Result < ( ) > {
283+ assert_matches ! (
284+ serde_json:: from_str( r#"{"ErrorNumber": -2147024882, "ErrorMessage": "Not enough storage"}"# ) ?,
285+ ASCOMError {
286+ code: ASCOMErrorCode :: UNSPECIFIED ,
287+ message,
288+ } if message. contains( "out of valid range" ) && message. contains( "Not enough storage" )
289+ ) ;
290+
291+ Ok ( ( ) )
292+ }
293+
294+ // In-range codes (including success `0`, which is outside the `0x400..=0xFFF` range but is a
295+ // valid `u16`) must round-trip unchanged so the client can still detect `OK` responses.
296+ #[ test]
297+ fn deserializes_in_range_error_numbers_unchanged ( ) -> eyre:: Result < ( ) > {
298+ assert_matches ! (
299+ serde_json:: from_str( r#"{"ErrorNumber": 0, "ErrorMessage": ""}"# ) ?,
300+ ASCOMError {
301+ code: ASCOMErrorCode :: OK ,
302+ ..
303+ }
304+ ) ;
305+
306+ assert_matches ! (
307+ serde_json:: from_str( r#"{"ErrorNumber": 1025, "ErrorMessage": "bad"}"# ) ?,
308+ ASCOMError {
309+ code: ASCOMErrorCode :: INVALID_VALUE ,
310+ ..
311+ }
312+ ) ;
313+
314+ Ok ( ( ) )
315+ }
316+ }
0 commit comments