Zhejiang University RoboMaster Vision

ROS2 基础

面向入队考核的 ROS2 入门:从安装环境到读懂 autoaim_sentry_2025,覆盖构建、节点话题、参数、自定义接口、launch、组件化、tf2 坐标变换与可视化工具。

Cicada约 33 分钟
#ROS2#Jazzy#colcon#话题#参数#launch#tf2#RViz2

本文只覆盖读懂入队参考项目所需的那部分 ROS2 知识。学完之后,你能编译、读懂并修改 25 哨兵自瞄开源 这个工程;更深的内容(service/action、QoS 深度调优、rosbag2 编程 API 等)只要求“知道存在”。

全文以这个项目的实际代码为例。建议对照工程阅读:先 colcon build 编译,再对照源码用 ros2 topic echo 观察话题。


总参考资料(当字典用)

这一节当字典用,不需要从头读到尾。遇到概念不清楚时来这里查;平时跟着正文学,每章末尾的“本章参考资料”更贴合当章内容。

类型名称链接建议
官方ROS2 Jazzy 官方文档https://docs.ros.org/en/jazzy/index.html接口/概念以它为准
官方ROS2 Tutorials(官方教程)https://docs.ros.org/en/jazzy/Tutorials.html官方手把手教程,按需做
社区鱼香ROS-动手学ROS2https://fishros.com/d2lros2/#/这个讲得不错

1. 安装 ROS2(先把环境搭起来)

1.1 版本说明

你的 Ubuntu对应 ROS2 发行版说明
22.04Humble更早的系统版本,基础概念一致
24.04Jazzy参考项目 autoaim_sentry_2025 面向的版本,推荐
26.04Lyrical(Lyrical Luth)26.04 的长期支持版本,2026 年 5 月发布

别装错版本:ROS2 每年 5 月发一个新版,每个发行版只对应一个 Ubuntu 长期支持版,apt 里也只为这一对提供预编译包。24.04 装 Jazzy,26.04 装 Lyrical,22.04 装 Humble;把 Jazzy 装到 26.04、或把 Lyrical 装到 24.04,都会因为找不到对应软件源而失败。另外 24.04 上还能见到一个叫 Kilted 的版本,它是短期支持版(2026 年 11 月停止维护),入队考核别选它。

三个发行版的基础概念一致。下面以 24.04 + Jazzy 为准;在 26.04 上把命令里的 jazzy 换成 lyrical,在 22.04 上跑仓库原工程时换成 humble 即可。

1.2 方式一:用 fishros 脚本安装(推荐)

fishros 一键安装脚本会读取系统版本号来匹配发行版,24.04 和 26.04 都在它的支持列表里,命令一样:

wget http://fishros.com/install -O fishros && . fishros

然后随提示依次选择:[1] 一键安装 ros,[1] 更换系统源再继续安装,[2] 更换系统源并清理第三方源,[1] 自动测速选择最快的源。脚本会根据你的系统列出可选发行版:24.04 选 jazzy,26.04 选 lyrical

26.04 是较新的系统,官方源刚上线不久,国内镜像同步可能滞后。如果脚本换源后更新失败或列不出 lyrical,可以在换源那步改选官方源(packages.ros)重试;仍然失败时改用下面的 Agent 方式或官方 apt 安装。

1.3 方式二:把 Prompt 交给 AI Agent

如果你用的是 opencode / Cursor / Claude 这类 Agent,直接把下面这段英文复制给它,它会按步骤完成安装(需要 sudo 时用你提供的密码)。先把 <SUDO_PASSWORD> 替换成你真实的 sudo 密码。

Environment: Ubuntu 24.04 (Noble) on my Linux machine. I want you to install ROS 2 Jazzy (desktop variant) for me, step by step, and verify it works.

Your sudo password is: <SUDO_PASSWORD>
Use `echo '<SUDO_PASSWORD>' | sudo -S <command>` for every command that needs root. Do NOT print the password back.

Please do the following and report each step's outcome concisely:

1. Run `sudo apt update` and install base tools: `software-properties-common` and `curl`.
2. Enable the `universe` repository with `sudo add-apt-repository universe`.
3. Add the official ROS 2 apt repository:
   - Download the ROS GPG key to `/usr/share/keyrings/ros-archive-keyring.gpg` with curl from
     https://raw.githubusercontent.com/ros/rosdistro/master/ros.key
   - Write `/etc/apt/sources.list.d/ros2.list` containing exactly:
     deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(. /etc/os-release && echo $UBUNTU_CODENAME) main
4. Run `sudo apt update`, then install `ros-jazzy-desktop`.
5. Install dev tools: `ros-dev-tools` and `python3-colcon-common-extensions`.
6. Append `source /opt/ros/jazzy/setup.bash` to `~/.bashrc` (only once, avoid duplicates), then run it.
7. Verify:
   - `ros2 --help` prints usage.
   - Run `ros2 run demo_nodes_cpp talker` in the background and `ros2 run demo_nodes_cpp listener` for a few seconds; show that messages are exchanged, then stop both.
