尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
ROS2自定义消息:从msg定义到Python/C++双端调用实战
简介ROS2消息自定义与使用完整示例包面向需要掌握消息机制并实现自定义节点通信的ROS2学习者。资料以可编译运行的工作空间为主体完整呈现从创建msg消息包、编写.msg字段定义、生成IDL接口到通过colcon构建安装并在发布订阅节点中使用自定义消息的全流程消息定义支持float32、string、bool等常见字段并附有发布端与订阅端两个可直接运行的示例节点。压缩包共645个文件大小3.43MB主要包含cmake/make构建配置、C/Python源码、消息头文件、msg与IDL定义、编译生成的库文件与可执行文件以及colcon相关环境脚本便于在工作空间中直接复现验证。已有472人学习下载。通过对照源码、构建脚本、生成接口以及最终可执行产物可直观理解ament包创建与编译机制、rclcpp::Publisher/Subscription的调用方式并为后续学习QoS策略、跨语言通信和复杂机器人系统设计打下基础。整个示例包结构清晰、体积小巧适合在Ubuntu/ROS2环境下边看边练快速内化自定义消息的开发流程。1. ROS2自定义消息当默认消息类型撑不住业务的时候ROS2 自带的 std_msgs、geometry_msgs 能覆盖大部分通用场景但真跑到业务里你会发现「两个节点之间传一包结构化数据」这件事默认类型经常不够用。比如某个产线采集节点要往状态中心上报设备编号、温度、坐标和几个布尔标志位硬塞进 String 再自己 split能跑但那叫一个难受调试一次就后悔一次。自定义消息就是把这类业务结构固化成接口类型编译期帮你查字段拼写运行期有完整工具链做验包、过滤、录制。这篇文章用一个真实场景的完整消息定义从建接口包、写 msg、编译、Python/C 双端调用到常见翻车现场都过一遍。适合已经建好 ROS2 工作空间、想在节点间传结构化数据、又不想再跟字符串解析纠缠的开发者。2. 接口包先行为什么自定义消息要单独建包2.1 消息包与节点包分离的工程理由先说一个关键限制ROS2 的消息类型不是写在一个普通包里就能自动生成代码的。.msg文件需要经过 rosidl 代码生成器在编译期生成 Python、C 以及中间件层需要的代码而rosidl_generate_interfaces这个 CMake 函数在 ROS2 里只支持 ament_cmake 构建类型。换句话说一个包如果同时放着节点和.msg文件并且这个包是 ament_python 类型就行不通。最省心的做法是单独建一个「接口包」里面只放.msg、.srv、.action文件不写任何功能代码。其次是复用问题。假设你写了三个节点包pub 节点发、sub 节点收、bridge 节点转发它们都要引用同一个消息类型。如果把消息定义放在 pub 节点包里sub 和 bridge 就得反向依赖 pub 包工程上非常别扭。独立接口包让所有节点包都依赖同一个纯接口层像依赖一个公共库一样谁也不欠谁。还有一个容易被忽略的理由是接口演进的可见度。消息字段改了哪些节点要重新编译没有独立接口包的时候全靠玄学有了独立接口包colcon build --packages-up-to xxx能明确算出受影响范围不至于漏编译或者多编译一堆无关包。2.2 用 colcon 创建接口包创建接口包的标准命令如下我一般会把工作空间路径先切到 src 下cd ~/ros2_ws/src ros2 pkg create my_interfaces \ --build-type ament_cmake \ --dependencies rosidl_default_generators \ rosidl_default_runtime--build-type ament_cmake是接口包的唯一选择只有这个构建类型支持 rosidl 代码生成。--dependencies后面跟的不是业务依赖而是代码生成工具链rosidl_default_generators负责编译期生成各语言代码rosidl_default_runtime提供运行期支持这两个在创建接口包时是标配不要省。创建完包内结构长这样my_interfaces/ ├── CMakeLists.txt ├── package.xml ├── msg/ ├── srv/ └── action/注意msg、srv、action这三个目录在ros2 pkg create时不会自动生成需要手动创建。如果只传自定义消息建 msg 目录就够了mkdir -p my_interfaces/msg2.3 package.xml 里必须写清楚的依赖创建接口包后package.xml 里至少要把下面四段确认清楚。我用一个引用了 std_msgs 和 geometry_msgs 的接口包举例buildtool_dependrosidl_default_generators/buildtool_depend dependrosidl_default_runtime/depend dependstd_msgs/depend dependgeometry_msgs/depend member_of_grouprosidl_interface_packages/member_of_groupbuildtool_depend声明的是编译期工具rosidl_default_generators不属于普通运行时库绝不能写成普通depend。depend声明的std_msgs和geometry_msgs是消息里要引用的类型包凡是消息里出现过的接口包必须在 package.xml 和 CMakeLists 的DEPENDENCIES里同时出现少一个都会在生成代码时报找不到类型。最后那一行member_of_group是最容易被忽略的。它告诉ros2 interface工具链这个包属于 ROS2 接口包漏掉它包能编译、节点也能用但ros2 interface list里永远看不到你的自定义消息排查起来特别迷惑。对应 CMakeLists.txt 里也要同步写好依赖查找和接口生成find_package(rosidl_default_generators REQUIRED) find_package(std_msgs REQUIRED) find_package(geometry_msgs REQUIRED) rosidl_generate_interfaces(${PROJECT_NAME} msg/DeviceStatus.msg DEPENDENCIES std_msgs geometry_msgs )DEPENDENCIES里写的是这个接口包引用了哪些其他接口包生成代码时会自动把这些依赖的头文件和 Python 模块一起带上。到这里包结构和依赖声明都准备好了下一步才是真正设计消息内容这是自定义消息的核心工作决定了下游代码怎么写、怎么写才不容易踩坑。3. 写清楚消息定义msg 语法与字段设计3.1 msg 文件能用的类型与命名约束msg文件不是随便写个字符串就行的它有一套严格的类型语法。支持的类型分三组我常用这张表对照分类类型说明基础类型bool、int8~uint64、float32、float64、string标量值数组int32[]、float64[3]、string[]动态数组 / 固定长度数组嵌套类型std_msgs/Header、geometry_msgs/Point引用其他接口包的消息数组类型里有个容易被忽略的写法int32[5]表示长度不超过 5 的有界数组传输层会做长度校验。float64[3]是固定长度数组适合坐标、姿态这类定长结构。命名规范也要从一开始就守住包名全小写msg 文件名首字母大写字段名小写下划线常量名全大写。DeviceStatus.msg生成的头文件是device_status.hpp类名保持驼峰DeviceStatusPython 模块则是my_interfaces.msg.DeviceStatus。这套命名规则是在 rosidl 代码生成器里固定死的改不了也别试图对抗照着来就行。3.2 从业务需求拆字段一个设备状态消息拿一个具体场景走一遍字段设计。假设车间里有个设备节点要周期性上报自己的编号、当前温度、所在位置、运行标志和故障码。需求拆下来消息定义第一版长这样std_msgs/Header header string device_id float64 temperature geometry_msgs/Point position bool is_running bool is_error uint8 error_code每个字段对应哪条业务拆开看就很清楚。header是行业惯例自带时间戳和 frame_id比自己在消息里定义一个 time 字段再手动维护靠谱得多device_id用字符串因为设备编号里经常带分隔符和字母用整型会丢信息temperature用 float64保留足够精度position直接复用geometry_msgs/Point不用自己拆三个 float 再拼回去is_running和is_error是布尔标志error_code用整型不同值对应不同类型故障。这样设计的好处是字段名自带语义任何接手代码的人不用看注释就能猜出消息里有什么。比字段名缩写成一两个字母再在代码顶部写一大段注释强得多。3.3 常量、默认值与 Header让消息更省事msg文件支持常量和默认值这两个特性看着不起眼实际能省掉大量发送端代码。在刚才的 DeviceStatus 基础上补上常量和默认值uint8 ERROR_NONE 0 uint8 ERROR_OVERTEMP 1 uint8 ERROR_COMM_LOST 2 std_msgs/Header header string device_id unknown float64 temperature 0.0 geometry_msgs/Point position bool is_running false bool is_error false uint8 error_code 0常量ERROR_NONE、ERROR_OVERTEMP、ERROR_COMM_LOST在发送端代码里可以直接引用C 里写DeviceStatus::ERROR_OVERTEMPPython 里写DeviceStatus.ERROR_OVERTEMP不用各自定义一套 magic number。默认值的作用是给接收端一个兜底如果发送端某个字段忘了赋值接收端拿到的是 0 或 unknown而不是随机值这对调试逻辑很有帮助。Header 本身也有约定俗成的用法。发送端一般这样初始化msg.header Header() msg.header.stamp self.get_clock().now().to_msg() msg.header.frame_id device_frame时间戳用节点时钟自动生成frame_id 填坐标系名。接收端拿到消息后可以直接读msg.header.stamp判断数据是否过期不用再额外传一个时间字段。4. 编译与双端调用消息从构建产物变成代码4.1 编译接口包构建顺序与产物确认接口包建好、msg 文件写好之后第一步是单独编译接口包不要急着直接编译整个工作空间cd ~/ros2_ws colcon build --packages-select my_interfaces source install/setup.bash--packages-select只编译选中的包单独先编接口包的原因有两个一是消息代码生成比较慢且是纯生成过程单独跑一遍报错信息干净二是后面编译节点包时消息类型必须已经存在否则find_package(my_interfaces REQUIRED)直接失败。编译完先别急着写代码确认一下产物。install目录下已经能看到生成的消息源码了。头文件一般位于install/my_interfaces/include/my_interfaces/ ├── my_interfaces/msg/device_status.hpp └── my_interfaces/msg/detail/Python 模块则生成在install/my_interfaces/lib/python3.*/site-packages/my_interfaces/看到这些文件说明 rosidl 代码生成链路是通的可以先给接口包做个快速体检确认消息被工具链识别。这一步我一般用ros2 interface list过滤自己的包名下一章细讲。4.2 Python 节点调用import 路径与嵌套消息实例化Python 端使用自定义消息核心就一句话from my_interfaces.msg import DeviceStatus。但发送端有细节要注意嵌套消息类型不会自动实例化需要先创建再赋值。一个完整的发布节点长这样import rclpy from rclpy.node import Node from std_msgs.msg import Header from geometry_msgs.msg import Point from my_interfaces.msg import DeviceStatus class DevicePublisher(Node): def __init__(self): super().__init__(device_publisher) self.pub self.create_publisher(DeviceStatus, device_status, 10) self.timer self.create_timer(1.0, self.publish_status) def publish_status(self): msg DeviceStatus() msg.header Header() msg.header.stamp self.get_clock().now().to_msg() msg.header.frame_id device_frame msg.device_id dev-001 msg.temperature 45.2 msg.position Point(x1.0, y2.0, z0.0) msg.is_running True msg.is_error False msg.error_code DeviceStatus.ERROR_NONE self.pub.publish(msg) def main(argsNone): rclpy.init(argsargs) node DevicePublisher() rclpy.spin(node) node.destroy_node() rclpy.shutdown()msg.header Header()这一行很关键嵌套消息字段在 Python 端不会自动构造直接写msg.header.stamp ...会报AttributeError。同理msg.position也必须先实例化Point。字段赋值时常量用DeviceStatus.ERROR_NONE引用不需要再定义局部变量。create_publisher的第二个参数是话题名第三个参数是队列深度10 是默认值高频发布场景建议调大到 50 以上。4.3 C 节点调用头文件与 CMake 链接C 端使用自定义消息头文件路径遵循命名转换规则DeviceStatus.msg对应my_interfaces/msg/device_status.hpp命名空间则是my_interfaces::msg::DeviceStatus。一个简单发布节点的骨架#include rclcpp/rclcpp.hpp #include std_msgs/msg/header.hpp #include geometry_msgs/msg/point.hpp #include my_interfaces/msg/device_status.hpp using my_interfaces::msg::DeviceStatus; int main(int argc, char** argv) { rclcpp::init(argc, argv); auto node std::make_sharedrclcpp::Node(device_publisher_cpp); auto pub node-create_publisherDeviceStatus(device_status, 10); auto timer node-create_wall_timer( std::chrono::seconds(1), [node, pub]() { auto msg DeviceStatus(); msg.header.stamp node-now(); msg.device_id dev-002; msg.temperature 42.5; msg.is_running true; msg.error_code DeviceStatus::ERROR_NONE; pub-publish(msg); } ); rclcpp::spin(node); rclcpp::shutdown(); return 0; }C 端访问嵌套字段比 Python 简单msg.header.stamp、msg.position都是内嵌对象不需要额外实例化。但这里藏着一个陷阱C 生成的成员名保持 msg 定义里的蛇形命名device_id就是device_id不是deviceId跟常见 C 风格不一样容易手滑写错。CMake 编写是 C 端另一件必须做对的事。节点包的 CMakeLists.txt 需要find_package(my_interfaces REQUIRED) find_package(rclcpp REQUIRED) add_executable(device_publisher_cpp src/device_publisher_cpp.cpp) ament_target_dependencies(device_publisher_cpp rclcpp my_interfaces )ament_target_dependencies会把 my_interfaces 的 include 路径和依赖库一起传下去。如果你用了geometry_msgs、std_msgs也要在find_package里列出来否则编译时头文件找不到报错信息千奇百怪。编译节点包的命令cd ~/ros2_ws colcon build --packages-select my_node source install/setup.bash前提是之前的接口包已经编译过并且在这个工作空间的 install 目录里。两个包都编译完运行节点前先确认source install/setup.bash已执行不然终端找不到my_interfaces。5. 避坑自定义消息最常见的五个翻车现场5.1 C 编译报找不到 device_status.hpp现象自己节点包编译时报fatal error: my_interfaces/msg/device_status.hpp: No such file or directory。原因可能性有几种。最常见的是节点包的 CMakeLists.txt 里没有find_package(my_interfaces REQUIRED)或者ament_target_dependencies里漏写了 my_interfaces。另一种可能是源文件里 include 路径写成了my_interfaces/msg/DeviceStatus.hpp忘记转成蛇形命名rosidl 生成头文件只认device_status.hpp。解决先确认ament_target_dependencies里同时列了my_interfaces再看 include 路径写法。有个土办法find install/my_interfaces -name *.hpp能看到实际生成的头文件路径照着写就不会错。5.2 Python 运行时 ModuleNotFoundError现象from my_interfaces.msg import DeviceStatus报ModuleNotFoundError: No module named my_interfaces。原因绝大多数情况是source install/setup.bash没执行或者终端是在编译之前打开的环境变量AMENT_PREFIX_PATH里没有当前工作空间的 install 路径。也有小概率是接口包根本没编译成功。解决依次执行cd ~/ros2_ws colcon build --packages-select my_interfaces和source install/setup.bash然后echo $AMENT_PREFIX_PATH确认路径里包含~/ros2_ws/install。如果在新终端里运行还会报错检查~/.bashrc里有没有自动 source 工作空间的 setup.bash没有的话手动 source。5.3 改了 msg 文件ros2 topic echo 还是旧字段现象修改了 DeviceStatus.msg加了字段重新编译后ros2 topic echo出来的消息还是老结构。原因只编译了节点包没有重新编译接口包。或者接口包重新编译了但运行中的节点还是旧进程没重启。ROS2 的消息类型是编译期定死的运行期不会热加载新字段。解决改完 msg 文件一定要回到工作空间根目录执行colcon build --packages-select my_interfaces再编译依赖它的节点包然后重启节点。如果节点是通过ros2 run启动的务必保证当前终端已经重新 source 过。这套顺序执行完echo 里才能看到新字段。5.4 两个接口包互相引用colcon 构建死锁现象接口包 A 的消息引用了 B 的消息B 的消息又引用了 A 的消息colcon build直接报出依赖循环构建失败。原因消息层互相引用形成了环rosidl 无法确定先生成哪个包这是接口设计层面的问题不是配置问题。解决拆掉循环引用。把两个消息包里共用的字段抽到第三个更基础的接口包里让 A 和 B 只依赖 C不直接互相依赖。设计消息结构时先画一下引用关系图出现环就停手重新拆不然编译过不去还得回头改。5.5 ros2 interface list 里看不到自定义消息现象编译成功节点也能用消息字段赋值都正常但ros2 interface list | grep my_interfaces没输出。原因package.xml 里缺了member_of_grouprosidl_interface_packages/member_of_group这一行。ros2 interface 工具只索引声明了成员组的接口包漏声明就查不到。解决在 package.xml 的依赖区加上该行重新colcon build --packages-select my_interfaces并 source。注意ros2 interface list是按环境变量索引的新终端也要先 source 再用。补上之后ros2 interface package my_interfaces也能正常输出该包下所有接口。6. 进阶用命令行工具给自定义消息做一套完整验证6.1 先从构建产物下手改完消息别急着写节点先确认生成的文件对得上。看头文件是否生成、路径是否符合预期这一步可以拦截大部分低级问题find install/my_interfaces -name *device_status*6.2 用 ros2 interface 三连确认接口ros2 interface package my_interfaces ros2 interface show my_interfaces/msg/DeviceStatus ros2 interface list | grep my_interfaces三条命令分别确认消息列表、字段完整性和工具链可见性。字段里多出个没注意的默认值都能看出来。6.3 用 topic echo 做传输验证ros2 topic echo /device_status --full-length ros2 topic echo /device_status --field temperature--field是排查利器日志刷得厉害的时候只挑一个字段看。这套验证流程看着笨但每次改完消息都走一遍能拦住大多数翻车。从那以后我每次改消息定义都强制自己先跑完三连验证再写业务代码。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

