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}