ROS2 C++ 小车控制完整实战(二):自定义 msg 消息发布与订阅保姆级教程

目录

摘要

前言

一、本章案例整体说明

[1.1 Topic 和 msg 的关系](#1.1 Topic 和 msg 的关系)

[1.2 本篇要实现什么](#1.2 本篇要实现什么)

[1.3 本篇涉及的功能包](#1.3 本篇涉及的功能包)

[二、自定义 msg 接口文件](#二、自定义 msg 接口文件)

[2.1 为什么需要自定义 LimoStatus.msg](#2.1 为什么需要自定义 LimoStatus.msg)

[2.2 LimoStatus.msg 文件位置](#2.2 LimoStatus.msg 文件位置)

[2.3 LimoStatus.msg 文件内容](#2.3 LimoStatus.msg 文件内容)

[2.4 字段含义说明](#2.4 字段含义说明)

[2.5 std_msgs/Header header 是什么](#2.5 std_msgs/Header header 是什么)

[2.6 msg 文件注意事项](#2.6 msg 文件注意事项)

(1)字段后面不需要写分号

(2)如果使用了其它功能包里的类型,需要在配置文件中声明依赖。

[(3)修改 .msg 文件后必须重新编译。](#(3)修改 .msg 文件后必须重新编译。)

[三、配置自定义接口包 limo_msgs](#三、配置自定义接口包 limo_msgs)

[3.1 rosidl_generate_interfaces 是什么](#3.1 rosidl_generate_interfaces 是什么)

[3.2 limo_msgs 的 CMakeLists.txt](#3.2 limo_msgs 的 CMakeLists.txt)

[3.3 CMakeLists.txt 核心解释](#3.3 CMakeLists.txt 核心解释)

[3.4 limo_msgs 的 package.xml](#3.4 limo_msgs 的 package.xml)

[3.5 package.xml 核心解释](#3.5 package.xml 核心解释)

[四、C++ 发布自定义 msg 状态消息](#四、C++ 发布自定义 msg 状态消息)

[4.1 自定义 msg 发布节点作用](#4.1 自定义 msg 发布节点作用)

[4.2 自定义 msg 发布完整代码](#4.2 自定义 msg 发布完整代码)

[4.3 代码核心名字和对象总结](#4.3 代码核心名字和对象总结)

(1)LimoStatusPub:自定义节点类

[(2) "limo_status_pub":节点名称](#(2) "limo_status_pub":节点名称)

(3)status_pub_:发布者对象

(4)"/limo_status":话题名称

(5)timer_:定时器对象

(6)timer_callback():定时器回调函数

(7)status_msg:消息对象

(8)核心总结:

[4.4 发布端核心代码解释](#4.4 发布端核心代码解释)

(1)引入自定义消息头文件:

(2)创建自定义消息发布器:

(3)创建定时器:

[(4)回调函数 timer_callback 的作用](#(4)回调函数 timer_callback 的作用)

[五、C++ 订阅自定义 msg 状态消息](#五、C++ 订阅自定义 msg 状态消息)

[5.1 为什么还要写 Subscriber](#5.1 为什么还要写 Subscriber)

[5.2 自定义 msg 订阅完整代码](#5.2 自定义 msg 订阅完整代码)

[5.3 订阅器核心代码解释](#5.3 订阅器核心代码解释)

(1)创建订阅器的代码如下:

[(2) 回调函数 status_callback 的作用](#(2) 回调函数 status_callback 的作用)

[5.4 Publisher 和 Subscriber 的关系](#5.4 Publisher 和 Subscriber 的关系)

[六、配置代码包 limo_learning](#六、配置代码包 limo_learning)

[6.1 limo_learning 的 package.xml](#6.1 limo_learning 的 package.xml)

[6.2 limo_learning 的 CMakeLists.txt](#6.2 limo_learning 的 CMakeLists.txt)

[6.3 CMakeLists.txt 核心理解](#6.3 CMakeLists.txt 核心理解)

七、编译与运行测试

[7.1 先编译自定义接口包](#7.1 先编译自定义接口包)

[7.2 再编译代码包](#7.2 再编译代码包)

[7.3 查看自定义 msg 是否生成成功](#7.3 查看自定义 msg 是否生成成功)

[7.4 运行自定义 msg 发布节点](#7.4 运行自定义 msg 发布节点)

[7.5 运行自定义 msg 订阅节点](#7.5 运行自定义 msg 订阅节点)

[7.6 使用命令查看话题](#7.6 使用命令查看话题)

八、常见问题总结

[8.1 找不到 limo_msgs/msg/limo_status.hpp](#8.1 找不到 limo_msgs/msg/limo_status.hpp)

[8.2 修改 LimoStatus.msg 后代码不生效](#8.2 修改 LimoStatus.msg 后代码不生效)

[8.3 ros2 interface show 找不到接口](#8.3 ros2 interface show 找不到接口)

[8.4 ros2 run 找不到节点](#8.4 ros2 run 找不到节点)

[8.5 订阅节点没有任何输出](#8.5 订阅节点没有任何输出)

[8.6 回调函数没有执行](#8.6 回调函数没有执行)

九、本章总结


摘要

上一篇文章主要讲解了 ROS2 C++ 中最基础的 Topic 话题通信,

ROS2 C++ 小车控制完整实战(一):一文搞懂 Topic 发布订阅、Twist 消息与 /cmd_vel 速度控制-CSDN博客https://blog.csdn.net/m0_58954356/article/details/161829804?spm=1001.2014.3001.5502包括:

  1. Topic + 官方 msg:geometry_msgs/msg/Twist
  2. Topic + 官方 msg:sensor_msgs/msg/LaserScan
  3. /cmd_vel 速度控制
  4. /scan 雷达数据订阅
  5. Publisher 发布器
  6. Subscriber 订阅器

通过上一篇内容,我们已经知道:

  • Topic 通信一定使用 msg 类型数据
  • msg 可以是 ROS2 官方标准消息 ,也可以是我们自己定义的自定义消息

上一篇主要使用的是 ROS2 官方已经提供好的标准消息,例如:

复制代码
geometry_msgs/msg/Twist
sensor_msgs/msg/LaserScan

这一篇继续往后走,开始讲ROS2 自定义 msg 消息实践。

本篇主要围绕一个自定义接口展开:

  1. 自定义 msg:limo_msgs/msg/LimoStatus

本篇要实现的功能包括:

  1. Topic + 自定义 msg:发布和订阅 LIMO 小车状态信息

通过本篇文章,需要重点理解:

  1. 自定义 msg 文件怎么写
  2. std_msgs/Header header 是什么
  3. 接口包 limo_msgs 如何配置
  4. 代码包 limo_learning 如何依赖自定义 msg
  5. C++ 中如何发布自定义消息
  6. C++ 中如何订阅自定义消息
  7. 自定义 msg 修改后为什么必须重新编译

前言

在 ROS2 中,通信接口是非常重要的一部分。

上一篇我们已经通过 /cmd_vel/scan 理解了 Topic 通信。

其中:

复制代码
/cmd_vel 使用 geometry_msgs/msg/Twist
/scan 使用 sensor_msgs/msg/LaserScan

这两个都是 ROS2 官方提供的标准 msg。

但是在实际机器人项目中,官方消息并不能覆盖所有业务需求。

比如我们想发布 LIMO 小车自己的状态信息:

  1. 车辆状态
  2. 控制模式
  3. 电池电压
  4. 错误码
  5. 运动模式

这时候用标准的 TwistLaserScanString 都不太合适。

因为这些字段属于具体机器人项目中的业务状态,标准消息不一定刚好满足。

更好的方式是自己定义一个消息:

复制代码
limo_msgs/msg/LimoStatus

然后通过 Topic 发布出去,再由其它节点订阅接收。

所以本篇文章的重点就是:

从官方标准 msg 走向自定义 msg

也就是从"会用 ROS2 自带消息",进一步到"会写自己的 ROS2 消息接口"。


一、本章案例整体说明

1.1 Topic 和 msg 的关系

很多初学者刚开始学习 ROS2 时,会把 Topic 和 msg 混在一起。

其实它们之间的关系可以这样理解:

  1. Topic 是通信方式
  2. msg 是 Topic 中传输的数据结构

也就是说:

Topic 通信一定要使用 msg 类型数据

但是 msg 分为两种:

  1. 官方标准 msg
  2. 自定义 msg

例如:

通信方式 对应接口类型 示例 是否自定义
Topic msg geometry_msgs/msg/Twist 官方标准 msg
Topic msg sensor_msgs/msg/LaserScan 官方标准 msg
Topic msg limo_msgs/msg/LimoStatus 自定义 msg

上一篇文章主要讲的是:

复制代码
Topic + 官方标准 msg

这一篇主要讲的是:

复制代码
Topic + 自定义 msg

1.2 本篇要实现什么

本篇要实现一个完整的自定义 msg 发布和订阅案例。

也就是定义一个:

复制代码
LimoStatus.msg

用来描述小车状态。

然后在 C++ 中分别写两个节点:

复制代码
limo_status_pub:发布小车状态
limo_status_sub:订阅小车状态

整体流程如下:

复制代码
定义 LimoStatus.msg
        ↓
配置 limo_msgs 接口包
        ↓
编译生成自定义消息头文件
        ↓
在 limo_learning 中 include 自定义消息
        ↓
编写 Publisher 发布节点
        ↓
编写 Subscriber 订阅节点
        ↓
运行发布端和订阅端测试

1.3 本篇涉及的功能包

建议使用两个功能包:

复制代码
limo_msgs        存放自定义 msg 接口
limo_learning    存放 C++ 节点代码

整体目录结构如下:

复制代码
agilex_open_class_ws/
└── src
    ├── limo_msgs
    │   ├── msg
    │   │   └── LimoStatus.msg
    │   ├── CMakeLists.txt
    │   └── package.xml
    │
    └── limo_learning
        ├── src
        │   ├── limo_status_pub.cpp
        │   └── limo_status_sub.cpp
        ├── CMakeLists.txt
        └── package.xml

其中:

|-----------------|------------------|
| 功能包 | 作用 |
| limo_msgs | 定义自定义消息接口 |
| limo_learning | 编写 C++ 发布节点和订阅节点 |

为什么要单独创建 limo_msgs

  1. 因为在实际项目中,自定义接口通常会单独放在一个接口包中。
  2. 这样其它功能包也可以复用它。

例如:

复制代码
limo_learning 可以使用 limo_msgs
navigation_pkg 也可以使用 limo_msgs
monitor_pkg 也可以使用 limo_msgs

所以可以把 limo_msgs 理解成:

专门存放通信数据结构的功能包

limo_learning 才是:

真正编写节点逻辑代码的功能包


二、自定义 msg 接口文件

2.1 为什么需要自定义 LimoStatus.msg

前面的 /cmd_vel/scan 都是 Topic 通信,也都使用了 msg。

但是它们使用的是 ROS2 官方标准消息:

复制代码
geometry_msgs/msg/Twist
sensor_msgs/msg/LaserScan

如果只是控制速度,使用 Twist 很合适。

如果只是接收雷达数据,使用 LaserScan 很合适。

但是如果我们想发布 LIMO 小车状态,例如:

复制代码
车辆状态
控制模式
电池电压
错误码
运动模式

这些字段就比较偏向具体业务。

这时候就适合自己定义一个消息:

复制代码
limo_msgs/msg/LimoStatus

2.2 LimoStatus.msg 文件位置

自定义 msg 文件一般放在接口包的 msg 目录下。

文件路径:

复制代码
limo_msgs/msg/LimoStatus.msg

注意文件名建议使用大驼峰命名:

复制代码
LimoStatus.msg

后面编译生成 C++ 头文件时,会变成小写加下划线形式:

复制代码
#include "limo_msgs/msg/limo_status.hpp"

也就是说:

复制代码
LimoStatus.msg
        ↓ 编译生成
limo_status.hpp

2.3 LimoStatus.msg 文件内容

LimoStatus.msg 内容如下:

复制代码
std_msgs/Header header

uint8 vehicle_state
uint8 control_mode
float64 battery_voltage
uint16 error_code
uint8 motion_mode

每一行的基本格式是:

复制代码
字段类型 字段名

例如:

复制代码
float64 battery_voltage

表示定义了一个 float64 类型字段,字段名叫 battery_voltage,可以用来表示电池电压。

再比如:

复制代码
uint16 error_code

表示定义了一个 uint16 类型字段,字段名叫 error_code,可以用来表示错误码。


2.4 字段含义说明

可以把 LimoStatus.msg 中的字段理解成下面这样:

|-------------------|-------------------|-------|
| 字段 | 类型 | 含义 |
| header | std_msgs/Header | 标准消息头 |
| vehicle_state | uint8 | 车辆状态 |
| control_mode | uint8 | 控制模式 |
| battery_voltage | float64 | 电池电压 |
| error_code | uint16 | 错误码 |
| motion_mode | uint8 | 运动模式 |

这些字段只是示例。

实际项目中,可以根据自己的机器人状态继续扩展。

例如还可以增加:

复制代码
float64 current_speed
float64 battery_percentage
bool emergency_stop

2.5 std_msgs/Header header 是什么

LimoStatus.msg 中,第一行是:

复制代码
std_msgs/Header header

这个字段表示标准消息头。

它通常包含两个重要信息:

复制代码
stamp:时间戳
frame_id:坐标系名称

可以简单理解为:

复制代码
stamp 表示这条消息是什么时候产生的
frame_id 表示这条消息属于哪个坐标系

例如后面 C++ 代码中会这样赋值:

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

其中:

复制代码
this->now() 表示当前 ROS2 时间
base_link 表示机器人底盘坐标系

为什么要有 header

因为机器人系统中经常需要知道:

  1. 这条状态消息是什么时候产生的?
  2. 这条传感器数据是什么时候采集的?
  3. 这条里程计数据属于哪个坐标系?

所以 header 在机器人开发中非常常见。


2.6 msg 文件注意事项

.msg 文件时要注意:

(1)字段后面不需要写分号

错误写法:

复制代码
float64 battery_voltage;

正确写法:

复制代码
float64 battery_voltage

(2)如果使用了其它功能包里的类型,需要在配置文件中声明依赖。

例如这里使用了:

复制代码
std_msgs/Header header

所以 limo_msgsCMakeLists.txtpackage.xml 中都要配置 std_msgs 依赖。

(3)修改 .msg 文件后必须重新编译。

因为 .msg 文件不能直接被 C++ 使用,ROS2 需要先根据 .msg 文件生成 C++ 头文件。


三、配置自定义接口包 limo_msgs

3.1 rosidl_generate_interfaces 是什么

在 ROS2 中,我们自己编写的**.msg 文件** ,本质上只是一个接口描述文件

例如:

复制代码
msg/LimoStatus.msg

它只是告诉 ROS2:

这个消息里面有哪些字段、字段类型是什么、字段名称叫什么。

但是这个**.msg 文件本身还不能直接被 C++ 或 Python 程序使用。也就是说,C++ 程序不能直接拿 .msg 文件来 include,Python 程序也不能直接拿 .msg 文件来 import**。

所以 ROS2 需要在编译时,根据我们写好的 .msg 文件,自动生成对应语言可以使用的接口代码。

这时候就需要用到:

复制代码
rosidl_generate_interfaces

它的作用可以理解为:

复制代码
自定义 .msg / .srv / .action 文件
        ↓
rosidl_generate_interfaces 参与编译生成
        ↓
生成 C++ / Python 可以使用的接口代码
        ↓
其它 ROS2 节点才能正常发布、订阅、调用这些自定义接口

需要注意的是,rosidl_generate_interfaces 不是在终端里单独执行的命令,而是写在 CMakeLists.txt 里面的一条 CMake 配置语句

也就是说,我们不是手动运行:

复制代码
rosidl_generate_interfaces

而是在 CMakeLists.txt 中配置:

复制代码
rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/LimoStatus.msg"
  DEPENDENCIES std_msgs
)

然后再通过:

复制代码
colcon build

编译整个工作空间。

编译过程中,ROS2 会自动读取 CMakeLists.txt 中的 rosidl_generate_interfaces 配置,根据 msg/LimoStatus.msg 生成对应的 C++、Python 等接口代码。

简单来说:

rosidl_generate_interfaces****就是 ROS2 自定义接口包的"代码生成开关"。

只有在 CMakeLists.txt 中正确配置它,ROS2 才知道当前功能包有哪些自定义接口文件需要参与编译生成。


3.2 limo_msgs 的 CMakeLists.txt

自定义接口包需要使用:

复制代码
rosidl_generate_interfaces

来生成接口代码。

limo_msgs/CMakeLists.txt 可以这样写:

cpp 复制代码
cmake_minimum_required(VERSION 3.8)
project(limo_msgs)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

find_package(ament_cmake REQUIRED)
find_package(rosidl_default_generators REQUIRED)
find_package(std_msgs REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/LimoStatus.msg"
  DEPENDENCIES std_msgs
)

ament_package()

3.3 CMakeLists.txt 核心解释

查找构建工具:

复制代码
find_package(ament_cmake REQUIRED)

表示当前功能包使用 ament_cmake 构建。

查找接口生成工具:

复制代码
find_package(rosidl_default_generators REQUIRED)

表示当前功能包需要使用 ROS2 接口生成工具。

查找 std_msgs

复制代码
find_package(std_msgs REQUIRED)

因为 LimoStatus.msg 中使用了:

复制代码
std_msgs/Header header

所以这里必须找到 std_msgs

生成接口:

复制代码
rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/LimoStatus.msg"
  DEPENDENCIES std_msgs
)

这句是自定义接口包的核心。

它的作用是根据:

复制代码
msg/LimoStatus.msg

生成 C++、Python 等语言可以使用的接口代码。

其中:

复制代码
DEPENDENCIES std_msgs

表示这个自定义消息依赖 std_msgs

最后:

复制代码
ament_package()

表示声明当前功能包是一个 ROS2 ament 功能包。


3.4 limo_msgs 的 package.xml

limo_msgs/package.xml 可以这样写:

XML 复制代码
<?xml version="1.0"?>
<package format="3">
  <name>limo_msgs</name>
  <version>0.0.0</version>
  <description>Custom messages for LIMO robot</description>
  <maintainer email="agilex@todo.todo">agilex</maintainer>
  <license>Apache-2.0</license>

  <buildtool_depend>ament_cmake</buildtool_depend>

  <build_depend>rosidl_default_generators</build_depend>
  <exec_depend>rosidl_default_runtime</exec_depend>

  <depend>std_msgs</depend>

  <member_of_group>rosidl_interface_packages</member_of_group>

  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

3.5 package.xml 核心解释

构建工具依赖:

复制代码
<buildtool_depend>ament_cmake</buildtool_depend>

表示当前功能包使用 ament_cmake 构建。

接口生成依赖:

复制代码
<build_depend>rosidl_default_generators</build_depend>

表示构建时需要接口生成工具。

运行时接口依赖:

复制代码
<exec_depend>rosidl_default_runtime</exec_depend>

表示运行时需要接口相关运行环境。

标准消息依赖:

复制代码
<depend>std_msgs</depend>

因为 LimoStatus.msg 中使用了:

复制代码
std_msgs/Header

所以这里必须声明 std_msgs

接口包声明:

复制代码
<member_of_group>rosidl_interface_packages</member_of_group>

这一项非常重要,表示当前功能包属于 ROS2 接口包。

如果忘记这一项,自定义接口可能无法正常生成或被其它功能包使用。


四、C++ 发布自定义 msg 状态消息

4.1 自定义 msg 发布节点作用

接下来在 limo_learning 功能包中写一个 C++ 发布节点:

复制代码
limo_status_pub

它的作用是:

  1. 每隔 500ms 创建一条 LimoStatus 消息
  2. 给车辆状态、电池电压、错误码等字段赋值
  3. 发布到 /limo_status 话题

整体流程如下:

复制代码
创建 ROS2 节点
        ↓
创建 Publisher
        ↓
创建定时器
        ↓
定时填充 LimoStatus 消息
        ↓
发布到 /limo_status

4.2 自定义 msg 发布完整代码

文件路径:

复制代码
limo_learning/src/limo_status_pub.cpp

代码如下:

cpp 复制代码
```cpp
// 引入智能指针相关头文件
// std::make_shared 会用到
#include <memory>

// 引入函数绑定相关头文件
// std::bind 会用到
#include <functional>

// 引入 ROS2 C++ 客户端库
// rclcpp 是 ROS2 中 C++ 编程最核心的库
#include "rclcpp/rclcpp.hpp"

// 引入自定义消息头文件
// 这个头文件是由 limo_msgs/msg/LimoStatus.msg 编译后自动生成的
#include "limo_msgs/msg/limo_status.hpp"

// 使用 chrono_literals 后,可以直接写 500ms、1s 这种时间单位
using namespace std::chrono_literals;

// 定义一个发布 LIMO 状态消息的节点类
// 该类继承自 rclcpp::Node,说明它是一个 ROS2 节点
class LimoStatusPub : public rclcpp::Node
{
public:
    // 构造函数
    // Node("limo_status_pub") 表示创建一个名为 limo_status_pub 的节点
    LimoStatusPub() : Node("limo_status_pub")
    {
        // 创建一个发布者,用来发布自定义消息 LimoStatus
        // 发布的话题名称是 /limo_status
        // 队列长度为 10
        status_pub_ = this->create_publisher<limo_msgs::msg::LimoStatus>(
            "/limo_status",
            10
        );

        // 创建一个定时器
        // 每隔 500ms 执行一次 timer_callback 回调函数
        timer_ = this->create_wall_timer(
            500ms,
            std::bind(&LimoStatusPub::timer_callback, this)
        );

        // 打印日志,表示节点已经启动
        RCLCPP_INFO(this->get_logger(), "limo_status_pub node has started.");
    }

private:
    // 定时器回调函数
    // 每隔 500ms 会自动执行一次
    void timer_callback()
    {
        // 创建一个自定义消息对象
        // 消息类型来自 limo_msgs/msg/LimoStatus.msg
        limo_msgs::msg::LimoStatus status_msg;

        // 给消息头 header 赋值
        // stamp 表示当前 ROS2 时间
        status_msg.header.stamp = this->now();

        // frame_id 表示该状态消息对应的坐标系
        // base_link 通常表示机器人底盘坐标系
        status_msg.header.frame_id = "base_link";

        // 给自定义消息中的各个字段赋值

        // vehicle_state 表示车辆状态
        // 这里假设 1 表示正常运行状态
        status_msg.vehicle_state = 1;

        // control_mode 表示控制模式
        // 这里假设 2 表示某种控制模式,例如遥控或自动控制
        status_msg.control_mode = 2;

        // battery_voltage 表示电池电压
        // 这里模拟当前电压为 24.5V
        status_msg.battery_voltage = 24.5;

        // error_code 表示错误码
        // 0 通常表示没有错误
        status_msg.error_code = 0;

        // motion_mode 表示运动模式
        // 这里假设 1 表示当前处于某种运动模式
        status_msg.motion_mode = 1;

        // 发布状态消息到 /limo_status 话题
        status_pub_->publish(status_msg);

        // 打印发布出去的状态信息,方便在终端观察
        RCLCPP_INFO(this->get_logger(),
                    "Publish status: battery = %.2f, error_code = %d",
                    status_msg.battery_voltage,
                    status_msg.error_code);
    }

private:
    // 自定义消息发布者对象
    // 用于发布 limo_msgs::msg::LimoStatus 类型的消息
    rclcpp::Publisher<limo_msgs::msg::LimoStatus>::SharedPtr status_pub_;

    // 定时器对象
    // 用于周期性触发 timer_callback 回调函数
    rclcpp::TimerBase::SharedPtr timer_;
};

int main(int argc, char * argv[])
{
    // 初始化 ROS2 C++ 客户端库
    // argc 和 argv 用于接收终端传入的参数
    rclcpp::init(argc, argv);

    // 创建 LimoStatusPub 节点对象,并让节点进入运行状态
    // spin 会持续运行节点,让定时器、回调函数等正常工作
    rclcpp::spin(std::make_shared<LimoStatusPub>());

    // 关闭 ROS2 客户端库
    rclcpp::shutdown();

    return 0;
}
```

4.3 代码核心名字和对象总结

(1)LimoStatusPub:自定义节点类

复制代码
class LimoStatusPub : public rclcpp::Node

LimoStatusPub 是我们自己定义的一个类。

它继承自:

复制代码
rclcpp::Node

说明它是一个 ROS2 节点。

可以理解为:

复制代码
LimoStatusPub = 一个专门发布 LIMO 状态信息的 ROS2 节点类

后面在 main() 函数中:

复制代码
rclcpp::spin(std::make_shared<LimoStatusPub>());

就是创建这个节点,并让它一直运行。


(2) "limo_status_pub":节点名称

复制代码
LimoStatusPub() : Node("limo_status_pub")

这里的:

复制代码
"limo_status_pub"

表示当前 ROS2 节点的名字。

启动后,可以通过命令查看:

复制代码
ros2 node list

正常情况下会看到:

复制代码
/limo_status_pub

也就是说:

复制代码
limo_status_pub = 节点名字

(3)status_pub_:发布者对象

复制代码
rclcpp::Publisher<limo_msgs::msg::LimoStatus>::SharedPtr status_pub_;

status_pub_ 是一个发布者对象。

它的作用是:

负责把 LimoStatus 类型的消息发布出去

它发布的消息类型是:

复制代码
limo_msgs::msg::LimoStatus

也就是我们自己定义的自定义消息。

在构造函数中,通过下面这句创建发布者:

复制代码
status_pub_ = this->create_publisher<limo_msgs::msg::LimoStatus>(
    "/limo_status",
    10
);

可以理解为:

  1. 创建一个发布者 status_pub_
  2. 它向 /limo_status 话题发布 LimoStatus 类型的消息
  3. 队列长度是 10

(4)"/limo_status":话题名称

复制代码
"/limo_status"

这是发布者发布消息的话题名称。

ROS2 中,节点之间不是直接通信,而是通过话题通信。

这里的通信关系可以理解为:

复制代码
limo_status_pub 节点
        ↓ 发布
/limo_status 话题
        ↓ 订阅
其它订阅者节点

如果有其它节点想要获取 LIMO 小车状态,就可以订阅:

复制代码
/limo_status

也可以用终端查看这个话题:

复制代码
ros2 topic list

或者查看话题数据:

复制代码
ros2 topic echo /limo_status

(5)timer_:定时器对象

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

timer_ 是一个定时器对象。

它的作用是:

周期性触发某个函数

在代码中,它每隔 500ms 调用一次 timer_callback()

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

可以理解为:

复制代码
每隔 500ms,自动执行一次 timer_callback 函数

所以这个节点不是只发布一次消息,而是会持续周期性发布。


(6)timer_callback():定时器回调函数

复制代码
void timer_callback()

timer_callback() 是定时器回调函数。

它的作用是:

  1. 创建消息对象
  2. 给消息字段赋值
  3. 发布消息
  4. 打印日志

也就是说,每次定时器触发,都会执行这里面的代码。

核心流程是:

复制代码
创建 LimoStatus 消息
        ↓
填写 header、车辆状态、电池电压、错误码等字段
        ↓
通过 status_pub_ 发布到 /limo_status
        ↓
打印日志

(7)status_msg:消息对象

复制代码
limo_msgs::msg::LimoStatus status_msg;

status_msg 是一个具体的消息对象。

它的类型是:

复制代码
limo_msgs::msg::LimoStatus

也就是自定义消息 LimoStatus.msg 编译后生成的 C++ 类型。

可以理解为:

复制代码
status_msg = 准备发布出去的一条 LIMO 状态消息

后面对它的字段赋值:

复制代码
status_msg.vehicle_state = 1;
status_msg.control_mode = 2;
status_msg.battery_voltage = 24.5;
status_msg.error_code = 0;
status_msg.motion_mode = 1;

就是在填写这条状态消息的具体内容。


(8)核心总结:

cpp 复制代码
LimoStatusPub        自定义节点类
limo_status_pub      节点名称
status_pub_          发布者对象
/limo_status         发布的话题名称
timer_               定时器对象
timer_callback       定时器回调函数
status_msg           要发布的自定义消息对象

4.4 发布端核心代码解释

(1)引入自定义消息头文件:

复制代码
#include "limo_msgs/msg/limo_status.hpp"

需要注意:

源文件叫:

复制代码
LimoStatus.msg

但是 C++ 中 include 的头文件是:

复制代码
limo_status.hpp

这是 ROS2 接口生成后的命名规则。

一般规律是:

复制代码
大驼峰 .msg 文件名
        ↓
小写 + 下划线 .hpp 头文件

例如:

复制代码
LimoStatus.msg  →  limo_status.hpp
RobotState.msg  →  robot_state.hpp

(2)创建自定义消息发布器:

复制代码
status_pub_ = this->create_publisher<limo_msgs::msg::LimoStatus>(
    "/limo_status",
    10
);

这句代码表示:

复制代码
创建一个 Publisher
向 /limo_status 话题发布数据
消息类型是 limo_msgs::msg::LimoStatus
队列长度是 10

对应关系如下:

|------------------------------|---------|
| 内容 | 含义 |
| limo_msgs::msg::LimoStatus | 自定义消息类型 |
| /limo_status | 发布的话题名称 |
| 10 | 消息队列长度 |

(3)创建定时器:

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

这句表示:

复制代码
每隔 500ms 自动执行一次 timer_callback 函数

也就是说,状态消息不是只发一次,而是定时持续发布。

std::bind 可以简单理解为:

复制代码
把当前对象中的 timer_callback 函数绑定起来,交给 ROS2 定时器自动调用

因为**timer_callback 是类中的成员函数**,所以要写:

复制代码
std::bind(&LimoStatusPub::timer_callback, this)

其中:

  1. &LimoStatusPub::timer_callback 表示类中的成员函数
  2. this 表示当前这个节点对象

(4)回调函数 timer_callback 的作用

回调函数:

复制代码
void timer_callback()

它不会由我们手动调用,而是由 ROS2 定时器自动调用。

每隔 500ms 执行一次。

它主要做四件事:

  1. 创建 LimoStatus 消息对象
  2. 给 header 赋值
  3. 给小车状态字段赋值
  4. 发布消息到 /limo_status

创建消息对象:

复制代码
limo_msgs::msg::LimoStatus status_msg;

给消息头赋值:

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

给状态字段赋值:

复制代码
status_msg.vehicle_state = 1;
status_msg.control_mode = 2;
status_msg.battery_voltage = 24.5;
status_msg.error_code = 0;
status_msg.motion_mode = 1;

真正发布消息:

复制代码
status_pub_->publish(status_msg);

这里一定要注意:

复制代码
RCLCPP_INFO 只是打印日志
publish 才是真正发布消息

五、C++ 订阅自定义 msg 状态消息

5.1 为什么还要写 Subscriber

前面已经完成了自定义 msg 的发布节点:

复制代码
limo_status_pub

它的作用是定时向:

复制代码
/limo_status

话题发布 limo_msgs/msg/LimoStatus 类型的小车状态消息。

但是 Topic 通信本质上是:

复制代码
Publisher 发布消息
Subscriber 订阅消息

如果只有发布者,没有订阅者,就只能说明消息发出去了,但还没有演示如何在 C++ 节点中接收这条自定义消息。

它的作用是:

  1. 订阅 /limo_status 话题
  2. 接收 LimoStatus 自定义消息
  3. 在回调函数中读取电池电压、错误码、车辆状态等字段
  4. 打印接收到的小车状态信息

整体流程如下:

cpp 复制代码
limo_status_pub 发布 /limo_status
        ↓
/limo_status 话题传输 LimoStatus 消息
        ↓
limo_status_sub 订阅 /limo_status
        ↓
回调函数接收并解析状态数据

5.2 自定义 msg 订阅完整代码

文件路径:

复制代码
limo_learning/src/limo_status_sub.cpp

代码如下:

cpp 复制代码
```cpp
// 引入智能指针相关头文件
// std::make_shared 会用到
#include <memory>

// 引入函数绑定相关头文件
// std::bind 和 std::placeholders::_1 会用到
#include <functional>

// 引入 ROS2 C++ 客户端库
// rclcpp 是 ROS2 中 C++ 编程最核心的库
#include "rclcpp/rclcpp.hpp"

// 引入自定义消息头文件
// 这个头文件由 limo_msgs/msg/LimoStatus.msg 编译后自动生成
#include "limo_msgs/msg/limo_status.hpp"

// 定义一个订阅 LIMO 状态消息的节点类
// 该类继承自 rclcpp::Node,说明它是一个 ROS2 节点
class LimoStatusSub : public rclcpp::Node
{
public:
    // 构造函数
    // Node("limo_status_sub") 表示创建一个名为 limo_status_sub 的节点
    LimoStatusSub() : Node("limo_status_sub")
    {
        // 创建一个订阅者,用来订阅自定义消息 LimoStatus
        // 订阅的话题名称是 /limo_status
        // 队列长度为 10
        // 当接收到消息后,会自动调用 status_callback 回调函数
        status_sub_ = this->create_subscription<limo_msgs::msg::LimoStatus>(
            "/limo_status",
            10,
            std::bind(&LimoStatusSub::status_callback,
                      this,
                      std::placeholders::_1)
        );

        // 打印日志,表示订阅者节点已经启动
        RCLCPP_INFO(this->get_logger(), "limo_status_sub node has started.");
    }

private:
    // 订阅回调函数
    // 当 /limo_status 话题上有新消息时,该函数会被自动调用
    // msg 表示接收到的 LimoStatus 消息
    void status_callback(const limo_msgs::msg::LimoStatus::SharedPtr msg)
    {
        // 打印接收到的 LIMO 状态信息
        // msg-> 表示通过指针访问消息对象中的字段
        RCLCPP_INFO(this->get_logger(),
                    "Receive status: frame_id = %s, vehicle_state = %d, control_mode = %d, battery = %.2f, error_code = %d, motion_mode = %d",
                    msg->header.frame_id.c_str(),
                    msg->vehicle_state,
                    msg->control_mode,
                    msg->battery_voltage,
                    msg->error_code,
                    msg->motion_mode);
    }

private:
    // 自定义消息订阅者对象
    // 用于订阅 limo_msgs::msg::LimoStatus 类型的消息
    rclcpp::Subscription<limo_msgs::msg::LimoStatus>::SharedPtr status_sub_;
};

int main(int argc, char * argv[])
{
    // 初始化 ROS2 C++ 客户端库
    // argc 和 argv 用于接收终端传入的参数
    rclcpp::init(argc, argv);

    // 创建 LimoStatusSub 节点对象,并让节点进入运行状态
    // spin 会持续运行节点,让订阅者可以一直等待和处理消息
    rclcpp::spin(std::make_shared<LimoStatusSub>());

    // 关闭 ROS2 客户端库
    rclcpp::shutdown();

    return 0;
}
```

5.3 订阅器核心代码解释

(1)创建订阅器的代码如下:

cpp 复制代码
status_sub_ = this->create_subscription<limo_msgs::msg::LimoStatus>(
    "/limo_status",
    10,
    std::bind(&LimoStatusSub::status_callback,
              this,
              std::placeholders::_1)
);

这句代码表示创建一个 Subscriber,用来订阅 /limo_status 话题。

其中:

|------------------------------|--------------|
| 内容 | 含义 |
| limo_msgs::msg::LimoStatus | 订阅的消息类型 |
| /limo_status | 订阅的话题名称 |
| 10 | 消息队列长度 |
| status_callback | 收到消息后执行的回调函数 |

也就是说,只要 /limo_status 话题上有新消息,ROS2 就会自动调用:

复制代码
status_callback()

(2) 回调函数 status_callback 的作用

订阅回调函数如下:

cpp 复制代码
void status_callback(const limo_msgs::msg::LimoStatus::SharedPtr msg)

这个回调函数接收的是一个 LimoStatus 类型的消息指针。

这里的 msg 只是形参名字,也就是"接收到的消息对象叫什么名字"。

可以任意修改:

cpp 复制代码
void status_callback(const limo_msgs::msg::LimoStatus::SharedPtr status_msg)
{
    RCLCPP_INFO(this->get_logger(),
                "battery = %.2f",
                status_msg->battery_voltage);
}

但是注意:你前面名字改了,函数里面也要跟着改。

  1. 接收到一条 LimoStatus 消息,
  2. 把这条消息临时命名为 msg,
  3. 然后在函数里面通过 msg->字段名 读取消息内容。

复制代码
msg

就是订阅器收到的自定义消息数据。

因为 LimoStatus.msg 中定义了这些字段:

复制代码
std_msgs/Header header
uint8 vehicle_state
uint8 control_mode
float64 battery_voltage
uint16 error_code
uint8 motion_mode

所以在 C++ 回调函数中,就可以通过:

复制代码
msg->header.frame_id
msg->vehicle_state
msg->control_mode
msg->battery_voltage
msg->error_code
msg->motion_mode

读取对应字段。

例如:

复制代码
msg->battery_voltage

表示读取电池电压。

复制代码
msg->error_code

表示读取错误码。

复制代码
msg->header.frame_id

表示读取消息对应的坐标系名称。

由于**frame_id 是字符串,打印时需要写成:**

复制代码
msg->header.frame_id.c_str()

5.4 Publisher 和 Subscriber 的关系

到这里,自定义 msg 的发布和订阅就都完整了。

发布端:

复制代码
limo_status_pub

负责创建 LimoStatus 消息,并发布到:

复制代码
/limo_status

订阅端:

复制代码
limo_status_sub

负责订阅:

复制代码
/limo_status

并在回调函数中读取消息内容。

完整通信流程如下:

bash 复制代码
LimoStatus.msg 定义自定义消息结构
        ↓
limo_status_pub 创建 LimoStatus 消息对象
        ↓
填充 vehicle_state、control_mode、battery_voltage 等状态数据
        ↓
通过 publish() 发布到 /limo_status 话题
        ↓
/limo_status 话题负责传输 LimoStatus 消息
        ↓
limo_status_sub 订阅 /limo_status 话题
        ↓
status_callback() 回调函数接收消息
        ↓
解析并打印 LIMO 小车状态数据

六、配置代码包 limo_learning

6.1 limo_learning 的 package.xml

因为 limo_learning 中使用了:

XML 复制代码
rclcpp       // ROS2 官方提供的 C++ 客户端库
limo_msgs    // 自己创建的 自定义接口包

所以 limo_learning/package.xml 中需要添加:

复制代码
<depend>rclcpp</depend>
<depend>limo_msgs</depend>

完整示例:

XML 复制代码
<?xml version="1.0"?>
<package format="3">
  <name>limo_learning</name>
  <version>0.0.0</version>
  <description>ROS2 C++ practice package for LIMO robot</description>
  <maintainer email="agilex@todo.todo">agilex</maintainer>
  <license>Apache-2.0</license>

  <buildtool_depend>ament_cmake</buildtool_depend>

  <depend>rclcpp</depend>
  <depend>limo_msgs</depend>

  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

其中:

|-------------|-----------------------|
| 依赖 | 作用 |
| rclcpp | ROS2 C++ 节点基础库 |
| limo_msgs | 使用自定义 LimoStatus 消息 |


6.2 limo_learning 的 CMakeLists.txt

limo_learning/CMakeLists.txt 可以这样配置:

cpp 复制代码
cmake_minimum_required(VERSION 3.8)
project(limo_learning)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(limo_msgs REQUIRED)

add_executable(limo_status_pub src/limo_status_pub.cpp)
ament_target_dependencies(limo_status_pub
  rclcpp
  limo_msgs
)

add_executable(limo_status_sub src/limo_status_sub.cpp)
ament_target_dependencies(limo_status_sub
  rclcpp
  limo_msgs
)

install(TARGETS
  limo_status_pub
  limo_status_sub
  DESTINATION lib/${PROJECT_NAME}
)

ament_package()

6.3 CMakeLists.txt 核心理解

这里最重要的是四类语句:

复制代码
find_package
add_executable
ament_target_dependencies
install

可以这样理解:

  1. find_package:找到依赖包
  2. add_executable:生成可执行程序
  3. ament_target_dependencies:把依赖包给具体程序用
  4. install:把程序放到 ROS2 能找到的位置

例如:

复制代码
find_package(limo_msgs REQUIRED)

表示找到 limo_msgs 这个接口包。

复制代码
add_executable(limo_status_pub src/limo_status_pub.cpp)

表示把:

复制代码
src/limo_status_pub.cpp

编译成:

复制代码
limo_status_pub

这个可执行程序。

复制代码
ament_target_dependencies(limo_status_pub
  rclcpp
  limo_msgs
)

表示 limo_status_pub 这个程序需要使用:

复制代码
rclcpp
limo_msgs

新增订阅节点后,也需要写:

复制代码
add_executable(limo_status_sub src/limo_status_sub.cpp)
ament_target_dependencies(limo_status_sub
  rclcpp
  limo_msgs
)

最后一定要把两个节点都放进 install

复制代码
install(TARGETS
  limo_status_pub
  limo_status_sub
  DESTINATION lib/${PROJECT_NAME}
)

如果没有写 install,可能会出现:

复制代码
colcon build 编译成功
但是 ros2 run 找不到节点

七、编译与运行测试

7.1 先编译自定义接口包

回到工作空间根目录:

复制代码
cd ~/agilex_open_class_ws

先编译自定义接口包:

复制代码
colcon build --packages-select limo_msgs
source install/setup.bash

为什么要先编译 limo_msgs

因为 limo_learning 中的 C++ 代码会引用:

复制代码
#include "limo_msgs/msg/limo_status.hpp"

这个头文件不是手写出来的,而是 ROS2 根据:

复制代码
LimoStatus.msg

自动生成的。

所以必须先编译接口包。


7.2 再编译代码包

再编译代码包:

复制代码
colcon build --packages-select limo_learning
source install/setup.bash

也可以直接完整编译整个工作空间:

复制代码
colcon build
source install/setup.bash

但是在学习阶段,建议先分开编译:

复制代码
先编译 limo_msgs
再编译 limo_learning

这样更容易理解接口包和代码包之间的依赖关系。


7.3 查看自定义 msg 是否生成成功

查看自定义消息结构:

复制代码
ros2 interface show limo_msgs/msg/LimoStatus

如果能正常显示:

复制代码
std_msgs/Header header
uint8 vehicle_state
uint8 control_mode
float64 battery_voltage
uint16 error_code
uint8 motion_mode

说明自定义 msg 已经生成成功。


7.4 运行自定义 msg 发布节点

打开第一个终端:

复制代码
cd ~/agilex_open_class_ws
source install/setup.bash
ros2 run limo_learning limo_status_pub

如果节点正常启动,会看到类似日志:

复制代码
limo_status_pub node has started.
Publish status: battery = 24.50, error_code = 0

这个节点会不断向 /limo_status 发布小车状态消息。


7.5 运行自定义 msg 订阅节点

打开第二个终端:

复制代码
cd ~/agilex_open_class_ws
source install/setup.bash
ros2 run limo_learning limo_status_sub

如果运行正常,订阅节点会打印类似信息:

复制代码
Receive status: frame_id = base_link, vehicle_state = 1, control_mode = 2, battery = 24.50, error_code = 0, motion_mode = 1

这说明:

复制代码
limo_status_pub 已经成功发布自定义消息
limo_status_sub 已经成功接收到自定义消息

7.6 使用命令查看话题

也可以使用命令查看 /limo_status

复制代码
ros2 topic echo /limo_status

查看话题信息:

复制代码
ros2 topic info /limo_status

查看自定义消息结构:

复制代码
ros2 interface show limo_msgs/msg/LimoStatus

这样就能从三个角度验证自定义 msg 是否正常:

复制代码
ros2 interface show:看接口结构是否生成
ros2 topic info:看话题发布订阅关系
ros2 topic echo:看话题实际数据

八、常见问题总结

8.1 找不到 limo_msgs/msg/limo_status.hpp

如果编译时报错:

复制代码
limo_msgs/msg/limo_status.hpp: No such file or directory

常见原因包括:

复制代码
limo_msgs 没有编译
没有 source install/setup.bash
limo_learning 没有依赖 limo_msgs
CMakeLists.txt 中没有 find_package(limo_msgs REQUIRED)
ament_target_dependencies 中没有添加 limo_msgs

解决方法:

复制代码
colcon build --packages-select limo_msgs
source install/setup.bash

colcon build --packages-select limo_learning
source install/setup.bash

8.2 修改 LimoStatus.msg 后代码不生效

如果修改了:

复制代码
LimoStatus.msg

一定要重新编译接口包:

复制代码
colcon build --packages-select limo_msgs
source install/setup.bash

如果 limo_learning 依赖了 limo_msgs,建议再编译代码包:

复制代码
colcon build --packages-select limo_learning
source install/setup.bash

原因是:

复制代码
.msg 文件不是直接被 C++ 使用的
ROS2 需要先根据 .msg 文件生成 C++ 头文件
然后 C++ 代码才能 include 和使用

8.3 ros2 interface show 找不到接口

如果执行:

复制代码
ros2 interface show limo_msgs/msg/LimoStatus

找不到接口,重点检查:

复制代码
limo_msgs 是否编译成功
当前终端是否 source install/setup.bash
LimoStatus.msg 路径是否正确
CMakeLists.txt 是否写了 rosidl_generate_interfaces
package.xml 是否写了 member_of_group

尤其是 package.xml 中不要漏掉:

复制代码
<member_of_group>rosidl_interface_packages</member_of_group>

8.4 ros2 run 找不到节点

如果运行:

复制代码
ros2 run limo_learning limo_status_pub

或者:

复制代码
ros2 run limo_learning limo_status_sub

提示找不到节点,重点检查:

复制代码
limo_learning 是否编译成功
是否 source install/setup.bash
CMakeLists.txt 中是否写了 add_executable
CMakeLists.txt 中是否写了 install
ros2 run 后面的可执行文件名是否写对

对应的 CMakeLists.txt 中必须有:

复制代码
add_executable(limo_status_pub src/limo_status_pub.cpp)
add_executable(limo_status_sub src/limo_status_sub.cpp)

并且 install 中必须包含:

复制代码
install(TARGETS
  limo_status_pub
  limo_status_sub
  DESTINATION lib/${PROJECT_NAME}
)

8.5 订阅节点没有任何输出

如果运行:

复制代码
ros2 run limo_learning limo_status_sub

后没有任何输出,可能原因包括:

复制代码
发布节点 limo_status_pub 没有启动
发布和订阅的话题名不一致
发布和订阅的消息类型不一致
当前终端没有 source install/setup.bash

重点检查发布端和订阅端的话题名是否完全一致。

发布端是:

复制代码
"/limo_status"

订阅端也必须是:

复制代码
"/limo_status"

8.6 回调函数没有执行

如果订阅节点启动了,但回调函数没有执行,常见原因是没有发布者,或者发布者话题名不一致。

可以执行:

复制代码
ros2 topic info /limo_status

正常情况下,应该能看到类似信息:

复制代码
Publisher count: 1
Subscription count: 1

如果 Publisher count 是 0,说明没有发布者。

如果 Subscription count 是 0,说明没有订阅者。


九、本章总结

本篇是在上一篇 Topic 官方 msg 实战基础上的进一步扩展。

上一篇主要讲:

复制代码
Topic + 官方 msg
geometry_msgs/msg/Twist
sensor_msgs/msg/LaserScan

本篇主要讲:

复制代码
Topic + 自定义 msg
limo_msgs/msg/LimoStatus

本章实现了两个核心节点:

复制代码
limo_status_pub:发布自定义小车状态消息
limo_status_sub:订阅自定义小车状态消息

本章最重要的理解是:

复制代码
Topic 使用 msg
msg 可以是官方标准 msg,也可以是自定义 msg

自定义 msg 开发中最容易忘记的是:

  1. 接口包要单独配置
  2. 接口包要先编译
  3. 接口修改后要重新编译
  4. 代码包要依赖接口包
  5. 编译后要 source install/setup.bash
  6. 发布节点和订阅节点都要写进 install

到这里,ROS2 C++ 中自定义 msg 的完整使用流程就跑通了。

下一篇将继续讲解:

ROS2 C++ 小车控制完整实战(三):自定义 srv 服务通信

也就是通过 Service Client 和 Service Server,实现"一次请求、一次响应"的小车控制流程。

相关推荐
-森屿安年-1 小时前
91. 解码方法
c++·动态规划
有点。1 小时前
C++(二分答案)
c++
程序喵大人2 小时前
【C++并发系列】第一章:多线程读写同一个变量为什么会出错
开发语言·c++·多线程·并发
梓䈑2 小时前
C++ 接入 SQLite 数据库:环境搭建、API 详解 与 两种执行方式对比
数据库·c++·sqlite
zh路西法3 小时前
基于yaml-cpp的C++参数服务器设计2:多级参数配置
linux·服务器·c++
啦啦啦啦啦zzzz3 小时前
算法总结(双指针)
c++·算法·双指针
QiLinkOS3 小时前
极客与商业思维的融合实践(1)
c语言·数据库·c++·人工智能·算法·开源协议
坚果派·白晓明4 小时前
鸿蒙PC】libuv适配:AtomCode Skills一站式指南
c语言·c++·华为·ai编程·harmonyos·atomcode
c++之路4 小时前
CMake 系列教程(五):进阶技巧
c语言·开发语言·c++