PowerShell脚本化Windows计划任务:从创建到批量部署指南

PowerShell脚本化Windows计划任务:从创建到批量部署指南

说起Windows上的定时任务,很多人第一反应是打开任务计划程序,点几下鼠标,填一个时间,选一个脚本,完事。但真到了需要给一批电脑统一部署计划任务、或者因为业务调整要整体更换触发时间的时候,图形界面就开始…

📅 2026/10/21 22:05:19
MATLAB量子计算实战:从量子比特建模到Grover搜索与QFT

MATLAB量子计算实战:从量子比特建模到Grover搜索与QFT

简介:量子计算算法的MATLAB实现资源,聚焦量子计算入门与算法验证场景,面向需要快速上手量子编程的初学者、相关课程学生及科研人员,帮助解决理论算法到可运行代码的转化问题。压缩包共5个文件,包含4个MATLAB脚本与1个M…

📅 2026/10/21 22:00:16
eWebEditor 11.0部署实战:Word导入与富文本编辑器问题排查指南

eWebEditor 11.0部署实战:Word导入与富文本编辑器问题排查指南

简介:这是一份完整可用的eWebEditor 11.0中文商业版资源包,面向需要为网站、内容管理系统或企业内部平台集成在线编辑功能的开发人员与站点管理员。编辑器具备所见即所得操作能力,可支持文章、新闻、通告、讨论帖等多种图文内容的在线排版&am…

