1use crate::config::ComponentConfig;
6use crate::context::CuContext;
7use crate::cutask::{CuMsg, CuMsgPayload, Freezable};
8use crate::reflect::Reflect;
9use alloc::borrow::Cow;
10use alloc::string::String;
11use core::fmt::{Debug, Formatter};
12use core::marker::PhantomData;
13use cu29_traits::CuResult;
14
15#[derive(Copy, Clone, Debug, Eq, PartialEq)]
21pub enum TxEmptyPolicy {
22 Skip,
24 Publish,
26}
27
28#[derive(Copy, Clone)]
29pub struct BridgeChannel<Id, Payload> {
30 pub id: Id,
32 pub default_route: Option<&'static str>,
34 pub tx_empty_policy: TxEmptyPolicy,
36 _payload: PhantomData<fn() -> Payload>,
37}
38
39impl<Id, Payload> BridgeChannel<Id, Payload> {
40 pub const fn new(id: Id) -> Self {
42 Self {
43 id,
44 default_route: None,
45 tx_empty_policy: TxEmptyPolicy::Skip,
46 _payload: PhantomData,
47 }
48 }
49
50 pub const fn with_channel(id: Id, route: &'static str) -> Self {
52 Self {
53 id,
54 default_route: Some(route),
55 tx_empty_policy: TxEmptyPolicy::Skip,
56 _payload: PhantomData,
57 }
58 }
59
60 pub const fn publish_empty(mut self) -> Self {
62 self.tx_empty_policy = TxEmptyPolicy::Publish;
63 self
64 }
65
66 pub const fn should_send(&self, has_payload: bool) -> bool {
68 has_payload || matches!(self.tx_empty_policy, TxEmptyPolicy::Publish)
69 }
70}
71
72impl<Id: Debug, Payload> Debug for BridgeChannel<Id, Payload> {
73 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
74 f.debug_struct("BridgeChannel")
75 .field("id", &self.id)
76 .field("default_route", &self.default_route)
77 .field("tx_empty_policy", &self.tx_empty_policy)
78 .finish()
79 }
80}
81
82pub trait BridgeChannelInfo<Id: Copy> {
84 fn id(&self) -> Id;
86 fn default_route(&self) -> Option<&'static str>;
88 fn tx_empty_policy(&self) -> TxEmptyPolicy;
90}
91
92impl<Id: Copy, Payload> BridgeChannelInfo<Id> for BridgeChannel<Id, Payload> {
93 fn id(&self) -> Id {
94 self.id
95 }
96
97 fn default_route(&self) -> Option<&'static str> {
98 self.default_route
99 }
100
101 fn tx_empty_policy(&self) -> TxEmptyPolicy {
102 self.tx_empty_policy
103 }
104}
105
106#[derive(Copy, Clone, Debug)]
109pub struct BridgeChannelDescriptor<Id: Copy> {
110 pub id: Id,
112 pub default_route: Option<&'static str>,
114 pub tx_empty_policy: TxEmptyPolicy,
116}
117
118impl<Id: Copy> BridgeChannelDescriptor<Id> {
119 pub const fn new(
120 id: Id,
121 default_route: Option<&'static str>,
122 tx_empty_policy: TxEmptyPolicy,
123 ) -> Self {
124 Self {
125 id,
126 default_route,
127 tx_empty_policy,
128 }
129 }
130}
131
132impl<Id: Copy, T> From<&T> for BridgeChannelDescriptor<Id>
133where
134 T: BridgeChannelInfo<Id> + ?Sized,
135{
136 fn from(channel: &T) -> Self {
137 BridgeChannelDescriptor::new(
138 channel.id(),
139 channel.default_route(),
140 channel.tx_empty_policy(),
141 )
142 }
143}
144
145#[derive(Clone, Debug)]
147pub struct BridgeChannelConfig<Id: Copy> {
148 pub channel: BridgeChannelDescriptor<Id>,
150 pub route: Option<String>,
152 pub config: Option<ComponentConfig>,
154}
155
156impl<Id: Copy> BridgeChannelConfig<Id> {
157 pub fn from_static<T>(
159 channel: &'static T,
160 route: Option<String>,
161 config: Option<ComponentConfig>,
162 ) -> Self
163 where
164 T: BridgeChannelInfo<Id> + ?Sized,
165 {
166 Self {
167 channel: channel.into(),
168 route,
169 config,
170 }
171 }
172
173 pub fn effective_route(&self) -> Option<Cow<'_, str>> {
175 if let Some(route) = &self.route {
176 Some(Cow::Borrowed(route.as_str()))
177 } else {
178 self.channel.default_route.map(Cow::Borrowed)
179 }
180 }
181}
182
183pub trait BridgeChannelSet {
189 type Id: Copy + Eq + 'static;
191
192 const STATIC_CHANNELS: &'static [&'static dyn BridgeChannelInfo<Self::Id>];
194}
195
196pub trait CuBridge: Freezable + Reflect {
202 type Tx: BridgeChannelSet;
204 type Rx: BridgeChannelSet;
206 type Resources<'r>;
208
209 fn new(
214 config: Option<&ComponentConfig>,
215 tx_channels: &[BridgeChannelConfig<<Self::Tx as BridgeChannelSet>::Id>],
216 rx_channels: &[BridgeChannelConfig<<Self::Rx as BridgeChannelSet>::Id>],
217 resources: Self::Resources<'_>,
218 ) -> CuResult<Self>
219 where
220 Self: Sized;
221
222 fn start(&mut self, _ctx: &CuContext) -> CuResult<()> {
224 Ok(())
225 }
226
227 fn preprocess(&mut self, _ctx: &CuContext) -> CuResult<()> {
229 Ok(())
230 }
231
232 fn send<'a, Payload>(
234 &mut self,
235 ctx: &CuContext,
236 channel: &'static BridgeChannel<<Self::Tx as BridgeChannelSet>::Id, Payload>,
237 msg: &CuMsg<Payload>,
238 ) -> CuResult<()>
239 where
240 Payload: CuMsgPayload + 'a;
241
242 fn receive<'a, Payload>(
246 &mut self,
247 ctx: &CuContext,
248 channel: &'static BridgeChannel<<Self::Rx as BridgeChannelSet>::Id, Payload>,
249 msg: &mut CuMsg<Payload>,
250 ) -> CuResult<()>
251 where
252 Payload: CuMsgPayload + 'a;
253
254 fn postprocess(&mut self, _ctx: &CuContext) -> CuResult<()> {
256 Ok(())
257 }
258
259 fn stop(&mut self, _ctx: &CuContext) -> CuResult<()> {
261 Ok(())
262 }
263}
264
265#[doc(hidden)]
266#[macro_export]
267macro_rules! __cu29_bridge_channel_ctor {
268 ($id:ident, $variant:ident, $payload:ty) => {
269 $crate::cubridge::BridgeChannel::<$id, $payload>::new($id::$variant)
270 };
271 ($id:ident, $variant:ident, $payload:ty, [publish_empty]) => {
272 $crate::cubridge::BridgeChannel::<$id, $payload>::new($id::$variant).publish_empty()
273 };
274 ($id:ident, $variant:ident, $payload:ty, $route:expr) => {
275 $crate::cubridge::BridgeChannel::<$id, $payload>::with_channel($id::$variant, $route)
276 };
277 ($id:ident, $variant:ident, $payload:ty, $route:expr, [publish_empty]) => {
278 $crate::cubridge::BridgeChannel::<$id, $payload>::with_channel($id::$variant, $route)
279 .publish_empty()
280 };
281}
282
283#[doc(hidden)]
284#[macro_export]
285macro_rules! __cu29_define_bridge_channels {
286 (
287 @accum
288 $vis:vis struct $channels:ident : $id:ident
289 [ $($parsed:tt)+ ]
290 ) => {
291 $crate::__cu29_emit_bridge_channels! {
292 $vis struct $channels : $id { $($parsed)+ }
293 }
294 };
295 (
296 @accum
297 $vis:vis struct $channels:ident : $id:ident
298 [ ]
299 ) => {
300 compile_error!("tx_channels!/rx_channels! require at least one channel");
301 };
302 (
303 @accum
304 $vis:vis struct $channels:ident : $id:ident
305 [ $($parsed:tt)* ]
306 $(#[$chan_meta:meta])* $( [ $publish_empty:ident ] )? $const_name:ident : $variant:ident => $payload:ty $(= $route:expr)? , $($rest:tt)*
307 ) => {
308 $crate::__cu29_define_bridge_channels!(
309 @accum
310 $vis struct $channels : $id
311 [
312 $($parsed)*
313 $(#[$chan_meta])* $( [ $publish_empty ] )? $const_name : $variant => $payload $(= $route)?,
314 ]
315 $($rest)*
316 );
317 };
318 (
319 @accum
320 $vis:vis struct $channels:ident : $id:ident
321 [ $($parsed:tt)* ]
322 $(#[$chan_meta:meta])* $( [ $publish_empty:ident ] )? $const_name:ident : $variant:ident => $payload:ty $(= $route:expr)?
323 ) => {
324 $crate::__cu29_define_bridge_channels!(
325 @accum
326 $vis struct $channels : $id
327 [
328 $($parsed)*
329 $(#[$chan_meta])* $( [ $publish_empty ] )? $const_name : $variant => $payload $(= $route)?,
330 ]
331 );
332 };
333 (
334 @accum
335 $vis:vis struct $channels:ident : $id:ident
336 [ $($parsed:tt)* ]
337 $(#[$chan_meta:meta])* $( [ $publish_empty:ident ] )? $name:ident => $payload:ty $(= $route:expr)? , $($rest:tt)*
338 ) => {
339 $crate::__cu29_paste! {
340 $crate::__cu29_define_bridge_channels!(
341 @accum
342 $vis struct $channels : $id
343 [
344 $($parsed)*
345 $(#[$chan_meta])* $( [ $publish_empty ] )? [<$name:snake:upper>] : [<$name:camel>] => $payload $(= $route)?,
346 ]
347 $($rest)*
348 );
349 }
350 };
351 (
352 @accum
353 $vis:vis struct $channels:ident : $id:ident
354 [ $($parsed:tt)* ]
355 $(#[$chan_meta:meta])* $( [ $publish_empty:ident ] )? $name:ident => $payload:ty $(= $route:expr)?
356 ) => {
357 $crate::__cu29_paste! {
358 $crate::__cu29_define_bridge_channels!(
359 @accum
360 $vis struct $channels : $id
361 [
362 $($parsed)*
363 $(#[$chan_meta])* $( [ $publish_empty ] )? [<$name:snake:upper>] : [<$name:camel>] => $payload $(= $route)?,
364 ]
365 );
366 }
367 };
368 (
369 $vis:vis struct $channels:ident : $id:ident {
370 $($body:tt)*
371 }
372 ) => {
373 $crate::__cu29_define_bridge_channels!(
374 @accum
375 $vis struct $channels : $id
376 []
377 $($body)*
378 );
379 };
380}
381
382#[doc(hidden)]
383#[macro_export]
384macro_rules! __cu29_emit_bridge_channels {
385 (
386 $vis:vis struct $channels:ident : $id:ident {
387 $(
388 $(#[$chan_meta:meta])*
389 $( [ $publish_empty:ident ] )? $const_name:ident : $variant:ident => $payload:ty $(= $route:expr)?,
390 )+
391 }
392 ) => {
393 #[derive(Copy, Clone, Debug, Eq, PartialEq, ::serde::Serialize, ::serde::Deserialize)]
394 #[repr(usize)]
395 #[serde(rename_all = "snake_case")]
396 $vis enum $id {
397 $(
398 $variant,
399 )+
400 }
401
402 impl $id {
403 pub const fn as_index(self) -> usize {
405 self as usize
406 }
407 }
408
409 $vis struct $channels;
410
411 #[allow(non_upper_case_globals)]
412 impl $channels {
413 $(
414 $(#[$chan_meta])*
415 $vis const $const_name: $crate::cubridge::BridgeChannel<$id, $payload> =
416 $crate::__cu29_bridge_channel_ctor!(
417 $id, $variant, $payload $(, $route)? $(, [ $publish_empty ])?
418 );
419 )+
420 }
421
422 impl $crate::cubridge::BridgeChannelSet for $channels {
423 type Id = $id;
424
425 const STATIC_CHANNELS: &'static [&'static dyn $crate::cubridge::BridgeChannelInfo<Self::Id>] =
426 &[
427 $(
428 &Self::$const_name,
429 )+
430 ];
431 }
432 };
433}
434
435#[macro_export]
466macro_rules! tx_channels {
467 (
468 $vis:vis struct $channels:ident : $id:ident {
469 $($body:tt)*
470 }
471 ) => {
472 $crate::__cu29_define_bridge_channels! {
473 $vis struct $channels : $id {
474 $($body)*
475 }
476 }
477 };
478 ({ $($rest:tt)* }) => {
479 $crate::tx_channels! {
480 pub struct TxChannels : TxId { $($rest)* }
481 }
482 };
483 ($($rest:tt)+) => {
484 $crate::tx_channels!({ $($rest)+ });
485 };
486}
487
488#[macro_export]
492macro_rules! rx_channels {
493 (
494 $vis:vis struct $channels:ident : $id:ident {
495 $($body:tt)*
496 }
497 ) => {
498 $crate::__cu29_define_bridge_channels! {
499 $vis struct $channels : $id {
500 $($body)*
501 }
502 }
503 };
504 ({ $($rest:tt)* }) => {
505 $crate::rx_channels! {
506 pub struct RxChannels : RxId { $($rest)* }
507 }
508 };
509 ($($rest:tt)+) => {
510 $crate::rx_channels!({ $($rest)+ });
511 };
512}
513
514#[cfg(test)]
515mod tests {
516 use super::*;
517 use crate::config::ComponentConfig;
518 use crate::context::CuContext;
519 use crate::cutask::CuMsg;
520 use alloc::vec::Vec;
521 use cu29_traits::CuError;
522 use serde::{Deserialize, Serialize};
523
524 #[derive(
526 Clone, Debug, Default, Serialize, Deserialize, bincode::Encode, bincode::Decode, Reflect,
527 )]
528 struct ImuMsg {
529 accel: i32,
530 }
531
532 #[derive(
533 Clone, Debug, Default, Serialize, Deserialize, bincode::Encode, bincode::Decode, Reflect,
534 )]
535 struct MotorCmd {
536 torque: i16,
537 }
538
539 #[derive(
540 Clone, Debug, Default, Serialize, Deserialize, bincode::Encode, bincode::Decode, Reflect,
541 )]
542 struct StatusMsg {
543 temperature: f32,
544 }
545
546 #[derive(
547 Clone, Debug, Default, Serialize, Deserialize, bincode::Encode, bincode::Decode, Reflect,
548 )]
549 struct AlertMsg {
550 code: u32,
551 }
552
553 tx_channels! {
554 struct MacroTxChannels : MacroTxId {
555 imu_stream => ImuMsg = "telemetry/imu",
556 [publish_empty] motor_stream => MotorCmd,
557 }
558 }
559
560 rx_channels! {
561 struct MacroRxChannels : MacroRxId {
562 status_updates => StatusMsg = "sys/status",
563 alert_stream => AlertMsg,
564 }
565 }
566
567 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
569 enum TxId {
570 Imu,
571 Motor,
572 }
573
574 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
575 enum RxId {
576 Status,
577 Alert,
578 }
579
580 struct TxChannels;
582
583 impl TxChannels {
584 pub const IMU: BridgeChannel<TxId, ImuMsg> =
585 BridgeChannel::with_channel(TxId::Imu, "telemetry/imu");
586 pub const MOTOR: BridgeChannel<TxId, MotorCmd> =
587 BridgeChannel::with_channel(TxId::Motor, "motor/cmd").publish_empty();
588 }
589
590 impl BridgeChannelSet for TxChannels {
591 type Id = TxId;
592
593 const STATIC_CHANNELS: &'static [&'static dyn BridgeChannelInfo<Self::Id>] =
594 &[&Self::IMU, &Self::MOTOR];
595 }
596
597 struct RxChannels;
598
599 impl RxChannels {
600 pub const STATUS: BridgeChannel<RxId, StatusMsg> =
601 BridgeChannel::with_channel(RxId::Status, "sys/status");
602 pub const ALERT: BridgeChannel<RxId, AlertMsg> =
603 BridgeChannel::with_channel(RxId::Alert, "sys/alert");
604 }
605
606 impl BridgeChannelSet for RxChannels {
607 type Id = RxId;
608
609 const STATIC_CHANNELS: &'static [&'static dyn BridgeChannelInfo<Self::Id>] =
610 &[&Self::STATUS, &Self::ALERT];
611 }
612
613 #[derive(Default, Reflect)]
615 struct ExampleBridge {
616 port: String,
617 imu_samples: Vec<i32>,
618 motor_torques: Vec<i16>,
619 status_temps: Vec<f32>,
620 alert_codes: Vec<u32>,
621 }
622
623 impl Freezable for ExampleBridge {
624 fn freeze<E: bincode::enc::Encoder>(
625 &self,
626 encoder: &mut E,
627 ) -> Result<(), bincode::error::EncodeError> {
628 bincode::Encode::encode(&self.imu_samples, encoder)?;
629 bincode::Encode::encode(&self.motor_torques, encoder)?;
630 bincode::Encode::encode(&self.status_temps, encoder)?;
631 bincode::Encode::encode(&self.alert_codes, encoder)?;
632 Ok(())
633 }
634
635 fn thaw<D: bincode::de::Decoder>(
636 &mut self,
637 decoder: &mut D,
638 ) -> Result<(), bincode::error::DecodeError> {
639 self.imu_samples = bincode::Decode::decode(decoder)?;
640 self.motor_torques = bincode::Decode::decode(decoder)?;
641 self.status_temps = bincode::Decode::decode(decoder)?;
642 self.alert_codes = bincode::Decode::decode(decoder)?;
643 Ok(())
644 }
645 }
646
647 impl CuBridge for ExampleBridge {
648 type Resources<'r> = ();
649 type Tx = TxChannels;
650 type Rx = RxChannels;
651
652 fn new(
653 config: Option<&ComponentConfig>,
654 _tx_channels: &[BridgeChannelConfig<TxId>],
655 _rx_channels: &[BridgeChannelConfig<RxId>],
656 _resources: Self::Resources<'_>,
657 ) -> CuResult<Self> {
658 let mut instance = ExampleBridge::default();
659 if let Some(cfg) = config
660 && let Some(port) = cfg.get::<String>("port")?
661 {
662 instance.port = port;
663 }
664 Ok(instance)
665 }
666
667 fn send<'a, Payload>(
668 &mut self,
669 _ctx: &CuContext,
670 channel: &'static BridgeChannel<TxId, Payload>,
671 msg: &CuMsg<Payload>,
672 ) -> CuResult<()>
673 where
674 Payload: CuMsgPayload + 'a,
675 {
676 match channel.id {
677 TxId::Imu => {
678 let imu_msg = msg.downcast_ref::<ImuMsg>()?;
679 let payload = imu_msg
680 .payload()
681 .ok_or_else(|| CuError::from("imu missing payload"))?;
682 self.imu_samples.push(payload.accel);
683 Ok(())
684 }
685 TxId::Motor => {
686 let motor_msg = msg.downcast_ref::<MotorCmd>()?;
687 let payload = motor_msg
688 .payload()
689 .ok_or_else(|| CuError::from("motor missing payload"))?;
690 self.motor_torques.push(payload.torque);
691 Ok(())
692 }
693 }
694 }
695
696 fn receive<'a, Payload>(
697 &mut self,
698 _ctx: &CuContext,
699 channel: &'static BridgeChannel<RxId, Payload>,
700 msg: &mut CuMsg<Payload>,
701 ) -> CuResult<()>
702 where
703 Payload: CuMsgPayload + 'a,
704 {
705 match channel.id {
706 RxId::Status => {
707 let status_msg = msg.downcast_mut::<StatusMsg>()?;
708 status_msg.set_payload(StatusMsg { temperature: 21.5 });
709 if let Some(payload) = status_msg.payload() {
710 self.status_temps.push(payload.temperature);
711 }
712 Ok(())
713 }
714 RxId::Alert => {
715 let alert_msg = msg.downcast_mut::<AlertMsg>()?;
716 alert_msg.set_payload(AlertMsg { code: 0xDEAD_BEEF });
717 if let Some(payload) = alert_msg.payload() {
718 self.alert_codes.push(payload.code);
719 }
720 Ok(())
721 }
722 }
723 }
724 }
725
726 #[test]
727 fn channel_macros_expose_static_metadata() {
728 assert_eq!(MacroTxChannels::STATIC_CHANNELS.len(), 2);
729 assert_eq!(
730 MacroTxChannels::IMU_STREAM.default_route,
731 Some("telemetry/imu")
732 );
733 assert!(MacroTxChannels::MOTOR_STREAM.default_route.is_none());
734 assert_eq!(
735 MacroTxChannels::IMU_STREAM.tx_empty_policy,
736 TxEmptyPolicy::Skip
737 );
738 assert_eq!(
739 MacroTxChannels::MOTOR_STREAM.tx_empty_policy,
740 TxEmptyPolicy::Publish
741 );
742 assert_eq!(MacroTxId::ImuStream as u8, MacroTxId::ImuStream as u8);
743 assert_eq!(MacroTxId::ImuStream.as_index(), 0);
744 assert_eq!(MacroTxId::MotorStream.as_index(), 1);
745
746 assert_eq!(MacroRxChannels::STATIC_CHANNELS.len(), 2);
747 assert_eq!(
748 MacroRxChannels::STATUS_UPDATES.default_route,
749 Some("sys/status")
750 );
751 assert!(MacroRxChannels::ALERT_STREAM.default_route.is_none());
752 assert_eq!(MacroRxId::StatusUpdates.as_index(), 0);
753 assert_eq!(MacroRxId::AlertStream.as_index(), 1);
754 }
755
756 #[test]
757 fn bridge_trait_compiles_and_accesses_configs() {
758 let mut bridge_cfg = ComponentConfig::default();
759 bridge_cfg.set("port", "ttyUSB0".to_string());
760
761 let tx_descriptors = [
762 BridgeChannelConfig::from_static(&TxChannels::IMU, None, None),
763 BridgeChannelConfig::from_static(&TxChannels::MOTOR, None, None),
764 ];
765 let rx_descriptors = [
766 BridgeChannelConfig::from_static(&RxChannels::STATUS, None, None),
767 BridgeChannelConfig::from_static(&RxChannels::ALERT, None, None),
768 ];
769
770 assert_eq!(
771 tx_descriptors[0]
772 .effective_route()
773 .map(|route| route.into_owned()),
774 Some("telemetry/imu".to_string())
775 );
776 assert_eq!(
777 tx_descriptors[1]
778 .effective_route()
779 .map(|route| route.into_owned()),
780 Some("motor/cmd".to_string())
781 );
782 assert_eq!(
783 tx_descriptors[0].channel.tx_empty_policy,
784 TxEmptyPolicy::Skip
785 );
786 assert_eq!(
787 tx_descriptors[1].channel.tx_empty_policy,
788 TxEmptyPolicy::Publish
789 );
790 let overridden = BridgeChannelConfig::from_static(
791 &TxChannels::MOTOR,
792 Some("custom/motor".to_string()),
793 None,
794 );
795 assert_eq!(
796 overridden.effective_route().map(|route| route.into_owned()),
797 Some("custom/motor".to_string())
798 );
799
800 let mut bridge =
801 ExampleBridge::new(Some(&bridge_cfg), &tx_descriptors, &rx_descriptors, ())
802 .expect("bridge should build");
803
804 assert_eq!(bridge.port, "ttyUSB0");
805
806 let context = CuContext::new_with_clock();
807 let imu_msg = CuMsg::new(Some(ImuMsg { accel: 7 }));
808 bridge
809 .send(&context, &TxChannels::IMU, &imu_msg)
810 .expect("send should succeed");
811 let motor_msg = CuMsg::new(Some(MotorCmd { torque: -3 }));
812 bridge
813 .send(&context, &TxChannels::MOTOR, &motor_msg)
814 .expect("send should support multiple payload types");
815 assert_eq!(bridge.imu_samples, vec![7]);
816 assert_eq!(bridge.motor_torques, vec![-3]);
817
818 let mut status_msg = CuMsg::new(None);
819 bridge
820 .receive(&context, &RxChannels::STATUS, &mut status_msg)
821 .expect("receive should succeed");
822 assert!(status_msg.payload().is_some());
823 assert_eq!(bridge.status_temps, vec![21.5]);
824
825 let mut alert_msg = CuMsg::new(None);
826 bridge
827 .receive(&context, &RxChannels::ALERT, &mut alert_msg)
828 .expect("receive should handle other payload types too");
829 assert!(alert_msg.payload().is_some());
830 assert_eq!(bridge.alert_codes, vec![0xDEAD_BEEF]);
831 }
832
833 #[test]
834 fn bridge_channel_should_send_respects_empty_policy() {
835 let default_channel = BridgeChannel::<TxId, MotorCmd>::new(TxId::Motor);
836 assert!(!default_channel.should_send(false));
837 assert!(default_channel.should_send(true));
838
839 let publish_empty_channel =
840 BridgeChannel::<TxId, MotorCmd>::new(TxId::Motor).publish_empty();
841 assert!(publish_empty_channel.should_send(false));
842 assert!(publish_empty_channel.should_send(true));
843 }
844}