Solutions: ROS 2 Architecture and Communication Patterns
Solution for Exercise 1: Basic Publisher-Subscriber
Complete Publisher Code
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from std_msgs.msg import Float64
import random
class TemperaturePublisher(Node):
def __init__(self):
super().__init__('temperature_publisher')
self.publisher = self.create_publisher(Float64, '/temperature', 10)
timer_period = 2.0 # seconds
self.timer = self.create_timer(timer_period, self.timer_callback)
self.get_logger().info('Temperature publisher started')
def timer_callback(self):
msg = Float64()
# Generate random temperature between 18 and 25 degrees Celsius
msg.data = random.uniform(18.0, 25.0)
self.publisher.publish(msg)
self.get_logger().info(f'Published temperature: {msg.data:.2f}°C')
def main(args=None):
rclpy.init(args=args)
temperature_publisher = TemperaturePublisher()
try:
rclpy.spin(temperature_publisher)
except KeyboardInterrupt:
pass
finally:
temperature_publisher.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Complete Subscriber Code
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from std_msgs.msg import Float64
class TemperatureSubscriber(Node):
def __init__(self):
super().__init__('temperature_subscriber')
self.subscription = self.create_subscription(
Float64,
'/temperature',
self.listener_callback,
10)
self.subscription # prevent unused variable warning
self.get_logger().info('Temperature subscriber started')
def listener_callback(self, msg):
temperature = msg.data
if 20.0 <= temperature <= 24.0:
status = "Comfortable temperature range"
else:
status = "Temperature outside comfortable range"
self.get_logger().info(f'Received temperature: {temperature:.2f}°C - {status}')
def main(args=None):
rclpy.init(args=args)
temperature_subscriber = TemperatureSubscriber()
try:
rclpy.spin(temperature_subscriber)
except KeyboardInterrupt:
pass
finally:
temperature_subscriber.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
How to Run
- Save the publisher code as
temperature_publisher.py - Save the subscriber code as
temperature_subscriber.py - Make sure your ROS 2 environment is sourced
- Run the publisher:
python3 temperature_publisher.py - In another terminal, run the subscriber:
python3 temperature_subscriber.py
Solution for Exercise 2: Service-Based Calculator
Service Definition
First, create a service definition file Calculator.srv in a srv directory:
# Request
float64 a
float64 b
string operation # add, subtract, multiply, divide
---
# Response
float64 result
bool success
string message
Service Server Code
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from your_package.srv import Calculator # Replace with your actual package name
class CalculatorService(Node):
def __init__(self):
super().__init__('calculator_service')
self.srv = self.create_service(Calculator, 'calculate', self.calculate_callback)
def calculate_callback(self, request, response):
a = request.a
b = request.b
operation = request.operation
if operation == 'add':
response.result = a + b
elif operation == 'subtract':
response.result = a - b
elif operation == 'multiply':
response.result = a * b
elif operation == 'divide':
if b == 0:
response.success = False
response.message = 'Error: Division by zero'
return response
response.result = a / b
else:
response.success = False
response.message = f'Error: Unknown operation {operation}'
return response
response.success = True
response.message = f'Success: {a} {operation} {b} = {response.result}'
self.get_logger().info(response.message)
return response
def main(args=None):
rclpy.init(args=args)
calculator_service = CalculatorService()
try:
rclpy.spin(calculator_service)
except KeyboardInterrupt:
pass
finally:
calculator_service.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Service Client Code
#!/usr/bin/env python3
import sys
import rclpy
from rclpy.node import Node
from your_package.srv import Calculator # Replace with your actual package name
class CalculatorClient(Node):
def __init__(self):
super().__init__('calculator_client')
self.cli = self.create_client(Calculator, 'calculate')
while not self.cli.wait_for_service(timeout_sec=1.0):
self.get_logger().info('Service not available, waiting again...')
def send_request(self, a, b, operation):
request = Calculator.Request()
request.a = a
request.b = b
request.operation = operation
future = self.cli.call_async(request)
return future
def main(args=None):
rclpy.init(args=args)
calculator_client = CalculatorClient()
if len(sys.argv) != 4:
print('Usage: python3 calculator_client.py <num1> <num2> <operation>')
print('Operations: add, subtract, multiply, divide')
return
a = float(sys.argv[1])
b = float(sys.argv[2])
operation = sys.argv[3]
future = calculator_client.send_request(a, b, operation)
rclpy.spin_until_future_complete(calculator_client, future)
try:
response = future.result()
if response.success:
print(f'Result: {response.message}')
else:
print(f'Error: {response.message}')
except Exception as e:
print(f'Service call failed: {e}')
calculator_client.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Solution for Exercise 3: Action-Based Navigation
Action Definition
Create an action definition file Navigate.action in an action directory:
# Goal
float64 target_x
float64 target_y
---
# Result
float64 final_x
float64 final_y
bool success
string message
---
# Feedback
float64 current_x
float64 current_y
float64 distance_remaining
int32 percentage_complete
Action Server Code
#!/usr/bin/env python3
import rclpy
from rclpy.action import ActionServer
from rclpy.node import Node
from your_package.action import Navigate # Replace with your actual package name
import time
class NavigateActionServer(Node):
def __init__(self):
super().__init__('navigate_action_server')
self._action_server = ActionServer(
self,
Navigate,
'navigate',
self.execute_callback)
def execute_callback(self, goal_handle):
self.get_logger().info('Received navigation goal')
# Simulate navigation
start_x, start_y = 0.0, 0.0 # Starting position
target_x = goal_handle.request.target_x
target_y = goal_handle.request.target_y
# Calculate distance
total_distance = ((target_x - start_x)**2 + (target_y - start_y)**2)**0.5
current_x, current_y = start_x, start_y
# Navigation simulation
step_size = 0.1 # Move 0.1 units per step
steps = int(total_distance / step_size)
feedback_msg = Navigate.Feedback()
for i in range(steps + 1):
if goal_handle.is_cancel_requested:
goal_handle.canceled()
result = Navigate.Result()
result.success = False
result.message = 'Goal canceled'
return result
# Calculate current position
ratio = i / steps if steps > 0 else 1.0
current_x = start_x + (target_x - start_x) * ratio
current_y = start_y + (target_y - start_y) * ratio
# Calculate remaining distance
distance_remaining = ((target_x - current_x)**2 + (target_y - current_y)**2)**0.5
percentage_complete = int(ratio * 100)
# Publish feedback
feedback_msg.current_x = current_x
feedback_msg.current_y = current_y
feedback_msg.distance_remaining = distance_remaining
feedback_msg.percentage_complete = percentage_complete
goal_handle.publish_feedback(feedback_msg)
self.get_logger().info(f'Progress: {percentage_complete}% - Distance remaining: {distance_remaining:.2f}')
# Simulate movement time
time.sleep(0.1)
# Complete the goal
goal_handle.succeed()
result = Navigate.Result()
result.final_x = current_x
result.final_y = current_y
result.success = True
result.message = f'Navigation completed successfully to ({current_x:.2f}, {current_y:.2f})'
self.get_logger().info(result.message)
return result
def main(args=None):
rclpy.init(args=args)
navigate_action_server = NavigateActionServer()
try:
rclpy.spin(navigate_action_server)
except KeyboardInterrupt:
pass
finally:
navigate_action_server.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Action Client Code
#!/usr/bin/env python3
import rclpy
from rclpy.action import ActionClient
from rclpy.node import Node
from your_package.action import Navigate # Replace with your actual package name
import time
class NavigateActionClient(Node):
def __init__(self):
super().__init__('navigate_action_client')
self._action_client = ActionClient(
self,
Navigate,
'navigate')
def send_goal(self, target_x, target_y):
goal_msg = Navigate.Goal()
goal_msg.target_x = target_x
goal_msg.target_y = target_y
self._action_client.wait_for_server()
# Send the goal
self._send_goal_future = self._action_client.send_goal_async(
goal_msg,
feedback_callback=self.feedback_callback)
self._send_goal_future.add_done_callback(self.goal_response_callback)
def goal_response_callback(self, future):
goal_handle = future.result()
if not goal_handle.accepted:
self.get_logger().info('Goal rejected')
return
self.get_logger().info('Goal accepted')
self._get_result_future = goal_handle.get_result_async()
self._get_result_future.add_done_callback(self.get_result_callback)
def feedback_callback(self, feedback_msg):
feedback = feedback_msg.feedback
self.get_logger().info(
f'Current position: ({feedback.current_x:.2f}, {feedback.current_y:.2f}), '
f'Distance remaining: {feedback.distance_remaining:.2f}, '
f'Progress: {feedback.percentage_complete}%')
def get_result_callback(self, future):
result = future.result().result
self.get_logger().info(f'Result: {result.message}')
def main(args=None):
rclpy.init(args=args)
action_client = NavigateActionClient()
# Send a goal to navigate to position (5.0, 3.0)
action_client.send_goal(5.0, 3.0)
try:
rclpy.spin(action_client)
except KeyboardInterrupt:
pass
finally:
action_client.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Solution for Exercise 4: QoS Configuration Challenge
QoS Example Code
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from std_msgs.msg import Float64
from rclpy.qos import QoSProfile, QoSReliabilityPolicy, QoSHistoryPolicy
class QoSPublisher(Node):
def __init__(self):
super().__init__('qos_publisher')
# QoS profile with RELIABLE reliability and KEEP_LAST history
qos_profile_reliable = QoSProfile(
depth=10,
reliability=QoSReliabilityPolicy.RELIABLE,
history=QoSHistoryPolicy.KEEP_LAST
)
self.publisher_reliable = self.create_publisher(Float64, 'temperature_reliable', qos_profile_reliable)
# QoS profile with BEST_EFFORT reliability and KEEP_ALL history
qos_profile_best_effort = QoSProfile(
depth=10,
reliability=QoSReliabilityPolicy.BEST_EFFORT,
history=QoSHistoryPolicy.KEEP_ALL
)
self.publisher_best_effort = self.create_publisher(Float64, 'temperature_best_effort', qos_profile_best_effort)
timer_period = 1.0 # seconds
self.timer_reliable = self.create_timer(timer_period, self.timer_callback_reliable)
self.timer_best_effort = self.create_timer(timer_period + 0.5, self.timer_callback_best_effort)
self.counter = 0
def timer_callback_reliable(self):
msg = Float64()
msg.data = 20.0 + (self.counter % 5) # Vary temperature slightly
self.publisher_reliable.publish(msg)
self.get_logger().info(f'Published (RELIABLE): {msg.data:.2f}')
self.counter += 1
def timer_callback_best_effort(self):
msg = Float64()
msg.data = 25.0 + (self.counter % 5) # Vary temperature slightly
self.publisher_best_effort.publish(msg)
self.get_logger().info(f'Published (BEST_EFFORT): {msg.data:.2f}')
def main(args=None):
rclpy.init(args=args)
qos_publisher = QoSPublisher()
try:
rclpy.spin(qos_publisher)
except KeyboardInterrupt:
pass
finally:
qos_publisher.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Solution for Exercise 5: Parameter-Based Configuration
Parameter-Based Node
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from rclpy.parameter import Parameter
from std_msgs.msg import String
from rcl_interfaces.msg import SetParametersResult
class ParameterNode(Node):
def __init__(self):
super().__init__('parameter_node')
# Declare parameters with default values
self.declare_parameter('publish_rate', 1.0) # Hz
self.declare_parameter('message_content', 'Hello from parameter node!')
self.declare_parameter('node_name', 'parameter_node')
# Get parameter values
self.publish_rate = self.get_parameter('publish_rate').value
self.message_content = self.get_parameter('message_content').value
self.node_name = self.get_parameter('node_name').value
# Create publisher
self.publisher = self.create_publisher(String, 'parameter_topic', 10)
# Create timer based on parameter
self.timer = self.create_timer(1.0 / self.publish_rate, self.timer_callback)
# Register parameter callback
self.add_on_set_parameters_callback(self.parameter_callback)
self.get_logger().info(f'Parameter node initialized with rate: {self.publish_rate}Hz')
def timer_callback(self):
msg = String()
msg.data = f'[{self.node_name}] {self.message_content} (Rate: {self.publish_rate}Hz)'
self.publisher.publish(msg)
self.get_logger().info(f'Published: {msg.data}')
def parameter_callback(self, params):
for param in params:
if param.name == 'publish_rate':
self.publish_rate = param.value
# Update timer with new rate
self.destroy_timer(self.timer)
self.timer = self.create_timer(1.0 / self.publish_rate, self.timer_callback)
self.get_logger().info(f'Publish rate updated to: {self.publish_rate}Hz')
elif param.name == 'message_content':
self.message_content = param.value
self.get_logger().info(f'Message content updated to: {self.message_content}')
elif param.name == 'node_name':
self.node_name = param.value
self.get_logger().info(f'Node name updated to: {self.node_name}')
return SetParametersResult(successful=True)
def main(args=None):
rclpy.init(args=args)
parameter_node = ParameterNode()
try:
rclpy.spin(parameter_node)
except KeyboardInterrupt:
pass
finally:
parameter_node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
How to Test Parameters
# Run the node
ros2 run your_package parameter_node
# In another terminal, change parameters
ros2 param set /parameter_node publish_rate 2.0
ros2 param set /parameter_node message_content "New parameter value!"
Implementation Guide
For Exercise 1:
- Make sure to properly initialize rclpy with
rclpy.init() - Use
rclpy.spin()to keep nodes running - Always properly clean up with
destroy_node()andrclpy.shutdown() - Use appropriate QoS settings for your use case
For Exercise 2:
- Define custom service interfaces in
.srvfiles - Use
create_service()to create service servers - Use
create_client()to create service clients - Handle edge cases like division by zero
- Properly handle asynchronous service calls
For Exercise 3:
- Define custom action interfaces in
.actionfiles - Use
ActionServerfor action servers - Use
ActionClientfor action clients - Implement proper feedback publishing during execution
- Handle goal cancellation and preemption
For Exercise 4:
- Import QoS classes from
rclpy.qos - Create
QoSProfilewith desired settings - Apply profiles to publishers and subscribers
- Understand the trade-offs between different QoS settings
For Exercise 5:
- Use
declare_parameter()to declare parameters - Use
get_parameter()to retrieve parameter values - Implement parameter callbacks with
add_on_set_parameters_callback() - Test parameter changes using ROS 2 command line tools
Best Practices
- Error Handling: Always implement proper error handling for services and actions
- Resource Management: Properly clean up resources when nodes are destroyed
- Logging: Use appropriate log levels (info, warn, error) for different messages
- Documentation: Comment your code to explain the purpose of different sections
- Testing: Test your nodes in isolation before integrating them into larger systems
- QoS Selection: Choose appropriate QoS settings based on your application requirements
- Parameter Validation: Validate parameter values to prevent unexpected behavior
Common Pitfalls to Avoid
- Forgetting to initialize rclpy: Always call
rclpy.init()before creating nodes - Not handling node destruction: Always properly clean up nodes and shutdown rclpy
- Using inappropriate QoS settings: Consider your application's requirements for reliability and performance
- Not handling service/client timeouts: Implement appropriate timeout handling for services
- Ignoring parameter validation: Validate parameters to prevent invalid configurations
- Not using proper message types: Use appropriate standard message types from std_msgs or geometry_msgs when possible
These solutions provide complete implementations for each exercise, demonstrating best practices for ROS 2 development. Use these as references when implementing your own solutions, and feel free to modify them to explore different approaches or add additional functionality.