1use std::time::Duration;
81
82use embedded_hal::delay::DelayNs;
83use embedded_hal::digital::{InputPin, OutputPin};
84use embedded_hal::spi::SpiDevice;
85use pamoja_core::{topic_matches, Error, Message, Receive, Result, Transport};
86use pamoja_lora::LinkSettings;
87use pamoja_mesh::{DynamicSeenCache, Frame};
88use tokio::time::{sleep, Instant};
89
90use crate::duty::DutyCycle;
91use crate::radio::{self, Radio};
92use crate::sx126x::{RadioError, Reception, Sx126x};
93use crate::sx127x::{self, Sx127x};
94
95pub const MAX_MESSAGE: usize = Frame::MAX_PAYLOAD - 1;
98
99pub const POLL: Duration = Duration::from_millis(1);
101
102pub const TRANSMIT_GRACE: Duration = Duration::from_secs(2);
104
105pub const SEEN_CAPACITY: usize = 64;
107
108pub trait LoraRadio {
114 type Error: core::fmt::Debug;
116
117 fn link(&self) -> Option<LinkSettings>;
124
125 fn start_transmit(&mut self, frame: &[u8]) -> std::result::Result<u64, Self::Error>;
139
140 fn finish_transmit(&mut self) -> std::result::Result<bool, Self::Error>;
151
152 fn listen(&mut self) -> std::result::Result<(), Self::Error>;
158
159 fn take_frame(&mut self, buffer: &mut [u8]) -> std::result::Result<Option<usize>, Self::Error>;
174}
175
176impl<SPI, BUSY, RESET, D> LoraRadio for Sx126x<SPI, BUSY, RESET, D>
177where
178 SPI: SpiDevice,
179 BUSY: InputPin,
180 RESET: OutputPin,
181 D: DelayNs,
182{
183 type Error = RadioError<SPI::Error>;
184
185 fn link(&self) -> Option<LinkSettings> {
186 self.config().map(|config| config.link)
187 }
188
189 fn start_transmit(&mut self, frame: &[u8]) -> std::result::Result<u64, Self::Error> {
190 Sx126x::start_transmit(self, frame)
191 }
192
193 fn finish_transmit(&mut self) -> std::result::Result<bool, Self::Error> {
194 Sx126x::finish_transmit(self)
195 }
196
197 fn listen(&mut self) -> std::result::Result<(), Self::Error> {
198 Sx126x::listen(self)
199 }
200
201 fn take_frame(&mut self, buffer: &mut [u8]) -> std::result::Result<Option<usize>, Self::Error> {
202 match Sx126x::take_frame(self, buffer)? {
203 Some(Reception::Frame { len, .. }) => Ok(Some(len)),
204 _ => Ok(None),
205 }
206 }
207}
208
209impl<SPI, RESET, D> LoraRadio for Sx127x<SPI, RESET, D>
210where
211 SPI: SpiDevice,
212 RESET: OutputPin,
213 D: DelayNs,
214{
215 type Error = sx127x::RadioError<SPI::Error>;
216
217 fn link(&self) -> Option<LinkSettings> {
218 self.config().map(|config| config.link)
219 }
220
221 fn start_transmit(&mut self, frame: &[u8]) -> std::result::Result<u64, Self::Error> {
222 Sx127x::start_transmit(self, frame)
223 }
224
225 fn finish_transmit(&mut self) -> std::result::Result<bool, Self::Error> {
226 Sx127x::finish_transmit(self)
227 }
228
229 fn listen(&mut self) -> std::result::Result<(), Self::Error> {
230 Sx127x::listen(self)
231 }
232
233 fn take_frame(&mut self, buffer: &mut [u8]) -> std::result::Result<Option<usize>, Self::Error> {
234 match Sx127x::take_frame(self, buffer)? {
235 Some(sx127x::Reception::Frame { len, .. }) => Ok(Some(len)),
236 _ => Ok(None),
237 }
238 }
239}
240
241impl<SPI, BUSY, RESET, D> LoraRadio for Radio<SPI, BUSY, RESET, D>
242where
243 SPI: SpiDevice,
244 BUSY: InputPin,
245 RESET: OutputPin,
246 D: DelayNs,
247{
248 type Error = radio::RadioError<SPI::Error>;
249
250 fn link(&self) -> Option<LinkSettings> {
251 Radio::link(self)
252 }
253
254 fn start_transmit(&mut self, frame: &[u8]) -> std::result::Result<u64, Self::Error> {
255 Radio::start_transmit(self, frame)
256 }
257
258 fn finish_transmit(&mut self) -> std::result::Result<bool, Self::Error> {
259 Radio::finish_transmit(self)
260 }
261
262 fn listen(&mut self) -> std::result::Result<(), Self::Error> {
263 Radio::listen(self)
264 }
265
266 fn take_frame(&mut self, buffer: &mut [u8]) -> std::result::Result<Option<usize>, Self::Error> {
267 match Radio::take_frame(self, buffer)? {
268 Some(radio::Reception::Frame { len, .. }) => Ok(Some(len)),
269 _ => Ok(None),
270 }
271 }
272}
273
274pub struct MeshRadio<R> {
279 radio: R,
280 node: u32,
281 next_id: u16,
282 hop_limit: u8,
283 relay: bool,
284 duty: DutyCycle,
285 epoch: Instant,
286 filters: Vec<String>,
287 seen: DynamicSeenCache,
288 pending: Option<Message>,
289 connected: bool,
290 buffer: [u8; 255],
291}
292
293impl<R: LoraRadio> MeshRadio<R> {
294 pub fn new(radio: R, node: u32, duty_cycle_permille: u32) -> MeshRadio<R> {
310 MeshRadio {
311 radio,
312 node,
313 next_id: 0,
314 hop_limit: Frame::DEFAULT_HOP_LIMIT,
315 relay: true,
316 duty: DutyCycle::new(duty_cycle_permille),
317 epoch: Instant::now(),
318 filters: Vec::new(),
319 seen: DynamicSeenCache::new(SEEN_CAPACITY),
320 pending: None,
321 connected: false,
322 buffer: [0; 255],
323 }
324 }
325
326 pub fn with_hop_limit(mut self, hop_limit: u8) -> MeshRadio<R> {
336 self.hop_limit = hop_limit;
337 self
338 }
339
340 pub fn without_relaying(mut self) -> MeshRadio<R> {
346 self.relay = false;
347 self
348 }
349
350 pub fn node(&self) -> u32 {
356 self.node
357 }
358
359 pub fn is_connected(&self) -> bool {
365 self.connected
366 }
367
368 pub fn duty_cycle(&self) -> &DutyCycle {
374 &self.duty
375 }
376
377 pub fn radio(&self) -> &R {
383 &self.radio
384 }
385
386 pub fn radio_mut(&mut self) -> &mut R {
392 &mut self.radio
393 }
394
395 pub fn release(self) -> R {
401 self.radio
402 }
403
404 fn now_us(&self) -> u64 {
405 u64::try_from(self.epoch.elapsed().as_micros()).unwrap_or(u64::MAX)
406 }
407
408 async fn transmit(&mut self, frame: &Frame) -> Result<()> {
409 let now_us = self.now_us();
410 let wait_us = self.duty.wait_us(now_us);
411 if wait_us > 0 {
412 return Err(Error::Transport(format!(
413 "the duty cycle keeps the radio silent for another {} ms",
414 wait_us.div_ceil(1000)
415 )));
416 }
417 let link = self
418 .radio
419 .link()
420 .ok_or_else(|| Error::Transport("the radio is not configured".to_owned()))?;
421 let bytes = frame.as_bytes();
422 let airtime_us = self.radio.start_transmit(bytes).map_err(radio_error)?;
423 self.duty.transmitted(now_us, &link, bytes.len());
424
425 let airtime = Duration::from_micros(airtime_us);
426 sleep(airtime).await;
427 let deadline = Instant::now() + TRANSMIT_GRACE;
428 while !self.radio.finish_transmit().map_err(radio_error)? {
429 if Instant::now() >= deadline {
430 return Err(Error::Transport(
431 "the radio never reported the frame sent".to_owned(),
432 ));
433 }
434 sleep(POLL).await;
435 }
436 self.radio.listen().map_err(radio_error)
437 }
438
439 async fn accept(&mut self, len: usize) -> Result<()> {
440 let Ok(frame) = Frame::parse(&self.buffer[..len]) else {
441 return Ok(());
442 };
443 if frame.src() == self.node || !self.seen.record(frame.dedup_key()) {
444 return Ok(());
445 }
446 if let Some((topic, payload)) = decode(frame.payload()) {
447 if self
448 .filters
449 .iter()
450 .any(|filter| topic_matches(filter, topic))
451 {
452 self.pending = Some(Message::new(topic, payload));
453 }
454 }
455 if self.relay {
456 if let Some(onward) = frame.relayed() {
457 if self.duty.ready(self.now_us()) {
458 self.transmit(&onward).await?;
459 }
460 }
461 }
462 Ok(())
463 }
464}
465
466impl<R: LoraRadio + Send> Transport for MeshRadio<R> {
467 async fn connect(&mut self) -> Result<()> {
468 self.radio.listen().map_err(radio_error)?;
469 self.connected = true;
470 Ok(())
471 }
472
473 async fn send(&mut self, topic: &str, payload: &[u8]) -> Result<()> {
474 if !self.connected {
475 return Err(Error::Closed);
476 }
477 let body = encode(topic, payload)?;
478 let frame = Frame::broadcast(self.node, self.next_id, &body)
479 .map_err(|error| Error::Transport(format!("mesh frame: {error:?}")))?
480 .with_hop_limit(self.hop_limit);
481 self.next_id = self.next_id.wrapping_add(1);
482 self.seen.record(frame.dedup_key());
483 self.transmit(&frame).await
484 }
485
486 async fn subscribe(&mut self, topic: &str) -> Result<()> {
487 if !self.connected {
488 return Err(Error::Closed);
489 }
490 self.filters.push(topic.to_owned());
491 Ok(())
492 }
493}
494
495impl<R: LoraRadio + Send> Receive for MeshRadio<R> {
496 async fn recv(&mut self) -> Result<Option<Message>> {
511 if !self.connected {
512 return Err(Error::Closed);
513 }
514 loop {
515 if let Some(message) = self.pending.take() {
516 return Ok(Some(message));
517 }
518 let received = self
519 .radio
520 .take_frame(&mut self.buffer)
521 .map_err(radio_error)?;
522 match received {
523 Some(len) => self.accept(len).await?,
524 None => sleep(POLL).await,
525 }
526 }
527 }
528}
529
530fn radio_error<E: core::fmt::Debug>(error: E) -> Error {
531 Error::Transport(format!("radio: {error:?}"))
532}
533
534fn encode(topic: &str, payload: &[u8]) -> Result<Vec<u8>> {
535 let too_long = || {
536 Error::Transport(format!(
537 "a {} byte topic and a {} byte payload exceed the {MAX_MESSAGE} bytes a mesh frame carries",
538 topic.len(),
539 payload.len()
540 ))
541 };
542 if topic.len() + payload.len() > MAX_MESSAGE {
543 return Err(too_long());
544 }
545 let topic_len = u8::try_from(topic.len()).map_err(|_| too_long())?;
546 let mut body = Vec::with_capacity(1 + topic.len() + payload.len());
547 body.push(topic_len);
548 body.extend_from_slice(topic.as_bytes());
549 body.extend_from_slice(payload);
550 Ok(body)
551}
552
553fn decode(body: &[u8]) -> Option<(&str, &[u8])> {
554 let (&topic_len, rest) = body.split_first()?;
555 let topic_len = usize::from(topic_len);
556 if rest.len() < topic_len {
557 return None;
558 }
559 let (topic, payload) = rest.split_at(topic_len);
560 Some((core::str::from_utf8(topic).ok()?, payload))
561}
562
563#[cfg(test)]
564mod tests {
565 use super::*;
566 use std::collections::VecDeque;
567 use std::convert::Infallible;
568
569 fn link() -> LinkSettings {
570 LinkSettings::new(7, 125_000)
571 }
572
573 #[derive(Default)]
574 struct Air {
575 sent: Vec<Vec<u8>>,
576 heard: VecDeque<Vec<u8>>,
577 listens: usize,
578 }
579
580 impl LoraRadio for Air {
581 type Error = Infallible;
582
583 fn link(&self) -> Option<LinkSettings> {
584 Some(link())
585 }
586
587 fn start_transmit(&mut self, frame: &[u8]) -> std::result::Result<u64, Infallible> {
588 self.sent.push(frame.to_vec());
589 Ok(link().airtime_us(frame.len()))
590 }
591
592 fn finish_transmit(&mut self) -> std::result::Result<bool, Infallible> {
593 Ok(true)
594 }
595
596 fn listen(&mut self) -> std::result::Result<(), Infallible> {
597 self.listens += 1;
598 Ok(())
599 }
600
601 fn take_frame(
602 &mut self,
603 buffer: &mut [u8],
604 ) -> std::result::Result<Option<usize>, Infallible> {
605 Ok(self.heard.pop_front().map(|frame| {
606 buffer[..frame.len()].copy_from_slice(&frame);
607 frame.len()
608 }))
609 }
610 }
611
612 fn on_air(src: u32, id: u16, hops: u8, topic: &str, payload: &[u8]) -> Vec<u8> {
613 let body = encode(topic, payload).unwrap();
614 Frame::broadcast(src, id, &body)
615 .unwrap()
616 .with_hop_limit(hops)
617 .as_bytes()
618 .to_vec()
619 }
620
621 #[tokio::test(start_paused = true)]
622 async fn nothing_goes_out_or_comes_in_before_connect() {
623 let mut node = MeshRadio::new(Air::default(), 0x0A, 1000);
624 assert!(matches!(node.send("a", b"1").await, Err(Error::Closed)));
625 assert!(matches!(node.subscribe("a").await, Err(Error::Closed)));
626 assert!(matches!(node.recv().await, Err(Error::Closed)));
627 assert!(!node.is_connected());
628 }
629
630 #[tokio::test(start_paused = true)]
631 async fn a_message_leaves_as_a_broadcast_frame_carrying_its_topic() {
632 let mut node = MeshRadio::new(Air::default(), 0x0A, 1000);
633 node.connect().await.unwrap();
634 node.send("garden/moisture", b"28.5").await.unwrap();
635
636 let air = node.release();
637 assert_eq!(air.listens, 2);
638 let sent = Frame::parse(&air.sent[0]).unwrap();
639 assert_eq!(sent.src(), 0x0A);
640 assert!(sent.is_broadcast());
641 assert_eq!(sent.id(), 0);
642 assert_eq!(sent.hop_limit(), Frame::DEFAULT_HOP_LIMIT);
643 let mut body = vec![15];
644 body.extend_from_slice(b"garden/moisture28.5");
645 assert_eq!(sent.payload(), body.as_slice());
646 }
647
648 #[tokio::test(start_paused = true)]
649 async fn the_duty_cycle_holds_back_the_next_message_for_its_off_time() {
650 let mut node = MeshRadio::new(Air::default(), 0x0A, 10);
651 node.connect().await.unwrap();
652 node.send("t", b"1").await.unwrap();
653
654 let refused = node.send("t", b"2").await;
655 assert!(
656 matches!(&refused, Err(Error::Transport(reason)) if reason.contains("duty cycle")),
657 "{refused:?}"
658 );
659 let len = node.radio().sent[0].len();
660 sleep(Duration::from_micros(link().min_off_time_us(len, 10))).await;
661 node.send("t", b"2").await.unwrap();
662 assert_eq!(node.radio().sent.len(), 2);
663 }
664
665 #[tokio::test(start_paused = true)]
666 async fn recv_delivers_each_matching_topic_once_and_ignores_its_own_frames() {
667 let mut air = Air::default();
668 air.heard.extend([
669 on_air(0x0B, 7, 0, "kitchen/light", b"on"),
670 on_air(0x0B, 8, 0, "garden/moisture", b"28.5"),
671 on_air(0x0B, 8, 0, "garden/moisture", b"28.5"),
672 on_air(0x0A, 1, 0, "garden/valve", b"open"),
673 on_air(0x0C, 1, 0, "garden/valve", b"shut"),
674 ]);
675 let mut node = MeshRadio::new(air, 0x0A, 1000);
676 node.connect().await.unwrap();
677 node.subscribe("garden/+").await.unwrap();
678
679 let first = node.recv().await.unwrap().unwrap();
680 assert_eq!(first, Message::new("garden/moisture", b"28.5"));
681 let second = node.recv().await.unwrap().unwrap();
682 assert_eq!(second, Message::new("garden/valve", b"shut"));
683 assert!(node.radio().heard.is_empty());
684 assert!(node.radio().sent.is_empty());
685 }
686
687 #[tokio::test(start_paused = true)]
688 async fn a_frame_with_hops_left_is_relayed_with_one_hop_spent() {
689 let heard = on_air(0x0B, 3, 2, "alerts/flood", b"high");
690 let mut air = Air::default();
691 air.heard.push_back(heard.clone());
692 let mut node = MeshRadio::new(air, 0x0A, 1000);
693 node.connect().await.unwrap();
694 node.subscribe("alerts/#").await.unwrap();
695
696 let alert = node.recv().await.unwrap().unwrap();
697 assert_eq!(alert, Message::new("alerts/flood", b"high"));
698 let relayed = Frame::parse(&node.radio().sent[0]).unwrap();
699 let original = Frame::parse(&heard).unwrap();
700 assert_eq!(relayed.src(), 0x0B);
701 assert_eq!(relayed.id(), 3);
702 assert_eq!(relayed.hop_limit(), 1);
703 assert_eq!(relayed.payload(), original.payload());
704 }
705
706 #[tokio::test(start_paused = true)]
707 async fn a_node_that_does_not_relay_only_listens() {
708 let mut air = Air::default();
709 air.heard
710 .push_back(on_air(0x0B, 3, 2, "alerts/flood", b"high"));
711 let mut node = MeshRadio::new(air, 0x0A, 1000).without_relaying();
712 node.connect().await.unwrap();
713 node.subscribe("#").await.unwrap();
714
715 assert!(node.recv().await.unwrap().is_some());
716 assert!(node.radio().sent.is_empty());
717 }
718
719 #[tokio::test(start_paused = true)]
720 async fn a_message_too_long_for_one_frame_is_refused() {
721 let mut node = MeshRadio::new(Air::default(), 0x0A, 1000);
722 node.connect().await.unwrap();
723 let most = [0u8; MAX_MESSAGE];
724 assert!(matches!(
725 node.send("t", &most).await,
726 Err(Error::Transport(_))
727 ));
728 node.send("", &most).await.unwrap();
729 assert_eq!(node.radio().sent[0].len(), Frame::MAX_LEN);
730 }
731
732 #[test]
733 fn a_body_that_claims_a_longer_topic_than_it_holds_is_not_a_message() {
734 assert_eq!(decode(&[5, b'a', b'b']), None);
735 assert_eq!(decode(&[]), None);
736 assert_eq!(decode(&[1, 0xFF, b'x']), None);
737 assert_eq!(decode(&[1, b't', b'x']), Some(("t", &b"x"[..])));
738 }
739}