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)]
21pub struct BridgeChannel<Id, Payload> {
22 pub id: Id,
24 pub default_route: Option<&'static str>,
26 _payload: PhantomData<fn() -> Payload>,
27}
28
29impl<Id, Payload> BridgeChannel<Id, Payload> {
30 pub const fn new(id: Id) -> Self {
32 Self {
33 id,
34 default_route: None,
35 _payload: PhantomData,
36 }
37 }
38
39 pub const fn with_channel(id: Id, route: &'static str) -> Self {
41 Self {
42 id,
43 default_route: Some(route),
44 _payload: PhantomData,
45 }
46 }
47}
48
49impl<Id: Debug, Payload> Debug for BridgeChannel<Id, Payload> {
50 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
51 f.debug_struct("BridgeChannel")
52 .field("id", &self.id)
53 .field("default_route", &self.default_route)
54 .finish()
55 }
56}
57
58pub trait BridgeChannelInfo<Id: Copy> {
60 fn id(&self) -> Id;
62 fn default_route(&self) -> Option<&'static str>;
64}
65
66impl<Id: Copy, Payload> BridgeChannelInfo<Id> for BridgeChannel<Id, Payload> {
67 fn id(&self) -> Id {
68 self.id
69 }
70
71 fn default_route(&self) -> Option<&'static str> {
72 self.default_route
73 }
74}
75
76#[derive(Copy, Clone, Debug)]
79pub struct BridgeChannelDescriptor<Id: Copy> {
80 pub id: Id,
82 pub default_route: Option<&'static str>,
84}
85
86impl<Id: Copy> BridgeChannelDescriptor<Id> {
87 pub const fn new(id: Id, default_route: Option<&'static str>) -> Self {
88 Self { id, default_route }
89 }
90}
91
92impl<Id: Copy, T> From<&T> for BridgeChannelDescriptor<Id>
93where
94 T: BridgeChannelInfo<Id> + ?Sized,
95{
96 fn from(channel: &T) -> Self {
97 BridgeChannelDescriptor::new(channel.id(), channel.default_route())
98 }
99}
100
101#[derive(Clone, Debug)]
103pub struct BridgeChannelConfig<Id: Copy> {
104 pub channel: BridgeChannelDescriptor<Id>,
106 pub route: Option<String>,
108 pub config: Option<ComponentConfig>,
110}
111
112impl<Id: Copy> BridgeChannelConfig<Id> {
113 pub fn from_static<T>(
115 channel: &'static T,
116 route: Option<String>,
117 config: Option<ComponentConfig>,
118 ) -> Self
119 where
120 T: BridgeChannelInfo<Id> + ?Sized,
121 {
122 Self {
123 channel: channel.into(),
124 route,
125 config,
126 }
127 }
128
129 pub fn effective_route(&self) -> Option<Cow<'_, str>> {
131 if let Some(route) = &self.route {
132 Some(Cow::Borrowed(route.as_str()))
133 } else {
134 self.channel.default_route.map(Cow::Borrowed)
135 }
136 }
137}
138
139pub trait BridgeChannelSet {
145 type Id: Copy + Eq + 'static;
147
148 const STATIC_CHANNELS: &'static [&'static dyn BridgeChannelInfo<Self::Id>];
150}
151
152pub trait CuBridge: Freezable + Reflect {
158 type Tx: BridgeChannelSet;
160 type Rx: BridgeChannelSet;
162 type Resources<'r>;
164
165 fn new(
170 config: Option<&ComponentConfig>,
171 tx_channels: &[BridgeChannelConfig<<Self::Tx as BridgeChannelSet>::Id>],
172 rx_channels: &[BridgeChannelConfig<<Self::Rx as BridgeChannelSet>::Id>],
173 resources: Self::Resources<'_>,
174 ) -> CuResult<Self>
175 where
176 Self: Sized;
177
178 fn start(&mut self, _ctx: &CuContext) -> CuResult<()> {
180 Ok(())
181 }
182
183 fn preprocess(&mut self, _ctx: &CuContext) -> CuResult<()> {
185 Ok(())
186 }
187
188 fn send<'a, Payload>(
190 &mut self,
191 ctx: &CuContext,
192 channel: &'static BridgeChannel<<Self::Tx as BridgeChannelSet>::Id, Payload>,
193 msg: &CuMsg<Payload>,
194 ) -> CuResult<()>
195 where
196 Payload: CuMsgPayload + 'a;
197
198 fn receive<'a, Payload>(
202 &mut self,
203 ctx: &CuContext,
204 channel: &'static BridgeChannel<<Self::Rx as BridgeChannelSet>::Id, Payload>,
205 msg: &mut CuMsg<Payload>,
206 ) -> CuResult<()>
207 where
208 Payload: CuMsgPayload + 'a;
209
210 fn postprocess(&mut self, _ctx: &CuContext) -> CuResult<()> {
212 Ok(())
213 }
214
215 fn stop(&mut self, _ctx: &CuContext) -> CuResult<()> {
217 Ok(())
218 }
219}
220
221#[doc(hidden)]
222#[macro_export]
223macro_rules! __cu29_bridge_channel_ctor {
224 ($id:ident, $variant:ident, $payload:ty) => {
225 $crate::cubridge::BridgeChannel::<$id, $payload>::new($id::$variant)
226 };
227 ($id:ident, $variant:ident, $payload:ty, $route:expr) => {
228 $crate::cubridge::BridgeChannel::<$id, $payload>::with_channel($id::$variant, $route)
229 };
230}
231
232#[doc(hidden)]
233#[macro_export]
234macro_rules! __cu29_define_bridge_channels {
235 (
236 @accum
237 $vis:vis struct $channels:ident : $id:ident
238 [ $($parsed:tt)+ ]
239 ) => {
240 $crate::__cu29_emit_bridge_channels! {
241 $vis struct $channels : $id { $($parsed)+ }
242 }
243 };
244 (
245 @accum
246 $vis:vis struct $channels:ident : $id:ident
247 [ ]
248 ) => {
249 compile_error!("tx_channels!/rx_channels! require at least one channel");
250 };
251 (
252 @accum
253 $vis:vis struct $channels:ident : $id:ident
254 [ $($parsed:tt)* ]
255 $(#[$chan_meta:meta])* $const_name:ident : $variant:ident => $payload:ty $(= $route:expr)? , $($rest:tt)*
256 ) => {
257 $crate::__cu29_define_bridge_channels!(
258 @accum
259 $vis struct $channels : $id
260 [
261 $($parsed)*
262 $(#[$chan_meta])* $const_name : $variant => $payload $(= $route)?,
263 ]
264 $($rest)*
265 );
266 };
267 (
268 @accum
269 $vis:vis struct $channels:ident : $id:ident
270 [ $($parsed:tt)* ]
271 $(#[$chan_meta:meta])* $const_name:ident : $variant:ident => $payload:ty $(= $route:expr)?
272 ) => {
273 $crate::__cu29_define_bridge_channels!(
274 @accum
275 $vis struct $channels : $id
276 [
277 $($parsed)*
278 $(#[$chan_meta])* $const_name : $variant => $payload $(= $route)?,
279 ]
280 );
281 };
282 (
283 @accum
284 $vis:vis struct $channels:ident : $id:ident
285 [ $($parsed:tt)* ]
286 $(#[$chan_meta:meta])* $name:ident => $payload:ty $(= $route:expr)? , $($rest:tt)*
287 ) => {
288 $crate::__cu29_paste! {
289 $crate::__cu29_define_bridge_channels!(
290 @accum
291 $vis struct $channels : $id
292 [
293 $($parsed)*
294 $(#[$chan_meta])* [<$name:snake:upper>] : [<$name:camel>] => $payload $(= $route)?,
295 ]
296 $($rest)*
297 );
298 }
299 };
300 (
301 @accum
302 $vis:vis struct $channels:ident : $id:ident
303 [ $($parsed:tt)* ]
304 $(#[$chan_meta:meta])* $name:ident => $payload:ty $(= $route:expr)?
305 ) => {
306 $crate::__cu29_paste! {
307 $crate::__cu29_define_bridge_channels!(
308 @accum
309 $vis struct $channels : $id
310 [
311 $($parsed)*
312 $(#[$chan_meta])* [<$name:snake:upper>] : [<$name:camel>] => $payload $(= $route)?,
313 ]
314 );
315 }
316 };
317 (
318 $vis:vis struct $channels:ident : $id:ident {
319 $($body:tt)*
320 }
321 ) => {
322 $crate::__cu29_define_bridge_channels!(
323 @accum
324 $vis struct $channels : $id
325 []
326 $($body)*
327 );
328 };
329}
330
331#[doc(hidden)]
332#[macro_export]
333macro_rules! __cu29_emit_bridge_channels {
334 (
335 $vis:vis struct $channels:ident : $id:ident {
336 $(
337 $(#[$chan_meta:meta])*
338 $const_name:ident : $variant:ident => $payload:ty $(= $route:expr)?,
339 )+
340 }
341 ) => {
342 #[derive(Copy, Clone, Debug, Eq, PartialEq, ::serde::Serialize, ::serde::Deserialize)]
343 #[repr(usize)]
344 #[serde(rename_all = "snake_case")]
345 $vis enum $id {
346 $(
347 $variant,
348 )+
349 }
350
351 impl $id {
352 pub const fn as_index(self) -> usize {
354 self as usize
355 }
356 }
357
358 $vis struct $channels;
359
360 #[allow(non_upper_case_globals)]
361 impl $channels {
362 $(
363 $(#[$chan_meta])*
364 $vis const $const_name: $crate::cubridge::BridgeChannel<$id, $payload> =
365 $crate::__cu29_bridge_channel_ctor!(
366 $id, $variant, $payload $(, $route)?
367 );
368 )+
369 }
370
371 impl $crate::cubridge::BridgeChannelSet for $channels {
372 type Id = $id;
373
374 const STATIC_CHANNELS: &'static [&'static dyn $crate::cubridge::BridgeChannelInfo<Self::Id>] =
375 &[
376 $(
377 &Self::$const_name,
378 )+
379 ];
380 }
381 };
382}
383
384#[macro_export]
411macro_rules! tx_channels {
412 (
413 $vis:vis struct $channels:ident : $id:ident {
414 $(
415 $(#[$chan_meta:meta])* $entry:tt => $payload:ty $(= $route:expr)?
416 ),+ $(,)?
417 }
418 ) => {
419 $crate::__cu29_define_bridge_channels! {
420 $vis struct $channels : $id {
421 $(
422 $(#[$chan_meta])* $entry => $payload $(= $route)?,
423 )+
424 }
425 }
426 };
427 ({ $($rest:tt)* }) => {
428 $crate::tx_channels! {
429 pub struct TxChannels : TxId { $($rest)* }
430 }
431 };
432 ($($rest:tt)+) => {
433 $crate::tx_channels!({ $($rest)+ });
434 };
435}
436
437#[macro_export]
441macro_rules! rx_channels {
442 (
443 $vis:vis struct $channels:ident : $id:ident {
444 $(
445 $(#[$chan_meta:meta])* $entry:tt => $payload:ty $(= $route:expr)?
446 ),+ $(,)?
447 }
448 ) => {
449 $crate::__cu29_define_bridge_channels! {
450 $vis struct $channels : $id {
451 $(
452 $(#[$chan_meta])* $entry => $payload $(= $route)?,
453 )+
454 }
455 }
456 };
457 ({ $($rest:tt)* }) => {
458 $crate::rx_channels! {
459 pub struct RxChannels : RxId { $($rest)* }
460 }
461 };
462 ($($rest:tt)+) => {
463 $crate::rx_channels!({ $($rest)+ });
464 };
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470 use crate::config::ComponentConfig;
471 use crate::context::CuContext;
472 use crate::cutask::CuMsg;
473 use alloc::vec::Vec;
474 use cu29_traits::CuError;
475 use serde::{Deserialize, Serialize};
476
477 #[derive(
479 Clone, Debug, Default, Serialize, Deserialize, bincode::Encode, bincode::Decode, Reflect,
480 )]
481 struct ImuMsg {
482 accel: i32,
483 }
484
485 #[derive(
486 Clone, Debug, Default, Serialize, Deserialize, bincode::Encode, bincode::Decode, Reflect,
487 )]
488 struct MotorCmd {
489 torque: i16,
490 }
491
492 #[derive(
493 Clone, Debug, Default, Serialize, Deserialize, bincode::Encode, bincode::Decode, Reflect,
494 )]
495 struct StatusMsg {
496 temperature: f32,
497 }
498
499 #[derive(
500 Clone, Debug, Default, Serialize, Deserialize, bincode::Encode, bincode::Decode, Reflect,
501 )]
502 struct AlertMsg {
503 code: u32,
504 }
505
506 tx_channels! {
507 struct MacroTxChannels : MacroTxId {
508 imu_stream => ImuMsg = "telemetry/imu",
509 motor_stream => MotorCmd,
510 }
511 }
512
513 rx_channels! {
514 struct MacroRxChannels : MacroRxId {
515 status_updates => StatusMsg = "sys/status",
516 alert_stream => AlertMsg,
517 }
518 }
519
520 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
522 enum TxId {
523 Imu,
524 Motor,
525 }
526
527 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
528 enum RxId {
529 Status,
530 Alert,
531 }
532
533 struct TxChannels;
535
536 impl TxChannels {
537 pub const IMU: BridgeChannel<TxId, ImuMsg> =
538 BridgeChannel::with_channel(TxId::Imu, "telemetry/imu");
539 pub const MOTOR: BridgeChannel<TxId, MotorCmd> =
540 BridgeChannel::with_channel(TxId::Motor, "motor/cmd");
541 }
542
543 impl BridgeChannelSet for TxChannels {
544 type Id = TxId;
545
546 const STATIC_CHANNELS: &'static [&'static dyn BridgeChannelInfo<Self::Id>] =
547 &[&Self::IMU, &Self::MOTOR];
548 }
549
550 struct RxChannels;
551
552 impl RxChannels {
553 pub const STATUS: BridgeChannel<RxId, StatusMsg> =
554 BridgeChannel::with_channel(RxId::Status, "sys/status");
555 pub const ALERT: BridgeChannel<RxId, AlertMsg> =
556 BridgeChannel::with_channel(RxId::Alert, "sys/alert");
557 }
558
559 impl BridgeChannelSet for RxChannels {
560 type Id = RxId;
561
562 const STATIC_CHANNELS: &'static [&'static dyn BridgeChannelInfo<Self::Id>] =
563 &[&Self::STATUS, &Self::ALERT];
564 }
565
566 #[derive(Default, Reflect)]
568 struct ExampleBridge {
569 port: String,
570 imu_samples: Vec<i32>,
571 motor_torques: Vec<i16>,
572 status_temps: Vec<f32>,
573 alert_codes: Vec<u32>,
574 }
575
576 impl Freezable for ExampleBridge {}
577
578 impl CuBridge for ExampleBridge {
579 type Resources<'r> = ();
580 type Tx = TxChannels;
581 type Rx = RxChannels;
582
583 fn new(
584 config: Option<&ComponentConfig>,
585 _tx_channels: &[BridgeChannelConfig<TxId>],
586 _rx_channels: &[BridgeChannelConfig<RxId>],
587 _resources: Self::Resources<'_>,
588 ) -> CuResult<Self> {
589 let mut instance = ExampleBridge::default();
590 if let Some(cfg) = config
591 && let Some(port) = cfg.get::<String>("port")?
592 {
593 instance.port = port;
594 }
595 Ok(instance)
596 }
597
598 fn send<'a, Payload>(
599 &mut self,
600 _ctx: &CuContext,
601 channel: &'static BridgeChannel<TxId, Payload>,
602 msg: &CuMsg<Payload>,
603 ) -> CuResult<()>
604 where
605 Payload: CuMsgPayload + 'a,
606 {
607 match channel.id {
608 TxId::Imu => {
609 let imu_msg = msg.downcast_ref::<ImuMsg>()?;
610 let payload = imu_msg
611 .payload()
612 .ok_or_else(|| CuError::from("imu missing payload"))?;
613 self.imu_samples.push(payload.accel);
614 Ok(())
615 }
616 TxId::Motor => {
617 let motor_msg = msg.downcast_ref::<MotorCmd>()?;
618 let payload = motor_msg
619 .payload()
620 .ok_or_else(|| CuError::from("motor missing payload"))?;
621 self.motor_torques.push(payload.torque);
622 Ok(())
623 }
624 }
625 }
626
627 fn receive<'a, Payload>(
628 &mut self,
629 _ctx: &CuContext,
630 channel: &'static BridgeChannel<RxId, Payload>,
631 msg: &mut CuMsg<Payload>,
632 ) -> CuResult<()>
633 where
634 Payload: CuMsgPayload + 'a,
635 {
636 match channel.id {
637 RxId::Status => {
638 let status_msg = msg.downcast_mut::<StatusMsg>()?;
639 status_msg.set_payload(StatusMsg { temperature: 21.5 });
640 if let Some(payload) = status_msg.payload() {
641 self.status_temps.push(payload.temperature);
642 }
643 Ok(())
644 }
645 RxId::Alert => {
646 let alert_msg = msg.downcast_mut::<AlertMsg>()?;
647 alert_msg.set_payload(AlertMsg { code: 0xDEAD_BEEF });
648 if let Some(payload) = alert_msg.payload() {
649 self.alert_codes.push(payload.code);
650 }
651 Ok(())
652 }
653 }
654 }
655 }
656
657 #[test]
658 fn channel_macros_expose_static_metadata() {
659 assert_eq!(MacroTxChannels::STATIC_CHANNELS.len(), 2);
660 assert_eq!(
661 MacroTxChannels::IMU_STREAM.default_route,
662 Some("telemetry/imu")
663 );
664 assert!(MacroTxChannels::MOTOR_STREAM.default_route.is_none());
665 assert_eq!(MacroTxId::ImuStream as u8, MacroTxId::ImuStream as u8);
666 assert_eq!(MacroTxId::ImuStream.as_index(), 0);
667 assert_eq!(MacroTxId::MotorStream.as_index(), 1);
668
669 assert_eq!(MacroRxChannels::STATIC_CHANNELS.len(), 2);
670 assert_eq!(
671 MacroRxChannels::STATUS_UPDATES.default_route,
672 Some("sys/status")
673 );
674 assert!(MacroRxChannels::ALERT_STREAM.default_route.is_none());
675 assert_eq!(MacroRxId::StatusUpdates.as_index(), 0);
676 assert_eq!(MacroRxId::AlertStream.as_index(), 1);
677 }
678
679 #[test]
680 fn bridge_trait_compiles_and_accesses_configs() {
681 let mut bridge_cfg = ComponentConfig::default();
682 bridge_cfg.set("port", "ttyUSB0".to_string());
683
684 let tx_descriptors = [
685 BridgeChannelConfig::from_static(&TxChannels::IMU, None, None),
686 BridgeChannelConfig::from_static(&TxChannels::MOTOR, None, None),
687 ];
688 let rx_descriptors = [
689 BridgeChannelConfig::from_static(&RxChannels::STATUS, None, None),
690 BridgeChannelConfig::from_static(&RxChannels::ALERT, None, None),
691 ];
692
693 assert_eq!(
694 tx_descriptors[0]
695 .effective_route()
696 .map(|route| route.into_owned()),
697 Some("telemetry/imu".to_string())
698 );
699 assert_eq!(
700 tx_descriptors[1]
701 .effective_route()
702 .map(|route| route.into_owned()),
703 Some("motor/cmd".to_string())
704 );
705 let overridden = BridgeChannelConfig::from_static(
706 &TxChannels::MOTOR,
707 Some("custom/motor".to_string()),
708 None,
709 );
710 assert_eq!(
711 overridden.effective_route().map(|route| route.into_owned()),
712 Some("custom/motor".to_string())
713 );
714
715 let mut bridge =
716 ExampleBridge::new(Some(&bridge_cfg), &tx_descriptors, &rx_descriptors, ())
717 .expect("bridge should build");
718
719 assert_eq!(bridge.port, "ttyUSB0");
720
721 let context = CuContext::new_with_clock();
722 let imu_msg = CuMsg::new(Some(ImuMsg { accel: 7 }));
723 bridge
724 .send(&context, &TxChannels::IMU, &imu_msg)
725 .expect("send should succeed");
726 let motor_msg = CuMsg::new(Some(MotorCmd { torque: -3 }));
727 bridge
728 .send(&context, &TxChannels::MOTOR, &motor_msg)
729 .expect("send should support multiple payload types");
730 assert_eq!(bridge.imu_samples, vec![7]);
731 assert_eq!(bridge.motor_torques, vec![-3]);
732
733 let mut status_msg = CuMsg::new(None);
734 bridge
735 .receive(&context, &RxChannels::STATUS, &mut status_msg)
736 .expect("receive should succeed");
737 assert!(status_msg.payload().is_some());
738 assert_eq!(bridge.status_temps, vec![21.5]);
739
740 let mut alert_msg = CuMsg::new(None);
741 bridge
742 .receive(&context, &RxChannels::ALERT, &mut alert_msg)
743 .expect("receive should handle other payload types too");
744 assert!(alert_msg.payload().is_some());
745 assert_eq!(bridge.alert_codes, vec![0xDEAD_BEEF]);
746 }
747}