第 3 章 ROS 通信进阶 —— plumbing_apis/plumbing_head/plumbing_head_src


API:应用程序编程接口

学习目标

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

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

预期达成的学习目标:

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

01 常用API —— plumbing_apis

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

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

1.1 初始化函数 —— demo01_apis_pub.cpp

  • ROS 初始化函数
  • ros::init(argc,argv,“erGouZi”,ros::init_options::AnonymousName)
ROS初始化函数

   参数:
     1. argc ----- 封装实参个数(传入n个参数,则 argc = n+1)第1个是文件自身
     2. argv ----- 封装参数数组(字符串数组)
     3. name ----- 为节点命名(唯一性,不允许出现重名节点)
     4. options -- 节点启动选项

     返回值:void

  使用细节:
    1. argc 与 argv 的使用
       如果按照ROS中的特定格式传入实参,那么ROS可以加以使用,比如用来设置全局参数,给节点重命名
    2. options的使用
       节点名称需要保证唯一,会导致一个问题:同一个节点不能重复启动
       问题:ROS中有重名的节点启动时,之前的节点就会被关闭
       需求:特定场景下,需要一个节点多次启动且能正常运行,怎么办?
       (场景:比如安装2个摄像头用的节点)
       解决:设置启动项 ros::init_options::AnonymousName
             当创建ROS节点时,会在用户自定义的节点名称后缀随机数,从而避免重名问题

1.2 话题与服务相关对象(发布方) —— demo01_apis_pub.cpp

  • 在 roscpp 中,话题和服务的相关对象一般由 NodeHandle 创建
  • ros::Publisher pub = nh.advertise<std_msgs::String>(“fang”,10,true)
作用:创建发布者对象

模板:被发布的消息的类型

参数:
  1. 话题名称
  2. 队列长度
  3. latch(可选) 如果设置为true,会保存发布方的最后一条消息,并且
     新的订阅对象连接到发布方时,发布方会将消息发送给订阅方
使用:
  latch设置为true的作用?
  以静态地图为例,
  方案1:可以使用固定频率发送地图数据,但效率低
  方案2:可以将地图发布对象的latch设置为true,并且发布方只发布一次数据,每当订阅者连接时,
         就可以将地图数据发送给订阅者(只发送一次),提高数据的发送效率。

1.1.1&1.2.1 C++ 实现

#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个参数,则 argc = n+1)第1个是文件自身
            2. argv ----- 封装参数数组(字符串数组)
            3. name ----- 为节点命名(唯一性)
            4. options -- 节点启动选项

            返回值:void

        使用细节:
            1. argc 与 argv的使用
               如果按照ROS中的特定格式传入实参,那么ROS可以加以使用,比如用来设置全局参数,给节点重命名
            2. options的使用
                节点名称需要保证唯一,会导致一个问题:同一个节点不能重复启动
                问题:ROS中有重名的节点启动时,之前的节点就会被关闭
                需求:特定场景下,需要一个节点多次启动且能正常运行,怎么办?
                (场景:比如安装2个摄像头用的节点)
                解决:设置启动项 ros::init_options::AnonymousName
                    当创建ROS节点时,会在用户自定义的节点名称后缀随机数,从而避免重名问题
    */
    ros::init(argc,argv,"erGouZi",ros::init_options::AnonymousName);//ergouzi是节点名称,相当于是相亲对象,这里的ergouzi就是talker

    // 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);
    //缓存10条数据,话题名称是fang,队列长10,由于网络阻塞,没有发出去,则存储在这里,最大容量是10,超出阈值后先放进去的先销毁

    // 5.编写发布逻辑并发布数据
    
    // 5.1 创建被发布的消息
    std_msgs::String msg;

    // 5.2 要求以10HZ 的频率发布数据,并且文本后添加编号
    // 5.2.1 发布频率
    ros::Rate rate(1);//每秒发布1次
    // 5.2.2 设置编号
    int count = 0;

    // 订阅时,第一条数据丢失;原因: 发送第一条数据时, publisher 还未在 roscore 注册完毕
    ros::Duration(3).sleep();//延迟3秒

    // 5.3 编写循环,循环要发布的消息
    while(ros::ok())//如果节点还活着,循环成立
    {
        // 如果计数器 >= 30,那么关闭节点
        if(count >= 30)
        {
            ros::shutdown();// 退出
        }
        // ------------
        count++;
        //msg.data = "hello";
        //实现字符串的拼接
        std::stringstream ss;
        ss << "hello ---> " << count;
        msg.data = ss.str();
        // if(count <= 10)
        // {
        //     pub.publish(msg);
        //     //添加日志:
        //     ROS_INFO("发布的数据是:%s",msg.data.c_str());
        // }
      
        pub.publish(msg);
        //添加日志:
        ROS_INFO("发布的数据是:%s",msg.data.c_str());
        
        rate.sleep();// 发布一次,睡0.1秒
        ros::spinOnce();//每次循环完都回头一次,spinOnce()只调用一次回调函数,之后会继续往下执行,官方建议,处理回调函数
        ROS_INFO("一轮回调执行完毕");
    }
    return 0;
}

