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::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/// Compile-time description of a single bridge channel, including the message type carried on it.
15///
16/// This links its identifier to a payload type enforced at compile time and optionally provides a
17/// backend-specific default route/topic/path suggestion. Missions can override that default (or
18/// leave it unset) via the bridge configuration file.
19#[derive(Copy, Clone)]
20pub struct BridgeChannel<Id, Payload> {
21    /// Strongly typed identifier used to select this channel.
22    pub id: Id,
23    /// Backend-specific route/topic/path default the bridge should bind to, if any.
24    pub default_route: Option<&'static str>,
25    _payload: PhantomData<fn() -> Payload>,
26}
27
28impl<Id, Payload> BridgeChannel<Id, Payload> {
29    /// Declares a channel that leaves the route unspecified and entirely configuration-driven.
30    pub const fn new(id: Id) -> Self {
31        Self {
32            id,
33            default_route: None,
34            _payload: PhantomData,
35        }
36    }
37
38    /// Declares a channel with a default backend route.
39    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
57/// Type-erased metadata exposed for channel enumeration and configuration.
58pub trait BridgeChannelInfo<Id: Copy> {
59    /// Logical identifier referencing this channel inside the graph.
60    fn id(&self) -> Id;
61    /// Default backend-specific route/topic/path the bridge recommends binding to.
62    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/// Static metadata describing a channel. Used to pass configuration data at runtime without
76/// leaking the channel's payload type.
77#[derive(Copy, Clone, Debug)]
78pub struct BridgeChannelDescriptor<Id: Copy> {
79    /// Strongly typed identifier used to select this channel.
80    pub id: Id,
81    /// Backend-specific default route/topic/path the bridge suggests binding to.
82    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/// Runtime descriptor that includes the parsed per-channel configuration.
101#[derive(Clone, Debug)]
102pub struct BridgeChannelConfig<Id: Copy> {
103    /// Static metadata describing this channel.
104    pub channel: BridgeChannelDescriptor<Id>,
105    /// Optional route override supplied by the mission configuration.
106    pub route: Option<String>,
107    /// Optional configuration block defined for this channel.
108    pub config: Option<ComponentConfig>,
109}
110
111impl<Id: Copy> BridgeChannelConfig<Id> {
112    /// Creates a descriptor by combining the static metadata and the parsed configuration.
113    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    /// Returns the route active for this channel (configuration override wins over defaults).
129    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
138/// Describes a set of channels for one direction (Tx or Rx) of the bridge.
139///
140/// This trait is implemented at compile time by Copper from the configuration.
141/// Implementations typically expose one `BridgeChannel<Id, Payload>` constant per logical channel and
142/// list them through `STATIC_CHANNELS` so the runtime can enumerate the available endpoints.
143pub trait BridgeChannelSet {
144    /// Enumeration identifying each channel in this set.
145    type Id: Copy + Eq + 'static;
146
147    /// Compile-time metadata describing all channels in this set.
148    const STATIC_CHANNELS: &'static [&'static dyn BridgeChannelInfo<Self::Id>];
149}
150
151/// Public trait implemented by every copper bridge.
152///
153/// A bridge behaves similarly to set of [`crate::cutask::CuSrcTask`] /
154/// [`crate::cutask::CuSinkTask`], but it owns the shared transport state and knows how to
155/// multiplex multiple channels on a single backend (serial, CAN, middleware, …).
156pub trait CuBridge: Freezable {
157    /// Outgoing channels (Copper -> external world).
158    type Tx: BridgeChannelSet;
159    /// Incoming channels (external world -> Copper).
160    type Rx: BridgeChannelSet;
161    /// Resources required by the bridge.
162    type Resources<'r>;
163
164    /// Constructs a new bridge.
165    ///
166    /// The runtime passes the bridge-level configuration plus the per-channel descriptors
167    /// so the implementation can cache settings such as QoS, IDs, baud rates, etc.
168    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    /// Called before the first send/receive cycle.
178    fn start(&mut self, _clock: &RobotClock) -> CuResult<()> {
179        Ok(())
180    }
181
182    /// Gives the bridge a chance to prepare buffers before I/O.
183    fn preprocess(&mut self, _clock: &RobotClock) -> CuResult<()> {
184        Ok(())
185    }
186
187    /// Sends a message on the selected outbound channel.
188    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    /// Receives a message from the selected inbound channel.
198    ///
199    /// Implementations should write into `msg` when data is available.
200    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    /// Called once the send/receive pair completed.
210    fn postprocess(&mut self, _clock: &RobotClock) -> CuResult<()> {
211        Ok(())
212    }
213
214    /// Notifies the bridge that no more I/O will happen until a subsequent [`start`].
215    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            /// Returns the zero-based ordinal for this channel (macro order).
352            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/// Declares the transmit channels of a [`CuBridge`] implementation.
384///
385/// # Examples
386///
387/// ```
388/// # use cu29_runtime::tx_channels;
389/// # struct EscCommand;
390/// tx_channels! {
391///     esc0 => EscCommand,
392///     esc1 => EscCommand = "motor/esc1",
393/// }
394/// ```
395///
396/// ```
397/// # use cu29_runtime::tx_channels;
398/// # struct StateMsg;
399/// tx_channels! {
400///     pub(crate) struct MyTxChannels : MyTxId {
401///         state => StateMsg,
402///     }
403/// }
404/// ```
405///
406/// Channels declared through the macro gain `#[repr(usize)]` identifiers and an
407/// inherent `as_index()` helper that returns the zero-based ordinal (matching
408/// declaration order), which is convenient when indexing fixed arrays.
409#[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/// Declares the receive channels of a [`CuBridge`] implementation.
437///
438/// See [`tx_channels!`](crate::tx_channels!) for details on naming and indexing.
439#[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    // ---- Generated channel payload stubs (Copper build output) ---------------
477    #[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    // ---- Generated channel identifiers --------------------------------------
512    #[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    // ---- Generated channel descriptors & registries -------------------------
525    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    // ---- User-authored bridge implementation --------------------------------
558    #[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}