ROS 概念和设计模式

正如我们所说,学习 ROS 类似于学习汽车。事实上,汽车很像机器人(有时它真的就是机器人;参见庞大而活跃的自动驾驶汽车行业)。现代汽车由许多相互连接的部分组成。方向盘连接到前轴,制动踏板连接到制动钳,氧气传感器连接到燃油喷射器,等等。从这个角度来看,汽车是一个分布式系统:每个部分都扮演着明确的角色,根据需要与其他部分进行通信(无论是电气还是机械),这种交响乐般的协调的结果就是一辆正常工作的汽车。

ROS 的一个关键哲学原则是,机器人软件也应该作为分布式系统进行设计和开发。我们的目标是将复杂系统的功能分离为相互交互的各个部分,以产生该系统所需的行为。在 ROS 中 我们将这些部分称为节点,并将它们之间的交互称为 主题(有时也称为服务,但我们稍后会讲到)。

ROS 通信图

假设我们正在构建一个追逐红球的轮式机器人。这个机器人需要一个摄像头来观察球,一个视觉系统来处理摄像头图像以确定球的位置,一个控制系统来决定移动的方向,以及一些电机来移动轮子,使其向球移动。使用 ROS,我们可以像这样构建系统:

image

这种设计将软件分为四个 ROS 节点:两个设备 驱动程序和两个算法。这些节点通过三个 ROS 主题相互通信,如图所示。我们将此结构称为 ROS 通信图:节点是图顶点,主题是图边。通过检查 ROS 系统的通信图,您可以了解很多有关该系统的信息。

摄像头驱动程序节点负责处理与物理摄像头交互的细节,这可能通过自定义 USB 协议、供应商提供的库或其他方式进行。无论这些细节是什么,它们都封装在摄像头驱动程序节点内,该节点为系统的其余部分提供了一个标准的 topic 接口。因此,blob 查找器节点不需要知道有关摄像头的任何信息;它只是以标准格式接收图像数据,该格式用于 ROS 中的所有摄像头。blob 查找器的输出是检测到的红球位置,也是标准格式。然后,目标跟随器节点可以读取球的位置并产生向球移动所需的转向方向,同样是标准格式。最后,电机驱动器节点的职责是将所需的转向方向转换为命令机器人车轮电机所需的特定指令。

发布-订阅消息:主题和类型

以追球机器人为例,我们可以添加一些术语来描述系统运行时发生的事情。首先,ROS 通信图基于一种众所周知的模式,称为“发布-订阅”消息传递,或简称为“发布-订阅”。在发布-订阅系统中,顾名思义,数据以“消息”的形式从“发布者”发送到“订阅者”。发布者可能有零个、一个或多个订阅者监听其发布的消息。消息可以随时发布,从而使系统“异步”。

在 ROS 中,节点通过主题发布和订阅,每个主题都有一个名称和类型。发布者通过广告主题来宣布它将发布数据。例如,相机驱动程序节点可以广告名为/image的主题,其类型为sensor_msgs/Image。如果 blob 查找器节点订阅了具有相同名称和类型的主题,则两个节点会找到彼此并建立连接,图像消息可以通过该连接从相机驱动程序传输到 blob 查找器(节点在称为发现的过程中找到彼此并建立这些连接,本书后面将详细介绍)。流经/image主题的每条消息的类型都将为sensor_msgs/Image

单个节点可以(并且通常是)既是发布者又是订阅者。在我们的示例中,blob 查找器订阅图像消息并发布球位置消息。类似地,目标跟随者订阅球位置消息并发布转向方向消息。

主题的类型非常重要。事实上,总的来说,ROS 类型是整个平台最有价值的方面之一。首先,类型会告诉您语法:消息包含哪些字段、哪些类型?其次,它会告诉您语义:这些字段是什么意思,应该如何解释?例如,温度计和压力传感器可能会产生看似相同的数据:浮点值。但在 ROS 中,设计良好的温度计驱动节点会发布一种明确定义的类型(例如,sensor_msgs/Temperature),而压力传感器驱动节点会发布另一种类型(例如,sensor_msgs/FluidPressure)。