8. Print a short summary of what was installed and how to start a fresh terminal with ROS 2 ready.

Notes:
- If `$UBUNTU_CODENAME` expands wrong in the source line, substitute `noble` manually.
- Prefer the official packages.ros.org source. Only if it fails, tell me and use a mirror (e.g. mirrors.tuna.tsinghua.edu.cn) with the same layout.
- Answer "yes" to any confirmation prompt.

用 Agent 的好处:环境问题(源、依赖、权限)它会自己排查,你把输出粘回给它就能继续。密码放进 Prompt 只建议在你自己的电脑、可信的 Agent 上使用。

1.4 本章参考资料


2. 这篇文档教什么

学习对象:本文以一个公开、完整的自瞄项目 25 哨兵自瞄开源(下称 autoaim_sentry_2025)的实际代码为例。建议对照源码阅读,第 3 章会带你把它在本地搭起来。

autoaim_sentry_2025 是 ROS2 工作空间,src/ 下的 10 个包串成一条流水线:

flowchart LR
    C["camera<br/>相机采集"] -->|"image_raw<br/>图像"| D["detector<br/>目标检测"]
    D -->|"detections<br/>检测结果"| S["selector<br/>目标选择"]
    S -->|"detections<br/>筛选结果"| L["locator<br/>位姿解算"]
    L -->|"poses<br/>位姿"| P["predictor<br/>状态估计 / 预测 / 火控"]
    P -->|"shoot_pos<br/>云台目标 + 开火标志"| G["下位机 / 云台"]

看懂这个工程需要会下面这几件事,它们也是本文的主干:

你会什么对应章节工程里的体现
编译工作空间第 3 章colcon build、10 个包
节点之间用话题通信第 4 章每个 *_node.cpp 都在订阅/发布
用参数改配置第 5 章每个包的 config/params.yaml
定义自己的消息第 6 章hw_sentry_interfaces/msg 下 11 个 msg
用 launch 启动整条链路第 7 章autoaim_launcher.launch.py 一次拉起 7 个节点
节点怎么“塞进一个进程”第 8 章launch 里的 ComposableNode
坐标怎么在相机/云台间换算第 9 章locator_node 里的 tf 调用
把调试信息画出来第 10 章RViz2 / Foxglove / Marker

3. 环境与构建:编译工程

3.1 先把参考项目搭起来

我们要学习的是一个公开、完整的自瞄项目:25 哨兵自瞄开源。它结构清晰,整条流水线(相机 → 检测 → 选择 → 解算 → 预测 → 火控)都完整实现,很适合边读边编译。

本章的目标是读懂这个项目:它有哪些包、包之间怎么用话题连起来、每个节点在做什么。把它完整跑起来需要相机、推理硬件等额外条件,属于学有余力再尝试的部分,不影响你先把代码读明白。

下面四步把项目搭到能编译的状态。命令里 # 开头的都是注释,讲解这一行在做什么,你不用把注释也敲进去。

第一步:把仓库 clone 到本地

# git clone 会把远程仓库完整下载下来,在当前目录生成一个 autoaim_sentry_2025 文件夹
git clone https://github.com/Polyacetone/autoaim_sentry_2025.git

第二步:下载缺失的接口包

这个项目的节点之间靠自定义消息通信(比如检测结果 Detections、位姿 Poses)。但定义这些消息的 hw_sentry_interfaces 包,作者当初单独放在别处,没有随仓库一起发布。缺了它,整个项目编译不过。我们已经根据代码把这个接口包补齐并验证过,点下面下载:

⬇️ 下载 hw_sentry_interfaces 接口包(.zip)

第三步:把接口包放进项目的 src/ 目录

# 假设你把 zip 下载到了 ~/Downloads/ 目录

# 先解压(-d 指定解压到哪个目录)
unzip ~/Downloads/hw_sentry_interfaces.zip -d ~/Downloads/

# 再把解压出来的 hw_sentry_interfaces 文件夹移动到 clone 下来的项目的 src/ 里
# ROS2 只会编译 src/ 下面的包,所以必须放对位置
mv ~/Downloads/hw_sentry_interfaces autoaim_sentry_2025/src/

放好之后,src/ 里应该能看到接口包和其它包并排放着:

autoaim_sentry_2025/
├── src/
│   ├── hw_sentry_interfaces/   # 【接口】定义所有自定义消息 ← 刚放进来的
│   ├── autoaim_camera/         # 【采集】工业相机取图
│   ├── autoaim_detector/       # 【检测】在图像里找装甲板 / 符
│   ├── autoaim_selector/       # 【选择】多个目标里决定打哪个
│   ├── autoaim_locator/        # 【解算】PnP 把像素角点算成三维位姿
│   ├── autoaim_predictor/      # 【预测+火控】滤波、弹道、开火判断
│   ├── autoaim_send_enemy/     # 【旁路】把视野内的敌人发给决策端
│   ├── autoaim_recorder/       # 【旁路】录制关键话题,供离线调试
│   ├── autoaim_launcher/       # 【启动】一份 launch 拉起整条流水线
│   └── autoaim_common_libs/    # 【公共库】枚举和工具函数,供其它包复用

