知识回顾

-上周我们介绍了Apollo控制技术——控制器的类型。主要介绍控制器的类型,大致分为三类,分别是开环控制、前馈环控制和后馈环控制。

其中详细地讲解了前馈环控制器的分类以及控制策略,控制策略主要介绍了三种,分别是Optimal Control(优先控制)、Adaptive Control(自适应控制)、Robust Control(鲁棒性控制)等。

下面我们对Apollo ROS做一个详细的介绍,让我们一起进入我们的进阶课堂吧。

一、Apollo ROS介绍——背景

自动驾驶系统包括障碍物检测、行为决策、路径规划等一系列复杂的工程模块,同时还要支持激光雷达、相机、GPS等一系列传感器的实时数据收集和实时处理。如何将这些功能模块相互独立又相互交互集成一起,构建成一个稳定的自动驾驶系统是一个巨大的挑战,也是自动驾驶计算框架所承载的基本功能。

首先自动驾驶系统还处在一个快速发展的阶段,算法和整体方案还在持续迭代优化。使用框架开发,可以将更多的精力放在算法模块的迭代上。开发者不需要关心具体配置管理、部署运行、底层通信等功能。

其次是感知、定位、决策控制等模块各自承载了一部分独立功能,相互之间有一定的数据依赖。框架能够在开发阶段减少各个模块之间的耦合,在运行阶段可以将各个模块串联起来。

最后是自动驾驶涉及大量的图像点云处理算法,对于可视化和调试工具有很大的需求。比如调试过程中,障碍物检测需要看Detection的那个框准不准;路径规划需要看自动驾驶算法规划的路径是否符合当前的一些决策行为;定位也需要看车辆当前是否在准确的车道内。


上图是一个ROS的基本框架,自动驾驶底层通信框架选择ROS,主要有三个方面的原因:

  1. ROS是一个比较强大、灵活的机器人编程框架。从软件架构的层面来说,它是一个基于消息传递的分布式多进程框架,很早就被机器人行业广泛使用。很多著名的机器人开发框架,如基于四元数的坐标转换、3D点预处理驱动、定位算法、SLAM等都是基于ROS开发的开源的现成方案。
  2. ROS基于消息机制,开发者可以根据功能把软件拆分成独立的子模块,子模块通过不断的组合能够建立起比较复杂的系统来完成复杂的功能。
  3. ROS是学术界广泛使用的一个框架,对实验各种新算法提供了一些支持。

基于以上三点,我们在初期选择ROS作为自动驾驶底层框架,用于快速验证顶层算法和技术方案。

二、Apollo ROS概述

ROS提供一些标准操作系统服务,例如硬件抽象、底层设备控制、常用功能实现、进程间消息以及数据包管理。ROS是基于一种图状架构,从而不同节点的进程能接受、发布、聚合各种信息(如传感、控制、状态、规划等)。

1.ROS的历史


ROS是2007年在斯坦福大学里面的一个实验室学生开发出来的一套机器人通用的一个框架,2013年被纳入OSRF机构统一管理,如今被很多公司和大学的研究所广泛地使用到一些科研项目中。

2.ROS的特征

ROS有5个比较明显的特征如下:

1.点对点:两个Node之间进行消息通讯是一个点对点的行为。
2.它支持分布式:在部署多机之间的消息通讯时,ROS提供了一个天然的支持。
3.它是跨语言,它并不关注每个节点之间是用什么语言来写的。你只需要按照ROS提供的一些接口完成消息的订阅和分发即可以完成一个消息之间的通信。
4.它是一个轻量级的ROS程序,用户只需要关注自己核心模块的算法逻辑,不需要关注底层是如何通信、如何断开通信、如何进行Service 和Param之间的一些交互的。
5.它是一个开源的框架,大家都可以往ROS里面贡献自己的一些想法和代码。

3.ROS的核心概念

松耦合:ROS是一个松耦合的框架,松耦合就是各个节点之间的通信是一个解耦合的关系。

节点:一个算法模块,比如自动驾驶系统里面的感知模块、定位模块、决策模块或者控制模块,这些模块就是一个简单的算法集合,在ROS里面被称为一个节点。

节点管理器:在ROS里面被定义为Master,用来集中式管理各个独立的、松耦合、无序节点之间的逻辑关系,它是轻量级的介入,当各个节点启动完成以后,他们在通信连接完成之前起到中转也就是类似于交换机的作用。

Topic:两个节点之间的通信主题。Topic内部使用的数据格式是Message。Message是一系统简单的数据类型或者是一些自定义的复杂数据类型,所组装成的一个描述文件。

以上几个概念之间的相互关系,如下图所示:


感知模块Perception,感知车辆周围的一些障碍物信息,用CNN或者RNN算法将障碍物信息提取出来,即Obstacle。再将这些信息输出给下游Planning节点。这两个节点之间的通信连接就需要Roscore,即节点管理器。

Perception、Planning在启动的时候没有先后关系,这是松耦合的一个具体体现。Perception先启动并向Roscore发送一个注册信息,同时会订阅名为Obstacle的Topic;Planning节点启动后也向Roscore发送一个注册信息,同时会订阅名为Obstacle的Topic;在这种情况下,Roscore会发送一个通知信息给Planning,在它发送注册信息之前已经有一个节点启动了。此时Planning会向Perception发送消息请求通信连接,Planning收到消息之后会在Planning和Perception两个节点中间建立一个实时通信链路。当通信链路建立之后,Roscore的功能就暂时完成了。

所以,松耦合在此有两种体现:

1.Perception和Planning两者之间的启动没有先后关系。
2.当通信链路建立之后,Roscore的功能就暂时完成了。

4.ROS的实践

步骤 过程
第一部分 启动Roscore
第二部分 启动一个简单的Talker程序
第三部分 通过命令查看Talker node
第四部分 启动一个Listener节点
第五部分 再次通过命令查看Node

这段内容比较考验实践操作能力,所以先不做过多讲解,等我实践操作过后,会对该段内容做一个补充。

5.ROS的Catkin编译系统

ROS是基于Cmake编写的Catkin编译系统。建立一个工程包,在ROS里面写一个节点,通过Catkin create 可以简单创建一个文件夹,这个文件夹里面会预先设置一些文件目录,通过Catkin build编译建立软件包的过程。Catkin build执行之后,里面会多两个文件夹:DEVEL、BUILD 。BUILD是编译中间过程产生的文件。编译完成之后,通过Source devel下面的Setup bash就可以把自己编写的节点程序给Source到ROS的环境里面,然后去执行我们节点里面的一些基本功能。


以上是三个比较重要的文件夹,第一个是SRC用来放源文件的一些目录;第二个是BUILD,第三个是DEVEL,这两个是在Catkin build的过程当中产生的临时文件夹。想重编译的话可以直接Catkin build,如果环境里面有一些冲突,可以通过Catkin clean 简单的去把编译产生的临时文件和之前的一些产出文件直接清除掉。

Catkin config指定了命令行编译的一些方式,这些方式可以在Cmakelists里面进行编写。Cmakelists里面指定了这个文件编译过程当中所依赖的一些库、产出的一些可执行文件和这些可执行文件链接了一些什么库,Cmakelists里面都有一些很清晰的定义。


在启动节点的时候使用了Roslaunch,Roslaunch是一个Shell脚本文件,Shell脚本文件里面根据语言定义的一些Xml格式去找到运行的一系列节点所在的位置然后执行它。它的执行格式是前面加上Package Name,后面加上实际的Launch文件。

6.ROS的仿真功能Gazebo


这个是Gazebo的Simulator仿真工具。我们在实际进行开发,不管是机器人还是自动驾驶相关的一些具体功能的时候,我们不可能就是开发一个功能然后到实体的机器人或者是自动驾驶的汽车上去进行模拟实验。ROS提供了仿真功能Gazebo,我们定义的节点在里面是实体的存在,通过控制一些参数和变量去模拟他们之间的一些交互,去验证算法在实际的运行中是否按预期进行表现。

三、Apollo ROS原理—1

ROS在开发过程中,基于功能把整个自动驾驶系统分成多个模块,每个模块负责自己消息的接收、处理、发布。当模块需要联调时,通过框架可以把各个模块快速的集成到一起。

1.ROS的不足


ROS是很多大学或者实验室进行探索性项目实验所采用的基本框架,但在实际的自动驾驶工程化需求面前,还有很多明显不足。

大数据传输性能瓶颈:因为在实际汽车驾驶过程中,数据处理量非常大,将雷达和相机等数据相加之后每10HZ就会产生200M的数据。所以传输起来很慢,有时延,这是非常危险的。

单中心的网络存在单点风险:中心化的网络存在明显的单点风险,整个ROS虽然是一个松耦合的架构,它包含一个节点管理器,节点管理器介入的时候,只是在节点建立通信之前有一个简单的拓扑映射,这种关系虽说极大程度释放了各个节点之间开发的耦合,但同时也带来了比较大的风险。如果Roscore存在一些故障退出,而节点之间使用了需要不定时的交互方式,像Service 、Parem进行数据交互的时候就会存在一定的风险。如果是分布式系统, Roscore只存在于一台机器上,Roscore如果出现故障,两台机器之间通信就处于一个不可信的状态。

