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