【ROS-Topic话题与Message消息——C++实现篇】

提示:本文基于Ubuntu20.04和ROS Noetic版本,版本不同可能会导致部分差异,敬请谅解。


前言

本期学习ROS中最常用的节点通信方式——Topic话题与Message消息


节点不能脱离软件包而独立存在;
建立两个软件包,使两个软件包的两个节点之间建立通信;
ssr_pkg (sensor package)中有个Ultrasound_node节点;
atr_pkg (actual package)中有个Motor_node节点;

节点间的通讯双方的定义:
发送信息的一方称Publisher——发布者;
发布者发布的话题称Topic——主题;
发布的消息称Message——消息;
接收信息的一方称Subsciber——定阅者;

一、Topic

1.话题Topic是节点间进行持续通信(消息不停地发送与接收)的一种形式;
2.话题通讯的两个节点是通过话题的名称建立起话题通讯连接;
3.消息Message通常会按照一定的频率持续不断地发送以保证消息数据的实时性;
4.对于多节点间通讯:
假如在ssr_pkg (sensor package)中额外添加IMU_node节点;

方式一:
每个Publisher节点会发布各自的消息到各自不同的Topic里,而不是在同一个Topic里发送所有Message,Subsciber会根据自己的需要订阅不同的话题;

方式二:
不同的Publisher节点在同一个话题里发布消息,Subsciber节点订阅这个话题;
注:在这种通讯方式中,同一时刻只能有一个发送信息的Publisher节点

5.一个ROS节点网络中可以存在多个话题(对应上述4中的方式一);一个话题可以有多个发布者(Publisher)和多个订阅者;任何一个节点可以对多个话题进行订阅,也可以发布多个话题;

6.不同的传感器消息通常只会拥有各自独立的话题名称,每个话题只有一个发布者;

二、Message

消息的作用是携带数据,数据从发布者传输到订阅者;

在生成消息包的时候通常要指定消息的类型;

前面用到的catkin_create_pkg ssr_pkg rospy roscpp std_msgs
其中std_msgs的消息类型是如何查看的?
index.ros.org
搜索栏输入std_msgs,可以查询到不同版本ROS对应的std_msgs的定义;
可以查到Bool、Byte、ByteMUltiArray(字节数组)
在这里插入图片描述

三、使用C++实现Publisher发布者节点实例

1.创建Publisher发布者节点

(不喜欢原理解释部分可以直接跳转到最终代码)
发布者Publisher节点
ssr_pkg (sensor package)中有个Ultrasound_node节点;
atr_pkg (actual package)中有个Motor_node节点;
订阅者Subsciber节点

本节目标:在Ultrasound_node节点的框架里添加代码,将其打造成一个话题发布者;
确定发布的话题名称;
确定发布的消息的类型;

确认消息类型:
打开index.ros.org搜索std_msgs,点击查看可以找到String字符串类型;

在节点初始化完成后定义一个NodeHandle对象;
这个对象是节点和ROS通讯的关键;

ros::NodeHandle nh;//在节点初始化完成后定义一个NodeHandle对象

定义一个Publisher对象,这个Publisher对象是我们发送消息的工具;
需要命名话题的名称、缓存的长度;
缓存长度表示:
订阅者处理数据的周期比较长,如果消息没有处理完而新的消息发送过来了,数据就会按发送次序排队等待处理,当缓存队列满了,最早发送过来的未被处理的数据就会被舍弃,通俗点讲缓存长度决定了缓存中可以容纳多少个消息包排队;
缓存长度既不能太大,也不能太小,需要根据延迟来进行计算;

ros::Publisher pub =nh.advertise<std_msgs::String >(“话题名称”,缓存长度);//构建消息发送对象
在本项目中改动如下

ros::Publisher pub = nh.advertise<std_msgs::String>("sensor_topic",10);//该节点向NodeHandle发送一个请求

#由于String类型的头文件还没有包含进来,会报错;
需要加入头文件:
#include <std_msgs/String.h>
按Table键可以补全代码;

接下来生成一个消息包:
消息类型是std_msgs::String
对象名称取为: msg;

给上述msg数据包的变量赋值
msg.data = “Ultrasound_data”;//给变量赋值

将数据包发送出去
pub.publish(msg);//将msg发布出去

最终程序示例:

#include <ros/ros.h>
#include <std_msgs/String.h>


