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 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/// Compile-time description of a single bridge channel, including the message type carried on it.
31///
32/// This links its identifier to a payload type enforced at compile time and optionally provides a
33/// backend-specific default route/topic/path suggestion. Missions can override that default (or
34/// leave it unset) via the bridge configuration file.
35#[derive(Copy, Clone)]
36pub struct BridgeChannel<Id, Payload> {
37    /// Strongly typed identifier used to select this channel.
38    pub id: Id,
39    /// Backend-specific route/topic/path default the bridge should bind to, if any.
40    pub default_route: Option<&'static str>,
41    _payload: PhantomData<fn() -> Payload>,
42}
43
44impl<Id, Payload> BridgeChannel<Id, Payload> {
45    /// Declares a channel that leaves the route unspecified and entirely configuration-driven.
46    pub const fn new(id: Id) -> Self {
47        Self {
48            id,
49            default_route: None,
50            _payload: PhantomData,
51        }
52    }
53
54    /// Declares a channel with a default backend route.
55    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
73/// Type-erased metadata exposed for channel enumeration and configuration.
74pub trait BridgeChannelInfo<Id: Copy> {
75    /// Logical identifier referencing this channel inside the graph.
76    fn id(&self) -> Id;
77    /// Default backend-specific route/topic/path the bridge recommends binding to.
78    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/// Static metadata describing a channel. Used to pass configuration data at runtime without
92/// leaking the channel's payload type.
93#[derive(Copy, Clone, Debug)]
94pub struct BridgeChannelDescriptor<Id: Copy> {
95    /// Strongly typed identifier used to select this channel.
96    pub id: Id,
97    /// Backend-specific default route/topic/path the bridge suggests binding to.
98    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/// Runtime descriptor that includes the parsed per-channel configuration.
117#[derive(Clone, Debug)]
118pub struct BridgeChannelConfig<Id: Copy> {
119    /// Static metadata describing this channel.
120    pub channel: BridgeChannelDescriptor<Id>,
121    /// Optional route override supplied by the mission configuration.
122    pub route: Option<String>,
123    /// Optional configuration block defined for this channel.
124    pub config: Option<ComponentConfig>,
125}
126
127impl<Id: Copy> BridgeChannelConfig<Id> {
128    /// Creates a descriptor by combining the static metadata and the parsed configuration.
129    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    /// Returns the route active for this channel (configuration override wins over defaults).
145    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
154/// Describes a set of channels for one direction (Tx or Rx) of the bridge.
155///
156/// This trait is implemented at compile time by Copper from the configuration.
157/// Implementations typically expose one `BridgeChannel<Id, Payload>` constant per logical channel and
158/// list them through `STATIC_CHANNELS` so the runtime can enumerate the available endpoints.
159pub trait BridgeChannelSet {
160    /// Enumeration identifying each channel in this set.
161    type Id: Copy + Eq + 'static;
162
163    /// Compile-time metadata describing all channels in this set.
164    const STATIC_CHANNELS: &'static [&'static dyn BridgeChannelInfo<Self::Id>];
165}
166
167/// Public trait implemented by every copper bridge.
168///
169/// A bridge behaves similarly to set of [`crate::cutask::CuSrcTask`] /
170/// [`crate::cutask::CuSinkTask`], but it owns the shared transport state and knows how to
171/// multiplex multiple channels on a single backend (serial, CAN, middleware, …).
172pub trait CuBridge: Freezable {
173    /// Outgoing channels (Copper -> external world).
174    type Tx: BridgeChannelSet;
175    /// Incoming channels (external world -> Copper).
176    type Rx: BridgeChannelSet;
177
178    /// Constructs a new bridge.
179    ///
180    /// The runtime passes the bridge-level configuration plus the per-channel descriptors
181    /// so the implementation can cache settings such as QoS, IDs, baud rates, etc.
182    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    /// Called before the first send/receive cycle.
191    fn start(&mut self, _clock: &RobotClock) -> CuResult<()> {
192        Ok(())
193    }
194
195    /// Gives the bridge a chance to prepare buffers before I/O.
196    fn preprocess(&mut self, _clock: &RobotClock) -> CuResult<()> {
197        Ok(())
198    }
199
200    /// Sends a message on the selected outbound channel.
201    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    /// Receives a message from the selected inbound channel.
211    ///
212    /// Implementations should write into `msg` when data is available.
213    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    /// Called once the send/receive pair completed.
223    fn postprocess(&mut self, _clock: &RobotClock) -> CuResult<()> {
224        Ok(())
225    }
226
227    /// Notifies the bridge that no more I/O will happen until a subsequent [`start`].
228    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            /// Returns the zero-based ordinal for this channel (macro order).
365            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/// Declares the transmit channels of a [`CuBridge`] implementation.
397///
398/// # Examples
399///
400/// ```
401/// # use cu29_runtime::tx_channels;
402/// # struct EscCommand;
403/// tx_channels! {
404///     esc0 => EscCommand,
405///     esc1 => EscCommand = "motor/esc1",
406/// }
407/// ```
408///
409/// ```
410/// # use cu29_runtime::tx_channels;
411/// # struct StateMsg;
412/// tx_channels! {
413///     pub(crate) struct MyTxChannels : MyTxId {
414///         state => StateMsg,
415///     }
416/// }
417/// ```
418///
419/// Channels declared through the macro gain `#[repr(usize)]` identifiers and an
420/// inherent `as_index()` helper that returns the zero-based ordinal (matching
421/// declaration order), which is convenient when indexing fixed arrays.
422#[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/// Declares the receive channels of a [`CuBridge`] implementation.
450///
451/// See [`tx_channels!`](crate::tx_channels!) for details on naming and indexing.
452#[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    // ---- Generated channel payload stubs (Copper build output) ---------------
493    #[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    // ---- Generated channel identifiers --------------------------------------
528    #[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    // ---- Generated channel descriptors & registries -------------------------
541    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    // ---- User-authored bridge implementation --------------------------------
574    #[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}