【Autolabor初级教程】ROS机器人入门 第 2 章 ROS通信机制 个人学习笔记

【Autolabor初级教程】ROS机器人入门 第 2 章 ROS通信机制 笔记

ROS 中的基本通信机制主要有如下三种实现策略:

  • 话题通信(发布订阅模式)
  • 服务通信(请求响应模式)
  • 参数服务器(参数共享模式)

2.1 话题通信

概念:以发布订阅的方式实现不同节点之间数据交互的通信模式

作用:用于不断更新的、少逻辑处理的数据传输场景

适用场景:话题通信适用于不断更新的数据传输相关的应用场景

理论模型

参考链接:视频讲得很通透

038话题通信_理论模型_Chapter2-ROS通信机制_哔哩哔哩_bilibili

image-20240318233507623

话题通信基本操作(C++)
  1. 发布方

    // 1.包含头文件
    #include "ros/ros.h"
    #include "std_msgs/String.h" // ros中文本类型
    #include <sstream>
    
    int main(int argc, char *argv[])
    {
        // 2.初始化ros节点
        ros::init(argc,argv,"pub");
        // 3.创建节点句柄
        ros::NodeHandle nh;
        // 4.创建发布者对象 第一个参数为发布的话题
        ros::Publisher pub = nh.advertise<std_msgs::String>("pos",10);
        // 5.编写发布逻辑并发布数据
        // 要求以10hz的频率发布数据,并且文本后添加编号
        // 先创建被发布消息
        std_msgs::String msg;
        // 发布频率
        ros::Rate rate(10); // 该参数为指定频率
        // 设置编号
        int count = 0;
        // 编写循环,循环中发布数据
        while(ros::ok())
        {
            // 输出中文
            setlocale(LC_ALL,"");
            count++;
            // 实现字符串拼接数字
            // msg.data = "here";
            std::stringstream ss;
            ss << "hello ---> " << count;
            msg.data = ss.str();
            pub.publish(msg);
            ROS_INFO("发布的数据是:%s",ss.str().c_str());
            // 根据前面制定的发送频率自动休眠 休眠时间 = 1/频率;
            rate.sleep();
        }
        return 0;
    }
    

    如果代码中无输出,验证此代码时,可以在新的终端输入 rostopic echo pos,屏幕中会显示数据

    image-20240319024753163

  2. 订阅方

    // 1.包含头文件
    #include "ros/ros.h"
    #include "std_msgs/String.h" // ros中文本类型
    
    void domsg(const std_msgs::String::ConstPtr &msg)
    {
        // 通过msg获取并操作订阅到的数据
        ROS_INFO("接受到的数据是:%s",msg->data.c_str());
    }
    
    int main(int argc, char *argv[])
    {
        setlocale(LC_ALL,"");
        // 2.初始化ros节点
        ros::init(argc,argv,"sub");
        // 3.创建节点句柄
        ros::NodeHandle nh;
        // 4.创建订阅者对象 第一个参数为订阅的话题,domsg为所进行的操作
        ros::Subscriber sub = nh.subscribe("pos",10,domsg);
        // 5.处理订阅到的数据
        // 6.循环读取接收的数据,并调用回调函数处理
        // 如果要用回调函数必须用spin
        ros::spin();
        return 0;
    }
    

    同时执行两代码得到结果

    image-20240319032615955

    可以发现,订阅者接收的信息有丢失(在launch文件中先开启的subscriber)

    原因是在publisher发送信息的时候,还未在roscore注册完毕,导致数据丢失

    解决方案:在循环前加入以下代码,延迟第一条信息的发送

    // 程序休眠,防止节点还未在roscore中注册时就发送消息
        ros::Duration(3).sleep();
    

    可以看到接收正常

    image-20240319033244631

程序运行时,可以新建终端输入指令rqt_graph,就可以看到计算图,帮助理解节点间关系

image-20240319033537197

话题通信自定义msg

