ROS通信机制(一) —— 话题(topic)与msg文件

文章目录

简述

话题消息通信是指发送信息的发布者和接收信息的订阅者以话题消息的形式发送和接收信息。希望接收话题的订阅者节点接收的是与在主节点中注册的话题名称对应的发布者节点的信息。基于这个信息,订阅者节点直接连接到发布者节点来发送和接收消息。

特点

话题是单向的,适用于需要连续发送消息的传感器数据,因为它们通过一次的连接连续发送和接收消息。另外,单个发布者可以与多个订阅者进行通信,相反,一个订阅者可以在单个话题上与多个发布者进行通信。

相关常用命令

命令 详细说明
rostopic list 显示活动的话题目录
rostopic echo [话题名称] 实时显示指定话题的消息内容
rostopic find [类型名称] 显示使用指定类型的消息的话题
rostopic type [话题名称] 显示指定话题的消息类型
rostopic bw [话题名称] 显示指定话题的消息带宽(bandwidth)
rostopic hz [话题名称] 显示指定话题的消息数据发布周期
rostopic info [话题名称] 显示指定话题的信息
rostopic pub [话题名称] [消息类型] [参数] 用指定的话题名称发布消息

通信模型

ROS通信机制(一) —— 话题(topic)与msg文件
PS:参考原文

核心元素

  1. ROS Master(管理者)
    必须首先被运行,并使用XMLRPC服务器,负责管理节点之间的消息通信中的连接信息。
  2. Talker (发布者)
    生成指定的话题,将TCPROS消息发布给所有的订阅者。
  3. Listener (订阅者)
    订阅指定的话题,从发布者那里接收指定的TCPROS的消息。

PS: XMLRPC(XML-Remote Procedure Call)是一种RPC协议,其编码形式采用XML编码格式,而传输方式采用既不保持连接状态、也不检查连接状态的请求和响应方式的HTTP协议。XMLRPC是一个非常简单的约定,仅用于定义小数据类型或命令,所以它比较简单。有了这个特点,XMLRPC非常轻便,支持多种编程语言,因此非常适合支持各种硬件和语言的ROS。

通信过程

  1. 运行主节点
    主节点(master)负责节点到节点的连接和消息通信,类似于名称服务器(Name Server)。roscore是它的运行命令,当运行主节点时,可以注册每个节点的名字,并根据需要获取信息。没有主节点,就不能在节点之间建立访问和消息交流(如话题和服务)。主节点使用XML远程过程调用(XMLRPC,XML-Remote Procedure Call)与节点进行通信。
  2. 运行发布者节点
    发布者节点向主节点注册发布者节点名称、话题名称、消息类型、URI地址和端口。
  3. 运行订阅者节点
    订阅者节点在运行时向主节点注册其订阅者节点名称、话题名称、消息类型、URI地址和端口。
  4. 主节点向订阅者节点发送发布者节点信息
    主节点向订阅者节点发送此订阅者希望访问的发布者的名称、话题名称、消息类型、URI地址和端口等信息。
  5. 订阅者节点向发布者节点发送连接请求
    订阅者节点根据从主节点接收的发布者信息,向发布者节点请求直接连接。在这种情况下,要发送的信息包括订阅者节点名称、话题名称和消息类型。
  6. 发布者节点进行连接响应
    发布者节点将TCP服务器的URI地址和端口作为连接响应发送给订阅者节点。
  7. 发布者节点和订阅者节点建立连接
    订阅者节点使用TCPROS创建一个与发布者节点对应的客户端,并直接与发布者节点连接。发布者和订阅者建立连接后,可以直接发送消息,不再通过ROS Master,所以,此时即使关闭掉ROS Master也不影响两者的通信。
  8. 发送消息
    发布者节点向订阅者节点发送消息。节点间通信使用一种称为TCPROS的TCP/IP方式。

代码示例(发布者和订阅者)

需求描述:编写发布者和订阅者文件,发布者以10HZ(每秒10次)的频率发布文本消息,订阅者订阅到消息后打印出来。

首先,这里创建了一个topic_test的包,然后分别创建talker.cpp(发布者)和listener(订阅者)两个文件。
ROS通信机制(一) —— 话题(topic)与msg文件

发布者(talker.cpp)

#include "ros/ros.h"
#include "std_msgs/UInt32.h"

