ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ROS2自定义消息从零到实践:msg文件编写与节点通信详解

ROS2自定义消息从零到实践:msg文件编写与节点通信详解 简介ROS2消息自定义与使用教程资源面向机器人通信开发初学者及需要扩展节点数据类型的ROS2开发者可应用于机器人导航、传感器数据封装、多节点协作等实际场景。资源基于ament工具链完整覆盖消息包创建、.msg文件定义、colcon构建安装、C接口生成及发布订阅节点实现等核心环节帮助读者打通从自定义消息到实际通信的完整链路。包体共645个文件包含cmake/make构建脚本、cpp/hpp源文件、py脚本、msg消息定义及colcon构建中间文件等压缩包仅3.43MB结构紧凑便于对照查阅已有470人学习使用。通过整理示例代码与工程文件读者可快速掌握消息类型设计方法并复用到自己项目中资源也呈现了典型构建配置与目录组织方式适合作为ROS2通信机制的自学参考或课堂教学示例能为后续深入学习服务质量、并发通信等高级主题打下基础。1. 为什么要折腾自定义消息先说个场景前阵子我在调一台差速小车节点想把“障碍物坐标置信度时间戳”一起发出来。用内置的std_msgs/Float32MultiArray也能凑合但数组里哪个元素是x、哪个是y全靠注释和默契。当时我就在想如果消息能像结构体一样字段名清清楚楚下游节点拿到的就是一份自解释的数据该多省心。这正是ROS2里自定义消息的用武之地。所谓消息就是话题、服务、动作之间传递数据的“通信协议”。内置的std_msgs、geometry_msgs能覆盖很多常见场景但实际项目一深入就发现不够用。要么字段组合太怪要么类型对不上要么希望一段数据里既有名字又有数值数组。与其硬编码、到处拼接解析不如直接定义一个符合业务逻辑的消息类型。这篇内容适合刚接触ROS2不久、已经会创建基础功能包、但对“接口定义”这件事还有点懵的朋友。看完之后你会明白怎么自己写.msg文件、怎么在CMakeLists和package.xml里做配置然后把自定义消息用在Python或C节点里。我也会把编译报错、接口找不到、QoS不匹配这些经典坑一起说出来。先立个共识在ROS2里自定义消息并不是什么玄学它本质上就是“定义数据结构 生成代码 被节点引用”。把这三个理解透剩下的都是套路。2. 搭建接口包把自定义消息建出来2.1 先分清三种接口文件msg、srv、action在动手之前先把类型搞清楚。ROS2里接口文件分为三种.msg定义话题消息。比如传感器数据、控制指令。.srv定义服务通信包含请求和响应两部分用---分隔。.action定义动作通信包含目标、结果、反馈三部分用两行---分隔。大多数人学自定义接口建议从.msg开始因为话题在顶层框架里用得最多也最容易理解。等消息搞明白了srv和action就是同一套思路的延伸。需要记住一个核心概念你写的是接口描述文件真正在C或Python里import的是由这些文件生成的代码。在C里你include的是“包名/msg/文件名.hpp”在Python里你import的是“包名.msg.文件名”。生成过程由构建系统自动完成但前提是你配置正确。2.2 创建接口功能包自定义消息不是随便放在执行包里建议单独建一个interface“接口包”这样多个功能包都能依赖它。用ros2 pkg create就能建ros2 pkg create my_interfaces --build-type ament_cmake这里用的构建类型是ament_cmake因为接口包需要CMake才方便生成代码。虽然也可以用ament_python包放msg但实际中维护麻烦我建议统一用CMake。建出来之后目录结构大概是这样my_interfaces/ ├── CMakeLists.txt ├── package.xml ├── msg/ │ └── (等你添加.msg文件) └── srv/ └── (如果需要服务)msg、srv、action这些目录不一定默认存在需要自己mkdir创建。2.3 写.msg文件字段类型有哪些讲究在msg目录下新建一个文件比如TargetPose.msg。文件名首字母大写这算ROS社区的约定你最好也遵守不然后面某些工具容易出怪问题。一个最简单的自定义消息string object_name float32 x float32 y float32 confidence builtin_interfaces/Time timestamp每一行就是一个字段格式是“类型 字段名”。类型可以是ROS2内置的基础类型比如int32、float64、string也可以是别的包里的消息类型比如geometry_msgs/PoseStamped甚至嵌套当前包里定义的其他msg。这里我踩过一个坑字段名不要用type、command这类和关键字贴近的词虽然编译不一定报错但某些代码生成环节会让人摸不着头脑。字段名建议语义化一看就知道是什么。如果你要在一个消息里放另一个自定义消息可以在类型前加包名比如my_interfaces/TargetPose target std_msgs/Header header2.4 CMakeLists与package.xml的必改项写完.msg如果直接编译会提示找不到头文件之类的问题因为你还没告诉构建系统“这里有接口要生成”。CMakeLists.txt里必须包含以下几块内容find_package(rosidl_default_generators REQUIRED) find_package(std_msgs REQUIRED) find_package(builtin_interfaces REQUIRED) rosidl_generate_interfaces(${PROJECT_NAME} msg/TargetPose.msg DEPENDENCIES std_msgs builtin_interfaces )第一行find_package(rosidl_default_generators REQUIRED)是生成接口代码的基础rosidl_generate_interfaces里的第一个参数是包名后续跟着所有.msg、.srv、.action文件路径最后的DEPENDENCIES写上引用了哪些外部包。package.xml里同样要加依赖dependrosidl_default_generators/depend dependstd_msgs/depend dependbuiltin_interfaces/depend member_of_grouprosidl_interface_packages/member_of_groupmember_of_group这行很容易漏掉它标记当前包属于接口包生成的消息才会被正确注册。漏了之后最典型的现象是ros2 interface list里看不到你的接口。改完之后回到工作空间根目录重新构建colcon build --packages-select my_interfaces source install/setup.bash2.5 编译并检查接口是否注册成功编译完先别急着写节点先确认接口真的“能用”。终端里输入ros2 interface list | grep my_interfaces能看到输出说明接口已经注册。还可以查看具体结构ros2 interface show my_interfaces/msg/TargetPose这个命令会打印字段列表和.msg里的内容一致。如果信息不对多半是source的路径不对或者构建缓存有问题清掉build/install再试试。3. 在Python节点中发送和接收自定义消息接口定义好之后你的功能包就需要依赖它然后在节点代码里引用。3.1 修改功能包的依赖假设你新建了一个功能包叫msg_demo那么package.xml和CMakeLists如果是C都要声明依赖my_interfaces。Python包只需要在package.xml里加dependmy_interfaces/depend同时如果使用Python包setup.py里的install_requires或data_files不一定需要特别处理只要通过ros2 pkg create生成的结构规范依赖一般没问题。但务必记得ament_python包的package.xml里也要加。3.2 发布端代码从填充字段到发布下面是发布端Python代码使用自定义消息TargetPoseimport rclpy from rclpy.node import Node from std_msgs.msg import Header from builtin_interfaces.msg import Time from my_interfaces.msg import TargetPose class TargetPosePublisher(Node): def __init__(self): super().__init__(target_pose_publisher) self.publisher self.create_publisher(TargetPose, target_pose, 10) self.timer self.create_timer(1.0, self.publish_callback) def publish_callback(self): msg TargetPose() msg.object_name obstacle msg.x 1.5 msg.y 2.0 msg.confidence 0.98 now self.get_clock().now().to_msg() msg.timestamp now self.publisher.publish(msg) self.get_logger().info(fPublished: {msg.object_name}, ({msg.x}, {msg.y}), conf{msg.confidence}) def main(argsNone): rclpy.init(argsargs) node TargetPosePublisher() try: rclpy.spin(node) except KeyboardInterrupt: pass finally: node.destroy_node() rclpy.shutdown() if __name__ __main__: main()这里有个关键点from my_interfaces.msg import TargetPose如果接口包编译或者source没做好import直接失败。此外内嵌的Time消息用self.get_clock().now().to_msg()得到准确可靠。发布端用create_publisher时第二个参数是话题名第三个参数是QoS队列深度。10表示缓冲区最多存10条这个数值不是越大越好后文会展开说。3.3 订阅端代码回调里怎么取数据订阅端几乎就是一个镜像操作import rclpy from rclpy.node import Node from my_interfaces.msg import TargetPose class TargetPoseSubscriber(Node): def __init__(self): super().__init__(target_pose_subscriber) self.subscription self.create_subscription( TargetPose, target_pose, self.listener_callback, 10 ) self.subscription # 防止被垃圾回收 def listener_callback(self, msg): self.get_logger().info( fReceived: {msg.object_name}, ({msg.x}, {msg.y}), conf{msg.confidence}, time{msg.timestamp} ) def main(argsNone): rclpy.init(argsargs) node TargetPoseSubscriber() try: rclpy.spin(node) except KeyboardInterrupt: pass finally: node.destroy_node() rclpy.shutdown() if __name__ __main__: main()回调函数里的msg参数字段访问方式和发布端完全一致。有人会问“能不能直接打印msg”可以但打印结果通常是一大串嵌套结构没有字段逐个访问来得直观。3.4 用命令行工具验证消息内容没有写任何可视化工具的时候最直接的验证方式是用命令行。先启动发布节点然后在另一个终端执行ros2 topic list ros2 topic echo /target_poseros2 topic echo会打印话题上的所有消息字段结构一目了然。如果你想手动发送一条自定义消息可以用ros2 topic pub /target_pose my_interfaces/msg/TargetPose {object_name: test, x: 1.0, y: 2.0, confidence: 0.5}前提是终端里已经source过install/setup.bash且发布端没和它冲突。命令行一次性地发消息很适合调试。4. 实际开发中的疑难杂症与排查清单自定义消息本身并不复杂但实际使用中容易栽在环境、依赖和QoS上。我把自己遇到的、以及帮别人排查过的典型问题列出来。4.1 “找不到接口”的N种原因最常见的报错说辞是ModuleNotFoundError: No module named my_interfaces或者Package my_interfaces not found这不是代码问题基本是这三类原因现象原因解决办法Python import不到msg没有source接口包重新source install/setup.bash或直接source整个工作空间colcon build时找不到接口包顺序问题构建时接口包没先构建先构建依赖包再用--packages-select或直接colcon build全量构建ros2 interface list看不到接口包内package.xml漏了member_of_group补上并重新构建终端source没问题但新终端又不行忘了把source写入.bashrc建议在.bashrc里source工作空间的install/setup.bash跨工作站拷贝代码后找不到接口包没同步构建换环境后重新编译并source很多新手在同一个终端窗口里又构建又运行容易遇到缓存混乱。我的习惯是构建时开一个终端运行程序时重新开一个终端并source。构建和运行分离能少很多玄学问题。4.2 QoS匹配问题两边都写10还是会丢数据ROS2话题通信的QoS策略不是简单“队列长度”。create_publisher(TargetPose, target_pose, 10)里的10只是history深度完整的QoS还包括可靠性、持久性等。默认情况下发布端和订阅端都使用“系统默认”设置一般两者能互通。但如果你给发布端设置了RELIABILITY (RELIABLE)订阅端保持默认通常也兼容。反之如果订阅端设成BEST_EFFORT发布端是RELIABLE在某些场景下会建立连接但会有丢包行为不好预测。如果出现“节点启动但话题收不到数据”先检查QoS是否兼容用ros2 topic info /target_pose --verbose查看两端最终协商的策略。这个命令很实用能看到发布端和订阅端各自的可靠性、耐久性设置。给一个经验值传感器高频场景适合BEST_EFFORT控制指令用RELIABLE。自定义消息本身不绑定QoS完全是节点端设置所以不要把这个锅甩给消息定义。4.3 代码生成失败与缓存坑有时候改完.msg重新构建代码还是旧的。清理命令rm -rf build/ install/ log/ colcon build我在一次项目里同时改了多个接口包结果一个包构建成功、另一个包还是旧版本最后全量清理才解决。特别是当你改了消息文件名或者删除了字段旧生成的代码不会自动消失必须清理。另外colcon build默认会跳过没有变化的包这本身是优化机制。但当依赖包的接口变了目标包由于“看起来没变”而不会被重新编译导致运行时代码找不到新字段。遇到这种情况可以用colcon build --packages-select my_interfaces msg_demo把相关包强制一起构建。4.4 Foxy、Humble、Iron版本差异ROS2版本迭代很快接口定义的核心逻辑一致但有些细节不一样Python API从Foxy到Humblerclpy的create_publisher、create_subscription签名基本稳定。C代码生成Foxy生成的C头文件是包名/msg/消息名.hpp更早版本可能用.hpp但eloquent以上都统一了。CMake最低版本Humble要求CMake 3.8Ubuntu官方源的版本一般没问题。rosidl_generate_interfaces从长期使用的角度该宏的用法一直保留但内部工具链有变化不必深究。如果你在Ubuntu 20.04用Foxy或者Ubuntu 22.04用Humble本文的配置方案都适用。如果是更新的Iron或Jazzy最好查一下对应官方迁移指南不过基本思路一致。5. 别把自定义消息想得太重serv和action同理消息定义不止用于话题服务接口定义流程几乎一模一样。在srv目录下建一个文件比如CalculateArea.srvfloat32 width float32 height --- float32 area---上面是请求下面是响应。编译配置只需要在rosidl_generate_interfaces里加上这个srv文件路径代码引用方式为from my_interfaces.srv import CalculateArea创建一个服务端节点时回调函数接收request和response两个对象def handle_calculate(request, response): response.area request.width * request.height return response这和话题消息在使用形式上不同但接口定义和构建依赖的思路完全一样。如果你后续要搞动作库.action原理也相通只是文件里多了一个分段。我建议新手先用一到两个msg项目练手跑通“定义-编译-发布-订阅-调试”闭环。自定义消息真正提升的是整个团队的“可读性”一个人乐呵乐呵的项目也许无所谓但项目一旦有交接清晰的接口就是最好的文档。最后说一个个人习惯我会在msg文件里给每个字段写注释不只是给人看更是给自己一个月后看。ROS2的消息描述文件支持#注释写清楚每个字段的单位和取值范围真的能省下很多排查时间。这也是我玩自定义接口这几年觉得性价比最高的“隐藏功能”。本文还有配套的精品资源点击获取
返回列表