作用:类似于C语言的结构体

在自定义msg时,可以适用的字段类型有

image-20240319040521435

话题通信自定义msg实现(C++)
  1. 自定义msg实现

    1. 功能包下新建msg目录,添加文件xxx.msg(注意后缀)

    2. 内容每行定义字段类型和字段名即可,回车换行

      string name
      uint16 age
      float64 height
      
    3. 修改package.xml,添加以下内容

      image-20240319041433751

    4. 修改CMakeLists.txt(没有的内容增加,被注释的解开)

      image-20240319041534299

      image-20240319041645673

      image-20240319041718312

      image-20240319041840122

    5. 编译

      编译后会在devel/include生成对应的头文件

      image-20240319041936167

  2. 后续代码使用自定义msg的准备工作

    配置.vscode/c_cpp_properties.json

    将自定义msg的头文件所在路径添加到"includePath"即可

    image-20240319042523814

  3. 发布者

    // 1.包含头文件 #include "demo_pub_sub/person.h"
    #include "ros/ros.h"
    #include "demo_pub_sub/person.h"
    
    int main(int argc, char *argv[])
    {
        setlocale(LC_ALL,"");
        // 2.初始化ros节点
        ros::init(argc,argv,"person_pub");
        // 3.创建节点句柄
        ros::NodeHandle nh;
        // 4.创建发布者对象
        ros::Publisher pub = nh.advertise<demo_pub_sub::person>("person",100);
        // 5.编写发布逻辑并发布数据
        // 5-1.创建被发布的数据
        demo_pub_sub::person person;
        person.age = 1;
        person.name = "haha";
        person.height = 1.83;
        // 5-2.设置发布频率
        ros::Rate rate(1);
        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++;
            rate.sleep();
            ros::spinOnce();
        }
    
        return 0;
    }
    

    在修改配置文件过程中,其他步骤与之前相同,额外增加一步

    image-20240319044306740

    测试时若rostopic echo "person"报错,则在终端重新执行source ./devel/setup.bash

    参考链接:ROS编译时出现Could not find a package configuration file provided by “gazebo_plugins“或gazebo_ros_control-CSDN博客

    image-20240319045952376

  4. 订阅者

    // 1.包含头文件 #include "demo_pub_sub/person.h"
    #include "ros/ros.h"
    #include "demo_pub_sub/person.h"
    
    void doperson(const demo_pub_sub::person::ConstPtr& p)
    {
        ROS_INFO("接收到的信息:姓名->%s,年龄->%d,身高->%.2f",p->name.c_str(),p->age,p->height);
    }
    
    int main(int argc, char *argv[])
    {
        setlocale(LC_ALL,"");
        // 2.初始化ros节点
        ros::init(argc,argv,"person_sub");
        // 3.创建节点句柄
        ros::NodeHandle nh;
        // 4.创建订阅者对象
        ros::Subscriber sub = nh.subscribe<demo_pub_sub::person>("person",100,doperson);
        // 5.处理订阅数据
        // 6.调用spin()函数
        ros::spin();
        return 0;
    }
    
  5. 测试结果

    image-20240319051902927

2.2 服务通信

概念:以请求响应的方式实现不同节点之间数据交互的通信模式

作用:用于偶然的、对时时性有要求、有一定逻辑处理需求的数据传输场景

适用场景:服务通信适用于对时时性有要求、具有一定逻辑处理的应用场景

理论模型

参考链接:063服务通信_理论模型_Chapter2-ROS通信机制_哔哩哔哩_bilibili

image-20240319053450967

