目录
[一、rclcpp 是什么?](#一、rclcpp 是什么?)
[1.1 rclcpp 的基本概念](#1.1 rclcpp 的基本概念)
[1.2 rclcpp 常见写法](#1.2 rclcpp 常见写法)
[二、ROS2 C++ 程序启动相关写法](#二、ROS2 C++ 程序启动相关写法)
[2.1 main() 函数基本结构](#2.1 main() 函数基本结构)
[2.2 rclcpp::init()](#2.2 rclcpp::init())
[2.3 rclcpp::spin()](#2.3 rclcpp::spin())
[2.4 rclcpp::shutdown()](#2.4 rclcpp::shutdown())
[三、rclcpp::Node 节点类](#三、rclcpp::Node 节点类)
[3.1 rclcpp::Node 是什么?](#3.1 rclcpp::Node 是什么?)
[3.2 为什么要继承 rclcpp::Node?](#3.2 为什么要继承 rclcpp::Node?)
[3.3 Node("节点名字") 是什么意思?](#3.3 Node("节点名字") 是什么意思?)
[3.4 this->get_logger()](#3.4 this->get_logger())
[四、rclcpp::Publisher 发布者](#四、rclcpp::Publisher 发布者)
[4.1 Publisher 是什么?](#4.1 Publisher 是什么?)
[4.2 发布者成员变量写法](#4.2 发布者成员变量写法)
[4.3 create_publisher() 创建发布者](#4.3 create_publisher() 创建发布者)
[4.4 publish() 发布消息](#4.4 publish() 发布消息)
[五、rclcpp::Subscription 订阅者](#五、rclcpp::Subscription 订阅者)
[5.1 Subscription 是什么?](#5.1 Subscription 是什么?)
[5.2 订阅者成员变量写法](#5.2 订阅者成员变量写法)
[5.3 create_subscription() 创建订阅者](#5.3 create_subscription() 创建订阅者)
[5.4 订阅者回调函数](#5.4 订阅者回调函数)
[5.5 为什么回调函数里用 msg->?](#5.5 为什么回调函数里用 msg->?)
[六、rclcpp::TimerBase 定时器](#六、rclcpp::TimerBase 定时器)
[6.1 TimerBase 是什么?](#6.1 TimerBase 是什么?)
[6.2 create_wall_timer() 创建定时器](#6.2 create_wall_timer() 创建定时器)
[6.3 500ms 是什么意思?](#6.3 500ms 是什么意思?)
[6.4 定时器常见运行流程](#6.4 定时器常见运行流程)
[七、ROS2 C++ 常见符号总结](#七、ROS2 C++ 常见符号总结)
[7.1 :: 作用域解析符](#7.1 :: 作用域解析符)
[7.2 <> 模板写法](#7.2 <> 模板写法)
[7.3 SharedPtr 智能指针](#7.3 SharedPtr 智能指针)
[7.4 this-> 当前对象指针](#7.4 this-> 当前对象指针)
[7.5 std::bind 和 std::placeholders::_1](#7.5 std::bind 和 std::placeholders::_1)
摘要
在 ROS2 C++ 代码中,经常会看到下面这些写法:
cpp
rclcpp::Node
rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr
rclcpp::Subscription<geometry_msgs::msg::Twist>::SharedPtr
rclcpp::TimerBase::SharedPtr
this->create_publisher()
this->create_subscription()
this->create_wall_timer()
std::bind()
这些内容看起来复杂,其实可以分成两类理解:
cpp
ROS2 相关写法
├── rclcpp::Node
├── rclcpp::Publisher
├── rclcpp::Subscription
├── rclcpp::TimerBase
├── create_publisher()
├── create_subscription()
└── create_wall_timer()
C++ 相关写法
├── ::
├── <>
├── SharedPtr
├── this->
└── std::bind
本篇文章主要围绕 ROS2 C++ 中最常见、最基础的 rclcpp 写法进行总结,重点讲解 ROS2 节点如何创建、程序如何启动、发布者和订阅者如何定义、定时器如何工作,以及常见符号应该如何理解。
这篇文章适合作为 ROS2 C++ 代码阅读的入门篇。
学完本篇之后,至少要能看懂下面这些问题:
- rclcpp::Node 是什么?
- 为什么要继承 rclcpp::Node?
- rclcpp::init() 有什么作用?
- rclcpp::spin() 为什么不能少?
- Publisher 和 Subscription 怎么声明?
- create_publisher() 和 create_subscription() 怎么理解?
- TimerBase 和 create_wall_timer() 是什么?
- this->、::、<>、SharedPtr、std::bind 分别是什么意思?
不过需要说明的是,ROS2 C++ 的常见写法并不只有这些。
本篇主要讲的是最基础的 rclcpp 入门写法,后续还会继续整理:
第二篇:ROS2 C++ 常见写法完整分类
├── Service 服务通信
├── Client 客户端通信
├── Action 动作通信
├── Parameter 参数
├── Logging 日志
├── QoS 通信质量
├── Executor 执行器
├── CallbackGroup 回调组
├── Lifecycle 生命周期节点
├── Component 组件化节点
├── tf2 坐标变换
└── message_filters 多传感器同步
第三篇:ROS2 C++ 中必须掌握的 C++ 语法├── ::
├── ->
├── .
├── <>
├── auto
├── using
├── SharedPtr
├── std::make_shared
├── std::bind
├── lambda
├── std::chrono_literals
├── std::thread
├── std::mutex
└── std::future
这样整个系列就会形成一个比较完整的学习路线:
第一篇
├── 先看懂 rclcpp 基础写法
第二篇
├── 再系统整理 ROS2 C++ 常见模块
第三篇
└── 最后补齐 ROS2 C++ 背后的 C++ 语法
只要把这三篇内容串起来,后面再去看小车控制、AGV 底盘运动控制、导航、SLAM、多传感器融合相关代码时,就不会只停留在"代码能跑",而是能真正知道:
- 这个对象是什么?
- 这个函数在哪里被调用?
- 这个回调什么时候触发?
- 这个智能指针为什么这样写?
- 这个 ROS2 模块在机器人系统中负责什么?
除此之外,还有 ROS2 与 C++ 相关保姆级教程:
(1)ROS2 C++ 回调函数 保姆级教程:
一、rclcpp 是什么?
1.1 rclcpp 的基本概念
rclcpp 是 ROS2 的 C++ 客户端库。
简单理解:
rclcpp = ROS2 C++ 编程接口
如果使用 C++ 写 ROS2 节点,就会大量使用 rclcpp 里面的类和函数。
例如:
#include "rclcpp/rclcpp.hpp"
这行代码表示引入 ROS2 C++ 的核心头文件。
只要代码中使用了:
cpp
rclcpp::Node
rclcpp::init()
rclcpp::spin()
rclcpp::Publisher
rclcpp::Subscription
一般都需要包含:
#include "rclcpp/rclcpp.hpp"
1.2 rclcpp 常见写法
常见写法如下:
| 写法 | 含义 |
|---|---|
rclcpp::init() |
初始化 ROS2 |
rclcpp::shutdown() |
关闭 ROS2 |
rclcpp::spin() |
让节点持续运行并处理回调 |
rclcpp::Node |
ROS2 C++ 节点基类 |
rclcpp::Publisher |
发布者类型 |
rclcpp::Subscription |
订阅者类型 |
rclcpp::Service |
服务端类型 |
rclcpp::Client |
客户端类型 |
rclcpp::TimerBase |
定时器基础类型 |
可以这样理解:
rclcpp
├── 提供 ROS2 C++ 程序运行能力
├── 提供 ROS2 节点创建能力
├── 提供话题发布和订阅能力
├── 提供服务通信能力
├── 提供定时器能力
├── 提供日志功能
├── 提供参数功能
└── 提供时间相关功能
二、ROS2 C++ 程序启动相关写法
2.1 main() 函数基本结构
ROS2 C++ 程序中,main() 函数一般会出现下面三个核心写法:
cpp
rclcpp::init(argc, argv);
rclcpp::spin(node);
rclcpp::shutdown();
完整结构一般如下:
cpp
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
auto node = std::make_shared<LimoTopicSub>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
2.2 rclcpp::init()
cpp
rclcpp::init(argc, argv);
作用是:
初始化 ROS2 C++ 运行环境
可以理解成:
rclcpp::init()
├── 启动 ROS2 C++ 程序环境
├── 解析 ROS2 相关命令行参数
├── 为后续创建节点做准备
└── 一般写在 main() 函数最开始
如果没有这行代码,后面的 ROS2 节点通常无法正常运行。
2.3 rclcpp::spin()
cpp
rclcpp::spin(node);
作用是:
让节点持续运行,并等待回调函数被触发
比如:
rclcpp::spin(node)
├── 等待订阅者收到消息
├── 等待定时器触发
├── 等待服务端收到请求
└── 让节点不会立刻退出
如果没有 spin(),节点创建完之后,程序可能很快就结束了。
- 对于订阅者来说,
spin()非常重要。- 因为订阅者需要一直等待消息,如果程序直接结束,就无法接收话题数据。
2.4 rclcpp::shutdown()
cpp
rclcpp::shutdown();
作用是:
关闭 ROS2 C++ 运行环境
可以理解成:
rclcpp::shutdown()
├── 释放 ROS2 相关资源
├── 结束 ROS2 通信环境
└── 一般写在 main() 函数退出前
三、rclcpp::Node 节点类
3.1 rclcpp::Node 是什么?
cpp
rclcpp::Node
表示ROS2 C++ 中的节点的基础类。
它封装了一个 ROS2 节点应该具备的基础能力,例如:
rclcpp::Node
├── 节点名字
├── 日志功能 get_logger()
├── 创建发布器 create_publisher()
├── 创建订阅器 create_subscription()
├── 创建定时器 create_wall_timer()
├── 创建服务端 create_service()
├── 创建客户端 create_client()
├── 参数功能
├── 时间功能 now()
└── 其他 ROS2 节点基础能力
3.2 为什么要继承 rclcpp::Node?
在 ROS2 C++ 中,我们经常这样写:
cpp
class LimoTopicSub : public rclcpp::Node
{
public:
LimoTopicSub() : Node("limo_topic_sub")
{
}
};
其中:
cpp
class LimoTopicSub : public rclcpp::Node
意思是:
定义一个自己的节点类 LimoTopicSub,并继承 ROS2 官方提供的节点基类 rclcpp::Node。
也就是说:
LimoTopicSub
├── 是我们自己写的 C++ 类
├── 继承了 rclcpp::Node
└── 所以它具备 ROS2 节点能力
3.3 Node("节点名字") 是什么意思?
cpp
LimoTopicSub() : Node("limo_topic_sub")
这里的:
cpp
Node("limo_topic_sub")
表示给当前节点设置名字。
启动节点后,可以通过命令查看:
ros2 node list
如果节点启动成功,正常情况下可以看到:
/limo_topic_sub
所以:
Node("limo_topic_sub")
├── 创建 ROS2 节点
├── 节点名字叫 limo_topic_sub
└── ros2 node list 中会显示 /limo_topic_sub
3.4 this->get_logger()
在 ROS2 C++ 中,经常会看到:
cpp
RCLCPP_INFO(this->get_logger(), "limo_topic_sub node has started.");
其中:
cpp
this->get_logger()
表示获取当前节点的日志对象。
可以理解成:
this->get_logger()
├── this 表示当前节点对象
├── get_logger() 表示获取当前节点的日志器
└── 配合 RCLCPP_INFO 输出日志
完整含义是:
RCLCPP_INFO(this->get_logger(), "xxx")
├── 使用当前节点的日志器
└── 输出一条普通信息日志
四、rclcpp::Publisher 发布者
4.1 Publisher 是什么?
发布者用于向某个 Topic 发布消息。
例如小车速度控制中,常见话题是:
/cmd_vel
发布的消息类型是:
geometry_msgs::msg::Twist
所以发布者类型可以写成:
rclcpp::Publisher<geometry_msgs::msg::Twist>
意思是:
这是一个发布 Twist 类型消息的 ROS2 发布者
4.2 发布者成员变量写法
常见写法:
cpp
rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr pub_vel_;
可以拆开理解:
rclcpp::Publisher<geometry_msgs::msg::Twist>
├── 表示发布者类型
├── 发布的消息类型是 geometry_msgs::msg::Twist
::SharedPtr
├── 表示智能指针
├── 用来管理发布者对象
pub_vel_
└── 是我们自己定义的发布者变量名
所以整行代码的意思是:
定义一个发布者智能指针,这个发布者专门发布 Twist 类型消息。
4.3 create_publisher() 创建发布者
创建发布者一般写成:
cpp
pub_vel_ = this->create_publisher<geometry_msgs::msg::Twist>(
"/cmd_vel",
10
);
可以理解成:
this->create_publisher<geometry_msgs::msg::Twist>()
├── this-> 表示当前节点对象
├── create_publisher 表示创建发布器
├── <geometry_msgs::msg::Twist> 表示发布的消息类型
├── "/cmd_vel" 表示发布到这个话题
└── 10 表示队列深度
完整意思是:
- 创建一个 /cmd_vel 发布者,
- 发布的消息类型是 geometry_msgs::msg::Twist,
- 队列深度是 10。
4.4 publish() 发布消息
发布消息时常见写法:
cpp
pub_vel_->publish(vel_cmd);
意思是:
通过 pub_vel_ 这个发布者,把 vel_cmd 这条消息发布出去。
可以拆开看:
pub_vel_
├── 发布者对象->
├── 通过指针调用成员函数publish()
├── 发布消息的函数vel_cmd
└── 要发布出去的消息对象
完整示例:
cpp
geometry_msgs::msg::Twist vel_cmd;
vel_cmd.linear.x = 0.1;
vel_cmd.angular.z = 0.0;
pub_vel_->publish(vel_cmd);
意思是:
- 创建一个 Twist 速度消息,
- 设置小车线速度和角速度,
- 然后通过 /cmd_vel 发布出去。
五、rclcpp::Subscription 订阅者
5.1 Subscription 是什么?
订阅者用于接收某个 Topic 上的消息。
例如订阅 /cmd_vel 速度话题:
cpp
rclcpp::Subscription<geometry_msgs::msg::Twist>
意思是:
这是一个订阅 Twist 类型消息的 ROS2 订阅者
5.2 订阅者成员变量写法
完整写法:
cpp
rclcpp::Subscription<geometry_msgs::msg::Twist>::SharedPtr cmd_sub_;
可以拆开理解:
rclcpp::Subscription<geometry_msgs::msg::Twist>
├── 表示订阅者类型
├── 订阅的消息类型是 geometry_msgs::msg::Twist
::SharedPtr
├── 表示智能指针
├── 用来管理订阅者对象
cmd_sub_
└── 是我们自己定义的订阅者变量名
所以整行代码意思是:
定义一个订阅者智能指针,这个订阅者接收 Twist 类型消息。
5.3 create_subscription() 创建订阅者
创建订阅者常见写法:
cpp
cmd_sub_ = this->create_subscription<geometry_msgs::msg::Twist>(
"/cmd_vel",
10,
std::bind(&LimoTopicSub::cmd_callback, this, std::placeholders::_1)
);
可以理解成:
this->create_subscription<geometry_msgs::msg::Twist>()
├── 创建一个订阅者
├── 订阅 /cmd_vel 话题
├── 接收 Twist 类型消息
├── 队列深度是 10
└── 收到消息后自动调用 cmd_callback 回调函数
5.4 订阅者回调函数
回调函数一般这样写:
cpp
void cmd_callback(const geometry_msgs::msg::Twist::SharedPtr msg)
{
RCLCPP_INFO(this->get_logger(),
"linear.x = %.2f, angular.z = %.2f",
msg->linear.x,
msg->angular.z);
}
这里的:
msg
表示订阅者收到的消息。
msg->linear.x
msg->angular.z
表示读取 Twist 消息里面的字段。
可以理解成:
cmd_callback()
├── 是订阅者回调函数
├── 不需要我们手动调用
├── ROS2 收到 /cmd_vel 消息后会自动调用
├── msg 表示收到的 Twist 消息
└── msg->linear.x / msg->angular.z 表示读取速度数据
5.5 为什么回调函数里用 msg->?
因为这里的**msg 是一个智能指针。**
const geometry_msgs::msg::Twist::SharedPtr msg
所以访问里面的成员变量时,需要使用:
msg->linear.x
而不是:
msg.linear.x
可以简单记住:
- 普通对象访问成员变量:用 .
- 指针访问成员变量:用 ->
例如:
geometry_msgs::msg::Twist vel_cmd;
vel_cmd.linear.x = 0.1;
这是普通对象,所以用 .。
而:
geometry_msgs::msg::Twist::SharedPtr msg;
msg->linear.x = 0.1;
这是指针,所以用 ->。
六、rclcpp::TimerBase 定时器
6.1 TimerBase 是什么?
定时器用于周期性执行某个函数。
在 ROS2 C++ 中,定时器成员变量常见写法是:
cpp
rclcpp::TimerBase::SharedPtr timer_;
意思是:
定义一个 ROS2 定时器智能指针
可以理解成:
rclcpp::TimerBase
├── 表示 ROS2 C++ 定时器基础类型
├── 可以周期性触发回调函数
└── 常用于定时发布消息
6.2 create_wall_timer() 创建定时器
创建定时器通常写成:
cpp
timer_ = this->create_wall_timer(
500ms,
std::bind(&LimoTopicCmd::timer_callback, this)
);
意思是:
每隔 500ms 自动触发一次 timer_callback()
可以拆开理解:
this->create_wall_timer()
├── this-> 表示当前节点对象
├── create_wall_timer 表示创建定时器
├── 500ms 表示每 500 毫秒触发一次
└── timer_callback 表示定时器触发后执行的函数
6.3 500ms 是什么意思?
代码中经常会看到:
cpp
using namespace std::chrono_literals;
有了这行代码后,就可以直接写:
500ms
1s
2s
例如:
cpp
timer_ = this->create_wall_timer(
500ms,
std::bind(&LimoTopicCmd::timer_callback, this)
);
如果没有:
using namespace std::chrono_literals;
那么 500ms 这种写法可能无法识别。
6.4 定时器常见运行流程
定时器最常见的用法是定时发布消息。
例如:
每 500ms 发布一次 /cmd_vel 速度消息
流程可以理解为:
create_wall_timer()
↓
每隔 500ms 触发一次
↓
调用 timer_callback()
↓
创建 Twist 速度消息
↓
通过 pub_vel_->publish() 发布出去
七、ROS2 C++ 常见符号总结
7.1 :: 作用域解析符
在 ROS2 C++ 中,经常看到:
cpp
rclcpp::Node
std::bind
geometry_msgs::msg::Twist
这里的:
::
叫作用域解析符
简单理解:
:: 表示"从某个命名空间或者类里面找东西"
例如:
rclcpp::Node
意思是:
从 rclcpp 里面找到 Node
geometry_msgs::msg::Twist
意思是:
从 geometry_msgs 里面找到 msg,
再从 msg 里面找到 Twist
std::bind
意思是:
从 std 标准库命名空间里面找到 bind 函数
7.2 <> 模板写法
例如:
cpp
rclcpp::Publisher<geometry_msgs::msg::Twist>
这里的:
cpp
<geometry_msgs::msg::Twist>
表示模板参数。
简单理解:
尖括号 <> 用来指定类型
比如:
rclcpp::Publisher<geometry_msgs::msg::Twist>
意思是:
创建一个发布 Twist 类型消息的发布者
如果换成:
rclcpp::Publisher<std_msgs::msg::String>
意思就是:
创建一个发布 String 类型消息的发布者
所以:
Publisher<Twist>
├── 发布 Twist 消息
Publisher<String>
├── 发布 String 消息
Subscription<Twist>
└── 订阅 Twist 消息
7.3 SharedPtr 智能指针
ROS2 C++ 中经常看到:
::SharedPtr
例如:
cpp
rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr pub_vel_;
rclcpp::Subscription<geometry_msgs::msg::Twist>::SharedPtr cmd_sub_;
rclcpp::TimerBase::SharedPtr timer_;
SharedPtr 表示共享智能指针。
简单理解:
SharedPtr
├── 是一种智能指针
├── 可以自动管理对象生命周期
├── 不需要手动 delete
└── ROS2 C++ 中大量使用
为什么发布者、订阅者、定时器都要用 SharedPtr?
因为这些对象需要在节点运行期间一直存在。
例如:
rclcpp::Subscription<geometry_msgs::msg::Twist>::SharedPtr cmd_sub_;
如果订阅者对象被释放了,节点就无法继续订阅消息。
所以一般会把它们定义成类的成员变量:
private:
rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr pub_vel_;
rclcpp::Subscription<geometry_msgs::msg::Twist>::SharedPtr cmd_sub_;
rclcpp::TimerBase::SharedPtr timer_;
7.4 this-> 当前对象指针
ROS2 C++ 中经常看到:
cpp
this->create_publisher()
this->create_subscription()
this->create_wall_timer()
this->get_logger()
this->now()
这里的:
this
表示当前对象。
this->
表示通过当前对象调用成员函数或成员变量。
因为我们的类继承了:
rclcpp::Node
所以当前类对象就拥有了 ROS2 节点能力。
例如:
this->create_publisher()
意思是:
通过当前节点对象创建发布者
this->get_logger()
意思是:
通过当前节点对象获取日志器
this->now()
意思是:
通过当前节点对象获取当前 ROS2 时间
7.5 std::bind 和 std::placeholders::_1
订阅者中经常看到:
std::bind(&LimoTopicSub::cmd_callback, this, std::placeholders::_1)
可以拆开理解:
&LimoTopicSub::cmd_callback
├── 表示 LimoTopicSub 类里面的 cmd_callback 函数
this
├── 表示当前这个 LimoTopicSub 对象
std::placeholders::_1
└── 表示预留一个参数位置,这个参数就是收到的消息 msg
完整意思是:
- 当 /cmd_vel 话题收到 Twist 消息时,
- ROS2 会把收到的消息作为第一个参数传入 cmd_callback()。
定时器中也会用到 std::bind:
std::bind(&LimoTopicCmd::timer_callback, this)
意思是:
- 把当前对象 this 里面的 timer_callback() 函数,
- 绑定给定时器使用。
八、总结
本篇主要梳理了 ROS2 C++ 中最常见、最基础的 rclcpp 写法。
可以用一句话总结:
rclcpp 是 ROS2 提供给 C++ 使用的开发接口。
在 ROS2 C++ 节点中,经常会看到下面这些核心写法:
rclcpp::init()
├── 初始化 ROS2
rclcpp::shutdown()
├── 关闭 ROS2
rclcpp::spin()
├── 让节点持续运行并处理回调
rclcpp::Node
├── ROS2 C++ 节点基类
rclcpp::Publisher<T>
├── 发布 T 类型消息
rclcpp::Subscription<T>
├── 订阅 T 类型消息
rclcpp::TimerBase
└── 定时器基础类型
对于初学者来说,最重要的是先把下面这些内容看懂:
节点类
├── class Xxx : public rclcpp::Node
发布者
├── rclcpp::Publisher<T>::SharedPtr
├── create_publisher<T>()
└── publish()
订阅者
├── rclcpp::Subscription<T>::SharedPtr
├── create_subscription<T>()
└── callback()
定时器
├── rclcpp::TimerBase::SharedPtr
└── create_wall_timer()
C++ 辅助语法
├── ::
├── <>
├── SharedPtr
├── this->
├── std::bind
└── std::placeholders::_1
如果把 ROS2 C++ 学习过程比作搭积木,那么本篇讲的内容就是最底层、最常用的积木。
无论后面学习 Topic 通信、Service 通信、Action 通信,还是自定义 msg、自定义 srv、自定义 action,都会反复看到这些写法。
所以,本篇的核心作用就是打基础。
先把 rclcpp::Node、Publisher、Subscription、Timer、this->、SharedPtr、std::bind 这些最常见写法理解清楚,后面继续学习 ROS2 C++ 的 Service、Action、QoS、Executor、tf2 和底盘控制代码时,就会轻松很多。