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}