3 ROS通信机制进阶

第 3 章 ROS通信机制进阶

上一章内容,主要介绍了ROS通信的实现,内容偏向于粗粒度的通信框架的讲解,没有详细介绍涉及的API,也没有封装代码,鉴于此,本章主要内容如下:

  • ROS常用API介绍;
  • ROS中自定义头文件与源文件的使用。

预期达成的学习目标:

  • 熟练掌握ROS常用API;
  • 掌握ROS中自定义头文件与源文件的配置。

3.1 常用API

首先,建议参考官方API文档或参考源码:

  • ROS节点的初始化相关API;
  • NodeHandle 的基本使用相关API;
  • 话题的发布方,订阅方对象相关API;
  • 服务的服务端,客户端对象相关API;
  • 时间相关API;
  • 日志输出相关API。

参数服务器相关API在第二章已经有详细介绍和应用,在此不再赘述。

另请参考:

  • http://wiki.ros.org/APIs
  • https://docs.ros.org/en/api/roscpp/html/

3.1.1 初始化


C++
初始化
/** @brief ROS初始化函数。
 *
 * 该函数可以解析并使用节点启动时传入的参数(通过参数设置节点名称、命名空间...) 
 *
 * 该函数有多个重载版本,如果使用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);

创建功能包及其依赖:

plumbing_apis

roscpp rospy std_msgs

创建新文件:demo01_apis_pub.cpp

#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>

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

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");
    // 2.初始化ROS节点;
    /* 
        作用:ROS初始化函数

        参数:
            1.argc    ------封装实参的个数(n+1)
            2.aegv    ------封装参数的数组
            3.name    ------为节点命名
            4.options ------节点启动选项

            返回值:void
        使用:
            1.argc 与 argv 的使用
                如果按照ROS中的特定格式传入实参,那么ROS可以加以使用,比如用来设置全局参数,给节点重命名
            2.options 的使用
                节点名称需要保证唯一,会导致一个问题:同一个节点不能重复启动
                结果:ROS 中当有重名节点启动时,之前的节点会被关闭
                需求:特定场景下,需要一个节点多次启动且能正常运行,怎么办?
                解决:设置启动项ros::init_options::AnonymousName
                     当创建ROS节点时,会在用户自定义的节点名称后加随机数,从而避免重名问题
    */
    ros::init(argc,argv,"erGouZi",ros::init_options::AnonymousName);
    // 3.创建节点句柄;
    ros::NodeHandle nh;
    // 4.创建发布者对象;
    ros::Publisher pub = nh.advertise<std_msgs::String>("fang",10);
    // 5.编写发布逻辑并发布数据。
    //要求以10HZ的频率发布数据,并且文本后添加编号
    // 5.1先创建被发布的消息
    std_msgs::String msg;   //创建消息对象
    // 5.2设置发布频率
    ros::Rate rate(1);     //ros已经封装好了,这就是10hz了
    // 5.3设置编号
    int count = 0;

    //编写循环,循环中发布数据
    ros::Duration(3).sleep();  //执行到这一步的时候休眠3秒钟

    // 编写循环,循环中发布数据
    while (ros::ok()) //判断
    {
        count++;   //每循环一次,count++一次
        
        //实现字符串拼接数字
        std::stringstream ss;
        ss << "hello --->" << count;
        //msg.data = "hello";
        msg.data = ss.str();  //输出数据流
        pub.publish(msg);
        //添加日志:
        ROS_INFO("发布的数据是:%s",ss.str().c_str());
        rate.sleep();

         ros::spinOnce(); //官方建议,处理回调函数
        //spinOnce()只会调用一次回调函数,之后往下继续执行,这里没有回调函数
    }
    
    return 0;
}

CMakeLists配置

跟之前的plumbing_pub_sub中的demo01_pub.cpp一样

验证:

rosceore

cd demo03_ws/
source ./devel/setup.bash
rosrun plumbing_apis demo01_apis_pub

source ./devel/setup.bash
rosrun plumbing_apis demo01_apis_pub

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qr8qYgIl-1679488996105)(image/image-20220704160005124.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fcF3TdmL-1679488996107)(image/image-20220704155950134.png)]

