Custom Messages
Define domain-specific message types by implementing required traits on Rust structs. Custom messages give you full control over data structures while maintaining ROS 2 compatibility for ros-z-to-ros-z communication.
Use custom messages for rapid prototyping and standalone applications. For production systems requiring ROS 2 interoperability, use auto-generated messages from .msg definitions.
Implementation Workflow
graph LR
A[Define Struct] --> B[Impl MessageTypeInfo]
B --> C[Add Serde Traits]
C --> D[Impl WithTypeInfo]
D --> E[Use in Pub/Sub]
Complete Example
The z_custom_message example demonstrates both custom messages and services:
use std::time::Duration;
use clap::Parser;
use ros_z::{
Builder, MessageTypeInfo, Result, ServiceTypeInfo, context::ZContextBuilder, entity::TypeHash,
msg::ZService,
};
use serde::{Deserialize, Serialize};
// Custom message for pub/sub example
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RobotStatus {
pub robot_id: String,
pub battery_percentage: f64,
pub position_x: f64,
pub position_y: f64,
pub is_moving: bool,
}
impl MessageTypeInfo for RobotStatus {
fn type_name() -> &'static str {
"custom_msgs::msg::dds_::RobotStatus_"
}
fn type_hash() -> TypeHash {
TypeHash::zero()
}
}
impl ros_z::WithTypeInfo for RobotStatus {}
// Custom service request
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct NavigateToRequest {
pub target_x: f64,
pub target_y: f64,
pub max_speed: f64,
}
impl MessageTypeInfo for NavigateToRequest {
fn type_name() -> &'static str {
"custom_msgs::srv::dds_::NavigateTo_Request_"
}
fn type_hash() -> TypeHash {
TypeHash::zero()
}
}
impl ros_z::WithTypeInfo for NavigateToRequest {}
// Custom service response
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct NavigateToResponse {
pub success: bool,
pub estimated_duration: f64,
pub message: String,
}
impl MessageTypeInfo for NavigateToResponse {
fn type_name() -> &'static str {
"custom_msgs::srv::dds_::NavigateTo_Response_"
}
fn type_hash() -> TypeHash {
TypeHash::zero()
}
}
impl ros_z::WithTypeInfo for NavigateToResponse {}
// Service type definition
pub struct NavigateTo;
impl ServiceTypeInfo for NavigateTo {
fn service_type_info() -> ros_z::entity::TypeInfo {
ros_z::entity::TypeInfo::new("custom_msgs::srv::dds_::NavigateTo_", TypeHash::zero())
}
}
impl ZService for NavigateTo {
type Request = NavigateToRequest;
type Response = NavigateToResponse;
}
#[derive(Debug, Parser)]
struct Args {
#[arg(
short,
long,
default_value = "status-pub",
help = "Mode: status-pub, status-sub, nav-server, or nav-client"
)]
mode: String,
#[arg(
long,
default_value = "robot_1",
help = "Robot ID (for status-pub mode)"
)]
robot_id: String,
#[arg(long, default_value = "10.0", help = "Target X coordinate")]
target_x: f64,
#[arg(long, default_value = "20.0", help = "Target Y coordinate")]
target_y: f64,
#[arg(long, default_value = "1.5", help = "Maximum speed")]
max_speed: f64,
}
#[tokio::main]
async fn main() -> Result<()> {
match Args::parse().mode.as_str() {
"status-pub" => run_status_publisher(Args::parse().robot_id).await,
"status-sub" => run_status_subscriber().await,
"nav-server" => run_navigation_server(),
"nav-client" => {
let args = Args::parse();
run_navigation_client(args.target_x, args.target_y, args.max_speed)
}
mode => {
eprintln!(
"Invalid mode: {mode}. Use 'status-pub', 'status-sub', 'nav-server', or 'nav-client'"
);
std::process::exit(1);
}
}
}
async fn run_status_publisher(robot_id: String) -> Result<()> {
println!("Starting robot status publisher for robot: {robot_id}");
let ctx = ZContextBuilder::default().build()?;
let node = ctx.create_node("robot_status_publisher").build()?;
let zpub = node.create_pub::<RobotStatus>("/robot_status").build()?;
let mut position_x = 0.0;
let mut position_y = 0.0;
let mut battery = 100.0;
let mut moving = true;
loop {
// Simulate robot movement and battery drain
if moving {
position_x += 0.5;
position_y += 0.3;
battery -= 0.1;
if battery < 20.0 {
moving = false;
println!("Low battery! Robot stopped.");
}
}
let status = RobotStatus {
robot_id: robot_id.clone(),
battery_percentage: battery,
position_x,
position_y,
is_moving: moving,
};
println!(
"Publishing status: pos=({:.1}, {:.1}), battery={:.1}%, moving={}",
status.position_x, status.position_y, status.battery_percentage, status.is_moving
);
zpub.async_publish(&status).await?;
tokio::time::sleep(Duration::from_secs(1)).await;
// Reset simulation when battery too low
if battery < 10.0 {
battery = 100.0;
moving = true;
println!("Battery recharged! Resuming movement.");
}
}
}
async fn run_status_subscriber() -> Result<()> {
println!("Starting robot status subscriber...");
let ctx = ZContextBuilder::default().build()?;
let node = ctx.create_node("robot_status_subscriber").build()?;
let zsub = node.create_sub::<RobotStatus>("/robot_status").build()?;
loop {
if let Ok(status) = zsub.recv() {
println!(
"Received status from {}: pos=({:.1}, {:.1}), battery={:.1}%, moving={}",
status.robot_id,
status.position_x,
status.position_y,
status.battery_percentage,
status.is_moving
);
if status.battery_percentage < 20.0 {
println!("WARNING: {} has low battery!", status.robot_id);
}
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
fn run_navigation_server() -> Result<()> {
println!("Starting navigation service server...");
let ctx = ZContextBuilder::default().build()?;
let node = ctx.create_node("navigation_server").build()?;
let mut zsrv = node.create_service::<NavigateTo>("/navigate_to").build()?;
println!("Navigation server ready, waiting for requests...");
loop {
if let Ok((request_id, request)) = zsrv.take_request() {
println!(
"Received navigation request: target=({:.1}, {:.1}), max_speed={:.1}",
request.target_x, request.target_y, request.max_speed
);
// Simulate path planning
std::thread::sleep(Duration::from_millis(500));
let distance = (request.target_x.powi(2) + request.target_y.powi(2)).sqrt();
let duration = distance / request.max_speed;
let response = if request.max_speed > 0.0 && request.max_speed < 5.0 {
NavigateToResponse {
success: true,
estimated_duration: duration,
message: format!(
"Path planned successfully. Distance: {:.2}m, ETA: {:.2}s",
distance, duration
),
}
} else {
NavigateToResponse {
success: false,
estimated_duration: 0.0,
message: "Invalid max_speed. Must be between 0 and 5 m/s.".to_string(),
}
};
println!("Sending response: {:?}", response);
zsrv.send_response(&response, &request_id)?;
}
std::thread::sleep(Duration::from_millis(100));
}
}
fn run_navigation_client(target_x: f64, target_y: f64, max_speed: f64) -> Result<()> {
println!("Starting navigation client...");
let ctx = ZContextBuilder::default().build()?;
let node = ctx.create_node("navigation_client").build()?;
let zcli = node.create_client::<NavigateTo>("/navigate_to").build()?;
let request = NavigateToRequest {
target_x,
target_y,
max_speed,
};
println!(
"Sending navigation request: target=({:.1}, {:.1}), max_speed={:.1}",
request.target_x, request.target_y, request.max_speed
);
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async { zcli.send_request(&request).await })?;
println!("Waiting for response...");
loop {
if let Ok(response) = zcli.take_response() {
println!("Received response:");
println!("Success: {}", response.success);
println!("Duration: {:.2}s", response.estimated_duration);
println!("Message: {}", response.message);
break;
}
std::thread::sleep(Duration::from_millis(100));
}
Ok(())
}
Required Traits
| Trait | Purpose | Key Method |
|---|---|---|
| MessageTypeInfo | Type identification | type_name(), type_hash() |
| WithTypeInfo | ros-z integration | Automatic with #[derive] |
| Serialize/Deserialize | Data encoding | From serde |
Message Implementation
Step 1 - Define the Struct:
#[derive(Debug, Clone, Serialize, Deserialize)]
struct RobotStatus {
battery_level: f32,
position_x: f32,
position_y: f32,
is_moving: bool,
}
Step 2 - Implement MessageTypeInfo:
impl MessageTypeInfo for RobotStatus {
fn type_name() -> &'static str {
"custom_msgs::msg::dds_::RobotStatus_"
}
fn type_hash() -> TypeHash {
TypeHash::zero() // For ros-z-to-ros-z only
}
}
Step 3 - Add WithTypeInfo:
impl WithTypeInfo for RobotStatus {
fn type_info() -> TypeInfo {
TypeInfo::new(Self::type_name(), Self::type_hash())
}
}
TypeHash::zero() works for ros-z-to-ros-z communication. For full ROS 2 compatibility, generate proper type hashes from message definitions.
Service Implementation
Define Request and Response:
#[derive(Debug, Clone, Serialize, Deserialize)]
struct NavigateToRequest {
target_x: f32,
target_y: f32,
max_speed: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct NavigateToResponse {
success: bool,
distance_traveled: f32,
}
Implement Service Traits:
struct NavigateTo;
impl ServiceTypeInfo for NavigateTo {
fn service_type_info() -> TypeInfo {
TypeInfo::new(
"custom_msgs::srv::dds_::NavigateTo_",
TypeHash::zero()
)
}
}
impl ZService for NavigateTo {
type Request = NavigateToRequest;
type Response = NavigateToResponse;
}
Running the Example
All examples require a Zenoh router to be running first. Start it with:
cargo run --example zenoh_router
Publisher/Subscriber Mode:
Terminal 1 - Start Zenoh Router:
cargo run --example zenoh_router
Terminal 2 - Start Subscriber:
cargo run --example z_custom_message -- --mode status-sub
Terminal 3 - Start Publisher:
cargo run --example z_custom_message -- --mode status-pub
Service Client/Server Mode:
Terminal 1 - Start Zenoh Router:
cargo run --example zenoh_router
Terminal 2 - Start Server:
cargo run --example z_custom_message -- --mode nav-server
Terminal 3 - Send Requests:
cargo run --example z_custom_message -- --mode nav-client \
--target_x 10.0 --target_y 20.0 --max_speed 1.5
You should see the server processing navigation requests and returning results with calculated distances.
Decision Guide
flowchart TD
A[Need Custom Messages?] -->|Yes| B{Production or Prototype?}
B -->|Prototype| C[Manual Implementation]
B -->|Production| D{ROS 2 Interop?}
D -->|Yes| E[Generate from .msg]
D -->|No| C
A -->|No| F[Use Standard Messages]
| Approach | Use When | Benefits | Limitations |
|---|---|---|---|
| Manual Custom | Prototyping, standalone apps | Fast iteration, full control | No ROS 2 interop |
| Generated | Production, ROS 2 systems | Proper type hashing, interop | Build complexity |
| Standard | Common data types | Zero setup, universal | Limited to ROS 2 standard types |
Manual custom messages work only between ros-z nodes. They won't interoperate with ROS 2 C++/Python nodes due to missing type hashes and metadata.
Type Naming Convention
Follow ROS 2 DDS naming conventions for consistency:
// Pattern: package::msg::dds_::MessageName_
"custom_msgs::msg::dds_::RobotStatus_"
// Pattern: package::srv::dds_::ServiceName_
"custom_msgs::srv::dds_::NavigateTo_"
The trailing underscore and dds_ infix match ROS 2's internal naming scheme used by DDS middleware.
Resources
- Message Generation - Understanding message architecture
- Protobuf Serialization - Using protobuf with custom messages
- Publishers & Subscribers - Using messages in pub-sub
- Services - Using messages in services
- Feature Flags - Pre-generated message packages
Start experimenting with custom messages, then transition to generated messages when you need ROS 2 interoperability.