服务通信自定义srv(C++)
  1. 自定义srv实现

    1. 功能包下新建srv目录,添加文件xxx.srv(注意后缀)

    2. 数据分成两部分,请求与响应,在 srv 文件中请求和响应使用---分割(srv文件内的可用数据类型与msg文件一致),回车换行

      # 客户请求
      int32 num1
      int32 num2
      ---
      # 服务器响应
      int32 sum
      
    3. 修改package.xml,添加以下内容(与自定义msg一样)

      image-20240319054445432

    4. 修改CMakeLists.txt(只有图2略有区别)

      image-20240319041534299

      image-20240319054622605

      image-20240319041718312

      image-20240319041840122

    5. 编译

      编译后会在devel/include生成对应的头文件

      image-20240319054902166

  2. 后续代码使用自定义srv的准备工作

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

    image-20240319042523814

  3. 服务器

    // 1.包含头文件 #include "demo_server_client/add_int.h"
    #include "ros/ros.h"
    #include "demo_server_client/add_int.h"
    
    bool donums(demo_server_client::add_int::Request &request,
                demo_server_client::add_int::Response &response)
    {
        // 1.处理请求
        int num1 = request.num1;
        int num2 = request.num2;
        ROS_INFO("这里是饲养员,让我偷偷告诉你%d加上%d等于多少!",num1,num2);
        // 2.组织响应
        int sum = num1 + num2;
        response.sum = sum;
        ROS_INFO("现在你知道了吗?");
        return true;
    }
    
    int main(int argc, char *argv[])
    {
        setlocale(LC_ALL,"");
        // 2.初始化ros节点
        ros::init(argc,argv,"gtt");
        // 3.创建节点句柄
        ros::NodeHandle nh;
        // 4.创建一个服务对象
        // 第一个参数为话题名字,第二个参数为回调函数
        ros::ServiceServer server = nh.advertiseService("speak",donums);
        ROS_INFO("饲养员已就位!");
        // 5.处理请求并产生响应
        // 6.调用spin()函数
        ros::spin();
        return 0;
    }
    

    修改配置文件多修改一步

    image-20240319061613197

    测试过程有两个容易出错的地方:

    1. 编译后记得执行source ./devel/setup.bash,否则会出现以下错误

      image-20240319062836312

    2. 测试指令

      image-20240319062914721

      a. 首先键入rosservice call speak(自己定义的话题名称),然后打一个空格,连续按两次tab,终端就会变成下图

      image-20240319063207278

      b. 用键盘的左右按键移到两个数字中进行编辑即可

      c. 按下回车终端中就会返回结果

  4. 客户端

    // 1.包含头文件 #include "demo_server_client/add_int.h"
    #include "ros/ros.h"
    #include "demo_server_client/add_int.h"
    
    int main(int argc, char *argv[])
    {
        setlocale(LC_ALL,"");
        // 2.初始化ros节点
        ros::init(argc,argv,"zxs");
        // 3.创建节点句柄
        ros::NodeHandle nh;
        // 4.创建一个客户端对象
        ros::ServiceClient client = nh.serviceClient<demo_server_client::add_int>("speak");
        ROS_INFO("张臭臭已就位!");
        // 5.提交请求并处理响应
        demo_server_client::add_int zcc;
        // 5-1.组织请求
        zcc.request.num1 = 2;
        zcc.request.num2 = 3;
        ROS_INFO("你好饲养员,请问%d加上%d等于多少",zcc.request.num1,zcc.request.num2);
        // 5-2.处理响应
        bool flag = client.call(zcc);
        if(flag)
        {
            ROS_INFO("臭臭知道了,%d加上%d等于%d,所以我要",zcc.request.num1,zcc.request.num2,zcc.response.sum);
            while(zcc.response.sum--)
            {
                ROS_INFO("哼");
            }
        }else {
            ROS_INFO("臭臭太笨了,臭臭不会呜呜呜");
        }
        return 0;
    }
    
  5. 测试结果

  6. 对于客户端的优化实现(通过命令行输入参数,增加交互)

    // 1.包含头文件 #include "demo_server_client/add_int.h"
    #include "ros/ros.h"
    #include "demo_server_client/add_int.h"
    
    // 优化实现(执行时不用launch)
        // 实现参数的动态提交
        // 1.格式 rosrun xxx xxx num1 num2
        // 2.节点执行时,需要获取命令中的参数,并组织进request
    int main(int argc, char *argv[])
    {
        setlocale(LC_ALL,"");
            
        // 优化实现
        if(argc != 3){
            ROS_INFO("臭臭说的好像不对!");
            return 1;
        }
        // 2.初始化ros节点
        ros::init(argc,argv,"zxs");
        // 3.创建节点句柄
        ros::NodeHandle nh;
        // 4.创建一个客户端对象
        ros::ServiceClient client = nh.serviceClient<demo_server_client::add_int>("speak");
        ROS_INFO("张臭臭已就位!");
        // 5.提交请求并处理响应
        demo_server_client::add_int zcc;
        // 5-1.组织请求
        zcc.request.num1 = atoi(argv[1]); // atoi()转化为整数
        zcc.request.num2 = atoi(argv[2]);
        ROS_INFO("你好饲养员,请问%d加上%d等于多少",zcc.request.num1,zcc.request.num2);
        // 5-2.处理响应
        bool flag = client.call(zcc);
        if(flag)
        {
            ROS_INFO("臭臭知道了,%d加上%d等于%d,所以我要",zcc.request.num1,zcc.request.num2,zcc.response.sum);
            while(zcc.response.sum--)
            {
                ROS_INFO("哼");
            }
        }else {
            ROS_INFO("臭臭太笨了,臭臭不会呜呜呜");
        }
        return 0;
    }
    

    运行结果:

    成功:

    QQ图片20240319071602

    失败:

    image-20240319071652127

