Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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?

FeatureDescriptionBenefit
Native RustPure Rust implementation with no C/C++ dependenciesMemory safety, concurrency without data races
Zenoh TransportHigh-performance pub-sub engineLow latency, efficient bandwidth usage
ROS 2 CompatibleWorks seamlessly with standard ROS 2 toolsIntegrate with existing robotics ecosystems
Multiple SerializationsSupport for various data representations: CDR (ROS default), ProtobufFlexible message encoding for different performance and interoperability needs
Type SafetyCompile-time message validationCatch errors before deployment
Modern APIIdiomatic Rust patternsErgonomic developer experience
Safety FirstOwnership model prevents common bugsNo data races, null pointers, or buffer overflows at compile time
High ProductivityCargo ecosystem with excellent toolingFast development without sacrificing reliability

Note

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:

PatternUse CaseLearn More
Pub/SubContinuous data streaming, sensor data, status updatesPub/Sub
ServicesRequest-response operations, remote procedure callsServices
ActionsLong-running tasks with feedback and cancellation supportActions

Tip

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.

Tip

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

Note

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

ComponentPurposeUsage
ZContextBuilderInitialize ros-z environmentEntry point, configure settings
ZContextManages ROS 2 connectionsCreate nodes from this
NodeLogical unit of computationPublishers/subscribers attach here
PublisherSends messages to topicsnode.create_pub::<Type>("topic")
SubscriberReceives messages from topicsnode.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

Success

You should see the listener receiving messages published by the talker in real-time. Press Ctrl+C to stop any process.

Why a Zenoh router?

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.

Info

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:

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.

Note

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

FeatureDescriptionBenefit
Type SafetyStrongly-typed messages using Rust structsCompile-time error detection
Zero-CopyEfficient message passing via ZenohReduced latency and CPU usage
QoS ProfilesConfigurable reliability, durability, historyFine-grained delivery control
Async/BlockingDual API for both paradigmsFlexible 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_count for 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_count and timeout parameters
  • 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

Tip

Use callbacks for low-latency event-driven processing. Use blocking/async receive when you need explicit control over when messages are processed.

Pattern Comparison

AspectBlocking ReceiveAsync ReceiveCallback
Control FlowSequentialSequentialEvent-driven
LatencyMedium (poll-based)Medium (poll-based)Low (immediate)
MemoryQueue size × messageQueue size × messageNo queue
BackpressureBuilt-in (queue full)Built-in (queue full)None (drops if slow)
Use CaseSimple scriptsAsync applicationsReal-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()?;

Tip

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

Success

ros-z provides full ROS 2 compatibility via Zenoh bridge or rmw_zenoh, enabling cross-language communication.

Resources

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.

Note

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

FeatureDescriptionBenefit
Type SafetyStrongly-typed service definitions with Rust structsCompile-time error detection
Pull ModelExplicit control over request processing timingPredictable concurrency and backpressure
Async/BlockingDual API for both paradigmsFlexible integration patterns
Request TrackingKey-based request/response matchingReliable 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_requests parameter 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

Success

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.

Info

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?

AspectPull Model (take_request)Push Model (callback)
ControlExplicit control over when to accept requestsInterrupts current work
ConcurrencyEasy to reason aboutRequires careful synchronization
BackpressureNatural - slow processing slows acceptanceCan overwhelm if processing is slow
ConsistencySame 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?;

Tip

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

Success

ros-z service servers and clients are fully compatible with ROS 2 via Zenoh bridge or rmw_zenoh, enabling cross-language service calls.

Resources

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.

Tip

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

ComponentTypePurpose
GoalInputDefines the desired outcome
FeedbackStreamProgress updates during execution
ResultOutputFinal outcome when complete
StatusStateCurrent 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

Info

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);

Warning

Always implement timeout mechanisms for action clients. Long-running actions can fail or hang, and clients need graceful degradation strategies.

Comparison with Other Patterns

PatternDurationFeedbackCancellationUse Case
Pub-SubContinuousNoN/ASensor data streaming
Service< 1 secondNoNoQuick queries
ActionSeconds to minutesYesYesLong-running tasks

Resources

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 types
  • geometry_msgs - Spatial data
  • sensor_msgs - Sensor data
  • nav_msgs - Navigation
  • example_interfaces - Tutorial services (AddTwoInts)
  • action_tutorials_interfaces - Tutorial actions (Fibonacci)
  • test_msgs - Test types

Tip

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:

PackageDefault BuildPurposeDependencies
ros-zYesCore Zenoh-native ROS 2 libraryNone
ros-z-codegenYesMessage generation utilitiesNone
ros-z-msgsNoPre-generated message typesNone (all vendored)
ros-z-testsNoIntegration testsros-z-msgs
rcl-zNoRCL C bindingsROS 2 required

