目录
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订阅话题消息
目标
- 确认先启动订阅者节点,后启动发布者节点时的现象。
- 确认先启动发布者节点,后启动订阅者节点时的现象。
- 确认先后启动两个同名节点时的现象。
程序设计
- nodeA.cpp实现名为“NodeA”的节点,用于发布名为“daniel_topic_0”的话题消息,消息类型为std_msgs::String。
- nodeB.cpp实现名为“NodeB”的节点,用于订阅名为“daniel_topic_0”的话题消息,消息类型为std_msgs::String。
-
新建源文件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; }
编译
-
在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})
-
编译功能包
在工作空间根目录中使用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。
-
新开终端,运行节点B
使用rosrun
启动单个节点,使用方式rosrun your_pkg_name your_node_name
rosrun pkg_topic_example NodeB
-
新开终端,运行节点A
rosrun pkg_topic_example NodeA
现象: 节点A运行后,节点B的终端就会不停打印下面的信息:
结论: 先启动订阅者节点,再启动发布者节点,订阅者能接收到所有的话题消息(忽略数据异常丢失的情形)。
场景:先启动发布者节点,后启动订阅者节点
操作: 先启动发布者节点A,等待3s再启动订阅者节点B。
现象: 节点B的终端就会不停打印下面的信息:
结论: 先启动发布者节点,再启动订阅者节点,订阅者只能接收到自订阅时间点后的话题消息(忽略数据异常丢失的情形)。
场景:先后启动两个同名节点
操作: 启动发布者节点A后,先后在两个终端中启动订阅者节点B。
现象:
结论: 如果网络中先后启动了两个同名节点,则先启动的节点会被关闭,后启动的节点正常运行。
示例2:同一个话题下面承载不同的消息类型
目标
- 确认一个话题承载多种不同消息类型的可行性。
程序设计
- 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。
-
新建源文件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; }
编译
-
在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})
-
编译功能包
在工作空间根目录中使用catkin_make
,编译该工作空间中的所有功能包。
如果需要编译指定的某个功能包(譬如:target_pkg),
使用catkin_make -DCATKIN_WHITELIST_PACKAGES="target_pkg"
。cd catkin_ws catkin_make 或者 catkin_make -DCATKIN_WHITELIST_PACKAGES="pkg_topic_example"
测试
操作:
-
新开终端,运行主节点(如果主节点已运行,则跳过此步骤)
roscore
使用
roscore
运行主节点。
-
先后运行4个节点,运行顺序:NodeB、NodeD、NodeA、NodeC
现象:
结论: 通过话题通信的节点之间建立连接后,还会校验双方同一个话题的消息类型是否匹配,如果不匹配则断开连接,不能互相通信。
总结
- 先启动订阅者节点,再启动发布者节点,订阅者能接收到所有的话题消息(忽略数据异常丢失的情形)。
- 先启动发布者节点,再启动订阅者节点,订阅者只能接收到自订阅时间点后的话题消息(忽略数据异常丢失的情形)。
- 如果网络中先后启动了两个同名节点,则先启动的节点会被关闭,后启动的节点正常运行。
- 通过话题通信的节点之间建立连接后,还会校验双方同一个话题的消息类型是否匹配,如果不匹配则断开连接,不能互相通信。