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