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