• ROS学习笔记04、ROS通信机制进阶—函数与自定义头文件(C++版)


    前言

    马上开学,目前学校很多实验室都是人工智能这块,大部分都是和机器人相关,然后软件这块就是和cv、ros相关,就打算开始学习一下。

    本章节是虚拟机安装Ubuntu18.04以及安装ROS的环境。

    学习教程:【Autolabor初级教程】ROS机器人入门,博客中一些知识点是来源于赵老师的笔记在线笔记,本博客主要是做归纳总结,如有侵权请联系删除。

    视频中的案例都基本敲了遍,这里给出我自己的源代码文件:

    链接:https://pan.baidu.com/s/13CAzXk0vAWuBsc4oABC-_g 
    提取码:0hws 
    

    所有博客文件目录索引:博客目录索引(持续更新)

    一、常用函数

    常用API:ROS-wiki-apisroscpp文档

    初始化函数(init(),含额外用处说明)

    额外用处总结:①可借助传入init中的argv,通过再ros命令中传入参数键值对来保存key,value键值到param server。②设置第四个参数即可实现单个节点文件可多次同时运行(动态修改节点名称)。

    /**
    	该函数可以解析并使用节点启动时传入的参数(通过参数设置节点名称、命名空间...) 
    	该函数有多个重载版本,如果使用NodeHandle建议调用该版本。 
    	param argc 参数个数
    	param argv 参数列表
    	param name 节点名称,需要保证其唯一性,不允许包含命名空间
    	param options 节点启动选项,被封装进了ros::init_options
     */
    void init(int &argc, char **argv, const std::string& name, uint32_t options = 0);
    

    用处1:通过ros命令启动节点,传入指定格式的参数即可实现向param服务存储key,value信息。

    image-20220909204347308

    # 运行该发布服务
    # _length:=2  的效果,会传入值到argv,接着随着我们的ros::init传入进去,就会识别到去注册param,键值对为【服务名/key=value】,在这里也就是/control/length 2
    rosrun communication_practice turtle_control _length:=2
    

    我们使用ros命令来看下效果:

    rosparam list
    
    rosparam get /control/length
    

    image-20220909204849279

    用处2:单节点文件多次启动,可设置init()函数中的第四个options参数为ros::init_options::AnonymousName

    效果:在每次启动该节点的时候,节点的名称为【``节点_时间戳`】,这样的好处就是单个文件可以重复运行多次。

    初始默认为false,也就是一个节点文件只能运行一次,若是第二个窗口去运行就会因为节点名字重复将原本启动的杀死,再执行当前的服务:

    image-20220909205252408

    我们来设置一下参数:

    ros::init(argc, argv, "control", ros::init_options::AnonymousName);
    

    image-20220909205851823

    来测试一下(修改后需编译):

    image-20220909205949902

    source ./devel/setup.bash
    
    # 前两个窗口去运行下面命令(都能够正常进行发布rosnode list)
    rosrun communication_practice turtle_control
    
    # bash窗口执行下面测试(查看节点名称)
    rosnode list
    

    image-20220909210153205


    话题与服务相关对象

    话题与服务相关对象

    //发布者对象
    ros::Publisher pub = nh.advertise("chatter", 10);
    //订阅者对象
    ros::Subscriber sub = nh.subscribe("chatter", 10, doMsg);
    //服务端对象
    ros::ServiceServer server = nh.advertiseService("AddInts", doReq);
    //客户端对象
    ros::ServiceClient client = nh.serviceClient("AddInts");
    

    发布者对象额外细节点

    示例:若是发布者先将10条数据发出去了,那么此时订阅端再启动就无法接收到最后一条数据,那么此时有一个需求就是发布者发送完了之后订阅端服务启动依旧能够获取到最后一条数据。

    使用方式:也就是=在发布者对象实例化的时候第四个参数设置为true即可。

    场景:静态地图发布。

    • 方案一:使用固定频率发送地图数据,效率低。
    • 方案二:将地图发布对象的latch设置为true,此时发布方只发送一条数据并保持不断线,每当订阅者连接到发布端时,发不断就可以将最后一次(也就是那一条数据)发送给订阅端,这样提高了数据的发送效率。

    使用与不使用对比

    不使用:

    ros::Publisher pub = nh.advertise("chatter", 10);
    

    image-20220909212937666

    • 对于发送9条停止发送逻辑如下:
    • image-20220909213053877

    此时启动订阅方:一条数据都没有接收到

    image-20220909213007030

    使用:

    image-20220909213151331

    ros::Publisher pub = nh.advertise<std_msgs::String>("chatter", 10, true);
    

    同上先执行完发布方,接着执行订阅方:

    image-20220909213313355

    可以看到确实能够访问到最后一条记录!!!

    上方使用的代码是plumbing_pub_sub包中的代码,对应的ros执行命令如下所示:

    # 刷新环境变量
    source ./devel/setup.bash
       
    # 执行发布者节点
    rosrun plumbing_pub_sub demo01_pub
    
    # 执行发布者节点
    rosrun plumbing_pub_sub demo01_sub
    

    回旋函数

    //处理一轮回调
    void spinOnce();
    
    //进入循环处理回调
    void spin();
    

    相同点:二者都用于处理回调函数;

    不同点:ros::spin() 是进入了循环执行回调函数,而 ros::spinOnce() 只会执行一次回调函数(没有循环),在 ros::spin() 后的语句不会执行到,而 ros::spinOnce() 后的语句可以执行。

    时间函数

    设置或指定时刻(Time,时间戳)

    #include "ros/ros.h"
    
    int main(int argc, char *argv[])
    {
        setlocale(LC_ALL, "");
    
        ros::init(argc, argv, "hello_time");
        //初始化句柄(若不初始化后面的时间函数无法使用)
        ros::NodeHandle nh;
    
        //获取当前时刻对象
        ros::Time right_now = ros::Time::now();
        ROS_INFO("当前时刻:%.2f", right_now.toSec());//获取距离 1970年01月01日 00:00:00 的秒数 (浮点值)
        ROS_INFO("当前时刻:%d", right_now.sec);//获取距离 1970年01月01日 00:00:00 的秒数 (整数值 )
    
        //指定时刻对象
        //需求:指定当前时刻为距离1970年01月01日 00:00:00,10秒31纳秒
        ros::Time t1(10, 311456789);
        ROS_INFO("指定时刻:%f", t1.toSec());
        //或直接设置在第一个参数中
        ros::Time t2(10.35);
        ROS_INFO("指定时刻:%f", t2.toSec());
    
        return 0;
    }
    

    image-20220910123948363

    持续时间(Duration,阻塞睡眠)

    //2、demo2:持续时间
    ROS_INFO("当前时刻:%.2f",ros::Time::now().toSec());
    ros::Duration du(10);//持续10秒钟,参数是double类型,以秒为单位
    du.sleep();//阻塞睡眠
    ROS_INFO("持续时间:%.2f", du.toSec());
    ROS_INFO("当前时刻:%.2f", ros::Time::now().toSec());
    

    image-20220910124401625

    持续时间与时刻时间运算

    //3、demo3
    //time与duration时间进行运算(time = time + duration、time = time - duration)
    ros::Time now = ros::Time::now();
    ros::Duration du1(10);
    ros::Duration du2(5);
    ros::Time after_now = now + du1;
    ros::Time before_now = now - du1;
    ROS_INFO("当前时刻之后:%.2f",after_now.toSec());
    ROS_INFO("当前时刻之前:%.2f",before_now.toSec());
    
    //duration之间进行运算:duration = duration + duration、duration = duration - duration
    ros::Duration du3 = du1 + du2;
    ros::Duration du4 = du1 - du2;
    ROS_INFO("du3 = %.2f",du3.toSec());
    

    image-20220910133103803

    指定运行频率(Rate)

    ros::Rate rate(1);//指定频率
    while (true)
    {
        ROS_INFO("-----------code----------");
        rate.sleep();//休眠,休眠时间 = 1 / 频率。
    }
    

    定时器(Timer,自动执行与手动执行、无限执行与一次执行)

    完整源码API:

    //period:持续的时间
    //callback:回调函数
    //oneshot:false表示无限执行,true表示执行一次。
    //autostart:true表示自动执行该定时器,false表示手动调用start才能够执行定时器。
    Timer createTimer(Duration period, const TimerCallback& callback, bool oneshot = false,bool autostart = true) const;
    

    示例:包含自动执行与手动执行、无限执行与一次执行。

    image-20220910141004883

    #include "ros/ros.h"
    
    void doTask(const ros::TimerEvent &event) {
        ROS_INFO("-------------");
        ROS_INFO("event:%s",std::to_string(event.current_real.toSec()).c_str());
    }
    
    int main(int argc, char *argv[])
    {
        setlocale(LC_ALL, "");
    
        ros::init(argc, argv, "hello_time");
        //初始化句柄(若不初始化后面的时间函数无法使用)
        ros::NodeHandle nh;
        //4、定时器
        //自动执行与手动执行
        ROS_INFO("自动执行...");
        //第三个参数默认是false:无限执行;若是设置为true,表示只执行一次!
        //第四个参数默认是true,表示是自动执行;若是设置为false,那么就需要手动调用timer.start()才能够执行。
        ros::Timer timer = nh.createTimer(ros::Duration(1), doTask, true, false);
        timer.start();
    
        ros::spin();
    
        return 0;
    }
    

    image-20220910134733476


    二、 其他函数

    node节点关闭函数

    C++中可以通过 ros::ok() 来判断节点状态是否正常,导致节点退出的原因主要有如下几种:

    • 节点接收到了关闭信息,比如常用的 ctrl + c 快捷键就是关闭节点的信号;
    • 同名节点启动,导致现有节点退出;
    • 程序中的其他部分调用了节点关闭相关的API(C++中是ros::shutdown()。【当前章节就是去实现节点关闭】

    image-20220910140942961

    #include "ros/ros.h"
    
    int main(int argc, char *argv[])
    {
        setlocale(LC_ALL, "");
        ros::init(argc, argv, "demo_node");
        ros::NodeHandle nh;
    
        int count = 0;
        ros::Rate rate(1);
        while (ros::ok()) {
            count++;
            ROS_INFO("hello...");
            if (count > 5) {
                ROS_INFO("节点关闭!");
                //关闭结点运行(直接退出程序)
                ros::shutdown();
            }else {
                rate.sleep();
            }
        }
        //一旦调用ros::shutdown(),那么该程序会直接终止,不会继续向下执行!!!
        ROS_INFO("666");
        return 0;
    }
    

    配置好CMakeLists.txt之后我们来尝试运行:

    source ./devel/setup.bash
    
    rosrun helloworld demo_node
    

    image-20220910140808497


    日志信息级别打印

    image-20220910142214534

    #include "ros/ros.h"
    
    int main(int argc, char *argv[])
    {
        setlocale(LC_ALL, "");
        ros::init(argc, argv, "log_node");
        ROS_DEBUG("debug级别(调试)");
        ROS_INFO("INFO级别(信息)");
        ROS_WARN("WARN级别(警告)");
        ROS_ERROR("ERROR级别(错误)");
        ROS_FATAL("FATAL级别(严重错误)");
        return 0;
    }
    

    配置好CMakeLists.txt之后进行运行:

    source ./devel/setup.bash
    
    rosrun helloworld demo_node
    

    image-20220910142254621

    三、自定义头文件与源文件

    额外说明(自定义代码高亮声明)

    对于自定义的头文件,我们需要将其路径引入到vscode中的c_cpp_properties.json:

    image-20220910171640045

    3.1、自定义头文件

    目标:自定义头文件,然后在对应的main方法的文件中实现该头文件的方法并进行调用。

    1、创建新工程包

    # 进入工程目录中的src目录下
    cd /home/workspace/roslearn/src
    
    # 创建功能包名为plumbing_head
    catkin_create_pkg --rosdistro melodic plumbing_head roscpp rospy std_msgs
    

    image-20220910171003152

    2、在include/plumbing_head目录下创建hello.h头文件

    #ifndef _HELLO_H
    #define _HELLO_H
    
    namespace hello_ns {
        class HelloPub {
            public: 
                void run();
        };
    }
    
    #endif
    

    3、在src目录下创建源文件,其中编写对应HelloPub类中的实现方法

    #include "ros/ros.h"
    #include "plumbing_head/hello.h"
    
    namespace hello_ns{
    
        void HelloPub::run() {
            ROS_INFO("自定义头文件的使用...");
        }   
    }
    
    int main(int argc, char *argv[])
    {   
        setlocale(LC_ALL, "");
        ros::init(argc, argv, "hello_node");
        hello_ns::HelloPub helloPub;
        helloPub.run();
        return 0;
    }
    

    最终在CMakeLists.txt中进行配置:

    # 放开include
    include_directories(
      include
      ${catkin_INCLUDE_DIRS}
    )
    
    add_executable(hello src/hello.cpp)
    
    add_dependencies(hello ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
    
    target_link_libraries(hello
      ${catkin_LIBRARIES}
    )
    

    编译项目,并来运行服务:

    source ./devel/setup.bash
    
    rosrun plumbing_head hello
    

    image-20220910171218229

    3.2、自定义源文件

    1、创建新的功能包

    # 进入工程目录中的src目录下
    cd /home/workspace/roslearn/src
    
    # 创建功能包名为plumbing_head_src
    catkin_create_pkg --rosdistro melodic plumbing_head_src roscpp rospy std_msgs
    

    image-20220910171330312

    2、在include/plumbing_head_src目录下创建头文件:haha.h

    #ifndef _HAHA_H
    #define _HAHA_H
    
    namespace hello_ns{
        class My {
            public:
                void run();
        };
    } 
    
    #endif
    

    3、自定义源文件去实现头文件的声明方法,在src中创建:haha.cpp

    #include "plumbing_head_src/haha.h"
    #include "ros/ros.h"
    
    namespace hello_ns {
        void My::run() {
            ROS_INFO("hello, head and src ....");
        }
    }
    

    4、接着去写我们实际的main方法,引入对应的头文件即可进行使用,在src目录下创建:src_test.cpp

    #include "ros/ros.h"
    #include "plumbing_head_src/haha.h"
    
    int main(int argc, char *argv[])
    {
        setlocale(LC_ALL, "");
        ros::init(argc, argv, "src_test_node");
        
        hello_ns::My my;
        my.run();
    
        return 0;
    }
    

    接着我们需要给我们的源文件与执行文件进行配置,在CMakeLists.txt中进行配置修改:

    针对源文件haha.cpp的配置:

    # 放开include配置
    include_directories(
      include
      ${catkin_INCLUDE_DIRS}
    )
    
    # 声明C++库
    add_library(head_src
      include/plumbing_head_src/haha.h
      src/haha.cpp
    )
    
    add_dependencies(head_src ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
    
    target_link_libraries(head_src
      ${catkin_LIBRARIES}
    )
    

    针对于执行文件src_test.cpp文件:

    add_executable(src_test src/src_test.cpp)
        
    add_dependencies(src_test ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
    
    target_link_libraries(src_test
      head_src
      ${catkin_LIBRARIES}
    )
    

    最后我们来编译整个项目,并进行刷新配置运行:

    source ./devel/setup.bash
    
    rosrun plumbing_head_src src_test
    

    image-20220910172226271

  • 相关阅读:
    Vue 实现拖拽模块
    分组函数之rollup、cube函数、grouping sets函数
    万字深剖 Linux I/O 原理
    QOS功能介绍
    基于springboot的校园店铺系统
    使用 Power Shell 修改 Hyper-V 虚拟机 UUID 的解决方案
    201.回溯算法:全排列(力扣)
    XLua中lua读写cs对象的原理
    k8s高可用
    【数据结构】线性表(三)循环链表的各种操作(创建、插入、查找、删除、修改、遍历打印、释放内存空间)
  • 原文地址:https://blog.csdn.net/cl939974883/article/details/126962710