【ROS2】动作通信机制 Action 代码示例(C++版)

7 阅读7分钟

在这里插入图片描述

ROS1 系列教程(总目录)

ROS2 系列教程(总目录)

ROS2 系列教程源码、学习群

@TOC

下面通过代码演示自定义 Action 的基本实现。

一、创建功能包

cd ros2_learning/src
ros2 pkg create --build-type ament_cmake action_learning_cpp

其中,

使用 --build-type 指定编译系统为 ament_cmake

action_learning_cpp:自定义功能包名称

生成的目录结构如下:

action_learning_cpp
├── CMakeLists.txt
├── include
│   └── action_learning_cpp
├── LICENSE
├── package.xml
└── src

二、Action服务端

我们编写一个服务端、客户端,实现计算斐波那契数列。

action_learning_cpp/src 目录下新增 action_server_base.cpp 文件,文件内容如下:

/**
 * @file action_server_basic.cpp
 * @brief Action Server 基础 —— 斐波那契数列服务端
 *
 * 知识点:
 * - rclcpp_action::create_server() 创建 Action Server
 * - handle_goal:决定接受/拒绝目标
 * - handle_cancel:决定允许/拒绝取消
 * - handle_accepted:启动执行线程
 * - execute:执行逻辑 + 发布 Feedback + 返回 Result
 *
 * 功能:
 *   客户端指定 order(项数),服务端逐步计算斐波那契数列,
 *   每计算出一项就通过 Feedback 发布当前已生成的部分数列,
 *   最终通过 Result 返回完整的斐波那契数列。
 *
 * 运行:
 *   ros2 run action_learning_cpp action_server_basic
 *   ros2 action send_goal /fibonacci action_learning_cpp/action/Fibonacci "{order: 10}"
 */

#include <rclcpp/rclcpp.hpp>
#include <rclcpp_action/rclcpp_action.hpp>
#include <action_learning_cpp/action/fibonacci.hpp>
#include <thread>

using Fibonacci = action_learning_cpp::action::Fibonacci;

class FibonacciServer : public rclcpp::Node
{
public:
    FibonacciServer() : Node("fibonacci_server")
    {
        action_server_ = rclcpp_action::create_server<Fibonacci>(
            this,
            "fibonacci",
            std::bind(&FibonacciServer::handle_goal, this,
                      std::placeholders::_1, std::placeholders::_2),
            std::bind(&FibonacciServer::handle_cancel, this,
                      std::placeholders::_1),
            std::bind(&FibonacciServer::handle_accepted, this,
                      std::placeholders::_1));

        RCLCPP_INFO(this->get_logger(), "=== Fibonacci Action Server started ===");
        RCLCPP_INFO(thresult_callbackis->get_logger(), "Action name: /fibonacci");
        RCLCPP_INFO(this->get_logger(), "Waiting for client to send goal...");
    }

private:
    rclcpp_action::GoalResponse handle_goal(
        const rclcpp_action::GoalUUID &uuid,
        std::shared_ptr<const Fibonacci::Goal> goal)
    {
        (void)uuid;

        RCLCPP_INFO(this->get_logger(), "Received goal request with order: %ld", goal->order);

        // ── 目标合法性校验 ──
        // 斐波那契数列至少需要 1 项
        if (goal->order <= 0)
        {
            RCLCPP_WARN(this->get_logger(), "Goal rejected: order must be > 0");
            return rclcpp_action::GoalResponse::REJECT;
        }

        RCLCPP_INFO(this->get_logger(), "Goal accepted with order: %ld", goal->order);
        return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
    }

    rclcpp_action::CancelResponse handle_cancel(
        const std::shared_ptr<rclcpp_action::ServerGoalHandle<Fibonacci>> goal_handle)
    {
        RCLCPP_INFO(this->get_logger(), "Cancel request received, cancel accepted");
        (void)goal_handle;
        return rclcpp_action::CancelResponse::ACCEPT;
    }

