1use tokio::sync::mpsc;
42
43use umsh_node_mgmt::admin::Failure;
44use umsh_ulcp::Status;
45use umsh_ulcp::frame::{self, Cmd, Frame, HEADER_FLG_PATTERN, HEADER_TID_MASK, TID_UNSOLICITED};
46
47use crate::ulcp::{FrameLink, UlcpError};
48
49const SYNTHETIC_BUF: usize = 8;
51
52#[derive(Clone, Debug)]
58pub enum MeshFault {
59 Exchange(String),
62 RequestTooLarge(usize),
64 Radio(String),
66}
67
68impl From<MeshFault> for UlcpError {
69 fn from(fault: MeshFault) -> Self {
70 match fault {
71 MeshFault::Exchange(message) | MeshFault::Radio(message) => Self::Transport(message),
74 MeshFault::RequestTooLarge(len) => Self::FrameTooLarge(len),
75 }
76 }
77}
78
79#[derive(Clone, Debug)]
82pub struct MeshRequest {
83 frame: Vec<u8>,
85 tid: u8,
87 cmd: Option<Cmd>,
90}
91
92impl MeshRequest {
93 pub fn frame(&self) -> &[u8] {
95 &self.frame
96 }
97
98 pub fn is_reset_class(&self) -> bool {
101 matches!(
102 self.cmd,
103 Some(Cmd::Reset | Cmd::Restore | Cmd::FactoryReset)
104 )
105 }
106}
107
108#[derive(Clone, Copy, Debug)]
110pub enum DeliveredOutcome<'a> {
111 Replied(&'a [u8]),
113 NoResponse,
115 Failed(Failure),
117}
118
119pub fn mesh_link() -> (MeshFrameLink, MeshEndpoint) {
121 let (request_tx, request_rx) = mpsc::unbounded_channel();
122 let (reply_tx, reply_rx) = mpsc::unbounded_channel();
123 (
124 MeshFrameLink {
125 requests: request_tx,
126 replies: reply_rx,
127 },
128 MeshEndpoint {
129 requests: request_rx,
130 replies: reply_tx,
131 },
132 )
133}
134
135pub struct MeshFrameLink {
138 requests: mpsc::UnboundedSender<Vec<u8>>,
139 replies: mpsc::UnboundedReceiver<Result<Vec<u8>, MeshFault>>,
140}
141
142impl FrameLink for MeshFrameLink {
143 async fn send_frame(&mut self, frame: &[u8]) -> Result<(), UlcpError> {
144 self.requests
145 .send(frame.to_vec())
146 .map_err(|_| UlcpError::Disconnected)
147 }
148
149 fn poll_recv_frame(
150 &mut self,
151 cx: &mut core::task::Context<'_>,
152 ) -> core::task::Poll<Result<Vec<u8>, UlcpError>> {
153 self.replies.poll_recv(cx).map(|received| match received {
154 Some(Ok(frame)) => Ok(frame),
155 Some(Err(fault)) => Err(fault.into()),
156 None => Err(UlcpError::Disconnected),
158 })
159 }
160}
161
162pub struct MeshEndpoint {
164 requests: mpsc::UnboundedReceiver<Vec<u8>>,
165 replies: mpsc::UnboundedSender<Result<Vec<u8>, MeshFault>>,
166}
167
168impl MeshEndpoint {
169 pub async fn next(&mut self) -> Option<MeshRequest> {
176 loop {
177 let mut frame = self.requests.recv().await?;
178 let Ok(parsed) = Frame::parse(&frame) else {
179 self.report(Err(MeshFault::Exchange(
183 "the request is not a well-formed ULCP frame".into(),
184 )));
185 continue;
186 };
187 let tid = parsed.header.tid();
188 let cmd = parsed.command();
189 if frame.len() > umsh_node_mgmt::REQUEST_MAX {
190 let len = frame.len();
191 self.report(Err(MeshFault::RequestTooLarge(len)));
192 continue;
193 }
194 frame[0] = HEADER_FLG_PATTERN;
197 return Some(MeshRequest { frame, tid, cmd });
198 }
199 }
200
201 pub fn deliver(&mut self, request: &MeshRequest, outcome: DeliveredOutcome<'_>) {
203 match outcome {
204 DeliveredOutcome::Replied(reply) => {
205 let mut reply = reply.to_vec();
206 if reply.is_empty() {
207 self.report(Err(MeshFault::Exchange(
208 "the device answered with an empty frame".into(),
209 )));
210 return;
211 }
212 reply[0] = HEADER_FLG_PATTERN | (request.tid & HEADER_TID_MASK);
213 self.report(Ok(reply));
214 }
215 DeliveredOutcome::NoResponse => self.synthesize(request),
216 DeliveredOutcome::Failed(failure) => {
217 self.report(Err(MeshFault::Exchange(describe(failure))))
218 }
219 }
220 }
221
222 pub fn refuse(&mut self, message: String) {
229 self.report(Err(MeshFault::Exchange(message)));
230 }
231
232 pub fn fail(self, fault: MeshFault) {
235 let _ = self.replies.send(Err(fault));
236 }
237
238 fn synthesize(&mut self, request: &MeshRequest) {
245 let status = match request.cmd {
246 Some(Cmd::Reset) => Status::RESET_SOFTWARE,
247 Some(Cmd::Restore) => Status::RESET_RESTORED,
248 Some(Cmd::FactoryReset) => return,
251 _ => {
256 self.report(Err(MeshFault::Exchange(
257 "the device answered nothing where a reply was due".into(),
258 )));
259 return;
260 }
261 };
262 let mut buf = [0u8; SYNTHETIC_BUF];
263 match frame::last_status(&mut buf, TID_UNSOLICITED, status) {
264 Ok(len) => self.report(Ok(buf[..len].to_vec())),
265 Err(_) => self.report(Err(MeshFault::Exchange(
266 "could not report the device's completion".into(),
267 ))),
268 }
269 }
270
271 fn report(&mut self, outcome: Result<Vec<u8>, MeshFault>) {
274 let _ = self.replies.send(outcome);
275 }
276}
277
278pub fn describe(failure: Failure) -> String {
280 match failure {
281 Failure::TimedOut => {
282 "no answer — the device may be out of range, or this host may not be one of its \
283 administrators"
284 .into()
285 }
286 Failure::CursorInvalid => {
287 "the device's state changed mid-read; run the command again".into()
288 }
289 Failure::TooLarge => "the answer is larger than this host reassembles".into(),
290 Failure::Malformed => "the device's answer could not be read".into(),
291 Failure::UnknownCriticalOption(number) => {
292 format!("the device's answer carries option {number}, which this host does not know")
293 }
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300 use umsh_ulcp::ids::prop;
301
302 async fn round_trip(
305 request: Vec<u8>,
306 answer: impl FnOnce(&MeshRequest) -> Option<Vec<u8>>,
307 ) -> (MeshFrameLink, Option<MeshRequest>) {
308 let (mut link, mut endpoint) = mesh_link();
309 link.send_frame(&request).await.unwrap();
310 let carried = endpoint.next().await;
311 if let Some(carried) = &carried {
312 match answer(carried) {
313 Some(reply) => endpoint.deliver(carried, DeliveredOutcome::Replied(&reply)),
314 None => endpoint.deliver(carried, DeliveredOutcome::NoResponse),
315 }
316 }
317 core::mem::forget(endpoint);
319 (link, carried)
320 }
321
322 #[tokio::test]
323 async fn the_binding_sees_tid_zero_and_the_caller_sees_its_own() {
324 let mut buf = [0u8; 8];
325 let len = frame::prop_get(&mut buf, 5, prop::DEV_NAME).unwrap();
326
327 let (mut link, carried) = round_trip(buf[..len].to_vec(), |carried| {
328 assert_eq!(carried.frame()[0] & HEADER_TID_MASK, 0);
330 let mut reply = [0u8; 32];
332 let len =
333 frame::prop_is(&mut reply, TID_UNSOLICITED, prop::DEV_NAME, b"node\0").unwrap();
334 Some(reply[..len].to_vec())
335 })
336 .await;
337
338 assert!(!carried.unwrap().is_reset_class());
339 let reply = link.recv_frame().await.unwrap();
340 let parsed = Frame::parse(&reply).unwrap();
341 assert_eq!(parsed.header.tid(), 5);
344 assert_eq!(parsed.command(), Some(Cmd::PropIs));
345 }
346
347 #[tokio::test]
348 async fn a_reset_is_answered_by_the_announcement_a_wire_would_have_carried() {
349 let mut buf = [0u8; 8];
350 let len = frame::reset(&mut buf, TID_UNSOLICITED).unwrap();
351
352 let (mut link, carried) = round_trip(buf[..len].to_vec(), |_| None).await;
353
354 assert!(carried.unwrap().is_reset_class());
355 let reply = link.recv_frame().await.unwrap();
356 let parsed = Frame::parse(&reply).unwrap();
357 assert_eq!(parsed.header.tid(), TID_UNSOLICITED);
358 assert_eq!(parsed.command(), Some(Cmd::PropIs));
359 assert_eq!(
361 umsh_ulcp::reply::status_of(&reply),
362 Some(Status::RESET_SOFTWARE)
363 );
364 }
365
366 #[tokio::test]
367 async fn a_restore_that_reset_announces_the_snapshot_it_came_up_on() {
368 let mut buf = [0u8; 8];
369 let len = frame::restore(&mut buf, 3).unwrap();
370
371 let (mut link, _) = round_trip(buf[..len].to_vec(), |_| None).await;
372
373 let reply = link.recv_frame().await.unwrap();
374 assert_eq!(
375 umsh_ulcp::reply::status_of(&reply),
376 Some(Status::RESET_RESTORED)
377 );
378 }
379
380 #[tokio::test]
381 async fn a_factory_reset_is_answered_by_nothing_at_all() {
382 let mut buf = [0u8; 8];
383 let len = frame::factory_reset(&mut buf, TID_UNSOLICITED).unwrap();
384 let (mut link, _) = round_trip(buf[..len].to_vec(), |_| None).await;
385
386 assert!(
389 tokio::time::timeout(core::time::Duration::from_millis(50), link.recv_frame())
390 .await
391 .is_err()
392 );
393 }
394
395 #[tokio::test]
396 async fn a_refused_command_costs_the_session_nothing() {
397 let (mut link, mut endpoint) = mesh_link();
398 let mut buf = [0u8; 8];
399 let len = frame::prop_get(&mut buf, 2, prop::DEV_NAME).unwrap();
400 link.send_frame(&buf[..len]).await.unwrap();
401 let carried = endpoint.next().await.unwrap();
402 assert_eq!(carried.frame()[0] & HEADER_TID_MASK, 0);
403 endpoint.refuse("gave up after 180 s".into());
404
405 match link.recv_frame().await {
406 Err(UlcpError::Transport(message)) => assert!(message.contains("gave up")),
407 other => panic!("expected a transport fault, got {other:?}"),
408 }
409 link.send_frame(&buf[..len]).await.unwrap();
412 assert!(endpoint.next().await.is_some());
413 }
414
415 #[tokio::test]
416 async fn a_request_too_large_for_one_payload_names_its_size() {
417 let (mut link, mut endpoint) = mesh_link();
418 let mut buf = vec![0u8; umsh_node_mgmt::REQUEST_MAX + 64];
419 let oversize = vec![0xAAu8; umsh_node_mgmt::REQUEST_MAX];
420 let len = frame::prop_set(&mut buf, 1, prop::DEV_NAME, &oversize).unwrap();
421 link.send_frame(&buf[..len]).await.unwrap();
422
423 assert!(
425 tokio::time::timeout(core::time::Duration::from_millis(50), endpoint.next())
426 .await
427 .is_err()
428 );
429 match link.recv_frame().await {
430 Err(UlcpError::FrameTooLarge(reported)) => assert_eq!(reported, len),
431 other => panic!("expected FrameTooLarge, got {other:?}"),
432 }
433 }
434
435 #[tokio::test]
436 async fn a_failed_exchange_keeps_its_prose_and_leaves_the_link_usable() {
437 let (mut link, mut endpoint) = mesh_link();
438 let mut buf = [0u8; 8];
439 let len = frame::prop_get(&mut buf, 1, prop::DEV_NAME).unwrap();
440 link.send_frame(&buf[..len]).await.unwrap();
441 let carried = endpoint.next().await.unwrap();
442 endpoint.deliver(&carried, DeliveredOutcome::Failed(Failure::TimedOut));
443
444 match link.recv_frame().await {
445 Err(UlcpError::Transport(message)) => assert!(message.contains("administrators")),
446 other => panic!("expected a transport fault, got {other:?}"),
447 }
448 link.send_frame(&buf[..len]).await.unwrap();
451 assert!(endpoint.next().await.is_some());
452 }
453
454 #[tokio::test]
455 async fn losing_the_driver_disconnects_the_handle() {
456 let (mut link, endpoint) = mesh_link();
457 drop(endpoint);
458 assert!(matches!(
459 link.recv_frame().await,
460 Err(UlcpError::Disconnected)
461 ));
462 }
463
464 #[tokio::test]
465 async fn a_terminal_radio_failure_is_reported_before_the_disconnect() {
466 let (mut link, endpoint) = mesh_link();
467 endpoint.fail(MeshFault::Radio("the radio stopped answering".into()));
468 match link.recv_frame().await {
469 Err(UlcpError::Transport(message)) => assert!(message.contains("stopped answering")),
470 other => panic!("expected a transport fault, got {other:?}"),
471 }
472 assert!(matches!(
473 link.recv_frame().await,
474 Err(UlcpError::Disconnected)
475 ));
476 }
477}