在前两章中,我们回顾了 ROS1 的核心架构并深入分析了其在实际工业应用中暴露的局限性。本章将聚焦于实际的迁移策略,为那些需要将现有 ROS1 系统升级到 ROS2 的团队提供系统化的指导。迁移不仅仅是代码层面的改写,更涉及架构设计、性能优化、团队协作等多个维度的考量。我们将通过 Toyota HSR(Human Support Robot)的实际迁移案例,展示如何在保证系统稳定性的前提下,逐步完成这一复杂的工程任务。
在开始任何迁移工作之前,首要任务是全面评估现有系统并制定合理的迁移策略。这个评估过程需要考虑技术债务、业务连续性、团队技能储备等多个因素。
复杂度评分 = α·节点数量 + β·自定义消息类型 + γ·外部依赖 + δ·实时性要求
其中权重系数 $\alpha = 0.2, \beta = 0.3, \gamma = 0.3, \delta = 0.2$ 是基于工业实践的经验值。
迁移到 ROS2 的主要收益包括:
ros1_bridge 是官方提供的 ROS1 和 ROS2 互操作性解决方案,它允许两个版本的节点在同一系统中共存并相互通信。这为渐进式迁移提供了技术基础。
ros1_bridge 本质上是一个双向协议转换器,它同时运行 ROS1 和 ROS2 的客户端库:
ROS1 Domain ROS2 Domain
┌──────────┐ ┌──────────┐
│ ROS1 Node│ │ ROS2 Node│
└─────┬────┘ └────┬─────┘
│ │
┌─────▼──────────────────────────▼─────┐
│ ros1_bridge │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ ROS1 Client│ │ ROS2 Client│ │
│ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │
│ ┌────▼─────────────────▼────┐ │
│ │ Message Type Mapping │ │
│ └───────────────────────────┘ │
└───────────────────────────────────────┘
桥接器维护了一个消息类型映射表,支持三种映射模式:
映射配置示例(YAML 格式):
ros1_package_name: 'geometry_msgs'
ros1_message_name: 'Twist'
ros2_package_name: 'geometry_msgs'
ros2_message_name: 'msg/Twist'
fields_1_to_2:
linear: 'linear'
angular: 'angular'
桥接器的性能开销主要来自:
性能优化策略:
动态桥接(默认模式):
静态桥接(参数化模式):
静态桥接启动示例:
ros2 run ros1_bridge parameter_bridge \
/cmd_vel@geometry_msgs/msg/Twist@geometry_msgs/Twist \
/scan@sensor_msgs/msg/LaserScan@sensor_msgs/LaserScan
从 ROS1 到 ROS2 的代码移植涉及 API 变化、编程范式转变和构建系统迁移等多个方面。
| ROS1 API | ROS2 API | 主要差异 |
|---|---|---|
ros::init() |
rclcpp::init() |
ROS2 支持多实例 |
ros::NodeHandle |
rclcpp::Node |
节点成为一等公民 |
ros::Publisher |
rclcpp::Publisher<T> |
强类型模板 |
ros::Subscriber |
rclcpp::Subscription<T> |
智能指针管理 |
ros::ServiceServer |
rclcpp::Service<T> |
类型安全增强 |
ros::Rate |
rclcpp::Rate |
支持多种时钟源 |
ros::Time::now() |
node->now() |
时间与节点关联 |
ros::param::get() |
node->declare_parameter() |
参数需要声明 |
ROS1 函数式风格:
// ROS1
void callback(const std_msgs::String::ConstPtr& msg) {
ROS_INFO("I heard: [%s]", msg->data.c_str());
}
int main(int argc, char** argv) {
ros::init(argc, argv, "listener");
ros::NodeHandle n;
ros::Subscriber sub = n.subscribe("chatter", 1000, callback);
ros::spin();
return 0;
}
ROS2 面向对象风格:
// ROS2
class MinimalSubscriber : public rclcpp::Node {
public:
MinimalSubscriber() : Node("minimal_subscriber") {
subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic", 10,
std::bind(&MinimalSubscriber::callback, this, std::placeholders::_1));
}
private:
void callback(const std_msgs::msg::String::SharedPtr msg) {
RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg->data.c_str());
}
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};
ROS2 的参数系统更加严格和类型安全:
// ROS1 - 运行时动态获取
double param;
nh.param<double>("my_param", param, 1.0);
// ROS2 - 需要先声明
this->declare_parameter("my_param", 1.0);
double param = this->get_parameter("my_param").as_double();
// ROS2 - 参数变更回调
auto param_callback = [this](const std::vector<rclcpp::Parameter>& params) {
for (const auto& param : params) {
if (param.get_name() == "my_param") {
// 处理参数更新
}
}
return rcl_interfaces::msg::SetParametersResult();
};
callback_handle_ = this->add_on_set_parameters_callback(param_callback);
服务迁移要点:
动作迁移差异:
构建系统的迁移是 ROS1 到 ROS2 转换中最具挑战性的部分之一。理解两个构建系统的差异对于顺利迁移至关重要。
Catkin (ROS1):
Colcon (ROS2):
架构差异示意:
Catkin 工作空间: Colcon 工作空间:
workspace/ workspace/
├── src/ ├── src/
│ ├── package1/ │ ├── package1/
│ └── package2/ │ └── package2/
├── build/ (共享) ├── build/
├── devel/ (开发空间) │ ├── package1/ (独立)
└── install/ (可选) │ └── package2/ (独立)
├── install/
└── log/ (构建日志)
ROS1 CMakeLists.txt 典型结构:
cmake_minimum_required(VERSION 3.0.2)
project(my_package)
find_package(catkin REQUIRED COMPONENTS
roscpp
std_msgs
message_generation
)
add_message_files(
FILES
MyMessage.msg
)
generate_messages(
DEPENDENCIES
std_msgs
)
catkin_package(
INCLUDE_DIRS include
LIBRARIES my_library
CATKIN_DEPENDS roscpp std_msgs message_runtime
)
include_directories(
include
${catkin_INCLUDE_DIRS}
)
add_library(my_library src/my_library.cpp)
add_executable(my_node src/my_node.cpp)
target_link_libraries(my_node ${catkin_LIBRARIES})
ROS2 CMakeLists.txt 对应结构:
cmake_minimum_required(VERSION 3.8)
project(my_package)
# 默认使用 C++14
if(NOT CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 14)
endif()
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
find_package(rosidl_default_generators REQUIRED)
# 消息生成
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/MyMessage.msg"
DEPENDENCIES std_msgs
)
# 库和可执行文件
add_library(my_library src/my_library.cpp)
ament_target_dependencies(my_library rclcpp std_msgs)
add_executable(my_node src/my_node.cpp)
ament_target_dependencies(my_node rclcpp std_msgs)
# 确保消息生成在编译前完成
rosidl_target_interfaces(my_node ${PROJECT_NAME} "rosidl_typesupport_cpp")
# 安装
install(TARGETS
my_library
my_node
DESTINATION lib/${PROJECT_NAME}
)
install(DIRECTORY include/
DESTINATION include
)
ament_package()
版本格式变化:
依赖声明差异:
<!-- ROS1 package.xml -->
<package format="2">
<name>my_package</name>
<version>0.0.1</version>
<description>Package description</description>
<buildtool_depend>catkin</buildtool_depend>
<build_depend>roscpp</build_depend>
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
<depend>std_msgs</depend>
</package>
<!-- ROS2 package.xml -->
<package format="3">
<name>my_package</name>
<version>0.0.1</version>
<description>Package description</description>
<buildtool_depend>ament_cmake</buildtool_depend>
<depend>rclcpp</depend>
<depend>std_msgs</depend>
<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
</package>
| 操作 | Catkin (ROS1) | Colcon (ROS2) |
|---|---|---|
| 构建所有包 | catkin_make |
colcon build |
| 构建单个包 | catkin_make --pkg <pkg> |
colcon build --packages-select <pkg> |
| 清理构建 | catkin_make clean |
rm -rf build install log |
| 并行构建 | catkin_make -j4 |
colcon build --parallel-workers 4 |
| 符号链接安装 | catkin_make install |
colcon build --symlink-install |
| 构建测试 | catkin_make run_tests |
colcon test |
| 查看构建日志 | catkin_make 2>&1 \| tee log |
cat log/latest_build/events.log |
ROS1 环境设置:
source /opt/ros/noetic/setup.bash
source ~/catkin_ws/devel/setup.bash
# ROS_PACKAGE_PATH 自动设置
ROS2 环境设置:
source /opt/ros/humble/setup.bash
source ~/ros2_ws/install/setup.bash
# AMENT_PREFIX_PATH 替代 ROS_PACKAGE_PATH
工作空间覆盖机制差异:
devel 空间覆盖 install 空间devel 概念,直接使用 install渐进式迁移是大型机器人系统从 ROS1 过渡到 ROS2 的推荐策略,它允许在保持系统运行的同时逐步完成迁移。
第一阶段:评估与准备(2-4 周)
任务清单:
□ 代码库依赖分析
□ 第三方包 ROS2 支持调研
□ 性能基准测试建立
□ 团队培训计划制定
□ 开发环境搭建(双版本共存)
第二阶段:基础设施迁移(4-8 周)
优先级顺序:
1. 消息和服务定义迁移
2. 构建系统迁移(CMake/Colcon)
3. 通用工具库迁移
4. 单元测试框架迁移
第三阶段:感知模块迁移(6-12 周)
迁移策略:
- 传感器驱动节点优先
- 使用 ros1_bridge 保持数据流
- 逐个替换处理节点
- 性能对比验证
第四阶段:控制与规划迁移(8-16 周)
关键考虑:
- 实时性验证
- 控制稳定性测试
- 故障注入测试
- 回滚方案准备
第五阶段:系统集成与优化(4-8 周)
收尾工作:
- 移除 ros1_bridge
- QoS 策略优化
- 生命周期管理完善
- 文档更新
在迁移过程中,混合系统架构是不可避免的。合理的架构设计可以最小化桥接开销:
┌─────────────────────────────────────────────────┐
│ System Monitor │
│ (ROS2 Lifecycle Manager) │
└─────────────┬───────────────────────────────────┘
│
┌─────────▼─────────┐ ┌─────────────────┐
│ Perception │ │ Planning │
│ (ROS2 Domain) │◄─────►│ (ROS1 Domain) │
└─────────┬─────────┘bridge └────────┬────────┘
│ │
┌─────────▼─────────────────────────▼────────┐
│ Hardware Abstraction │
│ (ROS2 Native) │
└─────────────────────────────────────────────┘
最小化桥接原则:
数据流重构示例:
原始架构 (纯 ROS1):
Camera → ImageProc → Detection → Tracking → Planning
过渡架构 (混合):
Camera(ROS2) → Bridge → ImageProc(ROS1) → Detection(ROS1) → Bridge → Planning(ROS2)
(性能损失: 2次桥接)
优化架构 (批量迁移):
Camera(ROS2) → ImageProc(ROS2) → Detection(ROS2) → Bridge → Planning(ROS1)
(性能损失: 1次桥接)
特性开关(Feature Toggle):
class DualStackNode {
bool use_ros2_impl_;
void process() {
if (use_ros2_impl_) {
process_ros2();
} else {
process_ros1_via_bridge();
}
}
};
双写验证(Shadow Mode):
监控指标:
Toyota Human Support Robot (HSR) 是服务机器人领域的标杆产品,其从 ROS1 到 ROS2 的迁移过程为业界提供了宝贵经验。
系统规模:
迁移驱动因素:
Toyota 团队采用了”核心先行,边缘跟进”的策略:
第一批迁移(3个月):
硬件抽象层 (HAL)
├── 电机驱动接口 [实时性关键]
├── IMU/编码器接口 [高频数据]
└── 急停系统 [安全关键]
选择理由:
- 直接受益于 ROS2 实时性改进
- 减少后续迁移的接口变更
第二批迁移(6个月):
感知管道
├── RGB-D 相机驱动
├── 点云处理节点
├── 物体识别服务
└── 人体姿态估计
优化成果:
- 点云处理延迟降低 40%(零拷贝传输)
- GPU 加速节点直接集成
第三批迁移(9个月):
任务规划与执行
├── 行为树执行器
├── 导航栈
├── 机械臂规划
└── 抓取控制器
技术难点:
- MoveIt1 到 MoveIt2 API 大幅变化
- 自定义规划器插件重写
1. 双模式运行架构
Toyota 开发了独特的双模式运行架构,允许机器人在 ROS1 和 ROS2 模式间无缝切换:
class HSRSystemManager {
private:
enum class Mode { ROS1_ONLY, ROS2_ONLY, HYBRID };
Mode current_mode_;
public:
void switchMode(Mode target_mode) {
// 1. 保存当前状态
auto state = captureSystemState();
// 2. 优雅停止当前节点
gracefulShutdown();
// 3. 根据模式启动对应节点
switch(target_mode) {
case Mode::ROS2_ONLY:
launchROS2Stack();
break;
case Mode::HYBRID:
launchHybridStack();
startBridge();
break;
}
// 4. 恢复系统状态
restoreSystemState(state);
}
};
2. 性能监控与自动降级
实时性能监控系统,当检测到性能下降时自动降级到 ROS1:
class PerformanceMonitor {
struct Metrics {
double control_loop_jitter_ms;
double message_latency_ms;
int dropped_messages;
};
void monitoringLoop() {
Metrics m = collectMetrics();
if (m.control_loop_jitter_ms > 2.0) { // 超过 2ms 抖动
RCLCPP_WARN(logger_, "Control jitter detected: %.2fms",
m.control_loop_jitter_ms);
if (consecutive_violations_++ > 10) {
triggerFallback(); // 降级到 ROS1
}
}
}
};
3. 消息转换优化
针对高频消息(如关节状态)的零拷贝桥接实现:
// 使用共享内存避免序列化开销
class ZeroCopyBridge {
using SharedJointState =
rclcpp::TypeAdapter<sensor_msgs::msg::JointState,
SharedMemoryJointState>;
void bridgeJointStates() {
// ROS1 端直接写入共享内存
auto shm_msg = shm_allocator_.allocate();
fillJointState(shm_msg);
// ROS2 端零拷贝发布
ros2_pub_->publish(std::move(shm_msg));
}
};
决策1:自定义 DDS 配置
<!-- Toyota HSR DDS 配置优化 -->
<profiles>
<participant profile_name="hsr_participant">
<rtps>
<builtin>
<discovery_config>
<leaseDuration>
<sec>10</sec> <!-- 快速故障检测 -->
</leaseDuration>
</discovery_config>
</builtin>
<resources>
<max_msg_size>10485760</max_msg_size> <!-- 10MB for point clouds -->
</resources>
</rtps>
</participant>
</profiles>
决策2:生命周期节点标准化
所有关键节点统一实现生命周期接口:
class HSRLifecycleNode : public rclcpp_lifecycle::LifecycleNode {
// 标准化的状态转换
CallbackReturn on_configure(const State &) override {
// 加载参数,分配资源
loadParameters();
allocateResources();
return CallbackReturn::SUCCESS;
}
CallbackReturn on_activate(const State &) override {
// 启动硬件,开始发布
startHardware();
startPublishers();
return CallbackReturn::SUCCESS;
}
};
| 指标 | ROS1 基线 | ROS2 优化后 | 改进幅度 |
|---|---|---|---|
| 控制循环频率 | 500 Hz (±50Hz) | 1000 Hz (±10Hz) | 100% ↑ |
| 点云传输延迟 | 45ms | 12ms | 73% ↓ |
| 系统启动时间 | 120s | 35s | 71% ↓ |
| 内存占用 | 4.2 GB | 3.1 GB | 26% ↓ |
| CPU 使用率(空闲) | 35% | 22% | 37% ↓ |
| 网络带宽(局域网) | 850 Mbps | 320 Mbps | 62% ↓ |
成功因素:
踩坑经历:
对于 24/7 运行的生产系统,零停机迁移是必须考虑的高级话题。本节介绍工业级的迁移策略。
蓝绿部署允许在两个完整的系统实例间切换:
┌─────────────────────────────────────────────┐
│ Load Balancer / Router │
│ (Topic-based Traffic Control) │
└──────────────┬─────────────┬────────────────┘
│ 70% │ 30%
┌─────────▼────────┐ ▼──────────────┐
│ Blue System │ │ Green System │
│ (ROS1 Stable) │ │ (ROS2 Testing)│
└──────────────────┘ └───────────────┘
│ │
┌─────────▼────────────────────▼────────┐
│ Shared Hardware Layer │
└────────────────────────────────────────┘
逐步将流量从 ROS1 切换到 ROS2:
class CanaryDeployment:
def __init__(self):
self.ros1_weight = 1.0
self.ros2_weight = 0.0
def gradual_rollout(self, target_ros2_weight, duration_hours):
steps = int(duration_hours * 60) # 每分钟调整一次
increment = target_ros2_weight / steps
for step in range(steps):
self.ros2_weight += increment
self.ros1_weight = 1.0 - self.ros2_weight
self.update_routing_weights()
# 监控关键指标
if self.detect_anomaly():
self.rollback()
break
time.sleep(60) # 等待1分钟
将生产流量镜像到 ROS2 系统进行验证:
class TrafficMirror {
void handleMessage(const MessageType& msg) {
// 1. 正常处理(ROS1)
auto result_ros1 = ros1_processor_->process(msg);
sendResponse(result_ros1);
// 2. 异步镜像到 ROS2
async_queue_.push([msg, this]() {
auto result_ros2 = ros2_processor_->process(msg);
// 3. 结果对比
if (!compareResults(result_ros1, result_ros2)) {
logDiscrepancy(msg, result_ros1, result_ros2);
}
});
}
};
确保切换时系统状态一致性:
class StateSynchronizer {
struct SystemState {
std::map<std::string, geometry_msgs::msg::Pose> robot_poses;
std::map<std::string, sensor_msgs::msg::JointState> joint_states;
std::map<std::string, nav_msgs::msg::OccupancyGrid> maps;
ros::Time timestamp;
};
SystemState captureState() {
SystemState state;
state.timestamp = ros::Time::now();
// 原子性地捕获所有状态
std::lock_guard<std::mutex> lock(state_mutex_);
state.robot_poses = current_poses_;
state.joint_states = current_joints_;
state.maps = current_maps_;
return state;
}
void restoreState(const SystemState& state) {
// 恢复到 ROS2 系统
for (const auto& [name, pose] : state.robot_poses) {
ros2_pose_pubs_[name]->publish(pose);
}
// ... 恢复其他状态
}
};
自动检测问题并回滚:
class CircuitBreaker {
enum class State { CLOSED, OPEN, HALF_OPEN };
State state_ = State::CLOSED;
int failure_count_ = 0;
const int failure_threshold_ = 5;
bool shouldProcess() {
if (state_ == State::OPEN) {
if (cooldown_expired()) {
state_ = State::HALF_OPEN;
return true; // 尝试恢复
}
return false; // 熔断中
}
return true;
}
void recordSuccess() {
failure_count_ = 0;
state_ = State::CLOSED;
}
void recordFailure() {
failure_count_++;
if (failure_count_ >= failure_threshold_) {
state_ = State::OPEN;
triggerRollback();
}
}
};
本章系统介绍了从 ROS1 迁移到 ROS2 的完整策略和实践方法。关键要点包括:
迁移决策框架:基于系统复杂度、实时性需求、安全要求等多维度评估迁移必要性和可行性
桥接技术:ros1_bridge 提供了渐进式迁移的技术基础,支持动态和静态两种模式,性能开销可控
迁移风险评估: \(R_{migration} = \alpha \cdot C_{complexity} + \beta \cdot T_{downtime} + \gamma \cdot S_{skills}\)
其中:
性能改进预期: \(P_{improvement} = \frac{L_{ROS1} - L_{ROS2}}{L_{ROS1}} \times 100\%\)
其中 $L$ 代表延迟指标
练习 3.1:解释 ros1_bridge 的工作原理,并说明为什么需要消息类型映射。
练习 3.2:列举三个 ROS2 相比 ROS1 在 API 层面的主要改进。