系列文章目录
第一章 ROS概述与环境搭建
第二章 ROS通信机制
第三章 ROS通信机制进阶
第四章 ROS运行管理
第五章 ROS常用组件
第六章 机器人系统仿真
第七章 机器人系统仿真
第八章 机器人系统仿真
第九章 机器人系统仿真
第十章 机器人系统仿真
文章目录
前言
- 用来复习回顾使用
- 本篇笔记视频讲解地址:Bilibili大学
- 笔记配合视频效果更好
- 如有错误和遗漏,可私信与评论进行指正,看到了会及时更改
怕什么真理无穷,进一寸有一寸的欢喜
- 与各位共勉
三、ROS通信机制进阶
API的英文即Application Programming Interface首字母的缩写。即程序之间的接口。我更倾向于把API理解为,程序之间的合约。
1.常用API
-
ROS节点的初始化相关API;
-
NodeHandle 的基本使用相关API;
-
话题的发布方,订阅方对象相关API;
-
服务的服务端,客户端对象相关API;
-
时间相关API;
-
日志输出相关API
准备工作:
- 在上一章节中的demo03_ws/src下创建/plumbing_apis功能包
实现流程:
-
右键src选择Create Catkin Package;
-
输入功能包名字/plumbing_apis,添加功能包依赖roscpp rospy std_msgs;
-
在/plumbing_apis/src下创建demo01_apis_pub.cpp;
-
代码如下:
#include "ros/ros.h" #include "std_msgs/String.h"//普通文本类型信息 #include <sstream> //字符拼接数字 //要求以10hz频率发布数据,并且文本后添加发布编号 // 1.包含头文件 int main(int argc, char *argv[]) { // 解决乱码问题 setlocale(LC_ALL,""); //2.初始化ros节点 ros::init(argc,argv,"talker"); //3.创建节点句柄 ros::NodeHandle nh; //4.创建发布者对象 ros::Publisher pub; pub = nh.advertise<std_msgs::String>("informations", 10); //5.编写发布逻辑并发布数据 std_msgs::String msg; //设置发布频率,专有函数调用就可以 ros::Rate rate(0.5); //设置发布编号 int count = 0; //发送数据之前添加一个休眠汗说,出现订阅丢失前几条数据,原因是注册第一条数据的时候,发布者还没有完成注册 ros::Duration(3).sleep(); //编写循环,循环发布数据 while (ros::ok()) { count++; //实现字符串拼接数字 两者需要拼接成一个字符串,需要包含新的头文件 std::stringstream ss; ss <<"hello ---> " << count; // msg.data = "hello"; msg.data = ss.str(); pub.publish(msg); //添加日志 先把字符串类stringstream转化为string再转化为c语言的string ROS_INFO("发布的数据:%s",ss.str().c_str()); //延时函数0.1s rate.sleep(); } return 0; }
-
在/plumbing_apis/src下创建demo02_apis_sub.cpp;
-
代码如下:
#include "ros/ros.h" #include "std_msgs/String.h"//普通文本类型信息 /** * @brief 订阅方实现:包含消息头文件、初始化ROS节点、创建节点句柄、创建订阅者对象、处理订阅到的数据、spin函数 * * @param argc * @param argv * @return int */ void doMsg(const std_msgs::String::ConstPtr &msg) { ROS_INFO("订阅的数据:%s",msg->data.c_str()); } int main(int argc, char *argv[]) { // 解决乱码问题 setlocale(LC_ALL,""); //2.初始化ros节点 节点名称要唯一 ros::init(argc,argv,"lisener"); //3.创建节点句柄 ros::NodeHandle nh; //4.创建发布者对象 ros::Subscriber sub; sub = nh.subscribe<std_msgs::String>("informations", 10, doMsg); // 5.处理订阅到的数据 ros::spin(); // spin函数是回旋函数,用于处理回调函数 return 0; }
-
在/plumbing_apis下新建目录srv并在目录下创建自定义文件/srv/AddInts.srv;
-
代码如下:
# 客户端请求时发送的两个数字 int32 num1 int32 num2 --- # 服务器响应发送的数据 int32 sum
-
在/plumbing_apis/src下创建demo03_apis_server.cpp;
-
代码如下:
#include "ros/ros.h" #include "plumbing_apis/AddInts.h" /** * @brief 服务端实现 * 解析客户端提交的数据,并运算产生响应 * 1、包含头文件 * 2、初始化ros节点 * 3、创建节点句柄 * 4、创建服务对象 * 5、处理请求并产生响应 * 6、spin()一定要加,循环则是spinOnce * @param argc * @param argv * @return int */ bool doNums(plumbing_apis::AddInts::Request &request, plumbing_apis::AddInts::Response &response) { // 1、处理请求 int num1 = request.num1; int num2 = request.num2; ROS_INFO("收到的请求数据是:num1=%d,num2=%d", num1, num2); // 2、组织响应 int sum = num1 + num2; response.sum = sum; ROS_INFO("求和结果sum=%d", sum); return true; } int main(int argc, char *argv[]) { setlocale(LC_ALL, ""); ROS_INFO("服务端已启动"); // * 2、初始化ros节点 ros::init(argc, argv, "HeiShui"); //保证名称唯一 // * 3、创建节点句柄 ros::NodeHandle nh; // * 4、创建服务对象 服务起了个话题名称addInts ros::ServiceServer server = nh.advertiseService("addInts", doNums); // * 5、处理请求并产生响应 // * 6、spin()一定要加,循环则是spinOnce ros::spin(); return 0; }
-
在/plumbing_apis/src下创建demo04_apis_client.cpp;
-
代码如下:
#include "ros/ros.h" #include "plumbing_apis/AddInts.h" /** * @brief * 1、包含头文件 * 2、初始化ros节点 * 3、创建节点句柄 * 4、创建一个客户端对象 * 5、提交申请,并进行处理响应 * * 6、添加argc、argv使用 * argc是判断有几个元素,写入argv * argv[]并不是指针,只是保存c风格的字符串数组,其中argv[0]保存的是客户端的地址,因此从argv[1]开始 * * 问题u: * 如果先启动客户端,那么会请求异常 * 需求: * 如果先启动客户端,不能直接出现异常,而是挂起,等待服务器启动之后,再正常请求 * 解决: * 在ros中内置了相关函数,这些函数可以让客户端启动后挂起,等待服务器的启动 * client.waitForExistence(); * ros::service::waitForService("服务话题"); */ int main(int argc, char* argv[]) { setlocale(LC_ALL, ""); if(argc!=3){ ROS_INFO("输入错误!请重新输入!"); return 1; } ROS_INFO("客户端启动成功"); // * 2、初始化ros节点 ros::init(argc, argv, "daBao"); // * 3、创建节点句柄 ros::NodeHandle nh; // * 4、创建一个客户端对象 ros::ServiceClient client = nh.serviceClient<plumbing_apis::AddInts>("addInts"); // * 5、提交申请,并进行处理响应 plumbing_apis::AddInts ai; // 5.1 发出请求 ai.request.num1 = atoi(argv[1]); ai.request.num2 = atoi(argv[2]); // 5.2 响应处理 // 调用判断服务器状态的函数 // 函数1 // client.waitForExistence(); ros::service::waitForService("addInts"); bool flag = client.call(ai); if (flag) { ROS_INFO("成功!"); ROS_INFO("sum=%d", ai.response.sum); } else { ROS_INFO("失败。。。"); } return 0; }
-
在/plumbing_apis/src下创建demo05_apis_time.cpp;
-
代码如下:
-
#include <ros/ros.h> /** * @brief * 需求1:演示时间相关的操作(获取当前时刻+设置指定时刻) * 实现: * 1、准备(头文件、节点初始化、节点句柄创建) * 2、获取当前时刻 * 3、设置指定时刻 * 需求2:程序执行中停顿5秒 * 实现: * 1、创建持续时间对象 * 2、休眠 * 需求3:已知程序开始运行的时刻 和程序运行的时间 ,求运行结束的时刻 * 实现: * 1、获取开始执行的时刻 * 2、模拟运行时间(N秒) * 3、计算结束时刻 = 开始 + 持续时间 * 注意: * 1、时刻与持续时间可以执行加减; * 2、持续时间之间也可以执行加减; * 3、时刻之间可以相减,不可以相加。 * 需求4:每隔1秒钟,在控制台输出一段文本 * 实现: * 1、策略1: ros::Rate() * 2、策略2: 定时器 * 注意: * 创建:nh.createTimer() * 參数1:时间间隔 * 參数2:回调函数(时间事件 TimerEvent) * 參数3:是否只执行一次 * 參数4:是香自动启动(当设置为 false, 需要手动调用 timer * * 定时器启动后:ros::spin() */ //回调函数 void cb(const ros::TimerEvent &event){ ROS_INFO("------------"); ROS_INFO("函数被调用的时刻:%.2f",event.current_real.toSec()); } int main(int argc, char *argv[]) { // *1、准备(头文件、节点初始化、节点句柄创建) setlocale(LC_ALL,""); ros::init(argc, argv, "hello_time"); ros::NodeHandle nh;//注意:必须创建句柄,否则会导致时间API调用失败(再NodeHandle会初始化时间操作) // *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; // error 不可以相加 ros::Duration du2 = begin - stop; ROS_INFO("时刻相减:%.2f", du2.toSec()); //持续时间与持续时间运算 ros::Duration du3 = du1 + du2;//0 ros::Duration du4 = du1 - du2;//10 ROS_INFO("持续时间与持续时间相加:%.2f", du3.toSec()); ROS_INFO("持续时间与持续时间相减:%.2f", du4.toSec()); //------------------------------------------------ ROS_INFO("---------------定时器---------------"); /* ros::Timer createTimer(ros::Duration period, //时间间隔----1s const ros::TimerCallback &callback, //回调函数----封装业务 bool oneshot = false, //是否是一次性 true隔1s执行一次,但只运行一次回调函数;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; }
-
在/plumbing_apis/src下创建demo06_apis_log.cpp;
-
代码如下:
-
#include <ros/ros.h> /** * @brief * 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; }
-
在/plumbing_apis/package.xml下添加依赖库相:
<build_depend>message_generation</build_depend> <exec_depend>message_runtime</exec_depend>
-
在/plumbing_apis/CMakeLists.txt下配置相关文件:
find_package(catkin REQUIRED COMPONENTS roscpp rospy std_msgs message_generation )
add_service_files( FILES AddInts.srv )
generate_messages( DEPENDENCIES std_msgs )
catkin_package( # INCLUDE_DIRS include # LIBRARIES plumbing_apis CATKIN_DEPENDS roscpp rospy std_msgs message_runtime # DEPENDS system_lib )
add_executable(demo01_apis_pub src/demo01_apis_pub.cpp) add_executable(demo02_apis_sub src/demo02_apis_sub.cpp) add_executable(demo03_apis_server src/demo03_apis_server.cpp) add_executable(demo04_apis_client src/demo04_apis_client.cpp) add_executable(demo05_apis_time src/demo05_apis_time.cpp) add_executable(demo06_apis_log src/demo06_apis_log.cpp)
add_dependencies(demo01_apis_pub ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS}) add_dependencies(demo02_apis_sub ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS}) add_dependencies(demo03_apis_server ${PROJECT_NAME}_gencpp) add_dependencies(demo04_apis_client ${PROJECT_NAME}_gencpp) add_dependencies(demo05_apis_time ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS}) add_dependencies(demo06_apis_log ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
target_link_libraries(demo01_apis_pub ${catkin_LIBRARIES} ) target_link_libraries(demo02_apis_sub ${catkin_LIBRARIES} ) target_link_libraries(demo03_apis_server ${catkin_LIBRARIES} ) target_link_libraries(demo04_apis_client ${catkin_LIBRARIES} ) target_link_libraries(demo05_apis_time ${catkin_LIBRARIES} ) target_link_libraries(demo06_apis_log ${catkin_LIBRARIES} )
1.1ROS节点的初始化相关API
C++
初始化API代码如下:
#include "ros/ros.h"
#include "std_msgs/String.h" //普通文本类型信息
#include <sstream>
//字符拼接数字
//要求以10hz频率发布数据,并且文本后添加发布编号
// 1.包含头文件
int main(int argc, char *argv[])
{
// 解决乱码问题
setlocale(LC_ALL, "");
/*
作用:ROS初始化函数
参数:
1、argc 封装函数传入的实参个数(n+1)
2、argv 封装参数的数组
3、name 为节点命名 应该保证唯一性
4、option 节点启动选择
返回值为空
使用:
1、argc argv 的使用
如果按照ros中的特定格式传入实参,那么ros可以加以使用,比如用来设置全局参数、给节点重新命名。。。。
2、options 的使用
节点名称需要保证唯一,同一个节点不能重复启动
同名节点再次启动,之前的的节点会被关闭
需求:特定场景下,一个节点需要多次启动,需要保证两次启动都是正常运行的情况,怎么办?
解决:设置启动项--ros::init_options::AnonymousName添加随机数
创建ROS节点是,会在用户自定义的节点名称后缀随机数,从而避免重名问题
*/
// 2.初始化ros节点
ros::init(argc, argv, "talker",ros::init_options::AnonymousName);
//3.创建节点句柄
ros::NodeHandle nh;
//4.创建发布者对象
ros::Publisher pub;
pub = nh.advertise<std_msgs::String>("informations", 10);
//5.编写发布逻辑并发布数据
std_msgs::String msg;
//设置发布频率,专有函数调用就可以
ros::Rate rate(0.5);
//设置发布编号
int count = 0;
//发送数据之前添加一个休眠汗说,出现订阅丢失前几条数据,原因是注册第一条数据的时候,发布者还没有完成注册
ros::Duration(3).sleep();
//编写循环,循环发布数据
while (ros::ok())
{
count++;
//实现字符串拼接数字 两者需要拼接成一个字符串,需要包含新的头文件
std::stringstream ss;
ss <<"hello ---> " << count;
// msg.data = "hello";
msg.data = ss.str();
pub.publish(msg);
//添加日志 先把字符串类stringstream转化为string再转化为c语言的string
ROS_INFO("发布的数据:%s",ss.str().c_str());
//延时函数0.1s
rate.sleep();
}
return 0;
}
注意-- 同名节点再次启动,上一个启动的节点会自动关闭,然后操作节点启动成功
2.NodeHandle 的基本使用相关API
在 roscpp 中,话题和服务的相关对象一般由 NodeHandle 创建 。
2.1消息发布对象
代码如下:
#include "ros/ros.h"
#include "std_msgs/String.h" //普通文本类型信息
#include <sstream>
//字符拼接数字
//要求以10hz频率发布数据,并且文本后添加发布编号
// 1.包含头文件
int main(int argc, char *argv[])
{
// 解决乱码问题
setlocale(LC_ALL, "");
/*
作用:ROS初始化函数
参数:
1、argc 封装函数传入的实参个数(n+1)
2、argv 封装参数的数组
3、name 为节点命名 应该保证唯一性
4、option 节点启动选择
返回值为空
使用:
1、argc argv 的使用
如果按照ros中的特定格式传入实参,那么ros可以加以使用,比如用来设置全局参数、给节点重新命名。。。。
2、options 的使用
节点名称需要保证唯一,同一个节点不能重复启动
同名节点再次启动,之前的的节点会被关闭
需求:特定场景下,一个节点需要多次启动,需要保证两次启动都是正常运行的情况,怎么办?
解决:设置启动项--ros::init_options::AnonymousName添加随机数
创建ROS节点是,会在用户自定义的节点名称后缀随机数,从而避免重名问题
*/
// 2.初始化ros节点
ros::init(argc, argv, "talker",ros::init_options::AnonymousName);
// 3.创建节点句柄
ros::NodeHandle nh;
// 4.创建发布者对象
ros::Publisher pub;
pub = nh.advertise<std_msgs::String>("informations", 10,true);
/*
作用:创建发布者对象
模板:被发布的消息的类型
参数:
1、话题名称
2、队列长度
3、latch(可选) 如果设置为true ,会保存发布方的最后一条消息,并且新的订阅对象连接到发布方时,发布方会将这条消息发布给订阅者
使用:
latch 设置为true 的作用?
导航之中 以静态地图发布为例,方案1:可以使用固定频率发送地图数据,但效率太低;方案2:可以将地图发布对象的latch设置为true ,并且发布方只发送一次数据,每当订阅者连接时,将地图数据发送给订阅者(只发送一次),提高数据发送效率
*/
// 5.编写发布逻辑并发布数据
std_msgs::String msg;
//设置发布频率,专有函数调用就可以
ros::Rate rate(0.5);
//设置发布编号
int count = 0;
//发送数据之前添加一个休眠汗说,出现订阅丢失前几条数据,原因是注册第一条数据的时候,发布者还没有完成注册
ros::Duration(3).sleep();
//编写循环,循环发布数据
while (ros::ok())
{
count++;
//实现字符串拼接数字 两者需要拼接成一个字符串,需要包含新的头文件
std::stringstream ss;
ss << "hello ---> " << count;
// msg.data = "hello";
msg.data = ss.str();
if(count<=10)
{
pub.publish(msg);
//添加日志 先把字符串类stringstream转化为string再转化为c语言的string
ROS_INFO("发布的数据:%s", ss.str().c_str());
}
//延时函数0.1s
rate.sleep();
}
return 0;
}
latch 设置为true 的作用?
导航之中 以静态地图发布为例:
方案1:可以使用固定频率发送地图数据,但效率太低;
方案2:可以将地图发布对象的latch设置为true ,并且发布方只发送一次数据,每当订阅者连接时,将地图数据发送给订阅者(只发送一次),提高数据发送效率。
2.2消息订阅对象
代码如下:
#include "ros/ros.h"
#include "std_msgs/String.h"//普通文本类型信息
/**
* @brief 订阅方实现:包含消息头文件、初始化ROS节点、创建节点句柄、创建订阅者对象、处理订阅到的数据、spin函数
*
* @param argc
* @param argv
* @return int
*/
void doMsg(const std_msgs::String::ConstPtr &msg)
{
ROS_INFO("订阅的数据:%s",msg->data.c_str());
}
int main(int argc, char *argv[])
{
// 解决乱码问题
setlocale(LC_ALL,"");
//2.初始化ros节点 节点名称要唯一
ros::init(argc,argv,"lisener");
//3.创建节点句柄
ros::NodeHandle nh;
//4.创建发布者对象
ros::Subscriber sub;
sub = nh.subscribe<std_msgs::String>("informations", 10, doMsg);
/*
作用:生成某个话题的订阅对象
模板:nh.subscribe <订阅消息的类型> ("订阅话题", 消息队列长度,回调函数)
参数:
1、话题名称
2、消息队列长度
3、回调函数 传入该消息的共享指针,该消息不能被修改,因为可能其他订阅对象也会使用该消息
使用:
回调函数的作用?
当订阅到一条消息时,自动执行回调函数(调用子函数),可将相关信息输出至屏幕。。。。
返回:
调用成功时,返回一个订阅者对象信息;
调用失败时,返回空对象;
*/
if(sub)
{
ROS_INFO("订阅成功");
}else{
ROS_INFO("订阅失败");
}
// 5.处理订阅到的数据
ros::spin(); // spin函数是回旋函数,用于处理回调函数
return 0;
}
修改话题名称根据if判断订阅对象是否成功。
2.3服务对象
代码如下:
#include "ros/ros.h"
#include "plumbing_apis/AddInts.h"
/**
* @brief 服务端实现
* 解析客户端提交的数据,并运算产生响应
* 1、包含头文件
* 2、初始化ros节点
* 3、创建节点句柄
* 4、创建服务对象
* 5、处理请求并产生响应
* 6、spin()一定要加,循环则是spinOnce
* @param argc
* @param argv
* @return int
*/
bool doNums(plumbing_apis::AddInts::Request &request,
plumbing_apis::AddInts::Response &response)
{
// 1、处理请求
int num1 = request.num1;
int num2 = request.num2;
ROS_INFO("收到的请求数据是:num1=%d,num2=%d", num1, num2);
// 2、组织响应
int sum = num1 + num2;
response.sum = sum;
ROS_INFO("求和结果sum=%d", sum);
return true;
}
int main(int argc, char *argv[])
{
setlocale(LC_ALL, "");
ROS_INFO("服务端已启动");
// * 2、初始化ros节点
ros::init(argc, argv, "HeiShui"); //保证名称唯一
// * 3、创建节点句柄
ros::NodeHandle nh;
// * 4、创建服务对象 服务起了个话题名称addInts
ros::ServiceServer server = nh.advertiseService("addInts", doNums);
/*
作用:该函数可以连接到 ROS master,并提供一个具有给定名称的服务对象
模板:ros::ServiceServer service = handle.advertiseService("my_service", callback);
参数:
1、my_service 服务的主题名称
2、callback 接收到请求时,需要处理请求的回调函数
使用:
生成服务端对象
返回:
成功返回 服务对象;
失败返回 空对象;
*/
// * 5、处理请求并产生响应
// * 6、spin()一定要加,循环则是spinOnce
ros::spin();
return 0;
}
2.4客户端对象
代码如下:
#include "ros/ros.h"
#include "plumbing_apis/AddInts.h"
/**
* @brief
* 1、包含头文件
* 2、初始化ros节点
* 3、创建节点句柄
* 4、创建一个客户端对象
* 5、提交申请,并进行处理响应
*
* 6、添加argc、argv使用
* argc是判断有几个元素,写入argv
* argv[]并不是指针,只是保存c风格的字符串数组,其中argv[0]保存的是客户端的地址,因此从argv[1]开始
*
* 问题u:
* 如果先启动客户端,那么会请求异常
* 需求:
* 如果先启动客户端,不能直接出现异常,而是挂起,等待服务器启动之后,再正常请求
* 解决:
* 在ros中内置了相关函数,这些函数可以让客户端启动后挂起,等待服务器的启动
* client.waitForExistence();
* ros::service::waitForService("服务话题");
*/
int main(int argc, char* argv[])
{
setlocale(LC_ALL, "");
if(argc!=3){
ROS_INFO("输入错误!请重新输入!");
return 1;
}
ROS_INFO("客户端启动成功");
// * 2、初始化ros节点
ros::init(argc, argv, "daBao");
// * 3、创建节点句柄
ros::NodeHandle nh;
// * 4、创建一个客户端对象
ros::ServiceClient client = nh.serviceClient<plumbing_apis::AddInts>("addInts");
/*
作用:创建一个请求服务的客户端对象
模板:nh.serviceClient <xxx.srv文件数据类型> ("服务话题名称")
参数:
1、服务主题名称
使用:
向服务端发出申请,根据服务端返回数据进行处理响应
*/
// * 5、提交申请,并进行处理响应
plumbing_apis::AddInts ai;
// 5.1 发出请求
ai.request.num1 = atoi(argv[1]);
ai.request.num2 = atoi(argv[2]);
// 5.2 响应处理
// 调用判断服务器状态的函数
// 函数1
// client.waitForExistence();
/*
作用:等待服务可用,否则一直处于阻塞状态
模板: client.waitForExistence();
参数:
1、service_name 被"等待"的服务的话题名称
使用:
等待服务函数
返回:
成功返回 bool true;
失败返回 bool false;
*/
ros::service::waitForService("addInts");
/*
作用:等待服务可用,否则一直处于阻塞状态
模板:ros::service::waitForService("服务话题名称");
参数:
1、service_name 被"等待"的服务的话题名称
使用:
等待服务函数1
返回:
成功返回 bool true;
失败返回 bool false;
*/
bool flag = client.call(ai);
/*
作用:发送请求
模板:bool xxx= client.call(Service& service);
参数:
1、Service& service xxx.srv文件数据类型数据
使用:
请求发送函数
返回:
成功返回 bool true;
失败返回 bool false;
*/
if (flag)
{
ROS_INFO("成功!");
ROS_INFO("sum=%d", ai.response.sum);
} else {
ROS_INFO("失败。。。");
}
return 0;
}
3.回旋函数相关API
在ROS程序中使用 ros::spin() 和 ros::spinOnce() 两个回旋函数,用于处理回调函数。
3.1spinOnce()
处理一轮回调
* 一般应用场景:
* 在循环体内,处理所有可用的回调函数
代码如下:
#include "ros/ros.h"
#include "std_msgs/String.h" //普通文本类型信息
#include <sstream>
//字符拼接数字
//要求以10hz频率发布数据,并且文本后添加发布编号
// 1.包含头文件
int main(int argc, char *argv[])
{
// 解决乱码问题
setlocale(LC_ALL, "");
/*
作用:ROS初始化函数
参数:
1、argc 封装函数传入的实参个数(n+1)
2、argv 封装参数的数组
3、name 为节点命名 应该保证唯一性
4、option 节点启动选择
返回值为空
使用:
1、argc argv 的使用
如果按照ros中的特定格式传入实参,那么ros可以加以使用,比如用来设置全局参数、给节点重新命名。。。。
2、options 的使用
节点名称需要保证唯一,同一个节点不能重复启动
同名节点再次启动,之前的的节点会被关闭
需求:特定场景下,一个节点需要多次启动,需要保证两次启动都是正常运行的情况,怎么办?
解决:设置启动项--ros::init_options::AnonymousName添加随机数
创建ROS节点是,会在用户自定义的节点名称后缀随机数,从而避免重名问题
*/
// 2.初始化ros节点
ros::init(argc, argv, "talker",ros::init_options::AnonymousName);
// 3.创建节点句柄
ros::NodeHandle nh;
// 4.创建发布者对象
ros::Publisher pub;
pub = nh.advertise<std_msgs::String>("informations", 10,true);
/*
作用:创建发布者对象
模板:被发布的消息的类型
参数:
1、话题名称
2、队列长度
3、latch(可选) 如果设置为true ,会保存发布方的最后一条消息,并且新的订阅对象连接到发布方时,发布方会将这条消息发布给订阅者
使用:
latch 设置为true 的作用?
导航之中 以静态地图发布为例,方案1:可以使用固定频率发送地图数据,但效率太低;方案2:可以将地图发布对象的latch设置为true ,并且发布方只发送一次数据,每当订阅者连接时,将地图数据发送给订阅者(只发送一次),提高数据发送效率
*/
// 5.编写发布逻辑并发布数据
std_msgs::String msg;
//设置发布频率,专有函数调用就可以
ros::Rate rate(0.5);
//设置发布编号
int count = 0;
//发送数据之前添加一个休眠汗说,出现订阅丢失前几条数据,原因是注册第一条数据的时候,发布者还没有完成注册
ros::Duration(3).sleep();
//编写循环,循环发布数据
while (ros::ok())
{
count++;
//实现字符串拼接数字 两者需要拼接成一个字符串,需要包含新的头文件
std::stringstream ss;
ss << "hello ---> " << count;
// msg.data = "hello";
msg.data = ss.str();
// if(count<=10)
// {
// pub.publish(msg);
// //添加日志 先把字符串类stringstream转化为string再转化为c语言的string
// ROS_INFO("发布的数据:%s", ss.str().c_str());
// }
pub.publish(msg);
//添加日志 先把字符串类stringstream转化为string再转化为c语言的string
ROS_INFO("发布的数据:%s", ss.str().c_str());
//延时函数0.1s
rate.sleep();
ros::spinOnce();
ROS_INFO("一轮回调执行完毕....");
}
return 0;
}
3.2spin()
进入循环处理回调
* 一般应用场景:
* 程序会一直回旋调用回调函数
代码如下:
#include "ros/ros.h"
#include "std_msgs/String.h"//普通文本类型信息
/**
* @brief 订阅方实现:包含消息头文件、初始化ROS节点、创建节点句柄、创建订阅者对象、处理订阅到的数据、spin函数
*
* @param argc
* @param argv
* @return int
*/
void doMsg(const std_msgs::String::ConstPtr &msg)
{
ROS_INFO("订阅的数据:%s",msg->data.c_str());
}
int main(int argc, char *argv[])
{
// 解决乱码问题
setlocale(LC_ALL,"");
//2.初始化ros节点 节点名称要唯一
ros::init(argc,argv,"lisener");
//3.创建节点句柄
ros::NodeHandle nh;
//4.创建发布者对象
ros::Subscriber sub;
sub = nh.subscribe<std_msgs::String>("informations", 10, doMsg);
/*
作用:生成某个话题的订阅对象
模板:nh.subscribe <订阅消息的类型> ("订阅话题", 消息队列长度,回调函数)
参数:
1、话题名称
2、消息队列长度
3、回调函数 传入该消息的共享指针,该消息不能被修改,因为可能其他订阅对象也会使用该消息
使用:
回调函数的作用?
当订阅到一条消息时,自动执行回调函数(调用子函数),可将相关信息输出至屏幕。。。。
返回:
调用成功时,返回一个订阅者对象信息;
调用失败时,返回空对象;
*/
// 5.处理订阅到的数据
ros::spin(); // spin函数是回旋函数,用于处理回调函数
ROS_INFO("spin后的语句");
return 0;
}
区别:
spinOnce()函数后面的语句可以运行,而spin()函数后面的语句并不会运行;
4.时间相关API
ROS中时间相关的API是极其常用,比如:获取当前时刻、持续时间的设置、执行频率、休眠、定时器…都与时间相关
4.1时刻
代码如下:
#include <ros/ros.h>
/**
* @brief
* 需求:演示时间相关的操作(获取当前时刻+设置指定时刻)
* 实现:
* 1、准备(头文件、节点初始化、节点句柄创建)
* 2、获取当前时刻
* 3、设置指定时刻
*/
int main(int argc, char *argv[])
{
// *1、准备(头文件、节点初始化、节点句柄创建)
setlocale(LC_ALL,"");
ros::init(argc, argv, "hello_time");
ros::NodeHandle nh;//注意:必须创建句柄,否则会导致时间API调用失败(再NodeHandle会初始化时间操作)
// *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;
}
参考系:1970年01月01日 00:00:00;
4.2持续时间
代码如下:
#include <ros/ros.h>
/**
* @brief
* 需求1:演示时间相关的操作(获取当前时刻+设置指定时刻)
* 实现:
* 1、准备(头文件、节点初始化、节点句柄创建)
* 2、获取当前时刻
* 3、设置指定时刻
* 需求2:程序执行中停顿5秒
* 实现:
* 1、创建持续时间对象
* 2、休眠
*/
int main(int argc, char *argv[])
{
// *1、准备(头文件、节点初始化、节点句柄创建)
setlocale(LC_ALL,"");
ros::init(argc, argv, "hello_time");
ros::NodeHandle nh;//注意:必须创建句柄,否则会导致时间API调用失败(再NodeHandle会初始化时间操作)
// *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());
return 0;
}
主要对一段时间进行操作;
4.3持续时间与时刻运算
代码如下:
#include <ros/ros.h>
/**
* @brief
* 需求1:演示时间相关的操作(获取当前时刻+设置指定时刻)
* 实现:
* 1、准备(头文件、节点初始化、节点句柄创建)
* 2、获取当前时刻
* 3、设置指定时刻
* 需求2:程序执行中停顿5秒
* 实现:
* 1、创建持续时间对象
* 2、休眠
* 需求2:已知程序开始运行的时刻 和程序运行的时间 ,求运行结束的时刻
* 实现:
* 1、获取开始执行的时刻
* 2、模拟运行时间(N秒)
* 3、计算结束时刻 = 开始 + 持续时间
* 注意:
* 1、时刻与持续时间可以执行加减;
* 2、持续时间之间也可以执行加减;
* 3、时刻之间可以相减,不可以相加。
*/
int main(int argc, char *argv[])
{
// *1、准备(头文件、节点初始化、节点句柄创建)
setlocale(LC_ALL,"");
ros::init(argc, argv, "hello_time");
ros::NodeHandle nh;//注意:必须创建句柄,否则会导致时间API调用失败(再NodeHandle会初始化时间操作)
// *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; // error 不可以相加
ros::Duration du2 = begin - stop;
ROS_INFO("时刻相减:%.2f", du2.toSec());
//持续时间与持续时间运算
ros::Duration du3 = du1 + du2;//0
ros::Duration du4 = du1 - du2;//10
ROS_INFO("持续时间与持续时间相加:%.2f", du3.toSec());
ROS_INFO("持续时间与持续时间相减:%.2f", du4.toSec());
return 0;
}
- 注意:
- 1、时刻与持续时间可以执行加减;
- 2、持续时间之间也可以执行加减;
- 3、时刻之间可以相减,不可以相加。
4.4设置运行频率
代码如下:
//设置发布频率,专有函数调用就可以
ros::Rate rate(0.5);
//延时函数0.1s
rate.sleep();
4.5定时器
代码如下:
#include <ros/ros.h>
/**
* @brief
* 需求1:演示时间相关的操作(获取当前时刻+设置指定时刻)
* 实现:
* 1、准备(头文件、节点初始化、节点句柄创建)
* 2、获取当前时刻
* 3、设置指定时刻
* 需求2:程序执行中停顿5秒
* 实现:
* 1、创建持续时间对象
* 2、休眠
* 需求3:已知程序开始运行的时刻 和程序运行的时间 ,求运行结束的时刻
* 实现:
* 1、获取开始执行的时刻
* 2、模拟运行时间(N秒)
* 3、计算结束时刻 = 开始 + 持续时间
* 注意:
* 1、时刻与持续时间可以执行加减;
* 2、持续时间之间也可以执行加减;
* 3、时刻之间可以相减,不可以相加。
* 需求4:每隔1秒钟,在控制台输出一段文本
* 实现:
* 1、策略1: ros::Rate()
* 2、策略2: 定时器
* 注意:
* 创建:nh.createTimer()
* 參数1:时间间隔
* 參数2:回调函数(时间事件 TimerEvent)
* 參数3:是否只执行一次
* 參数4:是香自动启动(当设置为 false, 需要手动调用 timer
*
* 定时器启动后:ros::spin()
*/
//回调函数
void cb(const ros::TimerEvent &event){
ROS_INFO("------------");
ROS_INFO("函数被调用的时刻:%.2f",event.current_real.toSec());
}
int main(int argc, char *argv[])
{
// *1、准备(头文件、节点初始化、节点句柄创建)
setlocale(LC_ALL,"");
ros::init(argc, argv, "hello_time");
ros::NodeHandle nh;//注意:必须创建句柄,否则会导致时间API调用失败(再NodeHandle会初始化时间操作)
// *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; // error 不可以相加
ros::Duration du2 = begin - stop;
ROS_INFO("时刻相减:%.2f", du2.toSec());
//持续时间与持续时间运算
ros::Duration du3 = du1 + du2;//0
ros::Duration du4 = du1 - du2;//10
ROS_INFO("持续时间与持续时间相加:%.2f", du3.toSec());
ROS_INFO("持续时间与持续时间相减:%.2f", du4.toSec());
//------------------------------------------------
ROS_INFO("---------------定时器---------------");
/* ros::Timer createTimer(ros::Duration period, //时间间隔----1s
const ros::TimerCallback &callback, //回调函数----封装业务
bool oneshot = false, //是否是一次性 true隔1s执行一次,但只运行一次回调函数;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;
}
ROS 中内置了专门的定时器,可以实现与 ros::Rate 类似的效果:。
5.其他函数API
5.1节点关闭APIros::shutdown()
循环发布消息时,循环的判断条件一般由节点状态来控制,C++中可以通过 ros::ok() 来判断节点状态是否正常,其中导致节点退出的原因主要有如下几种:
- 节点接收到了关闭信息,比如常用的 ctrl + c 快捷键就是关闭节点的信号;
- 同名节点启动,导致现有节点退出;
- 程序中的其他部分调用了节点关闭相关的API(C++中是ros::shutdown());
代码如下:
#include "ros/ros.h"
#include "std_msgs/String.h" //普通文本类型信息
#include <sstream>
//字符拼接数字
//要求以10hz频率发布数据,并且文本后添加发布编号
// 1.包含头文件
int main(int argc, char *argv[])
{
// 解决乱码问题
setlocale(LC_ALL, "");
/*
作用:ROS初始化函数
参数:
1、argc 封装函数传入的实参个数(n+1)
2、argv 封装参数的数组
3、name 为节点命名 应该保证唯一性
4、option 节点启动选择
返回值为空
使用:
1、argc argv 的使用
如果按照ros中的特定格式传入实参,那么ros可以加以使用,比如用来设置全局参数、给节点重新命名。。。。
2、options 的使用
节点名称需要保证唯一,同一个节点不能重复启动
同名节点再次启动,之前的的节点会被关闭
需求:特定场景下,一个节点需要多次启动,需要保证两次启动都是正常运行的情况,怎么办?
解决:设置启动项--ros::init_options::AnonymousName添加随机数
创建ROS节点是,会在用户自定义的节点名称后缀随机数,从而避免重名问题
*/
// 2.初始化ros节点
ros::init(argc, argv, "talker",ros::init_options::AnonymousName);
// 3.创建节点句柄
ros::NodeHandle nh;
// 4.创建发布者对象
ros::Publisher pub;
pub = nh.advertise<std_msgs::String>("informations", 10,true);
/*
作用:创建发布者对象
模板:被发布的消息的类型
参数:
1、话题名称
2、队列长度
3、latch(可选) 如果设置为true ,会保存发布方的最后一条消息,并且新的订阅对象连接到发布方时,发布方会将这条消息发布给订阅者
使用:
latch 设置为true 的作用?
导航之中 以静态地图发布为例,方案1:可以使用固定频率发送地图数据,但效率太低;方案2:可以将地图发布对象的latch设置为true ,并且发布方只发送一次数据,每当订阅者连接时,将地图数据发送给订阅者(只发送一次),提高数据发送效率
*/
// 5.编写发布逻辑并发布数据
std_msgs::String msg;
//设置发布频率,专有函数调用就可以
ros::Rate rate(0.5);
//设置发布编号
int count = 0;
//发送数据之前添加一个休眠汗说,出现订阅丢失前几条数据,原因是注册第一条数据的时候,发布者还没有完成注册
ros::Duration(3).sleep();
//编写循环,循环发布数据
while (ros::ok())
{
//如果计数器>=50,那么关闭节点
if (count >= 50)
{
ros::shutdown();
}
//------------------------
count++;
//实现字符串拼接数字 两者需要拼接成一个字符串,需要包含新的头文件
std::stringstream ss;
ss << "hello ---> " << count;
// msg.data = "hello";
msg.data = ss.str();
// if(count<=10)
// {
// pub.publish(msg);
// //添加日志 先把字符串类stringstream转化为string再转化为c语言的string
// ROS_INFO("发布的数据:%s", ss.str().c_str());
// }
pub.publish(msg);
//添加日志 先把字符串类stringstream转化为string再转化为c语言的string
ROS_INFO("发布的数据:%s", ss.str().c_str());
//延时函数0.1s
rate.sleep();
ros::spinOnce();
ROS_INFO("一轮回调执行完毕....");
}
return 0;
}
5.2spinOnce()
在ROS中常用的日志被划分成如下级别:
- DEBUG(调试):只在调试时使用,此类消息不会输出到控制台;
- INFO(信息):标准消息,一般用于说明系统内正在执行的操作;
- WARN(警告):提醒一些异常情况,但程序仍然可以执行;
- ERROR(错误):提示错误信息,此类错误会影响程序运行;
- FATAL(严重错误):此类错误将阻止节点继续运行。
代码如下:
#include <ros/ros.h>
/**
* @brief
* 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;
}
总结
- 基本掌握ROS常用API的使用