Skip to main content

Chapter 4: ROS 2 Services and Packages

Overview

This chapter explores ROS 2 services for synchronous communication and packages for code organization. You'll learn how to implement request-response patterns and structure your robotics projects effectively.

Learning Objectives

Learning Objectives

By the end of this chapter, you will be able to:

  • Implement service servers and clients for synchronous communication
  • Understand when to use services versus topics
  • Create and organize ROS 2 packages
  • Manage package dependencies and build configurations

ROS 2 Services

Services provide synchronous request/response communication between nodes. A service client sends a request to a service server, which processes the request and returns a response. Services are ideal for operations that have a clear beginning and end, such as configuration changes, computation tasks, or triggering specific actions.

Service Characteristics

  • Synchronous: Client waits for response before continuing
  • Request/Response: Defined request and response message types
  • Direct communication: Client communicates directly with server
  • Blocking: Service calls block until response is received
  • One-to-one: Each request is handled by a single service server
  • Stateless: Each service call is independent of others
  • Typed interfaces: Service definitions specify request and response message types

Service Architecture

The service pattern works as follows:

  1. A service server node creates a service server and registers it with a service name
  2. A service client node creates a service client and connects to the same service name
  3. The client sends a request to the server
  4. The server processes the request and sends back a response
  5. The client receives the response and continues execution

Service Implementation in Python

Here's how to implement service servers and clients in Python:

import rclpy
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts

class ServiceServerNode(Node):
def __init__(self):
super().__init__('service_server')
self.srv = self.create_service(
AddTwoInts,
'add_two_ints',
self.add_two_ints_callback
)
self.get_logger().info('Service server initialized')

def add_two_ints_callback(self, request, response):
response.sum = request.a + request.b
self.get_logger().info(f'Returning {request.a} + {request.b} = {response.sum}')
return response

class ServiceClientNode(Node):
def __init__(self):
super().__init__('service_client')
self.cli = self.create_client(AddTwoInts, 'add_two_ints')

while not self.cli.wait_for_service(timeout_sec=1.0):
self.get_logger().info('Service not available, waiting again...')

self.req = AddTwoInts.Request()

def send_request(self, a, b):
self.req.a = a
self.req.b = b
self.future = self.cli.call_async(self.req)
return self.future

Service Definition Files (.srv)

Service definitions are specified in .srv files that define the request and response message structure:

# Request part (before the '---')
int64 a
int64 b
---
# Response part (after the '---')
int64 sum

Service Management Commands

Common command-line tools for working with services:

# List all active services
ros2 service list

# Show information about a specific service
ros2 service info /service_name

# Call a service from command line
ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 1, b: 2}"

# Show the type of a service
ros2 service type /service_name

When to Use Services vs Topics

Use Services when:

  • You need request/response interaction
  • Operations have a clear beginning and end
  • You need guaranteed delivery and response
  • The operation is relatively fast (under a few seconds)
  • You need to pass parameters and get computed results

Use Topics when:

  • You need continuous data streaming
  • Communication should be decoupled in time
  • Multiple subscribers need the same data
  • Data is updated continuously (sensor data, status updates)

ROS 2 Packages

Packages are the basic building and distribution units in ROS 2. They contain source code, data, and configuration files organized in a standard structure. Packages provide a way to organize related functionality, manage dependencies, and distribute ROS 2 software.

Package Structure

A standard ROS 2 package follows this structure:

package_name/
├── CMakeLists.txt # Build configuration for C++ packages
├── package.xml # Package metadata and dependencies
├── src/ # Source code files
├── include/ # Header files (C++)
├── launch/ # Launch files for starting multiple nodes
├── config/ # Configuration files
├── test/ # Unit and integration tests
├── scripts/ # Standalone executable scripts
├── msg/ # Custom message definitions
├── srv/ # Custom service definitions
├── action/ # Custom action definitions
└── setup.py # Python package setup (for Python packages)

Package Metadata (package.xml)

The package.xml file contains metadata about the package and its dependencies:

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>my_robot_package</name>
<version>0.1.0</version>
<description>A package for my robot functionality</description>
<maintainer email="user@example.com">User Name</maintainer>
<license>Apache-2.0</license>

<buildtool_depend>ament_cmake</buildtool_depend>

<depend>rclpy</depend>
<depend>std_msgs</depend>
<depend>sensor_msgs</depend>
<depend>geometry_msgs</depend>

<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>

<export>
<build_type>ament_python</build_type>
</export>
</package>

Creating Packages

Packages can be created using the ros2 pkg create command:

# Create a Python package
ros2 pkg create --build-type ament_python my_python_package