3.1.2 话题与服务相关对象


C++

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

NodeHandle有一个重要作用是可以用于设置命名空间,这是后期的重点,但是本章暂不介绍。

1.发布对象
#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>

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

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");
    // 2.初始化ROS节点;
    /* 
        作用:ROS初始化函数

        参数:
            1.argc    ------封装实参的个数(n+1)
            2.aegv    ------封装参数的数组
            3.name    ------为节点命名
            4.options ------节点启动选项

            返回值:void
        使用:
            1.argc 与 argv 的使用
                如果按照ROS中的特定格式传入实参,那么ROS可以加以使用,比如用来设置全局参数,给节点重命名
            2.options 的使用
                节点名称需要保证唯一,会导致一个问题:同一个节点不能重复启动
                结果:ROS 中当有重名节点启动时,之前的节点会被关闭
                需求:特定场景下,需要一个节点多次启动且能正常运行,怎么办?
                解决:设置启动项ros::init_options::AnonymousName
                     当创建ROS节点时,会在用户自定义的节点名称后加随机数,从而避免重名问题
    */
    ros::init(argc,argv,"erGouZi",ros::init_options::AnonymousName);
    // 3.创建节点句柄;
    ros::NodeHandle nh;
    // 4.创建发布者对象;
    /* 
        作用:创建发布者对象

        模板:被发布的消息类型
        
        参数:
            1.话题名称
            2.队列长度
            3.latch(可选)  如果设置为true,会保存发布方的最后一条消息,并且新的订阅对象连接到发布方时,
                          发布方会将这条消息发送给订阅者
            
        使用:
            latch 设置为 true 的作用?
            以静态地图发布为例
                方案1:可以使用固定频率发送地图数据,但是效率低;
                方案2:可以将地图发布对象的 latch 设置为 true ,并且发布方只发送一次数据,每当订阅者连接时,
                      将地图数据发送给订阅者(只发送一次),这样提高了数据的发送效率
    */
    ros::Publisher pub = nh.advertise<std_msgs::String>("fang",10,true);
    // 5.编写发布逻辑并发布数据。
    //要求以10HZ的频率发布数据,并且文本后添加编号
    // 5.1先创建被发布的消息
    std_msgs::String msg;   //创建消息对象
    // 5.2设置发布频率
    ros::Rate rate(1);     //ros已经封装好了,这就是10hz了
    // 5.3设置编号
    int count = 0;

    //编写循环,循环中发布数据
    ros::Duration(3).sleep();  //执行到这一步的时候休眠3秒钟

    // 编写循环,循环中发布数据
    while (ros::ok()) //判断
    {
        count++;   //每循环一次,count++一次
        
        //实现字符串拼接数字
        std::stringstream ss;
        ss << "hello --->" << count;
        //msg.data = "hello";
        msg.data = ss.str();  //输出数据流
        if (count <= 10)
        {
            pub.publish(msg);
            //添加日志:
            ROS_INFO("发布的数据是:%s",ss.str().c_str());
        }
       
        rate.sleep();

         ros::spinOnce(); //官方建议,处理回调函数
        //spinOnce()只会调用一次回调函数,之后往下继续执行,这里没有回调函数
    }
    
    return 0;
}

这里需要一个demo02_apis_sub.cpp文件,记得配置

执行:

roscore

rosrun plumbing_apis demo01_apis_pub

rosrun plumbing_apis demo02_apis_sub

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OvQHNR1L-1679488996107)(image/image-20220704163658112.png)]

3.1.3 回旋函数


C++

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

1.spinOnce()
/**
 * \brief 处理一轮回调
 *
 * 一般应用场景:
 *     在循环体内,处理所有可用的回调函数
 * 
 */
ROSCPP_DECL void spinOnce();
2.spin()
/** 
 * \brief 进入循环处理回调 
 */
ROSCPP_DECL void spin();
3.二者比较

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

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