📅 2026/10/21 22:00:16
MORE NEWS

更多资讯

📰

MCP协议实战:从零开发第一个MCP Server,让Agent调用你的服务

最近连续接了几个项目,开场白几乎一模一样:能不能让 AI 帮我们操作一下内部系统?需求一听就懂,一做就卡住,卡就卡在模型根本没有一条标准的路去调用我那一堆内部服务。早期各家都在做自己的工具调用格式,接…

📰

语言模型与复杂网络结合的文化传播预测技术链路

从标题看,这是一个典型的交叉学科落地型项目:语言模型负责从海量非结构化文本里提取“人、事、情”的语义信号,复杂社会网络则负责承载这些信号在关系链上的传导与演化,最终目标是让“文化传播”这种看似虚无缥缈又极难量化的东西…

📰

Win11右键菜单一键恢复Win10经典样式:注册表CLSID修改教程

升级到Win11之后,最先让我觉得别扭的,不是居中的任务栏,也不是圆角窗口,而是那个被重新设计过的右键菜单。点一下桌面或文件夹,弹出的菜单把常用的“刷新”“新建”“复制”“粘贴”挤到一个小图标栏里,有些…

📰

共享内存原理与实战:Linux进程间通信零拷贝方案全解析

做服务端开发的人应该都体会过,进程之间传数据这件事看着简单,实际踩坑不少。最开始我调试一个图像传输模块时,两个进程要频繁搬运帧数据,用管道和UDP轮流试,要么拷贝开销大,要么延迟抖得离谱。后来换成C里…

📰

VS Code配置NS3开发环境:解决头文件找不到报错的完整指南

1. 先把报错现场说清楚我第一次在VS Code里打开NS3的代码时,顺手就点了侧边栏的调试按钮,想着赶紧跑一个demo看看结果。结果编译还没开始,编辑器先红了半边天——fatal error: ns3/applications-module.h: 没有那个文件或目录。当时的第一反应…

📰

智慧档案管理平台建设方案:从数据治理到全文检索落地

简介:智慧档案管理系统平台建设方案是一份面向档案馆及档案管理部门信息化规划的完整参考文档,重点解决传统档案管理中采集效率低、利用不便、资源共享不足等问题,既可用于整体顶层设计,也可用于具体功能模块的细化落地。资源包内…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