ROS学习笔记(二)

目录

话题通信

话题通信

话题通信C++实现

自定义msg

服务通信

服务通信模型

自定义srv

参数服务器

理论模型

 新增参数

获取参数

删除参数


话题通信

话题通信模型

话题通信是ROS中使用频率最高的一种通信模式,话题通信是基于发布订阅模式的,也即:一个节点发布消息,另一个节点订阅该消息。

以发布订阅的方式实现不同节点之间数据交互的通信模式。用于不断更新的、少逻辑处理的数据传输场景。

话题通信模型中涉及到三个角色:

  • ROS Master (管理者)
  • Talker (发布者)
  • Listener (订阅者)
  • ROS学习笔记(二)_第1张图片
  • 实现流程:

    0.Talker注册

    Talker启动后,会通过RPC在 ROS Master 中注册自身信息,其中包含所发布消息的话题名称。ROS Master 会将节点的注册信息加入到注册表中。

    1.Listener注册

    Listener启动后,也会通过RPC在 ROS Master 中注册自身信息,包含需要订阅消息的话题名。ROS Master 会将节点的注册信息加入到注册表中。

    2.ROS Master实现信息匹配

    ROS Master 会根据注册表中的信息匹配Talker 和 Listener,并通过 RPC 向 Listener 发送 Talker 的 RPC 地址信息。

    3.Listener向Talker发送请求

    Listener 根据接收到的 RPC 地址,通过 RPC 向 Talker 发送连接请求,传输订阅的话题名称、消息类型以及通信协议(TCP/UDP)。

    4.Talker确认请求

    Talker 接收到 Listener 的请求后,也是通过 RPC 向 Listener 确认连接信息,并发送自身的 TCP 地址信息。

    5.Listener与Talker件里连接

    Listener 根据步骤4 返回的消息使用 TCP 与 Talker 建立网络连接。

    6.Talker向Listener发送消息

    连接建立后,Talker 开始向 Listener 发布消息。

    注意1:上述实现流程中,前五步使用的 RPC协议,最后两步使用的是 TCP 协议

    注意2: Talker 与 Listener 的启动无先后顺序要求

    注意3: Talker 与 Listener 都可以有多个

    注意4: Talker 与 Listener 连接建立后,不再需要 ROS Master。也即,即便关闭ROS Master,Talker 与 Listern 照常通信。

  • 话题通信C++实现

src右键Create Catkin Pakage 输入包名plumbing_pub_sub回车,输入依赖roscpp rospy std_msgs

1.发布方

#include "ros/ros.h"
#include "std_msgs/String.h" //普通文本类型的消息
#include 

int main(int argc, char *argv[])
{   
    /*
        发布方实现:
            1.包含头文件
              ROS中文本类型 --> std_msgs/String.h
            2.初始化 ROS 节点;
            3.创建节点句柄;
            4.创建发布者对象;
            5.编写发布逻辑并发布数据。
    */

    //解决中文乱码
    setlocale(LC_ALL,"");
    //2.初始化 ROS 节点;erGouZi是节点名称
    ros::init(argc,argv,"erGouZi");
    //3.创建节点句柄;
    ros::NodeHandle nh;
    //4.创建发布者对象;话题一致 。队列中最大保存的消息数,超出此阀值时,先进的先销毁(时间早的先销毁)
    ros::Publisher pub = nh.advertise("fang",10);
    
    //5.编写发布逻辑并发布数据。要求以10hz的频率发布数据,并且文本后添加编号。
    //先创建被发布的消息
    std_msgs::String msg;
    
    //发布频率,1s/次
    ros::Rate rate(2);
    //设置编号,消息计数器
    int count = 0;
    //延时3秒发布,避免订阅者收不到前几条消息。
    ros::Duration(3).sleep();
    //编写循环,循环中发布数据
    while (ros::ok())
    {   
        count++;
        //msg.data = "hello";
        //实现字符串拼接数字
        std::stringstream ss;
        ss << "hello -->" << count;
        msg.data = ss.str();
        pub.publish(msg);//发布消息
        //添加日志:
        ROS_INFO("发布的数据是:%s",ss.str().c_str());
        rate.sleep();//根据前面制定的发送贫频率自动休眠 休眠时间 = 1/频率;
        ros::spinOnce();//处理回调函数
    }

  return 0;
}

测试:配置CMake文件,编译,测试。

 

 ROS学习笔记(二)_第2张图片

roscore