#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>

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

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");
    // 2.初始化ROS节点;
    /* 
        作用:ROS初始化函数

        参数:
            1.argc    ------封装实参的个数(n+1)
            2.aegv    ------封装参数的数组
            3.name    ------为节点命名
            4.options ------节点启动选项

            返回值:void
        使用:
            1.argc 与 argv 的使用
                如果按照ROS中的特定格式传入实参,那么ROS可以加以使用,比如用来设置全局参数,给节点重命名
            2.options 的使用
                节点名称需要保证唯一,会导致一个问题:同一个节点不能重复启动
                结果:ROS 中当有重名节点启动时,之前的节点会被关闭
                需求:特定场景下,需要一个节点多次启动且能正常运行,怎么办?
                解决:设置启动项ros::init_options::AnonymousName
                     当创建ROS节点时,会在用户自定义的节点名称后加随机数,从而避免重名问题
    */
    ros::init(argc,argv,"erGouZi",ros::init_options::AnonymousName);
    // 3.创建节点句柄;
    ros::NodeHandle nh;
    // 4.创建发布者对象;
    /* 
        作用:创建发布者对象

        模板:被发布的消息类型
        
        参数:
            1.话题名称
            2.队列长度
            3.latch(可选)  如果设置为true,会保存发布方的最后一条消息,并且新的订阅对象连接到发布方时,
                          发布方会将这条消息发送给订阅者
            
        使用:
            latch 设置为 true 的作用?
            以静态地图发布为例
                方案1:可以使用固定频率发送地图数据,但是效率低;
                方案2:可以将地图发布对象的 latch 设置为 true ,并且发布方只发送一次数据,每当订阅者连接时,
                      将地图数据发送给订阅者(只发送一次),这样提高了数据的发送效率
    */
    ros::Publisher pub = nh.advertise<std_msgs::String>("fang",10,true);
    // 5.编写发布逻辑并发布数据。
    //要求以10HZ的频率发布数据,并且文本后添加编号
    // 5.1先创建被发布的消息
    std_msgs::String msg;   //创建消息对象
    // 5.2设置发布频率
    ros::Rate rate(1);     //ros已经封装好了,这就是10hz了
    // 5.3设置编号
    int count = 0;

    //编写循环,循环中发布数据
    ros::Duration(3).sleep();  //执行到这一步的时候休眠3秒钟

    // 编写循环,循环中发布数据
    while (ros::ok()) //判断
    {
        count++;   //每循环一次,count++一次
        
        //实现字符串拼接数字
        std::stringstream ss;
        ss << "hello --->" << count;
        //msg.data = "hello";
        msg.data = ss.str();  //输出数据流
        // if (count <= 10)
        // {
        //     pub.publish(msg);
        //     //添加日志:
        //     ROS_INFO("发布的数据是:%s",ss.str().c_str());
        // }
       
        pub.publish(msg);
        //添加日志:
        ROS_INFO("发布的数据是:%s",ss.str().c_str());

        rate.sleep();

         ros::spinOnce(); //官方建议,处理回调函数
        //spinOnce()只会调用一次回调函数,之后往下继续执行,这里没有回调函数
        ROS_INFO("一轮回调执行完毕---");
    }
    
    return 0;
}
#include "ros/ros.h"
#include "std_msgs/String.h"

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

//订阅对象的回调函数
//常量指针引用 const std_msgs::String::ConstPtr &msg
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,"cuiHua");
    // 3.创建节点句柄;
    ros::NodeHandle nh;
    // 4.创建发布者对象;
    ros::Subscriber sub = nh.subscribe("fang",10,doMsg);
    // 5.处理订阅到的数据。

    // spin()函数,目的是让回调函数再次执行,而不是执行一次
    ros::spin();
    ROS_INFO("spin后的语句");  //理论上这条语句是执行不到的

    return 0;
}

执行:

roscore

rosrun plumbing_apis demo01_apis_pub

rosrun plumbing_apis demo02_apis_sub

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M0lvW90z-1679488996108)(image/image-20220704165819825.png)]

3.1.4 时间

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


C++
1.时刻

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

ros::init(argc,argv,"hello_time");
ros::NodeHandle nh;//必须创建句柄,否则时间没有初始化,导致后续API调用失败
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 的秒数

ros::Time someTime(100,100000000);// 参数1:秒数  参数2:纳秒
ROS_INFO("时刻:%.2f",someTime.toSec()); //100.10
ros::Time someTime2(100.3);//直接传入 double 类型的秒数
ROS_INFO("时刻:%.2f",someTime2.toSec()); //100.30
#include "ros/ros.h"

