(八)ROS通信机制进阶

前言

本文详细介绍了ROS中的C++的常用API,包括节点初始化、话题与服务对象创建、回旋函数、时间管理和日志功能。同时,探讨了ROS头文件和源文件的调用,包括自定义头文件和源文件的使用方法。内容涵盖了从节点初始化到程序运行控制的各个方面,是理解ROS程序设计的重要参考资料。 

一、常用API

1.init函数

新建一个工作空间demo04_ws,新建一个功能包plumbing_apis,新建文件demo_apis_pub.cpp。

流程如下:

ros:foxy(1) noetic(2) ?
2
lvl@lvl-Legion-Y7000P-IRX9:~$ mkdir -p ~/demo04_ws/src
lvl@lvl-Legion-Y7000P-IRX9:~$ cd demo04_ws/src
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws/src$ catkin_init_workspace
Creating symlink "/home/lvl/demo04_ws/src/CMakeLists.txt" pointing to "/opt/ros/noetic/share/catkin/cmake/toplevel.cmake"
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws/src$ cd ../
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ catkin_make
Base path: /home/lvl/demo04_ws
Source space: /home/lvl/demo04_ws/src
Build space: /home/lvl/demo04_ws/build
Devel space: /home/lvl/demo04_ws/devel
Install space: /home/lvl/demo04_ws/install
####
#### Running command: "cmake /home/lvl/demo04_ws/src -DCATKIN_DEVEL_PREFIX=/home/lvl/demo04_ws/devel -DCMAKE_INSTALL_PREFIX=/home/lvl/demo04_ws/install -G Unix Makefiles" in "/home/lvl/demo04_ws/build"
####
-- The C compiler identification is GNU 9.4.0
-- The CXX compiler identification is GNU 9.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Using CATKIN_DEVEL_PREFIX: /home/lvl/demo04_ws/devel
-- Using CMAKE_PREFIX_PATH: /home/lvl/catkin_ws/devel;/opt/ros/noetic
-- This workspace overlays: /home/lvl/catkin_ws/devel;/opt/ros/noetic
-- Found PythonInterp: /usr/bin/python3 (found suitable version "3.8.10", minimum required is "3") 
-- Using PYTHON_EXECUTABLE: /usr/bin/python3
-- Using Debian Python package layout
-- Found PY_em: /usr/lib/python3/dist-packages/em.py  
-- Using empy: /usr/lib/python3/dist-packages/em.py
-- Using CATKIN_ENABLE_TESTING: ON
-- Call enable_testing()
-- Using CATKIN_TEST_RESULTS_DIR: /home/lvl/demo04_ws/build/test_results
-- Forcing gtest/gmock from source, though one was otherwise available.
-- Found gtest sources under '/usr/src/googletest': gtests will be built
-- Found gmock sources under '/usr/src/googletest': gmock will be built
-- Found PythonInterp: /usr/bin/python3 (found version "3.8.10") 
-- Found Threads: TRUE  
-- Using Python nosetests: /usr/bin/nosetests3
-- catkin 0.8.10
-- BUILD_SHARED_LIBS is on
-- BUILD_SHARED_LIBS is on
-- Configuring done
-- Generating done
-- Build files have been written to: /home/lvl/demo04_ws/build
####
#### Running command: "make -j28 -l28" in "/home/lvl/demo04_ws/build"
####
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ cd src
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws/src$ catkin_create_pkg plumbing_apis std-msgs roscpp rospy
Created file plumbing_apis/package.xml
Created file plumbing_apis/CMakeLists.txt
Created folder plumbing_apis/include/plumbing_apis
Created folder plumbing_apis/src
Successfully created files in /home/lvl/demo04_ws/src/plumbing_apis. Please adjust the values in package.xml.
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws/src$ cd plumbing_apis/src
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws/src/plumbing_apis/src$ touch demo_apis_pub.cpp

编辑c++文件:

#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>
/*
        发布方实现
                 1.包含头文件
                 2.初始化ros节点
                 3.创建节点句柄
                 4.创建发布者对象
                 5.编写发布逻辑并发布数据
*/
int main(int argc ,char * argvs[])
{
    setlocale(LC_ALL,"");
    ros::init(argc,argvs,"erguizi",ros::init_options::AnonymousName);  //节点名称为erguizi
    ros::NodeHandle nh;
    ros::Publisher pub = nh.advertise<std_msgs::String>("house",10);    //话题为房子,队列中最多缓存十条数据
 
    //5-1.先创建没有被发布的信息
    std_msgs::String msg;
    ros::Rate rate(10);        //10hz
    int count = 0;
    //5-2.编写循环,循环中发布数据
    ros::Duration(3).sleep();   //程序执行到这里休眠三秒钟
    while(ros::ok())    //只要节点活着,则循环继续
    {
        count ++;
        //实现字符串拼接
        std::stringstream ss;
        ss << "hello --->"<<count;
        msg.data = ss.str();
        //msg.data = "hello";
        pub.publish(msg);
        //添加日志
        ROS_INFO("发布的数据是:%s",ss.str().c_str());
        rate.sleep();                        //睡觉0.1s
    }
    return 0;
}

ROS::init参数介绍

1.argc:封装实参的个数(传入n个参数,argc = 参数+1,因为第一个参数是文件名)

2.argv:封装参数的指针数组(字符串类型)

3.name:为节点命名,是字符串类型的,有唯一性

4.option:节点启动选项

返回值:void

argc 与 argv 的使用

如果按照ros中的特定格式传入实参,那么ROS可以加以使用,比如可以设置全局参数,给节点重命名

启动节点为参数服务器设置参数,采用一个下划线一个参数名冒号等号具体值,这时下划线length就被传递给argv了。

lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ rosrun plumbing_apis apis _length:=2
[ INFO] [1736577681.802182066]: 发布的数据是:hello --->1
[ INFO] [1736577681.804844789]: 发布的数据是:hello --->2
....
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ rosparam list
/rosdistro
/roslaunch/uris/host_lvl_legion_y7000p_irx9__45093
/rosversion
/run_id
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ rosrun plumbing_apis apis _length:=2
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ rosparam list
/erguizi_1736577943122039222/length
/rosdistro
/roslaunch/uris/host_lvl_legion_y7000p_irx9__45093
/rosversion
/run_id

查看参数服务器发现多了length参数,再查看其值:

lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ rosparam get /erguizi_1736577943122039222/length
2

options的使用

节点名称要保证唯一性,会导致一个问题:同一个节点不能重复启动,如果重复启动之前启动的会被关闭

需求:特定场景下需要一个节点多次启动且能正常运行,怎么办呢?使用options参数

 ros::init(argc,argvs,"erguizi",ros::init_options::AnonymousName);  //节点名称为erguizi

2.话题与服务相关的对象

在roscpp中,话题和服务相关对象一般由 NodeHandle 创建。

1. advertise函数

作用:创建发布者对象

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

模板:被发布消息的对象 <std_msgs::String>

参数:

1.参数名称及作用

2.话题名称:house,string类型

3.队列长度:缓冲区最多可容纳数据

4.latch(可选):如果设置为true,会保存发送方的最后一条信息,并且新的连接对象链接到发布方时,发布方会将这条信息发送给订阅者,默认值为false即不会保存

5.应用场景:即latch设置为true的作用

场景机器人导航时,需要一个静态地图,地图通过发布方来发布,如何设置发布频率,如果是一秒钟10HZ,地图是静态长期不会变,一个解决方案是只发一次地图信息,但将发布对象的latch对象值设置为true,这样意味着当订阅者链接到话题时候,就会将地图发送给订阅方。

6.code:对比两种不同方式

原来的代码(没有设置latch)

#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>
 
int main(int argc ,char * argvs[])
{
    setlocale(LC_ALL,"");
    ros::init(argc,argvs,"erguizi",ros::init_options::AnonymousName);  
    ros::NodeHandle nh;
    ros::Publisher pub = nh.advertise<std_msgs::String>("house",10);    
 
    std_msgs::String msg;
    ros::Rate rate(10);        
    int count = 0;
    ros::Duration(3).sleep();   
 
    while(ros::ok())    
    {
        count ++;
        std::stringstream ss;
        ss << "hello --->"<<count;
        msg.data = ss.str();
        if(count<=10)
        {
            pub.publish(msg);
            ROS_INFO("发布的数据是:%s",ss.str().c_str());
        }
        rate.sleep();                     
    }
    return 0;
}

当发布完10条消息后,此时订阅方接受不到任何消息。

