2、自定义消息接口:创建.msg文件、编译消息接口、在Python中使用自定义消息
好,咱们接着聊。上一节我们把话题通信的基本流程跑通了,用的都是ROS2自带的标准消息类型,比如String、Int32这些。但在实际项目中,你很快就会发现——这些标准消息根本不够用。
举个例子。我在做地图数据传输时,需要传递一张栅格地图。地图里有宽度、高度、分辨率、障碍物数据……这么多信息,你总不能拆成五六个话题分开发吧?那不乱套了?
这时候就需要自定义消息接口。说白了,就是你自己定义一个数据结构,把一堆相关的数据打包成一个整体,一次性发出去。
核心思想:自定义消息 = 你自定义的数据结构。就像C语言里的struct,Python里的class。
2.1 创建.msg文件
先说说怎么创建这个文件。我个人习惯在功能包下建一个msg文件夹,专门放消息定义文件。文件名后缀是.msg。
比如我们要定义一个地图消息,叫MapData.msg。内容长这样:
# MapData.msg
# 这是一个自定义地图消息
int32 width # 地图宽度(像素)
int32 height # 地图高度(像素)
float32 resolution # 分辨率(米/像素)
int32[] data # 障碍物数据,0表示空闲,100表示障碍
string frame_id # 坐标系名称
嗯,这里要注意几点:
- 字段类型:支持int32、float32、string、bool等基础类型,也支持数组(比如int32[])
- 注释:用
#开头,跟Python一样 - 命名规范:文件名首字母大写,用驼峰命名法
小技巧:我建议每个消息文件都加上注释,说明每个字段的含义。你想想看,三个月后你自己回来看代码,没有注释真的会懵。
2.2 编译消息接口
文件创建好了,接下来要告诉ROS2:“嘿,我定义了一个新消息,帮我编译一下。”这一步很多人会忘,结果运行时找不到消息类型,报错报得一脸懵。
具体分三步走:
- 修改CMakeLists.txt:在
find_package里加上rosidl_default_generators,在rosidl_generate_interfaces里列出你的.msg文件 - 修改package.xml:添加依赖
rosidl_default_generators和builtin_interfaces - 编译:用
colcon build编译整个工作空间
CMakeLists.txt里关键部分长这样:
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/MapData.msg"
)
package.xml里加上:
<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
避坑指南:我曾经有一次忘了加member_of_group这一行,结果编译通过了,但运行时ROS2根本找不到我的消息接口。折腾了半小时才发现是这里漏了。所以,package.xml里的每一行都别省。
编译命令:
cd ~/ros2_ws
colcon build --packages-select 你的功能包名
source install/setup.bash
编译成功后,你可以用ros2 interface show命令验证一下:
ros2 interface show 你的功能包名/msg/MapData
如果能看到你定义的字段,那就说明编译成功了。
2.3 在Python中使用自定义消息
消息编译好了,怎么在Python代码里用呢?其实跟用标准消息一模一样,只是import的路径变了。
发布端示例:
import rclpy
from rclpy.node import Node
from 你的功能包名.msg import MapData
class MapPublisher(Node):
def __init__(self):
super().__init__('map_publisher')
self.publisher = self.create_publisher(MapData, 'map_topic', 10)
timer_period = 1.0
self.timer = self.create_timer(timer_period, self.timer_callback)
def timer_callback(self):
msg = MapData()
msg.width = 100
msg.height = 100
msg.resolution = 0.05
msg.data = [0] * (100 * 100) # 全部初始化为空闲
msg.frame_id = "map"
self.publisher.publish(msg)
self.get_logger().info('发布地图数据')
def main(args=None):
rclpy.init(args=args)
map_publisher = MapPublisher()
rclpy.spin(map_publisher)
map_publisher.destroy_node()
rclpy.shutdown()
订阅端示例:
import rclpy
from rclpy.node import Node
from 你的功能包名.msg import MapData
class MapSubscriber(Node):
def __init__(self):
super().__init__('map_subscriber')
self.subscription = self.create_subscription(
MapData,
'map_topic',
self.listener_callback,
10)
def listener_callback(self, msg):
self.get_logger().info(f'收到地图: {msg.width}x{msg.height}, 分辨率: {msg.resolution}')
# 这里可以处理地图数据了
def main(args=None):
rclpy.init(args=args)
map_subscriber = MapSubscriber()
rclpy.spin(map_subscriber)
map_subscriber.destroy_node()
rclpy.shutdown()
你看,代码结构跟之前用String消息几乎一模一样。唯一不同的就是import路径和消息类型。
关键点:import路径是你的功能包名.msg,不是std_msgs.msg。这个别搞混了。
2.4 本章知识体系
为了让你更直观地理解整个流程,我画了一张图:
2.5 总结与避坑
自定义消息接口,说白了就是三步:定义 → 编译 → 使用。流程不复杂,但细节容易出错。
我把自己踩过的坑总结一下:
- 编译后别忘了source:每次编译完都要
source install/setup.bash,否则ROS2找不到你的新消息 - 消息类型要匹配:发布端和订阅端用的消息类型必须完全一致,包括功能包名
- 字段命名别用关键字:比如别用
type、class这些Python保留字 - 数组长度要小心:ROS2的数组是动态长度的,但如果你在C++端用固定长度数组接收,可能会出问题
个人经验:我建议你在项目初期就把所有需要传递的数据结构定义好,做成一个单独的msg功能包。这样其他功能包都依赖它,方便管理。别像我早期那样,每个功能包各自定义消息,最后改一个字段要改七八个地方……那叫一个酸爽。
好了,自定义消息接口就讲到这里。下一节我们聊聊更复杂的情况——如何在同一个话题上传输多种不同类型的消息。嗯,到时候你就知道自定义消息有多重要了。