2026 ROS 2 Lyrical C++ 入门(一):rclcpp、QoS、colcon 与 CMake 踩坑指南

环境 :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_URIROS_HOSTNAME ------ ROS 2 里不存在;
  • roscore ------ ROS 2 没有 master,自然没有 roscore。

读旧教程的习惯:凡是看到 masterroscpproscore,先提醒自己"这是 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

几个要点(重点):

  1. rcl 的职责要理解准确 。它是 ROS 2 面向 Client Library 的底层 C API ,提供 Node、Publisher/Subscription、Client/Service、Timer、Wait Set、Context 等基础能力;Action 等功能还由 rcl_action 等配套核心库共同实现 ,并不是全部塞在 librcl.so 里。
  2. rmw = ROS Middleware Interface ,作用是屏蔽不同中间件实现 。注意:ROS 2 的底层不只是 DDS ------Lyrical 已正式支持 non-DDS middleware,例如 Zenoh (rmw_zenoh 是当前正式存在的 RMW 实现)。所以更准确的说法是"Middleware Implementation",而不是"只有 DDS"。
  3. rclcpp / rclpy 不是"薄薄一层皮肤"。它们是为各自语言设计的高层 API / Client Library,尤其是 rclcpp,本身就包含 Node API、Executor(callback 调度)、callback 管理、QoS 接口等大量 C++ 层抽象。
  4. rclcpp 与 rclpy 在核心 ROS 概念上高度对应 (Node、Publisher、Subscription、Service、Action、Timer 等),但具体 API 和实现并非严格一一对应
  5. 为什么核心用 C? 因为 C 是"万能胶水",几乎所有语言都能通过 FFI 调用它。核心写一遍,各语言包一层即可,行为一致、维护一份、加新语言成本低(这正是 rclrs 能快速冒头的原因)。

3. 三个名字:包名 / 可执行名 / 节点名

这是 ROS 2 最容易混淆的地方,而且 turtlesim 恰好是个"反面教材":

概念 是什么 在哪定义 turtlesim 例子
包名 package 软件包(文件集合) package.xml / project() turtlesim
可执行文件名 executable 编译出的程序 CMakeLists.txtadd_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 目标 → 反馈 → 结果 长任务、有进度、可取消 移动/导航/执行轨迹

几个澄清:

  1. Service 的核心是 request/response(请求---响应),客户端既可以同步等待,也可以异步调用,ROS 2 官方 C++ 教程本身就提供异步请求模式。"阻塞"不是 Service 本身的定义。
  2. Action 才是"长任务 + 进度反馈 + 可取消"的正解。判断规则:"持续发数据"用 Topic;"一句话任务(要反馈、能取消)"用 Action。
  3. 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 多机通信通常只需要:

  1. 网络互通;
  2. 处于相同 ROS_DOMAIN_ID(默认 0);
  3. 使用兼容的 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.xmlCMakeLists.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); } 写进构造函数里,导致:

  1. 构造函数死循环,节点对象"永远构造不完";
  2. 你看到的日志是 while 打的,spin 形同虚设;
  3. 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. 踩坑速查表

  1. Lyrical 里 ament_target_dependencies 已移除 → 用 target_link_libraries(x PUBLIC rclcpp::rclcpp);
  2. 普通话题默认 QoS 是 Reliable (不是 Best Effort);Best Effort 是 SensorDataQoS 的默认;
  3. Python 里 Durationrclpy.duration ,不是 rclpy.qos;
  4. ROS 2 底层不只有 DDS,还有 Zenoh 等 non-DDS middleware;
  5. source install/setup.bashros2 run 报 package not found;
  6. install()DESTINATION 拼写 、变量用 ${} 不是 $();
  7. RCLCPP_INFO 只一层括号类定义结尾 ;make_shared<T>() 带括号;
  8. rosdep 在空 src 下什么都不装(先 clone/创建源码再 rosdep);
  9. 读旧教程先区分 ROS 1/ROS 2(master/roscpp 是 ROS 1 的)。
相关推荐
OPEN-F36 分钟前
C++进阶教程:类与对象深入
java·开发语言·c++
彷徨而立1 小时前
【C++11】内存模型的逻辑时序关系 happens‑before
c++
zmzb01031 小时前
C++课后习题训练记录Day201
c++·算法·图论
Escalating_xu1 小时前
【C++入门基础(上)】从发展历程、学习路线到命名空间与输入输出
开发语言·c++
jimy11 小时前
C++ 中的rValue的判断---是否能“隐式移动”
开发语言·c++
五_谷_丰_登1 小时前
平衡二叉搜索树讲解
数据结构·c++
星星.7221 小时前
【图论】最小生成树|Prim+Kruskal算法
数据结构·c++·算法·图论
charlie1145141911 小时前
Cinux · musl 静态移植:对齐 Linux ABI、铺初始栈,以及一个被 SMAP 拦下的潜伏 bug
linux·开发语言·c++·操作系统·开源项目