Skip to main content

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}