ros2 & c++学习笔记+参考ZY_75843《机器人操作系统(ROS2)入门与实践 》_刘相权等+记录

image

 

开发环境搭建

开发环境:ros2 humble    @  ubuntu22.04   

开发工具:VScode及插件

 

image

 

第3章  ROS2编程基础

3.1 节点与软件包

3.1.1 创建workspace 然后package

ubuntu@ubuntu-Lenovo-XiaoXin-14API-2019:~$ mkdir -p ros2_ws_cha3
ubuntu@ubuntu-Lenovo-XiaoXin-14API-2019:~$ mkdir -p ros2_ws_cha3/src
ubuntu@ubuntu-Lenovo-XiaoXin-14API-2019:~$ cd  ~/ros2_ws-cha3/src
ubuntu@ubuntu-Lenovo-XiaoXin-14API-2019:~/ros2_ws_cha3/src$ ros2 pkg create my_pkg
going to create a new package
package name: my_pkg
destination directory: /home/ubuntu/ros2_ws_cha3/src
package format: 3
version: 0.0.0
description: TODO: Package description
maintainer: ['ubuntu <ubuntu@todo.todo>']
licenses: ['TODO: License declaration']
build type: ament_cmake
dependencies: []
creating folder ./my_pkg
creating ./my_pkg/package.xml
creating source and include folder
creating folder ./my_pkg/src
creating folder ./my_pkg/include/my_pkg
creating ./my_pkg/CMakeLists.txt

[WARNING]: Unknown license 'TODO: License declaration'.  This has been set in the package.xml, but no LICENSE file has been created.
It is recommended to use one of the ament license identitifers:
Apache-2.0
BSL-1.0
BSD-2.0
BSD-2-Clause
BSD-3-Clause
GPL-3.0-only
LGPL-3.0-only
MIT
MIT-0

在VScode中打开工作空间 ros2_ws_cha3。在VScode文件树中观察工作空间中的文件结构。

image

 

3.1.2 编写node

3.1.2.1 编写节点代码

image

 按照上述图片展示的操作,添加first_node.cpp节点源文件

// 包含ROS 2 C++客户端库的头文件
#include "rclcpp/rclcpp.hpp"

int main(int argc, char * argv[])
{
  // 初始化ROS 2系统
  // 处理命令行参数并设置必要的通信环境
  rclcpp::init(argc, argv);

  // 创建一个共享指针指向ROS 2节点对象
  // std::make_shared<rclcpp::Node>("my_node") 创建了一个名为"my_node"的节点
  // auto关键字自动推导节点类型为 std::shared_ptr<rclcpp::Node>
  auto node = std::make_shared<rclcpp::Node>("my_node");

  // 使用ROS 2的日志系统输出信息
  // RCLCPP_INFO宏用于输出INFO级别的日志
  // node->get_logger() 获取节点的日志记录器
  // "Hello world!" 是要输出的信息
  RCLCPP_INFO(node->get_logger(), "Hello world!");

  // 空循环,保持程序运行
  // rclcpp::ok() 检查ROS 2系统是否正常运行
  // 当收到SIGINT信号(如Ctrl+C)或调用rclcpp::shutdown()时返回false
  while (rclcpp::ok())
  {
    ;  // 空语句,循环体什么都不做
  }

  // 关闭ROS 2系统
  // 清理资源,终止通信
  rclcpp::shutdown();

  // 程序正常退出
  return 0;
}
first_node.cpp
ubuntu@ubuntu-Lenovo-XiaoXin-14API-2019:~/ros2_ws_cha3$ colcon build
Starting >>> my_pkg
Finished <<< my_pkg [6.39s]
 
Summary: 1 package finished [6.85s]
ubuntu@ubuntu-Lenovo-XiaoXin-14API-2019:~/ros2_ws_cha3$ source install/setup.bash
ubuntu@ubuntu-Lenovo-XiaoXin-14API-2019:~/ros2_ws_cha3$ ros2 run my_pkg first_node
[INFO] [1773564269.835462423] [my_node]: Hello world!

image

 3.1.2.2设置编译规则

修改CMakeLists.txt

