use crate::config::ComponentConfig;
use crate::context::CuContext;
use crate::cutask::{CuMsg, CuMsgPayload, Freezable};
use crate::reflect::Reflect;
use alloc::borrow::Cow;
use alloc::string::String;
use core::fmt::{Debug, Formatter};
use core::marker::PhantomData;
use cu29_traits::CuResult;
#[derive(Copy, Clone)]
pub struct BridgeChannel<Id, Payload> {
pub id: Id,
pub default_route: Option<&'static str>,
_payload: PhantomData<fn() -> Payload>,
}
impl<Id, Payload> BridgeChannel<Id, Payload> {
pub const fn new(id: Id) -> Self {
Self {
id,
default_route: None,
_payload: PhantomData,
}
}
pub const fn with_channel(id: Id, route: &'static str) -> Self {
Self {
id,
default_route: Some(route),
_payload: PhantomData,
}
}
}
impl<Id: Debug, Payload> Debug for BridgeChannel<Id, Payload> {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
f.debug_struct("BridgeChannel")
.field("id", &self.id)
.field("default_route", &self.default_route)
.finish()
}
}
pub trait BridgeChannelInfo<Id: Copy> {
fn id(&self) -> Id;
fn default_route(&self) -> Option<&'static str>;
}
impl<Id: Copy, Payload> BridgeChannelInfo<Id> for BridgeChannel<Id, Payload> {
fn id(&self) -> Id {
self.id
}
fn default_route(&self) -> Option<&'static str> {
self.default_route
}
}
#[derive(Copy, Clone, Debug)]
pub struct BridgeChannelDescriptor<Id: Copy> {
pub id: Id,
pub default_route: Option<&'static str>,
}
impl<Id: Copy> BridgeChannelDescriptor<Id> {
pub const fn new(id: Id, default_route: Option<&'static str>) -> Self {
Self { id, default_route }
}
}
impl<Id: Copy, T> From<&T> for BridgeChannelDescriptor<Id>
where
T: BridgeChannelInfo<Id> + ?Sized,
{
fn from(channel: &T) -> Self {
BridgeChannelDescriptor::new(channel.id(), channel.default_route())
}
}
#[derive(Clone, Debug)]
pub struct BridgeChannelConfig<Id: Copy> {
pub channel: BridgeChannelDescriptor<Id>,
pub route: Option<String>,
pub config: Option<ComponentConfig>,
}
impl<Id: Copy> BridgeChannelConfig<Id> {
pub fn from_static<T>(
channel: &'static T,
route: Option<String>,
config: Option<ComponentConfig>,
) -> Self
where
T: BridgeChannelInfo<Id> + ?Sized,
{
Self {
channel: channel.into(),
route,
config,
}
}
pub fn effective_route(&self) -> Option<Cow<'_, str>> {
if let Some(route) = &self.route {
Some(Cow::Borrowed(route.as_str()))
} else {
self.channel.default_route.map(Cow::Borrowed)
}
}
}
pub trait BridgeChannelSet {
type Id: Copy + Eq + 'static;
const STATIC_CHANNELS: &'static [&'static dyn BridgeChannelInfo<Self::Id>];
}
pub trait CuBridge: Freezable + Reflect {
type Tx: BridgeChannelSet;
type Rx: BridgeChannelSet;
type Resources<'r>;
fn new(
config: Option<&ComponentConfig>,
tx_channels: &[BridgeChannelConfig<<Self::Tx as BridgeChannelSet>::Id>],
rx_channels: &[BridgeChannelConfig<<Self::Rx as BridgeChannelSet>::Id>],
resources: Self::Resources<'_>,
) -> CuResult<Self>
where
Self: Sized;
fn start(&mut self, _ctx: &CuContext) -> CuResult<()> {
Ok(())
}
fn preprocess(&mut self, _ctx: &CuContext) -> CuResult<()> {
Ok(())
}
fn send<'a, Payload>(
&mut self,
ctx: &CuContext,
channel: &'static BridgeChannel<<Self::Tx as BridgeChannelSet>::Id, Payload>,
msg: &CuMsg<Payload>,
) -> CuResult<()>
where
Payload: CuMsgPayload + 'a;
fn receive<'a, Payload>(
&mut self,
ctx: &CuContext,
channel: &'static BridgeChannel<<Self::Rx as BridgeChannelSet>::Id, Payload>,
msg: &mut CuMsg<Payload>,
) -> CuResult<()>
where
Payload: CuMsgPayload + 'a;
fn postprocess(&mut self, _ctx: &CuContext) -> CuResult<()> {
Ok(())
}
fn stop(&mut self, _ctx: &CuContext) -> CuResult<()> {
Ok(())
}
}
#[doc(hidden)]
#[macro_export]
macro_rules! __cu29_bridge_channel_ctor {
($id:ident, $variant:ident, $payload:ty) => {
$crate::cubridge::BridgeChannel::<$id, $payload>::new($id::$variant)
};
($id:ident, $variant:ident, $payload:ty, $route:expr) => {
$crate::cubridge::BridgeChannel::<$id, $payload>::with_channel($id::$variant, $route)
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __cu29_define_bridge_channels {
(
@accum
$vis:vis struct $channels:ident : $id:ident
[ $($parsed:tt)+ ]
) => {
$crate::__cu29_emit_bridge_channels! {
$vis struct $channels : $id { $($parsed)+ }
}
};
(
@accum
$vis:vis struct $channels:ident : $id:ident
[ ]
) => {
compile_error!("tx_channels!/rx_channels! require at least one channel");
};
(
@accum
$vis:vis struct $channels:ident : $id:ident
[ $($parsed:tt)* ]
$(#[$chan_meta:meta])* $const_name:ident : $variant:ident => $payload:ty $(= $route:expr)? , $($rest:tt)*
) => {
$crate::__cu29_define_bridge_channels!(
@accum
$vis struct $channels : $id
[
$($parsed)*
$(#[$chan_meta])* $const_name : $variant => $payload $(= $route)?,
]
$($rest)*
);
};
(
@accum
$vis:vis struct $channels:ident : $id:ident
[ $($parsed:tt)* ]
$(#[$chan_meta:meta])* $const_name:ident : $variant:ident => $payload:ty $(= $route:expr)?
) => {
$crate::__cu29_define_bridge_channels!(
@accum
$vis struct $channels : $id
[
$($parsed)*
$(#[$chan_meta])* $const_name : $variant => $payload $(= $route)?,
]
);
};
(
@accum
$vis:vis struct $channels:ident : $id:ident
[ $($parsed:tt)* ]
$(#[$chan_meta:meta])* $name:ident => $payload:ty $(= $route:expr)? , $($rest:tt)*
) => {
$crate::__cu29_paste! {
$crate::__cu29_define_bridge_channels!(
@accum
$vis struct $channels : $id
[
$($parsed)*
$(#[$chan_meta])* [<$name:snake:upper>] : [<$name:camel>] => $payload $(= $route)?,
]
$($rest)*
);
}
};
(
@accum
$vis:vis struct $channels:ident : $id:ident
[ $($parsed:tt)* ]
$(#[$chan_meta:meta])* $name:ident => $payload:ty $(= $route:expr)?
) => {
$crate::__cu29_paste! {
$crate::__cu29_define_bridge_channels!(
@accum
$vis struct $channels : $id
[
$($parsed)*
$(#[$chan_meta])* [<$name:snake:upper>] : [<$name:camel>] => $payload $(= $route)?,
]
);
}
};
(
$vis:vis struct $channels:ident : $id:ident {
$($body:tt)*
}
) => {
$crate::__cu29_define_bridge_channels!(
@accum
$vis struct $channels : $id
[]
$($body)*
);
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __cu29_emit_bridge_channels {
(
$vis:vis struct $channels:ident : $id:ident {
$(
$(#[$chan_meta:meta])*
$const_name:ident : $variant:ident => $payload:ty $(= $route:expr)?,
)+
}
) => {
#[derive(Copy, Clone, Debug, Eq, PartialEq, ::serde::Serialize, ::serde::Deserialize)]
#[repr(usize)]
#[serde(rename_all = "snake_case")]
$vis enum $id {
$(
$variant,
)+
}
impl $id {
pub const fn as_index(self) -> usize {
self as usize
}
}
$vis struct $channels;
#[allow(non_upper_case_globals)]
impl $channels {
$(
$(#[$chan_meta])*
$vis const $const_name: $crate::cubridge::BridgeChannel<$id, $payload> =
$crate::__cu29_bridge_channel_ctor!(
$id, $variant, $payload $(, $route)?
);
)+
}
impl $crate::cubridge::BridgeChannelSet for $channels {
type Id = $id;
const STATIC_CHANNELS: &'static [&'static dyn $crate::cubridge::BridgeChannelInfo<Self::Id>] =
&[
$(
&Self::$const_name,
)+
];
}
};
}
#[macro_export]
macro_rules! tx_channels {
(
$vis:vis struct $channels:ident : $id:ident {
$(
$(#[$chan_meta:meta])* $entry:tt => $payload:ty $(= $route:expr)?
),+ $(,)?
}
) => {
$crate::__cu29_define_bridge_channels! {
$vis struct $channels : $id {
$(
$(#[$chan_meta])* $entry => $payload $(= $route)?,
)+
}
}
};
({ $($rest:tt)* }) => {
$crate::tx_channels! {
pub struct TxChannels : TxId { $($rest)* }
}
};
($($rest:tt)+) => {
$crate::tx_channels!({ $($rest)+ });
};
}
#[macro_export]
macro_rules! rx_channels {
(
$vis:vis struct $channels:ident : $id:ident {
$(
$(#[$chan_meta:meta])* $entry:tt => $payload:ty $(= $route:expr)?
),+ $(,)?
}
) => {
$crate::__cu29_define_bridge_channels! {
$vis struct $channels : $id {
$(
$(#[$chan_meta])* $entry => $payload $(= $route)?,
)+
}
}
};
({ $($rest:tt)* }) => {
$crate::rx_channels! {
pub struct RxChannels : RxId { $($rest)* }
}
};
($($rest:tt)+) => {
$crate::rx_channels!({ $($rest)+ });
};
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::ComponentConfig;
use crate::context::CuContext;
use crate::cutask::CuMsg;
use alloc::vec::Vec;
use cu29_traits::CuError;
use serde::{Deserialize, Serialize};
#[derive(
Clone, Debug, Default, Serialize, Deserialize, bincode::Encode, bincode::Decode, Reflect,
)]
struct ImuMsg {
accel: i32,
}
#[derive(
Clone, Debug, Default, Serialize, Deserialize, bincode::Encode, bincode::Decode, Reflect,
)]
struct MotorCmd {
torque: i16,
}
#[derive(
Clone, Debug, Default, Serialize, Deserialize, bincode::Encode, bincode::Decode, Reflect,
)]
struct StatusMsg {
temperature: f32,
}
#[derive(
Clone, Debug, Default, Serialize, Deserialize, bincode::Encode, bincode::Decode, Reflect,
)]
struct AlertMsg {
code: u32,
}
tx_channels! {
struct MacroTxChannels : MacroTxId {
imu_stream => ImuMsg = "telemetry/imu",
motor_stream => MotorCmd,
}
}
rx_channels! {
struct MacroRxChannels : MacroRxId {
status_updates => StatusMsg = "sys/status",
alert_stream => AlertMsg,
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum TxId {
Imu,
Motor,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum RxId {
Status,
Alert,
}
struct TxChannels;
impl TxChannels {
pub const IMU: BridgeChannel<TxId, ImuMsg> =
BridgeChannel::with_channel(TxId::Imu, "telemetry/imu");
pub const MOTOR: BridgeChannel<TxId, MotorCmd> =
BridgeChannel::with_channel(TxId::Motor, "motor/cmd");
}
impl BridgeChannelSet for TxChannels {
type Id = TxId;
const STATIC_CHANNELS: &'static [&'static dyn BridgeChannelInfo<Self::Id>] =
&[&Self::IMU, &Self::MOTOR];
}
struct RxChannels;
impl RxChannels {
pub const STATUS: BridgeChannel<RxId, StatusMsg> =
BridgeChannel::with_channel(RxId::Status, "sys/status");
pub const ALERT: BridgeChannel<RxId, AlertMsg> =
BridgeChannel::with_channel(RxId::Alert, "sys/alert");
}
impl BridgeChannelSet for RxChannels {
type Id = RxId;
const STATIC_CHANNELS: &'static [&'static dyn BridgeChannelInfo<Self::Id>] =
&[&Self::STATUS, &Self::ALERT];
}
#[derive(Default, Reflect)]
struct ExampleBridge {
port: String,
imu_samples: Vec<i32>,
motor_torques: Vec<i16>,
status_temps: Vec<f32>,
alert_codes: Vec<u32>,
}
impl Freezable for ExampleBridge {}
impl CuBridge for ExampleBridge {
type Resources<'r> = ();
type Tx = TxChannels;
type Rx = RxChannels;
fn new(
config: Option<&ComponentConfig>,
_tx_channels: &[BridgeChannelConfig<TxId>],
_rx_channels: &[BridgeChannelConfig<RxId>],
_resources: Self::Resources<'_>,
) -> CuResult<Self> {
let mut instance = ExampleBridge::default();
if let Some(cfg) = config
&& let Some(port) = cfg.get::<String>("port")?
{
instance.port = port;
}
Ok(instance)
}
fn send<'a, Payload>(
&mut self,
_ctx: &CuContext,
channel: &'static BridgeChannel<TxId, Payload>,
msg: &CuMsg<Payload>,
) -> CuResult<()>
where
Payload: CuMsgPayload + 'a,
{
match channel.id {
TxId::Imu => {
let imu_msg = msg.downcast_ref::<ImuMsg>()?;
let payload = imu_msg
.payload()
.ok_or_else(|| CuError::from("imu missing payload"))?;
self.imu_samples.push(payload.accel);
Ok(())
}
TxId::Motor => {
let motor_msg = msg.downcast_ref::<MotorCmd>()?;
let payload = motor_msg
.payload()
.ok_or_else(|| CuError::from("motor missing payload"))?;
self.motor_torques.push(payload.torque);
Ok(())
}
}
}
fn receive<'a, Payload>(
&mut self,
_ctx: &CuContext,
channel: &'static BridgeChannel<RxId, Payload>,
msg: &mut CuMsg<Payload>,
) -> CuResult<()>
where
Payload: CuMsgPayload + 'a,
{
match channel.id {
RxId::Status => {
let status_msg = msg.downcast_mut::<StatusMsg>()?;
status_msg.set_payload(StatusMsg { temperature: 21.5 });
if let Some(payload) = status_msg.payload() {
self.status_temps.push(payload.temperature);
}
Ok(())
}
RxId::Alert => {
let alert_msg = msg.downcast_mut::<AlertMsg>()?;
alert_msg.set_payload(AlertMsg { code: 0xDEAD_BEEF });
if let Some(payload) = alert_msg.payload() {
self.alert_codes.push(payload.code);
}
Ok(())
}
}
}
}
#[test]
fn channel_macros_expose_static_metadata() {
assert_eq!(MacroTxChannels::STATIC_CHANNELS.len(), 2);
assert_eq!(
MacroTxChannels::IMU_STREAM.default_route,
Some("telemetry/imu")
);
assert!(MacroTxChannels::MOTOR_STREAM.default_route.is_none());
assert_eq!(MacroTxId::ImuStream as u8, MacroTxId::ImuStream as u8);
assert_eq!(MacroTxId::ImuStream.as_index(), 0);
assert_eq!(MacroTxId::MotorStream.as_index(), 1);
assert_eq!(MacroRxChannels::STATIC_CHANNELS.len(), 2);
assert_eq!(
MacroRxChannels::STATUS_UPDATES.default_route,
Some("sys/status")
);
assert!(MacroRxChannels::ALERT_STREAM.default_route.is_none());
assert_eq!(MacroRxId::StatusUpdates.as_index(), 0);
assert_eq!(MacroRxId::AlertStream.as_index(), 1);
}
#[test]
fn bridge_trait_compiles_and_accesses_configs() {
let mut bridge_cfg = ComponentConfig::default();
bridge_cfg.set("port", "ttyUSB0".to_string());
let tx_descriptors = [
BridgeChannelConfig::from_static(&TxChannels::IMU, None, None),
BridgeChannelConfig::from_static(&TxChannels::MOTOR, None, None),
];
let rx_descriptors = [
BridgeChannelConfig::from_static(&RxChannels::STATUS, None, None),
BridgeChannelConfig::from_static(&RxChannels::ALERT, None, None),
];
assert_eq!(
tx_descriptors[0]
.effective_route()
.map(|route| route.into_owned()),
Some("telemetry/imu".to_string())
);
assert_eq!(
tx_descriptors[1]
.effective_route()
.map(|route| route.into_owned()),
Some("motor/cmd".to_string())
);
let overridden = BridgeChannelConfig::from_static(
&TxChannels::MOTOR,
Some("custom/motor".to_string()),
None,
);
assert_eq!(
overridden.effective_route().map(|route| route.into_owned()),
Some("custom/motor".to_string())
);
let mut bridge =
ExampleBridge::new(Some(&bridge_cfg), &tx_descriptors, &rx_descriptors, ())
.expect("bridge should build");
assert_eq!(bridge.port, "ttyUSB0");
let context = CuContext::new_with_clock();
let imu_msg = CuMsg::new(Some(ImuMsg { accel: 7 }));
bridge
.send(&context, &TxChannels::IMU, &imu_msg)
.expect("send should succeed");
let motor_msg = CuMsg::new(Some(MotorCmd { torque: -3 }));
bridge
.send(&context, &TxChannels::MOTOR, &motor_msg)
.expect("send should support multiple payload types");
assert_eq!(bridge.imu_samples, vec![7]);
assert_eq!(bridge.motor_torques, vec![-3]);
let mut status_msg = CuMsg::new(None);
bridge
.receive(&context, &RxChannels::STATUS, &mut status_msg)
.expect("receive should succeed");
assert!(status_msg.payload().is_some());
assert_eq!(bridge.status_temps, vec![21.5]);
let mut alert_msg = CuMsg::new(None);
bridge
.receive(&context, &RxChannels::ALERT, &mut alert_msg)
.expect("receive should handle other payload types too");
assert!(alert_msg.payload().is_some());
assert_eq!(bridge.alert_codes, vec![0xDEAD_BEEF]);
}
}