# 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 # 当前的旋转角度
2
3
4
5
6
7
8
9
10
这个文件定义了三个部分:目标请求(speed),执行结果(success),以及执行过程中的反馈(current_angle)。
# 2 配置action文件
有一些配置在之前创建srv文件的时候已经添加了,添加没添加的。
使用 action
,需要在 CMakeLists.txt
和 package.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>
2
3
这些依赖允许 ROS2 在构建时生成对应的服务代码。
在 CMakeLists.txt
文件中,添加对服务文件的依赖。
首先,确保你已经添加了 ament_cmake
和 rosidl_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 # 如果用到其他依赖,可以在这里配置
)
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
然后下面在 action_python/action_python
创建两个节点,一个是服务端,一个是客户端。
# 2 引入action通信接口
通信接口是定义在 foooor_interface
功能包下的(查看服务通信章节),所以需要在当前功能包中引入 foooor_interface
功能包:
在功能包的 package.xml
中,添加如下配置:
<depend>foooor_interface</depend>
导入依赖是为了能够让我们的代码找到对应的通信接口。
# 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
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
服务器代码说明:
- 服务器初始化:
RotateActionServer
继承自 ROS2 的Node
,并在初始化时启动动作服务器。 - 执行回调:
execute_callback
是动作的核心,模拟机器人从 0° 旋转到 360° 的过程,并不断发送当前角度作为反馈。 - 旋转过程:使用
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
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
客户端代码说明:
- 动作接口:客户端使用与服务器相同的
Rotate.action
文件。 - 目标设置:客户端通过
send_goal
方法发送一个旋转请求(速度为 10 度/秒)。 - 反馈处理:
feedback_callback
方法会接收服务器发送的当前角度并打印到控制台。 - 结果处理:
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',
],
},
2
3
4
5
6
# 6 构建项目
现在代码已经编写完成了,需要构建项目。
在工作空间下执行如下命令:
colcon build
构建完成,会在 install
目录下生成文件。
# 7 运行节点
首先执行 source
命令,在工作空间下执行:
source install/local_setup.sh
上面的命令是让 ROS 找到我们的功能包,已经在 HelloWorld 章节说过了。
然后打开一个终端,启动服务端节点:
ros2 run action_python rotate_action_server
此时服务端已经在等待调用了。
打开另一个终端,运行客户端节点:
ros2 run action_python rotate_action_client
可以看到,客户端调用成功,并不停的得到反馈,如下:
服务端此时也可以看到收到请求和执行的日志:
# 7.4 C++实现服务通信
# 1 创建功能包
首先在 工作空间/src
创建一个功能包,我这里叫 action_cpp
:
ros2 pkg create --build-type ament_cmake action_cpp
然后下面在 action_cpp/src
下创建两个节点,一个是服务端,一个是客户端。
# 2 引入action通信接口
通信接口是定义在 foooor_interface
功能包下的(查看服务通信章节),所以需要在当前功能包中引入 foooor_interface
功能包:
在功能包的 package.xml
中,添加如下配置:
<depend>rclcpp_action</depend>
<depend>foooor_interface</depend>
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;
}
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;
}
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}
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
上面配置了 rotate_action_server
和 rotate_action_client
两个节点。
# 6 构建项目
现在代码已经编写完成了,需要构建项目。
在工作空间下执行如下命令:
colcon build
构建完成,会在 install
目录下生成文件。
# 7 运行节点
首先执行 source
命令,在工作空间下执行:
source install/local_setup.sh
上面的命令是让 ROS 找到我们的功能包,已经在 HelloWorld 章节说过了。
然后打开一个终端,启动服务端节点:
ros2 run action_cpp rotate_action_server
此时服务端已经在等待调用了。
打开另一个终端,运行客户端节点:
ros2 run action_cpp rotate_action_client
可以看到,客户端调用成功,和 Python 实现一样。