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#[cfg(feature = "embassy")]
14mod embassy_clock;
15#[cfg(feature = "embassy")]
16pub use embassy_clock::EmbassyClock;
17
18pub mod wall_clock;
19
20/// Signal-to-noise ratio represented in centibels (0.1 dB units).
21///
22/// This uses a slightly finer unit than whole decibels while still staying
23/// compact and integer-friendly. Some common LoRa radios report SNR in
24/// quarter-dB steps. Converting those readings into centibels requires
25/// rounding, introducing at most 0.5 cB (0.05 dB) of error.
26#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
27pub struct Snr(i16);
28
29impl Snr {
30    /// Construct an SNR value directly from centibels.
31    pub const fn from_centibels(centibels: i16) -> Self {
32        Self(centibels)
33    }
34
35    /// Construct an SNR value from whole decibels.
36    pub const fn from_decibels(db: i8) -> Self {
37        Self((db as i16) * 10)
38    }
39
40    /// Construct an SNR value from quarter-dB steps, rounding to the nearest
41    /// centibel.
42    pub const fn from_quarter_db_steps(steps: i16) -> Self {
43        let scaled = steps * 25;
44        let rounded = if scaled >= 0 {
45            (scaled + 5) / 10
46        } else {
47            (scaled - 5) / 10
48        };
49        Self(rounded)
50    }
51
52    /// Return the stored value in centibels.
53    pub const fn as_centibels(self) -> i16 {
54        self.0
55    }
56
57    /// Return the stored value in decibels.
58    pub const fn as_decibels(self) -> i16 {
59        self.0 / 10
60    }
61}
62
63impl core::fmt::Display for Snr {
64    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
65        let db = self.as_decibels();
66        let cdb = self.0 % 10;
67        write!(f, "{}.{:01}dB", db, cdb)
68    }
69}
70
71impl core::fmt::Debug for Snr {
72    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
73        write!(f, "Snr({self})")
74    }
75}
76
77/// Metadata returned with a received frame.
78#[derive(Clone, Copy)]
79pub struct RxInfo {
80    /// Number of bytes written into the receive buffer.
81    pub len: usize,
82    /// Received signal strength in dBm.
83    pub rssi: i16,
84    /// Signal-to-noise ratio in centibels.
85    pub snr: Snr,
86    /// Optional link-quality indicator in a radio-specific normalized scale.
87    pub lqi: Option<NonZeroU8>,
88}
89
90/// Channel-activity-detection (CAD) policy applied before a transmit.
91#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
92pub enum CadPolicy {
93    /// Skip CAD entirely and transmit immediately.
94    #[default]
95    Skip,
96    /// Perform CAD once and transmit only if the channel is currently clear.
97    /// Equivalent to a retry budget of zero.
98    Gate,
99    /// Retry CAD until the channel is clear or `timeout_ms` elapses.
100    RetryFor { timeout_ms: u32 },
101}
102
103/// Options controlling how a frame is transmitted.
104#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
105pub struct TxOptions {
106    /// Channel-activity-detection policy applied before this transmit.
107    pub cad: CadPolicy,
108}
109
110/// Error returned by [`Radio::transmit`].
111#[derive(Clone, Copy, Debug, Eq, PartialEq)]
112pub enum TxError<E> {
113    /// CAD did not find the channel clear before the timeout expired.
114    CadTimeout,
115    /// Platform-specific radio or transport failure.
116    Io(E),
117}
118
119/// Half-duplex radio abstraction used by the MAC coordinator.
120pub trait Radio {
121    type Error;
122
123    /// Transmit a complete raw UMSH frame.
124    async fn transmit(
125        &mut self,
126        data: &[u8],
127        options: TxOptions,
128    ) -> Result<(), TxError<Self::Error>>;
129
130    /// Poll reception of one frame into `buf`.
131    ///
132    /// `Poll::Pending` means no frame is currently available right now. The
133    /// call does not reserve any receive state; a later poll after transmit
134    /// completion can resume probing immediately.
135    fn poll_receive(
136        &mut self,
137        cx: &mut Context<'_>,
138        buf: &mut [u8],
139    ) -> Poll<Result<RxInfo, Self::Error>>;
140
141    /// Return the largest supported raw frame size.
142    fn max_frame_size(&self) -> usize;
143    /// Return the approximate airtime for a maximum-length frame.
144    fn t_frame_ms(&self) -> u32;
145}
146
147/// Monotonic millisecond clock.
148pub trait Clock {
149    /// Return milliseconds since an arbitrary monotonic epoch.
150    fn now_ms(&self) -> u64;
151
152    /// Poll a delay that completes when the monotonic clock reaches `deadline_ms`.
153    ///
154    /// Returns `Poll::Ready(())` if the deadline has already passed. Otherwise
155    /// the implementation MUST register `cx.waker()` with a platform timer and
156    /// return `Poll::Pending`, so the task is woken when the deadline elapses.
157    ///
158    /// The default implementation has no real timer: it schedules an immediate
159    /// re-poll (via `cx.waker().wake_by_ref()`) and returns `Poll::Pending`, causing
160    /// the caller to busy-poll until the monotonic clock reaches the deadline.
161    /// This is correct but wastes CPU; platform clocks backed by a real timer
162    /// (tokio, embassy, etc.) MUST override this to sleep efficiently. A default
163    /// that returned `Ready` unconditionally would spin just as hard; one that
164    /// returned `Pending` without waking would stall timer-driven work entirely.
165    fn poll_delay_until(&self, cx: &mut Context<'_>, deadline_ms: u64) -> Poll<()> {
166        let _ = deadline_ms;
167        cx.waker().wake_by_ref();
168        Poll::Pending
169    }
170}
171
172/// Persistent peer directory for the node layer.
173///
174/// Stores and retrieves peer records keyed by the raw 32-byte Ed25519 public
175/// key. `alias` is an optional UTF-8 label (the implementation may silently
176/// truncate values longer than 16 bytes). Implementors MUST treat `store_peer`
177/// as an upsert — calling it twice for the same key overwrites the record.
178pub trait PeerStore {
179    type Error;
180
181    /// Upsert the peer record for `key`. `alias`, if present, is a UTF-8
182    /// display label; passing `None` removes any existing alias.
183    async fn store_peer(&self, key: &[u8; 32], alias: Option<&[u8]>) -> Result<(), Self::Error>;
184
185    /// Remove the peer record for `key`. A no-op if the key is not present.
186    async fn delete_peer(&self, key: &[u8; 32]) -> Result<(), Self::Error>;
187
188    /// Invoke `f` for every persisted peer record.
189    ///
190    /// `alias` is `None` when no alias was stored for that key. Callers that
191    /// cannot handle an error mid-iteration should collect into a local buffer
192    /// first, then process asynchronously.
193    async fn for_each_peer(
194        &self,
195        f: &mut dyn FnMut(&[u8; 32], Option<&[u8]>),
196    ) -> Result<(), Self::Error>;
197}
198
199/// No-op peer store — use when peer persistence is not needed.
200pub struct NoPeerStore;
201
202impl PeerStore for NoPeerStore {
203    type Error = core::convert::Infallible;
204
205    async fn store_peer(&self, _: &[u8; 32], _: Option<&[u8]>) -> Result<(), Self::Error> {
206        Ok(())
207    }
208
209    async fn delete_peer(&self, _: &[u8; 32]) -> Result<(), Self::Error> {
210        Ok(())
211    }
212
213    async fn for_each_peer(
214        &self,
215        _: &mut dyn FnMut(&[u8; 32], Option<&[u8]>),
216    ) -> Result<(), Self::Error> {
217        Ok(())
218    }
219}
220
221/// Persistent channel directory for the node layer.
222///
223/// Stores and retrieves shared channel keys keyed by channel name (UTF-8,
224/// up to 16 bytes). Implementors MUST treat `store_channel` as an upsert.
225pub trait ChannelStore {
226    type Error;
227
228    /// Upsert the channel record for `name`.
229    async fn store_channel(&self, name: &[u8], key: &[u8; 32]) -> Result<(), Self::Error>;
230
231    /// Remove the channel record for `name`. A no-op if not present.
232    async fn delete_channel(&self, name: &[u8]) -> Result<(), Self::Error>;
233
234    /// Invoke `f` for every persisted channel record.
235    ///
236    /// `name` is UTF-8 channel name bytes; `key` is the 32-byte channel key.
237    async fn for_each_channel(
238        &self,
239        f: &mut dyn FnMut(&[u8], &[u8; 32]),
240    ) -> Result<(), Self::Error>;
241}
242
243/// No-op channel store — use when channel persistence is not needed.
244pub struct NoChannelStore;
245
246impl ChannelStore for NoChannelStore {
247    type Error = core::convert::Infallible;
248
249    async fn store_channel(&self, _: &[u8], _: &[u8; 32]) -> Result<(), Self::Error> {
250        Ok(())
251    }
252
253    async fn delete_channel(&self, _: &[u8]) -> Result<(), Self::Error> {
254        Ok(())
255    }
256
257    async fn for_each_channel(
258        &self,
259        _: &mut dyn FnMut(&[u8], &[u8; 32]),
260    ) -> Result<(), Self::Error> {
261        Ok(())
262    }
263}
264
265/// Persistent frame-counter storage.
266pub trait CounterStore {
267    type Error;
268
269    /// Load the stored counter for `context`, or `0` if missing.
270    async fn load(&self, context: &[u8]) -> Result<u32, Self::Error>;
271    /// Persist a counter value for `context`.
272    async fn store(&self, context: &[u8], value: u32) -> Result<(), Self::Error>;
273    /// Flush any buffered state to durable storage.
274    async fn flush(&self) -> Result<(), Self::Error>;
275}
276
277/// No-op counter store — counters restart from zero every boot.
278///
279/// Use only where counter persistence is not (yet) load-bearing: a node
280/// that sends nothing but unsecured broadcasts, or a bring-up stage whose
281/// durable store lands later. Reusing TX counters after a reboot breaks
282/// replay protection for secured traffic.
283pub struct NoCounterStore;
284
285impl CounterStore for NoCounterStore {
286    type Error = core::convert::Infallible;
287
288    async fn load(&self, _: &[u8]) -> Result<u32, Self::Error> {
289        Ok(0)
290    }
291
292    async fn store(&self, _: &[u8], _: u32) -> Result<(), Self::Error> {
293        Ok(())
294    }
295
296    async fn flush(&self) -> Result<(), Self::Error> {
297        Ok(())
298    }
299}
300
301/// Optional power-control hook for higher layers (e.g. the CLI).
302///
303/// Implementations request a controlled shutdown — the actual sequencing
304/// (display, storage flush, GPIO sense, entering System OFF, etc.) lives
305/// in the firmware that owns those peripherals. This call MUST return
306/// promptly; it's typically a `Signal::signal(())` to a shutdown task.
307pub trait PowerControl {
308    fn request_power_off(&self);
309
310    /// Request a soft reboot. The default implementation is a no-op; targets
311    /// without a wired reboot path can leave it as such. Like
312    /// [`request_power_off`](Self::request_power_off), this MUST return
313    /// promptly — typically a `Signal::signal(())` to a reboot task that
314    /// performs any final flushes before triggering a system reset.
315    fn request_reboot(&self) {}
316}
317
318/// No-op power control — use when shutdown is not implemented for the target.
319pub struct NoPowerControl;
320
321impl PowerControl for NoPowerControl {
322    fn request_power_off(&self) {}
323}
324
325/// Persistent key-value store used by higher layers for cached state.
326pub trait KeyValueStore {
327    type Error;
328
329    /// Load a value into `buf`, returning the stored length when present.
330    async fn load(&self, key: &[u8], buf: &mut [u8]) -> Result<Option<usize>, Self::Error>;
331    /// Store a value for `key`.
332    async fn store(&self, key: &[u8], value: &[u8]) -> Result<(), Self::Error>;
333    /// Delete any stored value for `key`.
334    async fn delete(&self, key: &[u8]) -> Result<(), Self::Error>;
335}
336
337/// No-op key-value store — loads find nothing, stores succeed silently.
338/// Use when a platform bundle needs the associated type but nothing in
339/// the deployment reads cached state back.
340pub struct NoKeyValueStore;
341
342impl KeyValueStore for NoKeyValueStore {
343    type Error = core::convert::Infallible;
344
345    async fn load(&self, _: &[u8], _: &mut [u8]) -> Result<Option<usize>, Self::Error> {
346        Ok(None)
347    }
348
349    async fn store(&self, _: &[u8], _: &[u8]) -> Result<(), Self::Error> {
350        Ok(())
351    }
352
353    async fn delete(&self, _: &[u8]) -> Result<(), Self::Error> {
354        Ok(())
355    }
356}