├── build/    # 编译中间产物(自动生成)
├── install/  # 编译结果(自动生成,source 这里才能运行)
└── log/      # 编译日志(自动生成)

先记住每个包的名字和它负责的那一件事,细节等读到对应章节再回来对照。每个包内部都有 CMakeLists.txtpackage.xml,多数还有 config/(参数)和 launch/(启动文件),3.4 节会讲包的组成。

第四步:编译

# 进入项目根目录
cd autoaim_sentry_2025

# 导入 ROS2 环境(每开一个新终端都要先 source 一次)
# 24.04 用 jazzy;26.04 换成 lyrical;22.04 换成 humble
source /opt/ros/jazzy/setup.bash

# 编译整个工作空间(第一次编译较慢,需要几分钟)
colcon build --symlink-install

# 让编译结果生效,之后才能运行里面的节点
source install/setup.bash

关于报错:如果你用的是较新的系统(如 26.04 + ROS2 Lyrical),可能因为 ROS2 版本差异需要改动一两处依赖写法才能编过——这类问题和接口包本身无关,只要 hw_sentry_interfaces 能正常生成消息,就说明接口部分没问题。

另外,autoaim_detectorautoaim_locator 依赖 Intel 的 OpenVINO 推理库,没装会报 openvino::runtime not found。学习阶段不必装它,可以先跳过这两个包只编译其余部分:

# --packages-skip 跳过指定的包,先把不依赖 OpenVINO 的部分编出来
colcon build --symlink-install --packages-skip autoaim_detector autoaim_locator

3.2 工作空间

一个 ROS2 工作空间由四个文件夹组成:

autoaim_sentry_2025/
├── src/        # 源码,放各个包
├── build/      # 编译中间产物
├── install/    # 编译结果,source 这里才能运行
└── log/        # 编译日志

你只需要写 src/,另外三个都是工具自动生成的。

3.3 编译与生效

# 先导入 ROS2 环境(22.04 用 humble,24.04 用 jazzy,26.04 用 lyrical)
source /opt/ros/jazzy/setup.bash

# 编译整个工作空间
colcon build

# 让编译结果"生效"(同样每个新终端都要 source)
source install/setup.bash

# 之后就能运行包里的节点了
ros2 launch autoaim_launcher autoaim_launcher.launch.py

本项目没有把构建命令封装成脚本,直接用 colcon build 即可。下面这张表解释几个常用参数,以后看到别人这么写不会陌生:

colcon build --symlink-install --parallel-workers 2 \
  --cmake-args -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ...
参数作用
--symlink-install安装用软链接,改头文件不用重新 build
--parallel-workers 2并行编译任务数
--cmake-args ...透传给 CMake 的参数(如换构建器、加编译选项)

换 CMake generator(比如 -G Ninja ↔ 默认 Makefiles)必须 rm -rf build install log 重新生成。

3.4 什么是包,以及一个包的组成

包(Package)是 ROS2 组织代码的最小单元:每个包负责一个明确的功能(如 autoaim_camera 负责相机采集、autoaim_detector 负责目标检测),可以单独编译、单独复用,也可以被别的包依赖。一个工作空间(src/)下放很多包,每个包都必须包含两个“身份证”文件:

文件作用类比
package.xml包的元信息:名字、版本、依赖了谁户口本(你是谁、认识谁)
CMakeLists.txt(C++ 包)编译规则:哪些源码、链接哪些库菜谱(怎么做出来)

src/autoaim_camera/ 是一个典型 C++ 包:

autoaim_camera/
├── CMakeLists.txt    # 构建规则:编译哪些源码、链接哪些库
├── package.xml       # 包元信息:名字、依赖
├── include/          # 头文件
├── src/              # 源码
├── config/           # 参数/配置 yaml
└── launch/           # launch 文件

判断一个文件夹是不是一个包:看有没有 package.xml。有就是包,能被 colcon 编译;没有就不是。

package.xml 最关键是声明依赖:

<package format="3">
  <name>autoaim_camera</name>
  <buildtool_depend>ament_cmake</buildtool_depend>
  <depend>rclcpp</depend>
  <depend>sensor_msgs</depend>
  ...
  <export><build_type>ament_cmake</build_type></export>
</package>

CMakeLists.txt 骨架:

cmake_minimum_required(VERSION 3.8)
project(autoaim_camera)

find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(sensor_msgs REQUIRED)

add_executable(camera_node src/camera_node.cpp)
ament_target_dependencies(camera_node rclcpp sensor_msgs)

install(TARGETS camera_node DESTINATION lib/${PROJECT_NAME})
install(DIRECTORY config launch DESTINATION share/${PROJECT_NAME})