当将latch设置成true时:

#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>
 
int main(int argc ,char * argvs[])
{
    setlocale(LC_ALL,"");
    ros::init(argc,argvs,"erguizi",ros::init_options::AnonymousName);  
    ros::NodeHandle nh;
    ros::Publisher pub = nh.advertise<std_msgs::String>("house",10);    
 
    std_msgs::String msg;
    ros::Rate rate(10);        
    int count = 0;
    ros::Duration(3).sleep();   
 
    while(ros::ok())    
    {
        count ++;
        std::stringstream ss;
        ss << "hello --->"<<count;
        msg.data = ss.str();
        if(count<=10)
        {
            pub.publish(msg);
            ROS_INFO("发布的数据是:%s",ss.str().c_str());
        }
        rate.sleep();                     
    }
    return 0;
}

发现订阅方可以订阅到最后一条数据

返回值: ros::Publisher型数据,用来创建发布方

3.回旋函数

1. 在ROS程序中,频繁的使用了 ros::spin() 和 ros::spinOnce() 两个回旋函数,可以用于处理回调函数

2.spin与spinonce区别

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

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

4.时间

ROS中时间相关的API是极其常用,比如:获取当前时刻、持续时间的设置、执行频率、休眠、定时器...都与时间相关。

1.时刻

作用:获取时刻,或是设置指定时刻

1.实现--获取时刻:

1.时间相关的头文件已经在 ros.h 中包含,无须包含其他头文件

2.API:ros::Time::now() ;     返回值类型为 ros::Time 类型,值为距离1970/01/01的时间。

ros::Time rightnow =  ros::Time::now();
#include "ros/ros.h"
int main(int argc, char  *argv[])
{
    setlocale(LC_ALL,"");
    ros::init(argc,argv,"helllo_time");
    ros::NodeHandle nh;
    
    //now函数会将当前时刻封装并返回,当前时刻:now被调用执行的时刻
    ros::Time rightnow =  ros::Time::now();
    //将离参考系的时间转换成秒
    ROS_INFO("当前时刻%.2f",rightnow.toSec());
    ROS_INFO("当前时刻%d",rightnow.sec);
    return 0;
}

运行结果:

lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ rosrun plumbing_apis time
[ INFO] [1736649022.118307474]: 当前时刻1736649022.12
[ INFO] [1736649022.118619666]: 当前时刻1736649022
2.实现--设置指定时刻: 
//设置时间为距离1970/01/01 过去20秒钟 + 2426655纳秒
ros::Time t1(20,2426655);
ROS_INFO("当前时刻%.2f",t1.toSec());

运行结果:

lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ rosrun plumbing_apis time01
[ INFO] [1736649652.954863383]: 当前时刻20.00

2.持续时间(程序休眠)

1.需求:程序中停顿5秒

2.步骤:

①创建持续时间对象

②休眠操作

3.code

ros::duration du(4.5);//持续时间封装了4.5秒
du.sleep();

运行结果:

lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ rosrun plumbing_apis time02
[ INFO] [1736650092.413371697]: 当前时刻1736650092.41
[ INFO] [1736650092.413655626]: 当前时刻1736650092
//这之间间隔了4.5秒
[ INFO] [1736650096.913956111]: 当前时刻20.00

3.时间运算

1.需求:已知程序开始运行的时刻和程序运行的时间,求程序运行结束的时刻

2.步骤:

①获取开始执行的时刻

②调用duration模拟运行时间

③计算结束时刻

3.code1

ros::Time begin = ros::Time::now();
ros::Duration du1(5);
ros::Time stop = begin + du1;
ROS_INFO("开始的时刻:%.2f",begin.toSec());
ROS_INFO("结束的时刻:%.2f",stop.toSec());

运行结果:

lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ rosrun plumbing_apis time03
[ INFO] [1736650770.871465894]: 开始的时刻:1736650770.87
[ INFO] [1736650770.872289194]: 结束的时刻:1736650775.87

 4.code2

ros::Duration du3 = begin - stop ;
ROS_INFO("时刻相减:%.2f",du3.toSec());

运行结果:

[ INFO] [1736650885.845939871]: 时刻相减:-5.00

除此之外,持续时间也可以相加。

4.设置运行频率

ros::Rate rate(1);    rate.sleep()  实现程序休眠。