# Create a C++ package
ros2 pkg create --build-type ament_cmake my_cpp_package

# Create a package with dependencies
ros2 pkg create --build-type ament_python my_package \
--dependencies rclpy std_msgs sensor_msgs geometry_msgs

Python Package Setup (setup.py)

For Python packages, the setup.py file configures the Python build process:

from setuptools import setup
import os
from glob import glob

package_name = 'my_robot_package'

setup(
name=package_name,
version='0.1.0',
packages=[package_name],
data_files=[
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
(os.path.join('share', package_name, 'launch'), glob('launch/*.py')),
(os.path.join('share', package_name, 'config'), glob('config/*.yaml')),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='User Name',
maintainer_email='user@example.com',
description='A package for my robot functionality',
license='Apache-2.0',
tests_require=['pytest'],
entry_points={
'console_scripts': [
'my_node = my_robot_package.my_node:main',
'another_node = my_robot_package.another_node:main',
],
},
)

Package Management Tools

ROS 2 provides several tools for managing packages:

  • ros2 pkg: Package management commands

    • ros2 pkg list: List all available packages
    • ros2 pkg xml <package_name>: Show package.xml content
    • ros2 pkg executables <package_name>: List executables in a package
  • colcon: Build system for ROS 2 packages

    • colcon build: Build all packages in the workspace
    • colcon build --packages-select <package_name>: Build specific package
    • colcon test: Run tests for packages

Package Dependencies

Dependencies in ROS 2 packages are categorized as:

  • buildtool_depend: Build system dependencies (e.g., ament_cmake)
  • build_depend: Dependencies needed during build time
  • exec_depend: Dependencies needed at runtime
  • test_depend: Dependencies needed for testing
  • depend: Shorthand for both build and exec dependencies

Best Practices for Package Organization

  • Use descriptive package names that clearly indicate functionality
  • Follow the ROS 2 naming conventions (lowercase, underscores)
  • Keep related functionality together in the same package
  • Separate different types of functionality into different packages
  • Use appropriate dependency management
  • Include proper documentation and examples
  • Write comprehensive tests for your packages

Code Examples

Service Server Example

import rclpy
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts

class ServiceServerNode(Node):
def __init__(self):
super().__init__('service_server_node')
self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_two_ints_callback)
self.get_logger().info('Service Server Node initialized')

def add_two_ints_callback(self, request, response):
response.sum = request.a + request.b
self.get_logger().info(f'Returning {request.a} + {request.b} = {response.sum}')
return response

def main(args=None):
rclpy.init(args=args)
service_server_node = ServiceServerNode()

try:
rclpy.spin(service_server_node)
except KeyboardInterrupt:
pass
finally:
service_server_node.destroy_node()
rclpy.shutdown()

if __name__ == '__main__':
main()

Service Client Example

import sys
import rclpy
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts

class ServiceClientNode(Node):
def __init__(self):
super().__init__('service_client_node')
self.cli = self.create_client(AddTwoInts, 'add_two_ints')
while not self.cli.wait_for_service(timeout_sec=1.0):
self.get_logger().info('Service not available, waiting again...')
self.req = AddTwoInts.Request()
self.get_logger().info('Service Client Node initialized')

def send_request(self, a, b):
self.req.a = a
self.req.b = b
self.future = self.cli.call_async(self.req)
rclpy.spin_until_future_complete(self, self.future)
return self.future.result()

def main(args=None):
rclpy.init(args=args)
service_client_node = ServiceClientNode()

try:
a = int(sys.argv[1]) if len(sys.argv) > 1 else 1
b = int(sys.argv[2]) if len(sys.argv) > 2 else 2

response = service_client_node.send_request(a, b)
service_client_node.get_logger().info(f'Result of {a} + {b} = {response.sum}')
except Exception as e:
service_client_node.get_logger().error(f'Service call failed: {e}')
finally:
service_client_node.destroy_node()
rclpy.shutdown()

if __name__ == '__main__':
main()

Summary

ROS 2 services provide synchronous request-response communication for operations with clear start and end points. Packages organize related code, dependencies, and resources into distributable units, enabling modular and maintainable robotics software development.

Key Takeaways

Key Takeaways
  • Services enable synchronous request-response communication patterns
  • Use services for operations with clear beginning and end, topics for continuous data
  • Packages organize code into modular, distributable units
  • Proper dependency management ensures reliable package builds and deployments

What's Next

In the next chapter, we'll explore Python agent integration with ROS 2 controllers, learning how to connect high-level AI decision-making with low-level robot control.

AI Assistant
How can I help you today?