ament_package()

依赖没配好就会报错package.xmldepend 了却没 find_package,或用了头文件却忘了加依赖,编译都会报 not found。这是新人最常见的编译报错。

3.5 常用 CLI

ros2 pkg list            # 列出已安装的包
ros2 pkg prefix <pkg>    # 查包装在哪
ros2 node list           # 列出正在运行的节点
ros2 node info <node>    # 查看一个节点的订阅/发布/服务
ros2 run <pkg> <node>    # 单独跑一个节点
ros2 launch <pkg> <file> # 用 launch 启动

3.6 本章参考资料

文档 / 在线书

视频


4. 节点与话题:ROS2 的核心通信模型

4.1 概念

  • 节点(Node):一个独立运行的可执行单元,负责某一件事(如 camera 负责出图)。
  • 话题(Topic):节点之间“发布/订阅”的通信管道。发布者往话题上发消息,订阅者收消息。一个话题配一种消息类型,可以一对多。
flowchart LR
    A["camera 节点"] -->|"发布 image_raw<br/>sensor_msgs/Image"| T["话题"]
    T -->|"订阅"| B["detector 节点"]

4.2 在代码里怎么用

autoaim_locator/src/locator_node.cpp 就是一个标准例子:

#include <rclcpp/rclcpp.hpp>
#include <sensor_msgs/msg/camera_info.hpp>
#include "hw_sentry_interfaces/msg/detections.hpp"
#include "hw_sentry_interfaces/msg/poses.hpp"

class LocatorNode : public rclcpp::Node {
public:
    explicit LocatorNode(const rclcpp::NodeOptions& options)
        : Node("autoaim_locator", options) {
        // 订阅:收到 detections 就触发回调
        detections_sub_ = create_subscription<Detections>(
            "autoaim/detector/detections", rclcpp::QoS(1),
            [this](const Detections::SharedPtr msg) { on_detections(msg); });

        // 发布:算好的位姿发出去
        poses_pub_ = create_publisher<Poses>("autoaim/locator/poses", rclcpp::QoS(1));
    }

private:
    void on_detections(const Detections::SharedPtr msg) {
        Poses poses;
        // ... 用 msg 里的数据算位姿 ...
        poses_pub_->publish(poses);
    }

    rclcpp::Subscription<Detections>::SharedPtr detections_sub_;
    rclcpp::Publisher<Poses>::SharedPtr poses_pub_;
};

要点:

  1. 消息类型写在模板尖括号里create_subscription<Detections>
  2. 回调里收到的是 SharedPtr(智能指针),用完不需要手动释放;
  3. QoS(1) 表示队列深度 1(缓存最近 1 帧,视觉里常用,避免延迟积压);
  4. 主题名统一约定成 包/节点/内容,如 autoaim/detector/detections

4.3 QoS

QoS(Quality of Service)决定“消息丢了怎么办、队列多深”:

概念含义视觉里的建议
History / Depth缓存多少条历史消息图像类用 1(只留最新帧)
Reliability是否保证不丢包图像/状态用 BEST_EFFORT(丢了就丢),指令类用 RELIABLE
Durability后加入的订阅者能否收到历史一般默认即可

项目里几乎全用 QoS(1) 就够了。真正需要调 QoS 的场景是图像订阅不上或图像掉帧,这时先怀疑 QoS 不匹配。

4.4 调试 CLI

ros2 topic list                       # 所有话题
ros2 topic echo /autoaim/detector/detections   # 实时打印某话题内容
ros2 topic hz /autoaim/camera/image_raw        # 看话题频率(确认相机有没有出图)
ros2 topic info <topic>               # 谁在发、谁在收、消息类型
ros2 node info /autoaim_camera        # 反查一个节点接了几根线
rqt_graph                             # 图形化看节点-话题网络(最直观)

4.5 本章参考资料


5. 参数:不改代码就能调配置

5.1 在节点里声明并读取

// camera_node.cpp 里:
float exposure = declare_parameter<float>("exposure");   // 声明并读取
float gain     = declare_parameter<float>("gain");

declare_parameter<类型>("名字") 的作用:向系统注册一个参数,并立刻把它的值读出来。之后参数可以在三个地方给值(见 5.3)。

5.2 参数文件 params.yaml

autoaim_camera:
  ros__parameters:
    exposure: 2000.0
    gain: 0.0

yaml 的层级是固定的:节点名: ros__parameters: 参数: 值

5.3 参数从哪里来(三种注入方式)

方式示例适用
launch 里指定文件parameters=[camera_params_yaml]工程标准做法
launch 里直接覆盖parameters=[{'exposure': 1000.0}]临时改
命令行ros2 run xxx node --ros-args -p exposure:=1000.0调试时改

5.4 工程思想:参数与代码解耦

每个包都把自己的参数放在 config/params.yaml,launch 启动节点时把这份文件喂给它:

autoaim_camera/config/params.yaml      # 相机:曝光、增益、帧率
autoaim_detector/config/params.yaml    # 检测器:置信度阈值、模型路径
autoaim_predictor/config/params.yaml   # 预测器:弹速、补偿时间

改参数只需改 yaml、不用重新编译,就能让同一份代码跑出不同行为。这就是参数化的价值:把会变的部分交给配置,把不变的部分留在代码里。

ros2 param list <node>     # 查看一个节点的参数
ros2 param get <node> <p>  # 读
ros2 param set <node> <p> <v>  # 写
ros2 param dump <node>     # 导出成 yaml

5.5 本章参考资料

文档

视频


6. 自定义接口

6.1 为什么要自定义

标准消息(sensor_msgs/Imagegeometry_msgs/Pose)不足以表达业务数据,比如“装甲板的四角点 + 置信度 + 类型”。于是每个战队都会定义自己的消息,放在 hw_sentry_interfaces/msg/

6.2 msg 文件语法

# hw_sentry_interfaces/msg/ArmorDetection.msg
int32 color                # 颜色枚举 (ColorType)
int32 label                # 装甲板编号枚举 (ArmorType)
float32 confidence         # 置信度
geometry_msgs/Point32 tl   # 引用其他包的类型:左上角点
geometry_msgs/Point32 tr   # 右上角点
geometry_msgs/Point32 bl   # 左下角点
geometry_msgs/Point32 br   # 右下角点
  • 一行一个字段:类型 名字
  • 类型可以用 C++ 基础类型(float32uint8)、数组(ArmorDetection[])、或其他包的 msg(geometry_msgs/Point32)。

6.3 让系统生成这个接口

hw_sentry_interfaces/CMakeLists.txt 里:

find_package(rosidl_default_generators REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/ArmorDetection.msg"
  "msg/Detections.msg"
  ...
  DEPENDENCIES std_msgs geometry_msgs sensor_msgs
)

package.xml 里还要声明它是接口包:

<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>

编译后,其他包 find_package(hw_sentry_interfaces REQUIRED) 就能 #include "hw_sentry_interfaces/msg/detections.hpp" 使用。

新人常见问题:改了 msg 文件却不重新 build hw_sentry_interfaces,其他包还在用旧版 → 报字段不存在。

6.4 查看接口

ros2 interface show hw_sentry_interfaces/msg/Detections

Service(请求/响应)与 Action(长任务):本项目完全没有用到。你只要知道 ROS2 有这两种通信方式即可,考核时能说出“我们项目用的是话题”就行。

6.5 本章参考资料

文档

视频


7. launch 文件:一条命令拉起整条流水线

7.1 作用

ros2 run 一次只能起一个节点;ros2 launch 用一份描述文件同时拉起一组节点并配好参数autoaim_sentry_2025autoaim_launcher.launch.py 就是一次拉起 7 个节点。

7.2 一个最小 launch.py

from launch_ros.actions import ComposableNodeContainer
from launch_ros.descriptions import ComposableNode

def generate_launch_description():
    container = ComposableNodeContainer(
        name='autoaim_container',
        package='rclcpp_components',
        executable='component_container_mt',   # 多线程容器
        composable_node_descriptions=[
            ComposableNode(
                package='autoaim_camera',
                plugin='autoaim_camera::CameraNode',
                name='autoaim_camera',
                parameters=[camera_params_yaml],
                extra_arguments=[{'use_intra_process_comms': True}],
            ),
            # ... 其他节点
        ],
    )
    return LaunchDescription([container])

7.3 读 launch

启动后可以这样验证工程跑没跑起来:

ros2 launch autoaim_launcher autoaim_launcher.launch.py
# 另开终端:
ros2 node list        # 应看到 7 个节点
rqt_graph             # 图形化:谁连谁一目了然
ros2 topic hz /autoaim/camera/image_raw   # 相机有图像输出?

需要有的能力:拿到一个陌生 launch,能顺着 parameters 找到每个节点的配置文件,再顺着订阅/发布关系画出数据流图。

7.4 本章参考资料

文档

视频(鱼香ROS《ROS 2 机器人开发从入门到实践》4.6 系列,从入门到进阶一套看完)


8. 组件化(Composable Node)

8.1 为什么需要组件化

一个普通节点 = 一个可执行文件 = 一个进程autoaim_sentry_2025 有 7 个节点,如果每个都是独立进程:

7 个进程(7 个可执行文件):
camera 进程、detector 进程、selector 进程、...、predictor 进程

问题:
- 每个进程都有独立的内存、启动开销(进程创建代价高);
- 节点之间通信要经过 DDS 序列化/反序列化,消息复制一份又一份;
- 图像最要命:1280×1024×3 ≈ 4 MB/帧,高帧率下在进程间复制是巨大开销。

组件化(Composable Node)的做法是:把节点从“可执行文件”变成“可加载的类(插件)”,由一个容器进程统一加载。多个节点跑在同一个进程里,既省进程开销,又能做零拷贝通信。

