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}