介绍
欢迎! 这是一本关于多机器人系统的书。 为什么? 因为它代表着未来!
机器人变得越来越便宜、功能越来越强大,并且在许多“现实生活”场景中越来越有用。 因此,我们看到越来越多的机器人需要共享空间并共同完成任务。 在本书中,我们将介绍机器人操作系统 2 (ROS 2) 以及机器人中间件框架 (RMF),它建立在 ROS 2 之上,并试图简化复杂多机器人系统的创建和操作。
本章介绍了 ROS 2 和用于集成多个机器人的 RMF 系统的动机和目标。
ROS 2
机器人操作系统 (ROS) 是一套用于构建机器人应用程序的软件库和工具。 从驱动程序到最先进的算法,再到强大的开发工具,ROS 可满足您下一个机器人项目的需求。 而且,ROS 全部都是开源的。
自 2007 年 ROS 启动以来,机器人和 ROS 社区发生了很大变化。 ROS 1 最初只是“ROS”,最初是作为 Willow Garage PR2 机器人的开发环境而诞生的,这是一款用于高级研究和开发的高性能移动操控平台。 ROS 的最初目标是为 tdaools 用户提供使用该机器人进行新颖研究和开发项目所需的软件。 同时,ROS 1 开发团队知道 PR2 不是世界上唯一的机器人,也不是最重要的机器人,因此他们希望 ROS 1 也能在其他机器人上发挥作用。 最初的重点是定义抽象级别(通常通过消息接口),以便将大部分软件在其他地方重复使用。
ROS 1 满足了 PR2 的使用情况,但也适用于种类繁多的机器人。 这包括与 PR2 类似的机器人,也包括各种尺寸的轮式机器人、有腿的人形机器人、工业手臂、户外地面车辆(包括自动驾驶汽车)、飞行器、地面车辆等。 ROS 1 的采用也发生了令人惊讶的转变,发生在最初关注的主要是学术研究界以外的领域。 基于 ROS-1 的产品纷纷上市,包括制造机器人、农业机器人、商业清洁机器人等。 政府机构也在更密切地关注 ROS 在其现场系统中的使用;例如,NASA 预计将在部署到国际空间站的 Robonaut 2 上运行 ROS。 所有这些应用程序无疑以意想不到的方式发展了 ROS 平台。 尽管 ROS 1 表现良好,但 ROS 1 团队相信,通过正面解决他们的新用例,他们可以更好地满足更广泛的 ROS 社区的需求。 于是,ROS 2 诞生了。
ROS 2 项目的最初目标是适应不断变化的环境,利用 ROS 1 的优点并改进缺点。 但也有希望保留 ROS 1 的原貌,继续工作并不受 ROS 2 开发的影响。 因此,ROS 2 被构建为一组并行的软件包,可以与 ROS 1 一起安装并与之互操作(例如,通过消息桥)。
在撰写本文时,我们已经发布了第 13 个也是最后一个 ROS 1 官方版本, Noetic Ninjemys, 以及 ROS 2 的第一个 LTS 版本, Foxy Fitzroy.
在网络上可以找到大量且不断增长的 ROS 2 资源。 ROS 索引页面是一个很好的起点 ROS 2 以及本书的 ROS 2 章节中的进一步内容。
祝您旅途愉快!
机器人中间件框架 (RMF)
暂时想象一下任何大型建筑。 它可以是购物中心、住宅区、大学大楼、工作场所、机场、医院、酒店等。 物品是否在建筑物内交付? 大楼地面是否定期清洁? 对于大多数建筑物来说,这两个问题的答案都是“是”。
现在,让我们想想当机器人开始执行这些任务时会发生什么。 在当今的机器人市场上,您可以购买到优秀的送货机器人,也可以购买到优秀的扫地机器人。 但是,如果在建筑物内运送物品的同时清洁地板怎么办? 当人类执行清洁和送货任务时,这种情况是微不足道的:推着推车的送货员和清洁地板的保管员快速一瞥就可以快速达成妥协。 一个人或两个人会找到一种方法来稍微改变他们的任务时间,以完成这两项任务。
不幸的是,机器人在抽象推理、计划和非正式沟通方面的能力远不及人类! 机器人中间件框架 (RMF) 试图避免这种情况的发生。 在当今的市场中,如果所有机器人都是从同一制造商购买的,那么这种单一供应商系统中的机器人将知道彼此的存在并避免彼此冲突。 然而,多供应商、多机器人系统仍然是一个悬而未决的问题,我们预计多供应商机器人部署将成为未来所有大型建筑的常态。 为了解决这种情况,RMF 提供了一套约定、工具和软件实现,允许多个机器人车队相互操作并与共享的建筑基础设施(例如电梯、门、走廊和其他交通自然“瓶颈”)进行互操作流程和任务。
如果没有适当的多供应商机器人框架,当建筑运营商和最终用户被迫使用单一系统或平台提供商时,他们可能会面临重大但隐藏的风险。 隐藏的风险可能会迫使最终用户限制对单一提供商的未来解决方案的选择,以最大限度地降低运营风险并避免多余的集成成本。 随着机器人部署的范围和规模的增加,这个问题变得更加严重,让客户觉得除了继续使用当前的供应商之外没有什么好的选择,并阻止市场新进入者使用机器人。
除了与不同提供商扩展部署的成本风险增加之外,还存在共享资源(例如电梯、门口、走廊、网络带宽、充电器、运营中心屏幕“不动产”以及 IT 人员等人力资源)的固有冲突。和维修技术人员。 随着机器人规模的增加,运营团队考虑管理大型、异构、多供应商的机器人环境变得更加麻烦。
这些问题陈述是 RMF 发展的根本动机。
在前面的“清洁和送货”场景中,RMF 可以充当交通控制器,帮助送货机器人和清洁机器人根据每个任务的相对优先级和重要性协商完成这两项任务的方式。 如果清洁任务很紧急(可能在繁忙的走廊发生泄漏),RMF 可以通过另一组走廊来安排交付任务。 如果送货任务时间紧迫,RMF 可以指示清洁机器人暂停工作并让开,直到送货机器人清理走廊。 当然,这些解决方案是显而易见的,并且可以针对这种特定的“清洁和交付”走廊共享场景轻松手写。挑战来自于尝试在许多场景中通用,同时还尝试“面向未来”以允许扩展到当前未知的机器人、应用程序和任务领域。
本书的其余部分将深入探讨这些细节,以展示 RMF 如何尝试预见和防止资源冲突并提高多供应商、多机器人系统的效率。 这里没有魔法! 所有实现都是开源的,可供检查和定制。
我们要感谢新加坡政府对启动这一雄心勃勃的研发项目的远见和支持,“标准化机器人中间件框架的开发 - RMF 详细设计和通用服务、大规模虚拟测试场基础设施和仿真建模 ”。该项目得到了卫生部(MOH)和国家机器人计划(NRP)的支持。
本材料中表达的任何意见、调查结果和结论或建议均为作者的观点,并不反映 NR2PO 和卫生部的观点。
那么什么是RMF呢?
RMF 是构建在 ROS 2 之上的可重用、可扩展的库和工具的集合,可实现任何类型机器人系统的异构队列的互操作性。 RMF 利用标准化通信协议来部署机器人的基础设施、环境和自动化,以优化关键资源(即机器人、电梯、门、通道等)的使用。 它通过资源分配和通过 RMF 核心防止共享资源冲突,为系统增添了智能,本书稍后将对此进行详细描述。
RMF 足够灵活且强大,几乎可以在任何通信层上运行并与任意数量的物联网设备集成。 RMF 架构的设计方式允许随着环境中自动化水平的提高而进行扩展。 系统和用户可以通过 API 和可定制的用户界面与 RMF 进行交互。 一旦部署在环境中,RMF 将通过允许共享资源和最小化集成来节省成本。 这是机器人开发人员和机器人客户一直在寻找的东西。 简而言之,RMF 如下:
RMF 如何创造奇迹?
我们将在本书后面的章节中更详细地探讨每个功能领域,但现在我们还想介绍一些在开发和与 RMF 集成时有用的其他实用程序。
RMF 演示
The demos 是 RMF 在各种环境中的功能演示。 该存储库可作为与 RMF 工作和集成的起点。
流量编辑器
Traffic Editor 是一个 GUI,用于创建和注释 RMF 中使用的平面图。
通过流量编辑器,您可以创建用于 RMF 的流量模式,并引入仿真模型来增强您的虚拟仿真环境。
可以轻松导出 .yaml
文件以在 Gazebo 中使用。
Free Fleet
Free Fleet 是一个开源机器人车队管理系统,适用于没有自己的车队管理器或更愿意使用开源车队管理实用程序并为其做出贡献的机器人开发人员。
RMF 时间表可视化工具
这 visualizer 是一个基于 rviz 的“rmf_core”可视化工具和控制面板。 它旨在成为 RMF 开发人员的功能工具。
RMF Web UI
rmf-web 是一个可配置的 Web 应用程序,可提供对 RoMi-H 系统的整体可视化和控制。 与前面提到的日程可视化工具相比,仪表板的设计更加“操作员友好”。
RMF Simulation
rmf_simulation 包含模拟 RMF 的模拟插件。插件可在gazebo
和ignition
中使用。
Simulation Assets
开源且可分发 simulation assets 创建和共享以加速模拟工作。
如需了解最新说明和更新,请直接查看 open-rmf/rmf repository.
安装 ROS 2.
首先,请按照 ROS 2 的安装说明进行操作。 如果您使用的是 Ubuntu 20.04 LTS 机器(建议), here is the binary install page for ROS 2 Galactic on Ubuntu 20.04.
设置 Gazebo 存储库
设置您的计算机以接受来自 packages.osrfoundation.org 的 Gazebo 包。
sudo apt update
sudo apt install -y wget
sudo sh -c 'echo "deb http://packages.osrfoundation.org/gazebo/ubuntu-stable `lsb_release -cs` main" > /etc/apt/sources.list.d/gazebo-stable.list'
wget https://packages.osrfoundation.org/gazebo.key -O - | sudo apt-key add -
二进制安装
OpenRMF 二进制包适用于 Ubuntu Focal 20.04 Foxy
, Galactic
和 Rolling
ROS 2 的版本。大多数 OpenRMF 软件包都有前缀 rmf
因此,你可以通过搜索模式找到它们 ros-<ro2distro>-rmf
apt-cache search ros-<ro2distro>-rmf
RMF 演示
安装的好方法 rmf
一次性安装一组软件包,就是安装主要软件包之一 RMF Demos 包。这将把所有其余的 OpenRMF 包作为依赖项。 RMF 演示的核心包含在 rmf_demos
包。但是,如果你想安装模拟支持,你应该安装 rmf_demos_gz
or rmf_demos_ign
分别带有 gazebo 或 ignition 支持的软件包。要安装带有 gazebo 支持包的 ROS 2 版本,您需要运行:
sudo apt install ros-<ro2distro>-rmf-demos-gz
从源代码构建
如果您想获得最新的开发成果,您可能需要从源代码安装并自行编译 OpenRMF。
附加依赖项
安装 OpenRMF 包的所有非 ROS 依赖项,
sudo apt update && sudo apt install \
git cmake python3-vcstool curl \
qt5-default \
-y
python3 -m pip install flask-socketio
sudo apt-get install python3-colcon*
安装 rosdep
rosdep
帮助安装跨不同发行版的 ROS 软件包依赖项。可以使用以下命令安装:
sudo apt install python3-rosdep
sudo rosdep init
rosdep update
下载源代码
设置新的 ROS 2 工作区并使用以下方式引入演示存储库 vcs
,
mkdir -p ~/rmf_ws/src
cd ~/rmf_ws
wget https://raw.githubusercontent.com/open-rmf/rmf/main/rmf.repos
vcs import src < rmf.repos
确保满足所有 ROS 2 先决条件,
cd ~/rmf_ws
rosdep install --from-paths src --ignore-src --rosdistro <ro2distro> -y
编译说明
NOTE: 由于源代码构建中发生了较新的更改,二进制文件安装的旧头文件可能会发生冲突和编译错误。请在从源代码构建之前删除二进制安装,使用
sudo apt remove ros-<ro2distro>-rmf*
.
编译于 Ubuntu 20.04
:
cd ~/rmf_ws
source /opt/ros/<ro2distro>/setup.bash
colcon build --cmake-args -DCMAKE_BUILD_TYPE=Release
NOTE: 首次构建时,将从 Ignition Fuel 下载许多模拟模型,以便在运行模拟时填充场景。 因此,首次构建可能需要很长时间,具体取决于服务器负载和您的 Internet 连接(例如 60 分钟)。
运行 RMF 演示
OpenRMF 的演示如下 rmf_demos.
Docker 容器
或者,你可以使用以下方式运行 RMF Demos docker.
从以下位置拉取 docker 镜像 open-rmf/rmf
github registry (setup refer here).
docker pull ghcr.io/open-rmf/rmf/rmf_demos:latest
docker tag ghcr.io/open-rmf/rmf/rmf_demos:latest rmf:latest
运行它!
docker run -it --network host rmf:latest bash -c "export ROS_DOMAIN_ID=9; ros2 launch rmf_demos_gz office.launch.xml headless:=1"
这将运行 rmf_demos
处于无头模式。打开 this link 使用浏览器启动任务.
(实验性)用户还可以运行 rmf_demos
以“非无头”图形形式,通过 rocker.
Roadmap
整个 OpenRMF 项目的近期路线图(包括及以后 rmf_traffic
) can be found in the user manual here.
与 RMF 集成
您可以找到有关如何将您的系统与 OpenRMF 集成的说明 here.
开源队列适配器
许多商业机器人已与 RMF 集成,其适配器的链接如下所示。
- Gaussian Ecobots
- OTTO Motors (and robots running the Clearpath Autonomy stack)
- Mobile Industrial Robots: MiR
- Temi- the personal robot
帮助我们添加到此列表!
将您的车队与 RMF 整合的一个有用起点是 fleet_adapter_template 包.
演示
在本章中,我们将简要演示 RMF 的功能 rmf_demos
.
这将让用户简要了解 RMF 的核心功能。
有关最新的 rmf_demos 运行说明,请参阅 here.
首先确保您已经安装了 Debian 包中提供的 RMF 演示:
# 使用 Gazebo 模拟器演示示例,使用 ros-foxy-rmf-demos-ign 进行点火
sudo apt-get install ros-foxy-rmf-demos-gz
运行您想要的演示。在本例中,我们将运行 airport terminal
:
在运行演示之前,我们可以通过以下方式确保所有必需的模型都已下载到本地:
ros2 run rmf_building_map_tools model_downloader rmf_demos_maps -s airport_terminal
ros2 launch rmf_demos_gz airport_terminal.launch.xml
# or with ignition
ros2 launch rmf_demos_ign airport_terminal.launch.xml
现在您应该能够在 Gazebo 中看到带有机器人的机场航站楼:
RMF Schedule Visualizer 应该已加载到 rviz 窗口中。此画布将显示 RMF 可用的所有集成机器人或基础设施。
在任务请求过程中,RMF 不会要求用户指定要完成任务的机器人名称,而是会将任务分配给最佳机器人。
RMF 目前支持 3 种类型的任务,即: loop
, delivery
or clean
. 用户可以通过 CLI 提交任务:
Loop Task
ros2 run rmf_demos_tasks dispatch_loop -s s07 -f n12 -n 3 --use_sim_time
Delivery Task
ros2 run rmf_demos_tasks dispatch_delivery -p mopcart_pickup -pd mopcart_dispenser -d spill -di mopcart_collector --use_sim_time
Clean Task
ros2 run rmf_demos_tasks dispatch_clean -cs zone_3 --use_sim_time
现在您可以观察到机器人在机场空间内漫游!
rmf_panel
观察和与 RMF 交互的另一种方式是通过网络 rmf_panel
. 打开网页: firefox https://open-rmf.github.io/rmf-panel-js/
or click here
您可以查看 RMF 下所有机器人的状态。要请求任务列表,请先选择 Airport
选项卡。用户可选择提交 (1) 临时任务或 (2) 任务列表。
将其复制粘贴到任务列表框。(或打开文件)
[{"task_type":"Delivery", "start_time":0, "description": {"option": "mop"}},
{"task_type":"Loop", "start_time":0, "description": {"num_loops":10, "start_name":"junction_north_west", "finish_name":"n14"}},
{"task_type":"Loop", "start_time":0, "description": {"num_loops":10, "start_name":"tinyRobot_n09", "finish_name":"s07"}},
{"task_type":"Clean", "start_time":0, "priority":0, "description":{"cleaning_zone":"zone_2"}}
]
然后点击提交,提交任务列表:
现在,请坐下来享受吧。
Jump in, the water is fine!
现在你已经了解了 RMF 的全部内容,是时候开始行动了。我们建议,如果你还没有阅读过,请花点时间阅读 RMF Demos 存储库,如果你想快速了解 RMF,请查看此 Mock Airport Terminal video demo (奥斯卡短片提名最受欢迎). 我们希望您发现 RMF 是一个有用的工具,可以帮助您扩展机器人部署和操作,我们期待未来的许多改进和贡献!
ROS 2 简介
在本章中,我们将介绍机器人操作系统 (ROS) 的基础知识,并为您提供构建、调试和理解机器人应用程序所需的所有工具。本章从决策者做出合理决策所需的最一般概念,到工程师开发新机器人应用程序所需的特定 API 参考。在高级概念和低级 API 命令之间,存在着维护和支持现场多机器人部署所需的知识。
学习 ROS 的一个很好的类比是学习机动车辆的过程。在实际的日常层面上,大多数人会学习如何启动车辆并在高速公路上安全使用它。对于这些人来说,学习 ROS 背后的高级概念以及特定于应用程序的命令可能就足够了。喜欢开车的人经常选择学习如何修理和保养他们的车辆。如果这是您的兴趣水平,我们建议您学习 ROS 命令行界面的基础知识。这将允许您“检查机器人系统的油”,并确保一切正常运行。最后,如果您是那种想要用更强大的引擎更换车辆引擎的人,或者可能从头开始制造一辆全新的车辆,那么 ROS API 就是一套可以实现这一目标的工具。一般来说,汽车工程师并不是完全成熟的,机器人专家也是如此。建议您在培养 ROS 技能的过程中 逐步理解每个阶段。
根据我们上面的类比,学习如何使用基于 ROS 构建的机器人系统的过程大致可以分为四个部分。本章使用 ROS 2 介绍这四个部分。后续章节将在此知识的基础上讨论具体应用的微妙之处。本章的四个部分如下。
-
对可用于帮助您学习过程的工具和资源进行元讨论。
-
对 ROS 中使用的设计模式进行高层次讨论。这些模式 大致类似于车辆中的子系统(发动机、 刹车、安全、气候控制等)。
-
ROS 的命令行界面 (CLI) 处理。CLI 是一组用于启动、检查、控制和监控 ROS 机器人的程序。您可以将本主题视为教您如何检查机器人的油,以及阅读仪表板。
-
ROS 应用程序编程接口简介。本节将向您展示如何创建自己的应用程序并修改现有软件以适应您的特定应用程序。
虽然本书旨在涵盖基础知识,但应该明确的是,ROS 与几乎所有软件一样,是一个不断变化的目标。技术发展迅速,虽然印刷媒体有助于提供高保真度的指导,但这种指导很快就会过时。因此,我们在本章的开头对可用于帮助您学习过程的 ROS 资源进行了元讨论。
ROS 启动
本章介绍了学习和获取 ROS 帮助的途径,以及如何通过设置和安装 ROS 来开始使用。
资源
有关 ROS 的最新信息可以在网络上找到。网上有大量资源可帮助您完成学习或实践之旅。需要记住的一点是,ROS 与大多数软件一样,有不同的版本,并且命令和 API 调用的格式和结构可能因版本而异(尽管开发人员试图保持尽可能稳定)。本书专门为 ROS 2、Eloquent Elusor 或 ROS Eloquent 编写,以简洁起见。
虽然 ROS 2 的新版本或旧版本通常相似,但值得注意的是发行版名称或版本号,因为版本之间存在变化。ROS 2 的主要版本通常对应于一个发行版,由一对匹配的字母形容词和与特定属和物种的海龟相关的特定名词表示(例如 Eloquent Elusor 或 Foxy Fitzroy)。值得注意的是,ROS 版本通常与 Ubuntu Linux 的特定版本挂钩。
ROS 伴随着现代网络而成长,因此它拥有各种资源和论坛,可帮助您解决问题并了解 API 和工具。我们的一些网络资源实际上早于更广泛使用的系统,因此了解它们的位置和使用方法会很有帮助。对于 ROS 用户来说,网络上最重要的资源可能是 answers.ros.org. Answers 是一个类似于 StackOverflow 的问答网站。注册 Answers 后,您可以提出或回答任何与 ROS 相关的问题。请注意,提出好问题可能很困难。您应该包含尽可能多的信息,以帮助其他人回答您的问题。这意味着您应该包含 ROS 版本、平台版本、您拥有的任何调试或堆栈跟踪信息以及有问题的源代码。
除了 ROS Answers 之外,您还应该查看 ROS 2 教程和 API 文档以及 ROS 1 wiki。ROS 1 wiki 可在以下网址找到 wiki.ros.org. 虽然它专门针对 ROS 1,但其中许多信息仍然与 ROS 2 相关。如果您正在搜索最新的 ROS 2 信息,您可以访问以下来源的 ROS 2 教程和 API 文档: index.ros.org/doc/ros2. 本书中的许多教程都直接取自此作品。如果您想了解最新的 ROS 新闻并讨论各种 ROS 功能,请访问 ROS Discourse 论坛 discourse.ros.org 是您的最佳选择。ROS discourse 是社区中心,开发人员可以在此讨论他们的最新项目 并讨论 ROS 开发的细节。
对于 ROS 应用程序开发人员,有许多工具可帮助您与更广泛的 ROS 开发人员社区建立联系。Open Robotics 支持 index.ros.org, 这是按版本排序的 ROS 软件包的扩展列表。如果您正在搜索特定硬件的 ROS 驱动程序,那么索引是一个很好的起点。如果您发现测试失败的软件包,或者想要了解任何 ROS 软件包的构建状态,请查看 build.ros.org. 同样,对于未编入索引的包 GitHub maintains a ROS code tag. 此标签将允许您搜索所有公开列出的标记存储库。 在撰写本文时,GitHub 上列出了近 4000 个存储库,因此您很有可能找到所需的内容。
最后,您应该了解各种非官方资源,它们可能很有用,特别是如果您想随时了解最新的 ROS 项目和功能。 Open Robotics and ROS 维护 Twitter 信息流以分享最新 新闻。我们每年还会举办一次 ROS 开发者大会,名为 ROSCon; 大多数讲座都可以在网上免费获取。还有一些其他资源也很有用,包括 ROS subreddit 以及“非官方” ROS Discord.
设置你的计算机
在本章中,我们假设您正在使用具有独立显卡的现代台式机。虽然本章不需要显卡,但后面的章节将涉及大量图形,拥有一张显卡将大大改善最终用户体验。此外,本书假设您正在使用 Ubuntu Linux 18.04 操作系统。虽然 ROS 2 支持其他操作系统,但本书中的所有教程和说明都假设您正在运行 Linux。如果您使用的是 Mac 或 Windows PC,则可以使用 ROS 2 installation instructions 页面。在 Mac 和 PC 上安装的另一种方法是使用虚拟机。大致过程如下:
- 安装虚拟机软件如 Virtual Box or VMWare 在您的主机上。
- 使用软件创建虚拟机并安装 Desktop Ubuntu 18.04 Bionic Beaver from the Canonical website. 按照您的意愿配置安装。
- 现在启动虚拟机并以用户身份登录。以下说明应该适用。
对于这些初始教程,我们将使用预编译的 ROS 2:
Eloquent Elusor 桌面版。这些说明直接来自
安装说明,可在 [Eloquent installation
page]https://index.ros.org/doc/ros2/Installation/Eloquent/Linux-Install-Debians/). 要运行这些命令,您需要一个终端窗口。要在 Ubuntu 18.04 中打开终端,请单击屏幕左下角的九个点。应该会出现一个对话框。输入单词 terminal 并单击终端图标以打开终端。或者,您可以同时按下 control、alt 和 't' 键来打开终端 (we abbreviate this CTRL-ALT-T
).
设置区域设置
第一步是确保你有一个支持 UTF-8
. 这意味着我们将检查您计算机使用的语言是否使用特定的文本格式。如果您处于最小环境(例如 Docker 容器),则语言环境可能为 POSIX 等最小语言环境。我们使用以下设置进行测试。如果您使用其他支持 UTF-8 的语言环境,则应该没问题。
sudo locale-gen en_US en_US.UTF-8
sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
export LANG=en_US.UTF-8
设置源
您需要将 ROS 2 apt 存储库添加到您的系统中。开箱即用的 Ubuntu 不知道 ROS 2 二进制程序位于何处,因此我们必须为其提供一个 安全的位置。为此,计算机将提示您输入 root 密码。对于更多技术读者,我们需要通过在终端中输入以下命令来使用 apt 授权 ROS GPG 密钥:
sudo apt update && sudo apt install curl gnupg2 lsb-release
curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | sudo apt-key add -
安装 ROS 2 软件包
安装 ROS 的下一步是进行系统更新(即检查是否有较新的程序),然后安装 ROS Eloquent。为此,我们运行以下命令。请注意,这些命令将下载大量数据,可能需要一段时间。最好在您的家庭网络上运行这些命令。
sudo apt update
桌面安装(推荐):ROS、RViz、演示、教程。
sudo apt install ros-eloquent-desktop
接下来我们将安装一组名为 TurtleSim
. 为此,我们运行
另一个 apt 命令。
sudo apt install ros-eloquent-turtlesim
ROS 2 命令行工具使用 argcomplete 进行自动完成。如果您想要自动完成,则必须安装 argcomplete。我们还将安装一些其他工具,以使我们的生活更轻松。
sudo apt install python3-argcomplete htop byobu
检查您的安装
ROS 使用环境变量
来帮助跟踪正在运行的 ROS 版本以及计算机上使用 ROS 的所有程序的位置。要设置这些环境变量,我们需要source
或加载一个 bash 脚本文件。bash 脚本文件并不神奇;它是一系列输入到终端的命令,就像我们刚刚输入的用于设置 ROS 的一系列命令一样。一台计算机上可以运行不同版本的 ROS。使用错误版本的 ROS 会导致各种问题,这是新用户常犯的错误!如果您遇到问题,请尝试获取正确的 ROS bash 文件。从现在开始,每当您打开新终端时,您都需要告诉计算机要使用哪个版本的 ROS。要为 ROS 设置必要的环境变量,您需要在每次打开新终端时source
一个 bash 文件。是的,这很烦人,但这是一种合理的方法,因为它
使你正在使用的 ROS 版本明确。在 Ubuntu 18.04 上,所有版本的 ROS 都位于 /opt/ros/
中。此目录中将有一个用于运行 ROS 的程序和脚本
文件。要告诉操作系统我们想要使用 ROS Eloquent
我们只需使用以下命令获取 ROS Eloquent setup.bash
文件:
source /opt/ros/eloquent/setup.bash
一旦该命令运行,您的终端就应该准备好运行 ROS 程序了。让我们通过运行两个名为talker
和listener
的小型 ROS 程序来测试我们的安装。这两个程序将使用 ROS 来回发送数据以执行通信。一个程序是用 C++ 编写的,另一个是用 Python 编写的。运行这两个不同的程序是一种快速简便的方法来检查您的 ROS 系统是否配置正确。要启动 talker,请运行以下命令:
source /opt/ros/eloquent/setup.bash
ros2 run demo_nodes_cpp talker
如果一切正常,您应该会看到类似下面的内容:
[INFO] [talker]: Publishing: 'Hello World: 1'
[INFO] [talker]: Publishing: 'Hello World: 2'
[INFO] [talker]: Publishing: 'Hello World: 3'
....
现在,让我们启动监听器。我们将在此示例中使用 Python 监听器,以确保我们正确安装了 Python。首先,我们需要第二个终端。我们可以在终端中输入 CTRL-SHIFT-T
打开一个新的终端选项卡。我们还可以通过按 CTRL-ALT-T
创建一个全新的终端。选择最适合您的方式。现在,在新终端中输入您的 bash 文件并运行以下命令:
source /opt/ros/eloquent/setup.bash
ros2 run demo_nodes_py listener
如果一切正常,您应该会看到类似下面的内容:
[INFO] [listener]: I heard: [Hello World: 264]
[INFO] [listener]: I heard: [Hello World: 265]
[INFO] [listener]: I heard: [Hello World: 266]
现在我们已经测试了我们的 ROS 安装,我们可以停止这两个程序了。在 ROS 中,大多数程序都会无限循环运行,直到机器人关闭。要停止这些程序,我们导航到运行程序的终端,然后同时按下 Ctrl
和 C
键。我们称此组合为 CTRL-C
,您可以使用它来停止终端中的几乎任何程序。使用它来停止 talker 和 listener 程序。
ROS 概念和设计模式
正如我们所说,学习 ROS 类似于学习汽车。事实上,汽车很像机器人(有时它真的就是机器人;参见庞大而活跃的自动驾驶汽车行业)。现代汽车由许多相互连接的部分组成。方向盘连接到前轴,制动踏板连接到制动钳,氧气传感器连接到燃油喷射器,等等。从这个角度来看,汽车是一个分布式系统:每个部分都扮演着明确的角色,根据需要与其他部分进行通信(无论是电气还是机械),这种交响乐般的协调的结果就是一辆正常工作的汽车。
ROS 的一个关键哲学原则是,机器人软件也应该作为分布式系统进行设计和开发。我们的目标是将复杂系统的功能分离为相互交互的各个部分,以产生该系统所需的行为。在 ROS 中 我们将这些部分称为节点,并将它们之间的交互称为 主题(有时也称为服务,但我们稍后会讲到)。
ROS 通信图
假设我们正在构建一个追逐红球的轮式机器人。这个机器人需要一个摄像头来观察球,一个视觉系统来处理摄像头图像以确定球的位置,一个控制系统来决定移动的方向,以及一些电机来移动轮子,使其向球移动。使用 ROS,我们可以像这样构建系统:
这种设计将软件分为四个 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 中正在发生的事情时
节点,查看回调;真正的工作就在这里发生。
The ROS Command Line Interface
ROS 命令行界面(简称 CLI)是一组用于启动、检查、控制和监控 ROS 机器人的程序。理解 CLI 的最佳方式是,它是一组小型和简单的程序,可让您在 ROS 中执行基本任务。从汽车类比来看,CLI 可以被认为是车辆的子系统:刹车、变速器、雨刷,所有这些较小的部件组合在一起构成了更大的车辆。我们将在本节中向您展示如何启动汽车、挂档、打开收音机,以及检查机油以执行日常维护。ROS 2 CLI 大量借鉴了 Unix/Linux 的理念,即可以组合在一起的小程序。如果您熟悉 Unix 和 Linux 中的命令行界面,或者在较小程度上熟悉 MacOS 或 Windows 中的命令行界面,那么您会感到非常熟悉。
ROS 命令行工具大量借鉴了上一节中提到的设计模式,并直接与我们将在下一节中介绍的 API 交互。CLI 接口本质上只是一组基于 ROS 2 API 构建的简单工具;此 API 只是我们在上一节中讨论的高级模式的实现。如果您的目标只是与使用 ROS 编写的特定软件交互,则 CLI 接口就是您启动、停止和控制底层 ROS 软件的方式。对于更高级的用户,这些工具将允许您通过探索系统中的底层软件流程来研究 ROS 系统。
本节中您只需记住两件事。 第一个命令只是告诉您的计算机您正在使用 ROS,以及您想要使用哪个版本的 ROS。让我们来看看 这个神奇的命令:
source /opt/ros/eloquent/setup.bash
如果一切正常,此命令应该会返回。您看不到任何内容 发生,但在幕后,您刚刚告诉这个特定的 shell,您正在使用 ROS 2 Eloquent Elusor,以及所有 ROS 程序和文件所在的位置。您应该计划在每次想要使用 ROS 时执行此操作。新用户最常犯的错误是没有运行此命令。如果您不确定是否在 shell 中运行了此命令,那没关系。该命令是幂等的;连续运行两次不会破坏任何东西。您可以连续运行一百万次, 也不会有任何区别。
您需要记住的另一个命令是ros2
。ROS 2 CLI 中的几乎所有内容都以ros2
开头。继续在刚刚获取安装文件的同一 shell 中尝试它。
如果一切正常,您应该会看到以下内容:
$ ros2
usage: ros2 [-h] Call `ros2 <command> -h` for more detailed usage. ...
ros2 is an extensible command-line tool for ROS 2.
optional arguments:
-h, --help show this help message and exit
Commands:
action Various action related sub-commands
component Various component related sub-commands
daemon Various daemon related sub-commands
doctor Check ROS setup and other potential issues
interface Show information about ROS interfaces
launch Run a launch file
lifecycle Various lifecycle related sub-commands
msg Various msg related sub-commands
multicast Various multicast related sub-commands
node Various node related sub-commands
param Various param related sub-commands
pkg Various package related sub-commands
run Run a package specific executable
security Various security related sub-commands
service Various service related sub-commands
srv Various srv related sub-commands
topic Various topic related sub-commands
wtf Use `wtf` as alias to `doctor`
Call `ros2 <command> -h` for more detailed usage.
通过这一个命令,您可以了解每个 ROS 2 CLI 程序的作用以及
如何使用它。ROS 2 CLI 的语法与大多数语言一样。所有 ROS CLI 命令都以
ros2
开头,后跟一个命令。命令后可以有许多其他内容
;您可以附加 --help
或 -h
来查看文档并找出任何命令需要的参数。
本节的其余部分只是逐一介绍每个命令。
使用命令行编写命令很棘手,而且容易出错。您可以使用一些工具使这个过程更加顺畅。第一个是 TAB
键,它会尝试自动完成您输入的任何内容。它无法读懂您的想法,但对于常见的命令组合,您通常只需要输入前一个或两个字母。另一个工具是向上箭头键。当您使用命令行时,有时您会输错命令,或者需要重新运行命令。按向上键将循环显示以前的命令,您可以根据需要修改并重新运行这些命令。
运行你的第一个 ROS 程序
让我们开始使用第一个 ROS CLI 命令。我们将访问的第一个命令是run
。让我们先查看run
命令的文档:
$ ros2 run
usage: ros2 run [-h] [--prefix PREFIX] package_name executable_name ...
ros2 run: error: the following arguments are required: package_name, executable_name, argv
要获取有关 ROS 2 命令的更完整信息,只需在命令中添加 --help
即可向命令寻求帮助。让我们再试一次:
$ ros2 run --help
usage: ros2 run [-h] [--prefix PREFIX] package_name executable_name ...
Run a package specific executable
positional arguments:
package_name Name of the ROS package
executable_name Name of the executable
argv Pass arbitrary arguments to the executable
optional arguments:
-h, --help show this help message and exit
--prefix PREFIX Prefix command, which should go before the executable.
Command must be wrapped in quotes if it contains spaces
(e.g. --prefix 'gdb -ex run --args').
我们可以看到,ros2 run
是“运行包特定的可执行文件”的命令。在 ROS 2 中,ROS 软件集合被收集到称为“包”的逻辑单元中。每个包都包含包的所有源代码以及各种其他数据,这些数据告诉 ROS 如何构建和编译包以及可以在包中找到的所有程序(也称为“可执行文件”)的名称。描述下方的行给出了包的_位置参数_。位置参数是位于 ros2
和您运行的命令之后的单词和值。在这种情况下,我们要编写的命令句的语法如下:
ros2 run <package name> <program/executable name> <args>
这里缺少一条信息。该命令要求的 argv
是什么?argv
元素是程序员对可变参数的简写,它只是意味着“由可执行文件确定的一些附加参数”。值得注意的是,程序可以有零个参数,您可以将其留空。这实际上是许多程序的工作方式。例如,假设我们有一个名为 math 的包,以及一个名为 add 的可执行文件,它接受两个数字并返回结果。在这种情况下,argv 将是要添加的两个数字。最终命令如下所示:
ros2 run math add 1 2
最后,在位置参数下面我们有_可选参数_。 除非需要,否则您不需要包含它们。
现在我们已经查看了帮助文件,让我们运行我们的第一个 ROS 程序。对于
这些教程,我们将使用一个名为turtlesim
的包,我们要运行的程序是turtlesim_node
。让我们运行这个程序(记住您的制表符已完成!)。您的命令应如下所示:
ros2 run turtlesim turtlesim_node
如果一切顺利,您应该看到以下内容:
[INFO] [turtlesim]: Starting turtlesim with node name /turtlesim
[INFO] [turtlesim]: Spawning turtle [turtle1] at x=[5.544445], y=[5.544445], theta=[0.000000]
还会弹出一个窗口,上面有一只可爱的小乌龟,如下所示:
ROS 的真正强大之处不在于它可以运行一个程序,而在于它可以同时运行 许多程序,这些程序共同控制一个机器人或 多个机器人,共同工作。为了说明这一点,让我们运行第二个 ROS 程序,让我们的小乌龟四处移动。
为此,我们首先打开一个新终端(使用CTRL-SHIFT-T
)。接下来,我们将使用“source”命令告诉该终端我们想要使用 ROS Eloquent。最后,我们将运行turtlesim
包中的另一个程序来绘制正方形。看看你是否能自己找到该程序(提示:使用TAB
)。如果一切正常,你应该已经输入了以下内容,并且应该可以看到以下输出:
$ source /opt/ros/eloquent/setup.bash
$ ros2 run turtlesim draw_square
[INFO] [draw_square]: New goal [7.544445 5.544445, 0.000000]
[INFO] [draw_square]: Reached goal
[INFO] [draw_square]: New goal [7.448444 5.544445, 1.570796]
[INFO] [draw_square]: Reached goal
你的屏幕看起来大致如下:
值得注意的是,您可以通过在终端中同时按下 Ctrl
和 C
键来停止任何 ROS 程序;我们称之为 CTRL-C
(请注意,CTRL-SHIFT-C
和 CTRL-SHIFT-V
负责在 Linux 终端中复制和粘贴)。
请随意尝试。启动和停止程序,然后
重新启动它们,然后再继续。
ROS Topics
我们现在有两个 ROS 2 程序从 turtlesim
包运行。
turtle_node
用于打开我们的海龟模拟,draw_square
用于使 turtle_node
中的海龟移动。这两个
程序如何通信?
ROS 程序(也称为_节点_)通过 ROS _消息总线_上的 _主题_进行通信。ROS _主题_使用命名空间来区分自己。 例如,在运行 ROS 的车辆中,每个车轮的位置可以 按以下方式组织:
/wheels/front/driver/velocity
/wheels/front/passenger/velocity
/wheels/rear/driver/velocity
/wheels/rear/passenger/velocity
关于主题,关键是要认识到它们包含的数据是动态的,这意味着它会不断变化。在我们的车辆示例中,每个车轮的速度可能每秒测量一千次或更多次。由于 ROS 主题中的数据不断变化,因此主题的一个重要区别是主题是“创建”还是像我们在 ROS 中所说的那样publishing
,或者它是否正在读取数据,我们称之为subscribing
主题。许多 ROS 节点订阅一组主题,处理输入数据,然后发布到另一组主题。
让我们回到我们的 turtlesim 示例,看看我们是否可以使用 ROS CLI 来
了解主题、发布者和订阅者。
要查看 topic
命令的子命令和语法,我们将运行:ros2 topic --help
。
此命令输出以下内容:
$ ros2 topic --help
usage: ros2 topic [-h] [--include-hidden-topics]
Call `ros2 topic <command> -h` for more detailed usage. ...
Various topic related sub-commands
optional arguments:
-h, --help show this help message and exit
--include-hidden-topics
Consider hidden topics as well
Commands:
bw Display bandwidth used by topic
delay Display delay of topic from timestamp in header
echo Output messages from a topic
find Output a list of available topics of a given type
hz Print the average publishing rate to screen
info Print information about a topic
list Output a list of available topics
pub Publish a message to a topic
type Print a topic's type
Call `ros2 topic <command> -h` for more detailed usage.
有相当多的子命令;我们不会讨论所有子命令,但让我们仔细看看其中几个。
子命令有自己的帮助命令。我们为什么不检查一下list
命令呢?重复我们的命令模式,让我们尝试运行ros2 topic list --help
。
usage: ros2 topic list [-h] [--spin-time SPIN_TIME] [-t] [-c]
[--include-hidden-topics]
Output a list of available topics
optional arguments:
-h, --help show this help message and exit
--spin-time SPIN_TIME
Spin time in seconds to wait for discovery (only
applies when not using an already running daemon)
-t, --show-types Additionally show the topic type
-c, --count-topics Only display the number of topics discovered
--include-hidden-topics
Consider hidden topics as well
正如此命令帮助文件顶部所示,ros2 topic list
将
“输出可用主题列表”。似乎有各种
可选 参数,如果我们不想的话,我们不需要包含它们。但是,
-t, --show-types
行看起来很有趣。值得注意的是,命令
参数(有时称为标志)可以有两种类型。短格式用单破折号(“-”)表示,长格式用双破折号
(“--”)表示。别担心,尽管两个版本的参数看起来不同,但它们的作用相同。让我们尝试运行此命令,子命令对与
-show-types
参数。
$ ros2 topic list --show-types
/parameter_events [rcl_interfaces/msg/ParameterEvent]
/rosout [rcl_interfaces/msg/Log]
/turtle1/cmd_vel [geometry_msgs/msg/Twist]
/turtle1/color_sensor [turtlesim/msg/Color]
/turtle1/pose [turtlesim/msg/Pose]
在左侧,我们可以看到系统上运行的所有 ROS 主题,每个主题都以/
开头。我们可以看到,它们中的大多数都聚集在/turtle1/
组中。该组定义了我们屏幕上的小海龟的所有输入和输出。主题名称右侧括号([]
)中的单词定义了主题上使用的消息。我们的车轮示例很简单,我们只发布速度,但 ROS 允许您发布由_消息类型_定义的更复杂的数据结构。当我们添加--show-types
标志时,我们告诉命令包含此信息。稍后我们将详细探讨消息。
最常用的主题子命令之一是
info
。毫不奇怪,info
提供有关主题的信息。让我们使用 ros2 topic info --help
查看其
帮助文件
$ ros2 topic info --help
usage: ros2 topic info [-h] topic_name
Print information about a topic
positional arguments:
topic_name Name of the ROS topic to get info (e.g. '/chatter')
optional arguments:
-h, --help show this help message and exit
这看起来非常简单。让我们在 /turtle1/pose
上运行它来尝试一下
$ ros2 topic info /turtle1/pose
Type: turtlesim/msg/Pose
Publisher count: 1
Subscriber count: 1
这个命令告诉我们什么?首先,它告诉我们 /turtle1/pose
主题的 消息类型,即 /turtlesim/msg/Pose
。由此我们可以确定消息类型来自 turtlesim 包,其类型为 Pose
。ROS 消息具有预定义的消息类型,可以由不同的编程语言和不同的节点共享。我们还可以看到,这个主题有一个发布者,也就是说,一个节点在主题上生成数据。该主题还有一个订阅者,也称为侦听器,它正在处理传入的姿势数据。
如果我们只想知道某个主题的消息类型,那么有一个专门用于该主题的子命令,名为type
。让我们看一下它的帮助文件及其结果:
$ ros2 topic type --help
usage: ros2 topic type [-h] topic_name
Print a topic's type
positional arguments:
topic_name Name of the ROS topic to get type (e.g. '/chatter')
optional arguments:
-h, --help show this help message and exit
kscottz@kscottz-ratnest:~/Code/ros2multirobotbook$ ros2 topic type /turtle1/pose
turtlesim/msg/Pose
虽然它不是 topic
命令的一部分,但我们值得简要地跳过并查看一个特定的命令,子命令对,即 interface
命令和 show
子命令。此子命令将打印与消息类型相关的所有信息,以便您更好地了解在主题上移动的数据。在前面的例子中,我们看到 topic type
子命令告诉我们 /turtle1/pose
主题的类型为 turtlesim/msg/Pose
。但是 turtlesim/msg/Pose
数据是什么样的?我们可以通过运行 ros2 interface show
子命令并将消息类型名称作为输入来查看此主题传输的数据结构。让我们看看这个子命令的帮助及其输出:
$ ros2 interface show --help
usage: ros2 interface show [-h] type
Output the interface definition
positional arguments:
type Show an interface definition (e.g. "std_msgs/msg/String")
optional arguments:
-h, --help show this help message and exit
$ ros2 interface show turtlesim/msg/Pose
float32 x
float32 y
float32 theta
float32 linear_velocity
float32 angular_velocity
我们可以看到值 x
和 y
是我们的海龟的位置坐标,
并且它们的类型为 float32
。
theta
是头部指向的方向。
接下来的两个值 linear_velocity
和 angular_velocity
分别表示海龟移动的速度和转动的速度。总而言之,这条消息告诉我们海龟在屏幕上的位置、它要去哪里以及它移动或旋转的速度。
现在我们知道了简单的 turtlesim 上的 ROS 主题及其消息类型,我们可以深入研究并了解更多有关一切工作原理的信息。如果我们回顾我们的主题子命令,我们可以看到一个名为“echo”的子命令。Echo 是计算机术语,意思是“重复”某事。如果您回显某个主题,则意味着您希望 CLI 重复该主题上的内容。让我们看看“echo”子命令的帮助文本:
$ ros2 topic echo --help
usage: ros2 topic echo [-h]
[--qos-profile {system_default,sensor_data,services_default,parameters,parameter_events,action_status_default}]
[--qos-reliability {system_default,reliable,best_effort}]
[--qos-durability {system_default,transient_local,volatile}]
[--csv] [--full-length]
[--truncate-length TRUNCATE_LENGTH] [--no-arr]
[--no-str]
topic_name [message_type]
Output messages from a topic
positional arguments:
topic_name Name of the ROS topic to listen to (e.g. '/chatter')
message_type Type of the ROS message (e.g. 'std_msgs/String')
optional arguments:
-h, --help show this help message and exit
--qos-profile {system_default,sensor_data,services_default,parameters,parameter_events,action_status_default}
Quality of service preset profile to subscribe with
(default: sensor_data)
--qos-reliability {system_default,reliable,best_effort}
Quality of service reliability setting to subscribe
with (overrides reliability value of --qos-profile
option, default: best_effort)
--qos-durability {system_default,transient_local,volatile}
Quality of service durability setting to subscribe
with (overrides durability value of --qos-profile
option, default: volatile)
--csv Output all recursive fields separated by commas (e.g.
for plotting)
--full-length, -f Output all elements for arrays, bytes, and string with
a length > '--truncate-length', by default they are
truncated after '--truncate-length' elements with
'...''
--truncate-length TRUNCATE_LENGTH, -l TRUNCATE_LENGTH
The length to truncate arrays, bytes, and string to
(default: 128)
--no-arr Don't print array fields of messages
--no-str Don't print string fields of messages
哇,功能真多。帮助文件的顶部说这个 CLI 程序“输出来自主题的消息”。当我们扫描位置参数时,我们会看到一个必需参数、一个主题名称和一个可选消息类型。我们知道消息类型是可选的,因为它周围有方括号([]
)。在处理一些可选元素之前,让我们先试一下这个简单案例。要记住两件事:第一,主题很长,很容易弄乱,所以请使用TAB
键。第二,这将快速打印大量数据。您可以使用CTRL-C
停止命令并停止所有输出。让我们看看/turtle1/pose
主题。
$ ros2 topic echo /turtle1/pose
x: 5.4078755378723145
y: 7.081490516662598
theta: -1.0670461654663086
linear_velocity: 1.0
angular_velocity: 0.0
---
x: 5.4155988693237305
y: 7.067478179931641
theta: -1.0670461654663086
linear_velocity: 1.0
angular_velocity: 0.0
---
x: 5.423322677612305
y: 7.053465843200684
theta: -1.0670461654663086
linear_velocity: 1.0
angular_velocity: 0.0
---
<<GOING ON FOREVER>>
让我们来看看到底发生了什么。在破折号(---
)之间是一条关于我们主题的 ROS 消息。如果仔细检查这些数字,您会发现它们正在变化,并且与乌龟的运动有关。回到我们的汽车示例,您可以看到这对于理解每个车轮的瞬时速度非常有用。
现在我们已经掌握了基础知识,让我们深入研究一些可选参数。我们看到各种以--qos
开头的命令。这里的“QOS”表示“服务质量”,这是一个非常酷的功能,仅在 ROS 2 中才有。不用太技术性,QOS 是一种要求一定程度的网络稳健性的方式。ROS 系统可以通过网络运行,就像流媒体视频或视频游戏一样,数据包可能会被丢弃或无法到达目的地。操作系统设置可帮助您控制哪些数据包最重要,应该获得最高优先级。
大多数其他命令都用于更改此 CLI 程序的输出格式,但有一个命令特别方便,它也是 ROS 2 中的新功能。--csv
标志代表“逗号分隔值”,它是一种定义电子表格的非常简单的方法。此参数的作用是使主题 echo 命令以逗号分隔值格式输出数据。许多命令行允许您将数据从屏幕发送到文件,保存数据以供以后查看或分析。要在 Linux 中执行此文件保存,我们使用 >
字符后跟文件名。以下是使用 --csv
参数的两个示例:
$ ros2 topic echo /turtle1/pose --csv
7.097168922424316,8.498645782470703,2.442624092102051,0.0,0.4000000059604645
7.097168922424316,8.498645782470703,2.449024200439453,0.0,0.4000000059604645
...
<<CTRL-C>>
$ ros2 topic echo /turtle1/pose --csv > mydata.csv
<<nothing happens>>
<<CTRL-C>>
上面的第二个命令创建了一个名为mydata.csv
的文件。您可以使用名为less
的 CLI 实用程序查看它(按 q 退出),或者使用您最喜欢的电子表格工具打开它。
现在我们已经看过了ros2 topic echo
,让我们来看看其他几个主题子命令。您可能已经注意到的一件事是主题可以输出大量数据!更复杂的机器人,比如自动驾驶汽车,可以以其产生的数据量使高速互联网连接饱和。有两个主题子命令可用于诊断性能问题。第一个子命令是topic hz
,它是赫兹的缩写,赫兹是频率的单位。Hz
子命令将告诉您特定主题生成消息的频率。同样,还有topic bw
子命令,其中bw
代表带宽,这是一个与生成的数据量相关的工程术语。高带宽连接可以传输更多数据(例如高清视频),而低带宽连接则可能传输广播节目。让我们来看看这两个命令的帮助:
$ ros2 topic hz --help
usage: ros2 topic hz [-h] [--window WINDOW] [--filter EXPR] [--wall-time]
topic_name
Print the average publishing rate to screen
positional arguments:
topic_name Name of the ROS topic to listen to (e.g. '/chatter')
optional arguments:
-h, --help show this help message and exit
--window WINDOW, -w WINDOW
window size, in # of messages, for calculating rate
(default: 10000)
--filter EXPR only measure messages matching the specified Python
expression
--wall-time calculates rate using wall time which can be helpful
when clock is not published during simulation
$ ros2 topic bw --help
usage: ros2 topic bw [-h] [--window WINDOW] topic
Display bandwidth used by topic
positional arguments:
topic Topic name to monitor for bandwidth utilization
optional arguments:
-h, --help show this help message and exit
--window WINDOW, -w WINDOW
window size, in # of messages, for calculating rate
(default: 100)
bw
和 hz
都遵循相同的模式,它们只是接受一个主题名称,后面跟着几个可选参数。唯一值得注意的参数是 window
参数。这两个命令都计算一系列消息的统计数据;计算这些统计数据时要使用多少条消息就是窗口大小。window
的默认值为 100,因此当您调用 ros2 topic bw
时,它将首先收集 100 条消息,然后使用该数据计算平均消息大小。让我们试一试(使用 TAB
完成,使用 CTRL-C
退出)。
$ ros2 topic hz /turtle1/pose
average rate: 60.021
min: 0.001s max: 0.073s std dev: 0.00731s window: 65
average rate: 61.235
min: 0.001s max: 0.073s std dev: 0.00523s window: 128
$ ros2 topic bw /turtle1/pose
Subscribed to [/turtle1/pose]
average: 1.44KB/s
mean: 0.02KB/s min: 0.02KB/s max: 0.02KB/s window: 46
average: 1.52KB/s
mean: 0.02KB/s min: 0.02KB/s max: 0.02KB/s window: 100
如上所示,hz
命令表示主题以 60.021 的频率发布消息,其中单位为 hz,即每秒 60.021 次。请注意,该命令以平均值的形式给出发布频率,后跟最小值、最大值和标准偏差(以秒为单位)。带宽子命令非常相似;我们可以看到该主题每秒产生 1.44 千字节的数据。此命令在最小值、最大值和平均值方面具有类似的输出。
探索主题时,一个方便的工具是了解它们的类型。虽然我们已经查看了 interface
命令以查看组成主题的整数类型,但 topic
命令既有查询主题类型的工具,也有搜索所有主题以查找特定类型的方法。如果您只想知道主题的类型,则可以使用 type
命令,它将返回一个类型,然后可以使用 interface
命令进一步探索该类型。如果您想知道哪些主题使用特定消息类型,则可以使用 topic find
命令、子命令对。topic type
和 topic interface
命令、子命令对的可选参数集非常有限,因此我们只需为它们提供所需的主题或消息类型即可。让我们一起看看这两个命令:
$ ros2 topic type --help
usage: ros2 topic type [-h] topic_name
Print a topic's type
positional arguments:
topic_name Name of the ROS topic to get type (e.g. '/chatter')
optional arguments:
-h, --help show this help message and exit
$ ros2 topic type /turtle1/pose
turtlesim/msg/Pose
$ ros2 topic find --help
usage: ros2 topic find [-h] [-c] [--include-hidden-topics] topic_type
Output a list of available topics of a given type
positional arguments:
topic_type Name of the ROS topic type to filter for (e.g.
'std_msg/msg/String')
optional arguments:
-h, --help show this help message and exit
-c, --count-topics Only display the number of topics discovered
--include-hidden-topics
Consider hidden topics as wel
$ ros2 topic find turtlesim/msg/Pose
/turtle1/pose
它允许您从命令行向任何 ROS 主题发布命令。虽然您不需要经常使用此命令,但在构建机器人系统时,它对于测试和调试特别方便。pub
命令有许多可选参数,允许您发送一条或多条消息,并具有不同的服务质量 (QoS) 预设。该命令的格式为 ros2 topic pub TOPIC_NAME MESSAGE_TYPE VALUES
,这意味着要使其成功运行,您必须包含目标主题、主题的消息类型以及消息的值。消息的值以 YAML 格式指定,我们可以使用 interface show
命令来了解格式。为了说明此命令的实用性,我们将通过发布到 /turtle1/cmd_vel/
主题来发出一条消息来旋转和停止我们的海龟。在构建命令之前,我们先来看一下 topic pub
文档:
$ ros2 topic pub --help
usage: ros2 topic pub [-h] [-r N] [-p N] [-1] [-n NODE_NAME]
[--qos-profile {system_default,sensor_data,services_default,parameters,parameter_events,action_status_default}]
[--qos-reliability {system_default,reliable,best_effort}]
[--qos-durability {system_default,transient_local,volatile}]
topic_name message_type [values]
Publish a message to a topic
positional arguments:
topic_name Name of the ROS topic to publish to (e.g. '/chatter')
message_type Type of the ROS message (e.g. 'std_msgs/String')
values Values to fill the message with in YAML format (e.g.
"data: Hello World"), otherwise the message will be
published with default values
optional arguments:
-h, --help show this help message and exit
-r N, --rate N Publishing rate in Hz (default: 1)
-p N, --print N Only print every N-th published message (default: 1)
-1, --once Publish one message and exit
-n NODE_NAME, --node-name NODE_NAME
Name of the created publishing node
--qos-profile {system_default,sensor_data,services_default,parameters,parameter_events,action_status_default}
Quality of service preset profile to publish with
(default: system_default)
--qos-reliability {system_default,reliable,best_effort}
Quality of service reliability setting to publish with
(overrides reliability value of --qos-profile option,
default: system_default)
--qos-durability {system_default,transient_local,volatile}
Quality of service durability setting to publish with
(overrides durability value of --qos-profile option,
default: system_default)
由于我们想要手动移动我们的乌龟,我们将使用 --once
标志发出一次命令。值得注意的是,用于控制乌龟速度的消息类型很复杂,因为它由其他消息类型组成,所以我们必须查询基本消息类型。以下是我们将要做的事情的粗略总结:
- 使用
ros2 topic type
打印cmd_vel
主题类型,即geometry_msgs/msg/Twist
- 使用
interface show
确定Twist
消息类型的结构。 - 再次使用
inteface show
命令确定Vector3
的结构,它是Twist
消息类型的一部分。 - 为我们的命令创建 YAML 语法。请注意下面的 YAML 语法,因为它相当棘手!YAML 用单引号和一组顶级花括号括起来,而后续级别遵循
name:value
模式,对于嵌套类型(如Twist
命令),则遵循name:{name1:val1,name2:val2}
。 - 使用
ros2 pub
发出命令。
$ ros2 topic type /turtle1/cmd_vel
geometry_msgs/msg/Twist
$ ros2 interface show geometry_msgs/msg/Twist
# This expresses velocity in free space broken into its linear and angular parts.
Vector3 linear
Vector3 angular
$ ros2 interface show geometry_msgs/msg/Vector3
# This represents a vector in free space.
float64 x
float64 y
float64 z
$ ros2 topic pub --once /turtle1/cmd_vel geometry_msgs/msg/Twist '{linear: {x: 4.0,y: 4.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}'
如果你做对了所有事情,你应该已经移动了屏幕上的乌龟。尝试更改命令以绘制一个小图片。
ROS 2 服务和操作
如前所述,服务是指可以快速完成的简短、同步的机器人行为,例如打开灯和打开或关闭组件。动作是长期、异步的任务,可能有中间步骤。动作的一个典型例子是导航:为机器人提供一个目标位置并要求其导航到该目标。尽管机器人可以尝试,但由于它不能无限快速地移动,因此需要时间才能移动到目标,有时其路径可能会被阻塞。
这两个原语是大多数使用 ROS 的机器人系统的骨干,学习如何通过命令行使用它们将使您能够快速轻松地命令机器人为您完成任务。为了帮助清晰地理解本节,我们还将介绍 ros2 node
命令,以确定哪个节点或软件进程正在执行特定操作或服务。
让我们快速解决节点问题。ROS 节点是小程序,在自己的进程中运行。ROS 系统可以同时运行十个、数百个甚至数千个节点。此外,ROS 系统可以在同一系统上同时运行同一节点的多个副本。在我们的海龟模拟中,我们实际上可以创建多个海龟,每个海龟都有自己的节点,所有海龟都运行完全相同的程序。ROS 节点与 ROS 主题一样,具有命名空间,因此您可以在运行同一节点(程序)的多个副本的情况下处理特定节点。让我们通过在终端中使用ros2 run turtlesim turtlesim_node
重新启动我们的海龟模拟来深入了解一下。现在,在新的终端中,让我们首先通过寻求帮助来检查ros2 nod
可以提供什么。
$ ros2 node --help
usage: ros2 node [-h]
Call `ros2 node <command> -h` for more detailed usage. ...
Various node related sub-commands
optional arguments:
-h, --help show this help message and exit
Commands:
info Output information about a node
list Output a list of available nodes
Call `ros2 node <command> -h` for more detailed usage.
与主题非常相似,我们看到两个子命令,info
和 list
。node list
的工作原理与 topic list
非常相似,只是打印所有正在运行的节点的列表。让我们看看我们的系统上运行了什么:
$ ros2 node list
/turtlesim
我们有一个名为“turtlesim”的单节点正在运行。node info
的工作方式与 topic info
非常相似,只不过它列出了我们提供给它的节点的信息。让我们用我们的单个 ROS 节点 /turtlesim
作为其参数来调用它:
$ ros2 node info /turtlesim
/turtlesim
Subscribers:
/parameter_events: rcl_interfaces/msg/ParameterEvent
/turtle1/cmd_vel: geometry_msgs/msg/Twist
Publishers:
/parameter_events: rcl_interfaces/msg/ParameterEvent
/rosout: rcl_interfaces/msg/Log
/turtle1/color_sensor: turtlesim/msg/Color
/turtle1/pose: turtlesim/msg/Pose
Service Servers:
/clear: std_srvs/srv/Empty
/kill: turtlesim/srv/Kill
/reset: std_srvs/srv/Empty
/spawn: turtlesim/srv/Spawn
/turtle1/set_pen: turtlesim/srv/SetPen
/turtle1/teleport_absolute: turtlesim/srv/TeleportAbsolute
/turtle1/teleport_relative: turtlesim/srv/TeleportRelative
/turtlesim/describe_parameters: rcl_interfaces/srv/DescribeParameters
/turtlesim/get_parameter_types: rcl_interfaces/srv/GetParameterTypes
/turtlesim/get_parameters: rcl_interfaces/srv/GetParameters
/turtlesim/list_parameters: rcl_interfaces/srv/ListParameters
/turtlesim/set_parameters: rcl_interfaces/srv/SetParameters
/turtlesim/set_parameters_atomically: rcl_interfaces/srv/SetParametersAtomically
Service Clients:
Action Servers:
/turtle1/rotate_absolute: turtlesim/action/RotateAbsolute
Action Clients:
哇,信息量很大,其中一些看起来很熟悉。我们可以看到节点订阅的所有主题,以及它发布到的所有节点。我们还可以看到许多“操作服务器”和“服务服务器”。值得注意的是这里的客户端和服务器关系。由于 ROS 可能有多个节点在运行,因此一些节点可能提供服务(这些是服务器),而其他 ROS 节点可能会调用这些服务器(这些是客户端)。客户端可以是其他 ROS 节点,或者对于这些示例,可以是使用 CLI 的人。
服务和操作的命令行界面非常相似,实际上它们都只有四个子命令。让我们运行 action
和 service
命令并进行比较:
$ ros2 action --help
usage: ros2 action [-h]
Call `ros2 action <command> -h` for more detailed usage.
...
Various action related sub-commands
optional arguments:
-h, --help show this help message and exit
Commands:
info Print information about an action
list Output a list of action names
send_goal Send an action goal
show Output the action definition
Call `ros2 action <command> -h` for more detailed usage.
$ ros2 service --help
usage: ros2 service [-h] [--include-hidden-services]
Call `ros2 service <command> -h` for more detailed usage.
...
Various service related sub-commands
optional arguments:
-h, --help show this help message and exit
--include-hidden-services
Consider hidden services as well
Commands:
call Call a service
find Output a list of available services of a given type
list Output a list of available services
type Output a service's type
Call `ros2 service <command> -h` for more detailed usage.
我们可以看到,这两个命令都有一个 list
命令,它提供了可用服务或操作的列表。如果我们有多个节点在运行,并且想要查看提供的每项服务,那么在每个节点上调用 ros2 node info
会非常低效,特别是如果我们有数十个甚至数百个节点在运行。在这种情况下,使用 list 命令执行操作和服务命令会更有效率。我们可以在下面运行这些命令,并看到我们在单个节点中列出的操作和服务列表大致相同:
$ ros2 service list
/clear
/kill
/reset
/spawn
/turtle1/set_pen
/turtle1/teleport_absolute
/turtle1/teleport_relative
/turtlesim/describe_parameters
/turtlesim/get_parameter_types
/turtlesim/get_parameters
/turtlesim/list_parameters
/turtlesim/set_parameters
/turtlesim/set_parameters_atomically
kscottz@kscottz-ratnest:~$ ros2 action list
/turtle1/rotate_absolute
让我们开始深入研究服务。似乎列出了相当多的服务。让我们看一下 /spawn
服务,它将创建更多
turtle。ROS 服务和操作使用与主题中使用的消息类似的消息进行通信。事实上,操作和服务是建立在消息之上的。
我们可以使用 service type
子命令来确定特定服务使用的消息类型。我们可以使用
interface show
命令找到消息的具体信息。让我们在 spawn
服务中实际看看这一点:
$ ros2 service type /spawn
turtlesim/srv/Spawn
$ ros2 interface show turtlesim/srv/Spawn
float32 x
float32 y
float32 theta
string name # Optional. A unique name will be created and returned if this is empty
---
string name
从上面的输出中我们可以看到,spawn 消息采用三个 float32
值作为其位置和方向,以及一个 string
作为其名称。---
表示服务的返回值。与主题不同,服务具有返回值,这使它们能够执行诸如执行计算和计算之类的操作。
让我们通过运行 ros2 service call --help
来检查调用服务的帮助:
$ ros2 service call --help
usage: ros2 service call [-h] [-r N] service_name service_type [values]
Call a service
positional arguments:
service_name Name of the ROS service to call to (e.g. '/add_two_ints')
service_type Type of the ROS service (e.g. 'std_srvs/srv/Empty')
values Values to fill the service request with in YAML format (e.g.
"{a: 1, b: 2}"), otherwise the service request will be
published with default values
optional arguments:
-h, --help show this help message and exit
-r N, --rate N Repeat the call at a specific rate in Hz
这里的语法与发布到主题非常相似,但我们使用的是服务名称,而不是主题名称。服务类型就像我们过去使用的主题类型一样,但我们使用的不是消息类型,而是服务类型。最后,我们以 YAML 格式为其赋值。YAML 字符串必须用单引号括起来。让我们尝试通过在所有值都为零的位置创建一个名为Larry
的海龟来调用服务(使用TAB
完成)。
$ ros2 service call /spawn turtlesim/srv/Spawn "{x: 0, y: 0, theta: 0.0, name: 'Larry'}"
requester: making request: turtlesim.srv.Spawn_Request(x=0.0, y=0.0, theta=0.0, name='Larry')
response:
turtlesim.srv.Spawn_Response(name='Larry')
如果一切正常,你现在应该在屏幕左下角看到一只名为“Larry”的乌龟:
尝试探索提供的其他服务,或 在不同位置创建更多海龟并移动它们。
让我们继续讨论操作。如前所述,操作在某些方面与服务不同,并且具有许多 优势。操作具有以下优势:
- 操作有一个
目标
。也就是说,你向他们发送一个目标,他们就会尝试完成它。 - 操作可以拒绝目标请求。这可以防止他们变得太忙。
- 操作是异步的,可以在“你等待时”执行任务。
- 操作将在你等待时为你提供有关其进度的“更新”。
- 操作是可抢占的,这意味着如果你改变主意,你可以取消它们。
就像服务一样,我们首先要弄清楚如何使用 action list
、action show
和 action info
命令来调用 ROS 系统中的单一操作。回想一下,当我们调用 ros2 action list
时,我们得到了一个单一服务。现在有了 Larry,事情就变了。让我们来看看:
$ ros2 action list
/Larry/rotate_absolute
/turtle1/rotate_absolute
现在有两个可用的操作,一个用于 Larry,一个用于turtle1
。
让我们将 turtle1 旋转到面向 Larry。首先,我们将使用/turtle1/rotate_absolute
作为输入调用action info
,然后查看我们得到的结果:
$ ros2 action info /turtle1/rotate_absolute
Action: /turtle1/rotate_absolute
Action clients: 0
Action servers: 1
/turtlesim
好吧,这告诉了我们有关客户端和服务器的信息,但这对我们移动 Larry 的目标真的没有帮助。我们为什么不看看 action send_goal
帮助,看看我们能否弄清楚如何使用它:
$ ros2 action send_goal --help
usage: ros2 action send_goal [-h] [-f] action_name action_type goal
Send an action goal
positional arguments:
action_name Name of the ROS action (e.g. '/fibonacci')
action_type Type of the ROS action (e.g.
'example_interfaces/action/Fibonacci')
goal Goal request values in YAML format (e.g. '{order: 10}')
optional arguments:
-h, --help show this help message and exit
-f, --feedback Echo feedback messages for the goal
此命令需要 YAML 中的操作名称、操作类型和目标。我们知道 操作名称,也知道如何编写 YAML,所以我们需要做的就是确定 操作类型。获取操作类型的最佳方式与我们发布消息的方式相同。
我们看到我们的每个海龟都有一个名为rotate_absolute
的服务。
让我们使用info
子命令深入研究此操作。此命令有一个-t
标志来列出消息的类型。
$ ros2 action info /turtle1/rotate_absolute -t
Action: /turtle1/rotate_absolute
Action clients: 0
Action servers: 1
/turtlesim [turtlesim/action/RotateAbsolute]
第一行列出了操作名称。第二行给出了该操作的当前客户端数量。Action servers
行给出了此操作的操作服务器总数。最后一行给出了该操作的包和消息类型。
我们在这里可以看到,我们需要知道操作名称、类型和值。现在唯一的问题是弄清楚操作类型的格式。
让我们了解 RotateAbsolute
操作消息
可以使用 ros2 interface show
命令来查找操作消息的类型。让我们来看看:
$ ros2 interface show turtlesim/action/RotateAbsolute
# The desired heading in radians
float32 theta #< --- This section is the GOAL
---
# The angular displacement in radians to the starting position
float32 delta #< --- This section is the final result, different from the goal.
---
# The remaining rotation in radians
float32 remaining # < --- This is the current state.
这说明了绝对旋转的什么?
- 有一个浮点输入
theta
,即所需航向。第一部分是实际目标。 delta
是与初始航向的角度。这是操作完成时返回的值。remaining
是要移动的剩余弧度。这是操作执行过程中由操作发布的值。
有了这些信息,我们可以创建对操作服务器的调用。我们将使用 -f
标志使其更清晰一些。留意你的乌龟!它应该慢慢移动。
$ ros2 action send_goal -f /turtle1/rotate_absolute turtlesim/action/RotateAbsolute {'theta: 1.70'}
Waiting for an action server to become available...
Sending goal:
theta: 1.7
Feedback:
remaining: 0.11599969863891602
Goal accepted with ID: 35c40e91590047099ae5bcc3c5151121
Feedback:
remaining: 0.09999966621398926
Feedback:
remaining: 0.06799960136413574
Feedback:
remaining: 0.03599953651428223
Result:
delta: -0.09600019454956055
Goal finished with status: SUCCEEDED
如果一切正常,我们应该看到我们的乌龟已经旋转了。
ROS Parameters
在 ROS 中,参数是系统中节点之间共享的值(如果您熟悉软件工程中的 黑板设计模式)。参数是任何节点都可以查询或写入的值。另一个很好的类比是普通软件程序中的全局常量。参数最适合用于配置您的机器人。例如,如果您正在建造一辆自动驾驶汽车,并希望将汽车的最大速度限制在 100 公里/小时,您可以创建一个名为“MAX_SPEED”的参数,该参数对所有节点都可见。
让我们通过运行 ros2 param --help
来查看 param
命令。
$ ros2 param --help
Various param related sub-commands
Commands:
delete Delete parameter
get Get parameter
list Output a list of available parameters
set Set parameter
Call `ros2 param <command> -h` for more detailed usage.
从高层次来看,ROS 2 的 param
命令具有用于获取和设置变量的子命令,以及 list
功能和 delete
命令。与我们已经研究过的大多数其他命令一样,首先查看 list
是有益的。让我们看看文档中关于 list
命令的说明,然后看看调用子命令时会发生什么:
$ ros2 param list --help
usage: ros2 param list [-h] [--spin-time SPIN_TIME] [--include-hidden-nodes]
[--param-prefixes PARAM_PREFIXES [PARAM_PREFIXES ...]]
[node_name]
Output a list of available parameters
positional arguments:
node_name Name of the ROS node
optional arguments:
-h, --help show this help message and exit
--spin-time SPIN_TIME
Spin time in seconds to wait for discovery (only
applies when not using an already running daemon)
--include-hidden-nodes
Consider hidden nodes as well
--param-prefixes PARAM_PREFIXES [PARAM_PREFIXES ...]
Only list parameters with the provided prefixes
$ ros2 param list
/turtlesim:
background_b
background_g
background_r
use_sim_time
此子命令中唯一值得注意的参数是 node_name
,它允许您将 param list
的范围缩小到特定节点使用的参数。就 turtlesim
节点中的参数而言,我们看到对 paramlist
的调用为我们提供了:三个名为 background_x
的背景颜色控制参数和一个 use_sim_time
参数。要了解有关 param
命令的所有信息,我们为什么不尝试使用 CLI 更改这些背景颜色参数呢?
更改背景颜色的第一步是查看当前颜色是什么。param get
子命令需要节点名称和参数名称。在上面的列表中,我们可以看到节点名称是顶级元素,前面带有正斜杠,即 /turtlesim
。param get
的语法是 ros2 param get <node_name> <param>
。让我们试一试,看看我们当前的背景颜色值。
$ ros2 param get /turtlesim background_b
Integer value is: 255
$ ros2 param get /turtlesim background_g
Integer value is: 86
$ ros2 param get /turtlesim background_r
Integer value is: 69
在大多数计算机上,颜色表示为 <R,G,B> 值的三元组。颜色值 <69,86,255> 对应于长春花蓝色。要更改 turtlesim 的颜色,我们需要先设置参数值,然后重置 turtlesim 以使其应用颜色更改。我们之前介绍了调用服务的基础知识,因此我们不会介绍构建服务调用的步骤。让我们尝试将背景颜色的蓝色分量设置为 128。
$ ros2 param set turtlesim background_b 128
Set parameter successful
$ ros2 service call /reset std_srvs/srv/Empty
requester: making request: std_srvs.srv.Empty_Request()
response:
std_srvs.srv.Empty_Response()
如果一切正常,你的乌龟应该看起来像下面的屏幕。
ROS Bags
ROS 包是 ROS 用于记录和重放数据的工具。ROS 包就像日志文件一样,可让您将数据与消息一起存储。ROS 系统可以生成大量数据,因此在打包数据时,您必须选择所需的主题。包是测试和调试应用程序的绝佳工具,也是构建强大单元测试的绝佳工具。
让我们通过在终端中输入ros2 bag --help
来查看 root ROS Bag 命令。如果出现错误,您可能需要安装 ROS Bag,因为它通常位于单独的包中。在 Linux 上,您可以运行“sudo apt install ros-eloquent-ros2bag”,它会自动为您安装包。
$ ros2 bag -h
usage: ros2 bag [-h] Call `ros2 bag <command> -h` for more detailed usage. ...
Various rosbag related sub-commands
Commands:
info ros2 bag info
play ros2 bag play
record ros2 bag record
如您所见,有三个子命令,record
、play
和 info
。使用这些
命令,您可以录制一个 bag 文件,播放/重放您录制的文件,并查找有关 bag 文件的信息。
让我们尝试录制我们的第一个 bag 文件。为此,我们需要三个终端,它们都运行 ROS。第一个终端应该已经运行了我们的 turtlesim。如果它没有运行,您可以使用 ros2 run turtlesim turtlesim_node
重新启动它。接下来
您需要再次启动 draw_square
演示以使默认的 turtle
移动。为此,运行 ros2 run turtlesim draw_square
。现在,在第三个
终端中,我们可以通过运行 bag 命令来打包一些数据。让我们首先通过运行 ros2 bag record -h
来查看 record 子命令
$ ros2 bag record -h
usage: ros2 bag record [-h] [-a] [-o OUTPUT] [-s STORAGE]
[-f SERIALIZATION_FORMAT] [--no-discovery]
[-p POLLING_INTERVAL] [-b MAX_BAG_SIZE]
[topics [topics ...]]
ros2 bag record
positional arguments:
topics topics to be recorded
optional arguments:
-h, --help show this help message and exit
-a, --all recording all topics, required if no topics are listed
explicitly.
-o OUTPUT, --output OUTPUT
destination of the bagfile to create, defaults to a
timestamped folder in the current directory
-s STORAGE, --storage STORAGE
storage identifier to be used, defaults to "sqlite3"
-f SERIALIZATION_FORMAT, --serialization-format SERIALIZATION_FORMAT
rmw serialization format in which the messages are
saved, defaults to the rmw currently in use
--no-discovery disables topic auto discovery during recording: only
topics present at startup will be recorded
-p POLLING_INTERVAL, --polling-interval POLLING_INTERVAL
time in ms to wait between querying available topics
for recording. It has no effect if --no-discovery is
enabled.
-b MAX_BAG_SIZE, --max-bag-size MAX_BAG_SIZE
maximum size in bytes before the bagfile will be
split. Default it is zero, recording written in single
bagfile and splitting is disabled.
我们从帮助文件中可以看到,记录包的语法只是为子命令提供要记录的主题列表。大多数其他参数都是为更高级的用户提供的,用于帮助配置如何以及何时存储数据。值得注意的是,有一个 -a, --all
命令可以记录所有数据。您还可以使用 -o, --output command
指定输出包文件。
让我们继续运行 bag 命令,并使用 -o
标志将 /turtle1/pose
主题上的姿势数据打包并保存到文件 turtle1.bag
中。请注意,程序将继续打包数据,直到您按下 CTRL-C
,因此在终止它之前,请给命令 30 秒的时间来收集数据。
$ ros2 bag record /turtle1/pose -o turtle1
[INFO] [rosbag2_storage]: Opened database 'turtle1'.
[INFO] [rosbag2_transport]: Listening for topics...
[INFO] [rosbag2_transport]: Subscribed to topic '/turtle1/pose'
[INFO] [rosbag2_transport]: All requested topics are subscribed. Stopping discovery...
^C[INFO] [rclcpp]: signal_handler(signal_value=2)
现在我们收集了数据,让我们检查一下我们的 bag 文件。您可以使用 ros2 bag info
命令检查任何 bag 文件。此命令将列出 bag 中的消息、文件的持续时间以及消息数量。
$ ros2 bag info turtle1
Files: turtle1.db3
Bag size: 268.4 KiB
Storage id: sqlite3
Duration: 68.705s
Start: May 4 2020 16:10:26.556 (1588633826.556)
End May 4 2020 16:11:35.262 (1588633895.262)
Messages: 4249
Topic information: Topic: /turtle1/pose | Type: turtlesim/msg/Pose | Count: 4249 | Serialization Format: cdr
收集到 bag 文件后,您可以像正在运行的系统一样重播该文件。bag 是调试和测试的绝佳工具。您可以将 ROS bag 视为正在运行的 ROS 系统的记录。播放 bag 文件时,您可以使用大多数 ros2 CLI 工具来检查录制的主题。
要重播 bag,首先使用 CTRL-C
关闭 turtlesim_node
和 draw_square
节点。现在在新的终端中使用以下命令重播 bag 文件:
$ ros2 bag play turtle1
[INFO] [rosbag2_storage]: Opened database 'turtle1'.
看起来应该没有什么事情发生,但实际上却发生了很多事情。
要查看发生了什么,请转到第二个终端。就像一个正在运行的机器人一样,
您应该能够list
和echo
主题:
$ ros2 topic list
/parameter_events
/rosout
/turtle1/pose
$ ros2 bag info turtle1
x: 3.8595714569091797
y: 3.6481313705444336
theta: -1.2895503044128418
linear_velocity: 1.0
angular_velocity: 0.0
---
ROS2 Component Command
ROS2 Daemon Command
ROS2 Doctor Command
任何复杂的系统有时都会出现问题,知道如何描述您的系统以及正在发生的事情可以极大地帮助其他人帮助您解决问题。ROS 2 有一个 doctor
命令,您可以使用它来打印各种报告,这些报告可用于帮助向试图提供帮助的其他人传达系统状态。无论是您的同事、供应商还是在线论坛,提供有关您的 ROS 系统的详细和完整信息都可以大大帮助您解决问题。让我们在 ROS 2 的 --help
命令上调用:
$ ros2 doctor --help
usage: ros2 doctor [-h] [--report | --report-failed] [--include-warnings]
Check ROS setup and other potential issues
optional arguments:
-h, --help show this help message and exit
--report, -r Print all reports.
--report-failed, -rf Print reports of failed checks only.
--include-warnings, -iw
Include warnings as failed checks. Warnings are
ignored by default.
从帮助文件中我们可以看到,我们有几个报告选项。一个选项是使用 -r
打印完整报告,或者使用 -rf
打印失败的内容。如果您正在运行 ros2 doctor -r
,您应该会看到一份相当长的报告,其中包含有关您的计算机操作系统、网络配置和正在运行的 ROS 系统的信息。如果您遇到问题,您应该始终包含这份完整报告。
ROS 2 Interface
正如您已经看到的,ROS 使用标准消息,以便不同的包和程序(可能用不同的编程语言编写)都可以相互通信。为了使这一切正常进行,ROS 使用标准消息和基于这些标准消息构建的通信协议。这可能会使查找有关特定消息、服务或操作的类型信息变得困难。为了帮助开发人员编写 CLI 命令调用和开发客户端代码,ROS CLI 具有接口命令。我们在其他部分简要介绍了此命令,因为它是获取消息类型信息的首选工具。
为了更好地理解 interface
命令,我们首先查看其高级帮助命令,看看有哪些子命令可用:
$ ros2 interface --help
usage: ros2 interface [-h]
Call `ros2 interface <command> -h` for more detailed
usage. ...
Show information about ROS interfaces
optional arguments:
-h, --help show this help message and exit
Commands:
list List all interface types available
package Output a list of available interface types within one package
packages Output a list of packages that provide interfaces
proto Output an interface prototype
show Output the interface definition
Call `ros2 interface <command> -h` for more detailed usage.
接口命令都旨在帮助您了解可用的消息类型。让我们深入研究一下 list
子命令。list
将列出系统上所有可用的消息、服务和操作。此命令具有可帮助您缩小搜索范围的标志。即使是基本的 ROS 安装也有很多消息,因此您应该熟悉的工具是 grep
。grep
可让您搜索一些文本以快速轻松地找到您要查找的内容。您可以使用 -i
标志后跟要搜索的文本以不区分大小写的方式进行 grep。我们可以使用 UNIX 管道运算符 |
将此 CLI 工具绑定到我们的接口工具。下面的示例向您展示了如何使用列表操作以及如何使用它进行搜索:
$ ros2 interface list --only-msgs
Messages:
action_msgs/msg/GoalInfo
action_msgs/msg/GoalStatus
... <DOZENS OF DIFFERENT TYPES> ...
visualization_msgs/msg/MarkerArray
visualization_msgs/msg/MenuEntry
$ ros2 interface list --only-msgs | grep -i point
geometry_msgs/msg/Point
geometry_msgs/msg/Point32
geometry_msgs/msg/PointStamped
map_msgs/msg/PointCloud2Update
pcl_msgs/msg/PointIndices
rcl_interfaces/msg/FloatingPointRange
sensor_msgs/msg/PointCloud
sensor_msgs/msg/PointCloud2
sensor_msgs/msg/PointField
trajectory_msgs/msg/JointTrajectoryPoint
使用 grep 搜索 CLI 输出是开发人员用来查找所需特定信息的常用策略。接下来的两个子命令 package
和 packages
可用于首先确定系统上有哪些 ROS 包,然后深入研究单个包以确定该包中有哪些消息。请注意,您可以像以前一样使用 grep
搜索您感兴趣的特定内容。下面的示例向您展示了如何首先确定 std_msgs
是否已安装,然后找出它包含哪种数组类型:
$ ros2 interface packages
action_msgs
action_tutorials_interfaces
actionlib_msgs
builtin_interfaces
composition_interfaces
diagnostic_msgs
example_interfaces
geometry_msgs
lifecycle_msgs
logging_demo
map_msgs
nav_msgs
pcl_msgs
pendulum_msgs
rcl_interfaces
rosgraph_msgs
rqt_py_common
sensor_msgs
shape_msgs
std_msgs
std_srvs
stereo_msgs
tf2_msgs
trajectory_msgs
turtlesim
unique_identifier_msgs
visualization_msgs
kscottz@kscottz-ratnest:~/Code/ros2multirobotbook/src$ ros2 interface package std_msgs | grep -i array
std_msgs/msg/Int8MultiArray
std_msgs/msg/Int32MultiArray
std_msgs/msg/MultiArrayLayout
std_msgs/msg/UInt64MultiArray
std_msgs/msg/Float32MultiArray
std_msgs/msg/UInt16MultiArray
std_msgs/msg/UInt32MultiArray
std_msgs/msg/Int16MultiArray
std_msgs/msg/ByteMultiArray
std_msgs/msg/Int64MultiArray
std_msgs/msg/Float64MultiArray
std_msgs/msg/UInt8MultiArray
std_msgs/msg/MultiArrayDimension
接下来的两个命令特别有用,你应该记住它们,因为它们会让你的生活变得轻松很多。正如我们之前讨论过的那样,CLI 中的所有消息发布、服务调用和操作调用都以 YAML 格式接收你想要传输的消息类型和数据。但是如果你不知道消息格式,并且对 YAML 了解不多,该怎么办?interface show
和 interface proto
命令分别通过首先告诉你消息类型,然后告诉你消息格式,使这个过程更容易。回想一下本章前面我们在 turtle 模拟中调用 spawn
服务时的情况。我们可以使用 interface show
来大致告诉我们有关服务的信息以及每个值的含义。然后我们可以使用 interface proto
(prototype 的缩写)来生成一条我们可以填写的空消息。请参见下面的示例:
$ ros2 interface show turtlesim/srv/Spawn
float32 x
float32 y
float32 theta
string name # Optional. A unique name will be created and returned if this is empty
---
string name
$ ros2 interface proto turtlesim/srv/Spawn
"x: 0.0
y: 0.0
theta: 0.0
name: ''
"
$ ros2 service call /spawn turtlesim/srv/Spawn "{<copy and paste proto here>}"
从上面的例子中,您可以看到这些工具是多么方便。值得注意的是,您需要将原型粘贴到一组引号和花括号中,才能使调用"{<prototype>}"
起作用。
ROS 2 Launch
launch
命令用于运行 ROS 启动文件。到目前为止,我们一直使用 run
命令手动运行单个 ROS 程序,但这不是大型 ROS 系统通常的操作方式,许多机器人会控制数十甚至数百个小程序。ROS 启动命令与大多数其他 ROS 命令不同,它没有子命令,并且只有一个功能,即启动执行多个程序的 ROS 启动文件。为了说明此命令,让我们看一下它的帮助文件。
$ ros2 launch -h
usage: ros2 launch [-h] [-d] [-p | -s] [-a]
package_name [launch_file_name]
[launch_arguments [launch_arguments ...]]
Run a launch file
positional arguments:
package_name Name of the ROS package which contains the launch file
launch_file_name Name of the launch file
launch_arguments Arguments to the launch file; '<name>:=<value>' (for
duplicates, last one wins)
optional arguments:
-h, --help show this help message and exit
-d, --debug Put the launch system in debug mode, provides more
verbose output.
-p, --print, --print-description
Print the launch description to the console without
launching it.
-s, --show-args, --show-arguments
Show arguments that may be given to the launch file.
-a, --show-all-subprocesses-output
Show all launched subprocesses' output by overriding
their output configuration using the
OVERRIDE_LAUNCH_PROCESS_OUTPUT envvar.
启动文件通常包含在 ROS 包中,通常存储在launch
子目录中。现代启动文件通常用 Python 编写,并以*.launch.py
文件扩展名结尾。launch
命令有两个参数,第一个是包名称,然后是启动文件名。如果您不知道包中的启动文件,可以使用制表符补全列出所有可用的启动文件。最后,一些启动文件具有可以附加到命令的参数。如果您不确定启动文件的作用或它需要什么参数,--print
和--show-args
命令将告诉您这些信息。让我们阅读multisym.launch.py
启动文件,然后按照以下示例运行它(使用CTRL-C
结束模拟):
$ ros2 launch turtlesim multisim.launch.py --show-args
Arguments (pass arguments as '<name>:=<value>'):
No arguments.
$ ros2 launch turtlesim multisim.launch.py --print
<launch.launch_description.LaunchDescription object at 0x7f75aab63828>
├── ExecuteProcess(cmd=[ExecInPkg(pkg='turtlesim', exec='turtlesim_node'), '--ros-args'], cwd=None, env=None, shell=False)
└── ExecuteProcess(cmd=[ExecInPkg(pkg='turtlesim', exec='turtlesim_node'), '--ros-args'], cwd=None, env=None, shell=False)
$ ros2 launch turtlesim multisim.launch.py
[INFO] [launch]: All log files can be found below /home/kscottz/.ros/log/2020-06-24-14-39-03-312667-kscottz-ratnest-20933
[INFO] [launch]: Default logging verbosity is set to INFO
[INFO] [turtlesim_node-1]: process started with pid [20944]
[INFO] [turtlesim_node-2]: process started with pid [20945]
^C[WARNING] [launch]: user interrupted with ctrl-c (SIGINT)
ROS 2 Lifecycle
ROS 2 有一项名为“生命周期”的新功能,可以更好地控制 ROS 节点的状态。大致来说,此功能允许节点拥有正确处理的复杂启动和关闭程序。这种节点的一个例子是控制传感器或执行器的节点,该节点需要在运行前执行开机自检或校准程序。ROS 设计文档 提供了有关生命周期节点中的状态和转换的很好的入门知识。让我们看看生命周期
命令以确定可用的子命令:
$ ros2 lifecycle -h
usage: ros2 lifecycle [-h]
Call `ros2 lifecycle <command> -h` for more detailed
usage. ...
Various lifecycle related sub-commands
optional arguments:
-h, --help show this help message and exit
Commands:
get Get lifecycle state for one or more nodes
list Output a list of available transitions
nodes Output a list of nodes with lifecycle
set Trigger lifecycle state transition
Call `ros2 lifecycle <command> -h` for more detailed usage.
nodes
子命令将列出给定系统上的所有生命周期节点。
列出节点后,您可以使用 ros2 lifecycle list <nodename>
列出每个节点的可用转换。这些转换由节点的当前状态决定,某些状态比其他状态具有更多可用的转换。如果您希望查询当前状态而不是可用转换,则可以使用 lifecycle get
返回目标节点的当前状态。一旦您满意地确定了节点的状态和可用的转换,就可以使用 lifecycle set
命令来触发节点转换到新状态。通常,这些 CLI 命令用于诊断系统的故障模式,或手动转换特定组件。
ROS 2 msg (Message)
ROS 2 Eloquent 是使用 msg
命令的最后一个 ROS 版本。msg
中的所有命令都反映在 interface
命令中。这些功能目前已弃用,并将在 Foxy 中删除。
ROS 2 pkg (Package)
ROS 2 package 命令是一个非常有用的命令,可以了解系统上安装了哪些 ROS 包、安装在哪里以及每个包中包含的可执行文件。这些工具对于了解现有的机器人配置和查找仅偶尔使用的工具特别有用。让我们首先看一下 pkg
命令的帮助文件:
$ ros2 pkg -h
usage: ros2 pkg [-h] Call `ros2 pkg <command> -h` for more detailed usage. ...
Various package related sub-commands
optional arguments:
-h, --help show this help message and exit
Commands:
create Create a new ROS2 package
executables Output a list of package specific executables
list Output a list of available packages
prefix Output the prefix path of a package
xml Output the XML of the package manifest or a specific tag
Call `ros2 pkg <command> -h` for more detailed usage.
此命令有多种子命令,其中许多命令现在看起来应该相当熟悉。list
子命令的作用方式与我们之前讨论过的 list
子命令非常相似,但这个命令仅列出已安装的系统包。此子命令通常与 grep
一起使用,以帮助您找出是否安装了特定包。
找到已安装的包后,您可以使用 executables
命令列出包中包含的可执行文件。这比手动查找可执行文件更实用。子命令接受一个参数,即包名称。executables 命令有一个可选参数 --full-path
,它将输出所有可执行程序的完整路径。下面的示例显示了如何使用这些命令检查所有 turtlesim 可执行文件的路径:
$ ros2 pkg list | grep turtle
turtlesim
$ ros2 pkg executables turtlesim --full-path
/opt/ros/eloquent/lib/turtlesim/draw_square
/opt/ros/eloquent/lib/turtlesim/mimic
/opt/ros/eloquent/lib/turtlesim/turtle_teleop_key
/opt/ros/eloquent/lib/turtlesim/turtlesim_node
如果您只是想知道 turtlesim 可执行文件的路径,则可以使用 prefix
子命令,该命令返回给定包的可执行文件的路径。
每个 ROS 包都包含一个 XML 文件,其中包含包的元数据,包括许可证、维护者及其依赖项等信息。ROS
pkg 有一个方便的 xml
子命令来将这些文件打印到屏幕上,为您省去了查找和打开文件的麻烦。您可以在此命令的输出中使用 grep
来获取所需的信息。以下是使用 xml
和 prefix
查找 turtlesim 目录、其维护者及其许可证的示例:
$ ros2 pkg prefix turtlesim
/opt/ros/eloquent
$ ros2 pkg xml turtlesim | grep maintainer
<maintainer email="dthomas@osrfoundation.org">Dirk Thomas</maintainer>
$ ros2 pkg xml turtlesim | grep license
<license>BSD</license>
kscottz@kscottz-ratnest:~$
pkg
命令中的最后一个子命令是 create
。create
是一个帮助您创建 ROS 包的工具。我们将在本章后面使用此子命令来创建一个新的 ROS 包。简而言之,您将您的包名称和包的所有相关信息作为可选参数提供给命令。
ROS 2 Security
The ROS API
ROS 包含许多软件库,这些库提供了在构建机器人应用程序时有用的各种功能。您需要的库取决于项目的细节。在本节中,我们将介绍在使用 ROS 进行开发时可能经常使用的两个核心库:
rclpy
: Python client libraryrclcpp
: C++ client library
ROS 客户端库 提供数据结构、函数和语法糖,方便使用特定编程语言进行开发。这里我们只介绍 Python 和 C++ 库,因为它们使用最广泛。但您可以找到许多其他语言的 ROS 客户端库,从 Ada 到 JavaScript 再到 Rust 等等。
使用 Python 发布和订阅主题
使用 ROS 发布数据很容易。这是一个完整的 Python 程序,用于发布字符串消息:
from time import sleep
import rclpy
from std_msgs.msg import String
rclpy.init()
node = rclpy.create_node('my_publisher')
pub = node.create_publisher(String, 'chatter', 10)
msg = String()
i = 0
while rclpy.ok():
msg.data = f'Hello World: {i}'
i += 1
print(f'Publishing: "{msg.data}"')
pub.publish(msg)
sleep(0.5)
自己尝试一下。(确保在每个使用的 shell 中都获取了 ROS 设置文件,如我们在上一章中讨论的那样;例如, source/opt/ros/foxy/setup.bash
。)将上面的代码块复制到一个文件中,将其命名为 talker.py
,然后将其提供给您的 Python3 解释器:
$ python3 talker.py
You should see:
Publishing: "Hello world: 0"
Publishing: "Hello world: 1"
Publishing: "Hello world: 2"
它打印到控制台,但数据会传到任何地方吗?我们可以使用前面介绍的ros2 topic
工具检查我们的工作。在另一个 shell 中(让您的 talker 保持运行),运行:
$ ros2 topic echo chatter
您应该会看到以下内容,但数字会根据两个命令之间的时间而有所不同:
data: 'Hello world: 13'
---
data: 'Hello world: 14'
---
data: 'Hello world: 15'
这样我们就有了一个可以工作的 talker。现在我们可以添加自己的监听器来代替ros2 topic
。这是一个完整的 Python 程序,它订阅字符串消息并将其打印到控制台:
import rclpy
from std_msgs.msg import String
def cb(msg):
print(f'I heard: "{msg.data}"')
rclpy.init()
node = rclpy.create_node('my_subscriber')
sub = node.create_subscription(String, 'chatter', cb, 10)
rclpy.spin(node)
自己尝试一下。将上面的代码块复制到一个文件中,并将其命名为listener.py
。让您的谈话者仍在一个 shell 中运行,在另一个 shell 中启动您的
监听器:
$ python3 listener.py
您应该会看到(再次强调,数字将根据时间而变化):
I heard: "Hello world: 35"
I heard: "Hello world: 36"
I heard: "Hello world: 37"
深入研究 Python 代码
现在我们知道这些程序可以运行,我们可以深入研究它们的代码。两个程序都以相同的前言开头:
import rclpy
from std_msgs.msg import String
我们需要导入 rclpy
客户端库,它为我们提供了用 Python 编写 ROS 应用程序所需的大部分内容。但我们还需要专门导入我们将使用的 ROS 消息类型。在本例中,我们使用简单的 std_msgs/String
消息,其中包含一个名为 data
的字段,类型为 string
。如果我们想使用代表相机图像的 sensor_msgs/Image
消息,那么我们会写 from sensor_msgs.msg import Image
。
导入后,两个程序都执行共同的初始化:
rclpy.init()
node = rclpy.create_node('my_node_name')
我们初始化 rclpy
库,然后调用它来创建一个 Node
对象,并为其命名。随后我们将对该 Node
对象进行操作。
在 talker 中,我们使用 Node
对象创建一个 Publisher
对象:
pub = node.create_publisher(String, 'chatter', 10)
我们声明要发布的数据类型(std_msgs/String
)、要发布的主题名称(chatter
)以及本地排队的最大出站消息数(10)。当我们发布的速度比订阅者使用数据的速度快时,最后一个参数就会发挥作用。
监听器中的等效步骤是创建一个Subscription
对象:
sub = node.create_subscription(String, 'chatter', cb, 10)
类型(String
)和主题名称(chatter
)参数具有与create_publisher()
调用相同的含义,最后一个参数(10)是为入站消息设置类似的最大队列大小。关键的区别是cb
参数,它指的是我们在侦听器中定义的这个回调函数:
def cb(msg):
print(f'I heard: "{msg.data}"')
每当侦听器收到消息时,就会调用该函数,
并且收到的消息将作为参数传递。在这种情况下, 我们只需将内容打印到控制台。
定义回调并创建“订阅”后,侦听器的其余部分就只有一行:
rclpy.spin(node)
此调用将控制权移交给 rclpy
,以等待新消息到达(更一般地是等待事件发生)并调用我们的回调。
回到谈话者,我们创建一个简单的循环来使用我们的 Publisher
:
msg = String()
i = 0
while rclpy.ok():
msg.data = f'Hello World: {i}'
i += 1
print(f'Publishing: "{msg.data}"')
pub.publish(msg)
sleep(0.5)
这些步骤非常清楚:我们创建一个消息对象,然后在循环的每次迭代中,我们更新消息内容并发布它,在迭代之间短暂休眠。
使用 C++ 发布和订阅主题
现在我们将用 C++ 编写相同的发送器和监听器对。
这是一个发布字符串消息的完整 C++ 程序:
#include <unistd.h>
#include <iostream>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
auto node = rclcpp::Node::make_shared("minimal_publisher");
auto pub = node->create_publisher<std_msgs::msg::String>("chatter", 10);
std_msgs::msg::String message;
auto i = 0;
while (rclcpp::ok()) {
message.data = "Hello world: " + std::to_string(i++);
std::cout << "Publishing: " << message.data << std::endl;
pub->publish(message);
usleep(500000);
}
return 0;
}
当然,和所有 C++ 一样,我们需要编译这个程序。管理 C++ 的编译参数很麻烦,所以我们使用 CMake 来帮忙。以下是完整的 CMake 代码,它允许我们构建 talker 示例:
cmake_minimum_required(VERSION 3.5)
project(talker_listener)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
add_executable(talker talker.cpp)
target_include_directories(talker PRIVATE ${rclcpp_INCLUDE_DIRS} ${std_msgs_INCLUDE_DIRS})
target_link_libraries(talker ${rclcpp_LIBRARIES} ${std_msgs_LIBRARIES})
自己尝试一下。将 C++ 代码复制到名为“talker.cpp”的文件中 并将 CMake 代码复制到名为“CMakeLists.txt”的文件中。将它们 并排放在一个目录中,然后调用“cmake”,然后调用“make”:
$ cmake .
$ make
你最终应该得到一个名为“talker”的编译可执行文件。运行它:
$ ./talker
You should see:
Publishing: "Hello world: 0"
Publishing: "Hello world: 1"
Publishing: "Hello world: 2"
保持谈话者运行,另一个 shell 尝试“ros2 topic”来监听:
$ ros2 topic echo chatter
您应该会看到(数字将根据两个命令之间的时间而变化):
data: 'Hello world: 13'
---
data: 'Hello world: 14'
---
data: 'Hello world: 15'
现在我们可以编写自己的监听器来代替“ros2 topic”。下面是一个完整的 C++ 程序,它订阅字符串消息并将其打印到控制台:
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
void cb(const std_msgs::msg::String::SharedPtr msg)
{
std::cout << "I heard: " << msg->data << std::endl;
}
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
auto node = rclcpp::Node::make_shared("my_subscriber");
auto sub = node->create_subscription<std_msgs::msg::String>("chatter", 10, cb);
rclcpp::spin(node);
return 0;
}
将代码块复制到名为“talker.cpp”的文件中。为了安排编译,我们还需要将一些相应的 CMake 代码添加到我们之前的“CMakeLists.txt”文件的底部:
add_executable(listener listener.cpp)
target_include_directories(listener PRIVATE ${rclcpp_INCLUDE_DIRS} ${std_msgs_INCLUDE_DIRS})
target_link_libraries(listener ${rclcpp_LIBRARIES} ${std_msgs_LIBRARIES})
Configure and build again:
$ cmake .
$ make
现在你应该也有一个 listener
可执行文件了。当你的 talker 仍在一个 shell 中运行时,在另一个 shell 中启动你的 listener:
$ ./listener
You should see (again, numbers will vary depending on timing):
I heard: "Hello world: 35"
I heard: "Hello world: 36"
I heard: "Hello world: 37"
深入研究 C++ 代码
现在我们知道这些程序可以运行,我们可以深入研究它们的代码。两个程序都以相同的前言开头:
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
我们总是需要包含 rclcpp
客户端库,它为我们提供了用 C++ 编写 ROS 应用程序所需的大部分内容。但我们还需要专门导入我们将使用的 ROS 消息类型。在本例中,我们使用简单的 std_msgs/String
消息,其中包含一个名为 data
的字段,类型为 string
。如果我们想使用代表相机图像的 sensor_msgs/Image
消息,那么我们将 #include "sensor_msgs/msg/image.hpp"
。
导入后,两个程序都会执行通用初始化:
rclcpp::init(argc, argv);
auto node = rclcpp::Node::make_shared("my_node_name");
我们初始化 rclcpp
库,然后调用它来创建一个 Node
对象,并为其命名。随后我们将对该 Node
对象进行操作。
在 talker 中,我们使用 Node
对象创建一个 Publisher
对象:
auto pub = node->create_publisher<std_msgs::msg::String>("chatter", 10);
我们通过模板声明我们将发布的数据类型(std_msgs/String
)、我们将发布的主题名称(chatter
)以及本地排队的最大出站消息数(10)。当我们发布的速度比订阅者使用数据的速度快时,最后一个参数就会发挥作用。
监听器中的等效步骤是创建一个Subscription
对象:
auto sub = node->create_subscription<std_msgs::msg::String>("chatter", 10, cb);
类型(String
)和主题名称(chatter
)参数具有与create_publisher()
调用相同的含义,数值参数(10)设置传入消息的类似最大队列大小。关键区别在于cb
参数,它引用了我们在侦听器中定义的此回调函数:
void cb(const std_msgs::msg::String::SharedPtr msg)
{
std::cout << "I heard: " << msg->data << std::endl;
}
每当侦听器收到消息时,就会调用该函数,
并且收到的消息将作为参数传递。在这种情况下, 我们只需将内容打印到控制台。
定义回调并创建“订阅”后,侦听器的其余部分就只有一行:
rclcpp::spin(node);
此调用将控制权移交给 rclcpp
,以等待新消息到达(更一般地是等待事件发生)并调用我们的回调。
回到谈话者,我们创建一个简单的循环来使用我们的 Publisher
:
std_msgs::msg::String message;
auto i = 0;
while (rclcpp::ok()) {
message.data = "Hello world: " + std::to_string(i++);
std::cout << "Publishing: " << message.data << std::endl;
pub->publish(message);
usleep(500000);
}
在这些步骤中,我们创建一个消息对象,然后在循环的每次迭代中更新消息内容并发布它,在迭代之间短暂休眠。
下一步该去哪里
以上只是非常简短的介绍,我们仅涵盖了主题,而不是服务、操作、参数或 ROS 的许多其他方面。幸运的是,在线 ROS 教程 是学习 ROS 其余部分的绝佳资源。我们特别推荐 初学者:客户端库 合集,这是阅读本章后自然而然的下一步。
关于快捷方式
在本节中,我们介绍了我们能想到的最简单、最简短的 ROS 程序示例。此类程序易于理解和学习,因为它们没有不必要的结构或修饰。但作为交换,此类程序不易扩展、不易组合或不易维护。
我们在本节的示例代码中使用的技术对于原型设计和实验(任何优秀机器人项目的重要方面!)很有用,但我们不建议将它们用于严肃的工作。 当您阅读 ROS 教程 并开始阅读现有的 ROS 代码时,您将了解许多概念、模式和约定,例如:
- 将代码组织到包中
- 将包组织到工作区中
- 管理包之间的依赖关系
- 使用
colcon
工具按依赖顺序在多个包中构建代码 - 使用
CMakeLists.txt
文件中的ament
模块 - 构建代码以允许运行时控制节点如何映射到进程
- 使用客户端库的控制台日志记录例程将内容输出到屏幕和其他地方
当您开始构建自己的 ROS 应用程序时,这些技术将为您提供很好的帮助,尤其是当您想要与其他人(无论是在您的团队中还是在世界范围内)共享您的代码时。
交通编辑
本节介绍流量编辑器 GUI 和模拟工具。
简介和目标
异构机器人车队的交通管理并非易事。协调管理的挑战之一来自于车队使用的信息模型中不同的语义。航路点、车道、充电/停靠站、禁区、门和电梯等基础设施系统的表示由供应商自行决定。然而,传达共享设施中车队能力和意图的标准化惯例对于规划至关重要。其他交通方式(如公路)中的多智能体参与者共同遵守一套规则和惯例,以最大限度地减少混乱。更重要的是,它们允许新参与者通过遵循规定的规则轻松融入系统。现有智能体可以容纳新参与者,因为它的行为是显而易见的。
多机器人系统的交通惯例并不存在。 traffic_editor
的目标是通过图形界面以标准化、中立的方式表达各个车队的意图,从而填补这一空白。然后可以导出来自不同车队的整理后的交通信息,以进行规划和控制。traffic_editor
的第二个目标和好处是促进生成准确反映物理环境的 3D 模拟世界。
概述
traffic_editor
存储库 是 traffic_editor
GUI 和工具的所在地,这些工具可从 GUI 输出自动生成模拟世界。
GUI 是一个易于使用的界面,可以创建和注释带有机器人交通以及建筑基础设施信息的 2D 楼层平面图。
通常,环境有现有的楼层平面图,例如建筑图纸,这简化了任务并为特定于供应商的地图提供了“参考”坐标系。
对于这种情况,traffic-editor
可以导入这些类型的“背景图像”作为画布,在其上绘制预期的机器人交通地图,并轻松跟踪模拟所需的重要墙段。
traffic_editor
GUI 项目存储为带有 .building.yaml
文件扩展名的 yaml
文件。
尽管典型的工作流程使用 GUI 并且不需要直接手动编辑 yaml
文件,但我们使用了 yaml
文件格式,以便在需要时使用自定义脚本轻松解析。
每个 .building.yaml
文件都包含用户注释的站点每个级别的几个属性。
下面显示一个空的 .building.yaml
文件。
GUI 尝试使向这些文件添加和更新内容变得容易。
levels:
L1:
doors:
- []
drawing:
filename:
fiducials:
elevation: 0
flattened_x_offset: 0
flattened_y_offset: 0
floors:
- parameters: {}
vertices: []
lanes:
- []
layers:
{}
measurements:
- []
models:
-{}
vertices:
{}
walls:
{}
lifts:
{}
name: building
GUI Layout
traffic_editor
的布局包括 Toolbar
、Working Area
和 Sidebar
,如下图所示:
工具栏包含各种工具,用于支持设置绘图比例、对齐多层场景的级别、向模拟环境添加虚拟模型、添加机器人交通车道、模拟地板等操作。
与现代 GUI 中通常的情况一样,顶部工具栏包含各种工具,用于与主工作区中的项目进行交互。
本文档将在创建示例项目时介绍和解释这些工具。 但是,工具栏中的前三个工具通常出现在 2D 绘图工具中,并且应该按预期运行:
Icon | Name | Shortkey | Function |
---|---|---|---|
Select | Esc | Select an entity in the Working Area | |
Move | m | Move an entity in the Working Area | |
Rotate | r | Rotate an entity in the Working Area |
工作区
是渲染楼层及其注释的地方。
用户可以通过鼠标滚轮缩放,并通过按下滚轮并移动鼠标光标来平移视图。
窗口右侧的 侧边栏
包含具有各种功能的多个选项卡:
- 楼层:为建筑物添加新楼层。这可以从头开始完成,也可以通过导入平面图图像文件来完成。
- 图层:将其他图像(例如激光雷达地图)叠加在楼层上
- 电梯:配置和添加建筑物的电梯
- 交通:选择当前正在编辑哪个“导航图”,并切换正在渲染的图形。
注释指南
本节将介绍注释设施的过程,同时重点介绍 traffic_editor
GUI 的功能。
要创建新的流量编辑器 Building
文件,请从终端窗口启动流量编辑器(如果 traffic-editor
是从源代码构建的,则首先获取工作区)。
然后,单击 Building -> New...
并为 .building.yaml
文件选择位置和文件名。
添加楼层
可以通过单击 Sidebar
的 levels
选项卡中的 Add
按钮来添加建筑物中的新楼层。
该操作将打开一个对话框,您可以在其中指定 name
、elevation
(以米为单位)和 2D drawing
文件(.png
)的路径。
在大多数用例中,楼层的平面图用作绘图。
如果未指定,用户可以在提供的字段中明确输入楼层的尺寸。
在上图中,添加了一个位于海拔 0m
的新楼层 L1
和一个楼层平面图,如 levels
选项卡所示。
默认比例为 1px = 5cm
。
可以通过添加测量值来设置实际比例。
用于对齐楼层的任何偏移都将反映在 X
和 Y
列中。
保存项目将更新 tutorial.building.yaml
文件,如下所示:
levels:
L1:
drawing:
filename: office.png
elevation: 0
flattened_x_offset: 0
flattened_y_offset: 0
layers:
{}
lifts:
{}
name: building
Adding a vertex
Icon | Shortkey |
---|---|
v |
顶点是多个注释的基本组成部分。 墙壁、测量、门、地板多边形和交通车道都是从两个或多个顶点创建的。 要创建顶点,请单击“工具栏”中的顶点图标,然后单击画布上的任意位置。 顶点的默认属性是其坐标以及一个空的名称字段。 可以通过首先选择顶点(将其变为红色),然后单击图中的“添加”按钮来添加其他属性。 这些的简短描述如下:
- **is_holding_point:**如果为真,并且航路点是车道的一部分,则
rmf_fleet_adapter
将在路径规划期间将其视为 等待点,即允许机器人在此航路点等待一段无限期的时间。 - is_parking_spot: 机器人的停车位。 Definition
- is_passthrough_point: 机器人不应停止的航点. Definition
- is_charger: 如果为真,并且航路点是车道的一部分,则
rmf_fleet_adapter
将把这里当作充电站。 - is_cleaning_zone 指示当前航点是否为清洁区域,专门用于“清洁”任务。
- dock_name: 如果指定,并且航路点是交通车道的一部分,则
当机器人接近该航路点时,
rmf_fleet_adapter
将向机器人发出rmf_fleet_msgs::ModeRequest
消息,其中MODE_DOCKING
和task_id
等于指定名称。当机器人执行其自定义对接序列(或自定义行进路径)时,使用此消息。 - spawn_robot_type: 在模拟中在此航点生成的机器人模型的名称。该值必须与资产存储库中模型的文件夹名称匹配。有关模拟所需的机器人模型和插件的更多详细信息,请参阅 模拟
- spawn_robot_name: 在此航路点生成的机器人的唯一标识符。此机器人发布的
rmf_fleet_msgs::RobotState
消息将具有与此值相等的name
字段。 - pickup_dispenser 用于“交付”任务的分配器工作单元的名称,通常是模型的名称。请参阅 [Workcell section] (https://osrf.github.io/ros2multirobotbook/simulation.html#workcells) 请参阅模拟章节以了解更多详细信息。
- dropoff_ingestor “交付”任务的摄取器工作单元的名称,通常是模型的名称。请参阅 [Workcell section] (https://osrf.github.io/ros2multirobotbook/simulation.html#workcells) of the Simulation Chapter for more details.
- human_goal_set_name The
goal_sets.set_area
名称,由人群模拟使用。有关“crowd_sim”的更多信息,请参阅 [Crowdsim section] (https://osrf.github.io/ros2multirobotbook/simulation.html#crowdsim) 请参阅模拟章节以了解更多详细信息。
每个顶点都以 x 坐标、y 坐标、海拔、vertex_name 和一组附加参数的列表形式存储在 tutorial.building.yaml
文件中。
vertices:
- [1364.76, 1336.717, 0, magni1_charger, {is_charger: [4, true], is_parking_spot: [4, true], spawn_robot_name: [1, magni1], spawn_robot_type: [1, Magni]}]
Adding a measurement
Icon |
---|
添加测量值可设置导入的 2D 图纸的比例,这对于规划和模拟准确性至关重要。 平面图中的比例尺或参考尺寸有助于完成此过程。通常,您可以在图纸中的参考比例尺上直接绘制测量线。 在编辑器处于_建筑_模式时,选择_添加测量_工具并单击两个已知尺寸的点。 地图上会呈现一条粉红色的线,其两端在选定点处有两个顶点。
注意: 可以通过单击现有顶点来绘制测量线。 在这种情况下,不会在其末端创建其他顶点。
选择该线会填充“侧边栏”属性窗口中的各种参数。 将“距离”参数设置为点之间的物理距离(以米为单位),然后将更新关卡的“比例”。 目前,您必须保存项目并重新启动“traffic-editor”才能看到反映的更改(待办事项:修复此问题...)。
上述过程将两个“顶点”和一个“测量”字段添加到“tutorial.building.yaml”文件,如下所示。 对于测量字段,前两个元素表示代表线端点的顶点的索引。 “距离”值存储在参数的子列表中。
levels:
L1:
drawing:
filename: office.png
elevation: 0
flattened_x_offset: 0
flattened_y_offset: 0
layers:
{}
measurements:
- [1, 0, {distance: [3, 8.409]}]
vertices:
- [2951.728, 368.353, 0, ""]
- [2808.142, 1348.9, 0, ""]
lifts:
{}
name: building
Adding a wall
Icon | Shortkey |
---|---|
w |
要在地图中注释墙壁,请从“工具栏”中选择“添加墙壁”图标,然后单击代表墙壁角落的连续顶点。 添加墙壁段的过程是连续的,可以按“Esc”键退出。 地图上呈现顶点之间的蓝线,代表绘制的墙壁。 如果不存在角顶点,则使用此工具时会自动创建它们。 注释墙壁的网格是在使用“building_map_generator”生成 3D 世界期间自动生成的。 默认情况下,墙壁厚度为 10 厘米,高度为 2.5 米。 “wall_height”和“wall_thickness”属性可以 修改 in the source code.
墙面纹理选项可用 here in the source code.
墙壁存放在 tutorial.building.yaml
文件作为一个列表,其中包含墙段起点和终点顶点的索引以及一个空参数集。
walls:
- [3, 4, {}]
- [4, 5, {}]
- [5, 6, {}]
- [6, 7, {}]
- [6, 8, {}]
- [8, 9, {}]
Adding a floor
Icon |
---|
地板对于模拟至关重要,因为它为机器人提供了行走的地面。 使用“建筑”编辑模式中“主工具栏”中的“添加地板多边形”工具对地板进行注释。 要定义地板,请选择连续的顶点以创建一个准确表示地板面积的多边形,如下所示。 需要在此步骤之前手动添加这些顶点。 创建后,保存项目并重新加载。 选择定义的地板会突出显示其纹理属性。同样, default list of available textures 可以在源代码中找到。
某些场景可能需要带有空腔的地板,例如,用来表示电梯井。 添加孔洞多边形 工具可用于此目的。 此外,可以使用 编辑多边形 工具修改绘制的多边形(地板或孔洞)的形状。 选择现有多边形后单击该工具,用户可以修改多边形的顶点。
Each polygon is stored in the tutorial.building.yaml
file in the format below:
floors:
- parameters: {texture_name: [1, blue_linoleum], texture_rotation: [3, 0], texture_scale: [3, 1]}
vertices: [11, 10, 14, 15, 13, 12]
Adding a door
Icon |
---|
可以在“建筑”编辑模式下添加两个顶点之间的门,方法是从“主工具栏”中选择“添加门”工具,然后单击代表门末端的顶点。 选择带注释的门会突出显示其属性,如下图所示。 目前,支持四种门“类型”:“铰链”、“双铰链”、“滑动”和“双滑动”。 “motion_degrees”参数指定铰链门的运动范围,而“motion_direction”指示摆动的方向。 为了使门在模拟中工作,必须为门指定一个“名称”。
门以列表形式存储在 tutorial.building.yaml
文件中,其中包含起始和结束顶点的索引以及描述门的一组参数。
doors:
- [24, 25, {motion_axis: [1, start], motion_degrees: [3, 90], motion_direction: [2, 1], name: [1, D001], type: [1, double_sliding]}]
Adding a traffic lane
traffic_editor
GUI 中最重要的工具之一是 Add lane 工具。
设施中运行的每个车队的允许运动通过其各自的图表传达,该图表由航路点和连接车道组成。
在这种方法中,我们假设机器人沿着航路点之间的有效直线路径行进。
虽然这可能被视为对能够自主导航的机器人所走路径的过度简化,但在实践中,该假设相当成立,因为这些机器人大多沿着走廊或过道行进,很少在无约束的开放空间中行进。
例如,即使在理论上无约束的空间(如建筑大厅或购物中心中庭),现场操作员也可能希望机器人在空间边缘的“交通车道”中运行,以免妨碍典型的人类交通流。
Sidebar
中的 traffic
选项卡默认为九个不同的车队提供了九个图表。要注释图形(例如图形 0)的车道,请从“交通”选项卡中选择图形,然后单击“添加车道”工具。
可以通过单击要连接的顶点来绘制此图形的车道。
如果顶点不存在,则会自动添加。 可以按照上一节所述为每个顶点分配属性。
要向需要机器人在任何航路点终止的航路点发出任务,必须为航路点分配名称。
每个图表的车道都有独特的颜色,可以使用“交通”选项卡中的复选框切换车道的可见性。
在两个航路点之间定义的车道可以配置以下附加属性:
- 双向:如果为“真”,则“rmf_fleet_adapter”将为其机器人规划路线,假设车道可以双向行驶。
非双向车道有箭头指示其方向性(上图中的靛蓝车道)。一个方便的快捷方式是,当选择车道段时,您可以
按“b”键在该车道的单向和双向运动之间切换。
-
graph_idx:车道对应的图表编号
-
方向:限制车道以使机器人以“向前”或“向后”方向行驶。例如,这对于接近对接点或充电器的最终车道段很有用。
虽然 traffic_editor
现在支持在楼层之间移动的电梯,但 demo_mock_floor_name 和 demo_mock_lift_name 属性最初是为了展示单层演示环境中的共享电梯访问而设计的,其中的“模拟”电梯接收电梯命令并传输电梯状态,但实际上并不在建筑物的任何不同楼层之间移动。
但是,由于可能对此类功能感兴趣,用于测试旨在模拟多层场景的单层硬件设置,因此保留了这些属性。
- demo_mock_floor_name:机器人在穿越车道时所在的楼层名称
- demo_mock_lift_name:机器人在穿越车道时进入或退出的电梯名称 为了进一步解释这些属性,请考虑以下导航图的表示,其中数字是航点,字母是车道:
1 <---a---> 2 <---b---> 3
Waypoint 1 is on floor L1
Waypoint 2 is inside the "lift" named LIFT001
Waypoint 3 is on floor L3
The properties of edge "a" are:
bidirectional: true
demo_mock_floor_name: L1
demo_mock_lift_name: LIFT001
The properties of edge "b" are:
bidirectional: true
demo_mock_floor_name: L3
demo_mock_lift_name: LIFT001
如果机器人要从路点 1 行进到路点 3,当机器人接近路点 1 时,rmf_fleet_adapter
将请求“模拟升降机”到达 L1。
在确认“升降机”位于 L1 且其门处于“打开”状态后,机器人将被指示进入“升降机”前往路点 2。
一旦“升降机”指示已到达 L3,机器人将沿着车道 b 驶出前往路点 3。
注意:注释图形时,强烈建议遵循图形索引的升序顺序,而不跳过中间数字。只有在“交通”选项卡中首先选择其关联的图形时,才能与绘制的车道进行交互。
注释的图形最终使用“building_map_generator”导出为“导航图”,然后由相应的“rmf_fleet_adapters”用于路径规划。
车道以以下格式存储在 tutorial.building.yaml
中。
数据结构是一个列表,其中前两个元素代表车道两个顶点的索引和一组具有配置属性的参数。
lanes:
- [32, 33, {bidirectional: [4, true], demo_mock_floor_name: [1, ""], demo_mock_lift_name: [1, ""], graph_idx: [2, 2], orientation: [1, forward]}]
Deriving coordinate-space transforms
坐标空间令人困惑! 由于历史原因,GUI 在内部通过注释图像来创建交通地图,因此“原始”注释实际上是在“基本”平面图图像的像素坐标中编码的,在像素坐标中,+X=右,+Y=下,原点位于基本平面图图像的左上角。 然而,在建筑地图生成步骤中,垂直轴被翻转以最终位于笛卡尔平面中,因此绝大多数 RMF(即交通编辑器和建筑地图生成器下游的所有内容)使用“正常”笛卡尔坐标系。 (顺便说一句——下一个版本的交通编辑器在这方面会更加灵活,默认使用普通笛卡尔坐标系(而不是基于图像的坐标系),甚至是全局坐标(纬度/经度)。虽然前期工作正在进行中,但在撰写本文时,这款下一代编辑器还没有硬性时间表,因此本章的其余部分将描述现有的“交通编辑器”。)
虽然“交通编辑器”目前使用基础平面图图像的左上角作为参考框架,但机器人生成的地图可能起源于其他地方,并且可能以不同的方向和比例缩放。
在“交通编辑器”地图和机器人地图中推导出坐标系之间的正确变换至关重要,因为
“rmf_fleet_adapters”期望所有机器人在 RMF 坐标系中发布其位置,而“rmf_fleet_adapters”也在同一框架中发出路径请求。
为了获得此类变换,traffic_editor
GUI 允许用户将机器人地图叠加在平面图上,并应用比例、平移和旋转变换,以使两个地图正确对齐。
然后,用户可以在为机器人编程接口时应用相同的变换来在机器人地图和 RMF 坐标之间进行转换。
可以通过单击“侧边栏”中“图层”选项卡上的“添加”按钮来导入机器人地图。 然后,对话框将提示用户上传机器人地图图像。 同一框包含用于设置图像比例以及应用平移和旋转的字段。 通过视觉反馈,用户可以确定这些字段的适当值。 如下图所示,将机器人生成的地图导入 GUI 后,其位置和方向与平面图不同。 使用正确的变换值,可以使两个地图重叠。
Adding fiducials
Icon |
---|
对于具有多个级别的地图,基准点提供了一种相对于参考级别缩放和对齐不同级别的方法。 这对于确保不同级别注释的尺寸准确性以及对齐模拟至关重要。 基准点是放置在两个或多个级别之间预计垂直对齐的位置的参考标记。 例如,结构柱可能贯穿多个楼层,其位置通常在楼层平面图上标明。 如果在级别和参考级别之间有两对或更多对相应的标记,则可以在两个级别之间得出几何变换(平移、旋转和缩放)。 然后可以将此变换应用于新定义级别中的所有顶点和模型。
首先,使用“添加基准点”工具向参考级别添加两个或更多具有唯一“名称”属性的非共线基准点(下图左图)。 在新创建的级别中,在预计垂直对齐的位置添加与参考级别相同数量的基准点,并使用匹配的名称(下图右图)。 保存并重新加载项目会计算级别之间的转换,这可以从“级别”选项卡中显示的新级别的比例和 X-Y 偏移中看出。 此级别现已准备好进行注释。
For each level, fiducials are stored in a list of their X & Y coordinates along with their name.
fiducials:
- [936.809, 1323.141, F1]
- [1622.999, 1379.32, F2]
- [2762.637, 346.69, F3]
Adding a lift
电梯是多层设施中人类和机器人车队共享的不可或缺的资源。 要向建筑物添加电梯,请单击“侧边栏”中“电梯”选项卡中的“添加”按钮。 将加载一个具有各种可配置属性的对话框。 必须指定其轿厢中心的名称、参考级别和 X&Y 坐标(像素单位)。 可以进一步添加偏航(弧度)以根据需要定位电梯。 轿厢的宽度和深度(米)也可以自定义。 电梯可以设计为具有多个轿厢门,可以在多个楼层打开。 要添加轿厢门,请单击轿厢图像下方框中的“添加”按钮。 每个轿厢门都需要一个名称以及位置和方向信息。 在这里,X&Y 坐标相对于轿厢中心。
配置的升降机存储在 tutorial.building.yaml
文件中,如下所述:
lifts:
LF001:
depth: 2
doors:
door1:
door_type: 2
motion_axis_orientation: 1.57
width: 1
x: 1
y: 0
door2:
door_type: 2
motion_axis_orientation: 1.57
width: 1
x: -1
y: 0
level_doors:
L1: [door1]
L2: [door2]
reference_floor_name: L1
width: 2
x: 827
y: 357.7
yaw: 1.09
添加电梯后,我们还希望让机器人横穿电梯。 为了实现这一点,用户需要创建位于每层电梯轿厢内的顶点/航点。 完成后,通过 add_lane 将电梯轿厢内的航点连接到其他顶点。
Adding environment assets
可以使用 建筑 编辑模式下的 添加模型 工具,用可用于模拟的模型缩略图注释楼层。 选择此工具将打开一个对话框,其中包含可导入地图的模型名称和匹配缩略图列表。 一旦进入地图,就可以使用 移动 和 旋转 工具调整它们的位置和方向。提供示例模型 here
The thumbnail_generator documentation 包含有关扩展其他型号缩略图列表的说明。
Note: If no models are shown on the add models window, Go to "Edit -> Preference", then indicate the thumbnail path. (
e.g. $HOME/rmf_ws/src/rmf/rmf_traffic_editor/rmf_traffic_editor_assets/assets/thumbnails
)
Conclusion
本章介绍了 traffic_editor
的各种功能,这些功能可用于注释设施地图,同时遵循标准化的语义集。
可以在 rmf_demos 存储库中找到其他交通编辑器项目的示例。
在 Simulation 一章中描述了在注释站点中使用 RMF 运行基于物理的模拟。
模拟
本章介绍如何从traffic_editor
文件生成建筑模型,然后在这些模型中模拟机器人队伍。
动机
用于测试机器人解决方案的模拟环境在研发和部署的各个阶段都具有巨大的价值。更值得注意的是,模拟具有以下好处:
-
Time and resource saving: 虽然硬件测试不可或缺,但这个过程可能会减慢开发速度,因为需要额外的设置时间、机器人停机时间和试验之间的重置时间。随着参与者数量的增加,购买硬件和测试耗材的成本也会增加。在使用 RMF 等解决方案时尤其如此,该解决方案旨在将多个移动/固定机器人与门和电梯等建筑系统集成在一起。模拟提供了一种潜在的经济高效且省时的替代方案,用于评估机器人系统在规模上的行为。更重要的是,模拟可以帮助回答部署前的问题,例如可以支持多少参与者,或者随着新车队的引入,现有行为将如何变化,这两者都可以为设施所有者提供购买决策信息。
-
Robust testing: 模拟中的机器人既不会耗尽电池,也不会在不幸撞到什么东西时产生费用。可以连续数小时以更快的速度测试场景,以微调算法并验证其稳健性。关于运行适当场景测试数量的一个考虑因素是,这取决于您希望为模拟提供多少计算能力。随着云模拟的引入,这个限制也是成本和速度的权衡。由于模拟中的场景是可重复的,因此可以轻松验证遇到的不良错误的修复。还可以通过模拟研究系统对罕见但后果严重的边缘情况的反应。从硬件试验记录的数据可用于在模拟中重现场景,这可能对调试更有帮助。最后,长时间运行的模拟可以在部署之前让设施所有者充满信心。
基于物理的模拟器(例如“Gazebo”)具有通过“gazebo_ros_pkgs”提供的包装器轻松与 ROS 2 节点交互的优势。
可以开发 Gazebo 插件来准确模拟机器人、传感器和基础设施系统的行为,从而提高模拟的整体保真度。值得强调的是,用于运行模拟的完全相同的代码也将在物理系统上运行,而无需进行任何更改。
然而,尽管有这些令人信服的好处,开发人员和系统集成商很少使用模拟,理由是生成环境和使用适当的插件配置它们的复杂性。在最近由 Afsoon Afzal、Deborah S. Katz、Claire Le Goues 和 Christopher S. Timperley 发表的《关于使用机器人模拟器进行测试的挑战的研究》中,他们指出了参与者不为特定项目使用模拟的主要原因,并总结了他们的发现如下:
Reason for not using simulation | # | % |
---|---|---|
Lack of time or resources | 15 | 53.57% |
Not realistic/accurate enough | 15 | 53.57% |
Lack of expertise or knowledge on how to use software-based simulation | 6 | 21.43% |
There was no simulator for the robot | 4 | 14.29% |
Not applicable | 4 | 14.29% |
Too much time or compute resources | 2 | 7.14% |
Nobody suggested it | 0 | 0.00% |
Other | 2 | 7.14% |
RMF 项目旨在通过简化设置多车队交通控制模拟环境的过程来解决这些障碍,我们将在本节中进一步解释。
建筑地图生成器
上一章讨论的“traffic_editor”是一种以供应商中立的方式用特定于车队的交通信息注释建筑平面图的工具。 这包括感兴趣的航点、交通车道和共享资源,如门口和电梯。它还可用于标记墙壁和地板,并添加环境中的物品缩略图。使用此注释地图自动生成 3D 世界的能力对于简化模拟的创建和管理具有重要价值。为此,“traffic_editor”中的“building_map_tools”包包含一个可执行文件“building_map_generator”。可执行文件以两种模式运行:
-
生成符合 Gazebo/Ignition 的“.world”文件
-
以导航图的形式导出特定于车队的交通信息,供“fleet_adapters”用于规划
为了自动生成 Gazebo 模拟世界,可执行文件接受命令参数“gazebo”以及下面描述的其他参数:
usage: building_map_generator gazebo [-h] [-o [OPTIONS [OPTIONS ...]]] [-n]
[-m MODEL_PATH] [-c CACHE]
INPUT OUTPUT_WORLD OUTPUT_MODEL_DIR
positional arguments:
INPUT Input building.yaml file to process
OUTPUT_WORLD Name of the .world file to output
OUTPUT_MODEL_DIR Path to output the map model files
optional arguments:
-h, --help show this help message and exit
-o [OPTIONS [OPTIONS ...]], --options [OPTIONS [OPTIONS ...]]
Generator options
-n, --no_download Do not download missing models from Fuel
-m MODEL_PATH, --model_path MODEL_PATH
Gazebo model path to check for models
-c CACHE, --cache CACHE
Path to pit_crew model cache
该脚本解析 .building.yaml
文件并生成每个楼层的地板和墙壁的网格。然后,这些网格被组合成 OUTPUT_MODEL_DIR/
目录中的 model.sdf
文件。每个楼层的 model.sdf
文件都导入到 .world
中,文件路径为 OUTPUT_WORLD
。traffic_editor
中注释的各种静态对象的模型子元素包含在 .world
中,如以下代码片段所示:
<include>
<name>OfficeChairBlack_6</name>
<uri>model://OfficeChairBlack</uri>
<pose>4.26201267190027 -7.489812761393875 0 0 0 1.1212</pose>
<static>True</static>
</include>
为带注释的机器人生成了类似的块。在 Gazebo 中加载 .world
文件之前,用户有责任将环境变量 $GAZEBO_MODEL_PATH
和模型的相关路径附加到该变量中。此过程可以通过 ROS 2 启动文件简化,将在后面的章节中讨论。
解析器还包括其他动态资产(如门和电梯)的 sdf 元素。它们的机制将在下一节中讨论。可以使用命令参数 ignition
生成与 Ignition 兼容的世界。
重新配置模拟环境变得像编辑 2D 图纸上的注释并重新运行 building_map_generator
一样简单。这对于在设施中的空间配置发生变化时快速评估交通流量非常有用。
要为车队适配器生成导航图,请使用命令参数 nav
执行 building_map_generator
。导航图以 .yaml
文件形式生成,并在启动期间由相应的舰队适配器进行解析。
usage: building_map_generator nav [-h] INPUT OUTPUT_DIR
positional arguments:
INPUT Input building.yaml file to process
OUTPUT_DIR Path to output the nav .yaml files
RMF 资产和插件
资产在模拟环境中重现起着关键作用。RMF、SubT 等项目允许开发人员创建和开源机器人、机械基础设施系统和场景对象的 3D 模型。它们可在 Ignition Fuel 应用程序 上下载。除了提供视觉准确性之外,资产还可以动态化,并通过插件与 RMF 核心系统交互。
为了模拟机器人模型和基础设施系统等硬件的行为,已经设计了多个 Gazebo 插件。这些插件是 ModelPlugin 类的衍生产品,并与标准 ROS 2 和 RMF 核心消息相结合以提供必要的功能。以下部分简要介绍了其中一些插件。
Robots
如前所述,已有多种机器人模型(SESTO、MiR100、Magni、Hospi)开源用于模拟。为了使这些模型模拟已与 RMF 集成的物理对应模型的行为,它们需要 1) 与 rmf_fleet_adapters
交互,以及 2) 导航到模拟世界中的位置。对于“完全控制”机器人类型,这些功能是通过 slotcar
插件 实现的。该插件订阅 /robot_path_requests
和 /robot_mode_requests
主题,并响应其 rmf_fleet_adapter
发布的相关 PathRequest
和 ModeRequest
消息。该插件还将机器人的状态发布到 /robot_state
主题。
为了通过 PathRequest
消息中的路径点来导航机器人,我们利用了一种简单的
“轨道式”导航算法,该算法使机器人沿着直线从当前位置加速和减速到下一个路径点。
该插件依赖于以下基本假设:
- 机器人模型为两轮差速驱动机器人
- 左、右轮关节分别命名为“joint_tire_left”和“joint_tire_right”
其他参数(其中大部分是机器人的运动学特性)是从 sdf 参数推断出来的:
<plugin name="slotcar" filename="libslotcar.so">
<nominal_drive_speed>0.5</nominal_drive_speed>
<nominal_drive_acceleration>0.25</nominal_drive_acceleration>
<max_drive_acceleration>0.75</max_drive_acceleration>
<nominal_turn_speed>0.6</nominal_turn_speed>
<nominal_turn_acceleration>1.5</nominal_turn_acceleration>
<max_turn_acceleration>2.0</max_turn_acceleration>
<tire_radius>0.1</tire_radius>
<base_width>0.3206</base_width>
<stop_distance>0.75</stop_distance>
<stop_radius>0.75</stop_radius>
</plugin>
在模拟过程中,假设机器人的路径没有静态障碍物,但插件仍包含逻辑,如果在路径中检测到障碍物,则暂停机器人的运动。虽然可以部署基于传感器的导航堆栈,但避免使用这种方法,以尽量减少系统在模拟中为每个机器人运行导航堆栈的计算负载。鉴于重点是异构车队的交通管理而不是机器人导航,slotcar
插件提供了一种有效的方法来模拟 RMF 核心系统和机器人之间的交互。
slotcar
插件旨在作为一种通用解决方案。我们鼓励供应商开发和分发能够更准确地代表其机器人功能和与 RMF 集成水平的插件。
Doors
与几何形状固定且可直接包含在生成的 .world
文件中的机器人模型不同,门是在 traffic_editor
中自定义的,并具有自己的生成管道。如下图所示,带注释的门具有多个属性,包括其末端的位置、门的类型(铰链门、双铰链门、滑动门、双滑动门)及其运动范围(对于铰链门)。
building_map_generator gazebo
脚本解析 .building.yaml
文件中的任意门,并自动生成带有门所需的链接和接头的 sdf 子元素以及配置的插件。上图中为门生成的 sdf 子元素如下所示。
<model name="coe_door">
<pose>8.077686357313898 -5.898342045416362 0.0 0 0 1.1560010438234292</pose>
<plugin filename="libdoor.so" name="door">
<v_max_door>0.5</v_max_door>
<a_max_door>0.3</a_max_door>
<a_nom_door>0.15</a_nom_door>
<dx_min_door>0.01</dx_min_door>
<f_max_door>500.0</f_max_door>
<door left_joint_name="left_joint" name="coe_door" right_joint_name="empty_joint" type="SwingDoor" />
</plugin>
<link name="left">
<pose>0 0 1.11 0 0 0</pose>
<visual name="left">
<material>
<ambient>120 60 0 0.6</ambient>
<diffuse>120 60 0 0.6</diffuse>
</material>
<geometry>
<box>
<size>0.8766026166317483 0.03 2.2</size>
</box>
</geometry>
</visual>
<collision name="left">
<surface>
<contact>
<collide_bitmask>0x02</collide_bitmask>
</contact>
</surface>
<geometry>
<box>
<size>0.8766026166317483 0.03 2.2</size>
</box>
</geometry>
</collision>
<inertial>
<mass>50.0</mass>
<inertia>
<ixx>20.17041666666667</ixx>
<iyy>23.36846728119012</iyy>
<izz>3.20555061452345</izz>
</inertia>
</inertial>
</link>
<joint name="left_joint" type="revolute">
<parent>world</parent>
<child>left</child>
<axis>
<xyz>0 0 1</xyz>
<limit>
<lower>-1.57</lower>
<upper>0</upper>
</limit>
</axis>
<pose>0.44330130831587417 0 0 0 0 0</pose>
</joint>
</model>
门插件 响应 DoorRequest
消息,其中 door_name
与其 model name
sdf 标签匹配。这些消息通过 /door_requests
主题发布。该插件与定义的门类型无关,并依靠 left_joint_name
和 right_joint_name
参数来确定在打开和关闭运动期间要启动哪些关节。在这些运动期间,关节被命令达到其在父元素中指定的适当限度。关节运动遵循 sdf 参数指定的运动学约束,同时遵循与 slotcar
类似的加速度和减速度曲线。
为了避免一个机器人请求另一个机器人关闭门的情况,在实践中部署了一个 door_supervisor
节点。该节点发布到 /door_requests
并订阅 /adapter_door_requests
,当机器人需要通过门进入时,队列适配器会发布到该节点。door_supervisor
会跟踪系统中所有队列适配器的请求,并将请求转发给门适配器,同时避免上述冲突。
Lifts
测试电梯集成的能力至关重要,因为这些系统通常是设施中的运营瓶颈,因为它们由人类和多机器人车队共同使用。与带注释的门一样,电梯可以在“traffic_editor”GUI 中以多种方式进行自定义,包括机舱的尺寸和方向以及将机舱门映射到建筑物楼层。
building_map_generator gazebo
脚本解析 .building.yaml
文件中的电梯定义,并自动生成客舱、客舱门和电梯井门的 sdf 元素。
客舱底部定义了一个棱柱形关节,由电梯插件驱动,使客舱在不同楼层之间移动。
虽然客舱门是客舱结构的一部分,但井门固定在建筑物上。
两组门在给定的楼层同时打开和关闭,并由电梯插件本身控制。
这些门使用与建筑物中其他门相同的方法创建,并且还包括门插件。
building_map_generator
还附加了一个升降机插件(TODO 将带有所需参数的链接元素添加到升降机的模型 sdf 块。)
<plugin filename="liblift.so" name="lift">
<lift_name>Lift1</lift_name>
<floor elevation="0.0" name="L1">
<door_pair cabin_door="CabinDoor_Lift1_door1" shaft_door="ShaftDoor_Lift1_L1_door1" />
</floor>
<floor elevation="10.0" name="L2">
<door_pair cabin_door="CabinDoor_Lift1_door1" shaft_door="ShaftDoor_Lift1_L2_door1" />
<door_pair cabin_door="CabinDoor_Lift1_door2" shaft_door="ShaftDoor_Lift1_L2_door2" />
</floor>
<floor elevation="20.0" name="L3">
<door_pair cabin_door="CabinDoor_Lift1_door1" shaft_door="ShaftDoor_Lift1_L3_door1" />
</floor>
<reference_floor>L1</reference_floor>
<v_max_cabin>2.0</v_max_cabin>
<a_max_cabin>1.2</a_max_cabin>
<a_nom_cabin>1.0</a_nom_cabin>
<dx_min_cabin>0.001</dx_min_cabin>
<f_max_cabin>25323.0</f_max_cabin>
<cabin_joint_name>cabin_joint</cabin_joint_name>
</plugin>
该插件订阅 /lift_requests
主题,并使用与其 model name
sdf 标签匹配的 lift_name
来响应 LiftRequest
消息。
计算机舱当前高度与 destination_floor
高度之间的位移,并对机舱关节施加合适的速度。
在任何运动之前,机舱门都会关闭,并且只有在 LiftRequest
消息中指定的情况下,才会在 destination_floor
处打开。
由于机舱和井道门配置了 door
插件,因此它们通过 lift
插件发布的 DoorRequest
消息进行命令。
类似于 door_supervisor
,在实践中启动了一个 lift_supervisor
节点 来管理来自不同机器人车队的请求。
Workcells
一种常见的用例是机器人在设施内执行交付,因此“交付”任务被配置到“rmf_fleet_adapters”中。 在交付任务中,有效载荷在一个位置(拾取路径点)装载到机器人上,并在另一个位置(卸下路径点)卸载。 设施中的机器人/工作单元可以自动将有效载荷装载到机器人上和从机器人上卸载。这些设备此后分别称为分配器和摄取器。
为了在模拟中复制装载和卸载过程,我们设计了 TeleportDispenser
和 TeleportIngestor
插件。
这些插件分别附加到 TeleportDispenser
和 TeleportIngestor
3D 模型。
要在模拟中设置有效载荷装载站:
- 在拾取航点旁边添加一个
TeleportDispenser
模型并为其分配一个 唯一的name
- 在
TeleportDispenser
模型旁边添加有效载荷模型(下图中的可乐罐) 要在模拟中设置有效载荷卸载站: - 在卸货航点旁边添加一个
TeleportIngestor
模型并为其分配一个 唯一的name
当发布 DispenserRequest
消息时,如果 target_guid
与 TeleportDispenser
模型的名称匹配,则插件会将有效载荷传送到最近的机器人模型上。相反,当发布 IngestorRequest
消息时,如果 target_guid
与 TeleportIngestor
模型的名称匹配,则 TeleportIngestor
插件会将有效载荷从机器人传送到其在世界上的位置。这些插件的组合允许模拟交付请求。
将来,这些机制将被实际的工作单元或机器人手臂取代,但底层消息交换将保持不变。
Crowdsim
人群模拟,又名“CrowdSim”,是 RMF 模拟中的一项可选功能。用户可以选择在“rmf_traffic_editor”上启用 crowdsim。在 RMF 中,crowdsim 插件使用 menge 作为核心来控制世界上每个模拟代理。
rmf_demos 的“airport_world”上演示了 crowdsim 的一个例子:
ros2 launch rmf_demos_gz airport_terminal.launch.xml use_crowdsim:=1
有关 crowdsim
的工作原理和配置方法的更多详细信息,
请深入了解 使用 Crowdsim 的详细指南。
创建模拟和运行场景
本节旨在概述 rmf_demos
存储库 中的各个组件,这些组件可作为设置其他模拟和为机器人分配任务的参考。在这里,我们将重点关注“办公室”世界。
地图包
rmf_demos_maps
包包含带注释的 traffic_editor
文件,这些文件将用于生成 3D 世界。在 traffic_editor
中打开 office.project.yaml
文件会显示一个单层平面图,其中标注了墙壁、地板、比例尺、门、车道和模型。所有机器人车道都设置为 双向
,graph_idx
等于“0”。后者表示所有车道都属于同一车队。在 airport
世界中,我们有两组图表,索引分别为“0”和“1”,分别反映两个车队可占用的车道。下图突出显示了分配给车道和作为机器人生成位置的航点的属性。
要导出 3D 世界文件以及导航图,请使用 building_map_generator
脚本。此包的 CMakeLists.txt
文件配置为在构建包时自动运行生成器脚本。输出安装到包的 share/
目录中。这样,演示中的其他包就可以轻松找到并使用生成的文件。
foreach(path ${traffic_editor_paths})
# Get the output world name
string(REPLACE "." ";" list1 ${path})
list(GET list1 0 name)
string(REPLACE "/" ";" list2 ${name})
list(GET list2 -1 world_name)
set(map_path ${path})
set(output_world_name ${world_name})
set(output_dir ${CMAKE_CURRENT_BINARY_DIR}/maps/${output_world_name})
set(output_world_path ${output_dir}/${output_world_name}.world)
set(output_model_dir ${output_dir}/models)
# first, generate the world
add_custom_command(
OUTPUT ${output_world_path}
COMMAND ros2 run rmf_building_map_tools building_map_generator gazebo ${map_path} ${output_world_path} ${output_model_dir}
DEPENDS ${map_path}
)
add_custom_target(generate_${output_world_name} ALL
DEPENDS ${output_world_path}
)
# now, generate the nav graphs
set(output_nav_graphs_dir ${output_dir}/nav_graphs/)
set(output_nav_graphs_phony ${output_nav_graphs_dir}/phony)
add_custom_command(
OUTPUT ${output_nav_graphs_phony}
COMMAND ros2 run rmf_building_map_tools building_map_generator nav ${map_path} ${output_nav_graphs_dir}
DEPENDS ${map_path}
)
add_custom_target(generate_${output_world_name}_nav_graphs ALL
DEPENDS ${output_nav_graphs_phony}
)
install(
DIRECTORY ${output_dir}
DESTINATION share/${PROJECT_NAME}/maps
)
endforeach()
启动文件
rmf_demos
包包含启动模拟世界和启动各种 RMF 服务所需的所有基本启动文件。使用 office.launch.xml
文件启动办公室模拟。首先,加载并启动 common.launch.xml
文件:
rmf_traffic_schedule
节点负责维护机器人轨迹数据库并监控交通冲突。如果检测到冲突,则向相关车队适配器发送通知,这些适配器开始协商过程以找到最佳解决方案。building_map_server
发布 UI 用于可视化的BuildingMap
消息。可执行文件将相关.building.yaml
文件的路径作为参数。使用find-pkg-share
替换命令可以找到rmf_demos_maps
包安装的office.building.yaml
文件,该文件存储在config_file
参数中。rmf_schedule_visualizer
是一个基于 RViz 的 UI,用于可视化车道、机器人的实际位置、反映在rmf_traffic_schedule
中的机器人预期轨迹以及门和电梯等建筑系统的状态。door_supervisor
和lift_supervisor
节点用于管理车队适配器和 UI 提交的请求。
<!-- Common launch -->
<include file="$(find-pkg-share demos)/common.launch.xml">
<arg name="use_sim_time" value="true"/>
<arg name="viz_config_file" value ="$(find-pkg-share demos)/include/office/office.rviz"/>
<arg name="config_file" value="$(find-pkg-share rmf_demos_maps)/office/office.building.yaml"/>
</include>
要在 Gazebo 中启动模拟世界,下面显示了来自 rmf_demos_gz 的代码片段。同样,用户也可以选择使用 ignition 模拟器 rmf_demos_ign 运行
<group>
<let name="world_path" value="$(find-pkg-share rmf_demos_maps)/maps/office/office.world" />
<let name="model_path" value="$(find-pkg-share rmf_demos_maps)/maps/office/models:$(find-pkg-share rmf_demos_assets)/models:/usr/share/gazebo-9/models" />
<let name="resource_path" value="$(find-pkg-share rmf_demos_assets):/usr/share/gazebo-9" />
<let name="plugin_path" value="$(find-pkg-prefix rmf_gazebo_plugins)/lib:$(find-pkg-prefix building_gazebo_plugins)/lib" />
<executable cmd="gzserver --verbose -s libgazebo_ros_factory.so -s libgazebo_ros_init.so $(var world_path)" output="both">
<env name="GAZEBO_MODEL_PATH" value="$(var model_path)" />
<env name="GAZEBO_RESOURCE_PATH" value="$(var resource_path)" />
<env name="GAZEBO_PLUGIN_PATH" value="$(var plugin_path)" />
<env name="GAZEBO_MODEL_DATABASE_URI" value="" />
</executable>
<executable cmd="gzclient --verbose $(var world_path)" output="both">
<env name="GAZEBO_MODEL_PATH" value="$(var model_path)" />
<env name="GAZEBO_RESOURCE_PATH" value="$(var resource_path)" />
<env name="GAZEBO_PLUGIN_PATH" value="$(var plugin_path)" />
</executable>
</group>
最后,为地图中注释的每种机器人类型启动“完全控制”rmf_fleet_adapter
的实例。building_map_generator
脚本生成的每个车队的导航图通过 nav_graph_file
参数传递。对于办公室地图,定义了一个 Magni
机器人车队。因此,启动一个配置了此机器人类型的运动学属性以及用于规划的空间阈值的单个 magni_adapter.launch.xml
文件。与车队适配器一起启动的还有 robot_state_aggregator
节点。此节点使用包含 robot_prefix
参数的 RobotState.name
聚合 RobotState
消息,并使用 fleet_name
参数指定的 FleetState.name
将聚合发布到 /fleet_states
。
<group>
<let name="fleet_name" value="magni"/>
<include file="$(find-pkg-share rmf_demos)/include/adapters/magni_adapter.launch.xml">
<arg name="fleet_name" value="$(var fleet_name)"/>
<arg name="use_sim_time" value="$(var use_sim_time)"/>
<arg name="nav_graph_file" value="$(find-pkg-share rmf_demos_maps)/maps/office/nav_graphs/0.yaml" />
</include>
<include file="$(find-pkg-share rmf_fleet_adapter)/robot_state_aggregator.launch.xml">
<arg name="robot_prefix" value="magni"/>
<arg name="fleet_name" value="$(var fleet_name)"/>
<arg name="use_sim_time" value="true"/>
</include>
</group>
在使用硬件测试 RMF 时,可以使用相同的启动文件,但启动“Gazebo”除外。 有关使用硬件运行演示的更多信息,请参阅有关集成的章节。
任务请求
RMF 支持各种开箱即用的任务。有关更多信息,请参阅 RMF 中的任务
提供基于 Web 的仪表板,允许用户向 RMF 发送命令。
启动 仪表板服务器 后,可通过 https://open-rmf.github.io/rmf-panel-js/ 访问。
另外,在“rmf_demos_tasks”中还存在几个脚本,可帮助用户从终端提交请求。目前,“dispatch_loop.py”、“dispatch_delivery.py”和“dispatch_clean.py”脚本可用于提交“Loop”、“Delivery”和“Clean”请求。
结论
本章介绍了如何使用 traffic_editor
工具创建带注释的地图,以便自动生成用于模拟的 3D 世界。
它还介绍了模拟中使用的资产以及 ROS 2 和 RMF 与它们交互所需的相应插件。
提供了一个这些组件一起运行的工作示例,以 rmf_demos_maps
包的形式提供,作为如何实现自定义系统的参考。
下一章将介绍 RMF 背后的基本概念。
RMF Core Overview
本章介绍了 RMF,它是一系列开放规范和软件工具的总称,旨在简化机器人系统、建筑基础设施和用户界面的集成和互操作性。rmf_core
包括:
- rmf_traffic: Core scheduling and traffic management systems
- rmf_traffic_ros2: rmf_traffic for ros2
- rmf_task: Task planner for rmf
- rmf_battery: rmf battery estimation
- rmf_ros2: ros2 adapters and nodes and python bindings for rmf_core
- rmf_utils: utility for rmf
Traffic deconfliction
避免移动机器人交通冲突是 rmf_core
的一项关键功能。
交通冲突解决有两个层次:(1) 预防,(2)
解决。
Prevention
尽可能地防止交通冲突是最好的情况。
为了促进交通冲突预防,我们实施了一个与平台无关的交通时间表数据库。交通时间表是一个动态数据库,其内容将随时间变化,以反映延误、取消或路线变更。所有集成到 RMF 部署中的车队经理都必须向交通时间表报告其车辆的预期行程。借助时间表上提供的信息,合规的车队经理可以为他们的车辆规划路线,避免与任何其他车辆发生冲突,无论它们属于哪个车队。rmf_traffic
提供了一个 Planner
类,以帮助为表现得像标准 AGV(自动导引车)的车辆提供便利,严格遵循预定网格的路线。未来,我们打算为 AMR(自主移动机器人)提供类似的实用程序,使其能够针对意外障碍物执行临时运动规划。
Negotiation
并非总能完美地防止交通冲突。
移动机器人可能会因为其环境中的意外障碍而遇到延误,或者预测的时间表可能由于多种原因而存在缺陷。
在发生冲突的情况下,rmf_traffic
有一个协商方案。
当交通时间表数据库检测到两个或多个时间表参与者之间即将发生冲突时,它将向相关车队经理发送冲突通知,车队经理之间的协商将开始。每个车队经理将提交其首选行程,并且每个车队经理都将以可以容纳其他行程的行程做出回应。第三方法官(由系统集成商部署)将选择一组被认为更可取的提案,并通知车队经理他们应该遵循哪些行程。
可能会出现需要执行突然的紧急任务(例如,应对紧急情况)的情况,而当前的交通时间表无法及时满足该任务。在这种情况下,交通参与者可能会故意将交通冲突发布到时间表上并强制进行协商。通过实施第三方法官始终偏向高优先级参与者,可以强制协商选择有利于紧急任务的行程安排。
Traffic Schedule
交通时间表是设施内所有预期机器人交通轨迹的集中数据库。请注意,它包含预期轨迹;它正在展望未来。时间表的工作是识别不同机器人车队意图中的冲突,并在发现冲突时通知车队。收到通知后,车队将开始交通协商,如上所述。
Fleet Adapters
参与 RMF 部署的每个机器人车队都应有一个车队适配器,用于将其特定于车队的 API 连接到核心 RMF 交通调度和协商系统的接口。车队适配器还负责处理车队与各种标准化智能基础设施接口之间的通信,例如开门、召唤电梯和唤醒分配器。
不同的机器人车队具有不同的特性和能力,这取决于它们的设计和开发方式。交通调度和协商系统不会对车队的能力做出任何假设。然而,为了最大限度地减少集成工作的重复,我们确定了 4 种不同的控制类别,我们预计现实世界中各种车队经理可能会遇到这些类别。
Fleet adapter type | Robot/Fleetmanager API feature set | Remarks |
---|---|---|
Full Control |
| RMF is provided with live status updates and full control over the paths that each individual mobile robot uses when navigating through the environment. This control level provides the highest overall efficiency and compliance with RMF, which allows RMF to minimize stoppages and deal with unexpected scenarios gracefully. (API available) |
Traffic Light |
| RMF is given the status as well as pause/resume control over each mobile robot, which is useful for deconflicting traffic schedules especially when sharing resources like corridors, lifts and doors. *(API available) |
Read Only |
| RMF is not given any control over the mobile robots but is provided with regular status updates. This will allow other mobile robot fleets with higher control levels to avoid conflicts with this fleet. Note that any shared space is allowed to have a maximum of just one "Read Only" fleet in operation. Having none is ideal. (Preliminary API available) |
No Interface | Without any interface to the fleet, other fleets cannot coordinate with it through RMF, and will likely result in deadlocks when sharing the same navigable environment or resource. This level will not function with an RMF-enabled environment. (Not compatible) |
简而言之,舰队与 RMF 的协作性越高,所有舰队和系统就能越和谐地协同运行。 请再次注意,共享空间中只能有一个“只读”舰队,因为任何两个或更多这样的舰队都将几乎不可能避免死锁或资源冲突。
目前,我们提供了可重复使用的 C++ API(以及 Python 绑定),用于集成车队管理的 完全控制 类别。 初步的 ROS 2 消息 API 可用于 只读 类别,但该 API 将在未来版本中弃用,转而使用 C++ API (Python 绑定 可用)。 交通灯 控制类别与核心 RMF 调度系统兼容,但我们尚未为其实现可重复使用的 API。 要实现 交通灯 车队适配器,系统集成商必须直接使用核心交通调度和协商 API,以及实现与各种基础设施 API(例如门、电梯和分配器)的集成。
完全控制 类别的 API 在“集成”章节的 移动机器人舰队 部分中描述,只读 类别的 API 在“集成”章节的 只读舰队 部分中描述。
Frequently Asked Questions
常见问题
RMF 具有许多系统设计约束,这些约束为交通管理带来了独特的挑战。RMF 的核心目标是促进不同供应商提供的异构移动机器人车队的系统集成,这些车队可能具有不同的技术能力。
供应商往往希望他们的计算系统独立于其他供应商。由于供应商通常负责确保其计算基础设施的正常运行时间和可靠性,因此他们可能认为与其他供应商共享计算资源是一种不可接受的责任。这意味着交通管理系统必须能够在网络上分布在不同机器上时运行。
不同的机器人平台可能具有不同的功能。目前部署的许多有价值的 AGV 平台无法动态更改其行程。一些 AGV 平台可以在收到指示时改变路线,只要它们遵循预定义的导航图即可。一些 AMR 平台可以动态地绕过其环境中的意外障碍物。由于 RMF 旨在成为一种使能技术,因此我们必须设计一个系统,使其能够最大限度地发挥所有这些不同类型系统的效用,而不会对它们中的任何一个施加不利的限制。
这些考虑导致了分布式冲突预防和分布式调度协商的当前设计。设计中有足够的空间来为符合某些要求的移动机器人类别创建更简单、更高效的子集,但这些优化可以在现有的完全通用框架之上构建,然后再添加。
谁来开门、关门和操作lifts?是机器人还是 RMF?还是两者兼而有之?
知道何时需要打开门并发送打开命令的责任属于“舰队适配器”。基本设计是:
- 车队适配器跟踪机器人的进度
- 当机器人需要通过一扇门时,车队适配器会识别这一点
- 车队适配器将向门发送打开信号
- 一旦门打开,车队适配器将命令机器人继续前进
- 一旦机器人通过门,车队适配器将命令机器人等待,直到门关闭
- 车队适配器将命令门关闭
- 一旦门关闭,车队适配器将命令机器人继续前进
车队适配器了解门的方式是通过解析提供给它的导航
图。导航图是 full_control
类型的车队适配器的必需参数。rmf_demos
显示了向车队适配器提供导航图的示例。
构建导航图的推荐方法是使用 traffic-editor
工具。rmf_demos
存储库显示了 traffic-editor
项目文件的一些示例。
但是,完全可以构建您自己的导航图。它们 使用 YAML 格式。
Are lifts supported?
适当的电梯支持(即指定可以在楼层之间移动的实际电梯,并将该信息导出到导航图中)尚未开发。
但是,出于测试和演示目的,有两个特殊的导航图边缘属性可允许 RMF 队列适配器模拟电梯使用情况。这适用于演示场景,其中已创建“模拟电梯”,该电梯接收电梯命令并传输电梯状态,但实际上不会在建筑物的任何不同楼层之间移动。例如,在实验室的地板上贴上胶带以指示“电梯舱”框,以便在不占用实际建筑物电梯的情况下进行开发和测试。
这些属性最初是为了演示目的而包含的,但它们被证明非常有用,我们可能会使它们成为官方支持的属性。由于“真实”电梯的成本和稀缺性,人们似乎对拥有模拟多层场景的单层硬件测试设置有着广泛的兴趣。
边缘属性为:
demo_mock_floor_name
: 机器人穿越边缘时所在楼层的名称demo_mock_lift_name
: 机器人穿越边缘时进入或离开的升降机的名称
这个想法是,如果您有一个单层演示环境,但想要演示与电梯的互动,那么您可以设置一个模拟“电梯”,并想象“电梯”的每一侧都通向不同的楼层,并且只有当“电梯”认为它在该楼层时,机器人才被允许进入/离开“电梯”的那一侧。这模拟了有两组门的电梯舱。
为了使这个想法更加具体,假设您有一个单层硬件测试区域,并且在地面上画了一个框,旁边有一个 LED 显示屏,显示模拟的楼层名称。模拟电梯将传输与 LED 显示的楼层相匹配的电梯状态消息。还有一些指示电梯门是打开还是关闭。您可以进一步想象,只有当电梯认为它在 L1 楼层时,才允许从“电梯”的西侧进入或离开,而只有当它认为它在 L3 楼层时,才允许从东侧进入或离开“电梯”。
在该设置中,为了让机器人“正确地”从 L1 上的航点导航到 L3 上的航点,机器人需要:
- 从西侧接近“电梯”
- 呼叫“电梯”下行至 L1
- 等待电梯状态显示电梯位于 L1 层且门打开
- 进入“电梯”(即地面上画出的方框)并请求电梯“移动”至 L3
- 等待“电梯”指示电梯已到达 L3 层且门打开
- 从东侧离开“电梯”
粗略的 ASCII 图表如下所示(数字为航点,字母为边):
1 <---a---> 2 <---b---> 3
- Waypoint 1 is on floor L1
- Waypoint 2 is inside the "lift" named LIFT001
- Waypoint 3 is on floor L3
- The properties of edge
a
are:- bidirectional: true
- demo_mock_floor_name: L1
- demo_mock_lift_name: LIFT001
- The properties of edge
b
are:- bidirectional: true
- demo_mock_floor_name: L3
- demo_mock_lift_name: LIFT001
如果多个舰队可以执行相同的任务,那么会选择哪一个?
虽然尚未实施,但已经设计了一个竞标系统,其中任务请求将转换为竞标请求。竞标请求将发送给每个舰队适配器,每个可以执行任务的舰队适配器将报告其完成任务所需的最佳估计。出价最低的舰队适配器将被分配任务。
API 和实施正在等待一些关键组件的最终完成。
一些机器人可以比其他机器人享有优先权吗?
协商系统概念确实支持优先考虑哪个机器人将容纳其他机器人。解决协商时可以使用任意度量或加权系统。但在我们当前使用的实施中,我们将所有车辆视为平等,并选择最小化所有机器人净延迟的解决方案,没有任何优先级或权重。
Since this codebase is open source, you can easily fork the code and modify it
to use any prioritization system that you'd like. Specifically, replace
rmf_traffic::schedule::QuickestFinishEvaluator()
with your own
Negotiation::Evaluator
class that behaves in whatever way you would like.
两个机器人之间保持多少距离?
这是可配置的。有两个相关参数:footprint_radius
和 vicinity_radius
。footprint_radius
表示车辆物理足迹的估计值。vicinity_radius
表示机器人需要其他车辆避开的区域的估计值。“计划冲突”定义为一辆车辆的“足迹”计划进入另一辆车辆的“附近”的情况。协商系统的工作是提出一个修复计划,使所有车辆的“足迹”远离所有其他车辆的“附近”。
作业调度是如何实现的?
调度规划器模块目前正在开发中。 到目前为止,为 RMF 开发的演示平台不需要规划器将任务调度到不同的车队,因为在迄今为止(2020 年底)完成的所有演示平台中,每种任务类型只能由一个(且只有一个)机器人车队执行。 因此,任务调度非常简单:任务分配给能够执行任务的任何车队,而其余车队则忽略任务请求。 我们目前正在开发一个真正的调度规划器和正式的任务竞标系统,我们计划将其纳入 1.2.0 版本,定于 2020 年 12 月底发布。 我们的想法是,每个可以执行任务请求的车队都会对执行任务的“成本”进行出价,出价最低的“成本”将成为赢家。 “成本”将由两个因素决定:
- 任务完成的速度
- 如果新任务需要抢占其他任务,其他任务会延迟多长时间
What is rmf_traffic
?
rmf_traffic
提供核心流量调度算法和实用程序的中间件中立实现。它不使用或依赖于 ROS 2。
What is rmf_traffic_ros2
?
rmf_traffic_ros2
提供了方便的包装器,用于将“rmf_traffic”用作分布式 ROS 2 系统的一部分。
Where is the costmap?
rmf_core
中没有成本地图表示。
成本地图是一种通常用于表示自主导航规划的体积占用率的方法。
虽然交通设施确实处理导航规划,但它们主要关注的是识别自动驾驶汽车预期路线之间的冲突。
交通设施不负责车辆在其环境中绕过静态障碍物的“本地”导航。
不同的机器人平台通常具有不同的成本地图或其他导航算法表示,其中许多可能是专有的。
最重要的是,由于机器人足迹的差异,具有相同成本地图表示的不同机器人平台可能仍需要不同的成本地图值。
由于这些因素,我们让机器人平台自己决定如何表示其成本地图。如果系统集成商在执行交通规划时考虑机器人的成本地图很重要,他们可以实现自定义 rmf_traffic::agv::RouteValidator
,在确定候选路线是否有效时使用机器人的自定义成本地图。
What is the core algorithm behind rmf_traffic
?
rmf_traffic
中 AGV 的冲突避免是通过对 A* 搜索 进行时间相关扩展来实现的
此搜索将时间考虑在内,以便能够找到穿越空间和时间的路径,这些路径可以考虑交通计划中其他代理的运动。
During negotiation, how do fleets compute their proposals?
由于该系统设计为可扩展且可适应各种场景和机器人供应商组合,包括许多目前尚不存在的组合,因此它有许多扩展的部分和挂钩。计算交通提案的顺序如下:
- 首先,车队适配器节点将收到冲突通知,告知它需要参与协商以解决时空冲突。此通知由
rmf_traffic_ros2::schedule::Negotiation
类接收。 - 为此,车队适配器创建协商通知订阅,以便在其控制下的特定机器人需要响应协商通知时,它会收到通知。
- 当机器人需要响应协商时,其实现就会被触发。
- 此实现将启动一个多线程的
Negotiate
服务,其主要实现可以在这里 找到。
多方协商中的每一步都以完全相同的方式使用 Negotiate
服务。
各个步骤之间的唯一区别在于它们需要处理哪些约束。
这些约束由传递给 respond(~)
函数的 rmf_traffic::schedule::Negotiation::Table::Viewer
对象描述。
因为可以使用相同的对象来描述图中所有不同块的约束,所以我们可以使用相同的代码来解决每个块。
还有可以根据需要调用的“拒绝”和“放弃”代码路径:
- 当其中一个舰队无法接受来自另一个舰队的提议时,将使用“拒绝”机制。 当执行拒绝时,拒绝舰队将提供一组可行轨迹(通常为 10-200 条轨迹),而收到拒绝的舰队应再次尝试为自己寻找理想的提议,但该理想提议必须至少接受拒绝时提供的一条轨迹替代方案。
- 当规划者很难找到任何解决方案时,将使用“放弃”机制。 当谈判中有许多参与者都在积极行动时,就会发生这种情况,这可能导致由于时间不一致而看似无法解决的情况。 通常,当使用放弃时,谈判会找到另一种可行的解决方案组合。 在最坏的情况下,如果谈判继续失败,机器人可能会遇到现实生活中的僵局。 当发生死锁时,参与者将保持静止,因此协商将达到稳定状态,不会受到异步不一致的负面影响。 当发生这种情况时,几乎可以保证成功解决。
前面的解释描述了“完全控制”风格的舰队适配器。“只读”和“交通灯”API 的实现略有不同。
Tasks in RMF
RMF 简化了跨多机队系统的任务分配和管理。 当用户提交新任务请求时,RMF 将智能地将其分配给机队中能够最佳执行任务的机器人。当
RMF 支持三种类型的现成任务请求:
- 清洁:适用于能够清洁设施内地板空间的机器人
- 配送:适用于能够在设施内不同位置之间配送物品的机器人
- 循环:适用于能够在设施内不同位置之间来回导航的机器人
注意:单个机器人可能能够执行上述一项或多项任务,并且可以配置队列适配器以反映其机器人的能力。 有关支持的任务类型的更多信息,请单击此处
在 RMF 21.04 及更高版本中,任务将根据由 Dispatcher 节点 rmf_dispatcher_node
协调的竞标过程的结果授予机器人队列。
当 Dispatcher 从仪表板或终端收到新任务请求时,它会向所有队列适配器发送 rmf_task_msgs/BidNotice
消息。如果队列适配器能够处理该请求,它会向 Dispatcher 提交 rmf_task_msgs/BidProposal
消息,并附带完成该任务所需的成本。队列适配器使用 rmf_task::agv::TaskPlanner
实例来确定如何最好地满足新请求。有关任务规划器的更多信息,请单击此处
然后,调度员将比较收到的所有“BidProposals”,并提交“rmf_task_msgs/DispatchRequest”消息,其中包含中标机器人的车队名称。调度员评估提案的方式有几种,例如最快完成、最低成本等,这些都可以配置。
电池充电与新的任务规划器紧密集成。当机器人的电量不足以完成一系列任务时,“ChargeBattery”任务将最佳地注入机器人的计划中。目前,我们假设地图中的每个机器人都有一个专用的充电位置,如交通编辑器地图中用“is_charger”选项注释的那样。
RMF Task Allocation Planner
即将推出...
Supported Tasks in RMF
Clean Task:
清洁机器人在各种设施中越来越受欢迎。虽然清洁方式多种多样(吸尘、拖地、消毒等),因此清洁机器人的种类也很多,但它们的工作流程都相同。设施中的地板空间被划分为多个“区域”或子区域以进行清洁。每个区域都有机器人的起点和终点。在这些位置之间,机器人在执行清洁操作时会沿着特殊路径移动。
RMF 完全支持各种清洁机器人的集成。此外,RMF 可以根据能力和可用资源智能地将清洁工作分配给可用的清洁机器人,同时优化整体生产力。大多数清洁机器人都为每个区域预先配置了清洁程序,可以从给定的起点运行。 RMF 的目标是引导机器人到达这个起点,触发清洁程序的执行,然后在清洁完成后将机器人引导到等待路径点。 RMF 中设计了一个“清洁”任务来协调此行为。
本节的其余部分概述了将清洁机器人与 RMF 集成所需的步骤。rmf_demos
中的 airport_terminal
示例是一个有用的参考。它展示了两个品牌的清洁机器人的集成:CleanerBotA
和 CleanerBotE
,它们分别在导航图 Graph 0
和 Graph 4
上运行。
Step 1: Defining waypoints for cleaning in Traffic Editor
需要在机器人的导航图中添加两个航点。第一个是机器人应启动其清洁程序的航点。在下图中,此点标记为“zone_1_start”。一旦机器人完成其清洁程序,RMF 将引导机器人返回此航点。连接到此航点的是航点“zone_1”,其“dock_name”属性设置为其名称。这是机器人完成清洁程序后最终到达的航点。在当前实现中,重要的是将这些航点的名称分别设置为“<zone_name>_start”和“<zone_name>”。当机器人从“zone_1_start”进入“zone_1”的通道时,车队适配器将请求机器人启动其对接(在本例中为清洁)程序。将“dock_name”参数设置为“zone_1”将导致车队适配器触发“RobotCommandHandle::dock()”函数。因此,用户实现该函数时应依次向机器人发出 API 调用,以开始清洁指定区域。清洁过程完成后,“RobotCommandHandle”应触发“docking_finished_callback()”。
Note: 为了触发
DockPhase
,车道的方向需要从<zone_name>_start
到<zone_name>
。
第 2 步:发布 DockSummary 消息
为了估计清洁过程中的资源消耗(这对于最佳任务分配规划至关重要),车队适配器需要机器人在清洁时将穿越的航点列表。
此信息可在发布到 /dock_summary
主题的 DockSummary 消息中进行汇总。
mock_docker 节点负责发布此信息。
它接受一个 yaml
配置文件,其中包含每个机队每个区域的航路点列表,用于填充 DockSummary
消息。
对于 airport_terminal
演示,文件位于 此处
步骤 3:配置舰队适配器以接受清理任务
舰队适配器需要配置为接受 Clean
类型的任务。否则,在任务竞标期间,它不会向调度程序节点提交此任务的竞标。
如果正在使用旧版 full_control
适配器,则需要在适配器启动文件中将 perform_cleaning 参数设置为 true
。
对于较新的舰队适配器,应使用以下方式调用 FleetUpdateHandle::accept_task_requests() 方法: AcceptTaskRequest 回调,如果收到带有 TaskProfile.Description.TaskType.TYPE_CLEAN 的请求,则返回 true
。
步骤 4:发送清理请求
如果上述步骤正确完成,则可以通过终端或 RMF_Demo_Panel 向 RMF 提交清理区域的请求。 要从终端发送清理请求,请使用 RMF 获取工作区,然后:
ros2 run rmf_demos_tasks dispatch_clean -cs zone_1 -st 0 --use_sim_time
This will submit a request to RMF to clean zone_1
. The --use_sim_time
argument is only required when testing in simulation. For more information on sending a clean request:
ros2 run rmf_demos_tasks dispatch_clean -h
配送任务:
移动机器人的另一个常见应用是在设施内进行配送。 配送通常涉及机器人前往取货地点,在那里装载物品,然后导航到卸货地点,在那里卸载物品。 在取货和卸货地点,移动机器人可能必须与机械臂、传送带或其他自动化系统交互。我们将装载物品的系统称为“分配器”,将卸载的系统称为“摄取器”。
为了将这些系统与 RMF 核心系统集成,定义了一组 dispenser 和 ingestor 消息。 尽管名称不同,但这些消息足够通用,可供执行类似操作的任何其他系统使用。
RMF 中设计了一个 Delivery
任务,用于引导移动机器人到达分配器所在的拾取位置。到达此处后,其 rmf_fleet_adapter
会发布 DispenserRequest
消息,工作单元会接收该消息并开始处理。
装载成功后,分配器会发布状态为 SUCCESS
的 DispenserResult
消息。
然后,rmf_fleet_adapter
会引导机器人到达摄取器所在的下车航点。
在这里,确保了类似的消息交换。rmf_fleet_adapter
发布 IngestorRequest
消息,指示摄取器卸载其有效载荷。完成后,它会发布状态为 SUCCESS
的 IngestorResult
消息。
要了解如何设置带有分配器和摄取器的模拟,请参阅 模拟
需要将舰队适配器配置为接受“交付”类型的任务。否则,在任务竞标期间,它不会向调度程序节点提交此任务的竞标。
如果正在使用旧版“full_control”适配器,则需要在适配器启动文件中将 perform_deliveries 参数设置为“true”。
对于较新的舰队适配器,应使用以下方式调用 FleetUpdateHandle::accept_task_requests() 方法: AcceptTaskRequest 回调,如果收到带有 TaskProfile.Description.TaskType.TYPE_DELIVERY 的请求,则返回 true
。
要提交“Delivery”请求,可以使用“rmf_demos_tasks”中的“dispatch_delivery”脚本。
ros2 run rmf_demos_tasks dispatch_delivery -h
usage: dispatch_delivery [-h] -p PICKUP -pd PICKUP_DISPENSER -d DROPOFF -di DROPOFF_INGESTOR
[-st START_TIME] [-pt PRIORITY] [--use_sim_time]
optional arguments:
-h, --help show this help message and exit
-p PICKUP, --pickup PICKUP
Start waypoint
-pd PICKUP_DISPENSER, --pickup_dispenser PICKUP_DISPENSER
Pickup dispenser name
-d DROPOFF, --dropoff DROPOFF
Finish waypoint
-di DROPOFF_INGESTOR, --dropoff_ingestor DROPOFF_INGESTOR
Dropoff ingestor name
-st START_TIME, --start_time START_TIME
Start time from now in secs, default: 0
-pt PRIORITY, --priority PRIORITY
Priority value for this request
--use_sim_time Use sim time, default: false
循环任务:
可以提交“循环”任务,以请求机器人在两个航路点之间来回导航给定次数的迭代(循环)。 与“清理”和“交付”任务一样,必须将车队适配器配置为接受“循环”请求。
要提交“循环”请求,可以使用“rmf_demos_tasks”中的“dispatch_loop”脚本。
ros2 run rmf_demos_tasks dispatch_loop -h
usage: dispatch_loop [-h] -s START -f FINISH [-n LOOP_NUM] [-st START_TIME] [-pt PRIORITY]
[--use_sim_time]
optional arguments:
-h, --help show this help message and exit
-s START, --start START
Start waypoint
-f FINISH, --finish FINISH
Finish waypoint
-n LOOP_NUM, --loop_num LOOP_NUM
Number of loops to perform
-st START_TIME, --start_time START_TIME
Start time from now in secs, default: 0
-pt PRIORITY, --priority PRIORITY
Priority value for this request
--use_sim_time Use sim time, default: false
ChargingTask
这是一个自生成任务,由 RMF 车队适配器自生成。当满足充电条件时,机器人将被引导回充电站。
用户可以通过在 traffic_editor
中将 is_parking_spot
设置为 true
来设置有效的充电站。
用户可以通过以下方式在 fleet_adapter.launch.xml
中配置充电条件:
- 充电阈值:
<arg name="recharge_threshold" value="0.2"/>
- 完成请求:
<arg name="finishing_request" value="charge"/>
请注意,目前 finishing_request
参数还支持:[charge, park, nothing]
。这些被视为自动生成的任务。
调试
在某些情况下,当提交任务请求时,调度程序不会收到来自车队适配器的任何出价:
-
车队适配器未配置为接受由 此 函数确定的提交的任务类型。如果您使用的是
full_control
实现,则应在启动文件中使用这些 参数 指示哪些任务类型是可行的。否则,舰队适配器将不会竞标已提交的任务。终端中应发布一条消息,指出:[full_control-15] [INFO] [1617245135.071996222] [tinyRobot_fleet_adapter]:舰队 [tinyRobot] 配置为不接受任务 [Clean0]
。如果您使用的是自定义车队适配器,请确保您正在调用FleetUpdateHandle::accept_task_requests()
。 -
车队适配器无法处理请求,因为请求中的字段无效。例如,如果提交的
loop
请求的起始或结束航点在机器人的导航图上不存在,则不会提交出价。终端中将打印一条解释性消息,例如[full_control-15] [INFO] [1617245206.473805336] [tinyRobot_fleet_adapter]:车队[tinyRobot]在其导航图中没有配置命名航点[bad_waypoint]。拒绝带有task_id的BidNotice:[Loop1]
-
调度员接受出价的持续时间小于车队适配器计算和提交出价所需的时间。持续时间参数在此处指定。如果是这种情况,您仍应在终端中看到一些打印输出,突出显示车队适配器计算并提交了出价:
[full_control-15] [INFO] [1617245621.881365568] [tinyRobot_fleet_adapter]:为 task_id 生成了 Loop 请求:[Loop2] [full_control-15] [INFO] [1617245621.881432804] [tinyRobot_fleet_adapter]:计划用于 [2] 个机器人和 [1] 个请求 [full_control-15] [INFO] [1617245621.886230967] [tinyRobot_fleet_adapter]:已提交 BidProposal 以适应机器人 [tinyRobot2] 的任务 [Loop2],且成本为新值[45.222308]
单击此处 了解如何开发对自定义任务的支持。
User-defined Custom Tasks in RMF Task
注意:用户定义的自定义任务目前处于实验阶段
新的通用任务组合系统正在开发中,讨论可在此处 找到。
处理 RMF 任务时,有两个包:
rmf_task
提供 API 和基类,用于在 RMF 中定义和管理任务。任务被定义为生成阶段的对象,这些阶段是一系列有意义的步骤,可产生理想的结果。每个任务都有一个描述和一个组件,允许我们在给定初始状态的情况下模拟机器人完成任务后的状态,还有一个组件将命令实际机器人执行任务。
rmf_task_sequence
提供了 rmf_task
的开箱即用实现,其中 Task
对象由一系列阶段定义。因此,此类任务生成的阶段将与用于定义它们的阶段序列相匹配。
rmf_task_sequence
中定义的阶段反过来是事件的集合,这些事件还具有用于在事件期间模拟最终状态和命令机器人的组件。目前支持 此处 定义的事件。
然后,用户可以通过将此类阶段/事件的序列串联在一起来构建任务的任意定义。RMF 能够规划和执行此类任务。
perform_action
是一个基于序列的事件,支持执行自定义操作。
perform_action
行为的可定制性是有限的,因此实施自定义逻辑的用户无需担心如何与交通系统交互、开门或使用电梯。这也最大限度地降低了系统集成商引入错误的风险,从而扰乱交通系统或任何其他资源共享系统。当 perform_action
运行时,机器人将成为“只读”交通代理,因此其他机器人将简单地避开它
用户将在 rmf_fleet_adapter
层上工作。在此层中,API 仅限于使用 rmf_task_sequence
来执行任务。仅支持某些事件,它们的描述可以在 此处 找到。
rmf_fleet_adapter
层充当用户可以使用的 API。它仅支持对 此处 中提到的现有事件中的 perform_action
进行自定义行为。
用户只能在 perform_action
中添加自定义任务。RMF 将命令传递给舰队适配器集成的平台特定端,并正式释放对机器人的控制,直到操作完成。
要在 perform_action
中使用自定义任务,用户需要使用 API 的两个部分。
- FleetUpdateHandle::add_performable_action 它由两部分组成:第一部分是动作的“类别”,第二部分是“考虑”部分,将根据该部分决定是否接受该动作。
以下是一个例子:
rmf_fleet:
name: "ecobot40"
limits:
linear: [1.2, 1.5] # velocity, acceleration
angular: [0.6, 0.6]
profile: # Robot profile is modelled as a circle
footprint: 0.5
vicinity: 0.6
reversible: False
battery_system:
voltage: 24.0
capacity: 40.0
charging_current: 26.4
mechanical_system:
mass: 80.0
moment_of_inertia: 20.0
friction_coefficient: 0.20
ambient_system:
power: 20.0
cleaning_system:
power: 760.0
recharge_threshold: 0.05
recharge_soc: 1.0
publish_fleet_state: True
account_for_battery_drain: True
task_capabilities: # Specify the types of RMF Tasks that robots in this fleet are capable of performing
loop: True
delivery: False
clean: True
finishing_request: "park"
action_categories: ["clean", "manual_control"]
def _consider(description: dict):
confirm = adpt.fleet_update_handle.Confirmation()
# Currently there's no way for user to submit a robot_task_request
# .json file via the rmf-web dashboard. Thus the short term solution
# is to add the fleet_name info into action description. NOTE
# only matching fleet_name action will get accepted
if (description["category"] == "manual_control" and
description["description"]["fleet_name"] != fleet_name):
return confirm
node.get_logger().warn(
f"Accepting action: {description} ")
confirm.accept()
return confirm
fleet_config = config_yaml['rmf_fleet']
task_capabilities_config = fleet_config['task_capabilities']
if 'action_categories' in task_capabilities_config:
for cat in task_capabilities_config['action_categories']:
node.get_logger().info(
f"Fleet [{fleet_name}] is configured"
f" to perform action of category [{cat}]")
fleet_handle.add_performable_action(cat, _consider)
- RobotUpdateHandle::set_action_executor 在这里,您可以告诉舰队适配器如何指示您的机器人开始执行操作。此函数的回调包括:
category(string)
类型的操作。description(JSON)
消息,其中包含有关如何执行操作的详细信息。execution(object)
对象,在执行操作时,队列适配器的平台特定端必须持有该对象,理想情况下会定期更新剩余时间estimates.
在“perform_action”中使用自定义任务时,机器人不会参与交通协商。这意味着它将被允许将其轨迹报告给交通调度,从而使其他机器人能够避开它。但是,在任务完成之前,机器人将无法容纳其他机器人。
这是一个例子:
# call this when starting cleaning execution
def _action_executor(self,
category: str,
description: dict,
execution:
adpt.robot_update_handle.ActionExecution):
with self._lock:
# only accept clean and manual_control
assert(category in ["clean", "manual_control"])
self.action_category = category
if (category == "clean"):
attempts = 0
self.api.set_cleaning_mode(self.config['active_cleaning_config'])
while True:
self.node.get_logger().info(f"Requesting robot {self.name} to clean {description}")
if self.api.start_clean(description["clean_task_name"], self.robot_map_name):
self.check_task_completion = self.api.task_completed # will check api
break
if (attempts > 3):
self.node.get_logger().warn(
f"Failed to initiate cleaning action for robot [{self.name}]")
# TODO: issue error ticket
self.in_error = True # TODO: toggle error back
execution.error("Failed to initiate cleaning action for robot {self.name}")
execution.finished()
return
attempts+=1
time.sleep(1.0)
elif (category == "manual_control"):
self.check_task_completion = lambda:False # user can only cancel the manual_control
# start Perform Action
self.node.get_logger().warn(f"Robot [{self.name}] starts [{category}] action")
self.start_action_time = self.adapter.now()
self.on_waypoint = None
self.on_lane = None
self.action_execution = execution
self.stubbornness = self.update_handle.unstable_be_stubborn()
# robot moves slower during perform action
self.vehicle_traits.linear.nominal_velocity *= 0.2
self.update_handle.set_action_executor(self._action_executor)
这些示例是以下内容的一部分 repository.
支持 RMF 中的新任务
随着 RMF Task V2, 用户现在可以根据自己的特定需求构建自定义任务。可以根据用户的偏好将不同的机器人任务组合或顺序分派给指定的机器人或最佳可用机器人队列。
新的灵活任务系统引入了阶段的概念。任务是生成阶段的对象。换句话说,任务通常由一系列或多个阶段组合作为其构建块组成。例如,送货任务需要机器人完成以下步骤:
- 从当前航点移动到上车地点
- 拾取货物
- 从上车地点移动到下车地点
- 放下货物
- 返回初始航点
每个步骤都可以视为一个阶段。用户可以使用以下公共 API 阶段来构建自己的任务:
定义并列出了其他阶段描述,包括支持公共 API 阶段的描述 here. 它们对于构建您自己的自定义任务很有用。
某些任务可能需要上面未提及的特定阶段。例如,如果送货任务涉及机器人从第一层移动到第二层,则需要“RequestLift”阶段。此类阶段由 RMF 内部使用,并在必要时自动添加到任务中,因此用户在创建自定义任务时无需担心它们。
构建自定义任务
用户可以通过发布 ApiRequest
消息来构建和发送自己的任务。您需要根据组成任务的阶段类型以及任务是针对特定机器人还是最佳可用队列来填写 request_id
和 json_msg
字段。您可以按照以下步骤构建自己的任务:
- 创建一个
ApiRequest
发布者,通过/task_api_requests
主题发送任务请求。 - 在
request_id
字段中填写一个唯一的字符串 ID,该 ID 可用于识别任务。 - 对于
json_msg
字段,- Use the
robot_task_request
schema and fill in the JSON payload type with"robot_task_request"
to send a task request to a specific robot - Use the
dispatch_task_request
schema and fill in the JSON payload type with"dispatch_task_request"
to send a task request to the best available fleet - The
request
fields for these objects follow thetask_request
schema
- Use the
- 使用所需信息填充对象字段。
- The
category
anddescription
fields under thetask_request
schema take in the string name of the task and the task description respectively. The JSON schema for these descriptions can be found here. There are currently four task descriptions available:- Clean: create your own clean task, requires the
Clean
phase description - Compose: create your own custom task that may comprise of a sequence of phases, requires descriptions for the relevant phases
- Delivery: create your own delivery task, requires the
PickUp
andDropOff
phase descriptions - Patrol: create your own patrol task, requires the
Place
description to indicate where you would like your robot to go to
- Clean: create your own clean task, requires the
- The
- 发布
ApiRequest
!
Examples of JSON Task Requests
For a Clean dispatch_task_request
:
{
"type": "dispatch_task_request",
"request": {
"unix_millis_earliest_start_time": start_time,
"category": "clean",
"description": {
"zone": "clean_lobby"
}
}
}
对于 Compose robot_task_request
,它命令特定机器人前往某个地方,然后执行 teleop
操作:
{
"type": "robot_task_request",
"robot": "tinyRobot1",
"fleet": "tinyRobot",
"request": {
"category": "compose",
"description": {
"category": "teleop",
"phases": [
{"activity": {
"category": "sequence",
"description": {
"activities": [
{"category": "go_to_place",
"description": "coe"
},
{"category": "perform_action",
"description": {"category": "teleop", "description": "coe"}
}
]
}
}}
]
}
}
}
For a Delivery dispatch_task_request
:
{
"type": "dispatch_task_request",
"request": {
"category": "delivery",
"description": {
"pickup": {
"place": "pantry",
"handler": "coke_dispenser",
"payload": [
{"sku": "coke",
"quantity": 1}
]
},
"dropoff": {
"place": "hardware_2",
"handler": "coke_ingestor",
"payload": [
{"sku": "coke",
"quantity": 1}
]
}
}
}
}
For a Patrol robot_task_request
:
{
"type": "robot_task_request",
"robot": "tinyRobot1",
"fleet": "tinyRobot",
"request": {
"category": "patrol",
"description": {
"places": ["pantry", "lounge"],
"rounds": 2
}
}
}
一些组合任务请求的示例可以在这里中找到作为参考。它们可以与rmf_demos
一起使用。您可以根据自己的应用程序随意修改这些文件。
任务管理控制
您可以通过向 RMF 发送请求来取消任务或跳过阶段,从而对任务进行额外的控制。此类请求的完整 JSON 架构列表定义在 此处。
SOSS
本章介绍了系统综合器 (SOSS),这是一种提供不同子系统之间协议转换的工具。 此类复合系统可称为 ROS-SOSS。 要查看当前实施状态,请参阅 SOSS 存储库。
动机和介绍
不同消息传递系统的生态系统庞大而多样。 由于没有一个系统被一致认为是所有应用程序的最佳选择,我们只能考虑如何将不同的消息传递系统结合在一起,以弥合在现代智能机器人解决方案中发挥关键作用的不同类型应用程序之间的差距。 用于机器人间通信的最佳协议可能不是远程操作员通信或最终用户通信的最佳协议。
这会产生可扩展性问题。
如果在机器人部署中使用 N
个不同的消息传递框架,并且在它们之间传递 M
个不同的消息类型,那么手动在它们之间创建相互兼容的桥梁可能会成为一个 O(MN^2)
复杂度问题。
这促使我们使用高度模块化、用户友好的集成系统,使尽可能多的不同消息传递框架能够尽可能自动地实现互操作性。
O(MN^2)
问题可以简化为 O(N)
复杂度,其中为每个 N
框架编写一个插件,并且所有 M
消息类型都会在其 N
个不同表示之间自动转换。
我们为此使用的集成服务称为系统合成器 (SOSS)。
基本 SOSS 包只是 C++ 库中定义的一些抽象接口,以及单个 soss
应用程序。
每个不同的消息传递系统都有自己的插件库,例如 DDS-SOSS、Websocket-SOSS、ROS-SOSS,它们实现了基本 SOSS 的抽象接口。
运行 soss
应用程序时,您将提供一个配置文件,该文件描述了您希望不同的消息传递系统如何相互连接。
然后,soss
应用程序可以找到满足配置文件要求的插件,并在启动时加载这些插件。
当消息开始在每个消息传递系统内移动时,soss
应用程序将根据提供给它的配置文件抓取、转换和推送消息到不同的系统边界。
可以同时运行任意数量的“soss”实例,但它们将彼此独立运行,因此确保它们的配置不重叠非常重要。
基础 SOSS 包还提供了一些 CMake 工具,帮助为在编译时需要静态消息定义的消息传递系统自动生成消息定义。 对于具有动态消息类型的消息传递系统,插件可以在运行时自动处理翻译,因此不需要自动生成。
要深入了解如何使用 SOSS,我们建议阅读 the documentation provided by eProsima.
Integration
本章介绍了将硬件与 RMF 集成的要求和基本步骤。这些包括 移动机器人、门、电梯 和 工作单元。 在每个部分中,我们将介绍如何构建和使用必要的 ROS 2 包和接口,以及可能发生此类交互的场景。
RMF 使用 ROS 2 消息和主题接口在整个 RMF 系统中的不同组件之间进行通信。 在大多数情况下,我们使用称为适配器的组件来桥接硬件特定接口和 RMF 的通用接口。 本章将讨论如何为不同类型的硬件组件开发 RMF 适配器。
与 RMF 集成的路线图数据要求
动机
RMF 使用机器人路线图来预测环境中工作的机器人的导航路径。RMF 为环境中所有活动的机器人生成路径预测,可用于主动避免各种机器人路径计划之间的冲突。这在 RMF 中通常被称为“交通管理”。除了交通管理之外,RMF 还可以帮助实现建筑物/机器人操作人员的多车队可视化,改善资源(例如电梯和走廊)的调度,减少机器人死锁等等。
大型建筑物中的机器人路线图很复杂,可能会随着客户要求和建筑物翻新而随着时间的推移而变化。因此,当脚本可以自动导入机器人路线图并在将来进行更改后重新导入它们时,RMF 的效果最佳。
所需的最少地图信息
- list of waypoints or nodes
- name of waypoint
- level name (B1, L1, L2, etc.)
- (x, y) location in meters within the level
- any special properties or flags, such as:
- is this a dropoff/pickup parking point?
- is this a charger?
- is this a safe parking spot during an emergency alarm?
- list of edges or "travel lanes" between nodes
- (start, end) waypoint names
- two-way or one-way traffic?
- if one-way, identify direction of travel
- any other information, such as speed limit along this segment
格式要求
我们可以编写导入脚本来处理几乎任何包含所需信息的“开放”文件格式。这包括(按优先顺序):
- YAML
- XML
- plain text (space or comma-separated ASCII, etc.)
- DXF
- DWG
- SVG
请注意,如果地图数据以文本形式提供,则屏幕截图有助于“检查”坐标系和与建筑特征的对齐。
交通编辑器
如果机器人路线图尚不存在,则可以使用 交通编辑器工具 来帮助创建路线图。交通编辑器工具还将以 RMF 友好格式导出路线图。
移动机器人车队整合
在这里,我们将介绍如何集成移动机器人车队,该车队提供完全控制类别的车队适配器,如 RMF 核心概述 章节中讨论的那样。 这意味着我们假设移动机器人车队管理器允许我们为机器人指定要遵循的明确路径,并且可以随时中断该路径并用新路径替换。 此外,每个机器人的位置都将在机器人移动时实时更新。
路线图
在集成这样的车队之前,您需要采购或制作路线图,如 上一节 中所述。车队适配器使用路线图为其控制的车辆规划可行的路线,同时考虑所有其他车辆的时间表。当发生调度冲突时,它还将使用路线图来决定如何与其他车队适配器协商。适配器只会考虑沿着路线图上指定的路线移动机器人,因此路线覆盖全面非常重要。同时,如果路线图上有多余的航点,适配器可能会花费比实际需要更多的时间来考虑所有可能性,因此最好在全面性和精简性之间取得平衡。
C++ API
可以在 rmf_ros2
存储库的 rmf_fleet_adapter
包中找到用于 Full Control 自动导引车 (AGV) 车队的 C++ API。该 API 由四个关键类组成:
Adapter
- 初始化并维持与其他核心 RMF 系统的通信。使用它来注册一个或多个舰队并接收每个舰队的“FleetUpdateHandle”。FleetUpdateHandle
- 允许您通过添加机器人并指定机器人组的设置(例如,指定机器人组可以执行哪些类型的配送)来配置机器人组。您可以随时将新机器人添加到机器人组。RobotUpdateHandle
- 使用它来更新机器人的位置,并在机器人的进度中断时通知适配器。RobotCommandHandle
- 这是一个纯抽象接口类。必须实现该类的功能才能调用正在适配的特定车队管理器的 API。
Easy Full Control 机群的 C++ API 为用户提供了一种简单且更易于访问的方式,可与 Full Control 库集成,而无需修改其内部逻辑。可以在 rmf_ros2
仓库的 rmf_fleet_adapter 包中找到它。EasyFullControl
类包含有用的方法,用户可从封装重要机群配置参数和导航图的 YAML 文件创建 Configuration
对象,以及使用 Configuration
对象制作自己的机群适配器。为用户提供了 add_robot(~)
方法,用于将机器人添加到新的机群适配器。此方法接受用户应编写的各种回调,并且每当 RMF 从舰队检索机器人状态信息或发出命令执行特定过程(导航、对接、操作等)时都会触发。EasyFullControl 舰队适配器的示例可以在 rmf_demos
repo 下的 fleet_adapter.py
中找到。
还可以在 rmf_ros2
repo 的 rmf_fleet_adapter
包中找到用于 交通灯控制 车队(即仅允许 RMF 暂停/恢复每个移动机器人的车队)的 C++ API。该 API 重用了 Adapter
类,并要求用户使用 此处 中的任一 API 来初始化其车队。用户可以选择通过 TrafficLight
API 进行集成,或者为了更方便,也可以通过 EasyTrafficLight
API 进行集成。
开发舰队适配器的基本工作流程如下:
- 创建一个链接到
rmf_fleet_adapter
库的应用程序。 - 让应用程序以所需的任何方式读取运行时参数(例如命令行参数、配置文件、ROS 参数、REST API 调用、环境变量等)。
- 为该应用程序为其提供适配器的每个舰队构建路线图(单个适配器应用程序可以为任意数量的舰队提供服务),和/或使用
rmf_fleet_adapter::agv::parse_graph
实用程序从 YAML 文件解析路线图。 - 使用
Adapter::make(~)
或Adapter::init_and_make(~)
实例化rmf_fleet_adapter::agv::Adapter
。 - 添加应用程序将负责适配的车队,并保存传回的
rmf_fleet_adapter::agv::FleetUpdateHandlePtr
实例。 - 为正在适配的车队管理器 API 实现
RobotCommandHandle
类。 - 添加适配器负责控制的机器人。可以根据启动配置添加机器人,也可以在运行时通过车队管理器 API 发现机器人时动态添加机器人(或两者兼而有之)。
- 添加机器人时,您需要创建您实现的自定义
RobotCommandHandle
的新实例。 - 您还需要提供一个回调,当适配器完成注册机器人时将触发该回调。此回调将为您的机器人提供一个新的
RobotUpdateHandle
。必须保存此更新句柄,以便您可以使用它来随时间更新机器人的位置。
- 当来自车队管理器 API 的新信息到达时,使用“RobotUpdateHandle”类的集合来使适配器保持机器人位置的更新。
可以在 full_control
向后兼容适配器 中找到一个可运行的舰队适配器应用程序示例。这是一个舰队适配器,其舰队端 API 是“舰队驱动程序 API”,这是 RMF Full Control 类别舰队适配器的弃用原型 API。此舰队适配器暂时存在,以保持与旧“舰队驱动程序”实现的向后兼容性,并作为如何使用新 C++ API 实现舰队适配器的示例。
Python Bindings
您也可以选择使用 Python 来实现您的舰队适配器。您可以在 rmf_fleet_adapter_python repo 中找到 C++ API 的 Python 绑定。Python 绑定实际上只是将 C++ API 移植到 Python,以便您可以使用 Python 而不是 C++ 开发舰队适配器。上述 API 和工作流程完全相同,只是改用 Python。这对于使用 REST API 的舰队来说应该非常有用,因为您将可以访问 Swagger 等工具,这些工具可以帮助您为舰队的 REST API 服务器生成客户端代码。
Fleet Adapter Template
为了使机器人队列与 RMF 的集成过程更加简单,我们开源了一个 完全控制 模板包,用户只需使用 API 调用更新特定代码块即可。这样,用户可以在队列适配器和机器人之间使用他们喜欢的 API 来集成 RMF。请注意,此模板只是将队列与基于 REST 或 websocket 的 API 集成的众多方法之一。下图说明了 RMF 如何使用用户和机器人供应商选择的 API 与队列机器人进行通信。

该舰队适配器系统也与我们的模拟机器人演示世界集成在一起,下一节将进一步阐述。
由于车队适配器模板已经应用了 C++ API 和 Python 绑定,您可以按照以下步骤在给定模板之上构建车队适配器:
- 修改
RobotCommandHandle.py
。如上所述,将为车队中的每个机器人创建一个新的RobotCommandHandle
实例。您应该查看代码并实现特定于应用程序的导航、到达估计和对接逻辑。 - 创建一个与您的车队机器人交互的车队管理器。您的车队管理器应该能够通过您的机器人供应商的 API 从您的机器人检索状态信息并向您的机器人发送导航命令。这可以是 ROS 消息或任何自定义 API。如果您有多个使用不同机器人 API 的车队,请确保为这些车队创建单独的车队管理器。您还应该选择一个 API 来与适配器通信,并相应地设计您的车队管理器。
- 在
RobotClientAPI.py
中填写缺失的代码。这是集成中最重要的部分。根据您选择在适配器和管理器之间交互的 API,您必须相应地格式化机器人的数据并返回模板中指定的值。这很关键,因为当适配器正在计划任务或更新交通时间表时,RobotClientAPI
中的函数会从RobotCommandHandle
调用。 - 对于每个机器人车队,创建一个
config.yaml
文件以包含重要的车队参数。这些参数将传递给舰队适配器并在初始化舰队时进行配置。
完成后,您可以运行舰队适配器和自定义舰队管理器。请记住在启动适配器时解析配置文件和导航图。
案例研究:RMF Demos Fleet Adapter
demos fleet adapter 演示了 Full Control fleet 适配器类的 Python 实现。基于 fleet 适配器模板,demos fleet 适配器使用 REST API 作为适配器和模拟机器人之间的接口:适配器向机器人发送命令,而机器人根据其当前状态信息更新适配器。这是通过创建一个 fleet_manager
节点来实现的,该节点包含 RobotClientAPI
交互所需的 REST 端点。

Demos Fleet Manager
每当有命令准备从适配器的“RobotCommandHandle”发送时,它就会调用“RobotClientAPI”中定义的相关 API 函数,并从“fleet_manager”节点中的 API 服务器查询相应的端点。每个函数要么检索有关机器人当前状态的特定信息(包括但不限于其最后已知位置、剩余电池电量以及是否已完成请求),要么向机器人发送命令以执行请求。机器人状态信息是车队适配器更新交通时间表、规划后续任务以及引导机器人穿越环境中的不同路径所必需的。
demos 车队适配器与模拟机器人集成,后者通过内部 ROS2 消息发布其状态信息,因此“fleet_manager”还用于整合其车队中不同机器人发布的消息,并将它们发送到正确机器人的“RobotCommandHandle”。 API 端点的设计使得适配器可以通过指定机器人的名称来查询信息或向特定机器人发送命令。fleet_manager
将确保机器人存在于机器人队列中,然后再返回请求的状态信息或将命令传递给模拟机器人。此外,适配器可以检索整个机器人队列的状态。
车队配置
我们的演示模拟中有四个完全控制车队,每个车队都有自己的车队特定参数。为了在初始化车队时更好地整合和设置这些配置,它们存储在 config.yaml
文件中。运行车队适配器和管理器时需要配置和导航图文件的路径。常规车队设置和功能在 rmf_fleet
部分下定义。
配置文件还负责处理车队内机器人特定的参数,例如车队中的机器人数量、每个机器人的名称及其起始航点。例如,Office 演示世界中的 tinyRobot
车队有两个机器人,因此我们将每个机器人的配置附加到配置文件中的 robots
部分。
对于在与 RMF 不同的坐标系中操作机器人的用户,配置文件中的“reference_coordinate”部分有助于执行任何必要的转换。请注意,RMF 和 slotcar 模拟机器人共享相同的坐标系,因此此转换未在演示车队适配器中实现。
Fleet Adapter Tutorial (Python)
fleet_adapter
充当机器人和核心 RMF 系统之间的桥梁。
其职责包括但不限于:
-
使用车队机器人的位置更新交通时间表
-
响应任务
-
控制供应商机器人。
fleet_adapter
接收有关队列中每个机器人的信息(位置、当前正在进行的任务、电池电量等),并将其发送到核心 RMF 系统进行任务规划和调度。
-
当核心 RMF 系统有任务要分派时,它会与各个队列适配器通信,以检查哪个队列适合执行此任务。
-
它发送请求,队列适配器通过提交其队列机器人的可用性和状态来响应。
-
RMF 确定最适合该任务的队列并响应中标,即选定的队列。响应包含与委派任务相关的导航命令。
-
然后,队列适配器将通过适当的 API 将导航命令发送给机器人。
下面提供的教程基于 rmf_demos,该实现在 rmf_demos 存储库中实现。此特定实现使用 Python 编写,并使用 REST API 作为舰队适配器和舰队管理器之间的接口。您可以选择使用其他 API 进行自己的集成。
1. Pre-requisites
获取依赖项
在运行您的车队适配器之前,请确保您已按照此处 的说明安装了 ROS 2 和 RMF。您可以选择安装二进制文件或从源代码构建两者。您可能还希望前往我们的 RMF Github repo 以获取 RMF 安装的最新更新和说明。
如果您从源代码构建了 ROS 2 和/或 RMF,请确保在继续下一步之前获取包含其构建代码的工作区。
在我们的示例中,rmf_demos_fleet_adapter
使用 REST API 作为车队适配器和机器人车队管理器之间的接口,因此要使演示正常运行,我们需要安装使用 FastAPI 所需的依赖项。
pip3 install fastapi uvicorn
此步骤仅适用于此实现;根据您自己的车队管理器使用的 API,您必须相应地安装任何必要的依赖项。
开始使用车队适配器模板
创建一个工作区并克隆 fleet_adapter_template 存储库。
mkdir -p ~/rmf_ws/src
cd ~/rmf_ws/src/
git clone https://github.com/open-rmf/fleet_adapter_template.git
此模板包含完全控制和轻松完全控制队列适配器的代码。两种实现均使用 RobotClientAPI.py
中的 API 调用与机器人进行通信。
2. Update the config.yaml
file
config.yaml
文件包含设置车队适配器的重要参数。用户应首先更新这些描述其车队机器人的配置。
务必遵循下面示例 config.yaml
中提供的字段,否则在将此 YAML 文件解析到车队适配器时会出现导入错误。如果您想编辑任何字段名称或值范围,甚至附加其他字段,请确保您还修改了车队适配器代码中处理此配置导入的部分。
某些字段是可选的,如下所示。
# FLEET CONFIG =================================================================
# RMF Fleet parameters
rmf_fleet:
name: "tinyRobot"
limits:
linear: [0.5, 0.75] # velocity, acceleration
angular: [0.6, 2.0] # velocity, acceleration
profile: # Robot profile is modelled as a circle
footprint: 0.3 # radius in m
vicinity: 0.5 # radius in m
reversible: True # whether robots in this fleet can reverse
battery_system:
voltage: 12.0 # V
capacity: 24.0 # Ahr
charging_current: 5.0 # A
mechanical_system:
mass: 20.0 # kg
moment_of_inertia: 10.0 #kgm^2
friction_coefficient: 0.22
ambient_system:
power: 20.0 # W
tool_system:
power: 0.0 # W
recharge_threshold: 0.10 # Battery level below which robots in this fleet will not operate
recharge_soc: 1.0 # Battery level to which robots in this fleet should be charged up to during recharging tasks
publish_fleet_state: 10.0 # Publish frequency for fleet state, ensure that it is same as robot_state_update_frequency
account_for_battery_drain: True
task_capabilities: # Specify the types of RMF Tasks that robots in this fleet are capable of performing
loop: True
delivery: True
actions: ["teleop"]
finishing_request: "park" # [park, charge, nothing]
responsive_wait: True # Should responsive wait be on/off for the whole fleet by default? False if not specified.
robots:
tinyRobot1:
charger: "tinyRobot1_charger"
responsive_wait: False # Should responsive wait be on/off for this specific robot? Overrides the fleet-wide setting.
tinyRobot2:
charger: "tinyRobot2_charger"
# No mention of responsive_wait means the fleet-wide setting will be used
robot_state_update_frequency: 10.0 # Hz
fleet_manager:
prefix: "http://127.0.0.1:8080"
user: "some_user"
password: "some_password"
# TRANSFORM CONFIG =============================================================
# For computing transforms between Robot and RMF coordinate systems
# Optional
reference_coordinates:
L1:
rmf: [[20.33, -3.156],
[8.908, -2.57],
[13.02, -3.601],
[21.93, -4.124]]
robot: [[59, 399],
[57, 172],
[68, 251],
[75, 429]]
-
rmf_fleet
: 重要的车队参数,包括车辆特性、任务能力和用于连接车队经理的用户信息。-
limits
: 线性和角加速度和速度的最大值。 -
profile
: 该车队车辆的覆盖范围半径和个人附近区域。 -
reversible
: 用于启用/禁用机器人反向遍历的标志。 -
battery_system
: 有关电池电压、容量和充电电流的信息。 -
recharge_threshold
: 设置最低充电值,低于该值时机器人必须返回其充电器。 -
recharge_soc
: 机器人需要充电至的总电池容量的分数。 -
task_capabilities
: 机器人在‘循环’、‘运送’和‘清洁’之间可以执行的任务。 -
account_for_battery_drain
: RMF 在调度任务之前是否应该考虑机器人的电池消耗。 -
action
[Optional]: 舰队可执行的自定义操作列表。 -
finishing_request
: 机器人完成任务后应该做什么,可以设置为“停止”,“充电”或“不做任何事”。 -
responsive_wait
[Optional]: True # 默认情况下,整个舰队的响应等待是否应打开/关闭?如果未指定,则为 false。 -
robots
: 有关队列中每个机器人的信息。本节中的每个项目对应于队列中单个机器人的配置。您可以相应地添加更多机器人。-
tinyRobot1
: 机器人的名称。-
charger
: 机器人充电点的名称。 -
responsive_wait
: 该特定机器人是否应打开/关闭其响应等待。覆盖全队设置。
-
-
-
robot_state_update_frequency
: 机器人应该多久更新一次车队。
-
-
fleet_manager
:可配置 prefix、user 和 password 字段以适合您所选的 API。如果您确实要修改它们,请确保也编辑RobotClientAPI.py
中的相应字段。这些参数将用于设置与您的车队管理器/机器人的连接。 -
reference_coordinates
[可选]:如果车队机器人未在与 RMF 相同的坐标系中运行,您可以提供两组 (x, y) 坐标,它们对应于每个系统中的相同位置。这有助于估计从一个帧到另一个帧的坐标转换。建议至少有 4 个匹配的航点。
注意:这未在 rmf_demos_fleet_adapter
中实现,因为演示机器人和 RMF 使用相同的坐标系。
3. 创建导航图
需要将导航图解析到车队适配器,以便 RMF 能够理解机器人的环境。可以使用提供的 RMF 交通编辑器 和 building_map_generator nav
CLI 创建导航图。请参阅交通编辑器 repo 的 README 以获取安装和地图生成说明。
您可能还想查看本书的 交通编辑器 部分,了解有关创建自己的数字地图的详细信息和说明。
现在您应该有一个 YAML 文件,其中包含有关车道和航点的信息(以及其他信息),这些信息描述了您的机器人车队可以采取的路径。
4. 填写您的 RobotAPI
RobotClientAPI.py
提供了一组由舰队适配器使用的方法。当 RMF 需要通过舰队适配器向托管机器人发送或检索信息时,会触发这些回调。为了满足您选择的界面,您需要在 RobotAPI
中填写标有 # IMPLEMENT YOUR CODE HERE #
的缺失代码块,并添加发送或检索相应信息的逻辑。例如,如果您的机器人使用 REST API 与舰队适配器交互,您将需要在这些函数中对适当的端点进行 HTTP 请求调用。
您可以参考为 rmf_demos_fleet_adapter
实现的 RobotAPI
类,了解如何填充这些方法的示例。
navigate
:向机器人 API 发送导航命令。它从 RMF 中获取目的地坐标、所需地图名称和可选速度限制。start_activity
:向机器人发送命令以开始执行任务。此方法对于由execute_action()
触发的自定义可执行操作很有用。stop
:命令机器人停止移动。position
、map
和battery_soc
:以[x, y, theta]
格式检索机器人在其坐标系中的当前位置、其当前地图名称和电池充电状态。在rmf_demos_fleet_adapter
中,这些方法合并在get_data()
下。is_command_completed
:检查机器人是否已完成正在进行的过程或任务。在rmf_demos_fleet_adapter
中,这在RobotUpdateData
类下实现。根据您的机器人 API,您可以选择以任何一种方式集成它。此回调将帮助 RMF 识别已调度命令的完成时间,并继续发送后续命令。
如果需要,可以向 RobotAPI
添加更多参数以用于这些回调,例如身份验证详细信息和任务 ID。您可能还希望在 RobotAPI
和 fleet_adapter.py
中为特定用例编写其他方法。 rmf_demos_fleet_adapter
实现演示了 Teleoperation
操作,这将在 PerformAction 教程 中进行更详细的阐述。
5. 创建您的车队适配器!
现在我们已经准备好组件,我们可以开始创建我们的车队适配器了。fleet_adapter.py
使用 Easy Full Control API 轻松创建 Adapter
实例,并通过解析我们之前准备的配置 YAML 文件来设置车队配置和机器人。由于我们已经定义了 RobotAPI
,因此 fleet_adapter.py
中的回调将使用所实现的方法,以便 RMF 可以检索机器人信息并适当地发送导航或操作命令。
您可能希望使用车队适配器模板中提供的 fleet_adapter.py
,并根据您希望车队实现的目标对其进行修改。
6. 运行您的舰队适配器
此时,您应该已准备好 4 个组件以运行您的舰队适配器:
fleet_adapter.py
RobotClientAPI.py
- 舰队
config.yaml
文件 - 导航图
构建你的舰队适配器包
如果您克隆了 fleet_adapter_template
存储库,那么您的 Python 脚本就已经包含在 ROS 2 包中。否则,您可以按照 此处 中的说明在您的工作区中创建一个包。对于以下说明,我们将使用 fleet_adapter_template
包中使用的包和模块名称。
将脚本放在相应的文件夹中后,返回工作区的根目录并构建包。
colcon build --packages-select fleet_adapter_template
运行!
我们现在将获取工作区并运行 fleet 适配器:
. ~/rmf_ws/install/setup.bash
ros2 run fleet_adapter_template fleet_adapter -c <path-to-config> -n <path-to-nav-graph>
7. 深入研究代码 [可选]
以下步骤详细说明了 Easy Full Control 车队适配器以及代码的每个部分的作用。
a. 导入重要参数并创建适配器
运行车队适配器时,我们需要解析我们在前面的步骤中创建的车队配置文件和导航图。这些文件将传递给 EasyFullControl API 以设置适配器的车队配置。
config_path = args.config_file
nav_graph_path = args.nav_graph
fleet_config = rmf_easy.FleetConfiguration.from_config_files(
config_path, nav_graph_path
)
assert fleet_config, f'Failed to parse config file [{config_path}]'
# Parse the yaml in Python to get the fleet_manager info
with open(config_path, "r") as f:
config_yaml = yaml.safe_load(f)
使用这些参数,我们可以创建一个适配器实例并向其添加一个 EasyFullControl 队列。如果适配器应根据模拟时钟运行或向任何 websocket 服务器广播任务更新,我们还需要配置 use_sim_time
和 server_uri
参数。
# ROS 2 node for the command handle
fleet_name = fleet_config.fleet_name
node = rclpy.node.Node(f'{fleet_name}_command_handle')
adapter = Adapter.make(f'{fleet_name}_fleet_adapter')
assert adapter, (
'Unable to initialize fleet adapter. '
'Please ensure RMF Schedule Node is running'
)
# Enable sim time for testing offline
if args.use_sim_time:
param = Parameter("use_sim_time", Parameter.Type.BOOL, True)
node.set_parameters([param])
adapter.node.use_sim_time()
adapter.start()
time.sleep(1.0)
if args.server_uri == '':
server_uri = None
else:
server_uri = args.server_uri
fleet_config.server_uri = server_uri
fleet_handle = adapter.add_easy_fleet(fleet_config)
b. 配置 RMF 和机器人之间的转换
我们定义了一个辅助函数来计算 RMF 和机器人坐标之间的转换。如果您的机器人在与 RMF 相同的坐标下运行(例如在模拟中),则您不需要这部分代码。
def compute_transforms(level, coords, node=None):
"""Get transforms between RMF and robot coordinates."""
rmf_coords = coords['rmf']
robot_coords = coords['robot']
tf = nudged.estimate(rmf_coords, robot_coords)
if node:
mse = nudged.estimate_error(tf, rmf_coords, robot_coords)
node.get_logger().info(
f"Transformation error estimate for {level}: {mse}"
)
return Transformation(
tf.get_rotation(),
tf.get_scale(),
tf.get_translation()
)
# Configure the transforms between robot and RMF frames
for level, coords in config_yaml['reference_coordinates'].items():
tf = compute_transforms(level, coords, node)
fleet_config.add_robot_coordinates_transformation(level, tf)
根据集成所需的地图数量(或级别),您将提取每个地图的相应坐标变换并将其添加到 FleetConfiguration 对象。如果您将 rclpy.Node
传递给此函数,则此函数将记录变换误差估计。
然后,在我们的 main
函数中,我们将计算出的变换添加到我们的 FleetConfiguration。EasyFullControl 车队适配器将处理这些变换并相应地在机器人的坐标中发出导航命令。
# Configure the transforms between robot and RMF frames
for level, coords in config_yaml['reference_coordinates'].items():
tf = compute_transforms(level, coords, node)
fleet_config.add_robot_coordinates_transformation(level, tf)
c. 初始化机器人 API 并设置 RobotAdapter
config.yaml
可能包含我们连接到机器人或机器人车队管理器所需的任何连接凭据。我们将其解析为 RobotAPI
,以便轻松在 RMF 和机器人的 API 之间进行交互。这完全是可选的;为了更安全地存储凭据,请相应地将它们导入 RobotAPI。
# Initialize robot API for this fleet
fleet_mgr_yaml = config_yaml['fleet_manager']
api = RobotAPI(fleet_mgr_yaml)
根据我们“config.yaml”中的已知机器人列表,我们可以为每个要添加到机器人队伍中的机器人初始化一个“RobotAdapter”类。
robots = {}
for robot_name in fleet_config.known_robots:
robot_config = fleet_config.get_known_robot_configuration(robot_name)
robots[robot_name] = RobotAdapter(
robot_name, robot_config, node, api, fleet_handle
)
d. 检索机器人状态并将机器人添加到车队
此更新循环将允许我们使用机器人的信息异步更新“RobotUpdateHandle”,这样从一个机器人检索状态时发生的任何错误都不会阻止其他机器人更新车队适配器。
update_period = 1.0/config_yaml['rmf_fleet'].get(
'robot_state_update_frequency', 10.0
)
def update_loop():
asyncio.set_event_loop(asyncio.new_event_loop())
while rclpy.ok():
now = node.get_clock().now()
# Update all the robots in parallel using a thread pool
update_jobs = []
for robot in robots.keys():
update_jobs.append(update_robot(robot))
asyncio.get_event_loop().run_until_complete(
asyncio.wait(update_jobs)
)
next_wakeup = now + Duration(nanoseconds=update_period*1e9)
while node.get_clock().now() < next_wakeup:
time.sleep(0.001)
update_thread = threading.Thread(target=update_loop, args=())
update_thread.start()
调用函数“update_robot()”以确保我们的机器人的当前地图、位置和电池充电状态将得到正确更新。如果机器人是新加入到车队句柄中的,我们将通过“add_robot()”将其添加进去。
@parallel
def update_robot(robot: RobotAdapter):
data = robot.api.get_data(robot.name)
if data is None:
return
state = rmf_easy.RobotState(
data.map,
data.position,
data.battery_soc
)
if robot.update_handle is None:
robot.update_handle = robot.fleet_handle.add_robot(
robot.name,
state,
robot.configuration,
robot.make_callbacks()
)
return
robot.update(state)
e. 在 RobotAdapter
类中
RobotAdapter
类帮助我们跟踪机器人可能正在执行的任何正在进行的过程,并在 RMFs 发送相应命令时执行正确的操作。
class RobotAdapter:
def __init__(
self,
name: str,
configuration,
node,
api: RobotAPI,
fleet_handle
):
self.name = name
self.execution = None
self.update_handle = None
self.configuration = configuration
self.node = node
self.api = api
self.fleet_handle = fleet_handle
我们需要将 3 个重要的回调传递给 EasyFullControl API:
navigate
stop
execute_action
如上所述,当 RMF 需要命令机器人执行某些操作时,每个回调都会被触发。因此,我们在“RobotAdapter”中定义了这些回调:
def navigate(self, destination, execution):
self.execution = execution
self.node.get_logger().info(
f'Commanding [{self.name}] to navigate to {destination.position} '
f'on map [{destination.map}]'
)
self.api.navigate(
self.name,
destination.position,
destination.map,
destination.speed_limit
)
def stop(self, activity):
if self.execution is not None:
if self.execution.identifier.is_same(activity):
self.execution = None
self.stop(self.name)
def execute_action(self, category: str, description: dict, execution):
''' Trigger a custom action you would like your robot to perform.
You may wish to use RobotAPI.start_activity to trigger different
types of actions to your robot.'''
self.execution = execution
# ------------------------ #
# IMPLEMENT YOUR CODE HERE #
# ------------------------ #
return
请注意,execute_action(~)
在舰队适配器模板中没有任何实现的代码。此回调旨在灵活,并满足 RMF 提供的任务下可能无法使用的自定义可执行操作。您可以在 PerformAction 教程 部分中了解如何设计和编写自己的操作并从舰队适配器执行它们。
def make_callbacks(self):
return rmf_easy.RobotCallbacks(
lambda destination, execution: self.navigate(
destination, execution
),
lambda activity: self.stop(activity),
lambda category, description, execution: self.execute_action(
category, description, execution
)
)
最后,我们使用RobotCallbacks()
API 将所有回调添加到我们的队列适配器。
PerformAction 教程(Python)
本教程是 Fleet Adapter 教程的扩展,将指导您在 Fleet Adapter 中编写自定义操作。虽然 RMF 提供了一些标准任务,但我们知道不同的机器人可能配备并编程为执行不同类型的操作,例如清洁、拾取物体、远程操作等。通过支持自定义任务,用户可以预先触发 Fleet Adapter 的 config.yaml
中指定的自定义操作,并且 RMF 将放弃对机器人的控制,直到收到机器人已完成自定义操作的信号。您可以探索 在 RMF 中支持新任务 部分,以了解有关支持自定义任务的更多信息以及如何创建自己的任务 JSON 以发送到 RMF。
在本教程中,我们将参考 rmf_demos_fleet_adapter
的简化版本,以在我们的 Fleet Adapter 中实现 Clean
PerformAction 功能。
1. 在舰队config.yaml
中定义 PerformAction
我们需要在舰队配置中定义操作的名称,以便 RMF 在提交任务时将此操作识别为可执行,并能够将其分派给可以完成该任务的舰队。在 rmf_fleet
部分下的 config.yaml
中,我们可以为舰队提供可执行操作的列表。例如,让我们将 clean
定义为此舰队支持的操作:
rmf_fleet:
actions: ["clean"]
2. 在我们的舰队适配器内应用操作执行逻辑
RMF 收到包含此操作的任务并将其分派到正确的舰队后,舰队适配器的 execute_action(~)
回调将被触发。解析到此回调的 category
对应于我们之前定义的操作名称,而 description
包含我们可能感兴趣的有关该操作的任何详细信息。
假设这是提交给 RMF 的任务 JSON:
{
"type": "dispatch_task_request",
"request": {
"unix_millis_earliest_start_time": start_time,
"category": "clean",
"description": {
"zone": "clean_lobby"
}
}
}
在我们的示例中,提供的“category”将是“clean”,而“description”将包含此任务将我们的机器人引导到哪个清洁区域,即“clean_lobby”。因此,我们需要在“execute_action(~)”中实现逻辑:
def execute_action(self, category: str, description: dict, execution):
self.execution = execution
if category == 'clean':
self.perform_clean(description['zone'])
def perform_clean(self, zone):
if self.api.start_activity(self.name, 'clean', zone):
self.node.get_logger().info(
f'Commanding [{self.name}] to clean zone [{zone}]'
)
else:
self.node.get_logger().error(
f'Fleet manager for [{self.name}] does not know how to '
f'clean zone [{zone}]. We will terminate the activity.'
)
self.execution.finished()
self.execution = None
由于我们的机器人队列可能能够执行多个自定义操作,因此我们需要进行检查以确保收到的“类别”与我们的目标机器人 API 相匹配。收到“清洁”操作后,我们可以相应地触发机器人的 API。
3. 为自定义操作实现机器人 API
这就是“RobotClientAPI.py”中的“start_activity(~)”方法发挥作用的地方。我们需要它实现对机器人的 API 调用以启动清洁活动。例如,如果机器人 API 使用 REST 调用机器人,则实现的方法可能如下所示:
def start_activity(
self,
robot_name: str,
activity: str,
label: str
):
''' Request the robot to begin a process. This is specific to the robot
and the use case. For example, load/unload a cart for Deliverybot
or begin cleaning a zone for a cleaning robot.'''
url = (
self.prefix +
f"/open-rmf/rmf_demos_fm/start_activity?robot_name={robot_name}"
)
# data fields: task, map_name, destination{}, data{}
data = {'activity': activity, 'label': label}
try:
response = requests.post(url, timeout=self.timeout, json=data)
response.raise_for_status()
if self.debug:
print(f'Response: {response.json()}')
if response.json()['success']:
return True
# If we get a response with success=False, then
return False
except HTTPError as http_err:
print(f'HTTP error for {robot_name} in start_activity: {http_err}')
except Exception as err:
print(f'Other error {robot_name} in start_activity: {err}')
return False
4. 完成操作
由于我们在“RobotAdapter”中存储了一个“self.execution”对象,因此当任何执行(导航、停止或操作)完成时,我们都会收到通知,因为更新循环会不断调用“is_command_completed”来检查其状态。
def update(self, state):
activity_identifier = None
if self.execution:
if self.api.is_command_completed():
self.execution.finished()
self.execution = None
else:
activity_identifier = self.execution.identifier
如果您的实现需要单独的回调来标记执行已完成,您可以创建一个新函数来进行此检查,并在操作完成时调用“self.execution.finished()”。
Free Fleet
如果用户希望集成不带车队管理系统的独立移动机器人,则可以使用开源车队管理系统“free_fleet”。
“free_fleet”系统分为客户端和服务器。客户端将与导航软件一起在每个独立移动机器人上运行,旨在直接控制移动机器人,同时监控其状态并向服务器报告。客户端的基本实现旨在允许与不同配置的移动机器人进行交互,但向同一服务器报告。这样,用户就可以使用“free_fleet”来管理异构机器人车队,每个机器人车队使用不同的 ROS 发行版、ROS 版本、导航软件或板载通信协议。
服务器在中央计算机上运行,整合来自每个客户端的传入状态更新,以便使用 UI 进行可视化,或向上游转发到 RMF。服务器还通过 UI 将来自用户的命令或从 RMF 转发到要执行的客户端。每个服务器可以同时与多个客户端合作,因此它充当车队管理系统的角色。该服务器可以实现并用作自己的车队管理系统,也可以与 RMF 等更大的系统一起工作,从而弥合每个移动机器人的 API 与 RMF 的 API 和接口之间的差距。
free_fleet
服务器和 free_fleet
客户端之间的通信是使用 CycloneDDS
实现的,因此我们不必担心移动机器人或中央计算机是否运行不同版本的 ROS。
在本节中,我们将介绍使用 free_fleet
与 RMF 集成的 4 种不同方法,特别是机器人使用的导航堆栈。每种方法都保持类似的系统架构,如下面的简单框图所示,但根据机器人开发人员使用的导航堆栈的软件选择,有具体的示例。

ROS 1 Navigation Stack
可以在 free_fleet 存储库 中找到与 ROS 1 导航堆栈配合使用的 free_fleet
客户端实现。该实现需要完全定义移动机器人的变换,移动机器人通过 move_base
操作库接受导航命令,并使用 sensor_msgs/BatteryState
消息发布其电池状态。
按照移动机器人 README 上的构建说明操作后,用户可以将客户端作为启动脚本的一部分启动,同时使用 rosparam
定义必要的参数。下面是一个小片段示例,说明如何启动客户端及其参数,
<node name="free_fleet_client_node"
pkg="free_fleet_client_ros1"
type="free_fleet_client_ros1" output="screen">
<!-- These parameters will be used to identify the mobile robots -->
<param name="fleet_name" type="string" value="example_fleet"/>
<param name="robot_name" type="string" value="example_bot"/>
<param name="robot_model" type="string" value="Turtlebot3"/>
<!-- These are the topics required to get battery and level information -->
<param name="battery_state_topic" type="string" value="example_bot/battery_state"/>
<param name="level_name_topic" type="string" value="example_bot/level_name"/>
<!-- These frames will be used to update the mobile robot's location -->
<param name="map_frame" type="string" value="example_bot/map"/>
<param name="robot_frame" type="string" value="example_bot/base_footprint"/>
<!-- The name of the move_base server for actions -->
<param name="move_base_server_name" type="string" value="example_bot/move_base"/>
<!-- These are DDS configurations used between Free Fleet clients and servers -->
<param name="dds_domain" type="int" value="42"/>
<param name="dds_state_topic" type="string" value="robot_state"/>
<param name="dds_mode_request_topic" type="string" value="mode_request"/>
<param name="dds_path_request_topic" type="string" value="path_request"/>
<param name="dds_destination_request_topic" type="string" value="destination_request"/>
<!-- This decides how long the client should wait for a valid transform and action server before failing -->
<param name="wait_timeout" type="double" value="10"/>
<!-- These define the frequency at which the client checks for commands and
publishes the robot state to the server -->
<param name="update_frequency" type="double" value="10.0"/>
<param name="publish_frequency" type="double" value="1.0"/>
<!-- The client will only pass on navigation commands if the destination or first waypoint
of the path is within this distance away, otherwise it will ignore the command -->
<param name="max_dist_to_first_waypoint" type="double" value="10.0"/>
</node>
正在运行的 free_fleet
客户端将通过 ROS 1 与机器人上运行的节点进行通信,同时使用 free_fleet
服务器发布其状态并通过 DDS 订阅请求。
free_fleet
服务器的当前实现是用 ROS 2 实现的,并使用 RMF fleet 适配器的上述 ROS 2 消息和主题接口与 RMF 进行通信。ROS 2 构建说明也可以在同一个存储库中找到。与客户端类似,已经实现了一个简单的 ROS 2 包装器,可以使用 .launch.xml
文件启动它,如下所示:
<node pkg="free_fleet_server_ros2"
exec="free_fleet_server_ros2"
name="free_fleet_server_node"
node-name="free_fleet_server_node"
output="both">
<!-- Fleet name will be used to identify robots -->
<param name="fleet_name" value="example_fleet"/>
<!-- These are the ROS2 topic names that will be used to communicate with RMF -->
<param name="fleet_state_topic" value="fleet_states"/>
<param name="mode_request_topic" value="robot_mode_requests"/>
<param name="path_request_topic" value="robot_path_requests"/>
<param name="destination_request_topic" value="robot_destination_requests"/>
<!-- These are the DDS specific configurations used to communicate with the clients -->
<param name="dds_domain" value="42"/>
<param name="dds_robot_state_topic" value="robot_state"/>
<param name="dds_mode_request_topic" value="mode_request"/>
<param name="dds_path_request_topic" value="path_request"/>
<param name="dds_destination_request_topic" value="destination_request"/>
<!-- This determines the frequency it checks for incoming state and request messages,
as well as how often it publishes its fleet state to RMF -->
<param name="update_state_frequency" value="20.0"/>
<param name="publish_state_frequency" value="2.0"/>
<!-- These transformations are required when the frame of the robot fleet is
different from that of RMF globally. In order to transform a pose from the RMF
frame to the free fleet robot frame, it is first scaled, rotated, then
translated using these parameters -->
<param name="scale" value="0.928"/>
<param name="rotation" value="-0.013"/>
<param name="translation_x" value="-4.117"/>
<param name="translation_y" value="27.26"/>
</node>
此外,在存储库中也可以找到此配置的示例,位于包“ff_examples_ros1”和“ff_exmaples_ros2”下。此示例启动示例模拟来自“ROBOTIS”,它有一个小型模拟世界,其中有 3 个 Turtlebot3 移动机器人,每个机器人都运行自己的 ROS 1 导航堆栈。
成功构建 ROS 1 和 ROS 2 工作区后,可以按照这些说明 启动模拟,其中还包括一个 ROS 2“free_fleet”服务器,发布舰队状态消息并通过 ROS 2 消息和主题接受模式和导航请求。
ROS 2 导航堆栈
使用 ROS 2 实现的机器人与前面描述的 ROS 1 导航堆栈类似。目前,ROS 2 free_fleet
客户端仍在开发中。重构、实现和测试完成后,将更新本节。
存储库中相同的现成 free_fleet
服务器实现将在此场景中工作,因为舰队适配器提供的接口仍然是相同的 ROS 2 消息和主题。
如果在此期间需要,用户可以通过使用包含 DDS 通信基本实现和 API 的 free_fleet
库来实现自己的 free_fleet
客户端。下一节 开发人员导航堆栈 将进一步阐述。
开发人员导航堆栈
在此实现中,假设移动机器人上运行的软件是由机器人开发人员自己(或其直接分包商)编写的,并且开发人员完全了解并可以访问其机器人的内部控制软件、API 和接口。这种级别的理解和访问对于实现您自己的 free_fleet
客户端包装器是必要的。下面的框图说明了此配置:

一旦开发人员的“free_fleet”客户端完全发挥作用,启动本节前面提到的相同的 ROS 2“free_fleet”服务器以通过 ROS 2 消息和主题与舰队适配器协同工作将是一个简单的任务。
只读队列集成
在本节中,我们将介绍用于集成只读类别的移动机器人队列的原型 API。这意味着我们假设移动机器人队列管理器仅允许 RMF 查看其机器人所在位置和它们打算去往何处的更新,但它不提供对机器人去往何处或如何移动的任何控制。这种类型的适配器主要针对在 RMF 之前开发的旧系统,并且没有预料到第三方能够指挥机器人的可能性。
队列驱动程序 API
队列驱动程序 API 是在 RMF 研究项目早期阶段开发的实验性 API。在正式支持的 C++ API 出现以取代它之前,它仍可用于只读队列适配器实现。
Fleet Driver API 使用来自 rmf_fleet_msgs
包的 ROS 2 消息。要使用此 API,您需要编写一个 ROS 2 应用程序(使用 rclcpp 或 rclpy),我们将其称为 Fleet Driver。Fleet Driver 的工作是将 rmf_fleet_msgs/FleetState
消息传输到 fleet_states
主题。
FleetState
消息内是 name
字段。请务必填写正确的舰队状态名称。还有一组 rmf_fleet_msgs/RobotState
消息。要将只读队列与 RMF 集成,RobotState
消息的最重要字段是:
name
- The name of the robot whose state is being specified.location
- The current location of the robot.path
- The sequence of locations that the robot will be traveling through.
在 rmf_fleet_msgs/Location
消息中,t
字段(代表时间)通常被只读车队适配器忽略。我们假设您的车队驱动程序进行时间预测过于繁琐,因此我们让只读车队适配器根据车辆的特性为您进行预测。
配置只读队列适配器
对于原型只读集成,需要启动两个应用程序:
- 上面提到的 Fleet Driver,您专门为舰队的自定义 API 编写
read_only
舰队适配器,必须通过 ROS 2 启动
要启动舰队适配器,您需要使用 ros2 launch
并包含 rmf_fleet_adapter/fleet_adapter.launch.xml
文件,其中填写了所需的参数。使用 ros2 launch 的 XML 前端的示例可在 rmf_demos
中找到,复制如下:
<?xml version='1.0' ?>
<launch>
<arg name="fleet_name" default="caddy" description="Name of this fleet of caddy robots"/>
<group>
<include file="$(find-pkg-share rmf_fleet_adapter)/fleet_adapter.launch.xml">
<!-- The name and control type of the fleet -->
<arg name="fleet_name" value="$(var fleet_name)"/>
<arg name="control_type" value="read_only"/>
<!-- The nominal linear and angular velocity of the caddy -->
<arg name="linear_velocity" value="1.0"/>
<arg name="angular_velocity" value="0.6"/>
<!-- The nominal linear and angular acceleration of the caddy -->
<arg name="linear_acceleration" value="0.7"/>
<arg name="angular_acceleration" value="1.5"/>
<!-- The radius of the circular footprint of the caddy -->
<arg name="footprint_radius" value="1.5"/>
<!-- Other robots are not allowed within this radius -->
<arg name="vicinity_radius" value="5.0"/>
<arg name="delay_threshold" value="1.0"/>
</include>
</group>
关键参数包括:
fleet_name
:必须与 Fleet Driver 赋予其FleetState
消息的name
值匹配。control_type
:必须为"read_only"
。linear_velocity
、angular_velocity
、linear_acceleration
和angular_acceleration
:这些是车辆运动学特性的估计值。为了有效调度,最好高估这些值而不是低估它们,因此最好将这些参数视为值的上限。footprint_radius
:车辆占据的物理空间半径。这应该覆盖物理足迹的最大范围。vicinity_radius
:机器人周围的半径,其他机器人禁止进入。假设另一个机器人进入此半径将干扰此机器人的运行能力。
当启动文件和 Fleet Driver 应用程序都准备就绪时,您可以并排启动它们,这样只读车队适配器的集成就完成了。
Doors
地图要求
在正确集成门之前,请确保使用“traffic_editor”在导航图上绘制具有正确门名的门位置。操作说明可在 Traffic Editor chapter.
一体化
将 RMF 集成到新环境时需要进行门集成。出于显而易见的原因,只有自动门可以与 RMF 集成,尽管可以触发警报以向指定人员打开手动门,但不建议这样做。自动门可以定义为可远程控制的电动门,可以使用远程触发器或配备计算单元,该计算单元能够使用某些接口在需要时命令门打开和关闭。
可以使用 ROS 2 门节点和门适配器将门与 RMF 集成,我们有时将其称为门监控器。下面的框图显示了每个组件之间的关系和通信模式:

为了处理门控制器模块的特定 API,必须根据要集成的门的品牌和型号来实现门节点。通信协议也将取决于门和控制器模型,可能是某种形式的“REST”、“RPCXML”等。门节点负责使用下面列出的消息和主题通过 ROS 2 发布其状态并接收命令:
Message Types | ROS2 Topic | Description |
---|---|---|
rmf_door_msgs/DoorState | /door_states | State of the door published by the door node |
rmf_door_msgs/DoorRequest | /door_requests | Direct requests subscribed by the door node and published by the door adapter |
rmf_door_msgs/DoorRequest | /adapter_door_requests | Requests to be sent to the door adapter/supervisor to request safe operation of doors |
门适配器位于其余 RMF 核心系统、队列适配器和门节点之间,充当状态监控器,确保门不会执行可能妨碍正在进行的移动机器人任务或意外关闭的请求。它跟踪来自门节点的门状态,并接收来自 adapter_door_requests
主题的请求,这些请求由队列适配器或 RMF 核心系统的其他部分发布。只有当门适配器认为请求足够安全可以执行时,它才会使用请求指示门节点。还应注意,直接发送到门节点的请求(未经过门适配器)将被门适配器否定,以将其返回到之前的状态,以防止移动机器人操作期间发生中断。
门适配器模板
为了使门与 RMF 的集成过程更加简单,我们开源了一个 模板包,用户只需使用特定门控制器的 API 调用更新某些代码块即可。
Lifts (i.e. Elevators)
地图要求
在正确集成电梯之前,请务必使用“traffic_editor”在导航图上绘制电梯位置以及正确的电梯名称和楼层。操作说明可在 Traffic Editor 一章中找到。
集成
电梯集成将允许 RMF 在多个楼层上工作,解决冲突并更大规模地管理共享资源。与门集成类似,基本要求是电梯控制器使用规定的协议接受命令,“OPC”就是这样一个例子。
电梯也将以与门类似的方式集成,依靠升降节点和升降适配器。以下框图显示了每个组件如何相互配合:

升降节点将充当驱动器,与升降控制器配合使用。升降节点的示例可在此 存储库 中找到。节点将使用下面列出的消息和主题通过 ROS 2 发布其状态并接收升降请求。
Message Types | ROS2 Topic | Description |
---|---|---|
rmf_lift_msgs/LiftState | /lift_states | State of the lift published by the lift node |
rmf_lift_msgs/LiftRequest | /lift_requests | Direct requests subscribed by the lift node and published by the lift adapter |
rmf_lift_msgs/LiftRequest | /adapter_lift_requests | Requests to be sent to the lift adapter/supervisor to request safe operation of lifts |
升降机适配器订阅“lift_states”,同时跟踪升降机的内部和期望状态,以防止其执行任何可能中断移动机器人或正常操作的操作。升降机适配器通过接收来自车队适配器和 RMF 核心系统的升降机请求并仅在适当的情况下将指令转发给升降机节点来执行此任务。任何直接发送到升降机节点而不经过升降机适配器的请求也将被升降机适配器否定,以防止对移动机器人车队操作造成不必要的干扰。
工作单元
目前RMF有2种类型的样品工作单元,分别是:“Dispenser”和“Ingestor”。
Message Types | ROS2 Topic | Description |
---|---|---|
rmf_dispenser_msgs/DispenserRequest | /dispenser_reqeusts | Direct requests subscribed by the dispenser node |
rmf_dispenser_msgs/DispenserResult | /dispenser_results | Result of a dispenser request, published by the dispenser |
rmf_dispenser_msgs/DispenserState | /dispenser_states | State of the dispenser published by the dispenser periodically |
rmf_ingestor_msgs/IngestorRequest | /ingestor_requests | Direct requests subscribed by the ingestor node |
rmf_ingestor_msgs/IngestorResult | /ingestor_results | Result of a ingestor request, published by the ingestor |
rmf_ingestor_msgs/IngestorState | /ingestor_states | State of the dispenser published by the ingestor periodically |
In rmf_demos
world, both TeleportDispenser
and TeleportIngestor
plugins 充当工作单元适配器节点。
工作单元目前与交付任务一起工作. In fleet_adapter.lauch.xml
,
perform_deliveries
机器人接受送货任务时需要为true
。
完整配送:
- 机器人将首先移动到
pickup_waypoint
- 请求
DispenserRequest
直到收到DispenserResult
。(分配完成) - 继续配送并移动到
dropoff_waypoint
- 请求
IngestorRequest
直到收到IngestorResult
。(摄取完成)
RMF Web
介绍
Open-RMF Web 提供了全面的 Web 应用程序工具包,其中包括 API 服务器、可重复使用的前端组件以及可自定义的前端仪表板。
有关设置和文档的更多信息,请参阅 rmf-web
存储库。
对于希望全面部署 Open-RMF 及其核心库、构建基础设施适配器、队列适配器和 Open-RMF Web 的用户,请参阅 Open-RMF deployment template.
相关存储库
UI
本文档的某些部分已被弃用,请参阅 rmf-web 了解最新开发情况。
简介
本章介绍如何编写集成到 RMF 的最终用户应用程序。我们将简要介绍 RMF UI 应用程序的要求,并提供编写与 RMF 交互的 React Webapp 的教程。
概述
编写 UI 应用程序最常见的方法是使用 UI 框架。有许多可用的 UI 框架,以下是一些流行的框架供参考:
- Multi-Paradigm
- Qt
- React native
- Xamarin
- Flutter
- Web
- React
- Angular
- Vue
- Desktop
- wxWidgets
- Gtk
- WPF (Windows only)
每个框架都有优点和缺点,我们并不认为存在一个适用于所有用例的“最佳”框架。话虽如此,由于其特定的底层技术,某些框架更适合与 RMF 集成。为了理解原因,让我们简要概述一下 RMF UI 应用程序的工作原理。
RMF UI 应用程序如何工作?
想象一个简单的 UI 应用程序,它有一个标签,显示门的当前状态,还有一个打开/关闭门的按钮。回想一下,在门适配器中,我们发布门状态并订阅门请求。在这里,我们做相反的事情;订阅门状态并发布门请求。我们将监听来自 RMF 的门状态并更新我们的显示以匹配,并且我们还将在用户单击打开或关闭按钮时发送门请求。
我们需要使用库来帮助我们做到这一点。在硬件适配器示例中,我们使用 rclcpp
库,但 rclcpp
并不是使用 ROS 2 的唯一方法。以下是一些同样有效的库
- Direct
- rcl (C)
- rclcpp (C++)
- rclpy (python)
- rclnodejs (nodejs)
- Indirect
- SOSS (websocket)
- rosbridge (websocket)
“直接”库能够直接从应用程序发送和接收 ROS 2 消息,而间接库则使用中间人服务来转发 ROS 2 消息。一般来说,“直接”方法更可取,但有时在目标平台上无法实现。在这些情况下,可以使用间接方法。
ros2-dotnet
project for example, provides C# bindings for ROS 2. You can also write your own bindings and middlewares!
编写 RMF UI 应用程序与编写任何其他 UI 应用程序没有太大区别,唯一的区别在于我们将根据用户输入发送/接收 ROS 2 数据并更新 UI 的状态。
教程:React WebApp
在本节中,我们将通过一个示例来创建基于 React 的 Web 应用程序来监控门状态并发送门打开/关闭请求。本教程将重点介绍与 RMF 通信的各个方面;需要具备 React 和 TypeScript 的基本知识。
请注意,这不是创建 RMF UI 应用程序的唯一方法,如前所述,您可以使用任何 UI 工具包,唯一的要求是能够发送/接收 ROS 2 消息。
本教程的代码已发布 here.
要求
- nodejs >= 10
- rmf_core
- soss
- rmf-soss-ros2
- rmf_demos
我们不会介绍设置依赖项的过程,可以在网络上或项目主页上轻松找到设置依赖项的说明。
设置
我们将使用 rmf_demo
中的示例作为我们将与之交互的 RMF 部署。这是测试您的 rmf_demo
安装是否正常运行的好时机,使用以下命令启动演示:
ros2 launch demos office.launch.xml
接下来让我们测试一下 SOSS 是否正常工作。为了运行 SOSS,您需要为其提供一个配置文件;您可以使用 此 模板作为开始。您需要一个 SOSS 证书;请参阅网上的各种教程来生成证书。如果您使用的是自签名证书,还要确保您的浏览器设置为接受它进行 websocket 连接。将您的证书和密钥的路径添加到 SOSS 配置并尝试使用以下命令启动 SOSS:
soss <path_to_config>
环境设置完成后,我们就可以继续设置实际的应用程序。首先,创建一个 React 工作区:
npx create-react-app react-app-tutorial --template typescript
进入新创建的“react-app-tutorial”目录并运行以下命令来安装我们将使用的所有 JavaScript 依赖项:
npm install @osrf/romi-js-core-interfaces @osrf/romi-js-soss-transport jsonwebtoken @types/jsonwebtoken
这些库不是严格必需的,但它们包含使用“soss”和与 RMF 通信的有用函数。如果您正在构建基于 JavaScript 的 RMF 应用程序,建议使用它们,我们稍后会看到它们如何简化与 RMF 的通信。
@osrf/romi-js-soss-transport
, there is also @osrf/romi-js-rclnodejs-transport
which is able to send ROS 2 messages directly, however it does not work on the browser. It is preferred if you are writing a Node.js based desktop application using something like Electron, or you are writing a server based application like a REST API provider.
门组件
首先,让我们从简单开始,创建一个组件来显示门的状态以及打开和关闭按钮。在“react-app-tutorial/src”目录中创建一个名为“Door.tsx”的新文件,并将以下代码片段复制到其中。
import React from 'react';
export interface DoorProps {
name: string;
state: string;
onOpenClick?(e: React.MouseEvent): void;
onCloseClick?(e: React.MouseEvent): void;
}
export const Door = (props: DoorProps) => {
const { name, state, onOpenClick, onCloseClick } = props;
return (
<div>
Door: {name}
<br />
State: {state}
<br />
<button onClick={(e) => onOpenClick && onOpenClick(e)}>Open</button>
<button onClick={(e) => onCloseClick && onCloseClick(e)}>Close</button>
<br />
<br />
</div>
);
};
export default Door;
这里还没有发生什么。我们只是渲染门的名称、状态以及打开和关闭按钮。
让我们通过运行它来测试一下。将“App.tsx”替换为以下内容:
import React from 'react';
import Door from './Door';
function App() {
return <Door name="example_door" state="Closed" />;
}
export default App;
并使用“npm start”启动它。你应该看到类似这样的内容:
太棒了!现在我们有了开始实现应用程序其余部分的基础。
获取门列表
之前我们制作了一个简单的门组件,并测试了使用硬编码值渲染它。显然这在合适的应用程序中不起作用,所以在这里我们将研究如何从 RMF 获取实际门的列表。
首先添加一个 React 状态来跟踪门列表:
const [doors, setDoors] = React.useState<RomiCore.Door[]>([]);
RMF 有一个“get_building_map”服务,我们可以使用它来获取门、电梯、楼层等许多其他数据的列表,为了使用该服务,我们需要进行 ROS 2 服务调用,因为浏览器不支持 ROS 2,我们将使用“soss”的“间接”方法。与 SOSS 建立 websocket 连接,然后 SOSS 将充当中间人并将我们的消息传递到 ROS 2 网络。
使用 SOSS 的一个简单方法是使用 @osrf/romi-js-soss-transport
包,现在就开始吧。将一个 React Effect Hook 添加到你的 App
组件中
React.useEffect(() => {
(async () => {
const token = jwt.sign({ user: 'example-user' }, 'rmf', { algorithm: 'HS256' });
const transport = await SossTransport.connect('example', 'wss://localhost:50001', token);
})();
}, []);
我们需要导入“SossTransport”,因此将其添加到文件顶部:
import { SossTransport } from '@osrf/romi-js-soss-transport';
这将执行与 SOSS 服务器的 websocket 连接。example
是我们将要使用的 ROS 2 节点名称,我们将连接到位于 wss://localhost:50001
的 SOSS 服务器。该服务器使用在 SOSS 配置中指定的密钥签名的 JWT 令牌。示例配置使用 rmf
。如果您更改了密钥,请确保也在此处进行更改。
现在我们已经连接到 SOSS,我们可以调用“get_building_map”服务。将其添加到 React 效果中:
const buildingMap = (await transport.call(RomiCore.getBuildingMap, {})).building_map;
setDoors(buildingMap.levels.flatMap((level) => level.doors));
它使用“RomiCore”,因此将其添加到您的导入中:
import * as RomiCore from '@osrf/romi-js-core-interfaces';
这将从 RMF 下载并解析建筑地图。romi-js
使用异步 call
方法简化了 ROS 2 服务调用。如果您熟悉 rclnodejs
,这大致相当于:
const client = node.createClient(
'building_map_msgs/srv/GetBuildingMap',
'get_building_map'
);
client.sendRequest({}, response => {
const buildingMap = response.building_map;
setDoors(buildingMap.levels.flatMap((level) => level.doors));
});
请注意,我们需要为其提供消息类型(building_map_msgs/srv/GetBuildingMap
)和服务名称(get_building_map
),但我们如何找出服务名称和类型呢?我们可以在 RMF 运行时阅读 RMF 手册或查询 ROS 2 系统。另一种方法是借助RomiCore
;它提供了已知 RMF 服务和消息的列表,因此您不必费心自己寻找它们。
在下面的语句中,我们使用RomiCore
调用get_building_map
服务,而无需知道服务名称和类型:
transport.call(RomiCore.getBuildingMap, {})
现在我们有了“RomiCore.Door”列表,让我们通过更新“Door.tsx”将其作为 prop 来简化事情。在此过程中,我们还可以让它接受“RomiCore.DoorState”作为 prop,因为我们稍后会使用它。
Door.tsx:
import * as RomiCore from '@osrf/romi-js-core-interfaces';
import React from 'react';
export interface DoorProps {
door: RomiCore.Door;
doorState?: RomiCore.DoorState;
onOpenClick?(e: React.MouseEvent): void;
onCloseClick?(e: React.MouseEvent): void;
}
export const Door = (props: DoorProps) => {
const { door, doorState, onOpenClick, onCloseClick } = props;
return (
<div>
Door: {door.name}
<br />
State: {doorState ? doorState.current_mode.value : 'Unknown'}
<br />
<button onClick={(e) => onOpenClick && onOpenClick(e)}>Open</button>
<button onClick={(e) => onCloseClick && onCloseClick(e)}>Close</button>
<br />
<br />
</div>
);
};
export default Door;
现在我们可以通过将门作为 props 传递来测试它。你的 App.tsx
组件现在应该如下所示:
import * as RomiCore from '@osrf/romi-js-core-interfaces';
import { SossTransport } from '@osrf/romi-js-soss-transport';
import * as jwt from 'jsonwebtoken';
import React from 'react';
import Door from './Door';
function App() {
const [doors, setDoors] = React.useState<RomiCore.Door[]>([]);
React.useEffect(() => {
(async () => {
const token = jwt.sign({ user: 'example-user' }, 'rmf', { algorithm: 'HS256' });
const transport = await SossTransport.connect('example', 'wss://localhost:50001', token);
const buildingMap = (await transport.call(RomiCore.getBuildingMap, {})).building_map;
setDoors(buildingMap.levels.flatMap((level) => level.doors));
})();
}, []);
return (
<React.Fragment>
{doors.map((door) => (
<Door door={door} />
))}
</React.Fragment>
);
}
export default App;
现在不用担心门的状态。如果一切顺利,您应该会看到列出的建筑物内的 3 扇门:
监听门的状态
之前我们设法在 RMF 系统中渲染了门的列表,但建筑地图并没有告诉我们门的状态,所以让我们来解决这个问题。首先,让我们添加一个 React 状态来跟踪门的状态:
const [doorStates, setDoorStates] = React.useState<Record<string, RomiCore.DoorState>>({});
可以通过订阅“door_states”主题来获取门的状态。将以下内容添加到您的效果中:
transport.subscribe(RomiCore.doorStates, (doorState) =>
setDoorStates((prev) => ({ ...prev, [doorState.door_name]: doorState })),
);
这将执行对 RomiCore.doorStates
主题的 ROS 2 订阅。与我们之前执行的服务调用类似,romi-js
抽象出 ROS 2 主题名称并提供类型信息。每次收到新的门状态消息时都会触发回调。在回调中,我们只需更新 doorStates
状态即可。
现在只需将门状态传递给门组件:
<Door door={door} doorState={doorStates[door.name]} />
你的 App.tsx
的最终结果应该如下所示:
import * as RomiCore from '@osrf/romi-js-core-interfaces';
import { SossTransport } from '@osrf/romi-js-soss-transport';
import * as jwt from 'jsonwebtoken';
import React from 'react';
import Door from './Door';
function App() {
const [doors, setDoors] = React.useState<RomiCore.Door[]>([]);
const [doorStates, setDoorStates] = React.useState<Record<string, RomiCore.DoorState>>({});
React.useEffect(() => {
(async () => {
const token = jwt.sign({ user: 'example-user' }, 'rmf', { algorithm: 'HS256' });
const transport = await SossTransport.connect('example', 'wss://localhost:50001', token);
const buildingMap = (await transport.call(RomiCore.getBuildingMap, {})).building_map;
setDoors(buildingMap.levels.flatMap((level) => level.doors));
transport.subscribe(RomiCore.doorStates, (doorState) =>
setDoorStates((prev) => ({ ...prev, [doorState.door_name]: doorState })),
);
})();
}, []);
return (
<React.Fragment>
{doors.map((door) => (
<Door door={door} doorState={doorStates[door.name]} />
))}
</React.Fragment>
);
}
export default App;
就这样,我们现在有了门的状态!
门状态是数字,如 0
、1
和 2
。这是因为 RMF 使用常量来表示门状态。我们可以运行一个简单的函数将这些常量转换为字符串:
function doorModeString(doorMode: RomiCore.DoorMode): string {
switch (doorMode.value) {
case 2:
return 'Open';
case 0:
return 'Closed';
case 1:
return 'Moving';
default:
return 'Unknown';
}
}
但是我们怎么知道“2”表示“打开”等?我们可以通过阅读 RMF 手册或检查 ROS 2 消息定义来找到答案,但我们可以使用“RomiCore”做得更好。它以更易读的形式提供了常量列表:
function doorModeString(doorMode: RomiCore.DoorMode): string {
switch (doorMode.value) {
case RomiCore.DoorMode.MODE_OPEN:
return 'Open';
case RomiCore.DoorMode.MODE_CLOSED:
return 'Closed';
case RomiCore.DoorMode.MODE_MOVING:
return 'Moving';
default:
return 'Unknown';
}
}
这样,每个常量代表什么就一目了然了,所以我们不必参考任何其他东西来找到它的含义。
继续将其添加到您的“Door.tsx”中。它现在应该看起来像这样:
import * as RomiCore from '@osrf/romi-js-core-interfaces';
import React from 'react';
export interface DoorProps {
door: RomiCore.Door;
doorState?: RomiCore.DoorState;
onOpenClick?(e: React.MouseEvent): void;
onCloseClick?(e: React.MouseEvent): void;
}
export const Door = (props: DoorProps) => {
const { door, doorState, onOpenClick, onCloseClick } = props;
const modeString = doorState ? doorModeString(doorState.current_mode) : 'Unknown';
return (
<div>
Door: {door.name}
<br />
State: {modeString}
<br />
<button onClick={(e) => onOpenClick && onOpenClick(e)}>Open</button>
<button onClick={(e) => onCloseClick && onCloseClick(e)}>Close</button>
<br />
<br />
</div>
);
};
function doorModeString(doorMode: RomiCore.DoorMode): string {
switch (doorMode.value) {
case RomiCore.DoorMode.MODE_OPEN:
return 'Open';
case RomiCore.DoorMode.MODE_CLOSED:
return 'Closed';
case RomiCore.DoorMode.MODE_MOVING:
return 'Moving';
default:
return 'Unknown';
}
}
export default Door;
Great! Now we have readable door states instead of cryptic numbers.
发送门请求
正如您现在可能已经预料到的那样,我们在这里要做的就是向 RMF 发送门请求。
首先,创建一个发布者。将其添加到渲染函数的开头:
const doorRequestPub = React.useRef<RomiCore.Publisher<RomiCore.DoorRequest> | null>(null);
然后添加这个辅助函数:
const requestDoor = (door: RomiCore.Door, mode: number) => {
if (doorRequestPub.current) {
const request: RomiCore.DoorRequest = {
door_name: door.name,
request_time: RomiCore.toRosTime(new Date()),
requested_mode: { value: mode },
requester_id: 'example-request',
};
doorRequestPub.current.publish(request);
}
};
它接收一个 RomiCore.Door
和一个代表所需模式的数字,并制作一个 RomiCore.DoorRequest
消息并使用发布者发送它。通常,您必须查阅 RMF 手册或 ROS 2 定义才能确切知道您需要发送什么。同样,RomiCore
提供了输入信息,以便更轻松地填写必填字段。
最后,将其添加到传递给门组件的 props 中:
onOpenClick={() => requestDoor(door, RomiCore.DoorMode.MODE_OPEN)}
onCloseClick={() => requestDoor(door, RomiCore.DoorMode.MODE_CLOSED)}
你最终的 App.tsx
应该如下所示:
import * as RomiCore from '@osrf/romi-js-core-interfaces';
import { SossTransport } from '@osrf/romi-js-soss-transport';
import * as jwt from 'jsonwebtoken';
import React from 'react';
import Door from './Door';
function App() {
const [doors, setDoors] = React.useState<RomiCore.Door[]>([]);
const [doorStates, setDoorStates] = React.useState<Record<string, RomiCore.DoorState>>({});
const doorRequestPub = React.useRef<RomiCore.Publisher<RomiCore.DoorRequest> | null>(null);
React.useEffect(() => {
(async () => {
const token = jwt.sign({ user: 'example-user' }, 'rmf', { algorithm: 'HS256' });
const transport = await SossTransport.connect('example', 'wss://localhost:50001', token);
const buildingMap = (await transport.call(RomiCore.getBuildingMap, {})).building_map;
setDoors(buildingMap.levels.flatMap((level) => level.doors));
transport.subscribe(RomiCore.doorStates, (doorState) =>
setDoorStates((prev) => ({ ...prev, [doorState.door_name]: doorState })),
);
doorRequestPub.current = transport.createPublisher(RomiCore.adapterDoorRequests);
})();
}, []);
const requestDoor = (door: RomiCore.Door, mode: number) => {
if (doorRequestPub.current) {
const request: RomiCore.DoorRequest = {
door_name: door.name,
request_time: RomiCore.toRosTime(new Date()),
requested_mode: { value: mode },
requester_id: 'example-request',
};
doorRequestPub.current.publish(request);
}
};
return (
<React.Fragment>
{doors.map((door) => (
<Door
door={door}
doorState={doorStates[door.name]}
onOpenClick={() => requestDoor(door, RomiCore.DoorMode.MODE_OPEN)}
onCloseClick={() => requestDoor(door, RomiCore.DoorMode.MODE_CLOSED)}
/>
))}
</React.Fragment>
);
}
export default App;
现在尝试单击打开和关闭按钮。您应该会看到门状态正在更新。您还可以在 Gazebo 中看到门打开/关闭。 恭喜,您刚刚编写了一个简单的 RMF UI 应用程序!显然,设计还有很多不足之处,因为我们没有做任何 CSS 样式,但这超出了本教程的范围。
扩展它以提供更多功能,如电梯控制、车队状态等,遵循相同的原则。RMF 公开的所有可用主题和服务都可以在“RomiCore”中找到,您可以通过阅读手册的其余部分找到更详细的信息。这也扩展到为其他平台和框架编写 UI 应用程序;从本质上讲,您实际上只是发布和订阅 ROS 2 消息,因此您可以将相同的原则应用于其他语言和框架。
结论
我们刚刚创建了一个最小的 RMF UI 应用程序,它报告门状态并允许用户控制门。为简单起见,本教程中没有包含太多功能,但本教程应提供如何创建 RMF UI 应用程序的基本知识,不仅在 React 中,而且在您喜欢的任何框架中。
如果您想要更多 React RMF 应用程序的示例,您可以查看官方 RoMi 仪表板。
附加:扩展 romi-js
在整个教程中,我们使用 romi-js
来简化与 RMF 的通信。您可能已经注意到,romi-js
实际上是一组包。这种设计使得可以轻松地使用新主题、服务甚至传输来扩展它。
添加主题和服务
主题和服务由以下接口定义:
export interface RomiTopic<Message> {
readonly validate: (msg: any) => Message;
readonly type: string;
readonly topic: string;
readonly options?: Options;
}
export interface RomiService<Request, Response> {
readonly validateRequest: (msg: any) => Request;
readonly validateResponse: (msg: any) => Response;
readonly type: string;
readonly service: string;
readonly options?: Options;
}
如果您熟悉 ROS 2,则 type
字段指定主题或服务期望的消息类型,而 topic
/service
分别是主题和服务名称。有时,主题或服务需要使用不同的 QoS 选项;例如,当状态发生变化时不发布并期望后期订阅使用瞬时本地 QoS 来接收最新状态的主题。options
指定应使用的“默认”QoS 选项。这样,用户不必参考使用说明来正确发布和订阅主题。
传输使用 validate*
方法将任意对象转换为主题或服务的预期类型。它应该检查对象是否具有正确的字段以及字段是否属于正确的类型。为了确保与不同传输的兼容性,这些方法应该能够将数字数组转换为类型数组,反之亦然。
我们可以通过实现这些接口来创建自定义主题或服务。然后可以将它们传递给传输的各种方法。
export const myTopic: RomiTopic<MyMessage> = {
validate: validateMyMessage(msg), // some function that valides MyMessage
type: 'my_messages/msg/MyMessage',
topic: 'my_topic',
};
添加传输
romi-js
中的 Transport
是一个具有以下接口的类:
export interface Subscription {
unsubscribe(): void;
}
export interface Publisher<Message> {
publish(msg: Message): void;
}
export interface Service<Request, Response> {
start(handler: (req: Request) => Promise<Response> | Response): void;
stop(): void;
}
export interface Transport extends TransportEvents {
readonly name: string;
createPublisher<Message extends unknown>(
topic: RomiTopic<Message>,
options?: Options,
): Publisher<Message>;
subscribe<Message extends unknown>(
topic: RomiTopic<Message>,
cb: SubscriptionCb<Message>,
options?: Options,
): Subscription;
call<Request extends unknown, Response extends unknown>(
service: RomiService<Request, Response>,
req: Request,
): Promise<Response>;
createService<Request extends unknown, Response extends unknown>(
service: RomiService<Request, Response>,
): Service<Request, Response>;
destroy(): void;
}
没有关于如何实现接口的通用指南,因为每个传输的细节都会有所不同。需要注意的一点是,返回从“any”派生的类型(例如“Publisher
为了确保与不同主题和服务的兼容性,传输必须将数据反序列化为普通的旧数据对象。它可以使用数字数组或类型数组。“validate*”方法应该支持将它们转换为预期的类型。
安全
本章介绍如何使用 DDS 安全工具为 RMF 系统提供身份验证、加密和访问控制。
RMF 系统的安全性可分为两个主要部分: 其 ROS 2 元素和仪表板。 ROS 2 元素的安全性由 DDS 安全工具提供,这些工具有助于确保身份验证、 加密和访问控制。仪表板为用户提供仪表板,同时确保通过 TLS 与服务器连接的加密、完整性和身份验证。用户身份验证和访问控制是通过针对数据库检查用户/密码,然后 为该用户提供与该用户角色相对应的安全 ROS 2 网络的访问权限来完成的。
RMF Demos Office World 存储库包含使用安全 ROS 2 通信的完整 RMF 应用程序示例以及分步说明。
ROS 2 安全
ROS 2 包含可帮助创建和加载所需安全工件的工具,以启用 DDS 安全。RMF 使用这些工具来启用其 ROS 2 元素上的安全性。这里简要介绍了这些工具及其用法。如需更深入地了解整个系统,请参阅 ROS 2 DDS 安全集成 文档。
DDS-Security 概述
DDS-Security 规范 在 DDS 规范 的基础上进行了扩展, 通过定义服务插件接口 (SPI) 架构、一组 SPI 的内置实现以及 SPI 强制执行的安全模型,增加了安全性增强功能。 具体来说,定义了五个 SPI:
- 身份验证:验证给定域参与者的身份。
- 访问控制:对经过身份验证的域参与者可以执行的 DDS 相关操作实施限制。
- 加密:处理所有必需的加密、签名和散列操作。
- 日志记录:提供审核 DDS 安全相关事件的能力。
- 数据标记:提供向数据样本添加标记的能力。
ROS 2 的安全功能目前仅使用前三个。 这是因为,日志记录和数据标记都不是符合 DDS-Security spec (参见第 2.3 节)所必需的,因此并非所有 DDS 实现都支持它们。
SROS 2 工具
由于 DDS-Security 插件需要每个域参与者一组安全文件,因此需要事先创建这些文件,以便为 RMF 的 ROS 2 元素提供身份验证、访问控制和加密。域参与者通常映射到 ROS 2 中进程内的上下文,因此每个进程都需要一组这样的文件。
由于 SROS 2 尚不支持启动文件,因此每个二进制文件都需要在其自己的终端上单独启动。或者,我们建议创建脚本来自动化此过程。
例如,[Office SROS 2 演示][https://github.com/open-rmf/rmf_demos/blob/main/docs/secure_office_world.md] 中使用的 tmux
脚本。
ros2 security
命令是访问 SROS 2 工具集以创建和管理 DDS-security 工件的方式。您可以通过 -h
标志访问文档来了解其功能:
$ ros2 security -h
usage: ros2 security [-h] Call `ros2 security <command> -h` for more detailed usage. ...
Various security related sub-commands
optional arguments:
-h, --help show this help message and exit
Commands:
create_key Create key
create_keystore Create keystore
create_permission Create permission
generate_artifacts Generate keys and permission files from a list of identities and policy files
generate_policy Generate XML policy file from ROS graph data
list_keys List keys
Call \`ros2 security <command> -h\` for more detailed usage.
SROS 2 环境变量
使用 SROS 2 时,您应该注意几个环境变量:
-
ROS_SECURITY_ENABLE 是 SROS 2 启用变量,它采用布尔值 (true 或 false)并指示是否启用了安全性。
-
ROS_SECURITY_STRATEGY 可以设置为
Enforce
或Permissive
,如果使用第一个,如果未找到安全文件,它将无法运行参与者,而如果使用第二个,如果未找到这些文件,它将仅在非安全模式下运行参与者。 -
ROS_SECURITY_KEYSTORE 应指向密钥库目录树的根。这将有助于
RCL
找到安全工件的位置以初始化 ROS 2 安全环境。
SROS 2 安全密钥库
密钥库是存储 DDS 安全工件的根目录。 RCL
将使用此目录的内容为 ROS 2 网络提供 DDS 安全性。ROS_SECURITY_KEYSTORE
环境变量应按照惯例指向此目录。为了初始化和填充 keystore_storage
目录文件,可以使用以下命令:
$ ros2 security create_keystore keystore_storage
creating keystore: keystore_storage
creating new CA key/cert pair
creating governance file: keystore_storage/enclaves/governance.xml
creating signed governance file: keystore_storage/enclaves/governance.p7s
创建密钥库后,其初始结构如下:
keystore_storage
├── enclaves
| ├── governance.p7s
| └── governance.xml
├── private
| ├── ca.key.pem
| ├── identity_ca.key.pem
| └── permissions_ca.key.pem
└── public
├── ca.key.pem
├── identity_ca.key.pem
└── permissions_ca.key.pem
public
目录包含任何可公开的内容,例如身份的公共证书或权限证书颁发机构 (CA)。因此,可以授予对所有可执行文件的读取权限。请注意,在默认情况下,identity_ca
和 permissions_ca
都指向同一个 CA 证书。
private
目录包含任何可私有的内容,例如上述证书颁发机构的私钥材料。在将密钥库部署到目标设备/机器人之前,应删除此目录。
enclaves
目录包含与各个安全区域相关的安全工件。
SROS 2 引入了安全“enclave”的概念,其中“enclave”是一个进程或一组进程,它们将共享相同的身份和访问控制规则。enclaves 文件夹可以递归嵌套子路径以组织单独的 enclave。
SROS 2 飞地密钥
初始化“密钥库”后,您可能希望为您的飞地创建安全密钥。这将使用必要的密钥和治理文件填充“飞地”目录。例如,为了为我们的“/hospital/rviz”飞地创建安全文件,将发出以下命令:
$ ros2 security create_key keystore_storage /hospital/rviz
creating key for identity: '/hospital/rviz'
creating cert and key
creating permission
在此之后,‘keystore_storage’目录应该包含 rviz enclave:
keystore_storage
├── enclaves
| ├── governance.p7s
| ├── governance.xml
│ └── hospital
| ├── rviz
│ | ├── cert.pem
│ | ├── key.pem
│ | ├── governance.p7s
| | ├── identity_ca_cert.pem
| | ├── permissions_ca.cert.pem
| | ├── permissions.p7s
| | └── permissions.xml
... ...
现在,有一个包含以下文件的 enclave:
- identity_ca.cert.pem:受 Authentication 插件信任的 CA 的 x.509 证书(“身份”CA)。
- cert.pem:此 enclave 实例的 x.509 证书(由身份 CA 签名)。
- key.pem:此 enclave 实例的私钥。
- permissions_ca.cert.pem:受 Access control 插件信任的 CA 的 x.509 证书(“权限”CA)。
- governance.p7s:向 Access control 插件指定应如何保护域的 XML 文档(由权限 CA 签名)。
- permissions.p7s:指定此特定 enclave 实例对访问控制插件的权限的 XML 文档(也由权限 CA 签名)。
SROS 2 访问控制
为了提供访问控制,需要将安全权限添加到 enclave 的权限文件中
并由 CA 签名。为此,需要遵循
SROS 2 策略架构 的策略文件。
此文件指定授予 ROS 2 网络内 enclave 的权限。
要创建和签署权限,可以使用 create_permission
选项:
$ ros2 security create_permission keystore_storage /hospital/rviz policy.xml
creating permission file for identity: '/hospital/rviz'
creating permission
运行此命令后,
enclave
/hospital/rviz 的 permissions.p7s
和 permissions.xml
文件将包含 policy.xml 中指定的签名权限。
在启动我们的流程时,我们必须通过 rosargs 指定此 enclave:
$ ros2 run <package> <executable> --rosargs ---enclave /hospital/rviz
SROS 2 自动生成
生成密钥和权限的过程有时可能很繁琐,因此 SROS 2 提供了工具来自动化此过程。提供了一种自动生成策略的方法,可以通过运行以下命令来触发:
$ ros2 security generate_policy policy.xml
此命令将在运行时获取 ROS 图并生成与其对应的 policy.xml 文件。请注意,由于它仅使用当前 ROS 图作为信息来源,因此可能仍会错过未来发布者、订阅者、服务或其他人的政策。
流程和密钥及权限的生成也可能很麻烦,SROS 2 提供了一个命令,您可以使用它一次性生成所有这些:
$ ros2 security generate_artifacts -k keystore_storage -p policy.xml
keystore_storage is not a valid keystore, creating new keystore
creating keystore: keystore_storage
creating new CA key/cert pair
creating governance file: keystore_storage/enclaves/governance.xml
creating signed governance file: keystore_storage/enclaves/governance.p7s
all done! enjoy your keystore in keystore_storage
cheers!
creating key for identity: '/hosptial/rviz'
creating cert and key
creating permission
creating permission file for identity: '/hosptical/rviz'
creating permission
...
最后,SROS 2 还提供了一种轻松列出某个密钥存储的密钥的方法:
$ ros2 security list_keys keystore_storage
/hospital/building_map_server
/hopsital/building_systems_visualizer
/hospital/door_supervisor
/hospital/fleet_state_visualizer
/hosptial/loop_request_publisher
/hosptial/rviz2
...
RMF Web 仪表板安全
[RMF Web] (https://github.com/open-rmf/rmf-web) 是一个 Web 应用程序,可提供对 RMF 系统的整体可视化和控制。它通过 TLS 提供服务,以确保与最终用户的通信的加密、完整性和身份验证。服务器使用 openid-connect (OIDC) 进行身份验证,这是一个基于 oauth 2.0 和 JOSE 的开放标准。目前,仪表板使用 Keycloack,这是 OIDC 的开源实现。它提供了一个用户管理系统,用于创建/删除用户。每个用户都被分配一个角色,该角色反映在用户生成的 id 令牌上。此 id 令牌经过安全签名并发送到 api 服务器,然后它可以查看用户的角色并采取相应行动。 API 服务器为每个角色运行一个安全的 ROS 2 节点,并根据每个用户的 id 令牌提供对它们的访问权限。
Roadmap
本页描述了我们目前正在研究的主题,并预计在未来 12 个月内取得进展。 我们无法承诺具体的开发时间表,但社区对哪些主题最受关注的反馈是可以帮助影响我们工作队列优先级的一个因素。 与任何研发项目一样,我们将对事物在多个维度上的发展做出反应。
Open-RMF 是一个开源项目。 我们将继续公开开发,以便社区成员可以实时看到正在发生的事情。 让我们一起努力吧! 除了我们自己的努力之外,我们鼓励(并且非常乐意看到!)来自社区的贡献,因此如果您看到您感兴趣的内容,请[通过 GitHub 与我们合作](https://github.com/open-rmf/rmf/discussions)!
可扩展性
谈到多机器人协调,可扩展性是一个复杂的话题,有许多不同的方面,每个方面对整个故事都至关重要。 可扩展性的担忧可以从变量及其产生的成本的角度来看待。
变量
- A. 共享空间的移动机器人数量
- B. 每个移动机器人的能力
- C. 共享空间的大小
- D. 狭窄通道和瓶颈的严重程度
- E. 系统网络拓扑(LAN、有线、Wi-Fi、远程/云)
成本
- 1. 计算资源(CPU 负载、RAM)
- 2. 寻找解决方案的时间
- 3. 解决方案的质量(接近全局最优、帕累托效率 或其他标准)
- 4. 操作质量(响应性、执行流畅性)
下表列出了为提高可扩展性而做出的持续努力,并指明了哪些变量将得到更好的扩展以及哪些成本将得到改善。请参阅下表了解每项改进的详细说明。
Improvement | A | B | C | D | E | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|---|---|---|---|
Reservation System | ✅ | ✅ | ✅ | ✅ | ✅ | ||||
Queuing System | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |||
Hierarchical Planning | ✅ | ✅ | ✅ | ✅ | ✅ | ||||
Custom Negotiation | ✅ | ✅ | ✅ | ✅ | |||||
Zenoh | ✅ | ✅ | ✅ | ✅ | ✅ |
预订系统
最初在 此处 中描述,预订系统将以全局最优的方式将相关资源分配给机器人。该系统最初是为停车位设计的,但也适用于电梯和工作单元。
排队系统
最初在 此处 中描述,当多个机器人需要按顺序使用一种资源(例如穿过一扇门)时,排队系统将管理严格定义的排队程序,而不是依赖交通协商系统来解决狭窄走廊的使用问题。
分层规划
分层交通规划将把整体规划空间分解为由阻塞点分隔的区域。每个阻塞点将由一个队列管理。交通协商系统只会考虑机器人从一个队列转换到另一个队列时发生的交通冲突。减少冲突范围将使协商更快、更少地频繁进行,并且 CPU 占用更低。
自定义协商
并非所有移动机器人或用例都适合“完全控制”、“交通灯”和“只读”这三个现成的类别。让系统集成商完全自定义车队适配器为其机器人规划和协商的方式将使 Open-RMF 能够支持更多类型的机器人并获得更好的结果。
Zenoh
ROS 2 中 Open-RMF 的实现存在各种效率低下的问题,与 DDS 发现机制以及 ROS 2 中缺乏消息密钥支持有关。直接使用 Zenoh 或将其作为 ROS 2 的 RMW 实现,将实现更高效的通信,尤其是通过 Wi-Fi 网络以及大量代理和子系统。
工具
管理和理解大型复杂系统需要能够全面洞察系统运行方式和原因的工具。我们正在努力将 站点编辑器 开发为可视化、事件模拟和数字孪生工具的基线,该工具可以帮助开发人员和最终用户了解 Open-RMF 系统(无论是模拟还是部署)中正在发生的事情及其原因。计划的功能包括:
- 大规模事件驱动模拟(实时或超实时模拟数百个移动机器人和其他 Open-RMF 系统)
- 实时部署的数字孪生 3D 视图,可视化正在做出的决策
- 调试工具以查看计划和协商,可视化解释每个解决方案产生的原因
功能
为了扩大 Open-RMF 的使用范围和使用方式,我们还计划推出新功能:
- 自由空间交通协商
- 通过绘制行为图来描述任务
- 支持具有并行活动线程的多代理任务
- 允许在任务之间定义约束
开发者体验
为了更广泛地采用和更快地部署,我们希望改善系统集成商的开发者体验。
* 更简单的 API,允许对机器人行为进行更大程度的自定义
- 更简单的集成选项,例如与 ROS 2 Nav Stack 的“本机”集成
- 我们将提供一个 ROS 2 nav stack 插件,使使用它的任何机器人都具有 Open-RMF 兼容性
- 更多文档,包括更新本书
- 站点编辑器中的交互式教程