类比:普通节点像“每个功能一个 App”,组件像“一个 App 里的插件”。

8.2 普通节点 vs 组件

普通可执行节点:

int main(int argc, char** argv) {
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<CameraNode>());
    rclcpp::shutdown();
}

组件节点——没有 main,只暴露一个类,构造函数收 NodeOptions

#include <rclcpp/rclcpp.hpp>
#include "rclcpp_components/register_node_macro.hpp"

class CameraNode : public rclcpp::Node {
public:
    explicit CameraNode(const rclcpp::NodeOptions& options)
        : Node("autoaim_camera", options) { ... }
};

// 把类注册成一个"可被容器加载的插件"
RCLCPP_COMPONENTS_REGISTER_NODE(autoaim_camera::CameraNode)

8.3 把普通节点改成组件

  1. 代码:构造函数改成接收 const rclcpp::NodeOptions& options,文件末尾加 RCLCPP_COMPONENTS_REGISTER_NODE 宏;
  2. CMake:不再 add_executable,改为编译成共享库并注册为组件:
find_package(rclcpp_components REQUIRED)

add_library(camera_node_component SHARED src/camera_node.cpp)
ament_target_dependencies(camera_node_component rclcpp rclcpp_components ...)

# 声明:这个库是一个组件容器插件,注册的节点类是 CameraNode
rclcpp_components_register_nodes(camera_node_component "autoaim_camera::CameraNode")
install(TARGETS camera_node_component
        ARCHIVE DESTINATION lib
        LIBRARY DESTINATION lib
        RUNTIME DESTINATION bin)
  1. 使用:在 launch 里用 ComposableNode 描述它(见 7.2),由容器进程加载。

8.4 容器与线程

容器有两种可执行程序,区别在执行器线程模型

容器线程模型适用
component_container单线程节点少、任务轻
component_container_mt多线程视觉流水线,工程用它

为什么视觉必须用 mt:如果单线程,一个节点回调没跑完(比如 detector 推理 20ms),其他节点的回调就得排队等——相机出图、predictor 发指令全被卡住。多线程让每个节点各跑各的。

8.5 intra-process 零拷贝(性能关键)

组件只是“同进程”,默认节点间通信仍然要序列化复制。想真正共享内存,还要在 launch 里开:

extra_arguments=[{'use_intra_process_comms': True}]

打开后,同一容器内、都开了这个选项的节点之间,消息不序列化、直接传指针。对 4MB 一帧的图像来说,省下的复制时间非常可观。

组件化解决“进程太多”,intra-process 解决“复制太多”。两者一起用,图像从 camera 到 detector 几乎零拷贝。

8.6 命令行调试组件

容器已经跑起来之后,可以手动往里面塞组件:

ros2 component list            # 看容器里已加载的组件
ros2 component types           # 列出系统里所有可加载的组件
ros2 component load /autoaim_container \
    autoaim_camera autoaim_camera::CameraNode   # 手动加载一个

这样不用重启整个 launch 就能加/换节点,适合调试。平时正常启动还是走 launch。

8.7 本章参考资料


9. tf2 坐标变换(重点)

9.1 为什么 tf2 这么重要

自瞄里到处都是坐标系:相机坐标系、云台坐标系、陀螺仪/IMU 坐标系、装甲板坐标系……位姿解算算出来的是“装甲板在相机系下的坐标”,但云台要打的是“在云台系下的角度”。没有一套统一的坐标变换,数据在不同节点之间就对不上。tf2 就是 ROS2 里管理坐标系换算的标准工具。

相机拍的装甲板角点(像素)
  → locator 解出"装甲板在 camera 系的位置"
  → 需要 tf 换算到 gimbal(云台)系
  → predictor 才能算云台该转多少

9.2 三个基本概念

  • frame(坐标系):一个有名字的坐标系,如 camera_linkgimbal_linkodom。机器上每个重要的传感器/机构都会建一个 frame;
  • Transform(变换):描述“子坐标系在父坐标系里的位姿”,= 平移 (x,y,z) + 旋转(四元数 (x,y,z,w));
  • TF 树:frame 之间按 parent→child 连接成一棵树(无环、单向)。tf2_ros::Buffer 缓存整棵树,供查询。

一个 geometry_msgs/TransformStamped 消息长这样:

header:
  frame_id: "gimbal_link"          # 父坐标系
child_frame_id: "camera_link"      # 子坐标系
transform:
  translation: {x: 0.05, y: 0.0, z: 0.1}   # 相机相对云台的平移
  rotation: {x: 0.0, y: 0.0, z: 0.0, w: 1.0}  # 旋转(四元数)

含义:camera_link 在 gimbal_link 里位于 (0.05, 0, 0.1),无旋转

9.3 核心类