cmake_minimum_required(VERSION 3.8)
project(my_pkg)

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)
# uncomment the following section in order to fill in
# further dependencies manually.
# find_package(<dependency> REQUIRED)
find_package(rclcpp REQUIRED)
add_executable(first_node src/first_node.cpp)
ament_target_dependencies(first_node "rclcpp")
install(TARGETS
  first_node
  DESTINATION lib/${PROJECT_NAME})
if(BUILD_TESTING) find_package(ament_lint_auto REQUIRED) # the following line skips the linter which checks for copyrights # comment the line when a copyright and license is added to all source files set(ament_cmake_copyright_FOUND TRUE) # the following line skips cpplint (only works in a git repo) # comment the line when this package is in a git repo and when # a copyright and license is added to all source files set(ament_cmake_cpplint_FOUND TRUE) ament_lint_auto_find_test_dependencies() endif()
ament_package()

 3.1.2.3 修改软件包信息

修改package.xml

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>my_pkg</name>
  <version>0.0.0</version>
  <description>TODO: Package description</description>
  <maintainer email="ubuntu@todo.todo">ubuntu</maintainer>
  <license>TODO: License declaration</license>

  <buildtool_depend>ament_cmake</buildtool_depend>

  <depend>rclcpp</depend> 

  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>

  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

 3.1.2.4 编译软件包

ubuntu@ubuntu-Lenovo-XiaoXin-14API-2019:~/ros2_ws_cha3$ colcon build
Starting >>> my_pkg  
Finished <<< my_pkg [6.39s]                   

Summary: 1 package finished [6.85s]

3.1.3 运行节点

ubuntu@ubuntu-Lenovo-XiaoXin-14API-2019:~/ros2_ws_cha3$ source install/setup.bash
ubuntu@ubuntu-Lenovo-XiaoXin-14API-2019:~/ros2_ws_cha3$ ros2 run my_pkg first_node
[INFO] [1773564269.835462423] [my_node]: Hello world!

 

3.2 函数式的话题和消息

 

3.2.0 话题通讯简介

 

话题通讯是ROS2使用频率最高的一种通信方式,模拟报纸或杂志传播过程,一个话题发布者,多个话题订阅者,有发布者发布指定话题的数据,订阅者只要订阅了该话题的数据,就可以接收到数据。

 

话题通信是基于发布/订阅模型,如图:

 

image8

 

话题数据传输的特性是从一个节点到另外一个节点,发送数据的对象称之为发布者,接收数据的对象称之为订阅者,每一个话题都需要有一个名字,传输的数据也需要有固定的数据类型。

 

3.2.1 编写话题发布者Publisher

编写节点代码

// 包含ROS 2 C++客户端库的头文件
#include "rclcpp/rclcpp.hpp"
// 包含标准字符串消息类型的头文件
// std_msgs/msg/string.hpp 定义了ROS 2中的字符串消息格式
#include "std_msgs/msg/string.hpp"

int main(int argc, char * argv[])
{
  // 初始化ROS 2系统
  // 处理命令行参数并设置必要的通信环境
  rclcpp::init(argc, argv);

  // 创建一个共享指针指向ROS 2节点对象
  // 节点名称为"publisher_node",用于在ROS 2网络中标识这个节点
  auto node = std::make_shared<rclcpp::Node>("publisher_node");

  // 创建发布者对象
  // create_publisher<消息类型>("话题名称", 队列大小)
  // 参数说明:
  //   - std_msgs::msg::String: 要发布的消息类型
  //   - "/my_topic": 话题名称,订阅者通过这个话题接收消息
  //   - 10: 消息队列大小,如果发布速度超过处理速度,最多缓存10条消息
  auto publisher = node->create_publisher<std_msgs::msg::String>("/my_topic", 10);

  // 创建字符串消息对象
  std_msgs::msg::String message;
  // 设置消息内容
  message.data = "Hello World!";

  // 创建速率控制对象
  // loop_rate(1) 表示每秒循环1次(即1Hz的频率)
  rclcpp::Rate loop_rate(1);

  // 主循环:当ROS系统正常运行时持续执行
  while (rclcpp::ok())
  {
    // 发布消息到话题"/my_topic"
    // 所有订阅了该话题的节点都会收到这条消息
    publisher->publish(message);

    // 控制循环速率
    // sleep()方法会使程序暂停足够长的时间,以保持1Hz的发布频率
    // 这样可以避免CPU占用过高,并控制消息发布的节奏
    loop_rate.sleep();
  }

  // 关闭ROS 2系统
  // 当收到中断信号(如Ctrl+C)退出循环后,清理资源
  rclcpp::shutdown();

  // 程序正常退出
  return 0;
}
publisher_node.cpp