Note

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:

  1. System ROS installation (AMENT_PREFIX_PATH, CMAKE_PREFIX_PATH)
  2. Common ROS paths (/opt/ros/{rolling,jazzy,iron,humble})
  3. 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

Warning

After changing feature flags or updating ROS 2, run cargo clean -p ros-z-msgs to force message regeneration.

Next Steps

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

DistributionStatusType Hash SupportDefault
Jazzy Jalisco✅ Fully Supported✅ YesYes
Humble Hawksbill✅ Supported❌ No (placeholder)No
Rolling Ridley✅ Supported✅ YesNo
Iron Irwini✅ Supported✅ YesNo

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.

Important

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:

BenefitDescription
ScalabilityCentralized discovery handles large deployments efficiently
Lower Network OverheadTCP-based discovery instead of multicast broadcasts
ROS 2 CompatibilityMatches rmw_zenoh_cpp behavior for seamless interoperability
Production ReadyBattle-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()?;

Success

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:

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.

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=value pairs
  • 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'

Tip

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:

BuilderMethodsPurpose
SessionConfigBuilderwith_router_endpoint(endpoint)Connect to custom router
RouterConfigBuilderwith_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()?;

Warning

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

Tip

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

SettingRouterSessionPurpose
ModerouterpeerRouter relays messages, peers connect directly
Listen Endpointtcp/[::]:7447-Router accepts connections
Connect Endpoint-tcp/localhost:7447Session connects to router
MulticastDisabledDisabledUses TCP gossip for discovery
Unicast Timeout60s60sHandles slow networks/large deployments
Query Timeout10min10minLong-running service calls
Max Sessions10,000-Supports concurrent node startup
Keep-Alive2s2sOptimized for loopback

Note

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.

Success

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

FeatureDescriptionBenefit
Build-time generationRuns during cargo buildNo manual steps
Bundled definitionsIncludes common ROS typesWorks without ROS 2
Type safetyFull Rust type systemCompile-time validation
CDR compatibleROS 2 DDS serializationFull interoperability
Optional protobufAdditional serializationCross-language support

Component Stack

ros-z-codegen

Internal message generation library for ros-z:

  • Parses .msg, .srv, and .action file syntax
  • Resolves message dependencies across packages
  • Calculates ROS 2 type hashes (RIHS algorithm)
  • Generates Rust structs with serde
  • Bundles common message definitions

Info

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 .proto files
  • 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

Note

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]
  1. System ROS: $AMENT_PREFIX_PATH, $CMAKE_PREFIX_PATH
  2. Standard paths: /opt/ros/{rolling,jazzy,iron,humble}
  3. 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::String
  • ros_z_msgs::ros::geometry_msgs::Point
  • ros_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;

Tip

Import the service type for creation, then use the request/response types when handling calls.

Message Packages

Bundled Packages

Available without ROS 2:

PackageMessagesUse Cases
std_msgsString, Int32, Float64, etc.Basic data types
geometry_msgsPoint, Pose, Twist, TransformSpatial data
sensor_msgsLaserScan, Image, Imu, PointCloud2Sensor readings
nav_msgsPath, Odometry, OccupancyGridNavigation
# 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:

PackageMessagesUse Cases
example_interfacesAddTwoInts, FibonacciTutorials
action_tutorials_interfacesFibonacci actionAction tutorials
test_msgsTest typesTesting
# 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 {}

Warning

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
ApproachProsConsUse When
ManualFast, flexibleNo ROS 2 interopPrototyping, internal only
GeneratedType hashes, portableRequires .msg filesProduction, 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

Info

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

ErrorCauseSolution
"Cannot find package"Missing dependencyEnable feature or install ROS 2 package
"Type conflict"Duplicate definitionRemove manual implementation
"Hash error"Version mismatchUpdate ros-z-codegen dependency

See Troubleshooting Guide for detailed solutions.

Resources

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:

ApproachDefinitionBest For
Rust-NativeWrite Rust structs directlyPrototyping, ros-z-only systems
Schema-GeneratedWrite .msg/.srv files, generate RustProduction, 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.

Warning

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

TraitPurposeKey Method
MessageTypeInfoType identificationtype_name(), type_hash()
WithTypeInforos-z integrationtype_info()
Serialize/DeserializeData encodingFrom 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.

Tip

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

FeatureRust-NativeSchema-Generated
DefinitionRust structs.msg/.srv files
Type HashesTypeHash::zero()Proper RIHS01 hashes
Standard Type RefsManualAutomatic (geometry_msgs, etc.)
ROS 2 InteropNoPartial (messages yes, services limited)
Setup ComplexityLowMedium (build.rs required)
Best ForPrototypingProduction

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

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.

Info

Protobuf support in ros-z enables two powerful use cases:

  1. ROS messages with protobuf encoding - Use standard ROS message types serialized via protobuf
  2. Pure protobuf messages - Send custom .proto messages directly through ros-z