1.1.2&1.2.2 配置 CMakeLists.txt

add_executable(demo01_apis_pub src/demo01_apis_pub.cpp)

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

target_link_libraries(demo01_apis_pub
  ${catkin_LIBRARIES}
)

1.1.3&1.2.3 编译执行(重要)

  • 启动 roscore(窗口1)
roscore
  • 启动发布节点(窗口2):后面跟着设置的参数名(下划线"_" + 参数名)
cd demo01_ws/
source ./devel/setup.bash
rosrun plumbing_apis demo01_apis_pub _length:= 2(后面要加冒号:)

在这里插入图片描述

  • 启动窗口3 rosparam list
    在这里插入图片描述

1.3 回旋函数 —— demo02_apis_sub.cpp

  • ros::spin() && ros::spinOnce
  • 相同点:二者都用于处理回调函数
  • 发布方用的 spinOnce(),订阅方使用的是spin()
  • 不同点:ros::spin() 是进入了循环执行回调函数,而 ros::spinOnce() 只会执行一次回调函数(没有循环),在ros::spin() 后的语句不会执行到,而 ros::spinOnce() 后的语句可以执行

spinOnce:只回调一次,执行完就结束,可以继续执行spinOnce后面的代码。
在这里插入图片描述

  • spin:进入cb开始回旋,一直循环执行
    在这里插入图片描述

1.3.1 C++ 实现

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

void doMsq(const std_msgs::String::ConstPtr &msg)// std_msgs String类型常量指针的引用,保证传入的实参是常量
{
    //通过msg获取并且操作订阅到的数据
    ROS_INFO("订阅的数据是:%s",msg->data.c_str());//指针的引用
    // ROS_INFO("我听见:%s",(*msg_p).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,doMsq);//fang是两者的共同话题,doMsq是回调函数
    // 5.处理订阅到的数据
    ros::spin();//spin意思是回头,main函数从上往下一次执行,回头调用doMsq函数,doMsq需要被频繁执行
    ROS_INFO("spin后的语句"); // 理论上这条语句执行不到
    return 0;
}

1.3.2 配置 CMakeLists.txt

add_executable(demo02_apis_sub src/demo02_apis_sub.cpp)

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

target_link_libraries(demo02_apis_sub
  ${catkin_LIBRARIES}
)

1.3.3 编译执行

  • 启动 roscore(窗口1)
roscore
  • 启动发布节点(窗口2)
cd demo01_ws/
source ./devel/setup.bash
rosrun plumbing_apis demo01_apis_pub 
  • 启动订阅节点(窗口3)
cd demo01_ws/
source ./devel/setup.bash
rosrun plumbing_apis demo02_apis_sub 

在这里插入图片描述
在这里插入图片描述

1.4 时间函数

1.4.1 时刻

  • 获取时刻,或是设置指定时刻
C++ 实现
#include"ros/ros.h"
/*
    需求: 演示时间相关的操作(获取当前时刻 + 设置制定时刻)
    实现: 
         1. 准备(头文件、节点初始化、NodeHandle创建)
         2. 获取当前时刻
         3. 设置指定时刻
*/

