第 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++实现中,如何使用头文件与源文件的方式封装代码,具体内容如下:
- 设置头文件,可执行文件作为源文件;
- 分别设置头文件,源文件与可执行文件。
在ROS中关于头文件的使用,核心内容在于CMakeLists.txt文件的配置,不同的封装方式,配置上也有差异。
3.2.1 自定义头文件调用
**需求:**设计头文件,可执行文件本身作为源文件。
流程:
- 编写头文件;
- 编写可执行文件(同时也是源文件);
- 编辑配置文件并执行。
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.头文件
头文件设置于 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
万分感谢赵老师事无巨细的视频教学和清晰明了的笔记~