【ROS2】Beginner: Client libraries - parameters / ros2doctor / pluginlib
一、在C++类中使用参数
目标:使用C++创建并运行包含ROS参数的类。
教程级别:初学者
预计时间:20分钟
目录
- 背景
- 前提条件
- 任务
- 创建功能包
- 编写C++节点
- 构建并运行
- 总结
- 下一步
背景
在创建自定义节点时,有时需要添加可通过启动文件设置的参数。本教程将展示如何在C++类中创建这些参数,以及如何通过启动文件设置它们。
前提条件
- 在之前的教程中,你已学会如何创建工作空间和功能包。
- 你还了解了参数的概念及其在ROS 2系统中的作用。
任务
1. 创建功能包
- 打开新终端,配置ROS 2环境,确保
ros2
命令可正常使用。 - 按照相关说明创建名为
ros2_ws
的新工作空间(若已存在可跳过此步骤)。 - 记得功能包需在
src
目录下创建(而非工作空间根目录),进入ros2_ws/src
并执行以下命令创建新功能包:ros2 pkg create --build-type ament_cmake --license Apache-2.0 cpp_parameters --dependencies rclcpp
终端会返回确认信息,提示cpp_parameters
功能包及其所需的所有文件和文件夹已创建。其中--dependencies
参数会自动在package.xml
和CMakeLists.txt
中添加必要的依赖项。
1.1 更新package.xml
由于创建功能包时使用了--dependencies
选项,无需手动向package.xml
或CMakeLists.txt
添加依赖。但仍需按惯例在package.xml
中补充功能包描述、维护者信息和许可证信息:
<description>C++ parameter tutorial</description>
<maintainer email="you@email.com">Your Name</maintainer>
<license>Apache-2.0</license>
2. 编写C++节点
- 进入
ros2_ws/src/cpp_parameters/src
目录,创建名为cpp_parameters_node.cpp
的新文件。 - 粘贴以下代码:
#include <chrono> #include <functional> #include <string>#include <rclcpp/rclcpp.hpp>using namespace std::chrono_literals;class MinimalParam : public rclcpp::Node { public:MinimalParam(): Node("minimal_param_node"){this->declare_parameter("my_parameter", "world");auto timer_callback = [this](){std::string my_param = this->get_parameter("my_parameter").as_string();RCLCPP_INFO(this->get_logger(), "Hello %s!", my_param.c_str());std::vector<rclcpp::Parameter> all_new_parameters{rclcpp::Parameter("my_parameter", "world")};this->set_parameters(all_new_parameters);};timer_ = this->create_wall_timer(1000ms, timer_callback);}private:rclcpp::TimerBase::SharedPtr timer_; };int main(int argc, char ** argv) {rclcpp::init(argc, argv);rclcpp::spin(std::make_shared<MinimalParam>());rclcpp::shutdown();return 0; }
2.1 代码解析
-
开头的
#include
语句引入了功能包依赖的头文件。 -
接下来的代码创建了类和构造函数:
- 构造函数的第一行创建了一个名为
my_parameter
、默认值为world
的参数。参数类型由默认值推断,此处为字符串类型。 - 随后声明了一个名为
timer_callback
的lambda函数:以引用方式捕获当前对象this
,无输入参数且返回void。 timer_callback
函数的第一行从节点中获取my_parameter
参数的值并存储到my_param
中;接着通过RCLCPP_INFO
函数记录日志事件;再通过set_parameters
函数将my_parameter
参数重置为默认字符串值world
(这样即使用户从外部修改了该参数,也能确保它始终重置为初始值)。- 最后初始化定时器
timer_
,周期为1000毫秒,这会使timer_callback
函数每秒执行一次。
class MinimalParam : public rclcpp::Node { public:MinimalParam(): Node("minimal_param_node"){this->declare_parameter("my_parameter", "world");auto timer_callback = [this](){std::string my_param = this->get_parameter("my_parameter").as_string();RCLCPP_INFO(this->get_logger(), "Hello %s!", my_param.c_str());std::vector<rclcpp::Parameter> all_new_parameters{rclcpp::Parameter("my_parameter", "world")};this->set_parameters(all_new_parameters);};timer_ = this->create_wall_timer(1000ms, timer_callback);}
- 构造函数的第一行创建了一个名为
-
代码最后是
timer_
的声明:private:rclcpp::TimerBase::SharedPtr timer_;
-
在
MinimalParam
类之后是main
函数:- 此处初始化ROS 2,创建
MinimalParam
类的实例,然后通过rclcpp::spin
开始处理来自节点的数据。
int main(int argc, char ** argv) {rclcpp::init(argc, argv);rclcpp::spin(std::make_shared<MinimalParam>());rclcpp::shutdown();return 0; }
- 此处初始化ROS 2,创建
2.1.1 (可选)添加参数描述符(ParameterDescriptor)
你可以选择性地为参数设置描述符。描述符允许你指定参数的文本说明及其约束(例如设为只读、指定数值范围等)。要实现此功能,需将构造函数中的代码修改为:
// ...class MinimalParam : public rclcpp::Node
{
public:MinimalParam(): Node("minimal_param_node"){auto param_desc = rcl_interfaces::msg::ParameterDescriptor{};param_desc.description = "This parameter is mine!";this->declare_parameter("my_parameter", "world", param_desc);auto timer_callback = [this](){std::string my_param = this->get_parameter("my_parameter").as_string();RCLCPP_INFO(this->get_logger(), "Hello %s!", my_param.c_str());std::vector<rclcpp::Parameter> all_new_parameters{rclcpp::Parameter("my_parameter", "world")};this->set_parameters(all_new_parameters);};timer_ = this->create_wall_timer(1000ms, timer_callback);}
其余代码保持不变。运行节点后,可执行ros2 param describe /minimal_param_node my_parameter
命令查看参数的类型和描述。
2.2 添加可执行文件配置
打开CMakeLists.txt
文件,在find_package(rclcpp REQUIRED)
这一依赖项下方添加以下代码:
add_executable(minimal_param_node src/cpp_parameters_node.cpp)
target_link_libraries(minimal_param_node rclcpp::rclcpp)install(TARGETSminimal_param_nodeDESTINATION lib/${PROJECT_NAME}
)
3. 构建并运行
-
检查依赖(仅Linux系统):
在工作空间根目录(ros2_ws
)下运行rosdep
,确保无缺失依赖:# Linux rosdep install -i --from-path src --rosdistro kilted -y
rosdep
仅在Linux上运行,macOS和Windows用户可直接跳过此步骤。 -
构建功能包:
返回工作空间根目录(ros2_ws
),构建新功能包:系统 命令 Linux/macOS colcon build --packages-select cpp_parameters
Windows colcon build --merge-install --packages-select cpp_parameters
-
运行节点:
-
打开新终端,进入
ros2_ws
目录,配置环境:系统 命令 Linux source install/setup.bash
macOS . install/setup.bash
Windows call install/setup.bat
-
运行节点,终端会每秒返回“Hello World”消息:
ros2 run cpp_parameters minimal_param_node [INFO] [minimal_param_node]: Hello world!
目前参数使用默认值,以下提供四种修改参数的方式:
-
3.1 通过控制台修改
这部分将运用你从参数相关教程中学到的知识,对刚创建的节点进行参数修改:
- 确保节点正在运行(若已关闭,需重新执行
ros2 run cpp_parameters minimal_param_node
)。 - 打开另一个终端,再次在
ros2_ws
目录下配置环境,输入以下命令:
在此处你会看到自定义参数ros2 param list
my_parameter
。要修改它,只需在控制台中运行以下命令:
若输出“Set parameter successful”,则表示修改成功。此时查看运行节点的终端,输出应会变为ros2 param set /minimal_param_node my_parameter earth
[INFO] [minimal_param_node]: Hello earth!
。
3.2 通过启动文件修改
你也可以在启动文件中设置参数,但首先需要添加启动目录:
-
在
ros2_ws/src/cpp_parameters/
目录下,创建一个名为launch
的新目录。在该目录中,创建一个名为cpp_parameters_launch.py
的新文件,内容如下:from launch import LaunchDescription from launch_ros.actions import Nodedef generate_launch_description():return LaunchDescription([Node(package='cpp_parameters',executable='minimal_param_node',name='custom_minimal_param_node',output='screen',emulate_tty=True,parameters=[{'my_parameter': 'earth'}])])
在此处可以看到,我们在启动节点
minimal_param_node
时,将my_parameter
设为了earth
。通过添加以下两行代码,可确保输出打印到控制台:output="screen", emulate_tty=True,
-
打开
CMakeLists.txt
文件,在之前添加的代码下方添加以下代码:install(DIRECTORY launchDESTINATION share/${PROJECT_NAME} )
-
打开控制台,进入工作空间根目录(
ros2_ws
),构建新功能包:系统 命令 Linux/macOS colcon build --packages-select cpp_parameters
Windows colcon build --merge-install --packages-select cpp_parameters
-
在新终端中配置环境:
系统 命令 Linux source install/setup.bash
macOS . install/setup.bash
Windows call install/setup.bat
-
使用刚创建的启动文件运行节点,终端首次应返回以下消息:
ros2 launch cpp_parameters cpp_parameters_launch.py [INFO] [custom_minimal_param_node]: Hello earth!
后续输出应会每秒显示
[INFO] [minimal_param_node]: Hello world!
。
3.3 通过启动文件加载YAML文件中的参数
除了在启动文件中列出参数及其值,你还可以创建一个单独的YAML文件,在启动文件中加载该文件。将参数放在YAML文件中更便于组织(例如按命名空间分组),更多相关内容可参考此处。
注意:在C++节点中声明、获取和设置参数值时,应使用点(
.
)作为参数命名空间与名称之间的分隔符。
3.4 通过命令行传递YAML文件作为节点启动参数
可回顾参数相关教程,复习如何通过命令行(CLI)在节点启动时加载参数文件。
总结
你创建了一个包含自定义参数的节点,该参数可通过启动文件或命令行进行设置。你还在功能包配置文件中添加了依赖项、可执行文件和启动文件,以便能够构建并运行它们,进而观察参数的实际作用。
下一步
现在你已经拥有了自己的功能包和ROS 2系统,下一个教程将展示如何在遇到问题时检查环境和系统中的问题。
二、开始使用ros2doctor
目标:使用ros2doctor
工具检查ROS 2系统的问题。
教程级别:初学者
预计时间:10分钟
目录
- 背景
- 前提条件
- 任务
- 安装ros2doctor
- 运行ros2doctor
- 修复问题
- 检查特定问题
- 总结
- 下一步
背景
ros2doctor
是一个命令行工具,用于检查ROS 2系统的设置和潜在问题。它可以诊断网络配置、系统要求、环境变量、正在运行的进程等方面的问题,并提供修复建议。
无论是遇到问题时需要调试,还是想定期检查系统健康状态,ros2doctor
都很有用。
前提条件
- 已安装ROS 2。
- 已配置基本的ROS 2环境(如工作空间)。
- 熟悉终端命令。
任务
1. 安装ros2doctor
在大多数ROS 2发行版中,ros2doctor
已包含在标准安装中。如果你的系统中没有,可以使用以下命令安装:
Debian/Ubuntu
sudo apt-get install ros-kilted-ros2doctor
RHEL/Fedora
sudo dnf install ros-kilted-ros2doctor
Windows
在ROS 2命令提示符中:
choco install ros-kilted-ros2doctor
2. 运行ros2doctor
打开终端,配置ROS 2环境后,运行:
ros2 doctor
ros2doctor
会开始检查系统的各个方面,并输出结果。检查内容包括:
- 操作系统和ROS 2发行版兼容性
- 网络配置(IP地址、主机名)
- 环境变量(
ROS_DOMAIN_ID
、ROS_LOCALHOST_ONLY
等) - 正在运行的ROS 2进程
- 依赖项状态
- 工作空间设置
输出示例:
# 正常状态
All checks passed!# 有警告
Warnings:
- The following environment variables are not set: ROS_DOMAIN_IDRecommendation: Set ROS_DOMAIN_ID to a unique integer between 0 and 101 to avoid conflicts on the network.# 有错误
Errors:
- ROS 2 process 'ros2 daemon' is not running.Recommendation: Start the daemon with 'ros2 daemon start'.
3. 修复问题
ros2doctor
不仅会报告问题,还会提供修复建议。按照建议解决问题后,再次运行ros2doctor
确认问题已修复。
例如,如果收到关于ROS_DOMAIN_ID
未设置的警告,可以通过以下方式设置:
临时设置(当前终端)
export ROS_DOMAIN_ID=0
永久设置(bash)
echo "export ROS_DOMAIN_ID=0" >> ~/.bashrc
source ~/.bashrc
永久设置(zsh)
echo "export ROS_DOMAIN_ID=0" >> ~/.zshrc
source ~/.zshrc
Windows(命令提示符)
setx ROS_DOMAIN_ID 0
设置后再次运行ros2doctor
,警告应该会消失。
4. 检查特定问题
ros2doctor
提供了选项来检查特定方面的问题:
-
检查网络配置:
ros2 doctor --network
-
检查系统信息:
ros2 doctor --system
-
检查ROS 2环境:
ros2 doctor --env
-
检查正在运行的进程:
ros2 doctor --nodes
-
显示帮助信息:
ros2 doctor -h
示例:运行ros2 doctor --network
可能会输出:
Network checks:
- Hostname: your_computer
- IP address: 192.168.1.100
- ROS_LOCALHOST_ONLY: not set (default: 0)
- No network issues detected.
总结
ros2doctor
是一个实用的工具,可帮助你诊断和修复ROS 2系统的常见问题。它检查系统配置、网络设置、环境变量和运行进程等多个方面,并提供具体的修复建议。
定期运行ros2doctor
,特别是在设置新环境或遇到问题时,可以节省调试时间。
下一步
接下来,你可以学习如何使用ROS 2的命令行工具来 introspect(内省)系统,例如查看节点、话题、服务等信息。这将帮助你更好地理解和调试ROS 2系统。
三、使用Pluginlib创建和使用插件
目标:学习如何创建和加载可动态加载的C++插件。
教程级别:初学者
预计时间:20分钟
目录
- 背景
- 前提条件
- 任务
- 创建功能包
- 定义基类接口
- 创建插件
- 导出插件
- 构建和安装
- 编写插件加载器
- 运行和验证
- 总结
- 下一步
背景
ROS 2中的插件系统允许你在运行时加载和使用未在编译时显式链接的代码。这对于创建可扩展系统非常有用,例如:
- 导航器可以加载不同的规划器插件
- 机器人可以切换不同的控制器插件
- 传感器驱动可以支持多种设备
Pluginlib是ROS 2的插件框架,它建立在Boost.DLL之上,提供了跨平台的动态加载能力。
前提条件
- 已创建ROS 2工作空间
- 基本了解C++类和继承
- 已安装
pluginlib
和ament_cmake
任务
1. 创建功能包
在工作空间的src
目录下创建新功能包:
ros2 pkg create --build-type ament_cmake --license Apache-2.0 my_pluginlib_tutorial --dependencies pluginlib rclcpp
2. 定义基类接口
插件系统需要一个所有插件都遵循的基类。创建include/my_pluginlib_tutorial/base.hpp
文件:
#ifndef MY_PLUGINLIB_TUTORIAL__BASE_HPP_
#define MY_PLUGINLIB_TUTORIAL__BASE_HPP_#include <string>namespace my_pluginlib_tutorial
{class Base
{
public:virtual ~Base() = default;virtual std::string get_value() = 0;
};} // namespace my_pluginlib_tutorial#endif // MY_PLUGINLIB_TUTORIAL__BASE_HPP_
3. 创建插件
创建两个插件实现:
插件A (src/plugin_a.cpp
):
#include "my_pluginlib_tutorial/base.hpp"namespace my_pluginlib_tutorial
{class PluginA : public Base
{
public:PluginA() {}~PluginA() {}std::string get_value() override { return "A"; }
};} // namespace my_pluginlib_tutorial#include "pluginlib/class_list_macros.hpp"
PLUGINLIB_EXPORT_CLASS(my_pluginlib_tutorial::PluginA, my_pluginlib_tutorial::Base)
插件B (src/plugin_b.cpp
):
#include "my_pluginlib_tutorial/base.hpp"namespace my_pluginlib_tutorial
{class PluginB : public Base
{
public:PluginB() {}~PluginB() {}std::string get_value() override { return "B"; }
};} // namespace my_pluginlib_tutorial#include "pluginlib/class_list_macros.hpp"
PLUGINLIB_EXPORT_CLASS(my_pluginlib_tutorial::PluginB, my_pluginlib_tutorial::Base)
4. 导出插件
创建插件描述文件my_pluginlib_tutorial/plugins.xml
:
<library path="my_pluginlib_tutorial"><class name="my_pluginlib_tutorial/PluginA" type="my_pluginlib_tutorial::PluginA" base_class_type="my_pluginlib_tutorial::Base"><description>Plugin A that returns "A".</description></class><class name="my_pluginlib_tutorial/PluginB" type="my_pluginlib_tutorial::PluginB" base_class_type="my_pluginlib_tutorial::Base"><description>Plugin B that returns "B".</description></class>
</library>
5. 构建和安装
修改CMakeLists.txt
:
cmake_minimum_required(VERSION 3.8)
project(my_pluginlib_tutorial)if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")add_compile_options(-Wall -Wextra -Wpedantic)
endif()# find dependencies
find_package(ament_cmake REQUIRED)
find_package(pluginlib REQUIRED)
find_package(rclcpp REQUIRED)include_directories(include)add_library(my_pluginlib_tutorialSHAREDsrc/plugin_a.cppsrc/plugin_b.cpp
)
ament_target_dependencies(my_pluginlib_tutorial"pluginlib""rclcpp"
)install(DIRECTORY include/DESTINATION include
)install(TARGETS my_pluginlib_tutorialEXPORT export_${PROJECT_NAME}ARCHIVE DESTINATION libLIBRARY DESTINATION libRUNTIME DESTINATION bin
)pluginlib_export_plugin_description_file(my_pluginlib_tutorial plugins.xml)ament_export_include_directories(include
)
ament_export_libraries(my_pluginlib_tutorial
)
ament_export_targets(export_${PROJECT_NAME}
)ament_package()
修改package.xml
添加导出信息:
<export><my_pluginlib_tutorial plugin="${prefix}/plugins.xml"/>
</export>
6. 编写插件加载器
创建src/plugin_loader.cpp
:
#include <pluginlib/class_loader.hpp>
#include <rclcpp/rclcpp.hpp>
#include "my_pluginlib_tutorial/base.hpp"int main(int argc, char** argv)
{rclcpp::init(argc, argv);// 创建类加载器pluginlib::ClassLoader<my_pluginlib_tutorial::Base> loader("my_pluginlib_tutorial", "my_pluginlib_tutorial::Base");try{// 加载并实例化插件Astd::shared_ptr<my_pluginlib_tutorial::Base> plugin_a =loader.createSharedInstance("my_pluginlib_tutorial/PluginA");std::cout << "Loaded plugin A, value: " << plugin_a->get_value() << std::endl;// 加载并实例化插件Bstd::shared_ptr<my_pluginlib_tutorial::Base> plugin_b =loader.createSharedInstance("my_pluginlib_tutorial/PluginB");std::cout << "Loaded plugin B, value: " << plugin_b->get_value() << std::endl;}catch(pluginlib::PluginlibException& ex){std::cerr << "The plugin failed to load for some reason. Error: " << ex.what() << std::endl;}rclcpp::shutdown();return 0;
}
在CMakeLists.txt
中添加可执行文件:
add_executable(plugin_loader src/plugin_loader.cpp)
ament_target_dependencies(plugin_loader"pluginlib""rclcpp"
)install(TARGETSplugin_loaderDESTINATION lib/${PROJECT_NAME}
)
7. 运行和验证
构建功能包:
colcon build --packages-select my_pluginlib_tutorial
运行插件加载器:
source install/setup.bash
ros2 run my_pluginlib_tutorial plugin_loader
你应该会看到输出:
Loaded plugin A, value: A
Loaded plugin B, value: B
总结
你已学会如何:
- 定义插件基类接口
- 创建插件实现
- 使用PLUGINLIB_EXPORT_CLASS宏导出插件
- 创建插件描述文件
- 配置CMakeLists.txt以构建和安装插件
- 使用pluginlib::ClassLoader在运行时加载插件
下一步
- 尝试创建更复杂的插件,带有参数和ROS 2回调
- 了解如何在不同功能包中分布插件
- 探索ROS 2中使用插件的现有包(如导航2、控制等)