ROS节点通信之话题

ROS计算图结构

在这里插入图片描述
ROS节点之间通过收发消息进行通信,消息收发机制分为话题(topic)、服务(service)和动作(action)三种。如上计算图中的节点2与节点3、节点2与节点5采用话题通信,节点2与节点4采用服务通信,节点1与节点2采用动作通信。计算图中的节点、话题、服务、动作都要有唯一名称作为标识。

ROS话题通信方式

话题通信方式是单向异步的,发布者只负责将消息发布到话题,订阅者只从话题订阅消息,发布者与订阅者之间并不需要事先确定对方的身份,话题充当消息存储容器的角色。这种机制很好地实现了发布者与订阅者程序之间的解耦。由于话题通信方式是单向的,即发布者并不能确定订阅者是否按时接收到消息,所以这种机制也是异步的。话题通信一般用在实时性要求不高的场景中,比如传感器广播其采集的数据。

编写程序测试话题通信方式

创建一个功能包,在此功能包中设计一些节点来测试话题通信方式。

创建功能包

假设:工作空间为catkin_ws,功能包为pkg_topic_example,依赖roscpp和std_msgs功能包。

cd catkin_ws/src
catkin_create_pkg pkg_topic_example roscpp std_msgs

关于创建功能包的更多信息,请参考《创建/删除ROS功能包

示例1:节点A发布话题消息,节点B订阅话题消息

目标

  1. 确认先启动订阅者节点,后启动发布者节点时的现象。
  2. 确认先启动发布者节点,后启动订阅者节点时的现象。
  3. 确认先后启动两个同名节点时的现象。

程序设计

  • nodeA.cpp实现名为“NodeA”的节点,用于发布名为“daniel_topic_0”的话题消息,消息类型为std_msgs::String。
  • nodeB.cpp实现名为“NodeB”的节点,用于订阅名为“daniel_topic_0”的话题消息,消息类型为std_msgs::String。
  1. 新建源文件nodeA.cpp nodeB.cpp

    cd catkin_ws/src/pkg_topic_example/src/
    touch nodeA.cpp nodeB.cpp
    
     nodeA.cpp
    
    /*
     * This node contains a publisher to publish message of topic "daniel_topic_0".
     * Message type is std_msgs::String.
     * Node name is "NodeA".
     * Topic name is "daniel_topic_0".
     */
     
     #include "ros/ros.h"
     #include "std_msgs/String.h"
     #include <sstream>
     
     #define NODE_NAME	"NodeA"
     #define TOPIC_NAME	"daniel_topic_0"
     
     int main(int argc, char* argv[])
     {
    	 ros::init(argc, argv, NODE_NAME);
    	 ros::NodeHandle nodeHandle;
    	 
    	 ros::Publisher publisher = nodeHandle.advertise<std_msgs::String>(TOPIC_NAME, 500);
    	 
    	 ros::Rate loopRate(1);
    	 
    	 int count = 0;
    	 
    	 while (ros::ok()) {
    		 std::stringstream ss;
    		 ss << "Here I come " << count++ << " times";
    		 
    		 std_msgs::String msg;
    		 msg.data = ss.str();
    		 
    		 publisher.publish(msg);
    		 
    		 ros::spinOnce();
    		 
    		 loopRate.sleep();
    	 }
    	 
    	 return 0;
     }
    
     nodeB.cpp
    
    /*
     * This node contains a subscriber to subscribe message of topic "daniel_topic_0".
     * Message type is std_msgs::String.
     * Node name is "NodeB".
     * Topic name is "daniel_topic_0".
     */
     
     #include "ros/ros.h"
     #include "std_msgs/String.h"
     
     #define NODE_NAME	"NodeB"
     #define TOPIC_NAME	"daniel_topic_0"
     
     void onRecvMsg(const std_msgs::String::ConstPtr& msg)
     {
    	 ROS_INFO("Publisher said:%s\n", msg->data.c_str());
     }
     
     int main(int argc, char* argv[])
     {
    	 ros::init(argc, argv, NODE_NAME);
    	 ros::NodeHandle nodeHandle;
    	 
    	 ros::Subscriber subscriber = nodeHandle.subscribe<std_msgs::String>(TOPIC_NAME, 1000, onRecvMsg);
    	 
    	 ros::spin();
    	 
    	 return 0;
    	 
     }
    

