Python-树莓派项目-全-

Python 树莓派项目(全)

原文:zh.annas-archive.org/md5/AC9839247134C458206EE3BE6D404A66

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

树莓派代表了计算机科学、教育、娱乐、业余黑客技术以及其他你可以将设备系列归类的几个类别中的最佳创新。即使这本书即将出版,树莓派系列产品已经成为史上第三大畅销电脑。任何人都无法预测,随着树莓派基金会持续的创新和遍布全球的数千人不断用各种树莓派版本展示更新、更好的创新解决方案,这一产品线可能会达到什么样的新高度!

树莓派的主要目标之一是价格低廉。本书的目的是让初学者学习 Python 编程,以及操作硬件。读者可能已经在硬件和编程方面做了一些工作,并希望加强这两个领域的技能。读者也可能只是对使用 Python 在树莓派 Zero 上做更多项目感兴趣,当然,本书中简要介绍的一些项目可能也会引起读者的兴趣。

本书从一些热身示例开始,帮助读者熟悉树莓派环境,随着本书的进展,项目种类和复杂性也在增加。虽然,那些在接触本书之前已经有所进步的读者可以跳过一些章节,但我们建议初学者从头到尾完成所有章节,因为概念是相互依赖的。

本书涵盖的内容

第一章,Python 和树莓派 Zero 入门,介绍了树莓派 Zero 和 Python 编程语言,它的历史和特点。我们将为 Python 开发设置树莓派,并编写第一个程序。

第二章,算术运算、循环和闪烁的灯,介绍了 Python 中的算术运算和循环。在第二章的后半部分,我们将讨论树莓派 Zero 的 GPIO 接口,然后学习如何使用 GPIO 引脚闪烁 LED 灯。

第三章,条件语句、函数和列表,讨论了 Python 中的条件语句类型、变量和逻辑运算符。我们还将讨论 Python 中的函数。然后,我们将学习编写一个用于控制直流电机的函数,该函数使用树莓派 Zero。

第四章,通信接口,涵盖了树莓派 Zero 上所有可用的通信接口。这包括 I2C、UART 和 SPI 接口。这些通信接口广泛用于连接传感器。因此,我们将通过使用传感器作为示例来演示每个接口的操作。

第五章,Python 中的数据类型和面向对象编程,讨论了 Python 中的面向对象编程和面向对象编程的优势。我们将通过一个实际示例来讨论这一点。

第六章,文件 I/O 和 Python 实用工具,讨论了读取和写入文件。我们讨论了创建和更新配置文件。我们还将讨论 Python 中可用的某些实用工具。

第七章,请求和 Web 框架,讨论了能够从网络中检索数据的库和框架。我们将讨论一个示例,获取本地天气信息。我们还将讨论在 Raspberry Pi Zero 上运行 Web 服务器。

第八章,使用 Python 可以开发的一些酷炫事物,讨论了能够从网络中检索数据的库和框架。我们将讨论例如获取本地天气信息的示例。我们还将讨论在 Raspberry Pi Zero 上运行 Web 服务器。

第九章,让我们来制作一个机器人!,展示了如何使用 Raspberry Pi Zero 作为控制器构建室内机器人,并以逐步指南的形式记录了我们的经验。我们希望展示 Python 和 Raspberry Pi Zero 外围设备的组合的神奇之处。

第十章,使用 Raspberry Pi Zero 进行智能家居自动化,讨论了四个项目,包括一个语音激活的个人助理、基于 Web 框架的家电控制、物理活动激励工具和智能草坪洒水器。通过这些项目,我们提供了更多关于新硬件和编程实现的示例。

第十一章,技巧和窍门,以有用的硬件和软件技巧和快捷方式结束本书,这些技巧和快捷方式将帮助你在超越本书中的概念和练习时实施自己的项目和解决方案,或者简单地作为爱好和娱乐来源探索编程和硬件黑客领域。

你需要这本书的东西

以下硬件推荐:

  • 一台笔记本电脑,任何操作系统

  • Raspberry Pi Zero

  • 一个 microSD 卡,8 GB 或 16 GB

  • 一个 USB 键盘

  • 一个 USB 鼠标

  • 一个带有 HDMI 输入的显示器

  • 一个 USB Wi-Fi 卡

  • 电源,最小 500 mA。

  • 显示器线缆

  • 其他配件,根据需要完成书中的各种项目

这本书面向的对象

本书主要面向爱好者和制造商。因此,假设读者对编程、硬件和 Linux 操作系统有一些基本了解。即使没有接触过这些领域,也有可能跟上并从本书中受益。尽可能的情况下,我们已尽力为您指出免费、开源和/或成本效益高的资源,以便跟随书中的项目。

习惯用法

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

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:“remove()方法找到传递的参数的第一个元素实例并将其从列表中删除。”

代码块如下设置:

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

任何命令行输入或输出都如下所示:

 sudo pip3 install schedule

新术语重要词汇以粗体显示。屏幕上显示的单词,例如在菜单或对话框中,在文本中显示如下:“从下拉菜单中选择 A8 序列选项。”

警告或重要注意事项以如下方式显示在框中。

技巧和窍门如下所示。

读者反馈

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

要发送一般反馈,只需发送电子邮件至feedback@packtpub.com,并在邮件主题中提及书籍标题。

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

客户支持

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

下载示例代码

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

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

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

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

  3. 点击代码下载与勘误。

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

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

  6. 从下拉菜单中选择您购买此书的来源。

  7. 点击代码下载。

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

  • WinRAR / 7-Zip for Windows

  • Zipeg / iZip / UnRarX for Mac

  • 7-Zip / PeaZip for Linux

您可以从属于此书的代码存储库下载最新的代码示例github.com/sai-y/pywpi。您可以在pywithpi.com找到包括额外项目在内的其他资源。

本书的相关代码包也托管在 Packt 的 GitHub 仓库中,网址为github.com/PacktPublishing/Python-Programming-with-Raspberry-Pi-Zero。我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!

下载本书中的彩色图像

我们还为您提供了一个包含本书中使用的截图/图表彩色图像的 PDF 文件。彩色图像将帮助您更好地理解输出的变化。您可以从www.packtpub.com/sites/default/files/downloads/PythonProgrammingwithRaspberryPiZero_ColorImages.pdf下载此文件。

错误清单

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

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

盗版

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

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

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

问题

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

第一章:使用 Python 和 Raspberry Pi Zero 入门

在过去的几年里,Raspberry Pi 单板计算机系列已被证明是一套革命性的工具,适用于学习、娱乐和多个严肃的项目!全世界的人们现在都拥有了学习计算机架构、计算机编程、机器人技术、传感系统、家庭自动化以及更多,轻松且不会掏空他们的钱包。本书旨在帮助您,读者,通过 Raspberry Pi Zero 学习 Python 编程的旅程。在编程语言中,Python 既是简单易学的一种,也是用途广泛的一种。在接下来的几章中,我们将首先熟悉 Raspberry Pi Zero,这是一款独特、简单且价格低廉的计算机,以及 Python,逐步构建越来越具有挑战性和复杂性的项目。

在本章中,我们将讨论以下内容:

  • Raspberry Pi Zero 及其功能简介

  • Raspberry Pi Zero 的设置

  • Python 编程语言的介绍

  • 开发环境设置和编写第一个程序

让我们开始吧!

在第一章中,我们将了解 Raspberry Pi Zero,为使用本书学习 Python 设置好一切,并在 Python 中编写我们的第一个代码片段。

本书所需物品

本书所需的物品如下。提供的来源仅供参考。读者可以从等效的替代来源购买:

名称 链接 成本(美元)
Raspberry Pi Zero(v1.3 或更高版本) (Raspberry Pi 的购买将另行讨论) $5.00
USB 集线器 amzn.com/B003M0NURK 约 $7.00
USB OTG 线缆 www.adafruit.com/products/1099 $2.50
Micro HDMI 到 HDMI 适配器线缆 www.adafruit.com/products/1358 $6.95
USB Wi-Fi 适配器 amzn.com/B00LWE14TO $9.45
Micro USB 电源 amzn.com/B00DZLSEVI $3.50
电子入门套件(或类似) amzn.com/B00IT6AYJO $25.00
2x20 接头 www.adafruit.com/products/2822 $0.95
NOOBS micro SD 卡或空白 8 GB micro SD 卡 amzn.com/B00ENPQ1GK $13.00
Raspberry Pi 摄像头模块(可选) a.co/6qWiJe6 $25.00
Raspberry Pi 摄像头适配器(可选) www.adafruit.com/product/3170 $5.95

除了本节中提到的其他物品外,还需要包括一个 USB 鼠标、USB 键盘和带有 HDMI 输出或 DVI 输出的显示器。我们还需要一根 HDMI 线(如果显示器有 DVI 输出,则需要 DVI 转 HDMI 线)。一些供应商,如 Pi Hut,将 Raspberry Pi Zero 配件作为套件出售(例如,thepihut.com/collections/raspberry-pi-accessories/products/raspberry-pi-zero-essential-kit)。

除了本节中提到的组件外,我们还将讨论 Raspberry Pi Zero 的某些特性以及使用传感器和 GPIO 扩展器等附加组件进行 Python 编程。这些组件是可选的,但在学习 Python 编程的不同方面时非常有用。

材料清单中提到的电子入门套件只是一个示例。您可以自由订购任何初学者电子套件(包含类似混合的电子元件)。

购买 Raspberry Pi Zero

Raspberry Pi Zero 由分销商如 Newark element14Pi HutAdafruit 销售。在撰写本书时,我们遇到了购买 Raspberry Pi Zero 的困难。我们建议监控如 www.whereismypizero.com 等网站,以了解 Raspberry Pi Zero 何时可用。我们相信,由于其受欢迎程度,Pi Zero 很难找到。我们不清楚未来 Raspberry Pi Zero 是否会有大量库存。本书中讨论的示例也与 Raspberry Pi 的其他版本兼容(例如,Raspberry Pi 3)。

图片

由 www.whereismypizero.com 提供的 Pi Zero 可用性信息

在购买 Raspberry Pi Zero 时,请确保板版本为 1.3 或更高。板版本印在板的背面(以下图片展示了这个例子)。在购买前,请使用卖家的产品描述验证板版本:

图片

Raspberry Pi 板版本

Raspberry Pi Zero 简介

Raspberry Pi Zero 是一款价格约为 5 美元、比信用卡还要小的微型电脑,由 Raspberry Pi 基金会(一个以教授学生计算机科学为使命的非营利组织)设计。Raspberry Pi Zero 的前身是 Raspberry Pi A 和 B 型号。关于 Raspberry Pi 的详细历史和不同型号的介绍可以在 elinux.org/RPi_General_History 上找到。Raspberry Pi Zero 于 2015 年 11 月 26 日(感恩节)发布。

对于读者来说的一个有趣的事实是,这本书的作者之一,Sai Yamanoor,在感恩节后的那天从旧金山开车到洛杉矶(单程 700 多英里)去当地商店购买 Raspberry Pi Zero。

Raspberry Pi Zero 的特点

Raspberry Pi Zero 由 1 GHz BCM2835 处理器和 512 MB RAM 供电。BCM2835 是由 Broadcom 半导体开发的系统级芯片SoC)。SoC 是一种所有运行计算机所需的组件都集成在单个芯片上的系统(例如,BCM2835 包括 CPU、GPU 以及 USB 接口等外围设备)。BCM2835 SoC 的文档可在www.raspberrypi.org/documentation/hardware/raspberrypi/bcm2835/README.md找到。

Raspberry Pi Zero 板版本 1.3

让我们简要地讨论一下 Raspberry Pi Zero 的特点,使用前面带有编号矩形的图片:

  1. mini HDMI 接口:mini HDMI 接口用于将显示器连接到 Raspberry Pi Zero。HDMI 接口可以用来驱动最大分辨率为 1920x1080 像素的显示器。

  2. USB On-The-Go 接口:为了保持低成本,Raspberry Pi Zero 配备了 USB On-The-GroupOTG)接口。此接口允许连接 USB 设备,如鼠标和键盘。使用 USB OTG 到 USB-A 公转母转换器。我们需要一个 USB 集线器来连接任何 USB 附件。

  3. 电源供应:使用 micro-B USB 适配器为 Raspberry Pi zero 供电,它大约消耗最大 200 mA 的电流。

  4. micro SD 卡槽:Raspberry Pi 的操作系统(OS)存储在 micro SD 卡中,处理器上的引导加载程序在开机时加载它。

  5. GPIO 接口:Raspberry Pi Zero 配备了一个 40 针的通用输入/输出GPIO)引脚头,排列成两排各 20 针。Raspberry Pi Zero 的 GPIO 接口出厂时未焊接引脚头。GPIO 引脚头用于连接传感器、控制执行器以及连接电器。GPIO 引脚头还包括通信接口,如 UART 和 I2C。我们将在第二章中详细讨论 GPIO。

  6. RUN 和 TV 引脚:在 GPIO 引脚头下方有两个标记为RUN的引脚。这些引脚用于通过一个小型触觉开关/按钮重置 Raspberry Pi。TV引脚用于提供复合视频输出。

  7. 摄像头接口:Raspberry Pi Zero 板(版本 1.3 或更高)配备了摄像头接口。这使得可以连接由 Raspberry Pi 基金会设计的摄像头(www.raspberrypi.org/products/camera-module-v2/))。

所有这些 Raspberry Pi 的特性都使得爱好者能够在涉及家庭自动化、假日装饰等项目中使用它们,仅限于你的想象力。科学家们已经将它们用于实验,包括蜜蜂追踪、野生动物追踪、执行计算密集型实验。工程师们使用 Raspberry Pi 来构建机器人、挖比特币、检查网络速度以在速度慢时发送 Twitter 消息,甚至订购披萨!

Raspberry Pi Zero 的设置

在本节中,我们将焊接一些引脚到 Raspberry Pi 上,将操作系统加载到微型 SD 卡上,并启动 Raspberry Pi Zero 进行第一个示例。

焊接 GPIO 引脚

在这本书中,我们将讨论使用 Raspberry Pi 的 GPIO 引脚进行 Python 编程的不同方面。Raspberry Pi Zero 出厂时不带 GPIO 引脚。让我们继续焊接 GPIO 引脚。我们还在本书的网站上上传了视频教程,演示如何将引脚焊接在 Raspberry Pi Zero 上。

如前所述,Raspberry Pi 的 GPIO 部分由 40 个引脚组成。这些引脚排列成两排,每排 20 个引脚。我们需要两套 20 针公头引脚或一个 20 针双排公头引脚。这些可以从 Digikey 和 Mouser 等供应商处获得。Raspberry Pi 的引脚也由 Pi Hut 等供应商作为套件出售(thepihut.com/collections/raspberry-pi-zero/products/raspberry-pi-zero-essential-kit)。

2x20 引脚用于 Raspberry Pi Zero

为了将引脚焊接在 Raspberry Pi Zero 上,按照以下图示将引脚排列在面包板上:

将引脚排列在 Raspberry Pi 上焊接

执行以下步骤:

  1. 将 Raspberry Pi 倒置放在引脚上。

  2. 轻轻握住 Raspberry Pi(以确保在焊接时引脚位置正确)并将引脚焊接在 Raspberry Pi 上。

  3. 检查电路板以确保引脚焊接正确,并小心地将 Raspberry Pi Zero 从面包板上取下。

焊接在 Raspberry Pi 上的引脚

我们已经准备好在这本书中使用 GPIO 引脚了!让我们继续到下一节。

如果不使用正确的温度设置,使用面包板焊接 Raspberry Pi 上的引脚可能会损坏面包板。面包板的金属触点可能会永久膨胀,导致永久性损坏。基本焊接技术的培训至关重要,并且有大量关于这个主题的教程。

Raspberry Pi Zero 的机箱

在外壳内设置 Raspberry Pi zero 完全可选,但在进行项目工作时非常有用。供应商出售了大量的外壳。或者,你也可以从 Thingiverse 下载外壳设计并 3D 打印它们。我们发现这个外壳符合我们的需求,因为它提供了访问 GPIO 引脚的接口。www.thingiverse.com/thing:1203246 我们还发现,通过 3D Hubs (www.3dhubs.com/) 的 3D 打印服务,可以通过本地打印机以 9 美元的费用打印外壳。或者,你也可以使用预设计的项目外壳或设计一个可以使用 亚克力板 或类似材料构建的外壳。

图片

带外壳的 Raspberry Pi Zero

Raspberry Pi 的操作系统设置

让我们继续准备一个 micro SD 卡来设置 Raspberry Pi Zero。在这本书中,我们将使用 Raspbian 操作系统。Raspbian 操作系统拥有广泛的用户基础,并且该操作系统由 Raspberry Pi 基金会官方支持。因此,在项目工作中更容易在论坛上找到支持,因为更多的人熟悉该操作系统。

micro SD 卡准备

如果你购买了一张预先闪存了 Raspbian New Out of the Box Software(NOOBS)图像的 micro SD 卡,你可以跳过 micro SD 卡的准备:

  1. 第一步是下载 Raspbian NOOBS 图像。该图像可以从 www.raspberrypi.org/downloads/noobs/ 下载。

图片

下载 Raspberry Pi NOOBS 图像

  1. 使用 SD 卡格式化工具格式化你的 SD 卡。确保 FORMAT SIZE ADJUSTMENT 如快照所示是开启的(从 www.sdcard.org/downloads/formatter_4/index.html 可获得):

图片

格式化 SD 卡

  1. 提取下载的 ZIP 文件,并将文件内容复制到格式化的 micro SD 卡中。

  2. 设置 Raspberry Pi(不一定是按照相同的顺序):

  • 通过迷你 HDMI 接口将 HDMI 线从显示器连接到 Raspberry Pi

  • 通过 Raspberry Pi Zero 的 USB OTG 接口使用 USB HUB

  • Micro-USB 线为 Raspberry Pi Zero 提供电

  • 将 Wi-Fi 天线、键盘和鼠标连接到 Raspberry Pi Zero

图片

配备键盘、鼠标和 Wi-Fi 天线的 Raspberry Pi Zero

  1. 启动 Raspberry Pi,它应该会自动将操作系统闪存到 SD 卡并在启动时启动桌面。

  2. 启动后的第一步是更改 Raspberry Pi 的密码。转到 菜单(位于左上角的 Raspberry Pi 图标)并在“首选项”下选择“Raspberry Pi 配置”。

图片

启动 Raspberry Pi 配置

  1. 在“系统”选项卡下,更改密码:

图片