When to Use Protobuf

Use CaseRecommendation
ROS 2 interoperabilityUse CDR (default)
Schema evolutionUse Protobuf
Cross-language data exchangeUse Protobuf
Existing protobuf infrastructureUse Protobuf
Performance criticalBenchmark both (typically similar)

Warning

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 (not ros_z_msgs::ros::*)
  • Use .with_serdes::<ProtobufSerdes<T>>() to select protobuf encoding
  • Message types automatically implement MessageTypeInfo trait
  • 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);
}

Important

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:

FormatNamespaceUse
CDRros_z_msgs::ros::*ROS 2 interop
Protobufros_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 {}

Note

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

AspectCDRProtobuf
ROS 2 Compatibility✅ Full❌ None
Schema Evolution❌ Limited✅ Excellent
Cross-languageROS 2 only✅ Universal
ToolingROS ecosystem✅ Protobuf ecosystem
Message SizeEfficientEfficient
Setup ComplexitySimpleModerate
ros-z SupportDefaultRequires 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:

  1. Add protobuf feature to dependencies
  2. Create protobuf topics with new names
  3. Run both CDR and protobuf publishers temporarily
  4. Migrate subscribers to protobuf
  5. 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

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

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

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

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

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

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

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

Warning

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 CaseEnvironmentBenefit
Team DevelopmentAll developersEveryone has identical toolchains and dependencies
CI/CD PipelinesGitHub Actions, GitLab CISame environment locally and in automation
Cross-PlatformLinux, macOS, WSLConsistent builds regardless of host OS
Multiple ROS VersionsSwitch environments easilyTest against different ROS distros without conflicts

Tip

Use Nix for consistent development environments across team members and CI/CD pipelines. The reproducibility guarantees catch integration issues early.

Learning More

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.

Success

Feature flags enable pay-per-use dependencies. Start minimal and enable features incrementally as requirements grow.

Feature Categories

CategoryPurposeExample Features
DistributionTarget specific ROS 2 versionshumble, jazzy, rolling
Message PackagesEnable ROS 2 message typesstd_msgs, geometry_msgs
SerializationAdditional encoding formatsprotobuf
IntegrationExternal system bindingsrcl-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

Info

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

Warning

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)

Tip

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:

FeaturePackageUse Case
std_msgsStandard messagesStrings, numbers, arrays
geometry_msgsGeometric primitivesPoints, poses, transforms
sensor_msgsSensor dataCameras, lidars, IMUs
nav_msgsNavigationPaths, maps, odometry
example_interfacesTutorial servicesAddTwoInts, Fibonacci
action_tutorials_interfacesTutorial actionsFibonacci action
test_msgsTest typesTesting 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

PackageFeatureRequires ROS 2Adds Dependencies
ros-z(none)NoNone
ros-zjazzy (default)NoNone
ros-zhumbleNoNone
ros-zrollingNoNone
ros-zprotobufNoprost, prost-types
ros-zrcl-zYesRCL libraries
ros-z-msgscore_msgs (default)NoNone (bundled)
ros-z-msgsbundled_msgsNoNone (bundled)
ros-z-msgsall_msgsNoNone (bundled)
ros-z-msgsprotobufNoprost, prost-types
ros-z-msgsjazzy (default)NoNone
ros-z-msgshumbleNoNone
ros-z-codegenprotobufNoprost-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

Tip

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:

  1. Most projects? → Use defaults (core_msgs) - includes common packages
  2. Minimal build? → Use --no-default-features with specific packages
  3. Custom messages only? → No message features
  4. Cross-language data? → Add protobuf feature
  5. C++ integration? → Add rcl-z feature (requires ROS 2)

Note

All message packages are vendored - no ROS 2 installation required for any message feature.

Info

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

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.

Tip

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

Root Cause: ROS 2 environment not sourced or packages not installed.

Solutions:

  1. Source ROS 2 environment:

    source /opt/ros/jazzy/setup.bash
    # or for rolling:
    source /opt/ros/rolling/setup.bash
    
  2. Verify environment variables:

    echo $AMENT_PREFIX_PATH
    echo $CMAKE_PREFIX_PATH
    
  3. Check package installation:

    ros2 pkg prefix example_interfaces
    # If fails, install:
    sudo apt install ros-jazzy-example-interfaces
    
  4. Clean and rebuild:

    cargo clean -p ros-z-msgs
    cargo build -p ros-z-msgs
    

Common Error Messages:

ErrorSolution
"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

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

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)

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

Root Cause: Zenoh router not running or nodes not configured correctly.

Solution:

  1. Ensure the router is running:

    cargo run --example zenoh_router
    
  2. 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

Root Cause: Another process is using port 7447.

Solutions:

  1. Stop the conflicting process

  2. 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

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

Most issues are environmental. Verify your setup matches the build scenario requirements before diving deeper.