ROS2——手把手教你编写一个话题
话题简介
ROS2将复杂的机器人系统拆解成许多模块节点,而这些节点之间则是通过一个至关重要的通道完成数据交换的,这个通道就是“话题”。

一个节点可以通过多个话题向外发布数据,也可以同时订阅多个其他节点发布的话题,相当于话题是一个多对多的订阅/发布模型。

可见,话题是节点之间实现数据传输的重要途径,也是机器人各个子系统之间交换数据的重要方式。
下面, 我们将从一个实例出发, 手把手写一段话题程序
案例描述
来看一个案例:
- 有一家
KFC
和一个饥肠辘辘的Customer
.
Customer
给KFC
10元钱就能买到一个汉堡, 这个Customer
饿得很快, 每秒都要吃一个汉堡.
KFC
收到Customer
发来的10元钱, 向Customer
发送汉堡, 为了推销大鸡腿, 每5秒发布一条广告.
以上案例实际上就实现了一个话题, KFC
和Customer
是两个节点Node
, 付钱/发送汉堡/发送广告即为发布者, 收钱/接收汉堡/收取广告即为订阅者.
下面来编写这段程序
新建工作空间
1 2
| mkdir -p ros2_ws/src cd ros2_ws/src
|
mkdir -p
: 递归创建目录,即使上级目录不存在,会按目录层级自动创建目录
新建功能包
1
| ros2 pkg create customer_and_kfc --build-type ament_cmake --dependencies rclcpp std_msgs
|
使用ament_cmake
作为编译类型, 并使用依赖rclcpp
和std_msgs
在ros2_ws/src/customer_and_kfc/src
下创建KFC.cpp
和Customer.cpp
编写KFC节点
直接献出程序, 每句都有注释, 看不懂你打我
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
| #include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp" #include "std_msgs/msg/u_int32.hpp"
using namespace std::chrono_literals;
using std::placeholders::_1;
class KFCNode : public rclcpp::Node { public: KFCNode(std::string name) : Node(name), count(1) { RCLCPP_INFO(this->get_logger(), "大家好, 我是%s的服务员.",name.c_str()); pub_hamburger = this->create_publisher<std_msgs::msg::String>("hamburger", 10); pub_advertisement = this->create_publisher<std_msgs::msg::String>("advertisement", 10); advertisement_timer = this->create_wall_timer(5000ms, std::bind(&KFCNode::advertisement_timer_callback, this)); sub_money = this->create_subscription<std_msgs::msg::UInt32>("money_of_hamburger", 10, std::bind(&KFCNode::money_callback, this, _1)); } private: size_t count;
rclcpp::TimerBase::SharedPtr advertisement_timer;
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr pub_hamburger; rclcpp::Subscription<std_msgs::msg::UInt32>::SharedPtr sub_money;
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr pub_advertisement;
void advertisement_timer_callback() { auto str_advertisement = std_msgs::msg::String(); str_advertisement.data = "大鸡腿降价啦"; RCLCPP_INFO(this->get_logger(), "KFC发布了一个广告:%s", str_advertisement.data.c_str()); pub_advertisement->publish(str_advertisement); } void money_callback(const std_msgs::msg::UInt32::SharedPtr msg) { if(msg->data == 10) { RCLCPP_INFO(this->get_logger(), "收款 %d 元", msg->data);
auto str_hamburger_num = std_msgs::msg::String(); str_hamburger_num.data = "第" + std::to_string(count++) + "个汉堡"; RCLCPP_INFO(this->get_logger(), "这是我卖出的%s", str_hamburger_num.data.c_str()); pub_hamburger->publish(str_hamburger_num); } } };
int main(int argc, char **argv) { rclcpp::init(argc, argv); auto node = std::make_shared<KFCNode>("KFC"); rclcpp::spin(node); rclcpp::shutdown(); return 0; }
|
编写Customer节点
此段程序与上面相同的语句不再解释, 请读者自行类比
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| #include "rclcpp/rclcpp.hpp" #include "std_msgs/msg/string.hpp" #include "std_msgs/msg/u_int32.hpp"
using namespace std::chrono_literals;
using std::placeholders::_1; using std::placeholders::_2;
class CustomerNode : public rclcpp::Node { public: CustomerNode(std::string name) : Node(name) { RCLCPP_INFO(this->get_logger(), "大家好,我是一个%s.",name.c_str());
sub_hamburger = this->create_subscription<std_msgs::msg::String>("hamburger", 10, std::bind(&CustomerNode::hamburger_callback, this, _1)); sub_advertisement = this->create_subscription<std_msgs::msg::String>("advertisement", 10, std::bind(&CustomerNode::advertisement_callback, this, _1)); hungry_timer = this->create_wall_timer(1000ms, std::bind(&CustomerNode::hungry_timer_callback, this));
pub_money = this->create_publisher<std_msgs::msg::UInt32>("money_of_hamburger", 10); money.data = 10;
pub_money->publish(money); RCLCPP_INFO(this->get_logger(), "我饿了, 我要吃汉堡! 付款 %d 元", money.data);
} private: std_msgs::msg::UInt32 money;
rclcpp::TimerBase::SharedPtr hungry_timer;
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr sub_hamburger;
rclcpp::Publisher<std_msgs::msg::UInt32>::SharedPtr pub_money;
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr sub_advertisement; void hamburger_callback(const std_msgs::msg::String::SharedPtr msg) { RCLCPP_INFO(this->get_logger(), "这是我吃的 %s ", msg->data.c_str()); }
void hungry_timer_callback() { RCLCPP_INFO(this->get_logger(), "我又饿了, 还想再吃一个! 付款 %d 元", money.data); pub_money->publish(money); }
void advertisement_callback(const std_msgs::msg::String::SharedPtr msg) { RCLCPP_INFO(this->get_logger(), "我收到了一条广告: %s ", msg->data.c_str()); } };
int main(int argc, char **argv) { rclcpp::init(argc, argv); auto node = std::make_shared<CustomerNode>("Customer"); rclcpp::spin(node); rclcpp::shutdown(); return 0; }
|
Cmakelist.txt
如果新建功能包的时候没有加--dependencies rclcpp std_msgs
等功能包, 则需要手动添加: (任意位置均可)
1 2
| find_package(rclcpp REQUIRED) find_package(std_msgs REQUIRED)
|
下面两对代码的作用是:
add_executable()
让编译器编译Customer.cpp
和KFC.cpp
这两个文件. 并生成可执行文件Customer_node
和KFC_node
ament_target_dependencies
添加编译的依赖
1 2 3 4 5
| add_executable(Customer_node src/Customer.cpp) ament_target_dependencies(Customer_node rclcpp std_msgs)
add_executable(KFC_node src/KFC.cpp) ament_target_dependencies(KFC_node rclcpp std_msgs)
|
将编译好的文件安装到install/customer_and_kfc/lib/customer_and_kfc
下
1 2 3 4 5
| install(TARGETS Customer_node KFC_node DESTINATION lib/${PROJECT_NAME} )
|
package.xml
同样地, 新建功能包的时候没有加--dependencies rclcpp std_msgs
等功能包, 则需要手动添加, 放置于<package>
标签下
1 2
| <depend>rclcpp</depend> <depend>std_msgs</depend>
|
也可自行修改下面这些声明, 与实现功能无关, 但是最好写全
1 2 3 4
| <version>0.0.0</version> <description>TODO: Package description</description> <maintainer email="fanziqi@fanziqi.site">fanziqi</maintainer> <license>TODO: License declaration</license>
|
编译
--packages-select
指定编译customer_and_kfc
功能包
1
| colcon build --packages-select customer_and_kfc
|
刷新环境
1 2
| echo "source /ros2_ws/install/setup.zsh" >> ~/.bashrc source ~/.bashrc
|
运行
新建一个终端窗口, 运行Customer节点
1
| ros2 run customer_and_kfc Customer_node
|
再另新建一个终端, 运行KFC节点
1
| ros2 run customer_and_kfc KFC_node
|
此时应该可以看见:
Customer端:

KFC端:

经验证, 需求全部实现~
相关工具
rqt_graph
使用rqt_graph
这个工具可以可视化显示节点和话题的连接关系
另起一个终端, 输入

上图清晰地展示了ROS计算图的网络形态, 可以清楚地看出一个节点的输入和输出是什么.
ros2 topic
查看系统中所有话题

想具体查看每个话题传输的数据类型, 则添加-t

输出实时话题内容
1
| ros2 topic echo /hamburger
|

查看主题信息
1
| ros2 topic info /hamburger
|

查看话题的数据类型
节点之间要想成功建立数据传输,必须发布和订阅同样数据类型的消息,发布者发布的是速度指令,订阅者想订阅位置信息可是行不通的。
上文用ros2 topic list -t
查看得知, /advertisement
的类型为std_msgs/msg/String
通过如下指令查看这个数据类型的具体数据结构
1
| ros2 interface show std_msgs/msg/String
|

可以看到, std_msgs/msg/String
中包含了string data
发布一个话题消息
1
| ros2 topic pub /test_topic std_msgs/msg/String 'data: "123"'
|

查看某一个话题的发布频率
1
| ros2 topic hz /hamburger
|