更改密码

  1. 在本地化选项卡下,根据您的区域更改区域设置、时区和键盘设置。

  2. 安装完成后,将 Raspberry Pi Zero 连接到无线网络(使用右上角的无线选项卡)。

图片

启动 Raspberry Pi 桌面

  1. 让我们启动 Raspberry Pi 的命令行终端来执行一些软件更新。

图片

启动命令行终端

  1. 从命令行终端运行以下命令:
       sudo apt-get update
 sudo apt-get upgrade

操作系统升级应在几分钟内完成。

Raspberry Pi 基金会在其网站上提供了一个视频,用于设置 Raspberry Pi 的视觉辅助。该视频可在vimeo.com/90518800找到。

让我们学习 Python!

Python 是由 Guido Van Rossum 发明的高级编程语言。以下是一些使用 Raspberry Pi 学习 Python 的优势:

  • 它具有非常简单的语法,因此很容易理解。

  • 它提供了将想法作为一系列脚本实现的可灵活性。这对于爱好者实现他们的想法非常有帮助。

  • 对于 Raspberry Pi 的 GPIO,有 Python 库。这使得将传感器/设备与 Raspberry Pi 轻松接口成为可能。

  • Python 被谷歌等科技巨头广泛应用于各种应用中。这些应用从简单的机器人到个人人工智能助手和太空中的控制模块都有涉及。

  • Raspberry Pi 拥有不断增长的粉丝群体。结合 Python 庞大的用户基础,这意味着学习资源和项目支持不会短缺。

在这本书中,我们将学习 Python 3.x 版本。我们将通过示例演示来学习 Python 编程的每个主要方面。通过自己动手实践来发现 Python 的奇妙之处!请记住,还有 Python 2.x,它与 Python 3.x 有一些细微的差别。

如果你熟悉 Linux 命令行终端,我们建议按照第十一章中所示设置 Raspberry Pi 进行远程开发,技巧与窍门

Hello World 示例

由于我们已经完成了 Raspberry Pi 的设置,让我们开始编写我们的第一段 Python 代码。在学习一门新的编程语言时,通常的做法是在计算机屏幕上打印Hello World。让我们使用 Python 打印以下信息:我非常兴奋地使用 Raspberry Pi Zero 学习 Python 编程

在这本书中,我们将使用集成开发和学习环境IDLE)工具来学习 Python。我们选择 IDLE 的原因如下:

  • 工具作为 Raspbian OS 镜像中的包安装和分发。无需额外的安装。

  • 它配备了一个交互式工具,可以用来检查一段代码或 Python 语言的具体特性。

  • 它附带一个文本编辑器,允许根据 Python 编程语言的约定编写代码。文本编辑器为 Python 脚本的不同元素提供颜色代码。这有助于相对容易地编写 Python 脚本。

  • 工具允许逐步执行任何代码示例并识别其中的问题。

设置 Raspberry Pi Zero 以进行 Python 编程

在我们开始之前,让我们先设置 Raspberry Pi Zero 以满足我们的需求:

  1. 让我们在 Raspberry Pi 的桌面上为 IDLE3(用于 Python 3.x 开发)添加一个快捷方式。在编程子菜单下(位于 Raspberry Pi Zero 桌面的左上角),右键单击 Python 3(IDLE)并点击“添加到桌面”。这将在桌面上添加 IDLE 工具的快捷方式,使其易于访问。

将 IDLE3 快捷方式添加到 Raspberry Pi 的桌面

  1. 为了保存所有代码示例,让我们在 Raspberry Pi 的桌面上创建一个名为 code_samples 的文件夹。在桌面上右键单击并创建一个新的文件夹。

IDLE 的交互式工具

让我们使用 IDLE 的交互式工具编写我们的第一个示例:

  1. 通过双击 Raspberry Pi Zero 的桌面上的 IDLE3(适用于 Python 3.x)工具来启动它。

  2. 从 IDLE 的交互式命令行工具中,输入以下行:

       print("I am excited to learn Python with the Raspberry Pi Zero")

  1. 这应该在交互式命令行工具的屏幕上打印以下内容:

我们做到了!我们编写了一行代码,将其打印到 Raspberry Pi 的屏幕上。

文本编辑方法

命令行工具对于测试编码逻辑很有用,但使用交互式工具编写代码既不实用也不优雅。一次编写大量代码并测试它更容易。让我们使用 IDLE 的文本编辑器重复相同的示例:

  1. 启动 IDLE 的文本编辑器(在 IDLE 中,文件 | 新文件),输入上一节讨论的 hello world 行并将其保存为 helloworld.py

  2. 现在,可以通过按 F5 键或从“运行”下拉菜单中选择“运行模块”来执行代码,你将得到以下图示的输出:

通过 Linux 终端启动 Python 解释器

也可以通过 Linux 终端 使用 Python 解释器。程序员通常使用它来测试他们的代码或参考 Python 文档工具 pydoc。如果读者计划使用除 IDLE 之外的其他文本编辑器,这种方法很方便:

  1. 从桌面工具栏启动 Raspberry Pi 的命令行终端。

启动命令行终端

  1. 输入命令,python3 并按 Enter 键。这应该在终端上启动 Python 3.x。

  2. 现在,尝试运行上一节讨论的相同代码片段:

       print("I am excited to learn Python with the Raspberry Pi Zero")

这将给出以下截图作为结果:

结果应该与前面两个部分类似

在 Linux 终端中,可以通过输入 exit() 并按回车键来关闭 Python 解释器。

使用 Linux 终端执行 Python 脚本

你可以通过 Linux 终端执行任何使用文本编辑器编写的代码。例如,假设文件 helloworld.py 存储在 Raspberry Pi 桌面上的名为 code_samples 的文件夹中。这个文件可以从 Linux 终端按如下方式执行:

如果你不太熟悉 Linux 命令行终端,我们在本书的网站上编写了一些教程,帮助你熟悉命令行终端。

  1. 在 Linux 终端上,切换到 Python 脚本所在的目录:
       cd /home/pi/Desktop/code_samples

  1. 按如下方式执行 Python 脚本:
       python3 helloworld.py

  1. 或者,可以使用 Python 脚本的绝对位置路径来执行:
       python3 /home/pi/Desktop/code_samples/hello_world.py

我们做到了!我们刚刚编写了我们的第一段代码,并讨论了执行代码的不同方法。

print() 函数

在我们的第一个 helloworld 示例中,我们讨论了如何在屏幕上打印内容。我们使用了 print() 函数来获取结果。在 Python 中,函数 是一个执行一系列定义任务的代码块。print() 函数是 Python 标准库的一部分,它可以将作为参数传递的任何数字和字母字符组合打印到屏幕上。print() 函数用于将信息打印到屏幕上。它在尝试调试代码时特别有用。在这个例子中,print() 函数被用来在屏幕上打印一条消息。

在本章中,函数 print() 执行了字符串 I am excited to learn Python programming with the Raspberry Pi Zero(我们将在本书的后续章节中讨论字符串)。你也可以编写自定义函数来执行用户需要的重复性任务。

同样,exit() 函数执行用户请求退出 Python 解释器的预定义任务。

help() 函数

在开始学习时,记住 Python 中每个函数的语法可能会有些困难。你可以通过 Python 中的 help 函数来参考函数的文档和语法。例如,为了找到 Python 中 print 函数的使用方法,我们可以在命令行终端或交互式外壳中调用 help,如下所示:

    help(print)

这将返回函数及其语法的详细描述:

概述

就这样!在本章中,我们设置了 Raspberry Pi Zero,以便用 Python 编写我们的第一个程序。我们还探索了编写 Python 程序的不同选项。你现在已经准备好,并且正在学习使用 Raspberry Pi 学习 Python 的路上。在下一章中,我们将深入了解 GPIO 引脚,并在执行一个使 LED 闪烁的简单项目时学习更多相关知识。

第二章:算术运算、循环和闪烁的 LED 灯

在上一章中,我们讨论了如何在屏幕上打印一行文本。在本章中,我们将回顾 Python 中的算术运算和变量。我们还将讨论字符串和 Python 中的用户输入。你将了解 Raspberry Pi 的 GPIO 及其特性,并使用 Python 编写代码,通过 Raspberry Pi 的 GPIO 使 LED 闪烁。我们还将讨论控制 Raspberry Pi 的 GPIO 的实际应用。

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

  • Python 中的算术运算

  • Python 中的位运算符

  • Python 中的逻辑运算符

  • Python 中的数据类型和变量

  • Python 中的循环

  • Raspberry Pi Zero 的 GPIO 接口。

本章所需的硬件

在本章中,我们将讨论控制 Raspberry Pi 的 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

  • 地板运算符//)是取模运算符的相反数。此运算符返回商的整数部分,即整数结果,并丢弃小数部分:
       >>>9//7 
       1 
       >>>7//3 
       2 
       >>>79//25 
       3

Python 中的位运算符

在 Python 中,可以对数字执行位级操作。这在解析来自某些传感器的信息时特别有用。例如,某些传感器以一定频率共享它们的输出。当有新的数据点可用时,某个位被设置,表示数据可用。位运算符可以用来检查在从传感器检索数据点之前,特定的位是否被设置。

如果你对位运算符的深入探讨感兴趣,我们建议从en.wikipedia.org/wiki/Bitwise_operation开始。

考虑数字32,它们的二进制表示分别为011010。让我们看看不同的运算符,它们会对数字的每一位执行操作:

  • 与运算符:与运算符用于对两个数执行与运算。尝试使用 Python 解释器运行以下示例:
       >>>3&2 
       2

这相当于以下与运算:

   0 1 1 &
   0 1 0
   --------
   0 1 0 (the binary representation of the number 2)

  • 或运算符:或运算符用于对两个数执行或运算,如下所示:
       >>>3|2 
       3

这相当于以下或运算:

   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,当执行位非操作时,结果是 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()函数向 Python 程序提供用户输入(docs.python.org/3/library/functions.html#input):

    var = input("Enter the first number: ")

在前面的例子中,我们正在使用input()函数来获取用户输入的数字。input()函数将提示("请输入第一个数字:")作为参数,并返回用户输入。在这个例子中,用户输入存储在变量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 函数还可以通过将起始值、停止值和步长值作为参数传递给 range() 函数来生成一个步长为 2 的数字列表:

    for i in range(0, 20, 2): 
       print("Loop execution no: ", i)

在这个例子中,0 是起始值,20 是停止值,2 是步长值。这生成了一个步长为两的 10 个数字列表:

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 编程语言的一个特性。只要代码具有相同的缩进级别,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 循环通常用于执行无限循环中的代码。让我们看看一个具体的例子,我们想要打印 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 函数。在前面提到的例子中,The value of i,j isiprint 函数的参数。您将在下一章中了解更多关于函数和参数的内容。这个特性使得可以格式化输出字符串以满足我们的需求。

Raspberry Pi 的 GPIO

Raspberry Pi Zero 配备了一个 40 引脚的 GPIO 头。在这 40 个引脚中,我们可以使用 26 个引脚来读取输入(来自传感器)或控制输出。其余的引脚是电源引脚(5V3.3V引脚):

Raspberry Pi Zero GPIO 映射(来源:https://www.raspberrypi.org/documentation/usage/gpio-plus-and-raspi2/README.md)

我们可以使用 Raspberry Pi 的 GPIO 的最多 26 个引脚来接口电器并控制它们。但是,某些引脚具有替代功能,这将在后续章节中讨论。

早期图像显示了 Raspberry Pi 的 GPIO 引脚映射。圆圈中的数字对应于 Raspberry Pi 处理器上的引脚编号。例如,GPIO 引脚 2(底部一行的左侧第二个引脚)对应于 Raspberry Pi 处理器上的 GPIO 引脚 2,而不是 GPIO 头上的物理引脚位置。

在一开始,尝试理解引脚映射可能会感到困惑。请保留一份 GPIO 引脚清单(与本章一起提供下载)以供参考。熟悉 Raspberry Pi Zero 的 GPIO 引脚映射需要一些时间。

Raspberry Pi Zero 的 GPIO 引脚可以承受 3.3V 电压,也就是说,如果施加的电压大于 3.3V,可能会永久损坏引脚。当设置为 时,引脚被设置为 3.3V 和 0V;当引脚被设置为 时。

闪烁灯

让我们讨论一个使用 Raspberry Pi Zero 的 GPIO 的例子。我们将连接一个 LED 到 Raspberry Pi Zero,并使其以 1 秒的间隔闪烁打开和关闭。

让我们连接 Raspberry Pi zero 来开始:

使用 Fritzing 生成的 Blinky 电路图

在前面的电路图中,GPIO 引脚 2 连接到 LED 的阳极(最长的腿)。LED 的阴极连接到 Raspberry Pi Zero 的地线引脚。还使用了一个 330 欧姆的限流电阻来限制电流的流动。

与 Raspberry Pi Zero 的面包板连接

代码

我们将使用 python3-gpiozero 库 (gpiozero.readthedocs.io/en/v1.3.1/)。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 循环以 1 秒的间隔来打开和关闭 LED。

gpiozero 库的 LED 类包含名为 on()off() 的函数,分别用于将 GPIO 引脚 2 设置为高电平和低电平:

while True: 
    led.on() 
    time.sleep(1) 
    led.off() 
    time.sleep(1)

在 Python 的 time 库中,有一个 sleep 函数,可以在打开/关闭 LED 之间引入 1 秒的延迟。这是在一个无限循环中执行的!我们刚刚使用 Raspberry Pi Zero 建立了一个实际示例。

将所有代码合并到一个名为 blinky.py 的文件中(与本书一起提供下载),从命令行终端运行代码(或者,您也可以使用 IDLE3):

    python3 blinky.py

GPIO 控制的应用

现在我们已经实现了第一个示例,让我们讨论一下能够控制 GPIO 的可能应用。我们可以使用 Raspberry Pi 的 GPIO 来控制家里的灯光。我们将使用相同的示例来控制台灯!

有一个名为 PowerSwitch Tail II 的产品 (www.powerswitchtail.com/Pages/default.aspx),它可以将交流电器(如台灯)连接到 Raspberry Pi。PowerSwitch Tail 配有控制引脚(可以接收 3.3V 高电平信号),可以用来打开/关闭灯。开关配备了必要的电路/保护,可以直接连接到 Raspberry Pi Zero:

Pi Zero 连接到 PowerSwitch Tail II

让我们拿上一节中的相同例子,将 GPIO 引脚 2 连接到 PowerSwitch Tail 的 +in 引脚。让我们将 Raspberry Pi Zero GPIO 头的接地引脚连接到 PowerSwitch Tail 的 -in 引脚。PowerSwitch Tail 应该连接到交流电源。灯应该连接到开关的交流输出。如果我们使用相同的代码并将灯连接到 PowerSwitch Tail,我们应该能够以 1 秒的间隔开关灯。

图片

PowerSwitch Tail II 连接到 Raspberry Pi Zero

使用 LED 闪烁代码进行家电控制只是一个示例。不建议在如此短的时间内开关台灯。在未来的章节中,我们将利用 Raspberry Pi Zero 的 GPIO 从互联网上的任何地方控制家电。

摘要

在本章中,我们回顾了 Python 中的整数、布尔和字符串数据类型,以及算术运算和逻辑运算符。我们还讨论了接受用户输入和循环。我们介绍了 Raspberry Pi Zero 的 GPIO 并讨论了一个 LED 闪烁示例。我们用同样的例子来控制台灯!

你听说过名为 Slack 的聊天应用吗?或者从你的工作笔记本电脑上控制家里的台灯?如果你对此感兴趣,那么在接下来的几章中,我们可以一起努力实现这个目标。

第三章:条件语句、函数和列表

在本章中,我们将基于你之前学到的内容进行构建。你将学习关于条件语句以及如何使用逻辑运算符通过条件语句检查条件。接下来,你将学习如何在 Python 中编写简单的函数,并讨论使用触摸开关(瞬间按键)将输入接口连接到树莓派的 GPIO 引脚。我们还将讨论使用树莓派 Zero 进行电机控制(这是最终项目的预热),并通过开关输入控制电机。让我们开始吧!

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

  • Python 中的条件语句

    • 使用基于 GPIO 引脚状态的 GPIO 输入进行操作

    • 使用条件语句跳出循环

  • Python 中的函数

    • GPIO 回调函数
  • Python 中的电机控制

条件语句

在 Python 中,条件语句用于通过测试条件是否为truefalse来确定是否满足特定条件。条件语句用于确定程序的执行方式。例如,条件语句可以用来确定是否是时候打开灯光了。语法如下:

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,屏幕上会打印消息您的地址已保存

同样,如果用户输入是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之外的随机字符,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)语句下的缩进代码块。如果用户输入其他内容,程序将打印消息:无效输入。请重试

需要注意的是,代码块的缩进决定了在满足特定条件时需要执行的代码块。我们建议修改条件语句块的缩进,并找出程序执行会发生什么。这将有助于理解缩进在 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 执行任务

在前面的章节中,我们讨论了将输出连接到 Raspberry Pi 的 GPIO。现在让我们讨论一个简单的按钮被按下的例子。按钮按下是通过读取 GPIO 引脚状态来检测的。我们将利用条件语句根据 GPIO 引脚状态执行任务。

让我们将一个按钮连接到 Raspberry Pi 的 GPIO。开始所需的所有东西只是一个按钮、上拉电阻和一些跳线。后面的图示展示了如何将按钮连接到 Raspberry Pi Zero。按钮的一个端子连接到 Raspberry Pi Zero GPIO 引脚的地线。

按钮接口的原理图如下所示:

Raspberry Pi GPIO 原理图

按钮的另一端通过一个 10 K 欧姆电阻拉到 3.3V。按钮端子和 10 K 欧姆电阻的连接点连接到 GPIO 引脚 2(参考前面章节中共享的 BCM GPIO 引脚图)。

将按钮连接到 Raspberry Pi Zero 的 GPIO - 使用 Fritzing 生成的图像

让我们回顾一下检查按钮状态的代码。我们使用循环和条件语句通过 Raspberry Pi Zero 读取按钮输入。

我们将使用前面章节中介绍的gpiozero库。本节的代码示例为GPIO_button_test.py,与本章一起提供下载。

在后面的章节中,我们将讨论面向对象编程OOP)。现在,让我们简要地讨论一下这个例子中的类概念。Python 中的是一个包含定义对象的全部属性的蓝图。例如,gpiozero库中的Button类包含了将按钮连接到 Raspberry Pi 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方法暂停,直到按钮释放。当按钮释放时,用于存储按下次数的变量会增加 1。

