# ROS2基础教程 - 7 动作通信

# 7.1 动作简介

ROS2 中的动作 action 是一种通信机制,用于处理长时间运行的任务。它结合了服务(Service)和话题(Topic)的特点。

与服务类似,动作也是采用服务端/客户端的 C/S 架构,动作有一个目标请求(Goal)和一个最终的结果(Result)。但与服务不同的是,动作还提供了反馈(Feedback)机制,允许在任务执行过程中持续地发送任务进度信息。

例如,在一个机器人导航任务中,动作的目标可以是导航到某个特定位置。在导航过程中,可以不断地发送机器人的当前位置作为反馈,当到达目标位置后,返回导航成功或失败的结果。


ROS2 动作基于话题(Topic)和服务(Service)机制进行实现。

在底层,动作的目标请求和结果消息的传递利用了服务的通信机制。当动作客户端发送一个目标请求给动作服务器时,这是通过 ROS2 的服务调用方式来实现的,请求会被封装并通过服务的消息通道发送给服务器。同样,当动作完成后,服务器返回结果消息给客户端也是通过服务的响应通道来完成的。这意味着 ROS2 的服务中间件(如 DDS 实现)会处理这些消息的可靠传输、请求 - 响应的匹配等功能,就像普通的服务调用一样。

动作的反馈消息是通过主题的发布和订阅机制来实现的。动作服务器作为反馈主题的发布者,会使用 ROS2 的主题发布功能来发送反馈消息。它会创建一个发布者对象,设置好消息类型和主题名称,然后在动作执行过程中,按照一定的频率或者事件触发来发布反馈消息。动作客户端作为订阅者,会创建一个订阅者对象,指定要订阅的主题名称和消息类型,并注册一个回调函数。当服务器发布反馈消息时,客户端的回调函数会被触发,就像普通主题消息订阅的处理方式一样。


下面就来看一下如何实现动作通信。

实现的功能:发起一个动作,让机器人旋转,只是模拟数据,就是服务器一直给客户端返回当前角度,从0到360度,最终完成动作通信。

# 7.2 定义通信接口

# 1 创建action文件

首先,我们需要定义一个新的 .action 接口文件,描述我们动作的接口。

还是在之前通信接口的功能包中定义 foooor_interface 功能包下定义,在 foooor_interface 功能包的 action 目录下创建。

这里我定义一个叫 Rotate.action 的文件,内容如下:

# 目标 (Goal)
float32 speed  # 旋转速度,单位为度/秒

---
# 结果 (Result)
bool success  # 动作是否成功完成

---
# 反馈 (Feedback)
float32 current_angle  # 当前的旋转角度
1
2
3
4
5
6
7
8
9
10

这个文件定义了三个部分:目标请求(speed),执行结果(success),以及执行过程中的反馈(current_angle)。

# 2 配置action文件

有一些配置在之前创建srv文件的时候已经添加了,添加没添加的。

使用 action ,需要在 CMakeLists.txtpackage.xml 进行一些配置。

修改 package.xml,在 package.xml 中,添加以下依赖项:

<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
1
2
3

这些依赖允许 ROS2 在构建时生成对应的服务代码。


CMakeLists.txt 文件中,添加对服务文件的依赖。

首先,确保你已经添加了 ament_cmakerosidl_default_generators 作为构建依赖项:

find_package(rosidl_default_generators REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  "srv/AddTwoInts.srv"    # 之前在服务通信中添加的
  "action/Rotate.action"   # 这里配置
  
  # 如果有多个srv或msg文件,在这里继续配置
  
  # DEPENDENCIES action_msgs std_msgs   # 如果用到其他依赖,可以在这里配置
)
1
2
3
4
5
6
7
8
9
10

这个时候可以执行 colcon build 来构建项目,会根据 action 文件生成供的Python 和 C++ 调用的数据结构。

# 7.3 Python实现动作通信

# 1 创建功能包

首先在 工作空间/src 创建一个功能包,我这里叫 action_python

ros2 pkg create --build-type ament_python action_python
1

然后下面在 action_python/action_python 创建两个节点,一个是服务端,一个是客户端。


# 2 引入action通信接口

通信接口是定义在 foooor_interface 功能包下的(查看服务通信章节),所以需要在当前功能包中引入 foooor_interface 功能包:

在功能包的 package.xml 中,添加如下配置:

<depend>foooor_interface</depend>
1

导入依赖是为了能够让我们的代码找到对应的通信接口。

# 3 创建服务端

创建服务端节点文件,例如 rotate_action_server.py,内容如下:

import rclpy                              # 导入 ROS2 Python 客户端库
from rclpy.action import ActionServer      # 导入动作服务器类
from rclpy.node import Node                # 导入节点基类
from rclpy.executors import MultiThreadedExecutor  # 多线程支持
from foooor_interface.action import Rotate  # 导入自定义的 Rotate 动作接口
import time                                 # 导入时间库

class RotateActionServer(Node):
    def __init__(self):
        super().__init__('rotate_action_server')  # 初始化节点
        # 初始化动作服务器
        self._action_server = ActionServer(
            self,
            Rotate,
            'rotate_robot',                       # 动作名称为 'rotate'
            self.execute_callback            # 设置执行回调函数
        )

    def execute_callback(self, goal_handle):
        self.get_logger().info(f'Received rotate request with speed: {goal_handle.request.speed} degrees/sec')

        feedback_msg = Rotate.Feedback()  # 创建反馈消息
        current_angle = 0.0               # 初始角度
        rotation_speed = goal_handle.request.speed  # 获取旋转速度
        rate = 0.1  # 每次更新的间隔时间为 0.1 秒
        
        # 开始模拟旋转
        while current_angle < 360.0:
            # 模拟延时并计算当前角度
            time.sleep(rate)
            current_angle += rotation_speed
            if current_angle > 360.0:
                current_angle = 360.0  # 角度不能超过 360 度
            
            # 发送反馈消息
            feedback_msg.current_angle = current_angle
            goal_handle.publish_feedback(feedback_msg)
            self.get_logger().info(f'Current angle: {current_angle}')

        # 完成旋转
        goal_handle.succeed()
        result = Rotate.Result()  # 创建结果消息
        result.success = True     # 设置结果为成功
        return result             # 返回结果

def main(args=None):
    rclpy.init(args=args)  # 初始化 ROS2
    rotate_action_server = RotateActionServer()  # 创建动作服务器实例

    executor = MultiThreadedExecutor()  # 使用多线程执行器
    executor.add_node(rotate_action_server)
    
    # 保持节点运行
    rclpy.spin(rotate_action_server, executor=executor)
    rclpy.shutdown()  # 关闭 ROS2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

服务器代码说明:

  1. 服务器初始化RotateActionServer 继承自 ROS2 的 Node,并在初始化时启动动作服务器。
  2. 执行回调execute_callback 是动作的核心,模拟机器人从 0° 旋转到 360° 的过程,并不断发送当前角度作为反馈。
  3. 旋转过程:使用 time.sleep(rate) 模拟每 0.1 秒更新一次角度,并通过 goal_handle.publish_feedback() 向客户端发送实时角度反馈。

# 4 创建客户端

创建客户端节点文件,例如 rotate_action_client.py,内容如下:

import rclpy                             # 导入 ROS2 Python 客户端库
from rclpy.action import ActionClient     # 导入动作客户端类
from rclpy.node import Node               # 导入节点基类
from foooor_interface.action import Rotate  # 导入自定义 Rotate 动作接口

class RotateActionClient(Node):
    def __init__(self):
        super().__init__('rotate_action_client')  # 初始化客户端节点
        # 初始化动作客户端
        self._action_client = ActionClient(self, Rotate, 'rotate_robot')

    # 发送请求
    def send_goal(self, speed):
        self.get_logger().info(f'Sending rotate goal with speed: {speed} degrees/sec')

        goal_msg = Rotate.Goal()  # 创建目标消息
        goal_msg.speed = speed    # 设置目标速度

        # 发送目标并设置反馈回调和结果回调
        self._action_client.wait_for_server()  # 等待服务器
        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 was 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 feedback_callback(self, feedback_msg):
        self.get_logger().info(f'Current angle: {feedback_msg.feedback.current_angle} degrees')

    # 最终结果回调
    def get_result_callback(self, future):
        result = future.result().result
        if result.success:
            self.get_logger().info('Rotation completed successfully')
        else:
            self.get_logger().info('Rotation failed')