/*  
    需求:演示时间相关操作(获取当前时刻 + 设置指定时刻)
    实现:
        1.准备(头文件,节点初始化,NodeHandle创建)
        2.获取当前时刻
        3.设置指定时刻
*/

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");
    // 1.准备(头文件,节点初始化,NodeHandle创建)
    ros::init(argc,argv,"hello_time");
    ros::NodeHandle nh;  //注意:必须初始化。否则会导致时间api调用失败。
    // 2.获取当前时刻
    //now 函数会将当前时刻封装并返回
    //当前时刻:now 被调用执行那一刻
    //参考系:1970年01月01日 00:00:00
    ros::Time right_now = ros::Time::now();
    ROS_INFO("当前时刻:%.2f",right_now.toSec());
    ROS_INFO("当前时刻:%d",right_now.sec);
    // 3.设置指定时刻
    ros::Time t1(20,312345678);
    ros::Time t2(100.35);
    ROS_INFO("t1 = %.2f",t1.toSec());
    ROS_INFO("t2 = %.2f",t2.toSec());

    return 0;
}

配置

add_executable(demo03_apis_time src/demo03_apis_time.cpp)

target_link_libraries(demo03_apis_time
  ${catkin_LIBRARIES}
)

运行

roscore

rosrun plumbing_apis demo03_apis_time 
2.持续时间

设置一个时间区间(间隔):

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());
3.持续时间与时刻运算

为了方便使用,ROS中提供了时间与时刻的运算:

ROS_INFO("时间运算");
ros::Time now = ros::Time::now();
ros::Duration du1(10);
ros::Duration du2(20);
ROS_INFO("当前时刻:%.2f",now.toSec());
//1.time 与 duration 运算
ros::Time after_now = now + du1;
ros::Time before_now = now - du1;
ROS_INFO("当前时刻之后:%.2f",after_now.toSec());
ROS_INFO("当前时刻之前:%.2f",before_now.toSec());

//2.duration 之间相互运算
ros::Duration du3 = du1 + du2;
ros::Duration du4 = du1 - du2;
ROS_INFO("du3 = %.2f",du3.toSec());
ROS_INFO("du4 = %.2f",du4.toSec());
//PS: time 与 time 不可以运算
// ros::Time nn = now + before_now;//异常
4.设置运行频率
ros::Rate rate(1);//指定频率
while (true)
{
    ROS_INFO("-----------code----------");
    rate.sleep();//休眠,休眠时间 = 1 / 频率。
}
5.定时器

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

ros::NodeHandle nh;//必须创建句柄,否则时间没有初始化,导致后续API调用失败

 // ROS 定时器
 /**
* \brief 创建一个定时器,按照指定频率调用回调函数。
*
* \param period 时间间隔
* \param callback 回调函数
* \param oneshot 如果设置为 true,只执行一次回调函数,设置为 false,就循环执行。
* \param autostart 如果为true,返回已经启动的定时器,设置为 false,需要手动启动。
*/
 //Timer createTimer(Duration period, const TimerCallback& callback, bool oneshot = false,
 //                bool autostart = true) const;

 // ros::Timer timer = nh.createTimer(ros::Duration(0.5),doSomeThing);
 ros::Timer timer = nh.createTimer(ros::Duration(0.5),doSomeThing,true);//只执行一次

 // ros::Timer timer = nh.createTimer(ros::Duration(0.5),doSomeThing,false,false);//需要手动启动
 // timer.start();
 ros::spin(); //必须 spin

定时器的回调函数:

void doSomeThing(const ros::TimerEvent &event){
    ROS_INFO("-------------");
    ROS_INFO("event:%s",std::to_string(event.current_real.toSec()).c_str());
}

c++代码:

#include "ros/ros.h"

