Python-物联网入门指南-全-

Python 物联网入门指南(全)

原文:zh.annas-archive.org/md5/4fe4273add75ed738e70f3d05e428b06

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

这个学习路径将带您进入机器人世界,并教会您如何利用树莓派和 Python 实现一切。

它教会您如何利用树莓派 3 和树莓派零的力量构建卓越的自动化系统,可以改变您的业务。您将学会创建文本分类器,预测单词的情感,并使用 Tkinter 库开发应用程序。当您使用 Python 构建人脸检测和识别系统以及家庭自动化系统时,事情将变得更有趣,不同的设备将使用树莓派进行控制。通过这些多样化的机器人项目,您将掌握机器人学的基础知识和功能,并了解机器人与物联网环境的整合。

通过学习路径的最后,您将涵盖从配置机器人控制器到使用 Python 创建自驾动机器车的一切。

这个学习路径包括以下 Packt 产品的内容:

  • Raspberry Pi 3 Cookbook for Python Programmers - Third Edition by Tim Cox, Dr. Steven Lawrence Fernandes

  • Python Programming with Raspberry Pi by Sai Yamanoor, Srihari Yamanoor

  • Python Robotics Projects by Prof. Diwakar Vaish

本书适合对象

这本书专为想要通过创建可以改善人们生活的机器人来提升技能的 Python 开发人员设计。熟悉 Python 和电子学将有助于理解本学习路径中的概念。

充分利用本书

要开始使用本书,读者应该了解 Python 编程的基础知识。读者对机器学习、计算机视觉和神经网络有基本的了解将是有益的。还建议使用以下硬件:

  • 带有任何操作系统的笔记本电脑

  • 树莓派

  • 一个 8GB 或 16GB 的 MicroSD 卡

  • USB 键盘、鼠标和 WiFi 卡

  • 带有 HDMI 输入的显示器

  • 电源供应,最低 500 毫安

  • 显示器电缆和其他配件

读者需要下载并安装 RASPBIAN STRETCH WITH DESKTOP;这将为我们提供树莓派的 GUI 界面

下载示例代码文件

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

您可以按照以下步骤下载代码文件:

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

  2. 选择“支持”选项卡。

  3. 单击“代码下载和勘误”。

  4. 在搜索框中输入书名,然后按照屏幕上的说明操作。

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

  • WinRAR/7-Zip 适用于 Windows

  • Zipeg/iZip/UnRarX 适用于 Mac

  • 7-Zip/PeaZip 适用于 Linux

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/GettingStartedwithPythonfortheInternetofThings。如果代码有更新,将在现有的 GitHub 存储库上进行更新。

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

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。以下是一个例子:“input()方法用于从用户那里获取输入。”

代码块设置如下:

try:         
    input_value = int(value)      
except ValueError as error:         
    print("The value is invalid %s" % error)

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

sudo pip3 install schedule

粗体:表示一个新术语,一个重要词或屏幕上看到的词。例如,菜单或对话框中的单词会在文本中显示为这样。这是一个例子:“如果你需要不同的东西,点击页眉中的下载链接以获取所有可能的下载:”

警告或重要说明会显示为这样。提示和技巧会显示为这样。

第一章:开始使用树莓派 3 电脑

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

  • 连接外围设备到树莓派

  • 使用 NOOBS 设置您的树莓派 SD 卡

  • 通过 LAN 连接器将您的树莓派连接到互联网

  • 在树莓派上使用内置的 Wi-Fi 和蓝牙

  • 手动配置您的网络

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

  • 通过 USB 无线网络适配器将您的树莓派连接到互联网

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

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

  • 使用 SSH(和 X11 转发)通过网络远程连接到树莓派

  • 通过 SMB 共享树莓派的主文件夹

  • 保持树莓派最新

介绍

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

最后,我们将利用网络提供远程连接和/或控制树莓派,以及确保系统保持最新状态的方法。

完成本章中的步骤后,您的树莓派将准备好供您进行编程使用。如果您已经设置并运行了您的树莓派,请确保您浏览以下部分,因为其中有许多有用的提示。

树莓派介绍

树莓派是由树莓派基金会创建的单板计算机,该基金会是一个旨在向英国儿童重新介绍低级计算机技能的慈善机构。其目标是重新点燃 20 世纪 80 年代的微型计算机革命,这产生了一整代熟练的程序员。

即使在 2012 年 2 月底发布计算机之前,树莓派已经在全球范围内获得了巨大的追随者,并且在撰写本书时已经销售了超过 1000 万台。以下图片显示了几种不同的树莓派型号:

树莓派 3B 型号,A+型号和 Pi Zero

名字是怎么回事?

树莓派的名称是希望创建一个以水果命名的替代计算机(如苹果,黑莓和杏子),并向最初的概念致敬,即可以使用 Python 编程的简单计算机(缩写为 Pi)。

在这本书中,我们将拿起这台小电脑,了解如何设置它,然后逐章探索它的功能,使用 Python 编程语言。

为什么选择 Python?

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

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

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

Python 解决了格式和表现方面的问题。缩进会增加可读性,在 Python 中缩进非常重要。它们定义了代码块如何组合在一起。一般来说,Python 运行速度较慢;因为它是解释性的,所以在运行程序时创建模块需要时间。如果需要对时间关键事件做出响应,这可能会成为一个问题。然而,您可以预编译 Python 或使用其他语言编写的模块来克服这个问题。

它隐藏了细节;这既是优点也是缺点。对于初学者来说很好,但当您不得不猜测数据类型等方面时可能会有困难。然而,这反过来又迫使您考虑所有可能性,这可能是一件好事。

Python 2 和 Python 3

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

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.6。Python 3 自 2008 年就已经推出,因此这些库往往是较老或较大的库,尚未被翻译。在许多情况下,旧库有新的替代方案;然而,它们的支持程度可能有所不同。

在这本书中,我们使用的是 Python 3.6,它也兼容 Python 3.5 和 3.3。

树莓派家族 - 树莓派的简史

自发布以来,树莓派已经推出了各种版本,对原始的树莓派 B 型进行了小型和大型的更新和改进。虽然一开始可能会令人困惑,但树莓派有三种基本类型可用(以及一个特殊型号)。

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

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

A 型一直被定位为精简版本。虽然具有与 B 型相同的 SOC,但连接有限,只有一个 USB 端口,没有有线网络(LAN)。A+型再次增加了更多的 GPIO 引脚和一个 microSD 卡槽。然而,RAM 后来升级为 512MB,再次只有一个 USB 端口/没有 LAN。Model A 上的 Broadcom BCM2835 SOC 到目前为止还没有更新(因此仍然是单核 ARM11);但是,可能会推出 3A 型(很可能使用 BCM2837)。

Pi Zero是树莓派的超紧凑版本,适用于成本和空间有限的嵌入式应用。它具有与其他型号相同的 40 针 GPIO 和 microSD 卡槽,但缺少板载显示(CSI 和 DSI)连接。它仍然具有 HDMI(通过迷你 HDMI)和单个 micro USB on-the-goOTG)连接。尽管在 Pi Zero 的第一个版本中没有,但最新型号还包括用于板载摄像头的 CSI 连接。

Pi Zero 在 2015 年被著名地发布,并随树莓派基金会的杂志The MagPi一起赠送,使该杂志成为第一本在封面上赠送计算机的杂志!这让我感到非常自豪,因为(正如你可能在本书开头的我的传记中读到的那样)我是该杂志的创始人之一。

特殊型号被称为计算模块。它采用 200 针 SODIMM 卡的形式。它适用于工业用途或商业产品中,所有外部接口将由主机/主板提供,模块将插入其中。示例产品包括 Slice Media Player(fiveninjas.com)和 OTTO 相机。当前模块使用 BCM2835,尽管有一个更新的计算模块(CM3)。

树莓派维基百科页面提供了所有不同变体及其规格的完整列表:en.wikipedia.org/wiki/Raspberry_Pi#Specifications

此外,树莓派产品页面提供了有关可用型号和配件规格的详细信息:www.raspberrypi.org/products/

选择哪种树莓派?

本书的所有部分都与当前所有版本的树莓派兼容,但建议首选 3B 型作为最佳型号。这提供了最佳性能(特别是对于 OpenCV 示例中使用的 GPU 示例,如第五章中的检测图像中的边缘和轮廓),大量连接和内置 Wi-Fi,非常方便。

Pi Zero 被推荐用于需要低功耗或减少重量/尺寸但不需要 Model 3B 全面处理能力的项目。然而,由于其超低成本,Pi Zero 非常适合在开发完成项目后部署。

连接到树莓派

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

准备就绪

在使用树莓派之前,您需要一个安装了操作系统或者在其中安装了新开箱系统NOOBS)的 SD 卡,如使用 NOOBS 设置树莓派 SD 卡中所讨论的那样。

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

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

操作步骤如下...

树莓派的布局如下图所示:

树莓派连接布局(3 B 型,A+型和 Zero 型)

有关上图的更多信息如下:

  • 显示:树莓派支持以下三种主要的显示连接;如果 HDMI 和复合视频都连接了,它将默认为仅 HDMI:

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

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

如果您希望使用旧的显示器(带有 VGA 连接),则需要额外的 HDMI 到 VGA 转换器。树莓派还支持一个基本的 VGA 适配器(VGA Gert666 Adaptor),它直接驱动 GPIO 引脚。然而,这会使用 40 针引脚排头的所有引脚(旧的 26 针型号不支持 VGA 输出):

HDMI 到 VGA 适配器

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

3.5 毫米音频模拟连接

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

    • 直接显示 DSI:由树莓派基金会生产的触摸显示器将直接连接到 DSI 插座。这可以与 HDMI 或模拟视频输出同时连接和使用,以创建双显示设置。
  • 立体声模拟音频(除 Pi Zero 外):这为耳机或扬声器提供了模拟音频输出。可以通过树莓派桌面上的配置工具或通过命令行使用amixeralsamixer在模拟(立体插孔)和数字(HDMI)之间进行切换。

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

 man amixer

有些命令还支持--help选项,以获得更简洁的帮助,如下所示:

 amixer --help

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

  • 内置 Wi-Fi 和蓝牙(仅限 Model 3 B):Model 3 B 具有内置的 802.11n Wi-Fi 和蓝牙 4.1;参见在树莓派上使用内置的 Wi-Fi 和蓝牙配方。

  • USB(1x Model A/Zero,2x Model 1 B,4x Model 2 B 和 3 B):使用键盘和鼠标:

  • 树莓派应该可以与大多数 USB 键盘和鼠标兼容。您也可以使用使用 RF dongles 的无线鼠标和键盘。但是,对于使用蓝牙 dongles 的设备需要额外的配置。

  • 如果您的电源供应不足或设备正在吸取过多电流,您可能会发现键盘按键似乎卡住了,并且在严重情况下,SD 卡可能会损坏。

早期 Model B 修订版 1 板的 USB 电源可能存在问题,这些板在 2012 年 10 月之前就已经上市。它们在 USB 输出上包含了额外的Polyfuses,如果超过 140 mA 的电流被吸取,就会跳闸。Polyfuses 可能需要数小时甚至数天才能完全恢复,因此即使电源改善了,也可能导致不可预测的行为。

您可以通过缺少后期型号上存在的四个安装孔来识别修订版 1 板。

    • Debian Linux(Raspbian 的基础)支持许多常见的 USB 设备,如闪存驱动器、硬盘驱动器(可能需要外部电源)、相机、打印机、蓝牙和 Wi-Fi 适配器。一些设备将被自动检测,而其他设备将需要安装驱动程序。
  • Micro USB 电源:树莓派需要一个能够舒适地提供至少 1,000 mA(特别是对于更耗电的 2 型和 3 型,建议提供 1,500 mA 或更多)的 5V 电源,带有一个 micro USB 连接。可以使用便携式电池组来为设备供电,比如适用于平板电脑的电池组。再次确保它们可以提供 5V 的电压,至少 1,000 mA。

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

还有更多...

除了标准的主要连接之外,树莓派还具有许多其他连接。

次要硬件连接

以下每个连接都提供了树莓派的其他接口:

  • 20 x 2 GPIO 引脚排针(Model A+,B+,2 B,3 B 和 Pi Zero):这是树莓派的主要 40 针 GPIO 引脚排针,用于直接与硬件组件进行接口。本书中的章节也适用于具有 13 x 2 GPIO 引脚排针的较旧型号的树莓派。

  • P5 8 x 2 GPIO 引脚排针(仅限 Model 1 B 修订版 2.0):我们在本书中不使用这个接口。

  • 复位连接:这个连接出现在较新的型号上(没有插脚)。当引脚 1(复位)和引脚 2(GND)连接在一起时,会触发复位。我们在第七章的A controlled shutdown button概念中使用了这个接口,Using Python to Drive Hardware

  • GPU/LAN JTAG联合测试行动组JTAG)是用于配置和测试处理器的编程和调试接口。这些接口出现在较新的型号上作为表面垫。使用这个接口需要专门的 JTAG 设备。我们在本书中不使用这个接口。

  • 直接相机 CSI:这个连接支持树莓派相机模块。请注意,Pi Zero 的 CSI 连接器比其他型号要小,因此需要不同的排线连接器。

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

使用 NOOBS 设置您的 Raspberry Pi SD 卡

在启动之前,树莓派需要将操作系统加载到 SD 卡上。设置 SD 卡的最简单方法是使用NOOBS;您可能会发现可以购买已经加载了 NOOBS 的 SD 卡。

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

准备工作

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

如果您使用的是以前使用过的 SD 卡,可能需要重新格式化以删除任何先前的分区和数据。NOOBS 期望 SD 卡由单个 FAT32 分区组成。

如果使用 Windows 或 macOS X,您可以使用 SD 卡协会的格式化程序,如下面的屏幕截图所示(可在www.sdcard.org/downloads/formatter_4/找到):

使用 SD 格式化程序清除 SD 卡上的任何分区

从选项设置对话框中,设置 FORMAT SIZE ADJUSTMENT。这将删除以前创建的所有 SD 卡分区。

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

完整的 NOOBS 软件包(通常略大于 1GB)包含了 Raspbian,最受欢迎的树莓派操作系统映像。还提供了一个精简版的 NOOBS,它没有预装的操作系统(尽管需要 Raspberry Pi 上的较小的初始下载 20MB 和网络连接来直接下载您打算使用的操作系统)。

NOOBS 可在www.raspberrypi.org/downloads上获得,文档可在github.com/raspberrypi/noobs上获得。

如何做...

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

  1. 准备好您的 SD 卡。

  2. 在新格式化或新的 SD 卡上,复制NOOBS_vX.zip文件的内容。复制完成后,您应该得到类似于 SD 卡以下屏幕截图的东西:

从 SD 卡中提取的 NOOBS 文件这些文件可能会因不同版本的 NOOBS 而略有不同,并且在您的计算机上显示的图标可能会有所不同。

  1. 您现在可以将卡插入树莓派,连接键盘和显示器,然后打开电源。有关所需的详细信息和操作方法,请参阅连接到树莓派配方。

默认情况下,NOOBS 将通过 HDMI 连接显示。如果您有其他类型的屏幕(或者什么也看不到),您需要通过按 1、2、3 或 4 手动选择输出类型,具体操作如下:

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

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

  • 键 3 代表复合 PAL(通过 RCA 模拟视频连接进行连接)

  • 键 4 代表复合 NTSC(同样,适用于通过 RCA 连接器连接)

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

过了一会儿,您将看到列出可用发行版的 NOOBS 选择屏幕(离线版本仅包括 Raspbian)。 有许多其他可用的发行版,但只有选定的发行版可以直接通过 NOOBS 系统获得。 点击 Raspbian,因为这是本书中使用的操作系统。

Enter或单击“安装操作系统”,并确认您希望覆盖卡上的所有数据

卡。 这将覆盖以前使用 NOOBS 安装的任何发行版,但不会删除 NOOBS 系统; 您可以在任何时候按下Shift键返回到它。

根据其速度,写入数据到卡上大约需要 20 到 40 分钟。 当完成并出现“图像应用成功”消息时,单击“确定”,树莓派将开始引导到树莓派桌面

它是如何工作的...

以这种方式将映像文件写入 SD 卡的目的是确保 SD 卡格式化为预期的文件系统分区和文件,以正确引导操作系统。

当树莓派启动时,它会加载存储在 GPU 内存中的一些特殊代码(通常被树莓派基金会称为二进制块)。 二进制块提供了读取 SD 卡上的BOOT分区所需的指令(在 NOOBS 安装的情况下,将从RECOVERY分区加载 NOOBS)。 如果此时按下Shift,NOOBS 将加载恢复和安装菜单。 否则,NOOBS 将根据SETTINGS分区中存储的首选项开始加载操作系统。

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

NOOBS 在 SD 卡上创建了几个分区,以允许安装多个

操作系统,并提供恢复

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

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

还有更多...

当您首次直接启动树莓派时,将加载桌面。 您可以使用 Raspberry Pi Configuration 菜单(在桌面上的首选项菜单下或通过sudo raspi-config命令)配置系统设置。 使用此菜单,您可以更改 SD 卡或设置一般首选项:

更改默认用户密码

确保在登录后更改pi用户帐户的默认密码,因为默认密码是众所周知的。 如果您连接到公共网络,这一点尤为重要。 您可以使用passwd命令来执行此操作,如下面的屏幕截图所示:

为 Pi 用户设置新密码

这样可以更加放心,因为如果您以后连接到另一个网络,只有您才能访问您的文件并控制您的树莓派。

确保安全关闭

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

sudo shutdown -h now  

或者,使用这个:

sudo halt  

在从树莓派断电之前,必须等待此命令完成(在 SD 卡访问指示灯停止闪烁后等待至少 10 秒)。

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

sudo reboot  

手动准备 SD 卡

使用 NOOBS 的替代方法是手动将操作系统映像写入 SD 卡。 尽管最初这是安装操作系统的唯一方法,但一些用户仍然更喜欢它。 它允许在将 SD 卡用于树莓派之前准备 SD 卡。 它还可以更容易地访问启动和配置文件,并且为用户留下更多的空间(与 NOOBS 不同,不包括RECOVERY分区)。

默认的 Raspbian 映像实际上由两个分区BOOTSYSTEM组成,可以放入 2GB 的 SD 卡(建议使用 4GB 或更多)。

您需要一台运行 Windows/Mac OS X/Linux 的计算机(尽管可以使用另一台树莓派来写入您的卡;请准备等待很长时间)。

下载您希望使用的操作系统的最新版本。 本书假定您正在使用www.raspberrypi.org/downloads上提供的最新版本的 Raspbian。

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

以下步骤适用于 Windows:

  1. 确保您已经下载了 Raspbian 映像,并将其提取到一个方便的文件夹以获取.img文件。

  2. 获取www.sourceforge.net/projects/win32diskimager上提供的Win32DiskImager.exe文件。

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

  4. 单击文件夹图标,导航到.img文件的位置,然后单击保存。

  5. 如果尚未这样做,请将 SD 卡插入卡读卡器并将其插入计算机。

  6. 从小下拉框中选择与您的 SD 卡对应的设备驱动器号。 仔细检查这是否是正确的设备(因为在写入映像时,程序将覆盖设备上的任何内容)。

在选择源映像文件之前,可能不会列出驱动器号。

  1. 最后,单击“写入”按钮,等待程序将映像写入 SD 卡,如下图所示:

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

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

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

  1. 使用您喜欢的网络浏览器下载 Raspbian 映像并将其保存在合适的位置。

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

unzip filename.zip  
  1. 如果尚未这样做,请将 SD 卡插入卡读卡器并将其插入计算机。

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

  3. 确保使用以下命令卸载 SD 卡上的所有分区

对于每个分区,使用umount /dev/sdXn命令,其中sdXn是要卸载的分区。

  1. 使用以下命令将映像文件写入 SD 卡:
sudo dd if=filename.img of=/dev/sdX bs=4M  
  1. 写入 SD 卡的过程需要一些时间,在完成时返回终端提示符。

  2. 使用以下命令卸载 SD 卡,然后从计算机中取出它:

umount /dev/sdX1  

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

  1. 使用您喜欢的网络浏览器下载 Raspbian 映像并将其保存在合适的位置。

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

unzip filename.zip  
  1. 如果尚未这样做,请将 SD 卡插入卡读卡器并将其插入计算机。

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

如果列出了 rdisk#,则使用它可以更快地写入(这使用原始路径并跳过数据缓冲)。

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

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

sudo dd if=filename.img of=/dev/diskX bs=1M  
  1. 该过程将花费一些时间来写入 SD 卡,并在完成时返回到终端提示符。

  2. 在从计算机中移除 SD 卡之前卸载 SD 卡,使用

以下命令:

unmountdisk /dev/diskX  

请参阅以下图表:

手动安装的 OS 映像的引导过程

扩展系统以适应您的 SD 卡

手动编写的映像将是固定大小的(通常是为了适应最小尺寸的 SD 卡)。要充分利用 SD 卡,您需要扩展系统分区以填满 SD 卡的其余部分。这可以通过 Raspberry Pi 配置工具实现。

选择“扩展文件系统”,如下截图所示:

Raspberry Pi 配置工具

访问 RECOVERY/BOOT 分区

Windows 和 macOS 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,如下截图所示:

df 命令的结果

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

cd /boot/  

要访问RECOVERYdata分区,我们必须通过执行以下操作来挂载它

以下步骤:

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

NOOBS 安装和数据分区

以下表格显示了分区的名称及其含义

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

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

  1. 创建一个文件夹,并将其设置为分区的挂载点;对于RECOVERY分区,请使用以下命令:
mkdir ~/recovery
sudo mount -t vfat /dev/mmcblk0p1 ~/recovery  

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

  1. /etc/rc.local中添加sudo挂载命令,然后是exit 0。如果您有不同的用户名,您需要将pi更改为匹配的用户名:
sudo nano /etc/rc.local
sudo mount -t vfat /dev/mmcblk0p1 /home/pi/recovery  
  1. Ctrl + XYEnter保存并退出。

添加到/etc/rc.local的命令将在登录到 Raspberry Pi 的任何用户上运行。如果您只希望该驱动器对当前用户进行挂载,则可以将命令添加到.bash_profile中。

如果必须在同一张卡上安装其他操作系统,则此处显示的分区标识符将不同。

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

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

要备份系统,或者使用树莓派克隆到另一个 SD 卡,请使用 SD 卡复制器(可通过桌面菜单的附件| SD 卡复制器获得)。

将 SD 卡插入树莓派的 USB 端口的读卡器中,并选择新的存储设备,如下截图所示:

SD 卡复制程序

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

dd命令也可以用来备份卡,如下所示:

  • 对于 Linux,用您的设备 ID 替换sdX,使用以下命令:
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  

通过以太网端口将树莓派连接到互联网的网络和连接,使用 CAT6 以太网电缆

将树莓派连接到互联网的最简单方法是使用 Model B 上的内置 LAN 连接。如果您使用的是 Model A 树莓派,则可以使用 USB 到 LAN 适配器(有关如何配置此适配器的详细信息,请参阅还有更多...部分的通过 USB 无线网络适配器连接树莓派到互联网配方)。

准备工作

您将需要访问适当的有线网络,该网络将连接到互联网,并且标准网络电缆(带有RJ45类型连接器,用于连接到树莓派)。

操作步骤

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

或者,如果没有 DHCP 服务器,您将不得不手动配置设置(有关详细信息,请参阅还有更多...部分)。

您可以通过以下步骤确认这一功能是否成功运行:

  1. 确保树莓派两侧的两个 LED 灯亮起(左侧橙色 LED 指示连接,右侧绿色 LED 显示闪烁的活动)。这将表明与路由器有物理连接,并且设备已经供电并正常工作。

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

sudo ping 192.168.1.254 -c 4
  1. 测试连接到互联网(如果您通常通过代理服务器连接到互联网,这将失败)如下:
sudo ping www.raspberrypi.org -c 4

  1. 最后,您可以通过发现来测试与树莓派的连接

在树莓派上使用hostname -I命令查找 IP 地址。然后,您可以在网络上的另一台计算机上使用ping命令来确保可以访问(使用树莓派的 IP 地址代替www.raspberrypi.org)。Windows 版本的ping命令将执行五次 ping 并自动停止,并且不需要-c 4选项。

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

还有更多...

如果您经常在网络上使用树莓派,您就不想每次连接时都要查找 IP 地址。

在一些网络上,您可以使用树莓派的主机名而不是其 IP 地址(默认为raspberrypi)。为了帮助实现这一点,您可能需要一些额外的软件,比如Bonjour,以确保网络上的主机名被正确注册。如果您使用的是 macOS X,那么 Bonjour 已经在运行了。

在 Windows 上,您可以安装 iTunes(如果您没有安装),它也包括了该服务,或者您可以单独安装(通过support.apple.com/kb/DL999提供的 Apple Bonjour Installer)。然后您可以使用主机名raspberrypiraspberrypi.local来连接到树莓派。如果您需要更改主机名,那么您可以使用之前显示的树莓派配置工具来进行更改。

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

一些路由器还可以设置静态 IP DHCP 地址的选项,这样相同的地址就会始终分配给树莓派(如何设置取决于路由器本身)。

如果您打算使用后面描述的远程访问解决方案之一,那么了解树莓派的 IP 地址或使用主机名尤其有用,这样就避免了需要显示器。

使用树莓派上的内置 Wi-Fi 和蓝牙

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

这种方法也适用于任何受支持的 USB Wi-Fi 和蓝牙设备;有关识别设备和安装固件(如果需要)的额外帮助,请参阅通过 USB Wi-Fi dongle 将树莓派连接到互联网的网络和连接配方。

准备好了

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

注意:如果您需要通过命令行配置 Wi-Fi,请参阅通过 USB Wi-Fi dongle 将树莓派连接到互联网的网络和连接配方以获取详细信息。Wi-Fi 和蓝牙配置应用程序

您可以使用内置的蓝牙连接无线键盘、鼠标,甚至无线扬声器。这对于一些需要额外电缆和线缆的项目非常有帮助,比如机器人项目,或者当树莓派安装在难以到达的位置时(作为服务器或安全摄像头)。

如何做...

以下是各种方法。

连接到您的 Wi-Fi 网络

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

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

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

为接入点提供密码

过一会儿,您应该会看到您已连接到网络,图标将变成 Wi-Fi 符号。如果遇到问题,请确保您有正确的密码/密钥:

成功连接到接入点

就是这样;就是这么简单!

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

sudo ping www.raspberrypi.com

连接到蓝牙设备

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

设置蓝牙为可发现状态

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

选择并配对所需的设备

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

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

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

手动配置您的网络

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

准备工作

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

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

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

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

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

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

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

ipconfig /all  

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

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

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

ifconfig  

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

less /etc/resolv.conf  

如何做到...

要设置网络接口设置,请使用编辑/etc/network/interfaces

以下代码:

sudo nano /etc/network/interfaces  

现在执行以下步骤:

  1. 我们可以添加我们特定网络的详细信息,我们要为其分配的 IP地址号,网络的netmask地址和gateway地址,如下所示:
iface eth0 inet static
 address 192.168.1.10
 netmask 255.255.255.0
 gateway 192.168.1.254

  1. 按下Ctrl + XYEnter保存并退出。

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

sudo nano /etc/resolv.conf

  1. 按照以下方式添加 DNS 服务器的地址:
nameserver 8.8.8.8
nameserver 8.8.4.4  
  1. 按下Ctrl + XYEnter保存并退出。

还有更多...

您可以通过编辑cmdline.txtBOOT分区中配置网络设置,并使用ip将设置添加到启动命令行。

ip选项采用以下形式:

ip=client-ip:nfsserver-ip:gw-ip:netmask:hostname:device:autoconf  
  • client-ip选项是您要分配给树莓派的 IP 地址

  • gw-ip选项将手动设置网关服务器地址

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

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

  • device选项允许您指定默认的网络设备(如果存在多个网络设备)

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

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

可以使用单个网络电缆直接连接树莓派 LAN 端口到笔记本电脑或计算机。这将在计算机之间创建一个本地网络链接,允许您进行连接到正常网络时可以做的所有事情,而无需使用集线器或路由器,包括连接到互联网,如果使用Internet Connection Sharing (ICS),如下所示:

只需使用一个网络电缆,一个标准镜像的 SD 卡和电源即可使用树莓派。

ICS 允许树莓派通过另一台计算机连接到互联网。但是,需要对计算机进行一些额外的配置,以便它们在链接上进行通信,因为树莓派不会自动分配自己的 IP 地址。

我们将使用 ICS 来共享来自另一个网络链接的连接,例如笔记本电脑上的内置 Wi-Fi。或者,如果不需要互联网或计算机只有一个网络适配器,我们可以使用直接网络链接(请参阅There's more...部分下的Direct network link部分)。

尽管这个设置对大多数计算机都适用,但有些设置比其他设置更困难。有关更多信息,请参见www.pihardware.com/guides/direct-network-connection

准备工作

您将需要带电源和标准网络电缆的树莓派。

树莓派 Model B LAN 芯片包括Auto-MDIXAutomatic Medium-Dependent Interface Crossover)。无需使用特殊的交叉电缆(一种特殊的网络电缆,布线使传输线连接到直接网络链接的接收线),芯片将根据需要自动决定和更改设置。

如果这是您第一次尝试,可能还需要键盘和显示器来进行额外的测试。

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

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

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

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

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

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

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

查找有线网络连接

  1. 右键单击其图标,然后单击属性。将出现对话框,如此屏幕截图所示:

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

  1. 选择名为 Internet Protocol(TCP/IP)或 Internet Protocol Version 4(TCP/IPv4)的项目(如果有两个版本(另一个是版本 6),然后单击属性按钮。

  2. 您可以通过使用自动设置或特定 IP 地址来确认您的网络设置(如果是这样,请记下此地址和其余细节,因为您可能希望在以后的某个时间点恢复设置)。

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

  1. 打开网络设置对话框,然后选择配置接口。请参考以下屏幕截图:

Linux 网络设置对话框

  1. 如果有任何手动设置,请确保记下它们,以便以后可以恢复它们。

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

  1. 打开系统偏好设置,然后单击网络。然后您可以确认 IP 地址是否自动分配(使用 DHCP)。

  2. 确保如果有任何手动设置,您记下它们,以便以后可以恢复它们。请参考以下屏幕截图:

OS X 网络设置对话框

如果您只需要访问或控制树莓派而无需互联网连接,请参考直接网络链接部分中的更多信息部分。

如何做...

首先,我们需要在我们的网络设备上启用 ICS。在这种情况下,我们将通过以太网连接将可用于无线网络连接的互联网共享到树莓派。

对于 Windows,执行以下步骤:

  1. 返回到网络适配器列表,右键单击链接

到互联网(在本例中,WiFi 或无线网络连接设备),然后单击属性:

查找有线网络连接

  1. 在窗口顶部,选择第二个选项卡(在 Windows XP 中称为高级;在 Windows 7 和 Windows 10 中称为共享),如下屏幕截图所示:

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

  1. 在 Internet Connection Sharing 部分,选中允许其他网络用户通过此计算机的 Internet 连接连接(如果有,请使用下拉框选择家庭网络连接:选项为以太网或本地连接)。单击确定并确认以前是否为本地连接设置了固定 IP 地址。

对于 macOS X,执行以下步骤启用 ICS:

  1. 单击系统偏好设置,然后单击共享。

  2. 单击 Internet 共享,然后选择要共享互联网的连接(在本例中,将是 Wi-Fi AirPort)。然后选择我们将连接树莓派的连接(在本例中,是以太网)。

对于 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软件将自动确保网络上的主机名正确注册。如前所述,如果您使用的是 Mac OS X,则已经运行了 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  
  1. 您可以通过使用ping连接到一个知名网站来测试与互联网的连接,假设您不是通过代理服务器访问互联网:
sudo ping www.raspberrypi.org -c 4  

如果一切顺利,您将可以通过计算机完全访问互联网,从而可以浏览网页以及更新和安装新软件。

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

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

  2. 您还可以通过以下命令检查树莓派启动时是否设置了正确的 IP 地址:

hostname -I  
  1. 检查防火墙设置,确保防火墙不会阻止内部网络连接。

它是如何工作的...

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

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

最后,我们检查计算机是否可以通过直接网络链路与树莓派进行通信,反之亦然,并且通过互联网进行通信。

还有更多...

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

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

直接网络链路

要使两台计算机之间的网络链接正常工作,它们需要使用相同的地址范围。可允许的地址范围由子网掩码确定(例如,255.255.0.0255.255.255.0表示所有 IP 地址应该相同,除了最后两个,或者只是 IP 地址中的最后一个数字;否则,它们将被过滤)。

要在不启用 ICS 的情况下使用直接链接,请检查您正在使用的适配器的 IP 设置

要连接到的 IP 地址,并确定它是自动分配还是固定的

特定的 IP 地址。

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

为了让 Raspberry Pi 能够通过直接链接进行通信,它需要在相同的地址范围169.254.X.X内具有 IP 地址。如前所述,Raspberry Pi 将自动为自己分配一个合适的 IP 地址并连接到网络。

因此,假设我们有 Apple Bonjour(前面提到过),我们只需要知道分配给 Raspberry Pi 的主机名(raspberrypi)。

另请参阅

如果您没有键盘或屏幕连接到 Raspberry Pi,您可以使用此网络链接远程访问 Raspberry Pi,就像在普通网络上一样(只需使用您为连接设置的新 IP 地址)。参考通过 VNC 远程连接到 Raspberry Pi通过 SSH(和 X11 转发)远程连接到 Raspberry Pi

我的网站pihw.wordpress.com/guides/direct-network-connection上提供了大量额外信息,包括额外的故障排除提示和连接到 Raspberry Pi 的其他几种方式,而无需专用屏幕和键盘。

通过 USB Wi-Fi dongle 进行网络连接和连接 Raspberry Pi 到互联网

通过在 Raspberry Pi 的 USB 端口添加USB Wi-Fi dongle,即使没有内置 Wi-Fi 的型号也可以连接并使用 Wi-Fi 网络。

准备工作

您需要获取一个合适的 USB Wi-Fi dongle,并且在某些情况下,您可能需要一个有源的 USB 集线器(这将取决于您拥有的 Raspberry Pi 的硬件版本和您的电源供应的质量)。USB Wi-Fi dongles 的一般适用性将取决于内部使用的芯片组以及可用的 Linux 支持水平。您可能会发现一些 USB Wi-Fi dongles 可以在不安装额外驱动程序的情况下工作(在这种情况下,您可以跳转到为无线网络配置它)。

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

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

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

  • 服务集标识符(SSID):这是您的 Wi-Fi 网络的名称,如果您使用以下命令,应该是可见的:
sudo iwlist scan | grep SSID  
  • 加密类型和密钥:此值将为 None、WEP、WPA 或 WPA2,密钥将是您通常在连接手机或笔记本电脑到无线网络时输入的代码(有时它会被打印在路由器上)。

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

sudo apt-get install firmware_file.deb  

如何做...

此任务有两个阶段:首先,我们识别并安装 Wi-Fi 适配器的固件,然后我们需要为无线网络配置它。

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

可以使用此命令找到支持的固件的近似列表:

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  

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

dmesg | grep 'Product:|Manufacturer:'

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

|(管道)将输出发送到grep命令;grep 'Product:|Manufacturer'检查它,并且只返回包含ProductManufacturer的行(因此我们应该得到列为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 dongle,以便它被检测到并加载驱动程序。我们现在可以使用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,通常是wlan0wlan1,如果您安装了多个,则会有更多。如果没有,请仔细检查所选的固件,或者尝试替代方案,或者在网站上查看故障排除提示。

一旦我们安装了 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保存并退出。

  1. 我们现在将我们网络的 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"     
} 
  1. 您可以使用以下命令启用适配器(如果需要,请再次替换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 网络。

  1. 最后,我们需要检查我们是否可以访问互联网。在这里,我们假设网络已自动配置为 DHCP,并且不使用代理服务器。如果不是,请参考通过代理服务器连接到互联网的示例。

拔掉有线网络电缆(如果仍然连接),然后查看是否可以 ping 通树莓派网站,如下所示:

**sudo ping** www.raspberrypi.org  

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

还有更多...

Raspberry Pi 的 A 型版本没有内置的网络端口,因此为了获得网络连接,必须添加 USB 网络适配器(可以是 Wi-Fi dongle,如前一节所述,也可以是 LAN-to-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  

如果失败(没有响应),您需要确保继续之前网络设置正确。

如何操作...

  1. 使用nano创建一个新文件,如下所示(如果文件中已经有一些内容,可以在末尾添加代码):
sudo nano -c ~/.bash_profile
  1. 要允许通过代理服务器进行基本的网页浏览,例如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 
} 
  1. 完成后,按Ctrl + XYEnter保存并退出。

脚本被添加到用户自己的.bash_profile文件中,在特定用户登录时运行。这将确保代理设置被分别保存给每个用户。如果您希望所有用户使用相同的设置,可以将代码添加到/etc/rc.local中(此文件必须在末尾有exit 0)。

工作原理...

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

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

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

还有更多...

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

  1. 使用以下命令打开一个新的sudoer文件:
sudo visudo -f /etc/sudoers.d/proxy  
  1. 在文件中输入以下文本(一行):
Defaults env_keep += "http_proxy HTTP_PROXY https_proxy HTTPS_PROXY ftp_proxy FTP_PROXY"  
  1. 完成后,按Ctrl + XYEnter保存并退出;不要更改proxy.tmp文件名(这对于visudo是正常的;完成后它会将其更改为 proxy)。

  2. 如果提示“现在怎么办?”,则命令中存在错误。按X退出而不保存并重新输入命令。

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

proxyenable
proxydisable  

在这里使用visudo很重要,因为它确保了为sudoers目录正确创建文件的权限(只能由root用户读取)。

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

通常,最好远程连接和控制树莓派跨网络,例如,使用笔记本电脑或台式电脑作为屏幕和键盘,或者当树莓派连接到其他地方时,也许甚至连接到一些需要靠近的硬件。

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

准备工作

确保您的树莓派已经启动并连接到互联网。我们将使用互联网连接来使用apt-get安装程序。这是一个允许我们直接从官方存储库中查找和安装应用程序的程序。

如何做...

  1. 首先,我们需要使用以下命令在树莓派上安装 TightVNC 服务器。建议先运行update命令以获取要安装的软件包的最新版本,如下所示:
sudo apt-get update
sudo apt-get install tightvncserver  
  1. 接受提示进行安装,并等待直到完成。要启动会话,请使用以下命令:
vncserver :1  
  1. 第一次运行时,它将要求您输入一个密码(不超过八个字符)以访问桌面(您在从计算机连接时将使用此密码)。

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

New 'X' desktop is raspberrypi:1  

如果您还不知道树莓派的 IP 地址,请使用hostname -I并记下它。

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

运行 VNC Viewer 时,将提示您输入服务器地址和加密类型。使用您的树莓派的 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(这在每次树莓派启动时执行)。

使用nano编辑器如下(-c选项允许显示行号):

sudo nano -c ~/.bash_profile  

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

vncserver :1  

下次启动时,您应该能够使用 VNC 从远程连接到树莓派

另一台计算机。

通过 SSH(和 X11 转发)远程连接到树莓派

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

SSH 的一个额外功能是能够将X11数据传输到运行在您的计算机上的X Windows服务器。这允许您启动通常在树莓派桌面上运行的程序,并且它们将出现在本地计算机上的自己的窗口中,如下所示:

在本地显示上的 X11 转发

X11 转发可用于在 Windows 计算机上显示在树莓派上运行的应用程序。

准备工作

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

如何做...

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

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

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

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

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

在 PuTTY 配置对话框中,导航到连接 | SSH | X11,并选中启用 X11 转发的复选框。如果将 X 显示位置选项留空,它将假定默认的Server 0:0如下(您可以通过将鼠标移到运行时系统托盘中的 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,您可以访问树莓派配置

在桌面的首选项菜单下的 SSH 中,单击接口选项卡,如下截图所示(大多数发行版通常默认启用 SSH,以帮助允许远程连接而无需显示器进行配置):

raspi-config 工具中的高级设置菜单

  1. 确保在树莓派上启用了 X11 转发(大多数发行版现在默认已启用此功能)。

  2. 使用以下命令使用nano

sudo nano /etc/ssh/sshd_config  
  1. /etc/ssh/sshd_config文件中查找控制 X11 转发的行,并确保它说yes(之前没有#符号),如下所示:
X11Forwarding yes  
  1. 如果需要,按Ctrl + XYEnter保存并重新启动(如果需要更改)如下:
sudo reboot  

还有更多...

SSH 和 X11 转发是远程控制树莓派的便捷方式;我们将在以下部分探讨如何有效使用它的一些额外提示。

通过 X11 转发运行多个程序

如果您想运行X 程序,但仍然可以在同一终端控制台上运行其他内容,可以使用&将命令在后台运行,如下所示:

leafpad &  

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

通过 X11 转发作为桌面运行

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

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

通过 X11 转发运行 Pygame 和 Tkinter

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

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

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

sudo cp ~/.Xauthority ~root/ 

使用 SMB 共享树莓派的主文件夹

当您将树莓派连接到网络时,您可以通过设置文件共享来访问主文件夹;这样可以更轻松地传输文件,并提供了一种快速简便的备份数据的方法。服务器消息块SMB)是一种与 Windows 文件共享、OS X 和 Linux 兼容的协议。

准备工作

确保您的树莓派已接通电源并连接到互联网。

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

如何做...

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

  1. 确保您使用以下命令来获取可用软件包的最新列表:
sudo apt-get update
sudo apt-get install samba  

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

  1. 安装完成后,我们可以按照以下方式复制配置文件,以便在需要时恢复默认设置:
sudo cp /etc/samba/smb.conf /etc/samba/smb.conf.backup
sudo nano /etc/samba/smb.conf  

向下滚动并找到名为Authentication的部分;将# security = user行更改为security = user

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

找到名为Share Definitions[homes]的部分,并将read only = yes行更改为read only = no

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

如果您已将默认用户从pi更改为其他内容,请在以下说明中进行替换。

  1. 现在,我们可以添加pi(默认用户)来使用samba
sudo pdbedit -a -u pi
  1. 现在,输入密码(您可以使用与登录相同的密码或选择不同的密码,但避免使用默认的树莓密码,这对某人来说将非常容易猜到)。重新启动samba以使用新的配置文件,如下所示:
sudo /etc/init.d/samba restart
[ ok ] Stopping Samba daemons: nmbd smbd.
[ ok ] Starting Samba daemons: nmbd smbd.  
  1. 要进行测试,您需要知道树莓派的hostname(默认hostnameraspberrypi)或其 IP 地址。您可以使用以下命令找到这两者。
hostname
  1. 对于 IP 地址,添加-I
hostname -I  

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

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

保持树莓派最新

树莓派使用的 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)

如果您在较新的板上使用较旧的固件(2012 年 11 月之前),这一点很重要,因为最初的 B 型板只有 254MB RAM。升级可以使固件利用额外的内存(如果可用)。

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

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

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

如何做...

  1. 在运行任何升级或安装任何软件包之前,值得确保您拥有存储库中最新的软件包列表。update命令获取可用软件和版本的最新列表:
sudo apt-get update  
  1. 如果您只想获取当前软件包的升级,upgrade将使它们全部保持最新状态:
sudo apt-get upgrade
  1. 为确保您运行的是最新版本的 Raspbian,可以运行dist-upgrade(请注意:这可能需要一小时或更长时间,具体取决于需要升级的数量)。这将执行upgrade将执行的所有更新,但还将删除多余的软件包并进行清理:
sudo apt-get dist-upgrade  

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

  1. 要更新固件,可以使用以下命令:
sudo rpi-update  

还有更多...

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

Pi Kitchen 允许在启动之前配置树莓派

Pi-Kitchen 允许配置各种口味,可以从 NOOBS 菜单中选择。每种口味都包括一系列食谱,每个食谱都提供最终操作系统的特定功能或特性。食谱可以从为 Wi-Fi 设备设置自定义驱动程序,到在您的网络上映射共享驱动器,再到提供一个功能齐全的网络服务器,所有这些都组合在一起,以满足您的要求。

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

第二章:划分文本数据和构建文本分类器

本章介绍了以下主题:

  • 构建文本分类器

  • 使用标记化预处理数据

  • 词干化文本数据

  • 使用分块划分文本

  • 构建词袋模型

  • 文本分类器的应用

介绍

本章介绍了构建文本分类器的方法。这包括从数据库中提取重要特征、训练、测试和验证文本分类器。最初,使用常用词训练文本分类器。后来,训练好的文本分类器用于预测。构建文本分类器包括使用标记化预处理数据、词干化文本数据、使用分块划分文本和构建词袋模型。

构建文本分类器

分类器单元通常被认为是将数据库分成各种类别的。朴素贝叶斯分类器方案被广泛认为是根据训练模型将文本分隔的文献。本章的这一部分最初考虑了一个带有关键词的文本数据库;特征提取从文本中提取关键短语并训练分类器系统。然后,实施词频-逆文档频率tf-idf)转换以指定单词的重要性。最后,使用分类器系统预测并打印输出。

如何做...

  1. 在新的 Python 文件中包含以下行以添加数据集:
from sklearn.datasets import fetch_20newsgroups 
category_mapping = {'misc.forsale': 'Sellings', 'rec.motorcycles': 'Motorbikes', 
        'rec.sport.baseball': 'Baseball', 'sci.crypt': 'Cryptography', 
        'sci.space': 'OuterSpace'} 

training_content = fetch_20newsgroups(subset='train', 
categories=category_mapping.keys(), shuffle=True, random_state=7) 
  1. 执行特征提取以从文本中提取主要单词:
from sklearn.feature_extraction.text import CountVectorizer 

vectorizing = CountVectorizer() 
train_counts = vectorizing.fit_transform(training_content.data) 
print "nDimensions of training data:", train_counts.shape 
  1. 训练分类器:
from sklearn.naive_bayes import MultinomialNB 
from sklearn.feature_extraction.text import TfidfTransformer 

input_content = [ 
    "The curveballs of right handed pitchers tend to curve to the left", 
    "Caesar cipher is an ancient form of encryption", 
    "This two-wheeler is really good on slippery roads" 
] 

tfidf_transformer = TfidfTransformer() 
train_tfidf = tfidf_transformer.fit_transform(train_counts) 
  1. 实现多项式朴素贝叶斯分类器:
classifier = MultinomialNB().fit(train_tfidf, training_content.target) 
input_counts = vectorizing.transform(input_content) 
input_tfidf = tfidf_transformer.transform(input_counts) 
  1. 预测输出类别:
categories_prediction = classifier.predict(input_tfidf) 
  1. 打印输出:
for sentence, category in zip(input_content, categories_prediction): 
    print 'nInput:', sentence, 'nPredicted category:',  
            category_mapping[training_content.target_names[category]] 

以下屏幕截图提供了根据数据库输入预测对象的示例:

它是如何工作的...

本章的前一部分提供了有关实施分类器部分和一些样本结果的见解。分类器部分是基于训练好的朴素贝叶斯中的先前文本与测试序列中的关键测试之间的比较工作的。

另请参阅

请参阅以下文章:

使用标记化预处理数据

数据的预处理涉及将现有文本转换为学习算法的可接受信息。

标记化是将文本分成一组有意义的片段的过程。这些片段被称为标记。

如何做...

  1. 介绍句子标记化:
from nltk.tokenize import sent_tokenize
  1. 形成一个新的文本标记器:
tokenize_list_sent = sent_tokenize(text)
print "nSentence tokenizer:" 
print tokenize_list_sent 
  1. 形成一个新的单词标记器:
from nltk.tokenize import word_tokenize 
print "nWord tokenizer:" 
print word_tokenize(text) 
  1. 引入一个新的 WordPunct 标记器:
from nltk.tokenize import WordPunctTokenizer 
word_punct_tokenizer = WordPunctTokenizer() 
print "nWord punct tokenizer:" 
print word_punct_tokenizer.tokenize(text) 

标记器得到的结果如下所示。它将一个句子分成单词组:

词干化文本数据

词干处理过程涉及为标记器的单词创建适当的缩写单词。

如何做...

  1. 使用新的 Python 文件初始化词干处理过程:
from nltk.stem.porter import PorterStemmer 
from nltk.stem.lancaster import LancasterStemmer 
from nltk.stem.snowball import SnowballStemmer 
  1. 让我们描述一些要考虑的词,如下所示:
words = ['ability', 'baby', 'college', 'playing', 'is', 'dream', 'election', 'beaches', 'image', 'group', 'happy'] 
  1. 确定要使用的一组词干处理器
stemmers = ['PORTER', 'LANCASTER', 'SNOWBALL'] 
  1. 为所选的“词干处理器”初始化必要的任务:
stem_porter = PorterStemmer() 
stem_lancaster = LancasterStemmer() 
stem_snowball = SnowballStemmer('english') 
  1. 格式化表格以打印结果:
formatted_row = '{:>16}' * (len(stemmers) + 1) 
print 'n', formatted_row.format('WORD', *stemmers), 'n' 
  1. 反复检查单词列表,并使用选择的“词干处理器”对它们进行排列:
for word in words:
  stem_words = [stem_porter.stem(word), 
  stem_lancaster.stem(word), 
  stem_snowball.stem(word)] 
  print formatted_row.format(word, *stem_words) 

词干处理过程得到的结果如下截图所示:

使用分块分割文本

分块过程可用于将大文本分成小的、有意义的单词。

如何做…

  1. 使用 Python 开发并导入以下包:
import numpy as np 
from nltk.corpus import brown 
  1. 描述将文本分成块的函数:
# Split a text into chunks 
def splitter(content, num_of_words): 
   words = content.split(' ') 
   result = [] 
  1. 初始化以下编程行以获取分配的变量:
   current_count = 0 
   current_words = []
  1. 开始使用单词进行迭代:
   for word in words: 
     current_words.append(word) 
     current_count += 1 
  1. 获取必要数量的单词后,重新组织变量:
     if current_count == num_of_words: 
       result.append(' '.join(current_words)) 
       current_words = [] 
       current_count = 0 
  1. 将块附加到输出变量:
       result.append(' '.join(current_words)) 
       return result 
  1. 导入“布朗语料库”的数据,并考虑前 10000 个单词:
if __name__=='__main__': 
  # Read the data from the Brown corpus 
  content = ' '.join(brown.words()[:10000]) 
  1. 描述每个块中的字大小:
  # Number of words in each chunk 
  num_of_words = 1600 
  1. 初始化一对重要的变量:
  chunks = [] 
  counter = 0 
  1. 通过调用“分割器”函数打印结果:
  num_text_chunks = splitter(content, num_of_words) 
  print "Number of text chunks =", len(num_text_chunks) 
  1. 分块后得到的结果如下截图所示:

构建词袋模型

在处理包含大单词的文本文档时,我们需要将它们转换为几种类型的算术表示。我们需要将它们制定为适合机器学习算法的形式。这些算法需要算术信息,以便它们可以检查数据并提供重要的细节。词袋程序帮助我们实现这一点。词袋创建一个文本模型,使用文档中的所有单词来发现词汇。随后,它通过构建文本中所有单词的直方图来为每个文本创建模型。

如何做…

  1. 通过导入以下文件初始化一个新的 Python 文件:
import numpy as np 
from nltk.corpus import brown 
from chunking import splitter 
  1. 定义main函数并从“布朗语料库”中读取输入数据:
if __name__=='__main__': 
        content = ' '.join(brown.words()[:10000]) 
  1. 将文本内容分成块:
    num_of_words = 2000 
    num_chunks = [] 
    count = 0 
    texts_chunk = splitter(content, num_of_words) 
  1. 基于这些“文本”块构建词汇表:
    for text in texts_chunk: 
      num_chunk = {'index': count, 'text': text} 
      num_chunks.append(num_chunk) 
      count += 1
  1. 提取文档词矩阵,有效地计算文档中每个单词的出现次数:
  from sklearn.feature_extraction.text      
  import CountVectorizer
  1. 提取文档术语“矩阵”:
from sklearn.feature_extraction.text import CountVectorizer 
vectorizer = CountVectorizer(min_df=5, max_df=.95) 
matrix = vectorizer.fit_transform([num_chunk['text'] for num_chunk in num_chunks]) 
  1. 提取词汇并打印它:
vocabulary = np.array(vectorizer.get_feature_names()) 
print "nVocabulary:" 
print vocabulary 
  1. 打印文档术语“矩阵”:
print "nDocument term matrix:" 
chunks_name = ['Chunk-0', 'Chunk-1', 'Chunk-2', 'Chunk-3', 'Chunk-4'] 
formatted_row = '{:>12}' * (len(chunks_name) + 1) 
print 'n', formatted_row.format('Word', *chunks_name), 'n' 
  1. 迭代单词,并打印每个单词在不同块中的重现:
for word, item in zip(vocabulary, matrix.T): 
# 'item' is a 'csr_matrix' data structure 
 result = [str(x) for x in item.data] 
 print formatted_row.format(word, *result)
  1. 执行词袋模型后获得的结果如下所示:

为了理解它在给定句子上的工作原理,请参考以下内容:

文本分类器的应用

文本分类器用于分析客户情绪,在产品评论中,当在互联网上搜索查询时,在社交标签中,预测研究文章的新颖性等等。

第三章:使用 Python 进行自动化和提高生产力

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

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

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

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

  • 自动整理您的照片

介绍

到目前为止,我们只专注于命令行应用程序;然而,树莓派不仅仅是命令行。通过使用图形用户界面GUI),通常更容易从用户那里获取输入并以更简单的方式提供反馈。毕竟,我们一直在不断处理多个输入和输出,所以为什么在不必要的情况下限制自己只使用命令行的程序格式呢?

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

首先,我们将以encryptdecrypt.py为例,演示可以编写和在各种方式中重复使用的有用模块。这是良好编码实践的一个例子。我们应该致力于编写可以进行彻底测试,然后在许多地方重复使用的代码。

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

然后,我们将探索在我们的应用程序中使用来显示,然后

整理照片。

使用 Tkinter 创建图形用户界面

我们将创建一个基本的 GUI,允许用户输入信息,然后程序可以用来加密和解密它。

准备工作

您必须确保该文件放置在相同的目录中。

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

Python3

>>> import tkinter

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

sudo apt-get install python3-tk

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

>>>help(tkinter)

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

>>>help(tkinter.Button)

以下dir命令将列出在module范围内的任何有效命令或变量:

>>>dir(tkinter.Button)

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

命令行将无法显示本章中创建的图形显示,因此您将需要启动树莓派桌面(使用startx命令),或者如果您是远程使用它。

确保您已启用X11 转发并且运行着X 服务器(参见第一章,使用树莓派 3 计算机入门)。

如何做...

我们将使用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()函数;它们将在以下部分中解释。

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

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

  • Label:这显示了加密消息的提示输入信息:

  • Entry:这提供了一个文本框来接收用户要加密的消息

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

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

  • Label:这显示了密钥:字段以提示用户输入加密密钥值

  • Entry:这提供了第二个文本框来接收加密密钥的值

这些控件将产生一个类似于以下截图所示的 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小部件,加密和解密,如下所示:

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

在这种情况下,我们可以设置一个函数,当点击Button小部件时调用该函数,方法是设置command属性。我们可以定义两个函数,当每个按钮被点击时将被调用。在以下代码片段中,我们有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 中有三种定义布局的方法:placepackgrid

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值,grid函数会默认为column=0。其他项目也是类似定义的。

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

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

本示例显示了如何定义我们自己的 Tkinter 对象的变体,以生成自定义控件并动态构建菜单。我们还将简要介绍使用线程来允许其他任务继续运行,同时执行特定任务。

准备工作

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

如何做...

  1. 要创建图形开始菜单应用程序,请创建以下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
  1. 上面的代码产生了以下应用程序:

应用程序菜单 GUI

它是如何工作的...

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

我们创建的类充当了appButtons项目要包含的蓝图或规范。每个项目将包括一个app_cmd的字符串值,一个名为startApp()的函数和一个__init__()函数。__init__()函数是一个特殊函数(称为构造函数),当我们创建一个appButtons项目时会调用它;它将允许我们创建任何所需的设置。

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

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

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 Image LibraryPIL);Python 3 的兼容版本是Pillow

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

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

以下命令可用于安装 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小部件上,但不幸的是,它不直接支持 JPEG,只支持 GIF 或 PPM。因此,我们只需将要显示的图像的小副本保存为 PPM 格式,然后让 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,它将是我们与Listbox小部件关联的listvariable参数。由于 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()来开始监视事件以做出响应。

自动整理您的照片

现在我们有了一个允许我们收集照片信息的类,我们可以将这些信息应用于执行有用的任务。在这种情况下,我们将使用文件信息自动将一个充满照片的文件夹组织成基于照片拍摄日期的子文件夹的子集。

以下屏幕截图显示了脚本的输出:

脚本输出以整理文件夹中的照片

准备工作

您需要在树莓派上的一个文件夹中放置一些照片。或者,您可以插入一个带有照片的 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获取目录中的所有文件的完整列表。我们将检查每个文件是否是 JPEG 文件,并获取每张照片的日期(使用Photo类中定义的函数)。接下来,我们将文件名和日期作为元组添加到self.photo_namedates列表中。

最后,我们将使用内置的 sorted 函数按日期顺序放置所有文件。虽然我们在这里不需要这样做,但如果我们在其他地方使用这个模块,这个函数将更容易删除重复的日期。

sorted 函数需要对列表进行排序,在这种情况下,我们希望按 date values: 进行排序。

   sorted(self.photo_namedates,key=lambda date: date[DATE])

我们将用 lambda date: 替换 date[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 来启用或禁用额外的调试消息。此外,FOLDERSONLY 当设置为 True 时,只生成文件夹而不移动文件(这对于测试新的子文件夹是否正确非常有帮助)。

运行脚本后,您可以检查所有文件夹是否已正确创建。最后,将 FOLDERSONLY 更改为 True,下次您的程序将根据日期自动移动和组织照片。建议您只在照片的副本上运行此操作,以防出现错误。

第四章:预测词语中的情感

本章介绍以下主题:

  • 构建朴素贝叶斯分类器

  • 逻辑回归分类器

  • 将数据集分割为训练集和测试集

  • 使用交叉验证评估准确性

  • 分析一个句子的情感

  • 使用主题建模识别文本中的模式

  • 情感分析的应用

构建朴素贝叶斯分类器

朴素贝叶斯分类器使用贝叶斯定理构建监督模型。

如何做...

  1. 导入以下软件包:
from sklearn.naive_bayes import GaussianNB
import numpy as np
import matplotlib.pyplot as plt
  1. 使用以下包含逗号分隔的算术数据的数据文件:
in_file = 'data_multivar.txt'
a = []
b = []
with open(in_file, 'r') as f:
  for line in f.readlines():
    data = [float(x) for x in line.split(',')]
    a.append(data[:-1])
    b.append(data[-1])
a = np.array(a)
b = np.array(b)
  1. 构建朴素贝叶斯分类器:
classification_gaussiannb = GaussianNB()
classification_gaussiannb.fit(a, b)
b_pred = classification_gaussiannb.predict(a)
  1. 计算朴素贝叶斯的准确性:
correctness = 100.0 * (b == b_pred).sum() / a.shape[0]
print "correctness of the classification =", round(correctness, 2), "%"
  1. 绘制分类器结果:
def plot_classification(classification_gaussiannb, a , b):
  a_min, a_max = min(a[:, 0]) - 1.0, max(a[:, 0]) + 1.0
  b_min, b_max = min(a[:, 1]) - 1.0, max(a[:, 1]) + 1.0
  step_size = 0.01
  a_values, b_values = np.meshgrid(np.arange(a_min, a_max,   step_size), np.arange(b_min, b_max, step_size))
  mesh_output1 = classification_gaussiannb.predict(np.c_[a_values.ravel(), b_values.ravel()])
  mesh_output2 = mesh_output1.reshape(a_values.shape)
  plt.figure()
  plt.pcolormesh(a_values, b_values, mesh_output2, cmap=plt.cm.gray)
  plt.scatter(a[:, 0], a[:, 1], c=b , s=80, edgecolors='black', linewidth=1,cmap=plt.cm.Paired)
  1. 指定图的边界:
plt.xlim(a_values.min(), a_values.max())
plt.ylim(b_values.min(), b_values.max())
*# specify the ticks on the X and Y axes* plt.xticks((np.arange(int(min(a[:, 0])-1), int(max(a[:, 0])+1), 1.0)))
plt.yticks((np.arange(int(min(a[:, 1])-1), int(max(a[:, 1])+1), 1.0)))
plt.show()
plot_classification(classification_gaussiannb, a, b)

执行朴素贝叶斯分类器后获得的准确性如下截图所示:

另请参阅

请参考以下文章:

  • 要了解分类器如何工作的示例,请参考以下链接:

en.wikipedia.org/wiki/Naive_Bayes_classifier

  • 要了解更多关于使用提议的分类器进行文本分类的信息,请参考以下链接:

sebastianraschka.com/Articles/2014_naive_bayes_1.html

  • 要了解更多关于朴素贝叶斯分类算法的信息,请参考以下链接:

software.ucv.ro/~cmihaescu/ro/teaching/AIR/docs/Lab4-NaiveBayes.pdf

逻辑回归分类器

可以选择这种方法,其中输出只能取两个值,0 或 1,通过/失败,赢/输,活着/死亡,健康/生病等。在因变量有两个以上的结果类别的情况下,可以使用多项逻辑回归进行分析。

如何做...

  1. 安装必要的软件包后,让我们构建一些训练标签:
import numpy as np
from sklearn import linear_model
import matplotlib.pyplot as plt
a = np.array([[-1, -1], [-2, -1], [-3, -2], [1, 1], [2, 1], [3, 2]])
b = np.array([1, 1, 1, 2, 2, 2])
  1. 初始化分类器:
classification = linear_model.LogisticRegression(solver='liblinear', C=100)
classification.fit(a, b)
  1. 绘制数据点和边界:
def plot_classification(classification, a , b):
  a_min, a_max = min(a[:, 0]) - 1.0, max(a[:, 0]) + 1.0
  b_min, b_max = min(a[:, 1]) - 1.0, max(a[:, 1]) + 1.0 step_size = 0.01
  a_values, b_values = np.meshgrid(np.arange(a_min, a_max, step_size), np.arange(b_min, b_max, step_size))
  mesh_output1 = classification.predict(np.c_[a_values.ravel(), b_values.ravel()])
  mesh_output2 = mesh_output1.reshape(a_values.shape)
  plt.figure()
  plt.pcolormesh(a_values, b_values, mesh_output2, cmap=plt.cm.gray)
  plt.scatter(a[:, 0], a[:, 1], c=b , s=80, edgecolors='black',linewidth=1,cmap=plt.cm.Paired)
 # specify the boundaries of the figure  plt.xlim(a_values.min(), a_values.max())
  plt.ylim(b_values.min(), b_values.max())
 # specify the ticks on the X and Y axes  plt.xticks((np.arange(int(min(a[:, 0])-1), int(max(a[:, 0])+1), 1.0)))
  plt.yticks((np.arange(int(min(a[:, 1])-1), int(max(a[:, 1])+1), 1.0)))
  plt.show()
  plot_classification(classification, a, b)

执行逻辑回归的命令如下截图所示:

将数据集分割为训练集和测试集

分割有助于将数据集分割为训练和测试序列。

如何做...

  1. 将以下代码片段添加到同一个 Python 文件中:
from sklearn import cross_validation
from sklearn.naive_bayes import GaussianNB
import numpy as np
import matplotlib.pyplot as plt
in_file = 'data_multivar.txt'
a = []
b = []
with open(in_file, 'r') as f:
  for line in f.readlines():
    data = [float(x) for x in line.split(',')]
    a.append(data[:-1])
    b.append(data[-1])
a = np.array(a)
b = np.array(b)
  1. 将 75%的数据用于训练,25%的数据用于测试:
a_training, a_testing, b_training, b_testing = cross_validation.train_test_split(a, b, test_size=0.25, random_state=5)
classification_gaussiannb_new = GaussianNB()
classification_gaussiannb_new.fit(a_training, b_training)
  1. 在测试数据上评估分类器的性能:
b_test_pred = classification_gaussiannb_new.predict(a_testing)
  1. 计算分类器系统的准确性:
correctness = 100.0 * (b_testing == b_test_pred).sum() / a_testing.shape[0]
print "correctness of the classification =", round(correctness, 2), "%"
  1. 绘制测试数据的数据点和边界:
def plot_classification(classification_gaussiannb_new, a_testing , b_testing):
  a_min, a_max = min(a_testing[:, 0]) - 1.0, max(a_testing[:, 0]) + 1.0
  b_min, b_max = min(a_testing[:, 1]) - 1.0, max(a_testing[:, 1]) + 1.0
  step_size = 0.01
  a_values, b_values = np.meshgrid(np.arange(a_min, a_max, step_size), np.arange(b_min, b_max, step_size))
  mesh_output = classification_gaussiannb_new.predict(np.c_[a_values.ravel(), b_values.ravel()])
  mesh_output = mesh_output.reshape(a_values.shape)
  plt.figure()
  plt.pcolormesh(a_values, b_values, mesh_output, cmap=plt.cm.gray)
  plt.scatter(a_testing[:, 0], a_testing[:, 1], c=b_testing , s=80, edgecolors='black', linewidth=1,cmap=plt.cm.Paired)
 # specify the boundaries of the figure  plt.xlim(a_values.min(), a_values.max())
  plt.ylim(b_values.min(), b_values.max())
  # specify the ticks on the X and Y axes
  plt.xticks((np.arange(int(min(a_testing[:, 0])-1), int(max(a_testing[:, 0])+1), 1.0)))
  plt.yticks((np.arange(int(min(a_testing[:, 1])-1), int(max(a_testing[:, 1])+1), 1.0)))
  plt.show()
plot_classification(classification_gaussiannb_new, a_testing, b_testing)

在以下截图中显示了数据集分割时获得的准确性:

使用交叉验证评估准确性

交叉验证在机器学习中是必不可少的。最初,我们将数据集分割为训练集和测试集。接下来,为了构建一个健壮的分类器,我们重复这个过程,但需要避免过度拟合模型。过度拟合表示我们对训练集获得了很好的预测结果,但对测试集获得了非常糟糕的结果。过度拟合导致模型的泛化能力差。

如何做...

  1. 导入软件包:
from sklearn import cross_validation
from sklearn.naive_bayes import GaussianNB
import numpy as np
in_file = 'cross_validation_multivar.txt'
a = []
b = []
with open(in_file, 'r') as f:
  for line in f.readlines():
    data = [float(x) for x in line.split(',')]
    a.append(data[:-1])
    b.append(data[-1])
a = np.array(a)
b = np.array(b)
classification_gaussiannb = GaussianNB()
  1. 计算分类器的准确性:
num_of_validations = 5
accuracy = cross_validation.cross_val_score(classification_gaussiannb, a, b, scoring='accuracy', cv=num_of_validations)
print "Accuracy: " + str(round(100* accuracy.mean(), 2)) + "%"
f1 = cross_validation.cross_val_score(classification_gaussiannb, a, b, scoring='f1_weighted', cv=num_of_validations)
print "f1: " + str(round(100*f1.mean(), 2)) + "%"
precision = cross_validation.cross_val_score(classification_gaussiannb,a, b, scoring='precision_weighted', cv=num_of_validations)
print "Precision: " + str(round(100*precision.mean(), 2)) + "%"
recall = cross_validation.cross_val_score(classification_gaussiannb, a, b, scoring='recall_weighted', cv=num_of_validations)
print "Recall: " + str(round(100*recall.mean(), 2)) + "%"
  1. 执行交叉验证后获得的结果如下所示:

为了了解它在给定的句子数据集上的工作情况,请参考以下链接:

  • 逻辑回归简介:

machinelearningmastery.com/logistic-regression-for-machine-learning/

分析一个句子的情感

情感分析是指找出特定文本部分是积极的、消极的还是中性的过程。这种技术经常被用来了解人们对特定情况的看法。它评估了消费者在不同形式中的情感,比如广告活动、社交媒体和电子商务客户。

如何做...

  1. 创建一个新文件并导入所选的包:
import nltk.classify.util
from nltk.classify import NaiveBayesClassifier
from nltk.corpus import movie_reviews
  1. 描述一个提取特征的函数:
def collect_features(word_list):
  word = []
  return dict ([(word, True) for word in word_list])
  1. 采用 NLTK 中的电影评论作为训练数据:
if __name__=='__main__':
  plus_filenum = movie_reviews.fileids('pos')
  minus_filenum = movie_reviews.fileids('neg')
  1. 将数据分成积极和消极的评论:
  feature_pluspts = [(collect_features(movie_reviews.words(fileids=[f])),
'Positive') for f in plus_filenum]
  feature_minuspts = [(collect_features(movie_reviews.words(fileids=[f])),
'Negative') for f in minus_filenum]
  1. 将数据分成训练和测试数据集:
  threshold_fact = 0.8
  threshold_pluspts = int(threshold_fact * len(feature_pluspts))
  threshold_minuspts = int(threshold_fact * len(feature_minuspts))
  1. 提取特征:
  feature_training = feature_pluspts[:threshold_pluspts] + feature_minuspts[:threshold_minuspts]
  feature_testing = feature_pluspts[threshold_pluspts:] + feature_minuspts[threshold_minuspts:]
  print "nNumber of training datapoints:", len(feature_training)
  print "Number of test datapoints:", len(feature_testing)
  1. 考虑朴素贝叶斯分类器,并用指定的目标进行训练:
  # Train a Naive Bayes classifiers
  classifiers = NaiveBayesClassifier.train(feature_training)
  print "nAccuracy of the classifiers:",nltk.classify.util.accuracy(classifiers,feature_testing)
  print "nTop 10 most informative words:"
  for item in classifiers.most_informative_features()[:10]:print item[0]
 # Sample input reviews  in_reviews = [
  "The Movie was amazing",
  "the movie was dull. I would never recommend it to anyone.",
  "The cinematography is pretty great in the movie",
  "The direction was horrible and the story was all over the place"
  ]
  print "nPredictions:"
  for review in in_reviews:
    print "nReview:", review
  probdist = classifiers.prob_classify(collect_features(review.split()))
  predict_sentiment = probdist.max()
  print "Predicted sentiment:", predict_sentiment
  print "Probability:", round(probdist.prob(predict_sentiment), 2)
  1. 情感分析的结果如下所示:

使用主题建模在文本中识别模式

主题建模是指识别手稿信息中隐藏模式的过程。其目标是在一系列文件中揭示一些隐藏的主题结构。

如何做...

  1. 导入以下包:
from nltk.tokenize import RegexpTokenizer
from nltk.stem.snowball import SnowballStemmer
from gensim import models, corpora
from nltk.corpus import stopwords
  1. 加载输入数据:
def load_words(in_file):
  element = []
  with open(in_file, 'r') as f:
    for line in f.readlines():
      element.append(line[:-1])
  return element
  1. 预处理文本的类:
classPreprocedure(object):
  def __init__(self):
 # Create a regular expression tokenizer    self.tokenizer = RegexpTokenizer(r'w+')
  1. 获取停用词列表以终止程序执行:
    self.english_stop_words= stopwords.words('english')
  1. 创建一个 Snowball 词干提取器:
    self.snowball_stemmer = SnowballStemmer('english')  
  1. 定义一个执行标记化、停用词去除和词干处理的函数:
  def procedure(self, in_data):
# Tokenize the string
    token = self.tokenizer.tokenize(in_data.lower())
  1. 从文本中消除停用词:
    tokenized_stopwords = [x for x in token if not x in self.english_stop_words]
  1. 对标记进行词干处理:
    token_stemming = [self.snowball_stemmer.stem(x) for x in tokenized_stopwords]
  1. 返回处理过的标记:
    return token_stemming
  1. main函数加载输入数据:
if __name__=='__main__':
 # File containing input data  in_file = 'data_topic_modeling.txt'
 # Load words  element = load_words(in_file)
  1. 创建一个对象:
  preprocedure = Preprocedure()
  1. 处理文件并提取标记:
  processed_tokens = [preprocedure.procedure(x) for x in element]
  1. 根据标记化的文档创建一个字典:
  dict_tokens = corpora.Dictionary(processed_tokens)
  corpus = [dict_tokens.doc2bow(text) for text in processed_tokens]
  1. 开发一个 LDA 模型,定义所需的参数,并初始化 LDA 目标:
  num_of_topics = 2
  num_of_words = 4
  ldamodel = models.ldamodel.LdaModel(corpus,num_topics=num_of_topics, id2word=dict_tokens, passes=25)
  print "Most contributing words to the topics:"
  for item in ldamodel.print_topics(num_topics=num_of_topics, num_words=num_of_words):
    print "nTopic", item[0], "==>", item[1]
  1. 执行topic_modelling.py时获得的结果如下截图所示:

情感分析的应用

情感分析在社交媒体如 Facebook 和 Twitter 中使用,以找出公众对某个问题的情感(积极/消极)。它们还用于确定人们对广告的情感以及人们对您的产品、品牌或服务的感受。

第五章:在图像中检测边缘和轮廓

本章介绍以下主题:

  • 加载、显示和保存图像

  • 图像翻转和缩放

  • 腐蚀和膨胀

  • 图像分割

  • 模糊和锐化图像

  • 在图像中检测边缘

  • 直方图均衡化

  • 在图像中检测角点

介绍

图像处理在几乎所有工程和医学应用中都起着至关重要的作用,用于从灰度/彩色图像中提取和评估感兴趣区域。图像处理方法包括预处理、特征提取和分类。预处理用于增强图像的质量;这包括自适应阈值处理、对比度增强、直方图均衡化和边缘检测。特征提取技术用于从图像中提取显著特征,以供后续分类使用。

本章介绍了构建图像预处理方案的程序。

加载、显示和保存图像

本节介绍了如何通过 OpenCV-Python 处理图像。此外,我们讨论了如何加载、显示和保存图像。

如何做...

  1. 导入计算机视觉包-cv2
import cv2 
  1. 使用内置的imread函数读取图像:
image = cv2.imread('image_1.jpg')
  1. 使用内置的imshow函数显示原始图像:
cv2.imshow("Original", image) 
  1. 等待按下任意键:
cv2.waitKey(0) 
  1. 使用内置的imwrite函数保存图像:
cv2.imwrite("Saved Image.jpg", image) 
  1. 用于执行 Python 程序Load_Display_Save.py的命令如下所示:

  1. 执行Load_Display_Save.py后获得的结果如下所示:

图像翻转

在图像翻转操作中,我们可以水平、垂直、水平和垂直翻转输入图像。

如何做...

  1. 导入计算机视觉包-cv2
import cv2 
  1. 使用内置的imread函数读取图像:
image = cv2.imread('image_2.jpg')
  1. 使用内置的imshow函数显示原始图像:
cv2.imshow("Original", image) 
  1. 等待按下任意键:
cv2.waitKey(0) 
  1. 对测试图像执行所需操作:
# cv2.flip is used to flip images 
# Horizontal flipping of images using value '1' 
flipping = cv2.flip(image, 1) 
  1. 显示水平翻转的图像:
# Display horizontally flipped image 
cv2.imshow("Horizontal Flipping", flipping) 
  1. 等待按下任意键:
cv2.waitKey(0) 
  1. 执行输入图像的垂直翻转:
# Vertical flipping of images using value '0' 
flipping = cv2.flip(image, 0) 
  1. 显示垂直翻转的图像:
cv2.imshow("Vertical Flipping", flipping) 
  1. 等待按下任意键:
cv2.waitKey(0) 
  1. 显示处理后的图像:
# Horizontal & Vertical flipping of images using value '-1' 
flipping = cv2.flip(image, -1) 
# Display horizontally & vertically flipped image 
cv2.imshow("Horizontal & Vertical Flipping", flipping) 
# Wait until any key is pressed 
cv2.waitKey(0)
  1. 停止执行并显示结果:
# Close all windows 
cv2.destroyAllWindows() 
  1. 用于执行Flipping.py Python 程序的命令如下所示:

  1. 执行Flipping.py后获得的原始和水平翻转的图像如下所示:

以下是水平翻转的图片:

  1. 执行Flipping.py后获得的垂直、水平和垂直翻转的图像如下所示:

以下是水平和垂直翻转的图片:

图像缩放

图像缩放用于根据要求修改输入图像的尺寸。在 OpenCV 中通常使用三种类型的缩放操作符,它们是立方、区域和线性插值。

如何做...

  1. 创建一个新的 Python 文件并导入以下包:
# Scaling (Resizing) Images - Cubic, Area, Linear Interpolations 
# Interpolation is a method of estimating values between known data points  
# Import Computer Vision package - cv2 
import cv2 
# Import Numerical Python package - numpy as np 
import numpy as np 
  1. 使用内置的imread函数读取图像:
image = cv2.imread('image_3.jpg') 
  1. 使用内置的imshow函数显示原始图像:
cv2.imshow("Original", image) 
  1. 等待按下任意键:
cv2.waitKey() 
  1. 根据操作员的命令调整图像大小:
# cv2.resize(image, output image size, x scale, y scale, interpolation) 
  1. 使用立方插值调整图像大小:
# Scaling using cubic interpolation 
scaling_cubic = cv2.resize(image, None, fx=.75, fy=.75, interpolation = cv2.INTER_CUBIC) 
  1. 显示输出图像:
# Display cubic interpolated image 
cv2.imshow('Cubic Interpolated', scaling_cubic) 
  1. 等待按下任意键:
cv2.waitKey()
  1. 使用区域插值调整图像大小:
# Scaling using area interpolation 
scaling_skewed = cv2.resize(image, (600, 300), interpolation = cv2.INTER_AREA) 
  1. 显示输出图像:
# Display area interpolated image 
cv2.imshow('Area Interpolated', scaling_skewed)  
  1. 等待操作员的指示:
# Wait until any key is pressed 
cv2.waitKey() 
  1. 使用线性插值调整图像大小:
# Scaling using linear interpolation 
scaling_linear  = cv2.resize(image, None, fx=0.5, fy=0.5, interpolation = cv2.INTER_LINEAR) 
  1. 显示输出图像:
# Display linear interpolated image 
cv2.imshow('Linear Interpolated', scaling_linear)  
  1. 等待按下任意键:
cv2.waitKey() 
  1. 完成图像缩放任务后,终止程序执行:
# Close all windows 
cv2.destroyAllWindows() 
  1. 用于执行Scaling.py Python 程序的命令如下所示:

  1. 用于缩放的原始图像如下所示:

  1. 执行Scaling.py文件后获得的线性插值输出如下所示:

  1. 执行Scaling.py文件后获得的面积插值输出如下所示:

  1. 执行Scaling.py文件后获得的立方插值输出如下所示:

腐蚀和膨胀

腐蚀和膨胀是形态学操作。腐蚀去除图像中对象边界的像素,而膨胀在图像中对象的边界上添加像素。

如何做...

  1. 导入计算机视觉包 - cv2
import cv2 
  1. 导入数值 Python 包 - numpy as np
import numpy as np 
  1. 使用内置的imread函数读取图像:
image = cv2.imread('image_4.jpg')
  1. 使用内置的imshow函数显示原始图像:
cv2.imshow("Original", image) 
  1. 等待按任意键:
cv2.waitKey(0) 
  1. 给定形状和类型,用 1 填充:
# np.ones(shape, dtype) 
# 5 x 5 is the dimension of the kernel, uint8: is an unsigned integer (0 to 255) 
kernel = np.ones((5,5), dtype = "uint8") 
  1. cv2.erode是用于腐蚀的内置函数:
# cv2.erode(image, kernel, iterations) 
erosion = cv2.erode(image, kernel, iterations = 1) 
  1. 使用内置的imshow函数显示腐蚀后的图像:
cv2.imshow("Erosion", erosion) 
  1. 等待按任意键:
cv2.waitKey(0) 
  1. cv2.dilate是用于膨胀的内置函数:
# cv2.dilate(image, kernel, iterations) 
dilation = cv2.dilate(image, kernel, iterations = 1) 
  1. 使用内置的imshow函数显示膨胀后的图像:
cv2.imshow("Dilation", dilation) 
  1. 等待按任意键:
cv2.waitKey(0) 
  1. 关闭所有窗口:
cv2.destroyAllWindows()
  1. 用于执行Erosion_Dilation.py文件的命令如下所示:

  1. 用于执行Erosion_Dilation.py文件的输入图像如下所示:

  1. 执行Erosion_Dilation.py文件后获得的腐蚀图像如下所示:

  1. 执行Erosion_Dilation.py文件后获得的膨胀图像如下所示:

图像分割

分割是将图像分成不同区域的过程。轮廓是对象边界周围的线条或曲线。本节讨论了使用轮廓进行图像分割。

如何做...

  1. 导入计算机视觉包 - cv2
import cv2 
# Import Numerical Python package - numpy as np 
import numpy as np 
  1. 使用内置的imread函数读取图像:
image = cv2.imread('image_5.jpg') 
  1. 使用内置的imshow函数显示原始图像:
cv2.imshow("Original", image) 
  1. 等待按任意键:
cv2.waitKey(0) 
  1. 执行Canny边缘检测系统:
# cv2.Canny is the built-in function used to detect edges 
# cv2.Canny(image, threshold_1, threshold_2) 
canny = cv2.Canny(image, 50, 200) 
  1. 使用内置的imshow函数显示检测到的边缘输出图像:
cv2.imshow("Canny Edge Detection", canny) 
  1. 等待按任意键:
cv2.waitKey(0)
  1. 执行轮廓检测系统:
# cv2.findContours is the built-in function to find contours 
# cv2.findContours(canny, contour retrieval mode, contour approximation mode) 
# contour retrieval mode: cv2.RETR_LIST (retrieves all contours)  
# contour approximation mode: cv2.CHAIN_APPROX_NONE (stores all boundary points) 
contours, hierarchy = cv2.findContours(canny, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE) 
  1. 在图像上勾画轮廓:
# cv2.drawContours is the built-in function to draw contours 
# cv2.drawContours(image, contours, index of contours, color, thickness) 
cv2.drawContours(image, contours, -1, (255,0,0), 10) 
# index of contours = -1 will draw all the contours 
  1. 显示图像的勾画轮廓:
# Display contours using imshow built-in function 
cv2.imshow("Contours", image) 
  1. 等待按任意键:
cv2.waitKey() 
  1. 终止程序并显示结果:
# Close all windows 
cv2.destroyAllWindows() 
  1. 执行Image_Segmentation.py文件后获得的结果如下所示:

以下是边缘检测输出:

模糊和锐化图像

模糊和锐化是用于增强输入图像的图像处理操作。

如何做...

  1. 导入计算机视觉包 - cv2
import cv2 
# Import Numerical Python package - numpy as np 
import numpy as np 
  1. 使用内置的imread函数读取图像:
image = cv2.imread('image_6.jpg') 
  1. 使用内置的imshow函数显示原始图像:
cv2.imshow("Original", image) 
  1. 等待按任意键:
cv2.waitKey(0) 
  1. 执行模糊操作的像素级操作:
# Blurring images: Averaging, cv2.blur built-in function 
# Averaging: Convolving image with normalized box filter 
# Convolution: Mathematical operation on 2 functions which produces third function. 
# Normalized box filter having size 3 x 3 would be: 
# (1/9)  [[1, 1, 1], 
#         [1, 1, 1], 
#         [1, 1, 1]] 
blur = cv2.blur(image,(9,9)) # (9 x 9) filter is used  
  1. 显示模糊的图像:
cv2.imshow('Blurred', blur) 
  1. 等待按任意键:
cv2.waitKey(0)
  1. 执行锐化操作的像素级操作:
# Sharpening images: Emphasizes edges in an image 
kernel = np.array([[-1,-1,-1],  
                   [-1,9,-1],  
                   [-1,-1,-1]]) 
# If we don't normalize to 1, image would be brighter or darker respectively     
# cv2.filter2D is the built-in function used for sharpening images 
# cv2.filter2D(image, ddepth, kernel) 
# ddepth = -1, sharpened images will have same depth as original image 
sharpened = cv2.filter2D(image, -1, kernel) 
  1. 显示锐化后的图像:
cv2.imshow('Sharpened', sharpened) 
  1. 等待按任意键:
cv2.waitKey(0) 
  1. 终止程序执行:
# Close all windows 
cv2.destroyAllWindows() 
  1. 用于执行Blurring_Sharpening.py的命令如下所示:

  1. 用于执行Blurring_Sharpening.py文件的输入图像如下所示:

  1. 执行Blurring_Sharpening.py文件后获得的模糊图像如下所示:

  1. 执行Blurring_Sharpening.py文件后获得的锐化图像如下所示:

在图像中检测边缘

边缘检测用于检测图像中的边界。它提供有关形状和区域属性的详细信息。这包括周长、主轴大小和次轴大小。

如何做...

  1. 导入必要的包:
import sys 
import cv2 
import numpy as np 
  1. 读取输入图像:
in_file = sys.argv[1] 
image = cv2.imread(in_file, cv2.IMREAD_GRAYSCALE) 
  1. 实现 Sobel 边缘检测方案:
horizontal_sobel = cv2.Sobel(image, cv2.CV_64F, 1, 0, ksize=5) 
vertical_sobel = cv2.Sobel(image, cv2.CV_64F, 0, 1, ksize=5) 
laplacian_img = cv2.Laplacian(image, cv2.CV_64F) 
canny_img = cv2.Canny(image, 30, 200) 
  1. 显示输入图像及其对应的输出:
cv2.imshow('Original', image) 
cv2.imshow('horizontal Sobel', horizontal_sobel) 
cv2.imshow('vertical Sobel', vertical_sobel) 
cv2.imshow('Laplacian image', laplacian_img) 
cv2.imshow('Canny image', canny_img) 
  1. 等待操作员的指示:
cv2.waitKey() 
  1. 显示输入图像和相应的结果:
cv2.imshow('Original', image) 
cv2.imshow('horizontal Sobel', horizontal_sobel) 
cv2.imshow('vertical Sobel', vertical_sobel) 
cv2.imshow('Laplacian image', laplacian_img) 
cv2.imshow('Canny image', canny_img) 
  1. 等待操作员的指示:
cv2.waitKey()
  1. 用于执行Detecting_edges.py Python 程序文件的命令,以及输入图像(baby.jpg)如下所示:

  1. 执行Detecting_edges.py文件后获得的输入图像和水平 Sobel 滤波器输出如下所示:

  1. 执行Detecting_edges.py文件后获得的垂直 Sobel 滤波器输出和拉普拉斯图像输出如下所示:

以下是拉普拉斯图像输出:

  1. 执行Detecting_edges.py文件后获得的Canny边缘检测输出如下所示:

它是如何工作的...

读者可以参考以下文档,了解边缘检测是什么,以及它对测试图片的影响:

citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.301.927

另请参见

请参阅以下文档:

直方图均衡化

直方图均衡化用于增强图像的可见性和对比度。它通过改变图像的强度来执行。这些程序在这里有清晰的描述。

如何做...

  1. 导入必要的包:
import sys 
import cv2 
import numpy as np 
  1. 加载输入图像:
in_file = sys.argv[1] 
image = cv2.imread(in_file) 
  1. 将 RGB 图像转换为灰度图像:
image_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 
cv2.imshow('Input grayscale image', image_gray) 
  1. 调整灰度图像的直方图:
image_gray_histoeq = cv2.equalizeHist(image_gray) 
cv2.imshow('Histogram equalized - grayscale image', image_gray_histoeq) 
  1. 调整 RGB 图像的直方图:
image_yuv = cv2.cvtColor(image, cv2.COLOR_BGR2YUV) 
image_yuv[:,:,0] = cv2.equalizeHist(image_yuv[:,:,0]) 
image_histoeq = cv2.cvtColor(image_yuv, cv2.COLOR_YUV2BGR) 
  1. 显示输出图像:
cv2.imshow('Input image', image) 
cv2.imshow('Histogram equalized - color image', image_histoeq) 
cv2.waitKey()
  1. 用于执行histogram.py Python 程序文件的命令,以及输入图像(finger.jpg)如下所示:

  1. 用于执行histogram.py文件的输入图像如下所示:

  1. 执行histogram.py文件后获得的直方图均衡化灰度图像如下所示:

  1. 执行histogram.py文件后获得的直方图均衡化彩色图像如下所示:

在图像中检测角点

角点是图像中用于提取推断图像内容的特殊特征的边界。角点检测经常用于图像配准、视频跟踪、图像拼接、运动检测、3D 建模、全景拼接和物体识别。

如何做...

  1. 导入必要的包:
import sys 
import cv2 
import numpy as np 
  1. 加载输入图像:
in_file = sys.argv[1] 
image = cv2.imread(in_file) 
cv2.imshow('Input image', image) 
image_gray1 = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 
image_gray2 = np.float32(image_gray1) 
  1. 实现 Harris 角点检测方案:
image_harris1 = cv2.cornerHarris(image_gray2, 7, 5, 0.04) 
  1. 膨胀输入图像并构造角点:
image_harris2 = cv2.dilate(image_harris1, None) 
  1. 实现图像阈值处理:
image[image_harris2 > 0.01 * image_harris2.max()] = [0, 0, 0] 
  1. 显示输入图像:
cv2.imshow('Harris Corners', image) 
  1. 等待操作员的指示:
cv2.waitKey() 
  1. 用于执行Detecting_corner.py Python 程序文件的命令,以及输入图像(box.jpg)如下所示:

  1. 用于执行Detecting_corner.py文件的输入图像如下所示:

  1. 执行Detecting_corner.py文件后获得的 Harris 角点如下所示:

要了解它如何作用于输入图片,请参考以下内容:

  • 图像角点检测涉及在给定图片中找到边缘/角点。它可以用于从灰度和 RGB 图片中提取重要的形状特征。参考这篇关于边缘和角点检测的调查论文:

pdfs.semanticscholar.org/24dd/6c2c08f5601e140aad5b9170e0c7485f6648.pdf

第六章:构建人脸检测器和人脸识别应用程序

本章介绍以下主题:

  • 人脸识别系统简介

  • 构建人脸检测器应用程序

  • 构建人脸识别应用程序

  • 人脸识别系统的应用

介绍

近年来,人脸识别已成为最热门的研究领域之一。人脸识别系统是一种具有检测和识别人脸能力的计算机程序。为了识别一个人,它考虑他们独特的面部特征。最近,它已被应用于多个安全和监控设施,以确保高风险区域、住宅区、私人和公共建筑等的安全。

构建人脸检测器应用程序

在本节中,我们讨论了如何从网络摄像头图像中检测人脸。需要将 USB 网络摄像头连接到树莓派 3 上,以实现实时人脸检测。

如何做...

  1. 导入必要的包:
import cv2 
import numpy as np 
  1. 加载人脸级联文件:
frontalface_cascade= cv2.CascadeClassifier('haarcascade_frontalface_alt.xml') 
  1. 检查人脸级联文件是否已加载:
if frontalface_cascade.empty(): 
  raiseIOError('Unable to load the face cascade classifier xml file') 
  1. 初始化视频捕获对象:
capture = cv2.VideoCapture(0) 
  1. 定义缩放因子:
scale_factor = 0.5 
  1. 直到按下Esc键为止执行操作:
# Loop until you hit the Esc key 
while True: 
  1. 捕获当前帧并调整大小:
  ret, frame = capture.read() 
  frame = cv2.resize(frame, None, fx=scale_factor, fy=scale_factor,  
            interpolation=cv2.INTER_AREA) 
  1. 将图像帧转换为灰度:
  gray_image = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) 
  1. 在灰度图像上运行人脸检测器:
  face_rectangle = frontalface_cascade.detectMultiScale(gray_image, 1.3, 5)
  1. 绘制矩形框:
  for (x,y,w,h) in face_rectangle: 
    cv2.rectangle(frame, (x,y), (x+w,y+h), (0,255,0), 3) 
  1. 显示输出图像:
    cv2.imshow('Face Detector', frame) 
  1. 检查是否按下了Esc键以终止操作:
  a = cv2.waitKey(1) 
  if a == 10: 
    break 
  1. 停止视频捕获并终止操作:
capture.release() 
cv2.destroyAllWindows() 

人脸检测系统中获得的结果如下所示:

构建人脸识别应用程序

人脸识别是在人脸检测之后执行的一种技术。检测到的人脸与数据库中存储的图像进行比较。它从输入图像中提取特征并将其与数据库中存储的人脸特征进行匹配。

如何做...

  1. 导入必要的包:
import cv2 
import numpy as np   
from sklearn import preprocessing 
  1. 加载编码和解码任务运算符:
class LabelEncoding(object): 
  # Method to encode labels from words to numbers 
  def encoding_labels(self, label_wordings): 
    self.le = preprocessing.LabelEncoder() 
    self.le.fit(label_wordings) 
  1. 为输入标签实现从单词到数字的转换:
  def word_to_number(self, label_wordings): 
    return int(self.le.transform([label_wordings])[0]) 
  1. 将输入标签从数字转换为单词:
  def number_to_word(self, label_number): 
    return self.le.inverse_transform([label_number])[0] 
  1. 从输入路径提取图像和标签:
def getting_images_and_labels(path_input): 
  label_wordings = [] 
  1. 迭代输入路径的过程并附加文件:
  for roots, dirs, files in os.walk(path_input): 
    for fname in (x for x in files if x.endswith('.jpg')): 
      fpath = os.path.join(roots, fname) 
      label_wordings.append(fpath.split('/')[-2])
  1. 初始化变量并解析输入寄存器:
  images = [] 
  le = LabelEncoding() 
  le.encoding_labels(label_wordings) 
  labels = [] 
  # Parse the input directory 
  for roots, dirs, files in os.walk(path_input): 
    for fname in (x for x in files if x.endswith('.jpg')): 
      fpath = os.path.join(roots, fname) 
  1. 读取灰度图像:
      img = cv2.imread(fpath, 0)  
  1. 提取标签:
      names = fpath.split('/')[-2] 
  1. 执行人脸检测:
      face = faceCascade.detectMultiScale(img, 1.1, 2, minSize=(100,100)) 
  1. 使用面部矩形迭代该过程:
      for (x, y, w, h) in face: 
        images.append(img[y:y+h, x:x+w]) 
        labels.append(le.word_to_number(names)) 
  return images, labels, le 
if __name__=='__main__': 
  path_cascade = "haarcascade_frontalface_alt.xml" 
  train_img_path = 'faces_dataset/train' 
  path_img_test = 'faces_dataset/test' 
  1. 加载人脸级联文件:
  faceCascade = cv2.CascadeClassifier(path_cascade) 
  1. 使用局部二值模式初始化人脸检测:
  face_recognizer = cv2.createLBPHFaceRecognizer()
  1. 从训练人脸数据集中提取人脸特征:
  imgs, labels, le = getting_images_and_labels(train_img_path) 
  1. 训练人脸检测系统:
  print "nTraining..." 
  face_recognizer.train(imgs, np.array(labels)) 
  1. 测试人脸检测系统:
  print 'nPerforming prediction on test images...' 
  flag_stop = False 
  for roots, dirs, files in os.walk(path_img_test): 
    for fname in (x for x in files if x.endswith('.jpg')): 
      fpath = os.path.join(roots, fname) 
  1. 验证人脸识别系统:
      predicting_img = cv2.imread(fpath, 0) 
            # Detect faces 
      face = faceCascade.detectMultiScale(predicting_img, 1.1,  
                    2, minSize=(100,100)) 
            # Iterate through face rectangles 
      for (x, y, w, h) in face: 
        # Predict the output 
        index_predicted, config = face_recognizer.predict( 
predicting_img[y:y+h, x:x+w]) 
        # Convert to word label 
        person_predicted = le.number_to_word(index_predicted) 
        # Overlay text on the output image and display it 
        cv2.putText(predicting_img, 'Prediction: ' +  person_predicted,  
                        (10,60), cv2.FONT_HERSHEY_SIMPLEX, 2, (255,255,255), 6) 
        cv2.imshow("Recognizing face", predicting_img) 
      a = cv2.waitKey(0) 
      if a == 27: 
        flag = True 
        break 
    if flag_stop: 
      break 

这里显示了人脸识别的输出结果:

工作原理...

人脸识别系统广泛用于实现个人安全系统。读者可以参考文章基于 OpenCV 的人脸检测系统,网址为ieeexplore.ieee.org/document/6242980/

另请参阅用于实时人脸检测系统的人脸检测算法研究,网址为ieeexplore.ieee.org/document/5209668

另请参阅

请参考以下文章:

人脸识别系统的应用

人脸识别广泛应用于安全、医疗保健和营销领域。各行业正在利用深度学习开发新型人脸识别系统,用于识别欺诈、区分人脸和照片之间的差异等。在医疗保健领域,人脸识别结合其他计算机视觉算法用于检测面部皮肤疾病。

第七章:使用 Python 驱动硬件

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

  • 控制 LED

  • 响应按钮

  • 控制关机按钮

  • GPIO 键盘输入

  • 多路复用彩色 LED

  • 使用视觉持久性编写消息

介绍

树莓派计算机的一个关键特性是它能够直接与其他硬件进行接口。树莓派上的通用输入/输出(GPIO)引脚可以控制各种低级电子设备,从发光二极管(LED)到开关、传感器、电机、伺服和额外的显示器。

本章将重点介绍如何连接树莓派与一些简单的电路,并掌握使用 Python 来控制和响应连接的组件。

树莓派硬件接口由板子一侧的 40 个引脚组成。

GPIO 引脚及其布局将根据您拥有的特定型号略有不同。

树莓派 3、树莓派 2 和树莓派 B+都具有相同的 40 针布局。

树莓派 1 代老款(非 plus 型号)有一个 26 针的引脚,与新款模型的 1-26 针相同。

树莓派 2、树莓派 B+和树莓派 Plus GPIO 引脚(引脚功能)

连接器的布局如上图所示;引脚编号从 GPIO 引脚的引脚 1 开始。

引脚 1 位于最靠近 SD 卡的一端,如下图所示:

树莓派 GPIO 引脚位置

在使用 GPIO 引脚时应当小心,因为它还包括电源引脚(3V3 和 5V),以及地线(GND)引脚。所有的 GPIO 引脚都可以用作标准 GPIO,但其中一些还具有特殊功能;这些被标记并用不同颜色突出显示。

工程师通常使用 3V3 标记来指定原理图中的值,以避免使用可能被忽略的小数位(使用 33V 而不是 3.3V 会对电路造成严重损坏)。同样的方法也可以应用于其他组件的值,比如电阻,例如,1.2K 欧姆可以写成 1K2 欧姆。

TX 和 RX 引脚用于串行通信,借助电压级转换器,信息可以通过串行电缆传输到另一台计算机或设备。

我们还有 SDA 和 SCL 引脚,它们能够支持一种名为 I²C 的双线总线通信协议(树莓派 3 和 Plus 板上有两个 I²C 通道:通道 1 ARM,用于通用用途,通道 0 VC,通常用于识别 HAT 模块上连接的硬件)。还有 SPI MOSI、SPI MISO、SPI SCLK、SPI CE0 和 SPI CE1 引脚,支持另一种名为 SPI 的高速数据总线协议。最后,我们有 PWM0/1 引脚,允许生成脉冲宽度调制信号,对于伺服和生成模拟信号非常有用。

然而,在本章中,我们将专注于使用标准的 GPIO 功能。GPIO 引脚布局如下图所示:

树莓派 GPIO 引脚(GPIO.BOARD 和 GPIO.BCM)

树莓派 Rev 2(2014 年 7 月之前)与树莓派 2 GPIO 布局相比有以下不同:

  • 26 个 GPIO 引脚的引脚头(匹配前 26 个引脚)。

  • 引脚头旁边的另一组八个孔(P5)。详细信息如下:

树莓派 Rev 2 P5 GPIO 引脚

  • 原始的树莓派 Rev 1(2012 年 10 月之前)总共只有 26 个 GPIO 引脚(匹配当前树莓派的前 26 个引脚),除了以下细节:

树莓派 Rev 1 GPIO 引脚头的差异

RPi.GPIO库可以使用两种系统之一引用树莓派上的引脚。中间显示的数字是引脚的物理位置,也是在GPIO.BOARD模式下RPi.GPIO库引用的数字。外部的数字(GPIO.BCM)是处理器物理端口的实际引用数字,指示哪些引脚被连接(这就是为什么它们没有特定的顺序)。当模式设置为GPIO.BCM时使用它们,并且它们允许控制 GPIO 引脚以及连接到其他 GPIO 线的任何外围设备。这包括 BCM GPIO 4 上的附加摄像头上的 LED 和板上的状态 LED。但是,这也可能包括用于读/写 SD 卡的 GPIO 线,如果干扰会导致严重错误。

如果您使用其他编程语言访问 GPIO 引脚,编号方案可能会有所不同,因此如果您了解 BCM GPIO 引用,将会很有帮助,它们指的是处理器的物理 GPIO 端口。

请务必查看附录硬件和软件清单,其中列出了本章中使用的所有物品以及您可以从哪里获得它们。

控制 LED

硬件上的hello world等同于 LED 闪烁,这是一个很好的测试,可以确保一切正常工作,并且你已经正确地连接了它。为了让它更有趣,我建议使用红色、蓝色和绿色(RGB)LED,但如果你只有单独的 LED 也可以。

准备工作

你将需要以下设备:

  • 4 x 杜邦母对公补丁线

  • 迷你面包板(170 个连接点)或更大的面包板

  • RGB LED(共阴)/3 个标准 LED(最好是红色、绿色和蓝色)

  • 面包板线(实心线)

  • 3 x 470 欧姆电阻

前面提到的每个组件成本都不会太高,并且可以在其他项目中重复使用。面包板是一个特别有用的物品,可以让你在不需要焊接的情况下尝试自己的电路:

RGB LED、标准 LED 和 RGB 电路的图表

以下图表显示了面包板电路:

连接到 GPIO 引脚的 RGB LED/标准 LED 的接线图有几种不同类型的 RGB LED 可用,因此请检查您组件的数据表以确认引脚顺序和类型。有些是 RGB 的,所以确保你按照相应的方式连接引脚,或者在代码中调整RGB_引脚设置。你也可以获得共阳极变种,这将需要阳极连接到 3V3(GPIO 引脚 1)才能点亮(它们还需要将RGB_ENABLERGB_DISABLE设置为01)。

本书的面包板和组件图是使用一个名为Fritzingwww.fritzing.org)的免费工具创建的;它非常适合规划您自己的树莓派项目。

如何做...

  1. 创建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
  1. RPi.GPIO库将需要sudo权限来访问 GPIO 引脚硬件,因此您需要使用以下命令运行脚本:
sudo python3 ledtest.py  

运行脚本时,您应该看到 LED 的红色、绿色和蓝色部分(或者如果您使用单独的 LED,则分别点亮)。如果没有,请仔细检查您的接线或确认 LED 是否正常工作,方法是暂时将红色、绿色或蓝色线连接到 3V3 引脚(GPIO 引脚 1)。

大多数与硬件相关的脚本都需要sudo命令,因为用户通常不会直接在这么低的层次上控制硬件。例如,设置或清除作为 SD 卡控制器一部分的控制引脚可能会损坏正在写入的数据。因此,出于安全目的,需要超级用户权限,以防止程序意外(或恶意)使用硬件。

工作原理...

要使用 Python 访问 GPIO 引脚,我们导入RPi.GPIO库,该库允许通过模块函数直接控制引脚。我们还需要time模块来暂停程序一定数量的秒。

然后,我们为 LED 的接线和激活状态定义值(请参阅本食谱的有更多...部分中的控制 GPIO 电流段)。

在程序使用 GPIO 引脚之前,我们需要通过指定编号方法(GPIO.BOARD)和方向(GPIO.OUTGPIO.IN)来设置它们(在这种情况下,我们将所有 RGB 引脚设置为输出)。如果引脚配置为输出,我们将能够设置引脚状态;同样,如果它配置为输入,我们将能够读取引脚状态。

接下来,我们使用GPIO.ouput()来控制引脚,指定 GPIO 引脚的编号和我们希望它处于的状态(1 = 高/开启,0 = 低/关闭)。我们打开每个 LED,等待五秒,然后关闭它。

最后,我们使用GPIO.cleanup()将 GPIO 引脚恢复到它们的原始默认状态,并释放对引脚的控制,以供其他程序使用。

有更多...

在树莓派上使用 GPIO 引脚必须小心,因为这些引脚直接连接到树莓派的主处理器,没有额外的保护。必须小心使用,因为任何错误的接线可能会损坏树莓派处理器,并导致其完全停止工作。

或者,您可以使用许多直接插入 GPIO 引脚排针的模块之一(减少接线错误的机会):

例如,Pi-Stop 是一个简单的预制 LED 板,模拟了一组交通信号灯,旨在成为那些对控制硬件感兴趣但又想避免损坏树莓派的人的一个过渡阶段。掌握了基础知识后,它也是一个出色的指示器,有助于调试。

只需确保您在ledtest.py脚本中更新LED CONFIG引脚引用,以引用您使用的硬件的引脚布局和位置。

请参阅附录中的硬件和软件清单,了解树莓派硬件零售商的清单。

控制 GPIO 电流

每个 GPIO 引脚在烧毁之前只能处理一定电流(单个引脚最大 16mA,总共 30mA),同样,RGB LED 的电流应限制在 100mA 以下。通过在 LED 之前或之后添加电阻,我们将能够限制通过 LED 的电流并控制其亮度(更大的电流将使 LED 更亮)。

由于我们可能希望同时点亮多个 LED,因此我们通常会尽量将电流设置得尽可能低,同时仍然提供足够的功率点亮 LED。

我们可以使用欧姆定律来告诉我们需要多少电阻来提供特定的电流。该定律如下图所示:

欧姆定律:电路中电流、电阻和电压之间的关系

我们将以最小电流(3mA)和最大电流(16mA)为目标,同时仍然从每个 LED 产生相当明亮的光。为了获得 RGB LED 的平衡输出,我测试了不同的电阻,直到它们提供了接近白光(通过卡片查看)。每个 LED 选择了 470 欧姆的电阻(您的 LED 可能略有不同):

需要电阻器来限制通过 LED 的电流

电阻器上的电压等于 GPIO 电压(Vgpio = 3.3V)减去特定 LED 的电压降(Vfwd);然后我们可以使用这个电阻来计算每个 LED 使用的电流,如下面的公式所示:

我们可以计算每个 LED 的电流

响应按钮

许多使用树莓派的应用程序要求在不需要连接键盘和屏幕的情况下激活操作。 GPIO 引脚为树莓派提供了一种优秀的方式,使其可以通过您自己的按钮和开关进行控制,而无需鼠标/键盘和屏幕。

准备工作

您将需要以下设备:

  • 2 x DuPont 母对公跳线

  • 迷你面包板(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(地)和 3V3 之间的输入。一些处理器使用 0V 到 5V 之间的电压,因此需要额外的组件才能安全地与它们接口。除非确定安全,否则永远不要连接使用 5V 的输入或组件,否则会损坏树莓派的 GPIO 端口。

上拉和下拉电阻电路

先前的代码设置了 GPIO 引脚使用内部上拉电阻。如果 GPIO 引脚上没有上拉电阻(或下拉电阻),电压可以在 3V3 和 0V 之间自由浮动,实际逻辑状态保持不确定(有时为 1,有时为 0)。

树莓派的内部上拉电阻为 50K 欧姆至 65K 欧姆,下拉电阻为 50K 欧姆至 65K 欧姆。外部上拉/下拉电阻通常用于 GPIO 电路(如下图所示),通常使用 10K 欧姆或更大的电阻出于类似的原因(当它们不活动时提供非常小的电流吸收)。

上拉电阻允许通过 GPIO 引脚流动少量电流,并且在开关未按下时提供高电压。当按下开关时,小电流被流向 0V 的大电流所取代,因此我们在 GPIO 引脚上得到低电压。开关在按下时为活动低电平和逻辑 0。它的工作原理如下图所示:

上拉电阻电路

下拉电阻的工作方式相同,只是开关为活动高电平(按下时 GPIO 引脚为逻辑 1)。它的工作原理如下图所示:

下拉电阻电路

保护电阻

除了开关外,电路还包括与开关串联的电阻,以保护 GPIO 引脚,如下图所示:

GPIO 保护限流电阻

保护电阻的目的是保护 GPIO 引脚,如果它被意外设置为输出而不是输入。例如,假设我们的开关连接在 GPIO 和地之间。现在 GPIO 引脚被设置为输出并打开(驱动到 3V3),一旦我们按下开关,没有电阻的情况下,GPIO 引脚将直接连接到 0V。 GPIO 仍然会尝试将其驱动到 3V3;这将导致 GPIO 引脚烧毁(因为它将使用太多电流来驱动引脚到高状态)。如果我们在这里使用 1K 欧姆电阻,引脚可以使用可接受的电流驱动高(I = V/R = 3.3/1K = 3.3 毫安)。

受控关机按钮

树莓派应该始终正确关机,以避免 SD 卡损坏(在对卡进行写操作时断电)。如果您没有连接键盘或屏幕(可能正在运行自动化程序或通过网络远程控制),这可能会造成问题,因为您无法输入命令或查看您正在做什么。通过添加我们自己的按钮和 LED 指示灯,我们可以轻松地命令关机和重启,然后再次启动以指示系统处于活动状态。

准备工作

您将需要以下设备:

  • 3 x DuPont 母对公跳线

  • 迷你面包板(170 个连接点)或更大的面包板

  • 按钮开关(瞬时闭合)

  • 通用 LED

  • 2 x 470 欧姆电阻

  • 面包板导线(实心)

关机电路的整个布局将如下图所示:

受控关机电路布局

如何操作...

  1. 创建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
  1. 要使这个脚本自动运行(一旦我们测试过它),我们可以将脚本放在~/bin中(如果只想复制它,可以使用cp而不是mv),并使用以下代码将其添加到crontab中:
mkdir ~/bin 
mv shtdwn.py ~/bin/shtdwn.py  
crontab -e 
  1. 在文件末尾,我们添加以下代码:
@reboot sudo python3 ~/bin/shtdwn.py 

它是如何工作的...

这次,当我们设置 GPIO 引脚时,我们将与关机按钮连接的引脚定义为输入,与 LED 连接的引脚定义为输出。我们打开 LED 以指示系统正在运行。

通过将DEBUG标志设置为True,我们可以测试脚本的功能,而不会导致实际关闭(通过读取终端消息);我们只需要确保在实际使用脚本时将DEBUG设置为False

我们进入一个while循环,并每秒检查引脚,以查看 GPIO 引脚是否设置为LOW(即检查开关是否被按下);如果是,我们就进入doShutdown()函数。

程序将等待三秒,然后再次测试按钮是否仍然被按下。如果按钮不再被按下,我们将返回到之前的while循环。但是,如果在三秒后它仍然被按下,程序将闪烁 LED 并触发关闭(还会使用flite提供音频警告)。

当我们对脚本的运行状态感到满意时,我们可以禁用DEBUG标志(将其设置为False),并将脚本添加到crontab中。crontab是一个在后台运行的特殊程序,允许我们在系统启动时(@reboot)安排程序和操作的特定时间、日期或周期性。这使得脚本可以在每次树莓派上电时自动启动。当我们按住关机按钮超过三秒时,它会安全地关闭系统并进入低功耗状态(LED 在此之前会关闭,表明很快就可以拔掉电源)。要重新启动树莓派,我们简单地拔掉电源;这将重新启动系统,当树莓派加载完成时,LED 会亮起。

还有更多...

我们可以通过添加额外的功能并利用额外的 GPIO 连接(如果可用)来进一步扩展这个示例。

重置和重新启动树莓派

树莓派上有用于安装复位标头的孔(在树莓派 3/2 上标有RUN,在树莓派 1 型 A 和 B Rev 2 上标有P6)。复位引脚允许使用按钮而不是每次都拔掉微型 USB 连接器来重置设备的电源:

树莓派复位标头-左边是树莓派 A/B 型(Rev2),右边是树莓派 3

要使用它,您需要将一根导线或引脚排焊接到树莓派上,并连接一个按钮(或每次在两个孔之间短暂触碰一根导线)。或者,我们可以扩展我们之前的电路,如下图所示:

受控关闭电路布局和复位按钮

我们可以将这个额外的按钮添加到我们的电路中,它可以连接到复位标头(这是树莓派 3 上最靠近中间的孔,其他型号上最靠近边缘的孔)。当暂时将此引脚拉低连接到地(例如旁边的孔或 GPIO 标头的第 6 引脚等其他地点),将重置树莓派并允许它在关闭后再次启动。

添加额外功能

由于现在脚本一直监视关机按钮,我们可以同时添加额外的按钮/开关/跳线来监视。这将允许我们通过改变输入来触发特定程序或设置特定状态。以下示例允许我们轻松地在自动 DHCP 网络(默认网络设置)和使用直接 IP 地址之间进行切换,就像第一章“使用树莓派 3 计算机入门”中的“直接连接到笔记本电脑或计算机”配方中使用的那样。

将以下组件添加到上一个电路中:

  • 一个 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的目录中。

  1. 完成后,可以使用以下命令删除 ZIP 文件:
rm master.zip  
  1. 使用以下命令安装所需的软件包(如果已经安装了它们,apt-get命令将忽略它们):
sudo apt-get install python3-setuptools python3-dev
sudo apt-get install libudev-dev  
  1. 使用以下命令编译和安装uInput
cd python-uinput-master
sudo python3 setup.py install  
  1. 最后,使用以下命令加载新的uinput内核模块:
sudo modprobe uinput  

为了确保在启动时加载,我们可以使用以下命令将uinput添加到modules文件中:

sudo nano /etc/modules  

在文件中新建一行并保存(Ctrl + X, Y)。

  1. 使用以下设备创建以下电路:
  • 面包板(半尺寸或更大)

  • 7 根 DuPont 母对公排线

  • 六个按钮

  • 6 个 470 欧姆电阻

  • 面包板导线(实心线)

GPIO 键盘电路布局

键盘电路也可以通过将组件焊接到 Vero 原型板(也称为条板)中,制成永久电路,如下图所示:

GPIO 键盘 Pi 硬件模块这个电路可以从PiHardware.com购买成套焊接套件。

  1. 通过将适当的按钮与适当的引脚相匹配,将电路连接到树莓派 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恢复它(这将使其在后台运行)。

更多信息...

我们可以使用uinput来为其他程序提供硬件控制,包括那些需要鼠标输入的程序。

生成其他按键组合

您可以在文件中创建几种不同的键映射以支持不同的程序。例如,events_z80键映射对于像Fuse这样的光谱模拟器非常有用(浏览raspi.tv/2012/how-to-install-fuse-zx-spectrum-emulator-on-raspberry-pi获取更多详细信息)。events_omx键映射适用于使用以下命令控制通过 OMXPlayer 播放的视频:

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。我们将使用五个 RGB LED,这些 LED 被布线,以便我们只需要使用八个 GPIO 引脚来控制它们的红色、绿色和蓝色元素,使用一种称为硬件多路复用的方法(请参阅本食谱的硬件多路复用子部分中的更多信息部分)。

准备工作

您将需要以下图片中显示的 RGB LED 模块:

PiHardware.com 的 RGB LED 模块

正如您在上面的照片中所看到的,来自pihardware.com/的 RGB LED 模块带有 GPIO 引脚和杜邦母对母电缆用于连接。虽然有两组从 1 到 5 标记的引脚,但只需要连接一侧。

或者,您可以使用五个共阳极 RGB LED、3 个 470 欧姆电阻和一个 Vero 原型板(或大型面包板)来重新创建自己的电路。电路将如下图所示:

RGB LED 模块的电路图严格来说,我们应该在这个电路中使用 15 个电阻(每个 RGB LED 元件一个),这样可以避免 LED 共用同一个电阻的干扰,并且在一起开启时也会延长 LED 的寿命。然而,使用这种方法只有轻微的优势,特别是因为我们打算独立驱动每个 RGB LED,以实现多种颜色效果。

您需要将电路连接到树莓派 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] 
  1. 定义使用以下代码设置 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()
  1. 使用以下代码定义我们的实用程序函数来帮助控制 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()
  1. 创建一个测试函数来演示模块的功能:
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 脚本导入的。

这使我们能够定义一个特殊的测试代码,只有在直接加载和运行文件时才执行。如果我们将此文件作为另一个脚本中的模块包含,那么此代码将不会被执行。

与以前一样,我们将使用try/finally来允许我们始终执行清理操作,即使我们提前退出。

为了测试脚本,我们将设置 LED 依次以各种颜色点亮。

还有更多...

我们可以通过一次打开 RGB LED 的一个或多个部分来创建几种不同的颜色。然而,通过一些巧妙的编程,我们可以创建整个颜色谱。此外,我们可以似乎同时在每个 LED 上显示不同的颜色。

硬件复用

LED 需要在阳极侧施加高电压,在阴极侧施加低电压才能点亮。电路中使用的 RGB LED 是共阳极的,因此我们必须在 RGB 引脚上施加高电压(3V3),在阴极引脚上施加低电压(0V)(分别连接到每个 LED 的 1 到 5 引脚)。

阴极和 RGB 引脚状态如下:

阴极和 RGB 引脚状态

因此,我们可以启用一个或多个 RGB 引脚,但仍然控制点亮哪个 LED。我们启用我们想要点亮的 LED 的引脚,并禁用我们不想点亮的引脚。这使我们可以使用比控制每个 RGB 线需要的引脚少得多的引脚。

显示随机图案

我们可以向我们的库中添加新的函数以产生不同的效果,例如生成随机颜色。以下函数使用randint()来获取 1 到颜色数量之间的值。我们忽略任何超出可用颜色数量的值,以便我们可以控制 LED 关闭的频率。执行以下步骤以添加所需的函数:

  1. 使用以下代码将random模块中的randint()函数添加到rgbled.py脚本中:
from random import randint
  1. 现在使用以下代码添加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) 
  1. 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 的组合)。幸运的是,树莓派等计算机可以很容易地做到这一点,甚至允许我们组合 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提供了对颜色的平滑过渡。

  1. 接下来,我们需要创建一个名为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) 
  1. 现在我们可以创建一个新的脚本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 或在圆圈中移动 LED 来在空中显示图像。这种效果的原理是因为您的眼睛无法调整得足够快,以分离出单独的闪光,因此您观察到一个合并的图像(显示的消息或图片):

使用 RGB LED 的视觉持续性

准备工作

这个配方使用了前一个配方中使用的 RGB LED 套件;您还需要以下额外的物品:

  • 面包板(半尺寸或更大)

  • 2 x DuPont 母对公跳线

  • 倾斜开关(适合滚珠类型)

  • 1 x 470 欧姆电阻(R_Protect)

  • 面包板线(实心线)

倾斜开关应添加到 RGB LED(如准备工作部分的多路复用彩色 LED配方中所述)。倾斜开关的接线如下:

倾斜开关连接到 GPIO 输入(GPIO 引脚 24)和 Gnd(GPIO 引脚 6)

为了重现 POV 图像,您需要能够快速移动 LED 并来回倾斜开关。请注意倾斜开关安装在侧面倾斜,因此当向左移动时开关将打开。建议将硬件安装在一根木头或类似设备上。您甚至可以使用便携式 USB 电池组和 Wi-Fi dongle 来通过远程连接为树莓派供电和控制(有关详细信息,请参见第一章中的通过网络远程连接树莓派使用 SSH(和 X11 转发)配方):

持续视觉硬件设置

您还需要已完成的rgbled.py文件,我们将在如何操作...部分进一步扩展它。

如何操作...

  1. 创建一个名为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 
  1. 您可以通过直接运行以下命令来测试脚本:
sudo python3 tilt.py
  1. 将以下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) 
  1. 现在,我们将创建以下文件,名为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 
  1. 添加一个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
  1. 添加一个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 
  1. 接下来,我们定义一个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) 
  1. 最后,我们创建一个main()函数来执行所需的每个步骤:

  2. 设置硬件组件(RGB LED 和倾斜开关)。

  3. 阅读letters.txt文件。

  4. 定义 LED 字母模式的字典。

  5. 生成一个缓冲区来表示所需的消息。

  6. 使用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 
  1. 创建以下文件,名为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()”将打开并读取给定文件的内容。然后使用“processFileContent()”返回一个包含文件中定义的字母对应的 LED 图案的 Python 字典。处理文件时,会处理文件中的每一行,忽略包含“#”字符的任何行,并检查“”字符以指示接下来的 LED 图案的名称。处理文件后,我们得到一个包含 LED 图案的 Python 字典,其中包含'_''C''B''K''O'字符。

'_': [[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 效果。

第八章:感知和显示真实世界的数据

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

  • 使用 I2C 总线的设备

  • 使用模拟数字转换器读取模拟数据

  • 记录和绘制数据

  • 通过 I/O 扩展器扩展树莓派 GPIO

  • 在 SQLite 数据库中捕获数据

  • 查看来自您自己的 Web 服务器的数据

  • 感知和发送数据到在线服务

介绍

在本章中,我们将学习如何收集来自现实世界的模拟数据并对其进行处理,以便在程序中显示、记录、绘制和共享数据,并利用这些数据。

我们将通过使用树莓派的 GPIO 连接来扩展树莓派的功能,与模拟数字转换器(ADC)、LCD 字母显示器和数字端口扩展器进行接口。

使用 I2C 总线的设备

树莓派可以支持多种高级协议,可以轻松连接各种设备。在本章中,我们将专注于最常见的总线,称为 I-squared-C(I²C)。它提供了一个用于通过两根导线与设备通信的中速总线。在本节中,我们将使用 I²C 与 8 位 ADC 进行接口。该设备将测量模拟信号,将其转换为 0 到 255 之间的相对值,并将该值作为数字信号(由 8 位表示)通过 I²C 总线发送到树莓派。

I²C 的优势可以总结如下:

  • 即使在总线上有许多设备的情况下,也能保持低引脚/信号计数

  • 适应不同从设备的需求

  • 容易支持多个主设备

  • 包括 ACK/NACK 功能以改进错误处理

准备工作

并非所有树莓派镜像都启用了 I²C 总线;因此,我们需要启用模块并安装一些支持工具。Raspbian 的新版本使用设备树来处理硬件外围设备和驱动程序。

为了使用 I²C 总线,我们需要在bootconfig.txt文件中启用 ARM I²C。

您可以使用以下命令自动执行此操作:

sudo raspi-config

从菜单中选择高级选项,然后选择 I²C,如下截图所示。当询问时,选择是以启用接口,然后点击是以默认加载模块:

raspi-config 菜单

从菜单中选择 I2C,选择是以启用接口并默认加载模块。

raspi-config程序通过修改/boot/config.txt以包括dtparam=i2c_arm=on来启用I2C_ARM接口。另一种总线(I2C_VC)通常保留用于与树莓派 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  

最后,在连接硬件之前关闭树莓派,以便应用更改,如下所示:

sudo halt  

您将需要一个 PCF8591 模块(这些的零售商在附录硬件和软件清单中列出)或者您可以单独获取 PCF8591 芯片并构建自己的电路(有关电路的详细信息,请参阅还有更多...部分):

来自 dx.com 的 PCF8591 ADC 和传感器模块

将 GND、VCC、SDA 和 SCL 引脚连接到树莓派的 GPIO 引脚头,如下所示:

树莓派 GPIO 引脚上的 I2C 连接您可以通过研究设备的数据表找出要发送/读取的消息以及用于控制设备的寄存器,使用相同的 I²C 工具/代码与其他 I²C 设备。

操作步骤...

  1. i2cdetect命令用于检测 I²C 设备(--y选项跳过有关可能干扰连接到 I²C 总线的其他硬件的警告)。以下命令用于扫描两个总线:
sudo i2cdetect -y 0
sudo i2cdetect -y 1 
  1. 根据您的树莓派板子版本,设备的地址应该在总线 0 上列出(适用于 Model B Rev1 板)或总线 1 上(适用于树莓派 2 和 3,以及树莓派 1 Model A 和 Model B Revision 2)。默认情况下,PCF8591 地址是0x48
要使用的 I²C 总线号 总线 00 总线 11
树莓派 2 和 3 HAT ID(I2C_VC) GPIO(I2C_ARM)
Model A 和 Model B Revision 2 P5 GPIO
Model B Revision 1 GPIO N/A
  1. 以下屏幕截图显示了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=的行(这将禁用对新设备树配置的支持,并禁用对某些树莓派 HAT 附加板的支持)

  • 使用lsmod检查模块是否已加载,并查找i2c-bcm2708i2c_dev

  1. 使用检测到的总线号(01)和设备地址(0x48),使用i2cget从设备读取(上电或通道更改后,您需要两次读取设备才能看到最新值),如下所示:
sudo i2cget -y 1 0x48
sudo i2cget -y 1 0x48 
  1. 要从通道1读取(这是模块上的温度传感器),我们可以使用i2cset0x01写入 PCF8591 控制寄存器。同样,使用两次读取来从通道1获取新样本,如下所示:
sudo i2cset -y 1 0x48 0x01
sudo i2cget -y 1 0x48
sudo i2cget -y 1 0x48
  1. 要循环遍历每个输入通道,请使用i2cset将控制寄存器设置为0x04,如下所示:
sudo i2cset -y 1 0x48 0x04
  1. 我们还可以使用以下命令控制 AOUT 引脚,将其完全打开(点亮 LED D1):
sudo i2cset -y 1 0x48 0x40 0xff 
  1. 最后,我们可以使用以下命令将其完全关闭(关闭 LED D1):
sudo i2cset -y 1 0x48 0x40 0x00  

工作原理...

设备上电后的第一次读取将返回0x80,并且还将触发通道 0 的新样本。如果再次读取,它将返回先前读取的样本并生成新样本。每次读取都将是一个 8 位值(范围从0255),表示电压到 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 0100来表示(0x表示值以十六进制表示,b 表示二进制数)。

二进制数中的位从右到左进行计数,从 0 开始 - 即,MSB 7 6 5 4 3 2 1 0 LSB。

第 7 位被称为最高有效位MSB),第 0 位被称为最低有效位LSB)。因此,通过设置第 2 位,我们最终得到b0000 0100(即0x04)。

还有更多...

I²C 总线允许我们只使用少量线路轻松连接多个设备。PCF8591 芯片可用于将自己的传感器连接到模块或仅连接芯片。

使用多个 I2C 设备

I²C 总线上的所有命令都是针对特定的 I²C 设备的(许多设备可以选择将一些引脚设为高电平或低电平以选择附加地址,并允许多个设备存在于同一总线上)。每个设备必须具有唯一地址,以便一次只有一个设备会做出响应。PCF8591 的起始地址是0x48,通过三个地址引脚可选择附加地址为0x4F。这允许在同一总线上使用多达八个 PCF8591 设备。

如果决定使用位于 GPIO 引脚 27 和 28(或位于 Model A 和 Revision 2 Model B 设备的 P5 标头)的 I2C_VC 总线,则可能需要在 I²C 线和 3.3V 之间添加 1k8 欧姆的上拉电阻。这些电阻已经存在于 GPIO 连接器上的 I²C 总线上。但是,一些 I²C 模块,包括 PCF8591 模块,已经安装了自己的电阻,因此可以在没有额外电阻的情况下工作。

I2C 总线和电平转换

I²C 总线由两根线组成,一根数据线(SDA)和一根时钟线(SCL)。两根线都通过上拉电阻被被动地拉到 VCC(在树莓派上,这是 3.3V)。树莓派将通过每个周期将时钟线拉低来控制时钟,数据线可以被树莓派拉低以发送命令,或者被连接的设备拉低以回应数据:

树莓派 I²C 引脚包括 SDA 和 SCL 上的上拉电阻

由于从机设备只能将数据线拉到GND,因此设备可以由 3.3V 甚至 5V 供电,而不会有驱动 GPIO 引脚电压过高的风险(请记住,树莓派 GPIO 无法处理超过 3.3V 的电压)。只要设备的 I²C 总线能够识别逻辑最大值为 3.3V 而不是 5V,这应该可以工作。I²C 设备不能安装自己的上拉电阻,因为这会导致 GPIO 引脚被拉到 I²C 设备的供电电压。

请注意,本章中使用的 PCF8591 模块已安装了电阻;因此,我们只能使用VCC = 3V3。双向逻辑电平转换器可用于克服逻辑电平的任何问题。其中一种设备是Adafruit I²C 双向逻辑电平转换模块,如下图所示:

Adafruit I²C 双向逻辑电平转换模块

除了确保任何逻辑电压适合您使用的设备之外,它还将允许总线在更长的导线上延伸(电平转换器还将充当总线中继)。

仅使用 PCF8591 芯片或添加替代传感器

下图显示了 PCF8591 模块不带传感器的电路图:

PCF8591 模块的电路图,不带传感器附件

如您所见,除了传感器外,只有五个额外的元件。我们有一个电源滤波电容(C1)和一个带有限流电阻(R5)的电源指示 LED(D2),所有这些都是可选的。

请注意,该模块包括两个 10K 上拉电阻(R8 和 R9)用于 SCL 和 SDA 信号。但是,由于树莓派上的 GPIO I²C 连接也包括上拉电阻,因此模块上不需要这些电阻(并且可以被移除)。这也意味着我们应该只将该模块连接到 VCC = 3.3V(如果我们使用 5V,则 SCL 和 SDA 上的电压将约为 3.56V,这对于树莓派的 GPIO 引脚来说太高)。

PCF891 模块上的传感器都是电阻性的,因此模拟输入上的电压电平将随着传感器电阻的变化在 GND 和 VCC 之间变化:

电位分压电路。这提供了与传感器电阻成比例的电压。

该模块使用一种称为电位分压器的电路。顶部的电阻平衡了底部传感器提供的电阻,以提供介于VCCGND之间的电压。

电位器的输出电压(V[out])可以计算如下:

R[t]和 R[b]分别是顶部和底部的电阻值,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上的数据表。

使用模拟数字转换器读取模拟数据

在命令行中使用的 I²C 工具(在上一节中使用)对于调试 I²C 设备非常有用,但对于 Python 来说并不实用,因为它们会很慢并且需要大量的开销。幸运的是,有几个 Python 库提供了 I²C 支持,允许有效地使用 I²C 与连接的设备进行通信并提供简单的操作。

我们将使用这样的库来创建我们自己的 Python 模块,它将允许我们快速轻松地从 ADC 设备获取数据并在我们的程序中使用它。该模块设计得非常灵活,可以在不影响其余示例的情况下放置其他硬件或数据源。

准备工作

要使用 Python 3 使用 I²C 总线,我们将使用Gordon Henderson 的 WiringPi2(有关更多详细信息,请参见wiringpi.com/)。

安装wiringpi2的最简单方法是使用 Python 3 的pippip是 Python 的软件包管理器,其工作方式类似于apt-get。您希望安装的任何软件包都将从在线存储库自动下载并安装。

要安装pip,请使用以下命令:

sudo apt-get install python3-dev python3-pip  

然后,使用以下命令安装wiringpi2

sudo pip-3.2 install wiringpi2

安装完成后,您应该看到以下内容,表示成功:

成功安装 WiringPi2

您需要将 PCF8591 模块连接到树莓派的 I²C 连接上,就像之前使用的那样:

PCF8591 模块和引脚连接到树莓派 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"] 
  1. 创建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 
  1. 在类中,定义一个函数以提供通道名称列表,如下所示:
def getName(self): 
  return self.NAME
  1. 定义另一个函数(仍然作为类的一部分)以返回 ADC 通道的新样本集,如下所示:
def getNew(self): 
  data=[] 
  for ch in ADC_CH: 
    time.sleep(BUS_GAP) 
    data.append(self.i2c.read(self.devADC)) 
  return data 
  1. 最后,在设备类之后,创建一个测试函数来测试我们的新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 以循环通过通道,但是我们使用wiringpi2I2C对象的readwrite函数,而不是i2cgeti2cset。初始化后,设备将准备好读取每个通道上的模拟信号。

该类还将有两个成员函数。第一个函数getName()返回一个通道名称列表(我们可以用它来将数据与其来源进行关联),第二个函数getNew()返回所有通道的新数据集。数据是使用i2c.read()函数从 ADC 读取的,由于我们已经将其放入循环模式,每次读取都将来自下一个通道。

由于我们计划稍后重用此类,因此我们将使用if __name__测试来允许我们定义在直接执行文件时要运行的代码。在我们的main()函数中,我们创建 ADC,这是我们新设备类的一个实例。如果需要,我们可以选择选择非默认地址;否则,将使用芯片的默认地址。我们使用getName()函数打印出通道的名称,然后我们可以从ADC(使用getNew())收集数据并显示它们。

还有更多...

以下允许我们在data_adc.py中定义设备类的另一个版本,以便可以在 ADC 模块的位置使用它。这将允许在本章的其余部分中尝试而无需任何特定的硬件。

无硬件收集模拟数据

如果您没有可用的 ADC 模块,则可以从树莓派内部获得大量可用数据,可以代替使用。

创建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 

前面的脚本允许我们使用以下命令从树莓派中收集系统信息(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包括,只需将data_adc包括替换为data_local)。

记录和绘制数据

现在我们能够采样和收集大量数据,重要的是我们能够捕获和分析它。为此,我们将使用一个名为matplotlib的 Python 库,其中包含许多有用的工具来操作、绘制和分析数据。我们将使用pyplot(它是matplotlib的一部分)来生成我们捕获数据的图表。有关pyplot的更多信息,请访问matplotlib.org/users/pyplot_tutorial.html

这是一个用于 Python 的类似 MATLAB 的数据可视化框架。

准备工作

要使用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  
  1. 从 Git 存储库下载源文件(命令应为单行)如下:
wget https://github.com/matplotlib/matplotlib/archive/master.zip
  1. 解压并打开创建的matplotlib-master文件夹,如下所示:
unzip master.zip
rm master.zip
cd matplotlib-master
  1. 运行设置文件进行构建(这将需要一段时间)并安装如下:
sudo python3 setup.py build
sudo python3 setup.py install  
  1. 以与自动安装相同的方式测试安装。

我们要么需要 PCF8591 ADC 模块(和之前安装的wiringpi2),要么我们可以使用上一节中的data_local.py模块(只需在脚本的导入部分用data_local替换data_adc)。我们还需要在新脚本的同一目录中拥有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%st%st%st%st%s" 
FORMATBODY = "%dt%st%ft%ft%ft%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 
  1. 创建一个名为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来获取当前的epoch 时间(这是自 1970 年 1 月 1 日以来的毫秒数),使用time.time()命令。我们使用strftime()将值转换为更友好的年-月-日 时:分:秒格式。

main()函数首先创建我们的device类的一个实例(我们在前面的示例中创建了这个类),它将提供数据。我们从data设备获取通道名称并构造header字符串。如果DEBUG设置为True,数据将打印到屏幕上;如果FILE设置为True,它将被写入文件。

在主循环中,我们使用设备的getNew()函数来收集数据并格式化以在屏幕上显示或记录到文件中。使用try: finally:命令调用main()函数,这将确保在脚本中止时,文件将被正确关闭。

第二个脚本log_graph.py允许我们读取日志文件并生成记录的数据的图表,如下图所示:

由 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()对象是一种称为generator的特殊类型的函数。generator函数每次被调用时都会产生一系列连续的值,甚至可以使用其先前的状态来计算下一个值(如果需要的话)。这用于执行连续的计算以进行绘图;这就是为什么它在这里使用的原因。在我们的情况下,我们只想连续运行相同的采样函数(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 扩展器扩展树莓派 GPIO

正如我们所看到的,利用更高级别的总线协议可以让我们快速轻松地连接到更复杂的硬件。通过使用 I²C,我们可以将树莓派上可用的 I/O 扩展,并提供额外的电路保护(在某些情况下,还提供额外的电源来驱动更多的硬件)。

有许多可用的设备可以通过 I²C 总线(以及 SPI)进行 I/O 扩展,但最常用的是 28 引脚设备 MCP23017,它提供 16 个额外的数字输入/输出引脚。作为 I²C 设备,它只需要两个信号(SCL 和 SDA 连接,加上地和电源),并且可以与同一总线上的其他 I²C 设备一起正常工作。

我们将看到 Adafruit I²C 16x2 RGB LCD Pi Plate 如何利用这些芯片来通过 I²C 总线控制 LCD 字母显示和键盘(如果没有 I/O 扩展器,这通常需要多达 15 个 GPIO 引脚)。

其他制造商的板也可以使用。16x2 LCD 模块和 I²C 到串行接口模块可以组合在一起,以拥有我们自己的低成本 I²C LCD 模块。

做好准备

您将需要 Adafruit I²C 16x2 RGB LCD Pi Plate(还包括五个键盘按钮),如下图所示:

带有键盘按钮的 Adafruit I²C 16x2 RGB LCD Pi Plate

Adafruit I²C 16x2 RGB LCD Pi Plate 直接连接到树莓派的 GPIO 连接器。

与之前一样,我们可以使用 PCF8591 ADC 模块,或者使用上一节中的data_local.py模块(在脚本的导入部分使用data_adcdata_local)。data_adc.pydata_local.py文件应该与新脚本在同一个目录中。

LCD Pi Plate 只需要四个引脚(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 
  1. 连接 LCD 模块后,按以下方式运行脚本:
sudo python3 lcd_i2c.py  

使用左右按钮选择要显示的数据通道,然后按 SELECT 按钮退出。

工作原理...

wiringpi2库对于 I/O 扩展器芯片(如 Adafruit LCD 字符模块所使用的芯片)有很好的支持。要使用 Adafruit 模块,我们需要为 MCP23017 端口 A 的所有引脚设置引脚映射,如下表所示(然后,我们使用偏移量100设置 I/O 扩展器引脚):

名称 SELECT RIGHT DOWN UP LEFT GREEN BLUE RED
MCP23017 端口 A A0 A1 A2 A3 A4 A6 A7 A8
WiringPi 引脚 100 101 102 103 104 106 107 108

MCP23017 端口 B 的所有引脚的引脚映射如下:

名称 DB7 DB6 DB5 DB4 E RW RS
MCP23017 端口 B B1 B2 B3 B4 B5 B6 B7
WiringPi 引脚 109 110 111 112 113 114 115

要设置 LCD 屏幕,我们初始化wiringPiSetup()和 I/O 扩展器mcp23017Setup()。然后,我们指定 I/O 扩展器的引脚偏移和总线地址。接下来,我们将所有硬件按钮设置为输入(使用pinMode(引脚号,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(这将退出主循环,允许脚本完成)。

main()函数调用gpiosetup()来创建我们的lcd对象;然后,我们创建我们的dataDevice对象并获取数据名称。在主循环中,我们获取新数据;然后,我们使用我们的printLCD()函数在顶部行上显示数据名称,并在第二行上显示数据值。最后,我们检查按钮是否已被按下,并根据需要设置索引到我们的数据。

还有更多...

使用诸如 MCP23017 之类的扩展器芯片提供了一种增加与树莓派的硬件连接性的绝佳方式,同时还提供了额外的保护层(更换扩展器芯片比更换树莓派便宜)。

I/O 扩展器的电压和限制

扩展器在使用时只使用少量功率,但如果您使用 3.3V 供电,那么您仍然只能从所有引脚中最多吸取 50mA。如果吸取的功率过多,那么您可能会遇到系统冻结或 SD 卡上的读/写损坏。

如果您使用 5V 供电扩展器,那么您可以吸取扩展器支持的最大功率(每个引脚最多约 25mA,总共 125mA),只要您的 USB 电源供应足够强大。

我们必须记住,如果扩展器使用 5V 电源供电,输入/输出和中断线也将是 5V,绝不能连接回树莓派(除非使用电平转换器将电压转换为 3.3V)。

通过更改扩展器芯片上的地址引脚(A0、A1 和 A2)的接线,最多可以同时在同一 I²C 总线上使用八个模块。为了确保每个模块都有足够的电流可用,我们需要使用单独的 3.3V 供电。像 LM1117-3.3 这样的线性稳压器将是合适的(这将提供最多 800mA 的 3.3V,每个 100mA),并且只需要以下简单的电路:

LM1117 线性稳压器电路

以下图表显示了如何将稳压器连接到 I/O 扩展器(或其他设备)以为驱动额外硬件提供更多电流:

使用稳压器与树莓派

输入电压(Vin)由树莓派提供(例如,来自 GPIO 引脚头,如 5V 引脚 2)。但是,只要在 4.5V 和 15V 之间并且能够提供足够的电流,Vin 可以由任何其他电源(或电池组)提供。重要的是要确保树莓派、电源(如果使用单独的电源)、稳压器和 I/O 扩展器的地连接(GND)都连接在一起(作为公共地)。

使用您自己的 I/O 扩展器模块

您可以使用可用的 I/O 扩展器模块(或者只是以下电路中的 MCP23017 芯片)来控制大多数 HD44780 兼容的 LCD 显示器:

I/O 扩展器和 HD44780 兼容显示器

D-Pad 电路,使用 Python 驱动硬件,也可以连接到扩展器的剩余端口 A 引脚(PA0到按钮 1,PA1到右,PA2到下,PA3到上,PA4到左,PA5到按钮 2)。与前面的例子一样,按钮将是PA0PA4(WiringPi 引脚编号 100 到 104);除此之外,我们还将第二个按钮添加到PA5(WiringPi 引脚编号 105)。

直接控制 LCD 字母显示器

或者,您也可以直接从树莓派驱动屏幕,连接如下:

我们这里不使用 I²C 总线。

LCD VSS VDD V0 RS RW E DB4 DB5 DB6 DB7
LCD 引脚 1 2 3 4 5 6 11 12 13 14
树莓派 GPIO 6 (GND) 2 (5V) 对比度 11 13 (GND) 15 12 16 18 22

上表列出了树莓派和 HD44780 兼容的字母显示模块之间所需的连接。

对比度引脚(V0)可以像以前一样连接到可变电阻器(一端连接到 5V 供电,另一端连接到 GND);尽管根据屏幕的不同,您可能会发现可以直接连接到 GND/5V 以获得最大对比度。

wiringpi2 LCD 库假定 RW 引脚连接到 GND(只读);这样可以避免 LCD 直接连接到树莓派时发送数据的风险(这将是一个问题,因为屏幕由 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>  

这将打开一个 SQLite 控制台,在其中我们直接输入 SQL 命令。例如,以下命令将创建一个新表,添加一些数据,显示内容,然后删除表:

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  

您将需要与以前的配方中准备就绪部分中详细描述的相同的硬件设置,使用 I²C 总线与设备配合使用。

操作步骤

创建以下脚本,名为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 样本。

它是如何工作的...

当首次运行脚本时,它将创建一个名为mydatabase.db的新 SQLite 数据库文件,该文件将添加一个名为recordeddata的表。该表由createTable()生成,该函数运行以下 SQLite 命令:

CREATE TABLE recordeddata 
( 
    itm_date DEFAULT (date('now','localtime')), 
    itm_time DEFAULT (time('now','localtime')), 
    itm_name, 
    itm_value 
) 

新表将包含以下数据项:

名称 描述
itm_date 用于存储数据样本的日期。创建数据记录时,当前日期(使用date('now','localtime'))被应用为默认值。
itm_time 用于存储数据样本的时间。创建数据记录时,当前时间(使用time('now','localtime'))被应用为默认值。
itm_name 用于记录样本的名称。
itm_value 用于保存采样值。

然后,我们使用与以前的记录和绘图数据配方中相同的方法从 ADC 中捕获 10 个数据样本(如captureSamples()函数中所示)。但是,这次,我们将使用以下 SQL 命令将捕获的数据添加到我们的新 SQLite 数据库表中(使用cursor.execute(sqlquery)应用):

INSERT INTO recordeddata 
    (itm_name, itm_value) VALUES ('0:Light', 210) 

当前日期和时间将默认添加到每个记录中。我们最终得到一组 40 条记录(每个 ADC 样本周期捕获 4 条记录),这些记录现在存储在 SQLite 数据库中:

已捕获并存储了八个 ADC 样本在 SQLite 数据库中

记录创建后,我们必须记得调用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 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 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命令将允许我们更改(SET)指定列中的数据值。我们还可以将其与WHERE命令结合使用,以限制应用更改的记录:

UPDATE table_name 
    SET column_name2=49name2=49,column_name3=30name3=30.111997 
    WHERE column_name1name1= 'Douglas Adams'Adams'; 

删除命令

DELETE命令允许使用WHERE选择的任何记录从指定的表中删除。但是,如果选择整个表,使用DELETE * FROM table_name将删除表的全部内容:

DELETE FROM table_name 
    WHERE columncolumn_name2=9999 

删除命令

DROP命令允许完全从数据库中删除表:

DROP table_name  

请注意,这将永久删除存储在指定表和结构中的所有数据。

从您自己的 Web 服务器查看数据

收集和整理信息到数据库非常有帮助,但如果它被锁在数据库或文件中,它就没有太多用处。然而,如果我们允许存储的数据通过网页查看,它将更容易访问;我们不仅可以从其他设备查看数据,还可以在同一网络上与其他人分享。

我们将创建一个本地 web 服务器来查询和显示捕获的 SQLite 数据,并允许通过 PHP web 界面查看。这将允许数据不仅可以通过树莓派上的 web 浏览器查看,还可以在本地网络上的其他设备上查看,如手机或平板电脑:

通过 web 页面显示的 SQLite 数据库中捕获的数据

使用 web 服务器输入和显示信息是允许广泛用户与您的项目互动的强大方式。以下示例演示了一个可以为各种用途定制的 web 服务器设置。

准备工作

确保您已完成上一个步骤,以便传感器数据已被收集并存储在 SQLite 数据库中。我们需要安装一个 web 服务器(Apache2)并启用 PHP 支持以允许 SQLite 访问。

使用以下命令安装 web 服务器和 PHP:

sudo apt-get update
sudo aptitude install apache2 php5 php5-sqlite  

/var/www/目录被 web 服务器使用;默认情况下,它将加载index.html(或index.php)- 否则,它将只显示目录中文件的链接列表。

要测试 web 服务器是否正在运行,请创建一个默认的index.html页面。为此,您需要使用sudo权限创建文件(/var/www/目录受到普通用户更改的保护)。使用以下命令:

sudo nano /var/www/index.html  

创建带有以下内容的index.html

<h1>It works!</h1> 

关闭并保存文件(使用Ctrl + XYEnter)。

如果您正在使用带屏幕的树莓派,您可以通过加载桌面来检查它是否正常工作:

startx  

然后,打开 web 浏览器(epiphany-browser)并输入http://localhost作为地址。您应该看到以下测试页面,表明 web 服务器处于活动状态:

树莓派浏览器显示位于 http://localhost 的测试页面

如果您远程使用树莓派或将其连接到您的网络,您还应该能够在网络上的另一台计算机上查看该页面。首先,确定树莓派的 IP 地址(使用sudo hostname -I),然后在 web 浏览器中使用此地址。您甚至可能发现您可以使用树莓派的实际主机名(默认情况下,这是http://raspberrypi/)。

如果您无法从另一台计算机上看到网页,请确保您没有启用防火墙(在计算机本身或路由器上)来阻止它。

接下来,我们可以测试 PHP 是否正常运行。我们可以创建一个名为test.php的网页,并确保它位于/var/www/目录中:

<?php 
  phpinfo(); 
?>; 

用于查看 SQLite 数据库中数据的 PHP 网页具有以下细节:

http://localhost/test.php 查看 test.php 页面

现在我们准备编写我们自己的 PHP 网页来查看 SQLite 数据库中的数据。

如何做...

  1. 创建以下 PHP 文件并将它们保存在名为/var/www/./的 web 服务器目录中。

  2. 使用以下命令创建 PHP 文件:

sudo nano /var/www/show_data_lite.php

  1. 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%'"; 
//Execute 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>
  1. 使用以下命令创建 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

  1. 如果您使用以下地址在 web 浏览器中打开show_data_lite.php文件,它将显示为一个网页:
http://localhost/showshow_data_lite.php
  1. 或者,您可以通过引用树莓派的 IP 地址(使用hostname -I确认 IP 地址)在网络中的另一台计算机上打开网页:
http://192.168.1.101/showshow_data_lite.php 

您可能还可以使用主机名(默认情况下,这将使地址为http://raspberrypi/show_data_lite.php)。但是,这可能取决于您的网络设置。

如果没有数据,请确保运行mysqlite_adc.py脚本以捕获额外的数据。

  1. 要使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 Data设置为页面头部的标题,并使用以下命令使页面每 10 秒自动重新加载:

<meta http-equiv="refresh" content="10" > 

接下来,我们定义一个Delete按钮,当单击时将加载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 

如“还有更多...”部分所述,这将从数据库中删除recordeddata表。如果response不等于 1,则操作未完成。这样做的最有可能原因是包含mydatabase.db文件的目录不可写入 Web 服务器(请参阅如何操作...部分中关于将文件所有者更改为www-data的注意事项)。

最后,我们提供另一个按钮,将用户带回show_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),我们可以使用一个名为PyMySQL的兼容 Python 3 的库来进行接口。有关如何使用此库的其他信息,请参阅 PyMySQL 网站(github.com/PyMySQL/PyMySQL)。

要在 PHP 中使用 MySQL,您还需要 PHP MySQL(php5-mysql);有关更多信息,请参阅 W3 Schools 的优秀资源(www.w3schools.com/php/php_mysql_connect.asp)。

您会注意到,尽管 SQL 实现之间存在细微差异,但无论您选择哪种,一般概念和命令现在应该对您来说都很熟悉。

感知和发送数据到在线服务

在本节中,我们将使用一个名为 Xively 的在线服务。该服务允许我们在线连接、传输和查看数据。Xively 使用一种称为REpresentational State TransferREST)的用于在 HTTP 上传输信息的常见协议。REST 被许多服务使用,如 Facebook 和 Twitter,使用各种密钥和访问令牌来确保数据在授权的应用程序和经过验证的站点之间安全传输。

您可以使用名为requests的 Python 库手动执行大多数 REST 操作(例如POSTGETSET等)。

然而,通常更容易使用特定于您打算使用的服务的特定库。它们将处理授权过程并提供访问功能,如果服务发生变化,可以更新库而不是您的代码。

我们将使用xively-python库,该库提供了 Python 函数,使我们能够轻松地与该站点进行交互。

有关xively-python库的详细信息,请参阅xively.github.io/xively-python/

Xively 收集的数据显示在以下截图中:

Xively 收集和以 REST 传输的数据绘图

准备工作

您需要在www.xively.com创建一个帐户,我们将使用该帐户接收我们的数据。转到该网站并注册一个免费的开发者帐户:

注册并创建 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 帐户并查看数据,使用标准的网络浏览器。这提供了发送数据和在世界各地远程监视数据的手段(如果需要,甚至可以同时从几个树莓派发送数据)。该服务甚至支持创建触发器,如果某些项目超出预期范围,达到特定值或符合设定标准,则可以发送额外的消息。触发器反过来可以用于控制其他设备或引发警报等。它们还可以用于其他平台,如 ThingSpeak 或 plot.ly。

另请参阅

AirPi 空气质量和天气项目(airpi.es)向您展示如何添加自己的传感器或使用他们的 AirPi 套件创建自己的空气质量和天气站(并将数据记录到您自己的 Xively 帐户)。该网站还允许您与世界各地的其他人分享您的 Xively 数据源。

第九章:构建光学字符识别的神经网络模块

本章介绍以下主题:

  • 使用光学字符识别OCR)系统

  • 使用软件可视化光学字符

  • 使用神经网络构建光学字符识别器

  • 应用 OCR 系统

介绍

OCR 系统用于将文本图像转换为字母、单词和句子。它被广泛应用于各个领域,用于从图像中提取信息。它还用于签名识别、自动数据评估和安全系统。它在商业上用于验证数据记录、护照文件、发票、银行对账单、电脑收据、名片、静态数据的打印输出等。OCR 是模式识别、人工智能和计算机视觉的研究领域。

可视化光学字符

光学字符可视化是一种常见的数字化印刷文本的方法,使得这些文本可以进行电子编辑、搜索、紧凑存储和在线显示。目前,它们广泛应用于认知计算、机器翻译、文本转语音转换、文本挖掘等领域。

如何做…

  1. 导入以下软件包:
import os 
import sys 
import cv2 
import numpy as np 
  1. 加载输入数据:
in_file = 'words.data'  
  1. 定义可视化参数:
scale_factor = 10 
s_index = 6 
e_index = -1 
h, w = 16, 8 
  1. 循环直到遇到Esc键:
with open(in_file, 'r') as f: 
  for line in f.readlines(): 
    information = np.array([255*float(x) for x in line.split('t')[s_index:e_index]]) 
    image = np.reshape(information, (h,w)) 
    image_scaled = cv2.resize(image, None, fx=scale_factor, fy=scale_factor) 
    cv2.imshow('Image', image_scaled) 
    a = cv2.waitKey() 
    if a == 10: 
      break 
  1. 键入python visualize_character.py来执行代码:

  1. 执行visualize_character.py时得到的结果如下:

使用神经网络构建光学字符识别器

本节描述基于神经网络的光学字符识别方案。

如何做…

  1. 导入以下软件包:
import numpy as np 
import neurolab as nl 
  1. 读取输入文件:
in_file = 'words.data'
  1. 考虑 20 个数据点来构建基于神经网络的系统:
# Number of datapoints to load from the input file 
num_of_datapoints = 20
  1. 表示不同的字符:
original_labels = 'omandig' 
# Number of distinct characters 
num_of_charect = len(original_labels) 
  1. 使用 90%的数据来训练神经网络,剩下的 10%用于测试:
train_param = int(0.9 * num_of_datapoints) 
test_param = num_of_datapoints - train_param 
  1. 定义数据集提取参数:
s_index = 6 
e_index = -1 
  1. 构建数据集:
information = [] 
labels = [] 
with open(in_file, 'r') as f: 
  for line in f.readlines(): 
    # Split the line tabwise 
    list_of_values = line.split('t') 
  1. 实施错误检查以确认字符:
    if list_of_values[1] not in original_labels: 
      continue 
  1. 提取标签并将其附加到主列表:
    label = np.zeros((num_of_charect , 1)) 
    label[original_labels.index(list_of_values[1])] = 1 
    labels.append(label)
  1. 提取字符并将其添加到主列表:
    extract_char = np.array([float(x) for x in     list_of_values[s_index:e_index]]) 
    information.append(extract_char)
  1. 一旦加载所需数据集,退出循环:
    if len(information) >= num_of_datapoints: 
      break 
  1. 将信息和标签转换为 NumPy 数组:
information = np.array(information) 
labels = np.array(labels).reshape(num_of_datapoints, num_of_charect) 
  1. 提取维度的数量:
num_dimension = len(information[0]) 
  1. 创建和训练神经网络:
neural_net = nl.net.newff([[0, 1] for _ in range(len(information[0]))], [128, 16, num_of_charect]) 
neural_net.trainf = nl.train.train_gd 
error = neural_net.train(information[:train_param,:], labels[:train_param,:], epochs=10000, show=100, goal=0.01) 
  1. 预测测试输入的输出:
p_output = neural_net.sim(information[train_param:, :]) 
print "nTesting on unknown data:" 
  for i in range(test_param): 
    print "nOriginal:", original_labels[np.argmax(labels[i])] 
    print "Predicted:", original_labels[np.argmax(p_output[i])]
  1. 执行optical_character_recognition.py时得到的结果如下截图所示:

工作原理…

构建了一个神经网络支持的光学字符识别系统,用于从图像中提取文本。该过程涉及训练神经网络系统,测试和验证使用字符数据集。

读者可以参考文章基于神经网络的光学字符识别系统,了解 OCR 背后的基本原理:ieeexplore.ieee.org/document/6419976/

另请参阅

请参考以下内容:

OCR 系统的应用

OCR 系统广泛用于从图像中提取/转换文本(字母和数字)。OCR 系统被广泛用于验证商业文件、自动车牌识别以及从文件中提取关键字符。它还用于使打印文件的电子图像可搜索,并为盲人和视障用户构建辅助技术。

第十章:算术运算、循环和闪烁灯

现在让我们来看看这一章,我们将回顾 Python 中的算术运算和变量。我们还将讨论 Python 中的字符串和接受用户输入。您将了解树莓派的 GPIO 及其特性,并使用 Python 编写代码,使 LED 使用树莓派 Zero 的 GPIO 闪烁。我们还将讨论控制树莓派的 GPIO 的实际应用。

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

  • Python 中的算术运算

  • Python 中的位运算符

  • Python 中的逻辑运算符

  • Python 中的数据类型和变量

  • Python 中的循环

  • 树莓派 Zero 的 GPIO 接口。

本章所需的硬件

在本章中,我们将讨论一些例子,我们将控制树莓派的 GPIO。我们需要一个面包板,跳线,LED 和一些电阻(330 或 470 欧姆)来讨论这些例子。

我们还需要一些可选的硬件,我们将在本章的最后一节中讨论。

算术运算

Python 可以执行所有标准的算术运算。让我们启动 Python 解释器,了解更多:

  • 加法:可以使用+操作符对两个数字进行相加。结果将打印在屏幕上。使用 Python 解释器尝试以下示例:
       >>>123+456 
       579
  • 减法:可以使用-操作符对两个数字进行相加:
       >>>456-123 
       333 
       >>>123-456 
       -333
  • 乘法:可以将两个数字相乘如下:
       >>>123*456 
       56088
  • 除法:可以将两个数字相除如下:
       >>>456/22 
 20.727272727272727 
       >>>456/2.0 
       228.0 
       >>>int(456/228) 
       2
  • 模运算符:在 Python 中,模运算符(%)返回除法运算的余数:
       >>>4%2 
       0 
       >>>3%2 
       1
  • floor 运算符//)是模运算符的相反。此运算符返回商的地板,即整数结果,并丢弃小数部分:
       >>>9//7 
       1 
       >>>7//3 
       2 
       >>>79//25 
       3

Python 中的位运算符

在 Python 中,可以对数字执行位级操作。这在从某些传感器解析信息时特别有帮助。例如,一些传感器以一定频率共享它们的输出。当新的数据点可用时,设置某个特定的位,表示数据可用。可以使用位运算符来检查在从传感器检索数据点之前是否设置了特定的位。

如果您对位运算符有兴趣,我们建议从en.wikipedia.org/wiki/Bitwise_operation开始。

考虑数字32,它们的二进制等价物分别是011010。让我们看看执行每个数字位操作的不同运算符:

  • AND 运算符:AND 运算符用于对两个数字执行 AND 操作。使用 Python 解释器尝试一下:
       >>>3&2 
       2

这相当于以下 AND 操作:

   0 1 1 &
   0 1 0
   --------
   0 1 0 (the binary representation of the number 2)
  • OR 运算符:OR 运算符用于对两个数字执行 OR 操作,如下所示:
       >>>3|2 
       3

这相当于以下 OR 操作:

   0 1 1 OR
   0 1 0
   --------
   0 1 1 (the binary representation of the number 3)
  • NOT 运算符:NOT 运算符翻转数字的位。看下面的例子:
       >>>~1 
       -2

在前面的例子中,位被翻转,即1变为00变为1。因此,1的二进制表示是0001,当执行按位 NOT 操作时,结果是1110。解释器返回结果为-2,因为负数存储为它们的二进制补码1的二进制补码是-2

为了更好地理解二进制补码等内容,我们建议阅读以下文章,wiki.python.org/moin/BitwiseOperatorsen.wikipedia.org/wiki/Two's_complement

  • XOR 运算符:可以执行异或操作如下:
       >>>3² 
       1
  • 左移运算符:左移运算符可以将给定值的位向左移动所需的位数。例如,将数字3向左移动一位会得到数字6。数字3的二进制表示是0011。将位左移一位将得到0110,即数字6
       >>>3<<1 
       6
  • 右移运算符:右移运算符可以将给定值的位向右移动所需的位数。启动命令行解释器并自己尝试一下。当你将数字6向右移动一个位置时会发生什么?

逻辑运算符

逻辑运算符用于检查不同的条件并相应地执行代码。例如,检测与树莓派 GPIO 接口连接的按钮是否被按下,并执行特定任务作为结果。让我们讨论基本的逻辑运算符:

  • 等于:等于(==)运算符用于比较两个值是否相等:
       >>>3==3 
       True 
       >>>3==2 
       False
  • 不等于:不等于(!=)运算符比较两个值,如果它们不相等,则返回True
       >>>3!=2 
       True 
       >>>2!=2 
       False
  • 大于:此运算符(>)如果一个值大于另一个值,则返回True
       >>>3>2 
       True 
       >>>2>3 
       False
  • 小于:此运算符比较两个值,如果一个值小于另一个值,则返回True
       >>>2<3 
       True 
       >>>3<2 
       False
  • 大于或等于(>=):此运算符比较两个值,如果一个值大于或等于另一个值,则返回True
       >>>4>=3 
       True 
       >>>3>=3 
       True 
       >>>2>=3 
       False
  • 小于或等于(<=):此运算符比较两个值,如果一个值小于或等于另一个值,则返回True
       >>>2<=2 
       True 
       >>>2<=3 
       True 
       >>>3<=2 
       False

Python 中的数据类型和变量

在 Python 中,变量用于在程序执行期间存储结果或值在计算机的内存中。变量使得可以轻松访问计算机内存中的特定位置,并且使得编写用户可读的代码成为可能。

例如,让我们考虑这样一个情景,一个人想要从办公室或大学获得一张新的身份证。这个人将被要求填写一个包括他们的姓名、部门和紧急联系信息在内的相关信息的申请表。表格将有必需的字段。这将使办公室经理在创建新的身份证时参考表格。

同样,变量通过提供存储信息在计算机内存中的方式来简化代码开发。如果必须考虑存储器映射,编写代码将会非常困难。例如,使用名为 name 的变量比使用特定的内存地址如0x3745092更容易。

Python 中有不同种类的数据类型。让我们来回顾一下不同的数据类型:

  • 一般来说,姓名、街道地址等都是由字母数字字符组成。在 Python 中,它们被存储为字符串。Python 中的字符串表示和存储在变量中如下:
       >>>name = 'John Smith' 
       >>>address = '123 Main Street'
  • 在 Python 中,数字可以存储如下:
       >>>age = 29 
       >>>employee_id = 123456 
       >>>height = 179.5 
       >>>zip_code = 94560
  • Python 还可以存储布尔变量。例如,一个人的器官捐赠者状态可以是TrueFalse
       >>>organ_donor = True
  • 可以同时赋值多个变量的值:
       >>>a = c= 1 
       >>>b = a
  • 可以删除变量如下:
       >>>del(a)

Python 中还有其他数据类型,包括列表、元组和字典。我们将在下一章中详细讨论这一点。

从用户读取输入

现在,我们将讨论一个简单的程序,要求用户输入两个数字,程序返回两个数字的和。现在,我们假设用户总是提供有效的输入。

在 Python 中,用户可以使用input()函数(docs.python.org/3/library/functions.html#input)提供输入给 Python 程序:

    var = input("Enter the first number: ")

在前面的例子中,我们使用input()函数来获取用户输入的数字。input()函数将提示("Enter the first number: ")作为参数,并返回用户输入。在这个例子中,用户输入存储在变量var中。为了添加两个数字,我们使用input()函数请求用户提供两个数字作为输入:

    var1 = input("Enter the first number: ") 
    var2 = input("Enter the second number: ") 
    total = int(var1) + int(var2) 
    print("The sum is %d" % total)

我们正在使用input()函数来获取两个数字的用户输入。在这种情况下,用户数字分别存储在var1var2中。

用户输入是一个字符串。我们需要在将它们相加之前将它们转换为整数。我们可以使用int()函数将字符串转换为整数(docs.python.org/3/library/functions.html#int)。

int()函数将字符串作为参数,并返回转换后的整数。转换后的整数相加并存储在变量total中。前面的例子可与本章一起下载,名称为input_function.py

如果用户输入无效,int()函数将抛出异常,表示发生了错误。因此,在本例中,我们假设用户输入是有效的。在后面的章节中,我们将讨论由无效输入引起的异常捕获。

以下快照显示了程序输出:

input_function.py 的输出

格式化的字符串输出

让我们重新讨论前一节中讨论的例子。我们打印了结果如下:

    print("The sum is %d" % total)

在 Python 中,可以格式化字符串以显示结果。在前面的例子中,我们使用%d来指示它是整数变量的占位符。这使得可以打印带有整数的字符串。除了作为print()函数的参数传递的字符串外,还传递需要打印的变量作为参数。在前面的例子中,变量是使用%运算符传递的。还可以传递多个变量:

    print("The sum of %d and %d is %d" % (var1, var2, total))

也可以按以下方式格式化字符串:

    print("The sum of 3 and 2 is {total}".format(total=5))

str.format()方法

format()方法使用大括号({})作为占位符来格式化字符串。在前面的例子中,我们使用total作为占位符,并使用字符串类的格式化方法填充每个占位符。

读者的另一个练习

使用format()方法格式化一个带有多个变量的字符串。

让我们构建一个从用户那里获取输入并在屏幕上打印的控制台/命令行应用程序。让我们创建一个名为input_test.py的新文件(可与本章的下载一起使用),获取一些用户输入并在屏幕上打印它们:

    name = input("What is your name? ") 
    address = input("What is your address? ") 
    age = input("How old are you? ") 

    print("My name is " + name) 
    print("I am " + age + " years old") 
    print("My address is " + address)

执行程序并查看发生了什么:

input_test.py 的输出

前面的例子可与本章一起下载,名称为input_test.py

读者的另一个练习

使用字符串格式化技术重复前面的例子。

连接字符串

在前面的例子中,我们将用户输入与另一个字符串组合打印出来。例如,我们获取用户输入name并打印句子My name is Sai。将一个字符串附加到另一个字符串的过程称为连接

在 Python 中,可以通过在两个字符串之间添加+来连接字符串:

    name = input("What is your name? ") 
    print("My name is " + name)

可以连接两个字符串,但不能连接整数。让我们考虑以下例子:

    id = 5 
    print("My id is " + id)

它将抛出一个错误,暗示整数和字符串不能结合使用:

一个异常

可以将整数转换为字符串并将其连接到另一个字符串:

    print("My id is " + str(id))

这将产生以下结果:

Python 中的循环

有时,特定任务必须重复多次。在这种情况下,我们可以使用循环。在 Python 中,有两种类型的循环,即for循环和while循环。让我们通过具体的例子来回顾它们。

一个 for 循环

在 Python 中,for循环用于执行n次任务。for循环会迭代序列的每个元素。这个序列可以是字典、列表或任何其他迭代器。例如,让我们讨论一个执行循环的例子:

    for i in range(0, 10): 
       print("Loop execution no: ", i)

在前面的例子中,print语句被执行了 10 次:

为了执行print任务 10 次,使用了range()函数(docs.python.org/2/library/functions.html#range)。range函数会为传递给函数的起始和停止值生成一个数字列表。在这种情况下,010被作为参数传递给range()函数。这将返回一个包含从09的数字的列表。for循环会按照步长为 1 的步骤迭代每个元素的代码块。range函数也可以按照步长为 2 生成一个数字列表。这是通过将起始值、停止值和步长值作为参数传递给range()函数来实现的:

    for i in range(0, 20, 2): 
       print("Loop execution no: ", i)

在这个例子中,0是起始值,20是停止值,2是步长值。这会生成一个 10 个数字的列表,步长为 2:

range函数可以用来从给定的数字倒数。比如,我们想要从10倒数到1

    for i in range(10, 0, -1): 
       print("Count down no: ", i)

输出将会是这样的:

range函数的一般语法是range(start, stop, step_count)。它会生成一个从startn-1的数字序列,其中n是停止值。

缩进

注意for循环块中的缩进

    for i in range(10, 1, -1): 
       print("Count down no: ", i)

Python 执行for循环语句下的代码块。这是 Python 编程语言的一个特性。只要缩进级别相同,它就会执行for循环下的任何代码块:

    for i in range(0,10): 
       #start of block 
       print("Hello") 
       #end of block

缩进有以下两个用途:

  • 它使代码可读性更强

  • 它帮助我们识别要在循环中执行的代码块

在 Python 中,要注意缩进,因为它直接影响代码的执行方式。

嵌套循环

在 Python 中,可以实现循环内的循环。例如,假设我们需要打印地图的xy坐标。我们可以使用嵌套循环来实现这个:

for x in range(0,3): 
   for y in range(0,3): 
         print(x,y)

预期输出是:

在嵌套循环中要小心代码缩进,因为它可能会引发错误。考虑以下例子:

for x in range(0,10): 
   for y in range(0,10): 
   print(x,y)

Python 解释器会抛出以下错误:

    SyntaxError: expected an indented block

这在以下截图中可见:

因此,在 Python 中要注意缩进是很重要的(特别是嵌套循环),以成功执行代码。IDLE 的文本编辑器会在你编写代码时自动缩进。这应该有助于理解 Python 中的缩进。

一个 while 循环

当特定任务需要执行直到满足特定条件时,会使用while循环。while循环通常用于执行无限循环中的代码。让我们看一个具体的例子,我们想要打印i的值从09

i=0 
while i<10: 
  print("The value of i is ",i) 
  i+=1

while循环内,我们每次迭代都会将i增加1i的值增加如下:

i += 1

这等同于i = i+1

这个例子会执行代码,直到i的值小于 10。也可以执行无限循环中的某些操作:

i=0 
while True: 
  print("The value of i is ",i) 
  i+=1

可以通过在键盘上按下Ctrl + C来停止这个无限循环的执行。

也可以有嵌套的while循环:

i=0 
j=0 
while i<10: 
  while j<10: 
    print("The value of i,j is ",i,",",j) 
    i+=1 
    j+=1

for循环类似,while循环也依赖于缩进的代码块来执行一段代码。

Python 可以打印字符串和整数的组合,只要它们作为print函数的参数呈现,并用逗号分隔。在前面提到的示例中,i,j 的值是iprint函数的参数。您将在下一章中了解更多关于函数和参数的内容。此功能使得格式化输出字符串以满足我们的需求成为可能。

树莓派的 GPIO

树莓派 Zero 配备了一个 40 针的 GPIO 引脚标头。在这 40 个引脚中,我们可以使用 26 个引脚来读取输入(来自传感器)或控制输出。其他引脚是电源引脚(5V3.3VGround引脚):

树莓派 Zero GPIO 映射(来源:https://www.raspberrypi.org/documentation/usage/gpio-plus-and-raspi2/README.md)

我们可以使用树莓派的 GPIO 最多 26 个引脚来接口设备并控制它们。但是,有一些引脚具有替代功能。

较早的图像显示了树莓派的 GPIO 引脚的映射。圆圈中的数字对应于树莓派处理器上的引脚编号。例如,GPIO 引脚2(底部行左侧的第二个引脚)对应于树莓派处理器上的 GPIO 引脚2,而不是 GPIO 引脚标头上的物理引脚位置。

一开始,尝试理解引脚映射可能会令人困惑。保留 GPIO 引脚手册(可与本章一起下载)以供参考。需要一些时间来适应树莓派 Zero 的 GPIO 引脚映射。

树莓派 Zero 的 GPIO 引脚是 3.3V 兼容的,也就是说,如果将大于 3.3V 的电压应用到引脚上,可能会永久损坏引脚。当设置为时,引脚被设置为 3.3V,当引脚被设置为低时,电压为 0V。

闪烁灯

让我们讨论一个例子,我们将使用树莓派 Zero 的 GPIO。我们将把 LED 接口到树莓派 Zero,并使其以 1 秒的间隔闪烁

让我们接线树莓派 Zero 开始:

使用 Fritzing 生成的 Blinky 原理图

在前面的原理图中,GPIO 引脚 2 连接到 LED 的阳极(最长的腿)。LED 的阴极连接到树莓派 Zero 的地引脚。还使用了 330 欧姆的限流电阻来限制电流的流动。

)。Raspbian Jessie操作系统映像带有预安装的库。这是一个非常简单易用的库,对于初学者来说是最好的选择。它支持一套标准设备,帮助我们轻松入门。

例如,为了接口 LED,我们需要从gpiozero库中导入LED类:

from gpiozero import LED

我们将在 1 秒的间隔内打开和关闭 LED。为了做到这一点,我们将导入time库。在 Python 中,我们需要导入一个库来使用它。由于我们将 LED 接口到 GPIO 引脚 2,让我们在我们的代码中提到这一点:

import time 

led = LED(2)

我们刚刚创建了一个名为led的变量,并定义我们将在LED类中使用 GPIO 引脚 2。让我们使用while循环来打开和关闭 LED,间隔为 1 秒。

gpiozero库的 LED 类带有名为on()off()的函数,分别将 GPIO 引脚 2 设置为高电平和低电平:

while True: 
    led.on() 
    time.sleep(1) 
    led.off() 
    time.sleep(1)

在 Python 的时间库中,有一个sleep函数,可以在打开/关闭 LED 之间引入 1 秒的延迟。这在一个无限循环中执行!我们刚刚使用树莓派 Zero 构建了一个实际的例子。

将所有代码放在名为blinky.py的文件中(可与本书一起下载),从命令行终端运行代码(或者,您也可以使用 IDLE3):

    python3 blinky.py

GPIO 控制的应用

现在我们已经实施了我们的第一个示例,让我们讨论一些能够控制 GPIO 的可能应用。我们可以使用树莓派的 GPIO 来控制家中的灯光。我们将使用相同的示例来控制台灯!

有一个名为PowerSwitch Tail II的产品(www.powerswitchtail.com/Pages/default.aspx),可以将交流家电(如台灯)与树莓派连接起来。PowerSwitch Tail 配有控制引脚(可以接收 3.3V 高电平信号),可用于打开/关闭灯。开关配有必要的电路/保护,可直接与树莓派 Zero 接口:

树莓派 Zero 与 PowerSwitch Tail II 接口

让我们从上一节中使用相同的示例,将 GPIO 引脚 2 连接到 PowerSwitch Tail 的+in引脚。让我们将树莓派 Zero 的 GPIO 引脚的地线连接到 PowerSwitch Tail 的-in引脚。PowerSwitch Tail 应连接到交流电源。灯应连接到开关的交流输出。如果我们使用相同的代码并将灯连接到 PowerSwitch Tail,我们应该能够以 1 秒的间隔打开/关闭。

连接到树莓派 Zero 的 PowerSwitch Tail II 使用 LED 闪烁代码进行家电控制只是一个例子。不建议在如此短的时间间隔内打开/关闭台灯。

总结

在本章中,我们回顾了 Python 中的整数、布尔和字符串数据类型,以及算术运算和逻辑运算符。我们还讨论了接受用户输入和循环。我们介绍了树莓派 Zero 的 GPIO,并讨论了 LED 闪烁示例。我们使用相同的示例来控制台灯!

您听说过名为Slack的聊天应用程序吗?您是否尝试过在工作时从笔记本电脑控制家里的台灯?如果这引起了您的兴趣,请在接下来的几章中与我们一起工作。

第十一章:条件语句、函数和列表

在本章中,我们将在前一章学到的基础上进行扩展。您将学习有关条件语句以及如何使用逻辑运算符来检查条件的使用。接下来,您将学习如何在 Python 中编写简单的函数,并讨论如何使用触摸开关(瞬时按键)将输入接口到树莓派的 GPIO 引脚。我们还将讨论使用树莓派 Zero 进行电机控制(这是最终项目的预演),并使用开关输入来控制电机。让我们开始吧!

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

  • Python 中的条件语句

  • 使用条件输入根据 GPIO 引脚状态采取行动

  • 使用条件语句跳出循环

  • Python 中的函数

  • GPIO 回调函数

  • Python 中的电机控制

条件语句

在 Python 中,条件语句用于确定特定条件是否满足,通过测试条件是true还是false。条件语句用于确定程序的执行方式。例如,条件语句可以用于确定是否是开灯的时间。语法如下:

if condition_is_true:

  do_something()

通常使用逻辑运算符来测试条件,并执行缩进块下的任务集。让我们考虑一个例子,check_address_if_statement.py(可在本章下载)中,程序需要使用yesno问题来验证用户输入:

check_address = input("Is your address correct(yes/no)? ") 
if check_address == "yes": 
  print("Thanks. Your address has been saved") 
if check_address == "no": 
  del(address) 
  print("Your address has been deleted. Try again")

在这个例子中,程序期望输入yesno。如果用户提供了输入yes,条件if check_address == "yes"true,则在屏幕上打印消息Your address has been saved

同样,如果用户输入是no,程序将执行在逻辑测试条件if check_address == "no"下的缩进代码块,并删除变量address

if-else 语句

在前面的例子中,我们使用if语句测试每个条件。在 Python 中,还有一种名为if-else语句的替代选项。if-else语句使得在主条件不为true时测试替代条件成为可能:

check_address = input("Is your address correct(yes/no)? ") 
if check_address == "yes": 
  print("Thanks. Your address has been saved") 
else: 
  del(address) 
  print("Your address has been deleted. Try again")

在这个例子中,如果用户输入是yes,则在if下的缩进代码块将被执行。否则,将执行else下的代码块。

if-elif-else 语句

在前面的例子中,对于除yes之外的任何用户输入,程序执行else块下的任何代码。也就是说,如果用户按下回车键而没有提供任何输入,或者提供了no而不是no,则if-elif-else语句的工作如下:

check_address = input("Is your address correct(yes/no)? ") 
if check_address == "yes": 
  print("Thanks. Your address has been saved") 
elif check_address == "no": 
  del(address) 
  print("Your address has been deleted. Try again") 
else: 
  print("Invalid input. Try again")

如果用户输入是yes,则在if语句下的缩进代码块将被执行。如果用户输入是no,则在elifelse-if)下的缩进代码块将被执行。如果用户输入是其他内容,则程序打印消息:Invalid input. Try again

重要的是要注意,代码块的缩进决定了在满足特定条件时需要执行的代码块。我们建议修改条件语句块的缩进,并找出程序执行的结果。这将有助于理解 Python 中缩进的重要性。

到目前为止,我们讨论的三个例子中,可以注意到if语句不需要由else语句补充。elseelif语句需要有一个前置的if语句,否则程序执行将导致错误。

跳出循环

条件语句可以用于跳出循环执行(for循环和while循环)。当满足特定条件时,可以使用if语句来跳出循环:

i = 0 
while True: 
  print("The value of i is ", i) 
  i += 1 
  if i > 100: 
    break

在前面的例子中,while循环在一个无限循环中执行。i的值递增并打印在屏幕上。当i的值大于100时,程序会跳出while循环,并且i的值从 1 打印到 100。

条件语句的应用:使用 GPIO 执行任务

在上一章中,我们讨论了将输出接口到树莓派的 GPIO。让我们讨论一个简单的按键按下的例子。通过读取 GPIO 引脚状态来检测按钮按下。我们将使用条件语句来根据 GPIO 引脚状态执行任务。

让我们将一个按钮连接到树莓派的 GPIO。你需要准备一个按钮、上拉电阻和几根跳线。稍后给出的图示展示了如何将按键连接到树莓派 Zero。按键的一个端子连接到树莓派 Zero 的 GPIO 引脚的地线。

按键接口的原理图如下:

树莓派 GPIO 原理图

按键的另一个端子通过 10K 电阻上拉到 3.3V。按键端子和 10K 电阻的交点连接到 GPIO 引脚 2(参考前一章中分享的 BCM GPIO 引脚图)。

将按键接口到树莓派 Zero 的 GPIO - 使用 Fritzing 生成的图像

让我们回顾一下需要查看按钮状态的代码。我们利用循环和条件语句来使用树莓派 Zero 读取按钮输入。

我们将使用在上一章介绍的gpiozero库。本节的代码示例是GPIO_button_test.py,可与本章一起下载。

在后面的章节中,我们将讨论面向对象编程OOP)。现在,让我们简要讨论类的概念。在 Python 中,是一个包含定义对象的所有属性的蓝图。例如,gpiozero库的Button类包含了将按钮接口到树莓派 Zero 的 GPIO 接口所需的所有属性。这些属性包括按钮状态和检查按钮状态所需的函数等。为了接口一个按钮并读取其状态,我们需要使用这个蓝图。创建这个蓝图的副本的过程称为实例化。

让我们开始导入gpiozero库,并实例化gpiozero库的Button类(我们将在后面的章节中讨论 Python 的类、对象及其属性)。按钮接口到 GPIO 引脚 2。我们需要在实例化时传递引脚号作为参数:

from gpiozero import Button 

#button is interfaced to GPIO 2 
button = Button(2)

gpiozero库的文档可在gpiozero.readthedocs.io/en/v1.2.0/api_input.html找到。根据文档,Button类中有一个名为is_pressed的变量,可以使用条件语句进行测试,以确定按钮是否被按下:

if button.is_pressed: 
    print("Button pressed")

每当按下按钮时,屏幕上会打印出消息Button pressed。让我们将这段代码片段放在一个无限循环中:

from gpiozero import Button 

#button is interfaced to GPIO 2 
button = Button(2)

while True: 
  if button.is_pressed: 
    print("Button pressed")

在无限的while循环中,程序不断检查按钮是否被按下,并在按钮被按下时打印消息。一旦按钮被释放,它就会回到检查按钮是否被按下的状态。

通过计算按钮按下次数来中断循环

让我们再看一个例子,我们想要计算按钮按下的次数,并在按钮接收到预定数量的按下时中断无限循环:

i = 0 
while True: 
  if button.is_pressed: 
    button.wait_for_release() 
    i += 1 
    print("Button pressed") 

  if i >= 10: 
    break

前面的例子可与本章一起下载,文件名为GPIO_button_loop_break.py

在这个例子中,程序检查is_pressed变量的状态。在接收到按钮按下时,程序可以使用wait_for_release方法暂停,直到按钮被释放。当按钮被释放时,用于存储按下次数的变量会增加一次。

当按钮接收到 10 次按下时,程序会跳出无限循环。

连接到树莓派 Zero GPIO 引脚 2 的红色瞬时按钮

Python 中的函数

我们简要讨论了 Python 中的函数。函数执行一组预定义的任务。print是 Python 中函数的一个例子。它可以将一些东西打印到屏幕上。让我们讨论在 Python 中编写我们自己的函数。

可以使用def关键字在 Python 中声明函数。函数可以定义如下:

def my_func(): 
   print("This is a simple function")

在这个函数my_func中,print语句是在一个缩进的代码块下编写的。在函数定义下缩进的任何代码块在代码执行期间调用函数时执行。函数可以被执行为my_func()

向函数传递参数:

函数总是用括号定义的。括号用于向函数传递任何必要的参数。参数是执行函数所需的参数。在前面的例子中,没有向函数传递参数。

让我们回顾一个例子,我们向函数传递一个参数:

def add_function(a, b): 
  c = a + b 
  print("The sum of a and b is ", c)

在这个例子中,ab是函数的参数。函数将ab相加,并在屏幕上打印总和。当通过传递参数32调用函数add_function时,add_function(3,2),其中a3b2

因此,执行函数需要参数ab,或者在没有参数的情况下调用函数会导致错误。可以通过为参数设置默认值来避免与缺少参数相关的错误:

def add_function(a=0, b=0): 
  c = a + b 
  print("The sum of a and b is ", c)

前面的函数需要两个参数。如果我们只向这个函数传递一个参数,另一个参数默认为零。例如,add_function(a=3)b默认为0,或者add_function(b=2)a默认为0。当在调用函数时未提供参数时,它默认为零(在函数中声明)。

同样,print函数打印传递的任何变量。如果调用print函数时没有传递任何参数,则会打印一个空行。

从函数返回值

函数可以执行一组定义的操作,并最终在结束时返回一个值。让我们考虑以下例子:

def square(a): 
   return a**2

在这个例子中,函数返回参数的平方。在 Python 中,return关键字用于在执行完成后返回请求的值。

函数中变量的作用域

Python 程序中有两种类型的变量:局部变量和全局变量。局部变量是函数内部的变量,即在函数内部声明的变量只能在该函数内部访问。例子如下:

def add_function(): 
  a = 3 
  b = 2 
  c = a + b 
  print("The sum of a and b is ", c)

在这个例子中,变量ab是函数add_function的局部变量。让我们考虑一个全局变量的例子:

a = 3 
b = 2 
def add_function(): 
  c = a + b 
  print("The sum of a and b is ", c) 

add_function()

在这种情况下,变量ab在 Python 脚本的主体中声明。它们可以在整个程序中访问。现在,让我们考虑这个例子:

a = 3 
def my_function(): 
  a = 5 
  print("The value of a is ", a)

my_function() 
print("The value of a is ", a)

程序输出为:

      The value of a is

      5

      The value of a is

      3

在这种情况下,当调用my_function时,a的值为5,在脚本主体的print语句中a的值为3。在 Python 中,不可能在函数内部显式修改全局变量的值。为了修改全局变量的值,我们需要使用global关键字:

a = 3 
def my_function(): 
  global a 
  a = 5 
  print("The value of a is ", a)

my_function() 
print("The value of a is ", a)

一般来说,不建议在函数内修改变量,因为这不是一个很安全的修改变量的做法。最佳做法是将变量作为参数传递并返回修改后的值。考虑以下例子:

a = 3 
def my_function(a): 
  a = 5 
  print("The value of a is ", a) 
  return a 

a = my_function(a) 
print("The value of a is ", a)

在上述程序中,a的值为3。它作为参数传递给my_function。函数返回5,保存到a中。我们能够安全地修改a的值。

GPIO 回调函数

让我们回顾一下在 GPIO 示例中使用函数的一些用途。函数可以用来处理与树莓派的 GPIO 引脚相关的特定事件。例如,gpiozero库提供了在按钮按下或释放时调用函数的能力:

from gpiozero import Button 

def button_pressed(): 
  print("button pressed")

def button_released(): 
  print("button released")

#button is interfaced to GPIO 2 
button = Button(2) 
button.when_pressed = button_pressed 
button.when_released = button_released

while True: 
  pass

在这个例子中,我们使用库的 GPIO 类的when_pressedwhen_released属性。当按钮被按下时,执行函数button_pressed。同样,当按钮被释放时,执行函数button_released。我们使用while循环来避免退出程序并继续监听按钮事件。使用pass关键字来避免错误,当执行pass关键字时什么也不会发生。

能够为不同事件执行不同函数的能力在家庭自动化等应用中非常有用。例如,可以用来在天黑时打开灯,反之亦然。

Python 中的直流电机控制

在本节中,我们将讨论使用树莓派 Zero 进行电机控制。为什么要讨论电机控制?随着我们在本书中不同主题的进展,我们将最终构建一个移动机器人。因此,我们需要讨论使用 Python 编写代码来控制树莓派上的电机。

为了控制电机,我们需要一个H 桥电机驱动器(讨论 H 桥超出了我们的范围。有几种资源可供 H 桥电机驱动器使用:www.mcmanis.com/chuck/robotics/tutorial/h-bridge/)。有几种专为树莓派设计的电机驱动器套件。在本节中,我们将使用以下套件:www.pololu.com/product/2753

Pololu产品页面还提供了如何连接电机的说明。让我们开始编写一些 Python 代码来操作电机:

from gpiozero import Motor 
from gpiozero import OutputDevice 
import time

motor_1_direction = OutputDevice(13) 
motor_2_direction = OutputDevice(12)

motor = Motor(5, 6)

motor_1_direction.on() 
motor_2_direction.on()

motor.forward()

time.sleep(10)

motor.stop()

motor_1_direction.off() 
motor_2_direction.off()

树莓派基于电机控制

为了控制电机,让我们声明引脚、电机的速度引脚和方向引脚。根据电机驱动器的文档,电机分别由 GPIO 引脚 12、13 和 5、6 控制。

from gpiozero import Motor 
from gpiozero import OutputDevice 
import time 

motor_1_direction = OutputDevice(13) 
motor_2_direction = OutputDevice(12) 

motor = Motor(5, 6)

控制电机就像使用on()方法打开电机,使用forward()方法向前移动电机一样简单:

motor.forward()

同样,通过调用reverse()方法可以改变电机方向。通过以下方式可以停止电机:

motor.stop()

读者的一些迷你项目挑战

以下是一些迷你项目挑战给我们的读者:

  • 在本章中,我们讨论了树莓派的输入接口和电机控制。想象一个项目,我们可以驱动一个移动机器人,该机器人从触须开关读取输入并操作移动机器人。结合限位开关和电机,是否可能构建一个沿墙行驶的机器人?

  • 在本章中,我们讨论了如何控制直流电机。我们如何使用树莓派控制步进电机?

  • 如何使用树莓派 Zero 接口运动传感器来控制家里的灯?

总结

在本章中,我们讨论了条件语句以及条件语句在 Python 中的应用。我们还讨论了 Python 中的函数,将参数传递给函数,从函数返回值以及 Python 程序中变量的作用域。我们讨论了回调函数和 Python 中的电机控制。

第十二章:通信接口

到目前为止,我们已经讨论了 Python 中的循环、条件语句和函数。我们还讨论了与树莓派接口的输出设备和简单的数字输入设备。

在本章中,我们将讨论以下通信接口:

  • UART - 串行端口

  • 串行外围接口

  • I²C 接口

我们将使用不同的传感器/电子元件来演示在 Python 中编写这些接口的代码。我们留给您选择一个您喜欢的组件来探索这些通信接口。

UART - 串行端口

通用异步收发器UART),即串行端口,是一种通信接口,数据以位的形式从传感器串行传输到主机计算机。使用串行端口是最古老的通信协议之一。它用于数据记录,微控制器从传感器收集数据并通过串行端口传输数据。还有一些传感器以串行通信的形式响应传入的命令传输数据。

我们不会深入讨论串行端口通信的理论(网络上有大量理论可供参考,网址为en.wikipedia.org/wiki/Universal_asynchronous_receiver/transmitter)。我们将讨论使用串行端口与树莓派接口不同的传感器。

树莓派 Zero 的 UART 端口

通常,UART 端口由接收器(Rx)和发送器(Tx)引脚组成,用于接收和发送数据。树莓派的 GPIO 引脚带有 UART 端口。 GPIO 引脚 14(Tx引脚)和 15(Rx引脚)用作树莓派的 UART 端口:

GPIO 引脚 14 和 15 是 UART 引脚(图片来源:https://www.rs-online.com/designspark/introducing-the-raspberry-pi-b-plus)

设置树莓派 Zero 串行端口

为了使用串行端口与传感器通信,串行端口登录/控制台需要被禁用。在Raspbian操作系统镜像中,默认情况下启用此功能,因为它可以方便调试。

串行端口登录可以通过raspi-config禁用:

  1. 启动终端并运行此命令:
       sudo raspi-config
  1. raspi-config的主菜单中选择高级选项:

从 raspi-config 菜单中选择高级选项

  1. 从下拉菜单中选择 A8 串行选项:

从下拉菜单中选择 A8 串行

  1. 禁用串行登录:

禁用串行登录

  1. 完成配置并在最后重新启动:

保存配置并重新启动

示例 1 - 将二氧化碳传感器与树莓派连接

我们将使用 K30 二氧化碳传感器(其文档可在此处找到,co2meters.com/Documentation/Datasheets/DS30-01%20-%20K30.pdf)。它的范围是 0-10,000 ppm,传感器通过串行端口以响应来自树莓派的特定命令提供二氧化碳浓度读数。

以下图显示了树莓派和 K30 二氧化碳传感器之间的连接:

与树莓派连接的 K30 二氧化碳传感器

传感器的接收器(Rx)引脚连接到树莓派 Zero 的发送器(Tx-GPIO 14(UART_TXD))引脚(前图中的黄色线)。传感器的发送器(Tx)引脚连接到树莓派 Zero 的接收器(Rx-GPIO 15(UART_RXD))引脚(前图中的绿色线)。

为了给传感器供电,传感器的 G+引脚(前图中的红线)连接到树莓派 Zero 的5V引脚。传感器的 G0 引脚连接到树莓派 Zero 的GND引脚(前图中的黑线)。

通常,串行端口通信是通过指定波特率、帧中的位数、停止位和流控来初始化的。

用于串行端口通信的 Python 代码

我们将使用pySerial库(pyserial.readthedocs.io/en/latest/shortintro.html#opening-serial-ports)来接口二氧化碳传感器:

  1. 根据传感器的文档,可以通过以波特率 9600、无奇偶校验、8 位和 1 个停止位初始化串行端口来读取传感器输出。 GPIO 串行端口为ttyAMA0。与传感器进行接口的第一步是初始化串行端口通信:
       import serial 
       ser = serial.Serial("/dev/ttyAMA0")
  1. 根据传感器文档(co2meters.com/Documentation/Other/SenseAirCommGuide.zip),传感器对二氧化碳浓度的以下命令做出响应:

从传感器数据表中借用的读取二氧化碳浓度的命令

  1. 命令可以如下传输到传感器:
       ser.write(bytearray([0xFE, 0x44, 0x00, 0x08, 0x02, 0x9F, 0x25]))
  1. 传感器以 7 个字节的响应做出响应,可以如下读取:
       resp = ser.read(7)
  1. 传感器的响应格式如下:

二氧化碳传感器响应

  1. 根据数据表,传感器数据大小为 2 个字节。每个字节可用于存储 0 和 255 的值。两个字节可用于存储高达 65,535 的值(255 * 255)。二氧化碳浓度可以根据消息计算如下:
       high = resp[3] 
       low = resp[4] 
       co2 = (high*256) + low
  1. 把它全部放在一起:
       import serial 
       import time 
       import array 
       ser = serial.Serial("/dev/ttyAMA0") 
       print("Serial Connected!") 
       ser.flushInput() 
       time.sleep(1) 

       while True: 
           ser.write(bytearray([0xFE, 0x44, 0x00, 0x08,
           0x02, 0x9F, 0x25])) 
           # wait for sensor to respond 
           time.sleep(.01) 
           resp = ser.read(7) 
           high = resp[3] 
           low = resp[4] 
           co2 = (high*256) + low 
           print() 
           print() 
           print("Co2 = " + str(co2)) 
           time.sleep(1)
  1. 将代码保存到文件并尝试执行它。

I2C 通信

I²C(Inter-Integrated Circuit)通信是一种串行通信类型,允许将多个传感器接口到计算机。 I²C 通信由时钟和数据线两根线组成。树莓派 Zero 的 I²C 通信的时钟和数据引脚分别为GPIO 3SCL)和GPIO 2SDA)。为了在同一总线上与多个传感器通信,通常通过 I²C 协议通信的传感器/执行器通常通过它们的 7 位地址进行寻址。可以有两个或更多树莓派板与同一 I²C 总线上的同一传感器进行通信。这使得可以在树莓派周围构建传感器网络。

I²C 通信线是开漏线路;因此,它们使用电阻上拉,如下图所示:

I²C 设置

让我们通过一个示例来回顾一下 I²C 通信。

示例 2 - PiGlow

PiGlow是树莓派的一个附加硬件,由 18 个 LED 与SN3218芯片接口。该芯片允许通过 I²C 接口控制 LED。芯片的 7 位地址为0x54

为了接口附加硬件,SCL引脚连接到GPIO 3SDA引脚连接到GPIO 2;地线引脚和电源引脚分别连接到附加硬件的对应引脚。

PiGlow 附带了一个抽象 I²C 通信的库:github.com/pimoroni/piglow

尽管该库是对 I²C 接口的封装,但我们建议阅读代码以了解操作 LED 的内部机制:

PiGlow 叠放在 Raspberry Pi 上

安装库

PiGlow 库可以通过从命令行终端运行以下命令来安装:

    curl get.pimoroni.com/piglow | bash

示例

安装完成后,切换到示例文件夹(/home/pi/Pimoroni/piglow)并运行其中一个示例:

    python3 bar.py

它应该运行闪烁灯效果,如下图所示:

PiGlow 上的闪烁灯

同样,还有库可以使用 I²C 通信与实时时钟、LCD 显示器等进行通信。如果你有兴趣编写自己的接口,提供 I²C 通信与传感器/输出设备的细节,请查看本书附带网站上的一些示例。

示例 3 - 用于树莓派的 Sensorian 附加硬件

Sensorian是为树莓派设计的附加硬件。这个附加硬件配备了不同类型的传感器,包括光传感器、气压计、加速度计、LCD 显示器接口、闪存存储器、电容触摸传感器和实时时钟。

这个附加硬件上的传感器足以学习本章讨论的所有通信接口的使用方法:

堆叠在树莓派 Zero 上的 Sensorian 硬件

在本节中,我们将讨论一个示例,我们将使用 I²C 接口通过树莓派 Zero 测量环境光水平。附加硬件板上的传感器是APDS-9300传感器(www.avagotech.com/docs/AV02-1077EN)。

用于光传感器的 I2C 驱动程序

传感器硬件的驱动程序可从 GitHub 存储库中获取(github.com/sensorian/sensorian-firmware.git)。让我们从命令行终端克隆存储库:

    git clone https://github.com/sensorian/sensorian-firmware.git 

让我们使用驱动程序(位于 ~/sensorian-firmware/Drivers_Python/APDS-9300文件夹中)从传感器的两个 ADC 通道读取值:

import time 
import APDS9300 as LuxSens 
import sys 

AmbientLight = LuxSens.APDS9300() 
while True: 
   time.sleep(1) 
   channel1 = AmbientLight.readChannel(1)                       
   channel2 = AmbientLight.readChannel(0) 
   Lux = AmbientLight.getLuxLevel(channel1,channel2) 
   print("Lux output: %d." % Lux)

有了两个通道的 ADC 值,驱动程序可以使用以下公式(从传感器数据表中检索)计算环境光值:

使用 ADC 值计算的环境光水平

这个计算是由属性getLuxLevel执行的。在正常照明条件下,环境光水平(以勒克斯为单位)约为2。当我们用手掌遮住光传感器时,测得的输出为0。这个传感器可以用来测量环境光,并相应地调整房间照明。

挑战

我们讨论了使用光传感器测量环境光水平。我们如何利用光输出(环境光水平)来控制房间照明?

SPI 接口

还有一种名为串行外围接口SPI)的串行通信接口。必须通过raspi-config启用此接口(这类似于在本章前面启用串行端口接口)。使用 SPI 接口类似于 I²C 接口和串行端口。

通常,SPI 接口由时钟线、数据输入、数据输出和从机选择SS)线组成。与 I²C 通信不同(在那里我们可以连接多个主机),在同一总线上可以有一个主机(树莓派 Zero),但可以有多个从机。SS引脚用于选择树莓派 Zero 正在读取/写入数据的特定传感器,当同一总线上连接了多个传感器时。

示例 4 - 写入外部存储器芯片

让我们查看一个示例,我们将通过 SPI 接口向 Sensorian 附加硬件上的闪存存储器写入数据。SPI 接口和存储器芯片的驱动程序可从同一 GitHub 存储库中获取。

由于我们已经下载了驱动程序,让我们查看一下驱动程序中提供的示例:

import sys 
import time   
import S25FL204K as Memory

让我们初始化并将消息hello写入存储器:

Flash_memory = Memory.S25FL204K() 
Flash_memory.writeStatusRegister(0x00) 
message = "hello" 
flash_memory.writeArray(0x000000,list(message), message.len())

现在,让我们尝试读取刚刚写入外部存储器的数据:

data = flash_memory.readArray(0x000000, message.len()) 
print("Data Read from memory: ") 
print(''.join(data))

本章提供了代码示例,可通过下载获得(memory_test.py)。

我们成功地演示了使用 SPI 读/写外部存储器芯片。

向读者提出挑战

在这里的图中,有一个 LED 灯带(www.adafruit.com/product/306)与树莓派附加硬件的 SPI 接口相连,使用了 Adafruit Cobbler(www.adafruit.com/product/914)。我们提供了一个线索,说明如何将 LED 灯带与树莓派 Zero 相连。我们希望看到您能否自己找到将 LED 灯带与树莓派 Zero 相连的解决方案。请参考本书网站获取答案。

LED 灯带与树莓派 Zero 的 Adafruit Cobbler 接口

总结

在本章中,我们讨论了树莓派 Zero 上可用的不同通信接口。这些接口包括 I²C、SPI 和 UART。我们将在我们的最终项目中使用这些接口。我们使用了二氧化碳传感器、LED 驱动器和传感器平台来讨论这些接口。在下一章中,我们将讨论面向对象编程及其独特的优势。我们将通过一个例子讨论面向对象编程的必要性。面向对象编程在您需要编写自己的驱动程序来控制机器人的组件或编写传感器的接口库的情况下尤其有帮助。

第十三章:Python 中的数据类型和面向对象编程

在本章中,我们将讨论 Python 中的数据类型和面向对象编程OOP)。我们将讨论 Python 中的列表、字典、元组和集合等数据类型。我们还将讨论 OOP,它的必要性以及如何在树莓派基于项目中编写面向对象的代码(例如,使用 OOP 来控制家用电器)。我们将讨论在树莓派 Zero 项目中使用 OOP。

列表

在 Python 中,列表是一种数据类型(其文档在此处可用,docs.python.org/3.4/tutorial/datastructures.html#),可用于按顺序存储元素。

本章讨论的主题如果不在实践中使用很难理解。任何使用此符号表示的示例:>>>都可以使用 Python 解释器进行测试。

列表可以包含字符串、对象(在本章中详细讨论)或数字等。例如,以下是列表的示例:

    >>> sequence = [1, 2, 3, 4, 5, 6]
 >>> example_list = ['apple', 'orange', 1.0, 2.0, 3]

在前面的一系列示例中,sequence列表包含介于16之间的数字,而example_list列表包含字符串、整数和浮点数的组合。列表用方括号([])表示。项目可以用逗号分隔添加到列表中:

    >>> type(sequence)
 <class 'list'>

由于列表是有序元素的序列,可以通过使用for循环遍历列表元素来获取列表的元素,如下所示:

for item in sequence: 
    print("The number is ", item)

输出如下:

 The number is  1
 The number is  2
 The number is  3
 The number is  4
 The number is  5
 The number is  6

由于 Python 的循环可以遍历一系列元素,它会获取每个元素并将其赋值给item。然后将该项打印到控制台上。

可以在列表上执行的操作

在 Python 中,可以使用dir()方法检索数据类型的属性。例如,可以检索sequence列表的可用属性如下:

    >>> dir(sequence)
 ['__add__', '__class__', '__contains__', '__delattr__',
    '__delitem__', '__dir__', '__doc__', '__eq__',
    '__format__', '__ge__', '__getattribute__', '__getitem__',
    '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', 
    '__iter__', '__le__', '__len__', '__lt__', '__mul__',
    '__ne__', '__new__', '__reduce__', '__reduce_ex__',
    '__repr__', '__reversed__', '__rmul__', '__setattr__', 
    '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 
    'append', 'clear', 'copy', 'count', 'extend', 'index',
    'insert', 'pop', 'remove', 'reverse', 'sort']

这些属性使得可以在列表上执行不同的操作。让我们详细讨论每个属性。

向列表添加元素:

可以使用append()方法添加元素:

    >>> sequence.append(7)
 >>> sequence
 [1, 2, 3, 4, 5, 6, 7]

从列表中删除元素:

remove()方法找到元素的第一个实例(传递一个参数)并将其从列表中删除。让我们考虑以下示例:

  • 示例 1
       >>> sequence = [1, 1, 2, 3, 4, 7, 5, 6, 7]
 >>> sequence.remove(7)
 >>> sequence
 [1, 1, 2, 3, 4, 5, 6, 7]
  • 示例 2
       >>> sequence.remove(1)
 >>> sequence
 [1, 2, 3, 4, 5, 6, 7]
  • 示例 3
       >>> sequence.remove(1)
 >>> sequence
 [2, 3, 4, 5, 6, 7]

检索元素的索引

index()方法返回列表中元素的位置:

    >>> index_list = [1, 2, 3, 4, 5, 6, 7]
 >>> index_list.index(5)
 4

在这个例子中,该方法返回元素5的索引。由于 Python 使用从 0 开始的索引,因此元素5的索引为4

    random_list = [2, 2, 4, 5, 5, 5, 6, 7, 7, 8]
 >>> random_list.index(5)
 3

在这个例子中,该方法返回元素的第一个实例的位置。元素5位于第三个位置。

从列表中弹出一个元素

pop()方法允许从指定位置删除一个元素并返回它:

    >>> index_list = [1, 2, 3, 4, 5, 6, 7]
 >>> index_list.pop(3)
 4
 >>> index_list
 [1, 2, 3, 5, 6, 7]

在这个例子中,index_list列表包含介于17之间的数字。通过传递索引位置(3)作为参数弹出第三个元素时,数字4从列表中移除并返回。

如果没有为索引位置提供参数,则弹出并返回最后一个元素:

    >>> index_list.pop()
 7
 >>> index_list
 [1, 2, 3, 5, 6]

在这个例子中,最后一个元素(7)被弹出并返回。

计算元素的实例数量:

count()方法返回元素在列表中出现的次数。例如,该元素在列表random_list中出现两次。

 >>> random_list = [2, 9, 8, 4, 3, 2, 1, 7] >>> random_list.count(2) 2

在特定位置插入元素:

insert()方法允许在列表中的特定位置添加一个元素。例如,让我们考虑以下示例:

    >>> day_of_week = ['Monday', 'Tuesday', 'Thursday',
    'Friday', 'Saturday']

在列表中,Wednesday缺失。它需要被放置在TuesdayThursday之间的位置 2(Python 使用零基索引,即元素的位置/索引从 0、1、2 等开始计数)。可以使用 insert 添加如下:

    >>> day_of_week.insert(2, 'Wednesday')
 >>> day_of_week
 ['Monday', 'Tuesday', 'Wednesday', 'Thursday',
    'Friday', 'Saturday']

读者的挑战

在前面的列表中,缺少 Sunday。使用列表的 insert 属性将其插入到正确的位置。

扩展列表

可以使用 extend() 方法将两个列表合并。day_of_weeksequence 列表可以合并如下:

    >>> day_of_week.extend(sequence)
 >>> day_of_week
 ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday',
    'Saturday', 1, 2, 3, 4, 5, 6]

列表也可以组合如下:

    >>> [1, 2, 3] + [4, 5, 6]
 [1, 2, 3, 4, 5, 6]

还可以将一个列表作为另一个列表的元素添加:

    sequence.insert(6, [1, 2, 3])
 >>> sequence
 [1, 2, 3, 4, 5, 6, [1, 2, 3]]

清除列表的元素

可以使用 clear() 方法删除列表的所有元素:

    >>> sequence.clear()
 >>> sequence
 []

对列表的元素进行排序

列表的元素可以使用 sort() 方法进行排序:

    random_list = [8, 7, 5, 2, 2, 5, 7, 5, 6, 4]
 >>> random_list.sort()
 >>> random_list
 [2, 2, 4, 5, 5, 5, 6, 7, 7, 8]

当列表由一组字符串组成时,它们按照字母顺序排序:

    >>> day_of_week = ['Monday', 'Tuesday', 'Thursday',
    'Friday', 'Saturday']
 >>> day_of_week.sort()
 >>> day_of_week
 ['Friday', 'Monday', 'Saturday', 'Thursday', 'Tuesday']

颠倒列表中的元素顺序

reverse() 方法使列表元素的顺序颠倒:

    >>> random_list = [8, 7, 5, 2, 2, 5, 7, 5, 6, 4]
 >>> random_list.reverse()
 >>> random_list
 [4, 6, 5, 7, 5, 2, 2, 5, 7, 8]

创建列表的副本

copy() 方法可以创建列表的副本:

    >>> copy_list = random_list.copy()
 >>> copy_list
 [4, 6, 5, 7, 5, 2, 2, 5, 7, 8]

访问列表元素

可以通过指定 list_name[i] 的索引位置来访问列表的元素。例如,可以按照以下方式访问 random_list 列表的第零个元素:

 >>> random_list = [4, 6, 5, 7, 5, 2, 2, 5, 7, 8] 
 >>> random_list[0]4>>> random_list[3]7

访问列表中的一组元素

可以访问指定索引之间的元素。例如,可以检索索引为 2 和 4 之间的所有元素:

    >>> random_list[2:5]
 [5, 7, 5]

可以按照以下方式访问列表的前六个元素:

    >>> random_list[:6]
 [4, 6, 5, 7, 5, 2]

可以按照以下方式以相反的顺序打印列表的元素:

    >>> random_list[::-1]
 [8, 7, 5, 2, 2, 5, 7, 5, 6, 4]

可以按照以下方式获取列表中的每个第二个元素:

    >>> random_list[::2]
 [4, 5, 5, 2, 7]

还可以跳过前两个元素后获取第二个元素之后的每个第二个元素:

    >>> random_list[2::2]
 [5, 5, 2, 7]

列表成员

可以使用 in 关键字检查一个值是否是列表的成员。例如:

 >>> random_list = [2, 1, 0, 8, 3, 1, 10, 9, 5, 4]

在这个列表中,我们可以检查数字 6 是否是成员:

    >>> 6 in random_list
 False
 >>> 4 in random_list
 True

让我们构建一个简单的游戏!

这个练习由两部分组成。在第一部分中,我们将回顾构建一个包含在 010 之间的十个随机数的列表。第二部分是给读者的一个挑战。执行以下步骤:

  1. 第一步是创建一个空列表。让我们创建一个名为 random_list 的空列表。可以按照以下方式创建一个空列表:
       random_list = []
  1. 我们将使用 Python 的 random 模块 (docs.python.org/3/library/random.html) 生成随机数。为了生成在 010 之间的随机数,我们将使用 random 模块的 randint() 方法。
       random_number = random.randint(0,10)
  1. 让我们将生成的数字附加到列表中。使用 for 循环重复此操作 10 次:
       for index in range(0,10):
             random_number = random.randint(0, 10)
             random_list.append(random_number)
       print("The items in random_list are ")
       print(random_list)
  1. 生成的列表看起来像这样:
       The items in random_list are
 [2, 1, 0, 8, 3, 1, 10, 9, 5, 4]

我们讨论了生成一个随机数列表。下一步是接受用户输入,我们要求用户猜一个在 010 之间的数字。如果数字是列表的成员,则打印消息 你的猜测是正确的,否则打印消息 对不起!你的猜测是错误的。我们将第二部分留给读者作为挑战。使用本章提供的 list_generator.py 代码示例开始。

字典

字典 (docs.python.org/3.4/tutorial/datastructures.html#dictionaries) 是一个无序的键值对集合的数据类型。字典中的每个键都有一个相关的值。字典的一个示例是:

 >>> my_dict = {1: "Hello", 2: "World"} >>> my_dict   
 {1: 'Hello', 2: 'World'}

通过使用大括号 {} 创建字典。在创建时,新成员以以下格式添加到字典中:key: value(如前面的示例所示)。在前面的示例中,12 是键,而 'Hello''World' 是相关的值。添加到字典的每个值都需要有一个相关的键。

字典的元素没有顺序,即不能按照添加的顺序检索元素。可以通过遍历键来检索字典的值。让我们考虑以下示例:

 >>> my_dict = {1: "Hello", 2: "World", 3: "I", 4: "am",
    5: "excited", 6: "to", 7: "learn", 8: "Python" }

有几种方法可以打印字典的键或值:

 >>> for key in my_dict: ... 
 print(my_dict[value]) 
 ... Hello World I 
 am excited to learn Python

在前面的示例中,我们遍历字典的键并使用键my_dict[key]检索值。还可以使用字典中可用的values()方法检索值:

 >>> for value in my_dict.values(): ... 

print(value) ... Hello World I am excited to learn Python

字典的键可以是整数、字符串或元组。字典的键需要是唯一的且不可变的,即创建后无法修改。无法创建键的重复项。如果向现有键添加新值,则字典中将存储最新值。让我们考虑以下示例:

  • 可以按以下方式向字典添加新的键/值对:
 >>> my_dict[9] = 'test' >>> my_dict {1: 'Hello', 2: 'World', 3: 'I', 4: 'am', 5: 'excited',
       6: 'to', 7: 'learn', 8: 'Python', 9: 'test'}
  • 让我们尝试创建键9的重复项:
 >>> my_dict[9] = 'programming' >>> my_dict {1: 'Hello', 2: 'World', 3: 'I', 4: 'am', 5: 'excited',
       6: 'to', 7: 'learn', 8: 'Python', 9: 'programming'}
  • 如前面的示例所示,当我们尝试创建重复项时,现有键的值会被修改。

  • 可以将多个值与一个键关联。例如,作为列表或字典:

 >>> my_dict = {1: "Hello", 2: "World", 3: "I", 4: "am",
      "values": [1, 2, 3,4, 5], "test": {"1": 1, "2": 2} } 

字典在解析 CSV 文件并将每一行与唯一键关联的场景中非常有用。字典也用于编码和解码 JSON 数据

元组

元组(发音为two-pletuh-ple)是一种不可变的数据类型,按顺序排列并用逗号分隔。可以按以下方式创建元组:

 >>> my_tuple = 1, 2, 3, 4, 5
 >>> my_tuple (1, 2, 3, 4, 5)

由于元组是不可变的,因此无法修改给定索引处的值:

    >>> my_tuple[1] = 3
 Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 TypeError: 'tuple' object does not support item assignment

元组可以由数字、字符串或列表组成。由于列表是可变的,如果列表是元组的成员,则可以修改。例如:

    >>> my_tuple = 1, 2, 3, 4, [1, 2, 4, 5]
 >>> my_tuple[4][2] = 3
 >>> my_tuple
 (1, 2, 3, 4, [1, 2, 3, 5])

元组在值无法修改的情况下特别有用。元组还用于从函数返回值。让我们考虑以下示例:

 >>> for value in my_dict.items(): ... 

 print(value) 
 ...
 (1, 'Hello') (2, 'World') (3, 'I') (4, 'am') ('test', {'1': 1, '2': 2}) ('values', [1, 2, 3, 4, 5])

在前面的示例中,items()方法返回一个元组列表。

集合

集合(docs.python.org/3/tutorial/datastructures.html#sets)是一个无序的不可变元素的集合,不包含重复条目。可以按以下方式创建集合:

 >>> my_set = set([1, 2, 3, 4, 5]) >>> my_set {1, 2, 3, 4, 5}

现在,让我们向这个集合添加一个重复的列表:

 >>> my_set.update([1, 2, 3, 4, 5]) >>> my_set {1, 2, 3, 4, 5}

集合可以避免重复条目并保存唯一条目。可以将单个元素添加到集合中,如下所示:

 >>> my_set = set([1, 2, 3, 4, 5]) >>> my_set.add(6)
 >>> my_set
 {1, 2, 3, 4, 5, 6}

集合用于测试元素在不同集合中的成员资格。有与成员资格测试相关的不同方法。我们建议使用集合的文档来了解每种方法(运行help(my_set)以查找成员资格测试的不同方法)。

Python 中的面向对象编程

面向对象编程有助于简化代码并简化应用程序开发。在重用代码方面尤其有用。面向对象的代码使您能够重用使用通信接口的传感器的代码。例如,所有配有 UART 端口的传感器可以使用面向对象的代码进行分组。

面向对象编程的一个例子是GPIO Zero 库www.raspberrypi.org/blog/gpio-zero-a-friendly-python-api-for-physical-computing/),在之前的章节中使用过。实际上,在 Python 中一切都是对象。

面向对象的代码在与其他人合作项目时特别有帮助。例如,您可以使用 Python 中的面向对象的代码实现传感器驱动程序并记录其用法。这使其他开发人员能够开发应用程序,而无需关注传感器接口背后的细节。面向对象编程为应用程序提供了模块化,简化了应用程序开发。我们将在本章中回顾一个示例,演示面向对象编程的优势。在本章中,我们将利用面向对象编程为我们的项目带来模块化。

让我们开始吧!

重新审视学生 ID 卡示例

让我们重新访问第十章中的身份证示例,算术运算、循环和闪烁灯input_test.py)。我们讨论了编写一个简单的程序,用于捕获和打印属于一个学生的信息。学生的联系信息可以按以下方式检索和存储:

name = input("What is your name? ") 
address = input("What is your address? ") 
age = input("How old are you? ")

现在,考虑一个情景,需要保存和在程序执行期间的任何时刻检索 10 个学生的信息。我们需要为用于保存学生信息的变量想出一个命名规范。如果我们使用 30 个不同的变量来存储每个学生的信息,那将会是一团糟。这就是面向对象编程可以真正帮助的地方。

让我们使用面向对象编程来重新编写这个例子,以简化问题。面向对象编程的第一步是声明对象的结构。这是通过定义一个类来完成的。类确定了对象的功能。让我们编写一个 Python 类,定义学生对象的结构。

由于我们将保存学生信息,所以类将被称为Student。类是使用class关键字定义的,如下所示:

class Student(object):

因此,定义了一个名为Student的类。每当创建一个新对象时,Python 会在内部调用__init__()方法。

这个方法是在类内定义的:

class Student(object): 
    """A Python class to store student information""" 

    def __init__(self, name, address, age): 
        self.name = name 
        self.address = address 
        self.age = age

在这个例子中,__init__方法的参数包括nameageaddress。这些参数被称为属性。这些属性使得可以创建一个属于Student类的唯一对象。因此,在这个例子中,在创建Student类的实例时,需要nameageaddress这些属性作为参数。

让我们创建一个属于Student类的对象(也称为实例):

student1 = Student("John Doe", "123 Main Street, Newark, CA", "29")

在这个例子中,我们创建了一个属于Student类的对象,称为student1,其中John Doe(姓名)、29(年龄)和123 Main Street, Newark, CA(地址)是创建对象所需的属性。当我们创建一个属于Student类的对象时,通过传递必要的参数(在Student类的__init__()方法中声明的),__init__()方法会自动调用以初始化对象。初始化后,与student1相关的信息将存储在对象student1下。

现在,属于student1的信息可以按以下方式检索:

print(student1.name) 
print(student1.age) 
print(student1.address)

现在,让我们创建另一个名为student2的对象:

student2 = Student("Jane Doe", "123 Main Street, San Jose, CA", "27")

我们创建了两个对象,分别称为student1student2。每个对象的属性都可以通过student1.namestudent2.name等方式访问。在没有面向对象编程的情况下,我们将不得不创建变量,如student1_namestudent1_agestudent1_addressstudent2_namestudent2_agestudent2_address等。因此,面向对象编程使得代码模块化。

向类添加方法

让我们为我们的Student类添加一些方法,以帮助检索学生的信息:

class Student(object): 
    """A Python class to store student information""" 

    def __init__(self, name, age, address): 
        self.name = name 
        self.address = address 
        self.age = age 

    def return_name(self): 
        """return student name""" 
        return self.name 

    def return_age(self): 
        """return student age""" 
        return self.age 

    def return_address(self): 
        """return student address""" 
        return self.address

在这个例子中,我们添加了三个方法,分别是return_name()return_age()return_address(),它们分别返回属性nameageaddress。类的这些方法被称为可调用属性。让我们回顾一个快速的例子,我们在其中使用这些可调用属性来打印对象的信息。

student1 = Student("John Doe", "29", "123 Main Street, Newark, CA") 
print(student1.return_name()) 
print(student1.return_age()) 
print(student1.return_address())

到目前为止,我们讨论了检索有关学生的信息的方法。让我们在我们的类中包含一个方法,使得学生的信息可以更新。现在,让我们在类中添加另一个方法,使学生可以更新地址:

def update_address(self, address): 
    """update student address""" 
    self.address = address 
    return self.address

让我们比较更新地址之前和之后的student1对象的地址:

print(student1.address()) 
print(student1.update_address("234 Main Street, Newark, CA"))

这将在屏幕上打印以下输出:

    123 Main Street, Newark, CA
 234 Main Street, Newark, CA

因此,我们已经编写了我们的第一个面向对象的代码,演示了模块化代码的能力。前面的代码示例可与本章一起下载,名称为student_info.py

Python 中的文档字符串

在面向对象的示例中,您可能已经注意到了一个用三个双引号括起来的句子:

    """A Python class to store student information"""

这被称为文档字符串。文档字符串用于记录有关类或方法的信息。文档字符串在尝试存储与方法或类的使用相关的信息时特别有帮助(稍后将在本章中演示)。文档字符串还用于在文件开头存储与应用程序或代码示例相关的多行注释。Python 解释器会忽略文档字符串,它们旨在为其他程序员提供有关类的文档。

同样,Python 解释器会忽略以#符号开头的任何单行注释。单行注释通常用于对一块代码做特定的注释。包括结构良好的注释可以使您的代码易读。

例如,以下代码片段通知读者,生成并存储在变量rand_num中的随机数在09之间:

# generate a random number between 0 and 9 
rand_num = random.randrange(0,10)

相反,提供没有上下文的注释将会让审阅您的代码的人感到困惑:

# Todo: Fix this later

当您以后重新访问代码时,很可能您可能无法回忆起需要修复什么。

self

在我们的面向对象的示例中,每个方法的第一个参数都有一个名为self的参数。self指的是正在使用的类的实例,self关键字用作与类的实例交互的方法中的第一个参数。在前面的示例中,self指的是对象student1。它相当于初始化对象并访问它如下:

Student(student1, "John Doe", "29", "123 Main Street, Newark, CA") 
Student.return_address(student1)

在这种情况下,self关键字简化了我们访问对象属性的方式。现在,让我们回顾一些涉及树莓派的 OOP 的例子。

扬声器控制器

让我们编写一个 Python 类(下载的tone_player.py),它会播放一个音乐音调,指示您的树莓派已完成启动。对于本节,您将需要一个 USB 声卡和一个连接到树莓派的 USB 集线器的扬声器。

让我们称我们的类为TonePlayer。这个类应该能够控制扬声器音量,并在创建对象时播放任何传递的文件:

class TonePlayer(object): 
    """A Python class to play boot-up complete tone""" 

    def __init__(self, file_name): 
        self.file_name = file_name

在这种情况下,必须传递给TonePlayer类要播放的文件的参数。例如:

       tone_player = TonePlayer("/home/pi/tone.wav")

我们还需要能够设置要播放音调的音量级别。让我们添加一个执行相同操作的方法:

def set_volume(self, value): 
    """set tone sound volume""" 
    subprocess.Popen(["amixer", "set", "'PCM'", str(value)], 
    shell=False)

set_volume方法中,我们使用 Python 的subprocess模块来运行调整声音驱动器音量的 Linux 系统命令。

这个类最重要的方法是play命令。当调用play方法时,我们需要使用 Linux 的play命令播放音调声音:

def play(self):
    """play the wav file"""
    subprocess.Popen(["aplay", self.file_name], shell=False)

把它全部放在一起:

import subprocess 

class TonePlayer(object): 
    """A Python class to play boot-up complete tone""" 

    def __init__(self, file_name): 
        self.file_name = file_name 

    def set_volume(self, value): 
        """set tone sound volume""" 
        subprocess.Popen(["amixer", "set", "'PCM'", str(value)],
        shell=False) 

    def play(self): 
        """play the wav file""" 
        subprocess.Popen(["aplay", self.file_name], shell=False) 

if __name__ == "__main__": 
    tone_player = TonePlayer("/home/pi/tone.wav") 
    tone_player.set_volume(75) 
    tone_player.play()

TonePlayer类保存到您的树莓派(保存为名为tone_player.py的文件),并使用来自freesoundwww.freesound.org/people/zippi1/sounds/18872/)等来源的音调声音文件。将其保存到您选择的位置并尝试运行代码。它应该以所需的音量播放音调声音!

现在,编辑/etc/rc.local并在文件末尾添加以下行(在exit 0行之前):

python3 /home/pi/toneplayer.py

这应该在 Pi 启动时播放一个音调!

灯光控制守护程序

让我们回顾另一个例子,在这个例子中,我们使用 OOP 实现了一个简单的守护程序,它在一天中的指定时间打开/关闭灯光。为了能够在预定时间执行任务,我们将使用schedule库(github.com/dbader/schedule)。可以按照以下方式安装它:

    sudo pip3 install schedule

让我们称我们的类为LightScheduler。它应该能够接受开启和关闭灯光的开始和结束时间。它还应该提供覆盖功能,让用户根据需要开启/关闭灯光。假设灯光是使用PowerSwitch Tail IIwww.powerswitchtail.com/Pages/default.aspx)来控制的。它的接口如下:

树莓派 Zero 与 PowerSwitch Tail II 的接口

以下是创建的LightSchedular类:

class LightScheduler(object): 
    """A Python class to turn on/off lights""" 

    def __init__(self, start_time, stop_time): 
        self.start_time = start_time 
        self.stop_time = stop_time 
        # lamp is connected to GPIO pin2.
        self.lights = OutputDevice(2)

每当创建LightScheduler的实例时,GPIO 引脚被初始化以控制 PowerSwitch Tail II。现在,让我们添加开启/关闭灯光的方法:

def init_schedule(self): 
        # set the schedule 
        schedule.every().day.at(self.start_time).do(self.on) 
        schedule.every().day.at(self.stop_time).do(self.off) 

    def on(self): 
        """turn on lights""" 
        self.lights.on() 

    def off(self): 
        """turn off lights""" 
        self.lights.off()

init_schedule()方法中,传递的开始和结束时间被用来初始化schedule,以便在指定的时间开启/关闭灯光。

把它放在一起,我们有:

import schedule 
import time 
from gpiozero import OutputDevice 

class LightScheduler(object): 
    """A Python class to turn on/off lights""" 

    def __init__(self, start_time, stop_time): 
        self.start_time = start_time 
        self.stop_time = stop_time 
        # lamp is connected to GPIO pin2.
        self.lights = OutputDevice(2) 

    def init_schedule(self): 
        # set the schedule 
        schedule.every().day.at(self.start_time).do(self.on) 
        schedule.every().day.at(self.stop_time).do(self.off) 

    def on(self): 
        """turn on lights""" 
        self.lights.on() 

    def off(self): 
        """turn off lights""" 
        self.lights.off() 

if __name__ == "__main__": 
    lamp = LightScheduler("18:30", "9:30") 
    lamp.on() 
    time.sleep(50) 
    lamp.off() 
    lamp.init_schedule() 
    while True:
        schedule.run_pending() 
        time.sleep(1)

在上面的例子中,灯光被安排在下午 6:30 开启,并在上午 9:30 关闭。一旦工作被安排,程序就会进入一个无限循环,等待任务执行。这个例子可以作为守护进程运行,通过在启动时执行文件(在/etc/rc.local中添加一行light_scheduler.py)。安排完工作后,它将继续作为后台守护进程运行。

这只是面向初学者的 OOP 及其应用的基本介绍。请参考本书网站以获取更多关于 OOP 的例子。

总结

在本章中,我们讨论了列表和 OOP 的优势。我们使用树莓派作为例子的中心,讨论了 OOP 的例子。由于本书主要面向初学者,我们决定在讨论例子时坚持 OOP 的基础知识。书中还有一些超出范围的高级方面。我们让读者通过本书网站上提供的其他例子来学习高级概念。

第十四章:文件 I/O 和 Python 工具

在本章中,我们将详细讨论文件 I/O,即读取、写入和追加文件。我们还将讨论 Python 工具,这些工具使得操作文件和与操作系统交互成为可能。每个主题都有不同的复杂程度,我们将通过一个例子来讨论。让我们开始吧!

文件 I/O

我们讨论文件 I/O 有两个原因:

  • 在 Linux 操作系统的世界中,一切都是文件。与树莓派上的外围设备交互类似于读取/写入文件。例如:在第十二章中,通信接口,我们讨论了串口通信。您应该能够观察到串口通信类似于文件读写操作。

  • 我们在每个项目中以某种形式使用文件 I/O。例如:将传感器数据写入 CSV 文件,或者读取 Web 服务器的预配置选项等。

因此,我们认为讨论 Python 中的文件 I/O 作为一个单独的章节会很有用(详细文档请参阅:docs.python.org/3/tutorial/inputoutput.html#reading-and-writing-files),并讨论它在开发树莓派 Zero 应用程序时可能发挥作用的示例。

从文件中读取

让我们创建一个简单的文本文件read_file.txt,其中包含以下文本:我正在使用树莓派 Zero 学习 Python 编程,并将其保存到代码示例目录(或您选择的任何位置)。

要从文件中读取,我们需要使用 Python 的内置函数:open来打开文件。让我们快速看一下一个代码片段,演示如何打开一个文本文件以读取其内容并将其打印到屏幕上:

if __name__ == "__main__":
    # open text file to read
    file = open('read_line.txt', 'r')
    # read from file and store it to data
    data = file.read()
    print(data)
    file.close()

让我们详细讨论这段代码片段:

  1. 读取文本文件内容的第一步是使用内置函数open打开文件。需要将所需的文件作为参数传递,并且还需要一个标志r,表示我们打开文件以读取内容(随着我们讨论每个读取/写入文件时,我们将讨论其他标志选项)。

  2. 打开文件时,open函数返回一个指针(文件对象的地址),并将其存储在file变量中。

       file = open('read_line.txt', 'r')
  1. 这个文件指针用于读取文件的内容并将其打印到屏幕上:
       data = file.read() 
       print(data)
  1. 读取文件的内容后,通过调用close()函数关闭文件。

运行前面的代码片段(可与本章一起下载的read_from_file.py)使用 IDLE3 或命令行终端。文本文件的内容将如下打印到屏幕上:

    I am learning Python Programming using the Raspberry Pi Zero

读取行

有时,有必要逐行读取文件的内容。在 Python 中,有两种选项可以做到这一点:readline()readlines()

  • readline(): 正如其名称所示,这个内置函数使得逐行读取成为可能。让我们通过一个例子来复习一下:
       if __name__ == "__main__": 
          # open text file to read
          file = open('read_line.txt', 'r') 

          # read a line from the file
          data = file.readline() 
          print(data) 

          # read another line from the file 
          data = file.readline() 
          print(data) 

          file.close()

当执行前面的代码片段(可与本章一起下载,文件名为read_line_from_file.py)时,read_line.txt文件被打开,并且readline()函数返回一行。这一行被存储在变量 data 中。由于该函数在程序中被调用两次,输出如下:

 I am learning Python Programming using the Raspberry Pi Zero. 

 This is the second line.

每次调用readline函数时都会返回一个新行,并且当到达文件结尾时会返回一个空字符串。

  • readlines(): 这个函数逐行读取文件的全部内容,并将每一行存储到一个列表中:
       if __name__ == "__main__": 
           # open text file to read
           file = open('read_lines.txt', 'r') 

           # read a line from the file
           data = file.readlines() 
           for line in data: 
               print(line) 

           file.close()

由于文件的行被存储为一个列表,可以通过对列表进行迭代来检索它:

       data = file.readlines() 
           for line in data: 
               print(line)

前面的代码片段可与本章一起下载,文件名为read_lines_from_file.py

写入文件

按照以下步骤进行写入文件:

  1. 写入文件的第一步是使用写入标志w打开文件。如果作为参数传递的文件名不存在,将创建一个新文件:
      file = open('write_file.txt', 'w')
  1. 文件打开后,下一步是将要写入的字符串作为参数传递给write()函数:
      file.write('I am excited to learn Python using
      Raspberry Pi Zero')
  1. 让我们将代码放在一起,我们将一个字符串写入文本文件,关闭它,重新打开文件并将文件的内容打印到屏幕上:
       if __name__ == "__main__": 
          # open text file to write
          file = open('write_file.txt', 'w') 
          # write a line from the file
          file.write('I am excited to learn Python using
          Raspberry Pi Zero \n') 
          file.close() 

          file = open('write_file.txt', 'r') 
          data = file.read() 
          print(data) 
          file.close()
  1. 前面的代码片段可与本章一起下载(write_to_file.py)。

  2. 当执行前面的代码片段时,输出如下所示:

       I am excited to learn Python using Raspberry Pi Zero

追加到文件

每当使用写入标志w打开文件时,文件的内容都会被删除,并重新打开以写入数据。还有一个叫做a的替代标志,它使得可以将数据追加到文件的末尾。如果打开的文件(作为打开的参数)不存在,这个标志也会创建一个新文件。让我们考虑下面的代码片段,我们将一行追加到上一节中的文本文件write_file.txt中:

if __name__ == "__main__": 
   # open text file to append
   file = open('write_file.txt', 'a') 
   # append a line from the file
   file.write('This is a line appended to the file\n') 
   file.close() 

   file = open('write_file.txt', 'r') 
   data = file.read() 
   print(data) 
   file.close()

当执行前面的代码片段(可与本章一起下载的append_to_file.py)时,字符串This is a line appended to the file将被追加到文件的文本末尾。文件的内容将包括以下内容:

    I am excited to learn Python using Raspberry Pi Zero
 This is a line appended to the file

寻找

一旦文件被打开,文件 I/O 中使用的文件指针会从文件的开头移动到文件的末尾。可以将指针移动到特定位置并从该位置读取数据。当我们对文件的特定行感兴趣时,这是非常有用的。让我们考虑上一个例子中的文本文件write_file.txt。文件的内容包括:

    I am excited to learn Python using Raspberry Pi Zero
 This is a line appended to the file

让我们尝试跳过第一行,只读取第二行,使用seek

if __name__ == "__main__": 
   # open text file to read

   file = open('write_file.txt', 'r') 

   # read the second line from the file
   file.seek(53) 

   data = file.read() 
   print(data) 
   file.close()

在前面的例子中(可与本章一起下载的seek_in_file.py),seek函数用于将指针移动到字节53,即第一行的末尾。然后文件的内容被读取并存储到变量中。当执行这个代码片段时,输出如下所示:

    This is a line appended to the file

因此,seek 使得移动文件指针到特定位置成为可能。

读取 n 个字节

seek函数使得将指针移动到特定位置并从该位置读取一个字节或n个字节成为可能。让我们重新阅读write_file.txt,并尝试读取句子I am excited to learn Python using Raspberry Pi Zero中的单词excited

if __name__ == "__main__": 
   # open text file to read and write 
   file = open('write_file.txt', 'r') 

   # set the pointer to the desired position 
   file.seek(5) 
   data = file.read(1) 
   print(data) 

   # rewind the pointer
   file.seek(5) 
   data = file.read(7) 
   print(data) 
   file.close()

前面的代码可以通过以下步骤来解释:

  1. 第一步,使用read标志打开文件,并将文件指针设置为第五个字节(使用seek)——文本文件内容中字母e的位置。

  2. 现在,我们通过将文件作为参数传递给read函数来从文件中读取一个字节。当整数作为参数传递时,read函数会从文件中返回相应数量的字节。当没有传递参数时,它会读取整个文件。如果文件为空,read函数会返回一个空字符串:

       file.seek(5) 
       data = file.read(1) 
       print(data)
  1. 在第二部分中,我们尝试从文本文件中读取单词excited。我们将指针的位置倒回到第五个字节。然后我们从文件中读取七个字节(单词excited的长度)。

  2. 当执行代码片段时(可与本章一起下载的seek_to_read.py),程序应该打印字母e和单词excited

       file.seek(5) 
       data = file.read(7) 
       print(data)

r+

我们讨论了使用rw标志读取和写入文件。还有另一个叫做r+的标志。这个标志使得可以对文件进行读取和写入。让我们回顾一个例子,以便理解这个标志。

让我们再次回顾write_file.txt的内容:

    I am excited to learn Python using Raspberry Pi Zero
 This is a line appended to the file

让我们修改第二行,改为:This is a line that was modified。代码示例可与本章一起下载(seek_to_write.py)。

if __name__ == "__main__": 
   # open text file to read and write 
   file = open('write_file.txt', 'r+') 

   # set the pointer to the desired position 
   file.seek(68) 
   file.write('that was modified \n') 

   # rewind the pointer to the beginning of the file
   file.seek(0) 
   data = file.read() 
   print(data) 
   file.close()

让我们回顾一下这个例子是如何工作的:

  1. 这个例子的第一步是使用r+标志打开文件。这使得可以对文件进行读取和写入。

  2. 接下来是移动到文件的第 68 个字节

  3. 在这个位置将that was modified字符串写入文件。字符串末尾的空格用于覆盖第二句原始内容。

  4. 现在,文件指针已设置到文件的开头,并读取其内容。

  5. 当执行前面的代码片段时,修改后的文件内容将打印到屏幕上,如下所示:

       I am excited to learn Python using Raspberry Pi Zero
 This is a line that was modified

还有另一个a+标志,它可以使数据追加到文件末尾并同时进行读取。我们将留给读者使用到目前为止讨论的示例来弄清楚这一点。

我们已经讨论了 Python 中读取和写入文件的不同示例。如果没有足够的编程经验,可能会感到不知所措。我们强烈建议通过本章提供的不同代码示例进行实际操作。

读者的挑战

使用a+标志打开write_file.txt文件(在不同的示例中讨论),并向文件追加一行。使用seek设置文件指针并打印其内容。您可以在程序中只打开文件一次。

使用with关键字

到目前为止,我们讨论了可以用于以不同模式打开文件的不同标志。我们讨论的示例遵循一个常见模式——打开文件,执行读/写操作,然后关闭文件。有一种优雅的方式可以使用with关键字与文件交互。

如果在与文件交互的代码块执行过程中出现任何错误,with关键字会确保在退出代码块时关闭文件并清理相关资源。让我们通过一个示例来回顾with关键字:

if __name__ == "__main__": 
   with open('write_file.txt', 'r+') as file: 
         # read the contents of the file and print to the screen 
         print(file.read()) 
         file.write("This is a line appended to the file") 

         #rewind the file and read its contents 
         file.seek(0) 
         print(file.read()) 
   # the file is automatically closed at this point 
   print("Exited the with keyword code block")

在前面的示例(with_keyword_example)中,我们跳过了关闭文件,因为with关键字在缩进的代码块执行完毕后会自动关闭文件。with关键字还会在由于错误离开代码块时关闭文件。这确保了资源在任何情况下都能得到适当的清理。接下来,我们将使用with关键字进行文件 I/O。

configparser

让我们讨论一些在使用树莓派开发应用程序时特别有用的 Python 编程方面。其中一个工具是 Python 中提供的configparserconfigparser模块(docs.python.org/3.4/library/configparser.html)用于读取/写入应用程序的配置文件。

在软件开发中,配置文件通常用于存储常量,如访问凭据、设备 ID 等。在树莓派的上下文中,configparser可以用于存储所有使用的 GPIO 引脚列表,通过 I²C 接口接口的传感器地址等。让我们讨论三个示例,学习如何使用configparser模块。在第一个示例中,我们将使用configparser创建一个config文件。

在第二个示例中,我们将使用configparser来读取配置值,在第三个示例中,我们将讨论修改配置文件的最终示例。

示例 1

在第一个示例中,让我们创建一个配置文件,其中包括设备 ID、使用的 GPIO 引脚、传感器接口地址、调试开关和访问凭据等信息:

import configparser 

if __name__ == "__main__": 
   # initialize ConfigParser 
   config_parser = configparser.ConfigParser() 

   # Let's create a config file 
   with open('raspi.cfg', 'w') as config_file: 
         #Let's add a section called ApplicationInfo 
         config_parser.add_section('AppInfo') 

         #let's add config information under this section 
         config_parser.set('AppInfo', 'id', '123') 
         config_parser.set('AppInfo', 'gpio', '2') 
         config_parser.set('AppInfo', 'debug_switch', 'True') 
         config_parser.set('AppInfo', 'sensor_address', '0x62') 

         #Let's add another section for credentials 
         config_parser.add_section('Credentials') 
         config_parser.set('Credentials', 'token', 'abcxyz123') 
         config_parser.write(config_file) 
   print("Config File Creation Complete")

让我们详细讨论前面的代码示例(可与本章一起下载作为config_parser_write.py):

  1. 第一步是导入configparser模块并创建ConfigParser类的实例。这个实例将被称为config_parser
       config_parser = configparser.ConfigParser()
  1. 现在,我们使用with关键字打开名为raspi.cfg的配置文件。由于文件不存在,将创建一个新的配置文件。

  2. 配置文件将包括两个部分,即AppInfoCredentials

  3. 可以使用add_section方法创建两个部分,如下所示:

       config_parser.add_section('AppInfo') 
       config_parser.add_section('Credentials')
  1. 每个部分将包含不同的常量集。可以使用set方法将每个常量添加到相关部分。set方法的必需参数包括参数/常量将位于的部分名称,参数/常量的名称及其对应的值。例如:id参数可以添加到AppInfo部分,并分配值123如下:
       config_parser.set('AppInfo', 'id', '123')
  1. 最后一步是将这些配置值保存到文件中。这是使用config_parser方法write完成的。一旦程序退出with关键字下的缩进块,文件就会关闭:
       config_parser.write(config_file)

我们强烈建议尝试自己尝试代码片段,并将这些片段用作参考。通过犯错误,您将学到很多,并可能得出比这里讨论的更好的解决方案。

执行上述代码片段时,将创建一个名为raspi.cfg的配置文件。配置文件的内容将包括以下内容所示的内容:

[AppInfo] 
id = 123 
gpio = 2 
debug_switch = True 
sensor_address = 0x62 

[Credentials] 
token = abcxyz123

示例 2

让我们讨论一个示例,我们从先前示例中创建的配置文件中读取配置参数:

import configparser 

if __name__ == "__main__": 
   # initialize ConfigParser 
   config_parser = configparser.ConfigParser() 

   # Let's read the config file 
   config_parser.read('raspi.cfg') 

   # Read config variables 
   device_id = config_parser.get('AppInfo', 'id') 
   debug_switch = config_parser.get('AppInfo', 'debug_switch') 
   sensor_address = config_parser.get('AppInfo', 'sensor_address') 

   # execute the code if the debug switch is true 
   if debug_switch == "True":
         print("The device id is " + device_id) 
         print("The sensor_address is " + sensor_address)

如果配置文件以所示格式创建,ConfigParser类应该能够解析它。实际上并不一定要使用 Python 程序创建配置文件。我们只是想展示以编程方式同时为多个设备创建配置文件更容易。

上述示例可与本章一起下载(config_parser_read.py)。让我们讨论一下这个代码示例是如何工作的:

  1. 第一步是初始化名为config_parserConfigParser类的实例。

  2. 第二步是使用实例方法read加载和读取配置文件。

  3. 由于我们知道配置文件的结构,让我们继续阅读位于AppInfo部分下可用的一些常量。可以使用get方法读取配置文件参数。必需的参数包括配置参数所在的部分以及参数的名称。例如:配置id参数位于AppInfo部分下。因此,该方法的必需参数包括AppInfoid

      device_id = config_parser.get('AppInfo', 'id')
  1. 现在配置参数已读入变量中,让我们在程序中使用它。例如:让我们测试debug_switch变量(用于确定程序是否处于调试模式)并打印从文件中检索到的其他配置参数:
       if debug_switch == "True":
           print("The device id is " + device_id) 
           print("The sensor_address is " + sensor_address)

示例 3

让我们讨论一个示例,我们想要修改现有的配置文件。这在需要在执行固件更新后更新配置文件中的固件版本号时特别有用。

以下代码片段可与本章一起下载,文件名为config_parser_modify.py

import configparser 

if __name__ == "__main__": 
   # initialize ConfigParser 
   config_parser = configparser.ConfigParser() 

   # Let's read the config file 
   config_parser.read('raspi.cfg') 

   # Set firmware version 
   config_parser.set('AppInfo', 'fw_version', 'A3') 

   # write the updated config to the config file 
   with open('raspi.cfg', 'w') as config_file: 
       config_parser.write(config_file)

让我们讨论一下这是如何工作的:

  1. 与往常一样,第一步是初始化ConfigParser类的实例。使用read方法加载配置文件:
       # initialize ConfigParser 
       config_parser = configparser.ConfigParser() 

       # Let's read the config file 
       config_parser.read('raspi.cfg')
  1. 使用set方法更新必需参数(在先前的示例中讨论):
       # Set firmware version 
       config_parser.set('AppInfo', 'fw_version', 'A3')
  1. 使用write方法将更新后的配置保存到配置文件中:
       with open('raspi.cfg', 'w') as config_file: 
          config_parser.write(config_file)

读者的挑战

使用示例 3 作为参考,将配置参数debug_switch更新为值False。重复示例 2,看看会发生什么。

读取/写入 CSV 文件

在本节中,我们将讨论读取/写入 CSV 文件。这个模块(docs.python.org/3.4/library/csv.html)在数据记录应用程序中非常有用。由于我们将在下一章讨论数据记录,让我们回顾一下读取/写入 CSV 文件。

写入 CSV 文件

让我们考虑一个场景,我们正在从不同的传感器读取数据。这些数据需要记录到一个 CSV 文件中,其中每一列对应于来自特定传感器的读数。我们将讨论一个例子,其中我们在 CSV 文件的第一行记录值123456789,第二行将包括值RedGreenBlue

  1. 写入 CSV 文件的第一步是使用with关键字打开 CSV 文件:
       with open("csv_example.csv", 'w') as csv_file:
  1. 下一步是初始化 CSV 模块的writer类的实例:
       csv_writer = csv.writer(csv_file)
  1. 现在,通过创建一个包含需要添加到行中的所有元素的列表,将每一行添加到文件中。例如:第一行可以按如下方式添加到列表中:
       csv_writer.writerow([123, 456, 789])
  1. 将所有内容放在一起,我们有:
       import csv 
       if __name__ == "__main__": 
          # initialize csv writer 
          with open("csv_example.csv", 'w') as csv_file: 
                csv_writer = csv.writer(csv_file) 
                csv_writer.writerow([123, 456, 789]) 
                csv_writer.writerow(["Red", "Green", "Blue"])
  1. 当执行上述代码片段(与本章一起提供的csv_write.py可下载)时,在本地目录中创建了一个 CSV 文件,其中包含以下内容:
 123,456,789
 Red,Green,Blue

从 CSV 文件中读取

让我们讨论一个例子,我们读取上一节中创建的 CSV 文件的内容:

  1. 读取 CSV 文件的第一步是以读模式打开它:
       with open("csv_example.csv", 'r') as csv_file:
  1. 接下来,我们初始化 CSV 模块的reader类的实例。CSV 文件的内容被加载到对象csv_reader中:
       csv_reader = csv.reader(csv_file)
  1. 现在 CSV 文件的内容已加载,可以按如下方式检索 CSV 文件的每一行:
       for row in csv_reader: 
           print(row)
  1. 将所有内容放在一起:
       import csv 

       if __name__ == "__main__": 
          # initialize csv writer 
          with open("csv_example.csv", 'r') as csv_file: 
                csv_reader = csv.reader(csv_file) 

                for row in csv_reader: 
                      print(row)
  1. 当执行上述代码片段(与本章一起提供的csv_read.py可下载)时,文件的内容将逐行打印,其中每一行都是一个包含逗号分隔值的列表:
       ['123', '456', '789']
 ['Red', 'Green', 'Blue']

Python 实用程序

Python 带有几个实用程序,可以与其他文件和操作系统本身进行交互。我们已经确定了我们在过去项目中使用过的所有这些 Python 实用程序。让我们讨论不同的模块及其用途,因为我们可能会在本书的最终项目中使用它们。

os 模块

正如其名称所示,这个模块(docs.python.org/3.1/library/os.html)可以与操作系统进行交互。让我们通过示例讨论一些应用。

检查文件是否存在

os模块可用于检查特定目录中是否存在文件。例如:我们广泛使用了write_file.txt文件。在打开此文件进行读取或写入之前,我们可以检查文件是否存在:

import os
if __name__ == "__main__":
    # Check if file exists
    if os.path.isfile('/home/pi/Desktop/code_samples/write_file.txt'):
        print('The file exists!')
    else:
        print('The file does not exist!')

在上述代码片段中,我们使用了os.path模块中提供的isfile()函数。当文件位置作为函数的参数传递时,如果文件存在于该位置,则返回True。在这个例子中,由于文件write_file.txt存在于代码示例目录中,该函数返回True。因此屏幕上打印出消息文件存在

if os.path.isfile('/home/pi/Desktop/code_samples/write_file.txt'): 
    print('The file exists!') 
else: 
    print('The file does not exist!')

检查文件夹是否存在

os.path.isfile()类似,还有另一个名为os.path.isdir()的函数。如果特定位置存在文件夹,则返回True。我们一直在查看位于树莓派桌面上的名为code_samples的文件夹中的所有代码示例。可以通过以下方式确认其存在:

# Confirm code_samples' existence 
if os.path.isdir('/home/pi/Desktop/code_samples'): 
    print('The directory exists!') 
else: 
    print('The directory does not exist!')

删除文件

os模块还可以使用remove()函数删除文件。将任何文件作为函数的参数传递即可删除该文件。在文件 I/O部分,我们讨论了使用文本文件read_file.txt从文件中读取。让我们通过将其作为remove()函数的参数来删除该文件:

os.remove('/home/pi/Desktop/code_samples/read_file.txt')

终止进程

可以通过将进程pid传递给kill()函数来终止在树莓派上运行的应用程序。在上一章中,我们讨论了在树莓派上作为后台进程运行的light_scheduler示例。为了演示终止进程,我们将尝试终止该进程。我们需要确定light_scheduler进程的进程pid(您可以选择由您作为用户启动的应用程序,不要触及根进程)。可以使用以下命令从命令行终端检索进程pid

 ps aux

它会显示当前在树莓派上运行的进程(如下图所示)。light_scheduler应用程序的进程pid为 1815:

light_scheduler 守护程序的 PID

假设我们知道需要终止的应用程序的进程pid,让我们回顾使用kill()函数终止该函数。终止函数所需的参数包括进程pid和需要发送到进程以终止应用程序的信号(signal.SIGKILL):

import os
import signal
if __name__ == "__main__":
    #kill the application
    try:
        os.kill(1815, signal.SIGKILL)
    except OSError as error:
        print("OS Error " + str(error))

signal模块(docs.python.org/3/library/signal.html))包含表示可用于停止应用程序的信号的常量。在此代码片段中,我们使用了SIGKILL信号。尝试运行ps命令(ps aux),您会注意到light_scheduler应用程序已被终止。

监控一个进程

在前面的示例中,我们讨论了使用kill()函数终止应用程序。您可能已经注意到,我们使用了称为try/except关键字来尝试终止应用程序。我们将在下一章详细讨论这些关键字。

还可以使用try/except关键字使用kill()函数来监视应用程序是否正在运行。在介绍使用try/except关键字捕获异常的概念后,我们将讨论使用kill()函数监视进程。

os模块中讨论的所有示例都可以与本章一起下载,文件名为os_utils.py

glob 模块

glob模块(docs.python.org/3/library/glob.html)使得能够识别具有特定扩展名或特定模式的文件。例如,可以列出文件夹中的所有 Python 文件如下:

# List all files
for file in glob.glob('*.py'):
    print(file)

glob()函数返回一个包含.py扩展名的文件列表。使用for循环来遍历列表并打印每个文件。当执行前面的代码片段时,输出包含属于本章的所有代码示例的列表(输出被截断以表示):

read_from_file.py
config_parser_read.py
append_to_file.py
read_line_from_file.py
config_parser_modify.py
python_utils.py
config_parser_write.py
csv_write.py

这个模块在列出具有特定模式的文件时特别有帮助。例如:让我们考虑这样一个场景,您想要上传来自实验不同试验的文件。您只对以下格式的文件感兴趣:file1xx.txt,其中x代表09之间的任意数字。这些文件可以按以下方式排序和列出:

# List all files of the format 1xx.txt
for file in glob.glob('txt_files/file1[0-9][0-9].txt'):
    print(file)

在前面的示例中,[0-9]表示文件名可以包含09之间的任意数字。由于我们正在寻找file1xx.txt格式的文件,因此作为参数传递给glob()函数的搜索模式是file1[0-9][0-9].txt

当执行前面的代码片段时,输出包含指定格式的所有文本文件:

txt_files/file126.txt
txt_files/file125.txt
txt_files/file124.txt
txt_files/file123.txt
txt_files/file127.txt

我们找到了一篇解释使用表达式对文件进行排序的文章:www.linuxjournal.com/content/bash-extended-globbing。相同的概念可以扩展到使用glob模块搜索文件。

读者的挑战

使用glob模块讨论的例子可以与本章一起下载,文件名为glob_example.py。在其中一个例子中,我们讨论了列出特定格式的文件。你将如何列出以下格式的文件:filexxxx.*?(这里的x代表09之间的任意数字。*代表任何文件扩展名。)

shutil 模块

shutil模块(docs.python.org/3/library/shutil.html)使得可以使用move()copy()方法在文件夹之间移动和复制文件。在上一节中,我们列出了文件夹txt_files中的所有文本文件。让我们使用move()将这些文件移动到当前目录(代码执行的位置),再次在txt_files中复制这些文件,最后从当前目录中删除这些文本文件:

import glob
import shutil
import os
if __name__ == "__main__":
    # move files to the current directory
    for file in glob.glob('txt_files/file1[0-9][0-9].txt'):
        shutil.move(file, '.')
    # make a copy of files in the folder 'txt_files' and delete them
    for file in glob.glob('file1[0-9][0-9].txt'):
        shutil.copy(file, 'txt_files')
        os.remove(file)

在前面的例子中(可以与本章一起下载,文件名为shutil_example.py),文件被移动和复制,源和目的地分别作为第一个和第二个参数指定。

使用glob模块识别要移动(或复制)的文件,然后使用它们对应的方法移动或复制每个文件。

subprocess 模块

我们在上一章简要讨论了这个模块。subprocess模块(docs.python.org/3.2/library/subprocess.html)使得可以在 Python 程序内部启动另一个程序。subprocess模块中常用的函数之一是Popen。需要在程序内部启动的任何进程都需要作为列表参数传递给Popen函数:

import subprocess
if __name__ == "__main__":
    subprocess.Popen(['aplay', 'tone.wav'])

在前面的例子中,tone.wav(需要播放的 WAVE 文件)和需要运行的命令作为列表参数传递给函数。subprocess模块中还有其他几个类似用途的命令。我们留给你去探索。

sys 模块

sys模块(docs.python.org/3/library/sys.html)允许与 Python 运行时解释器进行交互。sys模块的一个功能是解析作为程序输入提供的命令行参数。让我们编写一个程序,读取并打印作为程序参数传递的文件的内容:

import sys
if __name__ == "__main__":
    with open(sys.argv[1], 'r') as read_file:
        print(read_file.read())

尝试按以下方式运行前面的例子:

python3 sys_example.py read_lines.txt

前面的例子可以与本章一起下载,文件名为sys_example.py。在运行程序时传递的命令行参数列表可以在sys模块的argv列表中找到。argv[0]通常是 Python 程序的名称,argv[1]通常是传递给函数的第一个参数。

当以read_lines.txt作为参数执行sys_example.py时,程序应该打印文本文件的内容:

I am learning Python Programming using the Raspberry Pi Zero.
This is the second line.
Line 3.
Line 4.
Line 5.
Line 6.
Line 7.

总结

在本章中,我们讨论了文件 I/O - 读取和写入文件,以及用于读取、写入和追加文件的不同标志。我们谈到了将文件指针移动到文件的不同位置以检索特定内容或在特定位置覆盖文件内容。我们讨论了 Python 中的ConfigParser模块及其在存储/检索应用程序配置参数以及读写 CSV 文件中的应用。

最后,我们讨论了在我们的项目中潜在使用的不同 Python 工具。我们将广泛使用文件 I/O 和在本书中讨论的 Python 工具。我们强烈建议在进入本书中讨论的最终项目之前,熟悉本章讨论的概念。

在接下来的章节中,我们将讨论将存储在 CSV 文件中的传感器数据上传到云端,以及记录应用程序执行过程中遇到的错误。下一章见!

第十五章:请求和 Web 框架

本章的主要内容是 Python 中的请求和 Web 框架。我们将讨论使得从 Web 检索数据(例如,获取天气更新)、将数据上传到远程服务器(例如,记录传感器数据)或控制本地网络上的设备成为可能的库和框架。我们还将讨论一些有助于学习本章核心主题的话题。

try/except关键字

到目前为止,我们已经审查并测试了所有的例子,假设程序的执行不会遇到错误。相反,应用程序有时会由于外部因素(如无效的用户输入和糟糕的互联网连接)或程序员造成的程序逻辑错误而失败。在这种情况下,我们希望程序报告/记录错误的性质,并在退出程序之前继续执行或清理资源。try/except关键字提供了一种机制,可以捕获程序执行过程中发生的错误并采取补救措施。由于可能在代码的关键部分捕获和记录错误,try/except关键字在调试应用程序时特别有用。

通过比较两个例子来理解try/except关键字。让我们构建一个简单的猜数字游戏,用户被要求猜一个 0 到 9 之间的数字:

  1. 使用 Python 的random模块生成一个随机数(在 0 到 9 之间)。如果用户猜测的数字正确,Python 程序会宣布用户为赢家并退出游戏。

  2. 如果用户输入是字母x,程序会退出游戏。

  3. 用户输入使用int()函数转换为整数。进行了一个合理性检查,以确定用户输入是否是 0 到 9 之间的数字。

  4. 整数与随机数进行比较。如果它们相同,程序会宣布用户为赢家并退出游戏。

让我们观察当我们故意向这个程序提供错误的输入时会发生什么(这里显示的代码片段可以在本章的下载中找到,文件名为guessing_game.py):

import random

if __name__ == "__main__":
    while True:
        # generate a random number between 0 and 9
        rand_num = random.randrange(0,10)

        # prompt the user for a number
        value = input("Enter a number between 0 and 9: ")

        if value == 'x':
            print("Thanks for playing! Bye!")
            break

        input_value = int(value)

        if input_value < 0 or input_value > 9:
            print("Input invalid. Enter a number between 0 and 9.")

        if input_value == rand_num:
            print("Your guess is correct! You win!")
            break
        else:
            print("Nope! The random value was %s" % rand_num)

让我们执行前面的代码片段,并向程序提供输入hello

    Enter a number between 0 and 9: hello
 Traceback (most recent call last):
 File "guessing_game.py", line 12, in <module>
 input_value = int(value)
 ValueError: invalid literal for int() with base 10: 'hello'

在前面的例子中,当程序试图将用户输入hello转换为整数时失败。程序执行以异常结束。异常突出了发生错误的行。在这种情况下,它发生在第 10 行:

    File "guessing_game.py", line 12, in <module>
 input_value = int(value)

异常的性质也在异常中得到了突出。在这个例子中,最后一行表明抛出的异常是ValueError

    ValueError: invalid literal for int() with base 10: 'hello'

让我们讨论一个相同的例子(可以在本章的下载中找到,文件名为try_and_except.py),它使用了try/except关键字。在捕获异常并将其打印到屏幕后,可以继续玩游戏。我们有以下代码:

import random

if __name__ == "__main__":
    while True:
        # generate a random number between 0 and 9
        rand_num = random.randrange(0,10)

        # prompt the user for a number
        value = input("Enter a number between 0 and 9: ")

        if value == 'x':
            print("Thanks for playing! Bye!")

        try:
            input_value = int(value)
        except ValueError as error:
            print("The value is invalid %s" % error)
            continue

        if input_value < 0 or input_value > 9:
            print("Input invalid. Enter a number between 0 and 9.")
            continue

        if input_value == rand_num:
            print("Your guess is correct! You win!")
            break
        else:
            print("Nope! The random value was %s" % rand_num)

让我们讨论相同的例子如何使用try/except关键字:

  1. 从前面的例子中,我们知道当用户提供错误的输入时(例如,一个字母而不是 0 到 9 之间的数字),异常发生在第 10 行(用户输入转换为整数的地方),错误的性质被命名为ValueError

  2. 可以通过将其包装在try...except块中来避免程序执行的中断:

      try: 
         input_value = int(value) 
      except ValueError as error:
         print("The value is invalid %s" % error)
  1. 在接收到用户输入时,程序会在try块下尝试将用户输入转换为整数。

  2. 如果发生了ValueErrorexcept块会捕获error,并将以下消息与实际错误消息一起打印到屏幕上:

       except ValueError as error:
           print("The value is invalid %s" % error)
  1. 尝试执行代码示例并提供无效输入。您会注意到程序打印了错误消息(以及错误的性质),然后返回游戏循环的顶部并继续寻找有效的用户输入:
       Enter a number between 0 and 9: 3
 Nope! The random value was 5
 Enter a number between 0 and 9: hello
 The value is invalid invalid literal for int() with
       base 10: 'hello'
 Enter a number between 0 and 10: 4
 Nope! The random value was 6

try...except块带来了相当大的处理成本。因此,将try...except块保持尽可能短是很重要的。因为我们知道错误发生在尝试将用户输入转换为整数的行上,所以我们将其包装在try...except块中以捕获错误。

因此,try/except关键字用于防止程序执行中的任何异常行为,因为出现错误。它使得能够记录错误并采取补救措施。与try...except块类似,还有try...except...elsetry...except...else代码块。让我们通过几个例子快速回顾一下这些选项。

try...except...else

try...except...else块在我们希望只有在没有引发异常时才执行特定代码块时特别有用。为了演示这个概念,让我们使用这个块来重写猜数字游戏示例:

try:
    input_value = int(value)
except ValueError as error:
    print("The value is invalid %s" % error)
else:
    if input_value < 0 or input_value > 9:
        print("Input invalid. Enter a number between 0 and 9.")
    elif input_value == rand_num:
        print("Your guess is correct! You win!")
        break
    else:
        print("Nope! The random value was %s" % rand_num)

使用try...except...else块修改的猜数字游戏示例可与本章一起下载,文件名为try_except_else.py。在这个例子中,程序仅在接收到有效的用户输入时才将用户输入与随机数进行比较。否则,它会跳过else块并返回到循环顶部以接受下一个用户输入。因此,当try块中的代码没有引发异常时,try...except...else被用来执行特定的代码块。

try...except...else...finally

正如其名称所示,finally块用于在离开try块时执行一块代码。即使在引发异常后,这段代码也会被执行。这在我们需要在进入下一个阶段之前清理资源和释放内存时非常有用。

让我们使用我们的猜数字游戏来演示finally块的功能。为了理解finally关键字的工作原理,让我们使用一个名为count的计数器变量,在finally块中递增,以及另一个名为valid_count的计数器变量,在else块中递增。我们有以下代码:

count = 0
valid_count = 0
while True:
  # generate a random number between 0 and 9
  rand_num = random.randrange(0,10)

  # prompt the user for a number
  value = input("Enter a number between 0 and 9: ")

  if value == 'x':
      print("Thanks for playing! Bye!")

  try:
      input_value = int(value)
  except ValueError as error:
      print("The value is invalid %s" % error)
  else:
      if input_value < 0 or input_value > 9:
          print("Input invalid. Enter a number between 0 and 9.")
          continue

      valid_count += 1
      if input_value == rand_num:
          print("Your guess is correct! You win!")
          break
      else:
          print("Nope! The random value was %s" % rand_num)
  finally:
      count += 1

print("You won the game in %d attempts "\
      "and %d inputs were valid" % (count, valid_count))

上述代码片段来自try_except_else_finally.py代码示例(可与本章一起下载)。尝试执行代码示例并玩游戏。您将注意到赢得游戏所需的总尝试次数以及有效输入的数量:

    Enter a number between 0 and 9: g
 The value is invalid invalid literal for int() with
    base 10: 'g'
 Enter a number between 0 and 9: 3
 Your guess is correct! You win!
 You won the game in 9 attempts and 8 inputs were valid

这演示了try-except-else-finally块的工作原理。当关键代码块(在try关键字下)成功执行时,else关键字下的任何代码都会被执行,而在退出try...except块时(在退出代码块时清理资源时)finally关键字下的代码块会被执行。

使用先前的代码示例玩游戏时提供无效的输入,以了解代码块流程。

连接到互联网 - 网络请求

现在我们已经讨论了try/except关键字,让我们利用它来构建一个连接到互联网的简单应用程序。我们将编写一个简单的应用程序,从互联网上获取当前时间。我们将使用 Python 的requests库(requests.readthedocs.io/en/master/#)。

requests模块使得连接到网络和检索信息成为可能。为了做到这一点,我们需要使用requests模块中的get()方法来发出请求:

import requests
response = requests.get('http://nist.time.gov/actualtime.cgi')

在上述代码片段中,我们将一个 URL 作为参数传递给get()方法。在这种情况下,它是返回当前时间的 Unix 格式的 URL(en.wikipedia.org/wiki/Unix_time)。

让我们利用try/except关键字来请求获取当前时间:

#!/usr/bin/python3

import requests

if __name__ == "__main__":
  # Source for link: http://stackoverflow.com/a/30635751/822170
  try:
    response = requests.get('http://nist.time.gov/actualtime.cgi')
    print(response.text)
  except requests.exceptions.ConnectionError as error:
    print("Something went wrong. Try again")

在前面的例子中(可以与本章一起下载,命名为internet_access.py),请求是在try块下进行的,响应(由response.text返回)被打印到屏幕上。

如果在执行请求以检索当前时间时出现错误,将引发ConnectionErrorrequests.readthedocs.io/en/master/user/quickstart/#errors-and-exceptions)。这个错误可能是由于缺乏互联网连接或不正确的 URL 引起的。这个错误被except块捕获。尝试运行这个例子,它应该返回time.gov的当前时间:

    <timestamp time="1474421525322329" delay="0"/>

requests 的应用-检索天气信息

让我们使用requests模块来检索旧金山市的天气信息。我们将使用OpenWeatherMap API (openweathermap.org)来检索天气信息:

  1. 为了使用 API,注册一个 API 账户并获取一个 API 密钥(免费):

来自 openweathermap.org 的 API 密钥

  1. 根据 API 文档(openweathermap.org/current),可以使用http://api.openweathermap.org/data/2.5/weather?zip=SanFrancisco&appid=API_KEY&units=imperial作为 URL 来检索一个城市的天气信息。

  2. 用你的账户的密钥替换API_KEY,并在浏览器中使用它来检索当前的天气信息。你应该能够以以下格式检索到天气信息:

 {"coord":{"lon":-122.42,"lat":37.77},"weather":[{"id":800, 
       "main":"Clear","description":"clear sky","icon":"01n"}],"base": 
       "stations","main":{"temp":71.82,"pressure":1011,"humidity":50, 
       "temp_min":68,"temp_max":75.99},"wind":
       {"speed":13.04,"deg":291},
       "clouds":{"all":0},"dt":1474505391,"sys":{"type":3,"id":9966, 
       "message":0.0143,"country":"US","sunrise":1474552682, 
       "sunset":1474596336},"id":5391959,"name":"San 
       Francisco","cod":200}

天气信息(如前所示)以 JSON 格式返回。JavaScript 对象表示法JSON)是一种广泛用于在网络上传递数据的数据格式。JSON 格式的主要优点是它是一种可读的格式,许多流行的编程语言支持将数据封装在 JSON 格式中。如前面的片段所示,JSON 格式使得以可读的名称/值对交换信息成为可能。

让我们回顾一下使用requests模块检索天气并解析 JSON 数据:

  1. 用前面例子中的 URL(internet_access.py)替换为本例中讨论的 URL。这应该以 JSON 格式返回天气信息。

  2. requests 模块提供了一个解析 JSON 数据的方法。响应可以按以下方式解析:

       response = requests.get(URL) 
       json_data = response.json()
  1. json()函数解析来自 OpenWeatherMap API 的响应,并返回不同天气参数(json_data)及其值的字典。

  2. 由于我们知道 API 文档中的响应格式,可以从解析后的响应中检索当前温度:

       print(json_data['main']['temp'])
  1. 把所有这些放在一起,我们有这个:
       #!/usr/bin/python3

       import requests

       # generate your own API key
       APP_ID = '5d6f02fd4472611a20f4ce602010ee0c'
       ZIP = 94103
       URL = """http://api.openweathermap.org/data/2.5/weather?zip={}
       &appid={}&units=imperial""".format(ZIP, APP_ID)

       if __name__ == "__main__":
         # API Documentation: http://openweathermap.org/
         current#current_JSON
         try:
           # encode data payload and post it
           response = requests.get(URL)
           json_data = response.json()
           print("Temperature is %s degrees Fahrenheit" %
           json_data['main']['temp'])
         except requests.exceptions.ConnectionError as error:
           print("The error is %s" % error)

前面的例子可以与本章一起下载,命名为weather_example.py。该例子应该显示当前的温度如下:

    Temperature is 68.79 degrees Fahrenheit

requests 的应用-将事件发布到互联网

在上一个例子中,我们从互联网上检索了信息。让我们考虑一个例子,在这个例子中,我们需要在互联网上发布传感器事件。这可能是你不在家时猫门打开,或者有人踩在你家门口的地垫上。因为我们在上一章中讨论了如何将传感器与树莓派 Zero 连接,所以让我们讨论一个场景,我们可以将这些事件发布到Slack——一个工作场所通讯工具,Twitter,或者云服务,比如Phant (data.sparkfun.com/)。

在这个例子中,我们将使用requests将这些事件发布到 Slack。每当发生传感器事件,比如猫门打开时,让我们在 Slack 上给自己发送直接消息。我们需要一个 URL 来将这些传感器事件发布到 Slack。让我们回顾一下生成 URL 以将传感器事件发布到 Slack:

  1. 生成 URL 的第一步是创建一个incoming webhook。Webhook 是一种可以将消息作为有效负载发布到应用程序(如 Slack)的请求类型。

  2. 如果您是名为TeamX的 Slack 团队成员,请在浏览器中启动您团队的应用程序目录,即teamx.slack.com/apps

启动您团队的应用程序目录

  1. 在应用程序目录中搜索incoming webhooks,并选择第一个选项,Incoming WebHooks(如下截图所示):

选择 incoming webhooks

  1. 点击添加配置:

添加配置

  1. 当事件发生时,让我们向自己发送私人消息。选择 Privately to (you)作为选项,并通过单击添加 Incoming WebHooks 集成来创建一个 webhook:

选择 Privately to you

  1. 我们已经生成了一个 URL,用于发送有关传感器事件的直接消息(URL 部分隐藏):

生成的 URL

  1. 现在,我们可以使用先前提到的 URL 在 Slack 上向自己发送直接消息。传感器事件可以作为 JSON 有效负载发布到 Slack。让我们回顾一下如何将传感器事件发布到 Slack。

  2. 例如,让我们考虑在猫门打开时发布消息。第一步是为消息准备 JSON 有效负载。根据 Slack API 文档(api.slack.com/custom-integrations)),消息有效负载需要采用以下格式:

       payload = {"text": "The cat door was just opened!"}
  1. 为了发布此事件,我们将使用requests模块中的post()方法。在发布时,数据有效负载需要以 JSON 格式进行编码:
       response = requests.post(URL, json.dumps(payload))
  1. 将所有内容放在一起,我们有:
       #!/usr/bin/python3

       import requests
       import json

       # generate your own URL
       URL = 'https://hooks.slack.com/services/'

       if __name__ == "__main__":
         payload = {"text": "The cat door was just opened!"}
         try:
           # encode data payload and post it
           response = requests.post(URL, json.dumps(payload))
           print(response.text)
         except requests.exceptions.ConnectionError as error:
           print("The error is %s" % error)
  1. 在发布消息后,请求返回ok作为响应。这表明发布成功了。

  2. 生成您自己的 URL 并执行上述示例(与本章一起提供的slack_post.py一起下载)。您将在 Slack 上收到直接消息:

在 Slack 上直接发送消息

现在,尝试将传感器接口到 Raspberry Pi Zero(在前几章中讨论),并将传感器事件发布到 Slack。

还可以将传感器事件发布到 Twitter,并让您的 Raspberry Pi Zero 检查新邮件等。查看本书的网站以获取更多示例。

Flask web 框架

在我们的最后一节中,我们将讨论 Python 中的 Web 框架。我们将讨论 Flask 框架(flask.pocoo.org/)。基于 Python 的框架使得可以使用 Raspberry Pi Zero 将传感器接口到网络。这使得可以在网络中的任何位置控制设备并从传感器中读取数据。让我们开始吧!

安装 Flask

第一步是安装 Flask 框架。可以按以下方式完成:

    sudo pip3 install flask

构建我们的第一个示例

Flask 框架文档解释了构建第一个示例。根据文档修改示例如下:

#!/usr/bin/python3

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World!"

if __name__ == "__main__":
    app.run('0.0.0.0')

启动此示例(与本章一起提供的flask_example.py一起下载),它应该在 Raspberry Pi Zero 上启动一个对网络可见的服务器。在另一台计算机上,启动浏览器,并输入 Raspberry Pi Zero 的 IP 地址以及端口号5000作为后缀(如下快照所示)。它应该将您带到服务器的索引页面,显示消息 Hello World!:

基于 Flask 框架的 Raspberry Pi Zero 上的 Web 服务器

您可以使用命令行终端上的ifconfig命令找到 Raspberry Pi Zero 的 IP 地址。

使用 Flask 框架控制设备

让我们尝试使用 Flask 框架在家中打开/关闭电器。在之前的章节中,我们使用PowerSwitch Tail II来控制树莓派 Zero 上的台灯。让我们尝试使用 Flask 框架来控制相同的东西。按照以下图示连接 PowerSwitch Tail:

使用 Flask 框架控制台灯

根据 Flask 框架文档,可以将 URL 路由到特定函数。例如,可以使用route()/lamp/<control>绑定到control()函数:

@app.route("/lamp/<control>") 
def control(control): 
  if control == "on": 
    lights.on() 
  elif control == "off": 
    lights.off() 
  return "Table lamp is now %s" % control

在前面的代码片段中,<control>是一个可以作为参数传递给绑定函数的变量。这使我们能够打开/关闭灯。例如,<IP 地址>:5000/lamp/on打开灯,反之亦然。把它们放在一起,我们有这样:

#!/usr/bin/python3 

from flask import Flask 
from gpiozero import OutputDevice 

app = Flask(__name__) 
lights = OutputDevice(2) 

@app.route("/lamp/<control>") 
def control(control): 
  if control == "on": 
    lights.on() 
  elif control == "off": 
    lights.off() 
  return "Table lamp is now %s" % control 

if __name__ == "__main__": 
    app.run('0.0.0.0')

上述示例可与本章一起下载,文件名为appliance_control.py。启动基于 Flask 的 Web 服务器,并在另一台计算机上打开 Web 服务器。为了打开灯,输入<树莓派 Zero 的 IP 地址>:5000/lamp/on作为 URL:

这应该打开灯:

因此,我们建立了一个简单的框架,可以控制网络中的电器。可以在 HTML 页面中包含按钮,并将它们路由到特定的 URL 以执行特定的功能。Python 中还有几个其他框架可以开发 Web 应用程序。我们只是向您介绍了 Python 可能的不同应用程序。我们建议您查看本书的网站,了解更多示例,例如使用 Flask 框架控制万圣节装饰和其他节日装饰。

摘要

在本章中,我们讨论了 Python 中的try/except关键字。我们还讨论了从互联网检索信息的应用程序,以及将传感器事件发布到互联网。我们还讨论了 Python 的 Flask Web 框架,并演示了在网络中控制电器。在下一章中,我们将讨论 Python 中的一些高级主题。

第十六章:使用 Python 可以开发的一些很棒的东西

在本章中,我们将讨论 Python 中的一些高级主题。我们还将讨论一些独特的主题(如图像处理),让您开始使用 Python 进行应用程序开发。

使用 Raspberry Pi Zero 进行图像处理

Raspberry Pi Zero 是一款价格便宜的硬件,配备了 1 GHz 处理器。虽然它不足以运行某些高级图像处理操作,但可以帮助您在 25 美元的预算内学习基础知识(Raspberry Pi Zero 和摄像头的成本)。

我们建议您在 Raspberry Pi Zero 上使用 16 GB(或更高)的卡来安装本节讨论的图像处理工具集。

例如,您可以使用 Raspberry Pi Zero 来跟踪后院的鸟。在本章中,我们将讨论在 Raspberry Pi Zero 上开始图像处理的不同方法。

为了在本节中使用摄像头测试一些示例,需要 Raspberry Pi Zero v1.3 或更高版本。检查您的 Raspberry Pi Zero 的背面以验证板的版本:

识别您的 Raspberry Pi Zero 的版本

OpenCV

OpenCV是一个开源工具箱,包括为图像处理开发的不同软件工具。OpenCV 是一个跨平台的工具箱,支持不同的操作系统。由于 OpenCV 在开源许可下可用,全世界的研究人员通过开发工具和技术为其增长做出了贡献。这使得开发应用程序相对容易。OpenCV 的一些应用包括人脸识别和车牌识别。

由于其有限的处理能力,安装框架可能需要几个小时。在我们这里大约花了 10 个小时。

我们按照www.pyimagesearch.com/2015/10/26/how-to-install-opencv-3-on-raspbian-jessie/上的指示在 Raspberry Pi Zero 上安装 OpenCV。我们特别按照了使用 Python 3.x 绑定安装 OpenCV 的指示,并验证了安装过程。我们大约花了 10 个小时来完成在 Raspberry Pi Zero 上安装 OpenCV。出于不重复造轮子的考虑,我们不会重复这些指示。

安装的验证

让我们确保 OpenCV 安装及其 Python 绑定工作正常。启动命令行终端,并确保您已经通过执行workon cv命令启动了cv虚拟环境(您可以验证您是否在cv虚拟环境中):

验证您是否在 cv 虚拟环境中

现在,让我们确保我们的安装工作正常。从命令行启动 Python 解释器,并尝试导入cv2模块:

    >>> import cv2
 >>> cv2.__version__
 '3.0.0'

这证明了 OpenCV 已经安装在 Raspberry Pi Zero 上。让我们编写一个涉及 OpenCV 的hello world示例。在这个示例中,我们将打开一张图像(这可以是您的 Raspberry Pi Zero 桌面上的任何彩色图像),并在将其转换为灰度后显示它。我们将使用以下文档来编写我们的第一个示例:docs.opencv.org/3.0-beta/doc/py_tutorials/py_gui/py_image_display/py_image_display.html

根据文档,我们需要使用imread()函数来读取图像文件的内容。我们还需要指定要读取图像的格式。在这种情况下,我们将以灰度格式读取图像。这由作为函数的第二个参数传递的cv2.IMREAD_GRAYSCALE来指定:

import cv2 

img = cv2.imread('/home/pi/screenshot.jpg',cv2.IMREAD_GRAYSCALE)

现在图像以灰度格式加载并保存到img变量中,我们需要在新窗口中显示它。这是通过imshow()函数实现的。根据文档,我们可以通过将窗口名称指定为第一个参数,将图像指定为第二个参数来显示图像:

cv2.imshow('image',img)

在这种情况下,我们将打开一个名为image的窗口,并显示我们在上一步加载的img的内容。我们将显示图像,直到收到按键。这是通过使用cv2.waitKey()函数实现的。根据文档,waitkey()函数监听键盘事件:

cv2.waitKey(0)

0参数表示我们将无限期等待按键。根据文档,当以毫秒为单位的持续时间作为参数传递时,waitkey()函数会监听指定持续时间的按键。当按下任何键时,窗口会被destroyAllWindows()函数关闭:

cv2.destroyAllWindows()

将所有部件组装在一起,我们有:

import cv2

img = cv2.imread('/home/pi/screenshot.jpg',cv2.IMREAD_GRAYSCALE)
cv2.imshow('image',img)
cv2.waitKey(0)
cv2.destroyAllWindows()

上述代码示例可在本章的opencv_test.py中下载。安装 OpenCV 库后,尝试加载图像,如本示例所示。它应该以灰度加载图像,如下图所示:

树莓派桌面以灰度加载

这个窗口会在按下任意键时关闭。

向读者提出挑战

在上面的示例中,窗口在按下任意键时关闭。查看文档,确定是否可能在按下鼠标按钮时关闭所有窗口。

将相机安装到树莓派 Zero

测试我们下一个示例需要相机连接器和相机。购买相机和适配器的一个来源如下:

名称 来源
树莓派 Zero 相机适配器 thepihut.com/products/raspberry-pi-zero-camera-adapter
树莓派相机 thepihut.com/products/raspberry-pi-camera-module

执行以下步骤将相机安装到树莓派 Zero 上:

  1. 第一步是将相机连接到树莓派 Zero。相机适配器可以安装如下图所示。抬起连接器标签,滑动相机适配器并轻轻按下连接器:

  1. 我们需要在树莓派 Zero 上启用相机接口。在桌面上,转到首选项并启动树莓派配置。在树莓派配置的接口选项卡下,启用相机,并保存配置:

启用相机接口

  1. 通过从命令行终端运行以下命令来拍照测试相机:
       raspistill -o /home/pi/Desktop/test.jpg
  1. 它应该拍照并保存到树莓派桌面上。验证相机是否正常工作。如果无法使相机工作,我们建议查看树莓派基金会发布的故障排除指南:www.raspberrypi.org/documentation/raspbian/applications/camera.md

相机电缆有点笨重,拍照时可能会有些困难。我们建议使用相机支架。我们发现这个很有用(如下图所示)a.co/hQolR7O

使用树莓派相机的支架

让我们试试相机,并与 OpenCV 库一起使用:

  1. 我们将使用相机拍照,并使用 OpenCV 框架显示它。为了在 Python 中访问相机,我们需要picamera包。可以按照以下方式安装:
       pip3 install picamera
  1. 让我们确保包能够按预期使用一个简单的程序。picamera包的文档可在picamera.readthedocs.io/en/release-1.12/api_camera.html找到。

  2. 第一步是初始化PiCamera类。接下来是翻转图像,使其在垂直轴上翻转。这仅在相机倒置安装时才需要。在其他安装中可能不需要:

       with PiCamera() as camera: 
       camera.vflip = True
  1. 在拍照之前,我们可以使用start_preview()方法预览即将捕获的图片:
       camera.start_preview()
  1. 在我们拍照之前,让我们预览10秒钟。我们可以使用capture()方法拍照:
       sleep(10) 
       camera.capture("/home/pi/Desktop/desktop_shot.jpg") 
       camera.stop_preview()
  1. capture()方法需要文件位置作为参数(如前面的代码片段所示)。完成后,我们可以使用stop_preview()关闭相机预览。

  2. 总结一下,我们有:

       from picamera import PiCamera 
       from time import sleep

       if __name__ == "__main__": 
         with PiCamera() as camera: 
           camera.vflip = True 
           camera.start_preview() 
           sleep(10) 
           camera.capture("/home/pi/Desktop/desktop_shot.jpg") 
           camera.stop_preview()

上述代码示例可与本章一起下载,文件名为picamera_test.py。使用相机拍摄的快照如下图所示:

使用树莓派摄像头模块捕获的图像

  1. 让我们将此示例与上一个示例结合起来——将此图像转换为灰度并显示,直到按下键。确保您仍然在cv虚拟环境工作空间中。

  2. 让我们将捕获的图像转换为灰度,如下所示:

       img = cv2.imread("/home/pi/Desktop/desktop_shot.jpg",
       cv2.IMREAD_GRAYSCALE)

以下是捕获后转换的图像:

图像在捕获时转换为灰度

  1. 现在我们可以按如下方式显示灰度图像:
       cv2.imshow("image", img) 
       cv2.waitKey(0) 
       cv2.destroyAllWindows()

修改后的示例可作为picamera_opencvtest.py进行下载。

到目前为止,我们已经展示了在 Python 中开发图像处理应用程序。我们还建议查看 OpenCV Python 绑定文档中提供的示例(在本节介绍部分提供了链接)。

语音识别

在本节中,我们将讨论在 Python 中开发语音识别示例涉及语音识别。我们将利用requests模块(在上一章中讨论)来使用wit.aiwit.ai/)转录音频。

有几种语音识别工具,包括 Google 的语音 API、IBM Watson、Microsoft Bing 的语音识别 API。我们以wit.ai为例进行演示。

语音识别在我们希望使树莓派零对语音命令做出响应的应用中非常有用。

让我们回顾使用wit.ai在 Python 中构建语音识别应用程序(其文档可在github.com/wit-ai/pywit找到)。为了进行语音识别和识别语音命令,我们需要一个麦克风。但是,我们将演示使用一个现成的音频样本。我们将使用一篇研究出版物提供的音频样本(可在ecs.utdallas.edu/loizou/speech/noizeus/clean.zip找到)。

wit.ai API 许可证规定,该工具可免费使用,但上传到其服务器的音频用于调整其语音转录工具。

我们现在将尝试转录sp02.wav音频样本,执行以下步骤:

  1. 第一步是注册wit.ai帐户。请注意以下截图中显示的 API:

  1. 第一步是安装 requests 库。可以按以下方式安装:
       pip3 install requests 
  1. 根据wit.ai的文档,我们需要向我们的请求添加自定义标头,其中包括 API 密钥(用您的帐户中的令牌替换$TOKEN)。我们还需要在标头中指定文件格式。在这种情况下,它是一个.wav文件,采样频率为 8000 Hz:
       import requests 

       if __name__ == "__main__": 
         url = 'https://api.wit.ai/speech?v=20161002' 
         headers = {"Authorization": "Bearer $TOKEN", 
                    "Content-Type": "audio/wav"}
  1. 为了转录音频样本,我们需要将音频样本附加到请求体中:
       files = open('sp02.wav', 'rb') 
       response = requests.post(url, headers=headers, data=files) 
       print(response.status_code) 
       print(response.text)
  1. 将所有这些放在一起,我们得到了这个:
       #!/usr/bin/python3 

       import requests 

       if __name__ == "__main__": 
         url = 'https://api.wit.ai/speech?v=20161002' 
         headers = {"Authorization": "Bearer $TOKEN", 
                    "Content-Type": "audio/wav"} 
         files = open('sp02.wav', 'rb') 
         response = requests.post(url, headers=headers, data=files) 
         print(response.status_code) 
         print(response.text)

前面的代码示例可与本章一起下载,文件名为wit_ai.py。尝试执行前面的代码示例,它应该会转录音频样本:sp02.wav。我们有以下代码:

200
{
  "msg_id" : "fae9cc3a-f7ed-4831-87ba-6a08e95f515b",
  "_text" : "he knew the the great young actress",
  "outcomes" : [ {
    "_text" : "he knew the the great young actress",
    "confidence" : 0.678,
    "intent" : "DataQuery",
    "entities" : {
      "value" : [ {
        "confidence" : 0.7145905790744499,
        "type" : "value",
        "value" : "he",
        "suggested" : true
      }, {
        "confidence" : 0.5699616515542044,
        "type" : "value",
        "value" : "the",
        "suggested" : true
      }, {
        "confidence" : 0.5981701138805214,
        "type" : "value",
        "value" : "great",
        "suggested" : true
      }, {
        "confidence" : 0.8999612482250062,
        "type" : "value",
        "value" : "actress",
        "suggested" : true
      } ]
    }
  } ],
  "WARNING" : "DEPRECATED"
}

音频样本包含以下录音:他知道那位年轻女演员的技巧。根据wit.ai API,转录为他知道了那位年轻女演员。词错误率为 22%(en.wikipedia.org/wiki/Word_error_rate)。

自动化路由任务

在这一部分,我们将讨论如何在 Python 中自动化路由任务。我们举了两个例子,它们展示了树莓派 Zero 作为个人助手的能力。第一个例子涉及改善通勤,而第二个例子则是帮助提高词汇量。让我们开始吧。

改善日常通勤

许多城市和公共交通系统已经开始向公众分享数据,以增加透明度并提高运营效率。交通系统已经开始通过 API 向公众分享公告和交通信息。这使任何人都能开发提供给通勤者信息的移动应用。有时,这有助于缓解公共交通系统内的拥堵。

这个例子是受到一位朋友的启发,他追踪旧金山共享单车站点的自行车可用性。在旧金山湾区,有一个自行车共享计划,让通勤者可以从交通中心租一辆自行车到他们的工作地点。在像旧金山这样拥挤的城市,特定站点的自行车可用性会根据一天的时间而波动。

这位朋友想要根据最近的共享单车站点的自行车可用性来安排他的一天。如果站点上的自行车非常少,这位朋友更喜欢早点出发租一辆自行车。他正在寻找一个简单的技巧,可以在自行车数量低于某个阈值时向他的手机推送通知。旧金山的共享单车计划在feeds.bayareabikeshare.com/stations/stations.json上提供了这些数据。

让我们回顾一下构建一个简单的例子,可以使其向移动设备发送推送通知。为了发送移动推送通知,我们将使用If This Then ThatIFTTT)——这是一个使您的项目连接到第三方服务的服务。

在这个例子中,我们将解析以 JSON 格式可用的数据,检查特定站点的可用自行车数量,如果低于指定的阈值,就会触发手机设备上的通知。

让我们开始吧:

  1. 第一步是从共享单车服务中检索自行车的可用性。这些数据以 JSON 格式在feeds.bayareabikeshare.com/stations/stations.json上提供。数据包括整个网络的自行车可用性。

  2. 每个站点的自行车可用性都有一些参数,比如站点 ID、站点名称、地址、可用自行车数量等。

  3. 在这个例子中,我们将检索旧金山Townsend at 7th站点的自行车可用性。站点 ID 是65(在浏览器中打开前面提到的链接以找到id)。让我们编写一些 Python 代码来检索自行车可用性数据并解析这些信息:

       import requests 

       BIKE_URL = http://feeds.bayareabikeshare.com/stations 
       /stations.json 

       # fetch the bike share information 
       response = requests.get(BIKE_URL) 
       parsed_data = response.json()

第一步是使用GET请求(通过requests模块)获取数据。requests模块提供了内置的 JSON 解码器。可以通过调用json()函数来解析 JSON 数据。

  1. 现在,我们可以遍历站点的字典,并通过以下步骤找到Townsend at 7th站点的自行车可用性:

  2. 在检索到的数据中,每个站点的数据都附带一个 ID。问题站点的 ID 是65(在浏览器中打开之前提供的数据源 URL 以了解数据格式;数据的片段如下截图所示):

使用浏览器获取的自行车共享数据源的片段

  1. 我们需要遍历数值并确定站点id是否与Townsend at 7th的匹配:
              station_list = parsed_data['stationBeanList'] 
              for station in station_list: 
                if station['id'] == 65 and 
                   station['availableBikes'] < 2: 
                  print("The available bikes is %d" % station
                  ['availableBikes'])

如果站点上的自行车少于2辆,我们会向我们的移动设备推送移动通知。

  1. 为了接收移动通知,您需要安装IF by IFTTT应用程序(适用于苹果和安卓设备)。

  2. 我们还需要在 IFTTT 上设置一个配方来触发移动通知。在ifttt.com/注册一个账户。

IFTTT 是一个服务,可以创建连接设备到不同应用程序并自动化任务的配方。例如,可以将树莓派 Zero 跟踪的事件记录到您的 Google Drive 上的电子表格中。

IFTTT 上的所有配方都遵循一个通用模板——如果这样,那么那样,也就是说,如果发生了特定事件,那么就会触发特定的动作。例如,我们需要创建一个 applet,以便在收到 web 请求时触发移动通知。

  1. 您可以使用您的帐户下拉菜单开始创建一个 applet,如下截图所示:

开始在 IFTTT 上创建一个配方

  1. 它应该带您到一个配方设置页面(如下所示)。点击这个并设置一个传入的 web 请求:

点击这个

  1. 选择 Maker Webhooks 频道作为传入触发器:

选择 Maker Webhooks 频道

  1. 选择接收 web 请求。来自树莓派的 web 请求将作为触发器发送移动通知:

选择接收 web 请求

  1. 创建一个名为mobile_notify的触发器:

创建一个名为 mobile_notify 的新触发器

  1. 现在是时候为传入触发器创建一个动作了。点击那个。

点击这个

  1. 选择通知:

选择通知

  1. 现在,让我们格式化我们想要在设备上收到的通知:

为您的设备设置通知

  1. 在移动通知中,我们需要接收自行车共享站点上可用自行车的数量。点击+ Ingredient 按钮,选择Value1

格式化消息以满足您的需求。例如,当树莓派触发通知时,希望以以下格式收到消息:该回家了!Townsend & 7th 只有 2 辆自行车可用!

  1. 一旦您对消息格式满意,选择创建动作,您的配方就应该准备好了!

创建一个配方

  1. 为了在我们的移动设备上触发通知,我们需要一个 URL 来进行POST请求和一个触发键。这在您的 IFTTT 帐户的 Services | Maker Webhooks | Settings 下可用。

触发器可以在这里找到:

在新的浏览器窗口中打开前面截图中列出的 URL。它提供了POST请求的 URL 以及如何进行 web 请求的解释(如下截图所示):

使用之前提到的 URL 进行 POST 请求(为了隐私而隐藏密钥)

  1. 在发出请求时(如 IFTTT 文档中所述),如果我们在请求的 JSON 主体中包括自行车的数量(使用Value1),它可以显示在移动通知上。

  2. 让我们重新查看 Python 示例,当自行车数量低于一定阈值时进行网络请求。将IFTTT URL 和您的 IFTTT 访问密钥(从您的 IFTTT 帐户中检索)保存到您的代码中,如下所示:

       IFTTT_URL = "https://maker.ifttt.com/trigger/mobile_notify/ 
       with/key/$KEY"
  1. 当自行车数量低于一定阈值时,我们需要使用 JSON 主体中编码的自行车信息进行POST请求:
       for station in station_list: 
         if station['id'] == 65 and 
            station['availableBikes'] < 3: 
           print("The available bikes is %d" % 
           station['availableBikes']) 
           payload = {"value1": station['availableBikes']} 
           response = requests.post(IFTTT_URL, json=payload) 
           if response.status_code == 200: 
             print("Notification successfully triggered")
  1. 在上述代码片段中,如果自行车少于三辆,将使用requests模块进行POST请求。可用自行车的数量使用键value1进行编码:
       payload = {"value1": station['availableBikes']}
  1. 将所有这些放在一起,我们有这个:
       #!/usr/bin/python3 

       import requests 
       import datetime 

       BIKE_URL = "http://feeds.bayareabikeshare.com/stations/
       stations.json" 
       # find your key from ifttt 
       IFTTT_URL = "https://maker.ifttt.com/trigger/mobile_notify/
       with/key/$KEY" 

       if __name__ == "__main__": 
         # fetch the bike share information 
         response = requests.get(BIKE_URL) 
         parsed_data = response.json() 
         station_list = parsed_data['stationBeanList'] 
         for station in station_list: 
           if station['id'] == 65 and 
              station['availableBikes'] < 10: 
             print("The available bikes is %d" % station
             ['availableBikes']) 
  payload = {"value1": station['availableBikes']} 
             response = requests.post(IFTTT_URL, json=payload) 
             if response.status_code == 200: 
               print("Notification successfully triggered")

上述代码示例可与本章一起下载,名称为bike_share.py。在设置 IFTTT 上的配方后尝试执行它。如果需要,调整可用自行车数量的阈值。您应该会收到移动设备上的通知:

在您的移动设备上通知

读者的挑战

在此示例中,自行车信息被获取和解析,如果必要,将触发通知。您将如何修改此代码示例以确保它在一天中的特定时间执行?(提示:使用datetime模块)。

您将如何构建一个作为视觉辅助的桌面显示?

项目挑战

尝试找出您所在地区的交通系统是否向其用户提供此类数据。您将如何利用数据帮助通勤者节省时间?例如,您将如何使用此类数据向您的朋友/同事提供交通系统建议?

完成书后,我们将发布一个类似的示例,使用旧金山湾区快速交通(BART)的数据。

提高你的词汇量

使用 Python 可以提高您的词汇量!想象一下设置一个大型显示屏,它显眼地安装在某个地方,并且每天更新。我们将使用wordnik API(在www.wordnik.com/signup注册 API 密钥)。

  1. 第一步是为 python3 安装wordnik API 客户端:
       git clone https://github.com/wordnik/wordnik-python3.git
 cd wordnik-python3/
 sudo python3 setup.py install

wordnik API 有使用限制。有关更多详细信息,请参阅 API 文档。

  1. 让我们回顾一下使用wordnik Python 客户端编写我们的第一个示例。为了获取当天的单词,我们需要初始化WordsApi类。根据 API 文档,可以这样做:
       # sign up for an API key 
       API_KEY = 'API_KEY' 
       apiUrl = 'http://api.wordnik.com/v4' 
       client = swagger.ApiClient(API_KEY, apiUrl) 
       wordsApi = WordsApi.WordsApi(client)
  1. 现在WordsApi类已初始化,让我们继续获取当天的单词:
       example = wordsApi.getWordOfTheDay()
  1. 这将返回一个WordOfTheDay对象。根据wordnik Python 客户端文档,该对象包括不同的参数,包括单词、其同义词、来源、用法等。当天的单词及其同义词可以打印如下:
       print("The word of the day is %s" % example.word) 
       print("The definition is %s" %example.definitions[0].text)
  1. 将所有这些放在一起,我们有这个:
       #!/usr/bin/python3 

       from wordnik import * 

       # sign up for an API key 
       API_KEY = 'API_KEY' 
       apiUrl = 'http://api.wordnik.com/v4' 

       if __name__ == "__main__": 
         client = swagger.ApiClient(API_KEY, apiUrl) 
         wordsApi = WordsApi.WordsApi(client) 
         example = wordsApi.getWordOfTheDay() 
         print("The word of the day is %s" % example.word) 
         print("The definition is %s" %example.definitions[0].text)

上述代码片段可与本章一起下载,名称为wordOfTheDay.py。注册 API 密钥,您应该能够检索当天的单词:

       The word of the day is transpare
 The definition is To be, or cause to be, transparent; to appear,
       or cause to appear, or be seen, through something.

读者的挑战

您将如何将此应用程序守护程序化,以便每天更新当天的单词?(提示:cronjob 或datetime)。

项目挑战

可以使用wordnik API 构建一个单词游戏。想想一个既有趣又有助于提高词汇量的单词游戏。您将如何构建一个提示玩家并接受答案输入的东西?

尝试在显示器上显示当天的单词。您将如何实现这一点?

日志记录

日志(docs.python.org/3/library/logging.html)有助于解决问题。它通过跟踪应用程序记录的事件序列来确定问题的根本原因。让我们通过一个简单的应用程序来回顾日志。为了回顾日志,让我们通过发出一个POST请求来回顾它:

  1. 日志的第一步是设置日志文件位置和日志级别:
       logging.basicConfig(format='%(asctime)s : %(levelname)s :
       %(message)s', filename='log_file.log', level=logging.INFO)

在初始化logging类时,我们需要指定日志信息、错误等的格式到文件中。在这种情况下,格式如下:

       format='%(asctime)s : %(levelname)s : %(message)s'

日志消息的格式如下:

       2016-10-25 20:28:07,940 : INFO : Starting new HTTPS
       connection (1):
       maker.ifttt.com

日志消息保存在名为log_file.log的文件中。

日志级别确定我们应用程序所需的日志级别。不同的日志级别包括DEBUGINFOWARNERROR

在这个例子中,我们将日志级别设置为INFO。因此,属于INFOWARNINGERROR级别的任何日志消息都将保存到文件中。

如果日志级别设置为ERROR,则只有这些日志消息会保存到文件中。

  1. 让我们根据POST请求的结果记录一条消息:
       response = requests.post(IFTTT_URL, json=payload) 
       if response.status_code == 200: 
         logging.info("Notification successfully triggered") 
       else: 
         logging.error("POST request failed")
  1. 将所有这些放在一起,我们有:
       #!/usr/bin/python3 

       import requests 
       import logging 

       # find your key from ifttt 
       IFTTT_URL = "https://maker.ifttt.com/trigger/rf_trigger/
       with/key/$key" 

       if __name__ == "__main__": 
         # fetch the bike share information 
         logging.basicConfig(format='%(asctime)s : %(levelname)s
         : %(message)s', filename='log_file.log', level=logging.INFO) 
         payload = {"value1": "Sample_1", "value2": "Sample_2"} 
         response = requests.post(IFTTT_URL, json=payload) 
         if response.status_code == 200: 
           logging.info("Notification successfully triggered") 
         else: 
           logging.error("POST request failed")

前面的代码示例(logging_example.py)可与本章一起下载。这是 Python 中日志概念的一个非常简单的介绍。

Python 中的线程

在本节中,我们将讨论 Python 中的线程概念。线程使得能够同时运行多个进程成为可能。例如,我们可以在监听传感器的同时运行电机。让我们通过一个例子来演示这一点。

我们将模拟一个情况,我们希望处理相同类型传感器的事件。在这个例子中,我们只是打印一些内容到屏幕上。我们需要定义一个函数来监听每个传感器的事件:

def sensor_processing(string): 
  for num in range(5): 
    time.sleep(5) 
    print("%s: Iteration: %d" %(string, num))

我们可以利用前面的函数同时使用 Python 中的threading模块监听三个不同传感器的事件:

thread_1 = threading.Thread(target=sensor_processing, args=("Sensor 1",)) 
thread_1.start() 

thread_2 = threading.Thread(target=sensor_processing, args=("Sensor 2",)) 
thread_2.start() 

thread_3 = threading.Thread(target=sensor_processing, args=("Sensor 3",)) 
thread_3.start()

将所有这些放在一起,我们有:

import threading 
import time 

def sensor_processing(string): 
  for num in range(5): 
    time.sleep(5) 
    print("%s: Iteration: %d" %(string, num)) 

if __name__ == '__main__': 
  thread_1 = threading.Thread(target=sensor_processing, args=("Sensor 1",)) 
  thread_1.start() 

  thread_2 = threading.Thread(target=sensor_processing, args=("Sensor 2",)) 
  thread_2.start() 

  thread_3 = threading.Thread(target=sensor_processing, args=("Sensor 3",)) 
  thread_3.start()

前面的代码示例(可作为threading_example.py下载)启动三个线程,同时监听来自三个传感器的事件。输出看起来像这样:

Thread 1: Iteration: 0 
Thread 2: Iteration: 0 
Thread 3: Iteration: 0 
Thread 2: Iteration: 1 
Thread 1: Iteration: 1 
Thread 3: Iteration: 1 
Thread 2: Iteration: 2 
Thread 1: Iteration: 2 
Thread 3: Iteration: 2 
Thread 1: Iteration: 3 
Thread 2: Iteration: 3 
Thread 3: Iteration: 3 
Thread 1: Iteration: 4 
Thread 2: Iteration: 4 
Thread 3: Iteration: 4

Python 的 PEP8 样式指南

PEP8是 Python 的样式指南,它帮助程序员编写可读的代码。遵循某些约定以使我们的代码可读是很重要的。一些编码约定的例子包括以下内容:

  • 内联注释应以开头,后面跟着一个空格。

  • 变量应该遵循以下约定:first_var

  • 避免每行末尾的空格。例如,if name == "test":后面不应该有空格。

你可以在www.python.org/dev/peps/pep-0008/#block-comments阅读完整的 PEP8 标准。

验证 PEP8 指南

有工具可以验证您的代码是否符合 PEP8 标准。编写代码示例后,请确保您的代码符合 PEP8 标准。可以使用pep8包来实现。

    pip3 install pep8

让我们检查我们的代码示例是否符合 PEP8 规范。可以按照以下步骤进行:

    pep8 opencv_test.py

检查指出了以下错误:

    opencv_test.py:5:50: E231 missing whitespace after ','
 opencv_test.py:6:19: E231 missing whitespace after ','

根据输出结果,以下行缺少逗号后的空格,分别是第5行和第6行:

逗号后缺少尾随空格

让我们修复这个问题,并且我们的代码应该遵循 PEP8 规范。重新检查文件,错误将会消失。为了使你的代码可读,总是在将代码提交到公共存储库之前运行 PEP8 检查。

总结

在这一章中,我们讨论了 Python 中的高级主题。我们讨论了包括语音识别、构建通勤信息工具以及改善词汇量的 Python 客户端在内的主题。Python 中有许多在数据科学、人工智能等领域广泛使用的高级工具。我们希望本章讨论的主题是学习这些工具的第一步。

第十七章:机器人学 101

一提到机器人,我们就会被科幻小说所包围。我们可能会想起动画片《杰森一家》或者电影《终结者》。但事实上,机器人已经不再属于科幻小说。它们是真实存在的。环顾四周,指出任何物体;它可能没有机器人就不会被制造出来。现代时代已经被机器人塑造了。

但是,你也可以退一步思考,等一下,他所说的东西不是叫做机器而不是机器人吗?嗯,是的,你说得很对,但同时也错得很离谱。正是卡通和科幻小说赋予了一个被称为机器人的人形机器人的形象。但机器人远不止于此。

不幸的是,我们没有一个具体的、普遍认可的机器人定义,但是,正如我喜欢说的那样,任何能够执行物理和智力任务的机器都可以被称为机器人

现在,你可能会说,根据我的定义,甚至自动洗衣机都可以被称为机器人。嗯,从技术上讲,是的,为什么我们不称它为机器人呢?想想它为你做了什么,以及多年来进行了什么样的自动化。在你输入布料类型后,它会自动洗涤和烘干,就像你在 19 世纪自己做的那样。我想说的是,我们可以想象有各种各样的机器人,它们可以从根本上改变我们的生活方式。我们需要以更广阔的视角思考——不仅仅将机器人限制为人形机器人的形式。

我们生活在机器人和自动化的黄金时代,新产品的开发就像它可以变得那么简单。十年前可能需要一个工程师团队才能完成的工作,现在可以由一个人在卧室里在几分钟内完成,这要感谢开源世界。与此同时,有硬件计算能力可供你使用,你可以用几百美元在家里建立一个超级计算机。我们周围有各种问题,有些简单,有些复杂,等待着解决。整个过程中唯一缺失的环节就是你:一个有能力利用这些技术解决世界问题的创新思维。

为了让你的思维能够做到这一点,我们将从理解机器人学的根源和基础开始。这本书的目标不仅是制作书中提到的项目,而且是让你了解如何利用资源来建立你的梦想项目。

最后,我要祝贺你在正确的时间进入了这个令人惊叹和未来感的领域。我总是告诉我的学生一个规则,我也想和你分享:

  • 首先是科学家

  • 第二是研究员

  • 第三是工程师

  • 第四是技术员

  • 最后是技工

这意味着你越早进入任何领域的生命周期,你就越有可能在层次结构中升级。你越晚进入,就越难爬到顶部。

说了这么多,现在让我们直奔主题!在本章中,我们将涵盖以下主题:

  • 硬件装备

  • 设置树莓派

  • 编程

  • 玩电压

硬件装备

谈到机器人,它们由一些基本的有形组件组成,包括:

  • 计算单元

  • 传感器

  • 执行器

  • 底盘

  • 电源

首先,我们将讨论微控制器,并在书的过程中根据需要详细讨论其他有形组件。

每当你去购买笔记本电脑或电脑时,你一定听过微处理器这个词。这是必须做出所有决定的主要单位。我称它为“国王”,但没有帝国的国王算什么?国王需要一些可以为他做事的下属,就像微处理器需要一些下属,比如 RAM、存储、I/O 设备等。问题是,当我们放入所有这些东西时,整体单元变得昂贵和笨重。但是,正如我们所知,重量和大小在机器人方面非常重要,所以我们不能承受一个庞大笨重的系统来运行机器人。

因此,我们制造了一个叫做 SoC 的东西。现在,这是一个独角戏,因为这个小芯片本身就具有所有必要的系统来使其工作。所以,现在你不需要添加 RAM 或存储或任何其他东西来使其工作。这些小型微控制器可以变得非常强大,但缺点是,一旦制造商制造了一个 SoC,以后就不能对其进行任何更改。存储器的大小、RAM 或 I/O 都不能更改。但是我们通常可以接受这些限制,因为在编程机器人时,你可能不会使用微控制器的全部功能,直到你运行一些严肃的人工智能或机器学习代码。

这样一个伟大的硬件是树莓派。是的,听起来非常美味,但它还有更多的功能。这是一个超小但非常强大的微控制器。它通常被称为原型板,因为它被世界各地的机器人学家用来实现他们的想法并在短时间内使它们成为现实。它在全球范围内都可以获得,并且非常便宜。你可以在一个仅售 10 美元的设备上随时观看高清电影,上网等等。我想不出还有什么比这更荒谬的了。它非常容易使用,你可以使用 Python 来编程。

因此,基本上它符合我们所有的要求。这将是我们在整本书中将要使用的主要武器。

所以让我向你介绍树莓派!它看起来是这样的:

市场上有各种型号的树莓派。但我们将使用树莓派 Zero W;这将花费你大约 10 美元,比起大麦克汉堡更容易购买。确保你购买带有 W 的树莓派 Zero,这个 W 代表无线功能,比如 Wi-Fi 和蓝牙。还有一些其他东西,你需要订购或安排才能使其工作。以下是一些物品清单:

  • Micro USB 到标准 USB 适配器

  • 键盘

  • 鼠标

  • Micro SD 存储卡,16 或 32 GB

  • Micro SD 卡读卡器

  • Micro USB 电源适配器(2 安培或更高)

  • Micro HDMI 到 HDMI 端口

  • 面包板

  • 一堆跳线(公对公,公对母,母对母)

  • 3V LED

你可以立即从图像中看出,板载了一个微型 HDMI 端口,你可以通过它连接高清显示器或电视屏幕。其次是一个 Micro SD 卡槽。这将是这台电脑的主要存储设备。除此之外,你还会发现两个 USB 插座和一个摄像头总线。你可能会认为这就是全部,但最好的还在后面。树莓派有一个叫做 GPIO 的东西,它代表通用输入/输出。这些被伪装成树莓派的一个角落上的 40 个插孔,这就是它变得非常特别的原因。

现在,传统上您会将与计算机兼容的设备连接到计算机。因此,连接鼠标、键盘或游戏手柄就像插入 USB 端口一样简单,但是如果您需要将计算机连接到灯泡或空调呢?确切地说,您不能。这就是 GPIO 发挥作用的地方。这些引脚在机器人技术方面非常有用,因为它们可以用于连接各种组件,如传感器/电机。这些引脚的美妙之处在于,根据我们为其编程的需求,它们可以用作输入或输出。因此,正如我们将在后面看到的那样,这些引脚中的每一个都可以在程序中定义为输入或输出。

现在,这 40 个引脚中有 26 个是 GPIO。其余的引脚是通用电源或地线端口。还有两个称为ID EEPROM的端口,目前我们不需要它们。

正如您所看到的,树莓派能够为我们提供两种类型的电源供应:3.3V 和 5V。这基本上可以满足我们大部分的需求。

设置树莓派

我们将在一会儿讨论有关树莓派的 GPIO 和其他事项。首先,我们将了解如何首次设置此板。

您需要做的第一件事是确保树莓派的操作系统已经准备就绪。我假设您正在使用 Windows PC,但如果您也在其他操作系统上进行操作,那么差别不会太大。

要安装操作系统,请启动您的 PC 并按照以下步骤操作:

  1. 转到www.raspberrypi.org并单击 DOWNLOADS

  2. 现在点击 RASPBIAN,您将看到以下两个选项:

  • RASPBIAN STRETCH WITH DESKTOP

  • RASPBIAN STRETCH LITE

  1. 我们将下载 RASPBIAN STRETCH WITH DESKTOP;这将为我们提供树莓派的 GUI 界面

  2. 下载完成后,将包解压缩到一个文件夹中

现在我们需要将它复制到树莓派的存储卡上。您需要复制的存储卡必须经过低级格式化。基本上有两种格式化方式。一种只是擦除索引,另一种是我们所知道的低级格式化,即从索引和物理内存位置中删除所有数据。会有一个按钮来切换低级格式。确保在为此功能格式化存储卡之前点击它。我建议使用www.sdcard.org的 SD 卡格式化器。现在打开格式化器,您只需使用 32 KB 选项进行格式化。

在这里找到更多详细信息和更多最新信息:www.raspberrypi.org/documentation/installation/installing-images/README.md

完成后,您必须将映像复制到 SD 卡上。最简单的方法是使用 WinDisk Imager。您可以在线下载它而不会遇到任何问题。然后只需选择映像和 SD 卡上的位置,然后开始复制映像。

这可能需要几分钟。完成后,您的 SD 卡将准备就绪。将其插入树莓派,我们将准备好启动它。但在启动之前,使用 Micro HDMI 到 HDMI 线将显示器连接到树莓派,使用 Micro USB 将键盘和鼠标连接到树莓派,然后使用 Micro USB 适配器连接到标准 USB 适配器来为其供电。现在,使用树莓派上的另一个 USB 端口,使用 Micro USB 电源适配器为其供电。

一旦启动,您将看到一个启动屏幕,几秒钟后您将能够看到桌面。因此,我们的树莓派终于启动并运行了。

继续探索一些选项,上网冲浪,观看 YouTube 上的一些猫视频,并熟悉这个强大的设备。

到目前为止,您可能已经开始欣赏树莓派的强大。它可能比您平常使用的计算机稍慢。但是,拜托,这个东西只要 10 美元!

让我们编程

在本章中,我们将让您熟悉 Python 以及如何在此设备上使用 GPIO。要做到这一点,请点击左上角的树莓图标。您将看到 Python 控制台 3.0。也可能有旧版本的 Python。我们将在本书中使用更新的版本。

一旦窗口打开,您将看到您将在其中进行编码的游乐场。所以现在我们准备为 Python 机器人编写第一行代码。现在让我们看看它是如何完成的。

我们将首先写的是:

几乎所有的时候,当我们开始编写程序时,我们会首先写上述行。现在,在我们了解它的作用之前,我们需要了解库。通常在编写代码时,我们将不得不在多个地方一遍又一遍地编写代码。这需要很多时间,而且肯定不酷!

因此,为了解决这个问题,我们创建了函数。函数是一个微型程序,我们可能认为它会一遍又一遍地使用。在这个微型程序中,我们还提到它将被称为什么。

假设有一个代码,我们需要一遍又一遍地将两个数字相乘。所以,我们写一次代码并将其制作成一个函数。我们还将这个函数命名为Multiply

现在,每当我们需要相乘两个数字时,我们不必再次编写它的代码;相反,我们只需调用函数来代替我们编写相乘的代码。问题是,我们如何告诉程序要相乘哪个数字呢?

这也有一个解决方案。正如您以后可能看到的,每当调用一个函数时,我们在其后放上开放和关闭的括号,比如multiply()

如果括号是空的,那意味着没有给出用户输入。例如,如果我们要相乘23,我们只需写Multiply(2,3)

我们正在输入23。括号中的输入位置也很重要,因为括号中的位置将定义程序中的位置。

现在,假设您创建了这样的函数:

  • 加法

  • 减法

  • 相乘

  • 除法

假设您将它们堆叠在一起。然后,一堆函数组合在一起将被称为库。这些库可以有数百个函数。有一些函数已经在 Python 语言中,以便程序员更轻松地完成工作。其他可以定义为开源或根据您的方便开发。

现在,回到重点。我们正在调用RPi.GPIO库;这是由树莓派定义的库。这将使您在编程树莓派时更加轻松。因此,在程序中,一旦我们调用库,所有函数都可以随时使用。

在下一行,我们写Import.time。正如您可能已经猜到的那样,这是用来导入时间库的。我们很快就会了解它的作用。

下一行代码将如下所示:

在我们了解它的作用之前,让我们更多地了解一下 GPIO。这些引脚根据它们在树莓派中的物理位置进行了硬编号。但是,我们可以在软件中更改引脚的编号以便我们理解和方便使用。但在这个代码中,我们不会对此进行操作,而是将其设置为由 Broadcom 制造的默认设置,这是树莓派微控制器的制造商。

这一行使用了RPi.GPIO库的一个名为setmode的函数。这个函数的作用是将setmode的引脚配置设置为(GPIO.BCM)—BCMGPIO的一个进一步的函数。

现在我们可以使用基本引脚配置。GPIO 引脚的特点是可以同时用作输入和输出。但唯一的条件是我们必须在程序中指定它是要用作输入还是输出。它不能同时执行两个功能。下面是如何做到的:

下一行代码将如下所示:

同样,我们使用了 GPIO 库的一个函数output。它的作用是设置板子上特定引脚的状态。所以,这里我们指定了引脚号23必须设置为高电平。只是为了清楚起见,高电平表示开,低电平表示关。

下一行代码将如下所示:

在这一行中,我们使用了来自 time 库的一个函数。sleep 函数基本上会冻结所有 GPIO 引脚的状态。所以,例如,如果引脚23是高电平,那么它将保持高电平,直到执行time函数sleep。在sleep函数中,我们定义了值为3秒。

因此,3 秒内,树莓派的引脚状态将保持在这行代码之前的状态。

最后,代码的最后一行将是:

这将是每个程序之后的常见情景。GPIO 库的这个函数将重置程序中使用的每个引脚的状态,所有引脚的状态将变为低电平。记住,它只会影响程序中使用的引脚,而不会影响树莓派上的其他引脚。所以,例如,我们在程序中使用了引脚23,所以它只会影响引脚23,而不会影响树莓派上的其他引脚。

最后,你的程序会看起来像这样:

现在,你必须记住的一件事是,我们写的任何代码都将按顺序执行。所以,假设我们把import RPI.GPIO as GPIO放在底部,那么整个程序将无法工作。为什么?因为一旦它到达GPIO.setmode(GPIO.BCM),它将不理解GPIO是什么,也不会理解setmode是什么。因此,我们总是在开始编写代码时导入库。

现在,基于相同的概念,它将以以下方式执行程序:

  • GPIO.out(23,GPIO.High): 它会将引脚23设置为高电平

  • time.sleep(3): 它会等待 3 秒,而引脚仍然是高电平

  • GPIO.cleanup(): 最后,它会将引脚23的状态设置为低电平

现在,为了查看程序是否工作,让我们连接一些硬件来检查我们所写的是否真的发生了。

我假设读者已经知道如何使用面包板。如果你不熟悉,就去搜索一下。理解起来只需要 5 分钟。它非常简单,会派上用场。

现在继续连接 LED 到面包板上,然后将 LED 的地连接到树莓派上的地引脚,并将正极/VCC 连接到引脚号23(参考引脚图)。

你也可以参考以下图表:

完成后,运行代码看看会发生什么!

LED 将会发光 3 秒,然后再次关闭,正如我们预期的那样。现在让我们玩弄一下代码,做一点修改。这次,我们将添加一些用粗体标记的额外行:

import RPi.GPIO as GPIO
from time
import sleep
GPIO.setmode(GPIO.BOARD)
GPIO.setup(23, GPIO.OUT)
while True:
 for i in range(3):
  GPIO.output(23, GPIO.HIGH)
sleep(.5)
GPIO.output(23, GPIO.LOW)
sleep(.5)
sleep(1)
GPIO.cleanup()

在理解代码内部之前,你会注意到并不是每一行都对齐,它们已经被缩进了。这是什么意思?

一个与其他代码行一起缩进的行称为块。所以例如,如果你有一个语句如下

while True:
 for i in range(3):
  GPIO.output(23, GPIO.HIGH)
sleep(.5)
GPIO.output(23, GPIO.LOW)
sleep(.5)
sleep(1)
GPIO.cleanup()

现在在这行让我们看看代码将如何运行。

  • 一个 while true 循环将运行,这将运行其中的代码,即
for i in range(3):
  GPIO.output(23, GPIO.HIGH)
sleep(.5)
GPIO.output(23, GPIO.LOW)
sleep(.5)
sleep(1)
  • 之后,代码for I in range (3):将运行。它将运行 for 循环内的代码,直到I的值在范围内,因此下面的代码将运行。
GPIO.output(23, GPIO.HIGH)
sleep(.5)
GPIO.output(23, GPIO.LOW)
sleep(.5)

上面的代码可以称为一个代码块,它在for循环内。代码块可以通过缩进代码来制作。

现在,让我们看看它的作用。While True是一个循环,它将一遍又一遍地运行for循环,直到条件不再为假。我们在这里使用的条件是:

for i in range(3):

最大范围是3,每次语句运行时,i的值增加+1。因此,它基本上充当计数器。让我们看看程序实际会做什么。

它将检查i的值,并在此之后递增1。随着代码的进展,它将使 LED 在 0.5 秒内高亮,然后在 0.5 秒内关闭。然后它将等待 1 秒。这将重复,直到 while 循环为假,也就是i的值大于3,它将退出程序并终止。运行程序,看看它是否真的发生了。

到目前为止,你已经了解了在树莓派中编程是多么容易。为了更进一步,我们将编写另一个程序并对硬件进行一些更改。

我们将从引脚号 7 到 12 连接五个 LED。我们将使它们以一定模式开关。

连接后,我们将编写以下代码:

import RPi.GPIO as GPIO
from time
import sleep
GPIO.setmode(GPIO.BOARD)
GPIO.setup(7, GPIO.OUT)
GPIO.setup(8, GPIO.OUTPUT)
GPIO.setup(9, GPIO.OUTPUT)
GPIO.setup(10, GPIO.OUTPUT)
GPIO.setup(11, GPIO.OUTPUT)
while True:
  for i in range(7, 12):
  GPIO.output(i, GPIO.HIGH)
sleep(1)
GPIO.cleanup()

现在代码非常简单。让我们看看它的意思:

在我告诉你更多关于代码的事情之前,让我们继续运行它。

当你运行它时,你会明白,根据语句,它会逐个地址引脚,并在每隔 1 秒后将它们切换到高电平。

玩耍电压

到目前为止一切顺利!但你注意到了一件事吗?我们一直在使用树莓派作为开关,简单地打开和关闭各种组件。但是如果我们需要改变刚刚编程的 LED 的强度怎么办?这可能吗?答案是否定的。但我们仍然可以以某种方式完成!

让我们看看如何。计算机以二进制工作,这意味着它们可以表示01。这是因为任何系统中的主要计算单元都是基于可以表示01的晶体管。因此,从技术上讲,计算机只能进行二进制架构的切换。然而,有一个技巧。这个技巧叫做脉宽调制PWM)。

现在,在我详细解释任何内容之前,让我们继续将 LED 插入到引脚号为18,然后将此代码复制到树莓派并运行:

import RPi.GPIO as GPIO
import time                             
GPIO.setmode(GPIO.BCM)       
GPIO.setup(18,GPIO.OUT)         

pwm= GPIO.PWM(18,1)
duty_cycle = 50
pwm.start(duty_cycle)

time.sleep(10)

GPIO.cleanup()

你注意到了吗?LED 将以每秒一次的频率闪烁。现在让我们稍微调整一下,将PWM(18,1)改为PWM(18,5)。运行并看看会发生什么。

您可能已经注意到它现在每秒闪烁五次。因此数字5基本上表示频率,因为 LED 现在每秒闪烁五次。现在,再次重写代码并将5增加到50。一旦增加到50,它会在一秒内开关 LED 50 次,或者以 50 赫兹的频率。因此,对您来说,它看起来好像一直开着。

现在是有趣的部分。转到您的代码,并将duty_cycle = 50更改为duty_cycle = 10

你注意到了什么?你一定已经注意到 LED 的亮度现在低得多。实际上,它将是原来的一半。

让我们看看实际发生了什么:

从图表中可以看出,该函数基本上创建一个脉冲,我们正在更改其特性。第一个特性是频率,即每秒生成的脉冲。在代码行pwm= GPIO.PWM(18,1)中,我们基本上告诉微控制器在引脚号1上每秒生成一个脉冲。在第二行中,占空比是一个百分比值。它确定脉冲高电平的时间占总脉冲时间的百分比。对于以下代码,以下项目将是其特性:

pwm= GPIO.PWM(18,1)
duty_cycle = 50
  • 每个脉冲的时间/宽度为 1 秒

  • 它将打开的时间占 50%

  • 它将关闭的时间占 50%

  • 它将打开的时间为 0.5 秒

  • 它将关闭 0.5 秒

当我们增加频率超过 50 赫兹时,人眼很难分辨它是在开还是在关。理论上,引脚将保持高电平的时间占 50%,其余时间将是低电平。因此,如果我们取平均值,我们可以很容易地说整体电压将是原始电压的一半。使用这种方法,我们可以根据我们的需求调制任何引脚的电压输出。

总结

现在你一定已经明白了 GPIO 如何被用作输出,以及通过应用条件,我们如何改变它们的行为。

在下一章中,我们将了解这些引脚如何被用作输入。所以回来吧,我们在那里见!

第十八章:使用 GPIO 作为输入

在上一章中,我们了解了 GPIO 如何用于输出。但是,正如其名称所示,GPIO 既可以用于输入也可以用于输出。在本章中,我们将看到如何使用这些引脚将数据输入到树莓派上。

本章我们将涵盖的主题有:

  • 深入了解 GPIO

  • 与 PIR 传感器的接口

  • 与超声波接近传感器的接口

  • 通过 I2C 进行接口

深入了解 GPIO

我相信你还记得上一章的这行代码:

GPIO.setup(18,GPIO.OUT)

正如前面解释的,这基本上告诉我们在某个程序中 GPIO 引脚的行为。到现在为止,你一定已经猜到,通过改变这一行代码,我们可以改变引脚的行为,并将其从输出转换为输入。这就是你会这样做的方式:

GPIO.setup(18,GPIO.IN)

一旦在程序中写入这行代码,微控制器将知道在程序运行时,引脚号18只用于输入目的。

要理解这实际上是如何工作的,让我们回到我们的硬件,看看它是如何完成的。首先,你需要将 LED 连接到任何一个引脚;在这个程序中,我们将使用引脚号23。其次,你需要在引脚号24上连接一个开关。你可以参考接下来的图表来进行连接:

一旦连接好,你可以继续编写这个程序:

import time import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setup(24,GPIO.IN)
GPIO.setup(23,GPIO.OUT)
while True:
  button_state = GPIO.input(24)
    if button_state == True:
      GPIO.output(23,GPIO.HIGH)
    else:
      GPIO.output(23,GPIO.LOW)
  time.sleep(0.5)
GPIO.cleanup()

一旦程序上传,当你按下按钮时,LED 将自行打开。

让我们了解到底发生了什么。while True:基本上是一个无限循环;一旦应用了这个循环,循环内运行的代码会一遍又一遍地重复,直到有什么东西打破它,而所谓的打破是指导致程序停止并退出的某种中断。现在,理想情况下,我们通过按下Ctrl + C来退出程序,每当有一个无限循环时。

button_state = GPIO.input(24)  

在上面的一行中,程序理解它需要查找的地方;在这个程序中。在这一行中,我们告诉程序我们正在寻找 GPIO 24,这是一个输入:

if button_state == True:
   GPIO.output(23,GPIO.HIGH)  

如果按钮是高的,换句话说,当按钮被按下并且电流到达引脚号24时,那么 GPIO 引脚号23将被设置为高:

  else:
   GPIO.output(23,GPIO.LOW)

如果引脚号24不为真,它将遵循这行代码,并保持引脚号23低,换句话说关闭。

所以,这就是你用于输入目的的第一个 GPIO 程序。

与 PIR 传感器的接口

到目前为止一切顺利!在这个单元中,我们将继续接口我们的第一个传感器,即被动红外传感器,通常称为 PIR 传感器。这个传感器是一个非常特殊的传感器,在自动化项目中非常常见。它的低能耗使其成为物联网项目的绝佳竞争者。所以让我们看看它是如何工作的。

你一定注意到了,当我们将金属加热到高温时,它慢慢变成深红色,当我们进一步加热时,它变得更亮,慢慢从红色变成黄色,如下图所示,显示了一个红热的钢片。现在,随着温度的升高,发射辐射的波长减小;这就是为什么随着温度的升高,颜色从红色变成黄色,因为黄色的波长比红色短。

但有趣的是,即使物体没有被加热到足够的温度,它们也会发射辐射;事实上,任何高于绝对零度温度的物体都会发射某种形式的辐射。有些我们能用肉眼看到,有些我们看不到。因此,在室温下,物体会发射红外辐射,其波长比可见光更长。因此,我们的眼睛看不到它。尽管如此,它仍然存在。

这个 PIR 传感器的作用是感知周围物体发出的红外光,每当物体移动时,它可以感知其模式的整体变化,并且基于此可以检测到其附近是否发生了任何运动。

我们假设当房间里有人时,会有一些固有的运动发生,因此这种传感器非常常用作占用传感器。现在,让我们连接这个传感器,看看我们如何使用它:

一旦您按照上图连接好了,就可以上传代码了:

import time import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setup(23,GPIO.IN) 
GPIO.setup(24,GPIO.OUT)
while True:
 if GPIO.input(23) == 1: 
  GPIO.output(24,GPIO.HIGH)
 else: 
  GPIO.output(24,GPIO.LOW)

 time.sleep(1)
GPIO.cleanup()

现在,让我们看看发生了什么。逻辑非常简单。一旦 PIR 传感器检测到运动,它就会将输出引脚设置为高电平。我们所要做的就是监视该引脚,基本上就是这样。

逻辑与按键开关完全相似,它也会以类似的方式工作。因此,不需要太多解释。

接口超声波接近传感器

首先,基础知识。接近传感器是一种传感器,它可以感知到与其接近的物体。有许多传感器可以完成这项任务,以及许多技术可以让我们这样做。正如其名称所示,超声波接近传感器是基于超声波的工作原理。工作原理非常容易理解。超声波传感器发射一束超声波;这些波对人耳来说是听不见的,但它仍然是一种声波,它也像声波一样行为。

现在,我们知道声音会反射不同的表面并形成回声。当您在空房间说话时,您一定有过这种回声的经历。您可以听到自己的声音,但有轻微的延迟。这种延迟是由声音的特性引起的。声音是一种波,因此它有速度。声波有固定的传播速度。因此,为了覆盖特定的距离,它们需要一定的时间。通过计算这段时间,我们可以推导出声波在从表面反射之前走过的距离。

同样,在这种传感器中,我们向特定方向发射超声波,然后感知反射回来的回声。自然地,接收回声会有一定的延迟;这个延迟会与物体距离传感器的距离成正比,基于这个延迟,我们可以轻松计算出距离。

现在,要使用接近传感器,我们需要了解传感器的物理结构以正确接线。传感器上有四个引脚,它们是:

  • VCC(正极)

  • 触发

  • 回声

  • GND(地线)

显然我不需要解释 VCC 和地线的作用。因此,让我们直接转到触发。每当引脚高电平持续 10 微秒时,超声波传感器将向目标发送 8 个 40kHz 的声波周期。一旦触发周期完成,ECHO被设置为高电平。一旦接收到回声信号,ECHO引脚就会被设置回低电平。以下是一个图表,展示了它实际发生的过程:

这就是我们现在需要知道的全部。随后,随着我们的学习,我们会了解更多。现在,继续并让它运行起来,按照图表连接:

连接完成后,需要运行以下代码:

import RPi.GPIO as GPIO
import time

GPIO.setmode(GPIO.BCM)
GPIO.setup(23,GPIO.OUT) 
GPIO.setup(24,GPIO.IN) 

while True:
     pulse_start = 0
     pulse_stop = 0
     duration = 0
     distance = 0

     GPIO.output(23,GPIO.LOW)
     time.sleep(0.1) 
     GPIO.output(23,GPIO.HIGH)
     time.sleep(0.000010)
     GPIO.output(23,GPIO.LOW)

     while GPIO.input(24)==0:
         pulse_start = time.time()

     while GPIO.input(24)==1:
         pulse_stop = time.time()

     duration = pulse_stop - pulse_start

     distance = duration*17150.0
     distance = round(distance,2)
     print ("distance" + str(distance)) 

     time.sleep(0.2)
}

现在,一旦您运行这个程序,屏幕上的输出将每 0.2 秒显示一次物体的距离。现在,您一定想知道这是如何传达所有这些读数的:

GPIO.setup(23,GPIO.OUT) 

我们将分配引脚23在需要时给传感器的TRIGGER引脚提供脉冲:

GPIO.setup(24,GPIO.IN)

我们将分配引脚24来接收逻辑以确认接收到回声信号:

pulse_start = 0
 pulse_stop = 0
 duration = 0
 distance = 0

我们将使用上述作为变量,并且每次循环开始时,我们都会给它们赋值为0;这是为了清除在程序过程中存储的先前读数:

GPIO.output(23,GPIO.HIGH)
  time.sleep(0.000010)
  GPIO.output(23,GPIO.LOW)

我们保持触发引脚编号23高 0.000010 秒,以便超声波传感器可以发送一脉冲超声波:

  while GPIO.input(24)==0: 
 pulse_start = time.time()

这个 while 语句将一直记录pulse_start变量的时间,直到引脚编号24的时间为低电平。最终的时间读数将存储在pulse_start变量中,即记录脉冲发送的时间:

while GPIO.input(24)==1:
 pulse_stop = time.time()

在这个循环中的while语句将开始记录引脚编号24高电平的时间,并将一直记录时间,直到引脚编号24保持高电平。时间的最终读数将存储在pulse_stop变量中,即记录脉冲接收的时间:

 duration = pulse_stop - pulse_start

在这个声明中,我们正在计算脉冲从传感器到物体再反弹到传感器接收器所需的总时间:

 distance = duration*17150.0

这是制造商提供的一个算术公式,用于将超声波传播所需的时间转换为厘米的实际距离。你可能会问我们是如何得到这个方程的?

让我简要介绍一下。通过初等物理,我们会记得这个简单的方程:速度 = 距离 / 时间

现在你可能还记得声音的速度是 343 米每秒。现在 1 米有 100 厘米,因此要将这个速度转换为每秒厘米,我们必须将速度乘以 100,因此速度将是每秒 34,300 厘米。

现在我们知道方程的一个元素,即速度。所以让我们把速度的值放入方程中。现在方程看起来会是这样:34,300 = 距离 / 时间

现在我们知道声音传播的距离是实际距离的两倍。为什么?因为声音首先从传感器传向物体。然后它从表面反射回来并到达传感器。因此,它实质上覆盖了两倍的距离。因此,为了适应这个方程,我们必须做出一个小改变:34,300 / 2 = 距离 / 时间

现在我们想从这个方程中得到距离,所以让我们把所有其他部分移到另一边。现在方程看起来会是这样:17,150 * 时间 = 距离

所以这里有距离的公式。

distance = round(distance,2)

由于超声波传播的距离是实际距离的两倍(一次是朝物体前进,第二次是反弹到传感器),我们将其除以二得到实际距离:

print 'Distance = ',distance

最后,我们将通过以下声明打印测得的距离。任何在引号内的内容'...'都将按原样写入。但是,distance没有引号,distance 是一个变量。因此,存储在距离中的变量将在屏幕上的最终输出中写入:

 time.sleep(0.25)

代码将在这一行暂停 0.2 秒。如果没有这个暂停,那么值将以令人难以理解的速度出现,这对我们来说将很难阅读或理解。如果你在摆弄,我建议删除这个声明并运行代码看看会发生什么。

通过 I2C 进行接口

到目前为止,一切都很好。电子电路可能非常有趣,虽然它们看起来非常复杂,但我们经常发现工作非常简单。在前一节中,我们一次只接口一个传感器。我们可以继续接口多个传感器,但我们受到现有 GPIO 数量的限制。我们还看到一些传感器,如超声波传感器可能使用多个 GPIO 引脚进行工作。这进一步减少了我们可以与微控制器接口的传感器数量。一旦我们转向更复杂的电路,我们还会意识到布线可能会变得非常混乱,如果出现问题,找出问题所在将变得非常繁琐。

现在,我们在设计机器人系统时面临的一个更大的问题是时间的问题——系统中的所有工作都必顶同步。目前大多数系统都是顺序的,即一个单元的输出成为另一个单元的输入:

现在,为了完成任务,当需要时,处理单元 1必须将输入传递给处理单元 2处理单元 3也是如此。如果数据的时间不完美,那么处理单元 2要么会一直等待处理单元 1的输入,要么更糟糕的是,处理单元 1会在处理单元 2不需要数据的时候发送数据。在这种情况下,数据将丢失,过程将出现一些错误。

因此,为了解决这个问题,当时的计算机科学家发明了一种脉冲系统。时钟脉冲是一个非常简单的方波,具有 50%的占空比(回想一下脉冲宽度调制(PWM))。电路被设计为在时钟脉冲的上升沿或下降沿执行一次操作。由于这种同步,电路的每个部分都知道何时工作。时钟脉冲的样子如下:

现在,回到问题上,我们有两个问题:

  • 机器人连接的设备/传感器存在物理限制

  • 如何使传感器和互连电路的时间协调工作

为了解决这些问题,我们使用了一个非常常用的协议,称为 I2C,代表着互联集成电路。当我们需要在相同的 GPIO 上连接多个设备时,比如只有一组 GPIO 引脚可以连接多个传感器时,这个协议非常有用。这是由于为每个硬件分配了唯一的地址。该地址用于识别传感器,然后相应地与其通信。现在,要实现 I2C 协议,我们需要两条线路;这些线路如下:

  • 数据

  • 时钟

正如你可能已经猜到的那样,时钟线用于向连接的设备发送时钟脉冲,数据是数据流动的总线。

现在,整个 I2C 架构是基于主从配置工作的,其中主设备始终为从设备生成时钟信号,从设备必须不断寻找主设备发送的时钟脉冲和数据包。让我们看看是如何完成的。

如前所述,有两条线路:数据线称为串行数据(SDA),时钟线称为串行时钟(SCL)。从现在开始,我们将使用 SCL 和 SDA 这些术语:

让我们看看图中显示的主要要点:

  • 起始条件:为了开始通信,创建一个起始条件,表示通信即将发生。主设备通过在 SCL 之前保持 SDA 线低来表示这个条件。这表示所有从设备都准备好进行通信。

  • 地址帧:一旦通信开始,主设备发送需要通信的设备的地址。这是一个 7 位地址。在每个时钟脉冲中,一个位被发送,因此需要七个时钟脉冲来发送 7 位地址。在这 7 位地址之后是读/写位。这表明设备是否在这个操作中想要写入,还是想要读取一些数据。因此,总地址帧是 8 位,需要八个时钟脉冲来发送。在这八个脉冲之后,在第九个时钟脉冲期间,主设备等待来自设备的确认。当 SDA 线被被寻址的从设备拉低时,从设备发送这个确认。通过这种策略,主设备知道它发送的地址已经被接收,并且从设备现在准备好进行通信。如果没有发送确认,那么由主设备决定接下来该做什么。

  • 数据帧:一旦确认被发送,根据是读操作还是写操作,数据要么由主设备写入从设备,要么在读操作中,数据由从设备发送到主设备。这个数据帧的长度可以是任意的。

  • 停止帧:一旦数据传输完成,主设备发出停止条件,表示通信必须停止。当 SDA 线在 SCL 线从低电平变为高电平后,此条件被执行。

这基本上就是 I2C 通信的工作原理。对于每个设备,我们有一个 7 位地址,因此我们可以在单个总线上连接多达 128 个设备。这是很多设备。几乎可以忽略物理限制用完的可能性。现在让我们继续看看如何通过这种协议连接传感器。通常,不需要为 I2C 进行核心编程,因为这很冗长和繁琐。这就是开源的魔力所在。全球有很多开发人员正在研究这些传感器,其中大多数人足够慷慨,制作了一个库并分享给大家以便编程。这些库可以在线获取,其中大多数库都处理了通信的复杂过程。

现在是我们接口第一个 I2C 设备的时候了,这是一个模拟到数字转换器。你一定会想为什么我们首先使用这个转换器。回想一下我们开始理解 GPIO 引脚的时候。这些神奇的引脚可以用作输入和输出;你可能还记得这些引脚可以是开或关状态——这些都是数字引脚,不仅在输出时,也在输入时。但是有大量的传感器是通过模拟通信工作的。由于树莓派的数字架构,直接接口这些传感器是困难的。因此,我们使用模拟到数字转换器ADC),这个转换器将传感器的模拟值转换为树莓派可以理解的数字位。

我们将连接一个 LDR,电阻将根据光线的多少改变电阻值。因此,电压将取决于光线照射在 LDR 上的多少。

现在让我们看看如何实际操作。拿起你的树莓派,让我们开始吧。首先,我们需要在树莓派上启用 I2C;按照这里列出的步骤进行操作:

  1. 打开终端(Ctrl + Shift + T

  2. 输入sudo raspi-config

  3. 选择接口选项:

  1. 然后转到高级选项:

  1. 然后选择 I2C 以启用它。然后选择是:

现在安装adafruit库以接口 ADC1115:

  1. 打开终端并复制以下命令:
sudo apt-get install build-essential python-dev python-smbus python-pip 

这个命令将库和依赖项下载到树莓派上

  1. 现在输入以下内容:
sudo pip install adafruit-ads1x15

这个命令将库和依赖项安装到树莓派上。

现在软件已经设置好了,让我们准备好硬件。按照下图将树莓派连接到 ADS1115:

准备好后,继续在 Pi 上上传这段代码:

import time
import Adafruit_ADS1x15
import RPi.GPIO as GPIO
LED =14

GPIO.setmode(GPIO.BCM)
GPIO.setup(LED,GPIO.OUT)

adc = Adafruit_ADS1x15.ADS1115()
GAIN = 1
channel=0
adc.start_adc(channel, gain=GAIN)

while True:
    value = adc.get_last_result()
    print(str(value))
    time.sleep(0.1)
    if value >= 100:
        GPIO.output(LED,1)
    else :
        GPIO.output(LED,0)

adc.stop_adc()

请注意,有时这段代码可能不起作用,如果是这样,请尝试调整阈值的值:

if value >= 100:

你可能已经注意到,每当 LDR 面向光源时,LED 也会亮起,而当它远离光线时,LED 会熄灭。

现在你已经接口了一个 I2C 设备。让我们了解这段代码实际上是如何工作的:

import Adafruit_ADS1x15

上一行代码导入了Adafruit_ADS1x15库,以便我们在程序中使用它的所有函数。

adc = Adafruit_ADS1x15.ADS1115()

上一行代码创建了库Adafruit_ADS1x115的实例。.ADS1115()是创建实例adc的函数。明白了吗?让我用英语解释一下。

现在,我们可以简单地写adc而不是一直写Adafruit_ADS1x15,来调用库函数。此外,你可以使用任何单词代替adc;它可以是你猫的名字或你邻居的名字,它仍然可以工作:

GAIN = 1

这是传感将进行的值。1表示传感将在整个范围内进行。对于我们的 ADC 来说,范围是从 0V 到+/-4.096V 的电压范围。现在改变增益会导致传感范围的改变。也就是说,如果我们将增益值更改为2,那么传感范围将是原始范围的一半,即 0 到+/-2.048 伏。

现在你可能会问电压范围是多少,为什么我们要改变增益?

原因很简单。有不同类型的模拟传感器。它们的输出电压范围各不相同。有些传感器的输出范围是 0.5 伏到 4 伏,其他的可以是 0.1 伏到 0.98 伏。现在,如果我们将增益设置为1,那么所有这些传感器都可以轻松接口。因为它们都在 0 到 4.098 伏的感应范围内。然而,由于它是一个 16 位 ADC,因此 ADC 可以提供的离散值的总数将在 2¹⁶或 65,536 个读数之间。因此,在增益为1时,ADC 可以检测到的最小电压变化为:4.096 / 65536 = 0.000062

但是,如果增益增加到4,那么传感范围将减少到仅为0到+/-1.0245。因此,这将能够处理 0.1 伏到 0.98 伏之间的输出范围。但现在让我们看看它可以检测到的最小电压变化:1.0245 / 65536 = 0.00001563

现在你可以看到可以检测到的最小电压非常低。这对于与传感器的兼容性是一件好事。

现在,你可以决定你想要什么增益值。LDR 在 5V 上工作,因此最好使用整个增益读数为1

channel=0

当你仔细观察 ADC 硬件时,你会注意到有各种引脚,包括A0A1A2A4。这是一个四通道 ADC——它可以将四个模拟输入转换为数字数据。由于我们只使用一个数据流,我们将让 Pi 知道它连接在哪个引脚上。通过下面的代码,我们告诉 Pi 开始转换数据的过程:

adc.start_adc(channel, gain=GAIN)

在下一行中,我们指示 ADC 停止转换,代码到此结束。

adc.stop_adc()

摘要

本章主要讲述了如何将传感器与 GPIO 进行接口,以便传感器可以检索数据。

第十九章:制作园丁机器人

好了,朋友们,你已经了解了一些输入和输出的基础知识;现在是时候制作一些我们可以交出一些日常责任的东西了。这个机器人可能看起来并不像一个机器人,但相信我,它会让你的生活更轻松。最重要的是,你花园中的大部分植物都会因为你的制作而祝福你。

我们将涵盖以下主题:

  • 与电磁阀一起工作

  • 制作机器人

  • 使它更智能

  • 使它真正智能

与电磁阀一起工作

我们要做的是一个自动系统,它会在植物需要时给它们浇水。所以从技术上讲,一旦它建立起来,你就不用担心给你的绿色生物浇水了。无论你是在家里、在办公室还是度假,它都会不管任何情况下继续工作。

现在,你一定在想它是如何给植物浇水的,所以让我告诉你,对于这个世界上的每个问题,都存在一个解决方案。在我们的情况下,这个解决方案被称为电磁阀。它的基本作用是切换液体的流动。市场上有各种各样的电磁阀;一些识别特征如下:

  • 尺寸:它们有各种尺寸,如半英寸、四分之三英寸、1 英寸等。这基本上将决定电磁阀的流量。

  • 介质:无论是液体、气体、蒸汽等。

  • 正常状态

  • 通常打开:这个阀门在关闭状态下会允许液体流动——当阀门没有供电时

  • 通常关闭:这个阀门在关闭状态下会阻止液体流动——当阀门没有供电时

  • 方式数量:一个简单的阀门会有一个进口和一个出口。所以,当它打开时,它会允许液体从进口流向出口。然而,还可以有其他类型的阀门,比如三通阀,可能有两个出口和一个进口。它会调节液体的流动方向。

阀门的一些具体细节也可能会有所不同,但目前我们只需要知道这些。关于电磁阀要注意的一点是,这些阀门可以打开或关闭。无法实现这些阀门之间的任何状态或通过这些阀门控制流动。为此,我们可以使用伺服阀或电动阀。但目前我们不需要。

在本章中,我们将使用一个半英寸的水/液体阀,它通常是关闭的。当你仔细看这个阀时,你会发现它在 12 伏特下运行,电流消耗接近 1 安培。这对树莓派来说是很大的电流。树莓派每个引脚可以提供的电流上限约为 50 毫安。所以如果我们把这个阀接到树莓派上,它肯定不会工作。

我们现在该怎么办?这个问题的答案是继电器。继电器的基本工作是重新布置电路。基本上,它是一个电子控制开关。继电器的基本工作是打开和关闭具有比控制单元提供的更高电流/电压消耗的设备。这是一个相当简单的设备,正如你在图中所看到的。有两个电路。一个是蓝色的,是低电压和低电流电路。这个电路正在给线圈供电。另一个电路是红色和黑色的。这个电路是高电压、高电流电路。

在初始阶段,正如你所看到的,高电压高电流电路不完整,烤箱不会工作:

现在,在这第二个图中,你可以看到蓝色电路连接到 5V 电源,线圈被激活。每当线圈被激活,它就形成一个电磁铁,吸引高功率电路的金属片,使电路完整,从而给烤箱供电:

这就是电磁阀的工作原理。线圈的消耗几乎只有几毫安,因此通过微控制器驱动线圈非常容易。这反过来使得最终电路之间产生接触。

市场上有各种类型的继电器;一些识别特征如下:

  • 最大输出电压:它可以处理的最大电压

  • 最大输出电流:它可以承受的连接到它的任何输出设备的最大电流

  • 信号电压:它需要开关组件的电压

  • 正常条件:

  • 正常关闭:这将不允许任何电流流动,直到接收到信号为止

  • 正常开启:它将允许电流流动,直到接收到信号为止

现在,回到我们的园艺机器人,连接到它的电磁阀将在 1 安培和 12V 上工作,因此任何可以提供等于或大于 1 安培和 12V 的继电器都可以工作。

通常,市场上可用的继电器是 120V 和 12 安培直流。要记住的一件重要事情是交流电压和直流电压和电流将有两个单独的等级。由于我们的电磁阀将在 12V 下工作,我们只考虑直流的上限。

制作机器人

现在,让我们开始制作机器人。首先,您需要从水龙头到电磁阀的水管连接,从电磁阀到洒水器的连接。您还需要进行以下连接:

现在让我们开始编程。在这个机器人中,我们将接口一个土壤湿度传感器。该传感器的工作是确定土壤中的水量。通过确定这一点,我们可以了解花园是否需要水。这个土壤湿度传感器是一个模拟传感器,因此我们将使用 ADC 将模拟读数转换为树莓派可理解的数字值。所以让我们开始吧:

import time
import RPi.GPIO as GPIO
import Adafruit_ADS1x15
water_valve_pin = 23
moisture_percentage = 20
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(water_valve_pin, GPIO.OUT)
adc = Adafruit_ADS1x15.ADS1115()
channel = 0
GAIN = 1
while True:
 adc.start_adc(channel, gain=GAIN)
 moisture_value = adc.get_last_result()
 moisture_value= int(moisture_value/327)
 print moisture_value
 if moisture_value < moisture_percentage:
 GPIO.output(water_valve_pin, GPIO.HIGH)
 time.sleep(5)
 else:
 GPIO.output(water_valve_pin, GPIO.LOW)

在运行此代码之前,让我们先了解它实际上在做什么:

moisture_percentage = 20

moisture_percentage = 20是一个阈值百分比;如果土壤中的湿度水平低于 20%,那么您的花园就需要水。这是您的机器人将继续寻找的条件;一旦满足这个条件,就可以采取适当的行动。这个百分比也可以根据您花园的需要更改为3040或其他任何值:

moisture_value = int(moisture_value/327)

ADC 是一个 16 位设备——有 16 个二进制数字可以表示一个值。因此,该值可以在02¹⁵之间,换句话说,可以在032768之间。现在,很简单的数学,对于每个百分比的湿度,ADC 将给出以下读数:32768/100,或327.68。因此,要找出土壤中的湿度百分比,我们需要将 ADC 给出的实际值除以327.68

其余的代码非常简单,一旦您阅读它,您就不会很难理解。

使其更智能

祝贺您制作了您的第一个机器人!但您是否注意到了一个问题?我们制作的机器人一直在寻找湿度值,一旦注意到湿度值偏低,它就会突然泵水,并确保土壤的湿度始终高于 20%。然而,这是不必要的。一般来说,我们每天浇水一两次。如果我们浇水更多,那对植物可能不利。

因此,让我们继续使它稍微更智能化,并且只在特定时间土壤湿度低时给植物浇水。这一次,我们不需要对硬件进行任何更改;我们只需要微调代码。

让我们继续上传以下代码,然后看看到底发生了什么:

from time import sleep
from datetime import datetime
import RPi.GPIO as GPIO
import Adafruit_ADS1x15
water_valve_pin = 23
moisture_percentage = 20
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(water_valve_pin, GPIO.OUT)
adc = Adafruit_ADS1x15.ADS1115()
GAIN = 1
def check_moisture():
 adc.start_adc(0,gain= GAIN)
 moisture_value = adc.get_last_result()
 moisture_value = int(moisture_value/327)
 if moisture_value < moisture_level:
 GPIO.output(water_valve_pin, GPIO.HIGH)
 sleep(5)
 GPIO.output(water_valve_pin, GPIO.LOW)
 else:
 GPIO.output(water_valve_pin, GPIO.LOW)
while True:
 H = datetime.now().strftime('%H')
 M = datetime.now().strftime('%M')
 if H == ‘07’ and M <= ‘10’:
 check_moisture()
 if H == ‘17’ and M <= ‘01’:
 check_moisture()

这段代码可能对您来说有点陌生,但相信我,它就是这么简单。让我们一步一步地看看发生了什么:

from datetime import datetime

这行代码是从日期时间库中导入日期时间实例。这是 Python 中默认的一个库。我们只需要调用它。它的作用是在我们的代码中轻松确定时间。

def check_moisture():

有时我们必须一遍又一遍地做一些事情。这些代码集可以是几行重复的代码,也可以是多页的代码。因此,重写那些代码毫无意义。我们可以创建一个函数。在这个函数中,我们可以定义每次调用时会发生什么。在这行代码中,我们创建了一个名为check_moisture()的函数;现在,每当程序中调用这个函数时,将执行一系列活动。将要执行的一系列活动由用户定义。因此,每当我们写def时,就意味着我们正在定义一个函数;然后,我们写出需要定义的函数的名称。

完成后,然后我们在缩进中写的任何内容都将在调用函数时执行。请记住,每当我们调用或定义一个函数时,函数名称的末尾都有一个开放和关闭的()括号表示:

 moisture_value = adc.get_last_result()

adc.get_last_result()adc的一个函数。它的功能是简单地从之前定义的引脚(引脚号为0)获取结果,并将读数存储到变量moisture_value中。因此,在moisture_value之后将是 ADC 引脚号0的读数,或者换句话说,是湿度传感器的读数。

H = datetime.now().strftime('%H')

代码datetime.now()的一个实例和方法。这个函数的作用是更新时间。现在,datetime.now()已经更新了日期和时间的所有参数,包括小时、分钟、秒,甚至日期。我们可以选择是否要全部或者日期和时间的任何特定部分。目前,我们想要将小时的值放入变量H中,因此我们使用了.strftime('%H')方法。strftime代表时间的字符串格式。因此,它输出的任何值都是以字符串格式。('%H')表示它只会给我们小时的值。同样,我们也可以使用('%M')('%S)来获取分钟的时间。我们还可以使用以下语法获取日期、月份和年份的值:

  • 获取日期:('%d')

  • 获取月份:('%m')

  • 获取年份:('%Y')

if H == ‘07’ and M <= ‘10’:

在前面的条件中,我们正在检查时间是否为 7 点;此外,我们还在检查时间是否小于或等于 10 分钟。因此,只有当时间为 7 小时并且在 0 到 10 分钟之间时,此代码段才会运行if语句中的语句。

特别要注意的一点是,我们在两个条件之间使用了and,因此只有在两个语句都绝对为真时才会运行其中的代码。我们还可以在其中使用一些其他语句,比如or,在这种情况下,如果其中一个语句为真,它将运行代码。

如果我们在这个if语句中用or替换and,那么它将在每个小时的 0 到 10 分钟内运行代码,并且将在上午 7:00 到 7:59 之间的整个时间内连续运行代码:

check_moisture()

正如你可能记得的,之前我们定义了一个名为check_moisture()的函数。在定义该函数时,我们还定义了每次调用该函数时将发生的一系列活动。

现在是调用该函数的时候了。一旦程序到达代码的末尾,它将执行之前在函数中定义的一系列活动。

所以我们就是这样。现在,一旦你运行这段代码,它将等待程序中定义的时间。一旦达到特定的时间,它将检查湿度。如果湿度低于设定值,它将开始给植物浇水,直到湿度超过阈值为止。

真正智能化

了不起的工作!我们已经开始自己建造比我们更聪明的东西。但现在我们想要更进一步,让它比我们更聪明——这就是机器人存在的意义。不仅仅是做我们做的事情,而是以更好的方式做所有这些。

那么,我们能做些什么改进呢?在寒冷的冬天,我们不需要太多的水,但在夏天,我们需要比冬天喝的水多得多。植物也是一样的情况。

在冬天,它们需要的水量要少得多。此外,土壤中的水蒸发速度也较慢。因此,在这两种情况下,我们需要向花园供应不同数量的水。问题是,我们该如何做到呢?

首先,要知道外面是热还是冷,我们需要一个传感器。我们将使用一个名为 DHT11 的传感器。这是一个便宜但坚固的传感器,可以给我们提供温度和湿度的读数。最好的部分是,它的价格非常便宜,大约 2 美元。

它有四个引脚。但是,如果你认为它将适用于 I2C 协议,那么你就错了。它有自己的数据传输方法。拥有一个单一的协议来处理所有传感器是很好的,但通常你也会发现有各种传感器或设备使用不同或全新的协议。DHT11 就是这样的传感器。在这种情况下,我们可以选择要么理解整个通信方法,要么简单地从制造商那里获取库并随时使用。目前我们将选择后者。

现在让我们看看 DHT11 的引脚是什么样子的:

你可以看到这里只有一个信号引脚,它将完成所有数字通信。有两个电源引脚,其中一个引脚没有使用。也就是说,这个引脚没有明显的用途。它可能只是用于焊接或将来使用。这个传感器使用 5V 电源,只需要几毫安,因此我们可以通过树莓派来为其供电。现在,对于数据通信,我们将把信号引脚连接到 GPIO 引脚号4

在我们开始编写代码之前,让我们先安装 DHT11 和树莓派之间的通信库。我们之前已经在 ADS1115 的库中做过这个,但在这个库中有一些小技巧需要我们注意。所以让我们开始吧。

首先,我们需要确保你的树莓派操作系统是最新的。所以将树莓派连接到互联网,打开树莓派的命令提示符,输入以下命令:

sudo apt-get update

这个命令将自动更新你的树莓派的 raspbian 操作系统。然后继续输入这个命令:

sudo apt-get install build-essential python-dev python-openssl

在这个命令中,我们正在安装以下软件包:

  • build-essential

  • python-dev

  • python-openssl

你一定在想为什么我们要安装所有这些。好吧,长话短说,这些是我们即将安装的 DHT11 通信库的依赖项。如果这些软件包没有安装在树莓派上,我们将无法使用该库。

最后,我们必须安装库;这是一个通用库,其中还包括与 DHT11 传感器通信的功能。这应该足以满足我们的简单通信需求。以下是安装它的命令:

sudo python setup.py install

好了,我们准备好了。我们的系统已经准备好与 DHT11 进行通信。让我们首先看看我们到目前为止所做的是否按我们想要的方式工作。为了做到这一点,按照以下方式连接 DHT11;你可以将其他组件如电磁阀和土壤湿度传感器连接好。它们不应该干扰。现在在树莓派上上传以下代码:

from time import sleep
from datetime import datetime
import RPi.GPIO as GPIO
import Adafruit_DHT
sensor = 11
pin = 4
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
while True:
 humidity, temperature = Adafruit_DHT.read_retry(sensor, pin)
 print("Temperature: " +temperature+ "C")
 print("Humidity: " +humidity+ "%")
 time.sleep(2)

一旦你上传了这段代码,你将在屏幕上看到传感器的读数。这段代码只是简单地为你提供传感器的原始读数。这段代码非常简单,你会理解其中的一切,除了一些代码行,其中包括:

import Adafruit_DHT

在代码的这一行中,我们在代码中导入了Adafruit_DHT库。这是与 DHT11 传感器通信的相同库。

sensor = 11 

DHT 有不同的版本,如 DHT11、DHT22 等。我们需要告诉程序我们使用的是哪种传感器。因此,我们已经为变量传感器分配了一个值。稍后,你将看到我们将如何使用它:

pin = 4  

在这一行中,我们将值 4 赋给一个名为pin的变量。这个变量将用于告诉程序我们已经连接了 DHT11 的树莓派引脚。

humidity, temperature = Adafruit_DHT.read_retry(sensor, pin)

在这一行中,我们使用了Adafruit库的一个方法,名为Adafruit_DHT.read_retry()。现在,它的作用是读取 DHT 传感器,并将传感器的读数给变量humiditytemperature。需要注意的一点是,DHT11 每 2 秒更新一次读数。因此,你将在每 2 秒后收到更新的读数。

一旦这段代码完成,我们就可以确信传感器正在按我们想要的方式工作。最后,是时候将所有传感器整合在一起,制作一个完全智能的机器人了。由于电磁阀、湿度传感器和温度传感器已经连接好,我们所需要做的就是将代码上传到树莓派上,然后看魔法发生。

from time import sleep
from datetime import datetime
import RPi.GPIO as GPIO
import Adafruit_ADS1x15
import Adafruit_DHT
water_valve_pin = 23
sensor = 11
pin = 4
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(water_valve_pin, GPIO.OUT)
Channel =0
GAIN = 1
adc = Adafruit_ADS1x15.ADS1115()
def check_moisture(m):
 adc.start_adc(channel, gain=GAIN)
 moisture_value = adc.get_last_result()
 moisture_value = int(moisture_value/327)
 print moisture_value
 if moisture_value < m:
 GPIO.output(water_valve_pin, GPIO.HIGH)
 sleep(5)
 GPIO.output(water_valve_pin, GPIO.LOW)
 else:
 GPIO.output(water_valve_pin, GPIO.LOW)
while True:
 humidity, temperature = Adafruit_DHT.read_retry(sensor, pin)
 H = datetime.now().strftime(‘%H’)
 M = datetime.now().strftime(‘%M’)
 if H == ‘07’ and M <= ‘10’:
 if temperature < 15:
 check_moisture(20)
 elif temperature >= 15 and temperature < 28:
 check_moisture(30)
 elif temperature >= 28:
 check_moisture(40)
 if H == ‘17’ and M <= ‘10’:
 if temperature < 15:

 check_moisture(20)
 elif temperature >= 15 and temperature < 28:
 check_moisture(30)
 elif temperature >= 28:
 check_moisture(40)

代码很长,对吧?看起来是这样,但是一旦你逐行编写它,你肯定会明白,它可能比我们迄今为止编写的所有代码都长,但它一点也不复杂。你可能已经理解了大部分程序,但是让我解释一下我们在这里使用的一些新东西:

def check_moisture(m):
  adc.start_adc(channel, gain = GAIN)

moisture_value = adc.get_last_result()
moisture_value = int(moisture_value / 327)
print moisture_value

if moisture_value < m:
  GPIO.output(water_valve_pin, GPIO.HIGH)
  sleep(5)
  GPIO.output(water_valve_pin, GPIO.LOW)
else :
  GPIO.output(water_valve_pin, GPIO.LOW)

在这一行中,我们定义了一个名为check_moisture()的函数。以前,如果你还记得,当我们制作check_moisture函数时,我们基本上是在检查湿度值是否大于或小于 20%。如果我们需要检查 30%、40%和 50%的湿度怎么办?我们会为此制作一个单独的函数吗?

显然不是!我们所做的是向函数传递一个参数,参数基本上是放在函数括号内的变量。现在我们可以为这个变量分配值,例如check_moisture(30)-现在在执行该函数时m的值将为 30。然后,如果再次调用check_moisture(40),那么m的值将为 40。

现在,你可以看到我们在整个函数中比较m的值。

   if moisture_value < m:

if 语句将检查调用函数时分配的m的值。这使我们的工作变得非常简单。

让我们看看程序的其余部分在做什么:

            if temperature < 15:
                check_moisture(20)

每当达到所需的时间,它将继续检查温度。如果温度低于 15 度,它将调用函数check_moisture并将参数值设为 20。因此,如果湿度低于 20%,则会给花园浇水。

 elif temperature >= 15 and temperature < 28:
                check_moisture(30)

elifelse if语句在if语句之后使用。通俗地说,这意味着如果前面的if语句不成立,它将检查这个if语句。因此,在前一行中,它将检查温度是否在 15 到 28 摄氏度之间。如果是,它将检查土壤的湿度。在这一行中,函数的参数是 30。因此,它将检查湿度是否低于 30。如果是,它将给花园供水。

 elif temperature >= 28:
                check_moisture(40)

同样,在这行代码中,我们正在检查温度,如果温度等于或超过28摄氏度,那么它将把值40作为参数传递给函数check_moisture。因此,这次它将检查湿度是否达到或超过28

正如您所看到的,现在系统将检查环境温度,并根据此调节植物所需的水量。最好的部分是它是一致的,并将提供植物所需的正确水量。

本章中提到的数值仅为假设值。我强烈建议根据您所在地区和您花园中种植的植物来调整数值,以便系统发挥最佳效果。

总结

在本章中,我们涵盖了一些主题,如电磁阀集成和土壤湿度传感器,以构建一个可以自动给您的后院花园浇水的机器人。接下来,我们将介绍电机的基础知识。

第二十章:电机基础

好了!我们已经制作了一个照顾你花园的机器人,我希望它运行良好。现在是时候把事情提升到另一个水平了。

我们一直认为机器人就像 WALL-E 一样,四处移动并为我们做事。好吧,朋友,现在这个梦想并不遥远。事实上,在这一章中,我们将继续前进并制作一个。让我们看看如何做到。

我们将涵盖以下主题:

  • 基础知识

  • 让它滚动

  • 改变速度

基础知识

每当我们谈论从一个地方移动到另一个地方时,我们都会想到轮子,同样,每当我们想到移动机器人的车轮时,我们都会想到电机。存在各种不同类型的电机。因此,让我们首先看看最基本的电机类型,即称为刷式直流电机的电机。顾名思义,它是在直流电上工作的。你可能会发现这样的电机:

相信我,这些东西无处不在,从你为邻居买的圣诞礼物到最大的坏家伙机器,你都会发现这些电机隐藏在引擎盖下。这些电机之所以常见,是因为它们非常非常简单。如此简单,以至于只需要一块电池和两根导线就可以为它们供电。只需将正极连接到一个端子,负极连接到另一个端子,电机就会开始旋转。交换这些连接,旋转方向将改变。取两个电池并加倍电压,电机将旋转得更快。就是这么简单。

现在你可能会认为我们只需将这个电机连接到树莓派,然后就可以开始了。但不幸的是,情况并非如此。你可能还记得前几章提到的,树莓派只能提供大约 50 毫安,但电机的消耗可能要高得多。因此,为了运行一个电机,我们需要一个中间设备。

你脑海中首先想到的可能是使用继电器,为什么不呢?它们可以传输大量电流并且可以处理高电压。这应该是理想的选择。如果你这样想,你是对的,但只是在某种程度上,因为继电器只是一个我们可以用来打开或关闭电机的开关。我们将无法控制电机的速度或旋转方向。现在,你可能会认为这个问题并不新鲜,我们可以很容易地通过使用脉宽调制PWM)来解决,对吗?好吧,答案是否定的!因为这些继电器是机械设备,由于它们的机械性质,每秒开关的最大限制是有一些的。因此,它将无法应对 PWM 的频率。最后,我们仍然会面临改变电机的方向和速度的问题。那么现在我们该怎么办呢?

正如我经常说的,问题的美妙之处在于它总是有解决方案,而这里的解决方案被称为电机驱动器。电机驱动器主要是一组电子继电器——一种可以允许高电流但不是机械的开关。因此,我们可以每秒切换数百次。这些电子继电器要么由简单的晶体管制成,要么在高功率应用中,甚至可以使用 MOSFET 进行切换。我们可以简单地给这些电子开关提供 PWM,并在确保向电路传递足够电流的同时使电压调制。此外,正如我之前提到的,电机驱动器由一组这些电子继电器组成。它们排列的最常见和可行的方式称为全桥或 H 桥。在我进一步解释之前,让我们看看这到底是什么:

在全桥中,我们有四个与连接的电机相连的开关电路;根据需求,这些可以独立地打开或关闭。在关闭状态下,所有这些开关电路都处于断开状态,因此保持电机关闭。现在,每当我们想要启动电机时,我们将不得不打开两个开关,使电路完整,电机开始工作。让我们看看它会是什么样子:

在这里,我们打开了开关电路S2S3;这反过来完成了电路,让电流在电机中流动。现在,为了控制速度,这些相同的开关电路可以以非常高的频率以不同的占空比打开和关闭,以实现特定的平均电压。现在我们可以通过这两个开关电路改变电压来实现电机的特定速度,让我们看看如何改变电机的旋转方向:

在这个电路中,我们关闭了之前连接的S2S3,而是打开了S1S4,因此电机的极性被颠倒了。正如我们之前讨论的,每当直流刷电机的极性改变时,方向也随之改变。市场上有各种类型的电机驱动器。我们在这里理解的是称为刷式直流 H 桥电机驱动器;还有其他类型的电机驱动器用于控制其他类型的电机,但目前我们只会专注于刷式电机。在选择电机驱动器时,您应该非常仔细地检查电机驱动器的规格表。将提到的一些关键规格如下:

  • 电压等级:电机驱动器可以处理和调制的电压将有最小和最大限制。确保您的电机位于特定电压范围之间。

  • 电流评级:电机驱动器可以处理的绝对最大电流;超出这个范围将烧毁或损坏电机驱动器。这可能有点误导。让我们看看为什么。除了绝对最大值,可能会指定许多其他电流评级。这些可能是:

  • 重复最大电流:这是电机驱动器可以处理的最大电流,但不是持续的。给出这个评级是因为有时电机的负载可能会增加,可能会在短暂时刻需要更高的电流。电机驱动器将在重复的基础上提供足够的电流而不会损坏。但这种电流需求不应该是持续的。

  • 突发最大电流:这是电机驱动器能够处理的绝对最大电流;超过这个电流将损坏电机驱动器。直流电机在从静止状态启动时可能需要非常高的电流。因此,电机驱动器被设计为处理这些电流。但是这种电流的突发性不应该是重复的,否则会发生加热和随后的损坏。通常,制造商将突发最大电流称为最大电流。

  • 持续最大电流:这是真正的问题;持续最大电流是电机驱动器可以持续处理的最大电流。

  • 供电电压:这是电机驱动器的工作电压——必须将这个电压提供给电机驱动器进行内部工作。

  • 逻辑供电电压:这是提供给电机驱动器的控制信号,可以以 5V、3.3V 和 12V 等不同电压给出。因此,电机驱动器将规定它可以接受信号线上的最大逻辑电压。

现在,让我们看看我们得到了什么。在本书的过程中,我们将使用 L298N 电机驱动器模块,它目前是市场上最常见的电机驱动器模块之一。它有两个通道——您有两个 H 桥,因此可以将两个电机连接到它上。此外,该电机驱动器的规格也相当不错。以下是规格:

  • 电压等级:2.5V 至 46V

  • 重复最大电流:2.5 安培

  • 突发最大电流:3 安培

  • 连续最大电流:2 安培

  • 供电电压:4.5V 至 7V

  • 逻辑供电电压:4.5V 至 7V

一旦您拥有了物理电机驱动器,您将注意到以下引脚:

  • 电机 A:这是电机驱动器的第 1 通道。您可以将第一个电机连接到此端口。

  • 电机 B:这是电机驱动器的第 2 通道。您可以将第二个电机连接到此端口。如果您只有一个电机,可以简单地将此端口未连接。

  • GND:这是您将为电机连接的电源的接地。非常重要的是,您不仅要连接电源的接地,还要将树莓派的接地连接到此端口,以便树莓派和电机驱动器之间的电路完整。

  • VCC:这是电机驱动器的正极端口。这是您的电池或电源适配器的正极端子所在之处。

  • IN 1 和 IN 2:这是我们需要从微控制器提供给电机 A 的两个逻辑输入。每当 IN 1 接收到信号时,H 桥的一部分被激活——电机开始朝一个方向旋转。每当 IN 2 接收到信号时,H 桥的另一部分被激活,使电机朝相反方向旋转。

  • IN 3 和 IN 4:这是电机 B 的逻辑输入,其工作方式与 IN 1 和 IN 2 完全相同。

  • EN A 和 EN B:这些是两个通道的使能引脚。如果这些引脚不高,无论您在输入端口上发出什么信号,相应的通道都不会工作。您可能会注意到 EN 端口上有一个小电容。这被称为分流器。它的作用是使其连接的两个引脚之间接触。当存在于 EN 引脚上时,这意味着只要连接了这个分流器,它就会永久保持高电平。

开始运转

好的,这是很多理论,现在让我们通过树莓派启动其中一个电机。要做到这一点,继续连接电机和电机驱动器如下所示:

现在,一旦您完成了这一点,让我们上传代码并看看会发生什么:

import RPi.GPIO as GPIO
from time import sleep
GPIO.setmode(GPIO.BCM)

Motor1R = 20
Motor1L = 21

GPIO.setup(Motor1R,GPIO.OUT)
GPIO.setup(Motor1L,GPIO.OUT)

GPIO.output(Motor1R,GPIO.HIGH)
GPIO.output(Motor1L,GPIO.LOW)

sleep(5)

GPIO.output(Motor1R,GPIO.LOW)
GPIO.output(Motor1L,GPIO.HIGH)

sleep(5)

GPIO.cleanup()

现在,让我们稍微了解一下代码:

Motor1R = 20
Motor1L = 21

引脚编号20连接到电机驱动器的 IN 1。为了方便起见,我们已将电机 1 的右侧更改为Motor1R;实际上,电机可以以任何方向旋转,但我们只是为了方便和理解而这样写。同样,我们也对Motor1L做了同样的处理。这连接到 IN 2,因此这将导致电机以另一个方向旋转:

GPIO.output(Motor1R,GPIO.HIGH)
GPIO.output(Motor1L,GPIO.LOW)

在这里,我们使Motor1R或引脚编号20高,这意味着输入电机驱动器正在接收的是:

电机 引脚 输入 状态
Motor 1R 树莓派的引脚编号 20 IN 1
Motor 1L 树莓派的引脚编号 21 IN 2

现在,延迟 5 秒后,将运行以下代码,该代码将更改下表中所示的引脚状态:

GPIO.output(Motor1R,GPIO.LOW)
GPIO.output(Motor1L,GPIO.HIGH)
电机 引脚 输入 状态
Motor 1R 树莓派的引脚编号 20 IN 1
Motor 1L 树莓派的引脚编号 21 IN 2

现在,让我们看看一旦我们运行它会发生什么。电机首先会以一个方向旋转,然后会以另一个方向旋转。代码非常简单直接,我认为没有必要解释。我们在这里所做的就是简单地打开或关闭连接到电机驱动器的两个 GPIO 中的一个。一旦激活电机驱动器的 IN 1 输入,H 桥的一部分就会打开,导致电机朝一个方向旋转。每当电机驱动器的 IN 2 输入高时,那么 H 桥的另一部分就会打开,导致电机驱动器输出端的极性发生变化,因此电机朝另一个方向旋转。

改变速度

现在我们已经了解了如何使用电机驱动器改变电机的方向,是时候更进一步,使用电机驱动器控制电机的速度了。要做到这一点,我们实际上不需要做太多。电机驱动器是为了理解 PWM 信号而构建的。一旦向电机驱动器提供 PWM 信号,那么电机驱动器将调整电机的输出电压,从而改变电机驱动器的速度。PWM 必须在电机 A 的相同输入端口 IN 1 和 IN 2 上提供,并在电机 B 的输入端口 IN 3 和 IN 4 上提供。很明显,提供 PWM 的引脚将决定电机的移动方向,而 PWM 的占空比将决定电机旋转的速度。

现在我们已经了解了电机驱动器中的速度控制是如何工作的。现在是时候自己动手了。为此,我们不需要对连接进行任何更改;我们需要做的就是上传以下代码:

import RPi.GPIO as GPIO
from time
import sleep
GPIO.setmode(GPIO.BCM)

Motor1R = 20
Motor1L = 21

GPIO.setup(Motor1R, GPIO.OUT)
GPIO.setup(Motor1L, GPIO.OUT)

pwm = GPIO.PWM(Motor1R, 100)
pwm.start(0)

try:
while True:
  GPIO.output(Motor1L, GPIO.LOW)
for i in range(0, 101):
  pwm.ChangeDutyCycle(i)
sleep(0.1)

except KeyboardInterrupt:

  pwm.stop()
GPIO.cleanup()

你运行这段代码后发生了什么?我肯定电机开始缓慢转动,然后加速,最终达到最高速度,最终停止——这正是我们想要的。如果你记得,这段代码看起来非常熟悉。还记得在第一章中改变 LED 的亮度吗?它几乎是一样的;虽然有一些区别,所以让我们看看它们是什么:

pwm = GPIO.PWM(Motor1R, 100)

在这一行中,我们只是定义了我们需要在上面提供 PWM 的引脚——就是Motor1R,对应的是引脚号20。此外,我们定义了 PWM 的频率为100赫兹或每秒 100 次:

pwm.start(0)

如果你记得,前几章的先前命令pwm.start()主要用于定义信号的占空比。在这里,我们将占空比设置为0,即引脚将关闭:

GPIO.output(Motor1L,GPIO.LOW)

由于我们只在一个特定方向上运行电机,即1R,因此 H 桥的另一半应该关闭。通过上面的代码行,通过将1L置为 LOW 来实现。如果我们不这样做,那么引脚21可能处于任意状态,因此它可能是打开或关闭的。这可能会与电机移动的方向发生冲突,硬件将无法正常工作:

 for i in range(0,101):

现在,真正的问题来了;这一行,for i in range(0,101):将一直运行其中包含的程序,直到i的值在0101之间。它还会在每次循环运行时增加i的值。在这里,每次值都会增加一:

            pwm.ChangeDutyCycle(i)

现在,这是一个稍微新的命令。以前,我们使用了pwm.start(0)来为 PWM 分配占空比。由于我们已经为 PWM 分配了占空比值,要更改它,我们将使用先前提到的命令。占空比将与i的值相同。

因此,每次代码通过for循环时,值或占空比将增加一。非常简单,不是吗?

如果你做对了,机器人学中的一切都很容易。关键是将问题分解成小块并逐个解决;相信我,一旦你做到了,没有什么会让你觉得困难。

总结

在本章中,我们研究了电机的各个方面。接下来,通过使用所有的基础知识,我们将学习蓝牙与移动设备的交互,并构建一个蓝牙控制的机器人汽车。

第二十一章:蓝牙控制的机器人车

我们已经走了很长的路;现在是时候继续前进,做出更好的东西。世界正在为自动驾驶汽车的诞生而疯狂,这将成为新的常态。这些车辆中有很多技术。多个传感器、GPS 和遥测都实时计算,以确保车辆在正确的路线上安全行驶,因此制作一个机器人车辆被证明是学习机器人技术和未来技术的理想方式。在这本书中,我们将尝试制造不仅与现有技术一样好,而且在某些方面甚至更好的技术。所以,让我们继续,一步一步地制作这辆自动驾驶车辆。

本章将涵盖以下主题:

  • 车辆的基础知识

  • 准备车辆

  • 通过蓝牙控制车辆

车辆的基础知识

你一定在想:我们可能还能从这辆车上学到什么呢?这可能是真的,但在开始这一章之前,我们必须确保理解其中的一些内容。所以,让我们开始吧。

首先是我们将使用的底盘:这是一个四轮驱动底盘,所有四个车轮都由专用电机独立控制。因此,我们可以根据需要改变每个车轮的速度。我们选择了四轮驱动传动系统,因为它不容易在地毯和不平整的表面上被卡住。如果你愿意,你也可以选择两轮驱动传动系统,因为这不会有太大的区别。

现在,一旦你组装好底盘,你可能会发现它没有转向机构。这是否意味着车只能直行?显然不是。在制作小型车辆时,有许多方法可以改变车辆的方向。最好的方法被称为差速转向。

在传统汽车中,有一个发动机,这个发动机驱动车轮;因此原则上所有车轮以相同的速度转动。现在当我们直行时这很好用,但每当车要转弯时就会出现一个新问题。参考以下图表:

你会看到内侧的车轮直径较小,外侧的车轮直径较大。你可能还记得小学的一个事实:直径越大,周长越大,反之亦然。因此,内侧的车轮在同一时间内将行驶较短的距离,或者简单地说,内侧的车轮会转得更慢,外侧的车轮会转得更快。

这个问题导致了汽车差速器的发现,它是汽车轴的中心有一个圆形的凸起。它的作用是根据转弯半径改变车轮的旋转速度。天才,不是吗?现在,你可能会想:这都没错,但你为什么要告诉我这些?因为我们将做相反的操作来转动机器人。如果我们改变转向圈内外边缘电机的速度,那么车辆将试图向内转向,同样,如果我们对另一端这样做,它将试图向另一个方向转向。在制作轮式机器人时,这种策略并不新鲜。转向机构很复杂,在小型机器人上实现它们只是一个挑战。因此,这是一个更简单和容易的方法来转动你的车辆。

这种方式不仅简单而且非常高效,需要的零部件也很少。车辆的转弯半径也更小。事实上,如果我们以相同速度将车轮的相对侧向相反方向旋转,那么车辆将完全围绕自己的轴旋转,使转弯半径完全为零。这种配置称为履带转向驱动。对于室内使用的轮式机器人来说,这是一个杀手功能。

要了解更多,请阅读这里:groups.csail.mit.edu/drl/courses/cs54-2001s/skidsteer.html

准备车辆

现在是时候继续让机器人车辆成为现实了。所以让我们打开车辆底盘并将每个零件螺丝拧在一起。组装手册通常随套件一起提供,所以你很快就能完成它。

完成组装套件后,继续将每个电机的电线分开。这将是使车辆准备就绪的非常重要的部分。因此,一旦车辆上所有的电线都出来了,拿一个电池,给每个车轮供电。注意连接的极性,车轮是向前旋转的。你需要做的就是拿一个永久性的记号笔或者指甲油,标记电线,当电机向前旋转时,连接到正极的电线。由于所有这些电机完全依赖于极性来确定方向,这一步是关键,以确保无论何时给它们供电,它们总是以相同的方向旋转。相信我,这将为你节省很多麻烦。

现在,一旦这一切都完成了,按照以下图示将电线连接到电机驱动器(红色标记的电线是你之前标记的电线):

完美!现在一切似乎都准备好了,除了电机驱动器与电源和树莓派的连接。所以让我们看看我们将如何做到:

好了!是时候进行真正的交易了!所以我们要确保的第一件事是所有的连接都按照我们计划的方式工作。为此,我们将从一个简单的代码开始,它将简单地打开所有电机并向前旋转。所以这是代码:

import RPi.GPIO as GPIO
import time GPIO.setmode(GPIO.BCM) Motor1a = 20 Motor1b = 21 Motor2a = 2 Motor2b = 3  GPIO.setup(Motor1a,GPIO.OUT) GPIO.setup(Motor1b,GPIO.OUT) GPIO.setup(Motor2a,GPIO.OUT) GPIO.setup(Motor2b,GPIO.OUT) GPIO.output(Motor1a,1) GPIO.output(Motor1b,0) GPIO.output(Motor2a,1) GPIO.output(Motor2b,0)
time.sleep(10)
GPIO.cleanup() 

程序不可能比这更简单了;我们在这里所做的就是向电机驱动器发出命令,让电机单向旋转。可能会有一组电机会以相反方向旋转的情况,这种情况下你应该改变电机驱动器上的连接极性。这应该解决问题。有些人可能会认为我们也可以对代码进行更改来解决这个问题,但根据我的经验,从那里开始就会变得复杂,并且如果你选择其他路径,会给你带来麻烦。

好了,一切都准备就绪,一切都运行良好。继续尝试一些其他输出排列组合,看看车子会发生什么。别担心,无论你做什么,除非它从屋顶上跑下来,否则你都不会损坏车子!

通过蓝牙控制车辆

玩了一些尝试这些组合的乐趣吗?现在是时候我们继续前进,看看还有什么其他可能性了。我们都玩过遥控车,我相信每个人都会对那些敏捷的小玩具感到开心。我们将做类似的事情,但以一种更复杂的方式。

我们都知道蓝牙:这是与附近设备通信的最佳方式之一。蓝牙通信是一种中等数据速率、低功耗的通信方法。这在移动设备中几乎无处不在,因此是一个理想的开始方式。在本章中,我们将通过蓝牙使用手机来控制车辆。现在让我们看看我们如何做到这一点。

我们想要做的第一件事是将智能手机与机器人车配对,为此我们需要在树莓派上打开终端并执行以下步骤:

  1. 在命令行中输入~ $ bluetoothctl;这是一个蓝牙代理,允许两个蓝牙设备进行通信。没有蓝牙代理,这两个设备首先就无法进行通信。

  2. [Bluetooth] # power on命令简单地启动了树莓上的蓝牙。

  3. [Bluetooth] # agent on命令启动代理,然后可以为我们启动连接。

  4. [Bluetooth] # discoverable on命令使树莓派的蓝牙可发现。蓝牙可能已经打开,但我们必须使其可发现,以确保其他设备可以找到它并连接到它。

  5. [Bluetooth] # pairable on命令使设备可配对。如果蓝牙已打开,这并不意味着您的设备将能够连接,因此我们需要使其可配对,这个命令正是这样做的。

  6. [Bluetooth] # scan on命令开始扫描附近的蓝牙设备。这个命令的输出将是一些 MAC 地址以及蓝牙名称。MAC 地址是设备的物理地址;这是一个唯一的地址,因此它永远不会对两个设备相同。

  7. [Bluetooth] # pair 94:65:2D:94:9B:D3命令帮助您与您想要的设备配对。您只需输入带有 MAC 地址的命令。

只是为了明确,这是您的屏幕应该看起来的样子:

完成了这个过程后,您应该能够将树莓派连接到您的移动设备。现在您已经连接,是时候继续编写代码了,通过这些代码,我们将能够仅使用移动设备来控制蓝牙汽车。所以这是代码。继续,看一看,然后我们将进行解释:

import bluetooth
import time
import RPi.GPIO as GPIO
Motor1a = 20
Motor1b = 21
Motor2a = 2
Motor2b = 3
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(Motor1a,GPIO.OUT)
GPIO.setup(Motor1b,GPIO.OUT)
GPIO.setup(Motor2a,GPIO.OUT)
GPIO.setup(Motor2b,GPIO.OUT)
server_socket=bluetooth.BluetoothSocket( bluetooth.RFCOMM )
port = 1
server_socket.bind(("",port))
server_socket.listen(1)
client_socket,address = server_socket.accept()
print ("Accepted connection from "+str(address))
def stop_car():
  GPIO.output(Motor1a,0)
  GPIO.output(Motor1b,0)
  GPIO.output(Motor2a,0)
  GPIO.output(Motor2b,0)

while True:
  data = client_socket.recv(1024)
  if (data == "B" or data== "b"):
    GPIO.output(Motor1a,1)
    GPIO.output(Motor1b,0)
    GPIO.output(Motor2a,1)
    GPIO.output(Motor2b,0)
    time.sleep(1)
    stop_car()

  if (data == "F" or data == "f"):
    GPIO.output(Motor1a,0)
    GPIO.output(Motor1b,1)
    GPIO.output(Motor2a,0)
    GPIO.output(Motor2b,1)
    time.sleep(1)
    stop_car()

  if (data == "R" or data == "r"):
    GPIO.output(Motor1a,0)
    GPIO.output(Motor1b,1)
    GPIO.output(Motor2a,1)
    GPIO.output(Motor2b,0)
    time.sleep(1)
    stop_car()

  if (data == "L" or data == "l"):
    GPIO.output(Motor1a,1)
    GPIO.output(Motor1b,0)
    GPIO.output(Motor2a,0)
    GPIO.output(Motor2b,1)
    time.sleep(1)
    stop_car()

  if (data == "Q" or data =="q"):
    stop_car()

  if (data =='Z' or data == "z"):
    client_socket.close()
    server_socket.close() 

现在让我们看看这段代码实际在做什么:

import bluetooth

在这个程序中,我们将使用蓝牙的一些通用功能,因此我们调用bluetooth库,以便能够调用这些方法:

server_socket=bluetooth.BluetoothSocket( bluetooth.RFCOMM )

现在,每当我们连接两个蓝牙设备时,我们有各种通信方法;其中最简单的是无线电频率通信,这里称为RFCOMM。现在,在这一行中,我们使用bluetooth库的BluetoothSocket方法来定义我们在程序中使用的通信协议,现在你已经知道是RFCOMM。我们进一步将这些数据存储在一个名为server_socket的变量中,这样我们就不必一遍又一遍地重复这个步骤。而是,每当我们需要这些数据时,它将已经存储在名为server_socket的变量中:

port = 1

现在,蓝牙有多个端口;这是一个非常有用的概念,通过一个单一的蓝牙连接,我们可以将各种数据流传输到各种设备和程序。这避免了数据的冲突,并确保数据安全地传输到确切的接收者。我们现在使用的程序非常简单,我们不需要多个端口进行数据通信。因此,我们可以使用任何160个可用端口进行通信。在程序的这一部分,您可以写任何端口,您的程序将正常运行:

server_socket.bind(("",port))

现在,每当我们连接两个设备时,我们需要确保它们在整个通信过程中保持连接。因此,在这里我们写下这个命令:server_socket.bind。这将确保您的蓝牙连接在整个通信过程中保持连接。

正如您所看到的,参数中的第一个参数是空的。在这里,我们通常写下它必须绑定的 MAC 地址。然而,由于我们将其设置为空,它将自动绑定到我们已经配对的 MAC 地址。我们的第二个参数是它必须连接的端口。正如我们所知,port变量的值被设置为1。因此,它将自动连接到端口号1

server_socket.listen(1)

这是一条非常有趣的线。正如我们所知,我们可能不是唯一一个尝试连接到树莓的蓝牙设备的人,因此当树莓接收到另一个连接请求时,树莓应该怎么做呢?

在这一行中,我们只是在定义:我们正在调用一个名为listen(1)的方法。在这个函数中,我们已经将参数的值定义为1。这意味着它只会连接到一个设备。任何其他尝试连接的设备都无法通过。如果我们将这个参数改为2,那么它将连接到两个设备,但它会留在队列中,因此被称为队列连接

client_socket,address = server_socket.accept()

现在大部分连接的事情都已经完成,我们还需要知道我们是否连接到了正确的地址。server_socket.accept()方法的作用是返回套接字号和它正在服务的地址。因此,我们将其存储在两个名为client_socketaddress的变量中。然而,正如我们所知,套接字将仅保持为1,因此我们将不会再使用它:

print ("Accepted connection from "+str(address))

在这一行中,我们只是告诉用户连接已成功建立,通过使用str(address)函数,我们打印连接到的地址的值。这样我们可以确保连接已经建立到了正确的设备。

data = client_socket.recv(1024)

在这一行中,我们正在从客户端接收数据;同时,我们正在定义数据的长度。因此,在方法client_socket.recv(1024)中,我们在参数中传递了一个参数1024,这基本上表示数据包的最大长度为1024字节。一旦接收到数据,它就会传递给变量data供进一步使用。

在此之后,程序的其余部分非常简单。我们只需要比较移动设备接收到的值,并让汽车做我们想做的事情。在这里,我们让汽车向四个方向行驶,即前进、后退、右转和左转。您也可以根据自己的需求添加特定条件:

 client_socket.close()

在这一行中,我们正在关闭客户端套接字的连接,以便断开客户端并终止数据传输:

 server_socket.close()

在前一行中,我们正在关闭服务器套接字的连接,以便断开服务器连接。

总结

本章教会了我们如何使用蓝牙接口通过数据抓取和共享来自动化和控制汽车。接下来,我们将开发我们迄今为止所学到的内容,以便为避障和路径规划接口红外传感器。

第二十二章:障碍物避让的传感器接口

要制作一个能自行驾驶的机器人车,我们首先需要了解人类如何驾驶车辆。当我们开车时,我们不断分析空间和与其他物体的距离。然后,我们决定是否可以通过。这在我们的大脑-眼睛协调中不断发生。同样,机器人也需要做同样的事情。

在我们之前的章节中,你学到了我们可以使用传感器找到我们周围物体的接近程度。这些传感器可以告诉我们物体有多远,基于此,我们可以做出决定。我们之前使用超声波传感器主要是因为它非常便宜。然而,正如你记得的,附加超声波传感器并运行其代码稍微麻烦。现在是时候我们使用一个更简单的传感器并将其连接到汽车上了。

本章将涵盖以下主题:

  • 红外近距离传感器

  • 自主紧急制动

  • 赋予它自动转向能力

  • 使其完全自主

红外近距离传感器

以下照片描述了红外近距离传感器:

它由两个主要部分组成-传感器和发射器。发射器发射红外波;这些红外(IR)波然后击中物体并返回到传感器,如下图所示。

现在,正如你在前面的图表中所看到的,发射的红外波从与传感器不同距离的表面反弹回来,然后它们以一定角度接近传感器。现在,因为发射器和传感器之间的距离在任何时间点都是固定的,所以对应于反射的红外波的角度将与其反弹之前所走过的距离成比例。红外近距离传感器中有超精密传感器,能够感知红外波接近它的角度。通过这个角度,它给用户一个相应的距离值。这种找到距离的方法被称为三角测量,它在工业中被广泛使用。我们需要记住的另一件事是,正如我们在前面的章节中提到的,我们都被红外辐射所包围;任何绝对零度以上的物体都会发射相应的波。此外,我们周围的阳光也有大量的红外辐射。因此,这些传感器具有内置电路来补偿它;然而,它只能做到这么多。这就是为什么在处理直射阳光时,这个解决方案可能会有些麻烦。

现在,理论够了,让我们看看汽车实际上是如何工作的。我们在这个例子中使用的 IR 近距离传感器是夏普的模拟传感器,部件代码为 GP2D12。它的有效感应范围为 1000-800 毫米。范围还取决于所询问对象表面的反射性。物体越暗,范围越短。这个传感器有三个引脚。正如你可能已经猜到的,一个是 VCC,另一个是地,最后一个是信号。这是一个模拟传感器;因此,距离读数将基于电压给出。通常,大多数模拟传感器都会得到一个图表,其中会描述各种感应范围的各种电压。输出基本上取决于传感器的内部硬件和其结构,因此可能大不相同。下面是我们的传感器及其输出的图表:

好吧,到目前为止一切都很好。正如我们所知,树莓派不接受模拟输入;因此,我们将继续使用我们之前使用过的 ADC。我们将使用之前使用过的相同 ADC。

自主紧急制动

有一种新技术,新车配备了这种技术。它被称为自动紧急制动;无论我们在驾驶时有多认真,我们都会分心,比如 Facebook 或 WhatsApp 的通知,这些会诱使我们从道路上的屏幕上看向手机。这可能是道路事故的主要原因;因此,汽车制造商正在使用自动制动技术。这通常依赖于远程和近程雷达,它检测车辆周围其他物体的接近,在即将发生碰撞的情况下,自动将车辆刹车,防止它们与其他车辆或行人相撞。这是一个非常酷的技术,但有趣的是,我们今天将亲手制作它。

为了实现这一点,我们将使用红外接近传感器来感知周围物体的接近。现在,继续,拿一张双面胶带,把红外距离传感器粘在车子的前面。一旦完成这一步,按照这里所示的连接电路。

好了,我们已经准备好编写代码了。以下是代码,只需将其复制到你的树莓派上:

import RPi.GPIO as GPIO import time GPIO.setmode(GPIO.BCM)  import Adafruit_ADS1x15 adc0 = Adafruit_ADS1x15.ADS1115()   GAIN = 1  adc0.start_adc(0, gain=GAIN)   Motor1a = 20 Motor1b = 21 Motor2b = 23
Motor2a = 24  GPIO.setup(Motor1a,GPIO.OUT) GPIO.setup(Motor1b,GPIO.OUT) GPIO.setup(Motor2a,GPIO.OUT) GPIO.setup(Motor2b,GPIO.OUT)  def forward(): GPIO.output(Motor1a,0) GPIO.output(Motor1b,1) GPIO.output(Motor2a,0) GPIO.output(Motor2b,1)    def stop():  GPIO.output(Motor1a,0) GPIO.output(Motor1b,0) GPIO.output(Motor2a,0) GPIO.output(Motor2b,0) while True:  F_value = adc0.get_last_result()  F = (1.0  / (F_value /  13.15)) -  0.35  forward()  min_dist = 20  if F < min_dist:  stop()  

现在,让我们看看这段代码实际上发生了什么。一切都非常基础;红外线接近传感器感知到其前方物体的接近,并以模拟信号的形式给出相应的距离值。然后这些信号被 ADC 获取,并转换为数字值。这些数字值最终通过 I2C 协议传输到树莓派上。

到目前为止,一切都很好。但你一定想知道这行代码是做什么的?

 F = (1.0  / (F_value /  13.15)) -  0.35

这里我们并没有做太多事情,我们只是获取 ADC 给出的数字值,然后使用这个公式,将数字值转换为以厘米为单位的可理解的距离值。这个计算是由制造商提供的,我们不需要深究这个。大多数传感器都提供了这些计算。然而,如果你想了解我们为什么使用这个公式,我建议你查看传感器的数据表。数据表可以在以下链接上轻松找到:engineering.purdue.edu/ME588/SpecSheets/sharp_gp2d12.pdf

接下来,代码的主要部分如下:

min_dist = 20 If F < min_dist:
 stop()

这也很简单。我们输入了一个距离值,在这个程序中,我们将其设置为20。所以,每当F的值(红外接近传感器获取的距离)小于20时,就会调用stop()函数。stop函数只是让车子停下来,防止它与任何东西相撞。

让我们上传代码,看看它是否真的有效!确保你在室内测试这辆车;否则,如果没有障碍物,你将很难停下这辆车。玩得开心!

给车子自动转向的能力

希望你对这个小东西玩得开心。传感器的应用是如此简单,但它可以产生如此大的影响。既然你已经学会了基础知识,现在是时候向前迈进,给车子一些更多的能力了。

在之前的代码中,我们只是让机器人停在障碍物前面,为什么我们不让它绕过车子呢?这将非常简单又非常有趣。我们只需要调整stop()函数,使其能够转向。显然,我们还将把函数的名称从stop()改为turn(),只是为了清晰起见。要记住的一件事是,你不需要重写代码;我们只需要做一些微小的调整。所以,让我们看看代码,然后我会告诉你到底发生了什么变化以及为什么:

import RPi.GPIO as GPIO import time GPIO.setmode(GPIO.BCM)  import Adafruit_ADS1x15 adc0 = Adafruit_ADS1x15.ADS1115()   GAIN = 1  adc0.start_adc(0, gain=GAIN)   Motor1a = 20 Motor1b = 21 Motor2a = 23 Motor2b = 24  GPIO.setup(Motor1a,GPIO.OUT) GPIO.setup(Motor1b,GPIO.OUT) GPIO.setup(Motor2a,GPIO.OUT) GPIO.setup(Motor2b,GPIO.OUT)  def forward(): GPIO.output(Motor1a,0) GPIO.output(Motor1b,1) GPIO.output(Motor2a,0) GPIO.output(Motor2b,1)   def turn():
 GPIO.output(Motor1a,0) GPIO.output(Motor1b,1) GPIO.output(Motor2a,1) GPIO.output(Motor2b,0) )  while True:
   forward() F_value = adc0.get_last_result()  F = (1.0  / (F_value /  13.15)) -  0.35
     min_dist = 20

 while F < min_dist: turn()  

你可能已经注意到,除了以下内容,其他都基本保持不变:

def turn():
 GPIO.output(Motor1a,0) GPIO.output(Motor1b,1) GPIO.output(Motor2a,1) GPIO.output(Motor2b,0)

这部分代码定义了“转向()”函数,在这个函数中,车辆的对侧车轮会以相反的方向旋转;因此,使车辆绕着自己的轴转动:

 min_dist = 20 while F < min_dist: turn()

现在这是程序的主要部分;在这部分中,我们正在定义汽车在遇到任何障碍物时会做什么。在我们之前的程序中,我们主要是告诉机器人一旦遇到障碍物就停下来;然而,现在我们正在将“停止”函数与“转向”函数链接起来,这两个函数我们之前在程序中已经定义过了。

我们只是放入了一个条件,如下所示:

min_dist = 20 If F < min_dist:
 turn()

然后,它会转动一小段时间,因为微控制器会解析代码并执行它,然后跳出条件。为了做到这一点,我们的树莓派可能只需要几微秒。所以,我们甚至可能看不到发生了什么。因此,在我们的程序中,我们使用了一个while循环。这基本上保持循环运行,直到条件满足为止。我们的条件是while F < min_dist:,所以只要机器人在前面检测到物体,它就会继续执行其中的函数,而在我们的情况下,就是“转向()”函数。简而言之,直到它没有转到足够的程度来避开障碍物为止,车辆将继续转向,然后一旦循环执行完毕,它将再次跳回到主程序并继续直行。

简单吧?这就是编程的美妙之处!

使其完全自主

现在,你一定已经了解了使用简单的接近传感器进行自动驾驶的基础知识。现在是我们使其完全自主的时候了。要使其完全自主,我们必须了解并映射我们的环境,而不仅仅是在车辆遇到障碍物时转向。我们基本上需要将整个活动分为以下两个基本部分:

  • 扫描环境

  • 决定如何处理感知到的数据

现在,让我们先编写代码,然后看看我们需要做什么:

import RPi.GPIO as GPIO import time GPIO.setmode(GPIO.BCM) import Adafruit_ADS1x15
adc0 = Adafruit_ADS1x15.ADS1115() GAIN = 1 adc0.start_adc(0, gain=GAIN) Motor1a = 20 Motor1b = 21 Motor2a = 23 Motor2b = 24 GPIO.setup(Motor1a,GPIO.OUT) GPIO.setup(Motor1b,GPIO.OUT) GPIO.setup(Motor2a,GPIO.OUT) GPIO.setup(Motor2b,GPIO.OUT)  def forward(): GPIO.output(Motor1a,0) GPIO.output(Motor1b,1) GPIO.output(Motor2a,0) GPIO.output(Motor2b,1)  def right(): GPIO.output(Motor1a,0) GPIO.output(Motor1b,1) GPIO.output(Motor2a,1) GPIO.output(Motor2b,0)  def left(): GPIO.output(Motor1a,1) GPIO.output(Motor1b,0) GPIO.output(Motor2a,0) GPIO.output(Motor2b,1)  def stop(): GPIO.output(Motor1a,0) GPIO.output(Motor1b,0) GPIO.output(Motor2a,0) GPIO.output(Motor2b,0)  while True:  forward()  F_value = adc0.get_last_result() F = (1.0  / (F_value /  13.15)) -  0.35  min_dist = 20 if F< min_dist: stop() right() time.sleep(1) F_value = adc0.get_last_result()  F = (1.0  / (F_value /  13.15)) -  0.35  R = F left() time.sleep(2)  F_value = adc0.get_last_result()   F = (1.0  / (F_value /  13.15)) -  0.3  L = F if L < R: right()
        time.sleep(2) else: forward()  

现在大部分程序就像我们之前的所有程序一样;在这个程序中,我们定义了以下函数:

  • “前进()”

  • “右()”

  • “左()”

  • “停止()”

关于定义函数,我没有太多需要告诉你的,所以让我们继续前进,看看我们还有什么。

主要的操作是在我们的无限循环while True:中进行的。让我们看看到底发生了什么:

while True:

 forward() F_value = adc0.get_last_result() F = (1.0  / (F_value /  13.15)) -  0.35

 min_dist = 20 if F< min_dist: stop()

让我们看看这部分代码在做什么:

  • 一旦我们的程序进入无限循环,首先执行的是“前进()”函数;也就是说,一旦无限循环执行,车辆就会开始向前行驶。

  • 此后,F_value = adc.get_last_result()正在从 ADC 中获取读数并将其存储在一个名为F_value的变量中

  • F = (1.0/(F-value/13.15))-0.35正在计算可理解的度量距离值

  • min_dist = 20,我们只是定义了稍后将使用的最小距离

一旦这部分代码完成,那么if语句将检查是否F < min_dist:。如果是这样,那么if语句下的代码将开始执行。这部分代码的第一行将是“停止()”函数。所以每当车辆在前面遇到障碍物时,它将首先停下来。

现在,正如我所提到的,我们代码的第一部分是了解环境,所以让我们继续看看我们是如何做到的:

right()
 time.sleep(1) F_value = adc0.get_last_result()  F = (1.0  / (F_value /  13.15)) -  0.35
 R = F left() time.sleep(2)  F_value = adc0.get_last_result()
  F = (1.0  / (F_value /  13.15)) -  0.35
 L = F 

车辆停下后,它将立即向右转。正如你所看到的,代码的下一行是time.sleep(1),所以在另外的1秒钟内,车辆将继续向右转。我们随机选择了1秒的时间,你可以稍后调整它。

一旦它向右转,它将再次从接近传感器中获取读数,并使用这段代码R=F,我们将这个值存储在一个名为R的变量中。

在这样做之后,车辆将转向另一侧,也就是向左侧,使用left()函数,并且它将持续向左转动2秒,因为我们有time.sleep(2)。这将使车辆转向障碍物的左侧。一旦它向左转,它将再次接收接近传感器的值,并使用代码L = F将该值存储在变量L中。

所以,我们所做的实质上是扫描我们周围的区域。在中心,有一个障碍物。它将首先向右转,并获取右侧的距离值;然后,我们将向左转并获取左侧的距离值。因此,我们基本上知道了障碍物周围的环境。

现在我们来到了必须做出决定的部分,即我们必须向前走的方向。让我们看看我们将如何做到:

 if L < R: right()
        time.sleep(2) else: forward()

使用if语句,我们通过这段代码if L < R:比较障碍物左右侧的接近传感器的值。如果L小于R,那么车辆将向右转动2秒。如果条件不成立,那么else:语句将生效,车辆将前进。

现在,如果我们从更大的角度看代码,以下事情正在发生:

  • 车辆会一直前进,直到遇到障碍物

  • 遇到障碍时,机器人会停下来

  • 它将首先向右转,并测量其前方物体的距离

  • 然后,它将向左转,并测量其前方物体的距离

  • 之后,它将比较左右两侧的距离,并选择它需要前进的方向

  • 如果它需要向右转,它将向右转,然后前进

  • 如果它需要向左转,那么它已经处于左转方向,所以它只需要直走

让我们上传代码,看看事情是否按计划进行。请记住,尽管每个环境都不同,每辆车也不同,所以你可能需要调整代码以使其顺利运行。

现在我给你留下一个问题。如果在两种情况下传感器的读数都是无穷大或者它能给出的最大可能值,那么机器人会怎么做?

继续,进行一些头脑风暴,看看我们能做些什么来解决这个问题!

总结

在本章中,利用你迄今为止学到的所有基础知识,以及引入红外接近传感器,我们能够更进一步地发展我们的机器人车,以便检测障碍物并相应地改变方向。在下一章中,我们将学习如何制作我们自己的区域扫描仪——到时见!

第二十三章:现在,假设在 0 度的位置,电位器的输出电压为 4.8V;当我们将它旋转到 90 度时,值会变为大约 3.2V,当完全旋转到 180 度时,由于电阻的改变,电压会降至仅有 2V。

电机是令人惊奇的东西;它们有各种各样的形状和大小。主要上,它们可以被认为是大多数机器人的支撑。然而,在这个世界上没有什么是完美的。这些电机肯定也有一些缺点。到现在为止,你可能已经自己发现了一些。在上一章中,当我们让车子转弯时,你可能已经注意到转弯的角度从来不是完全相同的。同样,当车辆被命令直行时,它实际上并不会这样做。相反,它会试图向一侧轻微偏离。

但是当我们谈论机器人时,即使 1 度的精度可能还不够。如今的机器人学家期待的精度在两位小数的数量级内。因此,我们所说的精度接近 0.01 度。你现在怎么想?我们如何用电机实现这种精度水平呢?

激光雷达

  • 伺服电机

  • 现在我们有的是一个通过多个减速齿轮与电位器耦合的电机,它将减慢电机的速度并增加扭矩。在最终齿轮处,轴向外安装到机身上并与电位器耦合。列表

  • 现在,让我们把它放在一个有趣的组合中:

伺服电机

当电阻器的值改变时,电阻器的输出电压也会改变。有趣的是,如果电位器的输入电压是已知的,那么它的输出电压可以用来推断轴的位置。让我们看看如何做到:

而不用真正看电位器的轴,我们可以很容易地推断出,如果电阻器的输出电压为 4.8V,那么轴必须处于 0 度的位置。同样,我们可以说,如果电压为 3.2V,那么它就处于 90 度的位置,当电压为 2V 时,它就处于 180 度的位置。

制作你自己的区域扫描仪

所以,让我向你介绍伺服电机。伺服电机基本上是一个带有一些附加组件的电机。现在,要理解这些附加组件是什么,让我们先通过这个例子来看一下。假设你想去伦敦。现在,要了解你如何去那里以及到达伦敦的路线,你首先需要知道的是你现在的确切位置。如果你不知道你目前在哪里,就不可能计算出一条路线。同样,如果我们想要到达电机的某个位置,我们需要知道电机的轴现在所处的位置。为了做到这一点,我们使用一个电位器。电位器基本上是一个变阻器,它有一个轴,当旋转时会改变电阻的值。一个变阻器看起来像这样:

在这里,我们只是绘制了三个点,但对于电位器上的任何给定点,都会有一个非常特定的电阻与之对应。通过这个,我们可以精确计算出电位器的轴会在哪里。

通过以下主题,本章将回答所有这些问题:

向第一个问题打个招呼——精度。控制这些电机非常简单,但当我们必须将电机旋转到特定角度时,这些电机的问题就出现了。如果你需要将你的机器人车的电机只旋转 90 度,那么你该怎么做呢?你脑海中可能首先想到的是调整电机的时间。在这一点上你可能是对的。但是,要确保每次都能旋转到确切的 90 度是不可能的。

因此,正如您所了解的,电位器将能够感知输出轴指向的角度。然后,电位器连接到一个控制电路,该电路从电位器中读取数据,并进一步指导电机移动多少才能达到目标位置。由于这种闭环安排中控制电路知道轴在哪里,它可以计算需要移动电机多少才能达到目标位置。因此,这种安排能够将输出轴精确地转到任何给定的位置。

这种安排通常被称为舵机。在机器人行业中,这是控制精确运动最广泛使用的硬件之一。基本上,有三根导线进入控制电路——VCC、地线和信号。信号线将接收来自我们的树莓派的数据,并在接收后,它将进行必要的电机运动,使轴达到所需的位置。舵机的图像如下:

这些可以从非常便宜的价格开始,大约 4 到 5 美元,但它们的价格可以上升到数千美元。但是是什么决定了这些舵机的定价呢?在选择舵机时,有几个因素需要记住,但其中最重要的是扭矩

扭矩基本上是电机可以转动输出轴的转动力。通常以千克·厘米或牛顿·米来衡量。那这实际上是什么意思呢?让我们看下面的图表:

假设在前面的图表中,我们有一个扭矩为 10 千克·厘米的电机,附在上面的转子是 1 厘米。因此,它应该能够垂直从地面上拉起 10 千克的重量。然而,当我们将转子的半径改为 2 厘米时,可以被提起的重量减半。同样,如果半径增加到 10 厘米,那么可以被提起的重量只会减少到 1 千克。因此,基本上可以提起的重量将是扭矩/半径。

但是对于我们大多数的目的,我们不会使用之前显示的机制,所以让我们看下一个图表,看看如何进行计算:

现在,假设我们有一个长度为L的轴和轴的极端边上的负载。为了方便计算,我们将轴的重量视为可以忽略不计。现在,如果舵机的扭矩为 100 千克·厘米,轴的长度(L)为 10 厘米,那么通过简单的计算,我们可以提起的负载将是 100/10 = 10 千克。同样,如果长度增加到 100 厘米,可以提起的负载将减少到仅为 1 千克。

好了,我们已经对舵机有了相当多的了解。现在的问题是我们如何控制舵机?正如我提到的,有不同类型的舵机可供选择,可以通过各种方式进行控制。然而,用于业余用途最常用的是数字舵机。这些舵机需要PWM,根据 PWM 的占空比,轴的角度会改变。因此,让我们看看它是如何发生的。

通常,大多数舵机的频率为 50 赫兹。因此,每个脉冲的长度通常为 1/50 = 0.02 秒,换句话说就是 20 毫秒。此外,可以给这些舵机的占空比可以是 2.5%到 12.5%,这基本上意味着脉冲宽度为 0.5 毫秒到 2.5 毫秒。现在让我们看看它是如何工作的:

正如你所看到的,当给定一个 2.5%的占空比时,轴会下降到最小位置 0 度,当占空比增加到 7.5%时,轴会到达中间位置 90 度。最后,当占空比增加到 12.5%时,轴会到达最大位置 180 度。如果你想要中间的任何位置,你可以简单地选择相应的 PWM,它会改变舵机的位置到所需的角度。

但是你可能会想,如果我们想要超过 180 度怎么办?好问题,但是大多数数字舵机只能旋转 180 度。有些舵机可以完全旋转其轴,即 360 度;然而,它们的寻址略有不同。在本章之后,你基本上可以查看任何数字舵机的数据表,并按照自己的方式进行控制。

好了,理论够了;是时候做一些有趣的事情了。所以,让我们继续设置硬件,并用我们的双手控制一个舵机!将舵机连接到树莓派如下:

电线的颜色编码如下:

接下来,我们需要上传以下代码并看看会发生什么:

import RPi.GPIO as GPIO
import time

GPIO.setmode(GPIO.BCM)
GPIO.setup(14,GPIO.OUT)

pwm = GPIO.PWM(14, 50)
pwm.start(0)

while 1:

        pwm.ChangeDutyCycle(2.5)
        time.sleep(2)

        pwm.ChangeDutyCycle(5)
        time.sleep(2)

        pwm.ChangeDutyCycle(7.5)
        time.sleep(2)

        pwm.ChangeDutyCycle(10)
        time.sleep(2)

        pwm.ChangeDutyCycle(12.5)
        time.sleep(2)

当你运行这个程序时,你会看到舵机的轴从左到右移动,分别在 0 度、45 度、90 度、135 度和最后 180 度位置上停下。

让我们看看我们在程序中做了什么来实现它:

pwm = GPIO.PWM(14, 50)
pwm.start(0)

通过pwm = GPIO.PWM(14, 50)这一行,我们已经定义了 GPIO 引脚号 14 将用于 PWM,PWM 的频率将为 50。我们在之前的章节中也使用了pwm.start(0)这一行。它基本上将 PWM 引脚设置为 0,即没有占空比:

        pwm.ChangeDutyCycle(2.5)
        time.sleep(2)

        pwm.ChangeDutyCycle(5)
        time.sleep(2)

        pwm.ChangeDutyCycle(7.5)
        time.sleep(2)

        pwm.ChangeDutyCycle(10)
        time.sleep(2)

        pwm.ChangeDutyCycle(12.5)
        time.sleep(2)

现在所有之前的程序都在while循环中,也就是说,它将一遍又一遍地执行,直到程序被强制退出。现在,pwm.ChangeDutyCycle(2.5)这一行发送了一个 2.5%的 PWM 占空比给舵机。这将简单地将舵机转到 0 度角。接下来,我们使用老式的time.sleep(2),我们都知道这会使程序暂停两秒。

相同的循环正在重复,使用不同的 PWM 值,5%的 PWM 会将轴转到 45 度,7.5%的 PWM 会转到 90 度,10%的 PWM 会转到 135 度,12.5%的 PWM 会转到 180 度。这是一个非常简单的程序,可以清楚地了解舵机的基础知识。

到目前为止,你已经学会了如何控制舵机并使其朝我们想要的方向移动。现在,让我们再进一步,稍微改变代码使舵机平稳运行:

import RPi.GPIO as GPIO
import time

GPIO.setmode(GPIO.BCM)
GPIO.setup(14,GPIO.OUT)

pwm = GPIO.PWM(14, 50)
pwm.start(0)

i=2.5
j=12.5

while 1:
        while i<=12.5:
                pwm.ChangeDutyCycle(i)
                time.sleep(0.1)
                i = i + 0.1

        while j>=2.5:
                pwm.ChangeDutyCycle(j)
                time.sleep(0.1)
                j = j - 0.1

当你在你的树莓派上上传了这段代码时发生了什么?你会注意到舵机非常平稳地从左到右刷过去,然后从右到左。我们做了一个非常简单的技巧;让我们看看是什么:

        while i<=12.5:
                pwm.ChangeDutyCycle(i)
                time.sleep(0.1)
                i = i + 0.1

在这里,我们正在运行一个循环,直到值i<=12.5,因为我们在程序的开头已经定义了值i的默认值为2.5。之后,每次代码运行时,占空比被设置为i的值,程序暂停 0.1 秒,然后i的值增加 0.1。这增加了 PWM 的占空比。一旦值达到 12.5,循环退出。

我们的整个 PWM 范围是 2.5%到 12.5%,所以我们有 10%的空间可以使用。现在,如果我们将其映射到舵机的角度旋转,那么每个百分比的 PWM 对应于 180/10 = 18 度的变化。同样,每 0.1%的变化将导致 180/100 = 1.8 度的变化。因此,每 0.1 秒,我们增加 0.1%的占空比,换句话说,我们增加 1.8 度的角度。因此,我们发现这个动作非常平滑。

在程序的下一部分中,我们正在做类似的事情;然而,我们是为了反向运动。

列表

好的,那么,我们非常确定如何使用伺服,并根据我们的需求进行控制运动。现在是时候继续前进,了解我们将大量使用的另一个概念。它被称为数组。如果你在任何其他语言中编程过,你一定很熟悉它。但我们需要了解一些基本概念,这将使我们的生活变得更加轻松。所以,让我们开始吧。

首先,首先。在 Python 中,数组不被称为数组,而是被称为列表。列表基本上是一种可以同时存储多个元素的数据结构。唯一的限制是元素必须是相同的数据类型。比如,如果你存储整数,那么所有的值都应该是int。同样,如果你存储一个字符,那么列表的每个元素都应该是char。要定义一个列表,你所需要做的就是给列表命名,比如我们通过myList所做的那样;列表的名称可以是任何东西,接下来我们需要告诉编译器它实际上是一个列表。为了做到这一点,我们需要将值放在方括号内。它看起来像这样:

myList = [14,35,108,64,9]

需要记住的一件事是,每个值都应该用逗号分隔。每当我们想要访问列表的任何单个元素时,我们可以通过调用它们的索引号来简单地使用它们。这是基于元素在列表中的位置。Python 列表中的索引值从 0 开始。所以根据前面的声明,在索引 0 处,值将是14,在地址 4 处,值将是9。现在,当我们需要在程序中间打印这些元素时,我们需要写下以下代码:

print myList[2] 

一旦我们写了这个,程序将打印列表中第二个值的值。在我们的例子中,它将是35

现在,这是访问列表元素的一种方式;我们也可以以相反的顺序访问它。所以,假设你想访问数组的最后一个项目。然后,我们可以编写以下代码:

print myList[-1] 

这段代码将返回数组的最后一个元素的值。现在,每当我们在列表中使用负值时,它将以相反的顺序开始索引。所以,假设我们输入print myList[-2],这将给我们数组中倒数第二个值的值。在整个方案中需要记住的一件事是,编号将从 0 开始,而当我们以相反的顺序开始时,编号将从-1 开始。

Python 真的很有趣,如果你知道正确的工具,它也很简单。Python 的开发人员包含了一些非常有用的函数,可以在列表上使用。所以,让我们去探索一下它们。

第一个是向数组添加元素。为此,我们使用一个名为append()的函数。append()函数的作用是在数组的末尾添加你想要的值。所以,写下以下内容:

myList.append(45)

这样做的效果是在myList的末尾添加元素45。所以现在列表将如下所示:

myList = [14,35,108,64,9, 45]

简单,不是吗?但是如果你想在列表中间添加一个元素怎么办?显然,开发人员不会让你措手不及。他们也包含了一个名为insert(index, element)的函数;现在每当你使用这个函数时,你需要确保提到你想要这个元素的索引位置,其次是你想要放置的元素。所以它看起来像这样:

myList.insert(3,23)

当你使用这个函数时,数组将如下所示:

myList = [14,35,108,23,64,9,45]

显然,当开发人员给出添加元素的功能时,他们肯定也会给出删除元素的功能。但诀窍在于你可以用两种方法做到这一点。首先,是常见的方法。我们只需选择索引号并删除它。我们现在要做的就是这样:

del myList[2]

现在这样做的效果是删除数组的第二个元素,所以在执行此操作后,数组将如下所示:

myList = [14,35,108,64,9,45]

但现在真正的技巧来了;你也可以通过简单地指定元素来删除元素。就是这样做的:

myList.remove(9)

一旦你这样做了,它会找到列表中元素9的位置并将其从中删除。所以你不必关心元素在哪里;这个函数会说,我会找到你并杀死你!

四处张望

好了,够了电影台词。我们可以讨论我们可以在列表上使用的许多其他功能,但我们现在所做的已经足够了。我们将根据需要看到其余的功能。但现在让我们在机器人技术上迈出一步。你可能已经在许多自动驾驶汽车的顶部看到旋转的物体。生产车辆通常不会有这种装置,主要是因为价格昂贵,但研究用途的车辆总是装备有它。

那么这个装置是什么?它被称为激光雷达;这是光探测和测距的缩写。我知道这是个糟糕的缩写。激光雷达之所以如此普遍,是有原因的。它以非常精确的方式给出了周围区域的距离读数。然而,为我们的项目购买它可能有点杀鸡用牛刀,因为一个好的激光雷达可能会花费你接近 500 到 10,000 美元。如果你仍然认为这在你的预算范围内,那么你会非常幸运!但对于那些不想购买它的人,我有一个好消息。今天,我们将建立我们自己的激光雷达扫描仪。因此,为了制作一个区域扫描仪,我们需要一个伺服电机,我们将在其上安装我们的红外近距传感器。现在要做到这一点,我们需要一个轻微的临时安排。你可以拿一块硬纸板,像我们在这里的图片中所做的那样固定它,或者你也可以使用一个直角铝材并钻孔来固定组件,如果你想要以专业的方式来做。要记住的一件事是,传感器必须正好平行于地面,而不是向上或向下。

安装完成后,就是连接其余的硬件的时候了。所以继续连接硬件,如下图所示:

好的,让我们看看这个东西能做什么,准备好,上传这段代码:

import RPi.GPIO as GPIO
import time
import Adafruit_ADS1x15

adc = Adafruit_ADS1x15.ADS1115()
GAIN = 1

adc.start_adc(0, gain=GAIN)
GPIO.setmode(GPIO.BCM)
GPIO.setup(14,GPIO.OUT)
GPIO.setwarnings(False)

servo = GPIO.PWM(14, 50)

servo.start(0)

Def Distance():
    D_value = adc0.get_last_result()
    D =    (1.0 / (F_value / 13.15)) - 0.35
    Return D

j=12.5
k=2.5
i=0

distLR=[] 
distRL=[]

while True:
        while k<=12.5:
                servo.ChangeDutyCycle(k)
                time.sleep(.1)
                distLR.insert(i,Distance())
                k = k + 2.5
                i = i + 1
        print distLR

        i=0
        k=0

        del distLR[:]

        while j>=2.5:
                servo.ChangeDutyCycle(j)
                time.sleep(.1)
                j = j - 2.5
                distRL.insert(i,Distance())
                i = i + 1

        print distRL

        i=0
        k=2.5
        j=12.5

       del distRL[:]

代码做了什么?如果它正常运行,那么它应该将整个 180 度的扫描读数分成 10 个均匀的步骤返回给你。继续——试一试,然后返回看看实际发生了什么。

现在大部分代码都是基础的,你可能也已经对这段代码实际在做什么有了一个概念。然而,让我们深入了解一下,看看具体情况:

Def Distance():
    D_value = adc0.get_last_result()
    D =    (1.0 / (F_value / 13.15)) - 0.35
    Return D

在程序的这一部分,我们定义了一个名为Distance()的函数。你可以看到,它只是从 ADC 中获取读数在步骤D_value = adc0.get_last_result();然后,存储在变量D中的值在行D = (1.0/F-value/13.15)) - 0.35中计算,以获取 ADC 读数的度量读数。最后,使用行Return D,我们从函数中返回值D

distLR=[] 
distRL=[]

我们声明了两个列表:distLR,用于伺服从左到右扫描的距离,distRL用于伺服从右到左扫描的距离。你可能会想这些括号里面为什么什么都没有。声明一个空数组是完全正常的。它们最初没有必要有值:


        while k<=12.5:
                servo.ChangeDutyCycle(k)
                time.sleep(.1)
                distLR.insert(i,Distance())
                k = k + 1
                i = i + 1
        print distLR

现在这就是真正的行动发生的地方。while循环只会在k的值小于或等于12.5的时候执行。在接下来的一行中,servo.ChangeDutyCycle(k),占空比的值将是k的值。最初,k的值将是2.5,因为我们已经在程序的开头定义了。现在我们添加另一行time sleep(.1),这将使程序暂停.1秒。这是必要的;否则,程序会在毫秒内解析完这个循环,舵机将无法跟上。因此,这是一个短暂的延迟。在接下来的一行中,我们有distLR.insert(I,Distance())。这行程序做了很多事情。首先,我们在这一行内命名了一个Distance()函数。正如我们定义的那样,它将使用 ADC 和红外接近传感器来计算距离。然后,它会将该距离值插入到列表distLR的位置I。在我们的程序中,我们之前已经赋值了i = 0的值;因此,距离值将被放在数组中的第一个位置。一旦整个过程完成,然后我们继续向前移动,并在这一行中将值增加一,k = k + 1;然后,我们在I = I + 1中做同样的事情。现在最后,一旦这个循环执行完毕,列表的值将使用print distLR打印出来:

        i=0
        k=0

在这一行中,我们只是为下一个循环重置i = 0k = 0的值:

        del distLR[:]

这对你来说可能有点新。每当我们在括号内使用冒号时,基本上意味着整个数组的元素将被删除:

 while j>=2.5:
                servo.ChangeDutyCycle(j)
                time.sleep(.1)
                j = j - 2.5
                distRL.insert(i,Distance())
                i = i + 1

        print distRL

在这段代码中,发生的事情与我们对左到右滑动所做的事情相同;唯一的区别是我们将它保存在一个名为distRL的新列表中,并且滑动从 12.5%的占空比开始,结束于 2.5%:

   i=0
        k=2.5
        j=12.5

       del distRL[:]

当我们打印出所有的值后,我们再次重置i = 1k = 2.5j = 12.5的值,以便我们的第一个循环可以无缝地开始,此外,我们还确保列表distRL中没有剩下任何东西。

这就是我们的代码是如何工作的,直截了当!

自动驾驶汽车上的激光雷达

还记得上次我们制作自动驾驶汽车吗?很酷,当然,这可能是你可以向朋友炫耀的东西。然而,现在我们要做的肯定比我们迄今为止所做的任何事情都要酷。

我们将把这个区域扫描仪放在我们的机器人车上。但等等,我们之前使用相同的传感器扫描过这个区域,把车转向其他方向。我们做到了,而且效果还不错,几乎不错。我敢打赌,有时它的准确性并不像你想象的那样。但这不是真正的问题。主要问题是它不是无缝的。它必须在中间停下来检查空间,然后向任一方向移动。我们现在要做的是更进一步的事情。所以在做任何更多的解释之前,让我们继续制作这辆新的机器人车,然后你来判断它是否更酷。

因此,为了制作它,你需要将区域扫描仪安装在车辆上。建议你将其设置在车辆的前端,并确保舵机的臂能够旋转 180 度。你可以使用我们用来固定红外传感器在舵机顶部的类似方法。在做所有这些的时候,尝试使用电缆束带来确保电缆不凌乱,并确保为轴和顶部的传感器的移动留出一些松弛。这些电缆束带可以让你的生活变得非常简单。一旦我们都准备好了,你应该将红外接近传感器使用 ADS1115 连接到树莓派,然后连接电机驱动器,如下图所示:

完成后,继续上传以下代码:

import RPi.GPIO as GPIO
import time
import Adafruit_ADS1x15

adc0 = Adafruit_ADS1x15.ADS1115()
GAIN = 1
adc0.start_adc(0, gain=GAIN)

GPIO.setmode(GPIO.BCM)
GPIO.setup(14,GPIO.OUT)

servo = GPIO.PWM(14, 50)
servo.start(0)

def Distance():
    D_value = adc0.get_last_result()
    D =    (1.0 / (F_value / 13.15)) - 0.35
    Return D

GPIO.setup(20,GPIO.OUT)
GPIO.setup(21,GPIO.OUT)
GPIO.setup(23,GPIO.OUT)
GPIO.setup(24,GPIO.OUT)

LForward = GPIO.PWM(20, 50)
LReverse = GPIO.PWM(21, 50)
RForward = GPIO.PWM(23,50)
RReverse = GPIO.PWM(24,50)

def stop():
    LForward.changeDutyCycle(0)
    LReverse.changeDutyCycle(0)
    RForward.changeDutyCycle(0)
    RReverse.changeDutyCycle(0)

def direction(index):

 if index == 0 :
    LForward.changeDutyCycle(0)
    LReverse.changeDutyCycle(30)
    RForward.changeDutyCycle(30)
    RReverse.changeDutyCycle(0)

elif index == 1

    LForward.changeDutyCycle(20)
    LReverse.changeDutyCycle(0)
    RForward.changeDutyCycle(50)
    RReverse.changeDutyCycle(0)

 elif index == 2 :

    LForward.changeDutyCycle(50)
    LReverse.changeDutyCycle(0)
    RForward.changeDutyCycle(50)
    RReverse.changeDutyCycle(0)

elif index == 3 :

    LForward.changeDutyCycle(50)
    LReverse.changeDutyCycle(0)
    RForward.changeDutyCycle(20)
    RReverse.changeDutyCycle(0)

 elif index == 4 :

    LForward.changeDutyCycle(20)
    LReverse.changeDutyCycle(0)
    RForward.changeDutyCycle(0)
    RReverse.changeDutyCycle(20)

 else:
 stop()

j=12.5
k=2.5
i=0

dist1=[]
dist2=[]

while True:

    while k<=12.5:
    servo.ChangeDutyCycle(k)
    time.sleep(.2)
    dist1.insert(i,Distance())
    k = k + 2.5
    i = i + 1

 print dist1

 i=0
 k=2

 max_dist1 = max(dist1)
 max_dist1_index = dist1.index(max_dist1)

 direction(max_dist1_index)

 del dist1[:]

 print max_dist1
 print max_dist1_index

 while j>=2.5:
    servo.ChangeDutyCycle(j)
    time.sleep(.2)
     j = j - 2.5
     dist2.insert(i,Distance())
    i = i + 1

print dist2

i=0
j=12

 max_dist2 = max(dist2)
 max_dist2_index = dist2.index(max_dist2)

 direction(max_dist2_index)

 del dist2[:]

 print max_dist2
 print max_dist2_index

呼!那是很长,不是吗?但相信我,可能很长,但不难。所以让我们看看这段代码在做什么:

LForward = GPIO.PWM(20, 50)
LReverse = GPIO.PWM(21, 50)
RForward = GPIO.PWM(23,50)
RReverse = GPIO.PWM(24,50)

这些东西可能对你来说看起来很新。但其实并不是。我们正在定义哪个引脚号将以什么 PWM 频率运行。此外,我们已经为用于电机控制的每个 GPIO 引脚命名。好吧,我们做所有这些都没问题,但为什么我们突然开始给电机驱动器提供 PWM。我们不是很满意给一个简单的高脉冲吗?

答案非常直接。通过使用 PWM,我们能够在之前的章节中改变 LED 的亮度。同样,通过改变 PWM 输出到电机驱动器的控制引脚,你不仅可以定义旋转的方向,还可以定义旋转的速度。这一切都是通过 PWM 完成的。所以假设引脚号20正在以 50%的占空比获得 PWM。这基本上意味着连接到它的电机将获得电机驱动器接收到的输入电压的一半。所以现在我们不仅可以控制电机旋转的方向,还可以控制旋转的速度:

def direction(index):

 if index == 0 :
    LForward.changeDutyCycle(0)
    LReverse.changeDutyCycle(30)
    RForward.changeDutyCycle(30)
    RReverse.changeDutyCycle(0)

elif index == 1
    LForward.changeDutyCycle(20)
    LReverse.changeDutyCycle(0)
    RForward.changeDutyCycle(50)
    RReverse.changeDutyCycle(0)

在这个语句中,我们定义了一个direction(index)函数。它的作用是比较索引的值,并根据它给予电机动力。所以假设索引是 0。在这种情况下,左侧的车轮将向相反方向移动,而右侧的车轮将向相反方向移动,这将使机器人绕着轴旋转。

在下一条语句中,我们写了一个elif语句,所以如果else语句不成立,它将检查主体中的其余elif语句。在direction(index)的整个定义中有四个elif语句,这基本上意味着它将检查每一个并根据参数的值执行其中之一的活动。在这种情况下,它是索引。此外,还有一个最终的else语句,如果没有一个情况成立,它将调用一个停止的函数。这将停止车辆:

max_dist1 = max(dist1)

这一行非常有趣,因为我们正在使用我们已经使用过的列表的另一个有趣部分。因此,通过max()方法,我们可以找到列表中的最大值。因此,在这一行中,我们只是找到最大值并将其放入一个名为max_dist1的变量中:

max_dist1_index = dist1.index(max_dist1)

列表的美妙似乎没有尽头。在这一行中,我们正在使用另一种名为index()的方法;这种方法给出了列表中数值的索引。因此,我们可以知道数值在列表中的位置。因此,在这一行中,我们正在证明max_dist1的值。index()方法搜索索引号,并将该值存储在一个名为max_dist1_index的变量中:

 direction(max_dist1_index)

由于我们已经定义了Direction()函数,现在我们所做的就是调用函数来决定前进的方向。那么,给你的车辆加油,看看它们驾驶得如何,不要忘记拍摄视频并在网上发布。

玩得开心!

总结

专业激光扫描仪非常昂贵,因此在本章中,我们自己制作了一种替代品,并将其安装在我们的车辆上。在下一章中,我们将涵盖诸如视觉处理、物体检测、物体跟踪等主题,这将使我们能够进行基本的视觉处理,并使汽车朝着特定物体(如球)的方向移动。

第二十四章:基本开关

到目前为止一定是一段史诗般的旅程!回想一下你开始阅读这本书的时候,你是否曾想象过事情会变得如此简单?值得注意的是,一切都是从非常简单的开始,然后慢慢地,随着对更复杂系统的需求,技术的复杂性也增加了。回到个人计算并不是真正的事情的时候。它只在商业中使用,像 IBM 这样的公司只为商业客户提供服务。那时,想要个人计算机的人只有一个选择。他们需要从头开始建造,老实说,很多人过去都这样做。至少从我的角度来看,这真的并不难。但是,与那个时代相比,想想它们现在变成了什么样子。曾经想过在家里建造一台计算机吗?我说的是设计一切,而不仅仅是组装 CPU。这并不容易。

我在这里想告诉你的是,曾经有一段时间,计算机是稀有的;它们并不常见,功能也非常有限。然而,随着时间的推移和像史蒂夫·乔布斯、比尔·盖茨、休利特和帕卡德这样的人的智慧,计算机变得更加用户友好,更容易获得,并成为一种令人向往的商品。想象一下同样的情况发生在机器人身上。它们很昂贵;对于大多数人来说,它们并没有太多用处,而且在公共场所也很少见。但是,正如你所学到的,为我们个人使用构建机器人并不是很难,再加上一些调整和你这样有创造力的头脑,事情可以朝着完全不同的方向发展。你可能会因为你的愿景而受到嘲笑。但请记住,每个发明家在某个时候都被称为疯子。所以下次有人称你为疯子时,你可以非常确定你正在进步!

嗯,我非常确定,如果你是一个机器人爱好者,那么你一定看过电影《钢铁侠》。如果你还没有看过,那就停下来阅读这本书,去打开 Netflix 看看那部电影。

有一次我看了那部电影,我想要制作两件东西:一件是钢铁侠的战衣,另一件是他的个人助手贾维斯,他照顾他的一切需求。虽然战衣似乎是我可能需要一段时间来研究的东西,但到那时,你可以继续为自己建立个人助手。

想象一下你的家自己做事情。那会多酷啊?它知道你喜欢什么,你什么时候醒来,你什么时候回家,基于此,它会自动为你做事情。最重要的是,它不会是你从货架上购买的东西,而是你亲手制作的。

在你做任何这些之前,我必须告诉你,你将处理高电压和相当大的电流。电力不是闹着玩的,你必须随时小心并佩戴所有安全设备。如果你不确定,那么最好找一个电工来帮助你。在触摸或打开任何电气板之前,确保你穿着不导电的鞋子;还要检查螺丝刀、钳子、鼻钳、剪刀和其他工具是否绝缘良好且处于良好状态。戴手套是个好主意,增加安全性。如果你未满 18 岁,那么你必须有一个成年人随时帮助你。

既然说到这里,让我们开始看看我们有什么。

让贾维斯叫醒你

现在,这个非常有趣,正如大家所知,我们的人体是按照一定的方式编程的。因此,我们对不同的刺激作出非常熟悉的反应。比如当天黑了,我们的大脑会产生触发睡眠的激素。一旦阳光照到我们的眼睛,我们就会醒来。好吧,至少应该是这样!最近,我们的生活方式发生了巨大变化,开始违背这种周期。这就是为什么我们看到越来越多的失眠病例。被闹钟吵醒绝对不是自然的。因此,即使它的铃声是您最喜欢的歌曲,您早上听到闹钟也不会开心。我们的睡眠周期应该与阳光同步,但现在几乎没有人会通过这种方式醒来。因此,在本章中,让我们首先制作一个智能闹钟,模拟我们醒来的自然方式。

使用继电器和 PIR 传感器

由于我们正在处理高电压和更高电流,我们将使用继电器。为此,请按以下方式连接电线:

连接完成后,上传以下代码,让我们看看会发生什么:

import RPi.GPIO as GPIO import time LIGHT = 23 GPIO.setmode(GPIO.BCM) GPIO.setwarnings(False) GPIO.setup(LIGHT,GPIO.OUT) import datetime H = datetime.datetime.now().strftime('%H') M = datetime.datetime.now().strftime('%M') 
 while True: if H = '06'and M < 20 : GPIO.output(LIGHT,GPIO.HIGH) else: GPIO.output(LIGHT,GPIO.LOW)

好的,这是一个非常简单的代码,不需要太多解释。我们以前也做过一个非常类似的代码。你还记得吗?那是在最初的几章,当我们正在制作一个浇水机器人时,我们必须在特定时间给植物浇水。现在它所做的就是检查时间,以及时间是否为06小时,分钟是否小于20。也就是说,灯会在 07:00 到 07:19 之间打开。之后,它会关闭。

制作令人讨厌的闹钟

但是有一个问题。问题是灯会打开,无论您是否起床,灯都会在 20 分钟内自动关闭。这有点问题,因为您并不是每次都会在 20 分钟内醒来。那么,在这种情况下,我们应该怎么办呢?我们需要做的第一件事是检测您是否醒来了。这非常简单,这里不需要太多解释。如果您早上醒来,非常肯定您会离开床。一旦您离开床,我们就可以检测到运动,告诉我们的自动系统您是否真的醒来了。

现在,我们可以在这里做一些非常简单的事情。我们可以检测您的动作,并根据检测结果决定您是否真的醒来了。这似乎不是什么大任务。我们只需要添加一个运动检测传感器。为此,我们可以使用 PIR 传感器,它可以告诉我们是否检测到了运动。所以,让我们继续,在我们的系统顶部添加另一层传感器,看看会发生什么。

首先,按以下方式连接电路。在安装 PIR 传感器时,请确保它面向床,并检测其周围的任何运动。一旦 PIR 设置好,将传感器连接如下图所示,并看看会发生什么:

完成后,继续编写以下代码:

import RPi.GPIO as GPIO import time LIGHT = 23 PIR = 24 Irritation_flag = 3  GPIO.setmode(GPIO.BCM) GPIO.setwarnings(False) GPIO.setup(LIGHT,GPIO.OUT) GPIO.setup(PIR, GPIO.IN) import datetime H = datetime.datetime.now().strftime('%H') M = datetime.datetime.now().strftime('%M')       while True:

        if H = '07' and M <= '15' and Iriitation_Flag > 0 and GPIO.input(PIR) == 0:

  GPIO.output(LIGHT,GPIO.HIGH)

  if H = '07'and GPIO.input(PIR)==1:

 GPIO.output(LIGHT,GPIO.LOW)
            time.sleep(10) Irritation_Flag = Irritation_Flag - 1  for H = '07'and M > '15' and Irritation_Flag > 0 and GPIO.input(PIR) = 0: GPIO.output(LIGHT,GPIO.HIGH)
            time.sleep(5) GPIO.output(LIGHT,GPIO.LOW)
            time.sleep(5)  if H != '07':

            Irritation_flag = 3
            GPIOP.output(LIGHT, GPIO.LOW)  

好的,让我们看看我们做了什么。代码非常简单,但我们在其中有一个小变化,那就是“烦躁标志”:

Irritation_flag = 3

现在,这个变量的作用有点像贪睡按钮。我们知道,当我们醒来时,有时,或者事实上,大多数时候,我们会再次回去睡觉,直到很久以后才意识到我们迟到了。为了防止这种情况,我们有这个“烦躁标志”,它的基本作用是检测您停止闹钟的次数。我们稍后会看到它的使用方法:

        if H = '07' and M <= '15' and Irritation_Flag > 0 and GPIO.input(PIR) == 0:

  GPIO.output(LIGHT,GPIO.HIGH)

在这一行中,我们只是比较小时和分钟的时间值。如果小时是07,分钟少于或等于15,那么灯将关闭。还有一个条件是Irritation_Flag > 0,因为我们在开始时已经声明了Irritation_flag = 3;因此,最初这个条件总是为真。最后一个条件是GPIO.input(PIR) == 0;这意味着只有当 PIR 没有检测到任何运动时,条件才会满足。简单地说,如果 PIR 没有检测到任何运动,那么闹钟将在每天 07:00 和 07:15 之间响起:

  if H = '07'and GPIO.input(PIR)==1:

 GPIO.output(LIGHT,GPIO.LOW)
            time.sleep(10) Irritation_Flag = Irritation_Flag - 1

在程序的这一部分,只有当小时或H等于7并且 PIR 检测到一些运动时,条件才会为真。因此,每当时间在 07:00 和 07:59 之间,以及每当检测到运动时,条件就会为真。一旦为真,程序将首先使用GPIO.output*LIGHT,GPIO.LOW关闭灯。一旦关闭,它会使用time.sleep(10)等待10秒。时间到后,它将执行以下操作:Irritation_Flag - Irritation_Flag - 1。现在它所做的是每次检测到运动时将Irritation_Flag的值减少1。因此,第一次发生运动时,Irritation_Flag的值将为2;之后将为1,最后将为0

如果你看一下代码的前一部分,你会发现只有当Irritation_Flag的值大于0时,灯才会打开。因此,如果你想关闭灯,你至少要移动三次。为什么是三次?因为代码Irritation_Flag = Irritation - 1将被执行三次,以使值减少到0,这显然会使条件GPIO.input(PIR) > 0为假:

  for H = '07'and M > '15' and Irritation_Flag > 0 and GPIO.input(PIR) = 0: GPIO.output(LIGHT,GPIO.HIGH)
            time.sleep(5) GPIO.output(LIGHT,GPIO.LOW)
            time.sleep(5) 

现在,假设即使经过了所有这些,你仍然没有醒来。那么应该发生什么?我们在这里为您准备了一些特别的东西。现在,我们不是使用if条件,而是使用for循环。它将检查时间是否为07小时,分钟是否大于15Irritation_Flag > 0,显然没有检测到运动。只要所有这些条件都为真,灯就会在之后打开5秒,使用time.sleep(5)保持打开。然后灯会再次打开。这将一直持续下去,直到条件为真,或者换句话说,直到时间在 07:15 和 07:59 之间。Irritation)_Flag > 0,也就是说,连续三次未检测到运动。在此期间,for 循环将继续打开和关闭灯。由于频繁的灯光闪烁,你醒来的机会非常高。这可能非常有效,但肯定不是最方便的。然而,无论多么不方便,它仍然比传统的闹钟要好:

 if H != '07':

            Irritation_flag = 3

我们已经准备好了整个基于灯光的闹钟,可以在每天早上叫醒我们。但是,有一个问题。一旦关闭,Irritation_Flag的值将为0。一旦变为0,无论时间如何,灯都不会启动。因此,为了确保闹钟每天都在同一时间运行,我们需要将标志的值设置为大于0的任何数字。

现在,在前一行中,如果H != '07',那么Irritation_flag将为3。也就是说,每当时间不是07小时时,Irritation_Flag的值将为3

这很简单,不是吗?但我相信它会很好地确保你按时醒来。

让它变得更加恼人

您能完全依赖前面的系统吗?如果您真的能控制自己早上不想起床的情绪,那么,是的,您可以。但对于那些喜欢躺在床上并在按掉贪睡按钮后再次入睡的人来说,我相信您一定能找到一种方法来关闭灯光而不是真正醒来。因此,就像代码中一样,当检测到运动三次时,灯光会关闭。但运动可以是任何东西。您可以在床上挥手,系统会将其检测为运动,这将违背整个目的。那么现在我们该怎么办呢?

我们有一个解决方案!我们可以使用一种方法,确保您必须起床。为此,我们将使用我们之前在项目中使用过的红外近距传感器,并根据传感器的距离读数,我们可以检测您是否已经穿过了特定区域。这可能非常有趣,因为您可以将该传感器安装在床的另一侧,或者可能安装在浴室的门口,直到您穿过特定线路为止。系统不会关闭闹钟。所以让我们看看我们将如何做。首先,按照以下图表连接硬件:

完成图表后,继续上传以下代码:

import RPi.GPIO as GPIO import time import Adafruit_ADS1x15 adc0 = Adafruit_ADS1x15.ADS1115() GAIN = 1  adc0.start_adc(0, gain=GAIN)  LIGHT = 23 PIR = 24 Irritation_flag = 1 IR = 2 GPIO.setmode(GPIO.BCM) GPIO.setwarnings(False) GPIO.setup(LIGHT,GPIO.OUT) GPIO.setup(PIR, GPIO.IN)
GPIO.setup(IR. GPIO.IN) import datetime H = datetime.datetime.now().strftime('%H') M = datetime.datetime.now().strftime('%M')       while True:

  if H = '07' and M <= '15' and Iriitation_Flag > 0 and GPIO.input(PIR) == 0:

  GPIO.output(LIGHT,GPIO.HIGH)

  if H = '07'and GPIO.input(PIR)==1: M_snooze = datetime.datetime.now().strftime('%M')
   M_snooze = M_snooze + 5
 for M <= M_snoozeGPIO.output(LIGHT,GPIO.LOW) F_value = adc0.get_last_result()  F1 = (1.0  / (F_value /  13.15)) -  0.35

     time.sleep(0.1)

     F_value = adc0.get_last_result()  F2 = (1.0  / (F_value /  13.15)) -  0.35

     F_final = F1-F2 M = datetime.datetime.now().strftime('%M') if F_final > 25

         Irritation_flag = 0     for H = '07'and M > '15' and Irritation_Flag > 0 and GPIO.input(PIR) = 0: GPIO.output(LIGHT,GPIO.HIGH)
 time.sleep(5) GPIO.output(LIGHT,GPIO.LOW)
 time.sleep(5)  if H != '07':

 Irritation_flag = 1 

震惊了吗?这段代码似乎相当复杂,内部嵌套了条件,再加上更多的条件。欢迎来到机器人领域!这些条件构成了大部分机器人的编程。机器人必须不断观察周围发生的事情,并根据情况做出决策。这也是人类的工作方式,不是吗?

说了这么多,让我们看看我们实际上在这里做了什么。大部分代码基本上与上一个相同。主要区别在于编程部分的中间某处:

  if H = '07' and M <= '15' and Iriitation_Flag > 0 and GPIO.input(PIR) == 0:

  GPIO.output(LIGHT,GPIO.HIGH)

我们会在时间介于 07:00 和 07:15 之间时打开灯光:

  if H = '07'and GPIO.input(PIR)==1: M_snooze = datetime.datetime.now().strftime('%M')
   M_snooze = M_snooze + 5

07点的时候,每当 PIR 传感器被触发,或者换句话说,PIR 传感器检测到任何运动,那么它将在if条件内执行一系列活动,包括通过函数datetime.datetime.now().strftime('%M')记录时间,然后将其存储在名为M_snooze的变量中。

在下一行,我们取出存储在M_snooze中的分钟值,并再加上5分钟。因此,M_snooze的值现在增加了5

 for M <= M_snooze 

现在,在我们之前使用的相同if条件中,我们放置了一个for循环,看起来像这样:for M <= M_snooze。但这是什么意思?在这里,我们所做的事情非常简单。for循环内的程序将继续运行,并且会一直保持在循环中,直到我们所述的条件为真。现在,这里的条件规定了只要M小于或等于M_snooze的时间,条件就会保持为真。正如您之前学到的,M是当前的分钟值,而M_snooze是循环开始时的M的值,增加了5。因此,循环将在开始时的5分钟内保持为真:

 GPIO.output(LIGHT,GPIO.LOW) F_value = adc0.get_last_result()  F1 = (1.0  / (F_value /  13.15)) -  0.35

     time.sleep(0.1)

     F_value = adc0.get_last_result()  F2 = (1.0  / (F_value /  13.15)) -  0.35

     F_final = F1-F2

现在,这是程序中最有趣的部分。直到for M <= M_snooze为真,前面的代码行将运行。让我们看看它在做什么。在F-value = adc0.get_last_result()这一行中,它获取红外距离传感器的值并将其存储在F_value中。然后,在F1 = (1.0/(F_value/13.15))-0.35这一行中,我们简单地计算了以厘米为单位的距离。我们已经学习了这是如何发生的,所以这里不需要做太多解释。距离的值存储在一个名为F1的变量中。然后,使用time.sleep(0.1)函数,我们暂停程序0.1秒。然后,我们再次重复相同的任务;也就是说,我们再次获取距离的值。但是这次,计算出的距离值存储在另一个名为F2的变量中。最后,在所有这些都完成之后,我们计算F_final,即F_final = F1 - F2。所以我们只是计算了第一次和第二次读数之间的距离差。但是,你可能会问我们为什么要这样做。这有什么好处呢?

嗯,你还记得,我们把红外距离传感器放在浴室门口。现在,如果没有人经过,数值将保持相当恒定。但是每当有人经过时,距离就会发生变化。因此,如果从第一次到最后一次读数的总距离发生变化,那么我们可以说有人通过了红外传感器。

这很酷,但为什么我们不像以前那样保留一个阈值呢?答案很简单。因为如果你需要改变传感器的位置,那么你又需要根据位置重新校准传感器。所以这是一个简单但健壮的解决方案,可以在任何地方使用:

 if F_final > 10

        Irritation_flag = 1

现在我们已经得到了读数,可以告诉我们是否有人经过。但是除非我们把它放在某个地方,否则这些数据是没有用的。

所以,在条件if F_final > 10中,每当距离变化超过10厘米时,条件就会成立,Irritation_flag将被设置为1

如果你回到前面的行,你就会发现只有在时间在 07:00 和 07:15 之间,且Irritation_flag必须为0时,灯才会亮起。由于这个条件,我们通过将Irritation_flag = 1使条件的一部分变为假;因此,开灯的程序将不起作用。

现在,让我们回顾一下我们到目前为止所做的事情:

  • 当时间是 07:00-07:15 时,灯将被打开

  • 如果检测到有人移动,灯将被关闭

  • 另一个条件将再持续五分钟,等待红外距离传感器检测到人体运动

  • 如果一个人在五分钟内通过,那么警报将被停用,否则警报将再次开始打开灯

挺酷的,是吧? 话虽如此,让我们从之前的程序中再添加另一个功能:

  for H = '07'and M > '15' and Irritation_Flag = 0 and GPIO.input(PIR) = 0: GPIO.output(LIGHT,GPIO.HIGH)
    time.sleep(5) GPIO.output(LIGHT,GPIO.LOW)
    time.sleep(5)

你知道这是做什么的。如果在第一个15分钟内你不活动,也就是从 07:00 到 07:15,那么它将开始每五秒闪烁灯,迫使你醒来:

 if H != '07':

            Irritation_flag = 0 

最后,我们使用条件if H != '07':。所以,每当H的值不是07时,条件就会成立,这将把Irritation_flag重置为0。到现在为止,你知道将Irritation_flag设置为0的作用。

总结

所以,最后,我们做出了我们的第一个迷你贾维斯,它可以在早上叫醒你,甚至在你没有按时醒来时还会惹你生气。希望你通过学习两个运动传感器及其在自动化电器中的应用来真正享受了这一章节。所以,继续在家里尝试一下,根据自己的需求修改代码,制作一些真正酷炫的东西。接下来,我们将让我们的贾维斯做一些更酷炫的事情,并且我们将介绍一些更令人兴奋的有关人体检测的东西。

第二十五章:用贾维斯识别人类

到目前为止,我们已经在上一章中了解到如何将多层条件组合在一起以获得所需的功能。我们刚刚完成了让贾维斯为您工作的第一步。现在,是时候让它变得更加强大了。

在本章中,我们将使其控制更多您家中的电子设备,这些设备可以在您没有告诉系统任何内容的情况下自主控制。所以,不要拖延,让我们直接进入并看看我们的收获。

打开灯,贾维斯

智能家居的基本功能之一是在您附近时为您打开灯光。这是任何系统可以为您做的最基本的事情之一。我们将从您进入房间时打开灯光开始,然后我们将使系统变得更加智能。

因此,我们需要做的第一件事是识别您是否在房间里。有多种方法可以做到这一点。生活的一个重要特征就是运动的存在。您可能会说植物不会移动,但它们会生长,不是吗?因此,检测运动可能是检测某人是否在场的关键步骤!

这一步对您来说并不那么困难,因为我们之前已经接口化了这个传感器。我们说的是老式的 PIR 传感器。因此,传感器将感知区域内的任何运动。如果有任何运动,那么贾维斯将打开灯光。我相信这是您现在可以自己做到的事情。您仍然可以参考这里的代码和电路图:

现在上传以下代码:

import RPi.GPIO as GPIO
import time
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
PIR = 24
LIGHT = 23
GPIO.setup(DOPPLER,GPIO.IN)
GPIO.setup(BUZZER,GPIO.OUT)
While True:
   if GPIO.input(PIR) == 1:
       GPIO.output(LIGHT,GPIO.HIGH)
   if GPIO.input(PIR) == 0:
       GPIO.output(LIGHT,GPIO.LOW)

在上述代码中,我们只是在检测到运动时立即打开灯光,但问题是它只会在有运动的时候打开灯光。这是什么意思?简单来说,只要有一些运动,灯就会保持开启,一旦运动停止,灯就会关闭。

对于想要减肥的人来说,这可能是一个很好的代码,但对于我们大多数人来说,这将是令人讨厌的。因此,让我们包含一个小循环,我们在上一章中使用过,并使其变得更好一些:

import RPi.GPIO as GPIO
import time

GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)

PIR = 24
LIGHT = 23
TIME = 5

GPIO.setup(PIR,GPIO.IN)
GPIO.setup(BUZZER,GPIO.OUT)

While True:

   If GPIO.input(PIR) == 1:

       M = datetime.datetime.now().strftime('%M')
       M_final= M + TIME 
       for M < M_final:

         GPIO.output(LIGHT,GPIO.HIGH)
         M = datetime.datetime.now().strftime('%M')

         if GPIO.input(PIR) == 1:
            M_final = M_final + 1 if GPIO.input(PIR) = 0:

        GPIO.output(LIGHT, GPIO.LOW)} 

因此,在这个程序中,我们所做的就是添加了一个for循环,它会在设定的时间内打开灯光。这段时间有多长可以通过改变变量TIME的值来切换。

在那个循环中还有一个有趣的部分,如下所示:

 if GPIO.input(PIR) == 1
            M_final = M_final + 1 

你可能会想为什么我们要这样做?每当灯光被打开时,它将保持开启 5 分钟。然后,它将关闭并等待运动发生。因此,基本上,这段代码的问题是,如果您在房间里,灯光打开后,它将在 5 分钟内查看是否有运动被检测到。有可能在 5 分钟后寻找运动时您正在运动。但大多数情况下,这不会发生。因此,我们使用 PIR 传感器来检测运动。每当检测到运动时,通过M_final = M_final + 1这一行来增加M_final的值,从而延长灯光打开的时间。

理解运动

到目前为止,您一定已经意识到 PIR 传感器并不是我们打开或关闭灯光的最理想传感器。主要是因为,尽管运动是存在的最佳指标之一,但有时您可能根本不会移动,例如休息、阅读书籍、观看电影等。

现在我们该怎么办?嗯,我们可以做一个小技巧。还记得在上一章中我们使用我们的接近传感器来感知一个人是否穿过了特定区域吗?我们将在这里植入类似的逻辑;但不只是简单地复制粘贴代码,我们将改进它,使其变得更好。

因此,我们将使用两个红外接近传感器,而不是使用一个。安装如下图所示:

现在很明显,每当有人从门边走进房间边时,传感器 1在检测到人体时会显示较低的读数。然后,当他朝房间一侧走去时,传感器 2将显示类似的读数。

如果首先触发传感器 1,然后触发传感器 2,那么我们可以安全地假设这个人是从门边走向房间边。同样,如果相反发生,那么可以理解这个人是从房间里走出去。

现在,这相当简单。但是我们如何在现实生活中实现它呢?首先,我们需要按以下方式连接电路:

一旦完成,上传以下代码:

import GPIO library
import RPi.GPIO as GPIO
import time

import Adafruit_ADS1x15 adc0 = Adafruit_ADS1x15.ADS1115()   GAIN = 1
LIGHT = 23 adc0.start_adc(0, gain=GAIN) adc1.start_adc(1, gain=GAIN)

GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)

while True:

 F_value = adc0.get_last_result()  F1 = (1.0  / (F_value /  13.15)) -  0.35

   time.sleep(0.1)

 F_value = adc0.get_last_result()  F2 = (1.0  / (F_value /  13.15)) -  0.35

   F0_final = F1-F2

   if F0 > 10 :

        Time0 =  time.time()

 F_value = adc1.get_last_result()  F1 = (1.0  / (F_value /  13.15)) -  0.35

   time.sleep(0.1)

 F_value = adc1.get_last_result()  F2 = (1.0  / (F_value /  13.15)) -  0.35

   F1_final = F1-F2

   if F1 > 10: 

 Time1 =  time.time()

    if Time1 > Time0:

        GPIO.output(LIGHT, GPIO.HIGH)

    if Time1 < Time0:

        GPIO.output(LIGHT, GPIO.LOW)      }

现在,让我们看看我们在这里做了什么。和往常一样,大部分语法都非常简单明了。最重要的部分是逻辑。因此,让我们逐步了解我们在做什么。

 F_value = adc0.get_last_result()  F1 = (1.0  / (F_value /  13.15)) -  0.35

   time.sleep(0.1)

 F_value = adc0.get_last_result()  F2 = (1.0  / (F_value /  13.15)) -  0.35

在上面的代码行中,我们正在获取红外接近传感器的值,并计算相应的距离,将该值存储在一个名为F1的变量中。一旦完成,我们将使用time.sleep(0.1)函数停止 0.1 秒。然后,我们再次从同一传感器读取并将值存储在名为F2的变量中。为什么我们要这样做?我们在之前的章节中已经理解了。

 F0_final = F1-F2

一旦获得了F1F0的值,我们将计算差值以找出是否有人通过。如果没有人通过,那么读数几乎相同,差异不会很大。但是,如果有人通过,那么读数将是相当大的,并且该值将存储在一个名为F0_final的变量中。

 if F0 > 10 :

        Time0 =  time.time()

如果F0的值或第一次和第二次读数之间的距离差大于 10 厘米,则if条件将为真。一旦为真,它将将Time0变量的值设置为当前时间值。time.time()函数将记录下确切的时间。

 F_value = adc1.get_last_result()  F1 = (1.0  / (F_value /  13.15)) -  0.35

   time.sleep(0.1)

 F_value = adc1.get_last_result()  F2 = (1.0  / (F_value /  13.15)) -  0.35 
 F1_final = F1-F2   if F1 > 10: 

 Time1 =  time.time()

现在,我们将对传感器 2执行完全相同的步骤。这里没有什么新的要告诉的;一切都很简单明了。

    if Time1 > Time0:

        GPIO.output(LIGHT, GPIO.HIGH)

一旦所有这些都完成了,我们比较Time1 > Time0。为什么我们要比较呢?因为Time0传感器 1的记录时间。如果人在里面移动,那么传感器 1将首先被触发,然后传感器 2将被触发。因此,传感器 2的记录时间会更长,相对于传感器 1来说更早。如果发生这种情况,那么我们可以假设人正在进来。如果有人进来,我们只需要打开灯,这正是我们在这里要做的。

    if Time1 < Time0:

        GPIO.output(LIGHT, GPIO.LOW)

同样,当一个人走出去时,首先触发的传感器将是传感器 2,然后将触发传感器 1。使得记录在Time1中的时间比Time2更早;因此,每当这个条件为真时,我们就会知道这个人正在离开房间,灯可以关闭。

继续安装在门附近,看看它的反应。我相信这将比我们之前通过 PIR 做的要好得多。玩得开心,并尝试找出它可能存在的任何缺陷。

完善运动

你能在以前的代码中找到任何缺陷吗?它们并不难找到;当房间里只有一个人时,代码运行得很好。但是如果安装在有多人出入的地方,可能会有挑战。这是因为每当有人走出去时,灯就会熄灭。

现在问题显而易见,是时候让代码变得更加智能了。为了做到这一点,硬件将保持完全相同;我们只需要让代码更加智能。让我们看看我们可以如何做到:

import GPIO library
   import RPi.GPIO as GPIO
   import time
   import time
   import Adafruit_ADS1x15
   adc0 = Adafruit_ADS1x15.ADS1115()
GAIN = 1
 adc0.start_adc(0, gain=GAIN)
adc1.start_adc(1, gain=GAIN)
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
PCount = 0
while True:
   F_value = adc0.get_last_result()
   F1 = (1.0 / (F_value / 13.15)) - 0.35
   time.sleep(0.1)
   F_value = adc0.get_last_result()
   F2 = (1.0 / (F_value / 13.15)) - 0.35
   F0_final = F1-F2
   if F0 > 10 :
        Time0 = time.time()
   F_value = adc1.get_last_result()
   F1 = (1.0 / (F_value / 13.15)) - 0.35
   time.sleep(0.1)
   F_value = adc1.get_last_result()
   F2 = (1.0 / (F_value / 13.15)) - 0.35
   F1_final = F1-F2
   if F1 > 10:
        Time1 = time.time()
    if Time1 > Time0:
        PCount = PCount + 1
    if Time1 < Time0:
        PCount = PCount - 1

if PCount > 0:

           GPIO.output(LIGHT, GPIO.HIGH)
       else if PCount = 0:
          GPIO.output(LIGHT, GPIO.LOW)        

我们所做的是非常基础的。我们声明了一个名为PCount的变量。这个变量被声明为计算房间或家里的人数。正如你在代码的前几行中所看到的,我们声明了PCount的值为0。我们假设一旦我们开始,房间内的人数将为0

    if Time1 > Time0:

        PCount = PCount + 1

每当条件if Time1 > Time0:满足时,PCount的值就会增加1。众所周知,只有当有人在房子里走动时,条件才会成立。

    if Time1 < Time0:

        PCount = PCount - 1

同样,当一个人在外面走的时候,条件if Time1 < Time0:是真的;每当这种情况发生时,PCount的值就会减少1

    if PCount > 0:

       GPIO.output(LIGHT, GPIO.HIGH)

现在我们已经开始计算房间内的人数,我们现在应用条件,如果PCount的数量大于0,则会打开。因此,当房屋内的人数大于0时,灯将亮起。

    else if PCount = 0:

       GPIO.output(LIGHT, GPIO.LOW)

以非常相似的方式,如果PCount的值或者房屋内的人数达到0,灯将被关闭。

因此,完美!

控制强度

我们现在已经控制了很多灯。现在是时候控制我们的风扇和其他空气循环系统了。每当我们谈论风扇或任何其他空气循环设备时,本质上我们在谈论电机。正如我们之前学到的,电机是简单的设备,可以使用电机驱动器非常容易地进行控制。但是你知道,当时我们控制的是直流电机。直流电机是非常简单的设备。但是当我们谈论我们的家用电器时,那么大多数这些设备将使用交流电或交流电流。我假设你一定知道那是什么,以及它与直流电的区别。

现在你知道我们家用的电机是交流电机,你也必须考虑到他们的控制机制将与直流电机大不相同。如果你这样想,你是对的。然而,电子产品的好处是,没有什么真的困难或复杂。基本原理基本上是一样的。所以,让我们看看如何在交流电源中控制电机的速度。

正如我们之前所见,我们可以简单地给直流电机一个 PWM 信号,电机将以 PWM 信号的平均电压速度运行。现在,你一定在想,这也可以应用于交流。事实是,是的,如果你想控制灯或类似设备,这是可以做到的,这些设备在波形失真的情况下没有任何主要特性变化。然而,当我们谈论其他组件时,我们遇到了一个大问题。交流波形看起来像这样:

这基本上意味着电位定期变化。在大多数家庭中,这是每秒 50 次。现在,想象一下,如果我们有一个 PWM 控制的设备,它在特定间隔开关电路,只允许电源通过。然后,正弦波的不同部分将传递到最终输出。

正如你在前面的 PWM 中所看到的,幸运的是 PWM 信号与交流电源的相位匹配;然而,由于这个原因,只有相位的正端被传输到最终输出,而不是负端。这将给我们的负载造成严重问题,有很大的机会连接的设备将无法工作。

我们还有另一个例子,其中 PWM 是随机的,它让波的随机部分通过。在这种情况下,我们可以清楚地看到随机地传输波的任何部分,正负端电压不同步,这将是一个巨大的问题。因此,我们不使用 PWM,而是使用一些非常有趣的东西。

最常用的方法称为相位触发控制。有时也称为相角控制或相位切割。它的本质是在相位的某些部分切割波,让其余的波通过。困惑吗?让我在这里给你展示:

现在,正如你所看到的,交流波的后半部分的相位被切割了,没有传递到最终输出。这使得最终输出只有总输入的 50%。这种技术的作用是,在减小总体输出电压的同时,保持电源的交流特性。同样,如下图所示,波在已经传递了 75%后被切割。这导致输出相对较低:

现在你可能会问,我们到底是如何做到这一点的?这是通过一个相对复杂的电路来完成的,它检测波的相位角,然后打开或控制一个双向高功率半导体晶闸管。这导致电源在某些相位通过或停止。我们将把这个电路的确切工作留到下一次,因为它相当复杂,与本书无关。

现在来到基本点,我们知道相位切割是什么,我们也知道晶闸管是让我们做到这一点的基本设备。但如何使用树莓派来实现这一点是个问题。

首先,我们需要一个交流调光模块。这个模块已经具备了相位检测和切割的所有组件。所以我们需要做的就是简单地使用 PWM 来控制它。

虽然我可能不需要演示如何连接电路或代码应该是什么,但为了理解起见,让我们使用这个模块将灯泡连接到我们的 Arduino,然后控制灯泡。现在,首先要记住的是负载应该是灯泡,而不是其他任何东西,比如 LED 灯。所以继续按照下图所示连接电路:

完成后,上传以下代码:

import RPi.GPIO as GPIO
import time                             
GPIO.setmode(GPIO.BCM)       
GPIO.setup(18,GPIO.OUT)         
I = 0
pwm= GPIO.PWM(18,50)

for I < 100:

    I = I+1
    pwm.start(I)
    time.sleep(0.1)

GPIO.cleanup()}

预期的是,连接的灯将首先微弱发光,然后逐渐增加强度,直到达到 100%。控制这样一个复杂的过程是如此简单。

智能温度控制

现在基础知识已经掌握,让我们继续使用这个系统构建有意义的东西。将空调设置到完美的温度是不是很困难?无论你做什么,最终都感觉不是最舒适的位置。这是由于身体在一天中温度的生理变化所致。

当你醒来时,你的体温相对较低。它比正常体温低多达 1°F。随着一天的进展,体温会上升,直到你上床睡觉。一旦你入睡,你的体温又开始下降,直到早上 4:00-6:00 达到最低点。这就是为什么当你上床睡觉时感觉温暖,但醒来时可能会感觉很冷的原因。现代空调有一个叫做睡眠模式的功能。它的作用是通过整个夜晚逐渐提高温度,这样你在任何时候都不会感到寒冷。但它的工作效果如何也是一个问题。

现在我们对机器人技术非常了解,我们将继续制作一个系统,来照顾一切。

在这部分,我们将空调和风扇连接在一起,这样它们可以一起工作,让你睡得更好。现在,在直接开始之前,我想让你看一下继电器上标明的额定值。正如你所看到的,继电器只能处理 250V 和 5 安培。现在,如果你查看空调的宣传册,你很容易就能明白我为什么要向你展示所有这些。空调的功耗将远远高于你的继电器所能承受的。因此,如果你尝试使用普通继电器来运行空调,那么你肯定会把继电器烧坏。你的电器可能的电流等级低于你的继电器。但是对于任何带有电机的设备,要记住该设备的初始功耗远高于额定功耗。因此,如果你的空调需要额定 10 安培,那么起动负载可能高达 15 安培。你可能会想,这不是问题,为什么我们不购买一个额定更高的继电器呢。好吧,正确!这正是我们将要做的。但是,电子设备的命名有时可能会很棘手。处理更高功率更高电压的电机开关设备通常被称为接触器,而不是继电器。从技术上讲,它们有相同的工作原理;然而,在这一点上的构造差异,这不是我们关心的问题。因此,我们将使用接触器来控制空调开关和调速器来控制风扇速度。既然这一点已经澄清,让我们继续并按照以下图表连接硬件:

import RPi.GPIO as GPIO import time import Adafruit_DHT GPIO.setmode(GPIO.BCM) FAN = 18
AC = 17 pwm= GPIO.PWM(18,50)  GPIO.setup(FAN,GPIO.OUT) GPIO.setup(AC, GPIO.OUT)   while True: humidity, temperature = Adafruit_DHT.read_retry(sensor, pin)

    if temperature =>20 && temperature <=30: Duty = 50 + ((temperature-25)*10)
  pwm.start(Duty)

    if temperature <22 :

         GPIO.output(AC, GPIO.LOW)

    if temperature >= 24

         GPIO.output(AC, GPIO.HIGH)}

这里使用的逻辑非常基本。让我们看看它在做什么:

 humidity, temperature = Adafruit_DHT.read_retry(sensor, pin)

    if temperature =>20 && temperature <=30: Duty = 50 + ((temperature-25)*10)
  pwm.start(Duty)

在这里,我们获取了湿度温度的值。到目前为止一切都很好,但我们能否更进一步,使它变得更智能?以前的逻辑可能已经帮助你睡得更好,但我们能否让它对你来说更加完美?

我们身体中有多个指标可以让我们了解身体的状态。例如,如果你累了,你可能不会走得很快或者说得很大声。相反,你会做相反的事情!同样,有多个因素表明我们的睡眠周期是如何进行的。

其中一些因素是:体温、呼吸频率、快速动眼期睡眠和身体运动。测量准确的体温或呼吸频率和快速动眼期睡眠是一项挑战。但是当我们谈论身体运动时,我认为我们已经完善了。因此,基于身体运动,我们将感知我们的睡眠质量以及需要进行何种温度调节。

如果你注意到,每当有人睡觉并开始感到冷时,身体会呈胎儿姿势并且动作会少得多。这是自动发生的。然而,当一个人感到舒适时,会有一些不可避免的动作,比如翻身和手臂或腿部的运动。当一个人感到冷时,这是不会发生的。因此,通过这些动作,我们可以判断一个人是否感到冷。现在我们已经了解了身体的生理变化,让我们尝试围绕它构建一个程序,看看我们能实现什么。

为了做到这一点,首先,我们需要按照以下方式连接电路:

完成这些后,继续编写以下代码:

import RPi.GPIO as GPIO import time import Adafruit_DHT GPIO.setmode(GPIO.BCM) FAN = 18
AC = 17
PIR = 22 PIN = 11
Sensor = 4 pwm= GPIO.PWM(18,50)  GPIO.setup(FAN,GPIO.OUT) GPIO.setup(AC, GPIO.OUT)   while True: humidity, temperature = Adafruit_DHT.read_retry(sensor, pin) H = datetime.datetime.now().strftime('%H') 
M = datetime.datetime.now().strftime('%M')

    if H <= 6 && H <= 22:

        if M <=58 : M = datetime.datetime.now().strftime('%M') humidity, temperature = Adafruit_DHT.read_retry(sensor, pin)
 if GPIO.input(PIR) == 0 :

                Movement = Movement + 1
                time.sleep(10)

           if temperature < 28: if Movement > 5 :

                    Duty = Duty + 10 pwm.start(Duty)
                    Movement = 0     

        if M = 59 : 

            if Movement = 0 :

                Duty = Duty -10
                pwm.start(Duty)

            Movement = 0

        if temperature <22 :

           GPIO.output(AC, GPIO.LOW)

       if temperature >= 24 && H <= 6 && H >= 22:

           GPIO.output(AC, GPIO.HIGH)

        if temperature > 27

            pwm.start(100)

    for H > 7 && H < 20 

        GPIO.output(AC, GPIO.LOW)

    if H = 20 

        GPIO.output(AC,GPIO.HIGH)
  }

让我们来看看引擎盖下面发生了什么:

 if H <= 6 && H <= 22:

        if M <=58 : M = datetime.datetime.now().strftime('%M') humidity, temperature = Adafruit_DHT.read_retry(sensor, pin)

你会看到的第一件事是我们有一个条件:if H,= 6 && H<= 22:。只有在时间范围在上午 10 点到晚上 6 点之间时,这个条件才会成立。这是因为这是我们通常睡觉的时间。因此,在这个条件下的逻辑只有在睡觉的时候才会起作用。

第二个条件是如果 M <= 58,只有当时间在058分钟之间时才为真。因此,当时间为M = 59时,这个条件将不起作用。我们将看到为什么要有这个逻辑的原因。

此后,我们正在计算时间并将值存储在一个名为M的变量中。我们还在计算湿度和温度值,并将其存储在名为temperaturehumidity的变量中:

 if GPIO.input(PIR) == 0 :

                Movement = Movement + 1
                time.sleep(10) 

现在,在这一行中,我们正在实施一个条件,如果从 PIR 读取到的值很高,那么条件将为真。也就是说,会检测到一些运动。每当这种情况发生时,Movement变量将增加1。最后,我们使用time.sleep(10)函数等待10秒。这是因为 PIR 可能会在短暂的时间内保持高电平。在这种情况下,条件将一遍又一遍地为真,从而多次增加Movement的值。

我们增加Movement的值的目的是为了计算人移动的次数。因此,在一个时间内多次增加它将违背这个目标。

 if temperature < 28: if Movement > 5 :

                    Duty = Duty + 10 pwm.start(Duty)
                    Movement = 0

现在我们有另一个条件,即如果温度<28。对于条件何时为真,不需要太多解释。因此,每当条件为真,如果计数的Movement次数超过5,那么Duty的值将增加10。因此,我们将 PWM 发送到空调调光器,从而增加风扇的速度。最后,我们将Movement的值重置为0

因此,我们只是在计算移动次数。只有当温度低于 28°C 时才计算这一移动。如果移动次数超过5,那么我们将增加风扇速度 10%。

        if M = 59 : 

            if Movement = 0 :

                Duty = Duty -10
                pwm.start(Duty)

            Movement = 0

在前一节中,逻辑只有在时间在058之间时才有效,也就是计数将发生的时间。当M的值为59时,那么条件if Movement = 0将被检查,如果为真,那么Duty的值将减少10。这将减慢风扇的速度 10%。此外,一旦执行了这个条件,Movement的值将被重置为0。因此,下一个小时可以开始一个新的循环。

基本上,这意味着计数将以小时为单位进行。如果Movement超过5,那么Duty的值将立即增加。但是,如果不是这种情况,程序将等待直到分钟接近59的值,每当发生这种情况时,它将检查是否有任何运动,如果有,风扇速度将降低。

        if temperature <22 :

           GPIO.output(AC, GPIO.LOW)

        if temperature >= 24 && H <= 6 && H >= 22: 

           GPIO.output(AC, GPIO.HIGH)

        if temperature > 27

            pwm.start(100)

所有这些代码都非常直接。如果温度低于22,则空调将关闭。此外,如果温度等于或超过24,并且时间在晚上 10:00 到早上 6:00 之间,则空调将打开。最后,如果温度超过27,则风扇将以 100%的速度打开。

    for H > 7 && H < 20 

        GPIO.output(AC, GPIO.LOW)

    if H = 20 

        GPIO.output(AC,GPIO.HIGH)

最后,我们通过使用条件for H > 7 && H <20来确保在这段时间内空调始终处于关闭状态。此外,如果H = 20,则应打开空调,以便在准备睡觉之前冷却房间。

添加更多

正如你现在可能已经了解的那样,我们可以根据自己的需求控制任何空调电器。我们已经理解了开关,并且已经完善了我们可以改变灯光强度和风扇速度的方式。但你有没有注意到一件事?随着我们的系统变得越来越复杂,所需的 GPIO 数量将会增加。总有一个时刻,你会想要连接更多的设备到你的树莓派上;然而,由于物理端口的不足,你将无法这样做。

这在电子学中是非常常见的情况。和往常一样,这个问题也有解决方案。这个解决方案被称为复用器。复用器的基本工作是在任何计算机系统中扩大端口的数量。现在你一定在想,它是如何做到的呢?

这个概念非常简单。让我们首先看一下复用器的图表:

在上图中,您可以看到复用器有两端—一个是信号输出线,另一个是相对的。我们需要首先了解的是,复用器是一个双向设备,即它从复用器向连接的设备发送数据,反之亦然。

现在,首先是电源线,这很基本。它用于给复用器本身供电。然后,我们有信号线,它有两个端口,SigENEN代表使能,这意味着在EN不高的情况下,数据通信也不会发生。然后我们有一个叫做Sig的东西。这是连接到树莓派 GPIO 的用于数据通信的端口。接下来是选择线。正如您所看到的,我们有四个端口,分别是S0S1S2S3。选择线的目的是选择需要选择的特定端口。以下是一个将澄清发生了什么的表:

S0 S1 S3 S4 选定输出
0 0 0 0 C0
1 0 0 0 C1
0 1 0 0 C2
1 1 0 0 C3
0 0 1 0 C4
1 0 1 0 C5
0 1 1 0 C6
1 1 1 0 C7
0 0 0 1 C8
1 0 0 1 C9
0 1 0 1 C10
1 1 0 1 C11
0 0 1 1 C12
1 0 1 1 C13
0 1 1 1 C14
1 1 1 1 C15

在上表中,您可以看到通过在选择线上使用各种逻辑组合,可以寻址各种线路。例如,假设我们在选择引脚上有以下序列—S0 = 1,S1 = 0,S2 = 1,S3 = 1。如果这是来自树莓派的选择引脚的输入,那么将选择引脚号 C13。这基本上意味着现在 C13 可以与复用器的引脚Sig进行数据通信。此外,我们必须记住,使能引脚必须高才能进行数据传输。

以类似的方式,我们可以继续处理复用器的所有 16 个引脚。因此,从逻辑上看,通过使用树莓派的六个引脚,我们可以继续利用 16 个 GPIO。既然我们已经了解了复用的基础知识,让我们继续尝试使用其中的一个。

一旦硬件连接好了,让我们继续上传以下代码:

import RPi.GPIO as GPIO import time  
GPIO.setmode(GPIO.BCM) GPIO.setwarnings(False) S0 = 21 S1 = 22 S2 = 23 S3 = 24 GPIO.setup(S0,GPIO.OUT) GPIO.setup(S1,GPIO.OUT) GPIO.setup(S2,GPIO.OUT) While True:  GPIO.output(S0,1) GPIO.output(S1,0) GPIO.output(S2,1) GPIO.output(S4,1) time.sleep(1) GPIO.output(S0,1) GPIO.output(S1,1) GPIO.output(S2,1) GPIO.output(S4,1) time.sleep(1) GPIO.output(S0,1) GPIO.output(S1,0) GPIO.output(S2,0) GPIO.output(S4,1) time.sleep(1) 'GPIO.output(S0,0) GPIO.output(S1,0) GPIO.output(S2,0) GPIO.output(S4,1) time.sleep(1) GPIO.output(S0,0) GPIO.output(S1,1) GPIO.output(S2,0) GPIO.output(S4,1) time.sleep(1) }

在这里,我们所做的实质上是,逐个触发选择线,以寻址 LED 连接的每个单个端口。每当发生这种情况时,相应的 LED 会发光。此外,它发光的原因是因为信号端Sig连接到树莓派的 3.3V。因此,向其连接的任何端口发送逻辑高电平。

这是复用器工作的基本方式之一。当我们使用多个设备和传感器时,这可能非常有用。

总结

在本章中,我们使 Jarvis 能够在不同条件下自动化您的家用电器,并将各种属性应用于系统。因此,请继续尝试许多其他情景,以增强您的家庭自动化系统。

在下一章中,我们将启用 Jarvis IoT,从而使用 Wi-Fi 和互联网从您的手机控制电器。

第二十六章:使贾维斯成为物联网设备

曾经我们曾经想象用手指控制世界。现在,这种想象已经成为现实。随着智能手机的出现,我们已经在做一些在十年前只能想象的事情。随着手机变得智能,行业和企业也尽力跟上这种颠覆性的变化。然而,仍然有一部分落后了。那是哪一部分?你的家!

想想你可以用智能手机控制家里的什么?并不多!有一些设备可以打开或关闭一堆设备,比如你的空调。然而,这个清单是详尽的。因此,凭借在前几章中获得的所有知识和我们手中强大的硬件,为什么我们不成为引领潮流和颠覆者,创造一些仍然只存在于我们想象中的东西呢。

本章将涵盖以下主题:

  • 物联网IoT)的基础知识

  • 消息队列遥测传输MQTT)协议

  • 设置 MQTT 代理

  • 制作基于物联网的入侵检测器

  • 控制家庭

物联网的基础知识

在本章中,我们将使用智能手机控制家里的设备,但在这之前,我们应该了解这项技术的基础知识。本章的第一个主题是物联网——现代世界中被滥用的行话。这是每个人都想了解但却没有人知道的东西。物联网可以与一种技术相关联,你的冰箱会告诉你哪些物品供应不足,并会自动为你订购。可怜的东西!这项技术还需要一些时间来进入我们的家。但物联网不仅仅意味着这个。物联网是一个非常广泛的术语,几乎可以应用于所有的地方进行优化。那么,物联网是什么呢?

让我们来解释一下这个缩写,物联网,有时也被称为网络物理系统。那么,什么是?在这里,任何有能力在没有人类干预的情况下收集或接收数据的电子物体都可以被称为物。因此,这个物可以是你的手机、心脏起搏器、健康监测设备等等。唯一的条件是它必须连接到互联网并具有收集和/或接收数据的能力。第二个术语是互联网;互联网指的是互联网,废话!现在,所有这些物联网设备都会向云端或中央计算机发送和接收数据。它之所以这样做,是因为任何物联网设备,无论大小,都被认为是资源受限的环境。也就是说,资源,比如计算能力,要少得多。这是因为物联网设备必须简单和便宜。想象一下,你必须在所有的路灯上安装物联网传感器来监控交通。如果设备的成本是 500 美元,那么安装这种设备是不切实际的。然而,如果它可以做到 5-10 美元,那么没有人会在意。这就是物联网设备的问题;它们非常便宜。现在,这个故事的另一面是,它们没有很多计算能力。因此,为了平衡这个方程,它们不是在自己的处理器上计算原始数据,而是将这些数据简单地发送到云计算设备或者服务器,这些数据在那里被计算,得出有意义的结果。所以,这样就解决了我们所有的问题。嗯,不是!这些设备的第二个问题是它们也可以是电池操作的一次性设备。例如,在森林的各个地方安装了温度传感器;在这种情况下,没有人会每周去更换电池。因此,这些设备是这样制作的,它们消耗很少甚至几乎没有电力,从而使编程变得非常棘手。

现在我们已经了解了物联网的概念,在本章中,我们将使我们的家居具备物联网功能。这意味着,我们将能够从家中的传感器接收和收集数据,在我们的移动设备上查看数据,并且如果需要,我们也可以使用智能手机控制设备。不过有一点,我们不会在云端进行计算,而是简单地将所有数据上传到云端,只需访问该数据或将我们的数据发送到云端,然后可以访问。我们将在另一本书中讨论云计算方面,因为这可能是一个全新的维度,超出了本书的范围。

MQTT 协议

MQTT 是 ISO 认证的协议,被广泛使用。这个协议的有趣之处在于,它是由 Andy Stanford 和 Arlen Nipper 于 1999 年为监控沙漠中的油管开发的。您可以想象,在沙漠中,他们开发的协议必须是节能和带宽高效的。

这个协议的工作方式非常有趣。它具有发布-订阅架构。这意味着它有一个中央服务器,我们也称之为代理。任何设备都可以向该代理注册并发布任何有意义的数据。现在,被发布的数据应该有一个主题,例如,空气温度。

这些主题特别重要。为什么,您可能会问?对于代理,可以连接一个或多个设备。连接时,它们还需要订阅一个主题。假设它们订阅了主题Air-Temperature。现在,每当有新数据到来时,它都会发布到订阅设备。

需要知道的一件重要事情是,与 HTTP 中的请求不同,无需请求来获取代理的数据。相反,每当接收到数据时,它将被推送到订阅该主题的设备。很明显,TCP 协议也将一直处于工作状态,并且与代理相关的端口将始终连接以实现无缝的数据传输。但是,如果数据中断,代理将缓冲所有数据,并在连接恢复时将其发送给订阅者。

如您所见,运动传感器和温度传感器通过特定主题即TemperatureMotion向 MQTT 服务器提供数据。订阅这些主题的人将从此设备获取读数。因此,实际传感器和移动设备之间不需要直接通信。

整个架构的好处是,可以连接无限数量的设备,并且不需要任何可扩展性问题。此外,该协议相对简单,即使处理大量数据也很容易。因此,这成为物联网的首选协议,因为它为数据生产者和数据接收者之间提供了一种简单、可扩展和无缝的连接。

设置 MQTT 代理

在这个主题中,让我们看看我们需要做什么来设置这个服务器。打开命令行,输入以下命令:

sudo apt-get update
sudo apt-get upgrade

一旦更新和升级过程完成,继续安装以下软件包:

sudo apt-get install mosquitto -y

这将在您的树莓派上安装 Mosquitto 代理。该代理将负责所有数据传输:

sudo apt-get install mosquitto-clients -y

现在,这行将安装客户端软件包。您可以想象,树莓派本身将是代理的客户端。因此,它将处理必要的事情。

我们现在已经安装了软件包;是的,确切地说,就是这么简单。现在,我们需要做的就是配置 Mosquitto 代理。要做到这一点,您需要输入以下命令:

sudo nano etc/mosquitto/mosquitto.conf

现在,这个命令将打开保存 Mosquitto 文件配置的文件。要进行配置,您需要到达此文件的末尾,您将看到以下内容:

include_dir/etc/mosquitto/conf.d

现在,您可以通过在这些行之前添加#来注释掉前面的代码行。完成后,继续添加以下行:

allow_anonymous false

password_file /etc/mosquitto/pwfile

listener 1883

让我们看看我们在这里做了什么。allow_anonymous false这一行告诉经纪人不是每个人都可以访问数据。接下来的一行,password_file /etc/mosquitto/pwfile告诉经纪人密码文件的位置,位于/etc/mosquitto/pwfile。最后,我们将使用listener 1883命令定义这个经纪人的端口,即1883

最后,我们已经完成了在树莓派中设置 MQTT 客户端。现在我们准备继续并将其用于物联网启用的家庭。

制作基于物联网的入侵检测器

现在树莓派已经设置好,我们准备将其启用物联网,让我们看看我们将如何连接系统到互联网并使其正常工作。首先,我们需要将树莓派连接到我们想使用物联网技术控制的设备。所以继续使用以下图表进行连接:

一旦您设置好所有的组件,让我们继续上传以下代码:

import time  import paho.mqtt.client as mqtt import RPi.gpio as gpio
pir = 23
gpio.setmode(gpio.BCM)
gpio.setup(pir, gpio.IN)
client = mqtt.Client() broker="broker.hivemq.com" port = 1883
pub_topic = "IntruderDetector_Home" def SendData():
  client.publish(pub_topic,"WARNING : SOMEONE DETECTED AT YOUR PLACE")   def on_connect(client, userdata, flag,rc):
  print("connection returned" + str(rc))   SendData() while True:
 client.connect(broker,port) client.on_connect = on_connect   if gpio.output(pir) == gpio.HIGH :
    SendData() client.loop_forever() 

与迄今为止我们看到的其他代码块不同,这段代码对你来说可能会很新。所以我将解释除一些明显的部分之外的每个部分。所以,让我们看看我们在这里有什么:

import paho.mqtt.client as mqtt

在这部分,我们将pho.mqtt.client库导入为mqtt。所以每当需要访问这个库时,我们只需要使用mqtt这一行,而不是整个库的名称。

client = mqtt.Client()

我们使用mqtt库的client方法定义了一个客户端。这可以通过client变量来调用。

broker="broker.hivemq.com"

所以我们正在程序中定义经纪人。对于这个程序,我们使用的经纪人是broker.hivemq.com,它为我们提供了经纪人服务。

port = 1883

现在,我们将再次定义协议将工作的端口,即在我们的情况下是1883

pub_topic = "IntuderDetector_Home"

在这里,我们定义了名为pub_topic的变量的值,即IntruderDetector_Home。这将是在代码运行时可以订阅的最终主题。

def SendData():
 client.publish(pub.topic, "WARNING : SOMEONE DETECTED AT YOUR PLACE")

在这里,我们定义了一个名为SendData()的函数,将数据Warning : SOMEONE DETECTED AT YOUR PLACE发布到我们之前声明的主题的经纪人。

def on_message(client, userdata, message):
  print('message is : ')
 print(str(message.payload)) 

在这一行中,我们定义了一个名为on_message()的函数,它将打印一个值message is :,后面跟着数据是什么。这将使用print(str(message.payload))这一行来完成。它的作用是打印传递给函数参数的任何内容。

 def on_connect(client, userdata, flag,rc):

     print("connection returned" + str(rc)) 
  SendData()

在这一行中,我们定义了on_connect()函数,它将打印connection returned一行,后面跟着rc的值。rc代表返回码。所以,每当消息被传递时,都会生成一个代码,即使没有,也会返回特定的代码通知错误。所以,可以将其视为确认。完成后,我们之前定义的SendData()函数将用于将数据发送到经纪人。

client.connect(broker,port)

connect()是 MQTT 库的一个函数,它将客户端连接到经纪人。这很简单。我们只需要传递我们想要连接的经纪人的参数和要使用的端口。在我们的情况下,broker = broker.hivemq.comport = 1883。所以当我们调用这个函数时,树莓派就连接到我们的经纪人了。

client.on_connect = on_connect 

这是程序的核心。client.on_connect函数所做的是,每当树莓派连接到经纪人时,它就开始执行我们定义的on_connect函数。这将连续不断地将数据发送到经纪人,每隔 5 秒一次,就像我们在函数中定义的方式一样。这个过程也被称为回调,它使其成为事件驱动。也就是说,如果它没有连接,它就不会尝试将数据发送到经纪人。

  if gpio.output(pir) == HIGH :
        sendData()

当 PIR 传感器变高或者检测到运动时,将调用sendData()函数,消息将被发送到代理,警告有人在你的地方被探测到。

client.loop_forever()

这是我最喜欢的功能,特别是因为它有可爱的名字。正如你所期望的,client.loop_forver()函数将继续寻找任何事件,每当检测到事件时,它将触发数据发送到代理。现在我们将看到这些数据的部分。为此,我们需要从 App Store(如果你使用 iOS)或 Playstore(如果你使用 android)下载MyMQTT应用程序。

一旦你启动应用程序,你将看到上面的屏幕。你需要填写代理 URL 的名称,在我们的例子中是broker.hivemq.com。然后,填写端口,在我们的例子中是1883

完成后,你将看到一个类似以下的屏幕:

只需添加你需要的订阅名称,即IntruderDetector_Home。完成后,你将看到魔法发生!

在下一节中,我们将基于物联网来控制事物;到时见。

控制家庭

最后,使用以下图表进行连接并上传以下代码:

import time
import paho.mqtt.client as paho
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setup(14,GPIO.OUT)
broker="broker.hivemq.com"
sub_topic = light/control
client = paho.Client()
def on_message(client, userdata, message):
    print('message is : ')
    print(str(message.payload))
    data = str(message.payload)
    if data == "on":
        GPIO.output(3,GPIO.HIGH)
    elif data == "off":
        GPIO.output(3,GPIO.LOW)

def on_connect(client,userdata, flag, rc):
    print("connection returned" + str(rc))
    client.subscribe(sub_topic)
client.connect(broker,port)
client.on_connect = on_connect
client.on_message=on_message
client.loop_forever()

现在,在这段代码中,我没有太多需要告诉你的;它非常直接了当。我们发送数据就像上次一样。然而,这次我们使用了一个新的函数。所以,让我们看看这段代码到底是什么:

def on_message(client, userdata, message):
       print('message is : ')
 print(str(message.payload)) data = str(message.payload) if data == "on": GPIO.output(3,GPIO.HIGH) elif data == "off": GPIO.output(3,GPIO.LOW)

在这里,我们定义了on_message()函数在做什么。函数有三个参数,消息将在这些参数上工作。这包括client,我们之前已经声明过;userdata,我们现在没有使用;最后是message,我们将通过智能手机通过互联网发送。

一旦你查看程序内部,这个函数将使用print('message is : ')print(str(message.payload))来打印消息。完成后,data的值将被设置为订阅者发送的消息。

这些数据将由我们的条件来评估。如果数据保持on,那么 GPIO 端口号3将被设置为HIGH,如果字符串是off,那么 GPIO 端口号3将被设置为LOW—简单来说,打开或关闭你的设备。

def on_connect(client,userdata, flag, rc):
    print("connection returned" + str(rc))
    client.subscribe(sub_topic)

我们之前也定义了on_connect()函数。然而,这次有些不同。我们不仅打印连接返回的值rc,还使用了另一个名为client.subscribe(sub_topic)的函数,它将让我们在程序中之前定义的特定主题上连接到代理。

client.on_message=on_message

由于整个算法是基于事件驱动系统,这个client.on_message函数将一直等待接收消息。一旦接收到,它将执行on_message函数。这将决定是否打开或关闭设备。

要使用它,只需继续发送基于主题的数据,它将被你的树莓派接收。

一旦接收到,决策函数on_message()将决定 MyMQTT 应用程序接收到了什么数据。如果接收到的数据是on,那么灯将被打开。如果接收到的数据是off,那么灯将被关闭。就是这么简单。

总结

在本章中,我们已经了解了物联网的基础知识以及 MQTT 服务器的工作原理。我们还制作了一个入侵者检测系统,无论你身在何处,只要有人进入你的家,它都会提醒你。最后,我们还创建了一个系统,可以通过简单的手机命令打开家中的设备。在下一章中,我们将让贾维斯能够让你根据你的声音与系统交互。

第二十七章:给 Jarvis 发声

曾经想过是否可以使用机器人来完成我们的工作吗?是的!在一些高科技小说或漫威电影甚至漫画书中肯定是可能的。所以,系好安全带,准备好迎接这个令人惊叹的章节,在这里,您将实际实现我刚才提到的内容。

本章将涵盖以下主题:

  • 基本安装

  • 自动交付答录机

  • 制作一个交互式门答录机器人

  • 让 Jarvis 理解我们的声音

基本安装

有各种方法和方法可以控制我们的智能家居 Jarvis,其中一些我们之前已经探讨过,比如通过控制它。因此,首先,我们需要准备我们的系统以能够进行语音合成;为此,让我们执行以下过程。

首先,转到终端并输入以下命令:

sudo apt-get install alsa-utils

这将安装依赖项alsa-utilsalsa-utils包包含各种实用程序,用于控制您的声卡驱动程序。

完成后,您需要编辑文件。为此,我们需要打开文件。使用以下命令:

sudo nano /etc/modules

完成后,将打开一个文件;在该文件的底部,您需要添加以下行:

snd_bcm2835

您不需要深究我们为什么这样做。它只是用来设置事情。我可以给你解释;但是,在这个激动人心的时刻,我不想让你感到无聊。

此外,如果你幸运的话,有时你可能会发现该行已经存在。如果是这种情况,就让它在那里,不要动它。

现在,要播放我们需要 Jarvis 说的声音,我们需要一个音频播放器。不,不是你家里的那种。我们说的是能够播放的软件。

要安装播放器,我们需要运行以下命令:

sudo apt-get install mplayer

好了,我们已经完成了音频播放器;让我们看看接下来要做什么。现在,我们需要再次编辑媒体播放器的文件。我们将使用相同的步骤打开文件并编辑它:

sudo nano /etc/mplayer/mplayer.conf

这将打开文件。与之前一样,只需添加以下行:

nolirc=yes

最后,我们需要给它一些声音,所以运行以下命令:

sudo apt-get install festvox-rablpc16k

这将为 Jarvis 安装一个 16 kHz 的英国男声。我们喜欢英国口音,不是吗?

完美。一旦我们完成了之前提到的所有步骤,我们就可以开始了。要测试声音,只需将 USB 扬声器连接到树莓派并运行以下代码:

import os
from time import sleep
os.system('echo "hello! i am raspberry pi robot"|festival --tts ')
sleep(2)
os.system('echo "how are you?"| festival --tts ')
sleep(2)
os.system('echo "I am having fun."| festival --tts ')
sleep(2)

好了,让我们看看我们实际做了什么:

import os

您可能已经发现,我们正在导入名为os的库。该库提供了一种使用操作系统相关功能的方法:

os.system('echo "Hello from the other side"|festival --tts ')

在这里,我们使用了一个名为system()的方法;它的作用是执行一个 shell 命令。也许你会想知道这是什么。shell 命令是用户用来访问系统功能并与之交互的命令。所以现在我们想要将文本转换为语音,我们将向这个函数提供两个参数。首先,文本是什么?在我们的例子中,它是Hello from the other side;我们这里的第二个参数是festival --tts。现在festival是一个库,tts代表文本到语音转换。因此,当我们将其传递给参数时,系统将知道要将传递给参数的文本从文本转换为语音。

就是这样!是的,就是这样。这就是我们让您的树莓派说话所需做的一切。

自动交付答录机

如今,我们都在网上订购东西。然而,无论亚马逊的流程有多么自动化,在谈论 2018 年时,我们仍然有人类将包裹送到我们的门口。有时,你希望他们知道一些关于放置包裹的地方。现在我们变得越来越自动化,过去你可能会在大门外留个便条的日子已经一去不复返了。是时候用我们的技术做些有趣的事情了。要做到这一点,我们几乎不需要做任何严肃的事情。我们只需要按照以下图示连接组件即可:

PIR 传感器必须放置在大门周围有运动时产生逻辑高电平的位置。

完成后,继续上传以下代码:

import RPi.GPIO as GPIO
import time
Import os
GPIO.setmode(GPIO.BCM)
PIR = 13
GPIO.setup(PIR,GPIO.IN)
while True:

  if GPIO.input(PIR) == 1 :
     os.system('echo "Hello, welcome to my house"|festival --tts ')
     time.sleep(0.2)
     os.system('echo "If you are a delivery agent then please leave the package here"|festival --tts ')
     time.sleep(0.2)
     os.system('echo "If you are a guest then I'm sorry I have to leave I will be back after 7pm"|festival --tts ')
     time.sleep(0.2)
     os.system('echo "also Kindly don't step over the grass, its freshly grown and needs some time"|festival --tts ')
     time.sleep(1)
     os.system('echo "Thank you !"|festival --tts ')

现在我们所做的非常简单。一旦 PIR 传感器产生逻辑高电平,就会发出特定的指令。无需解释。如果需要澄清,可以参考之前的代码。

制作一个互动门 - 回答机器人

在上一章中,我们使用了 PIR 传感器来感知任何人类活动,然而传感器的问题是,无论谁来了或离开了,它都会传递相同的消息。这基本上意味着,即使你在漫长的一天后回家,它最终也会问同样的问题。相当愚蠢,是吧?

因此,在本章中,我们将使用之前的存储库,将视觉和语音整合在一起,形成一个令人惊叹的二人组。在这个过程中,摄像头将识别大门上的人,并且会识别是否是人类和陌生人,如果是的话,它会传递你打算传达的消息。另一方面,如果是你,它会简单地让你通过并问候。但是,如果检测到人脸但无法识别,则会向站在摄像头前的人提供一系列指令。

要实现这一切,你只需要在门口安装一个摄像头和 PIR。PIR 基本上是用来激活摄像头的。换句话说,只有在检测到运动时摄像头才会被激活。这个设置非常简单,不需要使用任何 GPIO。只需固定摄像头和 PIR,然后上传以下代码即可。

import RPi.GPIO as GPIO
import time
Import os
import cv2
import numpy as np
import cv2

faceDetect = cv2.CascadeClassifier('haarcascade_frontalface_default.xml')
cam = cv2.VideoCapture(0)
rec = cv2.face.LBPHFaceRecognizer_create()
rec.read("recognizer/trainningData.yml")
id = 0

while True:

  GPIO.setmode(GPIO.BCM)
PIR = 13
GPIO.setup(PIR, GPIO.IN)

if GPIO.input(PIR) == 1:

  ret, img = cam.read()
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
faces = faceDetect.detectMultiScale(gray, 1.3, 5)
for (x, y, w, h) in faces:
  cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 2)
id, conf = rec.predict(gray[y: y + h, x: x + w])

if id == 1:
  id = "BEN"
os.system('echo "Hello, welcome to the house BEN"|festival --tts ')
time, sleep(0.2)

else :

  os.system('echo "If you are a delivery agent then please leave the package here"|festival --tts ')
time, sleep(0.2)

os.system('echo "If you are a guest then I'
    m sorry I have to leave I will be back after 7 pm "|festival --tts ')
    time, sleep(0.2)

    os.system('echo "also Kindly don'
      t step over the grass, its freshly grown and needs some time "|festival --tts ')
      time.sleep(1)

      os.system('echo "Thank you !"|festival --tts ') cv2.imshow("face", img) if cv2.waitKey(1) == ord('q'):
      break cam.release()

      cv2.destroyAllWindows()
faceDetect = cv2.CascadeClassifier('haarcascade_frontalface_default.xml')

在上述代码中,我们使用CascadeClassifier方法创建级联分类器,以便摄像头可以检测到人脸。

cam = cv2.VideoCapture(0)
rec = cv2.face.LBPHFaceRecognizer_create()

在上述代码中,我们使用cv2VideoCapture(0)方法从摄像头读取帧。此外,正在创建人脸识别器以识别特定的人脸。

 ret, img = cam.read()

现在使用cam.read()从摄像头读取数据,就像在之前的代码中所做的那样。

gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
faces = faceDetect.detectMultiScale(gray,1.3,5)

图像被转换为灰色。然后,faceDetect.detectMultiScale()将使用灰色转换的图像。

 for (x,y,w,h) in faces:
     cv2.rectangle(img, (x,y), (x+w, y+h), (0,0,255), 2)
     id, conf = rec.predict(gray[y:y+h, x:x+w])
     if id==1:
         id = "BEN" 
         os.system('echo "Hello, welcome to my house BEN"|festival --tts ')
         time, sleep(0.2)

当检测到人脸时,包含人脸的图像部分将被转换为灰色并传递给预测函数。该方法将告诉我们人脸是否被识别,如果识别出人脸,还会返回 ID。假设这个人是BEN,那么 Jarvis 会说你好,欢迎来到我的家 BEN。现在BEN可以告诉 Jarvis 打开灯,然后当唤醒词 Jarvis 被激活时,Jarvis 会做出回应。如果识别不出这个人,那么可能是个快递员。然后,执行以下命令:

os.system('echo "If you are a delivery agent then please leave the package here"|festival --tts ')
time, sleep(0.2)

os.system('echo "If you are a guest then I'm sorry I have to leave I will be back after 7pm"|festival --tts ')
 time, sleep(0.2)

os.system('echo "also Kindly don't step over the grass, its freshly grown and needs some time"|festival --tts ')
time.sleep(1)

os.system('echo "Thank you !"|festival --tts ')

让 Jarvis 理解我们的声音

声音是沟通的本质。它帮助我们在很短的时间内传输大量数据。它肯定比打字更快更容易。因此,越来越多的公司正在努力制作能够理解人类语音和语言并根据其工作的系统。这绝对不容易,因为语言中存在着巨大的变化;然而,我们已经走了相当长的路。因此,不用花费太多时间,让我们的系统准备好识别我们的声音。

因此,在这里,我们将使用来自 Google Voice 的 API。您可能知道,Google 非常擅长理解您说的话。非常字面意思。因此,使用他们的 API 是有道理的。现在,它的工作方式非常简单。我们捕获声音,然后将其转换为文本。然后,我们比较文本是否与配置文件中定义的内容相似。如果匹配任何内容,则将执行与其关联的 bash 命令。

首先,我们需要检查麦克风是否连接。为此,请运行以下命令:

lsusb

此命令将显示连接到 USB 的设备列表。如果您在列表上看到自己的设备,那么很好,您走上了正确的道路。否则,请尝试通过连接找到它,或者尝试其他硬件。

我们还需要将录音音量设置为高。要做到这一点,请继续输入以下命令:

alsamixer

现在一旦 GUI 弹出到屏幕上,使用箭头键切换音量。

最好由您自己听取录制的声音,而不是直接将其传输到树莓派。为此,我们首先需要录制我们的声音,因此需要运行以下命令:

arecord -l

这将检查摄像头是否在列表中。然后,输入以下命令进行录制:

arecord -D plughw:1,0 First.wav

声音将以First.wav的名称记录。

现在我们也想听一下我们刚刚录制的声音。这样做的简单方法是输入以下命令:

aplay test.wav

检查声音是否正确。如果不正确,您可以自由调整系统。

一旦我们完成了检查声音和麦克风,就该安装真正的工作软件了。有简单的方法可以做到这一点。以下是您需要运行的命令列表:

wget –- no-check-certificate “http://goo.gl/KrwrBa” -O PiAUISuite.tar.gz

tar -xvzf PiAUISuite.tar.gz

cd PiAUISuite/Install/

sudo ./InstallAUISuite.sh

现在当您运行此程序时,将开始发生非常有趣的事情。它将开始向您提出各种问题。其中一些将是直截了当的。您可以用正确的思维以是或否的形式回答。其他可能非常技术性。由于这些问题可能随时间而变化,似乎没有必要明确提及您需要填写的答案,但作为一个一般的经验法则——除非您真的想说不,否则给出肯定的答案。

好了,我们已经安装了软件。现在在继续进行该软件之前,让我们继续编写以下程序:

import RPi.GPIO as GPIO
import time
import os
GPIO.setmode(GPIO.BCM)
LIGHT = 2
GPIO.setup(LIGHT,GPIO.OUT)
GPIO.output(LIGHT, GPIO.HIGH)
os.system('echo "LIGHTS TURNED ON "|festival --tts')

每当此程序运行时,连接到 PIN 号为2的灯将被打开。此外,它将朗读灯已打开。将此文件保存为lighton.py

import RPi.GPIO as GPIO
import time
import os
GPIO.setmode(GPIO.BCM)
LIGHT = 23
GPIO.setup(LIGHT,GPIO.OUT)
GPIO.output(LIGHT, GPIO.LOW)
os.system('echo "LIGHTS TURNED OFF "|festival --tts')

同样,在此程序中,灯将被关闭,并且它将朗读灯已关闭。将其保存为lightoff.py

import RPi.GPIO as GPIO
import time
Import os
GPIO.setmode(GPIO.BCM)
FAN = 22
GPIO.setup(FAN,GPIO.OUT)
GPIO.output(LIGHT, GPIO.HIGH)
os.system('echo "FAN TURNED ON "|festival --tts')

现在我们也为风扇做同样的事情。在这个中,风扇将被打开;将其保存为fanon.py

import RPi.GPIO as GPIO
import time
Import os
GPIO.setmode(GPIO.BCM)
FAN = 22
GPIO.setup(FAN,GPIO.OUT)
GPIO.output(LIGHT, GPIO.LOW)os.system('echo "FAN TURNED OFF "|festival --tts')

我不需要为此解释相同的事情,对吧?正如您所猜到的,将其保存为fanoff.py

好了!当所有这些都完成后,然后输入以下命令来检查软件是否正确安装:

voicecommand -c 

树莓派响应唤醒词pi;让我们将其更改为jarvis。可以在打开配置文件后使用以下命令进行所有这些更改:

voicecommand -e. 

在该文件中,输入您自己的命令。在这里,让我们添加以下代码:

LIGHT_ON

LIGHT_OFF

FAN_ON

FAN_OFF

现在对于每个命令,定义动作。动作将是运行包含打开或关闭灯光和风扇的代码的 Python 文件。代码基本且简单易懂。将以下内容添加到文件中:

LIGHT ON = sudo python lighton.py

LIGHT OFF = sudo python lightoff.py

FAN ON = sudo python fanon.py

FAN OFF = sudo python fanoff.py

现在,让我们看看我们做了什么。每当你说“贾维斯,开灯”,它会将你的语速转换成文本,将其与相应的程序进行比较,并执行程序中的操作。因此,在这个程序中,每当我们说“开灯”,灯就会亮起,其他命令也是类似。记得让它听到你说的话。你必须说“贾维斯”这个词,这样它才会听从命令并准备倾听。

总结

在这一章中,我们了解了如何与贾维斯互动,并根据我们的需求让它工作。如果这一章是关于口头交流,那么下一章将是关于手势识别,利用先进的电容技术,你将能够通过挥手来控制你的自动化系统。

第二十八章:手势识别

自人类诞生以来,人们就使用手势相互交流,甚至在没有任何正式语言之前。手势是交流的主要方式,这也可以从世界各地发现的古代雕塑中看出,手势一直是一种非常有效地传递大量数据的成功方式,有时甚至比语言本身更有效。

手势是自然的,它们可能是对某种情况的反射。即使在我们不知道的情况下,它也会在潜意识中发生。因此,它成为了与各种设备进行交流的理想方式。然而,问题仍然存在,如何?

我们可以肯定,如果我们谈论手势,那么我们肯定需要做大量的编程来识别视频中的手势;此外,这也需要大量的处理能力来实现。因此,这是不可能的。我们可以使用一系列接近传感器构建一些基本的手势识别系统。然而,识别的手势范围将非常有限,使用的端口也会多倍增加。

因此,我们需要找到一个易于使用且成本不会超过其提供的解决方案。

本章将涵盖以下主题:

  • 电场感应

  • 使用 Flick HAT

  • 基于手势识别的自动化

电场感应

近场传感是一个非常有趣的传感领域。准备好一些有趣的东西。如果你感到有点困倦,或者注意力不集中,那就喝点咖啡,因为这个系统的工作原理可能会有点新。

每当有电荷时,就会伴随着一个相关的电场。这些电荷在空间中传播并绕过物体。当这种情况发生时,与之相关的电场具有特定的特征。只要周围的环境是空的,这种特征就会保持不变。

对于我们使用的手势识别板,它周围的感应范围只有几厘米,所以超出这一点的任何东西都可以忽略不计。如果那个区域没有任何东西,那么我们可以安全地假设被感应到的电场模式不会改变。然而,每当一个物体,比如我们的手,靠近时,这些波就会被扭曲。这种扭曲直接与物体的位置和姿势有关。通过这种扭曲,我们可以感应到手指的位置,并通过持续的感应,看到正在执行的动作是什么。所讨论的板看起来像这样:

板上的中央交叉区域是发射器,两侧是四个矩形结构。这些是感应元件。它们感应空间中的波纹模式。基于此,它们可以推导出物体的 x、y 和 z 坐标。这由一个名为 MGC 3130 的芯片提供动力。这个芯片进行所有计算,并将原始读数传递给用户,关于坐标。

使用 Flick HAT

Flick HAT 以盾牌的形式出现,你可以简单地将其插入树莓派并开始使用。然而,一旦你这样做了,你就不会剩下任何 GPIO 引脚。因此,为了避免这个问题,我们将使用公对母导线连接它。这将使我们可以访问其他 GPIO 引脚,然后我们可以玩得开心。

所以,继续按以下方式连接。以下是 Flick 板的引脚图:

然后,按照以下方式进行连接:

连接完成后,只需上传这个代码,看看会发生什么:

import signal
import flicklib
import time
def message(value):
   print value
@flicklib.move()
def move(x, y, z):
   global xyztxt
   xyztxt = '{:5.3f} {:5.3f} {:5.3f}'.format(x,y,z)
@flicklib.flick()
def flick(start,finish):
   global flicktxt
   flicktxt = 'FLICK-' + start[0].upper() + finish[0].upper()
   message(flicktxt)
def main():
   global xyztxt
   global flicktxt
   xyztxt = ''
   flicktxt = ''
   flickcount = 0
   while True:

  xyztxt = ''
  if len(flicktxt) > 0 and flickcount < 5:
      flickcount += 1
  else:
      flicktxt = ''
      flickcount = 0
main()

现在一旦你上传了代码,让我们继续了解这个代码实际在做什么。

我们正在使用一个名为import flicklib的库,这是由这块板的制造商提供的。这个库的函数将在本章中用于与挥动板通信和获取数据。

def message(value):
    print value

在这里,我们定义了一个名为message(value)的函数,它将简单地打印传递给函数的任何值:

@flicklib.move()

这有一个特殊的装饰器概念。根据定义,装饰器是一个接受另一个函数并扩展后者行为的函数,而不明确修改它。在上一行代码中,我们声明它是一个装饰器@

这有一个特殊的作用:动态定义程序中的任何函数。这意味着用这种方法定义的函数可以根据用户的定义而有不同的工作方式。

函数move()将进一步由在其后定义的函数补充。这种函数称为嵌套函数。也就是函数内部的函数:

def move(x, y, z):
    global xyztxt
    xyztxt = '{:5.3f} {:5.3f} {:5.3f}'.format(x,y,z)

在这里,我们定义了一个名为move()的函数,它的参数是xyz。在函数内部,我们定义了一个名为xyztxt的全局变量;现在,xyztxt的值将以五位数字的形式呈现,小数点后有三位。我们是如何知道的呢?正如你所看到的,我们使用了一个名为format()的函数。这个函数的作用是根据用户的要求格式化给定变量的值。我们在这里声明值为{:5.3f}:5表示它将是五位数,3f表示小数点后将是三位数。因此,格式将是xxx.xx

def flick(start,finish):
    global flicktxt
    flicktxt = 'FLICK-' + start[0].upper() + finish[0].upper()
    message(flicktxt)

在这里,我们定义了一个名为flick(start, finish)的函数。它有两个参数:startfinish。使用行flicktxt = 'FLICK-' + start[0].upper() + finish[0].upper(),这是根据手势板识别的字符进行切片。如果检测到南-北挥动,则开始为南,结束为北。现在我们只使用单词的第一个字符:

    global xyztxt
    global flicktxt

我们再次全局定义了名为xyztxtflicktxt的变量。之前,我们所做的是在函数中定义它。因此,重要的是在主程序中定义它:

if len(flicktxt) > 0 and flickcount < 5:
            flickcount += 1
else:
            flicktxt = ''
            flickcount = 0

当检测到手势时,flicktxt变量将获得与手势相对应的值。如果没有手势,那么flicktxt将保持为空。一个名为flickcount的变量将计算它被刷过多少次。如果值超出指定范围,那么flicktxt将使用行flicktxt = ''清除为空字符串,flickcount将被设为 0。

这将产生一个文本输出,向用户提供手势挥动的方向。

基于手势识别的自动化

现在我们已经按照以下图表接口了连接:

让我们继续上传以下代码:

import signal
import flicklib
import time
import RPi.GPIO as GPIO
GIPO.setmode(GPIO.BCM)
GPIO.setup(light, GPIO.OUT)
GPIO.setup(fan,GPIO.OUT)
pwm = GPIO.PWM(fan,100)
def message(value):
   print value
@flicklib.move()
def move(x, y, z):
   global xyztxt
   xyztxt = '{:5.3f} {:5.3f} {:5.3f}'.format(x,y,z)
@flicklib.flick()
def flick(start,finish):
   global flicktxt
   flicktxt = 'FLICK-' + start[0].upper() + finish[0].upper()
   message(flicktxt)
def main():
   global xyztxt
   global flicktxt
   xyztxt = ''
   flicktxt = ''
   flickcount = 0
   dc_inc = 0
   dc_dec = 0

while True:
  pwm.start(0)
  xyztxt = ' '
  if len(flicktxt) > 0 and flickcount < 5:
    flickcount += 1
  else:
    flicktxt = ''

flickcount = 0
if flicktxt ==”FLICK-WE”:
  GPIO.output(light,GPIO.LOW)
if flicktxt ==”FLICK-EW”:
  GPIO.output(light,GPIO.HIGH)
if flicktxt ==”FLICK-SN”:
  if dc_inc < 100:
    dc_inc = dc_inc + 10
    pwm.changeDutyCycle(dc_inc)

else:
  Dc_inc = 10
  if flicktxt ==”FLICK-NS”:
    if dc_inc >0:
    dc_dec = dc_dec - 10
    pwm.changeDutyCycle(dc_dec)
main()

该程序是在我们之前完成的程序的基础上,我们始终有一些额外的功能,可以使用通过手势板接收到的数据来开启或关闭灯光。

与之前的程序一样,我们正在以手势板上的方向形式接收手势,并使用简单的条件来关闭灯光或打开它们。因此,让我们看看有哪些添加:

 if flicktxt ==”FLICK-WE”: GPIO.output(light,GPIO.LOW)

第一个条件很简单。我们正在将flicktxt的值与给定变量进行比较,在我们的情况下是FLICK-WE,其中WE代表从西。因此,当我们从西向东挥动,或者换句话说,当我们从左向右挥动时,灯光将被关闭:

 if flicktxt ==”FLICK-EW”: GPIO.output(light,GPIO.HIGH)

与之前一样,我们再次使用名为FLICK-EW的变量,它代表从东到西的挥动。它的作用是,每当我们从东向西挥动手,或者从右向左挥动手时,灯光将被打开:

 if flicktxt ==”FLICK-SN”: if dc_inc <= 100:  dc_inc = dc_inc + 20
 pwm.changeDutyCycle(dc_inc)

现在我们已经加入了一个调光器和一个风扇来控制风扇的速度;因此,我们将不得不给它一个与我们想要驱动它的速度相对应的 PWM。现在每当用户将手从南向北或从下到上甩动时。条件 if dc_inc <100 将检查 dc_inc 的值是否小于或等于 100。如果是,则它将增加 20 的值。使用函数 ChangeDutyCycle(),我们为调光器提供不同的占空比;因此改变了风扇的整体速度。每次你向上划动风扇的值,它将增加 20%:

 else: Dc_inc = 10 if flicktxt ==”FLICK-NS”: if dc_inc >0:  dc_dec = dc_dec - 10
 pwm.changeDutyCycle(dc_dec)

摘要

在本章中,我们能够理解手势识别是如何通过电场检测工作的概念。我们也了解到使用手势控制板和手势控制家庭是多么容易。我们将在下一章中涵盖机器学习部分。

第二十九章:机器学习

从原始时代到现在,机器人和计算机都被编程来执行一系列活动。这些活动可能非常庞大。因此,为了开发复杂的程序,需要大量的软件工程师,他们日夜工作以实现某种功能。当问题定义明确时,这是可行的。但是当问题也变得非常复杂时呢?

学习是使我们成为人类的东西。我们的经验使我们能够以更好和更有效的方式适应各种情况。每次我们做某事,我们都会学到更多。这使我们在一段时间内更擅长做这项任务。俗话说熟能生巧,通过一遍又一遍地做事情来学习,使我们变得更好。

然而,让我们退一步来定义学习是什么?我想引用 Google 的说法,根据它的说法,学习是通过学习、经验或教导获得的知识。因此,学习基本上是一种从我们周围获取信息以理解过程及其性质的方式。

现在,你可能会想,等一下,在之前的章节中,当我们制作守卫机器人时,我们已经让我们的系统学习了很多视觉数据。你的想法是完全正确的。然而,学习可以通过不同的方式进行。对一个问题有效的方法对另一种问题可能是无效的。因此,有各种类型的学习算法和原则。在本章中,我们将专注于一种名为k 最近邻的算法。它被称为懒惰算法。我个人喜欢这个算法用于分类。为什么?因为从技术上讲,它没有训练阶段。怎么做?

k 最近邻实际上是一个聪明的算法。它不是计算所提供数据的回归并进行大量的数学计算,而是简单地从提供的数据集中获取结构化数据。每当有新的数据输入进行预测时,它只是根据用户提供的数据在数据库中搜索最接近的k匹配数据,基于其给定的分类。因此,在本章中,我们将学习这个算法将如何工作,以及我们如何使用它来使我们的家变得智能。

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

  • 制作数据集

  • 使用数据集进行预测

  • 让你的家学习

  • 家庭学习和自动化

制作数据集

现在,我们需要制作一个虚拟数据集,以便机器学习算法可以根据该数据预测应该做什么。

要制作数据集,我们需要了解正在考虑的数据是什么。在本章中,我们将基于时间和温度制作一个机器学习算法,以预测风扇应该开启还是关闭。因此,我们至少需要向系统提供两样东西,一样是“温度”,另一样是“时间”,以便进行预测。但要记住的一件事是,我们正在谈论一个监督学习算法,因此为了训练模型,我们还需要将“温度”和“时间”的结果提供给风扇的状态。在这里,风扇的状态可以是开启或关闭。因此,我们可以用01来表示。现在让我们继续自己制作一个数据集。

现在,要制作数据集,你只需打开 Microsoft Excel 并开始编写数据集如下:

最好拥有超过 20 组数据的数据集。此外,数据具有明显的特征并且不是随机数据是很重要的。例如,在前面的案例中,你可以看到在温度为28时,时间为12.44时,风扇将开启;然而,在同一时间,当时间为12.13且温度为21时,风扇是关闭的。

创建数据集后,您必须以 CSV 格式将其保存为名为dataset的文件。可能有一些用户不使用 Microsoft Excel,在这种情况下,您可以在文本编辑器中以相同格式编写数据,最后以 CSV 格式保存。

一旦您有了dataset.csv文件,那么您必须继续将它们复制到您将保存即将到来的代码的地方。完成后,我们可以继续下一步。

请记住,数据的质量越好,学习过程就越好。因此,您可能需要花一些时间来精心制作数据集,以便它确实有意义。

使用数据集进行预测

不多说了,让我们看看以下代码:

import numpy as np
import pandas as pd
from sklearn.neighbors import KNeighborsClassifier

knn = KNeighborsClassifier(n_neighbors=5)
data = pd.read_csv('dataset.csv')

x = np.array(data[['Time', 'Temp']])
y = np.array(data[['State']]).ravel()

knn.fit(x,y)

time = raw_input("Enter time")
temp = raw_input("Enter temp")

data =. []

data.append(float(time))
data.append(float(temp))

a = knn.predict([data])

print(a[0])}

所以,让我们看看我们在这里做了什么:

import numpy as np

我们将numpy导入到我们的程序中;这有助于我们处理列表和矩阵:

import pandas as pd

在这里,我们正在导入一个名为pandas的库;这有助于我们读取逗号分隔值或者叫 CSV 文件。我们将使用 CSV 文件来存储我们的数据并访问它进行学习过程:

from sklearn.neighbors import KNeighborsClassifier

在这里,我们从sklearn库中导入KneighborsClassifiersklearn本身是一个庞大的库;因此,我们只导入其中的一部分,因为在这个程序中我们不会使用全部内容:

knn = KNeighborsClassifier(n_neighbors=5)

在这里,我们正在给变量knn赋值,其中值将是KNeighborsClassifer(n_neighbors =5);这意味着它正在使用KneighborsClassifer()函数,并将参数设置为n_neighbors=5。这个参数告诉KneighborsClassifer函数算法中将有五个邻居。进一步使用这个声明,整个函数可以使用knn来调用:

data = pd.read_csv('dataset.csv')

在这里,我们为名为data的变量提供值,传递的值是pd.read_csv('dataset.csv');这意味着每当调用data时,将调用pandas库中的pd.read_csv()函数。这个函数的目的是从 CSV 文件中读取数据。在这里,传递的参数是dataset.csv;因此,它指示函数将从一个名为dataset.csv的文件中读取数据:

x = np.array(data[['Time', 'Temp']])

在下一行中,我们为变量x传递值,传递的值是np.array(data[['Time, 'Temp']])。现在,np.array函数通过numpy库创建一个数组。这个数组将存储名为TimeTemp的数据:

y = np.array(data[['State']]).ravel()

就像上一次一样,我们将State存储在通过numpy库的.ravel()函数创建的数组中,最后会转置数组。这样做是为了使两个数组xy之间可以进行数学运算:

knn.fit(x,y)

在这一小行中,我们使用了knn库中的fit()函数,它的作用是使用x作为主要数据,y作为输出结果数据来拟合模型:

time = raw_input("Enter time")
temp = raw_input("Enter temp")

在这一行中,我们正在向用户请求数据。在第一行,我们将打印输入时间,然后等待用户输入时间。用户输入时间后,它将被存储在名为time的变量中。一旦完成,它将继续下一行;代码将打印输入温度,一旦提示用户输入温度,它将等待数据被收集。一旦用户收集到数据,它将把数据存储在名为temp的变量中:

data =. []

在这里,我们正在创建一个名为data的空列表;这个列表将用于计算输出的结果状态。由于所有的机器学习算法都是以列表数据类型工作的。因此,决策的输入必须以列表的形式给出:

data.append(float(time))
data.append(float(temp))

在这里,我们正在向我们刚刚创建的名为data的列表中添加数据。首先添加time,然后是temp

a = knn.predict([data])

完成后,将使用knn算法中的名为predict的函数来根据提供的名为data的列表来预测输出。预测算法的输出将被提取到一个名为a的变量中:

print(a[0])

最后,一旦预测完成,我们将读取a的值,并记住所有的数据 I/O 都是以列表的形式进行的。因此,预测算法给出的数据输出也将以列表格式呈现。因此,我们打印列表的第一个元素。

此输出将根据用户提供的数据集预测风扇的状态。因此,继续输入温度和时间,让系统为您预测结果。看看它是否正常工作。如果不正常,那么尝试向 CSV 文件添加更多数据集,或者查看数据集中的值是否真的有意义。我相信您最终会得到一个出色的预测系统。

让您的家学习

一旦这个构想完成了,继续将其连接起来,如下所示:

设置好之后,是时候将以下代码写入我们的树莓派了:

import Adafruit_DHT
import datetime
import RPi.GPIO as GPIO
import time
import numpy as np
import pandas as pd
import Adafruit_DHT
from sklearn.neighbors import KNeighborsClassifier

GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)

fan = 22
light = 23
sw1 = 13
sw2 = 14

GPIO.setup(led1,GPIO.OUT)
GPIO.setup(led2,GPIO.OUT)
GPIO.setup(sw1,GPIO.IN)
GPIO.setup(sw2,GPIO.IN)

sensor = 11
pin = 2

f = open("dataset.csv","a+")
count = 0
while count < 50:

 data = ""

 H = datetime.datetime.now().strftime('%H')
 M = datetime.datetime.now().strftime('%M')

 data = str(H)+"."+str(M)
 humidity,temperature = Adafruit_DHT.read_retry(sensor,pin)
 data = data + "," + str(temperature)

prev_state = state

 if (GPIO.input(sw1) == 0) and (GPIO.input(sw2) == 0):
     state = 0
     GPIO.output(light,GPIO.LOW)
     GPIO.output(fan,GPIO.LOW)

 elif (GPIO.input(sw1) == 0) and (GPIO.input(sw2) == 1):
     state = 1
     GPIO.output(light,GPIO.HIGH)
     GPIO.output(fan,GPIO.LOW)

 elif (GPIO.input(sw1) == 1) and (GPIO.input(sw2) == 0):
    state = 2
     GPIO.output(light,GPIO.LOW)
     GPIO.output(fan,GPIO.HIGH)

 elif (GPIO.input(sw1) == 1) and (GPIO.input(sw2) == 1):
    state = 3
     GPIO.output(light,GPIO.HIGH)
     GPIO.output(fan,GPIO.HIGH)

 data = ","+str(state)

if prev_state =! state:

     f.write(data)
     count = count+1

f.close()

现在,让我们看看我们在这里做了什么:

f = open("dataset.csv","a+")

在这行代码中,我们将值open("dataset.csv", "a+")赋给变量f。然后,open()函数将打开传递给它的文件,我们的情况下是dataset.csv;参数a+表示将值附加到 CSV 文件的末尾。因此,这行代码将打开文件dataset.csv并添加我们稍后将传递的值:

 data = ""

我们通过名称data声明了一个空字符串:

 data = str(H)+"."+str(M)

我们正在将小时和分钟的值添加到字符串中,用点号分隔以进行区分。因此,数据看起来像HH.MM

 humidity,temperature = Adafruit_DHT.read_retry(sensor,pin)

我们使用这行代码从 DHT 11 传感器读取湿度和温度读数,并将这些值传递给变量humiditytemperature

data = data + "," + str(temperature)

一旦数据被读取,我们也将温度添加到变量data中。因此,现在数据看起来像这样HH.MMTT.TT

 if (GPIO.input(sw1) == 0) and (GPIO.input(sw2) == 0):
 state = 0
 elif (GPIO.input(sw1) == 0) and (GPIO.input(sw2) == 1):
 state = 1
 elif (GPIO.input(sw1) == 1) and (GPIO.input(sw2) == 0):
 state = 2
 elif (GPIO.input(sw1) == 1) and (GPIO.input(sw2) == 1):
 state = 3

在这里,我们定义了不同类型的状态,这些状态对应于开关组合。其表格如下:

开关 1 开关 2 状态
0 0 0
0 1 1
1 0 2
1 1 3

因此,通过状态的值,我们可以了解哪个开关将被打开,哪个将被关闭:

 data = ","+str(state)

最后,状态的值也被添加到名为data的变量中。现在,最终,数据看起来像HH.MMTT.TTS

f.write(data)

现在,使用write()函数,我们正在将数据的值写入到我们之前定义的文件中,该文件的值为f

因此,每次开关打开或关闭时,数据都将被收集,并且该值将以时间戳记录在文件中。这些数据随后可以用于在任何给定时间预测家庭的状态,而无需任何干预:

if prev_state =! state:

     f.write(data)
     count = count+1

在这里,我们正在将状态与prev_state进行比较,您可以在我们的程序中看到。先前的状态是在程序开始时计算的。因此,如果系统的状态发生任何变化,那么prev_statestate的值将不同。这将导致if语句为真。当发生这种情况时,数据将使用write()函数写入到我们的文件中。传递的参数是需要写入的值。最后,计数的值增加了1

一旦这个程序运行了几个小时或者可能是几天,它将收集关于灯光和风扇开关模式的一些非常有用的数据。此后,这些数据可以被获取到之前的程序中,程序将能够根据时间和温度做出自己的决定。

家庭学习和自动化

现在,在前面的部分中,我们已经了解了学习的工作原理,现在是时候利用这个概念制作一个能够自动理解我们的功能并做出决策的机器人了。基于我们的决定,系统将判断应该做什么。但这一次,而不是由用户提供一组数据,让这个程序自己创建数据。一旦数据对自己的功能似乎足够,那么,不用太多的解释,让我们直接开始吧:

import Adafruit_DHT
import datetime
import RPi.GPIO as GPIO
import time
import numpy as np
import pandas as pd
from sklearn.neighbors import KNeighborsClassifier

GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)

light = 22
fan = 23
sw1 = 13
sw2 = 14

GPIO.setup(light,GPIO.OUT)
GPIO.setup(fan,GPIO.OUT)
GPIO.setup(sw1,GPIO.IN)
GPIO.setup(sw2,GPIO.IN)

sensor = 11
pin = 2

f = open("dataset.csv","a+")
count = 0

while count < 200:

        data = ""

        H = datetime.datetime.now().strftime('%H')
        M = datetime.datetime.now().strftime('%M')

        data = str(H)+"."+str(M)
        humidity,temperature = Adafruit_DHT.read_retry(sensor,pin)
        data = data + "," + str(temperature)

prev_state = state

 if (GPIO.input(sw1) == 0) and (GPIO.input(sw2) == 0):
     state = 0
     GPIO.output(light,GPIO.LOW)
     GPIO.output(fan,GPIO.LOW)

 elif (GPIO.input(sw1) == 0) and (GPIO.input(sw2) == 1):
     state = 1
     GPIO.output(light,GPIO.HIGH)
     GPIO.output(fan,GPIO.LOW)

 elif (GPIO.input(sw1) == 1) and (GPIO.input(sw2) == 0):
    state = 2
     GPIO.output(light,GPIO.LOW)
     GPIO.output(fan,GPIO.HIGH)

 elif (GPIO.input(sw1) == 1) and (GPIO.input(sw2) == 1):
    state = 3
     GPIO.output(light,GPIO.HIGH)
     GPIO.output(fan,GPIO.HIGH)

 data = ","+str(state)

 if prev_state =! state:

     f.write(data)
     count = count+1

Test_set = []
knn = KNeighborsClassifier(n_neighbors=5)
data = pd.read_csv('dataset.csv')

X = np.array(data[['Time', 'Temp']])
y = np.array(data[['State']]).ravel()

knn.fit(X,y)

While Count > 200:

    time = ""

    H = datetime.datetime.now().strftime('%H')
    M = datetime.datetime.now().strftime('%M')

    time = float(str(H)+"."+str(M))

    humidity, temperature = Adafruit_DHT.read_retry(sensor, pin)

 temp = int(temperature)
 test_set.append(time)
 test_set.append(temp)

 a = knn.predict([test_set]])
 Out = a[0]

 If out == 0:
 GPIO.output(light,GPIO.LOW)
 GPIO.output(fan,GPIO.LOW)

 If out == 1:
 GPIO.output(light,GPIO.LOW)
 GPIO.output(fan,GPIO.HIGH)

 If out == 2:
 GPIO.output(light,GPIO.HIGH)
 GPIO.output(fan,GPIO.LOW)

 If out == 3:
 GPIO.output(light,GPIO.HIGH)
 GPIO.output(fan,GPIO.HIGH)

现在让我们看看我们在这里做了什么。在这个程序中,条件while count < 200:内的程序的第一部分与我们在上一个代码中所做的完全相同。所以,它只是根据用户的要求做事情,同时,它正在从用户那里获取值以了解他们的工作行为:

while count > 200:

此后,当计数超过200时,代码的第二部分将开始执行,这是在前面的循环内部:

    time = ""

在这一行中,我们正在形成一个名为 time 的空字符串,我们将在其中存储时间的值:

    H = datetime.datetime.now().strftime('%H')
    M = datetime.datetime.now().strftime('%M')

我们将时间的值存储到名为HM的变量中:

    time = float(str(H)+"."+str(M))

我们现在将时间的值存储在字符串time中。这将包括小时和分钟:

 temp = int(temperature)

为了简化计算并减少系统的计算负载,我们正在减小温度变量的大小。我们通过去掉小数位来做到这一点。为了做到这一点TT.TT,我们只是消除小数点并将其转换为整数。这是通过名为int()的函数完成的。温度的整数值将存储在名为temp的变量中:

 test_set.append(time)
 test_set.append(temp)

在这里,我们将时间和温度的值添加到名为test_set的列表中,如果您查看程序,那么您将看到程序中间声明了一个空集。所以,现在这个test_set有了timetemp的值,这可以进一步被预测算法用来预测状态:

 a = knn.predict([test_set]])

使用名为predict()的简单函数从knn函数中,我们可以预测状态的值。我们只需要将数据或test_set列表传递给预测函数。这个函数的输出将是一个存储在变量a中的列表:

 Out = a[0]

Out的值将设置为列表a的第一个元素:

 If out == 0:
 GPIO.output(light,GPIO.LOW)
 GPIO.output(fan,GPIO.LOW)

 If out == 1:
 GPIO.output(light,GPIO.LOW)
 GPIO.output(fan,GPIO.HIGH)

 If out == 2:
 GPIO.output(light,GPIO.HIGH)
 GPIO.output(fan,GPIO.LOW)

 If out == 3:
 GPIO.output(light,GPIO.HIGH)
 GPIO.output(fan,GPIO.HIGH)

使用前面的代码块,我们能够根据算法预测的状态有选择地打开灯和风扇。因此,使用这个,程序将能够自动预测并打开或关闭灯和风扇,无需您的干预。

总结

在本章中,我们了解了即使没有学习,机器学习是如何工作的。我们了解了如何提供数据集,并且可以使用现有系统创建新的数据集。最后,我们了解了系统如何无缝地收集数据,从数据中学习,最终提供输入。

第三十章:制作机械臂

最后,我们终于到达了大多数人自本书开始以来就想要到达的地方。制作一个机械臂!在本章中,我们将学习机械臂工作背后的概念。毫无疑问,我们也将制作一个用于个人使用的机械臂,它可以为我们做无限的事情。

机械臂的基础

如果你看到一个人体,那么使我们能够与大多数其他物种不同的最显著的部分之一就是手臂。这是我们用来做大部分工作的身体部分。

人类的手臂是一个由关节和肌肉组成的非常复杂的机制,它们协同工作以赋予它我们所知道的灵巧性。以我们的肩关节为例。如果你注意观察,你会注意到它有能力向上、向下、向右、向左移动,甚至可以在自己的轴线上旋转,而这一切只是因为它只有一个单一的关节,我们称之为球关节。

当我们谈论机器人上的机械臂时,我们无疑是在谈论一种复杂的安排,它由执行器和身体(也称为底盘)组成,以在三维空间中获得所需的运动。

现在,让我们了解一些机械臂的基本部件。第一部分是执行器。我们可以使用电机来控制机械臂;然而,正如我们之前学过的,使用之前使用过的电机不是它的理想解决方案,因为它既不能保持位置,也没有反馈机制。因此,我们只剩下一个选择,那就是使用舵机。正如我们所知,它们有大量的扭矩,并且有能力知道它在哪里,并且可以保持其位置,只要我们想要。

机器人的第二部分是底盘,也就是将所有电机固定在一起并为机器人提供结构支持的部分。这必须以这样的方式制作,以便为任何给定的关节提供所有理想轴线的运动。这很重要,因为单个舵机只能在一个单一轴线上提供运动。然而,有多个地方可以使用复杂的安排使机器人在多个轴线上移动。此外,底盘应该是坚固的,这非常重要。正如我们所知,地球上的所有材料都具有一定程度的柔韧性。材料的构造也取决于材料的不服从性。这对于重复性非常重要。

现在,什么是重复性?你可能在工业或任何制造单位中看到,机器人被安装并一遍又一遍地执行相同的任务。这是可能的,因为机器人被编程执行一组特定的功能在特定的情况下。现在,假设机器人的底盘不是坚固的。在这种情况下,即使舵机是 100%精确并且一遍又一遍地到达完全相同的位置,机器人仍然可能与其实际目标位置不同。这是因为底盘可能有一定的柔韧性,这就是为什么最终位置可能会有所不同。因此,正确的底盘是必不可少的。当我们谈论大型机器人时,这变得更加重要,因为即使最轻微的变形也可能导致机械臂最终位置的非常大的变化。

我们在谈论机器人手臂时经常使用的一个术语是末端执行器。这基本上是机器人手臂的末端,它将为我们做所有最终的工作。在真实的人类手臂的情况下,末端执行器可以被认为是手。这位于手臂的顶部,手臂的所有运动基本上是为了在三维空间中表达手的位置。此外,正是手拿起物体或进行必要的物理动作。因此,术语末端执行器。

现在,由于机械臂在三维空间中移动,定义运动发生的轴成为一个真正的大问题。因此,我们通常使用正在执行的运动类型来定义运动,这给我们一个关于运动是什么以及在哪个轴上的现实想法。为了分析运动,我们使用偏航、俯仰和翻滚YPR)的概念。

前面的图表将清除关于 YPR 的大部分疑惑。这个概念通常用于飞机;然而,它也是机械手的一个重要部分。因此,正如您可以从前面的图表中看到的,当飞机的机头上下移动时,它将被视为俯仰运动。同样,如果飞机改变航向,那么偏航也可以相应地改变——偏航只是飞机在y轴上的运动。最后,我们有一个叫做翻滚的东西。它用于理解旋转的角度。正如您所看到的,所有这三个实体是彼此独立的,追逐其中任何一个都不会对其他产生影响。这个概念也很有用,因为无论飞机的方向如何,YPR 仍然保持不变,非常容易理解。因此,我们直接从飞机上将这个概念引入到我们的机器人中。

最后,我们怎么能忘记处理单元呢?它是命令所有执行器并进行协调和决策的单元。在我们的情况下,这个处理单元是树莓派,它将命令所有执行器。所有这些前述的组件构成了一个机械臂。

自由度

并非每个机械臂都相同。它们具有不同的负载评级,即末端执行器可以承受的最大负载,速度和范围,即末端执行器可以达到的距离。然而,机械臂非常重要的一部分是它所拥有的电机数量。因此,对于每个轴,您至少需要一个电机来使机器人在该轴上移动。例如,人类手臂在肩关节具有三维自由度。因此,为了模仿该关节,您将需要每个轴的电机,也就是说,至少需要三个电机才能使手臂在所有三个轴上独立移动。同样,当我们谈论手肘关节时,它只能在两个维度上移动。也就是手臂的张合和最终手臂的旋转,手肘不在第三维度上移动。因此,为了复制它的运动,我们至少需要两个电机,这样我们就可以在w轴上移动机器人。

根据我们目前所了解的,我们可以安全地假设电机数量越多,机器人的灵巧性也越高。这在大多数情况下是成立的;然而,您可能使用多个电机使机器人在单个轴上旋转。在这种情况下,通过计算执行器数量来确定机器人的灵巧性的基本概念将不起作用。那么我们如何确定机器人的灵巧性呢?

我们有一个叫做自由度DOF)的概念。如果按照标准定义,我可以非常肯定地说你会对它的实际含义感到困惑。如果你不相信,那就自己去 Google 上找找看。用非常简单和平实的英语来说,自由度是指关节可以在任何给定的轴上独立移动。所以,例如,如果我们谈论肩关节,那么我们在所有三个轴上都有运动。因此,自由度就是三。现在,让我们考虑一下我们手臂的肘关节。因为它只能在俯仰和滚动中移动,所以我们最终得到两个自由度。如果我们把肩关节和肘关节连接起来,那么自由度就会增加,整个系统将被称为具有六个自由度。请记住,这个定义是非常简化的。如果你选择深入挖掘,你会遇到多种复杂性。

现在,你会遇到的大多数机械臂都有接近六个自由度。虽然你可能会说这比人类的手臂少,但实际上,它完成了大部分工作,显然自由度较少意味着更少的电机数量,从而降低成本,显然编程复杂性也更低。因此,我们尽量使用尽可能少的自由度。

现在,在前面的图表中,你可以看到一个典型的机械臂,它有六个自由度。编号为1的基本执行器提供了滚动和改变俯仰的自由度。编号为2的肘部执行器只为机器人增加了一个俯仰的自由度。此外,第3关节有能力在俯仰和滚动中移动。最后,我们在这里有末端执行器作为夹具;夹具本身有一个自由度。因此,总体上,我们可以说这个机器人是一个六自由度的机器人。

动力源

在我们所有的项目中,我们一直在使用一个单位,但我想在这一章中强调一下。这个单位是功率单位。我们谈论它的原因是因为在这一章中我们将控制多个舵机。当我们谈论多个舵机时,自然我们将谈论大量的功耗。在机械臂中,我们有六个舵机电机。现在,根据电机的品牌和型号,功耗会有所不同。但是为了保险起见,假设每个舵机的功耗约为 1 安培是个好主意。你可能使用的大多数电源可能无法提供这么多的突发电流。那么我们该怎么办呢?

我们可以采取更高功率输出的简单方法。但是,相反,我们可以采取非常规的途径。我们可以有一个电池,在需要时可以提供这么多的功率。但问题是,任何电池都能满足我们的目的吗?显然,答案是否定的。

存在多种类型的电池。这些电池可以根据以下参数进行区分:

  • 电压

  • 容量

  • 功率重量比

  • 最大充电和放电速率

  • 化学成分

这些将在接下来的小节中详细介绍。

电压

电压是电池可以产生的总体电位差。每个电池都有它提供的特定电压。要记住的一件事是,这个电压会根据电池的充电状态略有变化。也就是说,当一个 12V 的电池充满电时,它可能会输出 12.6V。然而,当它完全放电时,可能会降到 11.4V。因此,电池电压的意思是电池将提供的名义电压。

容量

现在,第二个参数是容量。通常,当你购买电池时,你会看到它的容量以毫安时(mAh)或安时(Ah)为单位。这是一个非常简单的术语。让我用一个例子来解释这个术语给你。假设你有一个容量为 5Ah 的电池。现在,如果我连续绘制 5 安培 1 小时,那么电池将完全放电。相反,如果我连续绘制 10 安培,那么电池将在半小时内放电。通过这个,我们还可以使用以下简单的公式推导出电池的总功率:电池的总功率=电池的标称电压 x 电池的总容量

因此,如果你有一个 12V 的电池,其容量为 10Ah,那么总容量将是 120 瓦特。

功率重量比

重量在机器人技术中扮演着非常关键的角色,如果我们增加机器人的重量,那么移动它所需的力量可能会呈指数级增长。因此,功率重量比的概念就出现了。我们总是更喜欢一个极轻的电池,它在重量方面提供了大量的功率。功率重量比的方程可以定义如下:每公斤瓦时的功率重量比=瓦特的最大功率/电池的总重量

现在,假设一个电池提供了 500 瓦的功率,重量为 5 公斤,那么功率重量比将是 100 瓦时/公斤。功率重量比越高,电池就越好。

最大充电和放电速率

这可能是电池中最关键的部分之一。通常,电池能够让机器人运行 1 小时。然而,机器人的功耗并不是恒定的。假设在 90%的时间里,我们的机械臂消耗 2 安培的功率,所以电池容量为 2Ah。然而,在操作过程中的某些时刻,机器人需要所有电机以最大功率工作。机器人的峰值功耗约为 6 安培。现在,问题是,2Ah 的电池能否为机器人提供 6 安培的功率?

这是一个非常实际的挑战。你可能会说,最好选择一个比 2Ah 电池大得多的电池。但是,正如你所知,这将显著增加重量。那么解决方案是什么呢?

还有一个叫做峰值放电电流的东西。这由C评级表示。因此,如果我们的电池是 1C 评级,那么 2Ah 的电池一次只能提供最多 2Ah 的电源。然而,如果电池是 10C 评级,那么它应该能够提供高达 20 安培的突发电源。如今,你可以找到可以提供高达 100C 甚至更高的突发电源的电池。我们之所以有这个是因为机器人的峰值功耗可能比它们的恒定功耗高得多。如果在任何时候,电池无法提供足够的电力,那么机器人将表现异常,甚至可能关闭。

这个故事的第二部分是充电评级。这是你可以提供给电池的最大充电电流。它也由相同的 C 评级表示。因此,如果 C 评级为 0.5,那么你可以为 2Ah 的电池提供最大 1 安培的充电。

换句话说,你可以给电池充电的最快速度是 2 小时。

化学成分

市场上有不同类型的电池,它们通常根据其化学成分进行广泛分类。所有这些电池都有各自的优缺点。因此,我们不能说哪一个比另一个更好。这总是在各种因素之间进行权衡。以下是市场上可以找到的电池列表,以及它们的优缺点:

电池 峰值功率输出 功率重量比 价格
湿电池 极低 最便宜
镍氢电池 中等 便宜
锂离子
锂聚合物 极高 极好 极高

从这个表中可以看出,峰值功率输出是我们非常想要的,良好的功率重量比也是如此;因此,在锂聚合物电池上花费一定的金额是有道理的。

这些电池,至少具有 20C 的额定值,功率重量比约为普通湿电池的五倍。然而,它们的价格可能是普通湿电池的 10 倍。

现在我们知道了为这些更高电流要求选择哪些电池。一块 11.1V 和 2200 毫安时的锂聚合物电池不会花费你超过 20 美元,并且将为你提供你可能永远不需要的巨大功率。所以,我们已经解决了电源供应问题。现在是时候继续使机械手运行了。

寻找极限

机械臂套件在 eBay 或亚马逊上相对容易获得。这并不难组装,需要几个小时来准备。一些机械臂套件可能不会随舵机一起发货,如果是这样,你可能需要单独订购。我建议选择与舵机捆绑在一起的套件,因为如果你选择单独订购舵机,可能会出现兼容性问题。

正如你所知,这些舵机将使用 PWM 工作,控制它们也不难。所以,让我们直接开始并看看我们能做些什么。一旦你组装好了机械臂套件,将舵机的线连接如下:

现在,首先,我们需要知道我们机器人上连接的每个舵机的最大物理极限是什么。有各种各样的技术可以做到这一点。最基本的方法是进行物理测量。这种方法可能很好,但你将无法充分利用舵机电机的全部潜力,因为在测量时会有一定程度的误差。因此,你放入舵机的值将略小于你认为它可以达到的值。第二种方法是手动输入数据并找出确切的角度。所以,让我们继续用第二种方法做事情,并上传以下代码:

import RPi.GPIO as GPIO import time GPIO.setmode(GPIO.BCM) GPIO.setup(14,GPIO.OUT) GPIO.setup(16,GPIO.OUT) GPIO.setup(18,GPIO.OUT) GPIO.setup(20,GPIO.OUT) GPIO.setup(21,GPIO.OUT) GPIO.setup(22,GPIO.OUT)
  GPIO.setwarnings(False) pwm1 = GPIO.PWM(14, 50) pwm2 = GPIO.PWM(16, 50) pwm3 = GPIO.PWM(18, 50) pwm4 = GPIO.PWM(20, 50) pwm5 = GPIO.PWM(21, 50) pwm6 = GPIO.PWM(22, 50)
  pwm1.start(0) pwm2.start(0) pwm3.start(0) pwm4.start(0) pwm5.start(0) pwm6.start(0)  def cvt_angle(angle):     dc = float(angle/90) + 0.5
    return dc  while 1:

 j = input('select servo')  if j == 1: i = input('select value to rotate')
  pwm1.ChangeDutyCycle(cvt_angle(i))
 time.sleep(2)
  pwm1.ChangeDutyCycle(cvt_angle(90)) elif j ==2:  i = input('select value to rotate')   pwm2.ChangeDutyCycle(cvt_angle(i))
 time.sleep(2) pwm2.ChangeDutyCycle(cvt_angle(90))   elif j ==3:   i = input('select value to rotate') pwm3.ChangeDutyCycle(cvt_angle(i))
 time.sleep(2) pwm3.ChangeDutyCycle(cvt_angle(90))  elif j ==4:  i = input('select value to rotate')
  pwm4.ChangeDutyCycle(cvt_angle(i))
 time.sleep(2) pwm4.ChangeDutyCycle(cvt_angle(90))  elif j ==5:  i = input('select value to rotate')
  pwm5.ChangeDutyCycle(cvt_angle(i))
 time.sleep(2) pwm5.ChangeDutyCycle(cvt_angle(90))  elif j ==6:  i = input('select value to rotate')   pwm6.ChangeDutyCycle(cvt_angle(i))
 time.sleep(2) pwm6.ChangeDutyCycle(cvt_angle(90)) }

现在,让我们看看这段代码在做什么。这段代码看起来可能相当复杂,但它所做的事情非常简单。

 j = input('select servo from 1-6')

使用前面的代码行,我们正在为用户打印从 1-6 选择舵机的语句。当用户输入舵机的值时,这个值被存储在一个名为j的变量中:

 if j == 1: i = input('select value to rotate')
  pwm1.ChangeDutyCycle(cvt_angle(i))
 time.sleep(2) pwm1.ChangeDutyCycle(cvt_angle(90))  

这里的if条件检查j的值。如果在这一行中,j=1,那么它将运行与舵机编号1对应的代码。在这段代码中,第一行将打印选择要旋转的值。完成后,程序将等待用户输入。一旦用户输入任何值,它将被存储在一个名为I的变量中。然后,使用cvt_angle(i)函数,用户输入的值将被转换为相应的占空比值。这个占空比值将被获取到pwm1.ChangeDutyCycle()参数中,从而给予机器人你想要的特定关节角度。由于time.sleep(2)函数,舵机将等待到下一行。之后,我们使用pwm1.ChangeDutyCycle(cvt_angle(90))这一行,这将把它带回到 90 度。

你可能会问,为什么我们要这样做?这是一个非常重要的原因。假设您已经给它一个超出其物理极限的命令。如果是这种情况,那么舵机将继续尝试朝那个方向移动,不管发生什么。然而,由于物理限制,它将无法继续前进。一旦发生这种情况,然后在几秒钟内,您将看到蓝烟从舵机中冒出,表明它的损坏。问题在于,制造这种类型的错误非常容易,损失是非常明显的。因此,为了防止这种情况,我们迅速将其带回到中心位置,这样它就不会有任何烧毁的可能性。

现在,根据前面的代码,通过机器人对舵机 1-6 执行相同的操作。现在你知道发生了什么,是时候拿起笔和纸开始给舵机赋予角度值了。请记住,这段代码的最终目标是找出最大限制。因此,让我们从 90 度开始做起。在每一侧给它一个值,直到你能够接受的值。在纸上列出清单,因为我们将需要它用于下一段代码。

使机器人安全

在本章的前一部分中,通过我们的多次尝试,我们已经能够找到每个舵机的最大位置。现在是时候使用这些值了。在本章中,我们将为舵机编程其绝对最大值。在这个程序中,我们将确保舵机永远不需要超出两侧的定义参数。如果用户给出超出它的值,那么它将选择忽略用户输入,而不是造成自身损坏。

那么,让我们看看如何完成它。在程序的某些部分,数字值已经用粗体标出。这些是您需要用本章前面记录的值替换的值。例如,对于舵机 1,记录下的值是23170,作为每一侧的最大值。因此,代码的更改将从if a[0] < 160 and a[0] > 30变为ifa[0] < 170 and a[0] > 23。同样,对于每个舵机,必须遵循相同的程序:

import RPi.GPIO as GPIO import time GPIO.setmode(GPIO.BCM) GPIO.setup(14,GPIO.OUT) GPIO.setup(16,GPIO.OUT) GPIO.setup(18,GPIO.OUT) GPIO.setup(20,GPIO.OUT) GPIO.setup(21,GPIO.OUT) GPIO.setup(22,GPIO.OUT)
  GPIO.setwarnings(False) pwm1 = GPIO.PWM(14, 50) pwm2 = GPIO.PWM(16, 50) pwm3 = GPIO.PWM(18, 50) pwm4 = GPIO.PWM(20, 50) pwm5 = GPIO.PWM(21, 50) pwm6 = GPIO.PWM(22, 50)
  pwm1.start(cvt_angle(90)) pwm2.start(cvt_angle(90)) pwm3.start(cvt_angle(90)) pwm4.start(cvt_angle(90)) pwm5.start(cvt_angle(90)) pwm6.start(cvt_angle(90))

def cvt_angle(angle):
    dc = float(angle/90) + 0.5
    return dc

while True:

    a = raw_input("enter a list of 6 values")

    if a[0] < 160 and  a[0] > 30:
        pwm1.ChangeDutyCycle(cvt_angle(a[0]))

    if a[1] < 160 and  a[1] > 30:  pwm2.ChangeDutyCycle(cvt)angle(a[1]))

    if a[0] < 160 and  a[0] > 30: pwm3.ChangeDutyCycle(cvt_angle(a[2]))    if a[0] < 160 and  a[0] > 30: pwm4.ChangeDutyCycle(cvt_angle(a[3]))    if a[0] < 160 and  a[0] > 30: pwm5.ChangeDutyCycle(cvt_angle(a[4]))    if a[0] < 160 and  a[0] > 30: pwm6.ChangeDutyCycle(cvt_angle(a[5]))}

现在,在这段代码中,我们做了一些非常基础的事情。您可以放心地说,我们所做的一切就是将ChangeDutyCycle()函数放在一个if语句中。这个if语句将决定舵机是移动还是保持在原位。对一些人来说,将这个程序放在一个特殊的部分似乎很天真。但是,请相信我,不是这样的。这个语句现在将作为以后每个程序的一部分。为了检查通过这个if语句传递给舵机的最终值,必须检查为舵机移动编写的所有代码;因此,对代码的基本可视化是非常必要的。

现在解释完毕,是时候给出不同的命令并查看它们是否在安全工作限制内工作了。

编写多个帧

在上一章中,我们已经学习了如何确保机器人在安全限制下工作的基础知识。在本章中,我们将看看如何使机器人能够在点击按钮的同时执行不同的活动,而不是逐个输入值。

为了做到这一点,我们需要了解一些高级运动概念。每当您观看任何视频或玩任何视频游戏时,您一定会遇到“每秒帧数”(FPS)这个术语。如果您还没有听说过这个术语,那么让我为您解释一下。现在制作的每个视频实际上都是由静止图像制成的。这些静止图像是由摄像机捕捉的,每秒点击 25-30 次。当这些图像以与它们被捕捉的速率相同的速率在屏幕上播放时,它形成了一个平滑的视频。

同样,在机器人中,我们也有帧的概念。然而,这些帧不是图像,而是机器人必须遵循的多个步骤。在一个简单的机器人程序中,可能只有两个帧,即初始帧和最终帧。这两个帧将对应于初始位置或最终位置。

然而,在现实世界中,这并不总是可能的,因为当机器人直接从初始位置到最终位置时,它会沿着特定的路径运动,并具有特定的曲率。然而,在这条路径上可能会有障碍物,或者这条路径可能不是所需的,因为需要遵循的路径可能是另一条。因此,我们需要帧。这些帧不仅定义了机器人从初始位置到最终位置的运动,而且将这两个位置之间的过渡分解为多个步骤,使机器人遵循所需的路径。

这可以被称为帧编程,在本章中我们将介绍。要记住的一件事是,帧数越多,机器人的运行就越平稳。你还记得我们看到的闭路电视录像吗?我们可以说它不够平滑,而且有很多抖动。这是由于闭路电视摄像头的低帧率造成的。它们不是以 30FPS 工作,而是以 15FPS 工作。这是为了减少视频的存储空间。然而,如果你看到最新的视频,有一些游戏和视频的帧率比正常的要高得多。我们最新的摄像头有 60FPS 的工作,使视频更加平滑和愉快。机器人也是如此。帧数越多,运动就越平滑和可控。但是,请确保不要过度使用。

现在,要从一个位置移动到另一个位置,我们将不得不在一开始就放入每个舵机的角度值。一旦获取,它将自动开始逐个执行这些值。为此,请继续编写以下代码:

import RPi.GPIO as GPIO import time GPIO.setmode(GPIO.BCM) GPIO.setup(14,GPIO.OUT) GPIO.setup(16,GPIO.OUT) GPIO.setup(18,GPIO.OUT) GPIO.setup(20,GPIO.OUT) GPIO.setup(21,GPIO.OUT) GPIO.setup(22,GPIO.OUT)
  GPIO.setwarnings(False) pwm1 = GPIO.PWM(14, 50) pwm2 = GPIO.PWM(16, 50) pwm3 = GPIO.PWM(18, 50) pwm4 = GPIO.PWM(20, 50) pwm5 = GPIO.PWM(21, 50) pwm6 = GPIO.PWM(22, 50)
  pwm1.start(0) pwm2.start(0) pwm3.start(0) pwm4.start(0) pwm5.start(0) pwm6.start(0)

def cvt_angle(angle):
    dc = float(angle/90) + 0.5
    return dc

prev0 = 90
prev1 = 90
prev2 = 90
prev3 = 90
prev4 = 90
prev5 = 90 

while True:

    a = raw_input("enter a list of 6 values for motor 1")
    b = raw_input("enter a list of 6 values for motor 2")
    c = raw_input("enter a list of 6 values for motor 3")
    d = raw_input("enter a list of 6 values for motor 4")
    e = raw_input("enter a list of 6 values for motor 5")
    f = raw_input("enter a list of 6 values for motor 6")

    for i in range(6):

        if a[i] > 10 and a[i]< 180 :  
            pwm1.ChangeDutyCycle(cvt_angle(a[i]))

        if b[i] > 10 and b[i] < 180:
  pwm2.ChangeDutyCycle(cvt_angle(b[i]))

        if c[i] > 10 and c[i] < 180:
 pwm3.ChangeDutyCycle(cvt_angle(c[i]))

        if d[i] > 10 and d[i] < 180:
 pwm4.ChangeDutyCycle(cvt_angle(d[i]))

        if e[i] > 10 and e[i] < 180:
 pwm5.ChangeDutyCycle(cvt_angle(e[i]))

        if f[i] > 10 and f[i] < 180:
 pwm6.ChangeDutyCycle(cvt_angle(f[i])) 

在这个程序中,你可以看到我们复制了以前的程序并进行了一些非常小的改动。所以,让我们看看这些改动是什么:

    a = raw_input("enter a list of 6 values for motor 1")
    b = raw_input("enter a list of 6 values for motor 2")
    c = raw_input("enter a list of 6 values for motor 3")
    d = raw_input("enter a list of 6 values for motor 4")
    e = raw_input("enter a list of 6 values for motor 5")
    f = raw_input("enter a list of 6 values for motor 6")

在这里,我们正在为每个舵机获取输入值并将其存储在不同的列表中。对于舵机 1,将使用列表a;类似地,对于舵机 2,将使用b,依此类推直到f。在代码的前面几行中,机器人将提示用户填写电机 1的六个帧值。然后,它将要求电机 2的六个值,以此类推直到电机 6

    for i in range(6):

给舵机提供 PWM 的整个程序都集中在这个 for 循环中。这个循环将检查i的值并每次递增。i的值将从1开始,循环将运行并递增i的值直到达到6

        if a[i] > 10 and a[i]< 180 :  
            pwm1.ChangeDutyCycle(cvt_angle(a[i]))

在程序的这一行中,列表中包含的值是基于1的值进行排序的。因此,第一次它将读取a[1]的值,这将对应于列表a[]的第一个值。这个值应该在安全工作范围内,因此使用if循环。如果在安全工作范围内,那么if条件中的程序将执行,否则不会执行。在if循环内,我们有一个简单的语句:pwm1.ChangeDutyCycle(cvt_angle(a[I]))。这将简单地取a[1]的值并将其转换为相应的 PWM 值,并将其提取到ChangeDutyCycle()函数中,这将改变舵机 1 的 PWM。

对于其余的舵机也制作了类似的程序,从舵机 1 到舵机 6。因此,所有这些都将逐一读取其对应列表中的值,并根据用户编程的方式改变舵机的角度。此外,随着循环的执行,i的值将增加,从而使程序读取列表中提取的不同值。列表中舵机的每个值将对应一个不同的帧,从而使机器人通过它。

所以继续玩一些有趣的东西,让你的机器人做一些很棒的动作。只要小心对待它!

速度控制

能够如此轻松地制作一个机械臂真是太神奇了,只需一点点代码,我们现在就能够按照自己的意愿来控制它。然而,你可能已经注意到了一个问题,那就是,机器人按照我们的意愿移动,但速度不是我们想要的。这是在使用基于数字 PWM 的舵机时非常常见的问题。

这些舵机没有内置的速度控制。它们的控制系统被编程为尽可能快地移动舵机以达到目标位置。因此,要控制速度,我们必须对程序本身进行调整,并给它一个平稳的线性进展。

速度控制可以通过几种不同的技术来实现。所以,不多说了,让我们去看看代码。在你编写代码之前,先读一遍,然后看一下下面的解释。之后,你会更清楚我们在做什么。这将使编写代码更快、更容易。所以,让我们来看看:

import RPi.GPIO as GPIO import time GPIO.setmode(GPIO.BCM) GPIO.setup(14,GPIO.OUT) GPIO.setup(16,GPIO.OUT) GPIO.setup(18,GPIO.OUT) GPIO.setup(20,GPIO.OUT) GPIO.setup(21,GPIO.OUT) GPIO.setup(22,GPIO.OUT)
  GPIO.setwarnings(False) pwm1 = GPIO.PWM(14, 50) pwm2 = GPIO.PWM(16, 50) pwm3 = GPIO.PWM(18, 50) pwm4 = GPIO.PWM(20, 50) pwm5 = GPIO.PWM(21, 50) pwm6 = GPIO.PWM(22, 50)
  pwm1.start(0) pwm2.start(0) pwm3.start(0) pwm4.start(0) pwm5.start(0) pwm6.start(0)

def cvt_angle(angle):
    dc = float(angle/90) + 0.5
    return dc

prev0 = 90
prev1 = 90
prev2 = 90
prev3 = 90
prev4 = 90
prev5 = 90 

pwm1.ChangeDutyCycle(cvt_angle(prev0)) pwm2.ChangeDutyCycle(cvt_angle(prev1)) pwm3.ChangeDutyCycle(cvt_angle(prev2)) pwm4.ChangeDutyCycle(cvt_angle(prev3)) pwm5.ChangeDutyCycle(cvt_angle(prev4)) pwm6.ChangeDutyCycle(cvt_angle(prev5)) 

while True:

 a = raw_input("enter a list of 6 values for motor 1")
 b = raw_input("enter a list of 6 values for motor 2")
 c = raw_input("enter a list of 6 values for motor 3")
 d = raw_input("enter a list of 6 values for motor 4")
 e = raw_input("enter a list of 6 values for motor 5")
 f = raw_input("enter a list of 6 values for motor 6")

    speed = raw_input("enter one of the following speed 0.1, 0.2, 0.5, 1")

 for i in range(6):

   while prev0 =! a[i] and prev1 =! b[i] and prev2 =! c[i] and prev3 =! d[i] and prev4 =! e[i] and prev 5 =! f[i]

     if a[i] > 10 and a[i]< 180 : 

        if prev0 > a[i]
            prev0 = prev0 - speed

         if prev0 < a[i]
             prev0 = prev0 + speed

         if prev0 = a[i]
             prev0 = prev0 

         pwm1.ChangeDutyCycle(cvt_angle(prev0))

    if b[i] > 10 and b[i] < 180:

        if prev2 > b[i]
            prev2 = prev2 - speed

         if prev2 < b[i]
             prev2 = prev2 + speed

         if prev2 = b[i]
            prev2 = prev2

  pwm2.ChangeDutyCycle(cvt_angle(b[i]))

    if c[i] > 10 and c[i] < 180: if prev3 > c[i]
             prev3 = prev3 - speed

        if prev3 < c[i]
            prev3 = prev3 + speed

        if prev3 = c[i]
             prev3 = prev3

 pwm3.ChangeDutyCycle(cvt_angle(c[i]))

    if d[i] > 10 and d[i] < 180: if prev4 > d[i]
             prev4 = prev4 - speed

        if prev4 < d[i]
            prev4 = prev4 + speed

        if prev4 = d[i]
             prev4 = prev4

 pwm4.ChangeDutyCycle(cvt_angle(d[i]))

     if e[i] > 10 and e[i] < 180: if prev5 > e[i]
             prev5 = prev5 - speed

        if prev0 < e[i]
            prev5 = prev5 + speed

        if prev5 = e[i]
             prev5 = prev5

 pwm5.ChangeDutyCycle(cvt_angle(e[i]))

     if f[i] > 10 and f[i] < 180: if prev6 > f[i]
            prev6 = prev6 - speed

         if prev6 < f[i]
            prev6 = prev6 + speed

        if prev6 = f[i]
            prev6 = prev6

 pwm6.ChangeDutyCycle(cvt_angle(f[i]))

 flag = 0 

在这个程序中,有很多东西。我们应该逐一了解它们。所以,让我们看看我们在做什么:

prev0 = 90
prev1 = 90
prev2 = 90
prev3 = 90
prev4 = 90
prev5 = 90 

在这里,我们定义了六个新变量,名称为prev0prev5,它们都被赋予了值90。这里的术语prev代表之前的值,因此它将指示先前的值。

        while prev0 =! a[i] and prev1 =! b[i] and prev2 =! c[i] and prev3 =! d[i]   and prev4 =! e[i] and prev 5 =! f[i]

在代码行for i in range 6之后,我们有前面的代码行,基本上是检查a[i]的值与prev0的值。类似地,它正在检查b[i]的值与prev1的值,依此类推。直到所有这些条件都成立,while循环将为真,并在其中循环程序,直到条件不再为假。也就是说,所有的prev值恰好等于列表相应值的值。

再次,这对你可能有点奇怪,但相信我,它会非常有用,我们一会儿会看到:

     if a[i] > 10 and a[i]< 180 : 

         if prev0 > a[i]
             prev0 = prev0 - speed

         if prev0 < a[i]
             prev0 = prev0 + speed

         if prev0 = a[i]
             prev0 = prev0 

         pwm1.ChangeDutyCycle(cvt_angle(prev0))

现在,真正的问题来了。这是将控制舵机速度的主程序。在这个程序中,第一行很简单;它将检查给定的值是否有效,也就是在安全极限之间。一旦完成,它将检查a[Ii]的值是否小于或大于先前的值。如果大于a[i]的值,那么它将采用先前的值,并用用户指定的速度递减。如果小于a[i]的值,那么它将用指定的速度递增先前的值。

因此,如果你看一下,代码只是在while循环运行时每次递增或递减先前的值。现在,while循环将一直运行,直到prev的值等于相应列表值。也就是说,循环将一直递增值,直到达到指定位置。

因此,速度值越低,每次递增的值就越低,从而整体减慢速度。

这个过程对所有其他舵机也是一样的。听起来可能很复杂,但实际上并不是!编程很容易,每次你把它分解成小块并逐一理解时,它都会继续保持简单!

总结

在本章中,我们已经了解了机械臂的基础知识、其电源和其编程。通过一个非常简单的程序,我们能够找出舵机的极限,然后应用这些极限以确保舵机不会损坏自己。我们对框架有了一个基本的概念,并根据框架进行了一些编程。最后,我们还继续控制了舵机的速度,使用了我们自己的基本级别的程序。

posted @ 2025-09-22 13:19  绝不原创的飞龙  阅读(15)  评论(0)    收藏  举报