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}