Skip to content

Commit de09d8c

Browse files
committed
polishment
1 parent c6027f9 commit de09d8c

File tree

6 files changed

+271
-18
lines changed

6 files changed

+271
-18
lines changed

book/src/chapters/backends.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ Use the builder pattern with generic type parameters for compile-time backend se
7171
use ros_z::{Builder, backend::{RmwZenohBackend, Ros2DdsBackend}};
7272
use ros_z::qos::{QosProfile, QosHistory};
7373
use ros_z_msgs::std_msgs::String as RosString;
74+
use ros_z_msgs::example_interfaces::srv::AddTwoInts;
75+
use ros_z_msgs::action_tutorials_interfaces::action::Fibonacci;
7476
7577
// Create context and node
7678
let ctx = ZContextBuilder::default().build()?;
@@ -87,6 +89,30 @@ let sub_dds = node
8789
.create_sub::<RosString>("chatter")
8890
.with_backend::<Ros2DdsBackend>() // DDS bridge compatibility
8991
.build()?;
92+
93+
// Service client with RmwZenoh backend
94+
let client = node
95+
.create_client::<AddTwoInts>("add_two_ints")
96+
.with_backend::<RmwZenohBackend>()
97+
.build()?;
98+
99+
// Service server with Ros2Dds backend
100+
let mut server = node
101+
.create_service::<AddTwoInts>("add_two_ints")
102+
.with_backend::<Ros2DdsBackend>()
103+
.build()?;
104+
105+
// Action client with RmwZenoh backend
106+
let action_client = node
107+
.create_action_client::<Fibonacci>("fibonacci")
108+
.with_backend::<RmwZenohBackend>()
109+
.build()?;
110+
111+
// Action server with Ros2Dds backend
112+
let mut action_server = node
113+
.create_action_server::<Fibonacci>("fibonacci")
114+
.with_backend::<Ros2DdsBackend>()
115+
.build()?;
90116
```
91117

92118
**Key points:**
@@ -108,6 +134,28 @@ let pub2 = node.create_pub::<RosString>("topic")
108134
.build()?;
109135
```
110136

137+
### Multiple Backend Features
138+
139+
When both `rmw-zenoh` and `ros2dds` feature flags are enabled in `Cargo.toml`:
140+
141+
```toml
142+
[dependencies]
143+
ros-z = { version = "0.1", features = ["rmw-zenoh", "ros2dds"] }
144+
```
145+
146+
**Default behavior**: `RmwZenohBackend` is used by default (more established, backwards compatible).
147+
148+
To use the ros2dds backend, explicitly specify it:
149+
150+
```rust,ignore
151+
let publisher = node
152+
.create_pub::<RosString>("chatter")
153+
.with_backend::<Ros2DdsBackend>() // Explicit opt-in
154+
.build()?;
155+
```
156+
157+
**Rationale**: The `rmw-zenoh` backend is more established and maintains backwards compatibility with existing ros-z deployments. The `ros2dds` backend requires explicit opt-in to ensure users are aware they're using bridge-compatible key expressions.
158+
111159
## Architecture Diagrams
112160

113161
### RmwZenoh Backend Architecture

ros-z/src/backend/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,9 @@ pub type DefaultBackend = rmw_zenoh::RmwZenohBackend;
7878
#[cfg(all(feature = "ros2dds", not(feature = "rmw-zenoh")))]
7979
pub type DefaultBackend = ros2dds::Ros2DdsBackend;
8080

81-
// When both features are enabled, prefer ros2dds
81+
// When both features are enabled, prefer rmw-zenoh (more established, backwards compatible)
8282
#[cfg(all(feature = "rmw-zenoh", feature = "ros2dds"))]
83-
pub type DefaultBackend = ros2dds::Ros2DdsBackend;
83+
pub type DefaultBackend = rmw_zenoh::RmwZenohBackend;
8484

8585
// When no backend feature is enabled, default to rmw_zenoh
8686
#[cfg(not(any(feature = "rmw-zenoh", feature = "ros2dds")))]