int main(int argc, char *argv[])
{
    // 1. 准备(头文件、节点初始化、NodeHandle创建)
    setlocale(LC_ALL,"");
    ros::init(argc,argv,"hello_time");
    ros::NodeHandle nh;// 注意:必须。否则会导致时间API调用失败(再NodeHandle会初始化时间操作)

    // 2. 获取当前时刻
    // now函数会将当前时刻封装并返回
    // 当前时刻:now 被调用执行的那一刻
    // 参考系:1970年1月1日 00:00:00
    ros::Time right_now = ros::Time::now();//距离参考系逝去的时间
    ROS_INFO("当前时刻:%.2f",right_now.toSec());// 将逝去的时间总长以秒为单位计算,dounle类型
    ROS_INFO("当前时刻:%d",right_now.sec);// 将逝去的时间总长以秒为单位计算,整数类型
    
    // 3. 设置指定时刻
    ros::Time t1(20,312345678);//构造函数,意思是距离参考系过去20秒,以及312345678纳秒
    ros::Time t2(100.35);//浮点型B
    ROS_INFO("t1 = %.2f",t1.toSec());
    ROS_INFO("t2 = %.2f",t2.toSec());

    return 0;
}

在这里插入图片描述

Python 实现
if __name__ == "__main__":

    # 需求1: 演示时间相关操作(获取当前时刻 + 设置指定时刻)
    rospy.init_node("hello_time")

    # 获取时刻
    right_now = rospy.Time.now() # 获取当前时刻(1.now函数被执行的那一刻 2.参考1970年01月01日00:00)并封装成对象
    rospy.loginfo("当前时刻:%.2f",right_now.to_sec()) # 浮点
    rospy.loginfo("当前时刻:%.2f",right_now.secs) # 整型 secs后面没有()

    # 设置指定
    time1 = rospy.Time(100.5) # 将时间封装成time对象,将1970年01月01日00:00逝去100.5秒
    time2 = rospy.Time(100,312345678) # 后面是纳秒,将1970年01月01日00:00逝去100.312345678秒
    rospy.loginfo("指定时刻1:%.2f",time1.to_sec()) # 浮点
    rospy.loginfo("指定时刻1:%.2f",time2.to_sec()) # 浮点

    # 从某个时间值获取时间对象
    time3 = rospy.Time.from_sec(210.12)
    rospy.loginfo("指定时刻1:%.2f",time3.to_sec()) # 浮点

1.4.2 持续时间

C++ 实现
	ROS_INFO("------------------持续时间--------------");
	ros::Time start = ros::Time::now();
    ROS_INFO("开始休眠: %.2f",start.toSec());

    ros::Duration du(4.5); //持续时间封装了4.5秒
    du.sleep();//睡眠4.5s

    ros::Time end = ros::Time::now();
    ROS_INFO("休眠结束:%.2f",end.toSec());

在这里插入图片描述

Python实现
  # 需求2: 程序执行中停顿5秒
    rospy.loginfo("休眠前-------------------")
    # 1. 封装一个持续时间对象(5秒)
    du = rospy.Duration(5,0) # 第二个位置纳秒
    # 2. 再将持续时间休眠
    rospy.sleep(du)
    rospy.loginfo("休眠后-------------------")

1.4.3 持续时间与运算

    1. 时刻与持续时间可以执行加减
    1. 持续时间之间可以执行加减
    1. 时刻之间值可以相减,不可以相加:ros::Time sum = begin + stop 不可以相加,是错误语句
C++ 实现
    // 时刻与持续时间运算
    // 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;//-5
    ROS_INFO("时刻相减:%.2f",du2.toSec());

    // 持续时间与持续时间的运算
    ros::Duration du3 = du1 + du2;// 0
    ros::Duration du4 = du1 - du2;// 10
    ROS_INFO("du1 + du2 = %.2f",du3.toSec());
    ROS_INFO("du1 - du2 = %.2f",du4.toSec());