数据格式缺乏后向兼容:ROS是基于Message的分发和订阅的消息通讯框架,使用Message需要提前设置Message包含哪些类型的数据。把这个模块放到一个更复杂的系统里面的时候,要格外注意Message之间的数据兼容。
我们根据实际的场景需求,在定义的Obstacle信息里面加一段文字,那么相应的下游所有订阅此Obstacle的节点都要去做对应的适配,同时基于之前的Message所录制的一些实验数据,想在新的框架下使用也都需要一个批量的转化。ROS现有的数据格式缺少后向兼容,此问题在Apollo ROS里面得到解决。

2.Apollo ROS对ROS的改进


首先我们来看看通信性能优化的原因是什么?

  • 自动驾驶大量使用传感器引发很大的传输带宽需求


自动驾驶使用大量的传感器,这些传感器的数据量非常庞大。大量数据在目前ROS的通讯架构里面会带来比较高的延迟或是丢帧。节点之间通信是一帧一帧进行的,如果上一帧消息高延迟时,下一帧消息的发送就需要等待。ROS提供了这种消息丢弃的机制,如果等待时间长会丢弃一些数据,数据丢弃在实际自动驾驶系统中会造成比较大的风险。

  • 单路传感器消息有多个消费者负载成倍增长


自动驾驶系统发送传感器数据是一对一进行的。例如Lidar向自动驾驶系统发送数据时,如果只有一个订阅节点,传输的数据量是7M乘以10HZ,也就是70MB/S。自动驾驶系统是一个比较复杂的拓扑结构,一个传感器数据可能会有很多的下游订阅节点。例如感知的障碍物检测、通过视觉定位的模块、用红绿灯识别等都会订阅Camera信息。在单点的情况下是一对一,如果是一对多,传输的数据会被复制多次,造成网络负载成倍增加。


针对这一问题,Apollo ROS做了一个基于共享内存的通信机制减少数据的复制次数,从而提升这种通信模式的效率。

如上图所示,左侧是ROS原生的通讯框架,一个数据从发送方到接收方经历四次数据复制。第一次是从节点到用户内存的数据复制,第二次是从发送方到内核的数据复制,第三次是经过TCP连接,从内核再向接收节点用户态空间的复制,第四次是接收节点拿到这个信息之后,通过反序列化把信息取出来组成一个结构变化的信息。

右侧是Apollo ROS优化后的框架,它基于共享内存改进,可以减少两次数据拷贝。第一次是发送节点把消息序列化成流式数据,第二次是接收节点直接从共享内存里面取相应的消息指针,把共享内存消息取出来进行反序列化成结构化信息进行使用。减少了从用户到内核态以及从内核态到用户的两次数据拷贝。


对于有多个订阅节点的情况,例如Camera下游会有很多订阅节点,如果是三个节点,会有三条通信链路,分别是四次的内存拷贝,也就是12次数据拷贝。而在基于共享内存的通信方式下,每一条链路内存拷贝的次数只需要两次,三条链路只需要六次。


上图是一个有优化的Apollo ROS Benchmark效果展示,用Apollo ROS替代原生ROS Socket的通信方式之后,从实际的路测数据表现情况来看,性能提升非常明显。以上三张图分别从三个指标对比两者的性能。

  • 消息通信延时:
    如图右上角所示:随着消息逐渐增大,基于共享内存通信延时比基于原声ROS Socket的通信延时降低一半。以5M数据为例,传送一帧5M大小的数据,基于ROS Socket大概需要四毫秒左右的时间,基于共享内存通信只需要两毫秒左右。

  • 吞吐量:
    如图左下角所示:整个自动驾驶系统的网络拓扑结构非常复杂,数据流向的拓扑结构也比较复杂。在一些极端的情况下,整机数据量会增加。在一些多车道,路面状况比较复杂,车辆较多的情况下,感知和Planning模块,或者和其它模块之间的数据流就会成倍增加,所以在测试一些极端情况下,系统吞吐量也是自动驾驶需要考虑的一个重要方面。
    在吞吐量测试1:1情况下,整机性能可以达到5.5GB每秒的速度,如果是1:4,性能提升会更明显。

  • CPU资源占用率
    如图右下角所示:CPU资源占用率在共享内存通信情况下降低约30%, 主要是因为减少了多次内存复制。

四、Apollo ROS原理—2

在ROS系统中,从数据的发布到订阅节点之间需要进行数据的拷贝。在数据量很大的情况下,很显然这会影响数据的传输效率。所以Apollo项目对于ROS第一个改造就是通过共享内存来减少数据拷贝,以提升通信性能。

接着Apollo ROS对ROS的改进第二部分去中心化网络拓扑。


ROS是以Rosmaster节点管理器建立起来的一个P2P拓扑网络,这种拓扑网络有很明显的优势,如下:

1.节点之间相互独立,容错性比较强。
2.每个模块用不同的语言去开发,对其它的模块是透明的,其它模块不用关注和它通信的数据节点以及模块使用什么语言来开发。
3.模块开发之间是比较解耦合的,你只要定义好使用Topic/Service/Param的信息,然后按照这个格式去开发自己的模块。

但是也有两个比较明显的缺点。第一,节点之间的通信过于依赖Rosmaster单点。两个节点进行通信的链路过程,大概分为五部。第一步:发送节点去向Master注册一个发送节点。第二步:接收节点去向Master注册一个接收节点。第三步:Master向接收节点发送一个已有发送节点的一个信息拓扑。第四步:接收节点拿到这个拓扑信息之后去向发送节点请求建立一个tcp连接。第五步:在发送节点和接收节点建立一个P2P的单点拓扑连接之后就持续不断的向接收节点发送信息。整个过程中对Master依赖包含三步,在建立实际通信之后,对Master的依赖可能会降低很多,但是在建立之前是比较依赖Master节点的。

第二,ROS没有提供一种异常恢复机制。如果某一个节点挂掉,尤其是Master节点挂掉,其它的节点却不知道发生了这样的行为,还会认为整个系统运行仍然处在正确的状态中。比如发送节点里面有一些Service或者Param相关的请求,它还是会照常去发请求或者是设置这个参数信息,这样就会产生一些不可控的行为。


Master单点在多机的方案里,这个单机单点的不足就会更加凸显。比如现在很多自动驾驶厂商所采用的比较主流的Nvidia Drive PX2板卡,它就包括两个系统:一个主系统,一个是冗余备份系统即容错系统。如果使用ROS通信在PX2上进行部署,Master只能起在一个节点上。如上图所示:左侧是它的主系统,右侧是它的冗余备份系统。当主系统里面Rosmaster宕机之后,备份系统里面的节点其实并不知道Rosmaster已经处于一个宕机的状态,那么备份系统就起不到其目的和意义了。因为此时整个系统处于一个功能不完整的状态,所以就失去了冗余备份的意义。

  • 使用PTRS服务发现协议实现完全的P2P网络拓扑


Apollo ROS进行了比较大的改造:先把这个中心化的网络拓扑给去掉,然后建立了一个点对点之间的一个复杂网络拓扑,主要是使用RTPS服务发现协议去完成P2P网络拓扑。如上图所示:右侧是ROS Node的一个框架图。左下角是引入RTPS服务相关的一些功能。其它部分是ROS Node现有的一些功能。Ros Node是分层级式的结构,最上层是Handler,Handler提供节点和ROS整个通信的基本交互的句柄。下一层左侧和右侧定义了这个节点发送和订阅的Channel信息。再下一层是Middleware,Middleware是这个节点和其它节点进行通信的时候去完成链路的建立和数据的发送。

接下来的一层左下角RTPS是新引入的一个功能。改造之后的ROS Node架构,当一个节点被启动的时候,它会通过RTPS向所有的节点发送信息:现在有一个新的节点要加入到这个拓扑网络。当它离开的时候,也会发送消息告诉所有的节点:现在这个节点要退出。以前这些功能都是通过Rosmaster来完成的。

下面通过几张图来描述:节点建立连接和通讯的一个主要流程。

第一步:Sub节点启动,通过组播向网络注册。


订阅节点在启动的时候,它会向当前这个域里面所有的节点发送信息:现在有一个新的节点要启动。

第二步:通过节点发现,两两建立unicast


第三步:向新加入的节点发送它们已经有拓扑信息


所有已经存在的节点会向新加入的节点发送它们已经有拓扑信息,也就是在新节点加入之前每个节点其实是维护了它和其它所有节点的一个连接关系,这个连接关系发送给接收节点,供接收节点去更新自己的网络拓扑结构。

第四步:收发双方建立连接,开始通信。


当新加入节点接到所有节点发送出来的历史拓扑信息之后,它会根据它自己注册的实际消息内容去决定和哪些节点建立实际的通信连接。如上图所示:新加入节点只和右下角的一个节点之间有拓扑关系,它除了维护所有的节点给它发送出的整个网络拓扑信息之外,同时会和发送节点建立点对点的通信连接。

通过RTPS拓扑发现方式,Apollo ROS去除了对Rosmaster这一个单点的依赖,从而提升整个系统的鲁棒性。这个修改完全是对ROS底层的修改,用户基于原生ROS代码写的节点程序,到Apollo ROS是完全兼容的一个迁移即开发者不需要去改动任何的接口,就可以直接使用RTPS网络拓扑这种新的关系建立。

3.数据兼容性扩展

原因:如果有一个节点订阅的信息类型不是Channel预先指定的消息类型,这种通信连接是建立不起来的。