我们始终建议使用语义上有意义的消息类型。 例如,ROS 提供了简单的消息类型,如 std_msgs/Float64,它包含一个名为 data 的 64 位浮点字段。但您应该只使用这种通用类型进行快速原型设计和实验。当您构建一个真正的系统时,即使像 std_msgs/Float64 这样的东西可以在语法上完成工作,您也应该找到或定义一个与您的应用程序语义相匹配的消息。

为什么要采用发布-订阅模式?

考虑到它带来了额外的复杂性(节点、主题、类型等),我们有理由问为什么 ROS 遵循发布-订阅模式。经过十多年的构建和部署基于 ROS 的机器人系统,我们可以确定 几个主要优点:

  • 替代: 如果我们决定升级机器人的摄像头,我们只需要修改或更换摄像头驱动程序节点。系统的其余部分无论如何都不知道摄像头的详细信息。同样,如果我们找到一个更好的 blob 查找器节点,那么我们可以将其替换为旧节点,其他部分无需改变。
  • 重复使用: 精心设计的 blob 查找器节点今天可以在这个机器人上使用来追逐红球,明天可以在不同的机器人上重复使用来追逐一只橘色的猫,等等。节点的每次新用途都只需要更改配置(无需更改代码)。
  • 合作: 通过清晰地分离节点之间的关注点,我们让 blob 查找器专家独立于目标跟随者专家完成工作,他们都不会打扰设备驱动程序专家。机器人应用程序通常需要许多人的综合专业知识,确保每个人都能自信而高效地做出贡献的重要性怎么强调也不为过。
  • 内省: 由于节点通过主题明确地相互通信,因此我们可以监听。因此,当机器人无法追逐红球时,我们认为问题出在 blob 查找器中,我们可以使用开发人员工具来可视化、记录和回放该节点的输入和输出。以这种方式自省正在运行的系统的能力对于调试它至关重要。
  • 容错: 假设目标跟随者节点由于错误而崩溃。如果它在自己的进程中运行,那么这次崩溃不会影响系统的其余部分,我们只需重新启动目标跟随者即可让系统重新运行。一般来说,使用 ROS,我们可以选择在单独的进程中运行节点,这样可以实现这种容错能力,或者在单个进程中一起运行它们,这样可以提供更高的性能(当然,我们可以混合搭配这两种方法)。
  • 语言独立性: 可能会发生这样的情况:我们的 blob 查找器专家 使用 C++ 编写计算机视觉代码,而我们的目标跟踪器 专家则专注于 Python。我们只需在单独的进程中运行这些节点,就可以轻松满足这些偏好。在 ROS 中,以这种方式混合和匹配语言的使用是完全合理的,事实上也相当常见。

超越主题:服务、操作和参数

大多数 ROS 数据都流经我们在上一节中介绍过的主题。主题最适合流式数据,其中包括机器人技术中的许多常见用例。例如,回到我们的追球机器人,大多数相机自然会以某种速率(例如 30Hz)产生图像流。因此,相机驱动程序在收到包含这些图像的 ROS 消息后立即发布这些消息是有意义的。然后,blob 查找器将以 30Hz 的速率接收图像消息,因此它也可以以相同的速率发布其球位置消息,依此类推,通过目标跟随器到电机驱动器。我们可以说这样的系统是从相机计时:主传感器(在本例中为相机)的数据速率驱动系统的计算速率,每个节点都会对其他节点通过主题发布的消息的接收做出反应。这种方法相当常见,适用于像我们的追球机器人这样的系统。在您获得新的相机图像之前,没有必要做任何工作,一旦您获得新的相机图像,您就希望尽快处理它,然后命令适当的转向方向。

