机器人操作系统ROS从入门到放弃(五):在类中发布和接收消息

117
0
2020年10月10日 10时38分

本文作者:陈瓜瓜

 

许久没更新了,放假回家没想到还要被老师催论文,连玩耍的时间都不够= =.好了,闲话少说

例1

上一讲讲了一些类什么的基础.这一讲我们就谈谈怎么在类中发布和接收消息了.当你遇到稍微大型一点的程序时,在类中发布和接收消息才是主流,毕竟不在类中的callback函数返回值能做的有限.代码结构其实很类似,咱们直接先上代码再来找不同.同样以发布一个整型数Int8为例子.
同样先在 pub_sub_test/src中建立一个发布消息和一个接收消息的文件.分别叫pub_int8_class.cpp和sub_int8_class.cpp.在pub_int8_class.cpp中输入以下内容.

 

 

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

class pubInt8{
public:
   pubInt8(ros::NodeHandle& nh){
       chatter_pub = nh.advertise<std_msgs::Int8>("chatter", 1000); //assign value to chatter_pub
   };

   void pub(){
       int count = 0;
       ros::Rate loop_rate(10);
       while (ros::ok())
       {
           std_msgs::Int8 msg; 

           msg.data = count;// msg.data = ss.str();

           ROS_INFO("%d", msg.data);

           chatter_pub.publish(msg);

           ros::spinOnce();

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

private:
   ros::Publisher chatter_pub;//set the publisher as a member
};

int main(int argc, char **argv)
{
 ros::init(argc, argv, "talker");

 ros::NodeHandle n;

 pubInt8 p8(n);

 p8.pub();

 return 0;
}

 

对比我们在第二讲中的pub_int8.cpp中的内容, 总的来说大同小异,来看看不同的地方.
我们把ros::Publisher chatter_pub作为类的一个私有成员成员变量定义在了类中.这时候并没有对它赋值.然后在构造函数中才对他赋值的.
对publisher赋值需要rosNodeHandle,所以我们在创建对象时把rosNodeHandle作为一个参数传入了构造函数,这样我们就可以在构造函数中为chatter_pub赋值.
之后在主函数中我们使用类的pub函数实现信息的发布,发布的部分的代码和pub_int8.cpp中的一模一样.

 

下面来看看sub_int8_class.cpp

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

class subInt8{
public:
    subInt8(){};
    void chatterCallback(const std_msgs::Int8::ConstPtr& msg){
        ROS_INFO("I heard: [%d]", msg->data);
    }
};

int main(int argc, char **argv)
{
  ros::init(argc, argv, "listener");

  ros::NodeHandle n;

  subInt8 s8;

  ros::Subscriber sub = n.subscribe("chatter", 1000, &subInt8::chatterCallback, &s8);

  ros::spin();

  return 0;
}

 

 

同样来看看和sub_int8.cpp不同的地方.
callback函数内容一模一样,只是放到了类中.
在定义subscriber时和不使用类时有很大的不同.
不使用类时,我们是这么写的

ros::Subscriber sub = n.subscribe("chatter", 1000, chatterCallback);

使用类时,我们是这么写的

ros::Subscriber sub = n.subscribe("chatter", 1000, &subInt8::chatterCallback, &s8);

 

即原来我们只需要把回调函数名作为参数写在n.subscribe中,现在我们需要用&类名::函数名代替原来那个参数,之后还需要增加一个参数,即在定义subscriber之前定义好的类的对象名&对象名. 把这两个程序写入CMakeLists中,catkin_make后使用rosrun跑起来.会得到第二讲pub_int8程序一样的效果.

基本的类中发布接收消息就是这样了,注意不要死板,使用要灵活.比如说你写了一个在类中发布消息的程序,你不仅需要在构造函数当中使用rosNodeHandle,还需要在其他函数中使用,怎么做?你可以把ros::NodeHandle也作为一个私有成员(或者公有成员)写在类中,使用构造函数把外来这个nodeHandle传值给私有成员nodeHandle.或者甚至不在构造函数传值,专门写一个register nodehandle的函数

 

 

 

void registerNodeHandle(ros::NodeHandle& _nh){
    nh = _nh; //_nh is a member in class, _nh is the nodehandle you define, for example, in main function.
}

 

 

这样nh属于你的类,你就可以在类的其他函数进行任何有关它的操作了.
在类外,你只需要定义一个该类的对象,通过 对象.registerNodeHandle(主函数中(或者其他位置)定义的NodeHandle)把nodehandle传入类中.

 

例2

 

为了方便大家运用,我们再写一个例子.ros程序经常出现我们在接收到一个消息之后经过代码中一系列处理再发布给另一个topic.即咱们在同一个文件中既发布消息,又接收来自其他topic(对topic陌生了?翻回第一讲去看看)的消息.发布接收的消息使用double即float64吧.

首先写一个叫pubSub_class_example2A.cpp的程序,代码如下

 

#include "ros/ros.h"
#include "std_msgs/Float64.h"
#include <unistd.h>

class tl1{
public:
    tl1();
    void registerNodeHandle(ros::NodeHandle& _nh);
    void registerPubSub();
    void iniPub();
    void fCallback(const std_msgs::Float64::ConstPtr& msg);
private:
    ros::Publisher pub_f64;
    ros::Subscriber sub_f64;
    ros::NodeHandle nh;
};

int main(int argc, char **argv){

    ros::init(argc, argv, "talker_listener1");

    ros::NodeHandle nh;

    tl1 pubSub1;

    pubSub1.registerNodeHandle(nh);
    pubSub1.registerPubSub();

    pubSub1.iniPub();

    ros::spin();
}

tl1::tl1(){};

void tl1::registerNodeHandle(ros::NodeHandle& _nh){
    nh = _nh;
};

void tl1::registerPubSub(){
    pub_f64 = nh.advertise<std_msgs::Float64>("chatter1to2",1000);
    sub_f64 = nh.subscribe("chatter2to1",1000,&tl1::fCallback, this); //this pointer here means the class itself
};

//When you receive some data, it is highly possible you do some operations on that data and then publish something in/after the call back function
void tl1::fCallback(const std_msgs::Float64::ConstPtr& msg){
    std::cout<<"talker_listener1 heard "<<msg->data<<std::endl;

    std_msgs::Float64 pubData;

    pubData.data = msg->data - 1;

    ros::Rate sr(10);
    pub_f64.publish(pubData);
    sr.sleep();
};

void tl1::iniPub(){
    std::cout<<"publish the first data "<<std::endl;

    std_msgs::Float64 pubData;

    pubData.data = 1;

    usleep(500000);//wait for connection
    pub_f64.publish(pubData);
}

 

 

接下来写一个pubSub_class_example2B.cpp的程序.

 

 

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

class tl1{
public:
    tl1();
    void registerNodeHandle(ros::NodeHandle& _nh);
    void registerPubSub();
    void fCallback(const std_msgs::Float64::ConstPtr& msg);
private:
    ros::Publisher pub_f64;
    ros::Subscriber sub_f64;
    ros::NodeHandle nh;
};

int main(int argc, char **argv){

    ros::init(argc, argv, "talker_listener2");

    ros::NodeHandle nh;

    tl1 pubSub1;

    pubSub1.registerNodeHandle(nh);
    pubSub1.registerPubSub();

    ros::spin();
}

tl1::tl1(){};

void tl1::registerNodeHandle(ros::NodeHandle& _nh){
    nh = _nh;
};

void tl1::registerPubSub(){
    pub_f64 = nh.advertise<std_msgs::Float64>("chatter2to1",1000);
    sub_f64 = nh.subscribe("chatter1to2",1000,&tl1::fCallback, this); //this pointer here means the class itself
};

//When you receive some data, it is highly possible you do some operations on that data and then publish something in/after the call back function
void tl1::fCallback(const std_msgs::Float64::ConstPtr& msg){
    std::cout<<"talker_listener2 heard "<<msg->data<<std::endl;

    std_msgs::Float64 pubData;

    pubData.data = msg->data + 1;

    ros::Rate sr(10);
    pub_f64.publish(pubData);
    sr.sleep();
};

 

 

两个文件代码基本一样,
不同的地方是
1:…B中没有iniPub这个函数.
2:两个程序的node名字和topic名字有些差异,方便建立连接,自行观察,一会儿用rqt_graph就可以更明了地看到.
针对pubSub_class_example2A把代码中比较重要的点说明一下
1:在tl1的类中我们把ros NodeHandle也作为类的成员了,专门谢了一个registerNodeHandle的函数把nodeHandle传递到类中.另外类成员不仅有了publisher还有subscriber.我们也专门写了一个registerPubSub函数来给publisher和subscriber赋值.这样写虽然我们麻烦了一些,但是代码读起来就更加明了.
2:这里注意一下registerPubSub中给sub_f64赋值的方法,最后一个参数是this指针.this指针具体的作用大家去查.我们的第一个例子在类外定义了subscriber,最后一个参数是类的对象,我们要在类中定义subscriber,不可能给一个类的对象了,毕竟这时候我们都还不知道对象叫啥.最流行的说法是this指针指针指向类本身.当我们在类中需要指定类对象的时候,就可以用this指针代替那个我们不知道的对象.
3:回调函数fCallback中我做了一个很无聊的事儿,就是把接收到的数据减1而已.这代表了对你接收到的数据以操作.实际运用中你可能有很复杂的操作.这一系列操作完成后你需要将结果发布出去.我们再以每秒10次的频率发布出去.
4:iniPub函数用来初始化发布一个数据,这样2B程序的subcriber接收到后经过一系列操作(我其实就是加了1==),发布出来再被我们这个程序的subcriber接收,即fCallback接收,”一系列操作”(其实就是减1…)再发布出去,如此循环.
这里注意到有一个usleep(500000)代表休眠500ms,至关重要.两个节点的通过topic建立起来联系是需要时间的.我们这个程序,如果没有休眠一下,可以说程序一跑起来瞬间消息就发布出去了.但是我们接下来要写的另一个节点和这个节点还没连接起来,说白了就是ROS没反应过来.那这个消息就漏掉了.有些程序漏掉一两个消息没关系,我们这个程序第一个消息漏掉了就没有然后了.因为B程序没有接收到消息就没办法发布,A程序就没法接收,也没法继续发布了.
当你一则消息都不想漏掉时让程序睡一会儿,等待节点连接起来再发布消息
把两个代码写进CMakeLists跑起来,注意先跑B程序,如果先跑A程序,因为只休眠了500ms,你A程序iniPub的消息在你慢悠悠地在另一个terminal中跑B程序的时间之前也已经发布出去了并被漏掉了.跑起来后B可以看到一个程序一直接收1,另一个一直接收0.这时候再开一个terminal,像第一讲一样输入rqt_graph,可以看到下图.

 

 

1

 

 

程序A,即节点talker_listener1中通过topic chatter1to2把消息发送给节点talker_listener2,talker_listener2把消息接收到后,在callback函数中一系列操作再通过topic chatter2to1把消息发布回talker_listener1.可以回看代码中两个程序的subcriber和publisher赋值时他们对应的topic名字与这个图联系起来.

大家可以尝试把iniPub中的usleep(500000)注释掉再跑程序,看看会发生什么.

总结

这一讲讲了类中发布和接收消息的方法.1到5讲关于发布接收消息我们也说了这么多了.ROS其实很大程度上就是方便大家通讯各种传感器的东西,所以咱们关于发布接收消息讲了这么多.在类中操作才是使用ROS的主流.其实还有很多细节,以后如果有机会会慢慢说到.没有机会大家通过以后自己写程序出的毛病慢慢领悟hhhhhh.下一讲咱们谈谈使用roslaunch了.针对roslaunch大概就讲两个东西吧,一个当然是roslaunch跑程序的基本方法,另一个是使用roslaunch传递外界参数,这个东西帮了我很多忙.

发表评论

后才能评论