def main(args=None):
    rclpy.init(args=args)                     # 初始化 ROS2
    rotate_action_client = RotateActionClient()  # 创建动作客户端实例
    rotate_action_client.send_goal(10.0)      # 发送目标,旋转速度为 10 度/秒

    rclpy.spin(rotate_action_client)          # 保持节点运行
    rclpy.shutdown()                          # 关闭 ROS2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

客户端代码说明:

  1. 动作接口:客户端使用与服务器相同的 Rotate.action 文件。
  2. 目标设置:客户端通过 send_goal 方法发送一个旋转请求(速度为 10 度/秒)。
  3. 反馈处理feedback_callback 方法会接收服务器发送的当前角度并打印到控制台。
  4. 结果处理get_result_callback 用于接收服务器的最终结果,判断旋转是否完成。

# 5 配置节点

在功能包下的 setup.py 中配置两个节点,在 entry_points 中配置:

entry_points={
    'console_scripts': [
        'rotate_action_server = action_python.rotate_action_server:main',
        'rotate_action_client= action_python.rotate_action_client:main',
    ],
},
1
2
3
4
5
6

# 6 构建项目

现在代码已经编写完成了,需要构建项目。

工作空间下执行如下命令:

colcon build
1

构建完成,会在 install 目录下生成文件。

# 7 运行节点

首先执行 source 命令,在工作空间下执行:

source install/local_setup.sh
1

上面的命令是让 ROS 找到我们的功能包,已经在 HelloWorld 章节说过了。


然后打开一个终端,启动服务端节点:

ros2 run action_python rotate_action_server
1

此时服务端已经在等待调用了。


打开另一个终端,运行客户端节点:

ros2 run action_python rotate_action_client
1

可以看到,客户端调用成功,并不停的得到反馈,如下:

服务端此时也可以看到收到请求和执行的日志:

# 7.4 C++实现服务通信

# 1 创建功能包

首先在 工作空间/src 创建一个功能包,我这里叫 action_cpp

ros2 pkg create --build-type ament_cmake action_cpp
1

然后下面在 action_cpp/src 下创建两个节点,一个是服务端,一个是客户端。


# 2 引入action通信接口

通信接口是定义在 foooor_interface 功能包下的(查看服务通信章节),所以需要在当前功能包中引入 foooor_interface 功能包:

在功能包的 package.xml 中,添加如下配置:

<depend>rclcpp_action</depend>
<depend>foooor_interface</depend>
1
2

导入依赖是为了能够让我们的代码找到对应的通信接口。

# 3 创建服务端

创建节点文件 rotate_action_server.cpp,内容如下:

#include "rclcpp/rclcpp.hpp"  // 导入 ROS 2 C++ 客户端库
#include "rclcpp_action/rclcpp_action.hpp"  // 导入动作相关库
#include "foooor_interface/action/rotate.hpp"  // 导入自定义动作接口
#include <chrono>
#include <thread>  // 导入线程库以支持多线程

using namespace std::chrono_literals;  // 使用时间字面量
using Rotate = foooor_interface::action::Rotate;  // 简化类型名

class RotateActionServer : public rclcpp::Node {
public:
    using GoalHandleRotate = rclcpp_action::ServerGoalHandle<Rotate>;

    // 构造函数,初始化节点并创建动作服务器
    RotateActionServer()
    : Node("rotate_action_server") {
        this->action_server_ = rclcpp_action::create_server<Rotate>(
            this,
            "rotate_robot",  // 动作名称
            std::bind(&RotateActionServer::handle_goal, this, std::placeholders::_1, std::placeholders::_2),
            std::bind(&RotateActionServer::handle_cancel, this, std::placeholders::_1),
            std::bind(&RotateActionServer::handle_accepted, this, std::placeholders::_1)
        );
    }

private:
    rclcpp_action::Server<Rotate>::SharedPtr action_server_;  // 动作服务器指针

    // 处理收到的目标请求,判断是否接受
    rclcpp_action::GoalResponse handle_goal(
        const rclcpp_action::GoalUUID & uuid,
        std::shared_ptr<const Rotate::Goal> goal) {
        RCLCPP_INFO(this->get_logger(), "Received rotate request with speed: %.2f degrees/sec", goal->speed);
        (void)uuid;  // 避免未使用警告
        return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
    }