/*  
    需求1:演示时间相关操作(获取当前时刻 + 设置指定时刻)
    实现:
        1.准备(头文件,节点初始化,NodeHandle创建)
        2.获取当前时刻
        3.设置指定时刻
    
    需求2:程序执行中停顿 5 秒
    实现:
        1.创建持续时间对象
        2.休眠
    
    需求3:已知程序开始运行的时刻和程序运行的时间,求运行结束的时刻
    实现:
        1.获取开始执行的时刻
        2.模拟运行时间(N秒)
        3.计算结束时刻 = 开始 + 持续时间
    注意:
        1.时刻与持续时间可以执行加减;
        2.持续时间之间也可以执行加减;
        3.时刻之间值可以相减,不可以相加。

    需求4:每隔一秒钟,在控制台输出一段文本。
    实现:
        策略1:ros::Rate()
        策略2:定时器
    注意:
        创建:nh.createTimer()
        参数1:时间间隔
        参数2:回调函数(时间事件  TimerEvent)
        参数3:是否只执行一次
        参数4:是否自动启动(当设置为 false, 需要手动调用 timer.start())
        定时器启动后:ros::spin()
        
*/

//回调函数
void cb (const ros::TimerEvent &event) {
    ROS_INFO("-----------");
    ROS_INFO("函数被调用的时刻:%.2f",event.current_real.toSec());
}


int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");
    // 1.准备(头文件,节点初始化,NodeHandle创建)
    ros::init(argc,argv,"hello_time");
    ros::NodeHandle nh;  //注意:必须初始化。否则会导致时间api调用失败。
    // 2.获取当前时刻
    //now 函数会将当前时刻封装并返回
    //当前时刻:now 被调用执行那一刻
    //参考系:1970年01月01日 00:00:00
    ros::Time right_now = ros::Time::now();
    ROS_INFO("当前时刻:%.2f",right_now.toSec());
    ROS_INFO("当前时刻:%d",right_now.sec);
    // 3.设置指定时刻
    ros::Time t1(20,312345678);
    ros::Time t2(100.35);
    ROS_INFO("t1 = %.2f",t1.toSec());
    ROS_INFO("t2 = %.2f",t2.toSec());

    //------------------------------------------------------------------
    ROS_INFO("--------------------------持续时间-----------------------");
    ros::Time start = ros::Time::now();
    ROS_INFO("开始休眠:%.2f",start.toSec());
    ros::Duration du(4.5);
    du.sleep();
    ros::Time end = ros::Time::now();
    ROS_INFO("结束休眠:%.2f",end.toSec());

    //------------------------------------------------------------------
    ROS_INFO("--------------------------时间运算-----------------------");
    // 1.获取开始执行的时刻
    ros::Time begin = ros::Time::now();
    // 2.模拟运行时间(N秒)
    ros::Duration du1(5);
    // 3.计算结束时刻 = 开始 + 持续时间
    ros::Time stop = begin + du1;  //也可以减
    ROS_INFO("开始时刻:%.2f",begin.toSec());
    ROS_INFO("结束时刻:%.2f",stop.toSec());

    //时刻与时刻运算
    //ros::Time sum = begin + stop; //不可以相加
    ros::Duration du2 = begin - stop;
    ROS_INFO("时刻相减:%.2f",du2.toSec());
    //持续时间与持续时间的运算
    ros::Duration du3 = du1 + du2;
    ros::Duration du4 = du1 - du2;
    ROS_INFO("du1 + du2 = %.2f",du3.toSec());
    ROS_INFO("du1 - du2 = %.2f",du4.toSec());

    //------------------------------------------------------------------
    ROS_INFO("--------------------------定时器-----------------------");
    /* 
        ros::Timer createTimer (ros::Duration period,   //时间间隔----1s
                                const ros::TimerCallback &callback,  //回调函数----封装业务 
                                bool oneshot = false,        //是否是一次性
                                bool autostart = true)       //自动启动
    */
    // ros::Timer timer = nh.createTimer(ros::Duration(1),cb);
    // ros::Timer timer = nh.createTimer(ros::Duration(1),cb,true);
    ros::Timer timer = nh.createTimer(ros::Duration(1),cb,false,false);
    timer.start();  //手动启动

    ros::spin(); //需要回旋
    return 0;
}

运行:

roscore

rosrun plumbing_apis demo03_apis_time 

3.1.5 其他函数