Note

先启动客户端,未启动服务端如下图

image-20240319072118643

若要进行优化(当服务器未启动,客户端先启动时,将客户端挂起,而非进行错误处理,当服务端启动成功后才会继续执行

执行以下代码中的一句

client.waitForExistence();
ros::service::waitForService("xxx(话题)");

2.3 参数服务器

概念:以共享的方式实现不同节点之间数据交互的通信模式

作用:存储一些多节点共享的数据,类似于全局变量

适用场景:一般适用于存在数据共享的一些应用场景

理论模型

参考链接:077参数服务器_理论模型_Chapter2-ROS通信机制_哔哩哔哩_bilibili

image-20240319073506332

参数可使用的数据类型
  • 32-bit integers
  • booleans
  • strings
  • doubles
  • iso8601 dates(时间的表示方法)
  • lists
  • base64-encoded binary data
  • 字典

注意:参数服务器不是为高性能而设计的,因此最好用于存储静态的非二进制的简单数据

对于这句话的理解(来自gpt)可能这句话的意思是直接存放解析好的有意义的数据,比如说电机回传往往是0-65535之间的二进制数据,这里的意思是建议存储对应的解析到0-360的数据

image-20240319074009840

参数操作
参数服务器新增(修改)参数

主要依赖以下两个函数:

nh.setParam("键",值);
ros::param::set("键",值);

初次设置键为新增,再次调用相同键为修改

#include "ros/ros.h"

/*
    实现参数的新增与修改
    首先设置机器人的共享参数,类型,名字,体重(86kg)
    再修改体重(80kg)
    实现:
        ros::NodeHandle setParam()
        ros::param set()
*/

int main(int argc, char *argv[])
{
    // 初始化ros节点
    ros::init(argc,argv,"set_param");
    // 创建ros节点句柄
    ros::NodeHandle nh;
    // 参数增
    // 方案1:nh
    nh.setParam("type","pig");
    nh.setParam("name","zxs");
    nh.setParam("weight",86);
    // 方案2:ros::param
    ros::param::set("type_param","pig");
    ros::param::set("name_param","zxs");
    ros::param::set("weight_param",86);
    //参数改
    // 方案1:nh
    nh.setParam("weight",80);
    // 方案2:ros::param
    ros::param::set("weight_param",80);
    return 0; 
}

运行结果:

image-20240320190302257

image-20240320190600913

参数服务器获取参数

常用函数:

ros::NodeHandle
    
		param(,默认值) 
            存在,返回对应结果,否则返回默认值

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

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

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

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

        searchParam(参数1,参数2)
            搜索键,参数1是被搜索的键,参数2存储搜索结果的变量(实测参数2存储的是被搜索的键的名字)

运行代码:

#include "ros/ros.h"

/*
    演示参数查询
    实现:
        ros::NodeHandle
        ros::param 
*/

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");
    // 初始化ros节点
    ros::init(argc,argv,"set_param");
    // 创建ros节点句柄
    ros::NodeHandle nh;
    // nodehandle
    // 1.param
    double weight = nh.param("weight",100);
    ROS_INFO("weight = %.2f",weight);
    // 2.getParam
    double getParam_test = 0;
    bool result = nh.getParam("weight",getParam_test);
    if(result)
    {
        ROS_INFO("zxs的体重是%.2fkg",getParam_test);
    }else{
        ROS_INFO("被查询的变量不存在");
    }
    // 3.getParamNames
    std::vector<std::string> names;
    nh.getParamNames(names);
    for(auto &&name : names)
    {
        ROS_INFO("遍历的元素名字:%s",name.c_str());
    }
    // 4.hasParam
    bool flag1 = nh.hasParam("weight");
    bool flag2 = nh.hasParam("pig");
    ROS_INFO("weight存在吗?存在:1,不存在:0  回答:%d",flag1);
    ROS_INFO("pig存在吗?存在:1,不存在:0  回答:%d",flag2);
    // 5.searchParam
    std::string key;
    nh.searchParam("name",key);
    ROS_INFO("pig的名字是%s",key.c_str());
    return 0; 
}
  • param(键,默认值)

