欢迎光临
我们一直在努力

ROS2 C++ 小车控制完整实战(二):自定义 msg 消息发布与订阅保姆级教程

目录

摘要

前言

一、本章案例整体说明

1.1 Topic 和 msg 的关系

1.2 本篇要实现什么

1.3 本篇涉及的功能包

二、自定义 msg 接口文件

2.1 为什么需要自定义 LimoStatus.msg

2.2 LimoStatus.msg 文件位置

2.3 LimoStatus.msg 文件内容

2.4 字段含义说明

2.5 std_msgs/Header header 是什么

2.6 msg 文件注意事项

(1)字段后面不需要写分号

(2)如果使用了其它功能包里的类型,需要在配置文件中声明依赖。

(3)修改 .msg 文件后必须重新编译。

三、配置自定义接口包 limo_msgs

3.1 rosidl_generate_interfaces 是什么

3.2 limo_msgs 的 CMakeLists.txt

3.3 CMakeLists.txt 核心解释

3.4 limo_msgs 的 package.xml

3.5 package.xml 核心解释

四、C++ 发布自定义 msg 状态消息

4.1 自定义 msg 发布节点作用

4.2 自定义 msg 发布完整代码

4.3 代码核心名字和对象总结

(1)LimoStatusPub:自定义节点类

(2) "limo_status_pub":节点名称

(3)status_pub_:发布者对象

(4)"/limo_status":话题名称

(5)timer_:定时器对象

(6)timer_callback():定时器回调函数

(7)status_msg:消息对象

(8)核心总结:

4.4 发布端核心代码解释

(1)引入自定义消息头文件:

(2)创建自定义消息发布器:

(3)创建定时器:

(4)回调函数 timer_callback 的作用

五、C++ 订阅自定义 msg 状态消息

5.1 为什么还要写 Subscriber

5.2 自定义 msg 订阅完整代码

5.3 订阅器核心代码解释

(1)创建订阅器的代码如下:

(2) 回调函数 status_callback 的作用

5.4 Publisher 和 Subscriber 的关系

六、配置代码包 limo_learning

6.1 limo_learning 的 package.xml

6.2 limo_learning 的 CMakeLists.txt

6.3 CMakeLists.txt 核心理解

七、编译与运行测试

7.1 先编译自定义接口包

7.2 再编译代码包

7.3 查看自定义 msg 是否生成成功

7.4 运行自定义 msg 发布节点

7.5 运行自定义 msg 订阅节点

7.6 使用命令查看话题

八、常见问题总结

8.1 找不到 limo_msgs/msg/limo_status.hpp

8.2 修改 LimoStatus.msg 后代码不生效

8.3 ros2 interface show 找不到接口

8.4 ros2 run 找不到节点

8.5 订阅节点没有任何输出

8.6 回调函数没有执行

九、本章总结


摘要

上一篇文章主要讲解了 ROS2 C++ 中最基础的 Topic 话题通信,