当按钮接收到 10 次按下时,程序会跳出无限循环。

图片

一个红色瞬态按钮连接到 Raspberry Pi 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)

前面的函数期望两个参数。如果我们只向这个函数传递一个参数,则另一个参数默认为0。例如,add_function(a=3)b默认为0,或者add_function(b=2)a默认为0。当在调用函数时没有提供参数,它默认为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 示例中函数的一些用法。函数可以用来处理与 Raspberry Pi 的 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 中的直流电机控制

在本节中,我们将讨论使用 Raspberry Pi Zero 进行电机控制。为什么要讨论电机控制?随着我们在本书中探讨不同主题的进展,我们将最终构建一个移动机器人。因此,我们需要讨论使用 Raspberry Pi 编写代码来控制电机。

为了控制电机,我们需要一个H-bridge 电机驱动器(讨论 H-bridge 超出了我们的范围。有关 H-bridge 电机驱动器的资源有:www.mcmanis.com/chuck/robotics/tutorial/h-bridge/)。为 Raspberry Pi 设计了几个电机驱动器套件。在本节中,我们将使用以下套件: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()

基于 Raspberry Pi 的电机控制

为了控制电机,让我们声明引脚、电机速度引脚和方向引脚。根据电机驱动器的文档,电机由 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()

为读者提供的几个迷你项目挑战

这里有一些为读者准备的迷你项目挑战:

  • 在本章中,我们讨论了 Raspberry Pi 的输入接口和电机控制。考虑一个项目,我们可以驱动一个读取触须开关输入并操作移动机器人的移动机器人。结合限位开关和电机,能否构建一个跟随墙壁的机器人?

  • 本章我们讨论了直流电机的控制。我们如何使用 Raspberry Pi 控制步进电机?

  • 我们如何使用 Raspberry Pi Zero 将运动传感器连接到家中控制灯光?

继续阅读以了解更多!

想要在你的朋友面前玩些小把戏吗?用你的 Raspberry Pi Zero?查看这本书的网站!

摘要

本章我们讨论了条件语句及其在 Python 中的应用。我们还讨论了 Python 中的函数,包括向函数传递参数、从函数返回值以及在 Python 程序中变量的作用域。我们还讨论了回调函数和 Python 中的电机控制。

第四章:通信接口

到目前为止,我们已经讨论了 Python 中的循环、条件语句和函数。我们还讨论了接口输出设备和简单的数字输入设备。

在本章中,我们将讨论以下通信接口:

  • UART – 串行端口

  • 串行外围接口

  • I²C 接口

我们将使用不同的传感器/电子组件来演示编写用于这些接口的 Python 代码。我们将由你选择一个你喜欢的组件来探索这些通信接口。

UART – 串行端口

通用异步收发器UART),是一个串行端口,数据以位的形式从传感器串行传输到主机计算机。使用串行端口是通信协议的最古老形式之一。它用于数据记录,其中微控制器从传感器收集数据并通过串行端口传输数据。还有一些传感器通过串行通信作为对传入命令的响应来传输数据。

我们不会深入讨论串行端口通信的理论(网上有大量的理论资料,见 en.wikipedia.org/wiki/Universal_asynchronous_receiver/transmitter)。我们将讨论使用串行端口将不同的传感器与 Raspberry Pi 接口。

Raspberry Pi Zero 的 UART 端口

通常,UART 端口由一个接收器(Rx)和一个发送器(Tx)引脚组成,用于接收和发送数据。Raspberry Pi 的 GPIO 引脚头带有 UART 端口。GPIO 引脚 14(Tx 引脚)和 15(是 Rx 引脚)作为 Raspberry Pi 的 UART 端口:

GPIO 引脚 14 和 15 是 UART 引脚(图片来源:https://www.rs-online.com/designspark/introducing-the-raspberry-pi-b-plus)

设置 Raspberry Pi Zero 的串行端口

为了使用串行端口与传感器通信,需要禁用串行端口登录/控制台。在 Raspbian 操作系统映像中,这默认启用,因为它可以方便地进行调试。

可以通过 raspi-config 禁用串行端口登录:

  1. 启动终端并运行此命令:
       sudo raspi-config

  1. raspi-config 的主菜单中选择高级选项:

从 raspi-config 菜单中选择高级选项

  1. 从下拉菜单中选择 A8 串行选项:

从下拉菜单中选择 A8 串行

  1. 禁用串行登录:

禁用串行登录

  1. 完成配置并在最后重启:

保存配置并重启

示例 1 - 将二氧化碳传感器连接到 Raspberry Pi

我们将使用 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)是一种串行通信,允许将多个传感器连接到计算机。I²C 通信由时钟和数据线两根线组成。Raspberry Pi Zero 的 I²C 通信时钟和数据引脚分别为 GPIO 3SCL)和 GPIO 2SDA)。为了通过同一总线与多个传感器通信,通常通过它们的 7 位地址来寻址通过 I²C 协议通信的传感器/执行器。可以有两个或更多 Raspberry Pi 板与同一 I²C 总线上的同一传感器通信。这使您能够围绕 Raspberry Pi 建立一个传感器网络。

I²C 通信线是开漏线;因此,它们使用电阻拉起,如下图所示:

I²C 设置

让我们通过一个示例来回顾 I²C 通信。

示例 2 - PiGlow

PiGlow 是为 Raspberry Pi 设计的附加硬件,由 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 - 为 Raspberry Pi 设计的传感器附加硬件

Sensorian 是为 Raspberry Pi 设计的附加硬件。此附加硬件包含不同类型的传感器,包括光传感器、气压计、加速度计、LCD 显示接口、闪存、电容式触摸传感器和实时时钟。

此附加硬件上的传感器足以学习使用本章讨论的所有通信接口:

Sensorian 硬件堆叠在 Raspberry Pi Zero 之上

在本节中,我们将讨论一个示例,我们将通过 I²C 接口使用 Raspberry Pi Zero 测量环境光水平。附加硬件板上的传感器是APDS-9300传感器(www.avagotech.com/docs/AV02-1077EN)。

勒克斯传感器的 I2C 驱动程序

驱动程序可以从 GitHub 仓库获取的 Sensorian 硬件(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 通信(我们可以连接多个主设备)不同,只能有一个主设备(Raspberry Pi Zero),但同一总线上可以有多个从设备。SS引脚在多个传感器连接到同一总线上时,允许选择 Raspberry Pi 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)通过 Adafruit Cobbler(www.adafruit.com/product/914)与 Raspberry Pi 附加硬件的 SPI 接口相连。我们提供了一个如何将 LED 灯带连接到 Raspberry Pi Zero 的线索。我们想看看你是否能自己找到一种将 LED 灯带连接起来的解决方案。请参考本书的网站以获取答案。

图片

与 Raspberry Pi Zero 的 Adafruit Cobbler 接口的 LED 灯带

摘要

在本章中,我们讨论了 Raspberry Pi Zero 上可用的不同通信接口。这些接口包括 I²C、SPI 和 UART。我们将在我们的最终项目中使用这些接口。我们使用二氧化碳传感器、LED 驱动器和传感器平台来讨论这些接口。在下一章中,我们将讨论面向对象编程及其独特优势。我们将通过一个示例来讨论面向对象编程的必要性。面向对象编程在需要编写自己的驱动程序来控制机器人组件或编写传感器接口库的场景中尤其有帮助。

第五章:Python 中的数据类型和面向对象编程

在本章中,我们将讨论 Python 中的数据类型和面向对象编程(OOP)。我们将讨论包括列表、字典、元组和集合在内的 Python 中的数据类型。我们还将讨论 OOP 的必要性以及如何在基于 Raspberry Pi 的项目中编写面向对象的代码(例如,使用 OOP 控制家用电器)。我们还将讨论在 Raspberry Pi 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 之间的数字。如果数字是列表的成员,则屏幕上打印出消息 Your guess is correct,否则,打印出消息 Sorry! Your guess is incorrect。我们将第二部分留给读者作为挑战。可以从本章提供的下载代码示例 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 中的面向对象编程(OOP)

面向对象编程(OOP)是一个有助于简化代码并简化应用程序开发的理念。它特别有助于代码的重用。面向对象的代码允许为使用通信接口的传感器重用代码。例如,所有配备 UART 端口的传感器都可以使用面向对象的代码分组在一起。

面向对象编程的一个例子是之前章节中使用的GPIO Zero 库www.raspberrypi.org/blog/gpio-zero-a-friendly-python-api-for-physical-computing/)。实际上,在 Python 中,一切都是对象。

面向对象的代码在与他人合作的项目中特别有帮助。例如,你可以使用 Python 的面向对象代码实现一个传感器驱动程序,并记录其用法。这使得其他开发者可以在不关注传感器接口背后的细节的情况下开发应用程序。面向对象编程为应用程序提供了模块化,简化了应用程序开发。在本章中,我们将回顾一个示例,以展示面向对象编程的优势。在本章中,我们将利用面向对象编程为我们的项目带来模块化。

让我们开始吧!

回顾学生身份证示例

让我们回顾一下来自 第二章,算术运算、循环和闪烁灯input_test.py)的身份证示例。我们讨论了编写一个简单的程序来捕获并打印属于学生的信息。学生的联系信息可以按以下方式检索和存储:

name = input("What is your name? ") 
address = input("What is your address? ") 
age = input("How old are you? ")

现在,考虑一个场景,在程序执行期间需要保存和检索 10 名学生的信息。我们需要为用于保存学生信息的变量制定一个命名约定。如果我们使用 30 个不同的变量来存储每个学生的信息,将会很混乱。这正是面向对象编程可以真正发挥作用的地方。

让我们使用面向对象编程(OOP)重写这个例子以简化问题。OOP 的第一步是声明对象的结构。这是通过定义一个类来完成的。类决定了对象的功能。让我们编写一个 Python 类,定义学生对象的结构。

由于我们将保存学生信息,因此类将被命名为 Student。类是通过 class 关键字定义的,如下所示:

class Student(object):

因此,已定义了一个名为 Student 的类。每当创建一个新对象时,Python 会内部调用 __init__() 方法(下划线表示 init 方法是一个魔法方法,即它是 Python 在创建对象时调用的函数)。

此方法在类内部定义:

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")

在这个例子中,我们创建了一个名为 student1Student 类对象,其中 John Doe(姓名)、29(年龄)和 123 Main Street, Newark, CA(地址)是创建对象所需的属性。当我们通过传递必需的参数(在 Student 类的 __init__() 方法中先前声明)创建属于 Student 类的对象时,__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 解释器会忽略以 # 符号开始的任何单行注释。单行注释通常用于对代码块进行特定注释。包含良好结构的注释可以使你的代码易于阅读。

例如,以下代码片段通知读者生成了一个介于 09 之间的随机数,并将其存储在变量 rand_num 中:

# 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 和 Raspberry Pi 的示例。

扬声器控制器

让我们编写一个 Python 类(下载中的tone_player.py),该类播放一个音乐音调,表示你的 Raspberry Pi 启动完成。对于本节,你需要一个 USB 声卡和一个连接到 Raspberry Pi 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类保存到你的 Raspberry Pi(保存到名为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)来控制灯光。它如下所示连接:

图片

Raspberry Pi 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 关闭。一旦任务被安排,程序进入一个无限循环,等待任务执行。这个示例可以作为守护进程运行(在启动时执行文件,将名为light_scheduler.py的行添加到/etc/rc.local中)。在安排任务后,它将继续作为守护进程在后台运行。

这只是一个对面向对象编程及其应用的简单介绍(考虑到初学者的需求)。有关 OOP 的更多示例,请参阅本书的网站。

摘要

在本章中,我们讨论了列表和面向对象编程(OOP)的优势。我们以树莓派为中心,讨论了 OOP 的示例。由于本书主要面向初学者,我们决定在讨论示例时坚持 OOP 的基础知识。书中未涉及一些高级内容。我们留给读者使用本书网站上提供的其他示例来学习高级概念。

第六章:文件 I/O 和 Python 工具

在本章中,我们将详细讨论文件 I/O,即读取、写入和追加到文件。我们还将讨论 Python 工具,这些工具可以用来操作文件并与操作系统交互。每个主题都有不同的复杂度,我们将通过示例来讨论。让我们开始吧!

文件 I/O

我们讨论文件 I/O 的原因有两个:

  • 在 Linux 操作系统世界中,一切都是文件。与树莓派上的外围设备交互类似于从文件中读取/写入。例如:在 第四章 通信接口 中,我们讨论了串口通信。你应该能够观察到串口通信就像文件读写操作一样。

  • 我们在每一个项目中都会以某种形式使用文件 I/O。例如:将传感器数据写入 CSV 文件或读取为网络服务器预配置的选项,等等。

因此,我们认为讨论 Python 中的文件 I/O 作为单独的一章是有用的(详细文档可从这里获取:docs.python.org/3/tutorial/inputoutput.html#reading-and-writing-files)并讨论在树莓派零上开发应用程序时它可能发挥作用的示例。

从文件中读取

让我们创建一个简单的文本文件,read_file.txt,内容如下:我正在使用树莓派零学习 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() 函数关闭文件。

使用 IDLE3 或命令行终端运行前面的代码片段(与本章一起提供下载——read_from_file.py),文本文件的内容将按如下方式打印到屏幕上:

    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 的一部分,与本章一起提供)时,readline() 函数打开 read_line.txt 文件,并返回一个单独的行。这行被存储在变量 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,它允许将数据追加到文件末尾。如果传递给 open 函数的文件不存在,此标志也会创建一个新文件。让我们考虑下面的代码片段,其中我们将一行追加到前一个部分中的文本文件 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

seek

一旦文件被打开,用于文件输入/输出的文件指针将从文件开始移动到文件末尾。可以将指针移动到特定位置并从该位置读取数据。这在我们需要关注文件中的特定行时特别有用。让我们考虑前一个例子中的文本文件 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() 函数允许删除文件。任何作为函数参数传递的文件都将被删除。在 文件输入/输出 部分中,我们讨论了使用文本文件 read_file.txt 从文件中读取。让我们通过将文件作为参数传递给 remove() 函数来删除该文件:

os.remove('/home/pi/Desktop/code_samples/read_file.txt')

杀死进程

可以通过将进程 pid 传递给 kill() 函数来杀死在树莓派上运行的应用程序。在上一章中,我们讨论了作为树莓派后台进程运行的 light_scheduler 示例。为了演示杀死进程,我们将尝试杀死该进程。我们需要确定 light_scheduler 进程的进程 pid(你可以选择你作为用户启动的应用程序,不要触碰 root 进程)。进程 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关键字来尝试终止应用程序。我们将在下一章详细讨论这些关键字。

还可以使用kill()函数和try/except关键字来监控应用程序是否正在运行。在介绍使用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 模块(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 一起提供下载。在运行程序时传递的命令行参数列表作为 argv 列表在 sys 模块中可用。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 框架。我们将讨论使从网络检索数据(例如,获取天气更新)、上传数据到远程服务器(例如,记录传感器数据)或控制本地网络上的设备成为可能的库和框架。我们还将讨论有助于学习本章核心主题的话题。

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. 如果发生了ValueErrorerror会被except块捕获,并且实际错误信息会与以下信息一起打印到屏幕上:

       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关键字下的代码都会执行,而finally关键字下的代码块在退出try...except块时执行(在退出代码块时清理资源很有用)。

在玩游戏时,尝试使用之前的代码示例提供无效的输入,以了解代码块流程。

连接到互联网 - 网络请求

现在我们已经讨论了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. 将前面例子(internet_access.py)中的 URL 替换为本文中讨论的 URL。这将返回 JSON 格式的天气信息。

  2. 请求模块提供了一个解析 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

请求的应用 - 向互联网发布事件

在前面的例子中,我们从互联网检索了信息。让我们考虑一个例子,其中我们不得不在互联网上的某个地方发布一个传感器事件。这可能是在您离家时猫门打开,或者有人在家门口踩到门垫。因为我们已经在上一章讨论了将传感器连接到树莓派 Zero,让我们讨论一个可以将这些事件发布到Slack(一个工作场所沟通工具)、Twitter 或云服务(如Phant data.sparkfun.com/)的场景。

在此示例中,我们将使用requests将这些事件发布到 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. 当发生事件时,让我们给自己发送一条私密消息。选择“私下发送给你”作为选项,并通过点击添加 Incoming WebHooks 集成来创建 webhook:

图片

选择“私下发送给你”

  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 通过 Raspberry Pi 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 服务器。为了打开灯,输入 <Raspberry Pi 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 和摄像头的成本)。

我们建议使用 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/ 安装 OpenCV 到 Raspberry Pi Zero 的说明。我们特别遵循了使用 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 库的安装,请尝试按照此示例加载图像。它应该以灰度形式加载图像,如下所示:

以灰度加载的 Raspberry Pi 桌面

这个窗口将在按下任何键时关闭。

对读者的挑战

在前面的例子中,窗口在按下任何键时关闭。请查看文档,确定是否可以在按下鼠标按钮时关闭所有窗口。

将相机安装到 Raspberry Zero

测试我们下一个示例需要一个相机连接器和相机。以下是一个购买相机和适配器的来源:

名称 来源
Raspberry Pi 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。以下图显示了使用相机拍摄的照片快照:

使用 Raspberry Pi 摄像头模块捕获的图像

  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 中开发图像处理应用程序。在第十章[8ab7d103-3b8b-459e-b64c-fb95200c8a52.xhtml],使用 Raspberry Pi Zero 的家庭自动化中,我们展示了另一个使用 OpenCV 的示例。这应该可以帮助您开始学习 Python 中的 OpenCV。我们还建议查看 OpenCV Python 绑定文档中提供的示例(本节介绍部分提供的链接)。

语音识别

在本节中,我们将讨论在 Python 中开发涉及语音识别的语音识别示例。我们将使用上一章中讨论的 requests 模块,通过 wit.aiwit.ai/)进行音频转录。

有几种语音识别工具,包括 Google 的语音 API、IBM Watson、Microsoft Bing 的语音识别 API。我们以 wit.ai 作为示例。

语音识别在需要启用 Raspberry Pi Zero 对语音命令做出响应的应用程序中非常有用。例如,在第十章[8ab7d103-3b8b-459e-b64c-fb95200c8a52.xhtml],使用 Raspberry Pi Zero 的家庭自动化中,我们将进行一个家庭自动化项目的开发。我们可以利用语音识别来响应语音命令。

让我们回顾使用 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)。

我们将在我们的智能家居项目中使用语音转录 API 来发布语音命令。

自动化路由任务

