前言
本文详细介绍了ROS中的C++的常用API,包括节点初始化、话题与服务对象创建、回旋函数、时间管理和日志功能。同时,探讨了ROS头文件和源文件的调用,包括自定义头文件和源文件的使用方法。内容涵盖了从节点初始化到程序运行控制的各个方面,是理解ROS程序设计的重要参考资料。
一、常用API
1.init函数
新建一个工作空间demo04_ws,新建一个功能包plumbing_apis,新建文件demo_apis_pub.cpp。
流程如下:
ros:foxy(1) noetic(2) ?
2
lvl@lvl-Legion-Y7000P-IRX9:~$ mkdir -p ~/demo04_ws/src
lvl@lvl-Legion-Y7000P-IRX9:~$ cd demo04_ws/src
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws/src$ catkin_init_workspace
Creating symlink "/home/lvl/demo04_ws/src/CMakeLists.txt" pointing to "/opt/ros/noetic/share/catkin/cmake/toplevel.cmake"
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws/src$ cd ../
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ catkin_make
Base path: /home/lvl/demo04_ws
Source space: /home/lvl/demo04_ws/src
Build space: /home/lvl/demo04_ws/build
Devel space: /home/lvl/demo04_ws/devel
Install space: /home/lvl/demo04_ws/install
####
#### Running command: "cmake /home/lvl/demo04_ws/src -DCATKIN_DEVEL_PREFIX=/home/lvl/demo04_ws/devel -DCMAKE_INSTALL_PREFIX=/home/lvl/demo04_ws/install -G Unix Makefiles" in "/home/lvl/demo04_ws/build"
####
-- The C compiler identification is GNU 9.4.0
-- The CXX compiler identification is GNU 9.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Using CATKIN_DEVEL_PREFIX: /home/lvl/demo04_ws/devel
-- Using CMAKE_PREFIX_PATH: /home/lvl/catkin_ws/devel;/opt/ros/noetic
-- This workspace overlays: /home/lvl/catkin_ws/devel;/opt/ros/noetic
-- Found PythonInterp: /usr/bin/python3 (found suitable version "3.8.10", minimum required is "3")
-- Using PYTHON_EXECUTABLE: /usr/bin/python3
-- Using Debian Python package layout
-- Found PY_em: /usr/lib/python3/dist-packages/em.py
-- Using empy: /usr/lib/python3/dist-packages/em.py
-- Using CATKIN_ENABLE_TESTING: ON
-- Call enable_testing()
-- Using CATKIN_TEST_RESULTS_DIR: /home/lvl/demo04_ws/build/test_results
-- Forcing gtest/gmock from source, though one was otherwise available.
-- Found gtest sources under '/usr/src/googletest': gtests will be built
-- Found gmock sources under '/usr/src/googletest': gmock will be built
-- Found PythonInterp: /usr/bin/python3 (found version "3.8.10")
-- Found Threads: TRUE
-- Using Python nosetests: /usr/bin/nosetests3
-- catkin 0.8.10
-- BUILD_SHARED_LIBS is on
-- BUILD_SHARED_LIBS is on
-- Configuring done
-- Generating done
-- Build files have been written to: /home/lvl/demo04_ws/build
####
#### Running command: "make -j28 -l28" in "/home/lvl/demo04_ws/build"
####
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ cd src
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws/src$ catkin_create_pkg plumbing_apis std-msgs roscpp rospy
Created file plumbing_apis/package.xml
Created file plumbing_apis/CMakeLists.txt
Created folder plumbing_apis/include/plumbing_apis
Created folder plumbing_apis/src
Successfully created files in /home/lvl/demo04_ws/src/plumbing_apis. Please adjust the values in package.xml.
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws/src$ cd plumbing_apis/src
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws/src/plumbing_apis/src$ touch demo_apis_pub.cpp
编辑c++文件:
#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>
/*
发布方实现
1.包含头文件
2.初始化ros节点
3.创建节点句柄
4.创建发布者对象
5.编写发布逻辑并发布数据
*/
int main(int argc ,char * argvs[])
{
setlocale(LC_ALL,"");
ros::init(argc,argvs,"erguizi",ros::init_options::AnonymousName); //节点名称为erguizi
ros::NodeHandle nh;
ros::Publisher pub = nh.advertise<std_msgs::String>("house",10); //话题为房子,队列中最多缓存十条数据
//5-1.先创建没有被发布的信息
std_msgs::String msg;
ros::Rate rate(10); //10hz
int count = 0;
//5-2.编写循环,循环中发布数据
ros::Duration(3).sleep(); //程序执行到这里休眠三秒钟
while(ros::ok()) //只要节点活着,则循环继续
{
count ++;
//实现字符串拼接
std::stringstream ss;
ss << "hello --->"<<count;
msg.data = ss.str();
//msg.data = "hello";
pub.publish(msg);
//添加日志
ROS_INFO("发布的数据是:%s",ss.str().c_str());
rate.sleep(); //睡觉0.1s
}
return 0;
}
ROS::init参数介绍
1.argc:封装实参的个数(传入n个参数,argc = 参数+1,因为第一个参数是文件名)
2.argv:封装参数的指针数组(字符串类型)
3.name:为节点命名,是字符串类型的,有唯一性
4.option:节点启动选项
返回值:void
argc 与 argv 的使用
如果按照ros中的特定格式传入实参,那么ROS可以加以使用,比如可以设置全局参数,给节点重命名
启动节点为参数服务器设置参数,采用一个下划线一个参数名冒号等号具体值,这时下划线length就被传递给argv了。
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ rosrun plumbing_apis apis _length:=2
[ INFO] [1736577681.802182066]: 发布的数据是:hello --->1
[ INFO] [1736577681.804844789]: 发布的数据是:hello --->2
....
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ rosparam list
/rosdistro
/roslaunch/uris/host_lvl_legion_y7000p_irx9__45093
/rosversion
/run_id
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ rosrun plumbing_apis apis _length:=2
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ rosparam list
/erguizi_1736577943122039222/length
/rosdistro
/roslaunch/uris/host_lvl_legion_y7000p_irx9__45093
/rosversion
/run_id
查看参数服务器发现多了length参数,再查看其值:
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ rosparam get /erguizi_1736577943122039222/length
2
options的使用
节点名称要保证唯一性,会导致一个问题:同一个节点不能重复启动,如果重复启动之前启动的会被关闭
需求:特定场景下需要一个节点多次启动且能正常运行,怎么办呢?使用options参数
ros::init(argc,argvs,"erguizi",ros::init_options::AnonymousName); //节点名称为erguizi
2.话题与服务相关的对象
在roscpp中,话题和服务相关对象一般由 NodeHandle 创建。
1. advertise函数
作用:创建发布者对象
ros::Publisher pub = nh.advertise<std_msgs::String>("house",10);
模板:被发布消息的对象 <std_msgs::String>
参数:
1.参数名称及作用
2.话题名称:house,string类型
3.队列长度:缓冲区最多可容纳数据
4.latch(可选):如果设置为true,会保存发送方的最后一条信息,并且新的连接对象链接到发布方时,发布方会将这条信息发送给订阅者,默认值为false即不会保存
5.应用场景:即latch设置为true的作用
场景机器人导航时,需要一个静态地图,地图通过发布方来发布,如何设置发布频率,如果是一秒钟10HZ,地图是静态长期不会变,一个解决方案是只发一次地图信息,但将发布对象的latch对象值设置为true,这样意味着当订阅者链接到话题时候,就会将地图发送给订阅方。
6.code:对比两种不同方式
原来的代码(没有设置latch)
#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>
int main(int argc ,char * argvs[])
{
setlocale(LC_ALL,"");
ros::init(argc,argvs,"erguizi",ros::init_options::AnonymousName);
ros::NodeHandle nh;
ros::Publisher pub = nh.advertise<std_msgs::String>("house",10);
std_msgs::String msg;
ros::Rate rate(10);
int count = 0;
ros::Duration(3).sleep();
while(ros::ok())
{
count ++;
std::stringstream ss;
ss << "hello --->"<<count;
msg.data = ss.str();
if(count<=10)
{
pub.publish(msg);
ROS_INFO("发布的数据是:%s",ss.str().c_str());
}
rate.sleep();
}
return 0;
}
当发布完10条消息后,此时订阅方接受不到任何消息。
当将latch设置成true时:
#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>
int main(int argc ,char * argvs[])
{
setlocale(LC_ALL,"");
ros::init(argc,argvs,"erguizi",ros::init_options::AnonymousName);
ros::NodeHandle nh;
ros::Publisher pub = nh.advertise<std_msgs::String>("house",10);
std_msgs::String msg;
ros::Rate rate(10);
int count = 0;
ros::Duration(3).sleep();
while(ros::ok())
{
count ++;
std::stringstream ss;
ss << "hello --->"<<count;
msg.data = ss.str();
if(count<=10)
{
pub.publish(msg);
ROS_INFO("发布的数据是:%s",ss.str().c_str());
}
rate.sleep();
}
return 0;
}
发现订阅方可以订阅到最后一条数据
返回值: ros::Publisher型数据,用来创建发布方
3.回旋函数
1. 在ROS程序中,频繁的使用了 ros::spin() 和 ros::spinOnce() 两个回旋函数,可以用于处理回调函数
2.spin与spinonce区别
相同点:二者都用于处理回调函数;
不同点:ros::spin() 是进入了循环执行回调函数,而 ros::spinOnce() 只会执行一次回调函数(没有循环),在 ros::spin() 后的语句不会执行到,而 ros::spinOnce() 后的语句可以执行。
4.时间
ROS中时间相关的API是极其常用,比如:获取当前时刻、持续时间的设置、执行频率、休眠、定时器...都与时间相关。
1.时刻
作用:获取时刻,或是设置指定时刻
1.实现--获取时刻:
1.时间相关的头文件已经在 ros.h 中包含,无须包含其他头文件
2.API:ros::Time::now() ; 返回值类型为 ros::Time 类型,值为距离1970/01/01的时间。
ros::Time rightnow = ros::Time::now();
#include "ros/ros.h"
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
ros::init(argc,argv,"helllo_time");
ros::NodeHandle nh;
//now函数会将当前时刻封装并返回,当前时刻:now被调用执行的时刻
ros::Time rightnow = ros::Time::now();
//将离参考系的时间转换成秒
ROS_INFO("当前时刻%.2f",rightnow.toSec());
ROS_INFO("当前时刻%d",rightnow.sec);
return 0;
}
运行结果:
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ rosrun plumbing_apis time
[ INFO] [1736649022.118307474]: 当前时刻1736649022.12
[ INFO] [1736649022.118619666]: 当前时刻1736649022
2.实现--设置指定时刻:
//设置时间为距离1970/01/01 过去20秒钟 + 2426655纳秒
ros::Time t1(20,2426655);
ROS_INFO("当前时刻%.2f",t1.toSec());
运行结果:
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ rosrun plumbing_apis time01
[ INFO] [1736649652.954863383]: 当前时刻20.00
2.持续时间(程序休眠)
1.需求:程序中停顿5秒
2.步骤:
①创建持续时间对象
②休眠操作
3.code
ros::duration du(4.5);//持续时间封装了4.5秒
du.sleep();
运行结果:
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ rosrun plumbing_apis time02
[ INFO] [1736650092.413371697]: 当前时刻1736650092.41
[ INFO] [1736650092.413655626]: 当前时刻1736650092
//这之间间隔了4.5秒
[ INFO] [1736650096.913956111]: 当前时刻20.00
3.时间运算
1.需求:已知程序开始运行的时刻和程序运行的时间,求程序运行结束的时刻
2.步骤:
①获取开始执行的时刻
②调用duration模拟运行时间
③计算结束时刻
3.code1
ros::Time begin = ros::Time::now();
ros::Duration du1(5);
ros::Time stop = begin + du1;
ROS_INFO("开始的时刻:%.2f",begin.toSec());
ROS_INFO("结束的时刻:%.2f",stop.toSec());
运行结果:
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ rosrun plumbing_apis time03
[ INFO] [1736650770.871465894]: 开始的时刻:1736650770.87
[ INFO] [1736650770.872289194]: 结束的时刻:1736650775.87
4.code2
ros::Duration du3 = begin - stop ;
ROS_INFO("时刻相减:%.2f",du3.toSec());
运行结果:
[ INFO] [1736650885.845939871]: 时刻相减:-5.00
除此之外,持续时间也可以相加。
4.设置运行频率
ros::Rate rate(1); rate.sleep() 实现程序休眠。
5. 定时器
1.需求:ROS 中内置了专门的定时器,可以实现与 ros::Rate 类似的效果
2.code:输出结果,每隔一秒输入一行--------
#include "ros/ros.h"
void cb(const ros::TimerEvent & event)
{
ROS_INFO("----------------------");
}
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
ros::init(argc,argv,"helllo_time");
ros::NodeHandle nh;
//需求:每隔一秒钟 后台输出一段文本
ROS_INFO("-----------------------------定时器-----------------------------------");
//时间间隔 回调函数 false(定时器是否一次性,如果为true,一秒钟执行一次回调函数,只执行一次,false每隔一秒执行一次)
//true 定时器是否自动启动
ros::Timer timer = nh.createTimer(ros::Duration(1),cb);
ros::spin();
}
运行结果:
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ rosrun plumbing_apis time04
[ INFO] [1736656008.311804341]: -----------------------------定时器-----------------------------------
[ INFO] [1736656009.313603910]: ----------------------
[ INFO] [1736656010.313497906]: ----------------------
[ INFO] [1736656011.313486552]: ----------------------
[ INFO] [1736656012.313506048]: ----------------------
[ INFO] [1736656013.313499337]: ----------------------
[ INFO] [1736656014.313471610]: ----------------------
[ INFO] [1736656015.313490538]: ----------------------
3.定时器的其他参数
bool oneshot = false //定时器是否一次性,默认为false。
bool autostart = true //定时器是否自动启动,默认为true。
当定时器设置为一次性时:
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ rosrun plumbing_apis time04
[ INFO] [1736656500.289795698]: -----------------------------定时器-----------------------------------
[ INFO] [1736656501.290676285]: ----------------------
可以看出此时只会输出一次结果。
当定时器设置为不自动启动时:
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ rosrun plumbing_apis time04
[ INFO] [1736656588.918929581]: -----------------------------定时器-----------------------------------
可以看出此时不会输出回调函数的内容,那么该如何启动呢?需要调用timer.start()手动启动。
当加入手动启动时,可以看出输出结果为:
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ rosrun plumbing_apis time04
[ INFO] [1736656755.802738358]: -----------------------------定时器-----------------------------------
[ INFO] [1736656756.803763459]: ----------------------
[ INFO] [1736656757.803679492]: ----------------------
[ INFO] [1736656758.803596011]: ----------------------
[ INFO] [1736656759.803530504]: ----------------------
[ INFO] [1736656760.803727982]: ----------------------
[ INFO] [1736656761.803641094]: ----------------------
[ INFO] [1736656762.803615987]: ----------------------
[ INFO] [1736656763.803638777]: ----------------------
5.其他函数
1.节点退出的情况
1.节点接收到了关闭信息,比如常用的 ctrl + c 快捷键就是关闭节点的信号;
2.同名节点启动,导致现有节点退出;(可以在节点初始化时动手脚,加上ros::init_options::AnonymousName防止同名节点启动导致的掉线)
3.程序中的其他部分调用了节点关闭相关的API(C++中是ros::shutdown(),python中是rospy.signal_shutdown()。
2.ROS日志的五个级别
1.DEBUG(调试):只在调试时使用,此类消息不会输出到控制台;
2.INFO(信息):标准消息,一般用于说明系统内正在执行的操作;
3.WARN(警告):提醒一些异常情况,但程序仍然可以执行;
4.ERROR(错误):提示错误信息,此类错误会影响程序运行;
5.FATAL(严重错误):此类错误将阻止节点继续运行;
代码:
#include "ros/ros.h"
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
ros::init(argc,argv,"helllo_time");
ros::NodeHandle nh;
ROS_DEBUG("hello,DEBUG"); //不会输出
ROS_INFO("hello,INFO"); //默认白色字体
ROS_WARN("Hello,WARN"); //默认黄色字体
ROS_ERROR("hello,ERROR");//默认红色字体
ROS_FATAL("hello,FATAL");//默认红色字体
return 0;
}
运行结果:
lvl@lvl-Legion-Y7000P-IRX9:~/demo04_ws$ rosrun plumbing_apis time05
[ INFO] [1736657581.494153186]: hello,INFO
[ WARN] [1736657581.494632811]: Hello,WARN
[ERROR] [1736657581.494640603]: hello,ERROR
[FATAL] [1736657581.494644701]: hello,FATAL
二、ROS中的头文件和源文件
本节主要介绍ROS的C++实现中,如何使用头文件和源文件方式封装代码,具体内容如下:
1.设置头文件,可执行文件作为源文件。
2.分别设置头文件,源文件和可执行文件。
在ROS中关于头文件的使用,核心内容在于CMakeList.txt文件的篇日志,不同的封装方式,配置上也有差异。
1.自定义头文件的调用
1.需求及流程
1.需求:设计头文件,可执行文件本身作为源文件。
2.流程:编写头文件;编写可执行文件;编辑配置文件并执行。
2.具体实现
1.创建功能包
plumbing_head ,依赖于roscpp rospy std_msgs
2.创建头文件
在功能包下的include 下有个和功能包重名的目录,建立头文件hello.h
3.声明头文件保护及头文件实现
#ifndef _HELLO_H
#define _HELLO_H
/*
声明namespace ,namespace下面声明一个类,类下声明一个函数run
*/
namespace hello_ns
{
class Myhello
{
public:
void run();
};
}
#endif
4.可执行文件实现
1.配置includePath路径,在c_cpp_properties.json文件中配置头文件的路径
/home/liuhongwei/demo03_ws/src/plumbing_head/include
加入includePath中,加入结果如下:
操作在之前就已经实现过,如果忘记请即使回顾:
"includePath": [
"/opt/ros/noetic/include/**",
"/home/lvl/catkin_ws/src/hello/include/**",
"/usr/include/**",
"/home/lvl/demo05_ws/src/plumbing_head/include/plumbing_head"
],
2.源文件的实现
#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 mehello;
mehello.run();
return 0;
}
3.配置 CMakeList.txt
新增:
## Specify additional locations of header files
## Your package locations should be listed before other locations
include_directories(
include
${catkin_INCLUDE_DIRS}
)
还要添加可执行权限和依赖的库。
4.编译执行
2.自定义源文件的调用
1.需求及流程
1.需求:设计头文件和源文件,在可执行文件中包含头文件
2.流程:编写头文件;编写源文件;编写可执行文件;配置配置文件并执行
2.具体实现
1.创建功能包 plumbing_head_src,依赖为roscpp rospy std_msgs
2.创建头文件 hello.h
#ifndef _HELLO_H
#define _HELLO_H
namespace hello_ns
{
class Myhello
{
public:
void run();
};
}
#endif
3.创建源文件 hello.cpp
#include "plumbing_head_src/hello.h"
#include "ros/ros.h"
namespace hello_ns
{
void Myhello::run()
{
ROS_INFO("源文件中的run函数................");
}
}
4.修改c_cpp_properties.json下面的includPath文件(同上)
5.创建可执行文件 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_src");
hello_ns::Myhello my;
my.run();
return 0;
}
6.配置Cmakelist&头文件的配置
修改 include_directories
## Specify additional locations of header files
## Your package locations should be listed before other locations
include_directories(
include
${catkin_INCLUDE_DIRS}
)
配置 C++库 头文件;源文件位置
## Declare a C++ library
add_library(head_src
include/plumbing_head_src/hello.h
src/hello.cpp
)
add_dependencies(head_src ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
target_link_libraries(head_src
${catkin_LIBRARIES}
)
7.配置Cmakelist&可执行文件的配置
add_executable(use_hello src/use_hello.cpp)
add_dependencies(use_hello ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
target_link_libraries(use_hello
head_src
${catkin_LIBRARIES}
)
编译执行
运行结果:
lvl@lvl-Legion-Y7000P-IRX9:~/demo05_ws$ source devel/setup.bash
lvl@lvl-Legion-Y7000P-IRX9:~/demo05_ws$ rosrun plumbing_head_src hehe
[ INFO] [1736663553.497744788]: 源文件中的run函数................
结束语
以上就是我学习到的内容,如果对您有帮助请多多支持我,如果哪里有问题欢迎大家在评论区积极讨论,我看到会及时回复。