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

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