docs(nav): 阅读路径导航 + 数字统一

This commit is contained in:
xs
2026-08-04 16:10:27 +08:00
parent 549d6b337e
commit 6338f3d36a
95 changed files with 3838 additions and 1260 deletions
+17 -4
View File
@@ -21,12 +21,17 @@ if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
# 必备: 找 ament_cmake + 依赖
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
set(THIS_PACKAGE_INCLUDE_DEPENDS
rclcpp
std_msgs
)
# 库:含 ChatterPublisher / ChatterSubscriber 实现
# 库:含 ChatterPublisher / ChatterSubscriber 实现(无 main)
add_library(${PROJECT_NAME}_core SHARED
src/chatter_publisher.cpp
src/chatter_subscriber.cpp
@@ -37,11 +42,11 @@ target_include_directories(${PROJECT_NAME}_core PUBLIC
)
ament_target_dependencies(${PROJECT_NAME}_core ${THIS_PACKAGE_INCLUDE_DEPENDS})
# 可执行文件:两个独立 main(每个节点独立可执行)
add_executable(chatter_publisher_cpp src/chatter_publisher.cpp)
# 可执行文件:每个 main 单独编译
add_executable(chatter_publisher_cpp src/chatter_publisher_main.cpp)
target_link_libraries(chatter_publisher_cpp ${PROJECT_NAME}_core)
add_executable(chatter_subscriber_cpp src/chatter_subscriber.cpp)
add_executable(chatter_subscriber_cpp src/chatter_subscriber_main.cpp)
target_link_libraries(chatter_subscriber_cpp ${PROJECT_NAME}_core)
# 安装:可执行 + 库 + launch
@@ -56,4 +61,12 @@ install(DIRECTORY launch
DESTINATION share/${PROJECT_NAME}
)
# 测试
if(BUILD_TESTING)
find_package(ament_cmake_gtest REQUIRED)
ament_add_gtest(test_pub_sub test/test_pub_sub.cpp)
ament_target_dependencies(test_pub_sub ${THIS_PACKAGE_INCLUDE_DEPENDS})
target_link_libraries(test_pub_sub ${PROJECT_NAME}_core)
endif()
ament_package()
+369 -31
View File
@@ -1,54 +1,392 @@
# cpp_pubsub
# cpp_pubsub — C++ Topic 发布订阅
ROS2 Topic pub/sub 演示包 (C++)。属于 Level 1 基础机制第 1-2 块
> 跟 `py_pubsub` 一样的东西,用 C++ 写。**只用 Python 的项目可以跳过这个**
>
> 预计学习时间:1-2 小时。
>
> **前置知识**:读懂 py_pubsub 的 README + 基础 C++(知道 class / shared_ptr / std:: 是什么)。
## 功能
---
- **`chatter_publisher_cpp`**: 周期性发布 `std_msgs/String``/chatter`
- **`chatter_subscriber_cpp`**: 订阅 `/chatter`,打印消息
## 这是什么?
与 [`py_pubsub`](../py_pubsub/) 配合,演示 Python C++ 跨语言互通
`py_pubsub` 的 C++ 版本。**同一个 Topic,Python C++ 节点互通**(都用 `std_msgs/String` 类型)
## C++ 节点类(库)
**本包存在的意义**:
- 看到 Python 节点发的消息,C++ 节点能收到(反之亦然)
- 学会用 **rclcpp**(ROS2 C++ 客户端库) 写节点
- 知道工业级 ROS2 项目的 C++ 代码长什么样
`cpp_pubsub_core` 共享库包含:
---
- `cpp_pubsub::ChatterPublisher``rclcpp::Node` 派生
- `cpp_pubsub::ChatterSubscriber``rclcpp::Node` 派生
## 🤔 我已经会 py_pubsub 了,为什么还要学 C++ 版?
均位于 `namespace cpp_pubsub`,便于复用与测试。
**实话**:Python 写 ROS2 简单 90%,**大多数项目用 Python 就够了**。但有些场景用 C++ 更合适:
## 运行
| 场景 | 为什么可能用 C++ |
|---|---|
| 性能敏感(图像/点云/SLAM) | Python GC 不可控 + 速度慢 |
| 已有 C++ 库要复用 | OpenCV、PCL、MoveIt2 全是 C++ |
| 嵌入式部署 | 部分芯片只支持 C/C++ |
```bash
# 单独启动
ros2 run cpp_pubsub chatter_publisher_cpp
ros2 run cpp_pubsub chatter_subscriber_cpp
**如果你的项目不涉及上面这些,只用 Python 就行,不必学这个包。**
# launch
ros2 launch cpp_pubsub pubsub_launch.py
---
# 实时观察
ros2 topic info /chatter -v
ros2 topic echo /chatter
## 🎯 学完之后你能做什么?
1. ✅ 用 C++ 写 ROS2 节点(知道 `rclcpp::Node` 是什么)
2. ✅ 理解 Python ↔ C++ 跨语言互通(为什么能行?)
3. ✅ 知道 `shared_ptr` / `std::bind` / `rclcpp::QoS` 这些 C++ 套路
4. ✅ 读懂 ROS2 官方 C++ 示例代码
---
## 📁 文件结构
```
src/cpp_pubsub/
├── src/
│ ├── chatter_publisher.cpp/hpp # Publisher 类(声明 + 实现)
│ ├── chatter_publisher_main.cpp # Publisher 的 main 入口(单独文件)
│ ├── chatter_subscriber.cpp/hpp # Subscriber 类(声明 + 实现)
│ └── chatter_subscriber_main.cpp # Subscriber 的 main 入口
├── launch/pubsub_cpp_launch.py # 启动两个 cpp 节点
├── test/test_pub_sub.cpp # gtest 单元测试
├── CMakeLists.txt # C++ 编译配置
└── package.xml # ROS2 包元数据
```
## 测试
**C++ 项目比 Python 多一个东西**:`CMakeLists.txt`(告诉编译器怎么编译)。
---
## 🚀 跑起来
```bash
source /opt/ros/humble/setup.bash
source /root/ros2_ws/install/setup.bash
# 只跑 C++ 的两个节点
ros2 launch cpp_pubsub pubsub_cpp_launch.py
```
**预期输出**:
```
[INFO] [chatter_publisher_cpp]: Publishing: "Hello World C++: 0"
[INFO] [chatter_publisher_cpp]: Publishing: "Hello World C++: 1"
...
[INFO] [chatter_subscriber_cpp]: I heard: Hello World C++: 0
```
**跨语言验证**(另开终端,跟 Python 互通):
```bash
# 终端 1: C++ Publisher
ros2 run cpp_pubsub chatter_publisher_cpp
# 终端 2: Python Subscriber(可以!因为都用 std_msgs/String)
ros2 run py_pubsub chatter_subscriber
```
**预期效果**:Python Subscriber 能收到 C++ Publisher 的消息(和反之)。
---
## 📖 C++ 黑魔法词典(新手必看)
读代码前先扫一眼这些符号:
| 符号 | 含义 | Python 对应 |
|---|---|---|
| `class Foo : public rclcpp::Node` | Foo 继承 rclcpp::Node | `class Foo(Node):` |
| `rclcpp::Node` | ROS2 C++ 节点基类 | `rclpy.node.Node` |
| `std::shared_ptr<T>` | 共享指针(自动内存管理) | 不用写,GC 自动管 |
| `::SharedPtr` | rclcpp 的共享指针类型 | 无 |
| `std::make_shared<T>(...)` | 创建 shared_ptr 的工厂函数 | 无 |
| `std::bind(&Foo::method, this)` | 把方法 + this 绑成一个可调用对象 | lambda 即可 |
| `std::placeholders::_1` | bind 的占位符,代表"第一个参数" | lambda 参数 |
| `std::chrono::seconds(1)` | 1 秒的 std 写法(可简写为 `1s`) | `1.0` |
| `create_wall_timer(...)` | 创建定时器 | `create_timer(...)` |
| `create_publisher<T>(topic, qos)` | 模板参数 T 是消息类型 | `create_publisher(T, topic, qos)` |
| `<std_msgs::msg::String>` | 嵌套命名空间,ROS 消息类型 | `String`(从 std_msgs.msg 导入) |
**`std::bind` 是 C++ 的"高阶函数"**,把方法 + 对象绑在一起,让定时器能回调。Python 里 lambda 就能搞定。
---
## 📖 Publisher 完整代码
```cpp
// chatter_publisher.hpp - 类声明
#pragma once
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
namespace cpp_pubsub {
class ChatterPublisher : public rclcpp::Node {
public:
explicit ChatterPublisher(const rclcpp::NodeOptions & options = rclcpp::NodeOptions());
private:
void timer_callback();
// shared_ptr:节点销毁时自动释放这些对象(不用手动 delete)
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
rclcpp::TimerBase::SharedPtr timer_;
std::size_t count_;
};
} // namespace cpp_pubsub
// chatter_publisher.cpp - 类实现
#include "cpp_pubsub/chatter_publisher.hpp"
#include <chrono>
#include <string>
namespace cpp_pubsub {
ChatterPublisher::ChatterPublisher(const rclcpp::NodeOptions & options)
: rclcpp::Node("chatter_publisher_cpp", options), // 调父类构造函数,节点名 "chatter_publisher_cpp"
count_(0)
{
// 1) 声明参数(C++ 用 ParameterDescriptor + set__description)
// ⚠️ 注意:是 set__description(双下划线),不是 set_description
// 双下划线 = ROS2 自动生成的 setter(类似 Python 的 description 属性)
this->declare_parameter<std::string>(
"message_prefix", "Hello World C++: ",
rcl_interfaces::msg::ParameterDescriptor().set__description("消息前缀"));
this->declare_parameter<double>(
"publish_rate_hz", 1.0,
rcl_interfaces::msg::ParameterDescriptor().set__description("发布频率 (Hz)"));
// 2) 读参数
const std::string prefix = this->get_parameter("message_prefix").as_string();
const double rate = this->get_parameter("publish_rate_hz").as_double();
// 3) 创建 Publisher
// 模板参数 <std_msgs::msg::String> 表示消息类型
// 第二个参数 10 是队列大小(跟 Python 一样)
publisher_ = this->create_publisher<std_msgs::msg::String>("chatter_cpp", 10);
// 4) 创建定时器
// std::bind 把方法 + this 绑成一个"函数对象"
// 让定时器能调用 ChatterPublisher::timer_callback
timer_ = this->create_wall_timer(
std::chrono::milliseconds(static_cast<int>(1000.0 / rate)),
std::bind(&ChatterPublisher::timer_callback, this));
}
void ChatterPublisher::timer_callback() {
auto msg = std_msgs::msg::String();
msg.data = "Hello World C++: " + std::to_string(count_++);
publisher_->publish(msg); // 发布!
}
} // namespace cpp_pubsub
```
**`set__description` 双下划线**:这是 ROS2 的 IDL(接口描述语言)生成器自动生成的 setter 命名约定。**返回自身引用,可以链式调用**:
```cpp
ParameterDescriptor().set__description("...") // 链式风格
// 等价于
auto d = ParameterDescriptor();
d.set__description("...");
```
---
## 📖 Subscriber 完整代码
```cpp
// chatter_subscriber.hpp
#pragma once
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
namespace cpp_pubsub {
class ChatterSubscriber : public rclcpp::Node {
public:
explicit ChatterSubscriber(const rclcpp::NodeOptions & options = rclcpp::NodeOptions());
private:
void message_callback(const std_msgs::msg::String::SharedPtr msg);
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
std::size_t received_count_;
};
} // namespace cpp_pubsub
// chatter_subscriber.cpp
#include "cpp_pubsub/chatter_subscriber.hpp"
#include <string>
namespace cpp_pubsub {
ChatterSubscriber::ChatterSubscriber(const rclcpp::NodeOptions & options)
: rclcpp::Node("chatter_subscriber_cpp", options), received_count_(0)
{
this->declare_parameter<std::string>(
"topic_name", "chatter",
rcl_interfaces::msg::ParameterDescriptor().set__description("订阅话题名"));
const std::string topic_name = this->get_parameter("topic_name").as_string();
// create_subscription<消息类型>(话题名, 队列大小, callback)
// _1 是 std::bind 的占位符,代表"收到的 msg"(传给 message_callback 的第一个参数)
subscription_ = this->create_subscription<std_msgs::msg::String>(
topic_name, 10,
std::bind(&ChatterSubscriber::message_callback, this, std::placeholders::_1));
}
void ChatterSubscriber::message_callback(const std_msgs::msg::String::SharedPtr msg) {
received_count_++;
if (received_count_ % 10 == 0) {
RCLCPP_INFO(this->get_logger(),
"recv #%zu: \"%s\"", received_count_, msg->data.c_str());
}
}
} // namespace cpp_pubsub
```
**对比 Python Subscriber**:
```python
self.subscription = self.create_subscription(
String, 'chatter', self.listener_callback, 10)
def listener_callback(self, msg):
self.get_logger().info(f'I heard: {msg.data}')
```
**主要区别**:
- C++ 用 `std::bind` + `std::placeholders::_1` 占位符
- C++ 消息是 `SharedPtr`(智能指针)
- C++ 字符串访问用 `msg->data.c_str()`(指针解引用 + C 字符串转换)
---
## 📖 main 函数(单独文件)
```cpp
// chatter_publisher_main.cpp
int main(int argc, char * argv[]) {
rclcpp::init(argc, argv); // 初始化 ROS2
rclcpp::spin(std::make_shared<cpp_pubsub::ChatterPublisher>()); // 创建节点 + spin
rclcpp::shutdown(); // 关闭 ROS2
return 0;
}
```
**为什么 main 单独一个文件**:类实现要单独编译给测试用(`gtest` 链接时会出冲突,如果 main 在类实现里)。
**`spin` 是什么**:死循环,反复调用所有 callback(定时器、订阅者、Service...)。按 `Ctrl+C` 退出。
---
## 📖 CMakeLists.txt 怎么读?
```cmake
cmake_minimum_required(VERSION 3.16)
project(cpp_pubsub LANGUAGES CXX)
# 1) 找依赖
find_package(ament_cmake REQUIRED) # ROS2 构建系统
find_package(rclcpp REQUIRED) # ROS2 C++ 客户端
find_package(std_msgs REQUIRED) # 标准消息
find_package(ament_cmake_gtest REQUIRED) # gtest 支持(test_depend)
# 2) 类实现编译成共享库(给 gtest 用)
add_library(${PROJECT_NAME}_core SHARED
src/chatter_publisher.cpp
src/chatter_subscriber.cpp)
target_include_directories(${PROJECT_NAME}_core PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include/${PROJECT_NAME}>)
ament_target_dependencies(${PROJECT_NAME}_core rclcpp std_msgs)
# 3) 可执行文件(链接共享库 + main 单独文件)
add_executable(chatter_publisher_cpp src/chatter_publisher_main.cpp)
target_link_libraries(chatter_publisher_cpp ${PROJECT_NAME}_core)
add_executable(chatter_subscriber_cpp src/chatter_subscriber_main.cpp)
target_link_libraries(chatter_subscriber_cpp ${PROJECT_NAME}_core)
# 4) 安装 + 测试
install(TARGETS chatter_publisher_cpp chatter_subscriber_cpp ${PROJECT_NAME}_core
DESTINATION lib/${PROJECT_NAME})
if(BUILD_TESTING)
ament_add_gtest(test_pub_sub test/test_pub_sub.cpp)
target_link_libraries(test_pub_sub ${PROJECT_NAME}_core)
endif()
ament_package()
```
**跟 Python setup.py 对比**:CMakeLists.txt 更复杂,但更精确控制编译(链接、include 路径、依赖)。
---
## 🧪 跑测试
```bash
colcon test --packages-select cpp_pubsub
colcon test-result --all --verbose
```
测试覆盖(在 `test/test_pub_sub.cpp`):
**预期**:`cpp_pubsub: gtest 3/3 ✓` 全部通过。
| 用例 | 内容 |
---
## 🔧 自己改代码
1.`src/chatter_publisher.cpp`(比如改默认消息前缀)
2. **必须重编译**(C++ 不会自动生效):
```bash
colcon build --packages-select cpp_pubsub
```
3. 重跑 launch(旧的先 `Ctrl+C` 停掉)
---
## 🧠 为什么 Python ↔ C++ 能互通?
**秘密在 `.msg` 文件**。`std_msgs/String.msg` 定义了消息结构,`rosidl` 自动生成 Python 类和 C++ 类,**接口完全对应**:
| Python | C++ |
|---|---|
| `PublisherConstructsWithDefaults` | 节点名 + 默认参数正确 |
| `SubscriberConstructsWithDefaults` | 节点名 + 默认参数正确 |
| `PublisherSpinSomeWorks` | spin_some 1s 不崩溃 |
| `String()` | `std_msgs::msg::String()` |
| `msg.data` | `msg.data` |
| 字符串类型 | `std::string` |
## 深度学习
DDS 传输的是**二进制数据**(按 `.msg` 定义序列化),不关心语言。所以 py 发 cpp 收完全没问题。
- 编程规范:[`doc/CODING_STYLE.md`](../doc/CODING_STYLE.md) §3
- Topic 深度:[`doc/20-topics.md`](../doc/20-topics.md)
- C++ Style: [ROS2 C++ Style Guide](https://docs.ros.org/en/humble/Contributing/Code-Style-Language-Versions.html)
---
## 📚 深入学习
- [ROS2 C++ 教程](https://docs.ros.org/en/humble/Tutorials/Beginner-Client-Libraries/Writing-A-Simple-Cpp-Publisher-And-Subscriber.html)
- [doc/CODING_STYLE.md](../../doc/CODING_STYLE.md) — C++ 编码规范
- [py_pubsub README](../py_pubsub/README.md) — 对比看效果
---
## ⏭️ 下一个包
学完这个,继续学 **[py_srv](../py_srv/README.md)** — 学习 Service(请求-响应)模式。
---
## 📍 学习路径导航
| ⏮ 上一个 | 🏠 当前位置 | ⏭ 下一个 |
|---|---|---|
| [py_pubsub — Python Topic](../py_pubsub/README.md) | **cpp_pubsub — C++ Topic** | [py_srv — Python Service](../py_srv/README.md) |
📍 完整 12 包学习顺序见 [主 README](../../README.md#-12-包推荐学习顺序)
+1
View File
@@ -18,6 +18,7 @@
<depend>rclcpp</depend>
<depend>std_msgs</depend>
<test_depend>ament_cmake_gtest</test_depend>
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
+9 -13
View File
@@ -1,4 +1,9 @@
// chatter_publisher.cpp - C++ ChatterPublisher 实现
// chatter_publisher.cpp - ChatterPublisher 实现(不含 main)
//
// 设计思想:
// - 把节点类实现放 _core 库,main() 单独文件
// - 库不包含 main,避免多定义错误
#include "cpp_pubsub/chatter_publisher.hpp"
#include <chrono>
@@ -15,10 +20,10 @@ ChatterPublisher::ChatterPublisher(const rclcpp::NodeOptions & options)
// 1) 声明参数(类型模板版 + 描述符)
this->declare_parameter<int>(
"publish_rate_hz", 2,
rcl_interfaces::msg::ParameterDescriptor().set_description("发布频率 (Hz)"));
rcl_interfaces::msg::ParameterDescriptor().set__description("发布频率 (Hz)"));
this->declare_parameter<std::string>(
"topic_name", "chatter",
rcl_interfaces::msg::ParameterDescriptor().set_description("发布话题名"));
rcl_interfaces::msg::ParameterDescriptor().set__description("发布话题名"));
// 2) 读参数
const int publish_rate_hz = this->get_parameter("publish_rate_hz").as_int();
@@ -27,7 +32,6 @@ ChatterPublisher::ChatterPublisher(const rclcpp::NodeOptions & options)
// 3) 构造发布者 + 定时器
publisher_ = this->create_publisher<std_msgs::msg::String>(topic_name, 10);
// 防零除
const auto period = (publish_rate_hz > 0) ?
std::chrono::milliseconds(1000 / publish_rate_hz) :
std::chrono::milliseconds(1000);
@@ -52,12 +56,4 @@ void ChatterPublisher::timer_callback()
}
}
} // namespace cpp_pubsub
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<cpp_pubsub::ChatterPublisher>());
rclcpp::shutdown();
return 0;
}
} // namespace cpp_pubsub
@@ -0,0 +1,10 @@
// chatter_publisher_main.cpp - ChatterPublisher 节点入口
#include "cpp_pubsub/chatter_publisher.hpp"
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<cpp_pubsub::ChatterPublisher>());
rclcpp::shutdown();
return 0;
}
+8 -12
View File
@@ -1,4 +1,4 @@
// chatter_subscriber.cpp - C++ ChatterSubscriber 实现
// chatter_subscriber.cpp - ChatterSubscriber 实现(不含 main)
#include "cpp_pubsub/chatter_subscriber.hpp"
#include <string>
@@ -11,7 +11,7 @@ ChatterSubscriber::ChatterSubscriber(const rclcpp::NodeOptions & options)
{
this->declare_parameter<std::string>(
"topic_name", "chatter",
rcl_interfaces::msg::ParameterDescriptor().set_description("订阅话题名"));
rcl_interfaces::msg::ParameterDescriptor().set__description("订阅话题名"));
const std::string topic_name = this->get_parameter("topic_name").as_string();
@@ -25,18 +25,14 @@ ChatterSubscriber::ChatterSubscriber(const rclcpp::NodeOptions & options)
void ChatterSubscriber::message_callback(const std_msgs::msg::String::SharedPtr msg)
{
try {
RCLCPP_INFO(this->get_logger(), "recv #%zu: \"%s\"", received_count_++, msg->data.c_str());
received_count_++;
if (received_count_ % 10 == 0) {
RCLCPP_INFO(this->get_logger(),
"recv #%zu: \"%s\"", received_count_, msg->data.c_str());
}
} catch (const std::exception & exc) {
RCLCPP_ERROR(this->get_logger(), "callback failed: %s", exc.what());
}
}
} // namespace cpp_pubsub
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<cpp_pubsub::ChatterSubscriber>());
rclcpp::shutdown();
return 0;
}
} // namespace cpp_pubsub
@@ -0,0 +1,10 @@
// chatter_subscriber_main.cpp - ChatterSubscriber 节点入口
#include "cpp_pubsub/chatter_subscriber.hpp"
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<cpp_pubsub::ChatterSubscriber>());
rclcpp::shutdown();
return 0;
}
+6 -6
View File
@@ -1,9 +1,9 @@
# test/test_pub_sub.cpp - cpp_pubsub 单元测试
#
# 设计思想:
# - SetUpTestSuite / TearDownTestSuite 共享 rclcpp::init / shutdown
# - 用 spin_some(50ms) 代替 spin() 控制超时
# - 不依赖 launch_testing(避免环境耦合)
// test/test_pub_sub.cpp - cpp_pubsub 单元测试
//
// 设计思想:
// - SetUpTestSuite / TearDownTestSuite 共享 rclcpp::init / shutdown
// - 用 spin_some(50ms) 代替 spin() 控制超时
// - 不依赖 launch_testing(避免环境耦合)
#include <chrono>
#include <memory>