与demo_param_set一起运行时:

image-20240320194450309

单独运行时:

image-20240320194621121

  • getParam(键,存储结果的变量)

与demo_param_set一起运行时:

image-20240320195400245

单独运行时:

image-20240320195601400

分析可以得知,虽然在前面定义了参数weight,但是查询结果仍然返回false,证明这两个weight并非同一个weight,仅仅是声明的话,并没有将其放入参数服务器

  • getParamCached(键,存储结果的变量)–提高变量获取效率

效果与getParam相同,这里不做演示

提升效率的原理:getParam的工作原理是不管这个参数之前是否调用过,在查询时都会从服务器获取查询,getParamCached回首先在本地的缓冲区查看是否过,如果已经调用过参数,会直接从本地查询,提高效率

  • getParamNames(std::vectorstd::string)

image-20240320201047637

  • hasParam(键)

image-20240320201526284

  • searchParam(参数1,参数2)

与demo_param_set一起运行时:

实测返回的是键名,而非键的值

image-20240320202234015

单独运行时:

未返回结果

image-20240320202339411

PS:ros::paramros::NodeHandle对应的函数及用法是一致的,就不再展开

image-20240320203004851

  • param对应param
  • get对应getParam
  • getCached对应getParamCached
  • getParamNames对应getParamNames
  • has对应hasParam
  • search对应searchParam
参数服务器删除参数
#include "ros/ros.h"

/*
    演示参数的删除
    实现:
        ros::NodeHandle delParam()
        ros::param del()
*/

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");
    // 初始化ros节点
    ros::init(argc,argv,"set_param");
    // 创建ros节点句柄
    ros::NodeHandle nh;
    // 删除
    // 方案1:nh
    bool flag1 = nh.deleteParam("type");
    if(flag1){
        ROS_INFO("方案1删除成功!");
    }else{
        ROS_INFO("方案1删除失败!");
    }
    // 方案2:ros::param
    bool flag2 = ros::param::del("type_param");
    if(flag1){
        ROS_INFO("方案2删除成功!");
    }else{
        ROS_INFO("方案2删除失败!");
    }
    return 0;
}

初次运行删除成功,再次运行删除失败(已被删除)

