Actions
Actions in ROS 2 provide a high-level, asynchronous communication mechanism for long-running goals that require periodic feedback. They are built on top of topics and services and are particularly useful for tasks like navigating a robot to a goal, performing a complex manipulation sequence, or executing a pick-and-place operation.
An Action involves:
- Goal: The desired state or task to be achieved.
- Feedback: Periodic updates on the progress of the action.
- Result: The final outcome of the action once it's complete.
Goal/Feedback/Result Model
- Action Server: A node that receives action goals, executes the task, publishes feedback as the task progresses, and finally sends the result.
- Action Client: A node that sends an action goal to an action server, monitors the feedback, and processes the final result.
- Action Name: A unique string identifier for the action (e.g.,
/navigate_to_pose,/gripper_control). - Action Definition: Defined by
.actionfiles, which explicitly define the structure of the goal, feedback, and result messages. The syntax is similar to services, but with three sections separated by---.
Creating an Action (Python Example)
Let's create a simple action for a "Countdown" task.
Define the Action (Countdown.action)
First, create an action directory inside ~/ros2_ws/src/my_python_pkg (if it doesn't exist) and then Countdown.action:
int32 target_number
---
int32 final_count
---
int32 current_count
Update package.xml
Similar to services, you need to declare that your package exports an action. Add these lines to ~/ros2_ws/src/my_python_pkg/package.xml:
<buildtool_depend>ament_cmake</buildtool_depend>
<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
Update setup.py
You also need to tell setup.py to process the .action files.
Modify the data_files section in ~/ros2_ws/src/my_python_pkg/setup.py:
from setuptools import find_packages, setup
import os
from glob import glob
package_name = 'my_python_pkg'
setup(
name=package_name,
version='0.0.0',
packages=find_packages(exclude=['test']),
data_files=[
('share/' + package_name, ['package.xml']),
('share/' + package_name + '/srv', glob('srv/*.srv')),
('share/' + package_name + '/action', glob('action/*.action')), # Add this line
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
(os.path.join('share', package_name, 'launch'), glob(os.path.join('launch', '*launch.[pxy][yeml]*'))),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='your_name',
maintainer_email='your_email@example.com',
description='TODO: Package description',
license='TODO: License declaration',
tests_require=['pytest'],
entry_points={
'console_scripts': [
'talker = my_python_pkg.talker_node:main',
'talker_publisher = my_python_pkg.talker_publisher:main',
'listener_subscriber = my_python_pkg.listener_subscriber:main',
'add_two_ints_server = my_python_pkg.add_two_ints_server:main',
'add_two_ints_client = my_python_pkg.add_two_ints_client:main',
'countdown_action_server = my_python_pkg.countdown_action_server:main',
'countdown_action_client = my_python_pkg.countdown_action_client:main',
],
},
)
Action Server (countdown_action_server.py)
Create ~/ros2_ws/src/my_python_pkg/my_python_pkg/countdown_action_server.py:
import time
import rclpy
from rclpy.action import ActionServer
from rclpy.node import Node
from my_python_pkg.action import Countdown # Import your action type
class CountdownActionServer(Node):
def __init__(self):
super().__init__('countdown_action_server')
self._action_server = ActionServer(
self,
Countdown,
'countdown',
self.execute_callback)
self.get_logger().info('Countdown Action Server ready.')
async def execute_callback(self, goal_handle):
self.get_logger().info('Executing goal...')
feedback_msg = Countdown.Feedback()
for i in range(goal_handle.request.target_number, 0, -1):
feedback_msg.current_count = i
self.get_logger().info(f'Feedback: {i}')
goal_handle.publish_feedback(feedback_msg)
time.sleep(1)
goal_handle.succeed()
result = Countdown.Result()
result.final_count = 0 # Counted down to 0
self.get_logger().info('Goal succeeded!')
return result
def main(args=None):
rclpy.init(args=args)
countdown_action_server = CountdownActionServer()
rclpy.spin(countdown_action_server)
rclpy.shutdown()
if __name__ == '__main__':
main()
Action Client (countdown_action_client.py)
Create ~/ros2_ws/src/my_python_pkg/my_python_pkg/countdown_action_client.py:
import rclpy
from rclpy.action import ActionClient
from rclpy.node import Node
from my_python_pkg.action import Countdown # Import your action type
class CountdownActionClient(Node):
def __init__(self):
super().__init__('countdown_action_client')
self._action_client = ActionClient(self, Countdown, 'countdown')
self.get_logger().info('Countdown Action Client ready.')
def send_goal(self, target_number):
self.get_logger().info('Waiting for action server...')
self._action_client.wait_for_server()
goal_msg = Countdown.Goal()
goal_msg.target_number = target_number
self.get_logger().info('Sending goal request...')
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 get_result_callback(self, future):
result = future.result().result
self.get_logger().info(f'Result: Final count {result.final_count}')
rclpy.shutdown()
def feedback_callback(self, feedback_msg):
self.get_logger().info(f'Received feedback: {feedback_msg.feedback.current_count}')
def main(args=None):
rclpy.init(args=args)
action_client = CountdownActionClient()
action_client.send_goal(5) # Example: countdown from 5
rclpy.spin(action_client)
#rclpy.shutdown() # Shutdown handled in get_result_callback for this example
if __name__ == '__main__':
main()
Building and Running
- Build the workspace (including new action definition):
cd ~/ros2_ws
colcon build --packages-select my_python_pkg
source install/setup.bash - Run the action server in one terminal:
ros2 run my_python_pkg countdown_action_server - Run the action client in another terminal:
You should see the client sending a goal, the server executing it with feedback, and finally sending a result.
ros2 run my_python_pkg countdown_action_client
ros2 action Commands
ros2 action list: Lists all active actions.ros2 action info <action_name>: Displays information about an action, including clients and servers.ros2 action send_goal <action_name> <action_type> <goal_json>: Sends a goal to an action server from the command line.
Actions provide a robust way to handle complex, long-running tasks in a robotic system, giving clients better control and visibility over the task's progress.