Python-物联网编程实践(全)

Python 物联网编程实践(全)

原文:zh.annas-archive.org/md5/7FABA31DD38F615362E1254C67CC152E

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

欢迎阅读《物联网的实用 Python 编程》。本书的重点是围绕树莓派、电子、计算机网络、Python 编程语言以及如何将所有这些元素结合起来构建复杂多样的物联网项目。

我们将从多个角度研究这些元素,比较和对比不同的选项,并讨论我们构建的电子电路背后的如何为什么。当您阅读完本书时,您将拥有一个广泛的工具包,其中包括电子接口代码示例、网络代码示例和电子电路示例,您可以借鉴、调整和重新设计以满足自己的需求和项目。

我期待着与您一起踏上这段物联网之旅。

这本书是为谁写的

本书适用于应用程序开发人员、物联网专业人士和对利用 Python 编程语言构建物联网应用程序感兴趣的爱好者。它是为有一定经验的中高级软件工程师编写的,他们在桌面、Web 和移动开发方面经验丰富,但对电子、物理计算和物联网几乎没有接触。

本书涵盖了什么

第一章,设置开发环境,探讨了树莓派操作系统中的 Python 生态系统,并教您如何正确地为 Python 开发项目做好准备。您还将学习启动 Python 程序的替代方法,以及如何配置树莓派进行 GPIO 接口。

第二章,开始使用 Python 和物联网,教授了电子学和 Python 的 GPIO 接口的基础知识。您将构建并尝试使用 Python 控制的简单电子电路,并将这些知识结合起来,从头开始构建一个简单但完整的可通过互联网控制的物联网应用,使用 dweet.io 平台。

第三章,使用 Flask 进行 RESTful API 和 Web 套接字进行网络连接,探讨了如何使用两种方法在 Python 中构建网络服务器——RESTful API 和 Web 套接字。您将学习如何将这些服务器与 Python 和 HTML/JavaScript 用户界面结合使用,以便从 Web 浏览器控制网络上的电子电路。

第四章,使用 MQTT、Python 和 Mosquitto MQTT Broker 进行网络连接,教授使用消息队列遥测传输的网络连接方法,这是分布式物联网应用的热门选择。您将学习如何将 MQTT 与 Python 和 HTML/JavaScript 用户界面结合使用,以便从网络和 Web 浏览器控制电子电路。

第五章,将树莓派连接到物理世界,探讨了用于接口和控制电子的不同基于 Python 的软件选项和技术,使用树莓派的 GPIO 引脚。您还将构建并学习如何使用 ADS1115 模拟数字转换器模块来扩展树莓派的本机接口选项,并介绍脉宽调制PWM),这是一个重要的电子和接口概念,您将在后续章节中使用。

第六章,软件工程师的电子学 101,教授核心电子概念和基础知识。您将学习常见电子和接口电路背后的基本如何为什么,以及它们如何在实际中正确安全地与传感器和执行器进行接口。您还将学习数字和模拟电子学的区别,以及每种电子学如何适用于和影响接口电路要求。本章中学到的许多基础知识在后续章节中会以实际应用的形式呈现,因为我们将使用不同的电子元件和模块进行工作。

第七章《打开和关闭设备》教会你如何使用光耦、MOSFET 晶体管和继电器来使用树莓派和 Python 打开和关闭其他电路。你还将了解电路负载,如何测量电路负载,以及这如何影响在电路中选择和使用光耦、MOSFET 晶体管和继电器。

第八章《灯光、指示灯和信息显示》教会你如何使用 APA102 LED 灯带、RGB LED、OLED 显示屏和蜂鸣器结合 Python 创建视觉和声音定向电路和应用。

第九章《测量温度、湿度和光照水平》教会你如何使用树莓派和 Python 测量常见的环境属性。你将使用 DHT11/22 温湿度传感器构建电路,并学习使用光敏电阻(LDR)来检测光的存在或缺失。在本章中,你还将加深对模拟电子学的实际理解和经验,并应用基本原理构建湿度检测电路和应用。

第十章《使用舵机、电机和步进电机进行运动》教会你如何使用流行的机械设备和树莓派以及 Python 创建运动。你将学习如何使用 PWM 控制舵机以创建角度运动,使用 H 桥 IC 电路和电机控制其速度和旋转方向。此外,你还将学习如何调整 H 桥 IC 电路以与步进电机一起使用,以便在需要精确控制运动的项目中使用。

第十一章《测量距离和检测运动》教会你使用 HC-SR04 超声波距离传感器测量距离的原理,以及如何使用 HC-SR501 PIR 传感器在宏观尺度上检测运动。你还将学习如何使用比例式和开关式霍尔效应传感器来检测运动并在微观尺度上测量相对距离。

第十二章《高级 IoT 编程概念-线程、AsyncIO 和事件循环》是一个高级编程章节,探讨了构建复杂 Python 程序的替代方法。你将学习 Python 线程、异步 I/O、经典事件循环和发布-订阅模式,所有这些都在电子接口的背景下。到本章结束时,你将尝试并理解四种功能等效的应用程序,它们以四种非常不同的方式编写。

第十三章《IoT 可视化和自动化平台》是一次探索与 IoT 相关的在线服务和集成的旅程。你将基于第九章《测量温度、湿度和光照水平》中的 DHT11/22 温湿度电路创建两个环境监测应用。首先,你将利用第四章《使用 MQTT、Python 和 Mosquitto MQTT Broker 进行网络连接》中的 MQTT 理解,在 ThingSpeak.com 上创建在线仪表板,显示和绘制温度和湿度数据。然后,你还将应用第四章《使用 MQTT、Python 和 Mosquitto MQTT Broker 进行网络连接》中的 RESTful API 概念,并构建一个 If-This-Then-That(IFTTT.com)工作流 Applet,当温度升高或低于某一点时发送电子邮件给你。

第十四章,将所有内容绑在一起-物联网圣诞树,汇集了您在前几章学到的许多主题和概念,围绕一个连接到互联网的圣诞树的多方面示例。从电子学的角度来看,您将重新访问第八章中的 APA102 LED 灯带,灯光、指示灯和信息显示(这将是圣诞树的灯光),以及第十章中的舵机,使用舵机、电机和步进电机进行运动(用于提供摇晃或摇动树的机制)。从网络的角度来看,您将重新访问第二章中的 dweet.io,使用 Python 和物联网开始;第三章中的 RESTful-APIs,使用 Flask 进行 RESTful API 和 Web Sockets 网络;以及第四章中的 MQTT,使用 Mosquitto MQTT Broker 进行 MQTT 网络,并学习如何结合技术以实现需要桥接不同技术的复杂集成。最后,您将重新访问第十三章中的 IFTTT,物联网可视化和自动化平台,并创建两个 Applets,让您可以通过互联网控制树的灯光,并让树在互联网上摇晃或摇动。这三个 Applets 包括电子邮件控制,以及使用 Google 助手的语音激活控制。

为了充分利用本书

以下标题提供了您在本书中成功完成练习所需的硬件、软件、电子设备和外围设备的概述。

  • 硬件和软件:本书中的所有练习和代码都是在以下硬件和软件版本上构建和测试的:

    • 树莓派 4 型 B 型
  • 树莓派 OS Buster(带桌面和推荐软件)

  • Python 版本 3.5

我假设您将使用等效的设置;但是,可以合理地期望代码示例在树莓派 3 型 B 型或不同版本的 Raspbian OS 或树莓派 OS 上工作,只要您的 Python 版本是 3.5 或更高。

如果您对您的 Python 版本不太确定,不用担心。在第一章中,设置您的开发环境,我们的第一个任务之一将是了解树莓派上的 Python,并确定可用的版本。

  • 电子零件和设备:我们将在本书中使用许多电子零件。在每章的开头,我会列出您在该章节示例中需要的具体零件和数量。除了列出的零件外,还需要一个电子面包板和一些杜邦线/跳线。

为了方便起见,本书中使用的所有电子零件的目录表,它们被使用的章节,以及您需要的最小数量如下。如果您是新手购买电子零件,您还会在表格后找到一些提示,以帮助您开始:

零件名称 最小数量 描述/注释 用于章节
红色 LED 2 * 5mm 红色 LED。不同颜色的 LED 可能具有不同的电气特性。本书中的大多数示例将假定为红色 LED。 2, 3, 4, 5, 6, 7, 9, 12, 13
15Ω电阻 2 * 颜色带(4 带电阻)将是棕色、绿色、黑色、银色/金色 8
200Ω电阻 2 * 颜色带(4 带电阻)将是红色、黑色、棕色、银色/金色 2, 3, 4, 5, 6, 8, 9, 12, 13
1kΩ电阻 2 * 颜色带(4 带电阻)将是棕色、棕色、红色、银色/金色 6, 7, 9, 8, 11
2kΩ电阻 2 * 颜色带(4 带电阻)将是红色、黑色、红色、银色/金色 6, 11
10kΩ 电阻 1 * 色带(4 带电阻)将是棕色,黑色,橙色,银色/金色 9, 13
51kΩ 电阻 1 * 色带(4 带电阻)将是绿色,棕色,橙色,银色/金色 6
100kΩ 电阻 1 * 色带(4 带电阻)将是棕色,黑色,黄色,银色/金色 7, 8, 9
瞬时按钮开关 1 要找到一个面包板友好的按钮开关,可以尝试搜索大的触觉开关 1, 6, 12
10kΩ 线性 电位计 2 用手指调节的较大电位计在书中的示例中更容易使用,而小电位计需要用螺丝刀调节。确保你有线性电位计(不是对数的)。 5, 6, 12
2N7000 MOSFET 1 * 这是一个逻辑电平兼容的 MOSFET 晶体管。 7, 8
FQP30N06L 功率 MOSFET 1 * 可选。购买时,请确保零件号以 L 结尾,表示它是逻辑电平兼容的 MOSFET(否则,它将无法可靠地工作你的树莓派)。 7
PC817 光耦 1 * 也被称为光隔离器。 7
SDR-5VDC-SL-C 继电器 1 这些继电器非常受欢迎,很容易找到;但是它们不适合面包板。你需要给它们焊接端子或导线,这样你就可以把它们插入你的面包板。 7
1N4001 二极管 1 * 我们将使用二极管作为反冲电压抑制二极管,以保护其他电子元件免受电压峰值的影响。 7, 8
尺寸 R130 5 伏直流业余电机 2 尺寸 R130 只是一个建议。我们需要的是 5 伏兼容的直流电机,最好的是空载电流小于 800 毫安。虽然这些电机在拍卖网站上很容易找到,但它们的电流和工作电流可能记录不完整,所以你得碰运气。第七章,打开和关闭东西,将带你完成一个练习,测量你的电机的工作电流。 7, 10
RGBLED,共阳极类型 1 * 这是一种可以产生不同颜色的 LED。 8
无源蜂鸣器 1 一个可以在 5 伏时工作的无源蜂鸣器。 8
SSD1306 OLED 显示屏 1 这是一个小型的单色像素显示屏。 8
APA102 RGBLED 灯条 1 这是一条可寻址的 APA102 RGBLED 灯条。你只需要 LED 灯条,不需要为我们的练习购买电源或遥控器。请注意确保购买的是 APA102 LED,因为有不同(不兼容)类型的可寻址 LED 可用。 8, 14
DHT11 或 DHT22 温湿度传感器 1 DHT11 和 DHT22 是可以互换的。DHT22 稍微贵一些,但提供更高的精度,并且可以测量零下的温度。 9, 13
LDR 1 * 光敏电阻 9
MG90S 业余舵机 1 这只是一个建议。任何带有 3 根线(+,GND,信号)的 5 伏特业余舵机都应该合适。 10, 14
L293D H 桥 IC 1 * 确保你购买的零件号以 D 结尾,表示 IC 包括嵌入式反冲电压抑制二极管。 10
28BYJ-48 步进电机 1 确保购买 5 伏的步进电机,齿轮比为 1:64。 10
HC-SR501 PIR 传感器 1 PIR 传感器可以检测运动。它是通过热来工作的,所以可以检测到人和动物的存在。 11
HC-SR04 超声波距离传感器 1 超声波距离传感器利用声波估算距离。 11
A3144 霍尔效应传感器 1 * 这是一种非锁定开关型霍尔效应传感器,它在磁场存在时打开。 11
AH3503 霍尔效应传感器 1 * 这是一种比例式霍尔效应传感器,可以检测相对于磁场的接近程度。 11
磁铁 1 需要一个小磁铁用于霍尔效应传感器。 11
ADS1115 模拟-数字(ADC)转换器模块 1 这个模块将允许我们将模拟元件与树莓派进行接口。 5, 9, 12
逻辑电平转换器/转换器模块 1 这个模块将允许我们将 5 伏电气元件与树莓派进行接口。搜索逻辑电平转换器/转换器模块,并在 4 或 8 通道时寻找双向(首选)模块。 6, 8, 14
面包板 1 我们所有的电子示例都将建立在面包板上。我建议购买两个全尺寸的面包板并将它们连接在一起 - 更多的面包板工作区将使电路建立更容易。 2 - 14
杜邦/跳线电缆 3 套* 这些电缆用于在面包板上连接元件。我建议购买公对公,公对母和母对母类型的套装。 2 - 14
树莓派 GPIO 面包板转接板 1 这是可选的,但它将使你更容易地将树莓派的 GPIO 引脚与面包板进行接口。 2 - 14
数字万用表 1 作为指南,价格在 30-50 美元的数字万用表应该是完全合适的。避免使用最低廉和最便宜的万用表。 6, 7
外部电源供应 2 本书中的一些电路将需要比我们期望的树莓派提供更多的电源。作为最小来源,一个能够输出 1 安培的 3.3/5 伏面包板兼容电源供应将是合适的。你可能还想研究实验室电源供应作为更有能力和通用的替代方案。 7, 8, 9, 10, 14
焊接铁和焊锡 1 有时你需要把导线和端子焊接到元件上 - 例如,你很可能需要把端子焊接到你购买的 ADS1115 和逻辑电平转换器/转移模块上。你还需要把端子或导线焊接到你的 SDR-5VDC-SL-C 继电器上,这样你就可以把它插入面包板。

*建议备用。这些是如果连接或供电不正确或在使用中可能会发生物理损坏(例如,腿断裂)的元件。

这些零件之所以被选中,是因为它们的低价格和它们在 eBay.com、Bangood.com、AliExpress.com 和电子零售商等网站上的普遍可用性。

在购买之前,请考虑以下事项:

  • 最小数量列是你在本书中需要的练习数量,但强烈建议你购买备用零件,特别是 LED、电阻和 MOSFET,因为这些元件很容易损坏。

  • 你会发现许多零件需要大量购买。

  • 搜索电子元件入门套件,并将其包含的内容与表中列出的零件进行比较。你可能可以在单个(并且打折)交易中购买许多零件。

  • 许多可用的即插即用的传感器模块入门套件大部分情况下都不适用于本书中所介绍的电路和代码练习。我们的电子和代码示例的深度意味着我们需要使用核心电气元件。然而,在完成本书后,你将能够理解这些即插即用传感器模块是如何构建和工作的!

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

下载示例代码文件

你可以从你在www.packt.com的账户中下载本书的示例代码文件。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,让文件直接发送到你的邮箱。

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

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

  2. 选择支持选项卡。

  3. 单击“代码下载”。

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

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

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

该书的代码包也托管在 GitHub 上,网址是github.com/PacktPublishing/Practical-Python-Programming-for-IoT。如果代码有更新,将在现有的 GitHub 存储库上更新。

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

代码演示

本书的代码演示视频可以在bit.ly/316OvNu上观看。

下载彩色图像

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。您可以在这里下载:static.packt-cdn.com/downloads/9781838982461_ColorImages.pdf

使用的约定

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

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。例如:"让我们使用gpio_pkg_check.pypip来检查 GPIO 包的可用性。"

代码块设置如下:

# Global Variables ...  BROKER_HOST = "localhost"   # (2) BROKER_PORT = 1883 CLIENT_ID = "LEDClient" # (3) TOPIC = "led" # (4) client = None # MQTT client instance. See init_mqtt()   # (5) ...

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

# Global Variables ...  **BROKER_HOST = "localhost"**   # (2) BROKER_PORT = 1883 CLIENT_ID = "LEDClient" # (3) TOPIC = "led" # (4) client = None # MQTT client instance. See init_mqtt()   # (5) ...

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

$ python --version
Python 2.7.16

粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词会以这种方式出现在文本中。例如:"从您的 Raspbian 桌面,导航到 Raspberry 菜单|首选项|Raspberry Pi Configuration。"

警告或重要提示会出现在这样的样式中。提示和技巧会出现在这样的样式中。

第一部分:使用 Python 和树莓派进行编程

在我们旅程的第一部分中,我们的主要重点将放在物联网的互联网部分。

我们将首先学习如何正确设置您的 Python 开发环境,然后探索并使用 Python 玩各种网络技术,构建网络和互联网连接的服务和应用程序。我们还将创建简单的网络用户界面,与我们将要学习的技术和示例一起使用。

然而,我相信如果您正在阅读这本书,您渴望立即开始,了解并玩电子设备,并开始构建和修补。我知道我会!因此,第二章,使用 Python 和物联网开始,致力于从零开始构建一个简单的互联网物联网项目-包括电子设备-以便我们在以后的章节中有一个参考示例(和一些东西可以修补!)。

让我们开始吧!

这一部分包括以下章节:

  • 第一章,设置您的开发环境

  • 第二章,使用 Python 和物联网开始

  • 第三章,使用 Flask 进行 RESTful API 和 Web 套接字网络连接

  • 第四章,使用 MQTT、Python 和 Mosquitto MQTT 代理进行网络连接

第一章:设置您的开发环境

Python 编程的一个重要但经常被忽视的方面是如何正确设置和维护 Python 项目及其运行时环境。它经常被忽视,因为它对于 Python 生态系统来说是一个可选的步骤。虽然这对于学习 Python 语言基础知识可能没问题,但对于需要维护独立的代码库和依赖项以确保项目不会相互干扰的更复杂的项目来说,它可能会很快成为一个问题,或者更糟糕的是,像我们将讨论的那样,破坏操作系统工具和实用程序。

因此,在后面的章节中,当我们跳入IoT代码和示例时,非常重要的是我们覆盖设置 Python 项目及其运行时环境所需的步骤。

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

  • 了解您的 Python 安装

  • 设置 Python 虚拟环境

  • 使用pip安装 Python GPIO 包

  • 执行 Python 脚本的替代方法

  • 树莓派 GPIO 接口配置

技术要求

要执行本章的实践练习,您需要以下内容:

  • 树莓派 4 型 B

  • Raspbian OS Buster(带桌面和推荐软件)

  • 最低 Python 版本 3.5

这些要求是本书中的代码示例所基于的。可以合理地期望代码示例应该在不经修改的情况下在树莓派 3 型 B 或不同版本的 Raspbian OS 上工作,只要您的 Python 版本是 3.5 或更高。

本书的完整源代码可以在 GitHub 上找到,网址如下:github.com/PacktPublishing/Practical-Python-Programming-for-IoT。当我们来到设置 Python 虚拟环境部分时,我们将很快克隆这个存储库。

了解您的 Python 安装

在本节中,我们将找出您的树莓派上安装了哪些 Python 版本。正如我们将发现的那样,在 Raspbian OS 上预装了两个版本的 Python。基于 Unix 的操作系统(如 Raspbian OS)通常预装了 Python 2 和 3,因为有使用 Python 构建的操作系统级实用程序。

要找出您的树莓派上安装了哪些 Python 版本,请按照以下步骤操作:

  1. 打开一个新的终端并执行python --version命令:
$ python --version
Python 2.7.16

在我的例子中,我们看到 Python 版本 2.7.16 已安装。

  1. 接下来,运行python3 --version命令:
$ python3 --version
Python 3.7.3

在我的例子中,我们看到已安装的第二个 Python 版本(即python3,带有3)是版本 3.7.3。

如果次要版本(2 后面的.7.16 和 3 后面的.7.3)不相同,不要担心;重要的是主要版本 2 和 3。Python 2 是 Python 的旧版本,而 Python 3 是当前支持的版本,是写作时的 Python 版本。当我们开始新的 Python 开发时,我们几乎总是使用 Python 3,除非有我们需要处理的旧问题。

Python 2 在 2020 年 1 月正式终止生命周期。它不再得到维护,也不会再接收任何进一步的增强、错误修复或安全补丁。

如果您是一位经验丰富的 Python 程序员,您可能能够辨别脚本是为 Python 2 还是 3 编写的,但仅仅通过查看一小段代码通常并不明显。许多刚接触 Python 的开发人员在混合使用为不同 Python 版本编写的程序和代码片段时会感到沮丧。请记住,未经修改的 Python 2 代码不能保证与 Python 3 向上兼容。

一个快速提示我可以分享的是,要确定一个代码片段是为哪个 Python 版本编写的(如果程序员在代码注释中没有明确说明),可以查找print语句。

如果您看下面的例子,您会发现有两个print语句。没有括号的第一个print语句表明它只能在 Python 2 中使用:

print "Hello"  # No parentheses - This only works in Python 2, a dead give-away that this script is for Python 2.

print("Hello") # With parentheses - this will work in Python 2 and Python 3

当然,您可以始终针对 Python 2 和 3 运行代码,看看会发生什么。

我们现在已经看到 Raspbian OS 默认提供了两个 Python 版本,并提到有一些以 Python 编写的系统级实用程序依赖于这些版本。作为 Python 开发人员,我们必须小心,不要破坏全局 Python 安装,因为这可能会破坏系统级实用程序。

我们现在将把注意力转向一个非常重要的 Python 概念,即 Python 虚拟环境,这是我们将自己的 Python 项目与全局安装隔离或沙箱的方式。

设置 Python 虚拟环境

在本节中,我们将讨论 Python 如何与您的操作系统安装进行交互,并介绍设置和配置 Python 开发环境所需的步骤。此外,作为设置过程的一部分,我们将克隆包含本书所有代码(按章节组织)的 GitHub 存储库。

默认情况下,Python 及其包管理工具pip在系统级全局操作,并且可能会对 Python 初学者造成一些困惑,因为这种全局默认与许多其他语言生态系统形成对比,后者默认在项目文件夹级别本地操作。不经意地在全局 Python 环境中工作和进行更改可能会破坏基于 Python 的系统级工具,并且解决这种情况可能会成为一个主要的头痛。

作为 Python 开发人员,我们使用 Python 虚拟环境来隔离我们的 Python 项目,以便它们不会对系统级 Python 实用程序或其他 Python 项目造成不利影响。

在本书中,我们将使用一个名为venv的虚拟环境工具,它作为 Python 3.3 及以上版本的内置模块捆绑提供。还有其他虚拟环境工具可供选择,它们各自具有相对的优势和劣势,但它们都共享一个共同的目标,即将 Python 依赖项隔离到一个项目中。

virtualenvpipenv是两种替代的虚拟环境工具选项,它们提供比venv更多的功能。这些替代方案非常适合复杂的 Python 项目和部署。您可以在本章末尾的进一步阅读部分找到这些链接。

让我们开始克隆 GitHub 存储库并为本章的源代码创建一个新的 Python 虚拟环境。打开一个新的终端窗口,并按照以下步骤操作:

  1. 切换到或创建一个您想要存储本书源代码的文件夹,并执行以下命令。使用最后一个命令,我们将克隆的文件夹重命名为pyiot。这样做是为了帮助缩短本书中的终端命令示例:
$ cd ~
$ git clone https://github.com/PacktPublishing/Practical-Python-Programming-for-IoT
$ mv Practical-Python-Programming-for-IoT pyiot 
  1. 接下来,切换到包含与本章相关的代码的chapter01文件夹:
$ cd ~/pyiot/chapter01
  1. 执行以下命令,使用venv工具创建一个新的 Python 虚拟环境。重要的是您要输入python3(带有 3),并记住venv仅适用于 Python 3.3 及以上版本:
$ python3 -m venv venv

我们传递给python3的选项包括-m venv,它告诉 Python 解释器我们要运行名为venv的模块。venv参数是您的虚拟环境将被创建的文件夹的名称。

虽然在前面的命令中乍一看可能会让人困惑,但将虚拟环境的文件夹命名为venv是一种常见的约定。本章的虚拟环境解剖部分中,我们将探讨刚刚创建的venv文件夹下面的内容。

  1. 要使用 Python 虚拟环境,我们必须激活它,这可以通过activate命令完成:
# From with in the folder ~/pyiot/chapter01
$ source venv/bin/activate
(venv) $

当您的终端激活了 Python 虚拟环境时,所有与 Python 相关的活动都将被隔离到您的虚拟环境中。

请注意,在激活后,虚拟环境的名称venv会显示在终端提示文本中,即(venv) $。在本书中,每当您看到终端示例中的提示为(venv) $时,这是一个提醒,需要从激活的 Python 虚拟环境中执行命令。

  1. 接下来,在您的终端中执行which python(不带3),请注意 Python 可执行文件的位置位于您的venv文件夹下,如果您检查 Python 的版本,它是 Python 版本 3:
(venv) $ which python
/home/pi/pyiot/chapter01/venv/bin/python

(venv) $ python --version
Python 3.7.3
  1. 要离开激活的虚拟环境,请使用deactivate命令,如下所示:
(venv) $ deactivate
$ 

还要注意,一旦虚拟环境被停用,终端提示文本中的(venv) $将不再存在。

记住要输入deactivate来离开虚拟环境,而不是exit。如果您在虚拟环境中输入exit,它将退出终端。

  1. 最后,现在您已经退出了我们的 Python 虚拟环境,如果您执行which python(不带3)和python --version,请注意我们又回到了默认的系统级 Python 解释器,即版本 2:
$ which python
/usr/bin/python

$ python --version
Python 2.7.13

正如我们在前面的示例中所演示的,当我们在激活的虚拟环境中运行python --version时,我们看到它是 Python 版本 3,而在上一章的最后一个示例中,系统级的python --version是版本 2,我们需要输入python3 --version来获取版本 3。在实践中,python(没有数字)与 Python 的默认版本相关联。全局来看,这是版本 2。在您的虚拟环境中,我们只有一个版本的 Python,即版本 3,因此它成为默认版本。

使用venv创建的虚拟环境继承(通过符号链接)调用它的全局 Python 解释器版本(在我们的情况下,版本 3,因为命令是python3 -m venv venv)。如果您需要针对与全局版本不同的特定 Python 版本,可以使用virtualenvpipenv虚拟环境替代方案。

我们已经看到如何创建、激活和停用 Python 虚拟环境,以及为什么使用虚拟环境来隔离 Python 项目是很重要的。这种隔离意味着我们可以将自己的 Python 项目及其库依赖与其他项目隔离开来,这样可以防止我们潜在地破坏系统级安装的 Python 并破坏依赖于它们的系统级工具和实用程序。

接下来,我们将看到如何在虚拟环境中使用pip来安装和管理 Python 包。

使用 pip 安装 Python GPIO 包

在本节中,我们将学习如何在您在上一节中创建和探索的 Python 虚拟环境中安装和管理 Python 。Python (或者如果您更喜欢这个术语,)允许我们通过新功能和功能扩展核心 Python 语言。

在本书中,我们需要安装许多不同的包,但是作为入门,为了探索和学习与包安装和管理相关的基本概念,我们将在本节中安装两个常见的与 GPIO 相关的包,这些包将在本书中使用。这两个包如下:

  • GPIOZero库,一个入门级且易于使用的 GPIO 库,用于控制简单的电子设备

  • PiGPIO库,一个高级 GPIO 库,具有许多功能,用于更复杂的电子接口

在 Python 生态系统中,包管理是通过pip命令进行的(pip代表Python installs packages)。pip查询的官方公共包存储库称为Python Package Index,简称PyPi,可以在网上浏览pypi.org.

类似于pythonpython3,有pippip3pip(没有数字)将是与给定虚拟环境中的默认python命令匹配的默认pip命令。

在本书中会有一些代码示例,我们将与树莓派的 GPIO 引脚进行交互,因此我们需要安装一个或两个 Python 包,以便你的 Python 代码可以与树莓派的 GPIO 引脚一起工作。现在,我们只是要检查并安装两个与 GPIO 相关的包。在第二章和第五章中,我们将更详细地介绍这些 GPIO 包和其他替代方案。

在你的chapter01源代码文件夹中,你会找到一个名为gpio_pkg_check.py的文件,下面是它的内容。我们将使用这个文件作为学习pip和 Python 虚拟环境中包管理的基础。这个脚本根据import是否成功或引发异常来报告 Python 包的可用性:

"""
Source File: chapter01/gpio_pkg_check.py
"""
try:
 import gpiozero
 print('GPIOZero Available')
except:
 print('GPIOZero Unavailable. Install with "pip install gpiozero"')

try:
 import pigpio
 print('pigpio Available')
except:
 print('pigpio Unavailable. Install with "pip install pigpio"')

使用gpio_pkg_check.pypip检查 GPIO 包的可用性。我要打破悬念告诉你,它们还不能在你新建的虚拟环境中使用,但是我们将安装它们!

注意:如果你想在虚拟环境之外运行这个脚本来检查,这些包已经在系统级别安装好了。

接下来的步骤将引导我们升级pip,探索工具的选项,并安装包:

  1. 作为第一步,我们将升级pip工具。在终端窗口中,运行以下命令,记住后续的所有命令都必须在激活的虚拟环境中执行——这意味着你应该在终端提示符中看到(venv)文本:
(venv) $ pip install --upgrade pip
...output truncated...

上面的upgrade命令可能需要一两分钟才能完成,并且可能会在终端上输出大量文本。

遇到pip的问题了吗?如果在使用pip安装包时出现大量红色错误和异常,尝试首先升级pip版本,使用pip install --upgrade pip。这是在创建新的 Python 虚拟环境后推荐的第一步。

  1. 现在pip已经升级,我们可以使用pip list命令来查看虚拟环境中已经安装的 Python 包:
(venv) $ pip list
pip (9.0.1)
pkg-resources (0.0.0)
setuptools (33.1.1)

在上面我们看到的是我们新建虚拟环境中的默认 Python 包。如果确切的包列表或版本号与示例不完全匹配,不要担心。

  1. 使用python gpio_pkg_check.py命令运行我们的 Python 脚本,并观察到我们的 GPIO 包没有安装:
(venv) $ python gpio_pkg_check.py
GPIOZero Unavailable. Install with "pip install gpiozero"
pigpio Unavailable. Install with "pip install pigpio"
  1. 为了安装我们需要的两个 GPIO 包,我们可以使用pip install命令,如下例所示:
(venv) $ pip install gpiozero pigpio
Collecting gpiozero...
... output truncated ...
  1. 现在,再次运行pip list命令;我们将看到这些新包现在已经安装在我们的虚拟环境中:
(venv) $ pip list
colorzero (1.1)
gpiozero (1.5.0)   # GPIOZero
pigpio (1.42)      # PiGPIO
pip (9.0.1)
pkg-resources (0.0.0)
setuptools (33.1.1)

你可能已经注意到有一个叫做colorzero的包(这是一个颜色处理库)我们没有安装。gpiozero(版本 1.5.0)依赖于colorzero,所以pip已经自动为我们安装了它。

  1. 重新运行python gpio_pkg_check.py,现在我们看到我们的 Python 模块可以被导入了:
(venv) $ python gpio_pkg_check.py
GPIOZero Available
pigpio Available

太好了!我们现在有了一个安装了两个 GPIO 包的虚拟环境。在你进行 Python 项目时,你将不可避免地安装更多的包并希望跟踪它们。

  1. 使用pip freeze命令对你之前安装的包进行快照:
(venv) $ pip freeze > requirements.txt

上面的例子将所有安装的包冻结到一个名为requirements.txt的文件中,这是一个常用的文件名。

  1. 查看requirements.txt文件,你会看到所有的 Python 包及其版本号一起列出:
(venv) $ cat requirements.txt
colorzero==1.1
gpiozero==1.5.0
pigpio==1.42
pkg-resources==0.0.0

将来,如果您将 Python 项目移动到另一台机器或新的虚拟环境,您可以使用requirement.txt文件使用pip install -r requirements.txt命令一次性安装所有捕获的软件包。

我们的requirements.txt示例显示我们已安装了 GPIOZero 版本 1.5.0,这是写作时的当前版本。该版本依赖于 ColorZero 版本 1.1。可能不同(过去或将来)版本的 GPIOZero 可能具有与我们示例中所示的不同的依赖关系,因此在执行示例练习时,您自己的requirements.txt文件可能会有所不同。

我们现在已经完成了使用pip进行 Python 软件包的基本安装生命周期。请注意,每当您使用pip install安装新软件包时,您还需要重新运行pip freeze > requirements.txt来捕获新软件包及其依赖关系。

为了完成我们对pip和软件包管理的探索,这里还有一些其他常见的pip命令:

# Remove a package
(venv) $ pip uninstall <package name>

# Search PyPi for a package (or point your web browser at https://pypi.org)
(venv) $ pip search <query text>

# See all pip commands and options (also see Further Reading at the end of the chapter).
(venv) $ pip --help

恭喜!我们已经达到了一个里程碑,并介绍了您可以用于任何 Python 项目的基本虚拟环境原则,即使这些项目与树莓派无关!

在您的 Python 之旅中,您还会遇到其他名为easy_installsetuptools的软件包安装程序和工具。两者都有其用途;但是,大多数情况下您会依赖pip

现在我们已经看到了如何创建虚拟环境和安装软件包,让我们来看一下典型的 Python 项目文件夹结构,比如~/pyiot/chapter01,并发现venv文件夹下面有什么。

虚拟环境的解剖

本节涉及venv,这是本章中我们一直在使用的,也适用于virtualenv但不适用于我们列出的替代虚拟环境工具pipenv。该示例也特定于 Raspbian 操作系统,并且是标准 Unix 操作系统的典型情况。至少要了解虚拟环境部署的基本结构,因为我们将把自己的 Python 编程代码与组成虚拟环境的文件和文件夹混合在一起。

轻量级的venv工具随 Python 3.3 及以上版本一起提供,是virtualenv的一个子集。

这是我们虚拟环境的文件夹结构。是的,这是 Mac 上的屏幕截图。这样我就可以一次性将所有内容显示在屏幕上:

图 1.1 - 典型venv虚拟环境文件夹的内容

以下几点解释了我们在运行python3 -m venv venv并使用pip安装软件包后,在~/pyiot/chapter01文件夹中找到的核心子文件夹:

  • venv文件夹包含所有 Python 虚拟环境文件。实际上没有必要手动触摸该文件夹下的任何内容 - 让工具为您完成。请记住,该文件夹之所以被命名为venv,只是因为在创建时我们这样称呼它。

  • venv/bin文件夹包含 Python 解释器(在venv情况下,有符号链接到系统解释器)和其他核心 Python 工具,包括pip

  • venv/lib文件夹下面是虚拟环境的所有隔离的 Python 软件包,包括我们使用pip install安装的 GPIOZero 和 PiGPIO 软件包。

  • 我们的 Python 源文件gpio_pkg_check.py位于顶级文件夹~/pyiot/chapter01中,但是您可以在这里创建子文件夹来帮助组织您的代码和非代码文件。

  • 最后,requirements.txt按照惯例存放在顶级项目文件夹中。

虚拟环境文件夹venv实际上不需要放在项目文件夹中;但是,通常将其放在那里以便使用activate命令进行激活。

您的venv文件夹及其下的任何内容不应添加到源版本控制系统中,但您应该添加requirements.txt。只要有一个当前的requirements.txt文件,您就可以随时重新创建您的虚拟环境并将软件包恢复到已知状态。

重要的是要理解,作为 Python 开发人员,您将在自己的编程代码中混合虚拟环境系统的文件和文件夹,并且在选择添加到您的版本控制系统中的文件和文件夹时应该是务实的,如果您正在使用版本控制系统。

最后一点很重要,因为虚拟环境系统的大小可能会达到几兆字节(通常比您的程序代码大几倍),不需要进行版本控制(因为只要有requirements.txt文件,我们就可以随时重新创建虚拟环境),而且它是特定于主机平台的(也就是说,在 Windows、Mac 和 Linux 之间会有差异),不同的虚拟环境工具之间也会有差异(例如venvpipenv)。因此,在涉及许多开发人员在不同计算机上工作的项目中,虚拟环境通常不具备可移植性。

现在我们已经简要探讨了文件和文件夹的结构以及理解这种结构的重要性,我们将继续并查看运行沙盒化到虚拟环境的脚本的替代方法。

执行 Python 脚本的替代方法

让我们简要关注一下我们可以执行 Python 脚本的替代方法。正如我们将要了解的那样,选择适当的方法完全取决于您打算从何处启动脚本以及您的代码是否需要提升的权限。

运行 Python 脚本的最常见方法是在其虚拟环境中以当前登录用户的权限运行。但是,会有一些情况需要以 root 用户身份运行脚本或者在未激活的虚拟环境之外运行脚本。

以下是我们将要探索的方法:

  • 在虚拟环境中使用sudo

  • 在其虚拟环境之外执行 Python 脚本

  • 在启动时运行 Python 脚本

让我们从学习如何以 root 用户权限运行 Python 脚本开始。

在虚拟环境中使用 sudo

我相信在树莓派上工作时,您必须在终端中使用sudo前缀来执行命令,因为它们需要 root 权限。如果您需要以 root 身份运行虚拟环境中的 Python 脚本,您必须使用虚拟环境的 Python 解释器的完整路径。

在大多数情况下,即使我们在虚拟环境中,也不能简单地在python之前加上sudo,如下例所示。sudo操作将使用根用户可用的默认 Python,如示例的后半部分所示:

# Won't work as you might expect!
(venv) $ sudo python my_script.py

# Here is what the root user uses as 'python' (which is actually Python version 2).
(venv) $ sudo which python
/usr/bin/python

以 root 身份运行脚本的正确方法是传递虚拟环境的 Python 解释器的绝对路径。我们可以使用which python命令在激活的虚拟环境中找到绝对路径:

(venv) $ which python
/home/pi/pyiot/chapter01/venv/bin/python

现在,我们使用sudo来运行虚拟环境的 Python 解释器,脚本将作为 root 用户运行,并在我们的虚拟环境中运行:

(venv) $ sudo /home/pi/pyiot/chapter01/venv/bin/python my_script.py

接下来,我们将看到如何从虚拟环境之外运行沙盒化到虚拟环境的 Python 脚本。

在其虚拟环境之外执行 Python 脚本

sudo的前面讨论的自然延伸是*如何从虚拟环境之外运行 Python 脚本?*答案与前一节相同:只需确保您使用虚拟环境的 Python 解释器的绝对路径。

注意:在以下两个示例中,我们不在虚拟环境中——提示符上没有$ (venv)。如果您仍然需要退出 Python 虚拟环境,请键入deactivate

以下命令将以当前登录用户的身份运行脚本(默认为pi用户):

# Run script as logged-in user.
$ /home/pi/pyiot/chapter01/venv/bin/python gpio_pkg_check.py

或者以 root 身份运行脚本,加上sudo前缀:

# Run script as root user by prefixing sudo
$ sudo /home/pi/pyiot/chapter01/venv/bin/python gpio_pkg_check.py

由于我们使用虚拟环境的 Python 解释器,我们仍然被限制在我们的虚拟环境中,并且我们安装的任何 Python 包都是可用的。

接下来,我们将学习如何使 Python 脚本在您的树莓派启动时运行。

在启动时运行 Python 脚本

总有一天,当您开发出一个令人惊叹的 IoT 项目,并且希望它在每次启动树莓派时自动运行时,您可以使用cron的一个功能来实现这一点。如果您不熟悉cron的基础知识,请在网上搜索 cron 教程,您会找到很多。我在进一步阅读部分提供了精选链接。

以下是配置 cron 并在启动时运行脚本的步骤:

  1. 在您的项目文件夹中,创建一个 bash 脚本。我将其命名为run_on_boot.sh
#!/bin/bash

# Absolute path to virtual environment python interpreter
PYTHON=/home/pi/pyiot/chapter01/venv/bin/python

# Absolute path to Python script
SCRIPT=/home/pi/pyiot/chapter01/gpio_pkg_check.py

# Absolute path to output log file
LOG=/home/pi/pyiot/chapter01/gpio_pkg_check.log

echo -e "\n####### STARTUP $(date) ######\n" >> $LOG
$PYTHON $SCRIPT >> $LOG 2>&1

这个 bash 脚本将使用脚本和其 Python 解释器的绝对路径运行 Python 脚本。此外,它会捕获任何脚本输出并将其存储在日志文件中。在这个示例中,我们将简单地在启动时运行和记录gpio_pkg_check.py的输出。最后一行将所有内容联系在一起并运行和记录我们的 Python 脚本。结尾的2>&1部分是必要的,以确保错误和标准输出都被记录。

  1. run_on_boot.sh文件标记为可执行文件:
$ chmod u+x run_on_boot.sh

如果您不熟悉chmod命令(chmod表示更改模式),我们正在给操作系统权限来执行run_on_boot.sh文件。u+x参数表示对当前**用户,使文件可执行。要了解更多关于chmod的信息,您可以在终端中输入chmod --helpman chmod

  1. 编辑您的crontab文件,这是存储cron调度规则的文件:
$ crontab -e
  1. 将以下条目添加到您的crontab文件中,使用在步骤 1中创建的run_on_boot.sh bash 脚本的绝对路径:
@reboot /home/pi/pyiot/chapter01/run_on_boot.sh &

不要忘记在行尾加上&字符。这样可以确保脚本在后台运行。

  1. 在终端中手动运行run_on_boot.sh文件,以确保它可以正常工作。gpio_pkg_check.log文件应该被创建,并包含 Python 脚本的输出:
$ ./run_on_boot.sh
$ cat gpio_pkg_check.log
####### STARTUP Fri 13 Sep 2019 03:59:58 PM AEST ######
GPIOZero Available
PiGPIO Available
  1. 重新启动您的树莓派:
$ sudo reboot
  1. 一旦您的树莓派完成重新启动,gpio_pkg_check.log文件现在应该包含额外的行,表明脚本确实在启动时运行:
$ cd ~/pyiot/chapter01
$ cat gpio_pkg_check.log

####### STARTUP Fri 13 Sep 2019 03:59:58 PM AEST ######

GPIOZero Available
PiGPIO Available

####### STARTUP Fri 13 Sep 2019 04:06:12 PM AEST ######

GPIOZero Available
PiGPIO Available

如果重新启动后在gpio_pkg_check.log文件中看不到额外的输出,请仔细检查您在crontab中输入的绝对路径是否正确,并且按照步骤 5手动运行。还要查看系统日志文件/var/log/syslog,并搜索文本run_on_boot.sh

我们基于 cron 的在启动时运行脚本的示例是 Unix 操作系统(如 Raspbian)中的众多选项之一。另一个常见且更高级的选项是使用systemd,可以在树莓派网站上找到www.raspberrypi.org/documentation/linux/usage/systemd.md。无论您喜欢哪种选项,要记住的关键点是确保您的 Python 脚本在其虚拟环境中运行。

我们现在已经学会了运行 Python 脚本的替代方法,这将帮助您在将来正确地运行开发完成的基于 Python 的 IoT 项目,或者在需要时在树莓派启动时启动它们。

接下来,我们将确保您的树莓派已正确设置和配置 GPIO 和电子接口,以便在下一章节第二章中进行深入讨论,使用 Python 和 IoT 入门,以及后续章节。

配置我们的树莓派上的 GPIO 接口

在我们开始使用 Python GPIO 库和控制电子设备之前,我们需要执行的一个任务是在您的 Raspberry Pi 上启用 GPIO 接口。尽管我们已经为 GPIO 控制安装了 Python 包,但我们还没有告诉 Raspbian OS 我们想要在特定情况下使用树莓派的 GPIO 引脚。现在让我们来做这件事。

以下是要遵循的步骤:

  1. 从您的 Raspbian 桌面,导航到 Raspberry 菜单|首选项|Raspberry Pi 配置,如图 1.2所示:

图 1.2 - Raspberry Pi 配置菜单项的位置或者,可以使用sudo raspi-config命令在命令行中管理接口,并导航到“接口选项”菜单。

  1. 按照以下截图中显示的方式启用所有接口:

图 1.3 - Raspberry Pi 配置对话框

  1. 点击“确定”按钮。

在您点击“确定”按钮后,您可能会被提示重新启动您的 Raspberry Pi;然而,不要立即确认重新启动,因为我们还有一个任务需要先执行。我们接下来会看到这个任务。

配置 PiGPIO 守护程序

我们还需要启动 PiGPIO 守护程序,这是一个系统服务,需要运行,以便我们可以使用 PiGPIO GPIO 客户端库,我们将在第二章中开始使用它,使用 Python 和物联网入门

从架构上讲,PiGPIO 库包括两部分——一个服务器服务和一个客户端,它们通过本地管道或套接字与服务通信。我们将在第五章中更多地介绍这个基本架构,将您的 Raspberry Pi 连接到物理世界

在终端中执行以下操作。这将启动 PiGPIO 守护程序,并确保当您的 Raspberry Pi 启动时,PiGPIO 守护程序会自动启动:

$ sudo systemctl enable pigpiod
$ sudo systemctl start pigpiod

现在,是时候重新启动您的 Raspberry Pi 了!所以,在您的 Raspberry Pi 重新启动时休息一下。您值得拥有这个休息,因为我们已经涵盖了很多内容!

总结

在本章中,我们探索了 Python 生态系统,这是典型的基于 Unix 的操作系统(如 Raspbian OS)的一部分,并了解到 Python 是操作系统工具的核心元素。然后,我们介绍了如何创建和导航 Python 虚拟环境,以便我们可以隔离我们的 Python 项目,使它们不会相互干扰或与系统级 Python 生态系统相互干扰。

接下来,我们学习了如何使用 Python 包管理工具pip在虚拟环境中安装和管理 Python 库依赖项,并通过安装 GPIOZero 和 PiGPIO 库来实现这一点。由于我们将需要在某些时候以根用户身份执行 Python 脚本,从其虚拟环境外部或在启动时,我们也介绍了这些各种技术。

默认情况下,Raspbian 没有启用所有的 GPIO 接口,因此我们进行了必要的配置以启用这些功能,以便它们在后续章节中可以随时使用。我们还启动并学习了如何设置 PiGPIO 守护程序服务,以便它在每次启动 Raspberry Pi 时都会启动。

本章中所获得的核心知识将帮助您正确设置和导航沙盒化的 Python 开发环境,用于您自己的物联网(IoT)(和非 IoT)项目,并安全地安装库依赖项,以便它们不会干扰您的其他 Python 项目或 Python 的系统级安装。您对执行 Python 程序的不同方式的理解也将帮助您以提升的用户权限(即作为根用户)或在启动时运行您的项目,如果您的项目有这些要求的话。

接下来,在第二章 Python 和物联网入门中,我们将直接进入 Python 和电子领域,并创建一个端到端的互联网程序,可以通过互联网控制 LED。在将 LED 连接到互联网之前,我们将使用 GPIOZero 和 PiGPIO GPIO 库的两种替代方法来闪烁 LED,然后通过在线服务dweet.io将 LED 连接到互联网作为我们的网络层。

问题

最后,这里是一些问题列表,供您测试对本章材料的了解。您将在书的评估部分找到答案:

  1. 为什么您应该始终为 Python 项目使用虚拟环境的主要原因是什么?

  2. 您是否需要或应该将虚拟环境文件夹(即venv)放入版本控制?

  3. 为什么要创建requirements.txt文件?

  4. 您需要以 root 用户身份运行 Python 脚本。为确保脚本在其预期的虚拟环境上执行,您必须采取哪些步骤?

  5. source venv/bin/activate命令是做什么的?

  6. 您处于已激活的虚拟环境中。离开虚拟环境并返回主机 shell 的命令是什么?

  7. 您在 PyCharm 中创建了一个 Python 项目和虚拟环境。您能在终端上处理和运行项目的 Python 脚本吗?

  8. 您想要一个 GUI 工具来编辑和测试树莓派上的 Python 代码,但没有安装 PyCharm。您可以使用 Python 和 Raspbian 预安装的工具。

  9. 您在 Python 和电子知识方面有所进步,并尝试使用 I2C 将设备连接到树莓派,但无法使其工作。可能的问题是什么,以及您如何解决它?

进一步阅读

我们在本章介绍了venv虚拟环境工具。以下是它的官方文档链接:

如果您想了解virtualenvpipenv替代虚拟环境工具,这里是它们的官方文档:

以下是Python 包装指南的链接。在这里,您将找到有关 Python 包管理的全面指南,包括pip和 easy-install/setup 工具的替代方法:

如果您希望了解更多关于调度和 cron 的知识,这里有两个资源供您开始:

第二章:开始使用 Python 和 IoT

在第一章中,设置您的开发环境,我们介绍了 Python 生态系统、虚拟环境和软件包管理的基本知识,并为您的树莓派进行了开发和 GPIO 接口设置。在本章中,我们将开始我们的 Python 和 IoT 之旅。

本章涵盖的内容将奠定基础,并为我们在后续章节中将要涵盖的更高级内容提供一个工作参考点。我们将学习如何使用按钮、电阻和 LED(或发光二极管)创建一个简单的电路,并探索使用 Python 与按钮和 LED 交互的替代方法。然后,我们将继续创建和讨论一个完整的端到端 IoT 程序,以控制 LED 通过互联网,并通过查看您可以扩展程序的方式来完成本章。

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

  • 创建一个面包板原型电路

  • 阅读电子原理图

  • 探索两种在 Python 中闪烁 LED 的方法

  • 探索两种在 Python 中集成按钮的方法

  • 创建您的第一个 IoT 程序

  • 扩展您的 IoT 程序

技术要求

为了完成本章和整本书的练习,您将需要以下内容:

  • 树莓派 4 型 B 型。1 GB RAM 版本足以运行我们的示例。如果您直接在树莓派上工作而不是通过 SSH 会话;例如,更多的 RAM 建议以改善 Raspbian 桌面体验和响应能力。

  • 您将需要 Raspbian OS Buster(带桌面和推荐软件)。

  • 您将需要至少 Python 版本 3.5。

这些要求是本书中代码示例的基础。可以合理地期望代码示例应该在不修改的情况下在树莓派 3 型 B、树莓派 Zero W 或不同版本的 Raspbian OS 上工作,只要您的 Python 版本是 3.5 或更高。

您将在以下 URL 提供的 GitHub 存储库中的chapter02文件夹中找到本章的源代码:github.com/PacktPublishing/Practical-Python-Programming-for-IoT

您需要在终端中执行以下命令来设置虚拟环境并安装本章代码所需的 Python 库:

$ cd chapter02              # Change into this chapter's folder
$ python3 -m venv venv      # Create Python Virtual Environment
$ source venv/bin/activate  # Activate Python Virtual Environment
(venv) $ pip install pip --upgrade        # Upgrade pip
(venv) $ pip install -r requirements.txt  # Install dependent packages

以下依赖项已从requirements.txt中安装:

我们将需要一些物理电子元件:

  • 1 x 5 毫米红色 LED

  • 1 x 200Ω电阻:其色带将是红色、黑色、棕色,然后是金色或银色

  • 瞬时按钮(单极单刀开关-SPST)

  • 一个面包板

  • 母对公和公对公跳线(有时称为杜邦线)

您将在前言中找到一个完整的零件清单,其中列出了每一章所需的所有电子元件。

当您准备好您的电子元件后,我们可以继续并在您的面包板上安排它们。

创建一个面包板原型电路

在本书中,我们将建立许多电路,并将使用电子面包板进行。在最初的章节中,我将使用类似于本节末尾所示的面包板布局以及如图 2.8 所示的原理图来呈现许多电路。

随着我们在本书中的进展,以及您在构建面包板电路方面的经验增加,我将停止对更简单电路的面包板布局;然而,对于更复杂的电路,我仍会呈现它们,这样您就有东西可以与您的构建进行比较。

请注意,以下电路示例和讨论仅为简要介绍。在本书的这个阶段,我们打算构建一个简单的电子电路,这将是本章和第三章《使用 Flask 进行 RESTful API 和 Web 套接字的网络》以及第四章《使用 MQTT,Python 和 Mosquitto MQTT Broker 进行网络》中 Python 示例的基础。我们将在第五章《将您的 Raspberry Pi 连接到物理世界》中详细讨论树莓派及其引脚编号。此外,我们将在第六章《软件工程师的电子学 101》中详细介绍电路和电子基础知识,其中我们将学习按钮如何在电气上与您的树莓派进行交互的原因,以及为什么 200Ω电阻器伴随我们的 LED。

让我们开始构建我们的第一个电路。我将逐步为您介绍面包板的构建过程,并在我们使用它们时简要讨论每个组件。我们将从讨论面包板是什么以及它是如何工作的开始。

理解面包板

电子面包板,如图 2.1所示,是一个原型板,可以帮助您快速轻松地进行电气连接组件和电线。在本节中,我们将讨论面包板的一般属性,以便在接下来的部分中连接组件和电线时做好准备:

图 2.1 - 面包板

面包板有许多不同的尺寸,我们插图中的面包板是半尺寸面包板。然而,无论它们的尺寸如何,它们的基本布局和电气连接方式是相似的,只有一个小例外,我稍后会提到。

真正的面包板可能有,也可能没有,行和列号码标记。它们已经包括在插图中,以帮助以下讨论和解释。

面包板上的是您放置电子元件和电线以进行电气连接的地方。孔的电气连接方式如下:

  • 面包板的两个外部列孔通常被称为电源轨。面包板的两侧分别有一个正(+)列和一个负(-)列。每一列孔都是电气连接的,并且贯穿整个面包板的长度。因此,这个面包板上有四个独立的电源轨:面包板左侧有一个*+-轨,右侧也有一个*+-轨。

电源轨经常用于帮助在面包板上向组件分配电源。请注意,它们本身不提供电源!它们需要连接到电源供应或电池等电源源才能提供电源。

  • 面包板的中间有两排孔,我标记为A-EF-J。每个银行的每一行孔都是电气连接的。例如,孔 A1 到 E1 是电气连接的,孔 F1 到 J1 也是如此。然而,为了清楚地理解,A1-E1 与 F1-J1 并是电气连接的,因为它们在不同的银行上。

当我们将它们连接到面包板时,我们跨越两个银行之间的间隙放置集成电路ICs)—通常称为芯片。当我们使用 IC 来控制电机时,我们将在第十章《使用舵机,电机和步进电机进行运动》中看到一个例子。

以下是一些关于孔是如何连接的更多示例,可以帮助你理解:

  • B5 与 C5 电上连接(它们共享同一行)。

  • H25 与 J25 电上连接(它们共享同一行)。

  • A2 与 B2电上连接(它们不共享同一行)。

  • E30 与 F30电上连接(它们位于不同的银行)。

  • 左侧电源轨道顶部的第三个+孔(从面包板顶部开始)与左侧电源轨道的最后一个+孔电上连接(它们位于同一垂直列)。

  • 左侧电源轨道顶部的第三个+孔(从面包板顶部开始)与右侧电源轨道的第三个+孔电上连接(它们位于不同的电源轨道上)。

我在本节开始时提到,所有的面包板基本上都是一样的,只有一个小小的例外。这个例外与电源轨道有关。一些全尺寸的面包板可能会将它们的电源轨道分成两个独立的垂直银行(因此,电上地,轨道中的垂直孔并不贯穿整个面包板的长度)。电源轨道被分割并不总是在视觉上明显,因此需要根据面包板逐个发现。我提到这一点只是为了防止在使用电源轨道时出现连接问题时,以防你使用的是全尺寸的面包板。

现在我们已经介绍了面包板,并且了解了孔是如何在电上相互关联的,让我们开始将元件和导线插入我们的面包板,以创建我们的第一个电路。我们将从按键开始。

定位和连接按键

我们使用的是一个简单的开关按钮,也被称为单极单刀SPST)瞬时开关。一个例子如图 2.2所示:

图 2.2 - 一个按键和原理图符号

图 2.2的左侧是一个瞬时按键的照片,右侧显示了瞬时按键的原理图符号。我们将在下一节看到这个符号,并讨论原理图中出现这些类型符号的地方。

按键有各种形状和大小;然而,它们的一般操作是相同的。左侧显示的这个特定按键被称为触觉按键。它们小巧,非常适合与面包板一起使用。

图 2.3说明了我们需要在面包板上创建的按键连接。请在按照接下来的步骤时参考这个图:

图 2.3 - 连接按键

以下是如何将按键连接到你的面包板并连接到你的树莓派。以下步骤编号与图 2.3中编号的黑色圆圈相匹配:

  1. 按照所示将按钮放在面包板上。按钮放入哪一行孔并不重要,但是图 2.3显示了按钮的位置(左上腿)在孔 B10 处。

  2. 接下来,将一根跳线插入与按键最顶端腿部相同的那一行(我们的插图使用孔 A10)。将这根导线的另一端连接到树莓派的 GPIO 引脚头部外缘向下数第八个引脚。这个引脚被称为 GPIO 23。

你可以获得引脚标签和面包板兼容模块,以帮助你进行树莓派引脚连接和识别。这里有一个可打印版本的链接,可以帮助你入门:github.com/splitbrain/rpibplusleaf。我们将在第五章中介绍 GPIO 引脚及其编号,将你的树莓派连接到物理世界

  1. 最后,使用另一根线(标记为gnd'),将按钮的另一侧(B2 孔中的腿)连接到面包板上的负电源轨。我们的插图显示了从 A12 孔到左侧负(-)电源轨上的附近孔的gnd'线连接。缩写gnd表示地线。我们将在接下来的部分理解地线连接和符号中更详细地介绍这个术语。

电气上,一个 SPST 开关可以安装在任何方向。如果你的按钮有四条腿(两组将被电连接),并且当我们在探索两种方法在 Python 中集成一个按钮部分测试电路时不起作用,请尝试将按钮在面包板上旋转 90 度。

现在我们的按钮已经就位并接线,接下来我们将定位和连接 LED。

定位和连接 LED

LED 是由一个微小的晶体制成的小而明亮的灯,当电流连接到它时会发出颜色。

图 2.4显示了一个典型的 LED。图表的左侧显示了 LED 的物理表示,而右侧显示了 LED 的原理图符号:

图 2.4 - LED 和原理图符号

LED 需要正确连接到电路中,否则它们将无法工作。如果你仔细观察 LED,你会注意到 LED 外壳上有一面是平的。这一面的腿是cathode,连接到电源的负极或地线。cathode 腿也是 LED 腿中较短的一个。另一个腿称为anode,连接到电源的正极。如果你仔细观察 LED 符号,你会注意到 LED 的 cathode 一侧有一条线横穿三角形的顶端 - 如果你把这条线看作一个大的负号,它会帮助你记住符号的哪一侧是 cathode 腿。

图 2.5我们即将创建的 LED 连接。请在按照接下来的步骤时参考这个图表:

图 2.5 - 连接 LED

以下是如何将 LED 连接到面包板并连接到树莓派的方法。以下步骤编号与图 2.5中编号的黑色圆圈相匹配:

  1. 按照插图将 LED 连接到面包板上,确保 LED 安装的方向正确。我们的插图显示了 cathode 腿在 E15 孔,anode 腿在 E16 孔。

你可能需要弯曲 LED 的腿来使其就位。在安装 LED 时,请确保两条腿不要相互接触!如果它们接触,这将导致所谓的电短路,LED 电路部分将无法工作。

  1. 接下来,使用一根跳线(标记为gnd"),将 LED 的 cathode 腿连接到与按钮共享的相同电源轨。我们展示了这个连接,一端连接在 A15 孔,另一端连接到左侧负(-)电源轨上的附近孔。

  2. 最后,使用另一根跳线(标记为gnd),将负(-)电源轨连接到树莓派 GPIO 引脚头的第 17 个外侧引脚。这个引脚是树莓派上的地线(GND)引脚。

干得好!这就是我们的 LED 连接。接下来,我们添加电阻,这将完成我们的电路。

定位和连接电阻

电阻是用来限制(即阻止)电流流动和分压的电子元件,它们是非常常见的电子元件。

图 2.6中显示了一个物理电阻(左侧)和两个原理图符号(右侧)。这些原理图符号之间没有实际区别。它们代表不同的文档标准,您会发现原理图图表的作者会选择并坚持使用一种类型的符号。我们将在本书中始终使用这种波浪形符号:

图 2.6 - 电阻和原理图符号

电阻有许多形状、大小和颜色。作为一般指南,它们的物理形状和大小与它们的物理特性和能力有关,而它们外壳的颜色通常是无关紧要的,至少就它们的性能而言。然而,电阻上的彩色条带非常重要,因为它们标识了电阻的值。值得一提的是,小型通用电阻(我们将使用的)使用彩色条带来指定它们的值,而在高功率应用中使用的物理上更大的电阻通常在外壳上印有它们的电阻值。

电阻是一种无偏电子元件,这意味着它们可以以任何方式安装在电路中。然而,它们的值需要正确选择,否则电路可能无法按预期工作,或者更糟的是,电阻和/或其他组件(包括您的树莓派)可能会受到损坏。

在开始学习电路时,强烈建议并且最安全的做法是始终使用电路所列的预期电阻值。避免在没有正确值的情况下替换不同的值,因为这可能会导致元件甚至您的树莓派受损。

我们在本书中对电阻的使用将是实用的。虽然我将解释我们为什么选择特定值以及如何得出这些值的原因,从第六章开始,软件工程师的电子学 101。如果您对电阻不熟悉,您会在进一步阅读部分找到两个链接,您可以在其中了解更多信息,包括如何读取它们的值。

图 2.7展示了我们需要创建的电阻连接。请在按照接下来的步骤时参考这个图:

图 2.7 - 面包板上完成的按钮和 LED 电路

这是如何将电阻连接到您的面包板中的。以下步骤编号与图 2.7中编号的黑色圆圈相匹配:

  1. 将电阻的一条腿(无论哪一条)插入与 LED 的阳极腿相同行的孔中。这个连接显示在 D16 孔。将另一条腿插入一个空行,显示在 D20(在连接下一个导线之前,它将是面包板上的一个空行)。

  2. 使用跳线(从 A20 孔开始插入)*,我们将电阻的另一条腿连接到树莓派 GPIO 引脚的外侧边缘上的第 20 个引脚。这个引脚被称为 GPIO 21。

干得好!通过最后一次连接,我们已经创建了我们的第一个电路。我们将在本章的其余部分以及接下来的两章中使用这个基本电路,第三章,使用 Flask 进行 RESTful API 和 Web 套接字网络,以及第四章,使用 MQTT,Python 和 Mosquitto MQTT Broker 进行网络连接。我们将从第五章开始,开始探索一系列其他电路。

现在我们已经完成了面包板电路,并学会了如何连接面包板上的元件和导线,我们准备探索一种用于描述电路的图表技术。

阅读电子原理图

在上一节中,我们通过一系列图示步骤在面包板上建立了我们的第一个电路。在本节中,我们将学习原理图,这是一种记录和描述电路的正式方式。这些是你在电子文本和数据表中找到的图表。

我们将学习如何阅读简单的原理图,并了解它如何与我们刚刚创建的面包板布局相关联。理解这两者之间的关系,特别是能够从原理图创建面包板布局,是你在继续学习电子和物联网过程中需要发展的重要技能。

在本书中,我们将看到和使用的电子电路和原理图相对简单。我们将根据具体情况讨论重要概念和组件符号。对于我们的旅程来说,不需要对原理图的细节进行全面解释,这超出了本书的实际范围。然而,我鼓励你阅读进一步阅读部分提到的 Spark Fun 教程。它提供了对阅读原理图的简要但全面的概述,并将为你提供对这种图表技术及其语义的良好基础理解。

让我们从看一个代表我们刚刚创建的面包板电路的原理图开始,如图 2.7所示。我们的原理图如下所示:

图 2.8 - 图 2.7 中面包板电路的原理图

原理图可以以多种方式正确绘制;然而,我特意绘制了这个图表(并将在本书中适当的地方这样做),以使其与等效的面包板布局紧密相似,以帮助理解和解释。

我们将通过首先解释按钮连接和布线来学习阅读这个原理图。

阅读按钮原理图连接

我已经将面包板布局和原理图(带有一些额外的标签)结合如下:

图 2.9 - 组合面包板和原理图,第一部分共 2 部分

以下是如何阅读按钮连接。以下步骤编号与图 2.9中编号的黑色圆圈相匹配:

  1. 从标有wire 1的面包板开始。如果我们看这根导线的两端,我们会发现一端连接到树莓派上的 GPIO 23,而另一端(在孔 A10 处)连接到一个与按钮共享的行。

  2. 从原理图来看,这个面包板连接通过标有wire 1的线条图示。你会注意到线条的一端标有 GPIO23,而另一端通向按钮符号的一侧。

导线外壳的颜色没有固有的含义。颜色只是一种视觉辅助,用于区分不同的导线和连接。然而,有一些常见的惯例,比如使用红色导线表示正电源连接,黑色导线表示负极或地线。

  1. 接下来,从面包板上按钮的另一侧(孔 A12)开始,注意标有*gnd'*的导线。这根导线将按钮连接到面包板上的外部电源轨。

  2. 从这个第一个电源轨连接下来的五个孔,我们看到第二根地线(标有gnd),从面包板返回到树莓派上的一个 GND 引脚。

  3. 在原理图中,面包板上的gndgnd'线连接被表示为标有gnd的线,这条线从按钮出来并以一个向下的箭头符号注释为GND(记住面包板上的gndgnd'是电连接的,因此在逻辑上是一根线)。这是地线连接的符号,你会经常在原理图中看到这个符号。当我们到达标题为阅读和理解地线符号*的部分时,我会对这个符号有更多的话要说。

  4. 检查原理图中的按钮符号,你会注意到wire 1gnd线并没有连接,而是终止在按钮符号(小圆圈)。这被称为常开连接,或者在我们的特定情况下,是常开开关。你可以把常开理解为意味着线路断开(记住线代表导线)。现在,如果你想象按钮被按下,那么按钮会触碰每个圆圈,并连接蓝色gnd线,形成一个闭合连接,完成了 GPIO 23 和 GND 之间的电路。我们将在第六章中更详细地讨论这个概念,软件工程师的电子学 101

当你确信你理解了面包板上按键的连接如何与原理图上的按键部分匹配时,我们将继续讨论 LED 和电阻的连接。

阅读 LED 和电阻的原理图连接

继续上一节,我们学习了如何阅读和理解原理图上按键部分,接下来我们将通过讨论 LED 和电阻的连接来完成我们的解释,如下所示:

图 2.10 - 组合面包板和原理图,第二部分

以下是如何阅读 LED 和电阻的连接。以下步骤编号与图 2.10中编号的黑色圆圈相匹配:

  1. 从面包板上标有wire 2的线开始。这根线将树莓派上的 GPIO 21 连接到一个端子上,这个端子与一个电阻的一端(孔 A25)共用。

  2. 原理图上也标有wire 2的线连接。

  3. 在面包板上,电阻的另一端连接到 LED 的阳极腿(孔 E15)。记住,电阻和 LED 的阳极腿是电连接的,因为它们在面包板上的同一排孔中的同一组孔中。

  4. 我们在原理图中看到了电阻/LED 连接,电阻符号与 LED 符号相遇。我们知道电阻通过 LED 符号的方向连接到了 LED 的阳极一侧。

  5. 接下来,在面包板上,LED 的另一端(孔 E15)——阴极腿——连接到gnd'线(孔 A15),然后连接回外部电源轨,这也是按键的gnd'线所共用的(然后通过gnd线连接回树莓派的 GND 引脚)。

  6. 最后,在原理图上,LED 阴极腿到 GND 的连接由标有gnd的线表示(与按键使用的相同线)。

我们现在完成了我们的原理图解释。你做得怎么样?我希望你能够追踪原理图并看到它如何与我们在面包板上构建的电路相关联。

我们的最后一步展示了电子学中一个重要的概念——公共地。我们将在接下来更详细地讨论这个概念。

引入地线连接和符号

所有电路都需要一个共同的电参考点,我们称之为。这就是为什么我们看到按键和 LED 在面包板和原理图上共享一个公共连接的原因(作为提醒,请参考图 2.10)。

对于本书中提出的简单电路以及使用树莓派的 GPIO 引脚时,将考虑将术语视为可互换的是实用的。这是因为电源的端将是我们的电气参考点(是的,GPIO 引脚是电源,我们将在第六章,软件工程师的电子学 101中更多地探索)。

如前面在阅读按钮原理图连接部分提到的,在步骤 4中,我们用箭头符号标注了地点。我们的地面符号(由线段组成)是一个常见的地面符号变体。您将在图 2.11中看到另一个变体:

图 2.11 - 常见原理图地面符号

所有地点都是电气连接的,我们可以在原理图中多次重复符号,以帮助简化原理图。通过使用地面符号来指示共同的地面连接,我们消除了绘制许多互连线以连接所有地面连接在一起的需要(对于大型或更复杂的电路来说,这将变得非常混乱)。

我们的简单电路当然不属于大型复杂的范畴,但是为了说明共同地面的概念,我已经重新绘制了最初在图 2.8中显示的原理图,只是这次使用了多个地面符号:

图 2.12 - 图 2.7中面包板电路的替代原理图

尽管我们的替代原理图看起来像是两个独立的电路,但它们在电气上与我们原始的图 2.8中的原理图完全相同。

现在请花一点时间查看图 2.8图 2.12,看看你能否弄清楚这两个图是如何在电气上相同的。

我在这里所做的只是打破了线(在图 2.8中标记为gnd)并重新绘制了按钮子电路和 LED/电阻子电路,使用了不同的方向,并为每个子电路使用了单独的地面符号。

如前所述,在本书的这个阶段,我们不会深入讨论这个电路在电子上是如何工作的,或者它如何与树莓派上的 GPIO 引脚进行电气交互。当我们到达第六章,软件工程师的电子学 101时,我们将通过实际和说明性的练习来涵盖这些主题以及更多内容。

现在您已经看到了记录我们面包板电路的原理图,并了解了它们之间的关系,我们终于准备好深入代码,学习在 Python 中让 LED 闪烁的两种方法!

在 Python 中探索闪烁 LED 的两种方法

在本节中,我们将研究两种替代的 GPIO 库和在 Python 中让 LED 闪烁的方法,包括以下内容:

  • GPIOZero 库:一个入门级的 GPIO 库

  • PiGPIO 库:一个高级的 GPIO 库

当我们学习使用这两个库时,我们将看到它们如何不同地处理 GPIO 控制,并发现它们的相对优势和劣势。

完成本节(以及接下来的在 Python 中探索集成按钮的两种方法部分)后,您将探索并比较了 GPIO 控制的两种非常不同的方法——高级(使用 GPIOZero)和低级(使用 PiGPIO),并且对在构建电子接口程序时何时以及如何选择这两种方法有一个很好的入门理解。

让我们通过使用 GPIOZero 使 LED 闪烁来开始我们的实际练习。

使用 GPIOZero 闪烁

现在我们准备使用 GPIOZero 库来研究我们的第一种闪烁方法。您将在chapter02/led_gpiozero.py文件中找到我们即将涵盖的代码。请在继续之前查看此文件。

进一步阅读部分,您将找到与我们在本节中使用的库的特定功能相关的 GPIOZero API 文档的相关链接。

我们将从运行我们的示例代码开始。

使用以下命令运行程序,记住您需要在activated虚拟环境中(如果您需要关于如何激活 Python 虚拟环境的复习,请参阅第一章,设置您的开发环境):

(venv) $ python led_gpiozero.py

如果 LED 连接正确,它应该会闪烁。

如果在运行程序时收到有关 PiGPIO 的错误,请确保您已经按照第一章中概述的方式启用了pigpio守护程序。我们将在第五章中更多地讨论 PiGPIO 和 PiGPIO 守护程序,将您的树莓派连接到物理世界

现在我们已经运行了代码并看到 LED 闪烁,是时候浏览使这一切发生的代码了。

导入

我们将从查看我们在 Python 程序中导入的外部库开始我们的代码探索。它们出现在源文件的顶部附近,如下所示:

from gpiozero import Device, LED                # (1)
from gpiozero.pins.pigpio import PiGPIOFactory  # (2)
from time import sleep  

感兴趣的导入如下:

  • 在第(1)行,我们从 GPIOZero 包中导入DeviceLED类。

  • 在第(2)行,我们导入了一个 GPIOZero Pin Factory。这与Device类一起使用,接下来我们将看到。

接下来,我们将看到如何设置 GPIOZero Pin Factory 实现。

引脚工厂配置

在 GPIOZero 中使用Pin Factory指定 GPIOZero 将使用哪个具体的 GPIO 库来执行实际的 GPIO 工作。当我们在本章的比较 GPIOZero 和 PiGPIO 示例部分比较 GPIOZero 和 PiGPIO 示例时,我们将更详细地讨论 Pin Factory:

Device.pin_factory = PiGPIOFactory()  # (3)  

在第(3)行,我们告诉 GPIOZero 使用 PiGPIO 作为其Pin Factory,使用DevicePiGPIOFactory导入。

既然我们已经看到了如何设置 Pin Factory,让我们看看使我们的 LED 闪烁的代码。

LED 闪烁

在这里,我们在第(4)行看到了LED类,它被创建并分配给了led变量。LED的参数是物理 LED 连接到的 GPIO 引脚,如图 2.1中的面包板上所示:

GPIO_PIN = 21 
led = LED(GPIO_PIN)         # (4)
led.blink(background=False) # (5)

在第(5)行,我们开始 LED 闪烁。blink()background=False参数需要在主线程上运行 LED,以便程序不会退出(background=True的另一种选择是使用signal.pause()。我们将在下一节中看到一个例子)。

GPIOZero 使得与 LED 等常见电子元件进行接口非常容易。接下来,我们将执行相同的练习,只是这次使用 PiGPIO 库。

使用 PiGPIO 闪烁

现在我们已经看到了如何使用 GPIOZero 库闪烁 LED,让我们看看使用 PiGPIO 库的另一种方法。

我们即将要讲解的代码包含在chapter02/led_pigpio.py文件中。如果之前的示例仍在运行,请终止它,并运行led_pigpio.py。LED 应该会再次闪烁。

进一步阅读部分,您将找到与我们在本节中使用的 PiGPIO 库的特定功能相关的 PiGPIO API 文档的相关链接。

让我们浏览一下我们 LED 闪烁代码的 PiGPIO 版本。

导入

从文件的顶部开始,我们有源文件的import部分:

import pigpio           # (1)
from time import sleep

这一次,在第(1)行,我们只需要导入 PiGPIO 模块。

接下来,我们将看到如何配置 PiGPIO 并设置连接到我们的 LED 的 GPIO 引脚的 I/O 模式。

PiGPIO 和引脚配置

让我们看看配置 PiGPIO 和 LED 的 GPIO 引脚的代码:

GPIO_PIN = 21
pi = pigpio.pi()                        # (2)
pi.set_mode(GPIO_PIN, pigpio.OUTPUT)    # (3)

我们在第(2)行创建了一个 PiGPIO 的实例,并将其分配给了pi变量。我们将使用这个变量从代码的这一点开始与 PiGPIO 库进行交互。

在第(3)行,我们配置 GPIO 引脚 21 为输出引脚。配置引脚为输出意味着我们希望使用该引脚来从我们的 Python 代码控制连接到它的东西。在这个示例中,我们想要控制 LED。在本章的后面,我们将看到一个使用输入引脚来响应按钮按下的示例。

现在我们已经导入了所需的库并配置了 PiGPIO 和输出 GPIO 引脚,让我们看看我们是如何让 LED 闪烁的。

闪烁 LED

最后,我们让 LED 闪烁:

while True:
 pi.write(GPIO_PIN, 1) # 1 = High = On    # (4)
 sleep(1) # 1 second
 pi.write(GPIO_PIN, 0) # 0 = Low = Off    # (5)
 sleep(1) # 1 second

我们使用 PiGPIO 通过一个while循环来实现闪烁。当循环执行时,我们在 GPIO 引脚 21 上进行切换-我们的输出引脚-打开和关闭(第(4)和(5)行),之间有一个短的sleep()函数,因此 LED 看起来在闪烁。

接下来,我们将比较我们的两个库及它们对 LED 闪烁的不同方法。

比较 GPIOZero 和 PiGPIO 示例

如果您查看 GPIOZero 示例的代码,很明显我们正在让 LED 闪烁-代码中非常明显。但是 PiGPIO 示例呢?没有提到 LED 或闪烁。事实上,它可以做任何事情-只是我们知道 LED 连接到 GPIO 21。

我们的两个闪烁示例揭示了 GPIOZero 和 PiGPIO 的重要方面:

  • GPIOZero是一个更高级的包装库。在表面上,它将常见的电子元件(如 LED)抽象成易于使用的类,而在底层,它将实际的接口工作委托给一个具体的 GPIO 库。

  • PiGPIO是一个更低级的 GPIO 库,您可以直接使用、控制和访问 GPIO 引脚。

GPIOZero 中的“zero”指的是零样板代码库的命名约定,其中所有复杂的内部部分都被抽象化,以使初学者更容易上手。

GPIOZero 通过Pin Factory将其委托给外部 GPIO 库。在我们的示例中,我们使用了一行代码将其委托给了 PiGPIO,Device.pin_factory = PiGPIOFactory()。我们将在第五章中再次讨论 GPIOZero 和委托的主题,将您的树莓派连接到物理世界

在本书中,我们将同时使用 GPIOZero 和 PiGPIO。我们将使用 GPIOZero 来简化和压缩代码,同时我们将使用 PiGPIO 来进行更高级的代码示例,并教授通过 GPIOZero 抽象掉的核心 GPIO 概念。

接下来,我们将继续通过集成按钮来构建 LED 闪烁示例。

探索在 Python 中集成*按钮的两种方法

在前一节中,我们探讨了两种不同的方法来使我们的 LED 闪烁-一种使用 GPIOZero 库,另一种使用 PiGPIO 库。在本节中,我们将集成图 2.1中电路中的按钮,并看看如何使用 GPIOZero 和 PiGPIO 库集成按钮。

我们将首先使用 GPIOZero 库来使我们的 LED 与使用 GPIOZero 库集成的按钮打开和关闭。

使用 GPIOZero 响应按钮按下

我们将要讨论的代码包含在chapter02/button_gpiozero.py文件中。请查看并运行此文件。按下按钮时,LED 应该会打开和关闭。根据图 2.1中的电路,LED 仍连接到 GPIO 21,而我们的按钮连接到 GPIO 23。

如前面的创建面包板电路部分所述,如果您的按钮有四条腿(两组将被电气连接),而您的电路不起作用,请尝试将按钮在面包板上旋转 90 度。

让我们走一遍代码的重要部分,注意我们跳过了我们已经涵盖过的代码部分。

导入

从源文件的顶部开始,您将找到导入外部库的代码部分,如下所示:

from gpiozero import Device, LED, Button         # (1)
from gpiozero.pins.pigpio import PiGPIOFactory
import signal                                    # (2)

对于这个示例,我们还导入了 GPIOZero 的Button类(1)和 Python 的signal模块(2)。

现在您已经看到我们正在导入Button类,让我们看一下当按下按钮时将调用的处理程序函数。

按钮按下处理程序

我们正在使用回调处理程序来响应按钮按下,定义在pressed()函数中:

def pressed():
 led.toggle()                               # (3)
 state = 'on' if led.value == 1 else 'off'  # (4)
 print("Button pressed: LED is " + state)   # (5)

在第(3)行,我们的 LED 在每次调用pressed()时使用ledtoggle()方法打开和关闭。在第(4)行,我们查询ledvalue属性来确定 LED 是开启(value == 1)还是关闭(value == 0),并将其存储在state变量中,然后在第(5)行打印到终端。

您还可以使用led.on()led.off()led.blink()方法来控制 LED。您还可以通过设置led.value直接设置 LED 的开/关状态,例如,led.value = 1将打开 LED。

让我们继续看看如何创建和配置Button类实例,并注册pressed()函数,以便在按下物理按钮时调用它。

按钮配置

以下是用于配置按钮的行。在第(6)行,我们使用的类是Button。在 GPIOZero 中,我们使用Button类来表示任何可以处于开启或关闭状态的输入设备,例如按钮和开关:

button = Button(BUTTON_GPIO_PIN, 
                pull_up=True, bounce_time=0.1)  # (6)
button.when_pressed = pressed                   # (7)

在第(7)行,我们使用button实例注册了pressed()回调处理程序。

以下是第(6)行中Button构造函数的参数含义:

  • 第一个参数是按钮的 GPIO 引脚(BUTTON_GPIO_PIN == 23)。

  • 第二个参数pull_up=True为 GPIO 23 启用了内部上拉电阻。上拉和下拉电阻是数字电子学中的重要概念。我们现在将跳过这个概念,因为我们将在第六章“软件工程师的电子学 101”中更详细地介绍上拉和下拉电阻的重要性和用途。

  • 第三个参数bounce_time=0.1(0.1 秒)用于补偿开关或接触抖动

抖动是一种电气噪声,当物理按钮或开关内的金属触点接触时会发生。这种噪声的结果是在数字输入引脚上看到快速连续的开关(或高低)状态变化。这是不希望的,因为我们希望一个按钮的物理按下(或开关的切换)被视为输入引脚上的一个状态变化。在代码中通常使用去抖动阈值超时来实现这一点,而在我们的情况下,这是我们的树莓派在初始状态变化后忽略连续引脚状态变化的时间量。

尝试设置bounce_time=0(不去抖动)。您会发现按钮的行为非常不稳定。然后,使用更高的数字,例如bounce_time=5(5 秒),您会发现在第一次按下按钮后,按钮在持续时间到期之前都无响应。

在选择适当的去抖动阈值时,需要权衡用户需要多快按下按钮(这需要较低的阈值)与按钮固有的抖动量(这需要较高的阈值)。大约 0.1 秒是一个很好的建议起始值。

最后,让我们介绍一种常见的技术,用于防止电子接口 Python 程序退出。

防止主线程终止

在 GPIO 示例和程序中经常看到使用signal.pause()或等效构造:

signal.pause() # Stops program from exiting.  # (8)

第(8)行防止主程序线程达到其自然结束,正常情况下程序会在那里终止。

在开始时,忘记在 GPIO 接口 Python 程序的末尾添加signal.pause()是一个常见且常常令人困惑的错误。如果您的程序在启动后立即退出,请尝试将signal.pause()添加到程序的末尾作为第一步。

我们在以前的 LED 闪烁示例中不需要signal.pause()。原因如下:

  • 我们的 GPIOZero 示例(chapter02/led_gpiozero.py)在 LED 构造函数中使用了background=False。这通过将 LED 的线程保持在前台来防止程序退出。

  • 在 PiGPIO 示例(chapter02/led_pigpio.py)中,是while循环阻止程序退出。

如果这看起来令人困惑,不要担心!知道如何防止程序异常退出归根结底取决于经验、实践和理解 Python 和 GPIO 库的工作原理。

接下来,让我们看看如何使用 PiGPIO 集成按钮。

使用 PiGPIO 响应按钮按下

现在,我们将使用 PiGPIO 库复制与之前的 GPIOZero 示例相同的功能,通过按下按钮来打开和关闭 LED,只是这次使用 PiGPIO 库。我们的 PiGPIO 示例的代码可以在chapter02/button_pigpio.py文件中找到。请现在查看并运行此文件,并确认 LED 是否响应您的按钮按下。

让我们解开代码的有趣部分,从推按钮的 GPIO 引脚配置开始(再次注意,我们跳过了已经涵盖的代码部分)。

按钮引脚配置

从第 1 行开始,我们将 GPIO 引脚 23(BUTTON_GPIO_PIN == 23)配置为输入引脚:

pi.set_mode(BUTTON_GPIO_PIN, pigpio.INPUT)           # (1)
pi.set_pull_up_down(BUTTON_GPIO_PIN, pigpio.PUD_UP)  # (2)
pi.set_glitch_filter(BUTTON_GPIO_PIN, 10000)         # (3)

接下来,在第 2 行,我们为引脚 23 启用内部上拉电阻。在 PiGPIO 中,我们在第 3 行使用pi.set_glitch_filter()方法对推按钮进行去抖动。此方法以毫秒为参数。

请注意,在 PiGPIO 中,我们需要为我们的按钮配置每个属性(引脚输入模式、上拉电阻和去抖动)作为单独的方法调用,而在之前的 GPIOZero 示例中,当我们创建 GPIOZero LED 类的实例时,所有这些都发生在一行上。

按钮按下处理程序

我们的按钮回调处理程序在第 4 行开始定义,比之前的 GPIOZero 处理程序更复杂:

def pressed(gpio_pin, level, tick):                         # (4)
 # Get current pin state for LED.
 led_state = pi.read(LED_GPIO_PIN)                       # (5)

 if led_state == 1:                                      # (6)
 # LED is on, so turn it off.
 pi.write(LED_GPIO_PIN, 0) # 0 = Pin Low = Led Off
 print("Button pressed: Led is off")
 else: # 0
 # LED is off, so turn it on.
 pi.write(LED_GPIO_PIN, 1) # 1 = Pin High = Led On
 print("Button pressed: Led is on")

# Register button handler.
pi.callback(BUTTON_GPIO_PIN, pigpio.FALLING_EDGE, pressed)  # (7)

注意pressed(gpio_pin, level, tick)的签名。我们之前的 GPIOZero 版本没有参数,而 PiGPIO 有三个强制参数。我们的简单单按钮示例不使用这些参数;然而,为了完整起见,它们如下:

  • gpio_pin:这是负责调用回调的引脚。在我们的示例中将是 23。

  • level:这是引脚的状态。对我们来说,这将是pigpio.FALLING_EDGE(我们马上就会知道为什么)。

  • tick:这是自启动以来的微秒数。

在第 5 行,我们使用led_state = pi.read()将 GPIO 21(我们的 LED)的当前状态读入变量。然后,从第 6 行开始,根据 LED 当前是否打开(led_state == 1)或关闭(led_state == 0),我们使用pi.write()将 GPIO 21 设置为高电平或低电平,以切换 LED 到其相反的打开或关闭状态。

最后,回调处理程序在第 7 行注册。参数值pigpio.FALLING_EDGE意味着每当 GPIO 引脚BUTTON_GPIO_PIN(即 23)开始从数字高转换为数字低时,调用处理程序为pressed()。这比简单地测试引脚是还是更明确;然而,为了简单起见,考虑以下级别参数选项pi.callback()。尝试更改参数,看看当您按下按钮时会发生什么:

  • pigpio.FALLING_EDGE:这是低的(想向低处下降)。当您按下按钮时,将调用pressed()

  • pigpio.RAISING_EDGE:这是高的(想向高处升起)。当您释放按钮时,将调用pressed()

  • pigpio.EITHER_EDGE:这可以是高或低。当您按下释放按钮时,将调用pressed(),实际上意味着只有当您按住按钮时 LED 才会亮起。

在 PiGPIO 示例中,您是否注意到或认为当按下按钮时,即激活按钮时,GPIO 引脚 23 变为低电平(即第 7 行上的pigpio.FALLING_EDGE参数),这导致pressed()被调用?从编程的角度来看,这似乎有点前后颠倒或错误?我们将在第六章中重新讨论这个想法,并讨论背后的原因,即软件工程师的电子学 101

现在关于 GPIO 库和电子学的内容就到此为止。我们已经看到如何使用 GPIOZero 和 PiGPIO 库响应按钮按下。特别是,我们发现与涉及更多代码和更多配置的 PiGPIO 方法相比,GPIOZero 方法更简单直接。这与我们在上一节探索 Python 中闪烁 LED 的两种方法中发现的结果相同,即 GPIOZero 方法更简单。

一个方法是否比另一个更好?答案取决于您试图实现的目标以及您对电子接口的低级控制需求。在本书的这个阶段,我只是想给您提供有关 GPIO 库及其与电子设备接口的对比选项。当我们在第五章中重新讨论 Python 的流行 GPIO 库时,我们将更详细地讨论这个话题,将您的树莓派连接到物理世界

让我们继续创建一个 IoT 程序,以通过互联网控制我们的 LED。

创建您的第一个 IoT 程序

我们即将创建一个 Python 程序,与名为dweet.io的服务集成。这是他们网站对该服务的描述:“就像社交机器的 Twitter。”

我们将创建简单的dweets,它们是 dweet.io 版的tweet,通过将 URL 粘贴到 Web 浏览器中。

我们的程序将通过轮询 dweet.io 的 RESTful API 端点来监视和接收我们的 dweets。当接收到数据时,将对其进行解析,以查找指示是否应打开或关闭 LED 或使其闪烁的指令。根据此指令,将使用 GPIOZero 库更改 LED 状态。在我们讨论程序代码的下一部分理解服务器代码中,我们将查看从 dweet.io 接收的数据格式。

我们使用免费的公共dweet.io服务,所有信息都是公开可访问的,因此不要发布任何敏感数据。还有一个专业服务可用于dweetpro.io,提供数据隐私、安全性、dweet 保留和其他高级功能。

该程序的代码包含在chapter02/dweet_led.py文件中。在继续之前,阅读此文件中的源代码,以获得关于发生了什么的广泛视角。

运行和测试 Python 服务器

在本节中,我们将运行并与一个 Python 服务器程序进行交互,该程序将允许我们通过复制和粘贴链接从 Web 浏览器控制我们的 LED。一旦我们使用程序控制了我们的 LED,我们将在下一节深入探讨代码的机制以及它是如何工作的。

以下是要遵循的步骤:

  1. 运行chapter02/dweet_led.py程序。您应该会看到类似以下的输出:
(venv) $ python dweet_led.py
INFO:main:Created new thing name a8e38712                  # (1)
LED Control URLs - Try them in your web browser:
 On    : https://dweet.io/dweet/for/a8e38712?state=on      # (2)
 Off   : https://dweet.io/dweet/for/a8e38712?state=off
 Blink : https://dweet.io/dweet/for/a8e38712?state=blink

INFO:main:LED off
Waiting for dweets. Press Control+C to exit.

在第(1)行,程序已为我们的thing创建了一个唯一的名称,用于与 dweet.io 一起使用。您将在第(2)行开始的 URL 中注意到此名称。为您的thing创建的名称将与前面的示例不同。

dweet.io 中的thing名称类似于 Twitter 上的@handle。

  1. 将 URL 复制并粘贴到 Web 浏览器中(可以是树莓派以外的计算机)。经过短暂延迟后,LED 应根据使用的 URL 更改其状态(打开、关闭或闪烁)。

一旦您确认 LED 可以使用 URL 进行控制,我们将继续查看程序。

理解服务器代码

在本节中,我们将逐步介绍dweet_led.py程序的主要部分,并了解它的工作原理,从导入开始。

导入

首先,在源代码文件的开头,我们看到 Python 的导入:

...truncated...
import requests     # (1)

我想要引起您注意的一个特定的导入是在第 1 行,我们导入了request模块(这是在本章早些时候运行pip install -r requirements.txt时安装的)。requests是一个用于在 Python 中进行 HTTP 请求的高级库。我们的程序使用这个模块与 dweet.io API 进行通信,我们很快就会看到。

现在我们知道我们正在导入并且稍后将使用requests库,让我们来了解一下我们程序中使用的全局变量。

变量定义

接下来,我们定义了几个全局变量。现在,请查看下面的注释以了解它们的用途。随着我们在代码中的进展,您将看到它们被使用:

LED_GPIO_PIN = 21                  # LED GPIO Pin
THING_NAME_FILE = 'thing_name.txt' # Thing name file
URL = 'https://dweet.io'           # Dweet.io service API
last_led_state = None              # "on", "off", "blinking"
thing_name = None                  # Thing name
led = None                         # GPIOZero LED instance

当您阅读主源文件时,除了这些变量定义,您还会注意到我们使用 Python 日志系统而不是print()语句:

logging.basicConfig(level=logging.WARNING)
logger = logging.getLogger('main') # Logger for this module
logger.setLevel(logging.INFO) # Debugging for this file.   # (2)

如果您需要为程序打开调试以诊断问题或查看我们的程序与 dweet.io 服务之间交换的原始 JSON 数据,将第 2 行更改为logger.setLevel(logging.DEBUG)

接下来,我们将逐步介绍程序中的重要方法,并了解它们的作用。

resolve_thing_name()方法

resolve_thing_name()方法负责加载或创建用于 dweet.io 的thing的唯一名称。

我们使用这种方法的意图是始终重复使用一个名称,以便我们的 dweet LED 的 URL 在程序重新启动之间保持不变:

def resolve_thing_name(thing_file):
 """Get existing, or create a new thing name"""
 if os.path.exists(thing_file):                     # (3)
 with open(thing_file, 'r') as file_handle:
 name = file_handle.read()
 logger.info('Thing name ' + name + 
 ' loaded from ' + thing_file)
 return name.strip()
 else:
 name = str(uuid1())[:8]                        # (4)
 logger.info('Created new thing name ' + name)

 with open(thing_file, 'w') as f:               # (5)
 f.write(name)

 return name

在第 3 行,如果文件存在,我们加载之前存储在thing_file中的名称;否则,我们在第 4 行使用 Python 的UUID模块方法uuid1()创建一个 8 个字符的唯一标识符,并将其用作物体名称。我们在第 5 行将这个新创建的标识符兼名称存储在thing_file中。

接下来,我们将看一下检索发送到我们thing的最后一个 dweet 的函数。

get_lastest_dweet()方法

get_lastest_dweet()查询 dweet.io 服务以检索我们thing的最新 dweet(如果有)。以下是我们期望收到的 JSON 响应的示例。最终我们感兴趣的是第 1 行的content.state属性:

{
 this: "succeeded",
 by: "getting",
 the: "dweets",
 with: [
 {
 thing: "a8e38712-9886-11e9-a545-68a3c4974cd4",
 created: "2019-09-16T05:16:59.676Z",
 content: { 
 state: "on"                                       # (1)
 }
 }
 ]
}

看下面的代码,我们在第 6 行看到了用于查询 dweet.io 服务的资源 URL 的创建。对这个 URL 的调用将返回一个类似于前面所示的 JSON。您将在进一步阅读部分找到一个链接,链接到完整的 dweet.io API 参考。

接下来,在第 7 行,requests模块用于发出 HTTP GET 请求以检索最新的 dweet:

def get_lastest_dweet():
 """Get the last dweet made by our thing."""
 resource = URL + '/get/latest/dweet/for/' + thing_name   # (6)
 logger.debug('Getting last dweet from url %s', resource)

 r = requests.get(resource)                               # (7)

从以下第 8 行开始,我们检查请求是否在 HTTP 协议级别成功。如果成功,我们将在第 9 行继续解析 JSON 响应并提取并返回从第 10 行开始的content属性:

 if r.status_code == 200:                              # (8)
 dweet = r.json() # return a Python dict.
 logger.debug('Last dweet for thing was %s', dweet)

 dweet_content = None

 if dweet['this'] == 'succeeded':                  # (9)
 # Interested in the dweet content property.
 dweet_content = dweet['with'][0]['content']   # (10)

 return dweet_content
 else:
 logger.error('Getting last dweet failed 
 with http status %s', r.status_code)
 return {}

我们要讨论的下一个方法是poll_dweets_forever(),它将使用get_lastest_dweet()

poll_dweets_forever()方法

poll_dweets_forever()是一个长时间运行的函数,它周期性地调用第 11 行的get_lastest_dweet()方法,我们刚刚讨论过。当有 dweet 可用时,它将在第 12 行由process_dweet()处理,我们将很快讨论:

def poll_dweets_forever(delay_secs=2):
 """Poll dweet.io for dweets about our thing."""
 while True:
 dweet = get_last_dweet()                        # (11)
 if dweet is not None:
 process_dweet(dweet)                        # (12)

 sleep(delay_secs)                                   # (13)

我们在第 13 行休眠默认延迟 2 秒,然后继续循环。实际上,这意味着在使用 dweeting URL 之一请求 LED 状态更改和 LED 改变其状态之间可能会有长达约 2 秒的延迟。

在主源文件的这一点上,你会遇到一个名为stream_dweets_forever()的函数。这是一种替代的、更高效的基于流的方法,使用 HTTP 流来实时访问 dweets。

这里选择了poll_dweets_forever()的基于轮询的方法进行讨论,这样做是为了简单起见。当你继续阅读时,你会清楚地知道在哪里可以切换方法。

我们接下来要讨论的是我们用来控制 LED 的方法。

process_dweet()方法

正如我们之前所看到的,当poll_dweets_forever()(类似于stream_dweets_forever())获取一个 dweet 时,它会从 dweet 的 JSON 中解析出content属性。然后将其传递给process_dweet()进行处理,我们从content属性中提取state子属性:

def process_dweet(dweet):
 """Inspect the dweet and set LED state accordingly"""
 global last_led_state

 if not 'state' in dweet:
 return

 led_state = dweet['state']                         # (14)

 if led_state == last_led_state:                    # (15)
 return; # LED is already in requested state.

在第 15 行(和后续代码块的第 17 行),我们测试并保持 LED 的上一个已知状态,并且如果它已经处于请求的状态,则避免与 LED 进行交互。这将避免 LED 在已经闪烁时重复进入闪烁状态时可能出现的潜在视觉故障。

process_dweet()的核心是访问 dweet 的state属性并改变 LED 的状态,这从第 16 行开始:

 if led_state == 'on':                 # (16)
 led_state = 'on'
 led.on()
 elif led_state == 'blink':
 led_state = 'blink'
 led.blink()
 else: # Off, including any unhanded state.
 led_state = 'off'
 led.off()

 last_led_state = led_state           # (17)
 logger.info('LED ' + led_state)

在第 16 行之后,我们根据 dweet 设置 LED 状态(记住led变量是一个 GPIOZero LED 实例),然后在第 17 行跟踪新状态,如前所述,当在第 15 行调用process_dweet()进行后续测试时。

由于 GPIOZero 的简单性,我们的 LED 控制代码在代码中只是一闪而过!

我们将通过讨论程序的主入口来结束。

主程序入口点

在源文件的末尾,我们有以下代码:

# Main entry point
if __name__ == '__main__':
 signal.signal(signal.SIGINT, signal_handler) # Capture CTRL + C
 print_instructions()                              # (18)

    # Initialize LED from last dweet.
 latest_dweet = get_latest_dweet()                 # (19)
 if (latest_dweet):
 process_dweet(latest_dweet)

 print('Waiting for dweets. Press Control+C to exit.')

 #Only use one of the following.
 #stream_dweets_forever() # Stream dweets real-time. 
 poll_dweets_forever() # Get dweets by polling.    # (20)

在第 8 行,print_instructions()负责将 sweet URL 打印到终端上,而在第 19 行,我们看到了对get_latest_dweet()的调用。这个调用在程序启动时将我们的 LED 初始化为最后一个 dweet 的状态。最后,在第 20 行,我们开始轮询 dweet.io 服务以访问最新的 dweets。在这里,你可以将 dweet 轮询方法切换到流式方法。

这样,我们完成了对dweet_led.py的演示。通过这次讨论,我们已经看到了如何利用 dweet.io 服务来创建一个简单而实用的物联网程序。在完成本章之前,我想给你留下两个额外的源代码文件,你可以用它们来扩展你的物联网程序。

扩展你的物联网程序

chapter02文件夹中的以下两个文件补充了我们在本章中所涵盖的内容,结合了我们所学到的概念。由于整体代码和方法与我们已经涵盖的内容相似,我们不会详细讨论代码:

  • dweet_button.py提供了一个实现,展示了如何使用按钮创建一个 dweet,并通过 dweet.io 服务来改变 LED 的状态。

  • pigpio_led_class.py提供了一个代码级别的例子,展示了低级库 PiGPIO 和高级库 GPIOZero 之间的关系。

我们将从dweet_button.py开始讨论。

实现一个 dweeting 按钮

dweet_button.py中的这个程序将 GPIOZero 的按钮示例与 dweet.io 集成。在本章的前面部分,标题为运行和测试 Python 服务器的部分中,我们将 URL 复制并粘贴到 Web 浏览器中来控制我们的 LED。

当你运行dweet_button.py时,每次按下按钮,这个程序都会循环遍历 dweet.io 的 URL 来改变 LED 的状态。要配置这个程序,找到并更新以下行,使用你在dweet_led.py中使用的thing name

thing_name = '**** ADD YOUR THING NAME HERE ****'

记住,你还需要在终端中运行dweet_led.py程序,否则 LED 将不会对你的按钮按下做出响应。

接下来,我们将看到如何使用 PiGPIO 和 Python 类来模拟 GPIOZero。

PiGPIO LED 作为一个类

pigpio_led_class.py文件中,我们有一个 Python 类,它是对 PiGPIO LED 示例的重新设计,将其包装为一个模仿 GPIOZero LED类的类。它演示了 GPIOZero 如何将低级 GPIO 复杂性抽象化的基本原理。这个重新设计的类可以作为本章中 GPIOZero LED示例的一个替代,如此所示。有关更多信息,请参阅pigpio_led_class.py中的头部注释。

""" chapter02/dweet_led.py """
...
# from gpiozero import LED                    # Comment out import
from pigpio_led_class import PiGPIOLED as LED # Add new import

我希望您会对这两个额外的文件感兴趣,并且通过探索 PiGPIO LED 作为一个类的示例,您可以更好地理解高级 GPIOZero 库和低级 PiGPIO 库之间的关系。

在您的学习过程中,如果您对pigpio_led_class.py发生的情况有些不清楚,不要担心。我只是想简单地为您提供一个 GPIO 库交互的简要示例,供您在继续阅读时作为参考。我们将在第五章中更详细地介绍 GPIOPZero 和 PiGPIO 库(以及其他库),另外我们还将在第十二章中涵盖更高级的概念,比如电子接口程序中的线程(类似于pigpio_led_class.py中的线程)。

总结

通过本章,您刚刚使用树莓派和 Python 创建了一个真正的功能性物联网应用程序。我们看到了使用 GPIOZero 和 PiGPIO GPIO 库在 Python 中闪烁 LED 和读取按钮按下的两种替代方法。我们还比较了这些库的使用,并发现 GPIOZero 比低级 PiGPIO 库更高级和更抽象地处理编码和 GPIO 控制。我们还使用在线的 dweet.io 服务将 LED 连接到了互联网。通过简单的 URL,我们能够通过简单地访问网页浏览器中的 URL 来打开、关闭和闪烁 LED。

当您在本书的后续章节中继续学习时,我们将在本章学到的关于 GPIO 接口、电子电路和通过互联网控制电路的核心知识的基础上进行更深入的建设。我们将学习构建应用程序的替代方法,以及发现与 GPIO 控制和电子接口相关的核心原则。掌握了这些深入的知识,您将能够在完成本书时创建更强大、更宏伟的物联网解决方案!

在第三章中,使用 Flask 进行 RESTful API 和 Web 套接字网络,我们将研究流行的 Flask 微服务框架,并创建两个基于 Python 的 Web 服务器和相应的网页,以控制 LED 在本地网络或互联网上的状态。

问题

以下是一些问题列表,供您测试对本章材料的了解。您将在本书的评估部分找到答案:

  1. 你没有正确的电阻值。你能否用周围的另一个电阻值替代?

  2. GPIOZero 包是一个完整的 GPIO 库。这就是你所需要的全部吗?

  3. 在可能的情况下,您应该始终使用内置的 Python 网络包吗?

  4. 真或假:LED 是无偏的,意味着它可以以任何方式插入电路并仍然工作。

  5. 您正在构建一个与其他现有网络设备交互的物联网应用程序,但它超时了。可能出了什么问题?

  6. 哪个 Python 模块和函数可以用来阻止程序退出?

进一步阅读

我们使用 dweet.io 服务将 LED 连接到互联网,并调用其 RESTful API,这些 API 在以下文档中有所记录:

您可能希望简要了解一下 GPIOZero 库,以了解它的功能。它有大量的示例和详细的文档。以下是我们目前涵盖的 API 文档的一些有用链接:

关于 PiGPIO,这里是其 API 文档的相关部分。您会注意到 PiGPIO 是一个更高级的 GPIO 库,文档不太冗长。

电阻器是一种非常常见的电子元件。以下资源提供了电阻器的概述,以及如何阅读它们的色带以确定它们的电阻值(欧姆):

以下 Spark Fun 教程提供了一个很好的引言来阅读原理图:

第三章:使用 Flask 进行 RESTful API 和 Web Socket 的网络连接

在第二章中,使用 Python 和物联网入门,我们创建了一个基于dweet.io的网络化物联网应用程序,在这个应用程序中,您可以通过互联网控制连接到树莓派的 LED。我们的第一个物联网应用程序完全是通过 API 请求驱动的。

在本章中,我们将关注 Python 中创建网络服务的替代方法,这些服务可以被 Python 和非 Python 客户端访问。我们将看看如何在 Python 中构建一个 RESTful API 服务器和一个 Web Socket 服务器,并应用我们在上一章学到的电子接口技术,使它们与我们的 LED 进行交互。

完成本章后,您将了解使用 Python 构建服务器的两种不同方法,包括与服务器交互的伴随网页。这两个服务器将为您提供一个端到端的参考实现,您可以将其用作自己网络连接的物联网项目的起点。

由于本章是关于网络技术,我们将继续使用 GPIOZero 为基础的 LED,仅仅是为了简化和抽象,以便我们的示例直截了当、以网络为重点,而不被 GPIO 相关的代码所混淆。

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

  • 介绍 Flask 微服务框架

  • 使用 Flask 创建 RESTful API 服务

  • 添加 RESTful API 客户端网页

  • 使用 Flask-SocketIO 创建 Web Socket 服务

  • 添加 Web Socket 客户端网页

  • 比较 RESTful API 和 Web Socket 服务器

技术要求

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

  • 树莓派 4 型 B 型

  • Raspbian OS Buster(带桌面和推荐软件)

  • 至少 Python 版本 3.5

这些要求是本书中代码示例的基础。可以合理地期望,只要您的 Python 版本是 3.5 或更高,代码示例应该可以在树莓派 3 型 B 型或不同版本的 Raspbian OS 上无需修改地运行。

您可以在 GitHub 存储库的chapter03文件夹中找到本章的源代码:github.com/PacktPublishing/Practical-Python-Programming-for-IoT

您需要在终端中执行以下命令,以设置虚拟环境并安装本章所需的 Python 库:

$ cd chapter03              # Change into this chapter's folder
$ python3 -m venv venv      # Create Python Virtual Environment
$ source venv/bin/activate  # Activate Python Virtual Environment
(venv) $ pip install pip --upgrade        # Upgrade pip
(venv) $ pip install -r requirements.txt  # Install dependent packages

以下依赖项已从requirements.txt中安装:

我们将使用我们在第二章中创建的面包板电路,使用 Python 和物联网入门图 2.7

介绍 Flask 微服务框架

Flask 是一个流行且成熟的 Python 微服务框架,您可以使用它来创建 API、网站以及几乎任何其他您能想象到的网络服务。尽管 Flask 并不是 Python 唯一可用的选项,但其成熟性、各种附加组件和扩展,以及优质文档和教程的可用性使其成为一个绝佳的选择。

在本章中,我们可以想象只使用核心 Flask 框架来完成所有以下编码练习;然而,有一些优质的扩展可以让我们的生活变得更加轻松。这些扩展包括用于创建 RESTful API 服务的Flask-RESTful和用于构建 Web Socket 服务的Flask-SocketIO

Flask-RESTful 和 Flask-SocketIO(或任何 Flask 扩展)的官方 API 文档通常假定已经掌握了核心 Flask 框架、类和术语的知识。如果在扩展的文档中找不到问题的答案,请记得查看核心 Flask API 文档。您将在进一步阅读部分找到指向这些文档的链接。

让我们开始,在 Python 中使用 Flask-RESTful 创建一个 RESTful API 服务。

使用 Flask-RESTful 创建 RESTful API 服务

在本节中,我们将探索我们的第一个基于 Python 的服务器,这将是一个使用 Flask-RESTful 框架实现的 RESTful API 服务器。

RESTful API(REST 代表 Representational State Transfer)是一种用于构建 Web 服务 API 的软件设计模式。它是一种灵活的模式,既独立于技术又独立于协议。其技术独立性有助于促进不同技术和系统之间的互操作性,包括不同的编程语言。虽然它确实促进了协议的独立性,但它通常默认情况下是建立在 Web 服务器和 Web 浏览器使用的 HTTP 协议之上的。

RESTful API 是今天构建 Web 服务和 API 最常用的技术。事实上,它是如此常见,以至于许多人在没有理解它们是什么的情况下就学习并使用了这种设计模式!如果您对 RESTful API 还不熟悉,我鼓励您在进一步阅读部分找到一个链接,建议您在继续之前先阅读一下。

本节我们将重点关注使用 Python 和 Flask-RESTful 框架控制 LED 的 RESTful API 以及其实现方式。完成本节后,您将能够将这个 RESTful API 服务器作为自己物联网项目的起点,并将其与其他电子设备集成,特别是在本书第三部分物联网游乐场中学习更多关于电子执行器和传感器的知识。

在本章的示例中,我们假设您正在树莓派上本地工作和访问基于 Flask 的服务器。如果使用树莓派的 IP 地址或主机名,这些服务器也可以从本地网络上的其他设备访问。要使服务器直接通过互联网访问,需要配置特定防火墙和/或路由器,这在本书中实际上是不可行的。对于原型设计和创建演示,一个简单的替代方法是使用 Local Tunnels (localtunnel.github.io/www)或 Ngrok (ngrok.com)等服务,这将帮助您使树莓派上的 Flask 服务器可以通过互联网访问。

我们将首先运行和使用我们的 RESTful API 与 LED 进行交互,然后再审查服务器的源代码。

运行和测试 Python 服务器

您将在chapter03/flask_api_server.py文件中找到代码。在继续之前,请先查看此文件,以了解其包含的内容。

我们正在使用 Flask 内置的 HTTP 服务器运行我们的 Flask 示例。这对于开发目的已经足够了;但是,不建议在生产中使用。请参阅 Flask 文档中的部署选项部分,了解如何使用质量良好的 Web 服务器部署 Flask 应用程序的信息。您将在进一步阅读部分找到指向官方 Flask 网站和文档的链接。

要测试 Python 服务器,请执行以下步骤:

  1. 使用以下命令运行我们的 RESTful API 服务器:
(venv) $ python flask_api_server.py
... truncated ...
NFO:werkzeug: * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
... truncated ...

前面代码块中倒数第二行表示我们的服务器已成功启动。我们的服务器默认以调试模式运行,因此其日志输出将是冗长的,如果您对flask_api_server.py或其他资源文件进行任何更改,服务器将自动重新启动。

如果以调试模式启动flask_api_server.py时出现错误,请清除文件的执行位。这个问题发生在基于 Unix 的系统上,与 Flask 附带的开发 Web 服务器有关。以下是清除执行位的命令:

$ chmod -x flask_api_server.py

  1. 我们将很快创建一个网页来与 API 进行交互;但是,现在,请在 Web 浏览器中浏览到http://localhost:5000,并验证您是否可以使用网页上的滑块来更改 LED 的亮度。

我们的示例 URL 是http://localhost:5000,但是,如果您使用树莓派的 IP 地址而不是localhost,您将能够从本地网络上的其他设备访问该网页。

以下截图是您将看到的网页的示例:

图 3.1 - RESTful API 客户端网页

  1. 我们还可以使用curl命令行工具与 API 进行交互。我们现在将这样做,以观察 API 服务器请求的输入和输出 JSON。

以下是我们的第一个curl命令,它发出 HTTP GET 请求,我们在终端中以 JSON 格式打印 LED 的亮度级别(在 0 到 100 之间的数字)(第 1 行)。服务器启动时的默认 LED 亮度为 50(即 50%亮度):

$ curl -X GET http://localhost:5000/led
{
 "level": 50          # (1)
}  

curl的选项如下:

    • -X GET:用于发出请求的 HTTP 方法
  • :要请求的 URL

  1. 下一个命令执行 HTTP POST 请求,并将亮度级别设置为最大值 100(第 2 行),然后将其作为 JSON 返回并打印回终端(第 3 行):
$ curl -X POST -d '{"level": 100}' \    # (2) 
 -H "Content-Type: application/json" \
 http://localhost:5000/led
{
 "level": 100                            # (3)
}

curl的选项如下:

    • -X POST:这是 HTTP 方法;这次我们正在发出 POST 请求。
  • -d :这是我们要 POST 到服务器的数据。我们正在发布一个 JSON 字符串。

  • -H :这些是要与请求一起发送的 HTTP 标头。在这里,我们让服务器知道我们的数据(-d)是 JSON。

  • :这是要请求的 URL。

命令行上curl的替代方案是 Postman(getpostman.com)。如果您不熟悉 Postman,它是一个免费的 API 开发、查询和测试工具,在您开发和测试 RESTful API 服务时非常有价值。

尝试更改前面curl POST示例中的级别值为超出 0-100 范围的数字,并观察您收到的错误消息。我们很快将看到如何使用 Flask-RESTful 实现此验证逻辑。

让我们现在继续查看我们的服务器源代码。

了解服务器代码

在本节中,我们将浏览我们的 RESTful API 服务器的源代码,并讨论核心部分,以帮助您了解服务器的编码和操作方式。请记住,我们将要涵盖许多特定于 Flask 和 Flask-RESTful 框架的代码级工件,所以如果一开始有些概念不太明确,不要担心。

一旦您了解了基础知识,并对我们的服务器如何工作有了整体的概念,您将能够通过查阅它们各自的网站(您将在进一步阅读部分找到链接)深入了解 Flask 和 Flask-RESTful。此外,您将拥有一个可靠的参考 RESTful API 服务器,可以重新设计并用作将来项目的起点。

请注意,当我们讨论代码时,我们将跳过我们在早期章节中涵盖的任何代码和概念,比如GPIOZero

我们将从导入开始。

导入

在源代码文件的顶部,我们看到以下导入:

import logging
from flask import Flask, request, render_template           # (1)
from flask_restful import Resource, Api, reqparse, inputs   # (2)
from gpiozero import PWMLED, Device                         # (3)
from gpiozero.pins.pigpio import PiGPIOFactory

我们在第(1)和(2)行看到的与 Flask 和 Flask-RESTful 相关的导入都是我们在服务器中需要的 Flask 和 Flask-RESTful 的所有类和函数。您会注意到在第(3)行,我们导入的是PWMLED而不是我们在之前章节中所做的LED。在这个例子中,我们将改变 LED 的亮度而不仅仅是打开和关闭它。随着我们继续本章,我们将更多地涵盖 PWM 和PWMLED

接下来,在我们的源代码中,我们开始使用 Flask 和 Flask-RESTful 扩展。

Flask 和 Flask-RESTful API 实例变量

在接下来的第 4 行,我们创建了核心 Flask 应用的一个实例,并将其分配给app变量。参数是我们的 Flask 应用的名称,使用__name__作为root Flask 应用的常见约定(在我们的示例中只有一个 root Flask 应用)。每当我们需要使用核心 Flask 框架时,我们将使用app变量:

app = Flask(__name__) # Core Flask app.            # (4)
api = Api(app) # Flask-RESTful extension wrapper   # (5)

在第(5)行,我们用 Flask-RESTful 扩展包装核心 Flask 应用,并将其分配给api变量,正如我们很快将看到的,我们在使用 Flask-RESTful 扩展时会使用这个变量。在我们的appapi变量之后,我们定义了额外的全局变量。

全局变量

以下全局变量在整个服务器中使用。首先,我们有 GPIO 引脚和一个led变量,稍后将为其分配一个 GPIOZero PWMLED实例以控制我们的 LED:

# Global variables
LED_GPIO_PIN = 21
led = None # PWMLED Instance. See init_led()
state = {                                     # (6)
    'level': 50 # % brightness of LED.
}

在第(6)行,我们有一个state字典结构,我们将使用它来跟踪 LED 的亮度级别。我们本可以使用一个简单的变量,但选择了字典结构,因为它是一个更多功能的选项,因为它将被编组成 JSON 发送回客户端,我们稍后会看到。

接下来,我们创建并初始化了我们的led实例。

init_led()方法

init_led()方法只是创建一个 GPIOZero PWMLED实例并将其分配给我们之前看到的全局led变量:

def init_led():
    """Create and initialize an PWMLED Object"""
 global led
 led = PWMLED(LED_GPIO_PIN)
 led.value = state['level'] / 100        # (7)

在第(7)行,我们明确将 LED 的亮度设置为与服务器亮度状态的值匹配,以确保服务器的管理状态和 LED 在服务器启动时同步。我们除以 100 是因为led.value期望在 0-1 范围内的浮点值,而我们的 API 将使用 0-100 范围内的整数。

接下来,我们开始看到定义我们的服务器及其服务端点的代码,从提供我们之前访问的网页的代码开始。

提供网页

从第(8)行开始,我们使用 Flask @app.route()装饰器来定义一个回调方法,当服务器从客户端接收到对根 URL /的 HTTP GET 请求时,即对http://localhost:5000的请求时,将调用该方法:

# @app.route applies to the core Flask instance (app).
# Here we are serving a simple web page.
@app.route('/', methods=['GET'])                           # (8)
def index():
 """Make sure index_api_client.html is in the templates folder
 relative to this Python file."""
 return render_template('index_api_client.html',
                          pin=LED_GPIO_PIN)                # (9)

在第(9)行,render_template('index_api_client.html', pin=LED_GPIO_PIN)是一个 Flask 方法,用于向请求的客户端返回一个模板化的页面。pin=LED_GPIO_PIN参数是如何将一个变量从 Python 传递到 HTML 页面模板以进行渲染的示例。我们将在本章后面介绍这个 HTML 文件的内容。

请注意,在第(8)行的前面代码块中,我们有@app.route(...)app变量的存在意味着我们在这里使用和配置核心Flask 框架。

向客户端返回 HTML 页面是我们将在本书中涵盖的唯一核心 Flask 功能,但是在进一步阅读部分中将列出其他资源,供您进一步探索 Flask 的核心概念。

我们代码中的下一个停靠点是LEDController类。在这里,我们正在与 LED 和 GPIOZero 进行交互。

LEDControl 类

在 Flask-RESTful 中,API 资源被建模为扩展Resource类的 Python 类,如下片段的第(10)行中所示,我们看到了定义的LEDControl(Resource)类,其中包含了用于控制我们的 LED 的逻辑。稍后,我们将看到如何注册这个类到 Flask-RESTful,以便它响应客户端请求:

class LEDControl(Resource):                                    # (10)
 def __init__(self):
 self.args_parser = reqparse.RequestParser()            # (11)
 self.args_parser.add_argument(
 name='level',                  # Name of arguement
 required=True,                 # Mandatory arguement
 type=inputs.int_range(0, 100), # Allowed 0..100    # (12) 
 help='Set LED brightness level {error_msg}',
 default=None)

在第(11)行,我们创建了RequestParser()的一个实例,并将其分配给args_parser变量,然后使用add_argument()配置解析器。我们在 Flask-RESTful 中使用RequestParser()的实例来定义我们期望LEDControl资源处理的参数的验证规则。

在这里,我们定义了一个名为level的必需参数,它必须是 0 到 100 范围内的整数,如第(12)行所示。当level参数缺失或超出范围时,我们还提供了一个自定义的帮助消息。

我们将在稍后讨论post()方法时看到args_parser的使用,但首先让我们讨论get()方法。

get()类方法

get()类方法处理我们的LEDControl资源的 HTTP GET 请求。这是我们之前使用以下命令测试 API 时处理我们的 URL 请求的方法:

$ curl -X GET http://localhost:5000/led

get()只是简单地在第(13)行返回全局state变量:

 def get(self):
 """ Handles HTTP GET requests to return current LED state."""
 return state         # (13)

Flask-RESTful 将 JSON 响应返回给客户端,这就是为什么我们返回state变量。在 Python 中,state是一个可以直接映射到 JSON 格式的字典结构。我们之前在使用curl进行 GET 请求时看到了以下 JSON 示例:

{ "level": 50 }

这种类作为资源(例如LEDControl)和方法到 HTTP 方法的映射(例如LEDControl.get())是 Flask-RESTful 扩展如何使 RESTful API 开发变得简单的一个例子。

还有其他 HTTP 请求方法保留的方法名称,包括我们接下来要讨论的 POST。

post()类方法

post()类方法处理发送到LEDControl资源的 HTTP POST 请求。正是这个post()方法接收并处理了我们之前使用curl POST请求时所做的请求:

curl -X POST -d '{"level": 100}' \
 -H "Content-Type: application/json" \
 http://localhost:5000/led

post()比我们的get()方法更复杂。这是我们根据请求客户端的输入更改 LED 亮度的地方:

 def post(self):
 """Handles HTTP POST requests to set LED brightness level."""
 global state                                            # (14)

 args = self.args_parser.parse_args()                    # (15)

 # Set PWM duty cycle to adjust brightness level.
 state['level'] = args.level                             # (16)
 led.value = state['level'] / 100                        # (17)
 logger.info("LED brightness level is " + str(state['level']))

 return state                                            # (18)

在第(14)行,我们使用 Python 的global关键字表示我们将修改state全局变量。

在第(15)行,我们看到了我们之前讨论过的args_parser的使用。这是对args_parser.parse_args()的调用,它将解析和验证调用者的输入(记住level是一个必需参数,必须在 0-100 的范围内)。如果我们预定义的验证规则失败,用户将收到一个错误消息,并且post()将在此终止。

如果参数有效,它们的值将存储在args变量中,并且代码将继续到第(16)行,我们在那里使用新请求的亮度级别更新全局state变量。在第(17)行,我们使用 GPIOZero PWMLED 实例led来改变物理 LED 的亮度,该实例期望一个值在 0.0(关闭)和 1.0(全亮)之间,因此我们将 0-100 的level输入范围映射回 0-1。state的值在第(18)行返回给客户端。

我们的最后任务是将LEDController注册到 Flask-RESTful 并启动服务器。

LEDController 注册和启动服务器

在调用init_led()方法来初始化和默认输出 GPIOZero led实例之后,我们看到了如何在第(19)行使用api.add_resource()注册我们的LEDControl资源。在这里,我们将 URL 端点/led与我们的控制器进行了映射。

请注意,在第(19)行的代码块中,我们有api.add_resource(...)api变量的存在意味着我们在这里使用和配置Flask-RESTful 扩展

最后,在第 20 行,我们的服务器已经启动(调试模式),准备接收客户端请求。请注意,我们使用app变量中的core Flask 实例来启动服务器:

# Initialize Module.
init_led()
api.add_resource(LEDControl, '/led')          # (19)

if __name__ == '__main__':
 app.run(host="0.0.0.0", debug=True)       # (20)

干得好!我们刚刚在 Python 中完成了一个简单但功能齐全的 RESTful API 服务器的构建。您将在进一步阅读部分找到官方 Flask-RESTful 文档的链接,以便进一步扩展您的知识。

如前所述,我们在服务器中使用了PWMLED。在继续之前,让我们简要介绍一下PWM这个术语,并回顾一下伴随我们的 RESTful API 服务器的网页。

PWM 简介

在前面的示例中,我们使用了 GPIOZero 中的PWMLED,而不是LEDPWMLED允许我们使用一种称为脉冲宽度调制的技术来控制 LED 的亮度,通常缩写为PWM

PWM 是一种用于从源信号(可以是 3.3 伏特的 GPIO 引脚)中创建平均电压的技术。我们将在第六章中详细介绍 PWM 和 GPIO 引脚电压,软件工程师的电子学 101

对于我们当前的示例,简而言之(有些过于简化),PWM 以非常快的速度打开和关闭 LED,我们的眼睛观察到不同的脉冲持续时间(产生不同的电压),表现为 LED 的不同亮度级别。我们使用PWMLED实例的value属性来改变这个脉冲持续时间(称为duty-cycle),即在LEDControl.post()中的led.value = state["level"]。在第五章中,将您的树莓派连接到物理世界,我们将更详细地探讨 PWM。

我们现在已经完成了基于 Python 的 Flask-RESTful API 服务器,并学会了如何实现一个简单而功能齐全的 RESTful API 服务器,能够处理 GET 和 POST 请求,这是与 RESTful API 服务器交互的两种最流行的方式。此外,我们还看到了如何使用 Flask-RESTful 实现数据验证,这是一种简单有效的方式,可以保护我们的服务器免受无效输入数据的影响。

我们还学会了使用curl命令行工具与测试我们的服务器进行交互。在构建、测试和调试 RESTful API 服务器时,您会发现curl是开发工具包中的一个有用补充。

接下来,我们将看一下与我们的 API 交互的网页背后的代码。

添加 RESTful API 客户端网页

我们将讨论的网页是您之前与之交互以改变 LED 亮度的网页,当您在 Web 浏览器中访问http://localhost:5000时。网页的截图显示在图 3.1中。

在本节中,我们将学习如何使用 HTML 和 JavaScript 构建这个基本网页。我们将发现如何使 HTML 范围组件与我们在上一节中创建的 Flask-RESTful API 服务器进行交互,以便当我们改变范围控件(即滑动滑块)时,我们 LED 的亮度也会改变。

您将在chapter03/templates/index_api_client.html文件中找到网页的代码。在继续之前,请先查看此文件,以了解其包含的内容。

templates文件夹是 Flask 中的一个特殊文件夹,用于存放模板文件。在 Flask 生态系统中,HTML 页面被视为模板。您还会发现一个名为static的文件夹。这个文件夹是用来存放静态文件的地方。对于我们的示例,这是 jQuery JavaScript 库文件的副本所在的地方。

从 Flask 提供的网页中引用的所有文件和资源都是相对于服务器的根文件夹的。对于我们来说,这是chapter03文件夹。

让我们走一遍网页代码。

理解客户端代码

本节的代码是 JavaScript,并且我们将使用 jQuery JavaScript 库。了解基本的 JavaScript 和 jQuery 对于理解接下来的代码示例至关重要。如果您不熟悉 jQuery,可以在 jQuery.com 找到学习资源。

JavaScript 导入

我们在下面看到,第 1 行导入了包含在static文件夹中的 jQuery 库:

<!-- chapter03/templates/index_api_client.html -->
<!DOCTYPE html>
<html>
<head>
 <title>Flask Restful API Example</title>
 <script src="/static/jquery.min.js"></script>    <!--(1)-->
 <script type="text/javascript">

接下来,我们将开始介绍文件中的 JavaScript 函数。

getState()函数

getState()的主要目的是从服务器检索 LED 的当前状态。它使用 JQuery 的get()方法向我们的 API 服务器的/led资源发出 HTTP GET 请求。我们在上一节中看到,URL 路径/led映射到LEDControlPython 类,因为我们正在进行 GET 请求,所以LEDControl.get()将接收和处理我们的请求:

// GET request to server to retrieve LED state.
function getState() { 
 $.get("/led", function(serverResponse, status) { // (2)
 console.log(serverResponse) 
 updateControls(serverResponse)                // (3)
 }); }

服务器的响应包含在第 2 行的serverResponse参数中,它传递给第 3 行的updateControls()函数以更新网页控件。我们将很快介绍这个方法。

虽然getState()从我们的 Python 服务器获取数据,但我们的下一个方法postUpdate()发送(即*发布)数据到服务器。

postUpdate()函数

postUpdate()通过执行 HTTP POST 到服务器来改变 LED 的亮度。这次,在我们的 API 服务器中处理请求的是LEDControl.post()方法:

// POST Request to server to set LED state.
function postUpdate(payload) {                          // (4)
 $.post("/led", payload, function(serverResponse, status) {
 console.log(serverResponse)
 updateControls(serverResponse);                 // (5)
 });
}

在第 4 行,它接收并解析(记住arg_parser来自LEDControlpayload参数中的数据。payload是一个具有state子属性的 JavaScript 对象。我们将在稍后看到这个对象是如何在网页滑块的 change 事件处理程序中构造的。

为了保持一致,即使在我们的情况下,serverResponse变量将包含与payload参数相同的级别值,我们也会更新第 5 行的控件。

接下来,我们将看到第 5 行对updateControls()的调用做了什么。

updateControls()函数

updateControls()改变了网页控件的视觉外观。这个函数接收 JSON 输入作为data参数,格式为{"level":50}。从第 6 行开始,使用 jQuery 选择器,我们更新了网页上的滑块控件和文本,以反映新的级别值:

function updateControls(data) {
 $("input[type=range].brightnessLevel").val(data.level);  // (6)
 $("#brightnessLevel").html(data.level);
}

接下来,我们将看到如何使用 JQuery 创建一个事件处理程序,当我们或另一个用户更改网页的滑块组件时,它会做出响应。

使用 jQuery 注册事件处理程序

我们遵循 jQuery 最佳实践,并使用 jQuery 的文档就绪函数(即$(document).ready(...))来注册我们网页的滑块控件的事件处理程序并初始化我们的网页元素:

 $(document).ready(function() {
 // Event listener for Slider value changes.
 $("input[type=range].brightnessLevel")
 .on('input', function() {                    // (7)
 brightness_level = $(this).val();         // (8)
 payload = { "level": brightness_level }   // (9)
 postUpdate(payload);
 });

        // Initialize slider value form state on server.
 getState()                                       // (10)
 });
 </script>
</head>

在第 7 行,我们为滑块控件注册了一个input事件的事件处理程序。当用户与网页上的滑块交互时,将调用此处理程序函数。

从第 8 行开始,用户移动滑块后,我们使用val()提取滑块的新值(在 0 到 100 之间,我们稍后会在查看页面的 HTML 时看到原因)。

在第 9 行,我们创建一个包含我们新亮度级别的 JSON 对象,然后将其传递给postUpdate(),该函数调用我们的 RESTful API 来改变物理 LED 的亮度。

最后,在第 10 行,我们调用我们的getState()函数,它向服务器发出 HTTP 请求,以获取 LED 的当前亮度级别。正如我们之前看到的,getState()然后委托给updateControls(),然后更新滑块和页面文本以反映 LED 的亮度值。

我们将通过查看组成网页的 HTML 来结束本节。

网页 HTML

在我们的 Python 服务器中,我们之前有一行render_template('index_rest_api.html', pin=LED_GPIO_PIN)。在这个方法调用中,pin参数在第 11 行呈现在我们的网页上,由模板变量{{pin}}表示:

<body>
 <h1>Flask RESTful API Example</h1>
 LED is connected to GPIO {{pin}}<br>               <!--(11)-->
 Brightness: <span id="brightnessLevel"></span>%<br>
 <input type="range" min="0" max="100"              <!--(12)-->
 value="0" class="brightnessLevel">
</body>
</html>

最后,我们看到,在第 12 行,我们的 HTML 滑块组件被限制在 0-100 的范围内。正如我们之前看到的,是在文档准备好的处理程序中对getState()的调用更新了滑块的值属性,以匹配网页加载完成后服务器上存储的亮度级别。

恭喜!我们现在已经达到了一个里程碑,完成了一个基于 RESTful API 的完整端到端服务器和客户端示例。我们对 Flask 和 Flask-RESTful 的学习意味着我们已经学会了使用最受欢迎和功能丰富的 Python 库之一来构建 Web 服务。此外,学习构建 RESTful API 服务器和匹配的客户端意味着我们已经实际实现了当今用于客户端-服务器通信的最常见方法。

我们只是触及了 Flask、Flask-RESTful 和 RESTful API 的一小部分,还有很多可以探索的内容。如果你希望进一步了解这些主题,你会在进一步阅读部分找到链接。

接下来,我们将创建与本节中构建的相同的客户端和服务器场景,只是这一次使用 Web 套接字作为我们的传输层。

使用 Flask-SocketIO 创建 Web 套接字服务

我们现在将实现我们的第二个基于 Python 的服务器。在本节中,我们的总体目标与上一节中创建的 RESTful API 服务器和客户端类似,也就是说,我们将能够从 Web 浏览器控制我们的 LED。然而,这一次我们的目标是使用不同的技术方法,使用 Web 套接字作为我们的传输层。

Web 套接字是一种全双工通信协议,是实时客户端/服务器交互所需的常见技术选择。在我看来和经验中,Web 套接字最好通过实践而不是阅读来学习,特别是如果你是新手服务器开发者。对 Web 套接字的深入讨论超出了本章的范围;但是,在进一步阅读部分,你会找到两个链接,涵盖了基础知识。

如果你是 Web 套接字的新手,我强烈建议在继续之前阅读这两个资源作为入门。如果内容一开始没有理解,不要担心,因为我相信一旦你使用并理解了我们的 Python Web 套接字服务器和配套的 Web 套接字启用的网页是如何实现的,更大的 Web 套接字拼图的各个部分将开始串联起来。

对于我们的 Web 套接字服务器构建,我们将使用 Flask-SocketIO 库,该库是基于并兼容流行的 JavaScript 库 Socket.IO (socket.io)。

我们将首先运行和使用我们的 Web 套接字服务器与 LED 进行交互,然后再审查服务器的源代码。

运行和测试 Python 服务器

让我们首先快速查看一下我们的 Python Web 套接字服务器代码,并运行服务器以查看其运行情况。在我们详细讨论之前,这将给我们一个对代码的大致了解和对代码工作原理的第一手演示。

你会在chapter03/flask_ws_server.py文件中找到 Web 套接字服务器的代码。请在继续之前查看这个文件。

当你查看了代码后,我们将运行我们的服务器。以下是要遵循的步骤:

  1. 使用以下命令运行 Web 套接字服务器:
(venv) $ python flask_ws_server.py
... truncated ...
NFO:werkzeug: * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
... truncated ...

前面的输出与我们运行 RESTful API 服务器时看到的类似;但是,对于这个服务器,你可以在终端上期望看到更多的输出消息。你将看到的额外输出已经从前面的示例中截断。

如果在调试模式下启动flask_ws_server.py时出现错误,请清除文件的执行位。这个问题发生在基于 Unix 的系统上,与 Flask 附带的开发 Web 服务器有关。这里是清除执行位的命令:

$ chmod -x flask_ws_server.py

  1. 在网页浏览器中访问http://localhost:5000 URL。你会得到一个带有滑块的网页,如图 3.2所示。虽然网页的视觉外观与 RESTful API 服务器的网页相似,但底层的 JavaScript 是不同的:

图 3.2 - Web Socket 客户端网页

验证你是否可以使用网页上的滑块来改变 LED 的亮度。

打开第二个网页浏览器并访问http://localhost:5000(现在你有两个页面打开)。改变滑块,你会看到两个页面保持同步并实时更新!神奇的是,你已经发现了 Web Sockets 相对于 RESTful API 所提供的独特优势。

  1. 在网页上找到连接到服务器的行:Yes,然后执行以下操作:
  • 在终端中按下Ctrl + C来终止服务器,你会注意到行变成了连接到服务器:否。

  • 再次重新启动服务器,它会变成连接到服务器:Yes。

这说明了 Web Sockets 的双向性质。当我们审查它的 JavaScript 时,我们将看到它是如何实现的,但首先,我们将审查构成我们的 Web Socket 服务器的 Python 代码。

服务器代码演示

在本节中,我们将浏览我们的 Python 服务器源代码并讨论核心部分。同样,我们将跳过我们在早期章节中涵盖的任何代码和概念。首先,让我们看看我们正在导入什么。

导入

在源文件的顶部附近,我们有以下的导入:

from flask import Flask, request, render_template
from flask_socketio import SocketIO, send, emit            # (1)

与 RESTful API 导入相比,关于我们之前的导入的主要区别在于第(1)行,我们现在从 Flask-SocketIO 导入类和函数。

接下来,在我们的源代码中,我们开始使用 Flask 和 Flask-SocketIO 扩展。

Flask 和 Flask-RESTful API 实例变量

在第(2)行,我们创建了一个SocketIO的实例和 Flask-SocketIO 扩展,并将其赋值给socketio变量。这个变量将在整个服务器中使用,以访问和配置我们的 Web Socket 服务。

# Flask & Flask Restful Global Variables.
app = Flask(__name__) # Core Flask app.
socketio = SocketIO(app) # Flask-SocketIO extension wrapper  # (2)

在创建了 SocketIO 实例之后,我们再次从默认的 URL 端点/提供一个网页。

提供一个网页

与 RESTful API 示例类似,我们配置核心 Flask 框架,使用@app.route()装饰器从根 URL 提供一个网页:

@app.route('/', methods=['GET'])
def index():
 """Make sure index_web_sockets.html is in the templates folder
 relative to this Python file."""
 return render_template('index_web_sockets.html',         # (3)
 pin=LED_GPIO_PIN) 

对于我们的 Web Socket 服务器,这一次,我们提供的是 HTML 文件index_web_sockets.html,我们将在下一节中介绍,添加 Web Socket 客户端网页

接下来,我们开始看到设置和处理 Web Socket 事件消息的代码。

连接和断开处理程序

从代码的这一点开始,我们开始看到 RESTful API 服务器和这个 Web Socket 服务器之间的主要区别:

# Flask-SocketIO Callback Handlers
@socketio.on('connect')                                     # (4)
def handle_connect():
 logger.info("Client {} connected.".format(request.sid)) # (5)

    # Send initializating data to newly connected client.
 emit("led", state)                                     # (6)

我们看到,在第(4)行,如何使用 Python 装饰器符号注册messageevent处理程序。每个@socketio.on(<event_name>)的参数是我们的服务器将监听的事件的名称。connectdisconnect事件(在下面)是两个保留事件。每当客户端连接到服务器或断开连接时,这些处理程序都会被调用。

你会注意到,在第(5)行,我们在客户端连接时进行了日志记录,以及通过request.sid访问的客户端的唯一标识符。服务器与客户端的每个会话都会收到一个唯一的 SID。当你访问http://localhost:5000时,你会看到服务器记录这个连接消息。如果你在这个 URL 上打开两个或更多的网页浏览器(或标签),你会注意到每个会话都会收到一个唯一的 SID。

在第(6)行,我们emit当前 LED 状态回到连接的客户端,以便它可以根据需要初始化自己:

@socketio.on('disconnect')                               # (7) 
def handle_disconnect():
 """Called with a client disconnects from this server"""
 logger.info("Client {} disconnected.".format(request.sid))

我们在第(7)行的断开处理程序只是记录了客户端断开的事实。当你离开http://localhost:5000时,你会注意到服务器记录这条消息,以及断开连接的客户端的sid

接下来,我们遇到了控制我们的 LED 的事件处理程序。

LED 处理程序

在以下的第 8 行,我们有另一个消息处理程序——这次使用了一个名为led的自定义事件。还请注意在第 9 行,这个事件处理程序有一个data参数,而在前面的部分中连接和断开连接处理程序没有参数。data参数包含从客户端发送的数据,我们在第 10 行看到了datalevel子属性。所有来自客户端的数据都是字符串,所以在这里我们验证数据并在下一行将其转换为整数。在 Flask-SocketIO 中没有等效的内置参数验证和解析实用程序,因此我们必须手动执行验证检查,就像从第 11 行开始所示的那样:

@socketio.on('led')                                 # (8)
def handle_state(data):                             # (9)
 """Handle 'led' messages to control the LED."""
 global state
 logger.info("Update LED from client {}: {} "
 .format(request.sid, data)) 

 if 'level' in data and data['level'].isdigit(): # (10)
 new_level = int(data['level'])

 # Range validation and bounding.            # (11)
 if new_level < 0:
 new_level = 0
 elif new_level > 100:
 new_level = 100

在以下代码块中,第 12 行,我们设置 LED 的亮度。在第 13 行,我们看到了emit()方法的服务器端使用。这个方法调用发出一条消息给一个或多个客户端。"led"参数是将被客户端消耗的事件的名称。我们将 LED 控制相关的客户端和服务器端事件都称为相同的名称,ledstate参数是要传递给客户端的数据。与 RESTful API 服务器类似,它是一个 Python 字典对象。

broadcast=True参数意味着这个led消息将被发送到所有连接的客户端,而不仅仅是在服务器上发起led消息的客户端。这个事件的广播是为什么当您打开多个网页并在其中一个上更改滑块时,其他网页也会保持同步的原因:

 led.value = new_level / 100               # (12)
 logger.info("LED brightness level is " + str(new_level))

 state['level'] = new_level

 # Broadcast new state to *every* 
 # connected connected (so they remain in sync)
 emit("led", state, broadcast=True)            # (13)

我们的最后任务是讨论如何启动我们的 Web Socket 服务器。

启动服务器

最后,我们在第 14 行启动服务器。这一次,我们使用的是 Flask-SocketIO 实例socketio,而不是核心 Flaskapp实例,就像我们为 RESTful API 服务器所做的那样:

if __name__ == '__main__':
 socketio.run(app, host="0.0.0.0", debug=True)  # (14)

干得好!这就是我们的 Web Socket 服务器完成了。

我们现在已经看到了如何使用 Python 和 Flask-SocketIO 构建 Web Socket 服务器。虽然我们的 Web Socket 服务器实现的整体结果类似于我们的 RESTful API 服务器控制 LED,但我们学到的是实现相同结果的不同方法。此外,除此之外,我们展示了 Web Socket 方法提供的一个特性,即我们如何保持多个网页同步!

您将在进一步阅读部分找到到 Flask-SocketIO 文档的链接,这样您就可以进一步加深您的知识。

现在我们已经看到了 Web Socket 服务器的 Python 服务器实现,接下来我们将把注意力转向网页的 Web Socket 版本。

添加 Web Socket 客户端网页

在这一部分,我们将审查我们用于从 Web Socket 服务器控制 LED 的 HTML 网页。这个页面的示例如图 3.2所示。

我们将学习如何在网页中使用 Socket.IO JavaScript 库,以便我们可以与我们的 Python Flask-SocketIO Web Socket 服务器发送和接收消息(当我们在 Web Socket 环境中工作时,我们倾向于将数据称为消息)并从中接收。此外,当我们探索 JavaScript 和 Socket.IO 相关的代码时,我们将发现我们的客户端 JavaScript 代码如何与我们的 Python 服务器端代码相关联。

您将在chapter03/templates/index_ws_client.html文件中找到以下网页的代码。请审查此文件的内容,以获得对其包含的内容的广泛概述。

当您已经审查了我们的 HTML 文件,我们将继续讨论这个文件的重要部分。

理解客户端代码

现在您已经浏览了chapter03/templates/index_ws_client.html文件,是时候讨论这个文件是如何构建的以及它的作用了。我们将从我们需要用于 Web Socket 支持的额外 JavaScript 导入开始我们的代码漫游。

导入

我们的 Web Socket 客户端需要 Socket.IO JavaScript 库,在第 1 行看到了这个导入。如果您想了解更多关于这个库以及它的工作原理,您可以在进一步阅读部分找到 Socket.IO JavaScript 库的链接:

<!-- chapter03/templates/index_ws_client.html -->
<!DOCTYPE html>
<html>
<head>
 <title>Flask Web Socket Example</title>
 <script src="/static/jquery.min.js"></script>
 <script src="/static/socket.io.js"></script>   <!-- (1) -->
 <script type="text/javascript">

在导入之后,我们将看到与我们的 Python Web Socket 服务器集成的 JavaScript。

Socket.IO 连接和断开处理程序

在文件的<script>部分,第 2 行,我们创建了一个socket.io JavaScript 库的io()类的实例,并将其赋值给socket变量:

 var socket = io();                         // (2)

 socket.on('connect', function() {          // (3)
 console.log("Connected to Server");
 $("#connected").html("Yes");
 });

 socket.on('disconnect', function() {       // (4)
 console.log("Disconnected from the Server");
 $("#connected").html("No");
 });

在第 3 行,通过socket.on('connect', ...),我们注册了一个connect事件监听器。每当我们的网页客户端成功连接到我们的 Python 服务器时,这个处理程序都会被调用。这是客户端等价于我们用@socketio.on('connect')定义的 Python 服务器端的 on connect 处理程序。

在第 4 行,我们看到了disconnect处理程序,每当客户端网页失去与服务器的连接时都会被调用。这是客户端等价于 Python 服务器端@socketio.on('disconnect')处理程序。

请注意,在这两个处理程序中,我们更新我们的网页以指示它是否与服务器建立了连接。我们之前在终止并重新启动服务器时看到了这一操作。

接下来,我们有一个与我们的 LED 相关的处理程序。

关于 LED 处理程序

在第 5 行,我们有我们的led消息处理程序,负责更新 HTML 控件与我们 LED 的当前亮度级别:

socket.on('led', function(dataFromServer) {         // (5)
 console.log(dataFromServer)
 if (dataFromServer.level !== undefined) {
 $("input[type=range].brightnessLevel").val(dataFromServer.level);
 $("#brightnessLevel").html(dataFromServer.level);
 }
});

如果您回顾 Python 服务器的@socketio.on('connect')处理程序,您会注意到它包含了emit("led", state)这一行。当新客户端连接到服务器时,它会发送一个消息给连接的客户端,其中包含我们 LED 的当前状态。JavaScript 的socket.on('led', ...)部分在第 5 行消耗了这个消息。

接下来,我们有 jQuery 文档准备好回调。

文档准备好函数

jQuery 文档准备好回调是我们为 HTML 滑块设置事件处理程序的地方:

 $(document).ready(function(){
 // Event listener for Slider value changes.
 $("input[type=range].brightnessLevel")
              .on('input', function(){
 level = $(this).val();
 payload = {"level": level};
 socket.emit('led', payload);         // (6)
 });
 });
 </script>
</head>

在第 6 行,我们看到了如何在 JavaScript 中发送一个消息。调用socket.emit('led', payload)向 Python 服务器发送了一个我们想要应用到 LED 的亮度级别的消息。

这是 Python 的@socketio.on('led')处理程序,它接收这个消息并改变 LED 的亮度。

如果您回顾这个 Python 处理程序,您会注意到这一行:emit("led", state, broadcast=True)。这一行向所有连接的客户端广播了一个包含新 LED 状态的消息。每个客户端的socket.on('led', ...)处理程序都会消耗这个消息,并相应地同步他们的滑块。

最后,我们有构成我们网页的 HTML。

网页 HTML

与 RESTful API 网页唯一的区别是在第 7 行包含了一个消息,指示我们是否与 Python 服务器建立了连接:

<body>
 <h1>Flask Web Socket Example</h1>
 LED is connected to GPIO {{pin}}<br>
 Connected to server: <span id="connected">No</span> <!-- (7) -->
 <br><br>
 Brightness <span id="brightnessLevel"></span>:<br>
 <input type="range" min="0" max="100" 
 value="0" class="brightnessLevel">
</body>
</html>

恭喜!这是两个使用两种不同传输层的 Python 服务器和网页客户端。

我们已经看到了如何使用基于 RESTful API 的方法和基于 Web Sockets 的方法来实现相同的项目,以控制 LED 的亮度。这两种方法是实现 Web 服务和将网页(或任何客户端)集成到后端服务器的两种常见选项,因此了解和欣赏这两种技术是有用的,这样您就可以选择最适合自己应用的技术,或者在尝试理解现有应用的实现方式时使用。

让我们通过比较方法并了解更多关于每种方法最适合哪些问题领域来回顾我们所涵盖的内容。

比较 RESTful API 和 Web Socket 服务器

基于 RESTful 的 API 在概念上类似于设计、开发和测试,并且更常见于互联网上,需要一种单向请求/响应数据交换。

以下是这种方法的一些定义特征:

  • 通信协议是围绕 HTTP 方法构建的,GET、POST、PUT 和 DELETE 是最常见的。

  • 协议是半双工的请求-响应形式。客户端发出请求,服务器响应。服务器不能向客户端发起请求。

  • 我们有多种选择,包括命令行上的curl和诸如 Postman 之类的 GUI 工具来测试和开发 RESTful API。

  • 我们可以使用通用的网络浏览器来测试 HTTP GET API 端点

  • 在 Python 中,我们可以使用 Flask-RESTful 扩展来帮助我们构建 RESTful API 服务器。我们将端点建模为具有类方法(例如.get().post())的 Python 类,这些方法与 HTTP 请求方法匹配。

  • 对于网页客户端,我们可以使用 jQuery 等库向我们的 Python 服务器发出 HTTP 请求。

另一方面,Web Sockets 通常出现在聊天应用和游戏中,需要实时的双向数据交换,通常有许多同时在线的客户端。

以下是这种方法的一些定义特征:

  • 通信协议基于发布和订阅消息。

  • 协议是全双工的。客户端和服务器都可以相互发起请求。

  • 在 Python 中,我们可以使用 Flask-SocketIO 扩展来帮助我们创建 Web Socket 服务。我们创建方法并将它们指定为消息事件的回调处理程序。

  • 对于网页客户端,我们使用socket.io JavaScript 库。与 Python 类似,我们创建常见的 JavaScript 函数,并将它们注册到socket.io作为消息事件的回调处理程序。

有一个方法比另一个更好吗?没有单一的最佳或一刀切的方法,因此选择物联网应用的网络方法在很大程度上取决于您正在创建什么以及客户端将如何连接和使用您的应用。如果您是新手构建网络应用和 Web 服务,那么在学习概念和实验的同时,使用 Flask-RESTful 的 RESTful API 是一个很好的起点。这是一个非常常见和广泛使用的方法,而且如果您在开发时使用像 Postman(getpostman.com)这样的工具作为您的 API 客户端,那么您将有一个强大而快速的方式来玩耍和测试您创建的 API。

摘要

在本章中,我们介绍了使用 Python 构建网络服务的两种常见方法——RESTful API 和 Web Socket 服务。我们使用 Flask 微服务框架以及 Flask-RESTful 和 Flask-SocketIO 扩展在 Python 中构建了这些服务。在创建每个服务器之后,我们还创建了网页客户端。我们学会了如何使用 JavaScript jQuery 库发出 RESTful API 请求,以及使用 Socket.IO JavaScript 库执行 Web Socket 消息和订阅。

有了这些新知识,您现在已经掌握了使用 Python、HTML、JavaScript 和 jQuery 构建的基础和简单的端到端客户端-服务器框架,您可以扩展和实验,以创建更宏伟的物联网应用。例如,当您通过本书的第三部分《物联网游乐场》学习不同的电子传感器和执行器时,您将能够使用不同的电子元件扩展和构建本章的示例。当我们到达《第十四章》《将一切联系在一起-物联网圣诞树》时,我们将看到另一个 Flask-RESTful 和 RESTful API 的示例,介绍一个与 LED 灯带和伺服互动的网页。

在《第四章》《使用 MQTT、Python 和 Mosquitto MQTT Broker 进行网络》,我们将看到一种更高级和非常多才多艺的方法来构建物联网应用的网络层,这次是使用 MQTT,即消息队列遥测传输协议。

问题

最后,这里是一些问题列表,供您测试对本章材料的了解。您将在书的评估部分找到答案:

  1. Flask-RESTful 扩展的哪个特性可以帮助我们验证客户端的输入数据?

  2. 什么通信协议可以用于提供客户端和服务器之间的实时全双工通信?

  3. 我们如何使用 Flask-SocketIO 进行请求数据验证?

  4. Flask 的templates文件夹是什么?

  5. 在使用 jQuery 时,我们应该在哪里创建组件事件监听器并初始化我们的网页内容?

  6. 什么命令行工具可以用来向 RESTful API 服务发出请求?

  7. 当我们改变PWMLED实例的value属性时,物理 LED 会发生什么?

进一步阅读

在本章中我们经常提到“RESTful”这个词,但没有深入讨论它的确切含义。如果您想了解所有细节,可以在 SitePoint.com 上找到一篇很好的入门教程。

我们的 RESTful API 示例几乎没有涉及 Flask 和 Flask-RESTful 的基础知识,但提供了一个可以构建的工作示例。我鼓励您至少阅读 Flask 快速入门指南,然后再阅读 Flask RESTful 快速入门指南,以获得对这两个框架的良好基础和理解:

正如在本章中所述,在标题为介绍 Flask 微服务框架的部分中,如果您在 Flask-RESTful 中遇到困难并且在其文档中找不到答案,您还应该参考官方的核心 Flask 文档:

我们还只是初步了解了使用 Flask-SocketIO 和 Socket.IO 的 Web 套接字。以下链接指向官方的 Flask-SocketIO 和 Socket.IO 库。我还包括了两个额外的链接,提供了关于 Web 套接字的通用和简单介绍。作为提醒,Web 套接字是一种最好通过实践而不是阅读来学习的技术,特别是对于新手服务器开发者来说。因此,当您阅读关于 Web 套接字的入门材料时,期望核心潜在概念将通过各种不同的代码示例和库来进行说明,除了我们在本章中使用的 Flask-SocketIO 和 Socket.IO 库:

第四章:使用 MQTT,Python 和 Mosquitto MQTT 代理进行网络连接

在上一章中,我们使用 RESTful API 和 Web Socket 方法创建了两个 Python 服务器和相应的网页。在本章中,我们将涵盖另一种在物联网世界中常见的网络拓扑,称为MQTT消息队列遥测传输

我们将首先设置您的开发环境,并在树莓派上安装 Mosquitto MQTT 代理服务。然后,我们将使用 Mosquitto 附带的命令行工具学习 MQTT 的特性,以帮助您单独理解核心概念。之后,我们将进行一个使用 MQTT 作为其消息传输层的 Python 物联网应用程序,是的,它将完全关于 LED 的控制!

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

  • 安装 Mosquitto MQTT 代理

  • 通过示例学习 MQTT

  • 介绍 Python Paho-MQTT 客户端库

  • 使用 Python 和 MQTT 控制 LED

  • 构建基于 Web 的 MQTT 客户端

技术要求

要执行本章的练习,您需要以下内容:

  • 树莓派 4 型 B 型号

  • Raspbian OS Buster(带桌面和推荐软件)

  • 至少 Python 版本 3.5

这些要求是本书中代码示例的基础。可以合理地期望代码示例应该可以在树莓派 3 型 B 型或不同版本的 Raspbian OS 上无需修改地运行,只要您的 Python 版本是 3.5 或更高。

您可以在以下 URL 的 GitHub 存储库的chapter04文件夹中找到本章的源代码:github.com/PacktPublishing/Practical-Python-Programming-for-IoT

您需要在终端中执行以下命令,以设置虚拟环境并安装本章代码所需的 Python 库:

$ cd chapter04              # Change into this chapter's folder
$ python3 -m venv venv      # Create Python Virtual Environment
$ source venv/bin/activate  # Activate Python Virtual Environment
(venv) $ pip install pip --upgrade        # Upgrade pip
(venv) $ pip install -r requirements.txt  # Install dependent packages

requirements.txt中安装以下依赖项:

我们将使用我们在第二章中创建的面包板电路进行工作,使用 Python 和物联网入门图 2.7

安装 Mosquitto MQTT 代理

MQTT,或消息队列遥测传输,是一种专门针对物联网应用的轻量级和简单的消息传输协议。虽然树莓派足够强大,可以利用更复杂的消息传输协议,但如果您将其用作分布式物联网解决方案的一部分,很可能会遇到 MQTT;因此,学习它非常重要。此外,它的简单性和开放性使其易于学习和使用。

我们将使用一个名为Mosquitto的流行开源 MQTT 代理来进行 MQTT 的介绍,并将其安装在您的树莓派上。

本章涵盖的示例是使用 Mosquitto 代理和客户端版本 1.5.7 执行的,这是 MQTT 协议版本 3.1.1 兼容的。只要它们是 MQTT 协议版本 3.1.x 兼容的,代理或客户端工具的不同版本都将适用。

要安装 Mosquitto MQTT 代理服务和客户端工具,请按照以下步骤进行:

  1. 打开一个新的终端窗口并执行以下apt-get命令。这必须使用sudo执行:
$ sudo apt-get --yes install mosquitto mosquitto-clients
... truncated ...
  1. 要确保 Mosquitto MQTT 代理服务已启动,请在终端中运行以下命令:
$ sudo systemctl start mosquitto
  1. 使用以下service命令检查 Mosquitto 服务是否已启动。我们期望在终端上看到active (running)文本打印出来:
$ systemctl status mosquitto
... truncated ...
 Active: active (running)
... truncated ...
  1. 我们可以使用mosquitto -h命令检查 Mosquitto 和 MQTT 协议版本。 在这里,我们看到 Mosquitto 代理使用的是 MQTT 版本 3.1.1:
$ mosquitto -h
mosquitto version 1.5.7
mosquitto is an MQTT v3.1.1 broker.
... truncated ...
  1. 接下来,我们将配置 Mosquitto,以便它可以提供网页并处理 Web 套接字请求。 当我们在本章后面构建网页客户端时,我们将使用这些功能。

chapter4文件夹中,有一个名为mosquitto_pyiot.conf的文件,这里部分复制了该文件。 此文件中有一行我们需要检查的内容:

# File: chapter04/mosquitto_pyiot.conf
... truncated...
http_dir /home/pi/pyiot/chapter04/mosquitto_www

对于本章的练习,您需要更新最后一行的http_dir设置,使其成为树莓派上chapter04/mosquitto_www文件夹的绝对路径。 如果您在第一章*,设置您的开发环境*中克隆 GitHub 存储库时使用了建议的文件夹/home/pi/pyiot,那么先前列出的路径是正确的。

  1. 接下来,我们使用以下cp命令将mosquitto_pyiot.conf中的配置复制到适当的文件夹中,以便 Mosquitto 可以加载它:
$ sudo cp mosquitto_pyiot.conf /etc/mosquitto/conf.d/
  1. 现在我们重新启动 Mosquitto 服务以加载我们的配置:
$ sudo systemctl restart mosquitto 
  1. 要检查配置是否有效,请在树莓派上的 Web 浏览器中访问http://localhost:8083 URL,您应该看到类似以下截图的页面:

图 4.1 - Mosquitto MQTT 代理提供的网页

这是本章后面我们将要做的事情的线索! 目前,虽然您可以移动滑块,但它不会改变 LED 的亮度,因为我们没有运行 Python 端的代码。 我们将在本章后面逐步介绍。

如果您在启动 Mosquitto MQTT 代理时遇到问题,请尝试以下操作:

  • 在终端中执行sudo mosquitto -v -c /etc/mosquitto/mosquitto.conf。 这将在前台启动 Mosquitto,并且任何启动或配置错误都将显示在您的终端上。

  • 阅读mosquitto_pyiot.conf文件中的故障排除注释以获取其他建议。

Mosquitto 安装后的默认配置创建了一个未加密未经身份验证的 MQTT 代理服务。 Mosquitto 文档包含有关其配置以及如何启用身份验证和加密的详细信息。 您将在本章末尾的进一步阅读部分找到链接。

现在我们已经安装并运行了 Mosquitto,我们可以探索 MQTT 概念并执行示例以看到它们在实践中的应用。

通过示例学习 MQTT

MQTT 是基于代理的发布订阅消息协议(经常被简化为pub/sub),而 MQTT 代理(就像我们在上一节中安装的 Mosquitto MQTT 代理)是实现 MQTT 协议的服务器。 通过使用基于 MQTT 的架构,您的应用程序可以基本上将所有复杂的消息处理和路由逻辑交给代理,以便它们可以保持专注于解决方案。

MQTT 客户端(例如,您的 Python 程序和我们即将使用的命令行工具)与代理创建订阅并订阅它们感兴趣的消息主题。 客户端发布消息到主题,然后代理负责所有消息路由和传递保证。 任何客户端都可以扮演订阅者、发布者或两者的角色。

图 4.2展示了涉及泵、水箱和控制器应用程序的简单概念 MQTT 系统:

图 4.2 - MQTT 示例

以下是系统组件的高级描述:

  • 水位传感器 MQTT 客户端视为连接到水箱中的水位传感器的软件。 在我们的 MQTT 示例中,此客户端扮演发布者的角色。 它定期发送(即发布)关于水箱装满了多少水的消息到 MQTT 代理。

  • 将* Pump MQTT 客户端视为能够打开或关闭水泵的软件驱动程序。在我们的示例中,此客户端扮演发布者订阅者*的角色:

  • 作为订阅者,它可以接收一条消息(通过订阅)指示它打开或关闭水泵。

  • 作为发布者,它可以发送一条消息,指示水泵是打开并抽水还是关闭。

  • Controller MQTT 客户端视为所有控制逻辑所在的应用程序。此客户端还扮演发布者订阅者的角色:

  • 作为发布者,此客户端可以发送一条消息,告诉水泵打开或关闭。

  • 作为订阅者,此客户端可以从水箱水位传感器和水泵接收消息。

举例来说,Controller MQTT 客户端应用程序可以配置为在水箱水位低于 50%时打开水泵,并在水位达到 100%时关闭水泵。此控制器应用程序还可以包括一个仪表板用户界面,显示水箱的当前水位以及指示水泵是否打开或关闭的状态灯。

关于我们的 MQTT 系统需要注意的一点是,每个客户端都不知道其他客户端,客户端只连接到 MQTT 代理并与之交互,然后代理将消息适当地路由到客户端。这通过使用消息主题来实现,我们将在标题为探索 MQTT 主题和通配符的部分中进行介绍。

可以理解为什么水泵需要接收消息来告诉它打开或关闭,但是水泵还需要发送消息来说明它是打开还是关闭吗?如果您对此感到困惑,这就是原因。MQTT 消息是发送并忘记的,这意味着客户端不会收到它发布的消息的应用级响应。因此,在我们的示例中,虽然控制器客户端可以发布一条消息要求水泵打开,但如果水泵不发布其状态,控制器就无法知道水泵是否真的打开了。

在实践中,每次水泵打开或关闭时,水泵都会发布其开/关状态。这将允许控制器的仪表板及时更新水泵的状态指示器。此外,水泵还会定期发布其状态(就像水位传感器一样),而不依赖于其接收到的任何请求来打开或关闭。这样,控制器应用程序可以监视水泵的连接和可用性,并检测水泵是否脱机。

目前,如果您能理解前面示例中提出的基本思想,那么您就已经在更深入地理解本章其余部分将关注的核心 MQTT 概念的道路上了。到我们完成时,您将对如何使用和设计基于 MQTT 的应用程序有一个基本的端到端理解。

我们将从学习如何发布和订阅消息开始。

发布和订阅 MQTT 消息

让我们通过以下步骤来发送(即发布)和接收(即订阅)使用 MQTT 的消息:

  1. 在终端中运行以下命令。mosquitto_sub(Mosquitto 订阅)是一个命令行工具,用于订阅消息:
# Terminal #1 (Subscriber)
$ mosquitto_sub -v -h localhost -t 'pyiot'

选项如下:

    • -v(-详细信息):详细信息是为了在终端上打印消息主题消息负载。
  • -h(-主机):localhost 是我们要连接的代理的主机;这里是我们刚刚安装的代理。使用的默认端口是 1883。

  • -t(-主题):pyiot是我们要订阅和监听的主题。

在本章中,我们将需要两个或三个终端会话来进行示例。代码块的第一行将指示您需要在哪个终端中运行命令;例如,在前面的代码块中是终端#1,在下面的代码块中是终端#2

  1. 打开第二个终端并运行以下命令。mosquitto_pub(Mosquitto 发布)是一个命令行工具,用于发布消息:
# Terminal #2 (Publisher)
$ mosquitto_pub -h localhost -t 'pyiot' -m 'hello!' 

让我们看看选项:

    • -h-t的含义与前面的订阅命令相同。
  • -m 'hello!'(-message)是我们想要发布的消息。在 MQTT 中,消息是简单的字符串——如果你想知道 JSON,它只需要被序列化/反序列化为字符串。

  1. 终端#1上,我们看到主题和消息hello!被打印出来:
# Terminal #1 (Subscriber)
$ mosquitto_sub -v -h localhost -t 'pyiot'
pyiot hello!

最后一行的格式是<topic> <message payload>

因为我们使用了-v选项来订阅mosquitto_sub,所以hello!消息之前有主题名pyiot。如果没有-v选项,如果我们订阅多个主题,我们无法确定消息属于哪个主题。

现在,我们已经学会了如何使用简单主题发布和订阅消息。但是有没有办法更好地组织这些消息呢?继续阅读。

探索 MQTT 主题和通配符

MQTT 主题 用于以分层格式对消息进行分类或分组。我们已经在我们之前的命令行示例中使用主题,但是以非分层的方式。另一方面,通配符是订阅者用来创建灵活的主题匹配模式的特殊字符。

以下是来自具有传感器的假设建筑的一些分层主题示例。层次结构由/字符分隔:

  • level1/lounge/temperature/sensor1

  • level1/lounge/temperature/sensor2

  • level1/lounge/lighting/sensor1

  • level2/bedroom1/temperature/sensor1

  • level2/bedroom1/lighting/sensor1

在 MQTT 代理上没有必要预先创建主题。使用默认代理配置(我们正在使用),您只需随意发布和订阅主题。

当 Mosquitto 代理配置为使用身份验证时,有可能根据客户端 ID 和/或用户名和密码限制对主题的访问。

消息必须发布到特定主题,如pyiot,而订阅可以通过使用通配符字符+#订阅到特定主题或一系列主题:

  • +用于匹配层次结构的单个元素。

  • # 用于匹配层次结构中的所有剩余元素(它只能在主题查询的末尾)。

对主题和通配符的订阅最好通过示例来解释。使用上述假设的建筑和传感器,考虑以下表中的示例:

我们想要订阅... 通配符主题 主题匹配
到处的温度传感器 +/+/**temperature**/+
  • level1/lounge/**temperature**/sensor1

  • level1/lounge/**temperature**/sensor2

  • level2/bedroom1/**temperature**/sensor1

|

所有地方的传感器 +/+/**lighting**/+
  • level1/lounge/**lighting**/sensor1

  • level2/bedroom1/**lighting**/sensor1

|

level 2 上的每个传感器 **level2**/+/+/+
  • **level2**/bedroom1/temperature/sensor1

  • **level2**/bedroom1/lighting/sensor1

|

level 2 上的每个传感器(一个更简单的方法,其中#匹配每个剩余的子级) **level2**/#
  • level2/bedroom1/temperature/sensor1

  • **level2**/bedroom1/lighting/sensor1

|

到处只有sensor1 +/+/+/**sensor1**
  • level1/lounge/temperature/**sensor1**

  • level1/lounge/lighting/**sensor1**

  • **level2**/bedroom1/temperature/**sensor1**

  • level2/bedroom1/lighting/**sensor1**

|

到处只有sensor1(一个更简单的方法,其中#匹配每个剩余的子级) #/**sensor1** 无效,因为#只能在主题查询的末尾
每个主题 # 匹配所有内容
代理信息 $SYS/# 这是一个特殊的保留主题,代理在其中发布信息和运行时统计信息。

表 1 - MQTT 通配符主题示例

从前面的示例中可以明显看出,您需要在设计应用程序的主题层次结构时小心,以便使用通配符订阅多个主题是一致的、逻辑的和简单的。

如果您使用mosquitto_sub订阅使用+#通配符,请记住使用-v(--verbose)选项,以便主题名称打印在输出中,例如mosquitto_sub -h localhost -v -t '#'

在命令行上尝试一些示例,通过混合和匹配前面的主题和通配符来感受主题和通配符的工作原理。以下是一个示例的步骤,其中mosquitto_sub订阅了所有从根主题下两级的父主题为temperature的子主题:

  1. 在一个终端中,启动一个订阅通配符主题的订阅者:
# Terminal #1 (Subscriber)
mosquitto_sub -h localhost -v -t '+/+/temperature/+'
  1. 使用表 1 - MQTT 通配符主题示例中的主题,以下是两个mosquitto_pub命令,将发布的消息将被终端#1中的mosquitto_sub命令接收:
# Terminal #2 (Publisher)
$ mosquitto_pub -h localhost -t 'level1/lounge/temperature/sensor1' -m '20'
$ mosquitto_pub -h localhost -t 'level2/bedroom1/temperature/sensor1' -m '22'

我们刚刚看到如何使用通配符字符+*订阅主题层次结构。使用主题和通配符一起是一个设计决策,您需要根据数据流动的需求以及您设想客户端应用程序发布和订阅的方式在每个项目级别上做出的。在设计一致而灵活的基于通配符的主题层次结构方面投入的时间将有助于帮助您构建更简单和可重用的客户端代码和应用程序。

接下来,我们将学习有关消息服务质量的所有内容,以及这如何影响您通过 MQTT Broker 发送的消息。

将服务质量应用于消息

MQTT 为单个消息传递提供了三个服务质量QoS)级别 - 我强调单个消息传递,因为 QoS 级别适用于单个消息的传递,而不适用于主题。随着您逐步学习示例,这一点将变得更加清晰。

作为开发人员,您规定消息的 QoS,而代理负责确保消息传递符合 QoS。以下是您可以应用于消息的 QoS 以及它们对传递的含义:

QoS 级别 含义 传递的消息数量
级别 0 该消息将被传递最多一次,但也可能根本不传递。 0 或 1
级别 1 该消息将至少传递一次,但可能更多。 1 或更多
级别 2 该消息将被传递一次。 1

表 2 - 消息 QoS 级别

您可能会问:级别 0 和 1 似乎有点随机,那么为什么不总是使用级别 2 呢?答案是资源。让我们看看为什么...

与较低级别的 QoS 消息相比,代理和客户端将消耗更多的资源来处理较高级别的 QoS 消息 - 例如,代理将需要更多的时间和内存来存储和处理消息,而代理和客户端在确认确认和连接握手时消耗更多的时间和网络带宽。

对于许多用例,包括本章后续的示例,我们将注意不到 QoS 级别 1 和 2 之间的区别,我们也无法实际演示它们(级别 0 由于一个很好的原因被省略,我们稍后将在消息保留和持久连接时看到)。然而,设想一个分布式物联网系统,其中成千上万的传感器每分钟发布成千上万条消息,现在围绕 QoS 设计开始变得更有意义。

QoS 级别适用于消息订阅和消息发布,当你第一次思考时,这可能看起来有点奇怪。例如,一个客户端可以以 QoS 1 发布消息到一个主题,而另一个客户端可以以 QoS 2 订阅该主题(我知道我说 QoS 与消息有关,而不是与主题有关,但在这里,它与通过主题流动的消息有关)。这条消息的 QoS 是 1 还是 2?对于订阅者来说,是 1——让我们找出原因。

订阅客户端选择它想要接收的消息的最高 QoS,但可能会得到更低的 QoS。因此,实际上,这意味着客户端接收的交付 QoS 被降级为发布或订阅的最低 QoS。

以下是一些供您思考的示例:

发布者发送消息 订阅者订阅 订阅者获取的内容
QoS 2 QoS 0 传递符合 QoS 0 的消息(订阅者获取消息 0 次或 1 次)
QoS 2 QoS 2 传递符合 QoS 2 的消息(订阅者获取消息一次)
QoS 0 QoS 1 传递符合 QoS 0 的消息(订阅者获取消息 0 次或 1 次)
QoS 1 QoS 2 传递符合 QoS 1 的消息(订阅者获取消息 1 次或多次)
QoS 2 QoS 1 传递符合 QoS 1 的消息(订阅者获取消息 1 次或多次)

表 3 - 发布者和订阅者 QoS 示例

从这些示例中可以得出的结论是,在实践中,设计或集成物联网解决方案时,您需要了解主题两侧的发布者和订阅者使用的 QoS——QoS 不能在任一侧单独解释。

以下是播放 QoS 场景并实时查看客户端-代理交互的步骤:

  1. 在终端中,运行以下命令启动订阅者:
# Terminal 1 (Subscriber)
$ mosquitto_sub -d -v -q 2 -h localhost -t 'pyiot'
  1. 在第二个终端中,运行以下命令发布消息:
# Terminal 2 (Publisher)
$ mosquitto_pub -d -q 1 -h localhost -t 'pyiot' -m 'hello!'

在这里,我们再次在终端#1上订阅,并在终端#2上发布。以下是与mosquitto_submosquitto_pub一起使用的新选项:

    • -d:打开调试消息
  • -q <level>:QoS 级别

启用调试(-d)后,尝试在任一侧更改-q参数(为 0、1 或 2)并发布新消息。

  1. 观察终端#1终端#2中记录的消息。

终端#1终端#2中将出现一些调试消息,您将观察到订阅端发生的 QoS 降级(寻找q0q1q2),而在双方,您还将注意到不同的调试消息,具体取决于客户端和代理执行握手和交换确认时指定的 QoS:

# Terminal 1 (Subscriber)
$ mosquitto_sub -d -v -q 2 -h localhost -t 'pyiot' # (1)
Client mosqsub|25112-rpi4 sending CONNECT
Client mosqsub|25112-rpi4 received CONNACK (0)
Client mosqsub|25112-rpi4 sending SUBSCRIBE (Mid: 1, Topic: pyiot, QoS: 2) # (2)
Client mosqsub|25112-rpi4 received SUBACK
Subscribed (mid: 1): 2
Client mosqsub|25112-rpi4 received PUBLISH (d0, q1, r0, m1, 'pyiot', ... (6 bytes)) # (3)
Client mosqsub|25112-rpi4 sending PUBACK (Mid: 1)
pyiot hello!

以下是终端#1上订阅者的调试输出。请注意以下内容:

    • 在第 1 行,我们使用 QoS 2(-q 2)进行订阅。这在调试输出中反映为QoS:2,在第 2 行。
  • 在第 3 行,我们看到了 QoS 的降级。接收到的消息是 QoS 1(q1),这是消息在终端#1中发布的 QoS。

QoS 是较复杂的 MQTT 概念之一。如果您想更深入地了解 QoS 级别以及发布者、订阅者和代理之间进行的低级通信,您将在进一步阅读部分找到链接。

现在我们已经介绍了消息 QoS 级别,接下来我们将了解两个 MQTT 功能,确保离线客户端可以在重新上线时接收以前的消息。我们还将看到 QoS 级别如何影响这些功能。

保留消息以供以后传递

MQTT 代理可以被指示保留发布到主题的消息。消息保留有两种类型,称为保留消息和持久连接:

  • 保留消息是指代理保留在主题上发布的最后一条消息。这也通常被称为最后已知的好消息,任何订阅主题的客户端都会自动获取此消息。

  • 持久连接也涉及保留消息,但在不同的上下文中。如果客户端告诉代理它想要一个持久连接,那么代理将在客户端离线时保留 QoS 1 和 2 的消息。

除非特别配置,Mosquitto 不会在服务器重新启动时保留消息或连接。要在重新启动时保留此信息,Mosquitto 配置文件必须包含条目persistence true。树莓派上 Mosquitto 的默认安装应该包括此条目,但是,为了确保它也包含在我们之前安装的mosquitto_pyiot.conf中。请参阅官方 Mosquitto 文档以获取有关持久性的更多信息和配置参数。您将在本章末尾的进一步阅读部分找到链接。

接下来,我们将学习保留消息并在随后的部分中涵盖持久连接。

发布保留消息

发布者可以要求代理保留一个消息作为主题的最后已知的良好消息。任何新连接的订阅者将立即收到这个最后保留的消息。

让我们通过一个示例来演示保留消息:

  1. 运行以下命令,注意我们从终端#2开始,这个示例中是发布者:
# Terminal 2 (Publisher)
$ mosquitto_pub -r -q 2 -h localhost -t 'pyiot' -m 'hello, I have been retained!'

已添加了一个新选项,-r(--retain),告诉代理应该为该主题保留此消息。

一个主题只能存在一个保留的消息。如果使用-r选项发布另一条消息,则先前保留的消息将被替换。

  1. 在另一个终端中启动一个订阅者,然后立即您将收到保留的消息:
# Terminal 1 (Subscriber)
$ mosquitto_sub -v -q 2 -h localhost -t 'pyiot'
pyiot hello, I have been retained!
  1. 终端#1中按下Ctrl + C来终止mosquitto_sub

  2. 再次使用与步骤 2相同的命令启动mosquitto_sub,然后您将在终端#1中再次收到保留的消息。

您仍然可以发布普通消息(即使用-r选项),但是,新连接的订阅者将接收到使用-r选项指示的最后保留的消息。

  1. 我们的最后一个命令显示了如何清除先前保留的消息:
# Terminal 2 (Publisher)
$ mosquitto_pub -r -q 2 -h localhost -t 'pyiot' -m ''

在这里,我们正在发布(使用-r)一个带有-m ''的空消息。请注意,我们可以使用-n作为-m ''的替代方法来指示空消息。保留空消息的效果实际上是清除保留的消息。

当您向主题发送空消息以删除保留的消息时,当前订阅该主题的任何客户端(包括具有持久连接的离线客户端-请参阅下一节)都将收到空消息,因此您的应用代码必须适当地测试和处理空消息。

现在您了解并知道如何使用保留消息,我们现在可以探索 MQTT 中可用的另一种消息保留类型,称为持久连接

创建持久连接

订阅主题的客户端可以要求代理在其离线时保留或排队消息。在 MQTT 术语中,这被称为持久连接。为了使持久连接和传递工作,订阅客户端需要以特定的方式进行配置和订阅,如下所示:

  • 当客户端连接时,必须向代理提供唯一的客户端 ID。

  • 客户端必须使用 QoS 1 或 2(级别 1 和 2 保证传递,但级别 0 不保证)进行订阅。

  • 客户端只有在使用 QoS 1 或 2 进行发布的消息时才能得到保证。

最后两点涉及了一个示例,其中了解主题的发布和订阅双方的 QoS 对于物联网应用程序设计非常重要。

MQTT 代理可以在代理重新启动时保留消息,树莓派上 Mosquitto 的默认配置也可以这样做。

让我们通过一个示例来演示:

  1. 启动订阅者,然后立即使用Ctrl + C终止它,使其处于离线状态:
# Terminal #1 (Subscriber)
$ mosquitto_sub -q 1 -h localhost -t 'pyiot' -c -i myClientId123
$ # MAKE SURE YOU PRESS CONTROL+C TO TERMINATE mosquitto_sub

使用的新选项如下:

    • -i <client id>(-id )是一个唯一的客户端 ID(这是代理识别客户端的方式)。
  • -c(--disable-clean-session)指示代理保留订阅主题上到达的任何 QoS 1 和 2 消息,即使客户端断开连接(即保留消息)。

措辞有点反向,但通过使用-c选项启动订阅者,我们已要求代理通过在连接时不清除任何存储的消息来为我们的客户端创建一个持久连接

如果您使用通配符订阅一系列主题(例如,pyiot/#),并请求持久连接,那么通配符层次结构中所有主题的所有消息都将保留给您的客户端。

  1. 发布一些消息(当终端#1中的订阅者仍然离线时):
# Terminal #2 (Publisher)
$ mosquitto_pub -q 2 -h localhost -t 'pyiot' -m 'hello 1'
$ mosquitto_pub -q 2 -h localhost -t 'pyiot' -m 'hello 2'
$ mosquitto_pub -q 2 -h localhost -t 'pyiot' -m 'hello 3
  1. 终端#1中的订阅者重新连接,我们将看到在步骤 2中发布的消息被传送:
# Terminal 1 (Subscriber)
$ mosquitto_sub -v -q 1 -h localhost -t 'pyiot' -c -i myClientId123
pyiot hello 1
pyiot hello 2
pyiot hello 3

再次尝试步骤 13,只是这次在步骤 13中的订阅者中省略-c选项,您会注意到没有消息被保留。此外,当您在有保留消息等待传送时不使用-c标志连接时,那么所有保留消息都将被清除(这是您想要清除客户端的保留消息的方法)。

如果您在单个主题上同时使用保留消息(即最后已知的良好消息)和持久连接,并重新连接离线订阅者,您将收到保留消息两次—一次是保留消息,而第二次是来自持久连接的消息。

在围绕 MQTT 构建解决方案时,您对保留消息和持久连接的了解将对设计具有弹性和可靠性的系统至关重要,特别是在需要处理离线客户端的情况下。保留(最后已知的良好)消息非常适合在客户端重新上线时初始化客户端,而持久连接将帮助您为任何必须能够消费其订阅的每条消息的离线客户端保留和传送消息。

干得好!我们已经涵盖了很多内容,实际上您现在已经了解了构建基于 MQTT 的物联网解决方案时将使用的大多数核心 MQTT 功能。我们要了解的最后一个功能是称为Will

用 Will 说再见

我们探索的最后一个 MQTT 功能是称为 Will。客户端(发布者或订阅者)可以向代理注册一个特殊的Will消息,以便如果客户端死机并突然断开与代理的连接(例如,它失去了网络连接或其电池耗尽),代理将代表客户端发送Will消息,通知订阅者设备的消亡。

Will 只是一个消息和主题组合,类似于我们之前使用的。

让我们看看 Will 的作用,为此,我们将需要三个终端:

  1. 打开一个终端,并使用以下命令启动一个订阅者:
# Terminal #1 (Subscriber with Will)
$ mosquitto_sub -h localhost -t 'pyiot' --will-topic 'pyiot' --will-payload 'Good Bye' --will-qos 2 --will-retain

新的选项如下:

    • --will-payload:这是 Will 消息。
  • --will-topic:这是 Will 消息将要发布的主题。在这里,我们使用与我们订阅的相同主题,但也可以是不同的主题。

  • --will-qos:这是 Will 消息的 QoS。

  • --will-retain:如果存在此选项,那么如果客户端突然断开连接,Will 消息将被代理保留为 Will 主题的保留(最后已知的良好)消息

  1. 使用以下命令在第二个终端中启动一个订阅者:
# Terminal #2 (Subscriber listening to Will topic).
$ mosquitto_sub -h localhost -t 'pyiot'
  1. 在第三个终端中,使用以下命令发布一条消息:
# Terminal #3 (Publisher)
$ mosquitto_pub -h localhost -t 'pyiot' -m 'hello'
  1. 一旦在终端#3上执行步骤 3中的mosquitto_pub命令,您应该会在终端#1和**#2**上都看到hello被打印出来。

  2. 终端#1中,按下Ctrl + C来终止向代理注册 Will 的订阅者。Ctrl + C被视为与代理的非优雅或突然断开连接。

  3. 终端#2中,我们将看到遗嘱的“再见”消息:

# Terminal #2 (Subscriber listening to Will topic).
$ mosquitto_sub -h localhost -t 'pyiot'
'Good Bye'

好的,那么优雅地断开连接呢,订阅者如何正确地关闭与代理的连接?我们可以使用mosquitto_sub-C选项来演示这一点。

  1. 使用以下命令重新启动终端#1中的订阅者:
# Terminal #1 (Subscriber with Will)
$ mosquitto_sub -h localhost -t 'pyiot' --will-topic 'pyiot' --will-payload 'Good Bye, Again' --will-qos 2 --will-retain -C 2

新的-C <count>选项告诉mosquitto_sub在接收到指定数量的消息后断开(优雅地)并退出。

您会立即注意到打印的“再见”消息。这是因为我们之前在终端#1中指定了--retain-will选项。此选项使遗嘱消息成为主题的保留或最后已知的好消息,因此新连接的客户端将接收此消息。

  1. 终端#3中,发布一条新消息,终端#1中的订阅者将退出。请注意,在终端#3中,不会收到遗嘱消息“再见,再见”。这是因为我们的终端#1订阅者因为-C选项而优雅地与代理断开连接,并且如果您想知道-C 2中的2,则保留的遗嘱消息被计为第一条消息。

干得好!如果您已经完成了前面的每个 MQTT 示例,那么您已经涵盖了 MQTT 和 Mosquitto 代理的核心概念和用法。请记住,所有这些原则都适用于任何 MQTT 代理或客户端,因为 MQTT 是一个开放标准。

到目前为止,我们已经了解了消息订阅和发布,以及如何使用主题对消息进行分离,以及如何利用 QoS、消息保留、持久连接和遗嘱来控制消息的管理和传递。单单这些知识就为您提供了构建复杂和有弹性的分布式物联网系统的基础,使用 MQTT。

我将给您留下一个最后的提示(当我开始使用 MQTT 时,这个提示几次让我困惑)。

如果您的实时、保留或排队的持久连接消息似乎消失在黑洞中,请检查订阅和发布客户端的 QoS 级别。要监视所有消息,请启动一个命令行订阅者,使用 QoS 2,监听#主题,并启用详细和调试选项,例如mosquitto_sub -q 2 -v -d -h localhost -t '#'

我们现在已经完成了 MQTT 示例部分的所有示例,并学会了如何从命令行与 MQTT 代理进行交互。接下来,我想简要提一下公共代理服务。之后,我们将进入代码,看看如何利用 Python 与 MQTT。

使用 MQTT 代理服务

互联网上有几家 MQTT 代理服务提供商,您可以使用它们创建基于 MQTT 的消息传递应用程序,如果您不想托管自己的 MQTT 代理。许多还提供免费的公共 MQTT 代理,供您用于测试和快速概念验证,但请记住它们是免费和公共的,因此不要发布任何敏感信息!

如果您在使用免费公共代理服务时遇到挫折、断开连接或意外行为,请使用本地代理测试和验证您的应用程序。您无法可靠地了解或验证开放公共代理的流量拥塞、主题使用或配置细节以及这可能如何影响您的应用程序。

以下是一些免费的公共代理,您可以尝试。只需将前面示例中的-hlocalhost选项替换为代理的地址。访问以下页面以获取更多信息和说明:

在接下来的部分,我们将提升一个级别。最后,我们将进入 MQTT 的 Python 部分!请放心,我们刚刚讨论的一切在您开发使用 MQTT 的物联网应用程序时将非常宝贵,因为我们讨论的命令行工具和示例将成为您的 MQTT 开发和调试工具包的重要组成部分。我们将应用我们已经学到的核心 MQTT 概念,只是这次使用 Python 和 Paho-MQTT 客户端库。

介绍 Python Paho-MQTT 客户端库

在我们进入 Python 代码之前,我们首先需要一个 Python 的 MQTT 客户端库。在本章的技术要求部分开始时,我们安装了 Paho-MQTT 客户端库,它是requirements.txt的一部分。

如果您是 MQTT 的新手,并且还没有阅读前面的通过示例学习 MQTT部分,我建议现在停下来先阅读它,以便您对接下来的 Python 示例中将使用的 MQTT 概念和术语有所了解。

Paho-MQTT 客户端库来自 Eclipse 基金会,该基金会还维护 Mosquitto MQTT 代理。在进一步阅读部分,您将找到指向官方Paho-MQTT 客户端库 API文档的链接。在完成本章后,如果您希望加深对该库及其功能的理解,我建议阅读官方文档和其中的示例。

Python Paho-MQTT 库有三个核心模块:

  • 客户端:这为您在 Python 应用程序中完全管理 MQTT 的生命周期。

  • 发布者:这是一个用于消息发布的辅助模块。

  • 订阅者:这是一个用于消息订阅的辅助模块。

客户端模块非常适合创建更复杂和长时间运行的物联网应用程序,而发布者和订阅者辅助模块适用于短暂的应用程序和不需要完全生命周期管理的情况。

以下 Python 示例将连接到我们之前在安装 Mosquitto MQTT 代理部分安装的本地 Mosquitto MQTT 代理。

我们将使用 Paho 客户端模块,以便我们可以创建一个更完整的 MQTT 示例。然而,一旦您能够理解并跟随客户端模块,使用辅助模块创建替代方案将变得轻而易举。

作为提醒,我们将使用我们在第二章Python 和物联网入门图 2.7中创建的面包板电路。

现在我们对 Paho-MQTT 库有了基本的了解,接下来我们将简要回顾 Python 程序和配套的网页客户端的功能,并看到 Paho-MQTT 的实际应用。

使用 Python 和 MQTT 控制 LED

安装 Mosquitto MQTT 代理部分中,我们通过访问http://localhost:8083 URL 来测试安装,这给了我们一个带有滑块的网页。然而,当时我们无法改变 LED 的亮度。当您移动滑块时,网页会向 Mosquitto 代理发布 MQTT 消息,但没有程序接收消息来改变 LED 的亮度。

在本节中,我们将看到 Python 代码订阅名为led的主题并处理滑块生成的消息。我们将首先运行 Python 代码,并确保我们可以改变 LED 的亮度。

运行 LED MQTT 示例

您将在chapter04/mqtt_led.py文件中找到代码。在继续之前,请先查看此文件,以便对其内容有一个整体的了解,然后按照以下步骤操作:

  1. 使用以下命令在终端中运行程序:
# Terminal #1
(venv) $ python mqtt_led.py
INFO:main:Listening for messages on topic 'led'. Press Control + C to exit.
INFO:main:Connected to MQTT Broker
  1. 现在,打开第二个终端窗口并尝试以下操作,LED 应该会亮起(请确保 JSON 字符串格式正确):
# Terminal #2
$ mosquitto_pub -q 2 -h localhost -t 'led' -r -m '{"level": "100"}'
  1. 您是否注意到在步骤 2中使用了-r(--retain)选项?终止并重新启动mqtt_led.py,并观察终端#1中的日志输出和 LED。您应该注意到在启动时,mqtt_led.py从主题的保留消息接收 LED 的亮度值,并相应地初始化 LED 的亮度。

  2. 接下来,访问http://localhost:8083的 URL,并确保 LED 在您移动滑块时改变亮度。

保持网页打开,并再次尝试步骤 2中的命令。观察滑块的变化——它将与您指定的新级别值保持同步。

  1. 接下来,让我们看看持久连接是如何工作的。再次终止mqtt_led.py并执行以下操作:
  • 在网页上,随机移动滑块大约 5 秒钟。当您移动滑块时,消息将被发布到led主题的代理中。当mqtt_led.py重新连接时,它们将被排队等待传递。

  • 重新启动mqtt_led.py并观察终端和 LED。您会注意到终端上有大量的消息,并且 LED 会闪烁,因为排队的消息被mqtt_led.py接收和处理。

默认情况下,Mosquitto 配置为每个使用持久连接的客户端排队 100 条消息。客户端由其客户端 ID 标识,您在连接到代理时提供该 ID。

现在我们已经与mqtt_led.py进行了交互并看到它的运行情况,让我们来看看它的代码。

理解代码

当我们讨论在chapter04/mqtt_led.py中找到的代码时,特别注意代码如何连接到 MQTT 代理并管理连接生命周期。此外,当我们讨论代码如何接收和处理消息时,试着将代码工作流程与我们在上一小节中用于发布消息的命令行示例联系起来。

一旦您了解了我们的 Python 代码以及它如何与我们的 MQTT 代理集成,您将拥有一个端到端的工作参考解决方案,围绕 MQTT 消息构建,您可以根据自己的需求和项目进行调整。

我们将从导入开始。通常情况下,我们将跳过我们在之前章节中已经涵盖过的任何常见代码,包括日志设置和GPIOZero相关的代码。

导入

我们在这个例子中唯一新的导入是 Paho-MQTT 客户端:

import paho.mqtt.client as mqtt  # (1)

在第 1 行,我们导入 Paho-MQTT client类,并给它起了别名mqtt。如前所述,这是一个客户端类,它将允许我们在 Python 中创建一个完整的生命周期 MQTT 客户端。

接下来,我们将考虑全局变量。

全局变量

在第 2 行的BROKER_HOSTBROKER_POST变量是指我们本地安装的 Mosquitto MQTT 代理。端口1883是标准默认的 MQTT 端口:

# Global Variables ...  BROKER_HOST = "localhost"   # (2) BROKER_PORT = 1883 CLIENT_ID = "LEDClient" # (3) TOPIC = "led" # (4) client = None # MQTT client instance. See init_mqtt()   # (5) ...

在第 3 行,我们定义了CLIENT_ID,这将是我们用来标识我们的程序与 Mosquitto MQTT 代理连接的唯一客户端标识符。我们必须向代理提供一个唯一的 ID,以便我们可以使用持久连接

在第 4 行,我们定义了我们的程序将订阅的 MQTT 主题,而在第 5 行,client变量是一个占位符,将被分配 Paho-MQTT 客户端实例,我们很快就会看到。

set_led_level(data)方法

set_led_level(data)在第 6 行是我们与 GPIOZero 集成以改变 LED 亮度的地方,方法类似于我们在第三章中涵盖的相应方法,使用 Flask 进行 RESTful API 和 Web 套接字的网络,因此我们不会再次在这里涵盖内部情况:

def set_led_level(data):  # (6)
   ...

数据参数预期是一个 Python 字典,格式为{ "level": 50 },其中整数介于 0 和 100 之间,表示亮度百分比。

接下来,我们有 MQTT 的回调函数。我们将从审查on_connect()on_disconnect()开始。

on_connect()和 on_disconnect() MQTT 回调方法

on_connect()on_disconnect()回调处理程序是使用 Paho client类提供的完整生命周期的示例。我们将在覆盖init_mqtt()方法时看到如何实例化 Paho client实例并注册这些回调。

在以下代码块的第 7 行,on_connect()感兴趣的参数是client,它是对 Paho client类的引用,以及result_code,它是描述连接结果的整数。我们在第 8 行看到result_code用于测试连接的成功。注意connack_string()方法,它用于连接失败时将result_code转换为可读的字符串。

当我们谈论 MQTT client并在以下代码块的第 7 行看到client参数时,请记住这是我们 Python 代码的客户端连接到代理,而不是指客户端程序,比如网页。这个客户端参数在意义上与我们在*第三章中为 Flask-SocketIO Web Socket 服务器使用回调处理程序时看到的客户端参数非常不同。

供参考,user_data参数可用于在 Paho 客户端的回调方法之间传递私有数据,而flags是一个包含 MQTT 代理的响应和配置提示的 Python 字典:

def on_connect(client, user_data, flags, result_code): # (7)     if connection_result_code == 0:                    # (8)
  logger.info("Connected to MQTT Broker")
    else:
  logger.error("Failed to connect to MQTT Broker: " + 
                     mqtt.connack_string(result_code))

    client.subscribe(TOPIC, qos=2)                     # (9)

在第 9 行,我们看到 Paho client实例方法subscribe(),用于使用我们之前定义的全局变量TOPIC订阅led主题。我们还告诉代理我们的订阅是 QoS 级别 2。

总是在on_connect()处理程序中订阅主题。这样,如果客户端失去与代理的连接,它可以在重新连接时重新建立订阅。

接下来,在以下的第 10 行,我们有on_disconnect()处理程序,我们只是记录任何断开连接。方法参数的含义与on_connect()处理程序相同:

def on_disconnect(client, user_data, result_code):  # (10)
    logger.error("Disconnected from MQTT Broker")

我们现在将转到处理我们在on_connect()中订阅的led主题的回调方法,位于第 9 行。

on_message() MQTT 回调方法

在第 11 行的on_message()处理程序在我们的程序接收到订阅主题的新消息时被调用。消息通过msg参数可用,它是MQTTMessage的一个实例。

在第 12 行,我们访问msgpayload属性并将其解码为字符串。我们期望我们的数据是一个 JSON 字符串(例如,{ "level": 100 }),所以我们使用json.loads()将字符串解析为 Python 字典,并将结果赋给data。如果消息负载不是有效的 JSON,我们捕获异常并记录错误:

def on_message(client, userdata, msg):                    # (11)   data = None  try:                                                  
  data = json.loads(msg.payload.decode("UTF-8"))    # (12)
    except json.JSONDecodeError as e:
        logger.error("JSON Decode Error: " 
                   + msg.payload.decode("UTF-8"))

    if msg.topic == TOPIC:                                # (13)   set_led_level(data)                               # (14)
    else:
        logger.error("Unhandled message topic {} 
                 with payload " + str(msg.topic, msg.payload)))

在第 13 行使用msgtopic属性,我们检查它是否与我们预期的led主题匹配,在我们的情况下,它会匹配,因为我们的程序只订阅这个特定的主题。然而,这提供了一个参考点,关于在订阅多个主题的程序中执行条件逻辑和路由的位置和方式。

最后,在第 14 行,我们将解析的消息传递给set_led_level()方法,正如讨论的那样,这会改变 LED 的亮度。

接下来,我们将学习如何创建和配置 Paho 客户端。

init_mqtt()方法

我们在第 15 行看到 Paho-MQTT client实例被创建并分配给全局client变量。这个对象的引用是client参数,我们之前在on_connect()on_disconnect()on_message()方法中看到过。

client_id参数设置为我们之前在CLIENT_ID中定义的客户端名称,而clean_session=False告诉代理在连接时不要清除我们的客户端的任何存储消息。正如我们在命令行示例中讨论的那样,这是说我们希望建立持久连接,因此当我们的客户端离线时,发布到led主题的任何消息都会为我们的客户端存储。

def init_mqtt():
    global client   client = mqtt.Client(                                       # (15)
  client_id=CLIENT_ID,
        clean_session=False)

    # Route Paho logging to Python logging.   client.enable_logger()                                      # (16)   # Setup callbacks  client.on_connect = on_connect                              # (17)
  client.on_disconnect = on_disconnect
    client.on_message = on_message

    # Connect to Broker.
  client.connect(BROKER_HOST, BROKER_PORT)                    # (18)

需要注意的一个重要点是在第 16 行。我们的程序使用标准的 Python 日志包,因此我们需要调用client.enable_logger()来确保我们获得任何 Paho-MQTT 客户端日志消息。如果缺少这个调用,可能会导致有用的诊断信息未被记录。

最后,在第 18 行,我们连接到 Mosquitto MQTT 代理。一旦连接建立,就会调用我们的on_connect()处理程序。

接下来,我们将看到我们的程序是如何启动的。

主入口点

在初始化 LED 和客户端实例之后,我们进入了程序的主入口点。

我们在第 19 行注册了一个信号处理程序,以捕获Ctrl + C组合键。signal_handler方法(未显示)简单地关闭我们的 LED 并从代理中优雅地断开连接:

# Initialise Module init_led()
init_mqtt()

if __name__ == "__main__":
    signal.signal(signal.SIGINT, signal_handler)    # (19)   logger.info("Listening for messages on topic '" 
       + TOPIC + "'. Press Control + C to exit.")

    client.loop_start()                             # (20)
  signal.pause()

在第 20 行,调用client.loop_start()是允许我们的客户端启动、连接到代理并接收消息的方法。

您是否注意到 LED 程序是无状态的?我们没有在代码或磁盘中存储或持久化任何 LED 级别。我们的程序所做的就是订阅代理上的一个主题,并使用 GPIOZero 改变 LED 的亮度。我们通过依赖 MQTT 的保留消息(也称为最后已知的好消息)功能,将所有状态管理交给了 MQTT 代理。

我们现在已经完成了与 LED 和 MQTT 代理互动的 Python 代码的探索。我们学会了如何使用 Python Paho-MQTT 库连接到 MQTT 代理并订阅 MQTT 主题。当我们收到订阅主题上的消息时,我们看到了如何处理它们,并根据消息负载改变 LED 的亮度级别。

我们介绍的 Python 和 Paho-MQTT 框架和示例将为您自己的基于 MQTT 的物联网项目提供一个坚实的起点。

接下来,我们将查看一个使用 MQTT 和 Web 套接字的 Web 客户端。这个 Web 客户端将连接到我们的 Mosquitto MQTT 代理并发布消息以控制我们的 LED。

构建基于 Web 的 MQTT 客户端

在第三章中,使用 Flask 进行 RESTful API 和 Web 套接字网络,我们介绍了一个使用 Web 套接字的代码示例,其中包括一个 HTML 文件和 JavaScript Web 客户端。在本节中,我们还将查看使用 HTML 和 JavaScript 构建的基于 Web 套接字的 Web 客户端。然而,这一次,我们将利用 Mosquitto MQTT 代理提供的内置 Web 套接字功能以及兼容的 JavaScript Paho-JavaScript Web 套接字库(您将在进一步阅读部分找到此库的链接)。

作为对比,在第三章中,使用 Flask 进行 RESTful API 和 Web 套接字网络,我们使用 Flask-SocketIO 在 Python 中自己创建了 Web 套接字服务器,而我们的 Web 客户端使用了 Socket.io JavaScript Web 套接字库。

我们之前与即将探索的 Web 客户端进行了互动,以控制我们的 LED,位于安装 Mosquitto MQTT 代理的第 7 步。您可能希望快速查看第 7 步,以重新熟悉 Web 客户端以及如何在 Web 浏览器中访问它。

您将在chapter04/mosquitto_www/index.html文件中找到 Web 页面客户端的代码。请在继续之前查看此文件。

理解代码

虽然我们在此示例中使用的 JavaScript 库不同,但您会发现 JavsScript 代码的一般结构和用法与我们在第三章中看到的基于socket.io的 Web 客户端的代码类似,使用 Flask 进行 RESTful API 和 Web 套接字的网络。像往常一样,我们将从导入开始。

导入

我们的 Web 客户端在第 1 行导入了 Paho-MQTT JavaScript 客户端库:

<title>MQTT Web Socket Example</title>
<script src="./jquery.min.js"></script>
<script src="./paho-mqtt.js"></script> <!-- (1) --> 

paho-mqtt.js也可以在chapter04/mosquitto_www文件夹中找到。

Paho-MQTT JavaScript 库的官方文档页面位于www.eclipse.org/paho/clients/js,而其官方 GitHub 页面位于github.com/eclipse/paho.mqtt.javascript

当您进一步探索 Paho-MQTT JavaScript API 时,请从其 GitHub 网站开始,并注意其中提到的任何重大更改。已知文档页面包含不反映最新 GitHub 代码库的代码片段。

接下来,我们遇到了全局变量。

全局变量

在第 2 行,我们初始化了一个Client_ID常量,用于标识我们的 JavaScript 客户端与代理的连接。

每个 Paho JavaScript MQTT 客户端必须在连接到代理时具有唯一的主机名、端口客户端 ID组合。为了确保我们可以在单台计算机上运行多个网页进行测试和演示,我们使用随机数为每个网页创建一个准唯一的客户端 ID:

<script type="text/javascript" charset="utf-8">
    messagePubCount = 0;
    const CLIENT_ID = String(Math.floor(Math.random() * 10e16)) // (2)
    const TOPIC   = "led";                                      // (3)

在第 3 行,我们使用led定义了TOPIC常量,这是我们将要订阅和发布的 MQTT 主题的名称。接下来,我们创建我们的客户端实例。

Paho JavaScript MQTT 客户端

在第 4 行,我们创建了我们的 Paho-MQTT 客户端实例并将其分配给client变量。

Paho.MQTT.Client()的参数是代理的主机名和端口。我们通过 Mosquitto 提供此网页,因此代理的主机和端口将与网页相同:

const client = new Paho.Client(location.hostname,        // (4)
                               Number(location.port),
                               CLIENT_ID); 

您可能已经注意到在http://localhost:8083的 URL 中,端口是8083,而在 Python 中我们使用的是端口1883

  • 端口1883是代理上的 MQTT 协议端口。我们的 Python 程序直接连接到代理的这个端口。

  • 我们之前将端口8083配置为 Mosquitto 代理上的 Web 套接字端口。Web 页面可以使用 HTTP 和 Web 套接字协议,而不是 MQTT。

这提出了一个重要的观点。虽然我们在 JavaScript 代码的上下文中使用 MQTT 这个术语,但我们实际上是使用 Web 套接字来代理 MQTT 的想法与代理来回传递。

当我们谈到 MQTT client并在第 4 行创建了client实例时,请记住这是我们 JavaScript 代码的客户端连接到代理

接下来,我们将看到如何连接到代理并注册onConnect处理程序函数。

连接到代理

我们在第 5 行定义了onConnectionSuccess()处理程序,这将在我们的client成功连接到代理后调用。当我们成功连接时,我们将更新网页以反映成功的连接并启用滑块控件:

onConnectionSuccess = function(data) {         // (5)
    console.log("Connected to MQTT Broker");
    $("#connected").html("Yes");
    $("input[type=range].brightnessLevel")
          .attr("disabled", null);

    client.subscribe(TOPIC);                   // (6)
};

client.connect({                               // (7)
   onSuccess: onConnectionSuccess,
   reconnect: true
 });       

接下来,在第 6 行,我们订阅了led主题。在第 7 行,我们连接到了代理。请注意,我们将onConnectionSuccess函数注册为onSuccess选项。

请记住,与 Python 示例类似,总是订阅主题

onSuccess处理程序。这样,如果客户端失去与代理的连接,它可以在重新连接时重新建立订阅。

我们还指定了reconnect: true选项,这样我们的客户端在失去连接时将自动重新连接到代理。

已经观察到,JavaScript Paho-MQTT 客户端在失去连接后可能需要一分钟才能重新连接,所以请耐心等待。这与 Python Paho-MQTT 客户端形成对比,后者几乎可以立即重新连接。

接下来,我们有另外两个处理程序需要审查。

onConnectionLost 和 onMessageArrived 处理程序方法

在以下代码中,第(8)行和(9)行,我们看到如何使用 Paho-MQTT 的client实例注册onConnectionLostonMessageArrived处理程序:

client.onConnectionLost = function onConnectionLost(data) {    // (8)
  ...
}

client.onMessageArrived = function onMessageArrived(message) { // (9)
   ...
}

这两个函数在原则上类似于之前第三章中 socket.io 示例中的相应函数,即它们基于它们各自的datamessage参数中的数据更新滑块和网页文本。

接下来,我们有我们的文档准备函数。

JQuery 文档准备函数

最后,在第(10)行,我们遇到了文档准备函数,其中我们初始化了我们的网页内容并注册了滑块的事件监听器:

$(document).ready(function() {                                   // (10)
    $("#clientId").html(CLIENT_ID);

    // Event listener for Slider value changes.
    $("input[type=range].brightnessLevel").on('input', function() {
        level = $(this).val();

        payload = {
            "level": level
         };

        // Publish LED brightness.
        var message = new Paho.Message(                         // (11)
           JSON.stringify(payload)
        );

        message.destinationName = TOPIC;                        // (12)
        message.qos = 2;
        message.retained = true;                                // (13)
        client.send(message);
    });
});

在第(11)行的滑块事件处理程序中,我们看到了如何创建一个 MQTT 消息。请注意JSON.stringify(payload)的使用。Paho.Message构造函数期望一个String参数,而不是一个Object,因此我们必须将 payload 变量(它是一个Object)转换为字符串。

从第(12)行开始,我们将消息发布主题设置为led,并在标记其 QoS 级别为 2 之前,使用message.destinationName = TOPIC

接下来,在第(13)行,通过message.retained = true,我们指示希望保留此消息,以便它会自动传递给订阅led主题的新客户端。保留此消息是使mqtt_led.py能够在重新启动时重新初始化 LED 的先前亮度。

干得好!我们现在已经涵盖了简单基于 MQTT 的应用程序的 Python 和 JavaScript 两方面。

总结

在这一章中,我们探讨并实践了 MQTT 的核心概念。在您的树莓派上安装和配置 Mosquitto MQTT 代理之后,我们直接开始学习了一系列命令行示例。我们学习了如何发布和订阅 MQTT 消息,如何理解主题构建和名称层次结构,以及如何将 QoS 级别附加到消息上。

我们还涵盖了 MQTT 代理提供的两种机制,即持久连接和保留消息,用于存储消息以供以后传递。我们通过探索一种称为Will的特殊消息和主题类型来结束了我们对 MQTT 概念的讲解,其中客户端可以向代理注册一条消息,在客户端突然失去连接时自动发布到主题。

接下来,我们回顾并讲解了一个使用 Paho Python MQTT 库订阅 MQTT 主题并根据接收到的消息控制 LED 亮度的 Python 程序。然后我们讲解了一个使用 Paho JavaScript MQTT 库构建的网页,该网页发布了 Python 程序消费的消息。

您现在已经掌握了 MQTT 的工作知识,并且有一个可以用于自己的物联网应用的实用代码框架。这是我们在之前章节中探讨过的其他网络方法和代码框架的补充,例如 dweet.io 服务、Flask-RESTful 和 Flask-SocketIO。您用于项目的方法取决于您要创建什么,当然还取决于您个人的偏好。对于较大的项目和需要与外部系统集成的项目,您可能需要同时利用多种方法,甚至需要研究和探索其他技术。我毫不怀疑,到目前为止我们所涵盖的其他网络方法的学习和理解将对您理解遇到的其他方法有所帮助。

在下一章《将 Python 连接到物理世界》中,我们将探讨一系列与将树莓派连接到世界的主题相关的话题。我们将介绍流行的 Python GPIO 库选项,以及 GPIOZero 和 PiGPIO,并研究与树莓派一起使用的不同类型的电子接口选项和配置。我们还有一个全面的练习,我们将向您的树莓派添加一个模数转换器,并使用它创建一个程序来探索 PWM 技术和概念。

问题

最后,这里有一些问题供您测试对本章材料的了解。您将在书的评估部分找到答案:

  1. 什么是 MQTT?

  2. 您保留的 MQTT 消息从未被传递。您应该检查什么?

  3. 在什么条件下,MQTT 代理会发布遗嘱消息?

  4. 您选择使用 MQTT 作为物联网应用程序的消息传递层,并且必须确保消息被发送和接收。所需的最低 QoS 级别是多少?

  5. 您使用 MQTT 开发了一个应用程序,并使用 Mosquitto 代理,但现在您需要使用不同的代理。这对您的代码库和部署配置意味着什么?

  6. 在代码中的哪个位置(提示:哪个处理程序方法)应该订阅 MQTT 主题,以及为什么?

进一步阅读

在本章中,我们从操作层面介绍了 MQTT 的基础知识。如果您想从协议和数据层面了解更多关于 MQTT 的知识,HiveMQ(一个 MQTT 代理和服务提供商)在www.hivemq.com/blog/mqtt-essentials-part-1-introducing-mqtt上提供了一系列关于 MQTT 协议的精彩 11 部分系列文章。

Mosquitto MQTT 代理和客户端工具的主页位于以下 URL:

我们在本章中使用的 Paho-MQTT 库的文档和 API 参考资料可在以下 URL 找到:

除了 MQTT,HTTP RESTful API 和 Web Sockets 之外,还有一些专为受限设备设计的补充通信协议,称为 CoRA 和 MQTT-NS。 Eclipse Foundation 在www.eclipse.org/community/eclipse_newsletter/2014/february/article2.php上提供了这些协议的摘要。

第二部分:与物理世界互动的实用电子学

在本节中,我们将探讨与使用树莓派的P1 引脚连接物理世界的相关概念,这是主板上的一组大引脚,我们通常称之为GPIO 引脚

本质上,这一部分是软件世界和电子世界之间的桥梁。我们的目标是涵盖您需要了解的核心术语和实际概念,以便开始与简单和复杂的电子设备进行接口。到本节结束时,您将具备进一步探索和研究将电子设备与树莓派接口的挑战的知识,并能够根据您的用例和兴趣进行明智决策并进行有针对性的研究。

本节包括以下章节:

  • 第五章,将您的树莓派连接到物理世界

  • 第六章,软件工程师的电子学 101

第五章:将您的 Raspberry Pi 连接到物理世界

在本章中,我们将探讨与将您的 Raspberry Pi 连接到物理世界相关的硬件和软件概念。我们将介绍由 GPIO 库使用的流行编号方案,以引用您的 Raspberry Pi 上的 GPIO 引脚,并概述流行的 GPIO 库,除了我们在之前章节中使用的 GPIOZero 和 PiGPIO 库。正如我们将会了解的那样,理解 GPIO 编号方案对于确保您理解 GPIO 库如何与 GPIO 引脚一起工作至关重要。

在我们完成对 Raspberry Pi 的许多不同方式进行电子接口的概念概述和讨论之前,我们的旅程还将包括对两个重要的电子概念-脉宽调制PWM)和模数转换的详细练习和实际演示。

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

  • 理解 Raspberry Pi 引脚编号

  • 探索流行的 Python GPIO 库

  • 探索 Raspberry Pi 的电子接口选项

  • 与模数转换器进行接口

技术要求

要执行本章的练习,您需要以下物品:

  • Raspberry Pi 4 Model B

  • Raspbian OS Buster(带桌面和推荐软件)

  • 至少 Python 版本 3.5

这些要求是本书中代码示例的基础。可以合理地期望,只要您的 Python 版本是 3.5 或更高,代码示例应该可以在 Raspberry Pi 3 Model B 或 Raspbian OS 的不同版本上无需修改即可运行。

您可以在 GitHub 存储库的以下 URL 中的chapter05文件夹中找到本章的源代码:github.com/PacktPublishing/Practical-Python-Programming-for-IoT

您需要在终端中执行以下命令来设置虚拟环境并安装本章代码所需的 Python 库:

$ cd chapter05              # Change into this chapter's folder
$ python3 -m venv venv      # Create Python Virtual Environment
$ source venv/bin/activate  # Activate Python Virtual Environment
(venv) $ pip install pip --upgrade        # Upgrade pip
(venv) $ pip install -r requirements.txt  # Install dependent packages

以下依赖项是从requirements.txt中安装的:

除了前述的安装,我们在本章的练习中还需要一些物理电子组件:

  • 1 x 5 mm 红色 LED

  • 1 x 200 Ω电阻器-其色带将是红色,黑色,棕色,然后是金色或银色

  • 1 x ADS1115 ADC 拆分模块(例如,www.adafruit.com/product/1085

  • 2 x 10 kΩ电位器(范围在 10K 到 100K 之间的任何值都适用)

  • 一个面包板

  • 母对母和母对公跳线(也称为杜邦线)

理解 Raspberry Pi 引脚编号

到目前为止,您可能已经注意到您的 Raspberry Pi 上有很多引脚突出!自第二章 使用 Python 和物联网入门以及所有后续章节中,我们已经通过引用它们来引用这些引脚,例如GPIO 引脚 23,但这是什么意思?是时候我们更详细地了解这一点了。

有三种常见的方式可以引用 Raspberry Pi 的 GPIO 引脚,如图 5.1所示:

图 5.1 - GPIO 引脚编号方案

在之前的所有章节中,我们一直从 PiGPIO 的角度讨论 GPIO 引脚,它使用BroadcomBCM编号方案。BCM 是 Python GPIO 库中最常用的方案,我们将很快讨论的 GPIO 库都专门或默认使用 BCM。然而,了解其他方案的存在是有用的,因为它将有助于阅读或调试您在互联网和其他资源上遇到的代码片段。

GPIO 和引脚这两个术语在识别引脚时可能会被宽泛使用。您需要解释诸如GPIO 23引脚 23之类的用语,考虑到它所使用的上下文和方案。

让我们探索这些替代方案,如图 5.1所示:

  • Broadcom/BCM 编号:这指的是树莓派中 Broadcom 芯片的 GPIO 编号。使用 BCM 编号时,当我们说GPIO 23时,我们指的是在 BCM 引脚图中标记为GPIO 23。这是我们在本书中使用的方案,用于 GPIOZero 和 PiGPIO 示例。

  • 物理/板/P1 标头:在这种编号方案中,使用 P1 标头的物理引脚编号,例如,BCM GPIO 23 = 物理引脚 16。

  • WiringPi:这是一个名为 WiringPi 的流行的 C GPIO 库,引入了自己的引脚映射方案。由于 WiringPi 的成熟度(有一个 Python 端口),您会不时遇到这个方案——继续我们的例子,BCM GPIO 23 = 物理引脚 16 = WiringPi 引脚 4。

还有其他用于引用引脚和接口的方法和命名,需要注意的包括以下内容:

  • 虚拟文件系统:在/sys上挂载了一个虚拟文件系统,用于一般 GPIO 访问,/dev/*i2c用于 I2C,/dev/*spi*用于 SPI,/sys/bus/w1/devices/*用于 1-wire 设备。

  • 替代引脚功能图 5.1中的前面的 BCM 图表列出了 GPIO 引脚编号,以及括号中的 PWM0、I2C0 和 SPI0 等替代引脚功能。这些代表了引脚可以执行的基本数字 I/O 之外的替代角色。

  • 总线/通道编号:对于 SPI 和 I2C 接口以及硬件 PWM,库通常会使用总线或通道编号。例如,我们可以使用 BCM GPIO 18 作为通用数字输入和输出,或者我们可以在其备用功能模式下将其用作 PWM 通道 0 的硬件 PWM 输出。

pinout.xyz网站是一个探索引脚命名、替代功能和方案映射的好资源。

现在您已经了解了在树莓派上引用 GPIO 引脚可以使用的不同方案。虽然 BCM 方案往往是基于 Python 的 GPIO 库中最常见和通用的方案,但绝对不能假设一个 GPIO 库、代码示例,甚至是您正在使用的面包板布局或原理图图使用 BCM 方案来引用 GPIO 引脚。代码中使用的方案与用于将电子设备连接到树莓派的 GPIO 引脚的方案之间的不匹配是导致电路无法工作的常见错误。

我经常看到人们(我自己也这样做过!)在他们的电路与他们在网上找到的代码示例不匹配时,责怪他们的接线或认为电子元件必须是有故障的。作为诊断的第一步,请检查代码使用的引脚编号方案是否与您用来连接树莓派的 GPIO 引脚的方案相匹配。

现在我们了解了不同 GPIO 编号方案的使用和重要性,让我们继续并审查流行的 Python GPIO 库。

探索流行的 Python GPIO 库

如果你和我一样,当你第一次开始使用树莓派时,你可能只是想控制东西。如今,对于许多开发人员来说,使用树莓派进行物理计算的第一步将是通过官方树莓派网站和 GPIOZero 库。然而,当你玩弄按钮、LED 和电机等简单电子设备一段时间后,你可能会想要进行更复杂的接口。如果你已经迈出了这一步,或者即将迈出这一步,你可能会发现自己处于 GPIO 库和选项的令人困惑的世界。本节旨在通过介绍更受欢迎的选项来帮助你在这条道路上导航。

我在10xiot.com/gpio-comp-table上维护了一个 Python GPIO 库的摘要和比较表(包括以下部分未列出的其他库)。

我们将从 GPIOZero 开始对 GPIO 库进行概述。

审查 GPIOZero-初学者的简单接口

GPIOZero 库的重点是简单性,使其成为初学者进入物理计算和接口电子设备的无忧库。它通过抽象化底层技术复杂性来实现易用性,并允许您编写处理设备外围设备(如 LED、按钮和常见传感器)的代码,而不是编写直接管理引脚的低级别代码。

从技术上讲,GPIOZero 实际上并不是一个完整的 GPIO 库,它是围绕其他用于执行实际 GPIO grunt 工作的 GPIO 库的简化包装器。在第二章中,使用 Python 和 IoT 入门,我们看到了在 GPIOZero 和 PiGPIO 中的一个按钮和 LED 示例,说明了这一点。

以下是 GPIOZero 的主要亮点:

  • 描述:为初学者设计的高级 GPIO 库

  • 优点:易于学习和使用,具有出色的文档和许多示例

  • 缺点:在简单的电子接口之外的用途上有限

  • 网站gpiozero.readthedocs.io

接下来,我们将审查 RPi.GPIO,一个流行的低级 GPIO 库。

审查 RPi.GPIO-初学者的低级 GPIO

我们之前提到,GPIOZero 的本质是编写处理设备和组件的代码。而 RPi.GPIO 采用了一种不同且更经典的方法,我们编写的代码直接与 GPIO 引脚进行交互和管理。RPi.GPIO 是树莓派和电子学的流行低级介绍,因此您会发现许多使用它的示例在互联网上。

GPIOZero 文档中有一个关于 RPi.GPIO 的很好的部分,其中它解释了在 GPIOZero 和 RPi.GPIO 中等效的代码示例。这是一个很好的资源,可以开始学习更低级别的引脚级编程概念。

还有一个名为 RPIO 的库,它被创建为 RPi.GPIO 的性能替代品。RPIO 目前没有维护,并且不适用于树莓派 3 或 4 型号。

以下是 RPI.GPIO 的主要亮点:

  • 描述:轻量级低级 GPIO

  • 优点:成熟的库,在互联网上可以找到许多代码示例

  • 缺点:轻量级意味着它不是面向性能的库,没有硬件辅助的 PWM

  • 网站pypi.python.org/pypi/RPi.GPIO

接下来,我们将看一看另一个用于控制复杂设备的高级库。

审查 Circuit Python 和 Blinka-用于复杂设备的接口

Blinka 是 Circuit Python(circuitpython.org)的 Python 兼容层,这是专为微控制器设计的 Python 版本。它由电子公司 Adafruit 创建和支持,该公司分发许多电子扩展板和小工具。Adafruit 为其许多产品系列提供高质量的 Circuit Python 驱动程序,基本上延续了 GPIOZero 易用性的理念,适用于更复杂的设备。

在本章的后面,我们将使用 Blinka 和 Circuit Python 驱动程序库来为我们的 Raspberry Pi 添加模拟到数字功能,以使用 ADS1115 ADC 扩展模块。

以下是 Blinka 的主要亮点:

  • 摘要:用于控制复杂设备的高级库

  • 优点:无论您的经验水平如何,都可以轻松使用支持的设备

  • 缺点:对于基本 IO,它使用 RPi.GPIO,因此具有相同的基本限制

  • 网站pypi.org/project/Adafruit-Blinka

接下来,我们将介绍 Pi.GPIO,一个功能强大的低级 GPIO 库。

回顾 PiGPIO - 低级 GPIO 库

在功能和性能方面,PiGPIO 被认为是树莓派最完整的 GPIO 库选项之一。其核心是用 C 实现的,并且有一个官方的 Python 端口可用。

从架构上讲,PiGPIO 由两部分组成:

  • pigpiod 守护程序服务提供对底层 PiGPIO C 库的套接字和管道访问。

  • PiGPIO 客户端库使用套接字或管道与 pigpiod 服务进行交互。正是这种设计使得 PiGPIO 可以通过网络实现远程 GPIO 功能。

以下是 PiGPIO 的主要亮点:

  • 描述:高级低级 GPIO 库

  • 优点:提供了许多功能

  • 缺点:需要额外的设置;简单的文档假设了对底层概念的了解

  • 网站(Python 端口)abyz.me.uk/rpi/pigpio/python.html

在我们继续下一个库之前,我想提醒您一个这个库独有且非常有用的功能 - 远程 GPIO。

使用 PiGPIO(和 GPIOZero)探索远程 GPIO

一旦您在树莓派上启动了 pigpiod 服务(在第一章 设置您的开发环境中介绍),有两种方法可以使您的代码远程运行,通过远程,我的意思是您的程序代码可以在任何计算机上运行(不仅仅是树莓派),并控制远程树莓派的 GPIO。

方法 1:此方法涉及将远程树莓派的 IP 或主机地址传递给 PiGPIO 构造函数。使用这种方法,您还可以通过创建额外的pigpio.pi()实例来与多个树莓派 GPIO 进行接口。例如,在以下示例中,对pi实例调用的任何方法将在运行 pigpiod 服务的192.168.0.4主机上执行:

# Python Code.
pi = pigpio.pi('192.168.0.4', 8888) # Remote host and port (8888 is default if omitted)

方法 2:第二种方法涉及在计算机上设置环境变量并运行您的 Python 代码(您的 Python 代码只需要使用默认的 PiGPIO 构造函数,pi = pigpio.pi()):

# In Terminal
(venv) $ PIGPIO_ADDR="192.168.0.4" PIGPIO_PORT=8888 python my_script.py

远程 GPIO 可以成为一个很好的开发辅助工具,但会增加代码与 GPIO 引脚交互的延迟,因为数据通过网络传输。这意味着它可能不适用于非开发版本。例如,按钮按下可能感觉不够灵敏,对于需要快速定时的用例,远程 GPIO 可能不切实际。

您可能还记得第二章 使用 Python 和物联网入门中提到,GPIOZero 可以使用 PiGPIO 引脚工厂,当这样做时,GPIOZero 自动获得免费的远程 GPIO 功能!

最后,因为这是 PiGPIO 库的一个独特特性,如果我们想要远程 GPIO 功能,所有的代码都必须使用这个库。如果你安装第三方 Python 库来驱动一个电子设备,并且它使用(例如)RPi.GPIO,这个设备就不支持远程 GPIO。

接下来,我们将看一下两个常见的用于 I2C 和 SPI 通信的低级库。

审查 SPIDev 和 SMBus - 专用的 SPI 和 I2C 库

当使用 I2C 和 SPI 设备时,你将会遇到 SPIDev 和 SMBus 库(或类似的替代品)。SPIDev 是一个用于 SPI 通信的流行的低级 Python 库,而 SMBus2 是一个用于 I2C 和 SMBus 通信的流行的低级 Python 库。这两个库不是通用库,不能用于基本的数字 IO 引脚控制。

当开始时,你不太可能直接使用这些 I2C 或 SPI 库。相反,你将使用更高级的 Python 库来处理 SPI 或 I2C 设备,而这些库在底层会使用这些低级库来与物理设备进行通信。

以下是 SPIDev 和 SMBus2 的主要亮点:

  • 描述:这些是用于 SPI 和 I2C 接口的低级库。

  • 优点:使用低级库可以完全控制 SPI 或 I2C 设备。许多高级便利包只暴露最常用的功能。

  • 缺点:利用这些低级库需要你解释和理解如何使用低级数据协议和位操作技术与电子设备进行接口。

  • SPIDev 网站pypi.org/project/spidev

  • SMBus2 网站pypi.org/project/smbus2

为了完成关于 GPIO 库的部分,让我简要讨论一下为什么这本书主要基于 PiGPIO 库。

为什么 PiGPIO?

你可能会想知道,为什么在所有的选择中,我选择在这本书中主要使用 PiGPIO。作为这本书的读者,我假设你在编程和技术概念方面有很好的基础,并且使用和学习 PiGPIO 这样的库不会超出你的能力范围。如果你打算在 Python 中构建更复杂的物联网项目,并超越 GPIOZero 和 RPi.GPIO 提供的基础知识,PiGPIO 是一个全面的库。

你会发现 PiGPIO 的 API 和文档被分为初学者、中级和高级部分,因此在实践和学习过程中,你可以根据自己的经验水平和需求混合使用库的 API。

我们已经完成了对几种流行的 GPIO 库的探索,并审查了它们的基本架构和设计。接下来,我们将把注意力转向通过其他方法连接和控制树莓派上的电子设备。

探索树莓派的电子接口选项

我们刚刚涵盖了 GPIO 的软件部分,现在我们将把注意力转向电子方面。树莓派提供了许多标准的接口方式,可以连接简单和复杂的电子设备。通常,你的电子元件和模块的选择将决定你需要使用哪种接口技术,有时你可能会有选择的余地。

无论你是否有选择,你对不同选项的了解将帮助你理解电路及其相应代码背后的原因,并帮助你诊断和解决可能遇到的任何问题。

在接下来的部分中,我们将探索概念,然后进行实际练习。我们将从数字 IO 开始。

理解数字 IO

树莓派的每个 GPIO 引脚都可以执行数字输入和输出。数字简单地意味着某物要么完全开启,要么完全关闭——没有中间状态。在之前的章节中,我们一直在处理简单的数字 IO:

  • 我们的 LED 要么是开启的,要么是关闭的。

  • 我们的按钮要么被按下(开启),要么未被按下(关闭)。

您将遇到几个可互换使用的术语来描述数字状态,包括以下内容:

  • 开 = 高 = 真 = 1

  • 关闭 = 低 = 假 = 0

数字 IO 是一种基本 IO 形式。模拟 IO 是另一种,因此我们将在下面探讨它。

理解模拟 IO

而数字处理完全开启和关闭状态,模拟处理程度——开启、关闭或介于两者之间。想象一下你家里的窗户。在数字世界中,它可以完全打开(数字高)或完全关闭(数字低);然而,在现实中,它是模拟的,我们可以将其打开到完全关闭和完全打开之间的某个位置,例如,打开四分之一。

模拟电子元件的简单和常见示例包括以下内容:

  • 电位器(也称为旋钮):这是一个产生一系列电阻值的旋钮或滑块。现实世界的例子包括音量控制和加热器恒温控制。

  • 光敏电阻(LDR):这些是用于测量光照水平的电子元件,您会在自动夜灯中找到它们。

  • 热敏电阻:这些是用于测量温度的电子元件,您可能会在加热器、冰箱或任何需要测量温度的地方找到它们。

树莓派没有模拟 IO 功能,因此我们需要使用外部电子设备,称为模数转换器ADC)来读取模拟输入,这将是本章后面一个实际示例的核心重点,标题为与模数转换器进行接口

要输出模拟信号,我们有两个选择——要么使用数模转换器DAC),要么使用称为 PWM 的数字技术从数字输出产生类似模拟的信号。我们不会在本书中涵盖 DAC,但是我们将深入探讨 PWM,接下来我们将进行。

理解脉宽调制

脉宽调制PWM是一种通过快速脉冲引脚的开和关来产生介于完全开启(高电平)和完全关闭(低电平)之间的平均电压的技术。通过这种方式,它有点像从数字引脚提供伪模拟输出,并且用于各种控制应用,例如改变 LED 的亮度、电机速度控制和舵机角度控制。

PWM 由两个主要特征定义:

  • 占空比:引脚高电平的时间百分比

  • 频率:占空比重复的时间周期

图 5.2所示(对于固定频率),50%的占空比意味着引脚高电平占一半时间,低电平占一半时间,而 25%的占空比意味着引脚只有 25%的时间是高电平。虽然没有画出来,0%的占空比意味着引脚高电平占 0%的时间(始终低电平),因此实际上是关闭的,而 100%的占空比则始终是高电平:

图 5.2 - PWM 占空比上述图表摘自en.wikipedia.org/wiki/File:Duty_Cycle_Examples.png,作者为 Thewrightstuff。它属于 CC BY-SA 4.0:creativecommons.org/licenses/by-sa/4.0/deed.en

在树莓派上使用 PWM 很容易,尽管有其他方法可以创建 PWM 信号,我们将在下面看到。

创建 PWM 信号

不同的 GPIO 库以不同的方式生成 PWM 信号。三种常见的技术如下:

  • 软件 PWM:PWM 信号的频率和占空比定时由代码生成,并且可以在任何 GPIO 引脚上使用。这是创建 PWM 信号的最不准确的方法,因为定时可能会受到繁忙的树莓派 CPU 的不利影响。

  • 硬件定时 PWM:使用 DMA 和 PWM/PCM 硬件外设进行 PWM 定时。它非常精确,并且适用于任何 GPIO 引脚。

  • 硬件 PWM:硬件 PWM 完全通过硬件提供,并且是创建 PWM 信号的最准确的方法。树莓派有两个专用的硬件 PWM 通道,通过 GPIO 引脚 18 和 12 标记为 PWM0,通过 GPIO 引脚 13 和 19 标记为 PWM1(参见图 5.1)。

仅仅连接到 GPIO 12、13、18 或 19 并不能获得硬件 PWM。这些 GPIO 是 BCM GPIO,其替代功能列出了 PWM。如果要使用硬件 PWM,必须满足两个基本要求。首先,您使用的 GPIO 库必须支持硬件 PWM。其次,您必须正确使用库及其硬件 PWM 功能,这将在库的 API 文档中详细说明。共享相同硬件 PWM 通道的引脚将获得相同的占空比和频率,因此虽然有四个硬件 PWM 引脚,但只有两个唯一的 PWM 信号。

要使用哪种 PWM 技术将始终取决于您要构建的内容以及 PWM 信号需要多精确。有时,您将直接控制您的项目使用的 GPIO 库(因此 PWM 技术),而其他时候——特别是在使用第三方更高级的 Python 库时——您将被迫使用库开发人员使用的任何 PWM 技术。

一般规则是,当我控制 GPIO 库选择时,尽可能避免使用软件 PWM。如果我使用 PiGPIO 进行开发,那么我更倾向于使用硬件定时 PWM,因为我可以在任何 GPIO 引脚上使用它。

关于我们之前介绍的 GPIO 库,它们对 PWM 的支持如下:

  • GPIOZero:继承自其引脚工厂实现的 PWM 方法

  • RPi.GPIO:仅支持软件 PWM

  • PiGPIO:硬件定时 PWM 和硬件 PWM

  • Blinka:仅支持硬件 PWM

您可以连接外部硬件 PWM 模块到您的树莓派(通常通过 I2C),这将给您更多的硬件 PWM 输出。

现在我们已经看到了 PWM 信号可以被创建的三种方式,接下来我们将看 SPI、I2C 和 1-wire 接口。

理解 SPI、I2C 和 1-wire 接口

串行外围接口电路SPI)、I2C和 1-wire 是标准化的通信接口和协议,允许非平凡的电子设备进行通信。这些协议可以直接通过一些操作和数学运算来使用,也可以通过使用更高级的 Python 驱动程序模块间接地与电子外围设备一起工作,后者对于一般用途更为常见。

通过这些协议工作的设备的示例包括以下内容:

  • 模数转换器(SPI 或 I2C)

  • LED 灯带和 LCD 显示器(SPI 或 I2C)

  • 环境传感器,如温度传感器(1-wire)

本章稍后我们将更详细地探讨 I2C,当我们连接模数转换器到树莓派时。

最后,我们有串行通信和 UART。

理解串行/UART 协议

通用异步收发器UART)是一种已经存在很长时间并且在 USB 普及之前广泛使用的串行通信协议。UART 实际上是指用于实现串行协议的电子硬件,尽管它也可以在纯软件中实现。

今天,SPI 或 I2C 往往优先于 UART。GPS 接收器是串行通信仍然普遍存在的一个常见例子。如果您曾经将 Arduino 连接到 PC 进行烧录或调试,那么设备使用的是串行通信协议,Arduino 中存在 UART 硬件。

我们现在已经学会了许多标准的方法,可以用来将电子设备与树莓派进行接口连接,包括模拟和数字电子、PWM、I2C 和 SPI 等线路协议以及串行通信。随着我们在本书中的继续,我们将开始看到许多这些接口选项的实际应用,并了解哪种类型的接口适用于哪种类型的电子设备。

接下来,我们将通过向树莓派添加模数转换器来看一下本章我们已经涵盖的一些概念。

与模数转换器进行接口连接

恭喜您走到了这一步。我猜想您在阅读了这么多之后迫不及待地想要开始编写一些代码了!

现在我们将改变步调,并应用我们刚刚学到的知识,向您的树莓派添加一个 ADS1115 模数转换器。一个典型的 ADS1115 分立模块的示例如下图所示:

图 5.3 - ADS1115 分立模块

ADC 是一个非常方便的附加功能,因为这样就可以让您接触到模拟元件和设备的世界,否则这些设备是无法与树莓派一起使用的。

作为这个实际练习的一部分,我们将连接两个电位器(也称为电位器)到 ADS1115,并在 Python 中读取它们的值。我们将使用这些值来通过改变其占空比和频率来创建 PWM 信号。我们将通过观察它如何影响 LED 以及波形在一个名为 PiScope 的程序中如何变化来看到改变这些参数的效果,这是 PiGPIO 系列实用程序的一部分。

我们将在第六章 软件工程师的电子学 101中更详细地讨论电位器。

为了进行以下练习,请记住我们需要本章开头列出的电子元件,包括 ADS1115 分立模块。ADS1115 是一种常见且功能强大的模数转换器,它使用 I2C 连接到其主设备(在我们的案例中是树莓派)。

以下是我们从其数据表中提取的 ADS1115 的核心规格,这些规格是我们练习所需的:

  • 工作电压:2 至 5 伏特(所以我们知道它将与树莓派的 3.3 伏逻辑兼容)

  • 接口:I2C

  • 默认 I2C 地址:0x48

ADS1115 上的端子如下:

  • Vcc & GND:设备的电源。

  • SCL:时钟信号,用于同步主从之间的通信。

  • SDA:数据信号,用于在树莓派和 ADS1115 之间发送数据。

  • ADDR:如果需要,此端子可用于更改默认地址。

  • ALTR:高级用途的警报信号(我们不需要这个)。

  • A0 - A3:模拟输入通道(我们将把电位器连接到 A0 和 A1)。

在继续之前,请确保您的树莓派上已启用 I2C 接口。我们在第一章 设置您的开发环境中介绍了启用接口(包括 I2C)的步骤。

首先,让我们从在面包板上构建我们需要的电路开始。

构建 ADS1115 ADC 电路

让我们为本章的练习建立我们的面包板电路。我们将分步构建我们的电路,首先放置核心元件,如下图所示:

图 5.4 - 面包板 ADC 电路(3 部分之一)面包板上离散元件和导线的整体布置和放置并不是特别重要。然而,元件和导线之间创建的连接非常重要!如果您需要关于面包板、它们的工作原理以及最重要的是孔如何电气连接的复习,请参阅第二章 Python 和物联网入门

以下是如何在面包板上布置组件的方法。以下步骤编号与图 5.4中编号的黑色圆圈相匹配:

  1. 将 ADS1115 放在面包板上。

  2. 将电位器 VR1 放在面包板上。所示的电位器是全尺寸电位器。如果您有不同尺寸的电位器,它们的引脚配置可能跨越较少的面包板孔。

  3. 将电位器 VR2 放在面包板上。

  4. 将电阻放在面包板上。

  5. 将 LED 放在面包板上,注意确保其阴极腿与电阻共享同一行(在 D29 和 E29 孔上说明)。

接下来,我们将按照以下方式连接 ADS1115:

图 5.5 - 面包板 ADC 电路(第二部分)

以下是要遵循的步骤。这次,以下步骤编号与图 5.5中编号的黑色圆圈相匹配:

  1. 将树莓派的+3.3 伏引脚连接到面包板的正电源轨。

  2. 将 ADS1115 上的 VDD 端子连接到面包板的正电源轨。

  3. 将 ADS1115 上的 GND 端子连接到面包板的负电源轨。

  4. 将树莓派的 GND 引脚连接到面包板的负电源轨。

  5. 将树莓派上的 SCL 引脚连接到 ADS1115 上的 SCL 端子。

  6. 将树莓派上的 SDA 引脚连接到 ADS1115 上的 SDA 端子。

最后,我们将 LED、电阻和电位器连接起来,如下图所示:

图 5.6 - 面包板 ADC 电路(第三部分)

以下是要遵循的步骤。这次,以下步骤编号与图 5.6中编号的黑色圆圈相匹配:

  1. 将 ADS1115 上的 A0 端子连接到电位器 VR1 的中间腿。

  2. 将 ADS1115 上的 A1 端子连接到电位器 VR2 的中间腿。

  3. 将电位器 VR1 的上腿连接到面包板的负电源轨。

  4. 将电位器 VR1 的下腿连接到面包板的正电源轨。

  5. 将电位器 VR2 的上腿连接到面包板的负电源轨。

  6. 将电位器 VR2 的下腿连接到面包板的正电源轨。

  7. 将电阻的上腿连接到面包板的负电源轨。

  8. 将 LED 的阳极腿连接到树莓派的 BCM GPIO 12 / PWM 0 上。

干得好!您现在已经完成了这个电路。供您参考,图 5.7显示了描述面包板电路的语义图。

作为提醒,我们在第二章中介绍了如何阅读语义图的示例,Python 和物联网入门

我鼓励您在参考面包板布局时围绕这个语义图进行追踪,以了解图表上的线条和标签如何与面包板上的组件和导线相关联。投资时间来理解成对的原理图和面包板电路如何相互关联将有助于增强您直接从原理图创建面包板布局的能力:

图 5.7 - ADC 电路语义图

电路完成后,让我们检查一下树莓派是否能够看到 ADS1115。

确保 ADS1115 已连接到您的树莓派

I2C 设备通过唯一地址(即我们的树莓派)标识其主设备,并且 ADS1115 的默认地址为 0x48。由于 I2C 设备是有地址的,多个设备可以共享树莓派上的相同 I2C 通道(引脚)。

如果有多个设备共享相同地址,您可以更改大多数 IC2 设备上的 I2C 设备。这是 ADS1115 上的 ADDR 端子的目的,您可以在 ADS1115 数据表中找到其使用说明。

Raspbian OS 包含i2cdetect实用程序,用于查询树莓派的 I2C 接口以查找连接的设备。在终端中运行以下命令:

$ i2cdetect -y 1

-y选项假设我们对任何提示都回答是。1是 I2C 总线号。在树莓派 3 或 4 上始终是1。我们期望看到这样的输出:

     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
 00:          -- -- -- -- -- -- -- -- -- -- -- -- --
 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
 40: -- -- -- -- -- -- -- -- 48 -- -- -- -- -- -- --
 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
 70: -- -- -- -- -- -- -- --

我们看到48(十六进制地址)表明我们的树莓派已经检测到了 ADS1115。如果您没有得到这个结果,请检查您的接线,并确保 I2C 已经按照第一章中描述的方式启用。

现在我们已经验证了我们的 ADS1115 对我们的树莓派是可见的,让我们继续读取两个电位器作为模拟输入。

使用 ADS1115 读取模拟输入

现在我们已经将我们的 ADS1115 连接到我们的树莓派,是时候学习如何使用它来读取模拟值,特别是我们两个电位器产生的模拟值。我们很快将使用这些模拟值来产生 PWM 信号,进而控制 LED 的亮度。

我们即将涵盖的代码可以在文件chapter05/analog_input_ads1115.py中找到。请在继续之前查看此文件。

  1. 在终端中运行程序:
(venv) $ python analog_input_ads1115.py
  1. 您应该收到类似以下内容的输出流(您的值和伏特数将不同):
 Frequency Pot (A0) value=3 volts=0.000 Duty Cycle Pot (A1) value= 9286 volts=1.193
 Frequency Pot (A0) value=3 volts=0.000 Duty Cycle Pot (A1) value= 9286 volts=1.193
 ...truncated...
  1. 转动两个电位器并观察输出的变化-具体来说,您会注意到报告的值和伏特数会发生变化。值和电压将在以下范围内:
    • 值范围在 0 到 26294 之间(或附近)
  • 电压范围在 0 到 3.3 伏特(或附近)

输出将如下所示:

 Frequency Pot (A0) value=3 volts=0.000 Duty Cycle Pot (A1) value= 9286 volts=1.193
 Frequency Pot (A0) value=4 volts=0.001 Duty Cycle Pot (A1) value=26299 volts=3.288
 ...truncated...

正如我们将在第六章中讨论的那样,软件工程师的电子学 101,模拟输入是关于读取电压的,就我们这里而言,电压在 0 伏特/GND(我们的参考电压)和+3.3 伏特之间。整数值是 ADS1115 的原始输出,它的最大值取决于 ADS1115 IC 的配置方式(我们使用默认配置)。电压值是根据 ADS1115 配置的数学计算得出的。如果您感兴趣,所有细节都在 ADS1115 数据表和库源代码中。

在高级 ADC 库的表面下,许多低级设置会影响 ADC 芯片的工作方式(只需查看其数据表)。不同的库作者可能以不同的方式实现这些设置,或者使用不同的默认设置。实际上,这意味着相同 ADC 的两个库可能输出不同的原始值(有些库甚至可能不会向程序员提供这个值)。因此,永远不要假设预期的原始输出值是什么,而是依靠电压测量,这总是真相的来源。

当您调整两个电位器时,如果确切的范围末端与 0 和 3.3 伏特不完全匹配,或者值随机地微微抖动,不要担心。当我们处理模拟电子时,这种模糊的结果是预期的。

接下来,我们将检查代码。

理解代码

现在我们已经看到了 ADS1115 ADC 的基本操作,是时候看一下相应的代码,了解我们如何在 Python 中查询 ADS1115 以获取模拟读数。我们下面学到的内容将为本书的第三部分中我们将看到的模拟接口程序奠定基础。

我们将从导入开始我们的代码漫步。

导入

我们可以用两种方法在树莓派上使用 ADS1115 与 Python:

  • 阅读 ADS1115 数据表,并使用较低级别的 I2C,如 SMBus 来实现设备使用的数据协议。

  • 找到一个现成的 Python 库,通过 PyPi 可以使用pip安装。

有几个现成的 Python 模块可用于与 ADS1115 一起使用。我们使用了通过requirement.txt在本章开始时安装的 Adafruit Binka ADS11x5 ADC 库:

import board                                      # (1)
import busio
import adafruit_ads1x15.ads1115 as ADS
from adafruit_ads1x15.analog_in import AnalogIn

从第 1 行开始,我们看到了来自 Circuit Python(Blinka)的boardbusio导入,而以adafruit开头的最后两个导入来自 Adafruit ADS11x5 ADC 库,并用于配置 ADS1115 模块并读取其模拟输入,我们将在下面看到。

ADS1115 设置和配置

在以下代码块的第 2 行,我们使用busio导入来创建一个与 Circuit Python/Blika 的 I2C 接口。board.SLCboard.SDA参数表示我们正在使用树莓派上的专用 I2C 通道(GPIO 2 和 3 的替代功能):

# Create the I2C bus & ADS object.
i2c = busio.I2C(board.SCL, board.SDA)      # (2)
ads = ADS.ADS1115(i2c)

接下来,我们使用预配置的 I2C 接口创建ADS.ADS1115的实例,并将其分配给ads变量。从此刻起,在代码中,当我们与 ADS1115 模块交互时,我们将使用这个实例。

接下来,让我们考虑全局变量。

全局变量

在以下代码片段的第 3 行,我们从几个准常量开始,定义了我们希望通过模拟输入接收的最大和最小电压。当您之前运行代码时,您的端电压范围可能并不完全是 0 和 3.3 伏特。这种情况是可以预期的,并且可能会使程序感觉像电位器无法达到其旋转的端点。A_IN_EDGE_ADJ的值用于在代码中进行补偿。我们将在下一节重新访问这个变量:

A_IN_EDGE_ADJ = 0.002                     # (3)
MIN_A_IN_VOLTS = 0 + A_IN_EDGE_ADJ
MAX_A_IN_VOLTS = 3.3 - A_IN_EDGE_ADJ

接下来,从第 4 行开始,我们创建了两个与连接到我们的电位器的 ADS1115 的A0A1输入相关的AnalogIn实例。通过这些变量,我们确定用户旋转了我们的频率和占空比电位器的程度:

frequency_ch = AnalogIn(ads, ADS.P0)  #ADS.P0 --> A0    # (4)
duty_cycle_ch = AnalogIn(ads, ADS.P1) #ADS.P1 --> A1

接下来,我们来到程序的入口点,我们将在这里读取我们的模拟输入。

程序入口点

我们的程序不断循环,读取每个电位器的模拟输入值,并将格式化输出打印到终端。

在第 5 行,我们看到如何使用frequency_ch.value访问频率电位器的整数值,并使用frequency_ch.voltage访问电压值:

if __name__ == '__main__':
   try:
       while True: 
           output = ("Frequency Pot (A0) value={:>5} volts={:>5.3f} "
                     "Duty Cycle Pot (A1) value={:>5} volts={:>5.3f}")
           output = output.format(frequency_ch.value,          # (5)
                                  frequency_ch.voltage,
                                  duty_cycle_ch.value,
                                  duty_cycle_ch.voltage)
           print(output)
           sleep(0.05)
   except KeyboardInterrupt:
       i2c.deinit()                                            # (6)

最后,请注意程序被包裹在一个 try/except 块中,以捕获Ctrl + C,以便我们可以使用i2c.deinit()进行清理。

现在我们已经看到如何使用 ADS1115 读取模拟输入,接下来,我们将集成 LED。

使用 PWM 控制 LED

现在我们将 LED 添加到代码中,只是我们将以与之前章节不同的方式进行。此练习中 LED 的目的是为了直观地看到改变 PWM 的占空比和频率特性的效果。我们将使用两个电位器的模拟输入来定义 PWM 的占空比和频率。

本节讨论的代码扩展了我们刚刚在chapter05/analog_input_ads1115.py中涵盖的模拟代码示例,以使用 PiGPIO 创建硬件 PWM 信号。

本书提供了另外两个源代码文件,分别使用 PiGPIO 实现硬件定时 PWM 和使用 RPi.GPIO 实现软件 PWM:

  • chapter05/pwm_hardware_timed.py

  • chapter05/pwm_software.py

他们的整体代码类似,不同之处在于用于调用 PWM 的方法和输入参数。我们将在接下来的部分再次访问这些文件,可视化软件和硬件定时 PWM

我们即将讨论的代码可以在chapter05/pwm_hardware.py文件中找到。请在继续之前查看此文件:

  1. 在终端中运行程序并观察输出:
(venv) $ python pwm_hardware.py
Frequency 0Hz Duty Cycle 0%
... truncated ...
Frequency 58Hz Duty Cycle 0%
Frequency 59Hz Duty Cycle 0%
... truncated ...
  1. 调整电位器,直到频率读取为 60 赫兹,占空比读取为 0%。LED 不应点亮。LED 未点亮是因为占空比为 0%,因此 GPIO 12(PWM0)始终为低电平。非常缓慢地转动占空比电位器以增加占空比,并观察 LED 缓慢增加亮度。在 100%的占空比下,GPIO 12(PWM0)始终为高电平 100%的时间,LED 处于全亮状态。

如果您发现终端上打印的占空比在 Pot 移动范围的任一端都没有达到 0%或 100%,请尝试增加代码中A_IN_EDGE_ADJ的值(首先尝试+0.02)。如果您在频率范围和刻度上遇到类似问题,也可以调整此参数。

  1. 旋转占空比刻度,直到它显示小于 100%(例如 98%),然后调整频率刻度。LED 以这个频率闪烁。当你将频率降低到零时,LED 会闪烁得更慢。对于大多数人来说,在大约 50-60 赫兹时,LED 会闪烁得如此之快,以至于它看起来就像是一直开着。请记住,如果占空比为 0%或 100%,频率刻度不起作用!这是因为在占空比的任一端,PWM 信号完全关闭或打开——它不是脉冲,因此频率没有意义。

让我们来检查一下让这个工作的代码。

理解代码

这个示例使用了 PiGPIO 提供的硬件 PWM 功能。与我们之前的示例相同,ADS1115 相关的代码也是一样的,所以我们不会在这里再次介绍它。我们将首先看看额外的全局变量。

全局变量

在以下代码块的第 1 行和第 2 行,我们定义了两个变量,用于最小和最大占空比和频率值。这些值来自 PiGPIO hardware_PWM()方法的 API 文档,我们很快就会看到它们的使用:

MIN_DUTY_CYCLE = 0            # (1)
MAX_DUTY_CYCLE = 1000000
MIN_FREQ = 0                  # (2)
MAX_FREQ = 60 *# max 125000000*

我们已经将MAX_FREQ限制为 60 赫兹,以便我们的肉眼可以观察 LED 的效果。

接下来,我们有一个自定义函数来映射值范围。

范围映射函数

在第 3 行,我们有一个名为map_value()的函数:

def map_value(in_v, in_min, in_max, out_min, out_max):           # (3)
    *"""Helper method to map an input value (v_in)
       between alternative max/min ranges."""* v = (in_v - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
    if v < out_min: v = out_min elifv > out_max: v = out_max
    return v

这种方法的目的是将一个输入值范围映射到另一个值范围。例如,我们使用这个函数将模拟输入电压范围 0-3.3 伏特映射到 0-60 的频率范围。在处理模拟输入时,您经常会使用这样的值映射函数,将原始模拟输入值映射为代码中更有意义的值。

接下来,我们准备创建 PWM 信号。

生成 PWM 信号

下一个代码片段位于主while循环中。

在第 4 行和第 5 行,我们从频率和占空比 Pots 中读取电压值,然后使用map_value()函数将 0-3.3 伏特的电压范围转换为我们在全局变量中定义的所需频率和占空比范围。请注意,我们还将占空比格式化为百分比值以供显示:

frequency = int(map_value(frequency_ch.voltage,                # (4)
                          MIN_A_IN_VOLTS, MAX_A_IN_VOLTS,
                          MIN_FREQ, MAX_FREQ))
 duty_cycle = int(map_value(duty_cycle_ch.voltage,              # (5)
                           MIN_A_IN_VOLTS, MAX_A_IN_VOLTS,
                           MIN_DUTY_CYCLE, MAX_DUTY_CYCLE))

duty_cycle_percent = int((duty_cycle/MAX_DUTY_CYCLE) * 100)
 pi.hardware_PWM(LED_GPIO_PIN, frequency, duty_cycle)           # (6)

在第 6 行,我们使用pi.hardware_PWM()来使用树莓派的 PWM 硬件在 LED 引脚上生成 PWM 信号。

现在我们已经看到了改变 LED 频率和占空比的效果,我们将进行一个练习,使用逻辑分析仪来可视化 PWM 信号。

使用 PiScope 进行 PWM 的可视化探索

让我们进行一个练习,看看逻辑分析仪中的 PWM 波形,逻辑分析仪是一种用于可视化电子信号的设备。尽管 PWM 背后的一般原理在技术上很简单,但在刚开始学习时,通过可视化 PWM 信号的外观和观察其随着占空比和频率的变化而发生的变化,可以帮助学习。

PiGPIO 包含一个我们可以用于此目的的软件逻辑分析仪。现在,我需要指出的是,这是一个基本的软件逻辑分析仪,绝对不能与专业设备相比,但是对于我们的示例和教育来说,它将非常有效,并且不会花费我们任何费用。

让我们下载、安装并运行 PiScope。以下是要遵循的步骤:

  1. 首先,我们必须安装 PiScope。运行以下命令来下载、编译和安装 PiScope:
# Download and install piscope
$ cd ~
$ wget abyz.me.uk/rpi/pigpio/piscope.tar
$ tar xvf piscope.tar
$ cd PISCOPE
$ make hf
$ make install
  1. 使用以下命令运行 PiScope:
$ piscope

我建议在启动 PiScope 并进行此练习之前关闭任何资源密集型应用程序。由于我通过菜单关闭了一些 GPIO,所以下面的屏幕截图并不像您的默认情况下那样显示所有 GPIO。**如果您也关闭了显示器上的 GPIO,请记住保留 SDA(GPIO 2)和/或 SCL(GPIO 3)以进行此练习,因为这会为 PiScope 创建一个连续的输入信号,使显示器保持时间运动。如果没有这个连续的输入,PiScope 会在没有信号输入时暂停显示,因此我们的示例将在占空比或频率为 0 时暂停显示,这将使演示感觉笨拙。

  1. 确保chapter05/pwm_hardware.py程序在终端中运行。

  2. 慢慢地转动占空比和频率旋钮,并观察第 12 行上 PWM 信号的变化。保持我们的频率范围非常低(例如 0 到 60 赫兹)意味着我们可以在 PiScope 逻辑分析仪中轻松观察 PWM 信号:

图 5.8 - 10 赫兹下的 25%占空比

上面的屏幕截图显示了 10 赫兹下的 25%占空比。如果您检查屏幕截图中的最后一行,您会注意到 GPIO 12 在单个周期中高电平占 25%,低电平占 75%。

下面的屏幕截图显示了 10 赫兹下的 75%占空比。如果您检查屏幕截图中的最后一行,您会注意到 GPIO 12 在单个周期中高电平占 75%,低电平占 25%:

图 5.9 - 10 赫兹下的 75%占空比

我们现在已经通过 PiScope 看到了 PWM 信号波形的可视化,PiScope 是 PiGPIO 开发者提供的免费基本软件逻辑分析仪。我们将 PWM 信号可视化的主要目的是为了提供一个视觉辅助工具,帮助您理解 PWM 及其占空比和频率特性。

实际上,当您刚开始并与基本电子集成时,您可能不需要逻辑分析仪,甚至不需要可视化信号。然而,随着您的知识的提升以及在电子集成问题的调试上的需求,我希望这个对逻辑分析仪的基本介绍能够帮助您,并指引您进一步探索的方向。

接下来,我们将指向演示替代 PWM 技术的 Python 源文件。

可视化软件和硬件定时 PWM

我们之前章节的代码示例,使用 PWM 控制 LED使用 PiScope 进行 PWM 可视化,都是使用树莓派的 PWM 硬件创建 PWM 信号。本章的代码以及下表中列出的替代实现演示了硬件定时和软件生成的 PWM 信号的使用。您可能还记得我们在创建 PWM 信号部分讨论过这些替代方案:

文件 详情
pwm_hardware.py 这是使用 PiGPIO 的硬件 PWM(这是本章中看到的代码)。您必须使用 PWM 硬件 GPIO 引脚 12、13、18 或 19。
pwm_hardware_timed.py 这是使用 PiGPIO 的硬件定时 PWM。这将适用于任何 GPIO 引脚。
pwm_software.py 这是使用 RPi.GPIO 的软件 PWM(PiGPIO 不提供软件 PWM)。这将适用于任何 GPIO 引脚。

从功能上讲,这些示例是相同的,它们将改变 LED 的亮度,我预测您会发现硬件和软件 PWM 的表现相似。当您转动频率旋钮时,LED 和 PiScope 的变化会感觉平滑,而硬件定时 PWM 会感觉有些生硬。这是因为硬件定时频率(在 PiGPIO 中)必须是 18 个预定值中的一个,因此当您调整旋钮时,频率的变化不是逐渐的和线性的,而是跳到/从下一个预定义的频率。您将在pwm_hardware-timed.py中的数组中看到这些预定义的频率。

正如之前提到的,软件 PWM 是产生 PWM 信号的最不可靠的方法,因为如果您的树莓派 CPU 变得繁忙,它容易失真。

您可以尝试使用以下步骤创建和可视化 PWM 失真:

  1. 运行pwm_software.py并将占空比设置为高(例如 98%),频率为 60 赫兹。不要使用 100%的占空比,因为这是一个完全开启的状态,你会看到一个水平线,而不是重复的方波形。

  2. 在您的树莓派上启动一个资源密集型程序,比如尝试关闭并重新启动 Chrome 浏览器。

  3. 如果您仔细观察 LED,您可能会注意到 PWM 信号在某些时候会闪烁。或者,您可以在 PiScope 中观察到波形失真,如下截图中的箭头所示。当信号失真时,您会注意到条的宽度不均匀:

图 5.10 - PWM 信号中的失真,50%占空比,50 赫兹

干得好。您刚刚完成了一个详细的实际练习,使用 ADS1115 扩展了您的树莓派,以便您还可以将其与模拟电子设备进行接口。在此过程中,您还学会了如何使用 Python 产生 PWM 信号,看到了这个信号对 LED 的影响,并用 PiScope 进行了可视化观察。

摘要

做得好,因为肯定有很多东西需要我们理解!回顾一下,我们探讨了用于引用 GPIO 引脚的常见编号方案,并回顾了 Python 的流行 GPIO 库。我们还研究了用于将电子设备连接到树莓派的各种接口方法,并进行了一个实际练习,向您的树莓派添加 ADC,并使用它来通过 LED 和 PiScope 逻辑分析仪进行可视化探索 PWM 概念。

您对我们在本章中探讨和实验的基本概念的理解将有助于您理解树莓派如何与电子元件和设备进行接口,并让您第一手地了解我们如何与模拟元件(例如我们的电位计)和复杂设备(即我们的 ADS1115)进行交互。在本书的其余部分,我们将使用和建立许多这些基本原理。

本章主要关注软件库和代码。然而,在下一章《软件工程师的电子学 101》中,我们将把注意力转向电子概念和用于将电子设备与树莓派进行接口的常见电路。

问题

在我们结束时,这里有一系列问题供您测试对本章材料的了解。您将在书的评估部分找到答案:

  1. 哪种串行通信接口允许设备进行级联连接?

  2. 您有一个 I2C 设备,但不知道它的地址。您该如何找到它?

  3. 您第一次开始使用一个新的 GPIO Python 库,但似乎无法使任何 GPIO 引脚工作。您需要检查什么?

  4. 您正在 Windows 上使用 PiGPIO 和远程 GPIO 驱动远程树莓派。现在,您尝试安装一个第三方设备驱动程序库,但在 Windows 下安装失败,但您发现它在树莓派上成功安装了。可能的问题是什么?

  5. 真或假:树莓派上有 3.3 伏和 5 伏的引脚,因此在使用 GPIO 引脚时可以使用任一电压?

  6. 您创建了一个使用舵机的机器人。在简单测试期间,一切都很正常。然而,现在您完成了,您注意到舵机会随机抽搐。为什么?

  7. 当机器人的舵机移动时,您会注意到显示器上出现了一个闪电图标,或者显示器变黑了。这可能是为什么?

进一步阅读

GPIOZero 网站提供了一系列示例,展示了使用 GPIOZero 和 RPi.GPIO 的功能等效示例。这是一个很好的入门资源,可以帮助理解更低级别的 GPIO 编程概念和技术:

以下链接包含了有关本章讨论的接口和概念的额外材料:

第六章:软件工程师的电子学 101

到目前为止,本书大部分内容都集中在软件上。在本章中,我们将转而关注电子学。我们将通过学习基本的电子概念来学习如何将基本的电子传感器和执行器与您的 Raspberry Pi 进行接口。本章中学到的内容将为我们在第三部分“物联网游乐场”中讨论的许多电路奠定基础。

我们将首先介绍您在处理电子学时所需的基本车间工具,并提供实用的建议,以帮助您购买电子元件。接下来,我们将为您提供指南,以帮助您在使用物理 GPIO 引脚时保护您的 Raspberry Pi 不受损害。我们还将讨论电子元件常见的故障方式,以帮助您诊断不工作的电路。

然后我们将进入电子学!在这里,我们将研究两个重要的电子定律——欧姆定律和基尔霍夫定律,并通过一个实际示例来解释为什么我们在早期章节中使用 200Ω电阻器来配合我们的 LED 电路(如果您需要关于此 LED 电路的复习,请参见第二章,使用 Python 和物联网入门)。

接下来,我们将探讨数字和模拟电子学,并讨论用于将它们与您的 Raspberry Pi 集成的核心电路和思想。我们将通过学习逻辑电平转换来结束本章,这是一种实用的技术,用于接口操作不同电压的电子设备。

本章将涵盖以下主题:

  • 装备您的车间

  • 保护您的 Raspberry Pi

  • 电子元件故障的三种方式

  • 用于 GPIO 控制的电子接口原理

  • 探索数字电子学

  • 探索模拟电子学

  • 理解逻辑电平转换

技术要求

要执行本章的练习,您将需要以下内容:

  • Raspberry Pi 4 Model B

  • Raspbian OS Buster(带桌面和推荐软件)

  • 最低 Python 版本 3.5

这些要求是本书中代码示例的基础。只要您的 Python 版本为 3.5 或更高,代码示例应该可以在不需要修改 Raspberry Pi 3 Model B 或使用不同版本的 Raspbian OS 的情况下工作。

您可以在本书的 GitHub 存储库的chapter06文件夹中找到本章的源代码:github.com/PacktPublishing/Practical-Python-Programming-for-IoT

您需要在终端中执行以下命令来设置虚拟环境并安装本章所需的 Python 库:

$ cd chapter06              # Change into this chapter's folder
$ python3 -m venv venv      # Create Python Virtual Environment
$ source venv/bin/activate  # Activate Python Virtual Environment
(venv) $ pip install pip --upgrade        # Upgrade pip
(venv) $ pip install -r requirements.txt  # Install dependent packages

以下依赖项从requirements.txt中安装:

本章所需的硬件组件如下:

装备您的车间

拥有正确的工具和设备对于帮助您组装、构建、测试和诊断电子电路非常重要。以下是您在深入电子学并创建本书中所示电路的过程中需要的基本设备(除了电子元件):

  • 焊接铁:您将需要一个焊接铁(和焊料)来进行一些零散的工作,比如将排针连接到扩展板上或将导线焊接到元件上,以便它们可以插入您的面包板。

  • 焊料:寻找一种通用的 60/40(60%锡和 40%铅)树脂芯焊料,直径约为 0.5 毫米至 0.7 毫米。

  • 吸锡器/真空吸:我们都会犯错误,所以这个设备可以帮助您从接头中去除焊料并撤消您的焊接工作。

  • 湿海绵或抹布:始终保持焊接铁头的清洁,去除积聚的焊料 - 干净的铁头有助于清洁焊接。

  • 剥线器和剪刀:为您的电子工作保留一套剪线器和剥线器。来自其他用途的切割刀刃中的芯片和毛刺会降低其性能。

  • 数字万用表(DMM):入门级的 DMM 适用于一般工作,并将包括一系列标准功能,如电压、电流和电阻测量。

  • 面包板:我强烈建议购买两个全尺寸的面包板并将它们连接在一起,以获得更多的面包板空间。这将使得与面包板和元件一起工作更容易。

  • 杜邦(跳线)电缆:这些是与面包板一起使用的电缆。它们有各种类型:公-公、公-母和母-母。您将需要它们的混合。

  • 松散的排针:这些对于连接杜邦线并使不适合面包板的元件适合面包板非常有用。

  • 外部电源供应:这样您就可以从树莓派外部为电路供电。对于本书的目的,至少您将需要一个可以提供 3.3 和 5 伏的面包板电源。

  • 树莓派外壳:确保您的树莓派有一个外壳。一个没有外壳的树莓派下面的所有裸露的电子元件都是一场等待发生的事故。

  • GPIO 引脚扩展头:这使得与树莓派和面包板一起工作更容易。

如果您还没有上述设备,请在 eBay 和 Banggood 等网站上寻找焊接铁套件面包板入门套件。这些套件通常捆绑了许多列出的项目。

这个清单显示了我们需要的基本工具,但是实际的电子设备和小工具呢?我们接下来会看到。

购买电子模块和元件

本书中使用的所有组件和模块的目录都包含在附录中。在本节中,我想提供一些一般的提示和指导,以帮助您在购买电子元件时提供帮助,以防您之前没有做过太多这方面的工作。我们将从一些购买松散元件时的提示开始。

购买松散的元件

当购买电阻器、LED、按钮、晶体管、二极管和其他元件(我们将在本书的第三部分物联网游乐场 - 与物理世界互动的实际示例中探讨)等松散的元件时,有一些指导原则将帮助您,如下所示:

  • 附录中获取列出的特定组件值和零件号。购买许多备件,因为在学习使用它们时可能会损坏组件。

  • 如果您从 eBay 或 Banggood 等网站购买,请仔细查看物品的详细信息,并最好放大零件的图像并检查所示的零件号。永远不要仅仅依靠列表的标题。许多卖家在标题中添加各种术语以进行搜索优化,这些术语不一定与实际出售的物品相关。

  • 在 eBay 和 Banggood 等网站上搜索诸如电子入门套件之类的术语。您可能可以一次性购买一组松散的元件。

这些观点在购买传感器和模块时也适用,我们将在下一节中讨论。

购买开源硬件模块

我相信你们都知道开源软件,但也有开源硬件。这是一些电子硬件制造商公开发布设计和原理图,以便任何人都可以制造(和销售)这些硬件。您会发现许多来自不同供应商的分立模块(例如我们在第五章中使用的 ADS1115 模块,“将您的树莓派连接到物理世界”),它们具有不同的(或没有)品牌。不同的供应商也可能以不同的颜色制造他们的模块,虽然较少见,但物理布局也可能不同。

模块的核心心脏 - 尤其是更简单的模块 - 通常是一个单一的集成电路IC或芯片)。只要核心 IC 和 I/O 引脚相似,通常可以安全地假设板子将以相同的方式运行。

SparkFun(www.sparkfun.com/)和 Adafruit(adafruit.com/)是两家生产开源硬件的公司,许多其他公司都在克隆他们的产品。当您从这些公司购买产品时,您将获得一个很大的优势,通常他们的产品包括代码示例、教程和使用产品的技巧,并且产品质量很好。是的,您可能需要支付更多的钱,但在刚开始和尤其是对于更复杂的电子产品来说,这样的投资可以节省您大量的时间。便宜的克隆品常常出现故障 - 因此您需要购买两个或更多来规避风险。

我们现在已经介绍了一些建议和技巧,以帮助您装备您的车间并购买电子元件。拥有合适的工具并学会使用它们(特别是焊接,如果这是一项新技能,需要练习)对于使您的电子之旅顺利和高效至关重要。有时,购买散装元件可能会令人困惑,有时也容易出错,特别是在规格或标签上的细微差异可能会产生重大的实际影响的地方,因此如果您不确定,请勤勉地仔细检查并核对您购买的东西。最后,如附录中建议的,购买备用元件。如果一个元件损坏,您需要寻找或等待替换品到货,突然中断您的学习过程是不好玩的!

接下来,我们将讨论一些指南,以帮助您在将电子设备与树莓派连接时保持安全。

保护您的树莓派

在本节中,我们将介绍一些指南和建议,以帮助您在将电子设备与树莓派连接时保持安全。通过谨慎和勤勉的方法,这些指南将帮助您最大程度地减少对树莓派或电子元件造成损坏的潜在风险。

如果一些与电子相关的点,如电压和电流,目前还不清楚,不要担心。我们将在本章和本书的第三部分“物联网游乐场 - 与物理世界互动的实际示例”中涉及这些概念,因此会有更多的上下文:

  • 永远不要向任何输入 GPIO 引脚施加超过 3.3 伏特的电压。更高的电压可能会造成损坏。

  • 永远不要从任何单个输出 GPIO 引脚使用超过 8 毫安(它们可以处理高达〜16 毫安,但默认情况下,保持在 8 毫安以确保可靠的 GPIO 操作)。作为一个经验法则,除非您知道自己在做什么,否则不要为除 LED 和分立模块以外的任何东西供电。在第七章中,“打开和关闭东西”,我们将看看可以用来开关更高电流和电压负载的电路。

  • 永远不要在多个 GPIO 引脚上使用超过合计 50 毫安的电流。

  • 永远不要在配置为输入的 GPIO 引脚上使用超过 0.5 毫安。

  • 在连接或断开连接到树莓派或进行任何更改之前,始终断开电路的电源。

  • 在连接、断开或处理电路之前,一定要停止与 GPIO 引脚交互的所有运行程序。

  • 在给电路供电之前,一定要仔细检查你的布线。

  • 永远不要在电路中替换随机的元件值-它们不具有原理图中显示的正确和预期值。

  • 如果你在树莓派的显示器上看到闪电图标,或者当运行程序时显示器变黑,那就是树莓派告诉你你的电路从树莓派吸取了太多的电力。

  • 永远不要直接连接和使用感应负载和机械设备,比如使用 GPIO 引脚的电机、继电器或磁铁的螺线管。它们可能会吸取太多电流,并引起所谓的EMF flyback现象,这可能会损坏周围的电子设备,包括你的树莓派。

你为树莓派准备的电源最好是 3 安培(15 瓦)。许多手机充电器的额定功率低于这个值,它们的使用是看到闪电图标(或空白显示)的常见原因。

在处理电子设备时,有时候元件会受损或失效。让我们简要地看一下这种情况可能发生的方式。

电子元件失效的三种方式

在处理电子设备时,与软件不同。在软件世界中,我们可以更改代码、破坏代码、调试代码和修复代码,而且可以多次进行这些操作而没有真正的伤害。我们还可以自由备份和恢复状态和数据。在处理电子设备时,我们没有这种奢侈。我们处在物理世界,如果某样东西受损了,那就是最终的!

包括树莓派在内的元件和电路可能因连接不正确、过度供电、供电或输出过多电压、过热,甚至对组件进行错误处理而受损或失效,甚至物理上断裂或被你的身体静电损坏。

当元件失效时,可能会以几种不同的方式失效:

  • 它会在一股烟雾中失败,熔化,或以其他方式显示出受损的物理迹象。

  • 它悄然失败,没有任何视觉指示失败。

  • 它虽然受损,但基本上还是按预期的方式工作,但在将来的某个时候,它会在没有警告的情况下悄然失效。

以物理标志失败是我们想要的结果,因为很明显是什么失败了,需要被替换。这也给了我们一个起点,我们可以开始诊断我们的电路。无声失败和延迟失败是痛苦和耗时的,特别是在开始时。

以下是一些帮助你在开始时构建和调试故障电路的提示:

  • 在连接电源之前,始终要仔细检查电路。

  • 备有备用零件。如果你有已知的好零件可以替换到电路中,那么诊断和测试电路就会更容易。

  • 如果你认为某样东西受损了,那么立即丢弃它。你不需要有故障的零件和好的零件混在一起,特别是当没有明显的损坏迹象时。

接下来,我们将讨论核心电子原理,这些原理决定了电路中为什么以及如何选择元件,并通过我们的 LED 电路来说明这些概念。

GPIO 控制的电子接口原则

虽然这本书不是一本电子理论的书,但有一些核心原则是重要的,因为它们影响电路设计以及它们与你的树莓派的接口。本节的目标是向你介绍为什么电路以某种方式设计以及这与 GPIO 接口有关的基本理解。掌握了这些基本知识,我希望它能激励你更深入地探索核心思想和原则。你会在本章末尾的进一步阅读部分找到建议的资源。

我们将从电子原理开始,这可能是所有电气原理中最基本的两个原理 - 欧姆定律功率

欧姆定律和功率

欧姆定律是一个基本的电子原理,解释了电压电阻电流之间的关系。连同功率原理,这些是解释为什么在电路中选择某些值的核心基本原理。

欧姆定律表示为以下方程:

在这里,V是以伏特为单位的电压,I(大写 i)是以安培为单位的电流,R是以欧姯为单位的电阻,通常用希腊字母Ω表示。

另一方面,功率表示为以下方程:

在这里,P是以瓦特为单位的功率,I(大写 i)是以安培为单位的电流(与欧姆定律中相同),R是以欧姯为单位的电阻(与欧姆定律中相同)。

这些方程的重要原则是,您不能改变电子电路中的单个参数而不影响另一个参数。这意味着组件被选择和排列在电路中,以确保电压、电流和功率适当地比例分配给各个组件和电路的整体运行。

如果您是电子世界的新手,这些内容不会立即理解,不要灰心!这需要时间和实践。除了欧姆定律,我们还有基尔霍夫定律,下面我们将讨论它。

基尔霍夫电路定律

基尔霍夫的电压和电流定律是电路遵循的两个定律。它们是电气工程中的两个基本定律,陈述如下:

  • 环路中所有电压的代数和必须等于零。

  • 进入和退出节点的所有电流的代数和必须等于零。

这就是我们将要讨论的这些定律的深度。我在这里提到这些定律,因为电压定律是我们将在下一节中看到的定律,当我们计算为什么在本书的早期章节中为 LED 电路使用了 200 欧姆电阻时。

到目前为止,我们已经简要介绍了三个重要的电气原理或定律 - 欧姆定律、功率和基尔霍夫电路定律。现在是时候将这些原理付诸实践了。我们将通过一项练习来解释为什么我们在 LED 电路中一直使用 200Ω串联电阻。

为什么我们在 LED 电路中使用 200 欧姆电阻?

到目前为止,在本书中,我们的电子学大部分都围绕 LED 发展。我这样做是有充分理由的。LED(和电阻)是易于使用的组件,并为学习欧姆定律、功率和基尔霍夫电压定律等概念提供了基本的构建模块。掌握 LED 电路的基础知识和背后的计算,您将很快掌握更复杂的组件和电路。

让我们更深入地了解 LED,并探索其数据属性,并看看欧姆定律、功率和基尔霍夫电压定律的应用。通过一系列示例,我们将通过一个过程解释为什么在本书中之前看到的 LED 电路中使用了 200 欧姆电阻。

以下是一个基本的 LED 电路,类似于我们在本书中迄今为止使用的电路。如果您需要恢复这个电路,请回顾第二章,开始使用 Python 和物联网

图 6.1 - LED 和电阻电路

我们一直在使用典型的 5 毫米红色LED。我在这里提取了它的典型技术规格的一部分。强调典型红色的区别是因为 LED 的规格会有所不同,取决于它们的颜色、最大亮度、物理尺寸和制造商。即使是同一批次的 LED 也会有所不同。

以下是与我们参考的红色 LED 数据表相关的一些核心规格:

  • 正向电压降(VF)在 1.7 到 2.8 伏特之间,典型降为 2.1 伏特。这是 LED 需要照亮的电压。如果电路中的电压不足以点亮 LED,LED 将不会点亮。如果超过所需电压,那没关系-LED 将只取所需的电压。

  • 最大连续正向电流(IF)为 25 毫安。这是 LED 达到最大亮度所需的安全电流,当 LED 一直开启时,对于一些 LED 来说,这可能太亮了。提供更少的电流意味着 LED 会更暗,而提供更多的电流可能会损坏 LED。对于我们的 LED 和数据表,当脉冲 LED(例如使用 PWM)时,最大电流可以达到(IFP)100 毫安。

功率呢?LED 是根据电压和电流工作的组件。如果你看一下功率方程(),你会发现功率是电压(V)和电流(I)的函数。只要你在 LED 的电流额定范围内工作,你就在其功率容限范围内。

如果你没有 LED 的匹配数据表(在小批量推入时很常见),可以使用 2 伏特的电压降和 20 毫安的参考电流进行计算。你也可以使用数字万用表设置为二极管设置来测量 LED 的正向电压。

让我们继续看看我们如何得出 R1 电阻的值。

计算电阻值

在前面的电路图中,我们有以下参数:

  • 供电电压为 3.3 伏特

  • LED 典型正向电压为 2.1 伏特

  • LED 电流为 20 毫安(数据表中提到了电压降的毫安测试条件)

以下是计算电阻值的过程:

  1. 我们的电阻(标记为 R1)需要降低 1.2 伏特,这是我们之前简要提到的柯希霍夫电压定律的一个简单应用;即回路中所有电压的代数和必须等于零。因此,如果我们的源电压是+3.3 伏特,LED 降低 2.1 伏特,那么电阻必须降低 1.2 伏特。这意味着我们得到以下方程:

+3.3V + -2.1V + -1.2V = 0V

  1. 我们可以代数地排列欧姆定律,得到以下结果:

  1. 使用这个公式,我们计算出了我们电阻的值:

= 60Ω(因此,前面电路中的电阻 R1 为 60Ω)

但这不是 200Ω。到目前为止,我们的例子是一个简单的 LED 和电阻电路,连接到 3.3 伏特的电源,而不是树莓派。还有更多要考虑的,因为我们需要尊重树莓派 GPIO 引脚的电流限制,接下来我们将做到这一点。

考虑树莓派的电流限制

我们可以安全使用的与配置为输出的 GPIO 引脚的最大电流为 16 毫安。然而,GPIO 引脚有一个可配置的方面,这意味着,默认情况下,我们不应该使用超过每个 GPIO 8 毫安。这个限制可以配置,使其达到 16 毫安,但这超出了我们的范围。理想情况下,我们希望在需要更多电流时向外部电路移动,而不是不断提高引脚的电流。我们将在第七章中学习如何做到这一点,打开和关闭

虽然我们希望将单个 GPIO 输出引脚限制在 8 毫安,但我们不应该超过多个 GPIO 引脚的总和约 50 毫安。当涉及到 GPIO 输入引脚时,我们应该将电流限制在 0.5 毫安,以确保在连接外部输入设备或组件时安全操作。将输入 GPIO 引脚直接连接到树莓派的+3.3V 或 GND 引脚是可以的,因为测得的电流约为 70 微安(我们将在第七章中学习如何使用万用表测量电流,打开和关闭)。

让我们修改我们的计算并继续这个过程:

  1. 如果我们将电流限制在 8 毫安,我们可以使用之前的方程得出 R1 的值:

R1 = 150Ω

  1. 电阻器的额定值从来不会是精确的。它们有一个值的公差,如果我们的物理电阻器小于 150Ω,根据欧姆定律,我们会增加电路中的电流并超过 8 毫安的限制。

因此,我们将选择一个稍高一点的值。这可能就是使用经验法则,比如选择一个比 150Ω高两个标准电阻值,或者将 150Ω乘以我们电阻器的公差,然后选择下一个最高的标准值。让我们使用后一种方法,假设我们电阻器的公差是±20%(顺便说一句,这将是一个非常低质量的电阻器。5%和 10%更常见):

150Ω x 1.2 = 180Ω

180Ω恰好是一个标准的电阻值,所以我们可以使用它,但是我没有(经常会发现在计算后你也没有你想要的确切电阻值!)。然而,我有一些 200Ω的电阻器,所以我会使用其中一个。

对于原型设计和修补,从 180Ω到约 1kΩ的任何电阻器都足以满足我们电路的需求。只要记住,随着电阻器值的增加,电流会受到限制,所以 LED 会变得更暗。

但是电阻器上的功率和功率额定值呢?我们将在下面计算。

计算电阻器的功率耗散

我们面包板中使用的通用电阻器通常额定为 1/8 瓦特、1/4 瓦特或 1/2 瓦特。如果向电阻器提供过多的功率,它将烧毁并冒出一股难闻的气味。

当我们有一个 3.3 伏的电源时,这是我们计算 200Ω电阻器的功耗的方法:

  1. 电阻器的功率可以用以下公式计算。注意,电压V是电阻器两端的电压降,单位是伏特,而R是欧姆的电阻:

  1. 因此,当我们在公式中替换我们电阻器的电压降和电阻值时,我们得到以下结果:

= 0.0072 瓦特,或 7.2 毫瓦(或 mW)

  1. 我们的功率值为 7.2 毫瓦,甚至低于 0.25 瓦特的电阻器,因此 1/8 瓦特或更高的电阻器在我们的电路中是安全的,不会烧毁。

如果你觉得功率方程看起来与你之前看到的不同,你是对的。这是重新编写的功率方程,使用电压和电阻。这是一个方便的图表,我相信你在电子学学习过程中会看到的,它以不同的方式表达了欧姆定律和功率:

图 6.2 - 欧姆定律功率轮

我给你留下一个关于 LED 的最后提示,以及一些思考。

改变 LED 的亮度是由电流决定的。数据表中的 25 毫安值是驱动 LED 到最大亮度的最大连续安全电流。更少的电流也可以,只是意味着 LED 会变得更暗。

等一下 - 在第五章中,将您的树莓派连接到物理世界,我们使用了 PWM,这是一种伪模拟电压,用于改变 LED 的亮度。暂停一下,思考一下……发生了什么?这只是欧姆定律的一个应用。在我们的电路中,我们的电阻器固定在 200Ω。因此,通过改变电压,我们也改变了电流,从而改变了 LED 的亮度。

您认为呢?请放心,这是本书中数学的复杂程度。但我鼓励您重复这些练习,直到您对这个过程感到舒适。理解电子基础知识(以及相关的计算)是一个爱好者只是通过试错猜测组件直到电路工作的区别,和一个真正可以构建所需内容的工程师之间的区别。

接下来,我们将探讨与数字电子相关的核心概念。

探索数字电子

数字 I/O 基本上意味着检测或使 GPIO 引脚为高电平或低电平。在本节中,我们将探讨核心概念,并看一些数字 I/O 操作的示例。然后,我们将讨论这与您的树莓派以及您将与之接口的任何数字电子元件的关系。我们将通过查看和操作数字输出来开始或数字 I/O 之旅。

数字输出

简单来说,对于我们的树莓派来说,当我们将 GPIO 引脚设为高电平时,其电压测量值为3.3 伏特,当我们将其设为低电平时,测量值为0 伏特。

让我们用万用表观察一下:

不同的万用表可能有不同的连接和标记,与这里所示的万用表不同。如果您不确定如何设置测量电压,请参阅您的万用表手册。

  1. 将您的万用表设置为电压设置,并将其连接到 GPIO 21 和 GND,如下图所示:

图 6.3 - 将万用表连接到 GPIO 引脚

  1. 运行以下代码,您可以在chapter06/digital_output_test.py文件中找到。您会注意到仪表在大约 0 伏和大约 3.3 伏之间切换。我说“大约”,因为在电子设备中没有什么是完美或精确的;总是有公差。以下是代码的概要:
# ... truncated ...
GPIO_PIN = 21
pi = pigpio.pi()
pi.set_mode(GPIO_PIN, pigpio.OUTPUT)           # (1)

try:
    while True:                                # (2)
        # Alternate between HIGH and LOW
        state = pi.read(GPIO_PIN); # 1 or 0
        new_state = (int)(not state) # 1 or 0
        pi.write(GPIO_PIN, new_state);
        print("GPIO {} is {}".format(GPIO_PIN, new_state))
        sleep(3)
# ... truncated ...

在第 1 行,我们将 GPIO 21 配置为输出引脚,而在第 2 行,我们启动了一个while循环,该循环在每个状态转换之间有 3 秒的延迟,将 GPIO 21 的状态在高和低之间交替变换(即 0 和 1)。

您可能已经注意到,我们树莓派上的数字输出就是这么简单 - 高电平或低电平。现在,让我们考虑数字输入。

数字输入

通常,当我们考虑数字输入和 3.3 伏特设备的电压时,比如树莓派,我们认为将引脚连接到地(0 伏特)以使其低电平,或者连接到 3.3 伏特以使其高电平。在大多数应用中,这确实是我们努力做的事情。但实际上,这个故事还有更多内容,因为 GPIO 引脚不仅仅在两个离散的电压水平上工作。相反,它们在定义输入引脚为高和低的一系列电压范围内工作。这适用于树莓派和具有 GPIO 的类似计算机、微控制器、集成电路和分线板。

考虑以下图表,显示了 0 到 3.3 伏特之间的电压连续体,以及标有“低、悬空”和“高”的三个突出区域:

图 6.4 - 数字输入触发电压

这幅插图告诉我们,如果我们在 2.0 伏特和 3.3 伏特之间施加电压,那么输入引脚将被读取为数字高电平。或者,如果我们在 0.8 伏特和 0 伏特之间施加电压,引脚将被读取为数字低电平。超出这些范围的任何电压都是危险区域,您很可能会损坏您的树莓派。虽然您可能不会意外地向引脚施加负电压,但很可能会意外地向引脚施加超过 3.3 伏特的电压,因为通常会使用 5 伏特的数字电路。

那么,中间的灰色区域呢?我们是数字高电平还是数字低电平?答案是我们不知道,也永远无法可靠地知道。在这个范围内,引脚被称为“悬空”。

让我们看看悬空引脚的影响。我们将在面包板上创建以下电路:

图 6.5 - 按钮电路

以下是此步骤。这里的步骤编号与前面图中显示的带有编号的黑色圆圈相匹配:

  1. 将按钮放在面包板上。

  2. 将按钮的一端连接到树莓派上的 GND 引脚。在图中,我们将按钮的下部腿(显示在 E4 孔处)连接到 GND 引脚。

  3. 最后,将按钮的另一端(在图中,这是最上面的腿,显示在 E2 孔处)连接到树莓派上的 GPIO 21。

现在您的电路已经建立完成,让我们测试电路并看看会发生什么:

  1. 运行以下代码,可以在chapter06/digital_input_test.py文件中找到:
# ... truncated...
GPIO_PIN = 21
pi = pigpio.pi()
pi.set_mode(GPIO_PIN, pigpio.INPUT)   # (1)
# ... truncated...

try:
   while True:                        # (2)
   state = pi.read(GPIO_PIN)
   print("GPIO {} is {}".format(GPIO_PIN, state))
   sleep(0.02)

except KeyboardInterrupt:
   print("Bye")
   pi.stop() # PiGPIO cleanup.

此代码在第 1 行上将 GPIO21 配置为输入。在第 2 行上,使用while循环,我们快速读取 GPIO 引脚的值(1 或 0)并将其打印到终端。

  1. 用手指触摸面包板上的导线,以及开关周围的任何裸露的金属触点。导线和触点就像天线一样捕捉电气噪音,您应该看到终端输出在高(1)和低(0)之间波动 - 这是一个浮动引脚。这也说明了一个常见的误解,即配置为输入并且未连接任何东西的 GPIO 引脚默认总是低电平。

如果您最初的想法是“哇!我可以创建一个触摸开关”,那么抱歉;您会感到失望 - 这并不可靠,至少没有额外的电子设备。

接下来,我们将看两种常见的避免浮动引脚的方法。

使用上拉和下拉电阻

当引脚未连接到任何东西时,它被称为浮动。如前面的示例所示,它在周围漂移,从其他附近的组件、连接到它的导线和来自您自己的电荷中捕捉电气噪音。

再次参考前面的图表,当按钮按下时,电路完成,GPIO 21 连接到地,因此我们可以确定引脚为低电平。正如我们刚才看到的,当按钮按下时,GPIO 21 是浮动的 - 由于外部噪音,它可以在高电平和低电平之间波动。

这需要纠正,我们可以用电阻或代码来解决这个问题。

电阻解决方案

如果我们在电路中添加一个外部电阻,如下图所示,那么我们将引入所谓的上拉电阻,它的作用是(意思是连接)GPIO 引脚 21 上拉(意思是连接到正电压)到 3.3 伏:

图 6.6 - 带上拉电阻的按钮电路

以下是在面包板上创建此电路的步骤。这里的步骤编号与前面图中显示的带有编号的黑色圆圈相匹配:

  1. 将按钮放在面包板上。

  2. 在面包板上放置一个电阻(值在 50kΩ到 65kΩ之间)。电阻的一端与按钮的上部位置的腿共用同一行(显示在孔 B5 处)。电阻的另一端放在一个空行上。

  3. 将电阻的另一端连接到树莓派上的 3.3 伏引脚。

  4. 将按钮的下部腿连接到树莓派上的 GND 引脚。

  5. 最后,将按钮的上部腿和电阻的下部腿共用的行(显示在 D5 孔处)连接到树莓派上的 GPIO 21。

现在您已经创建了电路,这里是它的简要描述:

  • 当按钮 按下时,电阻将 GPIO 21 上拉到 3.3 伏引脚。电流沿着这条路径流动,引脚将被读取为保证的数字高电平。

  • 当按钮按下时,连接 GPIO 21 到地的电路段被创建。由于在这条路径中流动的电流更多,因为它的电阻更小(接近零),所以 GPIO 引脚连接到地,因此会读取为低电平。

chapter06/digital_input_test.py中运行相同的代码,只是这一次,当你触摸电线时,输出不应该波动。

如果你的电路不工作,而且你的接线是正确的,尝试将你的按钮在面包板上旋转 90 度。

为什么在前面的图中使用 50kΩ到 65kΩ的电阻?继续阅读-当我们看一个基于代码的替代方案时,我们将会找出原因。

代码解决方案

我们可以通过告诉我们的树莓派激活并连接一个嵌入式上拉电阻到 GPIO 21 来在代码中解决我们的浮动引脚问题,根据树莓派的文档,这个电阻将在 50kΩ-65kΩ的范围内,这就是为什么我们在前面的图中规定了这个范围。

下图显示了一个类似于前图所示的电路,但在外部电路中没有物理电阻。我在树莓派图中添加了一个电阻,以说明树莓派的电路中确实有一个物理电阻,尽管我们看不到它:

图 6.7 - 使用嵌入式上拉电阻的按钮电路

让我们在代码中启用一个上拉电阻并测试这个电路。以下是你需要遵循的步骤:

  1. 这个例子使用了之前在图 6.5中显示的按钮电路。在继续之前,请在面包板上重新创建这个电路。

  2. 接下来,编辑chapter06/digital_input_test.py文件,启用内部上拉电阻,如下所示:

#pi.set_pull_up_down(GPIO_PIN, pigpio.PUD_OFF) <<< COMMENT OUT THIS LINE
pi.set_pull_up_down(GPIO_PIN, pigpio.PUD_UP)   <<< ENABLE THIS LINE
  1. 再次运行chapter06/digital_input_test.py文件。当你按下按钮时,你应该看到终端上的高/低(0/1)值在改变;然而,触摸按钮的电线或终端不应该引起任何干扰。

当阅读前面的代码并观察终端输出时,如果终端在按钮 按下时打印1,在按钮按下时打印0(即按钮按下=引脚低)在编程意义上似乎有点前后颠倒,那么你是对的...也是错的。这是因为你是以程序员的身份看待电路。我故意这样做是因为这是你经常会看到的配置。这被称为主动低,这意味着当引脚低时按钮是活动的(按下)。

相反的电阻设置也是可能的,同样有效。也就是说,你可以设计一个将 GPIO 21 默认接地的电路,这样我们就使用了一个下拉电阻,无论是物理电阻还是在代码中激活的嵌入式电阻。在这种情况下,当按钮被按下时,引脚读取 1(高),在代码中可能会更舒服!

作为练习,尝试更改电路和代码,使其默认为下拉。

在阅读数字输入电路时,你需要结合伴随的代码来阅读电路,或者考虑你将要编写的代码。忽视上拉或下拉电阻的使用可能是看似简单的数字输入电路不工作的基础。

现在我们明白了我们可以有物理和代码激活的上拉和下拉电阻,我们可以说一个方法比另一个更好吗?简短的答案是,有时候...外部电阻确实有优势。

外部上拉或下拉电阻的优势在于它们始终存在。代码激活的上拉和下拉只有在满足两个条件时才存在:

  • 你的树莓派已经开机。

  • 你已经运行了激活上拉或下拉的代码。在此之前,引脚是浮动的!我们将在第七章中看到一个应用,打开和关闭设备

这并不是说代码激活的上拉和下拉电阻是次优的,只是意味着当您的树莓派关闭或您没有运行代码时,您需要考虑漂移引脚对电路的影响。

我们现在已经介绍了数字输入和输出的基础知识,这在许多方面是电子接口的支柱。我们还了解到,数字输入并不仅仅是高电平或低电平状态,实际上阈值电压水平确定了树莓派的数字高电平或数字低电平的电压水平。除此之外,我们还了解到在处理数字输入时,有必要适当地使用上拉或下拉电阻,以使输入电路可靠和可预测 - 也就是说,它不会漂移

当设计可预测的数字输入电路时,您对数字 I/O 的理解将对您有所帮助(漂移引脚和缺失或错误使用的上拉或下拉电阻在刚开始时是常见的错误来源!)。此外,当您与非树莓派设备和电子设备集成时,您对数字高/低电压水平的理解将是有价值的。我们将在本章后面再次提到这个数字电压主题,在逻辑电平转换部分。

现在,让我们从数字电子学转向模拟电子学。

探索模拟电子学

正如我们在前一节中看到的,数字 I/O 完全取决于电压确定的离散高电平或低电平。另一方面,模拟 I/O 完全取决于电压的程度。在本节中,我们将探讨一些核心概念,并查看模拟 I/O 的操作示例。

模拟输出

在第五章中,将您的树莓派连接到物理世界,我们讨论了通过在数字输出引脚上使用 PWM,我们可以创建伪模拟输出或可变输出电压的外观。此外,我们还在第三章中看到了 PWM 的使用,使用 Flask 进行 RESTful API 和 Web 套接字网络,当时我们使用了这个概念来控制 LED 的亮度。

在这一部分,我们将通过一个简短的练习进一步探讨 PWM 的基本概念。我们的示例与之前进行数字输出的示例类似,只是这一次,我们使用 PWM 在 GPIO 引脚上产生可变电压。以下是我们需要遵循的步骤:

  1. 将您的万用表连接到您的树莓派上,就像我们在图 6.3中为数字输出所做的那样。

  2. 运行以下代码,您可以在chapter06/analog_pwm_output_test.py文件中找到。

  3. 当代码运行时,您的万用表将步进到一系列不同的电压。虽然不会像终端屏幕输出所示的那样精确,但应该足够接近以说明意图:

(venv) $ analog_pwm_output_test.py
Duty Cycle 0%, estimated voltage 0.0 volts
Duty Cycle 25%, estimated voltage 0.825 volts
Duty Cycle 50%, estimated voltage 1.65 volts
Duty Cycle 75%, estimated voltage 2.475 volts
Duty Cycle 100%, estimated voltage 3.3 volts

让我们来看一下代码,部分代码如下。

它使用了 PiGPIO 的硬件定时 PWM,在第 1 行进行配置,同时在第 2 行定义了一组占空比百分比。这些是我们的代码将在第 3 行中步进的占空比值。在第 4 行,我们设置了 GPIO 21 的占空比,然后休眠 5 秒,这样您就可以在终端和您的万用表上读取值:

# ... truncated ...
pi.set_PWM_frequency(GPIO_PIN, 8000)                       # (1)

duty_cycle_percentages = [0, 25, 50, 75, 100]              # (2)
max_voltage = 3.3

try:
    while True:                                  
       for duty_cycle_pc in duty_cycle_percentages:        # (3)
           duty_cycle = int(255 * duty_cycle_pc / 100)
           estimated_voltage = max_voltage * duty_cycle_pc / 100
           print("Duty Cycle {}%, estimated voltage {} volts"
                 .format(duty_cycle_pc, estimated_voltage))
           pi.set_PWM_dutycycle(GPIO_PIN, duty_cycle)      # (4)
           sleep(5)

# ... truncated ...

如果您需要从您的树莓派提供更真实的模拟输出,那么您可能会喜欢探索如何使用数字模拟转换器DAC)。它们通常通过 I2C 或 SPI 进行接口,您将通过类似于 ADS1115 ADC 的驱动程序库来控制它们,只是您将输出可变电压而不是读取电压。

现在我们已经讨论了模拟输出,并看到了如何使用 PWM 创建一个简单的示例,接下来,我们将看看模拟电子学的输入端。

模拟输入

在第五章中,将您的树莓派连接到物理世界,我们学习了如何使用 ADS1115 ADC 扩展模块,模拟输入就是测量预定义范围内的电压,对于我们的目的来说,范围在 0 伏特到 3.3 伏特之间。在数字 I/O 中,我们会说在引脚上测量到 0 伏特意味着低,3.3 伏特意味着高,但在模拟 I/O 中,这方面没有高低的概念。

许多简单的模拟元件和传感器都是根据它们测量的内容来改变它们的电阻。例如,光敏电阻器(LDR)的电阻会随着它检测到的光的变化而变化。然而,模拟输入是关于测量电压的。为了将变化的电阻转换为变化的电压,我们使用电压分压器电路。

电压分压器

以下图显示了一个简单的两电阻器电压分压器电路。本例中,我们的电阻值是固定的,以说明基本原理。请注意,我们在本例中使用了 5 伏特。我们之所以这样做的原因很快就会揭晓,当我们讨论逻辑电平转换时:

图 6.8 - 测量电压跨电压分压器

电子学和电阻器的原理是,电压会按照它们的电阻值成比例地降低在串联电阻器上。在前述电路中,R1 的值是 R2 的两倍,所以它降低的电压是 R2 的两倍。以下是基本公式,应用于前述电路(实际上是再次应用了基尔霍夫定律和欧姆定律):

V[out] = 5 伏特 x 2000Ω / (1000Ω + 2000Ω)

V[out] = 3.33333 伏特

我们将在第三部分中看到电压分压器的应用,物联网游乐场-与物理世界互动的实际示例,但现在,为了看到这个原理在实践中的应用并帮助巩固概念,将数字万用表放在前图中标记的点之间,以验证测量的电压是否接近所示的值;即在 R1(前图中的 A 和 B 点)之间测量约 1.6 伏特,在 R2(前图中的 B 和 C 点)之间测量约 3.3 伏特。在前述方程中,R2(B 和 C 点)之间的测量是V[out]

那么电阻值的选择呢?对于电压分压器,电阻值选择最重要的部分是它们相对比例的选择,以便按照我们想要的方式分压电压。除此之外,还涉及到电流流动和电阻器功率额定值 - 再次,这些是欧姆定律和功率的应用。

还记得第五章中的电位器吗?它们实际上是电压分压器!我们将中间的拨片连接到 ADS1115 的 AIN1 和 AIN2 上,当您转动电位器上的拨片时,您所做的就是改变 A 和 B 端子之间相对于中心拨片的电阻,从而产生由 ADS1115 读取的可变电压。

以下图显示了电位器与语义图的关系。A、B 和 C 点与前述电路中指示的点是可比较的:

图 6.9 - 电位器是电压分压器

让我们进行一个实验,看看电位器如何作为电压分压器,创建如下电路:

图 6.10 - 电位器电路

以下是要遵循的第一组步骤。这里的步骤编号与前图中显示的带编号的黑色圆圈相匹配:

  1. 将 10kΩ电位器放在面包板上。您会注意到我已经标记了 A、B 和 C 三个端子,以便它们与图 6.9中显示的标签相匹配。

  2. 将外部电位器(标有 A)的端子连接到树莓派上的 3.3 伏针脚。在这个电路中,我们只使用树莓派作为电源。如果需要,您也可以使用外部电源或电池。

  3. 将电位器的另一个外部端子(标有 C)连接到树莓派的 GND 针脚。

  4. 将万用表的电压测量引线连接到电位器的中间端子(标有 B)。

  5. 将万用表的com端子连接到 GND(在我们的示例中,与标有 C 的电位器端子共用)。

  6. 打开您的万用表并选择电压模式。

现在,打开万用表,转动电位器的旋钮,观察万用表上的电压读数在 0 伏特和 3.3 伏特之间的变化。

这就结束了我们对模拟电子学的介绍。我们进行了一个简单的练习,用万用表演示和可视化了 PWM 如何产生可变的输出电压。我们还学习了电压分压器,它们的工作原理,以及它们为何是任何模拟输入电路的关键部分。最后,我们再次回顾了电位器,并看看它们如何作为可变的电压分压器

这些模拟概念虽然相对简短和简单,但是是每个电子工程师(无论您是专业人士还是业余爱好者)都需要理解的两个核心原则。这些概念,特别是电压分压器,将在接下来的章节中出现在许多电路中(我们将与 ADS1115 模数转换器一起使用它们),因此,请尝试使用前面的示例和原则来确保您掌握了基础知识!

接下来,我们将讨论逻辑电平转换,并看看电压分压器的另一个实际应用,这次是在数字输入空间中。

理解逻辑电平转换

有时候您需要从树莓派的 3.3 伏特 GPIO 引脚与 5 伏特设备进行接口。这种接口可能是为了 GPIO 输入、输出或双向 I/O。用于在逻辑电平电压之间转换的技术称为逻辑电平转换逻辑电平转移

有各种技术可以用来转移电压,我们将在本节中介绍其中两种比较常见的技术。一种是使用电压分压电路,我们在上一节中讨论过,而另一种是使用专用的逻辑电平转移模块。我们逻辑电平转换的第一个示例将是查看一种基于电阻的解决方案,称为电压分压器

电压分压器作为逻辑电平转换器

由适当选择的电阻构成的电压分压电路可以用于从 5 伏特降低到 3.3 伏特,使您可以将来自设备的 5 伏特输出用作输入到您的 3.3 伏特树莓派针脚。

为了让您清楚地理解和学习,在本节中,我们处理的是数字电子学,特别是数字输入和数字输入电路中电压分压器的应用。请确保在完成本章后,您对电压分压器在模拟和数字电路中的基本实际差异和应用感到满意。

以下图表是我们之前在图 6.8中看到的相同示例,只是这次是在不同的上下文中绘制的;也就是说,显示了如何将 5 伏特输入降低到 3.3 伏特:

图 6.11 - 使用电压分压器作为逻辑电平转移

电压分压器不能将电压从 3.3 伏特升高到 5 伏特。然而,回想一下我们在数字输入和图 6.4中的讨论,我们解释了输入引脚只要电压>= ~2.0 伏特就会读取数字高。同样的规则通常也适用于 5 伏特电路-只要输入电压>= ~2.0 伏特(3.3 伏特就是),5 伏特逻辑将会注册为逻辑高。数字低也是同样的道理,当电压<= ~0.8 伏特时。

通常情况下是这样,尽管您需要检查所涉及的 5 伏特设备的详细信息和数据表。它可能明确提到最低电压,或者可能只是提到它将使用 3.3 伏特逻辑。如果没有明显的迹象表明设备支持 3.3 伏特逻辑,您可以使用 3.3 伏特自行测试。这样做是安全的,因为 3.3 伏特小于 5 伏特,这意味着没有损坏的风险。最坏的情况下,它只是不起作用或者工作不可靠,这种情况下,您可以使用专用的逻辑电平转换器。我们将在下面讨论这个问题。

逻辑电平转换器 IC 和模块

电压分压电路的替代方案是专用的逻辑电平转换器。它们以 IC(芯片)形式和面包板友好的断路模块形式出现。因为它们基本上是即插即用的,所以不需要进行数学计算,并且它们包括多个通道,可以同时转换多个 I/O 流。

以下图片显示了典型的 4 通道(左侧)和 8 通道(右侧)逻辑电平转换断路模块。左侧的 4 通道是使用 MOSFET 构建的,而右侧的 8 通道使用了 TXB0108 IC。请注意,虽然我们将在第七章中介绍 MOSFET,打开和关闭物品,但我们的重点将是使用 MOSFET 作为开关,而不是逻辑电平转换应用。

图 6.12 - 逻辑电平转换器断路模块

逻辑电平转换器模块也有两个部分-低电压端和高电压端。关于树莓派,我们将其 3.3 伏特引脚和 GPIO 连接到低电压端,然后将另一个更高电压的电路(例如 5 伏特电路)连接到高电压端。

接下来的示例将基于类似于之前图片中的 4 通道 MOSFET 模块,它有 LV 和 HV 端子,以及两个 GND 端子。如果您使用不同的模块,您可能需要查阅其数据表,并根据示例调整接线。

让我们看看电平转换的实际操作。我们将通过构建一个电路并测量电压来实现这一点。在数字输出部分中,我们直接将万用表连接到树莓派的 GPIO 引脚上,并观察到当 GPIO 为高时,万用表读取3.3 伏特。这一次,我们将把我们的万用表连接到逻辑电平转换器的 HV 端,并观察到当 GPIO 引脚为高时,万用表读取5 伏特。

我们将从构建我们的电路开始,这将分为两部分:

图 6.13 - 可视化 3.3 伏特到 5 伏特电平转换(1/2)

以下是要遵循的第一组步骤,其中我们放置了连接逻辑电平转换器低电压端的组件。这里的步骤编号与前图中显示的编号黑色圆圈相匹配:

  1. 将您的逻辑电平转换器放在面包板上。

  2. 将逻辑电平转换器的 LV(低电压)端连接到左侧电源轨道的正极。我们将这个轨道称为低电压轨道,因为它将连接到我们供电电压中较低的那一侧(即 3.3 伏特)。LV 端是逻辑电平转换器的低电压端电源输入端子。

  3. 低电压轨道的正极连接到树莓派上的 3.3 伏特电源引脚。

  4. 将逻辑电平转换器低电压侧的 GND 端子连接到低电压轨道的负电源。

  5. 低电压轨道的负电源连接到树莓派上的 GND 引脚。

  6. 最后,将逻辑电平转换器的 A1 端口连接到树莓派上的 GPIO 21。

接下来,我们将连接逻辑电平转换器的高电压侧并连接我们的万用表:

图 6.14 - 可视化 3.3 伏特到 5 伏特电平转换(第二部分)

以下是要遵循的第二组步骤。这里的步骤编号与前图中显示的编号黑色圆圈相匹配:

  1. 将右侧电源轨道上的正电源连接到树莓派的 5 伏特引脚。我们将称这条轨道为高电压轨道,因为它将连接到我们供电电压中较高的那个(即 5 伏特)。HV 端子是逻辑电平转换器的高电压侧电源输入端子。

  2. 高电压轨道的负电源连接到低电压轨道的负电源。你可能还记得所有 GND 连接在电路中是共用的。如果你需要关于这个概念的复习,请回顾第二章中的引入地面连接和符号部分。

  3. 将逻辑电平转换器的 HV 端子连接到高电压轨道的正电源。

  4. 将逻辑电平转换器高电压侧的 GND 端子连接到高电压轨道的负电源。

  5. 将你的万用表的电压测量端子连接到逻辑电平转换器的 B1 端口。

  6. 将你的万用表的com端子连接到高电压轨道的负电源。

  7. 最后,将你的万用表设置为电压模式。

现在我们已经搭建好了电路,让我们运行一个 Python 程序,并确认当 GPIO 21 为高电平时,我们的万用表读取到了~5 伏特。以下是我们需要做的:

  1. 运行chapter06/digital_output_test.py文件中的代码 - 这是我们之前在数字输出部分使用的相同代码。

  2. 在低电压侧,我们的树莓派在通道 1 端口 A1 上的 GPIO 21 之间脉冲低(0 伏特)和高(3.3 伏特),而在高电压侧,我们的万用表连接到通道 1 端口 B1,将在 0 伏特和~5 伏特之间交替,说明了 3.3 伏特逻辑电平高到 5 伏特逻辑电平高的转变。

反向场景也是可能的;也就是说,如果你在高电压侧应用了 5 伏特输入,它将被转换成 3.3 伏特在低电压侧,这可以安全地被 3.3 伏特的树莓派 GPIO 引脚读取。

构建这个反向场景是一个你可能想要自己尝试的练习 - 你已经有了核心知识、代码和电路来实现这一点;你只需要把它全部连接起来!我鼓励你尝试一下,并为了帮助你开始,这里有一些提示:

  • 在你的面包板上放置一个按钮和上拉电阻,并将其连接到逻辑电平转换器高电压侧的 B1 端口。这个电路(在原理上)与你之前在图 6.6中看到的是相同的,只是现在的电源是 5 伏特,GPIO 引脚现在是 B1 端口。

  • 要测试你的电路,你可以使用我们之前使用的相同的数字输入代码,可以在chapter06/digital_input_test.py文件中找到。

  • 如果你遇到困难,需要参考面包板布局,或者希望检查你的电路搭建,你可以在chapter06/logic_level_input_breadboard.png文件中找到一个面包板布局。

当使用逻辑电平转换器 IC、分立模块或电压分压器作为电平转换器时,始终在连接到外部电路或树莓派之前用万用表测试输入/输出电压。这个检查将确保你已经正确连接了转换器,并且电压已经按照你的意图进行了转换。

让我们通过比较我们所看到的两种方法来结束我们对电平转换的讨论。

比较电压分压器和逻辑电平转换器

一个方法比另一个更好吗?这取决于情况,尽管我会说一个专用的转换器总是比基本的电压分压器更出色,而且它们在面包板上使用起来更方便。电压分压器更便宜,但只能在一个方向上工作(你需要两个电压分压器电路来执行双向 I/O)。它们还具有相对较高的电阻,这意味着在可变电阻改变和可测电压改变之间会发生实际的延迟。这种延迟足以使简单的电压分压器在高低状态之间快速切换的电路中变得不切实际。一个专用的逻辑电平转换器克服了这些限制,而且它们是多通道、双向、更快和更高效的。

总结

本章以对你在进一步学习电子学和我们将在第三部分中涵盖的电路中所需的基本工具和设备的快速概述开始(我们将在下一章开始)。然后,我们提出了一些建议,以帮助您在连接电子设备到树莓派的 GPIO 引脚时保持安全,以及在购买元件时的一些建议。

然后,我们探讨了欧姆定律(和非常简要地基尔霍夫定律),然后通过原因和计算来解释为什么我们的 LED 电路使用了 200 欧姆的电阻。我们通过查看数字电路的电子特性来跟进这个例子,其中我们探讨了逻辑电压电平、悬空引脚和上拉和下拉电阻。然后,我们查看了模拟电路,并通过一个电压分压器电路的例子来进行了工作。我们通过查看逻辑电平转换来结束了本章,并介绍了如何将 5 伏逻辑设备与 3.3 伏逻辑设备(如您的树莓派)进行接口。

本章的目标是向您介绍支撑基本电子学和特别是与树莓派等设备的电子接口的基本电子原理。我还努力解释了这些原理背后的基本为什么,以及它们如何影响为电路选择哪些元件。有了这些信息,您现在应该能够更好地理解如何构建与您的树莓派配合工作的简单电路。

此外,您可以利用这一理解作为进一步发展和提高您的电子技能的起点。在进一步阅读部分,您会找到一些有用的电子网站的链接,而且在我们继续第三部分 物联网游乐场时,我们会看到这些原则的许多应用。

当你准备好开始时,我会在下一章见到你——这也是第三部分 物联网游乐场的开始——在那里我们将探索不同的开关方法。

问题

随着我们的结束,这里有一些问题供你测试对本章材料的了解。你会在书的评估部分找到答案:

  1. 你有一个需要 200Ω电阻的电路,但你只有一个 330Ω的电阻可用。使用这个值安全吗?

  2. 你在电路中用一个更高阻值的电阻代替,但电路却无法工作。根据欧姆定律,可能出了什么问题?

  3. 您使用欧姆定律计算了电路的合适电阻值,但当您给电路加电时,电阻开始变色并冒烟。为什么?

  4. 假设 GPIO 21 通过 Python 配置为输入引脚,并且通过一根导线直接连接到+3.3 伏引脚,pi.read(21)会返回什么值?

  5. 你设置了一个按钮,当按下时,它将 GPIO 21 连接到 GND 引脚。当按钮没有被按下时,你会注意到你的程序表现不稳定,并且似乎接收到了一个幻象按钮按下。问题可能是什么?

  6. 你想将一个输出引脚在 5 伏特操作的设备安全地连接到树莓派的 GPIO 输入引脚。你可以如何安全地做到这一点?

  7. 真或假 - 电阻器电压分压电路可以用于将 3.3 伏特输入转换为 5 伏特,以用于 5 伏特逻辑输入设备。

进一步阅读

以下两个网站是电子制造商,它们都提供了广泛的入门到中级教程。它们侧重于电子学的实际方面,不会向你灌输太多理论。在它们的网站上搜索Raspberry Pi试试:

关于本章涵盖的概念,以下是上述网站上的一些具体链接:

如果你想深入了解,以下两个网站是优秀的(并且免费)资源,涵盖了电子基础和理论的各种主题:

我建议花一些时间在这些网站上浏览一下,了解它们包含的内容。这样,如果你在这本书中遇到想进一步探索的电子术语、元件或概念,你就会知道从哪里开始调查。以下是两个开始探索的链接:

如果你浏览这些网站的索引,你会发现包括欧姆定律、功率、基尔霍夫定律、电压分压器以及数字和模拟电子学在内的部分。

第三部分:物联网游乐场-与物理世界互动的实际示例

这是我们涵盖物联网中的部分的部分。我们将探索和实验各种常见传感器,执行器和电子电路,我们将使用 Python 与物理世界进行交互。在这个过程中,我们将看到许多核心电子原理在第二部分中学到的实际应用。在本节的后半部分,我们还将结合我们从第一部分学到的知识(即物联网的互联网部分),使用各种不同的方法创建端到端的物联网应用程序。

本节包括以下章节:

  • 第七章,打开和关闭

  • 第八章,灯光,指示灯和显示信息

  • 第九章,测量温度,湿度和光照水平

  • 第十章,使用伺服,电机和步进电机进行运动

  • 第十一章,测量距离和检测运动

  • 第十二章,高级物联网编程概念-线程,AsyncIO 和事件循环

  • 第十三章,物联网可视化和自动化平台

  • 第十四章,将一切联系在一起-物联网圣诞树

第七章:打开和关闭东西

在上一章中,我们讨论了当您将数字和模拟电路与树莓派的 GPIO 引脚进行接口时将会使用的核心电子电路和概念。

在本章中,我们将介绍如何打开和关闭需要比树莓派安全使用的更高电压和电流东西。在电子领域,可以使用数百种不同的元件来进行控制和开关。它们可以以成千上万种不同的方式进行配置。我们将重点关注三种常见的补充—光耦、晶体管和继电器。

当与树莓派进行接口时,了解如何控制和开关电路是一个非常重要的话题。正如我们在第五章中讨论的那样,树莓派的 GPIO 引脚只能安全地提供少量毫安的输出电流和固定的 3.3 伏特。完成本章后,您对光耦、晶体管和继电器的了解将意味着您可以开始控制具有不同电流和电压要求的设备。

以下是本章的内容:

  • 探索继电器驱动电路

  • 确定负载的电压和电流

  • 使用光耦作为开关

  • 使用晶体管作为开关

  • 使用继电器作为开关

技术要求

要完成本章的练习,您将需要以下设备:

  • 树莓派 4 型 B

  • 树莓派操作系统 Buster(带桌面和推荐软件)

  • 至少 Python 版本 3.5

这些要求是本书中代码示例的基础。可以合理地期望,只要您的 Python 版本是 3.5 或更高,本书中的代码示例应该可以在树莓派 3 型 B 或不同版本的 Raspbian OS 上无需修改即可运行。

您将在 GitHub 存储库的chapter07文件夹中找到本章的源代码,链接在这里:github.com/PacktPublishing/Practical-Python-Programming-for-IoT

您需要在终端中执行以下命令来设置虚拟环境并安装本章代码所需的 Python 库:

$ cd chapter07              # Change into this chapter's folder
$ python3 -m venv venv      # Create Python Virtual Environment
$ source venv/bin/activate  # Activate Python Virtual Environment
(venv) $ pip install pip --upgrade        # Upgrade pip
(venv) $ pip install -r requirements.txt  # Install dependent packages

以下依赖项已从requirements.txt中安装:

本章练习所需的电子元件如下:

探索继电器驱动电路

电子开关的常见介绍是机械继电器 - 一种像普通开关一样工作的设备,只是通过给它供电来打开和关闭。不幸的是,直接连接继电器到树莓派是危险的!继电器通常需要太多的电流和电压,并且(如果它们确实切换)可能会损坏您的树莓派。因此,我们需要一个驱动电路,位于树莓派和继电器之间。这个电路的示例如图 7.1 所示:

图 7.1 - 继电器驱动电路

这是我们将在本章逐步构建的电路。这个电路代表了您在 eBay、Banggood 和类似网站上找到的许多继电器控制模块。这些板确实很方便使用 - 当您让它们工作时。不幸的是,缺乏清晰的文档往往会使得让它们工作起来棘手和困难,特别是如果您是电子新手。

我们将要构建并探索图 7.1 中所示的三个子电路。这将帮助您了解光耦、晶体管和继电器作为开关的工作原理,以及它们为什么经常被串联在一起来控制继电器。这些知识还将帮助您在无法使其工作时逆向工程预制继电器控制模块。

在我们讨论光耦子电路之前,我们需要首先讨论负载电压和电流。

确定负载的电压和电流

负载是您想要控制的东西,或者在本章中,打开和关闭。LED、晶体管、光耦、继电器、灯、电动机、加热器、泵、自动车库门和电视都是负载的例子。如果您回到图 7.1,您会注意到图表右侧的“负载”一词。这是您连接想要打开或关闭的东西的地方。

晶体管、光耦继电器组件出现在上述负载列表中。回到图 7.1,继电器出现为晶体管子电路的负载,而晶体管子电路出现为光耦子电路的负载。

了解您想要控制的负载的两个属性是很重要的:

  • 负载需要多少电压?

  • 负载需要多少电流?

有时,这些属性可以在设备本身或其手册或数据表中找到。其他时候,它们需要被计算或者负载需要被手动测量。

知道这些属性很重要,因为它们影响了电路中选择哪些组件,包括适合的电源规格。在本章的整个电路构建过程中,我们将提到负载电流,因此稍后会有更多的上下文。现在,让我们看看如何测量直流电机的电流负载。

测量直流电机的电流需求

电机是人们想要控制的常见物品,它们是电流测量的一个很好的例子。让我们进行一个练习来测量我们的直流电机所使用的电流:

图 7.2 - R130 直流电机

上面的照片显示了一个典型的 130(R130)直流电机,以及一组跳线引线焊接到电机的端子上,以便可以轻松地将其插入面包板。这个电机有一个红色的背面,但其他颜色也很常见 - 尤其是透明/白色。颜色对电机规格没有影响。

在进行以下步骤时,如果您不确定如何将万用表置于电流测量模式,请参考您的万用表手册。

以下是要遵循的步骤:

  1. 按照图 7.3 中所示的连接电路:

图 7.3 - 使用万用表测量电流

我们假设这里的电机是章节开头的技术要求部分提到的电机。这个电机足够小,可以从面包板电源供电,通常可以提供 500mA 到 800mA。对于更大的电机(以及其他你不知道其额定值并想要测量的物品),你将需要一个更强大的电源。

如果你正在用 USB 手机充电器给面包板电源供电,请用万用表检查你的电源输出的 5 伏特电压,确保它提供的是大约 5 伏特。低功率充电器和质量差的 USB 电缆可能无法提供足够的电源使电源正常工作。理想情况下,阅读数据表并使用建议的电源适配器,通常是 7 到 12 伏特和 1 安培。

  1. 确保你的万用表设置为测量毫安mA),并且红色表笔连接到正确的引脚输入(通常标有 A 或 mA)。如果你的数字万用表有一个µA 输入,不要使用,否则可能会烧坏你的数字万用表的保护保险丝(保险丝可以更换)。

  2. 给电路供电,电机会旋转。

  3. 你的万用表将显示电机的电流消耗。记下这个值。这被称为连续自由电流,是电机在轴上自由旋转时使用的电流。

  4. 断开电机的电源。

  5. 用一把钳子,抓住电机的轴,使其无法旋转。

  6. 重新给电机供电,并迅速观察(并记下)数字万用表的读数。这个读数被称为堵转电流。电机在其轴被强行停止移动时会使用最大电流。

  7. 断开电机的电源。

我们现在已经测量了两种电流。我的 R130 电机的读数如下(你的可能会有所不同):

  • 连续或自由电流110mA 到200mA——随着电机使用而发热,它将使用更少;200mA 的测量是在电机冷却时进行的。一分钟后,它下降到了110mA。

  • 堵转电流:这是500mA 到600mA。

这意味着我们的电机在正常运行时需要 200mA 到 600mA 的电流,任何我们希望用于电机的电路必须能够真实地处理 600mA,以免在电机停转时受到损坏(或者我们需要设计合适的保护,然而,这超出了我们的范围)。

有趣的是,还有一个启动电流,这是电机启动时发生的瞬时峰值电流,但我们无法用通用数字万用表来测量。

现在我们已经得到了 R130 电机的电流消耗,让我们收集继电器和 LED 的更多电流数据。

测量继电器和 LED 的电流需求

当我们到达标题为使用继电器作为开关的部分时,我们还将测量 LED 和继电器的电流消耗。你可以使用前一部分的步骤 1 到 4来测量 LED 和电阻对的电流消耗。执行这个测量的设置如下图所示:

图 7.4 - 测量通过电阻/LED 电路的电流

这是我们遵循的基本流程:

  1. 我们在图 7.3中所示的电机位置上连接了一个 LED 和一个 1kΩ电阻(或继电器)。

  2. 将你的万用表设置为毫安模式。

  3. 给电路供电。

  4. 用你的万用表测量电流。

一旦你完成了(并记下)你得到的测量值,从面包板上移除 LED 和电阻,接入继电器并进行相同的测量。

下图显示了 SRD-05VDC-SL-C 继电器以及你需要连接的继电器端子。请注意,你需要焊接排针(如图所示)或导线(一个很好的选择是将杜邦线剪成一半)到继电器的端子上,因为它不能直接插入面包板:

图 7.5 - SRD-05VDC-SL-C 继电器

使用 5V 电源,您应该在万用表上获得类似以下数值:

  • 串联有 5mm 红色 LED 和 1000Ω电阻:3mA(来自欧姆计算的数值并向上取整I = (5V - 2.1V) / 1000Ω = 2.9mA)

  • 继电器:70mA 至 90mA(来自数据表并经过我的测量确认)

有关如何计算 LED 的电流的过程在第六章中已经讨论过,软件工程师的电子学 101。唯一的区别是,这里我们使用的是 5 伏电源和 1kΩ电阻,而不是在那一章中使用的 3.3 伏和 200Ω电阻。

请注意,我们将使用的光耦合器和 MOSFET 元件确实具有影响通过连接负载的电流的电压降方面。出于简洁起见,这些电压降的影响对我们的目的来说是不重要的,因此在本章的计算中不予考虑。

您现在已经学会了如何使用万用表测量直流电机、LED/电阻对和继电器的电流。了解您想要控制的设备以及您要连接的子电路的电流限制和期望,甚至是一个至关重要的信息,这样您在设计电路和选择合适的电源时可以选择合适额定的元件。

在本章中,我们将参考您在本节中进行的测量,探索光耦合器、MOSFET 和继电器。具体来说,我们将比较这些元件的电流额定值(在各自的数据表中找到)与我们的直流电机、LED/电阻和继电器的测量,并考虑可以用于直接控制哪个负载的元件。

我们将首先学习光耦合器及其用作开关的方法。

使用光耦合器作为开关

光耦合器(或光隔离器)是一种用于电气隔离两个电路的光控组件。这里显示了光耦合器的示意图和原理图符号:

图 7.6 - 光耦合器符号和带有标记引脚的元件

光耦合器的两侧可以描述如下:

  • 输入端:我们将连接到树莓派 GPIO 引脚的一侧

  • 输出端:我们将连接到另一个电路的一侧

输入端的光耦合器内部有一个内部 LED(您将在图 7.6中的光耦合器符号中注意到 LED 符号),而在输出端有一个对 LED 光线做出响应的光电晶体管。这意味着控制(即开关)从输入端到外部端的传递是通过光线完成的,因此两侧之间没有物理电气连接。对我们来说,这意味着输出端的任何故障或意外不应对我们的树莓派造成损害。PC817 的隔离额定为 5000 伏,远远超出我们预期在物联网电子设备中使用的任何电压。

输入端 LED 关闭时,输出端光电晶体管关闭。然而,当 LED 被照亮(它在光耦合器元件内部,所以您看不到它)通过向引脚 1(阳极)和 2(阴极)施加电流时,光电晶体管被激活(打开),并允许电流在引脚 4(集电极)和 3(发射极)之间流动。

让我们创建一个简单的电路来演示 PC817 光耦合器,其规格如下:

  • 输入端(LED):具有以下数值:

  • 典型的**正向电压(V[F])**为 1.2 伏直流

  • 最大**正向电流(I[F])**为 50mA 直流

  • 输出端(光电晶体管):具有以下数值:

  • 最大集电极-发射极电压(V[CEO]):80 伏直流

  • 最大集电流(I[C]):50 毫安直流

  • **集电极-发射极饱和电压 V[CE(sat)]**在 0.1 至 0.2 伏范围内(基本上是电压降)

牢记这些规格,让我们开始搭建电路。

搭建光耦合器电路

我们将要构建以下图示的电路。这个电路使用 PC817 光耦器来电气隔离我们的树莓派和 LED 子电路:

图 7.7 - 光耦器电路

这里的步骤编号与图 7.7中编号的黑色圆圈相匹配:

  1. 将 LED 放入你的面包板中,注意根据图示放置 LED 的阴极腿。

  2. 将一个 1kΩ电阻放入面包板中。这个电阻的一端与 LED 的阴极腿联通。

  3. 将 PC817 光耦 IC 放入面包板中。IC 上的白点表示 IC 的引脚 1。你的 IC 可能有也可能没有白点,但是 IC 上应该有一个明显的标记告诉你第一个引脚。请参考图 7.6以获取所有引脚编号信息。

  4. 将一个 1kΩ电阻放入你的面包板中。这个电阻的一端连接到 PC817 的引脚 1。

  5. 将 LED 的阳极腿连接到右侧电源轨的正电源。

  6. 将 PC817 的引脚 4 连接到你在步骤 2中放置的电阻的另一端。

  7. 将 PC817 的引脚 3 连接到右侧电源轨的负电源。

  8. 将 5 伏电源的正输出连接到右侧正电源轨。

  9. 将电源的负输出连接到右侧负电源轨。

  10. 将你在步骤 4中放置的电阻的另一端连接到树莓派上的 3.3 伏引脚。

  11. 最后,将 PC817 的引脚 2 连接到树莓派上的 GPIO 21。

图 7.7中,你可以直接将步骤 89(连接到外部电源)的导线连接到树莓派的+5 伏引脚和一个 GND 引脚。我们只是为红色 LED 使用了少量电流,但是对于更大的电流负载,你必须使用外部电源。树莓派上的+5 伏引脚直接连接到你用来给树莓派供电的电源。使用这个电源来为你的电路供电会有效地消耗树莓派可用的电流。如果消耗过多,你的树莓派将会重启!请注意(这很重要)这个行为的警告是****你将失去光耦器提供的电气隔离,因为你将会将输入输出端连接在一起(记住,输入输出端并不在光耦器内部电气连接,因为控制是通过光实现的)。

现在你已经完成了电路的搭建,我们将测试电路并探索使其工作的代码。

用 Python 控制光耦

首先运行chapter07/optocoupler_test.py文件中的代码,并观察 LED 的闪烁。以下是负责闪烁的代码部分:

# ... truncated ...
  pi.write(GPIO_PIN, pigpio.LOW) # On.     # (1)
  print("On")
  sleep(2)
  pi.write(GPIO_PIN, pigpio.HIGH) # Off.   # (2)
  print("Off")
  sleep(2)
# ... truncated ...

以下是发生的事情:

  • 在第(1)行,GPIO 21 是低电平,输入端的内部 LED 是开启的。输出端的光电晶体检测到这种光并被激活,允许电流在输出端的集电极(引脚 4)和发射极(引脚 3)之间流动,因此我们的红色 LED 亮起。

  • PC817 电路的输入端被布线为主动低电平,这就是为什么在第(1)行,GPIO 21 被设为低电平以打开电路,在第(2)行,GPIO 21 被设为高电平以关闭电路。另一种布线方式是主动高电平。如果你想要尝试并将电路改为主动高电平,你需要将图 7.7中的步骤 10的导线连接到 GND 引脚(而不是 3.3 伏引脚),并且在代码中颠倒pigpio.LOWpigpio.HIGH语句。

我们本可以使用更低阻值的电阻 R1 来驱动输入端 LED,但是 1kΩ的电阻提供了超过光耦合器电路内部 LED 所需的电流((3.3V - 1.2V)/1000Ω = 2.1mA)。您会看到 1kΩ、10kΩ和 100kΩ的电阻在许多电路中使用,因为这些都是比较常见的数值。我们还使用了 1kΩ的电阻 R2 来方便地驱动红色 LED。

您还记得上一章第六章中我们讨论过的内容吗?即我们不应该期望从树莓派 GPIO 引脚获得超过 8mA 的电流?是的,通过使用 PC817 光耦合器,我们现在可以通过在 GPIO 引脚和电路之间放置光耦合器来控制高达 50mA 的电流。此外,我们也不再受限于 GPIO 引脚的 3.3 伏电压,因为 PC817 可以处理高达 80 伏的电压。

记住,GPIO 引脚的主要作用是控制某物,而不是为其供电,因此始终要独立考虑控制电源需求。

在前一节中,我们计算(或测量)了我们的电机、继电器和 LED 的电流消耗。以下是在输出端使用 5 伏电源的 PC817 光耦合器的数据:

  • LED 和 1kΩ的电阻需要 3mA 的电流。

  • 继电器需要 70mA 至 90mA 之间的电流。

  • 电机需要约 500mA 至 600mA(堵转电流)。

LED 的 3mA 小于光耦合器输出端的最大额定 50mA,因此可以直接在输出端驱动 LED。然而,继电器和电机需要超出 PC817 限制的电流,因此在输出端使用它们可能会损坏光耦合器。

虽然我们可以使用光隔离器作为数字开关,但它们通常被用作隔离屏障来驱动其他组件,这些组件反过来可以驱动需要更高电流的负载。当我们从图 7.1构建完整的继电器驱动电路时,我们将在后面看到这一点,但现在,让我们学习如何将晶体管用作数字开关。

使用晶体管作为开关

晶体管是当今使用最广泛的电子元件,也是数字革命的支柱。它们可以以两种基本方式使用——作为放大器或数字开关。我们的重点将放在数字开关上,我们将使用一种名为金属氧化物半导体场效应晶体管MOSFET)的晶体管类型,具体来说是一种 N-沟道增强型 MOSFET——是的,这个名字有点拗口!

不要过分关注晶体管的冗长技术名称或存在的多种形式。这里的简单要点是,N-沟道增强型 MOSFET 作为数字开关效果很好,我们可以使用树莓派来控制它,或者后面将看到,也可以来自光耦合器等其他源。

场效应晶体管(FET)是电压控制的晶体管。另一种称为双极晶体管BJT)的晶体管是电流控制的晶体管。BJT 在树莓派上使用完全没问题,但需要额外考虑。在进一步阅读部分中,您会找到有关晶体管的更多学习资料的链接。

以下练习将使用 2N7000,一种 N-沟道增强型 MOSFET,如图 7.8所示。引脚名称为Source、Gate 和Drain。还有两种不同的封装样式,TO92 和 TO220。请注意,两种样式的 Source、Gate 和 Drain 引脚的排列方式是不同的:

图 7.8 - N-沟道增强型 MOSFET 符号和常见封装样式

2N7000 在其数据表中具有以下规格:

  • 最大**漏源电压(V[DSS])**为 60 伏直流

  • 最大**连续漏极电流(I[D])**为 200 毫安直流

  • 最大**脉冲漏极电流(I[DM])**为 500 毫安直流

  • **栅阈电压(V[GS(th)])**在 0.8 至 3 伏直流范围内

  • **漏极-源极开启电压(V[DS(on)])**在 0.45 至 2.5 伏特直流范围内(电压降)

以下是如何解释关于 2N7000 的这些参数:

  • 它可以安全地控制不超过 60 伏特(V[DSS])和持续 200 毫安(I[D])的负载,但脉冲 500 毫安(I[DM])是可以的。

  • 它理想情况下需要大于 3 伏特的电压才能打开(V[GS(th)])。

  • 它将在负载端电路上消耗 0.45 至 2.5 伏特(V[DS(on)])的电压。

2N7000(以及我们将很快讨论的 FQP30N06L)是逻辑电平可比较的 MOSFET。它们适用于树莓派,因为它们的最大门电压 V[GS(th)]小于 GPIO 引脚的 3.3 伏特。

让我们开始建立一个使用 2N7000 的电路与我们的树莓派。

构建 MOSFET 电路

我们将分两部分建立我们的电路,首先是在面包板上放置组件:

图 7.9 - MOSFET 晶体管电路(第一部分)

以下是我们建设的第一部分的步骤。步骤编号与图 7.9中编号的黑色圆圈相匹配:

  1. 将 MOSFET 放入面包板中,注意根据源极、栅极和漏极的正确方向放置组件。我们的示例布局假定了一个 2N7000 MOSFET。如果您需要帮助识别腿,请参阅图 7.8

  2. 将 100kΩ电阻放入面包板中。这个电阻的一端连接到 MOSFET 的栅极。

  3. 将 1kΩ电阻放入面包板中。这个电阻的一端也连接到 MOSFET 的栅极。

  4. 将 LED 放入面包板中,注意根据其阴极腿的显示方向放置组件。

  5. 将 1kΩ电阻放入面包板中。这个电阻的一端连接到 LED 的阴极腿。

  6. 将二极管放入面包板中,使组件朝向底部的阴极腿(二极管外壳上带有条纹的一端)。

现在我们已经将组件放入面包板中,让我们把它们全部连接起来:

图 7.10 - MOSFET 晶体管电路(第二部分)

以下是第二部分建设的步骤。步骤编号与图 7.10中编号的黑色圆圈相匹配:

  1. 将树莓派的 GND 引脚连接到右侧电源轨的负极。

  2. 连接右侧和左侧电源轨的负极。

  3. 将 100kΩ电阻连接到负电源轨。

  4. 将 MOSFET 的源腿连接到负电源轨。

  5. 将 MOSFET 的漏极连接到 1kΩ电阻。

  6. 将 LED 的阳极腿连接到二极管的阴极腿。

  7. 将 LED 的阳极腿(和二极管的阴极腿)连接到右侧电源轨的正极。

  8. 将 1kΩ电阻连接到树莓派的 GPIO 21。

  9. 将电源供应的正输出端连接到右侧电源轨的正极。

  10. 将电源供应的负输出端连接到右侧电源轨的负极。

干得好。我们的电路建设完成了。在我们测试之前,让我们简要讨论一下这个电路。

请注意图 7.10(和图 7.1)中的 100kΩ电阻 R3。这是一个外部下拉电阻,确保当 GPIO 21 为高电平时,MOSFET 的 Gate 腿被拉到+3.3 伏特,而当它没有被拉到高电平时,它被连接到 GND(0 伏特)。MOSFET 具有电容电荷,因此如果没有下拉电阻,MOSFET 在从通(GPIO 21 为高电平)到断(GPIO 21 为低电平)的转换时可能会显得粘滞和缓慢,因为它在放电(请注意,此电路为主动高电平)。下拉电阻确保快速放电到断开状态。我们使用外部下拉电阻而不是在代码中激活的下拉电阻,以确保即使树莓派关闭电源或代码未运行时,MOSFET Gate 也被拉低。

您还会注意到 R1 和 R3 构成了一个电压分压器。1kΩ和 100kΩ的比例适合确保>3 伏特到达 MOSFET 的 Gate 腿以打开它。如果您需要关于下拉电阻和电压分压器的复习,我们在第六章中讨论过它们,软件工程师的电子学 101

在电路中添加电阻时,例如添加下拉电阻,始终要考虑更改的更广泛影响。例如,如果添加电阻由于现有电阻的存在而创建了电压分压器,那么您需要评估更改对周围电路的影响。对于我们的情况,这是为了确保足够的电压到达 MOSFET 的 Gate 腿以打开它。

在运行下一节中的代码后,尝试移除 R3 并再次运行代码。我不能保证您会在您的端看到任何东西,但您可能会观察到,当 GPIO 21 变为低电平时,红色 LED 不是立即熄灭而是缓慢消失,并且它的行为不是平稳地淡入淡出而是表现得不稳定。

与光耦示例一样,由于 LED 示例的电流要求较低,您可以将电线的外部电源连接到树莓派的+5 引脚和一个 GND 引脚。

在对 MOSFET 电路有了基本理解之后,让我们运行并探索一个与我们的电路交互的简单 Python 程序。

用 Python 控制 MOSFET

运行chapter07/transistor_test.py文件中的代码,红色 LED 将先点亮然后熄灭,然后再淡入淡出。一旦确认电路工作正常,让我们继续看代码:

# ...truncated ...
pi.set_PWM_range(GPIO_PIN, 100)             # (1)

try:
  pi.write(GPIO_PIN, pigpio.HIGH) # On.     # (2)
  print("On")
  sleep(2)
  pi.write(GPIO_PIN, pigpio.LOW) # Off.
  print("Off")
  sleep(2)

在这个例子中,我们使用 PWM。在第(1)行中,我们告诉 PiGPIO,对于 GPIO 21(GPIO_PIN = 21),我们希望其占空比受限于值范围 0 到 100(而不是默认的 0 到 255)。这是一个示例,说明了我们如何改变 PiGPIO 中占空比值的粒度。我们使用 0 到 100 只是为了使报告更容易,因为它映射到终端输出的 0%到 100%。

接下来,在第(2)行中,我们简单地打开和关闭 GPIO 一段时间,以测试晶体管电路,然后我们将看到 LED 在 2 秒延迟后点亮然后熄灭。

在以下代码的第(3)行中,我们使用 PWM 来淡入 LED,然后在前面代码块中的第(1)行设置的占空比范围内再次淡出它(第(4)行):

  # Fade In.
  for duty_cycle in range(0, 100):                  # (3)
      pi.set_PWM_dutycycle(GPIO_PIN, duty_cycle) 
      print("Duty Cycle {}%".format(duty_cycle))
      sleep(0.01)

  # Fade Out.
  for duty_cycle in range(100, 0, -1):              # (4)
      pi.set_PWM_dutycycle(GPIO_PIN, duty_cycle) 
      print("Dyty Cycle {}%".format(duty_cycle))
      sleep(0.01)
# ...truncated ...

让我们检查一下我们的继电器和电机是否可以安全地与这个晶体管电路一起使用,考虑到我们的 2N7000 额定电流为 200 毫安:

  • 继电器可以用来替代 LED,因为它只需要 70mA 到 90mA 之间的电流。

  • 电机需要~200mA 来自由旋转(连续电流),所以可能是安全的...或者不是?让我们看看。

在本章早些时候测试电机时,我们预计它在冷态时需要约 200mA(冷态时的连续电流)和约 500mA 至 600mA(堵转电流)之间的电流- 请记住这些是我的测量值,所以请用您的测量值替换。因此,原则上,我们的 2N7000 只要电机不承受负载就可以。实际上,一旦我们在电机轴上施加负载,它将需要超过 200mA 的连续电流。在这方面,2N7000 可能不是驱动这个电机的理想晶体管。我们需要寻找一个可以轻松处理 600mA 或更多连续电流的 MOSFET。我们很快就会看到 FQP30N06L MOSFET,它可以处理这个电流以及更多。

当 LED 在 PWM 相关代码中逐渐变暗时,如果您将电机连接到电路中以替代 LED/电阻对,您会注意到它会加速然后减速。您刚刚发现了如何使用 PWM 的占空比属性来控制电机的速度!我们将在第十章中更详细地介绍电机,使用舵机、电机和步进电机进行运动

要使用电机或继电器,必须使用外部电源,而不是树莓派上的+5 伏引脚。如果您尝试使用+5 伏引脚,可能会发现在运行代码时树莓派会重置。

我们不使用 PWM 控制继电器,因为它们切换速度太慢,而且如果它们工作(在非常低的 PWM 频率下),只会使它们耗损-但无论如何尝试一下,看看会发生什么;短暂的测试不会有害(尝试将代码中的频率从 8000 调整到 10,即pi.set_PWM_frequency(GPIO_PIN, 10))。

在我们的电路中,还有 1N4001 二极管 D1。这被称为反冲或抑制二极管。它的作用是保护电路免受在关闭电源时可能发生的继电器或电机等电磁元件的反向电压脉冲。诚然,我们的 LED 不是磁性的,但是有它在场也不会有任何害处。

每当您控制一个工作在电磁学上的组件(也称为感性负载)时,都要正确安装反冲抑制二极管。

图 7.8中,我们还有 FQP30N06L 的插图。这是一种能够驱动高电流负载的功率 N-通道增强模式 MOSFET。它在数据表中具有以下规格:

  • 最大**漏源电压(V[DSS])**为 60 伏特直流

  • 最大**连续漏极电流(I[D])**为 32A DC(安培而不是毫安!)

  • 最大脉冲漏极电流(I[DM])为 128A DC

  • **栅极阈值电压(V[GS(th)])**在 1 至 2.5 伏特直流范围内(<5 伏特,因此它是逻辑电平兼容的)

  • **漏-源导通电压(V[SD])**最大为 1.5 伏特直流

您可以在前面的电路中替换 FQP30N06L(或另一个 N-通道增强模式逻辑电平 MOSFET),它会起作用,但请记住以下几点:

  • FQP30N06L 的 G、D 和 S 引脚的顺序与 2N7000 不同,因此您需要调整接线。

  • 在处理更高电压和电流时,最好使用光耦隔离器将 MOSFET 与树莓派隔离开(我们将在讨论继电器时看到这种配置)。

  • 在高电流下,功率 MOSFET 可能会变得非常热-周围的元件和导线甚至是面包板都可能会熔化,因此在使用时要小心谨慎。

当控制高功率负载时,高功率 MOSFET 可能会变热,并且可以安装散热器,例如,FQP30N06L 的金属顶部上有一个散热器连接的孔。关于何时需要散热器的决定因素和计算超出了我们的范围,但是如果您的 MOSFET 变得太热(并且您在其数据表参数范围内使用它),那么请添加散热器。

如果您喜欢使用 MOSFET 控制更高电流负载的想法,您可能会喜欢在 eBay 等网站上研究现成的 MOSFET 模块。在学习了光耦合器和 MOSFET 之后,您现在可以理解这些模块是如何构建的 - 有些直接使用 MOSFET 直接连接到控制设备(即 GPIO 引脚),就像我们刚刚做的那样,而其他一些则在控制设备和 MOSFET 之间放置光耦合器。

您已经学会了如何使用 MOSFET 晶体管作为数字开关的基础知识。接下来,我们将把这些知识与光耦合器的学习结合起来,在面包板上构建我们的继电器驱动电路。

使用继电器作为开关

经典继电器是一种电机械元件,允许较小电流的设备开关更高电流的设备或负载的开关。原则上,它们就像我们之前使用的 MOSFET 或光耦合器一样。那么为什么要使用继电器?以下是一些原因:

  • 对于高电压和电流负载,它们往往比等效的 MOSFET 便宜得多。

  • 在高电流下,它们不会像 MOSFET 那样变得不可触摸的热。

  • 与光耦合器类似,继电器还提供输入和输出电路之间的电气隔离。

  • 它们只是电控开关,因此非电气工程师很容易理解和使用。

  • 它们经受住了时间的考验,被证明是控制高负载的简单而健壮的方式(尽管它们最终会磨损 - SRD-05VDC-SL-C 的数据表列出了其额定寿命为 100,000 次操作)。

还有一种被称为固态继电器SSR)的继电器类型,它没有移动部件,但通常比可比较的机械继电器更昂贵。

我们的第一个任务是创建我们的电路,接下来我们将完成这一步。

构建继电器驱动电路

让我们构建我们的继电器驱动电路。我们将分三部分进行,首先是放置组件:

图 7.11 - 继电器驱动电路(第一部分)

以下是建设的第一部分的步骤。步骤编号与图 7.11中的编号黑色圆圈相匹配:

  1. 将 PC817 放入面包板中,注意 IC 的引脚 1 连接到左侧面包板银行,如图所示。

  2. 将 1kΩ电阻器放入面包板中。电阻器的一端连接到 PC817 的引脚 1。

  3. 将 MOSFET 放入面包板中,注意正确放置源腿、栅极和漏极。我们的示例布局假定使用 2N7000 MOSFET。如果需要帮助识别腿部,请参阅图 7.8

  4. 将 1kΩ电阻器放入面包板中。这个电阻器的一端连接到 MOSFET 的栅极。

  5. 将 100kΩ电阻器放入面包板中。这个电阻器的一端也连接到 MOSFET 的栅极。

  6. 将二极管放入面包板中,注意正确放置组件,使带有标记的阴极腿(组件末端)指向面包板底部。

现在您已经放置了各个组件,接下来我们将连接这些组件:

图 7.12 - 继电器驱动电路(第二部分)

以下是第二部分建设的步骤。步骤编号与图 7.12中的编号黑色圆圈相匹配:

  1. 将您在上一个步骤 2中放置的电阻器连接到树莓派上的 3.3 伏引脚。

  2. 将 PC817 的引脚 2 连接到树莓派上的 GPIO 21。

  3. 将 PC817 的引脚 4 连接到右侧电源轨的正轨。

  4. 将 MOSFET 的源腿连接到右侧电源轨的负轨。

  5. 将连接到 MOSFET 的漏极的 100kΩ电阻器连接到右侧电源轨的负轨。

  6. 将 PC817 的引脚 4 连接到二极管的阴极腿。

  7. 将 MOSFET 的漏极引脚连接到二极管的阳极引脚。

最后,我们将连接电源和继电器:

图 7.13 - 继电器驱动电路(3/3)

以下是构建的第三部分和最后一部分的步骤。步骤编号与图 7.13中的编号黑色圆圈相匹配:

  1. 将右侧电源轨道的正轨连接到 5 伏特电源的正输出端。

  2. 将右侧电源轨道的负轨连接到 5 伏特电源的负输出端。

  3. 将二极管的阳极引脚连接到继电器的一个线圈端子。

  4. 将二极管的阴极引脚连接到继电器的另一个线圈端子。

  5. 另一个5 伏特电源的负输出端连接到继电器的 com 端子。

步骤 5中,您必须为这个电路使用两个不同的外部电源,因为继电器线圈的电流要求和潜在的继电器负载很可能太大,无法从树莓派的电源中借用。

  1. 另一个5 伏特电源的正输出端连接到负载的正输入端(例如,电机上的一个端子)。

  2. 最后,将继电器的NO常开)端子连接到负载的正输入端。

在继电器上使用 NO 端子意味着负载默认处于关闭状态,只有在继电器吸合时才通电,这发生在 GPIO 21 为低电平时(记住这个电路是低电平有效)。如果将负载连接到继电器中的NC常闭)端子,负载将默认通电,即使树莓派关闭也是如此。

干得好!您完成了面包板电路,如图 7.13所示。这是与本章开头的图 7.1中显示的原理图相匹配的面包板电路。该面包板电路显示了一个 5 伏特的继电器线圈电源和一个 5 伏特的负载电源。然而,该电路可以使用不同的电源,但需要遵循以下几点:

  • 在这个电路中使用的电阻和 2N7000 MOSFET 能够驱动一个 12 伏特的继电器,比如 SRD-12VDC-SL-C。您只需要确保继电器线圈电源是 12 伏特而不是 5 伏特。

  • 负载电源被说明为 5 伏特,但是,如果您的负载需要更高的电压(在继电器规格内),可以增加。

现在我们有了一个完成的电路,让我们运行一个 Python 程序来控制继电器。

使用 Python 控制继电器驱动电路

运行以下代码,该代码位于chapter07/optocoupler_test.py文件中。继电器应该会发出点击声并在 2 秒后停止。这是我们创建和测试光耦电路时使用的相同代码,因为我们的树莓派连接到了光耦。

我们之前学习 MOSFET 时看到,我们可以直接将 MOSFET 连接到 GPIO 引脚并控制继电器,而无需光耦。那么,为什么前面的电路有一个光耦呢?

答案是我们的电路在技术上并不需要,而且有现成的继电器模块(尽管较少)不带有光耦。然而,有一个光耦存在并没有坏处,因为它提供了一定程度的电气隔离保护,以防继电器控制电路失效或在接线电源时发生意外。

最后,关于在 eBay 等网站上可以找到的具有多个继电器的继电器模块呢?通常只有一个继电器电路被复制多次——通常您可以为每个继电器计数一个晶体管和光耦对(尽管光耦和晶体管可以以芯片形式出现,即多个光耦或多个光耦在一个封装中,因此在某些模块上您可能只会看到芯片)。另外,请注意,有些模块将使用 BJT 而不是 MOSFET。如果您可以读取元件的零件号,您可以随时进行网络搜索以确定它们是什么。

最后,为了总结我们对打开和关闭的探索,这里是一个比较本章中使用的开关元件的表格:

- 光耦 MOSFET 继电器
- 结构 固态 固态 机械
- 电流 交流或直流(取决于光耦) 仅直流(从 TRIACS 开始研究交流) 交流和直流
- 成本 \(-\) \((低容量)至$$\)(高容量) $
- 很热(不能触摸) 对于高电流功率 MOSFET 是
- 控制电压/电流 低(需要打开和关闭内部 LED) 低(需要对栅极施加电压) 高(需要通电继电器线圈)
- 负载电压/电流 低(例如,PC817 最大 50mA) 低(例如,2N27000 为 200mA);高(例如,FQP30N06L 为 32A) 高(例如,SRD-05VDC-SL-C 10A)
- 电气隔离
- 示例应用 在控制电路和被控电路之间提供电气隔离 允许低电流/电压电路控制更高电压/电流电路 允许低电流/电压电路控制更高电压/电流电路
- 寿命 长寿命 长寿命 短寿命(活动部件最终会磨损)
- 使用 PWM 不是——继电器无法切换得足够快,而且只会更快地磨损继电器!

恭喜您完成了本章!您现在了解了多种控制具有电压和电流要求的负载的方法,这些要求超出了树莓派 GPIO 引脚的 3.3 伏特/8 毫安的限制。

总结

在本章中,我们学习了如何打开和关闭开关。我们首先简要回顾了典型的继电器驱动电路,然后学习了如何使用万用表测量直流电机、LED 和继电器的电流需求。接下来,我们讨论了光耦的特性,并学会了如何将其用作数字开关。然后,我们讨论了 MOSFET,并发现了如何将其用作开关和使用 PWM 进行电机速度控制。

本章中学到的信息、电路和练习将帮助您做出明智的决策,并进行必要的计算和测量,以选择合适的元件并创建可以安全地从树莓派引脚中获取更多电流和更高电压的电路,用于打开和关闭设备和其他负载。

我们对本章的方法是逐步探索和构建继电器驱动电路,这为您提供了一个实际示例,说明了为什么和如何将开关元件串联起来控制更高功率的元件和/或负载。此外,我们了解到光耦可以用于电气隔离电路,这可以是一种有用和实用的技术,可以帮助我们隔离和保护树莓派,以防电路故障或接线错误造成意外损坏。

在下一章中,我们将关注不同类型的 LED、蜂鸣器和可视元件,这些元件可以用来向用户发出信号或显示信息。

问题

最后,这里是一系列问题,供您测试对本章材料的了解。您将在本书的“评估”部分找到答案:

  1. 在控制晶体管时,MOSFET 和 BJT 有何不同?

  2. 您正在使用 MOSFET 控制电机,但是当您关闭 MOSFET(例如,使 GPIO 引脚低电平)时,电机并没有立即关闭,而是减速旋转。为什么?

  3. 您选择了一个随机的 MOSFET,想要从树莓派 3.3 伏特的 GPIO 控制它,但它不起作用。可能的问题原因是什么?

  4. 除了开关功能,光耦和继电器与晶体管不同的常见特征是什么?

  5. 主动低电平和主动高电平的 GPIO 有什么区别?

  6. 为什么我们更喜欢在 MOSFET 的栅极上使用物理下拉电阻,而不是在代码中激活的下拉电阻?

  7. 对于直流电机,失速电流代表什么?

  8. 直流电机的连续电流和自由电流有什么区别?

进一步阅读

以下教程是关于晶体管、它们的各种类型和应用的全面介绍:

  • [https://www.electronics-tutorials.ws/category/transistor](从 MOSFET 部分开始)

第八章:灯光、指示灯和显示信息

在上一章中,我们探讨并学习了如何使用光耦、晶体管和继电器电路,以及这三个组件如何共同工作以创建一个常见的继电器控制模块。我们还介绍了如何使用万用表测量负载的电流使用量,以便您可以就应该使用什么方法或组件来开关或控制外部负载做出明智的决定。

在本章中,我们将介绍使用 RGB LED 制作颜色的两种替代方法,并创建一个简单的应用程序来监视树莓派的 CPU 温度,并在 OLED 显示屏上显示结果。最后,我们将看到如何结合 PWM 和蜂鸣器来发出声音。

完成本章后,您将拥有知识、经验和代码示例,可以根据自己的项目需求来调整,以便在需要向用户显示信息、发出声音或简单地用灯光吸引他们的情况下使用。此外,您所学到的内容也可以适用于其他类型的兼容显示屏和照明设备,如果您希望进一步探索这些主题的话。

本章将涵盖以下主题:

  • 使用 RGB LED 制作颜色

  • 使用 SPI 控制多色 APA102 LED 灯带

  • 使用 OLED 显示屏

  • 使用蜂鸣器和 PWM 发出声音

技术要求

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

  • 树莓派 4 型 B

  • Raspbian OS Buster(带桌面和推荐软件)

  • 至少需要 Python 版本 3.5

这些要求是本书中代码示例的基础。可以合理地期望,只要您的 Python 版本是 3.5 或更高,代码示例应该可以在树莓派 3 型 B 或不同版本的 Raspbian OS 上无需修改即可运行。

您可以在此处的 GitHub 存储库的chapter08文件夹中找到本章的源代码:github.com/PacktPublishing/Practical-Python-Programming-for-IoT

您需要在终端中执行以下命令来设置虚拟环境并安装本章代码所需的 Python 库:

$ cd chapter08               # Change into this chapter's folder
$ python3 -m venv venv       # Create Python Virtual Environment
$ source venv/bin/activate   # Activate Python Virtual Environment
(venv) $ pip install pip --upgrade        # Upgrade pip
(venv) $ pip install -r requirements.txt  # Install dependent packages

以下依赖项已从requirements.txt中安装:

本章练习所需的电子元件包括以下内容:

让我们首先看看如何使用 PWM 来设置 RGB LED 的颜色。

使用 RGB LED 和 PWM 制作颜色

在本节中,我们将学习如何使用脉宽调制PWM)与 RGB LED 一起创建不同的颜色。作为提醒,PWM 是一种创建可变电压的技术,当应用于 LED 和电阻对时,可以用来改变 LED 的亮度。我们首先在第二章中讨论了 PWM 并用它来改变 LED 的亮度,使用 Python 和物联网入门。然后我们在第五章中更深入地讨论了 PWM,将树莓派连接到物理世界

RGB LED 是一个单一包装中的三个单色 LED(红色、绿色和蓝色),如图 8.1所示:

图 8.1 - RGB LED 品种

您会注意到显示了两种类型:

  • 共阴极:红色、绿色和蓝色 LED 共享一个公共的阴极腿,这意味着共腿连接到负电压或地面电压源 - 阴极 = 负极。

  • 共阳极:红色、绿色和蓝色 LED 共享一个公共的阳极腿,这意味着共腿连接到正电压源 - 阳极 = 正极。

共腿将是四条腿中最长的。如果最长的腿最靠近 LED 外壳的平面一侧,那么它是共阴极类型。另一方面,如果最长的腿靠近唇部(因此离平面一侧最远),那么它是共阳极类型。

我们之前在第五章*,将树莓派连接到物理世界*中学习了如何使用 PWM 来设置单个 LED 的亮度,但是如果我们改变 RGB LED 中三种单独颜色的亮度会发生什么?我们混合单独的颜色来创建新的颜色!让我们创建一个电路并开始混合。

创建 RGB LED 电路

在本节中,我们将创建一个简单的电路来控制 RGB LED,并且我们将使用共阴极RGB LED(即,三个单独的 LED 共享一个公共的 GND 连接)。

我们将首先按照面包板上显示的图 8.2构建电路:

图 8.2 - 共阴极 RGB LED 原理图

以下是我们即将构建的原理图的伴随面包板布局:

图 8.3 - 共阴极 RGB LED 电路

以下是要遵循的步骤,这些步骤与图 8.3中编号的黑色圆圈相匹配:

  1. 首先将 RGB LED 放入面包板中,注意 LED 的阴极腿的定位。

  2. 放置 200Ω电阻(R1)。这个电阻的一端连接到 LED 的红色腿。

  3. 放置第一个 15Ω电阻(R2)。这个电阻的一端连接到 LED 的蓝色腿。

  4. 放置第二个 15Ω电阻(R3)。这个电阻的一端连接到 LED 的绿色腿。

  5. 将树莓派上的一个地引脚连接到负电源轨道。

  6. 将树莓派上的 GPIO 16 连接到您在步骤 2中放置的 200Ω电阻(R1)的另一端。

  7. 将 RGB LED 的阴极腿连接到负电源轨道。

  8. 将树莓派上的 GPIO 20 连接到您在步骤 3中放置的 15Ω电阻(R2)的另一端。

  9. 将树莓派上的 GPIO 21 连接到您在步骤 4中放置的 15Ω电阻(R3)的另一端。

在我们测试 RGB LED 电路之前,让我们简要回顾一下我们是如何得到这个电路中的 200Ω和 15Ω电阻的。200Ω电阻(R1)是使用我们在第六章中介绍的相同过程得出的,软件工程师的电子学 101。R2 和 R3 的 15Ω电阻是使用相同的过程得出的,不同之处在于用于蓝色和绿色 LED 计算的典型正向电压为 3.2 伏特。如果你研究样本数据表,你会注意到蓝色和绿色 LED 的正向电压列出了最大正向电压为 4.0 伏特。即使在典型值 3.2 伏特下,我们也非常接近树莓派 GPIO 引脚的 3.3 伏特。如果你不幸需要超过 3.3 伏特的蓝色或绿色 LED 的 RGB LED,它将无法工作——尽管我从未遇到过这种情况...至少目前还没有。

现在我们准备测试我们的 RGB LED。

运行和探索 RGB LED 代码

现在你的电路已经准备好了,让我们运行我们的示例代码。我们的示例将点亮 LED 并使其交替显示不同的颜色。以下是要遵循的步骤:

  1. 运行chapter08/rgbled_common_cathode.py文件,你应该会看到 RGB LED 循环显示颜色。请注意前三种颜色,应该是红色、绿色,然后是蓝色。

要使用共阳RGB LED,它需要与图 8.2中显示的方式不同地接线——共阳腿必须连接到树莓派的+3.3V 引脚,而 GPIO 连接保持不变。另一个变化是在代码中我们需要反转 PWM 信号——你会在chapter08文件夹中找到一个名为rgbled_common_anode.py的文件,其中包含了已注释的差异。

  1. 如果你的前三种颜色不是红色、绿色,然后是蓝色,你的 RGB LED 可能与图 8.1中显示的 RGB LED 的引脚顺序不同,以及图 8.2中的电路。你需要做的是更改代码中的 GPIO 引脚编号(参见以下代码片段)并重新运行代码,直到颜色顺序正确为止。

  2. 在红色、绿色和蓝色循环之后,RGB LED 将以彩虹色动画显示,然后程序完成。

让我们讨论代码的有趣部分并看看它是如何工作的:

在第 1 行,我们从PIL.ImageColor模块导入getrgbgetrgb为我们提供了一种方便的方法,将常见的颜色名称(如红色)或 HEX 值(如#FF0000)转换为它们的 RGB 分量值(如(255, 0, 0)):

from time import sleep
import pigpio
from PIL.ImageColor import getrgb    # (1)

GPIO_RED = 16
GPIO_GREEN = 20
GPIO_BLUE = 21

pi.set_PWM_range(GPIO_RED, 255)      # (2)
pi.set_PWM_frequency(GPIO_RED, 8000)
# ... truncated ... 

从第 2 行开始,我们明确为每个 GPIO 引脚配置 PWM(占空比范围为 255,频率为 8000 是 PiGPIO 的默认值)。PWM 占空比范围从 0 到 255 完美地映射到 RGB 分量颜色值范围 0...255,我们很快就会看到这是我们如何设置每个颜色 LED 的亮度的方法。

在下面的代码中,在第 3 行,我们有set_color()的定义,它负责设置我们的 RGB LED 的颜色。color参数可以是一个常见的颜色名称,比如yellow,也可以是一个 HEX 值,比如#FFFF00,或者getrgb()可以解析的许多格式之一(请参阅rgbled_common_cathode.py源文件,了解常见格式的列表):

def set_color(color):                                 # (3)   rgb = getrgb(color)                               
  print("LED is {} ({})".format(color, rgb))
    pi.set_PWM_dutycycle(GPIO_RED,   rgb[0])          # (4)
  pi.set_PWM_dutycycle(GPIO_GREEN, rgb[1])
    pi.set_PWM_dutycycle(GPIO_BLUE,  rgb[2])

在第 4 行,我们看到如何使用 PWM 与单独的 GPIO 引脚来设置 RBG LED 的颜色。继续以黄色为例,我们看到以下内容:

  • GPIO_RED被设置为 0 的占空比。

  • GPIO_GREEN被设置为 255 的占空比。

  • GPIO_BLUE被设置为 255 的占空比。

绿色和蓝色的占空比值为 255 意味着这些 LED 完全开启,正如我们所知,混合绿色和蓝色会得到黄色。

当你浏览源文件时,你会在第 6 行和第 7 行遇到另外两个函数:

def color_cycle(colors=("red", "green", "blue"), delay_secs=1):   # (6)
    # ...truncated...

def rainbow_example(loops=1, delay_secs=0.01):                    # (7)
    # ...truncated...

这两种方法都委托给set_color()color_cycle()循环遍历其color参数提供的颜色列表,而rainbow_example()生成并循环遍历一系列颜色以产生彩虹序列。这些函数是我们在步骤 1中运行代码时生成光序列的原因。

我们的 RGB LED 电路有一些限制和缺点:

  • 首先,每个 RGB LED 需要三个 GPIO 引脚。

  • 其次,我们通过电阻将电流限制在 8mA,因此无法实现单个 LED 的最大亮度(我们需要约 20mA 才能实现全亮度)。

虽然我们可以引入晶体管(或适当的多通道 LED 驱动 IC)来增加电流,但我们的电路很快就会变得笨重!幸运的是,我们还有另一种方法可以用 LED 创建颜色,那就是可寻址 LED,我们将在下一节中讨论。

使用 SPI 控制多色 APA102 LED 灯带

APA102 是一种可寻址的多色(RGB)LED,使用串行外围接口(SPI)进行控制。简单来说,我们向 LED 发送指令询问它要显示什么颜色,而不是像在上一个例子中那样使用 PWM 单独控制 LED 的三个红绿蓝引脚。

如果您需要快速回顾 SPI,我们在第五章中已经涵盖了它,将您的树莓派连接到物理世界。我们还将在探索 APA102 特定代码后讨论 SPI,树莓派和 Python 的更多内容。

APA102 LED 也可以连接或串联在一起,以创建 LED 灯带或 LED 矩阵,从而创建动态和多 LED 的照明和显示解决方案。无论 LED 如何排列,我们都使用一种常见的技术来控制它们,即向一系列 APA102 LED 发送多组指令。每个单独的 LED 消耗一个指令,并将其余的传递给上游 LED 消耗。我们将在不久的将来使用 APA102 LED 灯带时看到这个想法。

APA102 LED 也被称为超级 LED、DotStar LED,有时也被称为下一代 NeoPixels。还有另一种可寻址 LED,WS2812,也被称为 NeoPixel。虽然原理和操作类似,但 WS2812 RGB LED 与 APA102 不兼容。

让我们创建一个电路并运行代码来控制我们的 APA102 LED 灯带。

创建 APA102 电路

在本节中,我们将创建我们的 APA102 电路,如下图所示。我们将在面包板上分两部分完成这个过程:

图 8.4 - APA102 LED 灯带电路原理图

让我们开始第一部分,即放置元件并连接逻辑电平转换器的低电压端:

图 8.5 - APA102 LED 电路(1/2)

以下是要遵循的步骤。步骤编号与图 8.5中编号的黑色圆圈相匹配:

  1. 将逻辑电平转换器(逻辑电平转换器)放入面包板中,将低电压端朝向树莓派。不同的逻辑电平转换器可能有不同的标记,但是低电压端应该是清楚的。在我们的示例中,一侧有一个LV(低电压)端子,另一侧有一个HV(高电压)端子,用于区分两侧。

  2. 连接左侧和右侧电源轨道上的负轨。

  3. 将树莓派上的 3.3 伏特引脚连接到左侧电源轨道的正轨。

  4. 将逻辑电平转换器上的 LV 端子连接到左侧电源轨道的正轨。

  5. 将树莓派上的MOSI(主输出从输入)引脚连接到逻辑电平转换器上的 A2 端子。

  6. 将树莓派上的SLCK(串行时钟)引脚连接到逻辑电平转换器上的 A1 端子。

  7. 将逻辑电平转换器上的 GND 端子连接到左侧电源轨的负轨。

  8. 将左侧电源轨上的负轨连接到树莓派的 GND 引脚。

现在我们已经将逻辑电平转换器的低电压端连接到了树莓派,接下来我们将把高电压端连接到 APA102 LED 灯带。作为提醒,树莓派的 GPIO 引脚工作在 3.3 伏(因此是电压),而 APA102 工作在 5 伏(因此是电压):

图 8.6 – APA102 LED 电路(2/2)

以下是我们搭建的第二部分的步骤。步骤编号与图 8.6中编号的黑色圆圈相匹配:

  1. 将逻辑电平转换器的 HV 端子连接到右侧电源轨的正轨。

  2. 从 B2 端子到面包板上未使用的一行放置一根跳线(在插图中,这显示在孔 G16 处)。

  3. 从 B1 端子到面包板上未使用的一行放置另一根跳线(在插图中,这显示在孔 H14 处)。

  4. 将逻辑电平转换器高电压端的 GND 端子连接到右侧电源轨的负轨。

  5. 将电源的正输出连接到右侧电源轨的正轨。

  6. 将电源的负输出连接到右侧电源轨的负轨。

  7. 将 APA102 LED 灯带的 VCC 端子或导线连接到右侧电源轨的正轨。

您的 APA102 必须正确连接。您会注意到 APA102 LED 灯带上的箭头,如图 8.4所示。这些箭头表示数据流的方向。确保您的 APA102 LED 灯带箭头与插图相匹配(即箭头指向面包板的反方向)。

如果您的 APA102 没有箭头,请查看端子的命名。LED 灯带的一侧可能有 CI/DI(I = 输入),而另一侧有 DO/CO(O = 输出)。我们需要连接的是输入端。

  1. 将 APA102 LED 灯带的CI(时钟输入)端子或导线连接到您在步骤 3中放置的连接回逻辑电平转换器的 B1 端子的导线。

  2. 将 APA102 LED 灯带的DI(数据输入)端子或导线连接到您在步骤 2中放置的连接回逻辑电平转换器的 B2 端子的导线。

  3. 最后,将 APA102 LED 灯带的 GND 端子或导线连接到右侧电源轨的负轨。

干得好!您现在已经完成了 APA102 LED 灯带电路。在完成这个电路搭建时,您会注意到我们使用了逻辑电平转换器。这是因为 APA102 需要 5 伏逻辑电才能正常运行。APA102 的数据表明明确提到最小逻辑电压为 0.7 VDD,即 0.7 x 5 伏=3.5 伏,这高于树莓派的 3.3 伏逻辑电平。

如果您需要关于逻辑电平和逻辑电平转换的复习,请参考第六章,软件工程师的电子学 101

让我们考虑一下(如果您在想)3.3 伏只比 3.5 伏略低一点——这肯定够接近了吧?您可以尝试用 3.3 伏来控制 APA102,它可能会带来一定程度的成功。然而,您可能也会遇到一些随机效果和混乱,例如,随机 LED 未按预期开启或关闭,LED 闪烁,或 LED 显示错误的颜色。不幸的是,APA102 是不兼容 3.3 伏的 5 伏逻辑设备之一,因此我们必须采取额外的步骤,使用逻辑电平转换器来满足其 3.5 伏最小逻辑电平要求。

现在您已经建立了 APA102 电路,接下来我们将讨论我们需要考虑的问题,以便为这个电路供电。

为 APA102 电路供电

在第七章*,打开和关闭东西*中,我们讨论了了解您正在使用的“负载”的电流要求的重要性。让我们将这个知识应用到我们的 APA102 LED 灯带上,以便我们可以正确地为其供电。我们的示例假设 LED 灯带包含 60 个 LED,但是您需要根据灯带上 LED 的数量调整计算。

举例来说,我们有以下内容:

  • 一个包含 60 个 LED 的 APA102 LED 灯带。

  • 每个 LED 使用(平均)最大 25mA(来自数据表并经过测量确认)。

  • LED 灯带在空闲时消耗大约 15mA(没有 LED 亮起)。

单个 RGB LED 在设置为白色时使用其最大电流,这是当每个 LED(红色、绿色和蓝色)都处于最大亮度时。

使用前面的值,我们可以计算出 60 个 LED 的预期最大电流需求,这超过了 1.5 安培:

如果我们假设我们使用的是面包板电源供应,那么如果我们保守地假设我们的面包板电源供应最多只能提供大约 700mA,我们实际上不能将 60 个 LED 灯带上的所有 LED 都设置为全白。如果这样做,那么(取决于电源供应)它可能会在其内部过载保护启动时关闭,它可能会冒烟,或者它可能会限制其输出电流,这可能会导致 LED 看起来呈红色而不是白色。

让我们逆向工作,计算出我们可以从 700mA 电源供应中供电的 LED 的安全数量:

如果我们然后减去 2 个 LED(50mA)作为一个小的安全缓冲区,我们得到 25 个 LED。记住这个数字(或者您计算的数字),因为我们在运行示例代码时将需要它。

计算出您可以使用的安全 LED 数量后,我们现在准备配置和运行我们的 Python 示例。

配置和运行 APA102 LED 灯带代码

现在您的电路已经准备好,我们知道 LED 灯带的预期电流使用情况,让我们配置并点亮 LED 灯带:

  1. 编辑chapter08/apa102_led_strip.py文件,并在文件顶部附近查找以下行。将数字调整为您之前计算的安全 LED 数量,或者如果您的灯带有足够能力的电源供应,则调整为灯带上的 LED 数量:
NUM_LEDS = 60     # (2)
  1. 保存您的编辑并运行代码。如果一切连接正确,您应该观察到 LED 灯带循环显示红色、绿色和蓝色,然后执行一些不同的光序列。

如果您的 LED 灯带没有工作,请查看本节后面的APA102 LED 灯带故障排除提示

如果您的灯带没有按照红、绿和蓝的顺序显示,那么您需要调整代码以设置正确的顺序——我将向您展示在代码的哪个部分可以调整 LED 的顺序。

现在我们在代码中配置了安全数量的 LED,让我们走一遍代码,看看它是如何工作的。

APA102 LED 灯带代码演示

从以下代码的第一行开始,我们有导入。我们将使用 Python 的deque集合实例(我只是简单地称之为数组)来在内存中模拟 APA102 LED 灯带——在将其应用于 LED 灯带之前,我们将在这个数组中构建和操作我们希望每个 LED 显示的颜色顺序。然后我们从 PIL 库中导入getrgb函数,用于处理颜色格式(就像我们在前面的 RGB LED 示例中所做的那样):

# ...truncated...
from collections import deque                                   # (1) from PIL.ImageColor import getrgb
from luma.core.render import canvas
from luma.led_matrix.device import apa102
from luma.core.interface.serial import spi, bitbang

最后,三个luma导入是用于 APA102 LED 灯带控制。Luma 是一个成熟的高级库,用于使用 Python 处理各种常见的显示设备。它支持 LCD、LED 灯带和矩阵等,还包括我们将在本章后面介绍的 OLED 显示器。

在本章中,我们只能浅尝 Luma 库的功能,所以我鼓励您探索其文档和各种示例——您将在本章末尾的进一步阅读部分找到链接。

接下来,我们来到下面代码的第 3 行,我们将color_buffer分配给deque的一个实例,该实例初始化为与我们条带中的 LED 数量相同的元素数。每个元素默认为黑色(即 LED 关闭):

# ...truncated...
color_buffer = deque(['black']*NUM_LEDS, maxlen=NUM_LEDS)      # (3) 

在下面代码的第 4 行中,我们开始创建我们的 APA102 的软件接口。在这里,我们创建了一个代表树莓派上默认硬件 SPI0 接口的spi()实例。要使用此接口,您的 APA102 必须连接到树莓派上的 SPI 引脚,如下所示:

  • DI 连接到 MOSI

  • CI 连接到 SCLK

在下面的代码片段中,port=0device=0与 SPI0 接口相关:

# ...truncated... serial = spi(port=0, device=0, bus_speed_hz=2000000)           # (4)  

bus_speed_hz参数设置了 SPI 接口的速度,对于我们的示例,我们将其从默认值 8,000,000 降低到 2,000,000,只是为了确保您的逻辑电平转换器能够工作。并非所有逻辑电平转换器都相同,它们将具有可以转换逻辑电平的最大速度。如果 SPI 接口的操作速度快于逻辑电平转换器可以转换的速度,我们的电路将无法工作。

在下面的代码中的第 5 行(已注释掉)中,我们有一个软件替代硬件 SPI 的选择,称为大砰,它可以在任何 GPIO 引脚上工作,但速度会受到影响。这类似于我们在第五章中讨论过的软件与硬件 PWM 的权衡,将您的树莓派连接到物理世界

# ...truncated... # serial = bitbang(SCLK=13, SDA=6)                             # (5)  # ...truncated... device = apa102(serial_interface=serial, cascaded=NUM_LEDS)    # (6)

在上述代码的第 6 行中,我们创建了一个apa102类的实例,指定了我们刚刚创建的serial实例和我们条带中 LED 的数量。从此刻开始,在代码中与 APA102 LED 条带进行交互,我们将使用device实例。

要初始化我们的 LED 条带,在下面代码的第 7 行中,我们调用device.clear()并将默认全局对比度设置为 128(即半亮度)。您可以调整此级别以找到您满意的亮度,记住更多的对比度/亮度意味着更多的电流使用。请注意,先前在计算安全 LED 数量时,计算中使用的每个 LED 的 25mA 假定最大亮度(即 255):

device.clear()                                                   # (7) contrast_level = 128 # 0 (off) to 255 (maximum brightness) device.contrast(contrast_level)

在下面代码的第 8 行中,我们有set_color()函数。我们使用此函数在color_buffer数组中将单个或所有元素设置为指定的颜色。这是我们在内存中构建 APA102 LED 条带要显示的颜色排列的方法:

def set_color(color='black', index=-1):                          # (8)
  if index == -1:
        global color_buffer
        color_buffer = deque([color]*NUM_LEDS, maxlen=NUM_LEDS)
    else:
        color_buffer[index] = color

现在,我们将跳转到下面代码块的第 12 行,到update()函数。这个函数循环遍历color_buffer,并使用代表我们 APA102 的 Luma device实例,使用draw.point((led_pos, 0), fill=color)来向设备提供要显示的颜色。这就是 Luma 库的魔力——它通过给我们一个非常简单的软件接口,使我们免受较低级别 APA102 和 SPI 数据和硬件协议的影响。

如果您想了解更多关于较低级别 SPI 使用和协议的知识,那么 APA102 是一个很好的起点。首先阅读 APA102 的数据协议的数据表,然后在pypi.org或 GitHub 上找到一个简单的 APA102 模块并查看其代码。在 PiGPIO 网站上也可以找到一个 APA102 的示例,进一步阅读部分中包含了链接。

重要的是要记住,在对color_buffer进行更改后需要调用update()

def update():                                                   # (12)
  with canvas(device) as draw:
        for led_pos in range(0, len(color_buffer)):
            color = color_buffer[led_pos]

            ## If your LED strip's colors are are not in the expected
 ## order, uncomment the following lines and adjust the indexes ## in the line color = (rgb[0], rgb[1], rgb[2]) # rgb = getrgb(color) # color = (rgb[0], rgb[1], rgb[2]) # if len(rgb) == 4: #     color += (rgb[3],)  # Add in Alpha    draw.point((led_pos, 0), fill=color)

如果出现 LED 灯带颜色不是标准的红、绿和蓝顺序,那么上面注释掉的代码部分可以用来改变颜色顺序。我从未遇到过非标准 APA102,但我读到过可寻址的 RGB LED 具有非标准顺序,所以我想我还是把那部分代码放进来,以防万一。

接下来是第(9)、(10)和(11)行,我们有三个简单操作color_buffer的函数:

def push_color(color):                                       # (9)   color_buffer.appendleft(color)

def set_pattern(colors=('green', 'blue', 'red')):           # (10)     range(0, int(ceil(float(NUM_LEDS)/float(len(colors))))):
        for color in colors:
            push_color(color)

def rotate_colors(count=1):                                 # (11)
    color_buffer.rotate(count)

push_color(color) 在第(9)行将一个新颜色推入color_buffer的索引 0,而第(10)行的set_pattern()用重复的颜色模式序列填充color_buffer。第(11)行的rotate_colors()旋转color_buffer中的颜色(并将它们包装起来——最后一个变成第一个)。你可以通过使用小于 0 的计数值向后旋转。

最后,在源代码的末尾,我们有以下函数,提供了你运行文件时看到的示例。这些函数使用之前讨论过的函数的组合来控制 LED 灯带:

  • cycle_colors(colors=("red", "green", "blue"), delay_secs=1)

  • pattern_example()

  • rotate_example(colors=("red", "green", "blue"), rounds=2, delay_secs=0.02)

  • rainbow_example(rounds=1, delay_secs=0.01)

我们将用一些结论性的笔记来完成对 APA102 使用 SPI 接口的覆盖。

APA102 和 SPI 接口的讨论

如果回想一下第五章,将树莓派连接到物理世界,我们讨论了串行外围接口 (SPI),你可能记得我们提到它使用四根导线进行数据传输。然而,如果你考虑我们在图 8.6中的电路,我们只使用了两根导线(DI 和 CI),而不是四根。怎么回事?

以下是 APA102 的 SPI 映射:

  • 树莓派上的Master-Out-Slave-In (MOSI) 连接到 APA102 上的Data In (DI)。在这里,你的树莓派是,向APA102 灯带发送数据。

  • Master-In-Slave-Out (MISO) 没有连接,因为 APA102 不需要将数据发送回树莓派。

  • 树莓派上的 SCLK 连接到 APA102 上的Clock In (CI)。

  • Client Enable/Slave Select (CE/SS) 没有连接。

最后一行重要的 CE/SS 值得进一步讨论。CE/SS 通道由主设备用于告诉特定的从设备它即将接收数据。正是这种机制允许单个 SPI 主控制多个 SPI 从。

但是,我们不会(也不能)使用 CE/SS 与 APA102,因为我们没有地方连接 CE/SS 引脚。这意味着 APA102 总是在等待来自主设备的指令,实际上占用了 SPI 通道。

如果我们使用 APA102(或任何没有 CE/SS 的设备),那么除非我们采取额外的步骤,否则我们不能将多个 SPI 设备连接到主硬件 SPI。以下是一些选项:

  • 如果性能降低没有不良影响,可以在通用 GPIO 引脚上使用大爆破。

  • 在树莓派上启用硬件 SPI1。它默认情况下是未启用的,需要编辑/boot/config.txt。如果搜索Raspberry Pi enable SPI1,你会在网上找到指南和提示。

  • 找到一个包括使能引脚的逻辑电平转换器,并编写代码手动控制这个引脚作为代理 CE/SS。

我们将用一些故障排除提示来结束关于 APA102 的部分。

APA102 LED 灯带故障排除提示

如果你无法点亮 APA102,或者发现随机 LED 未开启或关闭,或者显示意外颜色或随机闪烁,请尝试以下操作:

  • APA102 需要 5 伏逻辑电平:确保你使用的是逻辑电平转换器,并且连接正确——HV 连接到 5 伏,LV 连接到 3.3 伏。

  • 确保 APA102 的 DI/CI 端连接到逻辑电平转换器。

  • 确保您的电源可以提供足够的电流。例如,电流或电压的不足可能会使白色看起来更像红色。

  • 确保您的电源的地线连接到树莓派上的地线引脚。

  • 如果您正在使用大幅振荡,请转移到硬件 SPI。

  • 如果使用硬件 SPI(也就是创建spi()类的实例),请尝试以下操作:

  • 如果您收到错误消息SPI 设备未找到,请确保在 Raspbian OS 中已启用 SPI。我们在第一章中介绍了这一点,设置您的开发环境

  • 如果您之前已经使用 GPIO 8、9、10 或 11 进行常规 I/O 操作,那么要么按照前面的方法禁用并重新启用 SPI 接口,要么重新启动树莓派以重置硬件 SPI 接口。

  • 如果您的逻辑电平转换器无法跟上 SPI 总线速度,请尝试降低 SPI 总线速度——也就是说,它无法将 3.3 伏转换为 5 伏的信号,就像 SPI 接口产生它们一样(提示:将serial = spi(port=0, device=0, bus_speed_hz=2000000)中的bus_speed_hz参数降低到 1,000,000 或 500,000)。

  • 将 APA102 的 DI 和 CI 直接连接到树莓派上的 SDA 和 SCLK。这里的目标是绕过逻辑电平转换器,以排除它作为问题的可能性。

干得好!这是关于 APA102 的一个冗长而详细的部分。除了 APA102 本身之外,我们还涵盖了许多概念,包括如何计算 LED 灯带的功率需求以及 Luma 库的介绍,该库可以用于控制 APA102 之外的许多不同的照明和显示设备。然后,我们总结了在您的 APA102 电路、设置或代码第一次运行时的实际故障排除提示。

所有这些知识和经验都将适用于您进行的类似照明项目和 SPI-based 项目。特别是,它将是一个有用的参考,用于计算照明项目的功率需求,并在它们不起作用时排除电路和代码的故障。它还提供了我们将在下一节中构建的基本基础,我们将在其中研究如何将 OLED 显示器与我们的树莓派接口。

使用 OLED 显示器

OLED有机发光二极管显示器是一种用于制作屏幕的技术。我们的示例将使用 SSD1306,这是一种单色 128x64 像素显示器,但是这些信息也适用于其他 OLED 显示器。

我们的示例程序将读取您的树莓派的 CPU 温度,并将其与温度计图标一起显示在 OLED 显示器上。我们将假设 OLED 将使用 I2C 接口连接,但是,如果您使用spi()实例(就像在 APA102 示例中)用于serial对象,那么 SPI 接口设备也应该是兼容的。Luma 库更改交互方法的能力意味着您可以在最小更改代码的情况下重用现有代码与兼容的显示设备。

我们将首先连接 OLED 显示器到树莓派并验证它是否连接。

连接 OLED 显示器

让我们将您的 OLED 显示器连接到您的树莓派,如图 8.7所示:

图 8.7 - I2C OLED 显示电路关于为您的 OLED 供电的重要说明:我们的电路,如图 8.6所示,并且相关讨论使用了 5 伏的电源。如果您查阅本章开头提到的 SSD1306 OLED 数据表,您将发现它提到了最低供电电压为 7 伏。此外,您将找到其他来源和 SSD1306 OLED 模块,它们指出了不同的电压要求。请查阅文档或购买地点,以获取您的 OLED 的正确工作电压,并根据需要调整供电电压(步骤 78)。

您可以按照以下步骤连接 OLED,这些步骤对应于图 8.7中编号的黑色圆圈:

  1. 连接左侧和右侧电源轨道上的负极。

  2. 将你的树莓派的 SDA1(数据)引脚连接到面包板上的一个空行。

  3. 将你的 OLED 显示屏的 SDA(数据)端子或线连接到用于步骤 2的同一行。

  4. 将你的树莓派的 SCL1(时钟)引脚连接到面包板上的一个空行。

  5. 将你的 OLED 显示屏的 SCL(时钟)端子或线连接到用于步骤 4的同一行。

  6. 将你的树莓派的 GND 引脚连接到左侧电源轨道的负极。

  7. 将 5 伏电源的正输出连接到右侧电源轨道的正极。

  8. 将 5 伏电源的负输出连接到右侧电源轨道的负极。

  9. 将你的 OLED 显示屏的 GND 端子或线连接到右侧电源轨道的负极。

  10. 将你的 OLED 显示屏的 VCC 端子或线(也可能被命名为 VDD、Vin、V+或类似表示电压输入的名称)连接到右侧电源轨道的正极。

干得好!这完成了我们的 OLED 电路。正如你所看到的,我们正在用 5 伏电源为 OLED 供电,然而,SDA(数据)/SLC(时钟)通道直接连接到你的树莓派上。与我们在上一节中使用的 APA102 LED 灯带不同,SSD1306 OLED 兼容 3.3 伏逻辑,因此,在时钟和数据通道上我们不需要逻辑电平转换器来转换逻辑电平电压。

让我们简要考虑一下 SSD1306 OLED 的电流要求。我的测试结果如下:

  • 黑屏:~3mA

  • 白屏(每个像素都亮):~27mA

在最大电流使用量为~27mA 的情况下,你可以尝试将+5V 连接到树莓派的 5 伏引脚,但请记住这将从你的树莓派中取走保留电流(如果你的树莓派的电源供应不足,可能会在运行代码时重置)。

如果你需要回顾使用数字万用表进行电流测量,请参考第七章*,打开和关闭*。

接下来,将你的 OLED 连接到树莓派的 SDA 和 SCL 引脚,然后我们将使用i2cdetect实用程序验证树莓派是否检测到了它。

验证 OLED 显示屏是否连接

在第五章*,将你的树莓派连接到物理世界*中,我们使用了i2cdetect命令行工具来检查 I2C 设备是否连接,并验证其 I2C 地址。通过在终端中运行以下命令来检查你的树莓派是否能看到你的 OLED 显示屏:

$ i2cdetect -y 1

如果你的 OLED 已连接,你将看到以下输出,告诉我们 OLED 已被检测到,并具有十六进制地址0x3C

# ...truncated...
30: -- -- -- -- -- -- -- -- -- -- -- -- 3c -- -- -- 
# ...truncated...

如果你的地址不同,没关系,我们只需要在代码中调整地址,接下来我们将这样做。

配置和运行 OLED 示例

我们即将探讨的代码包含在chapter08/oled_cpu_temp.py文件中。在继续之前,请先查看这个文件,以获得它包含的内容的整体视图:

  1. 如果你在前面得到的 OLED I2C 地址与0x3C不同,请在源代码中找到以下行,并更新地址参数以匹配你的 OLED I2C 地址:
serial = i2c(port=1, address=0x3C)
  1. 运行程序,你应该看到 OLED 显示屏上绘制的 CPU 温度和温度计图标。

一旦你在代码中配置了你的 OLED 显示屏地址并确认了示例在你的 OLED 上运行正常,我们就准备好审查代码并学习它是如何工作的。

OLED 代码演示

从导入开始,在第(1)行,我们从PILPillow)模块中导入类,用于创建我们想要在 OLED 显示屏上呈现的图像。我们还从 Luma 模块中导入与我们的 SSD1306 OLED 及其 I2C 接口相关的几个其他类(SPI 也被导入以供参考)。

我们看到如何在第(2)行创建一个代表我们 OLED 连接的接口的 I2C 实例。被注释掉的是 SPI 替代方案。在第(3)行,我们创建了一个代表我们 OLED 显示器的ssd1306实例,并将其分配给device变量。如果您使用的 OLED 显示器与 SSD1306 不同,您需要识别和调整ssd1306导入行以及第(3)行创建的设备实例:

from PIL import Image, ImageDraw, ImageFont         # (1)
from luma.core.interface.serial import i2c, spi
from luma.core.render import canvas
from luma.oled.device import ssd1306  #...truncated...

# OLED display is using I2C at address 0x3C serial = i2c(port=1, address=0x3C)                  # (2)
#serial = spi(port=0, device=0)
  device = ssd1306(serial)                            # (3)
device.clear()
print("Screen Dimensions (WxH):", device.size)

在第(4)行,我们遇到了get_cpu_temp()函数,该函数调用一个命令行实用程序来检索树莓派的 CPU 温度,然后解析并返回结果,我们将很快用来构建我们的显示图像:

def get_cpu_temp():     # (4)   temp = os.popen("vcgencmd measure_temp").readline() # Eg 62.5'C
  data = temp.strip().upper().replace("TEMP=", "").split("'")
    data[0] = float(data[0])

    if data[1] == 'F':  # To Celsius just in case it ever returns Fahrenheit
  data[0] = (data[0] - 32) * 5/9
  data[1] = 'C'    return (data[0], data[1])  # Eg (62.5, 'C') 

在第(5)行的以下代码中,我们定义了影响我们 OLED 显示器上显示图标的温度阈值。我们还将使用高阈值使 OLED 显示屏闪烁,以帮助创建视觉吸引力。

在第(6)行,我们加载了三个温度计图像,并从第(7)行开始将它们缩小到与我们的 SSD1306 OLED 的 128x64 像素尺寸相适应的大小:

# Temperature thresholds used to switch thermometer icons  temp_low_threshold = 60 # degrees Celsius                     # (5) temp_high_threshold = 85 # degrees Celsius   # Thermometer icons image_high = Image.open("temp_high.png")                        # (6)
image_med  = Image.open("temp_med.png")
image_low  = Image.open("temp_low.png")

# Scale thermometer icons (WxH) aspect_ratio = image_low.size[0] / image_low.size[1]            # (7)
height = 50 width = int(height * aspect_ratio)
image_high = image_high.resize((width, height))
image_med  = image_med.resize((width, height))
image_low  = image_low.resize((width, height))

接下来,我们从以下第(8)行开始定义了两个变量。refresh_secs是我们检查 CPU 温度并更新 OLED 显示屏的速率,而high_alert用于标记最高温度阈值的违反并开始屏幕闪烁:

refresh_secs = 0.5  # Display refresh rate                           #(8) high_alert = False # Used for screen blinking when high temperature   try:
    while True:
        current_temp = get_cpu_temp()
        temp_image = None    canvas = Image.new("RGB", device.size, "black")              # (9)
        draw = ImageDraw.Draw(canvas)                                # (10)
        draw.rectangle(((0,0), 
                   (device.size[0]-1, device.size[1]-1)), 
                   outline="white")

while循环中,在第(9)行,我们看到了 PIL 模块的使用。在这里,我们使用与 OLED 设备相同尺寸(即 SSD1306 的 128x64)创建了一个空白图像,并将其存储在canvas变量中。在随后的代码中,我们会在内存中操作这个画布图像,然后将其发送到 SSD1306 进行渲染。

在第(10)行创建的 draw 实例是我们用于在画布上绘制的 PIL 辅助类。我们使用这个实例来在画布上放置一个边界矩形,并稍后用它来向画布添加文本。draw实例还可以用于绘制许多其他形状,包括线条、弧线和圆形。PIL API 文档的链接可以在进一步阅读部分找到。

以下代码从第(11)行开始的代码块将使我们的 OLED 显示在high_alertTrue时闪烁:

  if high_alert:                                     # (11)
            device.display(canvas.convert(device.mode))
            high_alert = False   sleep(refresh_secs)
            continue 

从第(12)行开始,我们将从get_cpu_temp()获得的温度读数与之前定义的阈值进行比较。根据结果,我们更改将显示的温度计图像,并且对于高阈值违规,我们将high_alert = True。将high_alert设置为True将导致 OLED 显示在下一个循环迭代中闪烁:

 if current_temp[0] < temp_low_threshold:           # (12)
            temp_image = image_low
            high_alert = False     elif current_temp[0] > temp_high_threshold:
            temp_image = image_high
            high_alert = True     else:
            temp_image = image_med
            high_alert = False  

我们从以下第(13)行开始构建我们的显示。我们计算image_xy是我们的温度计图像在显示器上居中的点,然后使用image_x_offsetimage_x_offset变量来偏移该点,以将图像移动到我们希望它呈现的位置。

在第(14)行,我们将温度计图像粘贴到画布上:

# Temperature Icon image_x_offset = -40                    # (13) image_y_offset = +7 image_xy = (((device.width - temp_image.size[0]) // 2) + 
        image_x_offset, ((device.height - temp_image.size[1]) // 2) 
        + image_y_offset)
canvas.paste(temp_image, image_xy)      # (14)

在以下代码块的第(15)行,我们创建了要显示在 OLED 屏幕上的文本,并使用与图像相同的技术在第(17)行将文本定位在画布上。请注意使用draw.textsize()来获取文本的像素尺寸。

在第(16)行,我们设置font = None,以便在示例中使用默认系统字体,因为我无法完全确定您的树莓派上有哪些字体可用。在第(16)行之后被注释掉的行显示了使用自定义字体的示例。

在终端中运行fc-list命令,可以查看树莓派上安装的字体列表。

最后,在第(18)行,我们在画布上绘制文本:

# Temperature Text (\u00b0 is a 'degree' symbol)                 # (15) text = "{}\u00b0{}".format(current_temp[0], current_temp[1]) # Eg 43'C   font = None # Use a default font.                                # (16)
# font = ImageFont.truetype(font="Lato-Semibold.ttf", size=20) 

text_size = draw.textsize(text, font=font)                       # (17)
text_x_offset = +15 text_y_offset = 0 text_xy = (((device.width - text_size[0]) // 2) + text_x_offset, 
((device.height -  text_size[1]) // 2) + text_y_offset)
draw.text(text_xy, text, fill="white", font=font)                # (18)

我们现在已经到达了 while 循环的尾端。在以下代码的第(19)行,我们使用代表 SSD1306 OLED 显示器的device实例来显示canvascanvas.convert(device.mode)调用将我们创建的画布图像转换为 SSD1306 可用的格式:

# Render display with canvas device.display(canvas.convert(device.mode))        # (19)
sleep(refresh_secs)

在我们完成对 OLED 的探索之前,我想向您指出更多示例。Luma 库包含许多示例,涵盖了使用 OLED 显示器的许多方面。可以在进一步阅读中找到示例的链接。

OLED 显示器成本低廉,体积小,耗电量低,因此经常用于电池操作设备。如果您想探索树莓派的其他显示选项,您可能想调查一下可用的树莓派 TFT 显示器范围(只需在 eBay.com 或 Banggood.com 等网站上搜索该术语)。这些是树莓派的全彩迷你监视器,甚至还有触摸屏选项可用。

这样我们就结束了关于使用树莓派和 Python 进行照明和显示的覆盖范围。到目前为止,您所学到的知识将使您能够使用和正确供电自己的简单 LED 照明项目,并利用各种 OLED 显示器,用于那些您希望向用户显示文本和图形信息的项目。

为了完成本章的练习,接下来,我们将简要回顾脉宽调制(PWM),并看看我们如何使用它来产生声音。

使用蜂鸣器和 PWM 发出声音

在本章的最后一节中,我们将演示如何使用 PWM 制作简单的声音和音乐。我们的示例程序将在蜂鸣器上演奏一个音阶,并且我们将使用一种名为**Ring Tone Text Transfer Language (RTTTL)**的音乐记谱格式,这是由诺基亚在智能手机时代之前开发用于创建手机铃声的。随着我们的学习,我们可以使用一个简单的 Python 库来解析 RTTTL 音乐记谱,并将其音符转换为 PWM 频率和持续时间,然后可以用来关联蜂鸣器以创建可听的旋律。

要使用 PWM 发出声音,我们需要一种形式的扬声器,我们将使用所谓的被动蜂鸣器。蜂鸣器有两种基本形式:

  • 主动蜂鸣器:这些蜂鸣器包含一个内部振荡器,可以产生单一的音调。您只需要给主动蜂鸣器施加直流电压,它就会发出声音。

  • 被动蜂鸣器:这些蜂鸣器不包含任何内部智能来使它们工作,因此振荡必须由控制设备完成。这样做的好处是我们可以根据需要设置和更改音调,并且我们可以使用 PWM 来实现这一点。

现在我们了解了如何使用蜂鸣器发出声音,让我们继续创建我们的发声电路。

建立 RTTTL 电路

在本节中,我们将建立一个驱动被动蜂鸣器的电路。这个电路如图 8.8所示,与我们在第七章中介绍的 MOSFET 电路非常相似,只是这次连接了一个蜂鸣器作为负载:

图 8.8 - 蜂鸣器驱动电路原理图

我们将通过将组件放入面包板来开始我们的电路构建:

图 8.9 - 蜂鸣器驱动电路(1/2)的一部分

以下步骤编号与图 8.9中编号的黑色圆圈相匹配:

  1. 将 MOSFET 放在面包板上,注意组件与引脚的方向。如果您需要帮助识别 MOSFET 的引脚,请参阅第七章中的图 7.7打开和关闭设备

  2. 将 100kΩ电阻(R2)放入面包板中。这个电阻的一端与 MOSFET 的栅极(G)腿共享同一排。

  3. 将 1kΩ电阻(R1)放入面包板中。这个电阻的一端也与 MOSFET 的栅极(G)腿共享同一排。

  4. 将二极管放入面包板中,带有阴极腿(带子朝向的一端)指向面包板的一端。

  5. 将蜂鸣器的正极线连接到与二极管阴极腿共享的同一排中。

  6. 将蜂鸣器的负极线连接到一个空闲的面包板行。

现在我们已经放置了组件,让我们将它们连接起来:

图 8.10 – 蜂鸣器驱动电路(第二部分)

以下步骤编号与图 8.10中编号的黑色圆圈相匹配:

  1. 将左侧电源轨的负极连接到 1kΩ电阻(R2)。

  2. 将 MOSFET 的源腿(S)连接到左侧电源轨的负极。

  3. 将左侧电源轨的负极连接到树莓派上的 GND 引脚。

  4. 将 100kΩ电阻(R1)的末端连接到树莓派的 GPIO 12/PWM0 上。提醒一下,GPIO 12 在其替代功能中是通道 PWM0,是一个硬件 PWM 引脚。

  5. 将 MOSFET 的漏极(D)连接到二极管的阳极腿。

  6. 将二极管的阳极腿连接到蜂鸣器的负极线。

  7. 将蜂鸣器的正极线/二极管的阴极腿连接到右侧电源轨的正极。

  8. 连接左侧和右侧电源轨的负极。

  9. 将电源供应的正极输出连接到右侧电源轨的正极。

  10. 将电源供应的负极输出连接到右侧电源轨的负极。

现在您已经完成了这个电路搭建,我们将继续运行我们的 Python 示例,这将制作一些音乐!

运行 RTTTL 音乐示例

运行chapter08/passive_buzzer_rtttl.py文件中的代码,您的蜂鸣器将播放一个简单的音阶。

执行此操作的代码非常简单。在以下代码的第(1)行中,我们使用rtttl模块将 RTTTL 音乐乐谱解析为由频率和持续时间定义的一系列音符。我们的乐谱存储在rtttl_score变量中:

from rtttl import parse_rtttl
rtttl_score = parse_rtttl("Scale:d=4,o=4,b=125:8a,8b,        # (1)
    8c#,8d,8e,8f#,8g#,8f#,8e,8d,8c#,8b,8a")

接下来,在第(2)行,我们循环遍历rtttl_score中解析的音符,并提取频率和持续时间:

    for note in rtttl_score['notes']:                        # (2)
        frequency = int(note['frequency'])   duration = note['duration'] # Milliseconds
        pi.hardware_PWM(BUZZER_GPIO, frequency, duty_cycle)  # (3)
        sleep(duration/1000)                                 # (4)

在第(3)行,我们使用 PWM 在蜂鸣器的 GPIO 引脚上设置频率,并在第(4)行保持音符的持续时间,然后继续到下一个音符。

在第(3)行,请注意我们正在使用 PiGPIO 的hardware_PWM(),并且BUZZER_GPIO 必须 是一个硬件兼容的 PWM 引脚。 PiGPIO 的硬件定时 PWM(可用于任何 GPIO 引脚)不适用于音乐创作,因为它受限于一系列离散的频率。如果您需要 PWM 技术的复习,请重新阅读第五章,将您的树莓派连接到物理世界

使用 RTTTL 制作音乐听起来非常电子化,并且是资源有限的微控制器中的一种流行技术。但是,请记住,对于我们的树莓派来说,我们有足够的资源和内置硬件来播放丰富的媒体,如 MP3。

尝试在网络上搜索RTTTL 歌曲,您会找到许多歌曲、复古电脑游戏和电视电影主题的乐谱。

如果您想通过 Python 探索播放和控制 MP3,您会发现网络上有许多资源、教程和示例。不幸的是,有许多方法可以实现这个任务(包括 Raspbian OS 的不同版本之间的更改),因此有时在可靠地设置和配置您的 Raspberry Pi 和 Raspbian OS 时可能会有点棘手。如果您选择这条路线,我的建议是首先在命令行上探索播放 MP3 和控制音频(即更改音量)。一旦您有了稳定可靠的设置,然后再探索基于 Python 的方法。

摘要

在本章中,我们学习了如何使用 PWM 来设置 RGB LED 的颜色,以及独立的单个 RGB LED 需要三个专用的 GPIO 引脚来工作——分别用于红色、绿色和蓝色。然后,我们探索了另一种类型的 RGB LED,即 APA102,它是一种 2 线 SPI 可控设备,可以串联在一起创建 LED 灯带。接下来,我们学习了如何使用 OLED 显示器,创建了一个示例应用程序,显示了树莓派的 CPU 温度。最后,我们通过解析 RTTTL 音乐乐谱,使用 PWM 和被动蜂鸣器制作声音。

在本章中学到的知识将使您能够为自己的项目添加可视和可审计的反馈。您还将能够相对容易地将您的学习扩展到其他类型的显示器,因为我们在本章中使用的 Luma 库能够与 APA102 LED 灯带和 SSD1306 OLED 设备以外的其他显示器类型和型号一起工作。

在下一章中,我们将研究用于测量环境条件(包括温度、湿度和光照)的组件和技术。

问题

随着我们的结束,这里有一些问题供您测试对本章材料的了解。您将在书的评估部分找到答案:

  1. 你的 APA102 LED 灯带设置为显示所有 LED 为白色,但实际上所有 LED 看起来都是红色的。可能是什么问题?

  2. APA102 对 SPI 有什么限制?

  3. 当您使用逻辑电平转换器时,您的 APA102 无法工作,但当您直接连接到树莓派的 MOSI 和 SCK 引脚时(因此绕过逻辑电平转换器),它似乎可以工作。问题的一些可能原因是什么?

  4. 使用 Luma OLED 库在 OLED 显示器上创建和显示图像的基本过程是什么?

  5. 什么是 RTTTL?

进一步阅读

APA102 是一个很好的选择,可以开始学习较低级别的数据协议和通信。在查看 APA102 数据协议的数据表后(请参见本章开头的技术要求下的链接),下一个逻辑步骤是查看一些较低级别的代码。 PiGPIO 的 APA102 示例是一个起点,但您会在 PyPi.org 上找到其他示例:

Luma 系列库提供了许多高级模块,用于将常见显示器与树莓派集成,超出了本章中涵盖的 APA102 和 SSD1306 OLED。此外,Luma 还包含大量的示例:

Luma 使用 PIL(Python Imaging Library)/Pillow 兼容的 API 来绘制和操作显示器。我们在我们的 OLED 示例中特别使用了ImageDraw。您可以在以下链接找到 PIL API 文档:

如果您想进一步探索 RTTTL 格式,可以从其维基百科网站开始:

第九章:测量温度、湿度和光照水平

在上一章中,我们探讨了使用 RGB LED 制作颜色的两种方法 - 使用普通的 RGB LED 和可寻址的 APA102 RGB LED 条。我们还学习了如何使用简单的 OLED 显示屏,以及如何使用 PWM 来使用无源蜂鸣器播放音乐。

在本章中,我们将研究一些常见的组件和电路,用于收集环境数据,包括温度、湿度、光线暗或亮以及如何检测湿度。

学习的电路和代码示例将对构建和实验自己的环境监测项目非常有用。这些电路可以被视为测量环境条件的输入或传感器电路。例如,您可以结合第七章中的电路思想和示例,打开和关闭东西,当土壤干燥时打开水泵浇水,或者在天黑时打开低电压 LED 灯。事实上,在第十三章中,我们有一个可视化平台的示例,IoT 可视化和自动化平台,我们将使用本章中的一个电路来捕获、记录和可视化历史温度和湿度数据!

此外,在本章中,我们将看到模拟电子学和相关概念的实际示例,例如电压分压器,在第六章中学到的。

以下是本章的内容:

  • 测量温度和湿度

  • 检测光线

  • 检测湿度

技术要求

要执行本章的练习,您需要以下内容:

  • 树莓派 4 型 B

  • Raspbian OS Buster(带桌面和推荐软件)

  • 最低 Python 版本 3.5

这些要求是本书中代码示例的基础。可以合理地期望代码示例在树莓派 3 型 B 或不同版本的 Raspbian OS 上无需修改即可工作,只要您的 Python 版本是 3.5 或更高。

您将在github.com/PacktPublishing/Practical-Python-Programming-for-IoT的 GitHub 存储库的chapter09文件夹中找到本章的源代码。

您需要在终端中执行以下命令来设置虚拟环境并安装本章代码所需的 Python 库:

$ cd chapter09              # Change into this chapter's folder
$ python3 -m venv venv      # Create Python Virtual Environment
$ source venv/bin/activate  # Activate Python Virtual Environment
(venv) $ pip install pip --upgrade        # Upgrade pip
(venv) $ pip install -r requirements.txt  # Install dependent packages

以下依赖项从requirements.txt中安装:

本章练习所需的电子元件如下:

  • 1 x DHT11(较低精度)或 DHT22(较高精度)温度和湿度传感器

  • 1 x LDR(光敏电阻,也称为光电池或光敏电阻)

  • 电阻:

  • 1 x 200Ω 电阻

  • 1 x 10kΩ 电阻

  • 1 x 1kΩ 电阻

  • 1 x 100kΩ 电阻

  • 1 x 红色 LED

  • 1 x ADS1115 模数转换器模块

  • 外部电源 - 至少需要一个 3.3V/5V 面包板可安装电源供应器。

测量温度和湿度

测量温度和相关环境属性是一个常见的任务,有许多不同类型的传感器可用于测量这些属性,包括热敏电阻(温度相关电阻)、通过 SPI 和 I2C 连接的复杂分支模块,以及 DHT11 或 DHT22 传感器等传感器种类,我们将在示例中使用。

当涉及准确性、响应时间(从中我们可以快速获取数据的速度)和成本时,所有传感器都有其相对的优势和劣势。

正如图 9.1所示,DHT 传感器价格便宜,耐用且易于使用:

图 9.1 – DHT11 和 DHT22 温度和湿度传感器

DHT11 是一种非常常见的低成本传感器。DHT22 是它的高精度表亲。两者都是引脚兼容的,并且适用于我们的示例。如前图所示,这些传感器的引脚分配如下:

  • Vcc:3 到 5 伏电源来源

  • Data:连接到 GPIO 引脚的数据引脚

  • NC:未连接,表示未使用此引脚

  • GND:连接到地面

以下是 DHT11 和 DHT22 之间的核心相似之处和不同之处:

DHT 11 DHT 22
工作电压 3 到 5 伏 3 到 5 伏
工作电流 µA(微安) µA(微安)
温度范围 0 到 50 摄氏度 - 40 到 125 摄氏度
温度精度 ±2% ±0.5%
湿度范围 20 - 80% 0 - 100%
湿度精度 ±5% ±2%到 5%
最大采样率 更快 – 每秒一次(1Hz) 更慢 – 每 2 秒一次(0.5Hz)

如前所述,DHT11 和 DHT22 传感器的引脚兼容。它们只在测量精度和范围上有所不同。任一传感器都适用于我们即将创建的用于测量温度和湿度的电路。

创建 DHT11/DHT22 电路

我们将从在我们的面包板上创建的图 9.2中的电路开始:

图 9.2 – DHT 传感器原理图

以下是我们即将构建的电路的面包板布局:

图 9.3 – DHT 传感器电路

以下是要遵循的步骤,与图 9.3中编号的黑色圆圈相匹配:

  1. 将您的 DHT11 或 DHT22 传感器放入面包板中。

  2. 将 10kΩ电阻(R1)放入面包板中。电阻的一端与 DHT 传感器的 DATA 引脚共用一行。我们将在完成电路构建后讨论这个电阻以及为什么它被标记为可选的图 9.2

  3. 将树莓派上的 3.3 伏引脚连接到电源轨的正极。

  4. 将 10kΩ电阻(R1)连接到正电源轨。

  5. 将 DHT Vcc 引脚连接到正电源轨。

  6. 将树莓派上的 GND 引脚连接到负电源轨。

  7. 将 DHT 传感器的 GND 引脚连接到负电源轨。

  8. 最后,将 DHT 传感器的 DATA 引脚连接到树莓派上的 GPIO 21。

现在我们的 DHT 传感器电路已经完成。

在我们的电路中,Vcc 连接到 3.3 伏,这使得 DHT 数据引脚以这个电压工作。DHT11 和 DHT22 额定为 5 伏;但是,如果您将 Vcc 连接到 5 伏,数据引脚就会成为一个 5 伏逻辑引脚,这对于树莓派的 3.3 伏 GPIO 引脚来说是不安全的。

10kΩ上拉电阻是可选的,因为我们使用的 DHT 软件库已经默认启用了树莓派的内部上拉电阻。我在电路原理图中包括了上拉电阻,因为它包含在许多 DHT11/DHT22 数据表的电路示例中。如果您需要关于上拉电阻的复习,请重新阅读第六章,软件工程师的电子学 101

在我们的电路和 DHT11/DHT22 中,标有NC的引脚表示未连接。 NC 是一个常用的缩写,用于表示传感器、IC 或元件的引脚或端子没有内部连接到任何东西。但是,当我们处理开关(包括继电器)时,标有 NC 的元件引脚或端子表示通常关闭连接路径...因此,始终根据您正在查看的元件的上下文来解释 NC。

一旦您创建了电路,我们就准备好运行和探索代码来测量温度和湿度。

运行和探索 DHT11/DHT22 代码

运行chapter09/dht_measure.py文件中的代码,测量的温度和湿度将被打印到您的终端,类似于以下内容:

(venv) python DHT_Measure.py
{'temp_c': 21, 'temp_f': 69.8, 'humidity': 31, 'valid': True}

在这里,我们有以下内容:

  • temp_c是摄氏度温度。

  • temp_f是华氏度温度。

  • humidity是相对湿度百分比。

  • valid表示通过内部传感器校验和检查是否认为读数有效。读数中value == False的必须被放弃。

源文件中的代码简洁,并在此处完全复制。

在第 1 行,我们导入 DHT 传感器库,并在第 2 行实例化它。更新该行以匹配您使用的 DHT11 或 DHT22 传感器:

from pigpio_dht import DHT11, DHT22   # (1)

SENSOR_GPIO = 21 
sensor = DHT11(SENSOR_GPIO)           # (2)
#sensor = DHT22(SENSOR_GPIO)

result = sensor.read(retries=2)       # (3)
print(result)

result = sensor.sample(samples=5)     # (4)
print(result)

在第 3 和第 4 行,我们使用pigpio-dht库从传感器请求温度和湿度测量。对read()的调用将查询传感器的测量,并在测量结果为valid == False时重试retries次。另一种测量方法是sample()方法,它将获取温度和湿度的多个单独样本并返回归一化的测量值。

sample()的优势,特别是对于精度较低的 DHT11 传感器,是温度和湿度读数更一致,因为异常值读数(随机尖峰)被移除;然而,它显著增加了读取测量所需的时间-请参考本节开头表格中的最大采样率行。

例如,对于最大采样率为 1 秒的 DHT11,对于 5 个样本,sample(samples=5)调用将花费大约1 秒 x 5 个样本 = 5 秒才能返回,而具有 2 秒采样率的 DHT22 将花费约 10 秒。

DHT11 和 DHT22 是引脚兼容的;然而,由于每个传感器编码其数据的方式不同,它们在软件上不兼容。例如,使用 DHT11 库的 DHT22 传感器将生成不准确的结果(这将是非常明显的-例如,说您的房间温度为 650+摄氏度!)

多么简单!DHT 系列是流行的低成本传感器,可以测量温度和湿度。对于那些需要进行更快速读数的情况,或者需要将传感器安装在恶劣环境中,例如水中或户外,直接暴露在外部环境中,您肯定能找到适合您需求的传感器。

以下是其他温度(和类似环境)传感器连接到树莓派的快速概述:

  • 热敏电阻是温度敏感电阻,非常小,非常适合狭小空间,并且您可以获得密封包装以供室外和液体使用。您可以将它们与电压分压电路一起使用(类似于我们将在下一节中介绍的光敏电阻LDR))。

  • 有许多种类的 I2C 和 SPI 传感器可用于快速查询,可能还具有其他额外的传感器,例如气压传感器。这些模块通常较大,可能最好不要直接暴露在外部环境中。

  • 1-wire温度传感器也是紧凑且易于密封的,其优势在于可以有长电线(100 米以上)。

通过这一部分,我们结束了关于测量温度和湿度的介绍。许多环境监测项目需要您测量温度和湿度,使用树莓派和 DHT11 或 DHT22 是一种简单且具有成本效益的方法。我们将在第十三章中再次讨论我们的 DHT11/22 电路,IoT 可视化和自动化平台,在那里我们将集成该传感器到 IoT 平台以收集和监测温度和湿度。

现在我们已经探讨了温度传感器,让我们学习如何检测光线。

检测光线

轻松实现检测光线的存在或不存在,使用一种特殊类型的电阻器,称为LDR。 LDR 是一种低成本的光传感器,我们可以在许多应用中找到它们,从光控开关和灯到在黑暗时调暗闹钟显示屏的电路的一部分,再到现金箱和保险柜上的警报电路。

您可能也会发现 LDR 被称为光敏电阻或光电池。

以下图显示了典型的 LDR 组件,以及几种 LDR 原理图符号。如果您仔细观察这些符号,您会注意到它们是带有向内箭头的电阻符号。您可以将这些箭头看作代表光照在电阻上:

图 9.4 - 一个物理 LDR 组件和各种原理图符号

LDR 随其检测到的相对光照变化其电阻。如果您将万用表的端子置于 LDR 上的欧姆模式下,您会发现(大约几秒钟后)以下情况:

  • 当 LDR 处于黑暗中(例如,如果您把它遮住),其电阻通常会测量许多兆欧姆。

  • 在一个正常照明的房间(例如,桌子上的吊灯亮着),LDR 的电阻将以千欧姆计。

  • 当 LDR 处于强光(直射阳光或照射手电筒),其电阻将测量几百欧姆或更低。

这为我们提供了明显的区域,使得我们可以确定光的存在或不存在。通过校准和微调,我们可以轻松地确定在黑暗和光之间的一个点,我们可以用它来触发一个事件。例如,您可以使用我们将在下一步创建的 LDR 电路来以编程方式控制我们在第七章中创建的开关电路。

LDR 只能很好地测量相对光照水平 - 光的存在或不存在。如果您想要绝对的测量,比如光照水平,甚至是检测颜色,那么有一系列的 I2C 或 SPI 断开模块形式的 IC 可以实现这一点。

利用这种基本理解,我们将构建我们的 LDR 电路来检测光线。

创建 LDR 光检测电路

正如讨论的那样,LDR 随其检测到的相对光照变化其电阻。为了在我们的树莓派上检测变化的电阻,我们需要采取一些在以前章节中讨论过的步骤:

  • 我们需要将变化的电阻转换为变化的电压,因为我们的树莓派 GPIO 引脚工作在电压上,而不是电阻。这是欧姆定律和电压分压电路的应用,我们在第六章中学习过,软件工程师的电子学 101

  • 我们的树莓派 GPIO 引脚只能读取数字信号 - 例如高(~3.3 伏)或低(~0 伏)信号。为了测量变化的电压,我们可以连接一个模数转换器ADC),例如 ADS1115。我们在第五章中介绍了 ADS1115 和相应的 Python 代码,将您的树莓派连接到物理世界

我们将在您的面包板上创建图 9.5中所示的电路。当检测到一定程度的黑暗时,这个电路和附带的代码将点亮 LED:

图 9.5 - 带有 ADS1115 ADC 原理图的 LDR 电路

我们将分两部分构建电路。对于第一部分,我们将把组件放在面包板上,如图所示:

图 9.6 - 带有 ADS1115 ADC 电路的 LDR 电路(第一部分,共 2 部分)

以下是要遵循的步骤,这些步骤与图 9.6中编号的黑色圆圈相匹配:

  1. 将 LDR 放在面包板上。

  2. 将 10kΩ电阻(R1)放在面包板上。这个电阻的一端与 LDR 的一端共用同一行。

  3. 将 ADS1115 ADC 放在面包板上。

  4. 将一个 200kΩ电阻(R2)放在面包板上。

  5. 将 LED 放在面包板上,特别注意将 LED 的阴极引脚连接到与 200kΩ电阻的一根引脚共享的同一行。

现在我们已经放置了我们的组件,我们将把它们连接起来:

图 9.7 - 带 ADS1115 ADC 电路的 LDR 电路(第二部分)

以下是要遵循的步骤;这次它们与图 9.7中编号的黑色圆圈相匹配:

  1. 将正电源轨道连接到 LDR。

  2. 将树莓派的 3.3 伏引脚连接到电源轨道的正电源轨道。

  3. 将树莓派的 GND 引脚连接到负电源轨道的负端。

  4. 将负电源轨道连接到 10kΩ电阻(R1)。

  5. 将 ADS1115 的 Vdd 端连接到正电源轨道。

  6. 将 ADS1115 的 GND 端连接到负电源轨道。

  7. 将 LDR 和 10kΩ电阻(R1)的交接处连接到 ADS1115 的 A0 端口(你能看到 LDR 和电阻如何创建电压分压器,变化的电压输出现在连接到 A0 吗?)。

  8. 将树莓派的 SDA 引脚连接到 ADS1115 的 SDA 端子。

  9. 将树莓派的 SCL 引脚连接到 ADS1115 的 SCL 端子。

  10. 将负电源轨道连接到 200kΩ电阻。

  11. 将 LED 的阳极引脚连接到树莓派的 GPIO 21 引脚。

希望你能看到由 LDR 和 10kΩ电阻 R1 形成的电压分压器。我们将在本章后面的LDR 配置摘要部分介绍 10kΩ电阻背后的原因。

由于 LDR 检测到的光的变化,它的电阻也会变化。这样做的效果是改变 R1(固定电阻)和 LDR 的电阻(变化电阻)的相对比例,从而改变在 LDR 和 R1 交叉处测量到的电压(这就是我们的 ADS1115 的 A(模拟输入)连接的地方,用于测量这个变化的电压)。

不要将 LED 放得太靠近 LDR。当 LED 发光时,LDR 可以检测到 LED 发出的光,这可能会干扰代码中的 LDR 读数。

现在您已经创建了 LDR 电路,我们将校准并运行我们的示例代码。

运行 LDR 示例代码

我们将要运行两个程序:

  • chapter09/ldr_ads1115_calibrate.py,这将帮助我们校准 LDR 读数

  • chapter09/ldr_ads1115.py,它监测光照水平,并在光线低于可配置水平时打开 LED

首先,我们应该检查 ADS1115 是否连接正确,并且树莓派能够识别它。在终端中运行i2cdetect命令。如果您的输出不包括一个数字(例如48),请验证您的接线是否正确。

$ i2cdetect -y 1
# ... truncated ...
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: -- -- -- -- -- -- -- -- 48 -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
# ... truncated ...

我们首先介绍了 ADS1115 模数转换器和第五章中的i2cdetect实用程序,将树莓派连接到物理世界

让我们从校准程序开始运行示例:

  1. 运行chapter09/ldr_ads1115_calibrate.py文件中的代码,并按照出现在终端上的说明进行操作,如下所示:

  2. “将 LDR 放在光线下并按 Enter 键”:在这个练习中使用环境光,并注意不要在 LDR 上投下阴影。在构建应用程序时,您将希望使用对您的目的有意义的光源,例如直射阳光、室内光或者用强光照射 LDR。

  3. “将 LDR 放在黑暗中并按 Enter 键”:我建议完全用黑布或杯子覆盖 LDR。用手指并不总是理想的,因为敏感的 LDR 可能仍然能够通过手指检测到一定程度的光:

(venv) python ldr_ads1115_calibrate.py Place LDR in the light and press Enter
Please wait...

Place LDR in dark and press Enter
Please wait...

File ldr_calibration_config.py created with:
# This file was automatically created by ldr_ads1115_calibrate.py
# Number of samples: 100
MIN_VOLTS = 0.6313
MAX_VOLTS = 3.2356 

校准程序从 ADS1115 在黑暗和光线条件下获取一定数量的样本(默认为100),并计算平均读数。接下来,程序将结果(也显示在终端中)写入ldr_calibration_config.py文件。这是我们示例的 Python 源文件,导入到我们实际的 LDR 和 LED 示例中,我们将在下一步中看到。

  1. 运行chapter09/ldr_ads1115.py文件中的程序,并观察终端上显示的输出,其中显示了 ADS1115 读取的电压:
LDR Reading volts=0.502, trigger at 0.9061 +/- 0.25, triggered=False

希望输出应该是triggered = False,LED 应该是关闭的。如果不是这种情况,请尝试重复步骤 1中的校准过程,或者继续阅读,您将发现如何在代码中调整触发点。

  1. 逐渐将手靠近 LDR,限制到达它的光线量。当您移动手时,您会注意到终端上的voltage读数发生变化,并且在某个电压水平时,将达到触发点,LED 将点亮:
LDR Reading volts=1.116, trigger at 0.9061 +/- 0.25, triggered=False
LDR Reading volts=1.569, trigger at 0.9061 +/- 0.25, triggered=True

您所见到的是电压分压器的功能,随着 LDR 的电阻随光线变化而变化。然后,ADS1115 读取这个电压。

您可能已经注意到,所产生的电压不是我们在第五章中使用 ADS1115 和电位计时产生的 0 伏特到 3.3 伏特的完整范围。我们受限范围是我们固定电阻(R1)和变阻(LDR)电路的副作用和限制,它无法变化到达0 或3.3 伏特所需的极端电阻。您将在电压分压器电路中遇到这种限制,因为它们将按设计包括固定电阻值。相比之下,我们的电位计是两个可变电阻器,创建电压分压器,我们可以有效地将分压器的一侧归零(接近 0Ω),这取决于我们将电位计的拨盘转向哪个方向,从而使我们能够接近 0 伏特和 3.3 伏特。

既然我们已经看到这段代码运行,让我们看看它是如何工作的。

LDR 代码演练

chapter09/ldr_ads1115_calibrate.pychapter09/ldr_ads1115_calibrate.py中的大部分代码是设置和配置 ADS1115 以及使用 PiGPIO 设置 LED 的样板代码。我们不会在这里重复常见的代码。如果您需要复习与 ADS1115 相关的代码,请查看第五章中的练习,“将您的树莓派连接到物理世界”。

让我们看看使我们的 LDR 工作的 Python 代码。

在第 1 行,我们看到我们正在导入之前使用校准程序创建的ldr_calibration_config.py文件。

接下来,在第 2 行,我们将校准值分配给LIGHT_VOLTS(LDR 在光线下被 ADS1115 检测到的电压)和DARK_VOLTS(您遮住 LDR 时检测到的电压)变量:

import ldr_calibration_config as calibration                   # (1)

# ... truncated ...

LIGHT_VOLTS = calibration.MAX_VOLTS                            # (2)
DARK_VOLTS = calibration.MIN_VOLTS

TRIGGER_VOLTS = LIGHT_VOLTS - ((LIGHT_VOLTS - DARK_VOLTS) / 2) # (3)
TRIGGER_BUFFER = 0.25                                          # (4)

在第 3 行,我们创建一个触发点。这是我们以后将在代码中使用的电压点来开启和关闭 LED。

您可以调整和实验TRIGGER_VOLTS的公式或值,以改变导致代码触发的照明条件。

第 4 行的TRIGGER_BUFFER变量用于在我们的触发器中创建缓冲区或滞后,电子术语中更为人所知的是滞后。这个值创建了一个小的窗口范围,检测到的电压可以在不引起触发或取消触发事件的情况下变化。如果没有这种滞后,触发器(和 LED)将在检测到的电压围绕TRIGGER_VOLTS触发电压振荡时快速打开和关闭。

要亲身体验这种效果,请将TRIGGER_BUFFER = 0,您会发现当您将手移动到 LDR 上方时,LED 对开和关非常敏感,并且在某一点甚至可能出现闪烁。当您增加TRIGGER_BUFFER的值时,您会注意到需要移动手才能使 LED 开关变得更加明显。

接下来,在第 5 行,我们来到了确定触发点是否已达到的代码。update_trigger()函数将 ADS1115 检测到的电压与调整为TRIGGER_BUFFERTRIGGER_VOLTS值进行比较,并在触发点被触发时更新triggered全局变量:

   triggered = False # (5)

   def update_trigger(volts):
       global triggered

       if triggered and volts > TRIGGER_VOLTS + TRIGGER_BUFFER:
           triggered = False
       elif not triggered and volts < TRIGGER_VOLTS - TRIGGER_BUFFER:
           triggered = True

在源文件的末尾附近,我们在第 6 行有一个while循环。我们正在读取 ADS1115 检测到的电压,更新全局triggered变量,然后将结果打印到终端:


trigger_text = "{:0.4f} +/- {}".format(TRIGGER_VOLTS, TRIGGER_BUFFER) 

  try:
      while True:                                                  # (6)
          volts = analog_channel.voltage

          update_trigger(volts)

          output = "LDR Reading volts={:>5.3f}, trigger at {}, triggered={}"
                   .format(volts, trigger_text, triggered)
          print(output)

          pi.write(LED_GPIO, triggered)                           # (7)
          sleep(0.05)

最后,在第 7 行,根据triggered的值切换 LED 的开关状态。

现在我们已经看到了如何使用我们的 LDR 电路和 Python 代码检测光线,我想简要介绍一下选择 LDR 电路的串联电阻的方法。

LDR 配置摘要

在使用 LDR 电路和代码时,您可能已经意识到有一些可调参数会影响电路和代码的工作方式,您是否想知道为什么我们使用了 10kΩ电阻?

没有两个 LDR 会给出相同的电阻-光线测量值,它们的电阻-光线范围也不是线性的。这意味着您的 LDR 以及您计划在其中使用它的照明条件可能会影响适当的固定电阻值。

以下是选择适当固定电阻的大致指南:

  • 如果您希望 LDR 在较暗的条件下更敏感,请使用更高值的电阻(例如,尝试 100kΩ)。

  • 如果您希望您的 LDR 在更明亮的条件下更敏感,请使用更低值的电阻(例如,尝试 1kΩ)。

请记住,这些只是建议,所以请随时根据自己的需求尝试不同的电阻。此外,每当更改固定电阻的值时,请重新运行校准代码。

还有一个名为 Axel Benz 的公式,可用于计算诸如 LDR 之类的模拟元件的参考电阻值。该公式表达如下:

公式中的参数如下:

  • R[ref]是固定电阻 R1 的值。

  • R[max]是 LDR 的最大电阻(在黑暗中)。典型值可能为 10Ω。

  • R[min]是 LDR 的最小电阻(在强光下)。典型值可能为 10MΩ。

因此,如果我们使用典型值,我们得到了用于 R1 的 10kΩ值:

使用万用表测量 LDR 的极端值,并查看您计算出的值。如果您的测量结果与典型的 10kΩ相差甚远,不要感到惊讶。考虑到我们使用的 LDR 欧姆范围约为10Ω至10,000,000Ω,差异可能仍然只是百分之一的一小部分!

我们之前在代码中看到两个变量影响我们的代码触发:

  • 更改TRIGGER_VOLTS的值以更改代码触发的点 - 例如,打开或关闭 LED。

  • 更改TRIGGER_BUFFER的值以改变触发器对光线变化的敏感度。

最后,请记住,LDR 以对数方式检测光线,而不是线性方式 - 例如,当您逐渐将手或物体放在 LDR 上以限制光线时,LDR 报告的电压不一定会与您限制的光线量成比例地变化。这就是为什么我们需要更改固定电阻值,如果我们希望 LDR 在更暗或更亮的条件下更敏感。

您可以尝试用可变电阻替换固定电阻 R1(例如,用 20kΩ可变电阻替换固定的 10kΩ,设置为 10kΩ。我们选择 20kΩ是因为我们可以将其调整到 10kΩ以上和以下。10kΩ可变电阻只能让我们降低电阻)。在代码校准为 10kΩ并定义了代码中的触发点后,您可以通过调整可变电阻来微调触发点。

这结束了我们对 LDR 的讨论。我们已经看到了如何与 ADS1115 ADC 一起构建简单的 LDR 电路,以及如何使用 Python 检测光线。您可以将这个简单的电路和配套的代码用于任何需要检测光线或黑暗的项目,例如光敏开关。

接下来,我们将学习如何检测湿度。

检测湿度

猜猜看……我们已经完成了检测湿度的繁重工作!这只是 LDR 电路和代码的另一个应用,只是我们用探针替换了 LDR。

对于这个练习,您可以使用两根带两端剥离的导线制作一组探针,并将它们安装在 LDR 的位置,如图 9.8所示。这与我们在上一节中构建的电路相同,并在图 9.7中显示,只是这一次,我们用两根导线替换了 LDR。现在让我们做出这个小改变:

图 9.8 - 湿度检测电路

以下是要遵循的步骤,这些步骤与图 9.8中编号的黑色圆圈相匹配:

  1. 从面包板上取下 LDR。

  2. 将一根带两端剥离的导线放入面包板的一行,该行先前连接到 LDR 的一条腿(在插图中,这根新导线连接回树莓派上的 3.3 伏特)。

  3. 将另一根带两端剥离的导线放入面包板的一行,该行先前连接了 LDR 的另一条腿(在插图中,这根新导线连接到与 10kΩ电阻(R1)共享的行)。

这个小改变 - 用裸导线替换 LDR - 将我们的电路变成了一个基本的湿度检测电路。让我们试试这个电路!

chapter09文件夹中,您会找到两个文件,名为moisture_calibrate.pymoisture_ads1115.py。这些文件与我们在上一节中使用的 LDR 文件集几乎相同,只是我已经将Light/Dark的措辞和变量名称更改为Wet/Dry。各个文件中的核心区别由注释标出。

鉴于相似性,我们不会详细介绍这些源文件和湿度电路;但是,供参考,以下是要遵循的步骤:

  1. 确保探针是干燥的。

  2. 运行moisture_calibrate.py并按照说明执行电压校准。

  3. 运行moisture_ads1115.py

  4. 检查终端输出是否指示trigger=False(代码在湿润条件下触发)。

  5. 将探针放入一杯水中(是的,这样做是安全的),观察终端上的电压读数的变化(如果探针意外短路也没关系,不会造成任何损坏)。

  6. 将探针浸入水中,检查终端输出是否显示trigger=True(探针湿润状态)。

  7. 如果触发器仍然为True,则需要在代码中调整TRIGGER_VOLTS的值。

您还可以将探针放入干燥的土壤中,并观察电压读数。慢慢地浇湿土壤,电压读数应该会改变。现在我们有了一个程序的基础,可以告诉您何时需要给植物浇水!

那么,为什么会起作用呢?很简单 - 水是电的导体,在我们的两个探针之间的行为就像一个电阻。

世界各地和不同来源的水 - 例如自来水与瓶装水 - 可能导电性不同。这意味着如果您的电路对 10kΩ电阻的反应不佳,您可能需要调整 R1 电阻的值。此外,您还可以尝试调整探针导线之间的距离和它们的大小。

我们将通过比较我们刚刚创建的内容与您可以购买的现成湿度检测器来结束我们对湿度检测的讨论。

比较检测选项

我们简单的电路和电线探针与您可以在 eBay 等零售网站上找到的水/湿度检测模块相比如何?这些产品通常包含某种探针,以及一个小型电子模块。这里显示了其中一个模块的图片,以及一些探针:

图 9.9 - 湿度检测模块和探针

这三个探针每个都有两个端子,只是电路板上的裸露铜线,类似于我们在图 9.8 中看到的裸露电线。一个关键的区别是这些探针暴露了更大的表面积,因此更敏感。此外,它们也可能比两根剥离的电线更不容易腐蚀(至少在短期到中期内)。

您可以直接将这些探针连接到我们在图 9.8 中显示的电路中的裸露电线,以扩展和增强电路的检测能力!

让我们讨论一下电子模块(在右侧的图 9.9 中放大并标记)。

除了 Vcc/Vin 和 GND 端子外,这些模块通常(不总是,但通常)还有两个输出端子或引脚,如下所示:

  • 模拟输出(在我们的示例中,这被标记为A

  • 一个数字输出(标记为S

请注意,我不会提供如何将先前显示的模块连接到您的树莓派的说明,而是会保持讨论的一般性。这些模块有许多变种,虽然它们的操作类似,但它们的接线方式可能有所不同。在本书的这个阶段,如果您对模拟与数字的基本原理、电压分压器和 ADC 感到满意,您就有了理解并对如何将这些模块接口到树莓派等设备做出明智决定的所有必要知识。一个很好的起点将是您模块的数据表或购买地点提供的任何信息。

模拟输出是一个通过到探针的通路。您可以直接将其连接到电压分压电路中,并使用诸如 ADS1115 之类的 ADC 测量可变电压 - 这正是我们在图 9.8 中创建的确切情景。如果您使用模拟通过,您将绕过模块上的所有其他电路(这就是为什么您可以直接将探针连接到我们的示例电路中)。

数字输出是使用模块电路的部分。典型的模块电路至少包括一个称为电压比较器的集成电路,一个固定电阻和一个可变电阻,这是一个触发点调节。固定电阻与探针一起创建一个电压分压器。电压比较器负责监视电压分压器上的电压,并在由调节触发的点上触发数字输出(例如,从“低”到“高”的转换)。图 9.9 中可以看到一个调节可变电阻的示例。

如果这种电压比较和触发听起来有点熟悉,那么您是正确的。这个模块及其电压比较器和可配置触发点原则上是我们创建的 LDR 和湿度电路以及 Python 代码的纯电子版本。是的,您可以在这些模块中使用 LDR 而不是探针!

因此,总之,什么更好 - 使用 ADS1115 和电压分压器类型的电路,如图 9.8 中所示,还是使用图 9.9 中所示的模块?没有一个最佳答案;然而,以下几点将帮助您做出自己的决定:

  • 使用类似图 9.8中的电路是一种模拟方法。传感器检测到的原始电压直接传递给您的 Raspberry Pi。这种方法的一个简单优点是您可以完全控制代码中的触发点。例如,您可以远程调整触发点从网页。这种方法的缺点是您需要一个涉及 ADS1115 和电压分压器的更复杂的电路。

  • 使用类似图 9.9中所示的模块作为数字方法,可以促进更简单的接口电路连接到您的 Raspberry Pi,只要模块的数字输出为 3.3 伏特。但缺点是您必须可以物理访问模块和调整电位器来更改触发点。

总结

在本章中,我们学习了如何使用常见的 DHT11 和/或 DHT22 传感器测量温度和湿度。我们还研究了如何使用 LDR 来检测光线,这使我们能够更详细地探索电压分压电路和 ADC。最后,我们改装了我们的 LDR 电路,以便我们可以检测湿度。

本章涵盖的示例电路和代码提供了使用现成传感器和简单电路测量环境条件的实际示例。您对这些传感器和电路的理解现在意味着您可以为自己的环境监测项目调整这些示例,包括将它们作为输入触发器与 Python 一起用于控制其他电路。

我们还看到了电压分压电路的新实际应用,以及它们在模拟电路中如何将可变电阻转换为可变电压,用于 ADC。这些示例和您对它们的理解代表了您可以适应并用于其他基于模拟的传感器的重要技能。

在下一章中,我们将学习如何更深入地控制直流电机,并学习如何控制舵机。

问题

在我们结束时,这里有一些问题供您测试本章材料的知识。您将在书的评估部分找到答案:

  1. 您能列举 DHT11 和 DHT22 温湿度传感器之间的两个区别吗?

  2. 为什么在我们的 DHT11/22 电路中外部 10kΩ上拉电阻是可选的?

  3. 描述 LDR 用于测量光线的基本电子原理。

  4. 您如何使 LDR 对特定光照条件更或者更不敏感?

  5. 您已经创建了一个 LDR 电路并校准了 Python 代码。现在,您更改了 LDR,并发现电压读数和代码中的触发点行为略有不同。为什么?

  6. 为什么将两根导线放入水中,配合电压分压器和 ADS1115 电路,可以作为基本的湿度检测器?

第十章:使用舵机、电机和步进电机进行运动

在上一章中,我们介绍了如何测量温度、湿度、光线和湿度。在本章中,我们将把注意力转向控制电机和舵机,这些是用于创建物理运动和动作的常见设备。本章中您将学习的核心概念、电路和代码将为您打开使用树莓派进行物理自动化和机器人技术的大门。

我们将学习如何使用脉冲宽度调制PWM)来设置舵机的角度,以及如何使用 H 桥集成电路来控制直流电机的方向和速度。我们将研究步进电机以及如何控制它们进行精确的运动。

本章我们将涵盖以下内容:

  • 使用 PWM 来旋转舵机

  • 使用 H 桥集成电路控制电机

  • 步进电机控制简介

技术要求

要执行本章的练习,您需要以下物品:

  • 树莓派 4 型 B

  • Raspbian OS Buster(带桌面和推荐软件)

  • 最低 Python 版本 3.5

这些要求是本书中代码示例的基础。可以合理地期望,只要您的 Python 版本为 3.5 或更高,代码示例应该可以在树莓派 3 型 B 或不同版本的 Raspbian OS 上无需修改即可运行。

你会在 GitHub 存储库的chapter10文件夹中找到本章的源代码,该存储库位于github.com/PacktPublishing/Practical-Python-Programming-for-IoT

您需要在终端中执行以下命令来设置虚拟环境并安装本章代码所需的 Python 库:

$ cd chapter10              # Change into this chapter's folder
$ python3 -m venv venv      # Create Python Virtual Environment
$ source venv/bin/activate  # Activate Python Virtual Environment
(venv) $ pip install pip --upgrade        # Upgrade pip
(venv) $ pip install -r requirements.txt  # Install dependent packages

以下依赖项已从requirements.txt中安装:

本章练习所需的电子元件如下:

让我们开始学习如何在树莓派、Python 和 PiGPIO 中使用舵机。

使用 PWM 来旋转舵机

常见的舵机或舵机是内部齿轮电机,允许您将其轴精确旋转到 180 度弧度内的特定角度。它们是工业机器人和玩具的核心组件,我们都熟悉玩具中的舵机,如遥控汽车、飞机和无人机中的舵机。

图 10.1中显示了一个全尺寸的业余风格舵机、一个微型舵机和一组排针,这些对于帮助将舵机连接到面包板非常有用,我们将在本节后面构建电路时需要用到:

图 10.1 - 舵机

舵机的一个很好的特性是它们基本上是一种即插即用的设备——在我们将它们连接到电源后,我们只需要发送一个编码了我们想要舵机旋转到的角度的 PWM 信号,然后就完成了。没有集成电路、没有晶体管,也没有任何其他外部电路。更好的是,舵机控制是如此普遍,以至于许多 GPIO 库——包括 PiGPIO——都包括了方便的控制方法。

让我们通过连接一个舵机到我们的树莓派来开始我们的舵机探索。

连接舵机到你的树莓派

我们舵机示例的第一个任务是将其连接到电源和我们的树莓派。显示这种布线的原理图如下:

图 10.2 – 舵机布线原理图

让我们开始使用面包板布线我们的舵机,如下所示:

图 10.3 – 舵机面包板布局

在我们逐步介绍布线程序之前,我首先想简要讨论一下舵机出线的颜色。虽然舵机线的颜色有些是标准的,但在不同的制造商和舵机之间可能会有所不同。在连接你的舵机时,请使用以下提示在步骤 456。如果你的舵机有我没有列在下面列表中的颜色线,你需要查阅你舵机的数据表。

常见的舵机线颜色如下:

  • 棕色或黑色的线连接到 GND

  • 红色线连接到+5 伏

  • 橙色、黄色、白色或蓝色的线是信号/PWM 输入线,连接到 GPIO 引脚

以下是创建面包板构建的步骤。步骤编号与图 10.3中的黑色圆圈中的数字相匹配:

  1. 将左侧和右侧的负电源轨道连接在一起。

  2. 将树莓派上的 GND 引脚连接到左侧的负电源轨道。

  3. 将舵机连接到面包板。如前所述,并如图 10.1所示,你需要一组排针(或者,作为替代,公对公跳线)来将你的舵机连接到你的面包板。

  4. 将舵机的黑色线(负/GND)连接到右侧电源轨道的负极。

  5. 将舵机的红色线(5 伏电源)连接到右侧电源轨道的正极。

  6. 将舵机的信号线连接到树莓派上的 GPIO 21。

  7. 将外部 5 伏电源的正输出端连接到右侧电源轨道的正极。

  8. 将电源供应的负输出端连接到右侧电源轨道的负极。

你需要使用外部的 5 伏电源(步骤 78)来为你的舵机供电。像 MG90S 这样的小型舵机在没有负载的情况下旋转时使用的电流约为 200 毫安,如果你在舵机上连接了重负载或者强行阻止旋转,最大电流为 400 毫安。直接从你的树莓派的 5 伏引脚中提取这个电流可能足以导致它重置。

许多廉价的类似汽车的玩具都有一个硬左/右模拟舵机用于他们的转向机构。它可能看起来像一个舵机,但实际上,它只是一个带有一些齿轮和弹簧的基本直流电机,用于创建硬左/右转向角度。当电机没有参与时,弹簧会将舵机返回到中心。如果你不能对角度进行精细控制,那它就不是一个真正的舵机。

在我们开始编写一些代码之前,让我们快速看一下 PWM 是如何用来控制舵机的。这将让你了解当我们到达代码时发生了什么。

如何使用 PWM 控制舵机

舵机通常需要大约 50 赫兹的 PWM 信号(50 赫兹左右的一些变化是可以的,但我们将坚持使用 50 赫兹作为常见参考点),以及在 1.0 毫秒和 2.0 毫秒之间的脉冲宽度来确定旋转角度。脉冲宽度、占空比和角度之间的关系在图 10.4中有所说明。如果你现在还没有完全理解,不要担心。当我们看到我们的舵机动作并在下一节中审查与舵机相关的代码时,这些应该会更清楚:

图 10.4 - 舵机的脉冲宽度、占空比和角度

我们之前没有涵盖脉冲宽度与我们之前对 PWM 的覆盖范围的关系;然而,这只是描述占空比的另一种方式。

这里有一个例子:

  • 如果我们有一个 50 赫兹的 PWM 信号(即每秒 50 个周期),那么这意味着 1 个 PWM 周期需要1 / 50 = 0.02秒,或者 20 毫秒。

  • 因此,以 1.5 毫秒的脉冲宽度表示的占空比为1.5 毫秒/20 毫秒=0.075,乘以 100 得到占空比为 7.5%。

往回推,我们有以下内容:

  • 7.5%的占空比除以 100 是 0.075。然后,0.075 x 20 毫秒=1.5 毫秒,即 1.5 毫秒的脉冲宽度。

如果你更喜欢一个公式来描述脉冲宽度频率占空比的关系,这里有一个:

要转换回来,我们有以下内容:

好了,数学的部分就到此为止。让我们运行并查看 Python 代码来让我们的舵机移动。

运行和探索舵机代码

我们即将运行的代码可以在chapter10/servo.py文件中找到。我建议在继续之前先查看源代码,以便对文件的内容有一个整体的了解。

当你运行chapter10/servo.py文件中的代码时,你的舵机应该会左右旋转几次。

让我们从代码开始,首先是在第 1 行定义的一些脉冲宽度变量:

LEFT_PULSE  = 1000   # Nano seconds          # (1)
RIGHT_PULSE = 2000
CENTER_PULSE = ((LEFT_PULSE - RIGHT_PULSE) // 2) + RIGHT_PULSE  # Eg 1500

这些脉冲宽度代表了我们舵机的极端左右旋转。

请注意,LEFT_PULSERIGHT_PULSE的值以纳秒为单位,因为这是 PiGPIO 舵机函数使用的单位。

LEFT_PULSE = 1000RIGHT_PULSE = 2000这些值是你经常看到的完美世界值。实际上,你可能需要对这些变量进行轻微调整,以便使舵机完全旋转。例如,我的测试舵机需要LEFT_PULSE = 600RIGHT_PULSE = 2450这些值才能实现完全旋转。如果你调整得太远,舵机在完全左转或右转时会保持连接并发出嘎吱嘎吱的声音。如果发生这种情况,立即断开电源以防止对舵机造成损坏,并重新调整数值。

如果你的舵机向后旋转 - 例如,当你期望它向右旋转时它向左旋转 - 交换LEFT_PULSERIGHT_PULSE的值。或者,只需将你的舵机倒置。

在第 2 行,我们定义了MOVEMENT_DELAY_SECS= 0.5变量,我们稍后需要在舵机移动之间添加延迟:

  # Delay to give servo time to move
  MOVEMENT_DELAY_SECS = 0.5            # (2)

当你使用舵机并发送 PWM 旋转信号时,你会发现它们的行为是异步的。也就是说,代码不会阻塞,直到舵机完成旋转。如果我们打算进行许多快速的舵机移动,并希望它们完全完成,我们必须添加一个短暂的延迟,以确保舵机有时间完成旋转。我们很快将介绍的sweep()函数中就有一个例子。0.5 秒的延迟只是一个建议,所以可以随意尝试不同的数字。

从第 3 行开始,我们定义了三个基本函数来控制我们的舵机:

 def left():                                               # (3)
       pi.set_servo_pulsewidth(SERVO_GPIO, LEFT_PULSE)

 def center():
       pi.set_servo_pulsewidth(SERVO_GPIO, CENTER_PULSE)

 def right():
       pi.set_servo_pulsewidth(SERVO_GPIO, RIGHT_PULSE)

left()函数只是使用 PiGPIO 的set_servo_pulsewidth()方法将 PWM 脉冲宽度设置为LEFT_PULSE在伺服的 GPIO 引脚上。这是 PiGPIO 提供的伺服控制的便利函数,作为使用我们在许多先前章节中看到的set_PWM_dutycycle()set_PWM_frequency()方法的实际替代方案。在我们回顾了代码之后,我们将更多地谈论这些方法。

center()right()函数执行与left()相应的等效操作。

如果您将伺服旋转到指定的角度并尝试用手移动齿轮,您会注意到伺服会抵抗变化。这是因为伺服持续以 50 赫兹的速率接收通过set_servo_pulsewidth()设置的最后一个脉冲,因此它会抵制任何试图改变其设置位置的尝试。

在前一节中,当我们将伺服连接到树莓派时,我们提到了伺服的最大电流约为~400+mA。前面的段落是一个例子,其中伺服吸取了这个最大电流。当伺服接收到脉冲宽度指令时,它会抵抗任何改变其位置的力,导致更多的电流使用。这与我们在第七章中讨论的直流电机的空载电流原理类似,打开和关闭物品

如果您将伺服的脉冲宽度设置为零,就像我们在第 4 行的idle()函数中所做的那样,您现在会发现可以轻松地用手旋转伺服。当我的测试伺服处于空闲状态(或静止状态)时,它大约使用了 6.5 毫安:

   def idle():                                      # (4)
      pi.set_servo_pulsewidth(SERVO_GPIO, 0)

到目前为止,我们已经看到了如何使伺服向左、中间和右边旋转,但是如果我们想将其旋转到特定的角度怎么办?很简单(有点),我们只需要一点数学,就像在第 5 行的angle()函数中所示:

  def angle(to_angle):                                   # (5)
      # Restrict to -90..+90 degrees
      to_angle = int(min(max(to_angle, -90), 90))

      ratio = (to_angle + 90) / 180.0                    # (6)
      pulse_range = LEFT_PULSE - RIGHT_PULSE
      pulse = LEFT_PULSE - round(ratio * pulse_range)    # (7)

      pi.set_servo_pulsewidth(SERVO_GPIO, pulse)

angle()函数接受-90 到+90 度范围内的角度(0 度为中心),在第 6 行计算出我们输入角度相对于我们伺服 180 度范围的比率,然后在第 7 行推导出相应的脉冲宽度。然后将此脉冲宽度发送到伺服,它将相应地调整其角度。

最后,我们在第 10 行遇到了sweep()函数。这是在您运行此代码时提供了伺服左右扫描运动的函数:

 def sweep(count=4):                        # (10)
      for i in range(count):
          right()
          sleep(MOVEMENT_DELAY_SECS)
          left()
          sleep(MOVEMENT_DELAY_SECS)

在这个函数中,我们看到了sleep(MOVEMENT_DELAY_SECS)的使用,这是必要的,以便给伺服完成每个旋转请求的时间,因为伺服的异步性质。如果您注释掉两个sleep()调用,您会发现伺服向左旋转并停止。这是因为当for循环迭代(没有sleep())时,每个left()调用会覆盖先前的right()调用,依此类推,最后在循环完成之前调用的是left()

我们刚刚看到了如何使用 PiGPIO 及其面向伺服的 PWM 函数set_servo_pulsewidth()来控制伺服。如果您对使用set_PWM_frequency()set_PWM_dutycycle()函数实现伺服的实现感兴趣,您会在chapter10文件夹中找到一个名为servo_alt.py的文件。它在功能上等同于我们刚刚介绍的servo.py代码。

这样就结束了我们的伺服示例。您学到的知识以及代码示例将为您提供开始在自己的项目中使用伺服所需的一切!我们的重点是使用角度运动伺服;然而,您学到的核心内容也可以通过一些试验和实验(主要是确定正确的脉冲宽度)来适应连续旋转伺服,我将在下一节中简要提到。

让我们用一个简短的考虑来结束我们对伺服的讨论,讨论不同类型的伺服。

不同类型的伺服

我们的示例使用了常见的 3 线,180 度角舵机。虽然这是一种非常常见的舵机类型,但也有其他变体,包括连续旋转舵机,具有三根以上线的舵机和特殊用途舵机:

  • 连续旋转舵机:有 3 根线,使用与 3 线角度舵机相同的 PWM 原理,只是 PWM 脉冲宽度确定了舵机的旋转方向(顺时针/逆时针)和速度

由于它们的内部控制电路和齿轮装置,连续旋转舵机是直流电机和 H-Bridge 控制器的便捷低速/高扭矩替代品(我们将在下一节中介绍)。

  • 4 线舵机:这些舵机有一组三根线和一根松散的第四根线。这第四根线是舵机的模拟输出,可用于检测角度。如果您需要在启动程序时知道舵机的静止角度,这将非常有用。

舵机使用嵌入电位器来跟踪它们的位置。第四根线连接到这样的电位器。

  • 特殊用途或重型工业用途舵机:具有不同的接线配置和使用要求-例如,它们可能没有内部电路来解码 PWM 信号,并且需要用户提供和创建电路来执行此功能。

我们现在已经了解了常见的业余舵机的工作原理,并且还发现了如何使用 PWM 在 Python 中设置它们的旋转角度。在下一节中,我们将学习更多关于直流电机以及如何使用 H-Bridge 这种集成电路来控制它们。

使用 H-Bridge 集成电路来控制电机

在第七章中,打开和关闭东西,我们学习了如何使用晶体管打开和关闭直流电机,并且还看到了如何使用 PWM 控制电机的速度。我们单个晶体管电路的一个限制是电机只能单向旋转。在本节中,我们将探讨一种让我们能够让电机在前后两个方向旋转的方法-使用所谓的H-Bridge电路。

H-Bridge 中的 H 来自于基本 H-Bridge 电路原理图(由四个单独的晶体管创建)形成字母 H 的感知。

如果您在 eBay 等网站上搜索 H-Bridge 模块,您将会发现许多相同目的的现成模块,我们将在本节中介绍。我们将在面包板上构建一个复制模块。一旦您的面包板复制品运行并了解其工作原理,您就能够理解这些现成模块的构造。

我们可以通过几种方式创建 H-Bridge 来驱动我们的电机:

  • 只需使用预制模块(模块和集成电路也可以称为电机驱动器或电机控制器)。这是最简单的方法。

  • 使用离散元件创建 H-Bridge 电路-例如,四个晶体管,许多二极管,一些电阻和大量的导线连接它们。这是最困难的方法。

  • 使用集成电路(内部组合了所有必要的离散部件)。

舵机,就像我们在上一节中使用的那样,由连接到 H-Bridge 样式电路的直流电机组成,该电路允许电机前后移动,以创建舵机的左右旋转。

我们将选择最后一种选择,并使用 L293D,这是一种常见且低成本的 H-Bridge 集成电路,我们可以用它来构建电机控制电路。

以下是从 L293D 的数据表中提取的基本规格:

  • 连续电流为 600 毫安,峰值/脉冲为 1.2 安。作为提醒,我们在第七章中探讨了电机和电流的使用,打开和关闭东西

  • 它可以控制电压在 4.5 伏至 36 伏之间的电机。

  • 它包括内部飞回二极管,因此我们不需要添加自己的。这就是 L293D中 D 的含义。如果您需要复习飞回二极管,请参阅第七章,打开和关闭

  • 它包括两个通道,因此可以同时驱动两个直流电机。

如果您想购买一个不同的电机驱动 IC 用于项目(例如,如果您需要一个更大电流的 IC),请记住要检查数据表,看看它是否嵌入了飞回二极管,否则您将需要自己提供。

让我们建立电路来控制我们的电机。

构建电机驱动电路

在本节中,我们将构建 H 桥电路,用于控制两个直流电机。以下原理图描述了我们将创建的电路。虽然这个电路看起来很繁忙,但我们的大部分工作将只是连接 L293D IC 的引脚到树莓派、电源和电机:

图 10.5 - L293D 和电机原理图

由于有很多导线连接要完成,我们将在面包板上分四部分构建这个电路。

我们将在电路构建中使用一个 IC。许多 IC(包括 L293D)对静电放电(ESD)敏感,如果暴露于静电放电,它们可能会受到损坏。一般规则是,您应该避免用手指触摸 IC 的引脚/腿,以免您体内的任何静电荷被释放到 IC 上。

让我们从第一部分开始,如下图所示:

图 10.6 - L293D 面包板布局(第一部分,共 3 部分)

以下是我们开始面包板构建的步骤。步骤编号与图 10.6中黑色圆圈中的数字相匹配:

  1. 首先将 L293D IC 放入面包板中,确保 IC 的引脚/腿朝向面包板顶部。IC 的引脚 1 通常由引脚旁边的小圆凹陷或点指示。在我们的插图中,为了方便查看,这个点是白色的;然而,它很可能与 IC 的外壳颜色相同。如果没有点,IC 的一端通常也有一个凹口部分。当您将 IC 的凹口朝向远离您时,引脚 1 是顶部左侧的引脚。

  2. 将树莓派的 5V 引脚连接到左侧电源轨的正电源。

  3. 将树莓派的 GND 引脚连接到左侧电源轨的负电源。

  4. 将 GPIO 18 连接到 L293D 的引脚 1。

  5. 将 GPIO 23 连接到 L293D 的引脚 2。

  6. 将 GPIO 24 连接到 L293D 的引脚 7。

  7. 将跳线引脚连接到 L293D 的引脚 3。此引脚的另一端(标有Output 1Y)目前未连接到任何东西。

  8. 将跳线引脚连接到 L293D 的引脚 6。此引脚的另一端(标有Output 2Y)目前未连接到任何东西。

  9. 使用跳线,将 L293D 的引脚 4 和引脚 5 连接在一起。

  10. 最后,将 L293D 的引脚 4 和引脚 5 连接到左侧电源轨的负电源。

我们刚刚完成的大部分工作涉及 L293D 的通道 1的布线。作为提醒,L293D 有两个输出通道,这意味着我们可以控制两个直流电机。

如果您回顾图 10.6,您会注意到(放置在步骤 78处)的导线构成了通道 1 的输出。在本节的后面,我们将把电机连接到这些导线。此外,在图中,您会注意到 GPIO 18、23 和 24 被标记为通道 1 控制 GPIOs。我们将学习这些 GPIO 是如何用于控制通道 1 电机的,当我们讨论伴随这个电路的代码时。

接下来,我们构建的下一部分主要涉及布线 L293D 的通道 2。这更多或多是我们刚刚执行的布线的镜像:

图 10.7 - L293D 面包板布局(第二部分)

以下是完成我们面包板搭建的第二部分所需遵循的步骤。 步骤编号与图 10.7中黑色圆圈中的数字相匹配:

  1. 将 L293D 的引脚 16 连接到左侧电源轨道的正轨道。 连接到引脚 16 的这个 5 伏电源为IC 的内部电路提供电源-它不是通道输出的电源(那是我们的电机)。 我们将在搭建的第三部分中将外部电源连接到 IC 以为通道的电机供电。

  2. 将 GPIO 16 连接到 L293D 的引脚 9。

  3. 将 GPIO 20 连接到 L293D 的引脚 10。

  4. 将 GPIO 21 连接到 L293D 的引脚 15。

  5. 将跳线引线连接到 L293D 的引脚 14。 此引线的另一端(标有Output 4Y)目前未连接到任何东西。

  6. 将跳线引线连接到 L293D 的引脚 11。 此引线的另一端(标有Output 3Y)目前未连接到任何东西。

  7. 使用跳线将 L293D 的引脚 12 和引脚 13 连接在一起。

  8. 最后,将 L293D 的引脚 12 和引脚 13 连接到右侧电源轨道的负轨道。

现在我们已经连接了通道 2 的输出,我们的第三个任务是连接外部电源:

图 10.8 - L293D 面包板布局(第三部分)

以下是完成我们面包板搭建的第三部分所需遵循的步骤。 步骤编号与图 10.8中黑色圆圈中的数字相匹配:

  1. 将电源的正输出端连接到右侧电源轨道的正轨道。

  2. 将电源的负输出端连接到右侧电源轨道的负轨道。

  3. 将 L293D 的引脚 8 连接到右侧电源轨道的正轨道。 L293D 的引脚 8 提供了用于驱动输出通道的输入电源。

  4. 最后,使用跳线将左侧和右侧电源轨道的负轨道连接起来。

这是我们的面包板布局完成。 但是,还有一个最后的任务,我们要连接我们的电机。 根据以下图表中的示例,您可以将一个电机连接到每个输出通道:

图 10.9 - L293D 电机连接

干得好!那是很多布线。 我想你现在面包板上的电线纠结看起来并不像插图那样优雅! 请务必花时间仔细检查这个电路的布线,因为错误放置的电线会阻止电路按预期工作。

在我们的电路搭建中,在第三部分,步骤 3中,我们将外部 5 伏电源连接到 L293D 的引脚 8。 这是用于驱动每个输出通道和因此我们的电机的电源。 如果您希望使用需要与 5 伏不同电压的电机,您可以更改此供电电压以满足您的需求,但前提是 L293D 的电源电压必须在 4.5 伏至 36 伏的范围内。 还要记住(如本节开头提到的),您的电机不应该吸取超过 600 毫安的持续电流(全开)或 1.2 安的峰值电流(例如,当使用 PWM 时,我们将在编码时介绍)。

如果您阅读 L293D 的数据表,它可能被称为四路半 H 驱动器。 驱动器类型 IC 的数据表可能具有各种不同的标题和措辞。 这里的重要一点是,为了驱动我们的电机向前和向后,我们需要一个完整的 H-Bridge 电路,因此对于 L293D:Quad=4 和 half=0.5,因此4 x 0.5 = 2 -也就是说,2 个完整的 H-Bridge-因此,我们可以控制 2 个电机。

一旦您创建了面包板电路并连接了电机,我们将运行示例代码并讨论其工作原理。

运行示例 H-Bridge 代码以控制电机

现在您已经创建了 H 桥驱动器电路并连接了电机,让我们运行能让电机旋转的代码。

这一节有两个文件,它们可以在chapter10/motor_class.pychapter10/motor.py中找到。运行chapter10/motor.py中的代码,您的电机将会转动,改变速度和方向。

在电机轴上贴一张胶带,以便更容易地看到它们旋转的方向。

当您确认您的电路可以与示例代码一起工作时,我们将继续讨论代码。由于 L293D 可以驱动两个电机,公共代码已经被抽象成了motor_class.py,它被motor.py导入并用于驱动我们的两个独立电机。

我们将从motor.py开始。

motor.py

从第 1 行开始,我们导入 PiGPIO 和motor_class.py文件中定义的Motor类,然后定义了几个变量,描述了我们如何将 L293D 连接到树莓派的 GPIO 引脚:

import pigpio                    # (1)
from time import sleep
from motor_class import Motor

# Motor A
CHANNEL_1_ENABLE_GPIO = 18       # (2)
INPUT_1Y_GPIO = 23 
INPUT_2Y_GPIO = 24

# Motor B
CHANNEL_2_ENABLE_GPIO = 16       # (3)
INPUT_3Y_GPIO = 20
INPUT_4Y_GPIO = 21

回顾图 10.3图 10.4,如果我们考虑电机 A(通道 1)电路的一侧,我们会看到逻辑引脚连接到第 2 行的 GPIO 23 和 24 - INPUT_1Y_GPIO = 23INPUT_2Y_GPIO = 24。这些逻辑引脚(以及我们很快将介绍的使能引脚)用于设置电机的状态和旋转方向。这些状态的真值表如下所示。

这个表格是从 L293D 的数据表中获取的,并进行了重新格式化和补充,以匹配我们的代码和电路:

行号 使能 GPIO 逻辑 1 GPIO 逻辑 2 GPIO 电机功能
1 HIGH 或 > 0% 占空比 向右转
2 HIGH 或 > 0% 占空比 向左转
3 HIGH 或 > 0% 占空比 刹车
4 HIGH 或 > 0% 占空比 刹车
5 LOW 或 0% 占空比 N/A N/A 关闭电机

L293D 有两个使能引脚 - 每个通道一个(即每个电机一个) - 例如,在前面的代码中的第 3 行,CHANNEL_1_ENABLE_GPIO = 18。使能引脚就像每个通道的主开关。当使能引脚设置为高时,它会打开相关的通道,从而给电机供电。或者,如果我们使用 PWM 脉冲使能引脚,我们可以控制电机的速度。当我们探索motor_class.py文件时,我们将很快看到处理逻辑和使能引脚的代码。

接下来,我们将创建pigpio.pi()的单个实例,如第 4 行所示,然后我们将创建两个Motor的实例来代表我们的两个物理电机:

pi = pigpio.pi()                 # (4)
motor_A = Motor(pi, CHANNEL_1_ENABLE_GPIO, INPUT_1Y_GPIO, INPUT_2Y_GPIO)
motor_B = Motor(pi, CHANNEL_2_ENABLE_GPIO, INPUT_3Y_GPIO, INPUT_4Y_GPIO)

在我们创建了motor_Amotor_B类之后,我们使用这些类对电机进行了一些操作,如下面的代码所示,从第 5 行开始 - 这就是您在上一节运行代码时所见到的:

 print("Motor A and B Speed 50, Right") 
 motor_A.set_speed(50)                                # (5)
 motor_A.right()
 motor_B.set_speed(50)
 motor_B.right() 
 sleep(2)

 #... truncated ... 

 print("Motor A Classic Brake, Motor B PWM Brake")
 motor_A.brake()                                      # (6) 
 motor_B.brake_pwm(brake_speed=100, delay_millisecs=50)
 sleep(2)

注意第 6 行的刹车,并观察电机。一个电机的刹车效果比另一个好吗?当我们在下一节的最后讨论两个刹车功能时,我们将进一步讨论这个问题。

让我们继续看motor_class.py。这是我们的树莓派与 L293D 集成的代码所在之处。

motor_class.py

首先,我们看到Motor类的定义及其构造函数:

class Motor:

  def __init__(self, pi, enable_gpio, logic_1_gpio, logic_2_gpio):

    self.pi = pi
    self.enable_gpio = enable_gpio
    self.logic_1_gpio = logic_1_gpio
    self.logic_2_gpio = logic_2_gpio

    pi.set_PWM_range(self.enable_gpio, 100) # speed is 0..100       # (1)

    # Set default state - motor not spinning and 
    # set for right direction.
    self.set_speed(0) # Motor off                                   # (2)
    self.right()

在第 1 行,我们定义了 PiGPIO PWM 使能引脚的占空比范围为0..100。这定义了我们可以在set_speed()函数中使用的最大范围值(即100)。

范围0..100表示我们有 101 个离散的整数 PWM 步骤,这方便地映射到 0%到 100%的占空比。如果您指定一个更高的数字,这并不意味着更多的占空比(或更高的电机速度);它只是改变了步骤的粒度 - 例如,默认的 PWM 范围0..255给我们 256 个离散的步骤,其中 255 = 100%的占空比。

请记住,我们即将讨论的内容涵盖了 L293D IC 电路的一个通道(一个电机)。我们讨论的所有内容也适用于另一个通道 - 只是 GPIO 引脚和 IC 引脚会有所变化。

我们的构造函数通过将电机初始化为关闭(零速度),并将电机默认为右旋转方向来完成,如前面代码中的第 2 行所示。

接下来,我们遇到了几个函数,我们用它们来使我们的电机旋转。我们在第 3 行和第 4 行看到了right()left()方法,它们根据前表中的第 1 行和第 2 行改变了 L293D 的逻辑引脚的高低状态。

 def right(self, speed=None):           # (3)
     if speed is not None:
         self.set_speed(speed)

     self.pi.write(self.logic_1_gpio, pigpio.LOW)
     self.pi.write(self.logic_2_gpio, pigpio.HIGH)

 def left(self, speed=None):           # (4)
     if speed is not None:
         self.set_speed(speed)

     self.pi.write(self.logic_1_gpio, pigpio.HIGH)
     self.pi.write(self.logic_2_gpio, pigpio.LOW)

我们可以通过查询逻辑引脚的当前状态来检查我们的电机是否设置为左旋转或右旋转,就像在is_right()中所示的那样。请注意,is_right()中查询的 GPIO 状态与right()中设置的状态相匹配。

   def is_right(self):                              # (5)
       return not self.pi.read(self.logic_1_gpio)   # LOW 
              and self.pi.read(self.logic_2_gpio)   # HIGH

我们在第 6 行的以下代码中看到了set_speed()方法中使用set_PWM_dutycycle(),在这里我们通过脉冲 L293D 的使能引脚来设置电机的速度。脉冲使能引脚的脉冲是使用我们在第七章中使用的相同基本原理进行的,打开和关闭事物,当我们脉冲一个晶体管来设置电机的速度时。

    def set_speed(self, speed):                      # (6)
        assert 0<=speed<=100
        self.pi.set_PWM_dutycycle(self.enable_gpio, speed)

您可以通过将速度设置为0来停止电机,这实际上是切断电机的电源(0%占空比=引脚低电平)。

接下来,我们发现了两种方法,即brake()brake_pwm(),它们可以用于快速停止电机。制动和通过切断电源(即set_speed(0))来停止电机的区别在于,set_speed(0)允许电机逐渐减速 - 这是前表中第 5 行的状态:

    def brake(self):                # (7)
        was_right = self.is_right() # To restore direction after braking

        self.set_speed(100)
        self.pi.write(self.logic_1_gpio, pigpio.LOW)
        self.pi.write(self.logic_2_gpio, pigpio.LOW)
        self.set_speed(0)

        if was_right:
            self.right()
        else:
            self.left()

当您在上一节中运行此代码,并且如果您自己尝试两种制动功能,我的猜测是您会发现brake()不起作用(或者根本不起作用),而brake_pwm()函数会起作用。

    def brake_pwm(self, brake_speed=100, delay_millisecs=50):    # (8)
        was_right = None # To restore direction after braking
        if self.is_right(): 
            self.left(brake_speed)
            was_right = True
        else:
            self.right(brake_speed)
            was_right = False
        sleep(delay_millisecs / 1000)
        self.set_speed(0)
        if was_right:
            self.right()
        else:
            self.left()

让我们讨论为什么我们定义了两种不同的制动方法,以及为什么一种方法比另一种方法更有效。

brake()的实现是经典的电机制动实现方式,其中逻辑 GPIO 同时设置为高电平或低电平,就像前表中的第 3 行或第 4 行。然而,问题在于,这种逻辑的性能可能会因您使用的 IC(内部构造方式)、电机、电压和电流使用情况而有所不同。在我们的示例中,我们使用的是一个小电机(轴上没有负载)、小电压和电流,以及一个 L293D IC。所有这些的结果是,经典制动方法不起作用,或者效果不佳。

我们使用 L293D IC 是因为它很受欢迎、易得、成本低。它已经生产了很多年,您将毫无问题地找到基于这个 IC 的示例电路和代码,用于各种应用。然而,它并不是最有效的 IC。这是经典制动在某些情况下不起作用的一个因素。

brake_pwm(reverse_speed, delay_secs)的实现采用了一种不同且更可靠的制动方式,即向电机施加一个小的相反电压。您可以使用brake_speeddelay_millisecs参数来调整制动,如果需要的话 - 速度和延迟太小,制动将不起作用,太大则电机会反向。

您是否注意到在全速(即set_speed(100))时,您的电机转速比直接连接到 5 伏特时要慢?L293D 中存在一个约 2 伏特的电压降。即使 V[cc1](电机电源)连接到 5 伏特,电机也没有获得这个完整的 5 伏特(更像是约 3 伏特)。如果您使用的是可变电源(即不是 3.3V/5V 面包板电源),您可以将输入电压增加到 V[cc1]周围的 7 伏特。然后电机将获得约 5 伏特(您可以使用万用表来验证)。

恭喜!您刚刚学会了如何操作伺服并掌握了直流电机在速度和制动方向上的控制。您刚刚获得的电路、代码和技能可以适应许多需要创建运动和角运动的应用,例如机器人车或机械臂。您甚至可以使用这些技能来改装电动玩具和其他电动小工具,并使它们可以由您的树莓派控制。

如果您想进一步扩展您的知识,您可能想探索如何从单独的元件(如晶体管、电阻和二极管)创建 H 桥电路。虽然有各种方法可以完成这个电路,但我们在本章和我们在第七章中使用晶体管时,涵盖了概念和组件的核心基础,打开和关闭东西

干得好!在本节中,我们学习了如何使用 L293D H 桥使直流电机旋转、改变方向和制动。在下一节中,我们将看看 L293D 的另一种用途,并了解如何使用它来控制步进电机。

步进电机控制简介

步进电机在精度和扭矩方面是一种独特的电机类型。与直流电机类似,步进电机可以在两个方向上连续旋转,同时它们可以像伺服一样被精确控制。

在下图中是一个 28BYJ-48 步进电机,以及可以用来将电机连接到面包板的引脚:

图 10.10 - 28BYJ-48 步进电机

步进电机的理论和实践可能会很快变得复杂!有不同形式和类型的步进电机,许多需要考虑的变量,如步距角和齿轮,以及各种布线和控制方式。我们不可能在这里涵盖所有这些参数,也不能深入了解步进电机的低级细节。

相反,我们将介绍一种常见且易得的步进电机 28BYJ-48 的实际操作。一旦您了解了适用于 28BYJ-48 的基本原理,您就可以扩展对步进电机的知识。

当您第一次开始使用步进电机时,控制步进电机可能会令人困惑和琐碎。与直流电机和伺服不同,您需要了解步进电机在机械和代码层面上的工作原理才能控制它们。

我们参考的 28BYJ-48 的基本规格如下:

  • 5 伏特(确保您的步进电机是 5 伏特,因为 28BYJ-48 也有 12 伏特)。

  • 64 的步距角,1:64 的齿轮比,每 360 度旋转64 x 64 = 4,096步。

使用步距角、齿轮比和序列,我们可以计算旋转我们的步进电机 360 度所需的逻辑步数:64 x 64 / 8 = 512

接下来,我们将把我们的步进电机连接到我们的树莓派。

将步进电机连接到 L293D 电路

为了将我们的步进电机连接到树莓派,我们将重复使用我们的 L293D 电路,如前一节中的图 10.8所示。我们需要做的是:

图 10.11 - 28BYJ-48 步进电机接线连接

以下步骤与图 10.11中显示的编号相匹配。请记住,我们从您在构建电机驱动器电路部分完成的电路开始,并在图 10.8中显示:

步骤 25中,我们将在我们的面包板电路中连接步进电机。建议使用引脚排针(如图 10.10中所示)将电机连接到面包板上的空行,然后将 L293D 的输出线连接到与步骤中提到的线颜色相匹配的适当行。

  1. 如果您还没有这样做,请断开两个直流电机与现有电路的连接。

  2. 将你的步进电机的橙线连接到*图 10.8.*中标有Output 4Y的线上。

  3. 将你的步进电机的黄线连接到*图 10.8.*中标有Output 3Y的线上。

  4. 将你的步进电机的粉红线连接到*图 10.8.*中标有Output 2Y的线上。

  5. 将你的步进电机的蓝线连接到*图 10.8.*中标有Output 1Y的线上。

在我们的示例场景中,我们使用我们的 L293D H-Bridge 来驱动我们的步进电机作为双极步进电机。在步进电机方面,你会遇到双极单极这两个术语。这些术语与电机的接线方式有关,这影响了你将如何控制它们。在学习的这个阶段,对双极和单极步进电机之间的区别进行简化的区分如下:

  • 一个双极步进电机需要一个能够改变电流流向的驱动电路。

  • 单极步进电机不需要一个能够改变电流流向的电路。

在我们的双极接线示例中,我们使用 H-Bridge 电路,因为它能够改变电流流向到线圈(例如,这就是我们在前一节中使直流电机改变方向的方法)。

ULN2003 IC 是一种流行的、低成本的达林顿晶体管阵列(带有内置飞回二极管);你也可以使用它来驱动你的步进电机作为单极步进电机。在这种设置中,你将使用连接到+5 伏特的红线,因为 ULN2003 无法改变电流的方向。

连接好步进电机后,我们可以继续使用代码来控制它。

运行和探索步进电机代码

我们即将运行的代码可以在chapter10/stepper.py文件中找到。我建议在继续之前先查看源代码,以便对文件的内容有一个整体的了解。

当你运行chapter10/stepper.py文件中的代码时,你的步进电机应该在一个方向上旋转 360 度,然后再返回。

在你的步进电机轴上贴一块胶带,以便在旋转时更容易看到它的方向。

从源文件的顶部开始,我们定义了所有的 GPIO 变量,包括我们的使能引脚在第 1 行,以及从第 2 行开始与我们的步进电机线圈线有关的变量。这些线必须正确识别和排序,因为线圈线的顺序很重要!

CHANNEL_1_ENABLE_GPIO = 18                                # (1)
CHANNEL_2_ENABLE_GPIO = 16

INPUT_1A_GPIO = 23 # Blue Coil 1 Connected to 1Y          # (2)
INPUT_2A_GPIO = 24 # Pink Coil 2 Connected to 2Y
INPUT_3A_GPIO = 20 # Yellow Coil 3 Connected to 3Y
INPUT_4A_GPIO = 21 # Orange Coil 4 Connected to 4Y

STEP_DELAY_SECS = 0.002                                   # (3)

我们将在代码中稍后看到使用STEP_DELAY_SECS在第 3 行,以在线圈步进之间增加一些延迟。更长的延迟会导致步进电机轴的旋转速度变慢;然而,如果数字太小,轴可能根本不会旋转,或者旋转会不稳定和抖动。随时尝试不同的延迟值以满足你的需求。

接下来,从第 4 行开始,我们将我们的线圈 GPIO 分组到一个 Python 列表(数组)中,并在第 5 行将这些 GPIO 初始化为输出。我们将 GPIO 存储在列表中,因为我们将在稍后使用rotate()函数时对这些 GPIO 进行迭代。我们还在第 6 行有off()函数,用于关闭所有线圈:

coil_gpios = [                             # (4)
    INPUT_1A_GPIO,
    INPUT_2A_GPIO,
    INPUT_3A_GPIO,
    INPUT_4A_GPIO
]

# Initialise each coil GPIO as OUTPUT.
for gpio in coil_gpios:                    # (5)
    pi.set_mode(gpio, pigpio.OUTPUT)

def off():
    for gpio in coil_gpios:                # (6)
       pi.write(gpio, pigpio.LOW) # Coil off

off() # Start with stepper motor off.

在第 7 行,我们在代码中将两个使能 GPIO 引脚设置为HIGH,因为我们正在重用之前直流电机控制示例中的电路。另一种非代码方法是直接将 L293D EN1 和 EN2 引脚连接到+5 伏特(即手动将它们拉高):

# Enable Channels (always high)
pi.set_mode(CHANNEL_1_ENABLE_GPIO, pigpio.OUTPUT)      # (7)
pi.write(CHANNEL_1_ENABLE_GPIO, pigpio.HIGH)
pi.set_mode(CHANNEL_2_ENABLE_GPIO, pigpio.OUTPUT)
pi.write(CHANNEL_2_ENABLE_GPIO, pigpio.HIGH)

从第 8 行开始,我们在一个名为COIL_HALF_SEQUENCECOIL_FULL_SEQUENCE的多维(2 x 2)数组中定义了两个步进序列,因此我们遇到了代码的部分,从这里开始,步进电机控制变得比直流电机或伺服控制更复杂!

步进序列定义了我们必须如何打开(通电)和关闭(不通电)步进电机中的每个线圈,以使其步进。序列中的每一行都有四个元素,每个元素都与一个线圈相关:

COIL_HALF_SEQUENCE = [             # (8)
    [0, 1, 1, 1],
    [0, 0, 1, 1],   # (a)
    [1, 0, 1, 1],
    [1, 0, 0, 1],   # (b)
    [1, 1, 0, 1],
    [1, 1, 0, 0],   # (c)
    [1, 1, 1, 0],
    [0, 1, 1, 0] ]  # (d)

COIL_FULL_SEQUENCE = [
    [0, 0, 1, 1],   # (a)
    [1, 0, 0, 1],   # (b)
    [1, 1, 0, 0],   # (c)
    [0, 1, 1, 0] ]  # (d)

具有八个步骤的序列称为半步序列,而全步序列有四行,是半序列的子集(在前面的代码中匹配*(a)(b)(c)(d)*行)。

半序列将为您提供更高的分辨率(例如,360 度革命的 4,096 步),而全步序列将提供一半的分辨率(2,048 步),但步进速度加倍。

步进电机的步进序列通常可以在其数据表中找到 - 但并非总是如此,正如我们在技术要求部分提到的 28BYJ-48 数据表所证明的那样,因此有时可能需要进行一些研究。

如果步进电机没有旋转,但发出声音和振动,这表明步进序列和线圈顺序不匹配。当您尝试盲目连接它们并希望它们工作时,这是步进电机的常见挫折。为了避免这种反复试验的方法,请花时间识别您的步进电机类型以及它的接线方式(例如,双极或单极),并找出线圈编号以及适合的线圈步进序列是什么样的。查阅您的步进电机的数据表是开始的最佳地方。

接下来,在第 9 行,我们定义了全局变量sequence = COIL_HALF_SEQUENCE,以在步进电机步进时使用半步序列。您可以将其更改为sequence = COIL_FULL_SEQUENCE以使用全步序列 - 所有其他代码保持不变:

sequence = COIL_HALF_SEQUENCE       # (9)
#sequence = COIL_FULL_SEQUENCE

在第 10 行,我们有rotate(steps)方法,这是发生所有魔术的地方,可以这么说。检查和理解这个方法做了什么是理解如何控制我们的步进电机的关键。steps参数可以是正数或负数,以使步进电机向相反方向旋转:

# For rotate() to keep track of the sequence row it is on.
sequence_row = 0 

def rotate(steps):                              # (10)
    global sequence_row
    direction = +1
    if steps < 0:
        direction = -1

rotate()函数的核心部分在两个for循环中,从第 11 行开始:

# rotate(steps) continued...

    for step in range(abs(steps)):                # (11)
      coil_states = sequence[sequence_row]        # (12)
      for i in range(len(sequence[sequence_row])):
          gpio = coil_gpios[i]                    # (13)
          state = sequence[sequence_row][i]       # (14)
          pi.write(gpio, state)                   # (15)
          sleep(STEP_DELAY_SECS)

当代码循环进行step次迭代时,我们在第 12 行得到下一个线圈状态的形式,sequence[sequence_row](例如,[0, 1, 1, 1]),然后在第 13 行循环获取相应的线圈 GPIO,并在第 14 行得到其HIGH/LOW状态。在第 15 行,我们使用pi.write()设置线圈的HIGH/LOW状态,这使我们的电机移动(即步进),然后休眠一小段时间。

接下来,从第 16 行开始,根据旋转方向(即steps参数是正数还是负数),更新sequence_row索引:

# rotate(steps) continued...

      sequence_row += direction            # (16)
      if sequence_row < 0:
          sequence_row = len(sequence) - 1
      elif sequence_row >= len(sequence):
          sequence_row = 0

在这段代码块的末尾,如果还有更多的步骤要完成,代码将返回到第 11 行进行下一个for steps in ...迭代。

最后,在第 17 行,我们来到了使我们的步进电机在运行示例时旋转的代码部分。请记住,如果您将第 9 行切换为sequence = COIL_FULL_SEQUENCE,则步数将为2048

if __name__ == '__main__':
    try:                                                   #(17)
        steps = 4096 # Steps for HALF stepping sequence.
        print("{} steps for full 360 degree rotation.".format(steps))
        rotate(steps) # Rotate one direction
        rotate(-steps) # Rotate reverse direction

    finally:
        off() # Turn stepper coils off
        pi.stop() # PiGPIO Cleanup

恭喜!您刚刚完成了关于步进电机控制的速成课程。

我明白,如果您是步进电机的新手,需要进行一些多维思考,并且您已经接触到了许多概念和术语,我们无法详细介绍。步进电机需要时间来理解;然而,一旦您掌握了控制一个步进电机的基本过程,那么您就已经在更深入地理解更广泛的概念的道路上了。

互联网上有许多步进电机教程和示例。许多示例的目标只是让步进电机工作,但由于底层复杂性,这并不总是清楚地解释了如何实现这一点。当您阅读步进电机的资料并探索代码示例时,请记住,步长的定义可能会有很大的差异,这取决于它的使用环境。这就是为什么两个示例可能会针对同一个步进电机引用显著不同的步数的原因。

总结

在本章中,您学会了如何使用三种常见类型的电机来利用树莓派创建复杂的运动 - 使用舵机创建角动量,使用带 H 桥驱动器的直流电机创建方向运动和速度控制,以及使用步进电机进行精确运动。如果您掌握了这些类型电机的一般概念,那么您值得表扬!这是一个成就。虽然电机在原理上很简单,它们的运动在日常用品和玩具中是我们每天都习以为常的,但正如您发现的那样,背后有很多事情在发生,以使得运动发生。

本章学到的知识,加上示例电路和代码,为您提供了一个基础,您可以用它来开始构建自己的应用程序,其中需要运动和动作。一个简单有趣的项目可以是创建一个程序来控制一个机器人汽车或机械臂 - 您可以在 eBay 等网站上找到汽车和机械臂的 DIY 套件和零件。

在下一章中,我们将探讨如何使用树莓派、Python 和各种电子元件来测量距离和检测运动的方法。

问题

最后,这里有一些问题供您测试本章材料的知识。您将在书的“评估”部分找到答案:

  1. 您的舵机无法完全向左或向右旋转。这是为什么,如何解决?

  2. 您的舵机在极左/右位置发出嘎吱声。为什么?

  3. 在控制直流电机时,H 桥相比单个晶体管有什么优势?

  4. 您正在使用 L293D H 桥集成电路。您按照数据表上的说明操作,但无法使电机制动。为什么?

  5. 为什么将 5 伏电机连接到使用 L293D 的 H 桥时会比直接连接到 5 伏电源时转速较慢?

  6. 您有一个步进电机无法工作 - 它会震动,但不会转动。可能是什么问题?

  7. 您能直接从四个树莓派的 GPIO 引脚驱动步进电机吗?

第十一章:测量距离和检测运动

欢迎来到我们的最后一个基于核心电子学的章节。在上一章中,我们学习了如何以复杂的方式控制三种不同形式的电机。在本章中,我们将把注意力集中在使用树莓派和电子设备检测运动和测量距离。

检测运动对于自动化项目非常有用,例如当您走进房间或建筑物时点亮灯光,警报系统,建筑物计数器或检测轴的旋转。我们将研究两种运动检测技术,包括使用被动红外PIR)传感器来检测人(或动物)的存在的热检测,以及数字霍尔效应传感器,它可以检测磁场的存在(或者更宽泛地说,我们可以说霍尔效应传感器可以检测到磁铁移过它的时候)。

距离测量对于许多项目也很有用,从碰撞检测电路到测量水箱水位。我们将研究两种距离测量形式,包括使用超声波声音传感器,可以测量大约 2 厘米到 4 米的距离,以及可以测量磁场接近度的模拟霍尔效应传感器,可以测量到毫米级的磁场接近度。

以下是本章的内容:

  • 使用 PIR 传感器检测运动

  • 使用超声波传感器测量距离

  • 使用霍尔效应传感器检测运动和距离

技术要求

要执行本章的练习,您需要以下内容:

  • 树莓派 4 型 B

  • Raspbian OS Buster(带桌面和推荐软件)

  • 最低 Python 版本 3.5

这些要求是本书中代码示例的基础。可以合理地期望代码示例应该可以在树莓派 3 型 B 或不同版本的 Raspbian OS 上无需修改即可工作,只要您的 Python 版本是 3.5 或更高。

您可以在 GitHub 存储库的chapter11文件夹中找到本章的源代码,网址为github.com/PacktPublishing/Practical-Python-Programming-for-IoT

您需要在终端中执行以下命令来设置虚拟环境并安装本章代码所需的 Python 库:

$ cd chapter11              # Change into this chapter's folder
$ python3 -m venv venv      # Create Python Virtual Environment
$ source venv/bin/activate  # Activate Python Virtual Environment
(venv) $ pip install pip --upgrade        # Upgrade pip
(venv) $ pip install -r requirements.txt  # Install dependent packages

requirements.txt安装以下依赖项:

本章练习所需的电子元件如下:

HC-SR04 有两种变体可用。更常见的 HC-SR04,输出 5 伏逻辑和 HC-SR04P,可以在 3 伏至 5.5 伏之间工作。这两种模块都适用于本章的练习。

使用 PIR 传感器检测运动

PIR 传感器是一种可以检测到物体(例如人)发出的红外光(热量)的设备。我们在周围的应用中看到这些类型的传感器,如安全系统和对我们的存在做出反应的自动门和灯。PIR 中的“被动”意味着传感器只是检测运动。要检测什么移动和如何移动,你需要一个主动红外设备,比如热成像摄像头。

PIR 传感器有几种不同的形式和品种;然而,它们的基本用法是相同的——它们作为一个简单的数字开关。当它们没有检测到运动时,它们输出数字LOW,当检测到运动时,它们输出数字HIGH

下图显示了我们将在示例中使用的 HC-SR501 PIR 传感器模块。图片中显示了模块的顶部、底部和 PIR 传感器的常见原理图符号:

图 11.1 - HC-SR501 PIR 传感器模块

一些 PIR 传感器,包括我们的 HC-SR501,上面有设置和校准调整。这些调整用于改变传感器的灵敏度范围和触发模式。在没有板载校准的情况下使用 PIR 设备意味着我们需要在代码中自行处理灵敏度调整。

关于 HC-SR501,它的端子如下:

  • GND:接地。

  • Vcc:连接到 5 伏至 20 伏的电源。

  • 数据:我们连接到 GPIO 引脚的数字输出。当 PIR 检测到运动时,此引脚变为HIGH;否则,在没有运动的情况下保持LOW。HC-SR501 输出 3.3 伏信号,尽管它需要 5 至 20 伏的电源。接下来我们将看到,板载的灵敏度调整定时调整触发模式跳线会影响数据引脚在检测到运动时保持HIGH的方式、时间和持续时间。

HC-SR501 的板载设置如下:

  • 灵敏度调整:改变有效的移动感应范围,从大约 3 米到大约 7 米。使用小螺丝刀旋转此设置的拨号。

  • 时间延迟调整:在检测到运动后数据端口保持HIGH的时间。调整范围约为 5 秒至 300 秒。使用小螺丝刀旋转此设置的拨号。

  • 触发模式跳线:在持续检测到运动的情况下,此跳线设置意味着在时间延迟到期后(由时间延迟调整设置),数据端口将执行以下操作:

  • 保持HIGH。这是可重复触发设置,通过将跳线放置在H位置来设置。

  • 恢复为LOW。这是单次触发设置,通过将跳线放置在L位置来设置。

你的 PIR 的最佳设置将取决于你打算如何使用它以及你部署传感器的环境。我的建议是,在完成电路搭建并运行后续部分的示例代码后,尝试调整设置,以了解如何改变设置会影响传感器的操作。记得查阅 HC-SR501 的数据表,以获取有关传感器及其板载设置的更多信息。

让我们把我们的 PIR 传感器接线并连接到我们的树莓派。

创建 PIR 传感器电路

在这一部分,我们将把我们的 PIR 传感器连接到我们的树莓派。以下是我们即将构建的电路的原理图。正如你所看到的,从 PIR 传感器的角度来看,它的布线相对简单:

图 11.2 - PIR 传感器模块电路

让我们按照下图所示将其连接到我们的树莓派:

图 11.3 - PIR 传感器电路面包板布局

以下是创建面包板搭建的步骤。步骤编号与图 11.3中的黑色圆圈中的编号相匹配:

  1. 将您的 PIR 传感器的每个端子连接到面包板上。您将需要三根公对公跳线。

  2. 将树莓派上的 5 伏特引脚连接到 PIR 的 Vcc 端使用的面包板行。PIR 传感器只使用少量电流,因此将 5 伏特 Vcc 引脚直接连接到树莓派上是可以的。

  3. 将树莓派上的 GND 引脚连接到 PIR 的 GND 端使用的面包板行。

  4. 将树莓派上的 GPIO 21 引脚连接到 PIR 的数据端使用的面包板行。

重要提示:我们参考的 HC-SR501 PIR 传感器需要>4.5 伏特的电源(Vcc),并在其 Sig 输出引脚上输出 3.3 伏特。如果您使用的是不同的 PIR 传感器,请查阅其数据表并检查输出引脚电压。如果它大于 3.3 伏特,您将需要使用电压分压器或逻辑电平转换器。在下一节中,我们将涵盖这种确切的情况,当我们将电压分压器与 HC-SR04 传感器配对,将其 5 伏特输出转换为树莓派友好的 3.3 伏特。

创建电路后,我们将继续并运行我们的 PIR 示例代码,这将让我们检测运动。

运行和探索 PIR 传感器代码

我们的 PIR 电路代码可以在chapter11/hc-sr501.py文件中找到。在继续之前,请查看源代码,以对该文件的内容有一个广泛的了解。

HC-SR501 数据表规定,传感器在上电后需要大约 1 分钟的时间来初始化和稳定自身。如果在传感器变得稳定之前尝试使用传感器,可能会在启动程序时收到一些错误的触发。

在终端中运行hc-sr501.py文件。当 HC-SR501 检测到运动时,程序将在终端上打印Triggered,或者在未检测到运动时打印Not Triggered,如下所示:

(venv) $ python hc-sr501.py 

PLEASE NOTE - The HC-SR501 Needs 1 minute after power on to initialize itself.

Monitoring environment...
Press Control + C to Exit
Triggered.
Not Triggered.
... truncated ...

如果您的程序没有按预期响应,请尝试调整我们之前在使用 PIR 传感器检测运动部分中讨论的灵敏度调整时间延迟调整触发模式跳线设置中的一个或多个。

您可以将 HC-SR501 视为基本开关。它要么是打开的(HIGH),要么是关闭的(LOW),就像普通的按钮开关一样。实际上,我们的代码类似于第二章中使用 Python 和物联网入门部分中介绍的 PiGPIO 按钮示例。我们只会在这里简要介绍核心代码部分;但是,如果您需要更深入的解释或复习,请重新查看第二章中的 PiGPIO 部分,使用 Python 和物联网入门

让我们讨论示例代码。首先,我们在第 1 行开始设置我们的 GPIO 引脚为带有下拉使能的输入引脚,而在第 2 行,我们启用了去抖动。我们的 HC-SR501 模块实际上不需要在代码中激活下拉,也不需要去抖动;但是,我为了完整性而添加了它:

# ... truncated ...
GPIO = 21

# Initialize GPIO
pi.set_mode(GPIO, pigpio.INPUT)                               # (1)
pi.set_pull_up_down(GPIO, pigpio.PUD_DOWN)
pi.set_glitch_filter(GPIO, 10000) # microseconds debounce     # (2)

接下来,在第 3 行,我们定义了callback_handler()函数,每当 GPIO 引脚改变其HIGH/LOW状态时都会被调用:

def callback_handler(gpio, level, tick):                       # (3)
    """ Called whenever a level change occurs on GPIO Pin.
      Parameters defined by PiGPIO pi.callback() """
    global triggered

    if level == pigpio.HIGH:
        triggered = True
        print("Triggered")
    elif level == pigpio.LOW:
        triggered = False
        print("Not Triggered")

最后,在第 4 行,我们注册了回调函数。正是第二个参数pigpio.EITHER_EDGE导致callback_handler()在 GPIO 变为HIGHLOW时被调用:

# Register Callback
callback = pi.callback(GPIO, pigpio.EITHER_EDGE, callback_handler) # (4)

作为对比,在第二章中,使用 Python 和物联网入门,对于我们的按钮示例,此参数为pigpio.FALLING_EDGE,意味着只有在按下按钮时才会调用回调,而松开按钮时不会调用。

正如我们所见,PIR 传感器只能检测物体的接近 - 例如,有人靠近我们的传感器吗? - 但它无法告诉我们物体的距离有多远或多近。

我们现在已经学会了如何创建和连接一个简单的 PIR 传感器电路到我们的树莓派,并且学会了如何在 Python 中使用它来检测运动。有了这些知识,你现在可以开始构建自己的运动检测项目,比如结合第七章中的示例,打开和关闭东西,或者作为你自己的警报和监控系统的重要部分。

接下来,我们将看一下能够估算距离的传感器。

用超声波传感器测量距离

在上一节中,我们学会了如何使用 PIR 传感器检测运动。正如我们发现的那样,我们的 PIR 传感器是一个数字设备,通过使其输出为数字HIGH来表示检测到运动。

现在是时候学习如何用树莓派测量距离了。有各种各样的传感器可以执行这项任务,它们通常要么使用声音,要么使用光。我们的示例将基于流行的 HC-SR04 超声波距离传感器(它使用声音),如下图所示:

图 11.4 - HC-SR04 超声波距离传感器模块

你通常会在现代汽车保险杠上找到超声波距离传感器(它们通常是小圆圈,这是与前面图中的 HC-SR04 不同的形状)。这些传感器计算你的车和附近物体之间的距离,例如,当你越来越接近物体时,会让车内的蜂鸣器越来越快地响起。

另一个常见的应用是用于测量液体水平,比如水箱中的水位。在这种情况下,(防水)超声波传感器测量从水箱顶部到水位的距离(声音脉冲反射在水上)。然后可以将测得的距离转换为水箱的大致容量。

让我们更仔细地看一下我们的 HC-SR04 传感器。参考 HC-SR04 数据表中的核心规格如下:

  • 电源电压 5 伏(HC-SR04)或 3 伏至 5.5 伏(HC-SR04P)

  • 逻辑电压 5 伏(HC-SR04)或 3 伏至 5.5 伏(HC-SR04P)

  • 工作电流 15 毫安,静态电流 2 毫安

  • 有效测量范围为 2 厘米至 4 米,精度为+/- 0.3 厘米

  • 10 微秒的触发脉冲宽度。我们将在标题为HC-SR04 距离测量过程的部分重新讨论这个脉冲宽度并进行更多讨论。

SC-SR04 有两个圆柱体。它们如下:

  • TTX:产生超声波脉冲的发射器

  • RRX:检测超声波脉冲的接收器

我们将在下一节讨论发射器和接收器如何一起工作来测量距离。

HC-SR04 有四个端子,它们如下:

  • Vcc:电源(树莓派 5 伏引脚将是可以的,考虑到最大电流为 15 毫安)。

  • GND:接地连接。

  • TRIG:触发输入端子 - 当HIGH时,传感器发送超声波脉冲。

  • ECHO:回声输出端子 - 当TRIG变为HIGH时,此引脚变为HIGH,然后在检测到超声脉冲时变为LOW

我们将在标题为HC-SR04 距离测量过程的部分讨论TRIGECHO端子的使用。

现在我们了解了超声波距离传感器的基本用法和 HC-SR04 的基本特性和布局,让我们讨论一下它是如何工作的。

超声波距离传感器的工作原理

让我们看看发射器(TX)和接收器(RX)如何一起工作来测量距离。超声波传感器的基本工作原理如下图所示:

图 11.5 - 超声波距离传感器操作

以下是发生的事情:

  1. 首先,传感器从发射器(TX)发送超声波脉冲。

  2. 如果传感器前面有物体,这个脉冲会反弹到物体上并返回到传感器,并被接收器(RX)检测到。

  3. 通过测量发送脉冲和接收脉冲之间的时间,我们可以计算传感器和物体之间的距离。

了解了传感器工作原理的高层次理解后,接下来,我们将深入讨论如何使用 HC-SR04 上的 TRIG 和 ECHO 端子一起估算距离的过程。

HC-SR04 距离测量过程

在本节中,我们将介绍使用 HC-SR04 测量距离的过程。 如果这一点不立即明白,不要担心。 我在这里提供了详细信息作为背景材料,因为这是我们示例程序实现的逻辑过程,以使传感器工作。 您还会在传感器的数据表中找到这个过程的记录。

我们通过正确使用和监控 TRIG 和 ECHO 引脚来测量 HC-SR04 的距离。 过程如下:

  1. 将 TRIG 引脚拉高 10 微秒。 拉高 TRIG 也会使 ECHO 引脚变高。

  2. 启动计时器。

  3. 等待以下任一情况发生:

  • ECHO 变为LOW

  • 经过 38 毫秒(从数据表中,这是>4 米的时间)

  1. 停止计时器。

如果经过了 38 毫秒,我们得出结论认为传感器前面没有物体(至少在有效范围内的 2 厘米到 4 米之间)。 否则,我们将经过的时间除以 2(因为我们想要传感器和物体之间的时间间隔,而不是传感器到物体再返回到传感器),然后使用基本物理学,使用以下公式计算传感器和物体之间的距离:

在这里,我们有以下内容:

  • d是以米为单位的距离。

  • v是以米/秒为单位的速度,我们使用声速,大约为 20°C(68°F)时的 343 米/秒。

  • t是以秒为单位的时间。

HC-SR04 只会估算距离。 有几个参数会影响其准确性。 首先,正如之前暗示的,声速随温度变化而变化。 其次,传感器的分辨率为±0.3 厘米。 此外,被测物体的大小,物体相对于传感器的角度,甚至物体的材质都会影响 ECHO 的定时结果,从而影响计算出的距离。

通过对如何使用 HC-SR04 估算距离的基本理解,让我们构建我们的电路,将 HC-SR04 连接到我们的树莓派。

构建 HC-SR04 电路

是时候构建我们的 HC-SR04 电路了。 我们电路的原理图如下图所示。 这种布线适用于 HC-SR04 或 HC-SR04P 模块:

图 11.6 - HC-SR04(5 伏逻辑 ECHO 引脚)电路

作为提醒,HC-SR04 模块(或像这样连接到 5 伏电源的 HC-SR04P)是一个 5 伏逻辑模块,因此您会注意到电路中由两个电阻器创建的电压分压器将 5 伏转换为 3.3 伏。 如果您需要关于电压分压器的复习,我们在第六章中详细介绍了它们,软件工程师的电子学 101

让我们在面包板上构建这个电路:

图 11.7 - HC-SR04 电路面包板布局(第一部分)

以下是创建面包板构建的第一部分的步骤。 步骤编号与图 11.7中的黑色圆圈中的数字相匹配:

  1. 将 1kΩ电阻(R1)放入面包板中。

  2. 将 2kΩ电阻(R2)放入面包板中。 第二个电阻的一个腿与第一个电阻的一个腿共用一行。 在插图中,这可以在右侧银行的第 21 行中看到。

  3. 将左侧和右侧的负电源导轨连接在一起。

  4. 将树莓派上的 GND 引脚连接到左侧电源轨的负电源。

  5. 将第二条 2kΩ电阻(R2)连接到右侧电源轨的负电源。

  6. 将 HC-SR04 传感器上的 GND 端子连接到右侧电源轨的负电源。

  7. 将 HC-SR04 传感器上的 Vcc 端子连接到右侧电源轨的正电源。

确保 R1 和 R2 电阻的连接方式如前图所示 - 即 R1(1kΩ)连接到 HC-SR04 的 ECHO 引脚。由 R1 和 R2 创建的电压分压器将 ECHO 引脚的 5 伏特转换为3.3 伏特。如果您将电阻安装反了,5 伏特将转换为1.67 伏特,这不足以在树莓派上注册逻辑HIGH

既然我们已经布置好了基本组件并进行了一些初步的接线连接,让我们完成我们的构建:

图 11.8 - HC-SR04 电路面包板布局(第二部分)

以下是要遵循的步骤。步骤编号与图 11.8中黑色圆圈中的数字相匹配:

  1. 将树莓派上的 GPIO 20 连接到 HC-SR04 传感器上的 Trig 端子。

  2. 将树莓派上的 GPIO 21 连接到 1kΩ(R1)和 2kΩ(R2)电阻的交汇处。这个连接在 F21 孔上的插图中有显示。

  3. 将 HC-SR04 传感器的 Echo 端子连接到 1kΩ电阻(R1)。这个连接在 J17 孔上有显示。

  4. 将 5 伏特电源的正端连接到右侧电源轨的正电源。

  5. 将 5 伏特电源的负端连接到右侧电源轨的负电源。

如前所述,我们的电路构建将适用于 HC-SR04 和 HC-SR04P 模块。如果您有 HC-SR04P 模块,可以选择更简单的接线选项。由于 HC-SR04P 将在 3.3 伏特下工作,因此您可以这样做:

  • 将 Vcc 连接到 3.3 伏特电源或树莓派上的 3.3 伏特引脚。

  • 将 ECHO 端子直接连接到 GPIO 21。

  • GND 仍然连接到 GND,TRIG 仍然直接连接到 GPIO 20。

由于此配置以 3.3 伏特供电,因此 ECHO 端子上的逻辑输出也是 3.3 伏特,因此可以安全地直接连接到树莓派的 GPIO 引脚。

太好了!现在我们的电路已经完成,接下来我们将运行我们的示例程序,并使用 HC-SR04 来测量距离,并了解使其发生的代码。

运行和探索 HC-SR04 示例代码

HC-SR04 的示例代码可以在chapter11/hc-sr04.py文件中找到。在继续之前,请查看源代码,以对该文件的内容有一个广泛的了解。

在 HC-SR04 前面放一个实物体(大约 10 厘米),并在终端中运行代码。当您将物体靠近或远离传感器时,终端中打印的距离将会改变,如下所示:

(venv) python hc-sr04.py
Press Control + C to Exit
9.6898cm, 3.8149"
9.7755cm, 3.8486"
10.3342cm, 4.0686"
11.5532cm, 4.5485"
12.3422cm, 4.8591"
...

让我们来审查代码。

首先,在第 1 行定义了TRIG_GPIOECHO_GPIO引脚,在第 2 行定义了声速的VELOCITY常数。我们使用 343 米每秒。

我们的代码使用 343 米/秒作为声速,而数据表建议的值为 340 米/秒。您还会发现其他使用略有不同数值的 HC-SR04 示例和库。这些差异是不同代码示例和库可能会对相同的传感器到物体距离产生略有不同读数的原因之一。

在第 3 行,我们定义了TIMEOUT_SECS = 0.10.1的值大于 38 毫秒(来自数据表)。任何大于这个值的时间,我们都会得出结论,我们的 HC-SR04 传感器前面没有物体,并返回SENSOR_TIMEOUT值,而不是get_distance_cms()函数中的距离,我们马上就会讲到:

TRIG_GPIO = 20                                       # (1)
ECHO_GPIO = 21

# Speed of Sound in meters per second
# at 20 degrees C (68 degrees F)
VELOCITY = 343                                       # (2)

# Sensor timeout and return value
TIMEOUT_SECS = 0.1 # based on max distance of 4m     # (3)
SENSOR_TIMEOUT  = -1

接下来,从第 4 行开始,我们找到了几个变量,用于帮助测量传感器超声脉冲的时间以及我们是否有一个成功的读数:

# For timing our ultrasonic pulse
echo_callback = None                             # (4)
tick_start = -1
tick_end = -1
reading_success = False

echo_callback将包含一个 GPIO 回调引用,以供稍后进行清理,而tick_starttick_end保存了用于计算超声脉冲回波的经过时间的开始和结束时间。术语tick用于与 PiGPIO 定时函数保持一致,我们将很快讨论这一点。只有在TIMEOUT_SECS过去之前我们有一个距离读数时,reading_success才为True

我们使用第 5 行显示的trigger()函数来启动我们的距离测量。我们在第 6 行简单地应用了数据表中的流程 - 也就是说,我们使 TRIG 引脚在 10 微秒内变为HIGH

def trigger():                                   # (5)
    global reading_success
    reading_success = False

    # Start ultrasonic pulses
    pi.write(TRIG_GPIO, pigpio.HIGH)             # (6)
    sleep(1 / 1000000) # Pause 10 microseconds
    pi.write(TRIG_GPIO, pigpio.LOW)

在第 7 行显示的get_distance_cms()函数是我们的主要函数,它通过调用trigger()来启动距离测量过程,然后在第 8 行等待,直到我们有一个成功的读数(也就是reading_success = True),或者TIMEOUT_SECS过去,此时我们返回SENSOR_TIMEOUT。在等待期间,一个名为echo_handler()的回调处理程序在后台监视ECHO_GPIO引脚以获取成功的读数。我们将在本节后面讨论echo_handler()

def get_distance_cms()                           # (7)
    trigger()

    timeout = time() + TIMEOUT_SECS              # (8)
    while not reading_success:
      if time() > timeout:
          return SENSOR_TIMEOUT
      sleep(0.01)

当我们有一个成功的读数时,我们的函数继续。在第 9 行,我们取tick_starttick_end变量(现在已经由回声回调处理程序设置了值)并计算经过的时间。记住,我们在第 9 行将经过的时间除以 2,因为我们想要从传感器到物体的时间,而不是从传感器到物体再返回传感器的完整超声脉冲往返时间:

# ... get_distance_cms() continued

    # Elapsed time in microseconds.
    #Divide by 2 to get time from sensor to object.
    elapsed_microseconds = 
                pigpio.tickDiff(tick_start, tick_end) / 2   # (9)

    # Convert to seconds
    elapsed_seconds = elapsed_microseconds / 1000000

    # Calculate distance in meters (d = v * t)
    distance_in_meters = elapsed_seconds * VELOCITY         # (10)

    distance_in_centimeters = distance_in_meters * 100
    return distance_in_centimeters

在第 10 行,我们应用了我们之前讨论过的公式,d = v × t,来计算传感器和物体之间的距离。

接下来,在第 11 行,我们遇到了echo_handler()函数,它监视ECHO_GPIO引脚的状态变化:

def echo_handler(gpio, level, tick):            # (11)
    global tick_start, tick_end, reading_success

    if level == pigpio.HIGH:
        tick_start = tick                       # (12)
    elif level == pigpio.LOW:
        tick_end = tick                         # (13)
        reading_success = True

根据数据表中的流程,我们捕获了在第 12 行发送脉冲时的时间,当ECHO_GPIO变为HIGH,并在第 13 行接收到脉冲回来时的时间,当ECHO_GPIO变为LOW。如果我们在超时之前(在第 8 行)检测到ECHO_GPIOLOW,我们将reading_success = True,这样get_distance_cms()就知道我们有一个有效的读数。

最后,我们在第 14 行使用 PiGPIO 注册了echo_handler()回调函数。pigpio.EITHER_EDGE参数表示我们希望在ECHO_GPIO转换为HIGHLOW状态时调用此回调函数:

echo_callback = 
    pi.callback(ECHO_GPIO, pigpio.EITHER_EDGE, echo_handler) # (14)

干得好!你刚刚连接、测试和学习了如何使用 HC-SR04 传感器以及 PiGPIO 来估算距离。你刚刚学到的电路和代码示例可以被改编并用于测量水箱水位,甚至作为机器人的碰撞检测(这是 HC-SR04 在业余机器人中非常常见的应用),或者在任何其他需要距离的项目中。

接下来,我们将简要探讨霍尔效应传感器,并学习它们如何用于检测运动和相对距离。

使用霍尔效应传感器检测运动和距离

本章的最后一个实际示例将说明霍尔效应传感器的使用。霍尔效应传感器是简单的组件,用于检测磁场的存在(或不存在)。与 PIR 或距离传感器相比,您可以使用霍尔效应传感器与磁铁一起监测小范围甚至非常快速的运动。例如,您可以将一个小磁铁固定在直流电机的轴上,并使用霍尔效应传感器来确定电机的每分钟转数。

霍尔效应传感器的另一个常见应用是在手机和平板电脑中。一些手机和平板电脑的外壳和套子中有一个小磁铁。当您打开或关闭外壳时,您的设备会通过霍尔效应传感器检测到这个磁铁的存在或不存在,并自动为您打开或关闭显示屏。

霍尔效应传感器有三种类型,如下所述:

  • 非锁定开关类型(数字):它们在磁场存在时输出数字状态(即),在磁场不存在时输出相反的数字状态。信号在磁场存在时是还是取决于传感器是主动还是主动(如果需要关于主动和主动概念的复习,请参考第六章,软件工程师的电子学 101)。

  • 锁定开关类型(数字):当检测到磁铁的一个极性(例如南极)时,它们输出(并锁定到)(或),当检测到另一个极性(例如北极)时返回到(或`低)(解锁)。

  • 比率类型(模拟):它们根据它们离磁场有多近而输出不同的电压。

一些读者可能熟悉一种叫做磁簧开关的组件,它是一种磁控开关。乍一看,它们在基本原理和操作上似乎与非锁定霍尔效应传感器相似。以下是重要的区别 - 与经典的磁簧开关不同,霍尔效应传感器是固态设备(没有活动部件),它们可以非常非常快地切换/触发(每秒数千次),并且它们需要一个适当的电路来使它们工作。

我们的示例将使用 A3144(非锁定数字开关)和 AH3503(模拟比率)霍尔效应传感器。由于这些特定部件的可用性和低成本,我们选择了这些特定部件;但是,我们将讨论的一般原则也适用于其他霍尔效应传感器。

图中显示了 A3144 霍尔效应传感器和常见的原理图符号:

图 11.9 - 霍尔效应传感器和符号

您会注意到最右边的符号有四个突出的输出,因为一些霍尔效应传感器确实有四条腿。您可以期望该符号的输出在适用于所指的传感器的原理图中被注释。我们将坚持使用三条腿的传感器和相应的三个输出符号。

我们组件的腿如下:

  • Vcc:5 伏电源。

  • GND:接地连接。

  • 输出:5 伏信号输出。请注意,A3144 是主动的,这意味着在磁场存在时,输出腿变为

输出腿的行为将取决于霍尔效应传感器的类型:

  • 锁定和非锁定开关类型输出腿将输出数字或数字

  • 比率类型:输出将是变化的电压(即模拟输出)。请注意,变化电压的范围不会是 0 到 5 伏之间的全部范围,而更可能是几百分之几伏的范围。

现在我们了解了霍尔效应传感器的腿配置,让我们构建我们的电路。

创建霍尔效应传感器电路

我们将在面包板上构建以下电路。与我们的 HC-SR04 示例和图 11.5中的电路类似,由于我们的霍尔效应传感器输出 5 伏逻辑,我们需要使用电压分压器将其降至 3.3 伏:

图 11.10 - 霍尔效应传感器电路

您会注意到该电路的输出是二元的,并且取决于您使用的传感器:

  • 对于非锁定开关锁定开关类型的霍尔效应传感器,您将直接将电路连接到 GPIO 21,因为传感器将输出数字/信号。

  • 对于比率类型的霍尔效应传感器,您需要通过 ADS1115 模数转换器将传感器连接到您的树莓派,因为传感器输出变化的模拟电压。

我没有在图 11.9或以下的步进面包板布局中包括 ADS1115 的接线。我们已经在之前的章节中看到了如何使用 ADS1115 将模拟输出连接到树莓派 - 例如电路和代码,请参考第五章,将您的树莓派连接到物理世界,和/或第九章,测量温度、湿度和光照水平

让我们在面包板上构建这个电路。这个布局是用于开关型霍尔效应传感器的:

图 11.11 - 霍尔效应传感器电路面包板布局

以下是完成面包板构建的步骤。步骤编号与图 11.10中的黑色圆圈中的数字相匹配:

  1. 将您的 A3144 霍尔效应传感器放入面包板中,注意其腿部的方向。如果需要帮助识别元件的腿部,请参考图 11.8

  2. 将 1kΩ电阻(R1)放入面包板中。

  3. 将 2kΩ电阻(R2)放入面包板中。这第二个电阻的一个腿与第一个电阻的一个腿共用一行。在插图中,这可以在左侧银行的第 17 行看到。

  4. 将树莓派的 5V 引脚连接到左侧电源轨道的正极。

  5. 将树莓派的 GND 引脚连接到左侧电源轨道的负极。

  6. 将霍尔效应传感器的 Vcc 腿连接到正电源轨道。

  7. 将霍尔效应传感器的 GND 腿连接到负电源轨道。

  8. 将霍尔效应传感器的 Out 腿连接到 1kΩ电阻(R1)。在插图中,这显示在 E13 孔。

  9. 将 1kΩ(R1)和 2kΩ(R2)电阻的交汇处连接到树莓派的 GPIO 21。

  10. 将 2kΩ电阻(R2)的左侧连接到负电源轨道。

要在这个电路中使用 AH3503 比率型霍尔效应传感器,在步骤 1步骤 9的电线将需要连接到 ADS1115 模块的输入端口(例如 A0)。

现在我们已经建立了霍尔效应传感器电路,准备好一个磁铁,因为我们准备运行示例代码,看看磁铁如何触发传感器。

运行和探索霍尔效应传感器代码

您可以在chapter11/hall_effect_digital.py文件中找到开关和锁定开关类型霍尔效应传感器的代码,以及chapter11/hall_effect_analog.py文件中找到比率型霍尔效应传感器的代码。

当您查看这两个文件时,您会发现以下内容:

  • chapter11/hall_effect_digital.py在功能上与我们在本章前面介绍的 PIR 代码示例相同,标题为运行和探索 PIR 传感器代码。PIR 和非锁定/锁定霍尔效应传感器都是数字开关。唯一的区别是我们的参考霍尔效应传感器是活动LOW

  • chapter11/hall_effect_analog.py类似于我们在使用 ADS1115 ACD 的其他模拟到数字示例中看到的,包括来自第五章,将您的树莓派连接到物理世界的电路布线和代码。

AH3503 比率型霍尔效应传感器输出的变化电压范围,并通过电压分压器由您的 ADC 测量,可能在几百毫伏的范围内。

当您运行示例代码时,将磁铁移过霍尔效应传感器。磁铁需要靠近传感器的外壳;然而,它不需要实际接触传感器。有多取决于您的磁铁的强度。

如果您无法使电路和代码正常工作,请尝试旋转磁铁以改变通过传感器的南/北极。还要注意,对于闸锁型霍尔效应传感器,一个磁铁极常常会锁定(触发)传感器,而另一个磁铁极则会解锁(取消触发)传感器。

由于代码相似性,我们不会在这里再次介绍代码。但是,我想说的是,现在在本书中,您已经可以连接并使用任何简单的模拟或数字元件的数字和模拟基础电路和代码。正如本章已经指出的那样,只需注意所需的电压和电流来为元件供电,特别是输出电压是多少,因为如果超过 3.3 伏,您将需要使用电压分压器或电平转换器。

总结

在本章中,我们探讨了如何使用树莓派检测运动并估计距离。我们学会了如何使用 PIR 传感器检测广泛的运动,以及如何使用开关型霍尔效应传感器来检测磁场的运动。我们还发现了如何使用超声波测距传感器在较大范围上估计绝对距离,以及如何使用比例型霍尔效应传感器在小范围上测量相对距离。

本章中所有的电路和示例都是输入为主 - 告诉我们的树莓派发生了某些事件,比如检测到有人移动或正在测量距离。

现在你已经处于一个很好的位置,可以将本章中涵盖的输入电路(还有第九章中的内容,测量温度、湿度和光照),与第七章中的输出电路和示例,打开和关闭设备,第八章,灯光、指示灯和信息显示,以及第十章,使用舵机、电机和步进电机进行运动,结合起来,创建可以控制和测量环境的端到端项目!

不要忘记我们在第二章中学到的内容,使用 Python 和物联网入门,第三章,使用 Flask 进行 RESTful API 和 Web Sockets 网络,以及第四章,使用 MQTT、Python 和 Mosquitto MQTT Broker 进行网络。这三章为您提供了创建网页界面和集成到外部系统的基础,可以控制和监测环境。

到目前为止,在本书中呈现的许多电子和代码示例都围绕着单个传感器或执行器发展。在下一章中,我们将探索几种基于 Python 的设计模式,这些模式在构建涉及多个需要相互通信的传感器和/或执行器的更复杂的自动化和物联网项目时非常有用。

问题

最后,这里有一些问题供您测试对本章材料的了解。您将在本书的评估部分找到答案:

  1. PIR 传感器能否检测物体移动的方向?

  2. 有哪些因素会影响超声波距离传感器的测量精度?

  3. 闸锁型或非闸锁型霍尔效应传感器的输出与比例霍尔效应传感器的输出有何不同?

  4. 关于这个 PiGPIO 函数调用,callback = pi.callback(GPIO, pigpio.EITHER_EDGE, callback_handler)pigpio.EITHER_EDGE参数是什么意思?

  5. 在由 1kΩ和 2kΩ电阻组成的 5 伏到 3.3 伏基于电阻的电压分压器中,为什么在电路中连接两个电阻值的方式很重要?

  6. HC-SR04 超声波距离传感器和 HC-SR501 PIR 传感器都使用 5 伏电压连接到它们各自的 Vcc 引脚。为什么我们要使用电压分压器将 HC-SR04 的输出从 5 伏降到 3.3 伏,而不是 HC-SR501?

第十二章:高级 IoT 编程概念-线程、异步 IO 和事件循环

在上一章中,我们学习了如何使用 PIR 传感器检测运动,以及如何使用超声波传感器和霍尔效应传感器测量距离和检测运动。

在本章中,我们将讨论在处理电子传感器(输入设备)和执行器(输出设备)时,构建Python 程序的替代方式。我们将首先介绍经典的事件循环编程方法,然后转向更高级的方法,包括在 Python 中使用线程、发布者/订阅者模型,最后是使用 Python 进行异步 IO 编程。

我向您保证,互联网上有很多博客文章和教程涵盖了这些主题;然而,本章将专注于实际的电子接口。本章的方法将涉及创建一个简单的电路,其中包括一个按钮、一个电位计和两个 LED,我们将使它们以不同的速率闪烁,并提供四种不同的编码方法来使电路工作。

以下是本章将涵盖的内容:

  • 构建和测试我们的电路

  • 探索事件循环的方法

  • 探索线程化方法

  • 探索发布者-订阅者的替代方案

  • 探索异步 IO 的方法

技术要求

为了完成本章的练习,您需要以下内容:

  • 树莓派 4 型 B

  • Raspbian OS Buster(带桌面和推荐软件)

  • 最低 Python 版本 3.5

这些要求是本书中代码示例的基础。可以合理地期望,只要您的 Python 版本是 3.5 或更高,本书中的代码示例应该可以在树莓派 3 型 B 或不同版本的 Raspbian OS 上无需修改即可运行。

您可以在 GitHub 存储库的chapter12文件夹中找到本章的源代码,该存储库位于github.com/PacktPublishing/Practical-Python-Programming-for-IoT

需要在终端中执行以下命令来设置虚拟环境并安装本章所需的 Python 库:

$ cd chapter12              # Change into this chapter's folder
$ python3 -m venv venv      # Create Python Virtual Environment
$ source venv/bin/activate  # Activate Python Virtual Environment
(venv) $ pip install pip --upgrade        # Upgrade pip
(venv) $ pip install -r requirements.txt  # Install dependent packages

以下依赖项已从requirements.txt中安装:

本章练习所需的电子元件如下:

  • 2 x 红色 LED

  • 2 x 200 Ω 电阻

  • 1 x 按钮开关

  • 1 x ADS1115 模块

  • 1 x 10k Ω 电位计

为了最大限度地提高您在本章中的学习效果,对于预先存在的知识和经验做出了一些假设:

  • 从电子接口的角度来看,我假设您已经阅读了本书前面的 11 章,并且对本书中始终出现的 PiGPIO 和 ADS1115 Python 库的工作感到满意。

  • 从编程的角度来看,我假设您已经掌握了面向对象编程OOP)技术以及它们在 Python 中的实现。

  • 熟悉事件循环线程发布者-订阅者同步与异步范式的概念也将是有利的。

如果前述任何主题对您来说是陌生的,您会发现有很多在线教程详细介绍了这些主题。请参阅本章末尾的进一步阅读部分以获取建议。

构建和测试我们的电路

我将以实际练习的形式呈现本章的电路和程序。让我们假设我们被要求设计和构建一个具有以下要求的小玩意

  • 它有两个 LED 灯在闪烁。

  • 电位计用于调整 LED 的闪烁速率。

  • 程序启动时,两个 LED 将以由电位计位置确定的相同速率闪烁。

  • 0 秒的闪烁速率意味着 LED 关闭,而 5 秒的最大闪烁速率意味着 LED 打开 5 秒,然后关闭 5 秒,然后重复循环。

  • 按下按钮用于选择调整闪烁速率的 LED,当调整电位计时。

  • 当按下并保持按下按钮 0.5 秒时,所有 LED 将同步到相同的速率,由电位计的位置确定。

  • 理想情况下,程序代码应该很容易扩展,以支持更多 LED,而编码工作量很小。

以下是一个说明使用这个小玩意的场景:

  1. 应用电源后(程序启动),所有 LED 以 2.5 秒的速率开始闪烁,因为电位计的刻度在旋转的中点(50%)。

  2. 用户调整电位计,使第一个LED 以 4 秒的速率闪烁。

  3. 接下来,用户简短地按下并释放按钮,以便电位计改变第二个LED 的闪烁速率。

  4. 现在,用户调整电位计,使第二个LED 以 0.5 秒的速率闪烁。

  5. 最后,用户按下并保持按钮 0.5 秒,使第一个第二个LED 以 0.5 秒的速率同步闪烁(由步骤 4中电位计设置的速率)。

现在是我提到的挑战 - 在我们进入本章的电路和代码之前,我挑战您停止阅读,尝试创建一个实现上述要求的电路并编写程序。

您可以在youtu.be/seKkF61OE8U上找到演示这些要求的短视频。

我预计您会遇到挑战,并对采取的最佳方法有疑问。没有最佳方法;然而,通过拥有自己的实现 - 无论是否有效 - 您将有东西可以与我在本章中将提出的四种解决方案进行比较和对比。我相信,如果您首先自己尝试一下,那么您将获得更深入的理解和更多的见解。也许您会创造出更好的解决方案!

如果您需要建议来帮助您入门,这里有一些建议:

  • 我们在《使用 Python 和物联网入门》的第二章中首次介绍了 LED 和按钮。

  • 我们首先在《将树莓派连接到物理世界》的第五章中介绍了电位计和模拟输入,使用了 ADS1115 模块。

当您准备好时,我们将看一个满足上述要求的电路。

构建参考电路

图 12.1中是一个符合我们刚列出的要求的电路。它有一个按钮,一个电位计,以电压分压器的形式连接到 ADS1115 模数转换器,和两个通过限流电阻连接的 LED。添加额外的 LED 将像在 GND 和一个空闲的 GPIO 引脚之间布线更多的 LED 和电阻对一样简单:

图 12.1 - 参考电路原理图

如果您还没有在面包板上创建类似的电路,我们现在将在您的面包板上创建这个电路。我们将分三部分构建这个电路。让我们开始吧:

图 12.2 - 参考电路(3 部分之一)

以下是创建我们的面包板构建的第一部分的步骤。步骤编号与图 12.2中黑色圆圈中的数字相匹配:

  1. 将 ADS1115 模块放入面包板中。

  2. 将电位计放入面包板中。

  3. 将 LED 放入面包板中,注意 LED 的引脚方向如图所示。

  4. 将第二个 LED 放入面包板中,注意 LED 的引脚方向如图所示。

  5. 将一个 200Ω电阻(R1)放入您的面包板中。这个电阻的一端与步骤 3中放置的 LED 的阳极腿共用一行。

  6. 将另一个 200Ω电阻(R2)放入您的面包板中。这个电阻的一端与步骤 5中放置的第二个 LED 的阳极腿共用一行。

  7. 将按键放入您的面包板中。

现在我们已经将组件放入面包板中,让我们开始将它们连接起来:

图 12.3 - 参考电路(2/3 部分)

以下是继续进行面包板组装的步骤。步骤编号与图 12.3中的黑色圆圈中的编号相匹配:

  1. 将树莓派的 3.3 伏特引脚连接到左侧电源轨的正电源轨。

  2. 将 ADS1115 的 Vdd 端连接到左侧电源轨的正电源轨。

  3. 将 ADS1115 的 GND 端连接到左侧电源轨的负电源轨。

  4. 将 ADS1115 的 SCL 端连接到树莓派的 SCL 引脚。

  5. 将 ADS1115 的 SDA 端连接到树莓派的 SDA 引脚。

  6. 将树莓派上的 GND 引脚连接到左侧电源轨的负电源轨。

  7. 将电位器的外端连接到左侧电源轨的正电源轨。

  8. 将电位器的另一个外端连接到左侧电源轨的负电源轨。

  9. 将电位器的中间端口连接到 ADS1115 的 A0 端口。

您是否记得,这种配置中的电位器正在创建一个可变电压分压器?如果没有,您可能需要重新阅读第六章,软件工程师的电子学 101。此外,如果您想对 ADS1115 模块进行详细复习,请参阅第五章,将树莓派连接到物理世界

让我们继续组装:

图 12.4 - 参考电路(3/3 部分)

以下是继续进行面包板组装的最后部分的步骤。步骤编号与图 12.4中的黑色圆圈中的编号相匹配:

  1. 将树莓派的 GPIO 26 连接到 200Ω电阻(R1)。

  2. 将树莓派的 GPIO 19 连接到第二个 200Ω电阻(R2)。

  3. 将树莓派的 GPIO 21 连接到按键的一腿。

  4. 将两个 LED 的阴极腿连接在一起。

  5. 将 LED 的阴极腿连接到左侧电源轨的负电源轨。

  6. 将按键的第二腿连接到左侧电源轨的负电源轨。

现在我们已经完成了电路组装,我们准备运行示例代码使电路工作。

运行示例

本章提供了四个不同版本的代码,可以与图 12.1中先前显示的电路配合使用。您将在chapter12文件夹中按版本组织的代码:

  • chapter12/version1_eventloop是一个基于事件循环的示例。

  • chapter12/version2_thread是一个基于线程和回调的示例。

  • chapter12/version3_pubsub是一个基于发布者-订阅者的示例。

  • chapter12/version4_asyncio是一个异步 IOAsyncIO)*-based 示例。

所有版本在功能上是等效的;但是它们在代码结构和设计上有所不同。在测试电路后,我们将更详细地讨论每个版本。

以下是运行每个版本(从版本 1 开始)并测试电路的步骤:

  1. 切换到version1_eventloop文件夹。

  2. 简要查看main.py源文件,以及文件夹中的任何其他 Python 文件,了解它们包含的内容以及程序的结构。

  3. 在终端中运行main.py(记得先切换到本章的虚拟环境)。

此时,如果您收到关于 I2C 或 ADS11x5 的错误,请记住有一个i2cdetect工具,可以用来确认 I2C 设备(如 ADS1115)是否正确连接并对您的树莓派可见。有关更多信息,请参阅第五章,将您的树莓派连接到物理世界

  1. 转动电位器拨号并观察第一个LED 的闪烁速率变化。

  2. 短按按钮。

  3. 转动电位器拨号并观察第二个LED 的闪烁速率变化。

  4. 按住按钮 0.5 秒,观察两个 LED 现在以相同速率同步闪烁。

以下是您将收到的终端输出示例:

(venv) $ cd version1_eventloop
(venv) $ python main.py
INFO:Main:Version 1 - Event Loop Example. Press Control + C To Exit.
INFO:Main:Setting rate for all LEDs to 2.5
INFO:Main:Turning the Potentiometer dial will change the rate for LED #0
INFO:Main:Changing LED #0 rate to 2.6
INFO:Main:Changing LED #0 rate to 2.7 
INFO:Main:Turning the Potentiometer dial will change the rate for LED #1
INFO:Main:Changing LED #1 rate to 2.6
INFO:Main:Changing LED #1 rate to 2.5
# Truncated
INFO:Main:Changing LED #1 rate to 0.5
INFO:Main:Changing rate for all LEDs to 0.5
  1. 在终端中按*Ctrl *+ C退出程序。

  2. version2_threadsversion3_pubsubversion4_asyncio重复步骤 18

您刚刚测试并浏览了四种不同程序的源代码(也许五种,如果您挑战自己创建了自己的程序),它们都以不同的方式实现了完全相同的最终结果。

现在是时候了解这些程序是如何构建的了。让我们从程序的事件循环版本开始。

探索事件循环方法

我们将通过讨论基于事件循环的方法来开始我们的代码探索,以构建我们在上一节中测试过的示例小玩意。

基于事件循环的方法的代码可以在chapter12/version1_eventloop文件夹中找到。您会找到一个名为main.py的文件。现在请花时间停下来阅读main.py中的代码,以基本了解程序的结构和工作原理。或者,您可以在代码中添加断点或插入print()语句,然后再次运行它以了解其工作原理。

您的体验如何,您注意到了什么?如果您认为或者在循环、if语句和状态变量的网络中迷失了,那么干得好!这意味着您已经投入了时间来考虑这种方法以及代码的构造方式。

我所说的事件循环方法在代码中通过第 1 行的while True:循环进行了演示:

# chapter12/version1_eventloop
#
# Setup and initialization code goes before while loop.
#

if __name__ == "__main__":
    # Start of "Event Loop"
    while True:                                # (1)
      #
      # ... Main body of logic and code is within the while loop...
      #
      sleep(SLEEP_DELAY)

当然,我可以使用函数甚至外部类来减少while循环中的代码数量(可能还可以增强可读性),但是,总体设计范式仍然是相同的-程序控制的主体部分处于永久循环中。

如果您熟悉 Arduino 编程,您将对这种编程方法非常熟悉。这就是为什么我将本节标题为事件循环,因为这种方法和术语的流行度相似。尽管如此,请注意事件循环这个术语在 Python 中有更广泛的上下文,当我们查看程序的 AsyncIO(版本 4)时会看到。

您可能已经意识到,本书中许多示例都使用了这种事件循环编程方法。以下是三个示例:

  • 当我们需要定时事件,比如闪烁 LED 时(第二章,使用 Python 和物联网入门

  • DHT 11 或 DHT 22 温度/湿度传感器的轮询(第九章,测量温度、湿度和光照水平

  • 轮询连接到光敏电阻LDR)的 ADS1115 模拟数字转换器(也第九章,测量温度、湿度和光照水平

在这种情况下,对于一个单一的专注示例,事件循环是有意义的。甚至在你进行试验和学习新的执行器或传感器时,它们也是纯粹为了方便而有意义的。然而,正如我们的version1_eventloop/main.py程序所示,一旦你添加了多个组件(比如电位计、两个 LED 和一个按钮)并且想要让它们为一个明确的目的一起工作,代码就会迅速变得复杂。

例如,考虑一下第 3 行的以下代码,它负责让所有 LED 闪烁,并记住这个代码块在每次循环迭代中被评估一次,负责让每个 LED 闪烁:

    #
    # Blink the LEDs.
    #
    now = time()                                               # (3)
    for i in range(len(LED_GPIOS)):
        if led_rates[i] <= 0:
            pi.write(LED_GPIOS[i], pigpio.LOW) # LED Off.
        elif now >= led_toggle_at_time[i]:
            pi.write(LED_GPIOS[i], not pi.read(LED_GPIOS[i])) # Toggle LED
            led_toggle_at_time[i] = now + led_rates[i]

与纯粹的替代方案相比(类似于我们将在其他方法中看到的),一眼看去,它们显然更容易理解:

   while True:
      pi.write(led_gpio, not pi.read(led_gpio)) # Toggle LED GPIO High/Low
      sleep(delay)

如果你再考虑一下从第 2 行开始的以下代码块,它负责检测按钮按下,那么你会发现在实际的main.py文件中有将近 40 行代码,只是为了检测按钮的操作:

while True:
    button_pressed = pi.read(BUTTON_GPIO) == pigpio.LOW        # (2)

    if button_pressed and not button_held:
        # Button has been pressed.
        # ... Truncated ...
    elif not button_pressed:
        if was_pressed and not button_held:
            # Button has been released
            # ... Truncated ...
    if button_hold_timer >= BUTTON_HOLD_SECS and not button_held:
        # Button has been held down
        # ... Truncated ...

    # ... Truncated ...

你会发现有多个变量在起作用 - button_pressedbutton_heldwas_pressedbutton_hold_timer - 它们在每次while循环迭代中都被评估,并且主要用于检测按钮按住事件。我相信你会理解,像这样编写和调试这样的代码可能会很乏味和容易出错。

我们本可以使用PiGPIO 回调来处理while循环之外的按钮按下,或者使用 GPIO Zero 的Button类。这两种方法都有助于减少按钮处理逻辑的复杂性。同样,也许我们本可以混合使用 GPIO Zero 的LED类来处理 LED 的闪烁。然而,如果这样做,我们的示例就不会是一个纯粹基于事件循环的示例。

现在,我并不是说事件循环是一种不好或错误的方法。它们有它们的用途,是必需的,实际上,每当我们使用while循环或其他循环结构时,我们都会创建一个 - 所以基本理念无处不在,但这并不是构建复杂程序的理想方法,因为这种方法使它们更难理解、维护和调试。

每当你发现你的程序正在走这条事件循环的道路时,停下来反思一下,因为也许是时候考虑重构你的代码,采用不同的 - 更易维护的 - 方法,比如线程/回调方法,我们将在下面看到。

探索线程方法

现在我们已经探索了一个基于事件循环的方法来创建我们的程序,让我们考虑一种使用线程、回调和面向对象编程的替代方法,并看看这种方法如何改进了代码的可读性和可维护性,并促进了代码的重用。

基于线程的方法的代码可以在chapter12/version2_threads文件夹中找到。你会找到四个文件 - 主程序main.py和三个类定义:LED.pyBUTTON.pyPOT.py

现在请花点时间停下来阅读main.py中包含的代码,以基本了解程序的结构和工作原理。然后,继续查看LED.pyBUTTON.pyPOT.py

它是如何进行的,你注意到了什么?我猜想你会发现这个程序的版本(在阅读main.py时)更快更容易理解,并且注意到没有繁琐复杂的while循环,而是一个pause()调用,这是必要的,用于阻止我们的程序退出,如第 3 行总结的那样:

# chapter12/version2_threads/main.py
if __name__ == "__main__":                                       # (3)
        # Initialize all LEDs
        # ... Truncated ...

        # No While loop!
        # It's our BUTTON, LED and POT classes and the 
        # registered callbacks doing all the work.
        pause()

在这个程序示例中,我们使用了面向对象的技术,并使用了三个类来组件化我们的程序:

  • 一个按钮类(BUTTON.py),负责所有按钮逻辑

  • 一个电位计类(POT.py),负责所有电位计和模拟数字转换逻辑

  • 一个 LED 类(LED.py),负责让单个LED 闪烁

通过使用面向对象的方法,我们的main.py代码大大简化了。它的作用现在是创建和初始化类实例,并包含使我们的程序工作的回调处理程序和逻辑。

考虑一下我们的按钮的面向对象的方法:

# chapter12/version2_threads/main.py
# Callback Handler when button is pressed, released or held down.
def button_handler(the_button, state):
    global led_index
    if state == BUTTON.PRESSED:                                 # (1)
        #... Truncated ...
    elif state == BUTTON.HOLD:                                  # (2)
        #... Truncated 

# Creating button Instance
button = BUTTON(gpio=BUTTON_GPIO,
               pi=pi,
               callback=button_handler)

与事件循环示例中的按钮处理代码相比,这大大简化了并且更易读——很明显这段代码在第 1 行响应按钮按下,第 2 行响应按钮保持。

让我们考虑一下BUTTON类,它在BUTTON.py文件中定义。这个类是一个增强的包装器,可以将按钮的 GPIO 引脚的HIGH/LOW状态转换为PRESSEDRELEASEDHOLD事件,如在BUTTON.py的第 1 行中总结的代码所示:

# chapter12/version2_threads/BUTTON.py
def _callback_handler(self, gpio, level, tick): # PiGPIO Callback  # (1)

     if level == pigpio.LOW: # level is LOW -> Button is pressed
         if self.callback: self.callback(self, BUTTON.PRESSED)

         # While button is pressed start a timer to detect
         # if it remains pressed for self.hold_secs
         timer = 0                                                 # (2)
         while (timer < self.hold_secs) and not self.pi.read(self.gpio):
             sleep(0.01)
             timer += 0.01

         # Button is still pressed after self.hold_secs
         if not self.pi.read(self.gpio):                
             if self.callback: self.callback(self, BUTTON.HOLD)

     else: # level is HIGH -> Button released            
         if self.callback: self.callback(self, BUTTON.RELEASED)

与事件循环示例中的按钮处理代码相比,我们没有引入和审问多个状态变量来检测按钮保持事件,而是将这个逻辑简化为在第 2 行的简单线性方法。

接下来,当我们考虑POT类(在POT.py中定义)和LED类(在LED.py中定义)时,我们将看到线程进入我们的程序。

您知道即使在多线程的 Python 程序中,也只有一个线程在活动吗?虽然这似乎违反直觉,但这是 Python 语言最初创建时做出的一个称为全局解释器锁GIL)的设计决定。如果您想了解更多关于 GIL 和使用 Python 实现并发的其他形式的信息,您可以在本章的进一步阅读部分找到相关资源。

以下是POT类的线程运行方法,可以在POT.py源文件中找到,从第 1 行开始说明了中间轮询 ADS1115 ADC 以确定电位器位置的方法。我们已经在本书中多次看到这个轮询示例,最早是在第五章中,将您的树莓派连接到物理世界,我们首次讨论模数转换、ADS1115 模块和电位器:

    # chapter12/version2_threads/POT.py
    def run(self):   
        while self.is_polling:                              # (1)
            current_value = self.get_value()  
            if self.last_value != current_value:            # (2)
                if self.callback:
                    self.callback(self, current_value)      # (3)
                self.last_value = current_value

            timer = 0  
            while timer < self.poll_secs:  # Sleep for a while
                sleep(0.01)
                timer += 0.01

        # self.is_polling has become False and the Thread ends.
        self.__thread = None

我们这里的代码不同之处在于我们正在监视 ADC 上的电压变化(例如,当用户转动电位器时),并将其转换为回调(在第 3 行),您在审查该文件中的源代码main.py时会看到。

现在让我们讨论一下我们如何实现version2 LED 相关的代码。正如您所知,闪烁 LED 的基本代码模式涉及while循环和sleep语句。这就是 LED 类中采用的方法,如LED.py中第 3 行的run()方法中所示:

# chapter12/version2_threads/LED.py
 def run(self):                                                    # (3)
     """ Do the blinking (this is the run() method for our Thread) """
     while self.is_blinking:
         # Toggle LED On/Off
         self.pi.write(self.gpio, not self.pi.read(self.gpio))

         # Works, but LED responsiveness to rate chances can be sluggish.
         # sleep(self.blink_rate_secs)

         # Better approach - LED responds to changes in near real-time.
         timer = 0
         while timer < self.blink_rate_secs:
             sleep(0.01)
             timer += 0.01

     # self.is_blinking has become False and the Thread ends.
     self._thread = None

我相信您会同意这比我们在前一节讨论的事件循环方法更容易理解。然而,重要的是要记住,事件循环方法是在单个代码块中使用和改变所有LED 的闪烁速率,并在单个线程——程序的主线程中进行的。

请注意前面代码中显示的两种睡眠方法。虽然使用 sleep(self.blink_rate_secs) 的第一种方法很常见且诱人,但需要注意的是它会阻塞线程,使其在整个睡眠期间无法立即响应速率变化,当用户转动电位器时会感觉迟钝。第二种方法,称为 #Better approach,缓解了这个问题,使 LED 能够(近乎)实时地响应速率变化。

我们的version2程序示例使用 LED 类及其自己的内部线程,这意味着我们现在有多个线程——每个 LED 一个——都独立地使 LED 独立地闪烁。

你能想到这可能引入的任何潜在问题吗?好吧,如果你已经阅读了version2源文件,这可能是显而易见的——当按钮按下 0.5 秒时,同步所有 LED 以同样的速率同时闪烁!

通过引入多个线程,我们引入了多个定时器(即sleep()语句),因此每个线程都在自己独立的时间表上闪烁,而不是从一个共同的参考点开始闪烁。

这意味着,如果我们简单地在多个 LED 上调用led.set_rate(n),虽然它们都会以速率n闪烁,但它们不一定会同步闪烁。

解决这个问题的一个简单方法是在开始以相同速率闪烁之前同步关闭所有 LED。也就是说,我们从一个共同的状态(即关闭)开始让它们一起闪烁。

这种方法在LED.py的第 1 行开始的以下代码片段中显示。同步的核心是在第 2 行的led._thread.join()语句中实现的:

    # chapter12/version2_threads/LED.py
    @classmethod                                           # (1)
    def set_rate_all(cls, rate):
        for led in cls.instances: # Turn off all LEDs.
            led.set_rate(0)

        for led in cls.instances:                        
            if led._thread:
                led._thread.join()                         # (2)

        # We do not get to this point in code until all 
        # LED Threads are complete (and LEDS are all off)

        for led in cls.instances:  # Start LED's blinking
            led.set_rate(rate)

这是同步的一个很好的第一步,对于我们的情况来说,实际上效果很好。正如前面提到的,我们所做的就是确保我们的 LED 从关闭状态同时开始闪烁(嗯,非常非常接近同时,取决于 Python 迭代for循环所花费的时间)。

尝试将前面代码中第 2 行的led._thread.join()和包含的for循环注释掉,然后运行程序。让 LED 以不同的速率闪烁,然后尝试通过按住按钮来同步它们。它总是有效吗?

但必须指出的是,我们仍然在处理多个线程和独立的定时器来让我们的 LED 闪烁,因此存在时间漂移的可能性。如果这曾经成为一个实际问题,那么我们将需要探索替代技术来同步每个线程中的时间,或者我们可以创建并使用一个单一的类来管理多个 LED(基本上使用事件循环示例中的方法,只是将其重构为一个类和一个线程)。

关于线程的要点是,当您将线程引入应用程序时,您可能会引入可能可以设计或同步的时间问题。

如果你的原型或新程序的第一次尝试涉及基于事件循环的方法(就像我经常做的那样),那么当你将代码重构为类和线程时,始终要考虑可能出现的任何时间和同步问题。在测试期间意外发现与同步相关的错误(或更糟糕的是,在生产中)是令人沮丧的,因为它们很难可靠地复制,并且可能导致需要进行大量的重做。

我们刚刚看到了如何使用面向对象编程技术、线程和回调创建样本小工具程序。我们已经看到了这种方法导致了更容易阅读和维护的代码,同时也发现了需要同步线程代码的额外要求和工作。接下来,我们将看一下我们的程序的第三种变体,它是基于发布-订阅模型的。

探索发布-订阅的替代方法

现在我们已经看到了使用线程、回调和面向对象编程技术创建程序的方法,让我们考虑第三种方法,使用发布-订阅模型。

发布-订阅方法的代码可以在chapter12/version3_pubsub文件夹中找到。你会找到四个文件——主程序main.py和三个类定义:LED.pyBUTTON.pyPOT.py

现在请花时间停下来阅读main.py中包含的代码,以基本了解程序的结构和工作原理。然后,继续查看LED.pyBUTTON.pyPOT.py

你可能已经注意到,整体程序结构(特别是类文件)与我们在上一个标题中介绍的version2线程/回调示例非常相似。

你可能也意识到,这种方法在概念上与 MQTT 采用的发布者/订阅者方法非常相似,我们在第四章中详细讨论了 MQTT、Python 和 Mosquitto MQTT Broker 的网络。主要区别在于,在我们当前的version3示例中,我们的发布者-订阅者上下文仅限于我们的程序运行时环境,而不是网络分布式程序集,这是我们 MQTT 示例的情况。

我已经使用PyPubSub Python 库在version3中实现了发布-订阅层,该库可以从pypi.org获取,并使用pip安装。我们不会详细讨论这个库,因为这种类型的库的整体概念和使用应该已经很熟悉了,如果没有,我相信一旦你审查了version3源代码文件(如果你还没有这样做),你会立刻明白发生了什么。

Python 通过 PyPi.org 提供了其他可选的 PubSub 库。选择在这个例子中使用PyPubSub是因为它的文档质量和提供的示例。你会在本章开头的技术要求部分找到这个库的链接。

由于version2(线程方法)和version3(发布者-订阅者方法)示例的相似性,我们不会详细讨论每个代码文件,只是指出核心差异:

  • version2(线程)中,这是我们的ledbuttonpot类实例之间的通信方式:

  • 我们在main.py上注册了buttonpot类实例的回调处理程序。

  • buttonpot通过这种回调机制发送事件(例如按钮按下或电位器调整)。

  • 我们直接使用set_rate()实例方法和set_rate_all()类方法与 LED 类实例进行交互。

  • 在“version3”(发布者-订阅者)中,这是类内通信结构和设计:

  • 每个类实例都是非常松散耦合的。

  • 没有回调。

  • 在类实例创建并注册到PyPubSub之后,我们不再直接与任何类实例进行交互。

  • 所有类和线程之间的通信都是使用PyPubSub提供的消息层进行的。

现在,说实话,我们的小玩意程序并不从发布者-订阅者方法中受益。我个人偏好采用回调版本来处理这样一个小程序。然而,我提供了发布者-订阅者的替代实现作为参考,这样你就有这个选择来考虑你自己的需求。

发布者-订阅者方法在更复杂的程序中表现出色,其中有许多组件(这里指的是软件组件,不一定是电子组件)需要共享数据,并且可以以异步的发布者-订阅者方式进行。

我们在本章中以四个非常离散和专注的例子来展示编码和设计方法。然而,在实践中,当创建程序时,通常会将这些方法(以及其他设计模式)以混合的方式结合起来。记住,使用的方法或方法组合应该是对你所要实现的目标最有意义的。

正如我们刚刚讨论过的,你在审查version3代码时会看到,我们的小玩意程序的发布者-订阅者方法是线程和回调方法的一个简单变体,我们不再直接使用回调与类实例交互,而是将所有代码通信标准化到一个消息层。接下来,我们将看看我们编写小玩意程序的最终方法,这次采用 AsyncIO 方法。

探索 AsyncIO 方法

到目前为止,在本章中,我们已经看到了三种不同的编程方法来实现相同的最终目标。我们的第四种和最终方法将使用 Python 3 提供的 AsyncIO 库构建。正如我们将看到的,这种方法与我们以前的方法有相似之处和不同之处,并且还为我们的代码及其操作方式增加了一个额外的维度。

根据我的经验,第一次体验 Python 中的异步编程可能会感到复杂、繁琐和令人困惑。是的,异步编程有一个陡峭的学习曲线(在本节中我们只能勉强触及表面)。然而,当您学会掌握这些概念并获得实际经验时,您可能会开始发现这是一种优雅而优美的创建程序的方式!

如果您是 Python 中异步编程的新手,您将在进一步阅读部分找到精心策划的教程链接,以加深您的学习。在本节中,我打算为您提供一个专注于电子接口的简单工作的 AsyncIO 程序,您可以在学习更多关于这种编程风格时作为参考。

基于异步的方法的代码可以在chapter12/version4_asyncio文件夹中找到。您会找到四个文件 - 主程序main.py和三个类定义:LED.pyBUTTON.pyPOT.py

现在请花时间停下来阅读main.py中包含的代码,以基本了解程序的结构和工作原理。然后继续查看LED.pyBUTTON.pyPOT.py

如果您也是 JavaScript 开发人员 - 特别是 Node.js - 您可能已经知道 JavaScript 是一种异步编程语言;但是,它看起来和感觉起来与您在 Python 中看到的非常不同!我可以向您保证,原则是相同的。以下是它们感觉非常不同的一个关键原因 - JavaScript 是默认异步的。正如任何有经验的 Node.js 开发人员所知道的,我们经常不得不在代码中采取(通常是极端的)措施来使我们的代码部分表现出同步行为。对于 Python 来说,情况正好相反 - 它是默认同步的,我们需要额外的编程工作来使我们的代码部分表现出异步行为。

当您阅读源代码文件时,我希望您将我们的version4 AsyncIO 程序视为同时具有version1基于事件循环的程序和version2线程/回调程序的元素。以下是关键差异和相似之处的摘要:

  • 整体程序结构与version2线程/回调示例非常相似。

  • main.py的末尾,我们有几行新的代码,在这本书中我们以前没有见过 - 例如,loop = asyncio.get_event_loop()

  • version2程序一样,我们使用了面向对象编程技术将组件分解为类,这些类也有一个run()方法 - 但请注意这些类中没有线程实例,也没有与启动线程相关的代码。

  • 在类定义文件LED.pyBUTTON.pyPOT.py中,我们在run()函数中使用了asyncawait关键字,并在while循环中延迟了 0 秒 - 也就是说,asyncio.sleep(0) - 因此我们实际上并没有睡觉!

  • BUTTON.py中,我们不再使用 PiGPIO 回调来监视按钮被按下,而是在while循环中轮询按钮的 GPIO。

Python 3 的 AsyncIO 库随着时间的推移发生了显著的演变(并且仍在演变),具有新的 API 约定,更高级功能的添加和废弃的函数。由于这种演变,代码可能会很快地与最新的 API 约定过时,两个代码示例展示了相同的基本概念,但可能使用看似不同的 API。我强烈建议您浏览最新的 Python AsyncIO 库 API 文档,因为它将为您提供有关新旧 API 实践的提示和示例,这可能有助于您更好地解释代码示例。

我将通过以简化的方式引导您了解程序的高级程序流程来解释这个程序是如何工作的。当您能够掌握正在发生的一般情况时,您就已经在理解 Python 中的异步编程方面迈出了重要的一步。

您还会发现一个名为chapter12/version4_asyncio/main_py37.py的文件。这是我们程序的 Python 3.7+版本。它使用自 Python 3.7 以来可用的 API。如果您浏览这个文件,差异是清楚地被注释了。

main.py文件的末尾,我们看到以下代码:

if __name__ == "__main__":
       # .... truncated ....

        # Get (create) an event loop.
        loop = asyncio.get_event_loop()      # (1)

        # Register the LEDs.
        for led in LEDS:
            loop.create_task(led.run())      # (2)

        # Register Button and Pot
        loop.create_task(pot.run())          # (3)
        loop.create_task(button.run())       # (4)

        # Start the event loop.
        loop.run_forever()                   # (5)

Python 中的异步程序围绕着事件循环发展。我们在第 1 行创建了这个事件循环,并在第 5 行启动了它。我们将在稍后回到在第 2、3 和 4 行之间发生的注册。

这个异步事件循环的整体原则与我们的 version1 事件循环示例类似;但是,语义是不同的。两个版本都是单线程的,两组代码都会在循环中运行。在version1中,这是非常明确的,因为我们的主要代码体包含在外部的while循环中。在我们的异步version4中,这更加隐含,并且有一个核心的区别——如果编写正确,它是非阻塞的,并且很快我们会看到,这是类run()方法中await asyncio.sleep()调用的目的。

正如前面提到的,我们已经在第 2、3 和 4 行将我们的类run()方法注册到循环中。在第 5 行启动事件循环后,简化来看发生了以下情况:

  1. 第一个LED 的run()函数(在下面的代码中显示)被调用:
# version4_asyncio/LED.py
async def run(self):
    """ Do the blinking """
    while True:                                           # (1)
        if self.toggle_at > 0 and 
              (time() >= self.toggle_at):                 # (2)
            self.pi.write(self.gpio, not self.pi.read(self.gpio))
            self.toggle_at += self.blink_rate_secs

        await asyncio.sleep(0)                            # (3)
  1. 它进入第 1 行的while循环,并根据闪烁速率从第 2 行切换 LED 的开关状态。

  2. 接下来,它到达第 3 行,await asyncio.sleep(0),并让出控制。在这一点上,run()方法实际上被暂停了,另一个while循环迭代不会开始。

  3. 控制权转移到第二个LED 的run()函数,并且它通过它的while循环运行一次,直到达到await asyncio.sleep(0)。然后它让出控制。

  4. 现在,pot 实例的run()方法(在下面的代码中显示)获得了运行的机会:

async def run(self):
    """ Poll ADC for Voltage Changes """
    while True:
        # Check if the Potentiometer has been adjusted.
        current_value = self.get_value()
        if self.last_value != current_value:

            if self.callback:
                self.callback(self, current_value)

            self.last_value = current_value

        await asyncio.sleep(0)
  1. run()方法执行while循环的一个迭代,直到达到await asyncio.sleep(0)。然后它让出控制。

  2. 控制权转移到button实例的run()方法(部分显示在下面的代码中),它有多个await asyncio.sleep(0)语句:

async def run(self):
    while True:
        level = self.pi.read(self.gpio) # LOW(0) or HIGH(1)

        # Waiting for a GPIO level change.
        while level == self.__last_level:
            await asyncio.sleep(0)

            # ... truncated ...

            while (time() < hold_timeout_at) and \
                   not self.pi.read(self.gpio):
                await asyncio.sleep(0)

        # ... truncated ...
        await asyncio.sleep(0)
  1. 一旦按钮的run()方法达到任何await asyncio.sleep(0)的实例,它就会让出控制。

  2. 现在,我们所有注册的run()方法都有机会运行,所以第一个LED 的run()方法将再次控制并执行一个while循环迭代,直到达到await asyncio.sleep(0)。同样,在这一点上它让出控制,第二个LED 的run()方法再次获得运行的机会...这个过程一遍又一遍地继续进行,每个run()方法以轮流的方式获得运行的机会。

让我们解决一些可能会有问题的问题:

  • 那么按钮的run()函数和它的许多await asyncio.sleep(0)语句呢?

当在任何await asyncio.sleep(0)语句处让出控制时,函数就在这一点上让出。下一次run()按钮获得控制时,代码将从await asyncio.sleep(0)语句下面的下一个语句继续执行。

  • 为什么睡眠延迟为 0 秒?

等待零延迟睡眠是放弃控制的最简单方法(请注意,这是asyncio库的sleep()函数,而不是time库的sleep()函数)。然而,你可以await任何异步方法,但这超出了我们简单示例的范围。

我在这个例子中使用了零秒延迟,以简化解释程序的工作原理,但你也可以使用非零延迟。这只是意味着放弃控制的run()函数会在这段时间内休眠 - 直到这段时间过去,事件循环才会让它运行。

  • 那么asyncawait关键字呢?我怎么知道在哪里使用它们?

这当然需要练习;然而,这里有一些基本的设计规则:

    • 如果你要向事件循环注册一个函数(例如run()),那么这个函数必须以async关键字开头。
  • 任何async函数必须包含至少一个await语句。

编写和学习异步程序需要练习和实验。你将面临的一个最初的设计挑战是知道在哪里放置await语句(以及有多少个),以及你应该放弃控制多长时间。我鼓励你玩一下version4代码库,添加你自己的调试print()或日志语句,然后进行实验和调试,直到你对它如何组合在一起有了感觉。在某个时候,你会有那个“啊哈”时刻,那时,你刚刚打开了进一步探索 Python AsyncIO 库提供的许多高级功能的大门。

现在我们已经看到了异步程序在运行时的结构和行为,我想给你一些可以进行实验和思考的东西。

异步实验

让我们试一试。也许你想知道为什么version4(AsyncIO)有点像我们的version1(事件循环)代码,只是它已经重构成类,就像version2(线程)代码一样。那么,我们是否可以将version1 while循环中的代码重构成类,创建并调用一个函数(例如run())在while循环中,而不必理会所有的异步内容及其额外的库和语法?

让我们试试。你会在chapter12/version5_eventloop2文件夹中找到一个与此类似的版本。尝试运行这个版本,看看会发生什么。你会发现第一个 LED 会闪烁,第二个 LED 会一直亮着,按钮和电位器不起作用。

你能想出原因吗?

简单的答案是:在main.py中,一旦第一个 LED 的run()函数被调用,我们就会永远停留在它的while循环中!

调用sleep()(来自time库)不会放弃控制;它只是在下一个while循环迭代发生之前暂停 LED 的run()方法。

因此,这就是为什么我们说同步程序是阻塞的(不会放弃控制),而异步程序是非阻塞的(它们放弃控制并让其他代码有机会运行)的一个例子。

希望你喜欢我们探索了四种不同的构建电子接口程序的方法,以及我们不应该使用的方法。让我们通过回顾本章学到的内容来结束。

总结

在本章中,我们看了四种不同的 Python 程序与电子设备接口的结构方式。我们了解了一种基于事件循环的编程方法,两种基于线程的变体 - 回调和发布-订阅模型 - 最后看了一下异步编程的工作方式。

我们讨论的四个例子都在方法上非常具体和离散。虽然我们在讨论过程中简要讨论了每种方法的相对优势和缺点,但值得记住的是,在实践中,你的项目可能会使用这些方法的混合(可能还有其他方法),这取决于你试图实现的编程和接口目标。

在下一章中,我们将把注意力转向物联网平台,并讨论可用于构建物联网程序的各种选项和替代方案。

问题

最后,这里有一些问题供您测试本章内容的知识。您可以在书的“评估”部分找到答案:

  1. 发布者-订阅者模型何时是一个好的设计方法?

  2. Python GIL 是什么,对于经典线程有什么影响?

  3. 为什么纯事件循环通常不适合复杂的应用程序?

  4. 事件循环方法是一个坏主意吗?为什么?

  5. thread.join()函数调用的目的是什么?

  6. 您已经使用线程通过模拟数字转换器来轮询您的新模拟组件。然而,您发现您的代码对组件的变化反应迟缓。可能的问题是什么?

  7. 在 Python 中设计物联网或电子接口应用的优越方法是什么——使用事件循环、线程/回调、发布者-订阅者模型还是基于 AsyncIO 的方法?

进一步阅读

realpython.com网站提供了一系列优秀的教程,涵盖了 Python 中的并发编程,包括以下内容:

以下是来自官方 Python(3.7)API 文档的相关链接:

第十三章:物联网可视化和自动化平台

在上一章中,我们探讨了与电子设备接口的 Python 程序结构的替代方法。这包括事件循环方法,两种基于线程的方法,显示回调和发布-订阅模型的使用,以及异步 I/O 方法。

在本章中,我们将讨论您可以与树莓派一起使用的物联网和自动化平台。术语物联网平台自动化平台是非常广泛的概念,因此在本章中,我所指的是任何软件服务-基于云或本地安装-为您提供一个现成的生态系统,以创建强大,灵活和有趣的物联网项目。

我们的主要重点将放在If-This-Then-ThatIFTTT)自动化平台上,我怀疑你们中的许多人对此都有一些了解,并且 ThingSpeak 平台用于数据可视化。我选择了这两个服务,因为它们都提供免费的定价层,并且允许我们创建和探索简单的演示和示例,您可以在此基础上构建。但是,除此之外,我还将讨论一些我有经验的其他物联网和自动化平台,这些平台将使您能够构建更强大的物联网解决方案。

本章将涵盖以下主题:

  • 从树莓派触发 IFTTT Applet

  • 从 IFTTT Applet 操作您的树莓派

  • 使用 ThingSpeak 平台可视化数据

  • 其他物联网和自动化平台供进一步探索

让我们开始吧!

技术要求

要执行本章的练习,您将需要以下物品:

  • 树莓派 4 型 B

  • Raspbian OS Buster(带桌面和推荐软件)

  • Python 版本至少为 3.5

这些要求是本书中代码示例的基础。可以合理地期望代码示例应该可以在树莓派 3 型 B 或不同版本的 Raspbian OS 上无需修改即可工作,只要您的 Python 版本是 3.5 或更高。

您将在本书的 GitHub 存储库的chapter13文件夹中找到本章的源代码,该存储库位于此处:github.com/PacktPublishing/Practical-Python-Programming-for-IoT

您需要在终端中执行以下命令来设置虚拟环境并安装本章代码所需的 Python 库:

$ cd chapter13              # Change into this chapter's folder
$ python3 -m venv venv      # Create Python Virtual Environment
$ source venv/bin/activate  # Activate Python Virtual Environment
(venv) $ pip install pip --upgrade        # Upgrade pip
(venv) $ pip install -r requirements.txt  # Install dependent packages

以下依赖项将从requirements.txt中安装:

本章练习所需的电子元件如下:

  • 1 x DHT11(较低精度)或 DHT22(较高精度)温湿度传感器

  • 1 x 红色 LED

  • 电阻:

  • 1 x 200Ω 电阻

  • 1 x 10kΩ 电阻(可选)

从树莓派触发 IFTTT Applet

你们中的许多人可能已经熟悉If-This-Than-ThatIFTTT)网络服务(ifttt.com),在那里您可以创建称为Applets的简单工作流自动化链。Applet 响应一个网络服务的更改(This),然后触发另一个网络服务的操作(That)。

以下是一些 Applet 配置(称为*Recipes)的常见示例:

  • 每当特定的 Twitter 标签发布时,向自己发送电子邮件。

  • 在一天的特定时间打开或关闭智能灯泡。

  • 当您接近您的房子时,使用手机的 GPS 打开您的联网车库门。

  • 在电子表格中记录您在办公室的时间。

  • ...以及成千上万的其他示例!

正如我们将在本节和下一节中学到的那样,我们的 Raspberry Pi 可以承担ThisThat的角色,以触发 Applet 或响应触发的 Applet 执行操作。

以下是我们将在本节中涵盖的内容的可视化表示;即,使我们的 Raspberry Pi 承担 IFTTT 工作流程中This角色:

图 13.1 - Raspberry Pi 在 IFTTT Applet 工作流程中承担This角色

我们即将介绍的 Python 示例将监视当前温度(This),并在特定温度时请求特殊的 IFTTT Webhook URL。此 URL 请求将触发我们的 Applet,然后发送电子邮件(That)。在构建我们的第一个 IFTTT Applet 时,我们将很快更详细地讨论 Webhooks。

首先,我们需要创建和测试我们的示例电路,接下来我们将这样做。

创建温度监测电路

在本示例中,我们将重用我们在第九章中创建的 DHT11/DHT22 温度电路,测量温度、湿度和光照水平

我们需要做的是:

  1. 构建图 9.2中所示的电路。

  2. 将数据引脚连接到 GPIO 24(在第九章中,测量温度、湿度和光照水平,我们使用了 GPIO 21,但我们将在本章后面使用 GPIO 21 来控制 LED)。

一旦您建立了电路,我们就可以继续并构建我们的第一个 IFTTT Applet。

创建和配置 IFTTT Applet

要创建我们的 IFTTT Applet,我们需要遵循许多步骤。这些步骤中的许多步骤都很简单和通用,无论您创建的 Applet 类型如何。虽然我们将逐步介绍这些通用步骤,但我们不会详细介绍它们,因为我相信您完全能够理解在过程中发生了什么。相反,我们将专注于与集成我们的 Raspberry Pi 相关的 IFTTT 的独特步骤和部分。

请注意,ifttt.com/免费定价层限制了您可以同时拥有的 Applet 数量。在撰写本文时,最大值为三个活动 Applet。在本章和下一章中,我们将创建四个 Applet,因此您需要在进行下一章时至少将一个 Applet 存档到 IFTTT 上,以保持在 IFTTT 免费定价层上。

以下是我们需要遵循的步骤:

  1. 登录或创建您的 IFTTT 帐户。如果您还没有 IFTTT 帐户,请访问ifttt.com/join并按照屏幕上的说明操作。

我们在 IFTTT 网站ifttt.com上执行这些步骤。IFTTT 手机和平板应用程序的操作流程将不同。

  1. 登录到 IFTTT 后,点击您的个人资料头像图标(在下图中用方框标出)以显示菜单:

图 13.2 - 个人资料头像图标

  1. 接下来,点击个人资料菜单中的创建选项,如下所示:

图 13.3 - 个人资料菜单

  1. 接下来您将看到的页面是创建您自己的页面。在这里,点击IfThis之间的+图标:

图 13.4 - 创建您自己的页面 - 第一部分

  1. 现在,您将被要求选择一个服务。我们需要选择的服务与我们的 Raspberry Pi 集成的服务称为 WebHook 服务,如下所示:

图 13.5 - 选择服务页面

  1. 找到并识别 Webhook 服务后,点击 Webhooks 图标继续。

  2. 接下来您将看到的页面是选择触发器页面,如下截图所示。在这里,点击接收 Web 请求选项:

图 13.6 - 选择触发器页面

  1. 接下来,您将看到完成触发字段页面,如下所示:

图 13.7 - 完成触发字段页面

事件名称对于我们的树莓派集成非常重要。在我们即将介绍的 Python 代码中,我们必须确保代码中使用的事件名称与我们在此页面中输入的名称匹配。在我们的示例中,我们将事件命名为 RPITemperature。

  1. 在“事件名称”框中输入 RPITemperature,然后点击“创建触发器”按钮继续。

Webhook 的事件名称是其唯一标识符(用于您的 IFTTT 帐户)。如果您创建了许多 Webhooks,则需要使用不同的事件名称来区分它们。

  1. 接下来,您将再次看到“创建您自己”的页面。这一次,您会发现This现在是 Webhook 图标:

图 13.8 - 创建您自己的页面 - 第二部分

我们现在已经完成了配置 IFTTT Applet 的一半。现在我们已经配置了 Webhook 触发器,我们需要配置我们的动作,即发送电子邮件。创建电子邮件动作后,我们将重新访问 Webhook 触发器,并发现用于触发此 Webhook 事件的 URL 和参数。

  1. 接下来,在“然后”和“那个”之间点击“+”图标。您将看到选择动作服务页面。在此页面上,搜索电子邮件并点击电子邮件图标:

图 13.9 - 选择动作服务页面

  1. 当您看到下图所示的选择动作页面时,请选择“发送电子邮件”选项:

图 13.10 - 选择动作页面

  1. 接下来,您将看到完成动作字段页面。请填写主题和正文文本字段,如下截图所示。您将在本章后面找到此动作生成的示例电子邮件:

图 13.11 - 完成动作字段页面

在前面的屏幕截图中,您会注意到一些文本被灰色框包围;例如,“Value1”和“OccuredAt”。这些被称为ingredients,在触发 Applet 时会动态替换。正如我们很快将在代码中看到的那样,我们将用当前温度、湿度和消息分别替换 Value1、Value2 和 Value3。

  1. 填写主题和正文文本字段后,点击“创建动作”按钮。

  2. 最后,在审查和完成页面上点击“完成”按钮,如下所示:

图 13.12 - 审查和完成页面

恭喜!您刚刚创建了一个 IFTTT Applet,当我们使用树莓派触发它时,它会发送一封电子邮件。但是我们如何做到的呢?这就是我们将在下一节中了解的内容。

触发 IFTTT Webhook

现在我们已经创建了我们的 IFTTT Applet,我们需要采取一些步骤来学习如何触发我们的 Webhook。这些步骤归结为知道在 IFTTT 中导航到哪里以发现您的唯一 Webhook URL。

以下是我们需要遵循的步骤:

  1. 首先,我们需要导航到 Webhooks 页面。我们可以通过几种方式来做到这一点,我会让您自行决定采取哪种方式:
  • 或者,导航到此网页的步骤如下:

  1. 点击个人资料头像图标(如图 13.2中所示)。

  2. 在出现的菜单中,选择“My Services”项目(参见图 13.3)。

  3. 在出现的页面上,找到并点击“Webhooks”项目。

无论您选择哪条路径,您将看到下图所示的页面:

图 13.13 - Webhooks 页面

  1. 单击页面右上角的 Documentation 按钮。您将看到这里显示的 Webhook 文档页面:

图 13.14 - Webhook 文档页面请注意,在前面的示例页面中,我已经填写了和 JSON Body 字段,以便在我们的讨论中引用它们。您的字段将最初为空。

这个页面包含了我们需要的关键信息,以便将这个 Webhook 触发与我们的 Raspberry Pi 集成。这个页面的关键部分如下:

  • 您的密钥:这是您的帐户的 Webhook API 密钥,并且是您独特的 Webhook URL 的一部分。

  • GET 或 POST 请求 URL:您独特的 Webhook URL。您的 API 密钥和事件名称的独特组合是将 URL 与可触发的 IFTTT 事件关联起来的。要与我们的 Raspberry Pi 集成,这是我们需要构建和请求的 URL。我们将很快在代码中介绍这一点。

  • 事件名称:您想触发的事件的名称。

  • JSON 主体:每个可触发的 Webhook 可以包含最多三个以 JSON 格式呈现的数据参数,它们必须命名为 value1、value2 和 value3。

  • cURL 命令行示例:在终端中运行此示例以触发 RPITemperature 事件(您将收到一封电子邮件)。

  • 测试按钮:单击此按钮将触发 RPITemperature 事件(您将收到一封电子邮件)。

现在我们已经创建了 IFTTT Applet,并发现了在哪里找到 Webhook URL 以及它是如何构建的,我们现在可以深入研究将触发我们的 IFTTT Applet 的 Python 代码。

在 Python 中触发 IFTTT Applet

我们将要探索一个简单的应用程序,基于我们在第九章中首次看到的 DHT 11/DHT 22 电路和代码,测量温度、湿度和光照水平。您可以在chapter13/ifttt_dht_trigger_email.py文件中找到这段代码。

这段代码将使用 DHT 11 或 DHT 22 传感器监视温度,如果违反了预先配置的高温或低温阈值,代码将调用您的 IFTTT Webhook URL,然后会像下面的截图中显示的那样给您发送一封电子邮件。这对应于您在步骤 13中配置的电子邮件主题和正文文本:

图 13.15 - 示例 IFTTT 电子邮件

在我们运行示例应用程序代码之前,我们需要执行一些配置步骤。让我们来看一下:

  1. 打开chapter13/ifttt_dht_trigger_email.py文件进行编辑。

  2. 找到由第(1)和(2)行表示的以下代码段。确认您的 DHT 传感器连接到适当的 GPIO 引脚,并且根据您拥有的传感器使用正确的 DHT11 或 DHT22 实例:

# DHT Temperature/Humidity Sensor GPIO. GPIO = 24                                                     # (1)   # Configure DHT sensor - Uncomment appropriate line 
# based on the sensor you have. dht = DHT11(GPIO, use_internal_pullup=True, timeout_secs=0.5) # (2) 
#dht = DHT22(GPIO, use_internal_pullup=True, timeout_secs=0.5)
  1. 现在,找到以下代码段,由行(3)、(4)和(5)表示,并将USE_DEGREES_CELSIUSHIGH_TEMP_TRIGGERLOW_TEMP_TRIGGER变量更新为在您的位置有意义的值:
USE_DEGREES_CELSIUS = True # False to use Fahrenheit   # (3)
HIGH_TEMP_TRIGGER   = 20 # Degrees                     # (4)
LOW_TEMP_TRIGGER    = 19 # Degrees                     # (5)

当温度达到HIGH_TEMP_TRIGGER度或降至LOW_TEMP_TRIGGER度时,您的 IFTTT Applet 将被触发并发送电子邮件。设置高温和低温触发的原因是为了创建一个小的温度缓冲区,以防止代码在温度在单个值以上下波动时触发多封电子邮件。

  1. 接下来,找到从第 6 行开始的以下代码部分,并更新显示的详细信息 - 特别是您在上一节中识别的 IFTTT API 密钥,在步骤 2中:
EVENT = "RPITemperature"                    # (6)
API_KEY = "<ADD YOUR IFTTT API KEY HERE>"

这就是我们所有的配置。您会注意到第 7 行,这是我们使用我们的 API 密钥和事件名称构建 IFTTT Webhook URL 的地方:

URL = "https://maker.ifttt.com/trigger/{}/with/key/{}".format(EVENT, API_KEY) # (7)

文件中的其余代码轮询 DHT11 或 DHT22 传感器,将读数与HIGH_TEMP_TRIGGERHIGH_TEMP_TRIGGER值进行比较,如果温度已超过,构造一个requests对象并调用 IFTTT Webhook URL 来触发您的 Applet。我们不会在这里涵盖该代码,因为根据您之前使用 DHT11/DHT22 传感器和 Python requests库的经验,这应该是不言自明的。

配置好我们的代码后,是时候在终端中运行程序了。您将收到类似以下的输出:

(venv) $ python ifttt_dht_trigger_email.py
INFO:root:Press Control + C To Exit.
INFO:root:Sensor result {'temp_c': 19.6, 'temp_f': 67.3, 'humidity': 43.7, 'valid': True}
INFO:root:Sensor result {'temp_c': 20.7, 'temp_f': 69.3, 'humidity': 42.9, 'valid': True}
INFO:root:Temperature 20.7 is >= 20, triggering event RPITemperature
INFO:root:Response Congratulations! You've fired the RPITemperature event
INFO:root:Successful Request.

我们的示例还显示了当温度超过 20 度时触发 IFTTT Applet。

现在,我们使用我们的树莓派在This角色中触发 IFTTT Applet 完成了我们的 IFTTT 示例。我们所涵盖的基本流程说明了实现这一点有多容易!我们发送了一封电子邮件,但您可以按照相同的整体流程创建其他触发其他操作的 IFTTT 配方,例如打开智能灯和电器,向 Google 电子表格添加行,以及创建 Facebook 帖子。您可能想要查看ifttt.com/discover以获取各种想法和可能性。请记住,从我们的角度和我们的学习来看,这是一个Webhook触发器,我们可以从我们的树莓派中使用它来实现这些想法。玩得开心!

接下来,我们将看看相反的情况,看看我们如何操作我们的树莓派。

从 IFTTT Applet 中操作您的树莓派

上一节教会了我们如何从我们的树莓派触发 IFTTT Applet。在本节中,我们将学习如何从 IFTTT Applet 中操作我们的树莓派。

对于我们的示例,我们将创建一个 IFTTT Applet,当收到电子邮件时将触发该 Applet。我们将使用此电子邮件的主题来控制连接到 GPIO 引脚的 LED。

我们将使用 IFTTT Webhook 服务,就像以前一样,只是这次 Webhook 服务将安装在我们 Applet 的That一侧,并且将请求我们指定的 URL。这个基本想法在下图中有所说明:

图 13.16 - 树莓派在 IFTTT Applet 中扮演That角色

让我们看看我们可以使用的两种可能的方法,通过 IFTTT Webhook 服务请求一个 URL,然后可以被我们的树莓派的 Python 代码看到。

方法 1 - 使用 dweet.io 服务作为中介

将 IFTTT 与我们的树莓派集成的一种方法是使用 dweet.io 服务。我们在第二章中介绍了 dweet.io 以及 Python 示例,使用 Python 和物联网入门

简而言之,我们将如何在 IFTTT Webhook 中使用 dweet.io 以及我们的 Python 代码:

  1. 在我们的 IFTTT Webhook 中,我们将使用 dweet.io URL 发布一个 dweet(包含打开、关闭或使 LED 闪烁的指令)。

  2. 我们的树莓派将运行 Python 代码来检索 IFTTT Webhook 发布的 dweet。

  3. 然后,我们的代码将根据 dweet 中指定的命令控制 LED。

这是我们示例中将要使用的方法。这种方法的优势在于我们无需担心在路由器上配置防火墙和端口转发规则。此外,这意味着我们可以在工作环境等环境中运行示例,而在这些环境中,路由器配置可能不切实际甚至不可能。

我们将在chapter13/dweet_led.py文件中使用此基于 dweet.io 的集成的代码,该文件是第二章中chapter02/dweet_led.py文件的精确副本,使用 Python 和物联网入门

方法 2 - 创建 Flask-RESTful 服务

要使用这种方法,我们需要创建一个类似于我们在第三章中所做的 RESTful 服务,使用 Flask 进行 RESTful API 和 Web 套接字服务chapter02/flask_api_server.py中的代码,它改变 LED 的亮度(而不是设置为开/关/闪烁),将是一个很好的起点)。

我们还需要将我们的树莓派暴露到公共互联网,这将需要我们在本地防火墙或路由器中打开一个端口并创建一个端口转发规则。然后,连同我们的公共 IP(或域名),我们可以构建一个 URL,并直接将其与 IFTTT Webhook 服务一起使用。

对于原型设计和创建演示,一个简单的替代方法是使用本地隧道(localtunnel.github.io/www)或 ngrok(ngrok.com)这样的服务,而不是打开防火墙和创建端口转发规则,这些服务可以帮助您将设备暴露到互联网上。

由于这种方法需要您进行配置和设置,这超出了我们作为本章的一部分可以实际完成的范围,因此我们将坚持使用前一节中显示的 dweet.io 方法。

接下来,我们将创建一个电路,我们可以在第二个 IFTTT Applet 中使用,我们将很快构建它。

创建 LED 电路

我们即将介绍的示例将需要一个 LED,以及连接到 GPIO 引脚(对于我们的示例是 GPIO 21)的一系列电阻。我相信,考虑到我们在本书中已经多次构建了 LED 电路,你可以毫无问题地自己完成这个连接!(如果你需要提醒,请参见第二章中的图 2.7使用 Python 和物联网入门

保留您为我们第一个 IFTTT Applet 示例创建的 DHT 11/DHT 22 电路,因为我们将在本章后面再次重用此电路。

当您的电路准备好后,我们将继续并运行我们的示例程序。

运行 IFTTT 和 LED Python 程序

在本节中,我们将运行我们的程序,并获取用于 dweet.io 服务的唯一物名称和 URL。

以下是要遵循的步骤:

  1. 在终端中运行chapter13/dweet_led.py文件中的代码。您将收到类似以下内容的输出(您的物名称和因此您的 URL 将不同):
(venv) $ python dweet_led.py
INFO:main:Created new thing name 749b5e60
LED Control URLs - Try them in your web browser:
  On : https://dweet.io/dweet/for/749b5e60?state=on
  Off : https://dweet.io/dweet/for/749b5e60?state=off
  Blink : https://dweet.io/dweet/for/749b5e60?state=blink

正如我们之前提到的,chapter13/dweet_led.py是我们在第二章中讨论的相同程序的精确副本,使用 Python 和物联网入门。如果您需要更多关于这个程序如何工作的上下文,请重新阅读那一章和其中包含的代码讨论。

  1. 保持终端打开并运行程序,因为我们将需要在下一节中复制其中一个 URL。我们还需要运行程序来测试我们即将进行的集成。

接下来,我们将创建另一个 IFTTT Applet,通过 dweet.io 与该程序集成。

创建 IFTTT Applet

我们即将创建另一个 IFTTT Applet。整个过程与我们之前创建的 Applet 非常相似,只是我们的树莓派(通过 Webhook 集成)将位于 Applet 的That端,如图 13.16所示。

以下是我们需要遵循的步骤来创建我们的下一个 Applet。由于它们与我们之前创建的 IFTTT Applet 过程非常相似,这次我省略了许多常见的屏幕截图:

  1. 登录到 IFTTT 后,点击个人资料头像图标,然后从下拉菜单中选择创建。

  2. 在 If + This Then Than 页面上,点击+图标。

  3. 在“选择服务”页面上,搜索并选择“电子邮件”服务。

  4. 在选择触发器页面上,选择发送带标签的 IFTTT 电子邮件(确保选项中包含这个词*tagged)。

  5. 在下一页中,输入 LED 作为标签输入,然后点击创建触发器按钮:

图 13.17 - 完成触发字段页面

  1. 在 If This Then + Than 页面上,点击+图标。

  2. 在选择操作服务页面上,搜索并选择 Webhooks 服务。

  3. 接下来,在选择操作页面上,选择进行网络请求。

  4. 接下来你会遇到的页面叫做“完成操作字段”。这是我们将使用上一节中程序打印到终端的 dweet URL 的地方:

图 13.18 - 完成操作字段页面

以下是您需要遵循的子步骤,以完成此页面上的字段:

    1. 从终端复制 On URL(例如,https://dweet.io/dweet/for/749b5e60?state=on - 注意您的thing name将不同)。
  1. 将此 URL 粘贴到 IFTTT URL 字段中。

  2. 在 URL 字段中,删除单词 on(所以 URL 现在是 https://dweet.io/dweet/for/749b5e60?state=)。

  3. 点击添加成分按钮(在 URL 字段下),选择主题(使 URL 现在为 https://dweet.io/dweet/for/749b5e60?state={{Subject}})。

  4. 其他字段可以保留为默认值。

  5. 点击创建操作按钮:

图 13.19 - 完成操作字段页面

  1. 最后,在“审查和完成”页面上,点击“完成”按钮。

干得好!我们现在创建了第二个 Applet。接下来,我们将使用这个 Applet 通过发送电子邮件来控制我们的 LED,指示 LED 打开、关闭或闪烁。

从电子邮件控制 LED

现在我们已经创建了一个 Applet 来通过电子邮件控制 LED,是时候测试集成了。

以下是创建电子邮件的步骤:

  1. 确保chapter13/dweet_led.py文件中的程序仍在终端中运行。

  2. 打开您喜欢的电子邮件程序并创建新邮件。

  3. 使用trigger@applet.ifttt.com作为电子邮件的收件人地址。

当向 IFTTT 发送触发电子邮件时,它必须来自您在 IFTTT 中使用的相同电子邮件地址(您可以访问ifttt.com/settings来检查您的电子邮件地址)。

  1. 作为主题,使用以下内容之一来控制 LED:
  • #LED On

  • #LED Off

  • #LED Blink

IFTTT 会去掉#LED 标签,因此我们的dweet_led.py程序只会收到打开、关闭或闪烁的文本。在我们的 Python 代码中,前导空格被去掉。

以下截图显示了一个使 LED 闪烁的示例电子邮件:

13.20 - 触发电子邮件示例

  1. 发送电子邮件。

  2. 等一会儿,LED 将改变状态。

现在我们已经学会了如何通过电子邮件使用 IFTTT 来控制我们的 LED,让我们快速介绍一些故障排除技巧。

IFTTT 故障排除

如果您的 IFTTT Applets 似乎没有触发和执行操作,这里有一些故障排除途径供您探索和尝试:

  • dweet_led.py中,尝试以下操作:

  • 打开调试日志记录;例如,logger.setLevel(logging.DEBUG)

  • 更改源文件末尾附近的 dweet 检索方法。如果您正在使用stream_dweets_forever(),请尝试改用poll_dweets_forever(),因为它对瞬时连接问题更具弹性。

  • 在 IFTTT 网站上,您可以通过以下方式检查任何 Applet 的活动日志:

  1. 导航到个人资料菜单下的我的服务选项

  2. 选择一个服务(例如,Webhooks)

  3. 选择要检查的 Applet

  4. 点击设置按钮

  5. 点击查看活动按钮和/或尝试立即检查按钮

IFTTT 还有一个最佳实践页面,您可以在help.ifttt.com/hc/en-us/categories/115001569787-Best-Practices上了解更多关于该平台的信息。

在我们讨论了从树莓派触发 IFTTT Applet部分之后,对于 IFTTT 触发器,您可以采用我们刚刚介绍的相同的整体流程来执行您的树莓派,以便从任何 IFTTT 配方中采取行动。再次查看ifttt.com/discover以获取一些想法,这一次,请记住,从我们的角度来看,我们在 IFTTT 配方中使用Webhook操作来控制我们的树莓派。这里有一个例子-使用 Google 助手语音控制您的树莓派!哦,等一下-我们将在下一章第十四章中做到这一点-将所有内容联系在一起-物联网圣诞树!

我们已经探讨了如何以两种方式将我们的树莓派与 IFTTT 集成-作为This角色来触发 Applet,以及在That角色中,我们可以从触发的 Applet 中执行我们的树莓派。接下来,我们将看一种方法来创建一个物联网仪表板,我们可以用来可视化数据。

使用 ThingSpeak 平台可视化数据

我们刚刚学习了如何使用 IFTTT 平台创建简单的自动化。在本节中,我们将与 ThingSpeak 平台集成,以可视化显示我们将使用 DHT 11 或 DHT 22 传感器收集的温度和湿度数据。我们将使用我们在本章前面创建的 DHT 11/DHT 22 电路。

ThingSpeak(thingspeak.com)是一个数据可视化、聚合和分析平台。我们将专注于数据可视化方面,特别是如何将我们的树莓派集成到该平台中。

我选择在本节中以 ThingSpeak 为例的原因有几个-它简单易用,并且对于我们将要做的简单数据可视化来说,它是免费的。还有许多其他可用的可视化平台,它们都有自己独特的功能、定价结构和复杂性。我在其他物联网和自动化平台供进一步探索部分中包含了一些建议供您探索。

如果您希望更深入地探索聚合和分析功能,您可以通过搜索 ThingSpeak 找到许多优质的示例、教程和文档。作为建议,从au.mathworks.com/help/thingspeak开始您的调查。

我们将在下面的屏幕截图中看到我们将要创建的仪表板的一个示例。请注意标签栏中显示的通道设置和 API 密钥项目-我们将很快提到这些标签:

图 13.21-ThingSpeak 通道仪表板

在我们可以集成我们的树莓派并将数据发送到 ThingSpeak 之前,我们的第一站是为我们的集成配置平台。

配置 ThinkSpeak 平台

配置 ThinkSpeak 相对简单-事实上,这是我遇到的同类平台中最简单的之一。以下是我们需要遵循的步骤:

  1. 首先,您需要为自己创建一个 ThingSpeak 账户。访问他们的网站thingspeak.com,然后点击“注册”按钮。

  2. 一旦您创建了 ThinkSpeak 账户并登录到平台,您应该会登陆到“我的通道”页面;也就是thingspeak.com/channels

在 ThingSpeak 生态系统中,通道是我们存储数据、仪表板和可视化的虚拟位置。这类似于一个工作区。

  1. 接下来,我们需要点击“新通道”按钮创建一个新通道:

图 13.22-ThingSpeak 通道配置

在新通道页面上,输入以下详细信息:

    • 名称:环境数据(或您选择的任何名称)
  • 字段 1:温度

  • 字段 2:湿度

您可以将所有其他字段保留为默认值。

如果以后需要查看或更改通道设置,可以在通道设置选项卡中找到,如*图 13.19.*中所示。

  1. 填写完字段后,滚动到页面底部,然后单击“保存通道”按钮。您将看到一个类似于图 13.19的页面,只是没有数据,是空白的。

要在图 13.19中看到的两个表中添加两个表,请执行以下操作:

    1. 按“添加小部件”按钮。
  1. 选择“仪表”图标,然后按“下一步”。

  2. 在“配置小部件参数”对话框中,输入仪表的名称(例如温度),并选择适当的字段编号(温度为 Field1,湿度为 Field2)。

  3. 您可以根据需要调整和实验其他参数,以设置仪表的最大/最小范围、颜色和其他显示属性。

  4. 为第二个表重复该过程。

如果仪表(或图表)显示“字段值不可用”,不要担心。这是正确的,因为我们还没有向 ThingSpeak 发送任何温度或湿度数据。

  1. 现在,是时候获取 API 密钥和通道 ID 了,我们需要这些信息来配置即将到来的 Python 代码。点击 API 密钥选项卡:

图 13.21 - API 密钥选项卡

这是我们的 Python 程序需要收集的信息:

    • 写 API 密钥(因为我们将向平台写入数据)
  • 通道 ID(这在所有 ThinkSpeak 页面上都有,靠近顶部)

现在我们已经创建并配置了一个简单的 ThinkSpeak 通道,并收集了我们的 API 密钥和通道 ID,我们可以继续进行 Python 代码。

配置和运行 ThinkSpeak Python 程序

我们提供了两个样本程序,可以与 ThinkSpeak 集成。它们如下:

  • chapter13/thingspeak_dht_mqtt.py:一个使用 MQTT 将数据发送到 ThinkSpeak 通道的示例。

  • chapter13/thingspeak_dht_http.py:一个使用 Python 请求库进行 RESTful API 调用,将数据发送到 ThinkSpeak 通道的示例。

这两个程序的核心概念在早期章节中已经讨论过。供您参考,它们如下:

  • MQTT:我们在第四章中讨论了 Paho-MQTT 库,使用 MQTT、Python 和 Mosquitto MQTT 代理进行网络通信。本章的一个关键区别是,我们使用 Paho-MQTT 简化客户端包装器来发布 MQTT 消息,而不是完整的生命周期示例。

  • 我们在第二章中介绍了 RESTful API 和请求库,Python 和物联网入门

  • 有关 DHT 11/DHT 22 温湿度传感器的代码在第九章中已经涵盖了,测量温度、湿度和光照水平

让我们配置这些程序,运行它们,并在 ThingSpeak 中看到数据。我们将演示chapter13/thingspeak_dht_mqtt.py中提供的示例代码;但是,对于chapter13/thingspeak_dht_http.py,整个过程都是相同的:

  1. 打开chapter13/thingspeak_dht_mqtt.py文件进行编辑。

  2. 在文件顶部附近,找到以下代码,从第(1)行开始,并确认您的 DHT 传感器连接到正确的 GPIO 引脚,并且正确的传感器实例在代码中启用:

# DHT Temperature/Humidity Sensor
GPIO = 24                                                   # (1)
#dht = DHT11(GPIO, use_internal_pullup=True, timeout_secs=0.5)
dht = DHT22(GPIO, use_internal_pullup=True, timeout_secs=0.5)
  1. 接下来,找到以下代码段,从第(2)行开始,并使用您的 ThingSpeak 写 API 密钥、通道 ID 和时区进行更新。请注意,CHANNEL_ID仅在 MQTT 集成中使用(因此它不会出现在thingspeak_dht_http.py文件中):
# ThingSpeak Configuration
WRITE_API_KEY = "" # <<<< ADD YOUR WRITE API KEY HERE   # (2)
CHANNEL_ID = ""    # <<<< ADD YOUR CHANNEL ID HERE

# See for values https://au.mathworks.com/help/thingspeak/time-zones-reference.html
TIME_ZONE = "Australia/Melbourne"
  1. 保存文件并运行程序。您应该会收到类似以下内容的输出:
(venv) $ python thing_speak_dht_mqtt.py
INFO:root:Collecting Data and Sending to ThingSpeak every 600 seconds. Press Control + C to Exit
INFO:root:Sensor result {'temp_c': 25.3, 'temp_f': 77.5, 'humidity': 43.9, 'valid': True}
INFO:root:Published to mqtt.thingspeak.com
  1. 几秒钟后,您应该会在 ThingSpeak 仪表板上看到您的数据出现!

恭喜!通过这样,你已经创建了一个 ThingSpeak 仪表板来可视化树莓派收集的数据。对于许多监控物联网项目来说,可视化数据是一个经常的需求,无论是简单的指示器显示,比如表盘,还是生成历史图表来可视化趋势。你如何处理数据的可视化完全取决于你的需求;然而,所有这些需求共同的一点是,有许多现成的服务,如 ThingSpeak,可以帮助你实现这一点,而不是自己定制编码仪表板和可视化应用。

现在,我将用一个简短的讨论来结束这一章,讨论一些其他流行的物联网平台,你可能会喜欢在未来的项目中探索和使用。

其他物联网和自动化平台供进一步探索

到目前为止,在本章中,我们已经看到了 IFTTT 和 ThingSpeak 的运作方式,以及如何将它们与我们的树莓派集成。我们看到了如何使用 IFTTT 创建简单的工作流程,以及如何使用 ThingSpeak 可视化数据——两个非常不同的想法,但它们都是物联网平台。

这两个平台都非常强大,并提供了广泛的功能和可能性,超出了我们在一个章节中所能涵盖的范围,所以我鼓励你查阅它们的文档和示例,以提升你的学习。

还有许多其他可用的物联网平台、应用程序和框架。本节将根据我的经验提供一个简短的策划清单。它们都与本书的 Python 和树莓派主题很好地契合。

Zapier

我们已经看到了 IFTTT 的运作方式。在支持的服务方面,IFTTT 更加面向消费者,而且正如我们所见,我们受限于单个 This 触发器和单个 That 动作。

Zappier 在原则上与 IFTTT 非常相似,但更加注重商业,包括一系列服务和集成,这些在 IFTTT 中不可用(IFTTT 也有独特的服务和集成)。此外,Zapier 还能够触发事件和动作的更复杂工作流程。

你会发现重新实现本章中我们的两个 IFTTT 示例在 Zappier 中相对简单。

网站:zapier.com

IFTTT 平台

在本章中,我们使用 IFTTT 作为最终用户,并使用 Webhooks 进行集成。如果你是一家希望创建作为一流 IFTTT 服务公开的小工具的企业,那么你应该了解一下 IFTTT 平台。

网站:platform.ifttt.com

ThingsBoard 物联网平台

ThingsBoard 是一个开源的物联网平台,你可以在树莓派上下载和托管。从表面上看,它将允许你构建仪表板和数据可视化,就像我们在 ThingSpeak 中所做的那样。与 ThingSpeak 相比,你会发现 ThingsBoard 在创建你的第一个仪表板时有一个更陡的学习曲线;然而,你也会发现它提供了更广泛的小部件和自定义选项。此外,与只能消耗数据的 ThingSpeak 不同,ThingsBoard 允许你将控件嵌入到仪表板中,让你使用 MQTT 与你的树莓派进行交互。

根据经验,如果你想学习如何使用这个平台,那么你必须仔细阅读 ThingsBoard 的文档和教程(许多都是视频),因为在你第一次访问其用户界面时,不会立即明显你需要做什么。

以下是他们网站上的一些具体资源:

在入门指南中没有 Python 特定的示例,但有 Mosquito MQTT 示例和 cURL 示例,演示了 RESTful API。建议使用本章中提供的两个 ThingSpeak 代码示例作为起点,并采用它们来使用 ThingBoard 特定的 MQTT 和/或 RESTful API。

网站:thingsboard.io

Home Assistant

Home Assistant 是一个纯 Python 家庭自动化套件。Home Assistant 可以与各种互联网设备连接,如灯、门、冰箱和咖啡机 - 仅举几例。

Home Assistant 在这里得到提及,不仅因为它是用 Python 构建的,而且因为它允许我们直接与主机树莓派的 GPIO 引脚集成,以及使用 PiGPIO 的远程 GPIO 功能与远程树莓派的 GPIO 引脚集成。此外,还有 MQTT 和 RESTful API 集成选项。

虽然在概念和最终用户操作上很简单,但是在配置 Home Assistant 时存在较高的学习曲线(需要相当多的实验),因为大多数集成是通过直接编辑YAML Ain't Markup LanguageYAML)文件来完成的。

关于 GPIO 集成,我从他们的网站上选择了一些资源来帮助您入门。我建议先阅读术语表,因为这将帮助您更好地理解 Home Assistant 的术语,从而帮助您更好地理解文档的其他部分:

网站:www.home-assistant.io

亚马逊网络服务(AWS)

另一个建议是亚马逊网络服务,具体来说是两项服务 - IoT Core 和 Elastic Beanstalk。这些选项将为您提供巨大的灵活性和几乎无穷无尽的选择,当涉及到创建物联网应用程序时。IoT Core 是亚马逊的物联网平台,您可以在其中创建仪表板、工作流和集成,而 Elastic Beanstalk 是他们的云平台,您可以在其中托管自己的程序 - 包括 Python - 在云中。

亚马逊网络服务是一个先进的开发平台,因此您需要投入几周的时间来学习它的工作原理,以及如何使用它构建和部署应用程序,但我可以向您保证,在这个过程中您会学到很多!此外,他们的文档和教程质量非常高。

亚马逊物联网核心:aws.amazon.com/iot-core

亚马逊弹性 Beanstalk:aws.amazon.com/elasticbeanstalk

Microsoft Azure、IBM Watson 和 Google Cloud

最后,我想提一下其他 IT 巨头,他们都提供自己的云和物联网平台。我之所以建议 AWS,纯粹是因为我对这个平台有更深入的经验。微软、IBM 和谷歌提供的比较平台也是高质量的,并且有着优秀的文档和教程支持,因此如果您个人偏好于这些提供商中的一个,您仍然是安全的。

摘要

在本章中,我们探讨并学习了如何将我们的 Raspberry Pi 与 IFTTT 和 ThinkSpeak IoT 平台一起使用。我们创建了两个 IFTTT 示例,其中我们的 Raspberry Pi 在 IFTTT Applet 中执行了This角色,以启动 IFTTT 工作流程。我们还看到了如何将我们的 Raspberry Pi 用作That角色,以便它可以被 IFTTT Applet 执行。接下来,我们介绍了如何与 ThinkSpeak IoT 平台集成,以可视化由我们的 Raspberry Pi 收集的温度和湿度数据的示例。最后,我们讨论了您可能希望调查和实验的其他 IoT 平台选项。

在本章中,我们确实只涵盖了可视化和自动化平台可能性的基础知识。我鼓励你寻找更多的 IFTTT 示例和你可以尝试的想法,并探索我们提到的其他平台。请记住,虽然每个平台都会有所不同并且有自己的集成考虑,但通常接受的实现集成的标准是 RESTful API 和 MQTT,这两者你现在都有经验了!

在下一章中,我们将涵盖一个全面的端到端示例,汇集了本书中涵盖的许多概念和示例。

问题

随着我们结束本章,这里有一些问题供你测试对本章材料的了解。你将在附录评估部分找到答案:

  1. 在我们的第一个 IFTTT Applet 中,我们监测温度时为什么使用了不同的高温和低温值来触发我们的 Applet 并发送电子邮件?

  2. 使用像 dweet.io 这样的中介服务与我们的 IFTTT Webhook 服务有什么优势?

  3. IFTTT 和 Zapier 之间的一些核心区别是什么?

  4. 你能从 ThingSpeak 仪表板控制你的 Raspberry Pi 吗?

  5. 关于数据,当 IFTTT Webhook 服务用作动作(即 applet 的That一侧)时有什么限制?

  6. 你想要原型化基于 Raspberry Pi 的 GPIO 引脚状态来开关柜台智能灯泡。你可以使用哪些平台?

第十四章:将所有内容联系在一起-物联网圣诞树

欢迎来到我们的最后一章!我们将通过整合前几章的各种主题和想法来完成本书,以构建一个多方面的物联网程序。具体来说,我们将构建一个可以通过互联网控制的圣诞树,一个IoTree,如果你不介意的话!

本章中我们的方法是重用前几章的两个电路,以创建圣诞树灯光(使用 APA102 LED 灯带)和一个摇摆机制来使树摇晃(我们将使用舵机)和发出叮当声(好吧,如果你用铃铛装饰树,它会在摇晃时发出叮当声!)。然后我们将重新审视和调整我们关于 RESTful API 和 MQTT 的学习,以创建两种方式来通过网络或互联网控制灯光和舵机。然后我们将重新审视 dweet.io 和If-This-Then-ThatIFTTT)并构建 IFTTT Applets 来通过电子邮件和您的声音使用 Google 助手来控制树!

以下是本章将涵盖的内容:

  • 物联网圣诞树概述

  • 构建 IoTree 电路

  • 配置、运行和使用 Tree API 服务

  • 配置、运行和使用 Tree MQTT 服务

  • 将 IoTree 与 dweet.io 集成

  • 与电子邮件和 Google 助手通过 IFTTT 集成

  • 扩展您的 IoTree 的想法和建议

技术要求

要执行本章的练习,您需要以下内容:

  • 树莓派 4 Model B

  • Raspbian OS Buster(带桌面和推荐软件)

  • 最低 Python 版本 3.5

这些要求是本书中代码示例的基础。可以合理地期望,只要您的 Python 版本是 3.5 或更高,代码示例应该可以在树莓派 3 Model B 或不同版本的 Raspbian OS 上无需修改地工作。

要完成标题为与 Google 助手集成的部分,至少需要以下先决条件:

  • 一个 Google 账户(如果您有 Gmail 邮箱账户,那就是您需要的全部)

  • 安卓手机或 iOS 的Google 助手应用

您将在 GitHub 存储库的chapter14文件夹中找到本章的源代码,链接在这里:github.com/PacktPublishing/Practical-Python-Programming-for-IoT

您需要在终端中执行以下命令来设置虚拟环境并安装本章代码所需的 Python 库:

$ cd chapter14              # Change into this chapter's folder
$ python3 -m venv venv      # Create Python Virtual Environment
$ source venv/bin/activate  # Activate Python Virtual Environment
(venv) $ pip install pip --upgrade        # Upgrade pip
(venv) $ pip install -r requirements.txt  # Install dependent packages

以下依赖项从requirements.txt中安装:

本章练习所需的电子元件如下:

  • 1 x MG90S 爱好舵机(或等效的 3 线、5 伏特爱好舵机)

  • 1 x APA102 RGB LED 灯带

  • 1 x 逻辑电平转换模块

  • 外部电源供应(至少是 3.3V/5V 面包板可安装的电源供应)

视频展示了这棵树的运作情况,网址是youtu.be/15Xfuf_99Io。请注意,这棵树使用 RGB LED 和交替闪烁的灯光动画。在本章中,我们将使用 APA102 LED 灯带,它能够创建更多的动画效果。演示树还可以演奏曲调,但我们在本章不会涉及(尽管您可以轻松地通过采用第八章中的 RTTTL 示例来添加该功能)。

IoT 圣诞树概述

在我们通过构建电路和查看代码来开始本章之前,让我们花一点时间了解一下我们的 IoTree 将会做什么以及我们将如何构建它。图 14.1中的树代表了您在完成本章后可能创建的东西:

图 14.1 - IoTree 示例

现在,我需要提前告诉您,我们只会涵盖 IoTree 的电子和编程部分。您需要发挥自己的主动性,并发挥您的制造技能来建造这棵树并让它活起来。我建议使用一棵小桌面圣诞树,因为我们的构建部分涉及使用伺服机来摇晃树。我们的业余级伺服机足够强大,可以摇动一棵小树;但是,它不太可能能够摇动一棵全尺寸的圣诞树(如果您希望将我们的构建升级到更大的树,请研究并获得更强大的伺服机,并且如果您这样做,请给我发一张照片!)。

我们的基本树将包括以下电子组件:

  • 用于树灯的 APA102 LED 灯带(我们在第八章中介绍了 APA102 LED 灯带,灯光、指示灯和信息显示)。

  • 一个伺服机使树摇晃叮当作响 - 为此,您需要在树上放一些铃铛装饰品,当树摇晃时它们会叮当作响(我们在第十章中介绍了伺服机,使用伺服机、电机和步进电机进行运动)。

在程序和结构上,我们的树程序将借鉴我们学到的以下概念:

  • dweet.io 服务:首次介绍于第二章,Python 和 IoT 入门,并在第十三章,IoT 可视化和自动化平台中重新讨论

  • 使用 Flask-RESTful 的 RESTful API:来自第三章,使用 Flask 进行 RESTful API 和 Web 套接字进行网络连接

  • 消息队列遥测传输(MQTT):在第四章中介绍,使用 MQTT、Python 和 Mosquitto MQTT 代理进行网络连接

  • IoT 程序的线程和发布-订阅(PubSub)方法:在第十二章中介绍,高级 IoT 编程概念-线程、AsyncIO 和事件循环

  • IFTTT IoT 平台:在第十三章中介绍,IoT 可视化和自动化平台

在我们继续本章之前,我们将假设您已经理解了前述每一章的概念,并且已经完成了每一章中提出的练习,包括构建电路和理解使电路工作的电路和代码级概念。

我们的第一个任务将是构建 IoTree 所需的电路,接下来我们将进行。

构建 IoTree 电路

是时候开始建设了!请构建图 14.2中所示的电路:

图 14.2 - IoTree 电路原理图

这个电路应该看起来很熟悉。它是我们之前见过的两个电路的组合:

  • 来自图 8.4的 APA102(带逻辑电平转换器)电路,位于第八章中,灯光、指示灯和信息显示

  • 来自图 10.2的舵机电路,位于第十章中,使用舵机、电机和步进电机进行运动

如果您需要逐步构建面包板上的电路的说明,请参阅相应的章节。

请记住,您需要使用外部电源来为 APA102 和舵机供电,因为它们会吸取太多的电流,无法使用树莓派的 5 伏引脚。

当您完成电路构建后,接下来让我们简要讨论三个可以用来控制此电路的程序。

三个 IoTree 服务程序

有三个单独的程序来配合我们的 IoTree,每个程序都采用了稍微不同的方法来处理我们的灯光和舵机。这些程序如下:

  • Tree API 服务(位于chapter14/tree_api_service文件夹中):此程序提供了一个使用 Flask-RESTful 创建的 RESTful API,用于控制灯光和舵机。它还包括一个使用 API 的基本 HTML 和 JavaScript Web 应用程序。我们将在标题为配置、运行和使用 Tree API 服务的部分进一步讨论 Tree API 服务。

  • Tree MQTT 服务(位于chapter14/tree_mqtt_service文件夹中):此程序将允许我们通过发布 MQTT 消息来控制灯光和舵机。我们将在标题为配置、运行和使用 Tree MQTT 服务的部分进一步讨论 Tree MQTT 服务。

  • dweet 集成服务(位于chapter14/dweet_integration_service文件夹中):此程序接收 dweets 并将它们重新发布为 MQTT 消息。我们可以将此程序与Tree MQTT 服务程序一起使用,以使用 dweet.io 来控制我们的灯光和舵机,从而为我们提供了一种将 IoTree 与 IFTTT 等服务集成的简单方法。我们将在标题为将 IoTree 与 dweet.io 集成的部分进一步讨论 dweet 集成服务。

现在我们已经简要讨论了构成本章示例的程序,让我们配置和运行我们的 Tree API 服务,并使用它来使灯光和舵机工作。

配置、运行和使用 Tree API 服务

Tree API 服务程序为控制我们的 IoTree 的 APA102 LED 灯带和舵机提供了一个 RESTful API 服务。您可以在chapter14/tree_api_service文件夹中找到 Tree API 服务程序。它包含以下文件:

  • README.md:Tree API 服务程序的完整 API 文档,包括示例。

  • main.py:这是程序的主要入口点。

  • config.py:程序配置。

  • apa102.py:与 APA102 LED 灯带集成的 Python 类。这段代码的核心与我们在第八章中探讨的 APA102 Python 代码非常相似,只是现在它被构造为 Python 类,使用线程来运行灯光动画,还有一些其他小的添加,比如让 LED 闪烁的代码。

  • apa102_api.py:提供 APA102 API 的 Flask-RESTful 资源类。它借鉴了第三章中 Flask-RESTful 代码和示例,使用 Flask 进行 RESTful API 和 Web 套接字的网络

  • servo.py:用于控制舵机的 Python 类。它借鉴了我们在第十章中介绍的舵机代码。

  • servo_api.py:提供舵机 API 的 Flask-RESTful 资源类。

  • 模板:此文件夹包含示例 Web 应用程序的index.html文件。

  • static:此文件夹包含 Web 应用程序使用的静态 JavaScript 库和图像。

图 14.3 显示了 Tree API 服务程序架构的图表:

图 14.3 - Tree API 服务架构块图

这是 Tree API 服务的高级操作,用于前面图表中显示的 API 请求:

  1. 外部客户端向#1 处的/lights/colors端点发出 POST 请求。

  2. 请求由 Flask 框架/服务器在#2 处处理。(Flask 和 Flask-RESTful 的设置可以在main.py中找到。)

  3. /lights/*端点被路由到适当的 Flask-RESTful 资源#3(APA102 - 也就是light - 资源在apa102_api.py中定义)。端点设置和资源注册在main.py中找到。

  4. 在#4 处,调用适当的资源(在本例中,将是ColorControl.post()),然后解析和验证查询字符串参数(即colors=red%20blue&pattern=yes)。

  5. 最后,在#5 处,ColorControl.post()调用 APA102 的实例中的适当方法(在apa102.py中定义,并在main.py中设置),直接与物理 APA102 LED 灯带接口并更新重复的红色和蓝色模式。

现在我们了解了我们的 Tree API 服务的工作原理,在运行 Tree API 服务之前,首先需要检查它的配置。我们接下来会做这个。

配置 Tree API 服务

Tree API 服务配置在chapter14/tree_api_service/config.py文件中。在这个文件中有许多配置选项,它们大多与 APA102(在第八章中讨论)和舵机(在第十章中讨论)的配置有关。你会发现这个文件和配置选项都有很好的注释。

默认配置足以在树莓派上本地运行示例;但是,你应该检查的一个配置参数是APA102_NUM_LEDS = 60。如果你的 APA102 LED 灯带包含不同数量的 LED,那么请相应地更新此配置。

让我们运行 Tree API 服务程序并创建一些灯光(和移动)!

运行 Tree API 服务

现在是时候运行 Tree API 服务程序并发送 RESTful API 请求使其工作了。以下是运行和测试我们的 Tree API 服务的步骤:

  1. 切换到chapter14/tree_api_service文件夹并启动main.py脚本,如下所示:
# Terminal 1
(venv) $ cd tree_api_service
(venv) $ python main.py
* Serving Flask app "main" (lazy loading)
... truncated ...
INFO:werkzeug: * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
  1. 接下来,打开第二个终端并运行以下curl命令,将重复的灯光模式序列设置为红色,蓝色,黑色
# Terminal 2
$ curl -X POST "http://localhost:5000/lights/color?colors=red,blue,black&pattern=yes"
  1. 同样在终端 2中,运行下一个命令开始让灯光动画起来:
# Terminal 2
$ curl -X POST "http://localhost:5000/lights/animation?mode=left&speed=5"

除了left,你还可以在mode参数中使用其他动画模式,包括rightblinkrainbowstopspeed参数接受110之间的值。

  1. 要清除或重置 LED 灯带,再次在终端 2中运行以下命令:
# Terminal 2
$ curl -X POST "http://localhost:5000/lights/clear"
  1. 要使舵机扫动(也就是使树摇晃),在终端 2中运行以下命令:
# Terminal 2
$ curl -X POST "http://localhost:5000/servo/sweep"

舵机应该来回扫动几次。如果你想让舵机扫动更多次或需要增加其运动范围,那么你可以在chapter14/tree_api_service/config.py文件中调整SERVO_SWEEP_COUNTSERVO_SWEEP_DEGREES配置参数。

如果发现当你移动舵机时 LED 变暗、闪烁或者表现不稳定,或者当你改变 APA102 LED 时舵机抽搐,很可能是你的外部电源无法提供足够的电流来同时运行 LED 和舵机。作为临时措施,如果你没有另一个电源,可以尝试减少 LED 的数量(config.py中的APA102_NUM_LEDS)和/或减少 LED 对比度(config.py中的APA102_DEFAULT_CONTRAST)。这将降低 LED 灯带的电流需求。

  1. 最后,让我们运行 Web 应用程序,并通过在树莓派桌面上打开 Web 浏览器并导航到 URL http://localhost:5000来控制我们的 IoTree。您应该看到一个类似于这里图片的网页:

图 14.4 - 示例 IoTree Web 应用程序

尝试以下操作:

  • 点击颜色栏中的颜色,观察该颜色被推送到 APA102 LED 灯带。

  • 点击Pattern Fill按钮填充 APA102 LED 灯带的选定颜色。

  • 点击左侧开始动画。

此 Web 应用程序背后的 JavaScript(在chapter14/tree_api_service/templates/index.html中找到)只是调用我们的 IoTree API,类似于我们已经使用curl做的事情,只是它使用 jQuery 来做。 jQuery 和 JavaScript 超出了本书的范围;但是,在第三章中简要介绍了它们,使用 Flask 进行 RESTful API 和 Web Sockets 的网络

您将在chapter14/tree_api_service/README.md文件中找到 IoTree 的完整 API 文档集,其中包含curl示例。

我们的 RESTful API 实现提供了本章所需的基本 API 端点;但是,我非常有信心您将能够扩展和调整此示例以适应您自己的项目,或者向您的 IoTree 添加新功能。我将在本章末尾的扩展您的 IoTree 的想法和建议部分提供关于如何根据本书学到的知识扩展您的 IoTree 的建议。

现在我们已经运行并看到如何使用 RESTful API 控制我们的 IoTree 的灯和舵机,接下来我们将看一种替代服务实现,它将允许我们使用 MQTT 控制我们的 IoTree。

配置、运行和使用 Tree MQTT 服务

Tree MQTT 服务程序提供了一个 MQTT 接口,用于通过发布 MQTT 消息到 MQTT 主题来控制树的 APA102 LED 灯带和舵机。您可以在chapter14/tree_mqtt_service文件夹中找到 Tree MQTT 服务程序,并包含以下文件:

  • README.md:控制您的 IoTree 的 MQTT 主题和消息格式的完整列表。

  • main.py:这是程序的主要入口点。

  • config.py:程序配置。

  • apa102.py:这是chapter14/tree_api_service/apa102.py的精确副本。文件

  • servo.py:这是chapter14/tree_api_service/servo.py文件的精确副本。

  • mqtt_listener_client.py:这是一个连接到 MQTT 代理并订阅将接收消息以控制 APA102 和舵机的主题的类。当接收到 MQTT 消息时,它们被转换为 PubSub 消息并使用PyPubSub库发布,我们在第十二章中讨论过,高级 IoT 编程概念-线程、AsyncIO 和事件循环

  • apa102_controller.py:此代码接收由mqtt_listener_client.py发送的 PubSub 消息,并根据需要更新 APA102 LED 灯带。

  • servo_controller.py:此代码接收由mqtt_listener_client.py发送的 PubSub 消息并控制舵机。

显示了 Tree MQTT 服务程序架构的图表如图 14.5所示:

图 14.5 - Tree MQTT 服务架构块图

以下是 Tree MQTT 服务的高级操作,用虚线表示在前面的图表中发布的 MQTT 发布:

  1. red blue消息发布到tree/lights/pattern主题上#1。

  2. 消息由 Paho-MQTT 客户端在#2 处接收。主题和消息在mqtt_listener_client.py中的on_message()方法中进行解析,并使用config.py中的MQTT_TO_PUBSUB_TOPIC_MAPPINGS映射字典映射到本地 PubSub 主题pattern

  3. 映射的消息和解析的数据使用PyPubSub库在#3 处分发。

  4. apa102_controller.py中的PyPubSub订阅接收pattern主题及其负载数据在#4 处

  5. apa102_controller.py处理#5 处的消息和数据,并在 APA102 实例(在apa102.py中定义)上调用适当的方法,直接与重复的红色和蓝色模式的物理 APA102 LED 灯带进行接口和更新。

如果你在想,使用PyPubSub并在mqtt_listener_client.py中重新分发 MQTT 消息是基于我个人偏好的设计决定,目的是将 MQTT 相关的代码和硬件控制相关的代码解耦,以使应用程序更易于阅读和维护。另一种同样有效的方法是在直接响应接收到的 MQTT 消息时在mqtt_listener_client.py中使用apa102.pyservo.py

现在我们已经了解了我们的 Tree MQTT 服务是如何工作的,在运行我们的 Tree MQTT 服务之前,首先我们需要检查它的配置。我们将在下一步进行。

配置 Tree MQTT 服务

Tree MQTT 服务配置位于chapter14/tree_mqtt_service/config.py文件中。与 Tree API 服务类似,它们主要涉及 APA102 和伺服器的配置。您还会发现这个文件及其配置选项都有很好的注释。

默认配置将足以在树莓派上本地运行示例;但是,就像我们为 Tree API 服务配置所做的那样,请检查并更新APA102_NUM_LEDS = 60参数。

如果您在运行 Tree API 示例时还需要更改APA102_DEFAULT_CONTRASTSERVO_SWEEP_COUNTSERVO_SWEEP_DEGREES参数中的任何一个,请现在也更新这些值以供 MQTT 示例使用。

一旦您对配置进行了任何必要的更改,我们将继续运行我们的 Tree MQTT 服务程序并发布 MQTT 消息以使我们的 IoTree 工作。

运行 Tree MQTT 服务程序

现在是时候运行 Tree MQTT 服务程序并发布 MQTT 消息来控制我们的 IoTree 了。以下是运行和测试我们的 Tree MQTT 服务的步骤:

  1. 我们必须在树莓派上安装并运行 Mosquitto MQTT 代理服务以及 Mosquitto MQTT 客户端工具。如果您需要检查您的安装,请参阅第四章,使用 MQTT、Python 和 Mosquitto MQTT 代理进行网络连接

  2. 切换到chapter14/tree_mqtt_service文件夹并启动main.py脚本,如下所示:

# Terminal 1
(venv) $ cd tree_mqtt_service
(venv) $ python main.py
INFO:root:Connecting to MQTT Broker localhost:1883
INFO:MQTTListener:Connected to MQTT Broker
  1. 接下来,打开第二个终端并使用以下命令发送 MQTT 消息:
# Terminal 2
$ mosquitto_pub -h "localhost" -t "tree/lights/pattern" -m "red blue black"

LED 灯带将以重复的颜色模式(红色、蓝色、黑色(黑色表示 LED 关闭))点亮。

尝试使用--retain-r保留消息选项来实验mosquirro_pub。如果您发布了一个保留消息,当它连接到 MQTT 代理并订阅tree/#主题时,它将被重新传递到您的 Tree MQTT 服务,这为您的 IoTree 在重新启动之间恢复其上次状态提供了一种方式。

  1. 现在,在终端 2中运行以下命令使 LED 灯带动画起来:
# Terminal 2
$ mosquitto_pub -h "localhost" -t "tree/lights/animation" -m "left"
  1. 要清除或重置 LED 灯带,请在终端 2中再次运行以下命令:
# Terminal 2
$ mosquitto_pub -h "localhost" -t "tree/lights/clear" -m ""

在这个例子(以及步骤 6中的下一个例子)中,我们没有任何消息内容;但是,我们仍然需要传递一个空消息,使用-m ""选项(或者,-n);否则,mosquitto_pub将中止。

  1. 最后,尝试以下命令来扫描伺服器:
# Terminal 2
$ mosquitto_pub -h "localhost" -t "tree/servo/sweep" -m ""

伺服器将根据chapter14/tree_mqtt_service/config.pySERVO_SWEEP_COUNTSERVO_SWEEP_DEGREES设置来来回扫动。

您将在chapter14/tree_mqtt_service/README.md文件中找到 Tree MQTT 服务识别的完整 MQTT 主题和消息格式的完整集合,包括mosquitto_pub示例。

与我们的 RESTful API 示例类似,我们的 MQTT 示例提供了本章所需的最小功能,但提供了一个基本框架,您可以在自己的未来项目中扩展,或者如果您扩展了 IoTree 的功能。

现在我们已经运行并看到如何使用 MQTT 控制 IoTree 的灯和伺服,让我们看看我们可以使用的集成服务,将我们的 Tree MQTT 服务与 dweet.io 耦合。

将 IoTree 与 dweet.io 集成

dweet 集成服务,位于chatper14/dweet_integration_service文件夹中,是一个基于 Python 的集成服务,它接收 dweets 并将它们重新发布为消息到 MQTT 主题。此服务为我们提供了一种简单的方法,将诸如 IFTTT 之类的服务与我们的 Tree MQTT 服务程序集成。

dweet 集成服务由以下文件组成:

  • main.py:主程序入口点。

  • config.py:配置参数。

  • thing_name.txt:保存您的物体名称的地方。当您第一次启动程序时,将创建此文件。

  • dweet_listener.py:核心程序代码。

我们的 dweet 服务的核心部分位于dweet_listener.py文件中。如果您检查此文件,您会注意到它几乎与第二章中涵盖的dweet_led.py文件以及第十三章,IoT 可视化和自动化平台中涵盖的文件几乎相同(除了现在它作为 Python 类包装)。

核心区别在于process_dweet()方法,在下面的代码中显示为(1)行,这里我们不是直接控制 LED,而是拦截 dweet,然后重新发布到 MQTT 主题:

def process_dweet(self, dweet):        # (1)

   # ...Truncated...
   # command is "<action> <data1> <data2> ... <dataN>"
   command = dweet['command'].strip()
   # ...Truncated...

   # elements (List) <action>,<data1>,<data2>,...,<dataN>
   elements = command.split(" ")
   action = elements[0].lower()
   data = " ".join(elements[1:])

   self.publish_mqtt(action, data)     # (2)

publish_mqtt()方法,在前面的代码中显示为(2)行,在下面的代码中显示为(3)行,然后根据chapter14/dweet_mqtt_service/config.py中的ACTION_TOPIC_MAPPINGS设置,将我们解析的命令字符串转换为基于 MQTT 主题的消息并发布:

    def publish_mqtt(self, action, data):                       # (3)
        if action in self.action_topic_mappings:
            # Map Action into MQTT Topic
            # (Eg mode --> tree/lights/mode). 
            # See config.py for mappings.

            topic = self.action_topic_mappings[action]
            retain = topic in self.mqtt_topic_retain_message    # (4)
            # ... truncated ...
            publish.single(topic, data, qos=0,                  # (5)
                          client_id=self.mqtt_client_id, 
                          retain=retain, hostname=self.mqtt_host, 
                          port=self.mqtt_port)
    # ... truncated ...

请注意,在(5)行,我们使用了 Paho-MQTT 的publish.single()便利方法,而不是我们在第四章中使用的完整的 MQTT 客户端方法,使用 MQTT、Python 和 Mosquitto MQTT 代理进行网络连接(并且在 Tree MQTT 服务程序中也使用)。

目前,我只想指出(4)行,我们在那里设置了retain变量(还注意到它在publish.single()中的使用)。在接下来的部分中,当我们讨论服务配置文件时,我们将更多地讨论此消息保留。

图 14.6 显示了树服务程序架构的图表:

图 14.6 - dweet 集成服务架构块图

这是 dweet 集成服务的高级操作,由前面图中蓝色虚线所示的请求:

  1. 在#1 处创建了一个 dweet。

  2. dweet_listener.py在#2 处接收 dweet 并解析command参数中包含的数据。命令中包含的操作使用config.py中找到的ACTION_TOPIC_MAPPINGS映射字典映射为 MQTT 主题。

  3. 消息被发布到 MQTT 代理到映射的 MQTT 主题#3。根据config.py中找到的TOPIC_RETAIN_MESSAGE映射字典,设置消息的保留标志。

发布 MQTT 消息后,如果您的 Tree MQTT 服务正在运行并连接到相同的 MQTT 代理,它将接收 MQTT 消息并相应地更新您的 IoTree。

现在我们了解了我们的 dweet 集成服务的工作原理,然后我们可以运行我们的 dweet 集成服务之前,首先需要检查其配置。我们接下来会做这个。

配置 Tree MQTT 服务

dweet 集成服务的配置位于chapter14/dweet_integration_service/config.py文件中。有许多与服务工作方式相关的配置选项,默认配置将足以在您的 Raspberry Pi 上本地运行此服务,同时您还在那里运行您的 Mosquitto MQTT 代理。这个文件中的配置参数有很好的注释;但是,我将提到ACTION_TOPIC_MAPPINGSTOPIC_RETAIN_MESSAGE参数:

ACTION_TOPIC_MAPPINGS = {
    "clear": "tree/lights/clear",
    "push": "tree/lights/push",
    ... truncated ...
}

dweet 集成服务将dweeted 命令映射到MQTT 主题。决定如何将命令映射到 MQTT 主题的是ACTION_TOPIC_MAPPINGS配置参数。我们将在下一节讨论这个命令的概念。

由 dweet 集成服务映射和使用的 MQTT 主题必须与 Tree MQTT 服务使用的主题相匹配。每个服务的默认配置都使用相同的主题。

以下代码中显示的TOPIC_RETAIN_MESSAGE配置确定了哪些 MQTT 主题将设置其消息的保留标志。正如我们在上一节中指出的那样,这个配置(TrueFalse)用于在single.publish()上设置retained参数:

TOPIC_RETAIN_MESSAGE = {
    "tree/lights/clear": False,
    "tree/lights/animation": True,
    ... truncated ...
}

现在我们已经讨论了配置文件,让我们启动我们的 dweet 集成服务,并发送 dweets 来控制我们的 IoTree。

运行 dweet 集成服务程序

我们的 dweet 集成服务通过按照我们在上一节中讨论的配置参数将预定义格式的 dweets 转换为 MQTT 主题和消息来工作。当我们运行和测试 dweet 集成服务时,我们将很快讨论这个 dweet 格式。以下是我们需要遵循的步骤:

  1. 首先,请确保您在终端上运行了上一节中的Tree MQTT 服务程序。正是 Tree MQTT 服务将接收并处理 dweet 集成服务发布的 MQTT 消息。

  2. 接下来,在新的终端中导航到chapter14/dweet_integration_service文件夹,并启动main.py程序,如下所示(请记住您的物体名称将不同):

(venv) $ cd dweet_service
(venv) $ python main.py
INFO:DweetListener:Created new thing name ab5f2504
INFO:DweetListener:Dweet Listener initialized. Publish command dweets to 'https://dweet.io/dweet/for/ab5f2504?command=...'
  1. 将以下 URL 复制并粘贴到 Web 浏览器中以控制您的 IoTree。使用您输出中显示的物体名称替换<thing_name>文本:
    • dweet.io/dweet/for/<thing_name>?command=pattern%20red%20blue%20black
  • dweet.io/dweet/for/<thing_name>?command=animation%20left

  • dweet.io/dweet/for/<thing_name>?command=speed%2010

  • dweet.io/dweet/for/<thing_name>?command=clear

  • dweet.io/dweet/for/<thing_name>?command=sweep

在调用这些 URL 之间可能需要一些时间,然后它将被您的 dweet 集成服务接收。

正如您在前面的 URL 中的command参数中所看到的,我们的 dweet 的格式是<action> <data1> <data2> <dataN>

config.py文件中,您将找到默认配置中识别的完整的 dweet 命令字符串集,包括示例 URL,在chapter14/dweet_integration_service/README.md文件中。

干得好!我们刚刚使用 dweet.io 和 MQTT 创建了一个简单的集成服务,并学会了一种简单且非侵入式的方法,可以让我们在互联网上控制我们的 tree,而无需进行任何网络或防火墙配置。

在设计物联网项目并考虑数据在互联网和网络中的传输方式时,通常会发现您需要设计和构建某种集成形式,以桥接建立在不同传输机制上的系统。本节中的示例说明了一个场景,我们在其中将 MQTT 服务(我们的 IoTree MQTT 服务)与基于轮询的 RESTful API 服务(dweet.io)进行了桥接。虽然每个集成都有自己的要求,但希望这个示例为您提供了一个大致的路线图和方法,以便在将来遇到这些情况时进行调整和构建。

现在我们的 dweet 集成服务正在运行,并且已经测试过它正在工作,让我们看看如何将其与 IFTTT 平台一起使用。

通过电子邮件和 Google 助手与 IFTTT 集成

现在是真正有趣的部分——让我们使我们的树可以通过互联网进行控制。作为一个剧透,我不会在这个集成中手把手地指导您,因为在第十三章中详细解释了使用 dweet.io 和 IFTTT 的核心概念,IoT 可视化和自动化平台。特别是,我们学会了如何将我们的树莓派与 IFTTT 和电子邮件集成,以控制 LED。

然而,我将给您我的 IFTTT 配置的截图,以便您可以验证您设置的内容。另外,作为一个额外的奖励,我还将给您一个关于如何与 Google 助手集成的提示和截图,以便您可以语音控制您的 IoTree!

在撰写本文时,IFTTT 有一个 Google 助手服务,可以接受任意口头输入(在 IFTTT 术语中称为成分)。我确实尝试了 Alexa 集成,但不幸的是,Alexa IFTTT 服务无法接受任意输入,因此与我们的示例不兼容。

首先,我们将看一下如何将我们的 IoTree 与电子邮件集成。

与电子邮件的集成

与电子邮件或 Twitter 集成的过程与我们在第十三章IoT 可视化和自动化平台中介绍的内容相同,只是有以下更改:

  1. 不要使用LED作为标签(在 IFTTT 的完成触发器字段页面步骤中),使用TREE。这样,您的电子邮件主题可以是#TREE 模式红蓝#TREE 动画闪烁之类的内容。

  2. 在配置 That webhook 服务时,您需要使用之前在终端上运行 dweet 集成服务时打印的 dweet URL。我的配置示例如下图所示。请记住,您 URL 中的thing name将是不同的:

图 14.7 – Webhook 配置

  1. 完成设置 IFTTT Applet 后,尝试发送电子邮件至trigger@applet.ifttt.com,主题如下:
    • #TREE 模式红蓝黑色
  • #TREE 动画向左

发送电子邮件或推文#TREE 模式红蓝黑色命令后的几分钟内,您的树灯将以这些颜色进行重复变换。同样,发送电子邮件或推文#TREE 动画向左后的几分钟内,您的树灯将开始动画。

请记住,为了使此示例工作,您需要在终端上同时运行 Tree MQTT 服务和 dweet 集成服务。在发送电子邮件或发布推文后,您的 IoTree 更改可能需要一些时间。

一旦您能够通过电子邮件控制您的 IoTree,接下来我们将看一下添加使用 Google 助手进行语音控制所需的步骤。

与 Google 助手的集成

让我们使用 Google 助手使我们的 IoTree 可以通过语音控制。

Google 助手有许多其他形式,包括 Google Home、Google Nest 和 Google Mini。只要它们登录到与 IFTTT 使用的相同的 Google 帐户,这些产品也将与 IFTTT Google 助手集成和您的 IoTree 一起使用。

要创建我们的集成,我们需要将您的 Google 帐户与 IFTTT Google 助手服务链接,并在接收命令时调用 dweet.io URL。以下是要遵循的高级步骤:

  1. 登录到您的 IFTTT 帐户。

  2. 创建一个新的 Applet。

  3. 对于 Applet 的这一部分,请使用 Google 助手服务。

  4. 接下来,您将被要求连接并允许 IFTTT 使用您的 Google 帐户。按照屏幕上的说明连接 IFTTT 和您的 Google 帐户。

  5. 现在是选择 Google 助手触发器的时候了。选择“说一个带有文本成分的短语”。示例触发器配置如图 14.8所示:

图 14.8 - Google 助手触发器示例

在前面的屏幕截图中显示的 Tree $中的$符号被转换为我们将与我们的 webhook 服务一起使用的 IFTTT 成分(我们将在后面的步骤中看到)。

有了这个触发器配置,你可以说出像以下这样的命令来控制你的 IoTree:

    • "Tree pattern red blue black"
  • "Set tree animation blink"

  • "Tree clear"

  1. 现在是配置 IFTTT Applet 的那部分的时间。搜索并选择 WebHook。

  2. webhook 服务的配置与我们在步骤 2中之前介绍的与电子邮件集成标题下的过程相同,并且如*图 14.7.*所示。

  3. 继续并完成创建你的 IFTTT Applet。

  4. 询问你的 Google 助手以下命令:

    • "Tree pattern red blue black"
  • "Tree animation blink"

  • "Tree clear"

  • "Tree sweep"(或“tree jingle”)

  • 或者chapter14/dweet_integration_service/README.md文件中记录的任何其他命令

记住,Google 助手承认你的请求后,你的 IoTree 可能需要一会儿才能开始改变。

这是我在 iPhone 上的 Google 助手对话框的屏幕截图:

图 14.9 - 用于控制 IoTree 的 Google 助手对话框

如果集成工作正常,Google 助手将回复“好的,正在更新树”(或者你在步骤 5中使用的任何文本),然后几分钟后,你的 IoTree 将做出响应。

重要的是要记住,我们必须准确地说出命令,就像它们被 dweet 集成服务解释的那样 - 例如,它们会出现在 dweet URL 的命令参数中,如https://dweet.io/dweet/for/<thing_name>?command=pattern red blue black

记得在它们之前加上“Tree”(或“Set Tree”)这个词。这个文本是触发你的 IFTTT Applet 的。只说一个命令本身不会触发你的 Applet。

如果你使用安卓手机或 iOS 的 Google 助手应用程序,你将能够看到你说出的话是如何转换为文本命令的,这可以帮助你排除不起作用或被误解的命令。

你刚刚学会了如何创建三个 IFTTT 集成,以使用电子邮件和语音控制你的 IoTree,并且你可以轻松地将相同的基本思想和流程适应于控制和自动化本书中所见的其他电子电路。

此外,正如我们在第十三章中讨论的那样,物联网可视化和自动化平台,IFTTT 提供了许多触发器操作,你可以组合起来构建自动化工作流Applets。在本章和上一章之间,你现在已经创建了几个 Applets,所以我完全相信你将能够探索 IFTTT 生态系统,并创建各种有趣的 Applets,这些 Applets 可以与你的树莓派一起工作。

在我们结束本章(和本书!)之前,我想给你留下一些想法和实验,以进一步扩展你的 IoTree 的功能。

扩展你的 IoTree 的想法和建议

我们在本章中使用的代码和电子设备为我们提供了一个基础,我们可以在此基础上构建。这可能是扩展你的 IoTree,也可能是其他物联网项目的基础。

以下是一些建议,你可以尝试:

  • 添加并集成一个 PIR 传感器,每当有人走过你的 IoTree 时,它就会播放一个 RTTTL 曲调。毕竟,如果不一遍又一遍地播放曲调,什么电子圣诞小工具才算完整呢...

  • 将 RGB LED 添加并集成到树的顶部(也许在透明的星星内),或者在 APA102 LED 条的位置使用 RGB LED 或与之一起使用。

  • 构建多个 IoTree。如果你使用 MQTT,它们将同步!

  • 尝试构建 WebSocket 集成和相应的 Web 应用程序。

  • 当前的 dweet Google 助手集成要求您精确发出命令。您能否创建一个更模糊的升级-也就是说,可以解析口头文本并找出口头命令是什么?

  • 在我们的 IFTTT 示例中,我们使用了 dweet.io(与 MQTT 一起),因此我们不必担心防火墙配置。您可能希望调查在您的位置打开防火墙端口或调查诸如 LocalTunnels(https://localtunnel.github.io/www)或 ngrok(https://ngrok.com)之类的服务。这些方法将允许您使用 IFTTT Webhooks 直接与您的 IoTree 的 RESTful API 进行通信。但是,请记住,我们的 RESTful API 示例没有得到保护-它们没有使用 HTTPS,也没有身份验证机制,例如用户名和密码来限制对 API 的访问,因此您可能还希望研究如何保护基于 Flask 的 API 并首先执行这些升级。

显然,这些只是我的一些建议。在我们的旅程中,我们涵盖了许多电路,所以发挥你的想象力,看看你能想出什么-并且要玩得开心!

总结

恭喜!我们现在已经到达了本章和整本书的结尾!

在本章中,我们运行了电子设备并测试了控制这些电子设备的程序,这些程序构成了物联网圣诞树的基础。我们看到了一个可以控制我们 IoTree 的灯和伺服的 RESTful API,以及一个类似的 MQTT 实现。我们还研究了一个 dweet.io 到 MQTT 的集成服务,我们将其与 IFTTT 配对,以提供一种使用电子邮件和 Google 助手来控制我们 IoTree 的机制。

在本书中的旅程中,我们涵盖了许多概念和技术,包括各种网络技术,电子和接口基础知识,以及使用传感器和执行器与树莓派的一系列实际示例。我们还研究了自动化和可视化平台,并在本章中完成了一个将我们的学习结合在一起的示例。

当我写这本书时,我有一些核心意图。我的一个意图是分享和解释我们如何将传感器和执行器连接到树莓派的原因,以及为什么我们要使用额外的组件,如电阻器来创建电压分压器。我的第二个核心意图是为您提供适用于物联网项目的各种网络技术和选项。

我相信,我们在旅程中学到的软件和硬件基础知识,以及实际示例,将为您提供许多技能和见解,不仅可以帮助您设计和构建自己的复杂物联网项目,还可以在软件、网络和电子方面在基本水平上理解现有的物联网项目是如何工作的。

我真诚地希望你喜欢这本书,学到了很多,并且在阅读过程中获得了许多实用的技巧!祝你在物联网之旅中一切顺利,希望你能创造一些了不起的东西!

问题

最后,这里是一些问题供您测试对本章材料的了解。您将在附录的评估部分中找到答案:

  1. 在我们的 MQTT 服务示例中,为什么我们使用PyPubSub重新分发 MQTT 消息?

  2. 在与或调试 IFTTT Google 助手 Applet 集成时,为什么在手机(或平板电脑)上使用 Google 助手应用程序很有用?

  3. 您正在处理一个现有的天气监测项目,该项目使用 MQTT 作为其网络传输层,以连接许多分布式设备。有人要求您将应用程序与 IFTTT 服务集成。您该如何做?

  4. 您想要构建多个 IoTree 并使它们一起协同工作。您可以采取哪两种方法来实现这一目标?

  5. 为什么在本章中我们使用了免费的dweet.io服务?您会在商业物联网项目中使用这种方法吗?

  6. 我们想要从命令行测试一个 RESTful API 服务。我们可以使用什么命令行工具?

  7. 您可以使用 MQTT 的哪个特性来在树莓派上电或重新启动时自动初始化 IoTrees?

  8. 关于问题 7,在设置和部署 Mosquitto MQTT 代理以实现这一目标时,您需要考虑哪些因素?

第十五章:评估

第一章

  1. 将项目特定的 Python 软件包和依赖项与其他项目和系统级 Python 软件包隔离开来。

  2. 不。您可以随时重新生成虚拟环境并重新安装软件包。

  3. 保持 Python 项目依赖的所有 Python 软件包(和版本)的列表。拥有一个维护良好的requirements.txt文件可以让您通过pip install -r requirements.txt命令轻松重新安装所有软件包。

  4. 确保您使用的是虚拟环境的bin文件夹中的 Python 解释器的绝对路径。

  5. 它激活了一个虚拟环境,以便 Python 和 pip 的所有用户都被隔离到虚拟环境中。

  6. deactivate。如果您输入exit(我们有时都会这样做!),它会退出终端窗口或关闭远程 SSH 会话!Grrrrr。

  7. 是的,只需切换到projects文件夹并激活虚拟环境。

  8. Python IDLE,但请记住,您需要在虚拟环境中使用python -m idlelib.idle [filename] &

  9. 检查在 Raspbian 中是否已启用了 I2C 接口。

第二章

  1. 按照答案编号排序,这样您就不会损坏其他组件或电阻器...除非您了解不同值将如何影响电子电路并且这样做是安全的。

  2. 错误。GPIO Zero 是其他 GPIO 库的封装。它旨在通过隐藏较低级别的 GPIO 接口细节,使初学者易于使用。

  3. 错误。在许多情况下,最好使用成熟的高级软件包,因为它们将有助于加快开发速度。Python API 文档也推荐这种方法。

  4. 不。LED 具有正(阳极)和负(阴极)端子(腿),必须正确连接。

  5. 有可能设备的时区处理存在不匹配。

  6. signal.pause()

第三章

  1. 我们可以创建和配置一个RequestParser的实例。我们在我们的控制器处理程序方法中使用这个实例,比如.get().post()来验证客户端的请求。

  2. WebSockets - 使用 Web Sockets 构建的客户端和服务器可以在任何方向上相互发起请求。这与 RESTful API 服务形成对比,后者只有客户端可以向服务器发起请求。

  3. Flask-SocketIO 不包括像 Flask-RESTful 那样的内置验证类。您必须手动执行输入验证。或者,您也可以从 PyPi.org 找到一个合适的第三方 Python 模块来使用。

  4. templates文件夹是 Flask 框架查找模板文件的默认位置。在这个位置,我们存储我们的 HTML 页面和模板。

  5. 我们应该在文档准备好的函数中初始化事件侦听器和网页内容,这个函数在网页完全加载后调用。

  6. 命令是curl。它默认安装在大多数基于 Unix 的操作系统上。

  7. 更改value属性会改变 LED 的 PWM 占空比。我们将这视为改变 LED 的亮度。

第四章

  1. MQTT,或消息队列遥测协议,是在分布式物联网网络中经常使用的轻量级消息协议。

  2. 检查 QoS 级别,确保它们是 1 级或 2 级。

  3. 如果客户端突然断开与代理的连接而没有干净地关闭连接,将代表客户端发布Will消息。

  4. 发布的消息和订阅的客户端都必须至少使用 QoS 级别 1,这可以确保消息被传递一次或多次。

  5. 理想情况下,除了可能需要更改代理主机和端口之外,您的 Python 代码不应该需要任何更改,因为 MQTT 是一个开放标准。前提是新代理配置与被替换的代理类似 - 例如,两个代理都配置类似以为客户端提供消息保留或持久连接功能。

  6. 你应该在成功连接类型的处理程序中订阅主题。这样,如果客户端失去了与代理的连接,它可以在重新连接时自动重新建立主题订阅。

第五章

  1. SPI(串行外围接口电路)。LED 灯带和矩阵是常见的例子。

  2. 你可以参考设备的官方数据表,或使用列出所有连接的 I2C 设备地址的命令行工具 i2cdetect。

  3. 确保你使用的是库期望的正确引脚编号方案,并/或者确保你已经配置了库以使用你喜欢的方案,如果库提供了这个选项。

  4. 驱动程序库不是建立在 PiGPIO 之上的,因此不支持远程 GPIO。

  5. 错误。所有的 GPIO 引脚额定电压为 3.3 伏特。连接任何高于这个电压的电压都可能损坏你的树莓派。

  6. 你使用来驱动舵机的库很可能是使用软件 PWM 来生成舵机的 PWM 信号。当树莓派的 CPU 变得繁忙时,软件 PWM 信号可能会失真。

  7. 如果你从树莓派的 5 伏特引脚为舵机供电,这将表明你正在吸取过多的电力,实际上是从树莓派中夺取电力。理想情况下,舵机应该由外部电源供电。

第六章

  1. 一般来说,是的。尝试是安全的,因为电阻越高,电路中的电流就越低(欧姆定律),而 330Ω相对接近期望的 200Ω电阻。

  2. 更高的电阻导致了更少的电流,以至于电路没有足够的电流来可靠地工作。

  3. 电阻要耗散的功率超过了电阻的功率额定值。除了使用欧姆定律来确定电阻值之外,你还需要计算电阻的预期功率耗散,并确保电阻的功率额定值(以瓦特为单位)超过你计算出的值。

  4. 1(全)。连接到+3.3 伏特的输入 GPIO 引脚是逻辑高。

  5. GPIO 21 是浮动的。它没有通过物理电阻或者通过代码使用函数调用(例如pi.set_pull_up_down(21, pigpio.PUD_UP))拉高到+3.3 伏特。

  6. 你必须使用逻辑电平转换器。这可以是一个简单的基于电阻的电压分压器,一个专用的逻辑电平转换器 IC 或模块,或者任何其他可以适当将 5 伏特转换为 3.3 伏特的形式。

  7. 错误。电阻分压器只能降低电压。但是,请记住,只要 5 伏特设备将 3.3 伏特注册为逻辑高,可能可以使用 3.3 伏特来驱动 5 伏特逻辑设备。

第七章

  1. MOSFET 是电压控制元件,而 BJT 是电流控制元件。

  2. 你在 MOSFET 的栅极上没有下拉电阻,所以它是悬空的。MOSFET 放电缓慢,这反映在电机减速。使用下拉电阻可以确保 MOSFET 迅速放电并关闭。

  3. (a)确保 G、S 和 D 腿正确连接,因为不同的封装样式(例如 T092 与 TP220)它们的腿的顺序是不同的。

(b)你还要确保 MOSFET 是逻辑电平兼容的,这样它就可以使用 3.3 伏特的电压源进行控制。

(c)确保在下拉电阻和限流电阻之间创建的电压分压器允许>〜3 伏特进入 MOSFET 的栅极腿。

  1. 光耦和继电器在电路的输入和输出端之间进行电气隔离。晶体管是在电路中的,虽然它们允许低电流设备控制更大的电流设备,但两个设备仍然都是电气连接的(例如,你会看到一个公共地连接)。

  2. 主动低是指使 GPIO 低电平以打开或激活连接的电路。主动高则相反,我们使 GPIO 引脚高电平以激活连接的电路。

  3. 代码激活的下拉仅在运行代码时变为下拉,因此 MOSFET 门基本上是悬浮的,直到运行代码。

  4. 堵转电流是电机在其轴被强行停止旋转时使用的电流。这是电机将吸取的最大电流。

  5. 没有区别-它们是两个可互换使用的术语,用来描述电机在轴上没有负载的情况下自由旋转时所使用的电流。

第八章

  1. 检查您的电源是否能够提供足够的电流(和电压)给 LED 灯带。电流需求随您想要点亮的 LED 数量以及它们设置的颜色和亮度而成比例增加。电流不足可能意味着内部的红/绿/蓝 LED 没有正确点亮,因此颜色不如您期望的那样。

  2. 缺少从选择或客户端启用引脚意味着 APA102 完全控制 SPI 接口。这意味着您不能将多个 SPI 从设备连接到一个 SPI 引脚(除非您使用额外的电子设备)。

  3. 首先,检查您的逻辑电平转换器是否连接正确。其次,可能逻辑电平转换器无法快速转换逻辑电平以跟上 SPI 接口。尝试降低 SPI 总线速度。

  4. 我们使用PILPython Imaging Library)创建一个内存中的图像,代表我们想要显示的内容。然后将此图像发送到 OLED 显示器进行渲染。

  5. RTTTL意味着Ring Tone Text Transfer Language,这是由诺基亚创建的一个铃声音乐格式。

第九章

  1. DHT22 是一种更精确的传感器,它能够感知更广泛的温度和湿度范围。

  2. 外部上拉电阻是可选的,因为我们的树莓派可以使用其内部嵌入的上拉电阻。

  3. LDR 是一种光敏电阻。当作为电压分压电路的一部分使用时,我们将变化的电阻转换为变化的电压。然后,这个电压可以被模拟到数字转换器(如连接到您的树莓派的 ADS1115)检测到。

  4. 尝试改变电压分压电路中固定电阻的电阻值。尝试更高阻值的电阻以使 LDR 在较暗的条件下更敏感。尝试更低的电阻值以使 LDR 对更明亮的条件更敏感。

  5. 当涉及到它们测量的电阻时,没有两个 LDR 是相同的。如果在电路中更换 LDR,请重新校准代码以确保。

  6. 水传导电。它在两个探针线之间充当电阻。这种电阻通过电压分压器转换为电压,ADS1115 ADC 可以检测到这种电压。

第十章

  1. 我们通常发现默认的参考脉冲宽度为 1 毫秒用于左,2 毫秒用于右的舵机。实际上,舵机可能需要略微调整的脉冲宽度才能达到其极限旋转位置。

  2. 您正在应用尝试将舵机旋转到其物理极限之外的脉冲宽度。

  3. H 桥还允许我们改变电机的旋转并快速制动以停止电机旋转。

  4. 许多因素影响制动的可靠性,包括 IC 和您的电机。您可以采用 PWM 式制动作为替代制动技术。

  5. 振动但不转动通常是线圈通电顺序和线圈步进序列不匹配的症状。您需要确定并确保步进电机的线圈连接正确并匹配步进序列。查阅您的步进电机的数据表是开始的最佳地点。

  6. L293D 的电压降约为 2 伏,因此您的电机只能获得约 3 伏。为了补偿这种电压降,您需要一个 7 伏的电源。

  7. 不。GPIO 引脚只提供 3.3 伏。虽然这可能刚好足够旋转 5 伏的步进电机,但是步进电机的电流要求将超出树莓派 GPIO 引脚的安全限制。

第十一章

  1. 不。被动红外(PIR)传感器只能检测抽象的运动。您将需要一种主动型红外传感器或类似热成像相机的设备(以及更复杂的代码)来提取更丰富的运动信息。

  2. 超声波传感器测量超声脉冲的往返时间,然后用于计算距离。影响超声脉冲时间或所用的声速常数的因素因此会影响计算出的距离。一些例子包括温度,因为这会影响声速,被检测物体的材料(例如,它是否吸收声音?),物体的大小以及其相对于传感器的角度。

  3. 锁定和非锁定霍尔效应传感器都输出数字信号-它们的输出引脚要么是高电平要么是低电平。相比之下,比例霍尔效应传感器输出与它们离磁场有多近的模拟信号(变化的电压)。

  4. callback_handler函数将在 GPIO 转换为高电平或低电平时调用。

  5. 因此,位于 5 伏特源和电压分压器输出(两个电阻之间)之间的电阻器上的相对电压降为 3.3 伏特,即 5 伏特2kΩ/(1kΩ+2kΩ) = 3.3 伏特。如果您在电路中颠倒了电阻值,电压分压器输出将为1.7 伏特,即 5 伏特1kΩ/(1kΩ+2kΩ) = ~1.7 伏特。

  6. 在查阅 HC-SR501 PIR 传感器的数据表后,我们了解到它的输出引脚始终在 3.3 伏特工作,即使它是由 5 伏特供电的,因此我们不需要电压分压器。(请注意,在实践中,我们最好也通过我们的测量来确认这一点。)

第十二章

  1. 发布-订阅方法促进了高度解耦的编程方法。当您有许多组件(例如传感器)发布数据,只需要在程序的其他地方消耗数据时,这可能是有益的。

  2. GIL 代表全局解释器锁。这是 Python 编程语言的一个设计方面,意味着一次只有一个线程可以访问 Python 解释器。

  3. 纯事件循环(例如,一个长的 while 循环)在程序增长时可能会变得复杂。对许多状态变量和非平凡的和干预的条件测试(例如,if 语句)的需求可能会使程序逻辑难以理解和调试。

  4. 不。每种方法都有其目的。事件循环在小而专注时是可以的。只有当它们变得庞大并执行多个操作时,它们才会变得复杂。

  5. 当您使用线程进行编程时,调用join()另一个线程会将该线程加入到当前线程。然后,您的当前线程将阻塞,直到所有加入的线程运行方法完成。这是同步多个线程完成的简单方法。

  6. 也许您正在使用sleep语句(来自time库),例如sleep(duration),它会阻塞整个持续时间。尝试使用以下示例中的方法,这将使您的程序保持对duration值的更改的响应性:

duration = 1   # 1 second
timer = 0
while timer < duration:
    timer += 0.01
    sleep(0.01)
  1. 没有一种方法是优越的。在 Python 中,达到编程目标的方法总是不止一种。最佳方法或方法的组合完全取决于您的项目以及您想要实现的目标。最佳方法也可能是根据您的个人偏好和首选编程风格而为您最佳的方法。

第十三章

  1. 我们使用不同的温度创建一个缓冲区,这样我们就不会在温度围绕单个温度值徘徊时生成多个触发器(和多个电子邮件)。

  2. 使用中介者意味着我们不需要担心防火墙、端口转发和其他必要的配置,以将您的树莓派暴露给公共互联网。

  3. IFTTT 更加面向消费者,而 Zapper 在提供集成方面更加面向企业。Zapper 还允许您创建更复杂的工作流程、触发器和操作场景。

  4. 不,ThingSpeak 只消耗数据以在仪表板上显示。一些平台,如 ThingBoard,将允许您将数据发送回设备,以控制该设备。

  5. 最多可以使用三个 JSON 属性– Value1Value2Value3

  6. 从开发的便捷性和速度的角度来看,IFTTT 或 Zapper 都是不错的选择,但您当然也可以使用 AWS 或其他主要的物联网平台,甚至是 Home Assistant。

第十四章

  1. 使用 PyPubSub 是一个设计决策,旨在将与 MQTT 相关的代码和逻辑与硬件控制代码和逻辑分离,以使代码更清晰、更易于维护。

  2. 使用 Google 助手应用程序时说的命令会显示在您的设备上,因此很容易看到 Google 助手是如何听到您说的命令,以及发送给您的 IFTTT Applet 的文本命令。

  3. 您需要构建一个集成服务,用于在 MQTT 和 RESTful API 之间传输数据(或者,也可以找到一个第三方服务来做到这一点,例如,可以查看io.adafruit.com和他们的 IFTTT 服务)。IFTTT 提供 RESTful webhooks 作为构建自定义集成的选项,但它不提供 MQTT 选项。

  4. 一种选择是使用 MQTT,就像我们在本章中介绍的示例一样。如果您使用 MQTT 将多个 IoTree 连接到中央 MQTT 代理,它们将一起接收指令。第二个选择可能是构建基于 WebSockets 的服务和应用程序(我们在第三章中介绍了这种方法,使用 Flask 进行 RESTful API 和 Web Sockets 的网络连接)。

  5. 我们使用免费的dweet.io服务,因为这样做非常方便,这样我们就不必担心防火墙、端口转发和路由器配置(以防您没有相关经验)。免费的dweet.io服务不提供安全性或隐私性,因此对许多项目来说并不理想。如果您喜欢dweet.io的想法,还有dweetpro.io,这是一个付费的替代方案,提供了免费版本中不可用的安全性和许多其他功能。

  6. CURL是一种常用的命令行工具,可用于测试 RESTful API。Postman (getpostman.com)是一种常用的 GUI 工具,也可以用于相同的目的。

  7. 如果您使用 MQTT 代理的保留消息功能,每个 IoTree 在连接时都会收到最后一条消息(例如,要显示什么颜色模式),因此可以初始化自己。我们在《使用 MQTT、Python 和 Mosquitto MQTT 代理进行网络连接》第四章中介绍了保留消息。

  8. 如果您的 MQTT 代理运行在与 IoTree 相同的树莓派上,并且重新启动了这个树莓派,除非 Mosquitto MQTT 代理在其配置中启用了持久性,否则所有保留的消息都将丢失。(我们在第四章中的配置,使用 MQTT、Python 和 Mosquitto MQTT 代理进行网络连接,确保了持久性已启用)。

posted @ 2024-05-04 21:30  绝不原创的飞龙  阅读(47)  评论(0编辑  收藏  举报