设置编译规则,修改CMakeLists.txt

cmake_minimum_required(VERSION 3.8)
project(my_pkg)

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)
# uncomment the following section in order to fill in
# further dependencies manually.
# find_package(<dependency> REQUIRED)
find_package(rclcpp REQUIRED)

add_executable(first_node src/first_node.cpp)
ament_target_dependencies(first_node "rclcpp")

find_package(std_msgs REQUIRED)

add_executable(publisher_node src/publisher_node.cpp)
ament_target_dependencies(publisher_node "rclcpp"  "std_msgs")
install(TARGETS first_node publisher_node DESTINATION lib/${PROJECT_NAME}) if(BUILD_TESTING) find_package(ament_lint_auto REQUIRED) # the following line skips the linter which checks for copyrights # comment the line when a copyright and license is added to all source files set(ament_cmake_copyright_FOUND TRUE) # the following line skips cpplint (only works in a git repo) # comment the line when this package is in a git repo and when # a copyright and license is added to all source files set(ament_cmake_cpplint_FOUND TRUE) ament_lint_auto_find_test_dependencies() endif()
ament_package()

修改软件包信息,修改package.xml

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>my_pkg</name>
  <version>0.0.0</version>
  <description>TODO: Package description</description>
  <maintainer email="ubuntu@todo.todo">ubuntu</maintainer>
  <license>TODO: License declaration</license>

  <buildtool_depend>ament_cmake</buildtool_depend>

  <depend>rclcpp</depend> 
  <depend>std_msgs</depend>

  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>

  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

 3.2.2 编写话题订阅者Subscriber

编写节点代码subscriber_node.cpp

// 包含ROS 2 C++客户端库,提供ROS 2核心功能
#include "rclcpp/rclcpp.hpp"
// 包含标准字符串消息类型,用于接收文本消息
#include "std_msgs/msg/string.hpp"

// 声明全局节点指针,以便在回调函数中使用
// 全局变量使得回调函数能够访问节点对象进行日志输出
std::shared_ptr<rclcpp::Node> node;

// 回调函数:当接收到新消息时自动调用
// 参数:接收到的消息的共享指针,包含消息数据
void Callback(const std_msgs::msg::String::SharedPtr msg)
{
    // 使用节点的日志记录器输出接收到的消息内容
    // RCLCPP_INFO宏用于输出INFO级别的日志信息
    // msg->data.c_str()获取字符串消息的内容并转换为C风格字符串
    RCLCPP_INFO(node->get_logger(),"Receive : %s", msg->data.c_str());
}

int main(int argc, char * argv[])
{
  // 初始化ROS 2系统,建立与ROS 2基础设施的连接
  rclcpp::init(argc, argv);

  // 创建ROS 2节点对象,节点名为"subscriber_node"
  // 使用全局变量node存储节点指针,以便在回调函数中访问
  node = std::make_shared<rclcpp::Node>("subscriber_node");

  // 创建订阅者对象
  // create_subscription模板参数指定消息类型为std_msgs::msg::String
  // 参数1:话题名称"/my_topic",订阅这个话题上的消息
  // 参数2:队列大小10,当接收速度超过处理速度时最多缓存10条消息
  // 参数3:回调函数指针&Callback,当收到新消息时自动调用此函数
  auto subscriber = node->create_subscription<std_msgs::msg::String>(
      "/my_topic",     // 话题名称
      10,              // 队列大小
      &Callback        // 回调函数
  );

  // spin()函数是ROS 2的核心循环
  // 它会阻塞程序运行,持续处理回调函数
  // 当收到新消息时,自动调用Callback函数
  // 当收到中断信号(如Ctrl+C)或调用rclcpp::shutdown()时退出
  rclcpp::spin(node);

  // 关闭ROS 2系统,清理资源
  // spin()退出后执行,确保所有通信资源被正确释放
  rclcpp::shutdown();

  // 程序正常退出
  return 0;
}
View Code

