【Autolabor初级教程】ROS机器人入门 第 3 章 ROS通信机制进阶 笔记
官方API文档及参考源码
3.1 常用API
3.1.1 初始化
/*
ros::init()
1.argc ——封装实参个数(n+1)
2.argv ——封装参数的数组
3.name ——为节点命名(唯一性)
4.options ——节点启动选项
返回值:void
*/
-
argc与argv的使用
如果按照特定格式传入实参,ros可以加以使用,如用来设置全局参数、给节点重命名等
eg:
设置全局参数格式
rosrun 包名 可执行文件名 _参数名:=参数值
(注意参数名前加下划线)实测如果没有传入参数值,该全局参数也不会被创建
其他使用比如通过命令行传入参数,参考第二章
服务通信自定义srv(C++)
小节(6.对于客户端的优化实现(通过命令行输入参数,增加交互)) -
options的使用
ros运行过程中,节点名称需保证唯一(测试时重复启动同一个节点,后运行的节点会打断前一个)
在特定场景中,如果需要一个节点多次启动且正常运行,可以设置启动项
ros::init_options::AnonymousName
,设置后在创建ros节点时,会在自定义的节点名称后缀随机数,避免重名使用方法:
ros::init(argc,argv,"pub",ros::init_options::AnonymousName);
运行结果:
3.1.2 话题与服务相关对象
/*
ros::NodeHandle nh;
nh.advertise<>();nh.advertise
作用:创建发布者对象
模板:被发布的消息的类型
参数:
1.话题名称
2.队列长度
3.latch(可选) 如果设置为true,会保存最后一条发布者消息
当新的订阅方连接到发布方时,发布方会将这条消息发送给订阅者
返回值:
*/
latch设置为true的应用场景(以静态地图发布为例)
当发布方发布静态地图(地图信息很少更新或几乎不更新)信息时:
方案1:以固定频率发送地图信息(频率太高会占用资源,导致效率低;方案太低可能会影响实时性)
方案2:将地图发布方的latch设置为true,并且只发送一次数据,这样每当订阅方连接到发布方时,可以订阅到地图数据(仅连接时一次),这样既保证实时性,又不影响效率
使用方法:
ros::Publisher pub = nh.advertise<std_msgs::String>("pos",10,true);
3.1.3 回旋函数
/*
ros::spinOnce()
作用:处理一轮回调;在循环体内,处理所有可用的回调函数
*/
/*
ros::spin()
作用:进入循环处理回调
*/
**相同点:**二者都用于处理回调函数;
不同点:
这里先了解ros如何处理回调函数
ROS:回调函数处理与回调队列_ros 回调函数队列-CSDN博客
A.ros::spin()
当函数执行到ros::spin()
,就不再继续向下执行,而是循环且监听消息队列。
ros::spin() 是进入了循环监听消息队列,且执行对应回调函数,在 ros::spin() 后的语句不会执行到; ros::spin() 一般不会放在循环体里执行(会使循环失去意义),常放到程序的末尾执行。
B.ros::spinOnce()
ros::spinOnce()
只会执行一次回调函数,处理结束后继续执行ros::spinOnce()
后的代码
个人理解:当每次执行到回调函数时,并不会立即调用并处理,而是将其放到一个回调函数的消息队列中,spin就是循环监听这个队列,等待新消息的到来,当队列中有了新的回调函数就处理,而spinonce是处理一次消息队列中的所有消息,然后就去执行其他代码。
示例:
# 发布方
// 1.包含头文件
#include "ros/ros.h"
#include "std_msgs/String.h" // ros中文本类型
#include <sstream>
int main(int argc, char *argv[])
{
// 2.初始化ros节点
ros::init(argc,argv,"pub");
// 3.创建节点句柄
ros::NodeHandle nh;
// 4.创建发布者对象
ros::Publisher pub = nh.advertise<std_msgs::String>("pos",10);
// 5.编写发布逻辑并发布数据
// 要求以10hz的频率发布数据,并且文本后添加编号
// 先创建被发布消息
std_msgs::String msg;
// 发布频率
ros::Rate rate(5); // 该参数为指定频率
// 设置编号
int count = 0;
// 程序休眠,防止节点还未在roscore中注册时就发送消息
ros::Duration(3).sleep();
// 编写循环,循环中发布数据
while(ros::ok())
{
// 输出中文
setlocale(LC_ALL,"");
count++;
// 实现字符串拼接数字
// msg.data = "here";
std::stringstream ss;
ss << "hello ---> " << count;
msg.data = ss.str();
pub.publish(msg);
ROS_INFO("发布的数据是:%s",ss.str().c_str());
// 根据前面制定的发送频率自动休眠 休眠时间 = 1/频率;
rate.sleep();
}
return 0;
}
# 订阅方
// 1.包含头文件
#include "ros/ros.h"
#include "std_msgs/String.h" // ros中文本类型
void domsg1(const std_msgs::String::ConstPtr &msg)
{
// 通过msg获取并操作订阅到的数据
//ros::Duration(1);
ROS_INFO("A接受到的数据是:%s",msg->data.c_str());
}
void domsg2(const std_msgs::String::ConstPtr &msg)
{
// 通过msg获取并操作订阅到的数据
ROS_INFO("B接受到的数据是:%s",msg->data.c_str());
}
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
// 2.初始化ros节点
ros::init(argc,argv,"sub");
// 3.创建节点句柄
ros::NodeHandle nh;
// 4.创建订阅者对象
ros::Subscriber sub1 = nh.subscribe("pos",10,domsg1);
ros::Subscriber sub2 = nh.subscribe("pos",10,domsg2);
int count_tmp = 0;
while(ros::ok()){
count_tmp++;
ros::Duration(1).sleep();
ROS_INFO("这是第%d次接受数据。",count_tmp);
ros::spinOnce();
}
// 5.处理订阅到的数据
// 6.循环读取接收的数据,并调用回调函数处理
// 如果要用回调函数必须用spin
ROS_INFO("I'm here!");
return 0;
}
运行结果:
分析:
发布方的发布频率是每秒5个包,订阅方每1秒执行一次ros::spinOnce()
,此时消息队列里面存放了过去1s两个订阅方收到的2*5条消息(除第1次,可能发布方发布消息时订阅方已经开始计时),程序会处理当时队列中存在的所有回调函数,处理完毕后继续向下执行。
理解过程中遇到的问题:
发布方代码如上,订阅方代码刚开始如下:
#include "ros/ros.h"
#include "std_msgs/String.h" // ros中文本类型
void domsg1(const std_msgs::String::ConstPtr &msg)
{
//ros::Duration(1);
ROS_INFO("A接受到的数据是:%s",msg->data.c_str());
}
void domsg2(const std_msgs::String::ConstPtr &msg)
{
ROS_INFO("B接受到的数据是:%s",msg->data.c_str());
}
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
ros::init(argc,argv,"sub");
ros::NodeHandle nh;
ros::Subscriber sub1 = nh.subscribe("pos",10,domsg1);
ros::Subscriber sub2 = nh.subscribe("pos",10,domsg2);
//!!!注意这里注释了延时函数
//ros::Duration(1).sleep();
ros::spinOnce();
ROS_INFO("I'm here!");
return 0;
}
运行结果:
在这种状态下多次执行订阅方代码并没有订阅到数据,预期应该是会执行一次回调函数,所以一直不理解ros::spinOnce()
的工作原理(其实这里应该是对消息订阅的工作模式没有理解)
此时,解开这句注释 ros::Duration(1).sleep();
再次执行:
分析:
在执行以下两行代码时,应该是打开对于话题"pos"的订阅状态,如果不延时,程序很快(执行一两行代码的时间)就跑到ros::spinOnce()
,而发布方的发布频率是每秒5个包,在这么短时间,确实没有订阅到话题"pos"的相关消息,导致相关回调函数也没有得到执行;而在代码后延时1s,在这1s过程中,就可以接收到"pos"话题相关的消息,从而使回调函数得到执行。
ros::Subscriber sub1 = nh.subscribe("pos",10,domsg1);
ros::Subscriber sub2 = nh.subscribe("pos",10,domsg2);
3.1.4 时间
/*
时间相关操作:
1.准备
头文件:包含在ros.h中
节点初始化
NodeHandle创建:必须要创建一个,否则会导致API调用失败
2.获取当前时刻
ros::Time right_now = ros::Time::now();
right_now.toSec() 这里返回的是double类型的距参考系过去的秒数
right_now.sec 这里返回的是int32类型的距参考系过去的秒数
now函数会返当前时刻(被调用执行是)封装并返回;
参考系:1970年1月1日 00:00:00
PS:这里应该是零时区时间,中国是东八区,实测中国时间的参考系是1970年1月1日08:00:00
3.设定指定时刻
有以下格式:
ros::Time t1(s,ns);
ros::Time t2(s);
4.在程序中休眠指定时间
A.创建持续时间对象
ros::Duration duration(3.5);
B.休眠
duration.sleep();
5.时间运算
A.时刻与持续时间可以相加减
B.时刻之间只能相加不能相减(相加无意义)
C.持续时间可以相加减
//感觉6和7有点像delay()和定时器中断
6.设置运行频率
A.设置运行频率
B.搭配循环及休眠使代码按照制定频率运行
ros::Rate rate(1);
while(ros::ok()){
rate.sleep();
}
7.定时器使用
ros::Timer createTimer(ros::Duration period, //定时器时间间隔设置s
const ros::TimerCallback &callback, //回调函数,封装执行的操作
bool oneshot = false, //定时器是否只执行一次回调函数
bool autostart = true) const //是否自动启动定时器
如果autostart设置为false,需要通过timer.start()手动打开对应的定时器
*/
eg:
#include "ros/ros.h"
void time1_callback(const ros::TimerEvent& event){
ROS_INFO("定时器time1回调函数被调用");
ROS_INFO("当前时刻:%.2f",event.current_real.toSec());
}
int main(int argc, char *argv[])
{
/* code */
setlocale(LC_ALL,"");
ros::init(argc,argv,"api_time");
ros::NodeHandle nh;
ROS_INFO("-------------------------------");
ROS_INFO("获取当前时刻");
ros::Time right_now = ros::Time::now();
ROS_INFO("当前时刻:%.2f",right_now.toSec());
ROS_INFO("当前时刻:%d",right_now.sec);
ROS_INFO("-------------------------------");
ROS_INFO("设定指定时刻");
ros::Time t1(20,312789456);
ros::Time t2(456.789);
ROS_INFO("当前时刻:%.2f",t1.toSec());
ROS_INFO("当前时刻:%.2f",t2.toSec());
ROS_INFO("-------------------------------");
ROS_INFO("休眠指定时间");
ros::Time start_sleep = ros::Time::now();
ROS_INFO("当前时刻:%.2f",start_sleep.toSec());
ROS_INFO("开始休眠");
ros::Duration duration(3.5);
duration.sleep();
ROS_INFO("结束休眠");
ros::Time end_sleep = ros::Time::now();
ROS_INFO("当前时刻:%.2f",end_sleep.toSec());
ROS_INFO("-------------------------------");
ROS_INFO("时间计算");
// A.时刻与持续时间计算
ros::Time begin1 = ros::Time::now();
ros::Duration duration1(2);
ros::Time end1 = begin1 + duration1;
ROS_INFO("开始时刻是%.2f,持续时间为%.2fs,结束时刻是%.2f",begin1.toSec(),duration1.toSec(),end1.toSec());
// B.时刻与时刻的运算
// 两时刻相加没有对应的重载,只能相减,且结果为ros::Duration类型
ros::Duration sub = end1 - begin1;
// ros::Duration sum = end1 + begin1;
ROS_INFO("开始时刻是%.2f,结束时刻是%.2f,持续时间为%.2fs",begin1.toSec(),end1.toSec(),sub.toSec());
// C.持续时间与持续时间的运算
ros::Duration sum1 = duration1 + sub;
ros::Duration sub1 = duration1 - sub;
ROS_INFO("持续时间1是%.2f,持续时间2是%.2f,相加为%.2f",duration1.toSec(),sub.toSec(),sum1.toSec());
ROS_INFO("持续时间1是%.2f,持续时间2是%.2f,相减为%.2f",duration1.toSec(),sub.toSec(),sub1.toSec());
ROS_INFO("-------------------------------");
ROS_INFO("定时器使用");
ros::Timer timer = nh.createTimer(ros::Duration(1),time1_callback,false,false);
ros::Time test1 = ros::Time::now();
ROS_INFO("当前时刻:%.2f,定时器未启动",test1.toSec());
ros::Duration(3).sleep();
timer.start();
ros::Time test2 = ros::Time::now();
ROS_INFO("当前时刻:%.2f,计时器启动",test2.toSec());
ros::spin();
return 0;
}
运行结果:
Note:
-
ros::NodeHandle nh;
尽管
nh
没有被使用,但也必须初始化,否则运行会出现以下问题(编译正常) -
时刻之间相加没有对应的重载函数
3.1.5 其他函数
A.节点关闭函数ros::shutdown();
直接关闭节点
#include "ros/ros.h"
int main(int argc, char *argv[])
{
/* code */
ros::init(argc,argv,"api_log");
ros::NodeHandle nh;
int count = 0;
ros::Rate rate(10);
while (ros::ok())
{
/* code */
count++;
ROS_INFO("%d---------I'm here!",count);
if(count >= 20){
ros::shutdown();
//break;
}
rate.sleep();
}
ROS_INFO("Finally-------------I'm here!");
return 0;
}
以上代码提供了两种跳出循环的方式:
i.ros::shutdown();
通过关闭节点的方式跳出循环,其后的代码将不再被执行;
ii.break;
就是正常的跳出while
,代码继续向下执行
两次运行结果对比
i.
ii.
B.日志函数
ROS_DEBUG("调试信息"); //不会输出
ROS_INFO("一般信息"); //默认白色字体
ROS_WARN("警告信息"); //默认黄色字体
ROS_ERROR("错误信息");//默认红色字体
ROS_FATAL("严重错误");//默认红色字体
#include "ros/ros.h"
int main(int argc, char *argv[])
{
/* code */
setlocale(LC_ALL,"");
ros::init(argc,argv,"api_log");
ros::NodeHandle nh;
int count = 0;
ros::Rate rate(10);
ROS_DEBUG("调试信息"); //不会输出
ROS_INFO("一般信息"); //默认白色字体
ROS_WARN("警告信息"); //默认黄色字体
ROS_ERROR("错误信息");//默认红色字体
ROS_FATAL("严重错误");//默认红色字体
return 0;
}
Ps:ROS_DEBUG()
一般不输出,若要输出其中的信息,可以通过相关配置输出调试信息
3.2 ROS中的头文件与源文件
有以下两种类型封装代码:
- 自定义头文件调用(相当于只声明.h,实现部分在主代码的文件中)
- 自定义源文件调用(相当于.h声明,.cpp实现,主代码文件调用封装即可)
3.2.1 自定义头文件调用
这里真是把坑都踩完了😵🙃😫
-
编写头文件
A.创建.h文件
务必!放在!
include/包名
!放在包名这里!再右键新建.h文件B.编写.h文件
内容格式如下(这里只声明):
// 注意class声明结束有一个
;
#ifndef _HELLO_H #define _HELLO_H /* 声明namespace |--class |--run */ namespace hello_ns{ class myhello{ public: void run(); }; } #endif
-
编写可执行文件(同时也是源文件)
编写前请先编辑
.vscode/c_cpp_properties.json
,否则在include导入自定义包时会报错复制include的路径,加上
/**
,加入到”includePath“中(记得在上一行加上,
)添加好路径之后,就可以编辑
hello.cpp
i.导入头文件
ii.在namespace中实现run函数
iii.在main函数中进行调用
#include "ros/ros.h" #include "hello_head/hello.h" namespace hello_ns{ void myhello::run(){ ROS_INFO("hello,run已经被执行!"); }; } int main(int argc, char *argv[]) { /* code */ setlocale(LC_ALL,""); ros::init(argc,argv,"hellohead"); hello_ns::myhello hello; hello.run(); return 0; }
注意!这会不要妄想去编译他!会报错!这没有问题!因为还没有配置!
刚刚就是吃了这个亏,编译的时候一直说找不到头文件,一直在找
.vscode/c_cpp_properties.json
的问题,只要在编写代码的时候能自动补全,就是配置好了! -
编辑配置文件
CMakeLists
并执行i.
include
解注释一定注意这一步!问就是踩坑了!
ii.这三个部分解注释,并编辑蓝色箭头的地方
现在代码可以正常执行:
3.2.1 自定义头文件调用
-
编写头文件
同上,并配置
.vscode/c_cpp_properties.json
-
编写源文件
在
src
下新建hello.cpp
,用于run函数的实现#include "ros/ros.h" #include "hello_src/hello.h" namespace hello_ns{ void myhello::run(){ ROS_INFO("hello,run已经被执行!"); } }
-
编写可执行文件
在
src
下新建use_hello.cpp
,编写main函数#include "ros/ros.h" #include "hello_src/hello.h" int main(int argc, char *argv[]) { /* code */ setlocale(LC_ALL,""); ros::init(argc,argv,"hellohead"); hello_ns::myhello haha; haha.run(); return 0; }
-
编辑配置文件并执行
来到
CMakeLists
,库的相关配置:以下两部分解注释并编辑修改为
继续库的相关配置
可执行文件的相关配置
注意,在链接库时比之前多了一步,还要将自己的库链接进去
编译执行:
Ps:这里提到的可执行文件指的是程序主要运行的节点部分所对应的源文件,并非严格意义上的可执行文件