rfm69_async/traits.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2
3use crate::Packet;
4
5/// Stable, lossy error enum for the transport-agnostic [`Transceiver`] trait.
6///
7/// The driver's parametric [`crate::Error`] preserves the underlying SPI /
8/// GPIO error types, which is useful for users that hold a concrete
9/// `Rfm69<...>` but does not survive a `dyn` boundary or a generic that
10/// abstracts over the backing radio. `TrxError` collapses those variants
11/// into a fixed vocabulary for use across the `Transceiver` boundary.
12///
13/// **The lossy shape is deliberate** (the alternative was to make
14/// `TrxError` parametric over the SPI / RESET / DIO0 error types). Reasons:
15///
16/// - Mirrors the `embassy-net::Stack` pattern — high-level Stack APIs
17/// should not leak the underlying driver's error types across the
18/// abstraction they were created to draw.
19/// - Keeps `Stack<'a>`, `Runner<'a, TRX>`, and the channel slot type
20/// free of an extra error-type generic, so user code threading `Stack`
21/// around stays terse.
22/// - Future multi-radio backends get to share one stable error
23/// vocabulary; user code keeps working when swapping backings.
24/// - Debuggability is preserved by the [`Runner`](crate::Runner) logging
25/// the underlying error chain via the internal `error!` macro before
26/// handing the collapsed `TrxError` to the user.
27///
28/// Power users who want the full parametric error chain can call the
29/// inherent `Rfm69::send` / `Rfm69::recv` directly, which still return
30/// `Error<SPI, RESET, DIO0>` — the lossy collapse only happens at the
31/// `Transceiver` boundary.
32#[derive(Debug)]
33#[cfg_attr(feature = "defmt", derive(defmt::Format))]
34pub enum TrxError {
35 /// Reading the version register failed or returned an unexpected value
36 /// during configuration / reset.
37 TrxNotFound,
38 /// The reset GPIO write failed.
39 Reset,
40 /// An SPI bus transaction failed.
41 Spi,
42 /// A DIO0 GPIO read or interrupt-wait failed.
43 Gpio,
44 /// A configuration step (e.g. sync-word size) was rejected.
45 Config,
46 /// A packet's framing was invalid (length out of range, etc.).
47 WrongPacketFormat,
48 /// The `Transceiver` has no active-recovery implementation. Returned by
49 /// the default [`Transceiver::recover`] so the `Runner` keeps the link
50 /// `Down` rather than treating an unimplemented recovery as success.
51 RecoverUnsupported,
52}
53
54// `async fn in trait` deliberately leaves the Send-bound on the returned
55// futures unspecified. embassy on the targets this crate supports is
56// single-executor, so Send isn't needed; if a future user runs across
57// executor threads they can desugar a wrapper trait that adds the bound.
58#[expect(async_fn_in_trait)]
59pub trait Transceiver {
60 async fn send(&mut self, packet: &Packet) -> Result<(), TrxError>;
61 async fn recv(&mut self) -> Result<Packet, TrxError>;
62
63 /// Hook the `Runner` invokes when the link transitions to
64 /// [`LinkState::Down`](crate::LinkState::Down) — a streak of consecutive
65 /// `TrxError`s on any radio operation.
66 ///
67 /// Implementations should drive the radio back to a usable state (e.g.
68 /// pulse `RESET` and re-apply a `config::*` helper). On `Ok(())` the
69 /// `Runner` resumes normal operation; the next successful `send` / `recv`
70 /// then flips the link back to `Up`. On `Err(_)` the `Runner` keeps the
71 /// link `Down` and re-invokes `recover` after a backoff configured on
72 /// `MacTiming`.
73 ///
74 /// Default: returns [`TrxError::RecoverUnsupported`]. A radio that doesn't
75 /// override `recover` therefore stays `Down` once the link drops — the
76 /// `Runner` keeps retrying `recover` every `MacTiming::recover_backoff`
77 /// but never makes progress. Override this to opt into active recovery.
78 async fn recover(&mut self) -> Result<(), TrxError> {
79 Err(TrxError::RecoverUnsupported)
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 /// A `Transceiver` that doesn't override `recover`, so it exercises the
88 /// trait's default. `send` / `recv` are never polled by the test.
89 struct NoRecover;
90 impl Transceiver for NoRecover {
91 async fn send(&mut self, _packet: &Packet) -> Result<(), TrxError> {
92 unreachable!()
93 }
94 async fn recv(&mut self) -> Result<Packet, TrxError> {
95 unreachable!()
96 }
97 }
98
99 #[test]
100 fn default_recover_reports_unsupported() {
101 // Guards the Runner contract: an impl without active recovery must
102 // surface an error so the link stays `Down`, not a silent `Ok`.
103 let mut trx = NoRecover;
104 let result = futures::executor::block_on(trx.recover());
105 assert!(matches!(result, Err(TrxError::RecoverUnsupported)));
106 }
107}