☰
ROS2自定义消息指南:msg文件设计、编译与发布订阅示例
2026/10/21 4:30:51 网站建设 项目流程

简介:ROS2(Robot Operating System 2)中的自定义消息机制是节点间结构化数据通信的基础。这份资源以一个完整可编译的小型工作空间为示例,面向刚入门ROS2的机器人开发爱好者,系统呈现了从创建消息包、编写MyMessage.msg,到使用colcon完成构建与安装,再到编写发布器和订阅器节点调用自定义消息的全部流程。压缩包共645个文件,大小约3.43MB,内部既包含CMakeLists.txt、package.xml、.msg等核心工程文件,也包含cmake、make、hpp/cpp、py、so等构建脚本、源码及动态库,目录层级与构建产物保存完整,便于逐步对照和排查问题。该资源已有472人学习下载,热度不俗。借助其中代码与配置,读者能够快速掌握rclcpp::Publisher和rclcpp::Subscription的典型用法,理解消息生成与引用路径,并学会在真实工程中组织自定义消息包,为后续复杂机器人通信开发打下扎实基础。

1. 先从最让人发怵的问题说起:ROS2的“消息”到底是什么

我第一次接触 ROS2 时,文档满屏都是“节点”“话题”“消息”,每个字都认识,连在一起就发怵。后来有人一句话点醒我:机器人各个模块要互相沟通,先把要沟通的数据打包成一张“表”,这张表就是消息。字段叫什么、是什么类型都约定好,再用发布与订阅的方式传出去。

ROS2 里的自定义消息,就是允许你自行设计这张表。内置消息覆盖了坐标、图像、激光等常见类型,但在同时传编号、温度、电量、坐标和使能状态这样的场景,内置类型就不够用了。很多人搜 ROS2 零基础资料时,也会看到 RSO2 这个拼法,其实指的是同一件事。这篇文章从零开始,告诉你这到底是什么、为什么要学,并带你跑通第一个自定义消息的例子。

2. 为什么要学自定义消息:一个话题不够用时怎么办

2.1 内置消息够用,为什么还要自己造

ROS2 自带一套标准消息集:std_msgs 管基本标量,geometry_msgs 管坐标和姿态,sensor_msgs 管图像、激光雷达和 IMU 数据。这些消息是给机器人通用场景设计的,很多需求确实能覆盖。但等你真的开始做项目,一定会碰到“字段差一点”的尴尬。

举个例子。我想让一台巡检机器人每 100 毫秒对外发一条状态,里面同时包含设备编号、x 坐标、y 坐标、温度、电量百分比和使能状态。用内置消息怎么拼?要么把所有字段拆成五六个话题分开发,订阅方同时订阅五六个话题,时间戳很难对齐;要么挪用现成消息,把温度塞进某个没有语义的 float 字段里。代码跑起来没毛病,但换一个人来看,根本分不清哪个 float 是温度、哪个是坐标。

自定义消息解决的正是这类问题。你按自己的业务场景定义字段、字段类型和字段名,消息从发布端传到订阅端,类型会被严格校验。别把“自定义”想成什么高深操作,它只是填写一张符合你需求的表格。字段粒度完全由你把控,这是 ROS2 开发里绕不开的基本功。

2.2 把消息想成快递单:.msg 文件就是在设计一张表格

我学消息时,最有用的一个类比是快递单。寄包裹要填收件人、电话、地址、重量,每个格子都有明确格式:电话不能写字母,重量不能写中文。填完之后,收件方不需要知道你寄的到底是一本书还是一个螺丝,他只要按同一个单子的字段去取,就能找到对应信息。

ROS2 的 .msg 文件也是这个设计思路。文件里每一行写一种数据类型和一个字段名,最终生成的代码按同样规则解析。我们后面要用的例子 RobotStatus.msg,大概长这样:

int32 robot_id float32 x float32 y float32 temperature int32 battery_percent bool enable

这其实就是六句话:“机器人编号是整数”“坐标 x 是浮点数”“坐标 y 是浮点数”“温度是浮点数”“电量是整数”“使能状态是布尔”。字段与字段之间用换行分隔,类型必须是小写的 ROS2 内建类型,常见的有 bool、int32、float32、float64、string 等。字段名建议用蛇形命名,也就是小写字母加下划线。

有了这张表之后,消息从 A 节点传到 B 节点,会先被序列化成二进制字节流,B 节点再按同一张表反序列化回来。真正保证两边能对上话的,就是这个 .msg 里的字段定义。两份 .msg 只要有一个字段类型不一样,运行时会报“类型不匹配”,这一点到避坑章节再展开。

