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

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.