umsh_ulcp_runtime/
radio_mux.rs

1//! Radio multiplexer: shares the one physical radio among multiple clients.
2//!
3//! Today the clients are the ULCP session and the device node. The
4//! physical radio's [`Channels`] bundle has a single `tx_done` signal, so
5//! completion attribution breaks the moment two clients transmit. The mux
6//! owns the real bundle and gives every client a private virtual
7//! [`Channels`] of the same type:
8//!
9//! - **TX**: requests are granted one at a time (round-robin under
10//!   contention) and forwarded to the real TX queue; the real `tx_done`
11//!   result is routed to the granting client's own `tx_done`, so each
12//!   client sees exactly the completions for its own requests.
13//! - **RX**: every received frame is fanned out to every client. Dual
14//!   delivery is what the ULCP spec requires — frames addressed
15//!   to the device identity are processed by the device itself *and*
16//!   independently offered to host receive filtering. A client whose RX
17//!   queue is full loses that frame (the same drop policy the radio
18//!   runner applies to the real queue) without stalling the others.
19//!
20//! RX fan-out continues while a transmit is in flight; only the grant of
21//! the *next* TX waits for the previous completion. The runner-side
22//! controls (`DeviceControl`: settings, RSSI sampling) are single-owner
23//! device-domain state and bypass the mux entirely.
24
25use core::future::poll_fn;
26use core::task::Poll;
27
28use embassy_futures::select::{Either3, select3};
29use embassy_sync::blocking_mutex::raw::RawMutex;
30use umsh_radio_loraphy::{Channels, RxFrame, TxRequest};
31
32/// Run the multiplexer over the real radio `Channels` bundle.
33///
34/// `real` must be the bundle served by the radio runner, and the mux must
35/// be that bundle's only client. Each entry in `clients` is one virtual
36/// bundle, owned (RX-drained and TX-fed) by exactly one radio client.
37pub async fn radio_mux<M, const RX: usize, const TX: usize>(
38    real: &'static Channels<M, RX, TX>,
39    clients: &'static [&'static Channels<M, RX, TX>],
40) -> !
41where
42    M: RawMutex,
43{
44    // Index into `clients` of the transmit currently at the radio, if any.
45    let mut in_flight: Option<usize> = None;
46    // Where the next contended TX scan starts, so one busy client cannot
47    // starve the others.
48    let mut arbitration_start: usize = 0;
49
50    loop {
51        // Only wait for a TX completion while one is outstanding, so a
52        // spurious tx_done can never be attributed to anyone.
53        let tx_done = async {
54            match in_flight {
55                Some(_) => real.tx_done.wait().await,
56                None => core::future::pending().await,
57            }
58        };
59        // Only grant a new transmit while the radio is free; queued
60        // requests keep waiting in their client's virtual TX queue.
61        let next_tx = async {
62            match in_flight {
63                None => receive_any_tx(clients, arbitration_start).await,
64                Some(_) => core::future::pending().await,
65            }
66        };
67
68        match select3(real.rx.receive(), tx_done, next_tx).await {
69            Either3::First(frame) => {
70                for client in clients {
71                    let copy = RxFrame {
72                        data: frame.data.clone(),
73                        info: frame.info,
74                    };
75                    if client.rx.try_send(copy).is_ok() {
76                        client.rx_waker.wake();
77                    }
78                }
79            }
80            Either3::Second(result) => {
81                if let Some(owner) = in_flight.take() {
82                    clients[owner].tx_done.signal(result);
83                }
84            }
85            Either3::Third((who, request)) => {
86                // Drop any stale latched completion (e.g. from an earlier
87                // transmit whose requester was cancelled before consuming
88                // it) so the client can only observe this request's
89                // result. `try_take` leaves a registered waiter intact,
90                // unlike `reset`, which would silently drop its waker.
91                let _ = clients[who].tx_done.try_take();
92                real.tx.send(request).await;
93                in_flight = Some(who);
94                arbitration_start = (who + 1) % clients.len();
95            }
96        }
97    }
98}
99
100/// Wait for a TX request from any client, scanning from `start` so
101/// arbitration round-robins instead of always favoring client 0.
102async fn receive_any_tx<M, const RX: usize, const TX: usize>(
103    clients: &[&Channels<M, RX, TX>],
104    start: usize,
105) -> (usize, TxRequest)
106where
107    M: RawMutex,
108{
109    poll_fn(move |cx| {
110        // Register with every queue before scanning: a send racing in
111        // behind an empty scan must still wake this future.
112        for client in clients {
113            let _ = client.tx.poll_ready_to_receive(cx);
114        }
115        for offset in 0..clients.len() {
116            let index = (start + offset) % clients.len();
117            if let Ok(request) = clients[index].tx.try_receive() {
118                return Poll::Ready((index, request));
119            }
120        }
121        Poll::Pending
122    })
123    .await
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use core::future::Future;
130    use core::sync::atomic::{AtomicUsize, Ordering};
131    use core::task::{Context, Waker};
132    use embassy_futures::select::{Either, select};
133    use embassy_sync::blocking_mutex::raw::NoopRawMutex;
134    use lora_phy::mod_params::RadioError;
135    use std::sync::Arc;
136    use umsh_hal::{CadPolicy, RxInfo, Snr, TxError};
137
138    type TestCh = Channels<NoopRawMutex, 4, 2>;
139
140    fn block_on<F: Future>(future: F) -> F::Output {
141        let mut future = core::pin::pin!(future);
142        let waker = Waker::noop();
143        let mut context = Context::from_waker(&waker);
144        loop {
145            if let Poll::Ready(output) = future.as_mut().poll(&mut context) {
146                return output;
147            }
148        }
149    }
150
151    fn channels() -> &'static TestCh {
152        Box::leak(Box::new(Channels::new()))
153    }
154
155    /// Drive the mux and a test scenario concurrently until the scenario
156    /// completes.
157    fn run<F: Future>(
158        real: &'static TestCh,
159        clients: &'static [&'static TestCh],
160        scenario: F,
161    ) -> F::Output {
162        block_on(async {
163            match select(radio_mux(real, clients), scenario).await {
164                Either::First(_) => unreachable!("mux never returns"),
165                Either::Second(output) => output,
166            }
167        })
168    }
169
170    fn rx_frame(tag: u8) -> RxFrame {
171        let mut data = heapless::Vec::new();
172        data.push(tag).unwrap();
173        RxFrame {
174            data,
175            info: RxInfo {
176                len: 1,
177                rssi: -40,
178                snr: Snr::from_decibels(5),
179                lqi: None,
180            },
181        }
182    }
183
184    fn tx_request(tag: u8) -> TxRequest {
185        let mut data = heapless::Vec::new();
186        data.push(tag).unwrap();
187        TxRequest {
188            data,
189            power_dbm: None,
190            cad: CadPolicy::Skip,
191        }
192    }
193
194    #[test]
195    fn tx_completions_route_to_the_requesting_client() {
196        let real = channels();
197        let a = channels();
198        let b = channels();
199        let clients: &'static [&'static TestCh] = Box::leak(Box::new([a, b]));
200
201        run(real, clients, async {
202            a.tx.send(tx_request(0xA1)).await;
203            b.tx.send(tx_request(0xB1)).await;
204
205            // Client A queued first from a fresh mux, so its request is
206            // granted first; B's stays held until A's completion.
207            let granted = real.tx.receive().await;
208            assert_eq!(granted.data.as_slice(), &[0xA1]);
209            assert!(
210                real.tx.try_receive().is_err(),
211                "B granted while A in flight"
212            );
213
214            real.tx_done
215                .signal(Err(TxError::Io(RadioError::TransmitTimeout)));
216            assert!(a.tx_done.wait().await.is_err());
217            assert!(b.tx_done.try_take().is_none(), "completion leaked to B");
218
219            let granted = real.tx.receive().await;
220            assert_eq!(granted.data.as_slice(), &[0xB1]);
221            real.tx_done.signal(Ok(()));
222            assert!(b.tx_done.wait().await.is_ok());
223            assert!(a.tx_done.try_take().is_none(), "completion leaked to A");
224        });
225    }
226
227    #[test]
228    fn tx_grants_round_robin_under_contention() {
229        let real = channels();
230        let a = channels();
231        let b = channels();
232        let clients: &'static [&'static TestCh] = Box::leak(Box::new([a, b]));
233
234        run(real, clients, async {
235            a.tx.send(tx_request(0xA1)).await;
236            a.tx.send(tx_request(0xA2)).await;
237            b.tx.send(tx_request(0xB1)).await;
238
239            // A1 goes first; B1 must beat A2 even though A queued earlier.
240            let mut order = std::vec::Vec::new();
241            for _ in 0..3 {
242                let granted = real.tx.receive().await;
243                order.push(granted.data[0]);
244                real.tx_done.signal(Ok(()));
245                // Consume the routed completion so the next wait is clean.
246                let owner = if order.last() == Some(&0xB1) { b } else { a };
247                assert!(owner.tx_done.wait().await.is_ok());
248            }
249            assert_eq!(order, [0xA1, 0xB1, 0xA2]);
250        });
251    }
252
253    #[test]
254    fn rx_fans_out_to_every_client() {
255        let real = channels();
256        let a = channels();
257        let b = channels();
258        let clients: &'static [&'static TestCh] = Box::leak(Box::new([a, b]));
259
260        struct CountingWake(AtomicUsize);
261        impl std::task::Wake for CountingWake {
262            fn wake(self: Arc<Self>) {
263                self.0.fetch_add(1, Ordering::SeqCst);
264            }
265        }
266
267        run(real, clients, async {
268            // Client B consumes frames the way LoraphyRadio does: via
269            // rx_waker, not the channel's own waitlist.
270            let wakes = Arc::new(CountingWake(AtomicUsize::new(0)));
271            b.rx_waker.register(&Waker::from(wakes.clone()));
272
273            real.rx.send(rx_frame(0x11)).await;
274
275            let got_a = a.rx.receive().await;
276            assert_eq!(got_a.data.as_slice(), &[0x11]);
277            assert_eq!(got_a.info.rssi, -40);
278            let got_b = b.rx.receive().await;
279            assert_eq!(got_b.data.as_slice(), &[0x11]);
280            assert!(wakes.0.load(Ordering::SeqCst) > 0, "rx_waker not woken");
281        });
282    }
283
284    #[test]
285    fn rx_overflow_drops_only_the_full_client() {
286        let real = channels();
287        let a = channels();
288        let b = channels();
289        let clients: &'static [&'static TestCh] = Box::leak(Box::new([a, b]));
290
291        run(real, clients, async {
292            // Nobody drains A, so it saturates at its queue depth of 4;
293            // B keeps receiving every frame with the mux never stalling.
294            for tag in 0..6u8 {
295                real.rx.send(rx_frame(tag)).await;
296                let got = b.rx.receive().await;
297                assert_eq!(got.data.as_slice(), &[tag]);
298            }
299            for expected in 0..4u8 {
300                let got = a.rx.try_receive().expect("frame dropped early");
301                assert_eq!(got.data.as_slice(), &[expected]);
302            }
303            assert!(a.rx.try_receive().is_err(), "overflow frame not dropped");
304        });
305    }
306
307    #[test]
308    fn rx_continues_while_tx_in_flight() {
309        let real = channels();
310        let a = channels();
311        let b = channels();
312        let clients: &'static [&'static TestCh] = Box::leak(Box::new([a, b]));
313
314        run(real, clients, async {
315            a.tx.send(tx_request(0xA1)).await;
316            let granted = real.tx.receive().await;
317            assert_eq!(granted.data.as_slice(), &[0xA1]);
318
319            // No completion yet — fan-out must not be blocked behind it.
320            real.rx.send(rx_frame(0x22)).await;
321            assert_eq!(b.rx.receive().await.data.as_slice(), &[0x22]);
322            assert_eq!(a.rx.receive().await.data.as_slice(), &[0x22]);
323
324            real.tx_done.signal(Ok(()));
325            assert!(a.tx_done.wait().await.is_ok());
326        });
327    }
328
329    #[test]
330    fn stale_latched_completion_is_cleared_at_grant() {
331        let real = channels();
332        let a = channels();
333        let clients: &'static [&'static TestCh] = Box::leak(Box::new([a]));
334
335        run(real, clients, async {
336            // A previous requester abandoned its completion.
337            a.tx_done
338                .signal(Err(TxError::Io(RadioError::TransmitTimeout)));
339
340            a.tx.send(tx_request(0xA1)).await;
341            let granted = real.tx.receive().await;
342            assert_eq!(granted.data.as_slice(), &[0xA1]);
343            // The grant cleared the stale result; only the real outcome
344            // of this transmit can reach the client now.
345            assert!(a.tx_done.try_take().is_none(), "stale completion survived");
346
347            real.tx_done.signal(Ok(()));
348            assert!(a.tx_done.wait().await.is_ok());
349        });
350    }
351}