原文:
zh.annas-archive.org/md5/7FABA31DD38F615362E1254C67CC152E
译者:飞龙
前言
欢迎阅读《物联网的实用 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并注册,让文件直接发送到你的邮箱。
您可以按照以下步骤下载代码文件:
-
登录或注册www.packt.com。
-
选择支持选项卡。
-
单击“代码下载”。
-
在搜索框中输入书名,然后按照屏幕上的说明操作。
文件下载后,请确保使用最新版本的解压缩或提取文件夹:
-
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.py
和pip
来检查 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 版本,请按照以下步骤操作:
- 打开一个新的终端并执行
python --version
命令:
$ python --version
Python 2.7.16
在我的例子中,我们看到 Python 版本 2.7.16 已安装。
- 接下来,运行
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 依赖项隔离到一个项目中。
virtualenv
和pipenv
是两种替代的虚拟环境工具选项,它们提供比venv
更多的功能。这些替代方案非常适合复杂的 Python 项目和部署。您可以在本章末尾的进一步阅读部分找到这些链接。
让我们开始克隆 GitHub 存储库并为本章的源代码创建一个新的 Python 虚拟环境。打开一个新的终端窗口,并按照以下步骤操作:
- 切换到或创建一个您想要存储本书源代码的文件夹,并执行以下命令。使用最后一个命令,我们将克隆的文件夹重命名为
pyiot
。这样做是为了帮助缩短本书中的终端命令示例:
$ cd ~
$ git clone https://github.com/PacktPublishing/Practical-Python-Programming-for-IoT
$ mv Practical-Python-Programming-for-IoT pyiot
- 接下来,切换到包含与本章相关的代码的
chapter01
文件夹:
$ cd ~/pyiot/chapter01
- 执行以下命令,使用
venv
工具创建一个新的 Python 虚拟环境。重要的是您要输入python3
(带有 3),并记住venv
仅适用于 Python 3.3 及以上版本:
$ python3 -m venv venv
我们传递给python3
的选项包括-m venv
,它告诉 Python 解释器我们要运行名为venv
的模块。venv
参数是您的虚拟环境将被创建的文件夹的名称。
虽然在前面的命令中乍一看可能会让人困惑,但将虚拟环境的文件夹命名为venv
是一种常见的约定。本章的虚拟环境解剖部分中,我们将探讨刚刚创建的venv
文件夹下面的内容。
- 要使用 Python 虚拟环境,我们必须激活它,这可以通过
activate
命令完成:
# From with in the folder ~/pyiot/chapter01
$ source venv/bin/activate
(venv) $
当您的终端激活了 Python 虚拟环境时,所有与 Python 相关的活动都将被隔离到您的虚拟环境中。
请注意,在激活后,虚拟环境的名称venv
会显示在终端提示文本中,即(venv) $
。在本书中,每当您看到终端示例中的提示为(venv) $
时,这是一个提醒,需要从激活的 Python 虚拟环境中执行命令。
- 接下来,在您的终端中执行
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
- 要离开激活的虚拟环境,请使用
deactivate
命令,如下所示:
(venv) $ deactivate
$
还要注意,一旦虚拟环境被停用,终端提示文本中的(venv) $
将不再存在。
记住要输入deactivate
来离开虚拟环境,而不是exit
。如果您在虚拟环境中输入exit
,它将退出终端。
- 最后,现在您已经退出了我们的 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 版本,可以使用virtualenv
和pipenv
虚拟环境替代方案。
我们已经看到如何创建、激活和停用 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.
类似于python
和python3
,有pip
和pip3
。pip
(没有数字)将是与给定虚拟环境中的默认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.py
和pip
检查 GPIO 包的可用性。我要打破悬念告诉你,它们还不能在你新建的虚拟环境中使用,但是我们将安装它们!
注意:如果你想在虚拟环境之外运行这个脚本来检查,这些包已经在系统级别安装好了。
接下来的步骤将引导我们升级pip
,探索工具的选项,并安装包:
- 作为第一步,我们将升级
pip
工具。在终端窗口中,运行以下命令,记住后续的所有命令都必须在激活的虚拟环境中执行——这意味着你应该在终端提示符中看到(venv)
文本:
(venv) $ pip install --upgrade pip
...output truncated...
上面的upgrade
命令可能需要一两分钟才能完成,并且可能会在终端上输出大量文本。
遇到pip
的问题了吗?如果在使用pip
安装包时出现大量红色错误和异常,尝试首先升级pip
版本,使用pip install --upgrade pip
。这是在创建新的 Python 虚拟环境后推荐的第一步。
- 现在
pip
已经升级,我们可以使用pip list
命令来查看虚拟环境中已经安装的 Python 包:
(venv) $ pip list
pip (9.0.1)
pkg-resources (0.0.0)
setuptools (33.1.1)
在上面我们看到的是我们新建虚拟环境中的默认 Python 包。如果确切的包列表或版本号与示例不完全匹配,不要担心。
- 使用
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"
- 为了安装我们需要的两个 GPIO 包,我们可以使用
pip install
命令,如下例所示:
(venv) $ pip install gpiozero pigpio
Collecting gpiozero...
... output truncated ...
- 现在,再次运行
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
已经自动为我们安装了它。
- 重新运行
python gpio_pkg_check.py
,现在我们看到我们的 Python 模块可以被导入了:
(venv) $ python gpio_pkg_check.py
GPIOZero Available
pigpio Available
太好了!我们现在有了一个安装了两个 GPIO 包的虚拟环境。在你进行 Python 项目时,你将不可避免地安装更多的包并希望跟踪它们。
- 使用
pip freeze
命令对你之前安装的包进行快照:
(venv) $ pip freeze > requirements.txt
上面的例子将所有安装的包冻结到一个名为requirements.txt
的文件中,这是一个常用的文件名。
- 查看
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_install
和setuptools
的软件包安装程序和工具。两者都有其用途;但是,大多数情况下您会依赖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 之间会有差异),不同的虚拟环境工具之间也会有差异(例如venv
与pipenv
)。因此,在涉及许多开发人员在不同计算机上工作的项目中,虚拟环境通常不具备可移植性。
现在我们已经简要探讨了文件和文件夹的结构以及理解这种结构的重要性,我们将继续并查看运行沙盒化到虚拟环境的脚本的替代方法。
执行 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 并在启动时运行脚本的步骤:
- 在您的项目文件夹中,创建一个 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
部分是必要的,以确保错误和标准输出都被记录。
- 将
run_on_boot.sh
文件标记为可执行文件:
$ chmod u+x run_on_boot.sh
如果您不熟悉chmod
命令(chmod表示更改模式),我们正在给操作系统权限来执行run_on_boot.sh
文件。u+x
参数表示对当前**用户,使文件可执行。要了解更多关于chmod
的信息,您可以在终端中输入chmod --help
或man chmod
。
- 编辑您的
crontab
文件,这是存储cron
调度规则的文件:
$ crontab -e
- 将以下条目添加到您的
crontab
文件中,使用在步骤 1中创建的run_on_boot.sh
bash 脚本的绝对路径:
@reboot /home/pi/pyiot/chapter01/run_on_boot.sh &
不要忘记在行尾加上&
字符。这样可以确保脚本在后台运行。
- 在终端中手动运行
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
- 重新启动您的树莓派:
$ sudo reboot
- 一旦您的树莓派完成重新启动,
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 引脚。现在让我们来做这件事。
以下是要遵循的步骤:
- 从您的 Raspbian 桌面,导航到 Raspberry 菜单|首选项|Raspberry Pi 配置,如图 1.2所示:
图 1.2 - Raspberry Pi 配置菜单项的位置或者,可以使用sudo raspi-config
命令在命令行中管理接口,并导航到“接口选项”菜单。
- 按照以下截图中显示的方式启用所有接口:
图 1.3 - Raspberry Pi 配置对话框
- 点击“确定”按钮。
在您点击“确定”按钮后,您可能会被提示重新启动您的 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 连接到互联网作为我们的网络层。
问题
最后,这里是一些问题列表,供您测试对本章材料的了解。您将在书的评估部分找到答案:
-
为什么您应该始终为 Python 项目使用虚拟环境的主要原因是什么?
-
您是否需要或应该将虚拟环境文件夹(即
venv
)放入版本控制? -
为什么要创建
requirements.txt
文件? -
您需要以 root 用户身份运行 Python 脚本。为确保脚本在其预期的虚拟环境上执行,您必须采取哪些步骤?
-
source venv/bin/activate
命令是做什么的? -
您处于已激活的虚拟环境中。离开虚拟环境并返回主机 shell 的命令是什么?
-
您在 PyCharm 中创建了一个 Python 项目和虚拟环境。您能在终端上处理和运行项目的 Python 脚本吗?
-
您想要一个 GUI 工具来编辑和测试树莓派上的 Python 代码,但没有安装 PyCharm。您可以使用 Python 和 Raspbian 预安装的工具。
-
您在 Python 和电子知识方面有所进步,并尝试使用 I2C 将设备连接到树莓派,但无法使其工作。可能的问题是什么,以及您如何解决它?
进一步阅读
我们在本章介绍了venv
虚拟环境工具。以下是它的官方文档链接:
如果您想了解virtualenv
和pipenv
替代虚拟环境工具,这里是它们的官方文档:
-
virtualenv
主页:virtualenv.pypa.io/en/latest
-
pipenv
主页:docs.pipenv.org/en/latest
以下是Python 包装指南的链接。在这里,您将找到有关 Python 包管理的全面指南,包括pip
和 easy-install/setup 工具的替代方法:
- Python 包装用户指南:
packaging.python.org
如果您希望了解更多关于调度和 cron 的知识,这里有两个资源供您开始:
-
cron 语法概述(以及 GUI 工具):
www.raspberrypi.org/documentation/linux/usage/cron.md
-
关于 cron 语法的详细教程:
opensource.com/article/17/11/how-use-cron-linux
第二章:开始使用 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
中安装:
-
GPIOZero:GPIOZero GPIO 库(
pypi.org/project/gpiozero
) -
PiGPIO:PiGPIO GPIO 库(
pypi.org/project/pigpio
) -
Requests:用于发出 HTTP 请求的高级 Python 库(
pypi.org/project/requests
)
我们将需要一些物理电子元件:
-
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-E和F-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中编号的黑色圆圈相匹配:
-
按照所示将按钮放在面包板上。按钮放入哪一行孔并不重要,但是图 2.3显示了按钮的位置(左上腿)在孔 B10 处。
-
接下来,将一根跳线插入与按键最顶端腿部相同的那一行(我们的插图使用孔 A10)。将这根导线的另一端连接到树莓派的 GPIO 引脚头部外缘向下数第八个引脚。这个引脚被称为 GPIO 23。
你可以获得引脚标签和面包板兼容模块,以帮助你进行树莓派引脚连接和识别。这里有一个可打印版本的链接,可以帮助你入门:github.com/splitbrain/rpibplusleaf
。我们将在第五章中介绍 GPIO 引脚及其编号,将你的树莓派连接到物理世界。
- 最后,使用另一根线(标记为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中编号的黑色圆圈相匹配:
- 按照插图将 LED 连接到面包板上,确保 LED 安装的方向正确。我们的插图显示了 cathode 腿在 E15 孔,anode 腿在 E16 孔。
你可能需要弯曲 LED 的腿来使其就位。在安装 LED 时,请确保两条腿不要相互接触!如果它们接触,这将导致所谓的电短路,LED 电路部分将无法工作。
-
接下来,使用一根跳线(标记为gnd"),将 LED 的 cathode 腿连接到与按钮共享的相同电源轨。我们展示了这个连接,一端连接在 A15 孔,另一端连接到左侧负(-)电源轨上的附近孔。
-
最后,使用另一根跳线(标记为gnd),将负(-)电源轨连接到树莓派 GPIO 引脚头的第 17 个外侧引脚。这个引脚是树莓派上的地线(GND)引脚。
干得好!这就是我们的 LED 连接。接下来,我们添加电阻,这将完成我们的电路。
定位和连接电阻
电阻是用来限制(即阻止)电流流动和分压的电子元件,它们是非常常见的电子元件。
图 2.6中显示了一个物理电阻(左侧)和两个原理图符号(右侧)。这些原理图符号之间没有实际区别。它们代表不同的文档标准,您会发现原理图图表的作者会选择并坚持使用一种类型的符号。我们将在本书中始终使用这种波浪形符号:
图 2.6 - 电阻和原理图符号
电阻有许多形状、大小和颜色。作为一般指南,它们的物理形状和大小与它们的物理特性和能力有关,而它们外壳的颜色通常是无关紧要的,至少就它们的性能而言。然而,电阻上的彩色条带非常重要,因为它们标识了电阻的值。值得一提的是,小型通用电阻(我们将使用的)使用彩色条带来指定它们的值,而在高功率应用中使用的物理上更大的电阻通常在外壳上印有它们的电阻值。
电阻是一种无偏电子元件,这意味着它们可以以任何方式安装在电路中。然而,它们的值需要正确选择,否则电路可能无法按预期工作,或者更糟的是,电阻和/或其他组件(包括您的树莓派)可能会受到损坏。
在开始学习电路时,强烈建议并且最安全的做法是始终使用电路所列的预期电阻值。避免在没有正确值的情况下替换不同的值,因为这可能会导致元件甚至您的树莓派受损。
我们在本书中对电阻的使用将是实用的。虽然我将解释我们为什么选择特定值以及如何得出这些值的原因,从第六章开始,软件工程师的电子学 101。如果您对电阻不熟悉,您会在进一步阅读部分找到两个链接,您可以在其中了解更多信息,包括如何读取它们的值。
图 2.7展示了我们需要创建的电阻连接。请在按照接下来的步骤时参考这个图:
图 2.7 - 面包板上完成的按钮和 LED 电路
这是如何将电阻连接到您的面包板中的。以下步骤编号与图 2.7中编号的黑色圆圈相匹配:
-
将电阻的一条腿(无论哪一条)插入与 LED 的阳极腿相同行的孔中。这个连接显示在 D16 孔。将另一条腿插入一个空行,显示在 D20(在连接下一个导线之前,它将是面包板上的一个空行)。
-
使用跳线(从 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中编号的黑色圆圈相匹配:
-
从标有wire 1的面包板开始。如果我们看这根导线的两端,我们会发现一端连接到树莓派上的 GPIO 23,而另一端(在孔 A10 处)连接到一个与按钮共享的行。
-
从原理图来看,这个面包板连接通过标有wire 1的线条图示。你会注意到线条的一端标有 GPIO23,而另一端通向按钮符号的一侧。
导线外壳的颜色没有固有的含义。颜色只是一种视觉辅助,用于区分不同的导线和连接。然而,有一些常见的惯例,比如使用红色导线表示正电源连接,黑色导线表示负极或地线。
-
接下来,从面包板上按钮的另一侧(孔 A12)开始,注意标有*gnd’*的导线。这根导线将按钮连接到面包板上的外部电源轨。
-
从这个第一个电源轨连接下来的五个孔,我们看到第二根地线(标有gnd),从面包板返回到树莓派上的一个 GND 引脚。
-
在原理图中,面包板上的gnd和gnd’线连接被表示为标有gnd的线,这条线从按钮出来并以一个向下的箭头符号注释为GND(记住面包板上的gnd和gnd’是电连接的,因此在逻辑上是一根线)。这是地线连接的符号,你会经常在原理图中看到这个符号。当我们到达标题为阅读和理解地线符号*的部分时,我会对这个符号有更多的话要说。
-
检查原理图中的按钮符号,你会注意到wire 1和gnd线并没有连接,而是终止在按钮符号(小圆圈)。这被称为常开连接,或者在我们的特定情况下,是常开开关。你可以把常开理解为意味着线路断开(记住线代表导线)。现在,如果你想象按钮被按下,那么按钮会触碰每个圆圈,并连接蓝色和gnd线,形成一个闭合连接,完成了 GPIO 23 和 GND 之间的电路。我们将在第六章中更详细地讨论这个概念,软件工程师的电子学 101。
当你确信你理解了面包板上按键的连接如何与原理图上的按键部分匹配时,我们将继续讨论 LED 和电阻的连接。
阅读 LED 和电阻的原理图连接
继续上一节,我们学习了如何阅读和理解原理图上按键部分,接下来我们将通过讨论 LED 和电阻的连接来完成我们的解释,如下所示:
图 2.10 - 组合面包板和原理图,第二部分
以下是如何阅读 LED 和电阻的连接。以下步骤编号与图 2.10中编号的黑色圆圈相匹配:
-
从面包板上标有wire 2的线开始。这根线将树莓派上的 GPIO 21 连接到一个端子上,这个端子与一个电阻的一端(孔 A25)共用。
-
原理图上也标有wire 2的线连接。
-
在面包板上,电阻的另一端连接到 LED 的阳极腿(孔 E15)。记住,电阻和 LED 的阳极腿是电连接的,因为它们在面包板上的同一排孔中的同一组孔中。
-
我们在原理图中看到了电阻/LED 连接,电阻符号与 LED 符号相遇。我们知道电阻通过 LED 符号的方向连接到了 LED 的阳极一侧。
-
接下来,在面包板上,LED 的另一端(孔 E15)——阴极腿——连接到gnd’线(孔 A15),然后连接回外部电源轨,这也是按键的gnd’线所共用的(然后通过gnd线连接回树莓派的 GND 引脚)。
-
最后,在原理图上,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 包中导入
Device
和LED
类。 -
在第(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,使用Device
和PiGPIOFactory
导入。
既然我们已经看到了如何设置 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()
时使用led
的toggle()
方法打开和关闭。在第(4)行,我们查询led
的value
属性来确定 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,我们将在下一节深入探讨代码的机制以及它是如何工作的。
以下是要遵循的步骤:
- 运行
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。
- 将 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 在本地网络或互联网上的状态。
问题
以下是一些问题列表,供您测试对本章材料的了解。您将在本书的评估部分找到答案:
-
你没有正确的电阻值。你能否用周围的另一个电阻值替代?
-
GPIOZero 包是一个完整的 GPIO 库。这就是你所需要的全部吗?
-
在可能的情况下,您应该始终使用内置的 Python 网络包吗?
-
真或假:LED 是无偏的,意味着它可以以任何方式插入电路并仍然工作。
-
您正在构建一个与其他现有网络设备交互的物联网应用程序,但它超时了。可能出了什么问题?
-
哪个 Python 模块和函数可以用来阻止程序退出?
进一步阅读
我们使用 dweet.io 服务将 LED 连接到互联网,并调用其 RESTful API,这些 API 在以下文档中有所记录:
- Dweet.io API 文档:
dweet.io
您可能希望简要了解一下 GPIOZero 库,以了解它的功能。它有大量的示例和详细的文档。以下是我们目前涵盖的 API 文档的一些有用链接:
-
GPIOZero 首页:
gpiozero.readthedocs.io
关于 PiGPIO,这里是其 API 文档的相关部分。您会注意到 PiGPIO 是一个更高级的 GPIO 库,文档不太冗长。
-
PiGPIO Python 首页:
abyz.me.uk/rpi/pigpio/python.html
-
write()
方法:abyz.me.uk/rpi/pigpio/python.html#write
-
callback()
方法:abyz.me.uk/rpi/pigpio/python.html#callback
-
set_glitch_filter()
:abyz.me.uk/rpi/pigpio/python.html#set_glitch_filter
电阻器是一种非常常见的电子元件。以下资源提供了电阻器的概述,以及如何阅读它们的色带以确定它们的电阻值(欧姆):
以下 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
中安装:
-
GPIOZero:GPIOZero GPIO 库(
pypi.org/project/gpiozero
) -
PiGPIO:PiGPIO GPIO 库(
pypi.org/project/pigpio
) -
Flask:核心 Flask 微服务框架(
pypi.org/project/Flask
) -
Flask-RESTful:用于创建 RESTful API 服务的 Flask 扩展(
pypi.org/project/Flask-RESTful
) -
Flask-SocketIO:用于创建 Web Socket 服务的 Flask 扩展(
pypi.org/project/Flask-SocketIO
)
我们将使用我们在第二章中创建的面包板电路,使用 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 服务器,请执行以下步骤:
- 使用以下命令运行我们的 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
- 我们将很快创建一个网页来与 API 进行交互;但是,现在,请在 Web 浏览器中浏览到
http://localhost:5000
,并验证您是否可以使用网页上的滑块来更改 LED 的亮度。
我们的示例 URL 是http://localhost:5000
,但是,如果您使用树莓派的 IP 地址而不是localhost
,您将能够从本地网络上的其他设备访问该网页。
以下截图是您将看到的网页的示例:
图 3.1 - RESTful API 客户端网页
- 我们还可以使用
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
- 下一个命令执行 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 扩展时会使用这个变量。在我们的app
和api
变量之后,我们定义了额外的全局变量。
全局变量
以下全局变量在整个服务器中使用。首先,我们有 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
,而不是LED
。PWMLED
允许我们使用一种称为脉冲宽度调制的技术来控制 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
映射到LEDControl
Python 类,因为我们正在进行 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
来自LEDControl
)payload
参数中的数据。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 套接字服务器的代码。请在继续之前查看这个文件。
当你查看了代码后,我们将运行我们的服务器。以下是要遵循的步骤:
- 使用以下命令运行 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
- 在网页浏览器中访问
http://localhost:5000
URL。你会得到一个带有滑块的网页,如图 3.2所示。虽然网页的视觉外观与 RESTful API 服务器的网页相似,但底层的 JavaScript 是不同的:
图 3.2 - Web Socket 客户端网页
验证你是否可以使用网页上的滑块来改变 LED 的亮度。
打开第二个网页浏览器并访问http://localhost:5000
(现在你有两个页面打开)。改变滑块,你会看到两个页面保持同步并实时更新!神奇的是,你已经发现了 Web Sockets 相对于 RESTful API 所提供的独特优势。
- 在网页上找到连接到服务器的行: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 装饰器符号注册message或event处理程序。每个@socketio.on(<event_name>)
的参数是我们的服务器将监听的事件的名称。connect
和disconnect
事件(在下面)是两个保留事件。每当客户端连接到服务器或断开连接时,这些处理程序都会被调用。
你会注意到,在第(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 行看到了data
的level
子属性。所有来自客户端的数据都是字符串,所以在这里我们验证数据并在下一行将其转换为整数。在 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 控制相关的客户端和服务器端事件都称为相同的名称,led
。state
参数是要传递给客户端的数据。与 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,即消息队列遥测传输协议。
问题
最后,这里是一些问题列表,供您测试对本章材料的了解。您将在书的评估部分找到答案:
-
Flask-RESTful 扩展的哪个特性可以帮助我们验证客户端的输入数据?
-
什么通信协议可以用于提供客户端和服务器之间的实时全双工通信?
-
我们如何使用 Flask-SocketIO 进行请求数据验证?
-
Flask 的
templates
文件夹是什么? -
在使用 jQuery 时,我们应该在哪里创建组件事件监听器并初始化我们的网页内容?
-
什么命令行工具可以用来向 RESTful API 服务发出请求?
-
当我们改变
PWMLED
实例的value
属性时,物理 LED 会发生什么?
进一步阅读
在本章中我们经常提到“RESTful”这个词,但没有深入讨论它的确切含义。如果您想了解所有细节,可以在 SitePoint.com 上找到一篇很好的入门教程。
- SitePoint.com 上的 REST:
www.sitepoint.com/developers-rest-api
我们的 RESTful API 示例几乎没有涉及 Flask 和 Flask-RESTful 的基础知识,但提供了一个可以构建的工作示例。我鼓励您至少阅读 Flask 快速入门指南,然后再阅读 Flask RESTful 快速入门指南,以获得对这两个框架的良好基础和理解:
-
Flask-RESTful 快速入门:
flask-restful.readthedocs.io/en/latest/quickstart.html
正如在本章中所述,在标题为介绍 Flask 微服务框架的部分中,如果您在 Flask-RESTful 中遇到困难并且在其文档中找不到答案,您还应该参考官方的核心 Flask 文档:
- Flask 文档:
flask.palletsprojects.com
我们还只是初步了解了使用 Flask-SocketIO 和 Socket.IO 的 Web 套接字。以下链接指向官方的 Flask-SocketIO 和 Socket.IO 库。我还包括了两个额外的链接,提供了关于 Web 套接字的通用和简单介绍。作为提醒,Web 套接字是一种最好通过实践而不是阅读来学习的技术,特别是对于新手服务器开发者来说。因此,当您阅读关于 Web 套接字的入门材料时,期望核心潜在概念将通过各种不同的代码示例和库来进行说明,除了我们在本章中使用的 Flask-SocketIO 和 Socket.IO 库:
-
Flask-SocketIO:
flask-socketio.readthedocs.io
-
Socket.IO(JavaScript 库):
socket.io
-
Web 套接字基础知识:
www.html5rocks.com/en/tutorials/websockets/basics
-
Web 套接字基础知识:
medium.com/@dominik.t/what-are-web-sockets-what-about-rest-apis-b9c15fd72aac