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