在本节中,我们将讨论在 Python 中自动化路由任务。我们选取了两个示例,以展示 Raspberry Pi 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'])

    1. 如果站点上的自行车可用量少于2辆,我们将向我们的移动设备推送移动通知。
  1. 为了接收移动通知,您需要安装IF by IFTTT应用程序(适用于苹果和安卓设备)。

  2. 我们还需要在 IFTTT 上设置一个食谱来触发移动通知。在ifttt.com/注册一个账户。

IFTTT 是一种服务,它允许创建食谱,将设备连接到不同的应用程序并自动化任务。例如,可以将 Raspberry Pi Zero 跟踪的事件记录到您的 Google Drive 上的电子表格中。

IFTTT 上的所有食谱都遵循一个通用模板—如果这个,那么那个,也就是说,如果发生了特定的事件,就会触发特定的动作。对于这个例子,我们需要创建一个应用,当接收到网络请求时触发移动通知。

  1. 您可以使用账户下的下拉菜单开始创建一个 applet,如下面的截图所示:

图片

在 IFTTT 上开始创建一个菜谱

  1. 应该会带您到一个菜谱设置页面(如下所示)。点击此处并设置一个传入的 Web 请求:

图片

点击此处

  1. 选择 Maker Webhooks 通道作为传入的触发器:

图片

选择 Maker Webhooks 通道

  1. 选择接收 Web 请求。来自树莓派的 Web 请求将作为发送移动通知的触发器:

图片

选择接收 Web 请求

  1. 创建一个名为mobile_notify的触发器:

图片

创建一个名为 mobile_notify 的新触发器

  1. 是时候为传入的触发器创建一个动作了。点击它。

图片

点击那个

  1. 选择通知:

图片

选择通知

  1. 现在,让我们格式化我们希望在设备上接收的通知:

图片

设置设备通知

  1. 在移动通知中,我们需要接收共享自行车站点的可用自行车数量。点击+成分按钮并选择Value1

图片

格式化消息以满足您的需求。例如,当树莓派触发通知时,收到以下格式的消息会很好:回家时间!Townsend & 7th 只有 2 辆自行车可用!

图片

  1. 一旦您对消息格式满意,选择创建动作,您的菜谱就应该准备好了!

图片

创建一个菜谱

  1. 为了在移动设备上触发通知,我们需要一个用于发送POST请求的 URL 和一个触发键。这可以在您的 IFTTT 账户中的“服务”|“Maker Webhooks”|“设置”下找到。

触发器可以在这里找到:

图片

在新浏览器窗口中打开前面截图中的 URL。它提供了POST请求的 URL 以及如何进行 Web 请求的解释(如下面的截图所示):

图片

使用前面提到的 URL(出于隐私考虑,键被隐藏)发送一个POST请求

  1. 在进行请求(如 IFTTT 文档中所述)时,如果我们包括请求的 JSON 体中的自行车数量(使用Value1),它可以在移动通知中显示。

  2. 让我们回顾一下 Python 示例,当自行车数量低于某个阈值时,进行 Web 请求。将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. 第一步是安装wordnik API 客户端的python3版本:
       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 ','

如输出所示,以下行在行56后缺少逗号后的空格:

逗号后缺少尾随空格

让我们修复这个问题,并确保我们的代码遵循 PEP8 约定。重新检查文件,错误就会消失。为了使您的代码可读,在将代码提交到公共存储库之前,始终运行 PEP8 检查。

摘要

在本章中,我们讨论了 Python 的高级主题。我们讨论了包括语音识别、构建通勤信息工具以及用于提高词汇量的 Python 客户端等主题。Python 中有一些高级工具,在数据科学、人工智能等领域被广泛使用。我们希望本章讨论的主题是学习这些工具的第一步。

第九章:让我们建造一个机器人吧!

在本章中,我们构建了一个室内机器人(使用 Raspberry Pi Zero 作为控制器),并以逐步指南的形式记录了我们的经验。我们想展示 Python 编程语言和 Raspberry Pi Zero 外围设备的组合的神奇之处。我们还提供了构建户外机器人的建议以及为您的机器人提供额外配件的建议。在本章末尾,我们还提供了构建您自己的机器人的额外学习资源。让我们开始吧!

在本章中,我们将通过远程登录(SSH)访问 Raspberry Pi Zero,并从 Raspberry Pi Zero 远程传输文件。如果您不熟悉命令行界面,我们建议您转到第十一章,技巧与窍门,以设置您的本地桌面环境。

由 Raspberry Pi Zero 供电的机器人

由于我们将使用摄像头来构建我们的机器人,因此在本章中需要 Raspberry Pi Zero v1.3 或更高版本。您的 Raspberry Pi Zero 的板版本可在背面找到。请参考以下图片:

识别您的 Raspberry Pi Zero 版本

机器人的组件

让我们使用标签图(如下所示)来讨论机器人的组件:

机器人的组件

以下是对机器人组件的解释:

  • Raspberry Pi Zero 通过电机驱动电路(堆叠在 Raspberry Pi Zero 上)控制机器人的移动

  • 机器人的电机连接到电机驱动电路

  • 使用 USB 电池组为 Raspberry Pi Zero 供电。使用单独的 AA 电池组驱动电机

  • 机器人还配备了一个摄像头模块,有助于控制机器人的移动

我们提供了一份建议的组件清单,其中我们选择了可用的最便宜的组件来源。欢迎您用您自己的组件替换。例如,您可以使用摄像头而不是使用 Raspberry Pi 摄像头模块:

组件 来源 数量 价格(美元)
底盘 www.adafruit.com/products/2943 1 9.95
底盘顶板 www.adafruit.com/products/2944 1 4.95
一套 M2.5 螺母、垫圈和螺母 a.co/dpdmb1B 1 11.99
伺服电机中的直流电机 www.adafruit.com/products/2941 2 3.50
轮子 www.adafruit.com/products/2744 2 2.50
万向轮 www.adafruit.com/products/2942 1 1.95
Raspberry Pi Zero www.adafruit.com/products/3400 1 5.00
Raspberry Pi Zero 摄像头模块 a.co/07iFhxC 1 24.99
Raspberry Pi Zero 摄像头适配器 www.adafruit.com/products/3157 1 5.95
Raspberry Pi Zero 电机驱动电路 www.adafruit.com/products/2348 1 22.50
USB 电池组 a.co/9vQLx2t 1 5.09
AA 电池组(4 节电池) a.co/hVPxfzD 1 5.18
AA 电池 NA 4 N.A.
Raspberry Pi 摄像头模块支架 www.adafruit.com/products/1434 1 4.95

为了节省时间,我们选择了现成的配件来构建机器人。我们特别选择了 Adafruit,因为它购买和运输都很方便。如果你对构建需要适应户外条件的机器人感兴趣,我们推荐一个类似www.robotshop.com/en/iron-man-3-4wd-all-terrain-chassis-arduino.html的车架。

作为制造商,我们建议你自己制作底盘和控制电路(特别是电机驱动)。你可以使用 Autodesk Fusion(链接可在资源部分找到)等软件来设计底盘。

设置远程登录

为了远程控制机器人,我们需要设置远程登录访问,即启用 SSH 访问。安全外壳SSH)是一种允许远程访问计算机的协议。出于安全原因,Raspbian 操作系统默认禁用了 SSH 访问。在本节中,我们将启用 Raspberry Pi Zero 的 SSH 访问并更改 Raspberry Pi Zero 的默认密码。

如果你不太熟悉 SSH 访问,我们已经在第十一章“技巧与窍门”中提供了一个快速教程。我们希望在本章中保持对构建机器人的关注。

更改密码

在我们启用 SSH 访问之前,我们需要更改 Raspberry Pi Zero 的默认密码。这是为了避免对你的电脑和你的机器人造成任何潜在威胁!我们已经在本章多次提倡更改默认密码。默认密码在互联网上造成了混乱。

推荐阅读 Mirai 机器人网络攻击fortune.com/2016/10/23/internet-attack-perpetrator/

在你的桌面上,转到菜单 | 首选项并启动 Raspberry Pi 配置。在系统选项卡下,有一个选项可以更改系统选项卡下的密码(如下面的截图所示):

图片

更改你的 Raspberry Pi Zero 的默认密码

启用 SSH 访问

在 Raspberry Pi 配置的接口选项卡下,选择启用 SSH(如下面的截图所示):

图片

在接口选项卡下启用 SSH

重新启动您的 Raspberry Pi Zero,您应该能够通过 SSH 访问您的 Raspberry Pi Zero。

请参阅第十一章,技巧与窍门,了解从 Windows、*nix 操作系统(超出本章范围)访问您的 Raspberry Pi Zero 的 SSH。

底盘设置

机器人将配备差速转向机构。因此,它将由两个电机控制。它将由一个起支撑作用的第三个万向轮支持。

在差速转向机构安排中,当机器人的两个车轮以相同方向旋转时,机器人会向前或向后移动。机器人可以通过使一个车轮比另一个车轮旋转得更快来实现左转或右转。例如,为了左转,右电机需要比左电机旋转得更快,反之亦然。

为了更好地理解差速转向机构,我们建议构建底盘并使用 Raspberry Pi Zero 进行测试(我们将在本章的后面部分使用一个简单的程序来测试我们的底盘)。

我们在本章末尾提供了关于差速转向的额外资源。

图片

为机器人准备底盘

  1. 底盘配备了安装电机所需的螺丝,以及所需的螺丝。确保电机的线缆朝向同一侧(请参阅后面的图片)。同样,万向轮可以像图片中所示那样安装在前面:

图片

组装电机和安装万向轮

  1. 下一步是安装车轮。车轮设计成可以直接压入电机轴上。

图片

将车轮组装到伺服电机上

  1. 使用螺丝(随车轮提供)将车轮固定到位

图片

将车轮锁紧在轴上

因此,我们已经完成了机器人底盘的设置。让我们继续到下一部分。

电机驱动器和电机选择

电机驱动器电路(www.adafruit.com/product/2348)可用于连接四个直流电机或两个步进电机。电机驱动器在连续运行时每个电机可提供 1.2 A 的电流。这足以满足机器人的电机功率需求。

准备电机驱动器电路

电机驱动器电路作为一套套件提供,需要一些焊接(如图所示)。

图片

Adafruit DC 和 Stepper Motor HAT for Raspberry Pi-Mini Kit(图片来源:adafruit.com)

  1. 组装过程的第一步是焊接 40 针头组件。将头组件堆叠在您的 Raspberry Pi Zero 顶部,如图所示:

图片

将头组件堆叠在 Raspberry Pi Zero 顶部

  1. 将电机驱动器(如图所示)放置在头组件上。握住电机驱动器板,以确保在焊接过程中板子不会倾斜。

图片

将电机 HAT 堆叠在 Raspberry Pi Zero 上方

  1. 首先焊接电机驱动器的角落引脚,然后继续焊接其他引脚。

图片

注意电机驱动器板焊接的方式,使板与 Raspberry Pi Zero 平行

  1. 现在,通过翻转板焊接 3.5 mm 端子(如图中蓝色部分所示)

图片

焊接 3.5 mm 端子

  1. 电机驱动器板已准备好使用!

图片

电机驱动器已准备好使用

Raspberry Pi Zero 和电机驱动器组装

在本节中,我们将测试机器人的运动。这包括测试电机驱动器和机器人的基本运动。

Raspberry Pi Zero 和电机驱动器组装

在本节中,我们将组装 Raspberry Pi Zero 和电机驱动器到机器人底盘上。

  1. 为了将 Raspberry Pi Zero 安装到底盘上,我们需要 4 个 M2.5 螺丝和螺母(安装孔规格可在www.raspberrypi.org/documentation/hardware/raspberrypi/mechanical/rpi-zero-v1_2_dimensions.pdf找到)。

  2. 我们选择的底盘带有插槽,可以直接将 Raspberry Pi Zero 安装到底盘上。根据您的底盘设计,您可能需要钻孔以安装 Raspberry Pi Zero。

图片

将 Raspberry Pi Zero 安装到底盘上

在安装 Raspberry Pi Zero 时,我们确保能够插入 HDMI 线、USB 线等,以便进行测试。

  1. 我们使用的底盘是由阳极氧化铝制成的;因此,它是非导电的。我们直接安装了 Raspberry Pi Zero,底盘和 Raspberry Pi Zero 之间没有任何绝缘。

确保您没有因意外将它们直接暴露在导电金属表面上而短路任何组件。

  1. 将电机驱动器堆叠在 Raspberry Pi Zero 上方(如前节所示)。

  2. 机器人的两个电机需要连接到 Raspberry Pi Zero:

  3. 电机驱动器带有 M1 至 M4 的电机端子。让我们将左边的直流电机连接到 M1,右边的直流电机连接到 M2。

图片

红色和黑色电线从两个电机连接到电机驱动器端子

  1. 每个电机都有两个端子,即黑色电线和红色电线。将黑色电线连接到 M1 桥的左侧端子,将红色电线连接到 M1 桥的右侧端子(如前图所示)。同样,右边的电机连接到 M2 桥。

现在,我们已经连接了电机,我们需要测试电机功能并验证电机是否以相同的方向旋转。为了做到这一点,我们需要设置机器人的电源。

机器人电源设置

在本节中,我们将讨论为 Raspberry Pi Zero 设置电源。我们将讨论为 Raspberry Pi Zero 和机器人电机供电。让我们讨论我们机器人的主要组件及其功耗:

总功耗估计约为 550 mA(150 + 150*2 + 250)。

为了计算电池容量,我们还需要决定在需要充电前的连续运行时间。我们希望机器人至少运行 2 小时后才能充电。电池容量可以使用以下公式计算:

图片 09_018

在我们的案例中,这将是:

550mA * 2 hours = 1100 mAh

我们还找到了一个来自 Digi-Key 的电池寿命计算器:

www.digikey.com/en/resources/conversion-calculators/conversion-calculator-battery-life

根据 Digi-Key 计算器,我们需要考虑影响电池寿命的因素。考虑到这些因素,电池容量将是:

1100 mAh /0.7 = 1571.42 mAh

我们在购买机器人电池时考虑了这个数字。我们决定购买这个 2200mAh 的 5V USB 电池组(稍后图片中展示,购买链接已在本章前面讨论的材料清单中分享):

图片 09_019

2200 mAh 5V USB 电池组

在将电池组组装到机器人上之前,请确保电池组已完全充电:

  1. 电池组完全充电后,使用双面胶将其固定到机器人上,并插入一根微型 USB 线,如图所示:

图片 09_020

2200 mAh 5V USB 电池组

  1. 我们需要验证当使用电池组时 Raspberry Pi Zero 是否可以启动。

  2. 插入 HDMI 线(连接到监视器),使用非常短的微型 USB 线,尝试启动 Raspberry Pi Zero 并确保一切正常启动。

设置电机电源

现在我们已经为机器人设置了电源并验证了 Raspberry Pi Zero 使用 USB 电池组可以启动,我们将讨论为机器人电机供电的电源选项。我们之所以讨论这个问题,是因为电机的类型及其电源决定了我们机器人的性能。让我们开始吧!

让我们回顾一下上一节中设置的电机驱动器。这个电机驱动器的独特之处在于它配备了自身的电压调节器和极性保护。因此,它允许连接外部电源来为电机供电(如图所示)

图片

电机驱动器电源端子

这个电机驱动器可以驱动任何需要 5-12V 电压和 1.2A 电流的电机。有两种方式可以为你的机器人电机供电:

  • 使用 Raspberry Pi Zero 的 5V GPIO 电源

  • 使用外部电源

使用 Raspberry Pi Zero 的 5V 电源

电机驱动器设计成可以作为原型平台。它有一组 5V 和 3.3V 电源引脚,这些引脚连接到 Raspberry Pi Zero 的 5V 和 3.3V GPIO 引脚。这些 GPIO 引脚的额定电流为 1.5A(来源:pinout.xyz/pinout/pin2_5v_power)。它们直接连接到 Raspberry Pi Zero 的 5V USB 输入。(在这个机器人中使用的 USB 电池组的输出额定为 5V,1A)。

  1. 连接 Raspberry Pi 的 5V GPIO 电源的第一步是焊接一根红色和黑色电线(长度适当)分别从 5V 和 GND 引脚(如图所示):

图片

从 5V 和 GND 引脚焊接红色和黑色电线

  1. 现在,将红色和黑色电线连接到标记为 5-12V 电机电源的端子(红色电线连接到+,黑色电线连接到-)。

图片

将 5V 和 GND 连接到电机电源端子

  1. 现在,启动你的 Raspberry Pi Zero,并测量电机电源端子之间的电压。它应该接收 5V,电机驱动器的电源 LED 应该发绿光(如图所示)。如果不是这样,请检查电机驱动器的焊接连接。

图片

当 Raspberry Pi Zero 通电时,绿色 LED 灯亮起

  1. 这种方法仅在低功耗电机使用时(如本章中使用的电机)才有效。如果你有一个电压额定值更高的电机(电压额定值大于 5V),你需要连接外部电源。我们将在下一节中回顾如何连接外部电源。

如果你发现你的 Raspberry Pi Zero 在驾驶机器人时经常自动重启,那么可能是 USB 电池组无法驱动机器人的电机。是时候连接外部电源了!

使用外部电源

在本节中,我们将讨论如何连接外部电源来驱动电机。我们将讨论如何连接一个 6V 电源来为电机供电。

  1. 我们将使用由 4 节 AA 电池组成的电池组来驱动电机(电池组可在a.co/hVPxfzD购买)。

  2. 我们需要安装电池组,以便其引线可以连接到电机驱动器的电源端子。

  3. 机器人底盘套件附带了一个额外的铝制板,可用于安装电池组(如下图中所示):

图片

用于固定电池组的额外铝制板

  1. 我们使用了四个 M2.5 支架(如下面图片所示)来固定铝制板:

图片

组装 M2.5 支架以固定铝制板

  1. 现在,我们使用了 M2.5 螺丝固定铝制板(如下图中所示):

图片

固定铝制板

  1. 使用双面胶带,将电池组(电池组包含四个 AA 电池)安装在铝制板上。然后,将电池组的红色和黑色线分别连接到电机驱动器的+和-端子(如下图中所示)。

图片

安装在铝制板上的电池

  1. 将电池组开关滑到 ON 位置,电机驱动器应该像前一部分所述那样打开。

因此,电源设置完成。在下一节中,我们将讨论进行机器人测试驾驶。

如果你正在寻找适合你的机器人的更高容量的电池,我们建议考虑使用锂聚合物电池。这也意味着你需要更好的电机评级和能够承受电池重量的底盘。

