Skip to main content

donglora_client/
errors.rs

1//! Error taxonomy for `donglora-client`.
2//!
3//! Mirrors the Python client's `DongloraError` hierarchy: one base error
4//! with specific variants for each spec-defined failure mode plus a
5//! handful of transport-level conditions. Callers who just want to
6//! surface failures match on the top-level [`ClientError`]; code that
7//! needs to retry on a specific kind (e.g. `ChannelBusy`) pattern-matches
8//! that variant.
9
10use donglora_protocol::ErrorCode;
11use thiserror::Error;
12
13/// Every fallible operation in `donglora-client` returns this error
14/// type. Variants mirror `PROTOCOL.md` §7 plus Rust-side conditions
15/// (timeouts, closed transports, bad frames).
16#[derive(Debug, Error)]
17pub enum ClientError {
18    /// Firmware returned `ERR(ENOTCONFIGURED)`. Callers usually want to
19    /// re-apply their config and retry; [`crate::Dongle`] does this
20    /// automatically via `_with_recovery`.
21    #[error("device is not configured (ENOTCONFIGURED)")]
22    NotConfigured,
23
24    /// Firmware returned `ERR(EBUSY)` — TX queue full. Host should back
25    /// off briefly and retry with a new tag.
26    #[error("firmware TX queue is full (EBUSY)")]
27    Busy,
28
29    /// Firmware returned `ERR(EPARAM)` — a parameter value is out of
30    /// range or invalid.
31    #[error("parameter out of range (EPARAM)")]
32    Param,
33
34    /// Firmware returned `ERR(ELENGTH)` — payload length is wrong for
35    /// the command or modulation.
36    #[error("payload length wrong (ELENGTH)")]
37    Length,
38
39    /// Firmware returned `ERR(EMODULATION)` — requested modulation not
40    /// supported on this chip.
41    #[error("modulation not supported (EMODULATION)")]
42    Modulation,
43
44    /// Firmware returned `ERR(EUNKNOWN_CMD)` — unknown command type
45    /// byte.
46    #[error("unknown command (EUNKNOWN_CMD)")]
47    UnknownCmd,
48
49    /// Firmware returned `ERR(ERADIO)` — SPI error or unexpected radio
50    /// hardware state.
51    #[error("radio hardware error (ERADIO)")]
52    Radio,
53
54    /// Firmware reported a framing error (`ERR(EFRAME)`), usually
55    /// because of CRC/COBS corruption on the H→D path. Rare on USB.
56    #[error("framing error (EFRAME)")]
57    Frame,
58
59    /// Firmware returned `ERR(EINTERNAL)` — firmware bug / invariant
60    /// violation.
61    #[error("firmware internal error (EINTERNAL)")]
62    Internal,
63
64    /// Firmware returned an error code this client doesn't recognise.
65    /// Preserves the raw u16 so forward-compat with minor-version
66    /// extensions doesn't lose information.
67    #[error("unknown error code 0x{0:04X}")]
68    UnknownCode(u16),
69
70    /// CAD detected activity and the TX was aborted before airtime.
71    /// Per spec §6.10, host should randomized-backoff and retry with a
72    /// **new tag**. Reported as a distinct variant from `Busy` because
73    /// the retry policy differs.
74    #[error("channel busy — CAD detected activity")]
75    ChannelBusy,
76
77    /// A queued TX was cancelled by a reconfigure or disconnect before
78    /// it reached the air. Don't retry — the cancellation is terminal.
79    #[error("TX cancelled before airtime")]
80    Cancelled,
81
82    /// Command did not complete before its deadline.
83    #[error("timed out waiting for {what}")]
84    Timeout { what: &'static str },
85
86    /// The underlying transport (USB, socket) closed or errored.
87    #[error("transport closed: {0}")]
88    TransportClosed(String),
89
90    /// Session reader thread died while waiting for a response.
91    #[error("session reader exited")]
92    ReaderExited,
93
94    /// An inbound frame failed CRC or COBS decoding and was dropped.
95    /// The session reader logs these as async events; callers can poll
96    /// for them via [`crate::Dongle::drain_async_errors`].
97    #[error("inbound frame corrupted: {0}")]
98    BadFrame(String),
99
100    /// Encoding an outbound frame failed (should not happen in normal
101    /// use — the protocol crate's limits are enforced at the type level).
102    #[error("frame encode failed: {0}")]
103    EncodeFailed(String),
104
105    /// Underlying I/O error from `tokio::io` or `tokio-serial`.
106    #[error("I/O error: {0}")]
107    Io(#[from] std::io::Error),
108
109    /// Catch-all for transport initialisation issues that don't have a
110    /// dedicated variant.
111    #[error("{0}")]
112    Other(String),
113}
114
115impl ClientError {
116    /// True if retrying the operation with a fresh tag is spec-sanctioned.
117    /// Used by [`crate::RetryPolicy`] to decide whether to loop.
118    #[must_use]
119    pub fn is_retryable(&self) -> bool {
120        matches!(self, Self::ChannelBusy | Self::Busy)
121    }
122
123    /// Map an DongLoRa Protocol `ErrorCode` from the wire to the matching variant.
124    #[must_use]
125    pub fn from_wire(code: ErrorCode) -> Self {
126        match code {
127            ErrorCode::ENotConfigured => Self::NotConfigured,
128            ErrorCode::EBusy => Self::Busy,
129            ErrorCode::EParam => Self::Param,
130            ErrorCode::ELength => Self::Length,
131            ErrorCode::EModulation => Self::Modulation,
132            ErrorCode::EUnknownCmd => Self::UnknownCmd,
133            ErrorCode::ERadio => Self::Radio,
134            ErrorCode::EFrame => Self::Frame,
135            ErrorCode::EInternal => Self::Internal,
136            ErrorCode::Unknown(raw) => Self::UnknownCode(raw),
137        }
138    }
139}
140
141/// Short alias for the crate's `Result` type.
142pub type ClientResult<T> = Result<T, ClientError>;