init: ROS2 learning suite
This commit is contained in:
@@ -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()
|
||||
@@ -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,
|
||||
}],
|
||||
),
|
||||
])
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user