环境 :ROS 2 Lyrical Luth (2026-05-22 发布)· Ubuntu 26.04 Resolute(官方 Tier 1 平台)
关键词 :ROS 2、Lyrical、rclcpp、QoS、colcon、ament_cmake、CMake、RMW、Zenoh
阅读收获:从 ROS 1/ROS 2 架构差异,一路梳理到写出并运行第一个 C++ 节点,并标注 Lyrical 的 breaking change。
0. 为什么写这篇
ROS 2 Lyrical 是 2026 年 5 月才正式发布的发行版,网上绝大多数教程还是 Humble / Jazzy / Kilted 时代的。直接用旧教程在 Lyrical 上开发,会撞上一堆"API 已经变了"的坑------其中最典型的,就是 ament_target_dependencies() 这个老宏在 Lyrical 上已经被移除,旧代码直接编译报错:
objectivec
CMake Error: Unknown CMake command "ament_target_dependencies"
如果你正卡在这个报错上,或者准备在 Lyrical 上从零开始,这篇是给你的。
1. ROS 1 → ROS 2:最大的变化是"去中心化"
| 特性 | ROS 1 | ROS 2 |
|---|---|---|
| 网络架构 | 中心式,依赖 Master | 默认不依赖中央 Master,靠 middleware discovery |
| 节点发现 | Master 负责注册、查找、牵线 | RMW / middleware discovery 自动发现 |
| 客户端库 | roscpp / rospy / rosjava | rclcpp / rclpy(rcljava 边缘化) |
关于 ROS 1 的 Master,一个容易夸大的点:
Master 的核心职责是注册、查找、帮助节点互相发现 ;节点彼此建立连接之后,Topic 数据通信是节点间直接进行的 。所以 Master 故障,影响的是"新节点的发现、注册和名称解析",而不能说"所有已建立的通信瞬间全部瘫痪"。
ROS 2 则默认不依赖 ROS 1 式中央 Master,通过 RMW / middleware discovery 完成节点发现(不同 middleware 和 discovery 配置可以有不同的拓扑)。
关于客户端库:不是简单"改名",是"重新设计"。
ROS 1 ROS 2 对应客户端库
roscpp → rclcpp
rospy → rclpy
ROS 2 使用重新设计 的 rclcpp / rclpy Client Library,对应 ROS 1 时代的 roscpp / rospy------不是把旧代码改个名。
已废弃的东西:
ROS_MASTER_URI、ROS_HOSTNAME------ ROS 2 里不存在;roscore------ ROS 2 没有 master,自然没有 roscore。
读旧教程的习惯:凡是看到
master、roscpp、roscore,先提醒自己"这是 ROS 1 的"。
2. rcl 架构:一个 C 核心 + 各语言 Client Library
rcl = R OS C lient L ibrary(ROS 客户端库)。它的实体是一个用 C 语言写的库 (librcl.so)。
分层结构如下:
scss
rclcpp(C++) rclpy(Python) rclrs(Rust)
\ | /
rcl / rcl_action(C 核心)
|
rmw
|
Middleware Implementation
/ | \
Fast DDS Cyclone DDS Zenoh
几个要点(重点):
rcl的职责要理解准确 。它是 ROS 2 面向 Client Library 的底层 C API ,提供 Node、Publisher/Subscription、Client/Service、Timer、Wait Set、Context 等基础能力;Action 等功能还由rcl_action等配套核心库共同实现 ,并不是全部塞在librcl.so里。rmw= ROS Middleware Interface ,作用是屏蔽不同中间件实现 。注意:ROS 2 的底层不只是 DDS ------Lyrical 已正式支持 non-DDS middleware,例如 Zenoh (rmw_zenoh是当前正式存在的 RMW 实现)。所以更准确的说法是"Middleware Implementation",而不是"只有 DDS"。- rclcpp / rclpy 不是"薄薄一层皮肤"。它们是为各自语言设计的高层 API / Client Library,尤其是 rclcpp,本身就包含 Node API、Executor(callback 调度)、callback 管理、QoS 接口等大量 C++ 层抽象。
- rclcpp 与 rclpy 在核心 ROS 概念上高度对应 (Node、Publisher、Subscription、Service、Action、Timer 等),但具体 API 和实现并非严格一一对应。
- 为什么核心用 C? 因为 C 是"万能胶水",几乎所有语言都能通过 FFI 调用它。核心写一遍,各语言包一层即可,行为一致、维护一份、加新语言成本低(这正是
rclrs能快速冒头的原因)。
3. 三个名字:包名 / 可执行名 / 节点名
这是 ROS 2 最容易混淆的地方,而且 turtlesim 恰好是个"反面教材":
| 概念 | 是什么 | 在哪定义 | turtlesim 例子 |
|---|---|---|---|
| 包名 package | 软件包(文件集合) | package.xml / project() |
turtlesim |
| 可执行文件名 executable | 编译出的程序 | CMakeLists.txt 的 add_executable() |
turtlesim_node |
| 节点名 node name | 运行时在 ROS 图里的名字 | 代码里 rclcpp::Node("...") |
turtlesim |
关键结论:三者是三个独立的东西,没有强制绑定。
ros2 run <包名> <可执行名>用前两个;ros2 node info <节点名>用第三个;- turtlesim 的坑在于:包名和节点名都叫
turtlesim(撞名),可执行名却是turtlesim_node。
可执行名与节点名为什么要解耦 ------ 为了"一个程序跑多个实例":
bash
ros2 run turtlesim turtlesim_node --ros-args -r __node:=turtle_A
ros2 run turtlesim turtlesim_node --ros-args -r __node:=turtle_B
# 两个节点,同一个可执行文件
常用查询命令:
bash
ros2 pkg list | grep <包> # 查包名
ros2 pkg executables <包名> # 查可执行名
ros2 node list # 查节点名(带 / 前缀)
4. 三种通信机制:Topic / Service / Action
| 机制 | 交互 | 特点 | 典型场景 |
|---|---|---|---|
| Topic | 发布 → 订阅 | 单向、持续流、无确认 | 传感器数据、速度指令 /cmd_vel |
| Service | 请求 → 应答 | 一问一答,适合短时操作 | 快速查询/计算 |
| Action | 目标 → 反馈 → 结果 | 长任务、有进度、可取消 | 移动/导航/执行轨迹 |
几个澄清:
- Service 的核心是 request/response(请求---响应),客户端既可以同步等待,也可以异步调用,ROS 2 官方 C++ 教程本身就提供异步请求模式。"阻塞"不是 Service 本身的定义。
- Action 才是"长任务 + 进度反馈 + 可取消"的正解。判断规则:"持续发数据"用 Topic;"一句话任务(要反馈、能取消)"用 Action。
- Nav2 的
NavigateToPose、MoveIt 2 的"移动到姿态"都是 Action,这是后面避不开的。
Action 示例(turtlesim 转到绝对角度):
bash
ros2 action send_goal /turtle1/rotate_absolute turtlesim/action/RotateAbsolute "{theta: 1.57}"
5. QoS:三个最容易混的"收不到消息"原因
"订阅者收不到消息"有三种不同原因,别混为一谈:
| 收不到的原因 | 对应 QoS | 含义 |
|---|---|---|
| 消息太旧了 | lifespan |
消息过了存活期被丢弃 |
| 订阅者晚到 | durability |
消息没保存,晚到者收不到 |
| 半路丢了 | reliability |
消息没送达 |
5.1 Reliability(可靠性)
- Reliable :ROS 2 普通 publisher/subscription 的默认策略(Keep Last 10、Volatile),尽可能保证消息送达,必要时重传;
- Best Effort :尽力发送,不保证每条消息到达。常用于激光雷达、相机等高频传感器数据,
SensorDataQoS默认使用 Best Effort(Keep Last 5)。
⚠️ 注意:普通话题默认是 Reliable,不是 Best Effort。这是新手最容易记错的一点。
5.2 Durability(持久性)
Volatile(默认):消息不保存,晚到的订阅者收不到旧消息;Transient Local:发送方本地保存最近消息,晚到者可补收(受 history depth 限制)。
场景:SLAM 地图、URDF 这类"需要晚到者也能拿到"的数据,要设
durability = Transient Local。
5.3 Lifespan(寿命)
定义"消息从写入起能存活多久",超过后直接丢弃。默认 Infinite(永不过期)。
设置示例:
cpp
// C++
rclcpp::QoS qos(10);
qos.lifespan(std::chrono::seconds(5));
python
# Python
from rclpy.qos import QoSProfile
from rclpy.duration import Duration
qos = QoSProfile(depth=10)
qos.lifespan = Duration(seconds=5)
注意:
Duration属于rclpy.duration,不是rclpy.qos。
6. 构建系统:workspace / rosdep / colcon
6.1 workspace ≠ namespace
- workspace :磁盘上的目录(如
ros2_ws),放源码、编译、装包; - namespace :节点/话题名字前的
/xxx前缀(如/turtlesim的斜杠)。
6.2 rosdep install
rosdep 根据 package.xml 中声明的 dependency key,查找对应的系统依赖 ,然后通过 apt 等系统包管理器安装------ROS 依赖本身也完全可能通过 ros-<distro>-<pkg> 的 deb 包被 rosdep 安装。
更准确的分工是:
ini
git clone = 获取源码
rosdep = 解决依赖
colcon = 构建工作区
rosdep 不会帮你 clone 源码,也不会替你跑 colcon build。另外,src/ 目录为空时,rosdep install 扫不到任何 package.xml,自然什么都不装。
首次使用先初始化:
bash
sudo rosdep init
rosdep update
源码构建标准三步:
bash
rosdep install --from-paths src --ignore-src -r -y # 1. 解决依赖
colcon build # 2. 构建
source install/setup.bash # 3. 生效
6.3 功能包 = 代码的"容器"
创建包只是"搭空架子",真正的开发是"往包里写节点代码"。典型 C++ 包结构:
bash
my_pkg/
├── package.xml # 包信息 + 依赖声明(rosdep 读它)
├── CMakeLists.txt # 构建配置
├── src/my_node.cpp # 节点代码(这才是开发)
├── launch/ # 启动文件
└── include/my_pkg/ # 头文件
7. 多机通信:理想环境下三步,实际部署有坑
理想局域网环境下,ROS 2 多机通信通常只需要:
- 网络互通;
- 处于相同
ROS_DOMAIN_ID(默认 0); - 使用兼容的 middleware discovery 配置。
但实际部署 还可能受到 WSL、Docker、防火墙、组播(multicast)以及 middleware 实现配置的影响。Lyrical 还专门提供了 ROS_AUTOMATIC_DISCOVERY_RANGE 环境变量来控制自动发现范围(取值:LOCALHOST / SUBNET / OFF / SYSTEM_DEFAULT),在 WSL2 这类 NAT 网络环境下尤其有用。
8. 从零写出第一个 rclcpp 节点(可全程复制)
8.1 创建功能包
bash
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
ros2 pkg create aaa \
--build-type ament_cmake \
--dependencies rclcpp
这会在 src/ 下生成一个叫 aaa 的包,里面自带 package.xml 和 CMakeLists.txt。
8.2 ⚠️ 最大的坑:ament_target_dependencies 已被移除
ament_target_dependencies() 的时间线:
- Humble / Jazzy:标准用法;
- Kilted(2025.05):弃用(deprecated,还能用但有警告);
- Lyrical :彻底移除 → 报
Unknown CMake command "ament_target_dependencies"。
替代写法(现代 CMake target):
cmake
# 旧(已移除)
ament_target_dependencies(my_node rclcpp)
# 新(推荐)
target_link_libraries(my_node PUBLIC rclcpp::rclcpp)
通用排查原则:遇到
ament_xxx报 Unknown command,先查该 API 是否在新发行版已弃用/移除,别急着怀疑find_package或环境。
8.3 CMakeLists.txt 完整示例
cmake
cmake_minimum_required(VERSION 3.20)
project(aaa)
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
add_executable(node_helloworld_class src/aaa.cpp)
target_link_libraries(node_helloworld_class PUBLIC rclcpp::rclcpp)
install(TARGETS
node_helloworld_class
DESTINATION lib/${PROJECT_NAME})
ament_package()
小坑:
install()里是DESTINATION(不是 DESTINGATION),变量用${PROJECT_NAME}(花括号,不是圆括号)。
8.4 节点代码(标准写法:定时器)
src/aaa.cpp:
cpp
#include "rclcpp/rclcpp.hpp"
using namespace std::chrono_literals;
class HelloWorldNode : public rclcpp::Node
{
public:
HelloWorldNode()
: rclcpp::Node("node_helloworld")
{
timer_ = this->create_wall_timer(1s, [this](){
RCLCPP_INFO(this->get_logger(), "Hello World");
});
}
private:
rclcpp::TimerBase::SharedPtr timer_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<HelloWorldNode>());
rclcpp::shutdown();
return 0;
}
8.5 架构教训:while + sleep 放构造函数是反模式
不少旧教程把 while(rclcpp::ok()) { ... sleep(1); } 写进构造函数里,导致:
- 构造函数死循环,节点对象"永远构造不完";
- 你看到的日志是 while 打的,
spin形同虚设; sleep()阻塞整个节点,以后加订阅/回调会被卡死。
正确做法:用定时器(上面的写法),或把 while 循环放到 main 里。
8.6 构建 + 运行
bash
cd ~/ros2_ws
colcon build --packages-select aaa # 构建
source install/setup.bash # 关键!让系统认识新编译的包(最容易忘)
ros2 run aaa node_helloworld_class # 运行
- 忘
source会报Package 'aaa' not found; source只对当前终端有效,新终端要重新 source;- 另开终端验证:
ros2 node list(应看到/node_helloworld)。
9. 踩坑速查表
- Lyrical 里
ament_target_dependencies已移除 → 用target_link_libraries(x PUBLIC rclcpp::rclcpp); - 普通话题默认 QoS 是 Reliable (不是 Best Effort);Best Effort 是
SensorDataQoS的默认; - Python 里
Duration在rclpy.duration,不是rclpy.qos; - ROS 2 底层不只有 DDS,还有 Zenoh 等 non-DDS middleware;
- 忘
source install/setup.bash→ros2 run报 package not found; install()里DESTINATION拼写 、变量用${}不是$();RCLCPP_INFO只一层括号 、类定义结尾;、make_shared<T>()带括号;- rosdep 在空 src 下什么都不装(先 clone/创建源码再 rosdep);
- 读旧教程先区分 ROS 1/ROS 2(master/roscpp 是 ROS 1 的)。