init: ROS2 learning suite

This commit is contained in:
xs
2026-08-03 18:09:35 +08:00
commit 5ef38ab508
95 changed files with 13322 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
cmake_minimum_required(VERSION 3.16)
project(cpp_pubsub VERSION 0.1.0)
if(NOT CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 17)
endif()
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(std_msgs REQUIRED)
include_directories(include)
add_executable(talker src/publisher_member_function.cpp)
ament_target_dependencies(talker rclcpp std_msgs)
add_executable(listener src/subscriber_member_function.cpp)
ament_target_dependencies(listener rclcpp std_msgs)
install(TARGETS
talker
listener
DESTINATION lib/${PROJECT_NAME}
)
install(DIRECTORY launch
DESTINATION share/${PROJECT_NAME}/
)
# ------------------------------------------------------------------
# 测试:ament_cmake_gtest 集成 gtest,
# colcon test --packages-select cpp_pubsub 时自动跑 test_pub_sub。
# ------------------------------------------------------------------
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 rclcpp std_msgs)
endif()
ament_package()
+68
View File
@@ -0,0 +1,68 @@
"""
pubsub_launch.py —— 启动 talker_cpp + listener_cpp,演示 launch 命令行参数。
与 py_pubsub 的同名 launch 文件对照学习:
1) 这里引入了 DeclareLaunchArgument:把 'topic' 声明为命令行可改的形参;
2) 用 LaunchConfiguration('topic') 在生成时取其值,
让 Node 的 topic 参数跟随命令行传入变化(launch_arguments 是
IncludeLaunchDescription 内部传参的标准做法)。
3) 这样从外部:
ros2 launch cpp_pubsub pubsub_launch.py topic:=my_topic
会让两个 C++ 节点都订阅/发布到 my_topic。
运行:
source install/setup.bash
ros2 launch cpp_pubsub pubsub_launch.py
ros2 launch cpp_pubsub pubsub_launch.py topic:=hello
"""
# LaunchDescription:容器类型。
from launch import LaunchDescription
# DeclareLaunchArgument:声明一个可被命令行 / 父 launch 覆盖的形参。
from launch.actions import DeclareLaunchArgument
# LaunchConfiguration:在生成 LaunchDescription 期间"懒求值"某个参数的值。
from launch.substitutions import LaunchConfiguration
# Node:声明运行一个 ROS2 节点的动作。
from launch_ros.actions import Node
def generate_launch_description():
"""导出函数,返回 LaunchDescription 实例。"""
# 声明 topic 形参,默认值 'chatter',在终端可用 topic:=xxx 覆盖。
topic_arg = DeclareLaunchArgument(
'topic',
default_value='chatter',
description='Topic name for both pubs/subs',
)
# 取出 LaunchConfiguration('topic') 的当前值,后续当参数用。
topic = LaunchConfiguration('topic')
return LaunchDescription([
topic_arg, # 把声明本身也放进 LaunchDescription 里(声明是动作之一)。
# C++ 发布节点
Node(
package='cpp_pubsub',
executable='talker', # CMakeLists 里 add_executable(talker ...)
name='talker_cpp',
output='screen',
parameters=[{
'period_ms': 500,
'topic': topic, # 用上面 LaunchConfiguration 取代硬编码字符串,
# 让命令行参数动态注入。
}],
),
# C++ 订阅节点
Node(
package='cpp_pubsub',
executable='listener',
name='listener_cpp',
output='screen',
parameters=[{
'topic': topic,
}],
),
])
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0"?>
<!--
package.xml —— cpp_pubsub 包的元数据。
对 C++ 包而言,<exec_depend> 一般可缺,因为 CMakeLists.txt 里 find_package
已经显式声明依赖;但仍建议写 <depend> 让 colcon 能正确排序构建顺序。
-->
<?xml-model
href="http://download.ros.org/schema/package_format3.xsd"
schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>cpp_pubsub</name>
<version>0.1.0</version>
<description>C++ talker/listener demo for ROS2 Humble</description>
<maintainer email="dev@example.com">xs</maintainer>
<license>Apache-2.0</license>
<!-- depend:这是 C++ 编译 + 链接 + 运行都需要的 ROS 库。 -->
<depend>rclcpp</depend> <!-- C++ 客户端库。 -->
<depend>std_msgs</depend> <!-- 内置基础消息。 -->
<!-- test_depend:仅在 colcon test 时被安装/使用。 -->
<test_depend>ament_lint_auto</test_depend> <!-- 自动调度几种 lint -->
<test_depend>ament_lint_common</test_depend> <!-- cpplint / uncrustify -->
<test_depend>ament_cmake_gtest</test_depend> <!-- gtest 集成 -->
<test_depend>launch_testing_ros</test_depend><!-- launch integration -->
<!-- export/build_type = ament_cmake:让 colcon 调用 CMake 构建。 -->
<export>
<build_type>ament_cmake</build_type>
</export>
</package>
@@ -0,0 +1,113 @@
// =============================================================================
// talker_cpp —— ROS2 C++ 发布者节点(Publisher)
//
// 用途:与 Python 版本的 talker_py 形成"跨语言通信"演示,二者通过同一 Topic
// 'chatter' 都用 std_msgs/String,体现 ROS2 的多语言互操作性
// (因为底层都是 DDS,语言只是同一套消息定义的视图)。
//
// ROS2 概念速览:
// Node —— 节点,所有发布/订阅/服务/参数都挂在节点上。
// Publisher<T> —— 发布者,T 是消息类型,publish(msg) 异步投递到 DDS。
// Timer —— 周期性回调,WallTimer / SteadyTimer 等类型。
// Parameter —— 节点配置,declare_parameter<T>() + get_parameter()。
// std_msgs/String —— 自动生成的消息结构体,只含 std::string data。
//
// 构建:本文件由 ament_cmake 编译,见 src/cpp_pubsub/CMakeLists.txt。
// 运行:ros2 run cpp_pubsub talker
// ros2 run cpp_pubsub talker --ros-args -p period_ms:=200 -p topic:=hello
// =============================================================================
#include <chrono> // std::chrono::milliseconds,用来描述 timer 的周期。
#include <memory> // std::shared_ptr / std::make_shared,ROS2 句柄大量用到。
#include <string> // std::string + std::to_string。
// rclcpp:ROS Client Library for C++,所有 ROS2 C++ 节点的入口库,提供
// rclcpp::Node / Publisher / Subscription / Timer 等高级封装。
#include "rclcpp/rclcpp.hpp"
// std_msgs/msg/string.hpp:由 .msg 文件在编译期生成的 C++ 结构体,
// 内含 std::string data;(RCLCPP_LOGGER 自动序列化/反序列化它到 DDS)。
#include "std_msgs/msg/string.hpp"
// using namespace std::chrono_literals; // 允许 500ms 这种字面量。
using namespace std::chrono_literals;
/// @brief 自定义发布者节点:TalkerCPP,继承自 rclcpp::Node 基类。
class TalkerCPP : public rclcpp::Node
{
public:
/// 构造函数:在这里完成 publisher/timer/parameter 三大初始化。
TalkerCPP()
: rclcpp::Node("talker_cpp"), count_(0)
{
// declare_parameter<T>(name, default):模板版的参数声明方式,比通用
// 指针版更类型安全。这里声明两个参数:
// period_ms —— 发布周期(整数,毫秒)
// topic —— 目标 topic 名(字符串)
this->declare_parameter<int>("period_ms", 500);
this->declare_parameter<std::string>("topic", "chatter");
// 取回参数实际值(as_int() / as_string()),用于初始化 publisher / timer。
int period_ms = this->get_parameter("period_ms").as_int();
std::string topic = this->get_parameter("topic").as_string();
// create_publisher<T>(topic, qos_depth):创建发布者。
// T = std_msgs::msg::String —— 模板参数告诉编译器消息类型。
// qos_depth = 10,队列最多缓存 10 条未送达样本(慢消费者场景)。
publisher_ = this->create_publisher<std_msgs::msg::String>(topic, 10);
// create_wall_timer(period, callback):挂一个 wall-clock 定时器。
// 当 period = std::chrono::milliseconds(period_ms) 时,
// 每次"节点事件循环"经过 period_ms 就回调 timer_callback 一次。
// 注意:std::bind 把 this + 成员函数绑定成可调用对象。
timer_ = this->create_wall_timer(
std::chrono::milliseconds(period_ms),
std::bind(&TalkerCPP::timer_callback, this));
// RCLCPP_INFO:节点级日志,前缀自动带 [INFO] [时间] [节点名]。
RCLCPP_INFO(this->get_logger(),
"talker_cpp started -> topic=%s, period=%dms",
topic.c_str(), period_ms);
}
private:
/// @brief 定时器回调,每次触发就构造并发布一条消息。
void timer_callback()
{
// 1) 构造一条 std_msgs/String 消息,填写 data。
auto msg = std_msgs::msg::String();
msg.data = "Hello from C++, seq=" + std::to_string(count_++);
// 2) publisher_->publish(msg) — 异步把消息放入 DDS 队列,
// 等订阅方 QoS 匹配即被接收;此调用不阻塞。
publisher_->publish(msg);
// 3) 顺手打印日志,便于观察节奏。
RCLCPP_INFO(this->get_logger(), "pub: \"%s\"", msg.data.c_str());
}
// SharedPtr:节点内所有"句柄"都用智能指针管理,可被 Node 持有,
// 并在节点析构时一起释放,避免生命周期问题。
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
rclcpp::TimerBase::SharedPtr timer_;
size_t count_; // 消息序号,用于调试观测。
};
/// @brief ROS2 C++ 节点的标准 main 模板:
/// init -> 实例化节点 -> spin -> shutdown。
/// 注意:C++ 里没有 KeyboardInterrupt 捕获,因为 Ctrl+C 在终端层
/// 直接发给 rclcpp::shutdown(),spin() 自动返回。
int main(int argc, char * argv[])
{
// rclcpp::init:解析 ROS 专属命令行参数(--ros-args 之后的部分),
// 初始化底层 DDS 上下文。
rclcpp::init(argc, argv);
// std::make_shared<TalkerCPP>():把节点对象交给 shared_ptr,
// 这是 rclcpp::spin 的强制要求,Node 内部要拿到 shared_ptr 计数。
rclcpp::spin(std::make_shared<TalkerCPP>());
// spin() 返回后做清理。
rclcpp::shutdown();
return 0;
}
@@ -0,0 +1,68 @@
// =============================================================================
// listener_cpp —— ROS2 C++ 订阅者节点(Subscriber)
//
// 与 talker_cpp / talker_py 配对使用,演示 C++ 侧订阅 + 跨语言互联互通。
//
// 关键概念:
// Subscription<T>:由 node.create_subscription<T>(...) 创建,
// 每收到一条 T 类型消息,ROS2 自动调用回调函数,回调签名:
// void cb(const T::SharedPtr msg)
// SharedPtr 指 std::shared_ptr<T>,可 0 拷贝访问 msg 字段。
//
// 工具:
// ros2 topic info chatter -v 看 publisher/subscriber 列表 + 消息类型。
// ros2 topic echo chatter 命令行实时打印消息内容。
// =============================================================================
#include <memory> // std::placeholders,std::bind 会用到。
#include <string> // std::string。
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
/// @brief 订阅者节点:ListenerCPP,继承自 rclcpp::Node。
class ListenerCPP : public rclcpp::Node
{
public:
ListenerCPP()
: rclcpp::Node("listener_cpp")
{
// 声明 + 读取 topic 参数,与 talker 配对。
this->declare_parameter<std::string>("topic", "chatter");
std::string topic = this->get_parameter("topic").as_string();
// create_subscription<T>(topic, qos_depth, callback):
// - T = std_msgs::msg::String —— 与 publisher 一致才能匹配。
// - topic —— 与 publisher 一致才能通信。
// - qos_depth = 10 —— 缓冲队列深度。
// - callback 用 std::bind 绑定 this + 成员函数,
// 而 `_1` (std::placeholders::_1) 表示回调的第一个参数 = msg。
subscription_ = this->create_subscription<std_msgs::msg::String>(
topic, 10,
std::bind(&ListenerCPP::topic_callback, this, std::placeholders::_1));
RCLCPP_INFO(this->get_logger(), "listener_cpp subscribed <- %s", topic.c_str());
}
private:
/// @brief 收到消息回调。const 修饰保证不修改成员,SharedPtr 安全。
void topic_callback(const std_msgs::msg::String::SharedPtr msg) const
{
// 这里只是示例,真实节点通常:
// 1) 解析 msg->data(若是 JSON,反序列化成业务对象);
// 2) 推入线程安全队列给控制线程消费;
// 3) 触发 TF 变换、可视化、决策等下游动作。
RCLCPP_INFO(this->get_logger(), "recv: \"%s\"", msg->data.c_str());
}
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};
/// @brief C++ 节点标准 main。
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<ListenerCPP>());
rclcpp::shutdown();
return 0;
}
+95
View File
@@ -0,0 +1,95 @@
// =============================================================================
// test_pub_sub.cpp —— C++ 节点端到端集成测试 (gtest)。
//
// 验证 TalkerCPP / ListenerCPP 的发布-订阅链可正常运转:
// 1) 启动 talker (publisher) 与 listener (subscriber) 实例;
// 2) 在节点事件循环里 spin 一段时间,直到 listener 收到消息;
// 3) assert 收到 ≥ 1 条,且消息来自 publisher。
//
// 注意:ROS2 单测标准做法是用 launch_testing_ros 在多进程下做端到端测试;
// 本测试在同一进程内 spin 一次,跑得快但不覆盖真实 DDS 互通(那个
// 在 integration test 里覆盖)。此处更偏"单元"性质。
//
// 运行:本测试由 colcon test --packages-select cpp_pubsub 自动调用,
// 其依赖在 CMakeLists.txt 的 if(BUILD_TESTING) ... 块内拉起。
// =============================================================================
#include <chrono>
#include <memory>
#include <string>
#include "gtest/gtest.h"
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using namespace std::chrono_literals;
// 测试夹具:继承 rclcpp::Node + 携带一个 listener 订阅,做"收到 N 条"断言。
class PubsubFixture : public rclcpp::Node
{
public:
PubsubFixture()
: rclcpp::Node("pubsub_test_node"), received_(0)
{
subscription_ = this->create_subscription<std_msgs::msg::String>(
"chatter_unit_test", 10,
[this](const std_msgs::msg::String::SharedPtr msg) {
(void)msg;
received_++;
});
}
int received_count() const { return received_; }
private:
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
int received_;
};
// Test 1:验证 publisher 在 1s 内能发出至少 1 条消息,
// 通过 ros2 topic hz 等价手段(同进程订阅计数)做侧面验证。
TEST(PubsubTest, TalkerPublishesAtLeastOnce)
{
auto node = std::make_shared<PubsubFixture>();
auto publisher = node->create_publisher<std_msgs::msg::String>("chatter_unit_test", 10);
// 用 wall timer 触发一次 publish,然后 spin 200ms 让回调跑完。
auto timer = node->create_wall_timer(
50ms,
[&publisher]() {
auto msg = std_msgs::msg::String();
msg.data = "unit-test-msg";
publisher->publish(msg);
});
// Humble 的 rclcpp::spin_some 只接 1 个参数,
// 用 SingleThreadedExecutor 显式给每次 spin 一段时间。
auto exec = std::make_shared<rclcpp::executors::SingleThreadedExecutor>();
exec->add_node(node);
auto end = std::chrono::steady_clock::now() + 500ms;
while (std::chrono::steady_clock::now() < end) {
exec->spin_some(50ms);
}
ASSERT_GE(node->received_count(), 1) <<
"subscriber should have received at least one message within 500ms";
}
// Test 2:验证 std_msgs/String 字段正确填充(数据类型契约)。
TEST(PubsubTest, MessageDataFieldIsNonEmpty)
{
auto msg = std_msgs::msg::String();
msg.data = "non-empty";
EXPECT_FALSE(msg.data.empty());
EXPECT_EQ(msg.data, "non-empty");
}
int main(int argc, char ** argv)
{
::testing::InitGoogleTest(&argc, argv);
rclcpp::init(argc, argv);
int rc = RUN_ALL_TESTS();
rclcpp::shutdown();
return rc;
}