Week 5: Actions for Long-Running Intelligent Tasks

Learning Overview

What You'll Learn

Action Concepts

Understanding actions for complex robotic tasks with feedback

Feedback Mechanisms

Goal, feedback, and result patterns for monitoring

Server-Client Pattern

Action servers and clients architecture

Long-Running Tasks

Monitoring and canceling operations

Project Deliverables
  • Navigation Action Server Drone/robot compatible autonomous navigation
  • Client with Feedback Monitoring Real-time progress tracking and telemetry
  • Demo Video Navigation with dynamic replanning demonstration

Core Concepts

What are ROS2 Actions?

Topics

One-way streaming data
Fire and forget

Services

Request-response
Quick operations

Actions ⭐

Goal-Feedback-Result
Long-running tasks

Action Components

What the client wants to achieve

  • Sent by the action client to the action server
  • Example: "Navigate to waypoint (x: 10.0, y: 5.0, z: 2.0)"
  • Can be accepted, rejected, or cancelled
# 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

  • Sent from server to client while processing
  • Example: "Current position: (3.5, 2.1, 1.8), Distance remaining: 7.2m"
  • Allows client to monitor progress in real-time
# 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

  • Sent once when action finishes
  • Example: "Success! Final position: (10.0, 5.0, 2.0), Total time: 25.3s"
  • Indicates success, failure, or cancellation
# 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"
Interactive Action Demo

Click "Start Mission" to see how actions work in real-time

1
2
3
Status:
Ready to start mission
Feedback Messages:

Feedback will appear here...

Action Server-Client Architecture

Communication Flow

Action Server

Responsibilities:

  • Receives goal requests
  • Executes the long-running task
  • Publishes periodic feedback
  • Sends final result
  • Handles cancellation requests
Execute Task
Send Updates
Action Client

Responsibilities:

  • Sends goal to server
  • Receives feedback updates
  • Monitors task progress
  • Gets final result
  • Can cancel ongoing tasks
Send Goal
Monitor Progress
Action Client Navigation Node Action Server Drone Controller 1. Send Goal 2. Feedback (continuous) 3. Result Cancel (optional)

Package Setup Guide

Complete Directory Structure

~/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)

Step-by-Step Setup Process

# Create and navigate to workspace
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src

Important: Interface packages MUST use 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
The three sections MUST be separated by exactly --- 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>

Critical: Build interfaces FIRST, then source, then build implementation packages!
# 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
If you see your action definition, congratulations! The interface is ready to use.

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
Open YouTube Video

Common Mistakes to Avoid

Wrong
Wrong Package Type
ros2 pkg create --build-type ament_python custom_interfaces Interface packages MUST be ament_cmake!
Correct
Correct Package Type
ros2 pkg create --build-type ament_cmake custom_interfaces Always use ament_cmake for interfaces!
Wrong
Wrong File Location
drone_navigation/action/Navigate.action Actions in implementation package won't work!
Correct
Correct File Location
custom_interfaces/action/Navigate.action Separate interface package!
Wrong
Forgetting to Source
colcon build --packages-select custom_interfaces
colcon build --packages-select drone_navigation
Missing source command causes import errors!
Correct
Always Source After Building
colcon build --packages-select custom_interfaces
source install/setup.bash
colcon build --packages-select drone_navigation
Source after building interfaces!

Quick Reference

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

Implementation Guide

Creating an Action Server

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()
Key Points:
  • Always check goal_handle.is_cancel_requested during execution
  • Call goal_handle.succeed() or goal_handle.canceled()
  • Publish feedback regularly to keep client informed
Creating an Action Client

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()
Pro Tips:
  • Use wait_for_server() to ensure server is ready
  • Implement all three callbacks: goal response, feedback, and result
  • Handle cancellation with cancel_goal_async() if needed
Defining Action Interfaces

Action interfaces define the structure of goals, feedback, and results.

File Location: 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
CMakeLists.txt Configuration
find_package(rosidl_default_generators REQUIRED)
find_package(geometry_msgs REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  "action/Navigate.action"
  DEPENDENCIES geometry_msgs
)
package.xml Dependencies
<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>

Hands-on Project

Project 5.1: Intelligent Flight/Navigation Action Server

Build a complete navigation system using ROS2 actions for autonomous drone/robot control

Project Requirements
Action Server
  • Create navigation action server
  • Support ground/aerial vehicles
  • Provide real-time progress feedback
  • Implement obstacle detection simulation
  • Support goal cancellation
Action Client
  • Monitor navigation progress
  • Display real-time telemetry
  • Handle dynamic goal updates
  • Support waypoint replanning
  • Implement cancellation logic
Features
  • Obstacle detection simulation
  • Dynamic goal cancellation
  • Waypoint navigation
  • Mission replanning
  • Battery/telemetry monitoring
Deliverables
  • Working action server code
  • Working action client code
  • Custom action interface
  • Demo video (with replanning)
  • Documentation/README
Implementation Checklist
Quick Start Commands
# 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

Knowledge Check

Test Your Understanding
Question 1: When should you use ROS2 Actions instead of Services?
When you need to execute long-running tasks with periodic feedback
For quick request-response operations
For streaming sensor data continuously
For publishing static parameters
Question 2: What are the three main components of a ROS2 Action?
Request, Response, Status
Goal, Feedback, Result
Start, Update, End
Input, Process, Output
Question 3: What should the action server check during execution to handle cancellation?
goal_handle.is_active
goal_handle.is_cancel_requested
goal_handle.is_completed
goal_handle.cancel_status
Question 4: How does the client receive progress updates from the server?
By polling the server periodically
Through the feedback_callback function
Via service calls
Through parameter updates
Question 5: In the project, what feature allows the drone to handle unexpected obstacles?
Emergency stop service
Automatic shutdown
Dynamic goal cancellation and replanning
Speed reduction only

Additional Resources

Tools & Commands
# 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