简介:这是面向ROS2初学者的消息自定义与通信实战包,基于C++与Python双语言讲解,重点解决标准消息类型无法满足定制需求时的开发问题。学习后能够独立创建消息包、定义.msg文件,并写出可运行的发布者与订阅者节点。压缩包共645个文件,约3.43MB,主要包含cmake构建脚本、cpp与hpp源码、msg消息定义文件、Python接口文件以及colcon构建产物,便于对照整个编译安装流程进行复盘,目录结构完整反映从包创建到运行的完整链路。当前已有472人学习下载。适合正在学习ROS2基础通信机制、准备搭建自定义消息通信模块,或在工程中需要定制复杂数据结构的开发者离线参考。通过研读包内代码和配置文件,可以快速掌握消息代码生成、节点编写、车订阅关系配置等关键环节,并借此理解构建工具的目录组织方式,为后续接触QoS设置、多线程并发等高级主题打下基础。
1. 先搞懂一件事:ROS2自定义消息到底是什么
刚把ROS2装好的那几天,满屏的话题、节点、编译日志,很容易让人发懵。网上搜《ROS2自定义消息》,要么一句话带过,要么直接甩一堆源码,跑不跑得通全看运气。其实缩写写成RSO2也好,ROS2也好,都是同一个刚上手就被绕晕的概念。说穿了,自定义消息就是你自己规定的一份「数据格式约定」。机器人世界里,一个节点要把数据递给另一个节点,双方必须认同一张「单子」:里面叫什么名、是什么类型、按什么顺序,这就是消息。这篇不碰底层原理,我会带你走一条能跑通的路——建一个消息包、定义一条消息、写一个发布者一个订阅者,二十分钟看结果。适合刚装好ROS2、还没敢碰自定义消息的人。
2. 为什么不能一直用内置消息:先看清消息的底层逻辑
2.1 消息的本质:一封信,一个双方都认的约定
在ROS2里,节点与节点之间靠话题通信。一个节点往话题里发布数据,另一个节点订阅话题收数据。搬运的东西,就是消息。为了把它说明白,我用快递单来打比方。
假设你是深圳仓库的发货员,我在北京收货。我们之间能顺利交接,靠的是一张格式固定的单子:收件人、电话、地址、物品名。你不能随便拿张纸写一句「我的货很重」,那样我拿到也不知道该记什么。ROS2里的消息就是那张单子,发布者是发货员,订阅者是我,话题是那条配送路线,消息格式就是单子上的栏目。
那消息到底长什么样?在ROS2里,每一条消息都定义在一个以.msg结尾的文件里。文件里每一行是一个字段,写的是「类型 + 字段名」。比如你定义一个Student.msg,里面写:
string name int32 score float32 height这段文本表达的意思是:凡是使用这条消息的节点,发送时必须带三个字段,name是文本,score是整数,height是小数。接收方也知道按这个顺序取,不会乱。这就是「约定」。
2.2 三个内置消息搞不定的场景
你可能会问:ROS2不是自带一堆消息吗,像std_msgs/msg/String、std_msgs/msg/Int32,直接拿来用不行吗?单次传输一个数字或一行文本时,确实够用。但真实的机器人代码里,你要传的往往是一组数据。
举一个最常见的例子:一个学生体测系统,要让发布者传「姓名、分数、身高、测试序号」四个字段。用内置的String消息,只能把所有东西拼成一行字符串去传:"张三,88,1.75,0"。接收方拿到之后,自己按逗号拆分,再手动转换类型。字段一多、格式一改,两边的代码全得跟着改,非常容易翻车。
第二个场景更像实际工程:传感器节点要上报一条带时间戳和可信标志的数据,比如设备ID、当前时间、温度、是否有效。这种「四元组」用内置消息同样拼不出来——你不可能找到一个现成的Float32Array还附带设备ID和布尔标志。
第三个场景是代码阅读性。你想让团队里的人看到msg.temperature、msg.device_id就觉得自然,而不是人人都去记「下标0是温度、下标1是设备ID」。自定义消息直接把这些语义固化成了代码里的字段名,别人接手代码时,不用猜。
2.3 自定义消息的完整链路:四步走
自定义消息到底怎么生效?很多人卡住,是因为没在脑子里建起整条链路。我按顺序给你捋一遍。
第一步,写.msg文件。你只是用文本定义一个数据结构,这一步不写任何程序逻辑。第二步,ROS2的生成器会把这份文本翻译成多种编程语言可以import的代码,比如C++的类、Python的类。第三步,用colcon编译整个功能包,让翻译后的代码进入install目录。第四步,业务节点里import这条消息,像使用内置消息一样用它。
明确一个很容易绕晕的点:消息要放在「接口包」里,而不是和你的节点代码混在一起。接口包专门放.msg、.srv这类定义文件;节点包负责写逻辑。你可以把接口包理解为「字典」,节点包是「文章」,字典先出版,文章才能写得更顺畅。
还有一个入门就该建立的意识:定义消息等于在定一个公共契约。你以后改了一个字段名,所有依赖它的包都得跟着重新编译,否则接收方还是按老字段名去解析,运行时就报错。这也是为什么很多老手建议,消息字段名设计要一次想好,别图快。
3. 第一步怎么迈:建一个消息包,定义第一条自定义消息
3.1 建包之前:先准备好一个干净的工作空间
入门建议先从「消息定义」下手,不要一上来就写节点代码。我第一次学的时候,把发布者、订阅者、消息文件全塞进同一个包,编译时一堆依赖互相纠缠,花了半小时才分清是哪一步出了问题。正确姿势是:先建一个只放接口的包。
启动一个终端,进入你的工作空间根目录。如果你还没有工作空间,就按下面这个最基础的方式建一个:
mkdir -p ~/ros2_ws/src cd ~/ros2_ws/src这里ros2_ws是工作空间名,可以换成任意你喜欢的名字。src目录专门放功能包。注意:在工作空间里,第一次按这套命令走之前,先确认一下ros2命令本身能正常执行,输入ros2 --help不报错再往下走。
3.2 创建消息包并编写msg文件
在src目录下,用ROS2自带的命令新建一个接口包:
ros2 pkg create --build-type ament_cmake tutorial_interfaces cd tutorial_interfaces mkdir msg包名tutorial_interfaces是我随便起的,意思是「教程用的接口包」。--build-type ament_cmake表示用CMake构建,接口包通常都用这个类型。接着我们用mkdir建一个msg目录来放消息定义文件。
下面是第一条自定义消息。在msg目录下新建文件Student.msg,把下面内容写进去:
string name int32 score float32 height int64 timestamp这份文件定义了五个字段:name是文本,score是整数,height是单精度小数,timestamp是长整数。这里的写法有严格约定:一行只能写一个字段,格式是「类型 + 空格 + 字段名」,字段名小写开头,多个字段之间不要用逗号或分号,很多新手第一次写会下意识加分号,结果编译报错,属于最常见的坑之一。
3.3 补上编译配置:package.xml与CMakeLists.txt
.msg文件本身只是文本,ROS2要把它变成可被代码import的模块,需要在package.xml和CMakeLists.txt里登记。这也是新手最常漏的一步。
打开package.xml,在<package>标签内加入三行内容:
<build_depend>rosidl_default_generators</build_depend> <exec_depend>rosidl_default_runtime</exec_depend> <member_of_group>rosidl_interface_packages</member_of_group>第一行声明编译时需要消息生成器,第二行是运行时的依赖,第三行告诉ROS2这个包属于接口包群体。写完之后保存。
再打开CMakeLists.txt,在已有的find_package代码附近,确认包含以下两行:
find_package(rosidl_default_generators REQUIRED) rosidl_generate_interfaces(${PROJECT_NAME} "msg/Student.msg" )第一句是找到生成器库,第二句是请它处理Student.msg。注意这里传的是相对路径,从msg/开头写,别写成/绝对路径/msg/Student.msg,否则编译时会直接找不到文件。
3.4 编译消息包并验证结果
回到工作空间根目录,执行编译:
cd ~/ros2_ws colcon build --packages-select tutorial_interfaces--packages-select只编译指定的包,比全量编译消息快很多。编译结束后,在同一个终端执行一次:
source install/setup.bash ros2 interface show tutorial_interfaces/msg/Student如果一切正常,终端会打印出你自己定义的那四个字段,和内置消息输出风格一致。看到这个输出,说明消息包编写成功。
提示:
source install/setup.bash这条命令只对当前终端生效,新开终端必须重新source一次,否则ros2 interface show报「找不到接口」很常见。
4. 把它用起来:编一个发布订阅小例子
4.1 新建一个节点包,把消息接进来
消息定义好了,就要让代码真正用起来。这里用Python来写,原因很简单:对零基础的人,Python能看到最少的模板代码,注意力不会被C++的宏和类型声明带跑。
同样在src目录下新建一个节点包:
cd ~/ros2_ws/src ros2 pkg create --build-type ament_python student_demo包名student_demo负责写业务逻辑。建立完成后,进入student_demo/student_demo目录,你会看到一个Python包的目录结构。接下来新建两个文件:publisher.py和subscriber.py。
4.2 发布者节点:定时发出消息
在student_demo/publisher.py中写入:
import rclpy from rclpy.node import Node from tutorial_interfaces.msg import Student class StudentPublisher(Node): def __init__(self): super().__init__("student_publisher") # 创建发布者:消息类型是Student,话题名是student_info,队列长度10 self.publisher = self.create_publisher(Student, "student_info", 10) # 每1秒触发一次回调 self.timer = self.create_timer(1.0, self.timer_callback) self.seq = 0 def timer_callback(self): msg = Student() msg.name = "zhangsan" msg.score = 88 msg.height = 1.75 msg.timestamp = self.seq self.publisher.publish(msg) self.get_logger().info(f"发布了: {msg.name}, {msg.score}, {msg.height}, {msg.timestamp}") self.seq += 1 def main(args=None): rclpy.init(args=args) node = StudentPublisher() rclpy.spin(node) node.destroy_node() rclpy.shutdown() if __name__ == "__main__": main()这里有几个关键点。from tutorial_interfaces.msg import Student这句说明消息包在Python里是从tutorial_interfaces.msg这个模块导入的,格式固定是「包名.msg.消息名」。create_publisher第一个参数是消息类型,第二个是话题名,第三个是队列长度10,表示消息缓存的最多条数。create_timer第一个参数是秒数,第二是回调函数名。在回调里给msg的四个字段依次赋值,赋值时字段名必须与.msg文件里一致,多一个少一个都会报错。
4.3 订阅者节点:收到就打印
在student_demo/subscriber.py中写入:
import rclpy from rclpy.node import Node from tutorial_interfaces.msg import Student class StudentSubscriber(Node): def __init__(self): super().__init__("student_subscriber") # 创建订阅者:订阅student_info话题,收到消息后调用回调 self.subscription = self.create_subscription( Student, "student_info", self.listener_callback, 10 ) def listener_callback(self, msg): self.get_logger().info( f"收到: {msg.name}, 分数{msg.score}, 身高{msg.height}, 序号{msg.timestamp}" ) def main(args=None): rclpy.init(args=args) node = StudentSubscriber() rclpy.spin(node) node.destroy_node() rclpy.shutdown() if __name__ == "__main__": main()订阅者的代码更短。create_subscription的四个参数分别是消息类型、话题名、回调函数、队列长度。这里可以对照看一下:发布者和订阅者必须使用完全相同的消息类型Student,且话题字符串"student_info"也一致,才能对上话。只要任何一个字段类型写错,运行时会收到类型不匹配的警告。
还要去setup.py里登记这两个脚本为可执行入口。在entry_points的console_scripts下加两行:
entry_points={ "console_scripts": [ "student_publisher = student_demo.publisher:main", "student_subscriber = student_demo.subscriber:main", ], },4.4 编译运行:在终端观察消息流动
回到工作空间根目录,先编译整个节点包:
cd ~/ros2_ws colcon build --packages-select student_demo source install/setup.bash然后开两个终端,两个终端都先执行一次source install/setup.bash。在第一个终端运行:
ros2 run student_demo student_publisher在第二个终端运行:
ros2 run student_demo student_subscriber这时订阅者终端会以一秒一次的频率打印出消息内容。我的血泪经验是:如果订阅者啥都没打印,不要先怀疑代码,先开第三个终端执行ros2 topic list,看话题student_info是否存在,再用ros2 topic echo /student_info直接看消息能不能被捕获。这样就能快速判断是发布者没发出来,还是订阅者没接上。
5. 排查手册:新手最容易踩的五个自定义消息问题
5.1 终端没source导致找不到接口
现象:ros2 interface show tutorial_interfaces/msg/Student报找不到包。原因:新开终端没有执行source install/setup.bash,环境变量里没有注册自定义消息包。解决:在终端手动执行source install/setup.bash,然后再执行一次验证命令,通常立刻恢复。
5.2 大小写和命名写错
现象:编译成功,但运行时提示消息类型不存在,或者import报ModuleNotFoundError。原因:文件名、消息名、Python导入名三者只要差一个字母大小写就对不上。比如消息文件叫student.msg,但定义的是Student,ROS2生成代码时会把它变成首字母大写的类名,文件与类名不一致就会错位。解决:Msg文件命名首字母大写,整个文件内容里不要出现与文件名不同的类名。
5.3 package.xml漏掉依赖
现象:编译接口包时报Could not find a package configuration file,或者rosidl_generate_interfaces找不到命令。原因:package.xml里没有声明rosidl_default_generators依赖。解决:按前面3.3里的三行内容补全,重新执行colcon build --packages-select tutorial_interfaces。
5.4 改了消息内容但旧代码还按旧字段解析
现象:消息包明明加了新字段,订阅者打印的字段值全不对或直接错位。原因:只重新编译了接口包,没有重新编译依赖它的student_demo包。解决:执行colcon build --packages-up-to student_demo,这个参数会从接口包开始,把依赖链上新改的包全部重新编译,避免缓存旧代码。
5.5 发布了却收不到话题
现象:发布者日志正常打印「发布了」,订阅者没有任何输出。原因:话题名不一致或队列长度概念混淆,前者是字符串拼接错误,后者不至于导致完全收不到。用ros2 topic list能看到话题但echo无输出,多半是发布频率太快,订阅者才启动时错过了消息,而队列长度只有10,消息被冲掉了。解决:先把发布者定时器调成2秒一帧,再启动订阅者,观察是否稳定收到。稳定后再改回1秒。
6. 下一步怎么学:从消息走向服务、动作与工具箱
自定义消息跑通之后,你的下一步有三个方向值得选。第一个方向是学service,也就是ROS2的服务通信,它对应.srv文件。消息解决的是「持续广播」,服务解决的是「一问一答」:请求方发一段数据过去,应答方处理后返回结果,适合像「给我当前楼层的坐标」这类临时询问。.srv的写法和.msg很像,区别是中间用一条---分隔请求和响应字段。
第二个方向是action,动作通信,涉及.action文件。它适合长任务,比如「让机械臂从A点移动到B点」,任务执行过程中要持续反馈进度,又可能被中途取消,action专门为这类场景设计。第三个方向是把学过的命令组合成固定习惯,用到自己的项目里,形成流程:改消息字段→编译接口包→source环境→编译节点包→运行验证,这五步走完再回看代码。
做一个既简单又能加深理解的「随手练习」:自行在Student.msg里加一个bool passed字段,表示是否及格,并在发布者和订阅者里分别加上赋值和打印。改完按上面流程走一遍,你会发现所有步骤连成一气,不再玄学。
从那以后,我每次给消息加字段,都要强制自己把「编译→source→echo」三连走一遍,这个习惯帮我省下了大量定位问题的时间。如果你刚跑通第一个例子,我真心建议你也把这三步当成肌肉记忆。希望帮到你。
本文还有配套的精品资源,点击获取