Understanding actions for complex robotic tasks with feedback
Goal, feedback, and result patterns for monitoring
Action servers and clients architecture
Monitoring and canceling operations
One-way streaming data
Fire and forget
Request-response
Quick operations
Goal-Feedback-Result
Long-running tasks
What the client wants to achieve
# Goal Message Example
goal_msg = Navigate.Goal()
goal_msg.target_pose.position.x = 10.0
goal_msg.target_pose.position.y = 5.0
goal_msg.target_pose.position.z = 2.0
Periodic updates during execution
# Feedback Message Example
feedback_msg = Navigate.Feedback()
feedback_msg.current_pose.position.x = 3.5
feedback_msg.current_pose.position.y = 2.1
feedback_msg.distance_remaining = 7.2
feedback_msg.estimated_time_remaining = 15.0
Final outcome when task completes
# Result Message Example
result = Navigate.Result()
result.success = True
result.final_pose.position.x = 10.0
result.final_pose.position.y = 5.0
result.total_elapsed_time = 25.3
result.message = "Navigation completed successfully"
Click "Start Mission" to see how actions work in real-time
Feedback will appear here...
Responsibilities:
Responsibilities:
custom_interfaces/action/Navigate.action?
~/ros2_ws/ # Workspace root
├── src/ # Source packages
│ ├── custom_interfaces/ # ✅ Interface package
│ │ ├── action/ # ✅ Action definitions folder
│ │ │ └── Navigate.action # ✅ YOUR ACTION FILE GOES HERE
│ │ ├── CMakeLists.txt # Build configuration
│ │ └── package.xml # Package metadata
│ │
│ └── drone_navigation/ # Implementation package
│ ├── drone_navigation/ # Python module
│ │ ├── __init__.py
│ │ ├── navigation_server.py # Action server
│ │ └── navigation_client.py # Action client
│ ├── setup.py # Python setup
│ ├── package.xml # Package metadata
│ └── resource/
│ └── drone_navigation
│
├── build/ # Build artifacts (auto-generated)
├── install/ # Installed packages (auto-generated)
└── log/ # Build logs (auto-generated)
The action definition file MUST be placed at:
custom_interfaces/action/Navigate.action
custom_interfaces/ is the package root directoryaction/ is a subdirectory within the packageNavigate.action is your action definition file# Create and navigate to workspace
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
ament_cmake build type!
# Create interface package (MUST be ament_cmake)
ros2 pkg create --build-type ament_cmake custom_interfaces
# Navigate into the package
cd custom_interfaces
# Create action directory
mkdir action
# Verify structure
ls -la
# You should see: action/ CMakeLists.txt package.xml
File: custom_interfaces/action/Navigate.action
cd ~/ros2_ws/src/custom_interfaces/action
nano Navigate.action # or use your favorite editor
File Contents:
# GOAL - What the client requests
geometry_msgs/PoseStamped target_pose
bool avoid_obstacles
float32 max_speed
---
# RESULT - Final outcome
bool success
geometry_msgs/PoseStamped final_pose
float32 total_elapsed_time
float32 final_error
string message
---
# FEEDBACK - Progress updates
geometry_msgs/PoseStamped current_pose
float32 distance_remaining
float32 estimated_time_remaining
float32 battery_percentage
string current_status
--- on its own line
Edit: custom_interfaces/CMakeLists.txt
cmake_minimum_required(VERSION 3.8)
project(custom_interfaces)
# Find dependencies
find_package(ament_cmake REQUIRED)
find_package(rosidl_default_generators REQUIRED)
find_package(geometry_msgs REQUIRED)
find_package(std_msgs REQUIRED)
# Generate interfaces - THIS IS THE KEY LINE
rosidl_generate_interfaces(${PROJECT_NAME}
"action/Navigate.action"
DEPENDENCIES geometry_msgs std_msgs
)
ament_package()
Edit: custom_interfaces/package.xml
<?xml version="1.0"?>
<package format="3">
<name>custom_interfaces</name>
<version>1.0.0</version>
<description>Custom action interfaces</description>
<maintainer email="you@example.com">Your Name</maintainer>
<license>Apache-2.0</license>
<buildtool_depend>ament_cmake</buildtool_depend>
<build_depend>rosidl_default_generators</build_depend>
<build_depend>geometry_msgs</build_depend>
<build_depend>std_msgs</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<exec_depend>geometry_msgs</exec_depend>
<exec_depend>std_msgs</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
</package>
# Navigate to workspace root
cd ~/ros2_ws
# Build ONLY the interface package first
colcon build --packages-select custom_interfaces
# Source the workspace (IMPORTANT!)
source install/setup.bash
# List all action interfaces
ros2 interface list | grep Navigate
# Expected output:
# custom_interfaces/action/Navigate
# Show the complete action definition
ros2 interface show custom_interfaces/action/Navigate
# You should see your Goal, Result, and Feedback definitions
Now create the package that will use your action:
# Navigate to src
cd ~/ros2_ws/src
# Create Python implementation package
ros2 pkg create --build-type ament_python drone_navigation \
--dependencies rclpy custom_interfaces geometry_msgs
# Create Python modules
cd drone_navigation/drone_navigation
touch __init__.py
touch navigation_server.py
touch navigation_client.py
Edit: drone_navigation/setup.py
from setuptools import setup
package_name = 'drone_navigation'
setup(
name=package_name,
version='1.0.0',
packages=[package_name],
data_files=[
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='Your Name',
maintainer_email='you@example.com',
description='Drone navigation using ROS2 actions',
license='Apache-2.0',
tests_require=['pytest'],
entry_points={
'console_scripts': [
'navigation_server = drone_navigation.navigation_server:main',
'navigation_client = drone_navigation.navigation_client:main',
],
},
)
# Navigate to workspace root
cd ~/ros2_ws
# Build all packages
colcon build
# Source the workspace
source install/setup.bash
# Verify both packages
ros2 pkg list | grep -E "(custom_interfaces|drone_navigation)"
# Expected output:
# custom_interfaces
# drone_navigation
ros2 pkg create --build-type ament_python custom_interfaces
Interface packages MUST be ament_cmake!
ros2 pkg create --build-type ament_cmake custom_interfaces
Always use ament_cmake for interfaces!
drone_navigation/action/Navigate.action
Actions in implementation package won't work!
custom_interfaces/action/Navigate.action
Separate interface package!
colcon build --packages-select custom_interfaces
colcon build --packages-select drone_navigation
Missing source command causes import errors!
colcon build --packages-select custom_interfaces
source install/setup.bash
colcon build --packages-select drone_navigation
Source after building interfaces!
| Item | Location / Command |
|---|---|
| Action File | ~/ros2_ws/src/custom_interfaces/action/Navigate.action |
| CMakeLists.txt | ~/ros2_ws/src/custom_interfaces/CMakeLists.txt |
| package.xml | ~/ros2_ws/src/custom_interfaces/package.xml |
| Build Interfaces | colcon build --packages-select custom_interfaces |
| Source Workspace | source install/setup.bash |
| Verify Action | ros2 interface show custom_interfaces/action/Navigate |
| Import in Python | from custom_interfaces.action import Navigate |
The action server executes long-running tasks and provides feedback to clients.
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from rclpy.action import ActionServer
from custom_interfaces.action import Navigate
import time
import math
class NavigationActionServer(Node):
def __init__(self):
super().__init__('navigation_action_server')
self._action_server = ActionServer(
self,
Navigate,
'navigate_to_waypoint',
self.execute_callback
)
self.get_logger().info('Navigation Action Server Started')
def execute_callback(self, goal_handle):
"""Execute the navigation goal"""
self.get_logger().info('Executing navigation goal...')
target_x = goal_handle.request.target_pose.pose.position.x
target_y = goal_handle.request.target_pose.pose.position.y
target_z = goal_handle.request.target_pose.pose.position.z
feedback_msg = Navigate.Feedback()
for i in range(1, 11):
if goal_handle.is_cancel_requested:
goal_handle.canceled()
self.get_logger().info('Goal canceled')
result = Navigate.Result()
result.success = False
result.message = 'Navigation cancelled by client'
return result
feedback_msg.current_pose.pose.position.x = target_x * (i / 10.0)
feedback_msg.current_pose.pose.position.y = target_y * (i / 10.0)
feedback_msg.current_pose.pose.position.z = target_z * (i / 10.0)
dx = target_x - feedback_msg.current_pose.pose.position.x
dy = target_y - feedback_msg.current_pose.pose.position.y
dz = target_z - feedback_msg.current_pose.pose.position.z
feedback_msg.distance_remaining = math.sqrt(dx**2 + dy**2 + dz**2)
feedback_msg.estimated_time_remaining = (10 - i) * 1.0
self.get_logger().info(f'Progress: {i*10}%')
goal_handle.publish_feedback(feedback_msg)
time.sleep(1.0)
goal_handle.succeed()
result = Navigate.Result()
result.success = True
result.final_pose = goal_handle.request.target_pose
result.total_elapsed_time = 10.0
result.message = 'Navigation completed successfully'
self.get_logger().info('Goal succeeded!')
return result
def main(args=None):
rclpy.init(args=args)
navigation_server = NavigationActionServer()
rclpy.spin(navigation_server)
navigation_server.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
goal_handle.is_cancel_requested during executiongoal_handle.succeed() or goal_handle.canceled()The action client sends goals and monitors progress.
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from rclpy.action import ActionClient
from custom_interfaces.action import Navigate
class NavigationActionClient(Node):
def __init__(self):
super().__init__('navigation_action_client')
self._action_client = ActionClient(
self,
Navigate,
'navigate_to_waypoint'
)
self.get_logger().info('Navigation Action Client Started')
def send_goal(self, x, y, z):
"""Send navigation goal to server"""
self.get_logger().info('Waiting for action server...')
self._action_client.wait_for_server()
goal_msg = Navigate.Goal()
goal_msg.target_pose.pose.position.x = x
goal_msg.target_pose.pose.position.y = y
goal_msg.target_pose.pose.position.z = z
goal_msg.target_pose.pose.orientation.w = 1.0
self.get_logger().info(f'Sending goal: ({x}, {y}, {z})')
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().error('Goal rejected by server')
return
self.get_logger().info('Goal accepted by server')
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):
"""Process feedback from server"""
feedback = feedback_msg.feedback
self.get_logger().info(
f'Feedback: Position=({feedback.current_pose.pose.position.x:.2f}, '
f'{feedback.current_pose.pose.position.y:.2f}, '
f'{feedback.current_pose.pose.position.z:.2f}), '
f'Distance Remaining={feedback.distance_remaining:.2f}m, '
f'ETA={feedback.estimated_time_remaining:.1f}s'
)
def get_result_callback(self, future):
"""Process final result"""
result = future.result().result
if result.success:
self.get_logger().info(
f'SUCCESS! {result.message} '
f'(Total time: {result.total_elapsed_time:.1f}s)'
)
else:
self.get_logger().warn(f'FAILED: {result.message}')
rclpy.shutdown()
def main(args=None):
rclpy.init(args=args)
client = NavigationActionClient()
client.send_goal(10.0, 5.0, 2.0)
rclpy.spin(client)
client.destroy_node()
if __name__ == '__main__':
main()
wait_for_server() to ensure server is readycancel_goal_async() if neededAction interfaces define the structure of goals, feedback, and results.
custom_interfaces/action/Navigate.action
# Goal - What we want to achieve
geometry_msgs/PoseStamped target_pose
bool avoid_obstacles
float32 max_speed
---
# Result - Final outcome
bool success
geometry_msgs/PoseStamped final_pose
float32 total_elapsed_time
string message
---
# Feedback - Progress updates
geometry_msgs/PoseStamped current_pose
float32 distance_remaining
float32 estimated_time_remaining
string current_status
find_package(rosidl_default_generators REQUIRED)
find_package(geometry_msgs REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"action/Navigate.action"
DEPENDENCIES geometry_msgs
)
<build_depend>rosidl_default_generators</build_depend>
<build_depend>geometry_msgs</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<exec_depend>geometry_msgs</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
Build a complete navigation system using ROS2 actions for autonomous drone/robot control
# Create workspace and package
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
ros2 pkg create --build-type ament_python drone_navigation
# Create action interface package
ros2 pkg create --build-type ament_cmake custom_interfaces
# Build workspace
cd ~/ros2_ws
colcon build
source install/setup.bash
# Run action server (Terminal 1)
ros2 run drone_navigation navigation_server
# Run action client (Terminal 2)
ros2 run drone_navigation navigation_client
# Monitor actions
ros2 action list
ros2 action info /navigate_to_waypoint
# List all actions
ros2 action list
# Get action info
ros2 action info /action_name
# Send test goal
ros2 action send_goal /action_name \
action_type '{goal_data}'
# Monitor feedback
ros2 action send_goal /action_name \
action_type '{goal_data}' --feedback