设置编译规则,修改CMakeLists.txt

cmake_minimum_required(VERSION 3.8)
project(my_pkg)

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)
# uncomment the following section in order to fill in
# further dependencies manually.
# find_package(<dependency> REQUIRED)
find_package(rclcpp REQUIRED)

add_executable(first_node src/first_node.cpp)
ament_target_dependencies(first_node "rclcpp")

find_package(std_msgs REQUIRED)

add_executable(publisher_node src/publisher_node.cpp)
ament_target_dependencies(publisher_node "rclcpp"  "std_msgs")

add_executable(subscriber_node src/subscriber_node.cpp)
ament_target_dependencies(subscriber_node "rclcpp"  "std_msgs")

install(TARGETS
  first_node
  publisher_node
  subscriber_node
  DESTINATION lib/${PROJECT_NAME})


if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  # the following line skips the linter which checks for copyrights
  # comment the line when a copyright and license is added to all source files
  set(ament_cmake_copyright_FOUND TRUE)
  # the following line skips cpplint (only works in a git repo)
  # comment the line when this package is in a git repo and when
  # a copyright and license is added to all source files
  set(ament_cmake_cpplint_FOUND TRUE)
  ament_lint_auto_find_test_dependencies()
endif()


ament_package()

 

修改软件包信息,修改package.xml

 同3.2.1的package.xml

3.3 类式的话题和消息

3.3.1 发布话题

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

/**
 * 发布者节点类
 * 这个类继承自 rclcpp::Node,代表一个ROS 2节点
 * 节点本身是一个逻辑实体,不直接对应线程,需要被添加到执行器中才能运行
 */
class PublisherNode : public rclcpp::Node
{
public:
  PublisherNode()
    // 调用父类构造函数,指定节点名称
    // 这个节点名称会在ROS 2网络中唯一标识该节点
    : Node("publisher_node")
  {
    // 创建发布者
    // 参数1: 话题名称 "/my_topic" - 其他节点可以通过订阅这个话题接收消息
    // 参数2: 队列大小 10 - 如果发布速度快于网络传输,最多缓存10条消息
    publisher_ = create_publisher<std_msgs::msg::String>("/my_topic", 10);
    
    // 创建定时器
    // 参数1: 时间间隔 1000ms - 每秒钟触发一次回调
    // 参数2: 回调函数 - 定时器触发时要执行的函数
    // 注意:定时器本身只是一个计时器对象,它依赖执行器来调用其回调函数
    timer_ = create_wall_timer(
      std::chrono::milliseconds(1000),  // 1秒定时
      std::bind(&PublisherNode::publishMessage, this)  // 绑定成员函数作为回调
      );
      
    // 思考:这里创建的回调(定时器回调)并不会立即执行
    // 它们被注册到节点中,等待被添加到执行器的线程中执行
  }

private:
  /**
   * 定时器回调函数
   * 这个函数会被执行器在检测到定时器超时时调用
   * 在单线程执行器中,这个函数会串行执行(不会被其他回调打断)
   * 在多线程执行器中,这个函数可能与其他回调并行执行
   */
  void publishMessage()
  {
    // 设置消息内容
    // 注意:message_ 是成员变量,在多线程环境下需要保护
    message_.data = "Hello World! from a class publisher node";
    
    // 发布消息
    // 发布操作是线程安全的,ROS 2内部已经处理好同步
    publisher_->publish(message_);
    
    // 可选的日志输出(取消注释即可使用)
    // RCLCPP_INFO(this->get_logger(), "Published: '%s'", message_.data.c_str());
  }

  // 发布者智能指针
  // SharedPtr 表示这是一个可以被多个所有者共享的指针
  rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
  