Message是两个节点进行消息通信的抽象描述文件。这个描述文件提前定义好两个节点之间进行消息通信的基本数据类型。ROS采用这种方式是因为能比较大概率地对两个节点之间进行解耦合,同时两个节点之间也是跨语言的,即不需要关注两个节点是用什么语言写的,都可以通过这种描述文件去进行实际的消息通信。通过Message通信的时候接、收节点在接受到信息之后,会进行MD5的校验、验证这个消息是否符合它的预先订阅,或者是在使用消息之后才会去进行消息的回调处理。


但是ROS基于Message这种通讯方式有很多的缺点。它最大限度解放两个点之点的一个耦合关系也带来了一些问题。比如Message接口升级,不同版本之间的兼容是需要做大量的适配工作。再如某个模块进行升级,之前所录制的一些实验数据,在进行回放的时候就会产生不匹配的现象。


历史数据在接口升级之后也面临着无法转化和兼容使用的问题。

  • 深度整合Protobuf功能,实现数据兼容性扩展


Apollo ROS实践里面引入了一种新的消息描述的格式去实现很好的向后兼容即Protobuf。只需要在使用的过程中,定义好必须的字段或者是一些新增的字段,新增的字段我们可以使用Optional属性去描述。在进行模块升级或者是模块之间的消息接口升级的时候,下游模块其实不需要关注新增字段对它来说会造成什么样的影响。如果它要去使用这个字段的话才需要去进行一定程度的适配。如果它的程序不使用这个新增的字段,就不需要做任何的修改。

上图是原生ROS和Apollo ROS对数据兼容支持的对比。

为了做好数据兼容,在原生ROS里面,开发者使用了一个trick:将Proto文件序列化成一个字符串信息放到Message信息里面,完成消息的向后兼容。比起Apollo ROS这个方式有两个明显的缺点:

1.它增加了一次数据序列化和反序列化。并把Proto序列化信息压到Message里面,增加了两次额外的数据Copy。
2.如果想实时调试信息,通过Rostopic echo打印出来Message里面那个序列化的字母串,若是采用Wrapper的方式,则这个字符串信息在屏幕上就会是一堆乱码。

Apollo ROS 为了满足数据兼容,深度整合了Protobuf的功能。用户可以直接定义Proto的字段信息,同时信息传递的过程不需要再进行额外的Message的数据转化。另外,在使用调试工具的时候,通过Rostopic echo可以看出原始消息传递的实际展示。

五、Apollo ROS原理—3

本节主要讲解四个比较基础的方面,如图所示。第一是TF坐标系转换,它在自动驾驶中用的非常广泛;第二是RQT用户接口;第三是机器人模型;第四是仿真描述。

1.TF坐标系转换

为什么需要TF坐标系转换?因为自动驾驶使用的ROS架构是一个松耦合关系,每个节点独立运行,节点有一套自己的XYZ坐标系,当把他们组装到一块时,每个节点的坐标系都是相对独立的,但整个自动驾驶系统需要把每一个节点所使用的信息和一些参数转化到同一个世界坐标系里。TF节点就提供了对应的坐标系转换功能,TF消息也是通过基于Message的订阅和发布消息来完成的。

例如,当下游的Planning节点想使用Obstacle信息时,需要将Obstacle信息转化到同一个世界坐标系,这时候它会发起一个TF去查询Obstacle处于哪一个世界坐标系里面的哪一个位置,从而感知整个车身周围的情况,基于此再做一个合理的规划和决策行为。

下图是结合之前将Publiser使用TF的一个例子,想使用TF,只需要改动两部分:第一是定义TF的对象;第二是直接进行TF数据查询,然后就可以得到一个世界坐标系。


2.RQT用户接口

RQT顾名思义,R实质是ROS的缩写,QT是可视化的图形工具,RQT是ROS给开发者提供的一套比较方便的图形化相关展示的一套工具。下面介绍几个比较常用的RQT功能:

1). 第一个是RQT imageview,这个主要是为自动驾驶顶层的一些传感器设计的,例如Camera图像,如果你想实时查看Driver接收图像是否正确,颜色、方位是否有问题,可以通过此工具简单选择对应Camera的channel,用 Camera的topic信息实时查看图像状态。


2)Multipot可以将二维的数据在一个二维坐标系里面进行实时展示,这样可以更直观地看到我们所需要的数据是否符合我们的预期。

3)RQT的graph工具,在开发的实际过程当中使用得比较广泛,这个工具把整个网络拓扑用图形化的方式展现出来。例如启动Perception、 Planning和Roscore这三个节点,它都会在RQT graph工具里面进行实时展示,同时两个节点之间所用的topic信息也会在里面实时展示。

