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}