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(Clone, Debug, Default, Serialize, Deserialize, bincode::Encode, bincode::Decode)]
479 struct ImuMsg {
480 accel: i32,
481 }
482
483 #[derive(Clone, Debug, Default, Serialize, Deserialize, bincode::Encode, bincode::Decode)]
484 struct MotorCmd {
485 torque: i16,
486 }
487
488 #[derive(Clone, Debug, Default, Serialize, Deserialize, bincode::Encode, bincode::Decode)]
489 struct StatusMsg {
490 temperature: f32,
491 }
492
493 #[derive(Clone, Debug, Default, Serialize, Deserialize, bincode::Encode, bincode::Decode)]
494 struct AlertMsg {
495 code: u32,
496 }
497
498 tx_channels! {
499 struct MacroTxChannels : MacroTxId {
500 imu_stream => ImuMsg = "telemetry/imu",
501 motor_stream => MotorCmd,
502 }
503 }
504
505 rx_channels! {
506 struct MacroRxChannels : MacroRxId {
507 status_updates => StatusMsg = "sys/status",
508 alert_stream => AlertMsg,
509 }
510 }
511
512 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
514 enum TxId {
515 Imu,
516 Motor,
517 }
518
519 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
520 enum RxId {
521 Status,
522 Alert,
523 }
524
525 struct TxChannels;
527
528 impl TxChannels {
529 pub const IMU: BridgeChannel<TxId, ImuMsg> =
530 BridgeChannel::with_channel(TxId::Imu, "telemetry/imu");
531 pub const MOTOR: BridgeChannel<TxId, MotorCmd> =
532 BridgeChannel::with_channel(TxId::Motor, "motor/cmd");
533 }
534
535 impl BridgeChannelSet for TxChannels {
536 type Id = TxId;
537
538 const STATIC_CHANNELS: &'static [&'static dyn BridgeChannelInfo<Self::Id>] =
539 &[&Self::IMU, &Self::MOTOR];
540 }
541
542 struct RxChannels;
543
544 impl RxChannels {
545 pub const STATUS: BridgeChannel<RxId, StatusMsg> =
546 BridgeChannel::with_channel(RxId::Status, "sys/status");
547 pub const ALERT: BridgeChannel<RxId, AlertMsg> =
548 BridgeChannel::with_channel(RxId::Alert, "sys/alert");
549 }
550
551 impl BridgeChannelSet for RxChannels {
552 type Id = RxId;
553
554 const STATIC_CHANNELS: &'static [&'static dyn BridgeChannelInfo<Self::Id>] =
555 &[&Self::STATUS, &Self::ALERT];
556 }
557
558 #[derive(Default, Reflect)]
560 struct ExampleBridge {
561 port: String,
562 imu_samples: Vec<i32>,
563 motor_torques: Vec<i16>,
564 status_temps: Vec<f32>,
565 alert_codes: Vec<u32>,
566 }
567
568 impl Freezable for ExampleBridge {}
569
570 impl CuBridge for ExampleBridge {
571 type Resources<'r> = ();
572 type Tx = TxChannels;
573 type Rx = RxChannels;
574
575 fn new(
576 config: Option<&ComponentConfig>,
577 _tx_channels: &[BridgeChannelConfig<TxId>],
578 _rx_channels: &[BridgeChannelConfig<RxId>],
579 _resources: Self::Resources<'_>,
580 ) -> CuResult<Self> {
581 let mut instance = ExampleBridge::default();
582 if let Some(cfg) = config
583 && let Some(port) = cfg.get::<String>("port")?
584 {
585 instance.port = port;
586 }
587 Ok(instance)
588 }
589
590 fn send<'a, Payload>(
591 &mut self,
592 _ctx: &CuContext,
593 channel: &'static BridgeChannel<TxId, Payload>,
594 msg: &CuMsg<Payload>,
595 ) -> CuResult<()>
596 where
597 Payload: CuMsgPayload + 'a,
598 {
599 match channel.id {
600 TxId::Imu => {
601 let imu_msg = msg.downcast_ref::<ImuMsg>()?;
602 let payload = imu_msg
603 .payload()
604 .ok_or_else(|| CuError::from("imu missing payload"))?;
605 self.imu_samples.push(payload.accel);
606 Ok(())
607 }
608 TxId::Motor => {
609 let motor_msg = msg.downcast_ref::<MotorCmd>()?;
610 let payload = motor_msg
611 .payload()
612 .ok_or_else(|| CuError::from("motor missing payload"))?;
613 self.motor_torques.push(payload.torque);
614 Ok(())
615 }
616 }
617 }
618
619 fn receive<'a, Payload>(
620 &mut self,
621 _ctx: &CuContext,
622 channel: &'static BridgeChannel<RxId, Payload>,
623 msg: &mut CuMsg<Payload>,
624 ) -> CuResult<()>
625 where
626 Payload: CuMsgPayload + 'a,
627 {
628 match channel.id {
629 RxId::Status => {
630 let status_msg = msg.downcast_mut::<StatusMsg>()?;
631 status_msg.set_payload(StatusMsg { temperature: 21.5 });
632 if let Some(payload) = status_msg.payload() {
633 self.status_temps.push(payload.temperature);
634 }
635 Ok(())
636 }
637 RxId::Alert => {
638 let alert_msg = msg.downcast_mut::<AlertMsg>()?;
639 alert_msg.set_payload(AlertMsg { code: 0xDEAD_BEEF });
640 if let Some(payload) = alert_msg.payload() {
641 self.alert_codes.push(payload.code);
642 }
643 Ok(())
644 }
645 }
646 }
647 }
648
649 #[test]
650 fn channel_macros_expose_static_metadata() {
651 assert_eq!(MacroTxChannels::STATIC_CHANNELS.len(), 2);
652 assert_eq!(
653 MacroTxChannels::IMU_STREAM.default_route,
654 Some("telemetry/imu")
655 );
656 assert!(MacroTxChannels::MOTOR_STREAM.default_route.is_none());
657 assert_eq!(MacroTxId::ImuStream as u8, MacroTxId::ImuStream as u8);
658 assert_eq!(MacroTxId::ImuStream.as_index(), 0);
659 assert_eq!(MacroTxId::MotorStream.as_index(), 1);
660
661 assert_eq!(MacroRxChannels::STATIC_CHANNELS.len(), 2);
662 assert_eq!(
663 MacroRxChannels::STATUS_UPDATES.default_route,
664 Some("sys/status")
665 );
666 assert!(MacroRxChannels::ALERT_STREAM.default_route.is_none());
667 assert_eq!(MacroRxId::StatusUpdates.as_index(), 0);
668 assert_eq!(MacroRxId::AlertStream.as_index(), 1);
669 }
670
671 #[test]
672 fn bridge_trait_compiles_and_accesses_configs() {
673 let mut bridge_cfg = ComponentConfig::default();
674 bridge_cfg.set("port", "ttyUSB0".to_string());
675
676 let tx_descriptors = [
677 BridgeChannelConfig::from_static(&TxChannels::IMU, None, None),
678 BridgeChannelConfig::from_static(&TxChannels::MOTOR, None, None),
679 ];
680 let rx_descriptors = [
681 BridgeChannelConfig::from_static(&RxChannels::STATUS, None, None),
682 BridgeChannelConfig::from_static(&RxChannels::ALERT, None, None),
683 ];
684
685 assert_eq!(
686 tx_descriptors[0]
687 .effective_route()
688 .map(|route| route.into_owned()),
689 Some("telemetry/imu".to_string())
690 );
691 assert_eq!(
692 tx_descriptors[1]
693 .effective_route()
694 .map(|route| route.into_owned()),
695 Some("motor/cmd".to_string())
696 );
697 let overridden = BridgeChannelConfig::from_static(
698 &TxChannels::MOTOR,
699 Some("custom/motor".to_string()),
700 None,
701 );
702 assert_eq!(
703 overridden.effective_route().map(|route| route.into_owned()),
704 Some("custom/motor".to_string())
705 );
706
707 let mut bridge =
708 ExampleBridge::new(Some(&bridge_cfg), &tx_descriptors, &rx_descriptors, ())
709 .expect("bridge should build");
710
711 assert_eq!(bridge.port, "ttyUSB0");
712
713 let context = CuContext::new_with_clock();
714 let imu_msg = CuMsg::new(Some(ImuMsg { accel: 7 }));
715 bridge
716 .send(&context, &TxChannels::IMU, &imu_msg)
717 .expect("send should succeed");
718 let motor_msg = CuMsg::new(Some(MotorCmd { torque: -3 }));
719 bridge
720 .send(&context, &TxChannels::MOTOR, &motor_msg)
721 .expect("send should support multiple payload types");
722 assert_eq!(bridge.imu_samples, vec![7]);
723 assert_eq!(bridge.motor_torques, vec![-3]);
724
725 let mut status_msg = CuMsg::new(None);
726 bridge
727 .receive(&context, &RxChannels::STATUS, &mut status_msg)
728 .expect("receive should succeed");
729 assert!(status_msg.payload().is_some());
730 assert_eq!(bridge.status_temps, vec![21.5]);
731
732 let mut alert_msg = CuMsg::new(None);
733 bridge
734 .receive(&context, &RxChannels::ALERT, &mut alert_msg)
735 .expect("receive should handle other payload types too");
736 assert!(alert_msg.payload().is_some());
737 assert_eq!(bridge.alert_codes, vec![0xDEAD_BEEF]);
738 }
739}