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