ros-z/src/backend/rmw_zenoh.rs

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,3 +239,205 @@ impl KeyExprBackend for RmwZenohBackend {
239239
Ok((false, qos))
240240
}
241241
}
242+
243+
#[cfg(test)]
244+
mod tests {
245+
use super::*;
246+
use crate::entity::{EndpointEntity, EntityKind, NodeEntity, TypeInfo};
247+
use crate::qos::{QosDurability, QosHistory, QosProfile, QosReliability};
248+
249+
#[test]
250+
fn test_mangle_demangle() {
251+
assert_eq!(RmwZenohBackend::mangle_name("/chatter"), "%chatter");
252+
assert_eq!(
253+
RmwZenohBackend::mangle_name("std_msgs/msg/String"),
254+
"std_msgs%msg%String"
255+
);
256+
assert_eq!(
257+
RmwZenohBackend::demangle_name("std_msgs%msg%String"),
258+
"std_msgs/msg/String"
259+
);
260+
}
261+
262+
#[test]
263+
fn test_qos_encode_decode() {
264+
let qos = QosProfile::default();
265+
let encoded = RmwZenohBackend::encode_qos(&qos, false);
266+
267+
let (keyless, decoded) = RmwZenohBackend::decode_qos(&encoded).unwrap();
268+
assert!(!keyless);
269+
assert_eq!(decoded.reliability, qos.reliability);
270+
assert_eq!(decoded.durability, qos.durability);
271+
}
272+
273+
#[test]
274+
fn test_qos_reliable_transient() {
275+
let qos = QosProfile {
276+
reliability: QosReliability::Reliable,
277+
durability: QosDurability::TransientLocal,
278+
history: QosHistory::from_depth(10),
279+
..Default::default()
280+
};
281+
let encoded = RmwZenohBackend::encode_qos(&qos, false);
282+
283+
let (keyless, decoded) = RmwZenohBackend::decode_qos(&encoded).unwrap();
284+
assert!(!keyless);
285+
assert_eq!(decoded.reliability, QosReliability::Reliable);
286+
assert_eq!(decoded.durability, QosDurability::TransientLocal);
287+
}
288+
289+
/// Test topic key expression format matches rmw_zenoh.
290+
///
291+
/// rmw_zenoh format: `<domain_id>/<topic>/<type>/<hash>`
292+
#[test]
293+
fn test_topic_key_expr_format() {
294+
let zid: zenoh::session::ZenohId = "1234567890abcdef1234567890abcdef".parse().unwrap();
295+
let node = NodeEntity::new(0, zid, 0, "test_node".to_string(), "/".to_string(), String::new());
296+
297+
let entity = EndpointEntity {
298+
id: 1,
299+
node,
300+
kind: EntityKind::Publisher,
301+
topic: "chatter".to_string(),
302+
type_info: Some(TypeInfo::new("std_msgs/msg/String", TypeHash::zero())),
303+
qos: QosProfile::default(),
304+
};
305+
306+
let topic_ke = RmwZenohBackend::topic_key_expr(&entity).unwrap();
307+
let ke_str = topic_ke.as_str();
308+
309+
// rmw_zenoh format: <domain_id>/<topic>/<type>/<hash>
310+
assert!(ke_str.starts_with("0/"), "Should start with domain ID '0/', got: {}", ke_str);
311+
assert!(ke_str.contains("/chatter/"), "Should contain '/chatter/', got: {}", ke_str);
312+
// Note: rmw_zenoh does NOT escape slashes in topic key expression
313+
assert!(ke_str.contains("std_msgs/msg/String"), "Should contain type name, got: {}", ke_str);
314+
}
315+
316+
/// Test liveliness key expression format matches rmw_zenoh.
317+
///
318+
/// Format: `@ros2_lv/<domain>/<zid>/<nid>/<eid>/<kind>/<enclave>/<namespace>/<node_name>/<topic>/<type>/<hash>/<qos>`
319+
#[test]
320+
fn test_liveliness_key_expr_format() {
321+
let zid: zenoh::session::ZenohId = "1234567890abcdef1234567890abcdef".parse().unwrap();
322+
let node = NodeEntity::new(0, zid, 0, "test_node".to_string(), "/".to_string(), String::new());
323+
324+
let entity = EndpointEntity {
325+
id: 1,
326+
node,
327+
kind: EntityKind::Publisher,
328+
topic: "chatter".to_string(),
329+
type_info: Some(TypeInfo::new("std_msgs/msg/String", TypeHash::zero())),
330+
qos: QosProfile::default(),
331+
};
332+
333+
let liveliness_ke = RmwZenohBackend::liveliness_key_expr(&entity, &zid).unwrap();
334+
let ke_str = liveliness_ke.as_str();
335+
336+
// Should start with @ros2_lv
337+
assert!(ke_str.starts_with("@ros2_lv/"), "Should start with '@ros2_lv/', got: {}", ke_str);
338+
339+
// Should contain domain ID
340+
assert!(ke_str.contains("/0/"), "Should contain domain '/0/', got: {}", ke_str);
341+
342+
// Should contain MP for Publisher
343+
assert!(ke_str.contains("/MP/"), "Should contain '/MP/' for Publisher, got: {}", ke_str);
344+
345+
// Should contain node name
346+
assert!(ke_str.contains("/test_node/"), "Should contain '/test_node/', got: {}", ke_str);
347+
348+
// Should contain escaped topic name
349+
assert!(ke_str.contains("/chatter/"), "Should contain '/chatter/', got: {}", ke_str);
350+
351+
// Should contain escaped type name
352+
assert!(
353+
ke_str.contains("std_msgs%msg%String"),
354+
"Should contain 'std_msgs%msg%String', got: {}",
355+
ke_str
356+
);
357+
}
358+
359+
/// Test subscriber liveliness key expression
360+
#[test]
361+
fn test_subscriber_liveliness_key_expr() {
362+
let zid: zenoh::session::ZenohId = "1234567890abcdef1234567890abcdef".parse().unwrap();
363+
let node = NodeEntity::new(0, zid, 0, "test_node".to_string(), "/".to_string(), String::new());
364+
365+
let entity = EndpointEntity {
366+
id: 1,
367+
node,
368+
kind: EntityKind::Subscription,
369+
topic: "chatter".to_string(),
370+
type_info: Some(TypeInfo::new("std_msgs/msg/String", TypeHash::zero())),
371+
qos: QosProfile::default(),
372+
};
373+
374+
let liveliness_ke = RmwZenohBackend::liveliness_key_expr(&entity, &zid).unwrap();
375+
let ke_str = liveliness_ke.as_str();
376+
377+
// Should contain MS for Subscription
378+
assert!(ke_str.contains("/MS/"), "Should contain '/MS/' for Subscription, got: {}", ke_str);
379+
}
380+
381+
/// Test service server liveliness key expression
382+
#[test]
383+
fn test_service_liveliness_key_expr() {
384+
let zid: zenoh::session::ZenohId = "1234567890abcdef1234567890abcdef".parse().unwrap();
385+
let node = NodeEntity::new(0, zid, 0, "test_node".to_string(), "/".to_string(), String::new());
386+
387+
let entity = EndpointEntity {
388+
id: 1,
389+
node,
390+
kind: EntityKind::Service,
391+
topic: "add_two_ints".to_string(),
392+
type_info: Some(TypeInfo::new("example_interfaces/srv/AddTwoInts", TypeHash::zero())),
393+
qos: QosProfile::default(),
394+
};
395+
396+
let liveliness_ke = RmwZenohBackend::liveliness_key_expr(&entity, &zid).unwrap();
397+
let ke_str = liveliness_ke.as_str();
398+
399+
// Should contain SS for Service
400+
assert!(ke_str.contains("/SS/"), "Should contain '/SS/' for Service, got: {}", ke_str);
401+
}
402+
403+
/// Test client liveliness key expression
404+
#[test]
405+
fn test_client_liveliness_key_expr() {
406+
let zid: zenoh::session::ZenohId = "1234567890abcdef1234567890abcdef".parse().unwrap();
407+
let node = NodeEntity::new(0, zid, 0, "test_node".to_string(), "/".to_string(), String::new());
408+
409+
let entity = EndpointEntity {
410+
id: 1,
411+
node,
412+
kind: EntityKind::Client,
413+
topic: "add_two_ints".to_string(),
414+
type_info: Some(TypeInfo::new("example_interfaces/srv/AddTwoInts", TypeHash::zero())),
415+
qos: QosProfile::default(),
416+
};
417+
418+
let liveliness_ke = RmwZenohBackend::liveliness_key_expr(&entity, &zid).unwrap();
419+
let ke_str = liveliness_ke.as_str();
420+
421+
// Should contain SC for Client
422+
assert!(ke_str.contains("/SC/"), "Should contain '/SC/' for Client, got: {}", ke_str);
423+
}
424+
425+
/// Test node liveliness key expression
426+
#[test]
427+
fn test_node_liveliness_key_expr() {
428+
let zid: zenoh::session::ZenohId = "1234567890abcdef1234567890abcdef".parse().unwrap();
429+
let node = NodeEntity::new(0, zid, 0, "test_node".to_string(), "/my_namespace".to_string(), String::new());
430+
431+
let liveliness_ke = RmwZenohBackend::node_liveliness_key_expr(&node).unwrap();
432+
let ke_str = liveliness_ke.as_str();
433+
434+
// Should start with @ros2_lv
435+
assert!(ke_str.starts_with("@ros2_lv/"), "Should start with '@ros2_lv/', got: {}", ke_str);
436+
437+
// Should contain NN for Node
438+
assert!(ke_str.contains("/NN/"), "Should contain '/NN/' for Node, got: {}", ke_str);
439+
440+
// Should contain node name
441+
assert!(ke_str.contains("/test_node"), "Should contain '/test_node', got: {}", ke_str);
442+
}
443+
}

