Skip to content

refactor: add rmw_zenoh_cpp compatible Zenoh configuration and use a Zenoh router by default#55

Merged
YuanYuYuan merged 9 commits intomainfrom
refactor/zenoh-router
Jan 6, 2026
Merged

refactor: add rmw_zenoh_cpp compatible Zenoh configuration and use a Zenoh router by default#55
YuanYuYuan merged 9 commits intomainfrom
refactor/zenoh-router

Conversation

@YuanYuYuan
Copy link
Collaborator

@YuanYuYuan YuanYuYuan commented Dec 19, 2025

Motivation

This PR aligns ros-z with the official ROS 2 Zenoh middleware (rmw_zenoh_cpp) by adopting its default Zenoh configuration. This ensures consistent behavior, better scalability, and compatibility with ROS 2 Zenoh-based systems. This closes #42.

Key alignment:

  • Same router/session architecture as rmw_zenoh_cpp
  • Identical network topology (centralized discovery via router)
  • Matching timeout and buffer size optimizations for large deployments
  • Disabled multicast discovery (uses TCP gossip instead)

Breaking Changes

Examples now require a Zenoh router

Before (main branch):

# Just run the example - multicast discovery worked automatically
cargo run --example demo_nodes_talker

After (this PR):

# Terminal 1: Start the router (NEW REQUIREMENT)
cargo run --example zenoh_router

# Terminal 2: Run the example (no code changes needed!)
cargo run --example demo_nodes_talker

Why this change?

The new configuration disables multicast discovery by default and uses a router-based architecture, matching rmw_zenoh_cpp behavior:

  • Better scalability: Centralized discovery through router instead of peer-to-peer multicast
  • Lower network overhead: No multicast traffic, reduced connections
  • Production-ready: Optimized for large ROS 2 deployments

How to Run Examples

Quick Start

# Terminal 1: Start the ROS-compatible router
cargo run --example zenoh_router

# Terminal 2: Run talker
cargo run --example demo_nodes_talker

# Terminal 3: Run listener
cargo run --example demo_nodes_listener

No code changes needed - existing examples work automatically with the new default config.

Managing Configuration Programmatically

Option 1: Use Default ROS Session Config (Recommended)

use ros_z::context::ZContextBuilder;
use ros_z::Builder;

// Uses ROS session config automatically (connects to tcp/localhost:7447)
let ctx = ZContextBuilder::default()
    .build()?;

Option 2: Connect to Custom Router Endpoint

let ctx = ZContextBuilder::default()
    .with_router_endpoint("tcp/192.168.1.100:7448")
    .build()?;

Option 3: Use Config Builders for Advanced Customization

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)
    .build()?;

zenoh::open(router_config).await?;

Option 4: Revert to Old Behavior (No Router Required)

// Use vanilla Zenoh config (multicast discovery enabled)
let ctx = ZContextBuilder::default()
    .with_zenoh_config(zenoh::Config::default())
    .build()?;

Option 5: Load from Config File

let ctx = ZContextBuilder::default()
    .with_config_file("/path/to/zenoh_config.json5")
    .build()?;

Generating Config Files

Config file generation is now opt-in via the generate-configs feature flag.

Generate to Default Location

cargo build --features generate-configs
# Output: target/debug/build/ros-z-*/out/ros_z_config/
#   - DEFAULT_RMW_ZENOH_ROUTER_CONFIG.json5
#   - DEFAULT_RMW_ZENOH_SESSION_CONFIG.json5

Generate to Custom Directory

# Absolute path
ROS_Z_CONFIG_OUTPUT_DIR=/etc/zenoh cargo build --features generate-configs

# Relative path (from package root: ros-z/ros-z/)
ROS_Z_CONFIG_OUTPUT_DIR=./config cargo build --features generate-configs

# From workspace root
ROS_Z_CONFIG_OUTPUT_DIR=$PWD/config cargo build -p ros-z --features generate-configs

Generated Files

  • DEFAULT_RMW_ZENOH_ROUTER_CONFIG.json5 - Router configuration
  • DEFAULT_RMW_ZENOH_SESSION_CONFIG.json5 - Session/peer configuration

Both files include inline comments explaining each setting and match rmw_zenoh_cpp defaults.

What's New

1. Configuration Module (src/config.rs)

Introduces a type-safe, zero-cost configuration system using LazyLock for compile-time constant storage:

pub struct ConfigOverride {
    pub key: &'static str,
    pub value: Value,
    pub reason: &'static str,
}

// Pre-configured overrides matching rmw_zenoh_cpp
pub fn router_overrides() -> Vec<ConfigOverride>;
pub fn session_overrides() -> Vec<ConfigOverride>;

// Builders for runtime customization
pub struct RouterConfigBuilder { /* ... */ }
pub struct SessionConfigBuilder { /* ... */ }