  // 消息对象(成员变量)
  // 这里复用一个消息对象以减少内存分配
  // 注意:如果使用多线程执行器,这里可能存在线程安全问题
  std_msgs::msg::String message_;
  
  // 定时器智能指针
  // 只要定时器存在,它就会持续触发回调
  rclcpp::TimerBase::SharedPtr timer_;
};

int main(int argc, char * argv[])
{
  /**
   * 初始化ROS 2
   * 这一步会设置全局状态,包括信号处理、全局参数等
   * 必须在任何ROS 2函数调用之前执行
   */
  rclcpp::init(argc, argv);
  
  /**
   * 创建节点实例
   * make_shared 在堆上创建节点对象,返回智能指针
   * 节点对象包含了所有的发布者、订阅者、定时器等
   * 但节点本身只是一个"静态"的实体,需要执行器来"驱动"
   */
  auto node = std::make_shared<PublisherNode>();
  
  /**
   * 关键点:spin 函数
   * 
   * 这个调用会:
   * 1. 在内部创建一个单线程执行器 (SingleThreadedExecutor)
   * 2. 将我们的节点添加到执行器中
   * 3. 让执行器在当前线程(主线程)中开始事件循环
   * 
   * 执行器的工作流程:
   *   while(ros ok) {
   *     等待任何就绪的回调(定时器、订阅消息、服务请求等)
   *     if (有就绪回调) {
   *       执行这个回调(如 publishMessage)
   *     }
   *   }
   * 
   * 因此,spin(node) 是一个阻塞调用,它会一直运行直到 rclcpp::shutdown() 被调用
   */
  rclcpp::spin(node);
  
  /**
   * 清理ROS 2资源
   * 当 spin 返回后(例如按了 Ctrl+C),执行清理工作
   * 会释放全局资源,销毁所有节点
   */
  rclcpp::shutdown();
  
  return 0;
}

设置编译规则,修改CMakeLists.txt,注意和3.2.1其他相同,修改为:

add_executable(publisher_clsnode src/publisher_clsnode.cpp)
ament_target_dependencies(publisher_clsnode "rclcpp" "std_msgs")
install(TARGETS
first_node
publisher_node
subscriber_node
publisher_clsnode
DESTINATION lib/${PROJECT_NAME})

3.3.2 订阅话题

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

/**
 * 订阅者节点类
 * 演示如何接收并处理来自话题的消息
 * 与发布者节点配合,构成完整的发布-订阅通信模型
 */
class SubscriberNode : public rclcpp::Node
{
public:
  SubscriberNode()
    : Node("subscriber_node")  // 节点名称,会在ROS网络中唯一标识
  {
    /**
     * 创建订阅者
     * 
     * 参数说明:
     * 1. "/my_topic" : 要订阅的话题名称
     * 2. 10 : 队列大小 - 当消息到达速度超过处理速度时,最多缓存10条消息
     * 3. 回调函数 : 收到消息时被执行的函数
     * 
     * 关键概念:
     * - 订阅者只是一个"监听器",不主动获取数据
     * - 当消息到达时,ROS 2的执行器(Executor)会调用注册的回调函数
     * - 回调函数在哪个线程执行,取决于执行器的配置(单线程/多线程)
     */
    subscriber_ = create_subscription<std_msgs::msg::String>(
      "/my_topic",
      10,
      std::bind(&SubscriberNode::Callback, this, std::placeholders::_1)
    );
  }

private:
  /**
   * 消息回调函数
   * 
   * 执行时机:每当有消息发布到"/my_topic"时,此函数会被执行器调用
   * 参数类型:std_msgs::msg::String::SharedPtr - 共享指针,避免复制消息数据
   * 
   * 线程安全注意:
   * - 在单线程执行器中,此回调会串行执行,不会同时有多个实例运行
   * - 在多线程执行器中,可能多个消息同时被处理,需要保护共享数据
   * 
   * 性能考虑:
   * - 回调函数应尽快返回,避免阻塞执行器
   * - 耗时操作应放到单独的线程或使用async处理
   */
  void Callback(const std_msgs::msg::String::SharedPtr msg)
  {
    // RCLCPP_INFO 是ROS 2的日志宏,会输出到控制台和日志文件
    // get_logger() 返回节点特有的日志器,日志会带上节点名称前缀
    RCLCPP_INFO(get_logger(), "Receive : %s", msg->data.c_str());
    
    // 可以在这里添加更多处理逻辑
    // 注意:如果处理耗时较长,应考虑使用回调组(Callback Groups)
    // 避免影响其他回调的执行
  }