cd demo03_ws/
source ./devel/setup.bash
rosrun plumbing_pub_sub demo01_pub


rostopic echo fang

2.订阅方

#include "ros/ros.h"
#include "std_msgs/String.h" //普通文本类型的消息

//传入的消息是std_msgs,String类型的,常量指针引用
void doMsg(const std_msgs::String::ConstPtr &msg){
    //通过msg获取并操作订阅到的数据
    ROS_INFO("翠花订阅的数据:%s",msg->data.c_str());
}

int main(int argc, char *argv[])
{   
    /*
        发布方实现:
            1.包含头文件
              ROS中文本类型 --> std_msgs/String.h
            2.初始化 ROS 节点;
            3.创建节点句柄;
            4.创建订阅者对象;
            5.处理订阅到的数据。
            6.spin()函数
    */

    //解决中文乱码
    setlocale(LC_ALL,"");
    //2.初始化 ROS 节点;cuiHua是节点名称
    ros::init(argc,argv,"cuiHua");
    //3.创建节点句柄;
    ros::NodeHandle nh;
    //4.创建订阅者对象; doMsg是回调函数
    ros::Subscriber sub = nh.subscribe("fang",10,doMsg);
    
    //5.处理订阅到的数据。
    ros::spin();
    
    return 0;
}

3.配置 CMakeLists.txt

 ROS学习笔记(二)_第3张图片

 编译运行

roscore

cd demo03_ws/
source ./devel/setup.bash
rosrun plumbing_pub_sub demo01_pub

cd demo03_ws/
source ./devel/setup.bash
rosrun plumbing_pub_sub demo02_sub

自定义msg

1.定义msgs文件。demo03/src/plumbing_pub_sub/msg/Person.msg

ROS学习笔记(二)_第4张图片

2.编写package.xml文件

3.编写CMake文件

ROS学习笔记(二)_第5张图片

Person.msg是文件名

ROS学习笔记(二)_第6张图片

ROS学习笔记(二)_第7张图片

4.编译后的中间文件

C++ 需要调用的中间文件(.../工作空间/devel/include/包名/Perso.h)

Python 需要调用的中间文件(.../工作空间/devel/lib/python3/dist-packages/包名/msg)

ROS学习笔记(二)_第8张图片

5.终端打开,pwd 复制

c_cpp文件,加入路径

ROS学习笔记(二)_第9张图片

6.编写发布者cpp文件

ROS学习笔记(二)_第10张图片

#include "ros/ros.h"
#include "plumbing_pub_sub/Person.h"
/*
    发布方:发布人消息
        1.包含头文件
            ROS中文本类型 --> std_msgs/String.h
        2.初始化 ROS 节点;
        3.创建节点句柄;
        4.创建发布者对象;
        5.编写发布逻辑并发布数据。
*/

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");
    ROS_INFO("这是消息的发布方");
    //  2.初始化 ROS 节点;
    ros::init(argc,argv,"banzhuren");
    //  3.创建节点句柄;
    ros::NodeHandle nh;
    // 4.创建发布者对象;
    ros::Publisher pub = nh.advertise("liaotian",10);
    // 5.编写发布逻辑并发布数据。
    // 5-1.创建被发布的数据。
    plumbing_pub_sub::Person person;
    person.name = "张三";
    person.age = 18;
    person.height = 1.73;
    // 5-2.设置发布频率
    ros::Rate rate(1);
    //延时3秒发布,避免订阅者收不到前几条消息。
    ros::Duration(3).sleep();
    // 5.3.循环发布数据
    while (ros::ok())
    {   
        
        //核心:数据发布
        pub.publish(person);
        ROS_INFO("发布的消息:%s,%d,%.2f",person.name.c_str(),person.age,person.height);
        //修改数据
        person.age += 1;
        //休眠
        rate.sleep();
        // 
        ros::spinOnce();
    }
    
    return 0;
}

7.编写订阅者cpp文件,注意回调函数。

#include "ros/ros.h"
#include "plumbing_pub_sub/Person.h"

