资讯动态

PX4 ROS 2 消息翻译节点(Message Translation Node)指南:跨版本消息兼容的实现原理与开发实践

发布时间:2026/10/3 1:56:47 来源:尧图企业网站定制
嵌入式物联网机器人自动驾驶智能硬件【免费下载链接】PX4-AutopilotPX4 Autopilot Software项目地址https://gitcode.com/gh_mirrors/px/PX4-Autopilot点击查看免费下载PX4 自 v1.16 起引入了基于消息版本化的 ROS 2 消息翻译节点Message Translation Node源码位于 msg/translation_node让使用不同版本 PX4 消息定义编译的 ROS 2 应用与当前版本的 PX4 之间能够互相通信而无需修改应用或 PX4 任何一侧。本文从安装运行、ROS 2 应用侧开发、消息版本化目录结构、版本更新全流程演练到源码级实现原理与已知限制系统讲解这一机制读完即可上手使用并参与消息翻译的开发维护。技术状态PX4 v1.16 引入目前标记为Experimental实验性。文中所有命令、路径与代码均以当前仓库实际内容为准。为什么需要消息翻译节点PX4 通过 uORB 消息版本化机制 跟踪消息定义的历史变更所有版本化消息的定义中必须包含uint32 MESSAGE_VERSION字段每次对消息定义做出破坏性修改时递增该字段并将旧版本定义归档保存。问题随之而来ROS 2 应用在编译时会把px4_msgs中的消息定义“固化”进自己的二进制文件。当应用基于旧版px4_msgs编译、而飞控端 PX4 固件已经升级到新消息版本时两者的 DDS 数据类型不再匹配通信就会失败。传统做法是强制升级应用重新编译成本高且难以保证所有第三方应用同步更新。消息翻译节点的思路是让版本差异在数据链路中自动消解。它能够访问 PX4 历史上定义过的所有消息版本动态观察 DDS 数据空间监控来自 PX4经由 uXRCE-DDS Bridge以及 ROS 2 应用的发布、订阅和服务。当检测到版本不一致时它会在幕后把消息转换为双方各自期望的当前版本从而保证兼容。为了在同一 ROS 2 域中支持同一消息的多个版本共存翻译节点引入了明确的命名约定发布、订阅和服务的主题名都带有各自消息版本号作为后缀形如topic_name_vversion例如fmu/out/vehicle_attitude_v2。版本 0 比较特殊——主题名不带任何后缀。安装与运行翻译节点翻译节点本身是一个独立的 ROS 2 功能包translation_node与px4_msgs_old历史消息归档包一起随 PX4 源码提供。安装步骤如下1.可选创建 ROS 2 工作区mkdir -p /path/to/ros_ws/src2. 使用辅助脚本把消息定义与翻译节点拷贝进工作区cd /path/to/ros_ws /path/to/PX4-Autopilot/Tools/copy_to_ros_ws.sh .copy_to_ros_ws.sh 会执行以下操作脚本源见 Tools/copy_to_ros_ws.sh#L21-L33拷贝msg/translation_node到src/translation_node拷贝msg/px4_msgs_old历史版本消息包到src/px4_msgs_old若src/px4_msgs尚不存在则克隆px4_msgs仓库并清空其消息文件将 PX4 源码中的msg/*.msg、msg/versioned/*.msg以及srv/*.srv拷贝到src/px4_msgs对应目录。3. 构建并 source 工作区colcon build source /path/to/ros_ws/install/setup.bash4. 运行翻译节点ros2 run translation_node translation_node_bin启动后应能看到类似输出[INFO] [1734525720.729530513] [translation_node]: Registered pub/sub topics and versions: [INFO] [1734525720.729594413] [translation_node]: Registered services and versions:翻译节点运行期间任何同时运行的、面向 PX4 通信的 ROS 2 应用只要使用节点可识别的消息版本即可正常通信若遇到未知的主题版本翻译节点会打印警告信息。注意如果修改了 PX4 中的消息定义或翻译节点代码需要从上述第 2 步开始重新执行以更新 ROS 工作区重新拷贝并colcon build。在 ROS 2 应用中使用版本化主题开发与 PX4 通信的 ROS 2 应用时无需手动记忆某个消息的具体版本号。消息类型本身通过MESSAGE_VERSION静态常量携带版本信息可以通用地把版本后缀拼接到主题名上Ctopic_name _v std::to_string(T::MESSAGE_VERSION)Pythontopic_name _v VehicleAttitude.MESSAGE_VERSION其中T为消息类型例如px4_msgs::msg::VehicleAttitude。最小订阅-发布节点示例原文档给出了一份同时使用两个版本化 PX4 消息订阅VehicleAttitude、发布VehicleCommand的最小示例这里完整给出C 版本#include string #include rclcpp/rclcpp.hpp #include px4_msgs/msg/vehicle_command.hpp #include px4_msgs/msg/vehicle_attitude.hpp // Template function to get the message version suffix // The correct message version is directly inferred from the message definition template typename T std::string getMessageNameVersion() { if (T::MESSAGE_VERSION 0) return ; return _v std::to_string(T::MESSAGE_VERSION); } class MinimalPubSub : public rclcpp::Node { public: MinimalPubSub() : Node(minimal_pub_sub) { // Use template function to define the correct topics automatically const std::string sub_topic /fmu/out/vehicle_attitude getMessageNameVersionpx4_msgs::msg::VehicleAttitude(); const std::string pub_topic /fmu/in/vehicle_command getMessageNameVersionpx4_msgs::msg::VehicleCommand(); _subscription this-create_subscriptionpx4_msgs::msg::VehicleAttitude( sub_topic, 10, std::bind(MinimalPubSub::attitude_callback, this, std::placeholders::_1)); _publisher this-create_publisherpx4_msgs::msg::VehicleCommand(pub_topic, 10); } private: void attitude_callback(const px4_msgs::msg::VehicleAttitude::SharedPtr msg) { RCLCPP_INFO(this-get_logger(), Received attitude message.); } rclcpp::Publisherpx4_msgs::msg::VehicleCommand::SharedPtr _publisher; rclcpp::Subscriptionpx4_msgs::msg::VehicleAttitude::SharedPtr _subscription; };Python 版本import rclpy from rclpy.node import Node from px4_msgs.msg import VehicleCommand, VehicleAttitude # Helper function to get the message version suffix # The correct message version is directly inferred from the message definition def get_message_name_version(msg_class): if msg_class.MESSAGE_VERSION 0: return return f_v{msg_class.MESSAGE_VERSION} class MinimalPubSub(Node): def __init__(self): super().__init__(minimal_pub_sub) # Use helper function to define the correct topics automatically sub_topic f/fmu/out/vehicle_attitude{get_message_name_version(VehicleAttitude)} pub_topic f/fmu/in/vehicle_command{get_message_name_version(VehicleCommand)} self._subscription self.create_subscription( VehicleAttitude, sub_topic, self.attitude_callback, 10 ) self._publisher self.create_publisher( VehicleCommand, pub_topic, 10 ) def attitude_callback(self, msg): self.get_logger().info(Received attitude message.)关键点版本后缀完全由消息定义驱动T::MESSAGE_VERSION直接取自编译时使用的px4_msgs消息定义无需开发者额外维护版本号版本 0 无后缀当MESSAGE_VERSION 0时返回空字符串即不添加_vversion后缀这也是 translation_util.h 中getVersionedTopicName的约定PX4 侧自动处理在 PX4 端DDS 客户端uXRCE-DDS Bridge会自动为包含uint32 MESSAGE_VERSION x字段的消息定义的主题名添加版本后缀应用侧无需关心飞控实际运行的消息版本。核心概念消息、版本化消息与版本翻译翻译机制建立在三个明确定义的概念之上详见 msg/translation_node/README.md 与 uORB 版本化文档消息message定义通信使用的数据格式。主题消息由.msg文件定义服务消息由.srv文件定义两者都是消息。版本化消息versioned message变更历史被跟踪的消息。每次变更导致版本号递增旧版定义存入历史归档。最新版本存放在msg/versioned/主题或srv/versioned/服务历史版本存放在msg/px4_msgs_old/msg/或msg/px4_msgs_old/srv/。版本翻译version translation定义一条或多条消息定义在不同版本间内容的双向映射。每个翻译是msg/translation_node/translations/下的一个独立.h头文件分为两类直接翻译direct translation单条消息在其两个版本之间的双向映射。这是最简单的情况应当优先使用。通用翻译generic translationn个输入消息与m个输出消息跨版本的双向映射。适用于消息的拆分、合并或把字段从一个消息移动到另一个消息的场景。消息目录结构PX4 v1.16 起从 PX4 v1.16 开始msg/与srv/目录按如下结构组织PX4-Autopilot ├── ... ├── msg/ ├── *.msg # 非版本化主题消息文件 ├── versioned/ # 最新版版本化主题消息文件 ├── px4_msgs_old/ # 版本化消息历史.msg .srv[ROS 2 包] └── translation_node/ # 翻译节点与翻译头文件 [ROS 2 包] └── srv/ ├── *.srv # 非版本化服务消息文件 └── versioned/ # 最新版版本化服务消息文件相对传统结构这里新增了三个目录versioned/、px4_msgs_old/和translation_node/。msg/versioned/与srv/versioned/存放每条消息的当前最新版本文件必须包含MESSAGE_VERSION字段以表明其是版本化消息文件名遵循常规命名不带版本后缀。当前仓库中msg/versioned/实际包含 38 个版本化主题消息例如 VehicleAttitude.msg、HomePosition.msg、VehicleStatus.msg 等srv/下目前仅有非版本化的 VehicleCommand.srv。参考目录结构如下PX4-Autopilot ├── ... ├── msg/ └── versioned/ ├── VehicleAttitude.msg # e.g. MESSAGE_VERSION 3 └── VehicleGlobalPosition.msg # e.g. MESSAGE_VERSION 2 └── srv/ └── versioned/ └── VehicleCommand.srv # e.g. MESSAGE_VERSION 2说明文档中的示例版本号如 VehicleAttitude 为 3仅用于演示实际各消息的版本号以仓库中msg/versioned/各文件内的MESSAGE_VERSION字段为准。px4_msgs_old/归档所有版本化消息的历史包括主题和服务消息分别在msg/与srv/子目录每个文件都包含MESSAGE_VERSION字段文件名反映消息版本带后缀如V1、V2。当前仓库 msg/px4_msgs_old/msg 已归档 21 个历史消息例如HomePositionV0.msg、HomePositionV1.msg、VehicleStatusV0.msgVehicleStatusV3.msg。示例结构... msg/ └── px4_msgs_old/ ├── msg/ ├── VehicleAttitudeV1.msg ├── VehicleAttitudeV2.msg └── VehicleGlobalPositionV1.msg └── srv/ └── VehicleCommandV1.srvtranslation_node/存放所有消息版本之间的翻译头文件每个翻译直接或通用是一个.h头文件all_translations.h 作为总入口头文件include 了全部翻译头文件。当前仓库 translations 目录 中已有 20 余个实际翻译如translation_vehicle_status_v1.htranslation_vehicle_status_v4.h、translation_home_position_v2.h等以及三个官方模板。示例结构... msg/ └── translation_node/ └── translations/ ├── all_translations.h # 主头文件 ├── translation_vehicle_attitude_v1.h # 直接翻译 v0 - v1 ├── translation_vehicle_attitude_v2.h # 直接翻译 v1 - v2 ├── translation_vehicle_attitude_v3.h # 直接翻译 v2 - latest (v3) ├── translation_vehicle_global_position_v1.h # 直接翻译 v0 - v1 ├── translation_vehicle_global_position_v2.h # 直接翻译 v1 - latest (v2) ├── translation_vehicle_command_v1.h # 直接翻译 v0 - v1 └── translation_vehicle_command_v2.h # 直接翻译 v1 - latest (v2)完整演练如何更新一个版本化消息本节以VehicleAttitude为例演示把消息版本从3升到4新增new_field字段并创建新直接翻译的完整流程共 5 个步骤。步骤 1归档当前版本化消息定义把版本化的.msg主题消息文件或.srv服务消息文件拷贝到px4_msgs_old/msg/或px4_msgs_old/srv/并在文件名后追加消息版本号。例如拷贝msg/versioned/VehicleAttitude.msg→msg/versioned/px4_msgs_old/msg/VehicleAttitudeV3.msg步骤 2更新既有翻译中对归档定义的引用更新既有翻译头文件msg/translation_node/translations/*.h使其引用新归档的消息定义将px4_msgs::msg::VehicleAttitude替换为px4_msgs_old::msg::VehicleAttitudeV3将#include px4_msgs/msg/vehicle_attitude.hpp替换为#include px4_msgs_old/msg/vehicle_attitude_v3.hpp。步骤 3更新版本化定义在msg/versioned/VehicleAttitude.msg中做出所需修改首先递增MESSAGE_VERSION字段然后更新触发版本变更的字段。修改前uint32 MESSAGE_VERSION 3 uint64 timestamp ...修改后uint32 MESSAGE_VERSION 4 # Increment uint64 timestamp float32 new_field # Make definition changes ...步骤 4新增翻译头文件创建桥接归档版本与最新版本之间的翻译头文件translation_node/translations/translation_vehicle_attitude_v4.h// Translate VehicleAttitude v3 -- v4 #include px4_msgs_old/msg/vehicle_attitude_v3.hpp #include px4_msgs/msg/vehicle_attitude.hpp class VehicleAttitudeV4Translation { public: using MessageOlder px4_msgs_old::msg::VehicleAttitudeV3; static_assert(MessageOlder::MESSAGE_VERSION 3); using MessageNewer px4_msgs::msg::VehicleAttitude; static_assert(MessageNewer::MESSAGE_VERSION 4); static constexpr const char* kTopic fmu/out/vehicle_attitude; static void fromOlder(const MessageOlder msg_older, MessageNewer msg_newer) { msg_newer.timestamp msg_older.timestamp; msg_newer.timestamp_sample msg_older.timestamp_sample; msg_newer.q[0] msg_older.q[0]; msg_newer.q[1] msg_older.q[1]; msg_newer.q[2] msg_older.q[2]; msg_newer.q[3] msg_older.q[3]; msg_newer.delta_q_reset msg_older.delta_q_reset; msg_newer.quat_reset_counter msg_older.quat_reset_counter; // Populate new_field with some value msg_newer.new_field -1; } static void toOlder(const MessageNewer msg_newer, MessageOlder msg_older) { msg_older.timestamp msg_newer.timestamp; msg_older.timestamp_sample msg_newer.timestamp_sample; msg_older.q[0] msg_newer.q[0]; msg_older.q[1] msg_newer.q[1]; msg_older.q[2] msg_newer.q[2]; msg_older.q[3] msg_newer.q[3]; msg_older.delta_q_reset msg_newer.delta_q_reset; msg_older.quat_reset_counter msg_newer.quat_reset_counter; // Discards new_field from MessageNewer } }; REGISTER_TOPIC_TRANSLATION_DIRECT(VehicleAttitudeV4Translation);翻译头文件模板可在仓库中直接参考直接主题消息翻译模板example_translation_direct_v1.h通用主题消息翻译模板example_translation_multi_v2.h直接服务消息翻译模板example_translation_service_v1.h仓库中还包含真实可运行的翻译示例例如 translation_home_position_v2.h它在fromOlder()中根据 v1 的roll/pitch/yaw是否为有限值推断出 v2 新增的valid_attitude字段并在toOlder()中把 v2 的无效姿态还原为NAN回写到 v1——体现了“新增字段的默认值推断”与“信息丢弃”的双向处理思路。步骤 5在all_translations.h中收录新头文件把所有新建的头文件添加到 translations/all_translations.h翻译节点才能找到它们。例如追加一行#include translation_vehicle_attitude_v4.h何时需要通用翻译上述示例以及大多数情况只需创建直接翻译因为变更只涉及单条消息。在拆分、合并或移动定义等更复杂的情况下必须创建通用翻译。例如把一个字段从消息 A 移动到消息 B 时应添加一个通用翻译以两个旧版本消息为输入、两个新版本消息为输出从而保证正向和反向翻译都不丢失信息——这正是 example_translation_multi_v2.h 展示的方式该模板省略了fromOlder()/toOlder()中实际修改字段的代码。通用翻译的类骨架如下来自 translation_util.h 的文档注释class MyTranslation { public: using MessagesOlder TypesArrayROS_MSG_OLDER_1, ROS_MSG_OLDER_2, ...; static constexpr const char* kTopicsOlder[] { fmu/out/msg_1, fmu/out/msg_2, ... }; using MessagesNewer TypesArrayROS_MSG_NEWER_1, ROS_MSG_NEWER_2, ...; static constexpr const char* kTopicsNewer[] { fmu/out/msg_1, fmu/out/msg_2, ... }; static void fromOlder(const MessagesOlder::Type1 msg_older1, ..., MessagesNewer::Type1 msg_newer1, ...) { /* ... */ } static void toOlder(const MessagesNewer::Type1 msg_newer1, ..., MessagesOlder::Type1 msg_older1, ...) { /* ... */ } };警告如果某个嵌套消息的定义发生变化所有包含该消息的消息也必须同步升级版本。例如若 PositionSetpointTriplet 被版本化其嵌套消息变更时它也必须升版。这一点对服务尤为重要因为服务消息更可能引用其他消息定义。实现原理动态监控与翻译图翻译节点内部由三个核心组件构成见 main.cpp 中的RosTranslationNodePubSubGraph维护主题发布/订阅的翻译图pub_sub_graph.hServiceGraph维护服务请求/响应的翻译图service_graph.hMonitor动态监控 DDS 数据空间检测外部发布者/订阅者/服务并触发图更新monitor.h。翻译节点动态监控主题与服务并按要求实例化对侧的发布/订阅例如检测到某个主题版本 1 的外部发布者和版本 2 的外部订阅者时节点便会在中间搭起翻译链路。图的构建与遍历节点内部维护一张“所有已知主题-版本元组”的图图节点是主题-版本元组图的边是消息翻译graph.h 中MessageIdentifier{topic_name, version}即节点标识。由于可以注册任意消息翻译图可能存在环且两个节点之间可能有多条路径因此每次主题更新时使用 BFS 最短路径算法遍历图graph.h#L201-L240 的translate()方法。从一个节点移动到下一个节点时会以当前主题数据调用消息翻译方法如果某节点因之前检测到外部订阅者而实例化了发布者则发布数据。这样同一主题任意版本的多个订阅者都能获得正确版本的数据。图的数据结构要点MessageNode代表一个消息端点TranslationNode夹在消息节点之间可拥有最多 32 个输入kMaxNumInputs见 graph.h#L96TranslationNode用std::bitset跟踪各输入是否就绪setInputReady/translate对多输入翻译只有全部输入消息就绪后翻译才继续graph.h#L83-L90遍历时以翻译节点为“屏障”仅当所有输入就绪才继续展开输出节点同时跳过已访问节点防止沿原路反向翻译造成死循环。翻译的注册机制翻译类通过宏注册到单例RegisteredTranslationstranslation_util.h#define REGISTER_TOPIC_TRANSLATION_DIRECT(class_name) // 直接主题翻译 #define REGISTER_SERVICE_TRANSLATION_DIRECT(class_name) // 直接服务翻译 #define REGISTER_TOPIC_TRANSLATION(class_name) // 通用主题翻译注册时registerDirectTranslation()/registerTranslation()会为每个输入/输出消息调用getTopicForMessageType()translation_util.h#L238-L267其中利用getVersionedTopicName生成带版本后缀的主题名并预构建订阅/发布工厂QoS 为best_effort、深度 1。服务注册则额外生成请求/响应两套翻译见registerServiceDirectTranslation()。所有翻译通过 translations.h 中的TopicTranslations/ServiceTranslations容器统一管理最终由RosTranslationNode构造时注入两个图。仓库还带有完整的单元测试msg/translation_node/test 下包含图算法测试graph.cpp、发布/订阅测试pub_sub.cpp和服务测试services.cpp含TestV0/TestV1/TestV2.srv三个测试服务可在colcon build时通过BUILD_TESTING选项启用见 CMakeLists.txt#L45-L79。已知限制使用翻译节点时需要注意以下限制均来自官方文档服务消息翻译不支持 ROS Humble但支持 ROS Jazzy当前实现依赖的某个服务 API 在 ROS Humble 中尚不可用因此在 Humble 上构建时服务翻译会被禁用CMakeLists.txt#L38-L43 中通过DISABLE_SERVICES宏实现并打印警告。主题消息翻译在所有支持的 ROS 版本上均完整可用。服务消息只支持线性历史即不支持消息的拆分或合并服务翻译图按线性链路处理。同一主题的两个不同版本同时存在发布者和订阅者时不受支持会触发无限循环发布。具体指如下问题配置app 1: pub topic_v1, sub topic_v1 app 2: pub topic_v2, sub topic_v2实际上该配置很少出现因为与 FMU 共享的 ROS 主题是有方向性的例如/fmu/out/vehicle_status或/fmu/in/trajectory_setpoint应用通常不会对同一主题同时发布和订阅。如需处理这一边界情况可以扩展翻译节点。总结PX4 的消息翻译节点利用 uORB 消息版本化与topic_name_vversion主题命名约定以“动态监控 翻译图 最短路径遍历”的方式在运行时自动完成新旧消息版本间的双向转换。对应用开发者而言只需依据消息类型的MESSAGE_VERSION拼接主题后缀即可获得跨版本兼容对固件开发者而言升级版本化消息只需遵循“归档旧定义 → 更新引用 → 递增版本号 → 新增翻译头 → 注册到all_translations.h”五步流程。结合仓库中的模板头文件、真实翻译示例与单元测试可以快速为 PX4 新增自己的消息版本翻译。赞分享嵌入式物联网机器人自动驾驶智能硬件【免费下载链接】PX4-AutopilotPX4 Autopilot Software项目地址https://gitcode.com/gh_mirrors/px/PX4-Autopilot点击查看免费下载相关推荐Claude for Legal监管合规插件深度解析监管跟踪、政策差异分析与合规缺口管理Claude for Legal监管合规插件深度解析监管跟踪、政策差异分析与合规缺口管理 Claude for Legal是一套专为法律工作流设计的插件套件AI 技能人工智能AI 应用PX4 UORB 消息详解MessageFormatRequest——跨进程消息兼容性校验协议PX4 UORB 消息详解MessageFormatRequest——跨进程消息兼容性校验协议 MessageFormatRequest 是 PX4 Auto嵌入式物联网机器人自动驾驶智能硬件NoneBot2 消息处理实战指南Message 消息序列、MessageSegment 消息段与消息模板深度解析NoneBot2 消息处理实战指南Message 消息序列、MessageSegment 消息段与消息模板深度解析 在 NoneBot2 中来自不同平台的同后端即时通讯上一篇IceCream的代码审查提升代码质量的流程下一篇3 种实战案例MobileViT-v2 在工业检测中的应用创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