  // 订阅者智能指针
  // 只要订阅者对象存在,回调函数就会一直有效
  rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscriber_;
};

int main(int argc, char * argv[])
{
  /**
   * ROS 2初始化
   * 必须在使用任何ROS 2功能之前调用
   * 初始化全局状态、命令行参数、信号处理等
   */
  rclcpp::init(argc, argv);
  
  /**
   * 创建订阅者节点实例
   * 此时节点对象已在内存中,但尚未"激活"
   * 订阅者已创建,但回调函数还不会被调用
   */
  auto node = std::make_shared<SubscriberNode>();
  
  /**
   * 关键:spin 函数
   * 
   * 这个调用会:
   * 1. 内部创建单线程执行器(SingleThreadedExecutor)
   * 2. 将节点添加到执行器中
   * 3. 当前线程进入事件循环,持续等待并处理事件
   * 
   * 事件循环的工作流程:
   * while (rclcpp::ok()) {           // 直到收到Ctrl+C或调用shutdown
   *   wait_for_work();               // 等待消息到达或定时器触发
   *   execute_any_ready_callbacks(); // 执行所有就绪的回调函数
   * }
   * 
   * 对于订阅者节点,这个循环会:
   * - 检测是否有新消息到达"/my_topic"
   * - 如果有,调用 Callback 函数处理消息
   * - 如果没有,继续等待
   */
  rclcpp::spin(node);
  
  /**
   * 清理ROS 2资源
   * spin返回后(通常是因为按了Ctrl+C),执行清理工作
   * 释放全局资源,销毁所有节点
   */
  rclcpp::shutdown();
  
  return 0;
}

/**
 * 运行时结构分析
 * 
 * 这个订阅者节点与发布者节点配合,构成完整的通信链路:
 * 
 * ┌─────────────────────────────────────────────────────────┐
 * │                    进程1 (发布者)                        │
 * │  ┌───────────────────────────────────────────────────┐  │
 * │  │ 线程1 (主线程)                                      │  │
 * │  │ └── 执行器                                         │  │
 * │  │     └── 节点 (publisher_node)                     │  │
 * │  │         └── 定时器回调 → 发布消息                  │  │
 * │  └───────────────────────────────────────────────────┘  │
 * └─────────────────────────────────────────────────────────┘
 *                            │
 *                            │ DDS中间件 (进程间通信)
 *                            ↓
 * ┌─────────────────────────────────────────────────────────┐
 * │                    进程2 (订阅者)                        │
 * │  ┌───────────────────────────────────────────────────┐  │
 * │  │ 线程1 (主线程)                                      │  │
 * │  │ └── 执行器                                         │  │
 * │  │     └── 节点 (subscriber_node)                    │  │
 * │  │         └── 收到消息 → 执行Callback()              │  │
 * │  └───────────────────────────────────────────────────┘  │
 * └─────────────────────────────────────────────────────────┘
 */

/**
 * 与发布者节点的对比总结
 * 
 * | 特性           | 发布者节点                     | 订阅者节点                     |
 * |---------------|-------------------------------|-------------------------------|
 * | 主要职责       | 主动产生数据                   | 被动接收数据                   |
 * | 核心组件       | Publisher + Timer             | Subscription                   |
 * | 回调触发       | 定时器到期                     | 消息到达                       |
 * | 数据流向       | 向外发送                       | 向内接收                       |
 * | 执行器角色     | 调用定时器回调                 | 调用消息回调                   |
 * 
 * 两者都遵循相同的ROS 2执行模型:
 * - 节点是功能容器
 * - 执行器是调度中心
 * - 线程是实际执行者
 * - 回调是具体工作单元
 */