Python 实现
# 需求3: 获取程序开始执行的限制,且一直持续运行的时间,计算程序结束的时间
    # 1. 获取一个时刻 t1
    t1 = rospy.Time.now()
    # 2. 设置一个持续时间 du1
    du1 = rospy.Duration(5)
    # 3. 结束时刻 t2 = t1 + du1
    t2 = t1 + du1
    rospy.loginfo("开始时刻:%.2f",t1.to_sec())
    rospy.loginfo("结束时刻:%.2f",t2.to_sec())

    du2 = du + du1
    rospy.loginfo("持续时间相加:%.2f",du2.to_sec())

1.4.4 运行频率和定时器

定时器C++ 实现

注意:
创建: nh.createTimer()
参数1: 时间间隔
参数2: 回调函数(时间事件 TimerEvent)
参数3: 是否只执行1次
参数4: 是否自动启动(当设置false时,需要手动调用 timer.start())
定时器启动后: ros::spin()

 /*
    ros::Timer createTimer(ros::Duration period, // 时间间隔 --- 1
            const ros::TimerCallback &callback,  // 回调函数 --- 封装业务
            bool oneshot = false,                // 是否为1次性,dalse的话每隔1秒中执行1次回调函数,循环执行
            bool autostart = true)               // 自动启动
    */
    // ros::Timer timer = nh.createTimer(ros::Duration(1),cb);//每隔1秒执行一次回调函数
    // ros::Timer timer = nh.createTimer(ros::Duration(1),cb,true);//true只执行一次回调函数
    ros::Timer timer = nh.createTimer(ros::Duration(1),cb,false,false);//false不会自动启动
    timer.start();// 手动启动
    ros::spin();// 需要回旋

在这里插入图片描述

定时器Python实现
def doMsg(event):
    rospy.loginfo("+++++++++++++")
    rospy.loginfo("调用回调函数的时刻:%.2f",event.current_real.to_sec()) # current_real 是rospy.Time类型
 # 需求4: 创建定时器,实现类似于 ros::Rate 的功能(隔某个时间间隔执行某个操作)
    
    """
        @param period: desired period between callbacks 回调函数之间调用时间间隔
        @type  period: rospy.Duration
        @param callback: callback to be called
        @type  callback: function taking rospy.TimerEvent
        @param oneshot: if True, fire only once, otherwise fire continuously until shutdown is called [default: False]
        @type  oneshot: bool
    """
    # timer = rospy.Timer(rospy.Duration(2),doMsg,True) # 创建一个定时器对象,每个2秒钟执行一次回调
    timer = rospy.Timer(rospy.Duration(2),doMsg) # 创建一个定时器对象,每个2秒钟执行一次回调

    rospy.spin()

1.4.5 总的程序