(我们正在做出各种简化假设,包括有足够的计算能力来以足够快的速度运行所有节点以跟上相机的数据速率;我们没有办法预测球在相机帧之间的去向;并且可以以与相机生成图像相同的速率来命令电机。)

服务

因此,主题可以完成基本的追球机器人的工作。但现在假设我们想要添加定期捕获超高分辨率图像的功能。相机可以做到这一点,但它需要中断我们依赖应用程序的常规图像流,所以我们只希望它按需发生。这种交互不适合主题的发布-订阅模式。幸运的是,ROS 还在第二个概念中提供了请求-回复模式:服务

ROS 服务是一种远程过程调用 (RPC),是分布式系统中的常见概念。调用 ROS 服务类似于通过代码 API 调用库中的正常函数。但由于调用可能会被分派到另一个进程甚至网络上的另一台机器,因此它不仅仅是复制指针。具体来说,ROS 服务是使用一对 ROS 消息实现的:一个请求和一个回复。调用服务的节点填充请求消息 并将其发送到实现服务的节点,在该节点上处理请求 并产生返回的回复消息。

我们可以像这样实现新的高分辨率快照功能:

  • 定义新的服务类型. 由于服务的使用范围不如主题广泛,因此预定义的“标准”服务类型相对较少。 在我们的例子中,新服务的请求消息可能包括快照所需的分辨率。请求消息可以是标准的 sensor_msgs/Image.
  • 实现服务. 在相机驱动程序中,我们将宣传新定义的服务,以便在收到请求时,暂时中断通常的图像处理,以允许设备交互来抓取一个高分辨率快照,然后将其打包到回复消息中并发送回调用该服务的节点。
  • 致电服务. 在目标跟随者节点中,我们可能会添加一个 计时器,以便每 5 分钟调用一次新服务。目标 跟随者将在每次调用后收到高分辨率快照, 然后可以将其添加到磁盘上的照片库中。

一般来说,如果您需要节点之间不频繁、按需交互,ROS 服务是一个不错的选择。

操作

有时,在构建机器人控制系统时,需要一种看起来像请求-回复的交互,但请求和回复之间可能需要大量时间。想象一下,我们想把追球控制系统包装成一个黑匣子,可以将其作为使机器人踢足球的更大系统的一部分来调用。在这种情况下,更高级别的足球控制器会定期说,“请追红球,直到它就在你面前。”一旦球在机器人面前,足球控制器就会停止追球控制器并调用接球控制器。我们可以使用 ROS 服务实现这种交互。我们可以定义一个追球服务并在目标跟随器中实现它。然后,当足球控制器想要追球时,它可以调用该服务。但追球可能需要相当长的时间才能完成,而且可能无法完成。不幸的是,在调用追球服务后,足球控制器会一直等待回复,类似于在代码中调用长时间运行的函数的情况。足球控制器不知道追逐进行得如何(或如何),也无法停止追逐。

对于这种以目标为导向、时间延长的任务,ROS 提供了第三个概念,它类似于服务,但功能更强大:动作。ROS 动作由三个 ROS 消息定义:目标、结果和反馈。目标由调用动作以启动交互的节点发送一次,表示动作试图实现的目标;对于追球,它可能是距离球的最小所需距离。结果由执行动作的节点在动作完成后发送一次,表示发生了什么;对于追球,它可能是追逐后距离球的最终距离。执行操作的节点会定期发送反馈,直到操作完成,向调用者更新事情的进展情况;对于追球,反馈可能是追球过程中与球的当前距离。此外,操作是可取消的,因此如果追逐时间过长或反馈消息显示成功的可能性很小,足球控制器可以决定放弃并转向另一种策略。

一般来说,如果您想支持按需、长期运行的行为,ROS 操作是一个不错的选择。

参数

