1use core::cell::RefCell;
26use core::task::{Context, Poll};
27
28use embassy_sync::blocking_mutex::{Mutex, raw::RawMutex};
29use embassy_sync::waitqueue::WakerRegistration;
30use heapless::{Deque, Vec};
31use umsh_lan_bearer::Datagram;
32use umsh_radio_loraphy::MAX_PAYLOAD;
33use umsh_ulcp::stats::{Counter, StatsLedger};
34
35use crate::radio_mux::BridgePort;
36
37const _: () = {
40 let [a, b, c, d] = umsh_ulcp::lan::DEFAULT_GROUP;
41 let [e, f, g, h] = umsh_lan_bearer::DEFAULT_GROUP;
42 assert!(a == e && b == f && c == g && d == h);
43 assert!(umsh_ulcp::lan::DEFAULT_PORT == umsh_lan_bearer::DEFAULT_PORT);
44};
45
46const QUEUE: usize = 4;
49
50const OUTGOING: usize = 4;
53
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub enum Verdict {
57 Accepted,
59 Inactive,
61 Malformed,
63 Foreign,
66 Limited,
68 Overflow,
70}
71
72pub fn classify(datagram: &[u8]) -> Verdict {
75 match umsh_lan_bearer::classify(datagram) {
76 Datagram::Umsh => Verdict::Accepted,
77 Datagram::MeshCoreV1 => Verdict::Foreign,
78 Datagram::Malformed => Verdict::Malformed,
79 }
80}
81
82#[derive(Clone, Copy, Debug)]
89pub struct IngressLimit {
90 per_minute: u16,
91 milli_tokens: u64,
94 last_ms: u64,
95}
96
97impl IngressLimit {
98 pub const fn new(per_minute: u16, now_ms: u64) -> Self {
99 let mut limit = Self {
100 per_minute,
101 milli_tokens: 0,
102 last_ms: now_ms,
103 };
104 limit.milli_tokens = limit.capacity_milli();
105 limit
106 }
107
108 const fn capacity_milli(&self) -> u64 {
109 let per_minute = self.per_minute as u64;
110 let tenth = per_minute / 10;
111 (if tenth == 0 { 1 } else { tenth }) * 1000
112 }
113
114 pub fn admit(&mut self, now_ms: u64) -> bool {
116 let per_minute = u64::from(self.per_minute.max(1));
117 let elapsed_ms = now_ms.saturating_sub(self.last_ms);
118 let refill = elapsed_ms.saturating_mul(per_minute) / 60;
122 self.last_ms = self.last_ms.saturating_add(refill * 60 / per_minute);
123 self.milli_tokens = self.milli_tokens.saturating_add(refill);
124 if self.milli_tokens >= self.capacity_milli() {
125 self.milli_tokens = self.capacity_milli();
126 self.last_ms = now_ms;
127 }
128 if self.milli_tokens >= 1000 {
129 self.milli_tokens -= 1000;
130 true
131 } else {
132 false
133 }
134 }
135}
136
137struct State {
138 active: bool,
139 limit: IngressLimit,
140 queue: Deque<Vec<u8, MAX_PAYLOAD>, QUEUE>,
141 waker: WakerRegistration,
142 outgoing: Deque<Vec<u8, MAX_PAYLOAD>, OUTGOING>,
143 outgoing_waker: WakerRegistration,
144}
145
146pub struct LanPort<M: RawMutex> {
152 state: Mutex<M, RefCell<State>>,
153 stats: Option<&'static StatsLedger>,
154}
155
156impl<M: RawMutex> LanPort<M> {
157 pub const fn new(stats: Option<&'static StatsLedger>) -> Self {
158 Self {
159 state: Mutex::new(RefCell::new(State {
160 active: false,
161 limit: IngressLimit::new(umsh_ulcp::lan::DEFAULT_INGRESS_LIMIT, 0),
162 queue: Deque::new(),
163 waker: WakerRegistration::new(),
164 outgoing: Deque::new(),
165 outgoing_waker: WakerRegistration::new(),
166 })),
167 stats,
168 }
169 }
170
171 pub fn configure(&self, active: bool, per_minute: u16, now_ms: u64) {
175 self.state.lock(|cell| {
176 let mut state = cell.borrow_mut();
177 if !active {
178 state.queue.clear();
179 state.outgoing.clear();
180 }
181 if active && (!state.active || state.limit.per_minute != per_minute) {
182 state.limit = IngressLimit::new(per_minute, now_ms);
183 }
184 state.active = active;
185 });
186 }
187
188 pub fn offer(&self, datagram: &[u8], now_ms: u64) -> Verdict {
191 let verdict = self.state.lock(|cell| {
192 let mut state = cell.borrow_mut();
193 if !state.active {
194 return Verdict::Inactive;
195 }
196 match classify(datagram) {
197 Verdict::Accepted => {}
198 other => return other,
199 }
200 if !state.limit.admit(now_ms) {
201 return Verdict::Limited;
202 }
203 let frame = Vec::from_slice(datagram).expect("classified as fitting");
204 if state.queue.push_back(frame).is_err() {
205 return Verdict::Overflow;
206 }
207 state.waker.wake();
208 Verdict::Accepted
209 });
210 if let Some(stats) = self.stats {
211 match verdict {
212 Verdict::Accepted => stats.bump(Counter::LanRx),
213 Verdict::Limited | Verdict::Overflow => stats.bump(Counter::LanRxLimited),
214 Verdict::Malformed => stats.bump(Counter::LanRxMalformed),
215 Verdict::Inactive | Verdict::Foreign => {}
216 }
217 }
218 verdict
219 }
220}
221
222impl<M: RawMutex> LanPort<M> {
223 pub async fn outgoing(&self) -> Vec<u8, MAX_PAYLOAD> {
225 core::future::poll_fn(|cx| {
226 self.state.lock(|cell| {
227 let mut state = cell.borrow_mut();
228 state.outgoing_waker.register(cx.waker());
229 match state.outgoing.pop_front() {
230 Some(frame) => Poll::Ready(frame),
231 None => Poll::Pending,
232 }
233 })
234 })
235 .await
236 }
237}
238
239impl<M: RawMutex> BridgePort for LanPort<M> {
240 fn transmitted(&self, data: &[u8]) {
243 if classify(data) != Verdict::Accepted {
244 return;
245 }
246 let outcome = self.state.lock(|cell| {
247 let mut state = cell.borrow_mut();
248 if !state.active {
249 return None;
250 }
251 let frame = Vec::from_slice(data).expect("classified as fitting");
252 let dropped = state.outgoing.is_full() && state.outgoing.pop_front().is_some();
255 let _ = state.outgoing.push_back(frame);
256 state.outgoing_waker.wake();
257 Some(dropped)
258 });
259 if let (Some(stats), Some(dropped)) = (self.stats, outcome) {
260 stats.bump(Counter::LanExported);
261 if dropped {
262 stats.bump(Counter::LanExportDropped);
263 }
264 }
265 }
266
267 fn poll_receive(&self, cx: &mut Context<'_>) -> Poll<Vec<u8, MAX_PAYLOAD>> {
268 self.state.lock(|cell| {
269 let mut state = cell.borrow_mut();
270 state.waker.register(cx.waker());
271 match state.queue.pop_front() {
272 Some(frame) => Poll::Ready(frame),
273 None => Poll::Pending,
274 }
275 })
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use embassy_sync::blocking_mutex::raw::NoopRawMutex;
283
284 const UMSH: u8 = 0b1100_0000;
285
286 #[test]
287 fn datagrams_are_judged_by_their_first_octet() {
288 assert_eq!(classify(&[]), Verdict::Malformed);
289 assert_eq!(classify(&[UMSH, 1, 2]), Verdict::Accepted);
290 assert_eq!(classify(&[0b0000_0000, 1]), Verdict::Foreign);
291 assert_eq!(classify(&[0b0100_0000, 1]), Verdict::Malformed);
292 assert_eq!(classify(&[UMSH; MAX_PAYLOAD + 1]), Verdict::Malformed);
293 assert_eq!(classify(&[UMSH; MAX_PAYLOAD]), Verdict::Accepted);
294 }
295
296 #[test]
297 fn the_ingress_limit_allows_a_burst_then_the_rate() {
298 let mut limit = IngressLimit::new(60, 0);
300 for _ in 0..6 {
301 assert!(limit.admit(0));
302 }
303 assert!(!limit.admit(0));
304 assert!(!limit.admit(999));
305 assert!(limit.admit(1_000));
306 assert!(!limit.admit(1_000));
307 for _ in 0..6 {
309 assert!(limit.admit(600_000));
310 }
311 assert!(!limit.admit(600_000));
312 }
313
314 #[test]
315 fn a_slow_limit_still_admits_one() {
316 let mut limit = IngressLimit::new(1, 0);
317 assert!(limit.admit(0));
318 assert!(!limit.admit(59_999));
319 assert!(limit.admit(60_000));
320 }
321
322 #[test]
323 fn the_port_counts_and_queues_what_it_accepts() {
324 static STATS: StatsLedger = StatsLedger::new();
325 let port = LanPort::<NoopRawMutex>::new(Some(&STATS));
326 assert_eq!(port.offer(&[UMSH, 1], 0), Verdict::Inactive);
327 port.configure(true, 10, 0);
328 assert_eq!(port.offer(&[UMSH, 1], 0), Verdict::Accepted);
329 assert_eq!(port.offer(&[UMSH, 2], 0), Verdict::Limited);
330 assert_eq!(port.offer(&[0x40], 0), Verdict::Malformed);
331 assert_eq!(port.offer(&[0x00], 0), Verdict::Foreign);
332 assert_eq!(STATS.get(Counter::LanRx), 1);
333 assert_eq!(STATS.get(Counter::LanRxLimited), 1);
334 assert_eq!(STATS.get(Counter::LanRxMalformed), 1);
335
336 let waker = core::task::Waker::noop();
337 let mut cx = Context::from_waker(waker);
338 match port.poll_receive(&mut cx) {
339 Poll::Ready(frame) => assert_eq!(frame.as_slice(), &[UMSH, 1]),
340 Poll::Pending => panic!("the accepted datagram was not queued"),
341 }
342 assert!(port.poll_receive(&mut cx).is_pending());
343
344 port.offer(&[UMSH, 3], 60_000);
345 port.configure(false, 10, 60_000);
346 assert!(port.poll_receive(&mut cx).is_pending(), "stopping drops it");
347 }
348
349 #[test]
352 fn transmitted_frames_queue_for_the_group() {
353 let port = LanPort::<NoopRawMutex>::new(None);
354 let next = || embassy_futures::block_on(port.outgoing());
355 port.transmitted(&[UMSH, 0]);
356 port.configure(true, 60, 0);
357 port.transmitted(&[0b0000_0000, 1]);
358 for tag in 1..=OUTGOING as u8 + 1 {
359 port.transmitted(&[UMSH, tag]);
360 }
361 for tag in 2..=OUTGOING as u8 + 1 {
362 assert_eq!(next().as_slice(), &[UMSH, tag]);
363 }
364 port.transmitted(&[UMSH, 9]);
365 port.configure(false, 60, 0);
366 port.configure(true, 60, 0);
367 port.transmitted(&[UMSH, 10]);
368 assert_eq!(next().as_slice(), &[UMSH, 10], "stopping drops what waited");
369 }
370}