umsh_hal/
lib.rs

1#![allow(async_fn_in_trait)]
2#![cfg_attr(not(feature = "std"), no_std)]
3
4//! Minimal hardware abstraction traits used by the higher UMSH layers.
5//!
6//! This crate is intentionally independent from the rest of the workspace so
7//! platform-specific radio or storage backends can depend on it without pulling
8//! in the full protocol stack.
9
10use core::num::NonZeroU8;
11use core::task::{Context, Poll};
12
13/// Signal-to-noise ratio represented in centibels (0.1 dB units).
14///
15/// This uses a slightly finer unit than whole decibels while still staying
16/// compact and integer-friendly. Some common LoRa radios report SNR in
17/// quarter-dB steps. Converting those readings into centibels requires
18/// rounding, introducing at most 0.5 cB (0.05 dB) of error.
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
20pub struct Snr(i16);
21
22impl Snr {
23    /// Construct an SNR value directly from centibels.
24    pub const fn from_centibels(centibels: i16) -> Self {
25        Self(centibels)
26    }
27
28    /// Construct an SNR value from whole decibels.
29    pub const fn from_decibels(db: i8) -> Self {
30        Self((db as i16) * 10)
31    }
32
33    /// Construct an SNR value from quarter-dB steps, rounding to the nearest
34    /// centibel.
35    pub const fn from_quarter_db_steps(steps: i16) -> Self {
36        let scaled = steps * 25;
37        let rounded = if scaled >= 0 {
38            (scaled + 5) / 10
39        } else {
40            (scaled - 5) / 10
41        };
42        Self(rounded)
43    }
44
45    /// Return the stored value in centibels.
46    pub const fn as_centibels(self) -> i16 {
47        self.0
48    }
49}
50
51/// Metadata returned with a received frame.
52pub struct RxInfo {
53    /// Number of bytes written into the receive buffer.
54    pub len: usize,
55    /// Received signal strength in dBm.
56    pub rssi: i16,
57    /// Signal-to-noise ratio in centibels.
58    pub snr: Snr,
59    /// Optional link-quality indicator in a radio-specific normalized scale.
60    pub lqi: Option<NonZeroU8>,
61}
62
63/// Options controlling how a frame is transmitted.
64#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
65pub struct TxOptions {
66    /// Carrier-activity detection policy applied before transmit.
67    ///
68    /// `None` skips CAD and transmits immediately.
69    /// `Some(0)` performs an immediate CAD gate and only transmits if the
70    /// channel is currently clear.
71    /// `Some(n)` retries CAD until it succeeds or the timeout budget expires.
72    pub cad_timeout_ms: Option<u32>,
73}
74
75/// Error returned by [`Radio::transmit`].
76#[derive(Clone, Copy, Debug, Eq, PartialEq)]
77pub enum TxError<E> {
78    /// CAD did not find the channel clear before the timeout expired.
79    CadTimeout,
80    /// Platform-specific radio or transport failure.
81    Io(E),
82}
83
84/// Half-duplex radio abstraction used by the MAC coordinator.
85pub trait Radio {
86    type Error;
87
88    /// Transmit a complete raw UMSH frame.
89    async fn transmit(
90        &mut self,
91        data: &[u8],
92        options: TxOptions,
93    ) -> Result<(), TxError<Self::Error>>;
94
95    /// Poll reception of one frame into `buf`.
96    ///
97    /// `Poll::Pending` means no frame is currently available right now. The
98    /// call does not reserve any receive state; a later poll after transmit
99    /// completion can resume probing immediately.
100    fn poll_receive(
101        &mut self,
102        cx: &mut Context<'_>,
103        buf: &mut [u8],
104    ) -> Poll<Result<RxInfo, Self::Error>>;
105
106    /// Return the largest supported raw frame size.
107    fn max_frame_size(&self) -> usize;
108    /// Return the approximate airtime for a maximum-length frame.
109    fn t_frame_ms(&self) -> u32;
110}
111
112/// Monotonic millisecond clock.
113pub trait Clock {
114    /// Return milliseconds since an arbitrary monotonic epoch.
115    fn now_ms(&self) -> u64;
116
117    /// Poll a delay that completes when the monotonic clock reaches `deadline_ms`.
118    ///
119    /// Returns `Poll::Ready(())` if the deadline has already passed.  Otherwise
120    /// the implementation should register `cx.waker()` with a platform timer and
121    /// return `Poll::Pending`.
122    ///
123    /// The default implementation returns `Ready(())` immediately, which causes
124    /// callers to busy-poll on timer deadlines.  Platform clocks backed by a
125    /// real timer (tokio, embassy, etc.) should override this.
126    fn poll_delay_until(&self, cx: &mut Context<'_>, deadline_ms: u64) -> Poll<()> {
127        let _ = (cx, deadline_ms);
128        Poll::Ready(())
129    }
130}
131
132/// Persistent frame-counter storage.
133pub trait CounterStore {
134    type Error;
135
136    /// Load the stored counter for `context`, or `0` if missing.
137    async fn load(&self, context: &[u8]) -> Result<u32, Self::Error>;
138    /// Persist a counter value for `context`.
139    async fn store(&self, context: &[u8], value: u32) -> Result<(), Self::Error>;
140    /// Flush any buffered state to durable storage.
141    async fn flush(&self) -> Result<(), Self::Error>;
142}
143
144/// Persistent key-value store used by higher layers for cached state.
145pub trait KeyValueStore {
146    type Error;
147
148    /// Load a value into `buf`, returning the stored length when present.
149    async fn load(&self, key: &[u8], buf: &mut [u8]) -> Result<Option<usize>, Self::Error>;
150    /// Store a value for `key`.
151    async fn store(&self, key: &[u8], value: &[u8]) -> Result<(), Self::Error>;
152    /// Delete any stored value for `key`.
153    async fn delete(&self, key: &[u8]) -> Result<(), Self::Error>;
154}