ROS 入门 —— 话题消息的定义与使用
ROS 入门 —— 话题消息的定义与使用
话题模型
在 ROS 已经订阅好的消息中,美誉能够满足我们需求的,这时我们就需要自己来定义消息的类型了,下面我们就要介绍消息的定义,以及 publisher 和 subscriber 的使用
如何定义一个话题消息?这里我们以 Person 为例,我们需要先定义一些数据,然后再定义一些确定的数据值就相当于程序中的宏定义,我们可以在程序中直接调用值:
这里给出的类型只是一个预先类型,仅定义了数据的长度、格式等,并不是最终的数据类型,最终的数据类型会根据编程语言的不同而扩展成不同的类型,在编译的时候会进行动态扩展,所以我们还需要加入一系列的编译选项才能完整正确的编译
具体流程如下:
定义数据接口
首先我们在我们的 learning_topic 包中创建一个 msg 文件夹,存储跟消息相关的定义,便于管理,进入 msg 目录下,创建我们的 msg 文件,Person.msg 文件内容如下:
string name
uint8 age
uint8 sex
uint8 unknown = 0
uint8 male = 1
uint8 female = 2
添加依赖
首先我们需要一个动态生成程序的功能包依赖,我们直接添加到 learning_topic 路径下的 package.xml 文件中:
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
这里的 message_generation 是动态生成程序的依赖,message_runtime 是运行程序时动态连接的依赖,添加位置如下:
在 xml 中添加了功能包依赖的同时,我们也要将依赖提添加到 CMakeLists.txt 文件中:
然后我们还需要在 CMakeLists.txt 文件中添加一个能够把 msg 编译成对应的不同程序文件的配置项:
add_message_files(
FILES
Person.msg
)
generate_messages(
DEPENDENCIES
std_msgs
)
add_message_files 会把我们定义的这个 msg 文件作为我们定义的接口,我们在编译的时候就会发现这个接口并针对它进行编译
generate_messages 会在编译 msg 文件的时候补全一些依赖的 ROS 中已有的包或库
添加位置如下:
最后我们还需要在 catkin_message 中创建一个 msg 运行的依赖:
这里我们直接取消注释就行,到这里,我们就完成了编译 msg 文件的所有基本配置,然后我们直接编译就行
调用 msg 中定义的变量
我们在编译成功后,会在 catkin_ws/devel/include/learning_topic 路径下生成 Person.msg 对应的头文件 Person.h
要想调用我们发布的 msg 的变量,我们可以直接调用生成的 Person.h 这个头文件,这里我们同时发布一个 publisher 和一个 subscriber:
点击查看完整代码:
person_publisher.cpp
//
// Created by ppqpp on 2023/2/27.
//
/**
* 该例程将发布/person_info话题,自定义消息类型learning_topic::Person
*/
#include <ros/ros.h>
#include "learning_topic/Person.h"
int main(int argc, char **argv)
{
// ROS节点初始化
ros::init(argc, argv, "person_publisher");
// 创建节点句柄
ros::NodeHandle n;
// 创建一个Publisher,发布名为/person_info的topic,消息类型为learning_topic::Person,队列长度10
ros::Publisher person_info_pub = n.advertise<learning_topic::Person>("/person_info", 10);
// 设置循环的频率
ros::Rate loop_rate(1);
int count = 0;
while (ros::ok())
{
// 初始化learning_topic::Person类型的消息
learning_topic::Person person_msg;
person_msg.name = "Tom";
person_msg.age = 18;
person_msg.sex = learning_topic::Person::male;
// 发布消息
person_info_pub.publish(person_msg);
ROS_INFO("Publish Person Info: name:%s age:%d sex:%d",
person_msg.name.c_str(), person_msg.age, person_msg.sex);
// 按照循环频率延时
loop_rate.sleep();
}
return 0;
}
person.subscriber.cpp
//
// Created by ppqpp on 2023/2/27.
//
/**
* 该例程将订阅/person_info话题,自定义消息类型learning_topic::Person
*/
#include <ros/ros.h>
#include "learning_topic/Person.h"
// 接收到订阅的消息后,会进入消息回调函数
void personInfoCallback(const learning_topic::Person::ConstPtr& msg)
{
// 将接收到的消息打印出来
ROS_INFO("Subcribe Person Info: name:%s age:%d sex:%d",
msg->name.c_str(), msg->age, msg->sex);
}
int main(int argc, char **argv)
{
// 初始化ROS节点
ros::init(argc, argv, "person_subscriber");
// 创建节点句柄
ros::NodeHandle n;
// 创建一个Subscriber,订阅名为/person_info的topic,注册回调函数personInfoCallback
ros::Subscriber person_info_sub = n.subscribe("/person_info", 10, personInfoCallback);
// 循环等待回调函数
ros::spin();
return 0;
}
person_publisher.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 该例程将发布/person_info话题,自定义消息类型learning_topic::Person
import rospy
from learning_topic.msg import Person
def velocity_publisher():
# ROS节点初始化
rospy.init_node('person_publisher', anonymous=True)
# 创建一个Publisher,发布名为/person_info的topic,消息类型为learning_topic::Person,队列长度10
person_info_pub = rospy.Publisher('/person_info', Person, queue_size=10)
#设置循环的频率
rate = rospy.Rate(10)
while not rospy.is_shutdown():
# 初始化learning_topic::Person类型的消息
person_msg = Person()
person_msg.name = "Tom";
person_msg.age = 18;
person_msg.sex = Person.male;
# 发布消息
person_info_pub.publish(person_msg)
rospy.loginfo("Publsh person message[%s, %d, %d]", person_msg.name, person_msg.age, person_msg.sex)
# 按照循环频率延时
rate.sleep()
if __name__ == '__main__':
try:
velocity_publisher()
except rospy.ROSInterruptException:
pass
person_subscriber.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 该例程将订阅/person_info话题,自定义消息类型learning_topic::Person
import rospy
from learning_topic.msg import Person
def personInfoCallback(msg):
rospy.loginfo("Subcribe Person Info: name:%s age:%d sex:%d",
msg.name, msg.age, msg.sex)
def person_subscriber():
# ROS节点初始化
rospy.init_node('person_subscriber', anonymous=True)
# 创建一个Subscriber,订阅名为/person_info的topic,注册回调函数personInfoCallback
rospy.Subscriber("/person_info", Person, personInfoCallback)
# 循环等待回调函数
rospy.spin()
if __name__ == '__main__':
person_subscriber()
我们这里在 subscriber 的程序中使用了回调函数,如果我们设置为全局接收,那么我们的程序将会循环运行,将占用大量的系统资源,而我们是了回调函数,程序仅在对方发出对应程序,我方成功接收到指定程序的基础下执行下一步程序,所以这里选择使用回调函数来接收信息
然后我们需要在 CMakeList.txt 文件中添加我们需要的配置:
add_executable(person_publisher src/person_publisher.cpp)
target_link_libraries(person_publisher ${catkin_LIBRARIES})
add_dependencies(person_publisher ${PROJECT_NAME}_generate_messages_cpp)
add_executable(person_subscriber src/person_subscriber.cpp)
target_link_libraries(person_subscriber ${catkin_LIBRARIES})
add_dependencies(person_subscriber ${PROJECT_NAME}_generate_messages_cpp)
我们前面已经介绍过前两句的作用了,我们现在来介绍 add_dependencies ,它的主要功能就是将我们所添加的 publisher 或 subscribe 与 在编译中生成的其他文件进行连接
这里我们直接编译发布就行
运行测试
这里我们直接运行我们前面发布的节点:
这里我们先运行订阅者(subscriber)程序,可以看到,由于我们没有运行发布者(publisher)程序,我们没有接收到任何内容
然后我们运行发布者(publisher)程序,这里可以看到我们已经进行了信息的发布
然后我们再看前面运行的订阅者(subscriber)程序,根据时间戳,我们可以判断我们在发布的同一时间接收到了信息
这里我们还可以进行一个测试,当我们将 ros-master 关闭后,我们的发布者和订阅者的程序是否还在正常运行?
我们可以直观的看到,当我们关闭了 ros-master 之后,程序依然在正常运行,其实 ros-master 在这里充当的就是一个中介的作用,当我们两个程序已经进行了连接,就已经不需要再有这个中介来对程序进行管理了