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