1.4.5.1 C++ 实现 —— demo03_apis_time.cpp
#include"ros/ros.h"
/*
    需求1: 演示时间相关的操作(获取当前时刻 + 设置制定时刻)
    实现: 
         1. 准备(头文件、节点初始化、NodeHandle创建)
         2. 获取当前时刻
         3. 设置指定时刻

    需求2: 程序执行中停顿5秒
    实现:
         1. 创建持续时间对象
         2. 休眠

    需求3: 已知程序开始运行的时刻和程序运行的时间,求运行结束的时刻
    实现:
        1. 获取开始执行的时刻
        2. 模拟运行时间(N秒)
        3. 计算结束时刻 = 开始+持续时间
    注意:
        1. 时刻与持续时间可以执行加减
        2. 持续时间之间可以可以执行加减
        3. 时刻之间值可以相减,不可以相加

    需求4: 每隔1秒中,在控制台输出一段文本。
    实现: 
        策略1:ros::Rate()
        策略2:定时器
    注意: 
        创建: nh.createTimer()
        参数1: 时间间隔
        参数2: 回调函数(时间事件 TimerEvent)
        参数3: 是否只执行1次
        参数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[])
{
    // 需求1
    // 1. 准备(头文件、节点初始化、NodeHandle创建)
    setlocale(LC_ALL,"");
    ros::init(argc,argv,"hello_time");
    ros::NodeHandle nh;// 注意:必须。否则会导致时间API调用失败(再NodeHandle会初始化时间操作)

    // 2. 获取当前时刻
    // now函数会将当前时刻封装并返回
    // 当前时刻:now 被调用执行的那一刻
    // 参考系:1970年1月1日 00:00:00
    ros::Time right_now = ros::Time::now();//距离参考系逝去的时间
    ROS_INFO("当前时刻:%.2f",right_now.toSec());// 将逝去的时间总长以秒为单位计算,dounle类型
    ROS_INFO("当前时刻:%d",right_now.sec);// 将逝去的时间总长以秒为单位计算,整数类型
    
    // 3. 设置指定时刻
    ros::Time t1(20,312345678);//构造函数,意思是距离参考系过去20秒,以及312345678纳秒
    ros::Time t2(100.35);//浮点型B
    ROS_INFO("t1 = %.2f",t1.toSec());
    ROS_INFO("t2 = %.2f",t2.toSec());

    // 需求2
    ROS_INFO("------------------持续时间--------------");
    ros::Time start = ros::Time::now();
    ROS_INFO("开始休眠: %.2f",start.toSec());

    ros::Duration du(4.5); //持续时间封装了4.5秒
    du.sleep();//睡眠4.5s

    ros::Time end = ros::Time::now();
    ROS_INFO("休眠结束:%.2f",end.toSec());

    // 需求3
    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;//-5
    ROS_INFO("时刻相减:%.2f",du2.toSec());

    // 持续时间与持续时间的运算
    ros::Duration du3 = du1 + du2;// 0
    ros::Duration du4 = du1 - du2;// 10
    ROS_INFO("du1 + du2 = %.2f",du3.toSec());
    ROS_INFO("du1 - du2 = %.2f",du4.toSec());

    //需求4 
    ROS_INFO("------------------定时器--------------");
    /*
    ros::Timer createTimer(ros::Duration period, // 时间间隔 --- 1
            const ros::TimerCallback &callback,  // 回调函数 --- 封装业务
            bool oneshot = false,                // 是否为1次性,dalse的话每隔1秒中执行1次回调函数,循环执行
            bool autostart = true)               // 自动启动
    */
    // ros::Timer timer = nh.createTimer(ros::Duration(1),cb);//每隔1秒执行一次回调函数
    // ros::Timer timer = nh.createTimer(ros::Duration(1),cb,true);//true只执行一次回调函数
    ros::Timer timer = nh.createTimer(ros::Duration(1),cb,false,false);//false不会自动启动
    timer.start();//手动启动
    ros::spin();//需要回旋

    return 0;
}
1.4.5.2 配置 CMakeLists.txt
add_executable(demo03_apis_time src/demo03_apis_time.cpp)

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

target_link_libraries(demo03_apis_time
  ${catkin_LIBRARIES}
)
1.4.5.3 编译执行
  • 启动 roscore(窗口1)
roscore
  • 启动时间节点(窗口2)
cd demo01_ws/
source ./devel/setup.bash
rosrun plumbing_apis demo03_apis_time
1.4.5.4 Python 实现 —— demo03_apis_time_p.py
#! /usr/bin/env python

import rospy

"""
    需求1: 演示时间相关操作(获取当前时刻 + 设置指定时刻)

    需求2: 程序执行中停顿5秒

    需求3: 获取程序开始执行的限制,且一直持续运行的时间,计算程序结束的时间

    需求4: 创建定时器,实现类似于 ros::Rate 的功能(隔某个时间间隔执行某个操作)

"""

def doMsg(event):
    rospy.loginfo("+++++++++++++")
    rospy.loginfo("调用回调函数的时刻:%.2f",event.current_real.to_sec()) # current_real 是rospy.Time类型

