【ROS2】Beginner: Client libraries - parameters / ros2doctor / pluginlib

一、在C++类中使用参数

目标:使用C++创建并运行包含ROS参数的类。

教程级别:初学者
预计时间:20分钟

目录

  • 背景
  • 前提条件
  • 任务
    1. 创建功能包
    2. 编写C++节点
    3. 构建并运行
  • 总结
  • 下一步

背景

在创建自定义节点时,有时需要添加可通过启动文件设置的参数。本教程将展示如何在C++类中创建这些参数,以及如何通过启动文件设置它们。

前提条件

  • 在之前的教程中,你已学会如何创建工作空间和功能包。
  • 你还了解了参数的概念及其在ROS 2系统中的作用。

任务

1. 创建功能包

  1. 打开新终端,配置ROS 2环境,确保ros2命令可正常使用。
  2. 按照相关说明创建名为ros2_ws的新工作空间(若已存在可跳过此步骤)。
  3. 记得功能包需在src目录下创建(而非工作空间根目录),进入ros2_ws/src并执行以下命令创建新功能包:
    ros2 pkg create --build-type ament_cmake --license Apache-2.0 cpp_parameters --dependencies rclcpp
    

终端会返回确认信息,提示cpp_parameters功能包及其所需的所有文件和文件夹已创建。其中--dependencies参数会自动在package.xmlCMakeLists.txt中添加必要的依赖项。

1.1 更新package.xml

由于创建功能包时使用了--dependencies选项,无需手动向package.xmlCMakeLists.txt添加依赖。但仍需按惯例在package.xml中补充功能包描述、维护者信息和许可证信息:

<description>C++ parameter tutorial</description>
<maintainer email="you@email.com">Your Name</maintainer>
<license>Apache-2.0</license>

2. 编写C++节点

  1. 进入ros2_ws/src/cpp_parameters/src目录,创建名为cpp_parameters_node.cpp的新文件。
  2. 粘贴以下代码:
    #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;
    }
    
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(TARGETS
    minimal_param_node
  DESTINATION lib/${PROJECT_NAME}
)

3. 构建并运行

  1. 检查依赖(仅Linux系统):
    在工作空间根目录(ros2_ws)下运行rosdep,确保无缺失依赖:

    # Linux
    rosdep install -i --from-path src --rosdistro kilted -y
    

    rosdep仅在Linux上运行,macOS和Windows用户可直接跳过此步骤。

  2. 构建功能包
    返回工作空间根目录(ros2_ws),构建新功能包:

    系统命令
    Linux/macOScolcon build --packages-select cpp_parameters
    Windowscolcon build --merge-install --packages-select cpp_parameters
  3. 运行节点

    • 打开新终端,进入ros2_ws目录,配置环境:

      系统命令
      Linuxsource install/setup.bash
      macOS. install/setup.bash
      Windowscall install/setup.bat
    • 运行节点,终端会每秒返回“Hello World”消息:

      ros2 run cpp_parameters minimal_param_node
      [INFO] [minimal_param_node]: Hello world!
      

    目前参数使用默认值,以下提供四种修改参数的方式:

3.1 通过控制台修改

这部分将运用你从参数相关教程中学到的知识,对刚创建的节点进行参数修改:

  1. 确保节点正在运行(若已关闭,需重新执行ros2 run cpp_parameters minimal_param_node)。
  2. 打开另一个终端,再次在ros2_ws目录下配置环境,输入以下命令:
    ros2 param list
    
    在此处你会看到自定义参数my_parameter。要修改它,只需在控制台中运行以下命令:
    ros2 param set /minimal_param_node my_parameter earth
    
    若输出“Set parameter successful”,则表示修改成功。此时查看运行节点的终端,输出应会变为[INFO] [minimal_param_node]: Hello earth!
3.2 通过启动文件修改

你也可以在启动文件中设置参数,但首先需要添加启动目录:

  1. ros2_ws/src/cpp_parameters/目录下,创建一个名为launch的新目录。在该目录中,创建一个名为cpp_parameters_launch.py的新文件,内容如下:

    from launch import LaunchDescription
    from launch_ros.actions import Node
    
    
    def 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,
    
  2. 打开CMakeLists.txt文件,在之前添加的代码下方添加以下代码:

    install(
      DIRECTORY launch
      DESTINATION share/${PROJECT_NAME}
    )
    
  3. 打开控制台,进入工作空间根目录(ros2_ws),构建新功能包:

    系统命令
    Linux/macOScolcon build --packages-select cpp_parameters
    Windowscolcon build --merge-install --packages-select cpp_parameters
  4. 在新终端中配置环境:

    系统命令
    Linuxsource install/setup.bash
    macOS. install/setup.bash
    Windowscall install/setup.bat
  5. 使用刚创建的启动文件运行节点,终端首次应返回以下消息:

    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分钟