ros-z/src/backend/ros2dds.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ mod tests {
303303
let qos = QosProfile {
304304
reliability: QosReliability::Reliable,
305305
durability: QosDurability::TransientLocal,
306-
history: QosHistory::KeepLast(10),
306+
history: QosHistory::from_depth(10),
307307
..Default::default()
308308
};
309309
let encoded = Ros2DdsBackend::encode_qos(&qos, false);
@@ -373,7 +373,7 @@ mod tests {
373373

374374
// Test KEEP_LAST with depth -> "0,depth"
375375
let qos = QosProfile {
376-
history: QosHistory::KeepLast(5),
376+
history: QosHistory::from_depth(5),
377377
..Default::default()
378378
};
379379
let encoded = Ros2DdsBackend::encode_qos(&qos, false);
@@ -397,7 +397,7 @@ mod tests {
397397
#[test]
398398
fn test_topic_key_expr_format() {
399399
let zid: zenoh::session::ZenohId = "1234567890abcdef1234567890abcdef".parse().unwrap();
400-
let node = NodeEntity::new(0, zid, 1, "test_node".to_string(), "/".to_string());
400+
let node = NodeEntity::new(0, zid, 1, "test_node".to_string(), "/".to_string(), String::new());
401401

402402
let entity = EndpointEntity {
403403
id: 1,
@@ -419,7 +419,7 @@ mod tests {
419419
#[test]
420420
fn test_liveliness_key_expr_format() {
421421
let zid: zenoh::session::ZenohId = "1234567890abcdef1234567890abcdef".parse().unwrap();
422-
let node = NodeEntity::new(0, zid, 1, "test_node".to_string(), "/".to_string());
422+
let node = NodeEntity::new(0, zid, 1, "test_node".to_string(), "/".to_string(), String::new());
423423

424424
let entity = EndpointEntity {
425425
id: 1,
@@ -455,7 +455,7 @@ mod tests {
455455
#[test]
456456
fn test_subscriber_liveliness_key_expr() {
457457
let zid: zenoh::session::ZenohId = "1234567890abcdef1234567890abcdef".parse().unwrap();
458-
let node = NodeEntity::new(0, zid, 1, "test_node".to_string(), "/".to_string());
458+
let node = NodeEntity::new(0, zid, 1, "test_node".to_string(), "/".to_string(), String::new());
459459

460460
let entity = EndpointEntity {
461461
id: 1,
@@ -477,7 +477,7 @@ mod tests {
477477
#[test]
478478
fn test_service_liveliness_key_expr() {
479479
let zid: zenoh::session::ZenohId = "1234567890abcdef1234567890abcdef".parse().unwrap();
480-
let node = NodeEntity::new(0, zid, 1, "test_node".to_string(), "/".to_string());
480+
let node = NodeEntity::new(0, zid, 1, "test_node".to_string(), "/".to_string(), String::new());
481481

482482
let entity = EndpointEntity {
483483
id: 1,
@@ -510,7 +510,7 @@ mod tests {
510510
#[test]
511511
fn test_client_liveliness_key_expr() {
512512
let zid: zenoh::session::ZenohId = "1234567890abcdef1234567890abcdef".parse().unwrap();
513-
let node = NodeEntity::new(0, zid, 1, "test_node".to_string(), "/".to_string());
513+
let node = NodeEntity::new(0, zid, 1, "test_node".to_string(), "/".to_string(), String::new());
514514

515515
let entity = EndpointEntity {
516516
id: 1,

ros-z/src/dynamic/registry.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ pub fn parsed_message_to_schema(
110110
msg: &ros_z_codegen::types::ParsedMessage,
111111
resolver: &impl Fn(&str, &str) -> Option<Arc<MessageSchema>>,
112112
) -> Result<Arc<MessageSchema>, DynamicError> {
113-
let fields: Result<Vec<FieldSchema>, _> = msg
113+
let fields: Result<Vec<FieldSchema>, DynamicError> = msg
114114
.fields
115115
.iter()
116116
.map(|f| {
@@ -169,10 +169,10 @@ fn convert_base_type(
169169
}
170170

171171
// Check for bounded string
172-
if base_type.starts_with("string<=") {
173-
if let Ok(max_len) = base_type[8..].parse::<usize>() {
174-
return Ok(FieldType::BoundedString(max_len));
175-
}
172+
if let Some(rest) = base_type.strip_prefix("string<=")
173+
&& let Ok(max_len) = rest.parse::<usize>()
174+
{
175+
return Ok(FieldType::BoundedString(max_len));
176176
}
177177

178178
// It's a message type - resolve it

0 commit comments

Comments
 (0)