2.3 一条自定义消息的完整生命周期

先对整个流程有个整体认识,后面操作起来就不会像无头苍蝇。

第一步是定义。在功能包的 msg 目录下,用纯文本写一个 .msg 文件,文件名一般用大驼峰风格,比如 RobotStatus.msg,里面只写字段列表。这一步相当于设计快递单的格式。

第二步是配置。修改 CMakeLists.txt 和 package.xml,把“这个包里需要生成消息接口”的信息告诉构建系统。CMakeLists.txt 里要调用 rosidl_generate_interfaces,指定包名与 .msg 文件路径;package.xml 里要声明依赖。这一步如果漏做,构建时往往不会报错,但消息就是不会生成,属于入门阶段最典型的隐形故障。

第三步是生成。运行 colcon build 后,ROS2 的 rosidl 工具链会读取 .msg 文件,为 C++、Python 等语言各生成一套对应的接口代码。你在 C++ 里 include 头文件,或者在 Python 里写from my_msgs.msg import RobotStatus,用的都是这套生成结果。

第四步才是使用。在同一个工作区里写发布者或订阅者节点,把自定义消息发到指定话题。对新手来说,最重要的是别手动去碰生成的那些代码,那是编译器分内的事;你要维护的只有 .msg 文件和两个配置文件。

3. 第一步怎么迈:搭好环境并创建你的第一个功能包

3.1 版本选择:新手为什么建议用 Humble

ROS2 的不同发行版对应不同的 Ubuntu 版本。Foxy 对应 Ubuntu 20.04,Humble 对应 Ubuntu 22.04,Jazzy 对应 Ubuntu 24.04。作为零基础,我不建议追新版本,直接选“Ubuntu 22.04 + ROS2 Humble”这个组合就好。原因有三个。

第一,Humble 是长期支持版本,官方维护周期长,不会半年就让你迁移。第二,大多数教程、开源示例、视频讲解都基于 Humble 写的,你敲的命令能和资料对得上,不至于在版本差异上翻车。第三,它的工具链,比如 rviz2、导航栈、机器人模拟器,生态兼容性最好,遇到问题最容易搜到答案。

如果手头不是 Ubuntu,也不用慌。Windows 和 macOS 虽然能装,但很多传感器驱动和可视化工具在 Linux 上更稳定。我这里只推荐两条路线:要么装双系统,要么用虚拟机,要么用带 ROS2 预装环境的 Docker。我第一次用的是 Windows 双系统,后来换成虚拟机反而更省心,因为宿主系统的文件损坏不会波及开发环境。如果你完全没碰过 Linux,建议先用虚拟机熟悉 cd、mkdir、ls 这些命令,再进入 ROS2,会顺畅很多。

安装好桌面版之后,建议把 source 命令写进 ~/.bashrc,让每个新终端自动加载 ROS2 环境。验证是否装好,可以执行ros2 topic list,如果能看到一串话题名,就说明环境已经通。

3.2 创建工作区:代码、编译、安装三件事都归这里管

ROS2 的项目一般放在一个叫工作区的地方,最外层目录通常叫 ros2_ws,里面必须有个 src 子目录。你在 src 里放功能包,colcon 编译时读 src 下的每个包,把生成的头文件、库和可执行文件写到 install 目录,把日志写到 log 目录。很多新手忘记的是:每次编译后要把 install 目录下的 setup.bash 用 source 加载一次,否则新生成的东西对当前终端不可见。

创建命令非常简单:

mkdir -p ~/ros2_ws/src cd ~/ros2_ws/src

为什么一定要在 src 里建包?因为 colcon 只扫描工作区 src 目录下的内容。你把包随便放在别处,构建时发现不了,运行时会提示找不到包。工作区目录名本身不重要,重要的是 src 结构不能变,这就是 ROS2 的基本约定。

3.3 用 ros2 pkg create 生成功能包并认识目录结构

接下来创建第一个功能包。执行下面这条命令:

ros2 pkg create my_msgs --build-type ament_cmake

包名选 my_msgs,构建类型选 ament_cmake。这里解释一下:ROS2 功能包主要有两种构建类型,ament_cmake 和 ament_python。我们之后要在包内存放 .msg 消息文件,消息生成工具链基本都基于 CMake,所以用 ament_cmake 最合适。如果以后你要写纯 Python 节点,再用 ament_python 建另一个包就好。

