机器人编程学习指南-全-

机器人编程学习指南(全)

原文:zh.annas-archive.org/md5/6f86c02400b44f7c565792c9032673f8

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

学习机器人编程是关于构建和编程具有智能行为的机器人。它涵盖了制作和构建从部件组成的设备所需的技能,包括如何选择它们。这些部件包括传感器、电机、摄像头、麦克风、扬声器、灯光和树莓派。

本书继续介绍如何编写代码使这些部件做些有趣的事情。本书使用Python,以及一点HTML/CSS和JavaScript。

使用的科技旨在包括可获取且负担得起的东西,代码展示用于演示概念,因此它们可以被使用和组合,以创建更有趣的代码和机器人。

这些主题结合了程序员和机器人制造商的方面,还加入了一些专业主题,如计算机视觉和语音助手。

本书的目标读者

本书面向有一定编程经验的人,或者更有经验但希望将技能应用于硬件项目的人。你不需要是专家级程序员,但必须写过一些代码,并且对循环、条件和函数感到舒适。了解面向对象(类和对象)的编程不是必需的,但本书会介绍这一内容。

本书不需要专门的工坊,尽管会有一些焊接和组装东西的工作。这些内容将在本书的后面部分介绍。

你不需要有任何电子或制作东西的经验,但希望你能对学习有浓厚的兴趣,因为书中介绍了许多非常基础的概念。对构建机器人、让它做事情以及找出下一步该做什么可能是本书最重要的方面。

本书涵盖的内容

第1章机器人简介,介绍了什么是机器人,在家和工业中找到例子,以及初学者构建的机器人类型。

第2章探索机器人构建模块 – 代码和电子学,探讨了机器人的组成部分。我们将开始选择机器人的部件,并查看系统和代码的框图。

第3章探索树莓派,介绍了树莓派及其连接以及我们将要在其上使用的Raspbian Linux操作系统,还涵盖了为机器人使用准备的SD卡。

第4章为机器人准备无头树莓派,展示了如何设置一个无需连接的树莓派并无线与之通信。

第5章使用Git和SD卡副本备份代码,展示了代码可能会丢失或损坏,以及保护你的工作和保留其历史的方法。

第6章, 构建机器人基础 - 轮子、电源和布线,介绍了购买和测试安装机器人底盘的权衡,然后组装它。

第7章, 驱动和转向 - 使用Python移动电机,展示了如何编写代码来移动机器人,为后续章节中的代码奠定了基础。

第8章, 使用Python编程距离传感器,添加传感器和代码,使机器人能够自主避开墙壁和障碍物。

第9章, 在Python中编程RGB灯带,为你的机器人添加多彩灯光。探索这个额外的输出,可用于调试或机器人上的娱乐。

第10章, 使用Python控制伺服电机,展示了如何使用这些电机定位传感器头部,以及它们可以在其他机器人的手臂或腿上使用的地方。

第11章, 使用Python编程编码器,演示了如何在代码中读取里程计/转速计轮,让你的机器人直线行驶、准确转向或记录行驶距离。本章还介绍了PID控制器。

第12章, 使用Python进行IMU编程,介绍了惯性测量单元IMU),一套用于测量温度、加速度、转向速度和磁场的传感器。本章还介绍了焊接和VPython的基础知识。

第13章, 机器人视觉 - 使用Pi摄像头和OpenCV,展示了如何从摄像头获取数据并使用计算机视觉根据机器人所看到的进行移动。本章还将处理后的视频流式传输到浏览器。

第14章, 在Python中使用摄像头进行循线,演示了如何使用树莓派摄像头实现循线行为。

第15章, 使用Mycroft进行机器人语音通信,构建了一个语音控制代理来连接你的机器人,让你可以通过语音来控制它并接收语音反馈。

第16章, 深入IMU,将我们在第12章使用Python进行IMU编程中学到的传感器结合起来,提供有关机器人方向的数据,构建响应指南针方向的行动。

第17章, 使用手机和Python控制机器人,从你的智能手机构建一个菜单系统和游戏风格的控制板,让你的机器人驾驶时可以看到机器人所看到的内容。

第18章将您的机器人编程技能提升到更高水平,探讨了更广泛的机器人世界,有哪些社区,如何与其他机器人构建者和制造商取得联系,潜在的发展领域,以及在哪里与机器人竞争。

第19章规划您的下一个机器人项目——整合所有内容,是本书的最后一章,我们将总结您在本书中看到的内容,同时鼓励您规划您下一个机器人的构建。

为了充分利用本书

在开始阅读本书之前,您需要使用文本编程语言进行过一些编程。我假设您对变量、条件语句、循环和函数有一定的了解。

您需要一个运行macOS、Linux或Windows的计算机,互联网连接和Wi-Fi。

在手动技能方面,我假设您可以使用螺丝刀,可以处理偶尔的繁琐操作,并且不会因为焊接东西的可能性而过于害怕。

代码示例已在Python 3和Raspbian Buster以及Picroft Buster Keaton上测试过。本书将向您展示在需要时如何安装这些软件。本书还将向您展示在需要时如何选择和寻找机器人部件。

Preface_Table.jpg

在购买机器人硬件之前,请阅读适当的章节,了解权衡和建议。

如果您使用的是本书的数字版,我们建议您自己输入代码或通过GitHub仓库(下一节中提供链接)访问代码。这样做将帮助您避免与代码复制粘贴相关的任何潜在错误。

阅读本书后,请加入Twitter上的#piwars社区,参与许多机器人讨论。

下载示例代码文件

您可以从www.packt.com的账户下载本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. www.packt.com登录或注册。

  2. 选择支持标签。

  3. 点击代码下载

  4. 搜索框中输入书的名称,并遵循屏幕上的说明。

下载文件后,请确保使用最新版本的软件解压缩或提取文件夹:

  • Windows的WinRAR/7-Zip

  • Mac的Zipeg/iZip/UnRarX

  • Linux的7-Zip/PeaZip

本书代码包也托管在GitHub上,网址为https://github.com/PacktPublishing/Learn-Robotics-Programming-Second-Edition。如果代码有更新,它将在现有的GitHub仓库中更新。

我们还有其他来自我们丰富图书和视频目录的代码包,可在https://github.com/PacktPublishing/找到。查看它们吧!

代码实战

本书的相关代码实战视频可在http://bit.ly/3bu5wHp查看。

下载彩色图像

我们还提供了一份包含本书中使用的截图/图表彩色图像的PDF文件。您可以从这里下载:http://www.packtpub.com/sites/default/files/downloads/9781839218804_ColorImages.pdf

使用的约定

本书使用了多种文本约定。

文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟URL、用户输入和Twitter昵称。以下是一个示例:“这将设置一个LED在led_number处为指定的color。”

代码块设置如下:

cyan_rgb = [int(c * 255) for c in cyan]

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

right_distance = self.robot.right_distance_sensor.distance
  # Display this
            self.display_state(left_distance, right_distance) 

任何命令行输入或输出都按照以下方式编写:

>>> r.leds.show()

粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“选择4其他USB麦克风并尝试声音测试。”

提示或重要注意事项

看起来是这样的。

联系我们

我们始终欢迎读者的反馈。

一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并通过mailto:customercare@packtpub.com与我们联系。

勘误:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现错误,我们将不胜感激,如果您能向我们报告此错误。请访问www.packtpub.com/support/errata,选择您的书,点击勘误提交表单链接,并输入详细信息。

盗版:如果您在互联网上以任何形式遇到我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过mailto:copyright@packt.com与我们联系,并提供材料的链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为本书做出贡献,请访问authors.packtpub.com

评论

请留下评论。一旦您阅读并使用过本书,为何不在您购买本书的网站上留下评论?潜在读者可以查看并使用您的客观意见来做出购买决定,Packt可以了解您对我们产品的看法,我们的作者也可以看到他们对本书的反馈。谢谢!

如需了解更多关于Packt的信息,请访问packt.com

第一章:第1节:基础知识 – 为机器人准备

在本节中,我们将通过实例了解什么是机器人,了解机器人中包含的内容,并为机器人实验准备树莓派。

本书这部分包含以下章节:

  • 第1章, 机器人简介

  • 第2章, 探索机器人构建模块 – 代码和电子学

  • 第3章, 探索树莓派

  • 第4章, 为机器人准备无头树莓派

  • 第5章, 使用Git和SD卡副本备份代码

第二章:第一章:机器人学简介

在整本书中,我们将构建一个机器人,并为其编写程序,使机器人表现出智能并能够做出决策。我们将编写代码来使用传感器观察机器人的周围环境,并构建包括视觉、语音识别和对话在内的现实世界高级主题的实例。

你将看到简单的构建技术,当与一点代码结合时,将产生一种感觉像某种宠物的机器。你还将看到如何在事情出错时对其进行调试,这很可能会发生。你将了解到如何让机器人有方法向你指示问题,以及选择你想要展示的行为。我们将将其连接到游戏手柄,赋予它语音控制,最后向你展示如何规划进一步的机器人构建。

在我们开始构建机器人之前,花点时间了解什么是机器人是值得的。我们可以探索一些机器人类型,以及将机器人与其他机器区分开来的基本原理。你会思考一下机器人和非机器人机器之间的界限在哪里,然后也许会通过一些模糊的事实稍微模糊这条界限。然后我们将查看在业余爱好者和业余机器人场景中构建的许多机器人。

在本章中,我们将讨论以下主题:

  • 机器人是什么意思?

  • 探索高级和令人印象深刻的机器人

  • 发现家中的机器人

  • 探索工业中的机器人

  • 竞技、教育和业余爱好机器人

机器人是什么意思?

机器人是一种基于传感器输入做出自主决定的机器。软件代理是一种自动处理输入并产生输出的程序。也许机器人最好被描述为具有传感器和运动输出的自主软件代理,或者它也可以被描述为在其上运行的软件的机电平台。无论如何,机器人都需要电子设备、机械部件和代码。

机器人这个词会让人联想到奇幻的科幻创作,拥有传奇般的力量和智能的装置。这些装置通常遵循人类的身体计划,使它们成为安卓,一种类似人类的机器人。它们通常被赋予个性,表现得像一个人,以一种简单的方式,天真无邪:

图片

图1.1 – 科幻与真实世界的机器人。所使用的图像来自公共领域的OpenClipArt库

机器人这个词来自科幻小说(也称为科幻)。这个词来源于捷克语中的“奴隶”一词,并在1921年卡雷尔·恰佩克的戏剧《罗素万能机器人》中首次使用。科幻作家艾萨克·阿西莫夫在探索智能机器人行为时创造了“机器人学”这个词。

我们家中和工业中的大多数真实机器人并非尖端且引人注目。大多数机器人并不站立在两条腿上,或者实际上根本没有任何腿。有些机器人是轮式的,而有些机器人虽然不能移动,但仍然有运动部件和传感器。

现代洗衣机、自动吸尘器、完全自调节的锅炉和空气采样风扇等机器人已经渗透到我们的家中,成为日常生活的一部分。它们并不具有威胁性,已经成为我们周围的其他机器之一。3D打印机、机器人臂和学习玩具则更有趣一些:

图1.2 – 简化和分解的机器人

在其核心,所有机器人都可以简化为输出,如电机,输入,如传感器,以及控制器,用于处理或运行代码。因此,一个基本的机器人可能看起来像这样:

  • 它有输入和传感器来测量和采样其环境的属性。

  • 它有如电机、灯光、声音、阀门或加热器等输出,以改变其环境。

  • 它使用其输入的数据来自主地做出关于如何控制其输出的决策。

现在,我们将继续探讨下一节中的高级机器人。

探索高级和令人印象深刻的机器人

现在你对机器人有了整体的了解,我将介绍一些代表最引人注目机器人的具体例子,以及它们的能力。除了火星机器人外,这些机器人制造商更倾向于选择人类和动物形态,因为它们具有适应性,与为工业用途设计的、旨在单一重复使用的机器人形成对比。

图1.3 展示了这些机器人与人类/动物之间的相似性:

图1.3 – 人类和类似动物的机器人选择。[图片来源:图片1:此图片可在https://commons.wikimedia.org/wiki/File:Cog,_1993-2004,view_2-MIT_Museum-DSC03737.JPG找到,属于公有领域;图片2:此图片可在https://commons.wikimedia.org/wiki/File:Honda_ASIMO(ver._2011)2011_Tokyo_Motor_Show.jpg找到,由Morio提供,根据CC BY-SA 3.0许可,在https://creativecommons.org/licenses/by-sa/3.0/deed.en下;图片3:此图片可在https://commons.wikimedia.org/wiki/File:Nao_Robot(Robocup_2016).jpg找到,属于公有领域;图片4:此图片可在https://commons.wikimedia.org/wiki/File:Atlas_from_boston_dynamics.jpg找到,由https://www.kansascity.com/news/business/technology/917xpi/picture62197987/ALTERNATES/FREE_640/atlas%20from%20boston%20dynamics提供,根据CC BY-SA 4.0许可,在https://creativecommons.org/licenses/by-sa/4.0/deed.en下;图片5:此图片可在https://commons.wikimedia.org/wiki/Commons:Licensing#Material_in_the_public_domain找到,属于公有领域]

这些机器人共有的特点是它们试图以下列方式模仿人类和动物:

  1. 机器人1来自麻省理工学院,名为Cog。Cog试图在运动和传感器方面实现类似人类的特性。

  2. 机器人2是本田ASIMO,它的行走和说话方式有点像人类。ASIMO的两个摄像头执行物体避障、手势和面部识别,并有一个激光测距传感器来感知地面。它使用红外传感器跟随地面上的标记。ASIMO接受英语和日语的语音命令。

  3. 机器人3是软银机器人的Nao机器人。这个可爱的58厘米高的机器人被设计成学习型玩具机器人,供用户编程。它有传感器来检测其运动,包括是否跌倒,以及超声波距离传感器来避免碰撞。Nao使用扬声器和麦克风进行语音处理。它有多个摄像头,可以执行与ASIMO类似的功能。

  4. 机器人4是波士顿动力学的Atlas。这个机器人在两条腿上跑得很快,动作看起来很自然。它有一个激光雷达LIDAR)阵列,它使用这个阵列来感知周围的环境,以便规划其移动并避免碰撞。

  5. 机器人5是波士顿动力学的BigDog,一个四足机器人。它可以行走和奔跑。它是最稳定的四足机器人之一,在被推、被挤和冰冻条件下仍能保持直立。

你将在你将要构建的机器人中添加一些这样的功能。我们将使用距离传感器来避免障碍物,使用超声波传感器的方式与Nao相同,并讨论像ASIMO那样的激光测距传感器。我们将探索用于视觉处理的摄像头,用于跟随地面标记的线传感器,以及用于处理语音命令的语音处理。我们将为摄像头构建一个像Cog头部那样的俯仰和倾斜机构。

火星漫游车

火星漫游车机器人的设计是为了在另一个星球上工作,如果它损坏了,没有机会进行人为干预。它们的设计本身就非常坚固。更新的软件只能通过远程链接发送到火星漫游车,因为将带有屏幕和键盘的人送上火星并不实际。火星漫游车设计上是无头

图1.4 – 美国宇航局好奇号火星车在格伦·埃蒂夫,火星(图片来源:NASA/JPL-Caltech/MSSS;https://mars.nasa.gov/resources/24670/curiosity-at-glen-etive/?site=msl)

火星漫游车依赖于轮子而不是腿,因为在一个使用轮子的机器人上稳定要比使用腿的机器人简单得多,而且出错的可能性更少。火星漫游车上的每个轮子都有自己的电机。轮子的排列旨在提供最大的抓地力和稳定性,以应对火星的崎岖地形和较低的引力。

好奇号火星车在火星上部署时,其敏感的摄像头是折叠起来的。着陆后,摄像头被展开并使用伺服电机定位。摄像头使用俯仰和倾斜机构进行指向。它需要尽可能多地摄入火星景观,并将视频和图片发送回NASA进行分析。

就像火星机器人一样,这本书中你要构建的机器人也使用由马达驱动的轮子。我们的机器人也是为了设计上无需键盘和鼠标而设计的,即无头设计。随着我们扩展机器人的功能,我们也会使用伺服电机来驱动俯仰和倾斜机构。

发现家中的机器人

许多机器人已经渗透到我们的家中。它们被忽视为机器人,因为乍一看,它们看起来很普通和日常。然而,它们比外表更复杂。

洗衣机

让我们从洗衣机开始。它在一些家庭中每天都会使用,有一连串的衣服要洗、甩干和烘干。但这是如何成为一个机器人的呢?

图1.5 – 洗衣机的组件

图1.5 展示了洗衣机作为框图的示例。其中有一个中央控制器连接到显示屏,并带有选择程序的控件。从控制器输出的线条是输出。进入控制器的连接是来自传感器的数据。从输出到传感器的虚线显示了现实世界中输出动作的闭环,这会导致传感器变化。这是反馈,是机器人技术中的一个基本概念。

洗衣机使用显示屏和按钮让用户选择设置并查看状态。按下启动按钮后,控制器检查门传感器,如果门是开着的,它会合理地拒绝启动。一旦门关闭并按下启动按钮,它将输出锁定门。之后,它使用加热器、阀门和泵将热水填充到滚筒中,使用传感器反馈来调节水位和温度。

每个过程都可以用一组像这样的语句来表示,这些语句同时填充滚筒并保持其加热:

start water pump
turn on the water heater
while water is not filled and water is not hot enough:
  if water filled then
    stop water pump
  if the water is hot enough then
    turn off heater
  else
    turn on the water heater

注意那里的else,以防水温略低于正确温度。然后洗衣机开始启动滚筒旋转序列 – 缓慢转动,快速旋转,感应速度以满足标准。它将排出滚筒中的水,将衣物甩干,释放门锁,并停止。

这台洗衣机在各个方面都是一个机器人。洗衣机有传感器和输出以影响其环境。处理允许它遵循程序,并使用带有反馈的传感器来达到并维持条件。洗衣机维修人员可能比我更像是一个机器人学家。

其他家用机器人

气压中央供暖锅炉有传感器、泵和阀门。锅炉使用反馈机制来维持房屋的温度、通过加热的水流、气流,并确保引火光保持点亮。锅炉是自动的,具有许多类似机器人的功能,但它固定不动,不能轻易地适应其他用途。同样,对于其他家用电器,如智能风扇和打印机,也是如此。

智能风扇使用传感器来检测室内温度、湿度和空气质量,然后通过风扇速度和加热元件输出。

家中的其他机器,例如微波炉,只有基于计时器的操作,它们不做出决策,并且过于简单,不能被视为机器人。

可能最明显的家用机器人是机器人吸尘器,如图1.6所示:

图片

图1.6 – 一款机器人吸尘器 – PicaBot(图片来源:Handitec [公有领域 - https://commons.wikimedia.org/wiki/File:PicaBot.jpg])

这款带轮子的移动机器人就像我们在这里将要构建的那样,但更漂亮。它们配备了传感器来检测墙壁、袋装水平以及障碍区域,并避免碰撞。它们最代表我们正在研究的机器人类型。这个机器人是自主的、移动的,并且可以被重新编程为不同的行为。

在我们构建机器人时,我们将探索如何使用其传感器来检测事物并对它们做出反应,形成我们在洗衣机中看到的相同反馈循环。

探索工业中的机器人

机器人常见的另一个地方是工业领域。最早的实用机器人被用于工厂,并且在那里已经存在很长时间了。

机器人臂

机器人臂的范围从用于翻蛋的微小精致的机器人到移动货柜的巨大怪物。机器人臂通常使用步进电机和伺服电机。我们将在本书中探讨用于俯仰和倾斜机制的伺服电机。大多数工业机器人臂(例如,ABB焊接机器人)遵循预定的移动模式,并且不具备任何决策能力。然而,对于更基于传感器和智能的系统,请看看图1.7中的Rethink Robotics的令人印象深刻的Baxter。Baxter是一款协作机器人,旨在与人类一起工作:

图片

图1.7 – Rethink Robotics的Baxter机器人(图片来源:© Xavier Caré / Wikimedia Commons [CC-BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0)])

许多机器人臂在旁边工作时是不安全的,可能会导致事故,需要在其周围设置笼子或警告标记。Baxter则不然;它可以感知人类,并在安全的情况下工作或暂停。在前面的图像中,这些传感器位于头部周围。手臂传感器和软关节还允许Baxter感知并反应碰撞。

Baxter具有培训和重复机制,以便工人能够将其适应任务。它使用传感器在训练或回放动作时检测关节位置。我们的机器人将使用编码器传感器来精确控制车轮运动。

仓库机器人

在工业中使用的另一种常见类型的机器人是那些在工厂地板或仓库中移动物品的机器人:

图片

图1.8 – 仓库机器人系统:TGWmechanics的Stingray系统 [CC BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0)],以及Mukeshhrs的Intellicart [公有领域]

图1.8中的图片1显示了用于在存储综合体中移动托盘的机器人起重机系统。它们接收在货架系统中移动货物的指令。

较小的物品搬运机器人,如图1.8 图片2中的Intellicart,使用线传感器,通过跟随地面上的线条、磁性地感应地面下的电线,或者通过跟随像ASIMO那样的标记信标来移动。我们的机器人将跟随这样的线条。这些跟随线条的手推车通常使用轮子,因为轮子易于维护,并能形成稳定的平台。

竞争性、教育性和业余爱好机器人

最有趣的机器人是由业余机器人建造者创造的。这是一个极具创新性的领域。

机器人技术始终在教育领域有立足之地,学术建造者使用它们作为学习和实验平台。许多商业项目也始于这个环境。大学机器人通常是团队合作,可以访问高科技设备来创建它们:

图1.9 – Kismet [Jared C Benedict CC BY-SA 2.5 https://creativecommons.org/licenses/by-sa/2.5] 和 OhBot [AndroidFountain [CC BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0)]]

Kismet (图1.9 图片1) 是在20世纪90年代末由麻省理工学院(MIT)创建的。许多业余爱好机器人都是从它衍生出来的。在当时,它具有开创性,使用电机驱动面部运动,模仿人类表情。OhBot,一个使用伺服电机的低价业余爱好者套件,基于Kismet。OhBot (图1.9 图片2) 通过连接Raspberry Pi,使用语音识别和摄像头处理来制作逼真的面部表情。

业余机器人技术 与开源软件/硬件社区紧密相连,利用GitHub (https://github.com) 等网站分享设计、代码,从而产生更多想法。业余爱好者可以通过互联网上可用的套件创建机器人,进行修改和添加。套件涵盖了广泛复杂度,从简单的三轮底盘到无人机套件和六足机器人。它们包含或不包含电子元件。套件的调查将在第6章,“构建机器人基础——轮子、电源和布线”中介绍。我使用六足机器人套件来构建SpiderBot (图1.10) 以探索行走动作:

图1.10 – 我制作的Spiderbot,基于套件。控制器是esp8266 + Adafruit 16伺服控制器

Skittlebot是我的Pi Wars 2018参赛作品,通过玩具黑客技术构建,将遥控挖掘机玩具改造成机器人平台。Pi Wars是一个基于Raspberry Pi的机器人自主挑战赛,包括手动和自主挑战。参赛作品中有装饰性外壳和富有创造性的工程。Skittlebot图1.11)使用三个距离传感器来避免墙壁,我们将在第8章,“使用Python编程距离传感器”中研究这种传感器。Skittlebot使用摄像头来寻找彩色物体,正如我们将在第13章,“机器人视觉 – 使用Pi摄像头和OpenCV”中看到的那样:

图片

图1.11 – Skittlebot – 我的PiWars 2018机器人,基于玩具

一些业余爱好者的机器人是从零开始构建的,使用3D打印、激光切割、真空成型、木工、数控和其他技术来构建底盘和部件:

图片

图1.12 – 构建ArmBot

我在2009年为伦敦机器人小组The Aurorans从头开始构建了图1.12中的机器人。2009年,这个机器人被称为EeeBot,因为它打算由Eee PC笔记本电脑驱动。The Aurorans是一个聚集讨论机器人的社区。后来,这个机器人被赋予了Raspberry Pi,一个机器人臂套件(uArm)似乎很合适,因此得名ArmBot

在当前市场上,有许多底盘套件,初学者不需要这样测量和切割材料来制作一个功能性的机器人。这些套件是为了实验而构建的,以及为了激发其他机器人构建者和孩子们的编码兴趣。在本书的结尾,我们将介绍一些正在构建和共享机器人的社区,以及如何使用构建技术从头开始制作它们的起点。

电视系列剧机器人战争是一个著名的机器人竞赛活动,以其令人印象深刻的构建和工程技能而闻名。然而,在机器人战争中没有自主行为;它们像遥控车一样手动驾驶。虽然洗衣机不如机器人战争那么刺激,但它们更智能,因此可以更严格地被认为是机器人。

摘要

在本章中,我们探讨了“机器人”一词的含义,以及与机器人相关的事实和虚构。我们定义了什么是真正的机器人。你已经看到了一台机器需要做什么才能被认为是机器人。

我们调查了家庭和工业中看到的机器人。你已经看到了一些旨在令人惊叹或前往其他星球的设计。我们还研究了业余爱好者和教育机器人,以及其中一些是如何仅仅为了乐趣而构建的。你已经看到了一些真实设备的框图,这些设备可能没有被考虑为机器人。你还注意到了我们的家中可能已经存在几个机器人。

我希望这一章能让你思考什么才能获得“机器人”的称号。洗衣机可以是完全自动的,在一段时间后启动,遵循一个程序,一些高级机器通过检测从衣物中流出的水质作为衡量它们清洁程度的指标来节水。然而,一个被称为机器人的机器,可能只是一个遥控设备,比如远程存在机器人或机器人战争机器人。毫无疑问,所有这些都需要复杂的工程,需要制造它们所需的大量相似技能。

虽然有些机器人很明显是机器人,比如本田ASIMO和Baxter,但有些其他机器更难划清界限。如果“决策型、机电机器”的广义概念适用于这些情况,那么它将排除遥控类型。如果“可移动的机器”的概念被应用,那么一个玩具遥控车将被包括在内,而一个完全自主的静止智能机器则被排除。一个机器可以通过具有拟人(类似人类)的特征来制作成看起来像机器人,但仅仅是通过机械运动,上下移动一个手臂——这是机器人吗?它并没有运行程序或对环境做出反应。

现在我们已经探讨了什么是机器人,接下来让我们进入下一章,我们将探讨如何规划一个机器人,以便我们可以构建它。

评估

在你的家中四处看看。你可能会发现其他带有许多机器人特征的自动机器。拿一个常见的家用机器(除了洗衣机),看看它的输入和输出。使用这些信息制作一个图表,展示它们进入或离开控制器的情况。思考一下如果它们在房子里移动,它们会如何移动。

进一步考虑这个系统中可能存在的反馈回路。它在监控什么?它是如何对那些信息做出反应的?

进一步阅读

参考以下链接:

第三章:第二章:探索机器人构建块 - 代码和电子

在本章中,我们将拆解一个机器人以查看其部件和系统。我们将探索机器人的组件,包括软件(代码、命令和库)和硬件,以及它们是如何组合在一起的。在开始制作机器人时,考虑你想要的部件以及它们之间的关系是非常有价值的。我建议绘制一个机器人计划——一个作为连接代码和部件指南的框图,我们将在本章中探讨这一点。

在本章中,我们将涵盖以下主题:

  • 查看机器人内部的情况

  • 探索机器人组件类型

  • 探索控制器和 I/O

  • 规划组件和代码结构

  • 规划物理机器人

技术要求

对于本章,你需要以下内容:

查看机器人内部的情况

我们可以从将机器人视为一个物理系统开始。在 图 2.1 中,我们可以看到一个简单的业余机器人:

图 2.1 - 组装好的业余机器人

图 2.2 显示了它的拆解形式:

图 2.2 - 业余机器人的组件布局

图 2.2 中的组件组包括九种类型的组件:

  1. 底盘或机身构成了机器人的主要结构;其他部件都附着在这里。

  2. 一个滚轮平衡了这个机器人。

  3. 两个驱动轮。其他机器人可能在这里使用更多的轮子或腿。

  4. 电机对于机器人移动是必不可少的。

  5. 一个电机控制器在控制器和连接的电机之间架起桥梁。

  6. 一个控制器,这里是一个 Raspberry Pi,运行指令,从传感器获取信息,并处理这些信息以通过电机控制器驱动输出,如电机。

  7. 所有机器人都必须有电源,通常是电池的一套或多套。

  8. 传感器提供有关机器人环境或其物理系统状态的信息。

  9. 最后,调试设备是允许机器人与人类沟通其代码正在做什么的输出,而且对于外观也很有用。

我们将在本章后面更详细地检查这些组件。

我们可以将机器人可视化为一个连接部件的框图(图 2.3)。框图使用简单的形状来展示事物可能连接的大致想法:

图 2.3 – 机器人框图

图 2.3 中的框图没有使用正式的符号。我创建的关键是从脑海中产生的,但它应该能够识别传感器、输出和控制器。它可能只是一个在废纸上的草图。关键因素是你可以在硬件中看到功能块,它们之间有高级的数据流。

您可以从这张图中制定更详细的计划,包括电气连接、电源需求、硬件以及所需空间的大小。绘制一个关于您想要创建的机器人的方块图是将其制作出来的第一步。

重要提示

方块图不是电路图,也不是完成机器人的比例图。它甚至不试图显示实际的电子连接。图片忽略了小细节,例如在超声波距离传感器响应之前如何发出信号。连接线给出了数据流的一般概念。方块图是显示电机和传感器的类型和数量的正确地方,以及它们可能需要的附加控制器。

这是对机器人组件的简要概述,我们看到一个与你将要构建的机器人相似的机器人,以及它被拆解成各个部分。我们查看了一个简单的机器人方块图及其意图。在下一节中,我们将更详细地查看机器人的每个组件,从电机开始。

探索机器人组件类型

在我们查看电机和传感器的类型之前,让我们简要了解它们各自是什么。

电机是一种在施加电力时旋转的输出设备。电机是称为执行器的一种机械的子集。它是一种从电能产生运动的输出设备。这种功率可以通过信号调节来控制运动。执行器的例子包括电磁阀、阀门和气缸。

传感器是一种向机器人提供输入的设备,使其能够感知其环境。传感器的类型比一本书能列出的要多,所以我们只关注常见且易于使用的那些。显示屏和指示器是调试输出设备,用于向人类用户/程序员提供有关机器人操作的反馈。本节将介绍其中的一些。

现在,让我们更详细地看看它们。

电机类型

机器人常用到的电机种类繁多。让我们看看每种电机的作用以及我们如何可能用于不同类型的运动:

重要提示

扭矩是一种旋转/扭转力,例如,电机为了转动轮子所需的力。如果扭矩增加,电机将需要更多的功率(即电流),在试图应对时会减慢速度。电机有一个极限,即堵转扭矩,此时它将停止移动。

图2.4 – 不同类型的电机 – 直流电机、直流齿轮电机、伺服电机和步进电机

为了确定每种电机的作用,让我们详细地看看它们:

  1. 直流电机:这是机器人中最简单的电机类型,是齿轮电机的基础。它使用直流电压,这意味着可以通过通过它的电压方向来简单地驱动它。电机的速度与通过它的电压成正比,与所需的扭矩成反比。像图2.4中的裸直流电机可能会转得太快而无法使用。它将没有多少扭矩,并且容易卡住。

  2. 直流齿轮电机:这是一种配备变速箱的直流电机。这个变速箱降低了速度并增加了它可以处理的扭矩。这种机械优势增加了电机移动负载的能力。请注意,这种齿轮电机缺少焊接的引线!我推荐这些电机类型用于机器人轮子。我们将在第6章第7章中使用这种类型的齿轮电机,构建机器人基础 – 轮子、电源和布线,以及驱动和转向 – 使用Python移动电机

  3. 伺服电机(或伺服机构):这种电机将齿轮电机、传感器和内置控制器结合在一起,如图2.5所示。控制器接收到的信号表示电机位置,控制器使用传感器的反馈来尝试达到这个位置。伺服电机用于云台机构、机械臂和肢体。我们将在第10章中更详细地研究并编程伺服电机,使用Python控制伺服电机

    图2.5 – 伺服电机机构的示意图

  4. 步进电机:这些电机通过按顺序给线圈供电,使电机以一定数量的度数步进。当需要精确运动时,工程师会使用步进电机。与直流电机或伺服电机相比,步进电机通常速度较慢,产生的热量较多。你会在精密控制应用中找到这些电机,例如3D打印机和高端机械臂。它们比其他电机更重、更贵。

  5. 无刷电机:这些在图中没有显示。它们由专用控制器驱动,可以达到高速和高扭矩。它们运行时更安静,在无人机中很受欢迎。没有齿轮电机等效物,因此可能需要创建变速箱。

    重要提示

    除了伺服电机外,所有电机都需要硬件来驱动,例如Raspberry Pi。这种硬件允许Pi在不破坏它们的情况下控制耗电设备。永远不要将直流电机、步进电机或电磁铁直接连接到Raspberry Pi!

接下来,让我们看看其他类型的执行器。

其他类型的执行器

线性执行器,如图2.6中所示,是将电信号转换为沿单轴运动的设备。这些可以是步进电机驱动固定封装中的螺钉,或者使用线圈和磁铁阵列:

图2.6 – 线性执行器:由Rollon91提供,[图片来源:https://commons.wikimedia.org/wiki/File:Uniline.jpg?uselang=fr [CC BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0)]]

电磁阀是一种简单的线性执行器,使用带金属芯的电磁线圈,当通电时会被拉或推。这种类型的常见用途在液压或气动阀门中。液压和气动系统产生像挖掘机中看到的那种强大的运动。

状态指示器 – 显示屏、灯光和声音

另一个有用的输出设备是显示屏。单个LED(一个小型电子灯)可以指示机器人某些部分的状态。LED阵列可以显示更多信息并增加颜色。图形显示屏可以显示一些文本或图片,就像在手机上看到的那样。我们将在第9章Python中的RGB条带编程中,将多彩LED条带连接到机器人作为显示屏。

扬声器和蜂鸣器可以通过发出声音使机器人与人类进行交流。这些设备的声音输出可以从简单的噪音到语音或播放音乐。

许多机器人没有显示屏,依赖于连接的手机或笔记本电脑来显示它们的状态。我们将在第17章使用手机和Python控制机器人中,使用手机来控制和查看我们机器人的状态。

传感器类型

图2.7展示了在机器人中使用的传感器类型集合。它们与我们将在本书中探索和使用的传感器类似。让我们检查一些它们的用途。请注意,这些可能看起来与之前看到的相同类型的传感器不同 – 做同样工作的传感器有很大的差异。当我们把它们添加到机器人中时,我们将更详细地介绍它们的变体:

图2.7 – 我机器人中的传感器选择:1 - 一款树莓派摄像头,2 - 一款光学距离传感器,3 - 一款超声波距离传感器,4 - 线性传感器,5 - 麦克风,6 - 一款光学中断传感器

让我们详细了解一下图2.7中的每个传感器:

  1. 树莓派摄像头模块:此模块连接到树莓派,为其提供成像能力。我们将在第13章机器人视觉 – 使用Pi摄像头和OpenCV中进行视觉处理编程。此模块可以捕获图像或视频序列。它可以快速生成大量数据,这是与机器人视觉相关的问题之一。它对光照条件敏感。

  2. 光学距离传感器:图2.7中的VL53L0X飞行时间激光测距传感器是一个距离传感器。它使用红外激光反射物体并检测它们有多远。它可能会受到光照条件的影响。

    VL53L0X 传感器使用 I2C 将检测到的距离发送到 Raspberry Pi,并且可以与许多其他设备共享它们的两个通信引脚。当您有许多传感器和输出,并且开始用完连接东西的地方时,I2C 非常有用。I2C 传感器可能是一个更昂贵的选项。

  3. 超声波距离传感器: HC-SR04 是另一种距离/测距传感器,它通过从物体上反弹声脉冲来检测距离。它受物体材质类型的影响,可能无法检测某些表面,但对光照条件不敏感。例如,一些表面,如织物,吸收声音过多,永远不会将其反射回来,而其他表面,如网格或网状物,与声波相互作用不多,对传感器来说是透明的。

    HC-SR04 需要在控制器中精确计时以计时回声,这我们需要在代码中管理。它的测距范围比 VL53L0X 激光传感器长,且价格更低,但在近距离时灵敏度较低。我们将在第 8 章,“使用 Python 编程距离传感器”中编程基于声音的测距传感器。

  4. 线传感器: 这些是一套三个线传感器,使用光来检测从光到暗的转变。它们可以调整以在不同的条件下感应。这些模块有几种变体。这些提供开或关的信号,取决于其下是光亮还是黑暗区域。它们是传感器中最简单的。

    麦克风: 第五个传感器是一对麦克风。这些可以直接连接到 Pi 的 PCM 引脚上。有些其他麦克风需要连接到电子设备上,以将它们的信号处理成 Raspberry Pi 可以使用的格式。我们将在第 15 章,“使用 Mycroft 与机器人进行语音通信”中使用麦克风进行语音处理。

  5. 光断路传感器: 这检测通过两个柱子之间缝隙的红外光,感知柱子之间是否有物体中断了光束。这些与带凹槽的轮子一起使用,通过计数凹槽来检测旋转和速度。当与轮子一起使用时,它们也被称为编码器。我们在第 11 章,“使用 Python 编程编码器”中使用编码器。

还有更多传感器,包括检测肢体位置、光线、烟雾、热源和磁场的传感器。这些可以用来制造更先进的机器人,并添加更多令人兴奋的行为。

我们已经涵盖了电机、显示屏、指示器和传感器,包括一些关于它们类型的示例和一些细节。这些是允许我们的机器人与世界交互的部件。现在我们将继续到控制器,这是机器人中运行代码并将传感器和电机连接在一起的部件。

探索控制器和 I/O

在机器人框图中心,如图图2.3所示,是控制器。机器人通常有一个主要控制器,某种类型的计算机。它们也可能有一些辅助控制器,而一些更不寻常的机器人有多个控制器。本书使事情保持简单,您的代码在传统的中央控制器上运行。控制器将所有其他部分连接在一起,并形成它们交互的基础。

在我们查看控制器之前,我们需要更好地理解一个重要的组件,它将控制器连接到其他组件,即I/O引脚。

I/O引脚

I/O引脚用于控制器输入和输出。它们赋予控制器连接到现实世界传感器和电机的功能。

控制器上的I/O引脚数量是限制您在不使用辅助控制器的情况下连接到机器人的因素之一。您也可能看到术语通用输入输出GPIO)。控制器I/O引脚具有不同的功能。

最简单的I/O引脚只能输出或读取开/关信号,如图图2.8所示。这些被称为数字I/O引脚。它们可以通过信号定时编程执行复杂任务。这是HC-SR04距离传感器所使用的原理。在图2.8中,此图表示电压随时间的变化。因此,当我们沿着x轴移动时,电压在y轴上。较高的水平代表数字逻辑高(1TrueOn)。较低的水平代表数字逻辑低(0FalseOff)。控制器将尝试将任何值解释为高或低:

图2.8 – 一个数字信号

模拟输入引脚可以读取不同的级别,如图2.9中的信号,这是一个电压随时间变化的图表。如果一个传感器产生变化的电阻或连续的值范围,那么模拟引脚就适用。这种情况下有一个分辨率限制,例如,8位模拟输入将读取256个可能值:

图2.9 – 一个模拟信号

脉冲宽度调制PWM)引脚输出一个循环的数字波形,如图图2.10所示。此图还显示了电压随时间的变化,尽管脉冲的定时代表了一个连续的水平,因此虚线显示了由定时产生的连续水平。PWM输出允许代码选择频率以及它们开启的时间。在一个周期中开启时间与关闭时间的长度变化以改变输出信号。这通常用于控制电机的速度:

图2.10 – 蓝色表示的PWM信号,虚线显示其近似值

我们将在第6章“构建机器人基础 – 轮子、电源和布线”和第7章“驱动和转向 – 使用Python移动电机”中花费更多时间讨论PWM引脚。

一些I/O引脚可以用来形成数据传输线,例如串行、I2S、I2C和SPI总线。它们被称为数据总线。数据总线用于向其他控制器和智能传感器发送或接收数据。我们将在第9章,“使用Python编程RGB条带”中使用SPI数据总线。

微控制器引脚可用于数字或模拟输入和输出,或数据总线的一部分。许多控制器允许在运行的软件中配置引脚的使用模式,但某些功能仅限于特定引脚。

控制器

虽然使用适当的技能和裸微控制器芯片创建周围电子设备和自己的PCB板是可能的,但为了简化本书的内容,我们将使用控制器模块。这些通常以包装和易于使用的系统形式出现:

图2.11 – 控制器模块的选择:树莓派、NodeMCU、Arduino和micro:bit

图2.11展示了我的几个最喜欢的控制器。它们都可以通过USB连接供电。除了树莓派,其他所有设备都可以通过USB编程。它们都有连接器,便于访问它们的I/O引脚。对于每个控制器,让我们看看它们是什么,以及它们的优缺点:

  1. 树莓派:它足够强大,可以进行视觉处理。它消耗的电量略多,价格也更高,但功能上类似于移动电话。它拥有最灵活的编程环境。有几种型号可供选择。它们有许多I/O引脚,但没有模拟输入引脚。

  2. NodeMCU:这是基于ESP8266控制器。这个控制器内置Wi-Fi,可以用Arduino C++、MicroPython或Lua编程。它有大量的I/O引脚,但只有一个可以读取模拟信号。它支持多种数据总线类型。它比Arduino快一些,可以存储更大的程序。它是这一系列中最便宜的控制器。

  3. Arduino Leonardo:这是基于Atmega 328芯片的。Arduino控制器模块构成了我2010-2012年左右大多数机器人的基础。Arduino因其能够通过USB轻松连接到PC并立即与连接到其I/O引脚的设备交互而变得非常重要。

    Arduino主要使用C++语言编程。它具有最灵活的内置I/O引脚——七个模拟引脚、许多数字引脚、PWM输出引脚,并且可以配置以处理大多数数据总线。Arduino的处理器非常简单;它无法执行视觉或语音处理任务。Arduino是这里展示的所有选项中功耗最低的。

  4. micro:bit:这款产品于 2015 年发布,用于教育目的,非常适合儿童。如果你需要比它随附的 3 个 I/O 引脚更多的引脚,那么在机器人中使用它需要额外的适配器,但仍然是一个相当强大的机器人控制器,并带有方便的内置 LED 矩阵。它可以使用 MicroPython、C、JavaScript 以及其他几种语言进行编程。

应该特别提一下未在此图中展示的 PIC 微控制器。这些在其他人之前很久就被用于业余机器人,并且有一个繁荣的社区。

下面是基于优缺点比较的控制器:

其他控制器可能运行简单的解释器或编译代码,而 Raspberry Pi 运行的是一个完整的操作系统。当前型号具有 Wi-Fi 和蓝牙功能,我们将使用这些功能使机器人无头并连接游戏控制器。

选择 Raspberry Pi

图 2.12 展示了一些当前的 Raspberry Pi 型号。随着新 Raspberry Pi 的发布,机器人制造商可能需要将其适应到最新版本。所有这些型号都具有 Wi-Fi 和蓝牙功能。Raspberry Pi 的 I/O 引脚支持许多数据总线类型和数字 I/O。对于模拟读取和一些其他 I/O 功能,需要外部控制器:

图 2.12 – Raspberry Pi 型号 – 4B、3A+ 和 Zero W

让我们逐一详细看看这些型号:

  1. Raspberry Pi 4B:这是在撰写本文时 Raspberry Pi 系列中的最新型号。作为最新型号,它是系列中最快、最强大的。它占用的空间更大,在这个群体中价格最高,并且消耗的功率最大。

  2. Raspberry Pi 3A+:这是我们为我们的机器人使用的控制器。它在尺寸和功率方面提供了极佳的折中方案。它能够通过摄像头进行视觉处理。它并不像 4B+ 那么快,但绝对足够我们使用。

  3. Raspberry Pi Zero W:这是其他 Raspberry Pi 型号的一个经济、轻便的替代品。摄像头和扬声器仍然得到支持。Zero WH 型号还包括 I/O 引脚。它的语音和视觉识别性能比 Raspberry Pi 3 和 4 慢。它们的小巧尺寸使它们也成为遥控板的有趣选择。

现在我们已经了解了每个型号,让我们比较一下它们的优缺点:

Raspberry Pi 4B 可能是最强大的,但 3A+ 的性能足够强大,可以响应这里所有的活动。

规划组件和代码结构

你现在已经简要地看到了一些你可能用于机器人的组件,并且你已经遇到了一个将这些组件组合起来的框图。这就是你开始采取下一步并进一步思考如何连接这些组件,以及你为它们编写的代码将如何结构化的地方。

当将代码视为逻辑块而不是一个大块时,推理起来更容易。以类似于硬件功能图的方式安排代码将有助于你在代码变得复杂时找到自己的路径。

因此,让我们回到图2.3中的机器人框图,来思考我们需要在代码中处理什么。该图有三个传感器和两个输出。每个组件(传感器、输出和控制板)可能需要一些代码来处理它,然后你需要一些代码来处理组合模块的行为。

电机控制器有多种类型。它们有不同的方式输出到电机,并且可能具有对电池电量的监控。一些智能电机控制器可以直接与轮编码器接口,以确保车轮已行驶了指定的距离。当我们为机器人编写行为时,如果我们更改电机控制器,可能不想重新编写它。将直接电机控制器代码与行为代码混合也会使推理变得更加困难。为此,我建议创建一个接口层,一个在真实电机控制器代码和标准接口之间的抽象层,这将使组件交换成为可能。我们将在第7章中看到这一点,使用Python驱动和转向 – 移动电机

这对每个传感器都类似。它们将有一些代码来管理如何获取信号并将它们转换为可用的数据。所有这些设备都可能有一些设置和拆卸代码,需要在启动或停止连接到它们的操作时运行。相机是这种需求的复杂示例,需要处理以获取我们可以用来执行任务的数据值:

图片

图2.13 – 一张快速的手绘软件框图,用笔在信封上绘制的,以及使用计算机绘制的相同图

就像硬件一样,一个简单的图可以表示软件。这可以在绘图程序中完成,或者在任何你手头的纸张上绘制。在图2.13中,我故意选择了一个手绘的版本,这样你就不会觉得你需要一个绘图工具来做这件事。这不会很整洁,但它可以快速重绘,甚至可以在外出就餐时在收据背面绘制。这里相关的事实是,如果你用铅笔,用钢笔或细线重新描一遍,这样它就不会褪色。为了使读者更容易理解,我还制作了一个计算机绘图,但你不必要这样做。

小贴士

扫描你的手绘文档。如果你有扫描仪,或者只是一部手机,我建议扫描或拍照你的草图,以供以后参考。将它们作为图像/PDF文件放入Evernote或OneNote等软件中,并添加有用的标签,这样你就可以快速查找它们。

在绘制手绘草图后,你可以使用软件工具。这会比手绘版本花费更长的时间,并且尽量不要被工具的怪癖和风格所分心。

在设计本身方面,这仍然是一个非常简化的视角。轮子框将是一块代码,处理请求轮子电机控制器执行事情。这可能位于电机控制器公司编写的代码之上,或者使用连接到控制器的I/O引脚。

距离传感器是代码块,用于从传感器读取距离,并在必要时触发它们。我们将查看两种不同的传感器并进行比较。通过在这个级别更改传感器,意味着其他代码不需要更改。

还有一个用于摄像头的代码块,执行诸如设置、分辨率、白平衡和其他我们将要讨论的部分。在这之上是一个将使用摄像头图像的层。该层可以获取彩色物体的位置,并将此位置返回到上层。

在电机和距离传感器之间有一个行为层,允许机器人避免碰撞,例如当它在一边低于某个阈值时。这将覆盖其他行为,使机器人避开那个障碍物并稍微驶离。

最顶层是另一个行为,它从获取对象位置代码中获取位置数据。使用这个位置来选择一个方向,然后指示电机驱动到该对象。如果这个行为通过了避免碰撞行为,可能会出现复杂的交互,导致机器人寻找正确的对象,同时避开障碍物并绕过物体。它也不会靠近检测到的对象,以免与之相撞。

每个模块相对简单,可能底层靠近硬件的部分更复杂,尤其是在摄像头的情况下。

将代码分解成这样的块意味着你可以一次处理一个块,测试和调整其行为,然后再专注于另一个。当你写出了这样的块时,你可以重用它们。你可能会需要多次使用电机代码,现在你不需要多次编写它。

使用这些块来描述我们的软件,让我们以不同的方式实现这些块及其交互。我们可以考虑是否将使用函数、类或服务来处理这些块。随着我们开始编写代码并展示不同的方法,我将花更多的时间在这上面。

规划物理机器人

现在我们将所有这些应用到实际中,并规划我们在这本书中制作的机器人物理部件的布局。当我们翻阅章节时,我们每次都会添加新的组件,并且在我们前进的过程中保持一个整体地图在我们的脑海中,这有助于我们了解自己的位置。想象机器人将要执行的所有事情是非常令人兴奋的。让我们从列出我们的机器人将要做什么和成为什么开始:

  • 它将配备轮子,能够在地面上行驶。

  • 它将配备一个Raspberry Pi 3A+控制器。

  • 它将有一个用于轮子的电机控制器。

  • 它将能够通过一组多色LED指示其状态。

  • 机器人将使用一对伺服电机来实现俯仰和倾斜机构。

  • 它将能够通过超声波或激光距离传感器避开墙壁并绕过障碍物。

  • 它将为每个轮子配备一个编码器,以了解它移动了多远。

  • 机器人将使用摄像头来感知彩色物体或人脸。

  • 它将能够通过摄像头跟随线条。

  • 机器人将配备麦克风和扬声器以处理语音命令。

  • 它将配备游戏手柄作为遥控器。

  • 它将需要为所有这些事物提供电力。

呼吁!这有很多功能。现在,我们需要绘制硬件模块。图2.14显示了我们的模块图。虽然是用Draw.io完成的,但一个简单的模块图草图是机器人规划的一个很好的开始。我的大多数机器人都是从这种方式开始的:

图片

图2.14 – 使用draw.io网络应用创建的我们将构建的机器人的模块图

虽然这看起来像是一大堆机器人,但我们将在每一章中关注一个功能区域,并在转移到其他区域之前构建它。这里的注释并不是任何正式的符号,它只是可视化所有需要连接的部件的一种方式。与此相关,我通常大致绘制出我将物理放置传感器和部件的位置:

图片

图2.15 – 使用Draw.io创建的机器人可能物理布局概述

图2.15中的草图并不详尽、准确或按比例,但它只是我想让部件最终到达位置的初步想法。注意以下图中的事项:

  • 传感器有一个清晰的视野,距离传感器指向两侧。我将在相关的传感器章节中详细介绍为什么这很重要。

  • 编码器被放置在轮子上,它们将在那里使用。

  • 重物,特别是电池,应保持低位(低于重心)以避免机器人翻倒。

  • 电池需要更换,因此要考虑它们的可访问性。

  • 尽量保持直接连接的组件相当接近。

  • 这是一个粗略的计划。它不需要这么详细,这也不是测试装配。实际尺寸、设计妥协和问题将意味着这会发生变化。这只是一个起点。

随着我们通过本书,我们将查看这些图中的细节,并开始完善真正的机器人,使一些内容不那么模糊。任何这样的图,在项目开始时,都应该被视为相当粗糙的。它不是按比例的,不应该盲目跟随。它是一个指南,或是一个快速地图,从这里开始工作。

摘要

在本章中,你已经能够看到组成机器人的多种不同组件,并通过一个作为计划的框图,开始可视化如何将这些模块组合成一个完整的机器人。你已经看到如何快速在信封上勾勒出你的机器人想法,以及电脑上的绘图工具可以用来制作更整洁的相同图示。你已经快速浏览了电机、传感器和控制器的相关知识,以及控制器与其他连接设备通信的几种方式,包括模拟、数字、PWM和数据总线。在此基础上,你已经看到了我们将在本书中构建的机器人的计划。

在下一章中,我们将探讨我们机器人中使用的Raspbian操作系统,并开始对其进行配置。

练习

  1. 尝试为不同的机器人创建一个框图,考虑输入、输出和控制器的因素。

  2. Raspberry Pi 4B和3A+是否仍然是最新版本?你会使用其他型号吗?会有哪些权衡?

  3. 激光测距传感器与超声波测距传感器相比有哪些缺点?

  4. 尝试为不同类型的机器人绘制一个大致的物理布局图,并使用不同的控制器。

进一步阅读

  • 《Raspberry Pi 传感器》鲁希·加贾尔Packt Publishing:学习如何将传感器集成到你的Raspberry Pi项目中,并让你的强大微控制器与物理世界互动。

  • 《Make 传感器:使用Arduino和Raspberry Pi监控真实世界的动手入门》特罗·卡尔维宁基莫·卡尔维宁维莱·瓦尔特卡里Maker Media, Inc.:学习如何使用传感器将Raspberry Pi或Arduino控制器与真实世界连接起来。

  • 《Make: 电子元件:通过探索学习》查尔斯·普拉特Make Community, LLC:如果你想要了解更多关于电子元件的信息,并深入探究各个单独的元件,这是一个有用的资源。

第四章:第3章: 探索树莓派

在上一章中,我们看到了树莓派在机器人拆解中的应用。因此,我们将使用树莓派来构建机器人,这并不令人惊讶。

在本章中,我们将使用树莓派3A+作为控制器。我们将检查这一选择的各种选项,并查看树莓派上的连接等特性,以及我们将如何使用它们来理解我们的决策。然后,我们将继续探索树莓派操作系统,并以准备操作系统在树莓派上使用而结束。

本章将涵盖以下主题:

  • 探索树莓派的功能

  • 选择连接

  • 什么是树莓派操作系统?

  • 准备带有树莓派操作系统的SD卡

技术要求

对于本章,你需要以下设备:

  • 存储容量为16 GB或更多的Micro SD卡

  • 树莓派3A+

  • 一台连接到互联网、能够读取/写入SD卡的Windows、Linux或macOS计算机或笔记本电脑

查看以下视频,以查看代码的实际运行情况:https://bit.ly/3bBJQt9

探索树莓派的功能

正如我们在第2章中看到的,《探索机器人构建模块 – 代码与电子学》,机器人使用的控制器可能是你做出的最关键的选择之一。这将决定你将有什么样的输入和输出,你的电子设备将有什么样的电源需求,你将能够使用哪些类型的传感器,以及你将运行什么代码。更换控制器可能意味着重写代码,重新设计控制器将放置的位置,以及改变电源需求。

树莓派是一系列专为教育用途设计的小型计算机。它具有I/O引脚,可以连接到定制硬件,同时作为一个完整的计算机,这使得它成为爱好者的最爱(一个指喜欢为爱好制作事物的人的术语,如机器人和小工具)。这得益于与标准计算设备相比,微控制器的相对便宜的价格和较小的尺寸。所有树莓派型号都具有连接摄像头、显示屏和键盘以及某种网络连接的能力。

速度和功率

树莓派足够强大,可以处理一些视觉处理任务,如面部识别和跟踪物体,较晚的型号能够更快地执行这些任务。同样,这也适用于语音识别任务。正因为如此,建议使用较快的4B、3B+和3A+型号。Zero和Zero W型号要慢得多,尽管系统仍然可以工作,但速度可能会令人沮丧。

Raspberry Pi 是一个 单板计算机SBC),其功能强大到足以运行完整的电脑操作系统,特别是 Linux 的版本。我们将在 什么是 Raspberry Pi OS 部分中探讨这一点,但这也允许我们使用 Python 通过其他人员维护良好的库和工具来进行视觉处理和语音处理。微控制器,如 ArduinoEsp8266micro:bit,根本无法执行这些任务。

一些可用的替代单板计算机(SBC)可以作为控制器运行 Linux,例如 BeagleBoneCHIPOnionIoTGumstix Linux 计算机,但这些设备要么比 Raspberry Pi 贵,要么功能较弱。只有一些配备了摄像头集成。尽管 BeagleBone 拥有优越的模拟 I/O 连接性,但 Raspberry Pi 3A+ 更像一个全能选手,并且有许多扩展选项。

连接性和网络

Raspberry Pi 3A+ 还配备了 USB 端口和 HDMI 端口。我们计划在这本书中不使用它们,尽管在出现问题时它们对于调试和与机器人失去联系时很有用。考虑到这一点,建议准备一个额外的屏幕和键盘。

Raspberry Pi 4、3 和 Zero W 系列都内置了 Wi-Fi 和蓝牙。在这本书中,我们将使用 Wi-Fi 连接到机器人,因此我们建议选择具有此功能的型号。Wi-Fi 可以用来编程机器人、驱动它以及在其上启动代码。

Raspberry Pi 拥有 I/O 引脚,允许您将其连接到传感器。在 Raspberry Pi 3A+ 中,通用输入/输出GPIO)连接已经准备好使用,因为引脚(称为引脚)已经预先焊接到位。Raspberry Pi Zero 和 Zero W 型号没有附带引脚。第一代 Raspberry Pi 板也配备了不同的 I/O 连接器。这些原因使得 3 和 4 系列的 Raspberry Pi 成为最佳选择。

选择 Raspberry Pi 3A+

将所有这些功能整合在一起,Raspberry Pi 3A+ 就是一个完整的电脑。以下列出的特性满足了我们的所有需求:

  • I/O

  • 一个用于摄像头的连接器

  • 能够进行视觉和语音处理

  • 内置 Wi-Fi 和蓝牙

  • 运行 Python 代码

  • 预焊接的引脚,便于连接到机器人设备

  • 小巧且相对便宜

此外,3A+ 配备了运行在 1.4 GHz 的四核 ARM 架构 CPU,这对于我们的使用场景来说已经足够。后续的 Raspberry Pi 版本可能会通过更快的处理速度和额外的功能来超越这个型号。

选择连接

在构建机器人时,我们将使用 Raspberry Pi 提供的连接子集。让我们看看这些连接是什么以及我们将如何使用它们。当我们将传感器和部件连接到 Raspberry Pi 时,我们将详细介绍这些连接,所以现在不需要记住这些。然而,以下引脚图可以作为这些连接的参考。

图 3.1 中,突出显示的区域显示了正在使用的连接:

图片

图3.1 – Raspberry Pi连接

首先,我们将使用标记为Power In并位于图底部左边的电源连接器。它通过一个类似于许多手机的micro-USB连接器连接。我们在学习无头操作时将使用它,这是为机器人供电的一种选择。如果USB电池组可以提供正确的功率,我们可以将其插入此端口。Raspberry Pi建议使用2.5 A的电源供应器,尽管通常2 A以上的电源就足够了。

下方的突出显示的端口是相机(Camera Serial InterfaceCSI))端口;这是用于Pi相机的,我们在准备进行视觉处理时将连接它。

我们将使用位于Raspberry Pi底部的Micro SD卡插槽来运行我们的代码。我们不会使用以太网或HDMI,因为我们将通过Wi-Fi与Raspberry Pi通信。图3.1顶部的较大连接器是GPIO端口:

图片

图3.2 – Raspberry Pi GPIO端口(B+、2、3、3B+、Zero和Zero W)

图3.2显示了GPIO端口的特写,其中包含一些引脚的名称和用途。这是我们连接大多数传感器和电机的地方。外部设备可以连接到SPII2C串行I2S数据总线,或者连接到数字I/O引脚。

电源引脚

5 V和3.3 V引脚用于供电,以及标记为GND的引脚。GND是ground的缩写,相当于电池或电源的负极。5 V引脚可以用来从电池为Pi供电。5 V和3.3 V可以用来为小型电子设备或传感器供电。

数据总线

SPI、I2C和串行用于在控制器和智能设备之间发送控制和传感器数据。I2S用于将编码的数字音频信号(PCM)从Raspberry Pi发送和接收。可以通过配置启用这些数据总线的端口,或者当数据总线关闭时,可以使用引脚作为通用数字引脚。

标记为SDASCL的引脚是I2C数据总线。我们使用这个总线来连接传感器和电机控制板。指令通过这个端口发送。

91011号引脚组成了SPI端口,我们使用它来驱动RGB LED。

虽然Raspberry Pi上有一个音频端口,但这并不适合驱动扬声器,因此我们将使用GPIO端口上的I2S引脚来完成这项工作。I2S引脚是18192021。因为它们也有音频输入引脚,所以我们使用这个引脚进行语音处理。

通用I/O

其他编号但没有特定文字或阴影类型的引脚是通用I/O引脚。通用I/O引脚用于数字输入和输出,与伺服电机、编码器和超声波传感器一起使用。

重要提示

为什么数字会混合?大多数Raspberry Pi文档中使用的数字是BCM编号,它们对应于主Broadcom芯片上的引脚。使用图3.2进行参考。

Raspberry Pi HATs

树莓派HAT(也称为帽子)是为插入GPIO引脚而设计的电路板,可以方便地将树莓派连接到电机或传感器等设备。

一些板子带有GPIO引脚,以便其他板子/连接可以使用它们,而其他一些则需要扩展板才能访问引脚。

HAT使用GPIO引脚用于不同的目的;例如,音频HAT将使用I2S引脚进行音频接口,但一些电机控制器HAT将使用相同的引脚来控制电机。使用这些HAT可能会出现问题,因此在使用多个HAT或特定总线时请注意这一点。我们将在第6章“构建机器人基础 - 轮子、电源和布线”中进一步探讨这个问题,当我们选择电机控制器时。

什么是树莓派操作系统?

树莓派操作系统是我们用来驱动树莓派的软件选择,我们的代码将在其中运行。它是树莓派基金会的官方操作系统,并附带了一些软件,旨在使使用树莓派变得更加容易。树莓派操作系统可以支持完整的桌面或最小化命令行和网络仅系统。

树莓派操作系统基于Debian Linux发行版。Debian是一组软件集合,旨在一起运行,提供了许多功能性和许多可能性。像这样的Linux发行版是许多互联网服务器、移动电话和其他设备的基础。操作系统的软件针对树莓派硬件进行了优化,特别是内核和驱动程序,它们是专门为其制作的。它还有一些方便的方式来配置树莓派用户可能需要的专用功能。

我们将以比桌面更简洁的方式使用它,放弃键盘、鼠标和显示器支持。这种最小化版本被称为树莓派操作系统轻量版,因为它在不需要桌面软件时下载量更小,并且在micro SD卡上占用的空间也更少。不运行窗口管理器可以释放内存并减少树莓派的处理能力,使其可用于视觉处理等活动。我们将使用我们将用于编程机器人的软件和工具扩展树莓派操作系统轻量版。

在阅读本书的过程中,你将主要通过代码和命令行与机器人交互。Linux和树莓派操作系统都是考虑到通过网络使用命令行编写的,这对于机器人编程的无头特性来说是一个很好的匹配。

我们使用Linux对Python编程语言的强大支持以及Linux提供的网络工具。树莓派操作系统在树莓派社区中广泛使用,并且在需要帮助时找到答案最容易。它不是Pi的唯一操作系统,但它是初学者在树莓派上最有用的选择之一。

使用树莓派操作系统准备SD卡

要在树莓派上使用树莓派操作系统,你需要将软件以某种方式放入micro SD卡中,这样树莓派就可以加载它。

树莓派创建了树莓派图像器,以便将软件安装到SD卡上。让我们下载它,并将正确的图像放到我们的卡上:

  1. 访问树莓派软件下载页面 raspberrypi.org/software,并选择如以下截图所示的 按钮下载你的电脑版本:img/B15660_03_03.jpg

    图3.3 – 下载树莓派图像器

    图3.3 展示了这将看起来是什么样子;它应该会突出显示你电脑的正确下载按钮。[img/B15660_03_03.jpg]

  2. 使用树莓派的使用说明进行安装。

  3. 将你的微SD卡插入笔记本电脑的正确端口。你可能需要一个适配器。

  4. 启动图像器。我们将从这里开始选择操作系统。选择 选择操作系统 按钮:img/B15660_03_04.jpg

    图3.4 – 选择操作系统按钮

    图3.4 展示了位于图像屏幕右下角的 选择操作系统 按钮。

  5. 当你选择此按钮时,它将弹出一个列表,显示可以闪存到卡上的操作系统:img/B15660_03_05.jpg

    图3.5 – 操作系统列表

    图3.5 展示了图像提供的操作系统列表。选择 树莓派操作系统(其他)

  6. 在其他菜单下,还有更多树莓派操作系统版本的选项:img/B15660_03_06.jpg

    图3.6 – 树莓派操作系统选择屏幕

    由于我们试图保持最小化,请从菜单中选择 树莓派操作系统轻量版(32位)

  7. 你现在应该点击 选择SD卡img/B15660_03_07.jpg

    图3.7 – 选择SD卡

  8. 这将弹出一个SD卡列表,应该显示你正在使用的卡:img/B15660_03_08.jpg

    图3.8 – SD卡选择

    选择此选项以继续。

  9. 你现在可以写入这个了。点击 写入 按钮:img/B15660_03_09.jpg

    图3.9 – 写入按钮

  10. 它会在这里询问你是否确定;点击 以继续。下载和写入图像将需要一些时间。

你可以将它加载到带有屏幕和键盘的树莓派上,但在我们能够使用这个树莓派作为机器人之前,我们将在下一章中对你的电脑上的SD卡进行更改。

摘要

在本章中,你看到了更多关于树莓派是什么,以及我们将使用树莓派的哪些连接。

我们已经了解了树莓派操作系统,它是从Linux派生出来的,如何下载它,以及如何将此软件安装到用于树莓派的微SD卡上。

在下一章中,我们将使这张卡无头,这样我们就不需要屏幕、键盘或鼠标来使用这个树莓派,并从我们的电脑上访问它。

评估

  1. 我推荐使用树莓派3A+。可能存在一些未被考虑的树莓派新模型。它们的权衡是什么?考虑成本、尺寸、功耗和计算速度。

  2. 尝试其他树莓派操作系统或树莓派发行版;一些可能需要键盘和鼠标。在继续阅读本书之前,请确保返回到树莓派操作系统轻量版。

  3. 我已经提到了相机(CSI)连接器、电源和GPIO端口。看看Raspberry Pi上的其他端口,也许你能看到它们能用来做什么。

进一步阅读

参考以下链接:

  • Raspberry Pi基金会关于安装Raspberry Pi操作系统的指南:https://www.raspberrypi.org/documentation/installation/installing-images/README.md

  • Raspberry Pi By Example》,作者Ashwin PajankarArush Kakkar,由Packt Publishing出版,其中包含关于Raspberry Pi替代操作系统的章节,以及许多令人兴奋的Raspberry Pi项目。

  • Raspberry Pi GPIO引脚图(https://pinout.xyz/):这描述了不同电路板如何根据它们实际使用的引脚连接到Raspberry Pi。了解大多数电路板只使用这些引脚的子集是有用的。

第五章:第4章:为机器人准备无头Raspberry Pi

在本章中,你将了解为什么机器人上的Raspberry Pi控制器应该是无线和无头(headless)的,无头意味着什么,以及为什么它在机器人技术中很有用。你将看到如何直接将Raspberry Pi设置为无头设备,以及如何在网络上一旦连接到这个Raspberry Pi,就发送你的第一条指令给它。到本章结束时,你将拥有自己的现成可用的Raspberry Pi,无需连接屏幕、键盘或有线网络,因此它可以作为机器人的移动设备。

本章我们将涵盖以下主题:

  • 什么是无头系统,为什么它在机器人中很有用?

  • 在Raspberry Pi上设置Wi-Fi并启用SSH

  • 在网络上找到你的Raspberry Pi

  • 使用PuTTY或SSH连接到你的Raspberry Pi

  • 配置Raspberry Pi OS

技术要求

要完成本章的练习,你需要以下内容:

  • 一个Raspberry Pi,最好是3A+(但Pi 3或4也可以)

  • 具备2.1安培输出能力的USB电源和Micro-USB线

  • 你在上一章中准备好的MicroSD卡

  • 一台连接到互联网并能够读写SD卡的Windows、Linux或macOS计算机

  • 计算机上的文本编辑器 – VS Code是一个合适的跨平台选项

  • Windows上的PuTTY软件(SSH软件已在Mac和Linux桌面上可用)

代码的GitHub链接如下:

https://github.com/PacktPublishing/Learn-Robotics-Programming-Second-Edition/tree/master/chapter4

查看以下视频以查看代码的实际应用:https://bit.ly/3bErI1I

什么是无头系统,为什么它在机器人中很有用?

一个无头系统是一种设计为通过网络从另一台计算机操作计算机的系统,在键盘、屏幕和鼠标访问设备不方便的时间和地点。无头访问用于服务器系统、构建机器人和制作小工具:

图4.1 – 连接到屏幕、键盘和鼠标的Raspberry Pi

图4.1显示了一个有头的系统,用户可以坐在设备前面。你需要将屏幕、键盘和鼠标连接到你的机器人上,因此它不太灵活。你可能可以根据需要连接/断开它们,但这也很不方便。有便携式系统设计用来与像这样的Raspberry Pi对接,但当机器人移动时,你需要断开连接或与机器人一起移动。

在某些活动中,我见过带有微型车载屏幕的机器人,由无线键盘和鼠标控制。然而,在这本书中,我们使用一个作为无头设备的机器人:

图4.2 – 以无头配置在机器人上的Raspberry Pi

图4.2中的树莓派作为无头设备安装在一个机器人上。这个树莓派没有被屏幕和键盘拖累;这些由另一台计算机处理。代码、指令和信息通过笔记本电脑的无线网络发送到树莓派。许多代码示例可以自主运行,计算机可以启动/停止这些操作。我们在第9章中添加了指示LED,用Python编程RGB条带。我们还在第17章中向你展示了如何使用手机控制机器人,用手机和Python控制机器人。手机可以启动和停止自主行为,查看机器人的状态,或者无需连接笔记本电脑就可以直接驾驶它。这个树莓派摆脱了屏幕和键盘的束缚。

提示

虽然你通常不需要屏幕和键盘,但如果你与树莓派失去联系,并且它拒绝通过网络响应,那么拥有这些设备是值得的。然后你可以使用屏幕和键盘连接到它,查看发生了什么。

为了无头访问树莓派,我们将使用安全外壳SSH)。SSH为你提供了一个命令行来向Pi发送指令,以及一个文件传输系统来将文件上传到它。

将树莓派设置为无头使其可以自由移动。它通过不需要携带或为屏幕和键盘供电来减轻机器人的重量。无头使机器人更小,因为显示器和键盘体积庞大。它还鼓励你,作为制造商,考虑自主行为,因为你不能总是向机器人输入命令。

在树莓派上设置Wi-Fi并启用SSH

现在你已经看到了无头系统可以得到什么,让我们修改SD卡,以便树莓派启动时可以作为无头设备使用。我们首先需要设置Wi-Fi:

  1. 将我们之前制作的MicroSD卡从计算机中取出并重新插入,以便计算机可以识别驱动器的新状态。

  2. 现在你将看到这张卡显示为两个磁盘驱动器。其中一个驱动器被称为boot;当Windows询问你时,请点击取消。SD卡的这个部分包含一个Windows无法读取的特定于Linux的文件系统。

  3. 现在,在boot中创建以下两个文件。我建议使用VSCode等编辑器来处理纯文本文件,查看文件扩展名,并创建空文件:

    • ssh:一个没有扩展名的空文件。

    • wpa_supplicant.conf:此文件包含你的Wi-Fi网络配置,如下所示:

      wpa_supplicant.conf file on your computer to use on other Raspberry Pi SD cards.Important noteThe `ssh` file must have no extension. It must not be `ssh.txt` or some other variation.
      
  4. 弹出MicroSD卡。请记住使用菜单来这样做。这确保在移除之前文件已经完全写入。

  5. 现在,有了这两个文件,你可以使用MicroSD卡来启动树莓派。将MicroSD卡插入树莓派底部的插槽中。它只能以正确的方向插入插槽。

  6. 最后,将 Micro-USB 线缆插入树莓派的侧面并连接到电源。你应该会看到灯光闪烁,表明它正在启动。

    重要提示

    对于树莓派,你需要一个至少提供 2.1 安培电流的电源。

你的树莓派无头系统现在已经设置好了。有了这个,我们朝着移动化迈出了重要的一步。现在我们需要在我们的网络上找到它,以便我们可以连接到它。

在网络上找到你的 Pi

假设你的 SSID 和 PSK 是正确的,你的树莓派现在已经在你的 Wi-Fi 网络上注册。然而,现在你需要找到它。树莓派使用动态地址(DHCP)。每次你将其连接到网络时,它可能都会得到一个不同的地址。访问你的 Wi-Fi 路由器的管理页面并记下 IP 地址在短期内是有效的。每次地址更改时都这样做会令人沮丧,并且在某些情况下可能不可用。

幸运的是,树莓派使用了一种名为 raspberrypi.local 的技术,树莓派会响应地址以找到它。这也被称为 Zeroconf 和 Bonjour。因此,你需要做的第一件事是确保你的电脑可以做到这一点。

如果你使用的是 macOS,你的电脑已经运行了 Bonjour 软件,它已经具备 mDNS 功能。此外,Ubuntu 和 Fedora 桌面版本已经很长时间具备 mDNS 兼容性。在其他 Linux 桌面系统中,你需要查找它们的 Zeroconf 或 Avahi 指令。许多最新的系统默认已经启用此功能。

但如果你使用的是 Windows,你需要 Bonjour 软件。所以让我们看看如何设置它。

为 Microsoft Windows 设置 Bonjour

如果你安装了较新的 Skype 或 iTunes 版本,你将拥有此软件。你可以使用此指南检查它是否已经存在并启用它:https://smallbusiness.chron.com/enable-bonjour-65245.html

你可以使用以下命令在命令提示符中检查它是否已经工作:

C:\Users\danny>ping raspberrypi.local

如果你看到这个,说明你已经安装了 Bonjour:

PING raspberrypi.local (192.168.0.53) 56(84) bytes of data.
64 bytes from 192.168.0.53 (192.168.0.53): icmp_seq=1 ttl=64 time=0.113 ms
64 bytes from 192.168.0.53 (192.168.0.53): icmp_seq=2 ttl=64 time=0.079 ms

如果你看到这个,你需要安装它:

Ping request could not find host raspberrypi.local. Please check the name and try again.

要这样做,浏览到 Apple Bonjour For Windows 网站 https://support.apple.com/downloads/bonjour_for_windows,下载它,然后安装 Download Bonjour Print Services for Windows。一旦运行,Windows 将能够通过名称请求 mDNS 设备。

测试设置

树莓派的绿灯应该停止闪烁,只应看到一个红色的电源灯。这表明 Pi 已经完成启动并连接到网络。

在 Windows 中,通过按 Windows 键并在窗口搜索栏中输入 CMD 来召唤命令行。在 Linux 或 macOS 中,打开终端。从终端,我们将尝试 ping 树莓派,即在网络上找到 Pi 并发送一条简短的消息以获取响应:

ping raspberrypi.local

如果一切顺利,计算机将显示它已连接到Pi:

$ ping raspberrypi.local
PING raspberrypi.local (192.168.0.53) 56(84) bytes of data.
64 bytes from 192.168.0.53 (192.168.0.53): icmp_seq=1 ttl=64 time=0.113 ms
64 bytes from 192.168.0.53 (192.168.0.53): icmp_seq=2 ttl=64 time=0.079 ms
64 bytes from 192.168.0.53 (192.168.0.53): icmp_seq=3 ttl=64 time=0.060 ms
64 bytes from 192.168.0.53 (192.168.0.53): icmp_seq=4 ttl=64 time=0.047 ms

如果你无法连接到树莓派怎么办?在下一节中,我们将尝试一些故障排除步骤。

故障排除

如果看起来树莓派没有响应ping操作,以下是一些你可以采取的步骤来尝试诊断和修复问题。尝试以下操作:

  1. 仔细检查你的连接。你应该看到几次绿灯闪烁和一个持续的红灯。如果没有,请拔掉电源,确保SD卡牢固地插入,并且电源供应可以提供2.1安培的电流,然后再次尝试。

  2. 使用树莓派启动后的Wi-Fi接入点设置,看看它是否已经获取了那里的IP地址。

  3. 如果你发现树莓派在你的Wi-Fi路由器上,这可能意味着你的计算机上mDNS没有正确运行。如果你还没有安装它,请返回并安装。在Windows上,如果同时安装了不同版本的Bonjour打印服务、Skype的Bonjour和iTunes的Bonjour,可能会发生冲突。使用Windows的添加/删除功能查看是否安装了多个Bonjour实例,然后删除所有Bonjour实例,然后再次安装官方版本。

  4. 接下来,关闭电源,取出SD卡,将其放回你的电脑中,并再次确认wpa_supplicant.conf文件存在并且具有正确的Wi-Fi详情和国家代码。此文件中最常见的错误如下:

    a) 错误的Wi-Fi详情

    b) 缺少引号或缺少/错误的标点符号

    c) 错误或缺失的国家代码

    d) 关键字大小写错误(关键字应为小写,国家代码为大写)

  5. 当树莓派启动时,SSH文件会被移除。如果你确定它曾经在那里并且已经被移除,这意味着Pi实际上已经启动了。

  6. 最后,你可能需要将Pi与屏幕和键盘连接后启动,并尝试诊断问题。显示屏将告诉你是否有关于wpa_supplicant.conf或其他问题的其他问题。使用屏幕文本并在网上搜索答案。我无法在这里重现所有这些问题,因为这里可能发生许多不同类型的问题。我还建议使用标签#raspberrypi在Twitter上提问,在Stack Overflow上,或在树莓派论坛https://www.raspberrypi.org/forums/上提问。

我们现在已经验证了我们的Pi已连接到网络,并在途中解决了故障排除问题。我们已经能够通过ping找到它。现在我们知道它在那里,让我们连接到它。

使用PuTTY或SSH连接到你的树莓派

之前,我们在树莓派的引导文件中添加了一个名为ssh的文件。这激活了树莓派上的SSH服务。如前所述,SSH是安全壳的缩写,旨在提供安全的网络访问。在这种情况下,我们并不是特别针对安全的加密功能,而是使用远程网络功能,在不接触树莓派的情况下发送和接收指令和文件。

重要提示

如果你已经使用SSH客户端,请注意,并非所有Windows命令行SSH客户端都支持mDNS。

PuTTY是一个方便的工具,用于访问SSH,适用于Windows、Linux和Mac。这些操作系统的安装信息可以在https://www.ssh.com/ssh/putty/找到。

一旦你从前面的链接安装了PuTTY,让我们将其连接到你的树莓派。请按照以下步骤操作:

  1. 启动PuTTY。你将看到一个类似于图4.3的屏幕。在raspberrypi.local上点击打开以登录到你的树莓派:

    图4.3 – 连接到树莓派

  2. 第一次这样做时,PuTTY会显示一个安全警告,要求你添加树莓派的密钥如果你信任它。点击;如果出现具有相同主机名(例如,新的树莓派)但不同密钥的其他设备,它只会再次询问你。

  3. 当你看到pi时,按Enter键,并使用密码raspberry。你现在将看到类似于图4.4的界面,显示你已经连接到树莓派:

图4.4 – 成功连接

在本节中,你已经使用PuTTY或你偏好的SSH客户端连接到树莓派,这让你可以配置它,向它发送命令,并与它交互。接下来,我们将看看如何配置它。

配置树莓派OS

现在我们已经连接上了,让我们做一些事情来为树莓派的使用做准备,比如更改用户密码和更改主机名以提高树莓派的安全性。

我们可以使用raspi-config工具执行许多这些任务,这是一个用于在树莓派OS上执行配置任务的菜单系统。我们通过另一个工具sudo启动它,它以root(主用户)的身份运行raspi-config。请参考以下命令:

sudo raspi-config

raspi-config界面将显示,如图4.5所示:

图4.5 – raspi-config工具

现在我们已经访问了raspi-config,我们可以使用它来更改树莓派上的一些设置。

重命名你的树莓派

每个新的树莓派镜像都被称为 myrobot,但我相信你可以想到更好的名字。你稍后也可以更改它。它只能包含字母、数字和破折号字符。请按照以下步骤操作:

  1. raspi-config中,选择网络选项,如图4.6所示。使用键盘上的箭头键突出显示并按Enter键选择条目:

    图4.6 – 网络选项

  2. 现在选择主机名,如图4.7所示:

    图4.7 – 更改主机名

  3. 你应该会看到一个等待输入主机名的屏幕。为你的机器人输入一个名字(我的是myrobot),然后按Enter键设置。请比我的名字更有创意。

你现在已经为你的机器人命名了,如果你有其他的Raspberry Pi,这将更加重要,同时也给机器人增添了一点个性。让我们也把密码改得更不常见一些。

保护你的Pi(一点点)

现在,你的Raspberry Pi与每个从镜像中新鲜安装的Raspberry Pi有相同的密码。建议你更改它。执行以下步骤:

  1. raspi-config的顶部菜单中选择更改用户密码图4.8):

    图4.8 – 更改密码

  2. 为你的机器人输入一个新的密码——一些你记得的,比raspberry更独特的东西。它不应该是你用于敏感事物(如电子邮件或银行)的任何密码。

更改密码个性化了你的机器人上的Raspberry Pi,并且大大降低了其他人连接到你的机器人并破坏你辛勤工作的可能性。现在我们已经进行了配置更改,我们需要重启Raspberry Pi以使更改生效。

重启并重新连接

是时候完成配置并重启Pi了:

  1. 使用Tab键跳转到完成项(图4.9)并按Enter键:

    图4.9 – 选择完成

  2. 下一屏会询问你是否想要重启Pi。选择并按Enter键(图4.10):

    图4.10 – 确认重启

    Raspberry Pi将开始重置,PuTTY会话也会在此时断开(如图4.11所示)。等待几分钟;Pi上的绿色活动灯会闪烁一下然后稳定下来。PuTTY会告诉你它已经失去了连接。Pi现在已关闭。红灯会一直亮着,直到你断开电源:

    图4.11 – PuTTY告诉你Pi连接已断开

    PuTTY只发送命令到机器人;它不理解这个命令已经关闭了Pi。它不期望连接关闭。我们比其他人更清楚,因为我们告诉Pi重启。点击确定以关闭错误。

  3. 使用PuTTY再次连接到你的机器人,使用你为机器人提供的新的主机名(在我的例子中,是myrobot),以.local结尾,并使用新密码,如图图4.12所示:

    图4.12 – 重新连接到Raspberry Pi

  4. 现在,你应该能够登录并看到你的提示符为pi@myrobot,或者你的机器人的名字是什么,如图图4.13所示:

图4.13 – 重新连接到Raspberry Pi

我们现在已重新连接到Raspberry Pi。我们可以尝试一个简单的Linux命令来查看我们使用了SD卡多少空间。Linux命令通常是你要让计算机执行的事情的缩写。

图4.13 中的 df 命令显示了连接到你的树莓派的各个存储位置所使用的空间。额外的 -h 使得 df 以人类可读的数字显示。它使用 GMK 后缀分别表示千兆字节、兆字节和千字节。输入 df -h 命令,如前一个屏幕截图所示,它将显示 /dev/root 接近SD卡的全尺寸,其他设备占据了剩余的空间。

更新你的树莓派软件

在这里要做的最后一件事是确保你的树莓派上安装了最新的软件。这是一个你可以开始并离开去吃饭的过程,因为它会花费一些时间。输入 sudo apt update -y && sudo apt upgrade -y 命令,你应该会看到以下类似的内容:

pi@myrobot:~ $ sudo apt update – y && sudo apt upgrade -y
Hit:1 http://raspbian.raspberrypi.org/raspbian buster InRelease
Hit:2 http://archive.raspberrypi.org/debian buster InRelease
. 
.
Reading package lists... Done
Building dependency tree
Reading state information... Done
Calculating upgrade... Done
The following packages will be upgraded:
  bluez-firmware curl libcurl3 libcurl3-gnutls libprocps6 pi-bluetooth procps
  raspi-config wget
9 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
Need to get 1,944 kB of archives.
After this operation, 16.4 kB of additional disk space will be used. Get:1 http://archive.raspberrypi.org/debian buster/main armhf bluez-firmware all 1.2-3+rpt6 [126 kB]
.
.
.

请让Pi继续运行,直到这里完成,不要中断或关闭电源,直到 pi@myrobot:~ $ 提示符重新出现。

小贴士

你可能已经在几个命令中看到了 sudo 的模式。此命令告诉树莓派以 root 用户(Linux管理员/超级用户)运行以下命令(如 raspi-configapt)。你可以将其读作 superuser do。这对于将更改系统或执行更新的软件是必需的。尽管如此,对于用户程序通常不需要。

在积极进行项目工作的同时,每月进行一次此更新/升级步骤是值得的。当树莓派更新到最新状态时,你将能够为机器人代码安装额外的软件。当软件过时,apt-get 安装可能不会工作。通常更新可以解决这个问题。

关闭树莓派

当你完成Pi的会话时,按照以下方式关闭它:

pi@myrobot:~ $ sudo poweroff 

等待绿灯活动停止;PuTTY 将检测到它已断开连接。你现在可以安全地断开电源。

重要提示

当树莓派未预期断电时,可能会导致文件丢失和SD卡损坏。你可能会丢失工作并损坏SD卡。始终使用正确的关机程序。

摘要

在本章中,你看到了如何通过使其无头从屏幕和键盘中解放树莓派。你设置SD卡以连接到你的Wi-Fi并启用SSH,以便你可以连接到它。你使用了 raspi-config 来个性化你的Pi并使用你自己的密码来保护它。然后,你迈出了探索它所运行的Linux系统的第一步。你还确保了树莓派是最新的,并运行了最新的软件。最后,我们看到了如何安全地将Pi置于关机模式,这样在拔掉电源时不会发生文件系统损坏。

现在,你已经学会了如何制作无头树莓派。你已经看到了如何保持其更新并连接到你的网络,树莓派现在可以开始构建了。你可以用它来构建树莓派供电的小工具,包括机器人。

在下一章中,我们将探讨如何确保在出现问题时不会丢失宝贵的代码或配置。我们将了解可能发生的问题以及如何使用 Git、SFTP 和 SD 卡备份来保护我们的辛勤工作。

评估

  1. 你可以用无头树莓派构建哪些其他小工具或项目?

  2. 尝试给你的树莓派设置一个不同的主机名,并使用 PuTTY 和 mDNS 在本地连接。

  3. 尝试在树莓派上使用其他 Linux 命令,例如 cdlscatman

  4. 在尝试这些操作后,请正确关闭树莓派。

进一步阅读

请参考以下内容以获取更多信息:

  • 《树莓派3物联网》马内什·拉奥Packt 出版公司:这本书使用有线无头树莓派进行其中的演示和实验。

第六章:第五章:使用Git和SD卡副本备份代码

在你为你的机器人创建和定制代码的过程中,你将投入许多小时来让它做些令人惊叹的事情,除非你采取预防措施,否则所有这些事情都可能突然消失。程序并不是全部的故事,因为你已经开始了为机器人使用Raspberry Pi OS的配置。你希望在灾难发生时保留你的代码和配置,以便在做出你后悔的改变时能够回退。

本章将帮助你了解代码或配置如何具体地被破坏,以及在你为机器人定制代码时可能遇到的灾难。然后我们将探讨三种防止这种情况的策略。

在本章中,你将了解以下内容:

  • 理解代码如何被破坏或丢失

  • 策略1 – 在PC上保留代码并上传

  • 策略2 – 使用Git回到过去

  • 策略3 – 制作SD卡备份

技术要求

对于本章,你需要以下内容:

  • 你在上一章准备好的Raspberry Pi和SD卡

  • 你与Pi一起使用的USB电源和电缆

  • 一台连接到互联网并能够读写SD卡的Windows、Linux或macOS计算机或笔记本电脑

  • 软件:FileZilla和Git

  • 在Windows上:Win32DiskImager

这是本章代码文件的GitHub链接:

https://github.com/PacktPublishing/Learn-Robotics-Fundamentals-of-Robotics-Programming/tree/master/chapter5

查看以下视频以查看代码的实际应用:https://bit.ly/3bAm94l

理解代码如何被破坏或丢失

代码及其紧密相关的配置需要时间和辛勤的工作。代码需要配置才能运行,例如Raspberry Pi OS配置、额外软件和必要的数据文件。两者都需要研究和学习,并需要设计、制作、测试和调试。

许多糟糕的情况可能导致代码丢失。这些事情发生在我将机器人带到展览会上的一周前,经过几周的工作,我艰难地学会了要非常认真地对待这个问题。那么,你的代码可能会发生什么?

SD卡数据丢失和损坏

SD卡损坏是指用于存储你的代码、Raspberry Pi OS以及你准备在SD卡上的任何内容的SD卡数据被破坏。文件变得不可读,或者卡变得无法使用。SD卡上的信息可能会永久丢失。

如果Raspberry Pi意外断电,SD卡可能会损坏,导致数据丢失。一个过热的Pi会慢慢地烘烤SD卡,损坏它。Pi的视觉处理是一种它可能变热的方式。如果Pi通过GPIO引脚或其电源发生电学上的糟糕事情,SD卡可能会损坏。MicroSD卡也很小,如果不放在Pi中,很容易丢失。

代码或配置的更改

我们都会犯错误。编码意味着尝试新事物。虽然有些事情会成功,但有些不会,事情会出错。在这些时候,你将想要回顾一下你做了什么更改。你可能能够使用差异来找到错误,或者如果你的实验看起来像是死胡同,你可能想要回到一个已知的工作版本。

你也可以通过错误的配置使机器人失效,例如树莓派未连接到网络或无法再启动。系统包的升级可能会出错,导致代码无法工作或需要对其进行大量更改才能再次工作。

这些问题可能结合在一起导致真正的噩梦。我曾看到代码的更改导致机器人行为不当并损坏自己,以至于SD卡损坏。在我更新操作系统包时,我意外地拔掉了电源线,导致SD卡损坏,并在一个重要的机器人活动两周前破坏了Raspberry Pi OS,重建它非常痛苦。这是一次艰难的教训。

备份代码和备份SD卡配置。在本章的剩余部分,我们将探讨一些解决方案,以保护你的机器人软件免受各种灾难的影响。

策略1——在PC上保留代码并上传

安全文件传输协议SFTP)允许你从计算机传输文件到Pi。此策略使你能够在计算机上编写代码,然后将其上传到树莓派。你可以选择你的编辑器,并拥有多个副本的安全感。

重要提示

但等等——哪个编辑器?编辑代码需要为这个目的设计的软件。对于Python的推荐是Mu、Microsoft VS Code、Notepad++和PyCharm。

SFTP使用SSH通过网络将文件从树莓派复制到其他设备。所以,让我们看看如何操作:

  1. 首先,在PC上创建一个文件夹来存储你的机器人代码;例如,my_robot_project

  2. 在那个文件夹内,使用你的编辑器创建一个测试文件,该文件只会打印一些文本。将此代码放入名为hello.py的文件中:

    print("Raspberry Pi is alive")
    
  3. 我们将把这个文件复制到机器人上并运行它。你可以使用来自https://filezilla-project.org的SFTP工具FileZilla进行复制。下载此工具并按照安装说明操作:

    图5.1 – FileZilla

  4. 插入并开启你的树莓派。你会在右侧面板的底部(图5.1)注意到,FileZilla显示未连接

  5. sftp://;例如,sftp://myrobot.local

  6. pi,并在密码框中输入你之前设置的密码。

  7. 点击快速连接按钮连接到树莓派。

  8. 连接后,你将在右侧远程站点面板中看到树莓派上的文件,如图5.2所示:

    图5.2 – 连接的树莓派

  9. 使用左侧本地站点面板转到你的计算机上的代码。

  10. 现在点击左上角高亮的hello.py,将其拖动到右下角的面板上,以便将其放置在Raspberry Pi上!img/B15660_05_03.jpg

    图5.3 – 文件传输

  11. 当您拖动文件时,您应该能在队列文件部分看到它,如图5.3所示。由于此文件很小,它将仅在队列状态中存在一瞬间。您还可以使用相同的系统来处理整个文件夹。您很快就会在远程站点(Raspberry Pi)上看到文件,如图5.3右侧的面板所示。

  12. 要运行此代码,请使用PuTTY登录到Pi并尝试以下命令:

    pi@myrobot:~ $ python3 hello.py
    Raspberry Pi is alive
    

这种策略是使代码更安全的一个很好的开始。通过在您的笔记本电脑/PC上工作并将代码复制到Pi上,您已经确保除了机器人上的代码外,始终还有另一份副本。您还可以在PC上使用您喜欢的任何代码编辑器,并在代码到达Raspberry Pi之前发现一些错误。现在我们有了副本,让我们看看我们如何跟踪代码的更改以及我们做了哪些更改。

策略2 – 使用Git回到过去

Git是一种流行的源代码控制形式,它记录了您对代码所做的更改的历史。您可以回顾这些更改,查看它们是什么,恢复旧版本,并保留一个注释日志,说明您为什么做出这些更改。Git还允许您在多个位置存储代码,以防硬盘故障。Git将代码及其历史存储在仓库中,或称为repos。在Git中,您可以创建分支,即整个代码集的副本,以并行尝试您的代码中的想法,然后稍后将其合并回主分支。

我将为您开始,但本节只能触及Git所能做到的一小部分。让我们开始:

  1. 按照以下网址的说明安装Git:https://git-scm.com/book/en/v2/Getting-Started-Installing-Git

    小贴士

    如果您使用Windows或macOS,我建议您使用GitHub应用以简化设置。

  2. Git要求您使用电脑上的命令行设置您的身份信息:

    > git config --global user.name "<Your Name>"
    > git config --global user.email <your email address>
    
  3. 为了将此项目置于源代码控制之下,我们需要初始化它并提交我们的第一行代码。请确保您已进入电脑上的命令行中您的代码文件夹(my_robot_project),并输入以下命令:

    git init . tells Git to make the folder into a Git repository. git add tells Git you want to store the hello.py file in Git. git commit stores this change for later, with -m <message> putting a message in the journal. Git responds to show you it succeeded.
    
  4. 我们现在可以通过git log查看日志:

    > git log
    commit 11cc8dc0b880b1dd8302ddda8adf63591bf340fe (HEAD -> master)
    Author: Your Name <your@email.com>
    Date: <todays date>
    Adding the starter code
    
  5. 现在修改hello.py中的代码,将其更改为以下内容:

    myrobot is alive! or whatever you set the hostname of your robot to be. However, we are interested in Git behavior. Note – more advanced Git usage could let you use Git to transfer code to the Raspberry Pi, but that is beyond the scope of this chapter. Let's see how this code is different from before: 
    
    

    打印行,然后在其位置添加一个导入语句和一个打印行。我们可以将此添加到Git中,以创建一个新版本,然后再次使用git log来查看两个版本:

    > git add hello.py
    > git commit -m "Show the robot hostname"
    [master 912f4de] Show the robot hostname
     1 file changed, 2 insertions(+), 1 deletion(-)
    > git log
    commit 912f4de3fa866ecc9d2141e855333514d9468151 (HEAD -> master)
    Author: Your Name <your@email.com>
    Date: <the time of the next commit>
    Show the robot hostname
    commit 11cc8dc0b880b1dd8302ddda8adf63591bf340fe (HEAD -> master)
    Author: Your Name <your@email.com>
    Date: <todays date>
    Adding the starter code
    
    
    

使用这种方法,您可以回到以前的版本,或者只是比较版本,并保护自己免受可能后悔的更改。然而,我们刚刚只是触及了Git的强大功能。请参阅“进一步阅读”部分中的参考,了解如何创建分支、使用远程服务、回滚到以前的版本以及找到浏览Git历史中代码的工具。

现在我们可以在时间上前后移动(至少对于我们的代码来说),我们可以更有信心地进行更改。只需记住经常提交更改 – 尤其是在某件事开始工作之后!接下来,我们将探讨如何保持配置和已安装的软件包。

策略3 – 制作SD卡备份

Git和SFTP非常适合保持代码安全,但它们不能帮助你重新安装和重新配置SD卡上的Raspberry Pi OS。Windows、Linux和macOS的此过程相当不同。基本思路是将SD卡插入并使用工具将其整个克隆到称为镜像的文件中,当你需要恢复时,可以使用balenaEtcher进行恢复。

重要提示

你应该只将镜像恢复到相同大小或更大的卡上。将镜像放在较小的设备上可能会失败,从而创建一个损坏的SD卡。

在我们开始之前,请正确关闭Raspberry Pi,取出其SD卡,并将其放入你的电脑中。这些干净镜像很大,所以不要将它们放入你的Git仓库。这超出了本章的范围,但我建议找到一种方法来压缩这些文件,因为它们现在大部分是空的。在任何情况下,由于镜像的大小,预期此操作需要20-30分钟。

Windows

对于Windows,我们将使用Win32DiskImager。因此,我们的第一步将是安装和设置此工具。请按照以下步骤操作:

  1. 你可以在https://sourceforge.net/projects/win32diskimager获取此安装程序。

  2. 运行此程序并按照安装说明进行操作。

    小贴士

    由于我们将立即使用它,我建议勾选立即启动复选框。

  3. 图5.4的右侧突出显示的是设备;这应该会自动找到SD卡设备。使用突出显示的文件夹图标选择镜像文件将存储的位置:img/B15660_05_04.jpg

    图5.4 – Win32 Disk Imager

  4. 图5.5中,我在文件名框中命名我的镜像为myrobot.img。然后点击打开按钮以确认这一点:img/B15660_05_05.jpg

    图5.5 – 选择位置

  5. 点击打开后,你将看到一个类似于图5.6左侧的屏幕,其中镜像文件框中显示你的选择位置。点击读取按钮开始复制镜像。在读取镜像时,你会看到一个进度条和剩余时间的估计。当镜像完成时,Win32 Disk Imager会告诉你读取成功,然后你可以退出软件:

img/B15660_05_06.jpg

图5.6 – 读取镜像

现在你已经在SD卡上创建了一个数据完整副本。如果你遇到损坏或配置问题,你可以将此镜像写回SD卡以恢复到这个点。

Mac

MacOS X有一个内置的方法来创建SD卡和磁盘镜像。这是通过使用内置的磁盘工具完成的。让我们看看它是如何工作的:

  1. 启动磁盘工具工具。加载后,它应该看起来像图5.7img/B15660_05_07.jpg

    图5.7 – 磁盘工具

  2. 点击查看菜单以显示图5.8img/B15660_05_08.jpg

    图5.8 – 查看菜单

  3. 现在点击显示所有设备选项。

  4. 你现在应该能看到图5.9中显示的屏幕。选择包含引导分区的设备!img/B15660_05_09.jpg

    图5.9 – 启用显示所有设备的磁盘工具

  5. 在菜单栏中,选择文件 | 新建映像图5.10):img/B15660_05_10.jpg

    图5.10 – 新建映像菜单

  6. 在此之下,选择从<您的存储设备>创建映像图5.11):img/B15660_05_11.jpg

    图5.11 – 存储设备图像

  7. 磁盘工具将显示一个对话框(图5.12)。设置文件名和位置,并将格式设置为DVD/CD主盘img/B15660_05_12.jpg

    图5.12 – 保存对话框

  8. 磁盘工具给这些文件一个.cdr扩展名(图5.13):img/B15660_05_13.jpg

    图5.13 – 扩展名为.cdr的文件

  9. 将其重命名为.isoimg/B15660_05_14.jpg

    图5.14 – 重命名为.iso

  10. 您需要确认您想要这样做(图5.15):

img/B15660_05_15.jpg

图5.15 – 确认扩展名更改

您现在可以使用balenaEtcher在macOS上创建SD映像。

Linux

在Linux的命令行中,通过使用dd命令来备份SD卡。在我们看到它是如何工作的之前,我们首先需要找到设备的位置。让我们开始:

  1. 插入卡并输入以下内容以找到设备的位置:

    $ dmesg
    
  2. 此命令将输出很多内容,但您只对接近结尾的一行感兴趣,看起来如下所示:

    [sdb], which may be different on your computer. The SD card location will be /dev/<drive location>, for example, /dev/sdb. Important noteBe careful to get the locations right, as you could destroy the contents of an SD card or your computer hard drive. If you are at all unsure, **do not** use this method.
    
  3. 一旦您有了SD位置(例如/dev/sdb/dev/disk1),您就可以使用dd命令开始克隆。此命令将数据从驱动器中转储到另一个驱动器:

    $ sudo dd if=/dev/sdb of=~/myrobot.img bs=32M
    Password:
    474+2 records in
    474+2 records out
    15931539456 bytes (16 GB, 15 GiB) copied, 4132.13 s, 3.9 MB/s
    

if参数是of参数,是你将要克隆到卡中的myrobot.img文件。

bs参数是32M,会使操作更快。

为了启动此操作,您需要输入用户密码。dd命令将在您的家目录中创建一个myrobot.img文件,作为整个SD卡的克隆。dd命令在完成前不会给出任何输出,完成后会显示操作统计信息。

摘要

在本章中,您已经学习了如何照顾您的代码和配置。您已经看到了事情可能出错的方式,以及保护您的工作免受其害的策略。您有了Git、SFTP和SD卡备份的起点,您可以将它们一起使用,对您的机器人进行一些实验,并无所畏惧地改变它。您可以使用SFTP在您的计算机上编辑,这样您至少有一份除了机器人上的代码之外的副本,并让您使用强大的编辑器。您可以使用Git回到过去,这样您可以从错误和实验中恢复,或者只是查看差异。您可以使用SD卡备份来获取Raspberry Pi使用的存储的完整映像,并在出错时恢复它。

在下一章中,我们将开始构建一个基本的机器人。我们将组装带有电机和轮子的机器人底盘,确定要使用的电源系统,然后测试调整我们机器人的整体形状。带上螺丝刀!

评估

  • 在你的电脑上尝试创建一个文件——一个简单的图片或文本文件。尝试使用 SFTP 将其发送到树莓派,然后使用 PuTTY,看看你是否可以使用 ls 命令列出该文件。该文件可以是一个简单的 Python 脚本,你可以在树莓派上尝试运行它。

  • hello.py 进行一个错误的更改。使用 diff 查看差异。使用 Git 资源(见 进一步阅读 部分)了解如何将其恢复到更改前的状态。

  • 使用前面的说明备份你的树莓派 SD 卡,对 /home/pi 中的数据进行一些更改,然后使用 balenaEtcher 恢复镜像。你甚至可以将备份恢复到另一张 SD 卡,并将其插入树莓派,就像它是原始卡一样。

  • 我建议了解 Git 如何用于管理你的代码,甚至作为将代码上传到树莓派的方法。使用 进一步阅读 部分了解更多关于 Git 的信息,以及如何将其融入你的编码工作流程。Git 可能很复杂,但它是一个值得学习的工具。

进一步阅读

请参考以下内容获取更多信息:

第七章:第2节:构建自主机器人 – 将传感器和电机连接到树莓派

在本节中,我们将构建一个机器人,并使用代码使其移动。我们还将连接传感器和电机到控制器,并编写代码以实现基本的自主导航行为。

本书本部分包括以下章节:

  • 第6章构建机器人基础 – 轮子、电源和布线

  • 第7章驱动和转向 – 使用Python移动电机

  • 第8章使用Python编程距离传感器

  • 第9章使用Python编程RGB灯带

  • 第10章使用Python控制伺服电机

  • 第11章使用Python编程编码器

  • 第12章使用Python进行IMU编程

第八章:第 6 章:构建机器人基础 – 轮子、电源和布线

在本章中,我们将开始构建机器人。我们将选择带有轮子和电机的机器人底盘套件、电机控制器和一些为机器人供电的电源,讨论权衡和要避免的事项。我们将了解如何确保一切适配,然后构建机器人。到本章结束时,你将拥有一个基本的机器人结构准备就绪。

现在就确定权衡和计划,这将给你一个可以构建和实验的机器人,确保在购买之前知道组件是否合适。

在本章中,你将学习以下内容:

  • 选择机器人底盘套件

  • 选择电机控制器板

  • 为机器人供电

  • 测试安装机器人

  • 组装底座

  • 将电机连接到树莓派

技术要求

对于本章,你需要以下物品:

  • 一台可以访问互联网的电脑。

  • 树莓派和一张 SD 卡。

  • 一套螺丝刀:M2.5、M3 斜口螺丝刀和一些珠宝匠螺丝刀。

  • 一把长鼻钳。可选,一套微型公制扳手。

  • 一些电工胶带。

  • 挂钩和搭扣或魔术贴胶带。

  • 绘图软件,如 app.diagrams.net、Inkscape、Visio 或类似软件。

  • M2.5 和 M3 螺纹的尼龙支撑套件。

  • 一些绝缘胶带。

  • 四节 AA 电池,已充电。

    重要提示

    在本章中,你将选择并购买底盘、电机控制器和电池盒,但不要现在就购买。

观看以下视频以查看代码的实际应用:https://bit.ly/3oLofCg

选择机器人底盘套件

底盘,就像控制器一样,是制作机器人的一个基本决定。虽然可以使用 3D 打印或玩具改造来自行制作,但最简单的开始方式是使用底盘套件。这些套件包含开始机器人构建所需的零件套件。底盘可以更换,但这将意味着重新构建机器人。

互联网上有许多底盘套件 – 太多了。那么,你如何选择一个呢?

尺寸

确定机器人的尺寸也很重要。如果机器人太小,你需要微型化电子技能;如果太大,你需要处理更多严重的电源。这两者都是初学者应该避免的事情:

![图 6.1 – 机器人底盘尺寸比较

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_06_01.jpg)

图 6.1 – 机器人底盘尺寸比较

我们可以从 图 6.1 中的底盘照片比较不同尺寸的机器人:

  • 底盘 1 的直径为 11 厘米,勉强可以装下一个控制器,但太小了。如此小巧使得构建机器人变得困难。将控制器、电源和所有传感器挤压到这个小空间需要超出初学者机器人构建范围的技术和经验。

  • 底盘2是Armbot。这个更大的机器人宽33厘米,长30厘米,提供了大量的空间和额外的300毫米手臂伸展范围。它需要八个AA电池、大电机和强大的电机控制器。这些增加了成本,并且可能对新手在处理电力、重量和刚性方面造成问题。Armbot是我最昂贵的机器人之一,不包括手臂的成本!

  • 底盘3适合Pi、电池和传感器,但不会很大或笨重。它的尺寸恰到好处,大约长15-20厘米,宽10-15厘米。那些有分层设计的可能适用,但不超过两层,因为三层或四层会使机器人顶部过重,导致其倾倒。底盘3有足够的空间,并且相对容易构建。

让我们来看看轮子的数量。

轮子数量

一些底盘套件有复杂的移动方式——腿、坦克履带、梅卡诺轮和三叉星轮,仅举几例。虽然这些很有趣,我也鼓励尝试它们,但这不是开始的地方。我建议在您的第一台机器人上使用彻底合理和基本的轮子。

有四轮驱动(如图6.2所示)和六轮驱动的套件。这些可能非常强大,需要更大的电机控制器。它们也可能消耗大量电池,并且增加了超载某物的可能性。额外的电机可能意味着更复杂的布线:

![图6.2 – 四轮驱动机器人

img/B15660_06_02.jpg

图6.2 – 四轮驱动机器人

两轮驱动最简单,通常需要一个第三轮来保持平衡。第三轮可以是万向轮(如图6.3所示)、滚球或仅为微型机器人设计的Teflon滑板。两个轮子也是最易于操控的,避免了使用四个或更多轮子的机器人所遇到的某些摩擦问题:

![图6.3 – 带有万向轮的两个轮子

img/B15660_06_03.jpg

图6.3 – 带有万向轮的两个轮子

两个轮子没有四轮或六轮驱动的牵引力,但它们简单且有效。它们也是最便宜的。

轮子和电机

对于初学者来说,套件应该包括轮子和电机。轮子应该有简单的橡胶轮胎。图6.4显示了低成本机器人轮子的常见样式。许多套件中都包含这些:

img/B15660_06_04.jpg

img/B15660_06_04.jpg

图6.4 – 常见的低成本机器人轮子

套件还应包括两个电机,一个用于每个轮子,并包括将其安装到底盘上的螺丝或部件。我建议使用直流齿轮电机,因为齿轮可以保持速度可用,同时增加机器人具有的机械推力。

重要的是,电机应该像图6.5中的第一个电机那样连接好电线:

![图6.5 – 有线和无线齿轮电机

img/B15660_06_05.jpg

图6.5 – 有线和无线齿轮电机

将这些电线焊接或连接到电机上的小标签上很棘手,而且连接不良的电线有令人沮丧的习惯,总是容易脱落。你想要开始使用的套件中已经包含了这些电线,如图图6.6所示:

![图6.6 – 编码器轮和插槽特写

![img/B15660_06_06.jpg]

图6.6 – 编码器轮和插槽特写

在电机安装的位置,套件应该有一些编码器轮和一个读取它们的插槽。编码器轮也被称为里程表、转速表或转速轮。

简单性

你不希望为你的第一个机器人搭建使用一个复杂或难以组装的套件。我反复强调,在两轮驱动的情况下,你想要两个带有焊接电线的电机。我避免使用大型机器人或独特而令人兴奋的移动系统,并不是因为它们有缺陷,而是因为从简单开始更好。这有一个限制,即购买时已经完全组装和封闭的机器人套件,留给学习和实验的空间很少。一个完全预制的机器人可能需要玩具黑客技能来定制。

成本

与简单性相关的是成本。你可以从大约15美元购买机器人底盘套件,到数千美元不等。更大、更复杂的机器人往往成本更高。对于这本书,我旨在保持较低的成本选项,或者至少展示它们是可能的。

结论

因此,现在你可以选择一个底盘套件,它包含两个轮子和一个万向轮,两个带有焊接电线的电机,插槽和编码器轮。这些激光切割的底盘并不昂贵,在流行的互联网购物网站上广泛可用,被称为智能车底盘,例如2WD

![图6.7 – 我正在使用的机器人套件

![img/B15660_06_07.jpg]

图6.7 – 我正在使用的机器人套件

我正在使用的套件在没有Raspberry Pi组装时看起来像图6.7

我们已经选择了一个底盘套件。这是一个中等大小的套件,不需要处理大量电力。它有一些空间限制,但并不小。我们可以使用这些属性来选择电机控制器。

选择电机控制器板

你接下来需要的重要部分是一个电机控制器。你不能直接将Raspberry Pi连接到直流电机,因为它们需要不同的电压和高电流,这会损坏GPIO引脚。电机控制器板还可以添加接口到其他设备,如传感器和其他类型的电机。

它是一个至关重要的机器人组件,将指导许多后续的决定。与电机一样,在购买之前有一些权衡和考虑:

![图6.8 – 电机控制板的选择

![img/B15660_06_08.jpg]

图6.8 – 电机控制板的选择

图6.8展示了一个小型的电机控制器板样本组。当我们比较我们电机板的需求时,我们参考那里展示的板作为例子。

集成程度

电机控制器可能只能控制一个电机(通常是2个),如 L298N,包含最基本的安全运行此芯片的配置。它们不是设计在 Raspberry Pi 上使用的,必须连接到 Pi 的 I/O 输出。

类似于 PiZMoto 的控制器安装在 Raspberry Pi 之上,减少了布线。它们仍然只包含一个电路来控制电机。它们还配有引脚,可以连接额外的设备,如距离传感器和线传感器,这些传感器会改变电压等级,我们稍后会看到。这和 L298N 都需要 Raspberry Pi 生成 PWM 信号来驱动电机。

在更高层次的集成中是全功能步进 HAT。它有一个专门用于 PWM 控制的芯片,能够驱动多个伺服电机和直流电机。这个 HAT 通过使用 I2C 释放了 Raspberry Pi 的 I/O 引脚。

PiConZero 是这里最集成的设备。它使用一个相当于 Arduino 的微控制器来控制直流电机和伺服电机,输出到灯光并从各种传感器接收输入。它还使用 I2C,减少了所需的引脚数量。

引脚使用

当在 Raspberry Pi HAT 形式购买电机控制器时,考虑正在使用的 I/O 引脚非常重要。使用相同引脚进行不兼容功能的板不会工作。

重要提示

虽然HAT插入了所有引脚,但这并不意味着它们都被使用。通常只有HAT上的一小部分引脚实际上被连接。

要了解不同板在 Raspberry Pi 上的引脚交互,请查看 https://pinout.xyz,它允许您选择 Raspberry Pi 板并查看它们的引脚配置。

使用 L298N 需要四个 I/O 引脚来控制直流电机,使用传感器或伺服电机则需要更多的引脚。PIZMoto 的功能与 L298N 类似,需要四个 I/O 引脚来控制直流电机。PIZMoto 还为线检测器分配了一个引脚,为距离感应分配了两个引脚,为LED分配了两个引脚,总共占用了九个 GPIO 引脚。PIZMoto 还需要额外的伺服电机支持。

PiConZero 和全功能步进 HAT 都使用 I2C 总线。使用 I2C 或串行总线可以有效地使用引脚,因为它们只使用两个引脚进行 I2C。多个设备可以共享 I2C 总线,因此这些引脚的使用也是共享的。PiConZero 将一些其他引脚分配给超声波设备等功能。全功能步进 HAT 在支持直流电机和5个伺服电机的同时,将所有其他引脚留出供使用,使其成为选择中最灵活的控制板之一。

尺寸

电机控制器的大小选择取决于底盘和您拥有的电机的大小。简单来说,底盘越大,所需的控制器就越大。我们指定电机控制器的功率处理能力为安培。对于像 图6.7 中所示的机器人,每个通道大约需要 1 到 1.5 安培。

电压过低可能导致机器人几乎无法移动,甚至起火。控制器过大对空间、重量和成本都有影响。散热器是一种保持控制器冷却并处理电流的方法,但会使控制器更大,如图 6.9 所示:

![图 6.9 – 配有散热器的 L298N

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_06_09.jpg)

图 6.9 – 配有散热器的 L298N

集成度的高低也会影响尺寸。一个堆叠在 Pi 板上的小板比单独的板占用空间更少。

另一个需要考虑的尺寸或形状因素是板是否限制了 Raspberry Pi 摄像头端口的使用。一些板,如 Pimoroni Explorer HAT Pro,完全覆盖了摄像头插槽,使得使用变得很麻烦。一些板有摄像头端口的插槽,如 Full Function Stepper HAT,而其他的是半尺寸帽子(pHat),如 PiConZero 和 PizMoto,它们不覆盖该区域。

由于我们在本书中使用摄像头,因此摄像头端口必须是可访问的,无论是通过插槽还是通过作为半尺寸(PHat)板。

焊接

当你为机器人选择板时,请注意,一些板本身是套件,需要你在上面焊接部件。如果你有这方面的经验,这可能是一个选择,但会花费时间成本。一个小型引脚头将是一个非常快速和简单的工作。一个作为组件袋和裸板一起提供的板可能需要花费一个晚上的时间,并且可能需要调试。

小贴士

焊接是机器人构建的基本技能,但对于第一个机器人来说不是必需的,因此对于这本书,我主要推荐预组装模块。

电源输入

电机可能需要比 Raspberry Pi 使用的 5 V 电源不同的电压。它们也可能会消耗大量电流。我们将在后面讨论电源选择,但通常最好将电机电源与 Raspberry Pi 的电源分开。图 6.8 中的所有板都有独立的电机电源输入。

PiConZero 和 L298N 允许用户从电机电源为 Raspberry Pi 提供电源,但这可能导致复位和断电条件。一些电机接口板,如 Adafruit Crickit 和 Pimoroni Explorer HAT Pro,使用与电机相同的 5 V 电源为 Raspberry Pi 提供电源,需要高电流容量的 5 V 电源。我的建议是确保电机板有独立的电机电源输入。

连接器

与焊接和电源输入密切相关的是电机和电池的连接器。我倾向于更喜欢图 6.10 中所示的螺钉型连接器:

![图 6.10 – 电机和电池连接的螺钉端子

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_06_10.jpg)

图 6.10 – 电机和电池连接的螺钉端子

其他类型可能需要特殊连接器的电机,或者机器人构建者需要具备压接技能。

结论

我们的机器人空间有限;因此,我们使用Raspberry Pi HAT类型的尺寸。我们还在努力减少它使用的引脚数量。基于I2C的HAT让我们做到了这一点。全功能步进电机HAT如图6.11所示。它也被称为全功能机器人扩展板,使我们能够访问所有Pi引脚,同时作为一个强大的电机控制器:

![图6.11 – 全功能步进电机HAT]

![img/B15660_06_11.jpg]

图6.11 – 全功能步进电机HAT

它在大多数国家都有售,有空间放置摄像头的带状线,并控制伺服电机。我推荐这本书中的机器人使用这个HAT。我们的代码与基于Full Function板上PCA9685芯片的板直接兼容。通过在代码和布线上进行一些小的修改,4tronix PiConZero也是一个合适的选择。

为机器人供电

机器人需要为其所有部件供电。我们必须考虑两个主要电源系统:所有数字部件的电源,如Raspberry Pi和传感器,然后是电机的电源。

电机需要单独的电源系统,原因有几个。首先,它们消耗的电能比机器人上大多数其他组件都要多。它们可能需要不同的电压;我见过低压、大电流的电机电源和高压电源。另一个原因是它们可以引起干扰。它们可以拉取足够的电力,导致其他电路出现电压降低。电压降低是指电路电压下降到足够低或持续时间足够长,以至于进入不一致或重置状态。重置可能导致Pi上的SD卡损坏。电机在使用时还可以向电源线引入电气噪声,这可能导致数字部件出现异常行为。

为带有电机的机器人供电有两种主要策略:

  • 双电池:电机和机器人其余部分完全独立的电池组,确保它们的电源是独立的。

  • 电池消除器:使用单个电池或设置BEC/稳压器。BEC是电池消除电路,如图6.12所示:

![图6.12 – BEC的图片]

![img/B15660_06_12.jpg]

图6.12 – BEC的图片

双电池是避免任何电压降低、断电或干扰问题的最可靠选择。这种选择比BEC占用更多空间。然而,为Raspberry Pi配备USB移动电源的双电源选项(如图6.13所示)是一种简单而有效的方法来避免电源问题。选择一个外壳尺寸小、功率评级高,例如10,000 mAh,输出至少2.1 A的电源:

![图6.13 – Armbot的USB移动电源 – 稍显陈旧且有些磨损,但仍然有效]

![img/B15660_06_13.jpg]

图6.13 – Armbot的USB移动电源 – 稍显陈旧且有些磨损,但仍然有效

请检查USB移动电源是否附带连接Raspberry Pi的电缆——Raspberry Pi 3(A+和B+)需要USB微型连接器,而Raspberry Pi 4需要USB-C连接器。移动电源的输出通常使用USB-A连接器。如果它们没有包含在移动电源中,您还需要购买这些电缆之一。

电机控制器板有时会提供电源来调节从电机电源到Pi的电力。这些通常输出额定值过低。它们可能非常低效,浪费大量电池电力。除非使用高电流处理电池,否则它们很可能导致电压降低,导致断电。

电池消除电路更轻,占用的空间更小。类型包括BEC、uBEC、开关电源或稳压器。电池消除器通常需要高功率电池,如锂离子电池。通过与电机共享电源,电池需要足够的电流容量,以避免电压下降导致控制器复位和线路噪声。这一要求影响了像Wide Input SHIM这样的开关电源Pi SHIM以及一些电机控制器中内置的电源。

您需要确保BEC输出至少可以处理2.1 A,最好是更多。3.4 A和4.2 A的电源银行很常见。5 A额定值的UBEC也很常见。

为了使这个机器人简单,并且不需要处理复位问题,我们采用双电池方案,并接受大量和重量上的成本:

![图6.14 – 我们与电机一起使用的4 x AA电池盒

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_06_14.jpg)

图6.14 – 我们与电机一起使用的4 x AA电池盒

对于电机,4 x AA电池就足够了。我建议使用镍金属氢化物可充电电池。这不仅仅是因为您可以充电,还因为它们在需要时可以提供比碱性电池更多的电流。为了节省空间,我们使用两个向上/两个向下背对背配置,就像图6.14中显示的电池盒一样。

总结来说,在我们的电池选择中,我们将为电机使用一个4 x AA金属氢化物电池组,并为Raspberry Pi和逻辑电路使用一个USB移动电源。

我们现在已经做出了零件选择。我们知道我们将使用哪种底盘、控制器和电池配置。我们已经看到了电机和轮子的尺寸,在前几章中,我们已经选择了一个Raspberry Pi型号来使用。在我们继续购买所有这些零件之前,我们应该进一步检查以确保它们都能安装。在下一节中,我们将学习如何测试安装机器人零件。

测试安装机器人

我建议在订购零件之前先进行测试安装。测试安装可以帮助构建者更有信心地确保组件匹配,并且您将大致知道这些组件的位置。这一步骤可以为您节省时间和金钱。

您可以使用纸张和笔进行测试装配,或者使用 diagrams.net 这样的应用程序。首先,我找到了所有部件的尺寸。图 6.15 展示了从亚马逊获取产品尺寸的方法:

![图 6.15 – 查找产品规格

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_06_15.jpg)

图 6.15 – 查找产品规格

需要进行一些信息挖掘才能找到这些尺寸。对于每个部件,首先找到一个可以购买的地方,例如亚马逊、几个在线商店或 eBay。然后,您可以搜索或询问有关每个板或物品尺寸的信息。确保您查看的是部件的尺寸,而不是其包装:

![图 6.16 – 带尺寸的电池盒产品图纸

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_06_16.jpg)

图 6.16 – 带尺寸的电池盒产品图纸

您可以在 图 6.16 中找到类似电池盒的图表,通过在尺寸或部件数据表中搜索图像。在这种情况下,尺寸以毫米为单位。正负号表示制造公差,即下一个数字的正负。在测试装配时,偏向较高的一侧,所以将 57±1 视为 58 mm。

因此,我拥有的尺寸如下:

  • Raspberry Pi 3a+:65 mm x 56 mm。

  • 底盘:我的建议是 100 mm x 200 mm。请注意,这里的尺寸是外尺寸,包括车轮。

  • 电机控制器覆盖在 Pi 上,因此它被计算为 Pi。这个控制器会使东西变高,但仅对多层机器人底盘有实际影响。

  • 4 x AA 电池盒:我建议的类型是 58 mm x 31.5 mm。

  • USB 电源银行:60 mm x 90 mm。

对于此,按比例绘制矩形就足够详细了。在 diagrams.net 中创建一个新的空白图表:

![图 6.17 – 使用 diagrams.net 创建测试装配部件

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_06_17.jpg)

图 6.17 – 使用 diagrams.net 创建测试装配部件

您可以按照说明创建如图 6.17 所示的测试装配部件:

  1. 使用左侧的通用调色板拖出矩形。

  2. 明确标注每个部件很有帮助。双击矩形并输入标签。按 Enter 键接受标签。我在底盘的 前面 添加了一个文本标签。

  3. 选择项目,使其具有蓝色高亮。点击右侧的选项卡以选择 排列 选项卡。

  4. 这里,将您的尺寸(将毫米转换为点)输入到 宽度高度 框中。

  5. 您可以为所有项目重复 步骤 14,然后拖动部件:

![图 6.18 – 测试装配

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_06_18.jpg)

图 6.18 – 测试装配

Pi 应该靠近机器人的前面,因为我们稍后会在这里放置传感器,电机线可以向前进入。在 图 6.18 中,我已经将矩形拖到适当的位置。Draw.io 通过显示用于居中和对齐对象的蓝色指南线来帮助您。我将电源银行放在后面,将 AA 电池放在 Pi 较近的位置,以便它们可以轻松地进入电机控制器。

这些部件看起来可以安装。虽然不是100%准确,但足够好,可以说这可能会工作。

现在,是时候购买部件了。我的购物清单如下:

  • 底盘套件。

  • 全功能步进电机HAT。

  • 4个AA电池盒。

  • 4个金属氢化物AA电池。如果你没有,你需要为这些电池也购买一个充电器。

  • 1个能够提供3安培或更多电流的USB移动电源。

现在你已经选择了部件并做出了权衡。你随后通过测试安装它们来查看它们的布局并检查它们是否都适合。现在是时候你去购买你的部件和底盘了。一旦完成,请回来这里,因为那时我们可以开始我们的组装。

组装底盘

假设你购买了一个与我类似的底盘,你可以按照以下步骤组装它。对于完全不同的底盘,我强烈建议咨询组装说明文档。与这里推荐的大相径庭的底盘可能会使接下来的几章更难跟随。

一些部件可能被一层纸覆盖(如图6.19所示)。这层纸可以防止塑料划伤,可以安全地移除。你可以通过在下面放一根钉子或使用手工刀来完成这项工作。这不是必须的,但机器人看起来没有它更好:

![图6.19 – 移除机器人部件的保护背衬

![图片 B15660_06_19.jpg]

图6.19 – 移除机器人部件的保护背衬

对于使用黄色电机的激光切割套件,电机有两种主要的安装方式。一种类型有塑料支架,另一种有金属电机支架。鉴于你可能购买任何一种风格的套件,让我们看看套件可能有哪些不同。这种差异仅在组装步骤中才有意义,所以购买你能够买到的套件。

对于带有塑料电机支架的套件,你应该有图6.20中显示的部件:

![图6.20 – 机器人套件部件

![图片 B15660_06_20.jpg]

图6.20 – 机器人套件部件

在套件中,你应该有以下部件:

  1. 两个轮子。

  2. 编码器轮。

  3. 一对带线的电机。

  4. 一个万向轮。

  5. 用于安装万向轮的螺栓和黄铜支架。我已经用非导电尼龙螺栓替换了一套。你应该可以从尼龙支架套件中做到同样的替换。

  6. 底盘板。

  7. 用于安装电机的塑料支架。你的套件可能有金属类型,它们的工作方式略有不同,并附带四个额外的螺丝。

  8. 四个螺栓和螺母用于安装电机。

图6.21 展示了金属电机支架部件的不同之处:

![图6.21 – 金属类型电机支架

![图片 B15660_06_21.jpg]

图. 6.21 – 金属类型电机支架

在套件中,你应该有以下部件:

  1. 金属支架替换了这里的塑料支架。

  2. 底盘到支架螺栓 – 这些支架需要通过螺栓固定到底盘上。

  3. 用于支架到电机的长螺栓仍然相同。

    重要提示

    其他未显示的部件将与这个非常相似。

现在,让我们看看如何安装编码器轮。

安装编码器轮

我们将首先将编码器轮安装到电机上。我们需要这些轮子用于后续章节中的传感器部分:

![图 6.22 – 编码器轮安装到电机上]

![图 B15660_06_22.jpg]

图 6.22 – 编码器轮安装到电机上

按照图 6.22 中的步骤操作:

  1. 观察电线连接到它的哪一侧。编码器轮应该安装在电线相同的一侧。

  2. 在编码器轮中找到带有扁平边的轴孔。

  3. 电机上的轴形状与这个孔相匹配。

  4. 将轴孔与电机的轴对齐,与电线在同一侧,轻轻推入。它应该有一点摩擦。对另一个电机重复此操作。

现在,你应该有两个带有编码器轮的电机,它们的轮子与电线在同一侧。接下来,我们将电机支架安装到机器人上。

安装电机支架

图中展示的激光切割底盘通常使用两种类型的电机支架。你应该使用与你所拥有的类型最相似的部件。

安装塑料电机支架

重要提示

如果你使用的是金属类型,则跳过此部分。

要安装塑料类型的支架,首先寻找将其安装的槽位,如图 6.23 所示:

![图 6.23 – 塑料电机支架]

![图 B15660_06_23.jpg]

图 6.23 – 塑料电机支架

要安装这些,请按照以下步骤操作:

  1. 箭头指向槽位。将塑料支架推过槽位。

  2. 将电机推到支架上。注意电线和编码器轮朝向内部。编码器应该位于底盘主体上的一个凹槽中。

  3. 底盘上有一个槽位用于安装外置支架,将电机夹在中间。将另一个支架推入这个槽位。

  4. 从外侧将长螺丝推入。

  5. 然后,将螺母推到螺丝上,并使用螺丝刀将其拧紧。

  6. 对于离底盘最近的螺母,其扁平的一边应该在你拧紧螺丝时将其固定在位。对于外部的螺母,请使用扳手或钳子。

  7. 你需要为另一侧重复相同的步骤。

本节已涵盖使用塑料支架将电机安装到底盘上的过程。如果你有金属支架,你应该使用以下部分:

安装金属电机支架

如果你已经组装了塑料电机支架,请跳过此部分。金属类型的支架略有不同;图 6.24 展示了其组装过程:

![图 6.24 – 组装金属电机支架]

![图 B15660_06_24.jpg]

图 6.24 – 组装金属电机支架

要这样做,请执行以下步骤:

  1. 你应该能够在支架顶部看到两个小螺丝孔;这些是螺纹的。每个支架有两个短螺丝。

  2. 底盘在轮子支架区域有孔,与这些孔相匹配。将它们对齐,然后将短螺丝从底盘孔中穿过,拧入支架。

  3. 拿起电机,确保电线远离你。将长螺丝从电机上的两个孔中推入。

  4. 然后,将这个电机组件推入支架侧面的孔中。

  5. 它应该这样安装进去。

  6. 现在,将螺母推到支架另一端的螺纹上。

  7. 你可以用钳子、扳手和螺丝刀拧紧离车架最远的螺母。较近的螺母卡在一边的平面上,所以你只需要螺丝刀。

  8. 你现在有了完成的组装,需要为另一边重复这些步骤。

完成任意一套步骤后,你现在应该在每一边都安装了一个电机。我们将使用这些作为驱动轮。但首先,我们需要万向轮。

添加万向轮

接下来,是安装万向轮的时候了。万向轮平衡机器人。它不是被驱动的,所以会被机器人拖着走。重要的是它要有很小的摩擦。图6.25以及随后的步骤将教你如何完成这项工作:

![Figure 6.25 – 安装万向轮

![img/B15660_06_25.jpg]

图6.25 – 安装万向轮

使用这些步骤与图6.25一起:

  1. 这就是万向轮。它有四个螺丝孔。

  2. 你需要将一个金属螺丝推过孔,使螺纹朝向远离轮子的方向。

  3. 现在,将一个黄铜支架拧入这个螺丝中。

  4. 为其他四个侧面重复此操作。

  5. 将支架的另一侧与车架上的四个孔对齐。注意,这个万向轮是矩形,不是正方形。确保轮子朝下。

  6. 将一个螺丝推过并拧紧。

  7. 我建议你拧紧对角线的角落。

  8. 这样使得剩下的两个螺丝更容易安装。

  9. 万向轮现在应该像这样固定在机器人上。

安装了万向轮后,机器人可以保持平衡,但它需要轮子才能移动,所以让我们添加轮子。

安装轮子

轮子现在需要按照图6.26所示推上去:

![Figure 6.26 – 安装轮子

![img/B15660_06_26.jpg]

图6.26 – 安装轮子

按照这些步骤进行:

  1. 首先,注意它们在轴孔中有两个扁平侧面,就像编码器轮一样。

  2. 将轮子与轴对齐,考虑到平边,并将它们推上去。不要推电线或编码器盘,因为它们可能会损坏。

  3. 有时,旋转轮子直到它们推入会有帮助。你应该能够将轮子推上去,同时确保从另一侧支撑电机。完成此操作后,你可能想要重新调整编码器轮与它们的槽位。

轮子已经安装在电机上,机器人开始成形。现在,机器人应该能够坐在三个轮子上。你可以手动滚动它,但它还不太能自己驱动。

拉起电线

车架组装的最后一个小步骤是将电线拉起来。电机控制器将在机器人的顶部,所以电线也需要在车架之上:

![Figure 6.27 – 拉起电线

![img/B15660_06_27.jpg]

图6.27 – 拉起电线

按照图6.27中的步骤进行:

  1. 首先,收集一个电机的两根线。找到底盘中间的小槽。

  2. 将电线推过去。

  3. 轻轻地将它们拉到底盘顶部,使它们像图中所示那样突出。为另一个电机重复此操作。

现在我们应该有一个看起来像图6.28(电机支架各异)的机器人:

![图6.28 – 组装好的底盘

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_06_28.jpg)

图6.28 – 组装好的底盘

电机就位,轮子准备就绪,我们可以看到机器人将如何移动。你已经建成了机器人的机械部分。电线已经到位,我们现在准备添加电子设备。我们将从添加中央控制器,即Raspberry Pi开始。

安装Raspberry Pi

我们现在不会安装电机控制器——我们将在下一章中解决这个问题,但现在我们可以安装Raspberry Pi并准备将其连接到其他板子上。我们需要在Pi上放置支撑螺母,以便将其固定到底盘上,但要留出电机支架安装的空间,以及稍后放置在Pi下面的传感器:

![图6.29 – 安装Raspberry Pi

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_06_29.jpg)

图6.29 – 安装Raspberry Pi

按照图6.29中所示进行操作:

  1. 你需要一个小的posidrive螺丝刀,一个小扳手或一把钳子,4 x M2.5 5mm螺丝,4 x M2.5 8mm带螺纹的支撑螺母,4 x M2.5 12mm支撑螺母,以及Raspberry Pi。

  2. 将8毫米的支撑螺母螺纹从Pi的底部穿过螺丝孔推上去。

  3. 然后,使用钳子/扳手将10毫米的支撑螺母拧到这些螺母的顶部,使螺纹向上,并用钳子/扳手固定支撑螺母。

  4. 为所有四个角落重复此操作。

  5. 将这两条线与底盘上的槽或螺丝孔对齐,并从下面拧紧。

  6. 在我使用的底盘上,只有两个孔对齐,所以我拧紧了这些孔,并使用其他支撑螺母保持Pi的水平。

这个机器人现在有一个主控制器,它将能够运行代码并控制机器人。然而,在它能够做很多事情之前,控制器和电机需要电力。

添加电池

你购买了两套电池:4 x AA电池夹,如图6.14(配有一套可充电金属氢化物电池),以及一个USB充电宝,如图6.13。充电宝包含一个锂离子电池和一个USB充电系统。

我们将这些安装到机器人的后面,它们将平衡我们稍后要添加的一些传感器。

设置USB充电宝

还未将充电宝连接到Raspberry Pi(或者如果你已经连接了,在拔出电源线之前,请确保登录并正确关闭它):

![图6.30 – 安装充电宝

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_06_30.jpg)

图6.30 – 安装充电宝

要将USB充电宝安装到底盘上,请按照以下说明和图6.30进行操作:

  1. 对于这个充电宝,我们使用了一些钩子和粘带。

  2. 看一下移动电源,并注意其中一侧有USB连接器。这个连接器最终应该位于机器人的左侧。如果它有一个LED充电显示,那么它应该位于顶部。

  3. 测量两段钩扣带。将粗糙的一面粘贴在机器人上的两条条带上。

  4. 将柔软的一面粘贴到移动电源上,并与机器人对齐。

  5. 将电源推下,使钩扣带粘在一起。这个连接很牢固。

  6. 这就是移动电源在机器人上的放置方式。

替代方案包括使用粘性胶带(用于便宜但脆弱的连接)、电缆扎带、双面胶带或橡皮筋来固定电池。这些适用于不同尺寸的电池。

安装AA电池托架

我们还将使用钩扣带添加一个AA电池托架作为电机电源。我们需要托架易于拆卸,以便更换电池。钩扣带是这种应用的便捷选择。添加AA电池托架如图图6.31所示:

![图6.31 – 安装AA电池托架

![img/B15660_06_31.jpg]

图6.31 – 安装AA电池托架

要安装AA电池托架,请使用图6.31并按照以下步骤操作:

  1. 确保AA电池托架中还没有电池。

  2. 在电池托架底部剪下一小条钩扣带,并将其粘贴上。

  3. 将另一条钩扣带粘贴到机器人上,正好位于移动电源前方(为了清晰起见已移除 – 你不需要移除移动电源)。

  4. 使用钩扣带将电池托架固定在这里。

在紧急情况下,可以使用粘性胶带,但请记住,AA电池盒需要可拆卸的,以便更换其中的电池。

完成的机器人底盘

现在,你已经完成了机器人底盘,它应该看起来像图6.32

![图6.32 – 完成的底盘

![img/B15660_06_32.jpg]

图6.32 – 完成的底盘

现在,你已经搭建了你的第一个机器人底盘!希望这是许多中的第一个。随着机器人底盘的完成,配备了轮子、Raspberry Pi和电池仓,它几乎可以滚动了。它还需要连接到电机控制器和一些代码来真正地活跃起来。

将电机连接到Raspberry Pi

在本节中,我们将连接电机到Raspberry Pi。一旦连接好,我们就可以使用Raspberry Pi上的代码来控制电机,使机器人移动。图6.33是我们在本章中构建的机器人的框图。我们将使用全功能步进电机HAT作为控制器板,简称Motor HAT。

这个框图类似于在第3章中显示的类型,探索Raspberry Pi。首先,它从Raspberry Pi开始,这里用灰色表示,因为我们选择了它作为我们的控制器。Pi连接到Motor HAT,指令从Raspberry Pi流向该板。Motor HAT及其连接在本章中添加这些部件时被突出显示。在后续章节中构建这个框图时,现有组件将以灰色显示。添加的组件将以红色突出显示,以显示新内容。最后,两个电机被添加到Motor HAT的左右两侧,箭头从Motor HAT指向,表示它正在控制电机:

![图6.33 – 机器人框图

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_06_33.jpg)

图6.33 – 机器人框图

连接电机的第一步是将Motor HAT安装到Raspberry Pi上。图6.34显示了Motor HAT:

![图6.34 – 全功能步进电机HAT

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_06_34.jpg)

图6.34 – 全功能步进电机HAT

让我们将这个HAT连接到我们的机器人上,并布线以便我们可以开始编程我们的机器人:

图6.35 – 安装电机控制器

参考图6.35,按照以下步骤操作:

  1. 将电机板插座与Pi引脚对齐。角落的四个孔也应该与向上的螺丝螺纹对齐。

  2. 轻轻且均匀地将电机板推到Raspberry Pi上,引导螺丝螺纹通过。继续直到板子牢固地安装在GPIO引脚上。

  3. 机器人现在应该看起来像这样。

现在板子已经连接到Raspberry Pi上,我们可以开始布线了。

布线电机HAT

我们现在将布线Motor HAT,首先连接到电机,也部分连接到电机电池。我们布线电机电池,以便电机有自己的电源,不会在Raspberry Pi上引起低功耗复位条件。电机控制器需要连接到电机以供电和控制它们。

图6.36显示了如何布线。在我们准备好供电之前,不要布线电池上的地线(黑色)。我建议使用一小段绝缘胶带将地线的尖端粘在底盘的塑料部分上,以免钩住任何东西。不布线地线让我们可以用它作为一种临时开关:

重要提示

电池上的黑色线可能被称为地线、GND或负极。红色线可以被称为正极+ve)、vIn电压输入)或电压输入额定值,例如,5 V – 12 V。

图6.36 – 如何连接电机和电池

图6.36 – 如何连接电机和电池

图6.37显示了连接步骤:

图6.37 – 连接电线步骤

参考图6.37执行以下步骤以连接电线:

  1. 松开5 V – 12 V连接、GND、两个M2连接器和M1连接器的螺钉端子。

  2. 将AA电池盒中的红色导线推入标记为5 V – 12 V的螺钉端子,确保导线的金属部分(导线芯)位于金属盖形成的槽中。

  3. 紧固螺钉,确保导线不易被拉出。确保导线芯被夹紧,而不是塑料外层(其绝缘层)。

  4. 对电机端子重复操作,连接如图像4所示的连接。

  5. 结果应类似于图像5。

我们现在已将电机连接到控制器,因此它可以驱动它们。我们已部分连接了电池电源,但我们留下了一个连接空闲,这样我们可以将其用作电源开关。让我们尝试启动机器人。

独立电源

到目前为止,尽管我们已经设置了一个无头Raspberry Pi,但我们仍然一直在将其插入墙壁。现在是我们尝试独立供电的时候了。我们将从AA电池为电机供电,从USB移动电源为Raspberry Pi供电。我们将看到设备上的灯光,以告诉我们它们正在供电:

图6.38 – 转向独立电源

按照图6.38中所示步骤操作:

  1. 将电缆的Micro USB(小)端插入Pi上由箭头指示的USB微型插座。

  2. 安装四个AA电池;你可能需要在安装后再次抬起并放下电池盒。

  3. 你现在可以给电机板供电。将电池盒中的黑色导线连接到箭头旁边的GND端子,位于5 V – 12 V旁边。当你这样做时,电机板上会出现一个灯,表明它处于活动状态。

  4. 通过将USB A(宽)端插入移动电源来打开Pi。从现在起,目的是保持micro-USB小端连接,仅在给Pi供电时连接/断开USB A(宽)端。

  5. 如图像5所示,Raspberry Pi和电机板现在已通电。

恭喜,你的机器人现在已使用独立电源运行,使Raspberry Pi摆脱了墙壁,并为电机供电:

图6.39 – 完整的第6章机器人

图6.39中的照片显示了到目前为止的我们的机器人。它底盘上有电机,安装了Raspberry Pi和电机控制板。机器人有一个为Raspberry Pi供电的电源 - 目前已通电。它还有一个为电机供电的电源,目前未连接,黑色地线被仔细地用胶带固定在机器人其他部分之外。电机已连接到控制板上。这已经足够让机器人开始移动了。

重要提示

SD卡在未关闭Pi的情况下断电可能会被损坏。关闭时,使用PuTTY登录并使用sudo poweroff在断电之前。

你的电机已经准备好驱动,树莓派也准备好了运行代码,无需连接到墙壁。将独立电源与无头Wi-Fi控制相结合意味着机器人可以通过来自电脑的指令进行驾驶。

摘要

在本章中,你现在已经学会了如何通过推理和做出一些重要的设计决策来选择机器人的部件。你使用了一个简单的工具来测试适配这些部件,在购买任何东西之前看看哪些是可行的。最后,你购买了部件并构建了你的起始机器人平台。

通过考虑权衡和再次测试适配,你已经获得了规划任何硬件项目的技能,包括在数据表/供应商网站上查找尺寸、制作一个简单的测试适配草图,以及考虑部件如何相互作用。你已经了解到机器人的大小如何影响电机和控制器的选择。你看到了如何使用搭扣和粘带使部件易于拆卸,并考虑了其他选项。

通过连接独立电源和电机,机器人拥有了在没有连接到墙壁的情况下自行移动所需的硬件。但它还没有任何移动的代码。在下一章中,我们将开始编写代码使这个机器人移动!

练习

  • 第二章《探索机器人构建模块——代码与电子》中,你为不同的机器人创建了框图。考虑它需要什么底盘、电源和部件。使用在线供应商寻找合适的部件。

  • 考虑是否有适合你设计的树莓派帽或罩的组合。使用资源如 https://pinout.xyz 检查它们的引脚使用是否兼容。

  • 对于你的新设计,哪些电源系统可能是合适的?

  • 是否有需要易于拆卸的组件?你将如何处理?你能想出替代搭扣和粘带的方案吗?请保持简单。

  • 为你的新机器人部件制作一个测试适配草图,检查它们是否使用部件尺寸适配。

进一步阅读

请参考以下信息以获取更多信息:

  • 关于底盘设计的进一步阅读,可以考虑《树莓派机器人蓝图》理查德·格里姆特博士Packt 出版公司。这包括将遥控车改装成机器人。

  • 对于更多机器人底盘类型,社区分享网站 https://www.instructables.com 有许多可构建的示例。其中一些非常有趣,比我们的机器人更先进。

第九章:第7章:驱动和转向 – 使用Python移动电机

在本章中,我们将继续构建上一章开始构建的机器人,将电机连接到树莓派,并构建Python代码来使它们移动。我们将介绍编程技术,在物理机器人和其行为代码之间创建一个层,以减少硬件变化的影响。我们的代码和构建将使机器人移动!我们通过编程机器人驾驶一个小路径来结束。机器人代码层将作为我们所有机器人行为的基础,而设定的路径将展示如何使用它。

本章我们将涵盖以下主题:

  • 编写代码来测试你的电机

  • 驾驶机器人

  • 创建一个Robot对象——我们的实验代码与机器人通信

  • 编写一个脚本以遵循预定的路径

技术要求

要完成本章的实验,你需要以下条件:

  • 一台可以访问互联网的电脑

  • 第6章构建机器人基础 – 轮子、电源和布线中构建的底盘

  • 第6章构建机器人基础 – 轮子、电源和布线中购买的电机控制器

  • 为机器人驾驶提供一个2米乘2米的平坦空间

    重要提示

    如果你使用桌子,请准备好停止你的机器人从边缘驶过!最好使用地板。

查看以下视频以查看代码的实际操作:https://bit.ly/39sHxWL

编写代码来测试你的电机

在我们开始使用电机做一些花哨的事情之前,我们需要设置并测试它们。这样,我们可以确保它们工作并消除任何问题。

我们需要下载库来与我们所选择的电机板一起工作。许多机器人部件,除了最简单的部件外,都有一个接口库来控制板上的电机和其他设备。现在是时候再次使用PuTTY登录到你的Pi了。

准备库

我们使用树莓派上的Git从GitHub上的一个项目下载此代码。因此,我们需要在Pi上安装Git;我们还需要I2C(i2c-toolspython3-smbus)和pip来将东西安装到Python中。输入以下命令:

pi@myrobot:~ $ sudo apt-get install -y git python3-pip python3-smbus i2c-tools

为了获取电机板库Raspi_MotorHAT,我们使用Git从GitHub下载它,并使用以下命令将其安装以供任何脚本使用:

pi@myrobot:~ $ pip3 install git+https://github.com/orionrobots/Raspi_MotorHAT
Collecting git+https://github.com/orionrobots/Raspi_MotorHAT
  Cloning https://github.com/orionrobots/Raspi_MotorHAT to /tmp/pip-c3sFoy-build
Installing collected packages: Raspi-MotorHAT
  Running setup.py install for Raspi-MotorHAT ... done
Successfully installed Raspi-MotorHAT-0.0.2

现在我们已经准备好了启动机器人的库。Raspi_MotorHAT库的文档很少,但可以在https://github.com/orionrobots/Raspi_MotorHAT找到,以及使用它的示例。

测试 – 找到电机板

树莓派使用I2C连接到这个电机板。raspi-config再次。我们在这里还启用了串行外设接口SPI)。我们可能需要它来连接其他板和传感器。输入以下命令:

$ sudo raspi-config

现在,我们使用此处的接口设置。图7.1显示了如何进行,如下所示:

![图7.1 – 使用raspi-config启用SPI和I2C

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_07_01.jpg)

图7.1 – 使用raspi-config启用SPI和I2C

参考图7.1中的截图并执行以下步骤:

  1. 首先,选择接口选项

  2. 接下来,选择I2C

  3. Pi会询问您是否想要启用此接口。选择<是>

  4. 然后,您将被带回到初始屏幕,需要再次导航到接口选项屏幕。从那里,选择SPI<是>

  5. 一个确认屏幕告诉您现在SPI已被启用。选择<确定>

  6. 最后,按两次Esc以完成raspi-config。它会询问您是否想要重新启动。选择sudo reboot以重新启动它。

使用I2C,我们需要一种方法来选择我们正在与之通信的设备。就像沿着道路的房子一样,一个地址允许我们说出我们具体想要哪一个。

我们应该检查树莓派是否可以通过以下代码使用sudo i2cdetect -y 1看到电机HAT:

pi@myrobot:~ $ sudo i2cdetect -y 1
     0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 6f
70: 70 -- -- -- -- -- -- --

这扫描I2C总线1上连接到我们的树莓派的设备。如果找到,会在地址上显示数字。地址6f70上的设备是我们的电机控制器。如果您看不到这个,请关闭树莓派的电源,并仔细检查电机HAT是否已正确插入,然后再次尝试。

地址是十六进制,其中每个数字计到16,使用数字0-9,然后是字母A-F,而不是只计算10。在代码中使用时,这些数字前面有一个0x前缀。这是一个然后是一个小写的x。

我们已经启用了I2C(和SPI)总线,然后我们使用了i2cdetect工具来找到我们的电机设备。这首先确认它已连接并响应,其次确认我们有了正确的地址—0x6f。我们现在可以开始向它发送命令了。

测试 – 展示电机移动

我们需要一个测试文件来演示电机是否工作。执行以下步骤:

  1. 创建以下文件,命名为test_motors.py

    from Raspi_MotorHAT import Raspi_MotorHAT
    import time
    import atexit
    mh = Raspi_MotorHAT(addr=0x6f)
    lm = mh.getMotor(1)
    rm = mh.getMotor(2)
    def turn_off_motors():
      lm.run(Raspi_MotorHAT.RELEASE)
      rm.run(Raspi_MotorHAT.RELEASE)
    atexit.register(turn_off_motors)
    lm.setSpeed(150)
    rm.setSpeed(150)
    lm.run(Raspi_MotorHAT.FORWARD)
    rm.run(Raspi_MotorHAT.FORWARD)
    time.sleep(1)
    
  2. 使用在第5章中找到的方法将此文件上传到您的树莓派,使用Git和SD卡副本备份代码

    重要提示

    将您的机器人从您的桌子移到地板上,进行下一步,因为当它移动时,它可能不会按照您预期的方向移动!

  3. 要运行此代码,通过Pi上的PuTTY,键入以下内容:

    pi@myrobot:~ $ python3 test_motors.py
    

您的机器人现在应该大致向前行驶。它可能稍微向一侧移动,但它不应该转弯或后退,并且两个电机都应该在移动。

故障排除

如果您看到任何问题,请尝试以下故障排除图表并返回:

到目前为止,您应该有一个能够向前行驶的机器人,已经看到它移动,并处理了前面的故障排除问题。

理解代码是如何工作的

现在,我们的电机正在移动,机器人使用test_motors.py代码进行驱动。但我们的电机测试代码实际上是如何工作的呢?在本节中,让我们仔细看看并理解这一点。

这里代码的前几行是导入:

from Raspi_MotorHAT import Raspi_MotorHAT
import time
import atexit

导入是Python代码如何拉入其他代码库以供使用的方式。Raspi_MotorHAT库是我们为了与电机交互而安装的库。time库允许我们处理时间;在这种情况下,我们用它来在启动和停止电机之间设置延迟。atexit库允许我们在文件退出时运行代码。

在以下几行中,我们将库连接到电机帽和我们所连接的两个电机:

mh = Raspi_MotorHAT(addr=0x6f)
lm = mh.getMotor(1)
rm = mh.getMotor(2)

这里的第一行创建了一个带有I2C地址0x6fRaspi_MotorHAT对象,这个地址我们在扫描中看到了。我们将返回的对象称为mh,作为连接的Raspi_MotorHAT的简称。

我们然后创建快捷方式来访问电机:lm代表左电机,rm代表右电机。我们从mh对象中获取这些电机控制,使用板上显示的电机编号。电机1是左边的,电机2是右边的。

我们现在定义一个函数,turn_off_motors,它在这个板上的每个电机上运行Raspi_MotorHAT.RELEASE——这是一个使电机停止的指令,如下面的代码片段所示:

def turn_off_motors():
  lm.run(Raspi_MotorHAT.RELEASE)
  rm.run(Raspi_MotorHAT.RELEASE)
atexit.register(turn_off_motors)

我们将这个传递给atexit.register(turn_off_motors),这是一个在文件结束时运行(当Python退出时)的命令。atexit即使在出现错误时也会运行。如果没有这个,代码可能会以一种有趣的方式崩溃,机器人会继续驱动。没有这种保护的机器人有从桌子上开走并撞到墙的习惯。如果它们在电机卡住时继续尝试驱动,可能会损坏电机、电机控制器和电池,所以最好是停止。

这个控制器/库的电机速度范围从0255。我们的代码将每个电机的速度设置为略高于一半的速度,然后运行Raspi_MotorHAT.FORWARD模式,这使得每个电机向前驱动,如下面的代码片段所示:

lm.setSpeed(150)
rm.setSpeed(150)
lm.run(Raspi_MotorHAT.FORWARD)
rm.run(Raspi_MotorHAT.FORWARD)

最后,我们让代码等待1秒钟,如下所示:

time.sleep(1)

sleep允许电机以正向驱动模式运行1秒钟。然后程序退出。由于我们告诉它在代码退出时停止电机,所以电机停止。

我们现在已经编写并理解了测试电机的代码。你也看到了它运行的情况。这证实了你有一个可行的机器人,并且你已经开始使用Python导入。你学习了使用atexit技巧来关闭事物,以及如何使用计时器,这样机器人在退出前有足够的时间运行。现在,我们来看看我们如何控制机器人。

控制机器人转向

现在,我们已经让机器人向前行驶。但我们是怎样控制它的?它是如何左转或右转的?为了理解这一点,我们首先需要了解一些重要的转向形式。让我们看看一些,确定我们的机器人使用的那个,并编写一些测试代码来演示它。

转向类型

驾驶轮式车辆(包括机器人)的最常见技术分为两大类——可转向轮和固定轮,如以下小节所述。每一类都带有几个稍微不寻常的变体。

可转向轮

在可移动轮设计中,机器人中的一个或多个轮子与其他轮子朝向不同的方向。当机器人行驶时,不同位置的轮子使机器人转向。机器人的可移动轮转向有两种常见风格,如图7.2所示:

![图7.2 – 可转向轮类型

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_07_02.jpg)

图7.2 – 可转向轮类型

绿色箭头显示移动方向。白色箭头显示机器人形状的变化和车轮的角度。通过图7.2,我们可以注意以下几点:

  1. 汽车通常使用齿条和齿轮转向。当直线行驶时,汽车会向前行驶。

  2. 当下方的杆被移动时,由白色箭头所示,汽车会转向。

  3. 另一种常见类型是火车式转向,用于家用赛车卡丁车。当直线行驶时,它会向前行驶。

  4. 通过转动前杆,你可以转向车辆。

除了我们之前讨论的变体之外,还有其他变体。它们包括以下内容:

  • 能够独立重新定位每个轮子并侧向行驶的机器人

  • 阿克曼转向,其中每个轮子旋转的角度不同

  • 后轮转向,其中前轮组和后轮组转向——用于长车辆

火车式转向的一个很好的例子是Unotron机器人,如图7.3所示。这是我的儿子用4tronix的Unotron底盘和Arduino Nano控制器制作的:

![图7.3 – 火车式转向Unotron机器人

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_07_03.jpg)

图7.3 – 火车式转向Unotron机器人

在Unotron设计中,有一个由电机驱动的单轮位于后方(在电机控制器下方)。伺服电机转动整个前板,从而转向两个前轮。

这种类型转向的缺点与空间、重量和复杂性有关。为可移动轮转向设置的底盘需要更多的活动部件和空间来容纳它们。Unotron是最简单的。其他设计中存在更多复杂性,这可能导致需要维护。

实现转向(称为转向半径)或对于具有可转向轮系统的机器人所需的距离更长,因为这些必须向前/向后行驶以转向。

你需要一个大型电机用于固定轴,因为你不能在两个电机之间分配动力,或者你需要复杂的机构来平衡输入。如果机构在转向后没有居中,那么机器人就会偏离方向。

固定轮子

固定转向轮在机器人中经常使用,其中车轮的轴线相对于底盘是固定的。每个车轮或一组车轮的相对速度决定了机器人的方向。也就是说,车轮不会从一侧转向另一侧;然而,通过一侧比另一侧更快,机器人可以转弯。这种典型用途被称为滑移转向,如下面的截图所示:

![图7.4 – 固定转向轮或滑移转向

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_07_04.jpg)

图7.4 – 固定转向轮或滑移转向

图7.4 展示了这一动作。白色箭头显示了电机的相对速度。绿色箭头显示了机器人的方向。

在前面的图中,我们可以看到以下内容:

  1. 电机以相同的速度运行,因此机器人正在直线前进。

  2. 右侧的电机转速快;左侧的电机转速慢。这个机器人正在向前和向左行驶。

这有几个优点。如果你打算使用坦克履带,你需要这种驱动系统。它在机械上很简单,因为每个车轮只需要一个驱动电机就可以转弯。滑移转向允许机器人原地转弯,在机器人最宽/最长部分的宽度内完成360度的转弯。

使用这种方法有一些缺点。当转向时,滑移转向系统可能会将车轮向侧面拖动,造成摩擦。此外,电机、它们的齿轮或控制器输出的任何微小差异都可能导致偏航。

其他转向系统

我们在机器人上使用的控制器允许我们控制四个电机通道。建造者可以使用四个电机来驱动特殊类型的轮子,称为麦轮。这些轮子允许滑移转向风格的运动以及蟹行运动,从而使机器人可以不转弯地左右行驶。从技术上讲,这仍然是固定转向轮。本图7.5中展示了带有麦轮的底盘:

![图7.5 – 由Gwpcmu提供的Uranus Pod上的麦轮 [CC BY 3.0

(https://creativecommons.org/licenses/by/3.0)]

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_07_05.jpg)

图7.5 – 由Gwpcmu提供的Uranus Pod上的麦轮 [CC BY 3.0 (https://creativecommons.org/licenses/by/3.0)]

这些轮子非常灵活,但机械结构复杂,维护成本高,重量大,而且比普通轮子贵一些。然而,它们很有趣。

控制我们正在构建的机器人转向

基于我们选择的三个轮子底盘,其中有一个万向轮和每侧一个驱动轮,独立控制,我们正在使用滑移转向。通过改变这些轮子的速度和方向,我们控制我们的机器人。我们还可以用它旋转360度。万向轮抵消了在四轮和六轮滑移转向机器人上看到的拖动问题。

我们可以通过对之前的代码进行一次更改,使机器人原地旋转。让一个电机后退而另一个电机前进会使机器人旋转。让我们看看如何做到这一点,如下所示:

  1. test_motors.py 文件中找到以下行:

    lm.run(Raspi_MotorHAT.FORWARD)
    rm.run(Raspi_MotorHAT.FORWARD)
    
  2. 按照以下方式修改,以便一个电机向BACKWARD方向行驶:

    lm.run(Raspi_MotorHAT.FORWARD)
    rm.run(Raspi_MotorHAT.BACKWARD)
    
  3. 使用python3 turn_motors.py在Pi上运行此代码,现在你的机器人会向右旋转。交换它们,使左(lm)为BACKWARD,右(rm)为FORWARD,它会向相反方向旋转。

  4. 关于不那么剧烈的转向,在前面的代码中,在方向行之前,我们也设置了每个电机的速度,如下所示:

    lm and rm modes to FORWARD, and then making one of the speeds smaller than the other, like this:
    
    

    lm.setSpeed(100)

    rm.setSpeed(150)

    lm.run(Raspi_MotorHAT.FORWARD)

    rm.run(Raspi_MotorHAT.FORWARD)

    
    This code makes the robot drive forward and turn gently to the left.
    

你现在已经看到了几种控制机器人的方法。基于我们机器人的设计,你已经将其中之一付诸实践,使机器人原地旋转,并且也能向前行驶和转向。在下一节中,我们将将其转化为不同行为可以使用机器人的层。

创建机器人对象 – 我们实验中与机器人通信的代码

现在我们已经看到了如何移动和转向我们的机器人,接下来我们将转向软件层,将一些硬件功能组合起来并将它们从行为中隔离出来。通过行为,我指的是使机器人以某种方式行为的代码,例如跟随线条或避开墙壁。我们为什么想要这种隔离?

当我们选择电机控制器时,我们做出了许多权衡,以找到适合我们项目的方案。当考虑因素改变或我们只是想要构建我们的下一个机器人时,电机控制器可能会改变。虽然控制两个电机的速度和方向是同一种操作,但每个控制器都以略微不同的方式执行。在控制器前面创建一个层让我们可以使用相同的命令,即使它发生了变化。这个层充当机器人功能的门面或接口。

每个控制器都有其特点。在这个控制器中,我们设置了一个运行模式和速度。许多控制器使用0表示停止,但这个控制器使用RELEASE模式,这与速度0略有不同,速度0会保持电机运行。控制器通常使用负数表示向后行驶;这个控制器有一个BACKWARD模式。这个控制器上的速度值从0到255。有些从-128到128,或从0到10。我们可以做的是创建一个具有接口的对象,以隐藏这个控制器特有的怪癖。

为什么创建这个对象?

你设计一个接口,以便你可以与其他代码进行交互。它可以简化或使不同的底层系统更加一致,使它们以相同的方式表现,就像所有提到的电机控制器类型一样。它还提供了一种将代码的不同部分干净地分离成层的方法。不同的层意味着你可以更改代码的一部分,而不会对另一部分造成重大变化,如下面的图所示:

图7.6 – 软件层

图7.6 – 软件层

图7.6 – 软件层

图7.6中,面板1展示了一块代码,其中混合了不同的系统。这很难更改;在这个代码中添加新的行为或交换电机控制器会相当棘手。避免以这种方式混合责任是明智的。

代码块 2 表示两个独立系统之间的交互。它们之间存在一种关系,其中 路径跟踪行为 控制着 机器人硬件设置和控制 代码。

在整本书中,我们编写了许多行为,我们可以重用硬件控制库,偶尔扩展它。毕竟,谁愿意一遍又一遍地写相同的代码?当你扩展并创建新的行为时,你还可以再次使用这一层。

图 7.6 的第二面板中的机器人硬件设置/控制块是我们的 Robot 对象。它是隐藏 全功能步进 HAT 板怪癖的接口。

这个标准接口意味着我们可以在其他机器人上创建一个外观相同的对象,并且我们的行为仍然有效。一些严肃的机器人构建者使用接口来交换真实控制器和机器人模拟,以测试复杂的行为。

我们在机器人对象中放什么?

对象是 Python 中创建接口的构建块。对象有方法——我们可以调用它来执行任务。对象还有成员,即数据片段或对其他对象的引用。

下一节将在 Robot 对象中构建代码,以执行以下操作:

  • 设置 Motor HAT 并将其电机存储为成员:left_motorright_motor

  • 处理 exit 状态。

  • 使用 stop_motors 方法停止电机。

  • 让我们用百分比来表示速度——值为 0 到 100。我们将此映射到控制器想要的值。

  • 模式是特定于这个控制器的。我们的接口使用负值表示向后移动。

  • 在稍后的阶段,Robot 对象可以充当数据总线的守门人,这些总线需要代码对其执行独占锁,以及一些硬件。

  • 我们的接口(因此我们的对象)不包含行为,除了退出时停止的保护措施。

我们将其放入一个名为 robot.py 的文件中,如下所示:

from Raspi_MotorHAT import Raspi_MotorHAT
import atexit
class Robot:
    def __init__(self, motorhat_addr=0x6f):
        # Setup the motorhat with the passed in address
        self._mh = Raspi_MotorHAT(addr=motorhat_addr)
        # get local variable for each motor
        self.left_motor = self._mh.getMotor(1)
        self.right_motor  = self._mh.getMotor(2)
        # ensure the motors get stopped when the code exits
        atexit.register(self.stop_motors)
    def stop_motors(self):
        self.left_motor.run(Raspi_MotorHAT.RELEASE)
        self.right_motor.run(Raspi_MotorHAT.RELEASE)

这个 class 有一个 __init__ 方法,这是一个特殊的方法,用于设置这一层。__init__ 方法将 Raspi_MotorHat 库中 getMotor 方法的输出存储在 left_motorright_motor 成员中。此方法还注册了一个停止系统。我添加了一些注释来说明代码片段的功能。

到目前为止,我们的 Robot 对象已经设置了我们的 Motor HAT,并且有一种停止电机的方法。代码与之前看到的设置代码相同,但结构略有不同。

我们可以在另一个名为 behavior_line.py 的文件中测试这一点,如下代码片段所示:

import robot
from Raspi_MotorHAT import Raspi_MotorHAT
from time import sleep
r = robot.Robot()
r.left_motor.setSpeed(150)
r.right_motor.setSpeed(150)
r.left_motor.run(Raspi_MotorHAT.FORWARD)
r.right_motor.run(Raspi_MotorHAT.FORWARD)
sleep(1)

这首先通过导入我们刚刚创建的 robot.py 文件开始。它向前移动 1 秒然后停止。使用 python3 behavior_line.py 运行。

我们仍然必须设置特定于该板的特定速度(不是 100)。让我们在 robot.py 中修复这个问题(新代码用粗体表示),如下所示:

from Raspi_MotorHAT import Raspi_MotorHAT
import atexit
class Robot(object):
    def __init__(self, motorhat_addr=0x6f):
        self._mh = Raspi_MotorHAT(addr=motorhat_addr)
        self.left_motor = self._mh.getMotor(1)
        self.right_motor  = self._mh.getMotor(2)
        atexit.register(self.stop_motors)
    def convert_speed(self, speed):
        return (speed * 255) // 100
    def stop_motors(self):
        self.left_motor.run(Raspi_MotorHAT.RELEASE)
        self.right_motor.run(Raspi_MotorHAT.RELEASE)

我们现在可以使用 convert_speed 来使用 0 到 100 的速度。这对于这个 Motor HAT 返回 0 到 255 的速度。对于其他电机板,它返回其他值。

我们将速度乘以255,然后除以100。这个公式是将百分比转换为255的分数的一种方法。我们首先乘以,因为我们正在进行整数(整数)数学,用整数除以80/100会得到0,但用(80*255)除以100会得到204。

尽管如此,这段代码仍然难以处理——要使用它,我们需要在behavior_line.py中包含以下内容:

import robot
from Raspi_MotorHAT import Raspi_MotorHAT
from time import sleep
r = robot.Robot()
r.left_motor.setSpeed(r.convert_speed(80))
r.right_motor.setSpeed(r.convert_speed(80))
r.left_motor.run(Raspi_MotorHAT.FORWARD)
r.right_motor.run(Raspi_MotorHAT.FORWARD)
sleep(1)

这仍然使用了Raspi_MotorHAT库的runsetSpeed方法,这些方法仅适用于这块控制板。其他板子的工作方式不同。我们还可以稍微简化一下繁琐的转换。

我们首先修改convert_speed方法。对于机器人来说,使用负值表示电机向后运行可能很方便。我们仍然需要缩放速度,但还需要确定运行模式。

我们需要做以下两件事:

  • 确定速度是高于、低于还是等于零,并为run函数设置模式。

  • setSpeed中移除速度的符号,使其始终为正值。

在速度为零时,我们得到的默认模式是RELEASE或停止。如果速度高于0,我们返回FORWARD模式,如果速度低于0,我们返回BACKWARD模式。

我们可以使用简单的if语句来获取正确的mode。让我们将类中的convert_speed方法替换为返回模式和正值。我已经使用注释来显示这个函数的两个部分。在robot.py中修改如下:

def convert_speed(self, speed):
        # Choose the running mode
        mode = Raspi_MotorHAT.RELEASE
        if speed > 0:
            mode = Raspi_MotorHAT.FORWARD
        elif speed < 0:
            mode = Raspi_MotorHAT.BACKWARD
        # Scale the speed
        output_speed = (abs(speed) * 255) // 100
        return mode, int(output_speed)

我们在速度计算中添加了一个额外的操作:abs(speed)。这个操作返回绝对值,从数字中移除了符号。例如,-80和80都会得到80,这意味着该方法始终有一个正输出。

接下来,我们添加了一些方法,可以直接设置机器人左右电机的速度和方向。这些方法调用convert_speed,并使用从中获得的模式和输出速度来调用Raspi_MotorHAT函数。

然后我们需要更改我们的电机移动方法,以使用这种速度转换,如下所示:

    def set_left(self, speed):
        mode, output_speed = self.convert_speed(speed)
        self.left_motor.setSpeed(output_speed)
        self.left_motor.run(mode)
    def set_right(self, speed):
        mode, output_speed = self.convert_speed(speed)
        self.right_motor.setSpeed(output_speed)
        self.right_motor.run(mode)

因此,对于每个电机,我们从传入的速度中获取模式和输出速度,然后调用setSpeedrun

现在的整个robot.py应该看起来如下:

from Raspi_MotorHAT import Raspi_MotorHAT
import atexit
class Robot:
    def __init__(self, motorhat_addr=0x6f):
        # Setup the motorhat with the passed in address
        self._mh = Raspi_MotorHAT(addr=motorhat_addr)
        # get local variable for each motor
        self.left_motor = self._mh.getMotor(1)
        self.right_motor = self._mh.getMotor(2)
        # ensure the motors get stopped when the code exits
        atexit.register(self.stop_motors)
    def convert_speed(self, speed):
        # Choose the running mode
        mode = Raspi_MotorHAT.RELEASE
        if speed > 0:
            mode = Raspi_MotorHAT.FORWARD
        elif speed < 0:
            mode = Raspi_MotorHAT.BACKWARD
        # Scale the speed
        output_speed = (abs(speed) * 255) // 100
        return mode, int(output_speed)
    def set_left(self, speed):
        mode, output_speed = self.convert_speed(speed)
        self.left_motor.setSpeed(output_speed)
        self.left_motor.run(mode)
    def set_right(self, speed):
        mode, output_speed = self.convert_speed(speed)
        self.right_motor.setSpeed(output_speed)
        self.right_motor.run(mode)

    def stop_motors(self):
        self.left_motor.run(Raspi_MotorHAT.RELEASE)
        self.right_motor.run(Raspi_MotorHAT.RELEASE)

我们在behavior_line.py中的简单行为现在只有几行,如下面的代码片段所示:

import robot
from time import sleep
r = robot.Robot()
r.set_left(80)
r.set_right(80)
sleep(1)

这种简化意味着我们可以在此基础上构建更多行为。我有一个通用接口,以及为我的其他机器人创建的Robot对象版本。一个令人兴奋的结果是,我可以在ArmBot(见第1章机器人简介)或我的其他Raspberry Pi机器人上运行这个behavior_lines.py代码。它们都以80%的电机速度向前行驶1秒。

编写脚本以跟随预定路径

因此,我们现在得到了第一个感觉像机器人的行为。让我们快速绘制一条路径草图,以便我们的机器人能够跟随。例如,请参见这里的图7.7

![图 7.7 – 我们机器人的路径

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_07_07.jpg)

图 7.7 – 我们机器人的路径

图 7.7 中,我绘制了一条路径。直线是用于前进的;1 表示 1 秒。我们还没有考虑行驶距离的方法,只有时间。我们可能能够根据距离猜测时间,但这不是非常精确或可重复的。温柔的曲线是转弯,我们减慢一个电机的速度比另一个电机更多。

最后的螺旋意味着当路径完成时在原地胜利旋转——我们可以通过将一个电机反转而另一个电机前进来实现这一点。

让我们编写这段代码。首先,我们需要导入 sleeprobot。但在我们做任何事情之前,让我们为这种行为创建一些辅助函数。我把我的文件命名为 behavior_path.py,代码如下所示:

import robot
from time import sleep
def straight(bot, seconds):
    bot.set_left(80)
    bot.set_right(80)
    sleep(seconds)
def turn_left(bot, seconds):
    bot.set_left(20)
    bot.set_right(80)
    sleep(seconds)
def turn_right(bot, seconds):
    bot.set_left(80)
    bot.set_right(20)
    sleep(seconds)
def spin_left(bot, seconds):
    bot.set_left(-80)
    bot.set_right(80)
    sleep(seconds)

辅助器使用我们用来描述行为的相同语言。我们有 straightturn_leftturn_rightspin_left。这些不是在 Robot 对象中,因为其他行为可能需要比这更连续的行为。我现在把 Robot 对象称为 bot,因为当代码量增加时,像 r 这样的单字母变量名变得不那么容易找到、阅读或推理。

这些辅助器分别设置电机速度,然后睡眠一定数量的秒数。然后我们可以创建 Robot 对象,并通过向 behavior_path.py 添加以下代码来对它们进行排序:

bot = robot.Robot()
straight(bot, 1)
turn_right(bot, 1)
straight(bot, 1)
turn_left(bot, 1)
straight(bot, 1)
turn_left(bot, 1)
straight(bot, 1)
spin_left(bot, 1)

现在,我们可以将其上传到 Raspberry Pi,并通过 PuTTY 运行,如下所示:

$ python3 behavior_path.py

现在,如果你的机器人像我的一样,你看到它行驶并转弯,但转弯以某种方式超出了范围,机器人可能向一侧偏航。我们可以通过减少转弯步骤中的时间来解决这个问题,如下所示:

bot = robot.Robot()
straight(bot, 1)
turn_right(bot, 0.6)
straight(bot, 1)
turn_left(bot, 0.6)
straight(bot, 1)
turn_left(bot, 0.6)
straight(bot, 1)
spin_left(bot, 1)

你需要调整这些值以接近 90 度的转弯。这种调整需要耐心:更改它们并上传。在代码中调整值是一种粗略的校准形式,以匹配我们机器人的特性。如果你在表面之间移动(例如,从木地板到地毯),那么时间将发生变化。

你可能能够通过调整一个电机在 straight 函数中的速度(根据你自己的机器人偏航进行调整)来解释一些偏航,如下所示:

def straight(bot, seconds):
    bot.set_left(80)
    bot.set_right(70)
    sleep(seconds)

这段代码可以维持一段时间,但可能难以微调。为什么我们会得到这种偏航?

电机速度可能不同,即使是同一制造商的产品。其他引起微小变化的原因包括轮径、轴位置、重量分布、光滑或不平的表面、导线电阻和电机控制器变化。这种变化使得从这种方式获得一条完美的直线变得不太可能。根据我们使用哪些传感器,这可能是问题,也可能不是问题。为了解决这个问题,我们在后面的章节中引入编码器/速度传感器,并校准这些传感器以获得更精确的路径行为版本。

没有传感器,机器人无法确定自己的位置或是否撞到了任何东西。如果机器人撞到了墙,你可能需要去移动它,让它有足够的空间移动。

摘要

在本章中,我们学习了如何安装电机板的库并演示我们的电机是否工作。然后我们开始构建用于我们行为的第一层代码,同时注意我们如何为其他机器人构建类似层次。我们看到了我们的机器人沿着路径移动,并对其进行了调整,同时发现了使用无传感器的电机的一些不足。

现在,在开始任何硬件项目时,你可以使用这个方法:首先测试电机/输出设备,然后创建一个用于行为的层,以便如果它们的硬件以后发生变化,你只需要更改电机代码。

在接下来的章节中,我们开始添加传感器并使用这些传感器构建行为。

练习

尝试以下想法来增强你对本章内容的理解:

  1. 绘制另一条简单的路径,并为机器人编写代码以跟随它。例如,尝试使用你的经验跟随一个8字形。

  2. 如果你有额外的输出要控制,你会向Robot对象添加哪些方法?也许是一个单独的发光二极管LED)?

  3. 考虑如何为具有卡丁车式转向的机器人布局Robot对象。它会有哪些方法?你不需要立即编写代码,但有一个接口的想法是一个良好的开始。提示——它可能有一个用于驱动的电机速度和一个用于转向的电机位置。

进一步阅读

请参考以下内容以获取更多信息:

  • 关于Robot对象使用的样式以及类似接口和类的使用,我推荐阅读面向对象编程学习指南,作者加斯顿·希尔Packt Publishing。这本书不仅通过Python解释了这些概念,而且更广泛地展示了面向对象OO)概念也适用于C#和JavaScript语言。

第十章:第 8 章:使用 Python 编程距离传感器

在本章中,我们研究距离传感器及其如何用于避免物体。避免障碍物是移动机器人的一项关键特性,因为撞到东西通常不是好事。它也是一种使机器人看起来更智能的行为,好像它正在智能地行动。

在本章中,我们将了解不同类型的传感器,并选择一个合适的类型。然后我们在机器人对象中构建一个层来访问它们,此外,我们创建了一个避免墙壁和物体的行为。

在本章中,你将了解以下主题:

  • 在光学传感器和超声波传感器之间进行选择

  • 安装和读取超声波传感器

  • 避免碰撞 – 编写避免障碍物的脚本

技术要求

要完成本章的动手实验,你需要以下设备:

  • Raspberry Pi 机器人和前几章中的代码。

  • 需要两个 HC-SR04P、RCWL-1601 或 Adafruit 4007 超声波传感器。它们必须具有 3.3 V 的输出。

  • 一个面包板。

  • 22 AWG 单芯线或预切割的面包板跳线套件。

  • 面包板友好的单刀双掷(SPDT)滑动开关。

  • 阳性到阴性的跳线,最好是连接在一起的跳线套件。

  • 两个用于传感器的支架。

  • 一把十字螺丝刀。

  • 小型扳手或小钳子。

本章的代码可在 GitHub 上找到:https://github.com/PacktPublishing/Learn-Robotics-Programming-Second-Edition/tree/master/chapter8

查看以下视频,了解代码的实际应用:https://bit.ly/2KfCkZM

在光学传感器和超声波传感器之间进行选择

在我们开始使用距离传感器之前,让我们了解一下这些传感器实际上是什么,它们是如何工作的,以及一些不同类型的传感器。

感测距离最常见的方法是使用超声波或光。这两种机制的原则是发射一个脉冲,然后感应其反射回波,使用其时间或角度来测量距离,如下面的图所示:

![图 8.1 – 在距离传感器中使用脉冲定时

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_08_01.jpg)

图 8.1 – 在距离传感器中使用脉冲定时

我们专注于测量响应时间,也就是所谓的飞行时间的传感器。图 8.1 展示了这些传感器如何使用反射时间。

通过对传感器工作原理的基本理解,我们现在将更深入地研究光学传感器和超声波传感器。

光学传感器

基于光的传感器,如 图 8.2 中的传感器,使用我们看不见的红外激光光。这些设备可以非常小;然而,在强烈的阳光和荧光灯下,它们可能会表现不佳,导致它们行为异常。一些物体反射光线不好或透明,这些传感器无法检测到:

![图 8.2 – VL530LOx 在载板上的应用

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_08_02.jpg)

图8.2 – VL530LOx在载板上的应用

在使用红外光束检测比赛时间的比赛中,光束和这些传感器可能会相互干扰。然而,与超声波传感器不同,当放置在机器人不同侧面时,这些不太可能导致错误检测。光学距离传感器可以具有更高的精度,但范围更有限。它们可能很昂贵,尽管市场上也有更便宜的固定范围类型的光传感器。

超声波传感器

许多基于声音的距离测量设备使用超声波,其频率超出人类听觉范围,尽管它们可能会打扰一些动物,包括狗。移动电话麦克风和一些相机会将它们的脉冲作为点击声捕捉。超声波设备通常比光学设备大,但由于声音传播速度比光慢且更容易测量,因此它们通常更便宜。对于不反射声音的柔软物体,如织物,这些设备可能更难检测。

图8.3展示了HC-SR04,这是一种常见且价格低廉的基于声音的距离传感器:

![图8.3 – HC-SR04

![图B15660_08_03.jpg]

图8.3 – HC-SR04

它们的范围从大约2厘米的最小值延伸到4米。

有许多基于超声波的设备,包括常见的HC-SR04,但并非所有都适合。我们将探讨逻辑水平,因为这是选择购买哪种传感器的一个重要因素。

逻辑水平和移位

树莓派上的I/O引脚仅适用于3.3 V的输入。市场上许多设备具有5 V的逻辑,无论是控制它们的输入还是它们的输出。让我们深入探讨我所说的逻辑水平,以及为什么在可能的情况下坚持使用原生电压水平是有意义的。

电压是衡量电流量中推动能量的度量。不同的电子设备被设计成能够承受或响应不同的电压水平。通过设备传递过高的电压可能会损坏它。另一方面,传递过低的电压可能导致你的传感器或输出简单地不响应或行为异常。我们处理的是输出高或低电压以表示真/假值的逻辑设备。这些电压必须高于阈值才能为真,低于阈值才能为假。我们必须了解这些电气特性,否则我们可能会损坏设备并无法使它们进行通信。

图8.4中的图表显示了不同级别的影响:

![图8.4 – 电压和逻辑水平

![图B15660_08_04.jpg]

图8.4 – 电压和逻辑水平

图8.4 中,我们展示了一个图表。在 y 轴(左侧),它显示了从0到5 V的电压标签。y 轴显示了不同的操作条件。图表中有4条虚线穿过。最低的虚线在0.8 V处;低于这个值,输入将被视为逻辑0。下一个线,大约在2.3 V处,是许多3.3 V设备认为逻辑1的地方。3.3 V的线显示了树莓派上逻辑1的预期输入和输出电平。在这条线以上,可能会损坏树莓派。大约在4.2 V处是某些5 V设备期望的逻辑1(尽管一些设备可能允许低至2 V) – 树莓派需要帮助才能与这些设备通信。

沿着图表有5个条形。第一个标记的条形在0处 – 意味着对所有设备都是清晰的逻辑0。下一个条形是树莓派的3.3 V清晰的逻辑1,但它也低于4.2 V,因此一些5 V设备可能不会识别这一点。标记为不清晰的条形在1.8 V处 – 在这个低和高阈值之间的区域,逻辑可能不清晰,应该避免。标记为模糊逻辑1的条形高于阈值,但仅略高于,可能会被误解或导致3.3 V设备出现异常结果。最后一个条形在5 V处,这是5 V设备输出的。如果没有电平转换器直接连接到树莓派,这将损坏树莓派。

图8.4 中,有1.7 V和2.3 V的条形。这些电压非常接近逻辑阈值,可能导致从输入随机数据。避免在所需逻辑电平之间的中间电压。3 V是可以的,但避免1.5 V,因为这可能是模糊的。

重要提示

将超过3.3 V的电压放入树莓派引脚会损坏树莓派。不要在没有逻辑电平转换器的情况下使用5 V设备。

如果你使用5 V的设备,你需要额外的电子设备来接口它们。这些电子设备包含更多的布线和部件,从而增加了机器人的电子成本、复杂度或尺寸:

图片

图8.5 – 将HC-SR04传感器连接到电平转换器

图8.5 展示了一个使用需要逻辑电平转换的HC-SR04 5v传感器的机器人布线图。此电路图显示了顶部的树莓派GPIO引脚。从左边的3个引脚来的分别是5 V、3.3 V(写作3v3)和地(GND)线。GPIO引脚下方是3.3 V和5 V线。

在电源线(或轨道)下方是两个电平转换器。进入电平转换器右边的是来自树莓派GPIO引脚5、6、17和27的连接。在这种图表风格中,一个黑点表示一个连接,没有连接的线用桥表示。

图表底部有一条从地线来的地线。这显示为正常,因为额外的电子设备将需要访问地线。

图表的左侧有两个距离传感器,连接到5 V和GND。每个传感器的触发回声引脚都连接到电平转换器。不难看出,添加更多需要电平转换器的传感器将使复杂性进一步增加。

幸运的是,现在有其他选项可供选择。在可以使用3.3 V原生设备或使用其供电电压作为逻辑高电平的设备的情况下,选择这些设备是值得的。在为机器人购买电子产品时,仔细考虑机器人主控制器使用的电压(如树莓派),并确认这些电子产品与控制器的电压兼容。

HC-SR04有几个具有这种能力的替代部件。HC-SR04P、RCWL-1601和Adafruit 4007型号输出3.3 V,可以直接连接到树莓派。

为什么使用两个传感器?

两个传感器允许行为检测哪一侧更近。有了这个,机器人可以检测开放空间的位置并朝它们移动。图8.6显示了这是如何工作的:

![图8.6 – 使用两个传感器

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_08_06.jpg)

图8.6 – 使用两个传感器

图8.6中,第二个机器人可以做出更有趣的决定,因为它有更多来自世界的数据来做出这些决定。

考虑所有这些选项,我建议你使用3.3 V变体,如HC-SR04P/RCWL-1601或Adafruit 4007,因为它们便宜,而且很容易添加两个或更多这样的传感器。

我们已经看到了一些距离传感器的类型,并讨论了为这个机器人所做的权衡和选择。你已经了解了电压级别,以及为什么这是机器人电子设备的一个关键考虑因素。我们还探讨了我们可以使用多少传感器以及我们可以将它们放在哪里。现在让我们看看如何添加它们。

连接和读取超声波传感器

首先,我们应该将这些传感器连接到机器人并固定好。然后,我们编写一些简单的测试代码,我们可以在下一节的行为代码上基于这些代码。完成本节后,机器人框图应该看起来像图8.7

![图8.7 – 带超声波传感器的机器人框图

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_08_07.jpg)

图8.7 – 带超声波传感器的机器人框图

此图基于图6.33的框图,来自第6章,构建机器人基础 – 轮子、电源和布线,通过添加左右超声波传感器。它们都有双向箭头指向树莓派,因为作为主动传感器,树莓派触发传感器测量并读取结果。让我们将传感器连接到机器人底盘。

将传感器固定到机器人上

技术要求部分,我添加了一个HC-SR04支架。虽然可以使用CAD和其他零件制造技能制作定制的支架,但使用库存设计更为合理。图8.8显示了我在使用的支架:

![图8.8 – 带有螺丝和硬件的超声波HC-SR04传感器支架

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_08_08.jpg)

图8.8 – 带有螺丝和硬件的超声波HC-SR04传感器支架

假设你的底盘足够像我的一样,有安装孔或槽来安装这个支架,那么这些很容易附着到你的机器人上:

![图8.9 – 安装传感器支架的步骤

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_08_09.jpg)

图8.9 – 安装传感器支架的步骤

要安装传感器支架,请使用图8.9作为以下步骤的指南:

  1. 将两个螺栓推入支架上的孔中。

  2. 将支架螺丝从机器人前部的孔中穿过。

  3. 从机器人下方穿过一个螺母并拧紧。对另一侧重复此操作。

  4. 安装两个支架后,机器人应该看起来像这样。

    图8.10展示了如何将传感器推入支架:

    ![图8.10 – 将传感器推入支架

    ](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_08_10.jpg)

    图8.10 – 将传感器推入支架

  5. 看看传感器。两个换能器元件,顶部有网布的圆形罐,将很好地适合支架上的孔。

  6. 距离传感器可以直接推入支架,因为它们有摩擦配合。传感器的电气连接器应朝上。

  7. 放入两个传感器后,机器人应该看起来像图8.10的7号面板。

你现在已经将传感器附着到底盘上了。在我们给它们接电线之前,我们将稍微偏离一下,添加一个有用的电源开关。

添加电源开关

在我们再次打开机器人之前,让我们为电机电源添加一个开关。这个开关比反复将电池的接地线拧入端子更方便。我们将看到如何用三个简单步骤来完成这个操作。请跟随:

  1. 确保你准备好了以下设备,如图图8.11所示:一个面包板,一些魔术贴,一个迷你面包板兼容的SPDT开关,以及一根单芯22 AWG线:![图8.11 – 添加电源开关所需的物品

    ](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_08_11.jpg)

    图8.11 – 添加电源开关所需的物品

  2. 现在,使用两块魔术贴将面包板粘贴在机器人电池的顶部,如图图8.12所示。魔术贴固定牢固,但需要拆卸机器人时容易移除:

![图8.12 – 添加魔术贴条

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_08_12.jpg)

图8.12 – 添加魔术贴条

面包板就位后,我们现在可以添加一个开关。

仔细看看图8.13,了解开关的连接细节:

![图8.13 – 连接开关

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_08_13.jpg)

图8.13 – 连接开关

图8.13显示了一个电路图,面包板的特写,以及如何在机器人上物理连接的推荐方式。让我们详细看看:

  1. 这是一个电路图,显示了电池、开关和电机电源输入连接器。顶部是电机电源输入端子。从该端子的正极(+)侧,一根电线沿着左侧下降到电池,显示为交替的粗细条。从电池出发,底部端子是它们的负极。一根电线从这绕到图右侧的开关上。开关的顶部通过一根电线连接到电机电源输入端子的负极(-)。这是制作连接的重要图。

  2. 在我们实际连接开关之前,值得讨论一下面包板的行列。这个面板显示了面包板的一个特写,其中2行用绿色线条突出显示。绿色线条表明这些行列以5个一组连接。面包板的排列有两个5孔(连接点)的连线组,每个行列(编号1至30)都有一个。中间有一个凹槽将它们分开。

  3. 物理布线使用面包板将电线连接到设备。它不会与图精确匹配。左侧显示电机板,从电池的正极(+或VIN)引脚进入,电池位于中间。一根黑色电线从电池进入面包板第3行第d列。在第e列,一个开关插入面包板,跨越第1、2和3行。一根橙色预切割的22 AWG电线从第2行连接到GND端子,并拧紧。滑动这个开关打开机器人电机的电源。

现在我们已经为我们的机器人提供了电机电池的电源开关,因此我们可以打开电机电源而无需螺丝刀。接下来,我们将使用相同的面包板来布线距离传感器。

布线距离传感器

每个超声波传感器有四个连接:

  • 一个触发引脚以请求读取

  • 一个用于检测返回的回声引脚

  • 一个VCC/电压引脚,应为3.3 V

  • 一个GND或地线引脚

在进行任何进一步操作之前,请确保整个机器人已经关闭。触发和回声引脚需要连接到树莓派的GPIO引脚。

图8.14显示了树莓派GPIO端口的特写,以帮助进行连接:

![图8.14 – 树莓派连接

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_08_14.jpg)

图8.14 – 树莓派连接

图8.14是树莓派GPIO连接器的图视图。这个连接器是位于Pi顶部的两行40个引脚集。许多机器人和小工具都使用它们。引脚编号/名称没有打印在树莓派上,但这个图应该有助于找到它们。

我们使用面包板进行这项布线。图8.15显示了所需的连接:

图8.15 – 传感器布线图

从树莓派到面包板,以及从传感器到面包板的线需要公对母跳线。面包板上的线(只有4根)使用短预切割线。图8.15 显示了上面的电路图,下面的面包板布线建议。

要布线传感器,请以图8.15 为指导,并按照以下步骤操作:

  1. 从电源连接开始。一根线从树莓派的3.3 V(通常在图表上写作3v3)引脚连接到面包板上的顶部红色标记轨道。我们可以使用这个红色 轨道 进行需要3.3 V的其他连接。

  2. 树莓派上的一根线连接到面包板上的黑色或蓝色标记的轨道。我们可以使用这个蓝色 轨道 进行需要GND的连接。

  3. 从每侧的公对母跳线中拉出一条4芯的线。

  4. 对于左侧的传感器,识别四个引脚——VCC、trig、echo和GND。从这个到面包板的连接中,保持4根线在一起很有用。从这个传感器取4个公对母连接器(如果可能的话,在一个连接条中),并将它们插入到板上。

  5. 在面包板上,使用预先切割的线将地线连接到蓝色轨道,并将VCC连接到红色轨道。

  6. 现在请使用一些跳线将信号连接从trig/echo引脚到树莓派GPIO引脚。

    重要提示

    根据您放置面包板的位置,距离传感器的线可能无法到达。如果是这种情况,将两个公对母线并排放置,并使用一些电工胶带将它们绑在一起。

为了整洁,我喜欢用螺旋线包裹电线;这完全是可选的,但可以减少机器人上的杂乱。

在继续之前,请务必仔细检查您的连接。您已经将距离传感器安装到机器人的硬件中,但为了测试和使用它们,我们需要准备软件组件。

安装Python库以与传感器通信

要与GPIO传感器和一些其他硬件一起工作,您需要一个Python库。让我们使用GPIOZero库,该库旨在帮助与这种硬件进行接口:

$ pip3 install RPi.GPIO gpiozero

现在库已经安装,我们可以编写测试代码。

读取超声波距离传感器

要为距离传感器编写代码,了解它们的工作原理很有帮助。如前所述,该系统通过从物体上反射声脉冲并测量脉冲返回时间来工作。

树莓派上的代码向触发引脚发送电子脉冲以请求读取。作为对此脉冲的响应,设备发出声脉冲并测量其返回时间。回声引脚也使用脉冲进行响应。这个脉冲的长度对应于声音的传播时间。

图8.16 中的图表显示了这些的时序:

![Figure 8.16 – 超声波距离传感器的脉冲时序和响应

![img/B15660_08_16.jpg]

Figure 8.16 – 超声波距离传感器的脉冲时序和响应

GPIOZero 库可以测量这个脉冲,并将其转换为距离,我们可以在代码中使用它。

如果声音没有很快回声,设备可能无法及时获得返回响应。也许物体超出了传感器的范围,或者有什么东西减弱了声音。

就像我们在之前的伺服电机控制类中所做的那样,我们应该使用注释和描述性名称来帮助我们解释这部分代码。我已经将这个文件命名为 test_distance_sensors.py

  1. 首先,导入 timeDistanceSensor 库:

    import time
    from gpiozero import DistanceSensor
    
  2. 接下来,我们设置传感器。我使用了 print 语句来显示正在发生的事情。在这些行中,我们为每个距离传感器创建库对象,注册我们连接它们的引脚。确保这些与你的接线匹配:

    queue_len parameter. The GPIOZero library tries to collect 30 sensor readings before giving an output, which makes it smoother, but less responsive. And what we'll need for our robot is responsive, so we take it down to 2 readings. A tiny bit of smoothing, but totally responsive.
    
  3. 这个测试将在我们取消它之前循环运行:

    while True:
    
  4. 我们然后打印出传感器的距离。.distance 是一个属性,就像我们在书中早些时候在LED系统上的 .count 属性所看到的那样。传感器会持续更新它。我们将其乘以100,因为 GPIOZero 的距离是以米为单位的:

        print("Left: {l}, Right: {r}".format(
    l=sensor_l.distance * 100, 
            r=sensor_r.distance * 100))
    
  5. 在循环中稍微休息一下可以防止输出过多,并防止紧密循环:

        time.sleep(0.1)
    
  6. 现在,你可以打开你的Raspberry Pi并上传这段代码。

  7. 将物体放在距离传感器4厘米到1米之间的任何地方,如下面的图片所示!

    图8.17 – 带有物体的距离传感器

    图8.17 显示了一个大约10.5厘米远处的传感器上的物体。这个物体是一个小工具箱。重要的是它很坚固,不是布制品。

  8. 使用 python3 test_distance_sensors.py 在Pi上启动代码。当你移动物体时,你的Pi应该开始输出距离:

    pi@myrobot:~ $ python3 test_distance_sensors.py 
    Prepare GPIO Pins
    Left: 6.565688483970461, Right: 10.483658125707734
    Left: 5.200715097982538, Right: 11.58136928065528
    
  9. 因为它在循环中,你需要按 Ctrl + C 来停止程序运行。

  10. 你会看到这里有很多小数位,这在这里并不太有帮助。首先,设备不太可能那么精确,其次,我们的机器人不需要亚厘米级的精度来做出决策。我们可以在循环中修改打印语句,使其更有帮助:

    :.2f changes the way text is output, to state that there are always two decimal places. Because debug output can be essential to see what is going on in the robot, knowing how to refine it is a valuable skill.
    
  11. 使用这个更改运行代码会得到以下输出:

pi@myrobot:~ $ python3 test_distance_sensors.py 
Prepare GPIO Pins
Left: 6.56, Right: 10.48
Left: 5.20, Right: 11.58

你已经证明了距离传感器正在工作。除此之外,我们还探索了如何调整传感器的输出以进行调试,这在制作机器人时你会做得多得多。为了确保你走在正确的道路上,让我们排除任何出现的问题。

故障排除

如果这个传感器没有按预期工作,请尝试以下故障排除步骤:

  • 线路中有什么东西热吗?用拇指和食指捏住线,将其夹在传感器之间。不应该有任何东西是热的,甚至温暖!如果是这样,请取出电池,关闭Raspberry Pi,并彻底检查所有线路与 图8.12 的对比。

  • 如果有语法错误,请将代码与示例进行对比。你应该已经使用 pip3 安装了Python库,并且正在使用 python3 运行。

  • 如果你仍然收到错误或无效的值,请检查代码和缩进。

  • 如果值始终为0,或者传感器没有返回任何值,那么您可能已经交换了触发和回声引脚。尝试在代码中交换触发/回声引脚编号并再次测试。不要在实时 Pi 上交换电缆!一次只对一个设备这样做。

  • 如果您仍然没有获取到任何值,请确保您已经购买了 3.3 V 兼容的系统。HC-SR04 模型与裸 Raspberry Pi 不兼容。

  • 如果值偏离太远或漂移,请确保您正在测试的表面是坚硬的。柔软的表面,如衣物、窗帘或您的手,不如玻璃、木材、金属或塑料反应灵敏。墙壁效果很好!

  • 不正确值的另一个原因是表面可能太小。确保您的表面相当宽。任何小于约 5 平方厘米的物体可能更难测量。

  • 作为最后的手段,如果其中一个传感器看起来正常,而另一个传感器有问题,那么可能是一个设备出现了故障。尝试交换传感器来检查这一点。如果结果不同,那么可能是一个传感器出现了问题。如果结果相同,那么可能是接线或代码出现了问题。

您现在已经对距离传感器进行了故障排除,并确保它正常工作。您已经看到它输出值以表明它正在工作,并使用物体测试了它的响应。现在,让我们提高一步,编写一个避免障碍的脚本。

避免墙壁 – 编写避免障碍的脚本

现在我们已经测试了两个传感器,我们可以将它们集成到我们的机器人类中,并为它们制作避障行为。这个行为循环读取传感器,然后根据结果选择相应的行为。

将传感器添加到机器人类中

因此,在我们可以在行为中使用传感器之前,我们需要将它们添加到Robot类中,为每个侧面分配正确的引脚编号。这样,如果引脚编号更改或传感器的接口更改,行为就不需要更改:

  1. 要使用DistanceSensor对象,我们需要从gpiozero导入它;新的代码以粗体显示:

    from Raspi_MotorHAT import Raspi_MotorHAT
    from gpiozero import DistanceSensor
    
  2. 我们在机器人类中为每个侧面创建一个DistanceSensor对象的实例。我们需要在机器人的构造函数中设置这些。我们使用与测试中相同的引脚编号和队列长度:

    class Robot:
        def __init__(self, motorhat_addr=0x6f):
            # Setup the motorhat with the passed in address
            self._mh = Raspi_MotorHAT(addr=motorhat_addr)
            # get local variable for each motor
            self.left_motor = self._mh.getMotor(1)
            self.right_motor = self._mh.getMotor(2)
            # Setup The Distance Sensors
            self.left_distance_sensor = DistanceSensor(echo=17, trigger=27, queue_len=2)
            self.right_distance_sensor = DistanceSensor(echo=5, trigger=6, queue_len=2)
            # ensure the motors get stopped when the code exits
            atexit.register(self.stop_all)
    

将此添加到我们的机器人层中,使其对行为可用。当我们创建我们的机器人时,传感器将测量距离。让我们创建一个使用它们的动作。

制作避障行为

本章全部关于获取行为;机器人如何驾驶和避免(大多数)障碍?传感器的规格限制了它,对于较小的物体或外壳柔软/毛茸茸的物体,如家具,可能无法检测到。让我们先从我们在图 8.18中想要表达的意思开始画起:

![图 8.18 – 避障基础知识

](https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/lrn-rbt-prog/img/B15660_08_18.jpg)

图 8.18 – 避障基础知识

在我们的例子(图8.18)中,一个基本的机器人检测到墙壁,转向避开,直到检测到另一面墙壁,然后再次避开。我们可以利用这一点来尝试制作我们的第一次避障行为尝试。

首次尝试避障

为了帮助我们理解这个任务,以下图表显示了该行为的流程图:

![图8.19 – 避障流程图

img/B15660_08_19.jpg

图8.19 – 避障流程图

图8.19中的流程图从顶部开始。

此图表描述了一个循环,执行以下操作:

  1. 开始框进入获取距离框,该框获取每个传感器的距离。

  2. 我们测试左侧传感器是否读取的距离小于20厘米(一个合理的阈值):

    a) 如果是,我们将左侧电机设置为反向,使机器人避开障碍物。

    b) 否则,我们驱动左侧电机向前。

  3. 我们现在检查右侧传感器,如果距离小于20厘米,则将其向后设置,否则向前设置。

  4. 程序等待一小段时间,然后再次循环。

我们将此循环放在run方法中。与此相关需要一些小的设置。我们需要将俯仰和倾斜设置为0,以免遮挡传感器。我已经将此代码放在simple_avoid_behavior.py中:

  1. 首先导入机器人,并使用sleep进行计时:

    from robot import Robot
    from time import sleep
    ...
    
  2. 以下类是我们行为的基础。在行为中存储了一个机器人对象。设置了一个速度,可以调整以使机器人行驶更快或更慢。太快,它反应的时间就会减少:

    ...
    class ObstacleAvoidingBehavior:
        """Simple obstacle avoiding"""
        def __init__(self, the_robot):
            self.robot = the_robot
            self.speed = 60
            ...
    
  3. 现在以下方法根据传感器检测到的距离为每个电机选择速度。较近的传感器距离会避开障碍物:

        ...
        def get_motor_speed(self, distance):
            """This method chooses a speed for a motor based on the distance from a sensor"""
            if distance < 0.2:
                return -self.speed
            else:
                return self.speed
        ...
    
  4. run方法是核心,因为它包含主循环。我们将俯仰和倾斜机构放在中间,以免遮挡传感器:

        ...
        def run(self):
            self.robot.set_pan(0)
            self.robot.set_tilt(0)
    
  5. 现在,我们开始主循环:

            while True:
                # Get the sensor readings in meters
                left_distance = self.robot.left_distance_sensor.distance
                right_distance = self.robot.right_distance_sensor.distance
                ...
    
  6. 我们随后在控制台打印出我们的读数:

                ...
                print("Left: {l:.2f}, Right: {r:.2f}".format(l=left_distance, r=right_distance))
                ...
    
  7. 现在,我们使用get_motor_speed方法中的距离,并将其发送到每个电机:

                ...
                # Get speeds for motors from distances
                left_speed = self.get_motor_speed(left_distance)
                self.robot.set_left(left_speed)
                right_speed = self.get_motor_speed(right_distance)
                self.robot.set_right(right_speed)
    
  8. 由于这是我们主要的循环,我们在再次循环之前会等待一小段时间。下面是设置和起始行为:

                ...
                # Wait a little
                sleep(0.05)
    bot = Robot()
    behavior = ObstacleAvoidingBehavior(bot)
    behavior.run()
    

这个行为的代码现在已经完成,可以运行。是时候尝试一下了。为了测试,设置一个测试空间,宽度为几平方米。避免传感器无法检测到的障碍物,如软垫家具或细小的障碍物,如椅子腿。我使用了文件夹和塑料玩具箱来制作这些课程。

将代码发送到机器人并尝试运行。它会行驶直到遇到障碍物,然后转向避开。这有点效果;你可以调整速度和阈值,但行为会在角落里卡住,并且会感到困惑。

也许现在是考虑更好的策略的时候了。

更复杂的物体避障

之前的行为可能会让机器人陷入停滞。它面对一些障碍物时显得犹豫不决,偶尔还会撞到其他物体。它可能无法及时停止或转向避开物体。让我们制作一个行驶更平稳的更好版本。

那么,我们的策略是什么呢?好吧,让我们从最靠近障碍物的传感器和最远的传感器来思考。我们可以计算出最靠近它的电机的速度,远离它的电机的速度,以及一个时间延迟。我们的代码使用时间延迟来决定是否从墙上转向,时间因素控制我们转多远。这减少了任何抖动。让我们对最后的行为做一些修改:

  1. 首先,将simple_avoid_behavior.py文件复制到一个名为avoid_behavior.py的新文件中。

  2. 我们将不再需要get_motor_speed,所以将其删除。我们用名为get_speeds的函数来替换它。这个函数接受一个参数,nearest_distance,它应该是读数较低的距离传感器:

    ...
        def get_speeds(self, nearest_distance):
            if nearest_distance >= 1.0:
                nearest_speed = self.speed
                furthest_speed = self.speed
                delay = 100
            elif nearest_distance > 0.5:
                nearest_speed = self.speed
                furthest_speed = self.speed * 0.8
                delay = 100
            elif nearest_distance > 0.2:
                nearest_speed = self.speed
                furthest_speed = self.speed * 0.6
                delay = 100
            elif nearest_distance > 0.1:
                nearest_speed = -self.speed * 0.4
                furthest_speed = -self.speed
                delay = 100
            else: # collison
                nearest_speed = -self.speed
                furthest_speed = -self.speed
                delay = 250
            return nearest_speed, furthest_speed, delay
    ...
    

    这些数字都是为了微调。关键因素是,根据距离,我们让远离障碍物的电机减速,如果我们太靠近,它会驶离。基于时间延迟,并且知道哪个电机是哪个,我们可以控制我们的机器人。

  3. 大部分剩余的代码保持不变。这是你之前已经见过的run函数:

        ...
        def run(self):
            # Drive forward
            self.robot.set_pan(0)
            self.robot.set_tilt(0)
            while True:
                # Get the sensor readings in meters
                left_distance = self.robot.left_distance_sensor.distance
                right_distance = self.robot.right_distance_sensor.distance            # Display this
                self.display_state(left_distance, right_distance)
                ...
    
  4. 现在它使用get_speeds方法来确定最近和最远的距离。注意,我们取两个距离中的min,即最小值。我们得到两个电机的速度和一个延迟,然后打印出变量,这样我们就可以看到发生了什么:

    .format (which we used previously). Putting the letter prefix f in front of a string allows us to use local variables in curly brackets in the string. We are still able to use .2f to control the number of decimal places.
    
  5. 现在,我们检查哪一侧更近,左边还是右边,并设置正确的电机:

                ...
                # Send this to the motors
                if left_distance < right_distance:
                    self.robot.set_left(nearest_speed)
                    self.robot.set_right(furthest_speed)
                else:
                    self.robot.set_right(nearest_speed)
                    self.robot.set_left(furthest_speed)
                ...
    
  6. 我们不是固定睡眠一段时间,而是根据delay变量中的时间睡眠。延迟是以毫秒为单位的,所以我们需要将其乘以得到秒数:

                ...
                # Wait our delay time
                sleep(delay * 0.001)
    ...
    
  7. 代码的其他部分保持不变。你可以在https://github.com/PacktPublishing/Learn-Robotics-Programming-Second-Edition/tree/master/chapter8找到这个文件的完整代码。

当你运行这段代码时,你应该看到更平滑的避开行为。你可能需要调整时间和值。最后两个条件,反转和反转转向,可能需要调整。如果机器人拉回不够,则将时间设置得更高,如果它转得太远,则设置得更低。

尽管如此,这种行为仍然存在缺陷。它根本不会构建地图,也没有反向传感器,所以当它避开前面的物体时,它可以相当快地反向撞到后面的物体。增加更多的传感器可以解决一些这些问题。然而,我们仍然不能构建地图,因为我们的机器人没有传感器来准确确定它转了多少圈或行驶了多少距离。

摘要

在本章中,我们为我们的机器人添加了传感器。这是一个重要的步骤,因为它使机器人能够自主行动,独立行为并对其环境做出某种反应。你已经学会了如何为我们的机器人添加距离感应,以及可用的不同类型的传感器。我们看到了使它工作并测试这些传感器的代码。然后我们创建了避免墙壁的行为,并研究了如何制作一个简化但存在缺陷的行为,以及一个更复杂、更平滑的行为将如何使系统更好。

通过这次经验,你可以考虑如何将其他传感器与你的机器人接口,以及一些简单的代码来与之交互。你可以从传感器输出数据,以便调试其行为并创建一个行为,使机器人能够自行进行简单的导航。

在下一章中,我们将进一步探讨使用编码器驱动预定路径和直线,以确保机器人移动得更加准确。我们使用编码器来比较电机的输出与预期的目标,以获得更精确的转向。

练习

  1. 一些机器人只需要一个传感器就能应对。你能想到一种只用一个传感器可靠地避开障碍物的方法吗?

  2. 我们有一个俯仰/倾斜机构,我们稍后会用它来安装摄像头。考虑在这个机构上放置一个传感器,以及如何将其整合到行为中。

  3. 在本章中,我们创建的机器人行为可以反向变成其他事物。你该如何解决这个问题呢?或许可以制定一个计划并尝试去构建它。

进一步阅读

请参考以下链接获取更多信息:

第十一章:第9章:在Python中编程RGB灯带

LED灯光可以与机器人一起使用,用于调试和提供反馈,以便机器人上运行的代码可以显示其状态。彩色RGB LED允许你混合光的红色、绿色和蓝色成分,以制作多种颜色,为机器人增添亮度和色彩。我们之前并没有过多关注使其看起来有趣,所以这次我们将专注于这一点。

通过混合不同的LED序列,可以实时传达信息。你可以使用它们的开关状态、亮度或颜色来表示信息。这种反馈比文本流更容易阅读,这有助于在机器人上添加传感器时。这也意味着机器人上的代码可以显示状态,而无需依赖SSH终端来完成。

在本章中,我们将学习以下内容:

  • 什么是RGB灯带?

  • 比较灯带技术

  • 将灯带连接到Raspberry Pi

  • 让机器人显示代码对象

  • 使用灯带调试避障行为

  • 使用LED制作彩虹显示

技术要求

要构建这个项目,你需要以下材料:

  • 一台可以访问互联网并连接Wi-Fi的电脑

  • 机器人、Raspberry Pi和上一章的代码

  • Pimoroni LED SHIM

本章的代码可在GitHub上找到:https://github.com/PacktPublishing/Learn-Robotics-Fundamentals-of-Robotics-Programming-Second-Edition/blob/master/chapter9

查看以下链接中的视频,以查看代码的实际应用:https://bit.ly/39vglXm

什么是RGB灯带?

使用灯光显示数据可以是一种简单而灵活的方式,将数据传递给用户,而无需连接完整的显示屏。例如,单个灯光可以打开或关闭,以指示机器人是否已开启或简单传感器的状态。多彩灯光可以改变颜色以显示更多细节,指示机器人处于几种不同的状态。在本章中,RGB代表红-绿-蓝,因此通过控制灯光中这些颜色通道的强度级别,可以显示多种颜色。我们将在“RGB值”部分稍后探讨这是如何发生的。

添加多个灯光可以显示更多数据。这些可以是灯带(一排灯光)、面板/矩阵、环形以及其他有趣的形状。

比较灯带技术

对于灯光和灯带,存在许多竞争技术。对于灯的类型,如旧灯泡这样的白炽灯,往往消耗大量电力,占用太多空间,在机器人中不实用。荧光灯,如厨房灯带或卷曲紧凑型灯,需要复杂的电源系统,也占用太多空间。电致发光线,也称为EL线,通常用于装饰物体,通过轮廓来装饰;它看起来很有趣,但控制起来很棘手。发光二极管LED)技术功耗低,通常体积小,易于控制,这使得它非常适合我们这样的机器人。LED也很便宜。

在我们这个案例中,最有用的一种,我们将在本章中使用,是可寻址RGB LED。可寻址意味着条带中的每个单独的LED都可以设置为不同的颜色和亮度,从而在条带上形成颜色序列。为了简化,我们将使用一种内置控制器的类型。

图9.1 展示了我实验过的几种可寻址RGB LED配置类型:

图9.1 – 可寻址RGB LED的类型

所有这些LED控制器都接收一系列数据。一些类型,如Neopixel、WS2812、SK9822、APA102C、DotStar和2801类型,取它们需要的红、绿、蓝组件,然后将剩余的数据传递给下一个LED。设计师将这些LED排列成条带、环形或方形矩阵,将它们串联起来以利用它们传递数据的方式。LED灯带可以是刚性棒或卷轴上的柔性条带。对于我们的机器人来说,8个或更多的LED就能构成一个很好的显示屏。

还有一些完全不同的技术,例如Pimoroni的LED SHIM和彩色LED矩阵,使用移位寄存器。Pimoroni LED SHIM是LED灯带中最容易使用的一种(与Raspberry Pi一起使用)。它包含一个控制器(IS31FL3731),通过I2C数据总线进行控制。Pimoroni LED SHIM有24个LED,这已经足够满足我们的需求。它不需要额外的电源处理,并且也广泛可用。

我们机器人使用I2C数据总线来控制电机,它通过拥有不同的地址,愉快地与其他设备共享,例如LED SHIM。I2C指令以设备地址发送,后面跟着一个要写入的I2C寄存器和它的值。

由于其简单性和与我们的机器人的兼容性,我将继续本章,使用Pimoroni LED SHIM。这种产品在大多数国家都可以从Mouser Electronics、Pimoroni、Adafruit和SparkFun购买。

RGB值

红色、绿色和蓝色可以混合成几乎任何颜色组合。系统用RGB值来表示这些颜色。RGB是您看到的几乎所有(如果不是所有)彩色显示屏所使用的相同原理。电视、移动电话和计算机屏幕都使用这个。多色LED使用相同的原理来产生多种颜色。代码通常指定混合每种颜色的数量,如下面的图所示:

图9.2 – RGB颜色空间 [SharkD / CC BY-SA (https://creativecommons.org/licenses/by-sa/3.0)]

图9.2中的图显示了RGB颜色立方体。它有箭头显示增加红色、绿色和蓝色各成分的轴。立方体的暴露表面显示了颜色组合在立方体内混合时的不同色调和强度。

底部前右角是蓝色,顶部前右角是青色(混合蓝色和绿色),底部前左角是紫色(混合红色和蓝色),底部远左角是红色(没有绿色或蓝色),顶部远左角是黄色(高红色和绿色,没有蓝色)。

随着每个值的增加,我们通过混合得到不同的颜色。顶部前左角将是三个中的最大值——白色。底部后右角将是三个中的最小值——黑色。裁剪图显示了以分数表示的强度颜色。

在我们的代码中,我们将使用从0(完全关闭)到255(全强度)的数字,中间的值表示许多级别的强度。颜色通过相加混合,所以将所有颜色在全亮度下相加得到白色。

虽然理论上这可以提供许多颜色,但在实践中,250和255之间的强度差异在大多数RGB LED上是不可辨别的。

您已经看到了一些LED技术以及如何混合它们颜色的信息。我们还决定使用哪种技术,即Pimoroni LED SHIM。由于我们将将其连接到我们的机器人上,请购买一个并在下一节回来。

将灯条连接到Raspberry Pi

在我们编写代码在LED SHIM上显示颜色序列之前,我们需要将其连接到我们机器人上的Raspberry Pi。完成本节后,机器人框图将看起来像图9.3

图9.3 – 带有LED灯条的机器人框图

现在的框图显示了LED灯条连接到Raspberry Pi,其中有一个箭头指示从Raspberry Pi到灯条的信息流。灯条被突出显示为系统的新增部分。让我们看看它是如何工作的。

将LED灯条连接到机器人

Pimoroni LED SHIM很容易连接到Raspberry Pi。我们将其放在电机控制器上,带有其旁路引脚,这样我们就可以看到顶部的灯光。看看图9.4看看如何:

图9.4 – 安装LED

使用以下步骤用图9.4来安装条带:

  1. 条带很小。将条带与从电机HAT顶部来的排针对齐。你需要拔掉已经插入Raspberry Pi的线来添加SHIM。

  2. 较宽的部分应该从HAT上突出出来。轻轻地将SHIM推到引脚上,一开始只推一点,沿着条带工作,直到所有引脚都进入孔中——它稍微有点硬,但应该能抓牢。

  3. 一旦所有引脚都插入,均匀地按下SHIM,使引脚大部分突出。

  4. 现在,你需要更换电线。请参考第8章中的图8.15使用Python编程距离传感器,以获取距离传感器的接线信息。

现在你已经连接了LED SHIM,这个机器人就准备好发光了。让我们来编程它。

使机器人显示代码对象

虽然我们是在围绕Pimoroni LED SHIM构建,但我们已经看到还有其他类型的RGB LED系统。由于我们可能稍后会用不同的系统替换SHIM,因此在LEDs上制作一个接口是个好主意。就像电机的接口一样,这可以解耦硬件处理和行为制作。

创建LED接口

那么,我们希望LEDs具有什么样的接口呢?首先,我们希望它们在机器上作为robot.leds可用。我们希望清除LEDs(将它们全部关闭),将每个单独的LED设置为不同的颜色,并将一组/范围的LED设置为颜色列表。

代码告诉我们有多少LEDs很有用,所以如果数量发生变化,动画或显示仍然有意义。

对于颜色,我们使用三个值——rgb——来表示红色、绿色和蓝色成分。Python有一个名为color的类型作为参数,这是一个(r, g, b)的元组。

条带上的LED是可寻址的,因此我们的代码使用从0开始的LED编号。

因此,从结构上讲,我们的代码从robot.leds开始。leds将是现有robot类的一个成员。它是一个具有以下成员的对象:

  • set_one(led_number, color): 这将led_number处的单个LED设置为指定的颜色。

  • set_range(led_range, color): 这将Python可迭代对象led_range定义的所有LED设置为color。Python可迭代对象可以是LED编号列表[0, 3],或者可以使用range函数创建的范围。例如,range(2,8)创建列表[2, 3, 4, 5, 6, 7]

  • set_all(color): 这将所有LED设置为指定的颜色。

  • clear(): 这会将所有LED清除为黑色,将它们全部关闭。

  • show(): 所有其他方法都准备了一个显示,允许你设置LED的组合。直到你的代码调用此方法之前,LED设备上不会更新任何内容。此方法反映了大多数LED条带期望从一个数据流中设置所有LED的方式。

  • count: 这保存了条带中LED的数量。

记住前面的要点,让我们为LED SHIM编写这段代码:

  1. 首先,我们需要安装LED SHIM库。因此,在Raspberry Pi上,输入以下内容:

    pi@myrobot:~ $ pip3 install ledshim
    
  2. 我们的代码必须首先导入此设置并设置设备。将以下代码放入leds_led_shim.py(以设备类型命名):

    import ledshim to set the device up.We have set up a property for the number of LEDs in our LED class, called `count`. This property can be read like a variable but is read-only, and our code can't accidentally overwrite it.  
    
  3. 现在,我们创建与条带交互的方法。设置单个LED相当直接:

    (r, g, b), the LED SHIM library expects them to be separate parameters. Python has a trick for expanding a tuple into a set of parameters by using an asterisk with the variable name. This expansion is what *color means on the second line.The LED SHIM code raises `KeyError` if the user attempts to set an LED out of range.
    
  4. 设置多个LEDs在我们的代码中也是一个简单的包装器:

        def set_range(self, led_range, color):
            ledshim.set_multiple_pixels(led_range, color)
    
  5. 我们还希望有一种方法来设置所有的LEDs。此代码与设置单个LED的代码类似:

        def set_all(self, color):
            ledshim.set_all(*color)
    
  6. 让我们添加一个清除LEDs的方法:

        def clear(self):
            ledshim.clear()
    
  7. 最后,我们需要show代码,将我们配置的颜色发送到LEDs。Pimoroni LED SHIM库使得这个过程非常简单:

        def show(self):
            ledshim.show()
    

我们已经安装了LED SHIM库,并为我们自己创建了一个接口。我们可以使用这个接口与LEDs通信,并且它被设计成可以替换为不同类型LED设备的兼容代码。现在,我们将这个LED接口在我们的Robot对象中可用。

将LEDs添加到Robot对象中

接下来,我们更新我们的robot.py文件以处理LED系统。为此,我们执行以下操作:

  1. 首先,将leds_led_shim文件添加到导入中(新的代码用粗体表示):

    from Raspi_MotorHAT import Raspi_MotorHAT
    from gpiozero import DistanceSensor
    import atexit
    import leds_led_shim
    
  2. 接下来,我们在Robot的构造函数(init)方法中添加SHIM的一个实例(新的代码用粗体表示):

    class Robot:
        def __init__(self, motorhat_addr=0x6f):
           # Setup the motorhat with the passed in address
           self._mh = Raspi_MotorHAT(addr=motorhat_addr)
           # get local variable for each motor
           self.left_motor = self._mh.getMotor(1)
           self.right_motor = self._mh.getMotor(2)
           # Setup The Distance Sensors
           self.left_distance_sensor = DistanceSensor(echo=17, trigger=27, queue_len=2)
            self.right_distance_sensor = DistanceSensor(echo=5, trigger=6, queue_len=2)
            # Setup the Leds
            self.leds = leds_led_shim.Leds()
    
  3. 由于我们需要停止的不仅仅是电机,我们将在atexit调用中将stop_motors替换为新的stop_all方法,以停止其他设备(如LEDs):

            # ensure everything gets stopped when the code exits
            atexit.register(self.stop_all)
    
  4. 创建stop_all方法,该方法停止电机并清除LEDs:

        def stop_all(self):
            self.stop_motors()
            # Clear the display
            self.leds.clear()
            self.leds.show()
    

    重要提示

    完整的代码可以在https://github.com/PacktPublishing/Learn-Robotics-Fundamentals-of-Robotics-Programming-Second-Edition/blob/master/chapter9找到。

我们现在已将LED支持添加到Robot类中,使之前设计的接口可用,并确保在机器人代码退出时清除LEDs。接下来,我们将开始测试和打开LEDs。

测试一个LED

我们已经安装了一些硬件及其库,并添加了代码以使这些内容在我们的机器人中可用。然而,在我们继续之前,我们应该确保一切都能通过测试正常工作。这是一个找到任何问题并进行故障排除的好地方。

让我们尝试测试一个单个LED。我们还没有探索的一个关于我们的机器人运行Python的方面是它可以运行Python REPL读取、评估、打印循环。这意味着你可以启动Python并立即输入要运行的代码。我们将使用这个来测试我们的LEDs:

  1. leds_led_shim.pyrobot.py代码复制到Raspberry Pi上。

  2. 通过SSH连接到机器人,并输入python3。Raspberry Pi应该这样响应:

    pi@myrobot:~ $ python3
    Python 3.7.3 (default, Apr  3 2019, 05:39:12) 
    [GCC 8.2.0] on linux
    Type "help", "copyright", "credits" or "license" for more information.
    >>>
    
  3. 让我们准备好我们的robot库以便使用。输入以下粗体显示的部分:

    >>> import robot
    >>> r = robot.Robot()
    
  4. 现在,尝试打开一个LED,将其设置为红色:

    >>> r.leds.set_one(0, (255, 0, 0))
    
  5. 嗯 – 没有发生任何事情。记住,我们需要调用leds.show来显示我们的设置:

    >>> r.leds.show()
    

    你现在应该看到一个红色的LED。

  6. 让我们尝试将另一种颜色设置为紫色,通过混合红色和蓝色LED:

    leds.show to send the colors to the LED device.
    
  7. 要停止此会话,请在空行上按 Ctrl +Datexit 代码会自动关闭所有LED。

现在,你应该已经看到LED工作并点亮成多种颜色。这表明到目前为止的代码是好的。如果不是,请参阅以下部分。如果一切正常,请跳到 测试所有LED 部分。

故障排除

如果你在尝试点亮LED时遇到问题,可以采取一些故障排除步骤。

如果运行代码出现错误,请执行以下操作:

  • 确认你已经启用了I2C(如第7章驱动和转向 – 使用Python移动电机所示)。

  • 使用 sudo i2cdetect -y 1,如第7章驱动和转向 – 使用Python移动电机所示。你应该看到地址为 74 的LED。

  • 确认你已经使用 pip3 安装了 ledshim Python包。

  • 仔细检查代码中的错误和错误。如果是GitHub上的代码,请创建一个问题!

如果LED根本不亮,请执行以下操作:

通过遵循这些故障排除提示,你将消除这个系统中最常见的问题。你现在应该有一个工作的LED,并且能够进入下一节。

测试所有LED

现在,我们可以尝试使用 set_all 方法。我们将制作一些简单的在LED上闪烁几种不同颜色的东西。创建一个名为 leds_test.py 的文件:

  1. 首先,我们需要导入。我们需要导入我们的 R 库和 time 来动画化:

    from robot import Robot
    from time import sleep
    
  2. 现在,让我们设置我们的机器人,以及一些命名颜色:

    bot = Robot()
    red = (255, 0, 0)
    blue = (0, 0, 255)
    
  3. 下一个部分是主循环。它交替两种颜色,使用 sleep

    set_all method to set all the LEDs to red and call the show method to send it to the device. The code uses sleep to wait for half a second, before switching to blue. Important noteThe complete code is at [https://github.com/PacktPublishing/Learn-Robotics-Fundamentals-of-Robotics-Programming-Second-Edition/blob/master/chapter8/leds_test.py](https://github.com/PacktPublishing/Learn-Robotics-Fundamentals-of-Robotics-Programming-Second-Edition/blob/master/chapter8/leds_test.py).
    
  4. 当你将这些文件上传到Raspberry Pi后,输入以下命令以显示红/蓝交替LED显示:

    pi@myrobot:~ $ python3 leds_test.py
    
  5. 在终端中按 Ctrl + C 停止运行。

我们现在已经展示了所有LED都在工作。这也展示了它们在不同颜色之间切换,使用时间产生一个非常简单的动画。我们可以在此基础上构建,产生更多有趣的颜色和动画应用,但首先,我们将转向学习更多关于混合颜色的知识。

使用LED制作彩虹显示

现在,我们可以用这些做一些有趣的事情。我们将扩展上一章中的避障行为,以显示对应于读取距离的彩虹条形图。我们也可以用这个来传感。在我们将运动与动画链接之前,彩虹是如何形成的?

颜色系统

RGB是硬件期望的颜色。然而,RGB在表达中间颜色或创建它们之间的渐变时不太方便。在视觉上看起来接近的颜色在RGB中可能有点远。正因为如此,还有其他颜色系统。

我们使用的另一种颜色系统是色调、饱和度和亮度HSV)。在本章中,我们使用HSV来制作彩虹型显示,并在后续章节中进行计算机视觉时,帮助我们的代码检测对象。

色调

想象将光谱的颜色放置在一个圆圈上,从红色到橙色,橙色到黄色,黄色到绿色,绿色到蓝色,蓝色到紫色,然后回到红色。色调表示这个圆圈上的一个点。它不影响颜色的亮度或其鲜艳程度。图9.5展示了我们如何在色轮上表示这些点:

图片

图9.5 – 色调色轮

图9.5中,圆圈显示在0度左右,可以看到红色色调。圆圈上的罗盘指针对应不同的颜色。当你从一个色调移动到另一个色调时,颜色会混合。你可能在一个绘画或绘图计算机程序的颜色轮装置中见过类似的东西。这种连续的轮状设置让我们能够制作彩虹。

饱和度

如果你选择一种颜色,比如红色,它可以是一种灰暗的/暗淡的红色,或者是一种鲜艳、强烈的红色。饱和度是颜色鲜艳程度的表达。当你接近零时,它只产生灰色调。当我们增加饱和度时,颜色开始出现——首先是淡色调,然后是海报色,然后是高饱和度尺度上的醒目的危险标志或纯色。

亮度

颜色的亮度是其亮度。它从0的黑色,通过到颜色的非常暗的版本,到一个非常亮的颜色。请注意,这并不接近白色(换句话说,粉红色),而是一个非常亮的红色。要制作白色,你还需要降低饱和度。其他颜色系统(如HSL)指定一个成分,这样可以使事物变得白色。

将HSV转换为RGB

在一个颜色系统转换到另一个颜色系统之间有复杂的公式。然而,Python可以自己完成这种转换。

我们将使用colorsys.hsv_to_rgb来完成这种转换。它接受三个HSV成分作为介于0和1之间的分数,包括1。

在色调成分的情况下,0是圆圈的起点,0.5代表180度,半圆,而1是完全绕回到360度,即整个圆圈。

饱和度成分在0时为灰色,完全未饱和,而在1时为最鲜艳的颜色。

亮度成分在0时为黑色,完全黑暗,而在1时是最亮的——一个完全发光的颜色。

要制作明亮的青色,我们需要将色调移动到大约0.6的位置,饱和度为1.0,亮度也为1.0:

cyan = colorsys.hsv_to_rgb(0.6, 1.0, 1.0)

然而,这还不够。colorsys调用的输出是一个元组,包含三个项目,用于R、G和B组件。

输出组件也是以0到1.0为单位的。大多数RGB系统期望值在0到255之间。为了使用它们,我们需要通过乘以它们将这些值转换回来:

cyan_rgb = [int(c * 255) for c in cyan]

在上一行中,我们遍历每个组件c,并将其乘以255。通过在Python中用方括号将for循环放在那里,我们可以遍历元素,并将结果放回列表中。

现在你已经知道了如何将HSV值转换为RGB值,让我们使用这些信息来制作彩虹。

在LED上制作彩虹

我们可以利用我们对颜色系统的理解,在LED上制作彩虹:

图片

图9.6 – 机器人上显示彩虹的LED

图9.6显示了在机器人上连接的LED上显示的彩虹。

让我们开始吧!创建一个名为led_rainbow.py的新文件:

import colorsys
def show_rainbow(leds, led_range):
    led_range = list(led_range)
    hue_step = 1.0 / len(led_range)
    for index, led_address in enumerate(led_range):
        hue = hue_step * index
        rgb = colorsys.hsv_to_rgb(hue, 1.0, 0.6)
        rgb = [int(c*255) for c in rgb]
        leds.set_one(led_address, rgb)

让我们逐行分析这个文件:

  • 这段代码首先导入colorsys

  • 我们定义了一个函数show_rainbow,它接受两个参数,一个指向我们的LED系统(这通常会被赋予robot.leds)和一个要设置的LED范围。

  • 因为我们想知道我们的LED范围长度,所以我们需要确保它是一个列表,因此我们在函数的第一行进行了转换。

  • 对于彩虹,色调值应该扫过一个完整的圆圈。在Python中,colorsys提供的是0到1的值。我们希望为范围中的每个LED前进一个分数步。通过将1.0除以范围中的LED数量,我们得到这个分数。

  • 然后我们遍历LED。enumerate给我们一个索引,而led_address递增。这段代码对范围没有做出任何假设,因此它可以使用任意LED列表。

  • 然后我们将hue_stepindex相乘以给出hue值,这是使用1.0的正确分数。下一行将这个值转换为具有固定饱和度和亮度值的RGB值。

  • 由于colorsys输出介于0和1之间的值,代码需要将这个值乘以255,并将结果数字转换为整数:rgb = [int(c*255) for c in rgb]

  • 代码使用leds.set_one方法与这个RGB值和LED地址一起使用。

让我们用名为test_rainbow.py的文件来测试这个:

from time import sleep
from robot import Robot
from led_rainbow import show_rainbow
bot = Robot()
while True:
    print("on")
    show_rainbow(bot.leds, range(bot.leds.count))
    bot.leds.show()
    sleep(0.5)
    print("off")
    bot.leds.clear()
    bot.leds.show()
    sleep(0.5)

这与我们的之前的红蓝测试非常相似。然而,在第一部分,我们使用了show_rainbow函数,该函数是从led_rainbow模块导入的。它传递了机器人的LED,并创建了一个覆盖所有LED的范围。

代码等待半秒钟,然后清除LED半秒钟。这些都在循环中,以产生开/关彩虹效果。用python3 test_rainbow.py启动它,并在看到它工作后使用Ctrl + C停止它。

现在你已经看到了一些简单的动画和多色LED的使用,我们可以通过让LED对传感器做出反应来将这个提升到下一个层次。

使用灯带调试避障行为

彩虹中的LED很有趣,颜色切换看起来也很漂亮。然而,LED也可以用于实际目的。在第8章,“使用Python编程距离传感器”中,我们向我们的机器人添加了传感器以避免障碍物。你可以在PuTTY窗口中跟随操作,通过读取数字来查看传感器正在检测什么。但我们可以做得更好;有了灯光条,我们可以在机器人上放置信息,告诉我们它正在检测什么。

在本节中,我们将LED输出与行为值关联起来,首先是通过基本照明,然后是通过制作一些彩虹颜色。

将基本LED添加到避免行为中

在我们变得花哨并重新引入彩虹之前,让我们先从基本版本开始。这里的意图是制作两个位于LED条左右两侧的指示条。对于每条条,当相应的距离传感器检测到更近的障碍物时,更多的LED将被点亮。我们将使这些条进入中间,这样当单个外部的LED被点亮时,障碍物就远离。当一侧的大多数或所有LED都被点亮时,障碍物就非常接近。

我们需要向我们的避免行为添加一些部分:

  • 一些用于设置LED显示的变量,以及我们的距离如何映射到它

  • 一种将距离转换为要显示的LED数量的方法

  • 使用前面提到的项目在LED上显示我们传感器状态的方法

  • 从行为的主循环中调用display_state方法

让我们看看如何结合前面的要点。打开你在第8章,“使用Python编程距离传感器”中制作的avoid_behavior.py文件,并跟随操作:

  1. 在我们可以在这种行为中使用LED之前,我们需要将它们分开成条。在ObstacleAvoidingBehavior__init__方法中添加以下内容:

            # Calculations for the LEDs
            self.led_half = int(self.robot.leds.leds_count/2)
    
  2. 接下来,我们需要为感应时的LED选择一种颜色。我选择了红色。我鼓励你尝试另一种颜色:

            self.sense_colour = 255, 0, 0
    
  3. 在设置好变量之后,让我们添加一个将距离转换为LED的方法。我在__init__方法之后添加了这个方法:

        def distance_to_led_bar(self, distance):
    
  4. 距离是以米为单位的,1.0代表1米,所以从1.0减去距离会反转这个值。max函数将返回两个值中的较大值,在这里它被用来确保我们不会低于零:

    # Invert so closer means more LED's. 
            inverted = max(0, 1.0 - distance)
    
  5. 现在,我们将这个数字乘以0到1之间的某个分数,乘以self.led_half的值,以得到要使用的LED数量。我们将其向上取整,并用int(round())将其转换为整数,因为我们只能打开整数的LED。向上取整意味着在我们的乘法之后,如果我们得到一个如3.8这样的值,我们将它向上取整到4.0,然后将其转换为整数以点亮四个LED。我们给这个数加1,这样至少总有一个LED被点亮,然后返回它:

            led_bar = int(round(inverted * self.led_half))
            return led_bar
    
  6. 下一个方法是一个更复杂的方法;它将创建两个条。让我们首先声明该方法并清除LED:

        def display_state(self, left_distance, right_distance):
            # Clear first
            self.robot.leds.clear()
    
  7. 对于左侧条形,我们将左侧传感器的距离转换为LED数量,然后创建一个从0到这个数量的范围。它使用set_range方法将一组LED设置为sense_color。请注意,你的LED可能方向相反,在这种情况下,在这个display方法中交换left_distanceright_distance

            # Left side
            led_bar = self.distance_to_led_bar(left_distance)
            self.robot.leds.set_range(range(led_bar), self.sense_colour)
    
  8. 右侧稍微复杂一些;在转换为LED数量后,我们需要为LED创建一个范围。变量led_bar保存要点亮的LED数量。为了点亮条形的右侧,我们需要从这个LED数量中减去,以找到第一个LED,并从那里创建一个到总长度的范围。我们必须从长度中减去1 – 否则它将多计算1个LED:

            # Right side
            led_bar = self.distance_to_led_bar(right_distance)
            # Bit trickier - must go from below the leds count up to the leds count.
            start = (self.robot.leds.count – 1) - led_bar
            self.robot.leds.set_range(range(start, self.robot.leds.count - 1), self.sense_colour)
    
  9. 接下来,我们想要展示我们现在制作的显示:

            # Now show this display
            self.robot.leds.show()
    
  10. 然后,我们在行为中的run方法内部调用display_state来在LED上显示我们的读数。这里有一些上下文中的几行,其中额外的一行被突出显示:

                # Get the sensor readings in meters
                left_distance = self.robot.left_distance_sensor.distance
                right_distance = self.robot.right_distance_sensor.distance
                # Display this
                self.display_state(left_distance, right_distance)
                # Get speeds for motors from distances
                nearest_speed, furthest_speed, delay = self.get_speeds(min(left_distance, right_distance))
    

保存此代码,将其发送到Raspberry Pi并运行。当它在运行时,你应该能够看到LED根据距离点亮成条形。这既令人满意,也很好地感受到了机器人正在检测的内容。让我们通过添加彩虹让它更有趣。

添加彩虹

我们可以使用我们的LED彩虹灯来让我们的距离感应演示变得更加有趣:

图9.7 – 距离传感器彩虹条

图9.7显示了每个距离传感器的彩虹条的照片。这是一个很好的LED动画视觉演示。

由于我们添加了一个用于显示彩虹的库,我们可以在这里重用它。让我们看看如何做:

  1. 打开上一节中的avoid_behaviour.py代码。

  2. 在顶部,导入led_rainbow以便我们可以使用它:

    from robot import Robot
    from time import sleep
    from led_rainbow import show_rainbow
    
  3. 我们现有的代码显示了左侧的条形。在这里,我们不需要条形,而是显示一个彩虹。我们需要确保至少有一个项目:

         # Left side
         led_bar = self.distance_to_led_bar(left_distance)
         show_rainbow(self.robot.leds, range(led_bar))
    
  4. 再次,右侧将稍微复杂一些;因为我们想让彩虹朝相反方向延伸,所以我们需要让彩虹的范围也倒计数。Python的range函数,连同startend参数,还有一个步长参数。通过将步长设置为-1,我们可以在范围内倒计数:

            start = (self.robot.leds.count – 1) - led_bar
            right_range = range(self.robot.leds.count - 1, start, -1)
            show_rainbow(self.robot.leds, right_range)
    
  5. 上传此代码并运行它,条形图将显示彩虹颜色而不是纯色。

你已经从单个LED变成了多个LED。通过一些对颜色系统的处理,我们能够生成彩虹并使用它来显示行为的状态。

摘要

在本章中,你学习了如何与RGB LED交互和使用,以及如何选择和购买与Raspberry Pi兼容的RGB LED灯带。你学习了如何为机器人上的LED编写代码,使用它们与机器人行为结合。你还看到了HSV颜色系统是如何工作的,它可以用来生成彩虹。

你可以将这里使用的技术用于向机器人添加基于LED的状态显示,并编写将它们与行为链接的代码。

在下一章中,我们将探讨伺服电机并构建一个用于移动传感器的云台机构。

练习

  1. 尝试混合不同的RGB颜色,或者查找一种颜色,并使用set_oneset_allset_range来点亮该颜色的LED。

  2. 使用show_left_rainbowshow_right_rainbow函数,在behaviour_path代码中使机器人转向时点亮对应侧的彩虹。

  3. 通过创建一个计时器循环并递增索引或改变范围,可以动画显示彩虹或使它们在LED条上扫描。试试这个方法。

  4. HSV颜色模型的其他部分能否用来制作亮度变化的闪烁LED灯带?

进一步阅读

请参考以下内容获取更多信息:

  • 《通过探索学习电子学》查尔斯·普拉特Make Community, LLC:我刚刚开始用开关和面包板介绍一些基本的电子学知识。要真正感受电子学的魅力,《通过探索学习电子学》是一本极好的入门书籍。

  • 对于更高级的电子学,尝试阅读《发明者实用电子学,第四版》保罗·舍尔茨西蒙·蒙克麦格劳-希尔教育TAB:这本书提供了电子学的实用构建块,可以用来将机器人控制器与几乎任何设备接口或构建新的传感器。

  • colorsys库,像大多数Python核心库一样,有一个很好的参考:https://docs.python.org/3/library/colorsys.html

  • Pimoroni有一些使用LED SHIM的演示,可以在https://github.com/pimoroni/led-shim/tree/master/examples找到。这些可能很有趣,可以改编到我们的LED层。

第十二章:第 10 章:使用 Python 控制伺服电机

伺服电机可以产生精确和可重复的运动。这种运动对于移动传感器、控制机器人腿或手臂并将它们移动到已知位置至关重要。其用途是工程师可以预测机器人行为,机器人可以在自动化中重复事物,或者代码可以将肢体移动到准确响应传感器指示的位置。Raspberry Pi 或附加板可以控制它们。在本章中,我们将使用这些电机构建一个俯仰-倾斜机构——一个用于定位传感器的头部。

在本章中,我们将涵盖以下主题:

  • 什么是伺服电机?

  • 使用 Raspberry Pi 定位伺服电机

  • 添加俯仰和倾斜机构

  • 创建俯仰和倾斜代码

技术要求

对于本章,你需要以下内容:

  • 在前几章中构建了 Raspberry Pi 的机器人

  • 螺丝刀——小型十字螺丝刀

  • 小型钳子或一套微型扳手

  • 尼龙螺栓和支架套件——2.5 毫米

  • 一个两轴微型俯仰-倾斜微型伺服电机套件

  • 两个微型 SG90/9g 或 MG90s 伺服电机,以及它们的硬件和伺服齿轮。俯仰-倾斜套件可能已经包括以下内容:

  • 剪切钳或侧剪钳

  • 安全眼镜

本章的代码可在https://github.com/PacktPublishing/Learn-Robotics-Programming-Second-Edition/blob/master/chapter10/找到。

查看以下视频,了解代码的实际应用:https://bit.ly/2LKh92g

什么是伺服电机?

伺服电机,或称伺服机构电机,用于定位机器人附件,如手臂、夹爪、腿和传感器支架。它们在位置是主要因素的地方产生其他运动,与轮电机(直流电机)不同,轮电机以速度为控制因素。伺服电机用于需要(在一定精度范围内)转向特定位置的地方。你可以使用代码来控制这些精确的位置运动或一系列运动:

图 10.1 – 伺服电机的小型选择

伺服电机有多种尺寸,从小型约 20-30 毫米(如图 10.1 所示)到足够大以移动重型机械的尺寸。图 10.2 展示了我为我的机器人使用的某些微型爱好伺服电机:

图 10.2 – 机器人中伺服电机的小型选择

现在你已经看到了伺服电机的可能应用,我们可以进一步探讨伺服电机的工作原理。

查看伺服电机内部结构

在伺服电机的紧凑型结构中隐藏着控制器、直流电机、变速箱(通常带有停止装置)和传感器。这些电机具有内置的反馈系统。伺服电机从控制器接收输入,控制器指定电机要到达的位置。伺服控制器的内部传感器获取电机的当前方向。控制器将当前电机位置与请求的位置进行比较,生成一个误差——差异。基于这个差异,伺服驱动电机以尝试将这个误差减少到零。当电机移动时,传感器会改变,误差值也会改变,从而产生反馈,形成图10.3中显示的控制回路:

图10.3 – 伺服电机控制回路

一些类型的伺服电机,例如在ArmBot(图10.2)中使用的电机,有一个额外的输出,允许你在代码中读取位置传感器的状态。

向伺服电机发送输入位置

使用脉冲宽度调制PWM)向伺服电机发送信号。PWM是我们在第2章中看到的相同系统,“探索机器人构建块 – 代码和电子”,并且在我们机器人上用于驱动轮电机。PWM是一个方波,只有两种状态,即开启关闭。有趣的是,信号的定时,如图10.4所示。你可以将波形视为脉冲流,其中每个脉冲的时间长度编码了伺服控制器的位置信息。这些脉冲以周期或频率重复循环。人们通常将频率表示为每秒周期数或赫兹。较短的脉冲是较低值,较长的脉冲是较高值。控制器保持周期/频率不变,而改变的是占空比(开启时间与关断时间的比率)。对于伺服电机,脉冲长度是信息编码特征:

图10.4 – 伺服电机的PWM

图10.4中,每个图表的x轴上都有时间。每个堆叠图表的y轴上,L代表逻辑低电平,H代表逻辑高电平。最上面的图表显示了短脉冲。底部的图表显示了脉冲时间的增加;然而,它们在重要方面有所不同。中间的图表中,关断时间没有改变,但周期已经改变。

图10.4的底部图表中,周期与第一个图表相同,但脉冲时间更长,关断时间更短。伺服电机测量脉冲长度而不是频率,所以第三个图表的变化是正确的类型。

在我们的机器人中,电机控制器中已经有一个芯片,它执行固定周期的PWM样式。该芯片设计用于控制LED,但很高兴地控制其他PWM设备。我们可以控制在固定周期内关断时间和开启时间应该何时开始,这意味着它在较长的脉冲宽度下表现得像底部的图表。

脉宽控制自然引出下一节,我们将使我们的机器人生成PWM来定位伺服电机。让我们准备一个伺服电机,将其插入,并使其移动。

使用Raspberry Pi定位伺服电机

要定位伺服电机,我们需要设置一个伺服电机臂来观察其移动,然后将它插入电机控制器板。伺服电机臂是一个带有或多个臂的小颈套,通常用于将伺服电机轴连接到它们要移动的机构。图10.5展示了如何将臂连接到伺服电机:

图10.5 – 安装伺服电机臂

图10.5中的图像展示了如何安装伺服电机臂。执行以下步骤:

  1. 伺服电机通常附带一些小型硬件袋,包含几种不同类型的臂和螺丝,用于将它们连接到伺服电机和它们要移动的部件。

  2. 使用非常短的螺丝钉来做这件事,因为较长的螺丝钉可能会损坏伺服电机。

  3. 将单臂伺服电机臂拧入伺服电机。电机臂的长颈套可以套在伺服电机的输出轴上。

  4. 现在伺服电机应该看起来像这样。不要拧得太紧,因为你可能需要松开它并重新设置中间位置。

在下一张图片中,我们将伺服电机插入控制板进行测试:

图10.6 – 将伺服电机插入控制板

按照图10.6查看如何将伺服电机连接到机器人上的全功能电机HAT板。确保在连接之前完全关闭机器人的电源:

  1. 这里轮廓箭头指示伺服连接器。伺服连接器有三个用实心箭头表示的引脚:棕色代表G)、红色代表电压V)、黄色/橙色/白色代表信号S)。

  2. 此面板显示Motor HAT有一个标记为PWM/Servo的4 x 3连接器块,由箭头指示。四个伺服通道列分别编号为(011415)。每个通道有三个引脚,标记为引脚标签(GVS)。GVS代表电压信号。将伺服连接器的黄色线与S行对齐,将棕色线与G行对齐,红色线居中。将此伺服电机插入通道0。

连接方式类似于PiConZero等控制器,但可能需要在其他电机板上进行一些焊接工作。现在你已经将这个电机连接好了,我们需要编写一些代码来测试它。

编写控制伺服电机的代码

一些板库可以直接将度数转换为伺服电机移动脉冲。使用这个库,我们需要编写相应的代码。一些计算结果会产生一些常数,一旦我们知道我们使用的是哪个伺服控制器和伺服电机,这些常数就不会改变。我们可以将这些结果存储为常数并重复使用。

你可以将此类计算的结果存储在变量中,但让计算机来做有几个优点:

  • 计算机在计算这些常数方面非常出色且快速。它可能会产生舍入误差,但可能比人从计算器复制时犯的错误要少。

  • 很明显,这些数字是从哪里来的,以及如何计算它们。将来自计算器的“魔法数字”放入其中会使它更难看出其来源。

  • 如果你更改调整因子,计算将保持最新。

  • 如果你发现错误,在这里更改它很容易。

    小贴士

    在代码中设置计算时,使用描述性的变量名和注释——描述性有助于你理解代码并检查数学是否合理。你会在编写代码后多次阅读代码,因此这个原则适用于任何变量或函数名。

让我们编写一些测试代码,将伺服电机移动到用户输入的度数位置。将此代码放入 servo_type_position.py 文件中:

  1. 我们用于机器人的 Raspi_MotorHAT 库有一个 PWM 模块,我们导入并创建一个对象来管理。我们再次使用 atexit 来确保控制器停止向电机发送信号:

    from Raspi_MotorHAT.Raspi_PWM_Servo_Driver import PWM
    import atexit
    
  2. 在设置PWM设备时,我们必须指定地址——它与我们用于电机的相同I2C设备,并且具有相同的地址:

    pwm = PWM(0x6f)
    
  3. 伺服电机以50 Hz的频率工作;然而,我们可以使用100 Hz,这样我们的电机也可以驱动。如果频率低,直流电机很容易卡住。这个频率将成为我们PWM频率的时间基准,我们将其保持在计算中使用:

    # This sets the timebase for it all
    pwm_frequency = 100
    pwm.setPWMFreq(pwm_frequency)
    
  4. 让我们将中间位置定义为0度。对于大多数伺服电机,旋转到-90度需要一个1毫秒的脉冲;移动到中心位置需要1.5毫秒:

    m or p means far less than servo_mid_point_ms.
    
  5. 旋转到90度需要一个2毫秒的脉冲;这是从中间点0.5毫秒,这为我们提供了90度的主校准点:

    # What a deflection of 90 degrees is in pulse length in milliseconds
    deflect_90_in_ms = 0.5
    
  6. 我们芯片上脉冲的长度也取决于频率。这个芯片将脉冲的大小指定为每周期的步数,使用4,096步(12位)来表示这一点。更高的频率需要脉冲中更多的步数来保持脉冲长度在毫秒内。我们可以为这个比率制作一个;每毫秒步数:

    # Frequency is 1 divided by period, but working ms, we can use 1000
    period_in_ms = 1000 / pwm_frequency
    # The chip has 4096 steps in each period.
    pulse_steps = 4096
    # Steps for every millisecond.
    steps_per_ms = pulse_steps / period_in_ms 
    
  7. 现在我们有了每毫秒的步数,并且知道90度脉冲应该有多少毫秒,我们可以得到每度步数的比率。我们也可以使用这个比率来重新定义我们的中间点为步数:

    # Steps for a degree.
    steps_per_degree = (deflect_90_in_ms * steps_per_ms) / 90
    # Mid-point of the servo in steps
    servo_mid_point_steps = servo_mid_point_ms * steps_per_ms
    
  8. 我们可以在 convert_degrees_to_pwm 函数中使用这些常数来获取任何角度所需的步数:

    def convert_degrees_to_steps(position):
        return int(servo_mid_point_steps + (position * steps_per_degree))
    
  9. 在移动任何东西之前,我们应该确保系统停止。我们通过将PWM设置为 4096 来停止。这个数字可能听起来很奇怪,但它不是提供一个长脉冲,而是在控制板上打开一个额外的位,这将完全关闭伺服引脚。关闭引脚会释放/放松伺服电机。否则,电机将试图寻找/保持我们给出的最后一个位置,直到断电。我们可以使用 atexit 来做这件事,就像我们停止电机时做的那样:

    atexit.register(pwm.setPWM, 0, 0, 4096)
    
  10. 我们现在可以在循环中请求用户输入。Python中的 input 函数会要求用户输入一些内容,并将其存储在一个变量中。我们将它转换为整数以使用:

    while True:
        position = int(input("Type your position in degrees (90 to -90, 0 is middle): "))
    
  11. 然后,我们可以使用之前的计算将位置转换为结束步数:

        end_step = convert_degrees_to_steps(position)
    
  12. 我们可以使用 pwm.setPWM 来以步进的方式设置脉冲。它需要伺服通道号、一个起始步数,我们将它保持在0,以及一个结束步数,这是我们之前计算过的:

        pwm.setPWM(0, 0, end_step)
    

你现在可以打开机器人并给它发送这段代码。当你运行这段代码时,它会要求你输入一个数字。从数字0开始。当你按下 Enter 键时,你会听到伺服电机移动。

你现在可以尝试其他值,但不要给出-90到90度之外的值,因为这可能会损坏伺服电机。我们稍后会添加代码来防止这种损坏。如果这个系统工作正常,你应该看到伺服电机在各个不同值之间移动。

故障排除

如果你发现运行时出现问题,请尝试以下方法:

  • 确保伺服电机已正确插入端口,并且方向正确。S引脚应插入大多数伺服电机上的黄色电缆。

  • 如果出现很多抖动或无法到达正确位置的情况,可能意味着你的电池电量不足——请确保它们是充满电的。

  • 当运行来自其他章节的直流电机行为时,如果伺服电机下垂,这也可能是由于电池电量较低。确保你使用金属氢化物充电电池。

在我们继续之前,这个电机HAT是如何控制伺服电机和直流电机的?让我们更仔细地看看它。

控制直流电机和伺服电机

我为这本书(以及读者可以选择其他)建议的HAT基于PCA9685芯片,它被广泛用于制造像这样的机器人。它是一个多通道PWM控制器。查看图解以了解它在机器人中的连接概述:

图片

图10.7 – 电机板框图

图10.7 展示了全功能电机HAT的框图。这不是布线图,它仍然是一个功能框图,但它显示了板上的组件和连接。最左边是Raspberry Pi,有一条线连接到PCA9685芯片。这条线是进入帽子的I2C通信,由标记的灰色盒子表示。

PWM发生器有许多输出连接。其中八个输出连接到控制TB6612电机驱动器。这些驱动器有适合直流电机(或步进电机)的电源输出。它们仍然在灰色盒子中,因为它们是帽子的一部分。我们将这些电源输出连接到我们的右电机(m1)、左电机(m2),并为其他电机留有m3/m4连接空间。

伺服通道直接暴露了这四个PWM输出。我们将将摇杆伺服电机连接到一个输出,倾斜连接到另一个。

在前面的代码中,我提到以100 Hz而不是50 Hz驱动PWM芯片。这是因为如果我们结合伺服电机和直流电机,芯片的时间基准适用于所有PWM输出,即使占空比(开启时间与关闭时间的比率)发生变化。

现在你已经测试了基础知识,我们可以校准伺服电机,找到0位置并确保90度移动的量是正确的。

校准你的伺服电机

伺服电机臂允许你看到伺服电机的运动。0应该接近中间:

  1. 首先,使用带有臂的螺丝刀将0与中间对齐。松开它,抬起它,将其移动到中间,然后再次按下。不要拧得太紧,因为我们很快就会再次移除它。

    重要提示

    如果伺服电机的运动受阻,包括尝试将其移动到其极限之外,它将拉高电流以尝试达到位置。这样卡住伺服电机可能会导致大量热量和损坏的电机。

  2. 现在尝试输入90和-90。你可能发现两边没有达到90度,因为伺服电机可能会有轻微的差异。增加deflect_90_in_ms的值来调整电机范围。以小的0.1增量这样做,因为在这里走得太远可能会导致伺服电机损坏。

  3. 当你校准了你的伺服电机后,在开始下一步之前让每个伺服电机移动到位置0是一个好主意。你可以通过将第二个伺服电机插入伺服电机的连接器上的通道1,然后交换每个调用pwm.setPWM方法的第一个参数从0到1来实现这一点。

你现在已经测试了一些基本的伺服代码,然后尝试了两个伺服电机和两个通道。现在我们可以使用伺服电机构建一个俯仰和倾斜机制来指向传感器。

添加俯仰和倾斜机制

现在我们将构建并添加一个俯仰和倾斜伺服电机机制到我们的机器人中。这个机制就像我们机器人的头部,可以将其上的传感器安装在上面。俯仰和倾斜机制,如图图10.8所示,在伺服电机的控制下通过两个轴移动传感器(或任何其他东西)。

Pan表示向左或向右转动。Tilt表示向上或向下倾斜:

图片

图10.8 – 典型套件中的俯仰和倾斜机制

类似于图10.8的套件可以从Mouser购买,以及Adafruit的商店。你可能需要单独购买伺服电机。还有其他类型的俯仰-倾斜。确保它是使用两个伺服电机的类型,并在制造商的文档中查找不同之处。我们组装套件,将其安装到我们的机器人上,并将其插入控制器。

我们带有伺服电机的机器人框图看起来像图10.9

图片

图10.9 – 添加伺服电机后的机器人框图

图10.9中的框图通过添加俯仰和倾斜伺服电机扩展了前一章的框图。这些电机连接到电机HAT。

现在你已经看到了它在机器人框图中的位置,是时候构建它了!

组装套件

您需要您的云台套件、一把螺丝刀和一把剪刀。图10.10展示了机构的部件布局:

图10.10 – 云台-倾斜机构的部件

注意我在图10.10中使用的不同塑料部件的术语;我使用这些术语进行组装。这些螺丝旁边就是套件中附带的那几个。通常在伺服电机的硬件袋里会有自攻M2螺丝——请确保您有这些。

重要提示

这里的塑料可能会弹飞,所以请在没有安全眼镜的情况下不要这样做。注意房间里的其他人以及飞溅的小而尖锐的塑料碎片。请为此步骤戴上安全眼镜!

一旦您准备好了部件,我们将从组装底座开始:

图10.11 – 准备云台底座

让我们按照图10.11所示,在以下步骤的帮助下组装底座:

  1. 测量并切割一个十字形的伺服电机叶片以适应底座。它必须适应底座中的凸脊。将伺服电机叶片的长臂缩短到略超过三个孔,并用剪刀使它们稍微薄一些。

  2. 将伺服电机叶片对齐到底座上,使臂位于凹槽区域,伺服电机叶片领圈朝向底座。

  3. 找到四个长M2自攻螺丝。

  4. 将伺服电机叶片安装到底座上。请注意,对于一些伺服电机叶片,只有水平或垂直的螺丝可能对齐;两个就足够了,但四个更安全。

我们的基础现在准备好了。接下来,我们将组装左臂:

图10.12 – 组装左臂和倾斜板

要组装左臂,请执行以下步骤:

  1. 将螺柱与倾斜板的孔对齐,如图10.12所示。

  2. 将螺柱推入这个孔中;您需要在下一步中保持这个位置。

  3. 取一个伺服电机和两个带领圈的螺丝。伺服电机 resting on the two brackets on the tilt plate,并且拧紧后,可以固定左臂。确保在拧紧之前,伺服电机的主轴与螺柱和孔对齐。

太好了!现在让我们继续组装右臂:

图10.13 – 组装右臂

按照以下步骤组装臂:

  1. 要组装右臂,您需要另一个伺服电机叶片——这次是只有领圈和单一直臂的那种。如图10.13所示,伺服电机叶片需要修剪以适应右臂上的预定凹槽。使用一个M2自攻螺丝将这个叶片固定到机构的右臂上。您所安装的伺服电机叶片位于机构的前部。

  2. 将这个组件翻转过来,并将另一个伺服电机(这是云台伺服电机)按照指示插入槽中。

  3. 它应该使主轴朝向照片的底部,如第三面板所示。这个伺服电机朝下。

我们接下来的步骤是将我们刚刚创建的左右臂组合起来。请按照以下步骤进行:

图10.14 – 组合左臂和右臂

您需要遵循以下步骤来组合手臂:

  1. 图10.14显示了如何将机构的左右臂组合在一起。在组合手臂时,右臂伺服电机螺母的套筒应该夹在您拧到倾斜板上的倾斜伺服电机上。

  2. 左臂组装中的云台伺服电机适合到一个匹配的凹槽中。

  3. 使用一个短螺丝将右臂的套筒固定到倾斜伺服电机上,保持倾斜板直立。

  4. 使用两颗小细螺丝将两个手臂拧在一起。

我们几乎完成了。最后一部分是将我们最初创建的底座与机构的其余部分组合起来:

图片

图10.15 – 将底座与机构组合

按照以下程序继续操作:

  1. 图10.15显示了如何将机构连接到底座上。将固定在底座上的伺服电机螺母从伺服电机螺杆推到云台伺服电机的轴上。调整使其对齐,使底座的长轴与机构的底部对齐。

  2. 使用非常短的螺丝将套筒固定到伺服电机上。

  3. 最后一个面板显示了完全组装好的云台和倾斜机构。

您已经看到了云台和倾斜机构是如何围绕伺服电机组装的。组装这样的结构对于了解这些机构的工作原理以及了解伺服电机移动时的感觉非常有价值。现在云台和倾斜机构已经组装好,我们可以在移动头部之前将其连接到机器人上。

将云台和倾斜机构连接到机器人上

机构需要成为机器人的一部分。我们需要将其物理连接并接线,以便电机控制器可以发送信号给它。

图10.16显示了如何将云台和倾斜机构连接到机器人:

图片

图10.16 – 将云台和倾斜机构连接到机器人

按照以下说明以及图10.16中显示的步骤操作:

  1. 为了做到这一点,您需要两个长螺栓和两个螺母来将云台和倾斜机构连接到机器人上。

  2. 将螺栓插入云台和倾斜底座的短端,使其指向下方。

  3. 我推荐的底盘前方有一个槽,这对于线传感器来说非常方便。这个槽非常适合安装这个云台和倾斜机构,通过槽中的螺丝固定。在另一个底盘上,您可能需要测量并钻孔来安装这个。

  4. 在机器人下方拧紧螺母。

  5. 将伺服电机接线。倾斜(上下伺服电机)应插入伺服通道0,而云台(左右)应插入伺服通道1。

恭喜!您的机器人准备好了,硬件设置已完成。现在您可以编写代码并尝试为您的机器人测试新的头部。那么,让我们直接开始吧。

创建云台和倾斜代码

我们分层构建我们的俯仰和滚转代码。我们创建一个 Servos 类并将之前的计算放入其中。我们设置我们的机器人类以拥有 Servos 类的实例,以及访问用于俯仰和滚转的伺服器的方法。

创建伺服器对象

在这个类中,我们封装(内部管理)将角度转换为伺服器动作以及我们的伺服板的一些怪癖,如通道号。我们为了这个目的在 servos.py 文件中创建了一个 Servos 类:

  1. servos.py 文件以一个导入开始,然后直接进入构造函数(__init__ 函数):

    deflect_90_in_ms so that it can be overridden with the value obtained while calibrating your servos.
    
  2. 接下来,我们将添加一个注释,这样当我们使用 Servos 类时,我们可以看到我们的意图。这里的文本将作为某些代码编辑器中我们类的帮助信息显示:

         """addr: The i2c address of the PWM chip.
    deflect_90_in_ms: set this to calibrate the servo motors. 
         it is what a deflection of 90 degrees is
         in terms of a pulse length in milliseconds."""
    

    构造函数顶部的三引号字符串是一种在 Python 中称为 docstring 的约定。任何在函数、方法、类或文件顶部声明的字符串都成为一种特殊的注释,许多编辑器使用它来显示库的更多帮助。这在任何类型的库层中都很有用。使用 docstring 的约定将补充我们从测试代码中携带的所有解释性注释。

  3. __init__ 方法的下一部分应该看起来很熟悉。它设置了在 编写控制伺服器的代码 部分的 步骤 37 中创建的所有计算,在 servos 对象内部。我们将一些变量存储在 self._pwm 中以供以后使用,其余的是中间计算:

            self._pwm = PWM(addr)
            # This sets the timebase for it all
            pwm_frequency = 100
            self._pwm.setPWMFreq(pwm_frequency)
            # Mid-point of the servo pulse length in milliseconds.
            servo_mid_point_ms = 1.5
            # Frequency is 1/period, but working ms, we can use 1000
            period_in_ms = 1000 / pwm_frequency
            # The chip has 4096 steps in each period.
            pulse_steps = 4096
            # Steps for every millisecond.
            steps_per_ms = pulse_steps / period_in_ms
            # Steps for a degree
            self.steps_per_degree = (deflect_90_in_ms * steps_per_ms) / 90
            # mid-point of the servo in steps
            self.servo_mid_point_steps = servo_mid_point_ms * steps_per_ms
    
  4. __init__ 方法的最后部分,我们创建 self._channels;这个变量使我们能够使用通道号 0、1、2 和 3,并将它们映射到板上的怪异数字:

            # Map for channels
            self._channels = [0, 1, 14, 15]
    
  5. 接下来,我们想要一个安全功能来关闭所有伺服电机。发送没有任何脉冲即可做到这一点,并释放伺服器,保护电源并防止电机损坏。此功能使用了在 编写控制伺服器的代码食谱步骤 9 中看到的技巧,设置开始时间为 04096 以生成无脉冲的关闭标志:

        def stop_all(self):
            # 0 in start is nothing, 4096 sets the OFF bit.
            off_bit = 4096
            self._pwm.setPWM(self.channels[0], 0, off_bit)
            self._pwm.setPWM(self.channels[1], 0, off_bit)
            self._pwm.setPWM(self.channels[2], 0, off_bit)
            self._pwm.setPWM(self.channels[3], 0, off_bit)
    
  6. 接下来是转换函数,我们在 步骤 8编写控制伺服器的代码 部分中看到了它,但将其本地化到类中。我们只会在内部使用这个转换。Python 的惯例是在它前面加上下划线:

        def _convert_degrees_to_steps(self, position):
            return int(self.servo_mid_point_steps + (position * self.steps_per_degree))
    
  7. 此外,我们还需要一个方法来将伺服器移动到某个角度。它将接受一个通道号和一个角度。我在这个方法中使用了另一个 docstring 来解释它做什么以及限制是什么:

        def set_servo_angle(self, channel, angle):
            """position: The position in degrees from the center. -90 to 90"""        
    
  8. 下几行验证输入。它限制角度以保护伺服器免受超出范围的值的损害,如果超出范围,将引发 Python 异常。异常会将此类问题推送到调用系统,直到其中一个处理它,如果没有人处理,则终止代码:

            # Validate
            if angle > 90 or angle < -90:
                raise ValueError("Angle outside of range")
    
  9. 此方法最后两行设置位置:

          # Then set the position
          off_step = self._convert_degrees_to_steps(angle)
          self._pwm.setPWM(self.channels[channel], 0, off_step)
    

您可以在https://github.com/PacktPublishing/Learn-Robotics-Programming-Second-Edition/blob/master/chapter10/servos.py找到完整的代码。

这个类现在可以集成到我们的机器人中。让我们在下一节中这样做。

将伺服电机添加到机器人类中

在我们开始在行为中使用前面的Servos类之前,我们将将其集成到我们的robot.py文件中,并为特定的伺服电机分配特定的用途。这样,一个行为可以通过交换机器人类来使用配置不同的云台和倾斜机制的机器人:

  1. 接下来,我们需要将其修补到robot.py中的Robot类中。首先,让我们在leds导入之后导入它:

    import leds_led_shim
    from servos import Servos
    ...
    
  2. 然后,在Robot的构造函数中设置servos对象,传递地址:

    class Robot:
        def __init__(self, motorhat_addr=0x6f):
           # Setup the motorhat with the passed in address
           self._mh = Raspi_MotorHAT(addr=motorhat_addr)
           # get local variable for each motor
           self.left_motor = self._mh.getMotor(1)
           self.right_motor = self._mh.getMotor(2)
           # Setup the Leds
           self.leds = leds_led_shim.Leds()
           # Set up servo motors for pan and tilt.
           self.servos = Servos(addr=motorhat_addr)
           # ensure the motors get stopped when the code exits
           atexit.register(self.stop_all)
    ...
    
  3. 现在,我们应该确保当机器人停止时它也停止,通过将其添加到stop_all代码中:

        def stop_all(self):
            self.stop_motors()
            # Clear the display
            self.leds.clear()
            self.leds.show()
            # Reset the servos
            self.servos.stop_all()
    ...
    
  4. robot中最后要做的就是在映射设置云台和倾斜值到实际的伺服电机:

        def set_pan(self, angle):
            self.servos.set_servo_angle(1, angle)
    
        def set_tilt(self, angle):
            self.servos.set_servo_angle(0, angle)
    

我们的Robot对象现在有方法可以与机器人底盘上的云台和倾斜伺服电机交互。这为我们提供了对机器人上伺服电机的特定控制,并在行为中提供了一个使用层。在下一节中,我们将创建一个使用这些方法来制作圆圈的行为。

使云台和倾斜头旋转圆圈

在本节中,我们使云台和倾斜头以大约30度的微小圆圈移动。这种行为展示了机制和与之通信的代码部分。代码创建了一种重复的动画行为,它使用一个时间基准——当前时间。我们使用时间基准来绘制圆圈:

  1. 创建一个新文件;我建议命名为circle_pan_tilt_behavior.py

  2. 我们从一系列导入开始;Robot对象、math库和一些计时:

    math library as we are going to use sine and cosine to calculate that circle.
    
  3. 由于我们的行为具有局部数据,我们将将其放入一个自己的类中。构造函数(the__init__方法)接受Robot对象:

    class CirclePanTiltBehavior:
        def __init__(self, the_robot):
            self.robot = the_robot
    
  4. 这个行为本质上是一种动画,因此它有一个时间和每个圆的帧数或位置。我们使用frames_per_circle变量来调整它经过的步数:

            self.current_time = 0
            self.frames_per_circle = 50
    
  5. 数学函数以弧度为单位工作。一个完整的圆周弧度是2倍的π。我们将这个值除以frames_per_circle,得到一个乘数,我们称之为radians_per_frame。我们可以用当前帧乘以这个值,以得到圆周的角度:

    radians_per_frame.
    
  6. 由于这是一个圆,它也应该有一个半径,表示我们的伺服电机从中间偏移的距离:

            self.radius = 30
    
  7. 行为中的下一个方法是run。这使行为进入一个while True循环,因此它将一直运行,直到用户停止它:

        def run(self):
            while True:
    
  8. 当我们的行为运行时,我们使用frames_per_circlecurrent_time进行取模(余数)操作,将其转换为帧数。取模操作将数字限制在零和帧数之间:

                frame_number = self.current_time % self.frames_per_circle
    
  9. 然后,我们将这个frame_number变量转换回弧度,即圆的位置,通过将其与radians_per_frame相乘。这个乘法给我们一个我们称之为frame_in_radians的值:

                frame_in_radians = frame_number * self.radians_per_frame
    
  10. 绘制圆的公式是将一个轴设置为角度的余弦值,乘以半径,另一个轴设置为角度的正弦值,乘以半径。因此,我们计算这个值,并将每个轴输入到伺服电机中:

                self.robot.set_pan(self.radius * math.cos(frame_in_radians))
                self.robot.set_tilt(self.radius * math.sin(frame_in_radians))
    
  11. 我们执行一个小的sleep(),给电机时间到达其位置,然后向当前时间添加一个值:

                sleep(0.05)
                self.current_time += 1
    
  12. 那个整个run方法(步骤7-11)如下:

        def run(self):
            while True:
                frame_number = self.current_time % self.frames_per_circle
                frame_in_radians = frame_number * self.radians_per_frame
                self.robot.set_pan(self.radius * math.cos(frame_in_radians))
                self.robot.set_tilt(self.radius * math.sin(frame_in_radians))
                sleep(0.05)
                self.current_time += 1
    
  13. 最后,我们只想启动并运行我们的行为:

    bot = Robot()
    behavior = CirclePanTiltBehavior(bot)
    behavior.run()
    

因此,我们构建了一个Servos类,并将其集成到我们的Robot代码中,以控制俯仰和倾斜机构。您已经看到了以动画方式移动伺服电机的代码。我们可以在下一节中将这些与物理俯仰-倾斜结合起来,以查看其运行情况。

运行它

您需要通过SFTP将servos.pyrobot.pycircle_pan_tilt_behavior.py发送到Raspberry Pi。在Raspberry Pi上,键入python3 circle_pan_tilt_behaviour.py以查看它。现在头部应该正在画圈。

这段代码是设备的演示代码,但将来可以通过在它上面安装摄像头来使用相同的设备追踪人脸。在这里使用帧来创建动画对于使机器人进行平滑的预定动作非常重要,控制随时间的小范围动作。

故障排除

如果这仍然无法运行,请尝试以下操作:

  • 确保您已经按照编写控制伺服电机的代码部分所示测试了伺服电机。对于这个测试,没有必要拆卸俯仰-倾斜机构,但请确保您已经使用那里的代码使伺服电机移动,并遵循了那个故障排除部分。

  • 如果在运行时看到错误,请确保您正在使用python3运行。请确保您已经检查了代码中的拼写错误。

  • 如果代码无法导入任何内容,请确保您已经复制了所有前面的文件,并且您已经在前面的章节中安装/设置了库。

  • 如果电机移动到极端位置,您可能错过了校准它们的步骤。您需要卸下每个伺服电机,将其从伺服齿轮上弹出,使用编写控制伺服电机的代码部分中的测试代码将其发送到位置0,然后在中立位置将其推回,并重新拧紧。

  • 如果伺服电机完全拒绝移动,请检查它是否已经正确连接,确保G对应黑色伺服电线,V对应红色电线,S对应伺服电机的黄色信号线。机器人代码假设伺服电机连接到电机控制板的通道0和1。

  • 确保电线或其绝缘没有断裂。我见过一批这种类型的伺服电机存在电线问题,不得不退货。您不应该看到任何裸露的电线部分。

  • 确保连接器被牢固地推入。如果在这里松动,那么信号可能无法到达伺服电机。

  • 再次强调:低电量会使伺服电机抖动或无法达到设定点。

现在,您应该已经排除了运行这个圆形伺服电机行为时常见的常见问题,并看到头部正在做小而慢的圆圈。我们将在后面的章节中使用这个系统来观察面部。

构建扫描声纳

使用我们在第8章中附加的距离传感器,使用Python编程距离传感器,与云台/倾斜机构一起,我们可以设置一个有趣的实验。如果我们将距离传感器连接到头部,然后缓慢地向一个方向(例如,倾斜方向)移动,我们就可以创建一个区域传感器的扫描。然后我们可以使用一些Python代码来绘制这个,制作一个机器人前方物体的小型地图。

与这种组合类似的传感器可以在高级机器人(如Boston Dynamics的产品)和自动驾驶汽车中找到。激光雷达和雷达传感器使用快速旋转的鼓和激光光或无线电频率来执行比我们的示例快得多的扫描。激光雷达传感器开始出现在业余市场上,但仍然有点昂贵。

为了可视化这一点,我们将使用一种特殊的图表 – 极坐标图。这种图表围绕一个圆圈绘制,其中x轴是我们围绕圆圈的位置(以弧度表示 – π的倍数)。y轴表示绘制点距离圆心的距离 – 因此,较大的值将更远离中心。这对于这个例子非常合适,因为我们正在通过角度扫描伺服电机并接收距离数据。我们必须考虑到伺服电机以度为单位工作,并将它们转换为弧度以供图表输出。

连接传感器

在这个步骤中,我们将延长传感器的电线并将传感器重新定位到云台/倾斜头上。首先关闭机器人的电源:

图10.17 – 弹出传感器并准备两根电线

按照以下步骤重新定位传感器,并参考图10.17:

  1. 您需要将一个距离传感器从机器人前部的支架中弹出。我使用了左侧的。

  2. 在那里的云台/倾斜头上,识别出小槽。

  3. 制作两根单芯电线或三明治夹具。每根大约18厘米的长度应该足够了:

    图10.18 – 步骤4到6;将电线放入槽中

  4. 对于每一侧,首先将电线穿过图10.18中所示的指示槽。

  5. 稍微弯曲它,将两端几乎推到一起,这样它就不会直接掉出来。

  6. 以相同的方式准备另一侧,这样两侧就都准备好了:

    图10.19 – 将电线缠绕在传感器下方

  7. 将传感器放置到位,并将从头部顶部来的左电线弯曲到传感器的正面。

  8. 现在按照图10.19中白色箭头所指的大圆形元件(超声波传感器)下弯。

  9. 将下面伸出的线绕过传感器弯曲:

    图10.20 – 拧线并重复右侧

  10. 将这根线绕在左侧传感器的顶部,并将两个线端合并在一起。

  11. 将顶部和底部的端头拧在一起,如图图10.20所示。

  12. 对右侧的线重复步骤811

    图10.21 – 传感器临时固定在俯仰头

  13. 现在传感器应该临时固定在俯仰头上,如图图10.21所示。你现在可以将其重新连线了:

    图10.22 – 延长线并插入线

    你可能需要延长跳线,如图图10.22所示。你很可能还有更多的公母跳线,所以使用四根这样的线来延长传感器。如果可能的话,使用相同的颜色来帮助确保你做出了相同的连接。

  14. 现在将线连接到传感器上。请参考第8章使用Python编程距离传感器,以获取参考。

我建议运行第8章使用Python编程距离传感器中的test_distance_sensor.py代码,并在继续之前检查传感器是否工作。

现在你已经将传感器安装在了头上,当我们指令伺服电机移动时,它也会移动。让我们确保我们首先在Raspberry Pi上安装了编写代码所需的正确工具。

安装库

代码将使用Python工具matplotlib来输出数据。它制作一个极坐标图,从一点径向出发的图形,看起来可能像你在电影中看到的声纳扫描。为此,你需要在你的Raspberry Pi上安装Matplotlib(及其依赖项)。

通过SSH(putty)连接到Raspberry Pi,并输入以下命令以获取Matplotlib所需的包:

$ sudo apt update
$ sudo apt install libatlas3-base libgfortran5

下一步你需要的是Matplotlib本身:

$ pip3 install matplotlib

Matplotlib可能需要一些时间来安装,并且在安装过程中可能会安装许多辅助包。

安装完库后,你就可以开始编写代码了。

行为代码

为了制作图表并获取数据,代码将把传感器移动到一侧的完全范围,然后逐步移动回来(例如,每次5度),并在每次移动时进行测量。这个例子将展示如何使用传感器和伺服电机一起,并介绍其他查看传感器数据的方法。

创建一个名为sonar_scan.py的文件,并按照以下步骤添加其内容:

  1. 我们将从一些导入开始;time导入,我们可以用它给电机和传感器一些工作时间,math将角度转换为弧度,matplotlib用于显示,以及robot用于与机器人接口:

    import time
    import math
    import matplotlib.pyplot as plt
    from robot import Robot
    
  2. 然后,我们有设置参数。我们将这些参数放在这里,以鼓励你尝试不同的转向速度和范围:

    start_scan =0
    lower_bound = -90
    upper_bound = 90
    scan_step = 5
    
  3. 接下来,让我们初始化Robot对象并确保倾斜方向水平:

    the_robot = Robot()
    the_robot.set_tilt(0)
    
  4. 我们需要准备一个存储扫描数据的地方。我们将使用一个从度数到感知值的字典映射:

    scan_data = {}
    
  5. 扫描循环从下限开始,以扫描步长递增,直到上限;这给出了我们的朝向范围:

    for facing in range(lower_bound, upper_bound, scan_step):
    
  6. 在循环中的每个朝向,我们需要指向传感器,并等待伺服电机移动以及传感器获取读数。我们在这里取反朝向,因为伺服电机旋转的方向与极坐标图相反:

        the_robot.set_pan(-facing)
        time.sleep(0.1)
    
  7. 然后,我们将传感器距离以厘米为单位存储在扫描数据的每个朝向中,使用朝向作为其键:

        scan_data[facing] = the_robot.left_distance_sensor.distance * 100
    
  8. 以下循环将朝向转换为弧度。伺服电机以度为单位工作,但Matplotlib的一个特性是极轴必须以弧度表示:

    axis = [math.radians(facing) for facing in scan_data.keys()]
    
  9. 现在我们制作极坐标图,告诉Matplotlib轴和数据,我们想要一条带有g-的绿色线:

    plt.polar(axis, list(scan_data.values()), 'g-')
    
  10. 最后一行将此图写入png图像文件,因此我们可以使用scp从树莓派下载并查看图表:

    plt.savefig("scan.png")
    

将此代码放在树莓派上,将机器人放在距离不到一米的障碍物附近的地方,并使用python3 sonar_scan.py运行代码。你应该看到伺服电机在边界范围内进行扫描。

当运行时,scan.png输出应类似于图10.23

图10.23 – 我实验室的声纳扫描图

图10.23显示了极坐标图上的声纳扫描输出。它以度为单位显示测量值,一条绿色线追踪传感器前方检测到的物品的轮廓。在这张图片中,我的下限是-90,上限是90,步长为2度,以获得略微更精细的分辨率。

选择更精细的分辨率(小于2度)将使其变慢。sleep值可以调整,但较低的值可能导致伺服电机未稳定或传感器未产生进一步的读数。

故障排除

如果在运行此工具时遇到问题,请尝试以下操作:

  • 首先,通过遵循第8章中的故障排除和测试步骤来确保距离传感器工作,使用Python编程距离传感器。验证接线并使用测试代码。

  • 验证伺服电机是否正常工作,如前文创建俯仰和倾斜代码部分所示。遵循那里的故障排除程序。

  • 如果在运行代码时出现错误,请确保已安装所有必需的库。

  • 检查你输入的代码中是否有错别字。

  • 记住,文件输出将在树莓派上,所以你需要将其复制回来查看。

  • 在将值放入plt.polar方法之前打印值可能会有所帮助。添加以下内容:

    print(axis)
    print(scan_data.values())
    

现在你应该能够制作声纳扫描并获得上图所示的图表。我建议你尝试不同的值来创建不同的图表分辨率,并将不同的物体组合放在传感器前。

让我们总结一下本章中我们看到的内容。

摘要

在本章中,你学习了伺服电机、如何使用电机控制器控制它们以及它们的工作原理。你用它们构建了一个旋转和倾斜机制,并向Robot对象添加了代码以与之协同工作。最后,你通过环绕行为展示了所有部件。

你将能够使用你看到的代码来控制其他伺服电机系统,例如机械臂。动画风格技术对于平滑运动和圆形运动非常有用。我在控制SpiderBot腿部的18个电机时使用了类似这样的系统。

你已经看到了如何使用带有传感器头部伺服机构来制作某种世界地图,并将其与更大、更昂贵的机器人上使用的激光雷达系统相关联。

在下一章中,我们将探讨另一种使用编码器来绘制和观察世界的方法。这些传感器将检测我们的机器人上的轮子转动,以确定我们的机器人是如何移动的。

练习

  1. 考虑你如何构建其他基于伺服的扩展。机器人臂至少需要四个伺服电机,但一个简单的夹爪/抓取器可以使用我们机器人剩余的两个通道。

  2. 环顾四周,看看夹爪套件、带有钳子的设计,以及可能的上/下控制。

  3. 你将如何编写这个夹爪的代码?你将向Robot对象添加什么?

  4. 你将为这个夹爪制作什么样的演示行为?

  5. 如果只是给夹爪一个不同的位置,它可能会移动得太猛烈。你将如何使其移动得更慢、更平稳?

进一步阅读

请参阅以下链接获取更多信息:

第十三章:第11章:使用Python编程编码器

在机器人技术中,感知电机轴和轮子的运动是有用的。我们曾在第7章中让机器人沿着路径行驶,使用Python驱动和转向——移动电机,但机器人不太可能一直保持在轨道上。检测这一点并行驶特定距离对于创建行为是有用的。本章将探讨传感器的选择,以及如何编程机器人以直线行驶和行驶特定距离。然后我们来看看如何进行特定的转弯。请注意,本章确实包含数学内容。但别担心,你会轻松跟上。

在本章中,你将学习以下主题:

  • 使用编码器测量行驶的距离

  • 将编码器连接到机器人上

  • 在Python中检测行驶的距离

  • 在直线上驾驶

  • 驾驶特定距离

  • 进行特定的转弯

技术要求

在我们开始之前,请确保你拥有以下部件:

  • Raspberry Pi机器人和上一章的代码:https://github.com/PacktPublishing/Learn-Robotics-Programming-Second-Edition/tree/master/chapter10

  • 两个带槽的速度传感器编码器。搜索带槽速度传感器、Arduino速度传感器、LM393速度传感器或光电中断传感器模块。包含术语3.3 V以确保其兼容性。参见我们使用的编码器部分中的图片。

  • 长的公对母跳线。

  • 使用直尺来测量你机器人轮子的尺寸——或者更好的是,如果你能使用的话,使用卡尺。

本章的代码可在GitHub上找到:https://github.com/PacktPublishing/Learn-Robotics-Programming-Second-Edition/tree/master/chapter11

查看以下视频以查看代码的实际运行效果:https://bit.ly/2XDFae0

使用编码器测量行驶的距离

编码器是随部件运动而改变值的传感器。它们检测轴的位置或轴旋转的次数。这些可以是旋转的或沿直线轨道感应的。

感知某物行驶的距离也被称为里程计,这些传感器也可以称为转速表,或简称转速计。在技术要求部分建议的传感器也可能在搜索中显示为Arduino转速计

机器使用编码器的位置

我们的机器人使用电子传感器。汽车和大型商用车辆使用电子或机械传感器作为速度表和转速表。

打印机和扫描仪将编码器与直流电机结合使用,作为步进电机的替代品。感应机器人转动过的弧度是伺服机构的一个基本组成部分,我们在第10章中看到了它,使用Python控制伺服电机。高端音频或电气测试/测量系统在控制旋钮中使用这些。这些是自包含的模块,看起来像音量旋钮,但用户可以无限旋转它们。

在对编码器有基本了解之后,现在让我们来看看它们的一些类型。

编码器的类型

图11.1显示了四种不同的编码器传感器,每个传感器都使用不同的机制来测量运动(1–3),以及面板4中的编码器轮和条:

图11.1 – 编码器传感器

这些传感器分为几个类别,如图11.1所示,并对应以下要点:

  1. 这是一个可变电阻器。这些模拟设备可以测量转动,但通常不允许连续旋转。它们在金属或电阻轨道上有机械刷,可能会磨损。这严格来说不是一个编码器,但很方便。在Raspberry Pi上,它们需要模拟到数字转换,所以不适合这个应用。伺服电机也使用这些。

  2. 这台电机包括由白色框突出的磁感应编码器,称为霍尔效应传感器。轮或条上的磁铁在传感器旁边通过,导致传感器值高低变化。

  3. 这是一个标准的光学传感器。使用带有红外光束通过的槽,它们感应光束被中断。计算机轨迹球、打印机和机器人使用这些。这些产生一系列脉冲。由于光束被中断,我们称它们为光断路器光学编码器光电断路器。我们将使用这种类型。

  4. 这显示了用于光学传感器的槽轮和槽条。带状条适合线性编码,而轮适合编码转动。它们都有透明和不透明部分。人们使用光传感器和光/暗单元来制作变体,但这些比较少见。

看过一些编码器的类型后,让我们更仔细地看看它们是如何表示速度和运动的。

编码绝对或相对位置

相对编码器可以编码位置的变化量 – 例如,我们顺时针或逆时针移动了一定数量的步数,或者沿着一个轴向前/向后移动,例如。它们只能通过计数通过的槽的数量来测量相对于上次测量的位置。这些也可以称为增量编码器。它们在硬件上既便宜又简单。相对编码器的限制在于它们记住前一个位置以创建下一个,并累积错误。

另一种类型是绝对编码器。这些可以沿或围绕轴编码位置到精确的位置。绝对编码器不需要有关先前位置的信息,但可能需要校准以确定编码如何与实际世界位置匹配。

以下图示展示了不同类型之间的比较:

图11.2 – 比较绝对和相对传感

图11.2中的图示说明了这种差异。左边的圆圈代表从记忆中的30度位置移动了30度。这

的工作原理,假设原始记忆中的位置是准确的。每次测量时,都会测量一个移动。右边的圆圈显示了从零点60度的位置。如果一个传感器可以告诉你某物在哪里,那么它是绝对的。如果你可以告诉你它移动了多少,那么它是相对的。

在粗略的形式中,可变电阻也可以是绝对编码器,如用于伺服电机以感知其位置的编码器。绝对位置编码可以通过轮或条上的光学或磁性标记来完成,从而实现绝对定位的极高精度。这些传感器可能体积庞大或昂贵,或需要许多输入。绝对编码器条或轮也被称为刻度尺。

编码方向和速度

基本相对编码衡量传感器通过的轮槽数量。这给出了速度和距离。通过使用稍微分开的两个传感器,你还可以编码方向。图11.3展示了这是如何工作的:

图11.3 – 使用多个传感器编码速度和方向

左边的系统编码速度。当槽通过传感器时,它们会产生电子脉冲。每个脉冲都有一个上升沿,它上升,以及一个下降沿,它下降。为了计数脉冲数,我们可以计数这些边缘。如果你用一个有方向的系统驱动电机,你可以使用像这样的单个传感器系统,正如我们将在我们的机器人中做的那样。

右边的轮通过添加第二个传感器来编码方向。槽边缘将在周期中的不同点改变传感器值,我们将其标记为1234。序列的方向指示轮的方向,以及其速度。由于有四个相位,这被称为正交编码。

工业机器人使用记录和回放接口。用户会按下记录按钮,推动一个机器人,例如一个手臂,通过一系列动作,然后按下停止按钮。用户已经记录了这一系列动作,他们可以要求机器人回放这些动作。

要构建具有这种记录和回放系统的机器人,或鼠标/轨迹球,方向是必要的信息,因此需要额外的复杂性来编码方向。

在我们的机器人中,我们将使用更便宜、更简单的选项,使用单个传感器仅测量相对速度。在我们的代码中,我们将假设每个车轮的速度传感器是按照我们驾驶的方向移动的。

我们使用的编码器

我们将在第6章“构建机器人基础 – 轮子、电源和布线”中添加的编码器轮上使用光学编码器,这些编码器轮的形状正好适合我们添加的编码器轮上方。这些编码器有数字输出,我们可以通过Python从它们计数脉冲来感知轮子转动的距离。图11.4 显示了我推荐的两类传感器:

图11.4 – 我们推荐的传感器类型

左边是FC-03光电中断模块,右边是Waveshare光电中断模块。具有3.3 V供电电压规范的模块是合适的。使用仅5 V的模块将需要电平转换器和额外的布线,如第8章“使用Python编程距离传感器”中所述。

我们将使用安装在电机轴上的编码器轮。这些轮子与车轮对齐。如果编码器轮的转速与车轮不同,我们需要考虑这一点。有一些条件它们无法考虑,例如打滑,以及车轮和轮胎的大小。安装在单独的惰轮上的编码器可以提供更好的数据,但它们连接到机器人并保持与地面的接触会更复杂。

现在,你已经看到了编码器能做什么,通过计算轮子转动的次数来确定你走了多远,你也看到了一些编码器的类型以及应该购买哪种(3.3 V)。你还快速地了解了它们是如何通过计数脉冲来工作的。在下一节中,我们将把它们集成到机器人中。

将编码器连接到机器人

我们的机器人现在变得相当忙碌,我们的树莓派位于编码器槽口之上。由于槽口位于树莓派下方,我们应该在将树莓派放回之前稍微提前布线。在拧紧树莓派后,我们将编码器连接到其GPIO,以及电源和地线。

图11.5 展示了在连接编码器后机器人的框图:

图11.5 – 带有编码器的机器人框图

这个框图添加了一个左编码器和右编码器,每个编码器都有一个箭头表示信息流,将它们连接到树莓派。突出显示的元素是新的。

在我们开始更改机器人和使其更难观察之前,我们需要知道编码器轮的槽口数量,以备后用:

图11.6 – 编码器轮

我展示在 图11.6 中的编码器轮最终有20个槽口。确保你使用你的编码器轮的槽口数量。

准备编码器

在我们能够使用编码器传感器之前,我们需要准备并安装它们。由于编码器将放在树莓派下方,我们现在应该将公对母跳线连接到传感器上。我建议用一小段绝缘胶带覆盖树莓派下方的任何突出来的电气接触点:

图11.7 – 带有电缆连接的传感器

显著的是,电缆应插入到图11.7所示的接地(GND)、电压(3 VVinVCC)和数字输出(D0/OUT)。如果存在,不要连接模拟输出(A0)引脚。如果可能,接地引脚应具有最深的颜色,或电压应是最浅的颜色。为了帮助保持清晰,我建议在信号线的末端缠绕一小段绝缘胶带。

重要提示

由于这些传感器的引脚配置可能不同,在将传感器放在树莓派下方之前,先拍一张传感器引脚标签的参考照片。

抬起树莓派

编码器传感器需要放在树莓派下方,因此需要轻轻地抬起树莓派(不要弄断电线)以适应它们。图11.8中的照片序列显示了如何抬起它:

图11.8 – 拧下并抬起树莓派

参考图11.8并执行以下步骤:

  1. 您需要小心地拧下固定树莓派到底盘的螺栓。将螺丝放在一边,以便在机器人上更换树莓派,这样它可以轻轻地抬起。

  2. 轻轻地将树莓派从机器人上抬起,不要弄断电线。照片显示了您的机器人应有的样子。

太好了!现在树莓派已经抬起,下面有空间安装编码器。

将编码器安装到底盘上

现在您已经连接了编码器,可以将它们安装到机器人底盘上。作为指导,图11.9显示了一个裸露的底盘,每种类型的传感器都已安装到位,以显示您应该将其推入的位置:

图11.9 – 安装编码器传感器

参考图11.9并完成以下步骤:

  1. 轻轻地将编码器推入编码器轮周围的槽中。

  2. 传感器应该摩擦地装入编码器轮子上的槽中,并保持在原位。

一旦这些安装到位,您就可以拧上螺丝,将树莓派固定到底盘上。您可能需要不同尺寸的垫圈来适应它。

重要提示

在此阶段,检查所有连接是否都已恢复原位。电机电线可能已经松动了。

在将编码器安装好并将树莓派放回原位后,您就可以开始连接电线了。

将编码器连接到树莓派的电线

我们现在可以使用面包板开始为传感器制作电线连接。我们将使用电路图来指导我们的连接。

图11.10显示了这些传感器的连接:

图11.10 – 将编码器连接到树莓皮的电路

图11.10显示了将编码器连接到树莓派的电路。这个图表是从第8章中看到的电路继续的,使用Python编程距离传感器。新的连接有更粗的电线。

图11.10的右侧是左右编码器模块。传感器将有一个VCCVIN3 V或仅V标签。将其连接到3.3 V线。找到标记为GND0V或仅G的GND引脚,并将其连接到面包板上的黑色/蓝色带。编码器还有一个标记为DO(数字输出)、SIG(信号)或仅S的引脚,连接到用于数字信号的GPIO引脚。我们将传感器的数字输出连接到GPIO引脚4(左侧编码器)和26(右侧编码器)。

一些编码器传感器有额外的连接,例如模拟输出AO),我们不会使用它,并将其保持未连接状态。

让我们执行以下步骤:

  1. 将传感器的引脚连接到面包板上。注意传感器的标签——有些有不同的引脚顺序。

  2. 将树莓派的GPIO引脚4和26连接到面包板上。

  3. 使用预切割的电线制作面包板的电源连接。

    重要提示

    这个机器人上的线对线点数使得添加新的连接或维修变得困难。尽管这超出了本书的范围,但制作定制的印刷电路板PCBs)可以使这堆电缆变得整洁得多。PCBs也更坚固,占用的空间更小。然而,改变它却要付出代价。

有可能通过跳过面包板,将传感器直接连接到树莓派;然而,面包板有助于分配电源连接并分组传感器连接。

我们在这个繁忙的机器人和其他连接的上下文中制作这个电路。如果传感器到机器人的电缆不够长,请使用一套额外的公对母电缆,使用与第10章中看到的超声波扫描相同的技巧。

在本节中,你已经学会了如何连接传感器,并使它们准备好编程。在下一节中,我们将编写一些代码来测试这些传感器,然后测量距离。

在Python中检测行驶的距离

使用编码器传感器设备需要我们计数脉冲。在本节中,我们将创建代码来打开电机并计数一段时间内的脉冲。代码验证传感器是否正确连接。然后我们将此代码作为行为集成到机器人类中。

介绍日志记录

到目前为止,在我们的Python代码中,我们一直在使用print来输出信息,以查看我们的机器人正在做什么。这没问题,但如果打印出我们可能想要检查的所有内容,打印可能会变得令人难以承受。日志记录允许我们仍然显示信息,但我们可以控制显示多少。通过导入logging模块,我们可以利用这一点。

首先,有日志级别,包括debuginfowarningerror。在修复问题或最初开发时,debug很有用——它可以显示一切——而info则用于显示比这少一点。warningerror级别仅保留用于那些类型的问题,因此当您对某些代码有相当信心时,您可以仅过滤到这个级别。logging.basicConfig函数允许我们配置日志级别和其他日志参数;您需要配置日志记录以启用infodebug日志。

您可以使用日志记录做的另一件事是使用logging.getLogger函数使用不同命名的记录器。这允许您为不同的模块设置不同的级别。使用命名记录器有助于在您使用的库模块中启用debug,同时在主模块上保持info

在本章的代码中,我们将开始使用日志记录和控制日志级别,以获取更多关于示例正在做什么的详细信息,可以随意打开和关闭部分。当我们介绍PID控制器时,这确实非常有用。我们可以在下一节中使用此日志记录来显示传感器脉冲计数。

简单计数

此代码计算每个轮子的信号引脚上升和下降的周期数,并在测试传感器时打印计数。运行此代码可以验证我们的传感器连接是否正常工作,让我们进行故障排除并继续前进。它还为跟踪轮子运动奠定了代码基础。我们运行电机大约1秒钟。请注意,此代码假设您是从第10章的代码开始的,即使用Python控制伺服电机

重要提示

您可以在https://github.com/PacktPublishing/Learn-Robotics-Programming-Second-Edition/blob/master/chapter11/test_encoders.py找到以下代码。

  1. 让我们创建一个名为test_encoders.py的文件,从常用的机器人类、importtime开始,并添加日志记录:

    from robot import Robot
    import time
    import logging
    ...
    
  2. 接下来,我们为GPIO Zero输入设备添加一个导入。我们可以使用它设置的引脚来计数脉冲:

    ...
    from gpiozero import DigitalInputDevice
    logger = logging.getLogger("test_encoders")
    ...
    

    我们还设置了一个与文件名匹配的系统日志记录器。

  3. 编码器生成脉冲;我们想要计数它们并跟踪它们的状态。我们使用多个它们。从一开始就创建一个类似乎是一个正确的策略。从这里,我们可以将我们的I/O引脚号传递给构造函数。它需要做的第一件事是设置脉冲计数器:

    ...
    class EncoderCounter(object):
        def __init__(self, pin_number):
            self.pulse_count = 0
            ...
    
  4. 仍然在构造函数中,我们需要设置设备和如何使用它来计数脉冲。设备有一个.pin对象,我们使用引脚号来设置它。.pin有一个when_changed事件,我们可以将处理程序放入其中,以便每次引脚改变时都会调用它。对于每个槽,引脚从上到下(上升和下降)变化:

          ...
          self.device = DigitalInputDevice(pin=pin_number)
          self.device.pin.when_changed = self.when_changed
      ...
    
  5. 我们需要为我们的类定义一个when_changed方法来将一个值添加到pulse_count。此方法必须尽可能小/快,因为GPIO Zero在后台为每次脉冲变化调用它。它需要一个time_ticks参数和一个state参数。我们不会使用time_ticks,所以用下划线标记:

        ...
        def when_changed(self, _, state):
            self.pulse_count += 1
    ...
    
  6. 我们可以设置我们的robot对象并为每侧的传感器创建一个EncoderCounter。我们将设备连接到引脚426

    ...
    bot = Robot()
    left_encoder = EncoderCounter(4)
    right_encoder = EncoderCounter(26)
    ...
    
  7. 要显示值,而不是仅仅使用sleep,我们循环,检查结束时间。在我们记录任何内容之前,logging.basicConfig设置日志参数。我们启动电机并进入主循环:

    ...
    stop_at_time = time.time() + 1
    logging.basicConfig(level=logging.INFO)
    bot.set_left(90)
    bot.set_right(90)
    while time.time() < stop_at_time:
        ...
    

    在这个循环中,我们记录了两个传感器的读数。

  8. 由于紧密循环可能会导致事物损坏(例如GPIO Zero从传感器线程调用我们的代码),它应该稍微休眠一下:

    f prefix lets us format variables into a string.
    

您可以将此代码发送到机器人并运行它。现在您可以看到机器人通过编码器的值偏斜。输出应该看起来有点像这样:

pi@myrobot:~ $ python3 test_encoders.py
INFO:test_encoders:Left: 0 Right: 0
INFO:test_encoders:Left: 0 Right: 1
INFO:test_encoders:Left: 2 Right: 2
INFO:test_encoders:Left: 3 Right: 4
INFO:test_encoders:Left: 5 Right: 7
INFO:test_encoders:Left: 8 Right: 10
INFO:test_encoders:Left: 10 Right: 14
...
INFO:test_encoders:Left: 56 Right: 74

编码器正在计数,这表明机器人左轮移动较少,右轮移动较多,并向左偏斜。INFO:test_encoders:部分是由日志引入的,显示日志级别和记录器名称。距离是以编码器脉冲计算的,每个计数事件为一个脉冲。

您现在已经尝试了这段代码,但如果您有任何问题,请参考故障排除部分。

故障排除

如果在运行此代码时遇到问题,请尝试以下步骤:

  • 确保您从第10章使用Python控制伺服电机的代码开始。如果您从当前章节下载了代码,您可能会看到与编码器已设置的代码的GPIO冲突。

  • 如果编码器值保持在零,请关闭Raspberry Pi,然后返回并仔细检查您的布线和引脚编号使用情况。

  • 检查您的布线 - 如果有任何东西过热,请立即断开电源并验证您的布线。

您已经在机器人上测试了编码器,看到它们移动时屏幕上的反馈。这表明它们在添加到Robot对象后可以用于更有趣的机器人行为。

将编码器添加到Robot对象中

要在其他代码或行为中使用此传感器,我们应该将其移动到Robot对象中。然后我们可以将我们的代码导入到Robot对象中,并使用正确的引脚设置两个侧面。您还需要添加一些清理代码以处理程序。

提取类

我们已经创建了EncoderCounter类,您可以从test_encoders.py复制到encoder_counter.py文件中(https://github.com/PacktPublishing/Learn-Robotics-Programming-Second-Edition/blob/master/chapter11/encoder_counter.py)。此代码需要导入DigitalInputDevice,相同的构造函数,以及when_changed处理程序:

  1. 让我们先添加导入和类声明。EncoderCounter类与上一节以相同的方式开始:

    from gpiozero import DigitalInputDevice
    class EncoderCounter:
        def __init__(self, pin_number):
            self.pulse_count = 0
    
  2. 我添加了一个direction成员来处理反转:

            self.direction = 1
    
  3. 构造函数(__init__)通过设置设备和分配when_changed处理器来完成:

          self.device = DigitalInputDevice(pin=pin_number)
          self.device.pin.when_changed = self.when_changed
    
  4. 我们的when_changed处理器应该添加方向而不是1,这样它可以向上或向下计数:

        def when_changed(self, time_ticks, state):
            self.pulse_count += self.direction
    
  5. 我们还应该有一个设置此方向的方法,这样我们就可以断言以验证我们的设置,如果不满足给定文本中的条件,则抛出异常——这是一种便宜但粗暴的方法,以确保输入值有意义:

        def set_direction(self, direction):
            """This should be -1 or 1."""
            assert abs(direction)==1, "Direction %s should be 1 or -1" % direction
            self.direction = direction
    
  6. 一个重置方法意味着我们可以处理在运动之间的计数器重启:

        def reset(self):
            self.pulse_count = 0
    
  7. 为了清理,我们需要一种方法来停止计数器,这样它们就不会再次调用处理器:

        def stop(self):
            self.device.close()
    

在编码器库准备就绪后,我们可以在代码中使用它。库意味着我们可以在不同的地方重用我们的编码器计数器,也可以用具有相似属性的另一个设备替换它。为了使它对许多行为可用,将其导入到机器人库中会很有用。

将设备添加到机器人对象

我们已经使用Robot对象作为处理硬件和行为的代码之间的主要接口。

我们将修改来自第10章使用Python控制伺服电机robot.py代码(https://github.com/PacktPublishing/Learn-Robotics-Programming-Second-Edition/blob/master/chapter10/robot.py)以添加传感器:

  1. 首先导入EncoderCounter

    ...
    import leds_led_shim
    from servos import Servos
    from encoder_counter import EncoderCounter
    ...
    
  2. __init__构造方法中,我们需要设置左右编码器。我就在距离传感器之后做了这件事:

            ...
            # Setup The Distance Sensors
    self.left_distance_sensor = DistanceSensor(echo=17, trigger=27, queue_len=2)
            self.right_distance_sensor = DistanceSensor(echo=5, trigger=6, queue_len=2)
            # Setup the Encoders
            self.left_encoder = EncoderCounter(4)
            self.right_encoder = EncoderCounter(26)
            ...
    
  3. 为了确保当我们的Robot对象停止时,代码清理了编码器处理器,我们在stop_all方法中调用编码器的stop方法:

            ...
            # Clear the display
            self.leds.clear()
            self.leds.show()
            # Clear any sensor handlers
            self.left_encoder.stop()
            self.right_encoder.stop()
            ...
    

带有编码器的robot.py的完整代码在GitHub上(https://github.com/PacktPublishing/Learn-Robotics-Programming-Second-Edition/blob/master/chapter11/robot.py)。我们现在可以使用它来构建一个测量毫米距离的行为。为此,我们将了解编码器脉冲与移动毫米距离之间的关系。

将脉冲转换为毫米

为了计算实际距离,我们需要车轮的大小。我们无法计算滑动,但我们可以找出车轮转动了多少,这与编码器相同。使用车轮的直径,我们可以计算出它转动了多远。使用尺子或卡尺,测量车轮的直径,如图图11.11所示:

图片

图11.11 – 测量车轮

所需的测量值如图图11.11所示:

  1. 您需要测量轮子的直径,如图中标记的D,以及车轮之间的距离,W。距离W相当于机器人上两个电机驱动轮的中点到中点的宽度。它更容易测量,如图所示,一个轮子的右侧,一直延伸到另一个轮子的右侧——这将与中点到中点相同。我的测量结果是大约130毫米。

  2. 您可以使用卡尺测量D,如图所示,通过将其套在轮子最宽的部分。我的轮子测量结果是70毫米,最接近的毫米。

我们知道编码器上有多少个槽,我们预计每个槽有两个脉冲(上升和下降),因此我们可以将槽的数量乘以2,这就是轮子整个转动的刻度数——在我的情况下,这是40。

圆周率π,或公式_11_001.png,是轮子直径与周长的比值。要得到周长,我们将直径乘以π,得到公式_11_002.png,其中D是直径。我们可以将π除以每转的总刻度数,然后当我们乘以计数的刻度数T,然后是直径D,我们得到轮子行驶的距离d

公式_11_003.png

那么,我们如何将其转换为代码?请参考以下步骤:

  1. 创建一个名为test_distance_travelled.py的新文件。在文件的顶部,我们需要导入math进行计算,Robot对象,以及time

    from robot import Robot
    import time
    import math
    import logging
    logger = logging.getLogger("test_distance_travelled")
    ...
    
  2. 接下来,我们定义我们的常数——轮子的直径和每转的刻度数。请使用您获得的数据,而不是我这里显示的值:

    ...
    wheel_diameter_mm = 70.0
    ticks_per_revolution = 40.0
    ...
    
  3. 创建一个函数,将计数的刻度转换为距离。由于毫米的分数不适用于这种测量,因此将其转换为整数。由于转换的一部分不会改变,我们也将其作为常数:

    ...
    ticks_to_mm_const = (math.pi / ticks_per_revolution) * wheel_diameter_mm
    def ticks_to_mm(ticks):
        return int(ticks_to_mm_const * ticks)
    ...
    
  4. 接下来,我们定义我们的机器人,设置停止时间,并启动电机:

    ...
    bot = Robot()
    stop_at_time = time.time() + 1
    logging.basicConfig(level=logging.INFO)
    bot.set_left(90)
    bot.set_right(90)
    ...
    
  5. 在循环中,我们通过调用ticks_to_mm函数在脉冲计数上显示距离:

    ...
    while time.time() < stop_at_time:
        logger.info("Left: {} Right: {}".format(
            ticks_to_mm(bot.left_encoder.pulse_count),
            ticks_to_mm(bot.right_encoder.pulse_count)))    time.sleep(0.05)
    

当上传到机器人并运行时,输出看起来像这样:

pi@myrobot:~ $ python3 test_distance_travelled.py
INFO:test_distance_travelled:Left: 0 Right: 0
INFO:test_distance_travelled:Left: 5 Right: 0
INFO:test_distance_travelled:Left: 16 Right: 10
INFO:test_distance_travelled:Left: 32 Right: 21
...
...
INFO:test_distance_travelled:Left: 368 Right: 384
INFO:test_distance_travelled:Left: 395 Right: 417

这个输出显示了左右两侧行驶的明显差异。右侧电机比左侧电机移动得稍快。这种差异会累积,使机器人转向。因此,在下一节中,让我们使用这些信息来调整方向。

直线行驶

到现在为止,您已经看到了输出中的差异——即,一个偏差。在我的左侧,在400毫米内大约落后于右侧20毫米,这是一个正在上升的错误。根据您的电机,您的机器人可能也有一些偏差。机器人完美直线行驶的情况很少见。我们使用传感器来纠正这一点。

小贴士

这种行为在木地板或MDF板上效果更好,而在地毯上效果较差。

这种校正仍然是死计算;在表面上滑动或测量不正确仍然可能导致偏离航向。我们如何使用电机和编码器来纠正航向并直线行驶?

使用 PID 调整偏航

一个用于自我校正转向和直线行驶的行为需要调整电机速度,直到车轮转动的量相同。如果车轮很快转动的量相同,那么它们将补偿主要的航向偏差。

我们的机器人将使用编码器传感器来测量每只轮子转过的程度。然后我们可以考虑这些差异来调整电机控制,并尝试保持电机以相同的速度运行。

这个技巧是找出测量值差异与调整电机速度之间的关系。这使我们转向查看一个将误差映射到调整和输出值的 PID 系统。

直线行驶需要一个闭环 反馈 环。图 11.12 展示了这个环是如何工作的:

图片

图 11.12 – 电机速度的闭环控制

我们从一个预期位置设定点开始。编码器位置提供来自现实世界的反馈数据。我们得到设定点和编码器位置之间的差异,我们称之为误差。代码将此输入到控制器中,生成速度调整。然后系统将应用此调整到电机速度,使电机转动更多或更少,改变编码器的反馈。

要直线行驶,我们取左电机值并减去右电机以获得编码器差异。我们的预期位置是 0。我们的误差是编码器之间的差异。然后我们可以使用控制器调整速度。

我们使用 PID 控制器来调整电机的速度;这有三个组成部分:

  • 比例P):误差值乘以一个常数。这可以纠正即时误差。

  • 积分I):到目前为止误差值的总和,乘以一个常数。这可以纠正持续误差。

  • 导数D):这取最后误差值和现在的差异,并乘以一个常数。这是为了稍微对抗突然的变化。

通过调整常数,我们可以调整每个因素对控制器结果的影响程度。我们不会使用导数组件来调整我们的行为,这相当于将其常数设置为 0。

积分可以给机器人一些自调功能,但需要一个非常小的常数,因为高值可能会使机器人开始摇摆。我们将调整加到一个电机的速度上,并从另一个电机中减去。

右电机速度如下:

...
integral_sum = integral_sum  + error
right_motor_speed = speed + (error * proportional_constant) + (integral_sum * integral_constant)
...

我们需要一个未使用的电机速度容量,以便能够稍微加速。如果速度太接近 100%,我们会得到剪辑。具有剪辑的积分行为可能会使机器人行为相当奇怪,所以要注意 100% 的剪辑!

小贴士

为 PID 调整留出空间 - 不要超过 80% 的电机速度。

现在我们对 PID 控制器的工作原理有了些了解,让我们在代码中构建一个。

创建一个 Python PID 控制器对象

PID 控制器代码是制作直线的基本机器人构建块,我们将在后续的相机驱动章节中再次使用它。你将在许多机器人系统中使用这里的基本概念:

  1. 我们在几个地方使用这个,所以让我们创建一个简化的 PID 控制对象。我把这个放在一个名为 pid_controller.py 的文件中。注意,这只是一个 proportional_constant 作为 pKintegral_constant 作为 iK。如果你愿意,你可以这样做。我在代码示例中使用了较长的名称,以便更容易阅读。

  2. 以下代码处理两个组件的值。处理积分的效果是增加积分总和:

    ...
        def handle_proportional(self, error):
            return self.proportional_constant * error
        def handle_integral(self, error):
            self.integral_sum += error
            return self.integral_constant * self.integral_sum
    ...
    
  3. 以下代码块处理误差以生成调整:

    p and i; since we log these values, we can configure logging to show them when debugging and tuning the controller.
    

在 PI 代码就绪后,我们就可以制作一个能够将误差与先前值结合,并按比例缩放以使其在某种运动环境中有用的机器人。我们将在下一节中使用这个 PID 控制器来调整直线。

编写直线行驶的代码

我把这个命名为 straight_line_drive.py (https://github.com/PacktPublishing/Learn-Robotics-Programming-Second-Edition/blob/master/chapter11/straight_line_drive.py):

  1. 让我们导入 Robot 对象、time 和我们新的 PI 控制器。我们将设置日志记录以获取 PID 控制器的调试信息。你可以将其调整回 INFO 或者在太多的情况下删除该行:

    from robot import Robot
    from pid_controller import PIController
    import time
    import logging
    logger = logging.getLogger("straight_line ")
    logging.basicConfig(level=logging.INFO)
    logging.getLogger("pid_controller").setLevel(logging.DEBUG)
    
  2. 也设置 Robot 对象,并设置一个稍长的 stop_at_time 值,这样我们的机器人可以行驶得更远:

    bot = Robot()
    stop_at_time = time.time() + 15
    ...
    
  3. 80 的主速度值开始,并将两个电机都设置为这个值:

    ...
    speed = 80
    bot.set_left(speed)
    bot.set_right(speed)
    ...
    
  4. 在进入主循环之前,设置控制器。你可能需要调整这些常数。注意积分常数的值有多小:

    ...
    pid = PIController(proportional_constant=5, integral_constant=0.3)
    ...
    
  5. 在循环中,我们稍微 sleep 一下,这样我们的编码器就有东西可以测量。通常,不使用 sleep 来给其他事物运行机会的“紧密”循环也是一个坏主意。获取编码器值并计算误差:

    ...
    while time.time() < stop_at_time:
        time.sleep(0.01)
        # Calculate the error
        left = bot.left_encoder.pulse_count
        right = bot.right_encoder.pulse_count
        error = left - right
        ...
    
  6. 这个误差需要由控制器处理,并用于生成 right_speed

        ...
        # Get the speed
        adjustment = pid.get_value(error)
        right_speed = int(speed + adjustment)
        left_speed = int(speed - adjustment)
        ...
    
  7. 我们可以在这里记录调试信息。注意我们有两个级别:用于错误和调整的调试,以及用于速度的信息。当前配置设置为 INFO,除非修改,否则我们不会看到调试信息:

        ...
        logger.debug(f"error: {error} adjustment: {adjustment:.2f}")
        logger.info(f"left: {left} right: {right}, left_speed: {left_speed} right_speed: {right_speed}")
        ...
    
  8. 然后我们将电机速度设置为调整后的值并完成循环:

        ...
        bot.set_left(left_speed)
        bot.set_right(right_speed)
    

当我们运行这个程序时,机器人应该会沿着一条相当直的路线行驶。它可能一开始不稳定,但应该会逐渐调整到一个恒定的值:

pi@myrobot:~ $ python3 straight_line_drive.py
DEBUG:pid_controller:P: 0, I: 0.00
INFO:straight_line:left: 3 right: 3, left_speed: 80 right_speed: 80
DEBUG:pid_controller:P: 0, I: 0.00
INFO:straight_line:left: 5 right: 5, left_speed: 80 right_speed: 80
DEBUG:pid_controller:P: -4, I: -0.20
INFO:straight_line:left: 5 right: 6, left_speed: 84 right_speed: 75
DEBUG:pid_controller:P: 0, I: -0.20
...
INFO:straight_line:left: 13 right: 15, left_speed: 89 right_speed: 71
DEBUG:pid_controller:P: -8, I: -1.40
INFO:straight_line:left: 15 right: 17, left_speed: 89 right_speed: 70
DEBUG:pid_controller:P: -8, I: -1.80
INFO:straight_line:left: 17 right: 19, left_speed: 89 right_speed: 70
DEBUG:pid_controller:P: -8, I: -2.20
INFO:straight_line:left: 19 right: 21, left_speed: 90 right_speed: 69
...
DEBUG:pid_controller:P: 0, I: 0.60
INFO:straight_line:left: 217 right: 217, left_speed: 79 right_speed: 80
DEBUG:pid_controller:P: 0, I: 0.60
INFO:straight_line:left: 219 right: 219, left_speed: 79 right_speed: 80
DEBUG:pid_controller:P: 0, I: 0.60
INFO:straight_line:left: 221 right: 221, left_speed: 79 right_speed: 80
DEBUG:pid_controller:P: 0, I: 0.60
INFO:straight_line:left: 223 right: 223, left_speed: 79 right_speed: 80

机器人启动时没有误差,因为电机开始工作,但右边速度更快。在 13 个脉冲时,控制器将调整拉得很高。注意 P 如何跳跃,但 I 在一段时间后会稳定在一个恒定值,这将使机器人保持直线。

调整 PI 常数以及循环时间可能会导致更早的纠正——初始编码器值太小,没有实际用途。

注意,这仍然可能导致偏离正确方向;它考虑了减少偏航,但可能调整得太晚,无法停止小的 S 形或其他错误。然而,这比不调整要直得多。调整 PID 可以帮助解决这个问题。

故障排除此行为

如果机器人摇摆或无法直线行驶,以下是一些步骤:

  • 如果机器人补偿得太慢,增加比例组件。

  • 如果机器人大幅超出(即,它先向一个方向偏移,然后向另一个方向偏移),则减少比例和积分 PID 组件的大小。

  • 如果机器人产生越来越大的摇摆,积分过高,右边的速度可能超过 100。降低积分组件,也许还有请求的速度。

  • 你可以将 straight_line 记录器或 basicConfig 设置为调试,以查看错误值。

当直线行驶和驾驶工作正常后,你现在已经纠正了偏航问题和两侧的差异。你现在可以在此基础上构建;让我们取一个已知的距离并驾驶到那里,然后停止。

驾驶特定距离

对于驾驶特定距离,我们再次使用 PI 控制器,并将距离测量纳入我们的编码器对象。我们计算左轮为了给定距离需要转动的脉冲数,然后使用这个值而不是超时组件。

将单位转换重构到 EncoderCounter 类

我们希望 EncoderCounter 类中的编码器转换能够用于这些行为。重构是移动代码或改进代码的同时保留其功能的过程。在这种情况下,转换距离是使用编码器的一个目的,因此将此代码移入其中是有意义的:

  1. 打开你的 encoder_counter.py 类。首先,我们需要导入 math

    from gpiozero import DigitalInputDevice
    import math
    ...
    
  2. 在类顶部,将 ticks_to_mm_const 添加为类变量(而不是实例变量)以使用它而无需任何类的实例。最初将其设置为 none,以便我们可以计算它:

    ...
    class EncoderCounter:
        ticks_to_mm_const = None # you must set this up before using distance methods
         ...
    
  3. 在我们的类中,我们想要直接从编码器获取车轮行驶的距离,以毫米为单位。将以下内容添加到文件末尾:

    ticks_to_mm_const from the class and not self (the instance). 
    
  4. 我们还想要计算相反的值:从毫米距离计算出的脉冲数。为此,将毫米距离除以我们乘以的相同常数。这被设置为 staticmethod,因此它不需要后续代码使用实例:

        ...
        @staticmethod
        def mm_to_ticks(mm):
            return mm / EncoderCounter.ticks_to_mm_const
        ...
    
  5. 在文件中添加设置常数的途径(用于不同的机器人配置):

        ...
        @staticmethod
     def set_constants(wheel_diameter_mm, ticks_per_revolution):
         EncoderCounter.ticks_to_mm_const = (math.pi / ticks_per_revolution) * wheel_diameter_mm
        ...
    

当你保存后,EncoderCounter现在可以转换距离和编码器刻度。我们现在需要设置特定机器人的轮子直径。

设置常数

到目前为止,我们可以在我们的行为中使用我们的机器人度量。现在,我们想让Robot对象存储我们的测量并将它们注册到编码器中。我们可以通过两个简单的步骤来完成:

  1. robot.py中,在构造函数之前,指定一些这些数字:

    ...
    class Robot:
        wheel_diameter_mm = 70.0
        ticks_per_revolution = 40.0
        wheel_distance_mm = 140.0
        def __init__(self, motorhat_addr=0x6f):
            ...
    
  2. 将这些与编码器注册:

            ...
            # Setup the Encoders
            EncoderCounter.set_constants(self.wheel_diameter_mm, self.ticks_per_revolution)
            self.left_encoder = EncoderCounter(4)
            self.right_encoder = EncoderCounter(26)
            ....
    

准备好常数后,我们已经使编码器准备好测量距离。我们可以使用这一点来创建一个行驶距离的行为。

创建行驶距离行为

我会把这段代码放入drive_distance.py

  1. 首先导入EncoderCounter以使用其度量,PIControllerRobot对象,并设置一个记录器:

    from robot import Robot, EncoderCounter
    from pid_controller import PIController
    import time
    import logging
    logger = logging.getLogger("drive_distance")
    ...
    
  2. 定义drive_distance函数,它接受一个机器人实例,一个以刻度为单位的距离,以及一个默认为80的可选速度。我们首先创建一个主电机和副电机以及控制器决策:

    set_left and set_right functions in variables – we can just call the variables like functions. 
    
  3. 现在我们有一个明确的主电机和副电机。设置一个PIController并启动两个电机:

        ...
        controller = PIController(proportional_constant=5, integral_constant=0.3)
        # start the motors and start the loop
        set_primary(speed)
        set_secondary(speed)
        ...
    
  4. 现在,我们处于驾驶距离循环中。我们应该继续循环,直到两个编码器都达到正确的距离。我们需要在循环的其余部分之前暂停,以便我们有数据用于计算:

        ...
        while primary_encoder.pulse_count < distance or secondary_encoder.pulse_count < distance:
            time.sleep(0.01)
            ...
    
  5. 获取错误并将其输入控制器:

            ...
            # How far off are we?
            error = primary_encoder.pulse_count - secondary_encoder.pulse_count
            adjustment = controller.get_value(error)
            ...
    
  6. 我们可以将这些发送到电机并调试数据。因为调整是一个非整数,所以我们使用{:.2f}允许两位小数:

    ... 
            # How fast should the motor move to get there?
            set_primary(int(speed - adjustment))
            set_secondary(int(speed + adjustment))
            # Some debug
    logger.debug(f"Encoders: primary: {primary_encoder.pulse_count}, secondary: {secondary_encoder.pulse_count}," 
                        f"e:{error} adjustment: {adjustment:.2f}")
            logger.info(f"Distances: primary: {primary_encoder.distance_in_mm()} mm, secondary: {econdary_encoder.distance_in_mm()} mm")
    ...
    
  7. 设置机器人,让它计算你想让它走多远,然后让它开始移动:

    ...
    logging.basicConfig(level=logging.INFO)
    bot = Robot()
    distance_to_drive = 1000 # in mm - this is a meter
    distance_in_ticks = EncoderCounter.mm_to_ticks(distance_to_drive)
    drive_distance(bot, distance_in_ticks)
    
  8. 我们让机器人清理(atexit)停止电机。

当你运行这个程序时,机器人会行驶一米然后停止。我的机器人在停止时看起来是这样的:

INFO:drive_distance:Distances: primary: 997 mm, secondary: 991 mm
INFO:drive_distance:Distances: primary: 1002 mm, secondary: 1002 mm

有2毫米的超调,这可能在舍入值和检测时间中丢失。我们不能制作部分刻度。

你现在已经看到了如何使机器人行驶特定的距离(或者非常接近它),同时试图保持直线行驶。你已经结合了你在本章中构建的测量和PID调整工具。但如果我们想转弯并测量这些呢?我们将在下一节中介绍。

进行特定转弯

我们可以用我们的编码器完成的下一个任务是进行特定的转弯。当机器人转弯时,每个轮子都在通过一个弧线。图11.13说明了这一点:

图11.13 – 说明通过弧线转弯时的轮子运动

内轮比外轮行驶的距离短,根据差速转向的基本原理,这就是我们如何转弯。为了进行精确转弯,我们需要计算这两个距离或它们之间的比率。图11.14显示了轮子和转弯之间的关系:

图11.14 – 将轮子与转向半径相关联

如果我们将转弯半径视为设置机器人中间的位置,那么内轮的转弯半径是转弯半径与轮距一半的差值:

外轮的转弯半径是转弯半径加上轮距的一半:

我们将我们的转向角度转换为弧度,然后我们可以将这个角度乘以每个轮子的半径,以得到每个轮子需要移动的距离:

Python有数学函数可以将度数转换为弧度。

让我们将这些函数转换为一些代码,通过尝试在正方形内行驶,并测量90度的转弯来演示它:

  1. drive_distance.py的副本开始,并将其命名为drive_square.py。添加math导入,如下所示:

    from robot import Robot, EncoderCounter
    from pid_controller import PIController
    import time
    import math
    import logging
    logger = logging.getLogger("drive_square")
    ...
    
  2. 我们可以修改此文件的末尾来声明我们想要做什么。命名你计划拥有的函数,然后实现它们以适应。我们还将其缩小到略小于一米的尺寸。为了测试半径,我在机器人的轮距上增加了100毫米。任何小于轮距和转弯中心的东西都在轮子之间而不是轮子外面:

    ...
    bot = Robot()
    distance_to_drive = 300 # in mm
    distance_in_ticks = EncoderCounter.mm_to_ticks(distance_to_drive)
    radius = bot.wheel_distance_mm + 100 # in mm
    radius_in_ticks = EncoderCounter.mm_to_ticks(radius)
    ...
    
  3. 由于我们在正方形内行驶,我们想要行驶四次。对于直线,让每个轮子行驶相同的距离,然后以半径为圆心画90度的弧。我已经降低了弧的速度,以减少打滑问题:

    ...
    for n in range(4):
        drive_distances(bot, distance_in_ticks, distance_in_ticks)
        drive_arc(bot, 90, radius_in_ticks, speed=50)
    
  4. 让我们回到文件顶部,将我们的驱动距离方法升级为一次驱动两个距离,一个用于每个轮子。我已经将drive_distance函数重命名为drive_distances

    ...
    def drive_distances(bot, left_distance, right_distance, speed=80):
        ...
    
  5. 根据我们想要转向的角度,任一电机都可以作为外电机,驱动更长的距离。由于速度有一个上限,我们根据更长的距离来选择我们的主电机和副电机。将设置主/副电机的代码替换为以下内容:

    abs, the absolute value, to decide, because a longer distance in reverse should *still* be the primary motor. So, to determine how far the secondary wheel should go, we compute a ratio – to multiply with speed now, and later the primary encoder output. 
    
  6. 由于我们多次使用此方法,重置编码器计数。我在设置PIController之前放置了这个:

        ...
        primary_encoder.reset()
        secondary_encoder.reset()
    
        controller = PIController(proportional_constant=5, integral_constant=0.2)
        ...
    
  7. 由于我们可能正在向任何方向行驶,设置编码器方向。Python有一个copysign方法来确定值的符号。然后,启动电机:

    ...
        # Ensure that the encoder knows which way it is going
        primary_encoder.set_direction(math.copysign(1, speed))
        secondary_encoder.set_direction(math.copysign(1, secondary_speed))
        # start the motors, and start the loop
        set_primary(speed)
        set_secondary(int(secondary_speed))
    ...
    
  8. 当我们开始这个循环时,我们再次需要意识到一个或两个电机可能正在倒退。我们再次使用abs来去除符号:

    ...
        while abs(primary_encoder.pulse_count) < abs(primary_distance) or abs(secondary_encoder.pulse_count) < abs(secondary_distance):
            time.sleep(0.01)
    ...
    
  9. 计算副电机的误差取决于两个距离之间的比率:

    ...
            # How far off are we?
            secondary_target = primary_encoder.pulse_count * primary_to_secondary_ratio
            error = secondary_target - secondary_encoder.pulse_count
            adjustment = controller.get_value(error)
            ...
    
  10. 这仍然通过pid进行相同的调整计算;然而,这种调整也可能导致方向上的变化。现在,我们设置副电机的速度:

          ...
          # How fast should the motors move to get there?
          set_secondary(int(secondary_speed + adjustment))
          secondary_encoder.set_direction(math.copysign(1, secondary_speed+adjustment))
          # Some debug
          logger.debug(f"Encoders: primary: {primary_encoder.pulse_count}, secondary: {secondary_encoder.pulse_count}, e:{error} adjustment: {adjustment:.2f}")
          logger.info(f"Distances: primary: {primary_encoder.distance_in_mm()} mm, secondary: {secondary_encoder.distance_in_mm()} mm")
          ...
    
  11. 您可以扩展我们为了二级速度和目标必须考虑的调试。现在,因为我们追求精确度,主电机可能会在二级电机之前达到目标,并且没有设置为反向。所以,当它达到目标时停止这个电机,并将二级电机的基准速度设置为零,这意味着只有如果有调整,则适用。注意,我们在这里仍然使用绝对值:

            ...
            # Stop the primary if we need to
            if abs(primary_encoder.pulse_count) >= abs(primary_distance):
                logger.info("primary stop")
                set_primary(0)
                secondary_speed = 0
            ...
    

我们已经完成了驾驶距离函数。我们可以使用它来直线行驶,或者为每个轮子提供单独的目标距离,并使用它来弧形行驶。我们将在下一节中利用这一点。

编写drive_arc函数

这里我们将角度转换为弧度,确定内半径,并为每个轮子设置驾驶距离。将此代码添加到drive_square_behaviour.py中,在drive_distances函数之后:

  1. 从函数定义和有用的文档字符串开始:

    ...
    def drive_arc(bot, turn_in_degrees, radius, speed=80):
        """ Turn is based on change in heading. """
        ...
    
  2. 我们将机器人的宽度转换为tick,这是距离的内部测量单位,并使用其中的一半来获取轮子半径。我们还确定哪个是内轮:

        ...
        # Get the bot width in ticks
        half_width_ticks = EncoderCounter.mm_to_ticks(bot.wheel_distance_mm/2.0)
        if turn_in_degrees < 0:
            left_radius = radius - half_width_ticks
            right_radius = radius + half_width_ticks
        else:
            left_radius = radius + half_width_ticks
            right_radius = radius - half_width_ticks
        logger.info(f"Arc left radius {left_radius:.2f}, right_radius {right_radius:.2f}")
        ...
    
  3. 我们显示调试信息,以了解半径是多少。将此与弧度转弯结合,以获取距离。我们将转弯的绝对值转换为度数。我们不想反向进入转弯,而是要向相反方向转弯:

        ...
        radians = math.radians(abs(turn_in_degrees))
        left_distance = int(left_radius * radians)
        right_distance = int(right_radius * radians)
        logger.info(f"Arc left distance {left_distance}, right_distance {right_distance}")
        ...
    
  4. 最后,将这些距离输入到drive_distances函数中:

        ...
        drive_distances(bot, left_distance, right_distance, speed=speed)
    ...
    

机器人应该能够以正方形形状行驶。它仍然可能因为打滑或测量不准确而错过目标。需要调整比例和积分控制值。

检查drive_distancesdrive_arc的完整代码,可能会明显看出在确定内/外和主/次部分方面存在一些重复,如果您选择,可以进行重构。

如果通过角落反向行驶,此代码可能表现不正确。

摘要

在本章中,我们看到了如何将轮编码器传感器集成到我们的机器人中,并使用它们来确定每个轮子转过的距离。我们看到了如何使用这个方法通过使用简化的PID控制器将机器人引导到更直的路径上,然后使用这个方法行驶特定距离。然后我们将计算进一步,以轮子移动来计算转弯,并使用这个方法在正方形中驾驶机器人。

PID控制器可以在许多需要应用测量值与期望值之间差异的情况中使用,您已经看到了如何将其与传感器结合使用。您可以使用相同的系统来控制连接到热传感器的加热元件。您还可以使用编码器以一定的精度移动机器人,因为在伺服电机中使用的运动范围有限,这没有意义。

在接下来的几章中,我们将探索为我们的机器人提供更多交互性和智能行为,包括使用Raspberry Pi摄像头进行视觉处理、使用Mycroft进行语音处理,以及使用智能手机远程驾驶或选择机器人的模式。

练习

  1. 尝试调整不同的日志级别和不同命名的记录器,调整机器人行为产生的输出量。

  2. 对于 PID 行为,调整 PID 参数,尝试为比例或积分使用高值,并观察这如何影响机器人的行为。你能将这和 matplotlib 中的绘图结合起来观察 PID 行为吗?

  3. 有几种方法可以改进驱动距离代码。将 PID 控制器应用于主驱动的移动距离可以使它更精确地接近确切的行驶距离。检测到任一编码器没有移动可以用作在超时后停止代码,这样它就不会在停止前行驶。试试这个。

  4. 你现在可以使用这段代码来制作更多的几何形状或跟随没有线的路径。尝试添加高级左转/右转 90 度函数作为直角路径构建的构建块,然后使用这些来制作路径。

  5. 考虑将这里的编码传感器与距离传感器结合起来;可能开始记住墙壁之间的距离。

进一步阅读

请参考以下信息获取更多信息:

  • PID 控制是一个深奥的主题。它是自平衡机器人、无人机和其他自主控制系统中的关键领域。这里有一系列优秀的视频,你可以进一步探索这些内容:

    YouTube: Brian Douglas – PID 控制 – 简要介绍: https://www.youtube.com/watch?v=UR0hOmjaHp0

  • 我已经大大简化了一些角落转弯算法。一篇关于如何使用这些算法赢得比赛的文章对更详细的方法进行了阐述:

    GW Lucas – 使用基于 PID 的技术进行竞技里程计和回声定位: http😕/www.seattlerobotics.org/encoder/200108/using_a_pid.html

第十四章:第12章:使用Python进行IMU编程

现代机器人需要知道它们相对于世界的位置。在第11章使用Python编程编码器中,我们探讨了编码器如何获得机器人移动或转向的概览。然而,这种转向是相对于机器人的位置,没有绝对参考。轮胎打滑可能导致错误读数。在本章中,你将看到机器人读取其位置变化和测量其运动的其他方法。

从原则上讲,一个惯性测量单元IMU)可以提供绝对位置测量并且不会滑动。在实践中,它们很复杂。本章是向你的机器人添加IMU的小型实用之旅。在本章中,我将介绍IMU的组件。你还将学习如何焊接,以便为扩展板添加引脚,这项技能将为你打开机器人部件的广阔世界。

我们将编写一些代码来尝试各种功能,并查看传感器提供的输出类型。然后我们将对传感器数据进行动画可视化。到本章结束时,你将能够使用这些高级传感器,有一些焊接经验,并组装仪表盘来监控传感器。随着你在机器人领域的进一步研究,你会发现,如果你想要看到机器人能看到的内容,动画仪表盘将是至关重要的。

在本章中,我们将涵盖以下主要主题:

  • 了解更多关于惯性测量单元的信息

  • 焊接 – 将引脚连接到IMU

  • 将IMU连接到机器人上

  • 读取温度

  • 读取陀螺仪

  • 读取加速度计

  • 读取磁力计

技术要求

对于本章,你需要以下物品:

  • 来自第7章使用Python驱动和转向 – 移动电机的机器人

  • 来自第11章使用Python编程编码器的机器人代码,可以在https://github.com/PacktPublishing/Learn-Robotics-Programming-Second-Edition/tree/master/chapter11找到。

  • 一个带有引脚的ICM20948扩展板,例如Pimoroni PIM448模块

  • 焊接铁和支架

  • 焊接铁尖端清洁线

  • 焊料 – 应该是电子用的含助焊剂的焊料

  • 焊接吸锡器

  • 一个用于焊接的明亮工作台

  • 通风空间或抽风机

  • 安全眼镜

  • 面包板

  • 2.5毫米支撑套件

  • 雌性到雌性杜邦跳线

要获取本章的完整代码,请访问https://github.com/PacktPublishing/Learn-Robotics-Programming-Second-Edition/tree/master/chapter12

查看以下视频以查看代码的实际应用:https://bit.ly/38FJgsr

了解更多关于IMU的信息

IMU是一组设计用于感知机器人在3D空间中运动的传感器。这些设备在无人机中很常见,在基于地面的机器人中很有用,对于平衡机器人至关重要。IMU不是一个单独的传感器,而是一组设计用于一起使用且其读数可以组合的传感器。

这些设备体积很小,但其根源在于具有大型旋转陀螺仪的飞行硬件。IMU使用微机电系统MEMS)技术将机械装置放置在微尺度芯片上。它们确实有微小的运动部件,并使用电子传感器来测量它们的运动。

由于一些测量是模拟的(见第2章探索控制器和I/O),IMU模块通常包括一个模拟到数字转换器ADC)并通过I2C进行通信。

IMU上存在不同的传感器组合。这些传感器如下:

  • 一个温度传感器,用于考虑温度对其他传感器的影响

  • 一个陀螺仪,用于测量旋转速率

  • 一个加速度计,用于测量加速度或力

  • 一个磁力计,用于测量磁场,可以作为指南针使用

随着我们与这些传感器类型之一一起工作,我们将更多地了解它们及其特性。

现在我们对IMU有了基本的了解,让我们学习如何选择一个。

建议的IMU型号

IMU可以通过单独的加速度计、陀螺仪和磁力计以及转换传感器输出的设备来构建。为了减少所需的布线和空间,我建议使用包含所有设备的板或单芯片解决方案。出于同样的原因,我推荐使用I2C或串行IMU。

IMU系统使用自由度DOF)来表示有多少个传感器轴存在。一个9自由度系统为每个传感器提供三个轴(X、Y和Z)。

BNO传感器更容易编写代码,但由于它们使用I2C总线的方式,与树莓派不兼容,并且可能需要一个中间接口芯片。

另一个需要考虑的是是否有文档(readme文件和手册)以及一个支持从Python控制设备的库。以下图片显示了一个建议的IMU模块:

图片

图12.1 – ICM20948的照片

上一张图片是ICM20948的PIM448模块板,这是一个广泛支持的9自由度传感器,适用于Python库。它还包含一个温度传感器。它也分布得很好。由于IMU是复杂的设备,我强烈建议选择PIM448作为本章的选择。

现在我们已经探讨了IMU设备是什么以及如何选择一个,是时候用一项新技能:焊接,为我们的机器人准备一个PIM448了。

焊接 – 将引脚连接到IMU

大多数IMU模块,包括建议的PM448,可能都会附带一个包装袋中的引脚,您需要将其焊接在板上。如果您想焊接这些引脚,您将需要一点点的指导:

图片

图12.2 – 带有引脚的裸PIM448

前面的图片显示了从袋中取出的PIM448。左边是只有孔而没有接头的ICM20948板。中间是公接头,而母接头在右边。我们将使用公接头,因为这些在焊接时更容易固定在位。

如我们在技术要求部分中提到的,你需要一个烙铁和焊料、烙铁支架、安全眼镜、吸尘器或通风良好的空间、额外的面包板和一个明亮的工作空间。焊接会产生烟雾,你不想吸入。

在这个阶段戴上你的安全眼镜。加热烙铁;根据烙铁的不同,这可能需要几分钟。同时准备好一些焊料。

制作焊点

现在,你就可以进行焊接了。

以下图片显示了焊接模块的三个阶段:

图12.3 – 焊接PIM448的阶段

要制作一个焊点,在查看前面的图片的同时执行以下步骤:

  1. 我们需要确保在焊料干燥的过程中,该部分不会移动。前面的图片显示了PIM448排放在公接头处,推入底部的面包板上,顶部是母接头。将部件固定在位的一个好方法是将接头的长边放入面包板中,我们的设备放在上面。由于我们正在连接公接头,我们将使用母接头来支撑另一侧。

  2. 焊接烙铁尖端应大约在300°C时加热并上锡。在尖端熔化一点焊料以测试它是否足够温暖。在你能够焊接之前,你需要将尖端上锡。上锡是将一小层焊料放在烙铁上,以提高其热导率并保护尖端免受氧化(当加热时生锈)。要上锡,将一点焊料推入烙铁的尖端,它将粘附在上面。焊料应该能够自由熔化。

  3. 确保尖端清洁 – 在烙铁加热时,将烙铁尖端推入黄铜清洁剂中,在电线中做擦拭动作。

  4. 加热接头和焊盘(引脚穿过的环形部分)。我们将从标有2-6V的引脚开始。加热引脚和焊盘以避免干焊,因为焊料不会正确地流过焊盘。干焊既电学上又机械上都很脆弱。

  5. 大约一秒后,轻轻地将一点焊料喂入引脚的另一侧;当引脚足够热时,焊料会熔化并流过焊盘,形成一个圆形帐篷状。这正好足够。你会看到焊剂树脂从焊料中流出。

  6. 前面的图片显示了下一步的中间步骤。在这里,我已经焊接了两个引脚;从现在开始事情会变得容易,因为板子不能移动。继续到下一个引脚并重复 – 加热引脚和焊盘,然后加入焊料。

  7. 如果你添加了太多的焊锡,请使用吸锡器移除多余的焊锡。按下活塞,将吸锡器靠近接头,熔化焊锡,然后按下活塞的释放按钮,使其吸走任何焊锡。你可以用少一点的焊锡重新制作这个接头。

  8. 如果你发现你用一小团焊锡连接了两个引脚(短路了它们),你可以用烙铁在引脚之间划下来再次分开它们。你可能还需要移除任何多余的焊锡,如步骤7中提到的。

  9. 对剩余的引脚重复前面的步骤。前面图像的右侧显示了所有引脚焊接后你的IMU应该看起来是什么样子。

    重要提示

    为了安全起见,确保你在做其他任何事情之前将烙铁放回支架上并关闭烙铁。烙铁是一个危险的设备,如果无人看管可能会导致烧伤或火灾。

  10. 一旦部件冷却,从面包板上拔下它。可选地,你可以使用异丙醇和棉签清除焊膏残留物,以获得更好的外观。

在我们接线之前,请进行以下检查:

  • 你已经将所有五个引脚焊接到位。

  • 每个焊接的引脚都像是一个银色的“帐篷”形状。气泡/圆形或扁平形状是不正确的,你需要再次进行那个连接。

  • 没有两个引脚有焊锡桥接 – 连接引脚的焊锡团。

恭喜你 – 你已经焊接了你的第一个部件!当你构建更多的机器人和电子设备时,你将需要再次使用这项技能。现在你已经焊接了ICM20948模块,让我们将其连接到你的机器人上。

将IMU连接到机器人

在我们能够使用IMU并为它编写代码之前,我们必须将其牢固地安装在机器人上,并接线以便树莓派能够与之通信。

物理放置

IMU磁力计对磁场敏感,需要远离电机。因此,它应该位于机器人上方的茎上。

IMU的朝向对于其他实验来说至关重要:

图12.4 – 将IMU与机器人对齐

IMU顶部有一个图示。前面的图示显示了该图示应该如何与机器人对齐。X轴应该朝前,Z轴应该朝上,IMU上的小正方形应该朝上。最后,Y轴应该指向左侧。

该传感器使用I2C。I2C对线缆距离敏感,因此我们应该将其安装在树莓派和电机控制板上方,那里的线缆距离较短。以下图像显示了你需要为此步骤准备的部件:

图12.5 – 安装IMU所需的部件

对于这个步骤,你需要前面图像中显示的部件:

  • IMU,带有安装好的引脚

  • 许多长支架,M2.5

  • 1x M2.5螺母

我们将使用支架将这些部件组装成一个长柱,如下面的图像所示:

图12.6 – 连接支架柱

以下步骤旨在与前图一起使用,以帮助您安装IMU:

  1. 如前图所示,您只需将一个支柱的螺纹拧入另一个支柱的插座中,即可得到一个长支柱。这应该给IMU一点距离,使其能够站在机器人上方。目标是在电缆长度下方:图片

    图12.7 – 将IMU拧到支柱上

  2. 如前图所示,将支柱螺纹穿过IMU上与轴标记相对的孔 (a)。引脚头 (b) 应向下面对支柱。螺纹非常紧,但应该能穿过。使用顶部的螺母 (c) 来固定它:图片

    图12.8 – 将IMU支柱拧到Raspberry Pi上

  3. 前一张图片显示了IMU支柱拧在从电机板伸出的螺纹上。我们建议在第6章机器人构建基础 – 轮子、电源和接线中提到的电机板后左边的I2C连接器。我们可以将IMU支柱拧到靠近该孔的孔中:图片

    图12.9 – 准备接线的IMU

  4. 上一张图片显示了ICM20948连接到支柱上,您需要将螺栓拧入电机板的顶部,其引脚已准备好接线。调整它,使X轴指向前方,Y轴指向左侧,同时拧紧顶部的螺母。越接近与机器人成方形,结果越好!

您现在已将IMU安装在机器人上。您已对其轴进行了对齐,因此我们知道我们的传感器会有什么表现。现在,我们已安装了此IMU模块,它已牢固地安装到位,但如果我们需要这样做,它可以被卸下。该模块现在已准备好接线。

将IMU连接到Raspberry Pi

接下来,您需要将IMU连接到Raspberry Pi的I2C引脚。虽然这似乎是一项简单的工作,但您必须小心,因为一些连接不是直接通过的。

电机板方便的I2C分线器应该会使这项工作更容易一些:

图片

图12.10 – ICM20948的接线

如前图所示,接线相当直接:IMU的GND连接到电机板I2C分线器的GNDSDA连接到SDASCL连接到SCL2-6V连接到5V(在2-6V范围内)。

GND 从电机板左侧延伸到IMU右侧。四根线中有一处弯曲,5V 线穿过此处。

实际上,我们会使用四根线的跳线带,如前图中的虚线所示。连接到IMU的一端会直接穿过。连接到电机板的一端有电源线穿过其他线:

图片

图12.11 – 连接到电机板的ICM20948 IMU

在前面的图像中,我使用了一段短的女性到女性的跳线来建立连接。IMU 板与其预定方向成 90 度,以便更清晰地布线;它应该有 X 面向前。注意,线中有扭曲,所以 GND 线(这里为白色)最终结束在另一侧的 GND 引脚上。执行以下步骤来建立这些连接:

  1. 小心地将四根线的带子拉掉。目标是找到深色用于 GND 和亮/鲜艳的颜色用于 5V 线。

  2. 将一侧直接插入 IMU,确保跳过 INT 引脚。

  3. 当你将线拉到下面的电机板时,请转一个小弯,使电缆朝相反的方向。

  4. 首先插入 GND,以设置方向。

  5. 然后插入 5V 线,这将需要跨越其他两根线。

  6. 最后两根线现在应该已经正确放置,用于 SDA 和 SCL;将它们都插入。

  7. 使用线颜色确保您已正确连接。

    我们不打算使用 INT 引脚。这个引脚的设计是用来向 Pi 发送一个 中断,以通知我们存在运动,用于唤醒运动类型的行为。然而,这超出了本书的范围。

    现在我们已经将这个传感器连线并连接到我们的机器人上,您就可以编写一些代码了。我们将从读取温度开始。

    读取温度

    在设备连线并连接后,您可能想要在上面尝试一些代码来确认我们可以与该设备通信并从中获取数据。让我们安装一些工具并使其工作。

    安装软件

    在我们开始与该设备交互之前,就像大多数设备一样,我们将安装一个辅助库来与之通信。我推荐的 ICM20948 模块供应商 Pimoroni 已经为 Python 制作了一个方便的库。我建议从 GitHub 下载他们的最新版本。

    执行以下步骤来安装它:

    在机器人上启动 Raspberry Pi。这个 Pi 应该之前用于电机板和 LED 填片,并且已启用 I2C。如果没有,请回到 第 7 章用 Python 驱动和转向 – 移动电机,并遵循准备 I2C 的步骤。

  8. 输入 i2cdetect -y 1 以检查您是否正确安装了设备。输出应该看起来像这样:

    pi@myrobot:~ $ i2cdetect -y 1
         0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
    00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
    10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
    20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
    30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
    40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
    50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
    60: -- -- -- -- -- -- -- -- 68 -- -- -- -- -- -- 6f 
    70: 70 -- -- -- -- -- -- —             
    
  9. 地址为 0x68 的设备是我们的新设备。如果您看不到这个,请关闭 Raspberry Pi 的电源并检查您的连线。其他两个设备(0x6f 和 0x70)是电机板和 LED 填片。

  10. 现在,我们可以安装这个库:

    pi@myrobot:~ $ git clone https://github.com/pimoroni/icm20948-python
    pi@myrobot:~ $ cd icm20948-python/
    pi@myrobot:~ $ sudo ./install.sh
    pi@myrobot:~ $ cd
    
  11. 您现在已经验证了 ICM20948 设备位于机器人的 I2C 总线上,并安装了 Pimoroni Python 库,以便与之通信。您现在可以开始与之通信了。

我们还将添加一些新的软件来实时可视化我们的数据。有一个名为 Visual PythonVPython)的系统,它被设计用来实时创建图表和 3D 表示:

pi@myrobot:~ $ pip3 install git+https://github.com/orionrobots/vpython-jupyter.git

现在,设备和库应该已经安装。如果您没有成功,请尝试查看下一节的 故障排除 部分。

故障排除

在这个早期阶段可能会出错。如果您到目前为止还没有让事情工作,请尝试按照以下步骤操作:

  1. 在这里,i2cdetect 阶段必须工作并显示设备在 0x68。如果没有,请检查您的接线。这里不应该有任何东西是热的

  2. 确保您已经完成了所有的焊接检查。

  3. 如果库安装失败,请确保您已连接到互联网。您可能需要最新的 Raspbian 版本才能使其工作。

现在您已经安装了设备并检查了常见问题,我们可以尝试使用它进行第一次实验并读取温度传感器。

读取温度寄存器

在本节中,我们将为 IMU 设置一个接口,然后添加来自 Raspberry Pi 的温度数据的实时图表。

创建接口

就像其他传感器和输出一样,我们必须创建一个接口,因为市场上有很多 IMU 设备。然而,相同的接口允许我们更换它们,而无需重写使用该接口的其他行为:

  1. 创建一个名为 robot_imu.py 的文件。

  2. 首先导入 Pimoroni 设备库 – 如果您使用其他 IMU 设备,这将是不同的:

    from icm20948 import ICM20948
    
  3. 我们将创建一个 IMU 类来表示我们的设备。这设置了一个单个 IMU:

    class RobotImu:
        def __init__(self):
            self._imu = ICM20948()
    
  4. 对于这个练习,我们只需要温度。让我们简单地包装一下:

        def read_temperature(self):
            return self._imu.read_temperature()
    

使用这个,接口就准备好了。现在,我们可以用它来读取设备的温度。

什么是 VPython?

VPython 或 Visual Python 是一个旨在在 Python 中创建视觉(甚至是 3D)显示的系统。它来自一个科学社区,并将在本章的整个过程中非常有用。它将输出发送到浏览器,并且使用在此安装的特定版本,它可以在 Raspberry Pi 上运行,同时在计算机或智能手机上显示输出。

它有几个小问题,其中之一是启动时间较慢,但结果是值得的。

绘制温度图表

通过使用图表是观察温度变化的好方法。

让我们使用 VPython 并创建一个显示我们 IMU 模块温度的图表:

  1. 创建一个名为 plot_temperature.py 的文件。

  2. 首先导入 VPython 和我们的机器人 IMU 接口:

    vpython by importing it as vp.
    
  3. 我们将在图表上绘制温度与时间的关系,因此我们需要一个时间参考。此外,我们将使用日志记录来查看发生了什么:

    import time
    import logging
    
  4. 让我们配置日志记录,以便我们可以看到所有 INFO 级别的日志:

    logging.basicConfig(level=logging.INFO)
    
  5. 创建我们的 IMU 实例:

    imu = RobotImu()
    
  6. 我们希望从图表中获得一些东西。由于 X 轴是时间,以秒为单位,将最小值设置为 0,最大值设置为 60,将显示一分钟的数据。我们还想让图表滚动,以便在记录超过一分钟的数据后显示新数据:

    vp.graph(xmin=0, xmax=60, scroll=True)
    temp_graph = vp.gcurve()
    
  7. 现在我们有了时间参考,让我们在进入循环之前记录开始时间:

    start = time.time()
    
  8. 主循环是一个while true类型的循环。然而,我们需要使用vp.rate来让VPython知道我们正在动画化,并为我们的系统设置一个帧/更新率:

    while True:
        vp.rate(100)
    
  9. 现在,我们可以捕捉我们的温度,同时,我们还可以记录这个:

        temperature = imu.read_temperature()
        logging.info("Temperature: {}".format(temperature))
    
  10. 要将这个数据放入图表中,我们需要获取X轴的经过时间。我们可以通过从当前时间减去开始时间来获取这个时间:

        elapsed = time.time() - start
    
  11. 最后,我们需要将这个数据绘制到我们的温度图表中,其中经过的时间作为x轴,温度作为y轴:

        temp_graph.plot(elapsed, temperature)
    

绘制温度的代码现在已生效。让我们在树莓派上运行这个代码。

运行温度绘图器

在我们将文件复制到树莓派之后,我们需要遵循几个步骤来运行它。我们的树莓派是无头服务器,因此我们需要远程查看VPython。为此,我们需要让VPython知道我们在做什么,并使用网络端口使其视图可用。然后我们可以使用浏览器查看它。让我们看看如何:

  1. 在树莓派的SSH会话中,输入以下命令:

    $ VPYTHON_PORT=9020 VPYTHON_NOBROWSER=true python3 plot_temperature.py
    

    我们选择了端口9020,这有点随意,但应该高于1000。我们将在本书的后面使用其他端口上的其他网络服务,而这个数字远远超出了它们的范围。当运行时,它应该记录一些消息来告诉你它已准备好:

    INFO:vpython.no_notebook:Creating server
    http://localhost:9020
    INFO:vpython.no_notebook:Server created
    INFO:vpython.no_notebook:Starting serve forever loop
    INFO:vpython.no_notebook:Started
    

    注意,它显示的是本地主机地址。我们打算远程使用它。

  2. 接下来,将你的浏览器(Chrome、Firefox或Safari)从桌面指向树莓派,并带有端口号。在我的情况下,根据我的机器人的主机名,这将是在http://myrobot.local:9020

  3. 现在,请准备好稍等片刻——VPython设置可能需要一点时间。之后,你会看到你的图表或任何错误/问题。

当它运行时,你会得到温度传感器的读数图表。你可以通过小心地将手指放在传感器上(PIM448上的大黑方块)来实验,并观察图表如何随着这个动作而上升/下降。你也可以找到冷或热的物体,比如吹风机,看看它是如何操纵的。然而,要小心不要让机器人弄湿,并且不要让金属接触引脚:

图12.12 – 温度图表

前面的图像是一个显示温度(Y轴)与时间(X轴)的图表。粗黑线表示当前的温度读数。它波动很大——这是一个噪声系统。

我在大约25秒时将手指放在传感器上。如图所示的前一个图表,环境温度为31度,上升至略低于34度。需要几秒钟才能升温。如果我将手指放在那里更长时间,它会增加更多。我有一个风扇,所以有一个急剧的下降——根据你的条件,可能会有更慢的下降。代码也将温度记录到控制台:

INFO:root:Temperature 32.43858387995327
INFO:root:Temperature 32.726120945278105
INFO:root:Temperature 32.726120945278105
INFO:root:Temperature 32.39066103573247
INFO:root:Temperature 32.39066103573247
INFO:root:Temperature 32.63027525683649

在这里,小数位有很多噪声,你可以忽略它们。当你关闭这个浏览器标签时,代码将停止绘图。

重要提示

关于测试温度的警告:不要在传感器上放置金属物体——这可能会短路引脚并损坏机器人。也不要在上面放置湿物体。非常冷的对象上可能会有冷凝。水甚至可能短路引脚并损坏传感器,以及可能损坏树莓派。

故障排除

我们正在将两个新的组件拉入我们的机器人代码中,因此可能会出错。以下是一些需要检查的事项:

  1. 请注意,VPython可能运行缓慢,因此启动可能需要很长时间。尝试在30秒后刷新浏览器标签。

  2. 使用VPython,显示错误消息可能需要很长时间。在尝试新代码时需要耐心。

  3. 如果您看到I/O或通信错误,请仔细检查IMU的接线。请回到安装软件故障排除部分以获取措施。如果在将手指放在传感器上时推挤一根线,或者更糟糕的是,如果尝试用金属物体冷却它并短路引脚,也可能发生I/O错误。不要在传感器上放置金属物体!

  4. 类似地,如果您看到导入错误,请检查您在导入中是否没有打字错误,并确保您已经检查了安装软件故障排除部分。

  5. 如果温度读数需要时间才能改变,请注意IMU有一些绝缘/热阻,因此需要一段时间才能升温(但会升温)和降温。板子也有热质量,这意味着它会全部升温或降温,从而减慢达到您所测量温度所需的时间。

  6. 温度读数不准确可能有几个原因。一方面,IMU可以产生一些热量——我们已经提到了热质量。它可以应用一个校准偏移值来使其更准确,但不要期望它能够完美地与温度计匹配到几分之一度。它当然应该能够将手指或手掌注册为接近37度,但在实践中,只要耐心,我通常能达到大约36点多。

我们现在的示例正在工作,但我们可以使启动测试更容易一些。

简化VPython命令行

在本章中,我们将大量使用VPython,并且我们不希望每次运行Python文件时都输入一大堆设置。让我们创建一个别名(命令行快捷方式)来节省我们每次都输入这些内容的时间:

  1. 让我们为当前会话设置它。alias命令创建了一个别名,我们可以在以后再次使用。在这里,它被命名为vpython。它包含设置和python3命令:

    pi@myrobot:~ $ alias vpython="VPYTHON_PORT=9020 VPYTHON_NOBROWSER=true python3"
    
  2. 为了在某个时候再次使用它,我们将将其放入当前用户的.bashrc文件中——这是一个Raspbian在您ssh登录时自动运行的文件:

    pi@myrobot:~ $ echo 'alias vpython="VPYTHON_PORT=9020 VPYTHON_NOBROWSER=true python3"' >>~/.bashrc
    

    将某物包裹在echo中会将文本写出来而不是运行命令。>>将此追加到文件中——在这种情况下,是.bashrc文件。~标记选择当前用户的家目录。

  3. 您可以使用vpython plot_temperature.py重新运行温度演示。

在本节中,你从IMU设备接收数据,并看到了它对温度的反应。这证实了IMU正在响应。你记录了数据,并绘制了图表,在这个过程中介绍了VPython系统,它可以作为一个强大的图形显示系统。我们将在本章中使用IMU和VPython做更多的事情。接下来,我们将查看陀螺仪,以便我们可以看到我们的机器人是如何旋转的。

在Python中读取陀螺仪

在本节中,我们将使用IMU中的陀螺仪。我们将用它来近似机器人在三维空间中的朝向。

但在我们这样做之前,让我们先了解一下。

理解陀螺仪

陀螺仪测量旋转为角度变化率,可能是每秒多少度。在每次测量中,它可以确定绕每个轴的旋转速度:

图12.13 – 陀螺仪示意图

传统的陀螺仪是一个机械系统,如前图所示。它有一个陀螺仪——一组同心环——通过枢轴连接,这样它们就可以绕X轴、Y轴和Z轴旋转。中间有一个旋转的质量,称为转子。当转子旋转时,移动把手(如图中底部所示)不会影响旋转的质量,它保持其朝向,陀螺仪允许它自由旋转。

在MEMS陀螺仪的情况下,它会使一个微小的质量快速来回移动(振荡)。当陀螺仪的朝向改变时,质量仍然会朝另一个方向移动。这种运动会改变传感器检测到的电场。在原始朝向中,这种运动看起来像是一种力,称为科里奥利力。

在我们能够编写一些代码来与陀螺仪一起工作之前,我们需要理解坐标系——在机器人和VPython中。

表示坐标和旋转系统

我们将在本章中使用坐标和旋转系统。以下图表应该能帮助你理解它们:

图12.14 – 机器人本体坐标系

前面的图表显示了我们将要使用的不同坐标系统。让我们看看它的不同部分:

  1. 这是机器人的本体坐标系——一个带有三个轴箭头的机器人3D草图。首先,是X轴,它指向机器人的前方。绕这个X轴旋转被称为翻滚。然后是Y轴,它指向机器人的左侧(当你观察机器人时是你的右手)。绕这个轴旋转被称为俯仰。最后,指向机器人上方的是Z轴。绕这个轴旋转被称为航向偏航

    旋转的方向很重要。这里有一个经验法则:伸出你的右手,将拇指竖起。如果你的拇指指向轴线,那么拳头上的手指就是旋转的方向。

  2. 这是VPython的世界坐标系。我们在VPython中在这里显示3D图像。VPython的坐标系是机器人身体系统的旋转。

    在前面的图中,Y轴向上,X轴向右,Z轴向前。

我们将用X、Y和Z分量来表示3D坐标——这被称为向量

当我们将测量值应用于VPython系统中的事物时,我们将使我们的视图与机器人坐标系对齐。当我们谈论相对于另一个坐标系的坐标系时,这被称为姿态。这是机器人相对于VPython坐标系的姿态。

让我们用一点代码来帮助我们表示这个:

  1. 创建一个名为robot_pose.py的文件。

  2. 我们正在操作VPython视图,因此我们需要导入它,如下所示:

    import vpython as vp
    
  3. 然后,我们可以添加我们的函数来设置视图;我将其命名为robot_view

    def robot_view():
    
  4. 在这个函数中,我们需要设置VPython用于控制相机方向的两个属性:img/B15660_12_15.jpg

    vp.scene.forward = vp.vector(-3, -1, -1) 
    
  5. 一个轴告诉我们沿着哪个方向看,但不知道哪个方向是向上的。我们需要相机将其对“向上”的定义与机器人(Z轴向上)对齐;否则,向量可能会颠倒或偏斜:

        vp.scene.up = vp.vector(0, 0, 1)
    

我们将在后面的章节中更多地使用这个姿态;然而,现在,看到Z轴现在是向上的,以及我们围绕不同轴旋转的位置是有用的。

现在,让我们设置陀螺仪以进行读取。

将陀螺仪添加到界面中

在我们可以读取陀螺仪之前,我们需要将其添加到我们的robot_imu.py接口中:

  1. 我们将处理来自IMU的一些x、y和z组。我们将导入一个向量类型来存储这些。我在这里突出显示了新代码:

    from icm20948 import ICM20948
    from vpython import vector
    
  2. 向量是三个分量坐标系统的表示。现在,我们需要从底层IMU库中获取陀螺仪数据并将其存储在向量中:我们使用的ICM20948库没有返回仅陀螺仪数据的调用,但它有一个返回加速度计和陀螺仪数据的调用。

    这个ICM20948库将数据作为六个项目的列表返回。在Python中,当解包返回值时,下划线字符_可以表示忽略的内容。

  3. 我们现在可以将三个陀螺仪值放入一个体向量中返回:

            return vector(x, y, z)
    

IMU库现在已准备好供我们从中读取陀螺仪数据。接下来,我们将读取它并在图表上绘制数据。

绘制陀螺仪

正如我们之前提到的,陀螺仪测量旋转速率。它在每个轴上以每秒度数进行测量。

让我们绘制这个设备的输出图:

  1. 创建一个名为plot_gyroscope.py的文件。

  2. 我们将从导入、设置日志和IMU开始,就像我们之前做的那样:

    import vpython as vp
    import logging
    import time
    from robot_imu import RobotImu
    logging.basicConfig(level=logging.INFO)
    imu = RobotImu()
    
  3. 我们为陀螺仪输出的三个轴设置三个图表——X轴旋转、Y轴旋转和Z轴旋转。请注意,我们给每个图表分配了不同的颜色:

    vp.graph(xmin=0, xmax=60, scroll=True)
    graph_x = vp.gcurve(color=vp.color.red)
    graph_y = vp.gcurve(color=vp.color.green)
    graph_z = vp.gcurve(color=vp.color.blue)
    

    这三个图表将叠加在同一行上。

  4. 现在,我们需要设置一个开始时间,启动一个循环,并测量经过的时间:

    start = time.time()
    while True:
        vp.rate(100)
        elapsed = time.time() – start
    
  5. 我们现在可以读取IMU并将三个读数放入图表中:

        gyro = imu.read_gyroscope()
        graph_x.plot(elapsed, gyro.x)
        graph_y.plot(elapsed, gyro.y)
        graph_z.plot(elapsed, gyro.z)
    
  6. 上传文件并使用 vpython plot_gyroscope.py 运行它们。

  7. 等待一分钟或更长时间,然后将浏览器指向 myrobot.local:9020 ——这可能需要最多1分钟才能出现。

  8. 开始移动机器人——抬起它并尝试在每个三个轴上倾斜。您应该看到以下图表:

图12.16 – 陀螺仪数据的VPython图表

前面的图表包含三条线。Y轴显示每秒的移动速率(以度为单位),而X轴显示自程序开始以来的时间(以秒为单位)。屏幕上的图表以红色、绿色和蓝色显示。

当您进行移动时,图表会急剧上升,然后回到零。尝试将机器人的前部(鼻子)向下推;这在Y轴上是正的。绿色线条应该上升(在前面的图表中大约在3秒处)。如果您保持它在那里,线条将变平。当您将机器人放平时,线条上会有一个负的绿色峰值。现在,尝试通过围绕X轴旋转来抬起左手边,在您的图表上创建一个正的红色峰值。当您将其放平时,您将得到一个负峰值。接下来,尝试将机器人向左转;这将创建一个正的蓝色峰值。现在,如果您将其向右转,将创建一个负的蓝色峰值。围绕轴移动以了解这些测量值。

除非您一直在旋转机器人,否则您可能会发现保持任何转向力相当困难;这表明陀螺仪数据是转向速率,而不是方向的测量。更有用的事情是近似机器人的航向。当我们深入了解时,我们将学习如何使用陀螺仪数据来完成这项工作。

在本节中,您已经看到了陀螺仪以及它是如何通过一个展示这一原理的图表来测量旋转速率的。现在,让我们继续学习加速度计,以便我们可以看到作用在我们机器人上的力!

在Python中读取加速度计

在本节中,我们将学习如何使用加速度计来测量作用在机器人上的力,以及最常见的向下方向。让我们更深入地了解它,然后编写一些代码来查看它是如何工作的。

理解加速度计

加速度计测量加速度或速度的变化,包括大小和方向。它是通过提供三个值来做到这一点的——每个轴(X、Y和Z)一个值:

图12.17 – 加速度计概念 – 带弹簧的质量

前面的图示显示了加速度计的概念视图。让我们更详细地看看它:

  1. 这显示了一个球(一个质量)被六个弹簧悬挂在盒子中。当盒子上没有力时,球保持在中间。

  2. 这显示了当大箭头推动它时,这个系统是如何表现的。质量通过向右移动保持惯性,压缩右边的弹簧并拉伸左边的弹簧。

测量质量的位置显示了加速度的方向和大小。一个MEMS加速度计与这个设备类似,由一个微小的硅质量和弹簧构成。它测量随着质量移动而变化的电场。

在地球上,质量受到重力的向下拉力。这个系统表现得像一个力在支撑盒子向上,所以加速度计通常会记录一个向上的力。我们可以使用这个测量来确定向下方向,并感知机器人的倾斜。

将加速度计添加到界面中

让我们从向我们的RobotImu库添加加速度计测量开始:

  1. 打开robot_imu.py文件。

  2. 添加以下代码进行读取:

        def read_accelerometer(self):
            accel_x, accel_y, accel_z, _, _, _ = self._imu.read_accelerometer_gyro_data()
            return vector(accel_x, accel_y, accel_z)
    

    这使用与陀螺仪相同的库调用;然而,现在它丢弃最后三个数据项,而不是前三个。

现在加速度计已经准备好读取,我们可以将其渲染出来,使数据可见。

将加速度计显示为向量

加速度是一个向量;它指向一个三维空间,具有方向和大小。展示这一点的一个好方法是在三维空间中画一个箭头。为了明确这个向量的位置,我们可以为X、Y和Z轴各画一个指示器:

  1. 创建一个名为accelerometer_vector.py的文件。从一些简单的导入开始,包括机器人视图、日志设置和初始化IMU:

    import vpython as vp
    import logging
    from robot_imu import RobotImu
    from robot_pose import robot_view
    logging.basicConfig(level=logging.INFO)
    imu = RobotImu()
    
  2. 让我们从我们倾向于观察机器人的角度来看看:

    robot_view()
    
  3. 现在,我们想要定义四个箭头。VPython箭头沿着一个轴指向,可以设置其颜色和长度:

    accel_arrow = vp.arrow(axis=vp.vector(0, 0, 0))
    x_arrow = vp.arrow(axis=vp.vector(1, 0, 0),
                       color=vp.color.red)
    y_arrow = vp.arrow(axis=vp.vector(0, 1, 0), 
                       color=vp.color.green)
    z_arrow = vp.arrow(axis=vp.vector(0, 0, 1), 
                       color=vp.color.blue)
    
  4. 现在,我们可以开始主循环:

    while True:
        vp.rate(100)
    
  5. 读取加速度计并记录:

        accel = imu.read_accelerometer()
        print(f"Accelerometer: {accel}")
    
  6. 因为颠簸可能会使我们的刻度失效,我们将向量归一化,使其长度为1。我们需要将这个放入箭头轴中:

        accel_arrow.axis = accel.norm()
    
  7. 将此上传到Raspberry Pi,并用vpython accelerometer_vector.py启动它。将你的浏览器指向它,以查看以下输出:![img/B15660_12_18.jpg]

    图12.18 - 加速度计向量

    上一张图显示了三个彩色箭头——红色代表X轴(指向观察者),绿色代表Y轴(指向左侧),蓝色代表Z轴(指向上方)。有一个灰色箭头显示了加速度计向量。加速度计向上,这显示了它如何对抗重力而保持向上。

  8. 现在,如果你倾斜机器人,箭头也会倾斜以显示相对于机器人的向上方向。你可以以几种方式倾斜机器人,看看感觉如何。

这很令人兴奋——你现在已经展示了相对于你的机器人,向上的方向在哪里。为了使用这个来旋转物体,我们需要将这个向量转换为俯仰角和横滚角,我们将在深入研究时学习如何做到这一点。

在本节中,你已经学会了如何从加速度计组件读取数据并将其显示为向量。现在,我们将继续学习IMU的下一个元素,即磁力计,并读取作用于我们系统的磁场。

使用磁力计

磁力计在3D空间中读取磁场强度以产生一个向量。你编写的代码可以使用这个向量来找到磁北极,就像指南针一样。在本节中,我们将更详细地了解这个设备,学习如何从它那里获取读数,并查看它产生的向量。

在一个磁铁很少的空间里可能很有用。让我们更深入地了解磁力计。

理解磁力计

指南针通过使用磁化的针或圆盘来测量地球磁场中的航向。以下是一张指南针的图片:

图12.19 – 一个传统的指南针

之前图像中显示的指南针有一个在中心针上平衡的旋转磁化圆盘。这种类型的是一个小型按钮指南针,直径约为25毫米。

我们选择的IMU包含一个称为磁力计的设备。这个设备可以电子感应磁场,并可以用作指南针。

大多数磁力计通过一个材料传递电流,当它暴露在磁场中时会产生电流,如下面的图示所示:

图12.20 – 一个霍尔效应传感器的风格化图片

以下图示展示了这一过程的例子:

  1. 这个电路从电池(左侧)通过一个导电板(灰色矩形)传递电流。箭头显示了在电路中移动的电子(负电荷载体),它们从板的顶部直接移动到底部。板两侧的小圆圈内有V,是一个电压(电流量)传感器,连接到板的两侧。电压传感器读数为0,因为没有电流流向传感器。

  2. 一个磁铁在板的上方,使电子偏转到一个方向。它们使板的这一侧带负电荷,另一侧带正电荷。这种电荷差异使电压通过传感器流动,如箭头所示。下面的读数现在高于零。

  3. 将磁铁放在传感器的另一侧会改变磁场;电子偏转到另一侧,导致反向电压流动。指向仪表的箭头方向相反,读数显示电压低于零。

这被称为霍尔效应。通过测量三个板,你可以测量三维空间中的磁场。磁力计对磁场和金属物体很敏感。

另一个特点是,在某些IMU中,磁力计的轴与其他设备不同:

图12.21 – 磁力计的轴

在前面的图中,我们之前查看的轴在左侧显示为陀螺仪和加速度计的轴。在右侧,我们可以看到磁力计的轴。在这里,我们可以看到Z轴向下,Y轴现在向后。这就像我们在X轴周围旋转了180度。

现在,让我们添加一些代码,以便我们可以读取这些信息。

添加磁力计接口

我们将以相同的方式封装这个,即通过将其添加到我们的接口库中:

  1. 打开robot_imu.py文件。

  2. RobotIMU类中,在read_gyroscope方法之后,添加新的读取方法:

        def read_magnetometer(self):
    
  3. 与加速度计和陀螺仪不同,它从底层的设备库中读取数据。我们将这个封装起来并返回一个向量。为了进行180度的俏皮旋转,我们取反Y和Z轴:

            mag_x, mag_y, mag_z = self._imu.read_magnetometer_data()
            return vector(mag_x, -mag_y, -mag_z)
    

现在这个接口已经准备好使用,让我们获取一些读数。

显示磁力计读数

我们可以可视化的方法之一是将磁力计输出转换为向量,如下所示:

  1. 创建一个名为magnetometer_vector.py的文件。

  2. 添加熟悉的导入和设置:

    import vpython as vp
    import logging
    from robot_imu import RobotImu
    from robot_pose import robot_view
    logging.basicConfig(level=logging.INFO)
    imu = RobotImu()
    robot_view()
    
  3. 现在,我们将创建一个表示磁力计读数的箭头,以及参考X、Y和Z轴:

    mag_arrow = vp.arrow(pos=vp.vector(0, 0, 0))
    x_arrow = vp.arrow(axis=vp.vector(1, 0, 0), color=vp.color.red)
    y_arrow = vp.arrow(axis=vp.vector(0, 1, 0), color=vp.color.green)
    z_arrow = vp.arrow(axis=vp.vector(0, 0, 1), color=vp.color.blue)
    
  4. 接下来,我们开始主循环:

    while True:
        vp.rate(100)
    
  5. 现在,我们可以读取磁力计:

        mag = imu.read_magnetometer()
    
  6. 最后,让我们设置一个与这个向量匹配的箭头轴。我们可以使用.norm()方法来归一化这个向量。我们还需要打印数据:

        mag_arrow.axis = mag.norm()
        print(f"Magnetometer: {mag}")
    
  7. 将此发送到机器人,并使用常规的VPython设置运行它。你应该会看到以下内容:

图片

图12.22 – 磁力计的读数

前面的图像显示了一个画布,其中X轴用指向前的红色箭头表示,Z轴用指向上的蓝色箭头表示,Y轴用指向右的绿色箭头表示。还有一个表示磁力计向量的灰色箭头,它指向后方(仅XZ轴)。

你的箭头可能指向的方向与我的不同。这是因为它很可能指向你的IMU上的插针头位置。为什么是这样?

插针头通常由磁性金属制成。你可以亲自检查一下,用磁铁看看它是否粘附在插针头上(使用一些备件或断电时进行此操作)。你也应该能够观察到这对箭头的影响。你也可以拿一些金属,比如螺丝刀,在磁力计周围挥动。这应该会让结果四处飞溅。

之后,我们需要补偿附近金属的影响,因为它可能会产生很大的偏移,大到足以完全压倒地球相对较弱的地磁场。

摘要

在本章中,你学习了如何读取惯性测量单元上的四个传感器,以及如何显示或绘制数据。然后你有了第一次焊接经验——这是制作机器人的关键技能。你还学习了关于机器人坐标系的内容。

在本书的后面部分,我们将更深入地探讨如何将IMU传感器编织在一起,以获得机器人方向的近似值。

在下一章中,我们将探讨计算机视觉;也就是说,如何从摄像头中提取信息并使机器人对其所看到的内容做出反应。

练习

  • 在温度图表中,你会在图表和输出中注意到很多噪声。Python的round函数接受一个数字和要保留的小数位数,默认为0。使用这个函数将温度四舍五入到更合理的值。

  • 尝试将加速度计的值放入一个X、Y和Z图中,就像我们为陀螺仪所做的那样。当你移动机器人时,观察图表中的变化。它是平滑的,还是有噪声?

  • 陀螺仪的值能否以向量的形式显示?

  • 是否有其他可以焊接的传感器,你可能觉得对你的机器人使用很有趣?

进一步阅读

请参考以下链接,获取有关本章所涵盖内容的更多信息:

第十五章:第3节:听与看——为机器人提供智能传感器

在本节中,你将使用OpenCV和Mycroft以及智能手机,让机器人显得智能且交互。

本书本部分包括以下章节:

  • 第13章, 使用Pi摄像头和OpenCV的机器人视觉

  • 第14章, 在Python中使用摄像头进行循线

  • 第15章, 使用Mycroft与机器人进行语音通信

  • 第16章, 深入使用IMU

  • 第17章, 使用手机和Python控制机器人

第十六章:第13章:机器人视觉 – 使用Pi摄像头和OpenCV

给机器人赋予看到事物的能力,使其能够以人类能够理解的方式行事。计算机视觉仍在积极研究中,但一些基础知识已经可用于我们的代码中,使用Pi摄像头和一点工作即可。

在本章中,我们将使用机器人和摄像头驱动到物体上,并使用我们的云台机构跟踪人脸。我们将再次使用PID算法,并将摄像头输出流式传输到网页上,这样你就可以看到你的机器人看到的内容。

本章将涵盖以下主题:

  • 设置树莓派摄像头

  • 设置计算机视觉软件

  • 构建树莓派摄像头流应用程序

  • 在流媒体传输时运行后台任务

  • 使用Python跟踪彩色物体

  • 使用Python跟踪人脸

技术要求

对于本章,你需要以下物品:

  • 来自第11章使用Python编程编码器的带有云台的机器人。

  • 第11章使用Python编程编码器的机器人代码,您可以从GitHub上下载https://github.com/PacktPublishing/Learn-Robotics-Programming-Second-Edition/tree/master/chapter11。我们将扩展和修改它以实现新的功能。

  • 一个树莓派摄像头。

  • 一根300毫米长的Pi摄像头线,因为随摄像头附带的线太短了。确保该线不是为Pi Zero(它有不同连接器)准备的。

  • 两个M2螺栓和一个M2螺母。

  • 一小块薄薄的硬纸板——一个谷物箱就可以。

  • 一把小珠宝螺丝刀。

  • 一支铅笔。

  • 一个儿童保龄球套装——带有不同颜色球柱(普通,没有图案)。

  • 一个光线充足的区域,供机器人行驶。

  • 互联网接入。

本章的代码在GitHub上,可在https://github.com/PacktPublishing/Learn-Robotics-Programming-Second-Edition/tree/master/chapter13找到。

查看以下视频以查看代码的实际应用:https://bit.ly/39xfDJ9

设置树莓派摄像头

在我们进入计算机视觉之前,我们需要在你的机器人上准备摄像头。这涉及到硬件安装和软件安装。

完成此安装后,我们的机器人框图将看起来像图13.1

图13.1 – 添加摄像头后的我们的机器人框图

图13.1继续展示了我们在整本书中展示的框图,左侧突出了摄像头的添加及其与树莓派的连接。

我们首先将相机连接到俯仰机构。然后我们可以使用更长的线缆将相机连接到Pi。让我们开始准备安装相机。

将相机连接到俯仰机构

第10章中,使用Python控制伺服电机,你为你的机器人添加了一个俯仰机构。你将把相机安装到这个机构的正面板上。有支架和套件,但它们并不普遍可用。如果你能将其适配到俯仰机构,请随意使用其中之一;如果不能,我有一些计划。

构建机器人需要创造性思维、适应性以及必要的技能。我在购买东西之前,经常查看我拥有的材料以寻找可能的解决方案。有时,你尝试的第一个方案可能不会成功,你需要计划B。我的计划A是直接将钩环式搭扣(如魔术贴)粘到相机上,但它并不很好地粘附在相机的背面。所以我不得不转向计划B,即使用方形纸板,在其上打孔以安装2毫米的螺丝,将相机固定到纸板上,然后使用钩环式搭扣将相机组件固定到Pi上。另一种可能性是在俯仰机构上钻更多的孔,使其与相机螺丝孔对齐。

小贴士

我能粘上这个吗?是的,就像我们大多数的机器人搭建一样,一些胶水——甚至强力胶——可以用来将相机粘接到俯仰和倾斜上。这可能会更容易搭建。然而,我很容易预见你会在某个时候需要更换或移除相机。原因可能是反转相机线或更换相机为另一个传感器,甚至是一个具有更好功能的更新相机。正因为如此,我在机器人搭建中通常避免使用胶水,寻找模块化和可更换的解决方案。

所需的部件如图图13.2所示:

图13.2 – 我们计划安装相机模块所需的部件

图13.2显示了摆放的工具和材料:一些薄纸板,2毫米螺栓和螺钉,Pi相机模块,一些剪刀,一个小扳手(或钳子),钩环式搭扣胶带,一个小螺丝刀。你还需要一支铅笔。

在制作这个时,请尽量不要触摸相机的镜头。那么,让我们开始吧。以下图示显示了安装相机的步骤:

图13.3 – 安装相机,步骤1-2

这是如何使用这些部件安装相机的:

  1. 首先,剪下一小部分用于钩环式搭扣的一侧,并将其粘贴到俯仰机构上,如图图13.3所示。

  2. 在相机略大的地方剪下一个小的方形纸板:

    图13.4 – 使用笔标记螺丝位置

  3. 然后使用钢笔或铅笔在摄像头螺丝孔中戳一个点,如图 13.4 所示。然后拿一个尖锐的工具(例如十字螺丝刀的尖端或数学套件中的圆规),在一个坚固的表面上,在标记的位置打一个孔:

    图 13.5 – 将摄像头固定到纸板上

  4. 使用几颗 M2 螺丝和螺母将摄像头固定到纸板载体上,如图 13.5 所示。注意,螺栓朝向的一侧在背面——这样任何突出的螺纹都不会干扰钩扣和搭扣:

    图 13.6 – 纸板/摄像头组件的背面,带有我们的钩扣式固定件

  5. 现在,剪下一小段钩扣式织物,将这段织物固定在万向节机构上的材料上,并将其粘贴到纸板的背面,如图 13.6 所示。

注意,摄像头可能有一层薄膜覆盖在镜头上——请将其取下。

摄像头已经准备好可以粘贴到机器人上了。现在不要连接摄像头,因为我们首先需要连接电缆。让我们在下一节中看看如何操作。

连接摄像头

准备好连接摄像头,我们需要使用树莓派摄像头线将其连接到树莓派上。我们需要移动一些部件以便到达树莓派连接器,并将扁平电缆穿过。

图 13.7 中的图像序列显示了我们将如何进行接线:

图 13.7 – 摄像头连接器槽和电机板

图 13.7 中的步骤显示了我们将如何准备电缆连接器:

  1. 树莓派有一个专门用于摄像头的槽——摄像头电缆可以插入这个槽中。我们将把摄像头连接到这个槽中,但电机板覆盖了我们的机器人上的这个槽。

  2. 为了绕过被覆盖的槽,我们需要抬起树莓派上的其他板。你需要暂时卸下 惯性测量单元IMU),这样电机板就不会被它覆盖。松开 IMU 顶部的螺母;然后你可以手动转动下方的垫圈柱,以卸下 IMU,留下 IMU 和支撑件组件完整。

  3. 断开电机线(注意你之前是如何连接它们的,或者拍张照片以供以后参考)。

  4. 现在,轻轻地将电机板从树莓派上抬起。

  5. 当你将摄像头连接到树莓派时,长电缆需要穿过电机板。在执行下一步时,请记住这一点。

我建议遵循 《官方树莓派摄像头指南》中的 “连接扁平电缆到摄像头” (https://magpi.raspberrypi.org/books/camera-guide),使用长 300 毫米的电缆连接摄像头。按照指南操作后,你应该已经正确地将扁平电缆安装在摄像头中,然后穿过电机板上的槽,并正确地插入树莓派的端口。

在更换电机板之前,仔细检查你的连接是否正确,这将为你节省大量时间。

要完成重新组装,请查看图 13.8

图 13.8 – 完成相机接口

按照以下步骤操作,并参考图 13.8

  1. 轻轻更换电机板,将其引脚推到 Raspberry Pi GPIO 引脚上,并将孔推到间隔器上。

  2. 将 IMU 重新安装回去。

  3. 根据你的参考重新连接电机电缆。

  4. 将相机推到云台头的搭扣附件上,确保电缆向上。

你已经看到了如何连接 Raspberry Pi 相机。现在相机已经连接好,准备使用。接下来,我们将开始准备软件以从相机获取图像。

设置计算机视觉软件

在我们可以开始编写代码之前,我们需要设置驱动程序、工具和库,以便与相机和软件进行交互,以帮助进行计算机视觉。

在本节中,我们将激活 Raspberry Pi OS 中的相机,并获取一张测试照片。然后我们将添加库以开始与相机进行交互,进行视觉处理。

然后,我们将使用工具构建我们的第一个应用程序,以证明部件已就位,并为我们提供行为的一个起点。让我们开始设置软件。

设置 Pi 相机软件

为了使相机准备好使用,我们需要启用它:

  1. 在此操作中,请使用外部电源(即插入 USB 墙式适配器)启动 Pi,暂时关闭电机电源。

  2. 通过 SSH 登录。在终端中,键入以下内容:

    pi@myrobot:~ $ sudo raspi-config
    
  3. 现在,你应该能看到 raspi-config。使用光标键选择接口选项菜单项,然后按Enter

  4. 选择 raspi-config 将会询问你是否希望启用相机接口。选择确定,然后完成

  5. 你需要重新启动以使此更改生效:

    pi@myrobot:~ $ sudo reboot
    

为了验证我们能否获取图片,我们需要 picamera 包。在撰写本文时,Raspberry Pi OS 中已经安装了 picamera

现在,相机已启用,让我们尝试获取第一张图片。

从 Raspberry Pi 获取图片

我们需要做的第一件事是确认我们的设置是否成功,即要求 Pi 相机拍照。如果相机未检测到,请返回并检查电缆连接是否正确,是否已安装 picamera,以及是否在 raspi-config 中启用了 Raspberry Pi 相机:

  1. 重新连接到 Raspberry Pi 并键入以下内容以获取图片:

    raspistill command takes a still image, and the -o parameter tells it to store that image in test.jpg. This command may take a while; taking a still can be slow if light conditions are poor.
    
  2. 然后,你可以使用你的 SFTP 客户端(我们在 第 4 章为机器人准备无头 Raspberry Pi)下载此镜像并在你的电脑上验证它。你会注意到图片是颠倒的,这是由于相机安装方式造成的。不用担心——我们将通过软件进行纠正。

拍摄一张照片后,你知道相机是正常工作的。现在我们可以安装使用相机进行视觉处理应用所需的其余软件。

安装OpenCV和支持库

我们需要一些辅助库来完成视觉处理的繁重工作,并以有用的方式显示输出。Open Computer VisionOpenCV)是一个包含用于操作图片和提取信息的工具集合的库。代码可以使用这些OpenCV工具一起制作有用的行为和图像处理管道。

要在树莓派上运行我们的代码,我们需要安装Python OpenCV库:

  1. OpenCV有一些依赖项需要首先安装:

    pi@myrobot:~ $ sudo apt install -y libavcodec58 libilmbase23 libgtk-3-0 libatk1.0-0 libpango-1.0-0 libavutil56 libavformat58 libjasper1 libopenexr23 libswscale5 libpangocairo-1.0-0 libtiff5 libcairo2 libwebp6 libgdk-pixbuf2.0-0 libcairo-gobject2 libhdf5-dev
    pi@myrobot:~ $ sudo pip3 install "opencv_python_headless<4.5" "opencv_contrib_python_headless<4.5"
    
  2. 树莓派操作系统需要为OpenCV工作而识别一个库。此行会在每次登录到Pi时识别该库。我们也应该为此会话做好准备:

    pi@myrobot:~ $ echo export LD_PRELOAD=/usr/lib/arm-linux-gnueabihf/libatomic.so.1 >>.bashrc
    pi@myrobot:~ $ source .bashrc
    
  3. Flask是一个用于创建网络服务器的库,我们将使用它将视频数据流式传输到浏览器:

    pi@myrobot:~ $ sudo pip3 install flask
    
  4. NumPy,这个数值Python库,非常适合处理大量数字。存储在计算机上的图像本质上是一大块数字,每个小点都有与我们在第9章,“用Python编程RGB LED灯带”中发送到LED的三色数字相似的内容:

    pi@myrobot:~ $ sudo apt install -y libgfortran5 libatlas3-base
    pi@myrobot:~ $ sudo pip3 install numpy
    
  5. 我们需要为picamera安装大型数组扩展。这将帮助我们将其数据转换为NumPy和OpenCV可以使用的形式:

    pi@myrobot:~ $ sudo pip3 install picamera[array]
    

在接下来的几个操作中,我们将继续在外部电源上进行测试。

你现在已经准备好了软件库,并验证了相机可以拍照。接下来,我们将构建一个应用程序,从相机流式传输视频到你的浏览器。

构建树莓派相机流应用程序

一次下载一张图片是可以的,但我们需要在我们的机器人上对这些图片进行处理。我们还需要一种方便的方法来查看机器人如何使用相机数据。为此,我们将学习如何使用Flask网络服务器来提供我们的图片,这样我们就可以在手机或笔记本电脑上查看输出。我们可以使用此应用程序的核心来创建几种不同的行为。我们将保留基本应用程序以供使用。

视频或视频流是一系列图像,通常称为

让我们设计我们的流服务器。

设计OpenCV相机服务器

图13.9中的图表展示了图像数据处理流程,从相机开始,经过处理,最终输出到我们的网页浏览器:

图13.9 – 图像服务器应用程序

图13.9中的图像服务器应用程序从相机开始。相机将图像数据馈送到一个“转换为OpenCV”步骤,提供原始照片。图像数据需要一些处理,以便OpenCV能够操作它。

转换为OpenCV将数据馈送到处理帧,这可以是任何我们需要的;在这个例子中,我们将应用一个颜色掩码,我们将在下一节中更深入地探讨。在处理帧步骤之上是一个使用红色颜色掩码后的图像示例。

原始帧和已处理帧进入下一步,与原始帧合并,这会创建一个包含两个图像的复合图像。在此步骤之上,是两个图像合并成单个更长的帧。

合并的图像进入jpeg,这是一种浏览器可以显示的图像编码,并且重要的是,可以显示为一系列帧的流媒体。

编码的数据通过HTTP服务发送,将数据放入你可以用网页浏览器查看的系统。它使用模板(一些布局和文本用于浏览器)来提供服务。

图像输出随后通过HTTP服务,通过网络传输到用户,浏览器。最后,浏览器将图像显示给用户。浏览器可以是笔记本电脑或手机。

是时候开始构建代码了。我们将将其分解为两个主要部分:首先是一个CameraStream对象,它将我们的帧发送到代码项目的第二部分,即image_server.py脚本。

编写CameraStream对象

作为我们系统的一部分,我们将创建一个辅助库来设置摄像头并从中获取数据流:

  1. 以以下导入开始camera_stream.py文件:

    PiCamera code needed to access our camera. cv2 is OpenCV, the computer vision library used to process the images. Here, NumPy is *aliased*, or nicknamed, np. 
    
  2. 接下来的几行设置了捕获大小和图像质量的参数:

    encode parameter. 
    
  3. 添加一个设置摄像头的函数:

    def setup_camera():
        camera = PiCamera()
        camera.resolution = size
        camera.rotation = 180
        return camera
    

    初始化摄像头后,我们将其分辨率设置为大小。我提到摄像头是颠倒的,因此我们将其旋转设置为180度以翻转图片。

  4. 我们需要一个函数来开始捕获一系列图像(一个视频,但一次一个帧):

    PiRGBArray instance, a type for storing RGB images. We then set up the stream of data with capture_continuous, a picamera method to take photos repeatedly. We pass it to the image store and tell it to format the output data as bgr (blue, green, red), which is how OpenCV stores color data. The last parameter to this is use_video_port, which, when set to true, results in a reduction in image quality in exchange for faster production of frames. 
    
  5. 我们可以循环遍历cam_stream直到我们选择停止。Python有一个概念,即for循环是一个生成器。每次循环都会产生流捕获的帧的原始.array。这意味着循环可以使用start_stream函数的输出,因此当循环遍历时,这个for循环中的代码将运行足够的时间以产生一个原始帧,然后是下一个,依此类推。Python生成器是构建处理管道的一种方式。

    循环的最后一行调用truncate以重置image_storage,使其准备好存储另一张图像。PiRGBArray可以按顺序存储许多图像,但我们只想保留最新的一张。在我们处理帧的同时,可能已经到达了多张图像,因此我们必须丢弃它们。

  6. 我们添加到camera_stream.py脚本中的最后一件事是一个函数,用于将图像编码为jpeg然后转换为字节以发送,如下所示:

    def get_encoded_bytes_for_frame(frame):
        result, encoded_image = cv2.imencode('.jpg', frame, encode_param)
        return encoded_image.tostring()
    

我们将使用camera_stream库来执行我们的某些行为,这使我们能够获取和编码摄像头帧,既可用于输入,也可用于显示。有了这些准备,让我们在一个测试应用程序中使用它来在浏览器中提供帧。

编写图像服务器主应用程序

应用程序的这一部分将设置 Flask,启动我们的相机流,并将它们连接起来。我们将把这个放在一个名为 image_server.py 的新脚本中:

  1. 我们需要导入所有这些组件并设置一个 Flask 应用程序:

    Flask app object, which handles routing; a way to render templates into output; and a way to make our web app response. We import the camera_stream library we've just made, and we import time so we can limit the frame rate to something sensible. After the imports, we create a Flask app object for us to register everything with.
    
  2. Flask 在路由中工作,这些路由是您击中 Web 服务器地址和注册的处理函数之间的链接。在我们的服务器应用程序中请求的匹配地址将运行相应的函数。让我们设置最基本的路由:

    '/' route will be the index page, what you get by default if you just land on the robot's app server. Our function renders a template, which we'll write in the next section. 
    
  3. 现在我们来到了一个棘手的部分,视频流。尽管 camera_stream 进行了一些编码,但我们还需要将帧转换为浏览器期望的连续数据流,即数据流。我将在 frame_generator 函数中实现这一点,我们稍后会对其进行分解。让我们先设置相机流:

    time.sleep is here because we need to let the camera warm up after turning it on. Otherwise, we may not get usable frames from it. 
    
  4. 接下来,我们需要遍历来自 camera_stream 的帧:

    start_stream, encoding each frame to JPG. 
    
  5. 为了将编码的帧字节发送回浏览器,我们使用另一个带有 yield 的生成器,这样 Flask 就会将其视为多部分流——由多个数据块组成的响应,这些块被延迟到稍后处理——这对于同一视频的多个帧来说很常见。注意,HTTP 内容声明是编码字节的序言:

    b in front of this string to tell Python to treat this as raw bytes and not perform further encoding on the information. The \r and \n items are raw line-ending characters. That completes the frame_generator function. 
    
  6. 下一个函数,命名为 display,将 Flask 路由到来自 frame_generator 的可循环 HTTP 帧流:

    display route generates a response from frame_generator. As that is a generator, Flask will keep consuming items from that generator and sending those parts to the browser. The response also specifies a content type with a boundary between items. This boundary must be a string of characters. We have used `frame`. The boundary must match in `mimetype` and the boundary (`--frame`) in the content (*step 5*).
    
  7. 现在,我们只需添加代码来启动 Flask。我已经将此应用程序放在端口 5001

    app.run(host="0.0.0.0", debug=True, port=5001)
    

应用程序几乎准备好了,但我们提到了一个模板——让我们用这个来描述相机流将在网页上显示的内容。

构建模板

Flask 使用 HTML 模板创建网页,这些模板将函数渲染到输出中,如果需要,在运行时替换一些元素。创建一个 templates 文件夹,然后在其中创建一个名为 image_server.html 的文件:

  1. 我们的模板从 HTML 标签开始,有一个标题和一级标题:

    <html>
        <head>
            <title>Robot Image Server</title>
        </head>
        <body>
            <h1>Robot Image Server</h1>
    
  2. 现在,我们添加一个图像链接来显示服务器输出:

    url_for here. Flask can use a template renderer, Jinja, to insert the URL from a route in Flask by its function name.
    
  3. 最后,我们只需在模板中关闭标签:

        </body>
    </html>
    

我们可以在主服务器应用程序中提供这个模板。

现在,我们可以上传这三个部分,确保您将模板上传到 Pi 的 templates 目录中。

服务器代码和模板准备就绪后,您应该能够运行图像服务器。

运行服务器

使用 python3 image_server.py 启动应用程序。

将您的浏览器指向应用程序,通过访问 http://myrobot.local:5001(或您的机器人地址),您应该会看到一个视频流,如图 图13.10 所示:

图13.10 – 机器人图像服务器的屏幕截图

图13.10 中的截图显示了在浏览器中我们的机器人图像服务器输出。顶部显示了浏览器搜索栏,其中包含 myrobot.local:5001 地址。下面是这个地址,是模板中的 机器人图像服务器 标题。标题下面是机器人相机拍摄的儿童红色保龄球柱的图像捕获——与视频流代码一起提供。

故障排除

如果您在运行服务器和查看图片时遇到问题,请尝试以下步骤:

  • 如果在运行代码时看到错误,请执行以下操作:

    a) 确保可以使用 raspistill 捕获图像。

    b) 确保已安装所有必需的依赖项。

    c) 如果是关于 libatomic 的,请确保您已执行了之前的 LD_PRELOAD 导出。

    d) 检查代码是否正确。

  • 如果图像是黑色的,请检查您的照明。树莓派摄像头对光照条件敏感,需要明亮的空间才能正常工作。请注意,如果摄像头没有获得足够的光线,以下任何跟踪都不会工作。

  • 预期速度会较慢——这不是快速或高质量的捕获。

现在,您可以从树莓派将图像流式传输到浏览器。接下来,我们将向应用程序添加后台工作任务和控制机制,因为整个服务器都依赖于缓慢的浏览器请求周期。

在流式传输时运行后台任务

我们的形象服务虽然工作,但有一个显著的缺陷。目前它将在每个动作之间等待请求,但如果我们想让我们的机器人做些什么呢?为了做到这一点,我们需要能够在服务器并行运行一个行为。这个行为和服务器都需要访问图像数据。

我们将通过将 Flask 网络应用程序作为次要进程,将行为作为机器人运行时的主要进程来解决这个问题。Python 有一个方便的工具可以精确地实现这种结构,称为 multiprocessing。更多信息请访问 https://docs.python.org/3/library/multiprocessing.html

在多个进程之间进行通信很复杂。如果两个进程同时尝试访问(读取或写入)相同的数据,结果可能是不可预测的,并可能导致奇怪的行为。因此,为了避免它们同时尝试访问数据,我们将使用多进程队列对象。队列允许一个进程在一端放入数据,另一个进程在另一端安全地消费它——这是一个信息单向流动。我们将使用一个队列将图像发送到服务器,另一个队列从浏览器中的用户交互获取控制数据。

图13.11 中的图表显示了数据将通过这些行为流动的方式:

图13.11 – 浏览器、服务器进程和机器人行为之间的数据流

图13.11 中,我们对 图13.9 中的某些部分进行了简化。首先,有来自摄像头的数据进入视觉处理行为(例如,跟踪一个对象)。这种行为将输出图像帧到图像队列。输出将是完全处理并合并的图像。

服务器进程,即网络应用程序,将从图像队列中获取图像并通过网络将它们发送到浏览器。然而,网络应用程序也将处理来自浏览器用户交互的命令。应用程序将它们作为消息放入控制队列。视觉处理行为将读取控制队列中的任何消息并对它们采取行动。

一些注意事项:视觉处理行为只有在图像队列空时才会将图像放入图像队列,因此队列将始终只包含一个图像。只允许一个可以防止视觉处理行为在服务器尝试输出图像时尝试覆盖共享内存中的图像。控制队列没有这样的限制;我们只期望用户交互不会产生比行为循环消耗它们更快的控制消息。

我们将把网络应用分为核心部分,然后基于它编写一个行为。我们可以多次使用网络应用核心。让我们编写这段代码。

编写网络应用核心

在这个设计中,网络应用核心将处理设置队列、运行服务器进程以及基于 Flask 的路由。我们将以 Flask 风格编写库,在模块中使用纯 Python 函数。

作为核心的接口,我们的其他行为将能够执行以下操作:

  • start_server_process(template_name) 将启动网络应用服务器,使用指定的模板。

  • put_output_image(encoded_bytes) 将图像放入显示队列。

  • get_control_instruction() 用于检查并返回控制队列中的指令。这个函数返回一个包含指令数据的字典。

应用程序的 Flask/web 服务器部分在行为上稍微独立一些,允许用户调整以查看其显示,但当用户不在场或浏览器卡住时,它不应该停止应用程序的运行:

  1. 让我们从一些导入开始。我们将这段代码放在 image_app_core.py 文件中:

    Queue and Process to create the process and communicate with it. We then use the same imports for Flask that we used previously. Note—we are *not* importing any of the camera parts in this module.
    
  2. 接下来,我们定义我们的 Flask 应用和队列。我们实际上只想有一个帧排队,但以防在传输过程中出现故障,我们放入了一个——尽管我们可以检查 Queue 实例是否为空,但这并不总是100%可靠的,我们不希望应用程序的一部分等待另一部分:

    app = Flask(__name__)
    control_queue = Queue()
    display_queue = Queue(maxsize=2)
    
  3. 我们还将在这里定义一个全局的 display_template,在其中我们将存储主应用程序模板:

    display_template = 'image_server.html'
    
  4. 现在我们为这个 Flask 应用添加路由。索引路由只在其使用 display_template 方面有所不同:

    @app.route('/')
    def index():
        return render_template(display_template)
    
  5. 接下来,我们将创建获取帧的循环:frame_generator 的修改版。这个函数是我们的主要视频源。为了防止它旋转(即,在紧密的循环中非常快速地运行),我们加入了一个0.05秒的睡眠,以将帧率限制在每秒20帧:

    def frame_generator():
        while True:
            time.sleep(0.05)
    
  6. 睡眠之后,我们应该尝试从 display_queue 中获取数据(我们稍后会把帧放入队列)。就像在 image_server 中做的那样,这个循环也将我们的数据转换成多部分数据:

            encoded_bytes = display_queue.get()
            yield (b'--frame\r\n'
                    b'Content-Type: image/jpeg\r\n\r\n' + encoded_bytes + b'\r\n')
    
  7. 现在通过一个显示块使其可用:

    @app.route('/display')
    def display():
        return Response(frame_generator(),
            mimetype='multipart/x-mixed-replace; boundary=frame')
    
  8. 我们需要一种方法来向我们的应用程序发送控制消息。control 路由接受这些消息,获取它们的表单数据(一个包含指令的字典),并使用 control_queue.put 将其传递给机器人行为:

    @app.route('/control', methods=['POST'])
    def control():
        control_queue.put(request.form)
        return Response('queued')
    
  9. 这为我们提供了所有核心内部结构,但我们也需要启动服务器进程。之前启动我们服务器的应用程序部分,我们现在将其放入一个名为 start_server_process 的函数中:

    template_name in the global display_template. The preceding index route uses the template. Instead of calling app.run, we create a Process object. The Process parameter target is a function to run (app.run), and some parameters need to be given to that function (the host and port settings). We then start the server process and return the process handle so our code can stop it later.
    
  10. 下一个界面任务是将在步骤1中创建的队列中的图像放入队列。为了确保我们不会消耗太多内存,我们只打算让队列长度为1。这意味着第一个帧会过时,但下一个帧很快就会到达,不会影响用户:

    def put_output_image(encoded_bytes):
        if display_queue.empty():
            display_queue.put(encoded_bytes)
    
  11. 最后,对于这个界面,我们需要一个函数来获取控制消息。这个函数不会等待,如果有消息则返回消息,否则返回None表示没有消息:

    def get_control_instruction():
        if control_queue.empty():
            return None
        else:
            return control_queue.get()
    

image_app_core.py文件为我们提供了一个可控的基础,我们可以用它来构建视觉处理机器人行为,或者实际上任何具有网络界面、控制指令、输出流和后台进程的行为。接下来,让我们用简单行为测试这个核心。

使行为可控

我们可以用一个将图像发送到网络服务并接受简单exit控制消息的行为来测试我们的核心:

  1. 让我们创建一个名为control_image_behavior.py的新文件,从导入image_app_core接口和camera_stream开始:

    import time
    from image_app_core import start_server_process, get_control_instruction, put_output_image
    import camera_stream
    
  2. 然后,我们添加一个函数,在主循环中运行我们的简单行为。由于这个函数有点复杂,所以我将其拆分。首先,我们将设置摄像头并使用睡眠来给摄像头预热时间:

    def controlled_image_server_behavior():
        camera = camera_stream.setup_camera()
        time.sleep(0.1)
    
  3. 接下来,我们在for循环中从摄像头流中获取帧,并将这些帧作为编码的字节放在输出队列中:

        for frame in camera_stream.start_stream(camera):
            encoded_bytes = camera_stream.get_encoded_bytes_for_frame(frame)
            put_output_image(encoded_bytes)
    
  4. 在循环中,我们将尝试接受一个退出控制指令。通常指令将是None,表示没有等待的控制指令。但如果我们有消息,我们应该匹配其中的命令以退出:

    return to stop the behavior when it receives the exit instruction from the control queue. 
    
  5. 然后,我们需要启动服务器并开始我们的行为。我们总是希望停止网络服务器进程。通过将行为包裹在tryfinally中,它将总是运行finally部分中的任何内容,在这种情况下,确保进程被终止(停止):

    process = start_server_process('control_image_behavior.html')
    try:
        controlled_image_server_behavior()
    finally:
        process.terminate()
    

现在我们有一个简单的可控行为;然而,它提到了control_image_behavior.html模板。我们需要提供这个模板。

制作控制模板

这个模板,在templates/control_image_behavior.html中,与之前的相同,但有两个重要的不同之处,这里用粗体标出:

<html>
    <head>
        <script src="img/jquery-3.3.1.min.js"></script>
        <title>Robot Image Server</title>
    </head>
    <body>
        <h1>Robot Image Server</h1>
        <img src="img/{{ url_for('display') }}"><br>
        <a href="#" onclick="$.post('/control', {'command': 'exit'}); ">Exit</a>
    </body>
</html>

差异如下:

  • 在这个模板中,我们在浏览器中加载了一个名为jquery的库,这对于交互式网页来说非常方便。jQuery在https://api.jquery.com/有很好的文档。

  • 我们有之前看到的图像和标题,但在这段代码中新增了一个a标签(用于锚点),当点击时,会将exit命令发送到我们的网络应用的'/control'路由。<br>创建一个换行,以在图像下方显示退出链接。

如果你想在没有互联网接入的地方运行这个程序,你需要服务器提供jquery库。这个模板告诉浏览器直接从互联网下载jquery

现在我们有了组件,我们应该尝试运行我们的可控行为。

运行可控制图像服务器

现在我们有了组件,让我们运行它并尝试一下命令:

  1. 要运行图像服务器,您需要上传所有三个文件:

    a) image_app_core.py

    b) control_image_behavior.py

    c) templates/control_image_behavior.html

  2. 在您的Pi上,使用python3 control_image_behavior.py来启动进程。

  3. 将您的浏览器指向http://myrobot.local:5001(或您的机器人地址)。您将再次看到这些图片。

  4. 如果您点击图片下方的退出链接,这将向您的应用发送控制指令,应用应该优雅地退出。

您现在已经看到了如何在行为中获取图像数据的同时将控制数据发送回行为。随着控制和流技术经过测试并准备就绪,以及一个用于它的框架,我们可以构建一个更有趣的行为。在下一节中,我们将使机器人跟随一个具有特定颜色的物体。

使用Python跟踪彩色物体

现在我们已经准备好了一些基础知识,我们可以用它来构建一些更有趣的行为。

我们将创建一个行为,使机器人追逐一个彩色物体,但不会离得太近。这种行为会使机器人看起来非常智能。我们将回顾在第9章中介绍的颜色模型,即在Python中编程RGB条带。我们将添加颜色掩码和过滤,并使用OpenCV轮廓工具检测图像中最大的彩色块,并将机器人指向它。

构建颜色追逐行为需要几个步骤。让我们从一张图开始,展示这个整个行为的概述,即图13.12

图13.12 – 颜色追踪行为

图13.12中的数据流从相机图像开始。这些图像经过视觉处理以从图像中获取物体信息。从图像中获取物体信息输出物体的尺寸(基于围绕它的圆的半径)和物体的位置(包围圆的中心)并将帧放入图像队列,供Web应用/浏览器使用。

物体大小进入一个速度比例积分微分(PID)控制器,该控制器也有一个物体大小参考值作为其设定点。根据预期大小和实际大小的差异,这个PID将为电机输出一个速度,优化半径以与参考大小相同。这样,机器人将保持与已知大小的物体之间的距离。这是两个电机的基准速度。

物体位置有一个x分量和一个y分量。这个行为将转向以使物体居中,所以我们感兴趣的是x坐标。x坐标进入PID以控制方向/航向。这个PID接受一个参考位置——摄像头视口的中心。这个方向PID将产生一个输出,试图使这些坐标之间的差异为零。通过增加一个电机的速度并减少另一个电机的速度,机器人将转向面对物体(或者,如果你为了乐趣而交换它们,它将转向远离!)

这些图像通过使用应用核心的图像队列发送到浏览器。图中未显示的控制队列包含启动电机、停止电机和退出行为的消息。

这个系统的最后一部分,也许是最有趣的,就是颜色追踪。标记为从图像获取对象信息的框执行追踪操作。让我们看看它是如何工作的。

将图片转换为信息

我们正在使用儿童保龄球套件中的彩色针。它们有漂亮的鲜艳的原色。我将用绿色作为例子。我们从一个图片开始。然而,需要一组数据转换来将图片转换为机器人可以用来做出决策的信息。

管道是设计一组转换的好方法。让我们看看图13.13中的颜色追踪作为图像处理管道:

图片

图13.13 – 从摄像头获取颜色对象信息

与其他管道一样,图13.13从摄像头开始。这被转换为低分辨率以保持速度。图示显示了这一步骤之上的摄像头图像。

这个过程将图像捕获的输出转换为HSV,这是我们提到的第9章在Python中编程RGB条带中提到的色彩空间。我们使用HSV,因为这意味着过程可以过滤特定色调范围内的颜色,通过它们的亮度(非常暗的物体可能会让我们困惑),以及饱和度,因此它不会包括几乎灰色的物体。RGB(或BGR)图像过滤起来很棘手,因为获取特定色调(比如蓝色)的不同亮度和饱和度级别是不可行的。图示显示了这一步骤之上的色调颜色轮。

OpenCV有一个函数,cv2.cvtColor,可以将整个图像在不同色彩空间之间转换。请注意,OpenCV使用0–179作为色调范围,而不是0–359。这样做是为了使其适合一个字节(0–255),但如果你知道你想要的值,你可以通过简单地除以2来转换色调值。

在转换为HSV之后,我们使用掩码过滤图像中的颜色,突出显示特定范围内的像素。如果物体在该范围内,则输出白色,如果不在此范围内,则输出黑色。在此步骤之上,色调颜色轮上的无阴影区域显示了范围,掩码输出紧邻其旁。OpenCV有一个函数可以做到这一点:cv2.inRange。这给我们一个非常简单的二进制输出,一个掩码图像,我们可以用它来绘制我们的系统。

然后,我们的管道使用轮廓系统来绘制蒙版图像的周围。轮廓仅指定我们物体的边界点。OpenCV提供了一个cv2.findContours函数来执行此操作,它返回一个形状列表,每个形状由其轮廓定义。前面的图显示了从蒙版中提取的轮廓绘制在原始图像上。注意,由于光线和阴影的影响,保龄球瓶的底部有点粗糙,因为它并不完全符合蒙版。

处理管道随后取轮廓(轮廓)并使用cv2.minEnclosingCircle在它们周围绘制圆圈。然后我们将有一些圆,由中心xy坐标和半径描述。前面的图显示了这些圆在原始图像上的投影。

我们的目标物体可能有高光,产生多个圆,其他物体也可能产生较小的圆。我们只对其中一个感兴趣,即这些圆中最大的一个,因此我们可以遍历这些圆,并只保留最大的一个。在获取最大圆步骤之上是只画了最大圆的原始图像。

这个最大圆的坐标和半径为我们机器人开始追逐物体提供了足够的信息。在上一个步骤之上,只是一个圆,上面有十字准星显示其位置。

重要提示

关于红色物体的注意事项:我们将使用绿色,因为红色稍微有点棘手,因为它需要两个蒙版。红色的色调跨越了179(我们色调范围的上限)和0(下限)之间的边界,因此我们不得不对图像进行两次蒙版处理,然后通过or操作将这些蒙版组合起来。你可以使用cv2.bitwise_or函数尝试对红色进行蒙版。

现在我们已经检查了管道的工作方式和其注意事项。我们看到了这个管道如何与PID控制器结合以创建有趣的行为。让我们构建这段代码。

增强PID控制器

我们将使用更多的PID控制器。我们仍然不需要微分组件,但我们将遇到积分组件在电机移动时积累的问题。如果存在恒定误差,积分的求和将开始增长。纠正这个误差是好的,但它可能导致大的超调。由于积分在机器人开始缓慢反应后仍然增长,这种超调称为积分风上

我们可以通过向PID中引入风上限制来防止这个求和值变得过大:

  1. 打开pid_controller.py文件,并在以下片段中做出粗体字的变化。首先,添加windup_limit参数,如果你不设置限制,它默认为None

    class PIController(object):
        def __init__(self, proportional_constant=0, integral_constant=0, windup_limit=None):
            self.proportional_constant = proportional_constant
            self.integral_constant = integral_constant
            self.windup_limit = windup_limit
            self.integral_sum = 0
    
  2. 如果我们有上限并达到它,我们想要防止积分增长。如果以下任何一种情况发生,积分将改变:

    a) 没有风上限制(你将其设置为None)。

    b) 求和的绝对值低于风上限制。

    c) 误差的符号会减少求和(因为它与之相反)。

    这防止了我们在有上限的情况下超过这个限制。

    让我们看看代码示例——这段代码将替换之前的handle_integral方法:

        def handle_integral(self, error):
            if self.windup_limit is None or \
                    (abs(self.integral_sum) < self.windup_limit) or \
                    ((error > 0) != (self.integral_sum > 0)):
                self.integral_sum += error
            return self.integral_constant * self.integral_sum
    
  3. 我们可以从网页上startstop这个行为。如果我们再次开始移动,我们不想让PID携带旧值。让我们添加一个reset函数来将积分总和清零:

        def reset(self):
            self.integral_sum = 0
    

PID控制器现在可以重置,并且有一个防风振限制来停止大的超调。让我们构建使用它的其他行为组件。

编写行为组件

这个行为有两个文件——一个模板,用于将控制按钮传递给我们的应用核心,然后是主要的行为代码。让我们先编写模板。

编写控制模板

这个模板是为流应用设计的,有一些不同的控制项:

  1. templates/control_image_behavior.html模板复制到templates/color_track_behavior.html

  2. 我们将添加两个额外的控制项,startstop,这里用粗体显示:

            <img src="img/{{ url_for('display') }}"><br>
            <a href="#" onclick="$.post('/control', {'command': 'start'});">Start</a>
            <a href="#" onclick="$.post('/control', {'command': 'stop'})">Stop</a><br>
            <a href="#" onclick="$.post('/control', {'command': 'exit'});">Exit</a>
    

    我们打算首先停止机器人运行程序,这样我们就可以用手机或浏览器进行微调,查看机器人检测到什么,然后点击开始按钮让它移动。

模板修改后,我们需要编写行为代码。

编写行为代码

我们将把这个新的行为放入一个名为color_track_behavior.py的文件中:

  1. 没有什么奇怪的,我们首先开始导入。因为我们正在组合许多元素,所以有很多,但我们之前都见过:

    import time
    from image_app_core import start_server_process, get_control_instruction, put_output_image
    import cv2
    import numpy as np
    import camera_stream
    from pid_controller import PIController
    from robot import Robot
    
  2. 现在,我们添加Behavior类来寻找并接近一个彩色对象。我们传递这个robot对象:

    class ColorTrackingBehavior:
        def __init__(self, robot):
            self.robot = robot
    
  3. 这些值旨在针对颜色掩码和对象大小进行调整:

    low_range and high_range values for the color filter (as seen in *Figure 13.13*). Colors that lie between these HSV ranges would be white in the masked image. Our hue is 25 to 80, which correspond to 50 to 160 degrees on a hue wheel. Saturation is 70 to 255—any lower and we'd start to detect washed out or gray colors. Light is 25 (very dark) to 255 (fully lit).The `correct_radius` value sets the size we intend to keep the object at and behaves as a distance setting. `center` should be half the horizontal resolution of the pictures we capture.
    
  4. 这里设置的最后一个成员变量是running。当我们想让机器人移动时,它将被设置为True。当设置为False时,处理仍然发生,但电机和PID将停止:

            self.running = False
    
  5. 下一段代码是处理来自Web应用的任何控制指令:

    start, stop, and exit buttons. It uses the running variable to start or stop the robot moving. 
    
  6. 接下来,我们有代码来从一个帧中找到一个对象。这实现了图13.13中显示的管道。不过,我们将稍微分解这个函数:

        def find_object(self, original_frame):
            """Find the largest enclosing circle for all contours in a masked image.
            Returns: the masked image, the object coordinates, the object radius"""
    

    因为这段代码很复杂,所以我们有一个文档字符串或docstring来解释它做什么以及它返回什么。

  7. 接下来,这个方法将帧转换为HSV,这样就可以使用inRange进行过滤,只留下我们帧中的masked像素:

            frame_hsv = cv2.cvtColor(original_frame, cv2.COLOR_BGR2HSV)
            masked = cv2.inRange(frame_hsv, self.low_range, self.high_range)
    
  8. 现在我们有了掩码图像,我们可以在它周围绘制轮廓(轮廓点):

    RETR_LIST. OpenCV is capable of more detailed types, but they take more time to process. The last parameter is the method used to find the contours. We use the `CHAIN_APPROX_SIMPLE` method to simplify the outline to an approximate chain of points, such as four points for a rectangle. Note the `_` in the return values; there is optionally a hierarchy returned here, but we neither want nor use it. The `_` means ignore the hierarchy return value.
    
  9. 下一步是找到每个轮廓的所有包围圆。我们使用一个微小的循环来完成这个操作。minEnclosingCircle方法获取完全包围轮廓中所有点的最小圆:

    cv2 returns each circle as a radius and coordinates—exactly what we want. 
    
  10. 然而,我们只想得到最大的一个。让我们过滤出来:

    largest value of 0, and then we loop through the circles. If the circle has a radius larger than the circle we last stored, we replace the stored circle with the current circle. We also convert the values to int here, as minEnclosingCircle produces non-integer floating-point numbers.
    
  11. 我们通过返回掩码图像、最大坐标和最大半径来结束这个方法:

            return masked, largest[0], largest[1]
    
  12. 我们下一个方法将接受原始帧和已处理帧,然后将它们转换成双屏显示(两个相同比例的水平拼接图像)并输出到队列,最终传递到Web应用:

    np.concatenate function to join the two images, which are equivalent to NumPy arrays. You could change the axis parameter to 0 if you wanted screens stacked vertically instead of horizontally.
    
  13. 下一个方法通过前面的函数处理一帧数据,找到对象并设置显示。然后,它以以下方式返回对象信息:

    cvtColor to change the masked image to a three-channel image—the original frame and processed frame must use the same color system to join them into a display. We use cv2.circle to draw a circle around the tracked object on the original frame so we can see what our robot has tracked on the web app, too.
    
  14. 下一个方法是将前面的坐标和半径转换为机器人运动的实际行为。当我们开始行为时,云台可能不会指向正前方。我们应该确保机制面向前方,将两个伺服电机设置为0,然后启动相机:

        def run(self):
            self.robot.set_pan(0)
            self.robot.set_tilt(0)
            camera = camera_stream.setup_camera()
    
  15. 当伺服电机移动且相机预热时,我们可以准备我们需要的两个PID控制器,用于速度(基于半径)和方向(基于水平中间的距离):

    speed_pid = PIController(proportional_constant=0.8, 
                integral_constant=0.1, windup_limit=100)
    direction_pid = PIController(proportional_constant=0.25, 
                integral_constant=0.05, windup_limit=400)
    

    这些值是通过大量调整得到的;你可能需要进一步调整这些值。调整PID控制器设置部分将介绍如何调整PID。

  16. 现在,我们等待一会儿,让相机和云台伺服电机稳定下来,然后在中位位置关闭伺服电机:

            time.sleep(0.1)
            self.robot.servos.stop_all()
    
  17. 我们通过print语句通知用户,并输出一些调试标题:

            print("Setup Complete")
            print('Radius, Radius error, speed value, direction error, direction value')
    
  18. 然后,我们可以进入主循环。首先,我们从帧中获取处理后的数据。注意,我们使用括号将coordinates解包到xy

            for frame in camera_stream.start_stream(camera):
                (x, y), radius = self.process_frame(frame)
    
  19. 在这一点上,我们应该检查我们的控制消息。然后,我们检查我们是否被允许移动,或者是否有足够大的对象值得寻找。如果有,我们可以这样开始:

                self.process_control()
                if self.running and radius > 20:
    
  20. 现在我们知道机器人应该移动,所以让我们计算误差值来输入PID控制器。我们获取大小误差并将其输入到速度PID以获取速度值:

                   radius_error = self.correct_radius - radius
                    speed_value = speed_pid.get_value(radius_error)
    
  21. 我们使用中心坐标和当前对象x来计算方向误差,并将其输入到方向PID中:

                    direction_error = self.center - x
                    direction_value = direction_pid.get_value(direction_error)
    
  22. 这样我们就可以调试这个了;我们在这里打印一个与之前显示的标题匹配的调试信息:

                    print(f"{radius}, {radius_error}, {speed_value:.2f}, {direction_error}, {direction_value:.2f}")
    
  23. 我们可以使用速度和方向值来产生左右电机速度:

                    self.robot.set_left(speed_value - direction_value)
                    self.robot.set_right(speed_value + direction_value)
    
  24. 我们已经处理了电机运行时应该做什么。如果它们没有运行,或者没有值得检查的对象,那么我们应该停止电机。如果我们按下了停止按钮,我们还应该重置PID,这样它们就不会积累奇怪的价值:

                else:
                    self.robot.stop_motors()
                    if not self.running:
                        speed_pid.reset()
                        direction_pid.reset()
    
  25. 我们现在已经完成了这个函数和ColorTrackingBehavior类。现在,剩下的只是设置我们的行为和Web应用核心,然后启动它们:

    print("Setting up")
    behavior = ColorTrackingBehavior(Robot())
    process = start_server_process('color_track_behavior.html')
    try:
        behavior.run()
    finally:
        process.terminate()
    

这段行为代码已经构建并准备好运行。你已经看到了如何转换图像,然后针对特定颜色进行掩码处理,以及如何围绕掩码中的blob绘制,然后找到最大的一个。我还展示了如何通过PID将这种视觉处理转换为机器人移动行为,通过输入这些数据并使用它们的输出控制电机运动。让我们试试吧!

运行行为

我相信你很想知道这个是如何工作的,并修复任何存在的问题。让我们开始吧:

  1. 要运行此行为,你需要上传 color_track_behavior.py、修改后的 pid_controller.py 文件和位于 templates/color_track_behavior.html 的模板。我将假设你已经上传了 robot.py 和其他支持文件。

  2. 使用 python3 color_track_behavior.py 启动应用程序,这将启动网络服务器并等待。

  3. 在这一点上,你应该使用你的浏览器连接到 http://myrobot.local:5001,你应该能够看到你的机器人的图像流。

    你可以看到对象及其圆圈,以及控制机器人的链接,如 图13.14 中的截图所示:

    图13.14 – 颜色跟踪网络应用程序

    图13.14 展示了我们的应用程序服务器运行跟踪彩色对象的代码的截图。在地址栏和标题下方是一个双屏类型的输出。左侧是来自摄像头的直接视频流,中间靠近一个儿童绿色保龄球柱,一个蓝色圆圈勾勒出柱子,这是由行为生成的,以显示它正在跟踪最大的匹配对象。右侧是掩码的输出,因此我们可以看到图像的哪些部分匹配,并在需要时调整掩码值。在此之下是开始停止退出链接,用于启动电机、停止电机和退出程序。

  4. 要使机器人开始移动,请按网页上的 开始 按钮。

    当机器人开始移动时,你将在控制台(PuTTY)中看到PID调试输出。这只有在机器人运行时才会显示。

  5. 你可以按网页上的 停止 按钮停止机器人移动或按 退出 按钮退出行为。

机器人可能不会正确移动;动作可能不足或过度。你需要调整PID控制器以获得正确效果,如下一节所示。

调整PID控制器设置

我从比例常数0.1开始,使用 nano 在Pi上快速编辑,直到机器人开始过度射击——也就是说,它超过了目标,然后返回很远——然后我将这个比例常数值减半。

然后,它可能有一个恒定的误差,所以我开始将积分常数提高约0.01来抵消这个误差。调整PID是一个缓慢的过程:首先将对象移至中心附近,调整 direction_pid 直到它相当不错,然后回来调整 speed_pid

重要提示

不要试图同时调整所有值——而是改变一件事并重试。

对于更深入的了解,请参阅 进一步阅读 部分的 调整PID控制器

故障排除

颜色跟踪是一种棘手的行为,有些事情可能会出错:

  • 如果电机停止或减速,最简单的修复方法是使用新电池。

  • 如果有语法错误,请仔细检查你的代码。

  • 确保网络应用程序示例与摄像头兼容,并解决那里出现的问题。

  • 你需要良好的照明,因为掩码可能无法检测到光线不足的对象。

  • 小心视野中可能匹配的其他物体;掩码可能会拾取除你想要的项目之外的东西。

  • 使用网络应用检查你的物体是否在视野中,并且掩码主要显示为白色。如果不是,那么你可能需要调整HSV的上下范围。色调是最可能引起问题的因素,因为饱和度和值范围相当宽容。

  • 如果机器人开始从一侧到另一侧编织,你可能需要调整方向PID。稍微减少比例元素。

  • 如果机器人几乎不转动,你可以稍微增加比例元素。

  • 如果机器人停止了但不是面向检测到的物体,那么增加方向PID的积分元素大约0.01。如果你在前后移动时遇到相同的问题,尝试应用相同的调整。

你已经看到了如何使用相机跟踪一个颜色鲜艳的物体,这是一种你可以用来在房间里定位物体,或者由工业机器人用来检测成熟水果的技术。观看起来非常令人印象深刻。然而,有些物体比颜色更微妙,例如,人脸。在下一节中,我们将探讨如何使用级联特征匹配来挑选出物体。

使用Python跟踪人脸

通过特征检测人脸(或其他物体)是一种智能行为。一旦我们的机器人开始检测人脸,它就会将俯仰和倾斜机制指向最近(好吧,最大的)人脸。

使用Haar级联是一种常见技术,在Paul Viola和Michael Jones(被称为Viola Jones)的一篇论文中有很好的记录。本质上,这意味着使用一系列特征匹配来搜索匹配的物体。我们将概述这项技术,然后将其应用于我们的机器人以创建一种有趣的行为。使用不同的级联模型文件,我们可以挑选出人脸或其他物体。

在图像中寻找物体

我们将使用OpenCV中实现的一个单一且有用的算法,这使得它非常容易使用。它提供了一种简单的方法来检测物体。更高级和复杂的方法涉及机器学习,但许多系统使用Haar级联,包括手机上的相机应用。我们的代码将图像转换为灰度(从黑到灰再到白)以进行这种检测方法。这里的每个像素都包含一个表示光强度的数字。

首先,让我们深入了解一种表示这些图像的方法:积分图像。

转换为积分图像

函数中应用了两个阶段。第一个阶段是生成一个积分图像,或称为求和区域表,如图图13.15所示:

图片

图13.15 – 积分图像和求和区域表

图13.15的左侧显示了一个笑脸类型的图像,其中的数字像素代表阴影,数字越大颜色越浅。每种阴影都有一个数字。

图13.15的右侧是积分图像。积分图像中的每个像素都是之前像素的总和或积分。它将自己添加到它上面的原始像素和左边的像素中。坐标2,2被圈出。它是3x3网格中的最后一个。这里的单元格值为44。44是突出显示的框中像素的总和(9 + 9 + 5 + 9 + 5 + 1 + 5 + 1 + 0)。

当代码对像素进行求和时,积分过程可以使用捷径并使用之前的总和。新的总和等于左边的像素加上上面的像素。例如,对于图像中较远的像素(8,8),也在图中用圆圈标出,我们可以添加所有数字,但重新使用我们已有的结果会更快。我们可以取像素值(1),加上上面的总和(166),再加上左边的总和(164)。这个总和将包括中间像素两次,所以我们需要减去这些像素,所以减去左上角的像素值(146)。这个总和将是1 + 164 + 166 – 146 = 185。计算机可以非常快速地完成这个操作。

这创建了一个与图像相同维度的数字数组。每个坐标是当前坐标和0,0之间所有像素强度的总和。

代码可以使用积分图像快速找到其中任何矩形区域的强度总和,无论其大小。你可以从图像的右下角像素开始,然后减去右上角的像素,留下右上角像素下方的像素总和。我们还想减去左下角的像素。这几乎将总和限制在只有矩形像素上,但我们已经两次减去了上方左上角的区域。为了纠正这一点,需要将左上角的像素值加回:

该方程适用于2x2的小矩形或300x200的大矩形。在进一步阅读部分查看Viola Jones论文以获取更多详细信息。好消息是,你不需要编写这段代码,因为它已经是OpenCV分类器的一部分。级联阶段可以使用这个积分图像快速执行其下一个强大的技巧。

扫描基本特征

这个谜题的下一部分是扫描图像以寻找特征。这些特征非常简单,涉及寻找两个矩形之间的差异,因此它们的应用非常快速。图13.16展示了这些基本特征的选择:

图13.16 – 简单矩形特征类型

图13.16的左上角显示了一个左右特征,其中左侧像素设置为1,右侧设置为0(并着色)。这将匹配垂直对比度特征。图的右上角有两行0(着色),两行1,然后是两行进一步着色的0;这将匹配水平条特征。中间的左侧将顶部三行设置为1,底部三行着色并设置为0,匹配水平对比度特征。图的中间右侧有两列着色的0,接着是两列1,然后是两行进一步着色的0;这将匹配垂直条特征。

底部图像显示一个特征,前几行是三个1,然后是三个0。接着是三行三个0和三个1。这形成了一个小棋盘图案,可以匹配具有对角线对比度的特征。

算法将以特定顺序和相对位置应用类似于图13.16中的矩形,然后每个匹配项将级联到进一步匹配另一个特征的尝试。文件将对象描述为一系列特征。有包含16,000个特征的人脸级联来应用。将每个特征应用到图像的每个部分将花费很长时间。因此,它们以组的形式应用,可能从只有一个开始。如果特征检查失败,则该图像部分将不再进行进一步的特征测试。相反,它们将级联到后续的组测试。这些组包括加权以及在不同角度应用这些特征的组。

如果所有特征检查都通过,则检查的区域被视为匹配。为此,我们需要找到将识别我们的对象的特征级联。幸运的是,OpenCV有一个为面部识别设计的文件,并且我们已经在我们的Raspberry Pi上安装了它。

应用累加区域,然后使用级联文件查找潜在匹配项的整个操作,都可通过两个OpenCV操作实现:

  • cv2.CascadeClassifier(cascade_filename) 将打开指定的级联文件,该文件描述了要测试的特征。该文件只需加载一次,并且可以在所有帧上使用。这是一个构造函数,返回一个CascadeClassifier对象。

  • CascadeClassifier.detectMultiScale(image) 将分类器检查应用于图像。

你现在对常见的(人脸和物体)识别技术有了基本了解。让我们利用现有的行为经验,通过级联分类器的视觉处理来规划人脸追踪行为。

规划我们的行为

我们可以使用与我们的颜色追踪行为相当相似的代码来追踪人脸。我们将设置机器人使用俯仰和倾斜机制来跟随相机中看到的最大人脸。图13.17中的框图显示了人脸行为的概述:

图13.17 – 人脸追踪行为

图13.17中的流程看起来非常熟悉。我们有相同的摄像头到视觉行为到图像队列,我们之前已经见过。这次,视觉处理是项目的xy坐标。我们将位置x输入到中心为x的PID中,以获得一个平移位置,然后由平移伺服电机使用。位置y输入到中心为y的PID中,输出一个倾斜位置到倾斜伺服电机。伺服电机移动摄像头,创建一个视图移动的反馈循环。

差异在于我们发送给PID控制器的数据,以及每个PID控制不同的伺服电机。

现在我们有一个计划;让我们编写代码。

编写人脸跟踪代码

此行为的代码看起来非常熟悉——将先前行为代码适应此目的。可能重构会产生更多通用代码,但当前使用副本更简单。此代码将放入face_track_behavior.py文件。我甚至没有创建一个新的模板,因为颜色跟踪模板对此也适用:

  1. 导入几乎与我们的color_track_behavior相同:

    import time
    from image_app_core import start_server_process, get_control_instruction, put_output_image
    import cv2
    import os
    import camera_stream
    from pid_controller import PIController
    from robot import Robot
    
  2. 行为类的init函数略有不同,它从加载Haar级联开始。同一目录下还有许多其他级联文件,你可以尝试跟踪除了人脸之外的其他事物。此代码使用assert来验证文件是否存在于此处,因为如果OpenCV找不到它,在detectMultiscale中会返回神秘的错误:

    class FaceTrackBehavior:
        def __init__(self, robot):
            self.robot = robot
            cascade_path = "/usr/local/lib/python3.7/dist-packages/cv2/data/haarcascade_frontalface_default.xml"
            assert os.path.exists(cascade_path), f"File {cascade_path} not found"
            self.cascade = cv2.CascadeClassifier(cascade_path)
    
  3. 调节参数包括中心位置和最小人脸大小。我还将PID控制器带到了类中,因此它们可以在这里调节,然后在控制处理程序中重置(你还可以将重置添加到先前的行为中):

            self.center_x = 160
            self.center_y = 120
            self.min_size = 20
            self.pan_pid = PIController(proportional_constant=0.1, integral_constant=0.03)
            self.tilt_pid = PIController(proportional_constant=-0.1, integral_constant=-0.03)
    
  4. 我们的构造函数仍然跟踪行为是否正在运行电机:

            self.running = False
    
  5. 此处的流程控制不同;当接收到stop指令时,它停止电机并重置PID:

        def process_control(self):
            instruction = get_control_instruction()
            if instruction:
                command = instruction['command']
                if command == "start":
                    self.running = True
                elif command == "stop":
                    self.running = False
                    self.pan_pid.reset()
                    self.tilt_pid.reset()
                    self.robot.servos.stop_all()
                elif command == "exit":
                    print("Stopping")
                    exit()
    
  6. 此行为仍然有一个find_object方法,它接受原始帧。首先,我们将图像转换为灰度以减少搜索所需的数据量:

        def find_object(self, original_frame):
            gray_img = cv2.cvtColor(original_frame, cv2.COLOR_BGR2GRAY)
    
  7. 接下来,我们使用带有级联detectMultiScale方法的灰度图像来获取匹配项列表:

    detectMultiScale method creates the integral image and applies the Haar cascade algorithm. It will return several objects as rectangles, with x, y, width, and height values. 
    
  8. 我们可以使用类似于颜色跟踪行为的循环来通过面积找到最大的矩形。首先,我们需要设置一个存储当前最大矩形的存储库,在一个包含面积的数据结构中,然后是一个包含xy、宽度和高度的子列表:

    largest = 0, (0, 0, 0, 0) 
            for (x, y, w, h) in objects:
                item_area = w * h
                if item_area > largest[0]:
                    largest = item_area, (x, y, w, h)
    
  9. 我们返回那个最大矩形的坐标和尺寸:

            return largest[1]
    
  10. make_display方法比颜色跟踪行为简单,因为只有一个图像。尽管如此,它仍然需要编码图像:

        def make_display(self, display_frame):
            encoded_bytes = camera_stream.get_encoded_bytes_for_frame(display_frame)
            put_output_image(encoded_bytes)
    
  11. process_frame方法找到对象,然后在帧上绘制一个矩形进行输出。cv2.rectangle函数需要两个坐标:起始xy和结束xy,以及一个颜色值。为了得到结束坐标,我们需要将宽度和高度加回来:

        def process_frame(self, frame):
            (x, y, w, h) = self.find_object(frame)
            cv2.rectangle(frame, (x, y), (x + w, y + w), [255, 0, 0])
            self.make_display(frame)
            return x, y, w, h
    
  12. 接下来是run函数。我们开始于摄像头设置和预热时间:

        def run(self):
            camera = camera_stream.setup_camera()
            time.sleep(0.1)
            print("Setup Complete")
    
  13. 与颜色跟踪行为一样,我们通过处理帧并检查控制指令来启动主循环:

            for frame in camera_stream.start_stream(camera):
                (x, y, w, h) = self.process_frame(frame)
                self.process_control()
    
  14. 我们只想在检测到足够大的对象(使用高度,因为人脸在这个维度上通常更大)并且机器人正在运行时移动:

                if self.running and h > self.min_size:
    
  15. 当我们知道机器人正在运行时,我们将PID和输出值直接发送到伺服电机,用于水平和垂直移动。请注意,为了找到对象的中心,我们取坐标并加上其宽度或高度的一半:

                    pan_error = self.center_x - (x + (w / 2))
                    pan_value = self.pan_pid.get_value(pan_error)
                    self.robot.set_pan(int(pan_value))
                    tilt_error = self.center_y - (y + (h /2))
                    tilt_value = self.tilt_pid.get_value(tilt_error)
                    self.robot.set_tilt(int(tilt_value))
    
  16. 为了跟踪这里发生的事情,建议使用调试print语句:

                    print(f"x: {x}, y: {y}, pan_error: {pan_error}, tilt_error: {tilt_error}, pan_value: {pan_value:.2f}, tilt_value: {tilt_value:.2f}")
    
  17. 最后,我们需要添加设置和运行我们行为的代码。请注意,我们仍然使用颜色跟踪模板:

    print("Setting up")
    behavior = FaceTrackBehavior(Robot())
    process = start_server_process('color_track_behavior.html')
    try:
        behavior.run()
    finally:
        process.terminate()
    

代码准备就绪,包括设置函数,我们可以尝试运行并查看行为运行。

运行人脸跟踪行为

要运行此行为,您需要已经上传了颜色跟踪行为文件:

  1. 上传face_track_behavior.py文件。

  2. 开始使用$ python3 face_track_behavior.py

  3. 将您的浏览器导航到http://myrobot.local:5001。您应该看到一个摄像头的单帧画面,其中最大的脸周围有一个矩形轮廓。

  4. 您必须按下开始按钮,机器人才能移动。

平移和倾斜机制上的伺服电机应该移动,试图将您的脸放在屏幕中间,这意味着摄像头正对着您。如果您四处移动头部,摄像头会(缓慢地)跟随您。如果您有人在您身后站立,行为不会检测到他们,但如果您用一只手遮住一半的脸,它将停止识别您,并转向他们的脸。

故障排除

从我们之前行为中提到的故障排除步骤开始——这应该能解决大部分问题——然后如果您需要,再尝试以下步骤:

  • 如果应用程序找不到Haar级联文件,请检查文件所在位置。这些文件在OpenCV包装版本之间可能已经移动,也可能再次移动。请确认您没有输入错误。如果没有,那么请尝试以下命令:

    $ find /usr/ -iname "haarcas*"
    

    此命令应显示Raspberry Pi上文件的位置。

  • 如果摄像头无法在图片中检测到人脸,请确保区域有良好的照明。

  • 检测算法仅适用于正对摄像头的人脸,任何遮挡人脸一部分的东西都会欺骗它。它有点挑剔,所以眼镜和帽子可能会使其困惑。

  • 只有部分在帧中的人脸也可能被忽略。距离太远或太小的人脸会被过滤。降低最小参数将检测到更多对象,但也会从微小的类似人脸对象中产生误报。

  • 请检查缩进是否匹配,因为这可能会改变Python中事件发生的意义。

您现在编写的代码将能够在摄像头视图中检测和跟踪人脸。人脸跟踪行为肯定会给人留下深刻印象。让我们总结一下本章所看到的内容。

摘要

在本章中,你看到了如何设置树莓派摄像头模块。然后你使用它来看到机器人看到的世界——机器人的视角。

你让机器人将摄像头作为手机或桌面上的Web应用显示,然后使用摄像头来驱动智能颜色和面部跟踪行为。我建议了增强这些行为的方法,并希望让你尝到计算机视觉能做什么。

在下一章中,我们将扩展我们的物体跟踪视觉处理,使用摄像头跟踪线条,看到更多使用摄像头的方法。

练习

这段代码很有趣,但你可以用很多方法来改进这些行为。以下是一些扩展此代码并加深你学习的建议方法:

  • 使用控制管道允许用户从网页调整颜色过滤器、半径和PID值。也许初始PID值应该接近其他可调整的值?

  • 有相当多的设置代码。你能把它放入一个函数/方法中吗?

  • 能否将发送到网页的队列用于将调试数据发送到页面,而不是在控制台打印它们?数据能否在图表中绘制?

  • 使用Pi摄像头进行跟踪的视野相当窄。一个广角镜头将大大提高视野,让机器人看到更多。

  • 当环境变暗时,摄像头表现不佳。机器人有一个LED灯带,但它并没有提供太多照明。你能为摄像头添加一个明亮的LED作为头灯吗?

  • 你可以通过尝试在树莓派上的/usr/share/opencv/haarcascades文件夹中找到的其他级联文件来跟踪其他物体。

  • 也许你可以尝试交换两种行为的特征,使用伺服电机来跟踪彩色物体,或者追逐面部?

  • 你能将俯仰机构与主轮结合来跟踪物体,然后启动主轮来追逐匹配的面部,同时保持摄像头中心对准并保持物体在视野中吗?这可能需要一些复杂的PID控制器思考。

有这些想法,你应该有足够多的方法来进一步挖掘这种视觉处理。

进一步阅读

视觉处理是一个深奥的话题,所以这只是一个你可以阅读更多关于使用摄像头进行视觉处理的小样本:

第十七章:第14章:使用Python进行相机线跟踪

在上一章中,我们学习了如何使用相机来追踪和跟踪物体。在本章中,我们将扩展相机代码以创建线感应行为。

我们将探讨机器人使用线跟踪的地方以及它的有用性。我们还将了解不同机器人中跟踪路径的不同方法,以及它们的权衡。你将看到如何构建一个简单的线跟踪轨道。

我们将了解一些不同的算法来使用,然后选择一个简单的算法。我们将制作一个数据流图来查看其工作原理,收集样本图像进行测试,并根据样本图像调整其性能。在这个过程中,我们将看到更多处理计算机视觉和从中提取有用数据的方法。

我们将增强我们的PID代码,将我们的线检测算法集成到机器人驾驶行为中,并看到机器人使用这种方式运行。本章结束时,我们将讨论如何进一步发展这一想法。

在本章中,我们将涵盖以下主要主题:

  • 线跟踪简介

  • 制作线跟踪测试轨道

  • 线跟踪计算机视觉流程

  • 使用测试图像尝试计算机视觉

  • 使用PID算法进行线跟踪

  • 再次找到线条

技术要求

对于本章,你需要以下物品:

  • 机器人及其代码来自第13章机器人视觉 – 使用Pi相机和OpenCV

  • 一些白色或黑色绝缘胶带

  • 一些A2纸或板子 – 与绝缘胶带相反的颜色

  • 一把剪刀

  • 良好的照明

本节代码可在https://github.com/PacktPublishing/Learn-Robotics-Programming-Second-Edition/tree/master/chapter14找到。

查看以下视频以查看代码的实际应用:https://bit.ly/3slLzbQ

线跟踪简介

在我们开始编写代码之前,让我们了解一下线跟踪机器人的行为,这些系统在哪里以及如何使用它们,以及不同的实现技术。

什么是线跟踪?

一些机器人需要在任务中保持特定的路径。对于机器人来说,导航线条比规划整个房间或建筑物的地图要简单。

简而言之,线跟踪是指能够自主地跟随标记路径。这些可以是视觉标记,例如蓝色胶带或黑色道路上的一条白色线条。当机器人沿着线条行驶时,它将不断寻找前方线条的位置,并纠正航向以跟随该线条。

在机器人竞赛中,沿着线条进行比赛是一个常见的挑战,准确性之后速度至关重要。

工业应用

线路跟踪行为最常见的使用场景是工业领域。被称为自动导引车AGVs)的机器人需要因多种原因遵循既定路径。这些任务可以是仓库机器人保持在堆叠产品通道之间的轨道上,或者工厂机器人保持在远离其他工作区域的路径上。线路可能标记了存储货架和装货区或机器人充电站和机器人工作区之间的路线:

图14.1 – IntellCart – Mukeshhrs的线路跟踪工业机器人[公有领域]

图14.1所示的IntelliCart使用明亮的蓝色引导胶带,尽管在大多数工业应用中,机器人使用地板下的磁轨。

路线可能包括选择点,从特定位置发出多条线路。根据其任务,机器人可能需要额外的线索来感知它已经到达这些点。工程师可以为完全自动化的系统设置重复路径。

这些明确的标记意味着你可以设置安全边界,并清楚地了解人类和机器人是否互动;这意味着机器人很少会在人们不太了解的区域外操作。

线路跟踪类型

线路跟踪和相关系统有几个主要分支。

视觉线路跟踪是目前最常见且易于设置的线路跟踪技术。它由机器人检测到的涂画、绘制或贴上的视觉线路组成。光学线路简单,但表面污垢和光照条件可能会使其不可靠。检测方式主要分为以下几类:

  • 通过光传感器检测:在这种情况下,我们将小型传感器附着在机器人底部靠近线路的位置。它们被调整以输出二进制开/关信号或模拟信号。它们通常有灯光照射到表面。这些传感器体积小且价格低廉,但需要额外的I/O。

  • 通过摄像头检测:如果你已经使用摄像头,并且有I/O引脚,这将节省空间。它简化了安装和布线的复杂性。然而,这需要在软件复杂性上做出权衡,因为你的机器人需要计算机视觉算法来分析这些。

磁性线路跟踪用于需要保护线路免受外界因素影响的情况。对于这种变化的某些变体,你可以引导机器人在多条路径上。以下是一些变体:

  • 在地板上运行磁条,可以让霍尔效应传感器(如第12章使用Python进行IMU编程)检测磁条的位置。一系列这样的传感器可以确定线路的方向并跟随它。这比画线路更容易更改,但可能成为绊脚石。

  • 在地板上或其下方运行带有电流的电线,可以达到相同的效果。通过多条电线和一些不同的电路,系统可以将机器人引导到不同的路径上。

  • 在地板下隐藏线条消除了绊倒危险,但意味着您需要在工业机器人跟随的路径上为人类画上警告标记。

现在,您已经看到了两种主要的线跟踪类型;值得提一下其他一些在现实世界中确定机器人路径的方法:

  • 信标:超声波、发光或无线电发射的信标可以放置在环境中以确定机器人的路径。这些可能是激光或其他光线的反射器。

  • 视觉线索:如果您在墙壁和柱子上放置二维码或其他可见标记,它们可以编码一个精确的位置。

您已经看到了机器人如何通过可见线条和隐藏线条(如地板下的电线和磁传感器)进行线感应。因为它更容易,我们将使用可见线条。

简单的光学传感器需要额外的布线,但如果我们已经有一个能够做到这一点的相机,为什么不利用它呢?

在本章中,我们将专注于使用我们已有的相机和视觉轨道进行跟踪,并跟随那里的线条。我们将接受代码的复杂性,同时简化我们机器人的硬件方面。

现在您已经对不同的线跟踪类型及其使用场合有了些了解,让我们创建一个机器人可以跟随的测试轨道。

制作一条线跟踪测试轨道

由于您将使您的机器人跟随一条线,我们需要从一个要跟随的线条部分开始。该轨道最初将用于测试我们的线检测算法,然后当我们打开电机并开始沿着线条行驶时,可以将其扩展到更令人兴奋的轨道。在本节中,我将向您展示的是易于制作和可扩展的。它允许您尝试不同的线条形状和曲线,并观察机器人如何响应。

您甚至可以尝试不同的颜色和对比度选项。

准备测试轨道材料

以下照片显示了所需的主要材料:

图片

图14.2 – 制作测试轨道的材料

图14.2中的照片显示了一卷黑色电气胶带铺在一张大白纸上。对于本节,您需要以下材料:

  • 一些A2纯白色纸张或板。

  • 一些黑色电气绝缘胶带或油漆工胶带。确保这种胶带是不透明的。

  • 一把剪刀。

如果它们是白色涂漆的,您可以用板子替换纸张。

您也可以通过使用黑色或深色纸张和白色胶带来更换物品。这种胶带必须是半透明的,以便与背景形成良好的强烈对比。

制作一条线

将纸张平铺。然后,用胶带在纸张中间画一条线:

图片

图14.3 – 在纸张上平滑胶带

图14.3中的照片显示了带有胶带线条的纸张和我用手指抚平胶带的情景。确保将胶带抚平。你不需要担心使其完全笔直,因为这个系统的整个目的就是在线条弯曲时也能跟随线条。

一旦你在纸上贴上几段这样的胶带,为什么不制作一些有趣的部件,比如以下图中的那些:

图14.4 – 一些与直线相邻的不同形状

图14.4所示,你可以尝试曲线和故意不完全直的线条。你可以将这些线条与直线结合起来,形成整个区域,就像一个机器人火车套装!这些线条在后续调整和玩转跟随代码时将会很有趣。

现在我们已经准备好了测试轨道,我们可以考虑如何视觉处理线条。

跟随线条的计算机视觉流程

正如我们在之前的计算机视觉任务中所做的那样,我们将将其视为一个流程。在我们这样做之前,有许多使用计算机视觉跟踪线条的方法。

摄像头线条跟踪算法

我们有兴趣选择其中最简单的一个,但就像往常一样,总有一个权衡,那就是其他算法可以处理更复杂的情况或比我们的算法更好地预测曲线。

这里是一些我们可以使用的方法:

  • 使用边缘检测: 可以运行边缘检测算法,例如Canny边缘检测器,对图像进行处理,将找到的任何过渡转换为边缘。如果我们想使用这个系统,OpenCV内置了边缘检测系统。该系统可以检测从暗到亮和从亮到暗的边缘。它对不太尖锐的边缘更加宽容。

  • 沿线条寻找差异: 这就像 cheeky 边缘检测,但只针对特定的一行。通过在图像中每一行的每个像素之间找到差异,任何边缘都会显示出显著差异。它比Canny算法简单且成本低;它可以处理任意方向的边缘,但需要尖锐的对比度。

  • 寻找亮度和使用绝对亮度区域作为线条: 这非常简单,但过于简单,难以给出好的结果。它对反转不敏感,但不需要跟踪边缘,因此不需要尖锐的对比度。

使用前面三种方法中的一种,你可以在一个图片区域找到线条,并直接瞄准那里。这意味着你将无法预先预测路线变化。这是最简单的方法。所选区域可能是屏幕底部的单行。

或者,你可以使用前面的方法来检测摄像头图像中的线条并为其绘制轨迹。这更复杂,但更能应对更陡峭的转弯。

值得注意的是,我们可以使用Pi摄像头的原始YUV数据制作一个更高效但更复杂的算法。为了简单起见,我们将坚持使用简单的算法。随着你在复杂性和理解上的进一步交易,你可以找到更快、更准确的方法。

我们系统的另一个主要限制是摄像头的视野宽度。你可以使用镜头让摄像头捕捉更宽的视觉范围,这样机器人就不会经常丢失线条。

我们将使用的方法是沿着线条寻找差异,因为它简单且能够应对不同的线条颜色。我们还将简单地沿着单列查找,这导致数学更加直接。

管道

我们可以将数据处理方式视为一个管道。在我们这样做之前,让我们快速解释一下离散差异。每个像素的亮度是一个介于0(黑色)和255(白色)之间的数字。为了得到差异,你从每个像素减去它右侧的像素:

图14.5 – 像素之间的离散差异

图14.5中,有六组不同色调的像素:

  1. 第一幅图显示了两个白色像素。它们之间没有差异。

  2. 当灰色像素后面跟着白色像素时,会产生一个小的差异。

  3. 黑色像素后面跟着白色像素会产生一个大的差异。

  4. 白色像素后面跟着黑色像素会产生一个大的负差异。

  5. 黑色像素后面跟着黑色像素不会产生差异。

  6. 灰色像素后面跟着黑色像素会产生一个小的负差异。

应该很容易看出,对比度高的线条边缘会产生最大的差异,无论是正的还是负的。我们的代码将寻找这些差异。

下面的图示展示了我们如何处理该方法的摄像头数据:

图14.6 – 寻找线条的图像处理管道

图14.6中,我们展示了寻找跟随线条的过程。它从摄像头开始,我们从320×240像素分辨率的摄像头捕获图像。管道中的下一步是将图像转换为灰度 – 我们现在只对亮度感兴趣。

由于图像可能有噪声或颗粒,我们模糊它;这并不是严格必要的,这取决于你从哪个清晰的环境中拍照。

我们从这个图像中切出候选行;这个行不应该太高,因为那条线可能太远,或者根据摄像头的位置,地平线以上可能有随机的东西。行也不应该太低,因为它会离机器人太近,以至于它无法及时反应。在切出候选行框上方是一个示例,展示了切出的行和它来自的图像。

我们然后将这一行视为一组数字,并获取它们之间的离散差异。在离散差异框上方的图表显示,当行从浅灰色变为黑色时,出现一个大的负峰值,随后当行再次从黑色变为浅灰色时,出现一个大的正峰值。注意,图表的大部分显示为零线,因为颜色块之间没有差异。

下一步是找到最大和最小位置,具体来说是在行中的哪个位置。我们想要的是高于零的最高点的位置/索引和低于零的最低点的位置/索引。现在我们知道了我们线条的边界可能在哪里。

我们可以通过将这些边界之间的位置相加并除以2来找到线条的中心,这将是一个相对于相机图像中间的X位置。

现在,您已经看到了一些测试图像的流程。是时候获取一些自己的测试图像,并尝试使用一些代码来运行这个算法了。

尝试使用测试图像进行计算机视觉

在本节中,我们将探讨如何以及为什么使用测试图像。我们将为这种行为编写我们的第一段代码,并在机器人的相机测试图像上尝试它。这些测试将为我们使用代码来控制机器人做准备。

为什么使用测试图像?

到目前为止,我们的计算机视觉工作都是直接与机器人行为一起编写的;这是它们的目标,但有时,您可能想要单独尝试视觉处理代码。

可能您想要让它工作或找出其中的错误,或者您可能想要看看您是否可以使代码更快并对其进行计时。为此,在机器人控制系统之外运行该特定代码是有意义的。

使用测试图像也是合理的。因此,您不必运行相机并需要光照条件,您可以使用您已经捕捉到的测试图像,并将它们与您从它们期望的结果进行比较。

对于性能测试,尝试同一图像100次或同一组图像将给出一致的结果,以便性能指标有意义。避免每次都使用新数据,因为这些可能会导致意外或可能是有噪声的结果。然而,添加新的测试图像以查看会发生什么是非常有趣的。

既然我们现在知道了为什么使用它们,让我们尝试捕捉一些测试图像。

捕捉测试图像

您可能还记得,在前一章中,我们使用raspistill来捕捉图像。我们在这里也将这样做。首先,我们想要将我们的相机放置到一个新的位置,朝下,这样我们就可以俯视线条。

本节需要从第第13章的设置中获取,使用Pi相机和OpenCV进行机器人视觉,以及从第9章的代码,在Python中编程RGB条带

将电机功率打开到Raspberry Pi上,然后在机器人上的Raspberry Pi上通过ssh会话,输入以下命令:

  1. 我们通过键入python3开始Python:

    pi@myrobot:~ $ python3
    Python 3.7.3 (default, Dec 20 2019, 18:57:59) 
    [GCC 8.3.0] on linux
    Type "help", "copyright", "credits" or "license" for more information.
    >>> 
    
  2. 现在,我们需要导入我们的机器人对象并创建它,以便我们可以与之交互:

    >>> import robot
    
  3. 让我们创建机器人对象:

    >>> r = robot.Robot()
    
  4. 现在,使用此方法将水平伺服机构设置为中间位置:

    >>> r.set_pan(0)
    

    水平伺服机构应该使相机居中。

  5. 接下来,我们将倾斜伺服机构设置为向下,以便观察线条:

    >>> r.set_tilt(90)
    

    伺服机构应该在这里直视下方。它不应该过度拉伸或发出咔哒声。

  6. 现在,您可以通过按Ctrl + D退出Python(并释放电机)。

    相机面向下方。现在您可以关闭电机开关,并将这个机器人放到您的测试轨道上。尽量将机器人定位,使相机正好位于线条上方。

  7. ssh终端中,键入以下内容以捕获测试图像:

    $ raspistill -o line1.jpg
    

您现在可以使用FileZilla(正如本书前面的章节所讨论的)将此图像下载到您的PC上。下一张图显示了一个测试图像,也用于前面的示例:

图14.7 – 一条线的测试图像

图14.7 展示了我的一张测试图像。请注意,线条大致从图片中间开始,但并不精确,也不需要精确。还要注意,光线有点粗糙,产生了阴影。这些值得注意,因为它们可能会使系统混淆。

从不同的角度捕捉机器人几条线的图像,稍微向左或向右偏离相机。

现在我们有了测试图像,我们可以编写代码来测试它们!

编写Python代码以找到线条的边缘

我们准备开始编写代码,使用我们的测试图像和前面的流程图。我们可以使结果非常直观,以便我们可以看到算法正在做什么。

提示

在计算机视觉中,使用最低的分辨率来完成工作是有用的。每个额外的像素都会增加更多的内存和处理能力。在320*200的分辨率下,这是76,800个像素。树莓派相机可以以1920 x 1080的分辨率记录 – 2,073,600个像素 – 这是27倍的数据!我们需要它快速,所以我们保持低分辨率。

本节中的代码将在树莓派上运行,但您也可以在安装了Python 3、NumPy、Matplotlib和Python OpenCV的PC上运行:

  1. 创建一个名为test_line_find.py的文件。

  2. 我们将需要导入NumPy以数值处理图像,OpenCV以操作图像,以及Matplotlib以绘制结果:

    import cv2
    import numpy as np
    from matplotlib import pyplot as plt
    
  3. 现在,我们加载图像。OpenCV可以加载jpg图像,但如果在加载过程中出现问题,它会产生一个空图像。因此,我们需要检查它是否加载了某些内容:

    line1.jpg and is in the same directory that we will run this file from.
    
  4. 捕获的图像将是相机的大默认分辨率。为了保持快速,我们将它调整到更小的图像:

    resized = cv2.resize(image, (320, 240))
    
  5. 我们还只想使用灰度;对于这个练习,我们不感兴趣的其他颜色:

    gray = cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY)
    
  6. 现在,我们将选择行;目前,我们将使用180,因为这在240像素高的图像上相当低。图像是存储的,使得行0是顶部。请注意,我们正在告诉NumPy将其转换为int32类型:

    int32 with a sign (plus or minus) so that our differences can be negative.
    
  7. 我们可以为这一行的每个像素获取差异列表。NumPy使这变得容易:

    diff = np.diff(row)
    
  8. 我们将要绘制这个diff列表。我们需要将x轴设置为像素编号;让我们创建一个从0到该范围的NumPy范围:

    x = np.arange(len(diff))
    
  9. 让我们绘制diff变量与像素索引(x)的关系图,并将结果保存:

    not_blurred. This is because we've not added the optional blurring step. With the graph, we'll be able to see the difference. 
    

指向我的测试图片,我得到了以下图表:

图14.8 – 无模糊差异图

图14.8中的图表以列数作为x轴,差异作为y轴。线条中有很多噪声。有两个明显的峰值 – 一个在约列145的零线以下,一个在约240的线上方。这里的噪声不会对此产生太大影响,因为峰值非常明显:

  1. 让我们尝试添加模糊以查看这如何改变事情。对代码进行以下更改。粗体区域显示更改的部分:

    gray = cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY)
    blurred = cv2.blur(gray, (5, 5))
    row = blurred[180].astype(np.int32)
    diff = np.diff(row)
    

    在此代码中,我们添加了额外的模糊步骤,模糊5x5块稍微一点。

  2. 为了我们可以看到不同的图表,让我们更改输出文件的名称:

    plt.savefig("blurred.png")
    

稍微模糊应该可以减少噪声,同时不会太多地影响我们的尖锐峰值。确实,以下图显示了这是多么有效:

图14.9 – 模糊后的diff图

图14.9中的图表与图14.8相似。坐标轴相同,并且有相同的峰值。然而,在0附近的线条周围的噪声少得多,这表明模糊使差异更清晰。关于这个问题,我认为不会改变结果太多。从峰值的位置和大小来看,我会说不会太多。因此,我们可以将其从最终跟踪中省略,以获得额外的速度。每个操作都会花费一点时间。

现在我们有了两个峰值,让我们使用它们来找到线条的位置。

从边缘定位线条

那些峰值是我们线条边缘的标记。要找到某物的中间位置,你将其左右坐标相加,然后除以2:

  1. 首先,我们必须获取坐标。让我们编写代码来请求最大值和最小值。我们将在分析代码和图表输出代码之间添加此代码:

    diff = np.diff(row)
    min_d and max_d, abbreviating the difference as d. Note that they cannot be called min and max as those names already belong to Python.
    
  2. 这些是值,但不是位置。我们现在需要找到位置索引。NumPy有一个np.where函数可以从数组中获取索引:

    where function returns an array of answers for each dimension – so, although diff is a one-dimensional array, we will still get a list of lists. The first [0] selects this first dimension's results list, and the second [0] selects the first item in the results. Multiple results mean it's found more than one peak, but we assume that there's only one for now.
    
  3. 要找到中间位置,我们需要将它们相加然后除以2:

    middle = (highest + lowest) // 2
    
  4. 现在我们已经找到了它,我们应该以某种方式显示它。我们可以在图上用三条线绘制这个图表。Matplotlib可以为图表指定颜色和样式。让我们从中间线开始:

    max_d and min_d for the Y coordinates, so the line draws from the highest peak to the lowest. The r- style specifier means to draw a solid red line.
    
  5. 我们可以为highestlowest位置做同样的事情,这次使用g--为绿色虚线:

    plt.plot([lowest, lowest], [max_d, min_d], "g--")
    plt.plot([highest, highest], [max_d, min_d], "g--")
    
  6. 就像我们对模糊处理所做的那样,让我们更改输出图的名称,以便我们可以进行比较:

    plt.savefig("located_lines.png")
    

运行此代码应输出以下图:

图14.10 – 显示最高、最低和中间线的图

图14.10中的图表显示我们已经找到了中线以及两个清晰的峰值。这段代码看起来对我们的机器人是可用的。

然而,当事情并不那么清晰时会发生什么?

尝试没有清晰线条的测试图片

让我们看看我们的线条查找代码在非常不同的测试图片上的表现。我们将看到这里会发生什么,这样我们就不那么惊讶于机器人的行为,并排除一些简单的错误。

例如,把我们的线条放在一个非常嘈杂的表面上,比如地毯上?或者没有线条的纸张,或者没有线条的地毯呢?

图片

图14.11 – 在更嘈杂条件下的差分图

通过像图14.11中的一系列图表,我们对我们系统了解了很多。

最上面的三张图片显示了原始照片。接下来的三张图表显示了在没有模糊的情况下寻找差异和中线时的图像外观。最下面的三张图表显示了启用模糊时会发生什么。

首先,当事情变得像第一张图片那样嘈杂(这已经超出了循线应该应对的范围),模糊使得找到线条和随机伪影之间的差异;尽管如此,在第二个图表中,一个具有类似向下峰值大小的随机伪影是一个接近的竞争者。在这种情况下,制作更大的Y模糊可能会平滑掉那个伪影,只留下线条。

仔细观察,那些图表的刻度尺也不相同。普通纸张图表在没有模糊的情况下测量+10/-10的峰值,而在模糊的情况下为+1/-1。因此,当差异如此之低时,我们甚至应该寻找峰值吗?地毯-only图表中的情况也类似。

我们可以对系统进行一些更改,使其将这些视为非线条。最简单的是添加一个条件,过滤掉-5以上的最小值和10以下的最大值。我说-5,因为否则会完全过滤掉第一张图中的线条。然而,更大的模糊区域可能会有所帮助。

根据条件的嘈杂程度,我们可能需要启用模糊。在光线充足的轨道上,模糊可能不是必需的。

下一个图表显示了我们的线条在地毯上,模糊设置为(5,40),进一步在行之间模糊并过滤掉更多的噪声:

图片

图14.12 – 带有更大模糊的地毯上的线条

图14.12中的图表比之前有更少的噪声,模糊处理大大平滑了噪声尖峰,而实际的线条尖峰仍然存在。我们只会在嘈杂的环境中这样做,因为它可能会使速度变慢。

如您所见,在测试图像上测试代码使我们对我们系统了解了很多。通过拍摄相同的照片并尝试不同的参数和管道更改,您可以针对不同的场景进行优化。随着您在计算机视觉方面的实验越来越多,养成这个习惯。

现在我们已经尝试在我们的测试图像上运行我们的视觉处理代码,是时候将其应用于机器人行为上了!

使用PID算法的循线

在本节中,我们将结合之前看到的视觉处理、PID控制循环和相机流,以及在第13章中看到的第13章机器人视觉 - 使用Pi相机和OpenCV。请从该章节中的代码开始。

你将需要的文件如下:

  • pid_controller.py

  • robot.py

  • servos.py

  • camera_stream.py

  • image_app_core.py

  • leds_led_shim.py

  • encoder_counter.py

  • 模板文件夹

我们将使用相同的模板来显示这个,但我们将添加一种快速而巧妙的方法,将OpenCV上的diff图渲染到输出帧上。Matplotlib对于这个来说太慢了。

创建行为流程图

在我们构建新的行为之前,创建一个数据流图将帮助我们了解处理数据后数据发生了什么。

系统看起来很熟悉,因为它与我们第13章中制作的非常相似第13章机器人视觉 - 使用Pi相机和OpenCV。看看下面的图:

图14.13 – 跟线行为

图14.13中,相机图像通过到从图像获取线块。该块输出对象的X位置(线的中间),它进入我们的PID。请注意,图像数据也从获取线到图像队列,这样你就可以在浏览器中看到这些数据。

PID控制还取一个参考中间点,即相机的中间。它使用这些之间的误差来计算偏移,并使用该偏移来驱动电机。

该图显示了带有反馈线到摄像头的电机,因为这些移动的间接影响是视图发生变化,所以我们将看到不同的线。

在此之前,我们将使我们的PID控制器变得更聪明一些。

将时间添加到我们的PID控制器

我们的机器人行为包括处理帧,然后在过程完成一个周期时将错误数据发送到PID。一个周期中有很多事情发生,时间可能会变化。当我们创建积分时,我们一直像时间恒定一样添加数据。为了得到一个更准确的图像,我们应该乘以时间:

  1. 打开pid_controller.py文件。

  2. handle_integral方法中,将参数更改为接受delta_time

        def handle_integral(self, error, delta_time):
    
  3. 然后我们将使用这个方法来添加积分项:

                self.integral_sum += error * delta_time
    
  4. 我们通常使用get_value方法来更新PID;然而,由于我们已经有使用这个方法的代码,我们应该让它对这些代码的行为保持不变。为此,我们将添加一个delta_time参数,但默认值为1

        def get_value(self, error, delta_time=1):
    
  5. 当这个get_value方法调用handle_integral时,它应该始终传递新的delta_time参数:

            p = self.handle_proportional(error)
            i = self.handle_integral(error, delta_time)
            logger.debug(f"P: {p}, I: {i:.2f}")
            return p + i
    

虽然这不是一个很大的变化,但它意味着我们可以计算PID代码更新之间的时间变化。

我们现在可以在我们的行为中使用这个方法。

编写初始行为

我们可以将所有这些元素组合起来,创建我们的循线行为:

  1. 创建一个名为line_follow_behavior.py的文件。

  2. 首先,我们需要导入image_app_core、NumPy、OpenCV、摄像头流、PID控制器和机器人。我们还有time模块,这样我们就可以稍后计算时间差:

    import time
    from image_app_core import start_server_process, get_control_instruction, put_output_image
    import cv2
    import numpy as np
    import camera_stream
    from pid_controller import PIController
    from robot import Robot
    
  3. 让我们创建行为类。构造函数,就像之前一样,接受机器人作为参数:

    class LineFollowingBehavior:
        def __init__(self, robot):
            self.robot = robot
    
  4. 现在,我们需要在构造函数中添加变量来跟踪我们的行为。首先,我们应该设置我们将要查找差异的行和阈值(低于该阈值我们不将其视为线条):

            self.check_row = 180
            self.diff_threshold = 10
    
  5. 就像我们之前的摄像头行为一样,我们有一个中心点的设定值,一个变量来表示电机是否应该运行,以及一个前进的速度:

            self.center = 160
            self.running = False
            self.speed = 60
    
  6. 我们将要创建一些有趣的显示。我们也会在这里存储我们计划使用的颜色——一个绿色的十字准线,红色用于中线,浅蓝色用于图表。这些是BGR格式,因为OpenCV期望这样:

            self.crosshair_color = [0, 255, 0]
            self.line_middle_color = [128, 128, 255]
            self.graph_color = [255, 128, 128]
    

    这就是行为构造函数的完整内容。

  7. 现在,我们需要控制变量来表示系统是否正在运行或应该退出。这段代码应该很熟悉,因为它与其他摄像头控制行为相似:

        def process_control(self):
            instruction = get_control_instruction()
            if instruction:
                command = instruction['command']
                if command == "start":
                    self.running = True
                elif command == "stop":
                    self.running = False
                if command == "exit":
                    print("Stopping")
                    exit()
    
  8. 接下来,我们将创建run方法,它将执行主要的PID循环并驱动机器人。我们将倾斜伺服器设置为90,将俯仰伺服器设置为0,这样它就会直视下方。我们也会设置摄像头:

        def run(self):
            self.robot.set_pan(0)
            self.robot.set_tilt(90)
            camera = camera_stream.setup_camera()
    
  9. 现在,我们为方向设置PID。这些值不是最终的,可能需要调整。我们有一个低比例值,因为方向误差与电机速度相比可能相当大:

            direction_pid = PIController( proportional_constant=0.4, integral_constant=0.01, windup_limit=400)
    
  10. 我们暂停一秒钟,以便摄像头可以初始化,伺服器达到其位置:

            time.sleep(1)
            self.robot.servos.stop_all()
            print("Setup Complete")
    

    我们停止伺服器,这样一旦它们达到位置,就不会再消耗更多电力。

  11. 由于我们将跟踪时间,我们在这里存储最后的时间值。时间是秒为单位的浮点数:

            last_time = time.time()
    
  12. 我们启动摄像头循环并将帧传递给process_frame方法(我们很快就会编写)。我们还可以处理控制指令:

            for frame in camera_stream.start_stream(camera):
                x, magnitude = self.process_frame(frame)
                self.process_control()
    

    从处理一个帧中,我们期望得到一个X值,幅度是差异中的最高值和最低值之间的差异。峰值之间的差距有助于检测它是否真的是线条而不是噪声。

  13. 现在,对于移动,我们需要检查机器人是否正在运行,以及我们找到的幅度是否大于阈值:

                if self.running and magnitude > self.diff_threshold:
    
  14. 如果是这样,我们开始PID行为:

    dt. This error and time delta are fed to the PID, getting a new value. So, we are ready for the next calculation: last_time now gets the new_time value.
    
  15. 我们现在记录这个信息并使用这个值来改变机器人的航向。我们将电机速度设置为基本速度,然后添加或减去电机的PID输出:

                    print(f"Error: {direction_error}, Value:{direction_value:2f}, t: {new_time}")
                    self.robot.set_left(self.speed - direction_value)
                    self.robot.set_right(self.speed + direction_value)
    
  16. 现在我们已经处理了检测到线条时会发生什么。那么,当我们没有检测到线条时呢?else会停止电机运行并重置PID,这样它就不会累积异常值:

                else:
                    self.robot.stop_motors()
                    if not self.running:
                        direction_pid.reset()
                    last_time = time.time()
    

    注意我们在这里仍然在更新最后的时间。否则,在停止和开始之间会有很大的差距,这会导致PID输入异常值。

  17. 接下来,我们需要填写处理框架时发生的事情。让我们添加我们的 process_frame 方法:

        def process_frame(self, frame):
            gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
            blur = cv2.blur(gray, (5, 5))
            row = blur[self.check_row].astype(np.int32)
            diff = np.diff(row)
            max_d = np.amax(diff, 0)
            min_d = np.amin(diff, 0)
    

    这段代码看起来都应该很熟悉;这是我们之前为测试代码编写的代码。

  18. 我们应该测试看看我们的读数是否使我们位于零线的两侧,并且我们找到了两个不同的位置。最大值不应该低于零,最小值也不应该高于它。如果它们失败了,就停止在这里——主循环将考虑这不是一条线:

            if max_d < 0 or min_d > 0:
                return 0, 0
    
  19. 我们将像之前一样找到行上的位置,以及它们的中间点:

            highest = np.where(diff == max_d)[0][0]
            lowest = np.where(diff == min_d)[0][0]
            middle = (highest + lowest) // 2
    
  20. 这样我们就可以用它来确定我们得到了一个正匹配,我们将计算最小值和最大值之间差异的幅度,确保我们没有捕捉到微弱的东西:

            mag = max_d - min_d
    
  21. 我们希望在这里向用户展示一些有用的信息。因此,这个方法调用了一个 make_display 方法,就像其他相机行为一样。我们传递一些变量到这个显示上进行绘图:

            self.make_display(frame, middle, lowest, highest, diff)
    
  22. 然后,我们返回中间点和幅度:

            return middle, mag
    
  23. 这段代码将驱动我们的机器人,但如果我们不能看到正在发生的事情,我们将很难调整它。所以,让我们创建一个 make_display 方法来处理这个问题:

    frame, the middle position for the line, the lowest difference position in the line, the highest difference position, and diff as the whole difference row.
    
  24. 我们在显示中首先想要的是中心参考。让我们在中心和选定的行上制作一个十字准线:

            cv2.line(frame, (self.center - 4, self.check_row), (self.center + 4, self.check_row), self.crosshair_color)
            cv2.line(frame, (self.center, self.check_row - 4), (self.center, self.check_row + 4), self.crosshair_color)
    
  25. 接下来,我们用另一种颜色显示我们找到的中间位置:

            cv2.line(frame, (middle, self.check_row - 8), (middle, self.check_row + 8), self.line_middle_color)
    
  26. 为了找到它,我们还围绕它绘制了 lowesthighest 的条形图,颜色不同:

            cv2.line(frame, (lowest, self.check_row - 4), (lowest, self.check_row + 4), self.line_middle_color)
            cv2.line(frame, (highest, self.check_row - 4), (highest, self.check_row + 4), self.line_middle_color)
    
  27. 现在,我们将要在一个新的空框架上绘制 diff。让我们创建一个空框架——这只是一个 NumPy 数组:

            graph_frame = np.zeros((camera_stream.size[1], camera_stream.size[0], 3), np.uint8)
    

    数组维度是行然后是列,所以我们将摄像机的 XY 值交换。

  28. 然后,我们将使用一个方法来制作一个简单的图表。我们将在下面进一步实现它。它的参数是要绘制图表的框架和图表的 Y 值。简单的图表方法暗示了 X 值为列号:

            self.make_cv2_simple_graph(graph_frame, diff)
    
  29. 现在我们有了框架和图表框架,我们需要将它们连接起来,就像我们在颜色检测代码中的框架一样:

            display_frame = np.concatenate((frame, graph_frame), axis=1)
    
  30. 我们现在可以对这些字节进行编码并将它们放入输出队列:

            encoded_bytes = camera_stream.get_encoded_bytes_for_frame(display_frame)
            put_output_image(encoded_bytes)
    
  31. 我们接下来需要实现的是这个 make_cv2_simple_graph 方法。它有点厚颜无耻,但在 x 轴上绘制了 Y 点之间的线条:

        def make_cv2_simple_graph(self, frame, data):
    
  32. 我们需要存储我们最后所在的位置,这样代码就可以相对于这个位置绘制下一个值,从而得到一个线图。我们从项目 0 开始。我们还为图表设置了一个稍微任意的中间 Y 点。记住,我们知道 diff 值可以是负数:

            last = data[0]
            graph_middle = 100
    
  33. 接下来,我们应该枚举要绘制的数据,以便绘制每个项目:

            for x, item in enumerate(data):
    
  34. 现在,我们可以从上一个项目 Y 位置绘制到下一个 X 位置的当前位置的线条。注意我们是如何通过图表中间偏移每个项目的:

                cv2.line(frame, (x, last + graph_middle), (x + 1, item + graph_middle), self.graph_color)
    
  35. 然后我们需要更新最后一个项目到当前这个:

                last = item
    

    好了——几乎完成了;这将把图表绘制在我们的框架上。

  36. 我们的行为已经完成;我们只需要外部代码来运行它!这段代码也应该类似于之前的相机示例:

    color_track_behavior.html template here.
    

您现在可以将其上传到您的机器人。然后,打开电机并运行它。因为这是基于网络的,请将浏览器指向 http://myrobot.local:5001

您应该看到以下内容:

图 14.14 – 跟随线条行为输出的截图

图 14.14 中的截图显示了上方两张图片的标题。在左边是线条的相机图片。在这个框架上画了一个绿色的十字线,显示了中心点位置。还有一个大红色的条形,显示了线条的中间,以及这个条形两侧的两个较短的红色条形,显示了线条的两侧。在右边是绘制模糊后强度差异的图表。图表中可见上峰值和下峰值。

在此之下是 开始停止退出 按钮。

将机器人放置在直线上,光线充足。如果看起来像前面的显示,请按 开始 按钮以查看它运行。它应该开始不稳定地沿着线条行驶。

调整 PID

在尝试跟踪曲线线条并找到其极限时,您可以稍微大胆一些。机器人有时会超调或欠调,这就是 PID 调节发挥作用的地方:

  • 如果它似乎转得太慢,尝试稍微增加比例常数。相反,如果它过度转向,尝试稍微降低比例常数。

  • 如果存在轻微的连续误差,尝试增加积分常数。

PID 调节是一个重复的过程,需要大量的耐心和测试。

故障排除

如果行为不太正常,请尝试以下步骤:

  • 如果将倾斜伺服设置为 90 度时看起来不垂直向下,可能没有正确校准。将 deflect_90_in_ms 值参数更改为 Servos 对象 – 以 0.1 的增量增加,以使其达到 90 度。

  • 如果它难以获得清晰的线条,请确保光线充足,它所在的表面是平坦的,例如纸张,并且线条对比度良好。

  • 如果它仍然难以找到线条,以 5 为增量增加垂直模糊量。

  • 如果它难以按时转向直线,尝试以 10 为增量降低速度。

  • 如果您发现相机在水平方向上晃动,可以从 line_follow_behavior 中移除 self.robot.servos.stop_all() 这一行。注意:这会以牺牲电机电池寿命为代价。

  • 如果机器人发现太多其他随机垃圾而不是线条,尝试增加垂直模糊度。此外,尝试以 1 或 2 的步长增加阈值。亮度对比度越尖锐,您需要做的就越少。

  • 确保您已经双重检查了代码,并且您已经在这里以及从 第 13 章机器人视觉 – 使用 Pi 相机和 OpenCV 中找到了之前的例子。

再次找到线条

需要考虑的一个重要问题是,如果机器人失去了线条,它应该做什么。回到我们工业环境的例子,这可能是一个安全措施。

我们当前的机器人会停止。这需要你将其放回线路。然而,当你这样做时,机器人会立即开始移动。这种行为对我们的小型机器人来说很好,但对于大型机器人来说可能是一个危险隐患。

你可以考虑的另一种行为是旋转,直到机器人再次找到线路。丢失线路可能是由于机器人不足/过度转向而偏离线路且无法再次找到它,或者可能是由于机器人已经超过了线路的末端。这种行为可能适合小型机器人竞赛。

我们需要仔细考虑这类事情以及你会在哪里使用这个机器人。请注意,对于竞赛型机器人或工业机器人,它们将具有多个不同角度的传感器或更宽角度的传感器——因此,它们不太可能像我们这样丢失线路。此外,对于更大的机器人来说,即使缓慢旋转也可能非常危险。因此,让我们实现一个简单的附加安全特性。

当它找不到线路时,它不会只是停止电机;它将运行标志设置为false,因此你需要手动再次启动它:

  1. 再次打开line_follow_behavior.py文件。

  2. 前往run方法并找到else:语句。

  3. 现在,我们可以修改这个声明的内容:

                else:
                    self.robot.stop_motors()
    running is false, we now set running to False every time. We also reset the PID every time.
    

将代码保存到机器人中,并运行它直到它丢失线路。这可能是因为偏离了路线或到达了线路的末端。机器人应该停止。它应该在尝试再次移动之前等待你按下启动按钮。请注意,你需要将其放回线路并按下启动按钮才能再次移动。

这个机器人现在处理丢失线路条件更加可预测。

概述

在本章中,你看到了如何使用摄像头检测线路以及如何绘制显示它所发现的数据的图表。然后你看到了如何将数据放入驾驶行为中,以便机器人跟随线路。你增加了你的OpenCV知识,并且我向你展示了一种将图表放入摄像头流输出渲染的帧中的巧妙方法。你看到了如何调整PID以使线路跟随更准确,以及如何确保当机器人丢失线路时它能够可预测地停止。

在下一章中,我们将看到如何通过语音代理Mycroft与我们的机器人进行通信。你将向树莓派添加麦克风和扬声器,然后添加语音识别软件。这将使我们能够向树莓派发出语音命令以发送到机器人,而Mycroft将响应以让我们知道它所做的一切。

练习

现在我们已经使这个系统工作,我们可以通过以下方式增强系统并使其更有趣:

  • 你能否在make_display方法中使用cv2.putText在帧上绘制如PID数据之类的值?

  • 考虑将PID和误差数据与时间写入文件,然后将其加载到另一个Python文件中,使用Matplotlib显示发生了什么。这种变化可能会使回顾时的不足/过度转向更加清晰。

  • 你可以修改电机处理代码,当线条靠近中间时加速,远离中间时减速。

  • 一个重要的改进是检查两行并找出它们之间的角度。这样你就知道线条离中间有多远,同时你也知道线条的方向,可以利用这一点来进一步引导你的转向。

这些练习应该会给你一些有趣的玩法和实验你在这章中学到的构建和知识的方法。

进一步阅读

以下内容可以帮助你进一步了解跟随线条:

第十八章:第 15 章:使用 Mycroft 与机器人进行语音通信

使用我们的语音请求机器人做某事并接收语音响应长期以来被视为智能的标志。我们周围的设备,如使用 Alexa 和 Google Assistant 的设备,都有这些工具。能够编程我们的系统以集成这些工具,使我们能够访问强大的语音助手系统。Mycroft 是一个基于 Python 的开源语音系统。我们将通过将其连接到扬声器和麦克风来在树莓派上运行它,然后我们将根据我们说的词语在机器人上运行指令。

在本章中,我们将概述 Mycroft,然后学习如何将扬声器/麦克风板添加到树莓派上。然后我们将安装和配置树莓派以运行 Mycroft。

我们还将扩展我们对 Flask 编程的使用,构建具有更多控制点的 Flask API。

在本章的末尾,我们将创建我们自己的技能代码以将语音助手连接到我们的机器人。你将能够利用这些知识来创建更多的语音代理技能。

本章涵盖了以下主题:

  • 介绍 Mycroft – 理解语音代理术语

  • 在机器人上监听语音的限制

  • 如何将扬声器/麦克风板添加到树莓派上

  • 如何安装和配置树莓派以运行 Mycroft

  • 编程 Flask 控制API

  • 如何创建我们自己的技能代码以将语音助手连接到我们的机器人

技术要求

为了本章,你需要以下硬件:

  • 另一个 Raspberry Pi 4(型号 B)。

  • 一张 SD 卡(至少 8 GB)。

  • 一台可以写入卡的 PC(带有 balenaEtcher 软件)。

  • ReSpeaker 2-Mics Pi HAT。

  • Mini Audio Magnet Raspberry Pi Speaker——一个带有 JST 连接器的微型扬声器或带有 3.5 毫米插孔的扬声器。

  • 拥有一条 Micro-HDMI 到 HDMI 的电缆可能有助于故障排除。

  • Micro USB 电源。

  • 上一章中的机器人(毕竟,我们打算让它动起来)。

本章的代码可在 GitHub 上找到:https://github.com/PacktPublishing/Learn-Robotics-Programming-Second-Edition/tree/master/chapter15

查看以下视频以查看代码的实际应用:https://bit.ly/2N5bXqr

介绍 Mycroft – 理解语音代理术语

Mycroft 是一个名为 语音助手 的软件套件。Mycroft 监听语音命令并根据这些命令采取行动。Mycroft 代码是用 Python 编写的,是开源和免费的。它的大部分语音处理都在云端进行。在命令处理完毕后,Mycroft 将使用语音对人类进行回应。

Mycroft 在线有文档和用户社区。在尝试了 Mycroft 之后,你可以考虑其他替代方案——例如 Jasper、Melissa-AI 和 Google Assistant。

那么,语音助手的概念是什么呢?让我们在以下小节中看看它们。

语音转文本

语音转文本STT)描述的是将包含人类语音的音频转换为计算机可以处理的单词序列的系统。

这些可以在本地运行,也可以在更强大的机器上云上运行。

唤醒词

语音助手通常有一个唤醒词——一个在命令其余部分之前说出的短语或单词,以引起语音助手的注意。例如,有“嘿,Siri”,“嗨,Google”和“Alexa”的表达。Mycroft使用单词“Mycroft”或短语“嘿,Mycroft”,但这可以更改。

语音助手通常只监听唤醒词,直到被唤醒才会忽略所有其他音频输入。唤醒词在设备上本地识别。唤醒词之后的样本声音被发送到语音转文本系统进行识别。

表达

表达是用户说的东西的术语。语音助手使用你定义的词汇来匹配表达与技能。特定的词汇将导致Mycroft调用意图处理器。

Mycroft中的词汇包括一个文件中可互换短语的列表。

一个好的例子是询问Mycroft关于天气:“嘿,Mycroft,天气怎么样?”

意图

意图是语音助手可以执行的任务,例如找出今天的天气。我们将构建意图与我们的机器人交互。意图是技能的一部分,定义了执行它的处理代码并选择一个对话框来响应。

以天气技能为例,表达“天气怎么样?”会触发获取配置位置当前天气的意图,然后向用户说出这些细节。对于我们机器人来说,一个例子是“让机器人测试LED”,这个意图会在机器人上启动LED彩虹行为(来自第9章在Python中编程RGB条带)。

对话

在Mycroft术语中,对话是Mycroft对用户说的短语。例如,“好的,机器人已经启动了”,或者“今天,天气晴朗”。

技能包含一系列对话。这些对话有可以说的同义词组,并且可以使用不同的语言。

词汇

你说的表达,一旦转换为文本,就会与词汇相匹配。词汇文件,就像对话一样,是意图的一部分,将表达与动作相匹配。词汇文件包含同义词组,并且可以组织成语言集,使你的技能多语言。

这将使像“天气怎么样?”,“天气晴朗吗?”,“我需要伞吗?”或“会下雨吗?”这样的短语具有同义性。你可以将某些内容分开——例如,“让机器人做”作为一个词汇项,而“向前行驶”作为另一个。

技能

技能是用于话语、对话和意图的整个词汇表的容器。一个用于闹钟的技能可能包含设置闹钟、列出闹钟、删除闹钟或更改闹钟的意图。它将包含一个用于说闹钟设置完成或确认每个闹钟的对话。

在本章的后面部分,我们将构建一个带有使机器人移动和停止意图的MyRobot技能。

现在你已经了解了一些关于语音代理术语和部件的知识。接下来,我们需要考虑我们将要构建什么。扬声器和麦克风将放在哪里?

在机器人上监听语音的限制

在我们开始构建这个之前,我们应该考虑我们将要制作什么。扬声器和麦克风应该放在机器人上还是其他地方?处理是本地还是云端?

这里有一些需要考虑的事项:

  • 噪音:带有电机的机器人是一个嘈杂的环境。如果麦克风靠近电机,它几乎就毫无用处。

  • 电源:语音助手一直在监听。机器人已经有很多传感器在运行,对电源的需求已经很大。这种电源需求既包括电池功率,也包括CPU功率。

  • 尺寸和物理位置:扬声器和语音HAT将增加机器人高度和布线复杂性,而这个机器人本身已经非常繁忙。

一个麦克风和扬声器的组合可以放在大型机器人的茎上——一个高支架上,那里还有一个第二个树莓派。但这对这个小而简单的机器人来说不合适。我们将创建一个独立的语音助手板,它将与我们的机器人通信,但我们不会直接将其放在机器人上。语音助手将是一个第二个树莓派。

我们还将使用一个发送到云端进行语音处理系统。虽然一个完全本地的系统会有更好的隐私性并且可以更快地响应,但在撰写本文时,还没有一个完整的打包语音助手适用于树莓派。Mycroft软件让我们在使用自己的技能时具有灵活性,并且有一个可插拔的后端用于语音处理,因此有一天它可能可以在本地运行。

现在我们已经选择了如何使用Mycroft和第二个树莓派来制作我们的语音代理,现在是时候开始构建它了。

为树莓派添加声音输入和输出

在我们能够使用语音处理/语音助手之前,我们需要给树莓派一些扬声器和麦克风。一些树莓派附加组件可以提供这些设备。我的推荐是带有麦克风阵列(以提高识别率)和扬声器连接的ReSpeaker 2-Mics Pi HAT,它非常容易获得。

下一张照片显示了ReSpeaker 2-Mics Pi HAT:

图15.1 – ReSpeaker 2-Mics Pi HAT

图 15.1 展示了 ReSpeaker 2-Mics Pi HAT 安装在 Raspberry Pi 上的照片。在左侧,我标记了左侧麦克风。该帽子有两个麦克风,每个侧面都有两个微小的矩形金属部件。下一个标签是 3 个 RGB LED 和一个连接到 GPIO 引脚的按钮。接下来是连接扬声器的两种方式 – 3.5mm 插孔或 JST 连接器。我建议您连接一个扬声器来听从这个 HAT 的输出。然后,最后一个标签突出了右侧麦克风。

我选择 ReSpeaker 2-Mic Pi HAT,因为它是一个便宜的设备,可以开始语音识别。非常便宜的 USB 麦克风对此不起作用。在 Mycroft 中有更好的支持的昂贵设备,但它们不会像帽子一样放在 Pi 上。这个 ReSpeaker 2-Mics Pi HAT 是一种权衡 – 对于硬件简单性和成本来说很好,但需要更多的软件设置。现在让我们看看如何物理安装这个 HAT。

硬件安装

ReSpeaker 2-Mics HAT 将直接安装在 Raspberry Pi 4 头部,板子悬在 Pi 上方。

扬声器将有一个微小的两针连接器(JST)类型,适合板上的单个两针插座,或者一个 3.5 mm 插孔。下一张照片显示了扬声器插入到其中:

图 15.2 – Mycroft 语音助手 ReSpeaker 设置

图 15.2 展示了我的 Mycroft 设置,ReSpeaker 2-Mics Pi HAT 已设置在我的桌子上。它已通电,Raspberry Pi 已点亮。我还连接了一个扬声器。

您可以使用 Raspberry Pi 外壳或项目箱,但确保麦克风没有被遮挡。

您还需要一个 SD 卡和一个电源供应器。

重要提示

在接下来的几节中,我建议使用市电电源。不要插上电源并启动它。

现在我们已经准备好了硬件,它配备了扬声器和麦克风。在下一节中,我们将设置 Raspbian 和语音代理软件。

在 Raspberry Pi 上安装语音代理

Mycroft 为此准备了 Raspbian 发行版。让我们将其放入 SD 卡中:

  1. 前往 Mycroft 网站,下载 Picroft 镜像:https://mycroft-ai.gitbook.io/docs/using-mycroft-ai/get-mycroft/picroft – 该镜像基于 Raspbian Buster。选择 稳定磁盘镜像

  2. 将 SD 卡插入您的计算机。使用 第 3 章 中的程序,探索 Raspberry Pi在 balenaEtcher 中闪存卡 部分。务必选择 Picroft 镜像而不是 Raspbian。

  3. 确保此镜像可以无头运行,启用 SSH 和 Wi-Fi,就像我们在 第 4 章 中所做的那样,在 为机器人准备无头 Raspberry Pi在 Raspberry Pi 上设置无线并启用 SSH 部分中。

准备好这张 SD 卡后,是时候尝试使用了。将其插入语音助手 Raspberry Pi,并使用 ReSpeaker 2-Mics Pi HAT 上的 USB 微型插座(而不是 Pi)供电。

重要提示

确保您通过 ReSpeaker 2-Mics Pi HAT 提供电源,而不是 Pi。该板需要电源来驱动其扬声器。该板的文档建议,如果您通过 Pi 为其供电,则扬声器不会输出声音。有关详细信息,请参阅 https://wiki.seeedstudio.com/ReSpeaker_2_Mics_Pi_HAT/#hardware-overview

其主机名最初为 picroft.local。您使用用户名 pi 和密码 mycroft。确保它已连接到 Wi-Fi,并且您可以通过 SSH(PuTTY)访问它。启动 Raspberry Pi 后,您就可以开始设置 Mycroft。

安装 ReSpeaker 软件

当您登录时,Mycroft 将显示安装指南。这将向您列出的问题进行提问:

  1. 当询问是否要进行引导设置时,按 Y 表示是。Mycroft 安装将下载大量更新。留出 30 分钟到 1 小时的时间来完成。

  2. Mycroft 现在将询问您的音频输出设备:

    3, to select USB speakers, which sets some basic defaults.
    
  3. Ctrl + C 退出引导设置并返回到 $ 提示符。

  4. 为了使安装生效,我们需要更新 SD 卡上的软件。在提示符下,输入 sudo apt update -y && sudo apt upgrade -y。更新可能需要一些时间。

  5. 使用 sudo reboot 重启 Pi 以使更新生效。重启 Pi 后,使用 ssh 登录。您将再次处于引导设置中。再次按 Ctrl + C

  6. 使用以下命令安装 ReSpeaker 2-Mics Pi HAT 的音频驱动程序:

    $ git clone https://github.com/waveshare/WM8960-Audio-HAT.git
    $ cd WM8960-Audio-HAT
    $ sudo ./install.sh
    

    Git 克隆可能需要一分钟左右。该板使用 WM8960 音频芯片。安装脚本将花费 20-30 分钟完成。

  7. 再次重启。在退出引导模式后按 Ctrl + C

    在我们继续之前,测试我们是否真的得到了音频是一个好主意。

  8. 输入 aplay -l 以列出播放设备。在输出中,您应该看到以下内容:

    card 1: wm8960soundcard [wm8960-soundcard], device 0: bcm2835-i2s-wm8960-hifi wm8960-hifi-0 [bcm2835-i2s-wm8960-hifi wm8960-hifi-0]
    

    这表明它已经找到了我们的卡。

  9. 我们现在可以通过让这张卡播放一个音频文件来测试它是否能够播放音频。使用以下命令:aplay -Dplayback /usr/share/sounds/alsa/Front_Left.wav

    此命令指定了带有设备 -D 标志的名为 playback 的设备,然后是要播放的文件。playback 设备是一个默认的 ALSA 处理器,它确保了混音的完成,并避免了比特率和通道数不匹配的问题。在 /usr/share/sounds/alsa 中还有其他测试音频文件。

  10. 然后,我们可以使用 arecord -l 命令检查录音设备。在以下输出中,我们可以看到 arecord 已经找到了这张卡:

card 1: wm8960soundcard [wm8960-soundcard], device 0: bcm2835-i2s-wm8960-hifi wm8960-hifi-0 [bcm2835-i2s-wm8960-hifi wm8960-hifi-0]

该卡现在已准备好使用。接下来,我们需要向 Mycroft 系统展示如何选择这张卡进行使用。

故障排除

如果您没有音频输出,有一些事情您可以检查:

  1. 首先,输入sudo poweroff来关闭Raspberry Pi。当它关闭时,检查连接。确保电路板已完全连接到Pi的GPIO引脚。确保你已经将扬声器连接到ReSpeaker 2-Mics Pi HAT的正确端口。

  2. 当你再次开机时,确保你使用的是ReSpeaker 2-Mics Pi HAT上的电源连接器,而不是Raspberry Pi。

  3. 如果你使用的是耳机插槽而不是扬声器插槽,你可能需要增加音量。输入alsamixer,选择WM8960声卡,并将耳机音量调高。然后再次尝试播放测试。

  4. 确保你已经执行了apt updateapt upgrade步骤。没有这些步骤,驱动程序的安装将无法进行。你需要在此之后重新启动,然后尝试重新安装驱动程序。

  5. 在安装驱动程序时,如果Git步骤失败,请仔细检查你获取的地址。

  6. 在尝试播放时,-D标志是区分大小写的。小写的d在这里不起作用。

如果这些步骤仍然没有帮助,请访问https://github.com/waveshare/WM8960-Audio-HAT网站,阅读他们的文档,或者提出一个问题。

现在我们已经检查了这一点,让我们尝试将声卡与Mycroft链接。

让Mycroft与声卡通信

现在你需要将Mycroft和声卡连接起来。通过编辑Mycroft配置文件来完成此操作:

  1. 使用sudo nano /etc/mycroft/mycroft.conf以root权限打开Mycroft配置文件。

  2. 文件中有描述Mycroft各个方面的行。然而,我们只对其中两行感兴趣:

    aplay command on device hardware 0,0 (the Pi headphone jack) – written as hw:0,0. This will be the wrong device. The second specifies it will play mp3 files using the mpg123 command and on the same incorrect device. Using a direct hardware device may make assumptions about the format of the sound being played, so it needs to go through the mixer device. Let's fix these.
    
  3. 将两个hw:0,0的出现更改为术语playback。这两行应该看起来像这样:

       "play_wav_cmdline": "aplay -Dplayback %1",
       "play_mp3_cmdline": "mpg123 -a playback %1",
    
  4. Ctrl + X来保存并退出。当被询问是否保存文件时,输入Y表示是。

  5. 再次重启;当你回来时,不要退出指导模式。

  6. Mycroft会要求测试设备。按T来测试扬声器。可能需要几秒钟,但你将听到Mycroft对你说话。如果声音有点小,尝试输入数字9,然后再次测试。这是一个激动人心的时刻!按D表示你已经完成了测试。

  7. 指导安装程序接下来会询问麦克风。选择4代表其他USB麦克风并尝试声音测试。安装程序会要求你对着麦克风说话,并且应该会回放你的声音。如果听起来不错,请按1

  8. 指导安装程序会询问你是否使用推荐设置;选择1以确认你想要使用它。将有一系列关于密码设置的问题。我建议不要添加sudo密码,而是将Pi的默认密码改为一个独特的密码。

  9. Mycroft将以大量紫色安装文本启动。

你已经配置并启动了Mycroft。它可以记录你的声音并回放给你听,你也已经听到了它的测试词。现在,是时候开始使用Mycroft并看看它能做什么了。

开始使用Mycroft

让我们稍微了解一下Mycroft,然后尝试与它交谈。我们将从调试界面、Mycroft客户端开始,它显示了系统正在发生的事情,然后我们将开始与它交谈。

Mycroft客户端

当你连接到Mycroft时,你会看到如下所示的显示界面:

图15.3 – Mycroft客户端界面

图15.3中的截图是Mycroft客户端。它允许你看到Mycroft正在做什么,但你不需要连接到这个客户端,Mycroft才能听到你的声音。右上角显示了有多少消息以及你能看到多少。在截图上,你可以看到0-10条消息,总共10条消息。

主要的中间部分显示消息。红色紫色消息来自Mycroft系统和插件。如果许多紫色消息快速闪过,说明Mycroft正在安装插件和更新,你可能需要等到它完成后再操作。绿色消息显示Mycroft与用户的交互。它显示当它检测到唤醒词、开始录音、结束录音以及它认为你所说的表述。如果它没有完全响应,你可以检查它是否正在拾取唤醒词以及表述是否与你试图说的相符。

在此之下,左侧是历史记录。在历史记录中,Mycroft从你的表述中处理的内容以蓝色显示。Mycroft所说的对话以黄色显示。你应该在扬声器上听到黄色文本的重复;然而,如果它非常忙碌,这可能需要一段时间。在右侧,它显示一个与日志文件颜色匹配的图例。更右边是一个麦克风扬声器电平计,除非Mycroft正在忙碌,或者你非常安静,否则你应该看到它在上下移动,因为它正在拾取房间内的噪音。注意 – 噪音太多,你可能会有困难与它交谈。

屏幕底部是一个输入区域,你可以在这里为Mycroft输入命令。

给系统大约30-40分钟的时间来完成所有安装。如果它没有响应,并不是卡住了,通常是在安装和编译额外的组件。

然后,Mycroft会告诉你需要在mycroft.ai进行配对。你需要使用它给出的代码来注册设备;这可以在Mycroft安装时完成。你需要在那里创建一个账户(或者如果这是一个第二设备/尝试,则登录)。请在继续之前完成此操作。

当你配对好Mycroft,并且它完成安装后,你就可以开始交互了。

与Mycroft交谈

现在你应该能够与你的语音助手交谈了:

  1. 首先,为了引起它的注意,你必须使用唤醒词Hey Mycroft。如果它准备好了(并且没有还在忙碌),它将发出一个扬声器音调来显示Mycroft正在倾听。你需要站在大约一米的Raspberry Pi麦克风附近。它可能会回应说请稍等,我正在完成启动。给它一分钟,然后再次尝试。

  2. 如果你听到声音,你现在可以要求它做某事。一个好的起点是告诉它:说你好。大约10秒后,Mycroft 应该会通过扬声器回应你好。你需要尽可能清晰地说话。我发现它需要你逐个发音;那些tn的音是必不可少的。

现在既然它已经工作,你可以用它来玩一些有趣的事情!你可以将嘿 Mycroft缩短为Mycroft。你还可以说的其他事情包括以下内容:

  • 嘿 Mycroft,天气怎么样?:这将使用天气技能并告诉你天气情况。可能不是你所在位置的天气;请使用 mycroft.ai 网站配置你的设备以适应你的位置。

  • Mycroft,23 乘以 76 是多少:这将使用 Wolfram 技能,它可以处理数学问题。

  • Mycroft,wiki banana:这将使用维基百科技能,Mycroft 将告诉你它关于香蕉的发现。

尝试这些来习惯与 Mycroft 交谈,以便它做出响应。它可能会说我不明白,日志会告诉你它听到了什么,这可以帮助你尝试调整发音以便于它理解。

我们现在可以创建一个技能,将 Mycroft 连接到我们的机器人。但首先,让我们检查是否有问题。

故障排除

如果无法让 Mycroft 说话或识别说话,请尝试以下方法:

  • 确保你离麦克风足够近或声音足够大。这可以通过观察麦克风(麦克风)水平是否超过 Mycroft 控制台中的虚线来检查。

  • 确保你的 Raspberry Pi 有良好的网络连接。Mycroft 只能在你可以访问互联网的地方工作。请参阅 Mycroft 文档了解如何处理代理。如果互联网连接不佳,Mycroft 可能无法正确启动。修复连接并重新启动可能会有所帮助。

  • 在 Pi 启动时连接监视器可能会显示错误消息。

  • Mycroft 有一个以 故障排除和已知错误 开始的故障排除系统(https://mycroft.ai/documentation/troubleshooting/)。

  • Mycroft 正在积极开发中。使用最新的 Picroft 图像并应用 ReSpeaker 驱动程序可能会有所帮助。简而言之,安装和运行此软件可能会发生变化。

在 Mycroft 说话并做出响应的情况下,我们需要为 Mycroft 准备机器人以便与之交谈。

编程 Flask API

本章旨在使用 Mycroft 控制我们的机器人。为此,我们需要为我们的机器人提供一种接收来自其他系统命令的方式。服务器上的 应用程序编程接口API)允许我们将此类系统解耦,以便通过网络向另一个系统发送命令并接收响应。Flask 系统非常适合构建此类系统。

基于网络的API有端点,其他系统会向其发送请求,并大致映射到Python模块中的函数或方法。正如你将看到的,我们将我们的API端点直接映射到Python robot_modes 模块中的函数。

在我们开始构建之前,让我们看看这个系统的设计——它还将揭示 Mycroft 的工作方式。

Mycroft 控制机器人的概述

下面的图示展示了用户如何通过 Mycroft 控制机器人:

图15.4 – 机器人技能概述

图15.4 中的图示展示了数据在这个系统中的流动:

  1. 在左侧,它从用户对 Mycroft 发出指令开始。

  2. 在识别唤醒词后,Mycroft 将声音发送到 Google STT 引擎。

  3. Google STT 返回文本,一个语音输入,Mycroft 会将其与技能/意图中的词汇进行匹配。我们稍后会更深入地探讨这些内容。

  4. 这将在机器人技能中触发意图,我们将构建这个技能。机器人技能将向机器人右侧的 Raspberry Pi 发送请求,作为对 Flask 控制API(网络)服务器的请求。

  5. 那个控制 API 服务器将启动机器人进程,并响应表示已完成。

  6. 机器人技能将选择对话来说明它已完成,并将此信息发送给 Mycroft。

  7. Mycroft 将然后向用户说出这个响应。

在这一点上,我们将在机器人上构建 Flask 服务器。你之前在视觉处理章节中已经见过 Flask,并且已经安装了这个库。

远程启动行为

我们将使用 HTTP 和一个网络服务器来完成这项工作,因为向其发送请求很简单,因此我们可以构建其他远程控制机器人的方法。HTTP 在 URL 中发送请求——首先,http:// 协议标识符;服务器主机名,myrobot.local;路径,/mode/foo;之后可能还有额外的参数。我们使用 URL 的路径来确定机器人做什么。

就像我们处理其他系统一样,我们创建了一些逻辑部分和块来处理这个系统的不同方面:

  • 管理机器人模式和启动/停止已知脚本的代码。它还可以给我们一个已知脚本的列表。

  • 一个网络服务器来处理网络请求。

我们首先需要构建模式管理器。

管理机器人模式

我们可以通过启动和停止我们的行为脚本作为子进程来管理模式。让我们创建一个配置来告诉模式管理器关于模式的信息。这个配置将模式名称映射到一个文件——一个 Python 文件。请注意,我们指定了一个文件列表,而不是推断它。尽管我们可以将模式/路径部分添加 .py 来获取一个文件,但这有两个原因是不好的:

  • 它将直接与脚本名称耦合;如果我们可以更改相同模式名称的底层脚本,那将很棒。

  • 尽管机器人不是一个安全的环境,允许任意子进程运行是非常糟糕的;限制它可以使机器人更加安全。

让我们开始构建它:

  1. 创建一个名为 robot_modes.py 的文件。这个文件包含一个名为 RobotModes 的类,用于处理机器人进程。

  2. 文件以一些导入和一些类定义的顶部开始:

    import subprocess
    class RobotModes(object):
    
  3. 接下来,我们创建一些模式映射,将模式名称映射到文件名:

        mode_config = {
            "avoid_behavior": "avoid_with_rainbows.py",
            "circle_head": "circle_pan_tilt_behavior.py",
            "test_rainbow": "test_rainbow.py"
        }
    

    模式名称是一个简短名称,也称为 slug,是可读性和机器可读性之间的折衷方案 – 它们通常仅限于小写和下划线字符,并且比完整的英文描述要短。我们的文件名已经相对接近 slug 名称。

  4. 在固定配置之外,这个类还在管理作为进程的运行行为。它一次只能运行一个。因此,我们需要一个成员变量来跟踪当前进程并检查它是否正在运行:

        def __init__(self):
            self.current_process = None
    
  5. 我们应该能够检查是否有东西已经在运行或已完成:

    subprocess is a way of running other processes and apps from within Python. We check whether we have a current process, and if so, whether it is still running. Processes have a return code, usually to say whether they completed or failed. However, if they are still running, it will be None. We can use this to determine that the robot is currently running a process.
    
  6. 下一个函数是运行一个进程。函数参数包括一个模式名称。该函数检查进程是否正在运行,如果不是,则启动一个进程:

    self.mode_config to map mode_name to a script name. We then use subprocess to start this script with Python. Popen creates a process, and the code stores a handle for it in self.current_process. This method returns True if we started it, and False if one was already running.
    
  7. 该类需要一种方法来请求它停止一个进程。请注意,当进程未运行时,它不会尝试停止进程。当我们停止脚本时,我们可以使用 Unix 信号,这允许我们以允许它们运行 atexit 代码的方式请求它们停止。它发送 SIGINT 信号,这是 Ctrl + C 键盘组合的等效信号:

        def stop(self):
            if self.is_running():
                self.current_process.send_signal( subprocess.signal.SIGINT)
                self.current_process = None
    

在我们发出信号后,我们将当前进程设置为 None – 丢弃句柄。

我们现在有了启动和停止进程的代码,它还将名称映射到脚本。我们需要将其封装在一个语音代理可以使用的网络服务中。

编程 Flask 控制API服务器

我们之前使用 Flask 创建了我们的视觉处理行为的网络服务器。这次我们将用它来做一些更简单的事情。

正如我们在图像服务器中的启动和停止按钮所看到的,Flask 允许我们为链接设置处理程序以执行任务。让我们编写一个脚本,作为我们的控制网络服务,它使用 Flask 和我们的 RobotModes 对象。

让我们按照以下步骤构建它:

  1. 创建一个名为 control_server.py 的脚本。我们可以从导入 Flask 和我们的机器人模式开始:

    from flask import Flask
    from robot_modes import RobotModes
    
  2. 现在,我们创建一个 Flask 应用程序来包含路由和之前创建的 RobotModes 类的实例:

    app = Flask(__name__)
    mode_manager = RobotModes()
    
  3. 接下来,我们需要一个路由,或 API 端点,来运行应用程序。它将模式名称作为路由的一部分:

    @app.route("/run/<mode_name>", methods=['POST'])
    def run(mode_name):
        mode_manager.run(mode_name)
        return "%s running"
    

    我们返回一个运行确认。

  4. 我们还需要另一个 API 端点来停止正在运行的过程:

    @app.route("/stop", methods=['POST'])
    def stop():
        mode_manager.stop()
        return "stopped"
    
  5. 最后,我们需要启动服务器:

    http for a web (hypertext) service. This is followed by a colon (:) and then two slashes // with a hostname or host address—the network address of the Raspberry Pi the resource will be on. As a host can have many services running, we can then have a port number, with a colon as a separator—in our case, :5000. After this, you could add a slash / then select a specific resource in the service.We can test this now:
    
  6. 启动机器人并复制 control_server.pyrobot_modes.py 文件到其中。

  7. 通过 SSH 连接到机器人,并使用 python3 control_server.py 启动控制服务器。你应该看到以下内容:

    $ python3 control_server.py
     * Serving Flask app "control_server" (lazy loading)
     * Environment: production
       WARNING: Do not use the development server in a production environment.
       Use a production WSGI server instead.
     * Debug mode: on
     * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
    
  8. 现在创建另一个 ssh 窗口进入 Mycroft Raspberry Pi – 我们可以测试它是否与另一个窗口通信。在 pi@picroft.local 中按一次 Ctrl + C 以进入 Linux 命令行($ 提示符)。

  9. curl 命令在像 Raspberry Pi 这样的 Linux 系统上经常被用来测试此类服务器。它向网络服务器发送请求,发送/接收数据,并显示结果。它非常适合测试此类 HTTP 控制API。

    我们打算发送一个 post 请求。输入以下命令:

    curl -X POST http://myrobot.local:5000/run/test_rainbow
    

    这应该会启动彩虹灯的开关,使用来自第9章在Python中编程RGB灯带的代码。curl命令指定我们使用POST方法发送请求,然后是一个包含端口号、机器人主机名、指令run和模式名称的URL。

  10. 你可以用curl -X POST http://myrobot.local:5000/stop停止LED灯。这个URL有stop指令。机器人的LED彩虹灯应该会停止。

    注意这两个URL都是以http://myrobot.local:5000/开头的。这个地址可能因你的主机名而异。这是这个控制服务器的基准URL。

  11. 你可以按Ctrl + C来停止它。

我们可以用这个来构建我们的 Mycroft 行为,但在继续之前,让我们检查一下是否有任何问题。

故障排除

如果这对你不起作用,我们可以检查一些事情来看看发生了什么:

  • 如果你收到任何语法错误,请检查你的代码并再次尝试。

  • 请验证你的机器人和你正在测试的设备都有互联网连接。

  • 注意,当我们启动子进程时,我们正在启动Python 3。如果没有3,可能会发生一些意外的事情。

  • 首先,记住控制服务器正在机器人的Raspberry Pi 3A+上运行。你需要在curl命令中用你的机器人地址替换它。

  • 确保你已经安装了Flask,如第13章机器人视觉 – 使用Pi摄像头和OpenCV中所示。

  • 确保你已经将控制服务器和机器人模式脚本复制到了机器人上。你还需要在机器人上安装来自第9章在Python中编程RGB灯带的代码来运行这个测试。

现在我们已经测试了控制服务器,你可以关闭Pi的电源。还有一些代码要写!让我们将其与Mycroft连接起来。

在Raspberry Pi上使用Mycroft编程语音代理

Flask控制系统提供的机器人后端足够我们创建Mycroft技能。

图15.4中,你看到在你说出唤醒词后,Mycroft会将你发出的声音传输到Google STT系统。Google STT随后会返回文本。

Mycroft会将这些与你在该地区使用的词汇文件进行匹配,并将其与技能中设置的意图相匹配。一旦匹配,Mycroft将在技能中调用一个意图。我们的机器人技能有意图,会向为我们机器人创建的Flask控制服务器发送网络(HTTP)请求。当Flask服务器响应表示它已处理请求(可能是行为已启动)时,机器人技能将选择一个对话框对用户说话,以确认它已成功执行请求或发现问题。

我们将从简单的技能开始,有一个基本的意图,然后你可以扩展它以执行更多操作。我选择了彩虹 LED 测试(来自 第 9 章在 Python 中编程 RGB 条带),因为它很简单。

值得注意的是,Google 处理语音所需的时间意味着这不适合紧急停止机器人;语音识别可能需要一些时间。你可以在意图中使用 GPIOZerowhen_pressed 处理器来触发控制服务器的停止处理器。

构建意图

我们可以从意图开始,然后看看一些词汇。为了构建它,我们将使用 Mycroft 内置的名为 adapt 的库:

  1. 创建一个名为 my-robot-skill 的文件夹,我们将在此文件夹中构建 Mycroft 技能。

  2. 主要意图文件将是此文件夹中的 __init__.py 文件。此文件名意味着 Python 将将整个文件夹视为一个 Python 库,称为 my-robot-skill/__init__.py

    IntentBuilder to build and define intents around vocabulary. MycroftSkill is a base class to plug our code into Mycroft. intent_handler marks which parts of our code are intents, associating the code with IntentBuilder. We import LOG to write information out to the Mycroft console and see problems there.The last import, `requests`, is a tool to talk to our control server in Python remotely.
    
  3. 接下来,我们将从 MycroftSkill 基类定义我们的技能。它需要设置其父类并准备设置:

    super calls a method from a class we've made our base; in this case, __init__ so we can let it set things up.The only setting we have is a `base_url` member for our control server on the robot. It is consulting a settings file, which we'll see later. It's usually a good idea to separate the configuration from the code.
    
  4. 下一步我们需要定义一个意图。我们通过一个 handle_test_rainbow 方法来实现,但你需要使用 @intent_handler 装饰器。在 Python 中,装饰器会将方法包装在进一步的处理中——在这种情况下,使其适合 Mycroft:

    intent_handler decorator takes some parameters to configure the vocabulary we will use. We will define vocabulary in files later. We require a vocabulary matching *robot* first, then another part matching *TestRainbow* – which could match a few phrases.
    
  5. 下一步,这个技能应该使用 requests.post 向机器人发送请求:

    base_url variable, plus the run instruction and the test_rainbow mode.
    
  6. 我们需要 Mycroft 说出一些话,表明它已经告诉机器人在这里做些什么:

    speak_dialog method tells Mycroft to pick something to say from dialog files, which allows it to have variations on things to say.
    
  7. 此请求可能因几个原因而失败,因此在代码片段的最后之前有 try。我们需要一个 except 来处理这种情况,并为用户说话一个对话框。我们还将 LOG 一个异常到 Mycroft 控制台:

    Unable to reach the robot, while not inspecting the result code from the server other than if the voice skill contacted the robot.
    
  8. 此文件需要提供一个 create_skill 函数,该函数位于类外部,Mycroft 预期在技能文件中找到:

    def create_skill():
        return MyRobot()
    

代码是这个系统的一部分,但在使用它之前我们需要配置它。

设置文件

我们的意图首先加载一个设置。我们将将其放在 my-robot-skill/settingsmeta.json 中,并定义我们的控制服务器的基准 URL。

如果你的机器人树莓派的域名/地址不同,请使用该域名/地址。此文件对于这个设置来说有点长,但意味着你可以稍后配置 URL:

{
    "skillMetadata": {
        "sections": [
            {
                "name": "Robot",
                "fields": [
                    {
                        "name": "base_url",
                        "type": "text",
                        "label": "Base URL for the robot control server",
                        "value": "http://myrobot.local:5000"
                    }
                ]
            }
        ]
    } 
}

我们已经设置了要使用的基准 URL,但我们需要配置 Mycroft 以加载我们的技能。

要求文件

我们的技能使用 requests 库。当 Mycroft 遇到我们的技能时,我们应该告诉它期待这个。在 Python 中,要求文件是这样做的一种标准方式。将以下内容放入 my-robot-skill/requirements.txt

requests

此文件并非仅限于 Mycroft,它还与许多 Python 系统一起使用,以安装应用程序所需的库。

现在,我们需要告诉 Mycroft 它需要监听什么,即词汇。

创建词汇文件

要定义词汇,我们需要定义词汇文件。你需要将它们放入一个遵循格式my-robot-skill/vocab/<IETF语言和区域设置>的文件夹中。语言/区域设置意味着我们应该能够为变体定义词汇,例如en-us代表美式英语和zn-cn代表简体中文;然而,在撰写本文时,en-us是最受支持的Mycroft语言。社区的一部分正在努力支持其他语言。

你使用一个或多个与词汇文件匹配的词汇部分来定义每个意图。词汇文件有代表预期话语方式的行。这允许人类自然地变化他们说话的方式,当机器无法以稍微不同的方式请求某事时,人们会注意到这一点。在为词汇文件想出类似短语时,有一些技巧。

我们需要为我们的意图定义两个词汇文件——一个用于robot同义词,另一个用于TestRainbow同义词:

  1. my-robot-skill下创建vocab文件夹,然后在其中创建en-us文件夹。

  2. 在那里创建一个路径和名称为my-robot-skill/vocab/en-us/robot.voc的文件。

  3. 添加一些用于请求机器人做某事的短语:

    robot in the intent handler.
    
  4. 让我们创建用于测试彩虹的词汇。将其放入my-robot-skill/vocab/en-us/TestRainbow.voc

    test rainbow
    test the leds
    deploy rainbows
    turn on the lights
    

    重要提示

    注意,词汇文件名的首字母大小写必须与意图构建器匹配;我随后使用了将非共享词汇部分首字母大写的惯例。

在测试这个时,你不可避免地会尝试说出一个听起来合理的短语,但这个短语并不在那里。Mycroft会告诉你“抱歉,我不明白”,然后你会在上面的词汇表中添加另一个表达。

对话文件

我们还想要定义Mycroft会对你说的短语。到目前为止,我们有三个短语是我们意图所需的。这些短语将放入与词汇文件类似结构的my-robot-skill/dialog/en-us文件夹中。让我们来构建它们:

  1. my-robot-skill下创建dialog文件夹,然后在其中创建en-us文件夹。

  2. 在该文件夹中创建路径为my-robot-skill/dialog/en-us/Robot.dialog的文件。我们可以在其中添加一些短语:

    The Robot
    Robot
    
  3. 下一个对话我们需要的是同一文件夹中的TestRainbow.dialog

    is testing rainbows.
    is deploying rainbows.
    is starting rainbows.
    is lighting up.
    
  4. 由于我们有一个错误处理器,我们也应该创建UnableToReach.dialog

    Sorry I cannot reach the robot.
    The robot is unreachable.
    Have you turned the robot on?
    Is the control server running on the robot?
    

通过定义多个可能的对话,Mycroft会随机选择一个,使其不那么重复。我们已经看到了如何制作词汇短语和对话短语。让我们简要回顾一下我们应该有什么。

当前技能文件夹

我们的技术文件夹应该看起来像以下截图:

图片

图15.5 – 机器人技能文件夹截图

图 15.5 中,我们看到一个截图显示了名为 my-robot-skill 的文件夹中的技能。这个技能文件夹包含 dialog 文件夹,其中包含 en-us 子文件夹和这里的三个对话文件。下面是 vocab 文件夹,包含 en-us 文件夹和两个词汇文件。在 vocab 文件夹下面,我们有 __init__.py 文件,它定义了意图、Mycroft 安装它的要求和一个设置文件。哇 - 我们在这里创建了很多东西,但这一切都将是值得的!

我们现在需要将整个文件夹结构上传到我们的机器人:

  1. 使用 SFTP(FileZilla),将此文件夹上传到你的 Mycroft Pi,在 /opt/mycroft/skills 文件夹中。

  2. Mycroft 将自动加载此技能;你将看到紫色文本在安装过程中闪烁。

  3. 如果你需要更新代码,再次上传文件到这个位置将导致 Mycroft 重新加载它。

    加载或使用技能时遇到的问题将在 Mycroft 输出中显示。你还可以在 /var/log/mycroft/skills.log 中找到结果——less Linux 工具对于查看此类日志输出很有用,使用 Shift + G 跳到文件末尾或输入 /myrobot 跳到其输出。

    你也可以使用 tail -f /var/log/mycroft/skills.log 来查看问题发生时的状态。使用 Ctrl + C 来停止。

  4. 现在,打开机器人的电源,使用 ssh 登录,并使用 python3 control_server.py 启动控制服务器。

  5. 然后,你可以尝试使用 Mycroft 测试你的技能:告诉机器人打开灯光

  6. Mycroft 应该发出哔哔声以向用户显示它已唤醒,一旦它从语音到文本转换中获取了单词,它将向机器人上的控制服务器发送 /run/test_rainbow。你应该听到 Mycroft 说出其中一个对话短语,例如 机器人正在测试彩虹,并看到 LED 灯亮起。

故障排除

如果你遇到意图响应的问题,请尝试以下操作:

  • 首先,检查之前 Python 代码的语法和缩进。

  • 确保你的机器人和语音助手 Raspberry Pi 在同一网络中;我发现一些 Wi-Fi 扩展器存在这个问题,需要使用 IP 地址而不是 myrobot.local。使用 settingsmeta.json 文件来配置此设置。

  • 确保你已经将整个结构 - 包括 vocabdialogsettingsmeta.json__init__.py - 复制到语音助手 Raspberry Pi 的 /opt/mycroft/skills 文件夹中。

  • 如果你的设置不正确,你需要在 https://account.mycroft.ai/skills 页面上更改它们。查找 My Robot 技能并在此处更改。你需要保存更改,可能需要重新启动 Mycroft 或等待几分钟以使更改生效。

  • 确保你与 Mycroft 对话的方式与你的词汇文件相匹配 - 否则它将无法识别你的单词。

  • 如果你遇到它识别你的声音的问题,你也可以在 Mycroft 控制台中输入短语。

我们的第一种意图已经生效!你能够对语音助手说话,并且它已经指导机器人执行操作。然而,我们现在开始闪烁 LED,唯一停止它们的方法是使用那个不方便的 curl 命令。我们可能需要通过添加另一个意图来修复这个问题。

添加另一个意图

现在我们有了自己的技能,为它添加一个停止的第二个意图变得相对容易,使用机器人控制服务器中的另一个端点。

词汇和对话

我们需要添加词汇和对话,以便我们的新意图能够理解我们所说的话,并且有一些话可以回应:

  1. 我们需要创建 stop 词汇;我们可以将其放在 my-robot-skill/vocab/en-us/stop.voc 中:

    stop
    cease
    turn off
    stand down
    
  2. 我们需要为 Mycroft 创建一个对话文件,以便在 my-robot-skill/dialog/en-us/stopping.dialog 中告诉机器人它正在停止:

    is stopping.
    will stop.
    

这些就足够了,但如果你想到了更多的同义词,也可以添加。

添加代码

现在我们需要将意图代码添加到我们的技能中:

  1. 我们将此放入 my-robot-skill/__init__.py 中的 MyRobot 类:

    stop vocabulary, the handler name (which could be anything – but must not be the same as another handler), and the URL endpoint. Identical code like that is ripe for refactoring. Refactoring is changing the appearance of code without affecting what it does. This is useful for dealing with common/repeating code sections or improving how readable code is. Both the intents have the same try/catch and similar dialog with some small differences. 
    
  2. 在同一文件中,添加以下内容:

    end_point as a parameter and uses that in its request. It takes a dialog_verb parameter to say after the Robot bit. All of the other dialog and error handling we saw before is here. 
    
  3. 这两个意图现在变得更为简单。将它们更改为以下内容:

        @intent_handler(IntentBuilder("")
                        .require("Robot")
                        .require("TestRainbow"))
        def handle_test_rainbow(self, message):
            self.handle_control('/run/test_rainbow', 'TestingRainbow')
        @intent_handler(IntentBuilder("")
                        .require("Robot")
                        .require("stop"))
        def handle_stop(self, message):
            self.handle_control('/stop', 'stopping')
    

添加新的意图现在更容易,因为我们能够重用 handle_control

使用新意图运行

你现在可以再次上传文件夹结构——因为 vocabdialog__init__ 文件已经更改。当你这样做时,请注意 Mycroft 将自动重新加载更改后的技能(或显示尝试这样做时出现的任何问题),因此它立即可以投入使用。

通过说出 Mycroft,告诉机器人停止 来尝试一下。

你现在已向系统中添加了第二个意图,定义了更多的词汇和对话。你也已经重构了这段代码,因为看到了一些重复。现在,你的机器人已经开始具备语音控制功能。

摘要

在本章中,你学习了语音助手术语、语音转文本、唤醒词、意图、技能、话语、词汇和对话。你考虑了麦克风和扬声器的安装位置,以及它们是否应该安装在机器人上。

你随后看到了如何将扬声器/麦克风组合物理安装在 Raspberry Pi 上,然后准备软件以便 Pi 使用它。你安装了 Picroft – 一个 Mycroft Raspbian 环境,获取了语音代理软件。

你随后能够与 Mycroft 互动,让它响应不同的语音命令,并将其与基础注册。

你还看到了如何使机器人准备好接受外部代理,例如使用 Flask API 的语音代理来控制它。你能够创建多个与机器人通信的技能,为创建更多技能提供了良好的起点。

在下一章中,我们将回顾在第 12 章第 12 章中引入的 IMU,使用 Python 进行 IMU 编程,并让它做更多有趣的事情——我们将平滑和校准传感器,然后将它们组合起来为机器人获取航向,编程机器人始终朝北转。

练习

尝试这些练习以充分利用本章内容并扩展你的经验:

  • 尝试从Mycroft网站安装一些其他Mycroft技能并与之互动。提示:说“Hey Mycroft,install pokemon”。

  • 机器人模式系统有一个缺陷;它假设你要求停止的过程确实停止了。它应该等待并检查返回代码以查看是否已停止吗?

  • 实现机器人模式的另一种方法可能是更新所有行为以干净地退出,这样你就可以导入它们而不是在子进程中运行。这会多么棘手?

  • 在测试交互时,你是否觉得词汇量不足?也许你可以用更自然的短语来开始不同的行为。同样,你也可以使对话更有趣。

  • 向技能添加更多意图,例如,避障。你可以添加一个停止意图,尽管响应时间可能会使这不太理想。

  • ReSpeaker 2-Mics Pi HAT上的RGB LED可以使用吗?项目https://github.com/respeaker/mic_hat有一个LED演示。

有这些想法,有足够的空间来进一步探索这个概念。进一步的阅读也会有所帮助。

进一步阅读

请参阅以下信息以获取更多信息:

  • 《树莓派机器人项目》理查德·格里姆特博士Packt Publishing,有一章介绍语音输入和输出。

  • 《语音用户界面项目》亨利·李Packt Publishing,专注于系统的语音界面。它展示了如何使用Alexa和Google Home语音代理构建聊天机器人和应用程序。

  • 《Mycroft AI – 语音栈介绍》 – Mycroft AI的一份白皮书提供了更多关于Mycroft栈如何工作及其组件的详细信息。

  • Mycroft有一个庞大的社区支持并讨论这项技术,请参阅https://community.mycroft.ai/。我建议咨询这个社区的故障排除信息。Mycroft正在积极开发中,既有许多怪癖,也有许多新功能。它也是一个分享你为它构建的技能的绝佳地方。

  • 查看https://github.com/respeaker/seeed-voicecard以获取Seeed Studio,ReSpeaker 2-Mics Pi HAT的创造者,提供的关于此设备的文档和代码,以及更大型的四麦克风和六麦克风版本。

第十九章:第16章: 使用IMU深入探索

第12章使用Python进行IMU编程中,我们读取了来自惯性测量单元IMU)的数据。我们现在对处理传感器数据、使用数学和管道进行决策有了更多的了解。

在本章中,我们将学习如何从IMU获取校准数据,结合传感器数据,并使用这些数据使机器人具有基于绝对方向的行性行为。在这个过程中,我们将看到用于提高精度/速度或准确性的算法。

到本章结束时,你将能够检测机器人的绝对方向,并在屏幕上显示,并将其与比例-积分-微分PID)行为相结合。

本章我们将涵盖以下主要内容:

  • 编程虚拟机器人

  • 使用陀螺仪检测旋转

  • 使用加速度计检测俯仰和滚转

  • 使用磁力计检测航向

  • 从磁力计获取大致航向

  • 结合传感器进行方向检测

  • 使用IMU数据驱动机器人

技术要求

对于本章,你需要以下物品:

本章的完整代码请访问https://github.com/PacktPublishing/Learn-Robotics-Programming-Second-Edition/tree/master/chapter16

查看以下视频,了解代码的实际应用:https://bit.ly/2LztwOO

编程虚拟机器人

我们首先将检测我们的机器人方向;将其显示为3D机器人模型将很有用。这部分基于第12章中的表示坐标和旋转系统部分,使用Python进行IMU编程。在本节中,我们将使用VPython构建我们机器人的简单模型。

在VPython中建模机器人

我们将使用称为 原语 的形状来模拟机器人。它们具有位置、旋转、大小和颜色。高度和宽度参数与 VPython 世界坐标系匹配(参见 第 12 章 中的 图 12.14 – 机器人身体坐标系使用 Python 进行 IMU 编程),因此我们必须旋转物体以匹配机器人身体坐标系。

首先,我们需要收集一些机器人测量值。以下图表显示了它们的位置。一旦完成主要测量,可以使用估计值进行较小测量:

图 16.1 – 虚拟机器人的测量值

图 16.1 展示了机器人的测量值。俯视图和左视图展示了覆盖不同方面的内容。这包括底座的宽度和高度——注意,我们在此目的将其视为矩形。需要测量或猜测车轮的大小和位置,以及万向轮的大小和位置。为你的机器人测量或猜测这些值。对我们来说,猜测就足够了。位置来自底盘的中间。

让我们编写代码来创建基本形状,如下所示:

  1. 创建一个名为 virtual_robot.py 的文件,并添加 vpython 导入和我们的机器人视图来启动它,如下所示:

    import vpython as vp
    from robot_pose import robot_view
    
  2. 我们将虚拟机器人放入一个函数中,以便在几种不同的行为中使用,如下所示:

    def make_robot():
    
  3. 我们将 图 16.1 中的机器人测量值放入变量中。我使用了 毫米mm)来表示所有这些值。代码如下所示:

        chassis_width = 155
        chassis_thickness = 3
        chassis_length = 200
        wheel_thickness = 26
        wheel_diameter = 70
        axle_x = 30
        axle_z = -20
        castor_position = vp.vector(-80, -6, -30)
        castor_radius = 14
        castor_thickness = 12
    
  4. 底座是一个默认位置为 (0, 0, 0) 的盒子。代码如下所示:

        base = vp.box(length=chassis_length,
                      height=chassis_thickness,
                      width=chassis_width)
    
  5. 将此盒子绕 x 轴旋转 90 度以匹配身体坐标系,将 z 轴向上,如下所示:

    base.rotate(angle=vp.radians(90), 
                    axis=vp.vector(1, 0, 0))
    
  6. 我们将使用两个圆柱体作为车轮。每个车轮到中间的距离大约是底盘宽度的一半。让我们用它来创建车轮的 y 位置,如下所示:

       wheel_dist = chassis_width/2
    
  7. 我们设置车轮位置以与电机轴的末端对齐。左轮有一个 y 坐标;-wheel_dist 将其移动到平台左侧,如下代码片段所示:

    cylinder axis says which way it is pointing. We set *y* to *-1* to point it left.
    
  8. 现在,我们设置右轮,wheel_dist 为正值,y 轴为 1,以便它指向右侧,如下代码片段所示:

        wheel_r = vp.cylinder(radius=wheel_diameter/2,
              length=wheel_thickness,
              pos=vp.vector(axle_x, wheel_dist, axle_z),
              axis=vp.vector(0, 1, 0))
    
  9. 我已经使用了一个圆柱体作为后轮的万向轮,如下代码片段所示:

        castor = vp.cylinder(radius=castor_radius,
              length=castor_thickness,
              pos=castor_position,
              axis=vp.vector(0, 1, 0))
    
  10. 现在,我们将所有这些部分组合成一个复合对象——一个单一的 3D 对象,如下所示:

        return vp.compound([base, wheel_l, wheel_r, castor])
    
  11. 为了测试它,让我们创建一个微小的 main 部分。此代码检查你是否直接启动了它,因此以下代码在将虚拟机器人作为库导入时不会运行:

    if __name__ == "__main__":
    
  12. 设置视图,将摄像机放置在机器人前方,如下所示:

        robot_view()
    
  13. 我们将添加轴来显示物品的位置,如下所示:

        x_arrow = vp.arrow(axis=vp.vector(200, 0, 0), color=vp.color.red)
        y_arrow = vp.arrow(axis=vp.vector(0, 200, 0), color=vp.color.green)
        z_arrow = vp.arrow(axis=vp.vector(0, 0, 200), color=vp.color.blue)
    
  14. 然后,我们将绘制机器人,如下所示:

        make_robot()
    
  15. 使用 vpython virtual_robot.py 上传并测试此代码。

  16. 打开浏览器,查看您机器人上的端口 9020 以查看您的虚拟机器人。您应该看到以下类似的图像!图片

    图16.2 – VPython 3D虚拟机器人的截图

    图16.2 中,我们可以看到红色表示的 x 轴向前,绿色表示的 y 轴向右,蓝色表示的 z 轴向上。这遵循右手定则坐标系。它显示了从前方看到的虚拟机器人,两侧各有一个轮子。它是灰色、方形的,但对我们剩余的实验来说足够了。

  17. 您可以右键单击并拖动它以获得另一个视角。鼠标滚轮也可以放大或缩小。以下截图显示了后轮:

图片

图16.3 – 虚拟机器人的不同视角

图16.3 展示了该虚拟机器人的左侧视角。

关闭浏览器标签页,然后按 Ctrl + C 完成此程序。让我们检查一下你是否能跟上。

故障排除

如果您没有让这个工作,让我们检查以下几点:

  1. 如果您收到显示 没有这样的模块 vpython 的错误,请确保您已遵循 第12章 中的 简化 VPython 命令行 部分,使用 Python 进行 IMU 编程,在 读取温度 部分。您需要 第12章 中的整个代码,使用 Python 进行 IMU 编程,以便本章工作。

  2. 如果您收到显示 没有这样的命令 vpython 的错误,请确保您已遵循 第12章 中的 简化 VPython 命令行 部分。VPython 的别名对于能够看到显示是必要的。

  3. 如果您看到语法错误,请检查您的代码是否有误。

  4. 如果您无法到达显示屏(并且已经检查了 步骤1),请确保您在机器人上使用端口 9020(我的机器人是 http://myrobot.local:9020)。

  5. 请耐心等待——VPython 启动可能需要一分钟或两分钟。

现在我们有一个可以玩耍的视觉机器人,我们可以重新审视陀螺仪,并尝试让屏幕上的机器人像我们的真实机器人一样移动。

使用陀螺仪检测旋转

我们已经从陀螺仪获得了一些原始数据,但为了更有效地使用它,我们需要执行两个操作,校准陀螺仪,然后将其积分,如下图中所示:

图片

图16.4 – 陀螺仪数据流

图16.4 展示了数据流,我们将在本节稍后更详细地探讨这些概念。第一个操作显示在顶部,显示了陀螺仪数据经过偏移校准以去除误差。这给我们一个校准后的陀螺仪,以每秒度数的变化率(每轴)显示——由圆圈周围的箭头表示。陀螺仪进行相对测量。

图表的下半部分是第二个操作,将时间差与校准后的陀螺仪(gyro)结合。我们需要积分这个值以找到绝对测量值。积分器将输入值乘以时间差并添加到先前结果中。在这种情况下,我们将陀螺仪速率乘以时间差以产生该期间的移动(由框中的乘法符号表示)。上面的圆圈有一个小扇形,用虚线表示移动的量。

我们将移动量加到该轴的最后一个值上,由加号符号框表示。上面的圆圈显示了一个实心灰色扇形表示现有位置,以及一个带有虚线的新的扇形段。当它们相加时,它们构成了该轴的总值——由一个带有大实心灰色扇形的圆圈表示,表示加法的结果。系统将俯仰、滚转或偏航结果反馈到下一个循环。

在我们这样做之前,我们需要纠正陀螺仪的错误。

校准陀螺仪

由于它们是从工厂出来的,微机电系统MEMS)陀螺仪通常有一些微小的缺陷,导致它们给出略微不准确的读数。这些缺陷将导致我们的积分出现漂移。

我们可以编写代码来检测这些并补偿;我们称之为校准。按照以下步骤进行:

  1. 创建一个名为calibrate_gyro.py的文件。

  2. 我们需要VPython来处理向量,time来稍微休息一下,以及设置IMU,如下面的代码片段所示:

    from robot_imu import RobotImu
    import time
    import vpython as vp
    imu = RobotImu()
    
  3. 我们需要向量来保存陀螺仪的最小和最大值,如下面的代码片段所示:

    gyro_min = vp.vector(0, 0, 0)
    gyro_max = vp.vector(0, 0, 0)
    
  4. 现在,对于循环,我们将在一段时间内进行多次读数,如下所示:

    for n in range(500):
        gyro = imu.read_gyroscope()
    
  5. 为了校准,我们需要测量一段时间以获取每个轴的最小和最大值。Python的min函数返回它给出的两个值中的较小值,如下所示:

        gyro_min.x = min(gyro_min.x, gyro.x)
        gyro_min.y = min(gyro_min.y, gyro.y)
        gyro_min.z = min(gyro_min.z, gyro.z)
    
  6. 我们对最大值也做同样的处理,使用Python的max函数,如下所示:

        gyro_max.x = max(gyro_max.x, gyro.x)
        gyro_max.y = max(gyro_max.y, gyro.y)
        gyro_max.z = max(gyro_max.z, gyro.z)
    
  7. 这些中间部分是我们距离零的估计值。我们可以通过将向量相加然后除以2来计算这个值,如下所示:

        offset = (gyro_min + gyro_max) / 2
    
  8. 在下一个循环之前稍微休息一下,如下所示:

        time.sleep(.01)
    
  9. 我们打印这些值以便使用,如下所示:

    print(f"Zero offset: {offset}.")
    
  10. 此代码已准备好运行。使用Python 3上传并运行此代码,直到程序退出之前,请确保机器人仍然位于平坦、稳定的表面上。

  11. 你应该看到控制台输出以类似以下内容结束:

    pi@myrobot:~ $ python3 calibrate_gyro.py
    Zero offset: <-0.583969, 0.675573, -0.530534>.
    

我们在这里测量的是当静止时陀螺仪平均变化了多少。这被计算为每个轴的偏移量。通过从测量值中减去这个值,我们将主要抵消陀螺仪的连续误差。让我们把它放在我们可以使用的地方,如下所示:

  1. 创建一个名为imu_settings.py的文件。

  2. 我们将导入vector类型,然后设置我们的校准读数。你可能只需要运行一次,或者如果你更换了IMU设备。请使用从你的机器人获得的读数。运行以下代码:

    from vpython import vector
    gyro_offsets = vector(-0.583969, 0.675573, -0.530534)
    
  3. 接下来,我们将我们的RobotImu类升级以处理这些偏移量——打开robot_imu.py

  4. 如果我们传递偏移量,我们的类将接受它们,或者如果我们不传递它们,则使用零。对RobotImu__init__方法进行以下突出显示的更改:

        def __init__(self, gyro_offsets=None):
            self._imu = ICM20948()
            self.gyro_offsets = gyro_offsets or vector(0, 0, 0)
    
  5. 我们需要修改read_gyroscope方法来考虑这些偏移量,如下所示:

        def read_gyroscope(self):
            _, _, _, gyro_x, gyro_y, gyro_z = self._imu.read_accelerometer_gyro_data()
            return vector(x, y, z) - self.gyro_offsets
    

现在,为了看看这行不行,让我们用它来移动一个虚拟机器人。

使用陀螺仪旋转虚拟机器人

我们提到了如何将陀螺仪测量值进行积分。看看以下图表,看看这是如何对单个轴起作用的:

图片

图16.5 – 整合陀螺仪轴

图16.5显示了一个虚线圆圈,表示轴的旋转圆。穿过圆的十字线表示其中心。圆圈上方和左边的粗箭头表示当前的航向。阴影区域表示一段时间内旋转的变化(以度为单位),我们将这些变化加到当前航向上,以得到新的航向估计——另一个粗箭头。

我们将旋转速率乘以时间以获得移动;这是一个估计,因为我们没有中间值。

时间自上次测量以来的概念是一个重要的概念,在第14章中可以看到,即“Python中用摄像头进行路径跟踪”。它更常被称为时间差。

我们可以将我们对陀螺仪的了解与虚拟机器人结合起来,使其在屏幕上旋转。让我们用这个来旋转我们的虚拟机器人,如下所示:

  1. 创建一个名为visual_gyroscope.py的新文件。我们在这里有很多导入,以便将组件组合在一起,如下面的代码片段所示:

    import vpython as vp
    from robot_imu import RobotImu
    import time
    import imu_settings
    import virtual_robot
    
  2. 这次,当我们设置RobotImu时,我们将使用我们设置的设置,如下所示:

    imu = RobotImu(gyro_offsets=imu_settings.gyro_offsets)
    
  3. 我们将要整合三个轴:俯仰角横滚角偏航角。让我们从零开始,如下所示:

    pitch = 0
    roll = 0
    yaw = 0
    
  4. 我们现在应该设置虚拟机器人和它的视图,如下所示:

    model = virtual_robot.make_robot()
    virtual_robot.robot_view()
    
  5. 我们将要跟踪时间差,所以我们首先获取最新的时间,如下所示:

    latest = time.time()
    
  6. 然后我们开始这个行为的主体循环。由于这是在VPython中动画化,我们需要设置循环速率并告诉它更新,如下所示:

    while True:
        vp.rate(1000)
    
  7. 我们现在计算时间差(dt),存储最新的时间,如下所示:

        current = time.time()
        dt = current - latest
        latest = current
    
  8. 代码读取gyro向量中的陀螺仪,如下所示:

        gyro = imu.read_gyroscope()
    
  9. 我们将当前速率(每秒度数)乘以dt(秒)进行积分,如下面的代码片段所示:

        roll += gyro.x * dt
        pitch += gyro.y * dt
        yaw += gyro.z * dt
    
  10. 我们将模型的朝向重置为准备旋转,如下所示:

        model.up = vp.vector(0, 1, 0)
        model.axis = vp.vector(1, 0, 0)
    
  11. 我们通过每个轴进行旋转。我们必须将这些转换为弧度,如下所示:

    1, 0, 0).
    
  12. 这段代码现在可以运行了;这将使虚拟机器人在现实世界中旋转时改变位置!上传文件并使用vpython visual_gyroscope.py运行。

  13. 如前所述,等待一分钟或更长时间,并将您的浏览器指向myrobot.local:9020。您应该看到以下显示:图片

    图16.6 – 旋转的机器人

    图16.6显示了通过移动真实机器人使虚拟机器人旋转到一定角度。移动一下你的机器人——尽量接近你在这里看到的样子。

  14. 你会注意到,当你移动机器人并将其放回原位时,它将不再正确对齐——这是陀螺仪积分造成的累积误差或漂移。

在这个实验中,虽然看到了一些很好的运动,但你同时也注意到仅使用陀螺仪无法准确跟踪旋转。我们将需要利用IMU设备中的其他传感器来提高这一点。

在继续之前,让我们检查它是否正常工作。

故障排除

如果它还没有完全工作,尝试以下步骤:

  1. 此代码需要使用vpython命令和浏览器来查看结果。

  2. 如果机器人静止时仍在移动,请重试校准和偏移。陀螺仪的特性是这不会完美——我们将在后面修复它。

  3. 如果机器人看起来无法控制地旋转或跳跃,请确保你已经记得将其转换为弧度。

  4. 如果机器人旋转的方向不正确(左右而不是上下),请检查旋转轴的参数。

现在你已经使它工作,让我们继续到加速度计,这样我们就可以看到作用在我们机器人上的力了!

使用加速度计检测俯仰和滚转

第12章,“使用Python进行IMU编程”中,我们得到了加速度计的矢量,但我们需要计算角度以考虑与陀螺仪和磁力计一起使用。为了使用这个来旋转物体,我们需要将这个矢量转换为俯仰和滚转角度。

从加速度计矢量获取俯仰和滚转

加速度计描述了在笛卡尔坐标系中发生的事情。我们需要将这些转换为相互垂直的一对俯仰和滚转角度。在第12章,“使用Python进行IMU编程”中,坐标和旋转系统部分显示滚转发生在x轴周围,俯仰发生在y轴周围。

考虑这种方法的粗略但有效的方式是将其视为两个平面。当围绕x轴旋转时,你可以在yz平面取一个矢量并找到其角度。当围绕y轴旋转时,则考虑xz平面。看看下一个图:

图16.7 – 加速度计矢量和角度

图16.7中,背景有xyz轴以及一个球体,周围有围绕xzyz平面的圆圈。

加速度计矢量为A。仅使用xz分量,我们将此矢量投影到点C处的xz圆上;因此,从z轴到C的角度是俯仰。我们再次将A投影到点B处的yz圆上;从z轴到B的角度是滚转。

当我们在一个平面上有两个组件(例如 xz)时,它们可以用在 atan2 函数(存在于大多数编程语言中)中以获取一个角度。这里的一个小问题是不同传感器组件的朝向意味着我们必须取俯仰的相反数。以下图表显示了这个过程:

图16.8 – 加速度计数据流

图16.8 显示了原始加速度计数据进入反正切函数以获取角度,并输出加速度计俯仰/滚转值。

让我们把加速度计读数转换成俯仰和滚转,然后放入图表中,如下所示:

  1. 首先,打开 robot_imu.py

  2. 将导入扩展到包括三角函数,如下所示:

    from vpython import vector, degrees, atan2
    
  3. read_accelerometer 方法之后,添加以下代码以执行所需的数学运算:

        def read_accelerometer_pitch_and_roll(self):
            accel = self.read_accelerometer()
            pitch = degrees(-atan2(accel.x, accel.z))
            roll = degrees(atan2(accel.y, accel.z))
            return pitch, roll
    
  4. 让我们在图表上显示这些角度,这将同时揭示使用加速度计时存在的重大缺陷。创建一个 plot_pitch_and_roll.py 文件。

  5. 从导入开始,如下所示:

    import vpython as vp
    import time
    from robot_imu import RobotImu
    imu = RobotImu()
    
  6. 我们创建图表,如下所示:

    vp.graph(xmin=0, xmax=60, scroll=True)
    graph_pitch = vp.gcurve(color=vp.color.red)
    graph_roll = vp.gcurve(color=vp.color.green)
    
  7. 现在,我们设置一个起始时间,以便我们可以创建一个基于时间的图表,如下所示:

    start = time.time()
    while True:
        vp.rate(100)
        elapsed = time.time() - start
    
  8. 我们现在可以获取新的俯仰和滚转值,如下所示:

        pitch, roll = imu.read_accelerometer_pitch_and_roll()
    
  9. 然后,我们可以将这两组数据放入图表中,如下所示:

        graph_pitch.plot(elapsed, pitch)
        graph_roll.plot(elapsed, roll)
    
  10. 上传 robot_imu.pyplot_pitch_and_roll.py 文件。使用 vpython plot_accel_pitch_and_roll.py 运行,并将浏览器指向机器人的端口 9020。这应该会产生以下结果:

图16.9 – 加速度计俯仰和滚转图

图16.9 显示了图表的截图。图表中的红色曲线代表俯仰,围绕 y 轴,而绿色曲线代表滚转,围绕 x 轴。虽然它显示了+90和-90度之间的摆动,但也很明显的是,图表有很多噪声,以至于不到一秒的移动都被它抹去了。

我们需要清理一下。一个常见的做法是通过互补滤波器,将新值与旧值结合以过滤掉快速振动噪声。我们将创建这样的滤波器,但这会使采样变慢。

让我们检查一下这是否有效。

故障排除

如果它还没有完全工作,让我们尝试一些修复方法,如下所示:

  1. 如果非常嘈杂,尝试更剧烈的转弯,并尽量保持双手稳定。这张图会因为加速度计本身的特性而嘈杂。

  2. 如果你看到图表在0-90度范围外断裂或行为异常,请确保你正在使用 atan2 函数——这个函数在大多数编程语言中执行三角函数的 CAST 规则。

  3. 注意,read_accelerometer_pitch_and_roll 方法在 atan2 函数前有一个负号。

  4. 如果在180度处出现问题——这是这个系统已知且预期的问题——尽量避免触及这一点。

现在,我们有了俯仰和滚转,但它们相当粗糙——一个合适的修复方法是结合传感器通过滤波器。我们还有一个传感器,它给我们一个综合的俯仰和滚转值:陀螺仪。

平滑加速度计

我们可以将我们对积分陀螺仪和加速度计的知识结合起来,以实现平滑的组合。

由于我们将更多地使用时间差概念,一个帮助的类将节省我们以后的工作。

时间差

我们之前看到如何跟踪绘图所需的时间差和更新之间的时间差以进行积分。让我们创建以下代码来帮助:

  1. 创建一个 delta_timer.py 文件,并首先导入 time,如下所示:

    import time
    
  2. 我们将创建一个 DeltaTimer 类来跟踪事物,如下所示:

    last and start variables with the current time.
    
  3. 这里的核心是一个 update 方法。每个循环都会调用这个方法;让我们先获取当前时间,如下所示:

        def update(self):
            current = time.time()
    
  4. 时间差将是当前时间和上次时间之间的差值,如下面的代码片段所示:

            dt = current - self.last
    
  5. 经过的时间是当前时间和开始时间之间的差值,如下面的代码片段所示:

            elapsed = current - self.start
    
  6. 现在,我们需要更新最后的时间以获取时间差并返回这些部分,如下所示:

            self.last = current
            return dt, elapsed
    

我们可以在需要用于绘图的时间差和经过的时间时使用这个类。让我们先使用它来组合加速度计和陀螺仪。

融合加速度计和陀螺仪数据

通过组合传感器,我们可以让每个传感器补充对方的弱点。加速度计作为俯仰角和横滚角的绝对测量值,以抵消陀螺仪看到的漂移。陀螺仪不像加速度计那样经历相同的噪声,但可以进行快速测量。让我们看看如何在以下图中将它们组合起来:

图16.10 – 陀螺仪和加速度计融合数据流

图16.10 展示了数据流图,使用互补滤波器融合陀螺仪和加速度计数据。以俯仰角为例。首先,系统将陀螺仪数据和时间差输入到积分器中。积分器将这个值加到之前的位置上。然后我们可以用这个值的95%来解释较大的运动变化。滤波器的其他5%是加速度计的测量值。这5%将在滤波过程中将测量值拖到平均加速度计读数,同时滤除混沌噪声元素。输出是一个滤波后的俯仰角或横滚角,然后反馈到积分器进行下一周期。

让我们将这些放入代码中,从滤波器开始,如下所示:

  1. 打开 robot_imu.py

  2. 添加 ComplementaryFilter 类,如下所示:

    class ComplementaryFilter:
    
  3. 我们可以使用左侧的值构建这个滤波器,存储这个值并计算其补数(左侧减一)来形成右侧,如下所示:

        def __init__(self, filter_left=0.9):
            self.filter_left = filter_left
            self.filter_right = 1.0 - filter_left
    
  4. 这个类有一个 filter 方法,它接受两个值并使用滤波器值将它们结合起来,如下所示:

        def filter(self, left, right):
            return self.filter_left * left + \
                   self.filter_right * right
    

    这样就完成了滤波器。

  5. 下一步,我们想要的是通过滤波器组合IMU传感器的代码 – 将它们融合在一起。我们将在 robot_imu.py 中添加一个类来实现这一点,如下所示:

    class ImuFusion:
    
  6. 在构造函数中,我们将存储 RobotImu 实例,创建一个滤波器,并初始化俯仰角和横滚值,如下所示:

        def __init__(self, imu, filter_value=0.95):
            self.imu = imu
            self.filter = ComplementaryFilter(filter_value).filter
            self.pitch = 0
            self.roll = 0
    
  7. 代码的核心部分是一个update函数。该函数接受一个dt(时间差)参数。它不会返回任何内容,只是更新俯仰/滚转成员,如下所示:

        def update(self, dt):
    
  8. 我们首先从加速度计获取俯仰和滚转值,如下所示:

            accel_pitch, accel_roll = self.imu.read_accelerometer_pitch_and_roll()
    
  9. 我们还想要陀螺仪的值,所以我们运行以下命令:

            gyro = self.imu.read_gyroscope()
    
  10. 现在,我们将陀螺仪的y读数和加速度计的俯仰值结合起来得到俯仰值,如下所示:

            self.pitch = self.filter(self.pitch + gyro.y * dt, accel_pitch)
    

    注意这里来自先前数据流的乘法和加法操作。

  11. 我们对滚转也做同样的处理,如下:

            self.roll = self.filter(self.roll + gyro.x * dt, accel_roll)
    

现在,我们已经准备好了带有滤波器的RobotImu类,并融合了传感器。让我们用以下代码进行一次图形测试:

  1. plot_pitch_and_roll.py文件中,我们将添加DeltaTimerImuFusion和陀螺仪校准导入。注意以下代码片段中已经移除了import time

    import vpython as vp
    from robot_imu import RobotImu, ImuFusion
    from delta_timer import DeltaTimer
    import imu_settings
    
  2. 接下来,我们使用陀螺仪设置设置RobotImu,然后创建fusion实例,如下代码片段所示:

    imu = RobotImu(gyro_offsets=imu_settings.gyro_offsets)
    fusion = ImuFusion(imu)
    
  3. 我们需要一个dt(时间差)用于融合计算和一个用于图形的已过时间。DeltaTimer类提供了这些。我们在循环开始前将其放在这里,替换了start的赋值,如下所示:

    timer = DeltaTimer()
    
  4. 现在,在计算elapsed的循环中,我们使用delta定时器,如下所示:

    while True:
        vp.rate(100)
        dt, elapsed = timer.update()
    
  5. 现在,用代码替换加速度计的读数,以更新融合的delta时间,使其进行计算,如下所示:

    fusion.update(dt)
    
  6. 我们现在可以从fusion对象中获取俯仰和滚转值,如下所示:

        graph_pitch.plot(elapsed, fusion.pitch)
        graph_roll.plot(elapsed, fusion.roll)
    
  7. robot_imu.pydelta_timer.pyplot_pitch_and_roll.py上传到机器人。

  8. 再次运行vpython plot_pitch_and_roll.py,并将你的浏览器指向机器人的9020端口。

表面上,它应该看起来与图16.9中的加速度计俯仰和滚转图相似。然而,当你移动机器人时,你应该注意到噪声少得多——图形更平滑——并且当你放下机器人或保持它静止时,它会水平。它应该能够快速处理快速转弯。系统平稳且准确!

故障排除

如果你有问题,尝试以下故障排除检查:

  1. 总是如此,如果你看到语法错误或异常行为,请仔细检查代码。

  2. 如果出现异常移动,确保你使用的是0.95(而不是95)作为滤波器值。

  3. 确保你已经上传了所有文件。

  4. 这个系统在图形开始后需要一两秒钟才能稳定下来。

你现在已经看到了如何从这些传感器获取准确和平滑的俯仰和滚转。一个轮式机器人可能不会遇到很多需要使用俯仰和滚转的原因,但其中之一将是使指南针工作。让我们进一步探讨磁力计。

使用磁力计检测航向

我们在 第12章使用Python进行IMU编程中看到了如何从磁力计绘制向量,以及磁性金属(如钢和铁的碎片)如何干扰它。甚至IMU板上的引脚头也会干扰。我们可以校准以补偿这一点。

获取 XYZ 分量并不那么有用;我们想要一个相对于磁北的航向。我们可以看到如何使用这个来进行精确的转向。

这个部分需要留出空间,因为磁铁非常少。笔记本电脑、手机、扬声器和磁盘驱动器会干扰这个传感器。使用指南针来揭示你空间中的磁场。我建议将机器人的支架尽可能长,允许的话多放一些支架;机器人的电机本身就有很强的磁场。

请避免以机器人面向南方开始——这会导致一些奇怪的结果,我们将在以后进行调查和修复。以机器人大致面向北方开始是个好主意。

校准磁力计

我们将执行一个称为硬铁偏移计算的校准。硬铁指的是任何与磁力计一起移动的磁性物体。我们将机器人移动到各个轴上来采样场强。我们将使用轴上所有读数的中间值来进行补偿,并将其添加到IMU设置中;这看起来与陀螺仪校准相似,但需要你移动机器人。

让我们编写代码,如下所示:

  1. 创建一个名为 magnetometer_calibration.py 的文件,从导入和 RobotImu 设置开始,如下所示:

    import vpython as vp
    from robot_imu import RobotImu
    imu = RobotImu()
    
  2. 我们将找到最小和最大向量,就像我们在陀螺仪中做的那样,如下面的代码片段所示:

    mag_min = vp.vector(0, 0, 0)
    mag_max = vp.vector(0, 0, 0)
    
  3. 我们将展示系统作为一组带有彩色点集群的三维散点图。这三个集群中的每一个都是一个结合了两个轴的图表:xyyzzx。我们的目标是通过对设备进行校准使这些集合对齐,如下所示:

    scatter_xy = vp.gdots(color=vp.color.red)
    scatter_yz = vp.gdots(color=vp.color.green)
    scatter_zx = vp.gdots(color=vp.color.blue) 
    
  4. 我们开始主循环并读取磁力计,如下所示:

    while True:
        vp.rate(100)
        mag = imu.read_magnetometer()
    
  5. 现在,我们以与陀螺仪相同的方式更新最小值,如下所示:

        mag_min.x = min(mag_min.x, mag.x)
        mag_min.y = min(mag_min.y, mag.y)
        mag_min.z = min(mag_min.z, mag.z)
    
  6. 然后,我们以同样的方式更新最大值,如下所示:

        mag_max.x = max(mag_max.x, mag.x)
        mag_max.y = max(mag_max.y, mag.y)
        mag_max.z = max(mag_max.z, mag.z)
    
  7. 我们随后以与陀螺仪相同的方式计算偏移量,如下所示:

        offset = (mag_max + mag_min) / 2
    
  8. 这个 print 语句显示了当前值和偏移量:

        print(f"Magnetometer: {mag}. Offsets: {offset}.")
    
  9. 现在,我们创建图表。它们将指导你获取足够的校准数据,并显示轴不对齐的地方。代码如下所示:

        scatter_xy.plot(mag.x, mag.y)
        scatter_yz.plot(mag.y, mag.z)
        scatter_zx.plot(mag.z, mag.x)
    
  10. 上传此文件并使用VPython运行它。你应该看到以下屏幕:图片

    图16.11 – 初始磁力计校准屏幕

    图16.11 展示了集群作为三个彩色块——右下角是红色(代表 xy),顶部是蓝色(代表 yz),右边是绿色(代表 zx)。这些集群在你这里将从一个不同的位置开始,这取决于你机器人的方向。

  11. 你需要移动机器人,慢慢地围绕整个 y 轴(围绕轮子)旋转。绿色图表应该更像一个椭圆,如下面的截图所示!

    图16.12 – 磁力计部分校准

    图16.12 展示了绿色值的椭圆,以及红色和蓝色散点图的更多数据。你越慢,数据越好。

  12. 将机器人绕 x 轴(机器人的长度)旋转,然后绕 z 轴(其高度)旋转。你移动的角度越多,越好。通过制作 8 的三维图形来填补空白。最终,它应该看起来像以下截图中的图形!

    图16.13 – 磁力计校准:良好的组合

    图16.13 展示了良好的数据集合应该看起来像什么,有红色、绿色和蓝色的圆圈。注意,由于机器人太靠近其他磁铁而摆动,存在异常值——小心这些!

  13. 你现在可以关闭浏览器了,因为你已经收集了大量校准数据。

  14. 控制台将显示校准偏移量,如下所示:

    Magnetometer: <30.6, -36.9, 10.35>. Offsets: <21.15, 3.15, 0.225>.
    Magnetometer: <31.5, -36.75, 11.25>. Offsets: <21.15, 3.15, 0.225>.
    

    开始时,这些偏移量变化很大;然而,随着你收集更多数据,它们将稳定下来,即使磁力计读数在变化。

我们现在有一些校准数字;我的例子给出了 21.15, 3.15, 0.225。让我们确保每个人都有一组值。

故障排除

这种校准可能不起作用——让我们看看原因如下:

  1. 如果数字看起来没有稳定下来,继续移动机器人。你必须尝试用它进行完整的 360 度运动以获得完整的范围。

  2. 如果奇怪的点出现在圆圈之外,请移动到其他地方并重新开始校准——这很可能是你测试时的磁场,并会影响你的结果。

  3. 有可能你的浏览器在尝试这样做时会变慢或耗尽内存——虽然我说要慢慢来,但你不能在运行时将其搁置一边,因为它会继续添加点。

  4. 如果你完全没有得到圆圈——线条或小块——请确保你有正确的 xyyzzx 绘图组合。

你现在应该正在获取校准偏移量。让我们使用这些值来对齐散点图。

测试校准值

为了看看这些是否有效,我们将它们放回代码中,并且相同的操作应该显示点簇对齐。它首先允许我们在 RobotImu 接口中设置偏移量。按照以下步骤进行:

  1. 打开 robot_imu.py 文件。

  2. __init__ 方法中,我们需要存储偏移量。我已经突出显示了新的代码,如下所示:

      def __init__(self, gyro_offsets=None,
                   mag_offsets=None):
          self._imu = ICM20948()
          self.gyro_offsets = gyro_offsets or vector(0, 0, 0)
          self. mag_offsets = mag_offsets or vector(0, 0, 0)
    
  3. read_magnetometer 方法需要减去磁力计偏移量,如下所示:

        def read_magnetometer(self):
            mag_x, mag_y, mag_z = self._imu.read_magnetometer_data()
            return vector(mag_x, -mag_z, -mag_y) - self. mag_offsets
    

我们现在可以将磁力计的偏移量包含在我们的脚本中。我们将把这些放在我们用于陀螺仪校准的相同设置文件中。按照以下步骤进行:

  1. 打开 imu_settings.py

  2. 按照以下方式添加你的机器人上的磁力计校准读数:

    from vpython import vector
    gyro_offsets = vector(0.557252, -0.354962, -0.522901)
    mag_offsets = vector(21.15, 3.15, 0.225)
    
  3. 现在,我们可以在散点图中使用它们。打开magnetometer_calibration.py,并将我们的设置添加到导入中,如下所示:

    import vpython as vp
    from robot_imu import RobotImu
    from imu_settings import mag_offsets
    
  4. 当我们创建了我们的RobotImu后,我们可以应用偏移量,如下所示:

    imu = RobotImu(mag_offsets=mag_offsets)
    
  5. 将文件发送到机器人,并重新运行magnetometer_calibration.py。你需要再次进行旋转和8字形图样,以在不同方向上获得许多样本点。当你收集了数据后,你应该有重叠的圆圈,如下面的截图所示:

图16.14 – 校准磁力计

图16.14显示了叠加的红色、蓝色和绿色圆圈。恭喜你——你已经校准了你的磁力计!

使用你的校准磁力计,我们可以尝试进行更多有用值的进一步实验。但首先,让我们排除任何问题。

如果圆圈没有在一起,应该怎么做

你可能已经到达了这个点,但圆圈没有收敛。如果出现这种情况,请尝试以下故障排除步骤:

  1. 你需要重新运行校准代码。在这样做之前,请注释掉应用于RobotImu类的偏移量应用(设置)行。在偏移量激活时运行校准代码会导致它偏移错误。

  2. 仔细检查你的校准和IMU代码,确保没有错误。

  3. 确保在机器人附近没有强磁铁或大块金属——例如扬声器或硬盘。尽量在离这些物体大约一米的地方做这个。甚至你的笔记本电脑或手机也可能产生干扰。

  4. 确保你缓慢地通过每个轴,并尝试8字形图样。继续这样做,直到你能看到三个椭圆。

  5. 你可以使用控制台输出,旋转机器人,并在每个方向上移动,然后看看这里输出的偏移值是否稳定。

  6. 当输出稳定后,再次尝试应用偏移量,并运行校准以查看圆圈是否收敛。

经过这些步骤后,你应该有了继续所需的校准值。让我们将其放回我们已有的矢量输出中,并确定航向。

从磁力计获取粗略航向

现在我们已经获得了校准设置,我们可以开始使用磁力计读数来估计北方在哪里,就像指南针一样。航向yaw意味着同一件事——相对于参考点的方向——在这种情况下,是磁北。让我们看看我们如何做到这一点。看看下面的截图:

图16.15 – 从磁力计获取近似航向

图16.15显示了我们将构建的方法。它使用校准数据应用了磁力计,并使用atan2,就像我们使用陀螺仪来近似航向一样。我们也可以添加一个粗略的指南针。

让我们这样做:

  1. 创建一个plot_mag_heading.py文件。从导入开始,如下所示:

    import vpython as vp
    from robot_imu import RobotImu
    from delta_timer import DeltaTimer
    import imu_settings
    
  2. 我们可以用设置初始化RobotImu,如下所示:

    imu = RobotImu(mag_offsets=imu_settings.mag_offsets)
    
  3. 要制作指南针显示,我们需要一个红色刻度盘(圆柱体)和指针(箭头),如下所示:

    vp.cylinder(radius=1, axis=vp.vector(0, 0, 1), 
                pos=vp.vector(0, 0, -1))
    needle = vp.arrow(axis=vp.vector(1, 0, 0), 
                      color=vp.color.red)
    
  4. 接下来,让我们制作一个图表来显示航向,以及一个用于显示经过时间的delta计时器,如下所示:

    vp.graph(xmin=0, xmax=60, scroll=True)
    graph_yaw = vp.gcurve(color=vp.color.blue)
    timer = DeltaTimer()
    
  5. 我们以速率启动主循环并获取经过时间,如下所示:

    while True:
        vp.rate(100)
        dt, elapsed = timer.update()
    
  6. 现在,我们通过运行以下命令来读取磁力计:

        mag = imu.read_magnetometer()
    
  7. 我们可以取xy平面,并找到这些值的atan2函数以获得航向,如下所示:

        yaw = -vp.atan2(mag.y, mag.x)
    
  8. 然后,我们以度为单位在图表上绘制这个,如下所示:

        graph_yaw.plot(elapsed, vp.degrees(yaw))
    
  9. 我们还可以使用sin/cos将针轴设置为我们的方向,将其转换回一个单位方向,如下所示:

        needle.axis = vp.vector(vp.sin(yaw), vp.cos(yaw), 0)
    
  10. 保存,上传并在VPython中运行此代码。将你的浏览器发送到机器人的9020端口。

  11. 如果你围绕机器人旋转,你会看到如下显示:

图16.16 – 磁力计航向估计

图16.16显示了一个指南针,顶部是机器人感知的北方,以及一个红色箭头。下面是一个蓝色图表,范围在+和-180度之间。当你移动机器人时,你会看到这个移动,0度是北方。不过,你需要确保机器人位于一个平坦的表面上。

注意,指南针是读取相对于机器人的北方,而不是机器人相对于北方的位置!

这个输出开始看起来合理。它可以指向北方并进行一些指南针测量,我们有一个航向。

这有点混乱,任何俯仰或横滚都可能使其错误。再次,通过将此数据与其他传感器的数据融合,我们可以改进这一点。

结合传感器进行定位

我们已经看到如何将加速度计和陀螺仪结合起来,以获得平稳的俯仰和横滚读数。我们还可以再次结合这些传感器,以正确地定位并平滑磁力计的读数。这个系统使我们能够近似机器人的绝对方向。

查看以下数据流以了解我们在做什么——它是基于之前阶段的:

图16.17 – 融合所有三个传感器

图16.17从左侧开始,显示我们之前阶段的数据。我们有灰色的滤波俯仰和横滚,因为它也是一个输出。有校准后的陀螺仪偏航、delta时间和校准后的磁力计作为输入。滤波后的俯仰和横滚通过倾斜补偿框,在那里我们旋转磁力计向量。磁力计数据然后通过一个xy-到极坐标框,使用atan2函数来获得航向。

在此之上,校准后的陀螺仪偏航和delta时间进入一个积分器,它将添加到之前的偏航读数。积分器和磁力计航向输出进入一个互补滤波器,其中积分器的输出占主导地位。这个滤波器的输出是一个航向/偏航输出,它将稳定且快速响应,并返回到绝对航向。我们现在有三个角度——俯仰、横滚和偏航!

让我们修改代码来实现这一点,如下所示;

  1. 打开robot_imu.py并转到ImuFusion类。

  2. 我们需要将其转换回弧度,因此我们需要将此添加到从VPython导入的内容中,如下所示:

    from icm20948 import ICM20948
    from vpython import vector, degrees, atan2, radians
    
  3. __init__方法中,我们应该添加一个变量来存储偏航,如下所示:

        def __init__(self, imu, filter_value=0.95):
            self.imu = imu
            self.filter = ComplementaryFilter(filter_value).filter
            self.pitch = 0
            self.roll = 0
            self.yaw = 0
    

    我们现在将使用相同的滤波器。

  4. update方法中,在计算俯仰和滚转之后,添加以下行以获取磁力计读数:

            mag = self.imu.read_magnetometer()
    
  5. mag变量是一个向量。我们使用俯仰和倾斜来旋转这个向量,以使xy分量水平,如下所示:

            mag = mag.rotate(radians(self.pitch), vector(0, 1, 0))
            mag = mag.rotate(radians(self.roll), vector(1, 0, 0))
    
  6. 我们现在可以从这个计算磁力计偏航,如下所示:

            mag_yaw = -degrees(atan2(mag.y, mag.x))
    
  7. 为了稳定它,我们现在可以使用互补滤波器与陀螺仪,如下所示:

            self.yaw = self.filter(self.yaw + gyro.z * dt, mag_yaw)
    

self.yaw值现在将具有补偿后的偏航(或航向)值,使这个IMU可以作为指南针使用。为了使用它,让我们以三种方式可视化它——作为图形、指南针和机器人的运动。按照以下步骤进行:

  1. 将此代码放入一个名为visual_fusion.py的新文件中。代码将非常熟悉。只有磁力计偏移量和偏航值是新的。导入如下代码片段所示:

    import vpython as vp
    from robot_imu import RobotImu, ImuFusion
    from delta_timer import DeltaTimer
    import imu_settings
    import virtual_robot
    
  2. 准备带有磁力计偏移量的RobotImu,并初始化fusion,如下所示:

    imu = RobotImu(gyro_offsets=imu_settings.gyro_offsets,
                   mag_offsets=imu_settings.mag_offsets)
    fusion = ImuFusion(imu)
    
  3. 我们将使用一个VPython画布来表示虚拟机器人,并为指南针使用一个单独的画布。每个画布让我们包含一个3D场景。让我们将当前画布设置为机器人视图并将其放在左侧。机器人模型将与这个视图相关联。代码如下所示:

    robot_view = vp.canvas(align="left")
    model = virtual_robot.make_robot()
    virtual_robot.robot_view()
    
  4. 为了配合机器人视图,我们将创建一个指南针画布,使用与之前相同的圆柱和箭头。请注意,最新的画布与之后创建的形状相关联。代码如下所示:

    compass = vp.canvas(width=400, height=400)
    vp.cylinder(radius=1, axis=vp.vector(0, 0, 1), 
                pos=vp.vector(0, 0, -1))
    needle = vp.arrow(axis=vp.vector(1, 0, 0), 
                      color=vp.color.red)
    
  5. 按如下设置俯仰、滚动和偏航的图形:

    vp.graph(xmin=0, xmax=60, scroll=True)
    graph_roll = vp.gcurve(color=vp.color.red)
    graph_pitch = vp.gcurve(color=vp.color.green)
    graph_yaw = vp.gcurve(color=vp.color.blue)
    
  6. 创建一个delta计时器,启动循环,并获取时间更新,如下所示:

    timer = DeltaTimer()
    while True:
        vp.rate(100)
        dt, elapsed = timer.update()
    
  7. 现在,我们使用时间更新fusion(它将读取IMU并执行计算),如下所示:

        fusion.update(dt)
    
  8. 现在,在我们旋转它之前,我们需要重置虚拟机器人模型,如下所示:

        model.up = vp.vector(0, 1, 0)
        model.axis = vp.vector(1, 0, 0)
    
  9. 然后,我们需要执行三个旋转——滚动、俯仰和偏航,如下所示:

        model.rotate(angle=vp.radians(fusion.roll), axis=vp.vector(1, 0, 0))
        model.rotate(angle=vp.radians(fusion.pitch), axis=vp.vector(0, 1, 0))
        model.rotate(angle=vp.radians(fusion.yaw), axis=vp.vector(0, 0, 1))
    
  10. 我们定位指南针针——注意我们的偏航是以度为单位,因此我们需要将其转换,如下所示:

        needle.axis = vp.vector(
    vp.sin(vp.radians(fusion.yaw)), 
    vp.cos(vp.radians(fusion.yaw)), 
                0)
    
  11. 然后,我们绘制三个图形轴,如下所示:

        graph_roll.plot(elapsed, fusion.roll)
        graph_pitch.plot(elapsed, fusion.pitch)
        graph_yaw.plot(elapsed, fusion.yaw)
    
  12. robot_imu.pyvisual_fusion.py上传到机器人。首先使用vpython visual_fusion.py,并将你的浏览器指向机器人的9020端口。

你应该能看到可视化的机器人、指南针以及所有三个轴的图形显示,每个都应该既相对稳定又响应迅速,如以下截图所示:

图16.18 – 俯仰、滚动和偏航图形

图16.18中的图形是显示的截图。左上角是虚拟机器人——你可以通过右键点击来更改其视图。左上角显示指南针。下面是滚转、俯仰和偏航的滚动图形。滚转用红色表示,俯仰用绿色表示,偏航用蓝色表示。图形最初会稳定,然后匹配你的机器人运动。当在一个轴上移动时,对其他轴有轻微的影响,但它们可以独立移动。

在±180度时,图表会表现不佳。让我们看看如何解决这个问题。

解决180度问题

需要认识到的是,圆上的角度是循环的;200度和-160度是等效的,-180度和180度也是相等的。我们没有让过滤器或代码意识到这一点,所以当我们达到180度点,atan2函数在-179.988和179.991(或一些非常接近的标记)之间翻转时,图表变得混乱,将小于1度的差异视为360度,然后尝试在这两者之间进行过滤。

为了解决这个问题,我们需要进行一些更改。首先,我们可以声明我们希望角度数值在-180度以下和180度以上,并以这种方式约束它们。由于我们打算使用角度的互补滤波器,我们可以对其进行特殊化,如下所示:

  1. robot_imu.py的顶部,在ComplementaryFilter类内部,让我们添加一个格式化角度的方法,如下所示:

        @staticmethod
        def format_angle(angle):
    
  2. 如果角度低于-180度,我们希望通过加上360将其绕回,如下所示:

            if angle < -180:
                angle += 360
    
  3. 如果角度大于180度,我们将通过减去360将其绕回,如下所示:

            if angle > 180:
                angle -= 360
            return angle
    
  4. 我们将用某种更智能地约束这些角度的方法来替换filter函数的内部。当我们进行过滤时,我们首先按照以下方式格式化传入的角度:

        def filter(self, left, right):
            left = self.format_angle(left)
            right = self.format_angle(right)
    
  5. 我们还希望将过滤后的角度放在相同的范围内。如果差异超过350度,我们可以假设某些东西已经绕回;因此,我们将最低的一个加上360度来过滤它们,如下所示:

            if left - right > 350:
                right += 360
            elif right - left > 350:
                left += 360
    filtered = self.filter_left * left + \
                   self.filter_right * right
    
  6. 这个操作可能会得到一个超出范围的答案。因此,我们将其格式化回,如下所示:

            return format_angle(filtered)
    
  7. 这个过滤器已经在使用中,所以我们可以重新运行visual_fusion.py并尝试再次通过180度。当你将浏览器指向端口时,在稳定后,那里的机器人应该会随着你的旋转而旋转——并且稳定,不会漂移!

注意,这个系统在启动时处理面向南方的情况仍然不太好。我们已经至少解决了系统的一个问题,并平滑了其缺陷。

这种行为很令人兴奋:你现在可以在屏幕上得到一个机器人来镜像你的旋转。然而,尽管在屏幕上移动很有趣,但我们希望看到它在现实世界中得到应用。让我们启动一些电机!

从IMU数据驱动机器人

在前面的章节中,我们看到了如何使用PID算法,在本章中,我们将学习如何从磁力计检测俯仰、滚转和偏航。我们的机器人不能改变其俯仰或滚转,但它可以改变其航向。

在这个演示中,我们将让机器人保持在航向上——尝试无论我们将其转向何方都能追踪北方。让我们看看如何。看一下以下图表:

图片

图16.19 – 航向驱动行为

图16.19显示了数据流。图的左侧从测量的航向开始,航向设定点进入PID——误差值将是两者的差值。测量的航向来自IMU + 融合算法。我们使用PID输出来驱动电机,使它们以固定速度加减该值移动,这样机器人就会转向以减少误差。机器人的移动将反馈到IMU + 融合算法中,通过PID循环。

让我们使用前面的流程来构建代码,如下所示:

  1. 以以下导入开始drive_north_behavior.py文件:

    from robot_imu import RobotImu, ImuFusion
    from delta_timer import DeltaTimer
    from pid_controller import PIController
    from robot import Robot
    import imu_settings
    
  2. 我们现在初始化RobotImufusionDeltaTimer,如下所示:

    imu = RobotImu(mag_offsets=imu_settings.mag_offsets,
                   gyro_offsets=imu_settings.gyro_offsets)
    fusion = ImuFusion(imu)
    timer = DeltaTimer()
    
  3. 我们可以设置PID(或PI)控制器和机器人,如下所示:

    pid = PIController(0.7, 0.01)
    robot = Robot()
    
  4. 然后,几个常数——机器人的基本速度,以及从北方起度的航向设定点,如下面的代码片段所示:

    base_speed = 70
    heading_set_point = 0
    
  5. 这里的主要循环更新计时器和IMU融合。注意在下面的代码片段中,这里没有视觉速率:

    while True:
        dt, elapsed = timer.update()
        fusion.update(dt)
    
  6. 我们现在计算误差,并将该误差和delta时间输入PID,如下所示:

        heading_error = fusion.yaw - heading_set_point
        steer_value = pid.get_value(heading_error, delta_time=dt)
    
  7. 我们打印值以进行调试,并设置电机速度,如下所示:

        print(f"Error: {heading_error}, Value:{steer_value:2f}, t: {elapsed}")
        robot.set_left(base_speed + steer_value)
        robot.set_right(base_speed - steer_value)
    

将此上传到机器人,打开电机,用常规Python 3运行。机器人将尝试向北行驶。如果你偏离航线,它将纠正回北方,而你转得越多,电机尝试转回的速度就越快。玩这个行为相当有趣!

完成后按 Ctrl + C 停止,并尝试不同的航向设定点。

在本节中,你通过构建数据流图和从它们编写代码来巩固了这些技能。你通过将传感器数据转换为这样的数字,展示了如何使用它构建基于PID的行为。然后你使用了我们计算出的航向,并用它和PID一起创建从你的机器人来的基于指南针的运动。

摘要

在本章中,你看到了如何将IMU传感器组合起来以近似空间中的绝对方向。你看到了如何在图表中呈现这些信息,以及如何使用虚拟机器人将它们显示在屏幕上。然后你看到了如何将这个传感器系统连接到PID控制器和电机,以使机器人行驶。

你已经学到了一些必要的数学知识,用于在3D空间中转换向量分量和角度,以及如何使用互补滤波器来补偿一个系统中的噪声和另一个系统中的漂移。你已经开始看到多个传感器融合在一起,以对世界做出推断。你的框图和数据流技能得到了锻炼,你在PID算法方面也有了更多的实践。

在下一章中,我们将探讨如何使用智能手机控制你的机器人,并从菜单中选择行为。

练习

这里有一些想法可以帮助你进一步理解,并给你一些关于如何使用本章概念做更有趣的事情的想法:

  • 读者可以使用更多颜色和复杂的形状来制作更好的机器人模型。这并非本章的目的,但这是一个有趣且有益的方法,可以让你更熟悉VPython。

  • 我们的磁力计设置是硬编码的,直接写入Python文件中。从数据文件中加载设置是一种良好的实践。一个良好的起点可以在http://zetcode.com/python/yaml/找到。

  • 能否使用视觉机器人来显示或调试其他传感器和集成?

  • 你能否将这里的绝对定位与编码器结合起来,以实现非常精确的转弯的方形?

进一步阅读

关于本章涵盖的主题的更多信息,请参考以下内容:

第二十章:第17章:使用手机和Python控制机器人

我们一直在编程的机器人有许多行为,但当你运行其中一些时,它们会导致机器人在房间的另一边停止。你可以尝试编写代码将其送回,但这可能很复杂。我们还有一个很棒的摄像头,可以提供一些视觉反馈,显示机器人正在做什么。难道不是很好奇有时可以控制并驾驶机器人吗?

我们一直在通过Secure ShellSSH)终端发送命令来驾驶我们的机器人,但如果可以通过菜单启动命令,这将使机器人更加有趣和舒适地进行演示。我们可以在第15章,“使用Mycroft与机器人进行语音通信”中制作的Web应用程序编程接口API)代码的基础上进行构建。

在本章中,我们将了解如何创建一个菜单系统来选择为手机设计的操作。然后我们将使用触摸表面构建一个控制系统,摄像头在视野中。你将了解为手机准备的Web应用程序,并控制机器人。

本章我们将涵盖以下主题:

  • 当语音控制不起作用时——为什么我们需要驾驶

  • 菜单模式——选择你的机器人行为

  • 选择控制器——我们将如何驾驶机器人,以及为什么

  • 为远程驾驶准备Raspberry Pi——启动基本驾驶系统

  • 使机器人完全可通过手机操作

  • 使菜单在Pi启动时启动

技术要求

对于本章,你需要以下物品:

  • 配置好摄像头并从上一章获取代码的Raspberry Pi机器人

  • 一种带Wi-Fi的手机等触摸屏设备

  • 无线网络

本章的GitHub代码位于https://github.com/PacktPublishing/Learn-Robotics-Programming-Second-Edition/tree/master/chapter17

使用0_starting_point文件夹查找上一章的完整代码,以及GitHub上的full_system文件夹以获取本章的完整代码。

查看以下视频以查看代码的实际操作:https://bit.ly/2Kb7rp8

当语音控制不起作用时 – 为什么我们需要驾驶

第15章,“使用Mycroft与机器人进行语音通信”,我们构建了一个Mycroft系统来启动行为。如果你尝试构建意图让机器人及时停止或向左或向右行驶,你可能已经注意到,即使是最清晰的说话也需要一些时间来响应。

语音控制也仅在安静的环境中真正有效。如果你的机器人在外面(你希望将其驶往某处),这就不太有用。

Mycroft完全依赖于能够访问互联网。对于机器人和控制器来说,有一个小型的共享网络是一回事;但总是需要互联网访问则是另一回事,当你不在家、学校或实验室时,这可能会变得很棘手。

使用SSH会话登录到机器人,然后输入命令来启动和停止行为,在测试阶段效果良好,但可能会很慢且繁琐。在演示条件下,误输入命令或只是重新启动SSH会话都会很耗时。

针对手机的目标浏览器应用可以响应,让你能够紧密控制机器人的动作。在有本地网络的情况下,它不需要外部互联网访问。你可以使用它来在行为运行后停止机器人并将其开回你身边,也可以用来停止错误的行为。经过一些思考,它还可以用来提供有用的——或者仅仅是有趣的——关于你的机器人正在做什么的反馈。

菜单模式 - 选择你的机器人行为

我们的书介绍了一系列机器人行为,并邀请你创建更多。我们讨论了SSH启动机器人程序可能会很繁琐——甚至只是记住你有的选项或按下Ctrl + C组合来停止都可能会让人沮丧。

在本节中,我们将创建一个菜单系统来选择它们。一种方便且适合手机的方式是将它提供给手机的浏览器,因此我们采用了这种方法来处理我们的机器人。我们还将使用桌面浏览器来测试此代码。

我们可以在第15章中“远程启动行为”部分的系统中扩展,添加用户界面UI)。我们将使用模板制作此UI,其中一些占位符将由代码替换。

让我们看一下以下图表,看看这个系统将如何工作:

图17.1 – 控制服务器和菜单系统的工作方式

图17.1显示了系统的概述。以下是它是如何工作的:

  1. 客户端浏览器(手机或电脑)通过Wi-Fi向机器人上的网络服务器发送页面请求以显示页面。

  2. 网络服务器使用机器人模式来获取模式列表:它可以启动的脚本列表。

  3. 网络服务器将此模式列表发送到模板以将其渲染成菜单页面,并将渲染后的菜单页面发送给用户。

  4. 在浏览器中,当你触摸或点击页面中的菜单项链接时,它们会向网络服务器发送控制请求。

  5. 机器人模式系统发送runstop命令。

  6. 机器人模式系统启动/停止行为脚本。

  7. 控制服务器客户端浏览器发送状态信息,表示它已被处理。

让我们先扩展脚本(模式)列表以及系统如何处理它们。

管理机器人模式

我们将重新审视在第15章,“使用Mycroft与机器人进行语音通信”中编写的代码,扩展要运行的模式列表,并添加菜单配置。

让我们扩展我们的模式系统所知道的项目数量,如下所示:

  1. 打开名为robot_modes.py的文件。

  2. 在此文件中找到mode_config变量。我们可以通过以下代码片段扩展它,添加更多行为:

        mode_config = {
            "avoid_behavior": "avoid_behavior.py",
            "circle_head": "circle_pan_tilt_behavior.py",
            "test_rainbow": "test_rainbow.py",
            "test_leds": "leds_test.py",
            "line_following": "line_follow_behavior.py",
            "behavior_line": "straight_line_drive.py",
            "drive_north": "drive_north.py"
        }
    
  3. mode_config变量之后,我们添加一个配置菜单的列表。顺序将与屏幕上的菜单项匹配。每个项目都有一个mode_name设置——与mode_config变量中的简短缩略名匹配,以及text——菜单选项的易读标签,如下面的代码片段所示:

        menu_config = [
            {"mode_name": "avoid_behavior", "text": "Avoid Behavior"},
            {"mode_name": "circle_head", "text": "Circle Head"},
            {"mode_name": "test_leds", "text": "Test LEDs"},
            {"mode_name": "test_rainbow", "text": "LED Rainbow"},
            {"mode_name": "line_following", "text": "Line Following"},
            {"mode_name": "behavior_line", "text": "Drive In A Line"},
            {"mode_name": "drive_north", "text": "Drive North"}
        ]
    

    如果我们想在菜单中添加一个行为,我们必须将其添加到menu_configmode_config变量中。

  4. 为了允许菜单用户在不按下run方法的情况下选择新的模式,我们可以通过停止任何现有进程来处理这个问题,如下所示:

        def run(self, mode_name):
            while self.is_running():
                self.stop()
            script = self.mode_config[mode_name]
            self.current_process = subprocess.Popen(["python3", script])
    

    此文件将作为配置文件使用,你可以扩展它来运行其他代码。我们现在可以测试一下。

  5. robot_modes.py上传到机器人。你应该已经上传了第15章,“使用Mycroft与机器人进行语音通信”的control_server.py文件。

  6. 在Pi上使用python3 control_server.py运行此代码。

  7. 正如我们在第15章,“使用Mycroft与机器人进行语音通信”中看到的,我们将使用curl命令来发送请求,如下所示:

    curl -X POST http//myrobot.local:5000/run/test_leds
    

    这应该会启动机器人上的发光二极管LEDs)闪烁。

  8. 让我们改变行为——这将停止当前行为并启动一个新的行为。运行以下代码:

    curl -X POST http//myrobot.local:5000/run/circle_head
    

    LED应该停止闪烁,假设电机已经打开,头部应该开始移动。

  9. 让我们通过运行以下代码来停止机器人:

    curl -X POST http//myrobot.local:5000/stop
    

我们在robot_modes.py中添加了一些额外的模式和配置来描述这些模式,并进行了测试。让我们检查是否有任何问题。

故障排除

当对菜单服务器的请求失败时,它可以在响应中输出错误代码。在我们的系统中,我们只使用了以下三个错误代码:

  • 200——这意味着服务器认为一切正常。可能仍然存在逻辑问题,但并未导致失败。

  • 404——当服务器找不到路由时显示。这意味着你可能在你发出的请求或服务器代码中的路由器中有一个拼写错误。检查它们是否匹配并再次尝试。

  • 500——这意味着服务器以某种方式失败了。这通常伴随着服务器上的回溯/异常。这可以被视为一个正常的Python错误。

现在我们已经准备好了模式配置列表,我们需要网络服务来显示它。

网络服务

第15章,“使用 Mycroft 与机器人进行语音通信”,我们已经在 control_server.py Flask 网络服务器中连接了 robot_modes.py。我们在 第13章,“机器人视觉——使用 Pi 摄像头和 OpenCV”,使用了 Flask 来渲染带有视频框的模板。在本节中,我们将创建一个菜单模板以显示用户的选项。

让我们先进行必要的更改以渲染模板,如下所示:

  1. 打开 control_server.py

  2. 将 Flask 的导入扩展到包括 render_template,如下所示:

    from flask import Flask, render_template
    from robot_modes import RobotModes
    
  3. 由于我们将更改样式表,我们需要停止持有过时缓存的表单的设备。我们可以通过向所有响应添加标题来实现这一点,如下所示:

    @app.after_request
    def add_header(response):
        response.headers['Cache-Control'] = "no-cache, no-store, must-revalidate"
        return response
    
  4. 现在我们需要添加显示我们菜单的路由。我们将创建一个名为 menu.html 的模板,它使用 menu_config 变量来显示。我们的大部分模式都需要这个。让我们添加渲染模板的代码,如下所示:

    @app.route("/")
    def index():
        return render_template('menu.html', menu=mode_manager.menu_config)
    

现在我们已经有了渲染模板的代码,它是基于我们已有的处理 runstop 请求的代码。然而,在我们能够运行此服务之前,我们需要提供模板,即 menu.html

模板

我们的 HTML 模板定义了我们的显示,并让我们将机器人菜单的外观与处理控制系统的方法分开。此模板结合了 第13章,“机器人视觉——使用 Pi 摄像头和 OpenCV” 和 第14章,“Python 中的摄像头跟随线” 中看到的 HTML,以及 第15章,“使用 Mycroft 与机器人进行语音通信” 中的 templates 文件夹——我们将在这里添加我们的菜单模板。我们可以进一步美化此模板;现在,我们将保持其简单性。

创建一个名为 templates/menu.html 的文件,然后按照以下步骤进行:

  1. 我们的模板从设置页面标题的标题开始,并使用我们之前看到的相同的 jQuery 工具,如下面的代码片段所示:

    <html>
    <head>
        <script src="img/jquery-3.5.1.min.js"></script>
        <title>My Robot Menu</title>
    </head>
    
  2. 我们模板的主体有 My Robot Menu 标题。你可以随意将其更改为你的机器人名称。代码如下所示:

    <body>
      <h1>My Robot Menu</h1>
    
  3. 接下来,有一个用于消息的空间;现在它是空的,正如你在这里可以看到的:

        <p id="message"></p>
    
  4. 下一个部分是一个列表——即菜单本身。我们使用 <ul> 标签然后是一个 for 循环,它为每个菜单项创建一个带有链接的列表项。双大括号 {{ }} 用于包围一个占位符,当运行时将被替换。它使用 mode_name 设置和 text 来创建该链接,将 /run 与模式名称结合,如下面的代码片段所示:

    run action in some code.
    
  5. 在关闭我们的列表之前,我们需要添加一个额外的菜单项——停止按钮,如下所示:

        <li><a href="#" onclick="run('/stop')">Stop</a></li>
      </ul>
    
  6. 我们在 JavaScript 代码中讨论了处理 run 动作。以下代码发送数据到 Web 服务器并从响应中更新消息的 POST 请求。我们需要将其放在 <script> 标签中,如下所示:

    run function calls the .post method with the => operator is JavaScript shorthand for defining a small function—in this case, one that has the response parameter. An important idea in JavaScript is that a function can be a bit of data. In JavaScript, passing a function in as a parameter to another function is a common way to do things. Because we often use this, functions used that way are not even given names; they are anonymous functions or lambdas.
    
  7. 现在,我们可以像这样关闭我们的 HTML 文档:

    </body>
    </html>
    

模板如这样的好处是,您可以在浏览器中预览此代码,而无需服务器,并理解它应该如何看起来。以下截图显示了它在预览模式下的样子:

图 17.2 – 预览模板

当您查看预览,如图 图 17.2 所示,模板占位符会显示,因为浏览器不知道如何渲染它们。

您需要运行应用才能正确渲染。

运行它

robot_modes.pycontrol_server.py 文件上传到机器人,然后是 templates 文件夹。在 Raspberry Pi 上,通过 SSH,您可以使用以下命令启动它:

pi@myrobot:~ $ python3 control_server.py 
 * Serving Flask app "control_server" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 270-549-285

您现在可以将浏览器指向您的机器人(http://myrobot.local:5000/)以查看菜单。以下截图显示了它应该看起来是什么样子:

图 17.3 – 浏览器中的我的机器人菜单

图 17.3 现在显示了渲染的列表。我们现在看到所有菜单项而不是模板占位符。您应该能够点击一个模式并看到机器人启动该行为。点击 robot_modes.py 代码将发送相当于 Ctrl + C 动作到运行的行为脚本,使其停止。

当您点击一个行为或停止时,它会在消息区域显示输出,如图下所示截图:

图 17.4 – 停止消息

图 17.4 再次显示了菜单。我已经点击了停止按钮,因此菜单显示了停止响应消息。

注意以下代码片段中行为输出的 print 语句出现在 Web 服务器控制台中:

192.168.1.149 - - [17/Oct/2020 22:42:57] "POST /run/test_leds HTTP/1.1" 200 -
red
blue
red
blue
red
Traceback (most recent call last):
  File "leds_test.py", line 16, in <module>
    sleep(0.5)
KeyboardInterrupt
192.168.1.149 - - [17/Oct/2020 22:43:41] "POST /stop HTTP/1.1" 200 -

您需要在 Pi 上按下 Ctrl + C 来退出此菜单服务器应用。

重要提示

这个微型的机器人 Web 应用没有安全机制、身份验证或密码。这超出了本书的范围,但如果您计划在共享 Wi-Fi 上使用它,这是一个值得进一步研究的严肃考虑。

有方法可以将脚本的控制台输出放到页面上。我建议查看 进一步阅读 部分的附加阅读推荐,以了解 Flask。

故障排除

希望这一切都能正常工作,但如果您有任何问题,请尝试以下步骤:

  • 输出日志显示了来自 Web 系统的返回代码。您可以使用这些状态代码——如您之前所见——来排除故障。

  • 200—系统认为一切正常。如果它未能运行某些内容,请检查 run 函数。

  • 404—未找到。您是否匹配了路由?

  • 500—您也应该看到与此相关的 Python 错误。

  • 如果渲染显示 { item.text },则需要使用双大括号,以便模板系统正常工作。

  • 如果你看到像jinja2.exceptions.TemplateSyntaxError: unexpected '<'这样的错误,那么你需要验证你是否正确地输出了前面的模板——你很可能会遗漏了一个闭合的大括号(})。

你现在有一个菜单系统来启动不同的机器人行为并停止它们。你可以将手机指向它——尽管它目前还不是特别适合手机。我们只是刚刚触及了这一点,这个系统相当基础。

我们将开始查看一个更有趣的手机界面来控制机器人,但我们可以先看看除了智能手机之外的其他选项。

选择控制器——我们将如何驾驶机器人,以及为什么

我们希望能够用手持无线设备来控制我们的机器人。给机器人拖一根线没什么意义。在看过我们的机器人在第 7 章,“使用 Python 驱动和转向 – 移动电机”中的驾驶方式后,我们将需要一个直接影响轮子的控制系统。

实现这一点的办法之一是使用蓝牙手柄。市场上有很多这样的手柄,可能需要专业的驱动程序来读取。蓝牙有时会在不合适的时候断开配对。

一些手柄使用定制的无线扩展器;这些比蓝牙更可靠,但扩展器在机器人上不太合适。

然而,你口袋里已经有一个手持设备了:你的手机。它有一个触摸屏,能够读取手指动作。通过一些正确的代码,你可以在控制器条之间显示视频,创建一种可以驾驶并观察的机器人潜望镜(在相机上驾驶相当困难——比从上方驾驶更难)。我们已经为机器人构建了可以通过 Wi-Fi 访问的网页应用,而大多数手机都可以连接到那个应用。因此,我们不会出去买一个新的手柄,而是将制作一个手机可以访问的网页应用来驾驶机器人并看到机器人的视角。

设计和概述

要制作一个手机网页应用,我们需要对如何期望它工作有一些设计。这个设计可能只是一个在纸条上用笔画的草图,或者使用绘图工具来获得专业的外观。下一张截图显示了这个的原型:

图 17.5 – 驾驶网页应用的屏幕截图

图 17.5 中的原型显示了横幅模式下的手机屏幕。屏幕顶部有一个退出按钮,我们可以将其设置为在指示应用退出后转到我们的菜单。

屏幕中间有一个来自机器人的视频流,使用了第 13 章,“机器人视觉 – 使用 Pi 相机和 OpenCV”中“构建 Raspberry Pi 相机流应用”的机制。左右两侧有滑块。下一张截图显示了它是如何工作的:

图 17.6 – 滑块返回中间行为

图17.6 展示了滑动机制。与模拟摇杆类似,您可以通过触摸将滑块拖动到其轨道上的任何位置,并且当松开时,它们会弹回到中间。

注意,它们不会立即回到中间,而是在几帧内动画回到这个位置。我们需要一点数学知识来在我们的代码中实现这一点。

这些滑块让您能够以坦克式(使用摇杆,您可以使用两个模拟摇杆进行此操作)驾驶机器人。每个滑块控制一个电机的速度。虽然这听起来很复杂(不像开车),但通过一点练习,这是一种优雅地驾驶两轮机器人的方式。滑块离中间越远,相关的电机速度越快。我们还将确保如果通信丢失,机器人电机将在一秒后停止。

这种左右速度的控制与您在整本书中一直在使用的相同控制系统,但已经变得交互式。下一张图显示了典型移动所需的一些动作:

图片

图17.7 – 两个滑块上的常见动作

图17.7 中的红色点代表您的拇指触摸屏幕的位置。通过同时向前滑动,机器人将向前行驶,而且您滑得越远,它行驶得越快。向后动作将它们都向后滑动。要旋转机器人,将它们向相反方向滑动。要向前行驶并稍微向左或向右,同时向前滑动,但将右侧滑块稍微抬得比左侧高。您还可以为此方向进行补偿。

我们有一个很好的用户界面设计。为了开始构建这个,我们将规划所需的代码块,并编写使它们在现实世界中工作的代码。

为远程驾驶准备Raspberry Pi——启动基本驾驶系统

我们的Raspberry Pi已经能够运行网络服务,使用Flask创建菜单服务器和视频服务器。我们可以使用图像和控制队列使行为与网络服务器交互。我们将重用这些功能。在手机应用中,滑块控制需要智能。下一张图显示了我们的手动驱动系统的各个部分:

图片

图17.8 – 手动驱动应用系统概述

图17.8 中的虚线框显示了代码的运行位置,顶部的虚线框是手机上运行的代码,下方的框是机器人在Raspberry Pi上运行的代码。在虚线框内部,带有实线轮廓的框是代码块或系统,我们的代码将需要它们。在图17.8 的底层,机器人框接受停止电机设置电机速度的调用。这些来自行为框,基于超时或来自Flask Web服务器控制消息队列。同时,行为循环也将从摄像头获取图像帧,对它们进行编码并将它们推送到显示帧队列

上一个层次是Flask Web服务器。该服务器消耗显示帧队列,为多部分的图像流提供帧。Flask服务器将处理控制请求并将它们推送到控制消息队列

页面脚本处理滑块更新,并使用jQuery库将它们转换为控制请求滑块小部件触摸转换为滑块更新(它将进行动画转换)。

页面本身使用img标签来显示视频流,就像之前一样,并放置滑块小部件。退出按钮会发出控制请求。

页面脚本滑块小部件将需要JavaScript和层叠样式表CSS)编程。在我们开始之前,我们需要从第14章Python中的摄像头跟随线中获取图像核心,并添加更多功能以将代码发送到浏览器。

增强图像应用核心

要构建这个,我们将首先添加一些静态文件链接,并重用我们在第14章Python中的摄像头跟随线中使用的图像应用核心。

静态文件不会让机器人做任何事情;系统被动地提供服务。我们将提供JavaScript和CSS,以及jQuery库的本地副本。Flask会自动完成这项工作。

让我们按照以下方式设置静态文件文件夹:

  1. 创建一个static文件夹。我们将把JavaScript和CSS代码放在这个static文件夹中。

  2. 我们将制作jQuery库的本地副本。在static下创建一个lib目录。

  3. https://code.jquery.com/jquery-3.5.1.min.js下载jQuery,按浏览器lib文件夹。你应该有一个static/lib/jquery-3.5.1.min.js文件。

  4. image_app_core.py文件中,我们还需要使用缓存文件来停止它,以便它重新加载我们的CSS和JavaScript文件,如下所示:

    @app.after_request
    def add_header(response):
        response.headers['Cache-Control'] = "no-cache, no-store, must-revalidate"
        return response
    

应用核心现在有一个可以离线使用的jQuery静态副本,所以我们的手机不需要依赖良好的信号与机器人通信。

编写手动驾驶行为

接下来我们需要的是行为。它基于之前代码中看到的第14章Python中的摄像头跟随线的概念,通过控制消息改变电机速度和纯视频输出。

这个系统将有一个超时——如果1秒内没有控制消息到达,它将停止驾驶。看到机器人开到远处或桌子上是很令人沮丧的,所以如果没有任何有意义的事情发生,它将恢复停止。

让我们按照以下方式构建它:

  1. 以类似以下方式开始一个名为manual_drive.py的文件,导入摄像头和控制:

    import time
    from robot import Robot
    from image_app_core import start_server_process, get_control_instruction, put_output_image
    import camera_stream
    
  2. 我们可以声明我们想要的超时阈值是多少秒,如下面的代码片段所示:

    TIMEOUT_IN_SECONDS = 1
    
  3. 我们将创建一个ManualDriveBehavior类。在这个类中,我们将存储一个robot对象并跟踪时间,如下面的代码片段所示:

    class ManualDriveBehavior(object):
        def __init__(self, robot):
            self.robot = robot
            self.last_time = time.time()
    
  4. 接下来,构建这个行为的控制部分。它为每个指令重置最后时间。代码可以在下面的代码片段中看到:

    self.handle_instruction.
    
  5. 我们的代码在handle_instruction中处理指令。这个指令是一个字典,其中指令名称和参数是其成员。我们可以检查这个命令是否是set_leftset_right,如下面的代码片段所示:

    int to convert it into an integer number for our motors. 
    
  6. 我们还需要处理exit命令,如下所示:

            elif command == "exit":
                print("stopping")
                exit()
    
  7. 至少在测试时,知道我们是否有未知的指令会有用。让我们通过抛出异常来处理这种情况,如下所示:

            else:
                raise ValueError(f"Unknown instruction: {instruction}")
    
  8. 我们的应用程序还需要创建一个显示,将帧放在服务器图像队列中,如下所示:

        def make_display(self, frame):
            encoded_bytes = camera_stream.get_encoded_bytes_for_frame(frame)
            put_output_image(encoded_bytes)
    
  9. 该行为有一个run方法来执行设置和主循环。我们首先将俯仰设置成直视前方,预热相机,并停止伺服电机,如下所示:

        def run(self):
            self.robot.set_pan(0)
            self.robot.set_tilt(0)
            camera = camera_stream.setup_camera()
            time.sleep(0.1)
            self.robot.servos.stop_all()
            print("Setup Complete")
    
  10. 然后,我们遍历来自相机的帧并处理控制指令,如下所示:

            for frame in camera_stream.start_stream(camera):
                self.make_display(frame)
                self.process_control()
    
  11. 最后,我们根据超时自动停止,如下所示:

                if time.time() > self.last_time + TIMEOUT_IN_SECONDS:
                    self.robot.stop_motors()
    
  12. 我们添加顶层代码来创建和启动组件,如下所示:

    print("Setting up")
    behavior = ManualDriveBehavior(Robot())
    process = start_server_process('manual_drive.html')
    
  13. 我们仍然想确保在退出或遇到错误时停止服务器,所以我们运行以下代码:

    try:
        behavior.run()
    except:
        process.terminate()
    

行为后端已完成,但需要一个模板来查看它,以及运行在手机上的样式和代码。

模板(网页)

模板是我们放置滑块和部分处理它们的代码的地方。

让我们开始,如下所示:

  1. 创建一个templates/manual_drive.html文件。从HTML前缀开始,如下所示:

    <html>
        <head>
    
  2. 我们希望显示适应手机屏幕,根据显示大小调整。我们也不希望用户的触摸交互意外地缩放显示。这一行代码告诉浏览器这是我们的意图:

            <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    
  3. 我们希望对这个(以及可能的其他界面)进行样式设计。为此,我们使用一个display.css样式表,如下面的代码片段所示:

            <link rel="stylesheet" type="text/css" href="/static/display.css?">
    
  4. 我们将使用jQuery库来使事物交互,并构建一个触摸滑块系统。这些是HTML的导入等效:

            <script src="img/jquery-3.5.1.min.js"></script>
            <script src="img/touch-slider.js?"></script>
    

    我们将在这个文件中放置一个非常具体的样式。其余的来自样式表。我们希望这个行为的视图占据整个屏幕,并且不滚动。代码如下所示:

            <style>html, body {margin: 0; height: 100%; overflow: hidden}</style>
    
  5. 头部以一个标题结束,该标题将显示在标签页的顶部,如下所示:

            <title>Manually Drive The Robot</title>
        </head>
    
  6. 我们现在从第一个滑块开始构建主体;我们使用svg标签来定义它,我们将用它来制作滑块轨迹,如下面的代码片段所示:

    slider_track class lets us style both tracks—we use HTML classes to identify multiple objects. The left_slider ID will help us position and add touch events to it. IDs in HTML are usually used to reference one object uniquely.The `viewBox` attribute defines the dimensions of the drawing internal to `svg` as lower *x*, lower *y*, width, and height. View box coordinates make sense even if we scale the `svg` element for a different device. The height range is -100/100, equivalent to the motor speeds, and the width range is -10 to +10.
    
  7. 在容器内部,我们将画一个圆。这个圆需要一个半径r,我们可以用视图框单位给出。中心在两个方向上都是0。这个代码如下所示:

                <circle r="18" class="slider_tick"/>
    

    圆的颜色将来自样式表。

  8. 接下来,我们需要一个退出链接来完成行为。它有一个类和ID来定义其样式,如下面的代码片段所示:

    button class with fonts and colors, and we can use the style for other buttons (for example, to enhance the menu app). We'll use the exitbutton ID to position this in the place we designed before.
    
  9. 接下来,我们有我们的视频块。视频的img标签包含在一个div标签内,以保持我们的视频在任何尺寸屏幕上的比例,同时允许它调整大小以适应空间,如下面的代码片段所示:

            <div id="video"><img src="img/{{ url_for('display') }}" /></div>
    
  10. 右侧的滑块是左侧的重复,只是ID不同。你可以复制并粘贴左侧的代码,更改ID。代码可以在这里看到:

            <svg id="right_slider" class="slider_track" viewBox="-10 -100 20 200">
                <circle r="18" class="slider_tick"/>
            </svg>
    
  11. 我们需要在我们的HTML中添加一些JavaScript代码来处理滑块。页面上的代码将滑块代码与我们的图形和电机链接起来。首先,我们声明JavaScript块,如下所示:

            <script type="text/javascript">
    
  12. 添加一个函数来发送电机控制到机器人。它接受一个名称(左或右)和一个速度,如下面的代码片段所示:

                function set_motor(name, speed) {
                    $.post('/control', {'command': 'set_' + name, 'speed': speed});
                }
    

    我们将这个控制指令POST到服务器。

  13. 下面的代码必须在页面完成加载后运行;我们想确保前面的JavaScript库已经完全加载。jQuery有一个特殊函数$(),当页面完成加载时,它会运行传递给它的任何函数,如下面的代码片段所示:

                $(() => {
    
  14. 我们需要将退出按钮链接到一个POST请求,完成后将转发到菜单,如下面的代码片段所示:

                    $('#exitbutton').click(function() {
                        $.post('/control', {'command': 'exit'});
                        window.location.replace('//' + window.location.hostname + ":5000");
                    });
    
  15. 我们设置了滑块并将它们与它们的svg元素ID和set_motor链接起来,以便每次它们改变时都会更新,如下面的代码片段所示:

    makeSlider, which uses id for the ID of the object we are turning into a slider (a svg track), and a function to call when the slider has changed.
    
  16. 现在,我们通过关闭所有标签来结束我们的页面,如下所示:

            </script>
        </body>
    </html>
    

这个页面没有样式;默认情况下,视频和滑块没有形状、大小或颜色——所以,如果你尝试加载这个,它将显示一个空白页面。我们还没有告诉浏览器我们想在页面上放置什么或使用什么颜色。我们还没有滑块的代码。

我们已经编写了发送退出按钮并将滑块与标签链接的代码。在下一节中,我们将添加一个样式表来使事物可见。

样式表

我们现在可以给我们的应用添加一些样式。样式表需要时间来调整和精确设置,所以这只是一个展示它能做什么的例子。如果你认为我的颜色选择很糟糕,请随意替换你自己的;我建议使用w3c颜色,在https://www.w3schools.com/colors/default.asp。你可以使用命名或十六进制(#1ab3c5)颜色。

CSS的本质是在页面上选择元素并将样式属性与它们关联。CSS样式部分以一个#标记开始。要全面了解CSS选择器,请参阅进一步阅读部分。每个部分使用花括号{}来界定一个样式部分。一个部分的样式由属性名、一个冒号:和一个设置组成。一个分号;跟在这些设置之后,以结束每个设置。

让我们定义样式,如下所示:

  1. 创建一个static/display.css文件来保存这些样式信息。

    我们可以将我们的滑块轨道设置为视口宽度的10%——即屏幕大小的10%。CSS有一个特殊的单位vw用于此,还有vh用于视口高度的百分比。在进一步阅读部分查看关于CSS单位的说明。此代码使用.slider_track CSS选择器,它适用于具有该类的所有对象。两个滑块都有这个类,所以这里的更改会影响它们两个。代码如下所示:

    .slider_track {
        width: 10vw;
        height: 90vh;
    
  2. 我们将给滑块轨道添加一个实心蓝色边框和浅蓝色背景,以匹配我们的原型,如下所示:

        border: 1px solid blue;
        background-color: lightblue;
    }
    
  3. 为了样式化勾选标记,即我们在滑块上看到的圆圈,我们可以添加一个淡粉色填充色,就像我们的原型一样,如下所示:

    .slider_tick {
        fill: mistyrose;
    }
    
  4. 接下来,我们想要将滑块(通过它们的ID)定位在左侧和右侧。当制作一个与屏幕原型非常接近的显示时,我们可以使用绝对定位和视口百分比来确切地说明事物应该在哪里,如下所示:

    #left_slider {
        position: absolute;
        left: 5vw;
        top: 5vh;
    }
    #right_slider {
        position: absolute;
        right: 5vw;
        top: 5vh;
    }
    
  5. 你现在可以通过上传它,停止运行行为,再次启动,然后重新加载来尝试这个。滑块看起来更好,但退出按钮和视频位置不正确。

  6. 让我们使退出按钮更像一个按钮。.button下的样式将应用于具有相同类的所有按钮。我们将使其成为一个块——一个使用宽度和高度属性的元素。这个块是视口高度的10%。代码如下所示:

    .button {
        display: block;
        height: 10vh;
    
  7. 然后,我们使用line-heighttext-align将文本居中,然后使用2em表示正常文本大小的两倍,如下所示:

        text-align: center;
        font-size: 2em;
        line-height: 10vh;
    
  8. 我们想要移除按钮文本的下划线,这通常是链接中出现的样式。我们还会给它添加一些颜色,一个蓝色背景和白色文本,如下所示:

        text-decoration: none;
        background-color: blue;
        color: white;
    }
    
  9. 我们使用退出按钮的ID来指定更多关于它的样式。我们将设置其宽度和顶部位置,但使用auto边距来居中它,如下所示:

    #exitbutton {
        width: 40vh;
        margin-top: 5vh;
        margin-left: auto;
        margin-right: auto;
    }
    

    尝试这个,你现在应该能看到退出按钮在正确的位置。

  10. 接下来,我们样式化视频。我们希望将视频居中在屏幕上。外部视频元素可以为我们做到这一点,如下所示:

    .video {
      text-align: center;
    }
    
  11. 然后,我们可以指定内部图像块的定位和大小。我们希望它从屏幕顶部20%的位置开始,使用vh单位。vmin单位是屏幕最小维度的百分比;它确保这个块永远不会太大,以至于会遮挡两个滑块条。我们使高度自动缩放。我们选择#video img来将此样式应用于video对象中包含的img对象,如下面的代码片段所示:

    #video img {
        margin-top: 20vh;
        width: 80vmin;
        height: auto;
    }
    

我们的页面已经完全样式化。你现在可以尝试一下,看看它看起来如何。将整个文件夹(包括模板)上传到机器人,然后运行python3 manual_drive.py。将桌面浏览器指向http://myrobot.local:5001/,用你的机器人的主机名或地址替换,以查看它。桌面浏览器非常适合发现HTML或JavaScript代码中的错误。在撰写本文时,Firefox和Chrome支持在浏览器中模拟移动设备和触摸事件。它应该看起来像以下截图所示的带有真实视频的模拟:

图17.9 – 手机上运行的应用程序截图

图17.9展示了在真实手机上运行的应用程序。滑块栏目前还没有任何功能。请注意,你可能需要强制浏览器重新加载样式表。

现在我们需要添加滑块代码。

创建滑块的代码

滑块需要响应用户的触摸事件,将圆圈移动到触摸位置,并发送一个更新消息来显示这次移动距离中间有多远。当触摸事件停止时,滑块将自动返回中心。JavaScript允许我们在浏览器中运行代码,因此我们将创建一个名为makeSlider的JavaScript函数。

首先,我们想看看触摸如何转换为滑块位置和电机速度。以下图表展示了这一点:

图17.10 – 从触摸事件到电机位置

我们的滑块在位置上有些复杂性,如图17.10所示。当用户触摸屏幕时,位置以屏幕坐标的形式到达。我们首先需要通过减去滑块的上边坐标来找到它在滑块中的位置。我们需要将这个结果除以滑块的高度,乘以200,然后减去100,以得到视图框位置(与绘制SVG使用的相同系统)。在视图框坐标中,顶部是-100,但为了我们的电机向前移动,我们需要+100,因此我们必须取视图框位置的负值以获得电机速度。

我们的脚本将设置移动滑块所需的数据,以及将内部函数映射到滑块事件、管理滑块的运动,并在我们移动滑块时调用manual_drive.html代码(或任何其他代码)。让我们编写滑块代码,如下所示:

  1. 我们将把这个放在static/touch-slider.js中。由于我们处于一个.js文件中,不需要<script>标签。

  2. 我们创建了一个makeSlider函数,这是一个工厂函数,用于创建滑块所需的一切,如下所示:

    function makeSlider(id, when_changed) {
    
  3. 我们首先需要一些内部数据。代码需要知道我们是否在触摸滑块,这样它就不会在我们仍然触摸时尝试移动回去。我们需要知道触摸位置是否已改变,并跟踪其位置。最后,我们将通过其ID找到我们的滑块,并保留找到的对象以供使用,如下所示:

        let touched = false;
        let changed = false;
        let position = 0;
        const slider = $('#' + id);
    
  4. 然后,我们需要一些函数来处理滑块。我们将从一个更新位置的函数开始,确保刻度被更新,我们只使用整数(因为浏览器在这里不接受小数点),并且更新changed标志,如下面的代码片段所示:

        const set_position = function(new_position) {
            position = Math.round(new_position);
            slider.find('.slider_tick')[0].setAttribute('cy', position);
            changed = true;
        };
    
  5. 下一步是处理触摸事件。事件处理器是在发生某些事情时被调用的函数(例如退出按钮处理器)。触摸事件有三个事件:touchstart——当有人开始触摸屏幕时,touchmove——当触摸移动到另一个区域时,和touchend——当触摸停止时。我们不会使用touchstart,所以我们将从一个匿名的touchmove函数开始,如下所示:

    touch variable from the event data. We get a list of touches, but we are only using the first one.
    
  6. 然后,我们从这个触摸从滑块顶部的相对位置获取,如下所示:

            let from_top = touch.pageY - slider.offset().top;
    
  7. 我们可以用这个高度将触摸位置转换为从-100到+100的数字,匹配SVG视图框坐标,如下所示:

            let relative_touch = (from_top / slider.height()) * 200;
            set_position(relative_touch - 100);
    
  8. 由于代码已经接收到触摸事件,我们应该将touched标志设置为true。我们还必须防止触摸事件产生任何其他效果,如下面的代码片段所示:

            touched = true;
            event.preventDefault();
        });
    
  9. 由于我们设置了一个标志来表示触摸事件正在发生,因此当触摸事件结束时,我们也应该清除它(将其设置为false),如下所示:

        slider.on('touchend', event => touched = false);
    
  10. 我们的系统是动态的,因此它需要一个更新周期来返回中间位置。更新应该只在没有触摸滑块时移动刻度,这样它就会停留在你放置拇指的位置。当触摸停止且它仍然不在零位置时,我们应该更新位置,如下所示:

        const update = function() {
            if(!touched && Math.abs(position) > 0) {
    
  11. 下一个部分看起来有点像比例-积分-微分PID)控制器的代码,因为这里有一个乘以比例组件的错误。我们将错误乘以0.3,并额外加/减0.5以使其接近1%的最小值。每次更新时,它都会将滑块移动到中间位置。代码可以在以下位置查看:

                let error = 0 - position;
                let change = (0.3 * error) + (Math.sign(error) * 0.5);
                set_position(position + change);
                // console.log(id + ": " + position);
            }
        };
    

    这段代码也是一个很好的记录位置的地方——当它出错时我们可以使用。

  12. 为了频繁运行这个update函数,我们可以使用setInterval内置函数,它在每个间隔内重复运行一个函数。这个显示更新应该很短,以保持其响应性。时间以毫秒为单位。代码可以在以下位置查看:

        setInterval(update, 50);
    
  13. 除了更新图像外,我们还需要调用when_changed函数。我们只想在发生变化时这样做,然后重置changed标志,这样我们就不在空闲时调用它。我们将调用这个update_when_changed。它检查变化,并且比显示更新运行得更频繁,因此不会淹没when_changed处理器和机器人上的队列。代码可以在以下位置查看:

    makeSlider function. 
    

现在,你应该准备好运行整个系统了。

运行这个

现在,你可以将整个文件集上传到你的机器人上的一个文件夹中。和之前一样,你可以使用python3 manual_drive.py来运行它。

在尝试在手机上使用之前,你可以在浏览器中使用开发者模式查看网页,如下所示:

  1. 将你的浏览器(Chrome或Firefox)指向http://myrobot.local:5001(使用你的机器人的主机名或地址)。

  2. 右键点击你的页面,然后点击标有检查或检查元素的菜单项。

  3. 在开发者工具中,将有模拟手机设备和触摸事件的按钮。启用手机模拟。

  4. 首先尝试在桌面浏览器中排除任何问题。检查拖动滑块是否得到期望的结果,并点击控制台按钮查看是否有JavaScript错误。

    JavaScript和CSS中常见的常见问题是缺少标点符号,如分号、逗号或括号。如果类或ID选择器不匹配(或缺少所需的点/井号语法),将导致样式无法应用或JavaScript中的元素查找没有结果。

  5. 要在手机上使用,你需要使用你的机器人的IP地址,因为主要的智能手机品牌不支持.local地址。你可以通过桌面上的ping myrobot.local找到它,如下面的代码片段所示:

    192.168.1.107. Your address will be different; note that down, and put that in the phone browser with the port. An example for my robot is http://192.168.1.107:5000.
    
  6. 使用手机,你应该能够用你的拇指来驾驶机器人。

驾驶机器人需要一些练习。我建议先练习从上往下驾驶,当你熟悉了这一点后,再尝试通过摄像头导航。摄像头的帧率不是很高,这个帧率目前限制了驾驶循环。

故障排除

这是一个相当复杂的Python、HTML、JavaScript和CSS的组合。如果你遇到麻烦,可以尝试以下方法:

  • 如果看到Python的错误,请对照前面的代码验证代码行。

  • 如果网页上的事情不起作用,尝试在浏览器模式中运行手机模拟,如之前建议的,然后选择检查器控制台选项卡并再次尝试操作。这将显示JavaScript错误。

  • 如果显示出现错误,部分内容位置不正确或颜色错误,请验证CSS/样式表部分和HTML是否正确。

  • 如果你收到404错误,请确保HTML中的URL与Flask/Python代码中的路由相匹配。

  • 如果你的机器人似乎在暂停,然后花一段时间赶上你的事件,你可以将update_if_changed间隔时间调整得更长一些。

现在,你有一个可以通过手机远程驾驶的机器人,同时通过其摄像头看到。你已经看到了如何处理触摸事件和使用SVG样式表来创建自定义小部件。你已经使用JavaScript通过动画使小部件生动起来,并发送控制消息回机器人。

在下一节中,我们将使菜单更易于触摸操作,这样我们就可以主要通过手机来控制机器人。

使机器人完全手机可操作

此处的目标是实现我们可以完全通过手机来控制机器人。我们需要确保在开启机器人时,机器人已经准备好运行,并确保菜单可以从手机上使用。我们之前制作的菜单看起来不太适合触摸操作。它也无法成功运行使用 Flask 的任何带有显示的行为。我们将使菜单按钮更大,并使其更易于触摸操作,使用与我们手动驾驶行为相似的样式。点击具有服务器(如本例或上一章的视觉跟踪行为)的行为后,菜单还将加载我们的服务器页面。

让我们先修复 Flask 行为。

使菜单模式与 Flask 行为兼容

如果您已经在控制服务器中尝试运行基于 Flask 的行为(如带有摄像头的那些),您将注意到一些非常奇怪的行为。您的行为似乎在机器人的传感器上做了正确的事情,但 Web 服务在 5001 端口上无法执行任何有用的操作。

Flask 使用子进程来管理其调试模式,这干扰了我们对它们的用法。我们不需要调试模式,所以修复方法是删除调试模式,如下所示:

  1. 打开 control_server.py 并跳转到最后几行。

  2. 通过运行以下代码从 app.run 行中移除 debug=True

    app.run(host="0.0.0.0")
    

现在,您可以向控制服务器添加手动驾驶、颜色跟踪和面部跟踪行为,并且它们将正常启动。

加载视频服务

当我们点击基于视频服务器的行为菜单选项后,在它启动后,我们需要将我们的浏览器发送到机器人的 5001 端口以查看其输出。

我们当前的 menu.html 文件从 control_server 进程获取响应并将其放入消息框中。我们可以升级它,让代码执行其他操作。我们可以从配置 mode_config 变量中需要显示服务器页面的项目开始。

mode_config 变量中的每个条目只包含模式脚本;我们可以将其更新为包含脚本以及是否需要显示服务器,如下所示:

  1. 打开 robot_modes.py

  2. mode_config 中,我们将用字典替换简单的脚本命名文本(例如 "avoid_behavior.py"),允许简单的案例({"script": "avoid_behavior.py"})或更复杂的案例({"script": "manual_drive.py", "server": True})。您需要在整个 mode_config 中的所有条目上更改这一点。代码如下面的代码片段所示:

        mode_config = {
            "avoid_behavior": {"script": "avoid_behavior.py"},
            "circle_head": {"script": "circle_pan_tilt_behavior.py"},
            ...
    
  3. 然后,我们需要使用更复杂的案例更新 mode_config 变量中的服务器类型脚本,如下所示:

            manual_drive configuration here, you need to add this to menu_config variable too so that it shows up on the menu.
    
  4. 我们需要修改 run 方法以从这种更改后的结构中选择脚本,如下所示:

        def run(self, mode_name):
            while self.is_running():
                self.stop()
            script = self.mode_config[mode_name]['script']
            self.current_process = subprocess.Popen(["python", script])
    
  5. 接下来,我们需要检查如果模式是服务器并且当前进程处于活动状态,我们是否应该进行重定向。我在以下代码片段中添加了明确的 is True,以使其更清晰,表明该值是一个 True/False 标志。

        def should_redirect(self, mode_name):
            return self.mode_config[mode_name].get('server') is True and self.is_running()
    

我们已经准备了robot_modes.pycontrol_server.py文件向网页发送响应。让我们使用与mode_config相同的技巧,返回一个包含数据的字典而不是只是一个字符串,如下所示:

  1. 我们将使用control_server.py并在Flask导入中添加jsonify,如下面的代码片段所示:

    from flask import Flask, render_template, jsonify
    
  2. 接下来,我们替换run方法,使其创建response字典,如下所示:

    @app.route("/run/<mode_name>", methods=['POST'])
    def run(mode_name):
        mode_manager.run(mode_name)
        response = {'message': f'{mode_name} running'}
    

    这个响应是基本消息。

  3. 如果我们打算重定向,我们应该在我们的响应中发送redirect设置,如下所示:

        if mode_manager.should_redirect(mode_name):
            response['redirect'] = True
    
  4. 我们需要发送编码为JSON的响应。JSON是将数据从Python发送到JavaScript的一种简单方式——它与字典数据特别好。运行以下代码:

        return jsonify(response)
    
  5. 由于我们在stop命令中也发送了消息,我们应该以相同的方式将其包装起来,如下所示:

    @app.route("/stop", methods=['POST'])
    def stop():
        mode_manager.stop()
        return jsonify({'message': "Stopped"})
    

控制服务器能够发送response字典,并在需要时进行重定向。现在,接收JSON对象的那一侧,需要在页面脚本中进行更改以处理新的响应。按照以下步骤进行:

  1. 打开templates/menu.html,找到run函数,如下面的代码片段所示:

        function run(url) {
    
  2. 这里消息处理需要更改。我们需要使用响应中的消息元素设置消息元素的HTML,如下所示:

          $.post(url, '', response => {
               $('#message').html(response.message);
    
  3. 然而,我们也可以检查是否需要重定向。如果是这样,我们使用与之前模板部分中手动驾驶行为中的退出按钮相同的技巧,但在超时中进行,如下所示:

    setTimeout function calls a function after a specified time. We give it 3,000 milliseconds (3 seconds), which gives a video behavior time to warm up first.
    

如果你上传这个并运行python3 control_server.py,你会看到菜单现在功能更强大,但看起来相当简单。你应该能够点击跟踪或驾驶行为,3秒后重定向到它们的页面。点击退出按钮应该会带你回到菜单。

是时候给它一些样式了。

菜单样式

我们已经在手动驾驶演示中使用了样式表来使退出按钮看起来更好。这个菜单也是一组按钮。我们可以在此基础上构建样式,使菜单更适用于手机。

将菜单模板转换为按钮

我们已经在static/display.css中有一个现有的样式表。我们可以在菜单中进一步使用这个样式表,也许需要一些调整。我们的菜单模板也可以优化以充分利用这个样式表。按照以下步骤进行:

  1. 打开templates/menu.html。我们将添加一个链接到样式表。我们还可以添加一个charset定义,如下所示:

    <head>
        <script src="img/jquery-3.5.1.min.js"></script>
        <title>My Robot Menu</title>
        <meta charset="UTF-8">
        <link rel="stylesheet" type="text/css" href="/static/display.css">
    </head>
    
  2. 菜单模板使用一个项目列表。给这个列表添加一个menu类,给链接添加一个button类,这样我们就可以使用它们现有的样式,如下面的代码片段所示:

        <ul class="menu">
          {% for item in menu %}
            <li>
                <a class="button" href="#" onclick="run('/run/{{ item.mode_name }}')">
                    {{ item.text }}
                </a>
            </li>
          {% endfor %}
        <li><a class="button" href="#" onclick="run('/stop')">Stop</a></li>
    
  3. 现在,打开static/display.css,我们将在这里定义menu类的样式,如下所示:

    .menu {
        width: 100%;
        margin-top: 0;
        margin-bottom: 0;
        padding: 0;
    }
    

    我们使列表容器填充屏幕宽度,没有任何额外的边距(项目外部的空间)或填充(项目内部和其子列表项之间的空间)。

  4. 菜单由列表项组成。默认情况下,这些项会带有一个点:一个项目符号。我们希望将其设置为none(无形状)以移除项目符号。我们可以使用CSS的list-style属性来更改它。这里的选择器适用于.menu类对象的子列表项(li)。代码可以在下面的代码片段中看到:

    .menu li {
        list-style-type: none;
        list-style-position: initial;
    }
    
  5. 为了使其触控友好,我们使按钮宽度相同。60vw(视口宽度的60%)应该足够宽。我们使用auto边距技巧来居中。我们还可以在它们上添加一个1像素的浅蓝色边框,如下面的代码片段所示:

    .menu .button {
        margin-left: auto;
        margin-right: auto;
        width: 60vw;
        border: 1px solid lightblue;
    }
    

上传整个目录并使用python3 control_server.py启动菜单服务器。现在这个菜单应该看起来更符合手机。

你现在已经看到了如何使我们的控制服务器在智能手机上运行良好,你应该对JavaScript、HTML和CSS与Python的交互更加熟悉。然而,这个系统有一个缺陷——我们仍然是从SSH终端启动它的。让我们看看如何解决这个问题。

让菜单在Pi启动时启动

你现在有一个菜单系统可以启动机器人行为。使用SSH登录是很好的调试方式,可以看到问题并修复它们。然而,当你想展示你的机器人时,SSH会话会变得不方便。

理想的情况是打开机器人,等待灯光亮起,然后将你的手机浏览器指向它来控制它。

我们将做两件事来使这个功能变得有用,如下所示:

  • 使用LED指示它已准备好(在菜单模式下),以便在手机连接到页面之前机器人能告诉我们

  • 使用systemd在机器人开启时自动启动菜单Flask服务器

让我们开始使用灯光。

向菜单服务器添加灯光

我们不希望整个机器人类加载到我们的菜单中,但它可以使用灯光来指示我们的机器人现在已准备好。我们将导入LED系统,在服务器启动时将其打开,然后在第一个/run请求到达时关闭/释放它。按照以下步骤操作:

  1. 打开control_server.py文件并导入LED,如下所示:

    from robot_modes import RobotModes
    from leds_led_shim import Leds
    
  2. 我们需要设置我们的LED并运行以下代码来使一个LED变绿:

    mode_manager = RobotModes()
    leds = Leds()
    leds.set_one(1, [0, 255, 0])
    leds.show()
    
  3. 当我们运行某些内容时,我们知道有人使用了菜单。在我们的run方法中,我们可以清除LED。由于我们只想做一次,我们可以将全局LED设置为None,然后在下次检查。注意以下代码片段中我们正在将高亮代码插入到现有的run函数中:

    def run(mode_name):
        global leds
        if leds:
            leds.clear()
            leds.show()
            leds = None
    ...
    

你可以通过上传菜单服务器代码并重新运行它来测试这一点。LED应该在启动时亮起,然后当你选择另一个行为时,它会熄灭。它应该能够正确地从菜单移动到LED测试行为。

使用systemd自动启动机器人

树莓派中的systemd

通过创建一个单元文件并将其复制到Pi的正确文件夹中来注册一个服务。按照以下步骤操作:

  1. 创建一个 menu_server.service 文件。用描述来启动它,并告诉 systemd 在我们的 Raspberry Pi 上建立网络后启动我们的服务,如下面的代码片段所示:

    [Unit]
    Description=Robot Menu Web Service
    After=network.target
    
  2. 现在,我们告诉 systemd 我们希望在 Pi 准备好用户登录时启动它,如下面的代码片段所示:

    [Install]
    WantedBy=multi-user.target
    
  3. 以下代码片段中显示的 Service 部分配置了如何运行我们的代码:

    [Service]
    
  4. 工作目录是你将机器人文件复制到的位置——例如,/home/pi。我们还可以设置我们一直在使用的 pi 用户。工作目录是代码找到其其他组件的方式。请查看以下代码片段:

    WorkingDirectory=/home/pi
    User=pi
    
  5. ExecStart 语句告诉 systemd 运行服务的命令。然而,它并不像 shell 那样假设路径,所以请在 python3 命令前加上 /usr/bin/env,如下所示:

    ExecStart=/usr/bin/env python3 control_server.py
    
  6. 你现在需要在 Raspberry Pi 上设置这个。将此文件上传到你的 Raspberry Pi 主目录。

  7. 你需要 sudo 权限将其复制到系统配置中。请在 Pi 上通过 SSH 输入此命令。注意,如果你遗漏了 sudo 命令,你会看到权限错误。代码可以在以下位置查看:

    $ sudo cp menu_server.service /etc/systemd/system/
    
  8. 我们现在应该要求 systemd 加载我们的配置,然后启用我们的服务,如下所示:

    $ sudo systemctl daemon-reload
    $ sudo systemctl enable menu_server
    
  9. 系统将确认你已经启用它,如下所示的消息:

    Created symlink /etc/systemd/system/multi-user.target.wants/menu_server.service → /etc/systemd/system/menu_server.service.
    
  10. 然后,你可以使用以下命令尝试启动你的服务:

    $ sudo systemctl start menu_server
    

如果启动此服务器成功,你会看到一个绿灯亮起,表示它已准备好。然后你将能够将浏览器指向机器人并控制它。

让我们检查一下这是否已经成功。

故障排除

这里可能会出错——如果发生这种情况,请尝试以下步骤来修复它或了解更多信息:

  1. 使用 systemd 启动/启用菜单服务器可能会失败,如果你发现 menu_server.service 文件有问题,你会看到 Unit menu_server.service is not loaded properly: Invalid argument。请验证其内容,将其复制回来,然后重新运行 sudo 命令来安装新文件。

  2. 如果你想要看到服务器正在做什么的更多信息,你可以使用以下命令:

    $ systemctl status menu_server
    

    Pi 将会响应如下:

    ● menu_server.service - Robot Menu Web Service
      Loaded: loaded (/etc/systemd/system/menu_server.service; enabled; vendor preset: enabled)
       Active: active (running) since Wed 2020-10-21 23:41:55 BST; 2s ago
     Main PID: 1187 (python3)
        Tasks: 1 (limit: 860)
       Memory: 10.0M
       CGroup: /system.slice/menu_server.service
               └─1187 python3 control_server.py
    Oct 21 23:41:55 myrobot systemd[1]: Started Robot Menu Web Service.
    Oct 21 23:41:56 myrobot env[1187]:  * Serving Flask app "control_server" (lazy loading)
    Oct 21 23:41:56 myrobot env[1187]:  * Environment: production
    Oct 21 23:41:56 myrobot env[1187]:    WARNING: This is a development server. Do not use it in a production deployment.
    
  3. systemctl 可以显示一些最近的活动,但你可能想跟踪运行中的行为输出。为此,你需要使用 journalctl 命令。使用 -u 来指定我们创建的服务,然后使用 -f 来跟踪日志,如下面的代码片段所示:

    $ journalctl -u menu_server -f
    

    我们将能够看到服务器在运行时的状态——这可能不是调试中最方便的,但对于启动服务来说很方便。使用 Ctrl + C 来停止查看日志。

你现在可以重新启动机器人,等待绿灯亮起,然后开始驾驶它。绿灯也会意味着你的 Mycroft 语音助手可以向机器人发送请求。

如果你上传了新的代码,你需要重新启动服务。你可以使用以下命令来这样做:

$ sudo systemctl restart menu_server

恭喜——你的机器人现在真正无头了!它甚至不需要 PC 或笔记本电脑来开始做事。

摘要

本章为我们的机器人添加了一个小型菜单系统,可以从连接的网页浏览器中启动不同的模式。

你已经看到了如何从手机驾驶机器人,以及如何使用 SVG 和 JavaScript 创建有趣的动画小部件。

你的机器人现在已经获得了手动驾驶的能力。你可能需要一段时间来习惯操作它,手动纠正偏航(电机行为略有不同)比 PID 系统自行纠正更具挑战性。尽管如此,你将学会用手机驾驶它。你可以使用机器人前部的摄像头来获得机器人视角的世界。

你已经将控制服务器变成了菜单服务器,并在打开机器人时自动启动它。你还看到了如何将你的菜单服务器连接到视频服务器应用程序,如手动驾驶、颜色追踪或人脸追踪应用程序。通过使菜单服务器上的按钮更易于触摸,你可以使用手机启动大多数行为。

最后,我们为菜单服务器提供了一个用 LED 指示机器人在机器人上就绪的方法,并将其设置为在打开机器人时自动启动。如果你的机器人和手机可以连接到同一网络(也许你可以在 wpa_supplicant.conf 文件中设置你的手机热点),你将能够从实验室外的地方启动行为并向人们展示它们。你已经用手机完全控制了机器人!

在下一章中,我们将探讨如何加入机器人制作社区,并寻找更多的机器人构建和编程技能,以继续构建。

练习

你可以通过许多方式增强系统。以下是一些进一步构建的建议:

  1. manual_drive.py 文件中,handle_instruction 函数使用一系列 if 语句来处理指令。如果这个命令处理器的列表超过五个,你可以通过使用字典(例如 menu_modes)并调用不同的处理方法来改进它。

  2. 你能否将触摸界面改为两个圆形垫片——也许这样左边的可以控制电机移动,右边的可以改变相机位置?

  3. 关于为其他行为创建手机友好的界面来控制它们的参数,你怎么看?

  4. 你可以通过添加圆形按钮或按钮之间的间距来装饰 CSS。

  5. 菜单仍然使用文本按钮。你能找到一种方法将每个行为与一个图像关联,并制作一个按钮网格?

  6. 添加一个 sudo poweroff 命令。

  7. 为了桌面兼容性,手动驾驶系统可以通过键盘交互来增强,以驾驶机器人,这不如手机有趣,但是一个方便的备用方案。

  8. 对驾驶系统进行一个严重的改进,将电机控制改为每秒计数,每个轮子一个 PID,匹配我们从编码器得到的脉冲计数与我们预期的脉冲计数。这个改进会使机器人直线行驶更直,因此更容易驾驶。

进一步阅读

要了解更多关于本章涵盖的主题,以下是一些建议:

第二十一章:第4节:将机器人技术更进一步

在本节中,我们将学习如何寻找更多有趣的机器人项目,继续提升本书中开始培养的技能,并探索有哪些社区存在。我们还将总结我们所学到的技能。

本书这部分包含以下章节:

  • 第18章进一步提升您的机器人编程技能

  • 第19章规划您的下一个机器人项目 – 将所有内容整合

第二十二章:第18章:进一步拓展你的机器人编程技能

你现在已经学习了一些基础的构建技能和一些我们可以用于机器人的更令人兴奋的编程技巧。然而,这个机器人实际上只适合在实验室使用;它还没有准备好参加比赛或巡游,这仅仅是你的机器人之旅的开始。还有一大群来自不同领域的机器人构建者和创客。

在本章中,你将学习如何继续你的旅程,如何寻找社区,如何寻找新的挑战,以及在哪里学习更多的机器人技能。你将了解这本书之外有哪些技能领域,以及为什么它们将帮助你制作更多的机器人。

你如何成为其中的一员?让我们来了解一下!

在本章中,我们将涵盖以下主题:

  • 在线机器人构建社区 – 论坛和社交媒体

  • 见识机器人构建者 – 比赛、创客空间和聚会

  • 进一步技能建议 – 3D打印、焊接、PCB和CNC

  • 寻找更多关于计算机视觉的信息

  • 扩展到机器学习

在线机器人构建社区 – 论坛和社交媒体

机器人构建是一个与一般创客社区共享空间的领域。创客无处不在。有业余无线电和电子爱好者,他们更倾向于机器人构建的电子方面,还有艺术家们正在使用Arduino和Raspberry Pi等设备使他们的创作栩栩如生。教师们使用这些设备向孩子们展示科技世界或教授其他科目。还有一些人需要解决问题或尝试一些聪明且有时疯狂的想法。

机器人技术是创客社区的一部分,该社区在Twitter、Instagram和YouTube上都有很强的存在感。搜索标签如#raspberrypi(https://twitter.com/hashtag/RaspberryPi)、#arduino(https://twitter.com/hashtag/Arduino)和#makersgonnamake(https://twitter.com/hashtag/makersgonnamake)来找到这些社区。一个集结点是Twitter上的@GuildOfMakers(https://twitter.com/guildofmakers)账号。我在我的账号@Orionrobots(https://twitter.com/orionrobots)上谈论制作机器人,我通过这个账号关注了许多机器人社区并分享我所制作的内容。

机器人社区的另一部分更加专注于机器人的人工智能方面,包括视觉处理、语音识别及其各种实现,以及更高级的主题,如神经网络、深度学习和遗传算法。这些社区可能接近大学和公司研究机构。对于语音处理,可以使用 #mycroft (https://twitter.com/hashtag/mycroft) 和 #voiceassistant (https://twitter.com/hashtag/voiceassistant) Twitter标签。对于视觉处理,可以使用 #computervision (https://twitter.com/hashtag/computervision) 和 #opencv (https://twitter.com/hashtag/opencv) 标签来找到相关的对话和博客。搜索TensorFlow和机器学习也会有所帮助。

通过寻找涉及大学的Twitter动态,例如MIT Robotics (https://twitter.com/MITRobotics)、CMU Robotics Institute (https://twitter.com/cmu_robotics) 和斯坦福大学视觉与学习实验室 (http://svl.stanford.edu/) 的 The Standford Vision and Learning Lab,可以揭示一些令人惊叹的项目。工业机器人公司对制作者的帮助可能较少,但可以成为灵感的来源。

在线机器人零部件供应商通常拥有许多优秀的项目,并拥有社区影响力。他们还提供国际配送服务。在英国,我们有Pimoroni (https://blog.pimoroni.com/)、4Tronix (http://4tronix.co.uk/blog/) 和 Cool Components (https://coolcomponents.co.uk/blogs/news) 等公司,仅举几个例子。在美国,有Adafruit (https://blog.adafruit.com/) 和 Sparkfun (https://www.sparkfun.com/news)。在社交媒体上找到这些供应商通常会揭示有关零部件和项目的机器人制作和讨论。

在线Instructables (https://www.instructables.com/) 社区分享了许多项目,包括机器人制作和其他有助于机器人制作者的东西,无论是经验还是工具。Hackaday (https://hackaday.com/) 网站也有很多精彩的故事和教程。

除了在线网站外,YouTube上还有机器人制作者的社区。

值得了解的YouTube频道

首先,是我的个人网站:Orionrobots (https://www.youtube.com/orionrobots)。我在我的频道上分享了许多机器人构建、传感器实验和代码。我把代码放在GitHub上,目的是让人们可以从我的想法中学习和构建。

詹姆斯·布鲁顿(https://www.youtube.com/user/jamesbruton),也被称为XRobots,制作非常复杂和大型3D打印机器人结构,并利用它们制作可与优秀大学机器人相媲美的作品,具有实际功能的机器人服装和自平衡行走器。

Ben Heck秀 (https://www.youtube.com/playlist?list=PLwO8CTSLTkijtGC2zFzQVbFnbmLY3AkIa)更多地关于一般制作,包括机器人,节目更多地关注制造者方面而不是编码方面,但是一个非常鼓舞人心的资源。

Computerphile (https://www.youtube.com/user/Computerphile)是一个YouTube频道,有关于编程的优秀视频,包括机器人、视觉处理和人工智能等方面。它还包括对一些仍在计算机领域活跃的重要人物的访谈。

Tested频道 (https://www.youtube.com/user/testedcom)由《神探夏洛克》团队的亚当·萨维奇主持,展示了非常熟练的制造商进行深入构建并分享他们的工作和技巧。

供应商Makezine (https://www.youtube.com/user/makemagazine)、Adafruit (https://www.youtube.com/user/adafruit)、Sparkfun (https://www.youtube.com/user/sparkfun)和Pimoroni (https://www.youtube.com/channel/UCuiDNTaTdPTGZZzHm0iriGQ)都有基于教程的YouTube频道(和网站),可以帮助你了解可用的资源。

如果你想要看到人们正在做什么,看到机器人制造商的工作,这些YouTube社区是很好的。互联网上也有专门的地方可以寻求帮助。

技术问题 - 哪里可以获得帮助

对于技术问题,Stack Exchange可以提供帮助,包括针对Raspberry Pi (https://raspberrypi.stackexchange.com/)、电子 (https://electronics.stackexchange.com/)、机器人 (https://robotics.stackexchange.com/)和Stack Overflow (https://stackoverflow.com)的一般编程帮助。Quora (https://hi.quora.com/)为技术问题提供了一个另一个问答社区。Raspberry Pi有一个论坛在https://www.raspberrypi.org/forums/,而Mycroft有一个社区论坛在https://community.mycroft.ai/

OpenCV有一个遵循Stack Overflow风格的论坛,用于技术问题,网址为http://answers.opencv.org/questions/

Twitter是一个更开放的格式,你可以在这里提出技术问题。为了做到这一点,确保你使用主题相关的标签,也许还可以标记一些有影响力的Twitter机器人专家以帮助你。

有关主题的视频频道是提问的好地方;当然,首先观看视频看看答案是否在那里。

在搜索引擎上找到替代技术和解决方案的一个技巧是输入你首先想到的技术,然后输入vs.(例如,versus),看看它们建议什么。这些建议将为你提供新的选项和解决问题的方法。

虽然在网上与人交流可以帮助解决许多问题,但没有什么能比得上与真正的机器人制造商见面并与之讨论问题。他们在哪里,你如何找到他们?

与机器人制造商见面——比赛、创客空间和聚会

当你开始构建更多东西时,与其他创客见面是必须的。首先,你将从社区的经验和知识中受益,但这也具有很大的社交价值。一些活动是免费的,但较大的活动将收取费用。

创客空间

这些空间为任何类型的创客提供,无论是机器人、手工艺、艺术还是无线电专家。它们作为工具集体,拥有创客可能需要的各种工具,以及使用它们的空间。

你可以期待在这些空间中找到一系列计算机数控CNC)机器,如3D打印机、激光切割机、车床和铣床,用于切割材料和处理钻孔。它们通常还配备了一个完整的电子工作台和许多种类的手工具。

一些有制作印刷电路板PCBs)的材料。创客空间也有一个使用这些工具进行项目的社区。人们在那里是为了社区,并且愿意与任何人分享他们的经验和知识。

创客空间是了解制作和练习技能的好地方。一些,如剑桥创客空间(https://twitter.com/cammakespace),有机器人俱乐部。

世界各地许多城市和城镇都有创客空间。它们也被称为创客集体、黑客空间和制造实验室。例如,对于西南伦敦,有伦敦黑客空间(https://london.hackspace.org.uk/)、里士满创客实验室(https://richmondmakerlabs.uk/)和南伦敦创客空间(https://southlondonmakerspace.org/)。另一个例子是孟买,有创客避难所(https://www.makersasylum.com/)。在https://makerspaces.make.co有一个创客空间目录,尽管在谷歌地图上搜索你附近的“创客空间”和“黑客空间”可能会产生结果。

这些空间在搜索引擎和社交媒体上很容易找到。如果你的地区没有这样的空间,通过社交媒体联系其他创客可能有助于你找到志同道合的人,他们可以帮助你组织类似的空间。当你找到一个场地时,要清楚该场地允许什么,例如,焊接可能是一个问题,直到你找到一个有足够大集体空间的专用空间。

创客市集、树莓派聚会和道场

在创客市集(https://makerfaire.com/)方面,许多国家举办基于制作的节日。这是人们聚集在一起展示和共同建造东西的地方,机器人通常是这类节日的一部分。这些可以是为期一天的赛事,也可以是露营节日,如英国的EmfCamp(https://www.emfcamp.org/)。这些是你可以开始学习新技能、展示和讲述你所制作的东西,以及看到其他人制作了什么的地方。

树莓派聚会(https://www.raspberrypi.org/jam/)和编码道场(https://coderdojo.com/)是聚集在一起定期练习编程和有时是创客技能的团体。编码道场是一个社区编程工作坊。树莓派聚会是一个类似的活动,与树莓派密切相关。有些树莓派聚会面向成年人,而有些则面向儿童,所以找出当地是否有这样的团体,以及他们的目标是什么。在道场或聚会中成为孩子的导师是了解其他感兴趣创客和程序员的绝佳方式。

每年举办的树莓派派对是一个有趣的聚会,但重点更多地在于见面,而不是一起建造。

所有这些团体都倾向于拥有相当鼓舞人心的Twitter动态。

竞赛

机器人竞赛在学术界之外仍然相对罕见。美国的FIRST(https://www.firstinspires.org/robotics/frc)工程倡议旨在让学校和大学建造机器人并参加比赛,美国以外也有一些零星的FIRST团队。FIRST挑战可以是自主的,也可以是手动驾驶的。大多数国家都有某种科学技术工程和数学(STEM)组织,例如英国的https://www.stem.org.uk/。他们有时会举办机器人竞赛,你可以在他们的网站和通讯中了解到这些信息;务必查看他们是否对公众开放,或者只是对学校开放。

在英国,PiWars (https://piwars.org/) 比赛每年举行一次,涉及许多围绕剑桥大学计算机学院设置的自主和手动挑战。它具有很强的社区元素,是一个作为参赛者或观众遇到机器人建造者的好地方。#piwars (https://twitter.com/hashtag/PiWars) 的Twitter标签有一个非常活跃的社区在讨论这个话题,尤其是在机器人制造者在活动前聚集起来建造和测试机器人时。

英国还有另一个比赛叫做Micromouse,http://www.micromouseonline.com/,这是一个关于迷宫解决机器人的比赛,尽管制造者们也展示了其他类型的机器人。这两个比赛也都有小型机器人市场。

Robotex国际 (http://robotex.international) 机器人展览在爱沙尼亚举行,结合了大量的展示和讲述,以及几天时间的比赛和丰厚的奖品。他们欢迎使用电子和Raspberry Pi、乐高和其他材料的机器人建造者。

由于这些活动需要旅行,你可能需要考虑一个足够大的箱子,里面装有气泡膜或包装泡沫,以确保你的机器人安全地往返于这些活动。

我建议你取出电池以减少由于乱线引起的短路的可能性,并将它们装入塑料袋中以隔离任何金属。开始考虑在机器人设计中电缆布线;尽管这超出了本书的范围,但它使机器人更加坚固。

我还建议你随身携带一个现场维修工具包,里面包括面包板、电线、备用电池、充电器、所有类型的螺丝刀、逻辑电平转换器的替换组件、搭扣带、支撑件套件,以及可能的多用电表。机器人到达活动时通常需要一点调整和维修。

在本节中,你已经了解了一些你可以遇到机器人制造者、使用工具和找到一些比赛的地方。接下来,让我们看看更多你可以利用来建造你的机器人的技能。

进一步技能的建议——3D打印、焊接、PCB和CNC

随着你建造更多机器人,你将想要创建更复杂或定制的系统。

要建造一个竞赛级别的机器人,你需要更多的硬件构建技能。

设计技能

我们在这本书中一直使用框图和简单的绘图。然而,为了更认真地从事机器人建造,你需要设计零件或检查购买的零件是否可以与你的机器人集成。你将需要创建外壳、底盘、传感器支架、支架、轮子类型以及任何数量的零件,其中计算机辅助设计CAD)是关键。

用于说明和图表的2D设计

对于 2D 设计和插图,我推荐 Inkscape (https://inkscape.org/)。Inkscape 比面向 CAD 的更具艺术性,但如果你想要制作标志和其他设计,它很方便。它相当复杂,所以我推荐一本像 Inkscape 初学者指南,作者 Bethany Hiitola,出版社 Packt Publishing 的书,以开始学习它。

Draw.io (https://app.diagrams.net) 对于创建像本书中的图表非常有用。你可以使用 Inkscape 将这两个系统结合起来,制作出可以在 Draw.io 中使用的新的形状。Inkscape 在形状操作方面提供了更多的自由度,但 Draw.io 在放置形状和连接事物方面做得更好。

3D CAD

透彻地了解像 FreeCAD (https://www.freecadweb.org) 和 Fusion 360 (https://www.autodesk.com/campaigns/fusion-360-for-hobbyists) 这样的 3D CAD 系统是非常值得的。FreeCAD 是免费且开源的;Fusion 360 为制造商提供了一个免费的入门级 CAD 系统。

3D CAD 系统让你可以设计零件,然后创建进一步的设计,以便你可以测试如何组装它们。你也可以从这些设计中制作出用于手工工具使用的图纸或导出用于 3D 打印。

所有这些都需要投入一些时间,所以我建议使用教程和 YouTube 视频来掌握它们。Maker's Muse 频道 (https://www.youtube.com/channel/UCxQbYGpbdrh-b2ND-AfIybg) 是开始这个的好地方。

Thingiverse (https://www.thingiverse.com/) 社区分享了用于打印和制造的 3D 设计。一个非常有效的技巧是,要么从那里获得灵感,要么重用或改造看到的作品。如果你能的话,将一个支架导入 FreeCAD 并添加你需要的特定孔/基座或连接器;这可能会为你节省数小时尝试从头绘制传感器支架的工作。社区也会提供有关打印这些作品的技巧。由于你并不总是能在 Thingiverse 找到你想要的东西,可以考虑 Pinshape (https://pinshape.com/) 和 GrabCad (https://grabcad.com/) 等替代方案。

一旦你有了零件的 CAD 绘图,你可以将它们发送出去进行制造,或者学习你可以用来自己制造它们的技巧。

塑形和建造的技能

作为一般建议,MIT 的 如何制作几乎所有东西 (http://fab.cba.mit.edu/classes/863.14/) 课程材料(每年更新)是寻找组装事物方法的绝佳资源——尽管它们看起来很朴素,但那里的链接非常有用。正如我们在 在线机器人建造社区——论坛和社交媒体 中提到的,YouTube 和其他频道在制作事物方面充满了实际例子和动手教程。

机器技能和工具

数控铣削、激光切割和3D打印允许你创建实体零件,并且可以产生很好的效果;然而,每个领域都有许多你必须学习的技能。激光切割允许你制作平面零件,但通过一些巧妙的设计,平面零件可以组装成复杂的实体3D对象。

YouTube频道NYC CNC (https://www.youtube.com/user/saunixcomp)涵盖了大量的数控技巧和用法;然而,由Michal Zalewski编写的在线书籍《数控加工、模具制作和树脂铸造游击指南》也是一个极好的资源。

对于这些加工技术,我建议不要出去购买自己的设备,而是去了解我们之前提到的当地社区创客空间,并使用他们那里的设施。一些图书馆也开始涉足这一领域,提供3D打印机和简单的创客材料。使用这些设备将比购买自己的更便宜;你将置身于一个有经验的社区中,而且这比独自尝试要容易得多。

如果你只需要3D打印或激光切割的零件,网上有一些地方可以为你制作。Ponoko (https://www.ponoko.com/)、RazorLAB (https://razorlab.online)、3DIng (https://www.3ding.in/)、Protolabs (https://www.protolabs.co.uk/)、Shapeways (https://www.shapeways.com/)和3D Hubs (https://www.3dhubs.com/)是一些提供此类服务的公司。通过搜索引擎查找你所在地区的3D打印和激光切割服务并不困难,但通过创客空间获得一些经验来了解这些机器的可行性和局限性将有所帮助。使用错误的机器或做出错误的设计决策可能会导致巨大的成本。

3D打印机、激光切割机和数控机床需要定期的维护和保养任务,例如校准3D打印床或调整数控夹具。它们还需要消耗品,如材料(塑料丝、用于铣削或激光切割的木材)、更换零件和床粘合材料。除非你经常打印,否则当你可以通过创客空间或在线市场访问其他设备时,拥有自己的设备通常并不经济。

虽然机器技能可以制造出非常精确的零件,但完成或修改这些零件还需要手工技能。对于一些零件来说,如果由人工制作,特别是为机器人的一次性使用,它们将更加适合。

手工技能和工具

拥有一些基本的木工和手工艺技能总是很有帮助。在创客空间练习这些技能将帮助你看到事物如何组合在一起。这样,你就会知道如何选择合适的木材,因为不合适的木材可能太软、太重或太不规则。正如之前提到的,木材可以手工雕刻或用于CNC机器。

学习建模技能,如使用塑料板(苯乙烯板)、制作模具和铸造,是制作3D部件的其他方法。塑料板是一种价格低廉、可弯曲的材料,厚度不一,可以很容易地手工切割,可能使用打印模板,然后组装。

你可以使用木工技术来制作模具和临时机器人底盘。模具允许你制作多个副本或在高品质部件中使用材料。铸造可能会有些棘手,尤其是当你处理气泡时,但关于这个主题有很好的书籍。为此,我推荐以下文章:https://medium.com/jaycon-systems/the-complete-guide-to-diy-molding-resin-casting-4921301873adhttps://youtu.be/BwLGK-uqQ90,以及提到的《游击队CNC指南》,它出现在《机械技能和工具》一书中。

进一步有趣的材料技能,如金属加工,允许制作更大的机器人。这意味着学习如何切割、塑形和焊接金属部件。

碳纤维或凯夫拉材料适用于更大的机器人、战斗机器人或需要处理更重材料的机器人。

Instructables (https://www.instructables.com/) 和 Hackaday (https://hackaday.com) 社区将帮助你学习本节中提到的技能。他们有关于构建事物的实际指导和教程。你可以跟随完整的项目进行学习,或者只是浏览以借鉴他们的技术。除了寻找机器人,还可以看看建模技术(通常相似)、塑料板构建、木工或金属工教程。创客空间也会教授这些技能。

有了一个学习如何制作结构和机械部件的指南,那么电气部件呢?

电子技能

下一步你必须做的是扩展你的电子技能。我们一直使用Raspberry Pi帽子和模块来构建我们的机器人。当我们刚开始时,这没问题,但当部件很多,对空间有要求或脆弱的电线使其远非理想时,这开始感觉笨拙。你会注意到我们的机器人上的电线非常拥挤。

电子原理

了解更多关于电子组件和常见电路功能的原理将帮助你更深入地理解你的机器人,扩展它,找到减小其尺寸的方法,或者消除机器人上的问题。

功率电子学将帮助你更好地理解你的机器人电机控制器和电池调节电路。数字电子学将让你连接其他逻辑设备,使用新的传感器,或以有用的方式聚合它们。模拟电子学也将打开新的传感器和执行器类型,并为你提供诊断许多可能出现的电气问题的工具。

因此,你应该学习如何绘制和阅读常见部件的原理图。在线课程和YouTube频道会逐步教授电子知识,例如由查尔斯·普拉特所著的《Make: 电子学》一书,提供了非常实用的学习路径。

EEVBlog频道在逐步教学方面较少,但在电子工程相关问题上提供了更广泛的沉浸式体验。

焊接的进一步学习

尽管我们进行过一点焊接,但这只是最基本的。关于这个主题还有很多东西要学习。焊接是许多制造商日常使用的技能。

一些好的起点包括Raspberry Pi焊接指南(https://www.raspberrypi.org/blog/getting-started-soldering/)、Adafruit优秀焊接指南(https://learn.adafruit.com/adafruit-guide-excellent-soldering)和EEVBlog焊接教程(https://www.youtube.com/watch?v=J5Sb21qbpEQ)。

我建议从当地的创客空间开始,在那里你可以从他人那里受益,并完成简单的焊接项目。将引脚焊接在模块上是一种相当基础的入门方式,同时使用Boltportclub等制造商的套件来进一步扩展这些技能。焊接让你开始思考创建电路板或Raspberry Pi帽子的想法。

你将从焊接简单的引脚和所谓的通孔组件开始,因为它们会穿过板上的孔。这是你应该实施的正确类型的构造,以获得对技术的信心。

随着你越来越自信,你会发现使用表面贴装焊接的套件。表面贴装组件没有穿过孔的腿,而是简单的金属垫,它们直接焊接到板上的铜垫上。它们占据的空间远小于传统组件,允许更小的构造,但它们也很麻烦,最终需要相当专业的工具来使用。一些简单的表面贴装组件,如LED、电阻和电容器,可以手工焊接。查看EEVBlog表面贴装教程(https://www.youtube.com/watch?v=b9FC9fAlfQE)作为起点。

拥有数十个引脚的设备可能无法工作,需要使用焊炉和焊膏。到那时,你可能正在制作定制电路,并且可能需要采取 印刷电路板和组装 (PCBA) 服务。

定制电路

随着你对电子和焊接的信心增强,你将想要创建更多电路,并将它们转移到更专业的 PCB 上以节省空间,也许还能使它们更容易布线。面包板适合学习和实验,但它们不适合竞争,很快就会变得庞大而杂乱,而点对点布线脆弱且容易出错。

定制、更永久电路的第一阶段是使用条形板或穿孔板,并将元件焊接在上面。这是从面包板到的一个很好的进一步步骤,并将节省空间。尽管如此,它们仍然可能有点庞大和杂乱。你也可能想要使用表面贴装元件或具有不规则排列的、不同尺寸的腿,这些腿不适合方便地安装在穿孔板或条形板上。

要将你的电路提升到下一个水平,你可以学习设计 PCB。你将能够节省更多空间,拥有更坚固的电路,并能够使用微型表面贴装元件。你甚至可以设计用于轻结构定位的 PCB。

对于面包板,你可以使用 Fritzing (http://fritzing.org/home/),但我不建议用它来做原理图或 PCB 设计。设计这些,使用 KiCad (https://kicad.org) 这样的软件是很好的业余爱好工具。我推荐视频课程 KiCad Like a Pro,作者 Peter Dalmaris,出版社 Packt Publishing

你可以使用当地创客空间提供的设施来制作 PCB,或者将它们发送到板厂进行精美制作,包括细轨、文字和花哨的彩色焊膏(你将在该领域看到更多此类术语)。定制 PCB 允许你调整布局以避免任何点对点布线,与微型表面贴装元件一起工作,在板上添加有用的文字以进行布线,并获得专业的外观。有些人甚至使用它来制作机器人的其他部件,包括结构部件和前面板,在 PCB 上。

寻找更多关于计算机视觉的信息

我们从 第 13 章 开始了解计算机视觉,即 Robot Vision – Using a Pi Camera and OpenCV。我们使用了 OpenCV 来追踪彩色物体和面部,但仅仅触及了计算机视觉的皮毛。

书籍

如果你希望继续学习 OpenCV,我推荐书籍 OpenCV with Python By Example,作者 Prateek Joshi,出版社 Packt Publishing。这本书使用计算机视觉来构建增强现实工具,并识别和跟踪物体,带你通过不同的图像变换和检查,并为每个变换提供截图。它还非常有趣,因为它包含了大量的动手代码。

你甚至可以将计算机视觉扩展到3D计算机视觉,使用Xbox 360 Kinect传感器条。尽管它们不再由微软生产,但在eBay上非常常见且相对便宜。请注意,有一个现代Azure Connect设备可以用于此,但在撰写本文时,它的价格是原来的20倍!XBox 360 Kinect传感器条也已与Raspberry Pi接口相连。Kinect具有3D感知系统,这使得它们在机器人应用中非常有价值。在《Raspberry Pi机器人项目》这本书中有一个关于将其连接到Raspberry Pi的部分,作者:Dr. Richard Grimmett,出版社:Packt Publishing

在线课程

PyImageSearch (https://www.pyimagesearch.com/)包含了学习OpenCV和实验机器视觉的一些最佳资源。

用Python和OpenCV学习计算机视觉》,作者:Kathiravan Natarajan,出版社:Packt Publishing (https://www.packtpub.com/in/application-development/learn-computer-vision-python-and-opencv-video),这本书深入探讨了颜色跟踪、特征检测和视频分析,同时使用优秀的Jupyter工具进行图像变换实验。

TensorFlow教程网站(https://www.tensorflow.org/tutorials/)(一个机器学习框架)包含了一些专门针对在计算机视觉中使用TensorFlow的教程。训练机器学习系统执行视觉识别可能需要大量时间和样本数据。

视频课程《高级计算机视觉项目》,作者:Matthew Rever,出版社:Packt Publishing (https://www.packtpub.com/big-data-and-business-intelligence/advanced-computer-vision-projects-video)提供了更多的计算机视觉项目,最终使用TensorFlow机器学习系统从摄像头输入中分析人体姿态。

社交媒体

我在在线机器人构建社区 - 论坛和社交媒体部分提到了Twitter标签#computervision#opencv,它们是询问问题或分享关于该主题的工作的好地方。

Computerphile有一个小的计算机视觉播放列表(https://www.youtube.com/watch?v=C_zFhWdM4ic&list=PLzH6n4zXuckoRdljSlM2k35BufTYXNNeF),解释了一些视觉处理算法的概念和理论,但不倾向于深入任何实际操作。

有了这些,你已经学会了在哪里可以找到更多关于计算机视觉的信息。然而,这引出了更高级的机器人主题:机器学习。

扩展到机器学习

一些听起来最聪明的机器人类型涉及机器学习。本书中使用的代码没有使用机器学习,而是使用了众所周知的算法。本书中使用的比例积分微分PID)控制器是一个调整以读取值的系统,但它不是机器学习。然而,优化PID值可能来自机器学习算法。我们使用了Haar Cascade模型来检测人脸;这也不是机器学习,尽管OpenCV的贡献者可能使用机器学习系统生成这些级联。

机器学习通常擅长优化任务和发现和匹配模式,但在形成完整、看似智能的行为方面表现较差。

许多机器学习系统的基本整体思想是拥有一些起始示例,其中包含一些关于哪些是匹配的、哪些不是匹配的信息。机器预计将根据匹配与否来确定或学习规则。这些规则可能是基于学习规则的适应度分数,以最大化这种分数。这一方面被称为训练系统。

对于PID控制系统,你可以根据从数据中训练得到的值(如机器变化、响应时间、速度等)来评估适应度,基于最少的步骤和尽可能小的超调量来达到设定点。

再次推荐Computerphile AI视频播放列表(https://www.youtube.com/watch?v=tlS5Y2vm02c&list=PLzH6n4zXuckquVnQ0KlMDxyT5YE-sA8Ps)视频系列,以了解机器学习周围的概念;这不是实践操作,而是更专注于理念。

机器学习可以非常专注于数据和统计,但本书中学到的技术可以应用于传感器数据,使机器学习与机器人更相关。有许多使用TensorFlow系统构建目标识别系统的例子。遗传算法演化的解决方案在多足机器人步态或寻找快速导航空间的方法中得到了很好的应用。

机器人操作系统

一些机器人社区使用机器人操作系统ROS);请参阅http://www.ros.org/。机器人构建者使用这个系统在机器人硬件和行为之间建立通用的、跨编程语言的抽象。它的目的是鼓励机器人构建者使用可重用的代码层。建立在ROS之上的AI系统可以与底层系统混合匹配。我们构建的行为/机器人层允许一些重用,但与ROS相比非常简化。

书籍《ROS编程:构建强大的机器人》由Anil Mahtani撰写,由Packt Publishing出版,涵盖了将TensorFlow AI系统链接到基于ROS的机器人。

对于一个更简单的介绍,《使用Python学习机器人学》莱汀·约瑟夫Packt Publishing使用ROS和Python构建了一个智能AI机器人,该机器人使用LIDAR。

摘要

在本章中,您学习了如何找出与我们本章所讨论的机器人类似的其他人和其他地方正在制造机器人,以及如何成为这些社区的一员。与其他机器人构建者分享知识将加速您的旅程。

您还学会了在哪里与机器人竞争,在哪里获取更多建议,以及如何找到信息来进一步发展您已经开始构建的不同技能。这种灵感和方向应该会使您轻松地继续提升您的机器人技能。

在下一章中,我们将总结本书所学到的所有内容,以便构建您的下一个机器人。

进一步阅读

以下是我喜欢的一些其他实用机器人书籍:

  • 《Python机器人项目》迪瓦卡·瓦伊什教授Packt Publishing:这本书为您提供了更多树莓派和Python机器人项目进行实践。

  • 《初学者机器人构建》大卫·库克Apress:这本书将引导您构建名为“三明治”的机器人,这是一个基于午餐盒的DIY机器人。它更多地基于制造者和电子技术,但这是一个非常有趣的项目。

  • 《学习树莓派》萨马尔特·沙哈Packt Publishing:您可以在本书的章节中深入了解树莓派可以做什么,并从中获得灵感,以增强您的机器人。

  • 《机器人构建宝典(第5版)》戈登·麦克康布McGraw-Hill Education TAB:这是一本有影响力的书,在如何制作机器人方面相当广泛。这是超越购买套件并构建更大、更复杂的机器人的最佳书籍。

第二十三章:第19章:规划你的下一个机器人项目——整合所有内容

在这本书的整个过程中,你现在已经看到了如何规划、设计、构建和编程一个机器人。我们已经涵盖了多个起始主题,并有一些实际操作经验,一个展示基本原理的例子,以及一些关于如何改进它们的想法。在这一章中,我们将思考你的下一个机器人。我们将回答以下问题:你将如何规划和设计它?你可能需要研究哪些技能并进行实验?你将构建什么?

本章我们将涵盖以下主题:

  • 可视化你的下一个机器人——它将是什么样子?

  • 制作方块图——确定输入/输出以及所需的部件

  • 选择部件——在选择机器人部件时,你会考虑哪些权衡?

  • 规划机器人的代码——这个机器人可能需要哪些软件层和组件,哪些行为会很有趣?

  • 让世界知道——你将如何与感兴趣的人分享你的计划或你的机器人?

技术要求

对于这一章,我建议使用一些绘图工具,如下所示:

  • 笔/铅笔

  • 纸张——草图本(或者也许是一些坐标纸)很好,但是信封的背面也行

  • 一台连接互联网并可以访问https://app.diagrams.net/的电脑

可视化你的下一个机器人

当我们开始这本书时,在第2章探索机器人构建模块——代码和电子学中,我们首先学习了如何将机器人视为一个草图。我建议你快速绘制草图,不要担心它们是否粗糙和草图化——在早期规划阶段这是完美的。使用铅笔或钢笔,然后稍后转向更正式的方块和布局图。

每个机器人都是从一点灵感开始的。可能有一个你想要尝试的比赛;也许你看到了一些东西,比如另一个机器人或你想要模仿的动物(螃蟹非常迷人!)。其他的灵感可能来自于看到一些独特的新部件,或者想要学习/玩耍一项新技能。你甚至可能已经列出了一个你想要尝试构建的令人惊叹的机器人清单。

在构建机器人之前,列出一张简短的要点清单,说明它将做什么,它将有哪些传感器/输出,以及它可能需要处理的事情。这份清单让你能够集中精力。以下是一个例子,这是我为我看到的SpiderBot项目所做的,见第10章使用Python控制伺服电机。这是我为其规划要做的:

  • 它将有六条腿(是的——一只昆虫,而不是蜘蛛)。

  • 我将用它来尝试腿和步态。

  • 它将能够避开墙壁。

你的快速草图可能首先是一个基本的六腿棍状图,一端有一些正方形来表示超声波传感器,也许还有几条带注释的箭头来描述它们的意义。你已经在第二章中详细看到了这种技术,探索机器人构建模块 – 代码与电子学。以下照片展示了一个简单的设计:

图19.1 – 在纸上绘制你的想法

图19.1所示,我首选的第一幅草图是用圆珠笔在坐标纸上绘制的,但我会用我有的任何纸张。

可以用2D、3D或轮廓草图来可视化机器人。这里有一些提示:

  • 轻轻绘制,然后在你对设计更有信心时用更坚定的笔触继续绘制。

  • 用任何想到的东西大量标注。

  • 不要担心比例、尺寸或完美的绘图;这只是为了捕捉想法。你会在稍后充实它们并做出更坚定的决定。

  • 给图片标注日期并给它一个工作名称是个好主意,即使你以后有更好的名字。

  • 随意将块状表示法与草图视觉版本结合使用。

  • 在某个地方随身携带一支圆珠笔/铅笔和记事本/便条纸,这样你就可以快速记下想法。如果你附近有白板,那将非常出色。铅笔可以让你擦除并重写,圆珠笔则容易放在包或口袋里。

  • 首先记下大的想法;回来补充细节。很容易陷入某个方面的细节中,忘记其他部分,时间不够用。你总是可以做一个小的笔记来提醒自己。

你可以在机器人构建的任何时间回顾这个过程,也许当你有更多想法时,当你遇到需要解决的问题时,或者当你想要细化它时。大多数想法都是从一些项目符号和一个草草的草图开始的;等待使用电脑或试图完美绘制它,会分散你心中已经有的下一个绝妙想法——先把它记下来。

现在你已经大致有了它的草图,并在纸上写了一个基本计划,我们可以开始用块状图来正式化它。

制作块状图

回想一下,在第二章探索机器人构建模块 – 代码与电子学,以及整本书中,我们创建的块状图展示了我们那里构建的机器人。你可以用这种方式表示任何机器人。这个图就是你在其中为每个输入和输出创建一个块,然后创建控制器和接口块来连接它们的图。不要担心图是否完美;主要目的是图片传达了哪些部分你需要连接到其他部分。也很有可能,最初的图在构建机器人和遇到你之前未意识到的限制时需要一些修改。

下面是SpiderBot的块状图的两个阶段:

图19.2 – SpiderBot块状图阶段

图 19.2 中,我最初知道每条腿有三个电机,但除此之外并不多。所以我画了那些块,以及我希望它拥有的距离传感器和Wi-Fi连接。

在下一步的图中,我添加了我要使用的控制器,并在图上进行了粗略的连接。这些只是块,这不是电路图。这些是用 app.diagrams.net 拼凑起来的。请注意,随着你对你的机器人和其控制器了解得更多,事情仍然可能发生变化。

你还可以考虑为附加组件和子组件制作图表。任何看起来复杂的部件可能都需要一个更详细的图表来探索它。重要的是要把这样的想法从你的脑海中转移到纸上,这样它们就会更清晰,这样你就不会忘记它们,这样你也许能够发现缺陷。

另一个需要考虑的块图是软件图,我们将在 规划机器人的代码 部分讨论。

现在,你已经有了机器人的粗略草图和块图,你可以开始选择你用来构建机器人的部件了。

选择部件

在整本书中,我们探讨了不同类型传感器、不同底盘套件、控制器等之间的权衡。这些权衡包括重量、复杂性、可用性(你不想有一个无法替代的部分),以及成本,这些内容在第 6 章中详细讨论,构建机器人基础——轮子、电源和布线

如果某个套件启发了机器人——例如,SpiderBot 是由六足机器人套件启发的;你的可能是机器人臂或履带式轨道套件——这可能会限制你需要做出的其他部分选择。我需要支持 18 个伺服电机;然而,我有一个 16 个电机的控制器可用,所以我选择了使用中央控制器的两个 输入/输出I/O) 引脚来连接剩余的两个伺服电机。这增加了软件复杂性,尽管如此。

另一个权衡是主控制器。我知道我想要 SpiderBot 具有Wi-Fi功能,但它不会进行视觉处理,所以一个小型、便宜且低功耗的控制器,如ESP8266,是一个很好的选择。

对于电源,我知道所有这些伺服电机将需要大量的电流,但它无法承载很大的重量,因此需要一个更专业的 锂聚合物LiPo) 电池,以及一个充电/保护电路。

选择部件的关键阶段是测试安装它们。

测试安装图

在选择零件时,考虑它们如何组合在一起:是否有明确的路径将电机控制器与你的主要控制器连接?这两个组件是否曾一起使用过,或者你是否准备好应对制作新接口的复杂性?根据你认为将要购买的零件,收集它们的尺寸,并尝试制作一个测试装配图,就像我们在第6章构建机器人基础 – 轮子、电源和布线中做的那样。在购买新零件之前,尽量进行测试装配。

购买零件

当时的问题是要找到批发商来购买它。我有一些当地的推荐(例如coolcomponents.co.ukshop.pimoroni.com,和thepihut.com),随着你组装更多,你也会在你的地区找到这些。寻找当地的Pimoroni、SparkFun、Raspberry Pi和Adafruit批发商将帮助你找到合适的商店。

你可以在你所在的地区的亚马逊、阿里巴巴或eBay上找到模块,但一定要清楚你购买的是什么,以及你将获得多少支持。你可以在Element14、Mouser、RS和Digi-Key等大型批发商那里找到单个零件;尽管他们通常没有很多预建的模块,但他们很可靠,拥有庞大的目录。

零件大多在网上销售。可能会有一些街边销售电子和机械零件的商家,但这越来越少了。

你也可以使用现有库存的零件,随着你组装机器人,你会逐渐积累这些零件。你可以将玩具转换成机器人底盘,这种做法被称为玩具黑客。机器人构建者可以从旧打印机和中机电系统中(小心操作)回收电机和传感器。在这种情况下,测试装配图将帮助你看到你可能需要更改什么才能使回收的零件工作。

组装你的机器人

现在,你准备好组装你的新机器人了。第6章构建机器人基础 – 轮子、电源和布线第7章驱动和转向 – 使用Python移动电机,以及第12章使用Python进行IMU编程中的基本焊接指南将帮助你入门。然而,第18章将你的机器人编程技能提升到更高水平中建议的额外阅读和技能将为你提供更多组装机器人的选项。

现在你已经准备好了零件,并且开始组装机器人,接下来要考虑的是机器人的代码。

规划机器人的代码

我们在第2章,“探索机器人构建块——代码和电子”,开始以层来规划代码,然后在第7章,“驱动和转向——使用Python移动电机”的“机器人对象”部分进一步探讨了这一点。

让我们回顾一下我们如何使用层来规划代码结构。

系统层

通用思路是在系统中创建代码层,如下面的图所示:

图19.3 – 机器人软件层

图19.3所示,有一些推荐的层级,如下所示:

  • 在堆栈的底部,我们一直在使用的gpiozero库或该生态系统的各种Arduino库。这一层可能包括I/O控制、总线层和网络堆栈。

  • 下一层是库和中间件。这些软件可能来自第三方——例如,用于与特定硬件接口的高级库。中间件层还包括你编写的抽象,例如使两轮机器人表现出相同的行为,即使第三方库不同。在同一层上还有算法和库。OpenCV存在于这一层,来自社区。你的比例-积分-微分PID)算法或物体识别管道可能属于这一层。这一层有构建应用程序和行为的组件。

  • 顶层是将一切粘合在一起。通过算法将硬件传感器输入转换为硬件电机输出,以及令人愉悦的控制界面或仪表板。顶层是创建机器人的行为和应用程序的地方。

对于一个基本的机器人,这些层中的组件可以是函数或类。对于更复杂的机器人,这些可能是不同的软件组件,它们在一个共享的软件总线上进行通信(例如消息队列或连接的服务)。我们已构建的库将适用于许多小型轮式机器人(在中间层)。随着你对这个库的实践经验增加,这个库将需要改进。如果你将行为与硬件关注点分开,你可以为新传感器和输出适配行为。

使用图表来绘制块和层,以表达这些边界在哪里。预期你将编写模块和块,并将它们链接起来以推理每个部分。在思考制作令人愉悦的LED图案(在行为层)时,不应该有必要陷入串行外设接口SPI)数据总线事务的细节(在供应商硬件层)。

一旦你考虑了层和一些粗略的组件,你将需要思考如何使用数据流图来处理它们之间的信息流动。

数据流图

你还可以使用图表从数据流的角度探索行为,例如在第13章机器人视觉——使用树莓派摄像头和OpenCV中使用的PID和反馈图表来表示行为。我们也使用这种图表风格来表示颜色对象和面部跟踪行为,作为显示图像转换的数据管道。不要期望一个图表就能捕捉整个故事;有时,需要几个图表来接近行为的不同方面。

在这里花时间考虑一些棘手的问题,比如如果传感器/运动关系复杂,可能需要的额外数学。你可能不会第一次就做对,所以构建它并推理为什么它的行为与你的预期不同将是必要的。在设计这个阶段,上网寻找类似的工作或阅读许多推荐书籍将有助于更深入地了解你所尝试的事情。在大多数情况下,坚持不懈会得到回报。

正式图表

对于流程图或统一建模语言(UML)类型等图表,有正式的表示方法。了解和学习这些作为资源来绘制图表是值得的。app.diagrams.net软件有一个很好的图表元素库。图表最重要的方面是传达信息——你应该尝试以对你来说有意义的方式表达你头脑中的想法,或者在未来6个月或你的团队(如果你有的话)看来有意义。

有时,构建简单的行为会给你一个库来使用,以便构建更复杂和有趣的行为。我们的直线驱动行为是构建方形行驶行为的基石。

编程机器人

你现在可以编程机器人了,但要做好准备,要经历几个规划、实施、测试和学习的循环。不要因为测试失败而气馁,因为这些是学习最好的机会。大部分的学习都是在规划和确定测试失败中完成的。如果一切第一次就成功了,那么它不太可能给你留下深刻的印象。本书中的每一个行为都让我尝试了多次才正确;调整它们是一个试错的过程。

你已经规划了代码,现在正在构建机器人。你如何让世界知道你在制作东西并寻求帮助?

让世界知道

你肯定会关于如何进行和需要解决的问题有所疑问——也许你在构建之前就已经遇到了这些问题。当你有问题或者已经取得了一些小进展时,就是时候上网并与机器人社区建立联系了,正如在第18章在线机器人构建社区部分所示,进一步拓展你的机器人编程技能

使用Twitter和Stack Overflow提问或甚至回答其他机器人建造者的提问。利用YouTube分享你的创作或建造故事,并观看他人的建造项目。你不需要等到拥有一个完美无瑕的产品后再分享。分享你所采取的步骤、你所遇到的挫折,甚至是你从失败中学到的经验。失败的情况往往能成为最好的故事。这些故事可能正是其他人继续坚持复杂建造项目的动力。

结合使用YouTube、Instructables和在线博客来练习新技能,或者——更好的是——去附近的创客空间、Coder Dojo或Raspberry Jam与其他也在创造和学习的人一起练习新技能。

成为机器人建造者会使你成为一个终身学习者;关于这个主题,总有更多东西要学习,这不仅是因为它仍然是一个研究热点。人们在推动机器人领域人类知识的边界。你可以在成为导师和帮助者的同时,推动你的技能和知识边界,扩展他人能够做到的边界。也许你会想出套件、模块和代码来降低入门门槛,你也可能找到使用机器人或构建传感器的创新方法,从而推动人类知识的边界。无论哪种方式,与机器人社区互动都是令人兴奋、令人耳目一新的,并让你不断寻找新事物去尝试。

在实验室条件下测试你的机器人是可以的,但最严格的测试是在户外,在比赛和演示中进行的。在这些情况下,你会发现新的错误,找到需要解决的新问题,并建立一个机器人建造者朋友和同行的网络。机器人有一个刻板印象,认为它是一个非常孤独的爱好或职业,但情况不必如此,因为有许多人在创造东西,所以和这些人一起做吧。

与团队一起建造可以非常有益且具有挑战性。这将使你能够创造出比独自一人更雄心勃勃的建造项目。参与任何社区,尤其是本地社区,可能是你找到团队成员的最佳机会。

你现在已经了解了如何接触更广泛的机器人建造者世界,并找到帮助、灵感和竞争。你看到了机器人可以成为一种协作和社交的爱好。也许你也受到了启发,想要开始自己的机器人博客或YouTube频道。我期待着在社区中见到你!

摘要

你现在已经在这本书中看到了如何建造和编程你的第一个机器人。你已经看到了如何了解更多信息以及如何扩展你的知识。在本章的最后,我们总结了你所学的知识,并建议如何利用这些知识来计划、建造和编程你的下一个机器人,以及如何带它去巡展并成为机器人社区的一员。

你已经看到了如何设计和规划一个机器人,以及如何构建、编程和测试一个机器人。你已经学习了硬件技能,例如焊接,简单的软件,例如移动机器人,并且对复杂领域如计算机视觉和惯性测量有了一定的接触。你已经排除了错误,做出了权衡,精细调整了系统,并学会了如何进行备份。你制作了用户界面,实现了智能行为,并使用智能手机控制了机器人。

你已经到达了这本书的结尾,但我希望这仅仅是你的机器人之旅的开始。

第二十四章:你可能还会喜欢的其他书籍

如果你喜欢这本书,你可能对 Packt 出版的以下其他书籍感兴趣:

动手实践 ROS 机器人编程

Bernardo Ronquillo Japón

ISBN: 978-1-83855-130-8

  • 掌握开发环境感知机器人的技能

  • 了解你的机器人在物理环境中的反应

  • 将所需的行为分解为一系列机器人动作

  • 将来自传感器的数据与上下文相关联以产生适应性响应

  • 应用强化学习使你的机器人能够通过试错来学习

  • 实施深度学习以使你的机器人能够识别其周围环境

ROS 机器人项目 - 第二版

Ramkumar Gandhinathan, Lentin Joseph

ISBN: 978-1-83864-932-6

  • 掌握 ROS 的基础知识并了解 ROS 的应用

  • 揭示 ROS-2 与 ROS-1 的不同之处

  • 使用状态机处理复杂的机器人任务

  • 与多个机器人通信并与之协作构建应用程序

  • 使用最新的嵌入式板,如 Tinker Board S 和 Jetson Nano 探索 ROS 的功能

  • 了解机器学习和深度学习技术在 ROS 中的应用

  • 使用 ROS 构建一辆自动驾驶汽车

  • 使用 Leap Motion 和 VR 头盔远程操作你的机器人

留下评论 - 让其他读者了解你的看法

请通过在购买书籍的网站上留下评论的方式与其他人分享你对这本书的看法。如果你从亚马逊购买了这本书,请在本书的亚马逊页面上留下一个诚实的评论。这对其他潜在读者来说至关重要,他们可以通过你的无偏见意见来做出购买决定,我们也可以了解客户对我们产品的看法,我们的作者也可以看到他们与 Packt 合作创作的标题的反馈。这只需要你几分钟的时间,但对其他潜在客户、我们的作者和 Packt 来说都非常有价值。谢谢!

目录

  1. 学习机器人编程

  2. 第二版

  3. 为什么订阅?

  4. 贡献者

  5. 关于作者

  6. 关于审稿人

  7. Packt 正在寻找像你这样的作者

  8. 前言

    1. 本书面向的对象

    2. 本书涵盖的内容

    3. 如何充分利用本书

    4. 下载示例代码文件

    5. 代码实战

    6. 下载彩色图像

    7. 使用的约定

    8. 取得联系

    9. 评论

  9. 第1节:基础知识 - 准备机器人

  10. 第1章:机器人简介

    1. 机器人是什么意思?

    2. 探索高级且令人印象深刻的机器人

      1. 火星漫游车
    3. 发现家中的机器人

      1. 洗衣机

      2. 其他家用机器人

    4. 探索工业中的机器人

      1. 机器人手臂

      2. 仓库机器人

    5. 竞技、教育和业余机器人

    6. 摘要

    7. 评估

    8. 进一步阅读

  11. 第2章:探索机器人构建模块 - 代码和电子

    1. 技术要求

    2. 看看机器人内部是什么

    3. 探索机器人组件的类型

      1. 电机的类型

      2. 其他类型的执行器

      3. 状态指示器 - 显示屏、灯光和声音

      4. 传感器的类型

    4. 探索控制器和 I/O

      1. I/O 引脚

      2. 控制器

      3. 选择树莓派

    5. 规划组件和代码结构

    6. 规划物理机器人

    7. 摘要

    8. 练习

    9. 进一步阅读

  12. 第3章:探索树莓派

    1. 技术要求

    2. 探索树莓派的能力

      1. 速度和功率

      2. 连接性和网络

      3. 选择 Raspberry Pi 3A+

    3. 选择连接

      1. 电源引脚

        1. 数据总线

        2. 通用 IO

      2. Raspberry Pi HATs

    4. 什么是 Raspberry Pi OS?

    5. 使用 Raspberry Pi OS 准备 SD 卡

    6. 总结

    7. 评估

    8. 进一步阅读

  13. 第 4 章:为机器人准备无头 Raspberry Pi

    1. 技术要求

    2. 什么是无头系统,为什么它在机器人中很有用?

    3. 在 Raspberry Pi 上设置 Wi-Fi 并启用 SSH

    4. 在网络中找到您的 Pi

      1. 在 Microsoft Windows 上设置 Bonjour

      2. 测试设置

      3. 故障排除

    5. 使用 PuTTY 或 SSH 连接到您的 Raspberry Pi

    6. 配置 Raspberry Pi OS

      1. 重命名您的 Pi

      2. 保护您的 Pi(一点)

      3. 重启和重新连接

      4. 更新您的 Raspberry Pi 上的软件

      5. 关闭您的 Raspberry Pi

    7. 总结

    8. 评估

    9. 进一步阅读

  14. 第 5 章:使用 Git 和 SD 卡副本备份代码

    1. 技术要求

    2. 理解代码如何被破解或丢失

      1. SD 卡数据丢失和损坏

      2. 代码或配置的更改

    3. 策略 1 – 在 PC 上保留代码并上传

    4. 策略 2 – 使用 Git 回到过去

    5. 策略3 – 制作SD卡备份

      1. Windows

      2. Mac

      3. Linux

    6. 总结

    7. 评估

    8. 进一步阅读

  15. 第2节:构建自主机器人 – 将传感器和电机连接到树莓派

  16. 第6章:构建机器人基础 – 轮子、电源和布线

    1. 技术要求

    2. 选择机器人底盘套件

      1. 尺寸

      2. 轮子数量

      3. 轮子和电机

      4. 简单性

      5. 成本

      6. 结论

    3. 选择电机控制器板

      1. 集成级别

      2. 引脚使用

      3. 尺寸

      4. 焊接

      5. 电源输入

      6. 连接器

      7. 结论

    4. 为机器人供电

    5. 测试机器人适配

    6. 组装底座

      1. 安装编码器轮

      2. 安装电机支架

        1. 安装塑料电机支架

        2. 安装金属电机支架

      3. 添加万向轮

      4. 安装轮子

      5. 拉起电线

      6. 安装树莓派

      7. 添加电池

        1. 设置USB移动电源

        2. 安装AA电池夹

      8. 完成的机器人底座

    7. 将电机连接到树莓派

      1. 连接 Motor HAT

      2. 独立电源

    8. 总结

    9. 练习

    10. 进一步阅读

  17. 第7章:驱动和转向 – 使用Python移动电机

    1. 技术要求

    2. 编写代码测试你的电机

      1. 准备库

      2. 测试 – 找到 Motor HAT

      3. 测试 – 展示电机移动

      4. 故障排除

      5. 理解代码的工作原理

    3. 控制机器人

      1. 转向类型

        1. 可转向的车轮

        2. 固定车轮

        3. 其他转向系统

      2. 控制我们正在构建的机器人

    4. 制作机器人对象 – 我们实验中与机器人通信的代码

      1. 为什么要制作这个物体?

      2. 我们在机器人对象中放什么?

    5. 编写脚本以跟随预定路径

    6. 总结

    7. 练习

    8. 进一步阅读

  18. 第8章:使用Python编程距离传感器

    1. 技术要求

    2. 在光学和超声波传感器之间选择

      1. 光学传感器

      2. 超声波传感器

      3. 逻辑电平和移位

      4. 为什么使用两个传感器?

    3. 连接并读取超声波传感器

      1. 将传感器固定到机器人上

      2. 添加电源开关

      3. 连接距离传感器

      4. 安装Python库以与传感器通信

      5. 读取超声波距离传感器

      6. 故障排除

    4. 避免墙壁 - 编写脚本以避免障碍物

      1. 将传感器添加到机器人类中

      2. 制作避障行为

        1. 第一次尝试避障

        2. 更复杂的物体避障

    5. 总结

    6. 练习

    7. 进一步阅读

  19. 第9章:在Python中编程RGB灯带

    1. 技术要求

    2. 什么是RGB灯带?

    3. 比较光带技术

      1. RGB值
    4. 将光带连接到Raspberry Pi上

      1. 将LED灯带连接到机器人上
    5. 使机器人显示代码对象

      1. 制作LED接口

      2. 将LED添加到机器人对象中

      3. 测试一个LED

        1. 故障排除
    6. 使用LED制作彩虹显示

      1. 色彩系统

        1. 色调

        2. 饱和度

        3. 将HSV转换为RGB

      2. 在LED上制作彩虹

    7. 使用光带调试避障行为

      1. 将基本LED添加到避障行为中

      2. 添加彩虹

    8. 总结

    9. 练习

    10. 进一步阅读

  20. 第10章:使用Python控制伺服电机

    1. 技术要求

    2. 伺服电机是什么?

      1. 查看伺服电机内部

      2. 向伺服电机发送输入位置

    3. 使用Raspberry Pi定位伺服电机

      1. 编写控制伺服的代码

      2. 故障排除

      3. 控制直流电机和伺服电机

      4. 校准伺服

    4. 添加俯仰和倾斜机构

      1. 构建套件

      2. 将俯仰和倾斜机构连接到机器人上

    5. 创建俯仰和倾斜代码

      1. 创建伺服对象

      2. 将伺服添加到机器人类中

      3. 环绕俯仰和倾斜头

      4. 运行程序

      5. 故障排除

    6. 构建扫描声纳

      1. 连接传感器

      2. 安装库

      3. 行为代码

        1. 故障排除
    7. 总结

    8. 练习

    9. 进一步阅读

  21. 第11章:使用Python编程编码器

    1. 技术要求

    2. 使用编码器测量行驶距离

      1. 机器使用编码器的位置

      2. 编码器的类型

      3. 编码绝对或相对位置

      4. 编码方向和速度

      5. 我们使用的编码器

    3. 将编码器连接到机器人上

      1. 准备编码器

      2. 抬起Raspberry Pi

      3. 将编码器安装到车架上

      4. 将编码器连接到Raspberry Pi

    4. 在Python中检测行驶距离

      1. 介绍日志记录

      2. 简单计数

        1. 故障排除
      3. 将编码器添加到Robot对象中

        1. 提取类

        2. 将设备添加到Robot对象中

      4. 将脉冲转换为毫米

    5. 直线行驶

      1. 使用PID纠正偏航

      2. 创建Python PID控制器对象

      3. 编写直线行驶的代码

      4. 故障排除此行为

    6. 驱动特定距离

      1. 将单位转换重构到EncoderCounter类中

      2. 设置常量

      3. 创建驱动距离行为

    7. 进行特定转向

      1. 编写drive_arc函数
    8. 总结

    9. 练习

    10. 进一步阅读

  22. 第12章:使用Python进行IMU编程

    1. 技术要求

    2. 了解更多关于IMU的信息

      1. 建议的IMU模型
    3. 焊接 – 将引脚连接到IMU

      1. 制作焊接接头
    4. 将IMU连接到机器人上

      1. 物理放置

      2. 将IMU连接到Raspberry Pi

    5. 读取温度

      1. 安装软件

      2. 故障排除

      3. 读取温度寄存器

        1. 创建界面

        2. 什么是VPython?

        3. 绘制温度图

        4. 运行温度绘图器

      4. 故障排除

      5. 简化VPython命令行

    6. 在Python中读取陀螺仪

      1. 理解陀螺仪

        1. 表示坐标和旋转系统
      2. 将陀螺仪添加到界面

      3. 绘制陀螺仪

    7. 在Python中读取加速度计

      1. 理解加速度计

      2. 将加速度计添加到界面

      3. 以矢量形式显示加速度计

    8. 与磁力计一起工作

      1. 理解磁力计

      2. 添加磁力计接口

      3. 显示磁力计读数

    9. 总结

    10. 练习

    11. 进一步阅读

  23. 第3节:听与看 – 为机器人提供智能传感器

  24. 第13章:机器人视觉 – 使用Pi相机和OpenCV

    1. 技术要求

    2. 设置树莓派相机

      1. 将相机连接到云台机构

      2. 连接相机

    3. 设置计算机视觉软件

      1. 设置Pi相机软件

      2. 从树莓派获取图片

      3. 安装OpenCV和支持库

    4. 构建树莓派相机流应用

      1. 设计OpenCV相机服务器

      2. 编写CameraStream对象

      3. 编写图像服务器主应用

      4. 构建模板

      5. 运行服务器

      6. 故障排除

    5. 在流式传输时运行后台任务

      1. 编写Web应用核心

        1. 使行为可控

        2. 制作控制模板

        3. 运行可控图像服务器

    6. 用Python跟踪彩色对象

      1. 将图片转换为信息

      2. 增强PID控制器

      3. 编写行为组件

        1. 编写控制模板

        2. 编写行为代码

      4. 运行行为

      5. 故障排除

    7. 用Python跟踪人脸

      1. 在图像中查找对象

        1. 转换为积分图像

        2. 扫描基本特征

      2. 规划我们的行为

      3. 编写人脸追踪代码

      4. 运行人脸追踪行为

      5. 故障排除

    8. 总结

    9. 练习

    10. 进一步阅读

  25. 第14章:使用Python进行线跟随

    1. 技术要求

    2. 线跟随简介

      1. 什么是线跟随?

      2. 工业应用

      3. 线跟随的类型

    3. 制作线跟随测试轨道

      1. 准备测试轨道材料

      2. 绘制线条

    4. 跟随线计算机视觉管道

      1. 相机线跟踪算法

      2. 管道

    5. 尝试使用测试图像进行计算机视觉

      1. 为什么使用测试图像?

      2. 捕获测试图像

      3. 用Python编写代码查找线的边缘

      4. 从边缘定位线条

      5. 尝试没有清晰线条的测试图片

    6. 使用PID算法进行循线

      1. 创建行为流程图

      2. 将时间添加到我们的PID控制器

      3. 编写初始行为

      4. 调整PID

      5. 故障排除

    7. 再次找到线条

    8. 总结

    9. 练习

    10. 进一步阅读

  26. 第15章:使用Mycroft与机器人进行语音通信

    1. 技术要求

    2. 介绍Mycroft – 理解语音代理术语

      1. 语音转文本

      2. 唤醒词

      3. 表述

      4. 意图

      5. 对话

      6. 词汇

      7. 技能

    3. 在机器人上监听语音的限制

    4. 将声音输入和输出添加到Raspberry Pi

      1. 物理安装

      2. 在Raspberry Pi上安装语音代理

      3. 安装ReSpeaker软件

        1. 故障排除
      4. 让Mycroft与声卡通信

      5. 开始使用Mycroft

        1. Mycroft客户端

        2. 与Mycroft交谈

      6. 故障排除

    5. 编程Flask API

      1. Mycroft控制机器人的概述

      2. 远程启动行为

        1. 管理机器人模式
      3. 编程Flask控制API服务器

      4. 故障排除

    6. 在树莓派上使用Mycroft编程语音代理

      1. 构建意图

        1. 设置文件

        2. 需求文件

        3. 创建词汇文件

        4. 对话文件

        5. 当前技能文件夹

      2. 故障排除

      3. 添加另一个意图

        1. 词汇和对话

        2. 添加代码

        3. 使用新意图运行

    7. 总结

    8. 练习

    9. 进一步阅读

  27. 第16章:使用IMU深入探索

    1. 技术要求

    2. 编程虚拟机器人

      1. 在VPython中建模机器人

        1. 故障排除
    3. 使用陀螺仪检测旋转

      1. 校准陀螺仪

      2. 使用陀螺仪旋转虚拟机器人

        1. 故障排除
    4. 使用加速度计检测俯仰和滚转

      1. 从加速度计向量获取俯仰和滚转

        1. 故障排除
      2. 平滑加速度计

        1. delta时间
      3. 融合加速度计和陀螺仪数据

        1. 故障排除
    5. 使用磁力计检测航向

      1. 校准磁力计

        1. 故障排除

        2. 测试校准值

        3. 如果圆圈没有在一起时该怎么做

    6. 从磁力计获取粗略航向

    7. 结合传感器进行定位

      1. 解决180度问题
    8. 从IMU数据驾驶机器人

    9. 总结

    10. 练习

    11. 进一步阅读

  28. 第17章:使用手机和Python控制机器人

    1. 技术要求

    2. 当语音控制不起作用时——为什么我们需要驾驶

    3. 菜单模式 – 选择机器人的行为

      1. 管理机器人模式

      2. 故障排除

      3. 网络服务

      4. 模板

      5. 运行它

      6. 故障排除

    4. 选择控制器——我们将如何驾驶机器人,以及为什么

      1. 设计和概述
    5. 为远程驾驶准备树莓派——启动基本驾驶系统

      1. 增强图像应用核心

      2. 编写手动驾驶行为

      3. 模板(网页)

      4. 样式表

      5. 创建滑块的代码

      6. 运行此程序

      7. 故障排除

    6. 使机器人完全可通过手机操作

      1. 使菜单模式与Flask行为兼容

      2. 加载视频服务

      3. 美化菜单

        1. 将菜单模板转换为按钮
    7. 使菜单在Pi启动时启动

      1. 向菜单服务器添加灯光

      2. 使用systemd自动启动机器人

        1. 故障排除
    8. 总结

    9. 练习

    10. 进一步阅读

  29. 第4节:将机器人技术进一步发展

  30. 第18章:进一步提升你的机器人编程技能

    1. 在线机器人构建社区 - 论坛和社交媒体

      1. 了解的YouTube频道

      2. 技术问题 - 哪里可以获得帮助

    2. 遇见机器人构建者 - 竞赛、创客空间和聚会

      1. 创客空间

      2. 创客市集、树莓派聚会和道场

      3. 竞赛

    3. 进一步技能的建议 - 3D打印、焊接、PCB和CNC

      1. 设计技能

        1. 用于说明和图表的2D设计

        2. 3D CAD

      2. 塑造和构建的技能

        1. 机械技能和工具

        2. 手工艺技能和工具

      3. 电子技能

        1. 电子原理

        2. 进一步学习焊接

        3. 自定义电路

    4. 在计算机视觉方面获取更多信息

      1. 书籍

      2. 在线课程

      3. 社交媒体

    5. 扩展到机器学习

      1. 机器人操作系统
    6. 总结

    7. 进一步阅读

  31. 第19章:规划你的下一个机器人项目 - 整合所有元素

    1. 技术要求

    2. 可视化你的下一个机器人

    3. 制作框图

    4. 选择零件

      1. 测试装配图

        1. 购买零件

        2. 组装你的机器人

    5. 为机器人编写代码的规划

      1. 系统层

        1. 数据流图

        2. 正式图表

        3. 编程机器人

    6. 让世界知道

    7. 摘要

  32. 你可能喜欢的其他书籍

    1. 留下评论 - 让其他读者了解你的想法

地标

  1. 封面

  2. 目录

posted @ 2025-09-23 21:56  绝不原创的飞龙  阅读(7)  评论(0)    收藏  举报