ROS2 C++ 小车控制完整实战(一):一文搞懂 Topic 发布订阅、Twist 消息与 /cmd_vel 速度控制-CSDN博客https://blog.csdn.net/m0_58954356/article/details/161829804?spm=1001.2014.3001.5502包括:

  • Topic + 官方 msg:geometry_msgs/msg/Twist
  • Topic + 官方 msg:sensor_msgs/msg/LaserScan
  • /cmd_vel 速度控制
  • /scan 雷达数据订阅
  • Publisher 发布器
  • Subscriber 订阅器
  • 通过上一篇内容,我们已经知道:

    • Topic 通信一定使用 msg 类型数据
    • msg 可以是 ROS2 官方标准消息,也可以是我们自己定义的自定义消息

    上一篇主要使用的是 ROS2 官方已经提供好的标准消息,例如:

    geometry_msgs/msg/Twist
    sensor_msgs/msg/LaserScan

    这一篇继续往后走,开始讲 ROS2 自定义 msg 消息实践。

    本篇主要围绕一个自定义接口展开:

  • 自定义 msg:limo_msgs/msg/LimoStatus
  • 本篇要实现的功能包括:

  • Topic + 自定义 msg:发布和订阅 LIMO 小车状态信息
  • 通过本篇文章,需要重点理解:

  • 自定义 msg 文件怎么写
  • std_msgs/Header header 是什么
  • 接口包 limo_msgs 如何配置
  • 代码包 limo_learning 如何依赖自定义 msg
  • C++ 中如何发布自定义消息
  • C++ 中如何订阅自定义消息
  • 自定义 msg 修改后为什么必须重新编译

  • 前言

    在 ROS2 中,通信接口是非常重要的一部分。

    上一篇我们已经通过 /cmd_vel 和 /scan 理解了 Topic 通信。

    其中:

    /cmd_vel 使用 geometry_msgs/msg/Twist
    /scan 使用 sensor_msgs/msg/LaserScan

    这两个都是 ROS2 官方提供的标准 msg。

    但是在实际机器人项目中,官方消息并不能覆盖所有业务需求。

    比如我们想发布 LIMO 小车自己的状态信息:

  • 车辆状态
  • 控制模式
  • 电池电压
  • 错误码
  • 运动模式
  • 这时候用标准的 Twist、LaserScan、String 都不太合适。

    因为这些字段属于具体机器人项目中的业务状态,标准消息不一定刚好满足。

    更好的方式是自己定义一个消息:

    limo_msgs/msg/LimoStatus

    然后通过 Topic 发布出去,再由其它节点订阅接收。

    所以本篇文章的重点就是:

    从官方标准 msg 走向自定义 msg

    也就是从“会用 ROS2 自带消息”,进一步到“会写自己的 ROS2 消息接口”。


    一、本章案例整体说明

    1.1 Topic 和 msg 的关系

    很多初学者刚开始学习 ROS2 时,会把 Topic 和 msg 混在一起。

    其实它们之间的关系可以这样理解:

  • Topic 是通信方式
  • msg 是 Topic 中传输的数据结构
  • 也就是说:

    Topic 通信一定要使用 msg 类型数据

    但是 msg 分为两种:

  • 官方标准 msg
  • 自定义 msg
  • 例如:

    通信方式对应接口类型示例是否自定义
    Topic msg geometry_msgs/msg/Twist 官方标准 msg
    Topic msg sensor_msgs/msg/LaserScan 官方标准 msg
    Topic msg limo_msgs/msg/LimoStatus 自定义 msg

    上一篇文章主要讲的是:

    Topic + 官方标准 msg

    这一篇主要讲的是:

    Topic + 自定义 msg


    1.2 本篇要实现什么

    本篇要实现一个完整的自定义 msg 发布和订阅案例。

    也就是定义一个:

    LimoStatus.msg

    用来描述小车状态。

    然后在 C++ 中分别写两个节点:

    limo_status_pub:发布小车状态
    limo_status_sub:订阅小车状态

    整体流程如下:

    定义 LimoStatus.msg

    配置 limo_msgs 接口包

    编译生成自定义消息头文件

    在 limo_learning 中 include 自定义消息

    编写 Publisher 发布节点

    编写 Subscriber 订阅节点

    运行发布端和订阅端测试


    1.3 本篇涉及的功能包

    建议使用两个功能包:

    limo_msgs 存放自定义 msg 接口
    limo_learning 存放 C++ 节点代码

    整体目录结构如下:

    agilex_open_class_ws/
    └── src
    ├── limo_msgs
    │ ├── msg
    │ │ └── LimoStatus.msg
    │ ├── CMakeLists.txt
    │ └── package.xml

    └── limo_learning
    ├── src
    │ ├── limo_status_pub.cpp
    │ └── limo_status_sub.cpp
    ├── CMakeLists.txt
    └── package.xml

    其中:

    功能包 作用
    limo_msgs 定义自定义消息接口
    limo_learning 编写 C++ 发布节点和订阅节点

    为什么要单独创建 limo_msgs?

  • 因为在实际项目中,自定义接口通常会单独放在一个接口包中。
  • 这样其它功能包也可以复用它。
  • 例如:

    limo_learning 可以使用 limo_msgs
    navigation_pkg 也可以使用 limo_msgs
    monitor_pkg 也可以使用 limo_msgs

    所以可以把 limo_msgs 理解成:

    专门存放通信数据结构的功能包

    而 limo_learning 才是:

    真正编写节点逻辑代码的功能包


    二、自定义 msg 接口文件

    2.1 为什么需要自定义 LimoStatus.msg

    前面的 /cmd_vel 和 /scan 都是 Topic 通信,也都使用了 msg。

    但是它们使用的是 ROS2 官方标准消息:

    geometry_msgs/msg/Twist
    sensor_msgs/msg/LaserScan

    如果只是控制速度,使用 Twist 很合适。

    如果只是接收雷达数据,使用 LaserScan 很合适。

    但是如果我们想发布 LIMO 小车状态,例如:

    车辆状态
    控制模式
    电池电压
    错误码
    运动模式

    这些字段就比较偏向具体业务。

    这时候就适合自己定义一个消息:

    limo_msgs/msg/LimoStatus


    2.2 LimoStatus.msg 文件位置

    自定义 msg 文件一般放在接口包的 msg 目录下。

    文件路径:

    limo_msgs/msg/LimoStatus.msg

    注意文件名建议使用大驼峰命名:

    LimoStatus.msg

    后面编译生成 C++ 头文件时,会变成小写加下划线形式:

    #include "limo_msgs/msg/limo_status.hpp"

    也就是说:

    LimoStatus.msg
    ↓ 编译生成
    limo_status.hpp


    2.3 LimoStatus.msg 文件内容

    LimoStatus.msg 内容如下:

    std_msgs/Header header

    uint8 vehicle_state
    uint8 control_mode
    float64 battery_voltage
    uint16 error_code
    uint8 motion_mode

    每一行的基本格式是:

    字段类型 字段名

    例如:

    float64 battery_voltage

    表示定义了一个 float64 类型字段,字段名叫 battery_voltage,可以用来表示电池电压。

    再比如:

    uint16 error_code

    表示定义了一个 uint16 类型字段,字段名叫 error_code,可以用来表示错误码。


    2.4 字段含义说明

    可以把 LimoStatus.msg 中的字段理解成下面这样:

    字段 类型 含义
    header std_msgs/Header 标准消息头
    vehicle_state uint8 车辆状态
    control_mode uint8 控制模式
    battery_voltage float64 电池电压
    error_code uint16 错误码
    motion_mode uint8 运动模式

    这些字段只是示例。

    实际项目中,可以根据自己的机器人状态继续扩展。

    例如还可以增加:

    float64 current_speed
    float64 battery_percentage
    bool emergency_stop


    2.5 std_msgs/Header header 是什么

    在 LimoStatus.msg 中,第一行是:

    std_msgs/Header header

    这个字段表示标准消息头。

    它通常包含两个重要信息:

    stamp:时间戳
    frame_id:坐标系名称

    可以简单理解为:

    stamp 表示这条消息是什么时候产生的
    frame_id 表示这条消息属于哪个坐标系

    例如后面 C++ 代码中会这样赋值:

    status_msg.header.stamp = this->now();
    status_msg.header.frame_id = "base_link";

    其中:

    this->now() 表示当前 ROS2 时间
    base_link 表示机器人底盘坐标系

    为什么要有 header?

    因为机器人系统中经常需要知道:

  • 这条状态消息是什么时候产生的?
  • 这条传感器数据是什么时候采集的?
  • 这条里程计数据属于哪个坐标系?
  • 所以 header 在机器人开发中非常常见。


    2.6 msg 文件注意事项

    写 .msg 文件时要注意:

    (1)字段后面不需要写分号

    错误写法:

    float64 battery_voltage;

    正确写法:

    float64 battery_voltage

    (2)如果使用了其它功能包里的类型,需要在配置文件中声明依赖。

    例如这里使用了:

    std_msgs/Header header

    所以 limo_msgs 的 CMakeLists.txt 和 package.xml 中都要配置 std_msgs 依赖。

    (3)修改 .msg 文件后必须重新编译。

    因为 .msg 文件不能直接被 C++ 使用,ROS2 需要先根据 .msg 文件生成 C++ 头文件。


    三、配置自定义接口包 limo_msgs

    3.1 rosidl_generate_interfaces 是什么

    在 ROS2 中,我们自己编写的 .msg 文件,本质上只是一个接口描述文件。

    例如:

    msg/LimoStatus.msg

    它只是告诉 ROS2:

    这个消息里面有哪些字段、字段类型是什么、字段名称叫什么。

    但是这个 .msg 文件本身还不能直接被 C++ 或 Python 程序使用。也就是说,C++ 程序不能直接拿 .msg 文件来 include,Python 程序也不能直接拿 .msg 文件来 import

    所以 ROS2 需要在编译时,根据我们写好的 .msg 文件,自动生成对应语言可以使用的接口代码。

    这时候就需要用到:

    rosidl_generate_interfaces

    它的作用可以理解为:

    自定义 .msg / .srv / .action 文件

    rosidl_generate_interfaces 参与编译生成

    生成 C++ / Python 可以使用的接口代码

    其它 ROS2 节点才能正常发布、订阅、调用这些自定义接口

    需要注意的是,rosidl_generate_interfaces 不是在终端里单独执行的命令,而是写在 CMakeLists.txt 里面的一条 CMake 配置语句

    也就是说,我们不是手动运行:

    rosidl_generate_interfaces

    而是在 CMakeLists.txt 中配置:

    rosidl_generate_interfaces(${PROJECT_NAME}
    "msg/LimoStatus.msg"
    DEPENDENCIES std_msgs
    )

    然后再通过:

    colcon build

    编译整个工作空间。

    编译过程中,ROS2 会自动读取 CMakeLists.txt 中的 rosidl_generate_interfaces 配置,根据 msg/LimoStatus.msg 生成对应的 C++、Python 等接口代码。

    简单来说:

    rosidl_generate_interfaces 就是 ROS2 自定义接口包的“代码生成开关”。

    只有在 CMakeLists.txt 中正确配置它,ROS2 才知道当前功能包有哪些自定义接口文件需要参与编译生成。


    3.2 limo_msgs 的 CMakeLists.txt

    自定义接口包需要使用:

    rosidl_generate_interfaces

    来生成接口代码。

    limo_msgs/CMakeLists.txt 可以这样写:

    cmake_minimum_required(VERSION 3.8)
    project(limo_msgs)

    if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
    add_compile_options(-Wall -Wextra -Wpedantic)
    endif()

    find_package(ament_cmake REQUIRED)
    find_package(rosidl_default_generators REQUIRED)
    find_package(std_msgs REQUIRED)

    rosidl_generate_interfaces(${PROJECT_NAME}
    "msg/LimoStatus.msg"
    DEPENDENCIES std_msgs
    )

    ament_package()


    3.3 CMakeLists.txt 核心解释

    查找构建工具:

    find_package(ament_cmake REQUIRED)

    表示当前功能包使用 ament_cmake 构建。

    查找接口生成工具:

    find_package(rosidl_default_generators REQUIRED)

    表示当前功能包需要使用 ROS2 接口生成工具。

    查找 std_msgs:

    find_package(std_msgs REQUIRED)

    因为 LimoStatus.msg 中使用了:

    std_msgs/Header header

    所以这里必须找到 std_msgs。

    生成接口:

    rosidl_generate_interfaces(${PROJECT_NAME}
    "msg/LimoStatus.msg"
    DEPENDENCIES std_msgs
    )

    这句是自定义接口包的核心。

    它的作用是根据:

    msg/LimoStatus.msg

    生成 C++、Python 等语言可以使用的接口代码。

    其中:

    DEPENDENCIES std_msgs

    表示这个自定义消息依赖 std_msgs。

    最后:

    ament_package()

    表示声明当前功能包是一个 ROS2 ament 功能包。


    3.4 limo_msgs 的 package.xml

    limo_msgs/package.xml 可以这样写:

    <?xml version="1.0"?>
    <package format="3">
    <name>limo_msgs</name>
    <version>0.0.0</version>
    <description>Custom messages for LIMO robot</description>
    <maintainer email="agilex@todo.todo">agilex</maintainer>
    <license>Apache-2.0</license>

    <buildtool_depend>ament_cmake</buildtool_depend>

    <build_depend>rosidl_default_generators</build_depend>
    <exec_depend>rosidl_default_runtime</exec_depend>

    <depend>std_msgs</depend>

    <member_of_group>rosidl_interface_packages</member_of_group>

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


    3.5 package.xml 核心解释

    构建工具依赖:

    <buildtool_depend>ament_cmake</buildtool_depend>

    表示当前功能包使用 ament_cmake 构建。

    接口生成依赖:

    <build_depend>rosidl_default_generators</build_depend>

    表示构建时需要接口生成工具。

    运行时接口依赖:

    <exec_depend>rosidl_default_runtime</exec_depend>

    表示运行时需要接口相关运行环境。

    标准消息依赖:

    <depend>std_msgs</depend>

    因为 LimoStatus.msg 中使用了:

    std_msgs/Header

    所以这里必须声明 std_msgs。

    接口包声明:

    <member_of_group>rosidl_interface_packages</member_of_group>

    这一项非常重要,表示当前功能包属于 ROS2 接口包。

    如果忘记这一项,自定义接口可能无法正常生成或被其它功能包使用。


    四、C++ 发布自定义 msg 状态消息

    4.1 自定义 msg 发布节点作用

    接下来在 limo_learning 功能包中写一个 C++ 发布节点:

    limo_status_pub

    它的作用是:

  • 每隔 500ms 创建一条 LimoStatus 消息
  • 给车辆状态、电池电压、错误码等字段赋值
  • 发布到 /limo_status 话题
  • 整体流程如下:

    创建 ROS2 节点

    创建 Publisher

    创建定时器

    定时填充 LimoStatus 消息

    发布到 /limo_status


    4.2 自定义 msg 发布完整代码

    文件路径:

    limo_learning/src/limo_status_pub.cpp

    代码如下:

    ```cpp
    // 引入智能指针相关头文件
    // std::make_shared 会用到
    #include <memory>

    // 引入函数绑定相关头文件
    // std::bind 会用到
    #include <functional>

    // 引入 ROS2 C++ 客户端库
    // rclcpp 是 ROS2 中 C++ 编程最核心的库
    #include "rclcpp/rclcpp.hpp"

    // 引入自定义消息头文件
    // 这个头文件是由 limo_msgs/msg/LimoStatus.msg 编译后自动生成的
    #include "limo_msgs/msg/limo_status.hpp"

    // 使用 chrono_literals 后,可以直接写 500ms、1s 这种时间单位
    using namespace std::chrono_literals;

    // 定义一个发布 LIMO 状态消息的节点类
    // 该类继承自 rclcpp::Node,说明它是一个 ROS2 节点
    class LimoStatusPub : public rclcpp::Node
    {
    public:
    // 构造函数
    // Node("limo_status_pub") 表示创建一个名为 limo_status_pub 的节点
    LimoStatusPub() : Node("limo_status_pub")
    {
    // 创建一个发布者,用来发布自定义消息 LimoStatus
    // 发布的话题名称是 /limo_status
    // 队列长度为 10
    status_pub_ = this->create_publisher<limo_msgs::msg::LimoStatus>(
    "/limo_status",
    10
    );

    // 创建一个定时器
    // 每隔 500ms 执行一次 timer_callback 回调函数
    timer_ = this->create_wall_timer(
    500ms,
    std::bind(&LimoStatusPub::timer_callback, this)
    );

    // 打印日志,表示节点已经启动
    RCLCPP_INFO(this->get_logger(), "limo_status_pub node has started.");
    }

    private:
    // 定时器回调函数
    // 每隔 500ms 会自动执行一次
    void timer_callback()
    {
    // 创建一个自定义消息对象
    // 消息类型来自 limo_msgs/msg/LimoStatus.msg
    limo_msgs::msg::LimoStatus status_msg;

    // 给消息头 header 赋值
    // stamp 表示当前 ROS2 时间
    status_msg.header.stamp = this->now();

    // frame_id 表示该状态消息对应的坐标系
    // base_link 通常表示机器人底盘坐标系
    status_msg.header.frame_id = "base_link";

    // 给自定义消息中的各个字段赋值

    // vehicle_state 表示车辆状态
    // 这里假设 1 表示正常运行状态
    status_msg.vehicle_state = 1;

    // control_mode 表示控制模式
    // 这里假设 2 表示某种控制模式,例如遥控或自动控制
    status_msg.control_mode = 2;

    // battery_voltage 表示电池电压
    // 这里模拟当前电压为 24.5V
    status_msg.battery_voltage = 24.5;

    // error_code 表示错误码
    // 0 通常表示没有错误
    status_msg.error_code = 0;

    // motion_mode 表示运动模式
    // 这里假设 1 表示当前处于某种运动模式
    status_msg.motion_mode = 1;

    // 发布状态消息到 /limo_status 话题
    status_pub_->publish(status_msg);

    // 打印发布出去的状态信息,方便在终端观察
    RCLCPP_INFO(this->get_logger(),
    "Publish status: battery = %.2f, error_code = %d",
    status_msg.battery_voltage,
    status_msg.error_code);
    }

    private:
    // 自定义消息发布者对象
    // 用于发布 limo_msgs::msg::LimoStatus 类型的消息
    rclcpp::Publisher<limo_msgs::msg::LimoStatus>::SharedPtr status_pub_;

    // 定时器对象
    // 用于周期性触发 timer_callback 回调函数
    rclcpp::TimerBase::SharedPtr timer_;
    };

    int main(int argc, char * argv[])
    {
    // 初始化 ROS2 C++ 客户端库
    // argc 和 argv 用于接收终端传入的参数
    rclcpp::init(argc, argv);

    // 创建 LimoStatusPub 节点对象,并让节点进入运行状态
    // spin 会持续运行节点,让定时器、回调函数等正常工作
    rclcpp::spin(std::make_shared<LimoStatusPub>());

    // 关闭 ROS2 客户端库
    rclcpp::shutdown();

    return 0;
    }
    ```


    4.3 代码核心名字和对象总结

    (1)LimoStatusPub:自定义节点类

    class LimoStatusPub : public rclcpp::Node

    LimoStatusPub 是我们自己定义的一个类。

    它继承自:

    rclcpp::Node

    说明它是一个 ROS2 节点。

    可以理解为:

    LimoStatusPub = 一个专门发布 LIMO 状态信息的 ROS2 节点类

    后面在 main() 函数中:

    rclcpp::spin(std::make_shared<LimoStatusPub>());

    就是创建这个节点,并让它一直运行。


    (2) "limo_status_pub":节点名称

    LimoStatusPub() : Node("limo_status_pub")

    这里的:

    "limo_status_pub"

    表示当前 ROS2 节点的名字。

    启动后,可以通过命令查看:

    ros2 node list

    正常情况下会看到:

    /limo_status_pub

    也就是说:

    limo_status_pub = 节点名字


    (3)status_pub_:发布者对象

    rclcpp::Publisher<limo_msgs::msg::LimoStatus>::SharedPtr status_pub_;

    status_pub_ 是一个发布者对象。

    它的作用是:

    负责把 LimoStatus 类型的消息发布出去

    它发布的消息类型是:

    limo_msgs::msg::LimoStatus

    也就是我们自己定义的自定义消息。

    在构造函数中,通过下面这句创建发布者:

    status_pub_ = this->create_publisher<limo_msgs::msg::LimoStatus>(
    "/limo_status",
    10
    );

    可以理解为:

  • 创建一个发布者 status_pub_
  • 它向 /limo_status 话题发布 LimoStatus 类型的消息
  • 队列长度是 10

  • (4)"/limo_status":话题名称

    "/limo_status"

    这是发布者发布消息的话题名称。

    ROS2 中,节点之间不是直接通信,而是通过话题通信。

    这里的通信关系可以理解为:

    limo_status_pub 节点
    ↓ 发布
    /limo_status 话题
    ↓ 订阅
    其它订阅者节点

    如果有其它节点想要获取 LIMO 小车状态,就可以订阅:

    /limo_status

    也可以用终端查看这个话题:

    ros2 topic list

    或者查看话题数据:

    ros2 topic echo /limo_status


    (5)timer_:定时器对象

    rclcpp::TimerBase::SharedPtr timer_;

    timer_ 是一个定时器对象。

    它的作用是:

    周期性触发某个函数

    在代码中,它每隔 500ms 调用一次 timer_callback():

    timer_ = this->create_wall_timer(
    500ms,
    std::bind(&LimoStatusPub::timer_callback, this)
    );

    可以理解为:

    每隔 500ms,自动执行一次 timer_callback 函数

    所以这个节点不是只发布一次消息,而是会持续周期性发布。


    (6)timer_callback():定时器回调函数

    void timer_callback()

    timer_callback() 是定时器回调函数。

    它的作用是:

  • 创建消息对象
  • 给消息字段赋值
  • 发布消息
  • 打印日志
  • 也就是说,每次定时器触发,都会执行这里面的代码。

    核心流程是:

    创建 LimoStatus 消息

    填写 header、车辆状态、电池电压、错误码等字段

    通过 status_pub_ 发布到 /limo_status

    打印日志


    (7)status_msg:消息对象

    limo_msgs::msg::LimoStatus status_msg;

    status_msg 是一个具体的消息对象。

    它的类型是:

    limo_msgs::msg::LimoStatus

    也就是自定义消息 LimoStatus.msg 编译后生成的 C++ 类型。

    可以理解为:

    status_msg = 准备发布出去的一条 LIMO 状态消息

    后面对它的字段赋值:

    status_msg.vehicle_state = 1;
    status_msg.control_mode = 2;
    status_msg.battery_voltage = 24.5;
    status_msg.error_code = 0;
    status_msg.motion_mode = 1;

    就是在填写这条状态消息的具体内容。


    (8)核心总结:

    LimoStatusPub 自定义节点类
    limo_status_pub 节点名称
    status_pub_ 发布者对象
    /limo_status 发布的话题名称
    timer_ 定时器对象
    timer_callback 定时器回调函数
    status_msg 要发布的自定义消息对象


    4.4 发布端核心代码解释

    (1)引入自定义消息头文件:

    #include "limo_msgs/msg/limo_status.hpp"

    需要注意:

    源文件叫:

    LimoStatus.msg

    但是 C++ 中 include 的头文件是:

    limo_status.hpp

    这是 ROS2 接口生成后的命名规则。

    一般规律是:

    大驼峰 .msg 文件名

    小写 + 下划线 .hpp 头文件

    例如:

    LimoStatus.msg → limo_status.hpp
    RobotState.msg → robot_state.hpp

    (2)创建自定义消息发布器:

    status_pub_ = this->create_publisher<limo_msgs::msg::LimoStatus>(
    "/limo_status",
    10
    );

    这句代码表示:

    创建一个 Publisher
    向 /limo_status 话题发布数据
    消息类型是 limo_msgs::msg::LimoStatus
    队列长度是 10

    对应关系如下:

    内容 含义
    limo_msgs::msg::LimoStatus 自定义消息类型
    /limo_status 发布的话题名称
    10 消息队列长度

    (3)创建定时器:

    timer_ = this->create_wall_timer(
    500ms,
    std::bind(&LimoStatusPub::timer_callback, this)
    );

    这句表示:

    每隔 500ms 自动执行一次 timer_callback 函数

    也就是说,状态消息不是只发一次,而是定时持续发布。

    std::bind 可以简单理解为:

    把当前对象中的 timer_callback 函数绑定起来,交给 ROS2 定时器自动调用

    因为 timer_callback 是类中的成员函数,所以要写:

    std::bind(&LimoStatusPub::timer_callback, this)

    其中:

  • &LimoStatusPub::timer_callback 表示类中的成员函数
  • this 表示当前这个节点对象
  • (4)回调函数 timer_callback 的作用

    回调函数:

    void timer_callback()

    它不会由我们手动调用,而是由 ROS2 定时器自动调用。

    每隔 500ms 执行一次。

    它主要做四件事:

  • 创建 LimoStatus 消息对象
  • 给 header 赋值
  • 给小车状态字段赋值
  • 发布消息到 /limo_status
  • 创建消息对象:

    limo_msgs::msg::LimoStatus status_msg;

    给消息头赋值:

    status_msg.header.stamp = this->now();
    status_msg.header.frame_id = "base_link";

    给状态字段赋值:

    status_msg.vehicle_state = 1;
    status_msg.control_mode = 2;
    status_msg.battery_voltage = 24.5;
    status_msg.error_code = 0;
    status_msg.motion_mode = 1;

    真正发布消息:

    status_pub_->publish(status_msg);

    这里一定要注意:

    RCLCPP_INFO 只是打印日志
    publish 才是真正发布消息


    五、C++ 订阅自定义 msg 状态消息

    5.1 为什么还要写 Subscriber

    前面已经完成了自定义 msg 的发布节点:

    limo_status_pub

    它的作用是定时向:

    /limo_status

    话题发布 limo_msgs/msg/LimoStatus 类型的小车状态消息。

    但是 Topic 通信本质上是:

    Publisher 发布消息
    Subscriber 订阅消息

    如果只有发布者,没有订阅者,就只能说明消息发出去了,但还没有演示如何在 C++ 节点中接收这条自定义消息。

    它的作用是:

  • 订阅 /limo_status 话题
  • 接收 LimoStatus 自定义消息
  • 在回调函数中读取电池电压、错误码、车辆状态等字段
  • 打印接收到的小车状态信息
  • 整体流程如下:

    limo_status_pub 发布 /limo_status

    /limo_status 话题传输 LimoStatus 消息

    limo_status_sub 订阅 /limo_status

    回调函数接收并解析状态数据


    5.2 自定义 msg 订阅完整代码

    文件路径:

    limo_learning/src/limo_status_sub.cpp

    代码如下:

    ```cpp
    // 引入智能指针相关头文件
    // std::make_shared 会用到
    #include <memory>

    // 引入函数绑定相关头文件
    // std::bind 和 std::placeholders::_1 会用到
    #include <functional>

    // 引入 ROS2 C++ 客户端库
    // rclcpp 是 ROS2 中 C++ 编程最核心的库
    #include "rclcpp/rclcpp.hpp"

    // 引入自定义消息头文件
    // 这个头文件由 limo_msgs/msg/LimoStatus.msg 编译后自动生成
    #include "limo_msgs/msg/limo_status.hpp"

    // 定义一个订阅 LIMO 状态消息的节点类
    // 该类继承自 rclcpp::Node,说明它是一个 ROS2 节点
    class LimoStatusSub : public rclcpp::Node
    {
    public:
    // 构造函数
    // Node("limo_status_sub") 表示创建一个名为 limo_status_sub 的节点
    LimoStatusSub() : Node("limo_status_sub")
    {
    // 创建一个订阅者,用来订阅自定义消息 LimoStatus
    // 订阅的话题名称是 /limo_status
    // 队列长度为 10
    // 当接收到消息后,会自动调用 status_callback 回调函数
    status_sub_ = this->create_subscription<limo_msgs::msg::LimoStatus>(
    "/limo_status",
    10,
    std::bind(&LimoStatusSub::status_callback,
    this,
    std::placeholders::_1)
    );

    // 打印日志,表示订阅者节点已经启动
    RCLCPP_INFO(this->get_logger(), "limo_status_sub node has started.");
    }

    private:
    // 订阅回调函数
    // 当 /limo_status 话题上有新消息时,该函数会被自动调用
    // msg 表示接收到的 LimoStatus 消息
    void status_callback(const limo_msgs::msg::LimoStatus::SharedPtr msg)
    {
    // 打印接收到的 LIMO 状态信息
    // msg-> 表示通过指针访问消息对象中的字段
    RCLCPP_INFO(this->get_logger(),
    "Receive status: frame_id = %s, vehicle_state = %d, control_mode = %d, battery = %.2f, error_code = %d, motion_mode = %d",
    msg->header.frame_id.c_str(),
    msg->vehicle_state,
    msg->control_mode,
    msg->battery_voltage,
    msg->error_code,
    msg->motion_mode);
    }

    private:
    // 自定义消息订阅者对象
    // 用于订阅 limo_msgs::msg::LimoStatus 类型的消息
    rclcpp::Subscription<limo_msgs::msg::LimoStatus>::SharedPtr status_sub_;
    };

    int main(int argc, char * argv[])
    {
    // 初始化 ROS2 C++ 客户端库
    // argc 和 argv 用于接收终端传入的参数
    rclcpp::init(argc, argv);

    // 创建 LimoStatusSub 节点对象,并让节点进入运行状态
    // spin 会持续运行节点,让订阅者可以一直等待和处理消息
    rclcpp::spin(std::make_shared<LimoStatusSub>());

    // 关闭 ROS2 客户端库
    rclcpp::shutdown();

    return 0;
    }
    ```


    5.3 订阅器核心代码解释

    (1)创建订阅器的代码如下:

    status_sub_ = this->create_subscription<limo_msgs::msg::LimoStatus>(
    "/limo_status",
    10,
    std::bind(&LimoStatusSub::status_callback,
    this,
    std::placeholders::_1)
    );

    这句代码表示创建一个 Subscriber,用来订阅 /limo_status 话题。

    其中:

    内容 含义
    limo_msgs::msg::LimoStatus 订阅的消息类型
    /limo_status 订阅的话题名称
    10 消息队列长度
    status_callback 收到消息后执行的回调函数

    也就是说,只要 /limo_status 话题上有新消息,ROS2 就会自动调用:

    status_callback()

    (2) 回调函数 status_callback 的作用

    订阅回调函数如下:

    void status_callback(const limo_msgs::msg::LimoStatus::SharedPtr msg)

    这个回调函数接收的是一个 LimoStatus 类型的消息指针。

    这里的 msg 只是形参名字,也就是“接收到的消息对象叫什么名字”。

    可以任意修改:

    void status_callback(const limo_msgs::msg::LimoStatus::SharedPtr status_msg)
    {
    RCLCPP_INFO(this->get_logger(),
    "battery = %.2f",
    status_msg->battery_voltage);
    }

    但是注意:你前面名字改了,函数里面也要跟着改。

  • 接收到一条 LimoStatus 消息,
  • 把这条消息临时命名为 msg,
  • 然后在函数里面通过 msg->字段名 读取消息内容。

  • msg

    就是订阅器收到的自定义消息数据。

    因为 LimoStatus.msg 中定义了这些字段:

    std_msgs/Header header
    uint8 vehicle_state
    uint8 control_mode
    float64 battery_voltage
    uint16 error_code
    uint8 motion_mode

    所以在 C++ 回调函数中,就可以通过:

    msg->header.frame_id
    msg->vehicle_state
    msg->control_mode
    msg->battery_voltage
    msg->error_code
    msg->motion_mode

    读取对应字段。

    例如:

    msg->battery_voltage

    表示读取电池电压。

    msg->error_code

    表示读取错误码。

    msg->header.frame_id

    表示读取消息对应的坐标系名称。

    由于 frame_id 是字符串,打印时需要写成:

    msg->header.frame_id.c_str()


    5.4 Publisher 和 Subscriber 的关系

    到这里,自定义 msg 的发布和订阅就都完整了。

    发布端:

    limo_status_pub

    负责创建 LimoStatus 消息,并发布到:

    /limo_status

    订阅端:

    limo_status_sub

    负责订阅:

    /limo_status

    并在回调函数中读取消息内容。

    完整通信流程如下:

    LimoStatus.msg 定义自定义消息结构

    limo_status_pub 创建 LimoStatus 消息对象

    填充 vehicle_state、control_mode、battery_voltage 等状态数据

    通过 publish() 发布到 /limo_status 话题

    /limo_status 话题负责传输 LimoStatus 消息

    limo_status_sub 订阅 /limo_status 话题

    status_callback() 回调函数接收消息

    解析并打印 LIMO 小车状态数据


    六、配置代码包 limo_learning

    6.1 limo_learning 的 package.xml

    因为 limo_learning 中使用了:

    rclcpp // ROS2 官方提供的 C++ 客户端库
    limo_msgs // 自己创建的 自定义接口包

    所以 limo_learning/package.xml 中需要添加:

    <depend>rclcpp</depend>
    <depend>limo_msgs</depend>

    完整示例:

    <?xml version="1.0"?>
    <package format="3">
    <name>limo_learning</name>
    <version>0.0.0</version>
    <description>ROS2 C++ practice package for LIMO robot</description>
    <maintainer email="agilex@todo.todo">agilex</maintainer>
    <license>Apache-2.0</license>

    <buildtool_depend>ament_cmake</buildtool_depend>

    <depend>rclcpp</depend>
    <depend>limo_msgs</depend>

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

    其中:

    依赖 作用
    rclcpp ROS2 C++ 节点基础库
    limo_msgs 使用自定义 LimoStatus 消息

    6.2 limo_learning 的 CMakeLists.txt

    limo_learning/CMakeLists.txt 可以这样配置:

    cmake_minimum_required(VERSION 3.8)
    project(limo_learning)

    if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
    add_compile_options(-Wall -Wextra -Wpedantic)
    endif()

    find_package(ament_cmake REQUIRED)
    find_package(rclcpp REQUIRED)
    find_package(limo_msgs REQUIRED)

    add_executable(limo_status_pub src/limo_status_pub.cpp)
    ament_target_dependencies(limo_status_pub
    rclcpp
    limo_msgs
    )

    add_executable(limo_status_sub src/limo_status_sub.cpp)
    ament_target_dependencies(limo_status_sub
    rclcpp
    limo_msgs
    )

    install(TARGETS
    limo_status_pub
    limo_status_sub
    DESTINATION lib/${PROJECT_NAME}
    )

    ament_package()


    6.3 CMakeLists.txt 核心理解

    这里最重要的是四类语句:

    find_package
    add_executable
    ament_target_dependencies
    install

    可以这样理解:

  • find_package:找到依赖包
  • add_executable:生成可执行程序
  • ament_target_dependencies:把依赖包给具体程序用
  • install:把程序放到 ROS2 能找到的位置
  • 例如:

    find_package(limo_msgs REQUIRED)

    表示找到 limo_msgs 这个接口包。

    add_executable(limo_status_pub src/limo_status_pub.cpp)

    表示把:

    src/limo_status_pub.cpp

    编译成:

    limo_status_pub

    这个可执行程序。

    ament_target_dependencies(limo_status_pub
    rclcpp
    limo_msgs
    )

    表示 limo_status_pub 这个程序需要使用:

    rclcpp
    limo_msgs

    新增订阅节点后,也需要写:

    add_executable(limo_status_sub src/limo_status_sub.cpp)
    ament_target_dependencies(limo_status_sub
    rclcpp
    limo_msgs
    )

    最后一定要把两个节点都放进 install:

    install(TARGETS
    limo_status_pub
    limo_status_sub
    DESTINATION lib/${PROJECT_NAME}
    )

    如果没有写 install,可能会出现:

    colcon build 编译成功
    但是 ros2 run 找不到节点


    七、编译与运行测试

    7.1 先编译自定义接口包

    回到工作空间根目录:

    cd ~/agilex_open_class_ws

    先编译自定义接口包:

    colcon build –packages-select limo_msgs
    source install/setup.bash

    为什么要先编译 limo_msgs?

    因为 limo_learning 中的 C++ 代码会引用:

    #include "limo_msgs/msg/limo_status.hpp"

    这个头文件不是手写出来的,而是 ROS2 根据:

    LimoStatus.msg

    自动生成的。

    所以必须先编译接口包。


    7.2 再编译代码包

    再编译代码包:

    colcon build –packages-select limo_learning
    source install/setup.bash

    也可以直接完整编译整个工作空间:

    colcon build
    source install/setup.bash

    但是在学习阶段,建议先分开编译:

    先编译 limo_msgs
    再编译 limo_learning

    这样更容易理解接口包和代码包之间的依赖关系。


    7.3 查看自定义 msg 是否生成成功

    查看自定义消息结构:

    ros2 interface show limo_msgs/msg/LimoStatus

    如果能正常显示:

    std_msgs/Header header
    uint8 vehicle_state
    uint8 control_mode
    float64 battery_voltage
    uint16 error_code
    uint8 motion_mode

    说明自定义 msg 已经生成成功。


    7.4 运行自定义 msg 发布节点

    打开第一个终端:

    cd ~/agilex_open_class_ws
    source install/setup.bash
    ros2 run limo_learning limo_status_pub

    如果节点正常启动,会看到类似日志:

    limo_status_pub node has started.
    Publish status: battery = 24.50, error_code = 0

    这个节点会不断向 /limo_status 发布小车状态消息。


    7.5 运行自定义 msg 订阅节点

    打开第二个终端:

    cd ~/agilex_open_class_ws
    source install/setup.bash
    ros2 run limo_learning limo_status_sub

    如果运行正常,订阅节点会打印类似信息:

    Receive status: frame_id = base_link, vehicle_state = 1, control_mode = 2, battery = 24.50, error_code = 0, motion_mode = 1

    这说明:

    limo_status_pub 已经成功发布自定义消息
    limo_status_sub 已经成功接收到自定义消息


    7.6 使用命令查看话题

    也可以使用命令查看 /limo_status:

    ros2 topic echo /limo_status

    查看话题信息:

    ros2 topic info /limo_status

    查看自定义消息结构:

    ros2 interface show limo_msgs/msg/LimoStatus

    这样就能从三个角度验证自定义 msg 是否正常:

    ros2 interface show:看接口结构是否生成
    ros2 topic info:看话题发布订阅关系
    ros2 topic echo:看话题实际数据


    八、常见问题总结

    8.1 找不到 limo_msgs/msg/limo_status.hpp

    如果编译时报错:

    limo_msgs/msg/limo_status.hpp: No such file or directory

    常见原因包括:

    limo_msgs 没有编译
    没有 source install/setup.bash
    limo_learning 没有依赖 limo_msgs
    CMakeLists.txt 中没有 find_package(limo_msgs REQUIRED)
    ament_target_dependencies 中没有添加 limo_msgs

    解决方法:

    colcon build –packages-select limo_msgs
    source install/setup.bash

    colcon build –packages-select limo_learning
    source install/setup.bash


    8.2 修改 LimoStatus.msg 后代码不生效

    如果修改了:

    LimoStatus.msg

    一定要重新编译接口包:

    colcon build –packages-select limo_msgs
    source install/setup.bash

    如果 limo_learning 依赖了 limo_msgs,建议再编译代码包:

    colcon build –packages-select limo_learning
    source install/setup.bash

    原因是:

    .msg 文件不是直接被 C++ 使用的
    ROS2 需要先根据 .msg 文件生成 C++ 头文件
    然后 C++ 代码才能 include 和使用


    8.3 ros2 interface show 找不到接口

    如果执行:

    ros2 interface show limo_msgs/msg/LimoStatus

    找不到接口,重点检查:

    limo_msgs 是否编译成功
    当前终端是否 source install/setup.bash
    LimoStatus.msg 路径是否正确
    CMakeLists.txt 是否写了 rosidl_generate_interfaces
    package.xml 是否写了 member_of_group

    尤其是 package.xml 中不要漏掉:

    <member_of_group>rosidl_interface_packages</member_of_group>


    8.4 ros2 run 找不到节点

    如果运行:

    ros2 run limo_learning limo_status_pub

    或者:

    ros2 run limo_learning limo_status_sub

    提示找不到节点,重点检查:

    limo_learning 是否编译成功
    是否 source install/setup.bash
    CMakeLists.txt 中是否写了 add_executable
    CMakeLists.txt 中是否写了 install
    ros2 run 后面的可执行文件名是否写对

    对应的 CMakeLists.txt 中必须有:

    add_executable(limo_status_pub src/limo_status_pub.cpp)
    add_executable(limo_status_sub src/limo_status_sub.cpp)

    并且 install 中必须包含:

    install(TARGETS
    limo_status_pub
    limo_status_sub
    DESTINATION lib/${PROJECT_NAME}
    )


    8.5 订阅节点没有任何输出

    如果运行:

    ros2 run limo_learning limo_status_sub

    后没有任何输出,可能原因包括:

    发布节点 limo_status_pub 没有启动
    发布和订阅的话题名不一致
    发布和订阅的消息类型不一致
    当前终端没有 source install/setup.bash

    重点检查发布端和订阅端的话题名是否完全一致。

    发布端是:

    "/limo_status"

    订阅端也必须是:

    "/limo_status"


    8.6 回调函数没有执行

    如果订阅节点启动了,但回调函数没有执行,常见原因是没有发布者,或者发布者话题名不一致。

    可以执行:

    ros2 topic info /limo_status

    正常情况下,应该能看到类似信息:

    Publisher count: 1
    Subscription count: 1

    如果 Publisher count 是 0,说明没有发布者。

    如果 Subscription count 是 0,说明没有订阅者。


    九、本章总结

    本篇是在上一篇 Topic 官方 msg 实战基础上的进一步扩展。

    上一篇主要讲:

    Topic + 官方 msg
    geometry_msgs/msg/Twist
    sensor_msgs/msg/LaserScan

    本篇主要讲:

    Topic + 自定义 msg
    limo_msgs/msg/LimoStatus

    本章实现了两个核心节点:

    limo_status_pub:发布自定义小车状态消息
    limo_status_sub:订阅自定义小车状态消息

    本章最重要的理解是:

    Topic 使用 msg
    msg 可以是官方标准 msg,也可以是自定义 msg

    自定义 msg 开发中最容易忘记的是:

  • 接口包要单独配置
  • 接口包要先编译
  • 接口修改后要重新编译
  • 代码包要依赖接口包
  • 编译后要 source install/setup.bash
  • 发布节点和订阅节点都要写进 install
  • 到这里,ROS2 C++ 中自定义 msg 的完整使用流程就跑通了。

    下一篇将继续讲解:

    ROS2 C++ 小车控制完整实战(三):自定义 srv 服务通信

    也就是通过 Service Client 和 Service Server,实现“一次请求、一次响应”的小车控制流程。

    赞(0)
    未经允许不得转载:171主机测评 » ROS2 C++ 小车控制完整实战(二):自定义 msg 消息发布与订阅保姆级教程
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址