Architecture:

  • 10 common overrides shared between router and session (deduplication)
  • 5 router-specific overrides for centralized routing
  • 6 session-specific overrides for peer nodes
  • LazyLock storage for zero allocation after first access

Key configurations:

Category Setting Value Rationale
Mode mode router / peer Router vs peer architecture
Discovery scouting/multicast/enabled false Disabled - use TCP gossip
Discovery scouting/gossip/target peers → router only Minimize traffic at launch
Endpoints Router listen/endpoints tcp/[::]:7447 IPv6 all interfaces
Endpoints Session connect/endpoints tcp/localhost:7447 Connect to local router
Timeouts transport/unicast/*_timeout 60000 ms Increased from 10s for large deployments
Timeouts queries_default_timeout 60000 ms Increased from 10s for slow services
Buffers transport/unicast/max_sessions 10000 Increased from 1000 for concurrent nodes
Buffers transport/unicast/accept_pending 10000 Increased from 100 for handshakes
Keep-alive transport/link/tx/keep_alive 2 Decreased from 4 for loopback
Congestion Router wait_before_close 5s Lower for WiFi routing
Congestion Session wait_before_close 60s Higher for loopback startup
SHM transport/shared_memory/enabled false Disabled until fully tested

2. Build-Time Config Generation (build.rs)

Generates JSON5 config files at build time:

  • Uses the ConfigOverride pattern (single source of truth in Rust code)
  • Feature-gated with generate-configs flag (opt-in)
  • Supports custom output directory via ROS_Z_CONFIG_OUTPUT_DIR
  • Eliminates code duplication (previously had hardcoded JSON5 strings)
  • Inline comments auto-generated from ConfigOverride::reason fields

3. Zenoh Router Example

Provides a drop-in replacement for the rmw_zenoh_cpp router:

cargo run --example zenoh_router

Features:

  • ROS-compatible configuration out-of-the-box
  • Customizable port and endpoint
  • Built-in Zenoh logging (via RUST_LOG)

Users can still download and use the official Zenoh router.

Technical Details

Zero-Cost Abstraction with LazyLock

All configuration overrides are stored as &'static [ConfigOverride] using Rust 1.80+'s std::sync::LazyLock:

fn common_overrides() -> &'static [ConfigOverride] {
    static COMMON: LazyLock<Vec<ConfigOverride>> = LazyLock::new(|| {
        vec![/* ... */]
    });
    &COMMON
}

Benefits:

  • Initialized once on first access
  • Zero allocation on subsequent calls
  • Thread-safe by default
  • Compile-time constant semantics

Configuration Deduplication

Common settings are extracted to avoid duplication:

router_overrides() = router_specific (5) + common (10) = 15 total
session_overrides() = session_specific (6) + common (10) = 16 total

Changes to common settings automatically propagate to both configs.

Build-Time Validation

Integration tests ensure all configurations create valid Zenoh sessions:

#[test]
fn test_session_config_creates_valid_session() {
    let config = session_config().expect("Failed to build");
    let session = zenoh::open(config).await;
    assert!(session.is_ok());
}

Migration Guide

If you were using default Zenoh configs

Before:

let ctx = ZContextBuilder::default().build()?;
// Multicast discovery worked automatically

After:

# Start router in separate terminal
cargo run --example zenoh_router
let ctx = ZContextBuilder::default().build()?;
// Now connects to tcp/localhost:7447 automatically

If you need multicast discovery (no router)

// Option 1: Use vanilla Zenoh config
let ctx = ZContextBuilder::default()
    .with_zenoh_config(zenoh::Config::default())
    .build()?;

// Option 2: Load custom config file
let ctx = ZContextBuilder::default()
    .with_config_file("./my_multicast_config.json5")
    .build()?;

Compatibility

  • Rust version: 1.80+ required for LazyLock
  • rmw_zenoh_cpp: Fully compatible with default configs
  • Zenoh: 1.0+ (tested with latest stable)
  • ROS 2: Works with Jazzy, Kilted, and Rolling distributions

@YuanYuYuan YuanYuYuan force-pushed the refactor/zenoh-router branch 2 times, most recently from 4fb7f98 to c62c6c7 Compare December 19, 2025 17:31
@github-actions
Copy link

github-actions bot commented Dec 19, 2025

PR Preview Action v1.8.0
Preview removed because the pull request was closed.
2026-01-06 16:59 UTC

@YuanYuYuan YuanYuYuan changed the title Refactor/zenoh router refactor: add rmw_zenoh_cpp compatible Zenoh configuration and use a Zenoh router by default Dec 19, 2025
@YuanYuYuan YuanYuYuan force-pushed the refactor/zenoh-router branch from 8df2693 to bc4263e Compare January 6, 2026 04:50
@YuanYuYuan YuanYuYuan merged commit 236b318 into main Jan 6, 2026
11 checks passed
@YuanYuYuan YuanYuYuan deleted the refactor/zenoh-router branch January 6, 2026 16:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use zenohd in examples/demo_node by default

1 participant