int main(int argc,char *argv[])
{
    
    
    ros::init(argc,argv,"Ultrasound_node");//初始化节点
    printf("Hello world!\n");

    ros::NodeHandle nh;//在节点初始化完成后定义一个NodeHandle对象
    ros::Publisher pub = nh.advertise<std_msgs::String>("sensor_topic",10);//该节点向NodeHandle发送一个请求
    while(ros::ok())
    {
    
    
        printf("Ultrasound_node is working!\n");
        std_msgs::String msg;//生成一个消息包 
        msg.data = "Ultrasound_data";//给变量赋值
        pub.publish(msg);//将msg发布出去
    }
    return 0;
}

编译CTR+SHIFT+B
CTRL+ALT+T启动终端窗口
在终端窗口运行ros,输入roscore
新建终端窗口,运行rosrun ssr_pkg Ultrasound_node,这里rossrun 包名 节点名,包和节点的名称取决于你的工程。
运行结果如下:
按CTRL+C可以终止程序运行
在这里插入图片描述

2.rostopic的使用

查看ros中活跃的话题有哪些:
重新运行刚刚终止的节点程序rosrun ssr_pkg Ultrasound_node
CTRL+SHIFT+O分割出新的终端(如果安装了Terminar程序);
rostopic list
运行结果描述:可以看到sensor_topic(程序中自定义的话题)
在这里插入图片描述
查看话题中的消息
输入rostopic echo 话题名称(输入首字母和Tab键自动补全)
rostopic echo sensor_topic
在该窗口点击一下,按CTRL+C暂停方便观察;(没显示就是因为刷新太快了,暂停即可)
可以看到topic中的信息为data: “Ultrasound_data”,其中Ultrasound_data为前文写入的信息;

在这里插入图片描述
如果前面写入的是中文信息打印错误,需要将data:后面的字段"中文"复制下来;
CTRL+SHIFT+O打开新终端;
输入==echo -e “中文”==即可正确解码为汉字信息;

使用rostopic查看话题里消息发送的频率;
CTRL+SHIFT+W关闭当前一个终端;
rostopic hz /话题名称 #可以得到消息发送的频率n次/秒;
在这里插入图片描述

修改信息发送的频率
ros::Rate Ultrasound_loop_rate(10);//生成一个频率对象,每秒执行十次;
Utrasound_loop_rate.sleep();//一段时间的阻塞,可以放到主函数的while循环中,使得循环执行频率能和上述参数对应;

#include <ros/ros.h>
#include <std_msgs/String.h>


int main(int argc,char *argv[])
{
    
    
    ros::init(argc,argv,"Ultrasound_node");//初始化节点
    printf("Hello world!\n");

    ros::NodeHandle nh;//在节点初始化完成后定义一个NodeHandle对象
    ros::Publisher pub = nh.advertise<std_msgs::String>("sensor_topic",10);//该节点向NodeHandle发送一个请求
    ros::Rate Ultrasound_loop_rate(10);//生成一个频率对象,每秒钟执行10次
    while(ros::ok())
    {
    
    
        printf("Ultrasound_node is working!\n");
        std_msgs::String msg;//生成一个消息包 
        msg.data = "Ultrasound_data";//给变量赋值
        pub.publish(msg);//将msg发布出去
        Ultrasound_loop_rate.sleep();//一段时间的阻塞,使得循环执行频率能和上述参数对应;
    }
    return 0;
}

在这里插入图片描述

3.复制当前节点并修改成新的节点

1.在vscode工程中复制之前创建的Ultrasound.cpp并右键当前的src目录进行粘贴,将文件名称修改为DHT11_node.cpp;
修改节点名称(必选)

ros::init(argc,argv,"DHT11_node");//初始化节点

2.修改话题名称(可选,前面讲过多个节点可以发布同一个话题)

ros::Publisher pub = nh.advertise<std_msgs::String>("sensor_topic",10);//该节点向NodeHandle发送一个请求

3.修改CMakeLists.txt文件,添加下列语句到文件末尾(和Ultrasound.cpp的创建步骤一样)

add_executable(DHT11_node src/DHT11_node.cpp)

target_link_libraries(DHT11_node
  ${
    
    catkin_LIBRARIES}
)

DHT11_node.cpp文件修改结果如下:

#include <ros/ros.h>
#include <std_msgs/String.h>


