写在前面的话
- 文档没有任何商业因素,本着共享的精神进行分享,如有素材侵权,请给我留言;
- 文档都是自己平时看书或工作中的笔记,观点错误的地方欢迎留言;
节点关系图:
我们尝试创建一个简单的话题通信的例子,节点创建一个Publisher 和 Topic,并向这个 Topic 发布字符串 “Hello World”;另一个节点创建一个 Listener,并订阅这个 Topic,打印出收到的消息;消息的类型,是基于系统提供的。
我们要创建一个如下的结构的计算图:
创建 base_communication
功能包
使用如下命令创建 base_communication
功能包:
$ cd ~/catkin_ws/src
$ catkin_create_pkg base_communication std_msgs rospy roscpp
创建 Publisher
创建 Publisher 的流程主要分为四步:
- 创始话节点;
- 向 Master 注册节点信息:包括要发布的话题名,消息类型,话题缓存大小;
- 按照一定的频率发布消息;
- 依次处理所有订阅者的回调函数;
源代码如下 base_communication\src\talker.cpp
:
#include <sstream>
#include "ros/ros.h"
#include "std_msgs/String.h"
int main(int argc, char **argv)
{
ros::init(argc, argv, "talker"); // ROS 节点初始化
ros::NodeHandle n; // 创建节点句柄
// 创建一个Publisher, 发布名称为 chatter的topic, 消息类型为 std_msgs::String
ros::Publisher chatter_pub = n.advertise<std_msgs::String>("chatter", 1000);
ros::Rate loop_rate(10); // 设置循环频率
int count = 0;
while(ros::ok()) {
// 初始化std_msg::String类型的消息
std_msgs::String msg;
std::stringstream ss;
ss << "hello world " << count;
msg.data = ss.str();
chatter_pub.publish(msg); // 发布消息
ROS_INFO("%s", msg.data.c_str());
ros::spinOnce(); // 逐个处理订阅者的回调函数
loop_rate.sleep(); // 按照循环频率延时
count++;
}
return 0;
}
代码分析:
- 头文件部分
#include "ros/ros.h" #include "std_msgs/String.h"
ros/ros.h
:包含了 ros 中要用到的绝大部分头文件;std_msgs/String.h
是由 String.msg 的消息结构定义文件自动生成;我们也可以自定义消息结构,并生成所需要的头文件;
- 初始化节点:
ros::init(argc, argv, "talker")
,这个函数用来初始化当前节点,参数含义如下:argc, argv
:命令行参数或者 launch 文件输入的参数,可以用来完成命名重映射等功能;talker
:定义了节点的名称,而且该名称必须是独一无二的;
- 创建 Publisher 和 发布主题
ros::NodeHandle n; ros::Publisher chatter_pub = n.advertise<std_msgs::String>("cahtter", 1000);
ros::NodeHandle n
:创建了一个节点句柄,用来管理节点资源;ros::Publisher chatter_pub = n.advertise<std_msgs::String>("chatter", 1000);
表示在 Master 中注册了一个 Publisher,并设置了信息类型 std_msgs::String,设置 Topic 名字 ”chatter“,设置消息队列的大小 1000 个;Publisher 发布的消息会依次存储在消息队列中,如果实际的消息数量超过了消息队列的大小,ROS 会自动删除最早入队的消息;
- 异常判断:
ros::ok()
:主要用来监测异常,如果出现异常就返回false
;这里的异常主要包括:- 收到 SIGINT 信号(Ctrl + C);
- 被另一个相同名称的节点踢掉线;
- 节点调用了关闭函数
ros::shutdown()
; - 所有
ros:: NodeHandles
句柄被销毁;
- 初始化消息
std_msgs::String msg; std::stringstream ss; ss << "hellow world " << count; msg.data = ss.str();
- 这里使用了最简单的消息类型 String,它只有一个成员,即 data;
- 发布消息
chatter_pub.publish(msg); ROS_INFO("%s", msg.data.c_str());
- 发布封装完毕的消息 msg。消息发布之后,Master 会查阅订阅该话题节点,帮助两个节点建立联系,完成消息传递;
ROS_INFO()
类似 C/C++ 中的 printf/cout 函数,用来打印日志消息;这里显示要发送的消息;
- 处理订阅者的回调函数:
ros::spinOnce()
- 该函数用来依次处理连接上此节点的所有订阅者的回调函数;
- 延时函数:
ros::Rate loop_rate(10); loop_rate.sleep();
ros::Rate loop_rate(10)
:设置了频率;loop_rate.sleep()
:按设置的频率进行休眠;
创建 Listener
创建 Subsriber 的流程主要分为四步:
- 初始化 ROS 节点;
- 向 Master 注册节点信息:订阅话题,指明话题名、消息类型,话题缓存大小;
- 按照一定的频率循环接收消息;
- 在回调函数中依次处理消息;
源码如下 base_communication\src\listener_1.cpp
和 base_communication\src\listener_2.cpp
,这里设置了两个节点,它们都订阅了上面设置的主题;
#include "ros/ros.h"
#include "std_msgs/String.h"
// 接收到订阅方的消息后,会进入消息回调函数
void chatterCallback(const std_msgs::String::ConstPtr& msg)
{
// 将接收到的消息打印出来
ROS_INFO("I heard: [%s], msg->data.c_str()");
}
int main(int argc, char **argv)
{
ros::init(argc, argv, "listener_1"); // 初始化ROS节点,另一个是 listener_2;
ros::NodeHandle n; // 创建节点
// 创建一个 Subscriber,订阅名为 chatter 的话题,注册回调函数 chatterCallback
ros::Subscriber sub = n.subscribe("chatter", 1000, chatterCallback);
ros::spin(); // 循环接收主题消息
return 0;
}
代码解析:
- 设置回调函数
void chatterCallback(const std_msgs::String::ConstPtr& msg) { // 将接收到的消息打印出来 ROS_INFO("I heard: [%s], msg->data.c_str()"); }
- 回调函数是订阅节点接收消息的基础机制,当有消息到达时会自动以消息指针作为参数,再调用回调函数,完成对消息内容的处理;
- 此回调函数完成对消息的打印操作;
- 订阅话题:
ros::NodeHandle n; // 创建节点 // 创建一个 Subscriber,订阅名为 chatter 的话题,注册回调函数 chatterCallback ros::Subscriber sub = n.subscribe("chatter", 1000, chatterCallback);
- 订阅节点订阅话题,指明话题名,消息队列大小,和回调函数;该操作会完成订阅节点在 Master 中的注册操作。Master 会关注系统中是否存在发布该话题的节点,如果存在会帮助两个节点建立,完成数据传输。
- 接收消息:
ros::spin()
:该代码会是节点进入循环状态,当有消息到达时,会尽快调用回调函数完成处理。ros::spin()
在ros::ok()
返回false
时退出。
修改 CMakeFile.txt 文件并编译
如果是用 C++ 编写的代码,则需要对其进行编译;如果是用 Python 编写的则不需要对其进行编译;
我们需要修改 base_communication/CMakeList.txt
中的部分配置。CMakeList.txt 中每一部分的设置都有详细的描述,我们只需要找到相关配置项,去掉注释并稍作修改。
相关的配置设置主要包含四个方面:
- include_directories:设置头文件的相对路径。
- 全局路径默认是功能包的所在目录,此处需要添加的是功能包下的 include 文件;
- ROS catkin 编译器默认包含的其他文件路径,比如 ROS 默认安装路径、Linux 系统路径等;
- add_executable:用于设置需要编译的代码和生成可执行文件:
- 第一个参数表示期望生成的可执行文件的名称;
- 第二个参数表示参与编译的源码,如果存在多个源码,依次列出,空格作为分隔;
- target_link_libraries:用于设置链接库;
- 很多功能需要使用到系统或第三方库函数,通过该选项可以配置执行文件链接的库文件;
- 第一个参数表示可执行文件的名称;
- 第二个参数表示需要链接的库,此处编译的 Publisher 和 Subscriber 没有使用其他库,添加默认链接库即可;
- add_dependencies:设置依赖项。
- 我们定义的是语言无关的消息类型,消息类型会在编译过程中产生相应语言的代码,如果编译的可执行代码文件依赖这些动态生成的代码,则需要使用 add_pendencies 添加
${PROJECT_NAME}_generate_message_cpp
配置,即该功能包动态产生消息代码; - 该编译规则也可以添加其他需要依赖的功能包;
- 我们定义的是语言无关的消息类型,消息类型会在编译过程中产生相应语言的代码,如果编译的可执行代码文件依赖这些动态生成的代码,则需要使用 add_pendencies 添加
include_directories(include ${catkin_INCLUDE_DIRS})
add_executable(talker src/talker.cpp)
target_link_libraries(talker ${catkin_LIBRARIES})
add_dependencies(talker ${PROJECT_NAME}_generate_messages_cpp)
add_executable(listener_1 src/listener.cpp)
target_link_libraries(listener_1 ${catkin_LIBRARIES})
add_executable(listener_2 src/listener.cpp)
target_link_libraries(listener_2 ${catkin_LIBRARIES})
使用如下命令会执行编译操作:
$ cd ~/catkin_ws
$ catkin_make
以上编译会产生三个可执行文件:talker、listener_1, listener_2;放置在目录 ~/catkin_ws/devel/lib/<packagename>
;
运行创建者和发布者
- 设置环境变量
$ cd ~/catkin_ws $ source ./devel/setup.bash // 或者 $ echo "source ~/catkin_ws/devel/setup.bash" >> ~/.bashrc $ source ~/.bashrc
- 启动 roscore:在运行节点之前要确保 ROS Master 已经成功启动;
$ roscore
- 启动 Publisher:
$ rosrun base_communication talker
; 结果如下:
- 启动 Subscribe_1:
$ rosrun base_communication listener_1
;结果如下:
- 启动 Subscribe_2:
$ rosrun base_communication listener_2
;结果如下: