在上一篇博客中,我们介绍了怎么创建一个ROS的最小系统,该系统包括工作空间、包和一个存放在包中的节点,然后我们让该节点打印“Hello ROS”。
在这一篇博客中,我们将进一步来搭建一个最小的话题系统。
在开始之前,我们来简单的介绍下ROS的”话题”。
回想我们以前编程,一般是用什么进行通信的?
是网络协议,比如我们要在两个程序中传输数据,一般的做法是建立一个TCP或者UDP连接,然后从A->B扔数据。
而ROS系统本质上是一个管理多进程工作的系统,它里面的一个节点就是一个进程,节点之间又不可避免的要进行数据交互,因此通信是免不了的。
我们可以非常容易的想到直接用TCP或者UDP来通信就可以了,但是ROS给我们一个更加上层的方法,那就是”话题”(虽然我们只需要花0.1s就能想到它的本质也是TCP或UDP,但人家帮我们封装好了,当然是更加方便的)。
那么”话题”是怎么实现通信的呢?
它的操作非常简单,节点A如果有数据要往外扔,那么它就直接通过”话题”告诉ROS系统它要把数据发布出来。节点B如果想要接收节点A的数据,则告诉ROS系统它要订阅这个话题。
这样,当节点A发布了话题而节点B订阅了该话题,则ROS系统会帮他们之间建立通信。
说白了,ROS相当于一个媒婆,将节点A和节点B连到一块去。
另外需要注意的是发布话题的时候需要先声明话题所具有的数据类型,这就相当于媒婆起码要知道你是男是女。
下面我们就做出两个节点,一个用话题发布数据,另外一个订阅话题接收数据。它们对应的是两个cpp,分别叫做”topicSend.cpp”和”topicReceive.cpp”。
如果你想新建一个工作空间或者包的话,请移步上一篇文章。本篇文章将直接沿用上一篇文章建好的工作空间和包。也就是在“/home/weixin/HelloRos/src/printHelloRosPK/src”这个路径下再加入两个cpp文件,内容和注释如下:
#include "ros/ros.h"
#include "std_msgs/String.h" //消息发送相关头文件
#include <sstream>
int main(int argc, char **argv)
{
ros::init(argc, argv, "topicSend");
//前面两个参数跟系统的重映射有关,在这里不考虑。第三个参数为节点名称,我们定义为topicSend
ros::NodeHandle n;
//节点句柄,第一个创建的节点句柄会为节点初始化。最后一个销毁的节点句柄则会释放该节点所占用的资源
ros::Publisher hello_pub = n.advertise<std_msgs::String>("hello", 1000);
//advertise()会告知ROS我们要发布一个话题,话题名是第一个参数的“hello”。这样ROS的管理进程就会告诉所
//有订阅了"hello"话题的节点,将要有数据发布。第二个参数是发布序列的大小。如果我们发布的消息的频率太高,
//缓冲区中的消息在大于1000个的时候就会开始丢弃先前发布的消息。
//advertise()返回一个ros::Publisher对象,它有两个作用:
//1)它有一个publish()成员函数可以让你在topic上发布消息;
//2)如果消息类型不对,它会拒绝发布。
ros::Rate loop_rate(10);
//ros::Rate对象可以允许你指定自循环的频率。它会追踪记录自上一次调用Rate::sleep()后时间的流逝,并休眠直到过了一个频率周期的时间。
//上面程序的写法就是让程序以10Hz的频率发布消息
int count = 0;//消息发送计数器
while (ros::ok())
{
//如果下列条件之一发生,ros::ok() 返回false:
/*
1.SIGINT 被触发 (Ctrl-C)
2.被另一同名节点踢出 ROS 网络
3.ros::shutdown() 被程序的另一部分调用
4.节点中的所有 ros::NodeHandles 都已经被销毁
*/
std_msgs::String msg;
//标准的String消息,它只有一个数据成员 "data"。当然,你也可以发布更复杂的消息类型。
std::stringstream ss;
ss << "hello world " << count;
msg.data = ss.str();
ROS_INFO("%s", msg.data.c_str());//将要发布的消息打印出来看看
hello_pub.publish(msg);//向所有订阅 chatter 话题的节点发送消息
ros::spinOnce();
//在这个例子中并不是一定要调用 ros::spinOnce(),因为我们不接受回调。然而,如果你的程序里包含其他回调函数,最好在这里加上
//ros::spinOnce()这一语句,否则你的回调函数就永远也不会被调用了。
loop_rate.sleep();
//调用ros::Rate对象来休眠一段时间以使得发布频率为10Hz
++count;
}
return 0;
}
topicReceive.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");
ros::NodeHandle n;
ros::Subscriber sub = n.subscribe("hello", 1000, chatterCallback);
//告诉ROS我们要订阅话题“hello”上的消息。当有消息发布到这个话题时,ROS就会调用chatterCallback()函数。
//第二个参数是队列大小,以防我们处理消息的速度不够快,当缓存达到1000条消息后,再有新的消息到来就将开始丢弃先前接收的消息。
ros::spin();
//ros::spin()进入自循环,可以尽可能快的调用消息回调函数。如果没有消息到达,它不会占用很多CPU,所以不用担心。
//一旦ros::ok()返回false,ros::spin()就会立刻跳出自循环。这有可能是ros::shutdown()被调用,或者是用户按下了Ctrl-C,
//使得 master 告诉节点要终止运行。也有可能是节点被人为关闭的。
return 0;
}
最后我们把下面几句话添加到CMakeLists.txt末尾,告诉编译器我们要编译什么,和它们需要什么依赖关系:
add_executable(topicSend /home/weixin/HelloRos/src/printHelloRosPK/src/topicSend.cpp)#定义了这个工程会生成一个文件名为"topicSend"的可执行文件
target_link_libraries(topicSend ${catkin_LIBRARIES})#指定在链接目标文件的时候需要链接的外部库
add_dependencies(topicSend beginner_tutorials_generate_messages_cpp)#为可执行文件添加对生成的消息文件的依赖
add_executable(topicReceive /home/weixin/HelloRos/src/printHelloRosPK/src/topicReceive.cpp)
target_link_libraries(topicReceive ${catkin_LIBRARIES})
add_dependencies(topicReceive beginner_tutorials_generate_messages_cpp)
接着编译下就可以了,回到工作空间中:
catkin_make
打开两个节点,就能看到两个节点直接一个在发送数据,另外一个在接收数据,因为比较简单,就省略了。
如果想要发布自定义的消息,则比较繁琐,流程如下: 1.在程序文件夹(与src同一级别 )中创建msg文件夹 2.在msg文件夹中创建消息obstacleMsg.msg文件(以”.msg”结尾),并定义消息类型:
int32 mObjId float32[4] xy int32 mObjState float32 mObjRCS
3.package.xml需要添加编译依赖项和运行依赖项
<build_depend>message_generation</build_depend> <exec_depend>message_runtime</exec_depend>
4.CmakeList.txt文件中, find_package()中添加message_generation
find_package(catkin REQUIRED COMPONENTS message_generation roscpp std_msgs )
add_message_files()中添加obstacleMsg.msg
add_message_files(FILES obstacleMsg.msg )
generate_messages()去掉注释
generate_messages(DEPENDENCIES std_msgs )
catkin_package()添加message_runtime
catkin_package( CATKIN_DEPENDS message_runtime )
到此就OK了 在需要用到该消息的cpp文件中包含消息头文件:
#include <cankvaser/obstacleMsg.h>
其中“cankvaser”是消息文件夹msg或该消息所在的ROS包的名字,“obstacleMsg.h”是消息的名字对应的”.h”文件。因为ROS归根结底是要转化为c++进行编译的,它只是把转换的工程自动化了,因此会出现这种可能看起来比较诡异的头文件包含。 消息发布者基本跟发布默认消息一致
ros::Publisher MWRadarPub = n.advertise<cankvaser::obstacleMsg>("MWRadar", 1000);//设置发送消息
消息赋值也非常直观:
cankvaser::obstacleMsg mobstacle; mobstacle.mObjId=mObjId; mobstacle.xy[0]=mObjX; mobstacle.xy[1]=mObjY; mobstacle.xy[2]=mObjXSpeed; mobstacle.xy[3]=mObjYSpeed; mobstacle.mObjState=mObjState; mobstacle.mObjRCS=mObjRCS;