/**
 * 扩展练习建议
 * 
 * 1. 运行这个订阅者节点,同时运行之前的发布者节点
 *    观察终端输出的消息
 * 
 * 2. 修改代码,添加多个订阅者订阅不同话题
 * 
 * 3. 尝试使用 MultiThreadedExecutor,观察多线程处理效果
 * 
 * 4. 在回调函数中添加sleep,观察单线程和多线程执行器的行为差异
 */

设置编译规则,修改CMakeLists.txt,注意和3.2.2其他相同,修改为:

add_executable(subscriber_clsnode src/subscriber_clsnode.cpp)
ament_target_dependencies(subscriber_clsnode "rclcpp" "std_msgs")

install(TARGETS
first_node
publisher_node
subscriber_node
publisher_clsnode
subscriber_clsnode
DESTINATION lib/${PROJECT_NAME})

3.4 自定义消息接口

3.4.1 通讯接口简介

在ROS系统中,无论话题、服务还是动作,都会用到一个重要的概念——通信接口。

通信并不是一个人自言自语,而是两个甚至更多个人,你来我往的交流,交流的内容是什么呢?为了让大家都好理解,我们可以给传递的数据定义一个标准的结构,这就是通信接口。

接口可以让程序之间的依赖降低,便于我们使用别人的代码,也方便别人使用我们的代码,这就是ROS的核心目标,减少重复造轮子。

image-20220528001533911

ROS有三种常用的通信机制,分别是话题、服务、动作,通过每一种通信种定义的接口,各种节点才能有机的联系到一起。

 

创建自定义接口流程

自定义接口消息的流程与在功能包中编写可执行程序的流程类似,主要步骤如下:

  1. 创建接口功能包;
  2. 创建并编辑 .msg文件 、.srv文件、.action文件
  3. 编辑配置文件;
  4. 编译;
  5. 测试。

文件树(一定要按照文件树添加代码文件) 

 

ros2 pkg create --build-type ament_cmake pkg_interfaces

 

image

 

3.4.2 用于 话题  的消息 topic         publish-subscribe

0、在工作空间的src目录下新建pkg_interfaces功能包

ros2 pkg create --build-type ament_cmake pkg_interfaces

1、在【ROS2动作通讯服务端实现】课程中我们已经创建了自定义接口功能包,功能包pkg_interfaces下新建 msg 文件夹,msg文件夹下新建Jointmotion.msg文件,文件中输入如下内容:

# 按照等腰梯形的速度规律控制电机位移
int32    id     # 关节编号,也是通信地址
string   cmd    # 命令
float64  d      # displacement    # 位移 (单位: 米)
float64  v      # velocity        # 速度 (单位: 米/秒)
float64  a      # acceleration    # 加速度 (单位: 米/秒²)

 

在ROS 2中自定义消息接口时,支持的基础数据类型主要分为三大类:标准基础类型、数组类型,以及其他消息类型(嵌套消息)。你可以将它们组合使用,来构建复杂的数据结构。

下面这个表格清晰地列出了所有支持的基础类型和它们在不同语言中的对应关系:

 
类型分类ROS 2 接口类型C++ 对应类型说明
整型 int8, int16, int32, int64 int8_t, int16_t, int32_t, int64_t 有符号整数
  uint8, uint16, uint32, uint64 uint8_t, uint16_t, uint32_t, uint64_t 无符号整数
浮点型 float32, float64 float, double 单精度和双精度浮点数
布尔型 bool bool 布尔值 (true/false)
字符与字符串 char, string char, std::string 单个字符和字符串
特殊类型 byte uint8_t 用于表示原始字节数据

📝 使用数组和边界限制

ROS 2的接口定义非常灵活,除了单个数据,你还可以轻松地定义数组,并为其设定大小边界,这在需要预分配内存或限制数据大小时非常有用。

text
# 数组定义示例
int32[]          unbounded_array      # 无界动态数组,大小可变
int32[5]         static_array         # 静态数组,固定包含5个元素
int32[<=10]      bounded_array        # 有界动态数组,最多包含10个元素

# 字符串长度限制示例
string           unbounded_string     # 无界字符串,长度不限
string<=20       bounded_string       # 有界字符串,最多20个字符

