一、使用rosdep管理依赖
目录
- 什么是rosdep?
- 关于package.xml文件的说明
<depend><build_depend><build_export_depend><exec_depend><test_depend>
- rosdep的工作原理
- 如何确定在package.xml中填写哪些键(key)?
- 如果我的库不在rosdistro中该怎么办?
- 如何使用rosdep工具?
- rosdep安装
- rosdep操作
目标:使用rosdep管理外部依赖。
教程级别:中级
预计时间:5分钟
本教程将介绍如何使用rosdep管理外部依赖。
警告:目前rosdep仅支持Linux和macOS系统,不支持Windows。长期计划为rosdep项目仓库添加Windows支持。
什么是rosdep?
rosdep是一款可处理功能包和外部库依赖的依赖管理工具,它是一款命令行工具,用于识别并安装构建或安装某个功能包所需的依赖。rosdep本身并非包管理器,而是一款“元包管理器”——它会利用自身对系统和依赖的认知,在特定平台上找到合适的待安装包,实际安装操作则通过系统包管理器(例如Debian/Ubuntu系统的apt、Fedora/RHEL系统的dnf等)完成。
rosdep最常见的用法是在构建工作空间前调用,用于安装工作空间内所有功能包的依赖。它既可以处理单个功能包,也可以处理包含多个功能包的目录(如工作空间)。
注意:尽管名称中带有“ROS”,但
rosdep对ROS并非强依赖。你可以将其作为独立的Python包安装,在非ROS软件项目中使用这款强大的工具。rosdep的正常运行依赖于“rosdep键(rosdep keys)”的可用性,通过几条简单命令即可从公共Git仓库下载这些键。
关于package.xml文件的说明
package.xml是rosdep查找依赖集的配置文件。确保package.xml中的依赖列表完整且正确至关重要——这能让所有工具准确识别功能包的依赖。依赖缺失或错误可能导致用户无法使用你的功能包、工作空间内功能包构建顺序混乱,甚至功能包无法发布。
package.xml中的依赖通常被称为“rosdep键”,这些依赖由功能包创建者手动填写到package.xml中,应包含功能包所需的所有非内置库和功能包。
这些依赖通过以下标签表示(完整规范请参考REP-149):
<depend>
此类依赖是功能包在构建阶段和运行阶段都需要的依赖。对于C++功能包,若不确定依赖类型,建议使用此标签。纯Python功能包通常没有构建阶段,因此不应使用此标签,而应使用<exec_depend>。
<build_depend>
若某个依赖仅在构建功能包时使用,而在运行阶段不需要,则可使用<build_depend>标签。
对于此类依赖,已安装的功能包二进制文件无需依赖该包即可运行。但需注意:如果你的功能包导出的头文件中包含了该依赖的头文件,那么还需要添加<build_export_depend>标签。
<build_export_depend>
若你的功能包导出的头文件中包含了某依赖的头文件,那么依赖你的功能包(通过<build_depend>声明依赖)的其他功能包也会需要该依赖。此标签主要适用于头文件和CMake配置文件。对于你导出的库所引用的库功能包,通常应使用<depend>标签——因为这些库在运行阶段同样需要。
<exec_depend>
此标签用于声明功能包在运行阶段所需的依赖,包括共享库、可执行文件、Python模块、启动脚本及其他运行时必需的文件。
<test_depend>
此标签用于声明仅测试阶段需要的依赖。此处的依赖不应与<build_depend>、<exec_depend>或<depend>标签中声明的键重复。
rosdep的工作原理
rosdep会在指定路径或特定功能包中查找package.xml文件,提取其中存储的rosdep键。随后,这些键会与一个中央索引进行交叉比对,在各类包管理器中找到对应的ROS功能包或软件库。最终,找到的包会被安装并可立即使用!
rosdep的工作机制是先将中央索引下载到本地机器,避免每次运行时都访问网络(在Debian/Ubuntu系统中,其配置存储在/etc/ros/rosdep/sources.list.d/20-default.list)。
这个中央索引被称为rosdistro,可在线访问。下一节将详细介绍。
如何确定在package.xml中填写哪些键(key)?
这是个很好的问题!
情况1:依赖的是ROS功能包
如果你需要依赖的功能包是基于ROS的,并且已发布到ROS生态系统中[1](例如nav2_bt_navigator),那么直接使用该功能包的名称作为键即可。你可以在rosdistro项目仓库中,找到对应ROS发行版的<distro>/distribution.yaml文件(例如humble/distribution.yaml),其中列出了所有已发布的ROS功能包。
情况2:依赖的是非ROS包(系统依赖)
若需依赖非ROS包(通常称为“系统依赖”),则需要找到对应库的rosdep键。通常可关注以下两个文件:
rosdep/base.yaml:包含apt系统依赖rosdep/python.yaml:包含Python依赖
查找键的方法是:在上述文件中搜索目标库,找到对应的名称,该名称即为需填写到package.xml中的键。
例如,假设某个功能包依赖doxygen(一款优秀的文档生成工具),在rosdep/base.yaml中搜索doxygen会找到以下内容:
doxygen:
arch: [doxygen]
debian: [doxygen]
fedora: [doxygen]
freebsd: [doxygen]
gentoo: [app-doc/doxygen]
macports: [doxygen]
nixos: [doxygen]
openembedded: [doxygen@meta-oe]
opensuse: [doxygen]
rhel: [doxygen]
ubuntu: [doxygen]
这表明对应的rosdep键是doxygen,该键会在不同操作系统的包管理器中解析为对应的包名,从而完成安装。
如果我的库不在rosdistro中该怎么办?
如果你的库不在rosdistro中,不妨体验开源软件开发的优势——你可以自行添加!rosdistro的拉取请求(Pull Request)通常会在一周内合并。
关于如何贡献新的rosdep键,可参考此处的详细说明。若因特殊原因无法公开贡献,也可fork rosdistro仓库,维护一个自定义索引供自己使用。
如何使用rosdep工具?
rosdep安装
方式1:随ROS发行版安装(推荐)
若在ROS环境中使用rosdep,它已随ROS发行版打包,可直接安装:
apt-get install python3-rosdep
注意:在Debian和Ubuntu系统中,存在一个名为
python3-rosdep2的同名包。若已安装该包,需先卸载,再安装python3-rosdep。
方式2:独立安装(非ROS环境)
若在非ROS环境中使用rosdep,系统包管理器可能未提供该包,此时可从PyPI直接安装:
pip install rosdep
rosdep操作
了解了rosdep、package.xml和rosdistro的相关知识后,就可以使用rosdep工具了!
步骤1:初始化与更新(首次使用)
若为首次使用rosdep,需先执行初始化:
sudo rosdep init
rosdep update
rosdep init用于初始化rosdep,rosdep update用于更新本地缓存的rosdistro索引。建议定期执行rosdep update,以获取最新的索引。
步骤2:安装依赖
最终可通过rosdep install命令安装依赖。通常在工作空间根目录执行该命令,一次性安装工作空间内所有功能包的依赖(假设源代码存储在src目录中):
rosdep install --from-paths src -y --ignore-src
对命令参数的说明:
--from-paths src:指定查找package.xml文件的路径(此处为src),用于解析rosdep键。-y:对包管理器的所有提示默认选择“yes”,无需手动确认即可安装。--ignore-src:若某个依赖包本身也在当前工作空间中,即使存在对应的rosdep键,也忽略该依赖的安装。
rosdep还提供了更多参数和选项,可通过rosdep -h查看,或参考rosdep完整文档。
[1] “发布到ROS生态系统”指该功能包在rosdistro数据库的<distro>/distribution.yaml目录中至少有一个条目。
二、创建动作(Action)
目标:在ROS 2功能包中定义一个动作(Action)。
教程级别:中级
预计时间:5分钟
目录
- 背景
- 前提条件
- 任务
- 创建接口功能包
- 定义动作
- 构建动作
- 总结
- 下一步
背景
在之前的“理解动作(Understanding actions)”教程中,你已经了解了动作的相关知识。与其他通信类型及其对应的接口(话题/msg、服务/srv)类似,你也可以在自己的功能包中自定义动作。本教程将展示如何定义并构建一个动作,该动作可用于你将在下一教程中编写的动作服务器(action server)和动作客户端(action client)。
前提条件
- 已安装ROS 2和colcon。
- 已掌握工作空间搭建和功能包创建方法。
- 记得先配置ROS 2环境。
任务
1. 创建接口功能包
根据不同操作系统,执行以下命令(可复用已有的同名工作空间):
Linux
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
ros2 pkg create --license Apache-2.0 custom_action_interfaces
macOS
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
ros2 pkg create --license Apache-2.0 custom_action_interfaces
Windows
md \ros2_ws\src
cd \ros2_ws\src
ros2 pkg create --license Apache-2.0 custom_action_interfaces
2. 定义动作
动作在.action文件中定义,文件结构如下:
# 请求(Request)
---
# 结果(Result)
---
# 反馈(Feedback)
一个动作定义由三个消息定义组成,各消息定义之间用---分隔:
- 请求消息(Request):由动作客户端发送给动作服务器,用于发起一个新目标(goal)。
- 结果消息(Result):由动作服务器发送给动作客户端,在目标完成时返回。
- 反馈消息(Feedback):由动作服务器定期发送给动作客户端,提供目标执行进度的更新。
通常将一个动作实例称为“目标(goal)”。
假设我们要定义一个用于计算斐波那契数列的新动作“Fibonacci”,步骤如下:
-
进入
custom_action_interfaces功能包目录,创建action目录:Linux/macOS
cd custom_action_interfaces mkdir actionWindows
cd custom_action_interfaces md action -
在
action目录下创建Fibonacci.action文件,添加以下内容:int32 order --- int32[] sequence --- int32[] partial_sequence- 目标请求(Request):
order(需计算的斐波那契数列的阶数)。 - 结果(Result):
sequence(最终计算出的斐波那契数列)。 - 反馈(Feedback):
partial_sequence(当前已计算出的部分斐波那契数列)。
- 目标请求(Request):
3. 构建动作
要在代码中使用新的Fibonacci动作类型,需将其定义传入rosidl代码生成流水线,步骤如下:
-
编辑
CMakeLists.txt:在ament_package()行之前添加以下代码:find_package(rosidl_default_generators REQUIRED) rosidl_generate_interfaces(${PROJECT_NAME} "action/Fibonacci.action" ) -
编辑
package.xml:添加所需依赖:<buildtool_depend>rosidl_default_generators</buildtool_depend> <member_of_group>rosidl_interface_packages</member_of_group> -
构建包含Fibonacci动作定义的功能包:
cd ~/ros2_ws # 进入工作空间根目录 colcon build # 执行构建 -
验证动作构建结果:
总结
在本教程中,你学习了动作定义的结构,掌握了如何通过CMakeLists.txt和package.xml正确构建新的动作接口,以及如何验证构建是否成功。
下一步
接下来,让我们利用你新定义的动作接口,创建动作服务器和客户端(可选择Python或C++实现)。
三、编写动作服务器与客户端(C++)
目标:使用C++实现动作服务器(Action Server)和动作客户端(Action Client)。
教程级别:中级
预计时间:15分钟
目录
- 背景
- 前提条件
- 任务
- 创建
custom_action_cpp功能包 - 编写动作服务器
- 编写动作客户端
- 创建
- 总结
背景
动作(Action)是ROS中的一种异步通信方式。动作客户端向动作服务器发送目标请求,动作服务器则向动作客户端发送目标反馈和结果。
前提条件
- 已完成上一教程《创建动作》,并拥有
custom_action_interfaces功能包及其中定义的Fibonacci.action接口。
任务
1. 创建custom_action_cpp功能包
正如在“创建功能包”教程中所学,我们需要创建一个新功能包来存放C++代码及相关支持文件。
1.1 创建custom_action_cpp功能包
进入上一教程创建的动作工作空间(记得配置工作空间环境),为C++动作服务器创建新功能包:
Linux
cd ~/ros2_ws/src
ros2 pkg create --dependencies custom_action_interfaces rclcpp rclcpp_action rclcpp_components --license Apache-2.0 -- custom_action_cpp
macOS
cd ~/ros2_ws/src
ros2 pkg create --dependencies custom_action_interfaces rclcpp rclcpp_action rclcpp_components --license Apache-2.0 -- custom_action_cpp
Windows
cd \ros2_ws\src
ros2 pkg create --dependencies custom_action_interfaces rclcpp rclcpp_action rclcpp_components --license Apache-2.0 -- custom_action_cpp
1.2 添加可见性控制
为确保功能包能在Windows系统上编译并正常工作,需添加“可见性控制”。更多细节可参考《Windows技巧与窍门》文档中的“Windows符号可见性”部分。
打开custom_action_cpp/include/custom_action_cpp/visibility_control.h文件,添加以下代码:
#ifndef CUSTOM_ACTION_CPP__VISIBILITY_CONTROL_H_
#define CUSTOM_ACTION_CPP__VISIBILITY_CONTROL_H_
#ifdef __cplusplus
extern "C"
{
#endif
// 此逻辑借鉴自gcc维基示例(并添加了命名空间):
// https://gcc.gnu.org/wiki/Visibility
#if defined _WIN32 || defined __CYGWIN__
#ifdef __GNUC__
#define CUSTOM_ACTION_CPP_EXPORT __attribute__ ((dllexport))
#define CUSTOM_ACTION_CPP_IMPORT __attribute__ ((dllimport))
#else
#define CUSTOM_ACTION_CPP_EXPORT __declspec(dllexport)
#define CUSTOM_ACTION_CPP_IMPORT __declspec(dllimport)
#endif
#ifdef CUSTOM_ACTION_CPP_BUILDING_DLL
#define CUSTOM_ACTION_CPP_PUBLIC CUSTOM_ACTION_CPP_EXPORT
#else
#define CUSTOM_ACTION_CPP_PUBLIC CUSTOM_ACTION_CPP_IMPORT
#endif
#define CUSTOM_ACTION_CPP_PUBLIC_TYPE CUSTOM_ACTION_CPP_PUBLIC
#define CUSTOM_ACTION_CPP_LOCAL
#else
#define CUSTOM_ACTION_CPP_EXPORT __attribute__ ((visibility("default")))
#define CUSTOM_ACTION_CPP_IMPORT
#if __GNUC__ >= 4
#define CUSTOM_ACTION_CPP_PUBLIC __attribute__ ((visibility("default")))
#define CUSTOM_ACTION_CPP_LOCAL __attribute__ ((visibility("hidden")))
#else
#define CUSTOM_ACTION_CPP_PUBLIC
#define CUSTOM_ACTION_CPP_LOCAL
#endif
#define CUSTOM_ACTION_CPP_PUBLIC_TYPE
#endif
#ifdef __cplusplus
}
#endif
#endif // CUSTOM_ACTION_CPP__VISIBILITY_CONTROL_H_
2. 编写动作服务器
接下来编写一个动作服务器,它将使用上一教程中创建的动作接口来计算斐波那契数列。
2.1 编写动作服务器代码
打开custom_action_cpp/src/fibonacci_action_server.cpp文件,添加以下代码:
#include <functional>
#include <memory>
#include <thread>
#include "custom_action_interfaces/action/fibonacci.hpp"
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
#include "rclcpp_components/register_node_macro.hpp"
#include "custom_action_cpp/visibility_control.h"
namespace custom_action_cpp
{
class FibonacciActionServer : public rclcpp::Node
{
public:
using Fibonacci = custom_action_interfaces::action::Fibonacci;
using GoalHandleFibonacci = rclcpp_action::ServerGoalHandle<Fibonacci>;
CUSTOM_ACTION_CPP_PUBLIC
explicit FibonacciActionServer(const rclcpp::NodeOptions & options = rclcpp::NodeOptions())
: Node("fibonacci_action_server", options)
{
using namespace std::placeholders;
// 处理目标请求的回调函数
auto handle_goal = [this](
const rclcpp_action::GoalUUID & uuid,
std::shared_ptr<const Fibonacci::Goal> goal)
{
RCLCPP_INFO(this->get_logger(), "Received goal request with order %d", goal->order);
(void)uuid; // 未使用的参数,避免编译警告
return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE; // 接受并执行目标
};
// 处理取消请求的回调函数
auto handle_cancel = [this](
const std::shared_ptr<GoalHandleFibonacci> goal_handle)
{
RCLCPP_INFO(this->get_logger(), "Received request to cancel goal");
(void)goal_handle; // 未使用的参数,避免编译警告
return rclcpp_action::CancelResponse::ACCEPT; // 接受取消请求
};
// 处理目标接受的回调函数
auto handle_accepted = [this](
const std::shared_ptr<GoalHandleFibonacci> goal_handle)
{
// 此回调需快速返回,避免阻塞执行器,因此在新线程中调用执行函数
auto execute_in_thread = [this, goal_handle](){return this->execute(goal_handle);};
std::thread{execute_in_thread}.detach();
};
// 创建动作服务器
this->action_server_ = rclcpp_action::create_server<Fibonacci>(
this, // 节点实例
"fibonacci", // 动作名称
handle_goal, // 目标处理回调
handle_cancel, // 取消处理回调
handle_accepted // 目标接受回调
);
}
private:
rclcpp_action::Server<Fibonacci>::SharedPtr action_server_; // 动作服务器指针
// 执行目标的核心函数(在独立线程中运行)
void execute(const std::shared_ptr<GoalHandleFibonacci> goal_handle) {
RCLCPP_INFO(this->get_logger(), "Executing goal");
rclcpp::Rate loop_rate(1); // 1Hz循环频率(每秒处理一次)
const auto goal = goal_handle->get_goal(); // 获取目标请求
auto feedback = std::make_shared<Fibonacci::Feedback>(); // 反馈对象
auto & sequence = feedback->partial_sequence; // 引用反馈中的部分数列
sequence.push_back(0); // 斐波那契数列初始值1
sequence.push_back(1); // 斐波那契数列初始值2
auto result = std::make_shared<Fibonacci::Result>(); // 结果对象
// 循环计算斐波那契数列(直到达到目标阶数或节点关闭)
for (int i = 1; (i < goal->order) && rclcpp::ok(); ++i) {
// 检查是否有取消请求
if (goal_handle->is_canceling()) {
result->sequence = sequence; // 保存当前数列作为取消结果
goal_handle->canceled(result); // 标记目标为已取消
RCLCPP_INFO(this->get_logger(), "Goal canceled");
return;
}
// 更新斐波那契数列(下一个数 = 前两个数之和)
sequence.push_back(sequence[i] + sequence[i - 1]);
// 发布反馈
goal_handle->publish_feedback(feedback);
RCLCPP_INFO(this->get_logger(), "Publish feedback");
loop_rate.sleep(); // 按1Hz频率休眠
}
// 若节点正常运行,标记目标为成功并返回结果
if (rclcpp::ok()) {
result->sequence = sequence; // 保存完整数列作为结果
goal_handle->succeed(result); // 标记目标为成功
RCLCPP_INFO(this->get_logger(), "Goal succeeded");
}
};
}; // class FibonacciActionServer
} // namespace custom_action_cpp
// 注册节点为组件(支持动态加载)
RCLCPP_COMPONENTS_REGISTER_NODE(custom_action_cpp::FibonacciActionServer)
代码解析
-
头文件导入:开头导入编译所需的所有头文件,包括动作接口、ROS 2核心库、动作库及组件注册库。
-
节点类定义:创建
FibonacciActionServer类,继承自rclcpp::Node,包含动作服务器的核心逻辑。 -
构造函数:
- 初始化节点名称为
fibonacci_action_server。 - 定义三个关键回调函数:
handle_goal:接收目标请求,此处实现为“接受所有目标”。handle_cancel:接收取消请求,此处实现为“接受所有取消请求”。handle_accepted:目标被接受后触发,通过新线程调用execute函数(避免阻塞执行器)。
- 调用
rclcpp_action::create_server创建动作服务器,绑定上述回调函数。
- 初始化节点名称为
-
execute函数:- 核心执行逻辑,在独立线程中运行,按1Hz频率计算斐波那契数列。
- 每次循环检查是否有取消请求,若有则返回当前数列并标记目标为“已取消”。
- 若无取消请求,更新数列并发布反馈,直至达到目标阶数。
- 目标完成后,标记目标为“成功”并返回完整数列。
2.2 编译动作服务器
要让动作服务器代码编译运行,需配置CMakeLists.txt:
打开custom_action_cpp/CMakeLists.txt,在find_package调用之后添加以下代码:
# 构建动作服务器库
add_library(action_server SHARED
src/fibonacci_action_server.cpp)
# 设置头文件目录
target_include_directories(action_server PRIVATE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>)
# 定义编译宏(用于Windows可见性控制)
target_compile_definitions(action_server
PRIVATE "CUSTOM_ACTION_CPP_BUILDING_DLL")
# 链接依赖库
target_link_libraries(action_server PUBLIC
${custom_action_interfaces_TARGETS}
rclcpp::rclcpp
rclcpp_action::rclcpp_action
rclcpp_components::component)
# 注册节点组件并生成可执行文件
rclcpp_components_register_node(action_server
PLUGIN "custom_action_cpp::FibonacciActionServer"
EXECUTABLE fibonacci_action_server)
# 安装库文件
install(TARGETS
action_server
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin)
进入ros2_ws工作空间根目录,执行编译:
colcon build
此命令会编译整个工作空间,包括custom_action_cpp功能包中的fibonacci_action_server。
2.3 运行动作服务器
动作服务器编译完成后,配置工作空间环境并运行:
# 配置环境(根据操作系统选择对应命令)
# Linux: source install/local_setup.bash
# macOS: source install/local_setup.console
# Windows: call install\local_setup.bat
# 运行动作服务器
ros2 run custom_action_cpp fibonacci_action_server
3. 编写动作客户端
3.1 编写动作客户端代码
打开custom_action_cpp/src/fibonacci_action_client.cpp文件,添加以下代码:
#include <functional>
#include <future>
#include <memory>
#include <string>
#include <sstream>
#include "custom_action_interfaces/action/fibonacci.hpp"
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
#include "rclcpp_components/register_node_macro.hpp"
namespace custom_action_cpp
{
class FibonacciActionClient : public rclcpp::Node
{
public:
using Fibonacci = custom_action_interfaces::action::Fibonacci;
using GoalHandleFibonacci = rclcpp_action::ClientGoalHandle<Fibonacci>;
// 构造函数(接收节点选项)
explicit FibonacciActionClient(const rclcpp::NodeOptions & options)
: Node("fibonacci_action_client", options)
{
// 创建动作客户端
this->client_ptr_ = rclcpp_action::create_client<Fibonacci>(
this, // 节点实例
"fibonacci" // 动作名称(需与服务器一致)
);
// 创建定时器(500毫秒后触发send_goal函数,且仅触发一次)
auto timer_callback_lambda = [this](){ return this->send_goal(); };
this->timer_ = this->create_wall_timer(
std::chrono::milliseconds(500),
timer_callback_lambda
);
}
// 发送目标请求的函数
void send_goal()
{
using namespace std::placeholders;
this->timer_->cancel(); // 取消定时器(确保仅发送一次目标)
// 等待动作服务器启动(超时则关闭节点)
if (!this->client_ptr_->wait_for_action_server()) {
RCLCPP_ERROR(this->get_logger(), "Action server not available after waiting");
rclcpp::shutdown();
}
// 创建目标请求(计算10阶斐波那契数列)
auto goal_msg = Fibonacci::Goal();
goal_msg.order = 10;
RCLCPP_INFO(this->get_logger(), "Sending goal");
// 配置目标发送选项(绑定回调函数)
auto send_goal_options = rclcpp_action::Client<Fibonacci>::SendGoalOptions();
// 目标响应回调(处理服务器对目标的接受/拒绝)
send_goal_options.goal_response_callback = [this](const GoalHandleFibonacci::SharedPtr & goal_handle)
{
if (!goal_handle) {
RCLCPP_ERROR(this->get_logger(), "Goal was rejected by server");
} else {
RCLCPP_INFO(this->get_logger(), "Goal accepted by server, waiting for result");
}
};
// 反馈回调(处理服务器发送的实时进度)
send_goal_options.feedback_callback = [this](
GoalHandleFibonacci::SharedPtr, // 未使用的目标句柄
const std::shared_ptr<const Fibonacci::Feedback> feedback)
{
std::stringstream ss;
ss << "Next number in sequence received: ";
for (auto number : feedback->partial_sequence) {
ss << number << " ";
}
RCLCPP_INFO(this->get_logger(), ss.str().c_str());
};
// 结果回调(处理服务器返回的最终结果)
send_goal_options.result_callback = [this](const GoalHandleFibonacci::WrappedResult & result)
{
// 根据结果代码处理不同情况
switch (result.code) {
case rclcpp_action::ResultCode::SUCCEEDED:
break; // 成功无需额外处理,直接打印结果
case rclcpp_action::ResultCode::ABORTED:
RCLCPP_ERROR(this->get_logger(), "Goal was aborted");
return;
case rclcpp_action::ResultCode::CANCELED:
RCLCPP_ERROR(this->get_logger(), "Goal was canceled");
return;
default:
RCLCPP_ERROR(this->get_logger(), "Unknown result code");
return;
}
// 打印最终结果(完整斐波那契数列)
std::stringstream ss;
ss << "Result received: ";
for (auto number : result.result->sequence) {
ss << number << " ";
}
RCLCPP_INFO(this->get_logger(), ss.str().c_str());
rclcpp::shutdown(); // 结果处理完成后关闭节点
};
// 异步发送目标请求
this->client_ptr_->async_send_goal(goal_msg, send_goal_options);
}
private:
rclcpp_action::Client<Fibonacci>::SharedPtr client_ptr_; // 动作客户端指针
rclcpp::TimerBase::SharedPtr timer_; // 定时器指针(用于触发目标发送)
}; // class FibonacciActionClient
} // namespace custom_action_cpp
// 注册节点为组件(支持动态加载)
RCLCPP_COMPONENTS_REGISTER_NODE(custom_action_cpp::FibonacciActionClient)
代码解析
-
节点类定义:创建
FibonacciActionClient类,继承自rclcpp::Node,包含动作客户端的核心逻辑。 -
构造函数:
- 初始化节点名称为
fibonacci_action_client。 - 调用
rclcpp_action::create_client创建动作客户端,指定与服务器一致的动作名称fibonacci。 - 创建500毫秒定时器,触发
send_goal函数(确保客户端启动后延迟发送目标,避免服务器未就绪)。
- 初始化节点名称为
-
send_goal函数:- 取消定时器,确保仅发送一次目标。
- 等待动作服务器启动,超时则关闭节点。
- 创建目标请求(
order=10,即计算10阶斐波那契数列)。 - 配置三个关键回调函数并绑定到
SendGoalOptions:goal_response_callback:处理服务器对目标的响应(接受/拒绝)。feedback_callback:实时接收并打印服务器发送的部分数列反馈。result_callback:根据结果代码处理最终结果(成功/中止/取消),打印完整数列后关闭节点。
- 调用
async_send_goal异步发送目标请求。
3.2 编译动作客户端
配置CMakeLists.txt以编译动作客户端代码:
打开custom_action_cpp/CMakeLists.txt,在动作服务器相关配置之后添加以下代码:
# 构建动作客户端库
add_library(action_client SHARED
src/fibonacci_action_client.cpp)
# 设置头文件目录
target_include_directories(action_client PRIVATE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>)
# 定义编译宏(用于Windows可见性控制)
target_compile_definitions(action_client
PRIVATE "CUSTOM_ACTION_CPP_BUILDING_DLL")
# 链接依赖库
target_link_libraries(action_client PUBLIC
${custom_action_interfaces_TARGETS}
rclcpp::rclcpp
rclcpp_action::rclcpp_action
rclcpp_components::component)
# 注册节点组件并生成可执行文件
rclcpp_components_register_node(action_client
PLUGIN "custom_action_cpp::FibonacciActionClient"
EXECUTABLE fibonacci_action_client)
# 安装库文件
install(TARGETS
action_client
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin)
进入ros2_ws工作空间根目录,执行编译:
colcon build
此命令会编译整个工作空间,包括custom_action_cpp功能包中的fibonacci_action_client。
3.3 运行动作客户端
-
首先在单独的终端中启动动作服务器(确保已配置环境):
ros2 run custom_action_cpp fibonacci_action_server -
打开新终端,配置工作空间环境并运行动作客户端:
# 配置环境(根据操作系统选择对应命令) # Linux: source install/local_setup.bash # macOS: source install/local_setup.console # Windows: call install\local_setup.bat # 运行动作客户端 ros2 run custom_action_cpp fibonacci_action_client
你将看到终端中输出以下日志信息:
- 目标被服务器接受的提示。
- 服务器发送的每一步反馈(部分斐波那契数列)。
- 最终计算完成的完整斐波那契数列结果。
总结
在本教程中,你逐步构建了C++动作服务器和动作客户端,并配置它们实现目标、反馈和结果的交互:
- 动作服务器:接收目标请求,按1Hz频率计算斐波那契数列,实时发送反馈,支持目标取消,并在完成后返回结果。
- 动作客户端:延迟发送目标请求,处理服务器的响应、反馈和结果,并在结果接收后关闭节点。
1994

被折叠的 条评论
为什么被折叠?



