ROS中的两种通信方式

ROS中的两种通信方式

一、 TOPIC通信

Topic是ROS里一种异步通信的模型,一般是节点间分工明确,有的只负责发送,有的只负责接收处理。对于绝大多数的机器人应用场景,比如传感器数据收发,速度控制指令的收发,Topic模型是最适合的通信方式。
本文以MOOC《机器人操作系统入门》中的gps信号收发相关的代码作为例子。

1.1 创建消息 (.msg文件)

事先建立topic_demo包,并在msg路径下创建gps.msg。即topic_demo/msg/gps.msg

string state   #工作状态
float32 x      #x坐标
float32 y      #y坐标

创建完了msg文件,记得修改CMakeLists.txt和package.xml,从而让系统能够编译自定义消息。

CMakeLists.txt中需要的改动:

find_package(catkin REQUIRED COMPONENTS roscpp std_msgs 
message_generation     #需要添加这一句
)

add_message_files(FILES gps.msg)  
    #catkin在cmake之上新增的命令,指定从哪个消息文件生成

generate_messages(DEPENDENCIES std_msgs) 
    #catkin新增的命令,用于生成消息
    #DEPENDENCIES后面指定生成msg需要依赖其他什么消息,由于gps.msg用到了flaot32这种ROS标准消息,因此需要再把std_msgs作为依赖

package.xml中需要添加的地方:

<build_depend>message_generation</build_depend>
<run_depend>message_runtime</run_depend>

若要在代码中使用自定义消息类型,只要#include <topic_demo/gps.h>,然后声明,按照对结构体操作的方式修改内容即可。例如:

topic_demo::gps mygpsmsg;
mygpsmsg.x = 1.6;
mygpsmsg.y = 5.5;
mygpsmsg.state = "working";

1.2 创建发布节点(talker.cpp)

新建 topic_demo/src/talker.cpp

#include <ros/ros.h>   
#include <topic_demo/gps.h>  //自定义msg产生的头文件

int main(int argc, char **argv)
{
  ros::init(argc, argv, "talker");  //用于解析ROS参数,第三个参数为本节点名
  ros::NodeHandle nh;    //实例化句柄,初始化node

  topic_demo::gps msg;  //自定义gps消息并初始化,msg为实例化的对象名称 
   ...

  ros::Publisher pub = nh.advertise<topic_demo::gps>("gps_info", 1); //创建publisher,往"gps_info"话题上发布消息. "gps_info"为定义的topic的名称
  ros::Rate loop_rate(1.0);   //定义发布的频率,1HZ 
  while (ros::ok())   //循环发布msg
  {
    ...   //处理msg
    pub.publish(msg);//以1Hz的频率发布msg
    loop_rate.sleep();//根据前面的定义的loop_rate,设置1s的暂停
  }
  return 0;
}

几个关键的地方:
1.创建publisher,并往"gps_info"话题上发布消息。

ros::Publisher pub = nh.advertise<topic_demo::gps>("gps_info", 1); //第一个参数"gps_info"定义了该topic的名称

2.实例化消息
topic_demo::gps msg; //自定义gps消息并初始化,msg为实例化的对象名称

1.3 创建消息接收节点

创建 topic_demo/src/listener.cpp

#include <ros/ros.h>
#include <topic_demo/gps.h>
#include <std_msgs/Float32.h>

void gpsCallback(const topic_demo::gps::ConstPtr &msg)
{  
    std_msgs::Float32 distance;  //计算离原点(0,0)的距离。std_msgs::Float32是ros中的一个结构体,类似于c++中的float distance
    distance.data = sqrt(pow(msg->x,2)+pow(msg->y,2));
    ROS_INFO("Listener: Distance to origin = %f, state: %s",distance.data,msg->state.c_str()); //输出
}

int main(int argc, char **argv)
{
  ros::init(argc, argv, "listener");
  ros::NodeHandle n;
  ros::Subscriber sub = n.subscribe("gps_info", 1, gpsCallback);  //设置回调函数gpsCallback
  ros::spin(); //ros::spin()用于调用所有可触发的回调函数,将进入循环,不会返回,类似于在循环里反复调用spinOnce() 
  //而ros::spinOnce()只会去触发一次
  return 0;
}
在topic接收方,有一个比较重要的概念,就是回调(CallBack),在本例中,回调就是预先给gps_info话题传来的消息准备一个回调函数,你事先定义好回调函数的操作,本例中是计算到原点的距离。只有当有消息来时,回调函数才会被触发执行。具体去触发的命令就是ros::spin(),它会反复的查看有没有消息来,如果有就会让回调函数去处理。
因此千万不要认为,只要指定了回调函数,系统就回去自动触发,你必须ros::spin()或者ros::spinOnce()才能真正使回调函数生效。

对于topic的接收方,有一个比较重要的概念——回调(CallBack),在本例中,回调就是预先给gps_info话题传来的消息准备一个回调函数,你事先定义好回调函数的操作。本例中是计算到原点的距离。
只有当有消息来时,回调函数才会被触发执行。具体去触发的命令就是ros::spin(),它会反复的查看有没有消息来,如果有就会让回调函数去处理。

1.4 再次修改CMakeLists.txt文件

add_executable(talker src/talker.cpp) #生成可执行文件talker
add_dependencies(talker topic_demo_generate_messages_cpp)
#表明在编译talker前,必须先生编译完成自定义消息
#必须添加add_dependencies,否则找不到自定义的msg产生的头文件
#表明在编译talker前,必须先生编译完成自定义消息
target_link_libraries(talker ${catkin_LIBRARIES}) #链接

add_executable(listener src/listener.cpp ) #声称可执行文件listener
add_dependencies(listener topic_demo_generate_messages_cpp)
target_link_libraries(listener ${catkin_LIBRARIES})#链接

1.5 编译运行

完成以上步骤后,进行在ROS工作空间根目录进行catkin_make编译。
使用rosrun分别启动talker 和listener即可查看结果。

二、service通信

Service是一种请求-反馈的通信机制。请求的一方通常被称为客户端,提供服务的一方叫做服务器端。Service机制相比于Topic的不同之处在于:

  1. 消息的传输是双向的,有反馈的,而不是单一的流向。
  2. 消息往往不会以固定频率传输,不连续,而是在需要时才会向服务器发起请求。
    本文以MOOC《机器人操作系统入门》中的相关代码作为例子。一个节点发出服务请求(姓名,年龄),另一个节点进行服务响应,答复请求。

2.1 创建服务(.srv文件)

创建service_demo/Greeting.srv文件,内容如下:

string name        #短横线上边部分是服务请求的数据
int32 age          
---                #短横线下面是服务回传的内容。
string feedback

srv格式的文件创建后,也需要修改CMakeLissts.txt,在其中加入

add_service_files(FILES Greeting.srv)

其余与添加msg的改动一样。
需要使用Greeting.srv文件时,只需要#include <service_demo/Greeting.h>,然后即可创建该类型的srv。例如:

service_demo::Greeting grt;  //grt分为grt.request和grt.response两部分
grt.request.name = "HAN"; //不能用grt.name或者grt.age来访问  
grt.request.age = "20";

2.2 创建服务提供节点(server.cpp)

创建service_demo/srv/server.cpp,内容如下:

#include <ros/ros.h>
#include <service_demo/Greeting.h>

bool handle_function(service_demo::Greeting::Request &req, service_demo::Greeting::Response &res){
    //显示请求信息
    ROS_INFO(“Request from %s with age %d”, req.name.c_str(), req.age);
    //处理请求,结果写入response
    res.feedback = “Hi ” + req.name + “. I’m server!”;
    //返回true,正确处理了请求
    return true;
}

int main(int argc, char** argv){
    ros::init(argc, argv, “greetings_server”);        //解析参数,命名节点
    ros::NodeHandle nh;                       //创建句柄,实例化node
    ros::ServiceServer service = nh.advertiseService(“greetings”, handle_function);  //写明服务的处理函数
    ros::spin();
    return 0;
}

在以上代码中,服务的处理操作都写在处理函数handle_function()中,它的输入参数就是Greeting的Request和Response两部分,而非整个Greeting对象。
通常在处理函数中,我们对Requst数据进行需要的操作将结果写入到Response中。
在roscpp中,handle_function()返回值是bool型,也就是服务是否成功执行。不要理解成输入Request,返回Response,在rospy中才是这样的。

2.3 创建服务请求节点(client.cpp)

创建service_demo/srv/client.cpp内容如下:

# include "ros/ros.h"
# include "service_demo/Greeting.h"

int main(int argc, char **argv)
{
    ros::init(argc, argv, "greetings_client");// 初始化,节点命名为"greetings_client"
    ros::NodeHandle nh;
    ros::ServiceClient client = nh.serviceClient<service_demo::Greeting>("greetings");
    // 定义service客户端,service名字为“greetings”,service类型为Service_demo

    // 实例化srv,设置其request消息的内容,这里request包含两个变量,name和age,见Greeting.srv
    service_demo::Greeting srv;
    srv.request.name = "HAN";
    srv.request.age = 20;

    if (client.call(srv))
    {
        // 注意我们的response部分中的内容只包含一个变量response,另,注意将其转变成字符串
        ROS_INFO("Response from server: %s", srv.response.feedback.c_str());
    }
    else
    {
        ROS_ERROR("Failed to call service Service_demo");
        return 1;
    }
    return 0;
}

以上代码比较关键的地方有两处:
第一个是建立client

nh.serviceClient<service_demo::Greeting>("greetings") // 指明服务的类型和服务的名称

第二个是调用服务

client.call(srv) //注意返回结果不是response,而是是否成功调用远程服务。


CMakeLists.txt和pacakge.xml修改方法和topic_demo修改方法类似,不再赘述。

参考:

参考资料:
[1]: MOOC网《机器人操作系统入门》

猜你喜欢

转载自blog.csdn.net/qq_43145072/article/details/85857883