    // 处理取消请求
    rclcpp_action::CancelResponse handle_cancel(
        const std::shared_ptr<GoalHandleRotate> goal_handle) {
        RCLCPP_INFO(this->get_logger(), "Cancel request received");
        (void)goal_handle;  // 避免未使用警告
        return rclcpp_action::CancelResponse::ACCEPT;
    }

    // 处理被接受的目标请求,启动新线程执行回调
    void handle_accepted(const std::shared_ptr<GoalHandleRotate> goal_handle) {
        std::thread{std::bind(&RotateActionServer::execute, this, goal_handle)}.detach();
    }

    // 执行回调函数,模拟旋转并提供反馈
    void execute(const std::shared_ptr<GoalHandleRotate> goal_handle) {
        RCLCPP_INFO(this->get_logger(), "Executing rotate action");

        const auto goal = goal_handle->get_goal();  // 获取目标请求
        auto feedback = std::make_shared<Rotate::Feedback>();  // 创建反馈消息
        auto result = std::make_shared<Rotate::Result>();  // 创建结果消息

        double current_angle = 0.0;  // 初始化当前角度
        double rotation_speed = goal->speed;  // 获取旋转速度
        double rate = 0.1;  // 每次更新的时间间隔

        // 模拟旋转,循环发送反馈直到完成
        while (current_angle < 360.0) {
            if (goal_handle->is_canceling()) {
                RCLCPP_INFO(this->get_logger(), "Goal canceled");
                result->success = false;
                goal_handle->canceled(result);
                return;
            }

            // 模拟旋转延时
            std::this_thread::sleep_for(std::chrono::duration<double>(rate));
            current_angle += rotation_speed * rate;  // 更新当前角度
            if (current_angle > 360.0) {
                current_angle = 360.0;  // 角度不能超过 360 度
            }

            // 发布反馈
            feedback->current_angle = current_angle;
            goal_handle->publish_feedback(feedback);
            RCLCPP_INFO(this->get_logger(), "Current angle: %.2f", current_angle);
        }

        // 操作完成,返回成功结果
        goal_handle->succeed(result);
        result->success = true;
        RCLCPP_INFO(this->get_logger(), "Rotate action completed successfully");
    }
};