4)RQT console是对应ROS日志系统所提供的一套可视化工具。ROS提供了五种级别的LOG展示:DEBUG 、IFNO、WARN、ERROR、FATAL。每个模块在某一时刻都会产生大量的日志信息,RQT console把这些信息统一规整到一个可视化工具里面,用户可以通过配置的方式快速定位和找到自己所需要的一些相关信息。


5)RQT logleve是为ROS日志系统所提供的另外一个可视化工具。在写代码的时候,可能5种类型的日志都会使用,但是在实际调试过程中可能只想看到某几种类别的实时信息,通过这个工具可以实时调整,让节点输出我们想要的级别的一些信息。例如我们只想看到ERROR或者FATAL信息,就可以把某一个节点的信息级别设置为ERROR,这样这个节点所打印的ERROR和FATAL的信息可以通过命令行或者LOG文件里面去看到,其它级别的信息不会干扰实时调试。


3.Robot Models URDF

在进行实际模拟的时候,可以用一套语言来定义机器人模型,这套语言被定义为统一机器人描述格式语言URDF。它也是一套xml的语言描述,这个描述格式里面包含两个核心的概念:一个是节点Link,一个是节点之间的连接关系Joint。Joint会指定Parent节点和Child节点,这样就可以描述一个完整的拓扑结构,也就是对整个网络拓扑结构的xml语言化描述。在进行仿真的时候,通过加载对应的URDF文件,在仿真环境里面实时地展示所需要调试的信息。


4.SDF Simulation Description Format

Simulation Description Format(SDF)是另外一个调试工具。之前介绍的Rviz调试工具,更多的是看到消息收发之间的实体化展示,例如展示点云、图像和其它一些信息。如果进行仿真模拟,如机器人模拟的时候,就用另一套工具Gazebo。Gazebo是ROS的一个开发包,它里面所使用的描述语言就是Simulation Description Format。用Gazebo加载URDF时,Gazebo首先把URDF描述语言转换成SDF语言,然后再进行加载和展示。


六、Apollo ROS原理—4

本节主要介绍几个在实际开发调试过程中使用比较广泛的一些概念。

1.ROS Services


ROS提供了三种节点之间通信的方式:第一种是大家最常用的基于消息的订阅发布模型,第二种就是ROS Service,第三种Param,它借鉴了Service的思想。Service在自动驾驶系统里面使用的比较广泛,与基于消息发布订阅模型类似, Service有一个service name,同时Service底层是一个SRV描述文件,它和MSG描述文件比较类似,不同是SRV描述文件定义了两种消息:请求信息的消息格式和响应格式。请求是Client向Server发出请求的消息定义格式,与Response逻辑类似。

对应Service,Rosservice提供了一系列命令行工具,例如常用的像List、Call等一些基本的功能响应。


这是一个SRV文件,可以看到所有的Service对应的SRV文件描述都有一个Request和Response方式。当然这两个都可以置为空,置空就没有意义了。此外,也可以写一些具体的类型,比如我向你发送一个什么样的消息请求,你在接受我的对应消息请求之后会返回一个什么样的响应的数据格式指令。


上面结合一个实际的例子,给出Service具体的使用方法。Service启动的时候,需要提前启动Roscore,即节点管理器。第二步启动Service的一个例子程序,之后通过List和Type命令可以看到在启动某一个节点后,这个节点里面注册了某个Service的一个实际展示。


与前面提到的Rostopic对应,ROS也提供了命令行方式调用一个Service,当然命令行方式调用Service也是把它当成了一个节点的方式进行Service、Client链路的建立和响应。


这个是结合实际的C++例子,描述Service的具体使用方法,可以看到Service和普通的Publisher、Subscriber比较类似,尤其是和Subscriber比较类似。但不同的是它有一个Service方法,定义了节点提供的服务类型。以及当Client向它发出请求时,节点会做出什么样的行为同时把这个结果再发送给Client。第二个不同点是在注册Service时,把Service的Name,和Service所提供的函数注册即可。与注册Server的节点对应,Client的节点也比较简单,只需要进行两步就可以完成一个Service的使用,第一步是声明Client对象,第二步是直接去调用Service,传入对应的Request就可以拿到对应的Response结果。

2.ROS Actions

ROS还提供了另外一种通讯方式,这种不常见通讯方式就是Actions,相比Service,它多了一个取消的功能和带有反馈机制。对于Service,发起一个Service请求需要等到返回一个正确的Response结果才会退出。Actions在发送一个Service请求之后,它可以发送取消的命令,取消这个Service请求,可用于一些较长时间的Service场景。当然目前这种场景在Apollo自动驾驶系统里面比较少。ROS Actions在.action文件里定义了action,跟srv其实类似,只不过是在.action文件里面,定义了更多的类型。

3。ROS Time