作用
tf2_ros::Buffer缓存 TF 树的查询接口,几乎所有查询都走它
tf2_ros::TransformBroadcaster动态广播一个变换(会随时间变化,如云台转动)
tf2_ros::StaticTransformBroadcaster广播固定变换(如相机装在云台上,相对位置不变)
tf2_ros::TransformListener把变换写进 Buffer(一般配合 Buffer 一起创建)

初始化(locator_node.cpp 里就是这么写的):

tf_buffer_ = std::make_shared<tf2_ros::Buffer>(get_clock());
tf_listener_ = std::make_unique<tf2_ros::TransformListener>(*tf_buffer_);

9.4 查询变换:lookupTransform

#include <tf2_ros/buffer.h>
#include <geometry_msgs/msg/transform_stamped.hpp>

// 查"camera_link 在 gimbal_link 系里的位姿"
// 参数:目标系,源系,时间
geometry_msgs::msg::TransformStamped t =
    tf_buffer_->lookupTransform("gimbal_link", "camera_link", tf2::TimePointZero);

// 拿到平移和旋转
double x = t.transform.translation.x;
double y = t.transform.translation.y;
double z = t.transform.translation.z;
tf2::Quaternion q(t.transform.rotation.x, t.transform.rotation.y,
                  t.transform.rotation.z, t.transform.rotation.w);
  • TimePointZero 表示“最近的可用变换”,视觉里常用(我们只关心现在这一刻);
  • 如果两个坐标系之间中间隔着别的 frame,Buffer 会自动把变换串起来(这就是“树”的价值)。

查之前先确认存在(防止抛异常):

if (tf_buffer_->canTransform("gimbal_link", "camera_link", tf2::TimePointZero)) {
    // 安全查询
}

9.5 把“一个点”从一个系转到另一个系

不止查两个系的关系,还能直接转换坐标:

#include <tf2_geometry_msgs/tf2_geometry_msgs.hpp>

geometry_msgs::msg::PointStamped camera_point;
camera_point.header.frame_id = "camera_link";
camera_point.point.x = 1.0; camera_point.point.y = 0.0; camera_point.point.z = 2.0;

// 把这个点在 camera_link 下的坐标,换算成在 gimbal_link 下的坐标
geometry_msgs::msg::PointStamped gimbal_point =
    tf_buffer_->transform(camera_point, "gimbal_link");

这就是自瞄里“相机系位置 → 云台系角度”的核心操作。输入一个带 frame 的点,输出另一个 frame 下的点。

9.6 广播自己的变换

动态变换(云台在转,相机跟着动):

tf2_ros::TransformBroadcaster broadcaster(this);
geometry_msgs::msg::TransformStamped t;
t.header.stamp = now();
t.header.frame_id = "gimbal_link";
t.child_frame_id = "camera_link";
t.transform.translation.x = 0.05;   // ...
t.transform.rotation.w = 1.0;
broadcaster.sendTransform(t);

静态变换(相对位置永远不变,用命令行最方便):

ros2 run tf2_ros static_transform_publisher 0.05 0 0.1 0 0 0 \
    gimbal_link camera_link
# 参数含义:xyz平移 yaw pitch roll旋转(弧度) 父frame 子frame

autoaim_sentry_2025 里,locator_node 会查询 autoaim_cameragimbal_pitchgimbal_yawchassisodom 等 frame 之间的变换(见 locator_node.cpp)。这些 frame 大多由导航、串口等其它部分广播,视觉节点负责查询,并补上自己这一段的固定安装关系。

9.7 命令行与可视化排查

ros2 run tf2_ros tf2_echo gimbal_link camera_link   # 实时打印两个 frame 的变换
ros2 run tf2_ros view_frames                        # 生成 tf 树图(PDF),看整体结构
ros2 run tf2_ros tf2_monitor                        # 监控各变换是否按时更新

9.8 常见问题

报错 / 现象原因与对策
Could not find transform ... Unknown frame "camera_link"frame 名字拼错了或没人广播它。先 ros2 run tf2_ros tf2_echo camera_link gimbal_link 确认名字
变换一直查不到,但 tf2_echo 能看到时间戳问题:查询用 TimePointZero 或先 canTransform 再查
一启动就查 tf 报错,等一会才好广播还没到位,查询前用 canTransform + 短暂重试,或用 waitForTransform
TF 树断开、view_frames 里缺分支某个节点没启动 / 没广播静态变换,顺着树图找断点

9.9 本章参考资料

文档

视频


10. 可视化工具

视觉开发里,把中间结果可视化通常比打印日志更容易定位问题。工程支持 RViz2 和 Foxglove 两套。

10.1 RViz2

source /opt/ros/jazzy/setup.bash   # 或 humble
rviz2

第一次使用要做的两件事

  1. 设置 Fixed Frame(固定坐标系):左上角 Global Options → Fixed Framegimbal_link(或你关注的主 frame)。所有显示都以它为参照,填错会看到坐标乱飞。
  2. Add 添加显示:点左下角 Add → By topic,把需要的面板加进来。

常用 Display(面板):