测试电机

在本节中,我们将验证 Raspberry Pi Zero 是否检测到电机驱动器并测试电机功能。在测试中,我们将验证电机是否以相同方向旋转。

电机驱动器检测

在本节中,我们将验证 Raspberry Pi Zero 是否检测到电机驱动器。Raspberry Pi Zero 通过 I²C 接口与电机驱动器通信(如果你不熟悉 I²C 接口,请参阅第四章,通信接口)。因此,我们需要启用 Raspberry Pi Zero 的 I²C 接口。有两种方法可以启用 I²C 接口:

方法 1:从桌面启动

就像通过从你的 Raspberry Pi Zero 的桌面启动 Raspberry Pi 配置来启用ssh一样,你可以从配置的接口选项卡中启用 I²C 接口(如下面快照所示):

图片

启用 I²C 接口

方法 2:从命令行启动

我们强烈建议使用这种方法作为在树莓派上熟悉命令行界面和通过ssh远程登录的实践。

  1. 通过ssh登录到你的 Raspberry Pi Zero(有关ssh访问教程,请参阅第十一章,技巧与窍门)。

  2. 登录后,按照以下方式启动raspi-config

       sudo raspi-config.

  1. 应该启动配置选项菜单(如下截图所示):

图片

raspi-config 菜单

  1. 选择选项 7:高级选项(使用键盘)并选择 A7:I2C

图片

选择 I²C 接口

  1. 选择是以启用 I²C 接口。

图片

启用 I²C 接口

  1. 现在 I²C 接口已启用,让我们开始检测电机驱动器。

检测电机驱动器

电机驱动器连接到 I²C 端口-1(I²C 端口-0 用于不同的目的。有关更多信息,请参阅第十一章,技巧与窍门)。我们将使用i2cdetect命令扫描通过 I²C 接口连接的设备。在您的命令行界面中,运行以下命令:

    sudo i2cdetect -y 1 

它提供了一个类似以下输出的结果:

    0

1

2

3

4

5

6

7

8

9

a

b

c

d

e

f 
 00:

-- -- -- -- -- -- -- -- -- -- -- -- -- 
 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
 60: 60 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
 70: 70 -- -- -- -- -- -- -- 

I²C 芯片带有 7 位地址,用于识别芯片并建立通信。在这种情况下,I²C 接口地址是0x60(请参阅Adafruit DC 和步进电机帽用于 Raspberry Pi 的文档)。如前所述的输出所示,Raspberry Pi Zero 检测到了电机驱动器。现在是时候测试我们是否可以控制电机了。

电机测试

在本节中,我们将测试电机;也就是说,确定我们是否可以使用电机驱动器驱动电机。在这个测试中,我们确定了 Raspberry Pi 的电源是否足以驱动电机(或者是否需要外部电池组)。

为了开始,我们需要安装电机驱动器库(由 Adafruit 在 MIT 许可下分发)及其依赖包。

依赖项

电机驱动器库的依赖项可以从 Raspberry Pi Zero 的命令行终端安装如下(如果您在第四章,通信接口)中安装了这些工具,则可以跳过此步骤):

    sudo apt-get update
 sudo apt-get install python3-dev python3-smbus

下一步是克隆电机驱动器库:

    git clone https://github.com/sai-y/Adafruit-Motor-HAT-Python-
    Library.git

这个库是Adafruit Motor HAT 库的一个分支。我们修复了一些问题,使库安装与 Python 3.x 兼容。

可以按照以下方式安装库:

    cd Adafruit-Motor-HAT-Python-Library
 sudo python3 setup.py install

现在库已经安装好了,让我们编写一个程序来连续旋转电机:

  1. 如往常一样,第一步是导入MotorHAT模块:
       from Adafruit_MotorHAT import Adafruit_MotorHAT, 
       Adafruit_DCMotor

  1. 下一步是创建MotorHAT类的实例并与电机驱动器建立接口(如前一小节所述,电机驱动器的 7 位地址是0x60)。

  2. 机器人的电机连接到通道 1 和 2。因此,我们需要初始化两个Adafruit_DCMotor类的实例,分别代表机器人的左右电机:

       left_motor = motor_driver.getMotor(1) 
       right_motor = motor_driver.getMotor(2)

  1. 下一步是设置电机速度和方向。电机速度可以使用介于0255之间的整数设置(这对应于电机额定 rpm 的 0%和 100%)。让我们将电机速度设置为 100%:
       left_motor.setSpeed(255) 
       right_motor.setSpeed(255)

  1. 让我们以正向方向旋转电机:
       left_motor.run(Adafruit_MotorHAT.FORWARD) 
       right_motor.run(Adafruit_MotorHAT.FORWARD)

  1. 让我们以正向方向旋转两个电机 5 秒钟,然后降低速度:
       left_motor.setSpeed(200) 
       right_motor.setSpeed(200)

  1. 现在,让我们以相反的方向旋转电机:
       left_motor.run(Adafruit_MotorHAT.BACKWARD) 
       right_motor.run(Adafruit_MotorHAT.BACKWARD)

  1. 当我们完成将电机反向旋转 5 秒后,让我们关闭电机:
       left_motor.run(Adafruit_MotorHAT.RELEASE) 
       right_motor.run(Adafruit_MotorHAT.RELEASE)

整合起来:

from Adafruit_MotorHAT import Adafruit_MotorHAT, Adafruit_DCMotor 
from time import sleep 

if __name__ == "__main__": 
  motor_driver = Adafruit_MotorHAT(addr=0x60) 

  left_motor = motor_driver.getMotor(1) 
  right_motor = motor_driver.getMotor(2) 

  left_motor.setSpeed(255) 
  right_motor.setSpeed(255) 

  left_motor.run(Adafruit_MotorHAT.FORWARD) 
  right_motor.run(Adafruit_MotorHAT.FORWARD) 

  sleep(5) 

  left_motor.setSpeed(200) 
  right_motor.setSpeed(200) 

  left_motor.run(Adafruit_MotorHAT.BACKWARD) 
  right_motor.run(Adafruit_MotorHAT.BACKWARD) 

  sleep(5) 

  left_motor.run(Adafruit_MotorHAT.RELEASE) 
  right_motor.run(Adafruit_MotorHAT.RELEASE)

前面的代码示例作为motor_test.py与本章一起提供下载。在测试电机之前,请先充电 USB 电池组。我们选择的测试时间足够长,以便验证电机方向、性能等。

如果您的 Raspberry Pi Zero 在运行时似乎在重置,或者电机没有以额定速度运行,这表明电机没有用足够的电流驱动。切换到满足要求的电源(这可能涉及从 GPIO 的电源切换到电池组或切换到容量更高的电池组)。

现在电机已经测试过了,让我们为机器人设置一个相机。

相机设置

您需要 Raspberry Pi Zero 1.3 或更高版本来设置相机。我们在本章开头讨论了识别 Raspberry Pi Zero 的板版本。如果您熟悉从第八章,“使用 Python 可以开发的一些酷炫事物”中设置相机,也可以跳过本节。

在本节中,我们将为机器人设置相机。Raspberry Pi Zero(从 v1.3 开始)附带相机适配器。这使得可以向机器人添加相机模块(由 Raspberry Pi 基金会设计和制造)。相机模块被设计成适合不同型号的 Raspberry Pi。

Raspberry Pi Zero 的相机接口需要一个与用于其他型号的适配器不同的适配器。购买相机和适配器的来源与本章的材料清单一起分享。

让我们开始吧:

  1. 确保您的 Raspberry Pi Zero 已关闭电源,并识别相机适配器的较短一侧。在此图中,较短的一侧在右侧。

图片

Pi Zero 相机适配器-图片来源:adafruit.com

  1. 小心滑出 Raspberry Pi Zero 的相机接口(如图片所示)。请注意避免损坏您的相机接口标签。

图片

小心滑动相机界面的标签

  1. 轻轻滑入相机模块。锁定相机适配器电缆,并轻轻拉扯它以确保适配器电缆不会从其位置滑出。相机适配器应如图片所示正确放置。

图片

相机适配器放置

  1. 重复练习,将相机适配器的另一端与相机模块接口连接。

图片

在另一侧插入适配器

  1. 在尝试将相机安装到机器人上时,相机适配器电缆可能会变得难以控制。我们建议安装一个支架(材料清单中分享的来源)。

图片

Raspberry Pi 相机模块安装

  1. 使用双面胶带,将相机安装到机器人的前面。

图片

机器人前部的摄像头

  1. 通过 ssh 登录到你的 Raspberry Pi Zero 桌面以启用并测试摄像头接口。

  2. 启用摄像头接口与本章前面讨论的启用 I²C 接口类似。使用 raspi-config 命令启动 Raspberry Pi 配置:

       sudo raspi-config

选择选项 P1:启用摄像头(在主配置菜单的接口选项下找到)并启用摄像头:

图片

Raspberry Pi 配置屏幕的截图

  1. 重启你的 Raspberry Pi Zero!

摄像头功能验证

  1. 重启完成后,从命令提示符运行以下命令:
       raspistill -o test_picture 

  1. 由于你的机器人已经完全组装好,你的 Raspberry Pi Zero 的 HDMI 端口可能无法访问。你应该使用 scp 命令检索文件。

在 Windows 机器上,你可以使用 WinSCP 等工具从你的 Raspberry Pi Zero 复制文件。在 Mac/Linux 桌面上,你可以使用 scp 命令。参考第十一章,技巧和窍门,获取有关远程登录和从 Raspberry Pi Zero 复制文件的详细教程。

       scp pi@192.168.86.111:/home/pi/test_output.

  1. 检查使用 Raspberry Pi 摄像头模块拍摄的图片以验证其功能

图片

使用 Raspberry Pi 摄像头模块拍摄的咖啡杯图片

现在我们已经验证了机器人组件的功能,我们将在下一节中将所有内容整合在一起。

Web 接口

我们构建这个机器人的目标之一是将本书讨论的主题应用于应用开发中。为此,我们将利用面向对象编程和 Web 框架来构建一个控制机器人的 Web 接口。

在第七章,请求和 Web 框架中,我们讨论了 flask Web 框架。我们将使用 flask 将摄像头模块的实时视图流式传输到浏览器。我们还将向 Web 接口添加按钮,以便控制机器人。让我们开始吧!

参考第七章,请求和 Web 框架,获取安装说明和 flask 框架的基本教程。

让我们从实现一个简单的 Web 接口开始,在这个接口中我们添加四个按钮来控制机器人的前进、后退、左转和右转方向。假设机器人以最大速度在所有方向上移动。

我们将利用面向对象编程来实现电机控制。我们将演示如何使用面向对象编程来简化事物(这种简化的概念称为抽象)。让我们实现一个 Robot 类,该类实现电机控制。这个 Robot 类将初始化电机驱动器并处理机器人的所有控制功能。

  1. 打开名为 robot.py 的文件以实现 Robot 类。

  2. 为了控制机器人的移动,机器人在初始化时需要使用(以驱动电机)的电机驱动器通道作为输入。

  3. 因此,Robot 类的 __init__() 函数可能如下所示:

       import time 
       from Adafruit_MotorHAT import Adafruit_MotorHAT 

       class Robot(object): 
         def __init__(self, left_channel, right_channel): 
           self.motor = Adafruit_MotorHAT(0x60) 
           self.left_motor = self.motor.getMotor(left_channel) 
           self.right_motor = self.motor.getMotor(right_channel)

  1. 在前面的代码片段中,__init__() 函数需要作为参数传递给连接左右电机到电机驱动器板的通道。

  2. 当创建 Robot 类的实例时,电机驱动器(Adafruit_MotorHAT)被初始化,电机通道也被初始化。

  3. 让我们编写方法来使机器人向前和向后移动:

       def forward(self, duration): 
         self.set_speed() 
         self.left_motor.run(Adafruit_MotorHAT.FORWARD) 
         self.right_motor.run(Adafruit_MotorHAT.FORWARD) 
         time.sleep(duration) 
         self.stop() 

       def reverse(self, duration): 
         self.set_speed() 
         self.left_motor.run(Adafruit_MotorHAT.BACKWARD) 
         self.right_motor.run(Adafruit_MotorHAT.BACKWARD) 
         time.sleep(duration) 
         self.stop()

  1. 让我们也编写方法来使机器人向左和向右移动。为了使机器人向左转,我们需要关闭左电机并保持右电机开启,反之亦然。这会产生一个转向力矩,使机器人向该方向转动:
       def left(self, duration): 
         self.set_speed() 
         self.right_motor.run(Adafruit_MotorHAT.FORWARD) 
         time.sleep(duration) 
         self.stop() 

       def right(self, duration): 
         self.set_speed() 
         self.left_motor.run(Adafruit_MotorHAT.FORWARD) 
         time.sleep(duration) 
         self.stop()

  1. 因此,我们实现了一个 Robot 类,该类可以驱动机器人在四个方向上移动。让我们实现一个简单的测试,以便在我们将其用于主程序之前测试 Robot 类:
       if __name__ == "__main__": 
         # create an instance  of the robot class with channels 1 and 2 
         robot = Robot(1,2) 
         print("Moving forward...") 
         robot.forward(5) 
         print("Moving backward...") 
         robot.reverse(5) 
         robot.stop()

前面的代码示例可以作为本章的附件 robot.py 下载。尝试使用电机驱动器运行程序。它应该在 5 秒内使电机向前和向后移动。现在我们已经实现了一个独立的机器人控制模块,让我们继续到 Web 界面。

Web 界面的摄像头设置

即使完全按照说明操作,你仍可能会遇到一些问题。我们在本章末尾包含了我们用来解决问题的参考资料。

在本节中,我们将设置摄像头以向浏览器进行流式传输。第一步是安装 motion 软件包:

    sudo apt-get install motion

一旦安装了软件包,需要应用以下配置更改:

  1. 编辑 /etc/motion/motion.conf 中的以下参数:
       daemon on
 threshold 99999
 framerate 90
 stream_maxrate 100
 stream_localhost off

  1. /etc/default/motion 中包含以下参数:
 start_motion_daemon=yes 

  1. 按照以下方式编辑 /etc/init.d/motion
       start)

       if check_daemon_enabled ; then

       if ! [ -d /var/run/motion ]; then

       mkdir /var/run/motion

       fi

       chown motion:motion /var/run/motion

       sudo modprobe bcm2835-v4l2

       chmod 777 /var/run/motion

       sleep 30

       log_daemon_msg "Starting $DESC" "$NAME"

  1. 重启你的树莓派 Zero。

  2. 下一步假设你已经安装了 Flask 框架并尝试了 第七章 中的基本示例,请求和 Web 框架

  3. 在你的 flask 框架所在的文件夹中创建一个名为 templates 的文件夹:

    Robot 类文件位于)并在该文件夹中创建一个名为 index.html 的文件,内容如下:

      <!DOCTYPE html>
       <html>
         <head>
           <title>Raspberry Pi Zero Robot</title>
         </head>

         <body>
          <iframe id="stream" 
          src="img/?action=stream" width="320" height="240">
          </iframe>
  </body>
       </html>

  1. 在前面的代码片段中,包括你的树莓派 Zero 的 IP 地址并将其保存为 index.html

  2. 创建一个名为 web_interface.py 的文件,并在模板文件夹中提供 index.html

       from flask import Flask, render_template
       app = Flask(__name__)

       @app.route("/")
       def hello():
           return render_template('index.html')

       if __name__ == "__main__":
           app.run('0.0.0.0')

  1. 使用以下命令运行 Flask 服务器:
       python3 web_interface.py

  1. 在你的笔记本电脑上打开浏览器,并访问你的树莓派 Zero 的 IP 地址(端口 5000),以查看树莓派摄像头模块的实时流。

图片

实时网络摄像头流的快照(树莓派摄像头模块)

让我们继续下一步,向 Web 界面添加按钮。

机器人控制按钮

在本节中,我们将向网络界面添加实施按钮以驱动机器人。

  1. 第一步是向index.html添加四个按钮。我们将使用 HTML 表格添加四个按钮(代码片段已缩短以节省篇幅,有关 HTML 表格的更多信息,请参阅www.w3schools.com/html/html_tables.asp):
       <table style="width:100%; max-width: 500px; height:300px;"> 
         <tr> 
           <td> 
             <form action="/forward" method="POST"> 
               <input type="submit" value="forward" style="float:
               left; width:80% ;"> 
               </br> 
             </form> 
           </td> 
       ... 
       </table>

  1. web_interface.py中,我们需要实现一个方法来接受来自按钮的POST请求。例如,可以按照以下方式实现接受来自/forward的请求的方法:
       @app.route('/forward', methods = ['POST']) 
       def forward(): 
           my_robot.forward(0.25) 
           return redirect('/')

  1. 将所有这些放在一起,web_interface.py看起来如下所示:
       from flask import Flask, render_template, request, redirect 
       from robot import Robot  

       app = Flask(__name__) 
       my_robot = Robot(1,2) 

       @app.route("/") 
       def hello(): 
           return render_template('index.html') 

       @app.route('/forward', methods = ['POST']) 
       def forward(): 
           my_robot.forward(0.25) 
           return redirect('/') 

       @app.route('/reverse', methods = ['POST']) 
       def reverse(): 
           my_robot.reverse(0.25) 
           return redirect('/') 

       @app.route('/left', methods = ['POST']) 
       def left(): 
           my_robot.left(0.25) 
           return redirect('/') 

       @app.route('/right', methods = ['POST']) 
       def right(): 
           my_robot.right(0.25) 
           return redirect('/') 

       if __name__ == "__main__": 
           app.run('0.0.0.0')

上述代码示例作为web_interface.py(以及index.html)的一部分提供下载。将以下行添加到/etc/rc.local(在exit 0之前):

    python3 /<path_to_webserver_file>/web_interface.py

重新启动 Raspberry Pi Zero,你应该能看到机器人的摄像头实时流。你也应该能够从浏览器中控制机器人!

图片

通过浏览器控制你的机器人!

故障排除技巧

在构建机器人的过程中,我们遇到了以下一些问题:

  • 在组装摄像头模块时,我们损坏了 Raspberry Pi Zero 的摄像头接口标签。我们不得不更换 Raspberry Pi Zero。

  • 我们在电机驱动电路中遇到了一些幽灵问题。在某些情况下,我们无法检测到电机驱动器。我们不得不更换电机驱动器的电源。当我们找到这个问题的根本原因时,我们将保持本书的网站更新。

  • 在为浏览器设置网络流时,我们遇到了很多问题。我们不得不调整很多设置才能使其工作。我们找到了一些文章来修复这个问题。我们已经在本书的参考文献部分分享了它们。

