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>;