目录
[1.1 Topic 和 msg 的关系](#1.1 Topic 和 msg 的关系)
[1.2 本篇要实现什么](#1.2 本篇要实现什么)
[1.3 本篇涉及的功能包](#1.3 本篇涉及的功能包)
[二、自定义 msg 接口文件](#二、自定义 msg 接口文件)
[2.1 为什么需要自定义 LimoStatus.msg](#2.1 为什么需要自定义 LimoStatus.msg)
[2.2 LimoStatus.msg 文件位置](#2.2 LimoStatus.msg 文件位置)
[2.3 LimoStatus.msg 文件内容](#2.3 LimoStatus.msg 文件内容)
[2.4 字段含义说明](#2.4 字段含义说明)
[2.5 std_msgs/Header header 是什么](#2.5 std_msgs/Header header 是什么)
[2.6 msg 文件注意事项](#2.6 msg 文件注意事项)
(2)如果使用了其它功能包里的类型,需要在配置文件中声明依赖。
[(3)修改 .msg 文件后必须重新编译。](#(3)修改 .msg 文件后必须重新编译。)
[三、配置自定义接口包 limo_msgs](#三、配置自定义接口包 limo_msgs)
[3.1 rosidl_generate_interfaces 是什么](#3.1 rosidl_generate_interfaces 是什么)
[3.2 limo_msgs 的 CMakeLists.txt](#3.2 limo_msgs 的 CMakeLists.txt)
[3.3 CMakeLists.txt 核心解释](#3.3 CMakeLists.txt 核心解释)
[3.4 limo_msgs 的 package.xml](#3.4 limo_msgs 的 package.xml)
[3.5 package.xml 核心解释](#3.5 package.xml 核心解释)
[四、C++ 发布自定义 msg 状态消息](#四、C++ 发布自定义 msg 状态消息)
[4.1 自定义 msg 发布节点作用](#4.1 自定义 msg 发布节点作用)
[4.2 自定义 msg 发布完整代码](#4.2 自定义 msg 发布完整代码)
[4.3 代码核心名字和对象总结](#4.3 代码核心名字和对象总结)
[(2) "limo_status_pub":节点名称](#(2) "limo_status_pub":节点名称)
[4.4 发布端核心代码解释](#4.4 发布端核心代码解释)
[(4)回调函数 timer_callback 的作用](#(4)回调函数 timer_callback 的作用)
[五、C++ 订阅自定义 msg 状态消息](#五、C++ 订阅自定义 msg 状态消息)
[5.1 为什么还要写 Subscriber](#5.1 为什么还要写 Subscriber)
[5.2 自定义 msg 订阅完整代码](#5.2 自定义 msg 订阅完整代码)
[5.3 订阅器核心代码解释](#5.3 订阅器核心代码解释)
[(2) 回调函数 status_callback 的作用](#(2) 回调函数 status_callback 的作用)
[5.4 Publisher 和 Subscriber 的关系](#5.4 Publisher 和 Subscriber 的关系)
[六、配置代码包 limo_learning](#六、配置代码包 limo_learning)
[6.1 limo_learning 的 package.xml](#6.1 limo_learning 的 package.xml)
[6.2 limo_learning 的 CMakeLists.txt](#6.2 limo_learning 的 CMakeLists.txt)
[6.3 CMakeLists.txt 核心理解](#6.3 CMakeLists.txt 核心理解)
[7.1 先编译自定义接口包](#7.1 先编译自定义接口包)
[7.2 再编译代码包](#7.2 再编译代码包)
[7.3 查看自定义 msg 是否生成成功](#7.3 查看自定义 msg 是否生成成功)
[7.4 运行自定义 msg 发布节点](#7.4 运行自定义 msg 发布节点)
[7.5 运行自定义 msg 订阅节点](#7.5 运行自定义 msg 订阅节点)
[7.6 使用命令查看话题](#7.6 使用命令查看话题)
[8.1 找不到 limo_msgs/msg/limo_status.hpp](#8.1 找不到 limo_msgs/msg/limo_status.hpp)
[8.2 修改 LimoStatus.msg 后代码不生效](#8.2 修改 LimoStatus.msg 后代码不生效)
[8.3 ros2 interface show 找不到接口](#8.3 ros2 interface show 找不到接口)
[8.4 ros2 run 找不到节点](#8.4 ros2 run 找不到节点)
[8.5 订阅节点没有任何输出](#8.5 订阅节点没有任何输出)
[8.6 回调函数没有执行](#8.6 回调函数没有执行)
摘要
上一篇文章主要讲解了 ROS2 C++ 中最基础的 Topic 话题通信,
- 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 可以这样写:
cpp
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
<?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
```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)核心总结:
cpp
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 自定义消息
- 在回调函数中读取电池电压、错误码、车辆状态等字段
- 打印接收到的小车状态信息
整体流程如下:
cpp
limo_status_pub 发布 /limo_status
↓
/limo_status 话题传输 LimoStatus 消息
↓
limo_status_sub 订阅 /limo_status
↓
回调函数接收并解析状态数据
5.2 自定义 msg 订阅完整代码
文件路径:
limo_learning/src/limo_status_sub.cpp
代码如下:
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)创建订阅器的代码如下:
cpp
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 的作用
订阅回调函数如下:
cpp
void status_callback(const limo_msgs::msg::LimoStatus::SharedPtr msg)
这个回调函数接收的是一个
LimoStatus类型的消息指针。
这里的 msg 只是形参名字,也就是"接收到的消息对象叫什么名字"。
可以任意修改:
cpp
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
并在回调函数中读取消息内容。
完整通信流程如下:
bash
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 中使用了:
XML
rclcpp // ROS2 官方提供的 C++ 客户端库
limo_msgs // 自己创建的 自定义接口包
所以 limo_learning/package.xml 中需要添加:
<depend>rclcpp</depend>
<depend>limo_msgs</depend>
完整示例:
XML
<?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 可以这样配置:
cpp
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,实现"一次请求、一次响应"的小车控制流程。