    void handle_accepted(
        const std::shared_ptr<rclcpp_action::ServerGoalHandle<Fibonacci>> goal_handle)
    {
        // 在新线程中执行,避免阻塞 executor
        std::thread{
            std::bind(&FibonacciServer::execute, this, std::placeholders::_1),
            goal_handle}
            .detach();
    }

    void execute(
        const std::shared_ptr<rclcpp_action::ServerGoalHandle<Fibonacci>> goal_handle)
    {
        RCLCPP_INFO(this->get_logger(), "Executing goal...");

        // 从 GoalHandle 获取目标
        auto goal = goal_handle->get_goal();
        // 创建 Result 和 Feedback 对象
        auto result = std::make_shared<Fibonacci::Result>();
        auto feedback = std::make_shared<Fibonacci::Feedback>();

        rclcpp::Rate loop_rate(1); // 1Hz — 每秒计算一项

        // 初始化斐波那契数列的前两项
        int64_t a = 0, b = 1;

        for (int64_t i = 0; i < goal->order; ++i)
        {
            // ── 检查是否被取消 ──
            if (goal_handle->is_canceling())
            {
                // 返回已计算出的部分数列
                result->sequence = feedback->current_sequence;
                goal_handle->canceled(result);
                RCLCPP_INFO(this->get_logger(),
                            "Goal canceled, computed %ld/%ld terms",
                            i, goal->order);
                return;
            }

            // ── 计算当前项 ──
            int64_t current_value;
            if (i == 0)
            {
                current_value = 0;
            }
            else if (i == 1)
            {
                current_value = 1;
            }
            else
            {
                current_value = a + b;
                a = b;
                b = current_value;
            }

            // ── 更新并发布 Feedback(当前已生成的部分数列)──
            feedback->current_sequence.push_back(current_value);
            goal_handle->publish_feedback(feedback);

            RCLCPP_INFO(this->get_logger(),
                        "Progress: %ld/%ld, current term: %ld",
                        i + 1, goal->order, current_value);

            // 保存到结果序列
            result->sequence.push_back(current_value);

            loop_rate.sleep();
        }

        // ── 执行成功 ──
        goal_handle->succeed(result);
        RCLCPP_INFO(this->get_logger(),
                    "Goal completed! Fibonacci sequence has %ld terms", result->sequence.size());
    }

    rclcpp_action::Server<Fibonacci>::SharedPtr action_server_;
};

int main(int argc, char **argv)
{
    rclcpp::init(argc, argv);
    auto node = std::make_shared<FibonacciServer>();
    rclcpp::spin(node);
    rclcpp::shutdown();
    return 0;
}

其中,line:34 创建了一个action服务端,并绑定回调函数,handle_goal 处理客户端的 send_goal 请求;handle_cancel 处理客户端的 async_cancel_goal 请求;handle_accepted 在服务端通过 handle_goal 返回 ACCEPT 后同步被回调,用于处理 goal 的任务:

action_server_ = rclcpp_action::create_server<Fibonacci>(
	this,
	"fibonacci",
	std::bind(&FibonacciServer::handle_goal, this,
		std::placeholders::_1, std::placeholders::_2),
	std::bind(&FibonacciServer::handle_cancel, this,
		std::placeholders::_1),
	std::bind(&FibonacciServer::handle_accepted, this,
		std::placeholders::_1));

handle_goal 中提供的参数 uuid 是目标请求会话的唯一标识符,可用于日志追踪。

rclcpp_action::GoalResponse handle_goal(
    const rclcpp_action::GoalUUID &uuid,
    std::shared_ptr<const Fibonacci::Goal> goal)

另外,需要注意的是,handle_accepted 应快速返回,避免阻塞执行器。一般处理是在这里创建一个新线程处理goal的任务:

void handle_accepted(const std::shared_ptr<rclcpp_action::ServerGoalHandle<Fibonacci>> goal_handle)
{
    std::thread{
    	std::bind(&FibonacciServer::execute, this, std::placeholders::_1), 
    	goal_handle
    }.detach();
}

三、Action客户端

action_learning_cpp/src 目录下新增 action_server_base.cpp 文件,文件内容如下:

/**
 * @file action_client_basic.cpp
 * @brief Action Client 基础 —— 斐波那契数列客户端
 *
 * 知识点:
 * - rclcpp_action::create_client() 创建 Action Client
 * - wait_for_action_server() 等待服务端上线
 * - async_send_goal() 发送目标
 * - SendGoalOptions 的三个回调:
 *     goal_response_callback —— 服务端接受/拒绝目标的回调
 *     feedback_callback      —— 收到反馈的回调
 *     result_callback        —— 收到最终结果的回调
 *
 * 运行(先启动 action_server_basic):
 *   ros2 run action_learning_cpp action_client_basic
 */

#include <rclcpp/rclcpp.hpp>
#include <rclcpp_action/rclcpp_action.hpp>
#include <action_learning_cpp/action/fibonacci.hpp>
#include <chrono>
#include <sstream>
#include <memory>

using Fibonacci = action_learning_cpp::action::Fibonacci;
using namespace std::chrono_literals;

class FibonacciClient : public rclcpp::Node
{
public:
    FibonacciClient() : Node("fibonacci_client")
    {
        client_ = rclcpp_action::create_client<Fibonacci>(this, "fibonacci");

        RCLCPP_INFO(this->get_logger(), "=== Fibonacci Action Client created ===");

        // 等待服务端上线
        if (!client_->wait_for_action_server(10s))
        {
            RCLCPP_ERROR(this->get_logger(),
                         "Action Server /fibonacci not available, exiting");
            return;
        }

        RCLCPP_INFO(this->get_logger(), "Action Server is available, sending goal...");

        // 发送目标:计算前 10 项斐波那契数列
        send_goal(10);
    }

private:
    // 将数列格式转为字符串,方便日志输出
    std::string format_sequence(const std::vector<int64_t> &seq)
    {
        std::ostringstream oss;
        oss << "[";
        for (size_t i = 0; i < seq.size(); ++i)
        {
            if (i > 0)
                oss << ", ";
            oss << seq[i];
        }
        oss << "]";
        return oss.str();
    }

    // ── 成员函数回调1: goal_response_callback ──
    // 服务端接受或拒绝目标时触发
    void goal_response_callback(const rclcpp_action::ClientGoalHandle<Fibonacci>::SharedPtr &goal_handle)
    {
        if (!goal_handle)
        {
            RCLCPP_ERROR(this->get_logger(), "Goal rejected!");
        }
        else
        {
            RCLCPP_INFO(this->get_logger(),
                        "Goal accepted, waiting for execution...");
        }
    }

    // ── 成员函数回调2: feedback_callback ──
    // 服务端发布反馈时触发
    void feedback_callback(
        rclcpp_action::ClientGoalHandle<Fibonacci>::SharedPtr,
        const std::shared_ptr<const Fibonacci::Feedback> feedback)
    {
        RCLCPP_INFO(this->get_logger(),
                    "Feedback received: %s",
                    format_sequence(feedback->current_sequence).c_str());
    }

    // ── 成员函数回调3: result_callback ──
    // 目标执行完成时触发(成功/失败/取消)
    void result_callback(const rclcpp_action::ClientGoalHandle<Fibonacci>::WrappedResult &result)
    {
        switch (result.code)
        {
        case rclcpp_action::ResultCode::SUCCEEDED:
            RCLCPP_INFO(this->get_logger(),
                        "Goal succeeded! Fibonacci sequence: %s",
                        format_sequence(result.result->sequence).c_str());
            break;
        case rclcpp_action::ResultCode::ABORTED:
            RCLCPP_ERROR(this->get_logger(), "Goal aborted!");
            break;
        case rclcpp_action::ResultCode::CANCELED:
            RCLCPP_WARN(this->get_logger(),
                        "Goal canceled, partial sequence: %s",
                        format_sequence(result.result->sequence).c_str());
            break;
        default:
            RCLCPP_ERROR(this->get_logger(), "Unknown result code");
            break;
        }
    }