在发布实现时,一般会循环发布消息,循环的判断条件一般由节点状态来控制,C++中可以通过 ros::ok() 来判断节点状态是否正常,而 python 中则通过 rospy.is_shutdown() 来实现判断,导致节点退出的原因主要有如下几种:

  • 节点接收到了关闭信息,比如常用的 ctrl + c 快捷键就是关闭节点的信号;
  • 同名节点启动,导致现有节点退出;
  • 程序中的其他部分调用了节点关闭相关的API(C++中是ros::shutdown(),python中是rospy.signal_shutdown())
#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>

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

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");
    // 2.初始化ROS节点;
    /* 
        作用:ROS初始化函数

        参数:
            1.argc    ------封装实参的个数(n+1)
            2.aegv    ------封装参数的数组
            3.name    ------为节点命名
            4.options ------节点启动选项

            返回值:void
        使用:
            1.argc 与 argv 的使用
                如果按照ROS中的特定格式传入实参,那么ROS可以加以使用,比如用来设置全局参数,给节点重命名
            2.options 的使用
                节点名称需要保证唯一,会导致一个问题:同一个节点不能重复启动
                结果:ROS 中当有重名节点启动时,之前的节点会被关闭
                需求:特定场景下,需要一个节点多次启动且能正常运行,怎么办?
                解决:设置启动项ros::init_options::AnonymousName
                     当创建ROS节点时,会在用户自定义的节点名称后加随机数,从而避免重名问题
    */
    ros::init(argc,argv,"erGouZi",ros::init_options::AnonymousName);
    // 3.创建节点句柄;
    ros::NodeHandle nh;
    // 4.创建发布者对象;
    /* 
        作用:创建发布者对象

        模板:被发布的消息类型
        
        参数:
            1.话题名称
            2.队列长度
            3.latch(可选)  如果设置为true,会保存发布方的最后一条消息,并且新的订阅对象连接到发布方时,
                          发布方会将这条消息发送给订阅者
            
        使用:
            latch 设置为 true 的作用?
            以静态地图发布为例
                方案1:可以使用固定频率发送地图数据,但是效率低;
                方案2:可以将地图发布对象的 latch 设置为 true ,并且发布方只发送一次数据,每当订阅者连接时,
                      将地图数据发送给订阅者(只发送一次),这样提高了数据的发送效率
    */
    ros::Publisher pub = nh.advertise<std_msgs::String>("fang",10,true);
    // 5.编写发布逻辑并发布数据。
    //要求以10HZ的频率发布数据,并且文本后添加编号
    // 5.1先创建被发布的消息
    std_msgs::String msg;   //创建消息对象
    // 5.2设置发布频率
    ros::Rate rate(1);     //ros已经封装好了,这就是10hz了
    // 5.3设置编号
    int count = 0;

    //编写循环,循环中发布数据
    ros::Duration(3).sleep();  //执行到这一步的时候休眠3秒钟

    // 编写循环,循环中发布数据
    while (ros::ok()) //判断
    {
          //如果计数器 >= 50, 那么关闭节点
        if (count >= 50)
        {
            ros::shutdown();
        }
        
        count++;   //每循环一次,count++一次
        
        //实现字符串拼接数字
        std::stringstream ss;
        ss << "hello --->" << count;
        //msg.data = "hello";
        msg.data = ss.str();  //输出数据流
        // if (count <= 10)
        // {
        //     pub.publish(msg);
        //     //添加日志:
        //     ROS_INFO("发布的数据是:%s",ss.str().c_str());
        // }

        pub.publish(msg);
        //添加日志:
        ROS_INFO("发布的数据是:%s",ss.str().c_str());

        rate.sleep();

         ros::spinOnce(); //官方建议,处理回调函数
        //spinOnce()只会调用一次回调函数,之后往下继续执行,这里没有回调函数
        ROS_INFO("一轮回调执行完毕---");
    }
    
    return 0;
}

另外,日志相关的函数也是极其常用的,在ROS中日志被划分成如下级别:

  • DEBUG(调试):只在调试时使用,此类消息不会输出到控制台;
  • INFO(信息):标准消息,一般用于说明系统内正在执行的操作;
  • WARN(警告):提醒一些异常情况,但程序仍然可以执行;
  • ERROR(错误):提示错误信息,此类错误会影响程序运行;
  • FATAL(严重错误):此类错误将阻止节点继续运行。
