ROS2 Jazzy:写一个简单的发布者/订阅者(C++)
ROS2 Jazzy:写一个简单的发布者/订阅者(C++)
背景知识
节点是通过 ROS 图进行通信的可执行进程。节点将通过话题以字符串消息的形式相互传递信息。这里使用的示例是一个简单的“发布者”和“订阅者”系统;一个节点发布数据,另一个节点订阅该话题以接收这些数据。
这些示例中使用的代码可以在此处找到(
https://github.com/ros2/examples/tree/jazzy/rclcpp/topics)。
操作步骤
1. 创建软件包
打开一个新终端,并加载你的 ROS2 安装环境,以便 ros2 命令可以正常工作。
创建工作空间 ros2_ws 目录,并在此目录下创建src目录。
进入 ros2_ws/src 目录,并运行创建软件包的命令:
ros2 pkg create --build-type ament_cmake --license Apache-2.0 cpp_pubsub
你的终端会返回一条消息,确认 cpp_pubsub 软件包已经创建,并且同时会创建所有必要的文件和文件夹。
进入
ros2_ws/src/cpp_pubsub/src 目录。请记住,在任何 CMake 软件包中,可执行文件的源文件都应该放在这个目录中。
2. 编写发布者节点
通过输入以下命令下载发布者示例代码:
wget -O publisher_lambda_function.cpp https://raw.githubusercontent.com/ros2/examples/jazzy/rclcpp/topics/minimal_publisher/lambda.cpp
现在会有一个名为
publisher_lambda_function.cpp 的新文件。使用文本编辑器打开该文件。
#include <chrono>
#include <memory>
#include <string>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using namespace std::chrono_literals;
/* 此示例创建了一个 Node 的子类,并使用了一个高级的 C++11 lambda 函数来简化回调语法,但这样做的代价是代码乍一看可能有点难以理解。 */
class MinimalPublisher : public rclcpp::Node
{
public:
MinimalPublisher()
: Node("minimal_publisher"), count_(0)
{
publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
auto timer_callback =
[this]() -> void {
auto message = std_msgs::msg::String();
message.data = "Hello, world! " + std::to_string(this->count_++);
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
this->publisher_->publish(message);
};
timer_ = this->create_wall_timer(500ms, timer_callback);
}
private:
rclcpp::TimerBase::SharedPtr timer_;
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
size_t count_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalPublisher>());
rclcpp::shutdown();
return 0;
}
2.1 代码分析
代码开头包含了你将使用的标准 C++ 头文件。在标准 C++ 头文件之后是 rclcpp/rclcpp.hpp,它允许你使用 ROS2 系统中最常用的部分。最后是 std_msgs/msg/string.hpp,它包含了你将用于发布数据的 ROS 内置消息类型。
#include <chrono>
#include <memory>
#include <string>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using namespace std::chrono_literals;
这些行表示节点的依赖项。这些依赖项必须添加到 package.xml 和 CMakeLists.txt 文件中,你将在下一部分完成这个操作。
下一行通过继承 rclcpp::Node 创建了节点类 MinimalPublisher。代码中的每个 this 都指的是该节点。
class MinimalPublisher : public rclcpp::Node
公共构造函数将节点命名为 minimal_publisher,并将 count_ 初始化为 0。
在构造函数内部,使用 String 消息类型、话题名称 topic 和所需的队列大小(用于在消息积压时限制消息数量)初始化发布者。
接下来,声明了一个名为 timer_callback 的 lambda 函数。它通过引用捕获当前对象 this,不接受任何输入参数并返回 void。
而timer_callback 函数创建一个新的 String 类型的消息,用所需的字符串设置它的数据并发布它。
RCLCPP_INFO 宏确保每个发布的消息都打印到控制台。最后,初始化 timer_,这里配置 timer_callback 函数每秒执行两次。
public:
MinimalPublisher()
: Node("minimal_publisher"), count_(0)
{
publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
auto timer_callback =
[this]() -> void {
auto message = std_msgs::msg::String();
message.data = "Hello, world! " + std::to_string(this->count_++);
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
this->publisher_->publish(message);
};
timer_ = this->create_wall_timer(500ms, timer_callback);
}
在类的底部是定时器、发布者和计数器字段的声明。
private:
rclcpp::TimerBase::SharedPtr timer_;
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
size_t count_;
在 MinimalPublisher 类之后是 main 函数,节点实际上在这里被运行。rclcpp::init 初始化 ROS2,rclcpp::spin 开始处理来自节点的数据,包括定时器的回调。
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalPublisher>());
rclcpp::shutdown();
return 0;
}
2.2 添加依赖项
返回上一级目录到 ros2_ws/src/cpp_pubsub 目录,这里已经为你创建了 CMakeLists.txt 和 package.xml 文件。
首先,打开 package.xml 文件。填写 <description>、<maintainer> 和 <license> 标签:
<description>Examples of minimal publisher/subscriber using rclcpp</description>
<maintainer email="you@email.com">Your Name</maintainer>
<license>Apache-2.0</license>
在 ament_cmake 构建工具依赖项之后添加一行,并粘贴以下依赖项:
<depend>rclcpp</depend>
<depend>std_msgs</depend>
这表明软件包在构建和执行其代码时需要 rclcpp 和 std_msgs。
2.3CMakeLists.txt文件
现在打开 CMakeLists.txt 文件。在现有的依赖项 find_package(ament_cmake REQUIRED) 下面,添加以下行:
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
之后,添加可执行文件并将其命名为 talker,这样你就可以使用 ros2 run 运行你的节点:
add_executable(talker src/publisher_lambda_function.cpp)
ament_target_dependencies(talker rclcpp std_msgs)
最后,添加 install(TARGETS...) 部分,以便 ros2 run 可以找到你的可执行文件:
install(TARGETS
talker
DESTINATION lib/${PROJECT_NAME})
接下来删除一些不需要的部分和注释清理 CMakeLists.txt 文件,最后它看起来像这样:
cmake_minimum_required(VERSION 3.5)
project(cpp_pubsub)
# 默认使用 C++14
if(NOT CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 14)
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)
add_executable(talker src/publisher_lambda_function.cpp)
ament_target_dependencies(talker rclcpp std_msgs)
install(TARGETS
talker
DESTINATION lib/${PROJECT_NAME})
ament_package()
你现在可以编译你的软件包了,但是先让我们创建订阅者节点,这样你就可以看到整个系统的运行情况。
3. 编写订阅者节点
返回
ros2_ws/src/cpp_pubsub/src 目录创建下一个节点。在终端中输入以下命令:
wget -O subscriber_lambda_function.cpp https://raw.githubusercontent.com/ros2/examples/jazzy/rclcpp/topics/minimal_subscriber/lambda.cpp
使用文本编辑器打开
subscriber_lambda_function.cpp 文件。
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
class MinimalSubscriber : public rclcpp::Node
{
public:
MinimalSubscriber()
: Node("minimal_subscriber")
{
auto topic_callback =
[this](std_msgs::msg::String::UniquePtr msg) -> void {
RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg->data.c_str());
};
subscription_ =
this->create_subscription<std_msgs::msg::String>("topic", 10, topic_callback);
}
private:
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalSubscriber>());
rclcpp::shutdown();
return 0;
}
3.1 代码分析
订阅者节点的代码与发布者的代码几乎相同。只法这现在节点名为 minimal_subscriber,构造函数使用节点的 create_subscription 函数来执行回调。
可以看到的是,这里没有定时器,因为订阅者只是在数据发布到 topic 话题时做出响应。
topic_callback 函数接收通过话题发布的字符串消息数据,并使用 RCLCPP_INFO 宏将其写入控制台。
在这里,发布者和订阅者使用的话题名称和消息类型必须匹配,这样它们才能进行通信。
public:
MinimalSubscriber()
: Node("minimal_subscriber")
{
auto topic_callback =
[this](std_msgs::msg::String::UniquePtr msg) -> void {
RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg->data.c_str());
};
subscription_ =
this->create_subscription<std_msgs::msg::String>("topic", 10, topic_callback);
}
这个类中唯一的字段声明是订阅者。
private:
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
main 函数完全相同,只是现在它启动的是 MinimalSubscriber 节点。对于发布者节点,启动意味着启动定时器,但是对于订阅者来说,它只是准备在消息到来时接收消息。
由于这个节点与发布者节点具有相同的依赖项,因此不需要在 package.xml 中添加新内容。
3.2CMakeLists.txt文件
重新打开 CMakeLists.txt 文件,并在发布者条目下方添加订阅者节点的可执行文件和目标。
add_executable(listener src/subscriber_lambda_function.cpp)
ament_target_dependencies(listener rclcpp std_msgs)
install(TARGETS
talker
listener
DESTINATION lib/${PROJECT_NAME})
4. 编译并运行
作为 ROS2 系统的一部分,你可能已经安装了 rclcpp 和 std_msgs 软件包。在构建之前,在工作空间的根目录(ros2_ws)中运行 rosdep 来检查是否缺少依赖项是一个好习惯:
rosdep install -i --from-path src --rosdistro jazzy -y
仍然在工作空间的根目录 ros2_ws 中,构建你的新软件包:
colcon build --packages-select cpp_pubsub
打开一个新终端,导航到 ros2_ws 目录,并加载设置文件:
. install/setup.bash
现在运行发布者节点:
ros2 run cpp_pubsub talker
终端应该每 0.5 秒开始发布信息消息,如下所示:
[INFO] [minimal_publisher]: Publishing: "Hello World: 0"
[INFO] [minimal_publisher]: Publishing: "Hello World: 1"
[INFO] [minimal_publisher]: Publishing: "Hello World: 2"
[INFO] [minimal_publisher]: Publishing: "Hello World: 3"
[INFO] [minimal_publisher]: Publishing: "Hello World: 4"
打开另一个终端,同样再次在 ros2_ws 目录中加载设置文件,然后启动订阅者节点:
ros2 run cpp_pubsub listener
订阅者将开始在控制台打印消息,如下所示,它会从发布者当时的消息计数开始打印:
[INFO] [minimal_subscriber]: I heard: "Hello World: 10"
[INFO] [minimal_subscriber]: I heard: "Hello World: 11"
[INFO] [minimal_subscriber]: I heard: "Hello World: 12"
[INFO] [minimal_subscriber]: I heard: "Hello World: 13"
[INFO] [minimal_subscriber]: I heard: "Hello World: 14"
在每个终端中输入 Ctrl+C 以停止节点的运行。
总结
你创建了两个节点,通过话题发布和订阅数据。在编译和运行它们之前,你将它们的依赖项和可执行文件添加到了软件包配置文件中。
关注【智践行】公众号,发送 【机器人】 获得机器人经典学习资料