项目增强

  • 考虑对网络界面进行增强,以便你可以改变机器人的速度。

  • 如果你计划构建一个在户外条件下运行的机器人,你可能会添加一个 GPS 传感器。大多数 GPS 传感器通过 UART 接口传输数据。我们建议阅读第四章,通信接口以获取示例。

  • 可以使用这个传感器测量障碍物的距离:www.adafruit.com/products/3317。这可能在遥测应用中很有帮助。

  • *在本书中,我们使用摄像头来驱动机器人。使用这个图像理解工具可以拍照并理解场景中的物体:cloud.google.com/vision/

摘要

在本章中,我们构建了一个由 Raspberry Pi 使用电机驱动器驱动的机器人,该机器人还配备了一个摄像头模块以帮助转向。它由两个电池组组成,分别供电给 Raspberry Pi Zero 和电机。我们还将上传一个机器人操作的录像到本书的网站上。

学习资源

第十章:使用 Raspberry Pi Zero 进行家庭自动化

如章节标题所示,我们将在本章讨论涉及 Raspberry Pi Zero 的家庭改进项目。我们选择的项目使得每个示例都可以作为一个周末项目执行。

项目包括以下主题:

  • 语音激活个人助理

  • 基于 Web 框架的家电控制

  • 身体活动激励工具

  • 智能草坪洒水器

语音激活个人助理

在我们的第一个项目中,我们将使用 Raspberry Pi Zero 来模拟 Google Home (madeby.google.com/home/) 和 Amazon Echo (a.co/cQ6zJk6)这样的个人助理。我们将构建一个应用程序,可以添加提醒和事件到日历,并控制家电。

我们将使用houndify (houndify.com)——一个旨在提供与智能设备交互的工具。我们将在 Raspberry Pi Zero 上安装所需的软件工具。我们将一个按钮连接到 Raspberry Pi Zero 的 GPIO。我们将编写一些代码,使用Houndify创建提醒和开关家电。

以下配件(除了你的 Raspberry Pi Zero 之外)推荐用于此项目:

项目 来源 价格(美元)
USB 声卡 a.co/824dfM8 8.79
可调增益麦克风放大板 www.adafruit.com/products/1713 7.95
3.5mm 辅助线缆 www.adafruit.com/products/2698 2.50
瞬时按钮套件 www.adafruit.com/products/1009 5.95
面板、电阻、跳线电容 N. A. N. A.
演讲者(建议) a.co/3h9uaTI 14.99

安装所需的软件包

第一步是安装项目所需的软件包。这包括以下软件包:python3-pyaudio python3-numpy。它们可以按照以下方式安装:

sudo apt-get update sudo apt-get install alsa-utils mplayer python3-numpy

它是如何工作的?

以下是需要执行的步骤:

  1. 一个按钮连接到 Raspberry Pi Zero 的 GPIO。当 GPIO 按钮被按下时,录音设备开启(扬声器发出蜂鸣声的开始)。

  2. 录音设备接受用户请求,并使用Houndify库进行处理。

  3. 助手使用Houndify处理音频文件,并响应用户请求。

在这个项目中,我们使用一个按钮来开始监听用户请求,而市面上可用的产品,如亚马逊的 Echo 或谷歌的 Home,具有特殊的硬件(以及软件)来启用此功能。我们使用按钮来简化问题。

设置音频工具

在本节中,我们将连接 USB 声卡、扬声器和项目所需的麦克风。

连接扬声器

执行以下步骤以连接到扬声器:

  1. 将 USB 声卡连接到您的 Raspberry Pi Zero,并使用 lsusb 命令(在您的 Raspberry Pi Zero 命令行终端)检查 USB 声卡是否枚举:

USB 声卡枚举

  1. 便宜的 USB 声卡通常有一个输入端子(用于连接麦克风)和一个输出端子(用于连接扬声器)。这两个端子都是标准的 3.5 毫米插孔。输入端子是粉红色的,通常带有麦克风符号。输出端子是绿色的,带有扬声器符号。

  2. 将扬声器连接到 USB 声卡的输出端子(绿色)。

  3. 在您的 Raspberry Pi Zero 命令行终端中,使用以下命令列出连接到您的 Raspberry Pi Zero 的所有音频源:

 aplay -l **** List of PLAYBACK Hardware Devices **** card 0: ALSA [bcm2835 ALSA], device 0: bcm2835 
         ALSA [bcm2835 ALSA] Subdevices: 8/8 Subdevice #0: subdevice #0 Subdevice #1: subdevice #1 Subdevice #2: subdevice #2 Subdevice #3: subdevice #3 Subdevice #4: subdevice #4 Subdevice #5: subdevice #5 Subdevice #6: subdevice #6 Subdevice #7: subdevice #7 card 0: ALSA [bcm2835 ALSA], device 1: bcm2835 
         ALSA [bcm2835 IEC958/HDMI] Subdevices: 1/1 Subdevice #0: subdevice #0 card 1: Set [C-Media USB Headphone Set], 
         device 0: USB Audio [USB Audio] Subdevices: 1/1 Subdevice #0: subdevice #0

  1. aplay 命令的输出所示,声卡列为准 card 1。我们需要这个信息来设置 USB 声卡为默认音频源

  2. 按照以下方式从命令行打开您的声音配置文件:

 nano ~/.asoundrc

  1. 确保配置文件中的源设置为 card 1(声卡):
       pcm.!default {
               type hw
               card 1
       }

       ctl.!default {
               type hw
               card 1
       }

保存配置文件(通过按 Ctrl X 并按 Y 确认文件名。按 Enter 保存文件。有关详细教程,请参阅 第十一章,技巧与窍门 部分)并重新启动您的 Raspberry Pi Zero。

  1. 重新启动后,通过下载波形文件(Freesound.org 有大量波形文件)来测试扬声器是否工作。从命令行终端,按照以下方式播放您的文件:
 aplay test.wav

如果一切配置正确,您应该能够使用您的 USB 声卡和扬声器播放音频。如果您无法播放音频,请检查连接,并确保您的 USB 声卡已正确枚举,并且在配置文件中选择了正确的音频源。在下一节中,我们将设置麦克风。

连接麦克风

在本节中,我们将设置一个全向麦克风以监听命令/输入。

我们测试了市售的驻极体麦克风,音频质量不足以在录制的音频样本上执行语音识别。作为替代方案,我们推荐使用边界麦克风以实现广泛的拾音,例如,a.co/8YKSy4c

MAX9814 配备全向麦克风 来源:Adafruit.com

  1. 放大器的增益可以设置为三个级别:当增益引脚未连接时为 60 dB,当增益引脚连接到地时为 50 dB,当增益引脚连接到 V[dd] 时为 40 dB

  2. VddGND 引脚连接到 Raspberry Pi 的 GPIO 引脚的 5VGND 引脚(Raspberry Pi 的 GPIO 的第 4 和 6 引脚)。

  3. 将 3.5 mm 电缆切成两半。它由连接到 3.5 mm 插座的 尖端套筒的三根线组成(如图所示)。使用万用表识别 3.5 mm 插座的 套筒尖端线。

切断辅助电缆并识别电缆的三根线

  1. 将一个 100 mF 的电解电容器连接到放大器的输出端,其中正极连接到输出端,另一端连接到 3.5 mm 插座的尖端。放大器的接地引脚连接到 3.5 mm 插座的套筒。

麦克风连接到 3.5 mm 插座

麦克风已准备好使用。使用 Raspberry Pi Zero 的 GPIO 引脚为麦克风供电,并将 3.5 mm 插座插入 USB 声卡的输入端子(带有麦克风符号标记)。

麦克风连接到 3.5 mm 插座

我们已准备好测试麦克风并设置最佳捕获音量。从 Raspberry Pi Zero 的命令行终端运行以下命令:

 arecord -f dat -D plughw:2 
         --duration=10~/home/pi/rectest.wav

这将录制文件 10 秒。使用 aplay 命令播放:

 aplay rectest.wav

您应该能够听到录制的对话。检查您的连接,如果您什么也听不到(GND5V、放大器输出引脚等。我们还在本章末尾包括了麦克风故障排除的额外资源)。

如果录音内容太响或太弱,请使用 alsamixer 调整捕获音量。从命令行终端启动 alsamixer

alsamixer 控制面板

F5 查看所有选项。使用箭头键调整值,按 M 禁用自动增益控制。让我们继续到下一节,在那里我们构建我们的应用程序。

Houndify

Houndify (www.houndify.com) 是一个允许向设备添加语音交互的工具。他们的免费账户允许执行 44 种不同的操作。在他们的网站上注册一个账户并激活它(通过您的电子邮件)。

  1. 一旦您的账户被激活,转到您的账户仪表板以创建一个新的客户端:

在创建新账户时,会自动创建一个新的客户端。这个客户端可能无法正常工作。请将其删除,并从仪表板创建一个新的客户端。

创建新客户端

  1. 给您的应用程序命名并选择平台为智能家居。

命名应用程序并选择平台

  1. 下一步是选择域,即助手必须支持的应用程序的性质。选择天气、股市、词典等。

启用域

  1. 点击保存并继续。一旦您创建了新的客户端,点击它(从仪表板)以检索以下凭证:客户端 ID 和客户端密钥。