/*
    发布方:发布人消息
        1.包含头文件
            ROS中文本类型 --> std_msgs/String.h
        2.初始化 ROS 节点;
        3.创建节点句柄;
        4.创建订阅者对象;
        5.处理订阅数据。
        6.调用spin()函数。
*/
//回调函数,常量指针引用。
void doPerson(const plumbing_pub_sub::Person::ConstPtr& person)
{
    ROS_INFO("订阅人的消息:%s,%d,%.2f",person->name.c_str(),person->age,person->height);
}
int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");
    ROS_INFO("订阅方实现");
    // 2.初始化 ROS 节点;
    ros::init(argc,argv,"jiazhang");
    // 3.创建节点句柄;
    ros::NodeHandle nh;
    // 4.创建订阅者对象;
    ros::Subscriber sub = nh.subscribe("liaotian",10,doPerson);
    // 5.处理订阅数据。
    // 6.调用spin()函数。
    ros::spin();
    return 0;
}

8.配置CMake文件

ROS学习笔记(二)_第11张图片

9.编译 ctrl+shift+B

roscore

cd demo03_ws/
source ./devel/setup.bash
rosrun plumbing_pub_sub demo03_pub_person

cd demo03_ws/
source ./devel/setup.bash
rosrun plumbing_pub_sub demo04_sub_person


计算图  rqt_graph

服务通信

服务通信模型

服务通信也是ROS中一种极其常用的通信模式,服务通信是基于请求响应模式的,是一种应答机制。也即: 一个节点A向另一个节点B发送请求,B接收处理请求并产生响应结果返回给A

以请求响应的方式实现不同节点之间数据交互的通信模式。用于偶然的、对时时性有要求、有一定逻辑处理需求的数据传输场景。

ROS学习笔记(二)_第12张图片

 

整个流程由以下步骤实现:

0.Server注册

Server 启动后,会通过RPC在 ROS Master 中注册自身信息,其中包含提供的服务的名称。ROS Master 会将节点的注册信息加入到注册表中。

1.Client注册

Client 启动后,也会通过RPC在 ROS Master 中注册自身信息,包含需要请求的服务的名称。ROS Master 会将节点的注册信息加入到注册表中。

2.ROS Master实现信息匹配

ROS Master 会根据注册表中的信息匹配Server和 Client,并通过 RPC 向 Client 发送 Server 的 TCP 地址信息。

3.Client发送请求

Client 根据步骤2 响应的信息,使用 TCP 与 Server 建立网络连接,并发送请求数据。

4.Server发送响应

Server 接收、解析请求的数据,并产生响应结果返回给 Client。

注意:

1.客户端请求被处理时,需要保证服务器已经启动;

2.服务端和客户端都可以存在多个。

自定义srv

服务通信中,数据分成两部分,请求与响应,在 srv 文件中请求和响应使用---分割,

新建plumbing_server_client 功能包下新建 srv 目录,添加 AddInts.srv 文件

ROS学习笔记(二)_第13张图片

1.书写AddInts.srv文件

int32 num1
int32 num2
---
int32 sum

2.配置xml文件

配置CMake文件

ROS学习笔记(二)_第14张图片

ROS学习笔记(二)_第15张图片

ROS学习笔记(二)_第16张图片

 3.需要像之前自定义 msg 实现一样配置c_cpp_properies.json 文件(加入路径)如果以前已经配置且没有变更工作空间,可以忽略,如果需要配置,配置方式与之前相同。

4.编写服务端cpp文件

#include "ros/ros.h"
#include "plumbing_server_client/AddInts.h"

/*
    服务端实现:解析客户端所提交的数据,并运算再产生响应。
        1.包含头文件
        2.初始化ROS节点
        3.创建节点句柄
        4.创建一个服务对象
        5.处理请求并产生响应
        6.spin()
*/

bool doNums(plumbing_server_client::AddInts::Request& request,
            plumbing_server_client::AddInts::Response& response){
    // 1.处理请求;
    int num1 = request.num1;
    int num2 = request.num2;
    ROS_INFO("收到的请求数据:num1 = %d,num2 = %d",num1,num2);
  
    // 2.组织响应;
    int sum = num1 + num2;
    response.sum = sum;
    ROS_INFO("求和结果:sum = %d",sum );

    return true;
}

int main(int argc, char *argv[])
{   
    setlocale(LC_ALL,"");
    //  2.初始化 ROS 节点;
    ros::init(argc,argv,"AddInts_Server");
    //  3.创建节点句柄;
    ros::NodeHandle nh;
    // 4.创建一个服务对象
    ros::ServiceServer server = nh.advertiseService("AddInts",doNums);
    ROS_INFO("服务已经启动...");
    // 5.处理请求并产生响应
    // 6.spin()
    ros::spin();
    return 0;
}

5.

配置CMake文件

ROS学习笔记(二)_第17张图片

编译,测试

roscore

