1use core::future::Future;
58use core::pin::Pin;
59use core::task::{Context, Poll};
60
61use pamoja_core::{Error, Message, Receive, Result, Store, Transport};
62
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
65pub enum Delivery {
66 Sent,
68 Buffered,
71}
72
73type Inbound<'a> = Pin<Box<dyn Future<Output = Result<Option<Message>>> + Send + 'a>>;
75
76trait DynTransport: Send {
86 fn connect(&mut self) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>>;
88
89 fn send<'a>(
91 &'a mut self,
92 topic: &'a str,
93 payload: &'a [u8],
94 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
95
96 fn subscribe<'a>(
98 &'a mut self,
99 topic: &'a str,
100 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
101
102 fn recv(&mut self) -> Inbound<'_>;
104}
105
106struct Erased<T>(T);
111
112impl<T: Transport + Receive + Send> DynTransport for Erased<T> {
113 fn connect(&mut self) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
114 Box::pin(Transport::connect(&mut self.0))
115 }
116
117 fn send<'a>(
118 &'a mut self,
119 topic: &'a str,
120 payload: &'a [u8],
121 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
122 Box::pin(Transport::send(&mut self.0, topic, payload))
123 }
124
125 fn subscribe<'a>(
126 &'a mut self,
127 topic: &'a str,
128 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
129 Box::pin(Transport::subscribe(&mut self.0, topic))
130 }
131
132 fn recv(&mut self) -> Inbound<'_> {
133 Box::pin(Receive::recv(&mut self.0))
134 }
135}
136
137struct Uplink<T>(T);
140
141impl<T: Transport + Send> DynTransport for Uplink<T> {
142 fn connect(&mut self) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
143 Box::pin(Transport::connect(&mut self.0))
144 }
145
146 fn send<'a>(
147 &'a mut self,
148 topic: &'a str,
149 payload: &'a [u8],
150 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
151 Box::pin(Transport::send(&mut self.0, topic, payload))
152 }
153
154 fn subscribe<'a>(
155 &'a mut self,
156 topic: &'a str,
157 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
158 Box::pin(Transport::subscribe(&mut self.0, topic))
159 }
160
161 fn recv(&mut self) -> Inbound<'_> {
162 Box::pin(async { Ok(None) })
163 }
164}
165
166struct Rung {
168 transport: Box<dyn DynTransport>,
169 listens: bool,
171 connected: bool,
174 subscribed: Vec<String>,
176}
177
178impl Rung {
179 fn drop_link(&mut self) {
182 self.connected = false;
183 self.subscribed.clear();
184 }
185
186 async fn place(&mut self, filter: &str) -> Result<()> {
191 if self.subscribed.iter().any(|placed| placed == filter) {
192 return Ok(());
193 }
194 match self.transport.subscribe(filter).await {
195 Ok(()) => {
196 self.subscribed.push(filter.to_owned());
197 Ok(())
198 }
199 Err(Error::Closed) => {
200 self.drop_link();
201 Err(Error::Closed)
202 }
203 Err(error) => Err(error),
204 }
205 }
206}
207
208struct FirstReady<'a> {
214 inbound: Vec<(usize, Inbound<'a>)>,
215 start: usize,
216}
217
218impl Future for FirstReady<'_> {
219 type Output = (usize, Result<Option<Message>>);
220
221 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
222 let this = &mut *self;
223 let count = this.inbound.len();
224 for offset in 0..count {
225 let (index, future) = &mut this.inbound[(this.start + offset) % count];
226 if let Poll::Ready(outcome) = future.as_mut().poll(cx) {
227 return Poll::Ready((*index, outcome));
228 }
229 }
230 Poll::Pending
231 }
232}
233
234pub struct TransportLadder<S> {
242 rungs: Vec<Rung>,
243 filters: Vec<String>,
244 buffer: S,
245 turn: usize,
246}
247
248impl<S: Store> TransportLadder<S> {
249 pub fn new(buffer: S) -> Self {
261 Self {
262 rungs: Vec::new(),
263 filters: Vec::new(),
264 buffer,
265 turn: 0,
266 }
267 }
268
269 pub fn rung(mut self, transport: impl Transport + Receive + Send + 'static) -> Self {
281 self.rungs.push(Rung {
282 transport: Box::new(Erased(transport)),
283 listens: true,
284 connected: false,
285 subscribed: Vec::new(),
286 });
287 self
288 }
289
290 pub fn uplink(mut self, transport: impl Transport + Send + 'static) -> Self {
305 self.rungs.push(Rung {
306 transport: Box::new(Uplink(transport)),
307 listens: false,
308 connected: false,
309 subscribed: Vec::new(),
310 });
311 self
312 }
313
314 pub async fn connect(&mut self) -> Result<()> {
330 for rung in self.rungs.iter_mut().filter(|rung| !rung.connected) {
331 if rung.transport.connect().await.is_err() {
332 continue;
333 }
334 rung.connected = true;
335 if !rung.listens {
336 continue;
337 }
338 for filter in &self.filters {
339 if matches!(rung.place(filter).await, Err(Error::Closed)) {
340 break;
341 }
342 }
343 }
344 Ok(())
345 }
346
347 pub async fn send(&mut self, topic: &str, payload: &[u8]) -> Result<Delivery> {
371 if self.buffer.len().await? == 0 && Self::deliver(&mut self.rungs, topic, payload).await {
372 return Ok(Delivery::Sent);
373 }
374 self.buffer.append(&frame(topic, payload)).await?;
375 Ok(Delivery::Buffered)
376 }
377
378 pub async fn send_text(&mut self, topic: &str, text: &str) -> Result<Delivery> {
399 self.send(topic, text.as_bytes()).await
400 }
401
402 pub async fn flush(&mut self) -> Result<usize> {
419 let mut forwarded = 0;
420 while let Some(record) = self.buffer.peek().await? {
421 let (topic, payload) = unframe(&record)?;
422 if !Self::deliver(&mut self.rungs, &topic, &payload).await {
423 break;
424 }
425 self.buffer.pop().await?;
426 forwarded += 1;
427 }
428 Ok(forwarded)
429 }
430
431 pub async fn buffered(&mut self) -> Result<usize> {
447 self.buffer.len().await
448 }
449
450 pub async fn subscribe(&mut self, topic: &str) -> Result<()> {
473 if !self.filters.iter().any(|filter| filter == topic) {
474 self.filters.push(topic.to_owned());
475 }
476 let mut live = false;
477 let mut refusal = None;
478 for rung in self
479 .rungs
480 .iter_mut()
481 .filter(|rung| rung.listens && rung.connected)
482 {
483 match rung.place(topic).await {
484 Ok(()) => live = true,
485 Err(Error::Closed) => {}
486 Err(error) => refusal = Some(error),
487 }
488 }
489 match refusal {
490 Some(error) if !live => Err(error),
491 _ => Ok(()),
492 }
493 }
494
495 pub async fn recv(&mut self) -> Result<Option<Message>> {
521 loop {
522 let inbound: Vec<(usize, Inbound<'_>)> = self
523 .rungs
524 .iter_mut()
525 .enumerate()
526 .filter(|(_, rung)| rung.listens && rung.connected)
527 .map(|(index, rung)| (index, rung.transport.recv()))
528 .collect();
529 if inbound.is_empty() {
530 return Err(Error::Closed);
531 }
532 let start = self.turn % inbound.len();
533 self.turn = self.turn.wrapping_add(1);
534 let (index, outcome) = FirstReady { inbound, start }.await;
535 match outcome {
536 Ok(Some(message)) => return Ok(Some(message)),
537 Ok(None) | Err(Error::Closed) => self.rungs[index].drop_link(),
538 Err(error) => return Err(error),
539 }
540 }
541 }
542
543 async fn deliver(rungs: &mut [Rung], topic: &str, payload: &[u8]) -> bool {
547 for rung in rungs.iter_mut().filter(|rung| rung.connected) {
548 match rung.transport.send(topic, payload).await {
549 Ok(()) => return true,
550 Err(Error::Closed) => rung.drop_link(),
551 Err(_) => {}
552 }
553 }
554 false
555 }
556}
557
558impl<S: Store + Send> Transport for TransportLadder<S> {
559 async fn connect(&mut self) -> Result<()> {
560 TransportLadder::connect(self).await
561 }
562
563 async fn send(&mut self, topic: &str, payload: &[u8]) -> Result<()> {
566 TransportLadder::send(self, topic, payload)
567 .await
568 .map(|_| ())
569 }
570
571 async fn subscribe(&mut self, topic: &str) -> Result<()> {
572 TransportLadder::subscribe(self, topic).await
573 }
574}
575
576impl<S: Store + Send> Receive for TransportLadder<S> {
577 async fn recv(&mut self) -> Result<Option<Message>> {
578 TransportLadder::recv(self).await
579 }
580}
581
582fn frame(topic: &str, payload: &[u8]) -> Vec<u8> {
587 let mut record = Vec::with_capacity(4 + topic.len() + payload.len());
588 record.extend_from_slice(&(topic.len() as u32).to_be_bytes());
589 record.extend_from_slice(topic.as_bytes());
590 record.extend_from_slice(payload);
591 record
592}
593
594fn unframe(record: &[u8]) -> Result<(String, Vec<u8>)> {
596 let header: [u8; 4] = record
597 .get(..4)
598 .ok_or_else(|| Error::Codec("ladder record is missing its length header".to_owned()))?
599 .try_into()
600 .expect("a four-byte slice");
601 let topic_len = u32::from_be_bytes(header) as usize;
602 let topic_bytes = record
603 .get(4..4 + topic_len)
604 .ok_or_else(|| Error::Codec("ladder record topic is truncated".to_owned()))?;
605 let topic =
606 String::from_utf8(topic_bytes.to_vec()).map_err(|err| Error::Codec(err.to_string()))?;
607 let payload = record[4 + topic_len..].to_vec();
608 Ok((topic, payload))
609}
610
611#[cfg(test)]
612mod tests {
613 use super::*;
614
615 use std::collections::VecDeque;
616 use std::sync::{Arc, Mutex};
617 use std::time::Duration;
618
619 use pamoja_codec::CborCodec;
620 use pamoja_core::{Actuator, Sensor};
621 use pamoja_loopback::{Faulty, LoopbackBroker, LoopbackTransport};
622 use pamoja_profile::{ControlSpec, Node, PowerSchedule, Profile};
623 use pamoja_sync::MemoryStore;
624
625 fn assert_send<T: Send>(_value: T) {}
627
628 fn assert_link<T: Transport + Receive>(_value: &T) {}
630
631 #[derive(Default)]
633 struct Log {
634 connects: usize,
635 subscriptions: Vec<String>,
636 sent: Vec<(String, Vec<u8>)>,
637 closed: bool,
638 }
639
640 struct Scripted {
643 inbound: VecDeque<Option<Message>>,
644 log: Arc<Mutex<Log>>,
645 }
646
647 impl Scripted {
648 fn new(inbound: impl IntoIterator<Item = Option<Message>>) -> (Self, Arc<Mutex<Log>>) {
649 let log = Arc::new(Mutex::new(Log::default()));
650 let link = Self {
651 inbound: inbound.into_iter().collect(),
652 log: Arc::clone(&log),
653 };
654 (link, log)
655 }
656 }
657
658 impl Transport for Scripted {
659 async fn connect(&mut self) -> Result<()> {
660 self.log.lock().expect("log").connects += 1;
661 Ok(())
662 }
663
664 async fn send(&mut self, topic: &str, payload: &[u8]) -> Result<()> {
665 let mut log = self.log.lock().expect("log");
666 if log.closed {
667 return Err(Error::Closed);
668 }
669 log.sent.push((topic.to_owned(), payload.to_vec()));
670 Ok(())
671 }
672
673 async fn subscribe(&mut self, topic: &str) -> Result<()> {
674 self.log
675 .lock()
676 .expect("log")
677 .subscriptions
678 .push(topic.to_owned());
679 Ok(())
680 }
681 }
682
683 impl Receive for Scripted {
684 async fn recv(&mut self) -> Result<Option<Message>> {
685 match self.inbound.pop_front() {
686 Some(next) => Ok(next),
687 None => std::future::pending().await,
688 }
689 }
690 }
691
692 #[test]
693 fn a_ladder_can_be_driven_from_a_spawned_task() {
694 let mut ladder = TransportLadder::new(MemoryStore::new())
698 .rung(LoopbackTransport::new(LoopbackBroker::new()));
699 assert_send(ladder.connect());
700 assert_send(ladder.send("sensors/1", b"21.5"));
701 assert_send(ladder.flush());
702 assert_send(ladder.buffered());
703 assert_send(ladder.subscribe("commands/#"));
704 assert_send(ladder.recv());
705 assert_link(&ladder);
706 }
707
708 async fn gateway(broker: &LoopbackBroker) -> LoopbackTransport {
710 let mut gateway = LoopbackTransport::new(broker.clone());
711 gateway.connect().await.expect("connect gateway");
712 gateway.subscribe("#").await.expect("subscribe gateway");
713 gateway
714 }
715
716 #[test]
717 fn frame_round_trips_topic_and_payload() {
718 let record = frame("sensors/1/temperature", b"21.5");
719 let (topic, payload) = unframe(&record).expect("unframe");
720 assert_eq!(topic, "sensors/1/temperature");
721 assert_eq!(payload, b"21.5");
722 }
723
724 #[test]
725 fn unframe_rejects_a_truncated_record() {
726 assert!(matches!(unframe(&[0, 0]), Err(Error::Codec(_))));
727 assert!(matches!(unframe(&[0, 0, 0, 4, b'a']), Err(Error::Codec(_))));
729 }
730
731 #[tokio::test]
732 async fn send_delivers_over_the_first_working_rung() {
733 let broker = LoopbackBroker::new();
734 let mut observer = gateway(&broker).await;
735
736 let mut ladder =
737 TransportLadder::new(MemoryStore::new()).rung(LoopbackTransport::new(broker.clone()));
738 ladder.connect().await.expect("connect");
739
740 let delivery = ladder
741 .send("sensors/1/temperature", b"21.5")
742 .await
743 .expect("send");
744 assert_eq!(delivery, Delivery::Sent);
745 assert_eq!(ladder.buffered().await.expect("buffered"), 0);
746
747 let message = observer.recv().await.expect("recv").expect("a message");
748 assert_eq!(message.topic, "sensors/1/temperature");
749 assert_eq!(message.payload, b"21.5");
750 }
751
752 #[tokio::test]
753 async fn send_falls_over_to_a_cheaper_rung_that_is_down() {
754 let preferred_broker = LoopbackBroker::new();
757 let fallback_broker = LoopbackBroker::new();
758 let mut preferred_observer = gateway(&preferred_broker).await;
759 let mut fallback_observer = gateway(&fallback_broker).await;
760
761 let preferred = Faulty::new(LoopbackTransport::new(preferred_broker.clone()), 1);
762 let fallback = LoopbackTransport::new(fallback_broker.clone());
763 let mut ladder = TransportLadder::new(MemoryStore::new())
764 .rung(preferred)
765 .rung(fallback);
766 ladder.connect().await.expect("connect");
767
768 let delivery = ladder.send("t", b"x").await.expect("send");
769 assert_eq!(delivery, Delivery::Sent);
770
771 let message = fallback_observer
772 .recv()
773 .await
774 .expect("recv")
775 .expect("a message");
776 assert_eq!(message.payload, b"x");
777 let starved =
779 tokio::time::timeout(Duration::from_millis(50), preferred_observer.recv()).await;
780 assert!(
781 starved.is_err(),
782 "the broken rung must not deliver anything"
783 );
784 }
785
786 #[tokio::test]
787 async fn buffers_when_every_rung_is_down_then_flushes_in_order() {
788 let broker = LoopbackBroker::new();
789 let mut observer = gateway(&broker).await;
790
791 let rung = Faulty::new(LoopbackTransport::new(broker.clone()), 1);
793 let mut ladder = TransportLadder::new(MemoryStore::new()).rung(rung);
794 ladder.connect().await.expect("connect");
795
796 assert_eq!(
799 ladder.send("out", b"a").await.expect("send"),
800 Delivery::Buffered
801 );
802 assert_eq!(
803 ladder.send("out", b"b").await.expect("send"),
804 Delivery::Buffered
805 );
806 assert_eq!(
807 ladder.send("out", b"c").await.expect("send"),
808 Delivery::Buffered
809 );
810 assert_eq!(ladder.buffered().await.expect("buffered"), 3);
811
812 let forwarded = ladder.flush().await.expect("flush");
814 assert_eq!(forwarded, 3);
815 assert_eq!(ladder.buffered().await.expect("buffered"), 0);
816
817 for expected in [b"a", b"b", b"c"] {
818 let message = observer.recv().await.expect("recv").expect("a message");
819 assert_eq!(message.topic, "out");
820 assert_eq!(message.payload, expected);
821 }
822 }
823
824 #[tokio::test]
825 async fn flush_stops_at_the_first_record_no_rung_accepts() {
826 let broker = LoopbackBroker::new();
827
828 let rung = Faulty::new(LoopbackTransport::new(broker.clone()), 3);
831 let mut ladder = TransportLadder::new(MemoryStore::new()).rung(rung);
832 ladder.connect().await.expect("connect");
833
834 ladder.send("out", b"a").await.expect("send");
835 ladder.send("out", b"b").await.expect("send");
836 ladder.send("out", b"c").await.expect("send");
837
838 assert_eq!(ladder.flush().await.expect("flush"), 0);
841 assert_eq!(ladder.buffered().await.expect("buffered"), 3);
842 }
843
844 #[tokio::test]
845 async fn a_subscription_goes_on_every_rung_and_a_receive_takes_whichever_delivers() {
846 let mesh = LoopbackBroker::new();
848 let backhaul = LoopbackBroker::new();
849 let mut ladder = TransportLadder::new(MemoryStore::new())
850 .rung(LoopbackTransport::new(mesh.clone()))
851 .rung(LoopbackTransport::new(backhaul.clone()));
852 ladder.connect().await.expect("connect");
853 ladder.subscribe("commands/#").await.expect("subscribe");
854
855 let mut over_backhaul = LoopbackTransport::new(backhaul);
857 over_backhaul.connect().await.expect("connect");
858 over_backhaul
859 .send("commands/valve", b"open")
860 .await
861 .expect("send");
862 let command = ladder.recv().await.expect("recv").expect("a command");
863 assert_eq!(command.topic, "commands/valve");
864 assert_eq!(command.payload, b"open");
865
866 let mut over_mesh = LoopbackTransport::new(mesh);
867 over_mesh.connect().await.expect("connect");
868 over_mesh
869 .send("commands/valve", b"close")
870 .await
871 .expect("send");
872 let command = ladder.recv().await.expect("recv").expect("a command");
873 assert_eq!(command.payload, b"close");
874 }
875
876 #[tokio::test]
877 async fn a_filter_placed_while_no_rung_is_up_takes_effect_on_connect() {
878 let broker = LoopbackBroker::new();
879 let mut ladder =
880 TransportLadder::new(MemoryStore::new()).rung(LoopbackTransport::new(broker.clone()));
881
882 ladder.subscribe("commands/#").await.expect("held");
885 assert!(matches!(ladder.recv().await, Err(Error::Closed)));
886
887 ladder.connect().await.expect("connect");
888 let mut publisher = LoopbackTransport::new(broker);
889 publisher.connect().await.expect("connect");
890 publisher.send("commands/1", b"go").await.expect("send");
891 let command = ladder.recv().await.expect("recv").expect("a command");
892 assert_eq!(command.payload, b"go");
893 }
894
895 #[tokio::test]
896 async fn an_uplink_sends_but_is_never_listened_on() {
897 let radio = LoopbackBroker::new();
898 let mut ground = gateway(&radio).await;
899 let mut ladder =
900 TransportLadder::new(MemoryStore::new()).uplink(LoopbackTransport::new(radio.clone()));
901 ladder.connect().await.expect("connect");
902
903 assert_eq!(
905 ladder.send("sensors/1", b"21.5").await.expect("send"),
906 Delivery::Sent
907 );
908 let reading = ground.recv().await.expect("recv").expect("a message");
909 assert_eq!(reading.payload, b"21.5");
910
911 ladder.subscribe("commands/#").await.expect("held");
914 ground.send("commands/1", b"go").await.expect("send");
915 assert!(matches!(ladder.recv().await, Err(Error::Closed)));
916 }
917
918 #[tokio::test]
919 async fn receiving_rotates_between_rungs_and_drops_one_whose_link_ends() {
920 let (first, first_log) = Scripted::new([
921 Some(Message::new("c", b"first-1")),
922 None,
923 Some(Message::new("c", b"never")),
924 ]);
925 let (second, second_log) = Scripted::new([
926 Some(Message::new("c", b"second-1")),
927 Some(Message::new("c", b"second-2")),
928 ]);
929 let mut ladder = TransportLadder::new(MemoryStore::new())
930 .rung(first)
931 .rung(second);
932 ladder.connect().await.expect("connect");
933 ladder.subscribe("c").await.expect("subscribe");
934 assert_eq!(first_log.lock().expect("log").subscriptions, ["c"]);
935 assert_eq!(second_log.lock().expect("log").subscriptions, ["c"]);
936
937 let one = ladder.recv().await.expect("recv").expect("a message");
940 let two = ladder.recv().await.expect("recv").expect("a message");
941 assert_eq!(one.payload, b"first-1");
942 assert_eq!(two.payload, b"second-1");
943
944 let three = ladder.recv().await.expect("recv").expect("a message");
947 assert_eq!(three.payload, b"second-2");
948
949 ladder.connect().await.expect("connect");
952 assert_eq!(first_log.lock().expect("log").connects, 2);
953 assert_eq!(first_log.lock().expect("log").subscriptions, ["c", "c"]);
954 assert_eq!(second_log.lock().expect("log").connects, 1);
955 assert_eq!(second_log.lock().expect("log").subscriptions, ["c"]);
956 let four = ladder.recv().await.expect("recv").expect("a message");
957 assert_eq!(four.payload, b"never");
958 }
959
960 #[tokio::test]
961 async fn a_rung_that_reports_closed_on_send_is_down_until_the_next_connect() {
962 let (link, log) = Scripted::new([]);
963 let mut ladder = TransportLadder::new(MemoryStore::new()).rung(link);
964 ladder.connect().await.expect("connect");
965 assert_eq!(ladder.send("t", b"1").await.expect("send"), Delivery::Sent);
966
967 log.lock().expect("log").closed = true;
970 assert_eq!(
971 ladder.send("t", b"2").await.expect("send"),
972 Delivery::Buffered
973 );
974 log.lock().expect("log").closed = false;
975 assert_eq!(ladder.flush().await.expect("flush"), 0);
976 assert_eq!(log.lock().expect("log").connects, 1);
977
978 ladder.connect().await.expect("connect");
979 assert_eq!(log.lock().expect("log").connects, 2);
980 assert_eq!(ladder.flush().await.expect("flush"), 1);
981 assert_eq!(log.lock().expect("log").sent.len(), 2);
982 }
983
984 struct Probe(f32);
986
987 impl Sensor for Probe {
988 type Reading = f32;
989
990 async fn read(&mut self) -> Result<f32> {
991 Ok(self.0)
992 }
993 }
994
995 struct Valve(Vec<bool>);
997
998 impl Actuator for Valve {
999 type Command = bool;
1000
1001 async fn apply(&mut self, open: bool) -> Result<()> {
1002 self.0.push(open);
1003 Ok(())
1004 }
1005 }
1006
1007 #[tokio::test]
1008 async fn a_profile_node_publishes_through_a_ladder() {
1009 let broker = LoopbackBroker::new();
1010 let mut observer = gateway(&broker).await;
1011 let mut ladder =
1012 TransportLadder::new(MemoryStore::new()).rung(LoopbackTransport::new(broker));
1013 ladder.connect().await.expect("connect");
1014
1015 let profile = Profile {
1016 name: "raised-bed-drip".to_owned(),
1017 description: None,
1018 reads: None,
1019 topic: "garden/bed-1/moisture".to_owned(),
1020 control: ControlSpec::Setpoint {
1021 setpoint: 37.5,
1022 hysteresis: 7.5,
1023 cooling: false,
1024 safe_band: 30.0,
1025 },
1026 power: PowerSchedule::new(300, 1800, 3600),
1027 presentation: None,
1028 };
1029 let mut node = Node::new(profile, Probe(20.0), Valve(Vec::new()), ladder, CborCodec)
1030 .expect("a built-in kind");
1031 let reaction = node.tick().await.expect("a tick").reaction;
1032 assert_eq!(reaction.actuator, Some(true));
1033
1034 let reading = observer.recv().await.expect("recv").expect("a reading");
1035 assert_eq!(reading.topic, "garden/bed-1/moisture");
1036 assert!(!reading.payload.is_empty());
1037 }
1038}