机器人操作系统-ROS
ROS的发展历史
一切的起点-柳树车库
参考资料-ROS史话36篇-2.柳树街68号 机器人是我们这个星球出现的新物种。 —张新宇
2006 年,无比好奇的一群人走在一起,组建了一个机器人研究实验室:柳树车库(Willow Garage)。他们利用开源软件这一无比诱惑人的馅饼,骗取这个星球上千上万人加入到这个宏伟计划中。在机器人历史上,从来没有这样的经历,组织全球的力量去实现一个机器人梦想。 机器人操作系统(ROS)就是这一宏伟计划的一部分。 这是一条叫柳树路(WillowRoad)的林荫道,这片绿荫覆盖的区域住满了硅谷的商业精英和IT新贵们,也拥挤着追求创业梦想的年轻人。 从脸书公司(Facebook)的总部沿着这条柳树街,从东北的尽头往西南走,也就是朝着斯坦福大学的方向走,会先穿过101公路,然后左手边路过一个加油站,加油站后面是一个橡树公园,公园斜对面,也就是西面,有一个条胡同,叫纳什胡同(NashAve)。
沿纳什胡同走100米,在第一个路口,向左,拐到圣玛格丽塔胡同(SantaMargarita Ave)上,向前走150米,是圣玛格丽塔胡同232号,那里正是谷歌诞生的仓库。圣玛格丽塔胡同232号距离斯坦福大学的标志性建筑胡佛纪念塔只有约4公里。
还是沿着圣玛格丽塔胡同,纳什胡同返回到柳树街吧。朝着斯坦福方向前进800米,就能到达,柳树街68号。这是一处优雅的别墅,边上就是公园,小河环绕。别墅的房东主人是一个叫斯科特·哈森(ScottHassan)的中年人,硅谷的亿万富翁。
哈森年纪轻轻就成了亿万富翁。俗话说,钱是男人胸中胆,这一点不假。换句话讲,说的是,男人就怕有钱,有钱就想干大事。
第一件事,他慷慨地投资了谷歌,那时他的好朋友谢尔盖·布林和拉里·佩奇的谷歌刚刚起步。后来随着谷歌上市,斯科特·哈森又获得了第二桶资金。
第二件事,买了一套房产。其实,那时他也不知道该干啥事。很多事实在看着没意义,没挑战。他干脆就先在硅谷风险投资机构聚集的门洛·帕克(MenloPark)买了一个办公室,希望作为以后的办公空间。
这就是后来柳树车库的办公地址:柳树街68号。这个地址也是后来柳树车库(Willow Garage)机器人公司名字的来源。这个车库其实名不符实,不像很多早期的创业公司,如微软、苹果,他们确实是在车库里创业的,但是这个柳树街68号完全不是一个车库,而是一栋别墅。
在当时,市面上除了一些教育类的机器人外,只有iRobot公司销售的扫地机器人。哈森认为扫地机器人实在是太低端了,他的梦想是希望开发一个机器人助手,帮助打理个人生活。那才叫机器人。
哈森看好个人机器人助手有诸多原因:
首先,他认为“机器人能够提高人们的生活水平”。在参观了丰田的汽车工厂之后,他对丰田的工业机器人和自动化生产线印象非常深刻。未来在他脑海的轻舞飞扬:如果这些机器人走出工厂,走进家庭,世界会变成什么样?
在WillowGarage成立的时候,人们关注的多是制造业相关的工业机器人,非工业机器人市场非常之小,没人愿意投资这个领域,更不要说个人服务机器人领域了。另外,机器人相关的人才对专业要求比较高,需要懂硬件、软件、电子、管理各路人才。哈森发现没有很好的方法把这些人聚集在一起。出于使命感,他感觉“这事他自己应该能做”。
还有一个原因是,哈森的母亲是个机器人爱好者,对他产生了潜移默化的影响。
哈森希望打造一款家用服务机器人。他看到机器人背后巨大的市场,这就是他在柳树街68号创建的柳树车库的理由。
PR与PR2
车库创立的后,斯科特·哈森找到了史蒂夫·库辛斯。当史蒂夫·库辛斯正式加入柳树车库时,哈森正热衷于无人车和无人船的项目的研发。无论是哈森,还是库辛斯,都没有机器人背景和开发经验。
这时候斯坦福大学的吴恩达教授告诉他们一个正在进行的项目,就是埃里克·博格(EricBerger)和基南·威罗拜克(KeenanWyrobek)的PR1(Personal Robot个人机器人)项目。那时,博格和威罗拜克正在斯坦福大学Salisbury教授的机器人实验室读书做研究,同时也参与吴恩达教授的项目。
这两位就是后来柳树车库机器人公司主要产品PR2的缔造者,堪称“车库双雄”。
这两位学生的梦想,是打造一款个人机器人,包括全套硬件和软件。这样一个项目也是这两位的论文题目。为了做这样一个项目,他们预期要花4百万美金,他们希望有跟多的人帮助他们打造这样一个机器人。他们四处找投资,可有谁会相信两个学生,哪怕他们带着的是斯坦福的光环。
在四处奔走,资金没有着落时候,有一个叫乔安娜·霍夫曼(JoannaHoffman)的女人和他的丈夫给了他们第一笔捐助:5万美金。乔安娜·霍夫曼是苹果麦金塔电脑的核心成员,人称“麦金塔电脑之母”,是史蒂夫·乔布斯背后最重要的女人。就是这位乔安娜·霍夫曼的第一笔资助,启动了被称为PR1的机器人项目。乔安娜·霍夫曼期望两位博士生能用着5万美金,先做点东西,慢慢积累。正是这个初级版本的PR1居然成了埃里克·博格和基南·威罗拜克的招牌。
正是这个招牌,吸引了柳树街房东——斯科特·哈森的注意,家庭服务机器人与斯科特·哈森的设想是一致的。一开始,斯科特·哈森想找些硬件工程师和软件工程师,跟这两个小孩PK,看谁做得快。后来斯科特·哈森改变了策略,采取直接跟这两个斯坦福的学生合作的方式,后来也成功吸引他们加入了车库。车库把埃里克·博格和基南·威罗拜克在斯坦福做的机器人称为PR1,后来在车库打造的机器人称为PR2(个人机器人2代)。
ROS是PR2项目其中的软件部分,车库采用同时与斯坦福大学人工智能实验室吴恩达教授合作的形式,将STAIR项目中的系统软件Switchyard为基础,构建ROS,并采用开源软件方式,邀请世界上(其实一开始主要是美国)感兴趣的人能参与进来。
什么是STAIR项目? STAIR项目是吴恩达老师的带领下,希望通过各种现有的硬件和软件,通过集成到一个机器人上的方式,打造一款“个人机器人”助手的项目。
创业初期,车库主要目标是推动PR2机器人的开发和ROS开源软件的维护。ROS的贡献中,吴恩达教授的博士生摩根·奎格利起到了关键作用,被称为ROS之父。
什么是ROS?
在上面的介绍中我们已经知道了,ROS在最开始是作为PR2项目的软件部分,使用开源的方式,邀请世界上感兴趣的人参与进来。 那么,更具体的来说,ROS是什么呢?他是像我们现在手机上的QQ,微信这样的软件?还是像Windows,MacOS,Linux一样是一个完备的操作系统? 答案是都不是 我们介绍过,STAIR项目是吴恩达老师的带领下,希望通过各种现有的硬件和软件,通过集成到一个机器人上的方式,打造一款“个人机器人”助手的项目。 2007年,摩根·奎格利和吴恩达将STAIR的成果发表在IEEE 国际机器人与自动化会议上,文章的题目是《STAIR:Hardware and Software Architecture》,软件系统的名称是Switchyard。这个Switchyard就是ROS前身。 2009年摩根·奎格利、吴恩达和柳树车库机器人公司的工程师们,在当年的IEEE国际机器人与自动化会议上发表了《ROS: An Open-Source Robot Operating System》,正式向外界介绍ROS。
正如文章中说强调的: ROS is not an operating system in the traditional sense of process management and scheduling; rather, it provides a structured communications layer above the host operating systems of a heterogenous compute cluster. (译文:ROS不是传统意义上的操作系统,不是用于进程管理和调度,而是构建在其它操作系统之上的一种结构化的通讯层。)
因此我们可以给出一个具体的定义:ROS本质上其实就是用于快速搭建机器人的软件库(核心是通信)和工具集。
ROS中的文件系统
Prerequisites-准备工作
在这一节中我们通过ROS中的一个内置包ros-noetic-ros-tutorials去了解ROS的文件系统,因此需要确定电脑中是否有这个包,打开终端执行下面的命令,如果没有这个包就会下载。
sudo apt-get install ros-noetic-ros-tutorials
值得说明的是,在这一节中所下载的ros-noetic-ros-tutorials并不是一个单独的包,而是多个教程包的集合,因此在终端中补全代码时,并不会补全出一个ros-noetic-ros-tutorials包出来。
文件系统概念快速概述
总的来说,ROS中的文件系统包括两点,一个是包-Package,一个是清单-Manifests(package.xml)
- Packgae: 包是 ROS 代码的软件组织单元。每个包可以包含库、可执行文件、脚本或其他工件。
- Manifests: 清单是对包的描述。它用于定义包之间的依赖关系,并捕获有关包的元信息,如版本、维护者、许可证等…
文件系统中的工具(rospack, roscd, rosls, Tab completion)
代码被分布在包中进行管理,而使用常规的cd 与 ls命令在终端中寻找包显得十分繁琐,因此ROS推出了一系列的工具来帮助开发者快速定位包。
rospack
1.概述
dpkg 是 Debian 及其衍生系统(如 Ubuntu)的包管理器,用于安装、卸载和管理 .deb 软件包。在ROS中,部分软件包被打包为.deb文件进行分发 pkg-config 是一个帮助开发人员在编译时找到库的编译和链接标志的工具,它通常用于确定编译时需要的 CFLAGS 和 LDFLAGS,以便正确地编译和链接使用特定库的程序。
rospack是ROS包管理工具。Rospack部分是dpkg,部分是pkg-config。也就是说 rospack 工具在工作时,一部分功能依赖于 dpkg 来获取软件包的安装和管理信息,另一部分功能依赖于 pkg-config 来获取软件包的编译和链接信息。
rospack的主要功能是遍历ROS_ROOT和ROS_PACKAGE_PATH中的包,读取和解析每个包的manifest.xml,并为所有包组装一个完整的依赖树。
使用这个依赖树,rospack可以回答许多关于包及其依赖关系的查询。常见的查询包括:
- rospack find : 返回被查询包的路径
- rospack depends : 返回被查询包的依赖列表
- rospack depends-on : 返回一个列表,列表中的包是依赖于被查询包的。
- rospack export : 返回构建和链接包所必需的标志
rospack的命令是跨平台的,无论是那个版本的ROS都具有同样的功能。
2.用法
rospack工具实现了许多打印ROS包信息的命令。所有这些命令都将它们的结果打印到标准输出。任何错误或警告都到stderr。这种分离确保错误输出不会混淆将rospack作为子进程执行的程序,例如,恢复包的构建标志。
rospack -h
roscd
roscd属于rosbash的一部分,在介绍roscd之前我们先介绍rosbash。 rosbash实际上是ros的一个包,而并不是一个独立的bash程序,同样的,rosbash并不只对bash进行支持,他也对zsh和tcsh提供同样地支持,但坦白的来说,效果并不如bash一样明显以及好用。 rosbash包含一些有用的bash函数,并向大量基本ros实用程序添加了Tab补全功能。
roscd允许您使用包名、堆栈名或特殊位置来更改当前的目录。
用法
标准的语法格式是
roscd <package-or-stack>[/subdir]
例如
roscd roscpp
# or
roscd roscpp/include/ros
rosls
rosls同样是rosbash的一部分,rosls允许你像bash中常见的ls一样查看包的内容,而不必打出复杂的路径。 举个例子
rosls roscpp
# or
rosls roscpp/include/ros
Tab补全
最后我们来聊一下Tab补全,Tab补全实际上也是rosbash的一部分,tab补全类似bash终端的补全,用户不必打出完整的包名或者工具名,只需打出前几个即可。
创建ROS软件包
ROS软件包的结构
在创建一个软件包之前我们要先了解什么是软件包,在前面我们提到过,包是 ROS 代码的软件组织单元。每个包可以包含库、可执行文件、脚本或其他工件。 首先我们使用git工具,下载一个后面会用到的仿真仓库
git clone https://github.com/6-robot/wpr_simulation.git
cd wpr_simulation
ls
CMakeLists.txt LICENSE README.md advanced config include launch maps media meshes models package.xml rviz scripts src worlds
对于一个基本的ROS软件包,或者是我们新创建的软件包,所必备的文件或文件夹分别是
- CMakeLists.txt
- package.xml
- src 而其余的文件或者文件夹,都是因为后面的工作而添加的。
ROS的编译系统-Catkin
程序从写出来到运行经历了什么?
我们以C语言为例,C语言的源代码为.c后缀的源文件,在经过C语言编译器(如GCC)编译后,成为了可执行文件,在Linux下它的后缀通常是.out,而在Windows上通常是.exe 从.c到.out,这之间发生了什么?
高级语言的实现通常有两种方式,编译(比如C语言)和解释(比如Python语言)。
- 编译:把源文件(如.c)编译为机器语言目标程序(如.exe)后执行
- 解释:在目标机器上实现一个源文件的解释器,由这个解释器去直接解释执行源程序
通常我们认为,对于C语言的编译一共经历了以下四个步骤:
- 预处理:预处理用于将所有的#include头文件以及宏定义替换成其真正的内容,预处理之后得到的仍然是文本文件,但文件体积会大很多。
- 编译:将经过预处理之后的程序转换成特定汇编代码(assembly code)的过程。
- 汇编:汇编过程将上一步的汇编代码转换成机器码(machine code),这一步产生的文件叫做目标文件,是二进制格式(通常后缀名是.o)。
- 链接:链接过程将多个目标文以及所需的库文件(.so等)链接成最终的可执行文件(executable file)。
Make与CMake
在上一节的讨论中,我们可以将GCC这个编译器,抽象为源文件(.c)到可执行文件(.out/.exe)的桥梁,是GCC完成了这一转变。 通常我们对单个C语言源文件的编译是使用类似下面的这个命令
gcc hello.c
./a.out #a.out是默认编译出来的可执行文件的名字,你也可以通过对gcc追加编译参数来修改编译后程序的名字
通常情况下(除了你刚开始学习一门编程语言),我们在一个项目中不会一次只编译一个单独的文件,而是编译许多的文件,最后得到一个编译的结果。 在最简单的情况下,我们会这样对许多的文件进行编译
gcc file1.c file2.c feil3.c ……
然而一但文件变得特别多,同时分布在不同的文件夹中,每次这样手打就显得有一些效率低下了,于是Make应运而生,然而随着工程项目的发展,Make也开始不能满足人们的需要了,于是CMake开始展露头角。 Make与CMake都可以看作是GCC这类编译器的调度工具,用来简化对编译器命令的调度,提升工程项目的效率。
Make与CMake都是一种自动化构建工具,用于管理和自动化软件的编译过程
Catkin
ROS也是一个大型的工程软件项目,因此ROS也使用CMake这类的自动化构建工具进行构建,Cmake是make工具的生成器,他简化了编译构建的过程,使得人们可以更加方便的管理大型的工程。而ROS这种体量更大的工程项目,对Cmake进行了一定的扩展,形成了Catkin编译系统,catkin继承了Cmake的优点,同时与ROS紧密相连。 我们可以用下面这张图来理解这几个构建工具之间的关系
Catkin的特点
- Catkin是基于Cmake的编译构建系统,具有以下特点:
- Catkin沿用了包管理的传统结构,像rospack中的find_package(),pkg-config
- 扩展了Cmake:
- 软件包生成后无需安装即可使用
- 自动生成find_package()代码,pkg-config文件
- 解决了多个软件包之间的构建顺序问题
- 一个catkin的软件包必须要包含两个文件:
- package.xml: 包括了package的描述信息
- CMakeLists.txt: 构建package所需的CMake文件
Catkin工作空间
Catkin工作空间是创建、修改、编译catkin软件包的目录。catkin的工作空间,直观的形容就是一个仓库,里面装载着ROS的各种项目工程,便于系统组织管理调用。其实,工作空间就是一个文件夹,通常我们自己写的ROS代码通常就放在这个文件夹中。
初始化catkin工作空间
catkin的工作空间是建立在已有的文件夹上的,因此需要先创建一个文件夹,然后使用catkin_make
初始化工作空间,比如:
# 如果使用的不是容器环境,可以直接在根目录下创建,当然也可以自己选择一个喜欢的路径
mkdir -p ~/catkin_ws/src
cd ~/catkin_ws/
# 初始化工作空间
catkin_make
soure devel/setup.bash
使用Catkin进行编译
要使用catkin编译一个工程或者是软件包,只需要使用catkin_make
指令,不过值得注意的是,执行catkin_make
时需要位于工作空间目录下,在软件包的子目录下是会失败的。 现在我们来讨论在catkin_make
时, catkin是如何工作的:
- 首先catkin会在工作空间目录下(这里假定为~/catkin_ws)的src文件夹中递归的查找每一个ROS的package
- 我们已经知道一个ROS的package中都会有package.xml和CMakeLists.txt文件, catkin依据CMakeLists.txt文件生成makefiles文件, 存放在catkin_ws/build文件夹下
- 执行make流程,从makefiles文件进行编译,链接生成可执行文件,并存放在catkin_ws/devel文件夹下。
也就是说,catkin将cmake与make指令进行了二次的封装, 是一个可以一键(catkin_make)完成整个编译流程的工具。
CMakeLists.txt作用
CMakeLists.txt原本是Cmake编译系统的配置文件,用于指定如何构建一个项目。通常来说, CMakeLists.txt是项目构建过程的核心, 它告诉 CMake 如何构建项目中的每个目标,以及如何配置编译器和链接器。 由于catkin编译系统基本沿用了Cmake的编程风格, 并只是针对ROS工程添加了一些宏定义,因此catkin的CMakeLists.txt与Cmake的基本一致。
package.xml
package.xml也是一个catkin的package必备文件,它是这个软件包的描述文件,在较早的ROS版本(rosbuild编译系统)中,这个文件叫做manifest.xml,用于描述pacakge的基本信息。如果你在网上看到一些ROS项目里包含着manifest.xml,那么它多半是hydro版本之前的项目了。
使用catkin创建一个ROS的软件包
这里我们依旧假定你的工作空间是catkin_ws 创建一个ROS的package需要在catkin_ws/src路径下, 用到
catkin_create_pkg
命令, 语法如下:
catkin_create_pkg package_name depends
其中package_name是包名, depends是包的依赖, 一个ROS的软件包只能有一个包名, 但是可以有多个依赖。 通常一个ROS包常用的依赖有roscpp
,rospy
,std_msgs
,以此为例, 我们可以在catkin_ws/src下新建一个软件包, 名字就叫做beginner_tutorials
catkin_create_pkg beginner_tutorials roscpp rospy std_msgs
接着我们可以观察一下这个新建的包的结构
user@bad76c4f6f0a:/home/taolin/Documents/Docker-files/ros-noetic/catkin_ws/src/beginner_tutorials$ ls
CMakeLists.txt include package.xml src
其中include文件夹与src文件夹,都是空的,但是catkin_create_pkg
帮你完成了软件包的初始化,填充好了CMakeLists.txt和package.xml,并且将依赖项填进了这两个文件中。
更新包路径
新建完包以后,需要回到工作空间下,执行catkin_make
编译整个工作空间,然后
source /devel/setup.bash
这样更新完包路径后,可以使用roscd, rosls等工具,通过包名进行索引。
ROS中的通信架构
Node 与 Master
Node
在ROS中,最小的进程单元就是Node,通常我们将它翻译为节点。一个软件包中可以有多个可执行文件,可执行文件在运行后就成为了一个进程, 在ROS中这个进程就叫做Node(节点)。 从程序的角度来说, Node就是一个可执行文件(通常是由catkin从cpp源代码编译而来,或者是python文件)被执行,加载到了内存之中。 从功能的角度来说,由于机器人的模块非常复杂,通常开发者不会将所有的功能集合在一个节点上(就像鸡蛋往往不会装在一个篮子里),而是采用分布式的方法,用一个Node负责一个单独的功能,这样以模块化的方式组织代码,既可以提高系统的稳定性,也让Debug变得容易。
Master
就像上文提到的那样,由于机器人的模块非常复杂,对应的节点也非常的多,在机器人工作时,往往会运行众多的node,负责感知世界、控制运动、决策和计算等功能。 那么如何进行合理的调配,管理这些Node呢,ROS提出了Master-节点管理器。 Master在整个网络通信架构中相当于是管理中心,管理者所有的节点。如果将Node比作是摆摊的小贩,那么Master就是城管。 Node首先需要在Master处进行注册,之后Master会将Node纳入到整个ROS程序之中,各个Node之间的通信也是由Master进行牵线,才能两两进行点对点的通信。因此在ROS程序启动时,需要先启动Master,才能启动其他的Node。
ROS中的通信方式
ROS的通信方式是ROS最为核心的概念,ROS系统的精髓就在于它提供的通信架构。ROS的通信方式有以下四种:
- Topic 话题
- Service 服务
- Parameter Service 参数服务器
- Action 动作
Topic
在ROS的通信中,Topic是常用的一种,对于实时性,周期性的消息,利用Topic来传递是最佳的选择。 Topic是一种点对点的通信方式,这里的点指得就是Node,也就是说Node和Node通过Topic的方式来传递信息。 Topic注册到ROS系统中要经历以下几个初始化的过程:首先,Publisher节点(也就是发布者)和Subscriber节点(也就是订阅者)都要到节点管理器(也就是master)进行注册,然后Publisher节点会发布Topic,Subscriber节点在包管理器master的指挥下订阅该Topic,从而建立起Subscriber和Publisher的通信,但要值得注意的是,这个通信是单向的。信息只能由Publisher节点流向Subscriber节点。 其示意结构图如下: Subscriber节点在接受到消息时会进行处理,一般这个处理的过程叫做回调(Callback),所谓回调提前写好一个处理函数,每当有消息到来的时候就会触发这个处理函数对消息进行处理。比如单片机中的串口中断函数,会对接收到的消息进行处理。
异步通信
参考上图,我们以摄像头画面的发布、处理、显示为例讲讲Topic通信的流程,在这个图中,调用机器人上的摄像头进行拍照的程序是一个Node(记作Node1),当然他也是一个Publisher发布者,在程序启动后,他开始发布Topic,比如它发布了一个Topic叫做/camera_rgb,是rgb颜色信息,即采集到的彩色图像。同时Node2是图像的处理程序,他订阅了/camera_rgb这个Topic,经过包管理器Master的介绍,他就能建立起和Node1的链接。 在整个通信的过程中,Node1每次发布完图像的数据之后就执行其他的动作去了,而并不会关心Node2是否接受到了图片,接受过程中是否出现了问题。Node2每次接受到了图片就去进行图像处理相关的工作,也不会关心Node1是以怎样的方式将图片数据发布了出来。因此我们可以说Node1和Node2对彼此通信的流程并不关心,不存在协同工作。我们称这样的通信方式是异步的。 ROS是一种分布式的架构,一个Topic可以被多个Node同时发布,也可以被多个Node同时订阅。
Topic Message
由于Topic是一种异步通信的通信方式,这就要求发布方与订阅方有规定好的通信格式,这种通信格式就叫做Topic Message。Message按照定义解释来说就是Topic内容的数据类型,也称之为Topic的格式标准。
常见的Topic Message
Vector3.msg
#文件位置:geometry_msgs/Vector3.msg
float64 x
float64 y
float64 z
Accel.msg
#定义加速度项,包括线性加速度和角加速度
#文件位置:geometry_msgs/Accel.msg
Vector3 linear
Vector3 angular
Header.msg
#定义数据的参考时间和参考坐标
#文件位置:std_msgs/Header.msg
uint32 seq #数据ID
time stamp #数据时间戳
string frame_id #数据的参考坐标系
Service
什么是Service
在Topic一节中我们介绍了,Topic是一种单向异步,用于实时性,周期性消息的通信方式。然而有些时候单向的通信满足不了通信要求,比如当一些节点只是临时而非周期性的需要某些数据,如果用topic通信方式时就会消耗大量不必要的系统资源,造成系统的低效率高功耗。 这种情况下,就需要有另外一种请求-查询式的通信模型。这节我们来介绍ROS通信中的另一种通信方式——Service(服务)。
Service的工作原理
为了补充Topic通信的不足,Service在通信模型上与Topic做了区别。 Service是一种双向同步通信,它不仅可以发送消息,同时还会有反馈。所以Service分为请求方(Clinet)和应答方/服务提供方(Server)。请求方(Client)发送一个Request,然后等待服务提供方(Server)处理,反馈一个Reply。通过这样一个类似”请求-应答”机制完成整个服务的通信。 这种通信的示意图如下: Node B是Server方,提供了一个服务接口/Service,通常我们会用/String类型来指定Service的名称。Node A向Node B发起了请求(Request),经过处理后的到了反馈(Reply) Service是一种同步通信方式,也就是说,Node A 在向Node B提交请求后会在原地等待Node B的反馈,直到得到了这个反馈才回去执行其他的流程。Node A在等待的过程是一种阻塞性的状态,这样的通信不会有频繁的消息传递,也没有冲突于高资源的占用,简单且高效。
Parameter Service
什么是Parameter Service
Parameter Service(后面我们称它为参数服务器),是一个共享的,多变量的字典(类比Python),可以通过网络IP进行访问。Node使用参数服务器在运行时存储和检索参数。 由于参数服务器不是为了高性能所设计的,因此它适合用于静态的非二进制数据,例如初始化和配置参数。同时参数服务器是全局可见的,这样工具可以很容易的检查系统的配置状况并根据需要作出修改。 参数服务器是使用XMLRPC实现的,并在ROS Master中运行,这意味着可以通过普通XMLRPC库访问它的API。
Parameter
Parameter(后面称 参数)使用正常的ROS命名约定来进行命名,这意味着ROS参数具有与主题和节点使用的名称空间相匹配的层次结构。这个层次结构是为了防止参数名冲突。分层方案还允许单独访问参数或作为树访问参数。
ROS命名约定
- 全局唯一性:ROS中的每个命名元素(Node,Topic,Service,Parameter)都应该具有全局唯一性,以避免冲突。
- 层次结构:ROS命名约定使用斜杠(/)来表示层次结构,类似于文件系统中的路径。例如,/my_robot/laser_sensor 表示一个名为 laser_sensor 的节点,它是 my_robot 命名空间的一部分。 例如下面的参数:
/camera/left/name: leftcamera
/camera/left/exposure: 1
/camera/right/name: rightcamera
/camera/right/exposure: 1.1
/camera/left/name参数的值为leftcamera。你也可以得到/camera/left的值,它是字典
name: leftcamera
exposure: 1
你也可以得到/camera的值,它有一个参数树的字典的字典表示:
left: { name: leftcamera, exposure: 1 }
right: { name: rightcamera, exposure: 1.1 }
Parameter Types
参数服务器使用XMLRPC数据类型作为参数值,包括:
- 32-bit integers
- booleans
- strings
- doubles
- iso8601 dates
- lists
- base64-encoded binary data
你也可以在参数服务器上存储字典(即结构),尽管它们有特殊的含义。参数服务器将ROS名称空间表示为字典。例如,假设您设置了以下三个参数:
/gains/P = 10.0
/gains/I = 1.0
/gains/D = 0.1
您可以分别读取它们,即检索/gain /P将返回10.0,或者您可以检索/gain,这将返回一个字典:
{ 'P': 10.0, 'I': 1.0, 'D' : 0.1 }
Private Parameters
ROS命名约定将name称为私有名称。这些私有名称主要用于特定于单个节点的参数。在节点的名称前加上前缀,以便将其用作半私有名称空间——它们仍然可以从系统的其他部分访问,但是它们通常受到保护,不会发生意外的名称冲突。 您可以使用重映射参数在命令行中指定节点参数,方法是将波浪形~更改为下划线_,例如:
rosrun rospy_tutorials talker _param:=1.0
这会使talker节点的~param参数为1.0
Parameter Tools - rosparam
rosparam命令行工具允许您使用YAML语法在参数服务器上查询和设置参数。
Python API
ROS参数服务器可以存储字符串、整数、浮点数、布尔值、列表、字典、iso8601日期和base64编码的数据。字典必须有字符串键。
rospy的API是Python内置xmlrpclib库的一个薄包装,因此您应该参考该文档了解如何正确编码值。在大多数情况下(布尔值、整型、浮点数、字符串、数组、字典),您可以按原样使用值。 注意:参数服务器方法不是线程安全的,所以如果你在多个线程中使用它们,你必须锁定适当的。
Getting parameters
rospy.get_param(param_name)
从参数服务器获取param_name的值。如果没有设置参数,您可以选择传递一个默认值来使用。
在 ROS (Robot Operating System) 中,使用 rospy.get_param() 函数获取参数时,如果指定的参数不存在,函数将抛出一个 ROSException。 为了避免这种情况,rospy.get_param() 允许您传递一个默认值作为第二个参数。如果请求的参数不存在,rospy.get_param() 将返回这个默认值,而不是抛出异常。
param_name相对于节点的命名空间进行解析。如果使用get_param()获取命名空间,则返回一个字典,其中的键等于该命名空间中的参数值。如果未设置参数则引发KeyError。 示例如下:
import rospy
def main():
rospy.init_node('my_node')
# 获取全局参数
global_name = rospy.get_param("/global_name")
# 获取相对参数
relative_name = rospy.get_param("relative_name")
# 获取私有参数
private_param = rospy.get_param('~private_name')
# 获取具有默认值的参数
default_param = rospy.get_param('default_param', 'default_value')
# 获取参数组
gains = rospy.get_param('gains', {'p': 1.0, 'i': 0.0, 'd': 0.0})
p, i, d = gains['p'], gains['i'], gains['d']
if __name__ == '__main__':
main()
Setting parameters
如前所述,您可以设置参数来存储字符串、整数、浮点数、布尔值、列表和字典。还可以使用iso8601日期和base64编码的数据,但不建议使用这些类型,因为它们在其他ROS客户机库中不常用。字典必须有字符串键,因为它们被认为是命名空间(参见下面的例子)。
rospy.set_param(param_name, param_value)
通过set_param在参数服务器上设置参数,参数的名称相对于节点的命名空间进行解析。 示例如下:
# Using rospy and raw python objects
rospy.set_param('a_string', 'baz')
rospy.set_param('~private_int', 2)
rospy.set_param('list_of_floats', [1., 2., 3., 4.])
rospy.set_param('bool_True', True)
rospy.set_param('gains', {'p': 1, 'i': 2, 'd': 3})
# Using rosparam and yaml strings
rosparam.set_param('a_string', 'baz')
rosparam.set_param('~private_int', '2')
rosparam.set_param('list_of_floats', "[1., 2., 3., 4.]")
rosparam.set_param('bool_True', "true")
rosparam.set_param('gains', "{'p': 1, 'i': 2, 'd': 3}")
rospy.get_param('gains/p') #should return 1
Parameter existence
如果您希望节省传输参数值的网络带宽,或者如果您不希望与get_param和delete_param一起使用try/except块,则检测参数是否存在是有用的。
rospy.has_param(param_name)
rospy.has_param() 函数用于检查 ROS 参数服务器上是否存在某个参数。如果参数存在,它返回 True;如果不存在,返回 False。
Deleting parameters
rospy.delete_param(param_name)
从参数服务器中删除一个参数,可以使用 rospy.delete_param() 函数。这个函数允许您指定要删除的参数名称,并且参数必须已经设置在参数服务器上(如果参数未设置,会抛出 KeyError)。参数名称是相对于节点的命名空间解析的。
Retrieve list of parameter names
rospy.get_param_names()
rospy.get_param_names() 用于获取当前 ROS 参数服务器上所有参数的名称。这个函数返回一个字符串列表,其中包含了参数服务器上所有参数的完整名称。
Searching for parameter keys
rospy.search_param(param_name)
通过使用rospy.search_param(param_name),我们可以以向上搜索的方式在参数服务器上查找param_name参数。向上搜索指的是在查找param_name参数时会从私有命名空间向上开始查找,直到找到匹配的参数。 为了有效的使用rospy.search_param(param_name),传入的应当是相对名称而不是全局或者是私有名称。 示例如下:
param_name = rospy.search_param('global_example')
v = rospy.get_param(param_name)
当您在节点 /foo/bar 中使用 rospy.search_param(‘global_example’) 函数时,它会按照以下顺序尝试查找名为 global_example 的参数:
私有命名空间:首先在当前节点的私有命名空间下搜索参数。对于节点 /foo/bar,这意味着它会查找参数 /foo/bar/global_example。
节点命名空间:如果私有命名空间中没有找到,它会在节点的命名空间下搜索参数。对于节点 /foo/bar,这意味着它会查找参数 /foo/global_example。
全局命名空间:如果节点命名空间中也没有找到,它会在全局命名空间下搜索参数,即直接查找参数 /global_example。
rospy.search_param 函数会返回第一个找到的参数的完整名称,或者如果所有命名空间中都没有找到,则返回 None。
C++
通过上面的描述我们已经知道了ROS参数服务器可以储存字符串、整数、浮点数、布尔值、列表、字典、iso8601日期和base64编码的数据。(当然后两者不建议使用,因为真的很不常用) roscpp的参数API支持所有这些,尽管它只易于使用字符串、整数、浮点数和布尔值。对其他选项的支持是使用XmlRpc::XmlRpcValue类完成的 roscpp有两个不同的参数api:位于ros::param命名空间中的“base”版本,以及通过ros::NodeHandle接口调用的“handle”版本。这两个版本将在下面解释每个操作。
Getting Parameters
handle版本
ros::NodeHandle::getParam()
从参数服务器获取一个值。每个版本都支持字符串、整数、双精度、布尔值和XmlRpc::XmlRpcValues。如果参数不存在或类型不正确,则返回False。还有一个版本与Python中一样,允许您传递一个默认值作为第二个参数。如果请求的参数不存在,rospy.get_param() 将返回这个默认值,而不是抛出异常。 通过NodeHandle版本检索的参数相对于NodeHandle的名称空间进行解析。 示例如下:
ros::NodeHandle nh;
std::string global_name, relative_name, default_param;
if (nh.getParam("/global_name", global_name))
{
...
}
if (nh.getParam("relative_name", relative_name))
{
...
}
// 另一个与Python中一样返回默认值的版本
nh.param<std::string>("default_param", default_param, "default_value");
base版本
ros::param::get()
通过“base”版本检索的参数相对于节点的名称空间进行解析。
std::string global_name, relative_name, default_param;
if (ros::param::get("/global_name", global_name))
{
...
}
if (ros::param::get("relative_name", relative_name))
{
...
}
// 另一个与Python中一样返回默认值的版本
ros::param::param<std::string>("default_param", default_param, "default_value");
Setting Parameters
与获取参数类似,每个版本都支持字符串、整数、双精度、布尔值和XmlRpc::XmlRpcValues。 handle版本
ros::NodeHandle::setParam()
通过NodeHandle版本检索的参数相对于NodeHandle的命名空间进行解析。 示例如下:
ros::NodeHandle nh;
nh.setParam("/global_param", 5);
nh.setParam("relative_param", "my_string");
nh.setParam("bool_param", false);
base版本 通过base版本检索的参数相对于Mode的命名空间进行解析。 示例如下:
ros::param::set("/global_param", 5);
ros::param::set("relative_param", "my_string");
ros::param::set("bool_param", false);
Checking Parameter Existence
handle版本
ros::NodeHandle::hasParam()
示例
ros::NodeHandle nh;
if (nh.hasParam("my_param"))
{
...
}
base版本
ros::param::has()
示例
if (ros::param::has("my_param"))
{
...
}
Deleting Parameters
handle版本
ros::NodeHandle::deleteParam()
示例
ros::NodeHandle nh;
nh.deleteParam("my_param");
base版本
ros::param::del()
示例
ros::param::del("my_param");
Accessing Private Parameters
根据使用接口版本的不同,访问私有参数的方式也有所不同。在handle版本中,你必须创建一个新的ros::NodeHandle,将私有命名空间当作这个handle的命名空间 handle版本 示例
ros::NodeHandle nh("~");
std::string param;
nh.getParam("private_name", param);
base版本 而在”base”接口中,你可以用相同的符号来访问私有参数,例如: 示例
std::string param;
ros::param::get("~private_name", param);
Searching for Parameter Keys
在Python一节中我们已经详细描述过了,这里就不再赘述。 handle版本
ros::NodeHandle::searchParam()
示例
std::string key;
if (nh.searchParam("bar", key))
{
std::string val;
nh.getParam(key, val);
}
base版本
ros::param::search()
示例
std::string key;
if (ros::param::search("bar", key))
{
std::string val;
ros::param::get(key, val);
}
Retrieve list of parameter names
您可以以字符串的std::vector的形式获得现有参数名称的列表。
您可以使用ros::NodeHandle::getParamNames接口或ros::param::search接口获取vectors(容器):
这里的 “vectors” 是用来描述 ros::NodeHandle::getParamNames 接口或 ros::param::search 函数返回的数据类型。 ros::NodeHandle::getParamNames 和 ros::param::search 都用于获取 ROS 参数服务器上的参数名称,它们返回的是一个包含所有参数名称的 std::vector 类型的对象。 这个 std::vector 中的每个元素都是一个字符串,代表参数服务器上的一个参数名称。 handle版本 示例
// Create a ROS node handle
ros::NodeHandle nh;
std::vector<std::string> keys;
nh.getParamNames(keys)
base版本 示例
std::vector<std::string> keys;
ros::param::search(keys)
Retrieving Lists
您可以获取和设置原语和字符串的列表和字典,作为std::vector和std::map容器,具有以下模板值类型:
- bool
- int
- float
- double
- string
例如,您可以使用ros::NodeHandle::getParam / ros::NodeHandle::setParam接口或ros::param::get / ros::param::set接口获取向量和映射:
// Create a ROS node handle
ros::NodeHandle nh;
// Construct a map of strings
std::map<std::string,std::string> map_s, map_s2;
map_s["a"] = "foo";
map_s["b"] = "bar";
map_s["c"] = "baz";
// Set and get a map of strings
nh.setParam("my_string_map", map_s);
nh.getParam("my_string_map", map_s2);
// Sum a list of doubles from the parameter server
std::vector<double> my_double_list;
double sum = 0;
nh.getParam("my_double_list", my_double_list);
for(unsigned i=0; i < my_double_list.size(); i++) {
sum += my_double_list[i];
}
Action 动作
前面章节学习了Tpoic(话题)、Service(服务)、Parameter(参数)。
话题适用于节点间单向的频繁的数据传输,服务则适用于节点间双向的数据传递,而参数则用于动态调整节点的设置
假设我们通过服务发送一个目标点给机器人,让机器人移动到该点,我们可能会遇到这样的问题:
- 你不知道机器人有没有处理移动到目标点的请求(不能确认服务端接收并处理目标)
- 假设机器人收到了请求,你不知道机器人此时的位置和距离目标点的距离(没有反馈)
- 假设机器人移动一半,你想让机器人停下来,也没有办法通知机器人
上面的场景在机器人控制当中经常出现,比如控制导航程序,控制机械臂运动,控制小乌龟旋转等,很显然单个话题和服务不能满足我们的使用,因此ROS针对控制这一场景,基于原有的话题和服务,设计了动作(Action)这一通信方式来解决这一问题。
Action的通信原理
- Action的工作原理是client-server模式,也是一个双向的通信模式
- 通信双方在ROS Action Protocol下通过消息进行数据的交流通信
- client和server为用户提供一个简单的API在客户端(Client)请求目标或在服务器端(Service)通过函数调用和回调来执行目标
- Action server:向ROS系统广播指定action的Node,其它Node可以向该Node发出action目标请求
- Action client:发出action目标请求的Node
下图是ROS Node Action通信的原理示意图,我们可以看到通信双方通过ROS Action Protocol(Action通信协议)实现通信。Action的通信协议建立在ROS的Message(消息)上。
Action的组成部分
Action通常认为有三大组成部分:目标(goal),反馈(feedback),结果(result)
- 目标:机器人执行一个动作,应该有明确的移动目标信息,包括一些参数的设定,如方向、角度、速度等等。从而使机器人完成动作任务。 目标在完成之前可被占用或被取消。 目标可处于ACTIVE、SUCCEEDED、ABORTED等不同的状态。
- 反馈:在动作进行的过程中,应该有实时的状态信息反馈给客户端,告诉客户端动作完成的状态,可以使客户端作出准确的判断去修正命令。
- 结果:当动作完成时,服务端把运动的结果数据发送给客户端,使客户端得到本次动作的全部信息,例如机器人的运动时长、最终位姿等。
我们可以认为Action是Topic和Service的组合体,通常一个Action由三个Service和两个Topic组成,如下图:
快速上手Action
Coding for Node
Client Library
在正式开始ROS的编程训练之前,我想先介绍一下ROS中的Client Library,ROS为机器人开发者们提供了不同语言的编程接口,比如C++接口roscpp,Python接口rospy,Java接口rosjava。 尽管编程语言不同,但这些接口都可以用来实现ros中的通信功能 目前ros支持的Client Library包括
Client Library | Introduction |
---|---|
roscpp | ROS的C++库,是目前最广泛应用的ROS客户端库,执行效率高 |
rospy | ROS的Python库,开发效率高,通常用在对运行时间没有太大要求的场合,例如配置、初始化等操作 |
roslisp | ROS的LISP库 |
roscs | Mono/.NET.库,可用任何Mono/.NET语言,包括C#,Iron Python, Iron Ruby等 |
rosgo | ROS Go语言库 |
rosjava | ROS Java语言库 |
rosnodejs | Javascript客户端库 |
也许未来还有对其他语言的支持 |
但坦白的来说,目前只有roscpp和rospy最常用也最能正常使用,其他的语言接口基本上都是处于测试版本。 从开发者的角度来看,一个客户端库,至少需要做到Master注册,名称注册,消息收发功能,才能给开发者提供ROS通信架构进行配置的方法。
roscpp
roscpp位于/opt/ros/noetic下(noetic是ros的版本号),用C++实现了ROS通信。 roscpp的主要部分包括
- ros::init(): 解析传入的ROS节点参数, 是创建Node时最先用到的函数
- ros::NodeHandle: Node与topic,service,param等交互的公共接口
- ros::master: 包含从master处查询信息的函数
- ros::this_node: 包含查询这个进程的函数
- ros::service: 包含查询服务的函数
- ros::param: 包含查询参数服务器的函数
- ros::names: 包含处理计算图资源的函数
从功能上,roscpp中包含的函数可以分为以下几类:
- Initialization and Shutdown 初始化与关闭
- Topics 话题
- Services 服务
- Parameter Server 参数服务器
- Timers 定时器
- NodeHandle 节点句柄
- Callbacks and Spinning 回调和自旋(或者翻译叫轮询?)
- Logging 日志
- Names and Node Information 名称管理
- Time 时钟
- Exception 异常