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}