int main(int argc, char **argv)
{
    // 设置编码
    setlocale(LC_ALL, "");

    // 1.初始化ROS节点
    // 参数3为节点名称,全局唯一
    ros::init(argc, argv, "talker");

    // 2.实例化ROS句柄
    ros::NodeHandle nh;

    // 3.实例化发布者对象
    // 参数1为数据要发布到的话题名称,参数2为缓冲区大小
    ros::Publisher pub = nh.advertise<std_msgs::UInt32>("chatter", 1000);

    // 4.定义要发布的数据
    int count = 0;
    std_msgs::UInt32 msg;

    // 5.定义数据发布的频率为每秒10次
    ros::Rate loop_rate(10);

    // ros::ok()表示只要节点还在运行即满足循环条件
    while (ros::ok())
    {
        msg.data = count;

        // 6.发布消息
        pub.publish(msg);

        // 打印发送的信息
        ROS_INFO("发送的消息是:%d", msg.data);

        // 处理ROS消息回调,只循环一次
        // 这里并没有接收到任何回调,所以这一句可以不写
        ros::spinOnce();

        count++;
        loop_rate.sleep();
    }
    
    return 0;
}

订阅者(listener.cpp)

#include "ros/ros.h"
#include "std_msgs/UInt32.h"

void chatterCallback(const std_msgs::UInt32::ConstPtr& msg)
{
    // 4.处理订阅的消息
    ROS_INFO("订阅到了消息: [%d]", msg->data);
}

int main(int argc, char **argv)
{
    // 设置编码
    setlocale(LC_ALL, "");

    // 1.初始化ROS节点
    // 参数3为节点名称,全局唯一
    ros::init(argc, argv, "listener");

    // 2.实例化ROS句柄
    ros::NodeHandle nh;

    // 3.实例化订阅者者对象
    // 参数1为要订阅的话题名称,参数2为缓冲区大小,参数3为处理订阅消息的回调函数
    ros::Subscriber sub = nh.subscribe<std_msgs::UInt32>("chatter", 1000, chatterCallback);

    // 处理ROS消息回调,一直循环,直到ROS节点退出(执行了Ctrl+C或者ros::shutdown()被调用)
    ros::spin();
    
    return 0;
}


配置 CMakeLists.txt

注意:这里的CMakeLists.txt指的是topic_test包下面的,而非整个工程下面的那个,在CMakeLists.txt文件中添加下面的内容。

# 节点构建选项,配置可执行文件
add_executable(talker src/talker.cpp)
add_executable(listener src/listener.cpp)

# 节点构建选项,配置目标链接库
target_link_libraries(talker
  ${catkin_LIBRARIES}
)
target_link_libraries(listener
  ${catkin_LIBRARIES}
)

编译和运行

使用Ctrl+Shift+B进行编译,然后使用roscore命令启动主节点,然后source下环境变量,分别运行发布者和订阅者节点即可看到通信数据的打印输出。
ROS通信机制(一) —— 话题(topic)与msg文件

问题扩展

上面先运行了发布者节点,从0开始打印信息,然后运行订阅者节点,信息直接从196开始打印了,订阅者运行之前发布者发布的信息就接收不到了。

尝试先运行订阅者节点,然后运行发布者节点,仍然存在信息丢失的情况。原因是在发布者开始发布消息时,其还未在主节点注册完成。
ROS通信机制(一) —— 话题(topic)与msg文件
可以在talker.cpp中添加延时来延迟第一条数据的发送时间。

ros::Duration(1.0).sleep(); 

在延时等待注册完成后再发送消息,就可以收到完整的消息了。
ROS通信机制(一) —— 话题(topic)与msg文件


自定义消息

ROS节点通过消息相互通信,ROS提供了一些标准的数据类型,也提供了一种基于标准消息类型开发自定义消息类型的机制。上面的示例中我们使用的是ROS标准的消息,实际使用中常常需要自定义消息来满足需求。

ROS的消息类型可以在这里查看。

需求描述:创建一个关于学生信息的msg文件,其中包含姓名、年龄和成绩,发布者发送学生信息,订阅者打印学生信息。

自定义msg文件

首先,在topic_test包中创建msg文件夹,然后,在文件夹中创建消息文件(Student.msg)。

string name
uint8 age
float32 score

ROS通信机制(一) —— 话题(topic)与msg文件

编辑配置文件

  • 在package.xml中添加编译依赖与执行依赖
<!-- 编译时依赖 -->
<build_depend>message_generation</build_depend>
<!-- 运行时依赖 -->
<exec_depend>message_runtime</exec_depend>
  • 在CMakeLists.txt编辑 msg 相关配置
