Skip to main content

Week 2: Python Integration and Node Development

This week focuses on Python integration with ROS 2 using the rclpy library. You'll learn to create publisher and subscriber nodes, implement service clients and servers, and manage node parameters.

Learning Objectives

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

  • Create publisher and subscriber nodes using rclpy
  • Implement service clients and servers
  • Manage parameters and node lifecycle
  • Create more complex node interactions

2.1 Introduction to rclpy Library

rclpy is the Python client library for ROS 2. It provides the Python API for developing ROS 2 applications and handles the communication between nodes.

Key Components of rclpy

  • Node: The basic execution unit
  • Publisher: Sends messages to topics
  • Subscriber: Receives messages from topics
  • Client: Calls services
  • Service: Provides services
  • Timer: Executes callbacks at regular intervals
  • Parameter: Handles configuration values

Basic Node Structure

import rclpy
from rclpy.node import Node

class MyNode(Node):
def __init__(self):
# Initialize the node with a name
super().__init__('my_node_name')

# Node initialization code goes here
self.get_logger().info('Node initialized')

def main(args=None):
# Initialize ROS 2
rclpy.init(args=args)

# Create node instance
node = MyNode()

# Spin the node (keep it running)
rclpy.spin(node)

# Cleanup
node.destroy_node()
rclpy.shutdown()

if __name__ == '__main__':
main()

2.2 Creating Publisher and Subscriber Nodes

Advanced Publisher Example

Let's create a publisher that sends sensor data:

import rclpy
from rclpy.node import Node
from std_msgs.msg import Float32
from sensor_msgs.msg import JointState
import random

class SensorPublisher(Node):
def __init__(self):
super().__init__('sensor_publisher')

# Create publishers for different sensor types
self.joint_pub = self.create_publisher(JointState, 'joint_states', 10)
self.temperature_pub = self.create_publisher(Float32, 'temperature', 10)

# Create a timer to publish data at regular intervals
timer_period = 0.1 # seconds
self.timer = self.create_timer(timer_period, self.publish_sensor_data)

# Initialize joint names for a humanoid robot
self.joint_names = [
'left_hip', 'left_knee', 'left_ankle',
'right_hip', 'right_knee', 'right_ankle',
'left_shoulder', 'left_elbow', 'left_wrist',
'right_shoulder', 'right_elbow', 'right_wrist'
]

self.get_logger().info('Sensor publisher node started')

def publish_sensor_data(self):
# Publish joint states
joint_msg = JointState()
joint_msg.name = self.joint_names
joint_msg.position = [random.uniform(-1.5, 1.5) for _ in self.joint_names]
joint_msg.velocity = [random.uniform(-0.5, 0.5) for _ in self.joint_names]
joint_msg.effort = [random.uniform(0, 10) for _ in self.joint_names]

self.joint_pub.publish(joint_msg)

# Publish temperature
temp_msg = Float32()
temp_msg.data = random.uniform(20.0, 35.0)
self.temperature_pub.publish(temp_msg)

self.get_logger().info(f'Published joint states and temperature: {temp_msg.data:.2f}°C')

def main(args=None):
rclpy.init(args=args)
node = SensorPublisher()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()

if __name__ == '__main__':
main()

Advanced Subscriber Example

import rclpy
from rclpy.node import Node
from sensor_msgs.msg import JointState
from std_msgs.msg import Float32

class SensorSubscriber(Node):
def __init__(self):
super().__init__('sensor_subscriber')

# Create subscribers
self.joint_sub = self.create_subscription(
JointState,
'joint_states',
self.joint_callback,
10
)

self.temperature_sub = self.create_subscription(
Float32,
'temperature',
self.temperature_callback,
10
)

self.get_logger().info('Sensor subscriber node started')

def joint_callback(self, msg):
self.get_logger().info(f'Received {len(msg.name)} joints')
# Process joint data
for i, name in enumerate(msg.name):
if i < len(msg.position):
self.get_logger().info(f'{name}: pos={msg.position[i]:.2f}, vel={msg.velocity[i]:.2f}')

def temperature_callback(self, msg):
self.get_logger().info(f'Current temperature: {msg.data:.2f}°C')

def main(args=None):
rclpy.init(args=args)
node = SensorSubscriber()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()

if __name__ == '__main__':
main()

2.3 Service Clients and Servers

Service Server Example

First, create a service definition file GetRobotStatus.srv in a srv directory:

