ESC
输入关键词搜索文章标题和内容

ROS2调试优化与工程化实践:从排错到交付

本文由 linuxROS 整理发布,首发于 linuxros.cn,转载请注明出处。

ROS2调试优化与工程化实践:从排错到交付

> 基于Jazzy Jalisco LTS,覆盖调试工具链、日志系统、性能分析、内存优化、ros2 bag数据管理、工程化实践、CI/CD流水线和版本管理。所有命令可直接运行验证。

一、调试工具链

ROS2的调试工具分散在CLI命令和GUI工具中,掌握组合用法才能高效排错。

调试工具全景图

flowchart TB A(["调试需求"]) --> B{"问题类型?"} B -->|"数据不对"| C["ros2 topic echo"] B -->|"频率异常"| D["ros2 topic hz/bw"] B -->|"节点状态"| E["ros2 node info"] B -->|"服务调用"| F["ros2 service call"] B -->|"动作执行"| G["ros2 action send_goal"] B -->|"计算图"| H["rqt_graph"] B -->|"数据趋势"| I["rqt_plot"] B -->|"3D可视化"| J["rviz2"] B -->|"录制回放"| K["ros2 bag"] style A fill:#E3F2FD,stroke:#1565C0 style B fill:#FFF8E1,stroke:#F57F17 style C fill:#E8F5E9,stroke:#388E3C style D fill:#E8F5E9,stroke:#388E3C style E fill:#E8F5E9,stroke:#388E3C style F fill:#F3E5F5,stroke:#7B1FA2 style G fill:#F3E5F5,stroke:#7B1FA2 style H fill:#E3F2FD,stroke:#1565C0 style I fill:#E3F2FD,stroke:#1565C0 style J fill:#E3F2FD,stroke:#1565C0 style K fill:#FFEBEE,stroke:#D32F2F

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对性能的影响

flowchart TB A(["节点回调"]) --> B{"Callback Group?"} B -->|"MutuallyExclusive"| C["同一Group内<br/>串行执行"] B -->|"Reentrant"| D["所有回调<br/>并行执行"] C --> E["优点:无竞争<br/>缺点:阻塞"] D --> F["优点:高吞吐<br/>缺点:需加锁"] E --> G["适用:状态机<br/>顺序逻辑"] F --> H["适用:独立计算<br/>传感器处理"] style A fill:#E3F2FD,stroke:#1565C0 style B fill:#FFF8E1,stroke:#F57F17 style C fill:#E8F5E9,stroke:#388E3C style D fill:#F3E5F5,stroke:#7B1FA2 style E fill:#E8F5E9,stroke:#388E3C style F fill:#F3E5F5,stroke:#7B1FA2 style G fill:#E8F5E9,stroke:#388E3C style H fill:#F3E5F5,stroke:#7B1FA2

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数据管理流程

flowchart TB A(["开始录制"]) --> B{"录制范围?"} B -->|"关键话题"| C["ros2 bag record<br/>/topic1 /topic2"] B -->|"全量录制"| D["ros2 bag record -a<br/>--max-bag-size限制"] C --> E["ros2 bag info<br/>检查数据完整性"] D --> E E --> F{"数据有效?"} F -->|"是"| G["ros2 bag play<br/>回放验证"] F -->|"否"| H["重新录制"] G --> I(["完成"]) style A fill:#E3F2FD,stroke:#1565C0 style B fill:#FFF8E1,stroke:#F57F17 style F fill:#FFF8E1,stroke:#F57F17 style I fill:#E8F5E9,stroke:#388E3C style H fill:#FFEBEE,stroke:#D32F2F

六、工程化实践

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中添加测试:

来自 linuxros.cn · linuxROS
# 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流水线

flowchart TB A(["代码提交"]) --> B["GitHub Actions<br/>触发CI"] B --> C["Docker容器<br/>拉取ros:jazzy镜像"] C --> D["rosdep install<br/>安装依赖"] D --> E["colcon build<br/>编译构建"] E --> F{"构建成功?"} F -->|"否"| G["构建失败通知"] F -->|"是"| H["colcon test<br/>运行测试"] H --> I{"测试通过?"} I -->|"否"| J["测试报告上传"] I -->|"是"| K["Docker镜像<br/>构建推送"] K --> L(["部署就绪"]) style A fill:#E3F2FD,stroke:#1565C0 style F fill:#FFF8E1,stroke:#F57F17 style I fill:#FFF8E1,stroke:#F57F17 style G fill:#FFEBEE,stroke:#D32F2F style J fill:#FFEBEE,stroke:#D32F2F style L fill:#E8F5E9,stroke:#388E3C

八、版本管理

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分支策略

flowchart TB M["main<br/>稳定发布"] --> D["develop<br/>开发集成分支"] D --> F1["feature/lidar-driver<br/>激光雷达驱动"] D --> F2["feature/nav-stack<br/>导航栈开发"] D --> F3["feature/arm-ctrl<br/>机械臂控制"] F1 -->|"PR + Review"| D F2 -->|"PR + Review"| D F3 -->|"PR + Review"| D D -->|"测试通过"| M style M fill:#E8F5E9,stroke:#388E3C style D fill:#E3F2FD,stroke:#1565C0 style F1 fill:#F3E5F5,stroke:#7B1FA2 style F2 fill:#F3E5F5,stroke:#7B1FA2 style F3 fill:#F3E5F5,stroke:#7B1FA2

分支命名规范:
| 分支类型 | 命名格式 | 示例 |
|:---------|:---------|:-----|
| 主分支 | 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,转载请注明出处。

版权声明

作者linuxROS
协议本作品采用 CC BY-NC-SA 4.0 许可协议:署名-非商业性使用-相同方式共享
关注欢迎关注微信公众号 linuxROS,获取更多机器人 / 嵌入式 / Linux 干货
返回首页