ros_gz_bridge---底层通信的实现

在现代机器人开发流程中,仿真先行 已经成为行业标准。ROS 2作为机器人软件的事实标准,与新一代Gazebo(原Ignition Gazebo)仿真器的无缝集成,是构建可靠机器人系统的关键。而ros_gz_bridge正是连接这两个世界的核心桥梁。

一、ros_gz_bridge桥接层的价值

ROS 2和Gazebo是两个完全独立、设计目标不同的软件系统,拥有各自独立的通信栈

新版之后变成完全独立了,之前gazebo直接嵌入ros2的时候是可以直接通过ros2的通信方式进行数据传递的。不过旧版耦合比较严重,容易牵连崩溃。

1.1 两个异构通信系统的本质差异

特性 ROS 2通信系统 Gazebo通信系统
底层中间件 DDS(Data Distribution Service) Ignition Transport
消息定义 ROS IDL(Interface Definition Language) Google Protocol Buffers
传输协议 UDP/IP(默认)、TCP/IP、共享内存 TCP/IP(控制)、UDP/IP(数据)、共享内存
QoS模型 丰富的QoS策略(可靠性、持久性、 deadline等) 简化的QoS策略
设计目标 分布式机器人系统通信 单机/集群仿真通信

这两个系统在序列化格式、传输协议、QoS模型、命名空间等方面完全不兼容。如果没有桥接层,ROS 2节点无法看到Gazebo发布的任何数据,Gazebo也无法接收ROS 2发送的任何控制指令。

1.2 桥接层的核心使命

ros_gz_bridge的核心使命就是:在两个异构通信系统之间建立透明的消息转发通道,让ROS 2和Gazebo能够像与同系统内的节点通信一样互相通信

它的设计理念是简单、通用、可扩展

  • 无需修改ROS 2或Gazebo的源码
  • 无需编写自定义通信代码
  • 通过简单的参数配置即可实现任意话题、服务、动作的桥接
  • 支持双向通信和动态配置

二、单进程双通信栈模型 架构

ros_gz_bridge最精妙的设计就是它的单进程双通信栈架构。它没有采用复杂的跨进程通信或网络代理,而是在同一个进程内同时运行ROS 2和Gazebo两套完整的通信栈,通过内存拷贝实现消息的高效转换。
ros_gz_bridge 进程
Gazebo 通信栈
ROS 2 通信栈
内存拷贝
ignition-transport
rclcpp
DDS
rosidl
Protobuf
gz-msgs

2.1 架构优势

  1. 高性能消息转换只需要内存拷贝,避免了跨进程通信的开销
  2. 低延迟:没有额外的网络转发,延迟控制在微秒级
  3. 高可靠性:单进程模型减少了故障点
  4. 易于部署:只需启动一个节点即可完成所有桥接工作

2.2 核心依赖库

ros_gz_bridge之所以能同时连接两个通信模块,是因为它在编译时链接了两个完全独立的通信库:

  • ROS 2端

    • rclcpp:ROS 2的C++客户端库,提供节点、发布者、订阅者等抽象
    • rosidl:ROS 2的消息定义和代码生成系统
    • rmw:ROS 2中间件抽象层,屏蔽不同DDS实现的差异
  • Gazebo端

    • ignition-transport:Gazebo的轻量级通信系统
    • gz-msgs:Gazebo的标准消息库(基于Protobuf)
    • protobuf:Google的序列化库

三、核心组件

ros_gz_bridge的源码非常精简,核心代码不到2000行,主要由以下四个核心组件组成:

3.1 参数解析器:BridgeConfig

parameter_bridge可执行文件的入口是parameter_bridge.cpp,它首先会解析命令行参数,生成BridgeConfig结构体。

3.1.1 桥接参数完整格式

除了基本的桥接参数格式,parameter_bridge还支持非常丰富的配置选项:

ROS话题名 @ ROS类型 方向 Gazebo话题名 @ Gazebo类型 : QoS配置