5. 定时器

1.需求:ROS 中内置了专门的定时器,可以实现与 ros::Rate 类似的效果

2.code:输出结果,每隔一秒输入一行--------

#include "ros/ros.h"
void cb(const ros::TimerEvent & event)
{
    ROS_INFO("----------------------");
}
 
 int main(int argc, char  *argv[])
{
    setlocale(LC_ALL,"");
    ros::init(argc,argv,"helllo_time");
    ros::NodeHandle nh;
    //需求:每隔一秒钟 后台输出一段文本
    ROS_INFO("-----------------------------定时器-----------------------------------");
    //时间间隔 回调函数 false(定时器是否一次性,如果为true,一秒钟执行一次回调函数,只执行一次,false每隔一秒执行一次) 
    //true 定时器是否自动启动
    ros::Timer timer = nh.createTimer(ros::Duration(1),cb);
    ros::spin();
}

运行结果:

lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ rosrun plumbing_apis time04
[ INFO] [1736656008.311804341]: -----------------------------定时器-----------------------------------
[ INFO] [1736656009.313603910]: ----------------------
[ INFO] [1736656010.313497906]: ----------------------
[ INFO] [1736656011.313486552]: ----------------------
[ INFO] [1736656012.313506048]: ----------------------
[ INFO] [1736656013.313499337]: ----------------------
[ INFO] [1736656014.313471610]: ----------------------
[ INFO] [1736656015.313490538]: ----------------------

3.定时器的其他参数

bool oneshot = false //定时器是否一次性,默认为false。
bool autostart = true //定时器是否自动启动,默认为true。

当定时器设置为一次性时:

lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ rosrun plumbing_apis time04
[ INFO] [1736656500.289795698]: -----------------------------定时器-----------------------------------
[ INFO] [1736656501.290676285]: ----------------------

可以看出此时只会输出一次结果。

当定时器设置为不自动启动时:

lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ rosrun plumbing_apis time04
[ INFO] [1736656588.918929581]: -----------------------------定时器-----------------------------------

可以看出此时不会输出回调函数的内容,那么该如何启动呢?需要调用timer.start()手动启动。

当加入手动启动时,可以看出输出结果为:

lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ rosrun plumbing_apis time04
[ INFO] [1736656755.802738358]: -----------------------------定时器-----------------------------------
[ INFO] [1736656756.803763459]: ----------------------
[ INFO] [1736656757.803679492]: ----------------------
[ INFO] [1736656758.803596011]: ----------------------
[ INFO] [1736656759.803530504]: ----------------------
[ INFO] [1736656760.803727982]: ----------------------
[ INFO] [1736656761.803641094]: ----------------------
[ INFO] [1736656762.803615987]: ----------------------
[ INFO] [1736656763.803638777]: ----------------------

5.其他函数

1.节点退出的情况

1.节点接收到了关闭信息,比如常用的 ctrl + c 快捷键就是关闭节点的信号;

2.同名节点启动,导致现有节点退出;(可以在节点初始化时动手脚,加上ros::init_options::AnonymousName防止同名节点启动导致的掉线)