生成后进入目录看结构:

cd ~/ros2_ws/src/my_msgs ls -R

你能看到 CMakeLists.txt、package.xml,以及 include、src 两个目录。CMakeLists.txt 控制编译过程,package.xml 声明包的依赖、版本和许可信息,include 放 C++ 头文件,src 放 C++ 源码。刚创建时这些目录基本是空的,没有可执行代码。

需要提醒一点:ROS2 的包名必须是小写字母加下划线,不能出现大写字母,也不建议用连字符。包名一旦写成大驼峰,后续很多工具会报错,趁早养成规范习惯。如果创建时包名写错了,直接用下面命令删掉重来:

cd ~/ros2_ws/src rm -rf my_msgs

目前工作区已经就绪,下一步就是亲手定义消息。

4. 完整走一遍:定义消息、编译并让话题跑起来

4.1 编写 RobotStatus.msg:把字段设计好

我们正式定义第一个自定义消息。假设场景是一个移动机器人,每 100 毫秒发布一条状态,包含编号、坐标、温度、电量和使能状态。先在功能包里创建 msg 目录:

mkdir -p ~/ros2_ws/src/my_msgs/msg cd ~/ros2_ws/src/my_msgs/msg

然后用你喜欢的文本编辑器新建 RobotStatus.msg,写入下面的字段:

int32 robot_id float32 x float32 y float32 temperature int32 battery_percent bool enable

写完之后,第一行int32 robot_id表示机器人编号是整数类型;第二三行float32 x和float32 y表示坐标带小数;第四行float32 temperature因为温度可能返回 36.5 这样的值,用浮点数;第五行int32 battery_percent用整数存百分比;第六行bool enable存是否使能。

这里再强调一次格式:每行先写类型,再写字段名,中间用空格隔开。类型必须是小写的内建类型名,字段名用下划线命名。如果你的业务里需要坐标点,也可以把 x、y 合并成一个嵌套消息,例如用内置的 geometry_msgs/msg/Point,但对第一个例子来说,从基础类型起步更直观。

4.2 修改 CMakeLists.txt 和 package.xml:开启消息生成

写完了 .msg,接下来要告诉构建系统“这个包需要生成消息接口”。打开 CMakeLists.txt,找到文件靠前的位置,把下面两行加到 find_package 区域:

find_package(rosidl_default_generators REQUIRED) find_package(std_msgs REQUIRED)

第一行是消息生成工具链的入口,少了它,后面定义 rosidl_generate_interfaces 时会报找不到函数。第二行是基础消息依赖,如果 .msg 里用了内置类型,或者后续准备嵌套标准消息,就需要它。

然后在文件靠后的位置,加上生成接口的声明:

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

这段的意思是:请根据 msg/RobotStatus.msg 生成各语言的接口代码,同时告诉编译系统依赖 std_msgs。如果以后新增了其他 .msg 文件,在这个括号里按相同格式继续加一行即可。

再打开 package.xml,在依赖部分补上三行:

<buildtool_depend>rosidl_default_generators</buildtool_depend> <depend>std_msgs</depend> <member_of_group>rosidl_interface_packages</member_of_group>

第一行声明构建工具依赖,第二行声明运行时依赖,第三行把这个包登记为“提供消息接口的包”。漏掉这些,构建时可能不报错,但消息就是网易生成,属于最典型的隐形坑。

4.3 编译消息并验收生成成功

配置完成后,回到工作区根目录,只编译 my_msgs 这一个包:

cd ~/ros2_ws colcon build --packages-select my_msgs

--packages-select的意思是只编译指定的包,避免把工作区内其他不相关的东西一起编译。第一次编译可能需要几十秒到一两分钟,只要最后看到Finished <<< my_msgs,没有Failed字样,就说明成功。

编译完成后,加载新的环境,然后验证消息是否真的注册了:

source install/setup.bash ros2 interface show my_msgs/msg/RobotStatus

如果屏幕输出正是你写的六行字段,说明消息定义、编译、注册整条链路全部打通。如果提示找不到接口,先检查是不是忘了 source,或者 CMakeLists.txt 里的函数名拼写有误。这一步相当于第一次打开了 ROS2 的幕后黑匣子,你会直观看到 .msg 到接口注册的闭环。

4.4 写一个发布者和订阅者节点:让消息真正跑起来

消息定义好了,现在写代码让它真正流动起来。这里先演示一个简单方式:在功能包的 scripts 目录下放两个 Python 脚本,直接运行。ROS2 对安装后的 Python 模块有完整路径支持,只要 source 了环境,脚本里就能正常 import。