    void send_goal(int64_t order)
    {
        // ── 创建目标消息 ──
        auto goal_msg = Fibonacci::Goal();
        goal_msg.order = order;

        // 配置 SendGoalOptions —— 三个核心回调
        auto send_goal_options = rclcpp_action::Client<Fibonacci>::SendGoalOptions();

        // ── 绑定成员函数作为回调 ──
        // 方法1: 使用 std::bind
        send_goal_options.goal_response_callback =
            std::bind(&FibonacciClient::goal_response_callback, this, std::placeholders::_1);

        send_goal_options.feedback_callback = 
            std::bind(&FibonacciClient::feedback_callback, this, std::placeholders::_1, std::placeholders::_2);

        // 方法2: 使用 lambda 包装成员函数(更现代的风格)
        send_goal_options.result_callback = 
            [this](const auto &result)
            {
                this->result_callback(result);
            };

        // ── 发送目标 ──
        RCLCPP_INFO(this->get_logger(), "Sending goal, order: %ld", order);
        client_->async_send_goal(goal_msg, send_goal_options);
    }

    rclcpp_action::Client<Fibonacci>::SharedPtr client_;
};

int main(int argc, char **argv)
{
    rclcpp::init(argc, argv);
    auto node = std::make_shared<FibonacciClient>();
    rclcpp::spin(node);
    rclcpp::shutdown();
    return 0;
}

其中,line:33 创建了一个action客户端,入参为节点指针和Action名称,返回Action客户端指针,用于后续操作。

client_ = rclcpp_action::create_client<Fibonacci>(this, "fibonacci");

line:54 将发送目标封装为一个成员函数,函数中包括创建目标对象、配置目标操作、发送目标等基本流程:

// 1.创建目标消息
auto goal_msg = Fibonacci::Goal();
goal_msg.order = order;

// 2.配置 SendGoalOptions —— 三个核心回调
auto send_goal_options = rclcpp_action::Client<Fibonacci>::SendGoalOptions();
send_goal_options.goal_response_callback = ...
send_goal_options.feedback_callback = ...
send_goal_options.result_callback = ...

// 3.发送目标
client_->async_send_goal(goal_msg, send_goal_options);

其中,注册回调函数展示了两种方法,第一种使用 std::bind 传统的标准C++风格,第二种使用 lambda 包装成员函数,更现代的C++风格。

// 方法1:使用 std::bind (传统的标准C++风格)
send_goal_options.goal_response_callback =
	std::bind(&FibonacciClient::goal_response_callback, this, std::placeholders::_1);

send_goal_options.feedback_callback = 
	std::bind(&FibonacciClient::feedback_callback, this, std::placeholders::_1, std::placeholders::_2);

// 方法2: 使用 lambda 包装成员函数(更现代的C++风格)
send_goal_options.result_callback = 
    [this](const auto &result)
    {
    	this->result_callback(result);
    };

四、编辑编译配置文件CMakeList.txt

action_learning_cpp/src 目录下的 CMakeList.txt 文件中,新增如下内容:

# 添加action依赖
find_package(rclcpp REQUIRED)
find_package(rclcpp_action REQUIRED)
find_package(std_msgs REQUIRED)

# 生成自定义 Action 消息
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
  "action/Fibonacci.action"
)

# 获取生成的消息类型支持目标
rosidl_get_typesupport_target(cpp_typesupport_target ${PROJECT_NAME} rosidl_typesupport_cpp)

# 辅助宏:创建可执行目标
macro(add_action_executable name)
  add_executable(${name} src/${name}.cpp)
  ament_target_dependencies(${name} rclcpp rclcpp_action rclcpp_lifecycle lifecycle_msgs std_msgs geometry_msgs)
  target_link_libraries(${name} "${cpp_typesupport_target}")
  install(TARGETS ${name} DESTINATION lib/${PROJECT_NAME})
endmacro()

# 添加节点
add_action_executable(action_server_basic)
add_action_executable(action_client_basic)

五、编译运行

进入到工作空间目录,执行如下指令编译该工程:

colcon build 

编译成功后,先设置环境,再依次启动服务端与客户端节点。

source install/setup.bash
ros2 run action_learning_cpp action_server_basic
ros2 run action_learning_cpp action_client_basic

结果如下:

在这里插入图片描述