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