1mod attachment;
2mod error;
3mod keyexpr;
4mod liveliness;
5mod node;
6mod topic;
7
8use attachment::encode_attachment;
9use cdr::{CdrLe, Infinite};
10use cu_ros2_payloads::RosBridgeAdapter;
11use cu29::cubridge::{BridgeChannel, BridgeChannelConfig, BridgeChannelSet, CuBridge};
12use cu29::prelude::*;
13use liveliness::{node_liveliness, publisher_liveliness, subscriber_liveliness};
14use node::Node;
15use topic::Topic;
16use zenoh::bytes::Encoding;
17use zenoh::{Config, Error as ZenohError};
18
19use std::any::{Any, TypeId, type_name};
20use std::collections::HashMap;
21use std::sync::{OnceLock, RwLock};
22
23const NODE_ID: u32 = 0;
25
26#[derive(Clone, Copy)]
27struct RosPayloadCodec {
28 namespace: &'static str,
29 type_name: &'static str,
30 type_hash: &'static str,
31 encode_payload: fn(&dyn Any) -> CuResult<Vec<u8>>,
32 decode_payload: fn(&[u8]) -> CuResult<Box<dyn Any>>,
33}
34
35struct RosPayloadRegistry {
36 codecs: RwLock<HashMap<TypeId, RosPayloadCodec>>,
37}
38
39impl RosPayloadRegistry {
40 fn new() -> Self {
41 Self {
42 codecs: RwLock::new(HashMap::new()),
43 }
44 }
45
46 fn register<Payload>(&self)
47 where
48 Payload: CuMsgPayload + RosBridgeAdapter + 'static,
49 {
50 let codec = RosPayloadCodec {
51 namespace: <Payload as RosBridgeAdapter>::namespace(),
52 type_name: <Payload as RosBridgeAdapter>::type_name(),
53 type_hash: <Payload as RosBridgeAdapter>::type_hash(),
54 encode_payload: encode_payload_with::<Payload>,
55 decode_payload: decode_payload_with::<Payload>,
56 };
57 self.codecs
58 .write()
59 .expect("RosPayloadRegistry lock poisoned")
60 .insert(TypeId::of::<Payload>(), codec);
61 }
62
63 fn codec_for<Payload>(&self) -> CuResult<RosPayloadCodec>
64 where
65 Payload: CuMsgPayload + 'static,
66 {
67 self.codecs
68 .read()
69 .expect("RosPayloadRegistry lock poisoned")
70 .get(&TypeId::of::<Payload>())
71 .copied()
72 .ok_or_else(|| {
73 CuError::from(format!(
74 "Ros2Bridge: No ROS codec registered for payload type {}",
75 type_name::<Payload>()
76 ))
77 })
78 }
79}
80
81fn encode_payload_with<Payload>(payload_any: &dyn Any) -> CuResult<Vec<u8>>
82where
83 Payload: CuMsgPayload + RosBridgeAdapter + 'static,
84{
85 let payload = payload_any.downcast_ref::<Payload>().ok_or_else(|| {
86 CuError::from(format!(
87 "Ros2Bridge: Registry encode payload mismatch for {}",
88 type_name::<Payload>()
89 ))
90 })?;
91 payload
92 .validate_ros_message()
93 .map_err(|e| CuError::from(format!("Ros2Bridge: Payload is not ROS-compatible: {e}")))?;
94 let ros_payload = payload.to_ros_message();
95 cdr::serialize::<_, _, CdrLe>(&ros_payload, Infinite)
96 .map_err(|e| CuError::new_with_cause("Ros2Bridge: Failed to serialize payload", e))
97}
98
99fn decode_payload_with<Payload>(bytes: &[u8]) -> CuResult<Box<dyn Any>>
100where
101 Payload: CuMsgPayload + RosBridgeAdapter + 'static,
102{
103 let ros_payload: <Payload as RosBridgeAdapter>::RosMessage = cdr::deserialize(bytes)
104 .map_err(|e| CuError::new_with_cause("Ros2Bridge: Failed to deserialize payload", e))?;
105 let payload = Payload::from_ros_message(ros_payload).map_err(CuError::from)?;
106 Ok(Box::new(payload))
107}
108
109fn payload_registry() -> &'static RosPayloadRegistry {
110 static REGISTRY: OnceLock<RosPayloadRegistry> = OnceLock::new();
111 REGISTRY.get_or_init(|| {
112 let registry = RosPayloadRegistry::new();
113 registry.register::<bool>();
114 registry.register::<i8>();
115 registry.register::<i16>();
116 registry.register::<i32>();
117 registry.register::<i64>();
118 registry.register::<u8>();
119 registry.register::<u16>();
120 registry.register::<u32>();
121 registry.register::<u64>();
122 registry.register::<f32>();
123 registry.register::<f64>();
124 registry.register::<String>();
125 registry
126 })
127}
128
129pub fn register_ros2_payload<Payload>()
134where
135 Payload: CuMsgPayload + RosBridgeAdapter + 'static,
136{
137 payload_registry().register::<Payload>();
138}
139
140#[derive(Debug, Clone)]
144enum RxQueueConfig {
145 Fifo,
146 Ring { size: usize },
147}
148
149impl RxQueueConfig {
150 fn from_config(config: Option<&ComponentConfig>) -> CuResult<Self> {
151 let Some(cfg) = config else {
152 return Ok(Self::Fifo);
153 };
154 match cfg.get::<String>("queue_mode")?.as_deref() {
155 Some("ring") => Ok(Self::Ring {
156 size: cfg.get::<u32>("ring_size")?.unwrap_or(1) as usize,
157 }),
158 Some("fifo") | None => Ok(Self::Fifo),
159 Some(other) => Err(CuError::from(format!(
160 "Ros2Bridge: unknown queue_mode '{other}'"
161 ))),
162 }
163 }
164}
165
166#[derive(Debug, Clone)]
167struct Ros2TxChannelConfig<Id: Copy> {
168 id: Id,
169 route: String,
170}
171
172#[derive(Debug, Clone)]
173struct Ros2RxChannelConfig<Id: Copy> {
174 id: Id,
175 route: String,
176 queue: RxQueueConfig,
177}
178
179enum Ros2Subscriber {
180 Fifo(zenoh::pubsub::Subscriber<zenoh::handlers::FifoChannelHandler<zenoh::sample::Sample>>),
181 Ring(zenoh::pubsub::Subscriber<zenoh::handlers::RingChannelHandler<zenoh::sample::Sample>>),
182}
183
184struct Ros2TxChannel<Id: Copy> {
185 id: Id,
186 route: String,
187 entity_id: u32,
188 sequence_number: u64,
189 publisher: Option<zenoh::pubsub::Publisher<'static>>,
190 publisher_token: Option<zenoh::liveliness::LivelinessToken>,
191}
192
193struct Ros2RxChannel<Id: Copy> {
194 id: Id,
195 route: String,
196 entity_id: u32,
197 queue: RxQueueConfig,
198 subscriber: Option<Ros2Subscriber>,
199 subscriber_token: Option<zenoh::liveliness::LivelinessToken>,
200}
201
202struct Ros2Context<TxId: Copy, RxId: Copy> {
203 session: zenoh::Session,
204 #[allow(dead_code)]
205 node_token: zenoh::liveliness::LivelinessToken,
206 tx_channels: Vec<Ros2TxChannel<TxId>>,
207 rx_channels: Vec<Ros2RxChannel<RxId>>,
208}
209
210#[derive(Reflect)]
211#[reflect(from_reflect = false, no_field_bounds, type_path = false)]
212pub struct Ros2Bridge<Tx, Rx>
213where
214 Tx: BridgeChannelSet + 'static,
215 Rx: BridgeChannelSet + 'static,
216 Tx::Id: Send + Sync + 'static,
217 Rx::Id: Send + Sync + 'static,
218{
219 #[reflect(ignore)]
220 session_config: Config,
221 domain_id: u32,
222 namespace: String,
223 node: String,
224 #[reflect(ignore)]
225 tx_channels: Vec<Ros2TxChannelConfig<Tx::Id>>,
226 #[reflect(ignore)]
227 rx_channels: Vec<Ros2RxChannelConfig<Rx::Id>>,
228 #[reflect(ignore)]
229 ctx: Option<Ros2Context<Tx::Id, Rx::Id>>,
230}
231
232impl<Tx, Rx> Freezable for Ros2Bridge<Tx, Rx>
233where
234 Tx: BridgeChannelSet + 'static,
235 Rx: BridgeChannelSet + 'static,
236 Tx::Id: Send + Sync + 'static,
237 Rx::Id: Send + Sync + 'static,
238{
239}
240
241impl<Tx, Rx> cu29::reflect::TypePath for Ros2Bridge<Tx, Rx>
242where
243 Tx: BridgeChannelSet + 'static,
244 Rx: BridgeChannelSet + 'static,
245 Tx::Id: Send + Sync + 'static,
246 Rx::Id: Send + Sync + 'static,
247{
248 fn type_path() -> &'static str {
249 "cu_ros2_bridge::Ros2Bridge"
250 }
251
252 fn short_type_path() -> &'static str {
253 "Ros2Bridge"
254 }
255
256 fn type_ident() -> Option<&'static str> {
257 Some("Ros2Bridge")
258 }
259
260 fn crate_name() -> Option<&'static str> {
261 Some("cu_ros2_bridge")
262 }
263
264 fn module_path() -> Option<&'static str> {
265 Some("cu_ros2_bridge")
266 }
267}
268
269impl<Tx, Rx> Ros2Bridge<Tx, Rx>
270where
271 Tx: BridgeChannelSet + 'static,
272 Rx: BridgeChannelSet + 'static,
273 Tx::Id: Send + Sync + 'static,
274 Rx::Id: Send + Sync + 'static,
275{
276 fn parse_session_config(config: &ComponentConfig) -> CuResult<Config> {
277 if let Some(path) = config.get::<String>("zenoh_config_file")? {
278 return Config::from_file(&path).map_err(|e| {
279 CuError::from(format!("Ros2Bridge: Failed to read config file: {e}"))
280 });
281 }
282 if let Some(json) = config.get::<String>("zenoh_config_json")? {
283 return Config::from_json5(&json).map_err(|e| {
284 CuError::from(format!("Ros2Bridge: Failed to parse config json: {e}"))
285 });
286 }
287 Ok(Config::default())
288 }
289
290 fn parse_domain_id(config: &ComponentConfig) -> CuResult<u32> {
291 Ok(config.get::<u32>("domain_id")?.unwrap_or(0))
292 }
293
294 fn parse_namespace(config: &ComponentConfig) -> CuResult<String> {
295 Ok(config
296 .get::<String>("namespace")?
297 .unwrap_or_else(|| "copper".to_string()))
298 }
299
300 fn parse_node_name(config: &ComponentConfig) -> CuResult<String> {
301 Ok(config
302 .get::<String>("node")?
303 .unwrap_or_else(|| "node".to_string()))
304 }
305
306 fn channel_route<Id: Copy + core::fmt::Debug>(
307 channel: &BridgeChannelConfig<Id>,
308 ) -> CuResult<String> {
309 channel
310 .effective_route()
311 .map(|route| route.into_owned())
312 .ok_or_else(|| {
313 let id = channel.channel.id;
314 CuError::from(format!(
315 "Ros2Bridge: Missing route/topic for channel {:?}",
316 id
317 ))
318 })
319 }
320
321 fn make_node<'a>(
322 domain_id: u32,
323 namespace: &'a str,
324 node_name: &'a str,
325 session: &'a zenoh::Session,
326 ) -> Node<'a> {
327 Node {
328 domain_id,
329 zid: session.zid(),
330 id: NODE_ID,
331 namespace,
332 name: node_name,
333 }
334 }
335
336 fn find_tx_channel_index(channels: &[Ros2TxChannel<Tx::Id>], id: Tx::Id) -> Option<usize> {
337 channels.iter().position(|channel| channel.id == id)
338 }
339
340 fn find_rx_channel_index(channels: &[Ros2RxChannel<Rx::Id>], id: Rx::Id) -> Option<usize> {
341 channels.iter().position(|channel| channel.id == id)
342 }
343
344 fn codec_for_payload<Payload>() -> CuResult<RosPayloadCodec>
345 where
346 Payload: CuMsgPayload + 'static,
347 {
348 payload_registry().codec_for::<Payload>()
349 }
350
351 fn topic_for_codec<'a>(route: &'a str, codec: RosPayloadCodec) -> Topic<'a> {
352 Topic::from_ros_type(route, codec.namespace, codec.type_name, codec.type_hash)
353 }
354
355 fn encode_payload<Payload>(msg: &CuMsg<Payload>, codec: RosPayloadCodec) -> CuResult<Vec<u8>>
356 where
357 Payload: CuMsgPayload + 'static,
358 {
359 let payload = msg
360 .payload()
361 .ok_or_else(|| CuError::from("Ros2Bridge: Cannot send empty payload through bridge"))?;
362 (codec.encode_payload)(payload as &dyn Any)
363 }
364
365 fn decode_payload_into<Payload>(
366 bytes: &[u8],
367 msg: &mut CuMsg<Payload>,
368 codec: RosPayloadCodec,
369 ) -> CuResult<()>
370 where
371 Payload: CuMsgPayload + 'static,
372 {
373 let payload_any = (codec.decode_payload)(bytes)?;
374 let payload = payload_any.downcast::<Payload>().map_err(|_| {
375 CuError::from(format!(
376 "Ros2Bridge: Codec produced wrong payload type for {}",
377 type_name::<Payload>()
378 ))
379 })?;
380 msg.set_payload(*payload);
381 Ok(())
382 }
383
384 fn init_tx_channel(
385 domain_id: u32,
386 namespace: &str,
387 node_name: &str,
388 ctx: &mut Ros2Context<Tx::Id, Rx::Id>,
389 tx_idx: usize,
390 codec: RosPayloadCodec,
391 ) -> CuResult<()> {
392 if ctx.tx_channels[tx_idx].publisher.is_some() {
393 return Ok(());
394 }
395
396 let route = ctx.tx_channels[tx_idx].route.clone();
397 let entity_id = ctx.tx_channels[tx_idx].entity_id;
398 let topic = Self::topic_for_codec(route.as_str(), codec);
399 let node = Self::make_node(domain_id, namespace, node_name, &ctx.session);
400
401 let publisher_token = zenoh::Wait::wait(
402 ctx.session
403 .liveliness()
404 .declare_token(publisher_liveliness(&node, &topic, entity_id)?),
405 )
406 .map_err(cu_error_map(
407 "Ros2Bridge: Failed to declare topic liveliness token",
408 ))?;
409
410 let publisher =
411 zenoh::Wait::wait(ctx.session.declare_publisher(topic.pubsub_keyexpr(&node)?))
412 .map_err(cu_error_map("Ros2Bridge: Failed to create publisher"))?;
413
414 ctx.tx_channels[tx_idx].publisher = Some(publisher);
415 ctx.tx_channels[tx_idx].publisher_token = Some(publisher_token);
416
417 Ok(())
418 }
419
420 fn init_rx_channel(
421 domain_id: u32,
422 namespace: &str,
423 node_name: &str,
424 ctx: &mut Ros2Context<Tx::Id, Rx::Id>,
425 rx_idx: usize,
426 codec: RosPayloadCodec,
427 ) -> CuResult<()> {
428 if ctx.rx_channels[rx_idx].subscriber.is_some() {
429 return Ok(());
430 }
431
432 let route = ctx.rx_channels[rx_idx].route.clone();
433 let entity_id = ctx.rx_channels[rx_idx].entity_id;
434 let topic = Self::topic_for_codec(route.as_str(), codec);
435 let node = Self::make_node(domain_id, namespace, node_name, &ctx.session);
436
437 let subscriber_token = zenoh::Wait::wait(
438 ctx.session
439 .liveliness()
440 .declare_token(subscriber_liveliness(&node, &topic, entity_id)?),
441 )
442 .map_err(cu_error_map(
443 "Ros2Bridge: Failed to declare subscriber liveliness token",
444 ))?;
445
446 let keyexpr = topic.pubsub_keyexpr(&node)?;
447 let subscriber = match ctx.rx_channels[rx_idx].queue {
448 RxQueueConfig::Fifo => Ros2Subscriber::Fifo(
449 zenoh::Wait::wait(ctx.session.declare_subscriber(keyexpr))
450 .map_err(cu_error_map("Ros2Bridge: Failed to declare subscriber"))?,
451 ),
452 RxQueueConfig::Ring { size } => Ros2Subscriber::Ring(
453 zenoh::Wait::wait(
454 ctx.session
455 .declare_subscriber(keyexpr)
456 .with(zenoh::handlers::RingChannel::new(size)),
457 )
458 .map_err(cu_error_map("Ros2Bridge: Failed to declare subscriber"))?,
459 ),
460 };
461
462 ctx.rx_channels[rx_idx].subscriber_token = Some(subscriber_token);
463 ctx.rx_channels[rx_idx].subscriber = Some(subscriber);
464 Ok(())
465 }
466
467 fn ctx_mut(&mut self) -> CuResult<&mut Ros2Context<Tx::Id, Rx::Id>> {
468 self.ctx
469 .as_mut()
470 .ok_or_else(|| CuError::from("Ros2Bridge: Context not initialized"))
471 }
472}
473
474impl<Tx, Rx> CuBridge for Ros2Bridge<Tx, Rx>
475where
476 Tx: BridgeChannelSet + 'static,
477 Rx: BridgeChannelSet + 'static,
478 Tx::Id: core::fmt::Debug + Send + Sync + 'static,
479 Rx::Id: core::fmt::Debug + Send + Sync + 'static,
480{
481 type Tx = Tx;
482 type Rx = Rx;
483 type Resources<'r> = ();
484
485 fn new(
486 config: Option<&ComponentConfig>,
487 tx_channels: &[BridgeChannelConfig<<Self::Tx as BridgeChannelSet>::Id>],
488 rx_channels: &[BridgeChannelConfig<<Self::Rx as BridgeChannelSet>::Id>],
489 _resources: Self::Resources<'_>,
490 ) -> CuResult<Self>
491 where
492 Self: Sized,
493 {
494 let default_config = ComponentConfig::default();
495 let config = config.unwrap_or(&default_config);
496
497 let session_config = Self::parse_session_config(config)?;
498 let domain_id = Self::parse_domain_id(config)?;
499 let namespace = Self::parse_namespace(config)?;
500 let node = Self::parse_node_name(config)?;
501
502 let mut tx_cfgs = Vec::with_capacity(tx_channels.len());
503 for channel in tx_channels {
504 let route = Self::channel_route(channel)?;
505 tx_cfgs.push(Ros2TxChannelConfig {
506 id: channel.channel.id,
507 route,
508 });
509 }
510
511 let mut rx_cfgs = Vec::with_capacity(rx_channels.len());
512 for channel in rx_channels {
513 let route = Self::channel_route(channel)?;
514 let queue = RxQueueConfig::from_config(channel.config.as_ref())?;
515 rx_cfgs.push(Ros2RxChannelConfig {
516 id: channel.channel.id,
517 route,
518 queue,
519 });
520 }
521
522 Ok(Self {
523 session_config,
524 domain_id,
525 namespace,
526 node,
527 tx_channels: tx_cfgs,
528 rx_channels: rx_cfgs,
529 ctx: None,
530 })
531 }
532
533 fn start(&mut self, _ctx: &CuContext) -> CuResult<()> {
534 let session = zenoh::Wait::wait(zenoh::open(self.session_config.clone()))
535 .map_err(cu_error_map("Ros2Bridge: Failed to open session"))?;
536
537 let node = Self::make_node(self.domain_id, &self.namespace, &self.node, &session);
538 let node_token =
539 zenoh::Wait::wait(session.liveliness().declare_token(node_liveliness(&node)?))
540 .map_err(cu_error_map(
541 "Ros2Bridge: Failed to declare node liveliness token",
542 ))?;
543
544 let tx_channels = self
545 .tx_channels
546 .iter()
547 .enumerate()
548 .map(|(index, channel)| Ros2TxChannel {
549 id: channel.id,
550 route: channel.route.clone(),
551 entity_id: (index + 1) as u32,
552 sequence_number: 0,
553 publisher: None,
554 publisher_token: None,
555 })
556 .collect();
557
558 let rx_channels = self
559 .rx_channels
560 .iter()
561 .enumerate()
562 .map(|(index, channel)| Ros2RxChannel {
563 id: channel.id,
564 route: channel.route.clone(),
565 entity_id: (index + 1) as u32,
566 queue: channel.queue.clone(),
567 subscriber: None,
568 subscriber_token: None,
569 })
570 .collect();
571
572 self.ctx = Some(Ros2Context {
573 session,
574 node_token,
575 tx_channels,
576 rx_channels,
577 });
578
579 Ok(())
580 }
581
582 fn send<'a, Payload>(
583 &mut self,
584 ctx: &CuContext,
585 channel: &'static BridgeChannel<<Self::Tx as BridgeChannelSet>::Id, Payload>,
586 msg: &CuMsg<Payload>,
587 ) -> CuResult<()>
588 where
589 Payload: CuMsgPayload + 'a,
590 {
591 let codec = Self::codec_for_payload::<Payload>()?;
592 let domain_id = self.domain_id;
593 let namespace = self.namespace.clone();
594 let node_name = self.node.clone();
595 let channel_id = channel.id();
596 let bridge_ctx = self.ctx_mut()?;
597
598 let tx_idx =
599 Self::find_tx_channel_index(&bridge_ctx.tx_channels, channel_id).ok_or_else(|| {
600 CuError::from(format!("Ros2Bridge: Unknown Tx channel {:?}", channel_id))
601 })?;
602
603 Self::init_tx_channel(domain_id, &namespace, &node_name, bridge_ctx, tx_idx, codec)?;
604
605 let encoded = Self::encode_payload(msg, codec)?;
606 let session_zid = bridge_ctx.session.zid();
607
608 let tx_channel = &mut bridge_ctx.tx_channels[tx_idx];
609 let publisher = tx_channel
610 .publisher
611 .as_mut()
612 .ok_or_else(|| CuError::from("Ros2Bridge: Tx publisher not initialized"))?;
613
614 let attachment = encode_attachment(tx_channel.sequence_number, ctx, &session_zid);
615 tx_channel.sequence_number += 1;
616
617 zenoh::Wait::wait(
618 publisher
619 .put(encoded)
620 .encoding(Encoding::APPLICATION_CDR)
621 .attachment(attachment),
622 )
623 .map_err(cu_error_map("Ros2Bridge: Failed to put value"))?;
624
625 Ok(())
626 }
627
628 fn receive<'a, Payload>(
629 &mut self,
630 ctx: &CuContext,
631 channel: &'static BridgeChannel<<Self::Rx as BridgeChannelSet>::Id, Payload>,
632 msg: &mut CuMsg<Payload>,
633 ) -> CuResult<()>
634 where
635 Payload: CuMsgPayload + 'a,
636 {
637 let codec = Self::codec_for_payload::<Payload>()?;
638 let domain_id = self.domain_id;
639 let namespace = self.namespace.clone();
640 let node_name = self.node.clone();
641 let channel_id = channel.id();
642 let bridge_ctx = self.ctx_mut()?;
643
644 let rx_idx =
645 Self::find_rx_channel_index(&bridge_ctx.rx_channels, channel_id).ok_or_else(|| {
646 CuError::from(format!("Ros2Bridge: Unknown Rx channel {:?}", channel_id))
647 })?;
648
649 Self::init_rx_channel(domain_id, &namespace, &node_name, bridge_ctx, rx_idx, codec)?;
650
651 msg.tov = Tov::Time(ctx.now());
652
653 let subscriber = bridge_ctx.rx_channels[rx_idx]
654 .subscriber
655 .as_mut()
656 .ok_or_else(|| CuError::from("Ros2Bridge: Rx subscriber not initialized"))?;
657 let sample = match subscriber {
658 Ros2Subscriber::Fifo(s) => s.try_recv(),
659 Ros2Subscriber::Ring(s) => s.try_recv(),
660 }
661 .map_err(|e| CuError::from(format!("Ros2Bridge: receive failed: {e}")))?;
662
663 if let Some(sample) = sample {
664 let payload = sample.payload().to_bytes();
665 Self::decode_payload_into(payload.as_ref(), msg, codec)?;
666 } else {
667 msg.clear_payload();
668 }
669
670 Ok(())
671 }
672
673 fn stop(&mut self, _ctx: &CuContext) -> CuResult<()> {
674 if let Some(Ros2Context {
675 session,
676 node_token: _,
677 tx_channels,
678 rx_channels,
679 }) = self.ctx.take()
680 {
681 for channel in tx_channels {
682 if let Some(publisher) = channel.publisher {
683 zenoh::Wait::wait(publisher.undeclare())
684 .map_err(cu_error_map("Ros2Bridge: Failed to undeclare publisher"))?;
685 }
686 }
687
688 for channel in rx_channels {
689 if let Some(subscriber) = channel.subscriber {
690 match subscriber {
691 Ros2Subscriber::Fifo(s) => zenoh::Wait::wait(s.undeclare())
692 .map_err(cu_error_map("Ros2Bridge: Failed to undeclare subscriber"))?,
693 Ros2Subscriber::Ring(s) => zenoh::Wait::wait(s.undeclare())
694 .map_err(cu_error_map("Ros2Bridge: Failed to undeclare subscriber"))?,
695 }
696 }
697 }
698
699 zenoh::Wait::wait(session.close())
700 .map_err(cu_error_map("Ros2Bridge: Failed to close session"))?;
701 }
702
703 Ok(())
704 }
705}
706
707fn cu_error_map(msg: &str) -> impl FnOnce(ZenohError) -> CuError + '_ {
708 move |e| CuError::from(format!("{msg}: {e}"))
709}
710
711#[cfg(test)]
712mod tests {
713 use super::*;
714 use cu29::bincode::{Decode, Encode};
715 use serde::{Deserialize, Serialize};
716
717 #[derive(Clone, Debug, Default, Encode, Decode, Serialize, Deserialize, Reflect)]
718 struct RejectingPayload;
719
720 impl RosBridgeAdapter for RejectingPayload {
721 type RosMessage = i32;
722
723 fn validate_ros_message(&self) -> Result<(), String> {
724 Err("test payload rejected".to_string())
725 }
726
727 fn namespace() -> &'static str {
728 "test_msgs"
729 }
730
731 fn type_hash() -> &'static str {
732 "test_hash"
733 }
734
735 fn to_ros_message(&self) -> Self::RosMessage {
736 0
737 }
738
739 fn from_ros_message(_msg: Self::RosMessage) -> Result<Self, String> {
740 Ok(Self)
741 }
742 }
743
744 #[test]
745 fn payload_validation_failure_stops_encoding() {
746 let error = encode_payload_with::<RejectingPayload>(&RejectingPayload)
747 .expect_err("validation failure should stop ROS encoding");
748
749 assert!(error.to_string().contains("test payload rejected"));
750 }
751
752 #[test]
753 fn scalar_payload_cdr_roundtrip_uses_little_endian_encapsulation() {
754 let mut src = CuMsg::<i32>::default();
755 src.set_payload(0x0102_0304);
756
757 let codec =
758 Ros2Bridge::<crate::tests::DummyTx, crate::tests::DummyRx>::codec_for_payload::<i32>()
759 .expect("codec should be registered");
760 let bytes =
761 Ros2Bridge::<crate::tests::DummyTx, crate::tests::DummyRx>::encode_payload(&src, codec)
762 .expect("encode should succeed");
763
764 assert_eq!(&bytes[..4], &[0, 1, 0, 0], "CDR_LE encapsulation");
765 assert_eq!(&bytes[4..8], &[4, 3, 2, 1], "little-endian i32 data");
766
767 let mut dst = CuMsg::<i32>::default();
768 Ros2Bridge::<crate::tests::DummyTx, crate::tests::DummyRx>::decode_payload_into(
769 bytes.as_slice(),
770 &mut dst,
771 codec,
772 )
773 .expect("decode should succeed");
774
775 assert_eq!(dst.payload(), Some(&0x0102_0304));
776 }
777
778 #[test]
779 fn default_scalar_codecs_registered() {
780 Ros2Bridge::<crate::tests::DummyTx, crate::tests::DummyRx>::codec_for_payload::<bool>()
781 .expect("bool codec");
782 Ros2Bridge::<crate::tests::DummyTx, crate::tests::DummyRx>::codec_for_payload::<i8>()
783 .expect("i8 codec");
784 Ros2Bridge::<crate::tests::DummyTx, crate::tests::DummyRx>::codec_for_payload::<i16>()
785 .expect("i16 codec");
786 Ros2Bridge::<crate::tests::DummyTx, crate::tests::DummyRx>::codec_for_payload::<i32>()
787 .expect("i32 codec");
788 Ros2Bridge::<crate::tests::DummyTx, crate::tests::DummyRx>::codec_for_payload::<i64>()
789 .expect("i64 codec");
790 Ros2Bridge::<crate::tests::DummyTx, crate::tests::DummyRx>::codec_for_payload::<u8>()
791 .expect("u8 codec");
792 Ros2Bridge::<crate::tests::DummyTx, crate::tests::DummyRx>::codec_for_payload::<u16>()
793 .expect("u16 codec");
794 Ros2Bridge::<crate::tests::DummyTx, crate::tests::DummyRx>::codec_for_payload::<u32>()
795 .expect("u32 codec");
796 Ros2Bridge::<crate::tests::DummyTx, crate::tests::DummyRx>::codec_for_payload::<u64>()
797 .expect("u64 codec");
798 Ros2Bridge::<crate::tests::DummyTx, crate::tests::DummyRx>::codec_for_payload::<f32>()
799 .expect("f32 codec");
800 Ros2Bridge::<crate::tests::DummyTx, crate::tests::DummyRx>::codec_for_payload::<f64>()
801 .expect("f64 codec");
802 Ros2Bridge::<crate::tests::DummyTx, crate::tests::DummyRx>::codec_for_payload::<String>()
803 .expect("string codec");
804 }
805
806 tx_channels! {
807 struct DummyTx : DummyTxId {
808 out => i8,
809 }
810 }
811
812 rx_channels! {
813 struct DummyRx : DummyRxId {
814 input => i8,
815 }
816 }
817}