ROS系统供了一套time机制,这个time的时间源来自于PC机的系统时间。Rostime基于此提供了一个重要的功能点:仿真时间。用ROS系统进行自动驾驶开发的时候,仿真模拟是一个不可缺少的环节。如刚才提到的Rviz、Gazebo都是为开发者进行离线仿真和模拟的强大调试工具,在使用这些调试工具的时候,实验数据可能是很早之前录制的,也有可能是在其他地方录制的。这种之前的数据在仿真环境下进行模拟时,如何回放当时的场景,或者是如何把当时的时间转化到现在的时间?Rostime就供了虚拟时钟功能,保证在回放一些历史实验数据,或者其他地方实验数据时,让整个仿真系统认为现在的场景就是所需要的那个系统时间和系统场景。

4.ROS Bags

ROS Bags有两个比较重要的功能,第一个是把实际车上调试的数据或者是把自动驾驶进行道路测试的原始传感器数据按一定格式录制到某个bag文件里。实验室或者开发环境可以根据bag文件不断的回放,去复现当时的网络场景。例如,有一个场景,车在某一个特定的地方做了一个错误的决策,我们想改一版算法去验证这个场景有没有被覆盖,这时可以拿那个Rosbag回放,验证新版算法的输出是不是符合预期。另外Rosbag数据对算法进行模型训练和调优也是非常有必要的。

5.调试工具

最后了解一下调试工具,如下图所示,ROS提供了一些简单的功能,比如说ROS WTF这种功能,可以让用户很简单地查看当前系统是环境变量设置的问题,还是其他的一些核心库链接的位置问题,还是其他的问题导致的一些运行失败,通过WTF都可以很快的定位,同时,ROS也提供了一些其他的Debug诊断功能供开发者在实际开发过程当中去使用。

七、Apollo ROS深入介绍

本节内容主要介绍ROS中一些不是很特别常见的属性。

1.ROS Packages


创建一个ROS开发环境和写一个C++工程有点类似,通过catkin create可以创建一个简单的工程。其中的文件组织方式如上图所示,包括:

SRC存放源文件;

MSG存放节点之间进行通信的消息定义;

SRV存放节点之间进行服务通信的时候的服务定义;

CONFIG存放配置文件相关的信息;

INCLUDE存放头文件相关的信息;

Launch存放节点启动和它相关的节点之间的启动文件。

上面介绍的package组织方式只是官方推荐的一种组织方式,使用catkin build编译,当source完环境变量之后,通过Ros提供的命令比如ROS run或者ROS launch启动时,package name可以自动补全,package里面包含的节点或者launch文件也是可以自动寻找,所以官方推荐使用这种组织方式。

**DEVEL和BUILD这两个目录是build时自动产生的两个临时目录。

此外,开发环境还有描述ROS Package相关工作区的两个文件:Package.xml和Cmakelists.txt。


Package.xml定义了可执行文件依赖的一些库,包括编译和运行时依赖库,同时定义了软件版本信息等常见的描述文件。


Cmakelists.txt定义了怎么编译ROS工程的规则,主要定义了以下几个部分:

1、指定Cmake的版本;

2、工程Package name;

3、工程的头文件信息;

4、指定所依赖的库,与在Package.xml里面所指定的依赖库是一一对应的;

5、install命令,将编译出来的临时文件放在指定的目录里。

下面是Cmakelists文件的一个例子。


从上到下依次指定了Cmake的版本、project的名字、ROS工程所依赖的c++的版本、另外是依赖的库文件,最后生成可执行文件以及这个文件所链接的依赖库。

2.Eclipse下编译ROS基本工程

工程建立好之后,catkin build可以直接对工程进行编译。直接用 catkin build去编译,会把整个工程目录里面的所有的package进行统一编译,如果是构建一个比较复杂的系统,可能一个文件夹包含了很多节点或者package包,编译时间会比较久,可以通过指定package名,编译某一个固定的package包,提升编译效率。

下面介绍在Eclipse下如何编译ROS基本工程



首先是设置工作ROS工作区,然后将ROS package导入到Eclipse设置的工作区。


通过Eclipse提供的build或者run等功能去调试ROS工程。同时Eclipse里面提供的快捷键在编译程序里面同样适用。

3.通过hello world 了解ROS基本的运行逻辑


上图的hello world程序展现了ROS框架写Node所使用的核心要素:
Include 就是include ROS的一个基本环境;

Main函数里面有三行需要重点注意:

1.init:引入ROS的一个基本环境,指定节点使用的node名字和一些参数信息;
2.NodeHandle:node和整个ROS框架进行通信所使用的一个句柄指针;
3.数据发送的频率:looprate(10)以10赫兹发送消息。

最底下的while循环以10赫兹的消息频率进行发送,同时进行计数。