从仪表板复制客户端 ID 和客户端密钥

  1. 我们还需要下载 Python 3.x 版本的 SDK(最新版本可在docs.houndify.com/sdks#python找到):
 wget 
       https://static.houndify.com/sdks/python
       /0.5.0/houndify_python3_sdk_0.5.0.tar.gz

  1. 按照以下方式提取包:
 tar -xvzf houndify_python3_sdk_0.5.0.tar.gzrm 
       houndify_python3_sdk_0.5.0.tar.gz

  1. SDK 附带了许多示例,可以帮助你开始。让我们考虑一个场景,你希望通过与语音助手交互来查找你当前位置的天气:

  2. 从像 Google Maps 这样的工具中获取你的当前 GPS 坐标。例如,加利福尼亚州旧金山的特定交叉口的 GPS 坐标是 37.778724, -122.414778。让我们尝试查找这个特定位置的天气。

  3. 打开sample_wave.py文件并修改文件的第39行:

              client.setLocation(37.778724, -37.778724)

  1. 保存文件,然后在命令行终端,切换到Houndify SDK 文件夹
              cd houndify_python3_sdk_0.5.0/ 
              ./sample_wave.py <client_id> <client_key> 
              test_audio/whatistheweatherthere_nb.wav

  1. 处理请求后,它应该打印出详细的响应:
              src="img/>              templates.min.js"></script>'}}, 'TemplateName': 
              'VerticalTemplateList', 'AutoListen': False, 
              'WeatherCommandKind': 'ShowWeatherCurrentConditions', 
              'SpokenResponseLong': 'The weather is 45 degrees and
              mostly clear in San Francisco.',

我们通过测试示例验证了 Houndify SDK 的功能和设置。我们上传了一个音频文件到 Houndify 服务器,请求当前的天气信息(播放音频文件并找出)。sample_wave.py脚本将client_idclient_key和音频文件作为输入。它打印出 Houndify 服务器的输出。

你需要启用特定的域来检索特定的信息。例如,我们启用了天气域来检索天气信息。也可以向程序中添加自定义命令。

在下一节中,我们将修改sample_wave.py来构建我们的应用程序。

构建语音命令

让我们开始构建我们可以用来查找天气和开关灯的语音助手。因为我们设置 Houndify 账户时启用了天气域,所以我们需要添加自定义命令来开关灯:

  1. 在你的 Houndify 仪表板上,前往客户的首页。仪表板 | 点击你的客户。

  2. 在左侧导航栏上找到自定义命令。让我们添加一个自定义命令来打开和关闭灯光。

  3. 删除与自定义命令一起提供的模板中的ClientMatch #1

定位自定义命令和删除 Client Match #1

  1. 选择“添加 ClientMatch”以添加一个自定义命令来打开灯光。填写以下信息:
  • Expression: ["Turn"].("Lights"). ["ON"]

  • Result: {"action": "turn_light_on"}

  • SpokenResponse: Turning Lights On

  • SpokenResponseLong: Turning your Lights On

  • WrittenResponse: Turning Lights On

  • WrittenResponseLong: Turning your Lights On

  1. 重复前面的步骤以添加一个命令来关闭灯光

使用sample_wave.py测试和验证这些命令是否工作。为测试制作你自己的录音。我们还在本章的下载中提供了音频文件(可在audio_files文件夹中找到)。

让我们复制 sample_wave.py 文件来构建我们的助手。我们建议阅读该文件并熟悉其功能。Houndify SDK 的详细文档可在 docs.houndify.com/sdks/docs/python 找到:

  1. 在文件 stream_wav.py 中,使用 StreamingHoundClient 类发送音频查询,例如请求天气信息或开关灯的命令。

  2. MyListener 类从 houndify SDK 中的 HoundListener 类继承。

  3. MyListener 类实现了三个场景的回调函数:

  • 部分转录(onPartialTranscript 方法)

  • 完整转录(onFinalResponse 方法)

  • 错误状态(onError 方法)

  1. 我们需要利用动作意图通过语音命令来开关灯。

  2. 当我们在 Houndify 网站上实现自定义命令时,为每个命令添加了一个动作意图。例如,打开灯的动作意图是:

       { 
           "action": "turn_light_on"
       }

  1. 为了根据接收到的动作意图开关灯,我们需要从 gpiozero 模块导入 OutputDevice 类:
       from gpiozero import OutputDevice

  1. 控制灯的 GPIO 引脚在 MyListener 类的 __init__ 方法中初始化:
       class MyListener(houndify.HoundListener): 
         def __init__(self): 
           self.light = OutputDevice(3)

  1. 在完成转录后,如果收到动作意图,灯将打开或关闭。实现方式如下:
       def onFinalResponse(self, response): 
         if "AllResults" in response: 
           result = response["AllResults"][0] 
           if result['CommandKind'] == "ClientMatchCommand": 
             if result["Result"]["action"] == "turn_light_on": 
               self.light.on() 
             elif result["Result"]["action"] == "turn_light_off": 
               self.light.off()

response 是一个包含解析后的 json 响应的字典。请参阅 SDK 文档,并尝试打印响应以了解其结构。

  1. 我们还需要在开关灯时宣布语音助手的动作。我们探索了不同的文本到语音工具,与谷歌 Home 或亚马逊 Echo 等现成产品相比,它们听起来很机械。我们遇到了这个利用 elinux.org/RPi_Text_to_Speech_(Speech_Synthesis)谷歌语音识别引擎 的脚本。

因为脚本使用了谷歌的文本到语音引擎,所以它连接到互联网以获取转录的音频数据。

  1. 从 Raspberry Pi 的命令行终端打开一个新的 shell 脚本:
 nano speech.sh

  1. 粘贴以下内容:
             #!/bin/bash 
 say() { local IFS=+;/usr/bin/mplayer
 -ao alsa -really-quiet -noconsolecontrols 
 "http://translate.google.com/translate_tts?
 ie=UTF-8&client=tw-ob&q=$*&tl=En-us"; } 
 say $*

  1. 使文件可执行:
              chmod u+x speech.sh

  1. 我们将使用此脚本来宣布助手的任何动作。使用以下代码从命令行测试它:
              ~/speech.sh "Hello, World!"

  1. 实现宣布语音助手动作的系统调用如下:
              if result["Result"]["action"] == "turn_light_on": 
                self.light.on() 
                os.system("~/speech.sh Turning Lights On") 
              elif result["Result"]["action"] == "turn_light_off": 
                self.light.off() 
                os.system("~/speech.sh Turning Lights Off")

让我们测试本节中构建的内容。前面的代码片段与本章一起作为 voice_assistant_inital.py 提供,可下载。按照以下方式使其可执行:

chmod +x voice_assistant_initial.py

按照以下方式测试程序(本章还提供了可下载的音频文件):

./voice_assistant.py turn_lights_on.wav

添加按钮

让我们在语音助手中添加一个按钮。这个瞬态按钮连接到引脚 2(BCM 编号),LED 连接到引脚 3。

语音助手界面设置

  1. 为了读取按钮的按下,我们需要从gpiozero导入Button类:
       from gpiozero import Button, OutputDevice

  1. 当按钮被按下时,语音助手需要播放一个蜂鸣声,以表明它正在等待用户的命令。您可以选择的蜂鸣声可以从www.freesound.org下载。

  2. 在蜂鸣声之后,用户命令将被记录 5 秒钟。然后使用Houndify SDK 处理记录的文件。以下代码片段显示了在按钮按下时调用的触发函数:

       def trigger_function(): 
         os.system("aplay -D plughw:1,0 /home/pi/beep.wav") 
         os.system("arecord -D plughw:2,0 -f S16_LE -d 5 
         /home/pi/query.wav") 
         os.system("aplay -D plughw:1,0 /home/pi/beep.wav") 
         call_houndify()

  1. 触发函数的注册方法如下:
       button = Button(4) 
       button.when_released = trigger_function

将按钮和 LED 连接到树莓派的 GPIO 接口以测试语音助手。

图片

语音助手设置

语音助手代码文件作为本章节的附件voice_assistant.py提供下载。下载代码示例并尝试以下命令:

What is the weather in San Francisco?What is the weather in Santa Clara, California?Turn Lights OnTurn Lights Off

我们分享了一个视频(在本书网站上),演示了语音助手的功能。现在,我们已经使用 LED 演示了语音助手。为了控制台灯,只需将 LED 替换为电源开关尾 II(www.powerswitchtail.com/Pages/default.aspx)。

图片

需要注意的事项

  1. voice_assistant.py添加到/etc/rc.local,以便在启动时自动启动。

  2. 整个设置可能难以操作。将组件组装在机箱内以组织布线。

  3. 由于项目涉及电器,请使用规定的电缆并正确终止它们。确保电缆连接正确。我们将在我们的网站上分享相同的示例。

项目想法和改进

  • 目前,助手仅在按钮按下时工作。您将如何让它监听关键词?例如,“好的,谷歌”或“阿里克斯”?

  • 是否可以有一个远程触发器?想想类似亚马逊 Tap的东西。

  • 如果您有如飞利浦 Hue 或互联网连接的开关,如 WeMo 开关智能插头或 TP-Link 智能开关,您可以使用语音助手来控制它们。IFTTT 提供了用于自行控制的 applets。创建一个 maker 通道 web 钩子来控制它们。请参阅第八章的示例。

基于 Web 框架的电器控制/仪表盘

在本节中,我们将回顾如何构建仪表盘以控制电器。这可能是一个用于水族箱的仪表盘,您可以在其中监控水箱所需的所有参数,或者是一个用于花园的仪表盘,您可以根据传感器的信息控制花园的流量控制阀。我们将通过一个简单的示例来演示,并展示如何使用它来满足您的需求。

我们将使用flask框架来构建我们的仪表盘。如果您还没有安装flask框架(从前面的章节),您可以按照以下方式安装:

sudo pip3 install flask

如果你不太熟悉 Flask 框架,我们在第七章“请求和 Web 框架”中编写了一些基础知识入门。我们将讨论从 Web 仪表板(可在 a.co/1qE0I3U 获取)控制继电器板(如图所示)。

图片

继电器模块

继电器板由八个继电器组成,可用于控制八个设备。继电器额定电流为 10A,交流电压 125V 和直流电压 10A,28V。

在尝试使用继电器板控制交流电器时,遵循安全规范非常重要。如果你是电子学领域的初学者,我们建议使用单元 a.co/9WJtANZ。它配备了必要的电路和保护(如图所示)。安全第一!

图片

适用于 Raspberry Pi 的高功率继电器

8 继电器板上的每个继电器由以下组件组成:光电耦合器、晶体管、继电器和一个续流二极管(如图所示):

图片

8 继电器板上一个继电器的原理图(使用 fritzing 生成)

原理图用于解释继电器板的功能;因此,它并不准确。它缺少一些离散组件。

  1. 继电器板需要一个 5V 电源(通过 Vcc 引脚):

图片

Vcc、GND 和 GPIO 引脚

  1. 每个继电器板上的继电器都由 IN1 至 IN8 引脚控制。每个引脚都连接到一个光电耦合器(原理图中标记为 U1 的光电隔离器)。隔离器的功能是将 Raspberry Pi 与连接到继电器的电压隔离。它保护继电器在切换时免受任何瞬态电压的影响(我们已在本章末尾提供了额外的资源,以更好地了解光电耦合器)。

  2. 光电晶体管连接到 NPN 晶体管的基极。NPN 晶体管的集电极引脚连接到继电器,而发射极连接到地。

  3. 继电器由低电平信号驱动,即当给 IN1 至 IN8 中的任一引脚提供 0V 信号时。光电晶体管(光电耦合器的一部分)向晶体管的基极发送一个 信号。在这里,晶体管充当开关。它闭合电路,从而为继电器供电。这基本上是我们之前章节中讨论的晶体管开关电路,除了一个额外的组件,即光电耦合器。一个 LED 点亮,表示继电器已供电。

图片

每个继电器电路的组件(已标记)

我们强烈建议阅读有关光电耦合器的知识,以了解其需求以及如何使用低电平信号驱动此继电器板。

  1. 在每个继电器之间有一个飞轮二极管。飞轮二极管在继电器断电/关闭时保护电路免受任何电感反冲电压的影响。(我们在本章末尾包含了一个关于继电器和电感反冲的阅读资源。)

  2. 每个继电器有三个端子,即公共端子、常开端子和常闭端子。当使用低电平信号驱动其中一个继电器时,公共端子与常开端子接触。当继电器断电时,公共端子与常闭端子接触。因此,端子有常开和常闭(如图中的标签 N.O.、C 和 N.C. 所示)的名称。

继电器的端子

  1. 需要通过网络仪表板控制的设备需要连接到继电器端子,如图中所示。例如,让我们考虑一个使用 12V 适配器供电的设备。该设备需要配置,使得电源插座的正极连接到继电器的公共端子。常开端子连接到设备的正线。设备的接地保持不变。保持电源适配器插入,只要继电器未通电,设备就不应该打开。让我们回顾一下如何使用网络仪表板控制这个设备。

使用继电器搭建 12V 直流电器的电路图

对于交流电源电器,我们建议使用电源开关尾 II 或本节中较早讨论的交流继电器单元。它们适用于业余级应用。

构建网络仪表板

第一步是创建仪表板的 HTML 模板。我们的仪表板将能够控制四个电器,即打开或关闭它们:

  1. 在我们的仪表板中,我们需要一个 html table,其中表格的每一行代表一个设备,如下所示:
       <table> 
           <tr> 
               <td> 
                   <input type="checkbox" name="relay" 
                    value="relay_0">Motor</input> </br> 
               </td> 
           <td> 
               <input type="radio" name="state_0" value="On">On
               </input> 
                   <input type="radio" name="state_0" value="Off" 
                   checked="checked">Off</input>
           </td> 
       </table>

  1. 在前面的代码片段中,每个设备状态都被封装在一个数据单元格 <td> 中,每个设备由一个 checkbox 表示,设备状态由一个开/关 radio 按钮表示。例如,一个电机表示如下:
       <td> 
          <input type="checkbox" name="relay" 
          value="relay_0">Motor</input> </br> 
       </td> 
       <td> 
          <input type="radio" name="state_0" value="On">On
          </input> 
           <input type="radio" name="state_0" value="Off" 
           checked="checked">Off</input>   
       </td>

在仪表板上,这将被表示如下:

由复选框和单选按钮表示的设备

  1. 表格被封装在一个 html form 中:
       <form action="/energize" method="POST"> 
          <table> 
          . 
          . 
          . 
          </table> 
       </form>

  1. 当用户点击 energize 按钮时,设备状态提交到 flask 服务器:
       <input type="submit" value="Energize" class="button">

充电按钮

  1. 在服务器端,我们需要设置用于控制继电器的 GPIO 引脚:
       NUM_APPLIANCES = 4 

       relay_index = [2, 3, 4, 14]

  1. 列表 relay_index 代表用于控制继电器的 GPIO 引脚。

  2. 在启动服务器之前,我们需要为所有设备创建一个 OutputDevice 对象(来自 gpiozero 模块):

       for i in range(NUM_APPLIANCES): 
               devices.append(OutputDevice(relay_index[i], 
                                      active_high=False))

  1. 为每个设备/继电器初始化并添加到 devices 列表的 OutputDevice 对象。

  2. 当表单提交(通过点击激活按钮)时,POST 请求由 energize() 方法处理。

  3. 我们正在控制由 relay_x 表示的四个设备,它们对应的状态由 state_x 表示,即开启或关闭。默认状态是关闭。

  4. 当用户提交表单时,我们确定 POST 请求是否包含与每个设备相关的信息。如果需要打开/关闭特定设备,我们调用该设备对象的 on()/off() 方法:

       relays = request.form.getlist("relay") 
       for idx in range(0, NUM_APPLIANCES): 
           device_name = "relay_" + str(idx) 
           if device_name in relays: 
               device_state = "state_" + str(idx) 
               state = request.form.get(device_state) 
               print(state) 
               if state == "On": 
                   print(state) 
                   devices[idx].on() 
               elif state == "Off": 
                   print(state) 
                   devices[idx].off()

  1. 在前面的代码片段中,我们以列表的形式获取与所有继电器相关的信息:
       relays = request.form.getlist("relay")

  1. 在表单中,每个设备由一个值 relay_x 表示(relay_0 通过 relay_3)。使用 for 循环确定特定的继电器是开启还是关闭。每个设备的状态由 state_x 表示,其中 x 对应于设备(从 0 到 3)。

  2. 在本例中使用的 GPIO 引脚连接到继电器板引脚,IN1 通过 IN4。继电器板由 Raspberry Pi 的 GPIO 电源供电。或者,你也可以使用外部电源供电。(你仍然需要将 Raspberry Pi Zero 的地线引脚连接到继电器板。)

  3. 之前提到的仪表板可以在子文件夹 flask_framework_appliance(包括 flask 服务器、html 文件等)下找到。在下面的快照中,电机和油箱灯 2 被检查并设置为开启。在这张图片中,第一和第三继电器被激活。

开启电机和油箱灯 2

继电器 1 和 3 被激活

读者练习

在本节中,我们使用了 POST 请求来控制设备。你将如何使用 GET 请求从温度传感器显示室温?

项目想法/改进

  • 如果你有一些基本的网页设计技能,你应该能够构建一个具有更好审美吸引力的仪表板。

  • 请记住,仪表板应尽可能提供详细的信息。确定数据可视化工具如何增强你的仪表板。

  • 考虑用滑动切换开关(在移动应用程序中使用的类型)替换复选框和单选按钮。

  • 你可以使用本地浏览器构建一个用于切换假日灯光序列的仪表板。考虑一下如何在假日期间与邻居竞争。

  • 你可以将继电器板和 Raspberry Pi Zero 永久安装在给定的防风雨外壳中,如www.mcmelectronics.com/product/21-14635所示。查看本书的网站以获取一些示例。

个人健康改善—久坐是新的吸烟

此项目使用了特定的配件。欢迎用替代品替换。

到目前为止,我们已经讨论了可以增强你生活环境的项目。在本节中,我们将在 Raspberry Pi Zero 上编写一些 Python 代码,并构建一个帮助你提升个人健康的工具。

根据世界卫生组织的数据,一周 150 分钟的身体活动可以帮助你保持健康。最近的研究发现,每天走 10000 步可以帮助避免生活方式疾病。我们一直在使用步数计来跟踪我们的日常身体活动。由于我们倾向于在日常承诺中忽视个人健康,因此很难保持身体活动的连贯性。例如,在下面的身体活动时间线中,你会注意到所有的身体活动都集中在一天的末尾。

一天的身体活动(从商业步数计获取的数据)

我们将构建一个视觉辅助工具,以提醒我们保持身体活跃。我们相信这个工具应该能帮助你充分利用个人健身追踪器。以下是此项目的推荐配件:

  • 步数计:步数计的价格从 20 美元到 100 美元不等。我们建议从 Fitbit 购买追踪器,因为它提供了丰富的开发者资源。不需要追踪器。我们将使用 Fitbit One(a.co/8xyNSmg)来演示这个视觉辅助工具,并在最后提出替代方案。

  • Pimoroni Blinkt(可选):这是一条 LED 灯带(www.adafruit.com/product/3195),可以堆叠在你的 Raspberry Pi Zero 的 GPIO 引脚上(如图所示)。

Pimoroni Blinkt

  • Pimoroni Rainbow HAT(可选 www.adafruit.com/products/3354):这是一个为 Raspberry Pi 上的 Android Things 平台设计的附加硬件。它配备了 LED 灯、14 段显示器和蜂鸣器。它对于项目来说可能很有用。

Android Things 的 Rainbow HAT

  • 或者,你可以用你的创造力向这个视觉辅助工具添加 LED 灯带和组件。

安装必需的软件包

第一步是安装必需的包。因为我们将使用 Fitbit 追踪器,所以我们需要安装fitbit python 客户端

sudo pip3 install fitbit cherrypy schedule

如果你打算使用 Pimoroni Blinkt LED 灯带,你应该安装以下包:

sudo apt-get install python3-blinkt

如果你打算使用彩虹 HAT,以下包需要安装:

curl -sS https://get.pimoroni.com/rainbowhat | bash

获取 Fitbit 客户端的访问密钥

我们需要访问密钥来使用 Fitbit API。fitbit python 客户端存储库中有可用的脚本,网址为github.com/orcasgit/python-fitbit

访问密钥也可以从 Linux 或 Mac OS 笔记本电脑的命令行终端获取。

  1. dev.fitbit.com/apps创建一个新的应用:

在 dev.fitbit.com 注册一个新的应用

  1. 在注册新应用时,填写描述,包括您应用的名称,并给出一个临时描述、组织、网站等,并将 OAuth 2.0 应用类型设置为个人,访问类型设置为只读。将回调 URL 设置为http://127.0.0.1:8080

设置回调 URL

  1. 一旦您的应用创建完成,从应用的仪表板复制 Client ID 和 Client Secret。

记录 client_id 和 client_secret

  1. 从 Raspberry Pi 的命令行终端下载以下脚本:
 wget https://raw.githubusercontent.com/orcasgit/
       python-fitbit/master/gather_keys_oauth2.py

下一步需要通过从 Raspberry Pi Zero 的桌面打开命令行终端(而不是通过远程访问)来执行。

  1. 通过传递client idclient secret作为参数来执行脚本:
 python3 gather_keys_oauth2.py <client_id> <client_secret>

  1. 它应该在您的 Raspberry Pi Zero 桌面启动浏览器,并将您导向一个请求授权访问您信息的www.fitbit.com/home页面。

授权访问您的数据

  1. 如果授权成功,它应该将您重定向到一个显示以下信息的页面:

授权访问 Fitbit API

  1. 关闭浏览器并复制命令提示符上显示的refresh_tokenaccess_token信息。

复制 access_token 和 refresh_token

我们已经准备好使用 Fitbit API 了!让我们来测试一下!

Fitbit API 测试

Fitbit API 的文档可在python-fitbit.readthedocs.org/找到。以下是一个获取今天身体活动的简单示例:

  1. 第一步是导入fitbit模块:
       import fitbit

  1. 我们必须使用本节中较早提到的client keyclient secretaccess_tokenrefresh_token初始化fitbit客户端:
       fbit_client = fitbit.Fitbit(CONSUMER_KEY, 
                                   CONSUMER_SECRET, 
                                   access_token=ACCESS_TOKEN, 
                                       refresh_token=REFRESH_TOKEN)

  1. 根据 Fitbit API 文档,可以使用intraday_time_series()方法检索当前日的身体活动。

  2. 获取身体活动所需的参数包括需要检索的资源;即步数、detail_level,即需要检索给定信息的最小时间间隔、开始时间和结束时间。

  3. 开始时间是当前日的午夜,结束时间是当前时间。我们将使用datetime模块来获取当前时间。有一个名为strftime的函数,它以小时:分钟格式给我们当前时间。

确保您的 Raspberry Pi Zero 的操作系统时间已正确配置为本地时区设置。

       now = datetime.datetime.now() 
       end_time = now.strftime("%H:%M") 
       response = fbit_client.intraday_time_series('activities/steps', 
         detail_level='15min', 
         start_time="00:00", 
         end_time=end_time)

  1. fitbit客户端返回一个包含当前日身体活动和 15 分钟间隔内日间活动的字典:
       print(response['activities-steps'][0]['value'])

  1. 这个示例作为本章的一部分提供下载,作为fitbit_client.py。如果你有一个 Fitbit 追踪器,注册一个应用程序并亲自测试这个示例。

构建视觉辅助工具

让我们构建一个视觉辅助工具,使用 LED 灯带显示给定一天内所走的步数。LED 灯带将根据每日体力活动像进度条一样亮起。

  1. 第一步是在构建视觉辅助工具时导入所需的库。这包括fitbitblinkt库。我们还将导入一些额外的库:
       import blinkt 
       import datetime 
       import fitbit 
       import time 
       import schedule

  1. 确保你已经拥有本节中讨论的所需令牌:
       CONSUMER_KEY = "INSERT_KEY" 
       CONSUMER_SECRET = "INSERT_SECRET" 
       ACCESS_TOKEN = "INSER_TOKEN" 
       REFRESH_TOKEN = "INSERT_TOKEN"

  1. 8小时需要一个新的刷新令牌。这是 API 授权机制的一个特性。因此,我们需要一个函数,使用现有的令牌获取新的令牌:
       def refresh_token(): 
           global REFRESH_TOKEN 
           oauth = fitbit.FitbitOauth2Client(client_id=CONSUMER_KEY, 
             client_secret=CONSUMER_SECRET, 
             refresh_token=REFRESH_TOKEN, 
             access_token=ACCESS_TOKEN) 
           REFRESH_TOKEN = oauth.refresh_token()

  1. refresh_token()函数中,我们使用了FitbitOauth2Client类来刷新令牌。需要注意的是,我们使用了global关键字。global关键字有助于修改REFRESH_TOKEN,并允许在程序的其它部分使用新的令牌。如果没有global关键字,对任何变量的更改都将限制在refresh_token()函数内。

通常来说,使用global关键字是一个不好的做法。请根据你的最佳判断使用它。

  1. 接下来,我们需要一个函数来使用Fitbit类检索步数。我们将使用与上一个示例相同的程序。初始化fitbit类并使用intraday_time_series检索步数:
       def get_steps(): 
           num_steps = 0 
           client = fitbit.Fitbit(CONSUMER_KEY, 
                                  CONSUMER_SECRET, 
                                  access_token=ACCESS_TOKEN, 
                                  refresh_token=REFRESH_TOKEN) 
           try: 
               now = datetime.datetime.now() 
               end_time = now.strftime("%H:%M") 
               response = 
                client.intraday_time_series('activities/steps', 
                  detail_level='15min', 
                  start_time="00:00", 
                  end_time=end_time) 
           except Exception as error: 
               print(error) 
           else: 
               str_steps = response['activities-steps'][0]['value'] 
               print(str_steps) 
               try: 
                   num_steps = int(str_steps) 
               except ValueError: 
                   pass 
           return num_steps

  1. 在主函数中,我们使用schedule库安排一个定时器,每8小时刷新一次令牌(pypi.python.org/pypi/schedule):
       schedule.every(8).hours.do(refresh_token)

  1. 我们每15分钟检查一次步数,并相应地点亮 LED。因为 Pimoroni Blinkt 由八个 LED 组成,我们可以为每1250步的体力活动点亮一个 LED:
       # update steps every 15 minutes 
       if (time.time() - current_time) > 900: 
           current_time  = time.time() 
           steps = get_steps() 

       num_leds = steps // 1250 

       if num_leds > 8: 
           num_leds = 8 

       for i in range(num_leds): 
           blinkt.set_pixel(i, 0, 255, 0) 

       if num_leds <= 7:  
           blinkt.set_pixel(num_leds, 255, 0, 0) 
           blinkt.show() 
           time.sleep(1) 
           blinkt.set_pixel(num_leds, 0, 0, 0) 
           blinkt.show() 
           time.sleep(1)

  1. 对于每1250步的倍数,我们使用blinkt.set_pixel()方法将 LED 的颜色设置为绿色。下一个 LED 闪烁红色。例如,在撰写本节时,总步数为 1604 步。这是(1250 x 1)+ 354 步。因此,我们点亮一个 LED 为绿色,下一个 LED 闪烁红色。这表明步数正在进展中。

  2. 这里的图片显示了当进度少于1250步时闪烁的红色 LED:

体力活动进度少于 1250 步

  1. 在走动之后,进度向右移动了一个 LED:

1604 步的体力活动

  1. 下一步是在没有最低体力活动时触发蜂鸣器。这是通过将蜂鸣器连接到 Raspberry Pi Zero 的 GPIO 引脚来实现的。我们在前面的章节中演示了蜂鸣器的使用。

  2. 早期示例作为visual_aid.py与本章一起提供。我们将让你自己找出在一段时间(例如,一小时)内没有最低限度的身体活动时触发蜂鸣器的逻辑。

将这个视觉辅助工具安装在一个显眼的位置,看看它是否能激励你保持身体活跃!如果你使用Pimoroni Rainbow HAT,你可以使用 14 段显示器显示步骤。

智能草坪喷水装置

在遭受干旱的州,如美国加利福尼亚州,该州某些地区对用水有严格的限制。例如:在夏季,一些城市通过一项法令将每天的用水量限制在 250 加仑。在这样的州,在下雨前一天发现草坪喷水装置开启是荒谬的。我们将构建一个草坪喷水控制器,只有在预测第二天不会下雨时才会打开。

为了构建一个智能草坪喷水装置,我们需要一个流量控制电磁阀(例如,www.sparkfun.com/products/10456)。确保阀门能够满足水压要求。这个流量控制阀可以通过前面章节中讨论的晶体管切换电路或本章前面讨论的继电器板与 Raspberry Pi Zero 接口。

  1. 我们将使用DarkSky API (darksky.net)来获取天气信息。它提供了一个简单的响应格式,可以用来确定第二天是否会下雨。

  2. 在网站上注册一个免费账户,并在控制台获取一个开发者密钥。

  3. 根据 API 文档,可以通过以下方式获取本地天气信息:

       https://api.darksky.net/forecast/[key]/[latitude],[longitude]

  1. 经纬度坐标可以通过简单的网络搜索获得。例如,Newark, CA 的请求 URL 是:
       URL = ("https://api.darksky.net/forecast/key" 
       "/37.8267,-122.4233?exclude=currently,minutely,hourly")

  1. 响应包括currentminutelyhourly预报。可以使用exclude参数排除,如前一个 URL 所示。

  2. 现在,我们只需要在第二天不会下雨的情况下打开喷水装置。根据 API 文档,天气预报以Data Point object返回。数据点包括一个名为icon的字段,表示天气将是clearcloudy还是rainy

  3. 让我们编写一个check_weather()方法,用于获取一周的天气:

       def check_weather(): 
          try: 
                response = requests.get(URL) 
          except Exception as error: 
                print(error) 
          else: 
                if response.status_code == 200: 
                      data = response.json() 
                      if data["daily"]["data"][1]["icon"] == "rain": 
                            return True 
                      else: 
                            return False

  1. 如果GET请求成功,这可以通过响应的状态码来确定,使用json()方法对json响应进行解码。

  2. 第二天的天气信息在data["daily"]["data"][1]中可用(打印出响应并自行验证)。

  3. 由于icon键提供的是机器可读的响应,它可以用来打开喷水装置。因此,check_weather()在将要下雨时返回True,反之亦然。

我们将让你自己找出如何使用 GPIO 引脚连接电磁阀。早期的代码示例可以作为本章的附件下载,文件名为lawn_sprinkler.py

读者练习

我们正在利用第二天的天气预报信息来开启洒水器。请查阅文档,并修改代码以考虑当前的天气信息。

项目改进

  • 你会如何给控制器添加湿度传感器?

  • 你会如何将传感器连接到 Raspberry Pi Zero 并利用它来开启洒水器?

摘要

在本章中,我们回顾了四个涉及 Raspberry Pi Zero(以及 Python 编程)的项目,这些项目专注于房屋周围的具体改进。这包括语音助手、基于 Web 框架的家电控制、身体活动激励工具和智能草坪洒水器。这些项目的理念在于展示 Python 编程在提高我们生活质量方面的应用。我们证明了使用 Raspberry Pi Zero 构建应用程序(作为昂贵现成产品的更好替代品)是可能的。

我们还推荐以下项目想法供你考虑:

  • 基于 Slack 通道的家电控制:当你不在家工作时,你是否担心宠物在家中的温度条件?为什么不设置一个温度传感器,当温度过高时发送 Slack 通道警报,建议你打开空调?

  • 桌面喷泉:使用 Raspberry Pi Zero 和 RGB LED 灯带,你可以为桌面喷泉创建灯光效果。

  • 鸟食器监控器:这是我们一直在努力的项目。我们正在尝试追踪来后院喂食的鸟类。鸟食器配备了 Raspberry Pi Zero 和摄像头。请关注本书的网站以获取一些更新。

  • 假日灯光控制器:在假日期间,一些特殊的灯光和音频效果如何?

  • 使用 Raspberry Pi Zero 控制现成产品:你是否有大量闲置未用的 Wi-Fi 电插座?为什么不尝试使用你的 Raspberry Pi Zero(提示:IFTTT)来控制它们?

  • 番茄钟计时器:你听说过提高生产力的番茄钟技术吗?为什么不制作一个交互式设备来提高你的生产力?

学习资源

第十一章:小贴士和技巧

我们已经讨论了 Python 编程的不同概念以及您可以使用树莓派 Zero 作为平台开发的 Python 应用程序。在我们结束今天的内容之前,我们想向您展示一些可能对您有用的技巧和窍门。我们还将讨论本章中承诺讨论的主题。

更改您的树莓派密码

当您开发应用程序时,您可能会倾向于购买多个树莓派 Zero 板。一旦您完成设置微型 SD 卡并通电,请更改您的 Raspbian 登录的默认密码。在 2016 年的Mirai 僵尸网络攻击中,连接设备上的默认密码造成了混乱(blogs.akamai.com/2016/10/620-gbps-attack-post-mortem.html)。考虑到攻击的规模,树莓派基金会发布了一个新的操作系统更新(www.raspberrypi.org/blog/a-security-update-for-raspbian-pixel/),默认禁用了树莓派的 SSH 访问。您可以通过以下两种方式更改密码:

  • 在桌面上将密码更改。转到菜单 | 首选项 | 树莓派配置。在系统选项卡下,选择更改密码。

  • 启动命令行终端,并在提示符下输入 passwd。它将提示您输入树莓派桌面的当前密码,然后是新密码。

更新您的操作系统

不时,可能会有针对 Raspbian OS 的相关安全更新。订阅树莓派基金会博客的更新,以了解此类警报。当您的操作系统有更新时,可以通过以下命令提示符进行更新:

    sudo apt-get update
 sudo apt-get dist-upgrade

有时,操作系统更新可能会破坏某些接口驱动程序的不稳定性。谨慎操作,并检查相关论坛上是否有问题报告。

设置您的开发环境

让我们讨论如何设置您的开发环境,以便在树莓派 Zero 上工作:

  1. 默认情况下,SSH 访问树莓派被禁用。从您的桌面启动树莓派配置。在左上角,转到菜单 | 首选项 | 树莓派配置:

图片

启动树莓派配置

  1. 在树莓派配置的接口选项卡下,启用 SSH 和 VNC,然后点击“确定”以保存更改。

图片

启用 SSH 和 VNC

  1. 启动树莓派的命令提示符,并输入以下命令:ifconfig。如果您使用 Wi-Fi 适配器将树莓派 Zero 连接到网络,请定位以下截图中的树莓派 Zero 的 IP 地址。在这种情况下,IP 地址是192.168.86.111

图片

树莓派 Zero 的 IP 地址

  1. 您也可以通过悬停在右上角的 Wi-Fi 符号上找到 IP 地址。

如果您的网络位于防火墙后面,IP 地址信息可能没有用。例如,像咖啡馆这样的公共无线网络都有防火墙。

现在 Raspberry Pi Zero 已设置用于 SSH 访问,让我们尝试远程访问 Raspberry Pi。

通过 Windows 进行 SSH 访问

  1. 如果您有一台 Windows 笔记本电脑,请从(www.chiark.greenend.org.uk/~sgtatham/putty/download.html)下载PuTTY客户端。启动putty.exe

启动 putty.exe

  1. 在高亮区域输入您的 Raspberry Pi Zero 的 IP 地址并点击“打开”。

  2. 如果出现提示(如下面的截图所示)以保存设置,请选择“是”:

选择“是”

  1. pi用户登录并输入您的 Raspberry Pi 的密码:

以 pi 用户登录

  1. 您现在已经获得了对您的 Raspberry Pi 的远程访问权限。尝试在您的桌面上执行命令。

体验远程桌面

  1. 如果由于某些原因 PuTTY SSH 会话断开连接,请右键单击并选择“重启会话”。

右键单击以重启会话

通过 Linux/macOS 进行 SSH 访问

执行以下步骤以通过 Linux/macOS 进行 SSH 访问:

  1. 在 macOS 上,您可以按照以下方式启动终端:按住 command 键和空格键并启动搜索工具。输入Terminal并点击第一个选项以启动新的终端窗口。

启动终端

  1. 在终端中,使用ssh命令登录到您的 Raspberry Pi:
       ssh pi@192.168.86.111 -p 22

  1. 当提示输入密码时,输入您的 Raspberry Pi 的密码并获取对 Raspberry Pi 的远程访问权限。

通过 macOS 终端进行远程访问

  1. 在 Ubuntu 桌面上,可以使用键盘上的快捷键Ctrl + Alt + T启动终端。SSH 访问类似于 macOS。

通过 Ubuntu 进行 SSH 访问

将文件从/到您的 Pi 传输

在编写项目代码时,在笔记本电脑上编写代码并将文件传输到 Raspberry Pi 进行远程测试会更方便。这在需要远程访问时尤其有用(例如,在第九章的机器人[6b31becd-56d1-47bc-ad5c-fdefd53c8c8f.xhtml],让我们来建造一个机器人!)。

WinSCP

  1. 在 Windows 机器上,可以使用 WinSCP (winscp.net/eng/index.php)进行双向文件传输。下载软件并在您的笔记本电脑上安装它。

  2. 启动 WinSCP 并在窗口中输入 Raspberry Pi Zero 的 IP 地址、用户名和密码。在文件协议:下拉菜单下,选择 SFTP 或 SCP。

输入 IP 地址、用户名和密码

  1. 当软件提示是否为受信任的主机时,请点击“是”:

在此警告中点击“是”

  1. 如果凭据正确,软件应提供树莓派 Zero 文件系统的远程访问。要下载文件或上传文件,只需右键单击并选择上传/下载选项(如下面的截图所示):

右键单击上传/下载文件

Mac/Linux 环境

在 Mac/Linux OS 环境中,可以使用 scp 命令将文件传输到/从树莓派。例如,可以使用以下语法将文件传输到树莓派:

       scp <filename> pi@<IP address>:<destination>
 scp my_file pi@192.168.1.2:/home/pi/Documents

可以按照以下方式从树莓派传输文件:

       scp pi@<IP address>:<file location> <local destination>
 scp pi@192.168.1.2:/home/pi/Documents/myfile Documents

Git

Git 是一个版本控制工具,在开发应用程序时非常有用。它还允许与他人共享代码示例。例如,这本书的最新代码示例都提交到了 github.com/sai-y/pywpi.git。由于 Git 允许版本控制,因此可以在项目的不同阶段保存代码的副本。如果代码中出现问题,还可以恢复到已知的工作版本。

在编写这本书的过程中,我们使用文本编辑器编写代码,例如 Notepad++Sublime Text editor,并将它们保存到我们在 github 上的仓库中。在树莓派方面,我们从 github 复制了仓库并测试了我们的代码。Git 还允许创建分支,这是代码仓库的克隆镜像。Git 分支允许在不破坏代码的现有版本的情况下,工作在新功能或修复现有问题。一旦功能实现或问题解决,我们可以将更改合并到仓库的主分支。我们建议完成这个教程,以了解 Git 的重要性,因此请参阅 guides.github.com/activities/hello-world/

命令行文本编辑器

有时可能需要从命令行对代码文件进行一些小的修改或更改配置文件。每次都使用图形文本编辑器是不切实际的。有一些命令行文本编辑器,经过一些练习后可以派上用场。

Raspbian OS 附带的一个有用的文本编辑器是 nanonano 是一个非常简单的文本编辑器,并且非常容易使用。例如,让我们考虑一个场景,我们想在代码文件中添加一个 API 的秘密密钥。这可以通过通过命令提示符(SSH 或桌面上的命令行终端)打开文件来完成:

    nano visual_aid.py

应该打开文件的 内容,如下面的截图所示:

纳米文本编辑器

  1. 使用键盘的箭头键导航到需要编辑的行。可以手动编辑该行,或者将秘密密钥粘贴到文件中(Mac 上为 CMD + V,Ubuntu 终端上为 Ctrl + Shift + V,在 PuTTY 中只需右键单击)。

  2. 文件编辑完成后,按 Ctrl + X 完成编辑,并按 Y 保存更改:

图片

保存更改

  1. 在下一个提示符下按Enter键以将内容保存到文件。

图片

将内容保存到原始文件

学习使用命令行文本编辑器在项目工作中非常有用。

还有其他文本编辑器,如vivim。然而,nano文本编辑器使用起来要简单得多。

图形文本编辑器

除了 IDLE 的图形文本编辑器外,还有很多其他的文本编辑器。在Windows上,我们推荐使用 Notepad++ (notepad-plus-plus.org/)。它附带了很多插件和功能,可以将 Python 关键字与其他代码部分区分开来。它还提供了视觉辅助,以正确缩进代码。

图片

Notepad++编辑器

虽然 Notepad++是免费的,但有一个名为 Sublime 的跨平台(有 Windows、Ubuntu 和 Mac 版本)的文本编辑器,它附带一个评估许可证,但许可证费用为 70 美元。我们相信这是值得的,因为它附带了一个丰富的开发环境。

图片

Sublime 文本编辑器

SSH 别名(在 Mac/Linux 终端)

在进行项目工作时,SSH 别名对于远程访问树莓派非常有用。别名是任何命令的快捷方式。例如,SSH 登录的别名可以这样实现:

    nano ~/.bash_aliases

将以下行添加到文件中(确保添加了树莓派的 IP 地址):

    alias my_pi='ssh pi@192.168.1.2 -p 2' 

加载别名文件:

    source ~/.bash_aliases

现在,我们只需在命令提示符中调用my_pi即可简单地访问 pi。自己试试吧!

在 PuTTY 上保存 SSH 会话

在 Windows 环境中,可以保存 SSH 会话以供重复使用。启动 PuTTY,输入树莓派的 IP 地址,并保存为会话名称,如下截图所示:

图片

保存会话

当需要 SSH 访问树莓派时,启动 PuTTY 并加载my_session(如下截图所示):

图片

加载会话

VNC 访问树莓派

在启用 SSH 的同时,我们也启用了 VNC。VNC 是一个允许远程查看树莓派桌面的工具。因为 VNC 已经在树莓派上启用,所以请下载并安装 VNC 查看器 (www.realvnc.com/download/viewer/)。VNC 查看器适用于所有操作系统:

  1. 登录非常简单。只需输入树莓派的用户名和密码:

图片

登录

  1. 它应直接带您进入树莓派的桌面。

图片

通过 VNC 访问树莓派桌面

通过 USB OTG 端口 SSH

这个技巧仅适用于高级用户。

我们在 gist.github.com/gbaman/975e2db164b3ca2b51ae11e45e8fd40a 上发现了这个实用的技巧。当你没有 USB 无线适配器和 USB OTG 转换器时,这很有帮助。我们可以直接使用微-USB 线缆将 Raspberry Pi 的 USB-OTG 端口连接到电脑的 USB 端口,并通过 SSH 访问它。这是由于 Raspberry Pi Zero 的 USB OTG 端口在连接到电脑时枚举为 USB-over-Ethernet 设备:

  1. 将 Raspbian Jessie OS 系统烧录到微 SD 卡后,打开 SD 卡的内容并找到文件 config.txt

  2. 在文件末尾 config.txt 中,添加以下行到文件中:dtoverlay=dwc2

  3. 在文件 cmdline.txt 中,在 rootwait 后面添加 modules-load=dwc2,g_ether。确保该文件的每个参数之间只有一个空格。

  4. 保存文件并将 SD 卡插入您的 Raspberry Pi Zero。

  5. 在命令行终端,使用 ssh pi@raspberrypi.local 登录。这应该在 Mac/Ubuntu 环境下工作。在 Windows 环境中,需要 Bonjour 协议驱动程序 (support.apple.com/kb/DL999?locale=en_US)。此外,还需要 RNDIS 以太网驱动程序 (developer.toradex.com/knowledge-base/how-to-install-microsoft-rndis-driver-for-windows-7)。

Raspberry Pi Zero 板的 RUN 开关

Raspberry Pi Zero 有一个标记为 RUN 的终端,有两个引脚。这个终端可以用来重置 Raspberry Pi Zero。特别是如果 Raspberry Pi Zero 安装在一个难以到达的地方,这尤其有用。可以通过连接一个瞬态按钮跨接两个引脚来重置 Raspberry Pi Zero。因为瞬态接触足以重置板子,所以这个终端在 Raspberry Pi Zero 安装在非常遥远的位置时可能很有用,并且可以通过互联网连接(如果存在另一个控制 RUN 终端引脚的设备)来重置。

Raspberry Pi Zero 上 Run 引脚的位置

我们还找到了这个优秀的教程来设置 Raspberry Pi 的重置开关 - blog.adafruit.com/2014/10/10/making-a-reset-switch-for-your-raspberry-pi-model-b-run-pads-piday-raspberrypi-raspberry_pi/

GPIO 引脚映射

对于 Raspberry Pi Zero 的绝对初学者来说,这个 GPIO 映射板可能很有用。它映射了 Raspberry Pi Zero 的所有 GPIO 引脚。

Raspberry Pi Zero 的 GPIO 板。图片来源:adafruit.com

它可以自由地适合并直接放置在 GPIO 引脚的顶部,如图所示。标签可以帮助原型设计。

可堆叠面包板

这种可堆叠的面包板对电子领域的初学者非常有用。它提供了对所有 GPIO 引脚的访问,面包板就紧挨着它。这有助于快速为你的项目需求原型化电路。

图片

可堆叠面包板

该面包板可在(rasp.io/prohat/)找到。我们还找到了另一个带有面包板的机箱-a.co/dLdxaO1

摘要

在本章中,我们讨论了使用 Raspberry Pi Zero 开始 Python 编程的不同技巧和窍门。我们希望本章以及所有其他章节提供的内容能帮助你入门。

我们编写这本书时考虑到了动手实践,我们相信我们已经展示了我们想法的最佳迭代。祝你的项目好运!

posted @ 2025-09-20 21:35  绝不原创的飞龙  阅读(140)  评论(0)    收藏  举报