int main(int argc,char *argv[])
{
    
    
    ros::init(argc,argv,"DHT11_node");//初始化节点
    printf("This is DHT11_node\n");

    ros::NodeHandle nh;//在节点初始化完成后定义一个NodeHandle对象
    ros::Publisher pub = nh.advertise<std_msgs::String>("sensor_topic",10);//该节点向NodeHandle发送一个请求
    ros::Rate loop_rate(10);//生成一个频率对象,每秒钟执行10次
    while(ros::ok())
    {
    
    
        printf("DHT11_node is working!\n");
        std_msgs::String msg;//生成一个消息包 
        msg.data = "DHT11_data";//给变量赋值
        pub.publish(msg);//将msg发布出去
        loop_rate.sleep();//一段时间的阻塞,使得循环执行频率能和上述参数对应;
    }
    return 0;
}

编译CTR+SHIFT+B
CTRL+ALT+T启动终端窗口
在终端窗口运行ros,输入roscore
CTRL+SHIFT+O 新建终端窗口
rosrun ssr_pkg DHT11_node 运行DHT11节点
CTRL+SHIFT+O 新建终端窗口
rostopic list 查看消息列表,可以看到如下信息:
/rosout
/rosout_agg
/sensor_topic
查看消息内容
rostopic echo /sensor_topic

运行结果如下:
在这里插入图片描述查看消息发送频率
rostopic hz /sensor_topic

在这里插入图片描述

4.本章小节

使用C++实现了Publisher发布者节点实例

熟练掌握如下命令:

列出当前系统中所有活跃着的话题
rostopic list

显示指定话题中发送的消息包内容
rostopic echo /话题名称
示例:rostopic echo /sensor_topic

统计指定话题中消息发送的频率
rostopic hz /话题名称
示例:
rostopic hz /sensor_topic

四、使用C++实现Subscrible订阅者节点实例

我们已经编写了两个发布者节点(Ultrasound_node和DHT11_node),现在来添加订阅者节点;

index.ros.org
搜索std_msgs,进入noetic版本
在(Noetic版本的)std_msgs界面点击Website;
翻到ROS Message Types查找类型名称;

1.创建一个包:

打开工作空间目录
cd catkin_ws/src/

创建一个包
catkin_create_pkg<包名称><依赖项列表>
catkin_create_pkg atr_pkg rospy roscpp std_msgs
前两项三对python和c++的支持,后面两项是所需的String消息类型所在的包;
在这里插入图片描述

2.创建一个节点(motor_node.cpp文件)

编写必要的文件内容

#include <ros/ros.h>
#include <std_msgs/String.h>

/*
函数功能描述:回调函数,对消息包到来时的一个响应;
每当一个新的消息到达订阅者节点ROS系统就会在后台调用这个回调函数;

参数即是需要接收的消息,程序中表述为最新获取的消息包对象

*/
void motor_callback(std_msgs::String msg)
{
    
    
     printf(msg.data.c_str());//从strng类型转化成字符数组
     printf("\n"); 
}

int main(int argc,char *argv[])
{
    
    
    ros::init(argc,argv,"motor_node");//初始化节点
    printf("This is motor_node\n");

    ros::NodeHandle nh;//在节点初始化完成后定义一个NodeHandle对象
    ros::Subscriber sub = nh.subscribe("sensor_topic",10,motor_callback);//设置订阅的话题、消息缓存长度、回调函数名

    //ros::Rate loop_rate(10);//生成一个频率对象,每秒钟执行10次
    while(ros::ok())
    {
    
    
        //printf("motor_node is working!\n");
        ros::spinOnce();
        //loop_rate.sleep();//一段时间的阻塞,使得循环执行频率能和上述参数对应;
    }
    return 0;
}

3.补充说明motor_node.cpp的解释

头文件声明前面已经讲过了;

回调函数的解释
/*
函数功能描述:回调函数,对消息包到来时的一个响应;
每当一个新的消息到达订阅者节点ROS系统就会在后台调用这个回调函数;

参数即是需要接收的消息,程序中表述为最新获取的消息包对象

*/
motor callback(std_msgs::String msg)
{
printf(msg.data.c_str());//从strng类型转化成字符数组
printf(“\n”);
}