Spinonce:有一帧消息就把这消息立马发送出去,同时进行下一轮的消息等待。

4.ROS提供的日志系统


在示例程序里面有一个ROS_INFO,它就是ROS提供的日志系统;

ROS的日志系统是分级的,即在编写节点程序的时候对打印的信息进行分级,对不同的分级,ROS会提供不同的颜色和格式进行展示。分级的作用是为了帮助开发者快速地定位到关键信息,不会对整个节点的逻辑产生实质性的影响。

日志系统提供了两种格式ROS_INFO与ROS_INFO_stream:

ROS_INFO:默认把信息打印到当时运行的屏幕上。
ROS_INFO_stream:它是流式数据,默认输出到后台这个节点所对应的日志文件。

5.ROS提供的subscriber和publisher功能



Subscriber与Publisher有三点明显的区别:

1、回调函数:subscriber作为信息的接收方有一个回调函数,回调函数定义了它接收到的每一帧信息如何使用;上图listener回调函数比较简单,它接收到信息后只是进行了打印处理。Publisher没有回调函数,它不需要对消息进行处理。
2、声明的时候:subscriber把回调函数传入到对应的node初始化程序里面。publisher声明的时候只需要注册要往哪一个topic上去发信息,同时还设置队列长度。
3、Rosspin:在ROS构架里所有的回调函数都不是主动触发的。Rosspin是阻塞性的,声明Rosspin之后,就阻塞在此,程序不会退出,它会一直监听自己对应的队列里面是否有新消息的到达,若有新消息到达就会触发回调函数处理。

如果Subscriber的主程序里除了订阅消息之外还有其他的功能则可以采用rosspin once,对所有已达消息进行回调函数的处理。同时可以写一个while循环,rosspin once按照一定的频率去处理回调函数的消息。ROS提供Rosspin这两种方式,就是为了满足这两种场景。第一种是阻塞,只有一个回调函数进行处理,第二种是订阅回调函数消息以外,他还进行了一些封装和处置。

看似很复杂的自动驾驶,节点整体写下来都是比较简单的,就是按照上图Subscriber与Publisher方式来写。但是在实际的自动驾驶系统里面,所有的模块都不是简单的一个角色,它可能既是消息的订阅者也是消息的发送者,是一个复杂交互的功能,甚至是用到很多数据融合或者是消息对齐。

6.ROS除了message的另外两种通信方式

ROS节点之间的通信除了基于message消息订阅和发布模型之外还有另外两种方式,虽然另外两种方式使用的比较少,但是在某些特定的场合是比较有用的。

service
节点可以启动service去注册一项服务,另外一个节点在使用这项服务的时候可以直接call service完成一些实时的数据通讯交互。Message是一个被动的消息行为,发送者发送消息的时候并不知道消息会被谁去消费,接收者在接收消息的时候也不知道目前有几个发送节点在发送,发送和接收之间是一个什么状态也是不知道的,他们是一个松耦合和透明的关系。Service弥补了这种通信方式的不足,它需要及时回应。Client向server去发送service请求的时候,需要实时等待一个response,根据响应做出下一步的行为指示。

parameter
Parameter通信方式借鉴了service的原理。它启动了Parameter service,Parameter service是一个全局的服务器,各个节点在进行参数设置和获取的时候可以通过Parameter service的方式轻易完成。因为Parameter不像基于message消息通讯方式那么频繁,一个参数在设置完成之后,在整个网络拓扑运行期间所有的节点只需要在一个地方取此参数就行或者某个节点根据自己的运行状态去改变这个参数。

Parameter对应有一套ROS所使用的基本命令行工具—rosparam。

rosparam其它工具相比,有两个不同的地方:get和set。get是get某一个全局参数的值;set是设置某一个全局参数的值.

7.ROS的可视化工具RViz


自动驾驶节点比较多,网络拓扑也比较复杂,每个节点在进行消息通讯的时候有很多channel同时运行,如果只是通过命令行工具去查看节点的状态和节点之间的拓扑,会很麻烦。ROS提供了一些比较好用的可视化工具立体化展示某一个拓扑结构里面的拓扑网络,RViz就是其中之一。

RViz在整个ROS生态里可以看成是一个节点,它定义了整个拓扑结构里面所有的消息,然后按照固定的格式进行图形化展示,同时提供很多debug相关的功能。因为RViz也是一个普通的节点,所以在启动的时候可以通过rosrun命令的方式去启动RViz相关的功能。


RViz也提供了很多插件可以放到诸如eclipse这样的功能插件里面,在进行eclipse开发时可以通过eclipse的plugin去调取RViz的相关功能,进行可视化调试。

所涉及内容繁琐,为防止总结失误,所以特此摘转阿波君公众号内容,希望对你有所帮助。**部分内容有自己总结改动