复制代码
<ros_topic_name>[@<ros_type_name>[<direction><gz_topic_name>[@<gz_type_name>]]][:<qos_profile>]

中括弧在上述表示式子中只表示分块,与Linux常用命令分割表示规则一样,不要误以为是direction 。

其中:

  • ros_topic_name:ROS 2话题名称(必填)
  • ros_type_name:ROS 2消息类型(必填)
  • direction:通信方向([=Gazebo→ROS2,]=ROS2→Gazebo,==双向)
  • gz_topic_name:Gazebo话题名称(可选,默认与ROS 2话题名相同)
  • gz_type_name:Gazebo消息类型(必填)
  • qos_profile:QoS配置文件(可选)

例如,关节状态桥接参数:

复制代码
话题名 @ ROS消息类型 [ Gazebo消息类型
fanuc_m20ia_joint_state@sensor_msgs/msg/JointState[gz.msgs.Model

会被解析成:

cpp 复制代码
struct BridgeConfig {
  std::string ros_topic_name = "fanuc_m20ia_joint_state";
  std::string gz_topic_name = "fanuc_m20ia_joint_state";
  std::string ros_type_name = "sensor_msgs/msg/JointState";
  std::string gz_type_name = "gz.msgs.Model";
  BridgeDirection direction = BRIDGE_GZ_TO_ROS;
  std::string qos_profile = "default";
};
3.1.2 QoS配置支持

ros_gz_bridge 0.24.0版本开始,支持指定QoS配置文件:

复制代码
/scan@sensor_msgs/msg/LaserScan[gz.msgs.LaserScan:SensorDataQoS

支持的QoS配置文件包括:

  • default:默认QoS(可靠传输,volatile durability)
  • SensorDataQoS:传感器数据QoS(尽力传输,volatile durability)
  • ParametersQoS:参数服务QoS(可靠传输,transient local durability)
  • ServicesQoS:服务QoS(可靠传输,volatile durability)

3.2 消息转换器工厂:Factory

Factory实现了无需手动编写桥接代码,就能自动支持任意ROS 2和Gazebo消息类型的转换。

3.2.1 实现原理:模板特化+静态注册

ros_gz_bridge采用了工厂模式+模板特化 的设计,为每一对支持的消息类型定义一个转换函数,并在程序启动时自动注册到工厂中

工厂类的核心定义如下:

cpp 复制代码
class Factory {
public:
  // 1.获取工厂单例
  static Factory& Instance();

  // 2.注册转换函数
  template<typename RosT, typename GzT>
  void Register(
    const std::string& ros_type_name,
    const std::string& gz_type_name,
    std::function<void(const GzT&, RosT&)> gz_to_ros = nullptr,
    std::function<void(const RosT&, GzT&)> ros_to_gz = nullptr);

  // 3.获取(匹配)转换函数(模板特化+静态注册)
  ConverterFunc GetGzToRosConverter(
    const std::string& gz_type_name,
    const std::string& ros_type_name);

  ConverterFunc GetRosToGzConverter(
    const std::string& ros_type_name,
    const std::string& gz_type_name);

private:
  Factory() = default;
  std::unordered_map<std::string, ConverterFunc> gz_to_ros_converters_;
  std::unordered_map<std::string, ConverterFunc> ros_to_gz_converters_;
};

一个转换文件 = 模板特化 + 末尾静态注册,固定流程如下:

cpp 复制代码
// 1. 头文件
#include <factory.h>
#include <sensor_msgs/msg/joint_state.h>
#include <gz/msgs/model.h>

// 2. 模板特化函数(核心转换逻辑)
template<>
void Factory::Register<sensor_msgs::msg::JointState, gz::msgs::Model>()
{
  RegisterConverter(
    "sensor_msgs/msg/JointState",
    "gz.msgs.Model",
    [](auto& gz_msg, auto& ros_msg) {
        // 转换逻辑
    },
    nullptr
  );
}

// 3. 静态自动注册(写在文件最后!)
static bool g_registered = []() {
  Factory::Instance().Register<
    sensor_msgs::msg::JointState,
    gz::msgs::Model
  >();
  return true;
}();

gz.msgs.Modelsensor_msgs/msg/JointState的转换为例,注册代码如下:

模板特化函数

cpp 复制代码
// 在conversions/model_jointstate.cpp中
template<>
void Factory::Register<sensor_msgs::msg::JointState, gz::msgs::Model>()
{
  RegisterConverter(
    "sensor_msgs/msg/JointState",
    "gz.msgs.Model",
    // Gazebo → ROS 2转换函数
    [](const gz::msgs::Model& gz_msg, sensor_msgs::msg::JointState& ros_msg) {
      // 转换时间戳
      ros_msg.header.stamp.sec = gz_msg.header().stamp().sec();
      ros_msg.header.stamp.nanosec = gz_msg.header().stamp().nsec();
      ros_msg.header.frame_id = gz_msg.header().data("frame_id");

      // 提取所有关节数据
      for (const auto& joint : gz_msg.joint()) {
        ros_msg.name.push_back(joint.name());
        
        if (joint.has_position()) {
          ros_msg.position.push_back(joint.position());
        }
        
        if (joint.has_velocity()) {
          ros_msg.velocity.push_back(joint.velocity());
        }
        
        if (joint.has_effort()) {
          ros_msg.effort.push_back(joint.effort());
        }
      }
    },
    // ROS 2 → Gazebo转换函数(本例不支持)
    nullptr
  );
}
3.2.2 静态自动注册机制

为了实现添加新转换无需修改工厂代码ros_gz_bridge使用了静态变量初始化自动注册 的技巧:

末尾静态注册

cpp 复制代码
// 在每个转换文件的末尾添加
static bool g_registered = []() {
  Factory::Instance().Register<sensor_msgs::msg::JointState, gz::msgs::Model>();
  return true;
}();

当程序启动时,所有静态变量会被初始化,从而自动将转换函数注册到工厂中。这种设计极大地提高了代码的可扩展性。

3.3 双向桥接器:Bridge

Bridge类是实际执行桥接逻辑的地方,它会根据BridgeConfig创建对应的发布者和订阅者。ros_gz_bridge提供了三种桥接器实现:

  • GzToRosBridge:Gazebo→ROS 2单向桥接
  • RosToGzBridge:ROS 2→Gazebo单向桥接
  • BidirectionalBridge:双向桥接
3.3.1 通用发布者与订阅者

ros_gz_bridge使用了ROS 2 Humble引入的通用发布者/订阅者GenericPublisher/GenericSubscription)特性,这是实现动态桥接的关键。

传统的ROS 2发布者/订阅者需要在编译时知道消息类型:

cpp 复制代码
// 编译时确定消息类型
auto pub = node->create_publisher<sensor_msgs::msg::JointState>("/joint_states", 10);

而通用发布者/订阅者可以在运行时动态创建任意类型的发布者和订阅者:

cpp 复制代码
// 运行时确定消息类型
auto pub = node->create_generic_publisher(
  "/joint_states",
  "sensor_msgs/msg/JointState",
  rclcpp::QoS(10)
);

通用发布者/订阅者的底层原理是:

  1. 在运行时通过rosidl获取消息类型的运行时类型信息
  2. 动态创建消息的序列化和反序列化函数
  3. 直接操作序列化后的二进制数据
3.3.2 Gazebo→ROS 2桥接核心实现
cpp 复制代码
class GzToRosBridge : public Bridge
{
public:
  GzToRosBridge(
    const BridgeConfig& config,
    rclcpp::Node::SharedPtr ros_node,
    ignition::transport::Node& gz_node)
    : config_(config)
  {
    // 1. 创建ROS 2通用发布者
    ros_pub_ = ros_node->create_generic_publisher(
      config.ros_topic_name,
      config.ros_type_name,
      parse_qos_profile(config.qos_profile)
    );

    // 2. 创建Gazebo订阅者
    if (!gz_node.Subscribe(
      config.gz_topic_name,
      &GzToRosBridge::GzMessageCallback,
      this))
    {
      throw std::runtime_error("Failed to subscribe to Gazebo topic: " + config.gz_topic_name);
    }

    // 3. 从工厂获取转换函数
    converter_ = Factory::Instance().GetGzToRosConverter(
      config.gz_type_name,
      config.ros_type_name
    );

    if (!converter_) {
      throw std::runtime_error(
        "No converter found for " + config.gz_type_name + " → " + config.ros_type_name);
    }
  }

private:
  void GzMessageCallback(const std::string& serialized_data)
  {
    // 1. 反序列化Gazebo Protobuf消息
    auto gz_msg = Factory::Instance().CreateGzMessage(config_.gz_type_name);
    if (!gz_msg->ParseFromString(serialized_data)) {
      RCLCPP_ERROR(
        rclcpp::get_logger("GzToRosBridge"),
        "Failed to parse Gazebo message of type %s",
        config_.gz_type_name.c_str());
      return;
    }

    // 2. 创建ROS 2消息
    auto ros_msg = ros_pub_->create_message();

    // 3. 执行消息转换
    converter_(*gz_msg, *ros_msg);

    // 4. 发布到ROS 2话题
    ros_pub_->publish(*ros_msg);
  }

  BridgeConfig config_;
  rclcpp::GenericPublisher::SharedPtr ros_pub_;
  ConverterFunc converter_;
};

四、数据流全链路解析

现在把所有组件串起来,以FANUC M-20i机械臂关节状态为例,讲解从Gazebo发布数据到ROS 2接收数据的完整过程。

4.1 步骤1:Gazebo仿真器发布关节状态

  1. 物理引擎计算:Gazebo的物理引擎(默认是DART)计算出机械臂所有关节的位置、速度和力矩
  2. 关节状态发布插件gz::sim::JointStatePublisher系统插件将物理引擎的数据打包成gz::msgs::Model Protobuf消息
  3. 发布到Ignition Transport :通过ignition::transport::Node::Publish()方法将消息发布到话题fanuc_m20ia_joint_state
  4. 本地传输 :消息通过本地TCP套接字发送到ign-transport守护进程

4.2 步骤2:ros_gz_bridge接收Gazebo消息

  1. 订阅回调触发ros_gz_bridgeignition::transport::Node从守护进程接收二进制序列化数据
  2. Protobuf反序列化 :调用gz::msgs::Model::ParseFromString()将二进制数据反序列化成C++对象
  3. 错误检查:验证消息的完整性和有效性

4.3 步骤3:消息格式转换

  1. 获取转换函数 :从Factory单例中获取gz.msgs.Modelsensor_msgs/msg/JointState的转换函数
  2. 字段映射
    • 将Gazebo的gz::msgs::Time转换为ROS 2的builtin_interfaces::msg::Time
    • 遍历gz::msgs::Model中的所有joint字段
    • 提取关节名称、位置、速度、力矩,填充到sensor_msgs::msg::JointState对象中
  3. 数据校验:确保转换后的消息符合ROS 2消息规范

4.4 步骤4:发布到ROS 2话题

  1. ROS 2序列化rclcpp::GenericPublishersensor_msgs::msg::JointState对象序列化成DDS格式
  2. DDS发布 :通过DDS中间件将序列化后的消息发布到ROS 2话题/fanuc_m20ia_joint_state
  3. ROS 2节点接收 :所有订阅了该话题的ROS 2节点(如robot_state_publishermoveit2)都会收到消息

五、性能优化

在实际工程中,ros_gz_bridge往往是仿真系统的性能瓶颈之一。以下是经过验证的性能优化技巧:

5.1 性能瓶颈分析

ros_gz_bridge的主要性能开销来自三个方面:

  1. 两次序列化/反序列化:Gazebo Protobuf→C++对象→ROS 2 DDS
  2. 内存拷贝:消息字段的逐个拷贝
  3. 线程切换:ROS 2和Gazebo都有自己的线程池

在极端情况下(如高频率点云数据),序列化/反序列化的开销可能占到总CPU使用率的50%以上。

5.2 优化技巧

1. 启用共享内存传输

ROS 2和Gazebo都支持共享内存传输,可以完全避免网络栈的开销:

  • ROS 2端:使用FastDDS的共享内存传输

    xml 复制代码
    <!-- 在fastdds.xml中配置 -->
    <transport_descriptors>
      <transport_descriptor>
        <transport_id>shm_transport</transport_id>
        <type>SHM</type>
      </transport_descriptor>
    </transport_descriptors>
  • Gazebo端:启用Ignition Transport的共享内存传输

    bash 复制代码
    export IGN_TRANSPORT_SHARED_MEMORY=1

启用共享内存后,消息传输延迟可以降低一个数量级。

2. 合理配置QoS
  • 对于高频传感器数据(如关节状态、IMU、激光雷达),使用SensorDataQoS(尽力传输)
  • 对于低频控制指令(如关节轨迹),使用默认QoS(可靠传输)
  • 避免使用transient local durability,除非确实需要历史数据
3. 减少桥接话题数量

只桥接真正需要的话题,避免桥接不必要的高频数据。例如,Gazebo默认会发布很多调试话题,这些话题在生产环境中不需要桥接。

4. 使用静态桥接

对于固定的桥接配置,可以使用ros_gz_bridge提供的静态桥接模式,避免运行时动态创建的开销:

cpp 复制代码
// 静态桥接代码示例
#include <ros_gz_bridge/bridge.hpp>

int main(int argc, char* argv[]) {
  rclcpp::init(argc, argv);
  auto node = rclcpp::Node::make_shared("static_bridge");
  
  ignition::transport::Node gz_node;
  
  // 创建静态桥接,静态消息类型写死固定
  ros_gz_bridge::create_gz_to_ros_bridge<sensor_msgs::msg::JointState, gz::msgs::Model>(
    node,
    gz_node,
    "/fanuc_m20ia_joint_state",
    "/joint_states",
    rclcpp::SensorDataQoS()
  );
  
  rclcpp::spin(node);
  rclcpp::shutdown();
  return 0;
}

静态桥接不需要运行时类型检查和转换函数查找,性能比动态桥接高10%-20%。

静态桥接 vs 动态参数桥接 对比表

对比维度 静态桥接 动态参数桥接(parameter_bridge 命令行版)
配置方式 代码里硬编码话题、消息类型 命令行/Launch/YAML 传参配置,不改代码
消息类型绑定 C++模板编译期写死,类型编译检查 运行时字符串解析绑定,无编译检查
话题名称 代码写死,运行时不能改 随时改命令行/配置即可,无需编译
新增/删减桥接 必须改代码、重新编译 直接加一行参数即可,秒级生效
运行时灵活性 极低,拓扑固定死 极高,支持多话题随意组合桥接
性能开销 无运行时参数解析、字符串匹配,延迟更低 有参数解析、动态注册开销,略慢一点
类型安全性 编译期类型校验,错类型直接编译报错 运行时才发现类型不匹配,容易崩溃
资源占用 更小,依赖少、无多余解析逻辑 稍大,内置参数解析、动态适配逻辑
适用场景 量产机器人、固定仿真拓扑、追求低延迟、正式部署 调试开发、临时测试、多机器人灵活拓扑、快速试错
维护方式 版本固化,适配硬件后不再改动 不用改源码,靠配置就能切换桥接关系

5.3 常见问题排查

1. 话题存在但没有数据
  • 检查桥接方向是否正确([]不要搞反)
  • 检查消息类型是否匹配(ROS 2和Gazebo的消息类型必须一一对应)
  • 检查Gazebo是否真的在发布数据(使用gz topic echo命令)
  • 检查防火墙是否阻止了本地通信
2. 时间戳错乱
  • 确保启用了use_sim_time=True
  • 确保时钟桥接正常工作(/clock话题有数据)
  • 检查Gazebo的仿真时间是否在正常运行
3. 消息延迟高
  • 启用共享内存传输
  • 降低消息发布频率
  • 使用SensorDataQoS代替默认QoS
  • 检查是否有过多的节点订阅同一个话题

六、高级特性

除了基本的话题桥接,ros_gz_bridge还支持一些高级特性:

6.1 服务桥接

ros_gz_bridge支持ROS 2服务与Gazebo服务之间的桥接:

ROS2 有自己的 service/client,Gazebo(gz-transport)也有自己的 request/response

  • 从 ROS2 发服务请求 → bridge → Gazebo 执行 → 回复原路返回
  • 从 Gazebo 发请求 → bridge → ROS2 服务执行 → 回复原路返回
python 复制代码
service_bridge = Node(
    package='ros_gz_bridge',
    executable='service_bridge',
    arguments=['/set_entity_state@ros_gz_interfaces/srv/SetEntityState[gz.msgs.EntityState'],
    output='screen'
)

6.2 动作桥接

ROS2 Action ⇄ bridge ⇄ Gazebo 的 "长时任务 + 反馈" 接口

ros_gz_bridge 0.25.0版本开始,支持ROS 2动作与Gazebo动作之间的桥接:

python 复制代码
action_bridge = Node(
    package='ros_gz_bridge',
    executable='action_bridge',
    arguments=['/follow_joint_trajectory@control_msgs/action/FollowJointTrajectory[gz.msgs.FollowJointTrajectory'],
    output='screen'
)

6.3 命名空间重映射

可以将Gazebo的长话题名重映射为ROS 2的短话题名:

python 复制代码
joint_state_bridge = Node(
    package='ros_gz_bridge',
    executable='parameter_bridge',
    arguments=['/joint_states@sensor_msgs/msg/JointState[gz.msgs.Model@/fanuc_m20ia_joint_state'],
    output='screen'
)

这样ROS 2端就可以通过/joint_states话题接收关节数据,而不需要使用冗长的Gazebo话题名。


总结

ros_gz_bridge是ROS 2与Gazebo集成的核心基石,它的设计理念是简单、通用、可扩展。通过在同一个进程内同时运行两套通信栈,它用最少的代码实现了两个异构系统之间的无缝通信。

参考资料

相关推荐
2zcode7 小时前
基于STM32的智能扫地机器人设计与实现
stm32·嵌入式硬件·机器人
Jasmine_llq7 小时前
《B4261 [GESP202503 三级] 2025》
开发语言·c++·算法·条件判断算法·位运算恒等式推导·简单算术运算
小张成长计划..7 小时前
【C++】32:智能指针
c++
砺星Leetx7 小时前
砺星伺服压机整线18台落地某头部新能源车企电驱动产线,轴承压装CT从13秒降至8秒
机器人·自动化·汽车·制造
咩咦7 小时前
C++学习笔记19:运算符重载基础与赋值运算符重载
c++·学习笔记·类和对象·运算符重载·赋值运算符·operator
无限进步_8 小时前
C++异常机制:抛出、捕获与栈展开
开发语言·c++·安全
数智工坊8 小时前
【UniT论文阅读】:用统一物理语言打通人类与人形机器人的知识壁垒
论文阅读·人工智能·深度学习·算法·机器人
王老师青少年编程8 小时前
csp信奥赛C++高频考点专项训练之前缀和&差分 --【一维前缀和】:宝石串
c++·前缀和·csp·高频考点·信奥赛·宝石串
梓䈑8 小时前
【算法题攻略】模拟
c++·算法