# 组合使用示例:一个有界字符串数组,数组最多包含5个元素,每个字符串最多10个字符
string<=10[<=5]  bounded_string_array

🧩 嵌套其他消息类型

你可以在自定义消息中直接使用其他已有的消息类型作为字段,这被称为嵌套消息。这使得复用官方或第三方包中定义好的消息结构成为可能。

text
# 引用其他包中的消息类型,格式为 "包名/消息名"
geometry_msgs/Point   position
std_msgs/Header       header

# 也可以引用同一个包内定义的其他消息类型
AnotherMessage        data

💡 实际应用示例

下面是一个综合运用了上述类型定义的自定义消息文件 (MyCustomMessage.msg):

text
# 基础类型
int32            id
string           name
float64          temperature

# 数组和嵌套消息示例
float32[]        readings              # 无界浮点数组
int32[<=5]       corner_ids            # 有界整数数组,最多5个元素
geometry_msgs/Point   estimated_pose   # 嵌套一个标准的位置消息

定义好这个 .msg 文件后,按照官方教程配置好 CMakeLists.txt 和 package.xml,ROS 2就会自动为你生成对应的C++、Python等语言的代码,你就可以在你的节点中使用它了。

⚠️ 一个重要的设计建议

虽然ROS 2提供了像 std_msgs/Int32 或 std_msgs/String 这样的基础类型包装,但官方文档不建议在长期项目或正式接口中直接使用它们。

原因在于这些消息只有一个名为 data 的字段,缺乏语义。例如,使用 std_msgs/Float64 发布一个温度值,任何人都无法从消息定义本身得知这个浮点数代表的是温度、压力还是其他物理量。因此,强烈推荐为你的应用创建自定义消息,并为其字段赋予清晰、有意义的名称(如 temperature_celsius),这会让你的接口更易用、更健壮。

 

2、在package.xml和CMakeLists.txt中添加如下配置:

image

 

CMakeLists.txt

find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/Jointmotion.msg"
)

 package.xml

<buildtool_depend>rosidl_default_generators</buildtool_depend>

<exec_depend>rosidl_default_runtime</exec_depend>

<depend>action_msgs</depend>    

<member_of_group>rosidl_interface_packages</member_of_group>

image


3、编译
colcon build --packages-select pkg_interfaces

4、刷新terminal环境
source install/setup.bash
5、查看接口类型,正常情况下,终端将会输出与Person.msg文件一致的内容。
ros2 interface show pkg_interfaces/msg/Person

3.4.3 用于 服务  的消息 service     request-response

服务通讯是一种基于请求响应的通信模型,在通信双方中,客户端发送请求数据到服务端,服务端响应结果给客户端。

客户端/服务器模型如下:

image8

从服务的实现机制上来看,这种你问我答的形式叫做客户端/服务器模型,简称为CS模型,客户端在需要某些数据的时候,针对某个具体的服务,发送请求信息,服务器端收到请求之后,就会进行处理并反馈应答信息。

这种通信机制在生活中也很常见,比如我们经常浏览的各种网页,此时你的电脑浏览器就是客户端,通过域名或者各种操作,向网站服务器发送请求,服务器收到之后返回需要展现的页面数据。

在【ROS2动作通讯服务端实现】课程中我们已经创建了自定义接口功能包,功能包pkg_interfaces下新建srv文件夹,srv文件夹下新建Add.srv文件,文件中输入如下内容:

 
int32 num1

int32 num2

---

int32 sum

3.4.4 用于 动作  的消息 action      goal feedback result

动作通信是一种带有连续反馈的通信模型,在通信双方中,客户端发送请求数据到服务端,服务端响应结果给客户端,但是在服务端接收到请求到产生最终响应的过程中,会发送连续的反馈信息到客户端。

动作通讯客户端/服务器模型如下:

image8

 

接着在pkg_interfaces功能包下面创建一个action的文件夹,并在action文件夹内新建【Progress.action】文件,文件内容如下
 
int64 num
---
int64 sum
---
float64 progress
posted @ 2026-03-15 17:01  辛河  阅读(113)  评论(0)    收藏  举报