先创建脚本目录:

mkdir -p ~/ros2_ws/src/my_msgs/scripts cd ~/ros2_ws/src/my_msgs/scripts

新建 publish_robot_status.py,内容如下:

import rclpy from rclpy.node import Node from my_msgs.msg import RobotStatus class RobotStatusPublisher(Node): def __init__(self): super().__init__('robot_status_publisher') # 创建发布者,话题名为 robot_status,队列长度 10 self.publisher_ = self.create_publisher(RobotStatus, 'robot_status', 10) # 每 0.1 秒触发一次定时器 self.timer_ = self.create_timer(0.1, self.on_timer) def on_timer(self): msg = RobotStatus() msg.robot_id = 1 msg.x = 1.5 msg.y = 3.2 msg.temperature = 36.5 msg.battery_percent = 88 msg.enable = True self.publisher_.publish(msg) self.get_logger().info('已发布一条自定义消息') def main(): rclpy.init() node = RobotStatusPublisher() try: rclpy.spin(node) except KeyboardInterrupt: pass finally: node.destroy_node() rclpy.shutdown() if __name__ == '__main__': main()

创建订阅者脚本 subscribe_robot_status.py:

import rclpy from rclpy.node import Node from my_msgs.msg import RobotStatus class RobotStatusSubscriber(Node): def __init__(self): super().__init__('robot_status_subscriber') # 订阅同一个话题 robot_status self.subscription_ = self.create_subscription( RobotStatus, 'robot_status', self.on_message, 10 ) def on_message(self, msg): self.get_logger().info( f'编号={msg.robot_id}, ' f'x={msg.x}, y={msg.y}, ' f'温度={msg.temperature}, ' f'电量={msg.battery_percent}%, ' f'使能={msg.enable}' ) def main(): rclpy.init() node = RobotStatusSubscriber() try: rclpy.spin(node) except KeyboardInterrupt: pass finally: node.destroy_node() rclpy.shutdown() if __name__ == '__main__': main()

打开第一个终端,加载环境并运行发布者:

cd ~/ros2_ws source install/setup.bash python3 src/my_msgs/scripts/publish_robot_status.py

再打开第二个终端,加载环境并运行订阅者:

cd ~/ros2_ws source install/setup.bash python3 src/my_msgs/scripts/subscribe_robot_status.py

订阅者终端会不断打印出编号、坐标、温度、电量和使能状态,说明自定义消息已经在话题上正常传递。你还可以在第三个终端运行ros2 topic echo /robot_status来直接观察话题里的原始数据流。

这里两个参数值得注意:create_publisher和create_subscription里的最后一个数字 10,是 QoS 队列深度,表示缓存最近 10 条消息。话题名robot_status由发布者和订阅者共同约定,必须一模一样,消息类型也必须完全一致,否则无法通信。

5. 新手避坑:自定义消息最常见的5个踩坑现场

5.1 编译通过,但运行时提示找不到消息

现象:colcon build 显示成功,执行ros2 interface show my_msgs/msg/RobotStatus却提示找不到接口,Python 脚本 import 时也报 ModuleNotFoundError。

原因:最常见的两种情况。一是编译之后没有 source 新的环境,当前终端还在用旧环境变量;二是包没有被放对位置,或者包名跟目录名不一致。底层逻辑是,生成的消息接口被安装到 install 目录,只有 source 了 install/setup.bash,系统才会知道。不 source 等于不告诉系统新生成的包存在。

解决:每次编译后执行source install/setup.bash。如果已经 source 过仍然找不到,检查包是否真的位于 ~/ros2_ws/src 下,再检查包名是否与 CMakeLists.txt 里的 project 名一致。两者必须完全一致,否则 ROS2 不认。

5.2 改了 .msg 却没重新编译就运行

现象:修改 .msg 里的字段,比如新增一个 float32 speed,然后直接运行节点,发现节点里访问不到 speed,或者打印出的是旧数据。

原因:ROS2 的消息接口代码是在编译阶段生成的,不是运行时从 .msg 动态读取的。你改了 .msg 文件,但 install 目录里的生成代码还是旧版本,自然访问不到新字段。这属于“改了文件但没生效”的典型情形。

解决:改完 .msg 后,必须重新执行colcon build --packages-select my_msgs,然后重新 source install/setup.bash。建议把“改消息 → 编译 → source”绑定成一个习惯动作,缺一不可。