cd demo03

source ./devel/setup.bash

rosrun plumbing_server_client demo01_server



cd demo03

source ./devel/setup.bash

rosservice call AddInts (Tab补齐,更改数字,回车)

6.编写客户端cpp文件

#include "ros/ros.h"
#include "plumbing_server_client/AddInts.h"

/*
    服务端实现:提交两个整数,并处理响应的结果。
        1.包含头文件
        2.初始化ROS节点
        3.创建节点句柄
        4.创建一个客户端对象
        5.提交请求并处理响应
        
    实现参数的动态提交:
        1.格式:ros xxx xxx 12 34. 此时argc包含三个参数--文件名 12 34
        2.节点执行时,需要获取命令中的参数,并组织进 request
    问题:
        如果先启动客户端,请求异常
    需求:
        如果先启动客户端,则先挂起,等服务器启动后,再正常请求。
    解决:
        在ros中有内置函数 ,这些函数可以让客户端启动后挂起,等待服务器启动
        client.waitForExistence();
        ros::service::waitForService("AddInts");
*/

int main(int argc, char *argv[])
{   
    setlocale(LC_ALL,"");
    if(argc != 3 )
    {
        ROS_INFO("提交的参数个数不对。");
        return 1;
    }
    // 2.初始化ROS节点
    ros::init(argc,argv,"AddInts_client");
    // 3.创建节点句柄
    ros::NodeHandle nh;
    // 4.创建一个客户端对象
    ros::ServiceClient client = nh.serviceClient("AddInts");
    // 5.提交请求并处理响应
    plumbing_server_client::AddInts ai;
    // 5-1 组织请求,用atoi把argv解析出来的字符串型变为整形
    ai.request.num1 = atoi(argv[1]);
    ai.request.num2 = atoi(argv[2]);
    // 5-2 处理响应。
    // 调用判断服务器状态的函数
    // client.waitForExistence();
    ros::service::waitForService("AddInts");
    bool flag = client.call(ai); 
    /*
    1.client.call(ai) 相当于我客户端访问了服务器并且把这个ai对象提交了,ai对象里面有request,request里面有num1和num2 2.他响应回来的结果就是那个sum会继续封装在这个ai对象,在这ai对象中除了有request还有response他会把那个结果封装在response,接下来我们就可以通过ai的response来获取数据了。3.客户端访问服务器:client.call(ai),客户端访问服务器并且把ai对象提交了。服务器会返回结果
    */
    if (flag)
    {
        ROS_INFO("响应成功");
        // 获取结果
        ROS_INFO("响应结果 = %d",ai.response.sum);
    }
    else
    {
        ROS_INFO("处理失败...");
    }
    return 0;
}

配置CMake文件

ROS学习笔记(二)_第18张图片

编译,运行

roscore
source ./devel/setup.bash
rosrun plumbing_server_client demo01_server
source ./devel/setup.bash
rosrun plumbing_server_client demo02_client

参数服务器

理论模型

参数服务器在ROS中主要用于实现不同节点之间的数据共享。参数服务器相当于是独立于所有节点的一个公共容器,可以将数据存储在该容器中,被不同的节点调用,当然不同的节点也可以往其中存储数据。以共享的方式实现不同节点之间数据交互的通信模式。

ROS学习笔记(二)_第19张图片

整个流程由以下步骤实现:

1.Talker 设置参数

Talker 通过 RPC 向参数服务器发送参数(包括参数名与参数值),ROS Master 将参数保存到参数列表中。

2.Listener 获取参数

Listener 通过 RPC 向参数服务器发送参数查找请求,请求中包含要查找的参数名。

3.ROS Master 向 Listener 发送参数值

ROS Master 根据步骤2请求提供的参数名查找参数值,并将查询结果通过 RPC 发送给 Listener。

ROS学习笔记(二)_第20张图片

 新增参数

#include "ros/ros.h"

/*
    需要实现参数的新增与修改
    需求:设置机器人的共享参数,类型\半径(0.15m)
        再修改半径(0.2m)
    实现:
        ros::NodeHandle
            setParam()
        ros::param
            set()
    修改:
        继续调用setParam()或set(),键已经存在的话,值会被替换
*/

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");
    //初始化节点
    ros::init(argc,argv,"set_param_c");
    // 创建ROS节点句柄
    ros::NodeHandle nh;
    //参数增--
    //方案一:nh
    nh.setParam("type","xiaohuang");
    nh.setParam("radius",0.15);
    // 方案二:ros::param
    ros::param::set("type_param","xiaobai");
    ros::param::set("radius_param",0.15);

    // 参数改
    // 方案1:nh
    nh.setParam("radius",0.2);//键重复,产生参数覆盖。
    // 方案2:ros::param
    ros::param::set("radius_param",0.25);

    return 0;
}

