umsh_node_mgmt/
node_adapter.rs

1//! Managing a device over the mesh, from a node.
2//!
3//! [`Exchange`] knows the binding and nothing else: it hands out payloads
4//! to send and reads the ones that come back. This binds it to a
5//! [`PeerConnection`], so an administrator's operation becomes a matter of
6//! servicing it until it finishes.
7//!
8//! The node layer delivers packets from inside the coordinator's borrow,
9//! so a response is queued where it arrives and read where it can be
10//! acted on. The caller owns the clock and the pump: it services the
11//! exchange, then drives its `Host` until the deadline the service call
12//! named, whichever comes first.
13
14use alloc::collections::VecDeque;
15use alloc::rc::Rc;
16use alloc::vec;
17use alloc::vec::Vec;
18use core::cell::{Cell, RefCell};
19
20use umsh_core::{PayloadType, PublicKey};
21use umsh_mac::SendOptions;
22use umsh_node::{
23    LocalNode, MacBackend, PeerConnection, ReceivedPacketRef, SendProgressTicket, Subscription,
24    Transport,
25};
26
27use crate::admin::{Exchange, Outcome, Reassembly, Step};
28use crate::{PAYLOAD_MAX, REQUEST_MAX};
29
30/// Responses held between the subscription that takes them in and the
31/// service call that reads them.
32///
33/// An administrator has one exchange outstanding at a time, so anything
34/// beyond a couple of payloads is a duplicate or a stray; a short queue
35/// keeps a talkative peer from growing this without bound.
36const INBOX: usize = 4;
37
38/// How large a reply this reassembles before giving up.
39///
40/// A continued read costs one exchange per fragment, so a reply of this
41/// size is already a slow operation over LoRa; the ceiling is what keeps
42/// a device that never stops issuing cursors from consuming the host.
43pub const REPLY_MAX: usize = 8 * 1024;
44
45/// Why an operation could not be started.
46#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub enum BeginError {
48    /// The request frame is larger than a Node Management payload holds.
49    RequestTooLarge,
50    /// An exchange is already outstanding. An administrator may have only
51    /// one with a given device.
52    Busy,
53}
54
55/// Why an outstanding exchange could not be carried further.
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57pub enum ManagementError<E> {
58    /// The node could not send.
59    Transport(E),
60    /// Nothing is outstanding to service.
61    Idle,
62}
63
64/// What the caller should do next.
65#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub enum Progress {
67    /// Nothing further until `deadline_ms`. Pump the host until then; a
68    /// response arriving earlier makes the next service call productive
69    /// sooner.
70    Waiting { deadline_ms: u64 },
71    /// The exchange is over.
72    Done(Outcome),
73}
74
75/// One administrator's dealings with one device.
76pub struct NodeManager<M: MacBackend> {
77    peer: PeerConnection<LocalNode<M>>,
78    inbox: Rc<RefCell<VecDeque<Vec<u8>>>>,
79    /// Payloads taken in that turned out to belong to nobody: another
80    /// exchange's token, an unauthenticated sender, or more than the
81    /// inbox holds.
82    stray: Rc<Cell<u32>>,
83    _subscription: Subscription,
84    exchange: Option<Exchange<REQUEST_MAX>>,
85    /// The reply being reassembled, and how much of it is assembled. Held
86    /// apart from [`Reassembly`], whose borrow cannot outlive a service
87    /// call.
88    reply: Vec<u8>,
89    assembled: usize,
90    /// The most recent send, for a reset-class command whose completion
91    /// is the acknowledgment rather than a response.
92    ticket: Option<SendProgressTicket>,
93    /// Distinguishes each exchange's token from the last.
94    counter: u16,
95    options: SendOptions,
96}
97
98impl<M: MacBackend> NodeManager<M> {
99    /// Watch `peer` for Node Management responses.
100    ///
101    /// `seed` picks the first token; anything unpredictable will do.
102    pub fn new(peer: PeerConnection<LocalNode<M>>, seed: u16) -> Self {
103        let inbox = Rc::new(RefCell::new(VecDeque::with_capacity(INBOX)));
104        let stray = Rc::new(Cell::new(0u32));
105        let subscription = {
106            let inbox = inbox.clone();
107            let stray = stray.clone();
108            peer.on_receive(move |packet: &ReceivedPacketRef<'_>| {
109                if packet.payload_type() != PayloadType::NodeManagementResponse {
110                    return false;
111                }
112                // The binding rests on what secure unicast guarantees, so
113                // an unauthenticated packet claiming to be a response is
114                // not one — and it is still this handler's to consume,
115                // since nothing else wants it either.
116                let room = inbox.borrow().len() < INBOX;
117                if !packet.source_authenticated() || !room {
118                    stray.set(stray.get().saturating_add(1));
119                    return true;
120                }
121                inbox.borrow_mut().push_back(packet.payload().to_vec());
122                true
123            })
124        };
125
126        Self {
127            peer,
128            inbox,
129            stray,
130            _subscription: subscription,
131            exchange: None,
132            reply: vec![0u8; REPLY_MAX],
133            assembled: 0,
134            ticket: None,
135            counter: seed,
136            options: SendOptions::default().with_ack_requested(true),
137        }
138    }
139
140    /// The device being managed.
141    pub fn device(&self) -> &PublicKey {
142        self.peer.peer()
143    }
144
145    /// How requests go out. An acknowledgment is requested by default: it
146    /// is what completes a reset, and elsewhere it turns an unreachable
147    /// path into an early answer rather than four silent retries.
148    pub fn send_options_mut(&mut self) -> &mut SendOptions {
149        &mut self.options
150    }
151
152    /// Payloads taken in that belonged to no outstanding exchange.
153    pub fn stray(&self) -> u32 {
154        self.stray.get()
155    }
156
157    /// The counter behind the last token any exchange here consumed,
158    /// the outstanding one included.
159    ///
160    /// A caller that builds one manager per operation seeds the next one
161    /// from this, so no token is ever issued twice against a device that
162    /// holds answered tokens against retransmission.
163    pub fn counter(&self) -> u16 {
164        self.exchange
165            .as_ref()
166            .map_or(self.counter, Exchange::counter)
167    }
168
169    /// The device's most recent estimate of octets not yet returned,
170    /// present only during a continued read.
171    pub fn remaining(&self) -> Option<u32> {
172        self.exchange.as_ref().and_then(Exchange::remaining)
173    }
174
175    /// Whether an exchange is outstanding.
176    pub fn is_busy(&self) -> bool {
177        self.exchange.is_some()
178    }
179
180    /// Begin an operation carrying `request`, one ULCP frame.
181    ///
182    /// The frame's TID is ignored over this binding; the envelope token
183    /// is what correlates the response.
184    pub fn begin(&mut self, request: &[u8], now_ms: u64) -> Result<(), BeginError> {
185        if self.exchange.is_some() {
186            return Err(BeginError::Busy);
187        }
188        self.counter = self.counter.wrapping_add(1);
189        let exchange = Exchange::new(request, self.counter, now_ms)
190            .map_err(|_| BeginError::RequestTooLarge)?;
191        // Anything still queued belongs to an exchange that is over.
192        self.inbox.borrow_mut().clear();
193        self.exchange = Some(exchange);
194        self.assembled = 0;
195        self.ticket = None;
196        Ok(())
197    }
198
199    /// Carry the outstanding exchange as far as it will go right now:
200    /// take in whatever arrived, then send if an attempt is due.
201    ///
202    /// A `Done` clears the exchange, leaving [`Self::reply`] readable
203    /// until the next [`Self::begin`].
204    pub async fn service(
205        &mut self,
206        now_ms: u64,
207    ) -> Result<Progress, ManagementError<<LocalNode<M> as Transport>::Error>> {
208        let Some(exchange) = self.exchange.as_mut() else {
209            return Err(ManagementError::Idle);
210        };
211
212        // A reset-class command is answered by no response payload, so
213        // the acknowledgment is what ends it.
214        if !exchange.expects_response()
215            && self
216                .ticket
217                .as_ref()
218                .is_some_and(SendProgressTicket::was_acked)
219        {
220            exchange.delivered();
221            return Ok(self.settle(Outcome::NoResponse));
222        }
223
224        let mut payload = [0u8; 1 + PAYLOAD_MAX];
225        payload[0] = PayloadType::NodeManagementRequest as u8;
226
227        let mut step = None;
228        while let Some(response) = pop(&self.inbox) {
229            let mut reassembly = Reassembly::resume(&mut self.reply, self.assembled);
230            let next = exchange.receive(&response, &mut reassembly, now_ms, &mut payload[1..]);
231            self.assembled = reassembly.len();
232            match next {
233                Some(next) => {
234                    step = Some(next);
235                    break;
236                }
237                None => self.stray.set(self.stray.get().saturating_add(1)),
238            }
239        }
240
241        let step = match step {
242            Some(step) => step,
243            None => exchange.poll(now_ms, &mut payload[1..]),
244        };
245
246        match step {
247            Step::Send { len } => {
248                // The exchange set its own deadline when it handed out
249                // the attempt, so ask rather than recompute; a send that
250                // fails outright leaves the exchange to time out.
251                let deadline_ms = exchange.deadline_ms().unwrap_or(now_ms);
252                let ticket = self
253                    .peer
254                    .send(&payload[..1 + len], &self.options)
255                    .await
256                    .map_err(ManagementError::Transport)?;
257                self.ticket = Some(ticket);
258                Ok(Progress::Waiting { deadline_ms })
259            }
260            Step::Wait { deadline_ms } => Ok(Progress::Waiting { deadline_ms }),
261            Step::Done(outcome) => Ok(self.settle(outcome)),
262        }
263    }
264
265    /// The reply frame of the exchange that just finished: one whole ULCP
266    /// frame, its trailing content the concatenation of every fragment.
267    pub fn reply(&self) -> &[u8] {
268        &self.reply[..self.assembled]
269    }
270
271    fn settle(&mut self, outcome: Outcome) -> Progress {
272        // A continued read rotated tokens the manager's own counter never
273        // saw. Taking the exchange's final count back is what keeps the
274        // next `begin` from reissuing one of them — which the device
275        // would answer with the old exchange's retained response.
276        if let Some(exchange) = &self.exchange {
277            self.counter = exchange.counter();
278        }
279        self.exchange = None;
280        self.ticket = None;
281        if let Outcome::Replied { len } = outcome {
282            self.assembled = len;
283        }
284        Progress::Done(outcome)
285    }
286}
287
288/// Take the next queued response without holding the borrow across the
289/// work that follows it.
290fn pop(inbox: &Rc<RefCell<VecDeque<Vec<u8>>>>) -> Option<Vec<u8>> {
291    inbox.borrow_mut().pop_front()
292}