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    /// Load persisted RX counter boundaries for all registered peers from
128    /// durable storage, storing them in each peer's [`PeerInfo::initial_rx_counter`].
129    ///
130    /// Call this once at boot, after all known peers have been registered with
131    /// [`add_peer`](Self::add_peer), and before the first call to
132    /// [`next_event`](crate::Mac::next_event). When pairwise keys are later
133    /// derived for a peer, the replay window is automatically initialised to
134    /// the loaded boundary.
135    pub async fn load_all_persisted_rx_counters(
136        &self,
137    ) -> Result<usize, <P::CounterStore as CounterStore>::Error> {
138        self.mac
139            .borrow_mut()
140            .await
141            .load_all_persisted_rx_counters()
142            .await
143    }
144
145    /// Registers or refreshes a remote peer in the shared registry.
146    pub async fn add_peer(&self, key: PublicKey) -> Result<PeerId, AddPeerError> {
147        self.mac.borrow_mut().await.add_peer(key)
148    }
149
150    /// Removes a registered peer and its per-peer transport state, reporting
151    /// whether the peer was registered. Persisted RX counter boundaries are
152    /// retained so replay protection survives a later re-add.
153    pub async fn remove_peer(&self, key: &PublicKey) -> bool {
154        self.mac.borrow_mut().await.remove_peer(key)
155    }
156
157    /// Ensures `key` is registered at least transiently (unpinned,
158    /// LRU-evictable), so an explicit reply to a stranger has a slot to send
159    /// through. Returns whether a slot is held.
160    pub async fn ensure_transient_peer(&self, key: &PublicKey) -> bool {
161        self.mac
162            .borrow_mut()
163            .await
164            .ensure_transient_peer(key)
165            .is_ok()
166    }
167
168    /// Adds or updates a shared channel and derives its multicast keys.
169    pub async fn add_channel(&self, key: ChannelKey) -> Result<(), CapacityError> {
170        self.mac.borrow_mut().await.add_channel(key)
171    }
172
173    /// Removes a previously added channel by its exact key. Returns
174    /// whether a channel was removed.
175    pub async fn remove_channel(&self, key: &ChannelKey) -> bool {
176        self.mac.borrow_mut().await.remove_channel(key)
177    }
178
179    /// Adds or updates a named channel using the coordinator's channel-key derivation.
180    ///
181    /// The name is canonicalized (ASCII lowercase fold) before derivation.
182    pub async fn add_named_channel(&self, name: &str) -> Result<(), crate::AddChannelError> {
183        self.mac.borrow_mut().await.add_named_channel(name)
184    }
185
186    /// Return whether inbound secure packets carrying a full source key may auto-register peers.
187    pub async fn auto_register_full_key_peers(&self) -> bool {
188        self.mac.borrow().await.auto_register_full_key_peers()
189    }
190
191    /// Enable or disable inbound full-key peer auto-registration.
192    pub async fn set_auto_register_full_key_peers(&self, enabled: bool) {
193        self.mac
194            .borrow_mut()
195            .await
196            .set_auto_register_full_key_peers(enabled);
197    }
198
199    /// Whether the MAC autonomously forwards overheard routable frames.
200    pub async fn repeater_enabled(&self) -> bool {
201        self.mac.borrow().await.repeater_config().enabled
202    }
203
204    /// Enable or disable autonomous repeater forwarding. Only the master
205    /// `enabled` switch is touched; every other [`RepeaterConfig`] field
206    /// (regions, RSSI/SNR gates, contention tuning) keeps its current
207    /// value. Toggling at runtime is safe: forwarding simply starts or
208    /// stops honoring newly received frames.
209    pub async fn set_repeater_enabled(&self, enabled: bool) {
210        self.mac.borrow_mut().await.repeater_config_mut().enabled = enabled;
211    }
212
213    /// Replace the forwarding policy applied to flood-forwarded frames.
214    ///
215    /// All four values are set together, since they are configured together
216    /// by whoever administers the repeater; passing an empty `regions` slice
217    /// or `None` clears that gate rather than leaving the previous value in
218    /// place. The master `enabled` switch, the flood-contention tuning, and
219    /// the amateur-radio fields are deliberately untouched — those are
220    /// separate concerns with their own accessors.
221    ///
222    /// Region codes beyond the configured capacity are ignored; callers that
223    /// need to know should check the returned count of codes actually stored.
224    pub async fn set_repeater_policy(
225        &self,
226        regions: &[[u8; 2]],
227        default_region: Option<[u8; 2]>,
228        min_rssi: Option<i16>,
229        min_snr: Option<i8>,
230    ) -> usize {
231        let mut mac = self.mac.borrow_mut().await;
232        let config = mac.repeater_config_mut();
233        config.regions.clear();
234        for region in regions {
235            if config.regions.push(*region).is_err() {
236                break;
237            }
238        }
239        config.default_region = default_region;
240        config.min_rssi = min_rssi;
241        config.min_snr = min_snr;
242        config.regions.len()
243    }
244
245    /// Installs pairwise transport keys for one local identity and remote peer.
246    ///
247    /// This is a crate-internal method. External callers should use the
248    /// `unsafe-advanced` feature or go through the node-layer PFS session manager.
249    #[cfg(any(feature = "unsafe-advanced", test))]
250    pub(crate) async fn install_pairwise_keys(
251        &self,
252        identity_id: LocalIdentityId,
253        peer_id: PeerId,
254        pairwise_keys: umsh_crypto::PairwiseKeys,
255    ) -> Result<Option<crate::peers::PeerCryptoState>, SendError> {
256        self.mac
257            .borrow_mut()
258            .await
259            .install_pairwise_keys(identity_id, peer_id, pairwise_keys)
260    }
261
262    /// Installs pairwise transport keys for one local identity and remote peer.
263    ///
264    /// # Safety (logical)
265    /// Installing wrong keys will silently corrupt the session. This method
266    /// is deliberately gated behind the `unsafe-advanced` feature. Prefer
267    /// going through the node-layer PFS session manager instead.
268    #[cfg(feature = "unsafe-advanced")]
269    pub async fn install_pairwise_keys_advanced(
270        &self,
271        identity_id: LocalIdentityId,
272        peer_id: PeerId,
273        pairwise_keys: umsh_crypto::PairwiseKeys,
274    ) -> Result<Option<crate::peers::PeerCryptoState>, SendError> {
275        self.install_pairwise_keys(identity_id, peer_id, pairwise_keys)
276            .await
277    }
278
279    /// Enqueues a broadcast frame for transmission.
280    pub async fn send_broadcast(
281        &self,
282        from: LocalIdentityId,
283        payload: &[u8],
284        options: &SendOptions,
285    ) -> Result<SendReceipt, SendError> {
286        self.mac
287            .borrow_mut()
288            .await
289            .send_broadcast(from, payload, options)
290            .await
291    }
292
293    /// Enqueues a multicast frame for transmission.
294    pub async fn send_multicast(
295        &self,
296        from: LocalIdentityId,
297        channel: &ChannelId,
298        payload: &[u8],
299        options: &SendOptions,
300    ) -> Result<SendReceipt, SendError> {
301        self.mac
302            .borrow_mut()
303            .await
304            .send_multicast(from, channel, payload, options)
305            .await
306    }
307
308    /// Enqueues a unicast frame for transmission.
309    pub async fn send_unicast(
310        &self,
311        from: LocalIdentityId,
312        dst: &PublicKey,
313        payload: &[u8],
314        options: &SendOptions,
315    ) -> Result<Option<SendReceipt>, SendError> {
316        self.mac
317            .borrow_mut()
318            .await
319            .send_unicast(from, dst, payload, options)
320            .await
321    }
322
323    /// Enqueues a blind-unicast frame for transmission.
324    pub async fn send_blind_unicast(
325        &self,
326        from: LocalIdentityId,
327        dst: &PublicKey,
328        channel: &ChannelId,
329        payload: &[u8],
330        options: &SendOptions,
331    ) -> Result<Option<SendReceipt>, SendError> {
332        self.mac
333            .borrow_mut()
334            .await
335            .send_blind_unicast(from, dst, channel, payload, options)
336            .await
337    }
338
339    /// Drive the shared MAC until one wake cycle completes and invoke `on_event` for emitted events.
340    ///
341    /// The exclusive borrow on the shared coordinator is released between
342    /// every internal phase so that other handles (CLI sends, UI queries,
343    /// counter-persistence services) can interleave their own async work
344    /// while this driver is waiting on the radio or a timer.
345    pub async fn next_event(
346        &self,
347        mut on_event: impl FnMut(LocalIdentityId, crate::MacEventRef<'_>),
348    ) -> Result<(), MacError<<P::Radio as umsh_hal::Radio>::Error>> {
349        loop {
350            // Phase 1: drain any ready transmit work.
351            self.mac
352                .borrow_mut()
353                .await
354                .drain_tx_queue(&mut on_event)
355                .await?;
356
357            // Phase 2: wait for a radio frame or timer deadline. Acquire the
358            // borrow briefly each poll so concurrent tasks can obtain it too.
359            // `poll_with_mut` keeps us registered on the cell's wake condition
360            // across Pending polls, so we re-poll both when the cell frees up
361            // and when another handle mutates coordinator state (e.g.
362            // `cli.send_unicast` enqueues a frame and drops its borrow) —
363            // without that, TX queued by concurrent handles would sit until
364            // the next radio/timer event. It also deregisters us around our
365            // own borrow so our guard release cannot self-wake into a spin,
366            // and the scoped ticket deregisters on drop so this wait can be
367            // cancelled (e.g. losing a `select!` race) without leaking its
368            // waker registration.
369            let mut buf = [0u8; FRAME];
370            let mut cond_ticket = self.mac.scoped_ticket();
371            let reason = poll_fn(|cx| {
372                self.mac.poll_with_mut(cx, &mut cond_ticket, |mac, cx| {
373                    // Register radio/timer wakers and check readiness in one shot.
374                    mac.poll_wait_for_wake(cx, &mut buf)
375                })
376            })
377            .await
378            .map_err(MacError::Radio)?;
379            drop(cond_ticket);
380
381            // Phases 3-5: re-acquire the borrow and finish the cycle.
382            self.mac
383                .borrow_mut()
384                .await
385                .process_wake_reason(reason, &mut buf, &mut on_event)
386                .await?;
387
388            // Flush any pending TX or RX counter boundaries to durable storage.
389            // Mirrors `Mac::next_event`. Errors are intentionally ignored —
390            // persistence is best-effort and must not block the radio event
391            // loop. Borrow is dropped before the next phase.
392            {
393                let mut mac = self.mac.borrow_mut().await;
394                let _ = mac.service_counter_persistence().await;
395                let _ = mac.service_rx_counter_persistence().await;
396            }
397
398            // If new transmit work appeared during processing (e.g. a
399            // retransmit was enqueued), loop back to drain it before
400            // waiting again.
401            let tx_empty = self.mac.borrow().await.tx_queue().is_empty();
402            if !tx_empty {
403                continue;
404            }
405            return Ok(());
406        }
407    }
408
409    /// Drive the shared MAC forever, invoking `on_event` for delivered events.
410    ///
411    /// This is the preferred long-lived driver API for standalone MAC-backed tasks.
412    pub async fn run(
413        &self,
414        mut on_event: impl FnMut(LocalIdentityId, crate::MacEventRef<'_>),
415    ) -> Result<(), MacError<<P::Radio as umsh_hal::Radio>::Error>> {
416        loop {
417            self.next_event(&mut on_event).await?;
418        }
419    }
420
421    /// Drive the shared MAC forever while ignoring emitted events.
422    pub async fn run_quiet(&self) -> Result<(), MacError<<P::Radio as umsh_hal::Radio>::Error>> {
423        self.run(|_, _| {}).await
424    }
425
426    /// Fills a caller-provided buffer with random bytes from the shared coordinator RNG.
427    pub async fn fill_random(&self, dest: &mut [u8]) {
428        self.mac.borrow_mut().await.rng_mut().fill_bytes(dest);
429    }
430
431    /// Returns the current coordinator clock time in milliseconds.
432    pub async fn now_ms(&self) -> u64 {
433        self.mac.borrow().await.clock().now_ms()
434    }
435
436    #[cfg(feature = "software-crypto")]
437    /// Registers an ephemeral software identity with the shared coordinator.
438    pub async fn register_ephemeral(
439        &self,
440        parent: LocalIdentityId,
441        identity: umsh_crypto::software::SoftwareIdentity,
442    ) -> Result<LocalIdentityId, CapacityError> {
443        self.mac
444            .borrow_mut()
445            .await
446            .register_ephemeral(parent, identity)
447    }
448
449    #[cfg(feature = "software-crypto")]
450    /// Removes a previously registered ephemeral identity.
451    pub async fn remove_ephemeral(&self, id: LocalIdentityId) -> bool {
452        self.mac.borrow_mut().await.remove_ephemeral(id)
453    }
454
455    /// Cancel a pending ACK-requested send, stopping retransmissions.
456    ///
457    /// Returns `true` if the pending ACK was found and removed.
458    pub async fn cancel_pending_ack(
459        &self,
460        identity_id: LocalIdentityId,
461        receipt: SendReceipt,
462    ) -> bool {
463        self.mac
464            .borrow_mut()
465            .await
466            .cancel_pending_ack(identity_id, receipt)
467    }
468
469    /// Return the live TX frame counter for one identity, if registered.
470    pub async fn frame_counter(&self, id: LocalIdentityId) -> Option<u32> {
471        self.mac
472            .borrow()
473            .await
474            .identity(id)
475            .map(|slot| slot.frame_counter())
476    }
477
478    /// Return the persisted TX frame-counter boundary for one identity, if registered.
479    pub async fn persisted_frame_counter(&self, id: LocalIdentityId) -> Option<u32> {
480        self.mac
481            .borrow()
482            .await
483            .identity(id)
484            .map(|slot| slot.persisted_counter())
485    }
486
487    /// Invoke `f` for every peer currently registered in the shared registry.
488    ///
489    /// This covers all known peers, not just those with an active crypto session.
490    pub async fn for_each_peer(&self, f: &mut dyn FnMut(umsh_core::PublicKey)) {
491        let mac = self.mac.borrow().await;
492        for (_, info) in mac.peer_registry().iter() {
493            f(info.public_key);
494        }
495    }
496
497    /// Return the route currently cached for `peer`, if the peer is registered
498    /// and a route has been learned for it.
499    pub async fn peer_route(&self, peer: &umsh_core::PublicKey) -> Option<crate::CachedRoute> {
500        let mac = self.mac.borrow().await;
501        let (peer_id, _) = mac.peer_registry().lookup_by_key(peer)?;
502        mac.peer_registry().get(peer_id)?.route.clone()
503    }
504
505    /// Forget the route cached for `peer`, returning whether one was held.
506    pub async fn clear_peer_route(&self, peer: &umsh_core::PublicKey) -> bool {
507        let mut mac = self.mac.borrow_mut().await;
508        let Some((peer_id, _)) = mac.peer_registry().lookup_by_key(peer) else {
509            return false;
510        };
511        mac.peer_registry_mut().clear_route(peer_id)
512    }
513
514    /// Invoke `f` for each peer with an established crypto state for `id`,
515    /// passing the peer's public key, last-accepted RX counter, and persisted RX boundary.
516    pub async fn for_each_peer_counter(
517        &self,
518        id: LocalIdentityId,
519        f: &mut dyn FnMut(umsh_core::PublicKey, u32, u32),
520    ) {
521        let mac = self.mac.borrow().await;
522        let Some(slot) = mac.identity(id) else {
523            return;
524        };
525        for (peer_id, state) in slot.peer_crypto().iter() {
526            let Some(info) = mac.peer_registry().get(*peer_id) else {
527                continue;
528            };
529            f(
530                info.public_key,
531                state.replay_window.last_accepted,
532                state.persisted_rx_counter,
533            );
534        }
535    }
536}