目录
简介
ROS诞生背景
机器人是一种高度复杂的系统性实现,机器人设计包含了机械加工、机械结构设计、硬件设计、嵌入式软件设计、上层软件设计....是各种硬件与软件集成,甚至可以说机器人系统是当今工业体系的集大成者。
机器人体系是相当庞大的,其复杂度之高,以至于没有任何个人、组织甚至公司能够独立完成系统性的机器人研发工作。
一种更合适的策略是:让机器人研发者专注于自己擅长的领域,其他模块则直接复用相关领域更专业研发团队的实现,当然自身的研究也可以被他人继续复用。这种基于"复用"的分工协作,遵循了不重复发明轮子的原则,显然是可以大大提高机器人的研发效率的,尤其是随着机器人硬件越来越丰富,软件库越来越庞大,这种复用性和模块化开发需求也愈发强烈。
ROS的设计目标
- 代码复用:ROS的目标不是成为具有最多功能的框架,ROS的主要目标是支持机器人技术研发中的代码重用。
- 分布式:ROS是进程(也称为Nodes)的分布式框架,ROS中的进程可分布于不同主机,不同主机协同工作,从而分散计算压力
- 松耦合:ROS中功能模块封装于独立的功能包或元功能包,便于分享,功能包内的模块以节点为单位运行,以ROS标准的IO作为接口,开发者不需要关注模块内部实现,只要了解接口规则就能实现复用,实现了模块间点对点的松耦合连接
- 精简:ROS被设计为尽可能精简,以便为ROS编写的代码可以与其他机器人软件框架一起使用。ROS易于与其他机器人软件框架集成:ROS已与OpenRAVE,Orocos和Player集成。
- 语言独立性:包括Java,C++,Python等。为了支持更多应用开发和移植,ROS设计为一种语言弱相关的框架结构,使用简洁,中立的定义语言描述模块间的消息接口,在编译中再产生所使用语言的目标文件,为消息交互提供支持,同时允许消息接口的嵌套使用
- 易于测试:ROS具有称为rostest的内置单元/集成测试框架,可轻松安装和拆卸测试工具。
- 大型应用:ROS适用于大型运行时系统和大型开发流程。
- 丰富的组件化工具包:ROS可采用组件化方式集成一些工具和软件到系统中并作为一个组件直接使用,如RVIZ(3D可视化工具),开发者根据ROS定义的接口在其中显示机器人模型等,组件还包括仿真环境和消息查看工具等
- 免费且开源:开发者众多,功能包多
ROS与ROS2
ROS 目前已经发布了ROS1 的终极版本: noetic,并建议后期过渡至 ROS2 版本。noetic 版本之前默认使用的是 Python2,noetic 支持 Python3。
ROS本身存在很多问题,比如:
- 强依赖master节点,一旦master节点挂掉,通信就可能出现问题。
- 通信基于TCP实现,实时性差、系统开销大
- 消息机制不兼容,想要使用protobuf就需要改其中的源码
- 没有加密机制、安全性不高
而ROS2做了很多的改进:
- 从原来的只支持linux平台变成了支持windows、mac甚至是嵌入式RTOS平台
- 去中心化master,ROS和ROS2中间件不同之处在于,ROS2取消了master节点。去中心化后,各个节点之间可以通过DDS的节点相互发现,各个节点都是平等的,且可以1对1、1对n、n对n进行互相通信。
- 通信直接更换为DDS进行实现,这里的DDS不是具体的哪一个模块,而是一种协议。
- 编译系统的改进(catkin到ament)
- 软件包更新到c++11
- 可用Python编写的Launch文件
- 多机器人协同通信支持
- 支持安全加密通信
- 同一个进程支持多个节点
- 支持Qos服务质量
- 支持节点生命周期管理
安装ROS
Ubuntu 安装完毕后,就可以安装 ROS 操作系统了,大致步骤如下:
- 配置ubuntu的软件和更新;
- 设置安装源;
- 设置key;
- 安装;
- 配置环境变量。
1.配置ubuntu的软件和更新
配置ubuntu的软件和更新,允许安装不经认证的软件。
首先打开“软件和更新”对话框,具体可以在 Ubuntu 搜索按钮中搜索。
打开后按照下图进行配置(确保勾选了"restricted", "universe," 和 "multiverse.")
2.设置安装源
官方默认安装源:
sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list'
或来自国内清华的安装源
sudo sh -c '. /etc/lsb-release && echo "deb http://mirrors.tuna.tsinghua.edu.cn/ros/ubuntu/ `lsb_release -cs` main" > /etc/apt/sources.list.d/ros-latest.list'
或来自国内中科大的安装源
sudo sh -c '. /etc/lsb-release && echo "deb http://mirrors.ustc.edu.cn/ros/ubuntu/ `lsb_release -cs` main" > /etc/apt/sources.list.d/ros-latest.list'
PS:
- 回车后,可能需要输入管理员密码
- 建议使用国内资源,安装速度更快。
3.设置key
sudo apt-key adv --keyserver 'hkp://keyserver.ubuntu.com:80' --recv-key C1CF6E31E6BADE8868B172B4F42ED6FBAB17C654
4.安装
首先需要更新 apt(以前是 apt-get, 官方建议使用 apt 而非 apt-get),apt 是用于从互联网仓库搜索、安装、升级、卸载软件或操作系统的工具。
sudo apt update
等待...
然后,再安装所需类型的 ROS:ROS 多个类型:Desktop-Full、Desktop、ROS-Base。这里介绍较为常用的Desktop-Full(官方推荐)安装: ROS, rqt, rviz, robot-generic libraries, 2D/3D simulators, navigation and 2D/3D perception
sudo apt install ros-noetic-desktop-full
等待......(比较耗时)
友情提示: 由于网络原因,导致连接超时,可能会安装失败,如下所示:
可以多次重复调用 更新 和 安装命令,直至成功。
5.配置环境变量
配置环境变量,方便在任意 终端中使用 ROS。
echo "source /opt/ros/noetic/setup.bash" >> ~/.bashrc source ~/.bashrc
安装可能出现的问题
安装构建依赖
在 noetic 最初发布时,和其他历史版本稍有差异的是:没有安装构建依赖这一步骤。随着 noetic 不断完善,官方补齐了这一操作。
首先安装构建依赖的相关工具
sudo apt install python3-rosdep python3-rosinstall python3-rosinstall-generator python3-wstool build-essential
ROS中使用许多工具前,要求需要初始化rosdep(可以安装系统依赖) -- 上一步实现已经安装过了。
sudo apt install python3-rosdep
初始化rosdep
sudo rosdep init rosdep update
如果一切顺利的话,rosdep 初始化与更新的打印结果如下:
但是,在 rosdep 初始化时,多半会抛出异常。
问题:
原因:
境外资源被屏蔽。
解决:
百度或google搜索,解决方式有多种(https://github.com/ros/rosdistro/issues/9721),可惜在 ubuntu20.04 下,集体失效。
新思路:将相关资源备份到 gitee,修改 rosdep 源码,重新定位资源。
实现:
1.先打开资源备份路径:赵虚左/rosdistro,打开 rosdistro/rosdep/sources.list.d/20-default.list文件留作备用(主要是复用URL的部分内容:gitee.com/zhao-xuzuo/rosdistro/raw/master)。
2.进入"/usr/lib/python3/dist-packages/" 查找rosdep中和raw.githubusercontent.com相关的内容,调用命令:
find . -type f | xargs grep "raw.githubusercontent"
3.修改相关文件,主要有: ./rosdistro/__init__.py、./rosdep2/gbpdistro_support.py、./rosdep2/sources_list.py 、./rosdep2/rep3.py。可以使用sudo gedit命令修改文件:
文件中涉及的 URL 内容,如果是:raw.githubusercontent.com/ros/rosdistro/master都替换成步骤1中准备的gitee.com/zhao-xuzuo/rosdistro/raw/master即可。
修改完毕,再重新执行命令:
sudo rosdep init rosdep update
就可以正常实现 rosdep 的初始化与更新了。
卸载
如果需要卸载ROS可以调用如下命令:
sudo apt remove ros-noetic-*
注意: 在 ROS 版本 noetic 中无需构建软件包的依赖关系,没有rosdep的相关安装与配置。
另请参考:noetic/Installation/Ubuntu - ROS Wiki。
ROS架构
一般我们可以从设计者、维护者、系统结构与自身结构4个角度来描述ROS结构:
1.设计者
ROS设计者将ROS表述为“ROS = Plumbing + Tools + Capabilities + Ecosystem”
- Plumbing: 通讯机制(实现ROS不同节点之间的交互)
- Tools :工具软件包(ROS中的开发和调试工具)
- Capabilities :机器人高层技能(ROS中某些功能的集合,比如:导航)
- Ecosystem:机器人生态系统(跨地域、跨软件与硬件的ROS联盟)
2.维护者
立足维护者的角度: ROS 架构可划分为两大部分
- main:核心部分,主要由Willow Garage 和一些开发者设计、提供以及维护。它提供了一些分布式计算的基本工具,以及整个ROS的核心部分的程序编写。
- universe:全球范围的代码,有不同国家的ROS社区组织开发和维护。一种是库的代码,如OpenCV、PCL等;库的上一层是从功能角度提供的代码,如人脸识别,他们调用下层的库;最上层的代码是应用级的代码,让机器人完成某一确定的功能。
3. 立足系统架构: ROS 可以划分为三层
- OS 层,也即经典意义的操作系统ROS 只是元操作系统,需要依托真正意义的操作系统,目前兼容性最好的是 Linux 的 Ubuntu,Mac、Windows 也支持 ROS 的较新版本
- 中间层是 ROS 封装的关于机器人开发的中间件,比如:
-
- 基于 TCP/UDP 继续封装的 TCPROS/UDPROS 通信系统
- 用于进程间通信 Nodelet,为数据的实时性传输提供支持
- 另外,还提供了大量的机器人开发实现库,如:数据类型定义、坐标变换、运动控制....
- 应用层功能包,以及功能包内的节点,比如: master、turtlesim的控制与运动节点...
ROS通信机制
话题通信
话题通信是ROS中使用频率最高的一种通信模式,话题通信是基于发布订阅模式的,也即:一个节点发布消息,另一个节点订阅该消息。话题通信的应用场景也极其广泛。
用于不断更新的、少逻辑处理的数据传输场景。
理论模型
话题通信实现模型是比较复杂的,该模型如下图所示,该模型中涉及到三个角色:
- ROS Master (管理者)
- Talker (发布者)
- Listener (订阅者)
ROS Master 负责保管 Talker 和 Listener 注册的信息,并匹配话题相同的 Talker 与 Listener,帮助 Talker 与 Listener 建立连接,连接建立后,Talker 可以发布消息,且发布的消息会被 Listener 订阅。
流程
0.Talker注册
Talker启动后,会通过RPC在 ROS Master 中注册自身信息,其中包含所发布消息的话题名称。ROS Master 会将节点的注册信息加入到注册表中。
1.Listener注册
Listener启动后,也会通过RPC在 ROS Master 中注册自身信息,包含需要订阅消息的话题名。ROS Master 会将节点的注册信息加入到注册表中。
2.ROS Master实现信息匹配
ROS Master 会根据注册表中的信息匹配Talker 和 Listener,并通过 RPC 向 Listener 发送 Talker 的 RPC 地址信息。
3.Listener向Talker发送请求
Listener 根据接收到的 RPC 地址,通过 RPC 向 Talker 发送连接请求,传输订阅的话题名称、消息类型以及通信协议(TCP/UDP)。
4.Talker确认请求
Talker 接收到 Listener 的请求后,也是通过 RPC 向 Listener 确认连接信息,并发送自身的 TCP 地址信息。
5.Listener与Talker件里连接
Listener 根据步骤4 返回的消息使用 TCP 与 Talker 建立网络连接。
6.Talker向Listener发送消息
连接建立后,Talker 开始向 Listener 发布消息。
注意1:上述实现流程中,前五步使用的 RPC协议,最后两步使用的是 TCP 协议
注意2: Talker 与 Listener 的启动无先后顺序要求
注意3: Talker 与 Listener 都可以有多个
注意4: Talker 与 Listener 连接建立后,不再需要 ROS Master。也即,即便关闭ROS Master,Talker 与 Listern 照常通信。
通信样例
发布方
/* 需求: 实现基本的话题通信,一方发布数据,一方接收数据, 实现的关键点: 1.发送方 2.接收方 3.数据(此处为普通文本) PS: 二者需要设置相同的话题 消息发布方: 循环发布信息:HelloWorld 后缀数字编号 实现流程: 1.包含头文件 2.初始化 ROS 节点:命名(唯一) 3.实例化 ROS 句柄 4.实例化 发布者 对象 5.组织被发布的数据,并编写逻辑发布数据 */ // 1.包含头文件 #include "ros/ros.h" #include "std_msgs/String.h" //普通文本类型的消息 #include <sstream> int main(int argc, char *argv[]) { //设置编码 setlocale(LC_ALL,""); //2.初始化 ROS 节点:命名(唯一) // 参数1和参数2 后期为节点传值会使用 // 参数3 是节点名称,是一个标识符,需要保证运行后,在 ROS 网络拓扑中唯一 ros::init(argc,argv,"talker"); //3.实例化 ROS 句柄 ros::NodeHandle nh;//该类封装了 ROS 中的一些常用功能 //4.实例化 发布者 对象 //泛型: 发布的消息类型 //参数1: 要发布到的话题 //参数2: 队列中最大保存的消息数,超出此阀值时,先进的先销毁(时间早的先销毁) ros::Publisher pub = nh.advertise<std_msgs::String>("chatter",10); //5.组织被发布的数据,并编写逻辑发布数据 //数据(动态组织) std_msgs::String msg; // msg.data = "你好啊!!!"; std::string msg_front = "Hello 你好!"; //消息前缀 int count = 0; //消息计数器 //逻辑(一秒10次) ros::Rate r(1); //节点不死 while (ros::ok()) { //使用 stringstream 拼接字符串与编号 std::stringstream ss; ss << msg_front << count; msg.data = ss.str(); //发布消息 pub.publish(msg); //加入调试,打印发送的消息 ROS_INFO("发送的消息:%s",msg.data.c_str()); //根据前面制定的发送贫频率自动休眠 休眠时间 = 1/频率; r.sleep(); count++;//循环结束前,让 count 自增 //暂无应用 ros::spinOnce(); } return 0; }
订阅方
/* 需求: 实现基本的话题通信,一方发布数据,一方接收数据, 实现的关键点: 1.发送方 2.接收方 3.数据(此处为普通文本) 消息订阅方: 订阅话题并打印接收到的消息 实现流程: 1.包含头文件 2.初始化 ROS 节点:命名(唯一) 3.实例化 ROS 句柄 4.实例化 订阅者 对象 5.处理订阅的消息(回调函数) 6.设置循环调用回调函数 */ // 1.包含头文件 #include "ros/ros.h" #include "std_msgs/String.h" void doMsg(const std_msgs::String::ConstPtr& msg_p){ ROS_INFO("我听见:%s",msg_p->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,"listener"); //3.实例化 ROS 句柄 ros::NodeHandle nh; //4.实例化 订阅者 对象 ros::Subscriber sub = nh.subscribe<std_msgs::String>("chatter",10,doMsg); //5.处理订阅的消息(回调函数) // 6.设置循环调用回调函数 ros::spin();//循环读取接收的数据,并调用回调函数处理 return 0; }
CmakeList
add_executable(Hello_pub src/Hello_pub.cpp ) add_executable(Hello_sub src/Hello_sub.cpp ) target_link_libraries(Hello_pub ${catkin_LIBRARIES} ) target_link_libraries(Hello_sub ${catkin_LIBRARIES} )
执行过程
1.启动 roscore;
2.启动发布节点;
3.启动订阅节点。
自定义消息的通信
简介
在 ROS 通信协议中,数据载体是一个较为重要组成部分,ROS 中通过 std_msgs 封装了一些原生的数据类型,比如:String、Int32、Int64、Char、Bool、Empty.... 但是,这些数据一般只包含一个 data 字段,结构的单一意味着功能上的局限性,当传输一些复杂的数据,比如: 激光雷达的信息... std_msgs 由于描述性较差而显得力不从心,这种场景下可以使用自定义的消息类型
msgs只是简单的文本文件,每行具有字段类型和字段名称,可以使用的字段类型有:
- int8, int16, int32, int64 (或者无符号类型: uint*)
- float32, float64
- string
- time, duration
- other msg files
- variable-length array[] and fixed-length array[C]
ROS中还有一种特殊类型:Header,标头包含时间戳和ROS中常用的坐标帧信息。会经常看到msg文件的第一行具有Header标头。
1.定义msg文件
string name uint16 age float64 height
2.编辑配置文件
<build_depend>message_generation</build_depend> <exec_depend>message_runtime</exec_depend> <!-- exce_depend 以前对应的是 run_depend 现在非法 -->
find_package(catkin REQUIRED COMPONENTS roscpp rospy std_msgs message_generation ) # 需要加入 message_generation,必须有 std_msgs ## 配置 msg 源文件 add_message_files( FILES Person.msg ) # 生成消息时依赖于 std_msgs generate_messages( DEPENDENCIES std_msgs ) #执行时依赖 catkin_package( # INCLUDE_DIRS include # LIBRARIES demo02_talker_listener CATKIN_DEPENDS roscpp rospy std_msgs message_runtime # DEPENDS system_lib )
发布方
/* 需求: 循环发布人的信息 */ #include "ros/ros.h" #include "demo02_talker_listener/Person.h" int main(int argc, char *argv[]) { setlocale(LC_ALL,""); //1.初始化 ROS 节点 ros::init(argc,argv,"talker_person"); //2.创建 ROS 句柄 ros::NodeHandle nh; //3.创建发布者对象 ros::Publisher pub = nh.advertise<demo02_talker_listener::Person>("chatter_person",1000); //4.组织被发布的消息,编写发布逻辑并发布消息 demo02_talker_listener::Person p; p.name = "sunwukong"; p.age = 2000; p.height = 1.45; ros::Rate r(1); while (ros::ok()) { pub.publish(p); p.age += 1; ROS_INFO("我叫:%s,今年%d岁,高%.2f米", p.name.c_str(), p.age, p.height); r.sleep(); ros::spinOnce(); } return 0; }
订阅方
/* 需求: 订阅人的信息 */ #include "ros/ros.h" #include "demo02_talker_listener/Person.h" void doPerson(const demo02_talker_listener::Person::ConstPtr& person_p){ ROS_INFO("订阅的人信息:%s, %d, %.2f", person_p->name.c_str(), person_p->age, person_p->height); } int main(int argc, char *argv[]) { setlocale(LC_ALL,""); //1.初始化 ROS 节点 ros::init(argc,argv,"listener_person"); //2.创建 ROS 句柄 ros::NodeHandle nh; //3.创建订阅对象 ros::Subscriber sub = nh.subscribe<demo02_talker_listener::Person>("chatter_person",10,doPerson); //4.回调函数中处理 person //5.ros::spin(); ros::spin(); return 0; }
Cmakelist
add_executable(person_talker src/person_talker.cpp) add_executable(person_listener src/person_listener.cpp) add_dependencies(person_talker ${PROJECT_NAME}_generate_messages_cpp) add_dependencies(person_listener ${PROJECT_NAME}_generate_messages_cpp) target_link_libraries(person_talker ${catkin_LIBRARIES} ) target_link_libraries(person_listener ${catkin_LIBRARIES} )
Vscode配置
为了方便代码提示以及避免误抛异常,需要先配置 vscode,将前面生成的 head 文件路径配置进 c_cpp_properties.json 的 includepath属性:
{ "configurations": [ { "browse": { "databaseFilename": "", "limitSymbolsToIncludedHeaders": true }, "includePath": [ "/opt/ros/noetic/include/**", "/usr/include/**", "/xxx/yyy工作空间/devel/include/**" //配置 head 文件的路径 ], "name": "ROS", "intelliSenseMode": "gcc-x64", "compilerPath": "/usr/bin/gcc", "cStandard": "c11", "cppStandard": "c++17" } ], "version": 4 }
服务通信
服务通信也是ROS中一种极其常用的通信模式,服务通信是基于请求响应模式的,是一种应答机制。也即: 一个节点A向另一个节点B发送请求,B接收处理请求并产生响应结果返回给A。比如如下场景:
机器人巡逻过程中,控制系统分析传感器数据发现可疑物体或人... 此时需要拍摄照片并留存。
在上述场景中,就使用到了服务通信。
- 一个节点需要向相机节点发送拍照请求,相机节点处理请求,并返回处理结果
与上述应用类似的,服务通信更适用于对时时性有要求、具有一定逻辑处理的应用场景。
理论模型
服务通信较之于话题通信更简单些,理论模型如下图所示,该模型中涉及到三个角色:
- ROS master(管理者)
- Server(服务端)
- Client(客户端)
ROS Master 负责保管 Server 和 Client 注册的信息,并匹配话题相同的 Server 与 Client ,帮助 Server 与 Client 建立连接,连接建立后,Client 发送请求信息,Server 返回响应信息。
整个流程由以下步骤实现:
0.Server注册
Server 启动后,会通过RPC在 ROS Master 中注册自身信息,其中包含提供的服务的名称。ROS Master 会将节点的注册信息加入到注册表中。
1.Client注册
Client 启动后,也会通过RPC在 ROS Master 中注册自身信息,包含需要请求的服务的名称。ROS Master 会将节点的注册信息加入到注册表中。
2.ROS Master实现信息匹配
ROS Master 会根据注册表中的信息匹配Server和 Client,并通过 RPC 向 Client 发送 Server 的 TCP 地址信息。
3.Client发送请求
Client 根据步骤2 响应的信息,使用 TCP 与 Server 建立网络连接,并发送请求数据。
4.Server发送响应
Server 接收、解析请求的数据,并产生响应结果返回给 Client。
注意:
1.客户端请求被处理时,需要保证服务器已经启动;
2.服务端和客户端都可以存在多个。
服务通信自定义srv
流程:
srv 文件内的可用数据类型与 msg 文件一致,且定义 srv 实现流程与自定义 msg 实现流程类似:
- 按照固定格式创建srv文件
- 编辑配置文件
- 编译生成中间文件
1.定义srv文件
服务通信中,数据分成两部分,请求与响应,在 srv 文件中请求和响应使用---分割,具体实现如下:
功能包下新建 srv 目录,添加 xxx.srv 文件,内容:
# 客户端请求时发送的两个数字 int32 num1 int32 num2 --- # 服务器响应发送的数据 int32 sum
2.编辑配置文件
package.xml中添加编译依赖与执行依赖
<build_depend>message_generation</build_depend> <exec_depend>message_runtime</exec_depend> <!-- exce_depend 以前对应的是 run_depend 现在非法 -->
CMakeLists.txt编辑 srv 相关配置
find_package(catkin REQUIRED COMPONENTS roscpp rospy std_msgs message_generation ) # 需要加入 message_generation,必须有 std_msgs add_service_files( FILES AddInts.srv ) generate_messages( DEPENDENCIES std_msgs )
3.编译
编译后的中间文件查看:
C++ 需要调用的中间文件(.../工作空间/devel/include/包名/xxx.h)
Python 需要调用的中间文件(.../工作空间/devel/lib/python3/dist-packages/包名/srv)
后续调用相关 srv 时,是从这些中间文件调用的
4. 服务端
/* 需求: 编写两个节点实现服务通信,客户端节点需要提交两个整数到服务器 服务器需要解析客户端提交的数据,相加后,将结果响应回客户端, 客户端再解析 服务器实现: 1.包含头文件 2.初始化 ROS 节点 3.创建 ROS 句柄 4.创建 服务 对象 5.回调函数处理请求并产生响应 6.由于请求有多个,需要调用 ros::spin() */ #include "ros/ros.h" #include "demo03_server_client/AddInts.h" // bool 返回值由于标志是否处理成功 bool doReq(demo03_server_client::AddInts::Request& req, demo03_server_client::AddInts::Response& resp){ int num1 = req.num1; int num2 = req.num2; ROS_INFO("服务器接收到的请求数据为:num1 = %d, num2 = %d",num1, num2); //逻辑处理 if (num1 < 0 || num2 < 0) { ROS_ERROR("提交的数据异常:数据不可以为负数"); return false; } //如果没有异常,那么相加并将结果赋值给 resp resp.sum = num1 + num2; return true; } int main(int argc, char *argv[]) { setlocale(LC_ALL,""); // 2.初始化 ROS 节点 ros::init(argc,argv,"AddInts_Server"); // 3.创建 ROS 句柄 ros::NodeHandle nh; // 4.创建 服务 对象 ros::ServiceServer server = nh.advertiseService("AddInts",doReq); ROS_INFO("服务已经启动...."); // 5.回调函数处理请求并产生响应 // 6.由于请求有多个,需要调用 ros::spin() ros::spin(); return 0; }
5. 客户端
/* 需求: 编写两个节点实现服务通信,客户端节点需要提交两个整数到服务器 服务器需要解析客户端提交的数据,相加后,将结果响应回客户端, 客户端再解析 服务器实现: 1.包含头文件 2.初始化 ROS 节点 3.创建 ROS 句柄 4.创建 客户端 对象 5.请求服务,接收响应 */ // 1.包含头文件 #include "ros/ros.h" #include "demo03_server_client/AddInts.h" int main(int argc, char *argv[]) { setlocale(LC_ALL,""); // 调用时动态传值,如果通过 launch 的 args 传参,需要传递的参数个数 +3 if (argc != 3) // if (argc != 5)//launch 传参(0-文件路径 1传入的参数 2传入的参数 3节点名称 4日志路径) { ROS_ERROR("请提交两个整数"); return 1; } // 2.初始化 ROS 节点 ros::init(argc,argv,"AddInts_Client"); // 3.创建 ROS 句柄 ros::NodeHandle nh; // 4.创建 客户端 对象 ros::ServiceClient client = nh.serviceClient<demo03_server_client::AddInts>("AddInts"); //等待服务启动成功 //方式1 ros::service::waitForService("AddInts"); //方式2 // client.waitForExistence(); // 5.组织请求数据 demo03_server_client::AddInts ai; ai.request.num1 = atoi(argv[1]); ai.request.num2 = atoi(argv[2]); // 6.发送请求,返回 bool 值,标记是否成功 bool flag = client.call(ai); // 7.处理响应 if (flag) { ROS_INFO("请求正常处理,响应结果:%d",ai.response.sum); } else { ROS_ERROR("请求处理失败...."); return 1; } return 0; }
6. CmakeList
add_executable(AddInts_Server src/AddInts_Server.cpp) add_executable(AddInts_Client src/AddInts_Client.cpp) add_dependencies(AddInts_Server ${PROJECT_NAME}_gencpp) add_dependencies(AddInts_Client ${PROJECT_NAME}_gencpp) target_link_libraries(AddInts_Server ${catkin_LIBRARIES} ) target_link_libraries(AddInts_Client ${catkin_LIBRARIES} )
7. 注意(客户端等待服务端连接)
在客户端发送请求前添加:client.waitForExistence();
或:ros::service::waitForService("AddInts");
这是一个阻塞式函数,只有服务启动成功后才会继续执行
此处可以使用 launch 文件优化,但是需要注意 args 传参特点
参数服务器
简单的说就是全局变量。
参数服务器在ROS中主要用于实现不同节点之间的数据共享。参数服务器相当于是独立于所有节点的一个公共容器,可以将数据存储在该容器中,被不同的节点调用,当然不同的节点也可以往其中存储数据,关于参数服务器的典型应用场景如下:
导航实现时,会进行路径规划,比如: 全局路径规划,设计一个从出发点到目标点的大致路径。本地路径规划,会根据当前路况生成时时的行进路径
上述场景中,全局路径规划和本地路径规划时,就会使用到参数服务器:
- 路径规划时,需要参考小车的尺寸,我们可以将这些尺寸信息存储到参数服务器,全局路径规划节点与本地路径规划节点都可以从参数服务器中调用这些参数
参数服务器,一般适用于存在数据共享的一些应用场景。
理论模型
参数服务器实现是最为简单的,该模型如下图所示,该模型中涉及到三个角色:
- ROS Master (管理者)
- Talker (参数设置者)
- Listener (参数调用者)
ROS Master 作为一个公共容器保存参数,Talker 可以向容器中设置参数,Listener 可以获取参数。
整个流程由以下步骤实现:
1.Talker 设置参数
Talker 通过 RPC 向参数服务器发送参数(包括参数名与参数值),ROS Master 将参数保存到参数列表中。
2.Listener 获取参数
Listener 通过 RPC 向参数服务器发送参数查找请求,请求中包含要查找的参数名。
3.ROS Master 向 Listener 发送参数值
ROS Master 根据步骤2请求提供的参数名查找参数值,并将查询结果通过 RPC 发送给 Listener。
参数可使用数据类型:
- 32-bit integers
- booleans
- strings
- doubles
- iso8601 dates
- lists
- base64-encoded binary data
- 字典
参数操作
设置
在 C++ 中实现参数服务器数据的增删改查,可以通过两套 API 实现:
- ros::NodeHandle
- ros::param
/* 参数服务器操作之新增与修改(二者API一样)_C++实现: 在 roscpp 中提供了两套 API 实现参数操作 ros::NodeHandle setParam("键",值) ros::param set("键","值") 示例:分别设置整形、浮点、字符串、bool、列表、字典等类型参数 修改(相同的键,不同的值) */ #include "ros/ros.h" int main(int argc, char *argv[]) { ros::init(argc,argv,"set_update_param"); std::vector<std::string> stus; stus.push_back("zhangsan"); stus.push_back("李四"); stus.push_back("王五"); stus.push_back("孙大脑袋"); std::map<std::string,std::string> friends; friends["guo"] = "huang"; friends["yuang"] = "xiao"; //NodeHandle-------------------------------------------------------- ros::NodeHandle nh; nh.setParam("nh_int",10); //整型 nh.setParam("nh_double",3.14); //浮点型 nh.setParam("nh_bool",true); //bool nh.setParam("nh_string","hello NodeHandle"); //字符串 nh.setParam("nh_vector",stus); // vector nh.setParam("nh_map",friends); // map //修改演示(相同的键,不同的值) nh.setParam("nh_int",10000); //param-------------------------------------------------------- ros::param::set("param_int",20); ros::param::set("param_double",3.14); ros::param::set("param_string","Hello Param"); ros::param::set("param_bool",false); ros::param::set("param_vector",stus); ros::param::set("param_map",friends); //修改演示(相同的键,不同的值) ros::param::set("param_int",20000); return 0; }
获取
在 roscpp 中提供了两套 API 实现参数操作
ros::NodeHandle
param(键,默认值)
存在,返回对应结果,否则返回默认值
getParam(键,存储结果的变量)
存在,返回 true,且将值赋值给参数2
若果键不存在,那么返回值为 false,且不为参数2赋值
getParamCached键,存储结果的变量)--提高变量获取效率
存在,返回 true,且将值赋值给参数2
若果键不存在,那么返回值为 false,且不为参数2赋值
getParamNames(std::vector<std::string>)
获取所有的键,并存储在参数 vector 中
hasParam(键)
是否包含某个键,存在返回 true,否则返回 false
searchParam(参数1,参数2)
搜索键,参数1是被搜索的键,参数2存储搜索结果的变量
ros::param ----- 与 NodeHandle 类似
#include "ros/ros.h" int main(int argc, char *argv[]) { setlocale(LC_ALL,""); ros::init(argc,argv,"get_param"); //NodeHandle-------------------------------------------------------- /* ros::NodeHandle nh; // param 函数 int res1 = nh.param("nh_int",100); // 键存在 int res2 = nh.param("nh_int2",100); // 键不存在 ROS_INFO("param获取结果:%d,%d",res1,res2); // getParam 函数 int nh_int_value; double nh_double_value; bool nh_bool_value; std::string nh_string_value; std::vector<std::string> stus; std::map<std::string, std::string> friends; nh.getParam("nh_int",nh_int_value); nh.getParam("nh_double",nh_double_value); nh.getParam("nh_bool",nh_bool_value); nh.getParam("nh_string",nh_string_value); nh.getParam("nh_vector",stus); nh.getParam("nh_map",friends); ROS_INFO("getParam获取的结果:%d,%.2f,%s,%d", nh_int_value, nh_double_value, nh_string_value.c_str(), nh_bool_value ); for (auto &&stu : stus) { ROS_INFO("stus 元素:%s",stu.c_str()); } for (auto &&f : friends) { ROS_INFO("map 元素:%s = %s",f.first.c_str(), f.second.c_str()); } // getParamCached() nh.getParamCached("nh_int",nh_int_value); ROS_INFO("通过缓存获取数据:%d",nh_int_value); //getParamNames() std::vector<std::string> param_names1; nh.getParamNames(param_names1); for (auto &&name : param_names1) { ROS_INFO("名称解析name = %s",name.c_str()); } ROS_INFO("----------------------------"); ROS_INFO("存在 nh_int 吗? %d",nh.hasParam("nh_int")); ROS_INFO("存在 nh_intttt 吗? %d",nh.hasParam("nh_intttt")); std::string key; nh.searchParam("nh_int",key); ROS_INFO("搜索键:%s",key.c_str()); */ //param-------------------------------------------------------- ROS_INFO("++++++++++++++++++++++++++++++++++++++++"); int res3 = ros::param::param("param_int",20); //存在 int res4 = ros::param::param("param_int2",20); // 不存在返回默认 ROS_INFO("param获取结果:%d,%d",res3,res4); // getParam 函数 int param_int_value; double param_double_value; bool param_bool_value; std::string param_string_value; std::vector<std::string> param_stus; std::map<std::string, std::string> param_friends; ros::param::get("param_int",param_int_value); ros::param::get("param_double",param_double_value); ros::param::get("param_bool",param_bool_value); ros::param::get("param_string",param_string_value); ros::param::get("param_vector",param_stus); ros::param::get("param_map",param_friends); ROS_INFO("getParam获取的结果:%d,%.2f,%s,%d", param_int_value, param_double_value, param_string_value.c_str(), param_bool_value ); for (auto &&stu : param_stus) { ROS_INFO("stus 元素:%s",stu.c_str()); } for (auto &&f : param_friends) { ROS_INFO("map 元素:%s = %s",f.first.c_str(), f.second.c_str()); } // getParamCached() ros::param::getCached("param_int",param_int_value); ROS_INFO("通过缓存获取数据:%d",param_int_value); //getParamNames() std::vector<std::string> param_names2; ros::param::getParamNames(param_names2); for (auto &&name : param_names2) { ROS_INFO("名称解析name = %s",name.c_str()); } ROS_INFO("----------------------------"); ROS_INFO("存在 param_int 吗? %d",ros::param::has("param_int")); ROS_INFO("存在 param_intttt 吗? %d",ros::param::has("param_intttt")); std::string key; ros::param::search("param_int",key); ROS_INFO("搜索键:%s",key.c_str()); return 0; }
删除
ros::NodeHandle
deleteParam("键")
根据键删除参数,删除成功,返回 true,否则(参数不存在),返回 false
ros::param
del("键")
根据键删除参数,删除成功,返回 true,否则(参数不存在),返回 false
#include "ros/ros.h" int main(int argc, char *argv[]) { setlocale(LC_ALL,""); ros::init(argc,argv,"delete_param"); ros::NodeHandle nh; bool r1 = nh.deleteParam("nh_int"); ROS_INFO("nh 删除结果:%d",r1); bool r2 = ros::param::del("param_int"); ROS_INFO("param 删除结果:%d",r2); return 0; }
常用命令
简介
在 ROS 同提供了一些实用的命令行工具,可以用于获取不同节点的各类信息,常用的命令如下:
- rosnode : 操作节点
- rostopic : 操作话题
- rosservice : 操作服务
- rosmsg : 操作msg消息
- rossrv : 操作srv消息
- rosparam : 操作参数
ROS/CommandLineTools - ROS Wiki
rosnode
rosnode 是用于获取节点信息的命令
rosnode ping 测试到节点的连接状态 rosnode list 列出活动节点 rosnode info 打印节点信息 rosnode machine 列出指定设备上节点 rosnode kill 杀死某个节点 rosnode cleanup 清除不可连接的节点
- rosnode ping测试到节点的连接状态
- rosnode list列出活动节点
- rosnode info打印节点信息
- rosnode machine列出指定设备上的节点
- rosnode kill杀死某个节点
- rosnode cleanup清除无用节点,启动乌龟节点,然后 ctrl + c 关闭,该节点并没被彻底清除,可以使用 cleanup 清除节点
rostopic
rostopic包含rostopic命令行工具,用于显示有关ROS 主题的调试信息,包括发布者,订阅者,发布频率和ROS消息。它还包含一个实验性Python库,用于动态获取有关主题的信息并与之交互。
rostopic bw 显示主题使用的带宽 rostopic delay 显示带有 header 的主题延迟 rostopic echo 打印消息到屏幕 rostopic find 根据类型查找主题 rostopic hz 显示主题的发布频率 rostopic info 显示主题相关信息 rostopic list 显示所有活动状态下的主题 rostopic pub 将数据发布到主题 rostopic type 打印主题类型
rosmsg
rosmsg是用于显示有关 ROS消息类型的 信息的命令行工具。
rosmsg show 显示消息描述 rosmsg info 显示消息信息 rosmsg list 列出所有消息 rosmsg md5 显示 md5 加密后的消息 rosmsg package 显示某个功能包下的所有消息 rosmsg packages 列出包含消息的功能包
rosservice
rosservice args 打印服务参数 rosservice call 使用提供的参数调用服务 rosservice find 按照服务类型查找服务 rosservice info 打印有关服务的信息 rosservice list 列出所有活动的服务 rosservice type 打印服务类型 rosservice uri 打印服务的 ROSRPC uri
rossrv
对标rosmsg
rossrv是用于显示有关ROS服务类型的信息的命令行工具,与 rosmsg 使用语法高度雷同。
rossrv show 显示服务消息详情 rossrv info 显示服务消息相关信息 rossrv list 列出所有服务信息 rossrv md5 显示 md5 加密后的服务消息 rossrv package 显示某个包下所有服务消息 rossrv packages 显示包含服务消息的所有包
rosparam
rosparam set 设置参数 rosparam get 获取参数 rosparam load 从外部文件加载参数 rosparam dump 将参数写出到外部文件 rosparam delete 删除参数 rosparam list 列出所有参数
常用API
初始化
/** @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);
def init_node(name, argv=None, anonymous=False, log_level=None, disable_rostime=False, disable_rosout=False, disable_signals=False, xmlrpc_port=0, tcpros_port=0): """ 在ROS msater中注册节点 @param name: 节点名称,必须保证节点名称唯一,节点名称中不能使用命名空间(不能包含 '/') @type name: str @param anonymous: 取值为 true 时,为节点名称后缀随机编号 @type anonymous: bool """
话题与服务相关对象
C++
在 roscpp 中,话题和服务的相关对象一般由 NodeHandle 创建。
1.发布对象
/** * \brief 根据话题生成发布对象 * * 在 ROS master 注册并返回一个发布者对象,该对象可以发布消息 * * 使用示例如下: * * ros::Publisher pub = handle.advertise<std_msgs::Empty>("my_topic", 1); * * \param topic 发布消息使用的话题 * * \param queue_size 等待发送给订阅者的最大消息数量 * * \param latch (optional) 如果为 true,该话题发布的最后一条消息将被保存,并且后期当有订阅者连接时会将该消息发送给订阅者 * * \return 调用成功时,会返回一个发布对象 * * */ template <class M> Publisher advertise(const std::string& topic, uint32_t queue_size, bool latch = false)
2.订阅对象
/** * \brief 生成某个话题的订阅对象 * * 该函数将根据给定的话题在ROS master 注册,并自动连接相同主题的发布方,每接收到一条消息,都会调用回调 * 函数,并且传入该消息的共享指针,该消息不能被修改,因为可能其他订阅对象也会使用该消息。 * * 使用示例如下: void callback(const std_msgs::Empty::ConstPtr& message) { } ros::Subscriber sub = handle.subscribe("my_topic", 1, callback); * * \param M [template] M 是指消息类型 * \param topic 订阅的话题 * \param queue_size 消息队列长度,超出长度时,头部的消息将被弃用 * \param fp 当订阅到一条消息时,需要执行的回调函数 * \return 调用成功时,返回一个订阅者对象,失败时,返回空对象 * void callback(const std_msgs::Empty::ConstPtr& message){...} ros::NodeHandle nodeHandle; ros::Subscriber sub = nodeHandle.subscribe("my_topic", 1, callback); if (sub) // Enter if subscriber is valid { ... } */ template<class M> Subscriber subscribe(const std::string& topic, uint32_t queue_size, void(*fp)(const boost::shared_ptr<M const>&), const TransportHints& transport_hints = TransportHints())
3.服务对象
/** * \brief 生成服务端对象 * * 该函数可以连接到 ROS master,并提供一个具有给定名称的服务对象。 * * 使用示例如下: \verbatim bool callback(std_srvs::Empty& request, std_srvs::Empty& response) { return true; } ros::ServiceServer service = handle.advertiseService("my_service", callback); \endverbatim * * \param service 服务的主题名称 * \param srv_func 接收到请求时,需要处理请求的回调函数 * \return 请求成功时返回服务对象,否则返回空对象: \verbatim bool Foo::callback(std_srvs::Empty& request, std_srvs::Empty& response) { return true; } ros::NodeHandle nodeHandle; Foo foo_object; ros::ServiceServer service = nodeHandle.advertiseService("my_service", callback); if (service) // Enter if advertised service is valid { ... } \endverbatim */ template<class MReq, class MRes> ServiceServer advertiseService(const std::string& service, bool(*srv_func)(MReq&, MRes&))
4.客户端对象
对象获取:
/** * @brief 创建一个服务客户端对象 * * 当清除最后一个连接的引用句柄时,连接将被关闭。 * * @param service_name 服务主题名称 */ template<class Service> ServiceClient serviceClient(const std::string& service_name, bool persistent = false, const M_string& header_values = M_string())
请求发送函数:
/** * @brief 发送请求 * 返回值为 bool 类型,true,请求处理成功,false,处理失败。 */ template<class Service> bool call(Service& service)
等待服务函数1:
/** * ros::service::waitForService("addInts"); * \brief 等待服务可用,否则一致处于阻塞状态 * \param service_name 被"等待"的服务的话题名称 * \param timeout 等待最大时常,默认为 -1,可以永久等待直至节点关闭 * \return 成功返回 true,否则返回 false。 */ ROSCPP_DECL bool waitForService(const std::string& service_name, ros::Duration timeout = ros::Duration(-1));
等待服务函数2:
/** * client.waitForExistence(); * \brief 等待服务可用,否则一致处于阻塞状态 * \param timeout 等待最大时常,默认为 -1,可以永久等待直至节点关闭 * \return 成功返回 true,否则返回 false。 */ bool waitForExistence(ros::Duration timeout = ros::Duration(-1));
回旋函数
C++
在ROS程序中,频繁的使用了 ros::spin() 和 ros::spinOnce() 两个回旋函数,可以用于处理回调函数。
1.spinOnce()
/** * \brief 处理一轮回调 * * 一般应用场景: * 在循环体内,处理所有可用的回调函数 * */ ROSCPP_DECL void spinOnce();
2.spin()
/** * \brief 进入循环处理回调 */ ROSCPP_DECL void spin();
对比
相同点:二者都用于处理回调函数;
不同点:ros::spin() 是进入了循环执行回调函数,而 ros::spinOnce() 只会执行一次回调函数(没有循环),在 ros::spin() 后的语句不会执行到,而 ros::spinOnce() 后的语句可以执行。
时间
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
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
其他
在发布实现时,一般会循环发布消息,循环的判断条件一般由节点状态来控制,C++中可以通过 ros::ok() 来判断节点状态是否正常,而 python 中则通过 rospy.is_shutdown() 来实现判断,导致节点退出的原因主要有如下几种:
- 节点接收到了关闭信息,比如常用的 ctrl + c 快捷键就是关闭节点的信号;
- 同名节点启动,导致现有节点退出;
- 程序中的其他部分调用了节点关闭相关的API(C++中是ros::shutdown(),python中是rospy.signal_shutdown())
另外,日志相关的函数也是极其常用的,在ROS中日志被划分成如下级别:
- DEBUG(调试):只在调试时使用,此类消息不会输出到控制台;
- INFO(信息):标准消息,一般用于说明系统内正在执行的操作;
- WARN(警告):提醒一些异常情况,但程序仍然可以执行;
- ERROR(错误):提示错误信息,此类错误会影响程序运行;
- FATAL(严重错误):此类错误将阻止节点继续运行。
ROS 的运行管理
ROS元功能包
显而易见的,逐一安装功能包的效率低下,在ROS中,提供了一种方式可以将不同的功能包打包成一个功能包,当安装某个功能模块时,直接调用打包后的功能包即可,该包又称之为元功能包(metapackage)。
概念
MetaPackage是Linux的一个文件管理系统的概念。是ROS中的一个虚包,里面没有实质性的内容,但是它依赖了其他的软件包,通过这种方法可以把其他包组合起来,我们可以认为它是一本书的目录索引,告诉我们这个包集合中有哪些子包,并且该去哪里下载。
作用
方便用户的安装,我们只需要这一个包就可以把其他相关的软件包组织到一起安装了。
实现
首先:新建一个功能包
然后:修改package.xml ,内容如下:
<exec_depend>被集成的功能包</exec_depend> ..... <export> <metapackage /> </export>
最后:修改 CMakeLists.txt,内容如下:
cmake_minimum_required(VERSION 3.0.2) project(demo) find_package(catkin REQUIRED) catkin_metapackage()
ROS节点运行管理launch文件
一个程序中可能需要启动多个节点,比如:ROS 内置的小乌龟案例,如果要控制乌龟运动,要启动多个窗口,分别启动 roscore、乌龟界面节点、键盘控制节点。如果每次都调用 rosrun 逐一启动,显然效率低下,如何优化?
采用的优化策略便是使用roslaunch 命令集合 launch 文件启动管理节点
概念
launch 文件是一个 XML 格式的文件,可以启动本地和远程的多个节点,还可以在参数服务器中设置参数。
作用
简化节点的配置与启动,提高ROS程序的启动效率。
使用
以 turtlesim 为例演示
1.新建launch文件
在功能包下添加 launch目录, 目录下新建 xxxx.launch 文件,编辑 launch 文件
<launch> <node pkg="turtlesim" type="turtlesim_node" name="myTurtle" output="screen" /> <node pkg="turtlesim" type="turtle_teleop_key" name="myTurtleContro" output="screen" /> </launch>
2.调用 launch 文件
roslaunch 包名 xxx.launch
注意:roslaunch 命令执行launch文件时,首先会判断是否启动了 roscore,如果启动了,则不再启动,否则,会自动调用 roscore
launch文件 结构
launch文件标签之launch
<launch>标签是所有 launch 文件的根标签,充当其他标签的容器
1.属性
- deprecated = "弃用声明"告知用户当前 launch 文件已经弃用
2.子级标签
所有其它标签都是launch的子级
launch文件标签之node
<node>标签用于指定 ROS 节点,是最常见的标签,需要注意的是: roslaunch 命令不能保证按照 node 的声明顺序来启动节点(节点的启动是多进程的)
1.属性
- pkg="包名"节点所属的包
- type="nodeType"节点类型(与之相同名称的可执行文件)
- name="nodeName"节点名称(在 ROS 网络拓扑中节点的名称)
- args="xxx xxx xxx" (可选)将参数传递给节点
- machine="机器名"在指定机器上启动节点
- respawn="true | false" (可选)如果节点退出,是否自动重启
- respawn_delay=" N" (可选)如果 respawn 为 true, 那么延迟 N 秒后启动节点
- required="true | false" (可选)该节点是否必须,如果为 true,那么如果该节点退出,将杀死整个 roslaunch
- ns="xxx" (可选)在指定命名空间 xxx 中启动节点
- clear_params="true | false" (可选)在启动前,删除节点的私有空间的所有参数
- output="log | screen" (可选)日志发送目标,可以设置为 log 日志文件,或 screen 屏幕,默认是 log
2.子级标签
- env 环境变量设置
- remap 重映射节点名称
- rosparam 参数设置
- param 参数设置
launch文件标签之include
include标签用于将另一个 xml 格式的 launch 文件导入到当前文件
1.属性
- file="$(find 包名)/xxx/xxx.launch"要包含的文件路径
- ns="xxx" (可选)在指定命名空间导入文件
2.子级标签
- env 环境变量设置
- arg 将参数传递给被包含的文件
launch文件标签之remap
用于话题重命名
1.属性
- from="xxx"原始话题名称
- to="yyy"目标名称
2.子级标签
- 无
launch文件标签之param
<param>标签主要用于在参数服务器上设置参数,参数源可以在标签中通过 value 指定,也可以通过外部文件加载,在<node>标签中时,相当于私有命名空间。
1.属性
- name="命名空间/参数名"参数名称,可以包含命名空间
- value="xxx" (可选)定义参数值,如果此处省略,必须指定外部文件作为参数源
- type="str | int | double | bool | yaml" (可选)指定参数类型,如果未指定,roslaunch 会尝试确定参数类型,规则如下:
-
- 如果包含 '.' 的数字解析未浮点型,否则为整型
- "true" 和 "false" 是 bool 值(不区分大小写)
- 其他是字符串
2.子级标签
- 无
launch文件标签之rosparam
<rosparam>标签可以从 YAML 文件导入参数,或将参数导出到 YAML 文件,也可以用来删除参数,<rosparam>标签在<node>标签中时被视为私有。
1.属性
- command="load | dump | delete" (可选,默认 load)加载、导出或删除参数
- file="$(find xxxxx)/xxx/yyy...."加载或导出到的 yaml 文件
- param="参数名称"
- ns="命名空间" (可选)
2.子级标签
- 无
launch文件标签之group
<group>标签可以对节点分组,具有 ns 属性,可以让节点归属某个命名空间
1.属性
- ns="名称空间" (可选)
- clear_params="true | false" (可选)启动前,是否删除组名称空间的所有参数(慎用....此功能危险)
2.子级标签
- 除了launch 标签外的其他标签
launch文件标签之arg
<arg>标签是用于动态传参,类似于函数的参数,可以增强launch文件的灵活性
1.属性
- name="参数名称"
- default="默认值" (可选)
- value="数值" (可选)不可以与 default 并存
- doc="描述"参数说明
2.子级标签
- 无
3.示例
<launch> <arg name="xxx" /> <param name="param" value="$(arg xxx)" /> </launch> roslaunch hello.launch xxx:=值
ROS工作空间覆盖
所谓工作空间覆盖,是指不同工作空间中,存在重名的功能包的情形。
实现
0.新建工作空间A与工作空间B,两个工作空间中都创建功能包: turtlesim。
1.在 ~/.bashrc 文件下追加当前工作空间的 bash 格式如下:
source /home/用户/路径/工作空间A/devel/setup.bash source /home/用户/路径/工作空间B/devel/setup.bash
2.新开命令行:source .bashrc加载环境变量
3.查看ROS环境环境变量echo $ROS_PACKAGE_PATH
结果:自定义工作空间B:自定义空间A:系统内置空间
4.调用命令:roscd turtlesim会进入自定义工作空间B
原因
ROS 会解析 .bashrc 文件,并生成 ROS_PACKAGE_PATH ROS包路径,该变量中按照 .bashrc 中配置设置工作空间优先级,在设置时需要遵循一定的原则:ROS_PACKAGE_PATH 中的值,和 .bashrc 的配置顺序相反--->后配置的优先级更高,如果更改自定义空间A与自定义空间B的source顺序,那么调用时,将进入工作空间A。
结论
功能包重名时,会按照 ROS_PACKAGE_PATH 查找,配置在前的会优先执行。
隐患
存在安全隐患,比如当前工作空间B优先级更高,意味着当程序调用 turtlesim 时,不会调用工作空间A也不会调用系统内置的 turtlesim,如果工作空间A在实现时有其他功能包依赖于自身的 turtlesim,而按照ROS工作空间覆盖的涉及原则,那么实际执行时将会调用工作空间B的turtlesim,从而导致执行异常,出现安全隐患。
ROS节点名称重名
场景:ROS 中创建的节点是有名称的,C++初始化节点时通过API:ros::init(argc,argv,"xxxx");来定义节点名称,在Python中初始化节点则通过 rospy.init_node("yyyy") 来定义节点名称。在ROS的网络拓扑中,是不可以出现重名的节点的,因为假设可以重名存在,那么调用时会产生混淆,这也就意味着,不可以启动重名节点或者同一个节点启动多次,的确,在ROS中如果启动重名节点的话,之前已经存在的节点会被直接关闭,但是如果有这种需求的话,怎么优化呢
在ROS中给出的解决策略是使用命名空间或名称重映射。
命名空间就是为名称添加前缀,名称重映射是为名称起别名。这两种策略都可以解决节点重名问题,两种策略的实现途径有多种:
- rosrun 命令
- launch 文件
- 编码实现
rosrun设置命名空间与重映射
rosrun设置命名空间
语法: rosrun 包名 节点名 __ns:=新名称
rosrun turtlesim turtlesim_node __ns:=/xxx
rosrun turtlesim turtlesim_node __ns:=/yyy
rosnode list查看节点信息,显示结果:
/xxx/turtlesim /yyy/turtlesim
rosrun名称重映射
语法: rosrun 包名 节点名 __name:=新名称
rosrun turtlesim turtlesim_node __name:=t1 | rosrun turtlesim turtlesim_node /turtlesim:=t1(不适用于python) rosrun turtlesim turtlesim_node __name:=t2 | rosrun turtlesim turtlesim_node /turtlesim:=t2(不适用于python)
rosnode list查看节点信息,显示结果:
/t1 /t2
rosrun命名空间与名称重映射叠加
语法: rosrun 包名 节点名 __ns:=新名称 __name:=新名称
rosrun turtlesim turtlesim_node __ns:=/xxx __name:=tn
rosnode list查看节点信息,显示结果:
/xxx/tn
使用环境变量也可以设置命名空间,启动节点前在终端键入如下命令:
export ROS_NAMESPACE=xxxx
launch文件设置命名空间与重映射
launch 文件在 node 标签中有两个属性: name 和 ns,二者分别是用于实现名称重映射与命名空间设置的。
launch文件
<launch> <node pkg="turtlesim" type="turtlesim_node" name="t1" /> <node pkg="turtlesim" type="turtlesim_node" name="t2" /> <node pkg="turtlesim" type="turtlesim_node" name="t1" ns="hello"/> </launch>
在 node 标签中,name 属性是必须的,ns 可选。
rosnode list查看节点信息,显示结果:
/t1 /t2 /t1/hello
编码设置命名空间与重映射
C++ 实现:重映射
在名称后面添加时间戳。
核心代码:ros::init(argc,argv,"zhangsan",ros::init_options::AnonymousName);
C++ 实现:命名空间
节点名称设置了命名空间。
std::map<std::string, std::string> map; map["__ns"] = "xxxx"; ros::init(map,"wangqiang");
ROS话题名称重复
概念
在 ROS 中节点终端,不同的节点之间通信都依赖于话题,话题名称也可能出现重复的情况,这种情况下,系统虽然不会抛出异常,但是可能导致订阅的消息非预期的,从而导致节点运行异常。这种情况下需要将两个节点的话题名称由相同修改为不同。
又或者,两个节点是可以通信的,两个节点之间使用了相同的消息类型,但是由于,话题名称不同,导致通信失败。这种情况下需要将两个节点的话题名称由不同修改为相同。
话题通信可以使用命名空间作为前缀、还可以使用节点名称最为前缀。两种策略的实现途径有多种:
- rosrun 命令
- launch 文件
- 编码实现
rosrun设置话题重映射
rosrun名称重映射语法: rorun 包名 节点名 话题名:=新话题名称
launch文件设置话题重映射
<node pkg="xxx" type="xxx" name="xxx"> <remap from="原话题" to="新话题" /> </node>
编码设置话题名称
C++ 实现
演示准备:
1.初始化节点设置一个节点名称
ros::init(argc,argv,"hello")
2.设置不同类型的话题
3.启动节点时,传递一个 __ns:= xxx
4.节点启动后,使用 rostopic 查看话题信息
1.1全局名称
格式:以/开头的名称,和节点名称无关
比如:/xxx/yyy/zzz
示例1:ros::Publisher pub = nh.advertise<std_msgs::String>("/chatter",1000);
结果1:/chatter
示例2:ros::Publisher pub = nh.advertise<std_msgs::String>("/chatter/money",1000);
结果2:/chatter/money
1.2相对名称
格式:非/开头的名称,参考命名空间(与节点名称平级)来确定话题名称
示例1:ros::Publisher pub = nh.advertise<std_msgs::String>("chatter",1000);
结果1:xxx/chatter
示例2:ros::Publisher pub = nh.advertise<std_msgs::String>("chatter/money",1000);
结果2:xxx/chatter/money
1.3私有名称
格式:以~开头的名称
示例1:
ros::NodeHandle nh("~");
ros::Publisher pub = nh.advertise<std_msgs::String>("chatter",1000);
结果1:/xxx/hello/chatter
示例2:
ros::NodeHandle nh("~");
ros::Publisher pub = nh.advertise<std_msgs::String>("chatter/money",1000);
结果2:/xxx/hello/chatter/money
PS:当使用~,而话题名称有时/开头时,那么话题名称是绝对的
示例3:
ros::NodeHandle nh("~");
ros::Publisher pub = nh.advertise<std_msgs::String>("/chatter/money",1000);
结果3:/chatter/money
ROS参数名称重复
概念
关于参数重名的处理,没有重映射实现,为了尽量的避免参数重名,都是使用为参数名添加前缀的方式,实现类似于话题名称,有全局、相对、和私有三种类型之分。
- 全局(参数名称直接参考ROS系统,与节点命名空间平级)
- 相对(参数名称参考的是节点的命名空间,与节点名称平级)
- 私有(参数名称参考节点名称,是节点名称的子级)
设置参数的方式也有三种:
- rosrun 命令
- launch 文件
- 编码实现
rosrun设置参数
语法: rosrun 包名 节点名称 _参数名:=参数值
launch文件设置参数
可以在 node 标签外,或 node 标签中通过 param 或 rosparam 来设置参数。在 node 标签外设置的参数是全局性质的,参考的是 / ,在 node 标签中设置的参数是私有性质的,参考的是 /命名空间/节点名称。
<launch> <param name="p1" value="100" /> <node pkg="turtlesim" type="turtlesim_node" name="t1"> <param name="p2" value="100" /> </node> </launch>
编码设置参数
C++实现
在 C++ 中,可以使用 ros::param 或者 ros::NodeHandle 来设置参数。
ros::param设置参数
设置参数调用API是ros::param::set,该函数中,参数1传入参数名称,参数2是传入参数值,参数1中参数名称设置时,如果以 / 开头,那么就是全局参数,如果以 ~ 开头,那么就是私有参数,既不以 / 也不以 ~ 开头,那么就是相对参数。代码示例:
ros::param::set("/set_A",100); //全局,和命名空间以及节点名称无关 ros::param::set("set_B",100); //相对,参考命名空间 ros::param::set("~set_C",100); //私有,参考命名空间与节点名称
运行时,假设设置的 namespace 为 xxx,节点名称为 yyy,使用 rosparam list 查看:
/set_A /xxx/set_B /xxx/yyy/set_C
ros::NodeHandle设置参数
设置参数时,首先需要创建 NodeHandle 对象,然后调用该对象的 setParam 函数,该函数参数1为参数名,参数2为要设置的参数值,如果参数名以 / 开头,那么就是全局参数,如果参数名不以 / 开头,那么,该参数是相对参数还是私有参数与NodeHandle 对象有关,如果NodeHandle 对象创建时如果是调用的默认的无参构造,那么该参数是相对参数,如果NodeHandle 对象创建时是使用:
ros::NodeHandle nh; nh.setParam("/nh_A",100); //全局,和命名空间以及节点名称无关 nh.setParam("nh_B",100); //相对,参考命名空间 ros::NodeHandle nh_private("~"); nh_private.setParam("nh_C",100);//私有,参考命名空间与节点名称
ROS分布式通信
ROS是一个分布式计算环境。一个运行中的ROS系统可以包含分布在多台计算机上多个节点。根据系统的配置方式,任何节点可能随时需要与任何其他节点进行通信。
因此,ROS对网络配置有某些要求:
- 所有端口上的所有机器之间必须有完整的双向连接。
- 每台计算机必须通过所有其他计算机都可以解析的名称来公告自己。
实现
1.准备
先要保证不同计算机处于同一网络中,最好分别设置固定IP,如果为虚拟机,需要将网络适配器改为桥接模式;
2.配置文件修改
分别修改不同计算机的 /etc/hosts 文件,在该文件中加入对方的IP地址和计算机名:
主机端:
从机的IP 从机计算机名
从机端:
主机的IP 主机计算机名
设置完毕,可以通过 ping 命令测试网络通信是否正常。
IP地址查看名: ifconfig
计算机名称查看: hostname
3.配置主机IP
配置主机的 IP 地址
~/.bashrc 追加
export ROS_MASTER_URI=http://主机IP:11311 export ROS_HOSTNAME=主机IP
4.配置从机IP
配置从机的 IP 地址,从机可以有多台,每台都做如下设置:
~/.bashrc 追加
export ROS_MASTER_URI=http://主机IP:11311 export ROS_HOSTNAME=从机IP
5. 启动
1.主机启动 roscore(必须)
2.主机启动订阅节点,从机启动发布节点,测试通信是否正常
3.反向测试,主机启动发布节点,从机启动订阅节点,测试通信是否正常
ROS 常用组件
TF坐标变换
机器人系统上,有多个传感器,如激光雷达、摄像头等,有的传感器是可以感知机器人周边的物体方位(或者称之为:坐标,横向、纵向、高度的距离信息)的,以协助机器人定位障碍物,可以直接将物体相对该传感器的方位信息,等价于物体相对于机器人系统或机器人其它组件的方位信息吗?显示是不行的,这中间需要一个转换过程。更具体描述如下:
现有一带机械臂的机器人(比如:PR2)需要夹取目标物,当前机器人头部摄像头可以探测到目标物的坐标(x,y,z),不过该坐标是以摄像头为参考系的,而实际操作目标物的是机械臂的夹具,当前我们需要将该坐标转换成相对于机械臂夹具的坐标,这个过程如何实现?
rosbag
机器人传感器获取到的信息,有时我们可能需要时时处理,有时可能只是采集数据,事后分析.
在ROS中关于数据的留存以及读取实现,提供了专门的工具: rosbag。
概念
是用于录制和回放 ROS 主题的一个工具集。
作用
实现了数据的复用,方便调试、测试。
本质
rosbag本质也是ros的节点,当录制时,rosbag是一个订阅节点,可以订阅话题消息并将订阅到的数据写入磁盘文件;当重放时,rosbag是一个发布节点,可以读取磁盘文件,发布文件中的话题消息。
rosbag使用_命令行
开始录制
rosbag record -a -O 目标文件
查看文件
rosbag info 文件名
回放文件
rosbag play 文件名
rosbag使用_编码
C++实现写bag
#include "ros/ros.h" #include "rosbag/bag.h" #include "std_msgs/String.h" int main(int argc, char *argv[]) { ros::init(argc,argv,"bag_write"); ros::NodeHandle nh; //创建bag对象 rosbag::Bag bag; //打开 bag.open("/home/rosdemo/demo/test.bag",rosbag::BagMode::Write); //写 std_msgs::String msg; msg.data = "hello world"; bag.write("/chatter",ros::Time::now(),msg); bag.write("/chatter",ros::Time::now(),msg); bag.write("/chatter",ros::Time::now(),msg); bag.write("/chatter",ros::Time::now(),msg); //关闭 bag.close(); return 0; }
C++实现读取bag
/* 读取 bag 文件: */ #include "ros/ros.h" #include "rosbag/bag.h" #include "rosbag/view.h" #include "std_msgs/String.h" #include "std_msgs/Int32.h" int main(int argc, char *argv[]) { setlocale(LC_ALL,""); ros::init(argc,argv,"bag_read"); ros::NodeHandle nh; //创建 bag 对象 rosbag::Bag bag; //打开 bag 文件 bag.open("/home/rosdemo/demo/test.bag",rosbag::BagMode::Read); //读数据 for (rosbag::MessageInstance const m : rosbag::View(bag)) { std_msgs::String::ConstPtr p = m.instantiate<std_msgs::String>(); if(p != nullptr){ ROS_INFO("读取的数据:%s",p->data.c_str()); } } //关闭文件流 bag.close(); return 0; }
python写bag
#! /usr/bin/env python import rospy import rosbag from std_msgs.msg import String if __name__ == "__main__": #初始化节点 rospy.init_node("w_bag_p") # 创建 rosbag 对象 bag = rosbag.Bag("/home/rosdemo/demo/test.bag",'w') # 写数据 s = String() s.data= "hahahaha" bag.write("chatter",s) bag.write("chatter",s) bag.write("chatter",s) # 关闭流 bag.close()
python读bag
#! /usr/bin/env python import rospy import rosbag from std_msgs.msg import String if __name__ == "__main__": #初始化节点 rospy.init_node("w_bag_p") # 创建 rosbag 对象 bag = rosbag.Bag("/home/rosdemo/demo/test.bag",'r') # 读数据 bagMessage = bag.read_messages("chatter") for topic,msg,t in bagMessage: rospy.loginfo("%s,%s,%s",topic,msg,t) # 关闭流 bag.close()
ROS进阶通信
action通信
关于action通信,我们先从之前导航中的应用场景开始介绍,描述如下:
机器人导航到某个目标点,此过程需要一个节点A发布目标信息,然后一个节点B接收到请求并控制移动,最终响应目标达成状态信息。
乍一看,这好像是服务通信实现,因为需求中要A发送目标,B执行并返回结果,这是一个典型的基于请求响应的应答模式,不过,如果只是使用基本的服务通信实现,存在一个问题:导航是一个过程,是耗时操作,如果使用服务通信,那么只有在导航结束时,才会产生响应结果,而在导航过程中,节点A是不会获取到任何反馈的,从而可能出现程序"假死"的现象,过程的不可控意味着不良的用户体验,以及逻辑处理的缺陷(比如:导航中止的需求无法实现)。更合理的方案应该是:导航过程中,可以连续反馈当前机器人状态信息,当导航终止时,再返回最终的执行结果。在ROS中,该实现策略称之为:action 通信。
概念
在ROS中提供了actionlib功能包集,用于实现 action 通信。action 是一种类似于服务通信的实现,其实现模型也包含请求和响应,但是不同的是,在请求和响应的过程中,服务端还可以连续的反馈当前任务进度,客户端可以接收连续反馈并且还可以取消任务。
作用
一般适用于耗时的请求响应场景,用以获取连续的状态反馈。
action通信自定义action文件
定义action文件首先新建功能包,并导入依赖: roscpp rospy std_msgs actionlib actionlib_msgs;
然后功能包下新建 action 目录,新增 Xxx.action(比如:AddInts.action)。
action 文件内容组成分为三部分:请求目标值、最终响应结果、连续反馈,三者之间使用---分割示例内容如下:
#目标值 int32 num --- #最终结果 int32 result --- #连续反馈 float64 progress_bar
编辑配置文件
find_package (catkin REQUIRED COMPONENTS roscpp rospy std_msgs actionlib actionlib_msgs ) add_action_files( FILES AddInts.action ) generate_messages( DEPENDENCIES std_msgs actionlib_msgs ) catkin_package( # INCLUDE_DIRS include # LIBRARIES demo04_action CATKIN_DEPENDS roscpp rospy std_msgs actionlib actionlib_msgs # DEPENDS system_lib )
服务端代码
#include "ros/ros.h" #include "actionlib/server/simple_action_server.h" #include "demo01_action/AddIntsAction.h" /* 需求: 创建两个ROS节点,服务器和客户端, 客户端可以向服务器发送目标数据N(一个整型数据) 服务器会计算1到N之间所有整数的和,这是一个循环累加的过程,返回给客户端, 这是基于请求响应模式的, 又已知服务器从接收到请求到产生响应是一个耗时操作,每累加一次耗时0.1s, 为了良好的用户体验,需要服务器在计算过程中, 每累加一次,就给客户端响应一次百分比格式的执行进度,使用action实现。 流程: 1.包含头文件; 2.初始化ROS节点; 3.创建NodeHandle; 4.创建action服务对象; 5.处理请求,产生反馈与响应; 6.spin(). */ typedef actionlib::SimpleActionServer<demo01_action::AddIntsAction> Server; void cb(const demo01_action::AddIntsGoalConstPtr &goal,Server* server){ //获取目标值 int num = goal->num; ROS_INFO("目标值:%d",num); //累加并响应连续反馈 int result = 0; demo01_action::AddIntsFeedback feedback;//连续反馈 ros::Rate rate(10);//通过频率设置休眠时间 for (int i = 1; i <= num; i++) { result += i; //组织连续数据并发布 feedback.progress_bar = i / (double)num; server->publishFeedback(feedback); rate.sleep(); } //设置最终结果 demo01_action::AddIntsResult r; r.result = result; server->setSucceeded(r); ROS_INFO("最终结果:%d",r.result); } int main(int argc, char *argv[]) { setlocale(LC_ALL,""); ROS_INFO("action服务端实现"); // 2.初始化ROS节点; ros::init(argc,argv,"AddInts_server"); // 3.创建NodeHandle; ros::NodeHandle nh; // 4.创建action服务对象; /*SimpleActionServer(ros::NodeHandle n, std::string name, boost::function<void (const demo01_action::AddIntsGoalConstPtr &)> execute_callback, bool auto_start) */ // actionlib::SimpleActionServer<demo01_action::AddIntsAction> server(....); Server server(nh,"addInts",boost::bind(&cb,_1,&server),false); server.start(); // 5.处理请求,产生反馈与响应; // 6.spin(). ros::spin(); return 0; }
客户端代码
#include "ros/ros.h" #include "actionlib/client/simple_action_client.h" #include "demo01_action/AddIntsAction.h" /* 需求: 创建两个ROS节点,服务器和客户端, 客户端可以向服务器发送目标数据N(一个整型数据) 服务器会计算1到N之间所有整数的和,这是一个循环累加的过程,返回给客户端, 这是基于请求响应模式的, 又已知服务器从接收到请求到产生响应是一个耗时操作,每累加一次耗时0.1s, 为了良好的用户体验,需要服务器在计算过程中, 每累加一次,就给客户端响应一次百分比格式的执行进度,使用action实现。 流程: 1.包含头文件; 2.初始化ROS节点; 3.创建NodeHandle; 4.创建action客户端对象; 5.发送目标,处理反馈以及最终结果; 6.spin(). */ typedef actionlib::SimpleActionClient<demo01_action::AddIntsAction> Client; //处理最终结果 void done_cb(const actionlib::SimpleClientGoalState &state, const demo01_action::AddIntsResultConstPtr &result){ if (state.state_ == state.SUCCEEDED) { ROS_INFO("最终结果:%d",result->result); } else { ROS_INFO("任务失败!"); } } //服务已经激活 void active_cb(){ ROS_INFO("服务已经被激活...."); } //处理连续反馈 void feedback_cb(const demo01_action::AddIntsFeedbackConstPtr &feedback){ ROS_INFO("当前进度:%.2f",feedback->progress_bar); } int main(int argc, char *argv[]) { setlocale(LC_ALL,""); // 2.初始化ROS节点; ros::init(argc,argv,"AddInts_client"); // 3.创建NodeHandle; ros::NodeHandle nh; // 4.创建action客户端对象; // SimpleActionClient(ros::NodeHandle & n, const std::string & name, bool spin_thread = true) // actionlib::SimpleActionClient<demo01_action::AddIntsAction> client(nh,"addInts"); Client client(nh,"addInts",true); //等待服务启动 client.waitForServer(); // 5.发送目标,处理反馈以及最终结果; /* void sendGoal(const demo01_action::AddIntsGoal &goal, boost::function<void (const actionlib::SimpleClientGoalState &state, const demo01_action::AddIntsResultConstPtr &result)> done_cb, boost::function<void ()> active_cb, boost::function<void (const demo01_action::AddIntsFeedbackConstPtr &feedback)> feedback_cb) */ demo01_action::AddIntsGoal goal; goal.num = 10; client.sendGoal(goal,&done_cb,&active_cb,&feedback_cb); // 6.spin(). ros::spin(); return 0; }
动态参数
概念
一种可以在运行时更新参数而无需重启节点的参数配置策略。
参数服务器的数据被修改时,如果节点不重新访问,那么就不能获取修改后的数据,例如在乌龟背景色修改的案例中,先启动乌龟显示节点,然后再修改参数服务器中关于背景色设置的参数,那么窗体的背景色是不会修改的,必须要重启乌龟显示节点才能生效。而一些特殊场景下,是要求要能做到动态获取的,也即,参数一旦修改,能够通知节点参数已经修改并读取修改后的数据,比如:
机器人调试时,需要修改机器人轮廓信息(长宽高)、传感器位姿信息....,如果这些信息存储在参数服务器中,那么意味着需要重启节点,才能使更新设置生效,但是希望修改完毕之后,某些节点能够即时更新这些参数信息。
在ROS中针对这种场景已经给出的解决方案: dynamic reconfigure 动态配置参数。
动态配置参数,之所以能够实现即时更新,因为被设计成 CS 架构,客户端修改参数就是向服务器发送请求,服务器接收到请求之后,读取修改后的是参数。
作用
主要应用于需要动态更新参数的场景,比如参数调试、功能切换等。典型应用:导航时参数的动态调试。
动态参数客户端
客户端实现流程:
- 新建并编辑 .cfg 文件;
- 编辑CMakeLists.txt;
- 编译。
添加.cfg文件
新建 cfg 文件夹,添加 xxx.cfg 文件(并添加可执行权限),cfg 文件其实就是一个 python 文件,用于生成参数修改的客户端(GUI)。
#! /usr/bin/env python """ 4生成动态参数 int,double,bool,string,列表 5实现流程: 6 1.导包 7 2.创建生成器 8 3.向生成器添加若干参数 9 4.生成中间文件并退出 10 """ # 1.导包 from dynamic_reconfigure.parameter_generator_catkin import * PACKAGE = "demo02_dr" # 2.创建生成器 gen = ParameterGenerator() # 3.向生成器添加若干参数 #add(name, paramtype, level, description, default=None, min=None, max=None, edit_method="") gen.add("int_param",int_t,0,"整型参数",50,0,100) gen.add("double_param",double_t,0,"浮点参数",1.57,0,3.14) gen.add("string_param",str_t,0,"字符串参数","hello world ") gen.add("bool_param",bool_t,0,"bool参数",True) many_enum = gen.enum([gen.const("small",int_t,0,"a small size"), gen.const("mediun",int_t,1,"a medium size"), gen.const("big",int_t,2,"a big size") ],"a car size set") gen.add("list_param",int_t,0,"列表参数",0,0,2, edit_method=many_enum) # 4.生成中间文件并退出 exit(gen.generate(PACKAGE,"dr_node","dr"))
chmod +x xxx.cfg添加权限
配置 CMakeLists.txt
generate_dynamic_reconfigure_options( cfg/mycar.cfg )
动态参数服务端A(C++)
#include "ros/ros.h" #include "dynamic_reconfigure/server.h" #include "demo02_dr/drConfig.h" /* 动态参数服务端: 参数被修改时直接打印 实现流程: 1.包含头文件 2.初始化 ros 节点 3.创建服务器对象 4.创建回调对象(使用回调函数,打印修改后的参数) 5.服务器对象调用回调对象 6.spin() */ void cb(demo02_dr::drConfig& config, uint32_t level){ ROS_INFO("动态参数解析数据:%d,%.2f,%d,%s,%d", config.int_param, config.double_param, config.bool_param, config.string_param.c_str(), config.list_param ); } int main(int argc, char *argv[]) { setlocale(LC_ALL,""); // 2.初始化 ros 节点 ros::init(argc,argv,"dr"); // 3.创建服务器对象 dynamic_reconfigure::Server<demo02_dr::drConfig> server; // 4.创建回调对象(使用回调函数,打印修改后的参数) dynamic_reconfigure::Server<demo02_dr::drConfig>::CallbackType cbType; cbType = boost::bind(&cb,_1,_2); // 5.服务器对象调用回调对象 server.setCallback(cbType); // 6.spin() ros::spin(); return 0; }
pluginlib
概念
pluginlib直译是插件库,所谓插件字面意思就是可插拔的组件,比如:以计算机为例,可以通过USB接口自由插拔的键盘、鼠标、U盘...都可以看作是插件实现,其基本原理就是通过规范化的USB接口协议实现计算机与USB设备的自由组合。同理,在软件编程中,插件是一种遵循一定规范的应用程序接口编写出来的程序,插件程序依赖于某个应用程序,且应用程序可以与不同的插件程序自由组合。在ROS中,也会经常使用到插件,场景如下:
1.导航插件:在导航中,涉及到路径规划模块,路径规划算法有多种,也可以自实现,导航应用时,可能需要测试不同算法的优劣以选择更合适的实现,这种场景下,ROS中就是通过插件的方式来实现不同算法的灵活切换的。
2.rviz插件:在rviz中已经提供了丰富的功能实现,但是即便如此,特定场景下,开发者可能需要实现某些定制化功能并集成到rviz中,这一集成过程也是基于插件的。
作用
- 结构清晰;
- 低耦合,易修改,可维护性强;
- 可移植性强,更具复用性;
- 结构容易调整,插件可以自由增减;
实现流程:
- 准备;
- 创建基类;
- 创建插件类;
- 注册插件;
- 构建插件库;
- 使插件可用于ROS工具链;
-
- 配置xml
- 导出插件
- 使用插件;
- 执行。
创建基类
在 xxx/include/xxx下新建C++头文件: polygon_base.h,所有的插件类都需要继承此基类,内容如下:
#ifndef XXX_POLYGON_BASE_H_ #define XXX_POLYGON_BASE_H_ namespace polygon_base { class RegularPolygon { public: virtual void initialize(double side_length) = 0; virtual double area() = 0; virtual ~RegularPolygon(){} protected: RegularPolygon(){} }; }; #endif
PS:基类必须提供无参构造函数,所以关于多边形的边长没有通过构造函数而是通过单独编写的initialize函数传参。
创建插件
在 xxx/include/xxx下新建C++头文件:polygon_plugins.h,内容如下:
#ifndef XXX_POLYGON_PLUGINS_H_ #define XXX_POLYGON_PLUGINS_H_ #include <xxx/polygon_base.h> #include <cmath> namespace polygon_plugins { class Triangle : public polygon_base::RegularPolygon { public: Triangle(){} void initialize(double side_length) { side_length_ = side_length; } double area() { return 0.5 * side_length_ * getHeight(); } double getHeight() { return sqrt((side_length_ * side_length_) - ((side_length_ / 2) * (side_length_ / 2))); } private: double side_length_; }; class Square : public polygon_base::RegularPolygon { public: Square(){} void initialize(double side_length) { side_length_ = side_length; } double area() { return side_length_ * side_length_; } private: double side_length_; }; }; #endif
注册插件
在 src 目录下新建 polygon_plugins.cpp 文件,内容如下:
//pluginlib 宏,可以注册插件类 #include <pluginlib/class_list_macros.h> #include <xxx/polygon_base.h> #include <xxx/polygon_plugins.h> //参数1:衍生类 参数2:基类 PLUGINLIB_EXPORT_CLASS(polygon_plugins::Triangle, polygon_base::RegularPolygon) PLUGINLIB_EXPORT_CLASS(polygon_plugins::Square, polygon_base::RegularPolygon)
该文件会将两个衍生类注册为插件。
构建插件库
在 CMakeLists.txt 文件中设置内容如下:
include_directories(include) add_library(polygon_plugins src/polygon_plugins.cpp)
至此,可以调用 catkin_make 编译,编译完成后,在工作空间/devel/lib目录下,会生成相关的 .so 文件。
使插件可用于ROS工具链
配置xml
功能包下新建文件:polygon_plugins.xml,内容如下:
<!-- 插件库的相对路径 --> <library path="lib/libpolygon_plugins"> <!-- type="插件类" base_class_type="基类" --> <class type="polygon_plugins::Triangle" base_class_type="polygon_base::RegularPolygon"> <!-- 描述信息 --> <description>This is a triangle plugin.</description> </class> <class type="polygon_plugins::Square" base_class_type="polygon_base::RegularPolygon"> <description>This is a square plugin.</description> </class> </library>
导出插件
package.xml文件中设置内容如下:
<export> <xxx plugin="${prefix}/polygon_plugins.xml" /> </export>
标签<xxx />的名称应与基类所属的功能包名称一致,plugin属性值为上一步中创建的xml文件。
编译后,可以调用rospack plugins --attrib=plugin xxx命令查看配置是否正常,如无异常,会返回 .xml 文件的完整路径,这意味着插件已经正确的集成到了ROS工具链。
使用插件
src 下新建c++文件:polygon_loader.cpp,内容如下:
//类加载器相关的头文件 #include <pluginlib/class_loader.h> #include <xxx/polygon_base.h> int main(int argc, char** argv) { //类加载器 -- 参数1:基类功能包名称 参数2:基类全限定名称 pluginlib::ClassLoader<polygon_base::RegularPolygon> poly_loader("xxx", "polygon_base::RegularPolygon"); try { //创建插件类实例 -- 参数:插件类全限定名称 boost::shared_ptr<polygon_base::RegularPolygon> triangle = poly_loader.createInstance("polygon_plugins::Triangle"); triangle->initialize(10.0); boost::shared_ptr<polygon_base::RegularPolygon> square = poly_loader.createInstance("polygon_plugins::Square"); square->initialize(10.0); ROS_INFO("Triangle area: %.2f", triangle->area()); ROS_INFO("Square area: %.2f", square->area()); } catch(pluginlib::PluginlibException& ex) { ROS_ERROR("The plugin failed to load for some reason. Error: %s", ex.what()); } return 0; }
执行
修改CMakeLists.txt文件,内容如下:
add_executable(polygon_loader src/polygon_loader.cpp) target_link_libraries(polygon_loader ${catkin_LIBRARIES})
Nodelet
概念
ROS通信是基于Node(节点)的,Node使用方便、易于扩展,可以满足ROS中大多数应用场景,但是也存在一些局限性,由于一个Node启动之后独占一根进程,不同Node之间数据交互其实是不同进程之间的数据交互,当传输类似于图片、点云的大容量数据时,会出现延时与阻塞的情况,比如:
现在需要编写一个相机驱动,在该驱动中有两个节点实现:其中节点A负责发布原始图像数据,节点B订阅原始图像数据并在图像上标注人脸。如果节点A与节点B仍按照之前实现,两个节点分别对应不同的进程,在两个进程之间传递容量可观图像数据,可能就会出现延时的情况,那么该如何优化呢?
ROS中给出的解决方案是:Nodelet,通过Nodelet可以将多个节点集成进一个进程。
nodelet软件包旨在提供在同一进程中运行多个算法(节点)的方式,不同算法之间通过传递指向数据的指针来代替了数据本身的传输(类似于编程传值与传址的区别),从而实现零成本的数据拷贝。
nodelet功能包的核心实现也是插件,是对插件的进一步封装:
- 不同算法被封装进插件类,可以像单独的节点一样运行;
- 在该功能包中提供插件类实现的基类:Nodelet;
- 并且提供了加载插件类的类加载器:NodeletLoader。
作用
应用于大容量数据传输的场景,提高节点间的数据交互效率,避免延时与阻塞。
流程说明
- 准备;
- 创建插件类并注册插件;
- 构建插件库;
- 使插件可用于ROS工具链;
- 执行。
准备
新建功能包,导入依赖: roscpp、nodelet;
创建插件类并注册插件
#include "nodelet/nodelet.h" #include "pluginlib/class_list_macros.h" #include "ros/ros.h" #include "std_msgs/Float64.h" namespace nodelet_demo_ns { class MyPlus: public nodelet::Nodelet { public: MyPlus(){ value = 0.0; } void onInit(){ //获取 NodeHandle ros::NodeHandle& nh = getPrivateNodeHandle(); //从参数服务器获取参数 nh.getParam("value",value); //创建发布与订阅对象 pub = nh.advertise<std_msgs::Float64>("out",100); sub = nh.subscribe<std_msgs::Float64>("in",100,&MyPlus::doCb,this); } //回调函数 void doCb(const std_msgs::Float64::ConstPtr& p){ double num = p->data; //数据处理 double result = num + value; std_msgs::Float64 r; r.data = result; //发布 pub.publish(r); } private: ros::Publisher pub; ros::Subscriber sub; double value; }; } PLUGINLIB_EXPORT_CLASS(nodelet_demo_ns::MyPlus,nodelet::Nodelet)
构建插件库
CMakeLists.txt配置如下:
... add_library(mynodeletlib src/myplus.cpp ) ... target_link_libraries(mynodeletlib ${catkin_LIBRARIES} )
编译后,会在 工作空间/devel/lib/先生成文件: libmynodeletlib.so。
使插件可用于ROS工具链
配置xml
新建 xml 文件,名称自定义(比如:my_plus.xml),内容如下:
<library path="lib/libmynodeletlib"> <class name="demo04_nodelet/MyPlus" type="nodelet_demo_ns::MyPlus" base_class_type="nodelet::Nodelet" > <description>hello</description> </class> </library>
导出插件
<export> <!-- Other tools can request additional information be placed here --> <nodelet plugin="${prefix}/my_plus.xml" /> </export>
执行
可以通过launch文件执行nodelet,示例内容如下:
<launch> <node pkg="nodelet" type="nodelet" name="my" args="manager" output="screen" /> <node pkg="nodelet" type="nodelet" name="p1" args="load demo04_nodelet/MyPlus my" output="screen"> <param name="value" value="100" /> <remap from="/p1/out" to="con" /> </node> <node pkg="nodelet" type="nodelet" name="p2" args="load demo04_nodelet/MyPlus my" output="screen"> <param name="value" value="-50" /> <remap from="/p2/in" to="con" /> </node> </launch>
运行launch文件,可以参考上一节方式向 p1发布数据,并订阅p2输出的数据,最终运行结果也与上一节类似。