Introduction
ros-z is a native Rust ROS 2 implementation powered by Zenoh, delivering high-performance robotics communication with type safety and zero-cost abstractions. Build reliable robot applications using modern Rust idioms while maintaining full ROS 2 compatibility.
Architecture
ros-z provides three integration paths to suit different use cases:
Why Choose ros-z?
| Feature | Description | Benefit |
|---|---|---|
| Native Rust | Pure Rust implementation with no C/C++ dependencies | Memory safety, concurrency without data races |
| Zenoh Transport | High-performance pub-sub engine | Low latency, efficient bandwidth usage |
| ROS 2 Compatible | Works seamlessly with standard ROS 2 tools | Integrate with existing robotics ecosystems |
| Multiple Serializations | Support for various data representations: CDR (ROS default), Protobuf | Flexible message encoding for different performance and interoperability needs |
| Type Safety | Compile-time message validation | Catch errors before deployment |
| Modern API | Idiomatic Rust patterns | Ergonomic developer experience |
| Safety First | Ownership model prevents common bugs | No data races, null pointers, or buffer overflows at compile time |
| High Productivity | Cargo ecosystem with excellent tooling | Fast development without sacrificing reliability |
ros-z is designed for both new projects and gradual migration. Deploy ros-z nodes alongside existing ROS 2 C++/Python nodes with full interoperability.
Communication Patterns
ros-z supports all essential ROS 2 communication patterns:
| Pattern | Use Case | Learn More |
|---|---|---|
| Pub/Sub | Continuous data streaming, sensor data, status updates | Pub/Sub |
| Services | Request-response operations, remote procedure calls | Services |
| Actions | Long-running tasks with feedback and cancellation support | Actions |
Start with pub/sub for data streaming, use services for request-response operations, and leverage actions for long-running tasks that need progress feedback.
Ergonomic API Design
ros-z provides flexible, idiomatic Rust APIs that adapt to your preferred programming style:
Flexible Builder Pattern:
let pub = node.create_pub::<Vector3>("vector")
// Quality of Service settings
.with_qos(QosProfile {
reliability: QosReliability::Reliable,
..Default::default()
})
// custom serialization
.with_serdes::<ProtobufSerdes<Vector3>>()
.build()?;
Async & Sync Patterns:
// Publishers: sync and async variants
zpub.publish(&msg)?;
zpub.async_publish(&msg).await?;
// Subscribers: sync and async receiving
let msg = zsub.recv()?;
let msg = zsub.async_recv().await?;
Callback or Polling Style for Subscribers:
// Callback style - process messages with a closure
let sub = node.create_sub::<RosString>("topic")
.build_with_callback(|msg| {
println!("Received: {}", msg);
})?;
// Polling style - receive messages on demand
let sub = node.create_sub::<RosString>("topic").build()?;
while let Ok(msg) = sub.recv() {
println!("Received: {}", msg);
}
Next Step
Ready to build safer, faster robotics applications? Start with the Quick Start Guide.
Quick Start
Get ros-z running in under 5 minutes with this hands-on tutorial. Build a complete publisher-subscriber system to understand the core concepts through working code.
This guide assumes basic Rust knowledge. If you're new to Rust, complete the Rust Book first for the best experience.
Setup
Add ros-z dependencies to your Cargo.toml:
[dependencies]
ros-z = "*"
ros-z-msgs = "*" # Standard ROS 2 message types
tokio = { version = "1", features = ["full"] } # Async runtime
An async runtime is required for ros-z. This example uses Tokio, the most popular choice in the Rust ecosystem.
Your 1st Example
Here's a complete publisher and subscriber in one application:
use std::time::Duration;
use ros_z::{
Builder, Result,
context::{ZContext, ZContextBuilder},
};
use ros_z_msgs::std_msgs::String as RosString;
/// Subscriber function that continuously receives messages from a topic
async fn run_subscriber(ctx: ZContext, topic: String) -> Result<()> {
// Create a ROS 2 node - the fundamental unit of computation
// Nodes are logical groupings of publishers, subscribers, services, etc.
let node = ctx.create_node("Sub").build()?;
// Create a subscriber for the specified topic
// The type parameter RosString determines what message type we'll receive
let zsub = node.create_sub::<RosString>(&topic).build()?;
// Continuously receive messages asynchronously
// This loop will block waiting for messages on the topic
while let Ok(msg) = zsub.async_recv().await {
println!("Hearing:>> {}", msg.data);
}
Ok(())
}
/// Publisher function that continuously publishes messages to a topic
async fn run_publisher(
ctx: ZContext,
topic: String,
period: Duration,
payload: String,
) -> Result<()> {
// Create a ROS 2 node for publishing
let node = ctx.create_node("Pub").build()?;
// Create a publisher for the specified topic
// The type parameter RosString determines what message type we'll send
let zpub = node.create_pub::<RosString>(&topic).build()?;
let mut count = 0;
loop {
// Create a new message with incrementing counter
let str = RosString {
data: format!("{payload} - #{count}"),
};
println!("Telling:>> {}", str.data);
// Publish the message asynchronously to all subscribers on this topic
zpub.async_publish(&str).await?;
// Wait for the specified period before publishing again
let _ = tokio::time::sleep(period).await;
count += 1;
}
}
// The #[tokio::main] attribute sets up the async runtime
// ros-z requires an async runtime (Tokio is the most common choice)
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
// Create a ZContext - the entry point for ros-z applications
// ZContext manages the connection to the Zenoh network and coordinates
// communication between nodes. It can be configured with different modes:
// - "peer" mode: nodes discover each other via multicast scouting
// - "client" mode: nodes connect to a Zenoh router
let ctx = if let Some(e) = args.endpoint {
ZContextBuilder::default()
.with_mode(args.mode)
.with_connect_endpoints([e])
.build()?
} else {
ZContextBuilder::default().with_mode(args.mode).build()?
};
let period = std::time::Duration::from_secs_f64(args.period);
zenoh::init_log_from_env_or("error");
// Run as either a publisher (talker) or subscriber (listener)
// Both share the same ZContext but perform different roles
if args.role == "listener" {
run_subscriber(ctx, args.topic).await?;
} else if args.role == "talker" {
run_publisher(ctx, args.topic, period, args.data).await?;
} else {
println!(
"Please use \"talker\" or \"listener\" as role, {} is not supported.",
args.role
);
}
Ok(())
}
use clap::Parser;
#[derive(Debug, Parser)]
struct Args {
#[arg(short, long, default_value = "Hello ROS-Z")]
data: String,
#[arg(short, long, default_value = "/chatter")]
topic: String,
#[arg(short, long, default_value = "1.0")]
period: f64,
#[arg(short, long, default_value = "listener")]
role: String,
#[arg(short, long, default_value = "peer")]
mode: String,
#[arg(short, long)]
endpoint: Option<String>,
}
Key Components
| Component | Purpose | Usage |
|---|---|---|
| ZContextBuilder | Initialize ros-z environment | Entry point, configure settings |
| ZContext | Manages ROS 2 connections | Create nodes from this |
| Node | Logical unit of computation | Publishers/subscribers attach here |
| Publisher | Sends messages to topics | node.create_pub::<Type>("topic") |
| Subscriber | Receives messages from topics | node.create_sub::<Type>("topic") |
Running the Application
ros-z uses a router-based architecture (matching ROS 2's rmw_zenoh), so you'll need to start a Zenoh router first.
Open three terminal windows and run:
Terminal 1 - Start the Zenoh Router:
cargo run --example zenoh_router
Terminal 2 - Start the Listener:
cargo run --example z_pubsub -- -r listener
Terminal 3 - Start the Talker:
cargo run --example z_pubsub -- -r talker
You should see the listener receiving messages published by the talker in real-time. Press Ctrl+C to stop any process.
ros-z now uses router-based discovery by default, aligning with ROS 2's official Zenoh middleware (rmw_zenoh_cpp). This provides:
- Better scalability for large deployments with many nodes
- Lower network overhead compared to multicast discovery
- Production-ready architecture used in real ROS 2 systems
See the Zenoh Configuration chapter for customization options, including how to revert to multicast scouting mode if needed.
What's Happening?
sequenceDiagram
participant T as Talker
participant Z as Zenoh Network
participant L as Listener
T->>Z: Publish "Hello 0"
Z->>L: Deliver message
L->>L: Print to console
Note over T: Wait 1 second
T->>Z: Publish "Hello 1"
Z->>L: Deliver message
L->>L: Print to console
The talker publishes messages every second to the /chatter topic. The listener subscribes to the same topic and prints each received message. Zenoh handles the network transport transparently.
Both nodes run independently. You can start/stop them in any order, and multiple listeners can receive from one talker simultaneously.
Next Steps
Now that you understand the basics:
Core Concepts:
- Pub/Sub - Deep dive into pub-sub patterns and QoS
- Services - Request-response communication
- Actions - Long-running tasks with feedback
- Message Generation - How message types work
- Custom Messages - Define your own message types
Development:
- Building - Build configurations and dependencies
- Networking - Zenoh router setup and options
Publishers and Subscribers
ros-z implements ROS 2's publish-subscribe pattern with type-safe, zero-copy messaging over Zenoh. This enables efficient, decoupled communication between nodes with minimal overhead.
The pub-sub pattern forms the foundation of ROS 2 communication, allowing nodes to exchange data without direct coupling. ros-z leverages Zenoh's efficient transport layer for optimal performance.
Visual Flow
graph TD
A[ZContextBuilder] -->|configure| B[ZContext]
B -->|create| C[Node]
C -->|publisher| D[Publisher]
C -->|subscriber| E[Subscriber]
D -->|publish| F[Topic]
F -->|deliver| E
E -->|callback| G[Message Handler]
Key Features
| Feature | Description | Benefit |
|---|---|---|
| Type Safety | Strongly-typed messages using Rust structs | Compile-time error detection |
| Zero-Copy | Efficient message passing via Zenoh | Reduced latency and CPU usage |
| QoS Profiles | Configurable reliability, durability, history | Fine-grained delivery control |
| Async/Blocking | Dual API for both paradigms | Flexible integration patterns |
Publisher Example
This example demonstrates publishing "Hello World" messages to a topic. The publisher sends messages periodically, showcasing the fundamental publishing pattern.
use std::time::Duration;
use ros_z::{
Builder, Result,
context::ZContext,
qos::{QosHistory, QosProfile},
};
use ros_z_msgs::std_msgs::String as RosString;
/// Talker node that publishes "Hello World" messages to a topic
///
/// # Arguments
/// * `ctx` - The ROS-Z context
/// * `topic` - The topic name to publish to
/// * `period` - Duration between messages
/// * `max_count` - Optional maximum number of messages to publish. If None, publishes indefinitely.
pub async fn run_talker(
ctx: ZContext,
topic: &str,
period: Duration,
max_count: Option<usize>,
) -> Result<()> {
// Create a node named "talker"
let node = ctx.create_node("talker").build()?;
// Create a publisher with a custom Quality of Service profile
let qos = QosProfile {
history: QosHistory::KeepLast(7),
..Default::default()
};
let publisher = node.create_pub::<RosString>(topic).with_qos(qos).build()?;
let mut count = 1;
loop {
// Create the message
let msg = RosString {
data: format!("Hello World: {}", count),
};
// Log the message being published
println!("Publishing: '{}'", msg.data);
// Publish the message (non-blocking)
publisher.async_publish(&msg).await?;
// Check if we've reached the max count
if let Some(max) = max_count
&& count >= max
{
break;
}
// Wait for the next publish cycle
tokio::time::sleep(period).await;
count += 1;
}
Ok(())
}
Key points:
- QoS Configuration: Uses
KeepLast(7)to buffer the last 7 messages - Async Publishing: Non-blocking
async_publish()for efficient I/O - Rate Control: Uses
tokio::time::sleep()to control publishing frequency - Bounded Operation: Optional
max_countfor testing scenarios
Running the publisher:
# Basic usage
cargo run --example demo_nodes_talker
# Custom topic and rate
cargo run --example demo_nodes_talker -- --topic /my_topic --period 0.5
# Publish 10 messages then exit
cargo run --example demo_nodes_talker -- --max-count 10
Subscriber Example
This example demonstrates subscribing to messages from a topic. The subscriber receives and displays messages, showing both timeout-based and async reception patterns.
use std::time::Duration;
use ros_z::{
Builder, Result,
context::ZContext,
qos::{QosHistory, QosProfile},
};
use ros_z_msgs::std_msgs::String as RosString;
/// Listener node that subscribes to a topic
///
/// # Arguments
/// * `ctx` - The ROS-Z context
/// * `topic` - The topic name to subscribe to
/// * `max_count` - Optional maximum number of messages to receive. If None, listens indefinitely.
/// * `timeout` - Optional timeout duration. If None, waits indefinitely.
///
/// # Returns
/// A vector of received messages
pub async fn run_listener(
ctx: ZContext,
topic: &str,
max_count: Option<usize>,
timeout: Option<Duration>,
) -> Result<Vec<String>> {
// Create a node named "listener"
let node = ctx.create_node("listener").build()?;
// Create a subscription to the "chatter" topic
let qos = QosProfile {
history: QosHistory::KeepLast(10),
..Default::default()
};
let subscriber = node.create_sub::<RosString>(topic).with_qos(qos).build()?;
let mut received_messages = Vec::new();
let start = std::time::Instant::now();
// Receive messages in a loop
loop {
// Check timeout
if let Some(t) = timeout
&& start.elapsed() > t
{
break;
}
// Try to receive with a small timeout to allow checking other conditions
let recv_result = if timeout.is_some() || max_count.is_some() {
subscriber.recv_timeout(Duration::from_millis(100))
} else {
// If no limits, use async_recv
subscriber.async_recv().await
};
match recv_result {
Ok(msg) => {
// Log the received message
println!("I heard: [{}]", msg.data);
received_messages.push(msg.data.clone());
// Check if we've received enough messages
if let Some(max) = max_count
&& received_messages.len() >= max
{
break;
}
}
Err(_) => {
// Continue if timeout on recv_timeout
if timeout.is_some() || max_count.is_some() {
continue;
} else {
break;
}
}
}
}
Ok(received_messages)
}
Key points:
- Flexible Reception: Supports timeout-based and indefinite blocking
- Testable Design: Returns received messages for verification
- Bounded Operation: Optional
max_countandtimeoutparameters - QoS Configuration: Uses
KeepLast(10)for message buffering
Running the subscriber:
# Basic usage
cargo run --example demo_nodes_listener
# Custom topic
cargo run --example demo_nodes_listener -- --topic /my_topic
# Receive 5 messages then exit
cargo run --example demo_nodes_listener -- --max-count 5
Complete Pub-Sub Workflow
To see publishers and subscribers in action together, you'll need to start a Zenoh router first:
Terminal 1 - Start Zenoh Router:
cargo run --example zenoh_router
Terminal 2 - Start Subscriber:
cargo run --example demo_nodes_listener
Terminal 3 - Start Publisher:
cargo run --example demo_nodes_talker
Subscriber Patterns
ros-z provides three patterns for receiving messages, each suited for different use cases:
Pattern 1: Blocking Receive (Pull Model)
Best for: Simple sequential processing, scripting
let subscriber = node
.create_sub::<RosString>("topic_name")
.build()?;
while let Ok(msg) = subscriber.recv() {
println!("Received: {}", msg.data);
}
Pattern 2: Async Receive (Pull Model)
Best for: Integration with async codebases, handling multiple streams
let subscriber = node
.create_sub::<RosString>("topic_name")
.build()?;
while let Ok(msg) = subscriber.async_recv().await {
println!("Received: {}", msg.data);
}
Pattern 3: Callback (Push Model)
Best for: Event-driven architectures, low-latency response
let subscriber = node
.create_sub::<RosString>("topic_name")
.build_with_callback(|msg| {
println!("Received: {}", msg.data);
})?;
// No need to call recv() - callback handles messages automatically
// Your code continues while messages are processed in the background
Use callbacks for low-latency event-driven processing. Use blocking/async receive when you need explicit control over when messages are processed.
Pattern Comparison
| Aspect | Blocking Receive | Async Receive | Callback |
|---|---|---|---|
| Control Flow | Sequential | Sequential | Event-driven |
| Latency | Medium (poll-based) | Medium (poll-based) | Low (immediate) |
| Memory | Queue size × message | Queue size × message | No queue |
| Backpressure | Built-in (queue full) | Built-in (queue full) | None (drops if slow) |
| Use Case | Simple scripts | Async applications | Real-time response |
Quality of Service (QoS)
QoS profiles control message delivery behavior:
use ros_z::qos::{QosProfile, QosHistory, Reliability};
let qos = QosProfile {
history: QosHistory::KeepLast(10),
reliability: Reliability::Reliable,
..Default::default()
};
let publisher = node
.create_pub::<RosString>("topic")
.with_qos(qos)
.build()?;
Use QosHistory::KeepLast(1) for sensor data and Reliability::Reliable for critical commands. Match QoS profiles between publishers and subscribers for optimal message delivery.
ROS 2 Interoperability
ros-z publishers and subscribers work seamlessly with ROS 2 C++ and Python nodes:
# List active topics
ros2 topic list
# Echo messages from ros-z publisher
ros2 topic echo /chatter
# Publish to ros-z subscriber from ROS 2
ros2 topic pub /chatter std_msgs/msg/String "data: 'Hello from ROS 2'"
# Check topic info
ros2 topic info /chatter
ros-z provides full ROS 2 compatibility via Zenoh bridge or rmw_zenoh, enabling cross-language communication.
Resources
- Custom Messages - Defining and using custom message types
- Message Generation - Generating Rust types from ROS 2 messages
- Quick Start - Getting started guide
Start with the examples above to understand the basic pub-sub workflow, then explore custom messages for domain-specific communication.
Services
ros-z implements ROS 2's service pattern with type-safe request-response communication over Zenoh. This enables synchronous, point-to-point interactions between nodes using a pull-based model for full control over request processing.
Services provide request-response communication for operations that need immediate feedback. Unlike topics, services are bidirectional and ensure a response for each request. ros-z uses a pull model that gives you explicit control over when to process requests.
Visual Flow
graph TD
A[ZContextBuilder] -->|configure| B[ZContext]
B -->|create| C[Client Node]
B -->|create| D[Server Node]
C -->|create_client| E[Service Client]
D -->|create_service| F[Service Server]
E -->|send_request| G[Service Call]
G -->|route| F
F -->|take_request| H[Request Handler]
H -->|send_response| G
G -->|deliver| E
E -->|take_response| I[Response Handler]
Key Features
| Feature | Description | Benefit |
|---|---|---|
| Type Safety | Strongly-typed service definitions with Rust structs | Compile-time error detection |
| Pull Model | Explicit control over request processing timing | Predictable concurrency and backpressure |
| Async/Blocking | Dual API for both paradigms | Flexible integration patterns |
| Request Tracking | Key-based request/response matching | Reliable message correlation |
Service Server Example
This example demonstrates a service server that adds two integers. The server waits for requests, processes them, and sends responses back to clients.
use ros_z::{Builder, Result, context::ZContext};
use ros_z_msgs::example_interfaces::{AddTwoIntsResponse, srv::AddTwoInts};
/// AddTwoInts server node that provides a service to add two integers
///
/// # Arguments
/// * `ctx` - The ROS-Z context
/// * `max_requests` - Optional maximum number of requests to handle. If None, handles requests indefinitely.
pub fn run_add_two_ints_server(ctx: ZContext, max_requests: Option<usize>) -> Result<()> {
// Create a node named "add_two_ints_server"
let node = ctx.create_node("add_two_ints_server").build()?;
// Create a service that will handle requests
let mut service = node.create_service::<AddTwoInts>("add_two_ints").build()?;
println!("AddTwoInts service server started, waiting for requests...");
let mut request_count = 0;
loop {
// Wait for a request
let (key, req) = service.take_request()?;
println!("Incoming request\na: {} b: {}", req.a, req.b);
// Compute the sum
let sum = req.a + req.b;
// Create the response
let resp = AddTwoIntsResponse { sum };
println!("Sending response: {}", resp.sum);
// Send the response
service.send_response(&resp, &key)?;
request_count += 1;
// Check if we've reached the max requests
if let Some(max) = max_requests
&& request_count >= max
{
break;
}
}
Ok(())
}
Key points:
- Pull Model: Uses
take_request()for explicit control over when to accept requests - Request Key: Each request has a unique key for matching responses
- Bounded Operation: Optional
max_requestsparameter for testing - Simple Processing: Demonstrates synchronous request handling
Running the server:
# Basic usage - runs indefinitely
cargo run --example demo_nodes_add_two_ints_server
# Handle 5 requests then exit
cargo run --example demo_nodes_add_two_ints_server -- --count 5
# Connect to specific Zenoh router
cargo run --example demo_nodes_add_two_ints_server -- --endpoint tcp/localhost:7447
Service Client Example
This example demonstrates a service client that sends addition requests to the server and displays the results.
use std::time::Duration;
use ros_z::{Builder, Result, context::ZContext};
use ros_z_msgs::example_interfaces::{AddTwoIntsRequest, srv::AddTwoInts};
/// AddTwoInts client node that calls the service to add two integers
///
/// # Arguments
/// * `ctx` - The ROS-Z context
/// * `a` - First number to add
/// * `b` - Second number to add
/// * `async_mode` - Whether to use async response waiting
pub fn run_add_two_ints_client(ctx: ZContext, a: i64, b: i64, async_mode: bool) -> Result<i64> {
// Create a node named "add_two_ints_client"
let node = ctx.create_node("add_two_ints_client").build()?;
// Create a client for the service
let client = node.create_client::<AddTwoInts>("add_two_ints").build()?;
println!(
"AddTwoInts service client started (mode: {})",
if async_mode { "async" } else { "sync" }
);
// Create the request
let req = AddTwoIntsRequest { a, b };
println!("Sending request: {} + {}", req.a, req.b);
// Wait for the response
let resp = if async_mode {
tokio::runtime::Runtime::new().unwrap().block_on(async {
client.send_request(&req).await?;
client.take_response_async().await
})?
} else {
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async { client.send_request(&req).await })?;
client.take_response_timeout(Duration::from_secs(5))?
};
println!("Received response: {}", resp.sum);
Ok(resp.sum)
}
Key points:
- Async Support: Supports both blocking and async response patterns
- Timeout Handling: Uses
take_response_timeout()for reliable operation - Simple API: Send request, receive response, process result
- Type Safety: Request and response types are enforced at compile time
Running the client:
# Basic usage
cargo run --example demo_nodes_add_two_ints_client -- --a 10 --b 20
# Using async mode
cargo run --example demo_nodes_add_two_ints_client -- --a 5 --b 3 --async-mode
# Connect to specific Zenoh router
cargo run --example demo_nodes_add_two_ints_client -- --a 100 --b 200 --endpoint tcp/localhost:7447
Complete Service Workflow
To see services in action, you'll need to start a Zenoh router first:
Terminal 1 - Start Zenoh Router:
cargo run --example zenoh_router
Terminal 2 - Start Server:
cargo run --example demo_nodes_add_two_ints_server
Terminal 3 - Send Client Requests:
# Request 1
cargo run --example demo_nodes_add_two_ints_client -- --a 10 --b 20
# Request 2
cargo run --example demo_nodes_add_two_ints_client -- --a 100 --b 200
Each client request is processed immediately by the server, demonstrating synchronous request-response communication over Zenoh.
Service Server Patterns
Service servers in ros-z follow a pull model pattern, similar to subscribers. You explicitly receive requests when ready to process them, giving you full control over request handling timing and concurrency.
This pull-based approach is consistent with subscriber's recv() pattern, allowing you to control when work happens rather than having callbacks interrupt your flow.
Pattern 1: Blocking Request Handling
Best for: Simple synchronous service implementations
let mut service = node
.create_service::<ServiceType>("service_name")
.build()?;
loop {
let (key, request) = service.take_request()?;
let response = process_request(&request);
service.send_response(&response, &key)?;
}
Pattern 2: Async Request Handling
Best for: Services that need to await other operations
let mut service = node
.create_service::<ServiceType>("service_name")
.build()?;
loop {
let (key, request) = service.take_request_async().await?;
let response = async_process_request(&request).await;
service.send_response(&response, &key)?;
}
Why Pull Model?
| Aspect | Pull Model (take_request) | Push Model (callback) |
|---|---|---|
| Control | Explicit control over when to accept requests | Interrupts current work |
| Concurrency | Easy to reason about | Requires careful synchronization |
| Backpressure | Natural - slow processing slows acceptance | Can overwhelm if processing is slow |
| Consistency | Same pattern as subscriber recv() | Different pattern |
Service Client Patterns
Service clients send requests to servers and receive responses. Both blocking and async patterns are supported.
Pattern 1: Blocking Client
Best for: Simple synchronous request-response operations
let client = node
.create_client::<ServiceType>("service_name")
.build()?;
let request = create_request();
client.send_request(&request)?;
let response = client.take_response()?;
Pattern 2: Async Client
Best for: Integration with async codebases
let client = node
.create_client::<ServiceType>("service_name")
.build()?;
let request = create_request();
client.send_request(&request).await?;
let response = client.take_response_async().await?;
Match your client and server patterns for consistency. Use blocking patterns for simple scripts and async patterns when integrating with async runtimes like tokio.
ROS 2 Interoperability
ros-z services work seamlessly with ROS 2 C++ and Python nodes:
# List available services
ros2 service list
# Call ros-z service from ROS 2 CLI
ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 42, b: 58}"
# Show service type
ros2 service type /add_two_ints
# Get service info
ros2 service info /add_two_ints
ros-z service servers and clients are fully compatible with ROS 2 via Zenoh bridge or rmw_zenoh, enabling cross-language service calls.
Resources
- Custom Messages - Defining and using custom service types
- Message Generation - Generating service definitions
- Actions - For long-running operations with feedback
Start with the examples above to understand the basic service workflow, then explore custom service types for domain-specific operations.
Actions
Actions enable long-running tasks with progress feedback and cancellation support, perfect for operations that take seconds or minutes to complete. Unlike services that return immediately, actions provide streaming feedback while executing complex workflows.
Use actions for robot navigation, trajectory execution, or any operation where you need progress updates and the ability to cancel mid-execution. Use services for quick request-response operations.
Action Lifecycle
stateDiagram-v2
[*] --> Idle
Idle --> Accepted: Send Goal
Accepted --> Executing: Start Processing
Executing --> Executing: Send Feedback
Executing --> Succeeded: Complete
Executing --> Canceled: Cancel Request
Executing --> Aborted: Error Occurs
Succeeded --> [*]
Canceled --> [*]
Aborted --> [*]
Components
| Component | Type | Purpose |
|---|---|---|
| Goal | Input | Defines the desired outcome |
| Feedback | Stream | Progress updates during execution |
| Result | Output | Final outcome when complete |
| Status | State | Current execution state |
Communication Pattern
sequenceDiagram
participant C as Client
participant S as Server
C->>S: Send Goal
S->>C: Goal Accepted
loop During Execution
S->>C: Feedback Update
end
alt Success
S->>C: Result (Success)
else Canceled
C->>S: Cancel Request
S->>C: Result (Canceled)
else Error
S->>C: Result (Aborted)
end
Use Cases
Robot Navigation:
- Goal: Target position and orientation
- Feedback: Current position, distance remaining, obstacles detected
- Result: Final position, success/failure reason
Gripper Control:
- Goal: Desired grip force and position
- Feedback: Current force, contact detection
- Result: Grip achieved, object secured
Long Computations:
- Goal: Computation parameters
- Feedback: Progress percentage, intermediate results
- Result: Final computed value, execution time
Actions excel when operations take more than a few seconds and users need visibility into progress. For sub-second operations, prefer services for simplicity.
Example Patterns
Action Server:
let action_server = node
.create_action_server::<Fibonacci>("/fibonacci")
.build()?;
loop {
let goal = action_server.accept_goal()?;
// Send periodic feedback
for i in 0..goal.order {
action_server.send_feedback(FeedbackMsg {
current: i,
sequence: compute_partial(i)
})?;
}
// Send final result
action_server.send_result(ResultMsg {
sequence: compute_final(goal.order)
})?;
}
Action Client:
let action_client = node
.create_action_client::<Fibonacci>("/fibonacci")
.build()?;
let goal_handle = action_client.send_goal(GoalMsg {
order: 10
}).await?;
while let Some(feedback) = goal_handle.feedback().await {
println!("Progress: {}", feedback.current);
}
let result = goal_handle.get_result().await?;
println!("Final: {:?}", result.sequence);
Always implement timeout mechanisms for action clients. Long-running actions can fail or hang, and clients need graceful degradation strategies.
Comparison with Other Patterns
| Pattern | Duration | Feedback | Cancellation | Use Case |
|---|---|---|---|---|
| Pub-Sub | Continuous | No | N/A | Sensor data streaming |
| Service | < 1 second | No | No | Quick queries |
| Action | Seconds to minutes | Yes | Yes | Long-running tasks |
Resources
- ROS 2 Actions Documentation - Official ROS 2 action guide
- ros-z Examples - Working action implementations
- Services - Simpler request-response pattern
Action implementation is evolving. Check the ros-z repository for the latest examples and API updates.
Building ros-z
ros-z is designed to work without ROS 2 dependencies by default, enabling pure Rust development while optionally integrating with existing ROS 2 installations. This flexible approach lets you choose your dependency level based on project requirements.
Philosophy
ros-z follows a dependency-optional design:
- Build pure Rust applications without ROS 2 installed
- Use bundled message definitions for common types
- Opt-in to ROS 2 integration when needed
- Pay only for what you use
Adding ros-z to Your Project
Get started by adding ros-z to your Cargo.toml. Choose the dependency setup that matches your needs:
Scenario 1: Pure Rust with Custom Messages
Use when: You want to define your own message types without ROS 2 dependencies
Add to your Cargo.toml:
[dependencies]
ros-z = "0.x"
tokio = { version = "1", features = ["full"] } # Async runtime required
What you get:
- Full ros-z functionality
- Custom message support via derive macros
- Zero external dependencies
- Fast build times
Scenario 2: Using Bundled ROS Messages
Use when: You need standard ROS 2 message types (no ROS 2 installation required)
Add to your Cargo.toml:
[dependencies]
ros-z = "0.x"
ros-z-msgs = "0.x" # Includes core_msgs by default
tokio = { version = "1", features = ["full"] }
Default message packages (core_msgs):
std_msgs- Primitive types (String, Int32, Float64, etc.)geometry_msgs- Spatial data (Point, Pose, Transform, Twist)sensor_msgs- Sensor data (LaserScan, Image, Imu, PointCloud2)nav_msgs- Navigation (Path, OccupancyGrid, Odometry)example_interfaces- Tutorial services (AddTwoInts)action_tutorials_interfaces- Tutorial actions (Fibonacci)
Scenario 3: All Message Packages
Use when: You need all available message types including test messages
Requirements: None (all messages are vendored)
Add to your Cargo.toml:
[dependencies]
ros-z = "0.x"
ros-z-msgs = { version = "0.x", features = ["all_msgs"] }
tokio = { version = "1", features = ["full"] }
Build your project:
cargo build
All available packages:
std_msgs- Basic typesgeometry_msgs- Spatial datasensor_msgs- Sensor datanav_msgs- Navigationexample_interfaces- Tutorial services (AddTwoInts)action_tutorials_interfaces- Tutorial actions (Fibonacci)test_msgs- Test types
The default core_msgs feature includes everything except test_msgs. Use all_msgs only if you need test message types.
ROS 2 Distribution Compatibility
ros-z defaults to ROS 2 Jazzy compatibility, which is the recommended distribution for new projects. If you need to target a different distribution like Humble, see the ROS 2 Distribution Compatibility chapter for detailed instructions.
Quick reference:
# Default (Jazzy) - works out of the box
cargo build
# For Humble - use --no-default-features
cargo build --no-default-features --features humble
# For Rolling/Iron - just add the feature
cargo build --features rolling
The distribution choice affects type hash support and interoperability with ROS 2 nodes. See the Distribution Compatibility chapter for full details.
Development
This section is for contributors working on ros-z itself. If you're using ros-z in your project, you can skip this section.
Package Organization
The ros-z repository is organized as a Cargo workspace with multiple packages:
| Package | Default Build | Purpose | Dependencies |
|---|---|---|---|
| ros-z | Yes | Core Zenoh-native ROS 2 library | None |
| ros-z-codegen | Yes | Message generation utilities | None |
| ros-z-msgs | No | Pre-generated message types | None (all vendored) |
| ros-z-tests | No | Integration tests | ros-z-msgs |
| rcl-z | No | RCL C bindings | ROS 2 required |
Only ros-z and ros-z-codegen build by default. Other packages are optional for development, testing, and running examples.
Building the Repository
When contributing to ros-z, you can build different parts of the workspace:
# Build core library
cargo build
# Run tests
cargo test
# Build with bundled messages for examples
cargo build -p ros-z-msgs
# Build all packages (requires ROS 2)
source /opt/ros/jazzy/setup.bash
cargo build --all
Message Package Resolution
The build system automatically locates ROS message definitions:
Search order:
- System ROS installation (
AMENT_PREFIX_PATH,CMAKE_PREFIX_PATH) - Common ROS paths (
/opt/ros/{rolling,jazzy,iron,humble}) - Bundled assets (built-in message definitions in ros-z-codegen)
This fallback mechanism enables builds without ROS 2 installed.
Common Development Commands
# Fast iterative development
cargo check # Quick compile check
cargo build # Debug build
cargo build --release # Optimized build
cargo test # Run tests
cargo clippy # Lint checks
# Clean builds
cargo clean # Remove all build artifacts
cargo clean -p ros-z-msgs # Clean specific package
After changing feature flags or updating ROS 2, run cargo clean -p ros-z-msgs to force message regeneration.
Next Steps
- ROS 2 Distribution Compatibility - Target Jazzy, Humble, or other distributions
- Running Examples - Try out the included examples
- Networking - Set up Zenoh router and session config
- Message Generation - Understand how messages work
- Troubleshooting - Solutions to common build issues
Start with the simplest build and add dependencies incrementally as your project grows.
ROS 2 Distribution Compatibility
ros-z supports multiple ROS 2 distributions through compile-time feature flags. This chapter explains the differences between distributions and how to target specific ROS 2 versions.
Supported Distributions
| Distribution | Status | Type Hash Support | Default |
|---|---|---|---|
| Jazzy Jalisco | ✅ Fully Supported | ✅ Yes | Yes |
| Humble Hawksbill | ✅ Supported | ❌ No (placeholder) | No |
| Rolling Ridley | ✅ Supported | ✅ Yes | No |
| Iron Irwini | ✅ Supported | ✅ Yes | No |
Default: ros-z defaults to Jazzy compatibility, which is the recommended distribution for new projects.
Distribution Differences
Type Hash Support
The most significant difference between distributions is type hash support:
Jazzy/Rolling/Iron (Modern):
- Supports real type hashes computed from message definitions
- Format:
RIHS01_<64-hex-chars>(ROS IDL Hash Standard version 1) - Enables type safety checks during pub/sub matching
- Type hashes are embedded in Zenoh key expressions for discovery
Humble (Legacy):
- Does not support real type hashes
- Uses constant placeholder:
"TypeHashNotSupported" - No type safety validation during discovery
- Compatible with rmw_zenoh_cpp v0.1.8
Example Key Expressions
Jazzy:
@ros2_lv/0/<zid>/<nid>/<eid>/MP/%/<namespace>/<node>/chatter/std_msgs%msg%String/RIHS01_1234567890abcdef.../...
Humble:
@ros2_lv/0/<zid>/<nid>/<eid>/MP/%/<namespace>/<node>/chatter/std_msgs%msg%String/TypeHashNotSupported/...
Building for Different Distributions
Using Jazzy (Default)
By default, ros-z builds with Jazzy compatibility. No special flags needed:
# Build with default (Jazzy)
cargo build
# Run examples
cargo run --example demo_nodes_talker
# Run tests
cargo nextest run
Using Humble
To build for Humble, use --no-default-features --features humble:
# Build for Humble
cargo build --no-default-features --features humble
# Run examples for Humble
cargo run --no-default-features --features humble --example demo_nodes_talker
# Run tests for Humble
cargo nextest run --no-default-features --features humble
Using Other Distributions
For Rolling or Iron, simply specify the distro feature:
# Build for Rolling
cargo build --features rolling
# Build for Iron
cargo build --features iron
Running Examples
Once you've added ros-z to your project, you can run the included examples to see it in action.
All examples require a Zenoh router to be running first (see Networking for why ros-z uses router-based architecture by default):
cargo run --example zenoh_router
Leave this running in a separate terminal, then run any example in another terminal.
Available Examples
# Pure Rust example with custom messages (no ros-z-msgs needed)
cargo run --example z_custom_message -- --mode status-pub
# Examples using bundled messages (requires ros-z-msgs)
cargo run --example z_pubsub # Publisher/Subscriber with std_msgs
cargo run --example twist_pub # Publishing geometry_msgs
cargo run --example battery_state_sub # Receiving sensor_msgs
cargo run --example z_srvcli # Service example with example_interfaces
See the Networking chapter for router setup details and alternative configurations.
Networking
Configure ros-z's Zenoh transport layer for optimal performance in your deployment environment. ros-z uses router-based architecture by default, matching ROS 2's official rmw_zenoh_cpp middleware for production-ready scalability.
Router-Based Architecture
ros-z uses a centralized Zenoh router for node discovery and communication, providing:
| Benefit | Description |
|---|---|
| Scalability | Centralized discovery handles large deployments efficiently |
| Lower Network Overhead | TCP-based discovery instead of multicast broadcasts |
| ROS 2 Compatibility | Matches rmw_zenoh_cpp behavior for seamless interoperability |
| Production Ready | Battle-tested configuration used in real robotics systems |
Quick Start
The simplest way to get started is using the built-in router example:
Terminal 1 - Start the Router:
cargo run --example zenoh_router
Terminal 2 - Run Your Application:
use ros_z::context::ZContextBuilder;
use ros_z::Builder;
// Uses default ROS session config (connects to tcp/localhost:7447)
let ctx = ZContextBuilder::default().build()?;
let node = ctx.create_node("my_node").build()?;
That's it! The default configuration automatically connects to the router on tcp/localhost:7447.
Running the Zenoh Router
Option 1: Built-in Router Example
ros-z provides a built-in router example that's ROS-compatible out of the box.
cargo run --example zenoh_router
Listens on tcp/[::]:7447 (all interfaces, port 7447).
Option 2: Official Zenoh Router
You can also use the official Zenoh router: https://zenoh.io/docs/getting-started/installation/#installing-the-zenoh-router.
Next Steps
Choose the configuration approach that fits your needs:
- Configuration Options - Six ways to configure Zenoh (from simple to complex)
- Advanced Configuration - Generate config files, run routers, configuration reference
- Troubleshooting - Solutions to connectivity issues
Ready to optimize your deployment? Experiment with different configurations and measure performance impact.
Configuration Options
ros-z provides multiple ways to configure Zenoh, from simple to advanced.
Option 1: Default Configuration (Recommended)
Use the built-in ROS session config for standard deployments:
let ctx = ZContextBuilder::default().build()?;
What it does:
- Connects to router at
tcp/localhost:7447 - Uses ROS-compatible timeouts and buffer sizes
- Disables multicast discovery (uses router instead)
Option 2: Custom Router Endpoint
Connect to a router on a different host or port:
let ctx = ZContextBuilder::default()
.with_router_endpoint("tcp/192.168.1.100:7447")
.build()?;
Use cases:
- Distributed systems with remote router
- Custom port configurations
- Multiple isolated networks
Option 3: Environment Variable Overrides
Override any Zenoh configuration setting using the ROSZ_CONFIG_OVERRIDE environment variable without changing code:
# Override mode and endpoint
export ROSZ_CONFIG_OVERRIDE='mode="client";connect/endpoints=["tcp/192.168.1.100:7447"]'
# Run your application
cargo run --example my_app
// No code changes needed - overrides are applied automatically
let ctx = ZContextBuilder::default().build()?;
Format:
- Semicolon-separated
key=valuepairs - Values use JSON5 syntax
- Keys use slash-separated paths (e.g.,
connect/endpoints,scouting/multicast/enabled)
Common examples:
# Connect to remote router
export ROSZ_CONFIG_OVERRIDE='connect/endpoints=["tcp/10.0.0.5:7447"]'
# Enable multicast scouting explicitly
export ROSZ_CONFIG_OVERRIDE='scouting/multicast/enabled=true'
# Multiple overrides
export ROSZ_CONFIG_OVERRIDE='mode="client";connect/timeout_ms=5000;scouting/multicast/enabled=false'
Environment variable overrides have the highest priority and will override any programmatic configuration or config file settings.
Option 4: Advanced Configuration Builders
Fine-tune session or router settings programmatically:
use ros_z::config::{SessionConfigBuilder, RouterConfigBuilder};
// Customize session config
let session_config = SessionConfigBuilder::new()
.with_router_endpoint("tcp/192.168.1.100:7447")
.build()?;
let ctx = ZContextBuilder::default()
.with_zenoh_config(session_config)
.build()?;
// Or build a custom router config
let router_config = RouterConfigBuilder::new()
.with_listen_port(7448) // Custom port
.build()?;
zenoh::open(router_config).await?;
Key builder methods:
| Builder | Methods | Purpose |
|---|---|---|
SessionConfigBuilder | with_router_endpoint(endpoint) | Connect to custom router |
RouterConfigBuilder | with_listen_port(port) | Set custom router port |
Option 5: Peer Mode Using Multicast Discovery (No Router Required)
Revert to multicast peer discovery for simple setups:
// Use vanilla Zenoh config (peer mode with multicast)
let ctx = ZContextBuilder::default()
.with_zenoh_config(zenoh::Config::default())
.build()?;
Multicast scouting discovery is convenient for quick testing but doesn't scale well and won't work with ROS 2 nodes using rmw_zenoh_cpp (which expects a zenoh router).
Option 6: Load from Config File
Use JSON5 config files for complex deployments:
let ctx = ZContextBuilder::default()
.with_config_file("/etc/zenoh/session_config.json5")
.build()?;
When to use:
- Deploying across multiple machines
- Environment-specific configurations
- Version-controlled infrastructure
Advanced Configuration
Generating Config Files
ros-z can generate JSON5 config files matching rmw_zenoh_cpp defaults. This is opt-in via the generate-configs feature flag.
Basic Generation
cargo build --features generate-configs
Output location:
target/debug/build/ros-z-*/out/ros_z_config/
├── DEFAULT_ROSZ_ROUTER_CONFIG.json5
└── DEFAULT_ROSZ_SESSION_CONFIG.json5
Custom Output Directory
Specify a custom directory using the ROS_Z_CONFIG_OUTPUT_DIR environment variable:
Absolute path:
ROS_Z_CONFIG_OUTPUT_DIR=/etc/zenoh cargo build --features generate-configs
Relative path (from package root):
ROS_Z_CONFIG_OUTPUT_DIR=./config cargo build --features generate-configs
From workspace root:
ROS_Z_CONFIG_OUTPUT_DIR=$PWD/config cargo build -p ros-z --features generate-configs
Generated files include inline comments explaining each setting, making them perfect documentation references.
Using Generated Files
let ctx = ZContextBuilder::default()
.with_config_file("./config/DEFAULT_ROSZ_SESSION_CONFIG.json5")
.build()?;
Configuration Reference
Key Settings Explained
| Setting | Router | Session | Purpose |
|---|---|---|---|
| Mode | router | peer | Router relays messages, peers connect directly |
| Listen Endpoint | tcp/[::]:7447 | - | Router accepts connections |
| Connect Endpoint | - | tcp/localhost:7447 | Session connects to router |
| Multicast | Disabled | Disabled | Uses TCP gossip for discovery |
| Unicast Timeout | 60s | 60s | Handles slow networks/large deployments |
| Query Timeout | 10min | 10min | Long-running service calls |
| Max Sessions | 10,000 | - | Supports concurrent node startup |
| Keep-Alive | 2s | 2s | Optimized for loopback |
These defaults are tuned for ROS 2 deployments and match rmw_zenoh_cpp exactly. Only modify them if you have specific performance requirements.
Message Generation
Automatic Rust type generation from ROS 2 message definitions at build time. The code generation system converts .msg, .srv, and .action files into type-safe Rust structs with full serialization support and ROS 2 compatibility.
Message generation happens automatically during builds. You write ROS 2 message definitions, ros-z generates idiomatic Rust code.
System Architecture
graph LR
A[.msg/.srv files] --> B[ros-z-codegen]
B --> C[Parse & Resolve]
C --> D[Type Hashing]
D --> E[Code Generation]
E --> F[CDR Adapter]
E --> G[Protobuf Adapter]
F --> H[Rust Structs + Traits]
G --> I[Proto Files + Rust]
H --> J[ros-z-msgs]
I --> J
Key Features
| Feature | Description | Benefit |
|---|---|---|
| Build-time generation | Runs during cargo build | No manual steps |
| Bundled definitions | Includes common ROS types | Works without ROS 2 |
| Type safety | Full Rust type system | Compile-time validation |
| CDR compatible | ROS 2 DDS serialization | Full interoperability |
| Optional protobuf | Additional serialization | Cross-language support |
Component Stack
ros-z-codegen
Internal message generation library for ros-z:
- Parses
.msg,.srv, and.actionfile syntax - Resolves message dependencies across packages
- Calculates ROS 2 type hashes (RIHS algorithm)
- Generates Rust structs with serde
- Bundles common message definitions
ros-z-codegen provides bundled messages for std_msgs, geometry_msgs, sensor_msgs, and nav_msgs. These work without ROS 2 installation.
Orchestration Layer
ros-z-codegen's orchestration capabilities:
- Coordinates message discovery across sources
- Manages build-time code generation
- Provides serialization adapters
- Generates ros-z-specific traits
Discovery workflow:
sequenceDiagram
participant B as build.rs
participant D as Discovery
participant S as Sources
B->>D: Find packages
D->>S: Check AMENT_PREFIX_PATH
alt Found in system
S-->>D: System messages
else Not found
D->>S: Check /opt/ros/*
alt Found in standard path
S-->>D: System messages
else Not found
D->>S: Check bundled assets
S-->>D: Bundled messages
end
end
D-->>B: Package paths
B->>B: Generate Rust code
Serialization Adapters
CDR Adapter (default):
- Generates structs with serde
- CDR-compatible serialization
- Full ROS 2 DDS interoperability
- No additional dependencies
Protobuf Adapter (optional):
- Generates
.protofiles - Protobuf-compatible types
- Cross-language data exchange
- Requires protobuf feature
Generated Code
For each ROS 2 message, ros-z generates:
Message Struct
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct String {
pub data: std::string::String,
}
Type Information Traits
impl MessageTypeInfo for std_msgs::String {
fn type_name() -> &'static str {
"std_msgs::msg::dds_::String_"
}
fn type_hash() -> TypeHash {
TypeHash::from_rihs_string("RIHS01_abc123...")
.expect("Invalid hash")
}
}
impl WithTypeInfo for std_msgs::String {
fn type_info() -> TypeInfo {
TypeInfo::new(Self::type_name(), Self::type_hash())
}
}
These traits enable:
- Runtime type identification
- ROS 2 compatibility validation
- Proper DDS topic naming
- Type-safe message passing
Type hashes are critical for ROS 2 interoperability. They ensure nodes agree on message structure before exchanging data.
Build Process
ros-z-msgs Build Script
The generation happens in build.rs:
flowchart TD
A[Start build.rs] --> B[Read enabled features]
B --> C[Discover package paths]
C --> D{Messages found?}
D -->|Yes| E[Parse message definitions]
D -->|No| F[Build error]
E --> G[Resolve dependencies]
G --> H[Generate Rust code]
H --> I[Write to OUT_DIR]
I --> J[Compile completes]
Configuration:
let config = GeneratorConfig {
generate_cdr: true, // CDR-compatible types
generate_protobuf: false, // Optional protobuf
generate_type_info: true, // Trait implementations
output_dir: out_dir,
};
Package Discovery Order
flowchart LR
A[Feature Flags] --> B{System ROS?}
B -->|Found| C[AMENT_PREFIX_PATH]
B -->|Not Found| D{/opt/ros/distro?}
D -->|Found| E[Standard paths]
D -->|Not Found| F[Bundled assets]
C --> G[Generate from system]
E --> G
F --> H[Generate from bundled]
- System ROS:
$AMENT_PREFIX_PATH,$CMAKE_PREFIX_PATH - Standard paths:
/opt/ros/{rolling,jazzy,iron,humble} - Bundled assets: Built-in message definitions in ros-z-codegen
This fallback enables development without ROS 2 installation.
Using Generated Messages
Import Pattern
use ros_z_msgs::ros::std_msgs::String as RosString;
use ros_z_msgs::ros::geometry_msgs::Twist;
use ros_z_msgs::ros::sensor_msgs::LaserScan;
Namespace Structure
ros_z_msgs::ros::{package}::{MessageName}
Examples:
ros_z_msgs::ros::std_msgs::Stringros_z_msgs::ros::geometry_msgs::Pointros_z_msgs::ros::sensor_msgs::Image
Service Types
Services generate three types:
// Service definition
use ros_z_msgs::ros::example_interfaces::AddTwoInts;
// Request type
use ros_z_msgs::ros::example_interfaces::AddTwoIntsRequest;
// Response type
use ros_z_msgs::ros::example_interfaces::AddTwoIntsResponse;
Message Packages
Bundled Packages
Available without ROS 2:
| Package | Messages | Use Cases |
|---|---|---|
| std_msgs | String, Int32, Float64, etc. | Basic data types |
| geometry_msgs | Point, Pose, Twist, Transform | Spatial data |
| sensor_msgs | LaserScan, Image, Imu, PointCloud2 | Sensor readings |
| nav_msgs | Path, Odometry, OccupancyGrid | Navigation |
# Build with bundled messages
cargo build -p ros-z-msgs --features bundled_msgs
Additional Packages
These packages are bundled and available without ROS 2 installation:
| Package | Messages | Use Cases |
|---|---|---|
| example_interfaces | AddTwoInts, Fibonacci | Tutorials |
| action_tutorials_interfaces | Fibonacci action | Action tutorials |
| test_msgs | Test types | Testing |
# All packages are bundled by default
cargo build -p ros-z-msgs --features all_msgs
Manual Custom Messages
For rapid prototyping without .msg files:
Define the Struct
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RobotStatus {
pub robot_id: String,
pub battery_percentage: f64,
pub position: [f64; 2],
pub is_moving: bool,
}
Implement Required Traits
use ros_z::{MessageTypeInfo, WithTypeInfo, entity::TypeHash};
impl MessageTypeInfo for RobotStatus {
fn type_name() -> &'static str {
"custom_msgs::msg::dds_::RobotStatus_"
}
fn type_hash() -> TypeHash {
// For ros-z-to-ros-z only
TypeHash::zero()
}
}
impl WithTypeInfo for RobotStatus {}
Manual messages with TypeHash::zero() work only between ros-z nodes. For ROS 2 interoperability, use generated messages with proper type hashes.
When to Use Each Approach
flowchart TD
A[Need Custom Message?] --> B{Prototyping?}
B -->|Yes| C[Manual Implementation]
B -->|No| D{ROS 2 Interop?}
D -->|Required| E[Generate from .msg]
D -->|Not Required| F{Want Type Safety?}
F -->|Yes| E
F -->|No| C
| Approach | Pros | Cons | Use When |
|---|---|---|---|
| Manual | Fast, flexible | No ROS 2 interop | Prototyping, internal only |
| Generated | Type hashes, portable | Requires .msg files | Production, ROS 2 systems |
Serialization Formats
CDR (Default)
Common Data Representation - ROS 2 standard:
- Full DDS compatibility
- Efficient binary encoding
- Used by all ROS 2 implementations
- Automatic via serde
// Generated with CDR support
#[derive(Serialize, Deserialize)]
pub struct String {
pub data: std::string::String,
}
Protobuf (Optional)
Protocol Buffers alternative:
cargo build -p ros-z-msgs --features protobuf
cargo build -p ros-z --features protobuf
Benefits:
- Schema evolution
- Cross-language compatibility
- Familiar ecosystem
- Efficient encoding
Tradeoffs:
- Not ROS 2 standard format
- Additional dependencies
- Requires feature flag
Use protobuf when you need schema evolution or cross-language data exchange beyond ROS 2 ecosystem. See Protobuf Serialization for detailed usage guide.
Extending Message Packages
Add new packages to ros-z-msgs:
1. Add Feature Flag
Edit ros-z-msgs/Cargo.toml:
[features]
bundled_msgs = ["std_msgs", "geometry_msgs", "your_package"]
your_package = []
2. Update Build Script
Edit ros-z-msgs/build.rs:
fn get_bundled_packages() -> Vec<&'static str> {
let mut names = vec!["builtin_interfaces"];
#[cfg(feature = "your_package")]
names.push("your_package");
names
}
3. Rebuild
cargo build -p ros-z-msgs --features your_package
The build system automatically:
- Searches for the package
- Parses all message definitions
- Generates Rust types with traits
- Outputs to generated module
Advanced Topics
Message Filtering
The generator automatically filters:
- Deprecated actionlib messages - Old ROS 1 format
- wstring fields - Poor Rust support
- Duplicate definitions - Keeps first occurrence
Type Hash Calculation
ros-z uses the RIHS (ROS IDL Hash) algorithm:
flowchart LR
A[Message Definition] --> B[Parse Structure]
B --> C[Include Dependencies]
C --> D[Calculate Hash]
D --> E[RIHS String]
E --> F[TypeHash Object]
Properties:
- Includes message structure and field types
- Incorporates dependency hashes
- Changes when definition changes
- Ensures type safety across network
In generated code:
TypeHash::from_rihs_string("RIHS01_1234567890abcdef...")
.expect("Invalid RIHS hash string")
Custom Code Generation
For custom build scripts:
use ros_z_codegen::{MessageGenerator, GeneratorConfig};
let config = GeneratorConfig {
generate_cdr: true,
generate_protobuf: false,
generate_type_info: true,
output_dir: out_dir.clone(),
};
let generator = MessageGenerator::new(config);
generator.generate_from_msg_files(&package_paths)?;
Troubleshooting
Package Not Found
# Check ROS 2 is sourced
echo $AMENT_PREFIX_PATH
# Verify package exists
ros2 pkg list | grep your_package
# Install if missing
sudo apt install ros-jazzy-your-package
# Bundled packages are built into ros-z-codegen
cargo build -p ros-z-msgs --features bundled_msgs
Build Failures
| Error | Cause | Solution |
|---|---|---|
| "Cannot find package" | Missing dependency | Enable feature or install ROS 2 package |
| "Type conflict" | Duplicate definition | Remove manual implementation |
| "Hash error" | Version mismatch | Update ros-z-codegen dependency |
See Troubleshooting Guide for detailed solutions.
Resources
- Feature Flags - Available message packages
- Building - Build configuration
- Custom Messages - Manual implementation
- Protobuf Serialization - Alternative serialization format
Message generation is transparent. Focus on writing ROS 2 message definitions and let ros-z handle the Rust code generation.
Custom Messages
ros-z supports two approaches for defining custom message types:
| Approach | Definition | Best For |
|---|---|---|
| Rust-Native | Write Rust structs directly | Prototyping, ros-z-only systems |
| Schema-Generated | Write .msg/.srv files, generate Rust | Production, ROS 2 interop |
flowchart TD
A[Need Custom Messages?] -->|Yes| B{ROS 2 Interop Needed?}
B -->|Yes| C[Schema-Generated]
B -->|No| D{Quick Prototype?}
D -->|Yes| E[Rust-Native]
D -->|No| C
A -->|No| F[Use Standard Messages]
Rust-Native Messages
Define messages directly in Rust by implementing required traits. This approach is fast for prototyping but only works between ros-z nodes.
Rust-Native messages use TypeHash::zero() and won't interoperate with ROS 2 C++/Python nodes.
Workflow of Rust-Native Messages
graph LR
A[Define Struct] --> B[Impl MessageTypeInfo]
B --> C[Add Serde Traits]
C --> D[Impl WithTypeInfo]
D --> E[Use in Pub/Sub]
Required Traits
| Trait | Purpose | Key Method |
|---|---|---|
| MessageTypeInfo | Type identification | type_name(), type_hash() |
| WithTypeInfo | ros-z integration | type_info() |
| Serialize/Deserialize | Data encoding | From serde |
Message Example
use ros_z::{MessageTypeInfo, entity::{TypeHash, TypeInfo}};
use ros_z::ros_msg::WithTypeInfo;
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct RobotStatus {
battery_level: f32,
position_x: f32,
position_y: f32,
is_moving: bool,
}
impl MessageTypeInfo for RobotStatus {
fn type_name() -> &'static str {
"my_msgs::msg::dds_::RobotStatus_"
}
fn type_hash() -> TypeHash {
TypeHash::zero() // ros-z-to-ros-z only
}
}
impl WithTypeInfo for RobotStatus {
fn type_info() -> TypeInfo {
TypeInfo::new(Self::type_name(), Self::type_hash())
}
}
Service Example
use ros_z::{ServiceTypeInfo, msg::ZService};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct NavigateToRequest {
target_x: f32,
target_y: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct NavigateToResponse {
success: bool,
}
struct NavigateTo;
impl ServiceTypeInfo for NavigateTo {
fn service_type_info() -> TypeInfo {
TypeInfo::new("my_msgs::srv::dds_::NavigateTo_", TypeHash::zero())
}
}
impl ZService for NavigateTo {
type Request = NavigateToRequest;
type Response = NavigateToResponse;
}
See the z_custom_message example:
# Terminal 1: Router
cargo run --example zenoh_router
# Terminal 2: Subscriber
cargo run --example z_custom_message -- --mode status-sub
# Terminal 3: Publisher
cargo run --example z_custom_message -- --mode status-pub
Schema-Generated Messages
Define messages in .msg/.srv files and generate Rust code using ros-z-codegen. This approach provides proper type hashes and can reference standard ROS 2 types.
Schema-Generated messages get proper RIHS01 type hashes and can reference types from ros_z_msgs like geometry_msgs/Point.
Workflow of Schema-Generated Messages
graph LR
A[Write .msg files] --> B[Create Rust crate]
B --> C[Add build.rs]
C --> D[Set ROS_Z_MSG_PATH]
D --> E[cargo build]
E --> F[Use generated types]
Step 1: Create Message Package
Create a ROS 2 style directory structure:
my_robot_msgs/
├── msg/
│ ├── RobotStatus.msg
│ └── SensorReading.msg
└── srv/
└── NavigateTo.srv
Step 2: Define Messages
Messages can reference standard ROS 2 types:
# RobotStatus.msg
string robot_id
geometry_msgs/Point position
bool is_moving
# SensorReading.msg
builtin_interfaces/Time timestamp
float64[] values
string sensor_id
# NavigateTo.srv
geometry_msgs/Point target
float64 max_speed
---
bool success
string message
Step 3: Create Rust Crate
Cargo.toml:
[package]
name = "my-robot-msgs"
version = "0.1.0"
edition = "2021"
# Standalone package (not part of parent workspace)
[workspace]
[dependencies]
ros-z-msgs = { version = "0.1" }
ros-z = { version = "0.1", default-features = false }
serde = { version = "1", features = ["derive"] }
smart-default = "0.7"
zenoh-buffers = "1"
[build-dependencies]
ros-z-codegen = { version = "0.1" }
anyhow = "1"
build.rs:
use std::path::PathBuf;
use std::env;
fn main() -> anyhow::Result<()> {
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
ros_z_codegen::generate_user_messages(&out_dir, false)?;
println!("cargo:rerun-if-env-changed=ROS_Z_MSG_PATH");
Ok(())
}
src/lib.rs:
// Re-export standard types from ros-z-msgs
pub use ros_z_msgs::*;
// Include generated user messages
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
Step 4: Build
Set ROS_Z_MSG_PATH and build:
ROS_Z_MSG_PATH="./my_robot_msgs" cargo build
For multiple packages, use colon-separated paths:
ROS_Z_MSG_PATH="./my_msgs:./other_msgs" cargo build
Step 5: Use Generated Types
use my_robot_msgs::ros::my_robot_msgs::{RobotStatus, SensorReading};
use my_robot_msgs::ros::my_robot_msgs::srv::NavigateTo;
use ros_z_msgs::ros::geometry_msgs::Point;
use ros_z_msgs::ros::builtin_interfaces::Time;
let status = RobotStatus {
robot_id: "robot_1".to_string(),
position: Point { x: 1.0, y: 2.0, z: 0.0 },
is_moving: true,
};
let reading = SensorReading {
timestamp: Time { sec: 1234, nanosec: 0 },
values: vec![1.0, 2.0, 3.0],
sensor_id: "lidar_1".to_string(),
};
See ros-z/examples/custom_msgs_demo/ for a working example:
cd ros-z/examples/custom_msgs_demo
ROS_Z_MSG_PATH="./my_robot_msgs" cargo build
Comparison
| Feature | Rust-Native | Schema-Generated |
|---|---|---|
| Definition | Rust structs | .msg/.srv files |
| Type Hashes | TypeHash::zero() | Proper RIHS01 hashes |
| Standard Type Refs | Manual | Automatic (geometry_msgs, etc.) |
| ROS 2 Interop | No | Partial (messages yes, services limited) |
| Setup Complexity | Low | Medium (build.rs required) |
| Best For | Prototyping | Production |
Type Naming Convention
Both approaches should follow ROS 2 DDS naming:
# Messages
package::msg::dds_::MessageName_
# Services
package::srv::dds_::ServiceName_
The trailing underscore and dds_ infix match ROS 2's internal naming scheme.
Resources
- Message Generation - How ros-z-msgs generates standard types
- Protobuf Serialization - Alternative serialization format
- Publishers & Subscribers - Using messages in pub-sub
- Services - Using messages in services
Protobuf Serialization
Use Protocol Buffers as an alternative serialization format for ros-z messages. While CDR is the default ROS 2-compatible format, protobuf offers schema evolution, cross-language compatibility, and familiar tooling for teams already using the protobuf ecosystem.
Protobuf support in ros-z enables two powerful use cases:
- ROS messages with protobuf encoding - Use standard ROS message types serialized via protobuf
- Pure protobuf messages - Send custom
.protomessages directly through ros-z
When to Use Protobuf
| Use Case | Recommendation |
|---|---|
| ROS 2 interoperability | Use CDR (default) |
| Schema evolution | Use Protobuf |
| Cross-language data exchange | Use Protobuf |
| Existing protobuf infrastructure | Use Protobuf |
| Performance critical | Benchmark both (typically similar) |
Protobuf-serialized messages are not compatible with standard ROS 2 nodes using CDR. Use protobuf when you control both ends of the communication or need its specific features.
Enabling Protobuf Support
Feature Flags
Enable protobuf in your Cargo.toml:
[dependencies]
ros-z = { version = "0.1", features = ["protobuf"] }
ros-z-msgs = { version = "0.1", features = ["geometry_msgs", "protobuf"] }
prost = "0.13"
[build-dependencies]
prost-build = "0.13"
Build Configuration
For custom .proto files, add a build.rs:
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut config = prost_build::Config::new();
// Enable serde support for ros-z compatibility
config.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]");
config.compile_protos(&["proto/sensor_data.proto"], &["proto/"])?;
println!("cargo:rerun-if-changed=proto/sensor_data.proto");
Ok(())
}
Approach 1: ROS Messages with Protobuf
Use auto-generated ROS message types with protobuf serialization:
use ros_z::msg::ProtobufSerdes;
use ros_z_msgs::proto::geometry_msgs::Vector3 as Vector3Proto;
let ctx = ros_z::context::ZContextBuilder::default().build()?;
let node = ctx.create_node("protobuf_node").build()?;
// Create publisher with protobuf serialization
let pub = node
.create_pub::<Vector3Proto>("/vector_proto")
.with_serdes::<ProtobufSerdes<Vector3Proto>>()
.build()?;
// Publish messages
let msg = Vector3Proto {
x: 1.0,
y: 2.0,
z: 3.0,
};
pub.publish(&msg)?;
Key points:
- Import from
ros_z_msgs::proto::*namespace (notros_z_msgs::ros::*) - Use
.with_serdes::<ProtobufSerdes<T>>()to select protobuf encoding - Message types automatically implement
MessageTypeInfotrait - Full type safety and compile-time checking
Approach 2: Custom Protobuf Messages
Send arbitrary protobuf messages defined in .proto files:
Step 1: Define Your Message
Create proto/sensor_data.proto:
syntax = "proto3";
package examples;
message SensorData {
string sensor_id = 1;
double temperature = 2;
double humidity = 3;
int64 timestamp = 4;
}
Step 2: Generate Rust Code
Configure build.rs as shown above. The build script generates Rust structs at compile time.
Step 3: Include Generated Code
pub mod sensor_data {
include!(concat!(env!("OUT_DIR"), "/examples.rs"));
}
use sensor_data::SensorData;
Step 4: Implement Required Traits
use ros_z::{MessageTypeInfo, WithTypeInfo, entity::TypeHash};
impl MessageTypeInfo for SensorData {
fn type_name() -> &'static str {
"examples::msg::dds_::SensorData_"
}
fn type_hash() -> TypeHash {
TypeHash::zero() // For custom protobuf messages
}
}
impl WithTypeInfo for SensorData {}
Step 5: Use in ros-z
let pub = node
.create_pub::<SensorData>("/sensor_data")
.with_serdes::<ProtobufSerdes<SensorData>>()
.build()?;
let msg = SensorData {
sensor_id: "sensor_01".to_string(),
temperature: 23.5,
humidity: 45.0,
timestamp: 1234567890,
};
pub.publish(&msg)?;
Complete Example
The protobuf_demo example demonstrates both approaches:
use clap::Parser;
use protobuf_demo::{run_pubsub_demo, run_service_client, run_service_server};
use ros_z::{Builder, Result, context::ZContextBuilder};
#[derive(Debug, Parser)]
#[command(
name = "protobuf_demo",
about = "Protobuf demonstration for ros-z - pub/sub and services"
)]
struct Args {
/// Mode to run: pubsub, service-server, service-client, or combined
#[arg(short, long, default_value = "pubsub")]
mode: String,
/// Service name (for service modes)
#[arg(long, default_value = "/calculator")]
service: String,
/// Maximum number of messages/requests (0 for unlimited)
#[arg(short = 'n', long, default_value = "3")]
count: usize,
/// Zenoh session mode (peer, client, router)
#[arg(long, default_value = "peer")]
zenoh_mode: String,
/// Zenoh router endpoint to connect to
#[arg(short, long)]
endpoint: Option<String>,
}
fn main() -> Result<()> {
let args = Args::parse();
// Initialize logging
zenoh::init_log_from_env_or("info");
// Create the ROS-Z context
let ctx = if let Some(ref e) = args.endpoint {
ZContextBuilder::default()
.with_mode(&args.zenoh_mode)
.with_connect_endpoints([e.clone()])
.build()?
} else {
ZContextBuilder::default()
.with_mode(&args.zenoh_mode)
.build()?
};
let max_count = if args.count == 0 {
None
} else {
Some(args.count)
};
match args.mode.as_str() {
"pubsub" => {
run_pubsub_demo(ctx, max_count)?;
}
"service-server" => {
run_service_server(ctx, &args.service, max_count)?;
}
"service-client" => {
let operations = vec![
("add", 10.0, 5.0),
("subtract", 10.0, 5.0),
("multiply", 10.0, 5.0),
("divide", 10.0, 5.0),
("divide", 10.0, 0.0), // This will fail
];
run_service_client(ctx, &args.service, operations)?;
}
"combined" => {
println!("\n=== Running Combined Demo ===\n");
// Create separate contexts for server and client
let server_ctx = if let Some(e) = args.endpoint.clone() {
ZContextBuilder::default()
.with_mode(&args.zenoh_mode)
.with_connect_endpoints([e])
.build()?
} else {
ZContextBuilder::default()
.with_mode(&args.zenoh_mode)
.build()?
};
let service_name = args.service.clone();
// Run server in background thread
let _server_handle =
std::thread::spawn(move || run_service_server(server_ctx, &service_name, Some(5)));
// Give server time to start
std::thread::sleep(std::time::Duration::from_millis(500));
// Run client
let operations = vec![
("add", 10.0, 5.0),
("subtract", 10.0, 5.0),
("multiply", 10.0, 5.0),
("divide", 10.0, 5.0),
("divide", 10.0, 0.0),
];
run_service_client(ctx, &args.service, operations)?;
}
_ => {
eprintln!("Unknown mode: {}", args.mode);
eprintln!("Valid modes: pubsub, service-server, service-client, combined");
std::process::exit(1);
}
}
Ok(())
}
Running the Demo
# Navigate to the demo directory
cd ros-z/examples/protobuf_demo
# Run the example
cargo run
Expected output:
=== Protobuf Serialization Demo ===
This demonstrates two ways to use protobuf with ros-z:
1. ROS messages with protobuf serialization (from ros-z-msgs)
2. Custom protobuf messages (from .proto files)
=====================================================
--- Part 1: ROS geometry_msgs/Vector3 with Protobuf ---
Publishing ROS Vector3 messages...
Published Vector3: x=0, y=0, z=0
Published Vector3: x=1, y=2, z=3
Published Vector3: x=2, y=4, z=6
--- Part 2: Custom SensorData message (pure protobuf) ---
Publishing custom SensorData messages...
Published SensorData: id=sensor_0, temp=20.0°C, humidity=45.0%, ts=1234567890
Published SensorData: id=sensor_1, temp=20.5°C, humidity=47.0%, ts=1234567891
Published SensorData: id=sensor_2, temp=21.0°C, humidity=49.0%, ts=1234567892
Successfully demonstrated both protobuf approaches!
Subscribers with Protobuf
Receive protobuf-encoded messages:
use ros_z::msg::ProtobufSerdes;
let sub = node
.create_sub::<Vector3Proto>("/vector_proto")
.with_serdes::<ProtobufSerdes<Vector3Proto>>()
.build()?;
loop {
let msg = sub.recv()?;
println!("Received: x={}, y={}, z={}", msg.x, msg.y, msg.z);
}
Publishers and subscribers must use the same serialization format. A protobuf publisher requires a protobuf subscriber.
Services with Protobuf
Both request and response use protobuf encoding:
Server
let service = node
.create_service::<MyService>("/my_service")
.with_serdes::<ProtobufSerdes<MyServiceRequest>, ProtobufSerdes<MyServiceResponse>>()
.build()?;
loop {
let (key, request) = service.take_request()?;
let response = process_request(&request);
service.send_response(&response, &key)?;
}
Client
let client = node
.create_client::<MyService>("/my_service")
.with_serdes::<ProtobufSerdes<MyServiceRequest>, ProtobufSerdes<MyServiceResponse>>()
.build()?;
client.send_request(&request)?;
let response = client.take_response()?;
Available ROS Messages
When ros-z-msgs is built with protobuf feature, it generates protobuf versions of ROS messages:
// Import from proto namespace
use ros_z_msgs::proto::std_msgs::String as StringProto;
use ros_z_msgs::proto::geometry_msgs::{Point, Pose, Twist};
use ros_z_msgs::proto::sensor_msgs::{LaserScan, Image};
Namespace mapping:
| Format | Namespace | Use |
|---|---|---|
| CDR | ros_z_msgs::ros::* | ROS 2 interop |
| Protobuf | ros_z_msgs::proto::* | Protobuf encoding |
Type Information
ROS Messages
Auto-generated messages from ros-z-msgs include MessageTypeInfo:
// No manual implementation needed
use ros_z_msgs::proto::geometry_msgs::Vector3;
// Vector3 already implements MessageTypeInfo
Custom Protobuf Messages
Manual implementation required:
impl MessageTypeInfo for MyProtoMessage {
fn type_name() -> &'static str {
// Follow ROS naming convention
"my_package::msg::dds_::MyProtoMessage_"
}
fn type_hash() -> TypeHash {
// Use zero for custom protobuf messages
TypeHash::zero()
}
}
impl WithTypeInfo for MyProtoMessage {}
TypeHash::zero() indicates the message doesn't have ROS 2 type compatibility. This is fine for ros-z-to-ros-z communication.
Protobuf vs CDR Comparison
| Aspect | CDR | Protobuf |
|---|---|---|
| ROS 2 Compatibility | ✅ Full | ❌ None |
| Schema Evolution | ❌ Limited | ✅ Excellent |
| Cross-language | ROS 2 only | ✅ Universal |
| Tooling | ROS ecosystem | ✅ Protobuf ecosystem |
| Message Size | Efficient | Efficient |
| Setup Complexity | Simple | Moderate |
| ros-z Support | Default | Requires feature flag |
Common Patterns
Mixed Serialization
Different topics can use different formats:
// CDR for ROS 2 compatibility
let ros_pub = node
.create_pub::<RosString>("/ros_topic")
.build()?; // CDR is default
// Protobuf for schema evolution
let proto_pub = node
.create_pub::<ProtoString>("/proto_topic")
.with_serdes::<ProtobufSerdes<ProtoString>>()
.build()?;
Migration Strategy
Gradual migration from CDR to protobuf:
- Add protobuf feature to dependencies
- Create protobuf topics with new names
- Run both CDR and protobuf publishers temporarily
- Migrate subscribers to protobuf
- Deprecate CDR topics
Build Integration
Project Structure
my_ros_project/
├── proto/
│ ├── sensor_data.proto
│ └── robot_status.proto
├── src/
│ └── main.rs
├── build.rs
└── Cargo.toml
Cargo.toml
[package]
name = "my_ros_project"
version = "0.1.0"
edition = "2021"
[dependencies]
ros-z = { version = "0.1", features = ["protobuf"] }
ros-z-msgs = { version = "0.1", features = ["geometry_msgs", "protobuf"] }
prost = "0.13"
serde = { version = "1.0", features = ["derive"] }
[build-dependencies]
prost-build = "0.13"
build.rs
use std::io::Result;
fn main() -> Result<()> {
let mut config = prost_build::Config::new();
// Enable serde for ros-z compatibility
config.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]");
// Compile all proto files
config.compile_protos(
&[
"proto/sensor_data.proto",
"proto/robot_status.proto",
],
&["proto/"]
)?;
// Rebuild if proto files change
println!("cargo:rerun-if-changed=proto/");
Ok(())
}
Troubleshooting
Error: protobuf feature not enabled
Error: protobuf feature not enabled
This error occurs when you try to use protobuf serialization without enabling the feature flag.
Solution:
Enable the protobuf feature in your Cargo.toml:
[dependencies]
ros-z = { version = "0.1", features = ["protobuf"] }
ros-z-msgs = { version = "0.1", features = ["geometry_msgs", "protobuf"] }
Error: MessageTypeInfo not implemented
Error: MessageTypeInfo not implemented
Custom protobuf messages need to implement required ros-z traits.
Solution:
Implement the required traits for your custom message:
use ros_z::{MessageTypeInfo, WithTypeInfo, entity::TypeHash};
impl MessageTypeInfo for MyMessage {
fn type_name() -> &'static str {
"package::msg::dds_::MyMessage_"
}
fn type_hash() -> TypeHash {
TypeHash::zero()
}
}
impl WithTypeInfo for MyMessage {}
Build fails with prost errors
Build fails with prost errors
Version mismatches between prost dependencies can cause build failures.
Solution:
Ensure prost versions match in your Cargo.toml:
[dependencies]
prost = "0.13"
[build-dependencies]
prost-build = "0.13"
If issues persist, try:
cargo clean
cargo build
Messages not receiving
Messages not receiving
Publisher and subscriber must use the same serialization format.
Solution:
Verify both sides use protobuf serialization:
// Publisher
let pub = node
.create_pub::<MyMessage>("/topic")
.with_serdes::<ProtobufSerdes<MyMessage>>()
.build()?;
// Subscriber
let sub = node
.create_sub::<MyMessage>("/topic")
.with_serdes::<ProtobufSerdes<MyMessage>>()
.build()?;
Note: A protobuf publisher cannot communicate with a CDR subscriber and vice versa.
Proto file not found during build
Proto file not found during build
The build script cannot locate your .proto files.
Solution:
Verify the path in your build.rs:
config.compile_protos(
&["proto/sensor_data.proto"], // Check this path
&["proto/"] // Check include directory
)?;
Ensure the proto directory exists:
ls proto/sensor_data.proto
Generated code not found
Generated code not found
The build script generated code but you can't import it.
Solution:
Ensure you're including from the correct location:
pub mod sensor_data {
include!(concat!(env!("OUT_DIR"), "/examples.rs"));
}
The filename after OUT_DIR should match your package name in the .proto file:
package examples; // Generates examples.rs
Resources
- Message Generation - Understanding message architecture
- Custom Messages - Manual message implementation
- Protobuf Documentation - Official protobuf guide
- prost Crate - Rust protobuf library
Use protobuf when you need schema evolution or cross-language compatibility beyond the ROS 2 ecosystem. Stick with CDR for standard ROS 2 interoperability.
Reproducible Development with Nix
This is an advanced topic for users familiar with Nix. If you're new to Nix, you can safely skip this chapter and use the standard Building instructions instead.
ros-z provides Nix flakes for reproducible development environments with all dependencies pre-configured.
Why Use Nix?
Reproducible Builds: Nix ensures every developer and CI system uses the exact same dependencies, eliminating "works on my machine" issues. All dependencies—from compilers to ROS 2 packages—are pinned to specific versions and cached immutably.
Uniform CI/CD: The same Nix configuration that runs locally is used in continuous integration, ensuring build consistency across development, testing, and deployment environments. No more divergence between local builds and CI failures.
Zero Setup: New team members can start developing with a single nix develop command—no manual ROS installation, no dependency hunting, no environment configuration.
Available Environments
# Default: ROS 2 Jazzy with full tooling
nix develop
# Specific ROS distros
nix develop .#ros-jazzy # ROS 2 Jazzy
nix develop .#ros-rolling # ROS 2 Rolling
# Pure Rust (no ROS)
nix develop .#pureRust # Minimal Rust toolchain only
Use Cases
| Use Case | Environment | Benefit |
|---|---|---|
| Team Development | All developers | Everyone has identical toolchains and dependencies |
| CI/CD Pipelines | GitHub Actions, GitLab CI | Same environment locally and in automation |
| Cross-Platform | Linux, macOS, WSL | Consistent builds regardless of host OS |
| Multiple ROS Versions | Switch environments easily | Test against different ROS distros without conflicts |
Use Nix for consistent development environments across team members and CI/CD pipelines. The reproducibility guarantees catch integration issues early.
Learning More
- Nix Official Guide - Introduction to Nix
- Nix Flakes - Understanding flakes
- ros-z flake.nix - Our Nix configuration
Feature Flags
Fine-grained control over dependencies and functionality through Cargo feature flags. Build exactly what you need, from zero-dependency core to full ROS 2 integration, without carrying unused code.
Feature flags enable pay-per-use dependencies. Start minimal and enable features incrementally as requirements grow.
Feature Categories
| Category | Purpose | Example Features |
|---|---|---|
| Distribution | Target specific ROS 2 versions | humble, jazzy, rolling |
| Message Packages | Enable ROS 2 message types | std_msgs, geometry_msgs |
| Serialization | Additional encoding formats | protobuf |
| Integration | External system bindings | rcl-z |
ros-z Core Features
protobuf
Enables Protocol Buffers serialization using prost.
cargo build -p ros-z --features protobuf
Use cases:
- Schema evolution support
- Language-agnostic data exchange
- Efficient binary encoding
- Familiar protobuf ecosystem
Dependencies: prost, prost-types
Protobuf is optional. CDR serialization (default) provides full ROS 2 compatibility without additional dependencies.
Distribution Compatibility Features
ros-z defaults to ROS 2 Jazzy. Use distribution features to target other ROS 2 versions.
jazzy (default)
Targets ROS 2 Jazzy Jalisco with modern type hash support.
# Automatically enabled (default)
cargo build
# Explicitly enable
cargo build --features jazzy
Features:
- ✅ Type hash support (RIHS01)
- ✅ Shared memory optimization
- ✅ Modern ROS 2 protocol
humble
Targets ROS 2 Humble Hawksbill (LTS) with legacy compatibility.
# Disable defaults and enable humble
cargo build --no-default-features --features humble
Features:
- ❌ No type hash (uses placeholder)
- ❌ No shared memory support
- ✅ LTS support until 2027
- ✅ Compatible with rmw_zenoh_cpp v0.1.8
Important: Humble requires --no-default-features to avoid conflicts with the jazzy default.
rolling and iron
Target newer distributions:
# Rolling
cargo build --features rolling
# Iron
cargo build --features iron
See also: ROS 2 Distribution Compatibility for detailed documentation.
rcl-z
Enables RCL (ROS Client Library) integration for C/C++ interoperability.
cargo build -p ros-z --features rcl-z
Use cases:
- Integrating with existing RCL-based code
- Leveraging C/C++ ROS 2 libraries
- Hybrid Rust/C++ applications
Requirements: ROS 2 installation with RCL libraries
This feature requires ROS 2 to be sourced before building. See Building Guide for setup instructions.
ros-z-msgs Features
Default Features
The default build includes commonly used message types via core_msgs:
cargo build -p ros-z-msgs
Includes:
std_msgs- Basic types (String, Int32, etc.)geometry_msgs- Spatial types (Point, Pose, Transform)sensor_msgs- Sensor data (LaserScan, Image, Imu)nav_msgs- Navigation (Path, Odometry, OccupancyGrid)example_interfaces- Tutorial services (AddTwoInts)action_tutorials_interfaces- Tutorial actions (Fibonacci)
All messages are vendored in assets - no ROS 2 installation required. Feature flags simply control which packages to include in your build.
Individual Package Features
All packages are bundled in assets and work without ROS 2:
| Feature | Package | Use Case |
|---|---|---|
std_msgs | Standard messages | Strings, numbers, arrays |
geometry_msgs | Geometric primitives | Points, poses, transforms |
sensor_msgs | Sensor data | Cameras, lidars, IMUs |
nav_msgs | Navigation | Paths, maps, odometry |
example_interfaces | Tutorial services | AddTwoInts, Fibonacci |
action_tutorials_interfaces | Tutorial actions | Fibonacci action |
test_msgs | Test types | Testing and validation |
Usage:
# Single package
cargo build -p ros-z-msgs --no-default-features --features std_msgs
# Multiple packages
cargo build -p ros-z-msgs --no-default-features --features "std_msgs,geometry_msgs"
# Default (core_msgs)
cargo build -p ros-z-msgs
Convenience Aliases
core_msgs (default):
The most commonly used packages for ROS 2 development.
cargo build -p ros-z-msgs # Uses core_msgs by default
Enables: std_msgs, geometry_msgs, sensor_msgs, nav_msgs, example_interfaces, action_tutorials_interfaces
common_interfaces:
cargo build -p ros-z-msgs --features common_interfaces
Enables: std_msgs, geometry_msgs, sensor_msgs
bundled_msgs:
cargo build -p ros-z-msgs --features bundled_msgs
Enables: std_msgs, geometry_msgs, sensor_msgs, nav_msgs
robotics:
Alias for core_msgs.
all_msgs:
cargo build -p ros-z-msgs --features all_msgs
Enables: All available packages including test_msgs
Protobuf Types
Generate protobuf types alongside ROS messages:
cargo build -p ros-z-msgs --features protobuf
Note: Requires ros-z/protobuf feature enabled as well.
ros-z-codegen Features
Protobuf Code Generation
Enable protobuf code generation support:
cargo build -p ros-z-codegen --features protobuf
Use case: Building tools that generate protobuf code from ROS messages
Feature Dependency Graph
graph TD
A[all_msgs] --> B[bundled_msgs]
A --> C[example_interfaces]
A --> D[action_tutorials_interfaces]
A --> E[test_msgs]
B --> F[std_msgs]
B --> G[geometry_msgs]
B --> H[sensor_msgs]
B --> I[nav_msgs]
J[core_msgs] --> F
J --> G
J --> H
J --> I
J --> C
J --> D
Common Feature Combinations
Minimal Development
Core library only, no messages:
cargo build -p ros-z
Dependencies: Rust, Cargo Use case: Custom messages only
Standard Development
Core with common message types:
cargo build -p ros-z-msgs # Uses default common_interfaces
cargo build -p ros-z
Dependencies: Rust, Cargo Use case: Most applications
Full Message Set
All available message packages:
cargo build -p ros-z-msgs --features all_msgs
Dependencies: Rust, Cargo Use case: Access to all bundled message types including test_msgs
RCL Integration
For C/C++ ROS 2 interoperability:
source /opt/ros/jazzy/setup.bash
cargo build -p rcl-z
Dependencies: Rust, Cargo, ROS 2 Use case: Hybrid Rust/C++ applications
Protobuf Development
Core with protobuf serialization:
cargo build -p ros-z-codegen --features protobuf
cargo build -p ros-z-msgs --features protobuf
cargo build -p ros-z --features protobuf
Dependencies: Rust, Cargo, Protobuf compiler Use case: Cross-language data exchange
Feature Matrix
| Package | Feature | Requires ROS 2 | Adds Dependencies |
|---|---|---|---|
| ros-z | (none) | No | None |
| ros-z | jazzy (default) | No | None |
| ros-z | humble | No | None |
| ros-z | rolling | No | None |
| ros-z | protobuf | No | prost, prost-types |
| ros-z | rcl-z | Yes | RCL libraries |
| ros-z-msgs | core_msgs (default) | No | None (bundled) |
| ros-z-msgs | bundled_msgs | No | None (bundled) |
| ros-z-msgs | all_msgs | No | None (bundled) |
| ros-z-msgs | protobuf | No | prost, prost-types |
| ros-z-msgs | jazzy (default) | No | None |
| ros-z-msgs | humble | No | None |
| ros-z-codegen | protobuf | No | prost-build |
Checking Active Features
View enabled features for a package:
# Show features for ros-z-msgs
cargo tree -p ros-z-msgs -e features
# Show all workspace features
cargo tree -e features
# Build with specific features and verify
cargo build -p ros-z-msgs --features std_msgs,geometry_msgs -v
Use cargo tree to debug feature resolution issues. It shows exactly which features are active and why.
Feature Selection Strategy
flowchart TD
A[Start Project] --> B{Need ROS messages?}
B -->|No| C[Zero features<br/>Custom messages]
B -->|Yes| D{Which messages?}
D -->|Common| E[core_msgs<br/>default]
D -->|Minimal| F[bundled_msgs or<br/>individual packages]
D -->|All| G[all_msgs]
C --> H[Minimal dependencies]
E --> I[Standard dependencies]
F --> I
G --> I
Decision guide:
- Most projects? → Use defaults (
core_msgs) - includes common packages - Minimal build? → Use
--no-default-featureswith specific packages - Custom messages only? → No message features
- Cross-language data? → Add protobuf feature
- C++ integration? → Add rcl-z feature (requires ROS 2)
First build with message generation is slow. Incremental builds are fast. Choose the minimal feature set that meets your needs.
Examples by Feature
Bundled Messages (Default)
cargo run --example z_pubsub # std_msgs
cargo run --example twist_pub # geometry_msgs
cargo run --example battery_state_sub # sensor_msgs
cargo run --example z_pingpong # std_msgs
cargo run --example z_srvcli # example_interfaces (now bundled)
Custom Messages
cargo run --example z_custom_message # No features needed
Resources
- Building Guide - Build procedures for each scenario
- ROS 2 Distribution Compatibility - Target Jazzy, Humble, or other distributions
- Message Generation - How messages are generated
Start with default features and add more as your project evolves. Feature flags provide flexibility without forcing early architectural decisions.
Troubleshooting
Quick solutions to common ros-z build and runtime issues. Click on any question to expand the answer.
Most issues fall into three categories: build configuration, runtime connectivity, or ROS 2 integration.
Build Issues
Build fails with 'Cannot find ROS packages' or package discovery errors
Build fails with 'Cannot find ROS packages' or package discovery errors
Root Cause: ROS 2 environment not sourced or packages not installed.
Solutions:
-
Source ROS 2 environment:
source /opt/ros/jazzy/setup.bash # or for rolling: source /opt/ros/rolling/setup.bash -
Verify environment variables:
echo $AMENT_PREFIX_PATH echo $CMAKE_PREFIX_PATH -
Check package installation:
ros2 pkg prefix example_interfaces # If fails, install: sudo apt install ros-jazzy-example-interfaces -
Clean and rebuild:
cargo clean -p ros-z-msgs cargo build -p ros-z-msgs
Common Error Messages:
| Error | Solution |
|---|---|
| "Package X not found" | Source ROS 2 environment |
| "Cannot find ament_index" | Install ROS 2 or use bundled msgs |
| "AMENT_PREFIX_PATH not set" | Run source /opt/ros/jazzy/setup.bash |
Compiler error: cannot find crate ros_z_msgs
Compiler error: cannot find crate ros_z_msgs
Root Cause: ros-z-msgs is not part of default workspace members.
Solution:
# Build ros-z-msgs explicitly
cargo build -p ros-z-msgs
# Then build your example
cargo build --example z_srvcli
Note: ros-z-msgs is excluded from default builds to avoid requiring ROS 2 for core development. Build it explicitly when needed.
Build takes too long to complete
Build takes too long to complete
Solutions:
# Use parallel builds (automatic on most systems)
cargo build -j $(nproc)
# Build only what you need
cargo build -p ros-z-msgs --features std_msgs,geometry_msgs
Linker errors during build (especially with rcl-z)
Linker errors during build (especially with rcl-z)
Solution:
# Clear cache and rebuild
cargo clean
source /opt/ros/jazzy/setup.bash
cargo build -p rcl-z
Warning: After changing feature flags or updating ROS 2, run cargo clean -p ros-z-msgs to force message regeneration.
Runtime Issues
Publishers and subscribers on different processes don't communicate
Publishers and subscribers on different processes don't communicate
Root Cause: Zenoh router not running or nodes not configured correctly.
Solution:
-
Ensure the router is running:
cargo run --example zenoh_router -
Verify endpoint matches in your code:
let ctx = ZContextBuilder::default() .with_router_endpoint("tcp/localhost:7447") // Must match router .build()?;
Router fails to start with 'Address already in use' error
Router fails to start with 'Address already in use' error
Root Cause: Another process is using port 7447.
Solutions:
-
Stop the conflicting process
-
Use a custom port:
// Custom router port let router_config = RouterConfigBuilder::new() .with_listen_port(7448) .build()?; // Connect sessions to custom port let ctx = ZContextBuilder::default() .with_router_endpoint("tcp/localhost:7448") .build()?;
Don't want to manage a router for simple local testing
Don't want to manage a router for simple local testing
Solution: Use peer mode with multicast discovery:
let ctx = ZContextBuilder::default()
.with_zenoh_config(zenoh::Config::default())
.build()?;
Warning: Peer mode won't interoperate with ROS 2 nodes using rmw_zenoh_cpp in router mode.
Resources
- Building Guide - Correct build procedures
- Networking - Zenoh router setup
- Feature Flags - Available features
- GitHub Issues - Report bugs
Most issues are environmental. Verify your setup matches the build scenario requirements before diving deeper.