ROS2 C++ 进阶语法保姆级教程

目录

摘要

一、程序入口与节点相关写法

[1.1 ROS2 C++ 程序入口基本结构](#1.1 ROS2 C++ 程序入口基本结构)

[1.2 常见程序入口写法](#1.2 常见程序入口写法)

[1.3 spin()、spin_some()、spin_until_future_complete() 区别](#1.3 spin()、spin_some()、spin_until_future_complete() 区别)

(1)rclcpp::spin(node)

(2)rclcpp::spin_some(node)

[(3)rclcpp::spin_until_future_complete(node, future)](#(3)rclcpp::spin_until_future_complete(node, future))

[1.4 节点类相关写法](#1.4 节点类相关写法)

[二、Topic 发布订阅与 Timer 定时器写法](#二、Topic 发布订阅与 Timer 定时器写法)

[2.1 Publisher 发布者写法](#2.1 Publisher 发布者写法)

[2.2 Subscription 订阅者写法](#2.2 Subscription 订阅者写法)

[2.3 回调函数参数的三种常见写法](#2.3 回调函数参数的三种常见写法)

[(1)SharedPtr 写法](#(1)SharedPtr 写法)

(2)引用写法

[(3)UniquePtr 写法](#(3)UniquePtr 写法)

[2.4 Timer 定时器写法](#2.4 Timer 定时器写法)

[三、Service 与 Action 通信写法](#三、Service 与 Action 通信写法)

[3.1 Service 服务通信写法](#3.1 Service 服务通信写法)

[3.2 Service 服务端写法](#3.2 Service 服务端写法)

[3.3 Service 客户端写法](#3.3 Service 客户端写法)

[3.4 Action 动作通信写法](#3.4 Action 动作通信写法)

[3.5 Action 服务端常见写法](#3.5 Action 服务端常见写法)

[3.6 Action 客户端常见写法](#3.6 Action 客户端常见写法)

[四、参数、日志、时间与 Graph 查询写法](#四、参数、日志、时间与 Graph 查询写法)

[4.1 Parameter 参数写法](#4.1 Parameter 参数写法)

[4.2 参数常见写法](#4.2 参数常见写法)

[4.3 Logging 日志写法](#4.3 Logging 日志写法)

[4.4 Time、Duration、Rate 时间相关写法](#4.4 Time、Duration、Rate 时间相关写法)

[4.5 Graph 查询相关写法](#4.5 Graph 查询相关写法)

[五、QoS、Executor 与 CallbackGroup 写法](#五、QoS、Executor 与 CallbackGroup 写法)

[5.1 QoS 是什么?](#5.1 QoS 是什么?)

[5.2 QoS 常见写法](#5.2 QoS 常见写法)

[5.3 Executor 执行器写法](#5.3 Executor 执行器写法)

[5.4 SingleThreadedExecutor 与 MultiThreadedExecutor 区别](#5.4 SingleThreadedExecutor 与 MultiThreadedExecutor 区别)

(1)单线程执行器

(2)多线程执行器

[5.5 CallbackGroup 回调组写法](#5.5 CallbackGroup 回调组写法)

[六、Lifecycle、Component、tf2 与 message_filters 写法](#六、Lifecycle、Component、tf2 与 message_filters 写法)

[6.1 LifecycleNode 生命周期节点写法](#6.1 LifecycleNode 生命周期节点写法)

[6.2 Component 组件化节点写法](#6.2 Component 组件化节点写法)

[6.3 tf2 坐标变换写法](#6.3 tf2 坐标变换写法)

[6.4 message_filters 多传感器同步写法](#6.4 message_filters 多传感器同步写法)

[七、面向 AGV 底盘控制的 ROS2 C++ 写法总结](#七、面向 AGV 底盘控制的 ROS2 C++ 写法总结)

[7.1 AGV 底盘控制中最常见的 ROS2 C++ 模块](#7.1 AGV 底盘控制中最常见的 ROS2 C++ 模块)

[7.2 不同写法在 AGV 中的作用](#7.2 不同写法在 AGV 中的作用)

[7.3 学习顺序建议](#7.3 学习顺序建议)

八、总结


摘要

前两篇文章已经分别整理了 ROS2 C++ 学习中最基础的两部分内容。

第一篇是:

复制代码
ROS2 C++ 常见 rclcpp 写法
├── rclcpp::init()
├── rclcpp::spin()
├── rclcpp::shutdown()
├── rclcpp::Node
├── Publisher
├── Subscription
└── Timer

它主要解决的是:

复制代码
如何看懂一个最基础的 ROS2 C++ 节点。

第一篇 ROS2 C++ 常见 rclcpp 写法 链接如下:

ROS2 C++ 常见 rclcpp 写法保姆级教程-CSDN博客https://blog.csdn.net/m0_58954356/article/details/162202727?sharetype=blogdetail&sharerId=162202727&sharerefer=PC&sharesource=m0_58954356&spm=1011.2480.3001.8118第二篇是:

复制代码
ROS2 C++ 基础语法
├── #include
├── main()
├── auto
├── const
├── class
├── public / private
├── 构造函数
├── this
├── . 和 ->
├── SharedPtr
├── std::make_shared
├── std::bind
└── lambda

它主要解决的是:

复制代码
ROS2 C++ 代码背后的 C++ 语法为什么要这样写。

第二篇 ROS2 C++ 基础语法 链接如下:

ROS2 C++ 基础语法保姆级教程-CSDN博客https://blog.csdn.net/m0_58954356/article/details/162235498?sharetype=blogdetail&sharerId=162235498&sharerefer=PC&sharesource=m0_58954356&spm=1011.2480.3001.8118为了让整个专栏学习路线更加清楚,可以按照下面顺序阅读:

第一篇:ROS2 C++ 常见 rclcpp 写法
第二篇:ROS2 C++ 基础语法
第三篇:ROS2 C++ 进阶语法
第四篇:ROS2 C++ 底盘运动控制算法写法

本篇作为第三篇,主要整理 ROS2 C++ 工程中更完整、更进阶的常见写法。

因为在真实机器人开发中,尤其是小车控制、AGV 底盘控制、导航控制、传感器数据处理、多传感器融合中,只掌握 Publisher、Subscription 和 Timer 还远远不够。

完整的 ROS2 C++ 工程中,还会经常遇到:

复制代码
ROS2 C++ 进阶写法
├── rclcpp::Service
├── rclcpp::Client
├── rclcpp_action::Server
├── rclcpp_action::Client
├── rclcpp::Parameter
├── RCLCPP_INFO / WARN / ERROR
├── rclcpp::Time
├── rclcpp::QoS
├── rclcpp::executors::SingleThreadedExecutor
├── rclcpp::executors::MultiThreadedExecutor
├── rclcpp::CallbackGroup
├── rclcpp_lifecycle::LifecycleNode
├── rclcpp_components
├── tf2_ros::TransformBroadcaster
├── tf2_ros::TransformListener
└── message_filters::Synchronizer

本篇文章的目标不是让大家一次性背完所有 API,而是建立一个完整的 ROS2 C++ 工程知识框架。

学完本篇之后,至少要能判断:

复制代码
这段代码属于哪一类?
它在 ROS2 系统中负责什么功能?
它为什么要这样写?
它在机器人项目中解决什么问题?

其中:

复制代码
Service
├── 适合一次请求、一次响应的短任务

Action
├── 适合导航、巡检、路径跟踪等长时间任务

Parameter
├── 适合配置轮距、轮径、最大速度、控制频率等参数

QoS
├── 适合控制通信质量,尤其是雷达、IMU、相机等传感器数据

Executor
├── 负责调度回调函数执行

CallbackGroup
├── 控制多个回调之间是否允许并发执行

Lifecycle
├── 适合管理底盘驱动、传感器节点等生命周期

tf2
├── 适合维护 odom、base_link、laser、camera 等坐标关系

message_filters
└── 适合多传感器数据同步

后续第四篇会在本篇基础上继续深入 AGV 底盘运动控制算法写法,重点讲解 PID、运动学模型、RobotState、ChassisParam、std::array、std::vector、std::clamp、rclcpp::Time、dt 计算、里程计积分等内容。

所以,本篇可以理解为从"基础 ROS2 C++ 节点"走向"完整 ROS2 C++ 工程"的学习地图。


一、程序入口与节点相关写法

1.1 ROS2 C++ 程序入口基本结构

ROS2 C++ 程序通常从 main() 函数开始。

最常见结构如下:

cpp 复制代码
int main(int argc, char * argv[])
{
    rclcpp::init(argc, argv);

    auto node = std::make_shared<MyNode>();

    rclcpp::spin(node);

    rclcpp::shutdown();
    return 0;
}

这几行代码是 ROS2 C++ 节点运行的基础模板

可以理解成:

cpp 复制代码
main()
├── rclcpp::init()        初始化 ROS2
├── make_shared<MyNode>() 创建节点对象
├── rclcpp::spin()        让节点持续运行
└── rclcpp::shutdown()    关闭 ROS2

1.2 常见程序入口写法

写法 含义
#include "rclcpp/rclcpp.hpp" 引入 ROS2 C++ 核心头文件
rclcpp::init(argc, argv); 初始化 ROS2
rclcpp::shutdown(); 关闭 ROS2
rclcpp::spin(node); 让节点持续运行并处理回调
rclcpp::spin_some(node); 只处理当前已经到来的回调
rclcpp::spin_until_future_complete(node, future); 等待异步任务完成
auto node = std::make_shared<MyNode>(); 创建节点对象智能指针

1.3 spin()、spin_some()、spin_until_future_complete() 区别

(1)rclcpp::spin(node)

这是最常见的写法。

cpp 复制代码
rclcpp::spin(node);

作用是:

让节点一直运行,并持续等待回调函数被触发。

比如:

复制代码
rclcpp::spin(node)
├── 等待订阅者接收消息
├── 等待定时器周期触发
├── 等待服务端收到请求
├── 等待 Action 收到目标
└── 节点不会立刻退出

对于订阅者、服务端、Action 服务端来说,spin() 非常重要。

如果没有 spin(),节点创建完成后程序可能直接结束,回调函数就没有机会执行。

(2)rclcpp::spin_some(node)

cpp 复制代码
rclcpp::spin_some(node);

作用是:

只处理当前已经到来的回调,处理完就继续往下执行。

不会像 spin() 一样一直阻塞。

可以理解成:

复制代码
spin()
├── 一直等待
└── 节点持续运行

spin_some()
├── 有回调就处理一下
└── 处理完继续执行后面的代码

这种写法适合一些需要自己控制循环流程的程序

例如:

cpp 复制代码
while (rclcpp::ok())
{
    rclcpp::spin_some(node);
    // 自己的控制逻辑
}

(3)rclcpp::spin_until_future_complete(node, future)

cpp 复制代码
rclcpp::spin_until_future_complete(node, future);

这个写法常见于 Service 客户端。

因为Service 客户端发送请求后,需要等待服务端返回结果。

例如:

cpp 复制代码
auto result = client_->async_send_request(request);
rclcpp::spin_until_future_complete(node, result);

可以理解成:

复制代码
客户端发送请求
        ↓
等待服务端处理
        ↓
等待 future 返回
        ↓
拿到 response 结果

1.4 节点类相关写法

ROS2 C++ 中,自定义节点类通常这样写:

cpp 复制代码
class MyNode : public rclcpp::Node
{
public:
    MyNode() : Node("my_node")
    {
    }
};

常见写法如下:

|------------------------------------------|--------------------|
| 写法 | 含义 |
| class MyNode : public rclcpp::Node | 自定义一个 ROS2 C++ 节点类 |
| MyNode() : Node("my_node") | 构造函数中设置节点名 |
| this->get_logger() | 获取当前节点日志对象 |
| this->get_name() | 获取节点名称 |
| this->get_namespace() | 获取节点命名空间 |
| this->get_fully_qualified_name() | 获取完整节点名 |
| rclcpp::Node::make_shared("node_name") | 快速创建节点对象 |

可以这样理解:

复制代码
rclcpp::Node
├── 是 ROS2 C++ 节点基类
├── 提供创建发布者的能力
├── 提供创建订阅者的能力
├── 提供创建服务端的能力
├── 提供创建客户端的能力
├── 提供创建定时器的能力
├── 提供日志功能
├── 提供参数功能
└── 提供时间功能

二、Topic 发布订阅与 Timer 定时器写法

2.1 Publisher 发布者写法

发布者用于向某个话题发布消息。

例如小车速度控制中,常见写法是:

cpp 复制代码
rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr pub_vel_;

这行代码表示:

定义一个发布者对象,这个发布者专门发布 geometry_msgs::msg::Twist 类型的消息。

创建发布者:

cpp 复制代码
pub_vel_ = this->create_publisher<geometry_msgs::msg::Twist>(
    "/cmd_vel",
    10
);

发布消息:

cpp 复制代码
geometry_msgs::msg::Twist vel_cmd;

vel_cmd.linear.x = 0.1;
vel_cmd.angular.z = 0.0;

pub_vel_->publish(vel_cmd);

常见写法总结如下:

|---------------------------------------------|---------------|
| 写法 | 含义 |
| this->create_publisher<MsgT>("topic", 10) | 创建发布者 |
| rclcpp::Publisher<MsgT>::SharedPtr pub_; | 声明发布者成员变量 |
| pub_->publish(msg); | 发布消息 |
| rclcpp::QoS(10) | 设置队列深度 |
| rclcpp::SensorDataQoS() | 传感器数据常用 QoS |
| rclcpp::SystemDefaultsQoS() | 使用系统默认 QoS |

其中:

复制代码
MsgT

表示消息类型

例如:

复制代码
geometry_msgs::msg::Twist
std_msgs::msg::String
sensor_msgs::msg::LaserScan
nav_msgs::msg::Odometry

2.2 Subscription 订阅者写法

订阅者用于接收某个话题上的消息。

常见写法:

复制代码
rclcpp::Subscription<geometry_msgs::msg::Twist>::SharedPtr cmd_sub_;

创建订阅者:

cpp 复制代码
cmd_sub_ = this->create_subscription<geometry_msgs::msg::Twist>(
    "/cmd_vel",
    10,
    std::bind(&MyNode::cmd_callback, this, std::placeholders::_1)
);

回调函数:

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);
}

常见写法总结如下:

|------------------------------------------------------------|-------------|
| 写法 | 含义 |
| this->create_subscription<MsgT>("topic", 10, callback) | 创建订阅者 |
| rclcpp::Subscription<MsgT>::SharedPtr sub_; | 声明订阅者对象 |
| void callback(const MsgT::SharedPtr msg) | 常见回调函数参数 |
| void callback(MsgT::UniquePtr msg) | 独占指针消息回调 |
| void callback(const MsgT & msg) | 引用方式接收消息 |
| std::bind(&Class::callback, this, std::placeholders::_1) | 绑定成员函数回调 |
| [this](MsgT::SharedPtr msg){...} | lambda 回调写法 |


2.3 回调函数参数的三种常见写法

(1)SharedPtr 写法

cpp 复制代码
void callback(const geometry_msgs::msg::Twist::SharedPtr msg)
{
    RCLCPP_INFO(this->get_logger(), "%.2f", msg->linear.x);
}

这种写法最常见。

因为 msg 是智能指针,所以访问字段时用:

复制代码
msg->linear.x

(2)引用写法

cpp 复制代码
void callback(const geometry_msgs::msg::Twist & msg)
{
    RCLCPP_INFO(this->get_logger(), "%.2f", msg.linear.x);
}

这种写法中,msg 是普通引用,所以访问字段时用:

复制代码
msg.linear.x

(3)UniquePtr 写法

cpp 复制代码
void callback(geometry_msgs::msg::Twist::UniquePtr msg)
{
    RCLCPP_INFO(this->get_logger(), "%.2f", msg->linear.x);
}

UniquePtr 表示独占指针,常见于更关注性能和内存拷贝的场景


2.4 Timer 定时器写法

定时器用于周期性执行某个函数。

常见写法:

复制代码
rclcpp::TimerBase::SharedPtr timer_;

创建定时器:

cpp 复制代码
timer_ = this->create_wall_timer(
    500ms,
    std::bind(&MyNode::timer_callback, this)
);

定时器回调函数:

cpp 复制代码
void timer_callback()
{
    RCLCPP_INFO(this->get_logger(), "timer callback");
}

常见写法总结如下:

|--------------------------------------------|------------------|
| 写法 | 含义 |
| using namespace std::chrono_literals; | 允许写 500ms1s |
| this->create_wall_timer(500ms, callback) | 创建周期定时器 |
| rclcpp::TimerBase::SharedPtr timer_; | 声明定时器对象 |
| std::bind(&Class::timer_callback, this) | 绑定无参数定时回调 |
| [this](){ timer_callback(); } | lambda 定时器回调 |

在小车控制中,Timer 最常见的用途就是:

复制代码
每隔固定时间
        ↓
进入定时器回调
        ↓
计算速度指令
        ↓
发布 /cmd_vel

三、Service 与 Action 通信写法

3.1 Service 服务通信写法

Service 是 ROS2 中的一次请求、一次响应通信方式。

可以理解成:

复制代码
客户端发送请求
        ↓
服务端处理请求
        ↓
服务端返回响应
        ↓
客户端得到结果

Service 适合做:

  1. 开关控制
  2. 参数设置
  3. 状态查询
  4. 短时间任务
  5. 一次性控制命令

3.2 Service 服务端写法

服务端对象声明:

cpp 复制代码
rclcpp::Service<limo_msgs::srv::LimoSrv>::SharedPtr srv_;

创建服务端:

cpp 复制代码
srv_ = this->create_service<limo_msgs::srv::LimoSrv>(
    "/limo_srv",
    std::bind(&MyNode::srv_callback,
              this,
              std::placeholders::_1,
              std::placeholders::_2)
);

服务端回调函数:

cpp 复制代码
void srv_callback(
    const limo_msgs::srv::LimoSrv::Request::SharedPtr request,
    limo_msgs::srv::LimoSrv::Response::SharedPtr response)
{
    RCLCPP_INFO(this->get_logger(),
                "request x = %.2f, y = %.2f, z = %.2f",
                request->x,
                request->y,
                request->z);

    response->success = true;
}

常见写法总结:

|---------------------------------------------------|-----------|
| 写法 | 含义 |
| rclcpp::Service<SrvT>::SharedPtr srv_; | 服务端对象 |
| this->create_service<SrvT>("service", callback) | 创建服务端 |
| SrvT::Request::SharedPtr request | 服务端回调中的请求 |
| SrvT::Response::SharedPtr response | 服务端回调中的响应 |
| response->success = true; | 设置响应结果 |


3.3 Service 客户端写法

客户端对象声明:

cpp 复制代码
rclcpp::Client<limo_msgs::srv::LimoSrv>::SharedPtr client_;

创建客户端:

cpp 复制代码
client_ = this->create_client<limo_msgs::srv::LimoSrv>("/limo_srv");

等待服务端上线:

cpp 复制代码
while (!client_->wait_for_service(1s))
{
    RCLCPP_INFO(this->get_logger(), "waiting for service...");
}

创建请求:

cpp 复制代码
auto request = std::make_shared<limo_msgs::srv::LimoSrv::Request>();

request->x = 0.2;
request->y = 0.0;
request->z = 0.0;

发送请求:

cpp 复制代码
auto future = client_->async_send_request(request);

等待结果:

cpp 复制代码
if (rclcpp::spin_until_future_complete(this->get_node_base_interface(), future)
    == rclcpp::FutureReturnCode::SUCCESS)
{
    auto response = future.get();
    RCLCPP_INFO(this->get_logger(), "success = %d", response->success);
}

常见写法总结:

|-----------------------------------------------------|-----------|
| 写法 | 含义 |
| rclcpp::Client<SrvT>::SharedPtr client_; | 客户端对象 |
| this->create_client<SrvT>("service") | 创建客户端 |
| client_->wait_for_service(1s) | 等待服务端上线 |
| auto request = std::make_shared<SrvT::Request>(); | 创建请求对象 |
| client_->async_send_request(request) | 异步发送请求 |
| future.get() | 获取服务端返回结果 |


3.4 Action 动作通信写法

Action 适合长时间任务。

例如:

复制代码
导航到目标点
巡检任务
机械臂运动
底盘运动一段时间
AGV 执行路径跟踪

Action 的通信过程是:

复制代码
客户端发送 Goal
        ↓
服务端接收 Goal
        ↓
服务端执行任务
        ↓
执行过程中不断反馈 Feedback
        ↓
执行结束返回 Result

也就是说,Action 比 Service 多了一个过程反馈。

Service 是:

复制代码
请求 → 响应

Action 是:

复制代码
目标 → 过程反馈 → 最终结果

3.5 Action 服务端常见写法

引入头文件:

cpp 复制代码
#include "rclcpp_action/rclcpp_action.hpp"

类型别名:

cpp 复制代码
using LimoAction = limo_msgs::action::LimoAction;
using GoalHandleLimoAction = rclcpp_action::ServerGoalHandle<LimoAction>;

Action 服务端对象:

cpp 复制代码
rclcpp_action::Server<LimoAction>::SharedPtr action_server_;

创建 Action 服务端:

cpp 复制代码
action_server_ = rclcpp_action::create_server<LimoAction>(
    this,
    "/limo_action",
    std::bind(&MyNode::handle_goal, this, std::placeholders::_1, std::placeholders::_2),
    std::bind(&MyNode::handle_cancel, this, std::placeholders::_1),
    std::bind(&MyNode::handle_accepted, this, std::placeholders::_1)
);

常见写法总结:

|-------------------------------------------------------------|-------------------|
| 写法 | 含义 |
| rclcpp_action::Server<ActionT>::SharedPtr action_server_; | Action 服务端对象 |
| rclcpp_action::create_server<ActionT>(...) | 创建 Action 服务端 |
| GoalHandle<ActionT> | 管理一个 Action 目标任务 |
| goal_handle->get_goal() | 获取客户端发来的目标 |
| goal_handle->publish_feedback(feedback) | 发布过程反馈 |
| goal_handle->succeed(result) | 返回成功结果 |
| goal_handle->abort(result) | 返回失败结果 |
| goal_handle->canceled(result) | 返回取消结果 |


3.6 Action 客户端常见写法

Action 客户端对象:

cpp 复制代码
rclcpp_action::Client<LimoAction>::SharedPtr action_client_;

创建客户端:

cpp 复制代码
action_client_ = rclcpp_action::create_client<LimoAction>(
    this,
    "/limo_action"
);

等待服务端:

cpp 复制代码
if (!action_client_->wait_for_action_server(5s))
{
    RCLCPP_ERROR(this->get_logger(), "Action server not available");
    return;
}

创建目标:

cpp 复制代码
auto goal_msg = LimoAction::Goal();

goal_msg.x = 0.2;
goal_msg.y = 0.0;
goal_msg.z = 0.0;

发送目标:

cpp 复制代码
auto send_goal_options =
    rclcpp_action::Client<LimoAction>::SendGoalOptions();

action_client_->async_send_goal(goal_msg, send_goal_options);

常见写法总结:

|-------------------------------------------------------------|-------------------|
| 写法 | 含义 |
| rclcpp_action::Client<ActionT>::SharedPtr action_client_; | Action 客户端对象 |
| rclcpp_action::create_client<ActionT>(...) | 创建 Action 客户端 |
| wait_for_action_server(5s) | 等待 Action 服务端上线 |
| ActionT::Goal goal_msg; | 创建目标消息 |
| async_send_goal(goal_msg, options) | 发送 Action 目标 |
| feedback_callback | 接收过程反馈 |
| result_callback | 接收最终结果 |


四、参数、日志、时间与 Graph 查询写法

4.1 Parameter 参数写法

ROS2 参数用于配置节点运行时的数据。

例如:

  1. 小车最大速度
  2. 控制频率
  3. 底盘轮距
  4. 是否开启调试模式
  5. 雷达话题名称
  6. 坐标系名称
  1. 如果这些值都写死在代码里,每次修改都需要重新编译。
  2. 使用参数后,可以通过 launch 文件或者命令行修改。

4.2 参数常见写法

(1)声明参数:

复制代码
this->declare_parameter("speed", 0.2);

(2)获取参数:

复制代码
double speed;
this->get_parameter("speed", speed);

(3)设置参数:

复制代码
this->set_parameter(rclcpp::Parameter("speed", 0.3));

(4)参数变化回调:

复制代码
this->add_on_set_parameters_callback(
    std::bind(&MyNode::param_callback, this, std::placeholders::_1)
);

常见写法总结:

|---------------------------------------------------------|---------|
| 写法 | 含义 |
| this->declare_parameter("speed", 0.2); | 声明参数 |
| this->get_parameter("speed", speed); | 获取参数 |
| this->set_parameter(rclcpp::Parameter("speed", 0.3)); | 设置参数 |
| this->list_parameters(...) | 列出参数 |
| this->describe_parameter("speed") | 查看参数描述 |
| this->add_on_set_parameters_callback(callback) | 参数变化回调 |
| rclcpp::Parameter | 参数对象 |
| rclcpp::ParameterValue | 参数值对象 |
| rclcpp::SyncParametersClient | 同步参数客户端 |
| rclcpp::AsyncParametersClient | 异步参数客户端 |
| rclcpp::ParameterEventHandler | 监听参数变化 |

可以这样理解:

复制代码
Parameter
├── 不修改代码
├── 不重新编译
├── 可以改变节点运行配置
└── 非常适合机器人调参

4.3 Logging 日志写法

ROS2 C++ 中常用日志宏如下:

|-------------------------------------------|-----------|
| 写法 | 含义 |
| RCLCPP_DEBUG(this->get_logger(), "...") | 调试日志 |
| RCLCPP_INFO(this->get_logger(), "...") | 普通信息 |
| RCLCPP_WARN(this->get_logger(), "...") | 警告 |
| RCLCPP_ERROR(this->get_logger(), "...") | 错误 |
| RCLCPP_FATAL(this->get_logger(), "...") | 致命错误 |
| RCLCPP_INFO_ONCE(...) | 只打印一次 |
| RCLCPP_WARN_THROTTLE(...) | 限频打印 |
| this->get_logger() | 获取当前节点日志器 |

普通日志:

复制代码
RCLCPP_INFO(this->get_logger(), "node has started.");

打印变量:

复制代码
RCLCPP_INFO(this->get_logger(), "speed = %.2f", speed);

警告日志:

复制代码
RCLCPP_WARN(this->get_logger(), "battery voltage is low.");

错误日志:

复制代码
RCLCPP_ERROR(this->get_logger(), "failed to connect chassis.");

可以这样区分:

复制代码
DEBUG
├── 调试信息

INFO
├── 普通运行信息

WARN
├── 程序还能运行,但需要注意

ERROR
├── 出现错误,需要处理

FATAL
└── 严重错误,程序可能无法继续运行

4.4 Time、Duration、Rate 时间相关写法

常见写法如下:

|-----------------------------------|-----------------|
| 写法 | 含义 |
| this->now() | 获取当前 ROS 时间 |
| rclcpp::Time time; | ROS2 时间对象 |
| rclcpp::Duration duration; | 时间间隔 |
| rclcpp::Clock clock; | 时钟对象 |
| rclcpp::Rate rate(10); | 10Hz 循环频率 |
| rate.sleep(); | 按频率休眠 |
| msg.header.stamp = this->now(); | 给消息打时间戳 |

例如自定义状态消息中常见写法:

cpp 复制代码
msg.header.stamp = this->now();
msg.header.frame_id = "base_link";

含义是:

复制代码
header.stamp
├── 当前时间戳

header.frame_id
└── 当前消息所属坐标系
  1. 在机器人中,时间戳非常重要。
  2. 因为雷达、IMU、里程计、相机数据都需要根据时间对齐。

4.5 Graph 查询相关写法

Graph 可以理解成 ROS2 当前通信系统的关系图。

例如:

  • 当前有哪些节点?
  • 当前有哪些话题?
  • 某个话题有几个发布者?
  • 某个话题有几个订阅者?
  • 当前有哪些服务?

常见写法如下:

|---------------------------------------|--------------|
| 写法 | 含义 |
| this->get_topic_names_and_types() | 获取当前话题和类型 |
| this->count_publishers("/cmd_vel") | 统计某话题发布者数量 |
| this->count_subscribers("/cmd_vel") | 统计某话题订阅者数量 |
| this->get_service_names_and_types() | 获取服务名称和类型 |
| this->wait_for_graph_change(...) | 等待 ROS 图发生变化 |

例如:

cpp 复制代码
auto topic_names = this->get_topic_names_and_types();

for (const auto & topic : topic_names)
{
    RCLCPP_INFO(this->get_logger(), "topic: %s", topic.first.c_str());
}

这类写法在调试工具节点、监控节点中比较常见。


五、QoS、Executor 与 CallbackGroup 写法

5.1 QoS 是什么?

QoS 全称是 Quality of Service,意思是通信质量策略

在 ROS2 中,QoS 用来控制消息通信的行为

比如:

  1. 消息要不要可靠送达?
  2. 消息队列保留多少条?
  3. 订阅者晚启动时,要不要收到之前的数据?
  4. 传感器数据是否允许丢帧?

这就是 QoS 要解决的问题。


5.2 QoS 常见写法

|-----------------------------|----------------------|
| 写法 | 含义 |
| rclcpp::QoS(10) | 队列深度 10 |
| rclcpp::KeepLast(10) | 只保留最近 10 条 |
| qos.reliable() | 可靠传输 |
| qos.best_effort() | 尽力传输 |
| qos.durability_volatile() | 不保留历史数据 |
| qos.transient_local() | 类似 ROS1 latch,保留最后数据 |
| rclcpp::SensorDataQoS() | 传感器数据常用 |
| rclcpp::ParametersQoS() | 参数服务常用 |
| rclcpp::ServicesQoS() | 服务通信常用 |

普通写法:

cpp 复制代码
auto pub = this->create_publisher<std_msgs::msg::String>(
    "/chatter",
    rclcpp::QoS(10)
);

更完整写法:

cpp 复制代码
auto qos = rclcpp::QoS(rclcpp::KeepLast(10));
qos.reliable();
qos.durability_volatile();

pub_ = this->create_publisher<std_msgs::msg::String>(
    "/chatter",
    qos
);

传感器数据常用写法:

cpp 复制代码
sub_ = this->create_subscription<sensor_msgs::msg::LaserScan>(
    "/scan",
    rclcpp::SensorDataQoS(),
    std::bind(&MyNode::scan_callback, this, std::placeholders::_1)
);

可以简单记住:

复制代码
普通控制话题
├── rclcpp::QoS(10)

雷达、相机、IMU 等传感器数据
├── rclcpp::SensorDataQoS()

参数相关通信
├── rclcpp::ParametersQoS()

服务通信
└── rclcpp::ServicesQoS()

5.3 Executor 执行器写法

Executor 可以理解成:

负责执行回调函数的调度器。

ROS2 中的回调函数包括:

  1. 订阅者回调
  2. 定时器回调
  3. 服务端回调
  4. 客户端响应回调
  5. Action 回调
  6. 参数变化回调

这些回调什么时候执行,通常由 Executor 负责

最基础写法:

cpp 复制代码
rclcpp::spin(node);

其实背后也可以理解成使用了默认的执行机制。

更明确的写法是:

(1)单线程执行器:

cpp 复制代码
rclcpp::executors::SingleThreadedExecutor executor;
executor.add_node(node);
executor.spin();

(2)多线程执行器:

cpp 复制代码
rclcpp::executors::MultiThreadedExecutor executor;
executor.add_node(node);
executor.spin();

常见写法总结:

|-------------------------------------------------------------|--------|
| 写法 | 含义 |
| rclcpp::executors::SingleThreadedExecutor executor; | 单线程执行器 |
| rclcpp::executors::MultiThreadedExecutor executor; | 多线程执行器 |
| executor.add_node(node); | 添加节点 |
| executor.spin(); | 执行回调 |


5.4 SingleThreadedExecutor 与 MultiThreadedExecutor 区别

(1)单线程执行器

复制代码
rclcpp::executors::SingleThreadedExecutor executor;

可以理解成:

  1. 所有回调函数排队执行
  2. 一个执行完,再执行下一个
  • 优点是简单、安全、不容易出现多线程数据竞争
  • 缺点是如果某个回调函数执行时间太长,其他回调就会被阻塞

(2)多线程执行器

复制代码
rclcpp::executors::MultiThreadedExecutor executor;

可以理解成:

  1. 多个回调函数可以并发执行
  2. 不同回调可能在不同线程中运行
  • 优点是可以提升并发能力
  • 缺点是需要考虑线程安全问题

例如 AGV 底盘控制中可能同时存在:

复制代码
/cmd_vel 订阅回调
/odom 里程计发布定时器
/imu IMU 订阅回调
/scan 雷达订阅回调
控制周期定时器

如果某个回调阻塞,可能影响控制实时性

这时候就可能需要 MultiThreadedExecutor 和 CallbackGroup。


5.5 CallbackGroup 回调组写法

CallbackGroup 用来控制回调之间的并发关系。

常见写法:

cpp 复制代码
auto callback_group =
    this->create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive);

或者:

cpp 复制代码
auto callback_group =
    this->create_callback_group(rclcpp::CallbackGroupType::Reentrant);

常见类型:

|------------------------------------------------|----------------|
| 写法 | 含义 |
| rclcpp::CallbackGroupType::MutuallyExclusive | 同组回调互斥执行 |
| rclcpp::CallbackGroupType::Reentrant | 同组回调允许并发执行 |

给订阅者指定回调组:

cpp 复制代码
rclcpp::SubscriptionOptions options;
options.callback_group = callback_group;

sub_ = this->create_subscription<std_msgs::msg::String>(
    "/chatter",
    10,
    std::bind(&MyNode::callback, this, std::placeholders::_1),
    options
);

可以这样理解:

复制代码
MutuallyExclusive
├── 同一个组里的回调不会同时执行
└── 更安全

Reentrant
├── 同一个组里的回调可以同时执行
└── 更灵活,但要注意线程安全

六、Lifecycle、Component、tf2 与 message_filters 写法

6.1 LifecycleNode 生命周期节点写法

  1. 普通节点只有启动和关闭。
  2. 生命周期节点可以把节点运行过程分成多个状态。

常见状态包括:

复制代码
配置 configure
激活 activate
停用 deactivate
清理 cleanup
关闭 shutdown

常见写法:

cpp 复制代码
#include "rclcpp_lifecycle/lifecycle_node.hpp"

定义生命周期节点:

cpp 复制代码
class MyLifecycleNode : public rclcpp_lifecycle::LifecycleNode
{
public:
    MyLifecycleNode() : LifecycleNode("my_lifecycle_node")
    {
    }
};

生命周期回调:

cpp 复制代码
CallbackReturn on_configure(const rclcpp_lifecycle::State &)
{
    RCLCPP_INFO(this->get_logger(), "on_configure");
    return CallbackReturn::SUCCESS;
}

常见写法总结:

|---------------------------------------------------------|----------|
| 写法 | 含义 |
| #include "rclcpp_lifecycle/lifecycle_node.hpp" | 引入生命周期节点 |
| class MyNode : public rclcpp_lifecycle::LifecycleNode | 定义生命周期节点 |
| on_configure() | 配置阶段 |
| on_activate() | 激活阶段 |
| on_deactivate() | 停用阶段 |
| on_cleanup() | 清理阶段 |
| on_shutdown() | 关闭阶段 |
| LifecyclePublisher | 生命周期发布者 |

生命周期节点适合:

复制代码
机器人驱动
底盘控制
传感器节点
导航相关节点
需要明确启动和停止状态的模块

例如底盘驱动节点可以这样设计:

复制代码
on_configure()
├── 读取参数
├── 初始化串口
├── 创建发布者和订阅者

on_activate()
├── 开始发布状态
├── 开始接收速度指令

on_deactivate()
├── 停止发送速度
├── 暂停底盘控制

on_cleanup()
└── 释放资源

6.2 Component 组件化节点写法

组件化可以把多个 ROS2 节点放到同一个进程中运行。

普通节点是:

复制代码
一个节点
├── 一个进程

组件化节点可以是:

复制代码
一个进程
├── 节点 A
├── 节点 B
├── 节点 C
└── 节点 D

常见写法:

复制代码
#include "rclcpp_components/register_node_macro.hpp"

注册组件节点:

cpp 复制代码
RCLCPP_COMPONENTS_REGISTER_NODE(MyNode)

构造函数常见写法:

cpp 复制代码
class MyNode : public rclcpp::Node
{
public:
    explicit MyNode(const rclcpp::NodeOptions & options)
    : Node("my_node", options)
    {
    }
};

常见写法总结:

|--------------------------------------------------------|----------------|
| 写法 | 含义 |
| #include "rclcpp_components/register_node_macro.hpp" | 引入组件注册宏 |
| RCLCPP_COMPONENTS_REGISTER_NODE(MyNode) | 注册组件节点 |
| rclcpp::NodeOptions options | 组件节点常用构造参数 |
| component_container | 组件容器 |
| ros2 component load ... | 动态加载组件 |
| ComposableNodeContainer | launch 中启动组件容器 |

组件化适合:

复制代码
减少进程数量
降低节点间通信开销
提高系统集成度
大型机器人系统模块化管理

初学阶段可以先了解,后面做复杂工程时再深入。


6.3 tf2 坐标变换写法

机器人系统中,经常会遇到不同坐标系之间的关系。

例如 AGV 底盘中常见坐标系:

复制代码
map
├── 地图坐标系

odom
├── 里程计坐标系

base_link
├── 机器人底盘坐标系

laser
├── 雷达坐标系

camera
└── 相机坐标系

tf2 的作用就是维护这些坐标系之间的变换关系。

常见写法如下:

|----------------------------------------|-----------|
| 写法 | 含义 |
| tf2_ros::TransformBroadcaster | 动态坐标变换发布器 |
| tf2_ros::StaticTransformBroadcaster | 静态坐标变换发布器 |
| tf2_ros::Buffer | 坐标变换缓存 |
| tf2_ros::TransformListener | 坐标变换监听器 |
| geometry_msgs::msg::TransformStamped | 坐标变换消息 |
| buffer_->lookupTransform(...) | 查询坐标变换 |
| tf2::doTransform(...) | 对数据进行坐标变换 |

发布坐标变换常见写法:

cpp 复制代码
geometry_msgs::msg::TransformStamped transform;

transform.header.stamp = this->now();
transform.header.frame_id = "odom";
transform.child_frame_id = "base_link";

transform.transform.translation.x = x;
transform.transform.translation.y = y;
transform.transform.translation.z = 0.0;

tf_broadcaster_->sendTransform(transform);

可以理解成:

复制代码
odom
 └── base_link

表示发布从 odombase_link 的坐标变换关系。

在 AGV 底盘控制中,tf2 非常重要。

因为导航系统需要知道:

复制代码
机器人现在在哪里?
雷达相对底盘在哪里?
相机相对底盘在哪里?
里程计坐标和底盘坐标是什么关系?

6.4 message_filters 多传感器同步写法

message_filters 主要用于多个话题消息的同步。

例如:

复制代码
相机图像
相机内参
雷达点云
IMU 数据
里程计数据

这些数据可能不是同一时刻到达的。

如果算法需要同时使用多种传感器数据,就需要进行时间同步

常见写法如下:

|---------------------------------------------------|---------------------|
| 写法 | 含义 |
| message_filters::Subscriber<MsgT> | message_filters 订阅器 |
| message_filters::TimeSynchronizer | 精确时间同步 |
| message_filters::sync_policies::ApproximateTime | 近似时间同步 |
| message_filters::Synchronizer<Policy> | 同步器 |
| sync->registerCallback(...) | 注册同步后的回调 |

可以这样理解:

复制代码
普通订阅者
├── 一个话题来一条消息
└── 就触发一次回调

message_filters
├── 多个话题都收到合适时间的数据
└── 再统一触发一个同步回调

例如:

复制代码
/scan
/odom
/imu
        ↓
message_filters 时间同步
        ↓
统一进入算法回调

在多传感器融合、SLAM、定位、导航中,这类写法很常见。


七、面向 AGV 底盘控制的 ROS2 C++ 写法总结

7.1 AGV 底盘控制中最常见的 ROS2 C++ 模块

做 AGV 底盘运动控制算法,常见模块大概是:

复制代码
/cmd_vel
├── 接收速度控制指令

/odom
├── 发布里程计信息

/imu
├── 接收 IMU 姿态和角速度

/scan
├── 接收激光雷达数据

/tf
├── 发布坐标变换

参数系统
├── 配置轮距、轮径、最大速度、控制频率

Timer
├── 周期性执行控制循环

Executor
├── 调度多个回调函数

CallbackGroup
├── 避免回调阻塞和死锁

QoS
├── 处理传感器数据通信质量

Action
└── 执行导航、巡检、路径跟踪等长时间任务

所以对于 AGV 来说,不能只会写 Publisher 和 Subscriber。

还需要逐步掌握:

复制代码
Service
├── 适合短任务请求

Action
├── 适合长时间运动任务

Parameter
├── 适合底盘参数配置

Timer
├── 适合固定频率控制循环

tf2
├── 适合坐标变换

Executor
├── 适合回调调度

CallbackGroup
└── 适合多线程回调管理

7.2 不同写法在 AGV 中的作用

|-----------------|------------------------------|
| 写法模块 | AGV 中的典型作用 |
| Publisher | 发布 /odom、底盘状态、调试信息 |
| Subscription | 订阅 /cmd_vel/imu/scan |
| Timer | 固定频率执行底盘控制算法 |
| Service | 查询状态、清除故障、设置模式 |
| Action | 执行导航、巡检、路径跟踪任务 |
| Parameter | 配置轮距、轮径、最大速度 |
| Logging | 输出调试信息和错误信息 |
| QoS | 适配雷达、IMU 等传感器数据 |
| Executor | 调度多个回调函数 |
| CallbackGroup | 控制回调并发执行 |
| Lifecycle | 管理驱动节点启动、激活、关闭 |
| tf2 | 发布 odom -> base_link 坐标变换 |
| message_filters | 同步多传感器数据 |


7.3 学习顺序建议

ROS2 C++ 常见写法,本质上就是围绕"节点如何运行、如何通信、如何调度、如何配置、如何和机器人坐标系统关联"展开的。

不要一上来就把所有写法都背下来。

更建议按照下面顺序学习:

复制代码
第一阶段:基础节点
├── rclcpp::Node
├── init / spin / shutdown
├── Publisher
├── Subscription
└── Timer

第二阶段:基础通信
├── Topic
├── Service
├── Client
├── Action Server
└── Action Client

第三阶段:工程增强
├── Parameter
├── Logging
├── QoS
├── Time
└── Graph

第四阶段:进阶控制
├── Executor
├── CallbackGroup
├── LifecycleNode
├── Component
├── tf2
└── message_filters

对于小车控制和 AGV 底盘运动控制来说,最核心的组合是:

复制代码
Subscription
├── 接收 /cmd_vel

Timer
├── 固定频率执行控制算法

Publisher
├── 发布 /odom 和底盘状态

tf2
├── 发布 odom -> base_link

Parameter
├── 配置底盘参数

QoS
├── 适配传感器通信

Executor + CallbackGroup
└── 管理多个回调并发执行

八、总结

本篇文章作为 ROS2 C++ 系列的第三篇,主要对 ROS2 C++ 进阶写法进行了系统分类。

前两篇文章分别解决了两个基础问题:

复制代码
第一篇:ROS2 C++ 常见 rclcpp 写法
├── 先看懂基础节点
├── Node
├── Publisher
├── Subscription
└── Timer

第二篇:ROS2 C++ 基础语法
├── 再补齐 C++ 基础
├── class
├── auto
├── const
├── this
├── . 和 ->
├── SharedPtr
├── std::make_shared
├── std::bind
└── lambda

而本篇继续扩展到完整 ROS2 C++ 工程中经常遇到的模块:

复制代码
Topic 通信
├── Publisher
├── Subscription
└── Timer

Service 通信
├── Service
└── Client

Action 通信
├── Action Server
├── Action Client
├── Goal
├── Feedback
└── Result

参数、日志与时间
├── Parameter
├── Logging
├── Time
└── Graph

通信质量与回调调度
├── QoS
├── Executor
└── CallbackGroup

机器人进阶模块
├── LifecycleNode
├── Component
├── tf2
└── message_filters

如果只是写一个简单的 ROS2 节点,只掌握 Publisher、Subscription、Timer 基本够用。

但是如果要做机器人底盘控制、AGV 运动控制、导航、SLAM、多传感器融合,就必须继续理解:

复制代码
Service
├── 用于短任务请求,例如清除故障、查询状态、切换模式

Action
├── 用于长时间任务,例如导航、巡检、路径跟踪

Parameter
├── 用于参数配置,例如轮距、轮径、最大速度、控制频率

QoS
├── 用于通信质量控制,例如传感器数据是否允许丢帧

Executor
├── 用于回调调度

CallbackGroup
├── 用于并发控制和避免回调阻塞

Lifecycle
├── 用于管理节点启动、配置、激活、停止、清理过程

tf2
├── 用于坐标变换,例如 odom -> base_link

message_filters
└── 用于多传感器时间同步

所以,本篇可以作为 ROS2 C++ 进阶写法的第二层学习地图。

建议学习顺序是:

复制代码
第一阶段:基础节点
├── rclcpp::Node
├── init / spin / shutdown
├── Publisher
├── Subscription
└── Timer

第二阶段:基础语法
├── class
├── auto
├── const
├── this
├── . 和 ->
├── SharedPtr
└── std::bind

第三阶段:进阶通信与工程模块
├── Service
├── Action
├── Parameter
├── QoS
├── Executor
├── CallbackGroup
├── Lifecycle
├── tf2
└── message_filters

第四阶段:底盘运动控制算法
├── PID
├── 运动学模型
├── RobotState
├── ChassisParam
├── dt 计算
├── 速度限幅
└── 里程计更新

下一篇将继续进入:

复制代码
第四篇:ROS2 C++ 底盘运动控制算法写法

也就是把前面学到的 rclcpp、C++ 基础语法、Service、Action、Parameter、QoS、Executor、tf2 等内容,进一步放到底盘运动控制算法中理解。

到了第四篇,就不只是看懂 ROS2 C++ 代码,而是开始理解:

复制代码
为什么底盘控制要封装 PID?
为什么要封装运动学模型?
为什么要定义 RobotState?
为什么要定义 ChassisParam?
为什么控制循环必须计算 dt?
为什么速度输出要限幅?
为什么里程计更新要用 sin、cos、atan2?

这样学习下来,就能从 ROS2 C++ 基础节点,逐步过渡到 AGV 底盘运动控制算法开发。