image-20240320205030070

2.4 通信机制比较

参数服务器是一种数据共享机制,可以在不同的节点之间共享数据

话题通信服务通信是在不同的节点之间传递数据

话题通信服务通信比较

A.都涉及四个要素:

  • 要素1: 消息的发布方/客户端(Publisher/Client)
  • 要素2: 消息的订阅方/服务端(Subscriber/Server)
  • 要素3: 话题名称(Topic/Service)
  • 要素4: 数据载体(msg/srv)

B.对比

image-20240320170009663

服务同步性指服务端和客户端基于请求响应的模式,按照顺序依次处理信息(服务端处理完,客户端才继续发送)。

话题异步性指发布方和订阅方在处理信息时没有严格顺序要求,并非只有订阅方处理完信息发布方才可继续发送。

2.5 常用命令

http://wiki.ros.org/ROS/CommandLineTools

文件操作命令是静态的,操作的是磁盘上的文件,而以下命令是动态的,在ROS程序启动后,可以动态的获取运行中的节点或参数的相关信息

常用命令类型:

  • rosnode : 操作节点
  • rostopic : 操作话题
  • rosservice : 操作服务
  • rosmsg : 操作msg消息
  • rossrv : 操作srv消息
  • rosparam : 操作参数

ps:感觉他们的关系是

rostopicrosmsg对应话题通信

rossrervicerossrv对应服务通信

rostopicrossrervice对应活动

rosmsgrossrv对应消息

2.5.1 rosnode
rosnode ping    测试到节点的连接状态
rosnode list    列出活动节点
rosnode info    打印节点信息
rosnode machine    列出指定设备上节点
rosnode kill    杀死某个节点
rosnode cleanup    清除不可连接的节点
2.5.2 rostopic
rostopic bw     显示主题使用的带宽
rostopic delay  显示带有 header 的主题延迟
rostopic echo   打印消息到屏幕
rostopic find   根据类型查找主题
rostopic hz     显示主题的发布频率
rostopic info   显示主题相关信息
rostopic list   显示所有活动状态下的主题
rostopic pub    将数据发布到主题
rostopic type   打印主题类型

eg:rostopic pub

rostopic pub /主题名称 消息类型 消息内容
rostopic pub -r 频率 /主题名称 消息类型 消息内容

tip:可以rostopic pub /主题名称然后敲两次回车,自动补全后面的信息

image-20240320232640286

2.5.3 rosmsg
rosmsg show    显示消息描述
rosmsg info    显示消息信息
rosmsg list    列出所有消息
rosmsg md5    显示 md5 加密后的消息
rosmsg package    显示某个功能包下的所有消息
rosmsg packages    列出包含消息的功能包
2.5.4 rosservice
rosservice args 打印服务参数
rosservice call    使用提供的参数调用服务
rosservice find    按照服务类型查找服务(这个应该是给服务类型打印出话题名字)
rosservice info    打印有关服务的信息
rosservice list    列出所有活动的服务
rosservice type    打印服务类型(这个应该是给话题名字打印出服务类型)
rosservice uri    打印服务的 ROSRPC uri

eg:rosservice find msg-type

推测这个msg-type的意思应该是定义的xxx.srv的名字,即服务的类型,可能需要加上包名以便于查找(rosservice find add_int无响应)

image-20240320234630637

2.5.5 rossrv
rossrv show    显示服务消息详情
rossrv info    显示服务消息相关信息
rossrv list    列出所有服务信息
rossrv md5    显示 md5 加密后的服务消息
rossrv package    显示某个包下所有服务消息
rossrv packages    显示包含服务消息的所有包
2.5.6 rosparam
rosparam set    设置参数
rosparam get    获取参数
rosparam load    从外部文件加载参数
rosparam dump    将参数写出到外部文件
rosparam delete    删除参数
rosparam list    列出所有参数
  • 18
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咕噜咕噜咕噜噜噜噜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值