if __name__ == "__main__":

    # 需求1: 演示时间相关操作(获取当前时刻 + 设置指定时刻)
    rospy.init_node("hello_time")

    # 获取时刻
    right_now = rospy.Time.now() # 获取当前时刻(1.now函数被执行的那一刻 2.参考1970年01月01日00:00)并封装成对象
    rospy.loginfo("当前时刻:%.2f",right_now.to_sec()) # 浮点
    rospy.loginfo("当前时刻:%.2f",right_now.secs) # 整型 secs后面没有()

    # 设置指定
    time1 = rospy.Time(100.5) # 将时间封装成time对象,将1970年01月01日00:00逝去100.5秒
    time2 = rospy.Time(100,312345678) # 后面是纳秒,将1970年01月01日00:00逝去100.312345678秒
    rospy.loginfo("指定时刻1:%.2f",time1.to_sec()) # 浮点
    rospy.loginfo("指定时刻1:%.2f",time2.to_sec()) # 浮点

    # 从某个时间值获取时间对象
    time3 = rospy.Time.from_sec(210.12)
    rospy.loginfo("指定时刻1:%.2f",time3.to_sec()) # 浮点

    # 需求2: 程序执行中停顿5秒
    rospy.loginfo("休眠前-------------------")
    # 1. 封装一个持续时间对象(5秒)
    du = rospy.Duration(5,0) # 第二个位置纳秒
    # 2. 再将持续时间休眠
    # rospy.sleep(du)

    rospy.loginfo("休眠后-------------------")

    # 需求3: 获取程序开始执行的限制,且一直持续运行的时间,计算程序结束的时间
    # 1. 获取一个时刻 t1
    t1 = rospy.Time.now()
    # 2. 设置一个持续时间 du1
    du1 = rospy.Duration(5)
    # 3. 结束时刻 t2 = t1 + du1
    t2 = t1 + du1
    rospy.loginfo("开始时刻:%.2f",t1.to_sec())
    rospy.loginfo("结束时刻:%.2f",t2.to_sec())

    du2 = du + du1
    rospy.loginfo("持续时间相加:%.2f",du2.to_sec())

    # 需求4: 创建定时器,实现类似于 ros::Rate 的功能(隔某个时间间隔执行某个操作)
    
    """
        @param period: desired period between callbacks 回调函数之间调用时间间隔
        @type  period: rospy.Duration
        @param callback: callback to be called
        @type  callback: function taking rospy.TimerEvent
        @param oneshot: if True, fire only once, otherwise fire continuously until shutdown is called [default: False]
        @type  oneshot: bool
    """
    # timer = rospy.Timer(rospy.Duration(2),doMsg,True) # 创建一个定时器对象,每个2秒钟执行一次回调
    timer = rospy.Timer(rospy.Duration(2),doMsg) # 创建一个定时器对象,每个2秒钟执行一次回调

    rospy.spin()

1.5 其他函数

1.5.1 日志

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

  • DEBUG(调试):只在调试时使用,此类消息不会输出到控制台;
  • INFO(信息):标准消息,一般用于说明系统内正在执行的操作;
  • WARN(警告):提醒一些异常情况,但程序仍然可以执行;
  • ERROR(错误):提示错误信息,此类错误会影响程序运行;
  • FATAL(严重错误):此类错误将阻止节点继续运行。
1.5.1.1 C++ 实现
#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;
}
1.5.2.1 Python 实现
#! /usr/bin/env python
import rospy

if __name__ == "__main__":

    # 演示日志函数
    rospy.init_node("hello_log")

    rospy.logdebug("DEBUG消息...")
    rospy.loginfo("INFO消息...")
    rospy.logwarn("WARN消息...")
    rospy.logerr("ERROR消息...")
    rospy.logfatal("FATAL消息...")

1.5.2 节点生命周期

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

  • 节点接收到了关闭信息,比如常用的 ctrl + c 快捷键就是关闭节点的信号;
  • 同名节点启动,导致现有节点退出;
  • 程序中的其他部分调用了节点关闭相关的API(C++中是ros::shutdown(),python中是rospy.signal_shutdown())
1.5.2.1 C++ 实现
  • 节点状态判断