while循环体中的ros::spinOnce();函数:
当节点在忙自己的事情的时候,当调用这个sinOnce()函数的时候,就会转过身去看看有没有新的消息 包接受到;
如果不调用这个函数那么节点就会在while()循环忙自己的事情,从而不响应新到 来的数据包;

4.CMakeLists.txt中添加必要的指令(生成可执行函数、将节点链接到ros库函数)

CMakeLists.txt中添加如下指令

add_executable(motor_node src/motor_node.cpp)

target_link_libraries(motor_node
${catkin_LIBRARIES}
)

5.运行ros、ssr_pkg中的Ultrasound_node发布者节点、atr_pkg中的motor_node订阅者节点

启动ros
CTRL+SHIFT+O
rocore

启动ssr_pkg中的Ultrasound_node发布者节点
CTRL+SHIFT+O
rosrun ssr_pkg Ultrasound_node

启动atr_pkg中的motor_node订阅者节点
CTRL+SHIFT+O
rosrun atr_pkg motor_node

查看消息列表
rostopic list
以下是消息列表内容
/rosout
/rosout_agg
/sensor_topic

查看sensor_topic话题中的消息
rostopic echo /sensor_topic

实验现象(四个窗口依次是ros启动界面、Ultrasound_node、motor_node、话题查看窗口)
atr_pkg中的motor_node订阅者节点接受并发送出ssr_pkg中的Ultrasound_node发布者节点中的内容;
在这里插入图片描述

熟练掌握快捷键的使用

6.使用rqt_graph查看节点间通信

运行ros
CTRL+SHIFT+O
rocore

启动atr_pkg中的motor_node订阅者节点
CTRL+SHIFT+O
rosrun atr_pkg motor_node

启动ssr_pkg中的Ultrasound_node发布者节点
CTRL+SHIFT+O
rosrun ssr_pkg Ultrasound_node

启动ssr_pkg中的DHT11_node发布者节点
CTRL+SHIFT+O
rosrun ssr_pkg DHT11_node

CTRL+SHIFT+O
rqt_graph
运行结果如下
在这里插入图片描述在这里插入图片描述这里可以看到Ultrasound_node和DHT11_node两个发布者节点各自发布了名为sensor_topic的话题,订阅者motor_node订阅了sensor_topic的话题;

话题并不独属于发布者和订阅者,只要有节点向NodeHandle提出话题发布订阅请求、话题发布请求,话题就会存在,哪怕只有单一的通信节点存在,话题就会自动产生;

7.本章小节

话题的订阅步骤
1.确定话题的名称和消息类型;
2.在代码文件中添加ros和消息类型的头文件;
3.在main函数中通过NodeHandler订阅一个话题并设置消息回调函数;
4.定义一个回调函数,对接收的消息包进行处理;
5.在main函数中执行ros::spinOnce(),让回调函数能够响应接收到的消息包;

五、拓展篇——使用launch一次性运行多个节点

上个章节中我们要运行ros和另外三个节点需要输入四条指令才能完成操作,如果是面对一个多节点组成的复杂通信网络岂不是要输入很多指令,这时我们可以用luanch指令来完成这个操作;

在任意一个工程包中新建luanch文件夹
在launch文件夹下新建contrl.launch文件
编写contrl.launch中文件内容;


<launch>

    <node pkg="ssr_pkg" type="Ultrasound_node" name="Ulrasound_node"/>
    <node pkg="ssr_pkg" type="DHT11_node" name="DHT11_node"/>

    <node pkg="atr_pkg" type="motor_node" name="motor_node" output="screen"/>


</launch>

在终端窗口运行
roslaunch atr_pkg contrl.launch

CTRL+C终止
可以看到结束了四个节点,查看运行窗口可以发现ros也是一个节点,但是这个节点不需要手动添加到launch文件中;
在这里插入图片描述
使用ros_rqt查看节点间的通信方式如下
在这里插入图片描述


总结

简单介绍了ROS中最常用的节点通信方式——Topic话题与Message消息;
使用C++实现Publisher实现了简单的发布者节点实例;
使用C++实现Subscrible实现了简单的订阅者节点实例;
话题并不独属于发布者和订阅者,只要有节点向NodeHandle提出话题发布订阅请求、话题发布请求,话题就会存在,哪怕只有单一的通信节点存在,话题就会自动产生;

猜你喜欢

转载自blog.csdn.net/weixin_46187287/article/details/143197168