umsh_mac/handle.rs
1use core::future::poll_fn;
2
3use rand::Rng;
4use umsh_core::{ChannelId, ChannelKey, PublicKey};
5use umsh_hal::{Clock, CounterStore};
6use umsh_sync::AsyncRefCell;
7
8use crate::{
9 AddPeerError, CapacityError, DEFAULT_ACKS, DEFAULT_CHANNEL_HINT_REPLAY, DEFAULT_CHANNEL_REPLAY,
10 DEFAULT_CHANNELS, DEFAULT_DUP, DEFAULT_FRAME, DEFAULT_IDENTITIES, DEFAULT_PEERS, DEFAULT_TX,
11 Platform,
12 coordinator::{CounterPersistenceError, LocalIdentityId, Mac, MacError, SendError},
13 peers::PeerId,
14 send::{SendOptions, SendReceipt},
15};
16
17/// Lightweight, cloneable handle for queuing MAC operations against shared state.
18///
19/// The handle borrows an [`AsyncRefCell`] that owns the underlying coordinator.
20/// Every operation takes the cell asynchronously: if another caller currently
21/// holds the coordinator (for example, the long-running `run()` loop that is
22/// waiting on the radio), operations wait rather than failing.
23pub struct MacHandle<
24 'a,
25 P: Platform,
26 const IDENTITIES: usize = DEFAULT_IDENTITIES,
27 const PEERS: usize = DEFAULT_PEERS,
28 const CHANNELS: usize = DEFAULT_CHANNELS,
29 const ACKS: usize = DEFAULT_ACKS,
30 const TX: usize = DEFAULT_TX,
31 const FRAME: usize = DEFAULT_FRAME,
32 const DUP: usize = DEFAULT_DUP,
33 const RN: usize = DEFAULT_CHANNEL_REPLAY,
34 const HN: usize = DEFAULT_CHANNEL_HINT_REPLAY,
35> {
36 mac: &'a AsyncRefCell<Mac<P, IDENTITIES, PEERS, CHANNELS, ACKS, TX, FRAME, DUP, RN, HN>>,
37}
38
39impl<
40 'a,
41 P: Platform,
42 const IDENTITIES: usize,
43 const PEERS: usize,
44 const CHANNELS: usize,
45 const ACKS: usize,
46 const TX: usize,
47 const FRAME: usize,
48 const DUP: usize,
49 const RN: usize,
50 const HN: usize,
51> Copy for MacHandle<'a, P, IDENTITIES, PEERS, CHANNELS, ACKS, TX, FRAME, DUP, RN, HN>
52{
53}
54
55impl<
56 'a,
57 P: Platform,
58 const IDENTITIES: usize,
59 const PEERS: usize,
60 const CHANNELS: usize,
61 const ACKS: usize,
62 const TX: usize,
63 const FRAME: usize,
64 const DUP: usize,
65 const RN: usize,
66 const HN: usize,
67> Clone for MacHandle<'a, P, IDENTITIES, PEERS, CHANNELS, ACKS, TX, FRAME, DUP, RN, HN>
68{
69 fn clone(&self) -> Self {
70 *self
71 }
72}
73
74impl<
75 'a,
76 P: Platform,
77 const IDENTITIES: usize,
78 const PEERS: usize,
79 const CHANNELS: usize,
80 const ACKS: usize,
81 const TX: usize,
82 const FRAME: usize,
83 const DUP: usize,
84 const RN: usize,
85 const HN: usize,
86> MacHandle<'a, P, IDENTITIES, PEERS, CHANNELS, ACKS, TX, FRAME, DUP, RN, HN>
87{
88 /// Creates a cloneable handle backed by shared coordinator state.
89 pub fn new(
90 mac: &'a AsyncRefCell<Mac<P, IDENTITIES, PEERS, CHANNELS, ACKS, TX, FRAME, DUP, RN, HN>>,
91 ) -> Self {
92 Self { mac }
93 }
94
95 /// Cumulative frame tallies from the shared coordinator.
96 pub async fn counters(&self) -> crate::MacCounters {
97 self.mac.borrow().await.counters()
98 }
99
100 /// Registers a local identity with the shared coordinator.
101 pub async fn add_identity(
102 &self,
103 identity: P::Identity,
104 ) -> Result<LocalIdentityId, CapacityError> {
105 self.mac.borrow_mut().await.add_identity(identity)
106 }
107
108 /// Load the persisted frame-counter boundary for one identity.
109 pub async fn load_persisted_counter(
110 &self,
111 id: LocalIdentityId,
112 ) -> Result<u32, CounterPersistenceError<<P::CounterStore as CounterStore>::Error>> {
113 self.mac.borrow_mut().await.load_persisted_counter(id).await
114 }
115
116 /// Persist all currently scheduled frame-counter reservations.
117 pub async fn service_counter_persistence(
118 &self,
119 ) -> Result<usize, <P::CounterStore as CounterStore>::Error> {
120 self.mac
121 .borrow_mut()
122 .await
123 .service_counter_persistence()
124 .await
125 }
126
127 /// Persist every frame-counter boundary — TX reservations and every
128 /// peer's RX replay boundary — to durable storage, now.
129 ///
130 /// The pump persists on a block cadence, which is the right trade
131 /// against flash wear right up until the device is about to reset on
132 /// purpose. Call this before a deliberate reboot: a replay boundary
133 /// left in RAM is a command the device will happily execute again
134 /// when the sender's retry arrives after boot.
135 pub async fn flush_frame_counters(&self) -> Result<(), ()> {
136 let mut mac = self.mac.borrow_mut().await;
137 let tx = mac.service_counter_persistence().await.map(|_| ());
138 let rx = mac.persist_all_rx_counters().await.map(|_| ());
139 tx.and(rx).map_err(|_| ())
140 }
141
142 /// Whether the transmit queue is empty.
143 ///
144 /// The coordinator borrow is held across a frame's whole time on the
145 /// air, so an answer of `true` from this method means every queued
146 /// frame — a pending MAC acknowledgment above all — has finished
147 /// transmitting, not merely left the queue.
148 pub async fn tx_queue_empty(&self) -> bool {
149 self.mac.borrow().await.tx_queue().is_empty()
150 }
151
152 /// Load persisted RX counter boundaries for all registered peers from
153 /// durable storage, storing them in each peer's [`PeerInfo::initial_rx_counter`].
154 ///
155 /// Call this once at boot, after all known peers have been registered with
156 /// [`add_peer`](Self::add_peer), and before the first call to
157 /// [`next_event`](crate::Mac::next_event). When pairwise keys are later
158 /// derived for a peer, the replay window is automatically initialized to
159 /// the loaded boundary.
160 pub async fn load_all_persisted_rx_counters(
161 &self,
162 ) -> Result<usize, <P::CounterStore as CounterStore>::Error> {
163 self.mac
164 .borrow_mut()
165 .await
166 .load_all_persisted_rx_counters()
167 .await
168 }
169
170 /// Registers or refreshes a remote peer in the shared registry.
171 pub async fn add_peer(&self, key: PublicKey) -> Result<PeerId, AddPeerError> {
172 self.mac.borrow_mut().await.add_peer(key)
173 }
174
175 /// Removes a registered peer and its per-peer transport state, reporting
176 /// whether the peer was registered. Persisted RX counter boundaries are
177 /// retained so replay protection survives a later re-add.
178 pub async fn remove_peer(&self, key: &PublicKey) -> bool {
179 self.mac.borrow_mut().await.remove_peer(key)
180 }
181
182 /// Ensures `key` is registered at least transiently (unpinned,
183 /// LRU-evictable), so an explicit reply to a stranger has a slot to send
184 /// through. Returns whether a slot is held.
185 pub async fn ensure_transient_peer(&self, key: &PublicKey) -> bool {
186 self.mac
187 .borrow_mut()
188 .await
189 .ensure_transient_peer(key)
190 .is_ok()
191 }
192
193 /// Adds or updates a shared channel and derives its multicast keys.
194 pub async fn add_channel(&self, key: ChannelKey) -> Result<(), CapacityError> {
195 self.mac.borrow_mut().await.add_channel(key)
196 }
197
198 /// Removes a previously added channel by its exact key. Returns
199 /// whether a channel was removed.
200 pub async fn remove_channel(&self, key: &ChannelKey) -> bool {
201 self.mac.borrow_mut().await.remove_channel(key)
202 }
203
204 /// Adds or updates a named channel using the coordinator's channel-key derivation.
205 ///
206 /// The name is canonicalized (ASCII lowercase fold) before derivation.
207 pub async fn add_named_channel(&self, name: &str) -> Result<(), crate::AddChannelError> {
208 self.mac.borrow_mut().await.add_named_channel(name)
209 }
210
211 /// Return whether inbound secure packets carrying a full source key may auto-register peers.
212 pub async fn auto_register_full_key_peers(&self) -> bool {
213 self.mac.borrow().await.auto_register_full_key_peers()
214 }
215
216 /// Enable or disable inbound full-key peer auto-registration.
217 pub async fn set_auto_register_full_key_peers(&self, enabled: bool) {
218 self.mac
219 .borrow_mut()
220 .await
221 .set_auto_register_full_key_peers(enabled);
222 }
223
224 /// Whether the MAC autonomously forwards overheard routable frames.
225 pub async fn repeater_enabled(&self) -> bool {
226 self.mac.borrow().await.repeater_config().enabled
227 }
228
229 /// Enable or disable autonomous repeater forwarding. Only the master
230 /// `enabled` switch is touched; every other [`RepeaterConfig`] field
231 /// (regions, RSSI/SNR gates, contention tuning) keeps its current
232 /// value. Toggling at runtime is safe: forwarding simply starts or
233 /// stops honoring newly received frames.
234 pub async fn set_repeater_enabled(&self, enabled: bool) {
235 self.mac.borrow_mut().await.repeater_config_mut().enabled = enabled;
236 }
237
238 /// Replace the forwarding policy applied to flood-forwarded frames.
239 ///
240 /// All four values are set together, since they are configured together
241 /// by whoever administers the repeater; passing an empty `regions` slice
242 /// or `None` clears that gate rather than leaving the previous value in
243 /// place. The master `enabled` switch, the flood-contention tuning, and
244 /// the amateur-radio fields are deliberately untouched — those are
245 /// separate concerns with their own accessors.
246 ///
247 /// Region codes beyond the configured capacity are ignored; callers that
248 /// need to know should check the returned count of codes actually stored.
249 pub async fn set_repeater_policy(
250 &self,
251 regions: &[[u8; 2]],
252 default_region: Option<[u8; 2]>,
253 min_rssi: Option<i16>,
254 min_snr: Option<i8>,
255 ) -> usize {
256 let mut mac = self.mac.borrow_mut().await;
257 let config = mac.repeater_config_mut();
258 config.regions.clear();
259 for region in regions {
260 if config.regions.push(*region).is_err() {
261 break;
262 }
263 }
264 config.default_region = default_region;
265 config.min_rssi = min_rssi;
266 config.min_snr = min_snr;
267 config.regions.len()
268 }
269
270 /// Installs pairwise transport keys for one local identity and remote peer.
271 ///
272 /// This is a crate-internal method. External callers should use the
273 /// `unsafe-advanced` feature or go through the node-layer PFS session manager.
274 #[cfg(any(feature = "unsafe-advanced", test))]
275 pub(crate) async fn install_pairwise_keys(
276 &self,
277 identity_id: LocalIdentityId,
278 peer_id: PeerId,
279 pairwise_keys: umsh_crypto::PairwiseKeys,
280 ) -> Result<Option<crate::peers::PeerCryptoState>, SendError> {
281 self.mac
282 .borrow_mut()
283 .await
284 .install_pairwise_keys(identity_id, peer_id, pairwise_keys)
285 }
286
287 /// Installs pairwise transport keys for one local identity and remote peer.
288 ///
289 /// # Safety (logical)
290 /// Installing wrong keys will silently corrupt the session. This method
291 /// is deliberately gated behind the `unsafe-advanced` feature. Prefer
292 /// going through the node-layer PFS session manager instead.
293 #[cfg(feature = "unsafe-advanced")]
294 pub async fn install_pairwise_keys_advanced(
295 &self,
296 identity_id: LocalIdentityId,
297 peer_id: PeerId,
298 pairwise_keys: umsh_crypto::PairwiseKeys,
299 ) -> Result<Option<crate::peers::PeerCryptoState>, SendError> {
300 self.install_pairwise_keys(identity_id, peer_id, pairwise_keys)
301 .await
302 }
303
304 /// Enqueues a broadcast frame for transmission.
305 pub async fn send_broadcast(
306 &self,
307 from: LocalIdentityId,
308 payload: &[u8],
309 options: &SendOptions,
310 ) -> Result<SendReceipt, SendError> {
311 self.mac
312 .borrow_mut()
313 .await
314 .send_broadcast(from, payload, options)
315 .await
316 }
317
318 /// Enqueues a multicast frame for transmission.
319 pub async fn send_multicast(
320 &self,
321 from: LocalIdentityId,
322 channel: &ChannelId,
323 payload: &[u8],
324 options: &SendOptions,
325 ) -> Result<SendReceipt, SendError> {
326 self.mac
327 .borrow_mut()
328 .await
329 .send_multicast(from, channel, payload, options)
330 .await
331 }
332
333 /// Enqueues a unicast frame for transmission.
334 pub async fn send_unicast(
335 &self,
336 from: LocalIdentityId,
337 dst: &PublicKey,
338 payload: &[u8],
339 options: &SendOptions,
340 ) -> Result<Option<SendReceipt>, SendError> {
341 self.mac
342 .borrow_mut()
343 .await
344 .send_unicast(from, dst, payload, options)
345 .await
346 }
347
348 /// Enqueues a blind-unicast frame for transmission.
349 pub async fn send_blind_unicast(
350 &self,
351 from: LocalIdentityId,
352 dst: &PublicKey,
353 channel: &ChannelId,
354 payload: &[u8],
355 options: &SendOptions,
356 ) -> Result<Option<SendReceipt>, SendError> {
357 self.mac
358 .borrow_mut()
359 .await
360 .send_blind_unicast(from, dst, channel, payload, options)
361 .await
362 }
363
364 /// Drive the shared MAC until one wake cycle completes and invoke `on_event` for emitted events.
365 ///
366 /// The exclusive borrow on the shared coordinator is released between
367 /// every internal phase so that other handles (CLI sends, UI queries,
368 /// counter-persistence services) can interleave their own async work
369 /// while this driver is waiting on the radio or a timer.
370 pub async fn next_event(
371 &self,
372 mut on_event: impl FnMut(LocalIdentityId, crate::MacEventRef<'_>),
373 ) -> Result<(), MacError<<P::Radio as umsh_hal::Radio>::Error>> {
374 loop {
375 // Phase 1: drain any ready transmit work.
376 self.mac
377 .borrow_mut()
378 .await
379 .drain_tx_queue(&mut on_event)
380 .await?;
381
382 // Phase 2: wait for a radio frame or timer deadline. Acquire the
383 // borrow briefly each poll so concurrent tasks can obtain it too.
384 // `poll_with_mut` keeps us registered on the cell's wake condition
385 // across Pending polls, so we re-poll both when the cell frees up
386 // and when another handle mutates coordinator state (e.g.
387 // `cli.send_unicast` enqueues a frame and drops its borrow) —
388 // without that, TX queued by concurrent handles would sit until
389 // the next radio/timer event. It also deregisters us around our
390 // own borrow so our guard release cannot self-wake into a spin,
391 // and the scoped ticket deregisters on drop so this wait can be
392 // cancelled (e.g. losing a `select!` race) without leaking its
393 // waker registration.
394 let mut buf = [0u8; FRAME];
395 let mut cond_ticket = self.mac.scoped_ticket();
396 let reason = poll_fn(|cx| {
397 self.mac.poll_with_mut(cx, &mut cond_ticket, |mac, cx| {
398 // Register radio/timer wakers and check readiness in one shot.
399 mac.poll_wait_for_wake(cx, &mut buf)
400 })
401 })
402 .await
403 .map_err(MacError::Radio)?;
404 drop(cond_ticket);
405
406 // Phases 3-5: re-acquire the borrow and finish the cycle.
407 self.mac
408 .borrow_mut()
409 .await
410 .process_wake_reason(reason, &mut buf, &mut on_event)
411 .await?;
412
413 // Flush any pending TX or RX counter boundaries to durable storage.
414 // Mirrors `Mac::next_event`. Errors are intentionally ignored —
415 // persistence is best-effort and must not block the radio event
416 // loop. Borrow is dropped before the next phase.
417 {
418 let mut mac = self.mac.borrow_mut().await;
419 let _ = mac.service_counter_persistence().await;
420 let _ = mac.service_rx_counter_persistence().await;
421 }
422
423 // If new transmit work appeared during processing (e.g. a
424 // retransmit was enqueued), loop back to drain it before
425 // waiting again.
426 let tx_empty = self.mac.borrow().await.tx_queue().is_empty();
427 if !tx_empty {
428 continue;
429 }
430 return Ok(());
431 }
432 }
433
434 /// Drive the shared MAC forever, invoking `on_event` for delivered events.
435 ///
436 /// This is the preferred long-lived driver API for standalone MAC-backed tasks.
437 pub async fn run(
438 &self,
439 mut on_event: impl FnMut(LocalIdentityId, crate::MacEventRef<'_>),
440 ) -> Result<(), MacError<<P::Radio as umsh_hal::Radio>::Error>> {
441 loop {
442 self.next_event(&mut on_event).await?;
443 }
444 }
445
446 /// Drive the shared MAC forever while ignoring emitted events.
447 pub async fn run_quiet(&self) -> Result<(), MacError<<P::Radio as umsh_hal::Radio>::Error>> {
448 self.run(|_, _| {}).await
449 }
450
451 /// Fills a caller-provided buffer with random bytes from the shared coordinator RNG.
452 pub async fn fill_random(&self, dest: &mut [u8]) {
453 self.mac.borrow_mut().await.rng_mut().fill_bytes(dest);
454 }
455
456 /// Returns the current coordinator clock time in milliseconds.
457 pub async fn now_ms(&self) -> u64 {
458 self.mac.borrow().await.clock().now_ms()
459 }
460
461 #[cfg(feature = "software-crypto")]
462 /// Registers an ephemeral software identity with the shared coordinator.
463 pub async fn register_ephemeral(
464 &self,
465 parent: LocalIdentityId,
466 identity: umsh_crypto::software::SoftwareIdentity,
467 ) -> Result<LocalIdentityId, CapacityError> {
468 self.mac
469 .borrow_mut()
470 .await
471 .register_ephemeral(parent, identity)
472 }
473
474 #[cfg(feature = "software-crypto")]
475 /// Removes a previously registered ephemeral identity.
476 pub async fn remove_ephemeral(&self, id: LocalIdentityId) -> bool {
477 self.mac.borrow_mut().await.remove_ephemeral(id)
478 }
479
480 /// Cancel a pending ACK-requested send, stopping retransmissions.
481 ///
482 /// Returns `true` if the pending ACK was found and removed.
483 pub async fn cancel_pending_ack(
484 &self,
485 identity_id: LocalIdentityId,
486 receipt: SendReceipt,
487 ) -> bool {
488 self.mac
489 .borrow_mut()
490 .await
491 .cancel_pending_ack(identity_id, receipt)
492 }
493
494 /// Return the live TX frame counter for one identity, if registered.
495 pub async fn frame_counter(&self, id: LocalIdentityId) -> Option<u32> {
496 self.mac
497 .borrow()
498 .await
499 .identity(id)
500 .map(|slot| slot.frame_counter())
501 }
502
503 /// Return the persisted TX frame-counter boundary for one identity, if registered.
504 pub async fn persisted_frame_counter(&self, id: LocalIdentityId) -> Option<u32> {
505 self.mac
506 .borrow()
507 .await
508 .identity(id)
509 .map(|slot| slot.persisted_counter())
510 }
511
512 /// Invoke `f` for every peer currently registered in the shared registry.
513 ///
514 /// This covers all known peers, not just those with an active crypto session.
515 pub async fn for_each_peer(&self, f: &mut dyn FnMut(umsh_core::PublicKey)) {
516 let mac = self.mac.borrow().await;
517 for (_, info) in mac.peer_registry().iter() {
518 f(info.public_key);
519 }
520 }
521
522 /// Invoke `f` for each transmitter heard on the air, most recent
523 /// reception first held.
524 ///
525 /// Copies each observation out rather than lending the table, so the
526 /// borrow ends with the call and a caller may send while it walks them.
527 pub async fn for_each_transmitter_observation(
528 &self,
529 f: &mut dyn FnMut(crate::TransmitterObservation),
530 ) {
531 let mac = self.mac.borrow().await;
532 for observation in mac.transmitter_observations().iter() {
533 f(*observation);
534 }
535 }
536
537 /// Return the route currently cached for `peer`, if the peer is registered
538 /// and a route has been learned for it.
539 pub async fn peer_route(&self, peer: &umsh_core::PublicKey) -> Option<crate::CachedRoute> {
540 let mac = self.mac.borrow().await;
541 let (peer_id, _) = mac.peer_registry().lookup_by_key(peer)?;
542 mac.peer_registry().get(peer_id)?.route.clone()
543 }
544
545 /// Forget the route cached for `peer`, returning whether one was held.
546 pub async fn clear_peer_route(&self, peer: &umsh_core::PublicKey) -> bool {
547 let mut mac = self.mac.borrow_mut().await;
548 let Some((peer_id, _)) = mac.peer_registry().lookup_by_key(peer) else {
549 return false;
550 };
551 mac.peer_registry_mut().clear_route(peer_id)
552 }
553
554 /// Install a route learned earlier, returning whether the peer was
555 /// registered to receive it.
556 ///
557 /// The counterpart to [`Self::peer_route`], for a host that outlives
558 /// the MAC it is talking through. A tool invoked once per command
559 /// builds a new MAC every time and would otherwise rediscover the
560 /// same path on every invocation, flooding the mesh to learn what it
561 /// already knew a second ago.
562 ///
563 /// Only for a peer already registered: a route to somebody nothing
564 /// is talking to has nowhere to live, and inventing a registration
565 /// for it would evict a peer that is in use. A stale route costs one
566 /// failed exchange, after which the ordinary ack-timeout retry
567 /// rediscovers the path and overwrites this.
568 pub async fn restore_peer_route(
569 &self,
570 peer: &umsh_core::PublicKey,
571 route: crate::CachedRoute,
572 ) -> bool {
573 let mut mac = self.mac.borrow_mut().await;
574 let Some((peer_id, _)) = mac.peer_registry().lookup_by_key(peer) else {
575 return false;
576 };
577 mac.peer_registry_mut().update_route(peer_id, route);
578 true
579 }
580
581 /// Invoke `f` for each peer with an established crypto state for `id`,
582 /// passing the peer's public key, last-accepted RX counter, and persisted RX boundary.
583 pub async fn for_each_peer_counter(
584 &self,
585 id: LocalIdentityId,
586 f: &mut dyn FnMut(umsh_core::PublicKey, u32, u32),
587 ) {
588 let mac = self.mac.borrow().await;
589 let Some(slot) = mac.identity(id) else {
590 return;
591 };
592 for (peer_id, state) in slot.peer_crypto().iter() {
593 let Some(info) = mac.peer_registry().get(*peer_id) else {
594 continue;
595 };
596 f(
597 info.public_key,
598 state.replay_window.last_accepted,
599 state.persisted_rx_counter,
600 );
601 }
602 }
603}