umsh_node/
ticket.rs

1use alloc::rc::Rc;
2use core::cell::RefCell;
3
4use umsh_mac::{LocalIdentityId, SendReceipt};
5
6/// Identity-scoped send token.
7///
8/// [`SendReceipt`] is only unique within a [`LocalIdentityId`] slot (it's allocated
9/// from a per-identity `next_receipt` counter). `SendToken` combines the two into a
10/// single value that is unique across all identity slots, suitable for use as a
11/// dispatcher key, ticket identifier, or cancellation handle.
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
13pub struct SendToken {
14    pub identity_id: LocalIdentityId,
15    pub receipt: SendReceipt,
16}
17
18impl SendToken {
19    /// Create a new send token from an identity slot and receipt.
20    pub fn new(identity_id: LocalIdentityId, receipt: SendReceipt) -> Self {
21        Self {
22            identity_id,
23            receipt,
24        }
25    }
26}
27
28/// Internal shared state updated by the dispatcher.
29#[derive(Clone, Debug, Default)]
30pub(crate) struct TicketState {
31    /// Frame was handed to the radio at least once.
32    pub transmitted: bool,
33    /// A repeater was overheard forwarding this frame.
34    pub repeated: bool,
35    /// Transport ACK received from the destination.
36    pub acked: bool,
37    /// ACK timeout — all retransmits exhausted without ACK.
38    pub failed: bool,
39    /// MAC is completely done with this send (no more events will fire).
40    pub finished: bool,
41    /// True for sends that don't request ACK (broadcast/multicast).
42    /// The dispatcher marks these finished as soon as Transmitted fires.
43    pub non_ack: bool,
44}
45
46/// Lightweight handle for observing the progress of an in-flight send.
47///
48/// The dispatcher updates the internal `TicketState` synchronously from the MAC
49/// event callback. The application queries progress at its own pace via polling
50/// methods (`was_transmitted`, `was_acked`, `is_finished`, etc.).
51///
52/// Dropping the ticket unregisters it from the dispatcher (via `Weak` reference
53/// invalidation). The MAC continues the in-flight send — dropping only stops
54/// *observation*, not the send itself.
55pub struct SendProgressTicket {
56    token: Option<SendToken>,
57    state: Rc<RefCell<TicketState>>,
58}
59
60impl SendProgressTicket {
61    /// Create a new ticket registered with the dispatcher for ACK-tracked sends.
62    pub(crate) fn new(token: SendToken, state: Rc<RefCell<TicketState>>) -> Self {
63        Self {
64            token: Some(token),
65            state,
66        }
67    }
68
69    /// Create a ticket for a send that has no receipt (e.g. non-ACK unicast).
70    ///
71    /// Without a receipt, the MAC's `Transmitted` event cannot be correlated
72    /// back to this ticket. The ticket is immediately marked as transmitted
73    /// and finished since there is nothing to track.
74    pub(crate) fn fire_and_forget() -> Self {
75        let state = Rc::new(RefCell::new(TicketState {
76            transmitted: true,
77            finished: true,
78            non_ack: true,
79            ..TicketState::default()
80        }));
81        Self { token: None, state }
82    }
83
84    /// The identity-scoped send token, if this is an ACK-tracked send.
85    pub fn token(&self) -> Option<SendToken> {
86        self.token
87    }
88
89    /// The underlying receipt, if this is an ACK-tracked send.
90    pub fn receipt(&self) -> Option<SendReceipt> {
91        self.token.map(|t| t.receipt)
92    }
93
94    /// True after the frame was handed to the radio at least once.
95    ///
96    /// For all send types, this reflects actual radio transmission: it
97    /// becomes `true` when the MAC fires the `Transmitted` event for this
98    /// ticket's receipt.
99    pub fn was_transmitted(&self) -> bool {
100        self.state.borrow().transmitted
101    }
102
103    /// True after a repeater was overheard forwarding this frame.
104    pub fn was_repeated(&self) -> bool {
105        self.state.borrow().repeated
106    }
107
108    /// True after a transport ACK was received from the destination.
109    pub fn was_acked(&self) -> bool {
110        self.state.borrow().acked
111    }
112
113    /// True when the ACK timed out — all retransmits exhausted without ACK.
114    pub fn has_failed(&self) -> bool {
115        self.state.borrow().failed
116    }
117
118    /// True when the MAC is completely done — no more retransmissions,
119    /// no more events will fire for this ticket.
120    ///
121    /// For non-ACK sends (broadcast/multicast), this becomes `true`
122    /// immediately after the first radio transmission. For ACK-tracked
123    /// sends, this becomes `true` after an ACK is received or all
124    /// retransmits are exhausted.
125    pub fn is_finished(&self) -> bool {
126        self.state.borrow().finished
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use umsh_mac::{LocalIdentityId, SendReceipt};
134
135    fn make_token() -> SendToken {
136        SendToken::new(LocalIdentityId(0), SendReceipt(42))
137    }
138
139    fn make_ticket() -> (SendProgressTicket, Rc<RefCell<TicketState>>) {
140        let state = Rc::new(RefCell::new(TicketState::default()));
141        let ticket = SendProgressTicket::new(make_token(), state.clone());
142        (ticket, state)
143    }
144
145    #[test]
146    fn new_ticket_all_false() {
147        let (ticket, _state) = make_ticket();
148        assert!(!ticket.was_transmitted());
149        assert!(!ticket.was_repeated());
150        assert!(!ticket.was_acked());
151        assert!(!ticket.has_failed());
152        assert!(!ticket.is_finished());
153    }
154
155    #[test]
156    fn new_ticket_token_and_receipt() {
157        let (ticket, _state) = make_ticket();
158        let token = ticket.token().unwrap();
159        assert_eq!(token.identity_id, LocalIdentityId(0));
160        assert_eq!(token.receipt, SendReceipt(42));
161        assert_eq!(ticket.receipt(), Some(SendReceipt(42)));
162    }
163
164    #[test]
165    fn fire_and_forget_initially_transmitted_and_finished() {
166        let ticket = SendProgressTicket::fire_and_forget();
167        assert!(ticket.was_transmitted());
168        assert!(ticket.is_finished());
169        assert!(!ticket.was_acked());
170        assert!(!ticket.has_failed());
171        assert!(ticket.token().is_none());
172        assert!(ticket.receipt().is_none());
173    }
174
175    #[test]
176    fn state_mutations_visible_through_ticket() {
177        let (ticket, state) = make_ticket();
178
179        state.borrow_mut().transmitted = true;
180        assert!(ticket.was_transmitted());
181
182        state.borrow_mut().repeated = true;
183        assert!(ticket.was_repeated());
184
185        state.borrow_mut().acked = true;
186        assert!(ticket.was_acked());
187
188        state.borrow_mut().failed = true;
189        assert!(ticket.has_failed());
190
191        state.borrow_mut().finished = true;
192        assert!(ticket.is_finished());
193    }
194
195    #[test]
196    fn cloned_state_reflects_same_updates() {
197        let (ticket, state) = make_ticket();
198        let ticket2 = SendProgressTicket::new(make_token(), state.clone());
199        state.borrow_mut().acked = true;
200        assert!(ticket.was_acked());
201        assert!(ticket2.was_acked());
202    }
203}