1use core::future::poll_fn;
47use core::sync::atomic::{AtomicBool, Ordering};
48use core::task::Poll;
49
50use embassy_futures::select::{Either3, select3};
51use embassy_sync::blocking_mutex::raw::RawMutex;
52use umsh_core::{Fcf, UMSH_VERSION};
53use umsh_hal::{RxInfo, RxOrigin, Snr, TxError};
54use umsh_radio_loraphy::{Channels, MAX_PAYLOAD, RxFrame, TxRequest};
55use umsh_ulcp::stats::{Counter, StatsLedger};
56
57const SESSION: usize = 0;
63
64pub struct MuxMode(AtomicBool);
68
69impl MuxMode {
70 pub const fn new() -> Self {
71 Self(AtomicBool::new(false))
72 }
73
74 pub fn set_backhaul(&self, enabled: bool) {
77 self.0.store(enabled, Ordering::Relaxed);
78 }
79
80 pub fn backhaul(&self) -> bool {
82 self.0.load(Ordering::Relaxed)
83 }
84}
85
86impl Default for MuxMode {
87 fn default() -> Self {
88 Self::new()
89 }
90}
91
92pub static MUX_MODE: MuxMode = MuxMode::new();
94
95pub async fn radio_mux<M, const RX: usize, const TX: usize>(
111 real: &'static Channels<M, RX, TX>,
112 clients: &'static [&'static Channels<M, RX, TX>],
113 mode: &'static MuxMode,
114 stats: Option<&'static StatsLedger>,
115) -> !
116where
117 M: RawMutex,
118{
119 let mut in_flight: Option<InFlight> = None;
124 let mut arbitration_start: usize = 0;
127
128 loop {
129 let tx_done = async {
132 match in_flight {
133 Some(_) => real.tx_done.wait().await,
134 None => core::future::pending().await,
135 }
136 };
137 let next_tx = async {
140 match in_flight {
141 None => receive_any_tx(clients, arbitration_start).await,
142 Some(_) => core::future::pending().await,
143 }
144 };
145
146 match select3(real.rx.receive(), tx_done, next_tx).await {
147 Either3::First(frame) => {
148 note_reception(stats, &frame.data);
151 let skip = mode.backhaul().then_some(SESSION);
154 deliver(clients, skip, &frame.data, frame.info);
155 }
156 Either3::Second(result) => {
157 let Some(sent) = in_flight.take() else {
158 continue;
159 };
160 let aired = result.is_ok();
161 if let Some(stats) = stats {
162 match &result {
166 Ok(()) => stats.bump(Counter::TxPackets),
167 Err(TxError::CadTimeout) => stats.bump(Counter::TxChannelBusy),
168 Err(_) => {}
169 }
170 }
171 clients[sent.owner].tx_done.signal(result);
172 if aired {
173 deliver(
174 clients,
175 Some(sent.owner),
176 &sent.data,
177 unmeasured(sent.data.len(), RxOrigin::LocalTx),
178 );
179 }
180 }
181 Either3::Third((who, request)) => {
182 let _ = clients[who].tx_done.try_take();
188 arbitration_start = (who + 1) % clients.len();
189
190 if who == SESSION && mode.backhaul() {
191 let delivered = deliver(
204 clients,
205 Some(SESSION),
206 &request.data,
207 unmeasured(request.data.len(), RxOrigin::Backhaul),
208 );
209 clients[SESSION].tx_done.signal(if delivered {
214 Ok(())
215 } else {
216 Err(TxError::CadTimeout)
217 });
218 continue;
219 }
220
221 let data = request.data.clone();
222 real.tx.send(request).await;
223 in_flight = Some(InFlight { owner: who, data });
224 }
225 }
226 }
227}
228
229struct InFlight {
231 owner: usize,
232 data: heapless::Vec<u8, MAX_PAYLOAD>,
233}
234
235fn note_reception(stats: Option<&'static StatsLedger>, data: &[u8]) {
247 let Some(stats) = stats else {
248 return;
249 };
250 let ours = match data.first() {
251 Some(&first) => {
252 let fcf = Fcf(first);
253 fcf.version() == UMSH_VERSION && fcf.reserved_valid()
254 }
255 None => false,
259 };
260 stats.bump(if ours {
261 Counter::RxPackets
262 } else {
263 Counter::RxNonUmsh
264 });
265}
266
267fn unmeasured(len: usize, origin: RxOrigin) -> RxInfo {
270 RxInfo {
271 len,
272 rssi: 0,
273 snr: Snr::from_centibels(0),
274 lqi: None,
275 origin,
276 }
277}
278
279fn deliver<M, const RX: usize, const TX: usize>(
283 clients: &[&Channels<M, RX, TX>],
284 skip: Option<usize>,
285 data: &heapless::Vec<u8, MAX_PAYLOAD>,
286 info: RxInfo,
287) -> bool
288where
289 M: RawMutex,
290{
291 let mut delivered = true;
292 for (index, client) in clients.iter().enumerate() {
293 if Some(index) == skip {
294 continue;
295 }
296 let copy = RxFrame {
297 data: data.clone(),
298 info,
299 };
300 if client.rx.try_send(copy).is_ok() {
301 client.rx_waker.wake();
302 } else {
303 delivered = false;
304 }
305 }
306 delivered
307}
308
309async fn receive_any_tx<M, const RX: usize, const TX: usize>(
312 clients: &[&Channels<M, RX, TX>],
313 start: usize,
314) -> (usize, TxRequest)
315where
316 M: RawMutex,
317{
318 poll_fn(move |cx| {
319 for client in clients {
322 let _ = client.tx.poll_ready_to_receive(cx);
323 }
324 for offset in 0..clients.len() {
325 let index = (start + offset) % clients.len();
326 if let Ok(request) = clients[index].tx.try_receive() {
327 return Poll::Ready((index, request));
328 }
329 }
330 Poll::Pending
331 })
332 .await
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338 use core::future::Future;
339 use core::sync::atomic::{AtomicUsize, Ordering};
340 use core::task::{Context, Waker};
341 use embassy_futures::select::{Either, select};
342 use embassy_sync::blocking_mutex::raw::NoopRawMutex;
343 use lora_phy::mod_params::RadioError;
344 use std::sync::Arc;
345 use umsh_hal::CadPolicy;
346
347 type TestCh = Channels<NoopRawMutex, 4, 2>;
348
349 fn block_on<F: Future>(future: F) -> F::Output {
350 let mut future = core::pin::pin!(future);
351 let waker = Waker::noop();
352 let mut context = Context::from_waker(&waker);
353 loop {
354 if let Poll::Ready(output) = future.as_mut().poll(&mut context) {
355 return output;
356 }
357 }
358 }
359
360 fn channels() -> &'static TestCh {
361 Box::leak(Box::new(Channels::new()))
362 }
363
364 fn run<F: Future>(
368 real: &'static TestCh,
369 clients: &'static [&'static TestCh],
370 scenario: F,
371 ) -> F::Output {
372 run_with_mode(real, clients, mode(), scenario)
373 }
374
375 fn run_with_mode<F: Future>(
376 real: &'static TestCh,
377 clients: &'static [&'static TestCh],
378 mode: &'static MuxMode,
379 scenario: F,
380 ) -> F::Output {
381 drive(real, clients, mode, None, scenario)
382 }
383
384 fn run_with_stats<F: Future>(
387 real: &'static TestCh,
388 clients: &'static [&'static TestCh],
389 mode: &'static MuxMode,
390 stats: &'static StatsLedger,
391 scenario: F,
392 ) -> F::Output {
393 drive(real, clients, mode, Some(stats), scenario)
394 }
395
396 fn drive<F: Future>(
397 real: &'static TestCh,
398 clients: &'static [&'static TestCh],
399 mode: &'static MuxMode,
400 stats: Option<&'static StatsLedger>,
401 scenario: F,
402 ) -> F::Output {
403 block_on(async {
404 match select(radio_mux(real, clients, mode, stats), scenario).await {
405 Either::First(_) => unreachable!("mux never returns"),
406 Either::Second(output) => output,
407 }
408 })
409 }
410
411 fn ledger() -> &'static StatsLedger {
412 Box::leak(Box::new(StatsLedger::new()))
413 }
414
415 fn mode() -> &'static MuxMode {
416 Box::leak(Box::new(MuxMode::new()))
417 }
418
419 fn backhaul_mode() -> &'static MuxMode {
420 let mode = mode();
421 mode.set_backhaul(true);
422 mode
423 }
424
425 fn rx_frame(tag: u8) -> RxFrame {
426 let mut data = heapless::Vec::new();
427 data.push(tag).unwrap();
428 RxFrame {
429 data,
430 info: RxInfo {
431 len: 1,
432 rssi: -40,
433 snr: Snr::from_decibels(5),
434 lqi: None,
435 origin: RxOrigin::Air,
436 },
437 }
438 }
439
440 fn tx_request(tag: u8) -> TxRequest {
441 let mut data = heapless::Vec::new();
442 data.push(tag).unwrap();
443 TxRequest {
444 data,
445 power_dbm: None,
446 cad: CadPolicy::Skip,
447 }
448 }
449
450 #[test]
451 fn tx_completions_route_to_the_requesting_client() {
452 let real = channels();
453 let a = channels();
454 let b = channels();
455 let clients: &'static [&'static TestCh] = Box::leak(Box::new([a, b]));
456
457 run(real, clients, async {
458 a.tx.send(tx_request(0xA1)).await;
459 b.tx.send(tx_request(0xB1)).await;
460
461 let granted = real.tx.receive().await;
464 assert_eq!(granted.data.as_slice(), &[0xA1]);
465 assert!(
466 real.tx.try_receive().is_err(),
467 "B granted while A in flight"
468 );
469
470 real.tx_done
471 .signal(Err(TxError::Io(RadioError::TransmitTimeout)));
472 assert!(a.tx_done.wait().await.is_err());
473 assert!(b.tx_done.try_take().is_none(), "completion leaked to B");
474
475 let granted = real.tx.receive().await;
476 assert_eq!(granted.data.as_slice(), &[0xB1]);
477 real.tx_done.signal(Ok(()));
478 assert!(b.tx_done.wait().await.is_ok());
479 assert!(a.tx_done.try_take().is_none(), "completion leaked to A");
480 });
481 }
482
483 #[test]
484 fn tx_grants_round_robin_under_contention() {
485 let real = channels();
486 let a = channels();
487 let b = channels();
488 let clients: &'static [&'static TestCh] = Box::leak(Box::new([a, b]));
489
490 run(real, clients, async {
491 a.tx.send(tx_request(0xA1)).await;
492 a.tx.send(tx_request(0xA2)).await;
493 b.tx.send(tx_request(0xB1)).await;
494
495 let mut order = std::vec::Vec::new();
497 for _ in 0..3 {
498 let granted = real.tx.receive().await;
499 order.push(granted.data[0]);
500 real.tx_done.signal(Ok(()));
501 let owner = if order.last() == Some(&0xB1) { b } else { a };
503 assert!(owner.tx_done.wait().await.is_ok());
504 }
505 assert_eq!(order, [0xA1, 0xB1, 0xA2]);
506 });
507 }
508
509 #[test]
510 fn rx_fans_out_to_every_client() {
511 let real = channels();
512 let a = channels();
513 let b = channels();
514 let clients: &'static [&'static TestCh] = Box::leak(Box::new([a, b]));
515
516 struct CountingWake(AtomicUsize);
517 impl std::task::Wake for CountingWake {
518 fn wake(self: Arc<Self>) {
519 self.0.fetch_add(1, Ordering::SeqCst);
520 }
521 }
522
523 run(real, clients, async {
524 let wakes = Arc::new(CountingWake(AtomicUsize::new(0)));
527 b.rx_waker.register(&Waker::from(wakes.clone()));
528
529 real.rx.send(rx_frame(0x11)).await;
530
531 let got_a = a.rx.receive().await;
532 assert_eq!(got_a.data.as_slice(), &[0x11]);
533 assert_eq!(got_a.info.rssi, -40);
534 let got_b = b.rx.receive().await;
535 assert_eq!(got_b.data.as_slice(), &[0x11]);
536 assert!(wakes.0.load(Ordering::SeqCst) > 0, "rx_waker not woken");
537 });
538 }
539
540 #[test]
541 fn rx_overflow_drops_only_the_full_client() {
542 let real = channels();
543 let a = channels();
544 let b = channels();
545 let clients: &'static [&'static TestCh] = Box::leak(Box::new([a, b]));
546
547 run(real, clients, async {
548 for tag in 0..6u8 {
551 real.rx.send(rx_frame(tag)).await;
552 let got = b.rx.receive().await;
553 assert_eq!(got.data.as_slice(), &[tag]);
554 }
555 for expected in 0..4u8 {
556 let got = a.rx.try_receive().expect("frame dropped early");
557 assert_eq!(got.data.as_slice(), &[expected]);
558 }
559 assert!(a.rx.try_receive().is_err(), "overflow frame not dropped");
560 });
561 }
562
563 #[test]
564 fn rx_continues_while_tx_in_flight() {
565 let real = channels();
566 let a = channels();
567 let b = channels();
568 let clients: &'static [&'static TestCh] = Box::leak(Box::new([a, b]));
569
570 run(real, clients, async {
571 a.tx.send(tx_request(0xA1)).await;
572 let granted = real.tx.receive().await;
573 assert_eq!(granted.data.as_slice(), &[0xA1]);
574
575 real.rx.send(rx_frame(0x22)).await;
577 assert_eq!(b.rx.receive().await.data.as_slice(), &[0x22]);
578 assert_eq!(a.rx.receive().await.data.as_slice(), &[0x22]);
579
580 real.tx_done.signal(Ok(()));
581 assert!(a.tx_done.wait().await.is_ok());
582 });
583 }
584
585 #[test]
586 fn a_transmit_reaches_every_other_client() {
587 let real = channels();
588 let a = channels();
589 let b = channels();
590 let clients: &'static [&'static TestCh] = Box::leak(Box::new([a, b]));
591
592 run(real, clients, async {
593 a.tx.send(tx_request(0xA1)).await;
594 assert_eq!(real.tx.receive().await.data.as_slice(), &[0xA1]);
595 real.tx_done.signal(Ok(()));
596 assert!(a.tx_done.wait().await.is_ok());
597
598 let copy = b.rx.receive().await;
599 assert_eq!(copy.data.as_slice(), &[0xA1]);
600 assert_eq!(copy.info.origin, RxOrigin::LocalTx);
601 assert!(!copy.info.origin.is_measured());
602 assert!(
603 a.rx.try_receive().is_err(),
604 "a transmitter must not hear itself"
605 );
606 });
607 }
608
609 #[test]
610 fn an_abandoned_transmit_reaches_nobody() {
611 let real = channels();
612 let a = channels();
613 let b = channels();
614 let clients: &'static [&'static TestCh] = Box::leak(Box::new([a, b]));
615
616 run(real, clients, async {
617 a.tx.send(tx_request(0xA1)).await;
618 assert_eq!(real.tx.receive().await.data.as_slice(), &[0xA1]);
619 real.tx_done.signal(Err(TxError::CadTimeout));
621 assert!(a.tx_done.wait().await.is_err());
622
623 assert!(
624 b.rx.try_receive().is_err(),
625 "a frame that never aired was copied anyway"
626 );
627 });
628 }
629
630 #[test]
631 fn backhaul_keeps_the_session_off_the_air() {
632 let real = channels();
633 let session = channels();
634 let node = channels();
635 let clients: &'static [&'static TestCh] = Box::leak(Box::new([session, node]));
636
637 run_with_mode(real, clients, backhaul_mode(), async {
638 session.tx.send(tx_request(0x51)).await;
640 let handed = node.rx.receive().await;
641 assert_eq!(handed.data.as_slice(), &[0x51]);
642 assert_eq!(handed.info.origin, RxOrigin::Backhaul);
643 assert!(
644 real.tx.try_receive().is_err(),
645 "backhaul frame reached the radio"
646 );
647 assert!(
648 session.tx_done.wait().await.is_ok(),
649 "a delivered frame must complete"
650 );
651
652 real.rx.send(rx_frame(0x22)).await;
654 assert_eq!(node.rx.receive().await.data.as_slice(), &[0x22]);
655 assert!(
656 session.rx.try_receive().is_err(),
657 "session heard the air in backhaul mode"
658 );
659
660 node.tx.send(tx_request(0x77)).await;
663 assert_eq!(real.tx.receive().await.data.as_slice(), &[0x77]);
664 real.tx_done.signal(Ok(()));
665 assert!(node.tx_done.wait().await.is_ok());
666 let copy = session.rx.receive().await;
667 assert_eq!(copy.data.as_slice(), &[0x77]);
668 assert_eq!(copy.info.origin, RxOrigin::LocalTx);
669 });
670 }
671
672 #[test]
673 fn a_backlogged_backhaul_reports_a_busy_channel() {
674 let real = channels();
675 let session = channels();
676 let node = channels();
677 let clients: &'static [&'static TestCh] = Box::leak(Box::new([session, node]));
678
679 run_with_mode(real, clients, backhaul_mode(), async {
680 for tag in 0..4u8 {
682 session.tx.send(tx_request(tag)).await;
683 assert!(session.tx_done.wait().await.is_ok());
684 }
685 session.tx.send(tx_request(0xFF)).await;
686 assert!(matches!(
689 session.tx_done.wait().await,
690 Err(TxError::CadTimeout)
691 ));
692 });
693 }
694
695 #[test]
696 fn stale_latched_completion_is_cleared_at_grant() {
697 let real = channels();
698 let a = channels();
699 let clients: &'static [&'static TestCh] = Box::leak(Box::new([a]));
700
701 run(real, clients, async {
702 a.tx_done
704 .signal(Err(TxError::Io(RadioError::TransmitTimeout)));
705
706 a.tx.send(tx_request(0xA1)).await;
707 let granted = real.tx.receive().await;
708 assert_eq!(granted.data.as_slice(), &[0xA1]);
709 assert!(a.tx_done.try_take().is_none(), "stale completion survived");
712
713 real.tx_done.signal(Ok(()));
714 assert!(a.tx_done.wait().await.is_ok());
715 });
716 }
717
718 fn umsh_rx_frame(tag: u8) -> RxFrame {
721 let mut frame = rx_frame(Fcf::new(umsh_core::PacketType::Broadcast, false, false).0);
722 frame.data.push(tag).unwrap();
723 frame.info.len = frame.data.len();
724 frame
725 }
726
727 #[test]
728 fn counts_transmits_at_the_radio_and_receptions_off_the_air() {
729 let real = channels();
730 let session = channels();
731 let node = channels();
732 let clients: &'static [&'static TestCh] = Box::leak(Box::new([session, node]));
733 let stats = ledger();
734
735 run_with_stats(real, clients, mode(), stats, async {
736 session.tx.send(tx_request(0xA1)).await;
739 let _ = real.tx.receive().await;
740 real.tx_done.signal(Ok(()));
741 assert!(session.tx_done.wait().await.is_ok());
742 assert_eq!(node.rx.receive().await.info.origin, RxOrigin::LocalTx);
745
746 node.tx.send(tx_request(0xB1)).await;
748 let _ = real.tx.receive().await;
749 real.tx_done.signal(Err(TxError::CadTimeout));
750 assert!(node.tx_done.wait().await.is_err());
751
752 node.tx.send(tx_request(0xB2)).await;
754 let _ = real.tx.receive().await;
755 real.tx_done
756 .signal(Err(TxError::Io(RadioError::TransmitTimeout)));
757 assert!(node.tx_done.wait().await.is_err());
758
759 real.rx.send(umsh_rx_frame(0x01)).await;
760 let _ = session.rx.receive().await;
761 let _ = node.rx.receive().await;
762 real.rx.send(rx_frame(0x00)).await;
764 let _ = session.rx.receive().await;
765 let _ = node.rx.receive().await;
766
767 assert_eq!(stats.get(Counter::TxPackets), 1);
768 assert_eq!(stats.get(Counter::TxChannelBusy), 1);
769 assert_eq!(stats.get(Counter::RxPackets), 1);
770 assert_eq!(stats.get(Counter::RxNonUmsh), 1);
771 });
772 }
773
774 #[test]
779 fn counts_nothing_for_a_frame_that_never_reached_the_air() {
780 let real = channels();
781 let session = channels();
782 let node = channels();
783 let clients: &'static [&'static TestCh] = Box::leak(Box::new([session, node]));
784 let stats = ledger();
785
786 run_with_stats(real, clients, backhaul_mode(), stats, async {
787 session.tx.send(tx_request(0x51)).await;
788 let _ = node.rx.receive().await;
789 assert!(session.tx_done.wait().await.is_ok());
790
791 for tag in 0..4u8 {
793 session.tx.send(tx_request(tag)).await;
794 assert!(session.tx_done.wait().await.is_ok());
795 }
796 session.tx.send(tx_request(0xFF)).await;
797 assert!(matches!(
798 session.tx_done.wait().await,
799 Err(TxError::CadTimeout)
800 ));
801
802 assert_eq!(stats.get(Counter::TxPackets), 0);
803 assert_eq!(stats.get(Counter::TxChannelBusy), 0);
804 assert_eq!(stats.get(Counter::RxPackets), 0);
805 });
806 }
807}