Skip to main content

cu29_runtime/
cubridge.rs

1//! Typed bridge traits and helpers used to connect Copper to external components both as a sink
2//! and a source.
3//!
4
5use 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/// Compile-time description of a single bridge channel, including the message type carried on it.
16///
17/// This links its identifier to a payload type enforced at compile time and optionally provides a
18/// backend-specific default route/topic/path suggestion. Missions can override that default (or
19/// leave it unset) via the bridge configuration file.
20#[derive(Copy, Clone, Debug, Eq, PartialEq)]
21pub enum TxEmptyPolicy {
22    /// Skip the bridge `send` call when the channel's input `CuMsg` has no payload.
23    Skip,
24    /// Still call the bridge `send` method with a metadata-only `CuMsg`.
25    Publish,
26}
27
28#[derive(Copy, Clone)]
29pub struct BridgeChannel<Id, Payload> {
30    /// Strongly typed identifier used to select this channel.
31    pub id: Id,
32    /// Backend-specific route/topic/path default the bridge should bind to, if any.
33    pub default_route: Option<&'static str>,
34    /// Static transmit policy for empty `CuMsg`s on this channel.
35    pub tx_empty_policy: TxEmptyPolicy,
36    _payload: PhantomData<fn() -> Payload>,
37}
38
39impl<Id, Payload> BridgeChannel<Id, Payload> {
40    /// Declares a channel that leaves the route unspecified and entirely configuration-driven.
41    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    /// Declares a channel with a default backend route.
51    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    /// Marks this channel to publish metadata-only bridge messages when the payload is empty.
61    pub const fn publish_empty(mut self) -> Self {
62        self.tx_empty_policy = TxEmptyPolicy::Publish;
63        self
64    }
65
66    /// Returns whether the runtime should call the bridge `send` method for this message.
67    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
82/// Type-erased metadata exposed for channel enumeration and configuration.
83pub trait BridgeChannelInfo<Id: Copy> {
84    /// Logical identifier referencing this channel inside the graph.
85    fn id(&self) -> Id;
86    /// Default backend-specific route/topic/path the bridge recommends binding to.
87    fn default_route(&self) -> Option<&'static str>;
88    /// Static transmit policy for metadata-only messages on this channel.
89    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/// Static metadata describing a channel. Used to pass configuration data at runtime without
107/// leaking the channel's payload type.
108#[derive(Copy, Clone, Debug)]
109pub struct BridgeChannelDescriptor<Id: Copy> {
110    /// Strongly typed identifier used to select this channel.
111    pub id: Id,
112    /// Backend-specific default route/topic/path the bridge suggests binding to.
113    pub default_route: Option<&'static str>,
114    /// Static transmit policy for metadata-only messages on this channel.
115    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/// Runtime descriptor that includes the parsed per-channel configuration.
146#[derive(Clone, Debug)]
147pub struct BridgeChannelConfig<Id: Copy> {
148    /// Static metadata describing this channel.
149    pub channel: BridgeChannelDescriptor<Id>,
150    /// Optional route override supplied by the mission configuration.
151    pub route: Option<String>,
152    /// Optional configuration block defined for this channel.
153    pub config: Option<ComponentConfig>,
154}
155
156impl<Id: Copy> BridgeChannelConfig<Id> {
157    /// Creates a descriptor by combining the static metadata and the parsed configuration.
158    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    /// Returns the route active for this channel (configuration override wins over defaults).
174    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
183/// Describes a set of channels for one direction (Tx or Rx) of the bridge.
184///
185/// This trait is implemented at compile time by Copper from the configuration.
186/// Implementations typically expose one `BridgeChannel<Id, Payload>` constant per logical channel and
187/// list them through `STATIC_CHANNELS` so the runtime can enumerate the available endpoints.
188pub trait BridgeChannelSet {
189    /// Enumeration identifying each channel in this set.
190    type Id: Copy + Eq + 'static;
191
192    /// Compile-time metadata describing all channels in this set.
193    const STATIC_CHANNELS: &'static [&'static dyn BridgeChannelInfo<Self::Id>];
194}
195
196/// Public trait implemented by every copper bridge.
197///
198/// A bridge behaves similarly to set of [`crate::cutask::CuSrcTask`] /
199/// [`crate::cutask::CuSinkTask`], but it owns the shared transport state and knows how to
200/// multiplex multiple channels on a single backend (serial, CAN, middleware, …).
201pub trait CuBridge: Freezable + Reflect {
202    /// Outgoing channels (Copper -> external world).
203    type Tx: BridgeChannelSet;
204    /// Incoming channels (external world -> Copper).
205    type Rx: BridgeChannelSet;
206    /// Resources required by the bridge.
207    type Resources<'r>;
208
209    /// Constructs a new bridge.
210    ///
211    /// The runtime passes the bridge-level configuration plus the per-channel descriptors
212    /// so the implementation can cache settings such as QoS, IDs, baud rates, etc.
213    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    /// Called before the first send/receive cycle.
223    fn start(&mut self, _ctx: &CuContext) -> CuResult<()> {
224        Ok(())
225    }
226
227    /// Gives the bridge a chance to prepare buffers before I/O.
228    fn preprocess(&mut self, _ctx: &CuContext) -> CuResult<()> {
229        Ok(())
230    }
231
232    /// Sends a message on the selected outbound channel.
233    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    /// Receives a message from the selected inbound channel.
243    ///
244    /// Implementations should write into `msg` when data is available.
245    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    /// Called once the send/receive pair completed.
255    fn postprocess(&mut self, _ctx: &CuContext) -> CuResult<()> {
256        Ok(())
257    }
258
259    /// Notifies the bridge that no more I/O will happen until a subsequent [`start`].
260    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            /// Returns the zero-based ordinal for this channel (macro order).
404            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/// Declares the transmit channels of a [`CuBridge`] implementation.
436///
437/// # Examples
438///
439/// ```
440/// # use cu29_runtime::tx_channels;
441/// # struct EscCommand;
442/// tx_channels! {
443///     esc0 => EscCommand,
444///     [publish_empty] esc1 => EscCommand = "motor/esc1",
445/// }
446/// ```
447///
448/// ```
449/// # use cu29_runtime::tx_channels;
450/// # struct StateMsg;
451/// tx_channels! {
452///     pub(crate) struct MyTxChannels : MyTxId {
453///         state => StateMsg,
454///     }
455/// }
456/// ```
457///
458/// Channels declared through the macro gain `#[repr(usize)]` identifiers and an
459/// inherent `as_index()` helper that returns the zero-based ordinal (matching
460/// declaration order), which is convenient when indexing fixed arrays.
461///
462/// By default, Tx channels skip bridge `send` calls when their upstream `CuMsg`
463/// has no payload. Prefix a Tx declaration with `[publish_empty]` to keep
464/// metadata-only sends enabled for backends that support them.
465#[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/// Declares the receive channels of a [`CuBridge`] implementation.
489///
490/// See [`tx_channels!`](crate::tx_channels!) for details on naming and indexing.
491#[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    // ---- Generated channel payload stubs (Copper build output) ---------------
525    #[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    // ---- Generated channel identifiers --------------------------------------
568    #[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    // ---- Generated channel descriptors & registries -------------------------
581    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    // ---- User-authored bridge implementation --------------------------------
614    #[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}