Python → ROS Control via rclpy
Chapter Objectives
- Master Python-based ROS 2 development using rclpy
- Implement robot control interfaces in Python
- Create control nodes for humanoid robot actuators
- Integrate with ROS 2 control frameworks
Introduction to rclpy
rclpy is the Python client library for ROS 2. It provides a Python API for creating ROS 2 nodes, publishers, subscribers, services, and actions.
Key Features of rclpy
- Object-oriented API that follows Python conventions
- Asynchronous programming support with asyncio
- Integration with Python's logging system
- Support for all ROS 2 communication patterns
Installation and Setup
rclpy is included with ROS 2 installations, but you can install additional Python packages:
pip3 install rclpy
pip3 install transforms3d # For 3D transformations
pip3 install numpy # For numerical computations
Basic Node Structure with rclpy
import rclpy
from rclpy.node import Node
from rclpy.qos import QoSProfile
import sys
class RobotController(Node):
def __init__(self):
super().__init__('robot_controller')
# Create a QoS profile for reliable communication
qos_profile = QoSProfile(depth=10)
# Initialize controller parameters
self.joint_positions = {}
self.target_positions = {}
self.get_logger().info('Robot Controller initialized')
def main(args=None):
rclpy.init(args=args)
controller = RobotController()
try:
rclpy.spin(controller)
except KeyboardInterrupt:
controller.get_logger().info('Shutting down robot controller...')
finally:
controller.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Implementing Joint Control
For humanoid robots, we need to control multiple joints. Let's create a joint controller:
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import JointState
from trajectory_msgs.msg import JointTrajectory, JointTrajectoryPoint
from control_msgs.msg import JointTrajectoryControllerState
import math
import time
class JointController(Node):
def __init__(self):
super().__init__('joint_controller')
# Joint names for a humanoid robot
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'
]
# Current joint states
self.current_positions = {name: 0.0 for name in self.joint_names}
self.current_velocities = {name: 0.0 for name in self.joint_names}
self.current_efforts = {name: 0.0 for name in self.joint_names}
# Publishers and subscribers
self.joint_state_pub = self.create_publisher(JointState, 'joint_states', 10)
self.joint_command_sub = self.create_subscription(
JointTrajectory, 'joint_trajectory', self.joint_command_callback, 10)
# Timer for publishing joint states
self.timer = self.create_timer(0.05, self.publish_joint_states) # 20 Hz
self.get_logger().info(f'Joint Controller initialized with {len(self.joint_names)} joints')
def joint_command_callback(self, msg):
"""Callback for joint trajectory commands"""
self.get_logger().info(f'Received trajectory with {len(msg.points)} points')
# For now, just execute the first point
if msg.points:
point = msg.points[0]
for i, joint_name in enumerate(msg.joint_names):
if joint_name in self.current_positions:
self.current_positions[joint_name] = point.positions[i]
if len(point.velocities) > i:
self.current_velocities[joint_name] = point.velocities[i]
if len(point.effort) > i:
self.current_efforts[joint_name] = point.effort[i]
def publish_joint_states(self):
"""Publish current joint states"""
msg = JointState()
msg.name = list(self.current_positions.keys())
msg.position = list(self.current_positions.values())
msg.velocity = list(self.current_velocities.values())
msg.effort = list(self.current_efforts.values())
msg.header.stamp = self.get_clock().now().to_msg()
msg.header.frame_id = 'base_link'
self.joint_state_pub.publish(msg)
def main(args=None):
rclpy.init(args=args)
controller = JointController()
try:
rclpy.spin(controller)
except KeyboardInterrupt:
controller.get_logger().info('Shutting down joint controller...')
finally:
controller.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Control Algorithms in Python
Let's implement a PID controller for joint position control:
class PIDController:
def __init__(self, kp=1.0, ki=0.0, kd=0.0, dt=0.01):
self.kp = kp # Proportional gain
self.ki = ki # Integral gain
self.kd = kd # Derivative gain
self.dt = dt # Time step
self.prev_error = 0.0
self.integral = 0.0
def compute(self, setpoint, measurement):
"""Compute control output using PID algorithm"""
error = setpoint - measurement
# Proportional term
p_term = self.kp * error
# Integral term
self.integral += error * self.dt
i_term = self.ki * self.integral
# Derivative term
derivative = (error - self.prev_error) / self.dt
d_term = self.kd * derivative
# Store error for next iteration
self.prev_error = error
# Compute output
output = p_term + i_term + d_term
return output
class JointPIDController(Node):
def __init__(self):
super().__init__('joint_pid_controller')
# Initialize PID controllers for each joint
self.pid_controllers = {}
self.joint_names = [
'left_hip_joint', 'left_knee_joint', 'left_ankle_joint',
'right_hip_joint', 'right_knee_joint', 'right_ankle_joint'
]
for joint_name in self.joint_names:
# Different PID parameters for different joints
if 'hip' in joint_name:
self.pid_controllers[joint_name] = PIDController(kp=2.0, ki=0.1, kd=0.05)
elif 'knee' in joint_name:
self.pid_controllers[joint_name] = PIDController(kp=1.5, ki=0.05, kd=0.03)
else: # ankle
self.pid_controllers[joint_name] = PIDController(kp=1.0, ki=0.02, kd=0.01)
# Current states
self.current_positions = {name: 0.0 for name in self.joint_names}
self.target_positions = {name: 0.0 for name in self.joint_names}
self.control_outputs = {name: 0.0 for name in self.joint_names}
# Publishers and subscribers
self.joint_state_sub = self.create_subscription(
JointState, 'joint_states', self.joint_state_callback, 10)
self.control_pub = self.create_publisher(JointTrajectory, 'joint_trajectory', 10)
# Timer for control loop (100 Hz)
self.control_timer = self.create_timer(0.01, self.control_loop)
self.get_logger().info('Joint PID Controller initialized')
def joint_state_callback(self, msg):
"""Update current joint positions"""
for i, name in enumerate(msg.name):
if name in self.current_positions:
self.current_positions[name] = msg.position[i]
def control_loop(self):
"""Main control loop"""
for joint_name in self.joint_names:
current_pos = self.current_positions[joint_name]
target_pos = self.target_positions[joint_name]
# Compute control output using PID
control_output = self.pid_controllers[joint_name].compute(target_pos, current_pos)
self.control_outputs[joint_name] = control_output
# For this example, we'll just publish the target positions
# In a real system, you would convert the control output to actual motor commands
# Publish the desired trajectory
self.publish_trajectory_command()
def publish_trajectory_command(self):
"""Publish trajectory command"""
msg = JointTrajectory()
msg.joint_names = self.joint_names
point = JointTrajectoryPoint()
point.positions = [self.target_positions[name] for name in self.joint_names]
point.velocities = [0.0] * len(self.joint_names) # Zero velocity
point.time_from_start.sec = 0
point.time_from_start.nanosec = 50000000 # 50ms
msg.points = [point]
self.control_pub.publish(msg)
def set_target_positions(self, targets):
"""Set target positions for joints"""
for joint_name, position in targets.items():
if joint_name in self.target_positions:
self.target_positions[joint_name] = position
Working with ROS 2 Control
ROS 2 Control is the standard framework for robot control. Let's see how to integrate with it:
# my_robot_package/my_robot_package/humanoid_controller.py
import rclpy
from rclpy.node import Node
from controller_manager_msgs.srv import SwitchController
from hardware_interface_msgs.msg import HardwareInterface
import time
class HumanoidController(Node):
def __init__(self):
super().__init__('humanoid_controller')
# Controller management client
self.switch_controller_client = self.create_client(
SwitchController, '/controller_manager/switch_controller')
# Wait for controller manager service
while not self.switch_controller_client.wait_for_service(timeout_sec=1.0):
self.get_logger().info('Controller manager service not available, waiting...')
self.get_logger().info('Humanoid Controller initialized')
def switch_controllers(self, start_controllers, stop_controllers, strictness=1):
"""Switch between controllers"""
request = SwitchController.Request()
request.start_controllers = start_controllers
request.stop_controllers = stop_controllers
request.strictness = strictness
future = self.switch_controller_client.call_async(request)
rclpy.spin_until_future_complete(self, future)
if future.result() is not None:
response = future.result()
if response.ok:
self.get_logger().info(f'Successfully switched controllers')
return True
else:
self.get_logger().error(f'Failed to switch controllers: {response.error_message}')
return False
else:
self.get_logger().error('Failed to call controller manager service')
return False
def initialize_robot(self):
"""Initialize the robot by starting necessary controllers"""
self.get_logger().info('Initializing humanoid robot...')
# Start joint state broadcaster
success = self.switch_controllers(
start_controllers=['joint_state_broadcaster'],
stop_controllers=[]
)
if success:
time.sleep(1.0) # Wait for controller to start
# Start position controllers for all joints
position_controllers = [
'left_leg_position_controller',
'right_leg_position_controller',
'left_arm_position_controller',
'right_arm_position_controller',
'head_position_controller'
]
success = self.switch_controllers(
start_controllers=position_controllers,
stop_controllers=[]
)
if success:
self.get_logger().info('Robot initialized successfully')
else:
self.get_logger().error('Failed to initialize robot')
return success
Asynchronous Programming with rclpy
rclpy supports asyncio for more complex control patterns:
import rclpy
from rclpy.node import Node
from rclpy.executors import MultiThreadedExecutor
from rclpy.callback_groups import MutuallyExclusiveCallbackGroup
import asyncio
class AsyncController(Node):
def __init__(self):
super().__init__('async_controller')
# Create callback groups for threading
self.group1 = MutuallyExclusiveCallbackGroup()
self.group2 = MutuallyExclusiveCallbackGroup()
# Create timers with different callback groups
self.timer1 = self.create_timer(0.1, self.async_task1, callback_group=self.group1)
self.timer2 = self.create_timer(0.2, self.async_task2, callback_group=self.group2)
def async_task1(self):
"""Asynchronous task 1"""
self.get_logger().info('Executing async task 1')
# Simulate some work
time.sleep(0.05)
def async_task2(self):
"""Asynchronous task 2"""
self.get_logger().info('Executing async task 2')
# Simulate some work
time.sleep(0.03)
def main(args=None):
rclpy.init(args=args)
node = AsyncController()
# Use multi-threaded executor for async operations
executor = MultiThreadedExecutor(num_threads=4)
executor.add_node(node)
try:
executor.spin()
except KeyboardInterrupt:
node.get_logger().info('Shutting down async controller...')
finally:
node.destroy_node()
rclpy.shutdown()
Best Practices for Humanoid Control
Performance Considerations
- Use appropriate control frequencies (typically 100-1000 Hz for joint control)
- Minimize computational overhead in control loops
- Use efficient data structures for joint management
- Consider real-time capabilities for critical control tasks
Safety Features
- Implement joint position and velocity limits
- Add emergency stop functionality
- Monitor control effort to detect issues
- Implement graceful degradation when problems occur
Code Organization
- Separate control logic from ROS 2 communication
- Use configuration files for controller parameters
- Implement proper error handling and logging
- Create reusable controller components
Hands-On Exercise
- Create a Python node that controls a simple humanoid robot model
- Implement PID controllers for at least 6 joints
- Create a trajectory publisher that moves the robot through a simple motion
- Add safety limits to prevent joint damage
Summary
Python provides a powerful platform for ROS 2 robot control through rclpy. With proper control algorithms and integration with ROS 2 Control, you can create sophisticated humanoid robot controllers. In the next chapter, we'll explore URDF for humanoid robot modeling.
Learning Path Adjustment
Based on your experience level, you may want to focus on:
- Beginner: Focus on basic node creation and simple control loops
- Intermediate: Dive deeper into PID control and trajectory generation
- Advanced: Explore real-time control, advanced control algorithms, and system integration