任何非平凡的系统都需要配置,ROS 也不例外。 当我们启动机器人的电机驱动器节点时,我们如何告诉它通过 /dev/ttyUSB1 连接到电机?我们不想将这些信息硬编码到节点中,因为在下一个机器人上,它可能是 /dev/ttyUSB0。ROS 通过第四个概念解决此类配置需求:参数。ROS 参数就是您可能期望的:一个命名的、类型化的、用于存储数据的位置。例如,电机驱动器节点可以定义一个名为 serial_port 的参数,类型为字符串。启动时,节点将使用该参数的值来知道要打开哪个设备以进入电机系统。

ROS 参数可以通过几种方式设置:

  • 默认值. 使用参数的 ROS 节点必须在其代码中嵌入该参数的某个默认值。如果系统中没有其他东西明确设置参数值,则节点需要某个值来工作。
  • 命令行. 设置参数有标准语法 启动节点时在命令行上的值。在此设定的价值观 方式覆盖代码中的默认值。
  • 启动文件. 通过launch工具启动节点时 通过命令行手动设置,您可以在 启动文件。以这种方式设置的值将覆盖代码中的默认值。
  • Service calls. ROS 参数可通过 标准 ROS 服务接口,允许在 飞行,如果托管参数的节点允许的话。在此设定的价值观 方式覆盖之前设置的任何值。

对于大多数节点来说,参数管理相对简单:定义一个 少量参数,每个参数都有合理的默认值;检索 启动时参数的值,这解释了通过 命令行或启动文件,然后开始执行并禁止未来 變化。这种模式对于电机驱动器来说很有意义,因为它需要 知道在启动时打开哪个 /dev/ttyUSB 设备文件,并且不知道 支持稍后更改该设置。但有些情况需要 更加精巧的处理。例如,blob 查找器节点可能 将各种阈值或其他设置作为参数公开, 配置它如何识别图像中的红球。这些类型的 可以随时更改设置,目标追随者可能想要 根据追逐的进展情况来做。在这种情况下,blob finder 需要确保其参数使用最新的值, 知道它们可能已被另一个节点改变。

一般来说,当你想存储稳定但可能变化的 节点中的配置信息,ROS参数是一个不错的选择。

Asynchrony in Code: Callbacks

在整个 ROS 中,你会看到代码中有一个常见的模式,即 使用回调函数,或者简称为回调。例如,当 订阅主题时,您需要提供一个回调函数,该函数 每次您的节点收到有关该主题的消息时都会被调用。 类似地,当你宣传一项服务时,你提供一个回调, 在调用服务时调用。行动也是如此(例如 处理目标、结果和反馈)和参数(用于处理 设定新价值观)。

使用回调进行编程对大多数人来说并不熟悉。它不同于 编程的标准顺序表示,其中你编写一个main() 先执行 A 函数,然后执行 B 函数,再执行 C 函数,依此类推。相比之下,在 ROS 中(以及 对于大多数专注于数据处理和/或控制的系统,我们遵循 基于事件的模式。在这个模式中,每当发生 X 时,我们就执行 A,每当发生 Y 时,我们就执行 B 发生等等。

ROS 节点的常见结构如下:

  • 获取参数值. 检索节点的配置, 考虑默认值以及可能从外部传入的内容。
  • 配置. 执行任何必要的操作来配置节点,例如 建立与硬件设备的连接。
  • 设置 ROS 接口. 宣传主题、服务和/或行动, 并订阅服务。每个步骤都提供了回调 由 ROS 注册以供稍后调用的函数。
  • 旋转. 现在一切都已配置完毕并准备就绪, 控制权交给 ROS。当消息流入和流出时,ROS 将调用 您注册的回调。

按照这种结构,ROS 节点中的 main() 函数通常非常 简而言之:初始化并配置所有内容,然后调用旋转函数来让 ROS 接管。当你尝试了解 ROS 中正在发生的事情时 节点,查看回调;真正的工作就在这里发生。