ROS2调试优化与工程化实践:从排错到交付
> 基于Jazzy Jalisco LTS,覆盖调试工具链、日志系统、性能分析、内存优化、ros2 bag数据管理、工程化实践、CI/CD流水线和版本管理。所有命令可直接运行验证。
一、调试工具链
ROS2的调试工具分散在CLI命令和GUI工具中,掌握组合用法才能高效排错。
调试工具全景图
1.1 话题数据检测
# 查看话题数据内容
ros2 topic echo /cmd_vel
# 只看一条消息(避免刷屏)
ros2 topic echo /cmd_vel --once
# 查看发布频率
ros2 topic hz /scan
# 输出: average rate: 10.001
# 查看带宽占用
ros2 topic bw /camera/image_raw
# 输出: average: 27.3MB/s
# 查看话题延迟(消息头时间 vs 当前时间)
ros2 topic delay /scan
# 输出: average delay: 0.003s
1.2 节点详情查看
# 列出所有活跃节点
ros2 node list
# /lidar_driver
# /slam_toolbox
# /nav2_controller
# 查看节点详细信息
ros2 node info /slam_toolbox
# 输出:
# Subscribers:
# /scan: sensor_msgs/msg/LaserScan
# Publishers:
# /map: nav_msgs/msg/OccupancyGrid
# Services:
# /slam_toolbox/serialize_map
# Action Servers:
# /slam_toolbox
1.3 手动调用服务
# 查看服务类型
ros2 service type /spawn_entity
# 输出: gazebo_msgs/srv/SpawnEntity
# 手动调用服务(注意YAML格式)
ros2 service call /spawn_entity gazebo_msgs/srv/SpawnEntity \
"{name: 'my_robot', xml: '<robot>...</robot>'}"
# 调用空参数服务
ros2 service call /reset_simulation std_srvs/srv/Empty
1.4 手动发送动作目标
# 查看动作类型
ros2 action type /navigate_to_position
# 输出: nav2_msgs/action/NavigateToPose
# 发送动作目标并接收反馈
ros2 action send_goal /navigate_to_position nav2_msgs/action/NavigateToPose \
"{pose: {header: {frame_id: 'map'}, pose: {position: {x: 2.0, y: 1.0, z: 0.0}}}}" \
--feedback
# 输出:
# Feedback: current_pose: {x: 0.5, y: 0.2}
# Feedback: current_pose: {x: 1.2, y: 0.6}
# Result: result: true
1.5 rqt_graph:计算图可视化
# 启动rqt_graph
rqt_graph
# 命令行方式查看计算图(无需GUI)
ros2 run rqt_graph rqt_graph
rqt_graph显示节点和话题的连接关系,三种模式:
- Nodes Only:只显示节点
- Nodes/Topics (all):显示节点和所有话题
- Nodes/Topics (active):只显示有数据流动的话题
1.6 rqt_plot:实时数据绘制
# 启动rqt_plot
rqt_plot
# 命令行直接指定绘图话题
rqt_plot /cmd_vel/linear/x /odom/twist/twist/linear/x
适合观察传感器数据趋势、PID调参时的实时反馈。
1.7 rviz2:3D可视化
# 启动rviz2
rviz2
# 指定配置文件启动
rviz2 -d my_config.rviz
rviz2常用功能:
- Add → By topic:直接添加话题的可视化
- Add → By display type:添加特定显示类型(Marker、TF、Grid等)
- Tool → Publish Point:手动点击获取3D坐标
1.8 ros2 bag:数据录制回放
# 录制指定话题
ros2 bag record /scan /tf /odom
# 录制所有话题
ros2 bag record -a
# 限制录制时长(秒)
ros2 bag record -a --duration 60
# 限制录制大小(MB)
ros2 bag record -a --max-bag-size 500
# 查看bag信息
ros2 bag info rosbag2_2026_06_07/
# 回放bag(发布clock话题供其他节点同步时间)
ros2 bag play rosbag2_2026_06_07/ --clock
# 2倍速回放
ros2 bag play rosbag2_2026_06_07/ --rate 2.0
二、日志系统
2.1 日志级别
ROS2定义5个日志级别,从低到高:
| 级别 | 用途 | C++宏 | Python函数 |
|:-----|:-----|:-------|:-----------|
| DEBUG | 调试细节 | RCLCPP_DEBUG | node.get_logger().debug() |
| INFO | 常规信息 | RCLCPP_INFO | node.get_logger().info() |
| WARN | 警告 | RCLCPP_WARN | node.get_logger().warn() |
| ERROR | 错误 | RCLCPP_ERROR | node.get_logger().error() |
| FATAL | 致命 | RCLCPP_FATAL | node.get_logger().fatal() |
2.2 C++节点日志示例
// minimal_logger_node.cpp
#include <rclcpp/rclcpp.hpp>
class LoggerNode : public rclcpp::Node {
public:
LoggerNode() : Node("logger_node") {
// 设置本节点日志级别
this->get_logger().set_level(rclcpp::Logger::Level::Debug);
timer_ = this->create_wall_timer(
std::chrono::seconds(1),
[this]() {
RCLCPP_DEBUG(this->get_logger(), "调试信息:定时器触发");
RCLCPP_INFO(this->get_logger(), "常规信息:系统运行正常");
RCLCPP_WARN(this->get_logger(), "警告信息:内存使用率80%%");
RCLCPP_ERROR(this->get_logger(), "错误信息:传感器读取失败");
});
}
private:
rclcpp::TimerBase::SharedPtr timer_;
};
int main(int argc, char** argv) {
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<LoggerNode>());
rclcpp::shutdown();
return 0;
}
2.3 命令行设置日志级别
# 启动时设置全局日志级别
ros2 run my_package my_node --ros-args --log-level debug
# 设置特定节点日志级别
ros2 run my_package my_node --ros-args --log-level logger_node:=debug
# 运行时动态修改日志级别
ros2 service call /logger_node/set_logger_level rcl_interfaces/srv/SetLoggerLevel \
"{logger_name: 'logger_node', level: 1}"
# level: 0=UNSET, 1=DEBUG, 2=INFO, 3=WARN, 4=ERROR, 5=FATAL
2.4 自定义日志文件(Jazzy新特性)
Jazzy版本支持将日志输出到指定文件,方便离线分析:
# 日志输出到文件
ros2 run my_package my_node --ros-args --log-file-name /tmp/my_node.log
# 同时保留终端输出
ros2 run my_package my_node --ros-args --log-file-name /tmp/my_node.log --log-level debug
2.5 Python节点日志
# logger_node.py
import rclpy
from rclpy.node import Node
from rclpy.logging import get_logger
class LoggerNode(Node):
def __init__(self):
super().__init__('logger_node')
# 设置日志级别
self.get_logger().set_level(rclpy.logging.LoggingSeverity.DEBUG)
self.timer = self.create_timer(1.0, self.timer_callback)
def timer_callback(self):
self.get_logger().debug('调试信息:定时器触发')
self.get_logger().info('常规信息:系统运行正常')
self.get_logger().warn('警告信息:内存使用率80%')
self.get_logger().error('错误信息:传感器读取失败')
def main(args=None):
rclpy.init(args=args)
node = LoggerNode()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
三、性能分析
3.1 话题性能指标
# 发布频率
ros2 topic hz /scan
# average rate: 10.001, min: 0.099s, max: 0.101s
# 带宽占用
ros2 topic bw /camera/image_raw
# average: 27.3MB/s
# 消息延迟
ros2 topic delay /scan
# average delay: 0.003s
# QoS详情(Jazzy增强)
ros2 topic info /scan --verbose
# 输出:
# QoS profile:
# Reliability: BEST_EFFORT
# Durability: VOLATILE
# Deadline: 0ns
# Lifespan: 0ns
# Liveliness: AUTOMATIC
# Subscription count: 2
# Publication count: 1
3.2 Callback Group对性能的影响
C++示例——两种Callback Group对比:
// callback_group_demo.cpp
#include <rclcpp/rclcpp.hpp>
#include <chrono>
class CallbackGroupDemo : public rclcpp::Node {
public:
CallbackGroupDemo() : Node("cb_group_demo") {
// 串行组:回调之间互斥,同一时刻只执行一个
serial_group_ = this->create_callback_group(
rclcpp::CallbackGroupType::MutuallyExclusive);
// 并行组:回调之间可并行
parallel_group_ = this->create_callback_group(
rclcpp::CallbackGroupType::Reentrant);
// 串行订阅
sub_serial_ = this->create_subscription<std_msgs::msg::String>(
"/topic_a", 10,
[this](const std_msgs::msg::String::SharedPtr msg) {
RCLCPP_INFO(this->get_logger(), "串行回调A: %s", msg->data.c_str());
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}, serial_group_);
// 并行订阅
sub_parallel_ = this->create_subscription<std_msgs::msg::String>(
"/topic_b", 10,
[this](const std_msgs::msg::String::SharedPtr msg) {
RCLCPP_INFO(this->get_logger(), "并行回调B: %s", msg->data.c_str());
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}, parallel_group_);
}
private:
rclcpp::CallbackGroup::SharedPtr serial_group_;
rclcpp::CallbackGroup::SharedPtr parallel_group_;
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr sub_serial_;
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr sub_parallel_;
};
3.3 Executor性能对比
| Executor | 回调执行 | 适用场景 | 注意事项 |
|---|---|---|---|
| SingleThreadedExecutor | 串行执行所有回调 | 简单节点、无并发需求 | 一个回调阻塞会影响全部 |
| MultiThreadedExecutor | 线程池并行执行 | 多回调需并发、计算密集 | 需配合Reentrant Group |
| StaticSingleThreadedExecutor | 串行但零拷贝优化 | 高频话题、低延迟场景 | Jazzy推荐替代Single |
# executor_demo.py
import rclpy
from rclpy.executors import MultiThreadedExecutor
def main():
rclpy.init()
node1 = MyNode('node1')
node2 = MyNode('node2')
# 多线程执行器,线程数=CPU核心数
executor = MultiThreadedExecutor(num_threads=4)
executor.add_node(node1)
executor.add_node(node2)
try:
executor.spin()
finally:
executor.shutdown()
node1.destroy_node()
node2.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
C++中使用MultiThreadedExecutor:
// multi_executor.cpp
#include <rclcpp/rclcpp.hpp>
int main(int argc, char** argv) {
rclcpp::init(argc, argv);
auto node1 = std::make_shared<MyNode>("node1");
auto node2 = std::make_shared<MyNode>("node2");
// 多线程执行器
rclcpp::executors::MultiThreadedExecutor executor(
rclcpp::ExecutorOptions(), 4); // 4个线程
executor.add_node(node1);
executor.add_node(node2);
executor.spin();
rclcpp::shutdown();
return 0;
}
3.4 常见性能瓶颈
| 瓶颈类型 | 症状 | 排查方法 | 解决方案 |
|---|---|---|---|
| 回调阻塞 | 话题hz骤降 | ros2 topic hz 对比预期频率 |
拆分回调、用Reentrant Group |
| 消息队列溢出 | 丢消息、延迟飙升 | ros2 topic info --verbose 查QoS |
增大queue_depth、调QoS |
| DDS配置不当 | 多机通信延迟高 | ros2 topic delay 检测 |
切换FastDDS配置、调Domain |
| CPU占用过高 | 系统卡顿 | top/htop 定位进程 |
降发布频率、优化算法 |
四、内存与资源优化
4.1 C++节点:valgrind内存泄漏检测
# 安装valgrind
sudo apt install valgrind
# 检测ROS2 C++节点内存泄漏
# 先source环境
source /opt/ros/jazzy/setup.bash
# 运行valgrind(注意:启动较慢,耐心等待)
valgrind --leak-check=full --show-leak-kinds=all \
--track-origins=yes --verbose \
--log-file=/tmp/valgrind_report.txt \
ros2 run my_package my_node
# 查看报告
cat /tmp/valgrind_report.txt | grep "LEAK SUMMARY" -A 5
# 期望输出:
# LEAK SUMMARY:
# definitely lost: 0 bytes in 0 blocks
# indirectly lost: 0 bytes in 0 blocks
常见内存泄漏场景:
// 错误:裸指针未释放
class BadNode : public rclcpp::Node {
public:
BadNode() : Node("bad_node") {
data_ = new float[1024]; // 泄漏:析构时未delete
}
private:
float* data_; // 应该用std::vector<float>
};
// 正确:使用智能指针或STL容器
class GoodNode : public rclcpp::Node {
public:
GoodNode() : Node("good_node") {
data_.resize(1024); // 自动管理内存
}
private:
std::vector<float> data_;
};
4.2 Python节点:tracemalloc内存追踪
# memory_tracker_node.py
import tracemalloc
import rclpy
from rclpy.node import Node
class MemoryTrackerNode(Node):
def __init__(self):
super().__init__('memory_tracker')
# 启动内存追踪
tracemalloc.start()
self.timer = self.create_timer(5.0, self.check_memory)
def check_memory(self):
# 获取内存快照
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
self.get_logger().info('=== 内存占用TOP5 ===')
for stat in top_stats[:5]:
self.get_logger().info(f' {stat}')
def main(args=None):
rclpy.init(args=args)
node = MemoryTrackerNode()
rclpy.spin(node)
node.destroy_node()
tracemalloc.stop()
rclpy.shutdown()
if __name__ == '__main__':
main()
4.3 CPU占用优化
# 优化前:高频发布 + 重复计算
class UnoptimizedNode(Node):
def __init__(self):
super().__init__('unoptimized')
# 100Hz发布,大部分消息没人消费
self.pub = self.create_publisher(String, '/topic', 10)
self.timer = self.create_timer(0.01, self.callback) # 100Hz
def callback(self):
# 每次都重新计算
result = self.heavy_computation()
msg = String()
msg.data = result
self.pub.publish(msg)
# 优化后:按需发布 + 缓存结果
class OptimizedNode(Node):
def __init__(self):
super().__init__('optimized')
self.pub = self.create_publisher(String, '/topic', 10)
# 降低到10Hz
self.timer = self.create_timer(0.1, self.callback)
self.cached_result = None
def callback(self):
# 只在结果变化时重新计算
if self.cached_result is None:
self.cached_result = self.heavy_computation()
msg = String()
msg.data = self.cached_result
self.pub.publish(msg)
4.4 网络优化:DDS Domain和QoS
# 设置DDS Domain(同一Domain的节点才能通信)
export ROS_DOMAIN_ID=30
# 多机通信时,避免Domain冲突
# 机器人A
export ROS_DOMAIN_ID=10
# 机器人B
export ROS_DOMAIN_ID=20
QoS优化示例——传感器数据用BEST_EFFORT,关键指令用RELIABLE:
# qos_optimization.py
from rclpy.node import Node
from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy
class QoSOptimizedNode(Node):
def __init__(self):
super().__init__('qos_optimized')
# 传感器数据:允许丢包,追求低延迟
sensor_qos = QoSProfile(
reliability=ReliabilityPolicy.BEST_EFFORT,
history=HistoryPolicy.KEEP_LAST,
depth=5
)
# 控制指令:必须可靠送达
control_qos = QoSProfile(
reliability=ReliabilityPolicy.RELIABLE,
history=HistoryPolicy.KEEP_LAST,
depth=10
)
self.scan_sub = self.create_subscription(
LaserScan, '/scan', self.scan_callback, sensor_qos)
self.cmd_pub = self.create_publisher(
Twist, '/cmd_vel', control_qos)
五、ros2 bag数据管理
5.1 录制与回放
# 录制指定话题
ros2 bag record /scan /tf /odom -o my_bag
# 录制所有话题(慎用,数据量大)
ros2 bag record -a -o all_topics_bag
# 限制录制时长
ros2 bag record -a --duration 120 -o timed_bag
# 限制录制大小
ros2 bag record -a --max-bag-size 1024 -o limited_bag
# 查看bag信息
ros2 bag info my_bag/
# 输出:
# Duration: 60.5s
# Start: 2026-06-07 10:00:00
# End: 2026-06-07 10:01:00
# Messages: 6050
# Topics with count:
# /scan: 605 (10Hz)
# /tf: 6050 (100Hz)
# /odom: 605 (10Hz)
5.2 回放控制
# 基本回放
ros2 bag play my_bag/
# 发布/clock话题(仿真时间同步)
ros2 bag play my_bag/ --clock
# 调整回放速率
ros2 bag play my_bag/ --rate 0.5 # 半速
ros2 bag play my_bag/ --rate 2.0 # 2倍速
# 只回放部分话题
ros2 bag play my_bag/ --topics /scan /odom
# 延迟启动(等待其他节点就绪)
ros2 bag play my_bag/ --delay 5.0
5.3 压缩存储
# 使用SQLite3存储(Jazzy默认)
ros2 bag record -a --storage sqlite3 -o sqlite_bag
# 查看存储配置
ros2 bag info sqlite_bag/
5.4 Jazzy新特性:录制服务调用
Jazzy版本支持录制和回放服务调用,这在调试服务交互时非常有用:
# 录制服务调用
ros2 bag record --services /spawn_entity /reset_simulation -o service_bag
# 回放时自动重放服务调用
ros2 bag play service_bag/
5.5 bag数据管理流程
六、工程化实践
6.1 工作空间组织
推荐的多包工作空间结构:
ros2_ws/
├── src/
│ ├── robot_bringup/ # 启动文件
│ │ ├── launch/
│ │ ├── config/
│ │ ├── package.xml
│ │ └── setup.py
│ ├── robot_description/ # 机器人描述包
│ │ ├── urdf/
│ │ ├── meshes/
│ │ ├── package.xml
│ │ └── setup.py
│ ├── robot_drivers/ # 驱动器包
│ │ ├── include/
│ │ ├── src/
│ │ ├── CMakeLists.txt
│ │ └── package.xml
│ ├── robot_navigation/ # 导航器包
│ │ ├── include/
│ │ ├── src/
│ │ ├── CMakeLists.txt
│ │ └── package.xml
│ └── robot_msgs/ # 自定义消息包
│ ├── msg/
│ ├── srv/
│ ├── action/
│ ├── CMakeLists.txt
│ └── package.xml
├── build/
├── install/
└── log/
6.2 package.xml依赖管理
<!-- package.xml 完整示例 -->
<package format="3">
<name>robot_navigation</name>
<version>1.0.0</version>
<description>机器人导航功能包</description>
<maintainer email="dev@linuxros.cn">linuxros</maintainer>
<license>Apache-2.0</license>
<!-- 构建依赖 -->
<buildtool_depend>ament_cmake</buildtool_depend>
<build_depend>rclcpp</build_depend>
<build_depend>nav_msgs</build_depend>
<build_depend>geometry_msgs</build_depend>
<build_depend>tf2_ros</build_depend>
<!-- 运行依赖 -->
<exec_depend>rclcpp</exec_depend>
<exec_depend>nav_msgs</exec_depend>
<exec_depend>geometry_msgs</exec_depend>
<exec_depend>tf2_ros</exec_depend>
<exec_depend>robot_msgs</exec_depend>
<!-- 测试依赖 -->
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
<test_depend>ament_cmake_gtest</test_depend>
<export>
<build_type>ament_cmake</build_type>
</export>
</package>
依赖类型说明:
| 依赖类型 | 用途 | 示例 |
|:---------|:-----|:-----|
| buildtool_depend | 构建工具 | ament_cmake, ament_python |
| build_depend | 编译时需要 | rclcpp, message_generation |
| exec_depend | 运行时需要 | rclcpp, message_runtime |
| test_depend | 仅测试需要 | ament_cmake_gtest |
6.3 colcon构建优化
# 基本构建
colcon build
# 只构建指定包(节省时间)
colcon build --packages-select robot_navigation
# 构建指定包及其依赖
colcon build --packages-up-to robot_navigation
# 并行构建(默认自动检测CPU核心数)
colcon build --parallel-workers 4
# 传递CMake参数
colcon build --cmake-args -DCMAKE_BUILD_TYPE=Release
# 跳过构建测试(加快构建速度)
colcon build --cmake-args -DBUILD_TESTING=OFF
# 构建完成后自动source
colcon build && source install/setup.bash
# 查看构建依赖
colcon list --topological-order
6.4 代码规范检测
# C++代码规范检测
sudo apt install ros-jazzy-ament-lint
# 检查C++代码风格
ament_cpplint --filter=-legal/copyright src/
# 检查Python代码风格
ament_flake8 src/
# 一键运行所有lint检测
ament_lint_cmake CMakeLists.txt
# 在包中集成lint(CMakeLists.txt)
# find_package(ament_lint_auto REQUIRED)
# ament_lint_auto_find_test_dependencies()
6.5 单元测试
C++测试(gtest):
// test/test_navigation.cpp
#include <gtest/gtest.h>
#include <rclcpp/rclcpp.hpp>
#include "robot_navigation/nav_engine.hpp"
TEST(NavEngineTest, ComputeDistance) {
NavEngine engine;
double dist = engine.compute_distance(0.0, 0.0, 3.0, 4.0);
EXPECT_NEAR(dist, 5.0, 0.001);
}
TEST(NavEngineTest, GoalReached) {
NavEngine engine;
bool reached = engine.is_goal_reached(1.0, 1.0, 1.01, 1.01, 0.1);
EXPECT_TRUE(reached);
}
int main(int argc, char** argv) {
rclcpp::init(argc, argv);
testing::InitGoogleTest(&argc, argv);
auto result = RUN_ALL_TESTS();
rclcpp::shutdown();
return result;
}
CMakeLists.txt中添加测试:
# CMakeLists.txt 测试部分
if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
ament_lint_auto_find_test_dependencies()
find_package(ament_cmake_gtest REQUIRED)
ament_add_gtest(nav_test test/test_navigation.cpp)
target_include_directories(nav_test PRIVATE include)
target_link_libraries(nav_test navigation_lib)
ament_target_dependencies(nav_test rclcpp)
endif()
Python测试(pytest):
# test/test_navigation.py
import pytest
from robot_navigation.nav_engine import NavEngine
def test_compute_distance():
engine = NavEngine()
dist = engine.compute_distance(0.0, 0.0, 3.0, 4.0)
assert abs(dist - 5.0) < 0.001
def test_goal_reached():
engine = NavEngine()
reached = engine.is_goal_reached(1.0, 1.0, 1.01, 1.01, 0.1)
assert reached is True
setup.py中添加测试:
# setup.py 测试配置
entry_points={
'pytest11': ['robot_navigation = pytest'],
},
运行测试:
# 运行所有测试
colcon test
# 运行指定包测试
colcon test --packages-select robot_navigation
# 查看测试结果
colcon test-result --verbose
# 查看测试日志
colcon test-result --all
七、CI/CD流水线
7.1 GitHub Actions配置
创建 .github/workflows/ros2-ci.yml:
name: ROS2 CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build-and-test:
runs-on: ubuntu-24.04
container:
image: ros:jazzy-ros-base
steps:
# 检出代码
- name: Checkout
uses: actions/checkout@v4
# 安装依赖
- name: Install dependencies
run: |
apt-get update
rosdep update
cd ${{ github.workspace }}
rosdep install --from-paths src --ignore-src -r -y
# 构建
- name: Build
run: |
cd ${{ github.workspace }}
source /opt/ros/jazzy/setup.bash
colcon build --cmake-args -DCMAKE_BUILD_TYPE=Release
# 测试
- name: Test
run: |
cd ${{ github.workspace }}
source /opt/ros/jazzy/setup.bash
source install/setup.bash
colcon test
colcon test-result --verbose
# 上传测试报告
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: |
log/latest_test/
build/*/test_results/
7.2 Docker容器化构建
Dockerfile示例:
# Dockerfile
FROM ros:jazzy-ros-base
# 安装构建依赖
RUN apt-get update && apt-get install -y \
python3-colcon-common-extensions \
ros-jazzy-ros2-control \
ros-jazzy-ros2-controllers \
&& rm -rf /var/lib/apt/lists/*
# 复制源码
WORKDIR /ros2_ws
COPY src src/
# 构建
RUN /bin/bash -c "source /opt/ros/jazzy/setup.bash && colcon build"
# 设置入口
ENTRYPOINT ["/bin/bash", "-c", "source /opt/ros/jazzy/setup.bash && source install/setup.bash && ros2 launch robot_bringup bringup.launch.py"]
构建和运行:
# 构建镜像
docker build -t robot_nav:jazzy .
# 运行容器
docker run --rm --net=host robot_nav:jazzy
# 交互式进入容器调试
docker run -it --rm --net=host robot_nav:jazzy /bin/bash
7.3 CI/CD流水线
八、版本管理
8.1 语义化版本
ROS2遵循语义化版本规范(SemVer):
MAJOR.MINOR.PATCH
MAJOR:不兼容的API变更
MINOR:向后兼容的功能新增
PATCH:向后兼容的Bug修复
package.xml中声明版本:
<package format="3">
<version>1.2.3</version>
<!-- MAJOR=1, MINOR=2, PATCH=3 -->
</package>
8.2 Git分支策略
分支命名规范:
| 分支类型 | 命名格式 | 示例 |
|:---------|:---------|:-----|
| 主分支 | main | main |
| 开发分支 | develop | develop |
| 功能分支 | feature/描述 | feature/lidar-driver |
| 修复分支 | fix/描述 | fix/tf-timeout |
| 发布分支 | release/版本号 | release/1.2.0 |
8.3 rosdistro包发布流程
将ROS2包发布到官方仓库供他人apt安装。
# 1. 确保package.xml版本号正确
# 2. 在GitHub创建Release标签
git tag 1.0.0
git push origin 1.0.0
# 3. Fork rosdistro仓库
# https://github.com/ros/rosdistro
# 4. 修改对应ROS版本的distribution.yaml
# 在jazzy/distribution.yaml中添加包信息
# 5. 提交PR到rosdistro
# 等待ROS2维护者Review和合并
# 6. 合并后,用户可通过apt安装
sudo apt install ros-jazzy-my-package
九、常见问题
Q1:节点启动后无输出?
现象:ros2 run启动节点后,终端没有任何日志输出。
排查步骤:
# 1. 检查日志级别(可能被设为WARN以上)
ros2 run my_package my_node --ros-args --log-level debug
# 2. 检查输出是否被重定向
# 某些launch文件会重定向日志到文件
ros2 run my_package my_node # 直接运行,不用launch
# 3. 检查节点是否真的在运行
ros2 node list | grep my_node
# 4. 检查日志文件(Jazzy特性)
ros2 run my_package my_node --ros-args --log-file-name /tmp/debug.log
cat /tmp/debug.log
Q2:话题延迟高
现象:ros2 topic delay显示延迟超过100ms。
排查步骤:
# 1. 检查QoS配置
ros2 topic info /scan --verbose
# 2. 检查是否有RELIABLE + 大消息的组合
# 传感器数据应该用BEST_EFFORT
# 3. 检查DDS配置
# FastDDS默认配置可能不适合高频率场景
export FASTRTPS_DEFAULT_PROFILES_FILE=/path/to/fastdds_profile.xml
# 4. 检查网络延迟
ping <remote_host>
# 5. 调整QoS为BEST_EFFORT
# Python示例
sensor_qos = QoSProfile(
reliability=ReliabilityPolicy.BEST_EFFORT,
history=HistoryPolicy.KEEP_LAST,
depth=1 # 只保留最新数据
)
Q3:colcon build失败?
现象:构建报错,找不到头文件或链接失败。
排查步骤:
# 1. 检查依赖是否安装
rosdep install --from-paths src --ignore-src -r -y
# 2. 清理构建缓存重新构建
rm -rf build/ install/ log/
colcon build
# 3. 检查package.xml依赖声明
# 编译错误通常是因为build_depend缺失
# 4. 检查CMakeLists.txt
# find_package是否包含所有依赖
# ament_target_dependencies是否完整
# 5. 只构建失败的包查看详细错误
colcon build --packages-select failing_package --event-handlers console_direct+
十、总结
ROS2项目从调试到交付的实践要点:
| 环节 | 核心工具/方法 |
|---|---|
| 调试 | ros2 topic echo/hz/bw、rqt_graph、rviz2 |
| 日志 | 5级日志体系、--log-level、--log-file-name |
| 性能 | Executor选型、Callback Group配置、QoS调优 |
| 内存 | valgrind(C++)、tracemalloc(Python) |
| 数据 | ros2 bag record/play/info |
| 工程 | colcon构建、ament_lint、单元测试 |
| CI/CD | GitHub Actions + Docker容器化 |
| 版本 | SemVer + Git分支策略 + rosdistro |
调试场景速查表
| 场景 | 首选工具 | 备选方法 |
|---|---|---|
| 话题数据不对 | ros2 topic echo |
rqt_plot |
| 发布频率异常 | ros2 topic hz |
ros2 topic bw |
| 节点连接关系 | rqt_graph |
ros2 node info |
| 服务调用失败 | ros2 service call |
查看日志 |
| 动作执行卡住 | ros2 action send_goal --feedback |
rviz2 |
| 3D数据可视化 | rviz2 |
rqt_plot |
| 话题延迟高 | ros2 topic delay |
调QoS |
| 内存泄漏 | valgrind / tracemalloc | top监控 |
| 构建失败 | colcon build --packages-select |
检查package.xml |
| 测试失败 | colcon test-result --verbose |
查看log/ |
本文首发于linuxros.cn,转载请注明出处。