面板用途
Image直接看 image_raw 等图像话题,确认相机/预处理结果
TF显示坐标系树(各 frame 的 XYZ 轴),一眼看出 tf 对不对
MarkerArray显示工程里发的 visualization_msgs/Marker(角点、位姿、预测)
Axes / Grid参考坐标轴 / 地面网格

10.2 Foxglove

10.2.1 Foxglove 与 bridge

Foxglove 是一个可视化平台,分两部分:

  • Foxglove 客户端:看数据的界面,有网页版(打开浏览器即可,免安装)和桌面版(单独 App)两种;
  • Foxglove bridge:跑在机器人/开发机上的桥梁节点,把 ROS2 话题转成 Foxglove 客户端能收的 WebSocket 数据。

bridge 在车上,客户端在你面前,中间走网络,这就是“网页可视化”的架构。

10.2.2 安装 bridge(机器人/开发机侧)

# 以 Ubuntu 24.04 + Jazzy 为例;26.04 + Lyrical 就把 jazzy 换成 lyrical,22.04 + Humble 换成 humble
sudo apt install -y ros-jazzy-foxglove-bridge ros-jazzy-rosbridge-server

# 启动 foxglove bridge(默认端口 8765)
ros2 launch foxglove_bridge foxglove_bridge_launch.xml port:=8765

bridge 是一个普通节点,用 ros2 launch 起一个就行,不需要项目里有专门的脚本。

10.2.3 打开 Foxglove 客户端

连接成功后,在 Layouts 里新建布局,用 Add panel 加面板:

面板用途
3D同 RViz2 的 3D 场景,看 Marker / TF / 点云
Image看图像话题,可叠加显示
Raw Messages查看某条消息的完整字段(调试 msg 内容最方便)
Topic Metrics实时看各话题频率/延迟,快速定位“哪一步卡了”

对比:RViz2 随系统自带、免配置、可离线用;Foxglove 在浏览器里运行,布局能存成文件分享,界面更好看、性能也更好,工程里常用它。

10.3 Marker:在 3D 里画调试信息

工程里 predictor 节点会发布 visualization_msgs/MarkerArray(对应参数 enable_visualization_marker),把预测的位姿、弹道等画在 3D 场景里。一个 Marker 的字段:

visualization_msgs::msg::Marker m;
m.header.frame_id = "gimbal_link";     // 画在哪个坐标系
m.ns = "predictor";                    // 命名空间(分组用)
m.id = 0;                              // 同 ns 下的编号
m.type = visualization_msgs::msg::Marker::SPHERE;  // ARROW/CUBE/SPHERE/TEXT_VIEW_FACING...
m.action = visualization_msgs::msg::Marker::ADD;   // ADD/DELETE
m.pose.position.x = 1.0;               // 位置
m.scale.x = 0.1; m.scale.y = 0.1; m.scale.z = 0.1;  // 尺寸(按 type 解释)
m.color.r = 1.0f; m.color.g = 0.0f; m.color.b = 0.0f; m.color.a = 1.0f;  // RGBA
m.lifetime = rclcpp::Duration(0, 100 * 1000000);   // 0.1s 后自动消失,避免残影

多个 Marker 放进 MarkerArray 一起发布:

visualization_msgs::msg::MarkerArray arr;
arr.markers.push_back(m);      // 可放几百个
marker_pub_->publish(arr);

调试习惯:把“我认为的”和“算法算出的”都画出来。比如画装甲板四角点(红点)、预测位姿(坐标轴)、预测弹道(连线),画面一对比就能看出哪一步错了。

10.4 本章参考资料

文档

视频


11. 离线调试:录制与回放(rosbag)

① 录制——把跑起来的现场存下来:

ros2 bag record -o bag/xxx /autoaim/detector/detections \
  /autoaim/locator/poses /autoaim/predictor/shoot_pos

② 回放——离线反复测算法:

ros2 bag play bag/xxx --loop   # --loop 无限循环重播

开发流程:赛场上录一段 → 回放反复调算法 → 改完再上实车。


12. 环境变量与多机部署

12.1 常用环境变量

变量作用
ROS_DOMAIN_ID隔离不同的 ROS2 网络,多台车同时跑不串号
RMW_IMPLEMENTATION指定底层的 DDS 实现
RCUTILS_CONSOLE_OUTPUT_FORMAT日志输出格式

调试或部署时通常先把它们 export 出来。多车同时在场时,给每台车分配不同的 ROS_DOMAIN_ID,就能各看各的话题、互不干扰。

12.2 开机自启(了解)

实车部署时一般用 systemd 把程序做成开机自启的服务,常见的几件事:

  • 通过环境变量文件在启动前加载 ROS_DOMAIN_ID 等配置;
  • 以具备串口权限的用户运行;
  • 给程序设置实时调度优先级;
  • 进程挂掉后自动重启。

部署部分新人会用、能看懂即可,不要求会写 systemd 配置。

返回入组培训
|