ROS2 基础
面向入队考核的 ROS2 入门:从安装环境到读懂 autoaim_sentry_2025,覆盖构建、节点话题、参数、自定义接口、launch、组件化、tf2 坐标变换与可视化工具。
本文只覆盖读懂入队参考项目所需的那部分 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-动手学ROS2 | https://fishros.com/d2lros2/#/ | 这个讲得不错 |
1. 安装 ROS2(先把环境搭起来)
1.1 版本说明
| 你的 Ubuntu | 对应 ROS2 发行版 | 说明 |
|---|---|---|
| 22.04 | Humble | 更早的系统版本,基础概念一致 |
| 24.04 | Jazzy | 参考项目 autoaim_sentry_2025 面向的版本,推荐 |
| 26.04 | Lyrical(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 本章参考资料
- 官方安装文档(Jazzy):https://docs.ros.org/en/jazzy/Installation/Ubuntu-Install-Debs.html —— 装环境报错先看它;
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.txt和package.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_detector和autoaim_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.xml里depend了却没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 本章参考资料
文档 / 在线书
- 《动手学ROS2》第 2 章:环境搭建与工作空间(鱼香ROS 在线书):https://fishros.com/d2lros2/#/humble/chapt2/%E7%AB%A0%E8%8A%82%E5%AF%BC%E8%AF%BB —— 章节导读,配合本章“工作空间/包”的概念一起读;
- 官方教程:Colcon 构建教程:https://docs.ros.org/en/jazzy/Tutorials/Beginner-Client-Libraries/Colcon-Tutorial.html —— colcon 的官方用法,编译报错先看它。
视频
- 古月居《ROS2 入门 21 讲》第 6 讲:工作空间与功能包:https://www.bilibili.com/video/BV16B4y1Q7jQ?p=6 —— “开发过程的大本营”,和本章 3.1~3.3 完全对应;
- 鱼香ROS《ROS 2 机器人开发从入门到实践》2.3 使用功能包组织 C++ 节点:https://www.bilibili.com/video/BV1Pb42177BJ —— 讲 C++ 包的组成,配合 3.3 看;
- 鱼香ROS《ROS 2 机器人开发从入门到实践》2.4 多功能包的最佳实践 WorkSpace:https://www.bilibili.com/video/BV1Hx4y147gW —— 多包工作空间的组织方式。
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_;
};
要点:
- 消息类型写在模板尖括号里:
create_subscription<Detections>; - 回调里收到的是
SharedPtr(智能指针),用完不需要手动释放; QoS(1)表示队列深度 1(缓存最近 1 帧,视觉里常用,避免延迟积压);- 主题名统一约定成
包/节点/内容,如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 本章参考资料
- 官方教程:理解节点(Understanding nodes):https://docs.ros.org/en/jazzy/Tutorials/Beginner-CLI-Tools/Understanding-ROS2-Nodes/Understanding-ROS2-Nodes.html
- 官方教程:理解话题(Understanding topics):https://docs.ros.org/en/jazzy/Tutorials/Beginner-CLI-Tools/Understanding-ROS2-Topics/Understanding-ROS2-Topics.html
- B站上对应的“节点/话题”讲解视频,你自己找效果最好的。
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 本章参考资料
文档
- 官方教程:理解参数(Understanding parameters):https://docs.ros.org/en/jazzy/Tutorials/Beginner-CLI-Tools/Understanding-ROS2-Parameters/Understanding-ROS2-Parameters.html
视频
- 古月居《ROS 入门 21 讲》第 16 讲:参数的使用与编程方法:https://www.bilibili.com/video/BV1zt411G7Vn?p=16 —— 注意这是 ROS1 版本的课,但“参数”的概念与用法和 ROS2 相通,可对照学习;
- 英文视频:参数相关讲解:https://www.youtube.com/watch?v=wY8MrBGVxYA
6. 自定义接口
6.1 为什么要自定义
标准消息(sensor_msgs/Image、geometry_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++ 基础类型(
float32、uint8)、数组(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 本章参考资料
文档
- 官方教程:理解接口(Understanding interfaces,msg/srv/action):https://docs.ros.org/en/jazzy/Tutorials/Beginner-CLI-Tools/Understanding-ROS2-Interfaces/Understanding-ROS2-Interfaces.html
视频
- 鱼香ROS《ROS 2 机器人开发从入门到实践》3.4.2 自定义通信接口:https://www.bilibili.com/video/BV1dTHgeeEPm —— 手把手教你写自定义 msg 并在包中使用,配合本章 6.2~6.3 看;
- 接口相关讲解:https://www.youtube.com/watch?v=E_xBPI8SQig
7. launch 文件:一条命令拉起整条流水线
7.1 作用
ros2 run 一次只能起一个节点;ros2 launch 用一份描述文件同时拉起一组节点并配好参数。autoaim_sentry_2025 的 autoaim_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 系列,从入门到进阶一套看完)
- 4.6.1 使用 Launch 启动多个节点:https://www.bilibili.com/video/BV1hdtLeZE9y —— 对应本章 7.2,多个节点怎么用一个 launch 拉起来;
- 4.6.2 使用 launch 传递参数:https://www.bilibili.com/video/BV18MtRePEdY —— launch 里给节点传参数,配合第 5 章参数看;
- 4.6.3 launch 使用进阶:https://www.bilibili.com/video/BV1BMbAeKEY8 —— 条件、复用等进阶用法。
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 把普通节点改成组件
- 代码:构造函数改成接收
const rclcpp::NodeOptions& options,文件末尾加RCLCPP_COMPONENTS_REGISTER_NODE宏; - 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)
- 使用:在 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 本章参考资料
- 官方教程:节点组成(Composition):https://docs.ros.org/en/jazzy/Tutorials/Intermediate/Composition.html
9. tf2 坐标变换(重点)
9.1 为什么 tf2 这么重要
自瞄里到处都是坐标系:相机坐标系、云台坐标系、陀螺仪/IMU 坐标系、装甲板坐标系……位姿解算算出来的是“装甲板在相机系下的坐标”,但云台要打的是“在云台系下的角度”。没有一套统一的坐标变换,数据在不同节点之间就对不上。tf2 就是 ROS2 里管理坐标系换算的标准工具。
相机拍的装甲板角点(像素)
→ locator 解出"装甲板在 camera 系的位置"
→ 需要 tf 换算到 gimbal(云台)系
→ predictor 才能算云台该转多少
9.2 三个基本概念
- frame(坐标系):一个有名字的坐标系,如
camera_link、gimbal_link、odom。机器上每个重要的传感器/机构都会建一个 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_camera、gimbal_pitch、gimbal_yaw、chassis、odom 等 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 本章参考资料
文档
- 官方教程:tf2 入门(Learning tf2):https://docs.ros.org/en/jazzy/Tutorials/Intermediate/Tf2/Tf2-Main.html
视频
- 鱼香ROS《ROS 2 机器人开发从入门到实践》5.1.1 通过命令行使用 TF:https://www.bilibili.com/video/BV1ietCe2E9F —— 命令行玩 tf(对应本章 9.7);
- 鱼香ROS《ROS 2 机器人开发从入门到实践》5.1.2 对 TF 原理的简单探究:https://www.bilibili.com/video/BV1wnsCe1EgM —— TF 树/变换原理,配合 9.2~9.5 看;
- 机器人工匠阿杰《ROS 快速入门教程》第 45 讲:初识 ROS 的 TF 系统:https://www.bilibili.com/video/BV1uh4y1377t —— 注意是 ROS1 课,概念可对照;
- 古月居《ROS 入门 21 讲》第 18 讲:tf 坐标系广播与监听的编程实现:https://www.bilibili.com/video/BV1zt411G7Vn?p=18 —— ROS1 课,广播/监听代码思路可对照本章 9.6。
10. 可视化工具
视觉开发里,把中间结果可视化通常比打印日志更容易定位问题。工程支持 RViz2 和 Foxglove 两套。
10.1 RViz2
source /opt/ros/jazzy/setup.bash # 或 humble
rviz2
第一次使用要做的两件事:
- 设置 Fixed Frame(固定坐标系):左上角
Global Options → Fixed Frame填gimbal_link(或你关注的主 frame)。所有显示都以它为参照,填错会看到坐标乱飞。 - 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 客户端
- 网页版(推荐,免安装):浏览器打开 https://app.foxglove.dev ,在连接框里填
ws://localhost:8765(bridge 在本机),点 Connect; - 桌面版:从 https://foxglove.dev/download 下载安装,连接同一个地址。
连接成功后,在 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 本章参考资料
文档
- 官方 RViz2 用户指南:https://docs.ros.org/en/jazzy/Tutorials/Intermediate/RViz/RViz-User-Guide.html
- Foxglove 官方文档:https://docs.foxglove.dev/
视频
- 古月居《ROS2 入门 21 讲》第 19 讲:Rviz 三维可视化显示平台:https://www.bilibili.com/video/BV16B4y1Q7jQ?p=19 —— RViz2 的可视化操作,配合本章 10.1 看。
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 配置。