ROS2学习笔记之C++编写简单发布订阅节点篇
学习目标:用C++创建并且运行订阅发布的简单节点
背景
节点是一个通过ROS网络进行数据交互的可执行文件的进程。在这篇教程当中,我们编写一个节点通过话题的方式进行字符串消息的传递。在这个例子中我们使用“talker” 和 “listener” 的模式,一个发布数据一个接收数据,以便更好的接收数据。
前期准备
了解前面的教程
学习内容
1. 为例程创建一个包
包应该放在src文件中,首先我们打开一个终端
cd dev_ws/src
然后我们创建一个包
ros2 pkg create --build-type ament_cmake cpp_pubsub
终端会显示创建了一些文件和目录表示创建成功。
然后我们进入到包的src文件夹下,前面我们讲过一个包的源文件一般放在这个地方。
cd cpp_pubsub/src
2. 编写publisher发布者节点
我们通过下面的命令下载talker的样例代码
wget -O publisher_member_function.cpp https://raw.githubusercontent.com/ros2/examples/master/rclcpp/minimal_publisher/member_function.cpp
下载完成过后我们选择一个编辑器打开,我们看到代码如下。ROS2这一来感觉就好麻烦,感觉和ros1的nodelet比较像
#include <chrono>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using namespace std::chrono_literals;
/* This example creates a subclass of Node and uses std::bind() to register a
* member function as a callback from the timer. */
class MinimalPublisher : public rclcpp::Node
{
public:
MinimalPublisher()
: Node("minimal_publisher"), count_(0)
{
publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
timer_ = this->create_wall_timer(
500ms, std::bind(&MinimalPublisher::timer_callback, this));
}
private:
void timer_callback()
{
auto message = std_msgs::msg::String();
message.data = "Hello, world! " + std::to_string(count_++);
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
publisher_->publish(message);
}
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 代码解释
第一行的chrono
头文件这个熟悉C++14就知道这是一个关于时间的库,然后我们可以使用chrono_literals
和500ms
,总之就是给时间操作提供了方便。memory
不多说关于指针的头文件。rclcpp/rclcpp.hpp
让我们可以使用ROS2中功能的头文件,至于为啥是这个名字,rclcpp是ros官方提供的一个C++的客户端。std_msgs/msg/string.hpp
很显然是消息类型的头文件。
这些include就代表了该节点的依赖在前面我们讲过依赖关系需要体现在package.xml
和 CMakeLists.txt
中,我们讲完这个cpp过后就会去将这些文件。
#include <chrono>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using namespace std::chrono_literals;
接下来我们创建了一个名为MinimalPublisher
的类,它继承自rclcpp::Node
。this
指针就代表了这个类。
class MinimalPublisher : public rclcpp::Node
public
部分是构造函数,初始化了节点的名字为minimal_publisher,并且初始化计数的count_为0.在构造函数内部,对发布者publisher进行了初始化,指定消息类型为String类型,同时设置了话题名称为topic
,和话题的消息缓冲池长度为10。然后初始化了一个定时器,每隔500ms执行一次timer_callback
函数。
public:
MinimalPublisher()
: Node("minimal_publisher"), count_(0)
{
publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
timer_ = this->create_wall_timer(
500ms, std::bind(&MinimalPublisher::timer_callback, this));
}
timer_callback
函数作为定时回调的函数,这里是消息创建和发布出去的地方。RCLCPP_INFO
是一个宏,和ros_info类似在终端打印消息。这样每发送一个消息我们就可以在终端看到输出。
Private:
void timer_callback()
{
auto message = std_msgs::msg::String();
message.data = "Hello, world! " + std::to_string(count_++);
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
publisher_->publish(message);
}
最后这部分是定时器、发布者、计数器的声明
rclcpp::TimerBase::SharedPtr timer_;
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
size_t count_;
MinimalPublisher
这个类完了就是main函数,这个节点真正执行的地方。rclcpp::init
对节点进行了初始化,rclcpp::spin
开始处理回调,包括定时器。
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalPublisher>());
rclcpp::shutdown();
return 0;
}
2.2 添加依赖
打开dev_ws/src/cpp_pubsub
文件夹下的package.xml
文件
根据前面讲的内容我们首先对下面关于这个包的描述、维护者、许可证进行编辑
<description>Examples of minimal publisher/subscriber using rclcpp</description>
<maintainer email="you@email.com">Your Name</maintainer>
<license>Apache License 2.0</license>
在ament_cmake
描述的编译依赖过后之后粘贴下面两行
<exec_depend>rclcpp</exec_depend>
<exec_depend>std_msgs</exec_depend>
这两行表示了在执行的时候需要rclpp
和 std_msgs
这两个依赖
然后保存文件
2.3 CMakeLists.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_member_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)
# Default to 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_member_function.cpp)
ament_target_dependencies(talker rclcpp std_msgs)
install(TARGETS
talker
DESTINATION lib/${PROJECT_NAME})
ament_package()
现在我们就可以编译节点然后source一下就可以运行了,但是我们等写好订阅者过后一起编译方便查看整体运行效果。
3. 编写subscriber订阅者节点
首先进入这个包的src文件夹
cd ~/dev_ws/src/cpp_pubsub/src
我们通过下面的命令下载listener的样例代码
wget -O subscriber_member_function.cpp https://raw.githubusercontent.com/ros2/examples/master/rclcpp/minimal_subscriber/member_function.cpp
下载完成过后我们通过ls
命令查看当前文件下的文件,我们可以看到现在目录下面有两个文件。
publisher_member_function.cpp subscriber_member_function.cpp
选择一个编辑器打开subscriber_member_function.cpp
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using std::placeholders::_1;
class MinimalSubscriber : public rclcpp::Node
{
public:
MinimalSubscriber()
: Node("minimal_subscriber")
{
subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
}
private:
void topic_callback(const std_msgs::msg::String::SharedPtr msg) const
{
RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg->data.c_str());
}
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
创建了回调。
在订阅者的代码中我们这次没有写定时器,因为订阅者我们不需要发布消息,只需要接收消息。
public:
MinimalSubscriber()
: Node("minimal_subscriber")
{
subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
}
还有一点话题和消息类型和发布者的保持一致,话题名字为topic
,消息类型为String
topic_callback
函数接收话题上的数据,然后将数据通过RCLCPP_INFO
打印到控制台
然后是subscription_
的声明。
private:
void topic_callback(const std_msgs::msg::String::SharedPtr msg) const
{
RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg->data.c_str());
}
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
main函数和发布者的差不多。spin
的作用这个和ros1中差不多。
由于发布者和订阅者的依赖是一样的,所以我们不需要对package.xml
添加任何内容。
3.2 CMakeLists.txt
打开CMakeLists.txt
然后在发布的下面添加生成订阅者可执行文件(add_executable)已经他的依赖链接(ament_target_dependencies),在install部分talker
下面添加listener
add_executable(listener src/subscriber_member_function.cpp)
ament_target_dependencies(listener rclcpp std_msgs)
install(TARGETS
talker
listener
DESTINATION lib/${PROJECT_NAME})
最后一个完整CMakeLists.txt
的像下面这样
cmake_minimum_required(VERSION 3.5)
project(cpp_pubsub)
# Default to 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_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})
ament_package()
然后记得保存CMakeLists.txt
,到现在我们就准备完成了,可以进行编译了。
4. 编译节点并运行
虽然我们已经安装了rclpp
和std_msgs
这些依赖,但是在编译之前进行依赖检查是一个好习惯。--from-path
后面根表示的是要进行依赖检查的目录。
cd ~/dev_ws
sudo rosdep install -i --from-path src/cpp_pubsub --rosdistro eloquent -y
开始编译,我们选择只编译我们新建的包
colcon build --packages-select cpp_pubsub
打开一个新的终端,我们运行发布者
cd ~/dev_ws
source install/setup.bash
ros2 run cpp_pubsub talker
发布者开始运行,同时在终端打印出了消息,频率为2hz
[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"
打开另外一个新的终端,我们运行订阅者
cd ~/dev_ws
source install/setup.bash
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关掉两个节点。
总结
经过了这么多的教程终于讲到了编写话题订阅的代码,ros2引入了不少的新特性,用到了很多C++14的新特性。