int main(int argc, char **argv) {
    rclcpp::init(argc, argv);  // 初始化 ROS 2
    auto node = std::make_shared<RotateActionServer>();  // 创建节点实例

    rclcpp::executors::MultiThreadedExecutor executor;  // 创建多线程执行器
    executor.add_node(node);  // 将节点添加到执行器
    executor.spin();  // 运行执行器

    rclcpp::shutdown();  // 关闭 ROS 2
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102

在上面的代码中,首先创建服务,然后指定了回调方法,在 handle_goal 回调方法中,指定是否要接受和处理请求,如果接受请求,在handle_accepted 方法中开启了一个线程处理请求,并不停的反馈。

# 4 创建客户端

创建节点文件 rotate_action_client.cpp,内容如下:

#include <memory>
#include <rclcpp/rclcpp.hpp>  // 导入 ROS2 C++ 客户端库
#include <rclcpp_action/rclcpp_action.hpp>  // 导入动作客户端库
#include "foooor_interface/action/rotate.hpp"  // 导入自定义的 Rotate 动作接口

class RotateActionClient : public rclcpp::Node {
public:
    using Rotate = foooor_interface::action::Rotate;  // 使用别名简化动作类型引用
    using GoalHandleRotate = rclcpp_action::ClientGoalHandle<Rotate>;  // 定义动作目标句柄类型

    RotateActionClient() : Node("rotate_action_client") {
        // 初始化动作客户端,与名为 "rotate_robot" 的动作服务器通信
        action_client_ = rclcpp_action::create_client<Rotate>(this, "rotate_robot");
    }

    void send_goal(float speed) {
        // 检查动作服务器是否可用,最多等待 10 秒
        if (!action_client_->wait_for_action_server(std::chrono::seconds(10))) {
            RCLCPP_ERROR(this->get_logger(), "Action server not available after waiting");
            return;
        }

        // 创建并配置旋转目标消息
        auto goal_msg = Rotate::Goal();
        goal_msg.speed = speed;

        RCLCPP_INFO(this->get_logger(), "Sending rotate goal with speed: %.2f degrees/sec", speed);

        // 设置发送目标选项,包括反馈和结果回调
        auto send_goal_options = rclcpp_action::Client<Rotate>::SendGoalOptions();
        send_goal_options.feedback_callback = std::bind(&RotateActionClient::feedback_callback, this, std::placeholders::_1, std::placeholders::_2);
        send_goal_options.result_callback = std::bind(&RotateActionClient::result_callback, this, std::placeholders::_1);

        // 异步发送目标请求
        action_client_->async_send_goal(goal_msg, send_goal_options);
    }

private:
    // 处理来自服务器的反馈的回调函数
    void feedback_callback(GoalHandleRotate::SharedPtr, const std::shared_ptr<const Rotate::Feedback> feedback) {
        RCLCPP_INFO(this->get_logger(), "Current angle: %.2f degrees", feedback->current_angle);
    }

    // 处理服务器返回的最终结果的回调函数
    void result_callback(const GoalHandleRotate::WrappedResult &result) {
        switch (result.code) {
            case rclcpp_action::ResultCode::SUCCEEDED:
                RCLCPP_INFO(this->get_logger(), "Rotation 完成");
                break;
            case rclcpp_action::ResultCode::ABORTED:
                RCLCPP_ERROR(this->get_logger(), "Rotation 被终端");
                break;
            case rclcpp_action::ResultCode::CANCELED:
                RCLCPP_WARN(this->get_logger(), "Rotation 被取消");
                break;
            default:
                RCLCPP_ERROR(this->get_logger(), "Unknown result code");
                break;
        }
    }

    // 动作客户端,用于发送请求和接收反馈
    rclcpp_action::Client<Rotate>::SharedPtr action_client_;
};

int main(int argc, char **argv) {
    rclcpp::init(argc, argv);  // 初始化 ROS2 客户端库
    auto node = std::make_shared<RotateActionClient>();  // 创建 RotateActionClient 节点实例

    // 发送旋转目标,设定速度为 10 度/秒
    node->send_goal(10.0);

    // 保持节点运行,等待反馈和结果
    rclcpp::spin(node);

    rclcpp::shutdown();  // 关闭 ROS2 客户端库
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

send_goal 方法中发起请求,通过 feedback_callback 回调方法不停的接收反馈,通过 result_callback 方法接收处理完成的结果。

# 5 配置CMakeLists.txt

在功能包下的 CMakeLists.txt 中,添加节点配置,可以添加在 find_package(ament_cmake REQUIRED) 下面。

# 找到此包需要的其他包
find_package(rclcpp REQUIRED)
find_package(rclcpp_action REQUIRED)
find_package(foooor_interface REQUIRED)

# 添加可执行文件,指向 rotate_action_server.cpp
add_executable(rotate_action_server src/rotate_action_server.cpp)
ament_target_dependencies(rotate_action_server rclcpp rclcpp_action foooor_interface)  # 注意需要rclcpp、foooor_interface

# 添加可执行文件,指向 rotate_action_client.cpp
add_executable(rotate_action_client src/rotate_action_client.cpp)
ament_target_dependencies(rotate_action_client rclcpp rclcpp_action foooor_interface)  # 注意需要rclcpp、foooor_interface

# 安装可执行文件
install(TARGETS
  rotate_action_server
  rotate_action_client
  DESTINATION lib/${PROJECT_NAME}
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

上面配置了 rotate_action_serverrotate_action_client 两个节点。

# 6 构建项目

现在代码已经编写完成了,需要构建项目。

工作空间下执行如下命令:

colcon build
1

构建完成,会在 install 目录下生成文件。

# 7 运行节点

首先执行 source 命令,在工作空间下执行:

source install/local_setup.sh
1

上面的命令是让 ROS 找到我们的功能包,已经在 HelloWorld 章节说过了。


然后打开一个终端,启动服务端节点:

ros2 run action_cpp rotate_action_server
1

此时服务端已经在等待调用了。


打开另一个终端,运行客户端节点:

ros2 run action_cpp rotate_action_client
1

可以看到,客户端调用成功,和 Python 实现一样。