# Request
---
# Response
string status
float32 battery_level
bool is_operational

Then implement the service server:

import rclpy
from rclpy.node import Node
from example_interfaces.srv import Trigger # Using built-in service for example
import random

class RobotStatusService(Node):
def __init__(self):
super().__init__('robot_status_service')

# Create a service
self.srv = self.create_service(
Trigger, # Using Trigger as example - you would use your custom service
'get_robot_status',
self.status_callback
)

self.get_logger().info('Robot status service started')

def status_callback(self, request, response):
# Simulate getting robot status
response.success = True
response.message = f'Robot operational at {random.uniform(80, 100):.1f}%'

self.get_logger().info(f'Service called, returning: {response.message}')
return response

def main(args=None):
rclpy.init(args=args)
node = RobotStatusService()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()

if __name__ == '__main__':
main()

Service Client Example

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

class StatusClient(Node):
def __init__(self):
super().__init__('status_client')
self.cli = self.create_client(Trigger, 'get_robot_status')

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

self.req = Trigger.Request()

def send_request(self):
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)
client = StatusClient()
response = client.send_request()

if response is not None:
client.get_logger().info(f'Response: {response.success}, {response.message}')
else:
client.get_logger().info('Service call failed')

client.destroy_node()
rclpy.shutdown()

if __name__ == '__main__':
main()

2.4 Parameter Management and Node Lifecycle

Parameter Declaration and Usage

import rclpy
from rclpy.node import Node
from rclpy.parameter import Parameter

class ParameterNode(Node):
def __init__(self):
super().__init__('parameter_node')

# Declare parameters with default values
self.declare_parameter('robot_name', 'humanoid_robot')
self.declare_parameter('max_velocity', 1.0)
self.declare_parameter('safety_distance', 0.5)

# Get parameter values
self.robot_name = self.get_parameter('robot_name').value
self.max_velocity = self.get_parameter('max_velocity').value
self.safety_distance = self.get_parameter('safety_distance').value

self.get_logger().info(f'Robot name: {self.robot_name}')
self.get_logger().info(f'Max velocity: {self.max_velocity}')
self.get_logger().info(f'Safety distance: {self.safety_distance}')

# Set up parameter callback for dynamic changes
self.add_on_set_parameters_callback(self.parameter_callback)

def parameter_callback(self, params):
for param in params:
if param.name == 'max_velocity' and param.value > 5.0:
return SetParametersResult(successful=False, reason='Max velocity too high')
return SetParametersResult(successful=True)

from rclpy.parameter_service import SetParametersResult

def main(args=None):
rclpy.init(args=args)
node = ParameterNode()

# You can change parameters at runtime
# ros2 param set /parameter_node max_velocity 2.0

rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()

if __name__ == '__main__':
main()

Node Lifecycle Management

import rclpy
from rclpy.node import Node
from rclpy.lifecycle import LifecycleNode, TransitionCallbackReturn
import time

class LifecycleRobotNode(LifecycleNode):
def __init__(self):
super().__init__('lifecycle_robot_node')
self.get_logger().info('Lifecycle node created, current state: unconfigured')

def on_configure(self, state):
self.get_logger().info('Configuring lifecycle node')
# Initialize resources here
self.publisher = self.create_publisher(String, 'robot_status', 10)
return TransitionCallbackReturn.SUCCESS

def on_activate(self, state):
self.get_logger().info('Activating lifecycle node')
# Start operations here
return TransitionCallbackReturn.SUCCESS

def on_deactivate(self, state):
self.get_logger().info('Deactivating lifecycle node')
# Pause operations here
return TransitionCallbackReturn.SUCCESS

def on_cleanup(self, state):
self.get_logger().info('Cleaning up lifecycle node')
# Clean up resources here
self.publisher = None
return TransitionCallbackReturn.SUCCESS

def main(args=None):
rclpy.init(args=args)
node = LifecycleRobotNode()

# Use lifecycle node manager
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()

if __name__ == '__main__':
main()

2.5 Practical Exercise: Building a Sensor Data Publisher

Create a comprehensive node that publishes multiple types of sensor data for a humanoid robot:

import rclpy
from rclpy.node import Node
from sensor_msgs.msg import JointState, Imu, LaserScan
from geometry_msgs.msg import Twist
from std_msgs.msg import Float32MultiArray
import random
import math