配置,编译,运行 

roscore
source ./devel/setup.bash
rosrun plumbing_param_server demo01_param_set
rosparam list
rosparam get /radius

获取参数

#include "ros/ros.h"

/*
    参数查询
        ros::NodeHandle -------------
            1.param(键,默认值)
            存在,返回对应结果,否则返回默认值。

            2.getParam(键,存储结果的变量)
                存在,返回 true,且将值赋值给参数2
                若果键不存在,那么返回值为 false,且不为参数2赋值

            3.getParamCached键,存储结果的变量)--提高变量获取效率
                存在,返回 true,且将值赋值给参数2
                若果键不存在,那么返回值为 false,且不为参数2赋值

            4.getParamNames(std::vector)
                获取所有的键,并存储在参数 vector 中 

            5.hasParam(键)
                是否包含某个键,存在返回 true,否则返回 false

            6.searchParam(参数1,参数2)
                搜索键,参数1是被搜索的键,参数2存储搜索结果的变量
        
        ros::param ------------- 与上述类似

*/

int main(int argc, char *argv[])
{   
    // 中文
    setlocale(LC_ALL,"");
    // 初始化节点
    ros::init(argc,argv,"get_param_c");
    // 创建节点句柄
    ros::NodeHandle nh;

    // ros::NodeHandle  ----------
    // 1.param
    double radius = nh.param("radius",0.5); //默认值是0.5
    ROS_INFO("radius = %.2f",radius);

    // 2.getParam
    double radius2 = 0.0;
    bool result = nh.getParam("radius",radius2);//把获取到的radius值存在radius2中。
    // 3.getParamCached 与 getParam 类似,只是性能有所提升
    if (result)
    {
        ROS_INFO("获取的半径是: %.2f",radius2);
    }
    else{
        ROS_INFO("被查询的变量不存在。");
    }

    // 4.getParamNames
    std::vector names;//表示可以改变大小的数组的序列容器。创建一个集合
    nh.getParamNames(names);
    for (auto &&name : names)/*auto的原理就是根据后面的值,来自己推测前面的类型是什么。
auto的作用就是为了简化变量初始化,如果这个变量有一个很长很长的初始化类型,就可以用auto代替。 &&右值引用,元素name,被遍历的是names*/
    {
        ROS_INFO("遍历的元素:%s",name.c_str());
    }
    // 5.hasParam
    bool flag1 = nh.hasParam("radius");
    bool flag2 = nh.hasParam("radiusxxx");
    ROS_INFO("radius 存在吗?%d",flag1);
    ROS_INFO("radiusxxx 存在吗?%d",flag2);

    // 6.searchParam
    std::string key;
    nh.searchParam("radius",key);
    ROS_INFO("搜索结果:%s",key.c_str());//%s字符串,c_str()函数返回一个指向正规C字符串的指针常量


    // ros::param---------
    double radius_param = ros::param::param("radius",100.5);
    ROS_INFO("radius_param = %.2f",radius_param);

    std::vector names_param;
    ros::param::getParamNames(names_param);
    for (auto &&name : names_param)
    {
        ROS_INFO("键:%s",name.c_str());
    }
    return 0;
}

删除参数

#include "ros/ros.h"

/* 
    参数服务器操作之删除_C++实现:

    ros::NodeHandle
        deleteParam("键")
        根据键删除参数,删除成功,返回 true,否则(参数不存在),返回 false

    ros::param
        del("键")
        根据键删除参数,删除成功,返回 true,否则(参数不存在),返回 false
*/

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");
    ros::init(argc,argv,"param_del_c");
    ros::NodeHandle nh;
    // 删除 NodeHandle
    bool flag1 = nh.deleteParam("radius");
    if (flag1)
    {
        ROS_INFO("删除成功");
    }
    else{
        ROS_INFO("删除失败");
    }
    // 删除 ros::param
    bool flag2 = ros::param::del("radius_param");
     if (flag2)
    {
        ROS_INFO("删除成功");
    }
    else{
        ROS_INFO("删除失败");
    }
    return 0;
}

你可能感兴趣的:(ros仿真,学习)