umsh_mac/
send.rs

1use core::num::NonZeroU8;
2use heapless::Vec;
3use umsh_core::{
4    ChannelId, ChannelKey, FloodHops, MicSize, NodeHint, PacketHeader, PacketType, ParsedOptions,
5    PayloadType, PublicKey, RouterHint, SecInfo,
6};
7use umsh_hal::Snr;
8
9use crate::{CapacityError, LocalIdentityId, MAX_RESEND_FRAME_LEN, MAX_SOURCE_ROUTE_HOPS};
10
11/// Opaque tracking token returned for ACK-requested transmissions.
12///
13/// When [`Mac::queue_unicast`](crate::Mac::queue_unicast) or
14/// [`Mac::queue_blind_unicast`](crate::Mac::queue_blind_unicast) is called with
15/// `options.ack_requested = true`, the coordinator allocates a `SendReceipt` from the
16/// identity slot's internal sequence counter and returns it wrapped in `Some(...)`.
17/// The application stores this token and watches for it to appear in a future MAC event
18/// callback — either confirming delivery (MAC ACK received and verified) or reporting
19/// failure (all retransmit attempts exhausted without a valid ACK).
20///
21/// Receipts are unique within the lifetime of an [`IdentitySlot`](crate::IdentitySlot)
22/// (wrapping after ~4 billion sends). They are not meaningful across reboots or after
23/// the identity slot is removed.
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
25pub struct SendReceipt(pub u32);
26
27/// High-level transmission options passed to [`Mac`](crate::Mac) send helpers.
28///
29/// `SendOptions` expresses *what* the application wants from a send — the coordinator
30/// translates these into packet-builder calls and enforces any
31/// [`OperatingPolicy`](crate::OperatingPolicy) constraints before building the frame.
32///
33/// The default configuration (`SendOptions::default()`) is a reasonable starting point:
34/// 16-byte MIC, encryption enabled, no ACK, 3-byte source hint, 5 flood hops, no trace
35/// route, no salt.
36///
37/// `SendOptions` exposes a fluent builder API so applications can override only what they
38/// care about:
39///
40/// ```rust
41/// # use umsh_mac::SendOptions;
42/// # use umsh_core::MicSize;
43/// let opts = SendOptions::default()
44///     .with_ack_requested(true)
45///     .with_mic_size(MicSize::Mic8)
46///     .no_flood()
47///     .with_trace_route();
48/// ```
49///
50/// ## Field notes
51///
52/// - **`mic_size`** — trading MIC length against frame overhead. 16-byte MIC is strongly
53///   preferred for unicast; 4-byte may be acceptable for low-bandwidth broadcast beacons.
54/// - **`flood_hops`** — `None` disables flood forwarding (point-to-point or source-routed
55///   only). `Some(n)` sets the initial `FHOPS_REM` budget; repeaters decrement it and drop
56///   at zero.
57/// - **`full_source`** — include the full 32-byte public key instead of the 3-byte hint,
58///   allowing the receiver to authenticate without a prior key exchange. Useful for first
59///   contact or identity announcements; costs 29 extra bytes per frame.
60/// - **`salt`** — append a random 2-byte salt to SECINFO, adding nonce diversity and
61///   preventing correlation of frames sharing the same counter value across sessions.
62/// - **`source_route`** — provide an explicit list of [`RouterHint`] values to route the
63///   frame along a known path rather than relying on flood forwarding. Setting a source route
64///   also constrains the flood-hop budget to the route length when `flood_hops` is unset.
65#[derive(Clone, Debug, PartialEq, Eq)]
66pub struct SendOptions {
67    /// Requested MIC size.
68    pub mic_size: MicSize,
69    /// Whether the payload should be encrypted when supported.
70    pub encrypted: bool,
71    /// Whether a transport ACK should be requested.
72    pub ack_requested: bool,
73    /// Whether to encode the full source public key.
74    pub full_source: bool,
75    /// Optional flood-hop budget.
76    pub flood_hops: Option<u8>,
77    /// Whether to include a trace-route option.
78    pub trace_route: bool,
79    /// Optional explicit source route.
80    pub source_route: Option<Vec<RouterHint, MAX_SOURCE_ROUTE_HOPS>>,
81    /// Optional region-code option.
82    pub region_code: Option<[u8; 2]>,
83    /// Whether to include a random salt in SECINFO.
84    pub salt: bool,
85}
86
87impl Default for SendOptions {
88    fn default() -> Self {
89        Self {
90            mic_size: MicSize::Mic16,
91            encrypted: true,
92            ack_requested: false,
93            full_source: false,
94            flood_hops: Some(5),
95            trace_route: false,
96            source_route: None,
97            region_code: None,
98            salt: false,
99        }
100    }
101}
102
103impl SendOptions {
104    /// Override the MIC size.
105    pub fn with_mic_size(mut self, mic_size: MicSize) -> Self {
106        self.mic_size = mic_size;
107        self
108    }
109
110    /// Set whether the send should request an ACK.
111    pub fn with_ack_requested(mut self, value: bool) -> Self {
112        self.ack_requested = value;
113        self
114    }
115
116    /// Set the flood-hop budget.
117    pub fn with_flood_hops(mut self, hops: u8) -> Self {
118        self.flood_hops = Some(hops);
119        self
120    }
121
122    /// Disable flood forwarding.
123    pub fn no_flood(mut self) -> Self {
124        self.flood_hops = None;
125        self
126    }
127
128    /// Request that a trace-route option be added.
129    pub fn with_trace_route(mut self) -> Self {
130        self.trace_route = true;
131        self
132    }
133
134    /// Copy a source route into fixed-capacity storage.
135    pub fn try_with_source_route(mut self, route: &[RouterHint]) -> Result<Self, CapacityError> {
136        let mut owned = Vec::new();
137        for hop in route {
138            owned.push(*hop).map_err(|_| CapacityError)?;
139        }
140        self.source_route = Some(owned);
141        self.flood_hops
142            .get_or_insert(route.len().min(u8::MAX as usize) as u8);
143        Ok(self)
144    }
145
146    /// Request a random salt in SECINFO.
147    pub fn with_salt(mut self) -> Self {
148        self.salt = true;
149        self
150    }
151
152    /// Force the source address to use the full public key.
153    pub fn with_full_source(mut self) -> Self {
154        self.full_source = true;
155        self
156    }
157
158    /// Disable encryption for this send.
159    pub fn unencrypted(mut self) -> Self {
160        self.encrypted = false;
161        self
162    }
163
164    /// Set the region-code option.
165    pub fn with_region_code(mut self, code: [u8; 2]) -> Self {
166        self.region_code = Some(code);
167        self
168    }
169}
170
171/// Tracks which phase of the two-stage ACK lifecycle a pending transmission is in.
172///
173/// UMSH ACK-requested sends go through several waiting phases before the
174/// coordinator either confirms delivery or gives up:
175///
176/// 1. **`Queued`** — the send has been accepted by the coordinator but has not yet gone
177///    on-air. Deadlines do not begin running until the first successful transmit.
178///
179/// 2. **`AwaitingForward`** — after a forwarded send is transmitted, the coordinator
180///    listens to see if the frame is re-broadcast by a repeater within
181///    `confirm_deadline_ms`. Because LoRa links are half-duplex, the sender may not be in
182///    direct range of the destination but *can* hear the repeater that retransmitted the
183///    frame, providing an early, cheap confirmation that the packet made it to the next
184///    hop.
185///
186/// 3. **`RetryQueued`** — the forwarding-confirmation timer expired, so the coordinator
187///    scheduled a retransmission after jittered retry backoff. No forwarding-confirmation
188///    timer runs in this state; a new one is armed only after the retransmission actually
189///    goes on-air.
190///
191/// 4. **`AwaitingAck`** — the coordinator waits for the destination to return a MAC ACK
192///    packet containing the correct ACK tag (a CMAC-derived value only the destination can
193///    compute after successfully decrypting the original frame). The absolute deadline is
194///    `PendingAck::ack_deadline_ms`; expiry means the send failed.
195///
196/// Nodes in direct radio range of the destination skip `AwaitingForward` entirely and move
197/// from `Queued` straight to `AwaitingAck` after the first successful transmit.
198#[derive(Clone, Copy, Debug, PartialEq, Eq)]
199pub enum AckState {
200    /// Accepted for transmission but not yet sent.
201    Queued { needs_forward_confirmation: bool },
202    /// Waiting to overhear forwarding confirmation from the next hop.
203    AwaitingForward { confirm_deadline_ms: u64 },
204    /// Retransmission is queued with a retry backoff delay.
205    RetryQueued,
206    /// Waiting for the final destination's transport ACK.
207    AwaitingAck,
208}
209
210/// Sealed frame bytes and optional source route retained for retransmission.
211///
212/// When the coordinator sends an ACK-requested packet, it must keep a verbatim copy of the
213/// already-sealed frame for potential retransmission — not just the plaintext — because
214/// re-building and re-sealing would produce a different ciphertext and a different ACK tag,
215/// which the destination would not recognize.
216///
217/// `ResendRecord` stores up to `FRAME` bytes of the original sealed frame alongside any
218/// source route that may need to be re-injected into the frame header on retransmit. Records
219/// are created via [`ResendRecord::try_new`] and embedded inside [`PendingAck`]. The `FRAME`
220/// const generic must be at least as large as the largest unicast frame the application will
221/// send; oversized frames are rejected at queue time with [`crate::CapacityError`].
222#[derive(Clone, Debug, PartialEq, Eq)]
223pub struct ResendRecord<const FRAME: usize = MAX_RESEND_FRAME_LEN> {
224    /// Exact sealed frame bytes.
225    pub frame: Vec<u8, FRAME>,
226    /// Optional source route retained for retransmission.
227    pub source_route: Option<Vec<RouterHint, MAX_SOURCE_ROUTE_HOPS>>,
228}
229
230impl<const FRAME: usize> ResendRecord<FRAME> {
231    /// Copy frame bytes and an optional route into fixed-capacity storage.
232    pub fn try_new(
233        frame: &[u8],
234        source_route: Option<&[RouterHint]>,
235    ) -> Result<Self, CapacityError> {
236        let mut stored_frame = Vec::new();
237        for byte in frame {
238            stored_frame.push(*byte).map_err(|_| CapacityError)?;
239        }
240
241        let stored_route = match source_route {
242            Some(route) => {
243                let mut owned = Vec::new();
244                for hop in route {
245                    owned.push(*hop).map_err(|_| CapacityError)?;
246                }
247                Some(owned)
248            }
249            None => None,
250        };
251
252        Ok(Self {
253            frame: stored_frame,
254            source_route: stored_route,
255        })
256    }
257}
258
259/// Complete tracking state for one in-flight ACK-requested transmission.
260///
261/// The coordinator's [`IdentitySlot`](crate::IdentitySlot) maintains a `LinearMap` of
262/// `PendingAck` records keyed by [`SendReceipt`], one per active ACK-requested send. The
263/// record holds everything needed to detect completion, detect timeout, and retransmit:
264///
265/// - **`ack_tag`** — the 8-byte CMAC-derived value that will appear in the destination's
266///   MAC ACK packet. Only a node that received and successfully decrypted the original frame
267///   can produce the correct tag, so a matching `ack_tag` is cryptographic proof of delivery.
268/// - **`peer`** — the destination's full public key, used to look up the correct pending
269///   entry when matching an inbound MAC ACK against the pending table.
270/// - **`resend`** — a verbatim copy of the sealed frame for retransmission. See
271///   [`ResendRecord`].
272/// - **`sent_ms`** — the monotonic millisecond timestamp at which the frame was first
273///   transmitted; useful for latency measurement.
274/// - **`ack_deadline_ms`** — absolute deadline for the final ACK. Expiry means failure and
275///   the entry is removed.
276/// - **`retries`** — the number of retransmissions already attempted; capped at
277///   [`MAX_FORWARD_RETRIES`](crate::MAX_FORWARD_RETRIES).
278/// - **`state`** — current position in the [`AckState`] lifecycle (forwarding confirmation
279///   wait or final-ACK wait).
280///
281/// Use [`PendingAck::direct`] for sends to nodes in direct radio range, or
282/// [`PendingAck::forwarded`] when routing through a repeater.
283#[derive(Clone, Debug, PartialEq, Eq)]
284pub struct PendingAck<const FRAME: usize = MAX_RESEND_FRAME_LEN> {
285    /// Internal ACK tag used for inbound matching.
286    pub ack_tag: [u8; 8],
287    /// Final destination peer.
288    pub peer: PublicKey,
289    /// Retransmission data.
290    pub resend: ResendRecord<FRAME>,
291    /// Initial send timestamp in milliseconds.
292    pub sent_ms: u64,
293    /// Absolute deadline for the final ACK.
294    pub ack_deadline_ms: u64,
295    /// Number of retries already attempted.
296    pub retries: u8,
297    /// Current state in the ACK lifecycle.
298    pub state: AckState,
299}
300
301impl<const FRAME: usize> PendingAck<FRAME> {
302    /// Create pending-ACK state for a direct send.
303    pub fn direct(ack_tag: [u8; 8], peer: PublicKey, resend: ResendRecord<FRAME>) -> Self {
304        Self {
305            ack_tag,
306            peer,
307            resend,
308            sent_ms: 0,
309            ack_deadline_ms: 0,
310            retries: 0,
311            state: AckState::Queued {
312                needs_forward_confirmation: false,
313            },
314        }
315    }
316
317    /// Create pending-ACK state for a forwarded send.
318    pub fn forwarded(ack_tag: [u8; 8], peer: PublicKey, resend: ResendRecord<FRAME>) -> Self {
319        Self {
320            ack_tag,
321            peer,
322            resend,
323            sent_ms: 0,
324            ack_deadline_ms: 0,
325            retries: 0,
326            state: AckState::Queued {
327                needs_forward_confirmation: true,
328            },
329        }
330    }
331}
332
333/// Errors returned when recording pending-ACK state in an identity slot.
334///
335/// Returned by [`IdentitySlot::try_insert_pending_ack`](crate::IdentitySlot::try_insert_pending_ack)
336/// when the coordinator attempts to register a new in-flight ACK-requested send.
337#[derive(Clone, Copy, Debug, PartialEq, Eq)]
338pub enum PendingAckError {
339    /// The [`LocalIdentityId`](crate::LocalIdentityId) supplied does not correspond to an
340    /// occupied slot — the identity was removed while the send was being set up.
341    IdentityMissing,
342    /// The pending-ACK `LinearMap` inside the identity slot has reached its `ACKS` capacity.
343    /// Wait for an in-flight send to complete or time out before issuing another ACK-requested
344    /// send on this identity.
345    TableFull,
346}
347
348/// Priority class assigned to entries in the [`TxQueue`].
349///
350/// The transmit queue services entries in priority order (lowest rank first) so that
351/// time-sensitive control traffic is never delayed by a backlog of application sends.
352/// Within the same priority class, entries are served in FIFO order by sequence number.
353///
354/// Priority levels from highest to lowest:
355///
356/// - **`ImmediateAck`** (rank 0) — MAC ACK frames generated in response to a received
357///   unicast or blind-unicast with ACK-requested. Must be sent as quickly as possible so
358///   the original sender's retransmit timer does not expire.
359/// - **`Forward`** (rank 1) — frames being forwarded by the repeater. Prompt forwarding
360///   feeds the sender's forwarding-confirmation window, so delays here can trigger
361///   unnecessary retransmissions at the source.
362/// - **`Retry`** (rank 2) — retransmissions of unacknowledged ACK-requested sends. These
363///   have already been delayed by a full forwarding-confirmation window and need to get out
364///   before the final ACK deadline expires.
365/// - **`Application`** (rank 3) — new application-originated frames (`queue_broadcast`,
366///   `queue_unicast`, `queue_multicast`, etc.). Lowest priority; yields to all control traffic.
367#[derive(Clone, Copy, Debug, PartialEq, Eq)]
368pub enum TxPriority {
369    /// Immediate transport ACK.
370    ImmediateAck,
371    /// Receive-triggered forwarding.
372    Forward,
373    /// Retransmission after missed confirmation.
374    Retry,
375    /// Application-originated send.
376    Application,
377}
378
379impl TxPriority {
380    pub(crate) const fn rank(self) -> u8 {
381        match self {
382            Self::ImmediateAck => 0,
383            Self::Forward => 1,
384            Self::Retry => 2,
385            Self::Application => 3,
386        }
387    }
388}
389
390/// One entry in the [`TxQueue`] waiting to be transmitted by the [`Mac`](crate::Mac) coordinator.
391///
392/// Each `QueuedTx` holds a complete, already-sealed frame ready to hand directly to the
393/// radio driver. The coordinator does not re-seal on retransmit; the frame bytes are the
394/// authoritative on-the-wire representation.
395///
396/// - **`priority`** — determines service order within the queue. See [`TxPriority`].
397/// - **`frame`** — the sealed frame bytes, at most `FRAME` bytes. The coordinator calls
398///   `radio.transmit(&entry.frame, tx_options).await` when this entry reaches the head of
399///   the queue and its `not_before_ms` has elapsed.
400/// - **`receipt`** — for ACK-requested sends, the associated [`SendReceipt`] so the
401///   coordinator can update the [`PendingAck`] state after a successful transmit.
402/// - **`sequence`** — a monotonic counter assigned at enqueue time, used to preserve
403///   FIFO ordering among entries sharing the same priority.
404/// - **`not_before_ms`** — earliest acceptable transmit time in monotonic milliseconds.
405///   Entries with a future `not_before_ms` are skipped until the clock advances past it.
406///   Used to introduce per-node forwarding delay jitter that reduces collision probability.
407///   Zero means transmit immediately.
408/// - **`cad_attempts`** — number of channel-activity-detection retries already consumed
409///   on this entry; compared against [`MAX_CAD_ATTEMPTS`](crate::MAX_CAD_ATTEMPTS) to bound
410///   medium contention retries.
411/// - **`forward_deferrals`** — number of times a queued flood-forward has already been
412///   deferred after overhearing another copy of the same packet before it transmitted.
413#[derive(Clone, Debug, PartialEq, Eq)]
414pub struct QueuedTx<const FRAME: usize = MAX_RESEND_FRAME_LEN> {
415    /// Priority class.
416    pub priority: TxPriority,
417    /// Stored frame bytes.
418    pub frame: Vec<u8, FRAME>,
419    /// Optional receipt associated with the frame.
420    pub receipt: Option<SendReceipt>,
421    /// Identity that owns this send; set for identity-originated sends, `None` for
422    /// internally generated frames (MAC ACKs, forwarded frames).
423    pub identity_id: Option<LocalIdentityId>,
424    /// Monotonic sequence number for stable ordering.
425    pub sequence: u32,
426    /// Earliest transmission timestamp.
427    pub not_before_ms: u64,
428    /// Number of CAD attempts already consumed.
429    pub cad_attempts: u8,
430    /// Number of overheard-repeat deferrals already consumed.
431    pub forward_deferrals: u8,
432}
433
434impl<const FRAME: usize> QueuedTx<FRAME> {
435    /// Create a queue entry ready to send immediately.
436    pub fn try_new(
437        priority: TxPriority,
438        frame: &[u8],
439        receipt: Option<SendReceipt>,
440        identity_id: Option<LocalIdentityId>,
441        sequence: u32,
442    ) -> Result<Self, CapacityError> {
443        Self::try_new_with_state(priority, frame, receipt, identity_id, sequence, 0, 0, 0)
444    }
445
446    /// Create a queue entry with explicit timer and CAD state.
447    pub fn try_new_with_state(
448        priority: TxPriority,
449        frame: &[u8],
450        receipt: Option<SendReceipt>,
451        identity_id: Option<LocalIdentityId>,
452        sequence: u32,
453        not_before_ms: u64,
454        cad_attempts: u8,
455        forward_deferrals: u8,
456    ) -> Result<Self, CapacityError> {
457        let mut stored_frame = Vec::new();
458        for byte in frame {
459            stored_frame.push(*byte).map_err(|_| CapacityError)?;
460        }
461
462        Ok(Self {
463            priority,
464            frame: stored_frame,
465            receipt,
466            identity_id,
467            sequence,
468            not_before_ms,
469            cad_attempts,
470            forward_deferrals,
471        })
472    }
473}
474
475/// Fixed-capacity, priority-ordered transmit queue owned by the [`Mac`](crate::Mac) coordinator.
476///
477/// The `TxQueue` serializes all outgoing frames — MAC ACKs, forwarded frames, retransmissions,
478/// and application sends — into a single ordered sequence for delivery to the radio one at a
479/// time. Entries are serviced in [`TxPriority`] order, with FIFO ordering within each class.
480///
481/// The queue capacity `N` is a compile-time constant (default [`DEFAULT_TX`](crate::DEFAULT_TX)).
482/// Attempts to enqueue beyond capacity fail with [`crate::CapacityError`], propagated as
483/// [`SendError::QueueFull`](crate::SendError::QueueFull) or
484/// [`MacError::QueueFull`](crate::MacError::QueueFull). Choose `N` large enough to absorb
485/// the worst-case burst: a forwarded frame, its MAC ACK, plus any application sends already
486/// queued, plus the retransmit backlog.
487///
488/// Internally the queue is an unsorted `heapless::Vec<QueuedTx, N>`. The `dequeue` operation
489/// does a linear scan for the highest-priority, lowest-sequence entry whose `not_before_ms`
490/// has elapsed, which is O(N) — acceptable for the small N typical in embedded deployments.
491#[derive(Clone, Debug)]
492pub struct TxQueue<const N: usize = 16, const FRAME: usize = MAX_RESEND_FRAME_LEN> {
493    entries: Vec<QueuedTx<FRAME>, N>,
494    next_sequence: u32,
495}
496
497impl<const N: usize, const FRAME: usize> Default for TxQueue<N, FRAME> {
498    fn default() -> Self {
499        Self::new()
500    }
501}
502
503impl<const N: usize, const FRAME: usize> TxQueue<N, FRAME> {
504    /// Create an empty transmission queue.
505    pub fn new() -> Self {
506        Self {
507            entries: Vec::new(),
508            next_sequence: 0,
509        }
510    }
511
512    /// Return the number of queued transmissions.
513    pub fn len(&self) -> usize {
514        self.entries.len()
515    }
516
517    /// Return whether no transmissions are queued.
518    pub fn is_empty(&self) -> bool {
519        self.entries.is_empty()
520    }
521
522    /// Enqueue a frame and return its internal sequence number.
523    pub fn enqueue(
524        &mut self,
525        priority: TxPriority,
526        frame: &[u8],
527        receipt: Option<SendReceipt>,
528        identity_id: Option<LocalIdentityId>,
529    ) -> Result<u32, CapacityError> {
530        let sequence = self.next_sequence;
531        let entry = QueuedTx::try_new(priority, frame, receipt, identity_id, sequence)?;
532        self.entries.push(entry).map_err(|_| CapacityError)?;
533        self.next_sequence = self.next_sequence.wrapping_add(1);
534        Ok(sequence)
535    }
536
537    /// Enqueue a frame with explicit timer and CAD state.
538    pub fn enqueue_with_state(
539        &mut self,
540        priority: TxPriority,
541        frame: &[u8],
542        receipt: Option<SendReceipt>,
543        identity_id: Option<LocalIdentityId>,
544        not_before_ms: u64,
545        cad_attempts: u8,
546        forward_deferrals: u8,
547    ) -> Result<u32, CapacityError> {
548        let sequence = self.next_sequence;
549        let entry = QueuedTx::try_new_with_state(
550            priority,
551            frame,
552            receipt,
553            identity_id,
554            sequence,
555            not_before_ms,
556            cad_attempts,
557            forward_deferrals,
558        )?;
559        self.entries.push(entry).map_err(|_| CapacityError)?;
560        self.next_sequence = self.next_sequence.wrapping_add(1);
561        Ok(sequence)
562    }
563
564    /// Remove and return the highest-priority queued frame.
565    pub fn pop_next(&mut self) -> Option<QueuedTx<FRAME>> {
566        let index = self
567            .entries
568            .iter()
569            .enumerate()
570            .min_by_key(|(_, entry)| (entry.priority.rank(), entry.sequence))
571            .map(|(index, _)| index)?;
572        Some(self.entries.swap_remove(index))
573    }
574
575    /// Return the earliest `not_before_ms` across all entries, if any are deferred.
576    pub fn earliest_not_before_ms(&self) -> Option<u64> {
577        self.entries
578            .iter()
579            .filter(|entry| entry.not_before_ms > 0)
580            .map(|entry| entry.not_before_ms)
581            .min()
582    }
583
584    /// Return whether the queue contains any entry that is ready to send now.
585    pub fn has_ready(&self, now_ms: u64) -> bool {
586        self.entries
587            .iter()
588            .any(|entry| entry.not_before_ms <= now_ms)
589    }
590
591    /// Remove and return the first queued frame matching `predicate`.
592    pub fn remove_first_matching(
593        &mut self,
594        mut predicate: impl FnMut(&QueuedTx<FRAME>) -> bool,
595    ) -> Option<QueuedTx<FRAME>> {
596        let index = self
597            .entries
598            .iter()
599            .enumerate()
600            .find_map(|(index, entry)| predicate(entry).then_some(index))?;
601        Some(self.entries.swap_remove(index))
602    }
603}
604
605/// Borrowing view of an inbound MAC event.
606#[derive(Clone, Copy, Debug, PartialEq, Eq)]
607pub struct ChannelInfoRef<'a> {
608    pub id: ChannelId,
609    pub key: &'a ChannelKey,
610}
611
612impl<'a> ChannelInfoRef<'a> {
613    pub fn id(&self) -> ChannelId {
614        self.id
615    }
616
617    pub fn key(&self) -> &'a ChannelKey {
618        self.key
619    }
620}
621
622/// Coarser grouping of on-wire packet types.
623///
624/// This is useful for applications that care about "unicast-like" or
625/// "blind-unicast-like" traffic without matching both ACK and non-ACK packet
626/// variants individually.
627#[derive(Clone, Copy, Debug, PartialEq, Eq)]
628pub enum PacketFamily {
629    Broadcast,
630    MacAck,
631    Unicast,
632    Multicast,
633    BlindUnicast,
634    Reserved,
635}
636
637impl PacketFamily {
638    pub fn includes(self, packet_type: PacketType) -> bool {
639        match self {
640            Self::Broadcast => packet_type == PacketType::Broadcast,
641            Self::MacAck => packet_type == PacketType::MacAck,
642            Self::Unicast => matches!(packet_type, PacketType::Unicast | PacketType::UnicastAckReq),
643            Self::Multicast => packet_type == PacketType::Multicast,
644            Self::BlindUnicast => {
645                matches!(
646                    packet_type,
647                    PacketType::BlindUnicast | PacketType::BlindUnicastAckReq
648                )
649            }
650            Self::Reserved => packet_type == PacketType::Reserved5,
651        }
652    }
653}
654
655/// Iterator over packed two-byte route hops from a source-route or trace-route option.
656#[derive(Clone, Copy, Debug)]
657pub struct RouteHops<'a> {
658    bytes: &'a [u8],
659    cursor: usize,
660}
661
662/// Local physical-layer observations captured when a frame was received.
663///
664/// SNR is represented in centibels (0.1 dB units). This is finer than whole
665/// decibels, but still compact and integer-friendly. Some common LoRa radios
666/// report packet SNR in quarter-dB steps; converting those readings into
667/// centibels may therefore introduce a small rounding error.
668#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
669pub struct RxMetadata {
670    rssi: Option<i16>,
671    snr: Option<Snr>,
672    lqi: Option<NonZeroU8>,
673    received_at_ms: Option<u64>,
674}
675
676impl RxMetadata {
677    pub fn new(
678        rssi: Option<i16>,
679        snr: Option<Snr>,
680        lqi: Option<NonZeroU8>,
681        received_at_ms: Option<u64>,
682    ) -> Self {
683        Self {
684            rssi,
685            snr,
686            lqi,
687            received_at_ms,
688        }
689    }
690
691    pub fn rssi(&self) -> Option<i16> {
692        self.rssi
693    }
694
695    pub fn snr(&self) -> Option<Snr> {
696        self.snr
697    }
698
699    pub fn lqi(&self) -> Option<NonZeroU8> {
700        self.lqi
701    }
702
703    pub fn received_at_ms(&self) -> Option<u64> {
704        self.received_at_ms
705    }
706}
707
708impl<'a> RouteHops<'a> {
709    pub fn new(bytes: &'a [u8]) -> Self {
710        Self { bytes, cursor: 0 }
711    }
712}
713
714impl Iterator for RouteHops<'_> {
715    type Item = RouterHint;
716
717    fn next(&mut self) -> Option<Self::Item> {
718        let chunk = self.bytes.get(self.cursor..self.cursor + 2)?;
719        self.cursor += 2;
720        Some(RouterHint([chunk[0], chunk[1]]))
721    }
722}
723
724/// Borrowed view of one accepted inbound packet together with parsed on-wire metadata.
725///
726/// `ReceivedPacketRef` is meant to stay close to the original packet rather than eagerly
727/// translating it into application-level events. It includes the accepted wire bytes, the
728/// decrypted/usable payload slice, parsed header and option metadata, resolved sender and
729/// channel information, and security details such as frame counter, salt, MIC bytes, and
730/// authentication status.
731#[derive(Clone, Debug, PartialEq, Eq)]
732pub struct ReceivedPacketRef<'a> {
733    wire: &'a [u8],
734    payload_bytes: &'a [u8],
735    payload_type: PayloadType,
736    payload: &'a [u8],
737    header: PacketHeader,
738    options: ParsedOptions,
739    from_key: Option<PublicKey>,
740    from_hint: Option<NodeHint>,
741    source_authenticated: bool,
742    channel: Option<ChannelInfoRef<'a>>,
743    rx: RxMetadata,
744}
745
746impl<'a> ReceivedPacketRef<'a> {
747    pub fn new(
748        wire: &'a [u8],
749        payload_bytes: &'a [u8],
750        header: PacketHeader,
751        options: ParsedOptions,
752        from_key: Option<PublicKey>,
753        from_hint: Option<NodeHint>,
754        source_authenticated: bool,
755        channel: Option<ChannelInfoRef<'a>>,
756        rx: RxMetadata,
757    ) -> Self {
758        let (payload_type, payload) = if payload_bytes.is_empty() {
759            (PayloadType::Empty, &[][..])
760        } else if let Some(payload_type) = PayloadType::from_byte(payload_bytes[0]) {
761            (payload_type, &payload_bytes[1..])
762        } else {
763            (PayloadType::Empty, payload_bytes)
764        };
765        Self {
766            wire,
767            payload_bytes,
768            payload_type,
769            payload,
770            header,
771            options,
772            from_key,
773            from_hint,
774            source_authenticated,
775            channel,
776            rx,
777        }
778    }
779
780    pub fn packet_type(&self) -> PacketType {
781        self.header.packet_type()
782    }
783
784    /// Return the coarse packet family for this frame.
785    pub fn packet_family(&self) -> PacketFamily {
786        match self.packet_type() {
787            PacketType::Broadcast => PacketFamily::Broadcast,
788            PacketType::MacAck => PacketFamily::MacAck,
789            PacketType::Unicast | PacketType::UnicastAckReq => PacketFamily::Unicast,
790            PacketType::Multicast => PacketFamily::Multicast,
791            PacketType::BlindUnicast | PacketType::BlindUnicastAckReq => PacketFamily::BlindUnicast,
792            PacketType::Reserved5 => PacketFamily::Reserved,
793        }
794    }
795
796    pub fn header(&self) -> &PacketHeader {
797        &self.header
798    }
799
800    pub fn options(&self) -> &ParsedOptions {
801        &self.options
802    }
803
804    pub fn wire_bytes(&self) -> &'a [u8] {
805        self.wire
806    }
807
808    /// Return the payload bytes after any successful decryption/authentication work.
809    ///
810    /// This is the application payload body only; it does not include the leading
811    /// typed-payload byte. Use [`Self::payload_type`] or [`Self::payload_bytes`]
812    /// to inspect the application envelope.
813    pub fn payload(&self) -> &'a [u8] {
814        self.payload
815    }
816
817    /// Return the application payload type carried by this frame.
818    pub fn payload_type(&self) -> PayloadType {
819        self.payload_type
820    }
821
822    /// Return the exact application payload bytes including the leading
823    /// typed-payload byte when present.
824    pub fn payload_bytes(&self) -> &'a [u8] {
825        self.payload_bytes
826    }
827
828    /// Return the exact on-wire body region before higher-layer payload parsing.
829    pub fn wire_body(&self) -> &'a [u8] {
830        self.wire
831            .get(self.header.body_range.clone())
832            .unwrap_or_default()
833    }
834
835    pub fn is_beacon(&self) -> bool {
836        self.header.is_beacon()
837    }
838
839    pub fn from_key(&self) -> Option<PublicKey> {
840        self.from_key
841    }
842
843    pub fn from_hint(&self) -> Option<NodeHint> {
844        self.from_hint
845    }
846
847    pub fn source_authenticated(&self) -> bool {
848        self.source_authenticated
849    }
850
851    /// Local radio observations captured when this frame was received.
852    pub fn rx(&self) -> &RxMetadata {
853        &self.rx
854    }
855
856    pub fn rssi(&self) -> Option<i16> {
857        self.rx.rssi()
858    }
859
860    pub fn snr(&self) -> Option<Snr> {
861        self.rx.snr()
862    }
863
864    pub fn lqi(&self) -> Option<NonZeroU8> {
865        self.rx.lqi()
866    }
867
868    pub fn received_at_ms(&self) -> Option<u64> {
869        self.rx.received_at_ms()
870    }
871
872    /// True when the source address in the accepted frame used the full public key form.
873    pub fn has_full_source(&self) -> bool {
874        self.header.fcf.full_source()
875    }
876
877    /// Resolved channel metadata, when this packet was accepted via a known private channel.
878    pub fn channel(&self) -> Option<ChannelInfoRef<'a>> {
879        self.channel
880    }
881
882    pub fn ack_requested(&self) -> bool {
883        self.packet_type().ack_requested()
884    }
885
886    /// Whether the accepted frame carried a valid SECINFO block.
887    pub fn is_secure(&self) -> bool {
888        self.packet_type().is_secure()
889    }
890
891    pub fn sec_info(&self) -> Option<SecInfo> {
892        self.header.sec_info
893    }
894
895    pub fn encrypted(&self) -> bool {
896        self.sec_info()
897            .map(|sec| sec.scf.encrypted())
898            .unwrap_or(false)
899    }
900
901    pub fn frame_counter(&self) -> Option<u32> {
902        self.sec_info().map(|sec| sec.frame_counter)
903    }
904
905    pub fn salt(&self) -> Option<u16> {
906        self.sec_info().and_then(|sec| sec.salt)
907    }
908
909    pub fn mic_size(&self) -> Option<MicSize> {
910        self.sec_info().and_then(|sec| sec.scf.mic_size().ok())
911    }
912
913    /// Return the authenticated MIC bytes from the original wire frame.
914    pub fn mic(&self) -> &'a [u8] {
915        self.wire
916            .get(self.header.mic_range.clone())
917            .unwrap_or_default()
918    }
919
920    pub fn mic_len(&self) -> usize {
921        self.mic().len()
922    }
923
924    pub fn flood_hops(&self) -> Option<FloodHops> {
925        self.header.flood_hops
926    }
927
928    pub fn region_code(&self) -> Option<[u8; 2]> {
929        self.options.region_code
930    }
931
932    pub fn min_rssi(&self) -> Option<i16> {
933        self.options.min_rssi
934    }
935
936    pub fn min_snr(&self) -> Option<i8> {
937        self.options.min_snr
938    }
939
940    pub fn has_unknown_critical_options(&self) -> bool {
941        self.options.has_unknown_critical
942    }
943
944    pub fn source_route(&self) -> Option<&'a [u8]> {
945        self.options
946            .source_route
947            .as_ref()
948            .and_then(|range| self.wire.get(range.clone()))
949    }
950
951    /// Iterate decoded source-route hops from the packed option bytes.
952    pub fn source_route_hops(&self) -> RouteHops<'a> {
953        RouteHops::new(self.source_route().unwrap_or(&[]))
954    }
955
956    pub fn trace_route(&self) -> Option<&'a [u8]> {
957        self.options
958            .trace_route
959            .as_ref()
960            .and_then(|range| self.wire.get(range.clone()))
961    }
962
963    /// Iterate decoded trace-route hops from the packed option bytes.
964    pub fn trace_route_hops(&self) -> RouteHops<'a> {
965        RouteHops::new(self.trace_route().unwrap_or(&[]))
966    }
967
968    pub fn source_route_hop_count(&self) -> usize {
969        self.source_route()
970            .map(|route| route.len() / 2)
971            .unwrap_or(0)
972    }
973
974    pub fn trace_route_hop_count(&self) -> usize {
975        self.trace_route().map(|route| route.len() / 2).unwrap_or(0)
976    }
977}
978
979/// Borrowing view of an inbound MAC event.
980#[derive(Clone, Debug, PartialEq, Eq)]
981pub enum MacEventRef<'a> {
982    /// Accepted inbound packet with parsed metadata and resolved sender/channel information.
983    Received(ReceivedPacketRef<'a>),
984    /// Matching transport ACK received.
985    AckReceived {
986        peer: PublicKey,
987        receipt: SendReceipt,
988    },
989    /// Pending ACK timed out.
990    AckTimeout {
991        peer: PublicKey,
992        receipt: SendReceipt,
993    },
994    /// Frame was successfully handed to the radio transmitter.
995    ///
996    /// `identity_id` + `receipt` together form the identity-scoped send token.
997    /// `receipt` is `Some` only for ACK-requested sends.
998    Transmitted {
999        identity_id: LocalIdentityId,
1000        receipt: Option<SendReceipt>,
1001    },
1002    /// A repeater was overheard forwarding this frame
1003    /// (AwaitingForward → AwaitingAck transition).
1004    Forwarded {
1005        identity_id: LocalIdentityId,
1006        receipt: SendReceipt,
1007        hint: Option<RouterHint>,
1008    },
1009}