编译

  1. pkg_topic_example/CMakeLists.txt的末尾加上下面的内容:

    #node A
    add_executable(NodeA ./src/nodeA.cpp)
    target_link_libraries(NodeA ${catkin_LIBRARIES})
    
    #node B
    add_executable(NodeB ./src/nodeB.cpp)
    target_link_libraries(NodeB ${catkin_LIBRARIES})
    

    ![在这里插入图片描述](https://img-blog.csdnimg.cn/95ad6e97afa34c62a689c147fde7011a.png

  2. 编译功能包
    在工作空间根目录中使用catkin_make,编译该工作空间中的所有功能包。
    如果需要编译指定的某个功能包(譬如:target_pkg),
    使用catkin_make -DCATKIN_WHITELIST_PACKAGES="target_pkg"

    cd catkin_ws
    catkin_make 或者 catkin_make -DCATKIN_WHITELIST_PACKAGES="pkg_topic_example"
    

    在这里插入图片描述

测试

新开终端,运行主节点(如果主节点已运行,则跳过此步骤)
bash roscore
使用roscore运行主节点。
在这里插入图片描述

场景:先启动订阅者节点,后启动发布者节点

操作: 先启动订阅者节点B,后启动发布者节点A。

  1. 新开终端,运行节点B
    使用rosrun启动单个节点,使用方式rosrun your_pkg_name your_node_name

    rosrun pkg_topic_example NodeB
    

    在这里插入图片描述

  2. 新开终端,运行节点A

    rosrun pkg_topic_example NodeA
    

    在这里插入图片描述

现象: 节点A运行后,节点B的终端就会不停打印下面的信息:
在这里插入图片描述
结论: 先启动订阅者节点,再启动发布者节点,订阅者能接收到所有的话题消息(忽略数据异常丢失的情形)。

场景:先启动发布者节点,后启动订阅者节点

操作: 先启动发布者节点A,等待3s再启动订阅者节点B。
现象: 节点B的终端就会不停打印下面的信息:
在这里插入图片描述
结论: 先启动发布者节点,再启动订阅者节点,订阅者只能接收到自订阅时间点后的话题消息(忽略数据异常丢失的情形)。

场景:先后启动两个同名节点

操作: 启动发布者节点A后,先后在两个终端中启动订阅者节点B。

现象:
在这里插入图片描述

结论: 如果网络中先后启动了两个同名节点,则先启动的节点会被关闭,后启动的节点正常运行。

示例2:同一个话题下面承载不同的消息类型

目标

  1. 确认一个话题承载多种不同消息类型的可行性。

程序设计

  • nodeA.cpp实现名为“NodeA”的节点,用于发布名为“daniel_topic_0”的话题消息,消息类型为std_msgs::String。
  • nodeB.cpp实现名为“NodeB”的节点,用于订阅名为“daniel_topic_0”的话题消息,消息类型为std_msgs::String。
  • nodeC.cpp实现名为“NodeC”的节点,用于发布名为“daniel_topic_0”的话题消息,消息类型为std_msgs::Int32。
  • nodeD.cpp实现名为“NodeD”的节点,用于订阅名为“daniel_topic_0”的话题消息,消息类型为std_msgs::Int32。
  1. 新建源文件nodeA.cpp nodeB.cpp nodeC.cpp nodeD.cpp

    cd catkin_ws/src/pkg_topic_example/src
    touch nodeA.cpp nodeB.cpp nodeD.cpp nodeD.cpp
    
     nodeA.cpp
    
    /*
     * This node contains a publisher to publish message of topic "daniel_topic_0".
     * Message type is std_msgs::String.
     * Node name is "NodeA".
     * Topic name is "daniel_topic_0".
     */
     
     #include "ros/ros.h"
     #include "std_msgs/String.h"
     #include <sstream>
     
     #define NODE_NAME	"NodeA"
     #define TOPIC_NAME	"daniel_topic_0"
     
     int main(int argc, char* argv[])
     {
    	 ros::init(argc, argv, NODE_NAME);
    	 ros::NodeHandle nodeHandle;
    	 
    	 ros::Publisher publisher = nodeHandle.advertise<std_msgs::String>(TOPIC_NAME, 500);
    	 
    	 ros::Rate loopRate(1);
    	 
    	 int count = 0;
    	 
    	 while (ros::ok()) {
    		 std::stringstream ss;
    		 ss << "Here I come " << count++ << " times";
    		 
    		 std_msgs::String msg;
    		 msg.data = ss.str();
    		 
    		 publisher.publish(msg);
    		 
    		 ros::spinOnce();
    		 
    		 loopRate.sleep();
    	 }
    	 
    	 return 0;
     }
    
     nodeB.cpp
    
    /*
     * This node contains a subscriber to subscribe message of topic "daniel_topic_0".
     * Message type is std_msgs::String.
     * Node name is "NodeB".
     * Topic name is "daniel_topic_0".
     */
     
     #include "ros/ros.h"
     #include "std_msgs/String.h"
     
     #define NODE_NAME	"NodeB"
     #define TOPIC_NAME	"daniel_topic_0"
     
     void onRecvMsg(const std_msgs::String::ConstPtr& msg)
     {
    	 ROS_INFO("Publisher said:%s\n", msg->data.c_str());
     }
     
     int main(int argc, char* argv[])
     {
    	 ros::init(argc, argv, NODE_NAME);
    	 ros::NodeHandle nodeHandle;
    	 
    	 ros::Subscriber subscriber = nodeHandle.subscribe<std_msgs::String>(TOPIC_NAME, 1000, onRecvMsg);
    	 
    	 ros::spin();
    	 
    	 return 0;
    	 
     }
    
     nodeC.cpp
    
    /*
     * This node contains a publisher to publish message of topic "daniel_topic_0".
     * Message type is std_msgs::Int32.
     * Node name is "NodeC".
     * Topic name is "daniel_topic_0".
     */
     
     #include "ros/ros.h"
     #include "std_msgs/Int32.h"
     #include <sstream>
     
     #define NODE_NAME	"NodeC"
     #define TOPIC_NAME	"daniel_topic_0"
     
     int main(int argc, char* argv[])
     {
    	 ros::init(argc, argv, NODE_NAME);
    	 ros::NodeHandle nodeHandle;
    	 
    	 ros::Publisher publisher = nodeHandle.advertise<std_msgs::Int32>(TOPIC_NAME, 500);
    	 
    	 ros::Rate loopRate(1);
    	 
    	 int count = 0;
    	 
    	 while (ros::ok()) {
    		 std_msgs::Int32 msg;
    		 msg.data = count++;
    		 
    		 publisher.publish(msg);
    		 
    		 ros::spinOnce();
    		 
    		 loopRate.sleep();
    	 }
    	 
    	 return 0;
     }
    
     nodeD.cpp
    
    /*
     * This node contains a subscriber to subscribe message of topic "daniel_topic_0".
     * Message type is std_msgs::Int32.
     * Node name is "NodeD".
     * Topic name is "daniel_topic_0".
     */
     
     #include "ros/ros.h"
     #include "std_msgs/Int32.h"
     
     #define NODE_NAME	"NodeD"
     #define TOPIC_NAME	"daniel_topic_0"
     
     void onRecvMsg(const std_msgs::Int32::ConstPtr& msg)
     {
    	 ROS_INFO("Publisher said:%d\n", msg->data);
     }
     
     int main(int argc, char* argv[])
     {
    	 ros::init(argc, argv, NODE_NAME);
    	 ros::NodeHandle nodeHandle;
    	 
    	 ros::Subscriber subscriber = nodeHandle.subscribe<std_msgs::Int32>(TOPIC_NAME, 1000, onRecvMsg);
    	 
    	 ros::spin();
    	 
    	 return 0;
    	 
     }
    

编译

  1. pkg_topic_example/CMakeLists.txt的末尾加上下面的内容:

    #node A
    add_executable(NodeA ./src/nodeA.cpp)
    target_link_libraries(NodeA ${catkin_LIBRARIES})
    
    #node B
    add_executable(NodeB ./src/nodeB.cpp)
    target_link_libraries(NodeB ${catkin_LIBRARIES})
    
    #node C
    add_executable(NodeC ./src/nodeC.cpp)
    target_link_libraries(NodeC ${catkin_LIBRARIES})
    
    #node D
    add_executable(NodeD ./src/nodeD.cpp)
    target_link_libraries(NodeD ${catkin_LIBRARIES})
    

    在这里插入图片描述

  2. 编译功能包
    在工作空间根目录中使用catkin_make,编译该工作空间中的所有功能包。
    如果需要编译指定的某个功能包(譬如:target_pkg),
    使用catkin_make -DCATKIN_WHITELIST_PACKAGES="target_pkg"

    cd catkin_ws
    catkin_make 或者 catkin_make -DCATKIN_WHITELIST_PACKAGES="pkg_topic_example"
    

    在这里插入图片描述

测试

操作:

  1. 新开终端,运行主节点(如果主节点已运行,则跳过此步骤)

    roscore
    

    使用roscore运行主节点。
    在这里插入图片描述

  2. 先后运行4个节点,运行顺序:NodeB、NodeD、NodeA、NodeC

现象:
在这里插入图片描述
结论: 通过话题通信的节点之间建立连接后,还会校验双方同一个话题的消息类型是否匹配,如果不匹配则断开连接,不能互相通信。

总结

  1. 先启动订阅者节点,再启动发布者节点,订阅者能接收到所有的话题消息(忽略数据异常丢失的情形)。
  2. 先启动发布者节点,再启动订阅者节点,订阅者只能接收到自订阅时间点后的话题消息(忽略数据异常丢失的情形)。
  3. 如果网络中先后启动了两个同名节点,则先启动的节点会被关闭,后启动的节点正常运行。
  4. 通过话题通信的节点之间建立连接后,还会校验双方同一个话题的消息类型是否匹配,如果不匹配则断开连接,不能互相通信。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值