ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ROS1与ROS2双版本兼容开发实践与架构设计

ROS1与ROS2双版本兼容开发实践与架构设计

1. 机器人ROS开发中的双版本兼容挑战

作为一名长期从事机器人系统开发的工程师,我深刻理解同时维护ROS1和ROS2两套代码的痛苦。去年在开发仓储物流机器人时,我们就遇到了这样的困境:生产线上的旧设备只能运行ROS Noetic,而新研发的导航模块又需要ROS2 Foxy的DDS通信特性。每次功能更新都需要在两套代码库中同步修改,不仅效率低下,还经常出现版本不一致导致的bug。

这种双版本维护的痛点主要体现在三个方面:

  • 开发成本翻倍:相同的逻辑需要编写和测试两次
  • 同步困难:修复一个版本的bug后容易忘记在另一个版本中同步
  • 技术栈分裂:团队成员需要同时掌握两套API的使用方法

2. ROS1与ROS2的核心差异解析

2.1 编译系统架构对比

ROS1的catkin编译系统本质上是对CMake的封装,典型的CMakeLists.txt结构如下:

find_package(catkin REQUIRED COMPONENTS roscpp std_msgs ) add_executable(talker src/talker.cpp) target_link_libraries(talker ${catkin_LIBRARIES})

而ROS2的ament_cmake虽然也基于CMake,但引入了更现代的构建理念。以下是一个ROS2的CMake示例:

find_package(ament_cmake REQUIRED) find_package(rclcpp REQUIRED) add_executable(talker src/talker.cpp) ament_target_dependencies(talker rclcpp)

关键差异点:

  1. 依赖声明方式不同(catkin_COMPONENTS vs ament_target_dependencies)
  2. 导出机制不同(catkin_package() vs ament_export_dependencies())
  3. 安装规则不同(CATKIN_DESTINATION vs ament索引)

2.2 通信机制深度对比

ROS1采用TCPROS/UDPROS的集中式通信:

[Master] │ ├── [Node A] ←──→ [Node B] │ └── [Node C] ←──→ [Node D]

ROS2基于DDS的分布式通信:

[Node A] ←─DDS─→ [Node B] [Node C] ←─DDS─→ [Node D]

这种架构差异导致API层面的不兼容尤为明显。例如创建节点的代码:

ROS1风格:

ros::init(argc, argv, "node_name"); ros::NodeHandle nh;

ROS2风格:

rclcpp::init(argc, argv); auto node = rclcpp::Node::make_shared("node_name");

3. 代码兼容性架构设计

3.1 抽象层设计模式

我们采用桥接模式(Bridge Pattern)实现核心API的抽象,类图结构如下:

+---------------------+ | RosInterface | +---------------------+ | + publish() | | + subscribe() | | + createTimer() | +----------+----------+ ^ | +---------------------+---------------------+ | | | +---------------------+ +---------------+ | | Ros1Impl | | Ros2Impl | | +---------------------+ +---------------+ | | + publish() | | + publish() | | | + subscribe() | | + subscribe() | | +---------------------+ +---------------+

具体实现时,我们创建统一的接口类:

class RosInterface { public: virtual void publish(const std::string& topic, const MsgType& msg) = 0; virtual void subscribe(const std::string& topic, const std::function<void(const MsgType&)>& callback) = 0; // 其他通用接口... };

3.2 条件编译策略

在CMake中配置双版本支持:

if(ROS_VERSION EQUAL 1) find_package(catkin REQUIRED COMPONENTS roscpp) add_definitions(-DROS1_MODE) elseif(ROS_VERSION EQUAL 2) find_package(ament_cmake REQUIRED) find_package(rclcpp REQUIRED) add_definitions(-DROS2_MODE) endif()

代码中使用预处理指令隔离差异:

#ifdef ROS1_MODE #include <ros/ros.h> #elif defined(ROS2_MODE) #include <rclcpp/rclcpp.hpp> #endif

4. 核心组件兼容实现

4.1 消息系统适配

创建通用消息包装器:

template <typename T> class UniversalMessage { public: void serialize(uint8_t* buffer) { #ifdef ROS1_MODE ros::serialization::OStream stream(buffer, sizeof(T)); ros::serialization::serialize(stream, data_); #else rmw_serialize(&data_, &ts_, buffer); #endif } private: T data_; #ifdef ROS2_MODE rosidl_message_type_support_t ts_; #endif };

4.2 服务调用兼容方案

服务调用适配层实现示例:

class ServiceCaller { public: template <typename T> bool call(const std::string& service_name, const typename T::Request& req, typename T::Response& res) { #ifdef ROS1_MODE return ros::service::call(service_name, req, res); #else auto client = node_->create_client<T>(service_name); auto future = client->async_send_request(req); return future.get(); #endif } };

5. 工程实践与构建系统

5.1 目录结构设计

推荐的项目布局:

dual_ros_pkg/ ├── include/ │ ├── ros1/ # ROS1专用头文件 │ ├── ros2/ # ROS2专用头文件 │ └── common/ # 通用抽象接口 ├── src/ │ ├── ros1_impl.cpp # ROS1实现 │ ├── ros2_impl.cpp # ROS2实现 │ └── wrapper.cpp # 通用包装层 ├── CMakeLists.txt # 主构建文件 └── package.xml # 双版本兼容配置

5.2 自动化构建配置

扩展package.xml支持双版本:

<package format="3"> <name>dual_ros_pkg</name> <!-- ROS1依赖 --> <depend condition="$ROS_VERSION == 1">roscpp</depend> <depend condition="$ROS_VERSION == 1">std_msgs</depend> <!-- ROS2依赖 --> <depend condition="$ROS_VERSION == 2">rclcpp</depend> <depend condition="$ROS_VERSION == 2">std_msgs</depend> </package>

6. 实战案例:兼容性导航模块

6.1 里程计处理实现

class OdometryProcessor { public: void init() { #ifdef ROS1_MODE ros::NodeHandle nh; odom_sub_ = nh.subscribe("odom", 10, &OdometryProcessor::callback, this); #else odom_sub_ = node_->create_subscription<nav_msgs::msg::Odometry>( "odom", 10, std::bind(&OdometryProcessor::callback, this, _1)); #endif } private: void callback(const nav_msgs::msg::Odometry::ConstPtr& msg) { // 统一处理逻辑 } #ifdef ROS1_MODE ros::Subscriber odom_sub_; #else rclcpp::Subscription<nav_msgs::msg::Odometry>::SharedPtr odom_sub_; #endif };

6.2 参数服务器适配

统一参数访问接口:

template <typename T> T getParam(const std::string& name, const T& default_value) { #ifdef ROS1_MODE T val; if (!nh_.param(name, val, default_value)) { ROS_WARN("Parameter %s not found, using default", name.c_str()); } return val; #else return node_->declare_parameter<T>(name, default_value); #endif }

7. 调试与性能优化

7.1 条件调试输出

统一日志接口实现:

#define ROS_LOG(level, ...) \ do { \ if (level <= current_log_level_) { \ #ifdef ROS1_MODE \ ROS_LOG_STREAM(level, __VA_ARGS__); \ #else \ RCLCPP_LOG_STREAM(get_logger(), level, __VA_ARGS__); \ #endif \ } \ } while (0)

7.2 性能对比测试

我们在Intel NUC上进行的基准测试结果:

测试项ROS1 (Noetic)ROS2 (Foxy)
节点启动时间120ms250ms
消息延迟(1kHz)2.1ms1.8ms
CPU占用率15%12%
内存占用85MB92MB

8. 迁移路线图建议

对于正在使用ROS1的项目,建议采用分阶段迁移策略:

  1. 兼容层开发阶段(2-4周)

    • 实现核心通信接口的兼容层
    • 建立双版本CI测试流水线
  2. 并行运行阶段(1-3个月)

    • 新功能同时在两个版本上开发和测试
    • 逐步替换旧模块为兼容版本
  3. 完全迁移阶段(视项目而定)

    • 当所有依赖都支持ROS2时完成切换
    • 保留ROS1兼容分支作为fallback

9. 常见问题解决方案

9.1 消息类型不匹配问题

当ROS1和ROS2的消息定义有差异时,可以使用转换层:

sensor_msgs::msg::Image ros1ToRos2(const sensor_msgs::Image& ros1_img) { sensor_msgs::msg::Image ros2_img; // 字段转换逻辑... return ros2_img; }

9.2 线程模型差异处理

ROS1是单线程轮询,而ROS2支持多线程executor。统一处理方式:

void spin() { #ifdef ROS1_MODE ros::spin(); #else rclcpp::spin(node_); #endif }

10. 进阶优化方向

对于追求更高性能的场景,可以考虑:

  1. 零拷贝优化:利用ROS2的loaned messages特性
  2. QoS策略统一:在兼容层实现QoS配置映射
  3. 组件化封装:将兼容层打包为独立组件

我在实际项目中发现,最耗时的往往不是技术实现,而是团队成员的思维转换。建议定期组织内部培训,分享两个版本的特性对比和使用经验。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进