5.3 CMakeLists.txt 没写生成函数,编译却正常

现象:ros2 interface show 一直提示找不到接口,但编译过程异常平静,没有任何报错。

原因:CMakeLists.txt 里没有调用 rosidl_generate_interfaces,也没有 find_package(rosidl_default_generators)。这种情况下,构建系统只是把普通代码编译了一遍,完全不知道需要生成消息接口,所以不报错,但也不会生成任何东西。这就是之前提到的“隐形失败”。

解决:在 CMakeLists.txt 里补充 find_package 依赖和 rosidl_generate_interfaces 声明,在 package.xml 里补上对应的 buildtool_depend、depend 和 member_of_group 标签,然后重新编译。验证方式仍然是 ros2 interface show。

5.4 Python 导入路径写错

现象:脚本运行时报 ModuleNotFoundError: No module named 'my_msgs',或者提示 my_msgs.msg 不存在。

原因:导入自定义消息的路径跟普通 Python 模块不一样。正确写法是from my_msgs.msg import RobotStatus,不是import my_msgs,也不是from my_msgs import RobotStatus。很多人把消息包当成普通包来导入,路径对不上,自然找不到。另外,包名大小写错误、文件名与类名不一致也会导致同样问题。

解决:统一使用from 包名.msg import 消息类名的格式。如果仍然报错,用python3 -c "from my_msgs.msg import RobotStatus"在安装了脚本同一环境里测试。确保 source 了 install/setup.bash,并且 .msg 生成的消息模块确实存在于 install 目录中。

5.5 发布者和订阅者消息类型对不上

现象:发布者正常运行,订阅者却没有打印任何内容,用ros2 topic info /robot_status查看,显示的消息类型跟预期不一致。

原因:发布者包里导入的消息类型和订阅者包里导入的类型不是同一个。比如一边写from my_msgs.msg import RobotStatus,另一边却用了内置类型,或者把包名打错,导致话题上的消息类型不同。ROS2 会按消息类型来匹配话题,类型不匹配时通信静默失败,不报错,也不显示数据。

解决:用ros2 topic info /robot_status -v查看发布者、订阅者数量和消息类型。如果类型不是 my_msgs/msg/RobotStatus,回到脚本里检查 import 语句,确保两个节点导入的类完全一致,然后重新运行。

6. 接下来学什么:从自定义消息走向真正的 ROS2 开发

现在你已经完成了一个最小闭环:定义消息、编译、发布、订阅。下一步不是急着学更多接口,而是把工程结构规范起来,同时扩展消息玩法。

第一件值得做的事,是把发布者和订阅者从消息包拆出来,单独建成一个节点包。比如创建 second 包,用ros2 pkg create my_nodes --build-type ament_python,然后在 setup.py 里声明依赖 my_msgs。这样消息包和节点包职责清晰,复用性更强。你还会顺带熟悉 ament_python 的包结构,这对以后写正式项目很重要。

第二件事,是尝试在消息里嵌套消息。回到第 2 章提过的例子,把 x、y 两个字段替换成geometry_msgs/msg/Point position。这时你会发现 .msg 里可以直接引入其他包的消息类型,前提是 .msg 中声明了对应依赖。这一步能帮你理解 ROS2 消息的复用机制:自定义消息不是从零造轮子,而是组合标准件。

第三件事,是学用 launch 文件。现在你开两个终端手动运行节点,一旦节点多了就会手忙脚乱。launch 文件可以用一段 XML 或 Python 配置,同时启动发布者、订阅者和其他工具。ROS2 的 launch 支持参数传递、条件启停,配合 rviz2 的可视化将会非常强大。

第四件事,是接触服务和动作的自定义接口。话题只解决“持续数据流”的问题,服务解决“请求响应”,动作解决“长时间任务”。它们的接口定义同样基于 .msg 字段设计思路,只是多了请求和响应的区分。你学会自定义消息之后,再去看服务和动作的 .srv、.action 文件,会发现结构几乎一样。

我从那次之后养成一个习惯:每次新建功能包,强制自己走一遍完整流程,创建包、检查 package.xml、配置 CMakeLists、编译、source、运行。以前我总嫌麻烦,直接跳过 source,结果浪费在“找不到包”这类问题上的时间,比按流程走多了好几倍。从那以后,再懒也会把 source 和重新编译放在一组命令里执行,少一步都不行。这套流程对你同样适用,坚持几次,ROS2 就从一个陌生的黑匣子变成你手里顺手的工具了。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询