# catkin构建时依赖的组件包,前3个创建ROS包时已经自动生成了,这里添加了message_generation
find_package(catkin REQUIRED COMPONENTS
  roscpp
  rospy
  std_msgs
  message_generation
)

# 配置msg源文件,FILES将引用当前功能包目录的msg目录中的*.msg文件,自动生成一个头文件(*.h)
add_message_files(
  FILES
  Student.msg
)

# 生成消息时依赖于std_msgs
generate_messages(
  DEPENDENCIES
  std_msgs
)

# 运行时依赖,描述了库、catkin构建依赖项和系统依赖的功能包
catkin_package(
#  INCLUDE_DIRS include
#  LIBRARIES topic_test
 CATKIN_DEPENDS roscpp rospy std_msgs message_runtime
#  DEPENDS system_lib
)

执行编译

使用Ctrl+Shift+B进行编译,然后可以在/devel/topic_test目录下看到自动生成的头文件Student.h,此时便可以代后面的代码中进行引用了。
ROS通信机制(一) —— 话题(topic)与msg文件
PS: 在修改代码之前,先在c_cpp_properties.json文件中添加下头文件路径,否则在代码中引用头文件时会出现找不到的情况。
ROS通信机制(一) —— 话题(topic)与msg文件

修改订阅者和发布者代码

下面我们在代码中使用自定义的消息来进行通信。

  • 发布者(talker.cpp)
#include "ros/ros.h"
#include "topic_test/Student.h"

int main(int argc, char **argv)
{
    // 设置编码
    setlocale(LC_ALL, "");

    // 1.初始化ROS节点
    // 参数3为节点名称,全局唯一
    ros::init(argc, argv, "talker");

    // 2.实例化ROS句柄
    ros::NodeHandle nh;

    // 3.实例化发布者对象
    // 参数1为数据要发布到的话题名称,参数2为缓冲区大小
    ros::Publisher pub = nh.advertise<topic_test::Student>("chatter", 1000);

    // 4.定义要发布的数据
    topic_test::Student msg;
    msg.name = "张三";
    msg.age = 18;
    msg.score = 85.6;

    // 5.定义数据发布的频率为每秒1次
    ros::Rate loop_rate(1);

    // ros::ok()表示只要节点还在运行即满足循环条件
    while (ros::ok())
    {
        // 6.发布消息
        pub.publish(msg);

        // 打印发送的信息
        ROS_INFO("发送的消息是:姓名-%s, 年龄-%d, 成绩-%.2f", msg.name.c_str(), msg.age, msg.score);

        // 处理ROS消息回调,只循环一次
        // 这里并没有接收到任何回调,所以这一句可以不写
        ros::spinOnce();

        loop_rate.sleep();
    }
    
    return 0;
}

  • 订阅者(listener.cpp)
#include "ros/ros.h"
#include "topic_test/Student.h"

void chatterCallback(const topic_test::Student::ConstPtr& msg)
{
    // 4.处理订阅的消息
    ROS_INFO("订阅到了消息: 姓名【%s】, 年龄【%d】, 成绩【%.2f】", msg->name.c_str(), msg->age, msg->score);
}

int main(int argc, char **argv)
{
    // 设置编码
    setlocale(LC_ALL, "");

    // 1.初始化ROS节点
    // 参数3为节点名称,全局唯一
    ros::init(argc, argv, "listener");

    // 2.实例化ROS句柄
    ros::NodeHandle nh;

    // 3.实例化订阅者者对象
    // 参数1为要订阅的话题名称,参数2为缓冲区大小,参数3为处理订阅消息的回调函数
    ros::Subscriber sub = nh.subscribe<topic_test::Student>("chatter", 1000, chatterCallback);

    // 处理ROS消息回调,一直循环,直到ROS节点退出(执行了Ctrl+C或者ros::shutdown()被调用)
    ros::spin();
    
    return 0;
}

编译和运行

使用Ctrl+Shift+B进行编译,然后使用roscore命令启动主节点,然后source下环境变量,分别运行发布者和订阅者节点即可看到通信数据的打印输出。
ROS通信机制(一) —— 话题(topic)与msg文件
☝ ★★★ — 返回 《ROS机器人开发笔记汇总》总目录 — ★★★ ☝

上一篇:乘风破浪,遇见未来元宇宙(Metaverse)之除了Meta之外不可忽视的硬核玩家,索尼PlayStation VR2虚拟现实头显


下一篇:SpringBoot 动态配置邮箱发件人