拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

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

PX4 ROS 2 消息翻译节点(Message Translation Node)指南:跨版本消息兼容的实现原理与开发实践
  • 嵌入式
  • 物联网
  • 机器人
  • 自动驾驶
  • 智能硬件

【免费下载链接】PX4-Autopilot

PX4 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>_v<version>(例如fmu/out/vehicle_attitude_v2)。版本 0 比较特殊——主题名不带任何后缀。

安装与运行翻译节点

翻译节点本身是一个独立的 ROS 2 功能包(translation_node),与px4_msgs_old(历史消息归档包)一起随 PX4 源码提供。安装步骤如下:

1.(可选)创建 ROS 2 工作区

mkdir -p /path/to/ros_ws/src

2. 使用辅助脚本把消息定义与翻译节点拷贝进工作区

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.bash

4. 运行翻译节点

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静态常量携带版本信息,可以通用地把版本后缀拼接到主题名上:

C++

topic_name + "_v" + std::to_string(T::MESSAGE_VERSION)

Python

topic_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" + getMessageNameVersion<px4_msgs::msg::VehicleAttitude>(); const std::string pub_topic = "/fmu/in/vehicle_command" + getMessageNameVersion<px4_msgs::msg::VehicleCommand>(); _subscription = this->create_subscription<px4_msgs::msg::VehicleAttitude>( sub_topic, 10, std::bind(&MinimalPubSub::attitude_callback, this, std::placeholders::_1)); _publisher = this->create_publisher<px4_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::Publisher<px4_msgs::msg::VehicleCommand>::SharedPtr _publisher; rclcpp::Subscription<px4_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时返回空字符串,即不添加_v<version>后缀(这也是 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 translation):n个输入消息与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.msg~VehicleStatusV3.msg。示例结构:

... msg/ └── px4_msgs_old/ ├── msg/ ├── VehicleAttitudeV1.msg ├── VehicleAttitudeV2.msg └── VehicleGlobalPositionV1.msg └── srv/ └── VehicleCommandV1.srv

translation_node/

  • 存放所有消息版本之间的翻译头文件;
  • 每个翻译(直接或通用)是一个.h头文件;
  • all_translations.h 作为总入口头文件,include 了全部翻译头文件。

当前仓库 translations 目录 中已有 20 余个实际翻译(如translation_vehicle_status_v1.h~translation_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 = TypesArray<ROS_MSG_OLDER_1, ROS_MSG_OLDER_2, ...>; static constexpr const char* kTopicsOlder[] = { "fmu/out/msg_1", "fmu/out/msg_2", ... }; using MessagesNewer = TypesArray<ROS_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 中的RosTranslationNode):

  • PubSubGraph:维护主题发布/订阅的翻译图(pub_sub_graph.h);
  • ServiceGraph:维护服务请求/响应的翻译图(service_graph.h);
  • Monitor:动态监控 DDS 数据空间,检测外部发布者/订阅者/服务并触发图更新(monitor.h)。

翻译节点动态监控主题与服务,并按要求实例化对侧的发布/订阅:例如检测到某个主题版本 1 的外部发布者和版本 2 的外部订阅者时,节点便会在中间搭起翻译链路。

图的构建与遍历

节点内部维护一张“所有已知主题-版本元组”的图,图节点是主题-版本元组,图的边是消息翻译(graph.h 中MessageIdentifier{topic_name, version}即节点标识)。由于可以注册任意消息翻译,图可能存在环,且两个节点之间可能有多条路径;因此每次主题更新时,使用 BFS 最短路径算法遍历图(graph.h#L201-L240 的translate()方法)。从一个节点移动到下一个节点时,会以当前主题数据调用消息翻译方法;如果某节点因之前检测到外部订阅者而实例化了发布者,则发布数据。这样,同一主题任意版本的多个订阅者都能获得正确版本的数据。

图的数据结构要点:

  • MessageNode代表一个消息端点,TranslationNode夹在消息节点之间,可拥有最多 32 个输入(kMaxNumInputs,见 graph.h#L96);
  • TranslationNode用std::bitset跟踪各输入是否就绪(setInputReady/translate),对多输入翻译,只有全部输入消息就绪后翻译才继续(graph.h#L83-L90);
  • 遍历时以翻译节点为“屏障”:仅当所有输入就绪才继续展开输出节点,同时跳过已访问节点,防止沿原路反向翻译造成死循环。

翻译的注册机制

翻译类通过宏注册到单例RegisteredTranslations(translation_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>_v<version>主题命名约定,以“动态监控 + 翻译图 + 最短路径遍历”的方式,在运行时自动完成新旧消息版本间的双向转换。对应用开发者而言,只需依据消息类型的MESSAGE_VERSION拼接主题后缀即可获得跨版本兼容;对固件开发者而言,升级版本化消息只需遵循“归档旧定义 → 更新引用 → 递增版本号 → 新增翻译头 → 注册到all_translations.h”五步流程。结合仓库中的模板头文件、真实翻译示例与单元测试,可以快速为 PX4 新增自己的消息版本翻译。

  • 嵌入式
  • 物联网
  • 机器人
  • 自动驾驶
  • 智能硬件

【免费下载链接】PX4-Autopilot

PX4 Autopilot Software

项目地址:https://gitcode.com/gh_mirrors/px/PX4-Autopilot
点击查看免费下载

相关推荐

上一篇:IceCream的代码审查:提升代码质量的流程
下一篇:3 种实战案例:MobileViT-v2 在工业检测中的应用

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

返回列表