3.程序中的其他部分调用了节点关闭相关的API(C++中是ros::shutdown(),python中是rospy.signal_shutdown()。

2.ROS日志的五个级别

1.DEBUG(调试):只在调试时使用,此类消息不会输出到控制台;

2.INFO(信息):标准消息,一般用于说明系统内正在执行的操作;

3.WARN(警告):提醒一些异常情况,但程序仍然可以执行;

4.ERROR(错误):提示错误信息,此类错误会影响程序运行;

5.FATAL(严重错误):此类错误将阻止节点继续运行;

代码:

#include "ros/ros.h"
int main(int argc, char  *argv[])
{
    setlocale(LC_ALL,"");
    ros::init(argc,argv,"helllo_time");
    ros::NodeHandle nh;
    
    ROS_DEBUG("hello,DEBUG"); //不会输出
    ROS_INFO("hello,INFO"); //默认白色字体
    ROS_WARN("Hello,WARN"); //默认黄色字体
    ROS_ERROR("hello,ERROR");//默认红色字体
    ROS_FATAL("hello,FATAL");//默认红色字体
    return 0;
}

运行结果:

lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ rosrun plumbing_apis time05
[ INFO] [1736657581.494153186]: hello,INFO
[ WARN] [1736657581.494632811]: Hello,WARN
[ERROR] [1736657581.494640603]: hello,ERROR
[FATAL] [1736657581.494644701]: hello,FATAL

二、ROS中的头文件和源文件 

本节主要介绍ROS的C++实现中,如何使用头文件和源文件方式封装代码,具体内容如下:

1.设置头文件,可执行文件作为源文件。

2.分别设置头文件,源文件和可执行文件。

在ROS中关于头文件的使用,核心内容在于CMakeList.txt文件的篇日志,不同的封装方式,配置上也有差异。

1.自定义头文件的调用

1.需求及流程

1.需求:设计头文件,可执行文件本身作为源文件。

2.流程:编写头文件;编写可执行文件;编辑配置文件并执行。

2.具体实现
1.创建功能包

plumbing_head ,依赖于roscpp rospy std_msgs

2.创建头文件

在功能包下的include 下有个和功能包重名的目录,建立头文件hello.h

3.声明头文件保护及头文件实现
#ifndef _HELLO_H
#define _HELLO_H
/*
声明namespace ,namespace下面声明一个类,类下声明一个函数run
*/
namespace hello_ns
{
    class Myhello
    {
        public:
            void run();
    };
}
 
#endif
4.可执行文件实现

1.配置includePath路径,在c_cpp_properties.json文件中配置头文件的路径

/home/liuhongwei/demo03_ws/src/plumbing_head/include

加入includePath中,加入结果如下:

操作在之前就已经实现过,如果忘记请即使回顾:

(三)ROS通信编程——话题通信-优快云博客

"includePath": [
        "/opt/ros/noetic/include/**",
        "/home/lvl/catkin_ws/src/hello/include/**",
        "/usr/include/**",
        "/home/lvl/demo05_ws/src/plumbing_head/include/plumbing_head"
      ],

2.源文件的实现

#include "ros/ros.h"
#include "plumbing_head/hello.h"
 
namespace hello_ns
{
    void Myhello::run()
    {
        ROS_INFO("run 函数执行...............");
    }
}
 
int main(int argc, char  *argv[])
{
    setlocale(LC_ALL,"");
    ros::init(argc,argv,"hello_head");
    hello_ns::Myhello mehello;
    mehello.run();
    return 0;
}

3.配置 CMakeList.txt 

新增:

## Specify additional locations of header files
## Your package locations should be listed before other locations
include_directories(
  include
  ${catkin_INCLUDE_DIRS}
)

 还要添加可执行权限和依赖的库。

4.编译执行

2.自定义源文件的调用

1.需求及流程

1.需求:设计头文件和源文件,在可执行文件中包含头文件

2.流程:编写头文件;编写源文件;编写可执行文件;配置配置文件并执行

2.具体实现

1.创建功能包 plumbing_head_src,依赖为roscpp rospy std_msgs

2.创建头文件 hello.h

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

3.创建源文件 hello.cpp

#include "plumbing_head_src/hello.h"
#include "ros/ros.h"
namespace hello_ns
{
    void Myhello::run()
    {
        ROS_INFO("源文件中的run函数................");
    }
}

4.修改c_cpp_properties.json下面的includPath文件(同上)

5.创建可执行文件 use_hello.cpp

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

6.配置Cmakelist&头文件的配置

修改 include_directories

## Specify additional locations of header files
## Your package locations should be listed before other locations
include_directories(
 include
  ${catkin_INCLUDE_DIRS}
)

配置 C++库  头文件;源文件位置 

## Declare a C++ library
 add_library(head_src
  include/plumbing_head_src/hello.h
   src/hello.cpp
 )
 add_dependencies(head_src ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
 
 target_link_libraries(head_src
   ${catkin_LIBRARIES}
 )

7.配置Cmakelist&可执行文件的配置

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

编译执行

运行结果:

lvl@lvl-Legion-Y7000P-IRX9:~/demo05_ws$ source devel/setup.bash
lvl@lvl-Legion-Y7000P-IRX9:~/demo05_ws$ rosrun plumbing_head_src hehe
[ INFO] [1736663553.497744788]: 源文件中的run函数................

结束语

以上就是我学习到的内容,如果对您有帮助请多多支持我,如果哪里有问题欢迎大家在评论区积极讨论,我看到会及时回复。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值