Python-树莓派编程秘籍第二版-全-

Python 树莓派编程秘籍第二版(全)

原文:zh.annas-archive.org/md5/4354b755f7835efe4b0675003f4b507e

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

自从 2012 年 2 月发布 Raspberry Pi 计算机以来,数百万人开始接触到了一种新的计算方式。现代家用电脑、平板电脑和手机通常专注于向用户提供内容以供消费,无论是作为被动观众还是通过游戏和活动进行基本互动。

然而,Raspberry Pi 颠覆了这一概念。其理念是用户提供输入和想象力,Raspberry Pi 成为他们创造力的延伸。Raspberry Pi 提供了一个简单、低成本的平台,您可以使用它来实验和玩弄自己的想法。它不会向您提供信息;它会让您亲身体验。

本书将我发现的与 Raspberry Pi 相关的所有令人兴奋和有趣的事情都整理成了一个易于遵循的格式。

我希望人们会阅读这本书,开始他们自己的 Raspberry Pi 之旅;它提供了很多机会,本书旨在展示您可以使用它实现什么。

就像任何一本好的食谱书一样,页面应该磨损并使用,它应该是始终被从架子上取下来参考的东西。我希望它将成为您自己的、个人的、首选的参考资料。

本书涵盖的内容

第一章,Raspberry Pi 计算机入门,介绍了 Raspberry Pi,并探讨了它可以以各种方式设置和使用的方法,包括如何在网络上使用它以及如何通过另一台计算机远程连接。

第二章,从 Python 字符串、文件和菜单开始,指导我们如何使用 Python 3 迈出第一步,从基础知识开始,操作文本,使用文件,并创建菜单来运行我们的程序。

第三章,使用 Python 进行自动化和生产率,解释了如何使用图形用户界面创建我们自己的应用程序和实用工具。

第四章,创建游戏和图形,解释了如何使用 Tkinter Canvas 创建绘图应用程序和图形游戏。

第五章,创建 3D 图形,讨论了我们可以如何利用 Raspberry Pi 图形处理单元的隐藏力量来学习 3D 图形和景观,并制作我们自己的 3D 迷宫进行探索。

第六章,使用 Python 驱动硬件,确立了这样一个事实:为了最大限度地体验 Raspberry Pi,我们真的必须使用它来配合我们自己的电子设备。它讨论了如何使用 LED 和开关创建电路,并使用它们来指示系统状态和提供控制。最后,它展示了如何创建我们自己的游戏控制器、光显示和视觉持久性文本显示。

第七章, 感知和显示现实世界数据,解释了使用模拟数字转换器向树莓派提供传感器读数的方法。我们发现了如何实时存储和绘制数据,以及如何在 LCD 文本显示器上显示它。接下来,我们将数据记录在 SQL 数据库中,并在我们自己的 Web 服务器上显示。最后,我们将数据传输到互联网,这将使我们能够在世界任何地方查看和共享捕获的数据。

第八章, 使用树莓派摄像头模块创建项目,教我们如何使用树莓派摄像头模块,创建我们自己的应用程序来制作延时视频、定格动画,以及通过二维码控制的睡前故事阅读器。此外,我们还利用功能强大的图像处理库 OpenCV 进行颜色识别和物体(或在这种情况下,是乌龟)跟踪。

第九章, 构建机器人,带你构建两种不同类型的机器人(Rover-Pi 和 Pi-Bug),以及驱动基于伺服的机器人臂。我们探讨了电机和伺服控制方法、使用传感器,以及添加指南针传感器进行导航。

第十章, 与技术接口,教我们如何使用树莓派触发远程主电源插座,通过它可以控制家用电器。我们学习了如何通过串行接口与树莓派通信,并使用智能手机通过蓝牙控制一切。最后,我们探讨了创建我们自己的应用程序来控制 USB 设备。

附录, 硬件和软件列表,为我们提供了本书中使用的全部硬件组件和模块的完整列表,以及购买它们的合适地点。还提供了使用的软件的完整列表,以及链接到文档。

你需要这本书的物品

本书侧重于使用 Python 3 与树莓派结合;因此,需要一个基本的树莓派设置。本书的第一章到第五章仅使用树莓派;除了标准设置外,不需要额外的硬件。

标准设置将包括树莓派(型号 A 或 B,版本 1、2 或 3);安装了 Raspbian 的 SD 卡;合适的微型 USB 电源;以及兼容 HDMI 的屏幕、键盘和鼠标。你还需要下载并安装各种软件包;因此,树莓派应该有一个工作的互联网连接。

第一章, 树莓派计算机入门,也描述了如何使用笔记本电脑或另一台计算机的屏幕/键盘/鼠标来访问树莓派(你只需要一根网络线和电源)。

第六章,使用 Python 驱动硬件,和第七章,感知和显示现实世界数据,展示了电子组件如何连接到树莓派的接口。为了完成这些章节,您将需要这些组件。

第八章,使用树莓派摄像头模块创建项目,需要每个项目都使用树莓派摄像头模块(尽管可以通过调整代码用兼容的 USB 摄像头替代)。

第九章,构建机器人,使用各种硬件和电子设备来构建您自己的机器人。您可以使用自己的部件或合适的套件来完成这项工作。

第十章,与技术接口,展示了如何使用各种模块和套件将附加硬件连接到树莓派的接口。

在附录中提供了所使用硬件的完整列表(以及可能的购买地点),硬件和软件列表

本书面向对象:

本书旨在为任何希望充分利用树莓派体验的人而编写。本书逐步介绍 Python,从基础知识开始,逐步过渡到更高级的主题,例如使用 3D 图形和与硬件接口。

虽然您不需要熟悉 Python、树莓派或电子学,但本书涉及了广泛的主题。理想情况下,您应该尝试每个章节,看看您喜欢什么,并以此作为起点去发现和学习更多。

书中的每个示例都包括完整的设置说明、完整的代码列表,以及您所做之事及其原因的概述。这将使您能够快速获得结果,最重要的是,理解您是如何实现这些结果的。

所有示例均使用 Python 3 编写,提供了清晰和详细的解释,以便您能够适应并在自己的项目中使用所有信息。

随着您在本书中的进展,它将解释如何高效地构建和开发您的代码,基于您在进展过程中可以应用的各种技术。到结束时,您将拥有一套技能工具,可以应用于您想象力所激发的任何活动。

安全性和使用电子设备

本书鼓励您进行实验并将自己的电路连接到通用的输入/输出树莓派 GPIO 引脚。这是同时学习电子和软件的极好方法。然而,重要的是要记住 GPIO 引脚是没有保护的,如果接线错误,很容易损坏,甚至可能导致树莓派完全停止工作。因此,在打开树莓派之前,应仔细遵循说明和接线图,并仔细检查一切。

本书描述的所有电路、模块和组件仅作为演示示例。它们未经长期使用测试,不应无人看管或在没有适当安全措施的情况下用于安全关键应用。请记住,所有电子设备都必须经过严格的安全测试,以确保在发生故障的情况下,不会对人员或财产造成伤害。

您绝不应在没有适当培训的情况下尝试修改或更改连接到市电的设备,并且您绝不应将任何自制的设备直接连接到市电。

章节

在本书中,您会发现一些经常出现的标题(准备工作、如何操作、工作原理、更多内容以及参考以下内容)。

为了清楚地说明如何完成食谱,我们使用以下这些章节:

准备工作

本节向您介绍食谱中可以期待的内容,并描述如何设置任何软件或食谱所需的任何初步设置。

如何操作…

本节包含遵循食谱所需的步骤。

工作原理…

本节通常包含对上一节发生情况的详细解释。

更多内容…

本节包含有关食谱的附加信息,以便使读者对食谱有更深入的了解。

参考以下内容

本节提供对食谱其他有用信息的链接。

惯例

在本书中,您会发现多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:“在格式化后或新的 SD 卡上,复制NOOBS_vX.zip文件的文件内容。”

代码块设置如下:

network={ 
ssid="theSSID" 
key_mgmt=NONE 
}

任何命令行输入或输出都应如下编写:

sudo mount –t vfat /dev/mmcblk0p1 ~/recovery 

新术语重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下显示:“对于 OS X 或 Linux,点击终端以打开与树莓派的连接。”

注意事项

警告或重要注意事项以如下框中的形式出现。

小贴士

小技巧和窍门如下所示。

读者反馈

我们读者的反馈总是受欢迎的。请告诉我们您对这本书的看法——您喜欢或不喜欢的地方。读者反馈对我们很重要,因为它帮助我们开发出您真正能从中获得最大价值的标题。

要发送一般性反馈,请简单地将电子邮件发送到<feedback@packtpub.com>,并在邮件主题中提及书的标题。

如果您在某个主题领域有专业知识,并且您有兴趣撰写或为本书做出贡献,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在,您已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大价值。

下载示例代码

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

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

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标指针悬停在顶部的支持标签上。

  3. 点击代码下载 & 错误清单

  4. 搜索框中输入书的名称。

  5. 选择您想要下载代码文件的书籍。

  6. 从下拉菜单中选择您购买本书的地方。

  7. 点击代码下载

您还可以通过点击 Packt Publishing 网站上书籍网页上的代码文件按钮下载代码文件。您可以通过在搜索框中输入书的名称来访问此页面。请注意,您需要登录到您的 Packt 账户。

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

  • Windows 版的 WinRAR / 7-Zip

  • Mac 版的 Zipeg / iZip / UnRarX

  • Linux 版的 7-Zip / PeaZip

本书代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Raspberry-Pi-for-Python-Programmers-Cookbook-Second-Edition。我们还有来自我们丰富图书和视频目录的其他代码包,可在github.com/PacktPublishing/找到。请查看它们!

错误清单

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。

要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。

海盗行为

互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的非法复制我们的作品,请立即向我们提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过<copyright@packtpub.com>与我们联系,并提供疑似盗版材料的链接。

我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面提供的帮助。

询问

如果您对本书的任何方面有问题,您可以通过<questions@packtpub.com>联系我们,我们将尽力解决问题。

第一章. 使用 Raspberry Pi 计算机入门

在本章中,我们将涵盖以下菜谱:

  • 连接 Raspberry Pi

  • 使用 NOOBS 设置你的 Raspberry Pi SD 卡

  • 通过 LAN 连接器将你的 Raspberry Pi 连接到互联网进行网络连接

  • 在 Raspberry Pi 上使用内置的 Wi-Fi 和蓝牙

  • 手动配置你的网络

  • 直接连接到笔记本电脑或计算机

  • 通过 USB Wi-Fi 拨号连接将你的 Raspberry Pi 连接到互联网

  • 通过代理服务器连接到互联网

  • 通过 VNC 在网络上远程连接到 Raspberry Pi

  • 通过 SSH(和 X11 转发)在网络上远程连接到 Raspberry Pi

  • 使用 SMB 与 Raspberry Pi 的家目录共享

  • 保持 Raspberry Pi 更新

简介

本章介绍了 Raspberry Pi 以及首次设置的过程。我们将连接 Raspberry Pi 到合适的显示器、电源和外设。我们将在 SD 卡上安装操作系统。这是系统启动所必需的。接下来,我们将确保我们能够通过本地网络成功连接到互联网。

最后,我们将利用网络提供远程连接到和/或控制 Raspberry Pi 的方法,以及确保系统保持更新。

一旦你完成了本章中的步骤,你的 Raspberry Pi 就可以用于编程了。如果你已经设置了你的 Raspberry Pi 并正在运行,请确保你浏览以下部分,因为这里有许多有用的提示。

介绍 Raspberry Pi

Raspberry Pi 是由 Raspberry Pi 基金会创建的单板电脑,该基金会是一个慈善机构,其主要目的是将低级计算机技能重新引入英国的儿童。目标是重新点燃 20 世纪 80 年代的微型计算机革命,这场革命产生了一代熟练的程序员。

即使在 2012 年 2 月底发布电脑之前,很明显 Raspberry Pi 已经在全球范围内获得了巨大的关注,并且在撰写本书时,销量已超过 1000 万台。以下图片显示了几个不同的 Raspberry Pi 型号:

介绍 Raspberry Pi

Raspberry Pi Model 3B、Model A+ 和 Pi Zero

这个名字有什么含义?

Raspberry Pi 这个名字是创建一个基于水果的替代电脑(如苹果、黑莓和杏)的愿望与对原始概念的一种致敬,即一个可以用 Python(简称 Pi)编程的简单电脑。

在这本书中,我们将探讨这个小电脑,找出如何设置它,然后使用 Python 编程语言逐章探索其功能。

为什么选择 Python?

人们经常问,“为什么选择 Python 作为 Raspberry Pi 上的编程语言?”事实是,Python 只是可以在 Raspberry Pi 上使用的许多编程语言之一。

你可以选择许多编程语言,从高级图形块编程,如Scratch,到传统的C,再到BASIC,甚至原始的机器代码汇编器。一个好的程序员通常必须成为代码多语言者,以便能够发挥每种语言的优势和劣势,以最好地满足他们期望的应用需求。了解不同的语言(和编程技术)如何尝试克服将“你想要的”转换为“你得到的”的挑战是有用的,因为这也是你在编程时试图做到的事情。

Python 被选为学习编程的好起点,因为它提供了一套丰富的编码工具,同时仍然允许编写简单的程序而无需烦恼。这使得初学者可以逐渐接触现代编程语言所基于的概念和方法,而无需一开始就了解所有内容。它非常模块化,有许多额外的库可以导入,以快速扩展功能。你会发现随着时间的推移,这会鼓励你做同样的事情,你将想要创建自己的模块,可以将它们插入到自己的程序中,从而迈出结构化编程的第一步。

与所有编程语言一样,Python 并不完美;例如,在行首添加空格通常会破坏你的代码(在 Python 中,缩进非常重要;它们定义了代码块是如何组合在一起的)。一般来说,Python 运行速度较慢;由于它是解释的,它在运行程序时需要时间来创建模块。如果你需要响应时间敏感的事件,这可能会成为一个问题。然而,你可以预编译 Python 或使用用其他语言编写的模块来克服这个问题。它隐藏了细节;这既是优点也是缺点。它非常适合初学者,但在你必须猜测数据类型等细节时可能会很困难。然而,这反过来又迫使你考虑所有可能性,这可能是好事。

Python 2 和 Python 3

对于初学者来说,一个巨大的困惑来源是,Raspberry Pi 上有两个版本的 Python(版本 2.7版本 3.4),它们之间不兼容,因此为 Python 2.7 编写的代码可能无法在 Python 3.4 上运行(反之亦然)。

Python 软件基金会一直在努力改进和推动语言的发展,这有时意味着他们不得不牺牲向后兼容性,以便拥抱新的改进(并且重要的是,移除冗余和过时的做事方式)。

注意

支持 Python 2 或 Python 3

有许多工具可以帮助从 Python 2 过渡到 Python 3,包括像2to3这样的转换器,它将解析并更新你的代码以使用 Python 3 的方法。这个过程并不完美,在某些情况下,你可能需要手动重写部分代码并完全重新测试一切。你可以编写同时支持两者的代码和库。import __future__语句允许你导入 Python 3 的友好方法,并使用 Python 2.7 运行它们。

应该使用哪个版本的 Python?

实际上,选择使用哪个版本将取决于你的意图。例如,你可能需要 Python 2.7 库,这些库在 Python 3.4 中尚未提供。Python 3 自 2008 年以来一直可用,因此这些通常是较旧或较大的库,尚未被翻译。在许多情况下,有新的替代方案可以替代旧库;然而,它们的支持可能各不相同。

在这本书中,我们使用了 Python 3.4,它也与 Python 3.3 和 3.2 兼容。

树莓派家族——Pi 的简要历史

自从发布以来,树莓派经历了各种迭代,包括对原始树莓派 Model B 单元的小型和大型的更新和改进。虽然一开始可能会让人困惑,但目前有三种基本的树莓派型号(以及一个特殊型号)。

主要的旗舰型号被称为Model B。它拥有所有的连接和功能,以及最大的 RAM 和最新的处理器。多年来,已经推出了几个版本,最值得注意的是 Model B(最初有 256 MB RAM,后来增加到 512 MB RAM)和 Model B+(将 26 针 GPIO 增加到 40 针,改为使用 micro SD 卡槽,并且有四个 USB 端口而不是两个)。这些原始型号都使用了 Broadcom BCM2835 SOC(系统级芯片的简称),由一个 700 MHz 的 ARM11 核心和 VideoCore IV GPU(图形处理单元的简称)组成。

2015 年发布的树莓派 2B 型(也称为 2B)引入了新的 Broadcom BCM2836 系统级芯片(SOC),提供了一款四核 32 位 ARM Cortex A7 1.2 GHz 处理器和 GPU,以及 1 GB 的 RAM。改进的 SOC 增加了对 Ubuntu 和 Windows 10 IoT 的支持。最后我们有了最新的树莓派 3B 型,它使用了另一个新的 Broadcom BCM2837 SOC,提供了一款四核 64 位 ARM Cortex-A53 和 GPU,同时还增加了板载 Wi-Fi 和蓝牙。

Model A一直被定位为一个简化版本。虽然与 Model B 具有相同的 SOC,但连接有限,仅有一个 USB 端口和没有有线网络(LAN)。Model A+再次增加了更多的 GPIO 引脚和 micro SD 插槽。然而,RAM 后来升级到 512 MB,并且仍然只有一个 USB 端口/没有 LAN。Model A 上的 Broadcom BCM2835 SOC 尚未更新(因此仍然是单核 ARM11);然而,预计 2016/2017 年将推出 Model 3A(很可能是使用 BCM2837)。

Pi Zero是 Raspberry Pi 的超紧凑版本,旨在用于成本和空间至关重要的嵌入式应用。它具有与其他型号相同的 40 针 GPIO 和微型 SD 卡插槽,但缺少板载显示(CSI 和 DSI)连接。它仍然具有 HDMI(通过迷你 HDMI)和单个微型 USB OTG(即插即用)连接。尽管在 Pi Zero 的第一版中未提供,但最新型号也包括用于板载摄像头的 CSI 连接。

注意

Pi Zero 在 2015 年因其与 Raspberry Pi 基金会杂志《The MagPi》一同赠送而闻名,这使得该杂志成为首个在其封面上赠送电脑的杂志!这让我感到非常自豪,因为(正如你可能在本书开头的传记中读到的那样)我是该杂志的创始人之一。

这种特殊型号被称为计算模块。它采用 200 针 SO-DIMM 卡的形式。它旨在用于工业用途或商业产品中,其中所有外部接口都由主机/主板提供,模块将被插入其中。示例产品包括 Slice 媒体播放器(fiveninjas.com)和 Otto 相机。当前模块使用 BCM2835,尽管预计 2016 年将推出更新的计算模块(CM3)。

Raspberry Pi 的维基百科页面提供了所有不同版本及其规格的完整列表:

en.wikipedia.org/wiki/Raspberry_Pi#Specifications

应该选择哪个 Pi?

本书的所有章节都与所有当前版本的 Raspberry Pi 兼容,但建议从 Model 3B 开始,这是最佳选择。它提供了最佳性能(特别是对于第五章中使用的 GPU 示例,即创建 3D 图形,以及第八章中使用的 OpenCV 示例,即使用 Raspberry Pi 摄像头模块创建项目非常有用),许多连接,以及内置 Wi-Fi,这可以非常方便。

Pi Zero 推荐用于那些希望低功耗或减小重量/尺寸但不需要 Model 3B 完整处理能力的项目。然而,由于其超低成本,Pi Zero 在开发完成后部署完整项目时非常理想。

连接 Raspberry Pi

有许多方法可以连接 Raspberry Pi 并使用各种接口查看和控制内容。对于典型用途,大多数用户将需要电源、显示(带音频)以及键盘和鼠标等输入方法。要访问互联网,请参考通过 LAN 连接器连接 Raspberry Pi 的网络和连接到互联网在 Raspberry Pi 上使用内置 Wi-Fi 和蓝牙的食谱。

准备工作

在您可以使用树莓派之前,您需要一个安装了操作系统或带有新开箱系统NOOBS)的 SD 卡,正如在使用 NOOBS 设置您的树莓派 SD 卡菜谱中讨论的那样。

以下部分将详细介绍您可以连接到树莓派的设备类型,以及如何和在哪里连接它们。

正如您稍后会发现的那样,一旦您设置了树莓派,您可能会决定通过远程连接并通过网络链接使用它,在这种情况下,您只需要电源和网络连接。请参阅以下部分:通过 VNC 远程连接到树莓派的网络通过 SSH(以及 X11 转发)远程连接到树莓派的网络

如何操作…

树莓派的布局如下所示:

如何操作…

树莓派的连接布局(3B 型号、A+型号和 Pi Zero)

以下是对前一个图的描述:

  • 显示:树莓派支持以下三种主要显示连接;如果同时连接了 HDMI 和复合视频,它将默认只使用 HDMI。

    • HDMI

      为了获得最佳效果,请使用具有 HDMI 连接的电视或显示器,这样就可以实现最佳分辨率显示(1080p),并且还可以输出数字音频。如果您的显示器有 DVI 连接,您可能可以使用适配器通过 HDMI 连接。DVI 连接有几种类型;一些支持模拟(DVI-A),一些支持数字(DVI-D),还有一些两者都支持(DVI-I)。树莓派只能通过 HDMI 提供数字信号,因此建议使用 HDMI-to-DVI-D 适配器(以下截图中有勾选标记)。这缺少了四个额外的模拟引脚(以下截图中有叉号标记),因此它可以适应 DVI-D 和 DVI-I 类型的插座:

      如何操作…

      HDMI-to-DVI 连接(DVI-D 适配器)

      如果您想使用较旧的显示器(具有 VGA 连接),则需要额外的 HDMI-to-VGA 转换器。树莓派还支持一个基本的 VGA 适配器(VGA Gert666 适配器),它直接从 GPIO 引脚驱动。然而,这会占用 40 引脚头上的所有引脚(较旧的 26 引脚型号将不支持 VGA 输出)。

    • 模拟

      另一种显示方法是使用模拟复合视频连接(通过话筒插座);这也可以连接到 S-Video 或欧洲 SCART 适配器。然而,模拟视频输出的最大分辨率为 640 x 480 像素,因此它不适合一般使用。

      如何操作…

      3.5mm 话筒模拟连接

    • 当使用 RCA 连接或 DVI 输入时,必须通过模拟音频连接单独提供音频。为了简化制造过程(通过避免通孔组件),Pi Zero 没有模拟音频或 RCA 插座用于模拟视频(尽管可以通过一些修改添加它们)。

      直接显示 DSI

      由树莓派基金会生产的触摸显示屏可以直接连接到 DSI 插槽。这可以在同时连接和使用 HDMI 或模拟视频输出时创建一个双显示器设置。

  • 立体声模拟音频(除 Pi Zero 外所有型号):这为耳机或放大扬声器提供模拟音频输出。可以通过桌面上的树莓派配置工具在模拟(立体声插座)和数字(HDMI)之间切换音频,或者通过命令行使用amixeralsamixer

    注意

    要在终端读取手册之前获取有关特定命令的更多信息,您可以在终端中使用以下man命令(大多数命令都应该有一个):

    man amixer
    
    

    一些命令也支持--help选项以提供更简洁的帮助,如下所示:

    amixer --help
    
    
  • 网络(不包括 A 型和 Pi Zero 型号):网络连接将在本章后面的“网络和通过 LAN 连接器连接树莓派到互联网”配方中进行讨论。如果我们使用 A 型树莓派,可以添加 USB 网络适配器以添加有线或甚至无线网络(请参阅“通过 USB Wi-Fi 适配器连接树莓派到互联网的网络和连接”配方)。

  • 板载 Wi-Fi 和蓝牙(仅限 3B 型号):

  • 3B 型号内置 802.11n Wi-Fi 和蓝牙 4.1;请参阅“在树莓派上使用内置 Wi-Fi 和蓝牙”配方。

  • USB(A 型和 Zero 型号 x1,1B 型号 x2,2B 和 3B 型号 x4)——使用键盘和鼠标:

    树莓派应该与大多数可用的 USB 键盘和鼠标兼容。您还可以使用使用 RF 适配器的无线鼠标和键盘,但对于使用蓝牙适配器的项目,需要额外的配置。

    如果电源供应不足或设备消耗过多电流,您可能会遇到键盘按键似乎卡住的情况,在严重的情况下,SD 卡可能会损坏。

    注意

    USB 电源可能在 2012 年 10 月之前可用的早期 Model B 修订版 1 的板子上成为一个更大的问题。它们在 USB 输出上增加了额外的多熔丝,如果超过 140 mA 的电流,则会跳闸。多熔丝可能需要几个小时或几天才能完全恢复,因此即使在电源改善后,也可能出现不可预测的行为。

    您可以通过它缺少的四个安装孔来识别修订版 1 的板子。

    Debian Linux(Raspbian 所基于的操作系统)支持许多常见的 USB 设备,例如闪存驱动器、硬盘驱动器(可能需要外部电源),相机、打印机、蓝牙和 Wi-Fi 适配器。一些设备将自动检测,而其他设备则需要安装驱动程序。

  • 微型 USB 电源:Raspberry Pi 需要一个 5V 电源,可以舒适地提供至少 1000 mA(建议 1500 mA 或更多,尤其是对于更耗电的 Model 2 和 3),通过微型 USB 连接。可以使用便携式电池组为单元供电,例如适合为平板电脑供电或充电的电池组。同样,确保它们可以提供 1000 mA 或以上的 5V 电压。

在连接电源之前,你应该尽量将所有其他连接到 Raspberry Pi 上。然而,USB 设备、音频和网络在运行时可以连接和移除,而不会出现问题。

还有更多...

除了你会在计算机上期望看到的标准主要连接外,Raspberry Pi 还有许多其他连接。

二级硬件连接

以下每个连接都为 Raspberry Pi 提供了额外的接口:

  • 20 x 2 GPIO 引脚头(Model A+、B+、2 B、3 B 和 Pi Zero):这是 Raspberry Pi 的主要 40 引脚 GPIO 引脚头,用于直接与硬件组件接口。我们在第 6、7、9 和 10 章中使用此连接。本书中的食谱也与具有 13 x 2 GPIO 引脚头的较老型号的 Raspberry Pi 兼容。

  • P5 8 x 2 GPIO 引脚头(仅限 Model 1 B 版本 2.0):我们在书中没有使用这个。

  • 复位连接:这在较晚的型号上存在(没有引脚)。当引脚 1(复位)和引脚 2(地)连接在一起时,会触发复位。我们在第六章的使用 Python 驱动硬件食谱中使用此功能。

  • GPU/LAN JTAG联合测试行动小组JTAG)是一种编程和调试接口,用于配置和测试处理器。这些在新型号上作为表面焊盘存在。使用此接口需要专用 JTAG 设备。我们在书中没有使用此功能。

  • 直接相机 CSI 连接:此连接支持 Raspberry Pi 相机模块(如第八章中所述,使用 Raspberry Pi 相机模块创建项目)。请注意,Pi Zero 的 CSI 连接器比其他型号小,因此需要不同的排线连接器。

  • 直接显示 DSI:此连接支持直接连接的显示器,一个 7 英寸 800 x 600 电容式触摸屏。

使用 NOOBS 设置你的 Raspberry Pi SD 卡

Raspberry Pi 在启动前需要将操作系统加载到 SD 卡上。设置 SD 卡最简单的方法是使用NOOBS;你可能发现你可以购买已经预装了 NOOBS 的 SD 卡。

NOOBS 提供了一个初始启动菜单,提供将多个可用的操作系统安装到 SD 卡上的选项。

准备工作

由于 NOOBS 创建了一个恢复分区以保留原始安装镜像,建议使用 8 GB 或更大的 SD 卡。您还需要一个 SD 卡读卡器(经验表明,一些内置读卡器可能会引起问题,因此建议使用外部 USB 类型读卡器)。

如果您使用的是之前使用过的 SD 卡,您可能需要重新格式化它以删除任何之前的分区和数据。NOOBS 期望 SD 卡包含一个单独的 FAT32 分区。如果您使用 Windows 或 Mac OS X,您可以使用 SD 协会的格式化工具,如下面的截图所示(可在www.sdcard.org/downloads/formatter_4/找到):

准备工作

使用 SD formatter 删除 SD 卡上的任何分区

选项设置对话框中,设置格式大小调整。这将删除之前创建的所有 SD 卡分区。

如果使用 Linux,您可以使用gparted清除任何之前的分区并将其重新格式化为 FAT32 分区。

完整的 NOOBS 包(通常略大于 1 GB)包含 Raspbian,这是最受欢迎的 Raspberry Pi 操作系统镜像。还有一个没有预装操作系统的 NOOBS 精简版(尽管需要 20 MB 的初始下载和 Raspberry Pi 的网络连接,以便直接下载您打算使用的操作系统)。

NOOBS 可在www.raspberrypi.org/downloads找到,文档可在github.com/raspberrypi/noobs找到。

如何操作…

通过执行以下步骤,我们将准备 SD 卡以运行 NOOBS。这将允许我们选择和安装我们想要使用的操作系统:

  1. 准备您的 SD 卡。

  2. 在新格式化的或新的 SD 卡上,复制NOOBS_vX.zip文件的文件内容。当复制完成时,您应该得到以下类似 SD 卡的截图:如何操作…

    NOOBS 文件已提取到 SD 卡上

    注意

    文件可能因 NOOBS 的不同版本而略有不同,您电脑上显示的图标也可能不同。

  3. 现在,您可以将卡插入 Raspberry Pi,连接键盘和显示器,并打开电源。有关所需物品和如何操作的详细信息,请参阅连接 Raspberry Pi菜谱。

默认情况下,NOOBS 将通过 HDMI 连接显示。如果您有另一种类型的屏幕(或者您什么也没看到),您需要手动通过按1234来选择输出类型,具体功能如下:

  • 键 1 代表标准 HDMI模式(默认模式)

  • 键 2 代表安全 HDMI模式(如果未检测到输出,则为替代 HDMI 设置)

  • 键 3 代表复合 PAL(用于通过 RCA 模拟视频连接的连接)

  • 键 4 代表复合 NTSC(再次,用于通过 RCA 连接器连接)

此显示设置也将应用于已安装的操作系统。

稍后,您将看到 NOOBS 选择屏幕,列出了可用的发行版(离线版本仅包括 Raspbian)。还有更多可用的发行版,但只有选定的那些可以通过 NOOBS 系统直接访问。点击Raspbian,因为这是本书中使用的操作系统。

按下Enter键或点击安装操作系统,并确认您希望覆盖卡上的所有数据。这将覆盖之前使用 NOOBS 安装的任何发行版,但不会删除 NOOBS 系统;您可以在任何时候通过在开机时按下Shift键返回。

根据卡的速度,将数据写入卡中大约需要 20 到 40 分钟。当操作完成并且出现图像应用成功的消息时,点击确定,树莓派将开始启动进入树莓派桌面

如何工作…

以这种方式将镜像文件写入 SD 卡的目的,是为了确保 SD 卡格式化带有预期的文件系统分区和正确启动操作系统所需的文件。

当树莓派开机时,它会加载 GPU 内部内存中的一些特殊代码(通常由树莓派基金会称为二进制 blob)。二进制 blob 提供了读取 SD 卡上BOOT 分区所需的指令,在这种情况下(如果是 NOOBS 安装),将从RECOVERY分区加载 NOOBS。如果在此时按下Shift键,NOOBS 将加载恢复和安装菜单。否则,NOOBS 将开始加载存储在设置分区中的偏好设置的操作系统。

在加载操作系统时,它将通过BOOT分区使用config.txt中定义的设置和cmdline.txt中的选项启动,最终加载到根分区上的桌面。请参考以下图表:

如何工作…

NOOBS 在 SD 卡上创建几个分区,以便安装多个操作系统并提供恢复功能

NOOBS 允许用户选择性地在同一张卡上安装多个操作系统,并提供一个启动菜单来选择它们(在超时期间提供设置默认值的选项)。

如果您以后添加、删除或重新安装操作系统,请首先确保您复制了任何文件,包括您希望保留的系统设置,因为 NOOBS 可能会覆盖 SD 卡上的所有内容。

还有更多...

当您第一次启动 Raspberry Pi 时,它将直接启动桌面。您现在可以使用Raspberry Pi 配置程序(在桌面的“首选项”菜单下或通过sudo raspi-config命令)来配置系统设置,这将允许您对 SD 卡进行更改并设置您的通用首选项。

还有更多…

Raspberry Pi 配置程序

更改默认用户密码

一旦登录,请确保您更改pi用户账户的默认密码,因为默认密码是众所周知的。如果您连接到公共网络,这尤其重要。您可以使用以下截图所示的passwd命令来完成此操作:

更改默认用户密码

为 pi 用户设置新密码

这提供了更大的信心,因为如果您稍后连接到另一个网络,只有您才能访问您的文件并控制您的 Raspberry Pi。

确保您安全关机

为了避免任何数据损坏,您必须确保通过发出shutdown命令正确关闭 Raspberry Pi,如下所示:

sudo shutdown –h now

或者使用这个:

sudo halt

您必须等待此命令完成后再从 Raspberry Pi 断电(在 SD 卡访问指示灯停止闪烁后至少等待 10 秒钟)。

您也可以使用reboot命令重新启动系统,如下所示:

sudo reboot

手动准备 SD 卡

使用 NOOBS 的另一种选择是手动将操作系统镜像写入 SD 卡。虽然这最初是安装操作系统的唯一方法,但一些用户仍然更喜欢这种方法。它允许在 Raspberry Pi 中使用之前准备 SD 卡。它还可以提供更容易访问启动和配置文件,并且为用户留下更多可用空间(与 NOOBS 不同,不包括RECOVERY分区)。

默认的 Raspbian 镜像实际上由两个分区组成,BOOTSYSTEM,它们可以适应 2GB 的 SD 卡(建议使用 4GB 或更大)。

您需要一个运行 Windows/Mac OS X/Linux 的计算机(尽管可以使用另一个 Raspberry Pi 来写入您的卡,但请准备好非常长的等待时间)。

下载您希望使用的操作系统的最新版本。在本书中,假设您正在使用可在www.raspberrypi.org/downloads找到的最新版本的 Raspbian。

根据您计划用于写入 SD 卡的计算机类型执行以下步骤(您需要的.img文件有时是压缩的,因此,在您开始之前,您需要提取文件)。

以下步骤适用于 Windows:

  1. 确保您已下载 Raspbian 镜像,如前所述,并将其提取到方便的文件夹中,以获得.img文件。

  2. www.sourceforge.net/projects/win32diskimager 获取可用的 Win32DiskImager.exe 文件。

  3. 从下载位置运行 Win32DiskImager.exe

  4. 单击文件夹图标并导航到 .img 文件的位置,然后单击 Save

  5. 如果您还没有这样做,请将 SD 卡插入您的卡读卡器并将其连接到计算机。

  6. 从小下拉框中选择与您的 SD 卡对应的 Device 驱动器字母。请务必确认这是正确的设备(因为当您写入镜像时,程序将覆盖设备上的所有内容)。

    注意

    驱动器字母可能不会列出,直到您选择源镜像文件。

  7. 最后,单击 Write 按钮,等待程序将镜像写入 SD 卡,如图所示:手动准备 SD 卡

    使用 Disk Imager 手动将操作系统镜像写入 SD 卡

完成后,您可以退出程序。您的 SD 卡已准备就绪!

以下步骤适用于大多数 Linux 发行版,如 Ubuntu 和 Debian:

  1. 使用您首选的网页浏览器下载 Raspbian 镜像并将其保存在合适的位置。

  2. 从文件管理器中提取文件或在终端中定位文件夹,并使用以下命令解压缩 .img 文件:

    unzip filename.zip
    
    
  3. 如果您还没有这样做,请将 SD 卡插入您的卡读卡器并将其连接到计算机。

  4. 使用 df –h 命令并识别 SD 卡的 sdX 标识符。每个分区将显示为 sdX1sdX2 等,其中 X 将是设备 ID 的 abcd 等。

  5. 确保使用 umount /dev/sdXn 命令为每个分区卸载 SD 卡上的所有分区,其中 sdXn 是正在卸载的分区。

  6. 使用以下命令将镜像文件写入 SD 卡:

    sudo dd if=filename.img of=/dev/sdX bs=4M
    
    
  7. 写入 SD 卡的过程将花费一些时间,完成后将返回终端提示符。

  8. 在从计算机中移除 SD 卡之前,使用以下命令卸载 SD 卡:

    umount /dev/sdX1
    
    

以下步骤适用于大多数 OS X 版本:

  1. 使用您首选的网页浏览器下载 Raspbian 镜像并将其保存在合适的位置。

  2. 从文件管理器中提取文件或在终端中定位文件夹,并使用以下命令解压缩 .img 文件:

    unzip filename.zip
    
    
  3. 如果您还没有这样做,请将 SD 卡插入您的卡读卡器并将其连接到计算机。

  4. 使用 diskutil list 命令并识别 SD 卡的 disk# 标识符。每个分区将显示为 disk#s1disk#s2 等,其中 # 将是设备 ID 的 1234 等。

    注意

    如果列出 rdisk#,请使用此方法进行更快的写入(这使用原始路径并跳过数据缓冲)。

  5. 确保使用 unmountdisk /dev/diskX 命令卸载 SD 卡,其中 diskX 是正在卸载的设备。

  6. 使用以下命令将镜像文件写入 SD 卡:

    sudo dd if=filename.img of=/dev/diskX bs=1M
    
    
  7. 将过程写入 SD 卡需要一些时间,完成后将返回终端提示符。

  8. 在从计算机中移除 SD 卡之前,请使用以下命令卸载 SD 卡:

    unmountdisk /dev/diskX
    
    

    参考以下图片:

    手动准备 SD 卡

    手动安装的 OS 镜像的启动过程

将系统扩展以适应您的 SD 卡

手动编写的镜像将具有固定大小(通常制作成适合尽可能小的 SD 卡)。为了充分利用 SD 卡,您需要将系统分区扩展以填充 SD 卡的剩余部分。这可以通过使用 Raspberry Pi 配置工具实现。

选择Expand Filesystem,如下面的屏幕截图所示:

将系统扩展以适应您的 SD 卡

Raspberry Pi 配置工具

访问 RECOVERY/BOOT 分区

Windows 和 Mac OS X 不支持ext4格式,因此当您读取 SD 卡时,只能访问文件分配表FAT)分区。此外,Windows 只支持 SD 卡上的第一个分区,因此如果您已安装 NOOBS,则只能看到RECOVERY分区。如果您手动写入卡,您将能够访问BOOT分区。

data分区(如果您通过 NOOBS 安装)和root分区都是ext4格式,通常在非 Linux 系统上不可见。

注意

如果您确实需要使用 Windows 从 SD 卡读取文件,一款免费软件程序Linux Reader(可在www.diskinternals.com/linux-reader获取)可以提供对 SD 卡上所有分区的只读访问。

从 Raspberry Pi 访问分区。要查看当前挂载的分区,请使用df,如下面的屏幕截图所示:

访问 RECOVERY/BOOT 分区

df 命令的结果

要从 Raspbian 内部访问BOOT分区,请使用以下命令:

cd /boot/

要访问RECOVERYdata分区,我们必须通过以下步骤将其挂载:

  1. 通过列出所有分区(包括未挂载的分区)来确定分区的名称,系统通过该名称引用它。sudo fdisk -l命令列出分区,如下面的屏幕截图所示:访问 RECOVERY/BOOT 分区

    Raspbian NOOBS 安装的分区表和数据分区

    mmcblk0p1 (vfat) RECOVERY
    mmcblk0p2 (扩展分区)包含(root, data, BOOT)
    mmcblk0p5 (ext4) root
    mmcblk0p6 (vfat) BOOT
    mmcblk0p7 (ext4) SETTINGS

    如果您在同一张卡上安装了额外的操作系统,前面表格中显示的分区标识将不同。

  2. 创建一个文件夹并将其设置为分区的挂载点,如下所示:

    • 对于RECOVERY分区,请使用以下命令:

      mkdir ~/recovery
      sudo mount –t vfat /dev/mmcblk0p1 ~/recovery
      
      

为了确保它们在每次系统启动时都挂载,请执行以下步骤:

  1. exit 0 之前将 sudo mount 命令添加到 /etc/rc.local。如果你有不同的用户名,你需要将 pi 改变以匹配:

    sudo nano /etc/rc.local
    sudo mount -t vfat /dev/mmcblk0p1 /home/pi/recovery
    
    
  2. Ctrl + XYEnter 保存并退出。

注意

添加到 /etc/rc.local 的命令将为任何登录到 Raspberry Pi 的用户运行。如果你只想为当前用户挂载驱动器,可以将命令添加到 .bash_profile 中。

如果你必须在同一张卡上安装额外的操作系统,这里显示的分区标识符将不同。

使用工具备份 SD 卡以防故障

你可以使用 Win32 Disk Imager 通过将 SD 卡插入你的读卡器,启动程序,并创建一个用于存储镜像的文件名来制作 SD 卡的完整备份镜像。只需点击 Read 按钮即可从 SD 卡读取镜像并将其写入新的镜像文件。

要备份你的系统,或使用 Raspberry Pi 将其克隆到另一个 SD 卡,请使用 SD 卡复制器(可通过桌面菜单中的 Accessories | SD Card Copier 获取)。

将 SD 卡插入到 Raspberry Pi 的一个备用 USB 端口的卡读卡器中,并选择新的存储设备,如下面的截图所示:

使用工具备份 SD 卡以防故障

SD 卡复制器程序

在继续之前,SD 卡复制器将确认你是否希望格式化和覆盖目标设备,如果空间足够,将制作系统的一个克隆。

可以使用类似的 dd 命令来备份卡,如下所示:

  • 对于 Linux,将 sdX 替换为你的设备 ID,使用此命令:

    sudo dd if=/dev/sdX of=image.img.gz bs=1M
    
    
  • 对于 OS X,将 diskX 替换为你的设备 ID,使用以下命令:

    sudo dd if=/dev/diskX of=image.img.gz bs=1M
    
    
  • 你还可以使用 gzip 和 split 来压缩卡的内容,并在需要时将它们分割成多个文件,如下所示:

    sudo dd if=/dev/sdX bs=1M | gzip –c | split –d –b 2000m – image.img.gz
    
    
  • 要恢复分割的镜像,使用以下命令:

    sudo cat image.img.gz* | gzip –dc | dd of=/dev/sdX bs=1M
    
    

通过 LAN 连接器将 Raspberry Pi 连接到互联网

将 Raspberry Pi 连接到互联网最简单的方式是通过 Model B 上的内置 LAN 连接。如果你使用的是 Model A Raspberry Pi,可以使用 USB-to-LAN 转换器(有关如何配置此转换器的详细信息,请参阅 Networking and connecting your Raspberry Pi to the Internet via a USB Wi-Fi dongle 菜单中的 There's more… 部分)。

准备工作

你需要访问一个合适的有线网络,该网络将连接到互联网,并使用标准网络线(带有 RJ45 类型连接器,用于连接到 Raspberry Pi)。

如何操作…

许多网络使用 动态主机配置协议DHCP)自动连接和配置,由路由器或交换机控制。如果是这种情况,只需将网络线缆插入路由器或网络交换机上的备用网络端口(如果适用,也可以是墙壁网络插座)。

或者,如果没有 DHCP 服务器可用,你必须手动配置设置(有关详细信息,请参阅 还有更多... 部分)。

你可以通过以下步骤确认其成功运行:

  1. 确保 Raspberry Pi 两边的两个 LED 灯亮起(左边的橙色 LED 表示连接,右边的绿色 LED 通过闪烁显示活动)。这将表明与路由器和设备的物理连接存在,设备已供电并正常工作。

  2. 使用 ping 命令测试本地网络链接。首先,找出网络上另一台计算机的 IP 地址(或者可能是路由器的地址,通常是 192.168.0.1192.168.1.254)。现在,在 Raspberry Pi 终端上使用 ping 命令(使用 -c 4 参数只发送四条消息;否则,按 Ctrl + C 停止)来 ping IP 地址,如下所示:

    sudo ping 192.168.1.254 -c 4
    
    
  3. 按以下方式测试互联网链接(如果你通常通过代理服务器连接到互联网,这将失败):

    sudo ping www.raspberrypi.org -c 4
    
    
  4. 最后,你可以通过在 Raspberry Pi 上使用 hostname -I 命令来发现 IP 地址,来测试链接回 Raspberry Pi。然后,你可以在网络上另一台计算机上使用 ping 命令来确保其可访问(用 Raspberry Pi 的 IP 地址代替 www.raspberrypi.org)。Windows 版本的 ping 命令将执行五个 ping 并自动停止,不需要 -c 4 选项。

如果上述测试失败,你需要检查你的连接,然后确认你的网络配置正确。

还有更多...

如果你经常在网络上使用你的 Raspberry Pi,你不会希望每次想要连接到它时都要查找 IP 地址。

在某些网络上,你可能可以使用 Raspberry Pi 的主机名而不是其 IP 地址(默认为 raspberrypi)。为此,你可能需要一些额外的软件,如 Bonjour,以确保网络上主机名的正确注册。如果你有 OS X Mac,你将已经运行了 Bonjour。在 Windows 上,你可以安装 iTunes(如果你还没有安装,它也包含该服务),或者你可以单独安装它(通过从 support.apple.com/kb/DL999 可用的 Apple Bonjour 安装程序)。然后你可以使用主机名 raspberrypiraspberrypi.local 来通过网络连接到 Raspberry Pi。如果你需要更改主机名,你可以在之前显示的 Raspberry Pi 配置工具中这样做。

或者,您可能发现手动设置 IP 地址到一个已知值很有帮助。但是,请记住,在连接到另一个网络时将其切换回使用 DHCP。

一些路由器还将有一个选项来设置静态 IP DHCP 地址,这样就可以始终给 Raspberry Pi 分配相同的地址(如何在路由器上设置将因路由器而异)。

了解您的 Raspberry Pi 的 IP 地址或使用主机名特别有用,如果您打算使用后面描述的远程访问解决方案之一,这可以避免需要显示器。

在 Raspberry Pi 上使用内置 Wi-Fi 和蓝牙

许多家庭网络通过 Wi-Fi 提供无线网络;如果您有 Raspberry Pi 3,则可以利用板载 Broadcom Wi-Fi 来连接。Raspberry Pi 3 还支持蓝牙,因此您可以连接大多数标准蓝牙设备,并像在其他任何计算机上一样使用它们。

此方法也应适用于任何支持的 USB Wi-Fi 和蓝牙设备,请参阅通过 USB Wi-Fi 外置网卡连接 Raspberry Pi 到互联网的网络和连接配方以获取识别设备和安装固件(如果需要)的额外帮助。

准备工作

Raspbian 的最新版本包括一些有用的实用程序,可以快速轻松地通过图形界面配置 Wi-Fi 和蓝牙。

注意

注意:如果您需要通过命令行配置 Wi-Fi,请参阅通过 USB Wi-Fi 外置网卡连接 Raspberry Pi 到互联网的网络和连接配方以获取详细信息。

准备工作

Wi-Fi 和蓝牙配置应用程序

您可以使用内置的蓝牙连接无线键盘、鼠标甚至无线扬声器。这在需要额外电缆和线缆的项目中特别有用,例如机器人项目,或者当 Raspberry Pi 安装在难以触及的位置时(作为服务器或安全摄像头)。

如何操作…

连接到您的 Wi-Fi 网络

要配置您的 Wi-Fi 连接,请点击网络符号以列出可用的本地 Wi-Fi 网络:

连接到您的 Wi-Fi 网络

该区域可用的接入点 Wi-Fi 列表

选择所需的网络(例如,Demo),如果需要,输入您的密码(也称为预共享密钥):

连接到您的 Wi-Fi 网络

提供接入点的密码

稍等片刻,您应该会看到您已经连接到网络,图标将更改为 Wi-Fi 符号。如果您遇到问题,请确保您有正确的密码/密钥。

连接到您的 Wi-Fi 网络

成功连接到接入点

就这么简单!

您现在可以使用网络浏览器导航到网站或通过在终端中使用以下命令来测试您的连接并确保它正在工作:

sudo ping www.raspberrypi.com

连接到蓝牙设备

首先,我们需要通过点击蓝牙图标并选择设置为可发现来将蓝牙设备置于可发现模式。您还需要将您想要连接的设备设置为可发现并准备好配对;这可能会因设备而异(例如,按配对按钮)。

连接到蓝牙设备

将蓝牙设置为可发现

接下来,选择添加设备...并选择目标设备,然后配对

连接到蓝牙设备

选择并配对所需的设备

然后开始配对过程;例如,BTKB-71DB键盘需要将配对码467572输入到键盘上以完成配对。其他设备可能使用默认配对码,通常设置为 0000、1111、1234 或类似。

连接到蓝牙设备

按照说明使用所需的配对码配对设备

一旦过程完成,设备将被列出,并且每次设备出现并启动时都会自动连接。

手动配置您的网络

如果您的网络不包含 DHCP 服务器或它已被禁用(通常,这些功能集成在大多数现代 ADSL/电缆调制解调器或路由器中),您可能需要手动配置网络设置。

准备工作

在开始之前,您需要确定您网络的网络设置。

您需要从路由器的设置或连接到网络的另一台计算机中获取以下信息:

  • IPv4 地址:此地址需要选择,以便与其他网络上的计算机相似(通常,前三个数字应该匹配,即如果子网掩码255.255.255.0,则为192.168.1.X),但它不应已被其他计算机使用。然而,避免使用x.x.x.255作为最后一个地址,因为这个地址已被保留为广播地址。

  • 子网掩码:此数字确定计算机将响应的地址范围(对于家庭网络,通常是255.255.255.0,这允许最多 254 个地址)。这有时也被称为子网掩码

  • 默认网关地址:此地址通常是您的路由器的 IP 地址,通过该地址计算机连接到互联网。

  • DNS 服务器:DNS 服务器(域名服务)通过查找将名称转换为 IP 地址。通常,它们已经配置在您的路由器上,在这种情况下,您可以使用路由器的地址。或者,您的互联网服务提供商ISP)可能提供一些地址,或者您可以使用位于地址8.8.8.88.8.4.4的谷歌公共 DNS 服务器。在某些系统中,这些也被称为名称服务器

对于 Windows,您可以通过连接到互联网并运行以下命令来获取此信息:

ipconfig /all

定位活动连接(如果您使用有线连接,通常称为 本地连接 1 或类似名称;如果您使用 Wi-Fi,则称为无线网络连接)并找到所需信息,如下所示:

准备中

ipconfig/all 命令显示了有关您的网络设置的有用信息

对于 Linux 和 Mac OS X,您可以使用以下命令获取所需信息(注意:它是 ifconfig 而不是 ipconfig):

ifconfig

DNS 服务器被称为名称服务器,通常列在 resolv.conf 文件中。您可以使用以下 less 命令来查看其内容(完成查看后按 Q 退出):

less /etc/resolv.conf

如何做到这一点...

要设置网络接口设置,使用以下代码编辑 /etc/network/interfaces

sudo nano /etc/network/interfaces

现在,执行以下步骤:

  1. 我们可以为我们的特定网络添加详细信息,包括我们想要分配给它的 IP 地址、网络的 子网掩码 地址和 网关 地址,如下所示:

    iface eth0 inet static
     address 192.168.1.10
     netmask 255.255.255.0
     gateway 192.168.1.254
    
    
  2. 通过按 Ctrl + X, YEnter 来保存并退出。

  3. 要设置 DNS 的名称服务器,使用以下代码编辑 /etc/resolv.conf

    sudo nano /etc/resolv.conf
    
    
  4. 按照以下方式添加您的 DNS 服务器地址:

    nameserver 8.8.8.8
    nameserver 8.8.4.4
    
    
  5. 通过按 Ctrl + X, YEnter 来保存并退出。

更多内容...

您可以通过编辑 BOOT 分区中的 cmdline.txt 来配置网络设置,并在启动命令行中添加设置 ip

ip 选项采用以下形式:

ip=client-ip:nfsserver-ip:gw-ip:netmask:hostname:device:autoconf

  • client-ip 选项是您想要分配给 Raspberry Pi 的 IP 地址

  • gw-ip 选项将在您需要手动设置时设置网关服务器地址

  • netmask 选项将直接设置网络的子网掩码

  • hostname 选项将允许您更改默认的 raspberrypi 主机名

  • device 选项允许您在存在多个网络设备的情况下指定默认网络设备

  • autoconf 选项允许您打开或关闭自动配置

直接连接到笔记本电脑或计算机

有可能使用一根网络线缆直接将 Raspberry Pi 的 LAN 端口连接到笔记本电脑或计算机。这将创建计算机之间的本地网络链路,允许您在没有集线器或路由器的情况下执行所有连接到普通网络时可以做的事情,包括如果使用以下 互联网连接共享ICS)连接到互联网:

直接连接到笔记本电脑或计算机的联网

利用仅网络线缆、标准镜像 SD 卡和电源的 Raspberry Pi

ICS 允许 Raspberry Pi 通过另一台计算机连接到互联网。然而,为了在链路中进行通信,需要对计算机进行一些额外的配置,因为 Raspberry Pi 不会自动分配自己的 IP 地址。

我们将使用 ICS 从另一个网络链接共享连接,例如笔记本电脑上的内置 Wi-Fi。如果不需要互联网或计算机只有一个网络适配器,我们还可以使用直接网络链接(请参阅“更多内容”部分下的“直接网络链接”部分)。

注意

虽然这个设置应该适用于大多数计算机,但有些设置比其他设置更复杂。有关更多信息,请参阅www.pihardware.com/guides/direct-network-connection

准备就绪

您需要一个带电源的标准网络线缆的树莓派。

注意

树莓派 Model B LAN 芯片包括Auto-MDIX自动介质依赖接口交叉)。无需使用特殊交叉线缆(一种特殊的有线网络线缆,其传输线连接到接收线以实现直接网络链接),芯片将自动决定并更改设置。

如果这是您第一次尝试此操作,那么拥有键盘和显示器进行额外的测试可能也有帮助。

为了确保您可以将网络设置恢复到原始值,您应该检查它是否有固定 IP 地址或网络是否已自动配置。

要检查 Windows 10 上的网络设置,请执行以下步骤:

  1. 从开始菜单打开设置,然后选择网络和互联网,然后选择以太网,并从相关设置列表中单击更改适配器选项

要检查 Windows 7 和 Vista 上的网络设置,请执行以下步骤:

  1. 控制面板中打开网络和共享中心,然后单击左侧的更改适配器设置

  2. 要检查 Windows XP 上的网络设置,请从控制面板打开网络连接

  3. 找到与您的有线网络适配器相关的项目(默认情况下,这通常被称为以太网本地连接,如下面的截图所示):准备就绪

    定位您的有线网络连接

  4. 右键单击其图标并单击属性。将出现一个对话框,如下面的截图所示:准备就绪

    选择 TCP/IP 属性并检查设置

  5. 如果有两个版本(另一个是版本 6),请选择互联网协议(TCP/IP)互联网协议版本 4(TCP/IPv4),然后单击属性按钮。

  6. 您可以使用自动设置或指定 IP 地址(如果需要,请注意此地址和其余详细信息,因为您可能希望在以后恢复设置)来确认您的网络设置。

要检查 Linux 上的网络设置,请执行以下步骤:

  1. 打开网络设置对话框并选择配置接口。请参考以下截图:准备就绪

    Linux 网络设置对话框

  2. 确保如果任何设置是手动设置的,你记下它们,以便如果你想的话可以稍后恢复。

要检查 Mac OS X 上的网络设置,请执行以下步骤:

  1. 打开系统偏好设置并点击网络。然后你可以确认 IP 地址是否是自动分配的(使用 DHCP)或不是。

  2. 确保如果任何设置是手动设置的,你记下它们,以便如果你想的话可以稍后恢复。参考以下截图:准备中

    OS X 网络设置对话框

如果你只需要在没有互联网连接的情况下访问或控制 Raspberry Pi,请参考更多内容…部分中的直接网络链接部分。

如何操作…

首先,我们需要在我们的网络设备上启用 ICS。在这种情况下,我们将通过连接到 Raspberry Pi 的以太网连接来共享可用的互联网,该互联网通过无线网络连接提供。

对于 Windows,请执行以下步骤:

  1. 返回网络适配器列表,右键点击连接到互联网的连接(在这种情况下,是WiFi无线网络连接设备),然后点击属性如何操作…

    右键点击你的无线设备并选择属性

  2. 在窗口顶部,选择第二个标签页(在 Windows XP 中称为高级;在 Windows 7 和 Windows 10 中称为共享),如图所示:如何操作…

    选择 TCP/IP 属性并记下分配的 IP 地址

  3. 互联网连接共享部分,勾选允许其他网络用户通过此计算机的互联网连接连接(如果存在,使用下拉框选择家庭网络连接选项作为以太网本地连接)。点击确定并确认你是否之前为本地连接设置了一个固定的 IP 地址。

对于 Mac OS X,要启用 ICS,请执行以下步骤:

  1. 点击系统偏好设置然后点击共享

  2. 点击互联网共享并选择我们想要共享互联网的连接(在这种情况下,将是 Wi-Fi AirPort)。然后选择我们将连接 Raspberry Pi 的连接(在这种情况下,以太网)。

对于 Linux,要启用 ICS,请执行以下步骤:

  1. 系统菜单,点击偏好设置然后点击网络连接。选择你想要共享的连接(在这种情况下,无线)并点击编辑配置。在IPv4 设置标签页,将方法选项更改为共享给其他计算机

网络适配器的 IP 地址将是树莓派上使用的网关 IP地址,并分配一个同一范围内的 IP 地址(除了最后一位)。例如,如果电脑的有线连接现在是192.168.137.1,树莓派的网关 IP 将是192.168.137.1,其自身的 IP 地址可能设置为192.168.137.10

幸运的是,由于操作系统的更新,Raspbian 现在将自动分配一个合适的 IP 地址以加入网络,并适当地设置网关。然而,除非我们将屏幕连接到树莓派或扫描我们的网络中的设备,否则我们不知道树莓派分配给自己的是什么 IP 地址。

幸运的是(如“更多...”部分中提到的网络和通过 LAN 连接器连接树莓派到互联网食谱中所述),苹果的Bonjour软件将自动确保网络上的主机名正确注册。如前所述,如果你有一台 OSX Mac,Bonjour 已经运行。在 Windows 上,你可以安装 iTunes,或者可以单独安装(从support.apple.com/kb/DL999获取)。默认情况下,可以使用主机名raspberrypi

我们现在可以按照以下步骤测试新的连接:

  1. 将网络线缆连接到树莓派和电脑的网络端口,然后开启树莓派,确保如果之前已经移除 SD 卡,则重新插入。如果你在树莓派上编辑了文件,要重启树莓派,请使用sudo reboot命令来重启。

  2. 等待一分钟或两分钟,让树莓派完全启动。我们现在可以测试连接。

  3. 从连接的笔记本电脑或电脑,通过以下命令测试连接,使用树莓派的计算机名进行 ping 操作(在 Linux 或 OS X 上,添加-c 4以限制为四条消息或按Ctrl + C退出):

    ping raspberrypi
    
    

希望你能找到一个有效的连接,并从树莓派收到回复。

如果你将键盘和屏幕连接到树莓派,你可以执行以下步骤:

  1. 你可以从树莓派终端返回 ping 电脑(例如,192.168.137.1)如下:

    sudo ping 192.168.137.1 -c 4
    
    
  2. 你可以通过使用ping命令连接到以下知名网站来测试互联网连接,假设你不是通过代理服务器访问互联网:

    sudo ping www.raspberrypi.org -c 4
    
    

如果一切顺利,你将通过电脑获得完整的互联网连接,允许你浏览网页以及更新和安装新软件。

如果连接失败,请执行以下步骤:

  1. 重复此过程,确保前三个数字组与树莓派和网络适配器的 IP 地址相匹配。

  2. 你还可以检查当树莓派启动时,是否使用以下命令设置了正确的 IP 地址:

    hostname -I
    
    
  3. 检查防火墙设置,确保它没有阻止内部网络连接。

它是如何工作的…

当我们在主计算机上启用 ICS 时,操作系统将自动为计算机分配一个新的 IP 地址。一旦连接并通电,树莓派将设置自己到一个兼容的 IP 地址,并使用主计算机的 IP 地址作为互联网网关。

通过使用 Apple Bonjour,我们能够使用raspberrypi主机名从连接的计算机连接到树莓派。

最后,我们检查计算机是否可以通过直接网络链路与树莓派通信,反过来也可以,并且还可以连接到互联网。

还有更多…

如果你不需要在树莓派上使用互联网,或者你的计算机只有一个网络适配器,我们仍然可以通过直接网络链路将计算机连接起来。请参考以下图表:

还有更多…

仅使用网络线缆、标准镜像 SD 卡和电源连接和使用树莓派

直接网络链路

为了在两台计算机之间建立网络链路,它们需要使用相同的地址范围。允许的地址范围由子网掩码确定(例如,255.255.0.0255.255.255.0意味着除了 IP 地址的最后两个或最后一个数字之外,所有 IP 地址都应该相同;否则,它们将被过滤)。

要使用直接链路而不启用 ICS,请检查你将要连接的适配器的 IP 设置,并确定它是自动分配的还是固定到特定的 IP 地址。

大多数直接连接到另一台计算机的 PC 将会分配一个在169.254.X.X范围内的 IP 地址(子网掩码为255.255.0.0)。然而,我们必须确保网络适配器设置为自动获取 IP 地址

为了树莓派能够通过直接链路通信,它需要有一个在同一地址范围内的 IP 地址,169.254.X.X。如前所述,树莓派将自动给自己一个合适的 IP 地址并连接到网络。

因此,假设我们有了Apple Bonjour(详情请见前文),我们只需要知道分配给树莓派的计算机名(raspberrypi)。

参见

如果你没有将键盘或屏幕连接到树莓派,你可以使用这个网络链路远程访问树莓派,就像在普通网络上一样(只需使用你为连接设置的新 IP 地址)。请参考通过 VNC 远程连接到树莓派的网络通过 SSH(以及 X11 转发)远程连接到树莓派的网络这两个菜谱。

在我的网站上有很多额外的信息,pihw.wordpress.com/guides/direct-network-connection,包括额外的故障排除技巧和几种不需要专用屏幕和键盘即可连接到你的树莓派的其他方法。

通过 USB 无线网卡连接树莓派到互联网

通过将USB 无线网卡插入树莓派的 USB 端口,即使没有内置 Wi-Fi 的型号也可以连接并使用 Wi-Fi 网络。

准备工作

您需要获取一个合适的 USB 无线网卡;在某些情况下,您可能需要一个带电的 USB 集线器(这取决于您拥有的树莓派的硬件版本和电源质量)。USB 无线网卡的通用适用性将取决于内部使用的芯片组和可用的 Linux 支持水平。您可能会发现某些 USB 无线网卡无需安装额外的驱动程序即可工作(在这种情况下,您可以跳转到配置无线网络)。

支持的无线适配器列表可在elinux.org/RPi_USB_Wi-Fi_Adapters找到。

您需要确保您的无线适配器也与您打算使用的网络兼容;例如,它支持相同的信号类型802.11bgn和加密WEPWPAWPA2(尽管大多数网络都是向后兼容的)。

您还需要以下网络详细信息:

  • 服务集标识符(SSID):这是您无线网络的名字,如果您使用以下命令,它应该是可见的:

    sudo iwlist scan | grep SSID
    
    
  • 加密类型和密钥:此值将是WEPWPAWPA2,密钥将是您连接手机或笔记本电脑到无线网络时通常输入的代码(有时,它打印在路由器上)。

您需要有一个正常工作的互联网连接(即有线以太网)来下载所需的驱动程序。否则,您可能能够找到所需的固件文件(它们将是.deb文件),并将它们复制到树莓派上(即通过 USB 闪存驱动器;如果您在桌面模式下运行,驱动器应自动挂载)。将文件复制到合适的位置,并使用以下命令进行安装:

sudo apt-get install firmware_file.deb

如何操作…

此任务分为两个阶段;首先,我们识别并安装无线适配器的固件,然后我们需要为无线网络进行配置。

我们将尝试识别您无线适配器的芯片组(处理连接的部分);这可能与设备的实际制造商不匹配。

可以使用以下命令找到支持的固件的大致列表:

sudo apt-cache search wireless firmware

这将产生类似于以下输出的结果(忽略任何标题中不带firmware的结果):

atmel-firmware - Firmware for Atmel at76c50x wireless networking chips.
firmware-atheros - Binary firmware for Atheros wireless cards
firmware-brcm80211 - Binary firmware for Broadcom 802.11 wireless cards
firmware-ipw2x00 - Binary firmware for Intel Pro Wireless 2100, 2200 and 2915
firmware-iwlwifi - Binary firmware for Intel PRO/Wireless 3945 and 802.11n cards
firmware-libertas - Binary firmware for Marvell Libertas 8xxx wireless cards
firmware-ralink - Binary firmware for Ralink wireless cards
firmware-realtek - Binary firmware for Realtek wired and wireless network adapters
libertas-firmware - Firmware for Marvell's libertas wireless chip series (dummy package)
zd1211-firmware - Firmware images for the zd1211rw wireless driver

要找出无线适配器的芯片组,将无线适配器插入树莓派,然后在终端运行以下命令:

dmesg | grep 'Product:\|Manufacturer:'

注意

此命令将两个命令合并为一个。首先,dmesg显示内核的消息缓冲区(这是自开机以来发生的系统事件的内部记录,例如检测到的 USB 设备)。您可以单独尝试此命令以观察完整的输出。

|(管道符)将输出发送到grep命令,grep 'Product:\|Manuf'检查它,并且只返回包含ProductManuf的行(因此我们应该得到任何列出的ProductManufacturer的项目的摘要)。如果你没有找到任何内容或者想要查看所有 USB 设备,尝试使用grep 'usb'代替。

这应该返回类似于以下输出(在这种情况下,我有一个ZyXEL设备,它有一个ZyDAS芯片组(快速谷歌搜索显示zd1211-firmware是用于 ZyDAS 设备的)):

[    1.893367] usb usb1: Product: DWC OTG Controller
[    1.900217] usb usb1: Manufacturer: Linux 3.6.11+ dwc_otg_hcd
[    3.348259] usb 1-1.2: Product: ZyXEL G-202
[    3.355062] usb 1-1.2: Manufacturer: ZyDAS

一旦你确定了你的设备和正确的固件,你就可以像安装任何其他通过apt-get提供的包一样安装它(其中zd1211-firmware可以替换为你需要的固件)。这显示在以下命令中:

sudo apt-get install zd1211-firmware

移除并重新插入 USB Wi-Fi 网卡,以便检测并加载驱动程序。现在我们可以使用ifconfig测试新适配器是否正确安装。输出如下所示:

wlan0     IEEE 802.11bg  ESSID:off/any
 Mode:Managed  Access Point: Not-Associated   Tx-Power=20 dBm
 Retry  long limit:7   RTS thr:off   Fragment thr:off
 Power Management:off

命令将显示系统上存在的网络适配器。对于 Wi-Fi,这通常是wlan0,或者如果你安装了多个,可能是wlan1等。如果没有,请再次检查所选固件,也许尝试一个替代方案或查看网站上的故障排除提示。

一旦我们为 Wi-Fi 适配器安装了固件,我们还需要配置它以连接我们希望连接的网络。我们可以使用如图所示的 GUI,或者我们可以通过以下步骤手动通过终端配置它:

  1. 我们需要将无线适配器添加到网络接口列表中,该列表设置在/etc/network/interfaces中,如下所示:

    sudo nano -c /etc/network/interfaces 
    
    

    如果需要,用之前的wlan#值替换wlan0,添加以下命令:

    allow-hotplug wlan0
    iface wlan0 inet manual
    wpa-conf /etc/wpa_supplicant/wpa_supplicant.conf
    
    

    当更改完成后,通过按Ctrl + XYEnter保存并退出。

  2. 我们现在将把我们的 Wi-Fi 网络设置存储在wpa_supplicant.conf文件中(如果你的网络没有使用wpa加密,不要担心;这只是文件的默认名称):

    sudo nano -c /etc/wpa_supplicant/wpa_supplicant.conf
    
    

    它应该包括以下内容:

    ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
    update_config=1
    country=GB
    

    网络设置可以按照以下方式写入此文件(即,如果 SSID 设置为theSSID):

    • 如果没有使用加密,请使用以下代码:

      network={
        ssid="theSSID"
        key_mgmt=NONE
      }
      
    • 使用WEP加密(即,如果WEP密钥设置为theWEPkey),请使用以下代码:

      network={
        ssid="theSSID"
        key_mgmt=NONE
        wep_key0="theWEPkey"
      }
      
    • 对于WPAWPA2加密(即,如果WPA密钥设置为theWPAkey),请使用以下代码:

      network={
        ssid="theSSID"
        key_mgmt=WPA-PSK
        psk="theWPAkey"	
      }
      
  3. 你可以使用以下命令启用适配器(再次,如果需要,替换wlan0):

    sudo ifup wlan0
    
    

    使用以下命令列出无线网络连接:

    iwconfig
    
    

    你应该会看到你的无线网络连接,你的 SSID 如下列出:

    wlan0     IEEE 802.11bg  ESSID:"theSSID"
     Mode:Managed  Frequency:2.442 GHz  Access Point: 00:24:BB:FF:FF:FF
     Bit Rate=48 Mb/s   Tx-Power=20 dBm
     Retry  long limit:7   RTS thr:off   Fragment thr:off
     Power Management:off
     Link Quality=32/100  Signal level=32/100
     Rx invalid nwid:0  Rx invalid crypt:0  Rx invalid frag:0
     Tx excessive retries:0  Invalid misc:15   Missed beacon:0
    
    

    如果没有,调整你的设置,并使用sudo ifdown wlan0关闭网络接口,然后使用sudo ifup wlan0重新打开它。

    这将确认你已经成功连接到你的 Wi-Fi 网络。

  4. 最后,我们需要检查我们是否有互联网访问权限。这里,我们假设网络已自动配置为 DHCP,并且没有使用代理服务器。如果不是这样,请参考 通过代理服务器连接到互联网 的配方。

    如果仍然连接,请拔掉有线网络电缆,并查看您是否可以像以下这样 ping 树莓派网站:

    sudo ping www.raspberrypi.org
    
    

小贴士

如果您想快速了解当前树莓派正在使用的 IP 地址,可以使用 hostname -I。或者,要找出哪个适配器连接到哪个 IP 地址,可以使用 ifconfig

更多内容...

模型 A 版本的树莓派没有内置网络端口;因此,为了获得网络连接,必须添加一个 USB 网络适配器(要么是前面章节中解释的 Wi-Fi 拨号器,要么是下一章节中描述的 LAN 到 USB 适配器)。

使用 USB 有线网络适配器

就像 USB Wi-Fi 一样,适配器的支持将取决于所使用的芯片组和可用的驱动程序。除非设备随 Linux 驱动程序一起提供,否则您可能需要在网上搜索以获取合适的 Debian Linux 驱动程序。

如果您找到一个合适的 .deb 文件,可以使用以下命令进行安装:

sudo apt-get install firmware_file.deb

也请使用 ifconfig 进行检查,因为某些设备将自动支持,显示为 eth1(或模型 A 上的 eth0),并立即准备好使用。

通过代理服务器连接到互联网

一些网络,如工作场所或学校,通常要求您通过代理服务器连接到互联网。

准备工作

您将需要您尝试连接的代理服务器的地址,包括如果需要的话,用户名和密码。

您应该确认树莓派已经连接到网络,并且您可以访问代理服务器。

使用 ping 命令如下检查:

ping proxy.address.com -c 4

如果这失败了(您没有收到任何响应),在继续之前,您需要确保您的网络设置是正确的。

如何做到这一点...

使用 nano 创建一个新文件,如下所示(如果文件中已有内容,您可以将代码添加到末尾):

sudo nano -c ~/.bash_profile

要在使用代理服务器的同时通过程序如 midori 允许基本的网页浏览,可以使用以下脚本:

function proxyenable {
# Define proxy settings
PROXY_ADDR="proxy.address.com:port"
# Login name (leave blank if not required):
LOGIN_USER="login_name"
# Login Password (leave blank to prompt):
LOGIN_PWD=
#If login specified - check for password
if [[ -z $LOGIN_USER ]]; then
  #No login for proxy
  PROXY_FULL=$PROXY_ADDR
else
  #Login needed for proxy Prompt for password -s option hides input
  if [[ -z $LOGIN_PWD ]]; then
    read -s -p "Provide proxy password (then Enter):" LOGIN_PWD
    echo
  fi
  PROXY_FULL=$LOGIN_USER:$LOGIN_PWD@$PROXY_ADDR
fi
#Web Proxy Enable: http_proxy or HTTP_PROXY environment variables
export http_proxy="http://$PROXY_FULL/"
export HTTP_PROXY=$http_proxy
export https_proxy="https://$PROXY_FULL/"
export HTTPS_PROXY=$https_proxy
export ftp_proxy="ftp://$PROXY_FULL/"
export FTP_PROXY=$ftp_proxy
#Set proxy for apt-get
sudo cat <<EOF | sudo tee /etc/apt/apt.conf.d/80proxy > /dev/null
Acquire::http::proxy "http://$PROXY_FULL/";
Acquire::ftp::proxy "ftp://$PROXY_FULL/";
Acquire::https::proxy "https://$PROXY_FULL/";
EOF
#Remove info no longer needed from environment
unset LOGIN_USER LOGIN_PWD PROXY_ADDR PROXY_FULL
echo Proxy Enabled
}

function proxydisable {
#Disable proxy values, apt-get and git settings
unset http_proxy HTTP_PROXY https_proxy HTTPS_PROXY
unset ftp_proxy FTP_PROXY
sudo rm /etc/apt/apt.conf.d/80proxy
echo Proxy Disabled
}

完成后,通过按 Ctrl + XYEnter 保存并退出。

注意

该脚本被添加到用户的 .bash_profile 文件中,当该特定用户登录时运行。这将确保为每个用户保留单独的代理设置。如果您想让所有用户使用相同的设置,可以将代码添加到 /etc/rc.local 中(此文件必须以 exit 0 结尾)。

它是如何工作的...

许多利用互联网的程序在连接之前会检查 http_proxyHTTP_PROXY 环境变量。如果存在,它们将使用代理设置进行连接。一些程序也可能使用 HTTPSFTP 协议,因此我们也可以在这里为它们设置代理设置。

注意

如果代理服务器需要用户名,则会提示输入密码。通常不建议在脚本中存储您的密码,除非您确信没有人会访问您的设备(无论是物理上还是通过网络)。

最后的部分允许任何使用sudo命令执行的程序在作为超级用户操作时使用代理环境变量(大多数程序会首先尝试使用正常权限访问网络,即使以超级用户身份运行,因此并不总是需要)。

还有更多...

我们还需要允许某些程序使用代理设置,这些程序在访问网络时使用超级用户权限(这取决于程序;大多数不需要这样做)。我们需要通过以下步骤将命令添加到存储在/etc/sudoers.d/目录中的文件中:

注意

在这里使用visudo非常重要,因为它确保了文件权限被正确创建,以便sudoers目录(只允许root用户读取)。

  1. 使用以下命令打开一个新的sudoer文件:

    sudo visudo -f /etc/sudoers.d/proxy
    
    
  2. 在文件中(单行内)输入以下文本:

    Defaults env_keep += "http_proxy HTTP_PROXY https_proxy HTTPS_PROXY ftp_proxy FTP_PROXY"
    
    
  3. 完成后,通过按Ctrl + XYEnter保存并退出;不要更改proxy.tmp文件名(这对于visudo来说是正常的;完成时会将其更改为 proxy)。

  4. 如果提示接下来做什么?,则命令存在错误。按X退出而不保存,并重新输入命令!

  5. 重新启动(使用sudo reboot)后,您可以使用以下命令分别启用和禁用代理:

    proxyenable
    proxydisable
    
    

通过网络使用 VNC 远程连接到树莓派

通常,通过网络远程连接并控制树莓派更可取,例如,使用笔记本电脑或台式计算机作为屏幕和键盘,或者当树莓派连接到其他地方时,可能甚至连接到它需要靠近的某些硬件。

VNC 只是您远程连接到树莓派的一种方式。它将创建一个新的桌面会话,该会话将被远程控制和访问。这里的 VNC 会话与可能正在树莓派显示器上活动的会话是分开的。

准备工作

确保您的树莓派已开机并连接到互联网。我们将使用互联网连接通过apt-get安装程序。这是一个允许我们从官方仓库直接查找和安装应用程序的程序。

如何操作...

首先,我们需要使用以下命令在树莓派上安装TightVNC服务器。建议首先运行一个update命令,以获取您要安装的软件包的最新版本,如下所示:

sudo apt-get update
sudo apt-get install tightvncserver

接受提示以安装,并等待安装完成。要开始会话,请使用以下命令启动会话:

vncserver :1

第一次运行此程序时,它会要求您输入一个密码(不超过八个字符),以便访问桌面(当您从计算机连接时将使用此密码)。

以下消息应确认已启动新的桌面会话:

New 'X' desktop is raspberrypi:1

如果你还没有知道 Raspberry Pi 的 IP 地址,请使用 hostname –I 并记下它。

接下来,我们需要运行一个 VNC 客户端,VNC Viewer 是一个合适的程序,可在 www.realvnc.com/ 获取,并且应该在 Windows、Linux 和 OS X 上运行。

当你运行 VNC Viewer 时,你将需要输入 服务器 地址和 加密 类型。使用你的 Raspberry Pi 的 IP 地址加上 :1。例如,对于 IP 地址 192.168.1.69,使用 192.168.1.69:1 地址。

你可以将加密类型设置为 关闭自动

根据你的网络,你可能能够使用主机名;默认为 raspberrypi,即 raspberrypi:1

你可能会收到关于之前未连接到计算机或没有加密的警告。如果你在使用公共网络或通过互联网进行连接(以阻止他人拦截你的数据),你应该启用加密。

更多信息…

你可以向命令行添加选项来指定分辨率以及显示的颜色深度。分辨率和颜色深度越高(可以调整到每像素使用 8 到 32 位以提供低或高颜色细节),通过网络链路传输的数据就越多。如果你觉得刷新率有点慢,可以尝试按以下方式减少这些数值:

vncserver :1 –geometry 1280x780 –depth 24

要在开机时自动启动 VNC 服务器,可以将 vncserver 命令添加到 .bash_profile(每次 Raspberry Pi 启动时都会执行)。

按照以下方式使用 nano 编辑器(-c 选项允许显示行号):

sudo nano -c ~/.bash_profile

将以下行添加到文件末尾:

vncserver :1

下次开机时,你应该能够从另一台计算机使用 VNC 远程连接。

通过 SSH(和 X11 转发)在网络中远程连接到 Raspberry Pi

SSH安全壳)通常是建立远程连接的首选方法,因为它只允许终端连接,并且通常需要的资源更少。

SSH 的一个额外功能是将 X11 数据传输到运行在你机器上的 X Windows 服务器。这允许你启动通常在 Raspberry Pi 桌面上运行的程序,它们将在本地计算机上显示为独立的窗口,如下所示:

通过 SSH(和 X11 转发)在网络中远程连接到 Raspberry Pi

在本地显示上启用 X11 转发

可以使用 X 转发来显示在 Raspberry Pi 上运行的应用程序,在 Windows 计算机上显示。

准备工作

如果你正在运行最新的 Raspbian 版本,SSH 和 X11 转发将默认启用(否则,请检查 如何工作… 部分中解释的设置)。

如何操作…

Linux 和 OS X 内置了对 X11 转发的支持;但如果你使用的是 Windows,你将需要在你的计算机上安装并运行 X Windows 服务器。

Xming网站([sourceforge.net/projects/xming/](http://sourceforge.net/projects/xming/))下载并运行xming

安装xming,按照安装步骤进行,包括如果你还没有的话安装PuTTY。你也可以从http://www.putty.org/单独下载 PuTTY。

接下来,我们需要确保我们使用的 SSH 程序在连接时启用了 X11。

对于 Windows 系统,我们将使用 PuTTY 连接到树莓派。

PuTTY 配置对话框中,导航到连接 | SSH | X11,勾选X11 转发复选框。如果你留空X 显示位置选项,它将默认为Server 0:0(你可以通过在 Xming 运行时将鼠标移到系统托盘中的 Xming 图标上来确认服务器编号):

如何操作…

在 PuTTY 配置中启用 X11 转发

会话设置中输入树莓派的 IP 地址(你也许也可以在这里使用树莓派的计算机名;默认计算机名是raspberrypi)。

使用合适的名称,例如RaspberryPi,保存设置,然后点击打开以连接到你的树莓派。

你可能会看到一个警告消息弹出,表明你之前没有连接到该计算机(这允许你在继续之前检查你是否一切都设置正确)。

如何操作…

使用 PuTTY 打开到树莓派的 SSH 连接

对于 OS X 或 Linux,点击终端以打开到树莓派的连接。

要使用默认的pi用户名,IP 地址为192.168.1.69,请使用以下命令;-X选项启用 X11 转发:

ssh -X pi@192.168.1.69

如果一切顺利,你应该会看到一个提示输入密码的提示符(记住pi用户的默认密码是raspberry)。

通过从你的计算机的开始菜单启动 Xming 程序来确保 Xming 正在运行。然后,在终端窗口中,输入通常在树莓派桌面中运行的程序,例如leafpadscratch。稍等片刻,程序应该会出现在你的计算机桌面上(如果你收到错误,你可能忘记启动 Xming,所以请运行它并再次尝试)。

它是如何工作的…

X Windows 和 X11 是提供方法,使得树莓派(以及许多其他基于 Linux 的计算机)可以作为桌面的一部分显示和控制图形窗口。

要在网络连接上使 X11 转发工作,我们需要在树莓派上同时启用 SSH 和 X11 转发。执行以下步骤:

  1. 要开启(或关闭)SSH,您可以在 桌面首选项 菜单下访问 Raspberry Pi 配置 程序,然后在 接口 选项卡中点击 SSH,如下面的截图所示(SSH 在大多数发行版中通常默认启用,以帮助允许远程连接,而无需监视器来配置它):工作原理…

    Raspberry Pi 配置工具中的“接口”选项卡

  2. 确保在 Raspberry Pi 上启用了 X11 Forwarding(再次提醒,现在大多数发行版默认都启用了此功能)。

  3. 使用以下命令的 nano

    sudo nano /etc/ssh/sshd_config
    
    
  4. /etc/ssh/sshd_config 文件中查找控制 X11 Forwarding 的行,并确保它说 yes(前面没有 # 符号),如下所示:

    X11Forwarding yes
    
    
  5. 如果需要,按 Ctrl + XYEnter 保存,并根据需要重新启动(如果需要更改它)如下所示:

    sudo reboot
    
    

还有更多…

SSH 和 X 11 Forwarding 是远程控制 Raspberry Pi 的便捷方式;在接下来的几节中,我们将探讨一些如何有效使用它的额外技巧。

使用 X11 Forwarding 运行多个程序

如果您想运行 X 程序,但仍然能够使用相同的终端控制台进行其他操作,您可以通过以下方式在后台运行命令:&

leafpad &

只需记住,您运行的程序越多,一切都会变得越慢。您可以通过输入 fg 切换到后台程序,并通过 bg 检查后台任务。

以桌面形式运行并启用 X11 Forwarding

您甚至可以通过 X11 运行完整的桌面会话,尽管它并不特别用户友好,VNC 会产生更好的结果。为了实现这一点,您必须使用 lxsession 而不是 startx(就像您通常从终端启动桌面一样)。

另一个选择是使用 lxpanel,它提供了一个程序菜单栏,您可以从菜单中启动和运行程序,就像在桌面上一样。

使用 X11 Forwarding 运行 PyGame 和 Tkinter

当运行 PyGameTkinter 脚本时,您可能会遇到以下错误(或类似错误):

_tkinter.TclError: couldn't connect to display "localhost:10.0"

在这种情况下,使用以下命令来修复错误:

sudo cp ~/.Xauthority ~root/

使用 SMB 共享 Raspberry Pi 的主文件夹

当您将 Raspberry Pi 连接到您的网络时,您可以通过设置文件共享来访问主文件夹;这使得文件传输变得容易得多,并提供了一种快速简单的方法来备份您的数据。服务器消息块SMB)是一种与 Windows 文件共享、OS X 和 Linux 兼容的协议。

准备工作

确保您的 Raspberry Pi 已开启并运行,并且与互联网有正常连接。

您还需要另一台在同一本地网络上的计算机来测试新的共享。

如何操作…

首先,我们需要安装 samba,这是一款软件,它以与 Windows 共享方法兼容的格式处理文件夹共享。

确保您使用以下命令来获取最新可用的软件包列表:update

sudo apt-get update
sudo apt-get install samba

安装需要大约 20MB 的空间,并花费几分钟时间。

安装完成后,我们可以按照以下步骤复制配置文件,以便在需要时恢复默认设置:

sudo cp /etc/samba/smb.conf /etc/samba/smb.conf.backup
sudo nano /etc/samba/smb.conf

滚动并找到名为身份验证的部分;将# security = user行更改为security = user

如文件所述,此设置确保你必须输入树莓派的用户名和密码才能访问文件(这对于共享网络来说很重要)。

找到名为共享定义[homes]的部分,将read only = yes行更改为read only = no

这将允许我们查看并写入共享主文件夹中的文件。完成后,通过按Ctrl + XYEnter保存并退出。

注意

如果你已将默认用户从pi更改为其他名称,请在以下说明中替换它。

现在,我们可以将pi(默认用户)添加到使用samba

sudo pdbedit -a -u pi

现在,输入密码(你可以使用与登录相同的密码或选择一个不同的密码,但避免使用默认的树莓派密码,因为这很容易被人猜到)。按照以下步骤重启samba以使用新的配置文件:

sudo /etc/init.d/samba restart
[ ok ] Stopping Samba daemons: nmbd smbd.
[ ok ] Starting Samba daemons: nmbd smbd.

为了测试,你需要知道树莓派的计算机名(默认计算机名是raspberrypi)或其 IP 地址。你可以使用以下命令找到这两个信息:

hostname

对于 IP 地址,添加-I

hostname –I

在网络上的另一台计算机上,在资源管理器路径中输入\\raspberrypi\pi地址。

根据你的网络,计算机应该能在网络上定位到树莓派,并提示输入用户名和密码。如果它无法使用主机名找到共享,你可以直接使用 IP 地址,其中192.168.1.69应更改为匹配的 IP 地址\\192.168.1.69\pi

保持树莓派更新

树莓派使用的 Linux 镜像通常会更新,包括增强、修复和对系统的改进,以及添加对新硬件的支持或对最新板所做的更改。你安装的大多数软件包也可以更新。

这尤其重要,如果你计划在另一块树莓派板上(尤其是较新的板上)使用相同的系统镜像(尤其是较旧的镜像将不支持任何布线更改或替代 RAM 芯片)。新固件应在较旧的树莓派板上工作,但较旧的固件可能不与最新硬件兼容。

幸运的是,你不必每次有新版本发布时都刷新你的 SD 卡,因为你可以选择更新它。

准备工作

你需要连接到互联网才能更新你的系统。始终建议首先备份你的镜像(至少复制你的重要文件)。

你可以使用uname -a命令检查你的当前固件版本,如下所示:

Linux raspberrypi 4.4.9-v7+ #884 SMP Fri May 6 17:28:59 BST 2016 armv7l GNU/Linux

可以使用/opt/vc/bin/vcgencmd version命令检查 GPU 固件,如下所示:

 May  6 2016 13:53:23
Copyright (c) 2012 Broadcom
version 0cc642d53eab041e67c8c373d989fef5847448f8 (clean) (release) 
This is important if you are using an older version of firmware (pre-November 2012) on a newer board since the original Model B board was only 254 MB RAM. Upgrading allows the firmware to make use of the extra memory if available.

free -h 命令将详细显示主处理器(总 RAM 在 GPU 和 ARM 核心之间分配)可用的 RAM,并将给出以下输出:

 total       used       free     shared    buffers     cached
Mem:          925M       224M       701M       7.1M        14M       123M
-/+ buffers/cache:        86M       839M
Swap:          99M         0B        99M

然后,在重启后重新检查前面的输出,以确认它们已被更新(尽管它们可能已经是最新版本)。

如何做到这一点…

在运行任何升级或安装任何软件包之前,确保你有仓库中最新软件包列表是值得的。update 命令获取最新可用的软件和版本列表:

sudo apt-get update

如果你只想获取当前软件包的升级,upgrade 将将它们全部更新到最新:

sudo apt-get upgrade

为了确保你正在运行最新的 Raspbian 版本,你可以运行dist-upgrade(警告:这可能需要一个小时左右,具体取决于需要升级的内容量)。这将执行upgrade将执行的所有更新,但还会删除冗余软件包并清理:

sudo apt-get dist-upgrade

这两种方法都将升级软件,包括启动和启动时使用的固件(bootcode.binstart.elf)。

还有更多…

你经常会发现你想要对你的设置进行干净安装;然而,这意味着你将不得不从头开始安装一切。为了避免这种情况,我开发了 Pi-Kitchen 项目(github.com/PiHw/Pi-Kitchen),基于 Kevin Hill 的基础工作。该项目旨在提供一个灵活的平台,用于创建可自动部署到 SD 卡的定制设置。

还有更多…

Pi Kitchen 允许在开机前配置 Raspberry Pi

Pi-Kitchen 允许配置一系列口味,这些口味可以从 NOOBS 菜单中选择。每种口味都由一系列食谱组成,每个食谱都为最终操作系统提供特定的功能或特性。食谱的范围可以从为 Wi-Fi 设备设置自定义驱动程序,到映射网络上的共享驱动器,到提供开箱即用的完整功能 Web 服务器,所有这些组合起来使你的所需设置成为可能。

此项目处于测试阶段,作为一个概念验证而开发,但一旦你配置好一切,直接将完整工作的设置部署到 SD 卡上可以非常有用。最终,该项目可以与 Kevin Hill 的高级版本 NOOBS 相结合,称为 PINN(代表 PINN Is Not NOOBS),旨在为高级用户提供额外功能,例如允许操作系统和配置存储在网络或外部 USB 闪存驱动器上。

第二章. 从 Python 字符串、文件和菜单开始

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

  • 处理文本和字符串

  • 使用文件和处理错误

  • 创建启动菜单

  • 创建一个自我定义的菜单

介绍

在本章中,我们将讨论如何使用 Python 通过打乱字母来执行一些基本的加密。这将介绍一些基本的字符串操作、用户输入,并逐步创建可重用的模块和图形用户界面。

接下来,我们将创建一些有用的 Python 脚本,可以将它们添加到启动时运行或在需要时快速运行的命令,提供对常用或频繁使用的命令的快捷方式。进一步来说,我们将利用线程来运行多个任务,并引入类来定义多个对象。

由于任何编程练习的传统做法都是以 Hello World 示例开始,我们现在就从这个例子开始。

使用 nano 创建 hellopi.py 文件,如下所示:

nano -c hellopi.py

在我们的 hellopi.py 文件中,添加以下代码:

#!/usr/bin/python3
#hellopi.py
print ("Hello Raspberry Pi")

完成后,保存并退出(Ctrl + XY,和 Enter)。要运行文件,请使用以下命令:

python3 hellopi.py

恭喜,您已经创建了您的第一个程序!

您的结果应类似于以下截图:

介绍

Hello Raspberry Pi 输出

处理文本和字符串

Python 的一个良好起点是了解基本的文本处理和字符串。字符串是一组字符作为一个值存储在一起。正如您将学到的,它们可以被视为字符的简单列表。

我们将创建一个脚本来获取用户的输入,使用字符串操作来交换字母,并打印出编码后的消息。然后我们将通过演示如何在不透露编码方法的情况下在各方之间传递编码消息,同时展示如何在其他 Python 模块中重用代码段来扩展这个示例。

准备就绪

您可以使用大多数文本编辑器来编写 Python 代码。它们可以直接在 Raspberry Pi 上使用,或者通过 VNC 或 SSH 远程使用。

以下是一些与 Raspberry Pi 一起提供的文本编辑器:

  • nano:这个文本编辑器在终端可用,包括语法高亮和行号(使用 -c 选项)。请参考以下截图:准备就绪

    nano 命令行编辑器

  • IDLE3:这个 Python 编辑器包括语法高亮功能、上下文帮助,并且可以直接从编辑器中运行脚本(按 F5)。此程序需要 X-Windows(Debian 桌面)或 X11 转发以远程运行。本书将使用 Python 3,所以请确保您运行 IDLE3(而不是 IDLE),它将使用 Python 3 来运行脚本,如下面的截图所示:准备就绪

    IDLE3 Python 编辑器

  • Geany:这个文本编辑器提供了一个集成开发环境IDE),支持多种编程语言,语法高亮,自动完成和易于代码导航。这是一个功能丰富的编辑器,但对于初学者来说可能难以使用,并且在 Raspberry Pi 上运行时有时可能会变慢。再次提醒,您需要使用 Debian 桌面或 X11 转发来运行此编辑器。请参考以下截图:准备中

    Geany IDE

    要安装 Geany,请使用以下命令,然后从编程菜单项运行Geany

    sudo apt-get install geany
    
    

    为了确保当你点击执行按钮(运行你的脚本)时 Geany 使用 Python 3,你需要更改构建命令。加载hellopi.py,然后点击构建菜单并选择设置构建命令。在出现的窗口中,如图所示,将编译执行部分中的python更改为python3。Python 在运行时总是自动编译(生成临时的.pyc文件),所以你不需要使用编译按钮,除非你可能需要检查代码的语法:

    准备中

    Geany 为 Python 3 的构建命令设置

如果你将 Raspberry Pi 的home目录通过网络共享(请参阅第一章中的使用 SMB 共享 Raspberry Pi 的 home 文件夹菜谱),你可以在另一台计算机上编辑文件。但是请注意,如果你使用 Windows,你必须使用支持 Linux 行结束符的编辑器,例如 Notepad++(你不应该使用标准的记事本程序)。

要为你的 Python 脚本创建空间,我们将使用以下命令在你的home目录中添加一个python_scripts文件夹:

mkdir ~/python_scripts

现在,你可以打开这个文件夹,并在需要时使用以下命令列出文件:

cd ~/python_scripts
ls

小贴士

你可以使用Tab键帮助在终端中完成命令,例如,键入cd ~/pyt然后按Tab键将为你完成命令。如果有多个以pyt开头的选项,再次按Tab键将列出它们。

要重复或编辑较旧的命令,请使用上箭头键和下箭头键在较旧和较新的命令之间切换,根据需要操作。

如何操作…

按照以下方式创建encryptdecrypt.py脚本:

#!/usr/bin/python3
#encryptdecrypt.py

#Takes the input_text and encrypts it, returning the result
def encryptText(input_text,key):
  input_text=input_text.upper()
  result = ""
  for letter in input_text:
    #Ascii Uppercase 65-90  Lowercase 97-122 (Full range 32-126)
    ascii_value=ord(letter)
    #Exclude non-characters from encryption
    if (ord("A") > ascii_value) or (ascii_value > ord("Z")):
      result+=letter
    else:
      #Apply encryption key
      key_value = ascii_value+key
      #Ensure we just use A-Z regardless of key
      if not((ord("A")) < key_val < ord("Z")):
        key_val = ord("A")+(key_val-ord("A"))\
                         %(ord("Z")-ord("A")+1)
      #Add the encoded letter to the result string
      result+=str(chr(key_value))
  return result

#Test function
def main():
  print ("Please enter text to scramble:")
  #Get user input
  try:
    user_input = input()
    scrambled_result = encryptText(user_input,10)
    print ("Result: " + scrambled_result)
    print ("To un-scramble, press enter again")
    input()
    unscrambled_result = encryptText(scrambled_result,-10)
    print ("Result: " + unscrambled_result)
  except UnicodeDecodeError:
    print ("Sorry: Only ASCII Characters are supported")

main()
#End

小贴士

在这个菜谱的还有更多…部分,我们将把main()更改为以下代码:

if __name__=="__main__":
  main()

如果你想跳过这一部分,请确保你在encryptdecrypt.py文件中包含这个更改,因为我们稍后会用到它。

它是如何工作的…

上述脚本实现了一种非常基本的通过称为凯撒密码的简单字符替换形式来打乱文本的方法。这种方法是以罗马皇帝凯撒命名的,他最初使用这种方法向他的军队发送秘密命令。

该文件定义了两个函数,encryptText()main()

当脚本运行时,main()函数使用input()命令获取用户的输入。结果存储在user_input变量中(该命令将在用户按下Enter键之前等待),如下所示:

user_input = input()

注意

input()函数不会处理非 ASCII 字符,因此我们使用try…except来处理这种情况,这会导致抛出UnicodeDecodeError。有关使用try…except的更多信息,请参阅本章的使用文件和处理错误配方。

我们将使用两个参数调用encryptText()函数,即要加密的文本和密钥。文本加密后,结果将被打印出来:

scrambled_result = encryptText(user_input,10)
print ("Result: " + scrambled_result)

最后,我们将使用input()再次等待用户输入(在这种情况下,提示按Enter;任何其他输入都将被忽略)。现在,我们将通过再次调用encryptText()并使用密钥的负值来反转加密,并显示结果,结果应该是原始消息。

encryptText()函数通过将消息中的字母替换为字母表中另一个字母(由加密key指定的字母数量决定)来执行一种简单的加密形式。这样,当加密key2时,字母A将变为C

为了简化过程,该函数将所有字符转换为大写。这使得我们可以使用 ASCII 字符集轻松地将每个字符转换为数字;字母A表示为65,而Z表示为90。这是通过input_text=input_text.upper()实现的,然后使用ord(letter)将其转换为 ASCII 值,这给我们提供了字符的数字表示。ASCII 是一个将数字 0 到 254(一个 8 位值)映射到常用字符和符号的标准:

A B C D E F G H I J K L M
65 66 67 68 69 70 71 72 73 74 75 76 77
N O P Q R S T U V W X Y Z
78 79 80 81 82 83 84 85 86 87 88 89 90

大写字母的 ASCII 表

接下来,我们将确保我们有一个空字符串,我们可以在这个字符串中构建我们的结果(result = ""),同时我们也将我们的加密key设置为密钥值。

input_text变量包含我们的字符串,它被存储为一个字母列表(这与数组类似)。我们可以使用input_text[0]访问列表中的第一个项目,依此类推;然而,Python 还允许我们使用for…in循环遍历列表,依次访问每个项目。

for letter in input_text: 这一行代码允许我们通过遍历 input_text 中的每个项目(在这种情况下,字符串中的字母)来将其拆分,并且将字母设置为该项目。所以如果 input_text 等于 HELLO,它将运行命令下缩进的所有代码五次;每次,letter 变量将被设置为 HELL,最后是 O。这使得我们可以单独读取每个字母,处理它,并将新的加密字母添加到 result 字符串中。

下一个部分,if (ord("A") > ascii_value) or (ascii_value > ord("Z")): 检查我们正在查看的字符是否不在 AZ 之间,这意味着它可能是一个数字或标点符号。在这种情况下,我们将从加密中排除该字符(直接将字符不变地传递到 result 字符串中)。

如果字母在 AZ 之间,我们可以将我们的加密 key 的值添加到我们的字母的值,以获得我们新的编码字母。也就是说,对于加密 key10 的情况,我们在输出中最终得到以下字母集:

Input Letter:  A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
Output Letter: K L M N O P Q R S T U V W X Y Z A B C D E F G H I J

由于我们希望加密的消息易于书写,我们已将输出限制在 AZ 之间。所以,如果字母以 X 开头,我们希望它绕回并从 A 继续计数。我们可以通过使用 %(模数)函数来实现这一点,它给出了当我们用一个数除以另一个数时的余数值。所以,如果 X24,我们加上 10 得到 3434%26(其中 26 是字母的总数)的值是 8。从 A 开始计数 8,我们到达 H

然而,在 ASCII 中,字母 A 是数字 65,因此我们将从 key_value 中减去这个偏移量,然后在得到模数值后将其加回。以下代码确保我们将 ASCII 值限制在 AZ 之间:

#Ensure we just use A-Z regardless of key
if not((ord("A")) < key_value < ord("Z")):
  key_value = ord("A")+(key_value-ord("A"))\
                       %(ord("Z")-ord("A")+1)

实际上,如果值不在 AZ 的值之间,我们将允许值通过(通过使用 AZ 之间字母的总数计算模数,即 26)。这也适用于密钥大于 26 的情况,或者我们在相反方向计数时,例如,如果加密密钥是负数,那么解密密钥就是正数。

最后,我们可以通过使用 chr()str() 函数将 key_value 转换回字母,并将其添加到结果字符串中。

注意

注意,我们使用 \ 来将代码拆分到另一行,这不会影响计算。Python 对换行非常挑剔,在某些情况下,你可以在代码中找到一个自然的断点,并用回车换行符分隔行,然而在其他时候,我们必须使用 \ 符号强制换行。

当然,在很短的时间内,这种简单的加密方法很容易被破解。记住,在加密结果重复之前,只有 25 种可能的组合可以选择(26 的倍数将导致没有任何加密)。

还有更多…

你可以尝试这个简单的实验。目前,使用这种基本的加密形式,你将向任何你想阅读你信息的人提供方法和密钥。然而,如果你想在不发送方法和密钥的情况下发送安全的传输会发生什么呢?

答案是发送相同的信息往返三次,如下面的图示所示:

还有更多…

我们不需要与其他人交换加密密钥

第一次,我们将对其进行加密并发送给另一方。然后他们将以自己的加密方式再次加密并发送回来。此时,信息已经应用了两层加密。我们现在可以移除我们的加密并返回。最后,他们将只收到他们的加密信息,他们可以移除并阅读信息。

只需记住,凯撒密码只有 25 种有用的加密组合,因此他们可能意外地解密了信息。

我们可以使用之前的文件作为模块,通过以下import命令使用:

import encryptdecrypt as ENC

这将允许使用ENC作为参考,访问encryptdecrypt文件中的任何函数。当导入此类文件时,它将运行通常会被运行的任何代码;在这种情况下,main()函数。

注意

为了避免这种情况,我们可以只将main()函数的调用改为在文件直接运行时发生。

如果文件直接运行,Python 将__name__设置为"__main__"全局属性。通过使用以下代码,我们可以在其他脚本中重用此 Python 脚本中的函数,而无需运行任何其他代码:

if __name__=="__main__":
  main()

在与encryptdecrypt.py相同的目录下创建keypassing.py脚本,使用以下代码:

#!/usr/bin/python3
#keypassing.py
import encryptdecrypt as ENC

KEY1 = 20
KEY2 = 50

print ("Please enter text to scramble:")
#Get user input
user_input = input()
#Send message out
encodedKEY1 = ENC.encryptText(user_input,KEY1)
print ("USER1: Send message encrypted with KEY1 (KEY1): " + encodedKEY1)
#Receiver encrypts the message again
encodedKEY1KEY2 = ENC.encryptText(encodedKEY1,KEY2)
print ("USER2: Encrypt with KEY2 & returns it (KEY1+KEY2): " + encodedKEY1KEY2)
#Remove the original encoding
encodedKEY2 = ENC.encryptText(encodedKEY1KEY2,-KEY1)
print ("USER1: Removes KEY1 & returns with just KEY2 (KEY2): " + encodedKEY2)
#Receiver removes their encryption
message_result = ENC.encryptText(encodedKEY2,-KEY2)
print ("USER2: Removes KEY2 & Message received: " + message_result)
#End

在运行前面的脚本时,我们可以看到,其他人不需要知道我们使用的加密密钥,任何拦截信息的人都将无法看到其内容。该脚本产生以下输出:

Please enter text to scramble:
"A message to a friend."
USER1: Send message encrypted with KEY1 (KEY1): U GYMMUAY NI U ZLCYHX.
USER2: Encrypt with KEY2 & returns it (KEY1+KEY2): S EWKKSYW LG S XJAWFV.
USER1: Removes KEY1 & returns with just KEY2 (KEY2): Y KCQQYEC RM Y DPGCLB.
USER2: Removes KEY2 & Message received: A MESSAGE TO A FRIEND.

这种方法被称为三次传递协议,由 Adi Shamir 于 1980 年开发(en.wikipedia.org/wiki/Three-pass_protocol)。这种方法的一个特定缺点是,第三方可能拦截信息(所谓的中间人攻击)并通过插入已知值和分析响应来识别加密方法。

使用文件和处理错误

除了易于字符串处理外,Python 还允许你轻松地读取、编辑和创建文件。因此,通过在先前的脚本上构建,我们可以利用我们的encryptText()函数来编码完整的文件。

读取和写入文件可能非常依赖于脚本直接控制之外的因素,例如我们试图打开的文件是否存在或文件系统是否有空间存储新文件。因此,我们还将探讨如何处理异常并保护可能产生错误的操作。

准备工作

以下脚本将允许你通过命令行指定一个文件,该文件将被读取并编码以生成输出文件。创建一个名为infile.txt的小文本文件并保存它,以便我们可以测试脚本。它应包含类似于以下的消息:

This is a short message to test our file encryption program.

如何操作…

使用以下代码创建fileencrypt.py脚本:

#!/usr/bin/python3
#fileencrypt.py
import sys #Imported to obtain command line arguments
import encryptdecrypt as ENC

#Define expected inputs
ARG_INFILE=1
ARG_OUTFILE=2
ARG_KEY=3
ARG_LENGTH=4

def covertFile(infile,outfile,key):
  #Convert the key text to an integer
  try:
    enc_key=int(key)
  except ValueError:
    print ("Error: The key %s should be an integer value!" % (key))
  #Code put on to two lines
  else:
    try:
      #Open the files
      with open(infile) as f_in:
        infile_content=f_in.readlines()
    except IOError:
      print ("Unable to open %s" % (infile))
    try:
      with open(outfile,'w') as f_out:
        for line in infile_content:
          out_line = ENC.encryptText(line,enc_key)
          f_out.writelines(out_line)
    except IOError:
      print ("Unable to open %s" % (outfile))
    print ("Conversion complete: %s" % (outfile))
  finally:
    print ("Finish")

#Check the arguments
if len(sys.argv) == ARG_LENGTH:
  print ("Command: %s" %(sys.argv))
  covertFile(sys.argv[ARG_INFILE], sys.argv[ARG_OUTFILE], sys.argv[ARG_KEY])
else:
  print ("Usage: fileencrypt.py infile outfile key")
#End

要运行脚本,请使用以下命令(在这里,infile可以是任何我们想要加密的文本文件,outfile是我们的加密版本,key是我们希望使用的密钥值):

python3 fileencrypt.py infile outfile key

例如,要使用30作为密钥加密infile.txt并输出为encrypted.txt,请使用以下命令:

python3 fileencrypt.py infile.txt encrypted.txt 30

要查看结果,使用less encrypted.txt。按Q键退出。

要使用-30作为密钥解密encrypted.txt并输出为decrypted.txt,请使用以下命令:

python3 fileencrypt.py encrypted.txt decrypted.txt -30

要查看结果,使用less decrypted.txt。按Q键退出。

它是如何工作的…

脚本要求我们使用命令行提供的参数。我们将通过导入名为sys的 Python 模块来访问它们。就像我们之前做的那样,我们也将使用import命令导入我们的encryptdecrypt模块。我们将使用as部分来允许我们使用ENC来引用它。

接下来,我们将设置值来定义每个命令行参数将代表什么。当你运行它时,你会看到sys.argv[]是一个值列表,如下所示:

['fileencrypt.py', 'infile.txt', 'encrypted.txt', '30']

因此,输入文件在列表中的索引是1(索引始终从 0 开始),然后是输出文件,最后是密钥,参数总数为ARG_LENGTH=4

接下来,我们将定义convertFile()函数,我们将在下一块代码中调用它。

为了避免错误,我们将检查sys.argv值的长度是否与命令行期望的参数数量相匹配。这将确保用户已经提供了足够的参数,我们不会尝试引用sys.argv[]列表中不存在的项。否则,我们将返回一条简短的消息,解释我们期望的内容。

我们现在将使用命令行值调用convertFile()函数,并利用 Python 内置的异常处理功能来确保错误得到相应的响应。

try…except代码允许你尝试运行一些代码,并在程序内部处理任何异常(错误),而不是让一切突然停止。

try代码伴随着以下四个可选部分:

  • except ValueError: – 当发生错误时,可以指定并处理特定类型的异常,具体取决于我们希望处理的错误(即,对于ValueError,我们可以检查值是否为浮点值并将其转换为整数,或者提示输入新的值)。根据需要,可以使用except (ValueError, IOError)捕获多个异常。

  • except: – 这是一个通用的捕获情况,其中我们可以处理我们尚未处理的任何其他异常。对于可能从其他地方调用的代码,我们可能还希望使用raise命令再次引发异常,以便其他程序部分可以处理它(例如,作为 GUI 的一部分,我们可以警告用户输入不正确,而无需在此阶段这样做)。通常,你应该处理特定的异常或确保再次引发它,以便特定的错误在失败时可见;如果根本未处理,Python 将在终端上报告它,并附带发生错误的函数的跟踪信息。

  • else: – 如果try代码成功且没有引发异常,则始终执行此代码段;然而,此代码中的任何错误都不会由它所属的try…except部分处理。

  • finally: – 无论是否引发异常或try代码运行无问题,此代码总是执行。

如果你熟悉其他语言,你会发现try…except类似于try…catchraisethrow是等效的。处理异常可以是一种相当的艺术形式;然而,使你的代码能够优雅有效地处理问题是良好设计的一部分。在许多情况下,捕捉出错的情况与成功执行预期功能一样重要。

如果将key参数转换为整数没有问题,我们将继续打开指定的输入文件,并将内容读取到infile_content列表中。这将包含文件内容,按单独的行分割成列表。

注意

在此示例中,我们将使用一种稍微不同的方法在print语句中显示值。

考虑以下代码作为示例:

print ("错误:键%s 应该是整数值!"%key)

这允许我们使用%s符号来确定键值打印的位置,也可以指定格式(%s是一个字符串)。对于数值,如浮点数和整数,我们可以使用%d显示整数,%f用于浮点数,甚至%.4f将值限制为四位小数。

你可能已经注意到我们使用with…as…:部分打开了文件。这是一种特殊的打开文件方式,它将确保文件完成操作后关闭(即使有错误)。请参考以下代码:

try:
  #Open the files
  with open(infile) as f_in:
    infile_content=f_in.readlines()
except IOError:
  print ("Unable to open %s" % (infile))

这相当于以下内容:

try:
  f_in = open(infile)
  try:
    infile_content=f_in.readlines()
  finally:
    f_in.close()
  except IOError:
    print ("Unable to open %s" % (infile))

如果在打开文件时出现异常(例如,如果文件不存在,它将引发IOError),我们可以向用户标记提供的文件名/路径存在问题。我们还将使用except:单独处理可能遇到的任何其他问题,例如编码类型或非文本文件。

接下来,我们将使用'w'模式打开一个文件作为可写文件进行输出。如果文件不存在,它将创建一个新文件;否则,它将覆盖文件。我们还将有使用'a'模式追加到文件的选择。我们将逐项遍历infile_content中的每个项目,通过传递它通过我们的ENC.encryptText()函数并将行写入f_out文件来转换每一行。再次强调,当我们完成with…as…:部分时,文件将被关闭,转换完成。

创建启动菜单

我们现在将应用之前脚本中介绍的方法,并将它们重新应用于创建一个我们可以自定义以展示一系列快速运行的命令和程序的菜单。

如何实现...

使用以下代码创建menu.py脚本:

#!/usr/bin/python3
#menu.py
from subprocess import call

filename="menu.ini"
DESC=0
KEY=1
CMD=2

print ("Start Menu:")
try:
  with open(filename) as f:
    menufile = f.readlines()
except IOError:
  print ("Unable to open %s" % (filename))
for item in menufile:
  line = item.split(',')
  print ("(%s):%s" % (line[KEY],line[DESC]))
#Get user input
running = True
while(running):
  user_input = input()
  #Check input, and execute command
  for item in menufile:
    line = item.split(',')
    if (user_input == line[KEY]):
      print ("Command: " + line[CMD])
      #call the script
      #e.g. call(["ls", "-l"])
      commands = line[CMD].rstrip().split()
      print (commands)
      running = False
      #Only run command is one if available
      if len(commands):
        call(commands)
  if (running==True):
    print ("Key not in menu.")
print ("All Done.")
#End

创建一个包含以下菜单项和命令的menu.ini文件:

Start Desktop,d,startx
Show IP Address,i,hostname -I
Show CPU speed,s,cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq
Show Core Temperature,t,sudo /opt/vc/bin/vcgencmd measure_temp
Exit,x,

您可以向列表中添加自己的命令,创建自己的自定义启动菜单。脚本将假设menu.ini文件格式正确,因此如果您遇到错误(例如ErrorIndex),那可能是因为文件不符合预期(例如缺少逗号或空白行)。我们可以使用except ErrorIndex:来处理任何错误,但我们最好突出显示输入文件中存在问题,以便可以修复。

工作原理...

为了在 Python 脚本中执行其他程序,我们需要使用call命令。这次,我们只想使用subprocess模块的call部分,因此我们可以简单地使用from subprocess import call。这仅仅导入了我们需要的部分。

我们将打开文件并将所有行读入一个menufile列表。然后我们可以使用item.split(',')处理每个项目(或文件的每一行),这将创建一个新的列表,该列表由','符号分隔的行的各个部分组成,如下所示:

line = ['Start Desktop', 'd', 'startx\n']

print语句所示,我们现在可以独立访问每个部分,因此我们可以打印出执行特定命令所需按下的键以及命令的描述。

一旦我们打印出整个命令菜单,我们将等待用户输入。这是在while循环中完成的;它将继续运行,直到我们将running内部的条件设置为False。这意味着如果按下了无效的键,我们可以输入另一个键,直到选择了一个命令或使用了退出项。然后我们将检查输入键是否与菜单项分配的键匹配,如下所示:

user_input == line[KEY]

如果有匹配项,我们将提取我们希望调用的命令。call命令要求命令及其参数是一个列表,因此我们将使用.split()将命令部分拆分成一个列表(其中命令中的每个空格都是列表中的新项)。此外,请注意startx之后有/n,这是来自menu.ini文件的行结束符。我们将首先使用.rstrip()移除它,该函数会从字符串的末尾移除任何空白(空格、制表符或换行符)。

一旦命令被格式化为参数列表,我们将设置 runningFalse(这样 while 循环就不会进入另一个循环),执行我们的命令,并完成脚本。如果用户选择 x,则不会有 commands 设置,这样我们就可以在未调用任何内容的情况下退出菜单。脚本会生成一个包含选项的小菜单,如下所示:

Start Menu:
(d):Start Desktop
(i):Show IP Address
(s):Show CPU speed
(t):Show Core Temperature
(x):Exit
g
Key not in menu.
i
Command: hostname -I
['hostname', '-I']
All Done.

更多内容…

为了让脚本每次都能运行,我们将启动树莓派;我们可以从 .bash_profile 中调用它,这是一个在用户配置文件加载时运行的 bash 脚本。

按照以下方式创建或编辑文件:

nano -c ~/.bash_profile

添加以下命令(假设 menu.py 位于 /home/pi/python_scripts 目录中):

cd /home/pi/python_scripts
python3 menu.py

完成后,保存并退出(Ctrl + XY,和 Enter)。

下次您启动树莓派时,您将有一个菜单可以从中运行您喜欢的命令,而无需记住它们。

注意

您也可以直接运行 Python 脚本,而不需要 python3 命令,使它们可执行,如下所示:

chmod +x menu.py

现在输入 ./menu.py,脚本将使用文件第一行定义的程序运行,如下所示:

#!/usr/bin/python3

创建一个自定义菜单

虽然前面的菜单对于定义我们在运行树莓派时可能使用的最常见命令和功能非常有用,但我们会经常改变我们正在做的事情或开发脚本来自动化复杂任务。

为了避免需要不断更新和编辑 menu.ini 文件,我们可以创建一个可以列出已安装脚本并从其中动态构建菜单的菜单,如下面的截图所示:

创建一个自定义菜单

当前目录中所有 Python 脚本的菜单

如何做到这一点…

使用以下代码创建 menuadv.py 脚本:

#!/usr/bin/python3
#menuadv.py
import os
from subprocess import call

SCRIPT_DIR="." #Use current directory
SCRIPT_NAME=os.path.basename(__file__)

print ("Start Menu:")
scripts=[]
item_num=1
for files in os.listdir(SCRIPT_DIR):
  if files.endswith(".py"):
    if files != SCRIPT_NAME:
      print ("%s:%s"%(item_num,files))
      scripts.append(files)
      item_num+=1
running = True
while (running):
  print ("Enter script number to run: 1-%d (x to exit)" % (len(scripts)))
  run_item = input()
  try:
    run_number = int(run_item)
    if len(scripts) >= run_number > 0:
      print ("Run script number:" + run_item)
      commands = ["python3",scripts[run_number-1]]
      print (commands)
      call(commands)
      running = False
  except ValueError:
    #Otherwise, ignore invalid input
    if run_item == "x":
      running = False
      print ("Exit")
#End

它是如何工作的…

此脚本允许我们采取不同的方法。而不是预先定义命令或应用程序的列表,我们只需保留一个有用的脚本文件夹,并扫描它以创建一个可供选择的列表。在这种情况下,菜单将只列出 Python 脚本,并调用它们而不带任何命令行选项。

为了能够访问目录中的文件列表,我们可以使用 os 模块的 os.listdir() 函数。此函数允许我们指定一个目录,它将返回该目录中文件和目录的列表。

使用 SCRIPT_DIR="." 将允许我们在当前目录(即脚本运行的目录)中搜索。我们可以指定一个绝对路径(即 "//home/pi/python_scripts"),一个相对路径(即 "./python_scripts_subdirectory"),或者从当前目录导航到结构中的其他目录(即 "../more_scripts",其中 .. 符号将从当前目录向上移动一个级别,然后进入 more_scripts 目录,如果它存在的话)。

注意

如果目录不存在,将会抛出一个异常(OSError)。由于这个菜单的目的是简单地运行并显示列表,我们最好让异常引发错误并停止脚本。这将鼓励用户修复目录而不是尝试处理错误(例如,每次都提示输入另一个路径)。当脚本不运行时,用户也更容易找到并纠正路径。

我们还将使用os.path.basename(__file__)获取脚本的名称,这允许我们稍后从列表选项中排除menuadv.py脚本。

我们将创建一个空的scripts列表,并确保将item_num初始化为1。现在,我们将在for…in循环中直接调用os.listdir(SCRIPT_DIR),这样我们就可以处理它返回的每个目录或文件名。接下来,我们可以使用endswith()函数(另一个有用的字符串函数)检查每个项目的末尾,这允许我们查找字符串的特定结尾(在这种情况下,Python 脚本的结尾)。在这个阶段,如果找到,我们还可以从列表中排除menuadv.py脚本。

我们将脚本的名称与item_num一起打印出来,并将其添加到脚本列表中,最后递增item_num以确保它对下一个项目是正确的。

我们现在将提示用户输入相关的脚本编号(介于1和脚本总数之间),并等待用户通过input()输入。脚本将检查输入是否有效。如果是数字,它将停留在try部分,然后我们可以检查该数字是否在正确的范围内(列表中的脚本编号之一)。如果正确,脚本将使用['python3', 'scriptname.py']和之前的call()函数调用。如果输入不是数字(例如,x),它将引发ValueError异常。在ValueError异常中,我们可以检查是否按下了x并通过将running设置为False退出while循环(否则,循环将重新打印提示并等待新输入)。

脚本现在已完成。

小贴士

如果需要,您可以调整前面的脚本以支持其他类型的脚本。只需将其他文件扩展名,如.sh,添加到脚本列表中,并使用shbash而不是python3来调用。

更多内容…

我们可以通过将所有有用的脚本放在一个地方并将menu脚本添加到路径中来进一步扩展这个例子。

替代脚本位置

虽然这并非完全必要(默认情况下,脚本将在当前目录中查找),但创建一个合适的位置来保存您希望与菜单一起使用的脚本将很有用。这可以是在您的home文件夹中的一个位置(~home文件夹路径的缩写,默认情况下是/home/pi)。以下是一个示例命令行:

mkdir ~/menupy
cd ~/menupy

小贴士

要复制文件,你可以使用 cp sourcefile targetfile 命令。如果你使用 -r 选项,如果目录不存在,它也会创建该目录。要移动或重命名文件,请使用 mv sourcefile targetfile 命令。要删除文件,请使用 rm targetfile 命令。你必须使用 -r 选项来删除目录。

确保如果脚本不在同一位置,更新 SCRIPT_DIR 的路径以指向所需的位置。

将脚本添加到 PATH

如前所述,我们可以将此脚本添加到启动文件,例如 .bash_profile,以便用户登录到 Raspberry Pi 时出现菜单。或者,我们可以将这些脚本放入如 /home/pi/bin 这样的文件夹中,我们可以在其中包含全局值调用 PATHPATH 设置是一系列目录,当脚本和程序尝试定位不在当前目录中的文件时(通常是已安装的程序和软件,但也包括常见的配置文件和脚本)会检查这些目录。

这将允许我们无论当前在哪个目录下都能运行脚本。

我们可以使用以下命令查看当前的 PATH 设置:

echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/games:/usr/games

PATH 设置的实际内容将取决于你使用的发行版的初始设置,以及你安装的应用程序。

如果 /home/pi/bin 没有包含在内,我们可以使用以下命令临时添加,直到下一次启动:

PATH=/home/pi/bin:$PATH

我们也可以将其添加到 .bash_profile 中,为当前用户每次设置,如下所示:

PATH=$HOME/bin:$PATH
export PATH

下次我们重启时,PATH 设置将如下所示(对于名为 pi 的用户):

/home/pi/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/games:/usr/games

注意

当项目通过 PATH 自动定位时,可能很难找到特定版本的一个文件或程序。为了克服这个问题,在文件名/命令前使用 whereis,它将列出所有可以找到的位置。

最后,如果你确实将脚本移动到 bin 目录,请确保更新 os.listdir("//home/pi/bin") 中的路径,以便定位和列出你希望在菜单中显示的脚本。

第三章. 使用 Python 进行自动化和生产力

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

  • 使用 Tkinter 创建图形用户界面

  • 创建图形化启动菜单应用程序

  • 在应用程序中显示照片信息

  • 自动组织您的照片

简介

到目前为止,我们主要关注的是命令行应用程序;然而,树莓派的功能远不止命令行。通过使用 图形用户界面GUIs),通常更容易从用户那里获取输入并以更自然的方式提供反馈。毕竟,我们不断地处理多个输入和输出,所以为什么我们非得限制自己使用命令行的过程格式,而不必这样做呢?

幸运的是,Python 可以支持这一点。与其他编程语言类似,如 Visual Basic 和 C/C++/C#,这可以通过提供标准控件的前构建对象来实现。我们将使用一个名为 Tkinter 的模块,它提供了一系列控件(也称为 小部件)和工具,用于创建图形应用程序。

首先,我们将使用之前在 Working with text and strings 章节中 How to do it… 小节讨论的 encryptdecrypt.py 模块,并在 第二章 Starting with Python Strings, Files, and Menus 中展示如何编写和以多种方式重用有用的模块。这是一个良好的编码实践的测试。我们应该努力编写可以彻底测试并在许多地方重用的代码。

接下来,我们将通过创建一个小的图形化启动菜单应用程序来扩展我们之前的示例,以便从该应用程序运行我们的最爱应用。

然后,我们将探讨在应用程序中使用 来显示和组织照片。

使用 Tkinter 创建图形用户界面

我们将创建一个小的图形用户界面(GUI),允许用户输入信息,然后程序可以用来加密和解密这些信息。

准备工作

您需要确保您已经完成了 第二章 Starting with Python Strings, Files, and Menus 章节中 There's more… 小节 Working with text and strings 的说明,其中我们创建了可重用的 encryptdecrypt.py 模块。您必须确保此文件放置在与以下脚本相同的目录中。

注意

由于我们正在使用 Tkinter(Python 可用的许多附加组件之一),我们需要确保它已安装。它应该默认安装在标准的 Raspbian 映像中。我们可以通过从 Python 提示符导入它来确认它已安装,如下所示:

python3
>>> import tkinter

如果它尚未安装,将会引发一个 ImportError 异常,在这种情况下,您可以使用以下命令进行安装(使用 Ctrl + Z 退出 Python 提示符):

sudo apt-get install python3-tk

如果模块已加载,您可以使用以下命令了解更多关于模块的信息(阅读完毕后使用Q退出):

>>>help(tkinter)

您还可以使用以下命令获取模块中所有类、函数和方法的信息:

>>>help(tkinter.Button)

以下dir命令将列出模块作用域内的任何有效命令或变量:

>>>dir(tkinter.Button)

您会看到我们自己的模块将包含由三引号标记的函数信息;如果我们使用help命令,这些信息将会显示。

命令行无法显示本章中创建的图形显示,因此您必须启动 Raspberry Pi 桌面(使用命令startx),或者如果您正在远程使用,请确保您已启用X11 转发并且有一个X 服务器正在运行(见第一章,使用 Raspberry Pi 计算机入门)。

如何操作…

我们将使用tkinter模块为我们在上一章中编写的encryptdecrypt.py脚本生成 GUI。

要生成 GUI,我们将创建以下tkencryptdecrypt.py脚本:

#!/usr/bin/python3
#tkencryptdecrypt.py
import encryptdecrypt as ENC
import tkinter as TK

def encryptButton():
    encryptvalue.set(ENC.encryptText(encryptvalue.get(),
                                     keyvalue.get()))

def decryptButton():
    encryptvalue.set(ENC.encryptText(encryptvalue.get(),
                                     -keyvalue.get()))
#Define Tkinter application
root=TK.Tk()
root.title("Encrypt/Decrypt GUI")
#Set control & test value
encryptvalue = TK.StringVar()
encryptvalue.set("My Message") 
keyvalue = TK.IntVar()
keyvalue.set(20)
prompt="Enter message to encrypt:"
key="Key:"

label1=TK.Label(root,text=prompt,width=len(prompt),bg='green')
textEnter=TK.Entry(root,textvariable=encryptvalue,
                   width=len(prompt))
encryptButton=TK.Button(root,text="Encrypt",command=encryptButton)
decryptButton=TK.Button(root,text="Decrypt",command=decryptButton)
label2=TK.Label(root,text=key,width=len(key))
keyEnter=TK.Entry(root,textvariable=keyvalue,width=8)
#Set layout
label1.grid(row=0,columnspan=2,sticky=TK.E+TK.W)
textEnter.grid(row=1,columnspan=2,sticky=TK.E+TK.W)
encryptButton.grid(row=2,column=0,sticky=TK.E)
decryptButton.grid(row=2,column=1,sticky=TK.W)
label2.grid(row=3,column=0,sticky=TK.E)
keyEnter.grid(row=3,column=1,sticky=TK.W)

TK.mainloop()
#End

使用以下命令运行脚本:

python3 tkencryptdecrypt

工作原理…

我们首先导入两个模块;第一个是我们的encryptdecrypt模块,第二个是tkinter模块。为了更容易地看到哪些项目来自哪里,我们使用ENC/TK。如果您想避免额外的引用,可以使用from <module_name> import *直接引用模块项目。

当我们点击加密解密按钮时,将调用encryptButton()decryptButton()函数;它们将在以下部分中解释。

主 Tkinter 窗口是通过使用Tk()命令创建的,它返回可以放置所有小部件/控件的主窗口。

我们将定义以下六个控件:

  • 标签: 这会显示提示输入要加密的消息

  • 输入框: 这提供了一个文本框,用于接收用户要加密的消息

  • 按钮: 这是一个加密按钮,用于触发消息加密

  • 按钮: 这是一个解密按钮,用于反转加密

  • 标签: 这会显示密钥字段,提示用户输入加密密钥值

  • 输入框: 这提供了一个第二个文本框,用于接收加密密钥的值

这些控件将生成一个与以下屏幕截图相似的 GUI:

工作原理…

加密/解密消息的 GUI

让我们来看看第一个label1定义:

label1=TK.Label(root,text=prompt,width=len(prompt),bg='green')

所有控件都必须链接到应用程序窗口;因此,我们必须指定我们的 Tkinter 窗口root。用于标签的文本由text设置;在这种情况下,我们将其设置为名为prompt的字符串,该字符串之前已定义,包含我们所需的文本。我们还设置了width以匹配消息的字符数(虽然不是必需的,但如果我们以后添加更多文本到标签,它将提供更整洁的结果),最后,我们使用bg='green'设置了背景颜色。

接下来,我们定义用于消息的文本Entry框:

textEnter=TK.Entry(root,textvariable=encryptvalue,
                   width=len(prompt))

我们将定义textvariable——这是一种将变量链接到框内容的实用方法,它是一个特殊的字符串变量。我们可以直接使用textEnter.get()访问text,但我们将使用Tkinter StringVar()对象间接访问它。如果需要,这将允许我们将我们正在处理的数据与处理 GUI 布局的代码分开。enycrptvalue变量在每次使用.set()命令时自动更新它所链接的Entry小部件(并且.get()命令从Entry小部件获取最新值)。

接下来,我们有我们的两个Button小部件,EncryptDecrypt,如下所示:

encryptButton=TK.Button(root,text="Encrypt",command=encryptButton)
decryptButton=TK.Button(root,text="Decrypt",command=decryptButton)

在这种情况下,我们可以通过设置command属性来设置当Button小部件被点击时调用的函数。我们可以定义当每个按钮被点击时将调用的两个函数。在下面的代码片段中,我们有encryptButton()函数,该函数将设置控制第一个Entry框内容的encryptvalue StringVar。该字符串被设置为通过调用ENC.encryptText()并传递我们想要加密的消息(encryptvalue的当前值)和keyvalue变量所得到的结果。decrypt()函数与它完全相同,除了我们将keyvalue变量设置为负值以解密消息:

def encryptButton():
    encryptvalue.set(ENC.encryptText(encryptvalue.get(),
                                     keyvalue.get()))

我们然后以类似的方式设置了最终的LabelEntry小部件。请注意,如果需要,textvariable也可以是一个整数(数值),但没有内置的检查来确保只能输入数字。当使用.get()命令时,您将得到一个ValueError异常。

在我们定义了 Tkinter 窗口中要使用的所有小部件之后,我们必须设置布局。在 Tkinter 中有三种定义布局的方式:place、pack 和 grid。

place布局允许我们使用精确的像素位置指定位置和大小。pack布局按照它们被添加的顺序将项目放置在窗口中。grid布局允许我们将项目放置在特定的布局中。建议尽可能避免使用place布局,因为任何一个小部件的微小变化都可能对其他所有小部件的位置和大小产生连锁反应;其他布局通过确定它们相对于窗口中其他项目的位置来解决这个问题。

我们将按照以下截图所示放置项目:

工作原理…

加密/解密 GUI 的网格布局

使用以下代码设置 GUI 中前两个项目的位置:

label1.grid(row=0,columnspan=2,sticky= TK.E+TK.W)
textEnter.grid(row=1,columnspan=2,sticky= TK.E+TK.W)

我们可以指定第一个 LabelEntry 框将跨越两个列(columnspan=2),并且我们可以设置粘性值以确保它们跨越到边缘。这是通过设置东边 TK.E 和西边 TK.W 来实现的。如果我们需要垂直地做同样的事情,我们将使用 TK.N 对于北边和 TK.S 对于南边。如果未指定 column 值,则网格函数默认为 column=0。其他项目以类似方式定义。

最后一步是调用 TK.mainloop(),这允许 Tkinter 运行;这允许监控按钮的点击,并调用与它们链接的函数。

创建图形应用程序 – 开始菜单

本食谱中的示例展示了我们如何定义自己的 Tkinter 对象变体来生成自定义控件,并使用它们动态构建菜单。我们还将快速查看如何使用线程,以便在执行特定任务时允许其他任务继续运行。

准备工作

要查看 GUI 显示,您需要一个显示 Raspberry Pi 桌面的监视器,或者需要连接到运行 X 服务器的另一台计算机。

如何操作…

要创建图形开始菜单应用程序,创建以下 graphicmenu.py 脚本:

#!/usr/bin/python3
# graphicmenu.py
import tkinter as tk
from subprocess import call
import threading

#Define applications ["Display name","command"]
leafpad = ["Leafpad","leafpad"]
scratch = ["Scratch","scratch"]
pistore = ["Pi Store","pistore"]
app_list = [leafpad,scratch,pistore]
APP_NAME = 0
APP_CMD  = 1

class runApplictionThread(threading.Thread):
    def __init__(self,app_cmd):
        threading.Thread.__init__(self)
        self.cmd = app_cmd
    def run(self):
        #Run the command, if valid
        try:
            call(self.cmd)
        except:
            print ("Unable to run: %s" % self.cmd)

class appButtons:
    def __init__(self,gui,app_index):
        #Add the buttons to window
        btn = tk.Button(gui, text=app_list[app_index][APP_NAME],
                        width=30, command=self.startApp)
        btn.pack()
        self.app_cmd=app_list[app_index][APP_CMD]
    def startApp(self):
        print ("APP_CMD: %s" % self.app_cmd)
        runApplictionThread(self.app_cmd).start()       

root = tk.Tk()
root.title("App Menu")
prompt = '      Select an application      '
label1 = tk.Label(root, text=prompt, width=len(prompt), bg='green')
label1.pack()
#Create menu buttons from app_list
for index, app in enumerate(app_list):
    appButtons(root,index)
#Run the tk window
root.mainloop()
#End

之前的代码生成以下应用程序:

如何操作…

应用程序菜单 GUI

工作原理…

我们创建 Tkinter 窗口的方式与之前相同;然而,我们不是单独定义所有项目,而是为应用程序按钮创建一个特殊类。

我们创建的 作为 appButtons 项目所需内容的蓝图或规范。每个项目将包括一个用于 app_cmd 的字符串值,一个名为 startApp() 的函数,以及一个 __init__() 函数。__init__() 函数是一个特殊函数(称为 构造函数),在创建 appButtons 项目时被调用;它将允许我们创建所需的任何设置。

在这种情况下,__init__() 函数允许我们创建一个新的 Tkinter 按钮,按钮上的文本设置为 app_list 中的项目,当按钮被点击时,将调用 startApp() 函数中的命令。使用 self 关键字,所调用的命令将是项目的一部分;这意味着每个按钮都将调用一个本地定义的函数,该函数可以访问项目的本地数据。

我们将 self.app_cmd 的值设置为 app_list 中的命令,并使其准备好由 startApp() 函数使用。现在我们创建 startApp() 函数。如果我们在这里直接运行应用程序命令,Tkinter 窗口将冻结,直到我们打开的应用程序再次关闭。为了避免这种情况,我们可以使用 Python 的 Threading 模块,它允许我们同时执行多个操作。

runApplicationThread()类是通过使用threading.Thread类作为模板创建的——这在新类中继承了threading.Thread类的所有功能。就像我们之前的类一样,我们也为这个类提供了一个__init__()函数。我们首先调用继承类的__init__()函数以确保其正确设置,然后我们将app_cmd值存储在self.cmd中。在runApplicationThread()函数创建并初始化后,调用start()函数。这个函数是threading.Thread的一部分,我们的类可以使用它。当调用start()函数时,它将创建一个单独的应用程序线程(即模拟同时运行两件事),允许 Tkinter 在执行类内的run()函数的同时继续监控按钮点击。

因此,我们可以将代码放置在run()函数中以运行所需的应用程序(使用call(self.cmd))。

还有更多...

使 Python 特别强大的一点是它支持面向对象设计OOD)中使用的编程技术。现代编程语言通常使用它来帮助我们将程序要执行的任务转化为代码中的有意义的构造和结构。OOD 的原则在于,我们认为大多数问题都由几个对象(一个 GUI 窗口、一个按钮等)组成,这些对象相互作用以产生期望的结果。

在上一节中,我们发现我们可以使用类来创建可重复使用的标准化对象。我们创建了一个appButton类,它生成一个具有所有类特征的对象,包括它自己的app_cmd版本,该版本将由startApp()函数使用。另一个appButton类型的对象将有自己的无关的[app_cmd]数据,其startApp()函数将使用。

你可以看到,类有助于将相关变量和函数集合在一个对象中,并且类将在一个地方保存其自己的数据。拥有多个相同类型的对象(类),每个对象内部都有它们自己的函数和数据,这会导致更好的程序结构。传统的方法是将所有信息放在一个地方,并将每个项目来回发送给各种函数处理;然而,在大型系统中这可能会变得繁琐。

以下图表显示了相关函数和数据的组织结构:

还有更多…

相关函数和数据可以组织到类和对象中

到目前为止,我们已经使用 Python 模块将程序的各个部分分离到不同的文件中;这使得我们可以在概念上分离程序的不同部分(一个接口、编码器/解码器,或者类库,例如 Tkinter)。模块可以提供控制特定硬件的代码,定义互联网的接口,或者提供常用功能的库;然而,它最重要的功能是控制接口(当导入项目时可用的一组函数、变量和类)。一个实现良好的模块应该有一个清晰的接口,这个接口围绕其使用方式而不是实现方式。这允许你创建多个模块,它们可以轻松地互换和更改,因为它们共享相同的接口。在我们的上一个例子中,想象一下,仅通过支持 encryptText(input_text,key),如何轻松地更换 encryptdecrypt 模块为另一个模块。复杂的功能可以被拆分成更小、更易于管理的块,这些块可以在多个应用程序中重复使用。

Python 一直使用类和模块。每次你导入一个库,如 sys 或 Tkinter,或者使用 value.str() 转换值,或者使用 for...in 遍历列表时,你都可以使用它们而不用担心细节。你不需要在编写的每一行代码中使用类或模块,但它们是程序员工具箱中非常有用的工具,在它们适合你所做的事情时可以使用。

我们将通过在本书的示例中使用类和模块,来理解它们如何帮助我们产生结构良好的代码,这些代码更容易测试和维护。

在应用程序中显示照片信息

在这个例子中,我们将创建一个实用类来处理照片,这个类可以被其他应用程序(作为一个模块)用来轻松访问照片元数据和显示预览图像。

准备工作

以下脚本使用了 Python 图像库PIL);Python 3 的兼容版本是 Pillow

Pillow 没有包含在 Raspbian 存储库中(由 apt-get 使用);因此,我们需要使用名为 PIPPython 包管理器来安装 Pillow。

要为 Python 3 安装包,我们将使用 Python 3 版本的 PIP(这需要 50 MB 的可用空间)。

以下命令可以用来安装 PIP:

sudo apt-get update
sudo apt-get install python3-pip

在使用 PIP 之前,请确保你已经安装了 libjpeg-dev,以便 Pillow 能够处理 JPEG 文件。你可以使用以下命令来完成此操作:

sudo apt-get install libjpeg-dev

现在,你可以使用以下 PIP 命令来安装 Pillow:

sudo pip-3.2 install pillow

PIP 还使得使用 uninstall 而不是 install 来卸载包变得容易。

最后,你可以通过运行 python3 来确认它已经成功安装:

>>>import PIL
>>>help(PIL)

你不应该遇到任何错误,并且会看到很多关于 PIL 及其用途的信息(按 Q 键完成)。如下检查安装的版本:

>>PIL.PILLOW_VERSION

你应该看到 2.7.0(或类似版本)。

注意

可以通过使用以下命令安装 pip-2.x 来使用 PIP 与 Python 2 一起使用:

sudo apt-get install python-pip

使用 sudo pip install 安装的任何包都仅适用于 Python 2。

如何做到这一点...

要在应用程序中显示照片信息,创建以下 photohandler.py 脚本:

##!/usr/bin/python3
#photohandler.py
from PIL import Image
from PIL import ExifTags
import datetime
import os

#set module values
previewsize=240,240
defaultimagepreview="./preview.ppm"
filedate_to_use="Exif DateTime"
#Define expected inputs
ARG_IMAGEFILE=1
ARG_LENGTH=2

class Photo:
    def __init__(self,filename):
        """Class constructor"""
        self.filename=filename
        self.filevalid=False
        self.exifvalid=False
        img=self.initImage()
        if self.filevalid==True:
            self.initExif(img)
            self.initDates()

    def initImage(self):
        """opens the image and confirms if valid, returns Image"""
        try:
            img=Image.open(self.filename)
            self.filevalid=True
        except IOError:
            print ("Target image not found/valid %s" %
                   (self.filename))
            img=None
            self.filevalid=False
        return img

    def initExif(self,image):
        """gets any Exif data from the photo"""
        try:
            self.exif_info={
                ExifTags.TAGS[x]:y
                for x,y in image._getexif().items()
                if x in ExifTags.TAGS
            }
            self.exifvalid=True
        except AttributeError:
            print ("Image has no Exif Tags")
            self.exifvalid=False

    def initDates(self):
        """determines the date the photo was taken"""
        #Gather all the times available into YYYY-MM-DD format
        self.filedates={}
        if self.exifvalid:
            #Get the date info from Exif info
            exif_ids=["DateTime","DateTimeOriginal",
                      "DateTimeDigitized"]
            for id in exif_ids:
                dateraw=self.exif_info[id]
                self.filedates["Exif "+id]=
                                dateraw[:10].replace(":","-")
        modtimeraw = os.path.getmtime(self.filename)
        self.filedates["File ModTime"]="%s" %
            datetime.datetime.fromtimestamp(modtimeraw).date()
        createtimeraw = os.path.getctime(self.filename)
        self.filedates["File CreateTime"]="%s" %
            datetime.datetime.fromtimestamp(createtimeraw).date()

    def getDate(self):
        """returns the date the image was taken"""
        try:
            date = self.filedates[filedate_to_use]
        except KeyError:
            print ("Exif Date not found")
            date = self.filedates["File ModTime"]
        return date

    def previewPhoto(self):
        """creates a thumbnail image suitable for tk to display"""
        imageview=self.initImage()
        imageview=imageview.convert('RGB')
        imageview.thumbnail(previewsize,Image.ANTIALIAS)
        imageview.save(defaultimagepreview,format='ppm')
        return defaultimagepreview        

上述代码定义了我们的 Photo 类;在我们将其在 还有更多... 部分和下一个示例中运行之前,对我们来说它没有用处。

它是如何工作的...

我们定义了一个通用的类 Photo;它包含有关自身的详细信息,并提供访问 可交换图像文件格式EXIF)信息和生成预览图像的功能。

__init__() 函数中,我们为我们的类变量设置值并调用 self.initImage(),这将使用 PIL 的 Image() 函数打开图像。然后我们调用 self.initExif()self.initDates() 并设置一个标志来指示文件是否有效。如果无效,Image() 函数将引发 IOError 异常。

initExif() 函数使用 PIL 从 img 对象中读取 EXIF 数据,如下面的代码片段所示:

self.exif_info={
                ExifTags.TAGS[id]:y
                for id,y in image._getexif().items()
                if id in ExifTags.TAGS
               }

上述代码是一系列复合语句,其结果是 self.exif_info 被填充为一个包含标签名称及其相关值的字典。

ExifTag.TAGS 是一个包含可能标签名称列表的字典,这些名称与它们的 ID 相关联,如下面的代码片段所示:

ExifTag.TAGS={
4096: 'RelatedImageFileFormat',
513: 'JpegIFOffset',
514: 'JpegIFByteCount',
40963: 'ExifImageHeight',
…etc…}

image._getexif() 函数返回一个包含所有由相机设置的值的字典,每个值都与相关的 ID 相关联,如下面的代码片段所示:

Image._getexif()={
256: 3264,
257: 2448,
37378: (281, 100),
36867: '2016:09:28 22:38:08',
…etc…}

for 循环将遍历图像 EXIF 值字典中的每个项目,并检查其在 ExifTags.TAGS 字典中的出现;结果将存储在 self.exif_info 中。此代码如下:

self.exif_info={
'YResolution': (72, 1),
 'ResolutionUnit': 2,
 'ExposureMode': 0, 
'Flash': 24,
…etc…}

再次,如果没有异常,我们设置一个标志来指示 EXIF 数据有效,或者如果没有 EXIF 数据,我们引发 AttributeError 异常。

initDates() 函数使我们能够收集所有可能的文件日期和 EXIF 数据中的日期,以便我们可以选择其中之一作为我们希望用于文件的日期。例如,它允许我们将所有图像重命名为标准日期格式的文件名。我们创建一个 self.filedates 字典,并用从 EXIF 信息中提取的三个日期填充它。然后,为了以防没有 EXIF 数据可用,我们添加文件系统日期(创建和修改)。os 模块允许我们使用 os.path.getctime()os.path.getmtime() 获取文件的纪元值——它也可以是文件被移动的日期和时间——以及文件修改——最后一次写入时(例如,它通常指的是拍照的日期)。纪元值是自 1970 年 1 月 1 日以来的秒数,但我们可以使用 datetime.datetime.fromtimestamp() 将其转换为年、月、日、小时和秒。添加 date() 仅将其限制为年、月和日。

现在,如果 Photo 类要被另一个模块使用,并且我们希望知道拍摄图像的日期,我们可以查看 self.dates 字典并选择一个合适的日期。然而,这要求程序员知道 self.dates 值是如何排列的,如果我们后来更改了它们的存储方式,这会破坏他们的程序。因此,建议我们通过访问函数来访问类中的数据,这样实现就独立于接口(这个过程被称为封装)。我们提供了一个在调用时返回日期的函数;程序员不需要知道它可能是五个可用日期之一,甚至不需要知道它们是以纪元值存储的。使用函数,我们可以确保无论数据如何存储或收集,接口都将保持不变。

最后,我们希望 Photo 类提供的最后一个函数是 previewPhoto()。这个函数提供了一个生成小缩略图并将其保存为便携式像素格式PPM)文件的方法。正如我们很快就会发现的,Tkinter 允许我们在其 Canvas 小部件上放置图像,但遗憾的是,它不支持 JPG(只支持 GIF 或 PPM)。因此,我们只需将我们想要显示的图像的小副本保存为 PPM 格式——附带一个额外的注意事项,即图像调色板也必须转换为 RGB——然后当需要时让 Tkinter 将其加载到 Canvas 上。

总结来说,我们创建的 Photo 类如下:

操作 描述
__init__(self,filename) 这是对象初始化函数
initImage(self) 这返回 img,一个 PIL 类型的图像对象
initExif(self,image) 这将提取所有存在的 EXIF 信息
initDates(self) 这将创建一个包含文件和照片信息中所有可用日期的字典
getDate(self) 这返回照片拍摄/创建时的日期字符串
previewPhoto(self) 这返回预览缩略图的文件名字符串

属性及其相应描述如下:

属性 描述
self.filename 照片的文件名
self.filevalid 如果文件成功打开,则设置为 True
self.exifvalid 如果照片包含 EXIF 信息,则设置为 True
self.exif_info 这包含来自照片的 EXIF 信息
self.filedates 这包含来自文件和照片信息的可用日期的字典

为了测试新类,我们将创建一些测试代码来确认一切是否按预期工作;请参阅以下部分。

还有更多...

我们之前创建了 Photo 类。现在我们可以在模块中添加一些测试代码,以确保它按预期工作。我们可以像之前一样使用 __name__ ="__main__" 属性来检测模块是否被直接运行。

我们可以在 photohandler.py 脚本的末尾添加后续代码部分,以生成以下测试应用程序,其外观如下:

还有更多…

照片查看演示应用程序

photohandler.py 的末尾添加以下代码:

#Module test code
def dispPreview(aPhoto):
    """Create a test GUI"""
    import tkinter as TK

    #Define the app window
    app = TK.Tk()
    app.title("Photo View Demo")

    #Define TK objects
    # create an empty canvas object the same size as the image
    canvas = TK.Canvas(app, width=previewsize[0],
                       height=previewsize[1])
    canvas.grid(row=0,rowspan=2)
    # Add list box to display the photo data
    #(including xyscroll bars)
    photoInfo=TK.Variable()
    lbPhotoInfo=TK.Listbox(app,listvariable=photoInfo,
                           height=18,width=45,
                           font=("monospace",10))
    yscroll=TK.Scrollbar(command=lbPhotoInfo.yview,
                         orient=TK.VERTICAL)
    xscroll=TK.Scrollbar(command=lbPhotoInfo.xview,
                         orient=TK.HORIZONTAL)
    lbPhotoInfo.configure(xscrollcommand=xscroll.set,
                          yscrollcommand=yscroll.set)
    lbPhotoInfo.grid(row=0,column=1,sticky=TK.N+TK.S)
    yscroll.grid(row=0,column=2,sticky=TK.N+TK.S)
    xscroll.grid(row=1,column=1,sticky=TK.N+TK.E+TK.W)

    # Generate the preview image
    preview_filename = aPhoto.previewPhoto()
    photoImg = TK.PhotoImage(file=preview_filename)
    # anchor image to NW corner
    canvas.create_image(0,0, anchor=TK.NW, image=photoImg) 

    # Populate infoList with dates and exif data
    infoList=[]
    for key,value in aPhoto.filedates.items():
        infoList.append(key.ljust(25) + value)
    if aPhoto.exifvalid:
        for key,value in aPhoto.exif_info.items():
           infoList.append(key.ljust(25) + str(value))
    # Set listvariable with the infoList
    photoInfo.set(tuple(infoList))

    app.mainloop()

def main():
    """called only when run directly, allowing module testing"""
    import sys
    #Check the arguments
    if len(sys.argv) == ARG_LENGTH:
        print ("Command: %s" %(sys.argv))
        #Create an instance of the Photo class
        viewPhoto = Photo(sys.argv[ARG_IMAGEFILE])
        #Test the module by running a GUI
        if viewPhoto.filevalid==True:
            dispPreview(viewPhoto)
    else:
        print ("Usage: photohandler.py imagefile")

if __name__=='__main__':
  main()
#End

之前的测试代码将运行 main() 函数,该函数接受要使用的照片文件名并创建一个名为 viewPhoto 的新 Photo 对象。如果 viewPhoto 打开成功,我们将调用 dispPreview() 来显示图像及其详细信息。

dispPreview() 函数创建四个 Tkinter 小部件以供显示:一个用于加载缩略图的 Canvas,一个用于显示照片信息的 Listbox 小部件,以及两个用于控制 Listbox 的滚动条。首先,我们创建一个大小与缩略图相同的 Canvas 小部件(previewsize)。

接下来,我们创建 photoInfo,它将成为我们的 listvariable 参数,并将其链接到 Listbox 小部件。由于 Tkinter 不提供 ListVar() 函数来创建合适的项,我们使用通用的 TK.Variable() 类型,并在设置值之前确保将其转换为元组类型。我们将 Listbox 小部件添加进去;我们需要确保 listvariable 参数设置为 photoInfo,并且设置字体为 monospace。这将允许我们使用空格对齐数据值,因为 monospace 是固定宽度字体,所以每个字符占据的宽度与其他任何字符相同。

我们通过设置垂直和水平滚动条的 Scrollbar 命令参数为 lbPhotoInfo.yviewlbPhotoInfo.xview 来定义两个滚动条,并将它们链接到 Listbox 小部件。然后,我们使用以下命令调整 Listbox 的参数:

 lbPhotoInfo.configure(xscrollcommand=xscroll.set, 
 yscrollcommand=yscroll.set)

configure命令允许我们在创建小部件后添加或更改其参数,在这种情况下,将两个滚动条链接起来,以便Listbox小部件也可以在用户在列表中滚动时控制它们。

和以前一样,我们使用网格布局来确保Listbox小部件旁边正确放置两个滚动条,而Canvas小部件位于Listbox小部件的左侧。

现在我们使用Photo对象来创建preview.ppm缩略图文件(使用aPhoto.previewPhoto()函数),并创建一个TK.PhotoImage对象,然后可以使用以下命令将其添加到Canvas小部件中:

canvas.create_image(0,0, anchor=TK.NW, image=photoImg)

最后,我们使用Photo类收集的日期信息和 EXIF 信息(确保它有效)来填充Listbox小部件。我们通过将每个项目转换为使用.ljust(25)分隔的字符串列表来实现这一点——它为名称添加左对齐并填充到 25 个字符宽。一旦我们有了列表,我们就将其转换为元组类型并设置listvariablephotoInfo)参数。

和往常一样,我们调用app.mainloop()来开始对事件的监控以进行响应。

自动整理您的照片

现在我们有一个可以让我们收集照片信息的类,我们可以将这些信息应用于执行有用的任务。在这种情况下,我们将使用文件信息来自动将一个装满照片的文件夹组织成基于拍摄日期的子文件夹集合。以下截图显示了脚本的输出:

自动整理您的照片

应用程序将使用照片信息按拍摄日期将图片排序到文件夹中

准备工作

您需要在 Raspberry Pi 上的一个文件夹中放置一组照片。或者,您可以将带有照片的 USB 闪存盘或卡读卡器插入——它们将位于/mnt/。但是,请确保您首先使用照片的副本测试脚本,以防万一有任何问题。

如何操作…

filehandler.py文件中创建以下脚本来自动整理您的照片:

#!/usr/bin/python3
#filehandler.py
import os
import shutil
import photohandler as PH
from operator import itemgetter

FOLDERSONLY=True
DEBUG=True
defaultpath=""
NAME=0
DATE=1

class FileList:
  def __init__(self,folder):
    """Class constructor"""
    self.folder=folder
    self.listFileDates()

  def getPhotoNamedates(self):
    """returns the list of filenames and dates"""
    return self.photo_namedates

  def listFileDates(self):
    """Generate list of filenames and dates"""
    self.photo_namedates = list()
    if os.path.isdir(self.folder):
      for filename in os.listdir(self.folder):
        if filename.lower().endswith(".jpg"):
          aPhoto = PH.Photo(os.path.join(self.folder,filename))
          if aPhoto.filevalid:
            if (DEBUG):print("NameDate: %s %s"%
                             (filename,aPhoto.getDate()))
            self.photo_namedates.append((filename,
                                         aPhoto.getDate()))
            self.photo_namedates = sorted(self.photo_namedates,
                                    key=lambda date: date[DATE])

  def genFolders(self):
    """function to generate folders"""
    for i,namedate in enumerate(self.getPhotoNamedates()):
      #Remove the - from the date format
      new_folder=namedate[DATE].replace("-","")
      newpath = os.path.join(self.folder,new_folder)
      #If path does not exist create folder
      if not os.path.exists(newpath):
        if (DEBUG):print ("New Path: %s" % newpath)
        os.makedirs(newpath)
      if (DEBUG):print ("Found file: %s move to %s" %
                        (namedate[NAME],newpath))
      src_file = os.path.join(self.folder,namedate[NAME])
      dst_file = os.path.join(newpath,namedate[NAME])
      try:
        if (DEBUG):print ("File moved %s to %s" %
                          (src_file, dst_file))
        if (FOLDERSONLY==False):shutil.move(src_file, dst_file)
      except IOError:
        print ("Skipped: File not found")

def main():
  """called only when run directly, allowing module testing"""
  import tkinter as TK
  from tkinter import filedialog
  app = TK.Tk()
  app.withdraw()
  dirname = TK.filedialog.askdirectory(parent=app,
      initialdir=defaultpath,
      title='Select your pictures folder')
  if dirname != "":
    ourFileList=FileList(dirname)
    ourFileList.genFolders()

if __name__=="__main__":
  main()
#End

工作原理…

我们将创建一个名为FileList的类;它将使用Photo类来管理特定文件夹内的照片。为此有两个主要步骤:我们首先需要找到文件夹内的所有图像,然后生成一个包含文件名和照片日期的列表。我们将使用这些信息来生成新的子文件夹并将照片移动到这些文件夹中。

当我们创建FileList对象时,我们将使用listFileDates()创建列表。然后我们将确认提供的文件夹是有效的,并使用os.listdir来获取目录中的文件完整列表。我们将检查每个文件是否是.jpg文件,并获取每张照片的日期(使用Photo类中定义的函数)。接下来,我们将文件名和日期作为一个元组添加到self.photo_namedates列表中。

最后,我们将使用内置的sorted函数按日期顺序排列所有文件。虽然我们在这里不需要这样做,但这个函数如果我们在其他地方使用此模块,将更容易删除重复的日期。

注意

sorted函数需要排序的列表,在这种情况下,我们希望按date值排序。

sorted(self.photo_namedates,key=lambda date: date[DATE])

我们将date[DATE]替换为lambda date:作为排序的值。

一旦初始化了FileList对象,我们就可以通过调用genFolders()来使用它。首先,我们将日期文本转换为适合我们文件夹的格式(YYYYMMDD),以便我们的文件夹可以按日期顺序排序。接下来,如果它们不存在,它将在当前目录中创建文件夹。最后,它将每个文件移动到所需的子文件夹中。

我们最终得到一个准备测试的FileList类:

操作 描述
__init__(self,folder) 这是对象初始化函数
getPhotoNamedates(self) 这个函数返回照片的文件名和日期列表
listFileDates(self) 这个函数创建文件夹中照片的文件名和日期列表
genFolders(self) 这个函数根据照片的日期创建新文件夹,并将文件移动到其中

属性如下列出:

属性 描述
self.folder 我们正在处理的文件夹
self.photo_namedates 这包含了一个文件名和日期列表

FileList类封装了所有函数和相关的数据,将所有内容保持在逻辑上的一致位置:

工作原理…

使用 Tkinter 的 filediaglog.askdirectory()选择照片目录

为了测试这个功能,我们使用 Tkinter 的filedialog.askdirectory()小部件来允许我们选择一个目标图片目录。我们使用app.withdrawn()来隐藏主 Tkinter 窗口,因为这次不需要它。我们只需要创建一个新的FileList对象,然后调用genFolders()来将所有我们的照片移动到新的位置!

注意

在这个脚本中定义了两个额外的标志,提供了额外的测试控制。DEBUG允许我们通过设置为TrueFalse来启用或禁用额外的调试消息。此外,当设置为True时,FOLDERSONLY只生成文件夹,不移动文件(这对于测试新子文件夹是否正确非常有用)。

一旦运行了脚本,你可以检查所有文件夹是否已正确创建。最后,将FOLDERSONLY改为True,下次程序将自动根据照片的日期移动和组织你的照片。建议你只在照片的副本上运行此操作,以防出现错误。

第四章:创建游戏和图形

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

  • 使用 IDLE3 调试你的程序

  • 使用鼠标在 Tkinter Canvas 上绘制线条

  • 创建一个棒球游戏

  • 创建一个滚动游戏

简介

游戏通常是一个探索和扩展编程技能的绝佳方式,因为它们提供了一个内在的激励因素来修改和改进你的创作,添加新功能,并创造新的挑战。它们也非常适合与他人分享你的想法,即使他们不感兴趣编程。

本章重点介绍使用 Tkinter Canvas 小部件在屏幕上创建和显示对象,以便用户与之交互。使用这些技术,可以创建各种游戏和应用,其限制仅取决于你自己的创造力。

我们还简要地看看如何使用内置在 IDLE 中的调试器,这是一个在无需编写大量测试代码的情况下测试和开发程序的有用工具。

第一个示例演示了我们可以如何监控和使用鼠标来创建对象并在 Canvas 小部件上直接绘制。接下来,我们创建一个棒球游戏,展示了如何控制对象的位姿以及如何检测和响应它们之间的交互。最后,我们更进一步,使用 Tkinter 将我们自己的图形放置在 Canvas 小部件上,以创建一个俯视图寻宝游戏。

使用 IDLE3 调试你的程序

编程的一个关键方面是能够测试和调试你的代码,而一个有用的工具就是调试器。IDLE 编辑器(确保你使用 IDLE3 以支持本书中使用的 Python 3 代码)包括一个基本的调试器。它允许你逐步执行你的代码,观察局部和全局变量的值,并设置断点。

如何做…

要启用调试器,请启动 IDLE3 并从 调试 菜单中选择 调试器;它将打开以下窗口(如果你当前正在运行某些代码,你需要先停止它):

如何做…

IDLE3 调试器窗口

打开你想要测试的代码(通过 文件 | 打开…),然后尝试运行它(F5)。你会发现代码不会启动,因为调试器已经自动在第一行停止。以下截图显示调试器已经停止在 filehandler.py 代码的第一行,即 line 3: import os

如何做…

代码开始时的 IDLE3 调试器

它是如何工作的…

以下截图显示的控制按钮允许你运行和/或跳过代码:

如何做…

调试器控制

控制按钮的功能如下:

  • 继续:此按钮将正常执行代码。

  • 步骤:此按钮将逐行执行代码,然后再次停止。如果调用了一个函数,它将进入该函数,并允许你逐步执行该函数。

  • 跳过:此按钮类似于步骤命令,但如果存在函数调用,它将执行整个函数,并在下一行停止。

  • 输出:此按钮将执行代码,直到它完成当前函数,继续执行直到你退出函数。

  • 退出:此按钮会立即结束程序。

除了之前提到的控制选项外,你可以在代码中直接设置断点清除断点。断点是一个可以在代码中插入的标记(通过在编辑器窗口中右键单击),当调试器到达该标记时,它将始终中断(停止)如以下截图所示:

如何工作…

在你的代码中直接设置和清除断点

复选框(在控制按钮的右侧)允许你在遍历代码或调试器由于断点而停止时选择要显示的信息。堆栈显示在主窗口中,类似于程序遇到未处理的异常时你会看到的情况。堆栈选项显示了到达代码当前位置所进行的所有函数调用,直到它停止的行。源代码选项突出显示当前正在执行的代码行,在某些情况下,还包括导入模块中的代码(如果它们是非编译库)。

你还可以选择是否显示局部和/或全局。默认情况下,源代码全局选项通常被禁用,因为如果有很多数据要显示,它们可能会使过程变得相当缓慢。

注意

Python 使用局部和全局变量的概念来定义作用域(变量在哪里以及何时有效)。全局变量在文件的顶层定义,并且在其定义之后代码的任何位置都可以看到。然而,为了从除了顶层以外的任何地方改变其值,Python 需要你首先使用 global 关键字。如果没有使用 global 关键字,你将创建一个具有相同名称的局部副本(当退出函数时,其值将丢失)。局部变量是在你在一个函数内创建变量时定义的;一旦超出函数范围,该变量就会被销毁,并且不再可见。

堆栈数据下方是局部变量,在这种情况下是 aPhotofilenameself。然后(如果启用),我们有所有当前有效的全局值,提供了关于程序状态的详细信息(DATE = 1DEBUG = TrueFOLDERSONLY = True 等):

如何工作…

调试器中的“堆栈”、“局部”和“全局”选项

调试器并不特别先进,因为它不允许你展开复杂对象,例如 photohandler.Photo 对象,以查看它包含的数据。然而,如果需要,你可以在测试期间调整你的代码,并将你想要观察的数据分配给一些临时变量。

学习如何使用调试器是值得的,因为它是一种更容易追踪特定问题并检查事物是否按预期工作的方式。

在 Tkinter 画布上使用鼠标绘制线条

Tkinter Canvas 小部件提供了一个区域来创建和绘制对象。以下脚本演示了如何使用鼠标事件与 Tkinter 交互。通过检测鼠标点击,我们可以使用 Tkinter 绘制一条跟随鼠标移动的线条:

在 Tkinter 画布上使用鼠标绘制线条

使用 Tkinter 的简单绘图应用程序

准备工作

如前所述,我们需要安装 Tkinter,并且要么运行 Raspbian 桌面(通过命令行执行startx),要么有一个带有 X11 转发和运行 X 服务器的 SSH 会话(参见第一章,使用 Raspberry Pi 计算机入门)。我们还需要连接一个鼠标。

如何做…

创建以下脚本,painting.py

#!/usr/bin/python3
#painting.py
import tkinter as TK

#Set defaults
btn1pressed = False
newline = True

def main():
  root = TK.Tk()
  the_canvas = TK.Canvas(root)
  the_canvas.pack()
  the_canvas.bind("<Motion>", mousemove)
  the_canvas.bind("<ButtonPress-1>", mouse1press)
  the_canvas.bind("<ButtonRelease-1>", mouse1release)
  root.mainloop()

def mouse1press(event):
  global btn1pressed
  btn1pressed = True

def mouse1release(event):
  global btn1pressed, newline
  btn1pressed = False
  newline = True

def mousemove(event):
  if btn1pressed == True:
    global xorig, yorig, newline
    if newline == False:
      event.widget.create_line(xorig,yorig,event.x,event.y,
                               smooth=TK.TRUE)
    newline = False
    xorig = event.x
    yorig = event.y

if __name__ == "__main__":
  main()
#End

它是如何工作的…

Python 代码创建了一个包含名为the_canvasCanvas对象的 Tkinter 窗口。在这里我们使用bind函数,它将特定事件(在这个小部件the_canvas上发生的事件)绑定到特定动作或按键。在这种情况下,我们将鼠标的<Motion>函数以及第一个鼠标按钮的点击和释放(<ButtonPress-1><ButtonRelease-1>)绑定。然后,这些事件被用来调用mouse1press()mouse1release()mousemove()函数。

这里的逻辑如下。我们使用mouse1press()mouse1release()函数跟踪鼠标按钮的状态。

如果鼠标已被点击,mousemove()函数将检查我们是否在绘制一条新线(我们为此设置了新坐标)或者继续一条旧线(我们从上一个坐标绘制一条线到触发mousemove()的当前事件的坐标)。我们只需要确保每次鼠标按钮释放时都重置到newline命令,以重置线的起始位置。

创建棒球游戏

可以使用画布的绘图工具和检测对象的碰撞来创建一个经典的棒球游戏。用户将能够通过使用左右光标键来控制绿色挡板,瞄准砖块并击打它们,直到所有砖块都被摧毁。

创建棒球游戏

控制挡板以瞄准砖块

准备工作

此示例需要图形输出,因此您必须将屏幕和键盘连接到 Raspberry Pi,或者如果从另一台计算机远程连接,则必须使用 X11 转发和 X 服务器。

如何做…

创建以下脚本,bouncingball.py

  1. 首先,导入tkintertime模块,并定义游戏图形的常量:

    #!/usr/bin/python3
    # bouncingball.py
    import tkinter as TK
    import time
    
    VERT,HOREZ=0,1
    xTOP,yTOP = 0,1
    xBTM,yBTM = 2,3
    MAX_WIDTH,MAX_HEIGHT = 640,480
    xSTART,ySTART = 100,200
    BALL_SIZE=20
    RUNNING=True
    
  2. 接下来,创建用于关闭程序、移动挡板左右和计算球的方向的函数:

    def close():
      global RUNNING
      RUNNING=False
      root.destroy()
    
    def move_right(event):
      if canv.coords(paddle)[xBTM]<(MAX_WIDTH-7):
        canv.move(paddle, 7, 0)
    
    def move_left(event):
      if canv.coords(paddle)[xTOP]>7:
        canv.move(paddle, -7, 0)
    
    def determineDir(ball,obj):
      global delta_x,delta_y
      if (ball[xTOP] == obj[xBTM]) or (ball[xBTM] == 
          obj[xTOP]):
        delta_x = -delta_x
      elif (ball[yTOP] == obj[yBTM]) or (ball[yBTM] == 
            obj[yTOP]):
        delta_y = -delta_y
    
  3. 设置 tkinter 窗口并定义画布:

    root = TK.Tk()
    root.title("Bouncing Ball")
    root.geometry('%sx%s+%s+%s' %(MAX_WIDTH, MAX_HEIGHT, 100, 100))
    root.bind('<Right>', move_right)
    root.bind('<Left>', move_left)
    root.protocol('WM_DELETE_WINDOW', close)
    
    canv = TK.Canvas(root, highlightthickness=0)
    canv.pack(fill='both', expand=True)
    
  4. 将边框、ballpaddle 对象添加到画布中:

    top = canv.create_line(0, 0, MAX_WIDTH, 0, fill='blue',
                           tags=('top'))
    left = canv.create_line(0, 0, 0, MAX_HEIGHT, fill='blue',
                            tags=('left'))
    right = canv.create_line(MAX_WIDTH, 0, MAX_WIDTH, MAX_HEIGHT,
                             fill='blue', tags=('right'))
    bottom = canv.create_line(0, MAX_HEIGHT, MAX_WIDTH, MAX_HEIGHT,
                              fill='blue', tags=('bottom'))
    
    ball = canv.create_rectangle(0, 0, BALL_SIZE, BALL_SIZE,
                                 outline='black', fill='black', 
                                 tags=('ball'))
    paddle = canv.create_rectangle(100, MAX_HEIGHT - 30, 150, 470,
                                   outline='black', 
                                   fill='green', tags=('rect'))
    
  5. 绘制所有砖块并设置球和挡板的位置:

    brick=list()
    for i in range(0,16):
      for row in range(0,4):
        brick.append(canv.create_rectangle(i*40, row*20,
                     ((i+1)*40)-2, ((row+1)*20)-2,
                     outline='black', fill='red',
                     tags=('rect')))
    
    delta_x = delta_y = 1
    xold,yold = xSTART,ySTART
    canv.move(ball, xold, yold)
    
  6. 创建游戏的主循环以检查碰撞和处理挡板和球的移动:

    while RUNNING:
      objects = canv.find_overlapping(canv.coords(ball)[0],
                                      canv.coords(ball)[1],
                                      canv.coords(ball)[2],
                                      canv.coords(ball)[3])
    
      #Only change the direction once (so will bounce off 1st
      # block even if 2 are hit)
      dir_changed=False
      for obj in objects:
        if (obj != ball):
          if dir_changed==False:
            determineDir(canv.coords(ball),canv.coords(obj))
            dir_changed=True
          if (obj >= brick[0]) and (obj <= brick[len(brick)-1]):
            canv.delete(obj)
          if (obj == bottom):
            text = canv.create_text(300,100,text="YOU HAVE MISSED!")
            canv.coords(ball, (xSTART,ySTART,
                        xSTART+BALL_SIZE,ySTART+BALL_SIZE))
            delta_x = delta_y = 1
            canv.update()
            time.sleep(3)
            canv.delete(text)
      new_x, new_y = delta_x, delta_y
      canv.move(ball, new_x, new_y)
    
      canv.update()
      time.sleep(0.005)
    #End
    

它是如何工作的…

我们创建一个 640 x 480 像素的 Tkinter 应用程序,并将 <Right><Left> 光标键绑定到 move_right()move_left() 函数。我们使用 root.protocol('WM_DELETE_WINDOW', close) 来检测窗口何时关闭,以便我们可以干净地退出程序(通过 close(),它将 RUNNING 设置为 False)。

然后,我们将一个 Canvas 小部件添加到应用程序中,该小部件将包含所有我们的对象。我们创建了以下对象:topleftrightbottom。这些构成了我们游戏区域的边界侧。画布坐标在左上角为 0,0,在右下角为 640,480,因此可以确定每一边的起始和结束坐标(使用 canv.create_line(xStart, yStart, xEnd, yEnd))。

它是如何工作的…

Canvas 小部件的坐标

你也可以给对象添加多个 tagstags 通常很有用,可以定义对象的特定动作或行为。例如,它们允许在特定对象或砖块被击中时发生不同类型的事件。我们将在下一个示例中看到 tags 的更多用法。

接下来,我们定义球和挡板对象,这些对象是通过 canv.create_rectangle() 添加的。这需要两组坐标,定义了不正确的图像的相对角,应该是以下图像,其中包含 4x16 块砖的对象(在这种情况下,是左上角和右下角)。

它是如何工作的…

Tkinter 矩形由两个角坐标定义。

最后,我们可以创建砖块!

我们希望砖块宽度为 40 x 20 像素,这样我们可以在 640 像素的游戏区域(分为四行)中放置 16 块砖。我们可以创建一个砖块对象列表,其位置自动定义,如下面的代码所示:

brick=list()
for i in range(0,16):
  for row in range(0,4):
    brick.append(canv.create_rectangle(i*40, row*20, 
                 ((i+1)*40)-2, ((row+1)*20)-2, outline='black', 
                 fill='red', tags=('rect')))

通过使砖块略微缩小(-2)以创建一个小间隙,提供了类似砖块的效果。

它是如何工作的…

在 Canvas 顶部生成 16 块砖的 4 行。

我们现在在开始主控制循环之前设置默认设置。球的移动将由 delta_xdelta_y 控制,这两个值在每个周期中加到或从球的当前位置中减去。

接下来,我们设置球的开局位置,并使用 canv.move() 函数移动球这个距离。move() 函数将 100 添加到球对象的 xy 坐标中,该球最初在位置 0,0 创建。

现在一切都已经设置好了,主循环可以运行;这将检查球是否没有碰到任何东西(使用 canv.find_overlapping() 函数),对 delta_xdelta_y 的值进行任何调整,然后将它们应用到移动球到下一个位置。

delta_xdelta_y 的符号决定了球的方向。正值会使球斜向下向右移动,而 -delta_x 将使其向左移动,具体是向下还是向上取决于 delta_y 是正还是负。

球移动后,我们使用 canv.update() 重新绘制对显示所做的任何更改,而 time.sleep() 允许在再次检查和移动球之前有一个小的延迟。

使用 canv.find_overlapping() 函数检测物体碰撞。这个函数返回一个列表,其中包含找到重叠由提供的坐标定义的矩形边界的画布对象。例如,在正方形球的情况下,画布对象的任何坐标是否在球占据的空间内?

工作原理…

检查物体以检测它们是否重叠

如果发现球与另一个物体重叠,例如墙壁、球拍或一个或多个砖块,我们需要确定球接下来应该向哪个方向移动。由于我们使用球的坐标作为检查的区域,球将始终被列出,因此我们在检查对象列表时忽略它们。

我们使用 dir_changed 标志来确保如果我们同时击中两个砖块,我们在移动球之前不会改变方向两次。否则,这会导致球即使在碰撞到砖块后仍然继续以相同方向移动。

因此,如果球与另一个物体重叠,我们可以使用球和物体的坐标调用 determineDir() 来确定新的方向应该是什么。

当球与物体碰撞时,我们希望球从其上弹回;幸运的是,这很容易模拟,因为我们只需要根据我们是否击中了侧面或顶部/底部来改变 delta_xdelta_y 的符号。如果球击中了另一个物体的底部,这意味着我们正在向上移动,现在应该向下移动。然而,我们将在 x 轴上继续以相同方向移动(无论是向左、向右还是向上)。这可以从以下代码中看出:

if (ball[xTOP] == obj[xBTM]) or (ball[xBTM] == obj[xTOP]):
    delta_x = -delta_x

determineDir() 函数查看球和物体的坐标,并寻找左和右 x 坐标或上和下 y 坐标之间的匹配。这足以判断碰撞是在侧面还是顶部/底部,我们可以相应地设置 delta_xdelta_y 的符号,如下面的代码所示:

if (obj >= brick[0]) and (obj <= brick[-1]):
    canv.delete(obj)

接下来,我们可以通过检查重叠的对象 ID 是否在第一个和最后一个砖块 ID 之间来确定是否击中了一块砖。如果是砖块,我们可以使用canv.delete()来移除它。

注意

Python 允许索引值环绕而不是访问无效的内存,因此索引值-1将为我们提供列表中的最后一个项目。我们使用它来引用最后一个砖块作为brick [-1]

我们还检查被重叠的对象是否是底部行(在这种情况下,玩家用挡板错过了球),因此会短暂显示一条简短的消息。我们重置balldelta_x/delta_y值的位置。canv.update()函数确保在删除消息之前(3 秒后)刷新显示。

最后,球通过delta_x/delta_y距离移动,并且显示更新。这里添加了小的延迟以减少更新速率和 CPU 使用时间。否则,你会发现如果你的树莓派将 100%的精力用于运行程序,它将变得无响应。

当用户按下光标键时,会调用move_right()move_left()函数。它们检查挡板对象的位置,如果挡板不在边缘,挡板将相应移动。如果球击中挡板,碰撞检测将确保球反弹,就像它击中了一块砖一样。

你可以通过添加每个被摧毁的块的分数来进一步扩展这个游戏,允许玩家有限的生命,当玩家错过球时生命会丢失,甚至可以编写一些代码来读取新的砖块布局。

创建俯视滚动游戏

通过在我们的程序中使用对象和图像,我们可以创建许多类型的 2D 图形游戏。

在这个菜谱中,我们将创建一个寻宝游戏,玩家试图找到隐藏的宝藏(通过按Enter键挖掘)。每次宝藏未被找到时,玩家都会得到关于宝藏距离的线索;然后他们可以使用光标键四处移动并搜索,直到找到它。

创建俯视滚动游戏

在你的俯视滚动游戏中挖掘宝藏!

虽然这是一个游戏的基本概念,但它可以很容易地扩展以包括多个布局、陷阱和需要避免的敌人,甚至可能包括额外的工具或需要解决的谜题。通过一些图形调整,角色可以探索地牢、太空船,或者跳跃在云层中收集彩虹!

准备工作

以下示例使用了许多图像;这些图像作为本书的资源的一部分提供。您需要将九个图像放置在与 Python 脚本相同的目录中。

所需的图像文件可以在本章的代码包中看到。

如何做到这一点...

创建以下脚本,scroller.py

  1. 首先导入所需的库和参数:

    #!/usr/bin/python3
    # scroller.py
    import tkinter as TK
    import time
    import math
    from random import randint
    
    STEP=7
    xVAL,yVAL=0,1
    MAX_WIDTH,MAX_HEIGHT=640,480
    SPACE_WIDTH=MAX_WIDTH*2
    SPACE_HEIGHT=MAX_HEIGHT*2
    LEFT,UP,RIGHT,DOWN=0,1,2,3
    SPACE_LIMITS=[0,0,SPACE_WIDTH-MAX_WIDTH,
                  SPACE_HEIGHT-MAX_HEIGHT]
    DIS_LIMITS=[STEP,STEP,MAX_WIDTH-STEP,MAX_HEIGHT-STEP]
    BGN_IMG="bg.gif"
    PLAYER_IMG=["playerL.gif","playerU.gif",
                "playerR.gif","playerD.gif"]
    WALL_IMG=["wallH.gif","wallV.gif"]
    GOLD_IMG="gold.gif"
    MARK_IMG="mark.gif"
    newGame=False
    checks=list()
    
  2. 提供处理玩家移动的功能:

    def move_right(event):
      movePlayer(RIGHT,STEP)
    def move_left(event):
      movePlayer(LEFT,-STEP)
    def move_up(event):
      movePlayer(UP,-STEP)
    def move_down(event):
      movePlayer(DOWN,STEP)
    
    def foundWall(facing,move):
      hitWall=False
      olCoords=[canv.coords(player)[xVAL],
                canv.coords(player)[yVAL],
                canv.coords(player)[xVAL]+PLAYER_SIZE[xVAL],
                canv.coords(player)[yVAL]+PLAYER_SIZE[yVAL]]
      olCoords[facing]+=move
      objects = canv.find_overlapping(olCoords[0],olCoords[1],
                                      olCoords[2],olCoords[3])
      for obj in objects:
        objTags = canv.gettags(obj)
        for tag in objTags:
          if tag == "wall":
            hitWall=True
      return hitWall
    
    def moveBackgnd(movement):
      global bg_offset
      bg_offset[xVAL]+=movement[xVAL]
      bg_offset[yVAL]+=movement[yVAL]
      for obj in canv.find_withtag("bg"):
        canv.move(obj, -movement[xVAL], -movement[yVAL])
    
    def makeMove(facing,move):
      if facing == RIGHT or facing == LEFT:
        movement=[move,0] #RIGHT/LEFT
        bgOffset=bg_offset[xVAL]
        playerPos=canv.coords(player)[xVAL]
      else:
        movement=[0,move] #UP/DOWN
        bgOffset=bg_offset[yVAL]
        playerPos=canv.coords(player)[yVAL]
      #Check Bottom/Right Corner
      if facing == RIGHT or facing == DOWN: 
        if (playerPos+PLAYER_SIZE[xVAL]) < DIS_LIMITS[facing]:
          canv.move(player, movement[xVAL], movement[yVAL])
        elif bgOffset < SPACE_LIMITS[facing]:
          moveBackgnd(movement)
      else:
        #Check Top/Left Corner
        if (playerPos) > DIS_LIMITS[facing]:
          canv.move(player, movement[xVAL], movement[yVAL])
        elif bgOffset > SPACE_LIMITS[facing]:
          moveBackgnd(movement)
    
    def movePlayer(facing,move):
      hitWall=foundWall(facing,move)
      if hitWall==False:
        makeMove(facing,move)
      canv.itemconfig(player,image=playImg[facing])
    
  3. 添加检查玩家距离隐藏黄金有多远的功能:

    def check(event):
      global checks,newGame,text
      if newGame:
        for chk in checks:
          canv.delete(chk)
        del checks[:]
        canv.delete(gold,text)
        newGame=False
        hideGold()
      else:
        checks.append(
                      canv.create_image(canv.coords(player)[xVAL],
                      canv.coords(player)[yVAL],
                      anchor=TK.NW, image=checkImg,
                      tags=('check','bg')))
        distance=measureTo(checks[-1],gold)
        if(distance<=0):
          canv.itemconfig(gold,state='normal')
          canv.itemconfig(check,state='hidden')
          text = canv.create_text(300,100,fill="white",
                                  text=("You have found the gold in"+ 
                                  " %d tries!"%len(checks)))
          newGame=True
        else:
          text = canv.create_text(300,100,fill="white",
                                  text=("You are %d steps away!"%distance))
          canv.update()
          time.sleep(1)
          canv.delete(text)
    
    def measureTo(objectA,objectB):
      deltaX=canv.coords(objectA)[xVAL]-\
                         canv.coords(objectB)[xVAL]
      deltaY=canv.coords(objectA)[yVAL]-\
                         canv.coords(objectB)[yVAL]
      w_sq=abs(deltaX)**2
      h_sq=abs(deltaY)**2
      hypot=math.sqrt(w_sq+h_sq)
      return round((hypot/5)-20,-1)
    
  4. 添加帮助找到隐藏黄金位置的功能:

    def hideGold():
      global gold
      goldPos=findLocationForGold()
      gold=canv.create_image(goldPos[xVAL], goldPos[yVAL],
                             anchor=TK.NW, image=goldImg,
                             tags=('gold','bg'),
                             state='hidden')
    
    def findLocationForGold():
      placeGold=False
      while(placeGold==False):
        goldPos=[randint(0-bg_offset[xVAL],
                 SPACE_WIDTH-GOLD_SIZE[xVAL]-bg_offset[xVAL]),
                 randint(0-bg_offset[yVAL],
                 SPACE_HEIGHT-GOLD_SIZE[yVAL]-bg_offset[yVAL])]
        objects = canv.find_overlapping(goldPos[xVAL],
                                        goldPos[yVAL],
                                        goldPos[xVAL]+GOLD_SIZE[xVAL],
                                        goldPos[yVAL]+GOLD_SIZE[yVAL])
        findNewPlace=False
        for obj in objects:
          objTags = canv.gettags(obj)
          for tag in objTags:
            if (tag == "wall") or (tag == "player"):
              findNewPlace=True
        if findNewPlace == False:
          placeGold=True
      return goldPos
    
  5. 创建 Tkinter 应用程序窗口并绑定键盘事件:

    root = TK.Tk()
    root.title("Overhead Game")
    root.geometry('%sx%s+%s+%s' %(MAX_WIDTH, 
                                  MAX_HEIGHT, 
                                  100, 100))
    root.resizable(width=TK.FALSE, height=TK.FALSE)
    root.bind('<Right>', move_right)
    root.bind('<Left>', move_left)
    root.bind('<Up>', move_up)
    root.bind('<Down>', move_down)
    root.bind('<Return>', check)
    
    canv = TK.Canvas(root, highlightthickness=0)
    canv.place(x=0,y=0,width=SPACE_WIDTH,height=SPACE_HEIGHT)
    
  6. 初始化所有游戏对象(背景瓦片、玩家、墙壁和黄金):

    #Create background tiles
    bgnImg = TK.PhotoImage(file=BGN_IMG)
    BGN_SIZE = bgnImg.width(),bgnImg.height()
    background=list()
    COLS=int(SPACE_WIDTH/BGN_SIZE[xVAL])+1
    ROWS=int(SPACE_HEIGHT/BGN_SIZE[yVAL])+1
    for col in range(0,COLS):
      for row in range(0,ROWS):
        background.append(canv.create_image(col*BGN_SIZE[xVAL],
                          row*BGN_SIZE[yVAL], anchor=TK.NW,
                          image=bgnImg,
                          tags=('background','bg')))
    bg_offset=[0,0]
    
    #Create player
    playImg=list()
    for img in PLAYER_IMG:
      playImg.append(TK.PhotoImage(file=img))
    #Assume images are all same size/shape
    PLAYER_SIZE=playImg[RIGHT].width(),playImg[RIGHT].height()
    player = canv.create_image(100,100, anchor=TK.NW,
                               image=playImg[RIGHT],
                               tags=('player'))
    
    #Create walls
    wallImg=[TK.PhotoImage(file=WALL_IMG[0]),
             TK.PhotoImage(file=WALL_IMG[1])]
    WALL_SIZE=[wallImg[0].width(),wallImg[0].height()]
    wallPosH=[(0,WALL_SIZE[xVAL]*1.5),
              (WALL_SIZE[xVAL],WALL_SIZE[xVAL]*1.5),
              (SPACE_WIDTH-WALL_SIZE[xVAL],WALL_SIZE[xVAL]*1.5),
              (WALL_SIZE[xVAL],SPACE_HEIGHT-WALL_SIZE[yVAL])]
    wallPosV=[(WALL_SIZE[xVAL],0),(WALL_SIZE[xVAL]*3,0)]
    wallPos=[wallPosH,wallPosV]
    wall=list()
    for i,img in enumerate(WALL_IMG):
      for item in wallPos[i]:
        wall.append(canv.create_image(item[xVAL],item[yVAL],
                    anchor=TK.NW, image=wallImg[i],
                    tags=('wall','bg')))
    
    #Place gold
    goldImg = TK.PhotoImage(file=GOLD_IMG)
    GOLD_SIZE=[goldImg.width(),goldImg.height()]
    hideGold()
    #Check mark
    checkImg = TK.PhotoImage(file=MARK_IMG)
    
  7. 最后,启动mainloop()命令以允许 Tkinter 监控事件:

    #Wait for actions from user
    root.mainloop()
    #End
    

工作原理…

如前所述,我们创建一个新的 Tkinter 应用程序,其中包含一个Canvas小部件,这样我们就可以添加所有的游戏对象。我们确保我们绑定了右键、左键、上键、下键和Enter键,这些将在游戏中作为我们的控制键。

首先,我们将背景图像(bg.gif)放置在Canvas小部件上。我们计算可以沿长度和宽度放置多少图像以铺满整个画布空间,并使用合适的坐标定位它们。

接下来,我们创建玩家图像(通过创建playImg,一个玩家可以转向的每个方向的 Tkinter 图像对象列表)并将其放置在画布上。

我们现在创建墙壁,其位置由wallPosHwallPosV列表定义。这些可以使用精确坐标定义,也许甚至可以从文件中读取以提供一种简单的方法来加载不同级别的布局。通过遍历列表,水平墙和垂直墙图像被放置在画布上。

为了完成布局,我们只需将黄金隐藏在某个地方。使用hideGold()函数,我们随机确定一个合适的地点来定位黄金。在findLocationForGold()中,我们使用randint(0,value)创建一个伪随机数(它不是完全随机的,但对于这个用途已经足够好了),介于0value之间。在我们的例子中,我们想要的值介于0和画布空间边缘减去黄金图像的大小以及任何已应用于画布的bg_offset之间。这确保它不会超出屏幕边缘。然后我们使用find_overlapping()函数检查潜在位置,看看是否有带有wallplayer标记的对象挡在路上。如果是这样,我们选择一个新的位置。否则,我们在画布上放置黄金,但使用state="hidden"值,这将使其从视图中隐藏。

然后,我们创建checkImg(一个 Tkinter 图像)并在检查黄金时使用它来标记我们检查的区域。最后,我们只需等待用户按下任一键。

当按下任一光标键时,角色将在屏幕上移动。玩家的移动由movePlayer()函数确定;它将首先检查玩家是否试图移动到墙壁中,然后在makeMove()函数中确定玩家是否在显示或画布空间的边缘。

工作原理…

每当按下光标键时,我们使用图中所示的逻辑来确定要做什么

foundWall()函数通过检查玩家图像覆盖区域内的任何带有wall标签的对象,以及玩家将要移动到的区域的一点点额外空间,来确定玩家是否会撞到墙。以下图表显示了如何确定olCoords坐标:

如何工作…

检查重叠对象(olCoords)的坐标被计算

makeMove()函数检查玩家是否会移动到显示的边缘(由DIS_LIMITS定义),以及他们是否位于画布空间的边缘(由SPACE_LIMITS定义)。在显示区域内,玩家可以朝光标方向移动,或者画布空间内所有带有bg标签的对象朝相反方向移动,模拟在玩家后面滚动。这是通过moveBackground()函数完成的。

当玩家按下Enter键时,我们将想要检查当前位置是否有金子。使用measureTo()函数,比较玩家和金子的位置(计算每个xy坐标之间的距离)。

如何工作…

玩家与金子之间的距离被计算

结果被缩放以提供一个大致的指示,说明玩家距离金子有多远。如果距离大于零,我们显示玩家距离金子的距离,并留下一个十字来显示我们检查的位置。如果玩家找到了金子,我们将显示一条消息说明这一点,并将newGame设置为True。下次玩家按下Enter键时,带有十字标记的位置将被移除,金子将被重新放置在新的位置。

再次隐藏了金子后,玩家准备再次开始!

第五章:创建 3D 图形

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

  • 从 3D 坐标和顶点开始

  • 创建和导入 3D 模型

  • 创建一个可以漫游的 3D 世界

  • 构建 3D 地图和迷宫

简介

原始 Raspberry Pi 的核心芯片(一款Broadcom BCM2835处理器)最初是为了移动和嵌入式应用而设计的图形处理单元GPU)。由于芯片上还有额外的空间,因此添加了驱动 Raspberry Pi 大部分功能的 ARM 核心;这使得这个强大的 GPU 可以作为系统级芯片SoC)解决方案使用。

如您所想,如果那个原始的 ARM 核心(ARM1176JZF-S,这是ARMv6架构)仅占 Raspberry Pi 芯片上的一小部分,那么您认为 GPU 必须表现相当出色是正确的。

注意

Raspberry Pi 3 的核心处理器已经升级(到Broadcom BCM2837处理器);它现在包含四个 ARM 核心(Cortex A53 ARMv8A),每个都比原始的ARMv6更强大。与上一代相同的 GPU 相结合,Raspberry Pi 3 在执行构建 3D 环境所需的计算方面装备得更加完善。然而,尽管 Raspberry Pi 3 可以更快地加载示例,但一旦生成 3D 模型,两种芯片版本的表现几乎相同。

VideoCore IV GPU由 48 个专用处理器组成,其中一些提供对视频的 1080p 高清编码和解码支持,而其他则支持OpenGL ES 2.0,这为 3D 图形提供了快速的运算。据说其图形处理能力相当于苹果 iPhone 4s 和原始的微软 Xbox。如果您在 Raspberry Pi 上运行Quake 3OpenArena,这一点会更加明显(有关详细信息,请访问www.raspberrypi.org/openarena-for-raspberry-pi)。

在本章中,我希望向您展示,虽然您可以通过使用 Raspberry Pi 的 ARM 侧执行操作来实现很多功能,但如果您探索 GPU 隐藏的那一侧,您可能会发现这个小电脑比最初看起来更有潜力。

Pi3D 库是由 Pi3D 团队(Patrick Gaunt、Tom Swirly、Tim Skillman 等人)创建的,它提供了一种通过创建 3D 图形来利用 GPU 的方法。

Pi3D 的维基百科和文档页面可以在以下链接找到:

pi3d.github.io/html/index.html

支持和开发小组可以在以下链接找到:

groups.google.com/forum/#!forum/pi3d

该库包含许多功能,因此在以下示例中不可能涵盖所有可用内容。建议您也花些时间尝试 Pi3D 演示。要发现更多用于创建和处理 3D 图形的选择,您可以查看构成库本身的某些 Python 模块(在文档或 GitHub 上的代码中描述,网址为 github.com/pi3d/pi3d.github.com)。希望这一章能向您介绍足够的概念,以展示您可用的原始潜力。

从 3D 坐标和顶点开始

我们周围的世界是三维的,因此为了模拟世界的一部分,我们可以创建一个 3D 表示并在我们的 2D 屏幕上显示它。

树莓派使我们能够模拟 3D 空间,在其中放置 3D 对象,并从选定的视角观察它们。我们将使用 GPU 将 3D 视图表示为 2D 图像,以便在屏幕上显示。

以下示例将展示我们如何使用 Pi3D(树莓派的 OpenGL ES 库)放置单个 3D 对象并在 3D 空间内显示它。然后我们将允许鼠标围绕对象旋转视图。

从 3D 坐标和顶点开始

准备就绪

树莓派必须直接连接到显示器,无论是通过 HDMI 还是模拟视频输出。GPU 渲染的 3D 图形将仅在本地显示器上显示,即使您通过网络远程连接到树莓派。您还需要使用本地连接的鼠标进行控制(然而,通过 SSH 连接键盘控制也可以工作)。

第一次使用 Pi3D 时,我们需要按照以下步骤下载和安装它:

  1. Pi3D 库使用 Pillow,这是与 Python 3 兼容的 Python 图像库版本,用于导入模型中使用的图形(如纹理和背景)。

    Pillow 的安装已在 第三章 的 准备就绪 部分中介绍,即 使用 Python 进行自动化和生产力

    安装命令如下所示(如果您之前已安装,它将跳过它们并继续):

    sudo apt-get update
    sudo apt-get install python3-pip
    sudo apt-get install libjpeg-dev
    sudo pip-3.2 install pillow
    
    
  2. 我们现在可以使用 PIP 使用以下命令安装 Pi3D:

    sudo pip-3.2 install pi3d
    
    

    注意

    Pi3D 团队持续开发和改进库;如果您遇到问题,这可能意味着新版本与旧版本不兼容。

    您还可以在 附录 中检查您拥有的 Pi3D 版本,如果需要,安装列出的相同版本。或者,联系 Google 群组中的 Pi3D 团队;他们将乐意帮助您!

    从 GitHub 网站获取 Pi3D 演示,如下所示命令行。您需要大约 90 MB 的空闲空间来下载和提取文件:

    cd ~
    wget https://github.com/pi3d/pi3d_demos/archive/master.zip
    unzip master.zip
    rm master.zip
    
    

    您会发现演示已解压缩到 pi3d_demos-master。默认情况下,预期演示位于 home/pi/pi3d;因此,我们将此目录重命名为 pi3d,如下所示命令:

    mv pi3d_demos-master pi3d
    
    
  3. 最后,检查树莓派的内存分割。运行 raspi-config (sudo raspi-config) 并确保您的内存分割设置为 128. (如果您之前更改过,则可能需要这样做,因为 128 MB 是默认值。)这确保您为 GPU 分配了足够的 RAM,以便在需要时能够处理大量的 3D 对象。

  4. 测试一切是否正常工作。现在您应该能够运行 pi3d_demos-master 目录中的任何脚本。有关它们如何工作的详细信息,请参阅 Pi3D 维基页面 (pi3d.github.io/html/ReadMe.html)。为了获得最佳性能,建议从命令提示符(不加载桌面)运行脚本:

    cd pi3d
    python3 Raspberry_Rain.py
    
    

    注意

    许多演示需要鼠标和键盘控制。

    虽然使用第四章中的方法,创建游戏和图形,对于使用 Tkinter 的鼠标和键盘输入是完全合理的,但 Pi3D 库中的许多演示使用 pi3d.Keyboardpi3d.Mouse 对象来提供对游戏手柄和游戏板的额外支持。pi3d.Keyboard 对象还支持通过 SSH 进行键盘控制(请参阅第一章的通过 SSH(和 X11 转发)远程连接到树莓派网络部分,开始使用树莓派计算机)。

    配置您自己脚本的设置。由于我们将使用演示中的一些纹理和模型,建议您在 pi3d 目录中创建脚本。如果您有与默认树莓派账户不同的用户名,您需要调整 /pi3d/demo.py。通过编辑文件将 USERNAME 部分替换为您自己的用户名:

    nano ~/pi3d/demo.py
    import sys
    sys.path.insert(1, '/home/USERNAME/pi3d')
    
    

    如果您想将文件移到其他位置,请确保在包含所需资源文件正确路径的文件夹中添加 demo.py 的副本。

如何做到这一点…

创建以下 3dObject.py 脚本:

#!/usr/bin/python3
""" Create a 3D space with a Tetrahedron inside and rotate the
    view around using the mouse.
"""
from math import sin, cos, radians

import demo
import pi3d

KEY = {'ESC':27,'NONE':-1}

DISPLAY = pi3d.Display.create(x=50, y=50)
#capture mouse and key presses
mykeys = pi3d.Keyboard()
mymouse = pi3d.Mouse(restrict = False)
mymouse.start()

def main():
  CAMERA = pi3d.Camera.instance()
  tex = pi3d.Texture("textures/stripwood.jpg")
  flatsh = pi3d.Shader("uv_flat")

  #Define the coordinates for our shape (x,y,z) 
  A = (-1.0,-1.0,-1.0)
  B = (1.0,-1.0,1.0)
  C = (-1.0,-1.0,1.0)
  D = (-1.0,1.0,1.0)
  ids = ["A","B","C","D"]
  coords = [A,B,C,D]
  myTetra = pi3d.Tetrahedron(x=0.0, y=0.0, z=0.0,
                             corners=(A,B,C,D))
  myTetra.set_draw_details(flatsh,[tex])
  # Load ttf font and set the font to black
  arialFont = pi3d.Font("fonts/FreeMonoBoldOblique.ttf",
                        "#000000")
  mystring = []
  #Create string objects to show the coordinates
  for i,pos in enumerate(coords):
    mystring.append(pi3d.String(font=arialFont,
                            string=ids[i]+str(pos),
                            x=pos[0], y=pos[1],z=pos[2]))
    mystring.append(pi3d.String(font=arialFont,
                            string=ids[i]+str(pos),
                            x=pos[0], y=pos[1],z=pos[2], ry=180))
  for string in mystring:
    string.set_shader(flatsh)

  camRad = 4.0 # radius of camera position
  rot = 0.0 # rotation of camera
  tilt = 0.0 # tilt of camera
  k = KEY['NONE']
  omx, omy = mymouse.position()

  # main display loop
  while DISPLAY.loop_running() and not k == KEY['ESC']:
    k = mykeys.read()
    mx, my = mymouse.position()
    rot -= (mx-omx)*0.8
    tilt += (my-omy)*0.8
    omx = mx
    omy = my

    CAMERA.reset()
    CAMERA.rotate(-tilt, rot, 0)
    CAMERA.position((camRad * sin(radians(rot)) *
                     cos(radians(tilt)), 
                     camRad * sin(radians(tilt)), 
                     -camRad * cos(radians(rot)) *
                     cos(radians(tilt))))
    #Draw the Tetrahedron
    myTetra.draw()
    for string in mystring:
      string.draw()

try:
  main()
finally:
  mykeys.close()
  mymouse.stop()
  DISPLAY.destroy()
  print("Closed Everything. END")
#End

要运行脚本,请使用 python3 3dObject.py

它是如何工作的…

我们导入 math 模块(用于角度计算——用于根据鼠标移动控制视图)。我们还导入 demo 模块,它仅提供此示例中着色器纹理的路径。

我们首先定义一些关键元素,这些元素将被 Pi3D 用于生成和显示我们的对象。我们将放置对象的空间是 pi3d.Display 对象;这定义了空间的大小并初始化屏幕以生成和显示 OpenGL ES 图形。

接下来,我们定义一个 pi3d.Camera 对象,这将允许我们在我们的空间内定义如何查看对象。为了渲染我们的对象,我们定义一个要应用到表面的纹理和一个将纹理应用到对象的着色器。着色器用于将所有效果和光照应用到对象上,并且它被编码为使用 GPU 的 OpenGL ES 内核而不是 ARM 处理器。

我们使用 pi3d.keyboard()pi3d.mouse() 定义了 keyboardmouse 对象,以便我们可以响应键盘和鼠标输入。mouse 对象的 restrict 标志允许鼠标的绝对位置超出屏幕限制(这样我们就可以连续旋转我们的 3D 对象)。当主循环运行时,会检查是否按下了 Esc 键,然后关闭所有内容(包括调用 DISPLAY.destroy() 释放屏幕)。我们使用 try: finally: 方法来确保即使在 main() 中发生异常,也能正确关闭显示。

主显示循环中使用 mymouse.position() 收集鼠标移动,它返回 xy 坐标。xy 移动的差异用于围绕对象旋转。

鼠标移动决定了摄像机的位置和角度。任何调整鼠标的前后位置都会用来移动它穿过或位于对象下方,并改变摄像机的角度(使用 tilt),使其始终指向对象。同样,任何侧向移动都会使用 CAMERA.reset() 函数将摄像机围绕对象移动。这确保了显示更新摄像机视图以显示新的位置,CAMERA.rotate() 用于改变角度,并使用 CAMERA.position() 将摄像机移动到距离对象中心 camRad 单位的位置。

我们将绘制一个名为 四面体 的三维对象,这是一个由四个三角形组成的形状,形成一个具有三角形底部的金字塔。该形状的四个角(三个在底部周围,一个在顶部)将由三维坐标 A、B、C 和 D 定义,如图所示:

如何工作…

在 X、Y 和 Z 轴上放置的四面体

pi3d.Tetrahedron 对象通过指定坐标来定位它在空间中的位置,然后指定将连接以形成构成形状的四个三角形的角。

使用 set_draw_details(flatsh,[text]),我们应用我们希望使用的着色器(们)和对象的纹理。在我们的例子中,我们只使用了一个纹理,但某些着色器可以使用多个纹理来实现复杂效果。

为了帮助突出坐标的位置,我们将通过设置字符串文本来指定 ID 和坐标,并将它们放置在所需的位置,添加一些pi3d.String对象。对于每个位置,我们将创建两个字符串对象,一个朝前,另一个朝后(ry=180在 y 轴上将对象旋转 180 度)。pi3d.String对象是单面的,所以如果我们只有一个面向前,当视图旋转时,它就不会从后面可见,并且会消失(此外,如果它是可见的,文本也会是反的)。再次使用flatsh着色器通过set_shader()字符串对象来渲染它。

现在剩下的工作就是绘制我们的四面体和字符串对象,同时检查任何键盘事件。每次while循环完成时,都会调用DISPLAY.loop_running(),这将根据需要更新显示,调整相机设置。

还有更多...

除了介绍如何在 3D 空间内绘制基本对象外,前面的示例还使用了以下四个在 3D 图形编程中使用的关键元素。

相机

相机代表我们在 3D 空间中的视角;探索和查看更多空间的一种方法是通过移动相机。Camera类的定义如下:

pi3d.Camera.Camera(at=(0, 0, 0), eye=(0, 0, -0.1),
                   lens=None, is_3d=True, scale=1.0)

相机是通过提供两个位置来定义的,一个是要看的对象(通常是我们希望看到的对象——由at定义),另一个是从哪个位置看(对象的位置——由eye定义)。相机的其他特性,如视野(lens)等,可以通过默认设置进行调整或使用。

注意

如果我们没有在我们的显示中定义相机,将创建一个默认的相机,指向原点(即显示的中心,也就是0,0,0),并稍微向前定位(0,0,-0.1)。

有关相机模块的更多详细信息,请参阅 Pi3D 文档。

着色器

着色器非常有用,因为它们允许将应用纹理和光照到对象上的复杂工作卸载到 Raspberry Pi 中更强大的 GPU 上。Shader 类定义如下:

class pi3d.Shader.Shader(shfile=None, vshader_source=None,
                                      fshader_source=None)

这允许你指定一个着色器文件(shfile)以及文件中的特定顶点和片段着色器(如果需要)。

Pi3D 库中包含了一些着色器,其中一些允许使用多个纹理进行反射、近距离细节和透明度效果。着色器的实现将决定光照和纹理如何应用到对象上(在某些情况下,例如uv_flat,着色器将忽略任何光照效果)。

着色器文件列在pi3d\shaders目录中。尝试使用不同的着色器进行实验,例如mat_reflect,它将忽略纹理/字体,但仍然应用光照效果;或者uv_toon,它将对纹理应用卡通效果。

每个着色器由两个文件组成,vs(顶点着色器)和fs(片段着色器),使用类似 C 的代码编写。它们协同工作以将所需的效果应用于对象。顶点着色器负责将顶点的 3D 位置映射到 2D 显示。片段着色器(有时也称为像素着色器)负责将光照和纹理效果应用于像素本身。这些着色器的构建和操作超出了本章的范围,但pi3d\shaders目录中有几个示例着色器,你可以比较、更改和实验。

灯光

在 3D 世界中,光照非常重要;它可以是从简单的通用光照(如我们的示例中使用的)到从不同方向提供不同强度和颜色的多个灯光。光照与物体相互作用以及它们产生的效果将由用于渲染它们的纹理和着色器决定。

光照由其方向、颜色和亮度以及定义背景(非方向性)光的环境光定义。Light类定义如下:

class pi3d.Light (lightpos=(10, -10, 20),
                       lightcol=(1.0, 1.0, 1.0),
                       lightamb=(0.1, 0.1, 0.2))

注意

默认情况下,显示将定义一个具有以下属性的光照:

  • lightpos=(10, -10, 20): 这是一个从空间前方(靠近左上角)向下照射到空间后方的光(向右)。

  • lightcol=(1.0, 1.0, 1.0): 这是一个明亮、白色的方向性光(方向由前面的维度定义,颜色由 RGB 值1.0, 1.0, 1.0定义)。

  • lightamb=(0.1, 0.1, 0.2): 这是一个整体略显暗淡、略带蓝色的光。

纹理

纹理可以通过将精细细节应用于对象的表面来为对象增添真实感;这可以是墙上的砖块或显示在角色上的人脸。当着色器使用纹理时,它通常可以被重新缩放并添加反射;一些着色器甚至允许你应用表面细节。

注意

我们可以将多个纹理应用于一个对象以将它们组合并产生不同的效果;具体如何应用将由着色器决定。

创建和导入 3D 模型

直接从代码中创建复杂形状通常既繁琐又耗时。幸运的是,可以将预构建的模型导入到你的 3D 空间中。

甚至可以使用图形 3D 建模程序生成模型,然后将其导出为适合你使用的格式。以下示例生成了一个 Raspberry Pi 主题的 Newell 茶壶,如图所示:

创建和导入 3D 模型

Newell Raspberry Pi 茶壶

准备工作

我们将使用位于pi3d\models目录中的茶壶 3D 模型(teapot.objteapot.mdl)。

注意

制作茶壶模型是 3D 中显示 Hello World 的传统做法。计算机图形研究员马丁·纽厄尔(Martin Newell)于 1975 年首次创建了纽厄尔茶壶(Newell Teapot),作为他工作的基本测试模型。纽厄尔茶壶很快成为快速检查 3D 渲染系统是否正常工作的标准模型(它甚至出现在《玩具总动员》和《辛普森一家》的 3D 集中)。

其他模型可在 pi3d\models 目录中找到(后来被使用的 monkey.obj/mdl 可在本书的资源文件中找到)。

如何操作…

创建并运行以下 3dModel.py 脚本:

#!/usr/bin/python3
""" Wavefront obj model loading. Material properties set in
    mtl file. Uses the import pi3d method to load *everything*
"""
import demo
import pi3d
from math import sin, cos, radians

KEY = {'ESC':27,'NONE':-1}

# Setup display and initialise pi3d
DISPLAY = pi3d.Display.create()
#capture mouse and key presses
mykeys = pi3d.Keyboard()
mymouse = pi3d.Mouse(restrict = False)
mymouse.start()

def main():
  #Model textures and shaders
  shader = pi3d.Shader("uv_reflect")
  bumptex = pi3d.Texture("textures/floor_nm.jpg")
  shinetex = pi3d.Texture("textures/stars.jpg")
  # load model
  #mymodel = pi3d.Model(file_string='models/teapot.obj', z=10)
  mymodel = pi3d.Model(file_string='models/monkey.obj', z=10)
  mymodel.set_shader(shader)
  mymodel.set_normal_shine(bumptex, 4.0, shinetex, 0.5)

  #Create environment box
  flatsh = pi3d.Shader("uv_flat")
  ectex = pi3d.loadECfiles("textures/ecubes","sbox")
  myecube = pi3d.EnvironmentCube(size=900.0, maptype="FACES",
                                 name="cube")
  myecube.set_draw_details(flatsh, ectex)

  CAMERA = pi3d.Camera.instance()
  rot = 0.0 # rotation of camera
  tilt = 0.0 # tilt of camera
  k = KEY['NONE']
  omx, omy = mymouse.position()

  while DISPLAY.loop_running() and not k == KEY['ESC']:
    k = mykeys.read()
    #Rotate camera - camera steered by mouse
    mx, my = mymouse.position()
    rot -= (mx-omx)*0.8
    tilt += (my-omy)*0.8
    omx = mx
    omy = my
    CAMERA.reset()
    CAMERA.rotate(tilt, rot, 0)
    #Rotate object
    mymodel.rotateIncY(2.0)
    mymodel.rotateIncZ(0.1)
    mymodel.rotateIncX(0.3)
    #Draw objects
    mymodel.draw()
    myecube.draw()

try:
  main()
finally:
  mykeys.close()
  mymouse.stop()
  DISPLAY.destroy()
  print("Closed Everything. END")
#End

它是如何工作的…

3dObject.py 示例一样,我们定义了 DISPLAY 着色器(这次使用 uv_reflect)和一些额外的纹理——bumptexfloor_nm.jpg)和 shinetexstars.jpg)——以供以后使用。我们定义了一个我们想要导入的模型,将其放置在 z=10(如果没有给出坐标,它将被放置在 (0,0,0)。由于我们没有指定相机位置,默认情况下它将被放置在视图中(有关相机的详细信息,请参阅相关部分)。

我们使用 set_shader() 函数应用着色器。接下来,我们使用 bumptex 作为表面纹理(按 4 缩放)添加一些纹理和效果。我们使用 shinetex 应用额外的闪亮效果,并使用 set_normal_shine() 函数应用反射强度为 0.5(强度范围从 0.0,最弱,到 1.0,最强)。如果你仔细观察模型的表面,bumptex 纹理提供了额外的表面细节,而 shinetex 纹理则可以看作是表面上的反射。

为了在比默认蓝色空间更有趣的环境中显示我们的模型,我们创建了一个 EnvironmentCube 对象。这定义了一个内部空间应用了特殊纹理的大空间(在这个例子中,它将从 textures\ecubes 目录中加载 sbox_front/back/bottom/leftsbox_right 图像),因此它有效地包围了对象。结果是,你得到了一个令人愉快的环境背景。

再次,我们定义了一个默认的 CAMERA 对象,带有 rottilt 变量来控制视图。在 DISPLAY.loop_running() 部分中,我们可以使用鼠标控制 CAMERA 对象的视图,并以不同的速率旋转模型以使其旋转并展示其所有侧面(使用 RotateIncX/Y/Z() 函数来指定旋转速率)。最后,我们通过绘制模型和环境立方体来确保 DISPLAY 被更新。

还有更多…

我们可以在模拟环境中创建各种对象。Pi3D 提供了导入我们自己的模型并将多个纹理应用到它们上的方法。

创建或加载自己的对象

如果你想在这个示例中使用自己的模型,你需要创建一个正确格式的模型;Pi3D 支持 obj(wavefront 对象文件)和 egg(Panda3D)。

一个优秀、免费的 3D 建模程序叫做 Blender(可在 www.blender.org 获取)。他们的网站上有很多示例和教程,可以帮助你开始基本的建模(www.blender.org/education-help/tutorials)。

Pi3D 对模型的支持有限,不会支持 Blender 可以嵌入到导出模型中的所有功能,例如可变形网格。因此,只支持基本的多部分模型。需要几个步骤来简化模型,以便它可以由 Pi3D 加载。

.obj 模型转换为与 Pi3D 一起使用,请按照以下步骤操作:

  1. 在 Blender 中创建或加载一个模型——在尝试更复杂的模型之前,先从简单的对象开始。

  2. 选择每个 对象 并切换到 编辑 模式(按 Tab)。

  3. 选择所有顶点(按 A)并对其 uv 映射(按 U 然后选择 Unwrap)。

  4. 返回到 对象 模式(按 Tab)。

  5. 将其导出为 obj 格式——从顶部的 文件 菜单中选择 导出,然后选择 Wavefront (.obj)。确保在左下角的选项列表中也选中了 Include Normals

  6. 点击 保存,并将 .obj.mtl 文件放置在 pi3d\models 目录中,并确保更新脚本以包含模型的文件名,如下所示:

    mymodel = pi3d.Model(file_string='models/monkey.obj', name='monkey', z=4)
    

当你运行更新后的脚本时,你将在 3D 空间中看到你的模型。例如,下面的截图显示了 monkey.obj 模型:

创建或加载自己的对象

使用 Blender 创建并由 Pi3D 显示的猴子头模型

更改对象的纹理和 .mtl 文件

应用到模型表面的纹理包含在模型的 .mtl 文件中。此文件定义了纹理及其应用方式,这些方式由建模软件设置。复杂模型可能包含多个纹理,用于对象的各个部分。

如果没有定义材质,则使用着色器中的第一个纹理(在我们的例子中,这是 bumptex 纹理)。要向对象添加新纹理,请在 .mtl 文件中添加(或编辑)以下行(即使用 water.jpg):

map_Kd ../textures/water.jpg

关于 .mtl 文件和 .obj 文件的更多信息可以在以下维基百科链接中找到:

en.wikipedia.org/wiki/Wavefront_.obj_file

拍摄截图

Pi3D 库包含一个有用的截图功能,可以捕获屏幕并将其保存为 .jpg.png 文件。我们可以添加一个新的按键事件来触发它,并调用 pi3d.screenshot("filename.jpg") 来保存图像(或使用计数器来拍摄多张截图),如下面的代码所示:

shotnum = 0 #Set counter to 0
while DISPLAY.loop_running()
...
  if inputs.key_state("KEY_P"):
    while inputs.key_state("KEY_P"):
      inputs.do_input_events()		# wait for key to go up
      pi3d.screenshot("screenshot%04d.jpg"%( shotnum))
      shotnum += 1
...

创建一个可以漫游的 3D 世界

现在我们能够在我们的 3D 空间中创建模型和对象,以及生成背景,我们可能想要创建一个更有趣的环境来放置它们。

3D 地形图提供了一种优雅的方式来定义非常复杂的地形。地形是通过使用灰度图像来设置土地的高程来定义的。以下示例显示了我们可以如何定义自己的景观并模拟在其上飞行,甚至在其表面上行走:

创建一个可以漫游的 3D 世界

由地形图生成的 3D 景观

准备工作

您需要将Map.png文件(可在本书的资源文件中找到)放置在 Pi3D 库的pi3d/textures目录中。或者,您可以使用现有的高程图之一——将Map.png的引用替换为另一个高程图,例如testislands.jpg

如何操作…

创建以下3dWorld.py脚本:

#!/usr/bin/python3
from __future__ import absolute_import, division
from __future__ import print_function, unicode_literals
""" An example of generating a 3D environment using a elevation map
"""
from math import sin, cos, radians
import demo
import pi3d

KEY = {'R':114,'S':115,'T':116,'W':119,'ESC':27,'NONE':-1}

DISPLAY = pi3d.Display.create(x=50, y=50)
#capture mouse and key presses
mykeys = pi3d.Keyboard()
mymouse = pi3d.Mouse(restrict = False)
mymouse.start()

def limit(value,min,max):
  if (value < min):
    value = min
  elif (value > max):
    value = max
  return value

def main():
  CAMERA = pi3d.Camera.instance()
  tex = pi3d.Texture("textures/grass.jpg")
  flatsh = pi3d.Shader("uv_flat")
  # Create elevation map
  mapwidth,mapdepth,mapheight = 200.0,200.0,50.0
  mymap = pi3d.ElevationMap("textures/Map.png",
                width=mapwidth, depth=mapdepth, height=mapheight,
                divx=128, divy=128, ntiles=20)
  mymap.set_draw_details(flatsh, [tex], 1.0, 1.0)

  rot = 0.0 # rotation of camera
  tilt = 0.0 # tilt of camera
  height = 20
  viewhight = 4
  sky = 200
  xm,ym,zm = 0.0,height,0.0
  k = KEY['NONE']
  omx, omy = mymouse.position()
  onGround = False
  # main display loop
  while DISPLAY.loop_running() and not k == KEY['ESC']:
    CAMERA.reset()
    CAMERA.rotate(-tilt, rot, 0)
    CAMERA.position((xm,ym,zm))
    mymap.draw()

    mx, my = mymouse.position()
    rot -= (mx-omx)*0.8
    tilt += (my-omy)*0.8
    omx = mx
    omy = my

    #Read keyboard keys
    k = mykeys.read()
    if k == KEY['W']:
      xm -= sin(radians(rot))
      zm += cos(radians(rot))
    elif k == KEY['S']:
      xm += sin(radians(rot))
      zm -= cos(radians(rot))
    elif k == KEY['R']:
      ym += 2
      onGround = False
    elif k == KEY['T']:
      ym -= 2
    ym -= 0.1 #Float down!
    #Limit the movement
    xm = limit(xm,-(mapwidth/2),mapwidth/2)
    zm = limit(zm,-(mapdepth/2),mapdepth/2)
    if ym >= sky:
      ym = sky
    #Check onGround
    ground = mymap.calcHeight(xm, zm) + viewhight
    if (onGround == True) or (ym <= ground):
      ym = mymap.calcHeight(xm, zm) + viewhight
      onGround = True

try:
  main()
finally:
  mykeys.close()
  mymouse.stop()
  DISPLAY.destroy()
  print("Closed Everything. END")
#End

如何工作…

一旦我们定义了将要使用的显示、摄像头、纹理和着色器,我们就可以定义ElevationMap对象。

它通过根据图像选定点的像素值给地形图像分配高度来实现。例如,图像的单行将提供ElevationMap对象的切片和 3D 表面上的高程点行:

如何工作…

将 map.png 像素阴影映射到地形高度

我们通过提供用于梯度信息的图像文件名(textures/Map.png)来创建一个ElevationMap对象,并且我们还创建了地图的尺寸(widthdepthheight——这是白色空间相对于黑色空间的高度):

如何工作…

地图的亮部将创建高点,而暗部将创建低点

Map.png纹理提供了一个示例地形图,该图被转换成三维表面。

我们还指定divxdivy,这决定了使用地形图的多少细节(使用地形图中的多少点来创建高程表面)。最后,ntiles指定使用的纹理将被缩放到覆盖表面20 倍

在主DISPLAY.loop_running()部分中,我们将控制摄像头,绘制ElevationMap,响应用户输入,并限制我们在空间中的移动。

如前所述,我们使用一个Keyboard对象来捕捉鼠标移动并将它们转换为控制摄像头的操作。我们还将使用mykeys.read()来确定是否按下了WSRT键,这允许我们前进和后退,以及上升和下降。

注意

为了允许轻松地在Keyboard对象返回的值及其等效意义之间进行转换,我们将使用一个 Python 字典:

KEY = {'R':114,'S':115,'T':116,'W':119,'ESC':27,'NONE':-1}

字典提供了一个简单的方法,在给定的值和结果字符串之间进行转换。要访问键的值,我们使用KEY['W']。我们还在第三章中使用了字典,在应用程序中显示照片信息,以在图像 Exif 标签名称和 ID 之间进行转换。

为了确保我们在移动到ElevationMap对象表面时不会掉下去,我们可以使用mymap.calcHeight()来为我们提供特定位置(x,y,z)的地形高度。我们可以通过确保相机设置为等于这个高度来跟随地面,或者通过确保我们永远不会低于它来在空中飞行。当我们检测到我们在地面上时,我们确保我们保持在地面,直到我们按下R键再次上升。

构建 3D 地图和迷宫

我们已经看到,Pi3D 库可以用来创建许多有趣的对象和环境。通过使用一些更复杂的类(或者通过构建自己的类),可以为用户设计出整个自定义空间来探索。

在以下示例中,我们使用一个名为Building的特殊模块,该模块被设计为允许您使用单个图像文件来构建整个建筑:

构建 3D 地图和迷宫

探索迷宫并找到标记出口的球体

准备工作

您需要确保在pi3d/textures目录中具有以下文件:

  • squareblocksred.png

  • floor.png

  • inside_map0.png, inside_map1.png, inside_map2.png

这些文件作为本书的资源的一部分提供,位于Chapter05\resource\source_files\textures

如何做到这一点…

让我们按照以下步骤运行以下3dMaze.py脚本:

  1. 首先,我们使用以下代码设置键盘、鼠标、显示和模型的设置:

    #!/usr/bin/python3
    """Small maze game, try to find the exit
    """
    from math import sin, cos, radians
    import demo
    import pi3d
    from pi3d.shape.Building import Building, SolidObject
    from pi3d.shape.Building import Size, Position
    
    KEY = {'A':97,'D':100,'H':104,'R':114,'S':115,'T':116,
           'W':119,'ESC':27,'APOST':39,'SLASH':47,'NONE':-1}
    
    # Setup display and initialise pi3d
    DISPLAY = pi3d.Display.create()
    #capture mouse and key presses
    mykeys = pi3d.Keyboard()
    mymouse = pi3d.Mouse(restrict = False)
    
    #Load shader
    shader = pi3d.Shader("uv_reflect")
    flatsh = pi3d.Shader("uv_flat")
    # Load textures
    ceilingimg = pi3d.Texture("textures/squareblocks4.png")
    wallimg = pi3d.Texture("textures/squareblocksred.png")
    floorimg = pi3d.Texture("textures/dunes3_512.jpg")
    bumpimg = pi3d.Texture("textures/mudnormal.jpg")
    startimg = pi3d.Texture("textures/rock1.jpg")    
    endimg = pi3d.Texture("textures/water.jpg")
    # Create elevation map
    mapwidth = 1000.0
    mapdepth = 1000.0
    #We shall assume we are using a flat floor in this example
    mapheight = 0.0
    mymap = pi3d.ElevationMap(mapfile="textures/floor.png",
                    width=mapwidth, depth=mapdepth, height=mapheight,
                    divx=64, divy=64)
    mymap.set_draw_details(shader,[floorimg, bumpimg],128.0, 0.0)
    levelList = ["textures/inside_map0.png","textures/inside_map1.png",
                 "textures/inside_map2.png"]
    avhgt = 5.0
    aveyelevel = 4.0
    MAP_BLOCK = 15.0
    aveyeleveladjust = aveyelevel - avhgt/2
    PLAYERHEIGHT = (mymap.calcHeight(5, 5) + avhgt/2)
    #Start the player in the top-left corner
    startpos = [(8*MAP_BLOCK),PLAYERHEIGHT,(8*MAP_BLOCK)]
    endpos = [0,PLAYERHEIGHT,0] #Set the end pos in the centre
    person = SolidObject("person", Size(1, avhgt, 1),
                    Position(startpos[0],startpos[1],startpos[2]), 1)
    #Add spheres for start and end, end must also have a solid object
    #so we can detect when we hit it
    startobject = pi3d.Sphere(name="start",x=startpos[0],
                              y=startpos[1]+avhgt,z=startpos[2])
    startobject.set_draw_details(shader, [startimg, bumpimg],
                                 32.0, 0.3)
    endobject = pi3d.Sphere(name="end",x=endpos[0],
                            y=endpos[1],z=endpos[2])
    endobject.set_draw_details(shader, [endimg, bumpimg], 32.0, 0.3)
    endSolid = SolidObject("end", Size(1, avhgt, 1),
                    Position(endpos[0],endpos[1],endpos[2]), 1)
    
    mazeScheme = {"#models": 3,
          (1,None): [["C",2]],      #white cell : Ceiling
          (0,1,"edge"): [["W",1]],  #white cell on edge next
                                    #   black cell : Wall
          (1,0,"edge"): [["W",1]],  #black cell on edge next
                                    #   to white cell : Wall
          (0,1):[["W",0]]}          #white cell next
                                    #   to black cell : Wall
    
    details = [[shader, [wallimg], 1.0, 0.0, 4.0, 16.0],
                [shader, [wallimg], 1.0, 0.0, 4.0, 8.0],
                [shader, [ceilingimg], 1.0, 0.0, 4.0, 4.0]]
    
    arialFont = pi3d.Font("fonts/FreeMonoBoldOblique.ttf",
                          "#ffffff", font_size=10)
    
  2. 然后,我们创建函数,使我们能够重新加载关卡并使用以下代码向玩家显示消息:

    def loadLevel(next_level):
      print(">>> Please wait while maze is constructed...")
      next_level=next_level%len(levelList)
      building = pi3d.Building(levelList[next_level], 0, 0, mymap,
          width=MAP_BLOCK, depth=MAP_BLOCK, height=30.0,
          name="", draw_details=details, yoff=-15, scheme=mazeScheme)
      return building
    
    def showMessage(text,rot=0):
      message = pi3d.String(font=arialFont, string=text,
                            x=endpos[0],y=endpos[1]+(avhgt/4),
                            z=endpos[2], sx=0.05, sy=0.05,ry=-rot)
      message.set_shader(flatsh)
      message.draw()
    
  3. 在主函数中,我们使用以下代码设置 3D 环境并绘制所有对象:

    def main():
      #Load a level
      level=0
      building = loadLevel(level)
      lights = pi3d.Light(lightpos=(10, -10, 20),
                          lightcol =(0.7, 0.7, 0.7),
                          lightamb=(0.7, 0.7, 0.7))
      rot=0.0
      tilt=0.0
      #capture mouse movements
      mymouse.start()
      omx, omy = mymouse.position()
    
      CAMERA = pi3d.Camera.instance()
      while DISPLAY.loop_running() and not \
                                   inputs.key_state("KEY_ESC"):
        CAMERA.reset()
        CAMERA.rotate(tilt, rot, 0)
        CAMERA.position((person.x(), person.y(),
                         person.z() - aveyeleveladjust))
        #draw objects
        person.drawall()
        building.drawAll()
        mymap.draw()
        startobject.draw()
        endobject.draw()
        #Apply the light to all the objects in the building
        for b in building.model:
          b.set_light(lights, 0)
        mymap.set_light(lights, 0)
    
    	#Get mouse position
        mx, my = mymouse.position()
        rot -= (mx-omx)*0.8
        tilt += (my-omy)*0.8
        omx = mx
        omy = my
        xm = person.x()
        ym = person.y()
        zm = person.z()
    
  4. 最后,我们监控按键,处理与对象的任何碰撞,并在迷宫中移动如下:

        #Read keyboard keys
        k = mykeys.read()
        if k == KEY['APOST']: #' Key
          tilt -= 2.0
        elif k == KEY['SLASH']: #/ Key
          tilt += 2.0
        elif k == KEY['A']:
          rot += 2.0
        elif k == KEY['D']:
          rot -= 2.0
        elif k == KEY['H']:
          #Use point_at as help - will turn the player to face
          #  the direction of the end point
          tilt, rot = CAMERA.point_at([endobject.x(), endobject.y(),
                                       endobject.z()])
        elif k == KEY['W']:
          xm -= sin(radians(rot))
          zm += cos(radians(rot))
        elif k == KEY['S']:
          xm += sin(radians(rot))
          zm -= cos(radians(rot))
    
        NewPos = Position(xm, ym, zm)
        collisions = person.CollisionList(NewPos)
        if collisions:
          #If we reach the end, reset to start position!
          for obj in collisions:
            if obj.name == "end":
              #Required to remove the building walls from the
              #  solidobject list
              building.remove_walls()
              showMessage("Loading Level",rot)
              DISPLAY.loop_running()
              level+=1
              building = loadLevel(level)
              showMessage("")
              person.move(Position(startpos[0],startpos[1],
                                   startpos[2]))
        else:
          person.move(NewPos)
    
    try:
      main()
    finally:
      mykeys.close()
      mymouse.stop()
      DISPLAY.destroy()
      print("Closed Everything. END")
    #End
    

它是如何工作的...

我们定义了在先前的示例中使用的大多数元素,例如显示、纹理、着色器、字体和照明。我们还定义了对象,如建筑本身、ElevationMap对象,以及迷宫的起点和终点。我们还使用SolidObjects来帮助检测空间内的移动。有关更多信息,请参阅此食谱更多内容部分的使用 SolidObjects 检测碰撞子部分。

最后,我们根据所选的地图图像(使用loadLevel()函数)创建实际的Building对象,并将相机(代表我们的第一人称视角)定位在起点。有关更多信息,请参阅此食谱的更多内容部分的Building 模块子部分。

main循环中,我们绘制我们空间中的所有对象并应用光照效果。我们还将监控鼠标(以控制相机的倾斜和旋转)或键盘以移动玩家(或退出/提供帮助)。

控制方式如下:

  • 鼠标移动:这会改变相机的倾斜和旋转。

  • ' 或 / 键:这会改变相机向下或向上倾斜。

  • A 或 D:这会改变相机从左向右或相反方向旋转。

  • W 或 S:这会使玩家向前或向后移动。

  • H:这通过将玩家旋转以面对迷宫的尽头来帮助玩家。有用的CAMERA.point_at()函数用于快速旋转和倾斜相机的视角,使其指向提供的坐标(终点位置)。

每当玩家移动时,我们都会检查新的位置(NewPos)是否与另一个SolidObject发生碰撞,使用CollisionList(NewPos)。该函数将返回任何与提供的坐标重叠的其他 SolidObject 的列表。

如果没有障碍物阻挡,我们将使玩家移动;否则,我们将检查是否有 SolidObject 的名称是end对象,在这种情况下,我们已经到达了迷宫的尽头。

当玩家到达终点时,我们将从旧的Building对象中移除墙壁并显示一个加载消息。如果我们不移除墙壁,属于前一个Building的所有 SolidObject 仍然会保留,在下一个关卡中创建不可见的障碍物。

我们使用showMessage()函数通知用户下一个关卡将很快被加载(因为构建建筑对象可能需要一段时间)。我们需要确保在绘制消息后调用DISPLAY.loop_running()。这确保了在开始加载关卡(之后人物将无法移动,因为正在加载)之前,消息已显示在屏幕上。我们需要确保无论玩家的哪一侧与end对象碰撞,消息总是面向玩家,通过使用相机的旋转(rot)来设置其角度。

如何工作...

当找到出口球时,将加载下一个关卡

当列表中的下一个关卡被加载(或者当所有关卡都完成后,再次加载第一个关卡),我们将消息替换为一个空白消息以移除它,并将人物的位置重置回起点。

您可以通过创建额外的地图文件(20 x 20 的 PNG 文件,用黑色像素标记墙壁,用白色标记通道)并在levelList中列出它们来设计和添加自己的关卡。玩家将从地图的左上角开始,出口放置在中心。

你会注意到加载层级可能需要相当长的时间;这是相对较慢的树莓派 ARM 处理器,它执行构建迷宫和定位所有组件所需的所有计算。一旦迷宫构建完成,更强大的 GPU 接管,这使得玩家探索空间时图形快速且流畅。

注意

此配方展示了原始树莓派处理器与树莓派 2 之间的区别。树莓派 2 加载第一个层级大约需要 1 分 20 秒,而原始树莓派可能需要长达 4 分 20 秒。树莓派 3 加载相同层级仅需惊人的 4 秒。

更多...

前面的示例创建了一个玩家可以探索和交互的建筑。为了实现这一点,我们使用 Pi3D 的Building模块创建建筑,并使用SolidObject检测碰撞。

建筑模块

pi3d.Building模块允许您使用地图文件定义整个建筑层级或楼层。就像前面示例中使用的地形地图一样,像素的颜色将被转换为不同层级的各个部分。在我们的案例中,黑色代表墙壁,白色代表通道和走廊,包括天花板:

建筑模块

建筑布局由图像中的像素定义

Building对象构建的部分由使用的方案定义。Scheme由两部分定义,即模型数量,然后是模型各个方面的定义,如下面的代码所示:

mazeScheme = {"#models": 3,
  (1,None): [["C",2]],     #white cell : Ceiling
  (0,1,"edge"): [["W",1]], #white cell on edge by black cell : Wall
  (1,0,"edge"): [["W",1]], #black cell on edge by white cell : Wall
  (0,1):[["W",0]]}         #white cell next to black cell : Wall 

第一个元组定义了所选模型应应用到的单元格/方块类型。由于地图中有两种像素颜色,方块将是黑色(0)或白色(1)。通过确定特定单元格/方块的位置和类型,我们可以定义我们想要应用哪些模型(墙壁、天花板或屋顶)。

我们定义了三种主要的单元格/方块位置类型:

  • 整个方块 (1,None):这是一个代表建筑中开放空间的白色单元格。

  • 一个单元格与另一个单元格相邻,在边缘 (0,1,"edge"):这是地图边缘处的黑色单元格旁边的一个白色单元格。这也包括 (1,0,"edge")。这将代表建筑的外墙。

  • 任何与白色单元格相邻的黑色单元格 (0,1):这将代表建筑的所有内部墙壁。

接下来,我们为该类型(WC)分配要应用的对象类型:

  • 墙壁 (W): 这是一个放置在指定单元格之间(如黑色和白色单元格之间)的垂直墙壁。

  • 天花板 (C): 这是一个用于覆盖当前单元格的天花板水平部分。

  • 屋顶 (R): 这是一个放置在天花板稍上方的额外水平部分,以提供屋顶效果。它通常用于可能需要从外部观看的建筑(在我们的示例中不使用)。

  • 天花板边缘 (CE): 这用于将天花板部分连接到建筑边缘的屋顶(在我们的例子中,由于我们是一个室内模型,所以不使用它)。

最后,我们指定每个对象将使用的模型。在这个例子中,我们使用三个模型(普通墙面、边缘墙面和天花板),因此可以通过指定 012 来定义使用的模型。

每个模型都在 details 数组中定义,这使得我们可以为每个模型设置所需的纹理和着色器(这包含通常由 .set_draw_details() 函数设置的相同信息),如下面的代码所示:

details = [[shader, [wallimg], 1.0, 0.0, 4.0, 16.0],
           [shader, [wallimg], 1.0, 0.0, 4.0, 8.0],
           [shader, [ceilingimg], 1.0, 0.0, 4.0, 4.0]]

在我们的例子中,内部墙面分配给 wallimg 纹理(textures/squareblocksred.png),天花板分配给 ceilingimg 纹理(textures/squareblocks4.png)。您可能可以从下面的屏幕截图中注意到,我们可以将不同的纹理模型(在我们的情况下,是稍微不同的缩放)应用于不同类型的方块。迷宫外部的墙面(带有边缘标识符)将使用 wallimg 模型纹理按 4x8 缩放(details[1]),而对于内部墙面,相同的模型纹理将按 4x16 缩放(details[0]):

建筑模块

左侧朝外的墙面与其他墙面相比应用了不同的缩放比例

当创建 pi3d.Building 对象时,会设置 schemedraw_details,如下面的代码所示:

building = pi3d.Building(levelList[next_level], 0, 0, mymap,width=MAP_BLOCK, depth=MAP_BLOCK, height=30.0, name="",draw_details=details, yoff=-15, scheme=mazeScheme)

使用地图文件(levelList[next_level])、方案(mazeScheme)和绘制细节(details),在环境中创建整个建筑:

建筑模块

我们创建的 3D 迷宫的俯视图

注意

尽管在这个例子中我们只使用了黑白两种颜色,但也可以使用其他颜色的像素来定义额外的方块类型(以及如果需要的话,不同的纹理)。如果添加了另一种颜色(如灰色),则颜色映射的索引会移动,使得黑色方块被引用为 0,新的彩色方块为 1,白色方块为 2。有关详细信息,请参阅 Pi3D 演示中的 Silo 示例。

我们还需要定义一个 ElevationMap 对象——mymappi3d.Building 模块使用 ElevationMap 对象的 calcHeight() 函数来正确地将墙面放置在 ElevationMap 对象的表面上。在这个例子中,我们将使用 textures/floor.png 应用基本的 ElevationMap 对象,这将生成一个平面表面,Building 对象将放置在其上。

使用 SolidObjects 检测碰撞

除了 Building 对象外,我们还将定义一个玩家对象,并定义两个对象来标记迷宫的起点和终点。尽管玩家的视角是第一人称视角(也就是说,我们实际上看不到他们,因为视角是有效通过他们的眼睛),但我们需要定义一个 SolidObject 来代表他们。

SolidObject 是一种特殊类型的不可见对象,可以通过检查来确定一个 SolidObject 将要占据的空间是否与另一个对象重叠。这将使我们能够使用person.CollisionList(NewPos)来获取任何其他 SolidObjects 的列表,这些 SolidObjects 将在NewPos位置与person对象接触。由于Building类为Building对象的各个部分定义了 SolidObjects,因此我们能够检测到玩家试图穿过墙壁(或者,出于某种原因,屋顶/天花板)并阻止他们穿过。

我们还在迷宫的起点和终点使用 SolidObjects。玩家开始的位置被设置为地图的左上角(地图左上角的空白像素)并由startpos对象(一个带有rock1.jpg纹理的小pi3d.Sphere)标记,该对象位于玩家的头部上方。迷宫的终点由另一个位于地图中心的pi3d.Sphere对象(带有water.jpg纹理)标记。我们还定义了另一个 SolidObject 在终点,这样我们就可以检测到玩家到达它并与之碰撞(并加载下一级!)。

第六章。使用 Python 驱动硬件

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

  • 控制 LED

  • 响应按钮

  • 一个受控的关机按钮

  • GPIO 键盘输入

  • 复合色 LED

  • 使用视觉持久性写入消息

简介

Raspberry Pi 计算机的一个关键特性是它能够直接与其他硬件接口,这使得它区别于大多数其他家庭/办公室计算机。Raspberry Pi 上的硬件 通用输入/输出GPIO)引脚可以控制从 发光二极管LED)到开关、传感器、电机、伺服机构和甚至额外显示器的广泛低级电子设备。

本章将重点介绍将 Raspberry Pi 与一些简单电路连接,并掌握使用 Python 控制和响应连接组件的方法。

Raspberry Pi 硬件接口由位于板边一侧的 40 个引脚组成。

注意

GPIO 引脚及其布局会根据您所拥有的特定型号略有不同。

Raspberry Pi 2 和 Raspberry Pi 1 Model A Plus 以及 B Plus 都有相同的 40 引脚布局。

较旧的 Raspberry Pi 1 型号(非 plus 类型)有一个 26 引脚的排针,这与较新型号的 1-26 引脚相同。

简介

Raspberry Pi 2 和 Raspberry Pi Model Plus GPIO 排针引脚(引脚功能)

连接器的布局在之前的图中显示;引脚编号是从 GPIO 排针的引脚 1 观察到的。

引脚 1 位于最靠近 SD 卡的末端,如下面的图片所示:

简介

Raspberry Pi GPIO 排针位置

使用 GPIO 排针时需小心,因为它还包括电源引脚(3V3 和 5V)以及地线引脚(GND)。所有 GPIO 引脚都可以用作标准 GPIO,但其中几个也有特殊功能;这些被标记并用不同颜色突出显示。

提示

工程师通常使用 3V3 符号来指定原理图中的值,以避免使用容易遗漏的小数点(使用 33V 而不是 3.3V 会造成严重损坏)。同样,这也适用于其他值,例如电阻,因此例如,1.2k 欧姆可以写成 1k2 欧姆。

TXRX 引脚,用于串行 RS232 通信,借助电压等级转换器,信息可以通过串行电缆传输到另一台计算机或设备。

我们有SDASCL引脚,它们能够支持一种称为I²C的双线总线通信协议(在 Model Plus 和 Raspberry Pi 2 板上,有两个 I²C 通道:通道 1 ARM用于通用,而通道 0 VC通常用于识别顶部附加的硬件模块(HAT))。还有SPI MOSISPI MISOSPI SCLKSPI CE0SPI CE1引脚,它们支持另一种称为SPI的高速数据总线协议。最后,我们还有PWM0/1,它允许生成脉冲宽度调制信号,这对于伺服和生成模拟信号非常有用。

然而,在本章中,我们将专注于使用标准的 GPIO 功能。GPIO 引脚布局如图所示:

介绍

Raspberry Pi GPIO 引脚(GPIO.BOARD 和 GPIO.BCM)

提示

Raspberry Pi Rev 2(2014 年 7 月之前)与 Raspberry Pi 2 GPIO 布局有以下不同之处:

  • 26 个 GPIO 引脚头(与第一个 26 个引脚相匹配)

  • 在引脚头旁边额外的一组八个孔(P5)。具体如下:

介绍

Raspberry Pi Rev 2 P5 GPIO 引脚头

原始的 Raspberry Pi Rev 1(2012 年 10 月之前)总共只有 26 个 GPIO 引脚(与当前 Raspberry Pi 的前 26 个引脚相匹配,除了以下细节):

介绍

Raspberry Pi Rev 1 GPIO 引脚头差异RPi.GPIO库可以使用两种系统之一来引用 Raspberry Pi 上的引脚。中间显示的数字是引脚的物理位置,也是 RPi.GPIO 在GPIO.BOARD模式下引用的数字。外部的数字(GPIO.BCM)是引脚连接到的处理器物理端口的实际引用(这就是为什么它们没有特定的顺序)。当模式设置为GPIO.BCM时,它们用于控制 GPIO 引脚头和连接到其他 GPIO 线的任何外围设备。这包括 BCM GPIO 4 上的附加摄像头上的 LED 和板上的状态 LED。然而,这也可以包括用于读取/写入 SD 卡的 GPIO 线,如果被干扰,将导致严重错误。

如果你使用其他编程语言来访问 GPIO 引脚,编号方案可能不同,因此了解 BCM GPIO 引用会有所帮助,这些引用指的是处理器的物理 GPIO 端口。

提示

一定要查看附录,硬件和软件列表,其中列出了本章中使用的所有项目及其获取地点。

控制一个 LED

硬件上的hello world等效于 LED 闪烁,这是一个很好的测试,以确保一切正常并且你已经正确接线。为了使其更有趣,我建议使用 RGB LED(它将红、绿、蓝 LED 组合成一个单元),但如果你只有可用的单独 LED,也可以使用。

准备就绪

你将需要以下设备:

  • 4 x 杜邦公对母排线

  • 小型面包板(170 个接线点)或更大的一个

  • RGB LED(共阴极)/3 个标准 LED(理想情况下为红/绿/蓝)

  • 面包板线(实心芯线)

  • 3 x 470 欧姆电阻

之前提到的每个组件只需花费几美元,并且在之后的项目中可以重复使用。面包板是一个特别有用的物品,它允许你在不需要焊接的情况下尝试自己的电路。

准备就绪

RGB LED、标准 LED 和 RGB 电路的图示

以下图示显示了面包板电路:

准备就绪

连接到 GPIO 引脚的 RGB LED/标准 LED 的接线

注意

可用的 RGB LED 有多种变体,因此请检查你组件的数据表以确认你拥有的引脚顺序和类型。有些是红、蓝、绿(RBG),因此请确保相应地接线或调整代码中的RGB_引脚设置。你也可以得到共阳极变体,这将需要将阳极连接到 3V3(GPIO-Pin1)才能点亮(并且需要将RGB_ENABLERGB_DISABLE设置为01)。

本书中的面包板和组件图是使用一个名为Fritzing的免费工具创建的(www.fritzing.org);它非常适合规划自己的树莓派项目。

如何做…

创建ledtest.py脚本如下:

#!/usr/bin/python3
#ledtest.py
import time
import RPi.GPIO as GPIO
# RGB LED module
#HARDWARE SETUP
# GPIO
# 2[======XRG=B==]26[=======]40
# 1[=============]25[=======]39
# X=GND R=Red G=Green B=Blue 
#Setup Active States
#Common Cathode RGB-LED (Cathode=Active Low)
RGB_ENABLE = 1; RGB_DISABLE = 0

#LED CONFIG - Set GPIO Ports
RGB_RED = 16; RGB_GREEN = 18; RGB_BLUE = 22
RGB = [RGB_RED,RGB_GREEN,RGB_BLUE]

def led_setup():
  #Setup the wiring
  GPIO.setmode(GPIO.BOARD)
  #Setup Ports
  for val in RGB:
    GPIO.setup(val,GPIO.OUT)

def main():
  led_setup()
  for val in RGB:
    GPIO.output(val,RGB_ENABLE)
    print("LED ON")
    time.sleep(5)
    GPIO.output(val,RGB_DISABLE)
    print("LED OFF")

try:
  main()
finally:
  GPIO.cleanup()
  print("Closed Everything. END")
#End

RPi.GPIO库需要sudo权限来访问 GPIO 引脚硬件,因此你需要使用以下命令运行脚本:

sudo python3 ledtest.py

当你运行脚本时,你应该看到 LED 的红色、绿色和蓝色部分(或每个 LED,如果使用单独的 LED)依次点亮。如果没有,请检查你的接线,或者通过临时将红色、绿色或蓝色线连接到 3V3 引脚(GPIO 引脚 1)来确认 LED 是否工作。

提示

对于大多数与硬件相关的脚本,需要使用sudo命令,因为用户通常不会在如此低级别直接控制硬件。例如,设置或清除 SD 卡控制器的一部分控制引脚可能会损坏正在写入的数据。因此,出于安全考虑,需要超级用户权限以防止程序意外(或恶意意图)使用硬件。

它是如何工作的…

要使用 Python 访问 GPIO 引脚,我们导入RPi.GPIO,这允许通过模块函数直接控制引脚。我们还需要time模块来暂停程序设定的时间数秒。

我们为 LED 布线和活动状态定义值(见更多内容…部分中的控制 GPIO 电流)。

在程序使用 GPIO 引脚之前,我们需要通过指定编号方法(GPIO.BOARD)和方向(GPIO.OUTGPIO.IN)(在这种情况下,我们将所有 RGB 引脚设置为输出)来设置它们。如果将引脚配置为输出,我们将能够设置引脚状态;同样,如果将其配置为输入,我们将能够读取引脚状态。

接下来,我们使用GPIO.output()通过指定 GPIO 引脚的编号和我们希望其处于的状态(1 = 高/开和0 = 低/关)来控制引脚。我们打开每个 LED,等待 5 秒钟,然后将其关闭。

最后,我们使用GPIO.cleanup()将 GPIO 引脚恢复到其原始默认状态,并释放引脚以供其他程序使用。

更多内容…

在 Raspberry Pi 上使用 GPIO 引脚时必须小心,因为这些引脚直接连接到 Raspberry Pi 的主处理器,没有任何额外的保护。必须小心操作,因为任何错误的接线都可能损坏 Raspberry Pi 处理器,并导致其完全停止工作。

或者,您可以使用许多可直接插入 GPIO 引脚头部的模块之一(减少接线错误的机会)。

小贴士

例如,Pi-Stop 是一个简单的预构建 LED 板,模拟一组交通信号灯,旨在为那些对控制硬件感兴趣但想避免损坏 Raspberry Pi 的风险的人提供一个跳板。在掌握基础知识之后,它也是一个出色的指示器,有助于调试。

只需确保更新ledtest.py脚本中的LED CONFIG引脚引用,以便引用您所使用的硬件的引脚布局和位置。

更多内容…

请参阅附录,硬件和软件列表,以获取 Raspberry Pi 硬件零售商的列表。

控制 GPIO 电流

每个 GPIO 引脚在烧毁之前只能处理一定量的电流(单个引脚不超过 16 mA,总电流不超过 30 mA),同样,RGB LED 也应限制在不超过 100 mA。通过在 LED 之前或之后添加电阻,我们可以限制通过它的电流,并控制其亮度(电流越大,LED 越亮)。

由于我们可能希望同时驱动多个 LED,我们通常试图将电流设置得尽可能低,同时仍然提供足够的功率来点亮 LED。

我们可以使用欧姆定律来告诉我们需要使用多少电阻来提供特定的电流。该定律如下所示:

控制 GPIO 电流

欧姆定律描述了电路中电流、电阻和电压之间的关系

我们将目标设定为最小电流(3 mA)和最大电流(16 mA),同时仍然从每个 LED 产生足够亮的光。为了得到 RGB LED 的平衡输出,我测试了不同的电阻,直到它们提供接近白色的光(通过卡片观察)。每个 LED 选择了 470 欧姆的电阻(你的 LED 可能略有不同)。

控制 GPIO 电流

需要电阻来限制通过 LED 的电流

电阻两端的电压等于 GPIO 电压(Vgpio = 3.3V)减去特定 LED 上的电压降(Vfwd);然后我们可以使用这个电阻来计算每个 LED 使用的电流,如下图中所示:

控制 GPIO 电流

我们可以计算每个 LED 的电流消耗

响应按钮

许多使用树莓派的应用程序都需要在没有键盘和屏幕连接的情况下激活操作。GPIO 引脚为树莓派提供了一个非常好的方式,通过你的按钮和开关来控制,而不需要鼠标/键盘和屏幕。

准备就绪

你将需要以下设备:

  • 2 x 杜邦公对母跳线

  • 小型面包板(170 个接线点)或更大的一个

  • 按钮开关(瞬间闭合)或通过导线连接来闭合/断开电路

  • 面包板导线(实心芯)

  • 1k 欧姆电阻

开关如下图中所示:

准备就绪

按钮开关和其他类型的开关

注意

在以下示例中使用的开关是单刀单掷SPST)瞬间闭合按钮开关。单刀SP)意味着有一组触点会建立连接。在这里使用的按钮开关的情况下,每侧的腿通过中间的单刀开关连接在一起。双刀DP)开关的行为就像单刀开关一样,除了两侧在电路上是分开的,允许你同时打开/关闭两个不同的组件。

单掷ST)表示开关将仅在一种位置上建立连接;另一侧将保持断开。双掷DT)表示开关的两个位置都将连接到不同的部分。

瞬间闭合意味着按钮在被按下时会闭合开关,并在释放时自动打开。锁定按钮开关将保持闭合,直到再次按下。

准备就绪

按钮电路布局

在这个例子中,我们将使用声音,因此你还需要将扬声器或耳机连接到树莓派的音频插座。

你需要使用以下命令安装名为flite的程序,这将使我们能够让树莓派说话:

sudo apt-get install flite

安装完成后,你可以使用以下命令进行测试:

sudo flite -t "hello I can talk"

如果声音有点太小(或太大),你可以使用以下命令调整音量(0-100%):

amixer set PCM 100%

如何做...

创建btntest.py脚本如下:

#!/usr/bin/python3
#btntest.py
import time
import os
import RPi.GPIO as GPIO
#HARDWARE SETUP
# GPIO
# 2[==X==1=======]26[=======]40
# 1[=============]25[=======]39
#Button Config
BTN = 12

def gpio_setup():
  #Setup the wiring
  GPIO.setmode(GPIO.BOARD)
  #Setup Ports
  GPIO.setup(BTN,GPIO.IN,pull_up_down=GPIO.PUD_UP)

def main():
  gpio_setup()
  count=0
  btn_closed = True
  while True:
    btn_val = GPIO.input(BTN)
    if btn_val and btn_closed:
       print("OPEN")
       btn_closed=False
    elif btn_val==False and btn_closed==False:
       count+=1
       print("CLOSE %s" % count)
       os.system("flite -t '%s'" % count)
       btn_closed=True
    time.sleep(0.1)

try:
  main()
finally:
  GPIO.cleanup()
  print("Closed Everything. END")
#End

它是如何工作的...

如前一个配方中所述,我们设置了所需的 GPIO 引脚,但这次将其设置为输入,并且我们还启用了内部拉电阻(有关更多信息,请参阅本配方中“还有更多...”部分的拉电阻和下拉电阻电路),使用以下代码:

GPIO.setup(BTN,GPIO.IN,pull_up_down=GPIO.PUD_UP)

在设置 GPIO 引脚后,我们创建一个循环,该循环将连续检查BTN的状态,使用GPIO.input()。如果返回的值是false,则引脚通过开关连接到 0V(地),我们将使用flite大声计数每次按钮被按下。

由于我们在try/finally条件内调用了主函数,即使我们使用Ctrl + Z关闭程序,它仍然会调用GPIO.cleanup()

注意

我们在循环中使用短暂的延迟;这确保了忽略开关接触上的任何噪声。这是因为当我们按下按钮时,我们按下或释放它时并不总是完美的接触,如果我们太快地再次按下它,可能会产生几个触发。这被称为软件去抖动;我们在这里忽略信号的抖动。

还有更多...

树莓派的 GPIO 引脚必须谨慎使用;用于输入的电压应在特定范围内,并且应通过保护电阻最小化从它们中吸取的任何电流。

安全电压

我们必须确保我们只连接介于 0(地)和 3.3V 之间的输入。一些处理器使用 0 到 5V 之间的电压,因此需要额外的组件才能安全地与之接口。除非你确定它是安全的,否则不要连接使用 5V 的输入或组件,否则会损坏树莓派的 GPIO 端口。

拉电阻和下拉电阻电路

之前的代码将 GPIO 引脚设置为使用内部拉电阻。如果没有在 GPIO 引脚上使用拉电阻(或下拉电阻),电压可以自由浮动在 3.3V 和 0V 之间,实际的逻辑状态将不确定(有时为 1,有时为 0)。

树莓派的内部拉电阻为 50k 欧姆-65k 欧姆,下拉电阻也是 50k 欧姆-65k 欧姆。外部拉电阻/下拉电阻通常用于 GPIO 电路(如下所示),通常使用 10k 欧姆或更大的电阻,原因类似(在不活动时提供非常小的电流消耗)。

拉电阻允许一小部分电流通过 GPIO 引脚,当开关未按下时,将提供高电压。当按下开关时,小电流被流向 0V 的大电流所取代,因此 GPIO 引脚上的电压变低。开关在按下时是低电平且逻辑 0。它的工作原理如下所示:

拉电阻和下拉电阻电路

拉电阻电路

下拉电阻的工作方式相同,只是开关是高电平有效(当按下时 GPIO 引脚为逻辑 1)。它的工作方式如下所示:

上拉和下拉电阻电路

一个下拉电阻电路

保护电阻

除了开关外,电路还包括一个与开关串联的电阻来保护 GPIO 引脚,如下所示:

保护电阻

一个 GPIO 保护限流电阻

保护电阻的目的是在 GPIO 引脚意外设置为输出而不是输入时保护 GPIO 引脚。想象一下,例如,我们的开关连接在 GPIO 和地之间。现在,GPIO 引脚被设置为输出并在按下开关时立即打开(将其驱动到 3.3V)。如果没有电阻,GPIO 引脚将直接连接到 0V。GPIO 仍然会尝试将其驱动到 3.3V;这会导致 GPIO 引脚烧毁(因为它会使用过多的电流将引脚驱动到高电平状态)。如果我们在这里使用一个 1k 欧姆的电阻,引脚就能够使用可接受的电流驱动到高电平(I = V/R = 3.3/1k = 3.3mA)。

一个控制关断按钮

Raspberry Pi 应该始终正确关机,以避免 SD 卡在执行写入操作时因断电而损坏。如果你没有连接键盘或屏幕(如果你正在运行自动化程序或通过网络远程控制并忘记关闭它),这可能会造成问题,因为你无法输入命令或看到你在做什么。通过添加我们自己的按钮和 LED 指示灯,我们可以轻松地发出关机、重置和再次启动的命令,以指示系统何时处于活动状态。

准备中

你需要以下设备:

  • 3 x 杜邦公对母排线

  • 小型面包板(170 个接线点)或更大的一个

  • 按钮开关(瞬间闭合)

  • 通用 LED

  • 2 x 470 欧姆电阻

  • 面包板线(实心芯线)

关断电路的整体布局将如下所示:

准备中

控制关断电路布局

如何做…

创建shtdwn.py脚本如下:

#!/usr/bin/python3
#shtdwn.py
import time
import RPi.GPIO as GPIO
import os

# Shutdown Script
DEBUG=True #Simulate Only
SNDON=True
#HARDWARE SETUP
# GPIO
# 2[==X==L=======]26[=======]40
# 1[===1=========]25[=======]39

#BTN CONFIG - Set GPIO Ports
GPIO_MODE=GPIO.BOARD
SHTDWN_BTN = 7 #1
LED = 12       #L

def gpio_setup():
  #Setup the wiring
  GPIO.setmode(GPIO_MODE)
  #Setup Ports
  GPIO.setup(SHTDWN_BTN,GPIO.IN,pull_up_down=GPIO.PUD_UP)
  GPIO.setup(LED,GPIO.OUT)

def doShutdown():
  if(DEBUG):print("Press detected")
  time.sleep(3)
  if GPIO.input(SHTDWN_BTN):
    if(DEBUG):print("Ignore the shutdown (<3sec)")
  else:
    if(DEBUG):print ("Would shutdown the RPi Now")
    GPIO.output(LED,0)
    time.sleep(0.5)
    GPIO.output(LED,1)
    if(SNDON):os.system("flite -t 'Warning commencing power down 3 2 1'")
    if(DEBUG==False):os.system("sudo shutdown -h now")
    if(DEBUG):GPIO.cleanup()
    if(DEBUG):exit()

def main():
  gpio_setup()
  GPIO.output(LED,1)
  while True:
    if(DEBUG):print("Waiting for >3sec button press")
    if GPIO.input(SHTDWN_BTN)==False:
       doShutdown()
    time.sleep(1)

try:
  main()
finally:
  GPIO.cleanup()
  print("Closed Everything. END")
#End

要使此脚本自动运行(一旦测试通过),我们可以将其放置在~/bin中(如果我们只想复制它,可以使用cp而不是mv)并将其添加到crontab中,如下所示:

mkdir ~/bin
mv shtdwn.py ~/bin/shtdwn.py 
crontab –e

在文件末尾,我们添加以下代码:

@reboot sudo python3 ~/bin/shtdwn.py

它是如何工作的…

这次,当我们设置 GPIO 引脚时,我们将连接到关断按钮的引脚定义为输入,将连接到 LED 的引脚定义为输出。我们打开 LED 以指示系统正在运行。

通过将DEBUG标志设置为True,我们可以在不实际关机的情况下测试脚本的功能(通过读取终端消息);我们只需确保在使用脚本时将DEBUG设置为False

我们进入一个while循环,每秒检查一次 GPIO 引脚是否设置为LOW(开关已被按下);如果是,我们进入doShutdown()函数。

程序将等待 3 秒钟,然后再次测试以查看按钮是否仍在被按下。如果按钮不再被按下,我们将返回到之前的while循环。然而,如果在 3 秒后按钮仍然被按下,程序将闪烁 LED 并触发关机(同时使用flite提供音频警告)。

当我们对我们脚本的运行方式满意时,我们可以禁用DEBUG标志(将其设置为False)并将脚本添加到crontab。Crontab 是一个特殊的程序,它在后台运行,允许我们在系统启动时(@reboot)安排(在特定时间、日期或定期)程序和动作。这允许脚本在每次树莓派上电时自动启动。当我们按下并保持关机按钮超过 3 秒时,它会安全地关闭系统并进入低功耗状态(LED 在此之前关闭,表示可以安全地在之后不久断电)。要重启树莓派,我们短暂地断电;这将重启系统,当树莓派加载时 LED 会点亮。

还有更多…

我们可以通过使用复位引脚来进一步扩展此示例,通过添加额外功能并利用额外的 GPIO 连接(如果可用)。

重置和重启树莓派

树莓派有孔可以安装复位引脚(在树莓派 2/3 上标记为RUN,在树莓派 1 Model B Rev 2 和 Model As 上标记为P6)。复位引脚允许使用按钮重置设备,而不是每次都拔掉微型 USB 连接器来循环电源:

重置和重启树莓派

树莓派复位引脚 – 左边是树莓派 Model A/B(Rev2),右边是树莓派 2

要使用它,您需要将电线或引脚头焊接在树莓派上,并将其连接到按钮上(或每次短暂触摸两个孔之间的电线)。或者,我们可以扩展我们之前的电路,如下面的图所示:

重置和重启树莓派

控制关机电路布局和复位按钮

我们可以将这个额外的按钮添加到我们的电路中,该按钮可以连接到复位引脚(这是树莓派 2 上靠近中间的孔,在其他型号上靠近边缘的孔)。当通过连接到地(例如旁边的孔或 GPIO 引脚 6 等另一个地点)临时将其拉低时,该引脚将重置树莓派,并在关机后允许其重新启动。

添加额外功能

由于我们现在始终在监控关机按钮的脚本,我们可以添加额外的按钮/开关/跳线以同时监控。这将允许我们通过更改输入来触发特定程序或设置特定状态。以下示例允许我们轻松地在自动 DHCP 网络(默认网络设置)和使用直接 IP 地址之间切换,正如在第一章中“直接连接到笔记本电脑或计算机”食谱中使用的,用于直接 LAN 连接。

向前面的电路添加以下组件:

  • 一个 470 欧姆的电阻

  • 两个带有跳线连接器的引脚头(或可选的开关)

  • 面包板电线(实心芯)

添加了前面的组件后,我们的控制关机电路现在看起来如下:

添加额外功能

控制关机电路布局、复位按钮和跳线引脚

在前面的脚本中,我们添加了一个额外的输入来检测LAN_SWA引脚的状态(我们添加到电路中的跳线引脚)使用以下代码:

LAN_SWA = 11    #2

确保在gpio_setup()函数中将它设置为输入(带有上拉电阻)使用以下代码:

  GPIO.setup(LAN_SWA,GPIO.IN,pull_up_down=GPIO.PUD_UP)

添加一个新函数以在 LAN 模式之间切换,并读取新的 IP 地址。doChangeLAN()函数检查自上次调用以来LAN_SWA引脚的状态是否已更改,如果是,则将网络适配器设置为 DHCP 或相应地设置直接 LAN 设置(如果可用,则使用flite说出新的 IP 设置)。最后,设置为直接连接的 LAN 会导致 LED 在此模式下缓慢闪烁。使用以下代码执行此操作:

def doChangeLAN(direct):
  if(DEBUG):print("Direct LAN: %s" % direct)
  if GPIO.input(LAN_SWA) and direct==True:
    if(DEBUG):print("LAN Switch OFF")
    cmd="sudo dhclient eth0"
    direct=False
    GPIO.output(LED,1)
  elif GPIO.input(LAN_SWA)==False and direct==False:
    if(DEBUG):print("LAN Switch ON")
    cmd="sudo ifconfig eth0 169.254.69.69"
    direct=True
  else:
    return direct
  if(DEBUG==False):os.system(cmd)
  if(SNDON):os.system("hostname -I | flite")
  return direct

添加另一个函数flashled(),每次调用时只需切换 LED 的状态。此函数的代码如下:

def flashled(ledon):
  if ledon:
    ledon=False
  else:
    ledon=True
  GPIO.output(LED,ledon)
  return ledon

最后,我们调整主循环以也调用doChangeLAN()并使用结果来决定是否使用ledon调用flashled()以跟踪 LED 的先前状态。现在main()函数应该更新如下:

def main():
  gpio_setup()
  GPIO.output(LED,1)
  directlan=False
  ledon=True
  while True:
    if(DEBUG):print("Waiting for >3sec button press")
    if GPIO.input(SHTDWN_BTN)==False:
       doShutdown()
    directlan= doChangeLAN(directlan)
    if directlan:
      flashled(ledon)
    time.sleep(1)

GPIO 键盘输入

我们已经看到我们可以如何监控 GPIO 上的输入以启动应用程序和控制树莓派;然而,有时我们需要控制第三方程序。使用uInput库,我们可以模拟键盘按键(甚至鼠标移动)来控制任何程序,使用我们自己的定制硬件。

更多关于使用uInput的信息,请访问tjjr.fi/sw/python-uinput/

准备工作

执行以下步骤以安装uInput

  1. 首先,我们需要下载uInput

    您需要使用以下命令从 GitHub 下载uInput Python 库(约 50 KB):

    wget https://github.com/tuomasjjrasanen/python-uinput/archive/master.zip
    unzip master.zip
    
    

    库将解压到名为python-uinput-master的目录。

    完成后,您可以使用以下命令删除 ZIP 文件:

    rm master.zip
    
    
  2. 使用以下命令安装所需的软件包(如果您已经安装了它们,apt-get 命令将忽略它们):

    sudo apt-get install python3-setuptools python3-dev
    sudo apt-get install libudev-dev
    
    
  3. 使用以下命令编译和安装 uInput

    cd python-uinput-master
    sudo python3 setup.py install
    
    
  4. 最后,我们使用以下命令加载新的 uinput 内核模块:

    sudo modprobe uinput
    
    

    为了确保它在启动时加载,我们可以使用以下命令将 uinput 添加到 modules 文件中:

    sudo nano /etc/modules
    
    

    在文件中将 uinput 放在新的一行,并保存它(Ctrl + X, Y)。

  5. 使用以下设备创建以下电路:

    • 面包板(半尺寸或更大)

    • 7 x 杜邦公对母排线

    • 六个按键

    • 6 x 470 欧姆电阻

    • 面包板线(实心芯线)

    准备中

    GPIO 键盘电路布局

    键盘电路也可以通过将组件焊接到一个 Vero 原型板(也称为条形板)中而永久构建,如图所示:

    准备中

    GPIO 键盘 Pi 硬件模块

    注意

    此电路可作为 PiHardware.com 的自焊套件获得。

  6. 按照以下方式将电路连接到 Raspberry Pi 的 GPIO 引脚:

    按钮 GPIO 引脚
    GND 6
    v B_DOWN 22
    < B_LEFT 18
    ^ B_UP 15
    > B_RIGHT 13
    1 B_1 11
    2 B_2 7

如何操作...

创建以下 gpiokeys.py 脚本:

#!/usr/bin/python3
#gpiokeys.py
import time
import RPi.GPIO as GPIO
import uinput

#HARDWARE SETUP
# GPIO
# 2[==G=====<=V==]26[=======]40
# 1[===2=1>^=====]25[=======]39
B_DOWN  = 22    #V
B_LEFT  = 18   #<
B_UP    = 15   #^
B_RIGHT = 13   #>
B_1  = 11   #1
B_2  = 7   #2

DEBUG=True
BTN = [B_UP,B_DOWN,B_LEFT,B_RIGHT,B_1,B_2]
MSG = ["UP","DOWN","LEFT","RIGHT","1","2"]

#Setup the DPad module pins and pull-ups
def dpad_setup():
  #Set up the wiring
  GPIO.setmode(GPIO.BOARD)
  # Setup BTN Ports as INPUTS
  for val in BTN:
    # set up GPIO input with pull-up control
    #(pull_up_down can be:
    #    PUD_OFF, PUD_UP or PUD_DOWN, default PUD_OFF)
    GPIO.setup(val, GPIO.IN, pull_up_down=GPIO.PUD_UP)

def main():
  #Setup uinput
  events = (uinput.KEY_UP,uinput.KEY_DOWN,uinput.KEY_LEFT,
           uinput.KEY_RIGHT,uinput.KEY_ENTER,uinput.KEY_ENTER)
  device = uinput.Device(events)
  time.sleep(2) # seconds
  dpad_setup()
  print("DPad Ready!")

  btn_state=[False,False,False,False,False,False]
  key_state=[False,False,False,False,False,False]
  while True:
    #Catch all the buttons pressed before pressing the related keys
    for idx, val in enumerate(BTN):
      if GPIO.input(val) == False:
        btn_state[idx]=True
      else:
        btn_state[idx]=False

    #Perform the button presses/releases (but only change state once)
    for idx, val in enumerate(btn_state):
      if val == True and key_state[idx] == False:
        if DEBUG:print (str(val) + ":" + MSG[idx])
        device.emit(events[idx], 1) # Press.
        key_state[idx]=True
      elif val == False and key_state[idx] == True:
        if DEBUG:print (str(val) + ":!" + MSG[idx])
        device.emit(events[idx], 0) # Release.
        key_state[idx]=False

    time.sleep(.1)

try:
  main()
finally:
  GPIO.cleanup()
#End

工作原理...

首先,我们导入 uinput 并定义键盘按钮的布线。对于 BTN 中的每个按钮,我们将其启用为输入,并启用内部上拉电阻。

接下来,我们设置 uinput,定义我们想要模拟的按键并将它们添加到 uinput.Device() 函数中。我们等待几秒钟以允许 uinput 初始化,设置初始按钮和按键状态,并开始我们的 main 循环。

main 循环分为两部分:第一部分检查按钮并记录在 btn_state 中的状态,第二部分将 btn_state 与当前的 key_state 数组进行比较。这样,我们可以检测 btn_state 的变化并调用 device.emit() 来切换按键的状态。

为了允许我们在后台运行此脚本,我们可以使用 & 如以下命令所示运行它:

sudo python3 gpiokeys.py &

注意

& 字符允许命令在后台运行,因此我们可以继续使用命令行运行其他程序。您可以使用 fg 将其带到前台,或者如果您有多个命令正在运行,可以使用 %1%2 等等。使用 jobs 获取列表。

您甚至可以通过按 Ctrl + Z 暂停进程/程序以获取命令提示符,然后使用 bg(这将允许它在后台运行)来恢复它。

您可以使用第四章中创建顶部滚动游戏食谱中创建的游戏来测试按键,您现在可以使用 GPIO 方向垫来控制它。别忘了,如果您是通过远程连接到 Raspberry Pi,任何按键操作都只会激活本地连接的屏幕。

更多内容…

我们可以使用uinput来为其他程序提供硬件控制,包括那些需要鼠标输入的程序。

生成其他按键组合

您可以在文件中创建多个不同的键映射来支持不同的程序。例如,events_z80键映射对于fuze这样的 Spectrum 模拟器非常有用(有关详细信息,请浏览raspi.tv/2012/how-to-install-fuse-zx-spectrum-emulator-on-raspberry-pi)。events_omx键映射适合通过以下命令控制通过 OMX Player 播放的视频:

omxplayer filename.mp4

您可以使用-k参数获取omxplayer支持的按键列表。

将定义events列表的行替换为新键映射,并通过以下代码将不同的映射分配给事件:

events_dpad = (uinput.KEY_UP,uinput.KEY_DOWN,uinput.KEY_LEFT,
              uinput.KEY_RIGHT,uinput.KEY_ENTER,uinput.KEY_ENTER)
events_z80 = (uinput.KEY_Q,uinput.KEY_A,uinput.KEY_O,
             uinput.KEY_P,uinput.KEY_M,uinput.KEY_ENTER)
events_omx = (uinput.KEY_EQUAL,uinput.KEY_MINUS,uinput.KEY_LEFT,
             uinput.KEY_RIGHT,uinput.KEY_P,uinput.KEY_Q)

您可以在input.h文件中找到所有的KEY定义;您可以使用less命令查看它(按Q退出),如下所示:

less /usr/include/linux/input.h

模拟鼠标事件

uinput库可以模拟鼠标和摇杆事件以及键盘按键。要使用按钮模拟鼠标,我们可以调整脚本以使用鼠标事件(以及定义mousemove来设置移动的步长)使用以下代码:

MSG = ["M_UP","M_DOWN","M_LEFT","M_RIGHT","1","Enter"]
events_mouse=(uinput.REL_Y,uinput.REL_Y, uinput.REL_X,
             uinput.REL_X,uinput.BTN_LEFT,uinput.BTN_RIGHT)
mousemove=1

我们还需要修改按钮处理,以提供连续的运动,因为我们不需要跟踪鼠标按键的状态。为此,请使用以下代码:

    #Perform the button presses/releases
    #(but only change state once)
    for idx, val in enumerate(btn_state):
      if MSG[idx] == "M_UP" or MSG[idx] == "M_LEFT":
        state = -mousemove
      else:
        state = mousemove
      if val == True:
        device.emit(events[idx], state) # Press.
      elif val == False:
        device.emit(events[idx], 0) # Release.

    time.sleep(0.01)

复用彩色 LED

本章的下一个示例演示了,如果用软件控制,一些看似简单的硬件可以产生一些令人印象深刻的结果。我们回到使用一些 RGB LED,这些 LED 的连接方式使得我们只需要使用八个 GPIO 引脚,通过一种称为硬件复用(见本食谱更多内容部分的硬件复用子部分)的方法来控制五个 RGB LED 的红色、绿色和蓝色元素。

准备就绪

您需要以下图片中显示的 RGB LED 模块:

准备就绪

来自 PiHardware.com 的 RGB LED 模块

如前图所示,来自PiHardware.com的 RGB LED 模块附带 GPIO 引脚和 Dupont 公对公电缆,用于连接。尽管有两套标有 1 到 5 的引脚,但只需连接其中一边。

或者,您可以使用以下电路重新创建自己的电路,该电路使用五个常见的阴极 RGB LED、3 个 470 欧姆电阻和一个 Vero 原型板(或大型面包板)。电路将如下所示:

准备就绪

RGB LED 模块电路图

注意

严格来说,在这个电路中我们应该使用 15 个电阻(每个 RGB LED 元件一个),这将避免 LED 共享相同电阻时的干扰,并且如果同时开启,也会延长 LED 本身的使用寿命。然而,这种优势微乎其微,尤其是当我们打算独立驱动每个 RGB LED 以实现多彩效果时。

您需要将电路连接到 Raspberry Pi 的 GPIO 引脚,如下所示:

RGB LED 1 2 3 4
Rpi GPIO 引脚 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40
Rpi GPIO 引脚 1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31 33 35 37 39
RGB LED 5 R G B

如何做…

创建rgbled.py脚本并执行以下步骤:

  1. 使用以下代码导入所有必需的模块并定义要使用的值:

    #!/usr/bin/python3
    #rgbled.py
    import time
    import RPi.GPIO as GPIO
    
    #Setup Active states
    #Common Cathode RGB-LEDs (Cathode=Active Low)
    LED_ENABLE = 0; LED_DISABLE = 1
    RGB_ENABLE = 1; RGB_DISABLE = 0
    #HARDWARE SETUP
    # GPIO
    # 2[=====1=23=4==]26[=======]40
    # 1[===5=RGB=====]25[=======]39
    #LED CONFIG - Set GPIO Ports
    LED1 = 12; LED2 = 16; LED3 = 18; LED4 = 22; LED5 = 7
    LED = [LED1,LED2,LED3,LED4,LED5]
    RGB_RED = 11; RGB_GREEN = 13; RGB_BLUE = 15
    RGB = [RGB_RED,RGB_GREEN,RGB_BLUE]
    #Mixed Colors
    RGB_CYAN = [RGB_GREEN,RGB_BLUE]
    RGB_MAGENTA = [RGB_RED,RGB_BLUE]
    RGB_YELLOW = [RGB_RED,RGB_GREEN]
    RGB_WHITE = [RGB_RED,RGB_GREEN,RGB_BLUE]
    RGB_LIST = [RGB_RED,RGB_GREEN,RGB_BLUE,RGB_CYAN,
                RGB_MAGENTA,RGB_YELLOW,RGB_WHITE]
    
  2. 使用以下代码定义设置 GPIO 引脚的函数:

    def led_setup():
      '''Setup the RGB-LED module pins and state.'''
      #Set up the wiring
      GPIO.setmode(GPIO.BOARD)
      # Setup Ports
      for val in LED:
        GPIO.setup(val, GPIO.OUT)
      for val in RGB:
        GPIO.setup(val, GPIO.OUT)
      led_clear()
    
  3. 使用以下代码定义我们的实用函数,以帮助控制 LED:

    def led_gpiocontrol(pins,state):
      '''This function will control the state of
      a single or multiple pins in a list.'''
      #determine if "pins" is a single integer or not
      if isinstance(pins,int):
        #Single integer - reference directly
        GPIO.output(pins,state)
      else:
        #if not, then cycle through the "pins" list
        for i in pins:
          GPIO.output(i,state)
    
    def led_activate(led,color):
      '''Enable the selected led(s) and set the required color(s)
      Will accept single or multiple values'''
      #Enable led
      led_gpiocontrol(led,LED_ENABLE)
      #Enable color
      led_gpiocontrol(color,RGB_ENABLE)
    
    def led_deactivate(led,color):
      '''Deactivate the selected led(s) and set the required
      color(s) will accept single or multiple values'''
      #Disable led
      led_gpiocontrol(led,LED_DISABLE)
      #Disable color
      led_gpiocontrol(color,RGB_DISABLE)
    
    def led_time(led, color, timeon):
      '''Switch on the led and color for the timeon period'''
      led_activate(led,color)
      time.sleep(timeon)
      led_deactivate(led,color)
    
    def led_clear():
      '''Set the pins to default state.'''
      for val in LED:
        GPIO.output(val, LED_DISABLE)
      for val in RGB:
        GPIO.output(val, RGB_DISABLE)
    
    def led_cleanup():
      '''Reset pins to default state and release GPIO'''
      led_clear()
      GPIO.cleanup()
    
  4. 创建一个测试函数以演示模块的功能:

    def main():
      '''Directly run test function.
      This function will run if the file is executed directly'''
      led_setup()
      led_time(LED1,RGB_RED,5)
      led_time(LED2,RGB_GREEN,5)
      led_time(LED3,RGB_BLUE,5)
      led_time(LED,RGB_MAGENTA,2)
      led_time(LED,RGB_YELLOW,2)
      led_time(LED,RGB_CYAN,2) 
    
    if __name__=='__main__':
      try:
        main()
      finally:
        led_cleanup()
    #End
    

它是如何工作的…

首先,我们通过定义根据使用的 RGB LED(公共阴极)类型来启用禁用LED 所需的状态来定义硬件设置。如果您使用的是公共阳极设备,只需反转启用/禁用状态即可。

接下来,我们定义 GPIO 到引脚的映射,以匹配我们之前所做的布线。

我们还通过组合红色、绿色和/或蓝色来定义一些基本的颜色组合,如下所示:

它是如何工作的…

LED 颜色组合

我们定义了一系列有用的函数,第一个是led_setup(),它将 GPIO 编号设置为GPIO.BOARD并定义所有用作输出的引脚。我们还调用一个名为led_clear()的函数,该函数将引脚设置为默认状态,所有引脚均被禁用。

注意

这意味着 LED 引脚 1-5(每个 LED 的公共阴极)被设置为HIGH,而 RGB 引脚(每个颜色的独立阳极)被设置为LOW

我们创建了一个名为 led_gpiocontrol() 的函数,它将允许我们设置一个或多个引脚的状态。isinstance() 函数允许我们测试一个值以查看它是否与特定类型匹配(在这种情况下,一个单独的整数);然后我们可以设置该单个引脚的状态,或者遍历引脚列表并设置每个引脚。

接下来,我们定义两个函数,led_activate()led_deactivate(),它们将启用和禁用指定的 LED 和颜色。最后,我们定义 led_time(),它将允许我们指定一个 LED、颜色和时间来开启它。

我们还创建了 led_cleanup() 函数来将引脚(和 LED)重置为默认值,并调用 GPIO.cleanup() 来释放使用的 GPIO 引脚。

此脚本旨在成为一个库文件,因此我们将使用 if __name__=='__main__' 检查来仅在直接运行文件时执行我们的测试代码:

注意

通过检查 __name__ 的值,我们可以确定文件是直接运行的(它将等于 __main__)还是被另一个 Python 脚本导入的。

这允许我们定义仅在直接加载和运行文件时执行的特定测试代码。如果我们将此文件作为模块包含在其他脚本中,则此代码将不会执行。

我们在 第二章 的 There's more… 部分和 Working with text and strings 菜单中使用了这种技术。

如前所述,我们将使用 try/finally 来确保我们总是能够执行清理操作,即使我们在早期退出。

为了测试脚本,我们将设置 LED 依次以各种颜色发光。

还有更多...

我们可以通过同时开启 RGB LED 的一个或多个部分来创建几种不同的颜色。然而,通过一些巧妙的编程,我们可以创建整个光谱的颜色。此外,我们可以在每个 LED 上显示不同的颜色,看起来似乎是同时进行的。

硬件复用

一个 LED 需要在阳极侧施加高电压,在阴极侧施加低电压才能发光。电路中使用的 RGB LED 是共阴极,因此我们必须在 RGB 引脚上施加高电压(3.3V),在阴极引脚上施加低电压(0V)(每个 LED 连接到 1 到 5 号引脚)。

阴极和 RGB 引脚状态如下:

硬件复用

阴极和 RGB 引脚状态

因此,我们可以启用一个或多个 RGB 引脚,但仍能控制哪些 LED 是亮的。我们启用我们想要点亮的 LED 的引脚,并禁用我们不需要的引脚。这允许我们使用比单独控制每个 15 个 RGB 线所需的引脚数量少得多。

显示随机图案

我们可以向我们的库添加新功能以产生不同的效果,例如生成随机颜色。以下函数使用 randint() 获取 1 到颜色数量的值。我们忽略任何超过可用颜色数量的值,这样我们可以控制 LED 灯关闭的频率。执行以下步骤添加所需的函数:

  1. randint() 函数从 random 模块添加到 rgbled.py 脚本中,使用以下代码:

    from random import randint
    
  2. 现在添加 led_rgbrandom() 使用以下代码:

    def led_rgbrandom(led,period,colors):
       ''' Light up the selected led, for period in seconds,
       in one of the possible colors. The colors can be
       1 to 3 for RGB, or 1-6 for RGB plus combinations,
       1-7 includes white. Anything over 7 will be set as
       OFF (larger the number more chance of OFF).''' 
      value = randint(1,colors)
      if value < len(RGB_LIST):
        led_time(led,RGB_LIST[value-1],period)
    
  3. main() 函数中使用以下命令创建一系列闪烁的 LED:

      for i in range(20):
        for j in LED:
          #Select from all, plus OFF
          led_rgbrandom(j,0.1,20)
    

混合多种颜色

到目前为止,我们一次只在单个或多个 LED 上显示一种颜色。如果你考虑电路是如何连接的,你可能想知道我们如何让一个 LED 显示一种颜色,而另一个同时显示不同的颜色?简单的答案是,我们不需要——我们只是做得很快!

我们需要做的只是每次显示一种颜色,但非常快速地交替改变,这样颜色看起来就像两种(甚至三种红/绿/蓝 LED 的组合)。幸运的是,这是像 Raspberry Pi 这样的计算机可以非常容易做到的事情,甚至允许我们将 RGB 元素组合起来,使所有五个 LED 显示多种颜色的阴影。执行以下步骤来混合颜色:

  1. rgbled.py 脚本的顶部添加组合颜色定义,在混合颜色定义之后,使用以下代码:

    #Combo Colors
    RGB_AQUA = [RGB_CYAN,RGB_GREEN]
    RGB_LBLUE = [RGB_CYAN,RGB_BLUE]
    RGB_PINK = [RGB_MAGENTA,RGB_RED]
    RGB_PURPLE = [RGB_MAGENTA,RGB_BLUE]
    RGB_ORANGE = [RGB_YELLOW,RGB_RED]
    RGB_LIME = [RGB_YELLOW,RGB_GREEN]
    RGB_COLORS = [RGB_LIME,RGB_YELLOW,RGB_ORANGE,RGB_RED,
                  RGB_PINK,RGB_MAGENTA,RGB_PURPLE,RGB_BLUE,
                  RGB_LBLUE,RGB_CYAN,RGB_AQUA,RGB_GREEN]
    

    上述代码将提供颜色组合以创建我们的阴影,其中 RGB_COLORS 提供了阴影的平滑过渡。

  2. 接下来,我们需要创建一个名为 led_combo() 的函数来处理单色或多色。该函数的代码如下:

    def led_combo(pins,colors,period):
      #determine if "colors" is a single integer or not
      if isinstance(colors,int):
        #Single integer - reference directly
        led_time(pins,colors,period)
      else:
        #if not, then cycle through the "colors" list
        for i in colors:
          led_time(pins,i,period)
    
  3. 现在我们可以创建一个新的脚本,rgbledrainbow.py,来利用我们 rgbled.py 模块中的新功能。rgbledrainbow.py 脚本如下:

    #!/usr/bin/python3
    #rgbledrainbow.py
    import time
    import rgbled as RGBLED
    
    def next_value(number,max):
      number = number % max
      return number
    
    def main():
      print ("Setup the RGB module")
      RGBLED.led_setup()
    
      # Multiple LEDs with different Colors
      print ("Switch on Rainbow")
      led_num = 0
      col_num = 0
      for l in range(5):
        print ("Cycle LEDs")
        for k in range(100):
          #Set the starting point for the next set of colors
          col_num = next_value(col_num+1,len(RGBLED.RGB_COLORS))
          for i in range(20):  #cycle time
            for j in range(5): #led cycle
              led_num = next_value(j,len(RGBLED.LED))
              led_color = next_value(col_num+led_num,
                                     len(RGBLED.RGB_COLORS))
              RGBLED.led_combo(RGBLED.LED[led_num],
                               RGBLED.RGB_COLORS[led_color],0.001)
    
        print ("Cycle COLORs")        
        for k in range(100):
          #Set the next color
          col_num = next_value(col_num+1,len(RGBLED.RGB_COLORS))
          for i in range(20): #cycle time
            for j in range(5): #led cycle
              led_num = next_value(j,len(RGBLED.LED))
              RGBLED.led_combo(RGBLED.LED[led_num],
                               RGBLED.RGB_COLORS[col_num],0.001)
      print ("Finished")
    
    if __name__=='__main__':
      try:
        main()
      finally:
        RGBLED.led_cleanup()
    #End
    

main() 函数将首先循环 LED,将 RGB_COLORS 数组中的每种颜色设置在所有 LED 上。然后,它将循环颜色,在 LED 上创建彩虹效果:

混合多种颜色

在五个 RGB LED 上循环多种颜色

使用视觉持久性书写信息

视觉持久性 (POV) 显示可以产生几乎神奇的效果,通过快速移动一串 LED 灯的来回或圆形旋转,在空中显示图像。这种效果之所以有效,是因为你的眼睛无法快速调整以分离出单个的光闪,因此你观察到的是一个合并的图像(显示的消息或图片)。

使用视觉持久性书写信息

使用 RGB LED 的视觉持久性

准备工作

此配方也使用之前配方中使用的 RGB LED 套件;你还需要以下额外物品:

  • 面板(半尺寸或更大)

  • 2 x 杜邦公对母排线

  • 倾斜开关(球轴承类型适合)

  • 1 x 470 欧姆电阻(R_Protect)

  • 面包板线(实心芯)

倾斜开关应添加到 RGB LED(如 多路复用彩色 LED 菜谱的 准备就绪 部分所述)。倾斜开关的接线如下:

准备就绪

倾斜开关连接到 GPIO 输入(GPIO 引脚 24)和 Gnd(GPIO 引脚 6)

要重现 POV 图像,你需要能够快速移动 LED 和倾斜开关。注意倾斜开关是如何倾斜安装到侧面的,这样当移动到左侧时开关会打开。建议将硬件安装在一根木头或其他类似材料上。你甚至可以使用便携式 USB 电池组和 Wi-Fi 拨号器,通过远程连接为 Raspberry Pi 提供电源和控制(有关详细信息,请参阅 第一章,使用 Raspberry Pi 入门 – 通过 SSH(和 X11 转发)在网络中远程连接到 Raspberry Pi):

准备就绪

视觉持久性硬件设置

你还需要完成的 rgbled.py 文件,我们将在 如何做到这一点… 部分进一步扩展。

如何做到这一点…

创建一个名为 tilt.py 的脚本,以报告倾斜开关的状态:

#!/usr/bin/python3
#tilt.py
import RPi.GPIO as GPIO
#HARDWARE SETUP
# GPIO
# 2[===========T=]26[=======]40
# 1[=============]25[=======]39
#Tilt Config
TILT_SW = 24

def tilt_setup():
  #Setup the wiring
  GPIO.setmode(GPIO.BOARD)
  #Setup Ports
  GPIO.setup(TILT_SW,GPIO.IN,pull_up_down=GPIO.PUD_UP)

def tilt_moving():
  #Report the state of the Tilt Switch
  return GPIO.input(TILT_SW)

def main():
  import time
  tilt_setup()
  while True:
    print("TILT %s"% (GPIO.input(TILT_SW)))
    time.sleep(0.1)

if __name__=='__main__':
  try:
    main()
  finally:
    GPIO.cleanup()
    print("Closed Everything. END")
#End

你可以通过以下命令直接运行脚本来测试脚本:

sudo python3 tilt.py

将以下 rgbled_pov() 函数添加到我们之前创建的 rgbled.py 脚本中;这将使我们能够显示图像的单行:

def rgbled_pov(led_pattern,color,ontime):
  '''Disable all the LEDs and re-enable the LED pattern in the required color'''
  led_deactivate(LED,RGB)
  for led_num,col_num in enumerate(led_pattern):
    if col_num >= 1:
      led_activate(LED[led_num],color)
  time.sleep(ontime)

我们现在将创建一个名为 rgbledmessage.py 的文件,以执行显示消息所需的操作。首先我们将导入使用的模块,更新的 rgbled 模块,新的 tilt 模块,以及 python 的 os 模块。最初,我们将 DEBUG 设置为 True,以便 Python 终端在脚本运行时显示额外的信息:

#!/usr/bin/python3
# rgbledmessage.py
import rgbled as RGBLED
import tilt as TILT
import os

DEBUG = True

添加一个 readMessageFile() 函数来读取 letters.txt 文件的内容,然后添加 processFileContent() 来生成每个字母的 LED 模式的 Python 字典

def readMessageFile(filename):
  assert os.path.exists(filename), 'Cannot find the message file: %s' % (filename)
  try:
    with open(filename, 'r') as theFile:
    fileContent = theFile.readlines()
  except IOError:
    print("Unable to open %s" % (filename))
  if DEBUG:print ("File Content START:")
  if DEBUG:print (fileContent)
  if DEBUG:print ("File Content END")
  dictionary = processFileContent(fileContent)
  return dictionary 

def processFileContent(content):
  letterIndex = [] #Will contain a list of letters stored in the file
  letterList = []  #Will contain a list of letter formats
  letterFormat = [] #Will contain the format of each letter
  firstLetter = True
  nextLetter = False
  LETTERDIC={}
  #Process each line that was in the file
  for line in content:
    # Ignore the # as comments
    if '#' in line:
      if DEBUG:print ("Comment: %s"%line)
    #Check for " in the line = index name  
    elif '"' in line:
      nextLetter = True
      line = line.replace('"','') #Remove " characters
      LETTER=line.rstrip()
      if DEBUG:print ("Index: %s"%line)
    #Remaining lines are formatting codes
    else:
      #Skip firstLetter until complete
      if firstLetter:
        firstLetter = False
        nextLetter = False
        lastLetter = LETTER
      #Move to next letter if needed
      if nextLetter:
        nextLetter = False
        LETTERDIC[lastLetter]=letterFormat[:]
        letterFormat[:] = []
        lastLetter = LETTER
      #Save the format data
      values = line.rstrip().split(' ')
      row = []
      for val in values:
        row.append(int(val))
      letterFormat.append(row)
  LETTERDIC[lastLetter]=letterFormat[:]
  #Show letter patterns for debugging
  if DEBUG:print ("LETTERDIC: %s" %LETTERDIC)
  if DEBUG:print ("C: %s"%LETTERDIC['C'])
  if DEBUG:print ("O: %s"%LETTERDIC['O'])
  return LETTERDIC

添加一个 createBuffer() 函数,该函数将消息转换为每个字母的 LED 模式序列(假设字母由 letters.txt 文件定义):

def createBuffer(message,dictionary):
  buffer=[]
  for letter in message:
    try:
      letterPattern=dictionary[letter]
    except KeyError:
      if DEBUG:print("Unknown letter %s: use _"%letter)
      letterPattern=dictionary['_']
    buffer=addLetter(letterPattern,buffer)
  if DEBUG:print("Buffer: %s"%buffer)
  return buffer

def addLetter(letter,buffer):
  for row in letter:
    buffer.append(row)
  buffer.append([0,0,0,0,0])
  buffer.append([0,0,0,0,0])
  return buffer

接下来,我们定义 displayBuffer() 来使用 rgbled 模块中的 rgbled_pov() 函数显示 LED 模式:

def displayBuffer(buffer):
  position=0
  while(1):
    if(TILT.tilt_moving()==False):
      position=0
    elif (position+1)<len(buffer):
      position+=1
      if DEBUG:print("Pos:%s ROW:%s"%(position,buffer[position]))
    RGBLED.rgbled_pov(buffer[position],RGBLED.RGB_GREEN,0.001)
    RGBLED.rgbled_pov(buffer[position],RGBLED.RGB_BLUE,0.001)

最后,我们创建一个 main() 函数来执行每个必需的步骤:

  1. 设置硬件组件(RGB LED 和倾斜开关)。

  2. 读取 letters.txt 文件。

  3. 定义 LED 字母模式字典。

  4. 生成一个缓冲区来表示所需的消息。

  5. 使用 rgbled 模块和 tilt 模块显示缓冲区:

    def main():
      RGBLED.led_setup()
      TILT.tilt_setup()
      dict=readMessageFile('letters.txt')
      buffer=createBuffer('_COOKBOOK_',dict)
      displayBuffer(buffer)
    
    if __name__=='__main__':
      try:
        main()
      finally:
        RGBLED.led_cleanup()
        print("Closed Everything. END")
    #End
    

创建以下文件,命名为letters.txt,以定义显示示例'_COOKBOOK_'消息所需的 LED 模式。注意,此文件只需为消息中的每个唯一字母或符号定义一个模式:

#COOKBOOK
"C"
0 1 1 1 0
1 0 0 0 1
1 0 0 0 1
"O"
0 1 1 1 0
1 0 0 0 1
1 0 0 0 1
0 1 1 1 0
"K"
1 1 1 1 1
0 1 0 1 0
1 0 0 0 1
"B"
1 1 1 1 1
1 0 1 0 1
0 1 0 1 0
"_"
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0

如何工作…

第一个函数readMessageFile()将打开并读取给定文件的正文(正如我们在第二章中之前所做的那样,从 Python 字符串、文件和菜单开始;有关更多详细信息,请参阅使用文件和处理错误配方)。然后它将使用processFileContent()返回一个包含文件中定义的字母对应模式的 Python 字典。文件中的每一行都会被处理,忽略任何包含#字符的行,并检查"字符以指示随后的 LED 模式的名称。在文件被处理后,我们最终得到一个包含'_''C''B''K''O'字符的 LED 模式的 Python 字典:

'_': [[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]
'C': [[0, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1]]
'B': [[1, 1, 1, 1, 1], [1, 0, 1, 0, 1], [0, 1, 0, 1, 0]]
'K': [[1, 1, 1, 1, 1], [0, 1, 0, 1, 0], [1, 0, 0, 0, 1]]
'O': [[0, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 1, 1, 0]]

现在我们有了一组可供选择的字母,我们可以使用createBuffer()函数创建一个 LED 模式序列。正如其名所示,该函数将通过查找消息中的每个字母并逐行添加相关模式来构建一个 LED 模式的缓冲区。如果一个字母在字典中找不到,则将使用空格代替。

最后,我们现在有一个准备显示的 LED 模式列表。为了控制何时开始序列,我们将使用 TILT 模块并检查倾斜开关的状态:

如何工作…

倾斜开关在未移动(左侧)和移动(右侧)时的位置

倾斜开关由一个装在空心绝缘圆柱体内的微型滚珠轴承组成;当球在圆柱体底部静止时,两个引脚之间的连接是闭合的。当球被移动到圆柱体的另一端,与引脚失去接触时,倾斜开关是打开的:

如何工作…

倾斜开关电路,开关关闭和开关打开

之前显示的倾斜开关电路将允许 GPIO 引脚 24 在开关关闭时连接到地,然后如果我们读取该引脚,当它处于静止状态时将返回False。通过将 GPIO 引脚设置为输入并启用内部上拉电阻,当倾斜开关打开时,它将报告True

如果倾斜开关是打开的(报告True),则我们假设单元正在移动,并开始显示 LED 序列,每次显示 LED 模式的一行时,都会递增当前位置。为了使模式更加多彩(只是因为我们能这么做!),我们将每一行重复一遍,用另一种颜色。一旦TILT.tilt_moving()函数报告我们停止移动或正在朝相反方向移动,我们将重置当前位置,以便再次从头开始整个模式:

如何工作…

信息是由 RGB LED 显示的——这里使用绿色和蓝色一起显示

当 RGB LED 模块和倾斜开关来回移动时,我们应该能看到空中显示的信息!

尝试使用不同的颜色组合、速度和手臂摆动来观察你能产生哪些效果。你甚至可以创建一个安装在轮子上的类似装置,以产生连续的 POV 效果。

第七章. 感知和显示现实世界数据

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

  • 使用带有 I²C 总线的设备

  • 使用模数转换器读取模拟数据

  • 记录和绘制数据

  • 使用 I/O 扩展器扩展 Raspberry Pi GPIO

  • 在 SQLite 数据库中捕获数据

  • 从您的自有网络服务器查看数据

  • 感知并将数据发送到在线服务

简介

在上一章中,我们使用了 Raspberry Pi GPIO 直接通过控制或读取 GPIO 引脚来控制并响应连接的硬件。在本章中,我们将学习如何从现实世界收集模拟数据并对其进行处理,以便我们可以显示、记录、绘图并共享数据,并在我们的程序中使用它。

我们将通过使用 Raspberry Pi 的 GPIO 连接与模数转换器ADC)、LCD 数码显示屏和数字端口扩展器来扩展 Raspberry Pi 的功能。

小贴士

请务必查看附录,硬件和软件列表,其中列出了本章中使用的所有项目及其获取地点。

使用带有 I²C 总线的设备

Raspberry Pi 可以支持多种高级协议,这使得更广泛的设备可以轻松连接。在本章中,我们将重点关注最常用的总线,称为I²CI-squared-C)。它提供了一个中速总线,用于通过两根线与设备通信。在本节中,我们将使用 I²C 与 8 位 ADC 进行接口。该设备将测量模拟信号,将其转换为介于 0 和 255 之间的相对值,并将该值作为数字信号(由 8 位表示)通过 I²C 总线发送到 Raspberry Pi。

准备工作

I²C 总线并非在所有 Raspberry Pi 图像中都已启用;因此,我们需要启用模块并安装一些支持工具。Raspbian 的新版本使用设备树来处理硬件外围设备和驱动程序。

为了使用 I²C 总线,我们需要在 \boot\config.txt 文件中启用 ARM I²C。

您可以使用以下命令自动完成此操作:

sudo raspi-config

如以下截图所示,从菜单中选择高级选项,然后选择I²C。当被询问时,选择以启用接口,并默认加载模块。

准备中

从菜单中选择I²C,并选择以启用接口并默认加载模块。

小贴士

raspi-config 程序通过修改 /boot/config.txt 以包含 dtparam=i2c_arm=on 来启用 I2C_ARM 接口。另一个总线(I2C_VC)通常保留用于与 Raspberry Pi HAT 扩展板进行接口(从板载存储设备读取配置信息);然而,您可以使用 dtparam=i2c_vc=on 来启用它。

如果您愿意,也可以通过 raspi-config 列表启用 SPI,这是另一种可以在第十章,与技术接口中看到的总线类型。

接下来,我们应该包含在开机时加载的 I²C 模块,如下所示:

sudo nano /etc/modules

在单独的行上添加以下内容并保存(Ctrl + X, Y, Enter):

i2c-dev
i2c-bcm2708

同样,我们也可以通过添加 spi-bcm2708 来启用 SPI 模块。

接下来,我们将安装一些工具,以便可以直接从命令行使用 I²C 设备,如下所示:

sudo apt-get update
sudo apt-get install i2c-tools

最后,在连接硬件之前关闭 Raspberry Pi,以便应用更改,如下所示:

sudo halt

您需要一个 PCF8591 模块(这些模块的零售商列在附录, 硬件和软件列表中),或者您可以单独获取 PCF8591 芯片并构建自己的电路(有关电路的详细信息,请参阅 还有更多… 部分)。

准备就绪

来自 dx.com 的 PCF8591 ADC 和传感器模块

GNDVCCSDASCL 引脚连接到 Raspberry Pi GPIO 引脚,如下所示:

准备就绪

Raspberry Pi GPIO 引脚上的 I²C 连接

注意

您可以通过研究设备的规格书来使用相同的 I²C 工具/代码与其他 I²C 设备进行交互,以找出要发送/读取的消息以及用于控制您的设备的哪些寄存器。

如何操作...

使用以下命令通过 i2cdetect(使用 --y 选项跳过关于可能与其他硬件(可能连接到 I²C 总线)发生干扰的任何警告)检测 I²C 设备:

sudo i2cdetect -y 0
sudo i2cdetect -y 1

根据您的 Raspberry Pi 板修订版,设备的地址应列在总线 0 上(对于 B 型 Rev1 板)或总线 1 上(对于 Raspberry Pi 2 & 3,Raspberry Pi 1 Model A 和 Model B Rev2)。默认情况下,PCF8591 地址为 0x48

要使用的 I²C 总线编号 总线 00 总线 11
Raspberry Pi 2 & 3 HAT ID (I2C_VC) GPIO (I2C_ARM)
A 型和 B 型修订 2 P5 GPIO
B 型修订 1 GPIOGPIO n/a

以下截图显示了 i2cdetect 的输出:

如何操作...

PCF8591 地址(48)在此显示在总线 1 上

如果没有列出任何内容,请关闭并检查您的连接(来自 www.dx.com 的 ADC 模块在供电时会显示红色 LED)。

注意

如果您收到 /dev/i2c1 总线不存在的错误,您可以执行以下检查。

确保文件 /etc/modprobe.d/raspi-blacklist.conf 为空(即,模块没有被列入黑名单),使用以下命令查看文件:

sudo nano /etc/modprobe.d/raspi-blacklist.conf

如果文件中存在任何内容(例如 blacklist i2c-bcm2708),请将其删除并保存。

检查 /boot/config 并确保没有一行包含 device_tree_param=(这将禁用对新设备树配置的支持,并禁用对某些 Raspberry Pi HAT 扩展板的兼容性支持)。

使用lsmod检查模块是否已加载,并查找i2c-bcm2708i2c_dev

使用检测到的总线号(01)和设备地址(0x48),使用i2cget从设备读取(在上电或通道更改后,你需要读取设备两次以查看最新值),如下所示:

sudo i2cget -y 1 0x48
sudo i2cget -y 1 0x48

要从通道1(这是模块上的温度传感器)读取,我们可以使用i2cset0x01写入 PCF8591 控制寄存器。同样,使用两次读取从通道1获取一个新的样本,如下所示:

sudo i2cset -y 1 0x48 0x01
sudo i2cget -y 1 0x48
sudo i2cget -y 1 0x48

要循环遍历每个输入通道,请使用i2cset将控制寄存器设置为0x04,如下所示:

sudo i2cset -y 1 0x48 0x04

我们还可以使用以下命令来控制 AOUT 引脚,将其完全打开(点亮 LED D1):

sudo i2cset -y 1 0x48 0x40 0xff

最后,我们可以使用以下命令将其完全关闭(关闭 LED D1):

sudo i2cset -y 1 0x48 0x40 0x00

它是如何工作的...

在上电后从设备读取第一次将返回0x80,并也会触发从通道 0 的新样本。如果你再次读取,它将返回之前读取的样本并生成一个新的样本。每次读取都将是一个 8 位值(范围从0255),表示电压从 0 到 VCC(在这种情况下,0V 到 3.3V)。在www.dx.com模块上,通道 0 连接到一个光传感器,所以如果你用手遮住模块并重新发送命令,你会观察到值的变化(变暗意味着值更高,变亮意味着值更低)。你会发现读取值总是落后一步;这是因为当它返回前一个样本时,它会捕获下一个样本。

我们使用以下命令来指定要读取的特定通道:

sudo i2cset -y 1 0x48 0x01

这将读取的通道更改为通道 1(在模块上标记为AIN1)。记住,你需要在看到新选择通道的数据之前执行两次读取。以下表格显示了通道和引脚名称以及哪些跳线连接器启用/禁用每个传感器:

通道 0 1 2 3
引脚名称 AIN0 AIN1 AIN2 AIN3
传感器 光敏电阻 热敏电阻 外部引脚 滑动变阻器
跳线 P5 P4 P6

接下来,我们通过设置控制寄存器的模拟输出使能标志(位 6)来控制 AOUT 引脚,并使用下一个值来设置模拟电压(0V-3.3V 0x00-0xFF),如下所示:

sudo i2cset -y 1 0x48 0x40 0xff 

最后,你可以将位 2(0x04)设置为自动递增,并按如下方式循环遍历输入通道:

sudo i2cset -y 1 0x48 0x04

每次运行i2cget -y 1 0x48,下一个通道将被选择,从通道 AIN0 开始,然后是 AIN1 到 AIN3,再回到 AIN0。

注意

要了解如何设置一个值中的特定位,查看数字的二进制表示很有帮助。8 位值0x04可以写成二进制b0000 01000x表示值是以十六进制或十六进制形式编写的,而 b 表示一个二进制数)。

二进制数中的位从右到左计数,从 0 开始,即 MSB 7 6 5 4 3 2 1 0 LSB。

位 7 被称为 最高有效位MSB),位 0 被称为 最低有效位LSB)。因此,通过设置位 2,我们最终得到 b0000 0100(这是 0x04)。

还有更多...

I²C 总线使我们能够通过仅使用几根线轻松地连接多个设备。PCF8591 芯片可以用来将您的传感器连接到模块或只是芯片。

使用多个 I²C 设备

I²C 总线上的所有命令都是针对特定的 I²C 设备的(许多设备可以选择将一些引脚设置为高电平或低电平以选择额外的地址,并允许多个设备存在于同一总线上)。每个设备都必须有一个唯一的地址,这样在任何时候只有一个设备会响应。PCF8591 的起始地址是 0x48,通过三个地址引脚可选择到 0x4F。这允许在同一总线上使用多达八个 PCF8591 设备。

注意

如果您决定使用位于 GPIO 引脚 27 和 28(或 Model A 和 Rev2 Model B 设备上的 P5 头部)的 I2C_VC 总线,您可能需要在 I²C 线和 3.3V 之间添加一个 1k8 欧姆的上拉电阻。这些电阻已经存在于 GPIO 连接器上的 I²C 总线上。然而,一些 I²C 模块,包括 PCF8591 模块,已经安装了自己的电阻,因此无需额外的电阻即可工作。

I²C 总线和电平转换

I²C 总线由两根线组成,一根是数据(SDA),另一根是时钟(SCL)。两者都通过上拉电阻被动地拉到 VCC(在 Raspberry Pi 上,这是 3.3V)。Raspberry Pi 将通过在每个周期中将时钟拉低来控制时钟,数据线可以通过 Raspberry Pi 拉低来发送命令,或者由连接的设备拉低以响应数据。

I2C 总线和电平转换

Raspberry Pi 的 I²C 引脚包括 SDA 和 SCL 上的上拉电阻

由于从设备只能将数据线拉到 GND,因此设备可以由 3.3V 或甚至 5V 供电,而不会存在驱动 GPIO 引脚过高的风险(记住 Raspberry Pi 的 GPIO 不能处理超过 3.3V 的电压)。只要设备的 I²C 总线能识别 3.3V 而不是 5V 的逻辑高电平,这应该就可以工作。I²C 设备不得配备自己的上拉电阻,因为这会导致 GPIO 引脚被拉到 I²C 设备的供电电压。

注意,本章中使用的 PCF8591 模块已安装了电阻;因此,我们只能使用 VCC=3V3。可以使用双向逻辑电平转换器来解决任何逻辑电平问题。这样的设备之一是 Adafruit I²C 双向逻辑电平转换器,如图所示:

I2C 总线和电平转换

Adafruit I²C 双向逻辑电平转换模块

除了确保任何逻辑电压适合您所使用的设备外,它还将允许总线通过更长的电线扩展(电平转换器还将充当总线中继器)。

仅使用 PCF8591 芯片或添加替代传感器

以下图示显示了未连接传感器的 PCF8591 模块的电路图:

仅使用 PCF8591 芯片或添加替代传感器

PCF8591 ADC 电路 – VCC、GND、SCL 和 SDA 如前所述连接到 Raspberry Pi

如您所见,除了传感器外,只有五个额外的组件。我们有一个电源滤波电容(C1)和电源指示 LED(D2)以及限流电阻(R5),所有这些都是可选的。

应注意,该模块包含两个 10k 上拉电阻(R8 和 R9)用于 SCL 和 SDA 信号。然而,由于 Raspberry Pi 上的 GPIO I²C 连接也包含上拉电阻,因此在该模块上不需要这些电阻(并且可以移除)。这也意味着我们应仅将此模块连接到 VCC=3.3V(如果我们使用 5V,则 SCL 和 SDA 上的电压将约为 3.56V,这对 Raspberry Pi 的 GPIO 引脚来说太高)。

PCF891 模块上的传感器都是电阻性的,因此当传感器的电阻变化时,模拟输入上的电压将在 GND 和 VCC 之间变化。

仅使用 PCF8591 芯片或添加替代传感器

电位分压器电路用于提供与传感器电阻成比例的电压

该模块使用一种称为电位分压器的电路。顶部的电阻平衡底部传感器的电阻,以提供介于 VCC 和 GND 之间的电压。

电位分压器输出电压(Vout)的计算如下:

仅使用 PCF8591 芯片或添加替代传感器

Rt 和 Rb 分别是顶部和底部的电阻值,VCC 是供电电压。

模块中的电位计电阻在顶部和底部之间根据调节器的位置分为 10k 欧姆。因此,在中点,每侧有 5k 欧姆,输出电压为 1.65V;四分之一处(顺时针方向),我们有 2.5k 欧姆和 7.5k 欧姆,产生 0.825V。

注意

我没有展示 AOUT 电路,它是一个电阻和 LED。然而,正如您将发现的,LED 不适合指示模拟输出(除了显示开/关状态)。

对于更敏感的电路,您可以使用更复杂的电路,如惠斯通电桥(允许检测非常小的电阻变化),或者您可以使用基于其读数的专用传感器输出模拟电压(如TMP36温度传感器)。PCF891 还支持差分输入模式,其中一个通道可以与另一个通道的输入进行比较(结果读数将是两个之间的差值)。

关于 PCF8591 芯片的更多信息,请参阅www.nxp.com/documents/data_sheet/PCF8591.pdf的数据表。

使用模数转换器读取模拟数据

I2CTools(在上一节中使用)在命令行中调试 I²C 设备非常有用,但它们在 Python 中使用并不实用,因为它们会变慢,并且使用时需要大量的开销。幸运的是,有几个 Python 库提供了 I²C 支持,允许高效地使用 I²C 与连接的设备通信并提供易于操作。

我们将使用这样的库来创建自己的 Python 模块,使我们能够快速轻松地从 ADC 设备获取数据,并在我们的程序中使用它。该模块设计得如此之好,以至于其他硬件或数据源可以替换它,而不会影响剩余的示例。

准备就绪

要使用 Python 3 的 I²C 总线,我们将使用 Gordon Henderson 的wiringPi2(更多详情请见wiringpi.com/)。

安装wiringPi2最简单的方法是使用 Python 3 的 PIP。PIP 是 Python 的一个包管理器,其工作方式与apt-get类似。您希望安装的任何包都将自动从在线仓库下载并安装。

要安装 PIP,请使用以下命令:

sudo apt-get install python3-dev python3-pip

然后使用以下命令安装wiringPi2

sudo pip-3.2 install wiringpi2

安装完成后,您应该会看到以下内容,表示成功:

准备就绪

成功安装 wiringPi2

您需要将 PCF8591 模块按照之前的方式连接到 Raspberry Pi 的 I²C 连接。

准备就绪

PCF8591 模块和引脚连接到 Raspberry Pi 的 GPIO 连接器(如前所述)

如何操作...

在下一节中,我们将编写一个脚本,使我们能够收集数据,然后在本章的后续部分使用这些数据。

创建以下脚本,data_adc.py,如下所示:

  1. 首先,导入模块并创建我们将使用的变量,如下所示:

    #!/usr/bin/env python3
    #data_adc.py
    import wiringpi2
    import time
    
    DEBUG=False
    LIGHT=0;TEMP=1;EXT=2;POT=3
    ADC_CH=[LIGHT,TEMP,EXT,POT]
    ADC_ADR=0x48
    ADC_CYCLE=0x04
    BUS_GAP=0.25
    DATANAME=["0:Light","1:Temperature",
              "2:External","3:Potentiometer"]
    
  2. 创建一个名为device的类,并使用构造函数来初始化它,如下所示:

    class device:
      # Constructor:
      def __init__(self,addr=ADC_ADR):
        self.NAME = DATANAME
        self.i2c = wiringpi2.I2C()
        self.devADC=self.i2c.setup(addr)
        pwrup = self.i2c.read(self.devADC) #flush powerup value
        if DEBUG==True and pwrup!=-1:
          print("ADC Ready")
        self.i2c.read(self.devADC) #flush first value
        time.sleep(BUS_GAP)
        self.i2c.write(self.devADC,ADC_CYCLE)
        time.sleep(BUS_GAP)
        self.i2c.read(self.devADC) #flush first value
    
  3. 在类中,定义一个函数以提供如下通道名称列表:

      def getName(self):
        return self.NAME
    
  4. 定义另一个函数(仍然作为类的一部分),以如下方式返回来自 ADC 通道的新样本集:

      def getNew(self):
        data=[]
        for ch in ADC_CH:
          time.sleep(BUS_GAP)
          data.append(self.i2c.read(self.devADC))
        return data
    
  5. 最后,在设备类之后,创建一个测试函数来测试我们的新device类,如下所示。这仅在脚本直接执行时运行:

    def main():
      ADC = device(ADC_ADR)
      print (str(ADC.getName()))
      for i in range(10):
        dataValues = ADC.getNew()
        print (str(dataValues))
        time.sleep(1)
    
    if __name__=='__main__':
      main()
    #End
    

您可以使用以下命令运行此模块的测试函数:

sudo python3 data_adc.py

它是如何工作的...

我们首先导入 wiringpi2,以便我们稍后可以与我们的 I²C 设备通信。我们将创建一个类来包含控制 ADC 所需的功能。当我们创建类时,我们可以初始化 wiringPi2 以便它可以使用 I²C 总线(使用 wiringpi2.I2C()),并且我们使用芯片的总线地址设置一个通用的 I²C 设备(使用 self.i2c.setup(0x48))。

注意

wiringPi2 还有一个用于与 PCF8591 芯片一起使用的专用类;然而,在这种情况下,使用标准的 I²C 功能来展示如何使用 wiringPi2 控制任何 I²C 设备更有用。通过参考设备数据表,你可以使用类似的命令与任何连接的 I²C 设备通信(无论它是否直接受支持)。

如前所述,我们执行设备读取并配置 ADC 在通道之间循环,但不是使用 i2cgeti2cset,而是使用 I²C 对象的 wiringPi2 读写函数。一旦初始化,设备将准备好读取每个通道上的模拟信号。

该类还将有两个成员函数。第一个函数 getName() 返回一个通道名称列表(我们可以用它来关联我们的数据与其来源),第二个函数 getNew() 返回所有通道的新数据集。数据通过 i2c.read() 函数从 ADC 读取,并且由于我们已经将其置于循环模式,每次读取都将来自下一个通道。

由于我们计划稍后重用此类,我们将使用 if __name__ 测试来允许我们在直接执行文件时运行一些代码。在我们的 main() 函数中,我们创建了一个 ADC 实例,它是我们新设备类的一个实例。如果我们需要,可以选择非默认地址;否则,将使用芯片的默认地址。我们使用 getName() 函数打印出通道名称,然后我们可以从 ADC(使用 getNew())收集数据并显示它们。

还有更多...

以下内容允许我们在 data_adc.py 中定义设备类的替代版本,以便它可以替代 ADC 模块使用。这将使得本章剩余部分可以在不需要任何特定硬件的情况下进行尝试。

无硬件收集模拟数据

如果你没有 ADC 模块可用,Raspberry Pi 内部有大量数据可供使用。

创建以下脚本,data_local.py

#!/usr/bin/env python3
#data_local.py
import subprocess
from random import randint
import time

MEM_TOTAL=0
MEM_USED=1
MEM_FREE=2
MEM_OFFSET=7
DRIVE_USED=0
DRIVE_FREE=1
DRIVE_OFFSET=9
DEBUG=False
DATANAME=["CPU_Load","System_Temp","CPU_Frequency",
          "Random","RAM_Total","RAM_Used","RAM_Free",
          "Drive_Used","Drive_Free"]

def read_loadavg():
  # function to read 1 minute load average from system uptime
  value = subprocess.check_output(
            ["awk '{print $1}' /proc/loadavg"], shell=True)
  return float(value)

def read_systemp():
  # function to read current system temperature
  value = subprocess.check_output(
            ["cat /sys/class/thermal/thermal_zone0/temp"],
            shell=True)
  return int(value)

def read_cpu():
  # function to read current clock frequency
  value = subprocess.check_output(
            ["cat /sys/devices/system/cpu/cpu0/cpufreq/"+
             "scaling_cur_freq"], shell=True)
  return int(value)

def read_rnd():
  return randint(0,255)

def read_mem():
  # function to read RAM info
  value = subprocess.check_output(["free"], shell=True)
  memory=[]
  for val in value.split()[MEM_TOTAL+
                           MEM_OFFSET:MEM_FREE+
                           MEM_OFFSET+1]:
    memory.append(int(val))
  return(memory)

def read_drive():
  # function to read drive info
  value = subprocess.check_output(["df"], shell=True)
  memory=[]
  for val in value.split()[DRIVE_USED+
                           DRIVE_OFFSET:DRIVE_FREE+
                           DRIVE_OFFSET+1]:
    memory.append(int(val))
  return(memory)

class device:
  # Constructor:
  def __init__(self,addr=0):
    self.NAME=DATANAME

  def getName(self):
    return self.NAME

  def getNew(self):
    data=[]
    data.append(read_loadavg())
    data.append(read_systemp())
    data.append(read_cpu())
    data.append(read_rnd())
    memory_ram = read_mem()
    data.append(memory_ram[MEM_TOTAL])
    data.append(memory_ram[MEM_USED])
    data.append(memory_ram[MEM_FREE])
    memory_drive = read_drive()
    data.append(memory_drive[DRIVE_USED])
    data.append(memory_drive[DRIVE_FREE])
    return data

def main():
  LOCAL = device()
  print (str(LOCAL.getName()))
  for i in range(10):
    dataValues = LOCAL.getNew()
    print (str(dataValues))
    time.sleep(1)

if __name__=='__main__':
  main()
#End

以下脚本允许我们使用以下命令从 Raspberry Pi 收集系统信息(subprocess 模块允许我们捕获结果并处理它们):

  • CPU 速度:

    cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq
    
    
  • CPU 负载:

    awk '{print $1}' /proc/loadavg
    
    
  • 核心温度(按 1,000 缩放):

    cat /sys/class/thermal/thermal_zone0/temp
    
    
  • 驱动信息:

    df
    
    
  • RAM 信息:

    free
    
    

每个数据项使用其中一个函数进行采样。在驱动和 RAM 信息的情况下,我们将响应拆分为一个列表(由空格分隔)并选择我们想要监控的项目(如可用内存和已用驱动器空间)。

这一切都被打包起来,以便以与 data_adc.py 文件和 device 类相同的方式工作(因此你可以在以下示例中选择使用其中一个,只需将 data_adc 包含文件替换为 data_local)。

日志记录和绘图数据

现在我们能够采样和收集大量数据,因此我们能够捕获和分析它变得非常重要。我们将使用一个名为 matplotlib 的 Python 库,它包括许多用于操作、绘图和分析数据的实用工具。我们将使用 pyplot(它是 matplotlib 的一部分)来生成我们捕获数据的图表。有关 pyplot 的更多信息,请访问 matplotlib.org/users/pyplot_tutorial.html

准备工作

要使用 pyplot,我们需要安装 matplotlib

注意

由于 matplotlib 安装程序的问题,使用 pip-3.2 进行安装并不总是正确。以下方法将通过手动执行 PIP 所做的所有步骤来克服这个问题;然而,这可能需要超过 30 分钟才能完成。

为了节省时间,你可以尝试使用 PIP 安装,这会快得多。如果不起作用,你可以使用这种方法手动安装。

尝试使用以下命令使用 PIP 安装 matplotlib

sudo apt-get install tk-dev python3-tk libpng-dev
sudo pip-3.2 install numpy
sudo pip-3.2 install matplotlib

你可以通过运行 python3 并尝试从 Python 终端导入它来确认 matplotlib 是否已安装,如下所示:

import matplotlib

注意

如果安装失败,它将响应如下:

ImportError: No module named matplotlib

否则,将没有错误。

使用以下步骤手动安装 matplotlib

  1. 按如下方式安装支持包:

    sudo apt-get install tk-dev python3-tk python3-dev libpng-dev
    sudo pip-3.2 install numpy
    sudo pip-3.2 install matplotlib
    
    
  2. 从 Git 仓库下载源文件(命令应该是一行)如下:

    wget https://github.com/matplotlib/matplotlib/archive/master.zip
    
    
  3. 解压并打开创建的 matplotlib-master 文件夹,如下所示:

    unzip master.zip
    rm master.zip
    cd matplotlib-master
    
    
  4. 运行设置文件进行构建(这将需要一段时间)并按如下方式安装:

    sudo python3 setup.py build
    sudo python3 setup.py install
    
    
  5. 以与自动安装相同的方式进行安装测试。

我们可能需要 PCF8591 ADC 模块(并且之前已安装 wiringPi2),或者我们可以使用上一节中的 data_local.py 模块(只需在脚本的导入部分将 data_adc 替换为 data_local)。我们还需要将 data_adc.pydata_local.py 放在新脚本相同的目录中,具体取决于你使用哪个。

如何操作...

  1. 创建以下脚本,log_adc.py

    #!/usr/bin/python3
    #log_adc.c
    import time
    import datetime
    import data_adc as dataDevice
    
    DEBUG=True
    FILE=True
    VAL0=0;VAL1=1;VAL2=2;VAL3=3 #Set data order
    FORMATHEADER = "\t%s\t%s\t%s\t%s\t%s"
    FORMATBODY = "%d\t%s\t%f\t%f\t%f\t%f"
    
    if(FILE):f = open("data.log",'w')
    
    def timestamp():
      ts = time.time() 
      return datetime.datetime.fromtimestamp(ts).strftime(
                                        '%Y-%m-%d %H:%M:%S')
    
    def main():
        counter=0
        myData = dataDevice.device()
        myDataNames = myData.getName()
        header = (FORMATHEADER%("Time",
                            myDataNames[VAL0],myDataNames[VAL1],
                            myDataNames[VAL2],myDataNames[VAL3]))
        if(DEBUG):print (header)
        if(FILE):f.write(header+"\n")
        while(1):
          data = myData.getNew()
          counter+=1
          body = (FORMATBODY%(counter,timestamp(),
                            data[0],data[1],data[2],data[3]))
          if(DEBUG):print (body)
          if(FILE):f.write(body+"\n")
          time.sleep(0.1)
    
    try:
      main()
    finally:
      f.close()
    #End
    
  2. 创建第二个脚本,log_graph.py,如下所示:

    #!/usr/bin/python3
    #log_graph.py
    import numpy as np
    import matplotlib.pyplot as plt
    
    filename = "data.log"
    OFFSET=2
    with open(filename) as f:
        header = f.readline().split('\t')
    
    data = np.genfromtxt(filename, delimiter='\t', skip_header=1,
                        names=['sample', 'date', 'DATA0',
                               'DATA1', 'DATA2', 'DATA3'])
    fig = plt.figure(1)
    ax1 = fig.add_subplot(211)#numrows, numcols, fignum
    ax2 = fig.add_subplot(212)
    ax1.plot(data['sample'],data['DATA0'],'r',
             label=header[OFFSET+0])
    ax2.plot(data['sample'],data['DATA1'],'b',
             label=header[OFFSET+1])
    ax1.set_title("ADC Samples")    
    ax1.set_xlabel('Samples')
    ax1.set_ylabel('Reading')
    ax2.set_xlabel('Samples')
    ax2.set_ylabel('Reading')
    
    leg1 = ax1.legend()
    leg2 = ax2.legend()
    
    plt.show()
    #End
    

它是如何工作的...

第一个脚本 log_adc.py 允许我们收集数据并将其写入日志文件。

我们可以通过将 data_adc 导入为 dataDevice 来使用 ADC 设备,或者我们可以导入 data_local 来使用系统数据。给 VAL0VAL3 分配的数字允许我们更改通道的顺序(如果使用 data_local 设备,则选择其他通道)。我们还定义了日志文件中标题和每行的格式字符串(以创建一个用制表符分隔数据的文件),使用 %s%d%f 来允许我们替换字符串、整数和浮点值,如下表所示:

如何工作...

从 ADC 传感器模块捕获的数据表

如果将日志记录到文件(当 FILE=True 时),我们使用 'w' 选项以写入模式打开 data.log(这将覆盖任何现有文件;要追加到文件,请使用 'a')。

作为我们数据日志的一部分,我们使用 timedatetime 生成 timestamp,以获取当前的 纪元时间(这是自 1970 年 1 月 1 日以来的毫秒数),使用 time.time() 命令。我们使用 strftime() 将值转换为更友好的 年-月-日 时:分:秒 格式。

main() 函数首先创建我们 device 类的一个实例(我们在前面的示例中创建了它),这将提供数据。我们从 data 设备获取通道名称并构建 header 字符串。如果 DEBUG 设置为 True,数据将被打印到屏幕上;如果 FILE 设置为 True,它将被写入文件。

在主循环中,我们使用设备的 getNew() 函数收集数据并将其格式化以在屏幕上显示或记录到文件。使用 try: finally: 命令调用 main() 函数,这将确保当脚本被终止时,文件将被正确关闭。

第二个脚本 log_graph.py 允许我们读取日志文件并生成记录数据的图表,如下所示:

如何工作...

由 light 和温度传感器生成的 log_graph.py 产生的图表

我们首先打开日志文件并读取第一行;这包含标题信息(然后我们可以使用它来识别后续的数据)。接下来,我们使用 numpy,这是一个专业的 Python 库,它扩展了我们可以操作数据和数字的方式。在这种情况下,我们使用它来读取文件中的数据,根据制表符分隔符拆分它,并为每个数据通道提供标识符。

我们定义一个图形来保存我们的图表,添加两个子图(位于 2 x 1 网格中,网格位置为 1 和 2 - 由值 211212 设置)。接下来,我们定义我们想要绘制的值,提供 x 值(data['sample']),y 值(data['DATA0']),color 值('r' 代表红色或 'b' 代表蓝色),以及 label(设置为从文件顶部读取的标题文本)。

最后,我们为每个子图设置一个标题,xy 标签,启用图例(以显示标签),并显示图表(使用 plt.show())。

还有更多...

现在我们有了查看我们一直在捕获的数据的能力,我们可以通过在采样时显示数据来更进一步。这将使我们能够立即看到数据如何对环境或刺激的变化做出反应。我们还可以校准数据,以便我们可以分配适当的缩放比例以产生实际单位中的测量值。

实时数据绘图

除了从文件中绘制数据外,我们还可以使用matplotlib绘制采样时的传感器数据。为了实现这一点,我们可以使用plot-animation功能,该功能会自动调用一个函数来收集新数据并更新我们的图表。

创建以下脚本,live_graph.py

#!/usr/bin/python3
#live_graph.py
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import data_local as dataDevice

PADDING=5
myData = dataDevice.device()
dispdata = []
timeplot=0
fig, ax = plt.subplots()
line, = ax.plot(dispdata)

def update(data):
  global dispdata,timeplot
  timeplot+=1
  dispdata.append(data)
  ax.set_xlim(0, timeplot)
  ymin = min(dispdata)-PADDING
  ymax = max(dispdata)+PADDING
  ax.set_ylim(ymin, ymax)
  line.set_data(range(timeplot),dispdata)
  return line

def data_gen():
  while True:
    yield myData.getNew()[1]/1000

ani = animation.FuncAnimation(fig, update, 
                              data_gen, interval=1000)
plt.show()
#End

我们首先定义我们的dataDevice对象并创建一个空数组dispdata[],它将保存所有收集到的数据。接下来,我们定义我们的子图和将要绘制的线条。

FuncAnimation()函数允许我们通过定义一个更新函数和一个生成函数来更新一个图形(fig)。生成函数(data_gen())将每隔(1,000 毫秒)被调用一次,并产生一个数据值。

此示例使用核心温度读数,当除以 1,000 时,给出实际的温度degC

小贴士

要使用 ADC 数据,请将dataDevice的导入更改为data_adc,并调整以下行以使用除[1]以外的通道,并应用不同于 1,000 的缩放比例:

yield myData.getNew()[1]/1000

实时数据绘图

树莓派实时绘图(核心温度(以摄氏度为单位)与时间(以秒为单位))

数据值传递给update()函数,这使得我们可以将其添加到我们的dispdata[]数组中,该数组将包含所有要在图中显示的数据值。我们调整x轴的范围,使其接近数据的minmax值,同时调整y轴,以便在继续采样更多数据时增长。

注意

FuncAnimation()函数需要data_gen()对象是一个特殊类型的函数,称为generatorgenerator函数在每次被调用时都会产生一系列连续的值,如果需要,甚至可以使用其先前状态来计算下一个值。这用于执行绘图时的连续计算;这就是为什么在这里使用它的原因。在我们的情况下,我们只想连续运行相同的采样函数(new_data()),因此每次调用它时,它将产生一个新的样本。

最后,我们使用dispdata[]数组(使用set_data()函数)更新xy轴的数据,这将绘制我们的样本与采样秒数的对应关系。要使用其他数据或绘制来自 ADC 的数据,请调整dataDevice的导入,并在data_gen()函数中选择所需的通道(和缩放比例)。

数据缩放和校准

你可能已经注意到,有时从模数转换器(ADC)读取的数据可能很难解释,因为它的值只是一个数字。除了告诉你它比前一个样本稍微热一点或暗一点之外,数字并没有太大的帮助。然而,如果你可以使用另一个设备提供可比较的值(例如当前房间温度),那么你可以校准你的传感器数据以提供更有用的现实世界信息。

为了获得粗略的校准,我们将使用两个样本来创建一个线性拟合模型,然后可以使用该模型来估计其他 ADC 读取的真实世界值(这假设传感器本身的响应主要是线性的)。以下图显示了在 25 度和 30 摄氏度下的两个读取值,为其他温度提供了估计的 ADC 值:

数据缩放和校准

样本用于线性校准温度传感器读取

我们可以使用以下函数来计算我们的模型:

  def linearCal(realVal1,readVal1,realVal2,readVal2):
    #y=Ax+C
    A = (realVal1-realVal2)/(readVal1-readVal2)
    C = realVal1-(readVal1*A)
    cal = (A,C)
    return cal

这将返回cal,它将包含模型斜率(A)和偏移量(C)。

我们可以使用以下函数通过使用该通道计算出的cal值来计算任何读取的值:

  def calValue(readVal,cal = [1,0]):
    realVal = (readVal*cal[0])+cal[1]
    return realVal

为了提高精度,你可以取几个样本,并在值之间使用线性插值(或者根据需要将数据拟合到其他更复杂的数学模型)。

使用 I/O 扩展器扩展 Raspberry Pi GPIO

正如我们所见,利用高级总线协议使我们能够快速轻松地连接到更复杂的硬件。通过使用 I²C 来扩展 Raspberry Pi 上的可用 I/O 以及提供额外的电路保护(在某些情况下,还可以提供额外的电源来驱动更多硬件),I²C 可以得到很好的利用。

有很多设备可以提供 I²C 总线的 I/O 扩展(以及 SPI),但最常用的是 28 引脚设备,MCP23017,它提供了 16 个额外的数字输入/输出引脚。作为一个 I²C 设备,它只需要两个信号(SCL 和 SDA 连接加上地线和电源)并且可以与其他 I²C 设备在同一总线上愉快地工作。

我们将看到 Adafruit I²C 16x2 RGB LCD Pi Plate 如何使用这些芯片之一来通过 I²C 总线控制 LCD 字符显示和键盘(如果没有 I/O 扩展器,这通常需要多达 15 个 GPIO 引脚)。

准备工作

你将需要 Adafruit I²C 16x2 RGB LCD Pi Plate(它还包括五个按键),如下图所示:

准备工作

带有按键的 Adafruit I²C 16x2 RGB LCD Pi Plate

Adafruit I²C 16x2 RGB LCD Pi Plate 直接连接到 Raspberry Pi 的 GPIO 连接器。

如前所述,我们可以使用 PCF8591 ADC 模块或使用上一节中的data_local.py模块(在脚本的导入部分使用data_adcdata_local)。data_adc.pydata_local.py文件应与新的脚本在同一目录中。

小贴士

LCD Pi 板只需要五个引脚(SDA,SCL,GND 和 5V);它连接到整个 GPIO 引脚头。如果我们想与其他设备一起使用它,例如 PCF8591 ADC 模块,则可以使用类似 PiBorg 的 TriBorg(它将 GPIO 端口分成三个部分)。

如何做到这一点...

  1. 创建以下脚本,lcd_i2c.py

    #!/usr/bin/python3
    #lcd_i2c.py
    import wiringpi2
    import time
    import datetime
    import data_local as dataDevice
    
    AF_BASE=100
    AF_E=AF_BASE+13;     AF_RW=AF_BASE+14;   AF_RS=AF_BASE+15
    AF_DB4=AF_BASE+12;   AF_DB5=AF_BASE+11;  AF_DB6=AF_BASE+10
    AF_DB7=AF_BASE+9
    
    AF_SELECT=AF_BASE+0; AF_RIGHT=AF_BASE+1; AF_DOWN=AF_BASE+2
    AF_UP=AF_BASE+3;     AF_LEFT=AF_BASE+4;  AF_BACK=AF_BASE+5
    
    AF_GREEN=AF_BASE+6;  AF_BLUE=AF_BASE+7;  AF_RED=AF_BASE+8
    BNK=" "*16 #16 spaces
    
    def gpiosetup():
      global lcd
      wiringpi2.wiringPiSetup()
      wiringpi2.mcp23017Setup(AF_BASE,0x20)
      wiringpi2.pinMode(AF_RIGHT,0)
      wiringpi2.pinMode(AF_LEFT,0)
      wiringpi2.pinMode(AF_SELECT,0)
      wiringpi2.pinMode(AF_RW,1)
      wiringpi2.digitalWrite(AF_RW,0)
      lcd=wiringpi2.lcdInit(2,16,4,AF_RS,AF_E,
                            AF_DB4,AF_DB5,AF_DB6,AF_DB7,0,0,0,0)
    
    def printLCD(line0="",line1=""):
      wiringpi2.lcdPosition(lcd,0,0)
      wiringpi2.lcdPrintf(lcd,line0+BNK)
      wiringpi2.lcdPosition(lcd,0,1)
      wiringpi2.lcdPrintf(lcd,line1+BNK)
    
    def checkBtn(idx,size):
      global run
      if wiringpi2.digitalRead(AF_LEFT):
        idx-=1
        printLCD()
      elif wiringpi2.digitalRead(AF_RIGHT):
        idx+=1
        printLCD()
      if wiringpi2.digitalRead(AF_SELECT):
        printLCD("Exit Display")
        run=False
      return idx%size
    
    def main():
      global run
      gpiosetup()
      myData = dataDevice.device()
      myDataNames = myData.getName()
      run=True
      index=0
      while(run):
        data = myData.getNew()
        printLCD(myDataNames[index],str(data[index]))
        time.sleep(0.2)
        index = checkBtn(index,len(myDataNames))
    
    main()
    #End
    
  2. 连接 LCD 模块后,按如下方式运行脚本:

    sudo python3 lcd_i2c.py
    
    

使用左右按钮选择要显示的数据通道,然后按SELECT按钮退出。

它是如何工作的...

wiringPi2库对 I/O 扩展器芯片(如用于 AdaFruit LCD 字符模块的芯片)有很好的支持。要使用 Adafruit 模块,我们需要为 MCP23017 端口 A 的所有引脚设置引脚映射,如下表所示(然后我们使用偏移量100设置 I/O 扩展器引脚):

Name SELECT RIGHT DOWN UP LEFT GREEN BLUE RED
MCP23017 PortA A0 A1 A2 A3 A4 A6 A7 A8
WiringPiPin 100 101 102 103 104 106 107 108

MCP23017 端口 B 的所有引脚的引脚映射如下:

Name DB7 DB6 DB5 DB4 E RW RS
MCP23017 PortB B1 B2 B3 B4 B5 B6 B7
WiringPiPin 109 110 111 112 113 114 115

要设置 LCD 屏幕,我们初始化wiringPiSetup()和 I/O 扩展器,mcp23017Setup()。然后我们指定 I/O 扩展器的引脚偏移和总线地址。接下来,我们将所有硬件按钮设置为输入(使用pinMode(pin number,0)),并将 LCD 的 RW 引脚设置为输出。wiringPi2 LCD 库期望 RW 引脚被设置为LOW(强制进入只读模式),因此我们将引脚设置为LOW(使用digitalWrite(AF_RW,0))。

我们通过定义屏幕的行数和列数,以及我们是否使用 4 位或 8 位数据模式(我们使用 8 条数据线中的 4 条,因此是 4 位模式)来创建一个lcd对象。我们还提供了我们使用的引脚映射(最后四条设置为0,因为我们只使用四条数据线)。

现在我们创建一个名为PrintLCD()的函数,它将允许我们向显示的每一行发送字符串。我们使用lcdPosition()lcd对象上设置每行的光标位置,然后打印每行的文本。我们还在每行末尾添加一些空白空间,以确保整行被覆盖。

下一个函数checkBtn()简要检查左右和选择按钮是否被按下(使用digitalRead()函数)。如果左右按钮被按下,则索引设置为数组中的前一个或下一个项目。如果SELECT按钮被按下,则run标志设置为False(这将退出主循环,允许脚本完成)。

以下图示展示了如何将电压稳压器连接到 I/O 扩展器(或其他设备)以提供更多电流来驱动额外的硬件:

还有更多...

使用如 MCP23017 的扩展器芯片提供了一种极好的方法来增加与树莓派的硬件连接数量,同时也提供了一层额外的保护(更换扩展器芯片比更换树莓派便宜)。

I/O 扩展器电压和限制

端口扩展器在使用时仅消耗少量电力,但如果您使用 3.3V 电源供电,那么您从所有引脚中总共只能抽取最大 50 mA 的电流。如果您抽取过多电力,那么您可能会遇到系统冻结或 SD 卡上的读/写损坏。

如果您使用 5V 电源为扩展器供电,那么只要您的 USB 电源足够强大,您就可以抽取扩展器所能支持的最大电流(每个引脚最大约 25 mA,总电流 125 mA)。

我们必须记住,如果扩展器使用 5V 供电,输入/输出和中断线也将是 5V,并且绝不应该连接回树莓派(除非使用电平转换器将电压降至 3.3V)。

通过改变扩展器芯片上的地址引脚(A0、A1 和 A2)的布线,可以同时使用多达八个模块在同一 I²C 总线上。为了确保每个模块都有足够的电流,我们需要使用单独的 3.3V 电源。一个如 LM1117-3.3 的线性稳压器就非常合适(这将在 3.3V 下提供高达 800 mA 的电流,每个模块 100 mA),并且只需要以下简单的电路:

I/O 扩展器电压和限制

LM1117 线性电压稳压器电路

使用您自己的 I/O 扩展器模块

I/O 扩展器电压和限制

使用电压稳压器与树莓派一起使用

输入电压(Vin)由树莓派提供(例如,来自 GPIO 引脚头,如 5V 引脚 2)。然而,Vin 可以由任何其他电源(或电池组)提供,只要它在 4.5V 到 15V 之间,并且能够提供足够的电流。重要的是确保树莓派、电源(如果使用单独的电源)、稳压器和 I/O 扩展器的地连接(GND)都连接在一起(作为公共地)。

使用您自己的 I/O 扩展器模块

您可以使用可用的 I/O 扩展器模块(或以下电路中的 MCP23017 芯片)来控制大多数与 HD44780 兼容的 LCD 显示器:

main()函数调用gpiosetup()来创建我们的lcd对象;然后我们创建我们的dataDevice对象并获取数据名称。在主循环中,我们获取新的数据;然后我们使用我们的printLCD()函数在顶部行显示数据名称,在第二行显示数据值。最后,我们检查按钮是否被按下,并根据需要设置索引到我们的数据。

I/O 扩展器和 HD44780 兼容显示屏

在 第六章 的食谱 The GPIO keypad input 中解释的 D-Pad 电路,也可以连接到扩展器的剩余 Port A 引脚(PA0 到按钮 1,PA1 到右,PA2 到下,PA3 到上,PA4 到左,PA5 到按钮 2)。与前面的示例一样,按钮将是 PA0PA4(WiringPiPin 编号 100 到 104);除此之外,我们还在 PA5(WiringPiPin 编号 105)上添加了第二个按钮。

直接控制 LCD 数码显示屏

或者,您也可以使用以下连接直接从 Raspberry Pi 驱动屏幕:

LCD VSS VDD V0 RS RW E DB4 DB5 DB6 DB7
LCD 引脚 1 2 3 4 5 6 11 12 13 14
Raspberry Pi GPIO 6 (GND) 2 (5V) 对比度 11 13 (GND) 15 12 16 18 22

以下表格列出了 Raspberry Pi 和 HD44780 兼容数码显示屏模块之间所需的连接。

对比度引脚(V0)可以像以前一样连接到一个可变电阻(一边接 5V,另一边接 GND);尽管如此,根据屏幕的不同,您可能发现可以直接连接到 GND/5V 以获得最大对比度。

wiringPi2 LCD 库假定 RW 引脚连接到 GND(只读);这避免了 LCD 在直接连接到 Raspberry Pi 时发送数据的风险(这将是一个问题,因为屏幕由 5V 供电,会使用 5V 逻辑发送数据)。

确保您使用新的 AF_XX 引用更新代码,并在 gpiosetup() 函数中更改设置以通过物理引脚编号进行引用。我们还可以跳过 MCP23017 设备的设置。

查看以下命令:

wiringpi2.wiringPiSetup()
wiringpi2.mcp23017Setup(AF_BASE,0x20)

将前面的命令替换为以下命令:

wiringpi.wiringPiSetupPhys()

您可以看到,我们只需更改引脚引用即可在使用 I/O 扩展器和不使用它之间切换,这显示了 wiringPi2 实现的便利性。

在 SQLite 数据库中捕获数据

数据库是存储大量结构化数据的同时保持访问和搜索特定数据能力的完美方式。结构化查询语言SQL)是一组标准的命令,用于更新和查询数据库。在此示例中,我们将使用 SQLite(一个轻量级的自包含 SQL 数据库系统实现)。

在本章中,我们将从我们的 ADC(或本地数据源)收集原始数据,并构建我们自己的数据库。然后我们可以使用名为 sqlite3 的 Python 库将数据添加到数据库中,然后查询它。

   ##            Timestamp  0:Light  1:Temperature   2:External  3:Potentiometer
    0 2015-06-16 21:30:51      225            212          122              216
    1  2015-06-16 21:30:52      225            212          148              216
    2  2015-06-16 21:30:53      225            212          113              216
    3  2015-06-16 21:30:54      225            212          137              216
    4  2015-06-16 21:30:55      225            212          142              216
    5  2015-06-16 21:30:56      225            212          115              216
    6  2015-06-16 21:30:57      225            212          149              216
    7  2015-06-16 21:30:58      225            212          128              216
    8  2015-06-16 21:30:59      225            212          123              216
    9  2015-06-16 21:31:02      225            212          147              216 

准备工作

要在数据库中捕获数据,我们将安装 SQLite 以与 Python 的 sqlite3 内置模块一起使用。使用以下命令安装 SQLite:

sudo apt-get install sqlite3

接下来,我们将使用 SQLite 执行一些基本操作,以了解如何使用 SQL 查询。

直接运行 SQLite,使用以下命令创建一个新的test.db数据库文件:

sqlite3 test.db
SQLite version 3.7.13 2012-06-11 02:05:22
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite>

这将在其中直接输入 SQL 命令的 SQLite 控制台内打开。例如,以下命令将创建一个新表,添加一些数据,显示内容,然后删除表:

CREATE TABLE mytable (info TEXT, info2 TEXT,);
INSERT INTO mytable VALUES ("John","Smith");
INSERT INTO mytable VALUES ("Mary","Jane");
John|Smith
Mary|Jane
DROP TABLE mytable;
.exit

你需要与之前配方中详细说明的准备就绪部分相同的硬件设置。

如何做...

创建以下脚本,mysqlite_adc.py:

#!/usr/bin/python3
#mysql_adc.py
import sqlite3
import datetime
import data_adc as dataDevice
import time
import os

DEBUG=True
SHOWSQL=True
CLEARDATA=False
VAL0=0;VAL1=1;VAL2=2;VAL3=3 #Set data order
FORMATBODY="%5s %8s %14s %12s %16s"
FORMATLIST="%5s %12s %10s %16s %7s"
DATEBASE_DIR="/var/databases/datasite/"
DATEBASE=DATEBASE_DIR+"mydatabase.db"
TABLE="recordeddata"
DELAY=1 #approximate seconds between samples

def captureSamples(cursor):
    if(CLEARDATA):cursor.execute("DELETE FROM %s" %(TABLE))
    myData = dataDevice.device()
    myDataNames=myData.getName()

    if(DEBUG):print(FORMATBODY%("##",myDataNames[VAL0],
                                myDataNames[VAL1],myDataNames[VAL2],
                                myDataNames[VAL3]))
    for x in range(10):
        data=myData.getNew()
        for i,dataName in enumerate(myDataNames):
            sqlquery = "INSERT INTO %s (itm_name, itm_value) " %(TABLE) + \
                       "VALUES('%s', %s)" \
                        %(str(dataName),str(data[i]))
            if (SHOWSQL):print(sqlquery)
            cursor.execute(sqlquery)

        if(DEBUG):print(FORMATBODY%(x,
                                    data[VAL0],data[VAL1],
                                    data[VAL2],data[VAL3]))
        time.sleep(DELAY)
    cursor.commit()

def displayAll(connect):
    sqlquery="SELECT * FROM %s" %(TABLE)
    if (SHOWSQL):print(sqlquery)
    cursor = connect.execute (sqlquery)
    print(FORMATLIST%("","Date","Time","Name","Value"))

    for x,column in enumerate(cursor.fetchall()):
       print(FORMATLIST%(x,str(column[0]),str(column[1]),
                         str(column[2]),str(column[3])))

def createTable(cursor):
    print("Create a new table: %s" %(TABLE))
    sqlquery="CREATE TABLE %s (" %(TABLE) + \
             "itm_date DEFAULT (date('now','localtime')), " + \
             "itm_time DEFAULT (time('now','localtime')), " + \
             "itm_name, itm_value)" 
    if (SHOWSQL):print(sqlquery)
    cursor.execute(sqlquery)
    cursor.commit()

def openTable(cursor):
    try:
        displayAll(cursor)
    except sqlite3.OperationalError:
        print("Table does not exist in database")
        createTable(cursor)
    finally:
        captureSamples(cursor)
        displayAll(cursor)

try:
    if not os.path.exists(DATEBASE_DIR):
        os.makedirs(DATEBASE_DIR)
    connection = sqlite3.connect(DATEBASE)
    try:
        openTable(connection)
    finally:
        connection.close()
except sqlite3.OperationalError:
    print("Unable to open Database")
finally:
    print("Done")

#End

注意

如果你没有 ADC 模块硬件,你可以通过将dataDevice模块设置为data_local来捕获本地数据。确保你有一个data_local.py(来自还有更多…部分中的使用模拟数字转换器读取模拟数据配方),它与该脚本位于同一目录下:

import data_local as dataDevice

这将捕获本地数据(RAM、CPU 活动、温度等)到 SQLite 数据库,而不是 ADC 样本。

工作原理...

当脚本首次运行时,它将创建一个新的 SQLite 数据库文件,名为mydatabase.db,并添加一个名为recordeddata的表。该表由createTable()生成,它运行以下 SQLite 命令:

CREATE TABLE recordeddata
(
    itm_date DEFAULT (date('now','localtime')),
    itm_time DEFAULT (time('now','localtime')),
    itm_name,
    itm_value
)

新表将包含以下数据项:

Name Description
itm_date 用于存储数据样本的日期。当创建数据记录时,当前日期(使用date('now','localtime'))作为默认值应用。
itm_time 用于存储数据样本的时间。当创建数据记录时,当前时间(使用time('now','localtime'))作为默认值应用。
itm_name 用于记录样本的名称。
itm_value 用于保存采样值。

我们然后使用与之前在记录和绘图数据配方中相同的方法从 ADC 捕获十个数据样本(如函数captureSamples()所示)。然而,这次我们将添加捕获的数据到我们新的 SQLite 数据库表中,使用以下 SQL 命令(通过cursor.execute(sqlquery)应用):

INSERT INTO recordeddata
    (itm_name, itm_value) VALUES ('0:Light', 210)

每条记录在创建时都会默认添加当前日期和时间。最终我们得到一组 40 条记录(每个 ADC 采样周期有 4 条记录),这些记录现在存储在 SQLite 数据库中。

工作原理...

已捕获并存储在 SQLite 数据库中的 ADC 样本有 8 个

在创建记录后,我们必须记得调用cursor.commit(),这将保存所有新记录到数据库中。

脚本的最后部分调用displayAll(),它将使用以下 SQL 命令:

 SELECT * FROM recordeddata

这将选择recordeddata表中的所有数据记录,我们使用cursor.fetch()提供选定的数据作为我们可以迭代的列表:

    for x,column in enumerate(cursor.fetchall()):
       print(FORMATLIST%(x,str(column[0]),str(column[1]),
                         str(column[2]),str(column[3])))

这允许我们打印出数据库的全部内容,显示捕获的数据。

注意

注意,在这里我们使用 tryexceptfinally 构造来尝试处理用户在运行脚本时可能遇到的大部分情况。

首先,我们确保如果数据库目录不存在,我们创建它。接下来,我们尝试打开数据库文件;如果不存在数据库文件,此过程将自动创建一个新的数据库文件。如果这些初始步骤中的任何一个失败(例如,由于没有读写权限),我们无法继续,因此我们报告无法打开数据库并简单地退出脚本。

接下来,我们尝试在数据库中打开所需的表并显示它;如果数据库文件是全新的,此操作将始终失败,因为它将是空的。然而,如果发生这种情况,我们只是捕获异常,在继续脚本以将样本数据添加到表并显示之前创建表。

这允许脚本优雅地处理潜在的问题,采取纠正措施,然后继续顺利运行。下次运行脚本时,数据库和表已经存在,因此我们不需要再次创建它们,我们可以将样本数据追加到同一数据库文件中的表内。

还有更多...

可用的 SQL 服务器版本有很多(例如 MySQL、Microsoft SQL Server 或 PostgreSQL);然而,它们至少应该具备以下主要命令(或等效命令):

CREATE, INSERT, SELECT, WHERE, UPDATE, SET, DELETE, and DROP

你应该会发现,即使你选择使用与这里使用的 SQLite 不同的 SQL 服务器,SQL 命令也将相对相似。

CREATE

CREATE TABLE 命令用于通过指定列名(如果需要,还可以设置默认值)来定义新表:

CREATE TABLE table_name (
    column_name1 TEXT, 
    column_name2 INTEGER DEFAULT 0,
    column_name3 REAL )

之前的 SQL 命令将创建一个名为 table_name 的新表,包含三个数据项。一列将包含文本,另一列包含整数(例如,1、3、-9),最后是一列用于实数(例如,5.6、3.1749、1.0)。

INSERT

INSERT 命令将向数据库中的表添加特定的条目:

INSERT INTO table_name (column_name1name1, column_name2name2, column_name3)name3)
    VALUES ('Terry'Terry Pratchett', 6666, 27.082015)082015)

这将把提供的值输入到表中的相应列中。

SELECT

SELECT 命令允许我们指定数据库表中的特定列或列,返回包含数据的记录列表:

SELECT column_name1, column_name2 FROM table_name

或者要选择所有项目,使用此命令:

SELECT * FROM table_name

WHERE

WHERE 命令用于指定要选择、更新或删除的具体条目:

SELECT * FROM table_name
    WHERE column_name1= 'Terry Pratchett'

这将 SELECT 任何 column_name1'Terry Pratchett' 匹配的记录。

UPDATE

UPDATE 命令将允许我们更改(SET)指定列中的数据值。我们还可以将此与 WHERE 命令结合使用,以限制更改应用到的记录:

UPDATE table_name
    SET column_name2=49name2=49,column_name3=30name3=30.111997
    WHERE column_name1name1= 'Douglas Adams'Adams';

DELETE

DELETE 命令允许使用 WHERE 选择的任何记录从指定的表中删除。但是,如果选择整个表,使用 DELETE * FROM table_name 将删除表中的全部内容:

DELETE FROM table_name
    WHERE columncolumn_name2=9999

DROP

DROP 命令允许将表完全从数据库中删除:

DROP table_name

警告:这将永久删除指定表中存储的所有数据及其结构。

查看您自己的网络服务器上的数据

收集和整理信息到数据库非常有用,但如果它们被锁在数据库或文件中,就没什么用了。然而,如果我们允许通过网页查看存储的数据,它将更容易访问;我们不仅可以从其他设备查看数据,还可以与同一网络上的其他人共享它。

我们将创建一个本地网络服务器来查询和显示捕获的 SQLite 数据,并通过 PHP 网络界面允许查看。这将允许数据不仅可以通过 Raspberry Pi 的网络浏览器查看,还可以在本地网络上的其他设备,如手机或平板电脑上查看:

查看您自己的网络服务器上的数据

通过网页显示的 SQLite 数据库中捕获的数据

使用网络服务器来输入和显示信息是允许广泛用户与您的项目互动的一种强大方式。以下示例演示了一个可以针对各种用途定制的网络服务器设置。

准备工作

确保您已完成了前面的食谱,以便传感器数据已被收集并存储在 SQLite 数据库中。我们需要安装一个网络服务器(Apache2)并启用 PHP 支持以允许 SQLite 访问。

使用以下命令安装一个网络服务器和 PHP:

sudo apt-get update
sudo aptitude install apache2 php5 php5-sqlite

/var/www/ 目录由网络服务器使用;默认情况下,它将加载 index.html(或 index.php),否则它将仅显示目录内文件的链接列表。

要测试网络服务器是否正在运行,创建一个默认的 index.html 页面。为此,您需要使用 sudo 权限创建文件(/var/www/ 目录受普通用户更改的保护)。使用以下命令:

sudo nano /var/www/index.html

创建包含以下内容的 index.html

<h1>It works!</h1>

关闭并保存文件(使用 Ctrl + XYEnter)。

如果您使用带有屏幕的 Raspberry Pi,可以通过加载桌面来检查它是否工作:

startx

然后,打开网络浏览器(epiphany-browser)并将 http://localhost 作为地址输入。您应该看到以下测试页面,表明网络服务器正在运行:

准备工作

Raspberry Pi 浏览器显示的测试页面,位于 http://localhost

如果您远程使用 Raspberry Pi 或它连接到您的网络,您也应该能够在您的网络上另一台计算机上查看该页面。首先,确定 Raspberry Pi 的 IP 地址(使用 sudo hostname -I),然后在您的网络浏览器中使用此地址。您甚至可能发现您可以使用 Raspberry Pi 的实际主机名(默认情况下这是 raspberrypi/)。

注意

如果您无法从另一台计算机上看到网页,请确保您没有启用防火墙(在计算机本身或您的路由器上),这可能正在阻止它。

接下来,我们可以测试 PHP 是否运行正确。我们可以创建以下网页 test.php 并确保它位于 /var/www/ 目录中:

<?php
phpinfo();
?>;

准备中

在以下位置查看 test.php 页面:http://localhost/test.php

我们已经准备好编写自己的 PHP 网页来查看 SQLite 数据库中的数据。

如何操作...

在 web 服务器目录 /var/www/./ 中创建以下 PHP 文件并保存:

使用以下命令创建 PHP 文件:

sudo nano /var/www/show_data_lite.php

show_data_lite.php 文件应包含:

<head>
<title>DatabaseDatabase Data</title>
<meta http-equiv="refresh" content="10" >
</head>
<body>

Press button to remove the table data
<br>
<input type="button" onclick="location.href = 'del_data_lite.php';" value="Delete">
<br><br>
<b>Recorded Data</b><br>
<?php
$db = new PDO("sqlite:/var/databases/datasitedatasite/mydatabase.db");
//SQL query
$strSQL = "SELECT * FROM recordeddatarecordeddata WHERE itmitm_name LIKE '%'%temp%'";
//Excute the query
$response = $db->query($strSQL);
//Loop through the response
while($column = $response->fetch())
{
   //Display the content of the response
   echo $column[0] . " ";
   echo $column[1] . " ";
   echo $column[2] . " ";
   echo $column[3] . "<br />";
}
?>
Done
</body>
</html>

使用以下命令创建 PHP 文件:

sudo nano /var/www/del_data_lite.php
<html>
<body>

Remove all the data in the table.
<br>
<?php
$db = new PDO("sqlite:/var/databases/datasitedatasite/mydatabase.db");
//SQL query
$strSQL = "DROPDROP TABLErecordeddata recordeddata";
//ExecuteExecute the query
$response = $db->query($strSQL);

if ($response == 1)
{
 echo "Result: DELETED DATA";
}
else
{
 echo "Error: Ensure table exists and database directory is owned by www-data";
}
?>
<br><br>
Press button to return to data display.
<br>
<input type="button" onclick="location.href = 'show'show_data_lite.php';" value="Return">

</body>
</html>

注意

注意:为了让 PHP 代码能够删除数据库中的表,它需要被 web 服务器可写。使用以下命令来允许它可写:

sudo chown www-data /var/databases/datasite -R

如果您通过以下地址在网页浏览器中打开 show_data_lite.php,它将显示为一个网页:

http://localhost/showshow_data_lite.php

或者,您可以通过引用树莓派的 IP 地址(使用 hostname -I 来确认 IP 地址)来打开网页(包括您网络中的另一台计算机):

http://192.168.1.101/showshow_data_lite.php

您可能可以使用主机名代替(默认情况下,这将使地址为 raspberrypi/show_data_lite.php)。然而,这可能取决于您的网络设置。

如果没有数据,请确保您运行 mysqlite_adc.py 脚本来捕获更多数据。

要在访问您的树莓派的网页地址时自动显示 show_data_lite.php 页面(而不是 "It works!" 页面),我们可以将 index.html 改为以下内容:

<meta http-equiv="refresh" content="0; URL='show_data_lite.php' " />

这将自动将浏览器重定向到加载我们的 show_data_lite.php 页面。

工作原理...

show_data_lite.php 文件应显示存储在 SQLite 数据库中的温度数据(无论是来自 ADC 样本还是本地数据源)。

show_data_lite.php 文件由标准的 HTML 代码以及特殊的 PHP 代码部分组成。HTML 代码将 ACD 数据 设置为页面标题部分的标题,并使用以下命令使页面每 10 秒自动重新加载:

<meta http-equiv="refresh" content="10" >

接下来,我们定义一个 删除 按钮,当点击时将加载 del_data_lite.php 页面:

<input type="button" onclick="location.href = 'del_data_lite.php';" value="Delete">

最后,我们使用 PHP 代码部分来加载 SQLite 数据库并显示通道 0 的数据。

我们使用以下 PHP 命令来打开我们之前存储数据的 SQLite 数据库(位于 /var/databases/testsites/mydatabase.db):

$db = new PDO("sqlite:/var/databases/testsite/mydatabase.db");

接下来,我们使用以下 SQLite 查询来选择所有文本中包含 0: 的条目(例如,0:Light):

SELECT * FROM recordeddatarecordeddata WHERE itm_namename LIKE '%temp%''

注意

注意,尽管我们现在使用 PHP,但我们与 SQLite 数据库使用的查询与使用 SQLite3 Python 模块时相同。

我们现在将查询结果收集到 $response 变量中:

$response = $db->query($strSQL);
Allowing us to use fetch() (like we used cursor.fetchall() previously) to list all the data columns in each of the data entries within the response.
while($column = $response->fetch())
{
   //Display the content of the response
   echo $column[0] . " ";
   echo $column[1] . " ";
   echo $column[2] . " ";
   echo $column[3] . "<br />";
}
?>

del_data_lite.php 文件相当相似;它首先像之前一样重新打开 mydatabase.db 文件。然后执行以下 SQLite 查询:

DROP TABLE recordeddata

如在更多... DROP部分所述,这将从数据库中删除recordeddata表。如果response不等于 1,则操作未完成。最可能的原因是包含mydatabase.db文件的目录无法被 Web 服务器写入(请参阅如何做部分中关于更改文件所有者为www-data的说明)。

最后,我们提供了一个按钮,用户点击后会返回到show_data_lite.php页面(这将显示记录的数据已经被清除)。

如何工作...

del_data_lite.php 页面包括一个按钮,用于返回到数据展示页面(show_data_lite.php)。

还有更多...

你可能已经注意到,这个食谱更多地关注 HTML 和 PHP 而不是 Python(是的,看看封面,这仍然是一本针对 Python 程序员的书籍!)。然而,重要的是要记住,工程的关键部分是将不同的技术集成和组合起来以产生预期的结果。

按照设计,Python 非常适合这类任务,因为它允许轻松定制并与大量其他语言和模块集成。我们本可以只用 Python 来完成所有这些...但为什么不利用现有的解决方案呢?毕竟,它们通常有很好的文档记录,经过广泛的测试,并且通常是行业标准。

安全性

SQL 数据库被用于许多地方来存储从产品信息到客户详情的各种信息。在这种情况下,用户可能需要输入信息,然后这些信息会被形成 SQL 查询。在一个实施不当的系统里,恶意用户可能能够在他们的响应中包含额外的 SQL 语法,从而允许他们破坏 SQL 数据库(例如访问敏感信息、修改它或简单地删除它)。

例如,当在网页中请求用户名时,用户可能会输入以下文本:

John; DELETE FROM Orders

如果直接使用它来构造 SQL 查询,我们最终会得到以下结果:

SELECT * FROM Users WHERE UserName = John; DELETE FROM CurrentOrders

结果是我们刚刚允许攻击者删除CurrentOrders表中的所有内容!

使用用户输入来形成 SQL 查询的一部分意味着我们必须小心允许执行哪些命令。在这个例子中,用户可能能够删除可能非常重要的信息,这对公司及其声誉可能造成很大的损失。

这种技术被称为 SQL 注入,可以通过使用 SQLite execute()函数的参数选项轻松防范。我们可以将我们的 Python SQLite 查询替换为一个更安全的版本,如下所示:

sqlquery = "INSERT INTO %s (itm_name, itm_value) VALUES(?, ?)" %(TABLE)
cursor.execute(sqlquery, (str(dataName), str(data[i]))

而不是盲目地构建 SQL 查询,SQLite 模块将首先检查提供的参数是否是数据库中可以输入的有效值,然后它会确保将这些参数插入到命令中不会产生额外的 SQL 操作。最后,dataNamedata[i]参数的值将被用来替换?字符,生成最终的安全的 SQLite 查询。

使用 MySQL 代替

在本食谱中使用的 SQLite 只是许多 SQL 数据库中的一种。它适用于只需要相对较小的数据库和最少资源的较小项目。然而,对于需要额外功能(如用户账户以控制访问和额外的安全性)的较大项目,您可以使用 MySQL 等替代品。

要使用不同的 SQL 数据库,您需要调整我们用来捕获条目的 Python 代码,以使用合适的 Python 模块。

对于 MySQL(mysql-server),我们可以使用一个与 Python 3 兼容的库,称为PyMySQL,来与之接口。有关如何使用此库的更多信息,请参阅 PyMySQL 网站(github.com/PyMySQL/PyMySQL)。

要使用 PHP 与 MySQL,你还需要 PHP MySQL(php5-mysql);更多信息,请参阅 W3 Schools 上的优秀资源(www.w3schools.com/php/php_mysql_connect.asp)。

您会注意到,尽管 SQL 实现之间有细微差别,但无论您选择哪个,一般概念和命令现在都应该熟悉。

感应并发送数据到在线服务

在本节中,我们将使用一个名为 Xively 的在线服务;该服务允许我们在线连接、传输和查看数据。Xively 使用一个常见的协议,即用于通过 HTTP 传输信息的 REST 协议。许多服务,如 Facebook 和 Twitter,都使用各种密钥和访问令牌来确保数据在授权应用程序和验证网站之间安全传输。

您可以使用一个名为requests的 Python 库手动执行大多数 REST 操作(如POSTGETSET等),docs.python-requests.org

然而,通常更容易使用为打算使用的服务提供的特定库。它们将处理授权过程;提供访问函数;如果服务发生变化,则可以更新库而不是您的代码。

我们将使用xively-python库,它提供了 Python 函数,使我们能够轻松地与该网站交互。

有关xively-python库的详细信息,请参阅xively.github.io/xively-python/

Xively 收集的数据如下截图所示:

感应并发送数据到在线服务

Xively 收集并绘制使用 REST 传输的数据

准备就绪

您需要在www.xively.com上创建一个账户,我们将使用它来接收我们的数据。访问网站并注册一个免费的开发者账户(通过开发者部分,personal.xively.com/signup)。

准备就绪

注册并创建 Xively 账户

在注册并验证您的账户后,您可以按照说明进行测试驾驶示例。这将演示如何将数据从您的智能手机(陀螺仪数据、位置等)链接到,这将让您尝到我们可以用树莓派做什么。

当你登录时,你将被带到开发设备仪表板(位于WebTools下拉菜单中):

准备就绪

添加新设备

选择+添加设备并填写详细信息,给你的设备起一个名字,并将设备设置为私有

你现在将看到你远程设备的控制页面,其中包含你连接所需的所有信息,以及你的数据将显示的位置。

准备就绪

示例 API 密钥和频道编号(这将为您设备唯一)

虽然这个页面上有很多信息,但你只需要其中的两部分:

  • API 密钥(在API Keys部分的长代码),如下所示:

    API_KEY = CcRxJbP5TuHp1PiOGVrN2kTGeXVsb6QZRJU236v6PjOdtzze
    
  • 频道编号(在API Keys部分提及,并在页面顶部列出),如下所示:

    FEED_ID = 399948883
    

现在我们有了连接 Xively 所需的详细信息,我们可以专注于树莓派这一方面。

我们将使用 pip-3.2 来安装 Xively,如下所示:

sudo pip-3.2 install xively-python

确保安装报告显示以下内容:

Successfully installed xively-python requests

你现在可以开始从你的树莓派发送一些数据了。

如何操作...

创建以下脚本,xivelyLog.py。确保在代码中设置FEED_IDAPI_KEY以匹配您创建的设备:

#!/usr/bin/env python3
#xivelylog.py
import xively
import time
import datetime
import requests
from random import randint
import data_local as dataDevice

# Set the FEED_ID and API_KEY from your account
FEED_ID = 399948883
API_KEY = "CcRxJbP5TuHp1PiOGVrN2kTGeXVsb6QZRJU236v6PjOdtzze"
api = xively.XivelyAPIClient(API_KEY) # initialize api client
DEBUG=True

myData = dataDevice.device()
myDataNames=myData.getName()

def get_datastream(feed,name,tags):
  try:
    datastream = feed.datastreams.get(name)
    if DEBUG:print ("Found existing datastream")
    return datastream
  except:
    if DEBUG:print ("Creating new datastream")
    datastream = feed.datastreams.create(name, tags=tags)
    return datastream

def run():
  print ("Connecting to Xively")
  feed = api.feeds.get(FEED_ID)
  if DEBUG:print ("Got feed" + str(feed))
  datastreams=[]
  for dataName in myDataNames:
    dstream = get_datastream(feed,dataName,dataName)
    if DEBUG:print ("Got %s datastream:%s"%(dataName,dstream))
    datastreams.append(dstream)

  while True:
    data=myData.getNew()
    for idx,dataValue in enumerate(data):
      if DEBUG:
        print ("Updating %s: %s" % (dataName,dataValue))
      datastreams[idx].current_value = dataValue
      datastreams[idx].at = datetime.datetime.utcnow()
    try:
      for ds in datastreams:
        ds.update()
    except requests.HTTPError as e:
      print ("HTTPError({0}): {1}".format(e.errno, e.strerror))
    time.sleep(60)

run()
#End

它是如何工作的...

首先,我们初始化 Xively API 客户端,并向其提供API_KEY(这授权我们向之前创建的Xively设备发送数据)。接下来,我们使用FEED_ID将我们链接到我们想要发送数据的特定频道。最后,我们请求要连接的数据流(如果它已经在频道中存在,get_datastream()函数将为我们创建一个)。

对于频道中的每个数据流,我们提供一个name函数和tags(这些是帮助我们识别数据的关键词;我们可以使用我们的数据名称来这样做)。

一旦我们定义了数据流,我们进入main循环;在这里,我们从dataDevice收集我们的数据值。然后我们设置每个数据项的current_value函数以及数据的时间戳,并将其应用于我们的数据流对象。

最后,当所有数据准备就绪时,我们更新每个数据流,并将数据发送到 Xively,几分钟后在设备的仪表板上显示。

我们可以使用标准网络浏览器登录到我们的 Xively 账户,并查看传入的数据。这提供了在任何地方发送数据和远程监控数据的方法(如果需要,可能一次从多个 Raspberry Pi 上进行)。该服务甚至支持创建触发器,如果某些项目超出预期范围、达到特定值或匹配设置的标准,则可以发送额外的消息。这些触发器反过来可以用来控制其他设备或触发警报等。

参见

AirPi 空气质量和天气项目 (airpi.es) 展示了如何添加您自己的传感器或使用他们的 AirPi 套件来创建您自己的空气质量和水文站(并将数据记录到您的 Xively 账户)。该网站还允许您与世界各地的其他人共享您的 Xively 数据流。

第八章。使用 Raspberry Pi 摄像头模块创建项目

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

  • 开始使用 Raspberry Pi 摄像头模块

  • 使用 Python 使用摄像头

  • 生成延时视频

  • 创建定格动画

  • 制作 QR 码阅读器

  • 探索和实验 OpenCV

  • 使用 OpenCV 进行颜色检测

  • 使用 OpenCV 进行运动跟踪

简介

Raspberry Pi 摄像头模块是 Raspberry Pi 的一个特殊附加组件,它利用摄像头串行接口CSI连接器。它直接连接到 Raspberry Pi 处理器的 GPU 核心,允许直接在单元上捕获图像。

我们将使用在第三章和第四章中使用的tkinter库创建一个基本的图形用户界面GUI)。这些章节分别是《使用 Python 进行自动化和生产率》和《创建游戏和图形》。这将构成以下三个示例的基础,在这些示例中,我们将通过添加额外的控件来扩展 GUI,以便我们可以将相机用于各种不同的项目。

最后,我们将设置功能强大的开放计算机视觉OpenCV)库以执行一些高级图像处理。我们将学习 OpenCV 的基础知识,并使用它根据颜色跟踪对象或检测运动。

小贴士

本章使用 Raspberry Pi 摄像头模块,该模块可在附录中列出的大多数零售商处获得,该附录位于Makers, hobbyists, and Raspberry Pi specialists部分,即《硬件和软件列表》。

开始使用 Raspberry Pi 摄像头模块

我们将首先安装和设置 Raspberry Pi 摄像头模块;然后我们将创建一个小的相机 GUI,使我们能够预览和拍照。我们将创建的第一个 GUI 如图所示:

开始使用 Raspberry Pi 摄像头模块

Raspberry Pi 摄像头模块的基本相机 GUI

准备工作

Raspberry Pi 摄像头模块由一个安装在小型印刷电路板PCB)上的摄像头组成,该电路板通过小型扁平电缆连接。扁平电缆可以直接连接到 Raspberry Pi 板的 CSI 端口(标记为S5,该端口位于 Raspberry Pi 上的 USB 和 HDMI 端口之间)。以下图像显示了 Raspberry Pi 摄像头模块:

准备工作

Raspberry Pi 摄像头模块

Raspberry Pi 基金会提供了有关如何在www.raspberrypi.org/archives/3890安装摄像头的详细说明(以及视频);执行以下步骤:

  1. 首先,将相机安装如图所示(确保您首先已将 Raspberry Pi 从任何电源断开):准备工作

    摄像头模块的接插件位于 HDMI 插座旁边

    要将扁平电缆插入 CSI 插座,您需要轻轻抬起并松开扁平电缆插座的卡扣。将扁平电缆插入带有金属触点的插槽中,面向 HDMI 端口。注意不要弯曲或折叠扁平电缆,确保它在插座中牢固且水平,然后再将卡扣推回原位。

  2. 最后,启用摄像头。您可以通过 Raspbian 桌面上的 Raspberry Pi 配置 GUI 来完成此操作(通过 接口 菜单打开)。准备中

    通过 Raspberry Pi 配置屏幕中的 接口 选项卡启用 Raspberry Pi 摄像头

或者,您也可以通过命令行使用 raspi-config 来完成此操作。使用 sudo raspi-config 来运行它,找到 启用摄像头 的菜单项,并启用它。之后,您将被提示重新启动。

如何操作…

您可以使用作为升级部分安装的两个程序——raspividraspistill——来测试摄像头。

要拍摄一张照片,请使用以下命令(-t 0 立即拍照):

raspistill -o image.jpg -t 0

要以 H.264 格式拍摄一个短的视频,时长为 10 秒,请使用以下命令(-t 值以毫秒为单位):

raspivid -o video.h264 -t 10000

它是如何工作的…

摄像头和 raspividraspistill 工具的完整文档可在 Raspberry Pi 网站上找到,链接为 www.raspberrypi.org/wp-content/uploads/2013/07/RaspiCam-Documentation.pdf

小贴士

要获取有关每个程序的信息,您可以使用 less 命令查看说明(使用 q 退出)如下所示:

raspistill > less
raspivid > less

每个命令都提供了对摄像头设置的全面控制,例如曝光、白平衡、锐度、对比度、亮度和分辨率。

使用 Python 操作摄像头

Raspberry Pi 上的摄像头模块不仅仅是一个标准网络摄像头。由于我们能够从自己的程序中完全访问控制和设置,它允许我们掌握控制权并创建自己的摄像头应用程序。

在本章中,我们将使用由 Dave Hughes 创建的名为 picamera 的 Python 模块来控制摄像头模块,该模块执行 raspividraspistill 所支持的所有功能。

请参阅 picamera.readthedocs.org 以获取更多文档和大量有用的示例。

准备工作

Raspberry Pi 摄像头模块应按照上一节中的详细说明连接和安装。

此外,我们还需要安装 Python 3 Pillow 库(如何在 第三章的 在应用程序中显示照片信息 菜谱中完成此操作的详细信息已涵盖),使用 Python 进行自动化和生产率

现在,使用以下命令为 Python 3 安装 picamera

sudo apt-get install python3-picamera

如何操作…

  1. 创建以下cameraGUI.py脚本,该脚本应包含 GUI 的主类:

    #!/usr/bin/python3
    #cameraGUI.py
    import tkinter as TK
    from PIL import Image
    import subprocess
    import time
    import datetime
    import picamera as picam
    
    class SET():
      PV_SIZE=(320,240)
      NORM_SIZE=(2592,1944)
      NO_RESIZE=(0,0)
      PREVIEW_FILE="PREVIEW.gif"
      TEMP_FILE="PREVIEW.ppm"
    
    class cameraGUI(TK.Frame):
      def run(cmd):
        print("Run:"+cmd)
        subprocess.call([cmd], shell=True)
      def camCapture(filename,size=SET.NORM_SIZE):
        with picam.PiCamera() as camera:
          camera.resolution = size
          print("Image: %s"%filename)
          camera.capture(filename)
      def getTKImage(filename,previewsize=SET.NO_RESIZE):
        encoding=str.split(filename,".")[1].lower()
        print("Image Encoding: %s"%encoding)
        try:
          if encoding=="gif" and previewsize==SET.NO_RESIZE:
            theTKImage=TK.PhotoImage(file=filename)
          else:
            imageview=Image.open(filename)
            if previewsize!=SET.NO_RESIZE:
              imageview.thumbnail(previewsize,Image.ANTIALIAS)
            imageview.save(SET.TEMP_FILE,format="ppm")
            theTKImage=TK.PhotoImage(file=SET.TEMP_FILE)
        except IOError:
          print("Unable to get: %s"%filename)
        return theTKImage
      def timestamp():
        ts=time.time() 
        tstring=datetime.datetime.fromtimestamp(ts)
        return tstring.strftime("%Y%m%d_%H%M%S")
    
      def __init__(self,parent):
        self.parent=parent
        TK.Frame.__init__(self,self.parent)
        self.parent.title("Camera GUI")
        self.previewUpdate = TK.IntVar()
        self.filename=TK.StringVar()
        self.canvas = TK.Canvas(self.parent,
                                width=SET.PV_SIZE[0],
                                height=SET.PV_SIZE[1])
        self.canvas.grid(row=0,columnspan=4)
        self.shutterBtn=TK.Button(self.parent,text="Shutter",
                                        command=self.shutter)
        self.shutterBtn.grid(row=1,column=0)
        exitBtn=TK.Button(self.parent,text="Exit",
                                 command=self.exit)
        exitBtn.grid(row=1,column=3)
        previewChk=TK.Checkbutton(self.parent,text="Preview",
                                  variable=self.previewUpdate)
        previewChk.grid(row=1,column=1)
        labelFilename=TK.Label(self.parent,
                               textvariable=self.filename)
        labelFilename.grid(row=2,column=0,columnspan=3)
        self.preview()
      def msg(self,text):
        self.filename.set(text)
        self.update()
      def btnState(self,state):
        self.shutterBtn["state"] = state
      def shutter(self):
        self.btnState("disabled")
        self.msg("Taking photo...")
        self.update()
        if self.previewUpdate.get() == 1:
          self.preview()
        else:
          self.normal()
        self.btnState("active")
      def normal(self):
        name=cameraGUI.timestamp()+".jpg"
        cameraGUI.camCapture(name,SET.NORM_SIZE)
        self.updateDisp(name,previewsize=SET.PV_SIZE)
        self.msg(name)
      def preview(self):
        cameraGUI.camCapture(SET.PREVIEW_FILE,SET.PV_SIZE)
        self.updateDisp(SET.PREVIEW_FILE)
        self.msg(SET.PREVIEW_FILE)
      def updateDisp(self,filename,previewsize=SET.NO_RESIZE):
        self.msg("Loading Preview...")
        self.myImage=cameraGUI.getTKImage(filename,previewsize)
        self.theImage=self.canvas.create_image(0,0,
                                      anchor=TK.NW,
                                      image=self.myImage)
        self.update()
      def exit(self):
        exit()
    #End
    
  2. 接下来,创建以下cameraGUI1normal.py文件以使用 GUI:

    #!/usr/bin/python3
    #cameraGUI1normal.py
    import tkinter as TK
    import cameraGUI as GUI
    
    root=TK.Tk()
    root.title("Camera GUI")
    cam=GUI.cameraGUI(root)
    TK.mainloop()
    #End
    
  3. 使用以下命令运行示例:

    python3 cameraGUI1normal.py
    
    

如何工作…

cameraGUI.py文件中,我们使用一个名为SET的类来包含应用程序的设置(你将在下面的示例中看到为什么这特别有用,并允许我们将所有对设置的引用都放在一个地方)。

我们将定义一个名为cameraGUI的基类(这样我们就可以将其附加到 Tkinter 对象上),它继承自TK.Frame类。cameraGUI类将包含创建 Tkinter 应用程序所需的所有方法,包括布局控件和提供所有必需的函数。

我们为该类定义了以下三个实用函数:

  • run(): 此函数将允许我们使用subprocess.call在命令行上发送要运行的命令(我们将在下面的示例中使用subprocess.call来执行视频编码和其他应用程序)。

  • getTKImage(): 此函数将允许我们创建一个适合在 Tkinter 画布上显示的TK.PhotoImage对象。Tkinter 画布无法直接显示 JPG 图像,因此我们使用Pillow 库PIL)将其调整大小以进行显示,并将其转换为PPM文件(可移植像素图格式,支持比 GIF 更多的颜色)。由于此转换和调整大小过程可能需要几秒钟,我们将使用 GIF 图像来提供快速的相机预览图像。

  • timestamp(): 此函数将允许我们生成一个时间戳字符串,我们可以使用它来自动命名我们拍摄的任何图像。

在类初始化器(__init__())中,我们定义所有控制变量,生成我们想要使用的所有 GUI 对象和控件,并使用grid()函数定位对象。GUI 布局如图所示:

如何工作…

相机 GUI 布局

我们定义以下控制变量:

  • self.previewUpdate: 这与预览复选框(previewChk)的状态相关联

  • self.filename: 这与labelFilename小部件显示的文本相关联

我们还将快门按钮(shutterBtn)链接到self.shutter(),每当按下快门按钮时,都会调用此函数,并将退出按钮(exitBtn)链接到self.exit()函数。

最后,在__init__()函数中,我们调用self.preview(),这将确保相机 GUI在应用程序启动后立即拍照并显示。

当按下快门按钮时,会调用self.shutter()。这会调用this.btnState("disabled")来禁用快门按钮,在我们拍摄新照片时,这将防止拍摄任何照片。当其他操作完成时,使用this.btnState("active")来重新启用按钮。

self.shutter()函数将根据预览复选框的状态(通过获取self.previewUpdate的值)调用self.normal()self.preview()函数。

cameraGUI.camCapture()函数使用pycamera创建摄像头对象,设置分辨率,并使用所需的文件名捕获图像。self.preview()函数使用在SET类中定义的PV_SIZE分辨率的一个名为PREVIEW_FILE的图像。

接下来,调用self.updateDisp(PREVIEW_FILE),它将使用cameraGUI.getTKImage()打开生成的PREVIEW.gif文件作为TK.PhotoImage对象,并将其应用到 GUI 中的Canvas对象上。现在我们调用self.update(),这是一个从TK.Frame类继承来的函数;self.update()将允许 Tkinter 显示更新(在这种情况下,使用新图像)。最后,self.preview()函数也会调用self.msg(),这将更新self.filename值,以显示的图像文件名(PREVIEW.gif)为准。同样,这也使用self.update()来更新显示。

如果预览复选框未选中,那么self.shutter()函数将调用self.normal()。然而,这次它将捕获一个更大的 2,592 x 1,944(500 万像素)JPG 图像,文件名设置为从self.timestamp()获取的最新<timestamp>值。生成的图像也将被调整大小并转换为 PPM 图像,以便它可以作为TK.PhotoImage对象加载,并在应用程序窗口中显示。

还有更多...

摄像头应用程序使用类结构来组织代码并使其易于扩展。在接下来的章节中,我们将解释我们定义的方法和函数类型,以允许这样做。

树莓派还可以使用标准的 USB 摄像头或网络摄像头。或者,我们可以使用额外的 Video4Linux 驱动程序,使摄像头模块像标准网络摄像头一样工作。

类成员和静态函数

cameraGUI类定义了两种类型的函数。首先,我们定义了一些静态方法(run()getTKImage()timestamp())。这些方法与类相关联,而不是与特定实例相关联;这意味着我们可以使用它们而不需要引用特定的cameraGUI对象,而是直接引用类本身。这很有用,因为可以定义与类相关的实用函数,因为它们可能在程序的其它部分也有用,并且可能不需要访问cameraGUI对象中的数据/对象。这些函数可以通过cameraGUI.run("command")来调用。

接下来,我们定义类成员函数,就像我们在之前的类中使用的那样,包括对self的引用。这意味着它们只能由类的实例(cameraGUI类型的对象)访问,并且可以使用对象内部包含的数据(使用self引用)。

使用 USB 网络摄像头代替

树莓派摄像头模块并不是唯一可以添加摄像头到树莓派的方法;在大多数情况下,你也可以使用 USB 摄像头。当前的树莓派 Raspbian 镜像应该会在你插入时自动检测到最常见的摄像头设备;然而,支持可能会有所不同。

要确定你的摄像头是否已被检测到,请运行以下命令检查系统上是否已创建以下设备文件:

ls /dev/video*

如果检测成功,你将看到/dev/video0或类似的内容,这将是你用来访问摄像头的参考。

使用以下命令安装一个合适的图像捕捉程序,例如fswebcam

sudo apt-get install fswebcam

你可以使用以下命令进行测试:

fswebcam -d /dev/video0 -r 320x240 testing.jpg

或者,你也可以使用以下方式使用dd进行测试:

dd if=/dev/video0 of=testing.jpeg bs=11M count=1

注意

摄像头可能需要从树莓派的 USB 端口获取额外的电源;如果你遇到错误,你可能发现使用带电源的 USB 集线器有帮助。有关支持的设备列表和故障排除信息,请参阅树莓派维基页面elinux.org/RPi_USB_Webcams

在前面的示例中,按照以下方式修改cameraGUI类中的以下函数:

  1. 从文件开头移除camCapture()import picamera as picam

  2. normal()函数中,将cameraGUI.camCapture(name,SET.NORM_SIZE)替换为以下内容:

        cameraGUI.run(SET.CAM_PREVIEW+SET.CAM_OUTPUT+
                      SET.PREVIEW_FILE)
    
  3. preview()函数中,将cameraGUI.camCapture(SET.PREVIEW_FILE,SET.PV_SIZE)替换为以下内容:

        cameraGUI.run(SET.CAM_NORMAL+SET.CAM_OUTPUT+name)
    
  4. SET类中,定义以下变量:

    CAM_OUTPUT=" "
    CAM_PREVIEW="fswebcam -d /dev/video0 -r 320x240"
    CAM_NORMAL="fswebcam -d /dev/video0 -r 640x480"
    

通过对cameraGUI类进行之前的修改,连接的 USB 摄像头将负责捕捉图像。

树莓派摄像头的额外驱动程序

Video4Linux 驱动程序适用于树莓派摄像头模块。虽然这些额外的驱动程序还不是官方的,但它们很可能在它们成为官方时被包含在 Raspbian 镜像中。有关更多详细信息,请参阅www.linux-projects.org/uv4l/

驱动程序将允许你像使用 USB 摄像头一样使用摄像头模块,作为一个/dev/video*设备,尽管在本章的示例中你可能不需要这样做。

执行以下步骤来安装额外的驱动程序:

  1. 首先,下载apt密钥并将源添加到apt源列表中。你可以使用以下命令完成此操作:

    wget http://www.linux-projects.org/listing/uv4l_repo/lrkey.asc
    sudo apt-key add ./lrkey.asc
    sudo nano /etc/apt/souces.list 
    
    
  2. 将以下内容添加到文件中(单行):

    deb http://www.linux-projects.org/listing/uv4l_repo/raspbian/ wheezy main
    
    
  3. 使用以下命令安装驱动程序:

    sudo apt-get update
    sudo apt-get install uv4l uv4l-raspicam
    
    
  4. 要使用uv4l驱动程序,使用以下命令加载它(单行):

    uv4l --driver raspicam --auto-video_nr --width 640 –height480 --encoding jpeg
    

然后,你可以通过/dev/video0(取决于你是否安装了其他视频设备)访问树莓派。它可以与标准的摄像头程序一起使用。

参见

关于使用 Tkinter 库的更多示例,请参阅第三章使用 Python 进行自动化和生产力,使用 Python 进行自动化和生产力,以及第四章创建游戏和图形,创建游戏和图形

生成时间间隔视频

将相机连接到计算机为我们提供了一个在可控间隔拍照并自动将它们处理成视频以创建时间间隔序列的绝佳方式。pycamera Python 模块有一个特殊的 capture_continuous() 函数,可以创建一系列图像。对于时间间隔视频,我们将指定每张图像之间的时间和需要拍摄的总图像数。为了帮助用户,我们还将计算视频的总时长,以提供所需时间的指示。

我们将向之前的 GUI 界面添加控件以运行时间间隔,并自动从结果生成视频剪辑。GUI 现在看起来类似于以下截图:

生成时间间隔视频

时间间隔应用程序

准备工作

您需要设置与上一个示例相同,包括在同一目录中创建的 cameraGUI.py 文件和安装的 pycamera。我们还将使用 mencoder,这将允许我们将时间间隔图像组合成视频剪辑。

要安装 mencoder,使用 apt-get,如下所示:

sudo apt-get install mencoder

命令行选项的解释可以在 mencoder 的 man 页面中找到。

如何操作...

在与 cameraGUI.py 相同的目录下创建 timelapseGUI.py,按照以下步骤操作:

  1. 首先导入支持模块(包括 cameraGUI),如下所示:

    #!/usr/bin/python3
    #timelapseGUI.py
    import tkinter as TK
    from tkinter import messagebox
    import cameraGUI as camGUI
    import time
    
  2. cameraGUI.SET 类扩展为以下时间间隔和编码设置:

    class SET(camGUI.SET):
      TL_SIZE=(1920,1080)
      ENC_PROG="mencoder -nosound -ovc lavc -lavcopts"
      ENC_PROG+=" vcodec=mpeg4:aspect=16/9:vbitrate=8000000"
      ENC_PROG+=" -vf scale=%d:%d"%(TL_SIZE[0],TL_SIZE[1])
      ENC_PROG+=" -o %s -mf type=jpeg:fps=24 mf://@%s"
      LIST_FILE="image_list.txt"
    
  3. 通过以下方式扩展主 cameraGUI 类以执行时间间隔的附加功能:

    class cameraGUI(camGUI.cameraGUI):
      def camTimelapse(filename,size=SET.TL_SIZE,
                        timedelay=10,numImages=10):
        with camGUI.picam.PiCamera() as camera:
          camera.resolution = size
          for count, name in \
                enumerate(camera.capture_continuous(filename)):
            print("Timelapse: %s"%name)
            if count == numImages:
              break
            time.sleep(timedelay)
    
  4. 添加以下代码片段中所示的时间间隔 GUI 的额外控件:

      def __init__(self,parent):
        super(cameraGUI,self).__init__(parent)
        self.parent=parent
        TK.Frame.__init__(self,self.parent,background="white")
        self.numImageTL=TK.StringVar()
        self.peroidTL=TK.StringVar()
        self.totalTimeTL=TK.StringVar()
        self.genVideoTL=TK.IntVar()
        labelnumImgTK=TK.Label(self.parent,text="TL:#Images")
        labelperoidTK=TK.Label(self.parent,text="TL:Delay")
        labeltotalTimeTK=TK.Label(self.parent,
                                  text="TL:TotalTime")
        self.numImgSpn=TK.Spinbox(self.parent,
                           textvariable=self.numImageTL,
                           from_=1,to=99999,
                           width=5,state="readonly",
                           command=self.calcTLTotalTime)
        self.peroidSpn=TK.Spinbox(self.parent,
                           textvariable=self.peroidTL,
                           from_=1,to=99999,width=5,
                           command=self.calcTLTotalTime)
        self.totalTime=TK.Label(self.parent,
                           textvariable=self.totalTimeTL)
        self.TLBtn=TK.Button(self.parent,text="TL GO!",
                                 command=self.timelapse)
        genChk=TK.Checkbutton(self.parent,text="GenVideo",
                                 command=self.genVideoChk,
                                 variable=self.genVideoTL)
        labelnumImgTK.grid(row=3,column=0)
        self.numImgSpn.grid(row=4,column=0)
        labelperoidTK.grid(row=3,column=1)
        self.peroidSpn.grid(row=4,column=1)
        labeltotalTimeTK.grid(row=3,column=2)
        self.totalTime.grid(row=4,column=2)
        self.TLBtn.grid(row=3,column=3)
        genChk.grid(row=4,column=3)
        self.numImageTL.set(10)
        self.peroidTL.set(5)
        self.genVideoTL.set(1)
        self.calcTLTotalTime()
    
  5. 添加以下支持函数来计算设置和处理时间间隔:

      def btnState(self,state):
        self.TLBtn["state"] = state
        super(cameraGUI,self).btnState(state)
      def calcTLTotalTime(self):
        numImg=float(self.numImageTL.get())-1
        peroid=float(self.peroidTL.get())
        if numImg<0:
          numImg=1
        self.totalTimeTL.set(numImg*peroid)
      def timelapse(self):
        self.msg("Running Timelapse")
        self.btnState("disabled")
        self.update()
        self.tstamp="TL"+cameraGUI.timestamp()
        cameraGUI.camTimelapse(self.tstamp+'{counter:03d}.jpg',
                               SET.TL_SIZE,
                               float(self.peroidTL.get()),
                               int(self.numImageTL.get()))
        if self.genVideoTL.get() == 1:
          self.genTLVideo()
        self.btnState("active")
        TK.messagebox.showinfo("Timelapse Complete",
                               "Processing complete")
        self.update()
    
  6. 添加支持函数来处理和生成时间间隔视频,如下所示:

      def genTLVideo(self):
        self.msg("Generate video...")
        cameraGUI.run("ls "+self.tstamp+"*.jpg > "
                                    +SET.LIST_FILE)
        cameraGUI.run(SET.ENC_PROG%(self.tstamp+".avi",
                                          SET.LIST_FILE))
        self.msg(self.tstamp+".avi")
    #End
    
  7. 接下来,创建以下 cameraGUI2timelapse.py 脚本来使用 GUI:

    #!/usr/bin/python3
    #cameraGUI2timelapse.py
    import tkinter as TK
    import timelapseGUI as GUI
    
    root=TK.Tk()
    root.title("Camera GUI")
    cam=GUI.cameraGUI(root)
    TK.mainloop()
    #End
    

我们导入 timelapseGUI 而不是 cameraGUI;这将把 timelapseGUI 模块添加到 cameraGUI 脚本中。

使用以下命令运行示例:

python3 cameraGUI2timelapse.py

它是如何工作的...

timelapseGUI.py 脚本允许我们使用 cameraGUI.py 中定义的类并扩展它们。之前的 cameraGUI 类继承了 TK.Frame 类的所有内容,通过使用相同的技巧,我们也可以在我们的应用程序中继承 SETcameraGUI 类。

我们向 SET 类添加一些额外的设置,以提供 mencoder(用于编码视频)的设置。

我们将通过从 camGUI.cameraGUI 继承并定义类的新版本 __init__() 来扩展基本的 cameraGUI 类。使用 super(),我们可以包含原始 __init__() 函数的功能,然后定义我们想要添加到 GUI 中的额外控件。扩展后的 GUI 如下截图所示:

如何工作…

扩展基本相机 GUI 的时间流逝 GUI 布局

我们定义以下控制变量:

  • self.numImageTL: 这与numImgSpn微调框控制器的值相关联,用于指定我们想要在时间流逝中拍摄的照片数量(并为camTimelapse提供numimages值)。

  • self.peroidTL: 这与peroidSpn微调框控制器的值相关联;它决定了时间流逝图像之间应该有多少秒(并为camTimelapse提供timedelay值)。

  • self.totalTimeTL: 这与totalTime标签对象相关联。它通过图像数量和每张图像之间的timedelay时间来计算,以指示时间流逝将运行多长时间。

  • self.genVideoTL: 这控制着genChk复选框控件的状态。它用于确定在拍摄时间流逝图像之后是否已生成视频。

我们将两个微调框控制器链接到self.calcTLTotalTime(),以便当它们被更改时,totalTimeTL值也会更新(尽管如果它们被直接编辑则不会调用)。我们将genChk链接到self.genVideoChk(),将TLBtn链接到self.timelapse()

最后,我们使用grid()指定控件的位置,并为时间流逝设置一些默认值。

genChk复选框被勾选或清除时,会调用self.genVideoChk()函数。这允许我们通过生成一个弹出消息框来告知用户此复选框的效果,说明视频是否将在时间流逝结束时生成,或者只是创建图像。

当按下TL GO!按钮(TLBtn)时,会调用self.timelapse();这将禁用快门TL GO!按钮(因为我们还扩展了self.btnState()函数)。self.timelapse()函数还将设置self.tstamp值,以便可以使用相同的时间戳用于图像和生成的视频文件(如果生成)。

时间流逝是通过camTimelapse()函数运行的,如下面的代码所示:

def camTimelapse(filename,size=SET.TL_SIZE,
                    timedelay=10,numImages=10):
    with camGUI.picam.PiCamera() as camera:
      camera.resolution = size
      for count, name in \
            enumerate(camera.capture_continuous(filename)):
        print("Timelapse: %s"%name)
        if count == numImages:
          break
        time.sleep(timedelay)

我们创建一个新的PiCamera对象,设置图像分辨率,并启动一个for…in循环用于capture_continuous()。每次拍摄图像时,我们打印文件名,然后等待所需的timedelay值。最后,当拍摄了所需数量的图像时,我们退出循环并继续。

一旦完成,我们检查self.genVideoTL的值以确定是否要生成视频(由genTLVideo()处理)。

要生成视频,我们首先运行以下命令以创建一个包含图像的image_list.txt文件:

ls <self.tstamp>*.jpg > image_list.txt

然后,我们使用合适的设置运行mencoder(参见mencoder手册页面了解每个项目的作用)来创建一个从时间流逝图像列表中生成的 MPEG4 编码(8 Mbps)AVI 文件,每秒 24 帧(fps)。等效命令(由ENC_PROG定义)如下:

mencoder -nosound -ovc lavc \
 -lavcopts vcodec=mpeg4:aspect=16/9:vbitrate=8000000 \
 -vf scale=1920:1080 -o <self.tstamp>.avi \
 -mf type=jpeg:fps=24 mf://@image_list.txt

小贴士

在命令终端中,可以使用\字符将长命令拆分为多行。这允许你在另一行继续编写命令,只有在你完成一行且没有\字符时才会执行该命令。

还有更多...

本章使用类继承和函数重写等方法以多种不同的方式组织和重用我们的代码。当正确使用时,这些方法可以让我们以逻辑和灵活的方式设计复杂的系统。

此外,在生成自己的延时摄影序列时,你可以选择关闭相机模块上的 LED 灯或使用树莓派相机的低光版本:NoIR 相机。

类继承和函数重写

在前面的例子中,我们使用了一些巧妙的编码来重用我们的原始cameraGUI类并创建一个扩展其功能的插件文件。

类名不必与cameraGUI相同(我们只是在这个例子中使用它,这样我们就可以通过更改导入的文件来替换额外的 GUI 组件)。实际上,我们可以定义一个包含几个通用函数的基本类,然后通过继承将其扩展到多个子类中;在这里,每个子类定义特定的行为、函数和数据。子类的扩展和结构在以下图中显示:

类继承和函数重写

此图显示了类如何扩展和结构化

为了说明这一点,我们将举一个非代码示例,其中我们编写了一个制作蛋糕的通用食谱。然后你可以通过继承所有basicCake元素来扩展basicCake食谱,并添加一些额外的步骤(相当于代码函数),例如在顶部添加糖霜/奶油霜以制作icedCake(basicCake)类。我们通过向现有类添加额外项(我们只是选择不更改名称)来这样做我们的SET类。

我们还可以向现有步骤添加一些额外的元素(在addIngredients步骤中添加一些葡萄干并创建currantCake(basicCake))。我们通过在代码中使用super()函数,通过向__init__()函数添加额外部分来实现这一点。例如,我们会使用super(basicCake.self).addIngredients()来包含在basicCake类中定义的addIngredients()函数中的所有步骤,然后添加一个额外的步骤来包含葡萄干。优点是,如果我们随后更改基本蛋糕的成分,它也会影响到所有其他类。

你甚至可以通过用新函数替换它们来覆盖一些原始函数;例如,你可以用制作chocolateCake(basicCake)的食谱替换原始的basicCake食谱,同时仍然使用相同的烹饪说明,等等。我们可以通过定义具有相同名称的替换函数来实现这一点,而不使用super()

以这种方式使用结构化设计可以变得非常强大,因为我们可以轻松地创建许多相同类型对象的变体,但所有公共元素都定义在同一个地方。这在测试、开发和维护大型复杂系统时具有许多优点。关键在于在开始之前对整个项目有一个全面的了解,并尝试识别公共元素。你会发现,你拥有的结构越好,开发和改进它就越容易。

关于这方面的更多信息,值得阅读关于面向对象设计方法和如何使用统一建模语言UML)来帮助您描述和理解您的系统的内容。

禁用摄像头 LED

如果你想在夜间或靠近窗户时创建时间流逝视频,你可能注意到红色摄像头 LED(每次拍摄都会点亮)会添加不需要的光线或反射。幸运的是,可以通过 GPIO 控制摄像头 LED。LED 是通过GPIO.BCM引脚 5 控制的;不幸的是,没有与之等效的GPIO.BOARD引脚编号。

要将其添加到 Python 脚本中,请使用以下代码:

import RPi.GPIO as GPIO

GPIO.cleanup()
GPIO.setmode(GPIO.BCM)
CAMERALED=5 #GPIO using BCM numbering
GPIO.setup(CAMERALED, GPIO.OUT)
GPIO.output(CAMERALED,False)

或者,你也可以将 LED 用于其他用途,例如,作为延迟计时器的一部分的指示器,该计时器提供倒计时和警告,表明相机即将拍照。

Pi NoIR – 拍摄夜景

还有一种名为Pi NoIR的 Raspberry Pi 摄像头模块的变体。这种摄像头的版本与原始版本相同,只是内部的红外滤光片已被移除。除此之外,这允许你在夜间使用红外灯光照亮区域(就像大多数夜间安全摄像头一样),并看到在黑暗中发生的一切!

《The MagPi》 第 18 期 (www.raspberrypi.org/magpi/) 发布了一篇出色的特色文章,解释了 Pi NoIR 摄像头模块的其他用途。

创建定格动画

定格动画(或逐帧动画)是拍摄一系列静态图像的过程,同时在每个帧中进行非常小的移动(通常是易于移动的对象,如娃娃或塑料模型)。当这些帧组合成视频时,小的移动组合起来产生动画。

创建定格动画

可以将多张图片组合成动画

传统上,这类动画是通过在电影摄像机(如 Cine Super 8 电影摄像机)上拍摄数百甚至数千张单独的照片来制作的,然后将胶片寄出进行冲洗,并在几周后播放结果。尽管 Aardman Animations 的 Nick Park 创作了一些鼓舞人心的作品,包括《华莱士和吉姆》系列(这是全长的定格动画电影),但对于大多数人来说,这仍然是一项有点难以触及的爱好。

在现代数字时代,我们可以快速轻松地拍摄多张照片,并且几乎可以立即查看结果。现在任何人都可以尝试制作自己的动画杰作,成本或努力都非常低。

我们将扩展我们的原始Camera GUI类,添加一些额外功能,这将使我们能够创建自己的停止帧动画。它将允许我们在生成最终视频之前,先以序列的形式拍摄图像并尝试它们。

准备中

此示例的软件设置将与之前的延时摄影示例相同。同样,我们需要安装mencoder,并且需要在同一目录中包含cameraGUI.py文件。

你还需要一些可以动画化的东西,理想情况下是你可以将其置于不同姿势的东西,就像以下图像中显示的两个娃娃一样:

准备中

我们停止帧动画的两个潜在明星

如何做到这一点...

通过以下步骤在cameraGUI.py同一目录中创建animateGUI.py

  1. 首先导入支持模块(包括cameraGUI),如下所示:

    #!/usr/bin/python3
    #animateGUI.py
    import tkinter as TK
    from tkinter import messagebox
    import time
    import os
    import cameraGUI as camGUI
    
  2. 如下扩展cameraGUI.SET类,以设置图像大小和编码:

    class SET(camGUI.SET):
      TL_SIZE=(1920,1080)
      ENC_PROG="mencoder -nosound -ovc lavc -lavcopts"
      ENC_PROG+=" vcodec=mpeg4:aspect=16/9:vbitrate=8000000"
      ENC_PROG+=" -vf scale=%d:%d"%(TL_SIZE[0],TL_SIZE[1])
      ENC_PROG+=" -o %s -mf type=jpeg:fps=24 mf://@%s"
      LIST_FILE="image_list.txt"
    
  3. 如下扩展主cameraGUI类,以添加动画所需的函数:

    class cameraGUI(camGUI.cameraGUI):
      def diff(a, b):
        b = set(b)
        return [aa for aa in a if aa not in b]
      def __init__(self,parent):
        super(cameraGUI,self).__init__(parent)
        self.parent=parent
        TK.Frame.__init__(self,self.parent,
                          background="white")
        self.theList = TK.Variable()
        self.imageListbox=TK.Listbox(self.parent,
                       listvariable=self.theList,
                           selectmode=TK.EXTENDED)
        self.imageListbox.grid(row=0, column=4,columnspan=2,
                                  sticky=TK.N+TK.S+TK.E+TK.W)
        yscroll = TK.Scrollbar(command=self.imageListbox.yview,
                                            orient=TK.VERTICAL)
        yscroll.grid(row=0, column=6, sticky=TK.N+TK.S)
        self.imageListbox.configure(yscrollcommand=yscroll.set)
        self.trimBtn=TK.Button(self.parent,text="Trim",
                                      command=self.trim)
        self.trimBtn.grid(row=1,column=4)
        self.speed = TK.IntVar()
        self.speed.set(20)
        self.speedScale=TK.Scale(self.parent,from_=1,to=30,
                                      orient=TK.HORIZONTAL,
                                       variable=self.speed,
                                       label="Speed (fps)")
        self.speedScale.grid(row=2,column=4)
        self.genBtn=TK.Button(self.parent,text="Generate",
                                     command=self.generate)
        self.genBtn.grid(row=2,column=5)
        self.btnAniTxt=TK.StringVar()
        self.btnAniTxt.set("Animate")
        self.animateBtn=TK.Button(self.parent,
                  textvariable=self.btnAniTxt,
                          command=self.animate)
        self.animateBtn.grid(row=1,column=5)
        self.animating=False
        self.updateList()
    
  4. 使用以下代码片段向列表中添加函数以列出已拍摄的照片,并从列表中删除它们:

      def shutter(self):
        super(cameraGUI,self).shutter()
        self.updateList()
    
      def updateList(self):
        filelist=[]
        for files in os.listdir("."):
          if files.endswith(".jpg"):
            filelist.append(files)
        filelist.sort()
        self.theList.set(tuple(filelist))
        self.canvas.update()
    
      def generate(self):
        self.msg("Generate video...")
        cameraGUI.run("ls *.jpg > "+SET.LIST_FILE)
        filename=cameraGUI.timestamp()+".avi"
        cameraGUI.run(SET.ENC_PROG%(filename,SET.LIST_FILE))
        self.msg(filename)
        TK.messagebox.showinfo("Encode Complete",
                               "Video: "+filename)
      def trim(self):
        print("Trim List")
        selected = map(int,self.imageListbox.curselection())
        trim=cameraGUI.diff(range(self.imageListbox.size()),
                                                    selected)
        for item in trim:
          filename=self.theList.get()[item]
          self.msg("Rename file %s"%filename)
          #We could delete os.remove() but os.rename() allows
          #us to change our minds (files are just renamed).
          os.rename(filename,
                    filename.replace(".jpg",".jpg.bak"))
          self.imageListbox.selection_clear(0,
                          last=self.imageListbox.size())
        self.updateList()
    
  5. 包含以下使用图像列表执行测试动画的函数:

      def animate(self):
        print("Animate Toggle")
        if (self.animating==True):
          self.btnAniTxt.set("Animate")
          self.animating=False
        else:
          self.btnAniTxt.set("STOP")
          self.animating=True
          self.doAnimate()
    
      def doAnimate(self):
        imageList=[]
        selected = self.imageListbox.curselection()
        if len(selected)==0:
          selected=range(self.imageListbox.size())
        print(selected)
        if len(selected)==0:
          TK.messagebox.showinfo("Error",
                          "There are no images to display!")
          self.animate()
        elif len(selected)==1:
          filename=self.theList.get()[int(selected[0])]
          self.updateDisp(filename,SET.PV_SIZE)
          self.animate()
        else:
          for idx,item in enumerate(selected):
            self.msg("Generate Image: %d/%d"%(idx+1,
                                            len(selected)))
            filename=self.theList.get()[int(item)]
            aImage=cameraGUI.getTKImage(filename,SET.PV_SIZE)
            imageList.append(aImage)
          print("Apply Images")
          canvasList=[]
          for idx,aImage in enumerate(imageList):
            self.msg("Apply Image: %d/%d"%(idx+1,
                                           len(imageList)))
            canvasList.append(self.canvas.create_image(0, 0,
                                      anchor=TK.NW,
                                      image=imageList[idx],
                                      state=TK.HIDDEN))
          self.cycleImages(canvasList)
    
      def cycleImages(self,canvasList):
        while (self.animating==True):
          print("Cycle Images")
          for idx,aImage in enumerate(canvasList):
            self.msg("Cycle Image: %d/%d"%(idx+1,
                                      len(canvasList)))
            self.canvas.itemconfigure(canvasList[idx],
                                      state=TK.NORMAL)
            if idx>=1:
              self.canvas.itemconfigure(canvasList[idx-1],
                                          state=TK.HIDDEN)
            elif len(canvasList)>1:
              self.canvas.itemconfigure(
                            canvasList[len(canvasList)-1],
                                          state=TK.HIDDEN)
            self.canvas.update()
            time.sleep(1/self.speed.get())
    #End
    
  6. 接下来,创建以下cameraGUI3animate.py文件以使用 GUI:

    #!/usr/bin/python3
    #cameraGUI3animate.py
    import tkinter as TK
    import animateGUI as GUI
    
    #Define Tkinter App
    root=TK.Tk()
    root.title("Camera GUI")
    cam=GUI.cameraGUI(root)
    TK.mainloop()
    #End
    
  7. 使用以下命令运行示例:

    python3 cameraGUI3animate.py
    
    

它是如何工作的…

再次,我们基于原始cameraGUI类创建了一个新类。这次,我们定义了以下具有六个额外控件的 GUI:

它是如何工作的…

动画 GUI 布局

我们创建了一个列表框控件(imageListbox),它将包含当前目录(self.theList)中的.jpg图像列表。此控件有一个与之链接的垂直滚动条(yscroll),以便轻松滚动列表,并且使用selectmode=TK.EXTENDED允许使用ShiftCtrl(用于块和组选择)进行多选。

接下来,我们添加一个Trim按钮(timeBtn),它将调用self.trim()。这将删除列表中未选中的任何项目。我们使用curselection()imageListbox控件获取当前选中的项目列表。curselection()函数通常返回一个索引列表,这些索引是数值字符串,因此我们使用map(int,...)将结果转换为整数列表。

我们使用此列表通过我们的实用程序diff(a,b)函数获取所有未选中的索引。该函数将完整的索引列表与选中的索引进行比较,并返回任何未选中的索引。

self.trim()函数使用os.rename()将所有非选中图片的文件扩展名从.jpg更改为.jpg.bak。我们可以使用os.remove()删除它们,但我们真正想要的是将它们重命名以防止它们出现在列表和最终视频中。列表通过self.updateList()重新填充,该函数更新self.theList为所有可用的.jpg文件列表。

我们添加了一个与self.speed链接的刻度控制(speedScale),用于控制动画测试的播放速度。同样,我们添加了一个Generate按钮(genBtn),它调用self.generate()

最后,我们添加了Animate按钮(animateBtn)。按钮的文本链接到self.btnAniTxt(这使得在程序中更改它变得容易),当按下时,按钮调用self.animate()

注意

我们通过添加对self.updateList()的调用覆盖了原始cameraGUI脚本中的原始shutter()函数。这确保了在拍摄完一张图片后,图像列表会自动更新为新图片。再次使用super()确保也执行了原始功能。

animate() 函数(通过点击Animate按钮调用)允许我们测试一系列图片,看看它们是否能够制作出好的动画。当按钮被点击时,我们将按钮的文本改为STOP,将self.animating标志设置为True(表示动画模式正在运行),并调用doAnimate()

doAnimate() 函数首先获取imageListbox控件中当前选中的图片列表,生成一系列TK.PhotoImage对象,并将它们附加到 GUI 中的self.canvas对象。然而,如果只选中了一张图片,我们将直接使用self.updateDisp()显示它。或者,如果没有选中任何图片,它将尝试使用所有图片(除非列表为空,在这种情况下,它将通知用户没有图片可以动画化)。当我们有多个TK.PhotoImage对象链接到画布时,我们可以使用cycleImages()函数遍历它们。

TK.PhotoImage对象都创建时其状态设置为TK.HIDDEN,这意味着它们在画布上不可见。为了产生动画效果,cycleImages()函数将每个图像设置为TK.NORMAL,然后再次设置为TK.HIDDEN,使得每个帧在显示下一帧之前显示 1 除以self.speed(由 Scale 控件设置的 fps 值)秒。

cycleImages()函数将在self.animatingTrue时执行动画,也就是说,直到再次点击animateBtn对象。

一旦用户对他们的动画满意,他们可以使用Generate按钮(genBtn)生成视频。generate()函数将调用mencoder生成imageListbox控件中所有图片的最终视频。

如果你真的想从事动画制作,你应该考虑添加一些额外的功能来帮助你,例如能够复制和重新排列帧的能力。你可能还想为相机添加一些手动调整,以避免由相机自动设置引起的白平衡和光照波动。

还有更多...

由于其小型尺寸和远程控制能力,相机模块非常适合近距离摄影。通过使用小镜头或添加硬件控制,你可以制作一个专用的动画机。

提高焦点

树莓派相机的镜头主要是为中等到长距离摄影设计的,因此它难以聚焦于 25 厘米(10 英寸)以内的物体。然而,使用一些基本镜头,我们可以调整有效焦距,使其更适合微距摄影。你可以使用适用于手机的附加镜头或信用卡式放大镜镜头来调整焦点,如下面的图片所示:

提高焦点

一个附加的宏观镜头(右)和一个信用卡放大镜(左)可以提高近距离物品的焦点

创建硬件快门

当然,虽然有一个可用的显示屏来查看拍摄的照片是有用的,但通常能够简单地按下一个物理按钮来拍摄照片也很方便。幸运的是,这只是一个将按钮(和电阻)连接到 GPIO 引脚的问题,就像我们之前做的那样(参见第六章中的响应按钮配方,使用 Python 驱动硬件),并创建适当的 GPIO 控制代码来调用我们的cameraGUI.camCapture()函数。代码如下:

#!/usr/bin/python3
#shutterCam.py
import RPi.GPIO as GPIO
import cameraGUI as camGUI
import time

GPIO.setmode(GPIO.BOARD)
CAMERA_BTN=12 #GPIO Pin 12
GPIO.setup(CAMERA_BTN,GPIO.IN,pull_up_down=GPIO.PUD_UP)
count=1
try:
  while True:
    btn_val = GPIO.input(CAMERA_BTN)
    #Take photo when Pin 12 at 0V
    if btn_val==False:
      camGUI.cameraGUI.camCapture("Snap%03d.jpg"%count,
                                   camGUI.SET.NORM_SIZE)
      count+=1
    time.sleep(0.1)
finally:
  GPIO.cleanup()
#End

当按钮被按下时,前面的代码将拍摄照片。以下图示显示了实现这一功能的连接和电路图:

创建硬件快门

按钮(以及 1K 欧姆电阻)应该连接在 12 号引脚和 6 号引脚(GND)之间

你甚至不需要停止在这里,因为如果你想要的话,可以为相机上的任何控制或设置添加按钮和开关。你甚至可以使用其他硬件(如红外传感器等)来触发相机拍摄照片或视频。

制作二维码读取器

你可能已经在各种地方见过二维码,也许甚至使用过几个来从海报或广告中获取链接。然而,如果你自己制作,它们可以变得更有用。以下示例讨论了我们可以如何使用树莓派来读取二维码和隐藏的内容(或者甚至链接到音频文件或视频)。

这可以用来创建你自己的个性化树莓派二维码音乐盒,也许作为帮助儿童解决数学问题的辅助工具,或者甚至在他们一页一页地跟随时播放你阅读孩子最喜欢的书籍的音频文件。以下截图是一个二维码的示例:

制作 QR 码阅读器

您可以使用 QR 码制作神奇的自读书籍

准备工作

此示例需要与之前的示例类似的设置(除了这次我们不需要 mencoder)。我们需要安装 ZBar,这是一个跨平台的 QR 码和条形码阅读器,以及 flite(一个文本到语音工具,我们在第六章使用 Python 驱动硬件中使用过,使用 Python 驱动硬件)。

要安装 ZBar 和 flite,请使用以下命令中的 apt-get

sudo apt-get install zbar-tools flite

小贴士

目前有适用于 Zbar 的 Python 2.7 库,但它们目前与 Python 3 不兼容。Zbar 还包括一个实时扫描器(zbarcam),它使用视频输入自动检测条形码和 QR 码。不幸的是,这与 Raspberry Pi 相机也不兼容。

对于我们来说这不是大问题,因为我们可以直接使用 zbarimg 程序从 picamera 拍摄的图像中检测 QR 码。

安装完软件后,您将需要一些 QR 码来扫描(请参阅 生成 QR 码 部分的 更多内容…),以及一些合适的 MP3 文件(这些可以是您阅读书籍页面的录音或音乐曲目)。

如何操作…

在与 cameraGUI.py 相同的目录中创建以下 qrcodeGUI.py 脚本:

#!/usr/bin/python3
#qrcodeGUI.py
import tkinter as TK
from tkinter import messagebox
import subprocess
import cameraGUI as camGUI

class SET(camGUI.SET):
  QR_SIZE=(640,480)
  READ_QR="zbarimg "

class cameraGUI(camGUI.cameraGUI):
  def run_p(cmd):
    print("RunP:"+cmd)
    proc=subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE)
    result=""
    for line in proc.stdout:
      result=str(line,"utf-8")
    return result
  def __init__(self,parent):
    super(cameraGUI,self).__init__(parent)
    self.parent=parent
    TK.Frame.__init__(self,self.parent,background="white")
    self.qrScan=TK.IntVar()
    self.qrRead=TK.IntVar()
    self.qrStream=TK.IntVar()
    self.resultQR=TK.StringVar()
    self.btnQrTxt=TK.StringVar()
    self.btnQrTxt.set("QR GO!")
    self.QRBtn=TK.Button(self.parent,textvariable=self.btnQrTxt,
                                              command=self.qrGet)
    readChk=TK.Checkbutton(self.parent,text="Read",
                               variable=self.qrRead)
    streamChk=TK.Checkbutton(self.parent,text="Stream",
                                 variable=self.qrStream)
    labelQR=TK.Label(self.parent,textvariable=self.resultQR)
    readChk.grid(row=3,column=0)
    streamChk.grid(row=3,column=1)
    self.QRBtn.grid(row=3,column=3)
    labelQR.grid(row=4,columnspan=4)
    self.scan=False
  def qrGet(self):
    if (self.scan==True):
      self.btnQrTxt.set("QR GO!")
      self.btnState("active")
      self.scan=False
    else:
      self.msg("Get QR Code")
      self.btnQrTxt.set("STOP")
      self.btnState("disabled")
      self.scan=True
      self.qrScanner()
  def qrScanner(self):
    found=False
    while self.scan==True:
      self.resultQR.set("Taking image...")
      self.update()
      cameraGUI.camCapture(SET.PREVIEW_FILE,SET.QR_SIZE)
      self.resultQR.set("Scanning for QRCode...")
      self.update()
      #check for QR code in image
      qrcode=cameraGUI.run_p(SET.READ_QR+SET.PREVIEW_FILE)
      if len(qrcode)>0:
        self.msg("Got barcode: %s"%qrcode)
        qrcode=qrcode.strip("QR-Code:").strip('\n')
        self.resultQR.set(qrcode)
        self.scan=False
        found=True
      else:
        self.resultQR.set("No QRCode Found")
    if found:
      self.qrAction(qrcode)
      self.btnState("active")
      self.btnQrTxt.set("QR GO!")
    self.update()
  def qrAction(self,qrcode):
    if self.qrRead.get() == 1:
      self.msg("Read:"+qrcode)
      cameraGUI.run("sudo flite -t '"+qrcode+"'")
    if self.qrStream.get() == 1:
      self.msg("Stream:"+qrcode)
      cameraGUI.run("omxplayer '"+qrcode+"'")
    if self.qrRead.get() == 0 and self.qrStream.get() == 0:
      TK.messagebox.showinfo("QR Code",self.resultQR.get())
#End

接下来,创建 cameraGUItimelapse.pycameraGUIanimate.py 的副本,并将其命名为 cameraGUIqrcode.py。再次确保您使用以下代码导入新的 GUI 文件:

import qrcodeGUI as GUI

带有 QR 码的 GUI 将看起来如下截图所示:

如何操作…

QR 码图形用户界面

如何工作…

新的 qrcodeGUI.py 文件添加了 读取播放 复选框控件以及一个按钮控件来开始扫描 QR 码。当点击 QR GO! 时,self.qrGet() 将启动一个循环,通过 zbarimg 拍摄图像并检查结果。如果 zbarimg 在图像中找到 QR 码,则扫描将停止,并将结果显示出来。否则,它将继续扫描,直到点击 停止 按钮。在扫描过程中,QRBtn 的文本将更改为 停止

为了捕获 zbarimg 的输出,我们需要稍微改变运行命令的方式。为此,我们定义 run_p(),它使用以下代码:

proc=subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE)

这将 stdout 作为 proc 对象的一部分返回,其中包含 zbarimg 程序的输出。然后我们从图像中提取读取到的结果 QR 码(如果找到了)。

当选择 读取 时,使用 flite 读取 QR 码,如果选择 播放,则使用 omxplayer 播放文件(假设 QR 码包含合适的链接)。

为了获得最佳结果,建议您先拍摄一个预览照片,以确保在运行 QR 扫描器之前正确对齐目标 QR 码。

如何工作…

示例 QR 码页面标记(page001.mp3 和 page002.mp3)

之前的二维码包含 page001.mp3page002.mp3。这些二维码允许我们在与脚本相同的目录下播放同名文件。您可以通过遵循本食谱中 还有更多… 部分的说明来生成您自己的二维码。

您甚至可以使用书的 ISBN 条形码根据读取的条形码选择不同的 MP3 目录;条形码允许您为任何喜欢的书籍重用同一组编号的二维码。

还有更多…

要使用前面的示例,您可以使用下一节的示例生成一系列二维码以供使用。

生成二维码

您可以使用 PyQRCode(更多信息请参阅 pypi.python.org/pypi/PyQRCode)来创建二维码。

您可以使用以下命令通过 PIP Python 管理器安装 PyQRCode(请参阅第三章中 显示应用程序中的照片信息 食谱的 准备就绪 部分,使用 Python 进行自动化和生产率):

sudo pip-3.2 install pyqrcode

要将二维码编码为 PNG 格式,PyQrCode 使用 PyPNG (github.com/drj11/pypng),可以使用以下命令安装:

sudo pip-3.2 install pypng

使用以下 generateQRCodes.py 脚本生成二维码以链接到文件,例如您已记录的 page001.mp3page002.mp3 文件:

#!/usr/bin/python3
#generateQRCodes.py
import pyqrcode
valid=False
print("QR-Code generator")
while(valid==False):
    inputpages=input("How many pages?")
    try:
      PAGES=int(inputpages)
      valid=True
    except ValueError:
      print("Enter valid number.")
      pass
print("Creating QR-Codes for "+str(PAGES)+" pages:")
for i in range(PAGES):
  file="page%03d"%(i+1)
  qr_code = pyqrcode.create(file+".mp3")
  qr_code.png(file+".png")
  print("Generated QR-Code for "+file)
print("Completed")
#End

使用以下命令运行此代码:

python3 generateQRCodes.py

之前的代码将创建一组二维码,可用于激活所需的 MP3 文件并大声读出页面(或播放您链接到它的文件)。

参见

开源计算机视觉OpenCV)项目是一个非常强大的图像和视频处理引擎;更多详细信息请参阅 opencv.org

通过将摄像头与 OpenCV 结合,Raspberry Pi 能够识别并与其环境交互。

这的一个优秀例子是 Samuel Matos 的 RS4 OpenCV 自平衡机器人(roboticssamy.blogspot.pt),它可以寻找并响应各种自定义标志;摄像头模块可用于导航和控制机器人。

探索和实验 OpenCV

OpenCV 库是一个旨在为多个平台提供实时计算机视觉处理的广泛库。本质上,如果您想进行任何严肃的图像处理、物体识别或分析,那么 OpenCV 是您开始的地方。

幸运的是,OpenCV(版本 3)的最新版本已添加了对通过 Python 3 进行接口的支持。尽管进行实时视频处理通常需要具有强大 CPU 的计算机,但它可以在相对有限的设备上运行,例如原始的 Raspberry Pi(版本 1)。强烈推荐使用更强大的 Raspberry Pi 2 来运行以下食谱。

图像和视频处理背后的概念和底层方法可能相当复杂。本食谱将演示如何使用 OpenCV,更重要的是提供一个简单的方法来可视化可能用于处理图像的各种阶段。

探索和实验 OpenCV

在进行相机测试时,请确保您有合适的测试对象可用

准备工作

OpenCV 库是用 C++编写的,在我们能够在 Raspberry Pi 上使用它之前,需要对其进行编译。为此,我们需要安装所有必需的包,然后从 OpenCV Git 仓库下载一个发布版本。OpenCV 在编译时可能需要大约 2.5GB 的空间;然而,从 NOOBS 安装的标准 Raspbian 版本大约需要 5.5GB。这意味着在 8GB SD 卡上可能空间不足。可能可以将 OpenCV 压缩到更小的 SD 卡上(通过安装自定义的 Raspbian 镜像或利用 USB 闪存设备);然而,为了避免复杂问题,建议您至少使用 16GB SD 卡来编译和安装 OpenCV。

此外,虽然这本书中的大多数菜谱都可以通过网络连接使用 SSH 和 X11 转发来运行,但如果您连接到本地屏幕(通过 HDMI)并直接使用本地输入设备,OpenCV 显示窗口似乎功能更为有效。

安装 OpenCV 是一个相当漫长的过程,但我认为结果是值得努力的:

  1. 确保 Raspberry Pi 尽可能更新,使用以下命令:

    sudo apt-get update
    sudo apt-get upgrade
    sudo rpi-update
    
  2. 并执行重启以应用更改:

    sudo reboot
    
    
  3. 在我们编译 OpenCV 之前,我们需要安装一些依赖项以支持构建过程:

    sudo apt-get install build-essential cmake pkg-config
    sudo apt-get install python2.7-dev python3-dev
    
  4. 我们还需要安装 OpenCV 使用的许多支持库和包(我们可能不会使用所有这些,但它们是构建过程的一部分)。这些也将为 OpenCV 内提供的广泛图像和视频格式提供支持:

    sudo apt-get install libjpeg-dev libtiff5-dev libjasper-dev libpng12-dev
    sudo apt-get install libavcodec-dev libavformat-dev libswscale-dev libv4l-dev
    sudo apt-get install libxvidcore-dev libx264-dev
    sudo apt-get install libgtk2.0-dev
    
    
  5. 我们还可以安装 NumPy,这在 OpenCV 中操作图像数组时非常有用,自动调优线性代数软件ATLAS),以及 GFortran 以提供额外的数学功能:

    sudo apt-get install python3-numpy
    sudo apt-get install libatlas-base-dev gfortran
    
    
  6. 现在我们有了支持包,我们可以直接从 GitHub 下载 OpenCV 和 OpenCV 贡献(额外模块)。我们还将创建一个用于下一步的构建位置:

    cd ~
    wget -O opencv.zip https://github.com/Itseez/opencv/archive/3.0.0.zip
    unzip opencv.zip
    wget -O opencv_contrib.zip https://github.com/Itseez/opencv_contrib/archive/3.0.0.zip
    unzip opencv_contrib.zip
    cd opencv-3.0.0
    mkdir build
    cd build
    
    

    注意

    注意:您可以使用以下链接下载最新版本,并选择特定的发布标签;然而,您可能需要额外的依赖项或模块才能成功编译该软件包。请确保您选择与 OpenCV 和贡献模块相同的发布版本。

    github.com/Itseez/opencv/

    github.com/Itseez/opencv_contrib/

  7. make文件可以使用以下命令创建。这大约需要 10 分钟才能完成(见以下截图):

    cmake -D CMAKE_BUILD_TYPE=RELEASE \
     -D CMAKE_INSTALL_PREFIX=/usr/local \
     -D INSTALL_C_EXAMPLES=ON \
     -D INSTALL_PYTHON_EXAMPLES=ON \
     -D OPENCV_EXTRA_MODULES_PATH=~/opencv_contrib-3.0.0/modules \
     -D BUILD_EXAMPLES=ON ..
    
    

    准备就绪

    确保 Python 2.7 和 Python 3 部分与这个截图匹配

  8. 我们现在可以编译 OpenCV 了;请注意,这个过程可能需要相当长的时间才能完成。幸运的是,如果你需要停止这个过程或者出现问题时,你可以继续执行 make 命令,检查并跳过任何已经完成的组件。要从头开始重新启动 make,请使用 make clean 清除构建并重新开始。

注意

注意:通过使用 Raspberry Pi 2 的所有四个处理核心,构建时间可以缩短到一小时以上。使用 –j4 开关来启用四个核心,这将允许在构建过程中运行多个作业。

构建过程可能需要近三个小时才能完成。如果你已经加载了 Raspbian 桌面或者你在后台运行其他任务,建议你注销到命令行并停止任何额外的作业,否则这个过程可能需要更长的时间才能完成。

对于 Raspberry Pi 1,使用以下命令使用单线程的 make 作业:

make

对于 Raspberry Pi 2,使用以下命令启用最多四个同时作业:

make -j4

准备就绪

完成的构建应该看起来像这样

OpenCV 编译成功后,可以安装:

sudo make install

现在所有这些都已完成,我们可以快速测试 OpenCV 是否现在可以通过 Python 3 使用。运行以下命令以打开 Python 3 终端:

python3

在 Python 3 终端中输入以下内容:

import cv2
cv2.__version__

这将显示你刚刚安装的 OpenCV 版本!

注意

注意:OpenCV 库会定期更新,这可能会在构建过程中引起问题。因此,如果你遇到问题,Py Image Search 网站 (www.pyimagesearch.com) 是一个极好的资源,它包含了在 Raspberry Pi 上安装 OpenCV 的最新指南和视频教程。

如何操作...

对于我们的第一个 OpenCV 测试,我们将使用它来显示捕获的图像。创建以下 openimage.py 文件:

#!/usr/bin/python3
#openimage.py
import cv2

# Load a color image in grayscale
img = cv2.imread('testimage.jpg',0)
cv2.imshow('Frame',img)
cv2.waitKey(0)
cv2.destroyAllWindows()

在运行脚本之前,请确保使用以下命令捕获要显示的图像:

raspistill -o testimage.jpg -w 640 -h 480

使用以下命令运行脚本:

python3 openimage.py

工作原理…

简单的测试程序首先通过导入 OpenCV (cv2) 和使用 cv2.imread() 加载图像。然后我们使用 cv2.imshow() 在一个带有标题 'Frame' 的图像框中显示我们的图像 (img)。然后我们等待按下任意键 (cv2.waitKey(0)) 才关闭显示窗口。

工作原理…

图像以灰度图像的形式显示在标准框架中

使用 OpenCV 进行颜色检测

我们将通过在实时图像数据上执行一些基本操作来开始使用 OpenCV 进行实验。在这个菜谱中,我们将执行一些基本的图像处理,以便检测不同颜色的物体并跟踪它们在屏幕上的位置。

准备就绪

除了之前的配方设置外,您还需要一个合适的彩色物体来跟踪。例如,一个小彩球、一个合适的彩色杯子,或者一个贴有彩色纸片的铅笔是理想的。示例应该允许您检测蓝色、绿色、红色、洋红色(粉红色)或黄色物体的位置(由颜色点指示)。

准备就绪

我们可以使用 OpenCV 在图像中检测彩色物体

如何操作…

创建以下 opencv_display.py 脚本:

#!/usr/bin/python3
#opencv_display.py
from picamera.array import PiRGBArray
from picamera import PiCamera
import time
import cv2

import opencv_color_detect as PROCESS  

def show_images(images,text,MODE):          
  # show the frame
  cv2.putText(images[MODE], "%s:%s" %(MODE,text[MODE]), (10,20),
              cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,255,255), 2)
  cv2.imshow("Frame", images[MODE])

def begin_capture():
  # initialize the camera and grab a reference to the raw camera capture
  camera = PiCamera()
  camera.resolution = (640, 480)
  camera.framerate = 50
  camera.hflip = True

  rawCapture = PiRGBArray(camera, size=(640, 480))

  # allow the camera to warmup
  time.sleep(0.1)
  print("Starting camera...")
  MODE=0

  # capture frames from the camera
  for frame in camera.capture_continuous(rawCapture, format="bgr", use_video_port=True):
    # capture any key presses
    key = cv2.waitKey(1) & 0xFF

	# grab the raw NumPy array representing the image
    images, text = PROCESS.process_image(frame.array,key)

    #Change display mode or quit
    if key == ord("m"):
      MODE=MODE%len(images)
    elif key == ord("q"):
      print("Quit")
      break

  #Display the output images
    show_images(images,text,MODE)

  # clear the stream in preparation for the next frame
    rawCapture.truncate(0)

begin_capture()
#End

在与 opencv_display.py 相同的目录中创建以下 opencv_color_detect.py 脚本:

#!/usr/bin/python3
#opencv_color_detect.py
import cv2
import numpy as np

BLUR=(5,5)
threshold=0
#Set the BGR color thresholds
THRESH_TXT=["Blue","Green","Red","Magenta","Yellow"]
THRESH_LOW=[[80,40,0],[40,80,0],[40,00,80],[80,0,80],[0,80,80]]
THRESH_HI=[[220,100,80],[100,220,80],[100,80,220],[220,80,220],[80,220,220]]

def process_image(raw_image,control):
  global threshold
  text=[]
  images=[]

  #Switch color threshold
  if control == ord("c"):
    threshold=(threshold+1)%len(THRESH_LOW)
  #Display contour and hierarchy details
  elif control == ord("i"):
    print("Contour: %s"%contours)
    print("Hierarchy: %s"%hierarchy)

  #Keep a copy of the raw image
  text.append("Raw Image %s"%THRESH_TXT[threshold])
  images.append(raw_image)

  #Blur the raw image
  text.append("with Blur...%s"%THRESH_TXT[threshold])
  images.append(cv2.blur(raw_image, BLUR))

  #Set the color thresholds
  lower = np.array(THRESH_LOW[threshold],dtype="uint8")
  upper = np.array(THRESH_HI[threshold], dtype="uint8")

  text.append("with Threshold...%s"%THRESH_TXT[threshold])
  images.append(cv2.inRange(images[-1], lower, upper))

  #Find contours in the threshold image
  text.append("with Contours...%s"%THRESH_TXT[threshold])
  images.append(images[-1].copy())
  image, contours, hierarchy = cv2.findContours(images[-1],
                                                cv2.RETR_LIST,
                                                cv2.CHAIN_APPROX_SIMPLE)

  #Display contour and hierarchy details
  if control == ord("i"):
    print("Contour: %s"%contours)
    print("Hierarchy: %s"%hierarchy)

  #Find the contour with maximum area and store it as best_cnt
  max_area = 0
  best_cnt = 1
  for cnt in contours:
    area = cv2.contourArea(cnt)
    if area > max_area:
      max_area = area
      best_cnt = cnt

  #Find the centroid of the best_cnt and draw a circle there
  M = cv2.moments(best_cnt)
  cx,cy = int(M['m10']/M['m00']), int(M['m01']/M['m00'])

  if max_area>0:
    cv2.circle(raw_image,(cx,cy),8,(THRESH_HI[threshold]),-1)
    cv2.circle(raw_image,(cx,cy),4,(THRESH_LOW[threshold]),-1)

  return(images,text)
#End

运行示例,请使用以下命令:

python3 opencv_display.py

使用 M 键在可用的显示模式之间循环,使用 C 键更改我们想要检测的特定颜色(蓝色、绿色、红色、洋红色或黄色),并使用 I 键显示检测到的轮廓和层次结构数据的详细信息。

如何操作…

原始图像(左上角)经过模糊(右上角)、阈值(左下角)和轮廓(右下角)操作处理。

工作原理…

第一个脚本(opencv_display.py)为我们提供了一个运行 OpenCV 示例的通用基础。该脚本包含两个函数,begin_capture()show_images()

begin_capture() 函数设置 PiCamera 以连续捕获帧(50 fps 和 640x480 分辨率),并将它们转换为适合 OpenCV 处理的原始图像格式。我们在这里使用相对较低的分辨率图像,因为我们不需要很多细节来执行我们旨在进行的处理。实际上,图像越小,它们使用的内存越少,我们需要的处理强度也越低。

通过使用 PiCamera 库的 camera.capture_continuous() 函数,我们将获得一个准备好的图像帧供我们处理。我们将每个新帧传递给 process_image() 函数,该函数将由 opencv_color_detect.py 文件提供,包括任何捕获的按键(以使用户获得一些控制)。process_image() 函数(我们将在稍后详细介绍)返回两个数组(图像和文本)。

我们将图像和文本数组传递给 show_images() 函数,以及选定的 MODE(用户通过按 M 键循环选择)。在 show_images() 函数中,我们使用给定的 MODE 的文本,并使用 putText() 将其添加到我们正在显示的图像中(再次强调,对应于所选的 MODE)。最后,我们使用 cv2.imshow() 在单独的窗口中显示图像。

工作原理…

脚本显示原始图像(包括跟踪标记)

所有真正的乐趣都包含在 opencv_color_detect.py 脚本中,该脚本执行所有必要的图像处理以处理我们的原始视频流。目标是简化源图像,然后识别任何匹配所需颜色的区域的中心。

小贴士

注意:脚本故意保留了每个处理阶段,以便您可以自己看到每个步骤对前一个图像的影响。这是迄今为止理解我们如何从标准视频图像到计算机能够理解的内容的最好方式。为了实现这一点,我们使用数组收集我们生成的图像(使用images.append()添加每个新图像,并使用一种Pythonic的方式来引用数组中的最后一个元素,即[-1]表示法。在其他编程语言中,这会产生错误,但 Python 中使用负数从数组的末尾向前计数是完全可接受的,因此-1 是数组末尾的第一个元素,-2 将是第二个从末尾开始)。

process_image()图像函数应生成四个不同的图像(我们在images数组中提供了对这些图像的引用)。在第一张图像中,我们只是保留了我们原始图像的副本(显示为0: Raw Image [Color])。由于这是一个未受干扰的全彩图像,因此这将是我们要展示检测到的对象位置的图像(这将在函数末尾添加)。

我们生成的下一张图像是原始图像(显示为1: with Blur…[Color])的模糊版本,通过使用cv2.blur()函数和BLUR元组来指定在(x,y)轴上的模糊量。通过轻微模糊图像,我们希望消除图像中的任何不必要的细节或错误噪声;这是理想的,因为我们只对大块的颜色感兴趣,所以细微的细节是不相关的。

第三张图像(显示为2:with Threshold…[color])是使用cv2.inRange()函数应用给定的高阈值和低阈值的结果。这产生了一个简单的黑白图像,其中任何位于上下颜色阈值之间的图像部分都以白色显示。希望您能够在将测试对象移到相机前时清楚地看到它作为一个大块白色区域。您可以检查这张图像,以确保您的背景不会与目标对象混淆。如果阈值图像主要是白色,则尝试不同的颜色目标,将相机移到不同的位置,或调整阈值数组中使用的颜色(THRESH_LOW/HI)。

注意

注意:本例中使用的颜色映射是 OpenCV 的BGR格式。这意味着像素颜色以三个整数的数组形式存储,分别代表蓝色、绿色和红色。因此,颜色阈值以这种格式指定;这与 HTML 网页颜色中更典型的 RGB 颜色格式相反。

最后一张图像提供了拼图的最后一部分;显示为3:with Contours...[color],它显示了cv2.findContours()函数的结果。OpenCV 将在图像中计算轮廓。这将发现阈值图像中的所有形状边缘,并将它们作为一个列表(轮廓)返回。每个单独的轮廓是图像中每个形状边界点的(x,y)坐标数组。

小贴士

注意cv2.findContours()函数直接将轮廓应用于提供的图像,这就是为什么我们制作阈值图像的副本(使用images[-1].copy()),这样我们就可以看到我们过程中的两个步骤。我们还使用cv2.CHAIN_APPROX_SIMPLE,它试图简化存储的坐标,因此可以跳过任何不需要的点(例如,任何沿直线上的点可以删除,只要我们有起点和终点)。或者,我们也可以使用cv2.CHAIN_APPROX_NONE,它保留所有点。

我们可以使用轮廓列表来确定每个轮廓的面积;在我们的案例中,我们最感兴趣的是最大的一个(它可能包含我们正在跟踪的对象,作为图像中具有给定阈值的颜色区域的最大面积)。我们将使用cv2.contourArea()对每个发现的轮廓进行面积计算,并保留最终面积最大的那个。

最后,我们可以列出矩度,它们是一系列数字,提供了形状的数学近似。矩度为我们提供了一个简单的计算方法,以获得形状的重心。重心就像形状的质心;例如,如果它是由一块平板固体材料制成,那么它将是你可以将其放在手指尖上平衡的点。

cx, cy = M['m10'] / M['m00'], M['m01'] / M['m00'])

我们使用计算出的坐标显示一个小标记(由上、下阈值颜色组成),以指示检测到的物体位置。

它是如何工作的…

物体的位置在图像中跟踪时用彩色点标记

关于 OpenCV 的轮廓和矩度的更多信息,请参阅 OpenCV-Python 教程(goo.gl/eP9Cn3)。

还有更多…

这个配方允许我们通过检测摄像头帧内的所需颜色来跟踪对象,这将提供对象的相对xy位置。

我们可以将树莓派摄像头安装在可移动平台上,例如一个漫游/昆虫机器人平台(如第九章中描述的第九章),或者使用伺服控制的倾斜和旋转摄像头支架(如图所示)。

还有更多…

树莓派摄像头可以通过伺服支架进行控制

通过结合摄像头输入和物体坐标,我们可以让树莓派追踪物体无论它去哪里。如果我们检测到物体已经移动到摄像头框架的一侧,我们可以使用树莓派硬件控制将物体重新定位在摄像头框架内(通过控制机器人或倾斜和移动摄像头)。

更多内容…

物体已经在屏幕的右上角被检测到,所以将摄像头转向右边和上方以追踪物体

使用 OpenCV 进行运动追踪

虽然能够追踪特定颜色的物体很有用,但有时我们只是对实际的运动过程感兴趣。这尤其适用于我们希望追踪的物体可能融入背景的情况。

注意

注意:安全摄像头通常使用红外探测器作为触发器;然而,这些依赖于检测传感器上检测到的热量的变化。这意味着如果物体相对于背景没有发出额外的热量,它们将无法工作,并且它们不会追踪运动的方向。

learn.adafruit.com/pir-passive-infrared-proximity-motion-sensor/how-pirs-work

以下食谱将演示如何使用 OpenCV 检测运动,并提供物体在一段时间内移动的记录。

使用 OpenCV 进行运动追踪

框架内物体的运动在屏幕上被追踪,允许记录并研究运动模式

准备中

以下脚本将使我们能够追踪一个物体并在屏幕上显示其路径。为此任务,我自愿选择了我们家的家龟;然而,任何移动的物体都可以使用。

准备中

我们的乌龟是一个出色的测试对象;看到她在白天四处游荡非常有趣

在这种情况下,设置效果特别好的原因如下。首先,由于乌龟的颜色与背景相似,我们无法使用之前的方法进行颜色检测(除非我们在她身上贴上一些标记)。其次,乌龟的笼子上方有一个有用的架子,允许树莓派和摄像头直接安装在上方。最后,笼子是人工照明的,所以在我们的测试期间,除了乌龟的运动外,观察到的图像应该保持相对稳定。当使用外部因素,如自然光进行此任务时,你可能会发现它们会干扰运动检测(使得很难确定变化是由于运动还是环境变化——参见更多内容部分以获取克服此问题的技巧)。

其余的设置将与之前的 OpenCV 食谱相同(参见使用 OpenCV 进行颜色检测)。

如何做到这一点…

创建以下脚本,命名为opencv_detect_motion.py

#!/usr/bin/python3
#opencv_motion_detect.py
import cv2
import numpy as np

GAUSSIAN=(21,21)

imageBG=None
gray=True

movement=[]
AVG=2
avgX=0
avgY=0
count=0

def process_image(raw_image,control):
  global imageBG
  global count,avgX,avgY,movement,gray

  text=[]
  images=[]
  reset=False

  #Toggle Gray and reset background
  if control == ord("g"):
    if gray:
      gray=not gray
    reset=True
    print("Toggle Gray")
  #Reset the background image
  elif control == ord("r"):
    reset=True

  #Clear movement record and reset background
  if reset:
    print("Reset Background")
    imageBG=None
    movement=[]

  #Keep a copy of the raw image
  text.append("Raw Image")
  images.append(raw_image)

  if gray:
    raw_image=cv2.cvtColor(raw_image,cv2.COLOR_BGR2GRAY)

  #Blur the raw image
  text.append("with Gaussian Blur...")
  images.append(cv2.GaussianBlur(raw_image, GAUSSIAN))

  #Initialise background
  if imageBG is None:
    imageBG=images[-1]

  text.append("with image delta...")  
  images.append(cv2.absdiff(imageBG,images[-1]))

  text.append("with threshold mask...")                
  images.append(cv2.threshold(images[-1], 25, 255,
                             cv2.THRESH_BINARY)[1])

  text.append("with dilation...")                
  images.append(cv2.dilate(images[-1],None, iterations=3))

  #Find contours
  if not gray:
    #Require gray image to find contours
    text.append("with dilation gray...")
    images.append(cv2.cvtColor(images[-1],cv2.COLOR_BGR2GRAY))
  text.append("with contours...")
  images.append(images[-1].copy())
  aimage, contours, hierarchy = cv2.findContours(images[-1],
                                                 cv2.RETR_LIST,
                                                 cv2.CHAIN_APPROX_SIMPLE)

  #Display contour and hierarchy details
  if control == ord("i"):
    print("Contour: %s"%contours)
    print("Hierarchy: %s"%hierarchy)

  #Determine the area of each of the contours
  largest_area=0
  found_contour=None
  for cnt in contours:
    area = cv2.contourArea(cnt)
    #Find which one is largest
    if area > largest_area:
      largest_area=area
      found_contour=cnt

  if found_contour != None:
    #Find the centre of the contour
    M=cv2.moments(found_contour)
    cx,cy=int(M['m10']/M['m00']),int(M['m01']/M['m00'])
    #Calculate the average
    if count<AVG:
      avgX=(avgX+cx)/2
      avgY=(avgY+cy)/2
      count=count+1
    else:
      movement.append((int(avgX),int(avgY)))
      avgX=cx
      avgY=cy
      count=0

  #Display
  if found_contour != None:
    cv2.circle(images[0],(cx,cy),10,(255,255,255),-1)
  if len(movement) > 1:
    for i,j in enumerate(movement):
      if i>1:
        cv2.line(images[0],movement[i-1],movement[i],(255,255,255))

  return(images,text)  
#End

接下来,在 opencv_display.py 文件中找到以下行(来自前面的食谱):

import opencv_color_detect as PROCESS 

变更为以下:

import opencv_motion_detect as PROCESS

要运行示例,请使用以下命令:

python3 opencv_display.py

使用 M 键在可用的显示模式之间循环,使用 G 键切换灰度模式,使用 I 键显示检测到的轮廓和层次结构数据的信息,使用 B 键重置我们设置为背景的图像。

它是如何工作的…

这种运动检测方法背后的原理简洁而优雅。首先,我们将初始图像作为我们的金图像(此时没有动作发生);我们将将其视为我们的静态背景。现在我们只需将任何后续图像与这个原始背景图像进行比较。如果与第一幅图像有任何显著差异,我们假设差异是由于运动引起的。一旦我们检测到运动,我们将在帧上生成运动轨迹并显示它。

它是如何工作的…

金图像(右侧)是原始图像(左侧)的灰度版本,并应用了高斯模糊。

当脚本运行时,我们确保重置标志设置为 True,这确保我们使用捕获的第一幅图像作为金图像(此外,如果用户按下 R,我们允许金图像通过新图像刷新)。我们还检测用户是否按下 G,这将切换在灰度或彩色中处理图像。默认是灰度,因为这种处理更有效,颜色在检测运动时并不重要(但看到图像仍然为彩色时的相同处理结果也很有趣)。

就像前面的食谱一样,我们将保留每个图像的副本,以便更好地理解过程中的每个阶段。首先显示的图像是 0:原始图像,它是相机图像的直接副本(我们将在该图像上叠加检测到的运动)。

在下一张图像中,1:with Gaussian Blur…,我们使用 cv2.GaussianBlur(raw_image, GAUSSIAN, 0),提供原始图像的平滑版本(希望从图像中去除高斯噪声)。像 blur 函数一样,我们提供要处理的图像和 x,y 放大值(对于高斯算法,这些值必须是正数且为奇数)。

注意

注意:您可以通过插入以下代码(在高斯模糊部分之前)并在模式之间循环来比较高斯模糊与标准模糊方法:

  text.append("with Low Blur...")
  images.append(cv2.blur(raw_image, (5,5))
  text.append("with High Blur...")
  images.append(cv2.blur(raw_image, (30,30))

使用此模糊图像设置背景图像(如果之前尚未设置或已重置)。

我们使用 cv2.absdiff(imageBG,images[-1]) 来确定 imageBG(原始背景图像)和最新高斯模糊图像之间的差异,以提供 2:with image delta...

它是如何工作的…

这张图像(在此处反转以使其更清晰)显示了与金图像的差异。乌龟移动到了图像的中间附近

接下来,我们应用二值阈值掩码(显示为3:with threshold mask…),这将设置介于上限(255)和下限(25)之间的任何像素为 255,从而得到一个显示主要运动区域的黑白图像。

如何工作…

对差分图像应用阈值滤波器,突出显示图像中的最大变化。

现在,我们使用cv2.dilate(images[-1], None, iterations=3)对阈值图像(显示为4:with dilation…)进行膨胀。dilate操作通过在每次迭代中使图像的白色部分增长一个像素来实现。通过将None作为第二个参数,我们设置内核使用默认值(或者,可以使用由 0s 和 1s 组成的数组来完全控制膨胀的应用方式)。

如何工作…

膨胀图像使检测到的运动点增长。

我们使用cv2.contours()函数,就像在之前的食谱中做的那样,来检测检测到的形状的轮廓;结果显示为5:with contours…。如果图像还不是灰度图,我们必须将其转换为灰度图,因为该函数最适合二值图像(黑白图像)。

如何工作…

计算轮廓的面积并用于确定主要运动区域的定位。

与之前一样,我们计算每个轮廓的面积,并使用cv2.contourArea()找出最大的轮廓。然后,我们通过找到矩(通过cv2.moments())来确定所选轮廓中心的坐标。最后,我们将这些坐标添加到矩数组中,以便在原始图像上显示检测到的运动的轨迹。

此外,为了追踪相对缓慢移动的物体,我们还可以平均几个检测到的坐标,以提供更平滑的运动轨迹。

如开头所述,外部因素可能会干扰这个简单的算法,即使是环境中的细微变化也可能导致运动检测错误。幸运的是,通过将长期平均应用于背景图像(而不是单次快照),可以将任何逐渐变化,如光照,纳入背景图像中。

还有更多…

尽管我们只是简要地触及了 OpenCV 库的一个小方面,但应该很清楚,它非常适合与树莓派一起使用。我们已经看到 OpenCV 提供了相对容易的非常强大的处理能力,而树莓派(尤其是树莓派 2 型)是运行它的理想平台。

如你所想,仅仅通过几个示例来涵盖 OpenCV 能够做到的所有事情并不实际,但我希望这至少已经激起了你的兴趣(并且为你提供了一个现成的设置,你可以从中进行实验并创建自己的项目)。

幸运的是,不仅网上有大量的教程和指南,还有几本书详细介绍了 OpenCV;特别是以下 Packt 出版的书籍被推荐:

  • 《使用 Python 的 OpenCV 计算机视觉》约瑟夫·豪斯 编著

  • 《树莓派计算机视觉编程》阿什温·帕贾卡尔 编著

在最后两个例子中,我尽量使代码尽可能简短,同时确保易于观察背后的工作原理。通过导入不同的模块以及使用你自己的 process_images() 函数,应该非常容易对其进行修改或添加。

对于更多想法和项目,以下网站上有一个非常优秀的列表:

www.intorobotics.com/20-hand-picked-raspberry-pi-tutorials-in-computer-vision/

第九章.构建机器人

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

  • 使用正向驱动电机构建 Rover-Pi 机器人

  • 使用高级电机控制

  • 构建一个六足 Pi-Bug 机器人

  • 使用 ServoBlaster 直接控制伺服机构

  • 避免物体和障碍物

  • 获得方向感

简介

一台“大脑大小如行星”的小型计算机(引用道格拉斯·亚当斯,《银河系漫游指南》的作者)是您自己机器人创造物的理想大脑。实际上,树莓派可能提供的处理能力远远超过一个小型机器人或越野车所需的;然而,其小巧的尺寸、优秀的连接性和相对较低的能量需求意味着它非常适合。

本章将重点探讨我们可以将电机或伺服机构以何种方式组合起来产生机器人运动,并使用传感器收集信息,使我们的创造物能够对其做出反应的各种方法。

小贴士

请务必查看附录,硬件和软件列表;它列出了本章中使用的所有物品及其获取地点。

使用正向驱动电机构建 Rover-Pi 机器人

构建机器人不必是一项昂贵的爱好。可以使用家用物品构建一个小型、越野车型的机器人作为底盘(即所有部件都连接到的基座),并且可以使用几个小型驱动电机来移动它。

Rover-Pi 机器人是一种小型、越野车型的机器人,它只有两个轮子和前部的滑轮或万向轮,以便它能够转向。以下图片展示了一个这样的机器人:

使用正向驱动电机构建 Rover-Pi 机器人

自制 Rover-Pi 机器人

虽然它可能无法与火星探测车相提并论,但正如您将看到的,您有很多可以实验的东西。

您还可以购买许多价格低廉的机器人套件,这些套件包含您在一个包装中所需的大部分东西(请参阅本例末尾的更多内容部分)。

准备中

我们正在构建的 Rover 将需要包含以下图中所示的元素:

准备中

Rover-Pi 机器人的各个部分

以下将详细讨论这些元素:

  • 底盘:本例使用了一个修改过的、电池供电的推杆夜灯(尽管任何合适的平台都可以使用)。请记住,您的机器人越大、越重,驱动电机就需要越强大才能移动它。或者,您也可以使用更多内容部分中列出的底盘套件之一。以下图片展示了一个合适的推杆夜灯:准备中

    这款推杆夜灯构成了 Rover-Pi 机器人的基本底盘

  • 前滑或转向轮: 这可以简单到用一个大曲别针(76 mm/3 英寸)弯曲成形状,或者一个小转向轮。滑轮在光滑表面上工作时效果最佳,但它可能会卡在地毯上。转向轮在所有表面上都能很好地工作,但有时可能会在转向时出现问题。

  • 轮子电机齿轮: Rover-Pi 机器人的轮子移动是电机、齿轮和轮子的组合。齿轮很有用,因为它们允许高速旋转的电机以较慢的速度和更大的力量(扭矩)转动轮子;这将允许更好地控制我们的机器人。以下图片显示了一个将轮子、电机和齿轮组合在一起的单元:准备中

    这些内置齿轮电机的轮子非常适合小型巡游车

  • 电池/电源: Rover-Pi 机器人将使用安装在底盘托架中的 4 节 AA 电池供电。或者,可以使用标准电池盒,甚至可以使用一根长线连接到合适的电源。建议您从独立于 Raspberry Pi 的电源为电机供电。这将有助于避免在驱动电机时,当需要大幅增加电流以移动时,Raspberry Pi 突然断电的情况。或者,您可以使用 5V 稳压器用电池为 Raspberry Pi 供电。以下图片显示了一个带有 4 节 AA 电池的底盘:准备中

    4 节 AA 电池为驱动轮提供电源

  • 电机驱动器/控制器: 电机需要比 GPIO 可以处理的电压和电流更大的电压和电流。因此,我们将使用 Darlington 阵列模块(它使用 ULN2003 芯片)。请参阅本例末尾的 更多内容... 部分,以了解更多有关该特定模块如何工作的详细信息。以下图片显示了一个 Darlington 阵列模块:准备中

    在 dx.com 上可用的 Darlington 阵列模块可以用来驱动小型电机

  • 小型电缆扎带或线扎带: 这将使我们能够将电机或控制器等物品固定在底盘上。以下图片显示了电缆扎带的使用:准备中

    我们使用电缆扎带来固定电机和轮子到底盘上

  • Raspberry Pi 连接: 最简单的设置是将控制线通过长线连接到 Raspberry Pi,这样您就可以通过连接的屏幕和键盘直接轻松控制您的机器人。稍后,您可以考虑将 Raspberry Pi 安装在机器人上,并远程(或者如果您包括传感器和智能来理解它们,甚至可以自主)控制它。

在本章中,我们将使用 WiringPi2 Python 库来控制 GPIO;有关如何使用 PIP(Python 包管理器)安装它的详细信息,请参阅第七章,感知和显示现实世界数据

如何做到这一点...

执行以下步骤以创建一个小型 Rover-Pi 机器人:

  1. 在底盘的前方,你需要通过将纸夹/电线弯曲成 V 形来安装滑轨。通过在两侧钻小孔将纸夹/电线固定在底盘前方,并通过电线周围的孔穿过电缆扎带并拉紧以固定。安装好的电线滑轨应类似于以下图片所示:如何操作…

    将滑轨安装在 Rover-Pi 机器人的前方

  2. 在安装轮子之前,你需要计算出底盘的大致重心(在底盘中安装电池时进行此操作,因为它们会影响平衡)。通过尝试用两侧的两根手指平衡单元来感受重心的位置,并找出底盘向前或向后倾斜的程度。对于我的单元,这大约是在中心后 1 厘米(大约三分之一英寸)的位置。你应该将轮轴稍微放在这个位置之后,以便 Rover 在滑轨上稍微向前倾斜。在底盘上标记轮子的位置。

  3. 在每侧钻三个孔,使用电缆扎带安装轮子。如果电缆扎带不够长,可以将两个扎带连接在一起,通过将一个扎带的末端穿过另一个扎带的末端(只拉到扎带能够抓住的程度,以便延长扎带)。以下图示显示了如何使用电缆扎带:如何操作…

    将电机牢固地固定在底盘上

  4. 接下来,通过将电池插入单元来测试电机;然后,断开原本连接到灯泡的电线,并将它们接触到电机触点上。确定电机上哪个连接应该是正极,哪个应该是负极,以便机器人向前移动(当机器人面向前方时,轮子的顶部应该向前移动)。将红黑电线连接到电机(在我的机器上,黑色在电机顶部是负极,红色在电机底部是正极),确保电线足够长,可以到达底盘上的任何位置(大约 14 厘米,即大约五又二分之一英寸就足够用于夜灯)。

    Rover-Pi 机器人的组件应按照以下图示进行接线:

    如何操作…

    Rover-Pi 机器人的接线布局

为了建立连接,请执行以下步骤:

  1. 将电机的黑色电线连接到达林顿模块的OUT 1(左侧)和OUT 2(右侧)输出,将红色电线连接到最后一个引脚(COM 连接)。

  2. 接下来,将电池电线连接到模块底部的GND/V-V+连接。

  3. 最后,将 GPIO 连接器的GND引脚 6)连接到相同的GND连接。

  4. 通过将 3.3V(GPIO 引脚 1)连接到IN1IN2来测试电机控制,以模拟 GPIO 输出。当你满意时,将 GPIO 引脚 16连接到IN1(用于左侧)和 GPIO 引脚 18连接到IN2(用于右侧)。

线路现在应与以下表格中给出的细节相匹配:

Raspberry Pi GPIO 达林顿模块
引脚 16:左侧 IN1
引脚 18:右侧 IN2
引脚 6:GND GND/V-(标记为“–”)
电机 4 x AA 电池达林顿模块
电池正极 V+(标记为“+”)
电池负极 GND/V-(标记为“–”)
电机
左电机:黑色导线 OUT 1(白色插座中的顶部引脚)
右电机:黑色导线 OUT 2(白色插座中的第二个引脚)
两个电机:红色导线 COM(白色插座中的最后一个引脚)

使用以下rover_drivefwd.py脚本来测试控制:

#!/usr/bin/env python3
#rover_drivefwd.py
#HARDWARE SETUP
# GPIO
# 2[==X====LR====]26[=======]40
# 1[=============]25[=======]39
import time
import wiringpi2
ON=1;OFF=0
IN=0;OUT=1
STEP=0.5
PINS=[16,18] # PINS=[L-motor,R-motor]
FWD=[ON,ON]
RIGHT=[ON,OFF]
LEFT=[OFF,ON]
DEBUG=True

class motor:
  # Constructor
  def __init__(self,pins=PINS,steptime=STEP):
    self.pins = pins
    self.steptime=steptime
    self.GPIOsetup()

  def GPIOsetup(self):
    wiringpi2.wiringPiSetupPhys()
    for gpio in self.pins:
      wiringpi2.pinMode(gpio,OUT)

  def off(self):
    for gpio in self.pins:
      wiringpi2.digitalWrite(gpio,OFF)

  def drive(self,drive,step=STEP):
    for idx,gpio in enumerate(self.pins):
      wiringpi2.digitalWrite(gpio,drive[idx])
      if(DEBUG):print("%s:%s"%(gpio,drive[idx]))
    time.sleep(step)
    self.off()

  def cmd(self,char,step=STEP):
    if char == 'f':
      self.drive(FWD,step)
    elif char == 'r':
      self.drive(RIGHT,step)
    elif char == 'l':
      self.drive(LEFT,step)
    elif char == '#':
      time.sleep(step)

def main():
  import os
  if "CMD" in os.environ:
    CMD=os.environ["CMD"]
    INPUT=False
    print("CMD="+CMD)
  else:
    INPUT=True
  roverPi=motor()
  if INPUT:
    print("Enter CMDs [f,r,l,#]:")
    CMD=input()
  for idx,char in enumerate(CMD.lower()):
    if(DEBUG):print("Step %s of %s: %s"%(idx+1,len(CMD),char))
    roverPi.cmd(char)

if __name__=='__main__':
  try:
    main()
  finally:
    print ("Finish")
#End

小贴士

记住,在运行本章中的脚本之前应该安装 WiringPi2(见第七章,感知和显示现实世界数据)。

使用以下命令运行之前的代码:

sudo python3 rover_drivefwd.py

脚本将提示以下信息:

Enter CMDs [f,r,l,#]:

你可以输入一系列要执行的命令,例如:

ffrr#ff#llff

之前的命令将指示 Rover-Pi 机器人执行一系列移动——向前(f)、向右(r)、暂停(#)和向左(l)。

它是如何工作的…

一旦你构建了机器人并将轮子连接到电机控制器,你就可以发现如何控制它。

首先导入time(这将允许你在电机控制中设置暂停)和wiringpi2以允许控制 GPIO 引脚。在这里使用wiringpi2,因为它使得在以后使用 IO 扩展器和其他 I²C 设备时更容易利用 IO 扩展器。

定义用于设置引脚ON/OFF、方向IN/OUT以及每个电机STEP持续时间的值。还要定义哪些PINS连接到电机控制和我们的移动FWDRIGHTLEFT。移动的定义方式是,通过同时打开两个电机,你可以向前移动,或者通过只打开一个电机,你可以转向。通过在文件开头使用变量设置这些值,我们的代码更容易维护和理解。

我们定义一个motor类,这将允许我们在其他代码中重用它或轻松地将其与替代motor类交换,这样我们就可以在需要时使用其他硬件。我们设置我们使用的默认引脚和我们的steptime值(steptime对象定义了每次步进时我们驱动电机的时间)。然而,如果需要,这两个值在初始化对象时也可以指定。

接下来,我们调用GPIOsetup();它选择物理引脚编号模式(因此我们可以根据它们在板上的位置来引用引脚)。我们还设置我们使用的所有引脚为输出模式。

最后,对于motor类,我们定义以下三个函数:

  • 我们定义的第一个函数(称为 off())将允许我们关闭电机,因此我们遍历引脚列表,将每个 GPIO 引脚设置为低电平(因此关闭电机)。

  • drive() 函数允许我们提供一个驱动操作列表(每个 GPOI 引脚的 ONOFF 组合)。同样,我们遍历每个引脚,将其设置为相应的驱动操作,等待步进时间,然后使用 off() 函数关闭电机。

  • 我们定义的最后一个函数(称为 cmd())简单地允许我们发送 char(单个字符)并使用它来选择我们想要使用的驱动操作集合(FWDRIGHTLEFT 或等待 #)。

对于测试,main() 允许我们通过以下命令从命令行指定需要执行的一系列操作:

sudo CMD=f#lrr##fff python3 rover_drivefwd.py

通过导入 os 模块以使用 os.environ,我们可以检查命令中的 CMD 并将其用作我们的驱动操作列表。如果没有提供 CMD 命令,我们可以使用 input() 函数直接提示输入驱动操作列表。要使用 motor 类,我们设置 roverPi=motor();这允许我们使用驱动操作列表中的每个字符调用电机类的 cmd() 函数。

还有更多...

你的机器人应该只受限于你自己的创造力。你可以使用很多合适的底盘,其他电机、轮子以及控制驱动轮子的方式。你应该进行实验和测试,以确定哪些组合效果最佳。这些都是乐趣的一部分!

达林顿阵列电路

达林顿晶体管是驱动更高功率设备(如电机甚至继电器)的低成本方式。它们由两个串联排列的晶体管组成,其中一个晶体管向另一个晶体管供电(允许电流增益相乘)。也就是说,如果第一个晶体管的增益为 20,第二个晶体管的增益也为 20,那么它们将提供总增益为 400。这意味着基极引脚(1)上的 1 mA 电流可以使你通过达林顿晶体管驱动高达 400 mA 的电流。达林顿晶体管的电气符号在以下图示中显示:

达林顿阵列电路

达林顿晶体管的电气符号显示了两个晶体管是如何封装在一起的

在前面的模块中使用了 ULN2003 芯片,它提供了 7 个 NPN 达林顿晶体管(如果需要更多输出或与两个步进电机一起使用,还有 8 通道版本的 ULN2803)。以下图示显示了如何使用达林顿阵列来驱动电机:

达林顿阵列电路

使用达林顿阵列驱动两个小型电机

芯片每个输出端最多可提供 500 mA 的电流,电压高达 50V(足以驱动大多数小型电机)。然而,随着使用时间的延长,芯片可能会过热,因此在驱动大型电机时建议使用散热片。芯片内部已集成在每个达林顿晶体管上跨接的保护二极管。这是必要的,因为当电机在没有驱动的情况下移动时(这可能是由于电机的自然惯性引起的),它将像发电机一样工作。会产生一个称为反电动势的反向电压,如果不通过二极管耗散,将会损坏晶体管。

芯片的一个缺点是,正电源电压必须始终连接到公共端(COM),因此每个输出端只能吸收电流。也就是说,它只能驱动电机单向转动,COM 端为正电压,OUT 端为负电压。因此,如果我们希望驱动 Rover-Pi 机器人以不同方向行驶,我们需要一个不同的解决方案(参见下一例“使用高级电机控制”配方)。

这些芯片也可以用来驱动某些类型的步进电机。dx.com 网站上的一个模块将步进电机作为套件的一部分。尽管齿轮是为了非常缓慢的运动设计的,大约每转 12 秒(对于漫游车来说太慢了),但仍然很有趣(比如用于时钟)。

晶体管和继电器电路

由于继电器是由电磁线圈控制的机械开关,可以处理更多高功率的电机,因此它们能够处理更多高功率的电机。然而,它们需要相当大的电流来开启,通常超过 3.3V。为了切换小型继电器,我们需要大约 60 mA 的 5V 电流(比 GPIO 提供的要多),因此我们仍然需要使用一些额外的组件来切换它。

我们可以使用达林顿阵列(如之前所述)或一个小型晶体管(任何小型晶体管,如 2N2222,都行)来提供切换所需的电流和电压。以下电路将允许我们做到这一点:

晶体管和继电器电路

用于驱动外部电路的晶体管和继电器电路

就像电机一样,继电器也可以产生电磁脉冲,因此也需要一个保护二极管来避免晶体管上出现任何反向电压。

这是一个非常有用的电路,不仅适用于驱动电机,也适用于任何外部电路;物理开关允许它与控制它的 Raspberry Pi 独立且电气隔离。

只要继电器正确标定,你就可以通过它驱动直流或交流设备。

注意

你可以使用一些继电器来控制由市电供电的设备。但是,这应该仅以极端谨慎和适当的电气培训进行。市电的电力可以致命或造成严重伤害。

PiBorg 有一个现成的模块,名为PicoBorg,它允许切换多达四个继电器。它使用称为MOSFETs的设备,它们实际上是具有相同原理的高功率晶体管版本。

连接或非连接机器人

在设计自己的 Rover-Pi 机器人时,一个重要的选择是决定你是否想使其完全自包含,或者你是否愿意有一个连接线(一个连接到 Rover-Pi 的长控制/电源电缆)。使用连接线,你可以降低 Rover-Pi 机器人的重量,这意味着小型电机可以轻松地移动单元。这将允许你将 Raspberry Pi 与主单元分开,以便它可以连接到屏幕和键盘,便于编程和调试。主要的缺点是,你将需要一个长长的脐带状连接到你的 Rover-Pi 机器人(每个控制信号都有一个电线),这可能会阻碍其移动。然而,正如我们稍后将要看到的,你可能只需要三根或四根电线就能提供所有需要的控制(参见下一道菜谱中的使用 I/O 扩展器部分)。

如果你打算直接将 Raspberry Pi 安装在 Rover-Pi 机器人上,你需要一个合适的电源,例如手机充电电池组。如果电池组有两个 USB 端口,那么你可能能够将其用作电源来驱动 Raspberry Pi 和电机。该单元必须能够独立维持供应,因为任何由驱动电机引起的电源尖峰都可能导致 Raspberry Pi 重置。

记住,如果现在 Raspberry Pi 已经连接到机器人上,你需要一种控制它的方法。这可以是一个 USB Wi-Fi 适配器,它允许通过 SSH 进行远程连接,或者一个无线键盘(使用 RF/蓝牙),甚至第六章中的 GPIO D-Pad,使用 Python 驱动硬件,可以用于直接控制。

然而,你安装到底盘上的东西越多,电机就越需要努力工作来移动。你可能会发现需要更强的电机,而不是这里使用的小电机。以下图像显示了由 USB 电池组供电的 Rover-Pi 机器人:

连接或非连接机器人

一个由电池供电的 Raspberry Rover-Pi 机器人,通过 Wi-Fi 控制(可选的电缆管理)

探路者套件

如果你不想自己制作底盘,也有许多现成的探路者底盘可供使用。它们如下:

  • 来自 SparkFun 的 2WD 魔术师机器人底盘

  • 来自 DX.com 的 4 电机智能小车底盘

  • 2 轮智能小车模型 DX.com探路者套件

    Tiddlybot 展示了多个组件如何在单个平台上集成在一起,如我在修改版本中所示

一个特别好的机器人设置是 Tiddlybot(来自PiBot.org),它结合了多个传感器、连续伺服、机载电池组和 Raspberry Pi 摄像头。SD 卡已设置,使 TiddlyBot 充当 Wi-Fi 热点,提供简单的拖放编程平台和遥控接口。这展示了如何将本章中描述的简单组件组合成一个完整的系统。

Rover 套件

Tiddlybot 图形用户界面提供了一个跨平台的拖放界面以及 Python 支持

小贴士

一定要查看附录,硬件和软件列表;它列出了本章中使用的所有物品及其获取地点。

使用高级电机控制

先前的驱动电路不适用于驱动多方向运行的电机(因为它们只能切换电机的开或关)。然而,使用名为 H 桥的电路,你还可以切换和控制电机的方向。开关组合如下所示:

使用高级电机控制

电机方向可以通过不同的开关组合来控制

使用不同的开关组合,我们可以通过切换电机的正负电源来改变电机的方向(SW1SW4激活电机,SW2SW3反转电机)。然而,我们不仅需要每个电机四个开关设备,而且由于 ULN2X03 设备以及 PiBorg 的 PicoBorg 模块只能吸收电流,因此还需要等效的设备来提供电流(以构成开关的上部部分)。

幸运的是,有专门设计的 H 桥芯片,如 L298N,它们内部包含先前的电路,以提供强大且方便的电机控制解决方案。

准备就绪

我们将用以下图像中所示的 H 桥电机控制器替换先前的达林顿阵列模块:

准备就绪

H 桥电机控制器允许控制电机的方向

关于这个单元的详细信息可在www.geekonfire.com/wiki/index.php?title=Dual_H-Bridge_Motor_Driver找到。

L298N 的数据表可在www.st.com/resource/en/datasheet/l298.pdf找到。

如何操作…

单元需要按照以下方式布线(这与其他 H 桥类型控制器类似,但如果有疑问,请查阅相关数据表)。

下表显示了电机和电机电源如何连接到 H 桥控制器模块:

模块的电机侧 – 连接到电池和电机
电机 A
---
左电机红线

以下表格显示了 H 桥控制器模块如何连接到树莓派:

模块的控制端 – 连接到树莓派 GPIO 引脚
ENA
---

建议您保持上拉电阻跳线(UR1-UR4)开启,并允许电机电源为板载电压稳压器供电,进而为 L298N 控制器(跳线 5V_EN)供电。板载稳压器(78M05 器件)可提供高达 500 mA 的电流,足以供电给 L298N 控制器以及任何额外的电路,如 IO 扩展器(更多信息请参阅更多内容部分)。ENA 和 ENB 引脚应断开连接(电机输出默认保持启用状态)。

您需要对之前的rover_drivefwd.py脚本进行以下更改(您可以将其保存为rover_drive.py)。

在文件顶部,重新定义PINS如下:

PINS=[15,16,18,22]   # PINS=[L_FWD,L_BWD,R_FWD,R_BWD]

按照以下方式更新控制模式:

FWD=[ON,OFF,ON,OFF]
BWD=[OFF,ON,OFF,ON]
RIGHT=[OFF,ON,ON,OFF]
LEFT=[ON,OFF,OFF,ON]

接下来,我们需要将反转命令添加到cmd()中,如下所示:

  def cmd(self,char,step=STEP):
    if char == 'f':
      self.drive(FWD,step)
    elif char == 'b':
      self.drive(BWD,step)
    elif char == 'r':
      self.drive(RIGHT,step)
    elif char == 'l':
      self.drive(LEFT,step)
    elif char == '#':
      time.sleep(step)

最后,我们可以更新main()函数内的提示,包括b(反转)作为选项,如下所示:

print("Enter CMDs [f,b,r,l,#]:")

工作原理...

H 桥电机控制器通过增加额外的电路来重现先前的切换电路,以确保电子开关不会造成短路(不允许SW1SW3SW2SW4同时启用)。H 桥电机控制器的切换电路如图所示:

工作原理...

H 桥切换电路的近似图(在电机关闭状态下)

输入(IN1IN2)将在电机上产生以下动作:

IN1IN2 0 1
0 电机关闭 电机反转
1 电机正转 电机关闭

正如我们在先前的菜谱中所做的那样,我们可以通过驱动两个电机向前来前进;然而,现在我们也可以驱动它们向后(以向后移动)以及相反方向(允许我们在原地转动 Rover-Pi 机器人)。

更多内容...

我们可以使用脉冲宽度调制PWM)信号来更精细地控制电机,并使用 IO 扩展器扩展可用输入/输出。

使用 PWM 控制电机速度

目前,Rover-Pi 机器人的电机是通过开关控制来控制的;然而,如果机器人移动得太快(例如,如果你安装了更大的电机或使用了更高的齿轮比),我们可以利用控制器上的ENAENB输入。如果这些设置为低,则禁用电机输出;如果设置为高,则再次启用。因此,通过 PWM 信号驱动它们,我们可以控制电机的速度。我们甚至可以设置不同的 PWM 比率(如果需要)来补偿电机/车轮或表面的任何差异,以使它们以不同的速度运行,如下面的图所示:

使用 PWM 控制电机速度

PWM 信号控制开启和关闭时间的比率

PWM 信号是一个数字的开启/关闭信号,其开启时间与关闭时间不同。使用 50:50 的开启:关闭信号的电机将以 100%开启信号的半功率运行,因此会运行得更慢。使用不同的比率,我们可以以不同的速度驱动电机。

我们可以使用树莓派的硬件 PWM(GPIO 引脚 12 可以使用 PWM 驱动器)。

注意

PWM 驱动器通常提供模拟音频输出的一个音频通道。有时,这会产生干扰;因此,建议你断开连接到模拟音频插座的任何设备。

wiringpi2中通过将引脚模式设置为2(这是PWM的值)并指定开启时间(表示为ON_TIME)来启用硬件 PWM 功能:

PWM_PIN=12; PWM=2; ON_TIME=512  #0-1024 Off-On

  def GPIOsetup(self):
    wiringpi2.wiringPiSetupPhys()
    wiringpi2.pinMode(PWM_PIN,PWM)
    wiringpi2.pwmWrite(PWM_PIN,ON_TIME)
    for gpio in self.pins:
      wiringpi2.pinMode(gpio,OUT)

然而,这仅适用于联合 PWM 电机控制(因为它连接到 ENA 和 ENB),因为只有一个可用的硬件 PWM 输出。

另一个替代方案是使用wiringpi2的软件 PWM 功能。这使用软件创建一个粗糙的 PWM 信号;根据你的需求,这可能可以接受。在 GPIO 引脚 7 和 GPIO 引脚 11 上生成软件 PWM 信号的代码如下:

PWM_PIN_ENA=7;PWM_PIN_ENA=11;RANGE=100 #0-100 (100Hz Max)
ON_TIME1=20; ON_TIME2=75 #0-100
ON_TIME1=20  #0-100
  def GPIOsetup(self):
    wiringpi2.wiringPiSetupPhys()
    wiringpi2.softPwmCreate(PWM_PIN_ENA,ON_TIME1,RANGE)
    wiringpi2.softPwmCreate(PWM_PIN_ENB,ON_TIME2,RANGE)
    for gpio in self.pins:
      wiringpi2.pinMode(gpio,OUT)

之前的代码将两个引脚都设置为 100 Hz,GPIO 引脚 7 设置为 2 ms 的开启时间(和 8 ms 的关闭时间),GPIO 引脚 11 设置为 7.5 ms/2.5 ms。

要调整 PWM 定时,请使用wiringpi2.softPwmWrite(PWM_PIN_ENA,ON_TIME2)

PWM 信号的准确性可能会被其他系统进程中断,但它可以控制一个小型微服电机,即使它有点抖动。

使用 I/O 扩展器

如我们之前所见(在第七章,感知和显示现实世界数据),wiringpi2允许我们轻松调整代码以使用 I²C 的 I/O 扩展器。在这种情况下,添加额外的电路可能很有用,例如传感器和 LED 状态指示器,甚至可能包括显示屏和控制按钮,以帮助调试和控制你正在开发的 Rover-Pi 机器人。

如果您打算将其用作固定设备,这将特别有用,因为您只需三根线就可以连接回 Raspberry Pi(I²C 数据 GPIO 引脚 3、I²C 时钟 GPIO 引脚 5 和地 GPIO 引脚 6),I²C VCC 由电机控制器的 5V 输出提供。

如前所述,添加以下 I²C 地址和引脚基的定义:

IO_ADDR=0x20
AF_BASE=100

然后,在 gpiosetup() 中,使用以下代码设置 MCP23017 设备:

wiringpi2.mcp23017Setup(AF_BASE,IO_ADDR)

确保您引用的任何引脚都是编号 100-115(以引用 I/O 扩展器的 A0-7 和 B0-7 引脚),并添加 AF_BASE(这是 I/O 扩展器的引脚偏移量)。

构建六足 Pi-Bug 机器人

控制电机对于创建类似车辆的机器人非常有用,但创建更自然行为的机器人组件,如伺服电机,可以提供出色的结果。有许多类似昆虫的机器人创意设计,甚至双足设计(具有类似人形的腿),它们使用伺服电机提供自然的关节运动。本例中的设计使用三个伺服电机,但这些原理和概念可以轻松应用于更复杂的设计,以控制使用多个伺服电机的腿/臂。以下图片展示了 Pi-Bug 机器人:

构建六足 Pi-Bug 机器人

六足 Pi-Bug 机器人使用伺服驱动器来控制三个伺服电机,以便在周围爬行

准备工作

您需要以下硬件:

  • PWM 驱动模块:需要像 Adafruit 16-Channel 12-bit PWM/Servo 驱动器这样的驱动模块。它使用 PCA9685 设备;有关详细信息,请参阅www.adafruit.com/datasheets/PCA9685.pdf 数据表。

  • 3 个微型伺服电机:MG90S 9g 金属齿轮伺服电机在低成本下提供合理的扭矩。

  • 重质线缆:这将形成腿;三个巨大的曲别针(76 毫米/3 英寸)非常适合此用途。

  • 轻质线缆/电缆扎带:这些将用于将腿部连接到伺服电机,并将伺服电机安装到主板上。

  • 一小块胶合板或纤维板:可以在其上钻孔,并将伺服电机安装在其上。

您需要安装 wiringPi2 来控制 PWM 模块,并且安装 I²C 工具进行调试将很有用。有关如何安装 WiringPi2 和 I²C 工具的详细信息,请参阅第七章,感知和显示现实世界数据

准备工作

Raspberry Pi GPIO 引脚头上的 I²C 连接

如何做…

Pi-Bug 机器人使用三个伺服电机,一个在两侧,一个在中间。通过在伺服电机本体两侧钻孔来安装每个伺服电机,通过它穿过一根电线或电缆扎带,并拉紧以牢固地固定伺服电机。

将纸夹线弯曲成合适的形状以形成 Pi-Bug 机器人的腿,并添加一个小弯折,以便您可以将腿安全地连接到伺服臂上。建议您在将伺服臂固定到位之前,先运行程序,并将 Pi-Bug 机器人设置为初始位置 h。这将确保腿位于中间。

以下图示显示了 Pi-Bug 机器人上的组件:

如何操作…

Pi-Bug 机器人上组件的布局

创建以下 servoAdafruit.py 脚本以控制伺服电机:

#!/usr/bin/env python3
#servoAdafruit.py
import wiringpi2
import time

#PWM Registers
MODE1=0x00
PRESCALE=0xFE
LED0_ON_L=0x06
LED0_ON_H=0x07
LED0_OFF_L=0x08
LED0_OFF_H=0x09

PWMHZ=50
PWMADR=0x40

class servo:
  # Constructor
  def __init__(self,pwmFreq=PWMHZ,addr=PWMADR):
    self.i2c = wiringpi2.I2C()
    self.devPWM=self.i2c.setup(addr)
    self.GPIOsetup(pwmFreq,addr)

  def GPIOsetup(self,pwmFreq,addr):
    self.i2c.read(self.devPWM)
    self.pwmInit(pwmFreq)

  def pwmInit(self,pwmFreq):
    prescale = 25000000.0 / 4096.0   # 25MHz / 12-bit
    prescale /= float(pwmFreq)
    prescale = prescale - 0.5 #-1 then +0.5 to round to
                              # nearest value
    prescale = int(prescale)
    self.i2c.writeReg8(self.devPWM,MODE1,0x00) #RESET
    mode=self.i2c.read(self.devPWM)
    self.i2c.writeReg8(self.devPWM,MODE1,
                       (mode & 0x7F)|0x10) #SLEEP
    self.i2c.writeReg8(self.devPWM,PRESCALE,prescale)
    self.i2c.writeReg8(self.devPWM,MODE1,mode) #restore mode
    time.sleep(0.005)
    self.i2c.writeReg8(self.devPWM,MODE1,mode|0x80) #restart

  def setPWM(self,channel, on, off):
    on=int(on)
    off=int(off)
    self.i2c.writeReg8(self.devPWM,
                       LED0_ON_L+4*channel,on & 0xFF)
    self.i2c.writeReg8(self.devPWM,LED0_ON_H+4*channel,on>>8)
    self.i2c.writeReg8(self.devPWM,
                       LED0_OFF_L+4*channel,off & 0xFF)
    self.i2c.writeReg8(self.devPWM,LED0_OFF_H+4*channel,off>>8)

def main():
  servoMin = 205  # Min pulse 1ms 204.8 (50Hz)
  servoMax = 410  # Max pulse 2ms 409.6 (50Hz)
  myServo=servo()
  myServo.setPWM(0,0,servoMin)
  time.sleep(2)
  myServo.setPWM(0,0,servoMax)

if __name__=='__main__':
  try:
    main()
  finally:
    print ("Finish")
#End

创建以下 bug_drive.py 脚本以控制 Pi-Bug 机器人:

#!/usr/bin/env python3
#bug_drive.py
import time
import servoAdafruit as servoCon

servoMin = 205  # Min pulse 1000us 204.8 (50Hz)
servoMax = 410  # Max pulse 2000us 409.6 (50Hz)

servoL=0; servoM=1; servoR=2
TILT=10
MOVE=30
MID=((servoMax-servoMin)/2)+servoMin
CW=MID+MOVE; ACW=MID-MOVE
TR=MID+TILT; TL=MID-TILT
FWD=[TL,ACW,ACW,TR,CW,CW]#[midL,fwd,fwd,midR,bwd,bwd]
BWD=[TR,ACW,ACW,TL,CW,CW]#[midR,fwd,fwd,midL,bwd,bwd]
LEFT=[TR,ACW,CW,TL,CW,ACW]#[midR,fwd,bwd,midL,bwd,fwd]
RIGHT=[TL,ACW,CW,TR,CW,ACW]#[midL,fwd,bwd,midR,bwd,fwd]
HOME=[MID,MID,MID,MID,MID,MID]
PINS=[servoM,servoL,servoR,servoM,servoL,servoR]    
STEP=0.2
global DEBUG
DEBUG=False

class motor:
  # Constructor
  def __init__(self,pins=PINS,steptime=STEP):
    self.pins = pins
    self.steptime=steptime
    self.theServo=servoCon.servo()

  def off(self):
    #Home position
    self.drive(HOME,step)

  def drive(self,drive,step=STEP):
    for idx,servo in enumerate(self.pins):
      if(drive[idx]==servoM):
        time.sleep(step)
      self.theServo.setPWM(servo,0,drive[idx])
      if(drive[idx]==servoM):
        time.sleep(step)
      if(DEBUG):print("%s:%s"%(gpio,drive[idx]))

  def cmd(self,char,step=STEP):
    if char == 'f':
      self.drive(FWD,step)
    elif char == 'b':
      self.drive(BWD,step)
    elif char == 'r':
      self.drive(RIGHT,step)
    elif char == 'l':
      self.drive(LEFT,step)
    elif char == 'h':
      self.drive(HOME,step)
    elif char == '#':
      time.sleep(step)

def main():
  import os
  DEBUG=True
  if "CMD" in os.environ:
    CMD=os.environ["CMD"]
    INPUT=False
    print("CMD="+CMD)
  else:
    INPUT=True
  bugPi=motor()
  if INPUT:
    print("Enter CMDs [f,b,r,l,h,#]:")
    CMD=input()
  for idx,char in enumerate(CMD.lower()):
    if(DEBUG):print("Step %s of %s: %s"%(idx+1,len(CMD),char))
    bugPi.cmd(char)

if __name__ == '__main__':
  try:
    main()
  except KeyboardInterrupt:
    print ("Finish")
#End

工作原理…

我们通过探索如何使用 PWM 控制伺服电机来解释前面的脚本是如何工作的。接下来,我们看看伺服类如何提供控制 PCA9685 设备的方法。最后,我们看看三个伺服电机的运动如何组合起来产生 Pi-Bug 机器人的前进和转向运动。

控制伺服电机

要控制用于 Pi-Bug 机器人的伺服电机,我们需要一个特殊的控制信号,该信号将确定伺服电机需要移动到的角度。我们将向伺服电机发送一个 PWM 信号,其中开启时间的持续时间将允许我们控制伺服臂的角度(从而允许我们控制 Pi-Bug 机器人的腿)。以下图示显示了如何使用 PWM 信号来控制伺服电机的角度:

控制伺服电机

伺服电机的角度由 PWM 信号的 Up Time 的持续时间来控制

大多数伺服电机的角度范围大约为 180 度,中间位置为 90 度。50 Hz 的 PWM 频率将有一个 20 毫秒的周期,90 度的中间位置通常对应于 Up Time 为 1.5 毫秒,对于接近 0 度和接近 180 度的范围为 +/- 0.5 毫秒到 0.4 毫秒。每种类型的伺服电机都会略有不同,但如果需要,您应该能够调整代码以适应。以下图示显示了如何使用不同的 PWM 上时间来控制伺服角度:

控制伺服电机

伺服电机的角度通过发送 1 毫秒到 2 毫秒之间的 PWM 上时间来控制

注意

另一种类型的伺服电机称为 连续伺服电机(此处未使用)。它允许您控制旋转速度而不是角度,并将以恒定速度旋转,这取决于已应用的 PWM 信号。这两种伺服电机都有内部反馈回路,将不断驱动伺服电机,直到达到所需的角速度。

虽然从理论上讲,可以使用软件生成这些信号,但你将发现系统上其他进程的任何微小中断都会干扰信号时序;这反过来又会导致伺服电机产生不规律的反应。这就是为什么我们使用硬件 PWM 控制器,它只需要设置特定的上升和下降时间,然后自动为我们生成所需的信号。

伺服电机类

伺服电机代码基于 Adafruit 为其模块使用的 PWM 驱动器;然而,它并不兼容 Python 3,因此我们需要创建自己的版本。我们将使用 Wiringpi2 的 I²C 驱动器来初始化和控制 I²C PWM 控制器。我们定义我们将需要使用的寄存器(请参阅 PCA9685 设备的数据表)以及其默认总线地址0x40(PWMADR)和 50 Hz(PWMHZ)的 PWM 频率。

在我们的伺服电机类中,我们在wiringpi2中初始化 I²C 驱动器,并在总线上设置我们的devPWM设备。接下来,我们初始化 PWM 设备本身(使用pwmInit())。我们必须计算设备所需的预分频器,以便将板载 25 MHz 时钟转换为 50 Hz 信号以生成所需的 PWM 频率;我们将使用以下公式:

伺服电机类

预分频寄存器值使用 12 位值设置 PWM 频率,以缩放 25 MHz 时钟

预缩放值被加载到设备中,并触发设备重置以启用它。

接下来,我们创建一个函数来允许控制 PWM 的开启和关闭时间。ONOFF时间是 12 位值(0-4096),因此每个值被分成高字节和低字节(每个 8 位),需要加载到两个寄存器中。对于L(低)寄存器,我们使用&0xFF屏蔽掉高 8 位,而对于H(高)寄存器,我们向下移位 8 位以提供高 8 位。每个 PWM 通道将有两个寄存器用于开启时间和两个用于关闭时间,因此我们可以将第一个 PWM 通道寄存器的地址乘以 4 和通道号以获取其他任何通道的地址。

为了测试我们的伺服类,我们定义伺服电机的最小和最大范围,我们计算如下:

  • 50 Hz 的 PWM 频率具有 20 ms 的周期(T=1/f*)

  • 开/关时间范围从 0-4,096(因此 0 ms 到 20 ms)

现在,我们可以计算 0 度(1 ms)和 180 度(2 ms)的控制值如下:

  • 1 ms(伺服最小)等于 4,096/20 ms,即 204.8

  • 2 ms(伺服最大)等于 4,096/10 ms,即 409.6

我们将值四舍五入到最接近的整数。

学习走路

Pi-Bug 机器人使用一种常见的设计,允许使用三个伺服电机来创建一个小型六足机器人。两端的伺服电机提供前进和后退运动,而中间的伺服电机提供控制。以下图像显示了安装的伺服电机:

学习走路

伺服电机被倒装在电路板的底部。

以下表格假设左右伺服器安装在电路板的底部,且中间伺服器垂直安装。如果安装方式不同,您需要调整代码。

以下表格显示了用于向前行走的伺服器运动:

方向 中间 (servoM) 左边 (servoL) 右边 (servoR)
主页 MID/中间 MID/中间 MID/中间
fwdStep1 TR/右侧向上 ACW/腿向前 ACW/腿向后
fwdStep2 TL/左侧向上 CW/腿向后 CW/腿向前

以下图显示了这种运动如何使 Pi-Bug 机器人向前迈步:

学习行走

Pi-Bug 机器人向前移动

虽然一开始可能有些令人困惑,但当你看到机器人移动时,应该会更容易理解。

对于第一个向前步骤,我们顺时针移动中间伺服器 (servoM),这样 Pi-Bug 机器人的左侧就能通过剩余中间腿的运动抬起离地。然后我们可以移动左侧伺服器 (servoL),将左侧的腿向前移动(为后续移动做准备,此时它们没有接触地面)。现在通过移动右侧伺服器 (servoR),我们可以将右侧的腿向后移动(允许 Pi-Bug 机器人在那一侧向前推进)。

第二个向前步骤与第一个相同,只是我们使用中间伺服器 (servoM) 将右侧抬起离地。再次,我们将离地的腿向前移动(为下一次准备),然后将另一侧的腿向后移动(允许 Pi-Bug 机器人的那一侧向前移动)。通过重复向前步骤,Pi-Bug 机器人将向前移动,或者通过交换中间伺服器 (servoM) 抬起的两侧,它将向后移动。结果是类似虫子一样的快速爬行!

要使 Pi-Bug 机器人转向,我们执行类似动作,但就像 Rover-Pi 机器人的高级电机控制一样,我们移动机器人的一侧向前,另一侧向后。以下表格显示了用于向右转的伺服器运动:

方向 中间 (servoM) 左边 (servoL) 右边 (servoR)
主页 MID/中间 MID/中间 MID/中间
rightStep1 TL/左侧向上 CW/腿向后 ACW/腿向后
rightStep2 TR/右侧向上 ACW/腿向前 CW/腿向前

将 Pi-Bug 机器人转向右侧的步骤在以下图中展示:

学习行走

Pi-Bug 机器人向右转

要向右转,我们将 Pi-Bug 机器人的左侧抬起离地,但这次,我们将两边的腿向后移动。这允许 Pi-Bug 机器人的右侧向前移动。步态的第二半将右侧抬起离地,然后我们向前移动腿(这将推动 Pi-Bug 机器人的左侧向后)。以这种方式,虫子会在步态中转向;再次强调,只需交换被抬起的侧面,我们就可以改变 Pi-Bug 机器人将转向的方向。

Pi-Bug 行走代码

Pi-Bug 机器人的代码被设计成提供与 Rover-Pi 机器人相同的接口,以便它们可以轻松互换。你应该注意到每个类都包含相同的四个函数(__init__()off()drive()cmd())。__init__() 函数定义了我们将要控制的引脚集合,行走动作的 steptime 值(这次,动作之间的间隔),以及之前定义的伺服模块。

再次强调,我们有一个 off() 函数,它提供了一个可以调用的函数来将伺服器设置在其中间位置(这在需要将腿定位到位置时非常有用,如之前在 home 位置中所述)。off() 函数使用 drive() 函数将每个伺服器设置到 MID 位置。MID 值位于 servoMinservoMax 之间的一半(1.5 ms 以给出 90 度的位置)。

drive() 函数与之前的电机控制版本类似;它按照我们之前讨论的各种运动模式(FWDBWDLEFTRIGHT)中定义的每个伺服动作依次循环。然而,为了重现所需的运动模式,我们每次移动中间伺服器(servoM)时都会插入一个小的延迟,并且循环每个伺服器两次。这为伺服器移动并提供必要的倾斜,以便在允许它们移动之前将其他腿从地面上抬起。

我们将每个伺服命令定义为伺服臂的顺时针(CW)或逆时针/逆时针(ACW)移动。由于伺服器是倒置安装的,因此左伺服器(servoL)的逆时针(从上方看为顺时针)移动将使腿向前移动,而右伺服器(servoR)的相同移动方向将使腿向后移动(这在之前的图中表示为 fwdStep1)。这样,每种模式都可以被定义。

再次提供测试函数,使用以下命令允许从命令行或直接在提示符中定义指令列表:

sudo CMD=fffll##rr##bb##h python3 bug_drive.py

这包括添加 h 以返回 home 位置,如果需要的话。

使用 Servoblaster 直接控制伺服器

之前的配方演示了使用专用伺服控制器来处理 PiBug 使用的伺服电机的控制。这有一个优点,即 Raspberry Pi 上正在进行的处理中的任何干扰都不会干扰微妙的伺服控制(因为控制器将继续发送正确的信号)。

然而,Raspberry Pi 也具备直接控制伺服电机的功能。为了实现这一点,我们将使用 Richard Hurst 的 Servoblaster,这是一个多伺服驱动器。

在这个配方中,我们将控制连接到 MeArm 的四个伺服电机,这是一个简单的激光切割机械臂;然而,你可以选择将伺服电机安装到你喜欢的任何设备上。

使用 Servoblaster 直接控制伺服电机

MeArm 是一个由四个微型伺服电机驱动的简单机械臂

准备工作

大多数伺服电机将有三条线和三个引脚的连接器,如下所示:

黑色/棕色 红色 橙色/白色/黄色/蓝色
地线 正电源(通常为小型伺服电机的 5V) 信号

虽然通常可以从 Raspberry Pi 5V 引脚直接为伺服电机供电,但在移动时它们会消耗大量的电流。除非你有非常好的电源,否则这可能会导致 Raspberry Pi 意外重置,从而风险损坏 SD 卡。因此,建议您单独为它们供电,例如,使用额外的 USB 电源和连接到地线和正电源的电缆。

默认情况下,伺服电机可以按照以下方式连接:

伺服电机 0 1 2 3 4 5 6 7 所有 GND 所有电源
Raspberry Pi GPIO 引脚
7 11 12 13 15 16 19 22 6 无连接
5V 电源 GND +5V

我们将假设我们正在控制四个伺服电机(0、1、2 和 3),这些伺服电机将稍后安装到 MeArm 或类似设备上。

准备工作

要安装 Servoblaster,首先从 Git 仓库下载源文件:

cd ~
wget https://github.com/richardghirst/PiBits/archive/master.zip

解压并打开matplotlib-master文件夹,如下所示:

unzip master.zip
rm master.zip
cd PiBits-master/ServoBlaster/user

我们将使用用户空间守护进程(位于用户目录中),称为servod。在我们能够使用它之前,我们应该使用此命令编译它:

make servod

应该没有错误,显示以下文本:

gcc -Wall -g -O2 -o servod servod.c mailbox.c -lm

使用以下命令获取使用信息:

./servod --help

现在我们可以测试一个伺服电机,首先启动servod守护进程(设置超时为 2,000ms,在伺服电机移动后关闭):

sudo servod --idle-timeout=2000

你可以将伺服电机的位置移动到伺服电机范围的 0%:

echo 0=0% > /dev/servoblaster

现在将伺服电机更新到 50%,使伺服电机旋转到 90 度(伺服中点):

echo 0=50% > /dev/servoblaster

如 MeArm 构建说明中所建议,伺服电机应在构建机械臂之前连接和校准,以确保每个伺服电机都能在其正确范围内移动机械臂。这是通过确保每个伺服电机都通电并命令其中点位置(50%/90 度)以及伺服电机臂在预期的方向上安装来完成的:

准备工作

在将伺服电机安装到 MeArm 之前,每个伺服电机都应该校准到正确的位置

现在,您可以在构建和安装到完成的手臂之前,将每个 MeArm 伺服电机(0、1、2 和 3)设置到中间位置(通过依次将每个命令到 50%)。

这些伺服电机可以用来控制除 MeArm 之外的各种替代设备,但您的伺服电机可能需要以类似的方式进行校准。

准备中

伺服电机的精度控制意味着它们可以用于各种应用。例如,控制模拟手

如何做…

创建以下servo_control.py脚本:

#!/usr/bin/env python3
#servo_control.py
import curses
import os
#HARDWARE SETUP
# GPIO	
# 2[=VX==2=======]26[=======]40
# 1[===013=======]25[=======]39
# V=5V X=Gnd
# Servo 0=Turn 1=Shoulder 2=Elbow 3=Claw
name=["Turn","Shoulder","Elbow","Claw"]
CAL=[90,90,90,90]
MIN=[0,60,40,60]; MAX=[180,165,180,180]
POS=list(CAL)
KEY_CMD=[ord('c'),ord('x')]
#Keys to rotate counter-clockwise
KEY_LESS={ord('d'):0,ord('s'):1,ord('j'):2,ord('k'):3}
#Keys to rotate clockwise
KEY_MORE={ord('a'):0,ord('w'):1,ord('l'):2,ord('i'):3}

STEP=5; LESS=-STEP; MORE=STEP #Define control steps
DEG2MS=1.5/180.0; OFFSET=1 #mseconds
IDLE=2000 #Timeout servo after command
SERVOD="/home/pi/PiBits-mater/ServoBlaster/user/servod" #Location of servod
DEBUG=True
text="Use a-d, w-s, j-l and i-k to control the MeArm. c=Cal x=eXit"

def initialize():
  cmd=("sudo %s --idle-timeout=%s"%(SERVOD, IDLE))
  os.system(cmd)

def limitServo(servo,value):
  global text
  if value > MAX[servo]:
    text=("Max %s position %s:%s"%(name[servo],servo,POS[servo]))
    return MAX[servo]
  elif value < MIN[servo]:
    text=("Min %s position %s:%s"%(name[servo],servo,POS[servo]))
    return MIN[servo]
  else:
    return value

def updateServo(servo,change):
  global text
  POS[servo]=limitServo(servo,POS[servo]+change)
  setServo(servo,POS[servo])
  text=str(POS)

def setServo(servo,position):
  ms=OFFSET+(position*DEG2MS)
  os.system("echo %d=%dus > /dev/servoblaster" %(servo, ms/1000))

def calibrate():
  global text
  text="Calibrate 90deg"
  for i,value in enumerate(CAL):
    POS[i]=value
    setServo(i,value)

def main(term):
  term.nodelay(1)
  term.addstr(text)
  term.refresh()
  while True:
    term.move(1,0)
    c = term.getch()
    if c != -1:
      if c in KEY_MORE:
        updateServo(KEY_MORE[c],MORE)
      elif c in KEY_LESS:
        updateServo(KEY_LESS[c],LESS)
      elif c in KEY_CMD:
        if c == ord('c'):
          calibrate()
        elif c == ord('x'):
          exit()
      if DEBUG:term.addstr(text+"   ")

if __name__=='__main__':
  initialize()
  curses.wrapper(main)
#End

运行脚本:

python3 servo_control.py

按提示控制安装到 MeArm(或您正在使用的任何设备)的伺服电机:

Use a-d, w-s, j-l and i-k to control the MeArm. c=Cal x=eXit

它是如何工作的…

脚本首先导入cursesos模块。标准的 Python input()命令需要在每次按键后按下Enter键,我们才能对其做出反应。然而,正如我们很快就会看到的,curses模块仅仅允许我们扫描键盘按键并立即对其做出响应。我们使用os模块来调用 servoblaster 命令,就像我们在终端中做的那样。

首先,我们定义我们的设置,例如伺服映射、校准位置、最小/最大范围、我们的控制键以及每个控制命令的步进大小(以度为单位)。我们还定义了我们的请求角度参数(以度为单位)和目标 PWM 信号持续时间(以毫秒为单位)的计算。

注意

注意:对于这些特定的伺服电机,1 毫秒的持续时间等于 0 度,2.5 毫秒等于 180 度,因此我们有一个 1 毫秒的偏移量(OFFSET)和 180 度/1.5 毫秒的缩放比例(DEG2MS)。

因此,我们所需的持续时间(以毫秒为单位)可以计算为OFFSET + (degrees*DEG2MS)。最后,我们定义 SERVOD 命令行和伺服IDLE超时以初始化 servoblaster 用户守护进程。在initialize()中,我们使用os.system()启动 servod 守护进程,就像我们之前做的那样。

为了检测按键,我们从curses.wrapper()中调用脚本的main()函数,允许 term 控制终端的输入和输出。我们使用term.nodelay(1),这样当我们检查任何按键(使用term.getch())时,执行将继续正常。我们使用term.addstr(text)来向用户显示控制键,然后通过term.refresh()更新显示。剩余的脚本检查终端是否有按键,并将结果分配给c。如果没有按键被按下,那么term.getch()返回-1,否则返回 ASCII 等效值,我们可以在我们定义的每个控制键的字典中检查它。我们将使用KEY_MOREKEY_LESS来改变伺服电机的位置,使用KEY_CMDcx)来允许我们将所有伺服电机设置到校准位置或干净地退出。最后,我们使用term.addstr()显示任何有用的调试信息(如果DEBUG设置为True),并确保它在终端的(1,0)位置显示(从顶部向下一行)。

对于常规控制,伺服电机的位置将通过updateServo()函数进行控制,该函数通过所需的改变(无论是+STEP还是–STEP)调整当前位置(存储在 POS 数组中)。我们确保新位置在定义的 MAX/MIN 限制内,并在达到这些限制时报告。然后,伺服电机被指令使用setServo()移动到所需位置,并指定所需的 PWM 上时间(以微秒为单位)。

最后一个函数calibrate(),在按下 c 键时调用,简单地将每个伺服电机设置为CAL数组中定义的角度(使用setServo()),并确保当前位置保持最新。

使用红外遥控器与 Raspberry Pi 配合使用

远程控制机器人通常很有用。添加额外输入的一个简单方法是使用红外(红外)接收器和标准遥控器。幸运的是,接收器得到了很好的支持。

我们将使用一个名为 LIRC 的模块来捕获和解析来自标准遥控器的红外信号。

准备就绪

LIRC 支持许多类型的红外探测器,例如 Energenie 的 PiMote 红外板;然而,由于我们只需要接收红外信号,我们可以使用一个简单的(TSOP38238)红外探测器。

准备就绪

TSOP38238 红外接收器的三个引脚可以直接连接到 Raspberry Pi 的引脚头上

使用apt-get安装以下软件包:

sudo apt-get install lirc lirc-x

将以下内容添加到/boot/config.txt。这将启用驱动程序并定义接收器安装的引脚(BCM GPIO24):

dtoverlay=lirc-rpi,gpio_in_pin=23

重新启动 Raspberry Pi,以便配置生效:

sudo reboot

我们现在应该会发现红外设备位于/dev/lirc0。如果我们用遥控器指向它并按下一些按钮,我们可以观察接收器的输出(使用以下命令,使用Ctrl + Z退出):

mode2 –d /dev/lirco0

注意

注意:如果lirc0资源报告为忙碌:

 mode2: could not open /dev/lirc0
 mode2: default_init(): Device or resource busy

我们需要停止lirc服务:

sudo /etc/init.d/lirc stop

这将给出以下响应:

[ ok ] Stopping lirc (via systemctl): lirc.service

当你准备好后,你可以再次启动服务:

sudo /etc/init.d/lirc start

这将给出以下响应:

[ ok ] Starting lirc (via systemctl): lirc.service

你将看到类似以下的内容(如果没有,请确保接收器已连接到 Raspberry Pi GPIO 的正确引脚上):

space 16300
pulse 95
space 28794
pulse 80
space 19395
pulse 83
...etc…

现在我们知道我们的设备正在工作,我们可以配置它。

如何做到这一点…

全局 LIRC 配置存储在/etc/lirc。我们感兴趣的文件如下:

hardware.conf 定义我们的红外传感器安装位置以及传感器的整体设置。
lircd.conf 遥控器配置文件;此文件包含您遥控器按键的记录输出,并将它们映射到特定的按键符号。您通常可以从lirc.sourceforge.net/remotes获取预录制的文件,或者我们可以像下面那样录制一个自定义的文件。
lircrc 此文件为每个按键符号提供映射到特定命令或键盘映射的映射。

注意

注意:存储在 /etc/lirc 中的所有 LIRC 配置对所有用户都可用;然而,如果需要,可以在特定的家目录中为每个用户定义不同的配置,例如 /home/pi/.config/,允许覆盖默认设置。

设置传感器有三个步骤,每个步骤对应一个 LIRC 配置文件:

  1. 首先,确保 hardware.conf 已设置。对于我们的传感器,我们必须确保以下设置正确:

        LIRCD_ARGS="--uinput"
        DRIVER="default"
        DEVICE="/dev/lirc0"
        MODULES="lirc_rpi"
    
  2. 接下来,获取一个 lircd.conf 文件,或者如果您没有为您的遥控器获取一个,我们可以生成它。以下过程将带您检测遥控器上的每个单独的键。对于本配方,我们只需要映射八个键(以控制前面配方中的四个伺服电机)。

  3. 如果你想映射额外的键,请使用以下命令来获取有效键符号的完整列表:

    irrecord --list-namespace
    
    
    KEY_UP KEY_RIGHT KEY_VOLUMEUP KEY_CHANNELUP
    KEY_DOWN KEY_LEFT KEY_VOLUMEDOWN KEY_CHANNELDOWN

    如何操作…

    我们可以使用这个 Goodmans 遥控器上的音量、频道和方向按钮作为我们的 MeArm 控制器

首先,我们需要停止 lirc 服务,如果它在运行,则会使用 /dev/lirc0 设备:

sudo /etc/init.d/lirc stop

接下来,使用以下命令启动捕获过程:

irrecord –d /dev/lirc0 ~/lircd.conf

如何操作…

使用 irrecord 工具记录遥控器上的每个按钮

现在我们已经捕获了所需的键,我们确保遥控器的名称已设置(默认情况下,当捕获按钮时,它将设置为 lirc.conf 文件的名称):

nano ~/lircd.conf

在文件中设置遥控器的名称,例如,Goodmans:

...
begin remote
 name  Goodmans
 bits           16 
...

最后,我们可以替换 /etc/lirc 文件夹中的配置:

sudo cp ~/lircd.conf /etc/lirc/lirc.conf

注意

注意:我们可以使用 irw 程序来确认映射到遥控器的键符号,如下所示:

irw

这将报告按下的键的详细信息以及遥控器的定义:

0000000000fe7a85 00 KEY_UP Goodmans
0000000000fe7a85 01 KEY_UP Goodmans
0000000000fe6a95 00 KEY_DOWN Goodmans
0000000000fe6a95 01 KEY_DOWN Goodmans
...

现在我们可以将键映射到特定的命令;在这种情况下,我们将它们映射到我们用于控制 MeArm 伺服电机的键。创建一个新的 /etc/lirc/lircrc 文件:

sudo nano /etc/lirc/lircrc

替换为以下内容:

begin
  prog=irxevent
  button=KEY_UP
  config=Key w CurrentWindow
end
begin
  prog=irxevent
  button=KEY_DOWN
  config=Key s CurrentWindow
end
begin
  prog=irxevent
  button=KEY_LEFT
  config=Key a CurrentWindow
end
begin
  prog=irxevent
  button=KEY_RIGHT
  config=Key d CurrentWindow
end
begin
  prog=irxevent
  button=KEY_VOLUMEUP
  config=Key i CurrentWindow
end
begin
  prog=irxevent
  button=KEY_VOLUMEDOWN
  config=Key k CurrentWindow
end
begin
  prog=irxevent
  button=KEY_CHANNELUP
  config=Key l CurrentWindow
end
begin
  prog=irxevent
  button=KEY_CHANNELDOWN
  config=Key j CurrentWindow
end

要应用配置,可能需要重新启动服务(如果不起作用,尝试重新启动树莓派):

sudo /etc/init.d/lirc restart

当我们在前面的配方中运行 servo_control.py 脚本时,遥控器应直接控制手臂。

还有更多…

LIRC 支持几个辅助程序,其中 irxevent 只是其中之一:

| remote | 默认情况下,LIRC 支持一些简单的控制,例如:

  prog=remote
  button=KEY_UP
  config=UP

这将为遥控器提供简单的光标控制(上、下、左、右以及回车键),非常适合简单的菜单控制。|

www.lirc.org/html/configure.html#lircrc_format
irxevent
www.lirc.org/html/irxevent.html

| irpty | 将红外遥控命令转换为特定程序的键盘输入:

  rog=irpty
  button=KEY_EXIT
  config=x

通过指定要控制的lircrc配置和程序来启动它:

irpty /etc/lirc/lircrc -- leafpad

|

www.lirc.org/html/irpty.html

| irexec | 允许直接从遥控器运行命令:

 prog=irexec
 button=KEY_POWER
 config=sudo halt #Power Down

|

www.lirc.org/html/irexec.html

你可以使用ircat和所需的prog来测试lircrc文件中的任何部分:

ircat irxevent

上述命令将报告以下内容:

Key k CurrentWindow
Key i CurrentWindow

最后,如果你连接了合适的红外发射器 LED(包括保护电阻/开关晶体管),你也可以使用 LIRC 从树莓派发送红外信号。为此,你可以使用irsend命令,例如:

irsend SEND_ONCE Goodmans KEY_PROGRAMUP

红外输出通道在/boot/config.txt文件中启用(假设连接到 GPIO 引脚 19):

dtoverlay=lirc-rpi,gpio_in_pin=24,gpio_out_pin=19

避免物体和障碍物

为了避免障碍物,你可以在机器人的周围放置传感器,以便在遇到物体时激活。根据你希望机器人如何行动,一种避免策略是反转导致任何传感器激活的最后一个动作(对于前进/后退动作,还需要额外的转向)。

准备就绪

当物体碰撞时,你需要一些微动开关被触发。根据你拥有的类型,你需要放置足够的开关来检测周围任何物体(如果需要,你可以使用额外的电线长度来扩展开关的感应范围)。以下图像显示了两种可能的传感器,当弹簧或金属臂撞击物体时,这些传感器将使开关激活。你需要确定哪个开关的触点打开或关闭电路(这取决于设备)。

准备就绪

小型微动开关可以用作碰撞传感器

如何操作...

使用与我们在第六章使用 Python 驱动硬件中使用的类似方法将开关连接到 GPIO。以下是开关的电路图:

如何操作…

开关应包括限流电阻(1K 欧姆是理想的)

你如何连接到树莓派的 GPIO 将取决于你的电机/伺服驱动器如何布线。例如,带有 H 桥电机控制器的 Rover-Pi 机器人可以布线如下:

模块的控制侧 – 连接到树莓派的 GPIO 引脚
ENA
---
None

可以将四个额外的接近/碰撞传感器连接到 Raspberry Pi 的 GPIO,如下所示:

接近/碰撞传感器 – 连接到 Raspberry Pi GPIO 引脚头
R_FWD
---
引脚 7

如果你以不同的方式布线,你可以在代码中根据需要调整引脚号。如果你需要额外的引脚,则可以使用任何多功能引脚,例如 RS232 RX/TX(引脚 8 和 10)或 SPI/I²C,也可以用作正常的 GPIO 引脚;只需将它们设置为输入或输出即可。通常,我们只是避免使用它们,因为它们通常对扩展和其他事情更有用,所以有时保留它们可用是有用的。

如果你只是使用以下示例代码,甚至可以使用单个 GPIO 引脚来控制所有传感器,因为动作是相同的,无论哪个传感器被触发。然而,通过单独布线每个传感器,你可以根据机器人周围障碍物的位置调整你的策略,或者提供有关哪个传感器被触发的额外调试信息。

创建以下avoidance.py脚本:

#!/usr/bin/env python3
#avoidance.py
import rover_drive as drive
import wiringpi2
import time

opCmds={'f':'bl','b':'fr','r':'ll','l':'rr','#':'#'}
PINS=[7,11,12,13]   # PINS=[L_FWD,L_BWD,R_FWD,R_BWD]
ON=1;OFF=0
IN=0;OUT=1
PULL_UP=2;PULL_DOWN=1

class sensor:
  # Constructor
  def __init__(self,pins=PINS):
    self.pins = pins
    self.GPIOsetup()

  def GPIOsetup(self):
    wiringpi2.wiringPiSetupPhys()
    for gpio in self.pins:
      wiringpi2.pinMode(gpio,IN)
      wiringpi2.pullUpDnControl(gpio,PULL_UP)    

  def checkSensor(self):
    hit = False
    for gpio in self.pins:
      if wiringpi2.digitalRead(gpio)==False:
        hit = True
    return hit

def main():
  myBot=drive.motor()
  mySensors=sensor()
  while(True):
    print("Enter CMDs [f,b,r,l,#]:")
    CMD=input()
    for idx,char in enumerate(CMD.lower()):
      print("Step %s of %s: %s"%(idx+1,len(CMD),char))
      myBot.cmd(char,step=0.01)#small steps
      hit = mySensors.checkSensor()
      if hit:
        print("We hit something on move: %s Go: %s"%(char,
                                              opCmds[char]))
        for charcmd in opCmds[char]:
          myBot.cmd(charcmd,step=0.02)#larger step

if __name__ == '__main__':
  try:
    main()
  except KeyboardInterrupt:
    print ("Finish")
#End

它是如何工作的...

我们导入rover_drive来控制机器人(如果我们使用 Pi-Bug 机器人,可以使用bug_drive)和wiringpi2,这样我们就可以使用 GPIO 来读取传感器(定义为PINS)。我们定义opCmds,它使用 Python 字典来根据原始命令分配新的命令(使用opCmds[char],其中char是原始命令)。

我们创建一个新的类sensor,并将每个开关设置为 GPIO 输入(每个都设置了内部上拉)。现在,每当我们要进行移动(如前所述,从main()函数中的请求命令列表),我们会检查是否有任何开关被触发(通过调用mySensor.checkSensor())。

如果一个开关被触发,我们将停止当前的运动,然后向相反方向移动。然而,如果我们正在前进时其中一个传感器被触发,我们将后退然后转向。这允许机器人逐渐从阻挡其路径的物体旁边转向,并继续向另一个方向移动。同样,如果我们正在后退并且传感器被触发,我们将向前移动然后转向。通过结合简单的物体避障和方向信息,机器人可以被命令绕过所需的路径。

还有更多...

也有方法检测机器人附近的对象,而无需与它进行实际的身体接触。一种方法是使用超声波传感器,通常用于车辆倒车/停车传感器。

超声波倒车传感器

超声波传感器提供了一种测量机器人与障碍物之间距离的极好方法(提供 2 厘米到 20 厘米的测量范围),并且大多数电子爱好商店都有售(见附录,“硬件和软件列表”)。超声波模块通过发送一短串超声波脉冲,然后测量接收器检测回波所需的时间来工作。模块随后在回波输出上产生一个脉冲,该脉冲等于测量的时间。这个时间等于行进距离除以声速(340.29 米/秒或 34,029 厘米/秒),即从传感器到物体再返回的距离。以下图片展示了超声波模块:

超声波倒车传感器

HC-SR04 超声波传感器模块

传感器需要 5V 来供电;它有一个接收触发脉冲的输入和一个发送回波脉冲的输出。虽然该模块可以使用 3.3V 的触发脉冲工作,但它会在回波线上以 5V 的信号响应;因此,需要一些额外的电阻来保护树莓派的 GPIO。

以下电路图显示了传感器输出的连接:

超声波倒车传感器

传感器回波输出必须通过分压器连接到树莓派

电阻RtRb构成了一个分压器;目的是将回波电压从 5V 降至约 3V(但不能低于 2.5V)。使用第七章中的以下方程式,“感知和显示现实世界数据”,来获取输出电压:

超声波倒车传感器

使用此方程式计算分压器的输出电压(Vout)

这意味着我们应该将RtRb的比例设置为 2:3 以获得 3V(并且不能低于 1:1,这将给出 2.5V);即Rt等于 2K 欧姆,Rb等于 3K 欧姆,或者 330 欧姆和 470 欧姆也可以。

如果你有电压表,你可以检查它(其他所有东西都断开连接)。将分压器的顶部连接到 GPIO 引脚 2(5V),底部连接到 GPIO 引脚 6(GND),并测量中间的电压(应该是约 3V)。

创建以下sonic.py脚本:

#!/usr/bin/python3
#sonic.py
import wiringpi2
import time
import datetime

ON=1;OFF=0; IN=0;OUT=1
TRIGGER=15; ECHO=7
PULSE=0.00001 #10us pulse

SPEEDOFSOUND=34029 #34029 cm/s

def gpiosetup():
  wiringpi2.wiringPiSetupPhys()
  wiringpi2.pinMode(TRIGGER,OUT)
  wiringpi2.pinMode(ECHO,IN)
  wiringpi2.digitalWrite(TRIGGER,OFF)
  time.sleep(0.5)

def pulse():
  wiringpi2.digitalWrite(TRIGGER,ON)
  time.sleep(PULSE)
  wiringpi2.digitalWrite(TRIGGER,OFF)
  starttime=time.time()
  stop=starttime
  start=starttime
  while wiringpi2.digitalRead(ECHO)==0 and start<starttime+2:
    start=time.time()
  while wiringpi2.digitalRead(ECHO)==1 and stop<starttime+2:
    stop=time.time()
  delta=stop-start
  print("Start:%f Stop:%f Delta:%f"%(start,stop,delta))
  distance=delta*SPEEDOFSOUND
  return distance/2.0

def main():
  global run
  gpiosetup()
  while(True):
    print("Sample")
    print("Distance:%.1f"%pulse())
    time.sleep(2)

if __name__ == '__main__':
  try:
    main()
  except KeyboardInterrupt:
    print ("Finish")
#End

首先,我们定义TRIGGERECHO引脚,触发脉冲的长度,以及声速(340.29 m/s)。将TRIGGER引脚设置为输出,将ECHO设置为输入(由于模块已经有一个上拉或下拉电阻,我们不需要额外的电阻)。

pulse()函数会发送一个短暂的触发脉冲(10 微秒);然后它会测量回波脉冲的持续时间。我们然后通过将持续时间除以声速(到物体的距离只是这个值的一半)来计算总行进距离。

不幸的是,该传感器可能会与某些类型的物体混淆;它可能会检测到在反射回之前从附近物体反弹的回声,或者无法拾取像椅子腿这样的细长物品。然而,结合局部碰撞传感器,超声波传感器可以帮助进行一般导航和避免大型物体。

对此设置的一种改进是在伺服电机上安装超声波传感器,从而允许您对机器人的周围环境进行传感器扫描。通过进行多次扫描,进行距离测量,并跟踪伺服电机的角度,您可以构建机器人周围环境的内部地图。

获取方向感

为了在环境中导航您的机器人,您需要跟踪机器人面向的方向。您可以通过测量在固定时间段内机器人转过的角度来估计机器人转动的角度。对于轮式机器人,您还可以使用旋转编码器(提供车轮旋转计数的设备)来测量每个轮子的旋转。然而,随着机器人进行多次转弯,由于表面和车轮或腿的抓地力差异,机器人转向的角度差异越来越大,导致机器人面向的方向变得越来越不确定。

幸运的是,我们可以使用电子罗盘;它通过提供一个从磁北方向的角度来确定机器人面向的方向。如果我们知道机器人面向哪个方向,我们可以接收请求特定角度的命令,并确保机器人朝该方向移动。这允许机器人进行受控运动并按需导航。

当给出目标角度时,我们可以确定需要转向哪个方向,直到达到它。

准备就绪

您需要一个磁力计设备,如 PiBorg 的XLoBorg模块(这是一个组合的 I²C 磁力计和加速度计)。在这个例子中,我们只关注磁力计(左侧较小的芯片)的输出。XLoBorg 模块的外观如下所示:

准备就绪

PiBorg XLoBorg 模块包含一个三轴磁力计和加速度计

此设备可以与两种类型的机器人一起使用,从模块接收到的角度信息可以用来确定机器人需要移动的方向。

该模块设计为直接连接到 GPIO 引脚头,这将阻塞所有剩余的引脚。因此,为了使用其他 GPIO 设备,可以使用 GPIO 分线器(例如 PiBorg TriBorg)。或者,您可以使用杜邦公对母排线仅连接 I²C 引脚。以下图中显示了需要建立的连接:

准备就绪

将 XLoBorg 模块手动焊接至 Raspberry Pi(使用标准 I²C 连接)

从底部看,PiBorg XLoBorg 引脚与 Raspberry Pi GPIO 引脚相反。

如何做到这一点…

按照以下方式使用wiringpi2创建 XLoBorg 库的 Python 3 友好版本(XLoBorg3.py):

#!/usr/bin/env python3
#XLoBorg3.py
import wiringpi2
import struct
import time

def readBlockData(bus,device,register,words):
  magData=[]
  for i in range(words):
    magData.append(bus.readReg16(device,register+i))
  return magData

class compass:
  def __init__(self):
    addr = 0x0E #compass
    self.i2c = wiringpi2.I2C()
    self.devMAG=self.i2c.setup(addr)
    self.initCompass()

  def initCompass(self):
    # Acquisition mode
    register = 0x11   # CTRL_REG2
    data  = (1 << 7)  # Reset before each acquisition
    data |= (1 << 5)  # Raw mode, do not apply user offsets
    data |= (0 << 5)  # Disable reset cycle
    self.i2c.writeReg8(self.devMAG,register,data)
    # System operation
    register = 0x10   # CTRL_REG1
    data  = (0 << 5)  # Output data rate
                      # (10 Hz when paired with 128 oversample)
    data |= (3 << 3)  # Oversample of 128
    data |= (0 << 2)  # Disable fast read
    data |= (0 << 1)  # Continuous measurement
    data |= (1 << 0)  # Active mode
    self.i2c.writeReg8(self.devMAG,register,data)

  def readCompassRaw(self):
    #x, y, z = readCompassRaw()
    self.i2c.write(self.devMAG,0x00)
    [status, xh, xl, yh, yl,
      zh, zl, who, sm, oxh, oxl,
      oyh, oyl, ozh, ozl,
      temp, c1, c2] = readBlockData(self.i2c,self.devMAG, 0, 18)
    # Convert from unsigned to correctly signed values
    bytes = struct.pack('BBBBBB', xl, xh, yl, yh, zl, zh)
    x, y, z = struct.unpack('hhh', bytes)
    return x, y, z

if __name__ == '__main__':
  myCompass=compass()
  try:
    while True:
      # Read the MAG Data
      mx, my, mz = myCompass.readCompassRaw()
      print ("mX = %+06d, mY = %+06d, mZ = %+06d" % (mx, my, mz))
      time.sleep(0.1)
  except KeyboardInterrupt:
    print("Finished")
#End

它是如何工作的…

该脚本基于适用于 XLoBorg 模块的 XLoBorg 库,除了我们使用 Python 3 友好的 WiringPi2 来执行 I²C 操作。就像我们的电机/伺服驱动器一样,我们也将其定义为类,这样我们就可以将其放入我们的代码中,并在需要时轻松地用其他设备替换它。

我们导入wiringpi2time以及一个名为struct的库(该库允许我们将从设备读取的数据块快速解包成单独的项目)。

我们创建了一个compass类,它将包括__init__()initCompass()readCompassRaw()函数。readCompassRaw()函数等同于他们库提供的标准 XLoBorg ReadCompassRaw()函数。

__init__()函数使用wiringpi2设置 I²C 总线,并在总线地址0x0E上注册degMAG设备。initCompass()函数使用设备所需的设置设置设备的CTRL_REG1CTRL_REG2寄存器,以便快速从设备获取原始读数。

更多关于 MAG3110 寄存器的详细信息可在www.freescale.com/files/sensors/doc/data_sheet/MAG3110.pdf找到。

readCompassRaw()函数以单个块的形式读取设备的寄存器数据(使用自定义函数readBlockData())。它读取设备的所有 18 个寄存器(从0x000x11)。我们需要读取的传感器读数包含在寄存器0x010x06中,这些寄存器包含xyz读数,分为高字节和低字节(8 位值)。struct.pack()struct.unpack()函数提供了一种简单的方法来打包它们,并将它们重新拆分为单独的字(16 位值)。

我们可以通过从compass类创建一个myCompass对象并使用myCompass.readCompassRaw()读取传感器值来测试我们的脚本。你将看到来自设备的原始 x、y 和 z 值,就像从标准的 XLoBorg 库中看到的那样。

你会发现,这些值本身并没有太大用处,因为它们未经校准,只能提供磁力计的原始读数。我们需要的是一个相对于磁北的更有用的角度(有关如何做到这一点的详细信息,请参阅以下更多内容…部分)。

更多内容…

到目前为止,基本库允许我们查看围绕传感器三个轴上的磁场强度(上下、左右和前后)。虽然我们可以看到这些值会随着我们移动传感器而改变,但这还不足以控制我们的机器人。首先,我们需要校准传感器,然后根据xy轴的读数确定机器人的方向。

校准指南针

指南针需要校准才能报告中心化和均衡的值。这是必要的,因为周围到处都有磁场,通过校准传感器,我们可以消除任何局部场的影响。

通过测量指南针在所有轴上的读数,我们可以确定每个轴的最小值和最大值。这将使我们能够计算读数的中间值以及缩放,以便每个轴在面向同一方向时都会读取相同的值。

在文件的顶部添加以下代码(在import语句之后):

CAL=100 #take CAL samples

将以下代码添加到compass类的__init__(self)中:

    self.offset,self.scaling=self.calibrateCompass()
    if DEBUG:print("offset:%s scaling:%s"%(str(self.offset),
                                           str(self.scaling)))

compass类中添加一个名为calibrateCompass()的新函数,如下所示:

  def calibrateCompass(self,samples=CAL):
    MAXS16=32768
    SCALE=1000.0
    avg=[0,0,0]
    min=[MAXS16,MAXS16,MAXS16];max=[-MAXS16,-MAXS16,-MAXS16]
    print("Rotate sensor around axis (start in 5 sec)")
    time.sleep(5)
    for calibrate in range(samples):
      for idx,value in enumerate(self.readCompassRaw()):
        avg[idx]+=value
        avg[idx]/=2
        if(value>max[idx]):
          max[idx]=value
        if(value<min[idx]):
          min[idx]=value
      time.sleep(0.1)
      if DEBUG:print("#%d min=[%+06d,%+06d,%+06d]"
                     %(calibrate,min[0],min[1],min[2])
                     +" avg[%+06d,%+06d,%+06d]"
                     %(avg[0],avg[1],avg[2])
                     +" max=[%+06d,%+06d,%+06d]"
                     %(max[0],max[1],max[2]))
    offset=[]
    scaling=[]
    for idx, value in enumerate(min):
      magRange=max[idx]-min[idx]
      offset.append((magRange/2)+min[idx])
      scaling.append(SCALE/magRange)
    return offset,scaling

compass类中添加另一个名为readCompass()的新函数,如下所示:

  def readCompass(self):
    raw = self.readCompassRaw()
    if DEBUG:print("mX = %+06d, mY = %+06d, mZ = %+06d"
                   % (raw[0],raw[1],raw[2]))
    read=[]
    for idx,value in enumerate(raw):
      adj=value-self.offset[idx]
      read.append(adj*self.scaling[idx])
    return read

如果你仔细观察读数(如果你使用readCompass()),你现在会发现所有的读数都具有相同的范围,并且围绕相同的值中心化。

计算指南针方位

XLoBorg 库仅提供对 MAG3110 设备原始值的访问,这些值可以衡量每个轴上磁场的强度。为了确定传感器的方向,我们可以使用x轴和y轴的读数(假设我们已经水平安装并校准了传感器)。x轴和y轴的读数与传感器周围每个方向的磁场成正比,如下面的图所示:

计算指南针方位

磁力计测量每个轴上磁场的强度

我们偏离北方的角度可以通过以下图中的公式计算:

计算指南针方位

我们指向相对于磁北的角度可以通过测量 Rx 和 Ry 来计算

我们现在可以通过向compass类添加以下readCompassAngle()函数来获取compass角度,如下所示:

  def readCompassAngle(self,cal=True):
    if cal==True:
      read = self.readCompass()
    else:
      read = self.readCompassRaw()
    angle = math.atan2 (read[1],read[0]) # cal angle in radians
    if (angle < 0):
      angle += (2 * math.pi) # ensure positive
    angle = (angle * 360)/(2*math.pi); #report in degrees
    return angle

我们还需要在其他import语句中添加以下导入:

import math

我们使用数学函数math.atan2()来计算我们的角度(atan2将返回相对于坐标read[1]read[2]x轴的角度——我们想要的角度)。角度以弧度为单位,这意味着一整圈定义为 2Pi 而不是 360 度。我们通过将其乘以 360 并除以 2Pi 将其转换为度。由于我们希望角度在 0 到 360 度的范围内(而不是-180 到 180 度),我们必须确保它是正的,通过向任何负值添加相当于一整圈(2Pi)的值。

传感器校准和角度计算完成后,我们现在应该有适当的指南针方位可以在我们的机器人上使用。为了比较,你可以通过调用函数readCompassAngle(cal=False)来查看使用未校准值计算的结果。

保存校准

在当前位置校准传感器一次后,每次运行机器人时都需要校准它将是不方便的。因此,你可以将以下代码添加到你的库中,以自动保存你的校准并在下次运行你的机器人时从文件中读取它。要创建一个新的校准,要么删除或重命名mag.cal(它位于你的脚本相同的文件夹中),要么使用compass(newCal=True)创建你的compass对象。

在文件顶部添加以下代码(在导入语句之后):

FILENAME="mag.cal"

__init__(self)改为__init__(self,newCal=False)

此外,考虑以下行:

self.offset,self.scaling=self.calibrateCompass()

将前面的行更改为以下行:

self.offset,self.scaling=self.readCal(newCal)

按如下方式将readCal()添加到compass类中:

  def readCal(self,newCal=False,filename=FILENAME):
    if newCal==False:
      try:
        with open(FILENAME,'r') as magCalFile:
          line=magCalFile.readline()
          offset=line.split()
          line=magCalFile.readline()
          scaling=line.split()
          if len(offset)==0 or len(scaling)==0:
            raise ValueError()
          else:
            offset=list(map(float, offset))
            scaling=list(map(float, scaling))
      except (OSError,IOError,TypeError,ValueError) as e:
        print("No Cal Data")
        newCal=True
        pass
    if newCal==True:
      print("Perform New Calibration")
      offset,scaling=self.calibrateCompass()
      self.writeCal(offset,scaling)
    return offset,scaling

按如下方式将writeCal()添加到compass类中:

  def writeCal(self,offset,scaling):
      if DEBUG:print("Write Calibration")
      if DEBUG:print("offset:"+str(offset))
      if DEBUG:print("scaling:"+str(scaling))
      with open(FILENAME,'w') as magCalFile:
        for value in offset:
          magCalFile.write(str(value)+" ")
        magCalFile.write("\n")
        for value in scaling:
          magCalFile.write(str(value)+" ")
        magCalFile.write("\n")

使用指南针驾驶机器人

现在我们需要做的只是使用指南针的方位来引导我们的机器人到期望的角度。

创建以下compassDrive.py脚本:

#!/usr/bin/env python3
#compassDrive.py
import XLoBorg3 as XLoBorg
import rover_drive as drive
import time

MARGIN=10 #turn until within 10degs
LEFT="l"; RIGHT="r"; DONE="#"

def calDir(target, current, margin=MARGIN):
  target=target%360
  current=current%360
  delta=(target-current)%360
  print("Target=%f Current=%f Delta=%f"%(target,current,delta))

  if delta <= margin:
    CMD=DONE
  else:
    if delta>180:
      CMD=LEFT
    else:
      CMD=RIGHT
  return CMD

def main():
  myCompass=XLoBorg.compass()
  myBot=drive.motor()
  while(True):
    print("Enter target angle:")
    ANGLE=input()
    try:
      angleTarget=float(ANGLE)
      CMD=LEFT
      while (CMD!=DONE):
        angleCompass=myCompass.readCompassAngle()
        CMD=calDir(angleTarget,angleCompass)
        print("CMD: %s"%CMD)
        time.sleep(1)
        myBot.cmd(CMD)
      print("Angle Reached!")
    except ValueError:
      print("Enter valid angle!")
      pass

if __name__ == '__main__':
  try:
    main()
  except KeyboardInterrupt:
    print ("Finish")
#End

我们导入我们之前创建的模块:XLoBorg3rover_drive(用于 Rover-Pi 机器人或所需的替代bug_drive),以及time。接下来,我们创建一个函数,该函数将根据给定的目标角度(用户请求)和当前角度(从compass类中读取)返回LEFTRIGHTDONE。如果指南针角度在目标角度 180 度以内,则转向LEFT。同样,如果它在 180 度以内,则转向RIGHT。最后,如果指南针角度在公差(+10 度/-10 度)内,则表示DONE。通过使用angle%360(它给出了角度除以 360 的余数),我们确保所有角度都是 0-360(即-90 变为 270)。

对于main()函数,我们创建myCompass(一个XLoBorg.compass对象)和myBot(一个drive.motor()对象);这些允许我们确定我们面对的方向,并为我们提供在期望方向上行驶的方法。在主循环中,我们提示输入目标角度,找到我们的机器人当前面对的角度,然后继续转向所需的角度,直到达到它(或达到足够近的地方)。

第十章。与技术接口

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

  • 使用远程插座自动化您的家庭

  • 使用 SPI 控制 LED 矩阵

  • 使用串行接口进行通信

  • 通过蓝牙控制 Raspberry Pi

  • 控制 USB 设备

简介

Raspberry Pi 与普通计算机区别的关键之一是其与硬件接口和控制的能 力。在本章中,我们使用 Raspberry Pi 远程控制带电插座,从另一台计算机通过串行连接发送命令,并远程控制 GPIO。我们利用 SPI(另一个有用的协议)来驱动 8 x 8 LED 矩阵显示屏。

我们还使用蓝牙模块与智能手机连接,允许设备之间无线传输信息。最后,我们通过 USB 发送的命令来控制 USB 设备。

小贴士

请务必查看附录中的硬件和软件列表部分,硬件和软件列表;它列出了本章中使用的所有项目及其获取地点。

使用远程插座自动化您的家庭

Raspberry Pi 可以通过提供精确的时间、控制和响应命令、按钮输入、环境传感器或来自互联网的消息的能力,成为家庭自动化的优秀工具。

准备工作

在控制使用市电的设备时必须格外小心,因为通常涉及高电压和电流。

注意

在没有适当培训的情况下,切勿尝试修改或更改连接到市电的设备。您绝对不能将任何自制的设备直接连接到市电。所有电子设备都必须经过严格的安全测试,以确保在发生故障的情况下不会对人员或财产造成风险或伤害。

在本例中,我们将使用遥控射频(RF)插头插座;这些插座使用一个独立的遥控单元发送特定的 RF 信号来切换连接到其上的任何电气设备的开/关。这允许我们修改遥控器,并使用 Raspberry Pi 安全地激活开关,而不会干扰危险的电压:

准备工作

遥控器和远程插座

本例中使用的特定遥控器上有六个按钮,可以直接切换三个不同的插座的开/关,并由 12V 电池供电。它可以切换到四个不同的频道,这将允许您控制总共 12 个插座(每个插座都有一个类似的选择器,将用于设置它将响应的信号)。

准备工作

遥控器内部

当按下遥控按钮时,将广播一个特定的 RF 信号(本例使用 433.92 MHz 的传输频率)。这将触发设置为相应频道(A、B、C 或 D)和数字(1、2 或 3)的任何插座。

内部,每个按钮将两个不同的信号连接到地,数字(1、2 或 3)和状态(开启或关闭)。这触发了遥控器要发出的正确广播。

准备就绪

将电线连接到遥控器 PCB 板上的 ON、OFF、1、2、3 和 GND 合适的位置(图中只连接了 ON、OFF、1 和 GND)

建议您不要将任何可能因开启或关闭而造成危险的物品连接到您的插座上。遥控器发送的信号不是唯一的(只有四个不同的频道可用)。因此,这使附近有类似插座组合的人无意中激活/关闭您的其中一个插座成为可能。建议您选择除默认的 A 以外的频道,这将略微降低他人意外使用相同频道的机会。

为了允许树莓派模拟遥控器的按钮按下,我们需要五个继电器来选择数字(1、2 或 3)和状态(开启或关闭)。

准备就绪

可以使用预制的继电器模块来切换信号

或者,可以使用第九章中的晶体管和继电器电路来模拟按钮按下。

将继电器控制引脚连接到树莓派 GPIO,并将插座遥控器连接到每个继电器输出,如下所示:

准备就绪

插座遥控电路

注意

虽然遥控器插座需要数字(1、2 或 3)和状态(开启或关闭)来激活插座,但激活射频传输的是状态信号。为了避免耗尽遥控器的电池,我们必须确保我们已经关闭了状态信号。

如何操作...

创建以下socketControl.py脚本:

#!/usr/bin/python3
# socketControl.py
import time
import RPi.GPIO as GPIO
#HARDWARE SETUP
# P1
# 2[V=G====XI====]26[=======]40
# 1[=====321=====]25[=======]39
#V=5V  G=Gnd
sw_num=[15,13,11]#Pins for Switch 1,2,3
sw_state=[16,18]#Pins for State X=Off,I=On
MSGOFF=0; MSGON=1
SW_ACTIVE=0; SW_INACTIVE=1

class Switch():
  def __init__(self):
    self.setup()
  def __enter__(self):
    return self
  def setup(self):
    print("Do init")
    #Setup the wiring
    GPIO.setmode(GPIO.BOARD)
    for pin in sw_num:
      GPIO.setup(pin,GPIO.OUT)
    for pin in sw_state:
      GPIO.setup(pin,GPIO.OUT)
    self.clear()
  def message(self,number,state):
    print ("SEND SW_CMD: %s %d" % (number,state))
    if state==MSGON:
      self.on(number)
    else:
      self.off(number)
  def on(self,number):
    print ("ON: %d"% number)
    GPIO.output(sw_num[number-1],SW_ACTIVE)
    GPIO.output(sw_state[MSGON],SW_ACTIVE)
    GPIO.output(sw_state[MSGOFF],SW_INACTIVE)
    time.sleep(0.5)
    self.clear()
  def off(self,number):
    print ("OFF: %d"% number)
    GPIO.output(sw_num[number-1],SW_ACTIVE)
    GPIO.output(sw_state[MSGON],SW_INACTIVE)
    GPIO.output(sw_state[MSGOFF],SW_ACTIVE)
    time.sleep(0.5)
    self.clear()
  def clear(self):
    for pin in sw_num:
      GPIO.output(pin,SW_INACTIVE)
    for pin in sw_state:
      GPIO.output(pin,SW_INACTIVE)
  def __exit__(self, type, value, traceback):
    self.clear()
    GPIO.cleanup()
def main():
  with Switch() as mySwitches:
    mySwitches.on(1)
    time.sleep(5)
    mySwitches.off(1)  

if __name__ == "__main__":
    main()
#End

插座控制脚本通过开启第一个插座 5 秒然后再次关闭来进行快速测试。

要控制其余的插座,创建以下 GUI 菜单:

如何操作...

遥控开关 GUI

创建以下socketMenu.py脚本:

#!/usr/bin/python3
#socketMenu.py
import tkinter as TK
import socketControl as SC

#Define Switches ["Switch name","Switch number"]
switch1 = ["Living Room Lamp",1]
switch2 = ["Coffee Machine",2]
switch3 = ["Bedroom Fan",3]
sw_list = [switch1,switch2,switch3]
SW_NAME = 0; SW_CMD  = 1
SW_COLOR=["gray","green"]

class swButtons:
  def __init__(self,gui,sw_index,switchCtrl):
    #Add the buttons to window
    self.msgType=TK.IntVar()
    self.msgType.set(SC.MSGOFF)
    self.btn = TK.Button(gui,
                  text=sw_list[sw_index][SW_NAME],
                  width=30, command=self.sendMsg,
                  bg=SW_COLOR[self.msgType.get()])
    self.btn.pack()
    msgOn = TK.Radiobutton(gui,text="On",
              variable=self.msgType, value=SC.MSGON)
    msgOn.pack()
    msgOff = TK.Radiobutton(gui,text="Off",
              variable=self.msgType,value=SC.MSGOFF)
    msgOff.pack()
    self.sw_num=sw_list[sw_index][SW_CMD]
    self.sw_ctrl=switchCtrl
  def sendMsg(self):
    print ("SW_CMD: %s %d" % (self.sw_num,
                              self.msgType.get()))
    self.btn.configure(bg=SW_COLOR[self.msgType.get()])
    self.sw_ctrl.message(self.sw_num,
                         self.msgType.get())

root = TK.Tk()
root.title("Remote Switches")
prompt = "Control a switch"
label1 = TK.Label(root, text=prompt, width=len(prompt),
                  justify=TK.CENTER, bg='lightblue')
label1.pack()
#Create the switch
with SC.Switch() as mySwitches:
  #Create menu buttons from sw_list
  for index, app in enumerate(sw_list):
    swButtons(root,index,mySwitches)
  root.mainloop()
#End

它是如何工作的...

第一个脚本定义了一个名为 Switch 的类;它在 setup 函数中设置了控制五个继电器所需的 GPIO 引脚。它还定义了 __enter____exit__ 函数,这些是 with..as 语句使用的特殊函数。当使用 with..as 创建类时,它使用 __enter__ 来执行任何额外的初始化或设置(如果需要),然后通过调用 __exit__ 来执行任何清理。当 Switch 类执行完毕后,所有继电器都关闭以保护遥控器的电池,并调用 GPIO.cleanup() 来释放 GPIO 引脚。__exit__ 函数的参数(typevaluetraceback)允许处理在 with..as 语句中执行类时可能发生的任何特定异常(如果需要)。

要控制插座,创建两个函数,这些函数将切换相关的继电器以激活遥控器,并发送所需的信号到插座。然后,稍后使用 clear() 再次关闭继电器。为了使控制开关更加容易,创建一个 message 函数,允许指定开关号和状态。

我们通过创建一个 Tkinter GUI 菜单来使用 socketControl.py 脚本。菜单由 swButtons 类定义的三组控制组成(每个开关一组)。

swButtons 类创建一个 Tkinter 按钮,以及两个 Radiobutton 控制器。每个 swButtons 对象都分配一个索引和 mySwitches 对象的引用。这允许我们为按钮设置一个名称,并在按下时控制特定的开关。通过调用 message() 来激活/停用插座,所需的开关号和状态由 Radiobutton 控制器设置。

还有更多...

之前的例子允许你重新布线大多数遥控插座的遥控器,但另一个选项是模拟信号以直接控制它。

直接发送射频控制信号

你可以不重新布线遥控器,而是使用与你的插座相同频率的发射器来复制遥控器的射频信号(这些特定的单元使用 433.94 MHz)。这取决于特定的插座和有时你的位置——一些国家禁止使用某些频率,你可能需要在发送自己的传输之前获得认证:

直接发送射频控制信号

433.94 MHz 射频发射器(左侧)和接收器(右侧)

可以使用由 ninjablocks.com 创建的 433Utils 来重新创建射频遥控器发送的信号。433Utils 使用 WiringPi,并使用 C++ 编写,允许高速捕获和复制射频信号。

使用以下命令获取代码:

cd ~
wget https://github.com/ninjablocks/433Utils/archive/master.zip
unzip master.zip

接下来,我们需要将我们的射频发射器(以便我们可以控制开关)和射频接收器(以便我们可以确定控制代码)连接到 Raspberry Pi。

发射器(较小的方形模块)有三个引脚,分别是电源(VCC)、地(GND)和数据输出(ATAD)。电源引脚上的电压将控制传输范围(我们将使用来自树莓派的 5V 电源,但你也可以将其替换为 12V,只要确保将地引脚连接到你的 12V 电源和树莓派)。

尽管接收器有四个引脚,但有一个电源引脚(VCC)、地引脚(GND)和两个数据输出引脚(DATA),它们是连接在一起的,所以我们只需要连接三根线到树莓派。

RF 发射器 RPi GPIO 引脚 RF 接收器 RPi GPIO 引脚
VCC (5V) 2 VCC (3V3) 1
数据输出 11 数据输入 13
GND 6 GND 9

在我们使用 RPi_Utils 内的程序之前,我们将进行一些调整以确保我们的 RX 和 TX 引脚设置正确。

433Utils-master/RPi_utils/ 中定位 codesend.cpp 以进行必要的更改:

cd ~/433Utils-master/RPi_utils
nano codesend.cpp -c

int PIN = 0;(位于大约第 24 行)改为 int PIN = 11;(RPi 物理引脚编号)。

wiringPi 改为使用物理引脚编号(位于大约第 27 行),通过将 wiringPiSetup() 替换为 wiringPiSetupPhy()。否则,默认是 WiringPi GPIO 编号;更多详情请见wiringpi.com/reference/setup/。找到以下行:

if (wiringPiSetup () == -1) return 1;

改成这样:

if (wiringPiSetupPhys () == -1) return 1;

使用 Ctrl + X, Y 保存并退出 nano

RFSniffer.cpp 进行类似的调整:

nano RFSniffer.cpp -c

找到以下行(位于大约第 25 行):

     int PIN = 2;

改成这样:

     int PIN = 13; //RPi physical pin number

找到以下行(位于大约第 27 行):

     if(wiringPiSetup() == -1) {

改成这样:

     if(wiringPiSetupPhys() == -1) {

使用 Ctrl + X, Y 保存并退出 nano

使用以下命令构建代码:

make all

应该可以无错误地构建,如下所示:

g++    -c -o codesend.o codesend.cpp
g++   RCSwitch.o codesend.o -o codesend -lwiringPi
g++    -c -o RFSniffer.o RFSniffer.cpp
g++   RCSwitch.o RFSniffer.o -o RFSniffer -lwiringPi

现在我们已经将 RF 模块连接到树莓派,并且代码已经准备好了,我们可以捕获来自遥控器的控制信号。运行以下命令并注意报告的输出:

sudo ./RFSniffer

通过将遥控器设置为频道 A 并按下按钮 1 OFF 来获取输出(注意我们可能会接收到一些随机噪声):

Received 1381716
Received 1381716
Received 1381716
Received 1381717
Received 1398103

我们现在可以使用 sendcode 命令发送信号来切换插座关闭(1381716)和开启(1381719):

sendcode 1381716
sendcode 1381719

你甚至可以设置树莓派使用接收器模块来检测来自遥控器的信号(在未使用的频道上)并对它们做出反应以启动进程、控制其他硬件或可能触发软件关闭/重启。

扩展射频发射器的范围

当由 5V 供电且没有附加天线时,发射器的范围非常有限。然而,在做出任何修改之前测试一切是值得的。

可以用 25 厘米的单芯线制作简单的线状天线,将 17 毫米侧连接到天线焊接点,然后绕 16 圈(使用细螺丝刀柄或类似物品),剩余的线在上面(大约 53 毫米)。更详细的描述请见 。

扩展射频发射器的范围

通过简单的天线,发射器的范围得到了极大的改善

确定遥控器代码的结构

记录每个按钮的代码,我们可以确定每个按钮的代码(并分解结构):

确定遥控器代码的结构

要选择通道 A、B、C 或 D,将两个位设置为 00。同样,对于按钮 1、2 或 3,将两个位设置为 00 以选择该按钮。最后,将最后两个位设置为 11 以开启,或设置为 00 以关闭。

请参阅arduinodiy.wordpress.com/2014/08/12/433-mhz-system-foryour-arduino/,该页面分析了这些以及其他类似的射频遥控器。

使用 SPI 控制 LED 矩阵

在第七章中,我们使用名为 I²C 的总线协议连接到设备。树莓派还支持另一种称为SPI串行外围接口)的芯片间协议。SPI 总线与 I²C 的不同之处在于它使用两条单方向数据线(而 I²C 使用一条双向数据线)。尽管 SPI 需要更多的线(I²C 使用两个总线信号,SDA 和 SCL),但它支持数据的同步发送和接收,并且比 I²C 具有更高的时钟速度。

使用 SPI 控制 LED 矩阵

SPI 设备与树莓派的通用连接

SPI 总线由以下四个信号组成:

  • SCLK:它提供时钟边缘以在输入/输出线上读写数据;由主设备驱动。当时钟信号从一个状态变化到另一个状态时,SPI 设备将检查 MOSI 信号的状态以读取一个比特。同样,如果 SPI 设备正在发送数据,它将使用时钟信号边缘来同步设置 MISO 信号状态的时刻。

  • CE:这指的是芯片使能(通常,每个从设备在总线上都使用一个单独的芯片使能)。主设备将芯片使能信号设置为低,以便与它想要通信的设备通信。当芯片使能信号设置为高时,它将忽略总线上的任何其他信号。此信号有时被称为芯片选择CS)或从选择SS)。

  • MOSI:这代表主输出,从输出(它连接到主设备的数据输出和从设备的数据输入)。

  • MISO:这代表主输入,从输出(它提供从从设备响应)。

以下图表显示了每个信号:

使用 SPI 控制 LED 矩阵

SPI 信号:SCLK(1)、CE(2)、MOSI(3)和 MISO(4)

之前的范围跟踪显示了通过 SPI 发送的两个字节。每个字节都使用SCLK (1)信号将时钟输入到 SPI 设备中。一个字节由八个时钟周期的一串(SCLK (1)信号上的低电平和随后的高电平)表示,当时钟状态改变时读取特定位的值。确切的采样点由时钟模式确定;在下面的图中,它是在时钟从低电平变为高电平时:

使用 SPI 控制 LED 矩阵

树莓派通过 MOSI(3)信号发送的第一个数据字节

发送的第一个字节是 0x01(所有位都是低电平,除了位 0)和第二个发送的是 0x03(只有位 1位 0是高电平)。同时,MOSI (4)信号从 SPI 设备返回数据——在这种情况下,0x08(位 3是高电平)和 0x00(所有位都是低电平)。SCLK (1)信号用于同步一切,包括从 SPI 设备发送的数据。

当数据被发送到特定 SPI 设备时,CE (2)信号保持低电平,以指示该设备监听MOSI (4)信号。当CE (2)信号再次设置为高电平时,它表示 SPI 设备传输已完成。

下图是一个通过SPI 总线控制的 8 x 8 LED 矩阵的图像:

使用 SPI 控制 LED 矩阵

一个显示字母 K 的 8 x 8 LED 模块

准备中

我们之前用于 I²C 的wiringPi库也支持 SPI。确保 wiringPi 已安装(有关详细信息,请参阅第七章,感知和显示现实世界数据),这样我们就可以在这里使用它。

接下来,如果我们之前在启用 I²C 时没有这样做,我们需要启用 SPI:

sudo nano /boot/config.txt

取消#前的注释以启用它,然后按(Ctrl + X, Y, Enter)保存:

dtparam=spi=on

你可以通过使用以下命令列出所有正在运行的模块并定位spi_bcm2835来确认 SPI 是激活的:

lsmod

你可以使用以下spiTest.py脚本来测试 SPI:

#!/usr/bin/python3
# spiTest.py
import wiringpi

print("Add SPI Loopback - connect GPIO Pin19 and Pin21")
print("[Press Enter to continue]")
input()
wiringpi.wiringPiSPISetup(1,500000)
buffer=str.encode("HELLO")
print("Buffer sent %s" % buffer)
wiringpi.wiringPiSPIDataRW(1,buffer)
print("Buffer received %s" % buffer)
print("Remove the SPI Loopback")
print("[Press Enter to continue]")
input()
buffer=str.encode("HELLO")
print("Buffer sent %s" % buffer)
wiringpi.wiringPiSPIDataRW(1,buffer)
print("Buffer received %s" % buffer)
#End

将输入 19 和 21 连接起来以创建一个用于测试的 SPI 环回。

准备中

SPI 环回测试

你应该得到以下结果:

Buffer sent b'HELLO'
Buffer received b'HELLO'
Remove the SPI Loopback
[Press Enter to continue]
Buffer sent b'HELLO'
Buffer received b'\x00\x00\x00\x00\x00'

下面的例子使用了一个由 SPI 控制的MAX7219 LED 驱动器驱动的 8 x 8 LED 矩阵显示器:

准备中

LED 控制器 MAX7219 引脚图,LED 矩阵引脚图,以及 LED 矩阵内部布线(从左到右)

尽管该设备已被设计用于控制八个独立的 7 段 LED 数字,但我们可以用它来制作我们的 LED 矩阵显示屏。当用作数字时,每个七段(加上小数点)都连接到一个 SEG 引脚上,每个数字的 COM 连接都连接到 DIG 引脚上。控制器随后根据需要打开每个段,同时将相关数字的 COM 置低以启用它。控制器可以通过快速切换 DIG 引脚来快速循环每个数字,以至于所有八个数字看起来同时点亮:

准备中

一个 7 段 LED 数字使用段 A 到 G,加上小数点 DP(decimal place)

我们以类似的方式使用控制器,除了每个 SEG 引脚将连接到矩阵中的一列,而 DIG 引脚将启用/禁用一行。

我们使用一个 8 x 8 模块,连接到 MAX7219 芯片,如下所示:

准备中

MAX7219 LED 控制器驱动 8 x 8 LED 矩阵显示屏

如何做…

要控制连接到 SPI MAX7219 芯片的 LED 矩阵,创建以下matrixControl.py脚本:

#!/usr/bin/python3
# matrixControl.py
import wiringpi
import time

MAX7219_NOOP        = 0x00
DIG0=0x01; DIG1=0x02; DIG2=0x03; DIG3=0x04
DIG4=0x05; DIG5=0x06; DIG6=0x07; DIG7=0x08
MAX7219_DIGIT=[DIG0,DIG1,DIG2,DIG3,DIG4,DIG5,DIG6,DIG7]
MAX7219_DECODEMODE  = 0x09
MAX7219_INTENSITY   = 0x0A
MAX7219_SCANLIMIT   = 0x0B
MAX7219_SHUTDOWN    = 0x0C
MAX7219_DISPLAYTEST = 0x0F
SPI_CS=1
SPI_SPEED=100000

class matrix():
  def __init__(self,DEBUG=False):
    self.DEBUG=DEBUG
    wiringpi.wiringPiSPISetup(SPI_CS,SPI_SPEED)
    self.sendCmd(MAX7219_SCANLIMIT, 8)   # enable outputs
    self.sendCmd(MAX7219_DECODEMODE, 0)  # no digit decode
    self.sendCmd(MAX7219_DISPLAYTEST, 0) # display test off
    self.clear()
    self.brightness(7)                   # brightness 0-15
    self.sendCmd(MAX7219_SHUTDOWN, 1)    # start display
  def sendCmd(self, register, data):
    buffer=(register<<8)+data
    buffer=buffer.to_bytes(2, byteorder='big')
    if self.DEBUG:print("Send byte: 0x%04x"%
                        int.from_bytes(buffer,'big'))
    wiringpi.wiringPiSPIDataRW(SPI_CS,buffer)
    if self.DEBUG:print("Response:  0x%04x"%
                        int.from_bytes(buffer,'big'))
    return buffer
  def clear(self):
    if self.DEBUG:print("Clear")
    for row in MAX7219_DIGIT:
      self.sendCmd(row + 1, 0)
  def brightness(self,intensity):
    self.sendCmd(MAX7219_INTENSITY, intensity % 16)

def letterK(matrix):
    print("K")
    K=(0x0066763e1e366646).to_bytes(8, byteorder='big')
    for idx,value in enumerate(K):
        matrix.sendCmd(idx+1,value)

def main():
    myMatrix=matrix(DEBUG=True)
    letterK(myMatrix)
    while(1):
      time.sleep(5)
      myMatrix.clear()
      time.sleep(5)
      letterK(myMatrix)

if __name__ == '__main__':
    main()
#End

运行脚本(python3 matrixControl.py)显示字母 K。

我们可以使用 GUI 通过matrixMenu.py来控制 LED 矩阵的输出:

#!/usr/bin/python3
#matrixMenu.py
import tkinter as TK
import time
import matrixControl as MC

#Enable/Disable DEBUG
DEBUG = True
#Set display sizes
BUTTON_SIZE = 10
NUM_BUTTON = 8
NUM_LIGHTS=NUM_BUTTON*NUM_BUTTON
MAX_VALUE=0xFFFFFFFFFFFFFFFF
MARGIN = 2
WINDOW_H = MARGIN+((BUTTON_SIZE+MARGIN)*NUM_BUTTON)
WINDOW_W = WINDOW_H
TEXT_WIDTH=int(2+((NUM_BUTTON*NUM_BUTTON)/4))
LIGHTOFFON=["red4","red"]
OFF = 0; ON = 1
colBg = "black"

def isBitSet(value,bit):
  return (value>>bit & 1)

def setBit(value,bit,state=1):
  mask=1<<bit
  if state==1:
    value|=mask
  else:
    value&=~mask
  return value

def toggleBit(value,bit):
  state=isBitSet(value,bit)
  value=setBit(value,bit,not state)
  return value

class matrixGUI(TK.Frame):
  def __init__(self,parent,matrix):
    self.parent = parent
    self.matrix=matrix
    #Light Status
    self.lightStatus=0
    #Add a canvas area ready for drawing on
    self.canvas = TK.Canvas(parent, width=WINDOW_W,
                        height=WINDOW_H, background=colBg)
    self.canvas.pack()
    #Add some "lights" to the canvas
    self.light = []
    for iy in range(NUM_BUTTON):
      for ix in range(NUM_BUTTON):
        x = MARGIN+MARGIN+((MARGIN+BUTTON_SIZE)*ix)
        y = MARGIN+MARGIN+((MARGIN+BUTTON_SIZE)*iy)
        self.light.append(self.canvas.create_rectangle(x,y,
                              x+BUTTON_SIZE,y+BUTTON_SIZE,
                              fill=LIGHTOFFON[OFF]))
    #Add other items
    self.codeText=TK.StringVar()
    self.codeText.trace("w", self.changedCode)
    self.generateCode()
    code=TK.Entry(parent,textvariable=self.codeText,
                  justify=TK.CENTER,width=TEXT_WIDTH)
    code.pack()
    #Bind to canvas not tk (only respond to lights)
    self.canvas.bind('<Button-1>', self.mouseClick)

  def mouseClick(self,event):
    itemsClicked=self.canvas.find_overlapping(event.x,
                             event.y,event.x+1,event.y+1)
    for item in itemsClicked:
      self.toggleLight(item)

  def setLight(self,num):
    state=isBitSet(self.lightStatus,num)
    self.canvas.itemconfig(self.light[num],
                           fill=LIGHTOFFON[state])

  def toggleLight(self,num):
    if num != 0:
      self.lightStatus=toggleBit(self.lightStatus,num-1)
      self.setLight(num-1)
      self.generateCode()

  def generateCode(self):
    self.codeText.set("0x%016x"%self.lightStatus)

  def changedCode(self,*args):
    updated=False
    try:
      codeValue=int(self.codeText.get(),16)
      if(codeValue>MAX_VALUE):
        codeValue=codeValue>>4
      self.updateLight(codeValue)
      updated=True
    except:
      self.generateCode()
      updated=False
    return updated

  def updateLight(self,lightsetting):
    self.lightStatus=lightsetting
    for num in range(NUM_LIGHTS):
      self.setLight(num)
    self.generateCode()
    self.updateHardware()

  def updateHardware(self):
    sendBytes=self.lightStatus.to_bytes(NUM_BUTTON,
                                        byteorder='big')
    print(sendBytes)
    for idx,row in enumerate(MC.MAX7219_DIGIT):
      response = self.matrix.sendCmd(row,sendBytes[idx])
      print(response)

def main():
  global root
  root=TK.Tk()
  root.title("Matrix GUI")
  myMatrixHW=MC.matrix(DEBUG)
  myMatrixGUI=matrixGUI(root,myMatrixHW)
  TK.mainloop()

if __name__ == '__main__':
    main()
#End

Matrix GUI 允许我们通过点击每个方块(或直接输入十六进制值)来切换每个 LED 的开/关,以创建所需的图案。

如何做…

控制 8 x 8 LED 矩阵的 Matrix GUI

它是如何工作的...

最初,我们为 MAX7219 设备使用的每个控制寄存器定义了地址。查看数据表以获取更多信息。

我们创建了一个名为matrix的类,这将使我们能够控制该模块。__init__()函数设置树莓派的 SPI(使用SPI_CS作为引脚 26 CS1 和SPI_SPEED作为 100 kHz)。

我们matrix类中的关键函数是sendCmd()函数;它使用wiringpi.wiringPiSPIDataRW(SPI_CS,buff)通过 SPI 总线发送buffer(这是我们想要发送的原始字节数据,同时当传输发生时将SPI_CS引脚置低)。每个命令由两个字节组成:第一个指定寄存器的地址,第二个设置需要放入的数据。要显示一排灯光,我们发送一个ROW寄存器(MC.MAX7219_DIGIT)的地址和我们想要显示的位模式(作为一个字节)。

注意

在调用wiringpi.wiringPiSPIDataRW()函数后,buffer包含从 MISO 引脚接收到的结果(该引脚在数据通过 MOSI 引脚发送的同时被读取)。如果连接,这将是从 LED 模块输出的结果(发送数据的延迟副本)。有关有关菊花链 SPI 配置的更多信息,请参阅以下更多内容…部分,了解如何使用芯片输出。

为了初始化 MAX7219,我们需要确保它配置在正确的模式。首先,我们将 扫描限制 字段设置为 7(这将启用所有 DIG0 - DIG7 输出)。接下来,我们禁用了内置的数字解码,因为我们正在使用原始输出显示(并且不希望它尝试显示数字)。我们还希望确保 MAX7219_DISPLAYTEST 寄存器被禁用(如果启用,它将点亮所有 LED)。

我们通过调用自己的 clear() 函数来确保显示被清除,该函数将 0 发送到每个 MAX7219_DIGIT 寄存器以清除每一行。最后,我们使用 MAX7219_INTENSITY 寄存器设置 LED 的亮度。亮度通过 PWM 输出控制,以根据所需的亮度使 LED 看起来更亮或更暗。

main() 函数中,我们通过发送一组 8 个字节(0x0066763e1e366646)来在网格上快速测试显示字母 K。

每个 8 x 8 模式由 8 字节中的 8 位组成(每列一位,使每个字节成为显示中的一行)

工作原理...

matrixGUI 类创建了一个画布对象,该对象填充了一个矩形对象的网格,以表示我们想要控制的 8 x 8 LED 网格(这些保存在 self.light 中)。我们还添加了一个文本输入框来显示我们将发送到 LED 矩阵模块的结果字节。然后我们将 <Button-1> 鼠标事件绑定到画布,以便在画布区域内发生鼠标点击时调用 mouseClick

我们使用特殊的 Python 函数 trace 将一个名为 changedCode() 的函数附加到 codeText 变量上,这允许我们监控特定的变量或函数。如果我们使用 trace 函数的 'w' 值,Python 系统将在值被写入时调用回调函数。

mouseClick() 函数被调用时,我们使用 event.xevent.y 坐标来识别该位置的对象。如果检测到项目,则使用项目的 ID(通过 toggleLight())来切换 self.lightStatus 值中的相应位,并且显示中的灯光颜色相应改变(通过 setLight())。codeText 变量也更新为 lightStatus 值的新十六进制表示。

changeCode() 函数允许我们使用 codeText 变量并将其转换为整数。这允许我们检查它是否是一个有效的值。由于可以在这里自由输入文本,我们必须对其进行验证。如果我们无法将其转换为整数,则使用 lightStatus 值刷新 codeValue 文本。否则,我们检查它是否太大,在这种情况下,我们通过四位位移操作将其除以 16,直到它在有效范围内。我们更新 lightStatus 值、GUI 灯光、codeText 变量,以及硬件(通过调用 updateHardware())。

updateHardware()函数利用使用MC.matrix类创建的myMatrixHW对象。我们一次发送一个字节,我们想要显示到矩阵硬件的字节(以及相应的MAX7219_DIGIT值以指定行)。

还有更多...

SPI 总线允许我们通过使用芯片使能信号来控制同一总线上多个设备。一些设备,如 MAX7219,还允许所谓的菊花链 SPI 配置。

Daisy-chain SPI configuration

你可能已经注意到,当我们通过 MOSI 线发送数据时,matrix类也会返回一个字节。这是从 MAX7219 控制器 DOUT 连接输出的数据。MAX7219 控制器实际上将所有 DIN 数据传递到 DOUT,这比 DIN 数据晚一集指令。这样,MAX7219 可以通过菊花链(每个 DOUT 输入到下一个 DIN)连接。通过保持 CE 信号低,可以通过相互传递数据来向多个控制器加载数据。当 CE 设置为低时,数据将被忽略,输出只有在再次将其设置为高时才会改变。这样,你可以为链中的每个模块时钟所有数据,然后设置 CE 为高以更新它们:

菊花链 SPI 配置

菊花链 SPI 配置

我们需要为每个我们希望更新的行做这件事(或者如果我们想保持当前行不变,可以使用MAX7219_NOOP)。这被称为菊花链 SPI 配置,由一些 SPI 设备支持,其中数据通过 SPI 总线上的每个设备传递到下一个设备,这允许使用三个总线控制信号来控制多个设备。

使用串行接口进行通信

传统上,串行协议如 RS232 是连接打印机、扫描仪以及游戏手柄和鼠标设备等设备的常用方式。现在,尽管被 USB 取代,但许多外围设备仍然使用此协议进行组件之间的内部通信、数据传输和固件更新。对于电子爱好者来说,RS232 是一个非常有用的协议,用于调试和控制其他设备,同时避免了 USB 的复杂性。

本例中的两个脚本允许控制 GPIO 引脚,以说明我们如何通过串行端口远程控制树莓派。串行端口可以连接到 PC、另一个树莓派,甚至嵌入式微控制器(如 Arduino、PIC 或类似设备)。

准备工作

通过串行协议连接到树莓派的最简单方法取决于你的计算机是否有内置串行端口。串行连接、软件和测试设置在以下三个步骤中描述:

  1. 在你的计算机和树莓派之间创建一个 RS232 串行连接。为此,你需要以下配置之一:

    • 如果您的计算机有内置的串行端口可用,您可以使用 RS232 到 USB 适配器和 Null-Modem 线连接到树莓派:准备就绪

      用于 RS232 适配器的 USB

      Null-Modem 是一种串行线/适配器,其 TX 和 RX 线交叉连接,使得一边连接到串行端口的 TX 引脚,而另一边连接到 RX 引脚:

      准备就绪

      通过 Null-Modem 线和 RS232 到 USB 适配器连接到树莓皮的 PC 串行端口

      注意

      支持的 USB 到 RS232 设备列表可在以下链接中找到:elinux.org/RPi_VerifiedPeripherals#USB_UART_and_USB_to_Serial_.28RS-232.29_adapters

      请参阅更多内容…部分以获取有关如何设置的详细信息。

      如果您的计算机没有内置的串行端口,您可以使用另一个 USB 到 RS232 适配器连接到 PC/笔记本电脑,将 RS232 转换为更常见的 USB 连接。

      如果树莓派上没有可用的 USB 端口,您可以直接使用串行控制线或蓝牙串行模块的 GPIO 串行引脚(有关详细信息,请参阅更多内容…部分)。这两者都需要一些额外的设置。

      对于所有情况,您可以使用 RS232 环回确认一切正常且设置正确(再次,请参阅更多内容…部分)。

  2. 接下来,准备您需要的软件。

    我们需要安装 pySerial,以便我们可以使用 Python 使用串行端口

  3. 使用以下命令安装 pySerial(您还需要安装 PIP;有关详细信息,请参阅第三章,使用 Python 进行自动化和生产率):

    sudo pip-3.2 install pyserial
    

    请参阅 pySerial 网站以获取更多文档:pyserial.readthedocs.io/en/latest/

    为了演示 RS232 串行控制,您需要将一些示例硬件连接到树莓派的 GPIO 引脚。

    serialMenu.py脚本允许通过串行端口发送的命令来控制 GPIO 引脚。为了完全测试这一点,您可以将适当的输出设备(如 LED)连接到每个 GPIO 引脚。您可以使用每个 LED 的 470 欧姆电阻来确保总电流保持较低,这样树莓皮可以提供的最大 GPIO 电流就不会超过:

    准备就绪

    一个用于通过串行控制测试 GPIO 输出的测试电路

如何做到这一点...

创建以下serialControl.py脚本:

#!/usr/bin/python3
#serialControl.py
import serial
import time

#Serial Port settings
SERNAME="/dev/ttyUSB0"
#default setting is 9600,8,N,1
IDLE=0; SEND=1; RECEIVE=1

def b2s(message):
  '''Byte to String'''
  return bytes.decode(message)
def s2b(message):
  '''String to Byte'''
  return bytearray(message,"ascii")

class serPort():
  def __init__(self,serName="/dev/ttyAMA0"):
    self.ser = serial.Serial(serName)
    print (self.ser.name)
    print (self.ser)
    self.state=IDLE
  def __enter__(self):
    return self
  def send(self,message):
    if self.state==IDLE and self.ser.isOpen():
      self.state=SEND
      self.ser.write(s2b(message))
      self.state=IDLE

  def receive(self, chars=1, timeout=5, echo=True,
              terminate="\r"):
    message=""
    if self.state==IDLE and self.ser.isOpen():
      self.state=RECEIVE
      self.ser.timeout=timeout
      while self.state==RECEIVE:
        echovalue=""
        while self.ser.inWaiting() > 0:
          echovalue += b2s(self.ser.read(chars))
        if echo==True:
          self.ser.write(s2b(echovalue))
        message+=echovalue
        if terminate in message:
          self.state=IDLE
    return message
  def __exit__(self,type,value,traceback):
    self.ser.close()      

def main():
  try:
    with serPort(serName=SERNAME) as mySerialPort:
      mySerialPort.send("Send some data to me!\r\n")
      while True:
        print ("Waiting for input:")
        print (mySerialPort.receive())
  except OSError:
    print ("Check selected port is valid: %s" %serName)
  except KeyboardInterrupt:
    print ("Finished")

if __name__=="__main__":
  main()
#End    

确保使用serName元素正确设置我们想要使用的串行端口(例如,对于 GPIO 引脚是/dev/ttyAMA0,对于 USB RS232 适配器是/dev/ttyUSB0)。

将另一端连接到笔记本电脑或计算机的串行端口(串行端口可以是另一个 USB 到 RS232 适配器)。

使用串行程序(如 Windows 的 HyperTerminal 或 RealTerm 或 OS X 的 Serial Tools)监控你计算机上的串行端口。你需要确保设置了正确的 COM 端口,并设置了 9600 bps 的波特率(奇偶校验=None数据位=8停止位=1硬件流控制=None)。

脚本将向用户发送请求数据,并等待响应。

要向树莓派发送数据,请在另一台计算机上输入一些文本,然后按 Enter 键将其发送到树莓派。

你将在树莓派终端上看到类似以下输出:

如何操作...

文本 "打开 LED 1" 已通过连接的计算机的 USB 到 RS232 电缆发送

你也会在串行监控程序中看到类似以下输出:

如何操作...

RealTerm 显示连接的串行端口的典型输出

在树莓派上按 Ctrl + C 停止脚本。

现在,创建一个 GPIO 控制菜单。创建 serialMenu.py

#!/usr/bin/python3
#serialMenu.py
import time
import RPi.GPIO as GPIO
import serialControl as SC
SERNAME = "/dev/ttyUSB0"
running=True

CMD=0;PIN=1;STATE=2;OFF=0;ON=1
GPIO_PINS=[7,11,12,13,15,16,18,22]
GPIO_STATE=["OFF","ON"]
EXIT="EXIT"

def gpioSetup():
  GPIO.setmode(GPIO.BOARD)
  for pin in GPIO_PINS:
    GPIO.setup(pin,GPIO.OUT)

def handleCmd(cmd):
  global running
  commands=cmd.upper()
  commands=commands.split()
  valid=False
  print ("Received: "+ str(commands))
  if len(commands)==3:
    if commands[CMD]=="GPIO":
      for pin in GPIO_PINS:
        if str(pin)==commands[PIN]:
          print ("GPIO pin is valid")
          if GPIO_STATE[OFF]==commands[STATE]:
            print ("Switch GPIO %s %s"% (commands[PIN],
                                         commands[STATE]))
            GPIO.output(pin,OFF)
            valid=True
          elif GPIO_STATE[ON]==commands[STATE]:
            print ("Switch GPIO %s %s"% (commands[PIN],
                                         commands[STATE]))
            GPIO.output(pin,ON)
            valid=True
  elif commands[CMD]==EXIT:
    print("Exit")
    valid=True
    running=False
  if valid==False:
    print ("Received command is invalid")
    response="  Invalid:GPIO Pin#(%s) %s\r\n"% (
                      str(GPIO_PINS), str(GPIO_STATE))
  else:
    response="  OK\r\n"
  return (response)

def main():
  try:
    gpioSetup()
    with SC.serPort(serName=SERNAME) as mySerialPort:
      mySerialPort.send("\r\n")
      mySerialPort.send("  GPIO Serial Control\r\n")
      mySerialPort.send("  -------------------\r\n")
      mySerialPort.send("  CMD PIN STATE "+
                        "[GPIO Pin# ON]\r\n")
      while running==True:
        print ("Waiting for command...")
        mySerialPort.send(">>")
        cmd = mySerialPort.receive(terminate="\r\n")
        response=handleCmd(cmd)
        mySerialPort.send(response)
      mySerialPort.send("  Finished!\r\n")
  except OSError:
    print ("Check selected port is valid: %s" %serName)
  except KeyboardInterrupt:
    print ("Finished")
  finally:
    GPIO.cleanup()

main()
#End

当你运行脚本(sudo python3 serialMenu.py)时,在串行监控程序中输入控制消息:

如何操作...

GPIO 串行控制菜单

树莓派上的终端输出将类似于以下截图,LED 灯应该相应地响应:

如何操作...

GPIO 串行控制菜单

树莓派验证从串行连接接收到的命令,并切换连接到 GPIO 引脚 7 和 11 的 LED 灯的开和关。

它是如何工作的...

第一个脚本 serialControl.py 为我们提供了一个 serPort 类。我们使用以下函数定义该类:

  • __init__(self,serName="/dev/ttyAMA0"):此函数将使用 serName 创建一个新的串行设备——默认的 "/dev/ttyAMA0" 是 GPIO 串行引脚的 ID(见 更多内容 部分)。初始化后,将显示设备信息。

  • __enter__(self):这是一个虚拟函数,允许我们使用 with…as 方法。

  • send(self,message):此函数用于检查串行端口是否打开且未被使用;如果是,它将使用 s2b() 函数将消息转换为原始字节后发送消息。

  • receive(self, chars=1, echo=True, terminate="\r"):在检查串行端口是否打开且未被使用后,此函数将等待通过串行端口接收数据。该函数将收集数据,直到检测到终止字符,然后返回完整消息。

  • __exit__(self,type,value,traceback):当 serPort 对象不再需要 with…as 方法时,将调用此函数,因此我们可以在此处关闭端口。

脚本中的 main() 函数通过通过串行端口向连接的计算机发送数据提示并等待带有终止字符的输入来对类进行快速测试。

下一个脚本serialMenu.py允许我们使用serPort类。

main()函数设置 GPIO 引脚为输出(通过gpioSetup()),创建一个新的serPort对象,并最终通过串行端口等待命令。每当接收到新命令时,handleCmd()函数用于解析消息以确保它在采取行动之前是正确的。

该脚本将根据通过串行端口接收到的命令切换特定的 GPIO 引脚的开或关,使用GPIO命令关键字。我们可以添加任意数量的命令关键字并控制(或读取)我们连接到 Raspberry Pi 的任何设备(或设备)。我们现在有一种非常有效的方法来通过串行链路连接的任何设备控制 Raspberry Pi。

还有更多...

除了串行发送和接收之外,RS232 串行标准还包括其他几个控制信号。为了测试它,您可以使用串行环回以确认串行端口是否设置正确。

为 Raspberry Pi 配置 USB 到 RS232 设备

一旦您将 USB 到 RS232 设备连接到 Raspberry Pi,请通过输入以下命令来检查是否列出了新的串行设备:

dmesg | grep tty

dmesg命令列出了系统上发生的事件;使用grep,我们可以过滤出任何提及tty的消息,如下面的代码所示:

[ 2409.195407] usb 1-1.2: pl2303 converter now attached to ttyUSB0

这表明基于 PL2303 的 USB-RS232 设备已连接(启动后 2,409 秒)并分配了ttyUSB0标识。您将看到在/dev/目录下已添加了一个新的串行设备(通常是/dev/ttyUSB0或类似)。

如果设备未被检测到,您可以尝试与第一章中使用的步骤类似的步骤,即使用 Raspberry Pi 计算机入门,以定位和安装合适的驱动程序(如果可用)。

RS232 信号和连接

RS232 串行标准有很多变体,包括六个额外的控制信号。

Raspberry Pi GPIO 串行驱动程序(以及以下示例中使用的蓝牙 TTL 模块)仅支持 RX 和 TX 信号。如果您需要支持其他信号,例如常用于 AVR/Arduino 设备编程前的重置的 DTR,则可能需要其他 GPIO 串行驱动程序来通过其他 GPIO 引脚设置这些信号。大多数 RS232 到 USB 适配器应支持标准信号;然而,请确保您连接的任何设备都能处理标准 RS232 电压:

RS232 信号和连接

RS232 9-Way D 连接器引脚排列和信号

有关 RS232 串行协议的更多详细信息以及了解这些信号如何使用,请访问以下链接en.wikipedia.org/wiki/Serial_port:

使用 GPIO 内置的串行引脚

标准 RS232 信号的范围从-15V 到+15V,因此您绝对不能直接将任何 RS232 设备连接到 GPIO 串行引脚。您必须使用 RS232 到 TTL 电压级别转换器(如 MAX232 芯片)或使用 TTL 级别信号的设备(如另一个微控制器或 TTL 串行控制台电缆):

使用 GPIO 内置串行引脚

USB 到 TTL 串行控制台电缆

树莓派 GPIO 引脚上有 TTL 级别的串行引脚,允许连接 TTL 串行 USB 电缆。线将连接到树莓派 GPIO 引脚,USB 将插入到您的计算机,并像标准 RS232 到 USB 电缆一样被检测。

使用 GPIO 内置串行引脚

将 USB 到 TTL 串行控制台电缆连接到树莓派的 GPIO

从 USB 端口为 5V 引脚供电是可能的;然而,这将绕过内置的 polyfuse,因此不建议一般使用(只需将 5V 线断开,并通过 micro-USB 正常供电)。

默认情况下,这些引脚被设置为允许远程终端访问,允许您通过 PuTTY 连接到 COM 端口并创建串行 SSH 会话。

注意

如果您想在未连接显示器的树莓派上使用它,串行 SSH 会话可能会有所帮助。

然而,串行 SSH 会话仅限于纯文本终端访问,因为它不支持 X10 转发,正如在第一章的“通过 SSH(和 X11 转发)远程连接到树莓派”部分中所述,开始使用树莓派计算机

为了将其用作标准串行连接,我们必须禁用串行控制台,使其可供我们使用。

首先,我们需要编辑/boot/cmdline.txt以删除第一个consolekgboc选项(不要删除其他console=tty1选项,这是您打开时的默认终端):

sudo nano /boot/cmdline.txt
dwc_otg.lpm_enable=0 console=ttyAMA0,115200 kgdboc=ttyAMA0,115200 console=tty1 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline rootwait

之前的命令行变为以下(确保这仍然是一条单独的命令行):

dwc_otg.lpm_enable=0 console=tty1 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline rootwait

我们还必须通过注释掉#来删除运行getty命令的任务(处理串行连接文本终端的程序)。这设置在/etc/inittab中如下:

sudo nano /etc/inittab
T0:23:respawn:/sbin/getty -L ttyAMA0 115200 vt100

之前的命令行变为以下:

#T0:23:respawn:/sbin/getty -L ttyAMA0 115200 vt100

要在我们的脚本中引用 GPIO 串行端口,我们使用其名称,/dev/ttyAMA0

RS232 环回

您可以使用串行环回检查串行端口连接是否正常工作。

简单的环回包括将 RXD 和 TXD 连接在一起。这些是树莓派 GPIO 引脚上的第 8 和第 10 脚,或者在 USB-RS232 适配器上标准 RS232 D9 连接器上的第 2 和第 3 脚:

RS232 环回

测试树莓派 GPIO(左)和 RS232 9 针 D 连接器的串行环回连接

RS232 全环回电缆还连接了 RS232 适配器上的 4 号引脚(DTR)和 6 号引脚(DSR),以及 7 号引脚(RTS)和 8 号引脚(CTS)。然而,在大多数情况下,这并不是必需的,除非使用这些信号。默认情况下,Raspberry Pi 上没有为这些额外信号分配引脚。

RS232 环回测试

RS232 全环回

创建以下serialTest.py脚本:

#!/usr/bin/python3
#serialTest.py
import serial
import time

WAITTIME=1
serName="/dev/ttyAMA0"
ser = serial.Serial(serName)
print (ser.name)
print (ser)
if ser.isOpen():
  try:
    print("For Serial Loopback - connect GPIO Pin8 and Pin10")
    print("[Type Message and Press Enter to continue]")
    print("#:")
    command=input()
    ser.write(bytearray(command+"\r\n","ascii"))
    time.sleep(WAITTIME)
    out=""
    while ser.inWaiting() > 0:
      out += bytes.decode(ser.read(1))
    if out != "":
      print (">>" + out)
    else:
      print ("No data Received")
  except KeyboardInterrupt:
    ser.close()
#End

当环回连接时,你会观察到消息被回显到屏幕上(当移除时,将显示No data Received):

RS232 环回测试

在 GPIO 串行引脚上进行的 RS232 环回测试

如果需要非默认设置,它们可以在初始化串行端口时定义(pySerial 文档在pyserial.readthedocs.io/en/latest/提供了所有选项的完整详情),如下面的代码所示:

ser = serial.Serial(port=serName, baudrate= 115200, 
    timeout=1, parity=serial.PARITY_ODD,
    stopbits=serial.STOPBITS_TWO,
    bytesize=serial.SEVENBITS)

通过蓝牙控制 Raspberry Pi

通过连接支持串行端口配置文件SPP)的 HC-05 蓝牙模块到 GPIO 串行 RX/TX 引脚,串行数据也可以通过蓝牙发送。这允许串行连接无线化,从而可以使用 Android 平板电脑或智能手机来控制事物并从 Raspberry Pi 读取数据:

通过蓝牙控制 Raspberry Pi

TLL 串行 HC-05 蓝牙模块

注意

虽然可以使用 USB 蓝牙适配器实现类似的结果,但根据所使用的特定适配器,可能需要额外的配置。TTL 蓝牙模块为物理电缆提供了一个即插即用的替代品,需要非常少的额外配置。

准备工作

确保串行控制台已被禁用(参见之前的更多内容…部分)。

模块应使用以下引脚连接:

准备工作

连接到 TLL 串行的蓝牙模块

如何操作...

在蓝牙模块配置并连接后,我们可以将模块与笔记本电脑或智能手机配对,以无线发送和接收命令。蓝牙 SPP Pro 为 Android 设备提供了一个简单的方法,通过蓝牙使用串行连接来控制或监控 Raspberry Pi。

或者,你可能在 PC/笔记本电脑上设置一个蓝牙 COM 端口,并以与之前有线示例相同的方式使用它:

  1. 当设备首次连接时,LED 快速闪烁以指示它正在等待配对。在您的设备上启用蓝牙并选择HC-05设备:如何操作...

    在蓝牙 SPP Pro 中可查看的 HC-05 蓝牙模块

  2. 点击配对按钮开始配对过程并输入设备的PIN(默认为1234):如何操作...

    使用 PIN 码(1234)配对蓝牙设备

  3. 如果配对成功,您将能够连接到设备,并向 Raspberry Pi 发送和接收消息:如何操作...

    连接到设备并选择控制方法

  4. 键盘模式下,您可以定义每个按钮的动作,以便在按下时发送合适的命令。

    例如,Pin12 ON可以设置为发送gpio 12 on,而Pin12 OFF可以设置为发送gpio 12 off

  5. 确保通过菜单选项设置结束标志为\r\n

  6. 确保将menuSerial.py设置为使用 GPIO 串行连接:

    serName="/dev/ttyAMA0"
    
    
  7. 运行menuSerial.py脚本(连接 LED):

    sudo python3 menuSerial.py
    
    
  8. 确认蓝牙串行应用显示的GPIO Serial Control菜单,如下面的截图所示:如何操作...

    通过蓝牙进行 GPIO 控制

从以下截图的输出中我们可以看到,命令已被接收,连接到 12 号引脚的 LED 已按需打开和关闭。

如何操作...

Raspberry Pi 通过蓝牙接收 GPIO 控制

它是如何工作的...

默认情况下,蓝牙模块被设置为类似于 TTL 串行从设备,因此我们可以直接将其插入 GPIO RX 和 TX 引脚。一旦模块与设备配对,它将通过蓝牙连接传输串行通信。这使得我们可以通过蓝牙发送命令和接收数据,并使用智能手机或 PC 控制 Raspberry Pi。

这意味着您可以将第二个模块连接到另一个具有 TTL 串行引脚的设备(例如 Arduino),并使用 Raspberry Pi(通过与其他 TTL 蓝牙模块配对或适当配置 USB 蓝牙适配器)来控制它。如果模块被设置为主设备,那么您需要重新配置它以作为从设备(请参阅还有更多…部分)。

还有更多...

现在,让我们了解如何配置蓝牙设置。

配置蓝牙模块设置

可以使用 KEY 引脚将蓝牙模块设置为两种不同的模式。

在正常操作中,串行消息通过蓝牙发送;然而,如果我们需要更改蓝牙模块本身的设置,我们可以通过将 KEY 引脚连接到 3V3 并将其置于 AT 模式来实现。

AT 模式允许我们直接配置模块,允许我们更改波特率、配对码、设备名称,甚至将其设置为主/从设备。

您可以使用 pySerial 的一部分miniterm发送所需的消息,如下面的代码所示:

python3 -m serial.tools.miniterm

当启动miniterm程序时,将提示使用端口:

Enter port name: /dev/ttyAMA0

您可以发送以下命令(您需要快速完成此操作,或者粘贴它们,因为如果存在间隔,模块将超时并响应错误):

  • AT:此命令应响应OK

  • AT+UART?:此命令将报告当前设置,格式为UART=<Param1>,<Param2>,<Param3>。此命令的输出将是OK

  • 要更改当前设置,使用AT+UART=<Param1>,<Param2>,<Param3>,即AT+UART=19200,0,0配置蓝牙模块设置

    HC-05 AT 模式 AT+UART 命令参数

关于如何配置模块作为成对的从主设备(例如,两个树莓派设备之间)的详细信息,Zak Kemble 已经编写了一个优秀的指南。它可在以下链接找到:blog.zakkemble.co.uk/getting-bluetooth-modules-talking-to-each-other/

关于 HC-05 模块的更多文档,请访问以下链接:www.robotshop.com/media/files/pdf/rb-ite-12-bluetooth_hc05.pdf

控制 USB 设备

通用串行总线USB)被计算机广泛用于通过通用标准连接提供额外的外围设备和扩展。

以下示例控制一个 USB 玩具导弹发射器,反过来它允许通过我们的 Python 控制面板进行控制。我们看到同样的原理可以应用于其他 USB 设备,例如机械臂,使用类似的技术,并且可以通过连接到树莓派 GPIO 的传感器激活控制:

控制 USB 设备

USB Tenx Technology SAM 导弹发射器

准备工作

我们需要使用pip-3.2以下方式为 Python 3 安装 PyUSB:

sudo pip-3.2 install pyusb

你可以通过运行以下命令来测试 PyUSB 是否已正确安装:

python3
> import usb
> help (usb)
> exit()

这应该允许你在安装正确的情况下查看软件包信息。

如何操作...

我们将创建以下missileControl.py脚本,它将包括两个类和一个默认的main()函数以进行测试:

  1. 按以下方式导入所需的模块:

    #!/usr/bin/python3
    # missileControl.py
    import time
    import usb.core
    
  2. 定义SamMissile()类,它提供 USB 设备的特定命令,如下所示:

    class SamMissile():
      idVendor=0x1130
      idProduct=0x0202
      idName="Tenx Technology SAM Missile"
      # Protocol control bytes
      bmRequestType=0x21
      bmRequest=0x09
      wValue=0x02
      wIndex=0x01
      # Protocol command bytes
      INITA     = [ord('U'), ord('S'), ord('B'), ord('C'),
                   0,  0,  4,  0]
      INITB     = [ord('U'), ord('S'), ord('B'), ord('C'),
                   0, 64,  2,  0]
      CMDFILL   = [ 8,  8,
                    0,  0,  0,  0,  0,  0,  0,  0,
                    0,  0,  0,  0,  0,  0,  0,  0,
                    0,  0,  0,  0,  0,  0,  0,  0,
                    0,  0,  0,  0,  0,  0,  0,  0,
                    0,  0,  0,  0,  0,  0,  0,  0,
                    0,  0,  0,  0,  0,  0,  0,  0,
                    0,  0,  0,  0,  0,  0,  0,  0]#48 zeros
      STOP      = [ 0,  0,  0,  0,  0,  0]
      LEFT      = [ 0,  1,  0,  0,  0,  0]
      RIGHT     = [ 0,  0,  1,  0,  0,  0]
      UP        = [ 0,  0,  0,  1,  0,  0]
      DOWN      = [ 0,  0,  0,  0,  1,  0]
      LEFTUP    = [ 0,  1,  0,  1,  0,  0]
      RIGHTUP   = [ 0,  0,  1,  1,  0,  0]
      LEFTDOWN  = [ 0,  1,  0,  0,  1,  0]
      RIGHTDOWN = [ 0,  0,  1,  0,  1,  0]
      FIRE      = [ 0,  0,  0,  0,  0,  1]
      def __init__(self):
        self.dev = usb.core.find(idVendor=self.idVendor,
                                    idProduct=self.idProduct)
      def move(self,cmd,duration):
        print("Move:%s %d sec"% (cmd,duration))
        self.dev.ctrl_transfer(self.bmRequestType,
                               self.bmRequest,self.wValue,
                               self.wIndex, self.INITA)
        self.dev.ctrl_transfer(self.bmRequestType,
                               self.bmRequest,self.wValue,
                               self.wIndex, self.INITB)
        self.dev.ctrl_transfer(self.bmRequestType,
                               self.bmRequest, self.wValue,
                               self.wIndex, cmd+self.CMDFILL)
        time.sleep(duration)
        self.dev.ctrl_transfer(self.bmRequestType,
                               self.bmRequest, self.wValue,
                               self.wIndex, self.INITA)
        self.dev.ctrl_transfer(self.bmRequestType,
                               self.bmRequest, self.wValue,
                               self.wIndex, self.INITB)
        self.dev.ctrl_transfer(self.bmRequestType,
                          self.bmRequest, self.wValue,
                          self.wIndex, self.STOP+self.CMDFILL)
    
  3. 定义Missile()类,它允许我们检测 USB 设备并提供命令功能,如下所示:

    class Missile():
      def __init__(self):
        print("Initialize Missiles")
        self.usbDevice=SamMissile()
    
        if self.usbDevice.dev is not None:
          print("Device Initialized:" +
                " %s" % self.usbDevice.idName)
          #Detach the kernel driver if active
          if self.usbDevice.dev.is_kernel_driver_active(0):
            print("Detaching kernel driver 0")
            self.usbDevice.dev.detach_kernel_driver(0)
          if self.usbDevice.dev.is_kernel_driver_active(1):
            print("Detaching kernel driver 1")
            self.usbDevice.dev.detach_kernel_driver(1)
          self.usbDevice.dev.set_configuration()
        else:
          raise Exception("Missile device not found")
      def __enter__(self):
        return self
      def left(self,duration=1):
        self.usbDevice.move(self.usbDevice.LEFT,duration)
      def right(self,duration=1):
        self.usbDevice.move(self.usbDevice.RIGHT,duration)
      def up(self,duration=1):
        self.usbDevice.move(self.usbDevice.UP,duration)
      def down(self,duration=1):
        self.usbDevice.move(self.usbDevice.DOWN,duration)
      def fire(self,duration=1):
        self.usbDevice.move(self.usbDevice.FIRE,duration)
      def stop(self,duration=1):
        self.usbDevice.move(self.usbDevice.STOP,duration)
      def __exit__(self, type, value, traceback):
        print("Exit")
    
  4. 最后,创建一个main()函数,如果文件直接运行,它将提供一个快速测试我们的missileControl.py模块,如下所示:

    def main():
      try:
        with Missile() as myMissile:
          myMissile.down()
          myMissile.up()
      except Exception as detail:
    
          time.sleep(2)
        print("Error: %s" % detail)
    
    if __name__ == '__main__':
        main()
    #End
    

当使用以下命令运行脚本时,你应该看到导弹发射器向下移动然后再向上:

sudo python3 missileControl.py

为了提供对设备的简单控制,创建以下 GUI:

如何操作...

Missile Command GUI

虽然这里使用了简单的命令,但如果需要,可以使用一系列预设命令。

missileMenu.py导弹命令创建 GUI:

#!/usr/bin/python3
#missileMenu.py
import tkinter as TK
import missileControl as MC

BTN_SIZE=10

def menuInit():
  btnLeft = TK.Button(root, text="Left",
                      command=sendLeft, width=BTN_SIZE)   
  btnRight = TK.Button(root, text="Right",
                       command=sendRight, width=BTN_SIZE)   
  btnUp = TK.Button(root, text="Up",
                    command=sendUp, width=BTN_SIZE)   
  btnDown = TK.Button(root, text="Down",
                      command=sendDown, width=BTN_SIZE)
  btnFire = TK.Button(root, text="Fire",command=sendFire,
                      width=BTN_SIZE, bg="red")
  btnLeft.grid(row=2,column=0)
  btnRight.grid(row=2,column=2)
  btnUp.grid(row=1,column=1)
  btnDown.grid(row=3,column=1)
  btnFire.grid(row=2,column=1)

def sendLeft():
  print("Left")
  myMissile.left()

def sendRight():
  print("Right")    
  myMissile.right()

def sendUp():
  print("Up")
  myMissile.up()

def sendDown():
  print("Down")
  myMissile.down()

def sendFire():
  print("Fire")
  myMissile.fire()

root = TK.Tk()
root.title("Missile Command")
prompt = "Select action"
label1 = TK.Label(root, text=prompt, width=len(prompt),
                  justify=TK.CENTER, bg='lightblue')
label1.grid(row=0,column=0,columnspan=3)
menuInit()
with MC.Missile() as myMissile:
  root.mainloop()
#End

它是如何工作的...

控制脚本由两个类组成:一个称为Missile的类,它为控制提供通用接口,另一个称为SamMissile的类,它提供了特定 USB 设备的所有详细信息。

为了驱动 USB 设备,我们需要大量有关设备的信息,例如其 USB 标识、其协议以及控制消息,这些消息是控制设备所需的。

Tenx Technology SAM 导弹设备的 USB ID 由供应商 ID (0x1130) 和产品 ID (0x0202) 确定。这是你可以在 Windows 的 设备管理器 中看到的相同标识信息。这些 ID 通常在 www.usb.org 注册;因此,每个设备应该是唯一的。再次提醒,你可以使用 dmesg | grep usb 命令来发现这些。

我们使用设备 ID 通过 usb.core.find 查找 USB 设备;然后,我们可以使用 ctrl_transfer() 发送消息。

USB 消息有五个部分:

  • 请求类型 (0x21): 这定义了消息请求的类型,例如消息方向(主机到设备)、其类型(供应商)和接收者(接口)

  • 请求 (0x09): 这是设置配置

  • (0x02): 这是配置值

  • 索引 (0x01): 这是我们要发送的命令

  • 数据:这是我们想要发送的命令(如以下所述)

SamMissile 设备需要以下命令来移动:

  • 它需要两个初始化消息(INITAINITB)。

  • 它也需要控制消息。这包括 CMD,它包含一个设置为 1 的控制字节,用于所需的组件。然后,CMD 被添加到 CMDFILL 以完成消息。

你会发现其他导弹装置和机械臂(见下文 更多内容… 部分)具有类似的消息结构。

对于每个装置,我们创建了 __init__()move() 函数,并为每个有效命令定义了值,当调用 left()right()up()down()fire()stop() 函数时,missile 类将使用这些值。

对于我们的导弹发射器的控制 GUI,我们创建了一个带有五个按钮的小 Tkinter 窗口,每个按钮都会向导弹设备发送一个命令。

我们导入 missileControl 并创建一个名为 myMissilemissile 对象,该对象将由每个按钮控制。

更多内容...

示例仅展示了如何控制一个特定的 USB 设备;然而,可以将此扩展以支持多种类型的导弹装置,甚至是一般意义上的其他 USB 设备。

控制类似导弹类型的装置

有几种 USB 导弹类型装置的变体,每种都有自己的 USB ID 和 USB 命令。我们可以通过定义它们自己的类来处理这些其他设备,以支持这些设备。

使用 lsusb -vv 确定与你的设备匹配的供应商和产品 ID。

对于 Chesen Electronics/Dream Link,我们必须添加以下代码:

class ChesenMissile():
  idVendor=0x0a81
  idProduct=0x0701
  idName="Chesen Electronics/Dream Link"
  # Protocol control bytes
  bmRequestType=0x21
  bmRequest=0x09
  wValue=0x0200
  wIndex=0x00
  # Protocol command bytes
  DOWN    = [0x01]
  UP      = [0x02]
  LEFT    = [0x04]
  RIGHT   = [0x08]
  FIRE    = [0x10]
  STOP    = [0x20]
  def __init__(self):
    self.dev = usb.core.find(idVendor=self.idVendor,
                             idProduct=self.idProduct)
  def move(self,cmd,duration):
    print("Move:%s"%cmd)
    self.dev.ctrl_transfer(self.bmRequestType,
                           self.bmRequest,
                           self.wValue, self.wIndex, cmd)
    time.sleep(duration)
    self.dev.ctrl_transfer(self.bmRequestType,
                           self.bmRequest, self.wValue,
                           self.wIndex, self.STOP)

对于 Dream Cheeky Thunder,我们需要以下代码:

class ThunderMissile():
  idVendor=0x2123
  idProduct=0x1010
  idName="Dream Cheeky Thunder"
  # Protocol control bytes
  bmRequestType=0x21
  bmRequest=0x09
  wValue=0x00
  wIndex=0x00
  # Protocol command bytes
  CMDFILL = [0,0,0,0,0,0]
  DOWN    = [0x02,0x01]
  UP      = [0x02,0x02]
  LEFT    = [0x02,0x04]
  RIGHT   = [0x02,0x08]
  FIRE    = [0x02,0x10]
  STOP    = [0x02,0x20]
  def __init__(self):
    self.dev = usb.core.find(idVendor=self.idVendor,
                             idProduct=self.idProduct)
  def move(self,cmd,duration):
    print("Move:%s"%cmd)
    self.dev.ctrl_transfer(self.bmRequestType,
                           self.bmRequest, self.wValue,
                           self.wIndex, cmd+self.CMDFILL)
    time.sleep(duration)
    self.dev.ctrl_transfer(self.bmRequestType,
                      self.bmRequest, self.wValue,
                      self.wIndex, self.STOP+self.CMDFILL)

最后,调整脚本以使用所需的类如下:

class Missile():
  def __init__(self):
    print("Initialize Missiles")
    self.usbDevice = ThunderMissile()

机械臂

另一个可以用类似方式控制的设备是具有 USB 接口的 OWI 机器人臂。

机器人臂

OWI USB 接口机器人臂(图片由 Chris Stagg 提供)

这在The MagPi杂志中多次出现,多亏了 Stephen Richards 关于 Skutter 的文章;USB 控制已在第 3 期(第 14 页)中详细解释,可在issuu.com/themagpi/docs/the_magpi_issue_3_final/14找到。它也可以在www.raspberrypi.org/magpi/issues/3/找到。

机器人臂可以通过以下类进行控制。记住,在调用move()函数时,你还需要调整UPDOWN等命令,如下面的代码所示:

class OwiArm():
  idVendor=0x1267
  idProduct=0x0000
  idName="Owi Robot Arm"
  # Protocol control bytes
  bmRequestType=0x40
  bmRequest=0x06
  wValue=0x0100
  wIndex=0x00
  # Protocol command bytes
  BASE_CCW    = [0x00,0x01,0x00]
  BASE_CW     = [0x00,0x02,0x00]
  SHOLDER_UP  = [0x40,0x00,0x00]
  SHOLDER_DWN = [0x80,0x00,0x00]
  ELBOW_UP    = [0x10,0x00,0x00]
  ELBOW_DWN   = [0x20,0x00,0x00]
  WRIST_UP    = [0x04,0x00,0x00]
  WRIST_DOWN  = [0x08,0x00,0x00]
  GRIP_OPEN   = [0x02,0x00,0x00]
  GRIP_CLOSE  = [0x01,0x00,0x00]
  LIGHT_ON    = [0x00,0x00,0x01]
  LIGHT_OFF   = [0x00,0x00,0x00]
  STOP        = [0x00,0x00,0x00]

深入 USB 控制

用于 USB 导弹设备的理论和控制方法也可以应用于非常复杂的设备,如 Xbox 360 的 Kinect(Xbox 游戏控制台的特殊 3D 摄像头附加设备)。

Adafruit 的网站上有一篇由 Limor Fried(也称为 Ladyada)撰写的非常有趣的教程,介绍了如何分析和调查 USB 命令;可在learn.adafruit.com/hacking-the-kinect访问。

如果你打算逆向工程其他 USB 设备,这非常值得一看。

附录 A. 硬件和软件列表

需要的软件(含版本) 硬件规格 需要的操作系统
Samba 4.x 服务器软件 常用 Unix 打印系统 Windows
Python 3.4 N/A Windows
树莓派 3 B 型号、A+ 型号和 Pi Zero Windows
NOOBS N/A Windows
Tkinter N/A
IDLE 3
树莓派摄像头模块 N/A

备注

请注意,关于所使用的软件和硬件的详细列表(包括技术规格和版本)可在本书的主页上找到。您可以参考它以深入了解相同的内容。

posted @ 2025-09-20 21:35  绝不原创的飞龙  阅读(53)  评论(0)    收藏  举报