/** \brief 检查节点是否已经退出
 -  -  ros::shutdown() 被调用且执行完毕后,该函数将会返回 false
 -  - \return true 如果节点还健在, false 如果节点已经火化了。
 */
bool ok();
  • 节点关闭
/*
*   关闭节点
*/
void shutdown();

02 ROS 中的源文件和头文件

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

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

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

2.1 自定义头文件调用 —— plumbing_head

2.1.0 需求 - 流程

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

流程

  • 编写头文件;
  • 编写可执行文件(同时也是源文件);
  • 编辑配置文件并执行

2.1.1 头文件 —— hello.h

在功能包下的 include/plumbing_head 目录下新建头文件: hello.h

#ifndef _HELLO_H
#define _HELLO_H

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

*/

namespace hello_ns{

class MyHello{

public:
    void run();
};
}
#endif

2.1.2 配置 c_cpp_properties.json(重要)

  • 点击 plumbing_head 目录下的 include 文件,右击选择“在终端中打开”
  • 输入 pwd
    在这里插入图片描述
  • 在c_cpp_properties.json输入
    “/home/rosmelodic/demo01_ws/src/plumbing_head/include/**”
  • 如果还报错的话,建议重新启动 vscode

2.1.3 源文件(也是可执行文件) —— hello.cpp

#include"ros/ros.h"
#include"plumbing_head/hello.h" // c_cpp_properties配置路径

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;
}

2.1.4 配置文件 - 头文件/可执行文件

  • 配置CMakeLists.txt文件,头文件相关配置如下
include_directories(
include
  ${catkin_INCLUDE_DIRS}
)
  • **可执行配置文件(就是源文件 hello.cpp)**配置方式与之前一致
add_executable(hello src/hello.cpp)

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

target_link_libraries(hello
  ${catkin_LIBRARIES}
)

2.1.5 编译执行

  • Ctrl+ shift + B
  • 启动 roscore(窗口1)
roscore
  • 启动 (窗口2)
cd demo01_ws/
source ./devel/setup.bash
rosrun plumbing_head hello

在这里插入图片描述

2.2 自定义源文件的调用 —— plumbing_head_src

2.2.0 需求 - 流程

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

流程:

  • 编写头文件;
  • 编写源文件;
  • 编写可执行文件;
  • 编辑配置文件并执行

2.2.1 头文件 —— hello.h

在功能包下的 include/plumbing_head_src 目录下新建头文件: hello.h

在这里插入代码片

2.2.2 配置 c_cpp_properties.json(重要)

  • 点击 plumbing_head_src 目录下的 include 文件,右击选择“在终端中打开”
  • 输入 pwd
    在这里插入图片描述
  • 在c_cpp_properties.json输入
    “/home/rosmelodic/demo01_ws/src/plumbing_head_src/include/**”
  • 如果还报错的话,建议重新启动 vscode

2.2.3 源文件 —— hello.cpp

在 src 目录下新建文件:hello.cpp

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

namespace hello_ns{

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

}

2.2.4 可执行文件(不同于源文件) —— use_hello.cpp

在 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;
}

2.2.5 配置文件(重要!!!)

  • 头文件与源文件相关配置:
include_directories(
include
  ${catkin_INCLUDE_DIRS}
)

# 配置C++库,head_src 表示库的名字
add_library(head_src # head_src 表示库的名字,可以随意取
  # 头文件
  include/${PROJECT_NAME}/hello.h
  # 源文件
  src/hello.cpp
)

# head_src 是库名字
add_dependencies(head_src ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})

target_link_libraries(head_src
  ${catkin_LIBRARIES}
)
  • 可执行文件配置:
add_executable(use_hello src/use_hello.cpp)

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

# 链接 可执行文件和C++库
target_link_libraries(use_hello
  head_src
  ${catkin_LIBRARIES}
)

2.2.6 编译执行

  • Ctrl+ shift + B
  • 启动 roscore(窗口1)
roscore
  • 启动 (窗口2)
cd demo01_ws/
source ./devel/setup.bash
rosrun plumbing_head_src use_hello

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

NqqGOGO

你的鼓励是我学习的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值