目录

  • 背景
  • 前提条件
  • 任务
    1. 安装ros2doctor
    2. 运行ros2doctor
    3. 修复问题
    4. 检查特定问题
  • 总结
  • 下一步

背景

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_IDROS_LOCALHOST_ONLY等)
  • 正在运行的ROS 2进程
  • 依赖项状态
  • 工作空间设置

输出示例:

# 正常状态
All checks passed!

# 有警告
Warnings:
- The following environment variables are not set: ROS_DOMAIN_ID
  Recommendation: 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分钟

目录

  • 背景
  • 前提条件
  • 任务
    1. 创建功能包
    2. 定义基类接口
    3. 创建插件
    4. 导出插件
    5. 构建和安装
    6. 编写插件加载器
    7. 运行和验证
  • 总结
  • 下一步

背景

ROS 2中的插件系统允许你在运行时加载和使用未在编译时显式链接的代码。这对于创建可扩展系统非常有用,例如:

  • 导航器可以加载不同的规划器插件
  • 机器人可以切换不同的控制器插件
  • 传感器驱动可以支持多种设备

Pluginlib是ROS 2的插件框架,它建立在Boost.DLL之上,提供了跨平台的动态加载能力。

前提条件

  • 已创建ROS 2工作空间
  • 基本了解C++类和继承
  • 已安装pluginlibament_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_tutorial
  SHARED
  src/plugin_a.cpp
  src/plugin_b.cpp
)
ament_target_dependencies(my_pluginlib_tutorial
  "pluginlib"
  "rclcpp"
)

install(
  DIRECTORY include/
  DESTINATION include
)

install(
  TARGETS my_pluginlib_tutorial
  EXPORT export_${PROJECT_NAME}
  ARCHIVE DESTINATION lib
  LIBRARY DESTINATION lib
  RUNTIME 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
  {
    // 加载并实例化插件A
    std::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;
    
    // 加载并实例化插件B
    std::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(TARGETS
  plugin_loader
  DESTINATION 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、控制等)
CMake Error at /opt/ros/humble/share/rosidl_adapter/cmake/rosidl_adapt_interfaces.cmake:59 (message): execute_process(/usr/bin/python3 -m rosidl_adapter --package-name chapt4_interfaces --arguments-file /home/lh/chapt4/chapt4_ws/build/chapt4_interfaces/rosidl_adapter__arguments__chapt4_interfaces.json --output-dir /home/lh/chapt4/chapt4_ws/build/chapt4_interfaces/rosidl_adapter/chapt4_interfaces --output-file /home/lh/chapt4/chapt4_ws/build/chapt4_interfaces/rosidl_adapter/chapt4_interfaces.idls) returned error code 1: Traceback (most recent call last): File "/usr/lib/python3.10/runpy.py", line 196, in _run_module_as_main return _run_code(code, main_globals, None, File "/usr/lib/python3.10/runpy.py", line 86, in _run_code exec(code, run_globals) File "/opt/ros/humble/local/lib/python3.10/dist-packages/rosidl_adapter/__main__.py", line 19, in <module> sys.exit(main()) File "/opt/ros/humble/local/lib/python3.10/dist-packages/rosidl_adapter/main.py", line 53, in main abs_idl_file = convert_to_idl( File "/opt/ros/humble/local/lib/python3.10/dist-packages/rosidl_adapter/__init__.py", line 24, in convert_to_idl return convert_srv_to_idl( File "/opt/ros/humble/local/lib/python3.10/dist-packages/rosidl_adapter/srv/__init__.py", line 28, in convert_srv_to_idl srv = parse_service_string(package_name, input_file.stem, content) File "/opt/ros/humble/local/lib/python3.10/dist-packages/rosidl_adapter/parser.py", line 850, in parse_service_string raise InvalidServiceSpecification( rosidl_adapter.parser.InvalidServiceSpecification: Could not find separator '---' between request and response Call Stack (most recent call first): /opt/ros/humble/share/rosidl_cmake/cmake/rosidl_generate_interfaces.cmake:130 (rosidl_adapt_interfaces) CMakeLists.txt:14 (rosidl_generate_interfaces)
11-25
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ray.so

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值