#include "ros/ros.h"

/* 
    ROS 中日志:
        演示不同级别日志的基本使用
*/

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");
    ros::init(argc,argv,"hello_log");
    ros::NodeHandle nh;

    //日志输出
    ROS_DEBUG("调试信息"); //不会打印在控制台
    ROS_INFO("一般信息");  //白
    ROS_WARN("警告信息");  //黄
    ROS_ERROR("错误信息"); //红
    ROS_FATAL("严重错误"); //红

    return 0;
}

运行:

rosrun plumbing_apis demo04_apis_log 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hp66gasi-1679488996109)(image/image-20220710151907803.png)]

3.2 ROS中的头文件与源文件

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

  1. 设置头文件,可执行文件作为源文件;
  2. 分别设置头文件,源文件与可执行文件。

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

3.2.1 自定义头文件调用

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

流程:

  1. 编写头文件;
  2. 编写可执行文件(同时也是源文件);
  3. 编辑配置文件并执行。

1.头文件

在功能包下的 include/功能包名 目录下新建头文件: hello.h,示例内容如下:

#ifndef _HELLO_H
#define _HELLO_H

/*  
    声明 namespace
        |--class
            |--run
*/

namespace hello_ns{

class MyHello{

    public:
        void run();
};

}

#endif

注意:

在 VScode 中,为了后续包含头文件时不抛出异常,请配置 .vscode 下 c_cpp_properties.json 的 includepath属性

 "/home/liu/demo03_ws/src/plumbing_head/include/**" 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RAEYhKJe-1679488996111)(image/image-20220710160241784.png)]

2.可执行文件

在 src 目录下新建文件:hello.cpp,示例内容如下:

#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 myHello;
    myHello.run();

    return 0;
}
3.配置文件

配置CMakeLists.txt文件,头文件相关配置如下:

在第119行把 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}
)

运行:

rosrun plumbing_head hello

3.2.2 自定义源文件调用

**需求:**设计头文件与源文件,在可执行文件中包含头文件。

流程:

  1. 编写头文件;
  2. 编写源文件;
  3. 编写可执行文件;
  4. 编辑配置文件并执行。

1.头文件

头文件设置于 3.2.1 类似,在功能包下的 include/功能包名 目录下新建头文件: hello.h

注意:

在 VScode 中,为了后续包含头文件时不抛出异常,请配置 .vscode 下 c_cpp_properties.json 的 includepath属性

        "/home/liu/demo03_ws/src/plumbing_head_src/include/**"
2.源文件

在 src 目录下新建文件:hello.cpp,示例内容如下:

#include "plumbing_head_src/hello.h"
#include "ros/ros.h"

namespace hello_ns{

    void MyHello::run(){
        ROS_INFO("源文件中的run函数....");
    }
}
3.可执行文件

在 src 目录下新建文件: 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_head_src");

    hello_ns::Myhello myHello;
    myHello.run();

    return 0;
}
4.配置文件

头文件与源文件相关配置:

include_directories(
include
  ${catkin_INCLUDE_DIRS}
)

## 声明C++库  第124行
add_library(head_src                 ##自己定义的库
  include/${PROJECT_NAME}/hello.h    ##头文件
  src/hello.cpp                      ##源文件
)

## 第148行
add_dependencies(head ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})

## 第152行
target_link_libraries(head
  ${catkin_LIBRARIES}
)

可执行文件配置:

add_executable(use_hello src/use_hello.cpp)

add_dependencies(use_hello ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})

#此处需要添加之前设置的 head_src 库(与前文自定义名字相同)
target_link_libraries(use_hello
  head_src        
  ${catkin_LIBRARIES}
)

运行:

source ./devel/setup.bash
rosrun plumbing_head_src use_hello

参考:

https://www.bilibili.com/video/BV1Ci4y1L7ZZ?p=1&vd_source=45da538703ca980e21d6c2bff2a7c144

http://www.autolabor.com.cn/book/ROSTutorials/di-3-zhang-ros-tong-xin-ji-zhi-jin-jie.html

万分感谢赵老师事无巨细的视频教学和清晰明了的笔记~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值