ROS1与ROS2双版本兼容开发实践与架构设计
发布时间:2026/11/4 8:17:32 锦皓数字建站

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)关键差异点依赖声明方式不同catkin_COMPONENTS vs ament_target_dependencies导出机制不同catkin_package() vs ament_export_dependencies()安装规则不同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::functionvoid(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 #endif4. 核心组件兼容实现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_clientT(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 format3 namedual_ros_pkg/name !-- ROS1依赖 -- depend condition$ROS_VERSION 1roscpp/depend depend condition$ROS_VERSION 1std_msgs/depend !-- ROS2依赖 -- depend condition$ROS_VERSION 2rclcpp/depend depend condition$ROS_VERSION 2std_msgs/depend /package6. 实战案例兼容性导航模块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_subscriptionnav_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::Subscriptionnav_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_parameterT(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.8msCPU占用率15%12%内存占用85MB92MB8. 迁移路线图建议对于正在使用ROS1的项目建议采用分阶段迁移策略兼容层开发阶段2-4周实现核心通信接口的兼容层建立双版本CI测试流水线并行运行阶段1-3个月新功能同时在两个版本上开发和测试逐步替换旧模块为兼容版本完全迁移阶段视项目而定当所有依赖都支持ROS2时完成切换保留ROS1兼容分支作为fallback9. 常见问题解决方案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. 进阶优化方向对于追求更高性能的场景可以考虑零拷贝优化利用ROS2的loaned messages特性QoS策略统一在兼容层实现QoS配置映射组件化封装将兼容层打包为独立组件我在实际项目中发现最耗时的往往不是技术实现而是团队成员的思维转换。建议定期组织内部培训分享两个版本的特性对比和使用经验。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。