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
625 impl CuBridge for ExampleBridge {
626 type Resources<'r> = ();
627 type Tx = TxChannels;
628 type Rx = RxChannels;
629
630 fn new(
631 config: Option<&ComponentConfig>,
632 _tx_channels: &[BridgeChannelConfig<TxId>],
633 _rx_channels: &[BridgeChannelConfig<RxId>],
634 _resources: Self::Resources<'_>,
635 ) -> CuResult<Self> {
636 let mut instance = ExampleBridge::default();
637 if let Some(cfg) = config
638 && let Some(port) = cfg.get::<String>("port")?
639 {
640 instance.port = port;
641 }
642 Ok(instance)
643 }
644
645 fn send<'a, Payload>(
646 &mut self,
647 _ctx: &CuContext,
648 channel: &'static BridgeChannel<TxId, Payload>,
649 msg: &CuMsg<Payload>,
650 ) -> CuResult<()>
651 where
652 Payload: CuMsgPayload + 'a,
653 {
654 match channel.id {
655 TxId::Imu => {
656 let imu_msg = msg.downcast_ref::<ImuMsg>()?;
657 let payload = imu_msg
658 .payload()
659 .ok_or_else(|| CuError::from("imu missing payload"))?;
660 self.imu_samples.push(payload.accel);
661 Ok(())
662 }
663 TxId::Motor => {
664 let motor_msg = msg.downcast_ref::<MotorCmd>()?;
665 let payload = motor_msg
666 .payload()
667 .ok_or_else(|| CuError::from("motor missing payload"))?;
668 self.motor_torques.push(payload.torque);
669 Ok(())
670 }
671 }
672 }
673
674 fn receive<'a, Payload>(
675 &mut self,
676 _ctx: &CuContext,
677 channel: &'static BridgeChannel<RxId, Payload>,
678 msg: &mut CuMsg<Payload>,
679 ) -> CuResult<()>
680 where
681 Payload: CuMsgPayload + 'a,
682 {
683 match channel.id {
684 RxId::Status => {
685 let status_msg = msg.downcast_mut::<StatusMsg>()?;
686 status_msg.set_payload(StatusMsg { temperature: 21.5 });
687 if let Some(payload) = status_msg.payload() {
688 self.status_temps.push(payload.temperature);
689 }
690 Ok(())
691 }
692 RxId::Alert => {
693 let alert_msg = msg.downcast_mut::<AlertMsg>()?;
694 alert_msg.set_payload(AlertMsg { code: 0xDEAD_BEEF });
695 if let Some(payload) = alert_msg.payload() {
696 self.alert_codes.push(payload.code);
697 }
698 Ok(())
699 }
700 }
701 }
702 }
703
704 #[test]
705 fn channel_macros_expose_static_metadata() {
706 assert_eq!(MacroTxChannels::STATIC_CHANNELS.len(), 2);
707 assert_eq!(
708 MacroTxChannels::IMU_STREAM.default_route,
709 Some("telemetry/imu")
710 );
711 assert!(MacroTxChannels::MOTOR_STREAM.default_route.is_none());
712 assert_eq!(
713 MacroTxChannels::IMU_STREAM.tx_empty_policy,
714 TxEmptyPolicy::Skip
715 );
716 assert_eq!(
717 MacroTxChannels::MOTOR_STREAM.tx_empty_policy,
718 TxEmptyPolicy::Publish
719 );
720 assert_eq!(MacroTxId::ImuStream as u8, MacroTxId::ImuStream as u8);
721 assert_eq!(MacroTxId::ImuStream.as_index(), 0);
722 assert_eq!(MacroTxId::MotorStream.as_index(), 1);
723
724 assert_eq!(MacroRxChannels::STATIC_CHANNELS.len(), 2);
725 assert_eq!(
726 MacroRxChannels::STATUS_UPDATES.default_route,
727 Some("sys/status")
728 );
729 assert!(MacroRxChannels::ALERT_STREAM.default_route.is_none());
730 assert_eq!(MacroRxId::StatusUpdates.as_index(), 0);
731 assert_eq!(MacroRxId::AlertStream.as_index(), 1);
732 }
733
734 #[test]
735 fn bridge_trait_compiles_and_accesses_configs() {
736 let mut bridge_cfg = ComponentConfig::default();
737 bridge_cfg.set("port", "ttyUSB0".to_string());
738
739 let tx_descriptors = [
740 BridgeChannelConfig::from_static(&TxChannels::IMU, None, None),
741 BridgeChannelConfig::from_static(&TxChannels::MOTOR, None, None),
742 ];
743 let rx_descriptors = [
744 BridgeChannelConfig::from_static(&RxChannels::STATUS, None, None),
745 BridgeChannelConfig::from_static(&RxChannels::ALERT, None, None),
746 ];
747
748 assert_eq!(
749 tx_descriptors[0]
750 .effective_route()
751 .map(|route| route.into_owned()),
752 Some("telemetry/imu".to_string())
753 );
754 assert_eq!(
755 tx_descriptors[1]
756 .effective_route()
757 .map(|route| route.into_owned()),
758 Some("motor/cmd".to_string())
759 );
760 assert_eq!(
761 tx_descriptors[0].channel.tx_empty_policy,
762 TxEmptyPolicy::Skip
763 );
764 assert_eq!(
765 tx_descriptors[1].channel.tx_empty_policy,
766 TxEmptyPolicy::Publish
767 );
768 let overridden = BridgeChannelConfig::from_static(
769 &TxChannels::MOTOR,
770 Some("custom/motor".to_string()),
771 None,
772 );
773 assert_eq!(
774 overridden.effective_route().map(|route| route.into_owned()),
775 Some("custom/motor".to_string())
776 );
777
778 let mut bridge =
779 ExampleBridge::new(Some(&bridge_cfg), &tx_descriptors, &rx_descriptors, ())
780 .expect("bridge should build");
781
782 assert_eq!(bridge.port, "ttyUSB0");
783
784 let context = CuContext::new_with_clock();
785 let imu_msg = CuMsg::new(Some(ImuMsg { accel: 7 }));
786 bridge
787 .send(&context, &TxChannels::IMU, &imu_msg)
788 .expect("send should succeed");
789 let motor_msg = CuMsg::new(Some(MotorCmd { torque: -3 }));
790 bridge
791 .send(&context, &TxChannels::MOTOR, &motor_msg)
792 .expect("send should support multiple payload types");
793 assert_eq!(bridge.imu_samples, vec![7]);
794 assert_eq!(bridge.motor_torques, vec![-3]);
795
796 let mut status_msg = CuMsg::new(None);
797 bridge
798 .receive(&context, &RxChannels::STATUS, &mut status_msg)
799 .expect("receive should succeed");
800 assert!(status_msg.payload().is_some());
801 assert_eq!(bridge.status_temps, vec![21.5]);
802
803 let mut alert_msg = CuMsg::new(None);
804 bridge
805 .receive(&context, &RxChannels::ALERT, &mut alert_msg)
806 .expect("receive should handle other payload types too");
807 assert!(alert_msg.payload().is_some());
808 assert_eq!(bridge.alert_codes, vec![0xDEAD_BEEF]);
809 }
810
811 #[test]
812 fn bridge_channel_should_send_respects_empty_policy() {
813 let default_channel = BridgeChannel::<TxId, MotorCmd>::new(TxId::Motor);
814 assert!(!default_channel.should_send(false));
815 assert!(default_channel.should_send(true));
816
817 let publish_empty_channel =
818 BridgeChannel::<TxId, MotorCmd>::new(TxId::Motor).publish_empty();
819 assert!(publish_empty_channel.should_send(false));
820 assert!(publish_empty_channel.should_send(true));
821 }
822}