class HumanoidSensorPublisher(Node):
def __init__(self):
super().__init__('humanoid_sensor_publisher')

# Create publishers for different sensor types
self.joint_pub = self.create_publisher(JointState, 'joint_states', 10)
self.imu_pub = self.create_publisher(Imu, 'imu/data', 10)
self.laser_pub = self.create_publisher(LaserScan, 'scan', 10)
self.cmd_vel_pub = self.create_publisher(Twist, 'cmd_vel', 10)

# Timer for publishing sensor data
timer_period = 0.05 # 20 Hz
self.timer = self.create_timer(timer_period, self.publish_sensor_data)

# Initialize humanoid joint names
self.joint_names = [
'left_hip_joint', 'left_knee_joint', 'left_ankle_joint',
'right_hip_joint', 'right_knee_joint', 'right_ankle_joint',
'left_shoulder_joint', 'left_elbow_joint', 'left_wrist_joint',
'right_shoulder_joint', 'right_elbow_joint', 'right_wrist_joint',
'head_pan_joint', 'head_tilt_joint'
]

self.get_logger().info('Humanoid sensor publisher started')

def publish_sensor_data(self):
# Publish joint states
joint_msg = JointState()
joint_msg.header.stamp = self.get_clock().now().to_msg()
joint_msg.name = self.joint_names
joint_msg.position = [random.uniform(-1.5, 1.5) for _ in self.joint_names]
joint_msg.velocity = [random.uniform(-0.5, 0.5) for _ in self.joint_names]
joint_msg.effort = [random.uniform(0, 10) for _ in self.joint_names]
self.joint_pub.publish(joint_msg)

# Publish IMU data
imu_msg = Imu()
imu_msg.header.stamp = self.get_clock().now().to_msg()
imu_msg.header.frame_id = 'imu_link'
# Simulate orientation (simplified)
imu_msg.orientation.x = random.uniform(-0.1, 0.1)
imu_msg.orientation.y = random.uniform(-0.1, 0.1)
imu_msg.orientation.z = random.uniform(-0.01, 0.01)
imu_msg.orientation.w = 1.0 # Normalize
self.imu_pub.publish(imu_msg)

# Publish laser scan
scan_msg = LaserScan()
scan_msg.header.stamp = self.get_clock().now().to_msg()
scan_msg.header.frame_id = 'laser_frame'
scan_msg.angle_min = -math.pi / 2
scan_msg.angle_max = math.pi / 2
scan_msg.angle_increment = math.pi / 180 # 1 degree
scan_msg.time_increment = 0.0
scan_msg.scan_time = 0.1
scan_msg.range_min = 0.1
scan_msg.range_max = 10.0
num_ranges = int((scan_msg.angle_max - scan_msg.angle_min) / scan_msg.angle_increment)
scan_msg.ranges = [random.uniform(0.5, 5.0) for _ in range(num_ranges)]
self.laser_pub.publish(scan_msg)

self.get_logger().info('Published sensor data')

def main(args=None):
rclpy.init(args=args)
node = HumanoidSensorPublisher()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()

if __name__ == '__main__':
main()

2.6 Mind Map: Connecting ROS 2 Concepts, Python Code, and URDF

mindmap
root((ROS 2 Concepts))
Nodes
Publisher
rclpy.create_publisher()
Publish messages
Topic-based communication
Subscriber
rclpy.create_subscription()
Receive messages
Callback functions
Service Server
rclpy.create_service()
Request/Response pattern
Service Client
rclpy.create_client()
Call services
Messages
Standard Messages
std_msgs
geometry_msgs
sensor_msgs
Custom Messages
Define .msg files
Generate message types
Parameters
Declare Parameters
declare_parameter()
Get/Set parameters
Dynamic Reconfiguration
Parameter callbacks
Runtime updates
Python Integration
rclpy library
Node class
Publishers/Subscribers
Services/Actions
ROS 2 Client Library
C++ backend
Python bindings
URDF Connection
Robot Description
Joint definitions
Link properties
Joint State Publisher
Publish joint positions
Synchronize with simulation

Summary

This week focused on Python integration with ROS 2 using rclpy. You learned to create complex publisher and subscriber nodes, implement services, manage parameters, and handle node lifecycles. These skills are essential for developing humanoid robot applications.

Next Week Preview

Next week, we'll explore URDF (Unified Robot Description Format) and learn how to create and manipulate robot models for humanoid robots.