Python-开发者的树莓派-3-秘籍-全-
Python 开发者的树莓派 3 秘籍(全)
原文:
zh.annas-archive.org/md5/d7dcc3fe2ea1927a3e18eff14d3be78e
译者:飞龙
前言
这本书旨在为任何想要使用树莓派构建软件应用或硬件项目的人提供帮助。本书逐步介绍了文本分类、创建游戏、3D 图形和情感分析等内容。我们还逐步深入到更高级的主题,例如构建计算机视觉应用、机器人以及神经网络应用。具备基本的 Python 知识将是非常理想的;然而,所有编程概念都进行了详细的解释。所有示例均使用 Python 3 编写,并提供了清晰且详细的解释,以便您能够适应并在自己的项目中使用所有这些信息。到本书结束时,您将具备使用树莓派构建创新软件应用和硬件项目所需的所有技能。
这本书面向的对象
这本书适合任何想要使用 Raspberry Pi 3 掌握 Python 编程技能的人。具备 Python 基础知识将是一个额外的优势。
为了最大限度地利用这本书
阅读者应了解 Python 编程的基础知识。
读者对机器学习、计算机视觉和神经网络有一个基本了解将会有益。
下载示例代码文件
您可以从www.packtpub.com的账户下载本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便直接将文件通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
在 www.packtpub.com 登录或注册。
-
选择“支持”标签。
-
点击代码下载与勘误表。
-
在搜索框中输入书籍名称,并遵循屏幕上的指示。
一旦文件下载完成,请确保您使用最新版本解压或提取文件夹:
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
-
7-Zip/PeaZip for Linux
该书的代码包也托管在 GitHub 上,地址为github.com/PacktPublishing/Raspberry-Pi-3-Cookbook-for-Python-Programmers-Third-Edition
。如果代码有更新,它将在现有的 GitHub 仓库中进行更新。
我们还有其他来自我们丰富的书籍和视频目录的代码包,可在github.com/PacktPublishing/
找到。去看看吧!
下载彩色图片
我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:
Raspberry Pi 3 Cookbook for Python Programmers Third Edition with Color Images.
使用的约定
本书使用了多种文本约定。
CodeInText
: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:
"在这里我们使用bind
函数,它将此小部件(the_canvas
)上发生的特定事件绑定到特定的动作或按键。"
代码块设置如下:
#!/usr/bin/python3
# bouncingball.py
import tkinter as TK
import time
VERT,HOREZ=0,1
xTOP,yTOP = 0,1
xBTM,yBTM = 2,3
MAX_WIDTH,MAX_HEIGHT = 640,480
xSTART,ySTART = 100,200
BALL_SIZE=20
RUNNING=True
任何命令行输入或输出都应按照以下格式编写:
sudo nano /boot/config.txt
粗体: 表示新术语、重要单词或屏幕上出现的单词。例如,菜单或对话框中的单词在文本中会这样显示。以下是一个示例:“点击“配对”按钮开始配对过程并输入设备的 PIN 码。”
警告或重要提示会像这样显示。
小贴士和技巧看起来是这样的。
部分
在这本书中,您会发现几个经常出现的标题(准备就绪,如何操作...,它是如何工作的...,还有更多...,以及另请参阅)。
为了清楚地说明如何完成食谱,请按照以下部分使用:
准备就绪
本节向您介绍在食谱中可以期待的内容,并描述如何设置任何软件或为食谱所需的任何初步设置。
如何做到这一点...
本节包含遵循食谱所需的步骤。
它是如何工作的...
本节通常包含对上一节发生事件的详细解释。
还有更多...
本节包含有关食谱的附加信息,以便您对食谱有更深入的了解。
参见
本节提供了对其他有用信息的链接,以帮助食谱的制作。
联系我们
我们始终欢迎读者的反馈。
总体反馈:请发送电子邮件至 feedback@packtpub.com
并在邮件主题中提及书籍标题。如果您对本书的任何方面有疑问,请通过 questions@packtpub.com
发送邮件给我们。
勘误: 尽管我们已经尽最大努力确保内容的准确性,错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告这一点,我们将不胜感激。请访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
盗版: 如果你在互联网上以任何形式遇到我们作品的非法副本,如果你能提供位置地址或网站名称,我们将不胜感激。请通过发送链接至copyright@packtpub.com
与我们联系。
如果您想成为一名作者:如果您在某个领域有专业知识,并且对撰写或参与一本书籍感兴趣,请访问 authors.packtpub.com。
评论
请留下您的评价。一旦您阅读并使用了这本书,为何不在购买它的网站上留下评价呢?潜在读者可以查看并使用您的客观意见来做出购买决定,我们 Packt 公司可以了解您对我们产品的看法,而我们的作者也可以看到他们对书籍的反馈。谢谢!
如需了解 Packt 的更多信息,请访问packtpub.com.
第一章:Raspberry Pi 3 电脑入门
在本章中,我们将介绍以下食谱:
-
将外围设备连接到树莓派
-
使用 NOOBS 设置您的树莓派 SD 卡
-
通过 LAN 连接器将你的树莓派连接到互联网进行网络连接
-
使用树莓派内置的 Wi-Fi 和蓝牙
-
手动配置您的网络
-
直接连接到笔记本电脑或计算机
-
通过 USB Wi-Fi 网卡将你的树莓派连接到互联网进行网络连接
-
通过代理服务器连接到互联网
-
通过 VNC 在网络上远程连接到 Raspberry Pi
-
通过 SSH(以及 X11 转发)远程连接到 Raspberry Pi 网络
-
将树莓派的主文件夹通过 SMB 共享
-
保持树莓派更新
简介
本章介绍了树莓派及其首次设置的流程。我们将把树莓派连接到合适的显示器、电源和外设。我们将在 SD 卡上安装操作系统。这是系统启动所必需的。接下来,我们将确保可以通过本地网络成功连接到互联网。
最后,我们将利用网络提供远程连接到和/或控制 Raspberry Pi 的方法,以及确保系统保持更新的途径。
一旦完成本章中的步骤,您的树莓派(Raspberry Pi)将准备就绪
为您用于编程。如果您已经设置了并运行了您的树莓派,请确保您浏览以下部分,因为这里有许多有用的提示。
介绍 Raspberry Pi
树莓派是由树莓派基金会创建的单板计算机,该基金会是一个慈善机构,其主要目的是将低级计算机技能重新介绍给英国的孩子们。目标是重新点燃 20 世纪 80 年代的微型计算机革命,这场革命培养了一代熟练的程序员。
即使在 2012 年 2 月底电脑发布之前,很明显 Raspberry Pi 已经在全球范围内获得了巨大的追随者群体,并且在撰写本书时,销量已超过 1000 万部。以下图片展示了几个不同的 Raspberry Pi 型号:
树莓派 3B、Model A+和 Pi Zero
这个名字是什么意思?
Raspberry Pi 这个名字是结合了想要创造一个以水果命名的替代电脑(例如 Apple、BlackBerry 和 Apricot)的愿望,以及对原始概念的一种致敬,即一个可以用 Python(简称 Pi)编程的简单电脑。
在这本书中,我们将使用这个小电脑,了解如何设置它,然后逐章探索其功能,使用 Python 编程语言。
为什么选择 Python?
经常有人问,“为什么选择 Python 作为在树莓派上使用的语言?”事实上,Python 只是可以在树莓派上使用的众多编程语言之一。
你可以选择许多编程语言,从高级图形块编程,例如Scratch,到传统的C语言,再到BASIC,甚至原始的机器代码汇编器。一个好的程序员通常需要成为代码多语言者,以便能够发挥每种语言的优势和劣势,以最好地满足他们期望的应用需求。了解不同的语言(以及编程技术)如何尝试克服将你想要的转换为你得到的的挑战是有用的,因为这也是你在编程时试图做到的事情。
Python 被选为学习编程的绝佳起点,因为它提供了一套丰富的编码工具,同时仍然允许在不费周折的情况下编写简单的程序。这使得初学者可以逐步接触并了解现代编程语言所基于的概念和方法,而无需一开始就全部掌握。它非常模块化,拥有许多可以导入的附加库,可以快速扩展功能。你会发现,随着时间的推移,这会鼓励你做同样的事情,你将想要创建自己的模块,并将它们集成到自己的程序中,从而迈出结构化编程的第一步。
Python 解决了格式和展示方面的问题。由于缩进可以增加更好的可读性,因此在 Python 中缩进非常重要。它们定义了代码块是如何组合在一起的。一般来说,Python 运行较慢;因为它被解释,所以在运行程序时创建模块需要花费时间。如果你需要响应时间敏感的事件,这可能会成为一个问题。然而,你可以预编译 Python 或使用用其他语言编写的模块来克服这个问题。
它隐藏了细节;这既是优点也是缺点。对于初学者来说非常好,但当你需要猜测诸如数据类型等细节时可能会有些困难。然而,这也迫使你考虑所有可能性,这可能是件好事。
Python 2 和 Python 3
对于初学者来说,一个巨大的困惑来源是,树莓派上有两个版本的 Python(版本 2.7和版本 3.6),它们彼此不兼容,因此为 Python 2.7 编写的代码可能无法在 Python 3.6 上运行(反之亦然)。
Python 软件基金会持续致力于改进和推动语言的发展,这有时意味着他们必须牺牲向后兼容性以接纳新的改进(并且,重要的是,移除冗余和过时的做法)。
支持 Python 2 和 Python 3
有许多工具可以帮助您从 Python 2 转换到 Python 3,包括像 2to3
这样的转换器,它将解析并更新您的代码以使用 Python 3 方法。这个过程并不完美,在某些情况下,您可能需要手动重写部分代码并全面重新测试所有内容。您可以编写同时支持两者的代码和库。import __future__
语句允许您导入 Python 3 的友好方法,并使用 Python 2.7 运行它们。
你应该使用哪个版本的 Python?
实际上,选择使用哪个版本将取决于你的意图。例如,你可能需要 Python 2.7 库,这些库在 Python 3.6 中尚未提供。Python 3 自 2008 年以来就已经可用,因此这些往往是较旧或较大的库,尚未被翻译。在许多情况下,有新的替代方案可以替代旧库;然而,它们的支持情况可能有所不同。
在这本书中,我们使用了 Python 3.6,它也兼容 Python 3.5 和 3.3。
Raspberry Pi 系列 - Pi 的简要历史
自从发布以来,树莓派经历了各种迭代,包括对原始树莓派 B 型单元的小型和大型的更新与改进。尽管一开始可能会感到困惑,但市面上有三种基本的树莓派型号(以及一个特殊型号)。
主要旗舰型号被称为Model B。这款型号拥有所有连接和功能,以及最大 RAM 和最新的处理器。多年来,已经推出了几个版本,最显著的是Model B(最初有 256 MB RAM,后来升级到 512 MB RAM)以及Model B+(将 26 针 GPIO 增加到 40 针,改为使用 microSD 卡槽,并且有四个 USB 端口而不是两个)。这些原始型号都使用了 Broadcom BCM2835 系统级芯片(SOC),由一个 700 MHz 的 ARM11 核心和 VideoCore IV 图形处理单元(GPU)组成。
2015 年发布的树莓派 2B 型(也称为 2B)引入了新的 Broadcom BCM2836 系统单芯片(SOC),提供了一款四核 32 位 ARM Cortex-A7 1.2 GHz 处理器和 GPU,以及 1 GB 的 RAM。改进的 SOC 增加了对 Ubuntu 和 Windows 10 IoT 的支持。最后,我们迎来了最新的树莓派 3B 型,它使用了另一款新的 Broadcom BCM2837 SOC,该 SOC 提供了一款四核 64 位 ARM Cortex-A53 处理器和 GPU,同时还集成了板载 Wi-Fi 和蓝牙。
模型 A 一直被视为一个简化版本。虽然与模型 B 具有相同的 SOC,但连接有限,仅包含一个 USB 端口,没有有线网络(LAN)。模型 A+又增加了更多的 GPIO 引脚和一个 microSD 插槽。然而,内存后来升级到 512 MB RAM,但仍然只有一个 USB 端口/没有 LAN。模型 A 上的 Broadcom BCM2835 SOC 至今未更新(因此仍然是单核 ARM11);然而,有一个模型 3A(很可能是使用 BCM2837)。
Pi Zero 是一种超紧凑的 Raspberry Pi 版本,专为成本和空间至关重要的嵌入式应用而设计。它拥有与其他型号相同的 40 引脚 GPIO 和 microSD 卡槽,但缺少了板载显示(CSI 和 DSI)连接。它仍然配备了 HDMI(通过 mini-HDMI)和一个单独的 micro USB on-the-go (OTG)连接。尽管在 Pi Zero 的第一版中并未提供,但最新的型号也包括了用于板载摄像头的 CSI 连接。
Pi Zero 于 2015 年闻名发布,并随 Raspberry Pi 基金会的杂志《The MagPi》一同赠送,这使得该杂志成为首个在封面上赠送电脑的杂志!这确实让我感到相当自豪,因为(正如你可能在本书开头的传记中读到的那样)我是该杂志的创始人之一。
该特殊型号被称为计算模块。它采用 200 针 SODIMM 卡的形式。它适用于工业用途或商业产品中,其中所有外部接口都由主机/主板提供,模块将被插入其中。例如产品包括 Slice 媒体播放器(fiveninjas.com
)和 OTTO 相机。当前模块使用 BCM2835,尽管有一个更新的计算模块(CM3)。
Raspberry Pi 的维基百科页面提供了所有不同变体及其规格的完整列表:
zh.wikipedia.org/wiki/Raspberry_Pi#规格
此外,Raspberry Pi 产品页面提供了关于可用型号和配件规格的详细信息:www.raspberrypi.org/products/
应该选择哪个 Pi?
本书的所有章节都与所有当前的 Raspberry Pi 版本兼容,但建议从 Model 3B 开始,因为它是最适合入门的最佳型号。这提供了最佳的性能(尤其是对于第七章,创建 3D 图形,以及第六章,检测图像中的边缘和轮廓)中使用的 OpenCV 示例非常有用),众多连接端口,以及内置 Wi-Fi,这可以非常方便。
Pi Zero 推荐用于那些需要低功耗或减小重量/尺寸但不需要 Model 3B 完整处理能力的项目。然而,由于其超低成本,Pi Zero 在你开发完项目后部署完成项目是理想的。
连接到树莓派
有许多方法可以连接 Raspberry Pi 并使用各种接口来查看和控制内容。对于典型使用,大多数用户将需要电源、显示(带音频)以及一种输入方法,例如键盘和鼠标。要访问互联网,请参考 通过 LAN 连接器连接 Raspberry Pi 到互联网的网络和连接 或 在 Raspberry Pi 上使用内置 Wi-Fi 和蓝牙 的食谱。
准备就绪
在您可以使用您的树莓派之前,您需要一个安装了操作系统或带有新开箱系统(NOOBS)的 SD 卡,正如在使用 NOOBS 设置您的树莓派 SD 卡菜谱中所述。
以下部分将详细介绍您可以连接到树莓派的设备类型,以及如何和在哪里将它们插入。
如您稍后会发现,一旦您设置了您的 Raspberry Pi,您可能会决定通过远程连接并通过网络链路使用它,在这种情况下,您只需要电源和网络连接。请参考以下章节:通过 VNC 远程连接到 Raspberry Pi 的网络和通过 SSH(以及 X11 转发)远程连接到 Raspberry Pi 的网络。
如何做到这一点...
Raspberry Pi 的布局如图所示:
Raspberry Pi 连接布局(型号 3 B、型号 A+ 和 Pi Zero)
关于前述图示的更多信息如下:
-
显示:树莓派支持以下三种主要显示连接;如果同时连接了 HDMI 和复合视频,它将默认只使用 HDMI:
- HDMI: 为了获得最佳效果,请使用具有 HDMI 连接的电视或显示器,从而实现最佳分辨率显示(1080p)以及数字音频输出。如果您的显示器有 DVI 连接,您可能可以使用适配器通过 HDMI 连接。DVI 连接有几种类型;一些支持模拟(DVI-A),一些支持数字(DVI-D),还有一些两者都支持(DVI-I)。树莓派只能通过 HDMI 提供数字信号,因此建议使用 HDMI-to-DVI-D 适配器(以下截图中有勾选标记)。这缺少了四个额外的模拟引脚(以下截图中有叉号标记),因此可以适配 DVI-D 和 DVI-I 类型的插座:
HDMI 至 DVI 连接(DVI-D 适配器)
如果你希望使用老式的显示器(带有 VGA 连接),则需要一个额外的 HDMI-to-VGA 转换器。树莓派也支持一个基本的 VGA 适配器(VGA Gert666 适配器),该适配器直接由 GPIO 引脚驱动。然而,这会占用 40 引脚头上的所有引脚,除了四个(较老的 26 引脚型号将不支持 VGA 输出):
HDMI 转 VGA 适配器
-
- 模拟: 另一种显示方法是使用模拟复合视频连接(通过电话插座);这也可以连接到 S-Video 或欧洲 SCART 适配器。然而,模拟视频输出的最大分辨率为 640 x 480 像素,因此它并不适合一般用途:
3.5 毫米电话模拟连接
当使用 RCA 连接或 DVI 输入时,音频必须通过模拟音频连接单独提供。为了简化制造过程(通过避免通孔组件),Pi Zero 没有模拟音频或 RCA 插孔用于模拟视频(尽管可以通过一些修改添加它们):
-
- 直接显示 DSI: 由树莓派基金会生产的触摸显示屏将直接连接到 DSI 插槽。这可以在同时连接和使用 HDMI 或模拟视频输出的情况下创建一个双显示设置。
-
立体声模拟音频(除 Pi Zero 外):这为耳机或放大扬声器提供模拟音频输出。可以通过桌面上的 Raspberry Pi 配置工具在模拟(立体声插座)和数字(HDMI)之间切换音频,或者使用
amixer
或alsamixer
命令行工具进行切换。
要获取终端中特定命令的更多信息,你可以在终端读取手册之前使用以下man
命令(大多数命令都应该有一个):
man amixer
一些命令也支持--help
选项以获得更简洁的帮助,如下所示:
amixer --help
-
网络(不包括模型 A 和 Pi Zero):网络连接将在本章后面的通过 LAN 连接器连接 Raspberry Pi 到互联网的网络和连接部分进行讨论。如果我们使用 Model A Raspberry Pi,则可以添加 USB 网络适配器以添加有线网络甚至无线网络(请参阅通过 USB Wi-Fi 外置设备连接 Raspberry Pi 到互联网的网络和连接部分)。
-
USB (1x 型号 A/零,2x 型号 1 B,4x 型号 2 B 和 3 B): 使用键盘和鼠标:
-
Raspberry Pi 应该与大多数 USB 键盘和鼠标兼容。您还可以使用无线鼠标和键盘,这些设备使用射频(RF)适配器。然而,对于使用蓝牙适配器的设备,需要额外的配置。
-
如果电源供应不足或设备消耗电流过多,您可能会遇到键盘按键似乎粘滞的情况,在严重情况下,还可能导致 SD 卡损坏。
-
USB 电源可能在 2012 年 10 月之前可用的早期 Model B 修订 1 版板上成为一个更大的问题。这些板子上增加了额外的Polyfuses在 USB 输出端,如果超过 140 mA 的电流被抽取,它们会跳闸。Polyfuses 可能需要几个小时或几天才能完全恢复,因此即使在电源得到改善的情况下,也可能保持不可预测的行为。
您可以通过缺少后来型号中存在的四个安装孔来识别修订版 1 的电路板。
-
- Debian Linux(Raspbian 基于此)支持许多常见的 USB 设备,例如闪存驱动器、硬盘驱动器(可能需要外部电源),相机、打印机、蓝牙和 Wi-Fi 适配器。一些设备将自动检测,而其他设备则需要安装驱动程序。
-
微型 USB 电源:树莓派需要一个能够舒适地提供至少 1,000 mA(推荐 1,500 mA 或更多,尤其是对于更耗电的 2 型和 3 型)的 5V 电源,通过微型 USB 连接。可以使用便携式电池组为设备供电,例如适合为平板电脑供电或充电的电池组。再次强调,确保它们能够提供至少 1,000 mA 或更多的 5V 电源。
在连接电源之前,你应该尽量完成所有其他与树莓派的连接。然而,在运行过程中,USB 设备、音频和网络可以连接和移除,而不会出现问题。
还有更多...
除了您在电脑上预期看到的常规主要连接外,树莓派还有许多其他连接。
二级硬件连接
以下每个连接都为 Raspberry Pi 提供了额外的接口:
-
20 x 2 GPIO 引脚头(Model A+、B+、2 B、3 B 和 Pi Zero):这是 Raspberry Pi 的主要 40 引脚 GPIO 引脚头,用于直接与硬件组件进行接口连接。我们在第六章,在图像中检测边缘和轮廓,第七章,创建 3D 图形,第九章,使用 Python 驱动硬件,以及第十章,感知和显示现实世界数据中使用这个连接。本书中的食谱也适用于具有 13 x 2 GPIO 引脚头的较老型号的 Raspberry Pi。
-
P5 8 x 2 GPIO 引脚排针(仅限型号 1 B 版本 2.0):我们在书中没有使用这个。
-
重置连接:这在较晚的型号上存在(没有安装引脚)。当引脚 1(重置)和引脚 2(地)连接在一起时,会触发重置。我们在第九章的使用 Python 驱动硬件食谱中使用了这个功能,可控关机按钮。
-
GPU/LAN JTAG: 联合测试行动小组(JTAG)是一种编程和调试接口,用于配置和测试处理器。这些接口在新型号上以表面焊盘的形式存在。使用此接口需要专用 JTAG 设备。本书中我们不使用此接口。
-
直接相机 CSI 接口:此连接支持树莓派相机模块。请注意,Pi Zero 的 CSI 接口比其他型号更小,因此需要不同的排线连接器。
-
直接显示 DSI:此连接支持直接连接的显示设备,例如 7 英寸 800 x 600 电容式触摸屏。
使用 NOOBS 设置您的树莓派 SD 卡
Raspberry Pi 需要在启动前将操作系统加载到 SD 卡上。设置 SD 卡最简单的方法是使用 NOOBS;你可能发现你可以购买已经预装了 NOOBS 的 SD 卡。
NOOBS 提供了一个初始启动菜单,该菜单提供了将多个可用的操作系统安装到您的 SD 卡上的选项。
准备就绪
由于 NOOBS 创建了一个恢复分区以保留原始安装镜像,建议使用 8 GB SD 卡或更大容量。您还需要一个 SD 卡读卡器(经验表明,某些内置读卡器可能会引起问题,因此建议使用外部 USB 类型读卡器)。
如果你正在使用之前使用过的 SD 卡,你可能需要重新格式化它以删除任何之前的分区和数据。NOOBS 期望 SD 卡只包含一个 FAT32 分区。
如果使用 Windows 或 macOS X,您可以使用 SD 协会提供的格式化工具,如图下截图所示(可在www.sdcard.org/downloads/formatter_4/
获取):
使用 SD 格式化工具清除 SD 卡上的任何分区
从选项设置对话框中,设置格式大小调整。这将移除之前创建的所有 SD 卡分区。
如果使用 Linux 系统,您可以使用 gparted
工具清除任何之前的分区并将其重新格式化为 FAT32 分区。
完整的 NOOBS 软件包(通常超过 1 GB)包含 Raspbian,这是内置的最受欢迎的树莓派操作系统镜像。同时,也提供了一种轻量级的 NOOBS 版本,该版本没有预装操作系统(尽管需要下载一个较小的初始文件 20 MB,并在树莓派上建立网络连接,以便直接下载您打算使用的操作系统)。
NOOBS 可在 www.raspberrypi.org/downloads
获取,相关文档可在 github.com/raspberrypi/noobs
查阅。
如何做到这一点...
通过执行以下步骤,我们将准备 SD 卡以运行 NOOBS。这样我们就可以选择并安装我们想要使用的操作系统:
-
准备好您的 SD 卡。
-
在格式化后或新的 SD 卡上,复制
NOOBS_vX.zip
文件的内容。当复制完成后,你应该得到类似于以下 SD 卡截图的内容:
NOOBS 文件已提取到 SD 卡上
文件可能因 NOOBS 的不同版本而略有差异,您电脑上显示的图标也可能不同。
- 您现在可以将这张卡插入到您的 Raspberry Pi 中,连接键盘和显示器,然后打开电源。有关所需物品和如何操作的详细信息,请参阅连接到 Raspberry Pi菜谱。
默认情况下,NOOBS 将通过 HDMI 连接显示。如果您有另一种类型的屏幕(或者您什么都没有看到),您需要通过按 1、2、3 或 4 来手动选择输出类型,具体功能如下:
-
关键词 1 代表标准 HDMI 模式(默认模式)
-
关键 2 代表安全 HDMI 模式(如果未检测到输出,则为替代 HDMI 设置)
-
关键 3 代表复合 PAL(用于通过 RCA 模拟视频连接进行的连接)
-
第四个键代表复合 NTSC(再次强调,通过 RCA 连接器进行连接)
此显示设置也将应用于已安装的操作系统。
稍等片刻,你将看到 NOOBS 选择屏幕,该屏幕列出了可用的发行版(离线版本仅包括 Raspbian)。还有更多可用的发行版,但只有选中的那些可以通过 NOOBS 系统直接访问。点击 Raspbian,因为这个书中使用的是这个操作系统。
按下 Enter 键或点击安装操作系统,并确认您希望覆盖所有数据
这张卡。这将覆盖之前使用 NOOBS 安装的任何分布,但不会删除 NOOBS 系统;您可以在开机时按Shift键随时返回到它。
根据其速度,将数据写入卡片大约需要 20 到 40 分钟。当操作完成并且出现“图像应用成功”的消息时,点击“确定”,树莓派将开始启动进入树莓派桌面
。
它是如何工作的...
以这种方式将图像文件写入 SD 卡的目的,是为了确保 SD 卡格式化成预期的文件系统分区和文件,以便正确引导操作系统。
当树莓派启动时,它会加载存储在 GPU 内部内存中的一些特殊代码(通常由树莓派基金会称为二进制 blob)。这个二进制 blob 提供了读取 SD 卡上BOOT
分区的指令,这在 NOOBS 安装的情况下,将从RECOVERY
分区加载 NOOBS。如果此时按下Shift键,NOOBS 将加载恢复和安装菜单。否则,NOOBS 将开始加载存储在SETTINGS
分区中的偏好设置所指定的操作系统。
当加载操作系统时,它将通过BOOT
分区启动,使用config.txt
中定义的设置和cmdline.txt
中的选项,最终加载到root
分区上的桌面。请参考以下图示:
NOOBS 在 SD 卡上创建多个分区,以便安装多个
操作系统并提供恢复功能
NOOBS 允许用户选择性地在同一张卡上安装多个操作系统,并提供一个启动菜单来在这些操作系统之间进行选择(在超时期间可以选择设置一个默认值)。
如果您以后添加、删除或重新安装操作系统,请首先确保您复制了所有文件,包括您希望保留的系统设置,因为 NOOBS 可能会覆盖 SD 卡上的所有内容。
还有更多...
当您第一次直接启动 Raspberry Pi 时,桌面将会加载。您可以使用 Raspberry Pi 配置菜单(位于桌面上的“首选项”菜单下或通过 sudo raspi-config
命令)来配置系统设置。使用此菜单,您可以更改您的 SD 卡或设置您的常规偏好设置:
更改默认用户密码
确保您在登录后更改pi
用户账户的默认密码,因为默认密码是众所周知的。这一点在连接到公共网络时尤为重要。您可以使用passwd
命令来完成此操作,如下面的截图所示:
为 Pi 用户设置新密码
这提供了更大的信心,因为如果你后来连接到另一个网络,只有你才能访问你的文件并控制你的树莓派。
确保您安全关闭
为了避免任何数据损坏,您必须确保通过发出shutdown
命令正确关闭 Raspberry Pi,如下所示:
sudo shutdown -h now
或者,使用这个:
sudo halt
您必须在从 Raspberry Pi 上移除电源之前等待此命令完成(在 SD 卡访问指示灯停止闪烁后至少等待 10 秒)。
您也可以使用reboot
命令重新启动系统,如下所示:
sudo reboot
手动准备 SD 卡
使用 NOOBS 的另一种选择是手动将操作系统镜像写入 SD 卡。虽然这最初是安装操作系统的唯一方式,但一些用户仍然更喜欢这种方式。它允许在将 SD 卡用于 Raspberry Pi 之前对其进行准备。它还可以提供更方便的访问启动和配置文件的方法,并且为用户留下更多可用空间(与 NOOBS 不同,不包括RECOVERY
分区)。
默认的 Raspbian 系统镜像实际上由两个分区组成,BOOT
和 SYSTEM
,它们可以适应一个 2 GB 的 SD 卡(建议使用 4 GB 或更大)。
您需要一个运行 Windows/Mac OS X/Linux 的计算机(尽管使用另一台 Raspberry Pi 来编写您的卡片也是可能的;请做好长时间等待的准备)。
下载您希望使用的操作系统最新版本。为了本书的目的,假设您正在使用可在www.raspberrypi.org/downloads
找到的最新版本的 Raspbian。
根据您计划用于写入 SD 卡(您需要的.img
文件有时是压缩的,因此在开始之前,您需要提取该文件)的计算机类型,执行以下步骤。
以下步骤适用于 Windows:
-
确保您已下载如前所述的 Raspbian 镜像,并将其提取到方便的文件夹中,以获得一个
.img
文件。 -
获取位于
www.sourceforge.net/projects/win32diskimager
的Win32DiskImager.exe
文件。 -
从您下载的位置运行
Win32DiskImager.exe
。 -
点击文件夹图标,导航到
.img
文件的位置,然后点击保存。 -
如果您还没有这样做,请将您的 SD 卡插入卡读卡器,并将其连接到您的电脑。
-
从小下拉框中选择与您的 SD 卡对应的设备驱动器字母。请务必确认这是正确的设备(因为当您写入镜像时,程序将覆盖设备上的所有内容)。
驱动器字母可能直到您选择源映像文件时才会列出。
- 最后,点击“写入”按钮,等待程序将图像写入 SD 卡,如下所示截图:
使用 Disk Imager 手动将操作系统镜像写入 SD 卡
- 一旦完成,您就可以退出程序。您的 SD 卡已准备就绪。
以下步骤适用于大多数常见的 Linux 发行版,例如 Ubuntu 和 Debian:
-
使用您偏好的网络浏览器,下载 Raspbian 图像并将其保存在合适的位置。
-
从文件管理器中提取文件或在终端中定位文件夹,然后使用以下命令解压
.img
文件:
unzip filename.zip
-
如果您还没有这样做,请将您的 SD 卡插入卡读卡器,并将其连接到您的电脑。
-
使用
df -h
命令并识别 SD 卡的 sdX 标识符。每个分区将显示为 sdX1、sdX2 等等,其中 X 将是设备 ID 的a
、b
、c
、d
等等。 -
确保使用以下命令卸载 SD 卡上的所有分区
umount /dev/sdXn
命令用于每个分区,其中sdXn
是正在卸载的分区。 -
使用以下命令将图片文件写入 SD 卡:
sudo dd if=filename.img of=/dev/sdX bs=4M
-
将数据写入 SD 卡的过程需要一些时间,完成后会返回到终端提示符。
-
在从电脑中移除 SD 卡之前,请使用以下命令卸载它:
umount /dev/sdX1
以下步骤适用于大多数版本的 OS X:
-
使用您偏好的网络浏览器,下载 Raspbian 镜像并将其保存在合适的位置。
-
从文件管理器中提取文件或在终端中定位文件夹,然后使用以下命令解压
.img
文件:
unzip filename.zip
-
如果您还没有这样做,请将您的 SD 卡插入卡读卡器,并将其连接到您的电脑。
-
使用
diskutil list
命令并识别 SD 卡的 disk# 标识符。每个分区将显示为 disk#s1, disk#s2,等等,其中 # 将是1
、2
、3
、4
等等,代表设备 ID。
如果列出 rdisk#,则使用此方法进行更快的写入(这使用原始路径并跳过数据缓冲)。
-
确保使用
unmountdisk /dev/diskX
命令卸载 SD 卡,其中diskX
是要卸载的设备。 -
使用以下命令将图片文件写入 SD 卡:
sudo dd if=filename.img of=/dev/diskX bs=1M
-
将数据写入 SD 卡的过程需要一些时间,完成后会返回到终端提示符。
-
在从电脑中移除 SD 卡之前,请先卸载 SD 卡,使用
以下命令:
unmountdisk /dev/diskX
参考以下图示:
手动安装的操作系统镜像的引导过程
将系统扩展以适应您的 SD 卡
手写生成的镜像将具有固定的大小(通常制作成适合可能的最小尺寸 SD 卡)。为了充分利用 SD 卡,您需要将系统分区扩展以填充 SD 卡的剩余部分。这可以通过使用 Raspberry Pi 配置工具来实现。
选择“展开文件系统”,如图下所示:
树莓派配置工具
访问 RECOVERY/BOOT 分区
Windows 和 macOS X 不支持 ext4
格式,因此当你读取 SD 卡时,只有 文件分配表 (FAT) 分区可访问。此外,Windows 只支持 SD 卡上的第一个分区,所以如果你已安装 NOOBS,则只能看到 RECOVERY
分区。如果你是手动写入的卡,你将能够访问 BOOT
分区。
data
分区(如果你是通过 NOOBS 安装的)和root
分区都是ext4
格式,通常在非 Linux 系统上不可见。
如果你确实需要在 Windows 系统下读取 SD 卡中的文件,一款免费软件 Linux Reader(可在 www.diskinternals.com/linux-reader 获取)可以提供对 SD 卡上所有分区的只读访问权限。
从树莓派访问分区。要查看当前挂载的分区,请使用 df
命令,如下截图所示:
df 命令的结果
要从 Raspbian 内部访问 BOOT
分区,请使用以下命令:
cd /boot/
要访问RECOVERY
或data
分区,我们必须通过执行以下操作来挂载它:
以下步骤:
- 确定分区名称,因为系统通过列出所有分区(包括未挂载的分区)来引用分区名称。
sudo fdisk -l
命令列出分区,如下面的截图所示:
NOOBS 安装和数据分区
以下表格显示了分区名称及其含义
分区名称 | 含义 |
---|---|
mmcblk0p1 |
(VFAT ) RECOVERY |
mmcblk0p2 |
(扩展分区) 包含 (root , data , BOOT ) |
mmcblk0p5 |
(ext4 ) root |
mmcblk0p6 |
(VFAT ) BOOT |
mmcblk0p7 |
(ext4 ) 设置 |
如果你在同一张卡上安装了额外的操作系统,前面表格中显示的分区标识符将会有所不同。
- 创建一个文件夹并将其设置为分区的挂载点;对于
RECOVERY
分区,请使用以下命令:
mkdir ~/recovery
sudo mount -t vfat /dev/mmcblk0p1 ~/recovery
确保每次启动系统时都能安装,请执行以下步骤:
- 在
exit 0
之前将sudo
挂载命令添加到/etc/rc.local
文件中。如果你有不同的用户名,你需要将pi
更改为匹配:
sudo nano /etc/rc.local
sudo mount -t vfat /dev/mmcblk0p1 /home/pi/recovery
- 通过按 Ctrl + X,Y 和 Enter 来保存并退出。
添加到 /etc/rc.local
的命令将为任何登录到树莓派的用户执行。如果你只想为当前用户挂载驱动器,可以将命令添加到 .bash_profile
中代替。
如果您必须在同一张卡上安装额外的操作系统,这里显示的分区标识符将不同。
使用工具备份您的 SD 卡以防万一出现故障
您可以使用Win32 Disk Imager通过将 SD 卡插入到您的读卡器中,启动程序,并创建一个用于存储图像的文件名来制作 SD 卡的全备份镜像。只需点击“读取”按钮即可从 SD 卡读取图像并将其写入新的图像文件。
要备份您的系统,或者使用树莓派克隆到另一张 SD 卡,请使用 SD 卡复制器(可通过桌面菜单中的附件 | SD 卡复制器访问)。
将 SD 卡插入到 Raspberry Pi 的空闲 USB 端口上的卡读卡器中,并选择新的存储设备,如图下截图所示:
SD 卡复制程序
在继续之前,SD 卡复制器将确认您是否希望格式化和覆盖目标设备,并在有足够空间的情况下,制作您系统的克隆。
dd
命令同样可以用来备份卡片,如下所示:
- 对于 Linux 系统,将
sdX
替换为您的设备 ID,请使用以下命令:
sudo dd if=/dev/sdX of=image.img.gz bs=1M
- 对于 OS X,将
diskX
替换为您的设备 ID,请使用以下命令:
sudo dd if=/dev/diskX of=image.img.gz bs=1M
- 您还可以使用
gzip
和 split 命令来压缩卡片的内容,并在需要时将它们分割成多个文件,以便于归档,具体操作如下:
sudo dd if=/dev/sdX bs=1M | gzip -c | split -d -b 2000m - image.img.gz
- 要恢复分割的图像,请使用以下命令:
sudo cat image.img.gz* | gzip -dc | dd of=/dev/sdX bs=1M
通过以太网端口使用 CAT6 以太网线将你的树莓派连接到互联网进行网络连接
将 Raspberry Pi 连接到互联网最简单的方式是通过 Model B 上的内置 LAN 连接。如果你使用的是 Model A Raspberry Pi,可以使用 USB-to-LAN 转换器(有关如何配置此转换器的详细信息,请参阅 Networking and connecting your Raspberry Pi to the internet via a USB Wi-Fi dongle 菜单中的 There's more... 部分)。
准备就绪
您需要访问一个合适的有线网络,该网络将连接到互联网,并配备一根标准网络线(带有RJ45类型连接器,用于连接到树莓派)。
如何做到这一点...
许多网络使用动态主机配置协议(DHCP)自动连接和配置,该协议由路由器或交换机控制。如果情况如此,只需将网络线缆插入路由器或网络交换机上的一个备用网络端口(如果适用,也可以是墙壁网络插座)。
如果没有可用的 DHCP 服务器,您将不得不手动配置设置(有关详细信息,请参阅更多内容...部分)。
您可以通过以下步骤确认此功能是否成功运行:
-
确保树莓派两侧的两个 LED 灯亮起(左侧的橙色 LED 表示连接,右侧的绿色 LED 通过闪烁显示活动)。这表明设备与路由器有物理连接,并且设备已通电且正在运行。
-
使用
ping
命令测试您本地网络的链接。首先,找出网络上另一台计算机的 IP 地址(或者可能是路由器的地址,例如,通常是192.168.0.1
或192.168.1.254
)。现在,在树莓派终端上,使用ping
命令(使用-c 4
参数只发送四条消息;否则,按Ctrl + C停止)来 ping IP 地址,如下所示:
sudo ping 192.168.1.254 -c 4
- 按以下方式测试连接到互联网的链接(如果你通常通过代理服务器连接到互联网,则此操作将失败):
sudo ping www.raspberrypi.org -c 4
-
最后,你可以通过发现以下链接回测试 Raspberry Pi。
在树莓派上使用
hostname -I
查看 IP 地址。然后您可以在网络上的另一台计算机上使用 ping 命令来确保其可访问(用树莓派的 IP 地址代替www.raspberrypi.org)。Windows 版本的 ping 命令将执行五次 ping 并自动停止,不需要-c 4
选项。
如果上述测试失败,您需要检查您的连接,然后确认您网络的正确配置。
还有更多...
如果你经常在网络上使用你的树莓派,你不会希望每次想要连接到它时都要查找 IP 地址。
在某些网络上,您可能可以使用树莓派的计算机名而不是其 IP 地址(默认为raspberrypi
)。为了辅助这一操作,您可能需要一些额外的软件,例如Bonjour,以确保网络上的计算机名正确注册。如果您使用的是 macOS X,那么 Bonjour 已经运行在您的系统上了。
在 Windows 上,您可以选择安装 iTunes(如果您还没有的话),它还包括该服务,或者您也可以单独安装(通过从 support.apple.com/kb/DL999
可用的 Apple Bonjour 安装程序)。然后您可以使用主机名,raspberrypi
或 raspberrypi.local
,通过网络连接到 Raspberry Pi。如果您需要更改主机名,那么您可以使用之前显示的 Raspberry Pi 配置工具进行更改。
或者,您可能发现通过手动设置 IP 地址将 IP 地址固定为一个已知值是有帮助的。然而,请记住在连接到另一个网络时将其切换回使用 DHCP。
一些路由器还提供设置静态 IP DHCP 地址的选项,因此相同的地址总是分配给树莓派(具体如何设置将取决于路由器本身)。
了解你的树莓派 IP 地址或使用主机名特别有用,如果你打算使用后面描述的远程访问解决方案之一,这样可以避免需要显示器。
使用树莓派内置的 Wi-Fi 和蓝牙
许多家庭网络通过 Wi-Fi 提供无线网络;如果你有 Raspberry Pi 3,那么你可以利用板载的 Broadcom Wi-Fi 来连接它。Raspberry Pi 3 还支持蓝牙,因此你可以连接大多数标准蓝牙设备,并像在其他任何电脑上一样使用它们。
此方法也适用于任何支持的 USB Wi-Fi 和蓝牙设备;请参阅通过 USB Wi-Fi 适配器连接 Raspberry Pi 到互联网的网络和连接食谱,以获取识别设备和安装固件(如有需要)的额外帮助。
准备就绪
Raspbian 的最新版本包括通过图形界面快速轻松配置 Wi-Fi 和蓝牙的有用工具。
注意:如果您需要通过命令行配置 Wi-Fi,请参阅通过 USB Wi-Fi 外置网卡将 Raspberry Pi 连接到互联网的网络和连接方法以获取详细信息.
Wi-Fi 和 Bluetooth 配置应用程序
您可以使用内置的蓝牙连接无线键盘、鼠标,甚至无线扬声器。这在需要额外电缆和电线的情况下特别有用,例如在机器人项目中,或者当树莓派安装在难以触及的位置时(作为服务器或安全摄像头)。
如何做到...
这里提供了各种方法。
连接到您的 Wi-Fi 网络
要配置您的 Wi-Fi 连接,请点击网络图标以列出可用的本地 Wi-Fi 网络:
该区域可用的接入点 Wi-Fi 列表
选择所需的网络(例如,Demo
),如果需要,请输入您的密码(也称为预共享密钥
):
提供接入点的密码
稍微过一会儿,你应该能看到你已经连接到了网络,并且图标
将变为 Wi-Fi 符号。如果您遇到问题,请确保您有正确的密码/密钥:
成功连接到接入点
就这样;就这么简单!
您现在可以通过使用网络浏览器访问网站或通过在终端中使用以下命令来测试您的连接并确保其正常工作:
sudo ping www.raspberrypi.com
连接到蓝牙设备
首先,我们需要通过点击蓝牙图标并选择设置为可发现来将蓝牙设备置于可发现模式。您还需要确保您想要连接的设备设置为可发现并准备好配对;这可能会因设备而异(例如,按下配对按钮):
将蓝牙设置为可发现
接下来,选择添加设备...并选择目标设备,然后配对:
选择并配对所需的设备
然后将开始配对过程;例如,BTKB-71DB 键盘需要输入配对码467572
到键盘上以完成配对。其他设备可能使用默认配对码,通常设置为 0000、1111、1234 或类似:
按照说明将设备与所需的配对码进行配对
一旦过程完成,设备将被列出,并且每次设备出现并启动时都会自动连接。
手动配置您的网络
如果您的网络中没有包含 DHCP 服务器或者它已被禁用(通常,这些功能集成在大多数现代 ADSL/电缆调制解调器或路由器中),您可能需要手动配置您的网络设置。
准备就绪
在开始之前,您需要确定您网络的网络设置。
您需要从路由器的设置或连接到网络的另一台计算机中找到以下信息:
-
IPv4 地址:此地址需要选择与网络中其他计算机相似(通常,前三个数字应匹配,即如果
子网掩码
为255.255.255.0
,则为192.168.1.X
),但不应已被其他计算机使用。然而,请避免使用x.x.x.255
作为最后一个地址,因为这是保留为广播地址的。 -
子网掩码:这个数字决定了计算机将响应的地址范围(对于家庭网络,通常是
255.255.255.0
,这允许最多 254 个地址)。这有时也被称为网络掩码。 -
默认网关地址:这个地址通常是您的路由器的 IP 地址,通过这个地址,计算机连接到互联网。
-
DNS 服务器:域名系统(DNS)服务器通过查找将名称转换为 IP 地址。通常,它们已经配置在您的路由器上,在这种情况下,您可以使用路由器的地址。或者,您的互联网服务提供商(ISP)可能提供一些地址,或者您可以使用位于地址
8.8.8.8
和8.8.4.4
的谷歌公共 DNS 服务器。在某些系统中,这些也被称为名称服务器。
对于 Windows 系统,您可以通过连接互联网并运行以下命令来获取此信息:
ipconfig /all
定位活动连接(如果你使用有线连接,通常称为本地连接 1
或类似名称;如果你使用 Wi-Fi,则称为无线网络连接)并查找所需信息,如下所示:
ipconfig/all 命令显示了有关您的网络设置的详细信息
对于 Linux 和 macOS X,您可以使用以下命令获取所需信息(注意,这里使用的是 ifconfig
而不是 ipconfig
):
ifconfig
DNS 服务器被称为 nameservers,通常列在resolv.conf
文件中。您可以使用以下less
命令来查看其内容(在完成查看后按 Q 键退出):
less /etc/resolv.conf
如何做到这一点...
要设置网络接口设置,请使用编辑器编辑 /etc/network/interfaces
文件。
以下代码:
sudo nano /etc/network/interfaces
现在执行以下步骤:
- 我们可以为我们的特定网络添加详细信息,包括我们想要分配给它的 IP
地址
号、网络的子网掩码
地址以及网关
地址,如下所示:
iface eth0 inet static
address 192.168.1.10
netmask 255.255.255.0
gateway 192.168.1.254
-
通过按 Ctrl + X,Y 和 Enter 来保存并退出。
-
要设置 DNS 的名称服务器,请使用以下代码编辑
/etc/resolv.conf
:
sudo nano /etc/resolv.conf
- 按照以下步骤添加您的 DNS 服务器地址:
nameserver 8.8.8.8
nameserver 8.8.4.4
- 通过按 Ctrl + X,Y 和 Enter 来保存并退出。
还有更多...
您可以通过编辑BOOT
分区中的cmdline.txt
来配置网络设置,并使用ip
将设置添加到启动命令行中。
ip
选项采用以下形式:
ip=client-ip:nfsserver-ip:gw-ip:netmask:hostname:device:autoconf
-
client-ip
选项是你想要分配给树莓派的 IP 地址 -
gw-ip
选项将在需要手动设置时设置网关服务器地址 -
netmask
选项将直接设置网络的netmask
-
hostname
选项将允许您更改默认的raspberrypi
主机名 -
device
选项允许您在存在多个网络设备的情况下指定默认网络设备 -
autoconf
选项允许自动配置功能开启或关闭
直接连接到笔记本电脑或计算机
可以使用一根网络线直接将 Raspberry Pi 的 LAN 端口连接到笔记本电脑或计算机。这将在这两台计算机之间创建一个本地网络连接,允许你完成所有在连接到普通网络时可以做的事情,无需使用集线器或路由器,包括连接到互联网,如果使用互联网连接共享(ICS),则操作如下:
利用树莓派,只需一根网线、一张标准镜像 SD 卡和电源即可。
ICS 允许 Raspberry Pi 通过另一台计算机连接到互联网。然而,为了计算机能够通过该链路进行通信,需要进行一些额外的配置,因为 Raspberry Pi 不会自动分配自己的 IP 地址。
我们将使用 ICS 来共享来自另一个网络连接的连接,例如笔记本电脑上的内置 Wi-Fi。或者,如果不需要互联网或计算机只有一个网络适配器,我们可以使用直接网络连接(参考“更多...”部分下的“直接网络连接”章节)。
尽管这个设置应该适用于大多数电脑,但有些设置比其他设置更困难。有关更多信息,请参阅www.pihardware.com/guides/direct-network-connection。
准备就绪
您需要配备电源和标准网络线的树莓派。
树莓派 B 型 LAN 芯片包括Auto-MDIX(自动中继器依赖接口交叉)。无需使用专用交叉线缆(一种特殊布线的网络线缆,使得发送线连接到接收线以实现直接网络连接),芯片将自动决定并更改所需的设置。
拥有一个键盘和显示器进行额外的测试也可能很有帮助,尤其是如果你这是第一次尝试这样做。
为了确保您能够将网络设置恢复到原始值,您应该检查它是否具有固定 IP 地址或网络是否已自动配置。
要检查 Windows 10 的网络设置,请执行以下步骤:
- 从开始菜单打开设置,然后选择网络和互联网,接着选择以太网,并在相关设置列表中点击更改适配器选项。
要检查 Windows 7 和 Vista 上的网络设置,请执行以下步骤:
-
从控制面板打开网络和共享中心,然后点击左侧的更改适配器设置。
-
要检查 Windows XP 上的网络设置,请从控制面板打开网络连接。
-
找到与您的有线网络适配器相关的项目(默认情况下,这通常被称为以太网或本地连接,如下面的截图所示):
定位您的有线网络连接
- 右键单击其图标并点击属性。将出现一个对话框,如图所示:
选择 TCP/IP 属性并检查设置
-
如果存在两个版本(另一个是版本 6),请选择名为“互联网协议(TCP/IP)”或“互联网协议版本 4(TCP/IPv4)”的选项,然后点击“属性”按钮。
-
您可以通过使用自动设置或指定 IP 地址(如果是这样,请注意此地址和剩余的详细信息,因为您可能希望在以后某个时间点恢复设置)来确认您的网络设置。
要检查 Linux 上的网络设置,请执行以下步骤:
- 打开网络设置对话框并选择配置接口。请参考以下截图:
Linux 网络设置对话框
- 如果有任何设置是手动设置的,请确保你记下它们,以便你以后想要时可以恢复。
要检查 macOS X 上的网络设置,请执行以下步骤:
-
打开系统偏好设置并点击网络。然后你可以确认 IP 地址是否是自动分配的(使用 DHCP)。
-
确保如果手动设置了任何设置,您要记下它们,以便您以后想要时可以恢复。请参考以下截图:
OS X 网络设置对话框
如果你只需要在没有互联网连接的情况下访问或控制树莓派,请参考“更多内容...”部分中的“直接网络链接”部分。
如何做到这一点...
首先,我们需要在我们的网络设备上启用 ICS。在这种情况下,我们将通过以太网连接到树莓派,通过无线网络连接共享互联网。
对于 Windows 系统,请按照以下步骤操作:
-
返回到网络适配器列表,右键点击连接到
连接到互联网(在这种情况下,是 WiFi 或无线网络连接设备),然后点击属性:
定位您的有线网络连接
- 在窗口顶部,选择第二个标签页(在 Windows XP 中,它被称为高级;在 Windows 7 和 Windows 10 中,它被称为共享),如下截图所示:
选择 TCP/IP 属性并记录分配的 IP 地址
- 在“互联网连接共享”部分,勾选允许其他网络用户通过此计算机的互联网连接进行连接(如果存在,请使用下拉框选择家庭网络连接选项:以太网或局域网连接)。点击“确定”并确认您之前是否已为局域网连接设置了固定的 IP 地址。
对于 macOS X,要启用 ICS,请执行以下步骤:
-
点击系统偏好设置,然后点击共享。
-
点击“互联网共享”,然后选择我们想要共享互联网的连接(在这种情况下,将是 Wi-Fi AirPort)。然后选择我们将连接树莓派的连接(在这种情况下,是以太网)。
要在 Linux 上启用 ICS,请执行以下步骤:
- 从系统菜单中,点击“首选项”然后点击“网络连接”。选择您想要共享的连接(在本例中为无线连接)并点击“编辑”或“配置”。在 IPv4 设置选项卡中,将方法选项更改为“共享给其他计算机”。
网络适配器的 IP 地址将是 Raspberry Pi 上使用的网关 IP地址,并将分配在同一范围内的 IP 地址(除了最后一位数字,其他都会匹配)。例如,如果电脑的有线连接现在是192.168.137.1
,Raspberry Pi 的网关 IP 将是192.168.137.1
,其自身的 IP 地址可能设置为192.168.137.10
。
幸运的是,由于操作系统的更新,Raspbian 现在将自动分配一个合适的 IP 地址以加入网络,并适当地设置网关。然而,除非我们将屏幕连接到 Raspberry Pi 或在我们的网络上扫描设备,否则我们不知道 Raspberry PI 给自己分配了什么 IP 地址。
幸运的是(如There's more...部分中提到的通过 LAN 连接器将 Raspberry Pi 连接到互联网的联网和连接菜谱中所述),苹果的Bonjour软件将自动确保网络上的主机名正确注册。如前所述,如果您有 Mac OS X,您已经运行了 Bonjour。在 Windows 上,您可以选择安装 iTunes,或者您可以单独安装它(可从support.apple.com/kb/DL999
获取)。默认情况下,可以使用主机名raspberrypi。
我们现在准备测试新的连接,如下所示:
-
将网络线连接到树莓派和电脑的网络端口,然后开启树莓派,确保如果之前已经取出 SD 卡,请重新插入。若您在树莓派上编辑了文件,要重启树莓派,请使用
sudo reboot
命令来重启。 -
给予 Raspberry Pi 一分钟或两分钟的时间来完全启动。我们现在可以测试连接了。
-
从连接的笔记本电脑或计算机,通过使用 Raspberry Pi 的主机名进行 ping 测试以检查连接,如下所示命令(在 Linux 或 OS X 上,添加
-c 4
以限制为四条消息或按 Ctrl + C 退出):
ping raspberrypi
希望你能找到一个有效的连接并收到回复
树莓派
如果你将键盘和屏幕连接到树莓派,你就可以执行
以下步骤:
- 您可以从树莓派终端以如下方式 ping 回电脑(例如,
192.168.137.1
):
sudo ping 192.168.137.1 -c 4
- 您可以通过使用
ping
命令连接到知名网站来测试互联网链接,如下所示,前提是您不是通过代理服务器访问互联网:
sudo ping www.raspberrypi.org -c 4
如果一切顺利,你将通过电脑将完整的互联网连接到树莓派,让你能够浏览网页以及更新和安装新的软件。
如果连接失败,请执行以下步骤:
-
重复此过程,确保前三个数字与树莓派和网络适配器的 IP 地址相匹配。
-
您还可以使用以下命令检查当树莓派启动时,是否正在设置正确的 IP 地址:
hostname -I
- 检查您的防火墙设置以确保防火墙没有阻止内部网络连接。
它是如何工作的...
当我们在主计算机上启用 ICS 时,操作系统将自动为计算机分配一个新的 IP 地址。一旦连接并启动,树莓派将自动设置到一个兼容的 IP 地址,并使用主计算机的 IP 地址作为互联网网关。
通过使用 Apple Bonjour,我们可以使用raspberrypi
主机名从连接的计算机连接到 Raspberry Pi。
最后,我们检查计算机是否可以通过直接网络链路与树莓派通信,反过来,也能连接到互联网。
还有更多...
如果您不需要在树莓派上使用互联网,或者您的电脑只有一个网络适配器,您仍然可以通过直接网络链接将计算机连接在一起。请参考以下图示:
仅使用网线、标准镜像 SD 卡和电源即可连接和使用 Raspberry Pi
直接网络链接
要在两台计算机之间建立网络链接,它们需要使用相同的地址范围。允许的地址范围由子网掩码确定(例如,255.255.0.0
或 255.255.255.0
表示除了最后两个或仅最后一位以外的所有 IP 地址都应该相同;否则,它们将被过滤)。
要使用直接链接而不启用 ICS,请检查您所使用的适配器的 IP 设置。
将要连接到并确定它是否自动分配或固定到
特定 IP 地址。
大多数直接连接到另一台计算机的 PC 将分配一个在169.254.X.X
范围内的 IP 地址(子网掩码为255.255.0.0
)。然而,我们必须确保网络适配器设置为自动获取 IP 地址。
为了让树莓派能够通过直接链接进行通信,它需要拥有与同一地址范围内的 IP 地址,即169.254.X.X
。如前所述,树莓派会自动为自己分配一个合适的 IP 地址并连接到网络。
因此,假设我们已经有了之前提到的 Apple Bonjour,我们只需要知道分配给 Raspberry Pi 的主机名(raspberrypi
)。
参见
如果你的 Raspberry Pi 没有连接键盘或屏幕,你可以使用这个网络链接远程访问 Raspberry Pi,就像在普通网络上一样(只需使用你为连接设置的新的 IP 地址)。请参考 通过 VNC 网络远程连接到 Raspberry Pi 和 通过 SSH(以及 X11 转发)网络远程连接到 Raspberry Pi 的食谱。
在我的网站上提供了大量额外信息,pihw.wordpress.com/guides/direct-network-connection
,包括额外的故障排除技巧以及几种无需专用屏幕和键盘即可连接到您的树莓派的其他方法。
通过 USB Wi-Fi 网卡将你的树莓派连接到互联网进行网络连接
通过将USB Wi-Fi 拓展卡插入树莓派的 USB 端口,即使是没有内置 Wi-Fi 的型号也能连接并使用 Wi-Fi 网络。
准备就绪
您需要获取一个合适的 USB Wi-Fi 网卡,在某些情况下,您可能还需要一个带电的 USB 集线器(这取决于您所拥有的树莓派硬件版本以及电源的质量)。USB Wi-Fi 网卡的一般适用性将根据内部使用的芯片组和可用的 Linux 支持水平而有所不同。您可能会发现某些 USB Wi-Fi 网卡无需安装额外的驱动程序即可工作(在这种情况下,您可以跳转到配置无线网络)。
支持的 Wi-Fi 网卡列表可在 elinux.org/RPi_USB_Wi-Fi_Adapters
查找。
您需要确保您的 Wi-Fi 适配器也兼容您打算使用的网络;例如,它支持相同的信号类型 802.11bgn 以及加密方式 WEP、WPA 和 WPA2(尽管大多数网络都向后兼容)。
您还需要以下网络详细信息:
- 服务集标识符 (SSID): 这是您 Wi-Fi 网络的名称,如果您使用以下命令,它应该可见:
sudo iwlist scan | grep SSID
- 加密类型和密钥:此值将为 None、WEP、WPA 或 WPA2,密钥将是您连接手机或笔记本电脑到无线网络时通常输入的代码(有时,它打印在路由器上)。
您需要稳定的互联网连接(即有线以太网)来下载所需的驱动程序。否则,您可能能够找到所需的固件文件(它们将是.deb
文件)并将它们复制到树莓派上(即通过 USB 闪存驱动器;如果您在桌面模式下运行,驱动器应会自动挂载)。将文件复制到合适的位置,并使用以下命令进行安装:
sudo apt-get install firmware_file.deb
如何做到这一点...
这个任务分为两个阶段:首先,我们需要识别并安装 Wi-Fi 适配器的固件,然后我们需要为无线网络进行配置。
我们将尝试识别您的 Wi-Fi 适配器芯片组(处理连接的部分);这可能与设备的实际制造商不匹配。
可以使用以下命令找到支持的固件的大致列表:
sudo apt-cache search wireless firmware
这将产生类似于以下输出的结果(忽略任何包标题中不包含firmware
的结果):
atmel-firmware - Firmware for Atmel at76c50x wireless networking chips.
firmware-atheros - Binary firmware for Atheros wireless cards
firmware-brcm80211 - Binary firmware for Broadcom 802.11 wireless cards
firmware-ipw2x00 - Binary firmware for Intel Pro Wireless 2100, 2200 and 2915
firmware-iwlwifi - Binary firmware for Intel PRO/Wireless 3945 and 802.11n cards
firmware-libertas - Binary firmware for Marvell Libertas 8xxx wireless cards
firmware-ralink - Binary firmware for Ralink wireless cards
firmware-realtek - Binary firmware for Realtek wired and wireless network adapters
libertas-firmware - Firmware for Marvell's libertas wireless chip series (dummy package)
zd1211-firmware - Firmware images for the zd1211rw wireless driver
要找出无线适配器的芯片组,请将 Wi-Fi 适配器插入树莓派,然后在终端中运行以下命令:
dmesg | grep 'Product:|Manufacturer:'
此命令将两个命令合并为一个。首先,dmesg
显示内核的消息缓冲区(这是自开机以来发生的系统事件的内部记录,例如检测到的 USB 设备)。您可以单独尝试此命令以观察完整的输出。
|
(管道符)将输出发送到grep
命令;grep 'Product:|Manufacturer'
进行检查,并且只返回包含Product
或Manufacturer
的行(因此我们应该得到任何列出的作为Product
和Manufacturer
的项目的摘要)。如果你没有找到任何内容或者想要查看所有 USB 设备,尝试使用grep 'usb'
命令代替。
这应该返回类似于以下输出结果——在这种情况下,我有一个ZyXEL
设备,它有一个ZyDAS
芯片组(快速谷歌搜索显示zd1211-firmware
是用于ZyDAS
设备的):
[ 1.893367] usb usb1: Product: DWC OTG Controller
[ 1.900217] usb usb1: Manufacturer: Linux 3.6.11+ dwc_otg_hcd
[ 3.348259] usb 1-1.2: Product: ZyXEL G-202
[ 3.355062] usb 1-1.2: Manufacturer: ZyDAS
一旦你已识别出你的设备和正确的固件,你可以像安装任何其他通过 apt-get
可用的软件包(其中 zd1211-firmware
可以替换为你所需的固件)一样进行安装。这可以通过以下命令展示:
sudo apt-get install zd1211-firmware
移除并重新插入 USB Wi-Fi 网卡,以便检测到它并加载驱动程序。现在我们可以使用ifconfig
测试新适配器是否正确安装。输出如下:
wlan0 IEEE 802.11bg ESSID:off/any
Mode:Managed Access Point: Not-Associated Tx-Power=20 dBm
Retry long limit:7 RTS thr:off Fragment thr:off
Power Management:off
该命令将显示系统上存在的网络适配器。对于 Wi-Fi,这通常是wlan0
或wlan1
等,如果你安装了多个,则依次类推。如果没有,请仔细检查所选固件,或许可以尝试一个替代方案或查看网站上的故障排除技巧。
一旦我们为 Wi-Fi 适配器安装了固件,我们就需要将其配置为我们想要连接的网络。我们可以使用如图前一个菜谱中所示的用户界面,或者我们可以通过以下步骤手动在终端中配置它:
- 我们需要将无线适配器添加到网络接口列表中,该列表设置在
/etc/network/interfaces
文件中,具体如下:
sudo nano -c /etc/network/interfaces
如有需要,使用之前的 wlan#
值代替 wlan0
,然后添加以下命令:
allow-hotplug wlan0
iface wlan0 inet manual
wpa-conf /etc/wpa_supplicant/wpa_supplicant.conf
当更改完成后,通过按下 Ctrl + X,Y 和 Enter 来保存并退出。
- 我们现在将把我们的网络 Wi-Fi 设置存储在
wpa_supplicant.conf
文件中(如果你的网络不使用wpa
加密,请不要担心;这只是文件的默认名称):
sudo nano -c /etc/wpa_supplicant/wpa_supplicant.conf
应包括以下内容:
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
country=GB
网络设置可以按照以下方式写入此文件(即,如果 SSID 设置为theSSID
):
-
- 如果没有使用加密,请使用以下代码:
network={
ssid="theSSID"
key_mgmt=NONE
}
-
- 使用
WEP
加密(即,如果将WEP
密钥设置为theWEPkey
),请使用以下代码:
- 使用
network={
ssid="theSSID"
key_mgmt=NONE
wep_key0="theWEPkey"
}
-
- 对于
WPA
或WPA2
加密(即,如果将WPA
密钥设置为theWPAkey
),请使用以下代码:
- 对于
network={
ssid="theSSID"
key_mgmt=WPA-PSK
psk="theWPAkey"
}
- 您可以使用以下命令启用适配器(再次提醒,如果需要,请将
wlan0
替换为相应的接口名称):
sudo ifup wlan0
使用以下命令来列出无线网络连接:
iwconfig
您应该看到您的无线网络已连接,并且您的 SSID 列在以下内容中:
wlan0 IEEE 802.11bg ESSID:"theSSID"
Mode:Managed Frequency:2.442 GHz Access Point:
00:24:BB:FF:FF:FF
Bit Rate=48 Mb/s Tx-Power=20 dBm
Retry long limit:7 RTS thr:off Fragment thr:off
Power Management:off
Link Quality=32/100 Signal level=32/100
Rx invalid nwid:0 Rx invalid crypt:0 Rx invalid frag:0
Tx excessive retries:0 Invalid misc:15 Missed beacon:0
如果不是,请调整您的设置,并使用 sudo ifdown wlan0
来关闭网络接口,然后使用 sudo ifup wlan0
来重新开启它。这将确认您已成功连接到您的 Wi-Fi 网络。
- 最后,我们需要检查我们是否有互联网访问权限。在这里,我们假设网络已通过 DHCP 自动配置,并且没有使用代理服务器。如果没有,请参考通过代理服务器连接互联网的步骤。
断开有线网络线缆,如果尚未断开,请尝试 ping Raspberry Pi 网站,操作如下:
**sudo ping** www.raspberrypi.org
如果你想快速了解当前 Raspberry Pi 正在使用的 IP 地址,可以使用hostname -I
,或者要找出哪个适配器连接到了哪个 IP 地址,可以使用ifconfig
。
还有更多...
Raspberry Pi 的 Model A 版本没有内置网络端口,因此要获取网络连接,必须添加一个 USB 网络适配器(要么是一个 Wi-Fi 外置设备,如前文所述,要么是一个 LAN 到 USB 适配器,如下文所述)。
使用 USB 有线网络适配器
就像 USB Wi-Fi 一样,适配器的支持将取决于所使用的芯片组和可用的驱动程序。除非设备自带 Linux 驱动程序,否则你可能需要在网上搜索以获取合适的 Debian Linux 驱动程序。
如果你找到一个合适的 .deb
文件,你可以使用以下命令进行安装:
sudo apt-get install firmware_file.deb
此外,使用 ifconfig
进行检查,因为某些设备将自动支持,显示为 eth1
(或 Model A 上的 eth0
),并立即准备好使用。
通过代理服务器连接到互联网
一些网络,例如工作场所或学校内的网络,通常要求您通过代理服务器连接到互联网。
准备就绪
您将需要连接到的代理服务器的地址,包括用户名和密码(如果需要的话)。
您应确认树莓派已经连接到网络,并且可以访问代理服务器。
使用ping
命令来检查,如下所示:
ping proxy.address.com -c 4
如果这失败了(没有收到回复),在继续之前,你需要确保你的网络设置是正确的。
如何做到这一点...
- 使用
nano
创建一个新文件,方法如下(如果文件中已有内容,您可以将代码添加到末尾):
sudo nano -c ~/.bash_profile
- 要在使用代理服务器的同时通过程序如Midori进行基本的网页浏览,你可以使用以下脚本:
function proxyenable {
# Define proxy settings
PROXY_ADDR="proxy.address.com:port"
# Login name (leave blank if not required):
LOGIN_USER="login_name"
# Login Password (leave blank to prompt):
LOGIN_PWD=
#If login specified - check for password
if [[ -z $LOGIN_USER ]]; then
#No login for proxy
PROXY_FULL=$PROXY_ADDR
else
#Login needed for proxy Prompt for password -s option hides input
if [[ -z $LOGIN_PWD ]]; then
read -s -p "Provide proxy password (then Enter):" LOGIN_PWD
echo
fi
PROXY_FULL=$LOGIN_USER:$LOGIN_PWD@$PROXY_ADDR
fi
#Web Proxy Enable: http_proxy or HTTP_PROXY environment variables
export http_proxy="http://$PROXY_FULL/"
export HTTP_PROXY=$http_proxy
export https_proxy="https://$PROXY_FULL/"
export HTTPS_PROXY=$https_proxy
export ftp_proxy="ftp://$PROXY_FULL/"
export FTP_PROXY=$ftp_proxy
#Set proxy for apt-get
sudo cat <<EOF | sudo tee /etc/apt/apt.conf.d/80proxy > /dev/null
Acquire::http::proxy "http://$PROXY_FULL/";
Acquire::ftp::proxy "ftp://$PROXY_FULL/";
Acquire::https::proxy "https://$PROXY_FULL/";
EOF
#Remove info no longer needed from environment
unset LOGIN_USER LOGIN_PWD PROXY_ADDR PROXY_FULL
echo Proxy Enabled
}
function proxydisable {
#Disable proxy values, apt-get and git settings
unset http_proxy HTTP_PROXY https_proxy HTTPS_PROXY
unset ftp_proxy FTP_PROXY
sudo rm /etc/apt/apt.conf.d/80proxy
echo Proxy Disabled
}
- 完成后,通过按Ctrl + X,Y,和Enter键保存并退出。
脚本被添加到用户的个人 .bash_profile
文件中,该文件在特定用户登录时运行。这将确保每个用户的代理设置是分开的。如果您希望所有用户使用相同的设置,可以将代码添加到 /etc/rc.local
文件中代替(此文件必须在末尾有 exit 0
)。
它是如何工作的...
许多利用互联网的程序在连接之前会检查http_proxy
或HTTP_PROXY
环境变量。如果它们存在,它们将使用代理设置进行连接。一些程序也可能使用HTTPS
和FTP
协议,因此我们也可以在这里为它们设置代理设置。
如果代理服务器需要用户名,将会提示输入密码。通常不建议将密码存储在脚本中,除非你确信没有其他人能够访问你的设备(无论是通过物理方式还是通过互联网)。
最后部分允许使用 sudo
命令执行的所有程序使用代理
在以超级用户身份操作时(大多数程序都会尝试访问
首先使用普通权限的网络,即使以超级用户身份运行,因此它不是
总是需要的)。
还有更多...
我们还需要允许某些程序使用代理设置,这些程序在访问网络时使用超级用户权限(这取决于程序;大多数不需要这样做)。我们需要按照以下步骤将命令添加到存储在 /etc/sudoers.d/
的文件中:
- 使用以下命令来打开一个新的
sudoer
文件:
sudo visudo -f /etc/sudoers.d/proxy
- 在文件中输入以下文本(在同一行上):
Defaults env_keep += "http_proxy HTTP_PROXY https_proxy HTTPS_PROXY ftp_proxy FTP_PROXY"
-
完成后,通过按Ctrl + X,Y,和Enter键保存并退出;不要更改
proxy.tmp
文件名(这在visudo
中是正常的;完成后它会将其更改为 proxy)。 -
如果提示“现在怎么办?”,说明命令中存在错误。按X键退出而不保存并重新输入命令。
-
重启(使用
sudo reboot
)后,您可以使用以下命令分别启用和禁用代理:
proxyenable
proxydisable
在这里使用 visudo
非常重要,因为它确保了文件权限被正确创建,以便 sudoers
目录(仅由 root
用户读取)。
通过 VNC 在网络上远程连接到 Raspberry Pi
通常,通过网络远程连接和控制树莓派更为可取,例如,使用笔记本电脑或台式计算机作为屏幕和键盘,或者当树莓派连接到其他地方时,也许甚至连接到它需要靠近的某些硬件。
VNC 是您远程连接到树莓派的一种方式。它将创建一个新的桌面会话,该会话将通过远程进行控制和访问。这里的 VNC 会话与可能正在树莓派显示屏上激活的会话是分开的。
准备就绪
确保您的树莓派已开启电源并连接到互联网。我们将使用互联网连接来使用 apt-get
安装程序。这是一个允许我们从官方仓库直接查找和安装应用程序的程序。
如何做到这一点...
- 首先,我们需要在树莓派上使用以下命令安装 TightVNC 服务器。建议首先运行一个
update
命令,以获取您要安装的软件包的最新版本,如下所示:
sudo apt-get update
sudo apt-get install tightvncserver
- 接受提示进行安装并等待其完成。要开始一个会话,请使用以下命令:
vncserver :1
- 第一次运行此程序时,它会要求您输入一个密码(不超过八个字符),以便访问桌面(您将在从您的电脑连接时使用此密码)。
以下信息应确认已启动新的桌面会话:
New 'X' desktop is raspberrypi:1
如果你还没有知道树莓派的 IP 地址,请使用hostname -I
并记下它。
接下来,我们需要运行一个 VNC 客户端。VNC Viewer是一个合适的程序,可以在www.realvnc.com/
找到,并且应该在 Windows、Linux 和 OS X 上都能运行。
当你运行 VNC Viewer 时,系统会提示你输入服务器地址和加密类型。请使用你的 Raspberry Pi 的 IP 地址,并在其后加上:1
。也就是说,对于 IP 地址192.168.1.69
,请使用192.168.1.69:1
这个地址。
您可以将加密类型设置为关闭或自动。
根据您的网络设置,您可能可以使用主机名;默认值为 raspberrypi
,即 raspberrypi:1
。
你可能会收到关于之前未连接到电脑或没有加密的警告。如果你正在使用公共网络或者在进行互联网连接(为了阻止他人截取你的数据),你应该启用加密。
还有更多...
您可以在命令行中添加选项来指定分辨率以及显示的颜色深度。分辨率和颜色深度越高(可以调整到每像素 8 位到 32 位以提供低或高颜色细节),通过网络链路传输的数据就越多。如果您觉得刷新率有点慢,可以尝试按照以下方式减小这些数值:
vncserver :1 -geometry 1280x780 -depth 24
为了在开机时自动启动 VNC 服务器,你可以将 vncserver
命令添加到 .bash_profile
文件中(每次 Raspberry Pi 启动时都会执行此文件)。
按以下方式使用 nano
编辑器(-c
选项允许显示行号):
sudo nano -c ~/.bash_profile
将以下行添加到文件末尾:
vncserver :1
下次开机时,您应该能够通过 VNC 远程连接
另一台计算机。
通过 SSH(以及 X11 转发)远程连接到 Raspberry Pi 网络
安全外壳(SSH)通常是建立远程连接的首选方法,因为它只允许终端连接,并且通常需要更少的资源。
SSH 的一个额外功能是将X11数据传输到运行在您机器上的X Windows服务器。这使得您能够启动通常在树莓派桌面上运行的程序,并且它们将出现在本地计算机上的独立窗口中,如下所示:
在本地显示上启用 X11 转发
X11 转发可用于在 Windows 计算机上显示运行在树莓派上的应用程序。
准备就绪
如果你正在运行最新的 Raspbian 版本,SSH 和 X11 转发将默认启用(否则,请检查 如何工作... 部分中解释的设置)。
如何做到这一点...
Linux 和 OS X 内置了对 X11 转发的支持,但如果你使用的是 Windows,你将需要在你的计算机上安装并运行 X Windows 服务器。
从Xming网站下载并运行xming
(sourceforge.net/projects/xming/
).
安装 xming
,按照安装步骤进行,包括如果你还没有的话,安装PuTTY。你也可以从www.putty.org/
单独下载 PuTTY。
接下来,我们需要确保在我们连接时,我们使用的 SSH 程序已启用 X11。
对于 Windows 系统,我们将使用 PuTTY 连接到树莓派。
在 PuTTY 配置对话框中,导航到连接 | SSH | X11 并勾选启用 X11 转发的复选框。如果您留空 X 显示位置选项,它将默认为 Server 0:0
,如下所示(您可以通过在 Xming 运行时将鼠标移至系统托盘中的 Xming 图标来确认服务器编号):
在 PuTTY 配置中启用 X11 转发
在会话设置中输入 Raspberry Pi 的 IP 地址(你也许会发现这里也可以使用 Raspberry Pi 的主机名;默认主机名为raspberrypi
)。
使用合适的名称,例如RaspberryPi
,保存设置,然后点击“打开”以连接到您的 Raspberry Pi。
你可能会看到一个警告信息弹出,提示你之前尚未连接到电脑(这允许你在继续之前检查你是否一切都设置正确):
使用 PuTTY 打开到 Raspberry Pi 的 SSH 连接
对于 OS X 或 Linux,点击“终端”以打开与树莓派的连接。
要连接到默认的pi
用户名,IP 地址为192.168.1.69
,请使用以下命令;-X
选项启用 X11 转发:
ssh -X pi@192.168.1.69
如果一切顺利,你应该会看到一个要求输入密码的提示(记住pi
用户的默认密码是raspberry
)。
确保 Xming 正在运行,通过从您的计算机开始菜单启动 Xming 程序。然后,在终端窗口中,输入通常在树莓派桌面中运行的程序,例如leafpad
或scratch
。稍等片刻,程序应该会出现在您的计算机桌面上(如果您收到错误,您可能忘记启动 Xming,所以请运行它并再次尝试)。
它是如何工作的...
X Windows 和 X11 是提供方法,使得树莓派(以及许多其他基于 Linux 的计算机)能够作为桌面的一部分显示和控制图形窗口。
要在网络连接上使 X11 转发工作,我们需要在 Raspberry Pi 上同时启用 SSH 和 X11 转发。执行以下步骤:
-
要开启(或关闭)SSH,您可以访问树莓派配置
在桌面上的“首选项”菜单下选择程序,然后在“接口”标签中点击 SSH,如图所示(SSH 通常在大多数发行版中默认启用,以帮助允许无需监视器即可配置的远程连接):
raspi-config 工具中的高级设置菜单
-
确保在树莓派上启用 X11 转发(再次强调,现在大多数发行版默认已启用此功能)。
-
使用以下命令与
nano
配合:
sudo nano /etc/ssh/sshd_config
- 在
/etc/ssh/sshd_config
文件中查找控制 X11 转发的行,并确保它显示为yes
(前面没有#
符号),如下所示:
X11Forwarding yes
- 如有需要,请按 Ctrl + X,Y 和 Enter 保存,并根据需要(如果需要更改)按照以下步骤重新启动:
sudo reboot
还有更多...
SSH 和 X11 转发是远程控制树莓派的一种便捷方式;我们将在接下来的章节中探讨一些如何有效使用它的额外技巧。
使用 X11 转发运行多个程序
如果你想要运行一个X 程序,同时还能使用相同的终端控制台进行其他操作,你可以通过以下方式在后台运行命令,使用&
符号:
leafpad &
只需记住,你运行的程序越多,一切都会变得越慢。你可以通过输入fg
来切换到后台程序,并使用bg
来检查后台任务。
以桌面形式运行并启用 X11 转发
您甚至可以通过 X11 运行一个完整的桌面会话,尽管这并不是特别
用户友好且 VNC 将产生更好的结果。为了实现这一点,您必须使用lxsession
而不是startx
(以您通常从终端启动桌面的方式)。
另一个选择是使用 lxpanel
,它提供了一个程序菜单栏,您可以从菜单中启动和运行程序,就像在桌面上一样。
在 X11 转发下运行 Pygame 和 Tkinter
当运行 Pygame 或 Tkinter 脚本时,可能会遇到以下(或类似)错误:
_tkinter.TclError: couldn't connect to display "localhost:10.0"
在此情况下,使用以下命令来修复错误:
sudo cp ~/.Xauthority ~root/
将树莓派的主文件夹通过 SMB 共享
当你的树莓派连接到网络时,你可以通过设置文件共享来访问家庭文件夹;这使得文件传输变得容易得多,同时也提供了一个快速简便的方法来备份你的数据。服务器消息块(SMB)是一种与 Windows 文件共享、OS X 和 Linux 兼容的协议。
准备就绪
确保你的树莓派已经开机并且能够连接到互联网。
你还需要在同一局域网内另一台电脑上测试新的共享。
如何做到这一点...
首先,我们需要安装samba
,这是一款处理文件夹共享的软件,其格式与 Windows 共享方法兼容:
- 确保您按照以下方式使用
update
以获取最新可用的软件包列表:
sudo apt-get update
sudo apt-get install samba
安装大约需要 20 MB 的空间,并花费几分钟时间。
- 一旦安装完成,我们可以按照以下步骤复制配置文件,以便在需要时恢复默认设置:
sudo cp /etc/samba/smb.conf /etc/samba/smb.conf.backup
sudo nano /etc/samba/smb.conf
滚动并找到名为Authentication
的部分;将# security = user
行更改为security = user
。
如文件中所述,此设置确保您必须输入 Raspberry Pi 的用户名和密码才能访问文件(这对于共享网络来说很重要)。
找到名为 Share Definitions
和 [homes]
的部分,并将 read only = yes
行更改为 read only = no
。
这将使我们能够查看并写入共享的主文件夹中的文件。完成后,通过按Ctrl + X,Y和Enter键保存并退出。
如果你已将默认用户从 pi
改为其他用户,请在以下说明中替换它。
- 现在,我们可以添加
pi
(默认用户)以使用samba
:
sudo pdbedit -a -u pi
- 现在,输入一个密码(你可以使用与登录相同的密码或选择一个不同的密码,但请避免使用默认的树莓派密码,因为这很容易被人猜到)。按照以下步骤重启
samba
以使用新的配置文件:
sudo /etc/init.d/samba restart
[ ok ] Stopping Samba daemons: nmbd smbd.
[ ok ] Starting Samba daemons: nmbd smbd.
- 为了测试,你需要知道 Raspberry Pi 的
主机名
(默认的主机名是raspberrypi
)或者它的 IP 地址。你可以使用以下命令找到这两个信息:
hostname
- 对于 IP 地址,添加
-I
:
hostname -I
在网络上的另一台计算机上,输入 \raspberrypi
地址
探索路径。
根据您的网络环境,计算机应在网络上定位到 Raspberry Pi,并提示输入用户名和密码。如果无法通过hostname
找到共享,您可以直接使用 IP 地址,其中192.168.1.69
应更改为匹配 IP 地址\192.168.1.69pi
。
保持树莓派系统更新
树莓派使用的 Linux 图像通常会更新,包括对系统的增强、修复和改进,以及添加对新硬件的支持或对最新板卡的更改。许多您安装的软件包也可以更新。
这一点尤其重要,如果你打算在另一块 Raspberry Pi 板上使用相同的系统镜像(尤其是较新的板),因为较老的镜像将不支持任何布线更改或替代的 RAM 芯片。新的固件应该能在较老的 Raspberry Pi 板上工作,但较老的固件可能不与最新的硬件兼容。
幸运的是,你不必每次有新版本发布时都重新刷新你的 SD 卡,因为你可以选择更新它。
准备就绪
您需要连接到互联网以更新您的系统。始终建议首先备份您的镜像(并且至少复制您的关键文件)。
您可以使用uname -a
命令检查当前固件的版本,如下所示:
Linux raspberrypi 4.4.9-v7+ #884 SMP Fri May 6 17:28:59 BST 2016 armv7l GNU/Linux
可以使用/opt/vc/bin/vcgencmd version
命令来检查 GPU 固件,具体如下:
May 6 2016 13:53:23
Copyright (c) 2012 Broadcom
version 0cc642d53eab041e67c8c373d989fef5847448f8 (clean) (release)
这对于使用较旧版本的固件(2012 年 11 月之前)的新板来说很重要,因为原始的 Model B 板只有 254 MB 的 RAM。升级可以让固件在可用的情况下利用额外的内存。
free -h
命令将详细显示主处理器(总 RAM 在 GPU 和 ARM 核心之间分配)可用的 RAM,并将输出以下内容:
total used free shared buffers cached
Mem: 925M 224M 701M 7.1M 14M 123M
-/+ buffers/cache: 86M 839M
Swap: 99M 0B 99M
您可以在重启后重新检查先前的输出,以确认它们已被更新(尽管它们可能已经是最新版本)。
如何做到这一点...
- 在运行任何升级或安装任何软件包之前,确保你有仓库中最新软件包列表是很值得的。
update
命令获取最新可用的软件和版本列表:
sudo apt-get update
- 如果你只想升级你当前的软件包,使用
upgrade
命令将它们全部更新到最新版本:
sudo apt-get upgrade
- 为了确保您正在运行最新的 Raspbian 版本,您可以运行
dist-upgrade
(请注意:这可能需要一个小时左右,具体取决于需要升级的量)。这将执行upgrade
将执行的所有更新,但还会删除冗余包并进行清理:
sudo apt-get dist-upgrade
两种方法都将升级软件,包括启动和启动时使用的固件(bootcode.bin
和 start.elf
)。
- 更新固件时,可以使用以下命令:
sudo rpi-update
还有更多...
你经常会发现你想要对你的设置进行一次干净的安装,然而,这意味着你将不得不从头开始安装一切。为了避免这种情况,我开发了 Pi-Kitchen 项目(github.com/PiHw/Pi-Kitchen
),该项目基于凯文·希尔的基础工作。这个项目旨在提供一个灵活的平台,用于创建可自动部署到 SD 卡的定制设置:
Pi Kitchen 允许在开机前配置 Raspberry Pi
Pi-Kitchen 允许配置一系列口味,这些口味可以从 NOOBS 菜单中选择。每个口味都包含一系列食谱,每个食谱都为最终操作系统提供特定的功能或特性。食谱的范围可以从为 Wi-Fi 设备设置自定义驱动程序,到映射您网络上的共享驱动器,到提供开箱即用的完整功能网页服务器,所有这些组合起来构成了您所需的设置。
该项目处于测试阶段,作为一个概念验证而开发,但一旦你完成所有配置,直接将完整的工作设置部署到 SD 卡上会非常有用。最终,该项目可以与凯文·希尔(Kevin Hill)的 NOOBS 高级版本相结合,称为PINN Is Not NOOBS(PINN),旨在为高级用户提供额外功能,例如允许操作系统和配置存储在网络或外部 USB 闪存驱动器上。
第二章:分割文本数据并构建文本分类器
本章介绍了以下食谱:
-
构建文本分类器
-
使用分词进行数据处理
-
对文本数据进行词干提取
-
使用分块划分文本
-
构建词袋模型
-
文本分类器的应用
简介
本章介绍了构建文本分类器的配方。这包括从数据库中提取关键特征、训练、测试和验证文本分类器。最初,文本分类器使用常用词汇进行训练。随后,训练好的文本分类器用于预测。构建文本分类器包括使用分词对数据进行预处理、对文本数据进行词干提取、使用分块对文本进行划分,以及构建词袋模型。
构建文本分类器
分类单元通常被认为是将数据库划分为各种类别。在文献中,朴素贝叶斯分类方案被广泛认为可以根据训练模型来分离文本。本章的这一部分最初考虑了一个包含关键词的文本数据库;特征提取从文本中提取关键短语并训练分类系统。然后,实施词频-逆文档频率(tf-idf)转换来指定单词的重要性。最后,使用分类系统预测并打印输出。
如何做到这一点...
- 在一个新的 Python 文件中包含以下行以添加数据集:
from sklearn.datasets import fetch_20newsgroups
category_mapping = {'misc.forsale': 'Sellings', 'rec.motorcycles': 'Motorbikes',
'rec.sport.baseball': 'Baseball', 'sci.crypt': 'Cryptography',
'sci.space': 'OuterSpace'}
training_content = fetch_20newsgroups(subset='train',
categories=category_mapping.keys(), shuffle=True, random_state=7)
- 对文本执行特征提取以提取主要单词:
from sklearn.feature_extraction.text import CountVectorizer
vectorizing = CountVectorizer()
train_counts = vectorizing.fit_transform(training_content.data)
print "nDimensions of training data:", train_counts.shape
- 训练分类器:
from sklearn.naive_bayes import MultinomialNB
from sklearn.feature_extraction.text import TfidfTransformer
input_content = [
"The curveballs of right handed pitchers tend to curve to the left",
"Caesar cipher is an ancient form of encryption",
"This two-wheeler is really good on slippery roads"
]
tfidf_transformer = TfidfTransformer()
train_tfidf = tfidf_transformer.fit_transform(train_counts)
- 实现多项式朴素贝叶斯分类器:
classifier = MultinomialNB().fit(train_tfidf, training_content.target)
input_counts = vectorizing.transform(input_content)
input_tfidf = tfidf_transformer.transform(input_counts)
- 预测输出类别:
categories_prediction = classifier.predict(input_tfidf)
- 打印输出:
for sentence, category in zip(input_content, categories_prediction):
print 'nInput:', sentence, 'nPredicted category:',
category_mapping[training_content.target_names[category]]
以下截图提供了基于数据库输入预测对象的示例:
它是如何工作的...
本章的前一部分提供了关于实现分类器部分和一些样本结果的见解。分类器部分是基于在训练好的朴素贝叶斯分类器中的先前文本与测试序列中的关键测试之间的比较来工作的。
参见
请参阅以下文章:
-
情感分析算法与应用:综述
www.sciencedirect.com/science/article/pii/S2090447914000550
. -
在线评论的情感分类:使用基于句子的语言模型来学习情感预测是如何工作的。
www.tandfonline.com/doi/abs/10.1080/0952813X.2013.782352?src=recsys&journalCode=teta20
. -
使用产品评论数据进行情感分析 和 在存在模态的情况下进行句子级情感分析,以了解更多在
journalofbigdata.springeropen.com/articles/10.1186/s40537-015-0015-2
和link.springer.com/chapter/10.1007/978-3-642-54903-8_1
中使用的推荐系统指标。
使用分词进行数据预处理
数据预处理涉及将现有文本转换为学习算法可接受的信息。
分词是将文本划分为一组有意义的片段的过程。这些片段被称为标记。
如何进行操作...
- 介绍句子分词:
from nltk.tokenize import sent_tokenize
- 形成一个新的文本分词器:
tokenize_list_sent = sent_tokenize(text)
print "nSentence tokenizer:"
print tokenize_list_sent
- 形成一个新的单词分词器:
from nltk.tokenize import word_tokenize
print "nWord tokenizer:"
print word_tokenize(text)
- 介绍一个新的 WordPunct 分词器:
from nltk.tokenize import WordPunctTokenizer
word_punct_tokenizer = WordPunctTokenizer()
print "nWord punct tokenizer:"
print word_punct_tokenizer.tokenize(text)
分词器得到的结果在此展示。它将一个句子划分为词组:
文本数据归一化
词干处理过程涉及为分词器中的单词创建一个带有减少字母的合适单词。
如何做到这一点...
- 使用新的 Python 文件初始化词干提取过程:
from nltk.stem.porter import PorterStemmer
from nltk.stem.lancaster import LancasterStemmer
from nltk.stem.snowball import SnowballStemmer
- 让我们描述一些需要考虑的词汇,如下所示:
words = ['ability', 'baby', 'college', 'playing', 'is', 'dream', 'election', 'beaches', 'image', 'group', 'happy']
- 确定要使用的一组
词干提取器
:
stemmers = ['PORTER', 'LANCASTER', 'SNOWBALL']
- 初始化所选
stemmers
所需的必要任务:
stem_porter = PorterStemmer()
stem_lancaster = LancasterStemmer()
stem_snowball = SnowballStemmer('english')
- 格式化表格以打印结果:
formatted_row = '{:>16}' * (len(stemmers) + 1)
print 'n', formatted_row.format('WORD', *stemmers), 'n'
- 反复检查单词列表,并使用选定的
词干提取器
对它们进行排列:
for word in words:
stem_words = [stem_porter.stem(word),
stem_lancaster.stem(word),
stem_snowball.stem(word)]
print formatted_row.format(word, *stem_words)
从词干处理过程中获得的结果显示在下述截图:
使用分块划分文本
块状处理程序可以用来将大文本分割成小而有意义的单词。
如何做到这一点...
- 使用 Python 开发并导入以下包:
import numpy as np
from nltk.corpus import brown
- 描述一个将文本分割成块的功能:
# Split a text into chunks
def splitter(content, num_of_words):
words = content.split(' ')
result = []
- 初始化以下编程行以获取指定的变量:
current_count = 0
current_words = []
- 使用以下词汇开始迭代:
for word in words:
current_words.append(word)
current_count += 1
- 在获得基本词汇量后,重新组织变量:
if current_count == num_of_words:
result.append(' '.join(current_words))
current_words = []
current_count = 0
- 将块附加到输出变量:
result.append(' '.join(current_words))
return result
- 导入
布朗语料库
的数据,并考虑前10000
个单词:
if __name__=='__main__':
# Read the data from the Brown corpus
content = ' '.join(brown.words()[:10000])
- 描述每个块中的字大小:
# Number of words in each chunk
num_of_words = 1600
- 初始化一对重要变量:
chunks = []
counter = 0
- 通过调用
splitter
函数来打印结果:
num_text_chunks = splitter(content, num_of_words)
print "Number of text chunks =", len(num_text_chunks)
- 将数据块化后得到的结果如下截图所示:
构建词袋模型
当处理包含大量单词的文本文档时,我们需要将它们转换为几种不同的算术表示形式。我们需要将它们制定成适合机器学习算法的形式。这些算法需要算术信息,以便它们可以检查数据并提供重要的细节。词袋(Bag-of-words)过程帮助我们实现这一点。词袋通过使用文档中的所有单词来发现词汇,创建一个文本模型。随后,它通过构建文本中所有单词的直方图来为每篇文本创建模型。
如何做到这一点...
- 通过导入以下文件来初始化一个新的 Python 文件:
import numpy as np
from nltk.corpus import brown
from chunking import splitter
- 定义
main
函数并从Brown 语料库
读取输入数据:
if __name__=='__main__':
content = ' '.join(brown.words()[:10000])
- 将文本内容分成块:
num_of_words = 2000
num_chunks = []
count = 0
texts_chunk = splitter(content, num_of_words)
- 基于这些
文本
块构建词汇:
for text in texts_chunk:
num_chunk = {'index': count, 'text': text}
num_chunks.append(num_chunk)
count += 1
- 提取文档词矩阵,该矩阵有效地统计文档中每个单词出现的次数:
from sklearn.feature_extraction.text
import CountVectorizer
- 提取文档术语
matrix:
from sklearn.feature_extraction.text import CountVectorizer
vectorizer = CountVectorizer(min_df=5, max_df=.95)
matrix = vectorizer.fit_transform([num_chunk['text'] for num_chunk in num_chunks])
- 提取词汇并打印:
vocabulary = np.array(vectorizer.get_feature_names())
print "nVocabulary:"
print vocabulary
- 打印文档项
矩阵
:
print "nDocument term matrix:"
chunks_name = ['Chunk-0', 'Chunk-1', 'Chunk-2', 'Chunk-3', 'Chunk-4']
formatted_row = '{:>12}' * (len(chunks_name) + 1)
print 'n', formatted_row.format('Word', *chunks_name), 'n'
- 遍历所有单词,并打印每个单词在各种块中的重复出现:
for word, item in zip(vocabulary, matrix.T):
# 'item' is a 'csr_matrix' data structure
result = [str(x) for x in item.data]
print formatted_row.format(word, *result)
- 执行词袋模型后得到的结果如下所示:
为了理解它在特定句子中的工作原理,请参考以下内容:
文本分类器的应用
文本分类器用于分析客户情绪,在产品评论中,在互联网搜索查询时,在社会标签中,预测研究文章的新颖性,等等。
第三章:使用 Python 进行自动化和生产力提升
在本章中,我们将涵盖以下主题:
-
使用 Tkinter 创建图形用户界面
-
创建图形化启动菜单应用程序
-
在应用程序中显示照片信息
-
自动整理您的照片
简介
到目前为止,我们一直专注于命令行应用程序;然而,树莓派的功能远不止命令行那么简单。通过使用图形用户界面(GUIs),通常更容易从用户那里获取输入并以更简单的方式提供反馈。毕竟,我们一直在不断地处理多个输入和输出,所以为什么非得限制自己只使用命令行的程序格式呢?
幸运的是,Python 可以支持这一点。和其他编程语言一样,例如 Visual Basic 和 C/C++/C#,这可以通过使用预构建的对象来实现,这些对象提供了标准控件。我们将使用一个名为 Tkinter 的模块,它提供了一系列良好的控件(也称为 小部件)和工具,用于创建图形应用程序。
首先,我们将以一个示例,encryptdecrypt.py
,来展示如何以多种方式编写和重用有用的模块。这是一个良好的编码实践示例。我们应该努力编写可以彻底测试并在许多地方重用的代码。
接下来,我们将通过创建一个小的图形化启动菜单应用程序来扩展我们之前的示例,以便从该应用程序运行我们的最爱应用。
然后,我们将探讨在我们的应用程序中使用类来显示信息,然后进行操作的方法。
组织照片。
使用 Tkinter 创建图形用户界面
我们将创建一个基本的图形用户界面,以便用户输入信息,然后程序可以用来加密和解密这些信息。
准备就绪
您必须确保此文件放置在同一目录下。
由于我们正在使用 Tkinter(Python 众多可用插件之一),我们需要确保它已安装。它应该默认安装在标准的 Raspbian 镜像中。我们可以通过从 Python 提示符导入它来确认它已安装,如下所示:
Python3
>>> import tkinter
如果未安装,将会抛出 ImportError
异常,在这种情况下,您可以使用以下命令进行安装(使用 Ctrl + Z 退出 Python 提示符):
sudo apt-get install python3-tk
如果模块已成功加载,您可以使用以下命令来了解更多关于该模块的信息(阅读完毕后,使用 Q 退出):
>>>help(tkinter)
您也可以使用以下命令获取模块中所有类、函数和方法的详细信息:
>>>help(tkinter.Button)
以下 dir
命令将列出 module:
范围内的任何有效命令或变量。
>>>dir(tkinter.Button)
你会发现我们自己的模块将包含由三引号标记的函数信息;如果我们使用help
命令,这些信息将会显示出来。
命令行无法显示本章中创建的图形显示,因此您需要启动树莓派桌面(使用命令startx
),或者如果您正在远程使用它。
确保您已启用X11 转发并且有一个X 服务器正在运行(参见第一章,使用 Raspberry Pi 3 计算机入门)。
如何做到这一点...
我们将使用 tkinter
模块为 encryptdecrypt.py
脚本创建一个图形用户界面。
要生成 GUI,我们将创建以下tkencryptdecrypt.py
脚本:
#!/usr/bin/python3
#tkencryptdecrypt.py
import encryptdecrypt as ENC
import tkinter as TK
def encryptButton():
encryptvalue.set(ENC.encryptText(encryptvalue.get(),
keyvalue.get()))
def decryptButton():
encryptvalue.set(ENC.encryptText(encryptvalue.get(),
-keyvalue.get()))
#Define Tkinter application
root=TK.Tk()
root.title("Encrypt/Decrypt GUI")
#Set control & test value
encryptvalue = TK.StringVar()
encryptvalue.set("My Message")
keyvalue = TK.IntVar()
keyvalue.set(20)
prompt="Enter message to encrypt:"
key="Key:"
label1=TK.Label(root,text=prompt,width=len(prompt),bg='green')
textEnter=TK.Entry(root,textvariable=encryptvalue,
width=len(prompt))
encryptButton=TK.Button(root,text="Encrypt",command=encryptButton)
decryptButton=TK.Button(root,text="Decrypt",command=decryptButton)
label2=TK.Label(root,text=key,width=len(key))
keyEnter=TK.Entry(root,textvariable=keyvalue,width=8)
#Set layout
label1.grid(row=0,columnspan=2,sticky=TK.E+TK.W)
textEnter.grid(row=1,columnspan=2,sticky=TK.E+TK.W)
encryptButton.grid(row=2,column=0,sticky=TK.E)
decryptButton.grid(row=2,column=1,sticky=TK.W)
label2.grid(row=3,column=0,sticky=TK.E)
keyEnter.grid(row=3,column=1,sticky=TK.W)
TK.mainloop()
#End
使用以下命令运行脚本:
python3 tkencryptdecrypt
它是如何工作的...
我们首先导入两个模块;第一个是我们自己的encryptdecrypt
模块,第二个是tkinter
模块。为了更方便地看到哪些项目来自哪里,我们使用ENC
/TK
。如果你想要避免额外的引用,你可以使用from <module_name> import *
来直接引用模块中的项目。
当我们点击加密和解密按钮时,将会调用encryptButton()
和decryptButton()
函数;它们将在接下来的章节中进行解释。
使用 Tk()
命令创建主 Tkinter 窗口,该命令返回一个主窗口,其中可以放置所有小部件/控件。
我们将如下定义六个控制项:
-
标签
: 这会显示用于加密的提示输入信息: -
Entry
: 这提供了一个文本框,用于接收用户要加密的消息 -
按钮
: 这是一个加密按钮,用于触发消息加密 -
按钮
: 这是一个解密按钮,用于反转加密 -
标签
: 这会显示“键:”字段以提示用户输入加密密钥值 -
条目
:这提供了一个第二个文本框来接收加密密钥的值
这些控件将生成一个类似于以下截图所示的图形用户界面:
加密/解密消息的 GUI
让我们来看看第一个label1
的定义:
label1=TK.Label(root,text=prompt,width=len(prompt),bg='green')
所有控件都必须与应用程序窗口相关联;因此,我们必须指定我们的 Tkinter 窗口root
。用于标签的文本由text
设置;在这种情况下,我们将其设置为名为prompt
的字符串,该字符串之前已经定义了我们需要的文本。我们还设置了width
以匹配消息的字符数(虽然不是必需的,但如果我们以后要添加更多文本到标签中,它将提供一个更整洁的结果),最后,我们使用bg='green'
设置了背景颜色。
接下来,我们为我们的消息定义文本Entry
框:
textEnter=TK.Entry(root,textvariable=encryptvalue,
width=len(prompt))
我们将定义textvariable
——一种将变量链接到框内容的实用方法,该框是一个特殊的字符串变量。我们可以直接使用textEnter.get()
来访问text
,但我们将使用Tkinter StringVar()
对象来间接访问它。如果需要,这将允许我们将我们正在处理的数据与处理 GUI 布局的代码分开。每当使用.set()
命令时(以及使用.get()
命令从Entry
小部件获取最新值时),enycrptvalue
变量会自动更新它所链接的Entry
小部件。
接下来,我们有我们的两个按钮
小部件,加密和解密,如下所示:
encryptButton=TK.Button(root,text="Encrypt",command=encryptButton)
decryptButton=TK.Button(root,text="Decrypt",command=decryptButton)
在这种情况下,我们可以通过设置command
属性来定义一个在点击Button
小部件时被调用的函数。我们可以定义当每个按钮被点击时将被调用的两个函数。在下面的代码片段中,我们有encryptButton()
函数,该函数将设置控制第一个Entry
框内容的encryptvalue StringVar
。这个字符串被设置为通过调用ENC.encryptText()
得到的我们想要加密的消息(encryptvalue
的当前值)和keyvalue
变量。decrypt()
函数与此完全相同,只是我们将keyvalue
变量设置为负值以解密消息:
def encryptButton():
encryptvalue.set(ENC.encryptText(encryptvalue.get(),
keyvalue.get()))
我们随后以类似的方式设置了最终的标签
和条目
小部件。请注意,如果需要,textvariable
也可以是一个整数(数值),但是没有内置的检查来确保只能输入数字。当使用.get()
命令时,你将遇到ValueError
异常。
在我们定义了 Tkinter 窗口中要使用的所有小部件之后,我们必须设置布局。在 Tkinter 中有三种定义布局的方法:放置、打包和网格。
场地布局允许我们使用精确的像素位置来指定位置和大小。打包布局按照项目添加的顺序将项目放置在窗口中。网格布局允许我们在特定的布局中放置项目。建议尽可能避免使用场地布局,因为对任何一个项目的微小更改都可能对其他所有项目的位置和大小产生连锁反应;其他布局通过确定它们相对于窗口中其他项目的位置来解决这个问题。
我们将按照以下截图所示放置项目:
加密/解密 GUI 的网格布局
使用以下代码设置 GUI 中前两项的位置:
label1.grid(row=0,columnspan=2,sticky= TK.E+TK.W)
textEnter.grid(row=1,columnspan=2,sticky= TK.E+TK.W)
我们可以指定第一个标签
和条目
框将跨越两列(columnspan=2
),并且我们可以设置粘性值以确保它们延伸到边缘。这是通过设置东边的TK.E
和西边的TK.W
来实现的。如果我们需要垂直地做同样的事情,我们可以使用TK.N
代表北边和TK.S
代表南边。如果未指定column
值,网格函数默认为column=0
。其他项目也是类似定义的。
最后一步是调用 TK.mainloop()
,这允许 Tkinter 运行;这允许监控按钮的点击事件,并且 Tkinter 会调用与它们关联的函数。
创建图形应用程序 – 开始菜单
本食谱中的示例展示了我们如何定义自己的 Tkinter 对象变体以生成自定义控件,并使用它们动态构建菜单。我们还将简要探讨使用线程,以便在执行特定任务的同时允许其他任务继续运行。
准备就绪
要查看 GUI 显示,您需要一个显示 Raspberry Pi 桌面的显示器,或者您需要连接到运行 X 服务器的另一台计算机。
如何做到这一点...
- 要创建一个图形化启动菜单应用程序,创建以下
graphicmenu.py
脚本:
#!/usr/bin/python3
# graphicmenu.py
import tkinter as tk
from subprocess import call
import threading
#Define applications ["Display name","command"]
leafpad = ["Leafpad","leafpad"]
scratch = ["Scratch","scratch"]
pistore = ["Pi Store","pistore"]
app_list = [leafpad,scratch,pistore]
APP_NAME = 0
APP_CMD = 1
class runApplictionThread(threading.Thread):
def __init__(self,app_cmd):
threading.Thread.__init__(self)
self.cmd = app_cmd
def run(self):
#Run the command, if valid
try:
call(self.cmd)
except:
print ("Unable to run: %s" % self.cmd)
class appButtons:
def __init__(self,gui,app_index):
#Add the buttons to window
btn = tk.Button(gui, text=app_list[app_index][APP_NAME],
width=30, command=self.startApp)
btn.pack()
self.app_cmd=app_list[app_index][APP_CMD]
def startApp(self):
print ("APP_CMD: %s" % self.app_cmd)
runApplictionThread(self.app_cmd).start()
root = tk.Tk()
root.title("App Menu")
prompt = ' Select an application '
label1 = tk.Label(root, text=prompt, width=len(prompt), bg='green')
label1.pack()
#Create menu buttons from app_list
for index, app in enumerate(app_list):
appButtons(root,index)
#Run the tk window
root.mainloop()
#End
- 之前的代码生成了以下应用:
应用菜单 GUI
它是如何工作的...
我们创建 Tkinter 窗口的方式与之前相同;然而,我们不是单独定义所有项目,而是为应用程序按钮创建一个特殊的类。
我们创建的类充当了appButtons
项目所需包含内容的蓝图或规范。每个项目将包括一个用于app_cmd
的字符串值,一个名为startApp()
的函数,以及一个__init__()
函数。__init__()
函数是一个特殊函数(称为构造函数),在我们创建appButtons
项目时会被调用;它将允许我们创建所需的任何设置。
在这种情况下,__init__()
函数允许我们创建一个新的 Tkinter 按钮,按钮上的文本将被设置为 app_list
中的一个项目,并且当按钮被点击时,将调用 startApp()
函数中的命令。使用 self
关键字是为了确保被调用的命令是该项目的部分;这意味着每个按钮都会调用一个本地定义的函数,该函数可以访问项目的本地数据。
我们将self.app_cmd
的值设置为app_list
中的命令,并通过startApp()
函数使其准备就绪使用。我们现在创建startApp()
函数。如果我们在这里直接运行应用程序命令,Tkinter 窗口将冻结,直到我们打开的应用程序再次关闭。为了避免这种情况,我们可以使用 Python 的线程模块,它允许我们同时执行多个操作。
使用 threading.Thread
类作为模板创建的 runApplicationThread()
类——这将在新类中继承 threading.Thread
类的所有功能。就像我们之前的类一样,我们也为这个类提供了一个 __init__()
函数。我们首先调用继承类的 __init__()
函数以确保其正确设置,然后将 app_cmd
值存储在 self.cmd
中。在 runApplicationThread()
函数创建并初始化后,调用 start()
函数。这个函数是 threading.Thread
的一部分,我们的类可以使用它。当调用 start()
函数时,它将创建一个单独的应用程序线程(即模拟同时运行两件事),允许 Tkinter 在类内部执行 run()
函数的同时继续监控按钮点击。
因此,我们可以将代码放置在run()
函数中以运行所需的应用程序(使用call(self.cmd)
)。
还有更多...
Python 特别强大的一个方面是它支持面向对象设计(OOD)中使用的编程技术。这在现代编程语言中是常见的,用于帮助我们将我们希望程序执行的任务转化为代码中的有意义的构造和结构。OOD 的原则在于,我们将大多数问题视为由几个对象(如 GUI 窗口、按钮等)组成,这些对象相互交互以产生期望的结果。
在上一节中,我们发现我们可以使用类来创建独特的对象,这些对象可以被多次重用。我们创建了一个appButton
类,它生成一个具有类所有特性的对象,包括它自己的个人版本app_cmd
,该版本将由startApp()
函数使用。另一个appButton
类型的对象将拥有其自己的无关的[app_cmd]
数据,其startApp()
函数将使用这些数据。
你可以看到,类(classes)对于将一组相关的变量和函数组合在一个单一的对象中非常有用,并且类会在一个地方保存它自己的数据。拥有多个相同类型的对象(类),每个对象内部都有它们自己的函数和数据,这会导致程序结构更加良好。传统的方法是将所有信息都保存在一个地方,并将每个项目来回传递给不同的函数进行处理;然而,在大系统中这可能会变得繁琐。
以下图表展示了相关功能和数据的组织结构:
数据和函数
到目前为止,我们已经使用 Python 模块将我们的程序的不同部分分离成不同的
文件;这使我们能够在概念上分离程序的不同部分(一个接口、编码器/解码器,或者类库,例如 Tkinter)。模块可以提供代码来控制特定的硬件部分,定义互联网接口,或者提供常用功能的库;然而,它最重要的功能是控制接口(当导入项目时可用的一组函数、变量和类)。一个实现良好的模块应该有一个清晰的接口,这个接口围绕其使用方式而非实现方式来设计。这允许你创建多个模块,它们可以轻松地互换和更改,因为它们共享相同的接口。在我们的前一个例子中,想象一下,仅通过支持encryptText(input_text,key)
,如何轻松地更换encryptdecrypt
模块为另一个模块。复杂的功能可以被拆分成更小、更易于管理的块,这些块可以在多个应用程序中重复使用。
Python 一直都在使用类和模块。每次你导入一个库,比如 sys
或 Tkinter,或者使用 value.str()
转换一个值,以及使用 for...in
遍历一个列表时,你都可以使用它们而无需担心细节。你不必在所写的每一行代码中都使用类或模块,但它们是程序员工具箱中非常有用的工具,在你所做的事情适合它们的时候。
我们将通过在本书的示例中使用类和模块,来理解它们如何使我们能够编写出结构良好、易于测试和维护的代码。
在应用程序中显示照片信息
在本例中,我们将创建一个实用类来处理照片,该类可以被其他应用程序(作为模块)用来轻松访问照片元数据和显示预览图像。
准备就绪
以下脚本使用了Python 图像库(PIL);适用于 Python 3 的兼容版本是Pillow。
Pillow 没有包含在 Raspbian 仓库(由 apt-get
使用)中;因此,我们需要使用一个名为 PIP 的 Python 包管理器 来安装 Pillow。
要安装 Python 3 的包,我们将使用 Python 3 版本的 PIP(这需要 50 MB 的可用空间)。
以下命令可用于安装 PIP:
sudo apt-get update
sudo apt-get install python3-pip
在使用 PIP 之前,请确保您已安装 libjpeg-dev
以允许 Pillow 处理 JPEG 文件。您可以使用以下命令来完成此操作:
sudo apt-get install libjpeg-dev
现在您可以使用以下 PIP 命令安装 Pillow:
sudo pip-3.2 install pillow
PIP 还使得使用 uninstall
而不是 install
来卸载包变得简单。
最后,您可以通过运行 python3
来确认它已成功安装:
>>>import PIL
>>>help(PIL)
你不应该遇到任何错误,并且会看到很多关于 PIL 及其用途的信息(按 Q 键完成)。如下检查已安装的版本:
>>PIL.PILLOW_VERSION
你应该看到2.7.0
(或类似版本)。
PIP 也可以通过使用以下命令安装 pip-2.x 与 Python 2 一起使用:
sudo apt-get install python-pip
使用 sudo pip install
安装的任何包都仅适用于 Python 2。
如何做到这一点...
在应用程序中显示照片信息,请创建以下photohandler.py
脚本:
##!/usr/bin/python3
#photohandler.py
from PIL import Image
from PIL import ExifTags
import datetime
import os
#set module values
previewsize=240,240
defaultimagepreview="./preview.ppm"
filedate_to_use="Exif DateTime"
#Define expected inputs
ARG_IMAGEFILE=1
ARG_LENGTH=2
class Photo:
def __init__(self,filename):
"""Class constructor"""
self.filename=filename
self.filevalid=False
self.exifvalid=False
img=self.initImage()
if self.filevalid==True:
self.initExif(img)
self.initDates()
def initImage(self):
"""opens the image and confirms if valid, returns Image"""
try:
img=Image.open(self.filename)
self.filevalid=True
except IOError:
print ("Target image not found/valid %s" %
(self.filename))
img=None
self.filevalid=False
return img
def initExif(self,image):
"""gets any Exif data from the photo"""
try:
self.exif_info={
ExifTags.TAGS[x]:y
for x,y in image._getexif().items()
if x in ExifTags.TAGS
}
self.exifvalid=True
except AttributeError:
print ("Image has no Exif Tags")
self.exifvalid=False
def initDates(self):
"""determines the date the photo was taken"""
#Gather all the times available into YYYY-MM-DD format
self.filedates={}
if self.exifvalid:
#Get the date info from Exif info
exif_ids=["DateTime","DateTimeOriginal",
"DateTimeDigitized"]
for id in exif_ids:
dateraw=self.exif_info[id]
self.filedates["Exif "+id]=
dateraw[:10].replace(":","-")
modtimeraw = os.path.getmtime(self.filename)
self.filedates["File ModTime"]="%s" %
datetime.datetime.fromtimestamp(modtimeraw).date()
createtimeraw = os.path.getctime(self.filename)
self.filedates["File CreateTime"]="%s" %
datetime.datetime.fromtimestamp(createtimeraw).date()
def getDate(self):
"""returns the date the image was taken"""
try:
date = self.filedates[filedate_to_use]
except KeyError:
print ("Exif Date not found")
date = self.filedates["File ModTime"]
return date
def previewPhoto(self):
"""creates a thumbnail image suitable for tk to display"""
imageview=self.initImage()
imageview=imageview.convert('RGB')
imageview.thumbnail(previewsize,Image.ANTIALIAS)
imageview.save(defaultimagepreview,format='ppm')
return defaultimagepreview
之前的代码定义了我们的Photo
类;在我们将其在还有更多...部分和下一个示例中运行之前,它对我们没有任何用处。
它是如何工作的...
我们定义了一个通用的类,称为Photo
;它包含关于自身的详细信息并提供
访问可交换图像文件格式(EXIF)信息并生成功能的函数
预览图片。
在__init__()
函数中,我们为我们的类变量设置值,并调用self.initImage()
,这将使用 PIL 的Image()
函数打开图像。然后我们调用self.initExif()
和self.initDates()
,并设置一个标志来指示文件是否有效。如果无效,Image()
函数将引发IOError
异常。
initExif()
函数使用 PIL 从 img
对象中读取 EXIF 数据,如下代码片段所示:
self.exif_info={
ExifTags.TAGS[id]:y
for id,y in image._getexif().items()
if id in ExifTags.TAGS
}
之前的代码是一系列复合语句,其结果是self.exif_info
被填充了一个包含标签名称及其相关值的字典。
ExifTag.TAGS
是一个包含可能标签名称列表及其 ID 的字典,如下代码片段所示:
ExifTag.TAGS={
4096: 'RelatedImageFileFormat',
513: 'JpegIFOffset',
514: 'JpegIFByteCount',
40963: 'ExifImageHeight',
...etc...}
image._getexif()
函数返回一个字典,其中包含图像相机设置的所有值,每个值都与它们的相关 ID 相链接,如下所示代码片段:
Image._getexif()={
256: 3264,
257: 2448,
37378: (281, 100),
36867: '2016:09:28 22:38:08',
...etc...}
for
循环将遍历图像 EXIF 值字典中的每个条目,并在ExifTags.TAGS
字典中检查其出现情况;结果将被存储在self.exif_info
中。相应的代码如下:
self.exif_info={
'YResolution': (72, 1),
'ResolutionUnit': 2,
'ExposureMode': 0,
'Flash': 24,
...etc...}
再次强调,如果没有异常,我们设置一个标志来指示 EXIF 数据是有效的,或者如果没有 EXIF 数据,我们抛出一个AttributeError
异常。
initDates()
函数使我们能够收集所有可能的文件日期以及从 EXIF 数据中提取的日期,以便我们可以从中选择一个作为我们希望用于文件的日期。例如,它允许我们将所有图片重命名为标准日期格式的文件名。我们创建一个 self.filedates
字典,并用从 EXIF 信息中提取的三个日期填充它。然后,我们添加文件系统日期(创建和修改),以防没有 EXIF 数据可用。os
模块允许我们使用 os.path.getctime()
和 os.path.getmtime()
来获取文件创建的纪元值。它也可以是文件被移动时的日期和时间——以及文件修改——最后一次被写入时(例如,它通常指的是拍照的日期)。纪元值是自 1970 年 1 月 1 日以来的秒数,但我们可以使用 datetime.datetime.fromtimestamp()
将其转换为年、月、日、小时和秒。添加 date()
仅将其限制为年、月和日。
现在,如果Photo
类要被其他模块使用,而我们又想了解所拍摄图片的日期,我们可以查看self.dates
字典并挑选一个合适的日期。然而,这要求程序员知道self.dates
值的排列方式,而且如果我们后来改变了它们的存储方式,这将会破坏他们的程序。因此,建议我们通过访问函数来访问类中的数据,这样实现就与接口独立(这个过程被称为封装)。我们提供了一个当被调用时返回日期的函数;程序员不需要知道它可能是五个可用日期中的任何一个,甚至不需要知道它们是以纪元值存储的。使用函数,我们可以确保无论数据如何存储或收集,接口都将保持不变。
最后,我们希望Photo
类提供的最后一个功能是previewPhoto()
。这个函数提供了一个生成小缩略图并保存为便携式像素格式(PPM)文件的方法。正如我们很快就会发现的,Tkinter 允许我们在其Canvas
小部件上放置图像,但遗憾的是,它不支持 JPEG 格式,只支持 GIF 或 PPM。因此,我们只需将想要显示的图像的小副本保存为 PPM 格式——附带警告,图像调色板也必须转换为 RGB 格式——然后当需要时让 Tkinter 将其加载到Canvas
上。
总结来说,我们创建的Photo
类如下:
操作 | 描述 |
---|---|
__init__(self,filename) |
这是对象初始化器。 |
initImage(self) |
这返回 img ,一个 PIL 类型的图像对象。 |
initExif(self,image) |
这将提取所有存在的 EXIF 信息。 |
initDates(self) |
这将创建一个包含从文件和照片信息中可用的所有日期的字典。 |
getDate(self) |
这返回了照片拍摄/创建时的日期字符串。 |
previewPhoto(self) |
这返回预览缩略图的文件名字符串。 |
属性及其相应的描述如下:
属性 | 描述 |
---|---|
self.filename |
照片的文件名。 |
self.filevalid |
如果文件成功打开,则此值设置为 True 。 |
self.exifvalid |
如果照片包含 EXIF 信息,则此值设置为True 。 |
self.exif_info |
这包含照片的 EXIF 信息。 |
self.filedates |
这包含了一个从文件和照片信息中获取的可用日期的字典。 |
为了测试新类,我们将编写一些测试代码以确认一切按预期工作;请参阅以下部分。
还有更多...
我们之前创建了Photo
类。现在我们可以在我们的模块中添加一些测试代码来确保它按预期工作。我们可以使用__name__ ="__main__"
属性
如前所述,以检测模块是否被直接运行。
我们可以在photohandler.py
脚本的末尾添加后续的代码部分,以生成以下测试应用程序,其外观如下:
照片查看演示应用程序
在photohandler.py
文件的末尾添加以下代码:
#Module test code
def dispPreview(aPhoto):
"""Create a test GUI"""
import tkinter as TK
#Define the app window
app = TK.Tk()
app.title("Photo View Demo")
#Define TK objects
# create an empty canvas object the same size as the image
canvas = TK.Canvas(app, width=previewsize[0],
height=previewsize[1])
canvas.grid(row=0,rowspan=2)
# Add list box to display the photo data
#(including xyscroll bars)
photoInfo=TK.Variable()
lbPhotoInfo=TK.Listbox(app,listvariable=photoInfo,
height=18,width=45,
font=("monospace",10))
yscroll=TK.Scrollbar(command=lbPhotoInfo.yview,
orient=TK.VERTICAL)
xscroll=TK.Scrollbar(command=lbPhotoInfo.xview,
orient=TK.HORIZONTAL)
lbPhotoInfo.configure(xscrollcommand=xscroll.set,
yscrollcommand=yscroll.set)
lbPhotoInfo.grid(row=0,column=1,sticky=TK.N+TK.S)
yscroll.grid(row=0,column=2,sticky=TK.N+TK.S)
xscroll.grid(row=1,column=1,sticky=TK.N+TK.E+TK.W)
# Generate the preview image
preview_filename = aPhoto.previewPhoto()
photoImg = TK.PhotoImage(file=preview_filename)
# anchor image to NW corner
canvas.create_image(0,0, anchor=TK.NW, image=photoImg)
# Populate infoList with dates and exif data
infoList=[]
for key,value in aPhoto.filedates.items():
infoList.append(key.ljust(25) + value)
if aPhoto.exifvalid:
for key,value in aPhoto.exif_info.items():
infoList.append(key.ljust(25) + str(value))
# Set listvariable with the infoList
photoInfo.set(tuple(infoList))
app.mainloop()
def main():
"""called only when run directly, allowing module testing"""
import sys
#Check the arguments
if len(sys.argv) == ARG_LENGTH:
print ("Command: %s" %(sys.argv))
#Create an instance of the Photo class
viewPhoto = Photo(sys.argv[ARG_IMAGEFILE])
#Test the module by running a GUI
if viewPhoto.filevalid==True:
dispPreview(viewPhoto)
else:
print ("Usage: photohandler.py imagefile")
if __name__=='__main__':
main()
#End
之前的测试代码将运行 main()
函数,该函数接受要使用的照片文件名并创建一个名为 viewPhoto
的新 Photo
对象。如果 viewPhoto
成功打开,我们将调用 dispPreview()
来显示图像及其详细信息。
dispPreview()
函数创建四个 Tkinter 小部件以供显示:一个用于加载缩略图的 Canvas
小部件,一个用于显示照片信息的 Listbox
小部件,以及两个滚动条来控制 Listbox
。首先,我们创建一个大小与缩略图相同的 Canvas
小部件(previewsize
)。
接下来,我们创建photoInfo
,它将成为我们的listvariable
参数,与Listbox
小部件相链接。由于 Tkinter 没有提供创建合适项目的ListVar()
函数,我们使用通用的类型TK.Variable()
,并在设置值之前确保将其转换为元组类型。我们将添加Listbox
小部件;我们需要确保将listvariable
参数设置为photoInfo
,并且设置字体为monospace
。这将允许我们使用空格对齐我们的数据值,因为monospace
是一种固定宽度字体,所以每个字符占据的宽度与其他任何字符相同。
我们通过设置垂直和水平滚动条的Scrollbar
命令参数为lbPhotoInfo.yview
和lbPhotoInfo.xview
,将两个滚动条定义为与Listbox
小部件相关联。然后,我们使用以下命令调整Listbox
的参数:
lbPhotoInfo.configure(xscrollcommand=xscroll.set,
yscrollcommand=yscroll.set)
configure
命令允许我们在创建小部件后添加或更改其参数,在这种情况下,将两个滚动条链接起来,这样当用户在列表内滚动时,Listbox
小部件也可以控制它们。
如前所述,我们利用网格布局来确保Listbox
小部件的两个滚动条正确地放置在其旁边,并且Canvas
小部件位于Listbox
小部件的左侧。
我们现在使用Photo
对象来创建preview.ppm
缩略图文件(使用aPhoto.previewPhoto()
函数),并创建一个可以随后通过以下命令添加到Canvas
小部件的TK.PhotoImage
对象:
canvas.create_image(0,0, anchor=TK.NW, image=photoImg)
最后,我们使用Photo
类收集的日期信息和 EXIF 信息(首先确保其有效性)来填充Listbox
小部件。我们通过将每个项目转换为使用.ljust(25)
间隔的字符串列表来实现这一点——它为名称添加左对齐并填充至 25 个字符宽。一旦我们有了列表,我们就将其转换为元组类型,并设置listvariable
(photoInfo
)参数。
和往常一样,我们调用app.mainloop()
来开始监控事件以进行响应。
自动整理您的照片
现在我们有一个可以让我们收集照片信息的类,我们可以将这个信息应用到执行有用任务中。在这种情况下,我们将使用文件信息自动将一个装满照片的文件夹组织成基于日期的子文件夹集合
照片是在...拍摄的。以下截图显示了脚本的输出:
将脚本输出到文件夹中组织照片
准备就绪
您需要在 Raspberry Pi 上的一个文件夹中放置一组照片。或者,您可以将带有照片的 USB 存储棒或读卡器插入——它们将位于 /mnt/
。然而,请确保您首先使用您照片的副本测试脚本,以防万一有任何问题。
如何做到这一点...
在 filehandler.py
文件中创建以下脚本以自动整理您的照片:
#!/usr/bin/python3
#filehandler.py
import os
import shutil
import photohandler as PH
from operator import itemgetter
FOLDERSONLY=True
DEBUG=True
defaultpath=""
NAME=0
DATE=1
class FileList:
def __init__(self,folder):
"""Class constructor"""
self.folder=folder
self.listFileDates()
def getPhotoNamedates(self):
"""returns the list of filenames and dates"""
return self.photo_namedates
def listFileDates(self):
"""Generate list of filenames and dates"""
self.photo_namedates = list()
if os.path.isdir(self.folder):
for filename in os.listdir(self.folder):
if filename.lower().endswith(".jpg"):
aPhoto = PH.Photo(os.path.join(self.folder,filename))
if aPhoto.filevalid:
if (DEBUG):print("NameDate: %s %s"%
(filename,aPhoto.getDate()))
self.photo_namedates.append((filename,
aPhoto.getDate()))
self.photo_namedates = sorted(self.photo_namedates,
key=lambda date: date[DATE])
def genFolders(self):
"""function to generate folders"""
for i,namedate in enumerate(self.getPhotoNamedates()):
#Remove the - from the date format
new_folder=namedate[DATE].replace("-","")
newpath = os.path.join(self.folder,new_folder)
#If path does not exist create folder
if not os.path.exists(newpath):
if (DEBUG):print ("New Path: %s" % newpath)
os.makedirs(newpath)
if (DEBUG):print ("Found file: %s move to %s" %
(namedate[NAME],newpath))
src_file = os.path.join(self.folder,namedate[NAME])
dst_file = os.path.join(newpath,namedate[NAME])
try:
if (DEBUG):print ("File moved %s to %s" %
(src_file, dst_file))
if (FOLDERSONLY==False):shutil.move(src_file, dst_file)
except IOError:
print ("Skipped: File not found")
def main():
"""called only when run directly, allowing module testing"""
import tkinter as TK
from tkinter import filedialog
app = TK.Tk()
app.withdraw()
dirname = TK.filedialog.askdirectory(parent=app,
initialdir=defaultpath,
title='Select your pictures folder')
if dirname != "":
ourFileList=FileList(dirname)
ourFileList.genFolders()
if __name__=="__main__":
main()
#End
它是如何工作的...
我们将创建一个名为 FileList
的类;它将使用 Photo
类来管理
在特定文件夹内的照片。这个操作主要有两个步骤:我们首先需要找到文件夹内的所有图片,然后生成一个包含文件名和照片日期的列表。我们将使用这些信息来生成新的子文件夹并将照片移动到这些文件夹中。
当我们创建FileList
对象时,我们将使用listFileDates()
来创建列表。然后,我们将确认提供的文件夹是有效的,并使用os.listdir
来获取目录中文件的完整列表。我们将检查每个文件是否为 JPEG 文件,并获取每张照片的日期(使用Photo
类中定义的函数)。接下来,我们将文件名和日期作为一个元组添加到self.photo_namedates
列表中。
最后,我们将使用内置的 sorted
函数来按照文件的日期顺序排列所有文件。虽然在这里我们不需要这样做,但如果我们在其他地方使用这个模块,这个函数将使删除重复日期变得更加容易。
sorted
函数需要列表已排序,在这种情况下,我们希望按 日期值
排序:
sorted(self.photo_namedates, key=lambda date: date[DATE])
我们将用 lambda date:
替换 date[DATE]
作为排序的值。
一旦初始化了FileList
对象,我们就可以通过调用genFolders()
来使用它。首先,我们将日期文本转换为适合我们文件夹的格式(YYYYMMDD),以便我们的文件夹可以按日期顺序轻松排序。接下来,如果当前目录中不存在这些文件夹,它将创建这些文件夹。最后,它将每个文件移动到所需的子文件夹中。
我们最终得到了一个准备测试的FileList
类:
操作 | 描述 |
---|---|
__init__(self,folder) |
这是对象初始化器。 |
getPhotoNamedates(self) |
这将返回照片日期的文件名列表。 |
listFileDates(self) |
这将创建一个包含文件夹中照片的文件名和日期的列表。 |
genFolders(self) |
这将根据照片的日期创建新的文件夹,并将文件移动到这些文件夹中。 |
属性如下列出:
属性 | 描述 |
---|---|
self.folder |
我们正在工作的文件夹。 |
self.photo_namedates |
这包含了一个文件名和日期的列表。 |
FileList
类封装了所有功能和相关数据,将一切保持在逻辑上的一个位置:
Tkinter filediaglog.askdirectory() 用于选择照片目录
为了测试这个功能,我们使用 Tkinter 的filedialog.askdirectory()
小部件来允许我们选择一个目标图片目录。我们使用app.withdrawn()
来隐藏主 Tkinter 窗口,因为这次不需要它。我们只需要创建一个新的FileList
对象,然后调用genFolders()
来将我们所有的照片移动到新的位置!
在此脚本中定义了两个额外的标志,它们为测试提供了额外的控制。DEBUG
允许我们通过将它们设置为 True
或 False
来启用或禁用额外的调试信息。此外,当 FOLDERSONLY
设置为 True
时,只会生成文件夹,不会移动文件(这对于测试新子文件夹是否正确非常有用)。
一旦运行了脚本,你可以检查所有文件夹是否已正确创建。最后,将FOLDERSONLY
更改为True
,下次程序将自动根据照片的日期移动和组织你的照片。建议你
只在您照片的副本上运行此操作,以防出现错误。
第四章:预测词语中的情感
本章介绍了以下食谱:
-
构建朴素贝叶斯分类器
-
逻辑回归分类器
-
将数据集拆分为训练和测试
-
使用交叉验证评估准确性
-
分析句子的情感
-
使用主题模型在文本中识别模式
-
情感分析的应用
构建朴素贝叶斯分类器
高斯贝叶斯分类器使用贝叶斯定理构建一个监督模型。
如何操作...
- 导入以下包:
from sklearn.naive_bayes import GaussianNB
import numpy as np
import matplotlib.pyplot as plt
- 使用以下数据文件,其中包含以逗号分隔的算术数据:
in_file = 'data_multivar.txt'
a = []
b = []
with open(in_file, 'r') as f:
for line in f.readlines():
data = [float(x) for x in line.split(',')]
a.append(data[:-1])
b.append(data[-1])
a = np.array(a)
b = np.array(b)
- 构建一个朴素贝叶斯分类器:
classification_gaussiannb = GaussianNB()
classification_gaussiannb.fit(a, b)
b_pred = classification_gaussiannb.predict(a)
- 计算朴素贝叶斯算法的准确率:
correctness = 100.0 * (b == b_pred).sum() / a.shape[0]
print "correctness of the classification =", round(correctness, 2), "%"
- 绘制分类器的结果:
def plot_classification(classification_gaussiannb, a , b):
a_min, a_max = min(a[:, 0]) - 1.0, max(a[:, 0]) + 1.0
b_min, b_max = min(a[:, 1]) - 1.0, max(a[:, 1]) + 1.0
step_size = 0.01
a_values, b_values = np.meshgrid(np.arange(a_min, a_max, step_size), np.arange(b_min, b_max, step_size))
mesh_output1 = classification_gaussiannb.predict(np.c_[a_values.ravel(), b_values.ravel()])
mesh_output2 = mesh_output1.reshape(a_values.shape)
plt.figure()
plt.pcolormesh(a_values, b_values, mesh_output2, cmap=plt.cm.gray)
plt.scatter(a[:, 0], a[:, 1], c=b , s=80, edgecolors='black', linewidth=1,cmap=plt.cm.Paired)
- 指定图形的边界:
plt.xlim(a_values.min(), a_values.max())
plt.ylim(b_values.min(), b_values.max())
*# specify the ticks on the X and Y axes* plt.xticks((np.arange(int(min(a[:, 0])-1), int(max(a[:, 0])+1), 1.0)))
plt.yticks((np.arange(int(min(a[:, 1])-1), int(max(a[:, 1])+1), 1.0)))
plt.show()
plot_classification(classification_gaussiannb, a, b)
执行朴素贝叶斯分类器后获得的准确度显示在下述截图:
参见
请参阅以下文章:
- 要了解分类器如何工作的示例,请参考以下链接:
- 要了解更多关于使用所提出分类器的文本分类信息,请参阅以下链接:
sebastianraschka.com/Articles/2014_naive_bayes_1.html
- 要了解更多关于朴素贝叶斯分类算法的信息,请参考以下链接:
software.ucv.ro/~cmihaescu/ro/teaching/AIR/docs/Lab4-NaiveBayes.pdf
逻辑回归分类器
这种方法适用于输出只能取两个值的情况,例如 0 或 1、通过/失败、赢/输、生/死、健康/生病等。在因变量有超过两个结果类别的情况下,可以使用多项逻辑回归进行分析。
如何做到这一点...
- 安装完必需的包后,让我们构建一些训练标签:
import numpy as np
from sklearn import linear_model
import matplotlib.pyplot as plt
a = np.array([[-1, -1], [-2, -1], [-3, -2], [1, 1], [2, 1], [3, 2]])
b = np.array([1, 1, 1, 2, 2, 2])
- 初始化分类器:
classification = linear_model.LogisticRegression(solver='liblinear', C=100)
classification.fit(a, b)
- 绘制数据点和边距:
def plot_classification(classification, a , b):
a_min, a_max = min(a[:, 0]) - 1.0, max(a[:, 0]) + 1.0
b_min, b_max = min(a[:, 1]) - 1.0, max(a[:, 1]) + 1.0 step_size = 0.01
a_values, b_values = np.meshgrid(np.arange(a_min, a_max, step_size), np.arange(b_min, b_max, step_size))
mesh_output1 = classification.predict(np.c_[a_values.ravel(), b_values.ravel()])
mesh_output2 = mesh_output1.reshape(a_values.shape)
plt.figure()
plt.pcolormesh(a_values, b_values, mesh_output2, cmap=plt.cm.gray)
plt.scatter(a[:, 0], a[:, 1], c=b , s=80, edgecolors='black',linewidth=1,cmap=plt.cm.Paired)
# specify the boundaries of the figure plt.xlim(a_values.min(), a_values.max())
plt.ylim(b_values.min(), b_values.max())
# specify the ticks on the X and Y axes plt.xticks((np.arange(int(min(a[:, 0])-1), int(max(a[:, 0])+1), 1.0)))
plt.yticks((np.arange(int(min(a[:, 1])-1), int(max(a[:, 1])+1), 1.0)))
plt.show()
plot_classification(classification, a, b)
执行逻辑回归的命令如下截图所示:
将数据集拆分为训练和测试
分割有助于将数据集划分为训练和测试序列。
如何做到这一点...
- 将以下代码片段添加到相同的 Python 文件中:
from sklearn import cross_validation
from sklearn.naive_bayes import GaussianNB
import numpy as np
import matplotlib.pyplot as plt
in_file = 'data_multivar.txt'
a = []
b = []
with open(in_file, 'r') as f:
for line in f.readlines():
data = [float(x) for x in line.split(',')]
a.append(data[:-1])
b.append(data[-1])
a = np.array(a)
b = np.array(b)
- 分配 75%的数据用于训练和 25%的数据用于测试:
a_training, a_testing, b_training, b_testing = cross_validation.train_test_split(a, b, test_size=0.25, random_state=5)
classification_gaussiannb_new = GaussianNB()
classification_gaussiannb_new.fit(a_training, b_training)
- 评估分类器在测试数据上的性能:
b_test_pred = classification_gaussiannb_new.predict(a_testing)
- 计算分类系统的准确率:
correctness = 100.0 * (b_testing == b_test_pred).sum() / a_testing.shape[0]
print "correctness of the classification =", round(correctness, 2), "%"
- 绘制测试数据的点集和边界:
def plot_classification(classification_gaussiannb_new, a_testing , b_testing):
a_min, a_max = min(a_testing[:, 0]) - 1.0, max(a_testing[:, 0]) + 1.0
b_min, b_max = min(a_testing[:, 1]) - 1.0, max(a_testing[:, 1]) + 1.0
step_size = 0.01
a_values, b_values = np.meshgrid(np.arange(a_min, a_max, step_size), np.arange(b_min, b_max, step_size))
mesh_output = classification_gaussiannb_new.predict(np.c_[a_values.ravel(), b_values.ravel()])
mesh_output = mesh_output.reshape(a_values.shape)
plt.figure()
plt.pcolormesh(a_values, b_values, mesh_output, cmap=plt.cm.gray)
plt.scatter(a_testing[:, 0], a_testing[:, 1], c=b_testing , s=80, edgecolors='black', linewidth=1,cmap=plt.cm.Paired)
# specify the boundaries of the figure plt.xlim(a_values.min(), a_values.max())
plt.ylim(b_values.min(), b_values.max())
# specify the ticks on the X and Y axes
plt.xticks((np.arange(int(min(a_testing[:, 0])-1), int(max(a_testing[:, 0])+1), 1.0)))
plt.yticks((np.arange(int(min(a_testing[:, 1])-1), int(max(a_testing[:, 1])+1), 1.0)))
plt.show()
plot_classification(classification_gaussiannb_new, a_testing, b_testing)
在以下屏幕截图中显示了在分割数据集时获得的准确性:
使用交叉验证评估准确性
交叉验证在机器学习中至关重要。最初,我们将数据集分为训练集和测试集。接下来,为了构建一个鲁棒的分类器,我们需要重复这个步骤,但需要避免模型过拟合。过拟合意味着我们在训练集上获得出色的预测结果,但在测试集上结果非常糟糕。过拟合会导致模型泛化能力差。
如何操作...
- 导入所需的包:
from sklearn import cross_validation
from sklearn.naive_bayes import GaussianNB
import numpy as np
in_file = 'cross_validation_multivar.txt'
a = []
b = []
with open(in_file, 'r') as f:
for line in f.readlines():
data = [float(x) for x in line.split(',')]
a.append(data[:-1])
b.append(data[-1])
a = np.array(a)
b = np.array(b)
classification_gaussiannb = GaussianNB()
- 计算分类器的准确率:
num_of_validations = 5
accuracy = cross_validation.cross_val_score(classification_gaussiannb, a, b, scoring='accuracy', cv=num_of_validations)
print "Accuracy: " + str(round(100* accuracy.mean(), 2)) + "%"
f1 = cross_validation.cross_val_score(classification_gaussiannb, a, b, scoring='f1_weighted', cv=num_of_validations)
print "f1: " + str(round(100*f1.mean(), 2)) + "%"
precision = cross_validation.cross_val_score(classification_gaussiannb,a, b, scoring='precision_weighted', cv=num_of_validations)
print "Precision: " + str(round(100*precision.mean(), 2)) + "%"
recall = cross_validation.cross_val_score(classification_gaussiannb, a, b, scoring='recall_weighted', cv=num_of_validations)
print "Recall: " + str(round(100*recall.mean(), 2)) + "%"
- 执行交叉验证后得到的结果如下所示:
为了了解它在一个特定的句子数据集上的工作原理,请参考以下内容:
- 逻辑回归简介:
分析句子的情感
情感分析指的是寻找文本指定部分是正面、负面还是中性的过程。这项技术通常被认为可以了解人们对特定情况的想法。它评估消费者在不同形式中的情感,例如广告活动、社交媒体和电子商务客户。
如何做到这一点...
- 创建一个新文件并导入选定的包:
import nltk.classify.util
from nltk.classify import NaiveBayesClassifier
from nltk.corpus import movie_reviews
- 描述一个提取特征的函数:
def collect_features(word_list):
word = []
return dict ([(word, True) for word in word_list])
- 将 NLTK 中的电影评论作为训练数据:
if __name__=='__main__':
plus_filenum = movie_reviews.fileids('pos')
minus_filenum = movie_reviews.fileids('neg')
- 将数据分为正面和负面评论:
feature_pluspts = [(collect_features(movie_reviews.words(fileids=[f])),
'Positive') for f in plus_filenum]
feature_minuspts = [(collect_features(movie_reviews.words(fileids=[f])),
'Negative') for f in minus_filenum]
- 将数据划分为训练集和测试集:
threshold_fact = 0.8
threshold_pluspts = int(threshold_fact * len(feature_pluspts))
threshold_minuspts = int(threshold_fact * len(feature_minuspts))
- 提取特征:
feature_training = feature_pluspts[:threshold_pluspts] + feature_minuspts[:threshold_minuspts]
feature_testing = feature_pluspts[threshold_pluspts:] + feature_minuspts[threshold_minuspts:]
print "nNumber of training datapoints:", len(feature_training)
print "Number of test datapoints:", len(feature_testing)
- 考虑朴素贝叶斯分类器,并使用指定的目标对其进行训练:
# Train a Naive Bayes classifiers
classifiers = NaiveBayesClassifier.train(feature_training)
print "nAccuracy of the classifiers:",nltk.classify.util.accuracy(classifiers,feature_testing)
print "nTop 10 most informative words:"
for item in classifiers.most_informative_features()[:10]:print item[0]
# Sample input reviews in_reviews = [
"The Movie was amazing",
"the movie was dull. I would never recommend it to anyone.",
"The cinematography is pretty great in the movie",
"The direction was horrible and the story was all over the place"
]
print "nPredictions:"
for review in in_reviews:
print "nReview:", review
probdist = classifiers.prob_classify(collect_features(review.split()))
predict_sentiment = probdist.max()
print "Predicted sentiment:", predict_sentiment
print "Probability:", round(probdist.prob(predict_sentiment), 2)
- 情感分析得到的结果如下所示:
使用主题模型在文本中识别模式
主题建模指的是在文献信息中识别隐藏模式的过程。其目标是揭示一组文档中的一些隐藏主题配置。
如何做这件事...
- 导入以下包:
from nltk.tokenize import RegexpTokenizer
from nltk.stem.snowball import SnowballStemmer
from gensim import models, corpora
from nltk.corpus import stopwords
- 加载输入数据:
def load_words(in_file):
element = []
with open(in_file, 'r') as f:
for line in f.readlines():
element.append(line[:-1])
return element
- 文本预处理课程:
classPreprocedure(object):
def __init__(self):
# Create a regular expression tokenizer self.tokenizer = RegexpTokenizer(r'w+')
- 获取一个停止词列表以终止程序执行:
self.english_stop_words= stopwords.words('english')
- 创建一个 Snowball 词干提取器:
self.snowball_stemmer = SnowballStemmer('english')
- 定义一个函数以执行分词、停用词移除和词干提取:
def procedure(self, in_data):
# Tokenize the string
token = self.tokenizer.tokenize(in_data.lower())
- 从文本中删除停用词:
tokenized_stopwords = [x for x in token if not x in self.english_stop_words]
- 在标记上实现词干提取:
token_stemming = [self.snowball_stemmer.stem(x) for x in tokenized_stopwords]
- 返回处理后的标记:
return token_stemming
- 从
main
函数中加载输入数据:
if __name__=='__main__':
# File containing input data in_file = 'data_topic_modeling.txt'
# Load words element = load_words(in_file)
- 创建一个对象:
preprocedure = Preprocedure()
- 处理文件并提取标记:
processed_tokens = [preprocedure.procedure(x) for x in element]
- 基于分词文档创建一个字典:
dict_tokens = corpora.Dictionary(processed_tokens)
corpus = [dict_tokens.doc2bow(text) for text in processed_tokens]
- 开发一个 LDA 模型,定义所需参数,并初始化 LDA 目标:
num_of_topics = 2
num_of_words = 4
ldamodel = models.ldamodel.LdaModel(corpus,num_topics=num_of_topics, id2word=dict_tokens, passes=25)
print "Most contributing words to the topics:"
for item in ldamodel.print_topics(num_topics=num_of_topics, num_words=num_of_words):
print "nTopic", item[0], "==>", item[1]
- 执行
topic_modelling.py
时得到的结果如下截图所示:
情感分析的应用
情感分析被用于社交媒体如 Facebook 和 Twitter,以找出公众对一个问题的情感(正面/负面)。它们也被用来确定人们对广告的情感,以及人们对你的产品、品牌或服务的感受。
第五章:创建游戏和图形
在本章中,我们将涵盖以下主题:
-
使用 IDLE3 调试您的程序
-
使用鼠标在 Tkinter Canvas 上绘制线条
-
创建一个棒球游戏
-
创建一个滚屏游戏
简介
游戏通常是一个探索和扩展编程技能的绝佳方式,因为它们本身就具有内在的激励力量,促使你修改和改进你的创作,添加新功能,并创造新的挑战。它们也非常适合与他人分享你的想法,即使他们不感兴趣编程。
本章重点介绍如何使用 Tkinter Canvas 小部件在屏幕上创建和显示用户可以与之交互的对象。利用这些技术,可以创建各种游戏和应用,其局限性仅取决于您的创造力。
我们还将快速浏览一下如何在 IDLE3 中使用内置的调试器,这是一个在无需编写大量测试代码的情况下测试和开发程序的有用工具。
第一个示例演示了我们可以如何监控并利用鼠标来创建对象并在画布小部件上直接绘制。然后,我们创建了一个蝙蝠与球游戏,展示了如何控制对象的位置以及如何检测和响应它们之间的交互。最后,我们更进一步,使用 Tkinter 将我们自己的图形放置到画布小部件上,以创建一个俯视图寻宝游戏。
使用 IDLE3 调试您的程序
编程的一个关键方面是能够测试和调试你的代码,而实现这一目标的一个有用工具是调试器。IDLE 编辑器(确保你使用 IDLE3 以支持本书中使用的 Python 3 代码)包含一个基本的调试器。它允许你逐步执行你的代码,观察局部和全局变量的值,并设置断点。
如何做到这一点...
- 要启用调试器,请启动 IDLE3 并从调试菜单中选择调试器;它将打开以下窗口(如果你当前正在运行某些代码,你需要先停止它):
IDLE3 调试器窗口
- 打开你想要测试的代码(通过文件 | 打开...),然后尝试运行它(F5)。你会发现代码无法启动,因为调试器已经自动停止在第一行。以下截图显示了调试器已经停止在
filehandler.py
代码的第一行,即line 3: import os
:
代码开始处的 IDLE3 调试器
它是如何工作的...
下面的截图显示的控制按钮允许您运行和/或跳过代码:
调试器控制
控制按钮的功能如下:
-
Go: 此按钮将正常执行代码。
-
步骤:此按钮将逐行执行代码块,然后再次停止。如果调用了一个函数,它将进入该函数,并允许你逐行执行该函数。
-
Over:此按钮类似于 Step 命令,但如果存在函数调用,它将执行整个函数并在下一行停止。
-
输出:此按钮将一直执行代码,直到它完成当前函数,继续执行直到你退出该函数。
-
退出:此按钮立即结束程序。
除了之前提到的控制之外,你可以在代码中直接设置断点和清除断点。断点是一个可以在代码中插入的标记(通过在编辑器窗口上右键单击),当调试器到达该标记时,它将始终中断(停止),如下面的截图所示:
直接在您的代码中设置和清除断点
复选框(位于控制按钮的右侧)允许您选择在遍历代码或当调试器由于断点而停止时显示哪些信息。堆栈显示在主窗口中,类似于程序遇到未处理的异常时您会看到的情况。堆栈选项显示了为到达代码当前位置所调用的所有函数调用,直至它停止的行。源选项突出显示当前正在执行的代码行,在某些情况下,还包括导入模块中的代码(如果它们是非编译库)。
您还可以选择是否显示局部变量和/或全局变量。默认情况下,源代码和全局变量选项通常被禁用,因为如果需要显示大量数据,这可能会使过程变得相当缓慢。
Python 使用局部和全局变量的概念来定义作用域(变量在哪里以及何时可见和有效)。全局变量在文件的顶层定义,并且在其定义之后可以从代码的任何位置访问。然而,为了从除了顶层以外的任何地方更改其值,Python 需要你首先使用 global 关键字。如果没有使用 global 关键字,你将创建一个具有相同名称的局部副本(其值将在你退出代码块时丢失)。局部变量是在你在一个函数内创建变量时定义的;一旦超出函数范围,该变量就会被销毁,并且不再可见。
在栈数据之后是局部变量 – 在这种情况下,aPhoto
、filename
和 self
。然后(如果启用),我们拥有所有当前有效的全局值,提供了关于程序状态的详细信息(例如 DATE = 1
、DEBUG = True
、FOLDERSONLY = True
等):
调试器中的 Stack(栈)、Locals(局部变量)和 Globals(全局变量)选项
调试器并不特别先进,因为它不允许你展开复杂对象,例如photohandler.Photo
对象,以查看它包含的数据。然而,如果需要,你可以在测试期间调整你的代码,并将你想要观察的数据分配给一些临时变量。
值得学习如何使用调试器,因为它是一种更简单的方法来追踪特定问题并检查事物是否按预期运行。
使用鼠标在 Tkinter Canvas 上绘制线条
Tkinter Canvas 小部件提供了一个区域来创建和绘制对象。以下脚本演示了如何使用鼠标事件与 Tkinter 进行交互。通过检测鼠标点击,我们可以使用 Tkinter 绘制一条跟随鼠标移动的线:
使用 Tkinter 的一个简单绘图应用程序
准备就绪
如前所述,我们需要安装 Tkinter,并且要么是 Raspbian 桌面正在运行(通过命令行执行startx
),要么是带有 X11 转发和一个正在运行的 X 服务器的 SSH 会话(参见第一章,使用 Raspberry Pi 3 计算机入门)。我们还需要连接一个鼠标。
如何做到这一点...
创建以下脚本,painting.py
:
#!/usr/bin/python3
#painting.py
import tkinter as TK
#Set defaults
btn1pressed = False
newline = True
def main():
root = TK.Tk()
the_canvas = TK.Canvas(root)
the_canvas.pack()
the_canvas.bind("<Motion>", mousemove)
the_canvas.bind("<ButtonPress-1>", mouse1press)
the_canvas.bind("<ButtonRelease-1>", mouse1release)
root.mainloop()
def mouse1press(event):
global btn1pressed
btn1pressed = True
def mouse1release(event):
global btn1pressed, newline
btn1pressed = False
newline = True
def mousemove(event):
if btn1pressed == True:
global xorig, yorig, newline
if newline == False:
event.widget.create_line(xorig,yorig,event.x,event.y,
smooth=TK.TRUE)
newline = False
xorig = event.x
yorig = event.y
if __name__ == "__main__":
main()
#End
它是如何工作的...
Python 代码创建了一个包含名为Canvas
对象的 Tkinter 窗口
the_canvas
. 我们在这里使用 bind
函数,它将此小部件(the_canvas
)上发生的特定事件绑定到特定动作或按键。在这种情况下,我们绑定了鼠标的 <Motion>
函数,以及第一个鼠标按钮的点击和释放(<ButtonPress-1>
和 <ButtonRelease-1>
)。然后,这些事件分别用于调用 mouse1press()
、mouse1release()
和 mousemove()
函数。
这里逻辑是使用mouse1press()
和mouse1release()
函数跟踪鼠标按钮的状态。
如果鼠标按钮已被点击,mousemove()
函数将检查我们是在绘制一条新线(我们为此设置了新的坐标)还是继续一条旧线(我们从上一个坐标绘制一条线到触发 mousemove()
的当前事件的坐标)。我们只需确保在鼠标按钮释放时重置到 newline
命令,以重置线的起始位置。
创建一个棒球游戏
使用canvas
绘图工具和检测物体碰撞,可以创建一个经典的蝙蝠和球游戏。用户将能够控制绿色的挡板,使用左右光标键瞄准球,击打砖块,直到它们全部被摧毁:
正在进行的游戏
准备中
此示例需要图形输出,因此您必须将屏幕和键盘连接到树莓派,或者如果从另一台计算机远程连接,则使用 X11 转发和 X 服务器。
如何做到这一点...
创建以下脚本,bouncingball.py
:
-
首先,导入
tkinter
和time
模块,并定义相关常量。游戏图形:
#!/usr/bin/python3
# bouncingball.py
import tkinter as TK
import time
VERT,HOREZ=0,1
xTOP,yTOP = 0,1
xBTM,yBTM = 2,3
MAX_WIDTH,MAX_HEIGHT = 640,480
xSTART,ySTART = 100,200
BALL_SIZE=20
RUNNING=True
- 接下来,创建用于关闭程序、移动挡板左右以及计算球的方向的函数:
def close():
global RUNNING
RUNNING=False
root.destroy()
def move_right(event):
if canv.coords(paddle)[xBTM]<(MAX_WIDTH-7):
canv.move(paddle, 7, 0)
def move_left(event):
if canv.coords(paddle)[xTOP]>7:
canv.move(paddle, -7, 0)
def determineDir(ball,obj):
global delta_x,delta_y
if (ball[xTOP] == obj[xBTM]) or (ball[xBTM] ==
obj[xTOP]):
delta_x = -delta_x
elif (ball[yTOP] == obj[yBTM]) or (ball[yBTM] ==
obj[yTOP]):
delta_y = -delta_y
- 设置
tkinter
窗口并定义canvas
:
root = TK.Tk()
root.title("Bouncing Ball")
root.geometry('%sx%s+%s+%s' %(MAX_WIDTH, MAX_HEIGHT, 100, 100))
root.bind('<Right>', move_right)
root.bind('<Left>', move_left)
root.protocol('WM_DELETE_WINDOW', close)
canv = TK.Canvas(root, highlightthickness=0)
canv.pack(fill='both', expand=True)
- 将边框、
球
和挡板
对象添加到画布
中:
top = canv.create_line(0, 0, MAX_WIDTH, 0, fill='blue',
tags=('top'))
left = canv.create_line(0, 0, 0, MAX_HEIGHT, fill='blue',
tags=('left'))
right = canv.create_line(MAX_WIDTH, 0, MAX_WIDTH, MAX_HEIGHT,
fill='blue', tags=('right'))
bottom = canv.create_line(0, MAX_HEIGHT, MAX_WIDTH, MAX_HEIGHT,
fill='blue', tags=('bottom'))
ball = canv.create_rectangle(0, 0, BALL_SIZE, BALL_SIZE,
outline='black', fill='black',
tags=('ball'))
paddle = canv.create_rectangle(100, MAX_HEIGHT - 30, 150, 470,
outline='black',
fill='green', tags=('rect'))
- 绘制所有砖块并设置球和挡板的位置:
brick=list()
for i in range(0,16):
for row in range(0,4):
brick.append(canv.create_rectangle(i*40, row*20,
((i+1)*40)-2, ((row+1)*20)-2,
outline='black', fill='red',
tags=('rect')))
delta_x = delta_y = 1
xold,yold = xSTART,ySTART
canv.move(ball, xold, yold)
- 为游戏创建主循环以检测碰撞并处理挡板和球体的移动:
while RUNNING:
objects = canv.find_overlapping(canv.coords(ball)[0],
canv.coords(ball)[1],
canv.coords(ball)[2],
canv.coords(ball)[3])
#Only change the direction once (so will bounce off 1st
# block even if 2 are hit)
dir_changed=False
for obj in objects:
if (obj != ball):
if dir_changed==False:
determineDir(canv.coords(ball),canv.coords(obj))
dir_changed=True
if (obj >= brick[0]) and (obj <= brick[len(brick)-1]):
canv.delete(obj)
if (obj == bottom):
text = canv.create_text(300,100,text="YOU HAVE MISSED!")
canv.coords(ball, (xSTART,ySTART,
xSTART+BALL_SIZE,ySTART+BALL_SIZE))
delta_x = delta_y = 1
canv.update()
time.sleep(3)
canv.delete(text)
new_x, new_y = delta_x, delta_y
canv.move(ball, new_x, new_y)
canv.update()
time.sleep(0.005)
#End
它是如何工作的...
我们创建了一个 640 x 480 像素的 Tkinter 应用程序,并将<Right>
和<Left>
光标键绑定到move_right()
和move_left()
函数。我们使用root.protocol('WM_DELETE_WINDOW', close)
来检测窗口何时关闭,以便我们可以干净地退出程序(通过close()
函数,该函数将RUNNING
设置为False
)。
我们随后将一个用于存放所有对象的 Canvas 小部件添加到应用程序中。我们创建了以下对象:top
、left
、right
和 bottom
。这些构成了我们游戏区域的边界。canvas
坐标在左上角为 0,0
,在右下角为 640,480
,因此可以确定每一边的起始和结束坐标(使用 canv.create_line(xStart, yStart, xEnd, yEnd)
):
Canvas 小部件的坐标
你还可以给对象添加多个 标签
;标签
通常用于定义对象的特定动作或行为。例如,当特定的对象或积木被击中时,它们允许发生不同类型的事件。我们将在下一个示例中看到 标签
的更多用途。
接下来,我们定义球和桨的物体,这些物体是通过使用canv.create_rectangle()
添加的。这需要两组坐标,用于定义图像的对角线(在这种情况下,是左上角和右下角)。
Tkinter 矩形由两个角坐标定义:
Tkinter 矩形示例
最后,我们可以创建砖块了!
我们希望我们的砖块宽度为 40 x 20 像素,这样我们可以在 640 像素的游戏区域(分为四行)中放置 16 块砖。我们可以创建一个带有自动定义位置的砖块对象列表,如下面的代码所示:
brick=list()
for i in range(0,16):
for row in range(0,4):
brick.append(canv.create_rectangle(i*40, row*20,
((i+1)*40)-2, ((row+1)*20)-2, outline='black',
fill='red', tags=('rect')))
通过将砖块略微缩小(-2
)以创建一个小缝隙,从而产生类似砖块的效果:
4 x 16 行块
我们现在将在开始主控制循环之前设置默认设置。球的移动将由delta_x
和delta_y
控制,这些值在每个周期中会加到或从球的当前位置中减去。
接下来,我们设置球的起始位置,并使用canv.move()
函数将球移动相应的距离。move()
函数将100
加到球对象的x
和y
坐标上,该球对象最初是在位置0,0
创建的。
现在一切都已经设置好了,主循环可以运行;这将检查球是否没有碰到任何东西(使用canv.find_overlapping()
函数),对delta_x
或delta_y
值进行任何调整,然后将它们应用到移动球到下一个位置。
delta_x
和 delta_y
的符号决定了球的方向。正值会使球斜向下向右移动,而 -delta_x
将使其向左移动——是向下还是向上取决于 delta_y
是正值还是负值。
球被移动后,我们使用 canv.update()
来重新绘制对显示所做的任何更改,而 time.sleep()
允许在再次检查和移动球之前有一个短暂的延迟。
使用 canv.find_overlapping()
函数来检测对象碰撞。此函数返回一个列表,其中包含被发现在由提供的坐标定义的矩形边界重叠的 canvas
对象。例如,在正方形球体的案例中,是否有任何 canvas
对象的坐标位于球体占据的空间内?请参见以下内容:
检查对象以确定它们是否相互重叠
如果球被发现在与其他物体重叠,例如墙壁、球拍或一块或多块砖,我们需要确定球接下来应该向哪个方向移动。由于我们使用球的坐标作为检查区域,球总是会被列出,因此在我们检查物体列表时,我们可以忽略它们。
我们使用dir_changed
标志来确保在同时击中两个砖块时,我们在移动球之前不会改变方向两次。否则,它会导致球继续沿同一方向移动,即使它已经与砖块发生了碰撞。
因此,如果球体与另一个物体重叠,我们可以使用球体和物体的坐标调用determineDir()
方法来计算出新的方向应该是什么。
当球与物体碰撞时,我们希望球从其上弹开;幸运的是,这很容易模拟,因为我们只需要改变delta_x
或delta_y
的符号,具体取决于我们是否击中了侧面或顶部/底部。如果球击中了另一个物体的底部,这意味着我们之前是向上移动的,现在应该向下移动。然而,我们将在x
轴上继续以相同方向移动(无论是向左还是向右,或者只是向上)。这可以从以下代码中看出:
if (ball[xTOP] == obj[xBTM]) or (ball[xBTM] == obj[xTOP]):
delta_x = -delta_x
determineDir()
函数会查看球体和物体的坐标,并寻找左右 x
坐标或上下 y
坐标的匹配。这足以判断碰撞是在侧面还是顶部/底部,我们可以相应地设置 delta_x
或 delta_y
的符号,如下面的代码所示:
if (obj >= brick[0]) and (obj <= brick[-1]):
canv.delete(obj)
接下来,我们可以通过检查重叠的对象 ID 是否位于第一个和最后一个 ID 砖块之间来确定是否遇到了障碍。如果是砖块,我们可以使用canv.delete()
将其移除。
Python 允许索引值循环,而不是访问无效内存,因此索引值 -1
将为我们提供列表中的最后一个项目。我们用这个来引用最后一个砖块为 brick [-1]
。
我们还会检查被覆盖的对象是否是底线(在这种情况下,玩家用球拍没接到球),因此会短暂显示一条简短信息。我们重置ball
的位置和delta_x
/delta_y
值。canv.update()
函数确保在删除信息(三秒后)之前,显示先被刷新。
最后,球通过delta_x
/delta_y
距离移动,并且显示更新。这里添加了小的延迟以减少更新速率和使用的 CPU 时间。否则,你会发现如果你的树莓派将 100%的精力用于运行程序,它将变得无响应。
当用户按下光标键时,会调用move_right()
和move_left()
函数。它们会检查挡板对象的位置,如果挡板不在边缘,挡板将会相应地移动。如果球击中挡板,碰撞检测将确保球反弹,就像它击中了一块砖块一样。
您可以通过为每个被摧毁的方块添加分数来进一步扩展这个游戏,让玩家拥有有限的生命值,当玩家错过球时生命值会减少,甚至可以编写一些代码来读取新的砖块布局。
创建一个滚屏游戏
通过在我们的程序中使用对象和图像,我们可以创建许多类型的二维图形游戏。
在这个菜谱中,我们将创建一个寻宝游戏,玩家试图找到隐藏的宝藏(通过按下 Enter 键来挖掘)。每次宝藏未被找到时,玩家都会得到一个关于宝藏距离的线索;然后玩家可以使用光标键四处移动并搜索,直到找到宝藏:
在你自己的滚动游戏中挖掘宝藏
尽管这是一个游戏的基本概念,但它可以很容易地扩展,包括多个布局、陷阱和需要避免的敌人,甚至可能还有额外的工具或需要解决的谜题。只需对图形进行一些调整,角色就可以探索地牢、太空船,或者跳跃在云层之间,收集彩虹!
准备就绪
以下示例使用了多个图片;这些图片作为本书资源的一部分提供。您需要将这九张图片放置在与 Python 脚本相同的目录中。
所需的图片文件可以在本章的代码包中查看。
如何做到这一点...
创建以下脚本,scroller.py
:
- 首先导入所需的库和参数:
#!/usr/bin/python3
# scroller.py
import tkinter as TK
import time
import math
from random import randint
STEP=7
xVAL,yVAL=0,1
MAX_WIDTH,MAX_HEIGHT=640,480
SPACE_WIDTH=MAX_WIDTH*2
SPACE_HEIGHT=MAX_HEIGHT*2
LEFT,UP,RIGHT,DOWN=0,1,2,3
SPACE_LIMITS=[0,0,SPACE_WIDTH-MAX_WIDTH,
SPACE_HEIGHT-MAX_HEIGHT]
DIS_LIMITS=[STEP,STEP,MAX_WIDTH-STEP,MAX_HEIGHT-STEP]
BGN_IMG="bg.gif"
PLAYER_IMG=["playerL.gif","playerU.gif",
"playerR.gif","playerD.gif"]
WALL_IMG=["wallH.gif","wallV.gif"]
GOLD_IMG="gold.gif"
MARK_IMG="mark.gif"
newGame=False
checks=list()
- 提供处理玩家移动的功能:
def move_right(event):
movePlayer(RIGHT,STEP)
def move_left(event):
movePlayer(LEFT,-STEP)
def move_up(event):
movePlayer(UP,-STEP)
def move_down(event):
movePlayer(DOWN,STEP)
def foundWall(facing,move):
hitWall=False
olCoords=[canv.coords(player)[xVAL],
canv.coords(player)[yVAL],
canv.coords(player)[xVAL]+PLAYER_SIZE[xVAL],
canv.coords(player)[yVAL]+PLAYER_SIZE[yVAL]]
olCoords[facing]+=move
objects = canv.find_overlapping(olCoords[0],olCoords[1],
olCoords[2],olCoords[3])
for obj in objects:
objTags = canv.gettags(obj)
for tag in objTags:
if tag == "wall":
hitWall=True
return hitWall
def moveBackgnd(movement):
global bg_offset
bg_offset[xVAL]+=movement[xVAL]
bg_offset[yVAL]+=movement[yVAL]
for obj in canv.find_withtag("bg"):
canv.move(obj, -movement[xVAL], -movement[yVAL])
def makeMove(facing,move):
if facing == RIGHT or facing == LEFT:
movement=[move,0] #RIGHT/LEFT
bgOffset=bg_offset[xVAL]
playerPos=canv.coords(player)[xVAL]
else:
movement=[0,move] #UP/DOWN
bgOffset=bg_offset[yVAL]
playerPos=canv.coords(player)[yVAL]
#Check Bottom/Right Corner
if facing == RIGHT or facing == DOWN:
if (playerPos+PLAYER_SIZE[xVAL]) < DIS_LIMITS[facing]:
canv.move(player, movement[xVAL], movement[yVAL])
elif bgOffset < SPACE_LIMITS[facing]:
moveBackgnd(movement)
else:
#Check Top/Left Corner
if (playerPos) > DIS_LIMITS[facing]:
canv.move(player, movement[xVAL], movement[yVAL])
elif bgOffset > SPACE_LIMITS[facing]:
moveBackgnd(movement)
def movePlayer(facing,move):
hitWall=foundWall(facing,move)
if hitWall==False:
makeMove(facing,move)
canv.itemconfig(player,image=playImg[facing])
- 添加功能以检查玩家距离隐藏的金币有多远:
def check(event):
global checks,newGame,text
if newGame:
for chk in checks:
canv.delete(chk)
del checks[:]
canv.delete(gold,text)
newGame=False
hideGold()
else:
checks.append(
canv.create_image(canv.coords(player)[xVAL],
canv.coords(player)[yVAL],
anchor=TK.NW, image=checkImg,
tags=('check','bg')))
distance=measureTo(checks[-1],gold)
if(distance<=0):
canv.itemconfig(gold,state='normal')
canv.itemconfig(check,state='hidden')
text = canv.create_text(300,100,fill="white",
text=("You have found the gold in"+
" %d tries!"%len(checks)))
newGame=True
else:
text = canv.create_text(300,100,fill="white",
text=("You are %d steps away!"%distance))
canv.update()
time.sleep(1)
canv.delete(text)
def measureTo(objectA,objectB):
deltaX=canv.coords(objectA)[xVAL]-
canv.coords(objectB)[xVAL]
deltaY=canv.coords(objectA)[yVAL]-
canv.coords(objectB)[yVAL]
w_sq=abs(deltaX)**2
h_sq=abs(deltaY)**2
hypot=math.sqrt(w_sq+h_sq)
return round((hypot/5)-20,-1)
- 添加功能以帮助找到一个藏匿金子的地点:
def hideGold():
global gold
goldPos=findLocationForGold()
gold=canv.create_image(goldPos[xVAL], goldPos[yVAL],
anchor=TK.NW, image=goldImg,
tags=('gold','bg'),
state='hidden')
def findLocationForGold():
placeGold=False
while(placeGold==False):
goldPos=[randint(0-bg_offset[xVAL],
SPACE_WIDTH-GOLD_SIZE[xVAL]-bg_offset[xVAL]),
randint(0-bg_offset[yVAL],
SPACE_HEIGHT-GOLD_SIZE[yVAL]-bg_offset[yVAL])]
objects = canv.find_overlapping(goldPos[xVAL],
goldPos[yVAL],
goldPos[xVAL]+GOLD_SIZE[xVAL],
goldPos[yVAL]+GOLD_SIZE[yVAL])
findNewPlace=False
for obj in objects:
objTags = canv.gettags(obj)
for tag in objTags:
if (tag == "wall") or (tag == "player"):
findNewPlace=True
if findNewPlace == False:
placeGold=True
return goldPos
- 创建 Tkinter 应用程序窗口并绑定键盘事件:
root = TK.Tk()
root.title("Overhead Game")
root.geometry('%sx%s+%s+%s' %(MAX_WIDTH,
MAX_HEIGHT,
100, 100))
root.resizable(width=TK.FALSE, height=TK.FALSE)
root.bind('<Right>', move_right)
root.bind('<Left>', move_left)
root.bind('<Up>', move_up)
root.bind('<Down>', move_down)
root.bind('<Return>', check)
canv = TK.Canvas(root, highlightthickness=0)
canv.place(x=0,y=0,width=SPACE_WIDTH,height=SPACE_HEIGHT)
-
初始化所有游戏对象(背景瓷砖、玩家、墙壁等)
the gold):
#Create background tiles
bgnImg = TK.PhotoImage(file=BGN_IMG)
BGN_SIZE = bgnImg.width(),bgnImg.height()
background=list()
COLS=int(SPACE_WIDTH/BGN_SIZE[xVAL])+1
ROWS=int(SPACE_HEIGHT/BGN_SIZE[yVAL])+1
for col in range(0,COLS):
for row in range(0,ROWS):
background.append(canv.create_image(col*BGN_SIZE[xVAL],
row*BGN_SIZE[yVAL], anchor=TK.NW,
image=bgnImg,
tags=('background','bg')))
bg_offset=[0,0]
#Create player
playImg=list()
for img in PLAYER_IMG:
playImg.append(TK.PhotoImage(file=img))
#Assume images are all same size/shape
PLAYER_SIZE=playImg[RIGHT].width(),playImg[RIGHT].height()
player = canv.create_image(100,100, anchor=TK.NW,
image=playImg[RIGHT],
tags=('player'))
#Create walls
wallImg=[TK.PhotoImage(file=WALL_IMG[0]),
TK.PhotoImage(file=WALL_IMG[1])]
WALL_SIZE=[wallImg[0].width(),wallImg[0].height()]
wallPosH=[(0,WALL_SIZE[xVAL]*1.5),
(WALL_SIZE[xVAL],WALL_SIZE[xVAL]*1.5),
(SPACE_WIDTH-WALL_SIZE[xVAL],WALL_SIZE[xVAL]*1.5),
(WALL_SIZE[xVAL],SPACE_HEIGHT-WALL_SIZE[yVAL])]
wallPosV=[(WALL_SIZE[xVAL],0),(WALL_SIZE[xVAL]*3,0)]
wallPos=[wallPosH,wallPosV]
wall=list()
for i,img in enumerate(WALL_IMG):
for item in wallPos[i]:
wall.append(canv.create_image(item[xVAL],item[yVAL],
anchor=TK.NW, image=wallImg[i],
tags=('wall','bg')))
#Place gold
goldImg = TK.PhotoImage(file=GOLD_IMG)
GOLD_SIZE=[goldImg.width(),goldImg.height()]
hideGold()
#Check mark
checkImg = TK.PhotoImage(file=MARK_IMG)
- 最后,启动
mainloop()
命令以允许 Tkinter 监视事件:
#Wait for actions from user
root.mainloop()
#End
它是如何工作的...
如前所述,我们创建了一个新的 Tkinter 应用程序,其中包含一个 Canvas
小部件,这样我们就可以添加所有的游戏对象。我们确保我们绑定了右键、左键、上键、下键和 Enter 键,这些将在游戏中作为我们的控制键。
首先,我们将背景图片(bg.gif
)放置到Canvas
小部件上。我们计算可以沿长度和宽度方向容纳的图片数量,以铺满整个画布空间,并使用合适的坐标定位它们。
接下来,我们创建玩家图像(通过创建playImg
,一个包含玩家可以转向的每个方向的 Tkinter 图像对象的列表)并将其放置在画布上。
我们现在创建墙壁,其位置由wallPosH
和wallPosV
列表定义。这些可以通过精确坐标来定义,也许甚至可以从文件中读取,以提供一个简单的方法来加载不同级别的布局,如果需要的话。通过遍历列表,水平墙壁和垂直墙壁的图像被放置在画布上。
要完成布局,我们只需将金币隐藏在某个地方即可。使用hideGold()
函数,我们可以随机确定一个合适的位置来放置金币。在findLocationForGold()
函数中,我们使用randint(0,value)
来生成一个介于0
和value
之间的伪随机数(它不是完全随机的,但对于这个用途已经足够好了)。在我们的例子中,我们想要的值介于0
和画布空间的边缘之间,减去金币图像的大小以及可能已经应用到画布上的任何bg_offset
。这确保了它不会超出屏幕边缘。然后我们检查潜在的位置,使用find_overlapping()
函数来查看是否有带有wall
或player
标签的对象挡在路上。如果是这样,我们选择一个新的位置。否则,我们在画布上放置金币,但使用state="hidden"
值,这将使其从视图中隐藏。
我们随后创建checkImg
(一个 Tkinter 图像),在检查黄金时使用它来标记我们已检查的区域。最后,我们只需等待用户按下其中一个键。
当按下任意一个光标键时,角色将在屏幕上移动。玩家的移动由movePlayer()
函数决定;它首先会检查玩家是否试图移动到墙壁上,然后(在makeMove()
函数内)确定玩家是否位于显示或画布空间的边缘。
每当按下光标键时,我们使用图中所示的逻辑来确定要执行的操作:
光标键按下动作逻辑
foundWall()
函数通过检查玩家图像覆盖区域内的任何带有 wall
标签的对象,以及玩家将要移动到的区域的一点点额外空间,来确定玩家是否会撞到墙壁。以下图表展示了如何确定 olCoords
坐标:
olCoords 坐标确定
makeMove()
函数检查玩家是否将移动到显示器的边缘(由 DIS_LIMITS
定义),以及他们是否位于画布空间的边缘(由 SPACE_LIMITS
定义)。在显示器内,玩家可以朝向光标的方向移动,或者画布空间内所有标记为 bg
的对象都朝相反方向移动,模拟玩家背后的滚动。这是通过 moveBackground()
函数实现的。
当玩家按下 Enter 键时,我们希望检查当前位置是否有金子。使用 measureTo()
函数,比较玩家的位置和金子的位置(计算每个的 x
和 y
坐标之间的距离,如下所示图所示):
玩家和金币距离计算
结果被缩放以提供一个大致的指示,说明玩家距离金子有多远。如果距离大于零,我们显示玩家距离金子的距离,并留下一个十字来表示我们检查过的地方。如果玩家找到了金子,我们会显示一条消息说明这一点,并将newGame
设置为True
。下次玩家按下Enter键时,带有十字标记的地方将被移除,金子将被重新放置到新的位置。
再次隐藏了金子,玩家准备重新开始!
第六章:在图像中检测边缘和轮廓
本章介绍了以下食谱:
-
加载、显示和保存图片
-
图像翻转和缩放
-
侵蚀和膨胀
-
图像分割
-
图像模糊和锐化
-
图像边缘检测
-
直方图均衡化
-
在图像中检测角点
简介
图像处理在几乎所有工程和医学应用中都发挥着至关重要的作用,用于从灰度/彩色图像中提取和评估感兴趣的区域。图像处理方法包括预处理、特征提取和分类。预处理用于提高图像质量;这包括自适应阈值、对比度增强、直方图均衡化和边缘检测。特征提取技术用于从图像中提取显著特征,这些特征可以随后用于分类。
在菜谱中介绍了构建图像预处理方案的步骤。
加载、显示和保存图片
本节介绍了如何通过 OpenCV-Python 处理图像。此外,我们还讨论了如何加载、显示和保存图像。
如何做到这一点...
- 导入计算机视觉包 -
cv2
:
import cv2
- 使用内置的
imread
函数读取图像:
image = cv2.imread('image_1.jpg')
- 使用内置的
imshow
函数显示原始图像:
cv2.imshow("Original", image)
- 等待直到按下任意键:
cv2.waitKey(0)
- 使用内置的
imwrite
函数保存图片:
cv2.imwrite("Saved Image.jpg", image)
- 执行 Python 程序
Load_Display_Save.py
所使用的命令如下:
- 执行
Load_Display_Save.py
后得到的结果如下所示:
图像翻转
在图像翻转操作中,我们可以水平翻转、垂直翻转、水平翻转和垂直翻转输入图像。
如何做到这一点...
- 导入计算机视觉包 -
cv2
:
import cv2
- 使用内置的
imread
函数读取图像:
image = cv2.imread('image_2.jpg')
- 使用内置的
imshow
函数显示原始图像:
cv2.imshow("Original", image)
- 等待直到按下任意键:
cv2.waitKey(0)
- 在测试图像上执行所需的操作:
# cv2.flip is used to flip images
# Horizontal flipping of images using value '1'
flipping = cv2.flip(image, 1)
- 显示水平翻转的图像:
# Display horizontally flipped image
cv2.imshow("Horizontal Flipping", flipping)
- 等待直到按下任意键:
cv2.waitKey(0)
- 对输入图像进行垂直翻转:
# Vertical flipping of images using value '0'
flipping = cv2.flip(image, 0)
- 显示垂直翻转的图像:
cv2.imshow("Vertical Flipping", flipping)
- 等待直到按下任意键:
cv2.waitKey(0)
- 显示处理后的图像:
# Horizontal & Vertical flipping of images using value '-1'
flipping = cv2.flip(image, -1)
# Display horizontally & vertically flipped image
cv2.imshow("Horizontal & Vertical Flipping", flipping)
# Wait until any key is pressed
cv2.waitKey(0)
- 停止执行并显示结果:
# Close all windows
cv2.destroyAllWindows()
- 执行
Flipping.py
Python 程序所使用的命令如下:
- 执行
Flipping.py
后获得的原始图像和水平翻转图像如下所示:
以下是水平翻转的图片:
- 垂直和水平翻转的图像,在执行
Flipping.py
后获得,如下所示:
沿着水平和垂直翻转的图片:
图像缩放
图像缩放用于根据需求修改输入图像的尺寸。在 OpenCV 中,通常使用三种类型的缩放算子,它们是立方插值、面积插值和线性插值。
如何做到这一点...
- 创建一个新的 Python 文件并导入以下包:
# Scaling (Resizing) Images - Cubic, Area, Linear Interpolations
# Interpolation is a method of estimating values between known data points
# Import Computer Vision package - cv2
import cv2
# Import Numerical Python package - numpy as np
import numpy as np
- 使用内置的
imread
函数读取图像:
image = cv2.imread('image_3.jpg')
- 使用内置的
imshow
函数显示原始图像:
cv2.imshow("Original", image)
- 等待直到按下任意键:
cv2.waitKey()
- 根据操作员的指令调整图像大小:
# cv2.resize(image, output image size, x scale, y scale, interpolation)
- 使用三次插值调整图像大小:
# Scaling using cubic interpolation
scaling_cubic = cv2.resize(image, None, fx=.75, fy=.75, interpolation = cv2.INTER_CUBIC)
- 显示输出图像:
# Display cubic interpolated image
cv2.imshow('Cubic Interpolated', scaling_cubic)
- 等待直到按下任意键:
cv2.waitKey()
- 使用区域插值调整图像大小:
# Scaling using area interpolation
scaling_skewed = cv2.resize(image, (600, 300), interpolation = cv2.INTER_AREA)
- 显示输出图像:
# Display area interpolated image
cv2.imshow('Area Interpolated', scaling_skewed)
- 等待操作员的指令:
# Wait until any key is pressed
cv2.waitKey()
- 使用线性插值调整图像大小:
# Scaling using linear interpolation
scaling_linear = cv2.resize(image, None, fx=0.5, fy=0.5, interpolation = cv2.INTER_LINEAR)
- 显示输出图像:
# Display linear interpolated image
cv2.imshow('Linear Interpolated', scaling_linear)
- 等待直到按下任意键:
cv2.waitKey()
- 完成图像缩放任务后,终止程序执行:
# Close all windows
cv2.destroyAllWindows()
- 执行
Scaling.py
Python 程序的命令如下所示:
- 用于缩放的原始图像如下所示:
- 执行
Scaling.py
文件后得到的线性插值输出如下所示:
- 执行
Scaling.py
文件后得到的面积插值输出如下所示:
- 执行
Scaling.py
文件后得到的立方插值输出如下所示:
侵蚀与膨胀
腐蚀和膨胀是形态学操作。腐蚀移除图像中物体边界的像素,而膨胀则向图像中物体边界添加像素。
如何做到这一点...
- 导入计算机视觉包 –
cv2
:
import cv2
- 导入数值 Python 包 –
numpy as np
:
import numpy as np
- 使用内置的
imread
函数读取图像:
image = cv2.imread('image_4.jpg')
- 使用内置的
imshow
函数显示原始图像:
cv2.imshow("Original", image)
- 等待直到按下任意键:
cv2.waitKey(0)
- 给定形状和类型,用 1 填充它:
# np.ones(shape, dtype)
# 5 x 5 is the dimension of the kernel, uint8: is an unsigned integer (0 to 255)
kernel = np.ones((5,5), dtype = "uint8")
cv2.erode
是用于腐蚀的内置函数:
# cv2.erode(image, kernel, iterations)
erosion = cv2.erode(image, kernel, iterations = 1)
- 使用内置的
imshow
函数显示腐蚀后的图像:
cv2.imshow("Erosion", erosion)
- 等待直到按下任意键:
cv2.waitKey(0)
cv2.dilate
是用于膨胀操作的内置函数:
# cv2.dilate(image, kernel, iterations)
dilation = cv2.dilate(image, kernel, iterations = 1)
- 使用内置的
imshow
函数显示放缩后的图像:
cv2.imshow("Dilation", dilation)
- 等待直到按下任意键:
cv2.waitKey(0)
- 关闭所有窗口:
cv2.destroyAllWindows()
- 执行
Erosion_Dilation.py
文件所使用的命令如下:
- 用于执行
Erosion_Dilation.py
文件输入的图像如下所示:
- 执行
Erosion_Dilation.py
文件后获得的侵蚀图像如下所示:
- 执行
Erosion_Dilation.py
文件后得到的膨胀图像如下所示:
图像分割
分割是将图像划分为不同区域的过程。轮廓是围绕物体边界的线条或曲线。本节讨论了使用轮廓进行图像分割的内容。
如何做到这一点...
- 导入计算机视觉包 -
cv2
:
import cv2
# Import Numerical Python package - numpy as np
import numpy as np
- 使用内置的
imread
函数读取图像:
image = cv2.imread('image_5.jpg')
- 使用内置的
imshow
函数显示原始图像:
cv2.imshow("Original", image)
- 等待直到按下任意键:
cv2.waitKey(0)
- 执行
Canny
边缘检测系统:
# cv2.Canny is the built-in function used to detect edges
# cv2.Canny(image, threshold_1, threshold_2)
canny = cv2.Canny(image, 50, 200)
- 使用内置的
imshow
函数显示边缘检测输出图像:
cv2.imshow("Canny Edge Detection", canny)
- 等待直到按下任意键:
cv2.waitKey(0)
- 执行轮廓检测系统:
# cv2.findContours is the built-in function to find contours
# cv2.findContours(canny, contour retrieval mode, contour approximation mode)
# contour retrieval mode: cv2.RETR_LIST (retrieves all contours)
# contour approximation mode: cv2.CHAIN_APPROX_NONE (stores all boundary points)
contours, hierarchy = cv2.findContours(canny, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
- 在图像上绘制轮廓:
# cv2.drawContours is the built-in function to draw contours
# cv2.drawContours(image, contours, index of contours, color, thickness)
cv2.drawContours(image, contours, -1, (255,0,0), 10)
# index of contours = -1 will draw all the contours
- 显示图像的草图轮廓:
# Display contours using imshow built-in function
cv2.imshow("Contours", image)
- 等待直到按下任意键:
cv2.waitKey()
- 终止程序并显示结果:
# Close all windows
cv2.destroyAllWindows()
- 执行
Image_Segmentation.py
文件后得到的结果如下所示:
以下是边缘检测的输出:
模糊和锐化图像
模糊和锐化是用于增强输入图像的图像处理操作。
如何做到这一点...
- 导入计算机视觉包 -
cv2
:
import cv2
# Import Numerical Python package - numpy as np
import numpy as np
- 使用内置的
imread
函数读取图像:
image = cv2.imread('image_6.jpg')
- 使用内置的
imshow
函数显示原始图像:
cv2.imshow("Original", image)
- 等待直到按下任意键:
cv2.waitKey(0)
- 使用模糊操作执行像素级动作:
# Blurring images: Averaging, cv2.blur built-in function
# Averaging: Convolving image with normalized box filter
# Convolution: Mathematical operation on 2 functions which produces third function.
# Normalized box filter having size 3 x 3 would be:
# (1/9) [[1, 1, 1],
# [1, 1, 1],
# [1, 1, 1]]
blur = cv2.blur(image,(9,9)) # (9 x 9) filter is used
- 显示模糊图像:
cv2.imshow('Blurred', blur)
- 等待直到按下任意键:
cv2.waitKey(0)
- 使用锐化操作执行像素级动作:
# Sharpening images: Emphasizes edges in an image
kernel = np.array([[-1,-1,-1],
[-1,9,-1],
[-1,-1,-1]])
# If we don't normalize to 1, image would be brighter or darker respectively
# cv2.filter2D is the built-in function used for sharpening images
# cv2.filter2D(image, ddepth, kernel)
# ddepth = -1, sharpened images will have same depth as original image
sharpened = cv2.filter2D(image, -1, kernel)
- 显示锐化的图像:
cv2.imshow('Sharpened', sharpened)
- 等待直到按下任意键:
cv2.waitKey(0)
- 终止程序执行:
# Close all windows
cv2.destroyAllWindows()
- 执行
Blurring_Sharpening.py
Python 程序文件的命令如下所示:
- 用于执行
Blurring_Sharpening.py
文件输入的图像如下所示:
- 执行
Blurring_Sharpening.py
文件后得到的模糊图像如下所示:
- 执行
Blurring_Sharpening.py
文件后得到的锐化图像如下所示:
图像边缘检测
边缘检测用于检测图像中的边界。它提供了关于形状和区域特性的详细信息。这包括周长、主轴尺寸和副轴尺寸。
如何操作...
- 导入必要的包:
import sys
import cv2
import numpy as np
- 读取输入图像:
in_file = sys.argv[1]
image = cv2.imread(in_file, cv2.IMREAD_GRAYSCALE)
- 实现 Sobel 边缘检测方案:
horizontal_sobel = cv2.Sobel(image, cv2.CV_64F, 1, 0, ksize=5)
vertical_sobel = cv2.Sobel(image, cv2.CV_64F, 0, 1, ksize=5)
laplacian_img = cv2.Laplacian(image, cv2.CV_64F)
canny_img = cv2.Canny(image, 30, 200)
- 显示输入图像及其对应输出:
cv2.imshow('Original', image)
cv2.imshow('horizontal Sobel', horizontal_sobel)
cv2.imshow('vertical Sobel', vertical_sobel)
cv2.imshow('Laplacian image', laplacian_img)
cv2.imshow('Canny image', canny_img)
- 等待操作员的指令:
cv2.waitKey()
- 显示输入图像及其对应的结果:
cv2.imshow('Original', image)
cv2.imshow('horizontal Sobel', horizontal_sobel)
cv2.imshow('vertical Sobel', vertical_sobel)
cv2.imshow('Laplacian image', laplacian_img)
cv2.imshow('Canny image', canny_img)
- 等待操作员的指令:
cv2.waitKey()
- 执行
Detecting_edges.py
Python 程序文件以及输入图像(baby.jpg
)所使用的命令如下所示:
- 执行
Detecting_edges.py
文件后得到的输入图像和水平 Sobel 滤波器输出如下所示:
- 执行
Detecting_edges.py
文件后得到的垂直 Sobel 滤波器输出和拉普拉斯图像输出如下所示:
以下为拉普拉斯图像输出:
- 执行
Detecting_edges.py
文件后得到的Canny
边缘检测输出如下所示:
它是如何工作的...
读者可以参考以下文档来了解边缘检测及其对测试图像的影响:
citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.301.927
参见
请参阅以下文档:
直方图均衡化
直方图均衡化用于增强图像的可见性和对比度。它通过改变图像强度来实现。这些过程在这里有清晰的描述。
如何操作……
- 导入必要的包:
import sys
import cv2
import numpy as np
- 加载输入图像:
in_file = sys.argv[1]
image = cv2.imread(in_file)
- 将 RGB 图像转换为灰度图:
image_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
cv2.imshow('Input grayscale image', image_gray)
- 调整灰度图像的直方图:
image_gray_histoeq = cv2.equalizeHist(image_gray)
cv2.imshow('Histogram equalized - grayscale image', image_gray_histoeq)
- 调整 RGB 图像的直方图:
image_yuv = cv2.cvtColor(image, cv2.COLOR_BGR2YUV)
image_yuv[:,:,0] = cv2.equalizeHist(image_yuv[:,:,0])
image_histoeq = cv2.cvtColor(image_yuv, cv2.COLOR_YUV2BGR)
- 显示输出图像:
cv2.imshow('Input image', image)
cv2.imshow('Histogram equalized - color image', image_histoeq)
cv2.waitKey()
- 执行
histogram.py
Python 程序文件的命令,以及输入图像(finger.jpg
),如下所示:
- 用于执行
histogram.py
文件输入的图像如下所示:
- 执行
histogram.py
文件后得到的直方图均衡化灰度图像如下所示:
- 执行
histogram.py
文件后得到的直方图均衡化彩色图像如下所示:
在图像中检测角点
角点是用于提取图像中特殊特征的边界,这些特征可以推断出图像的内容。角点检测常用于图像配准、视频跟踪、图像拼接、运动检测、3D 建模、全景拼接和物体识别。
如何操作...
- 导入必要的包:
import sys
import cv2
import numpy as np
- 加载输入图像:
in_file = sys.argv[1]
image = cv2.imread(in_file)
cv2.imshow('Input image', image)
image_gray1 = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
image_gray2 = np.float32(image_gray1)
- 实现哈里斯角点检测方案:
image_harris1 = cv2.cornerHarris(image_gray2, 7, 5, 0.04)
- 放大输入图像并构建角点:
image_harris2 = cv2.dilate(image_harris1, None)
- 实现图像阈值化:
image[image_harris2 > 0.01 * image_harris2.max()] = [0, 0, 0]
- 显示输入图像:
cv2.imshow('Harris Corners', image)
- 等待操作员的指令:
cv2.waitKey()
- 执行
Detecting_corner.py
Python 程序文件以及输入图像(box.jpg
)所使用的命令如下所示:
- 用于执行
Detecting_corner.py
文件的输入图像如下所示:
- 执行
Detecting_corner.py
文件后获得的 Harris 角点如下所示:
为了了解输入图像的工作原理,请参考以下内容:
- 图像角点检测涉及在给定图片中寻找边缘/角点。它可以用于从灰度和 RGB 图片中提取关键形状特征。请参阅关于边缘和角点检测的这篇综述论文:
pdfs.semanticscholar.org/24dd/6c2c08f5601e140aad5b9170e0c7485f6648.pdf
.
第七章:创建 3D 图形
在本章中,我们将涵盖以下主题:
-
开始学习 3D 坐标和顶点
-
创建和导入 3D 模型
-
创建一个可探索的 3D 世界
-
构建三维地图和迷宫
简介
原始树莓派(搭载一款博通 BCM2835 处理器)的核心芯片最初是为了作为移动和嵌入式应用的图形处理单元(GPU)而设计的。由于芯片上还有额外的空间,因此添加了驱动树莓派大部分功能的 ARM 核心;这使得这个强大的 GPU 能够作为片上系统(SoC)解决方案使用。
一个 SoC 是一个集成了所有必要的电子电路和计算机或电子系统部件的集成服务微芯片,它用于智能手机或可穿戴计算机,集成在一个集成电路(IC)上。
如您所想,如果那个原始的 ARM 核心(ARM1176JZF-S,它是ARMv6架构)仅占 Raspberry Pi 芯片的一小部分,那么您认为 GPU 必须表现相当出色是正确的。
Raspberry Pi 3 的核心处理器已经升级(升级到了Broadcom BCM2837处理器);它现在包含四个 ARM 核心(Cortex A53 ARMv8A),每个都比原始的ARMv6更强大。与上一代相同的 GPU 相结合,Raspberry Pi 3 在执行构建 3D 环境所需的计算方面装备得更加完善。然而,尽管 Raspberry Pi 3 将更快地加载示例,但一旦生成 3D 模型,两种芯片版本的表现都相当出色。
VideoCore IV GPU由 48 个专门设计的处理器组成,其中一些提供对 1080p 高清视频编码和解码的支持,而另一些则支持OpenGL ES 2.0,这为 3D 图形提供了快速的运算。据说其图形处理能力相当于苹果 iPhone 4S 和原始的微软 Xbox。如果你在树莓派上运行Quake 3或OpenArena,这一点会更加明显(详情请访问www.raspberrypi.org/openarena-for-raspberry-pi
)。
在本章中,我希望向大家展示,虽然你可以通过使用树莓派的 ARM 端执行操作来取得很多成就,但如果你冒险进入隐藏 GPU 的那一侧,你可能会发现这个小巧的电脑比最初看起来还有更多潜力。
由pi3d
团队(帕特里克·冈特、汤姆·斯威利、蒂姆·斯凯尔曼等人)创建的pi3d
库提供了一种通过创建 3D 图形来利用 GPU 的方法。
pi3d
维基和文档页面可以在以下链接找到:pi3d.github.io/html/index.html.
支持和开发小组可以在以下链接找到:groups.google.com/forum/#!forum/pi3d.
该库包含许多功能,因此在下述示例中不可能涵盖所有可用内容。建议您也花些时间尝试一下pi3d
演示。要发现更多用于创建和处理 3D 图形的选项,您可以查看构成库本身的某些 Python 模块(在文档或 GitHub 上的代码中描述,网址为github.com/pi3d/pi3d.github.com
)。希望这一章能向您介绍足够的概念,以展示您可利用的一些原始潜力。
开始学习 3D 坐标和顶点
我们周围的世界是三维的,因此为了模拟世界的一部分,我们可以创建一个 3D 表示并在我们的 2D 屏幕上显示它。
树莓派使我们能够模拟一个 3D 空间,在其中放置 3D 对象,并从选定的视角观察它们。我们将使用 GPU 将 3D 视图表示为 2D 图像,以便在屏幕上显示。
以下示例将向您展示我们如何使用pi3d
(一个适用于 Raspberry Pi 的 OpenGL ES 库)来放置一个单独的 3D 对象并在 3D 空间中显示它。然后我们将允许鼠标围绕对象旋转视图:
单个 3D 对象
准备中
Raspberry Pi 必须直接连接到显示器,无论是通过 HDMI 还是模拟视频输出。GPU 渲染的 3D 图形将仅在本地显示器上显示,即使您通过网络远程连接到 Raspberry Pi。您还需要使用本地连接的鼠标进行控制(然而,通过 SSH 连接键盘控制也是可行的)。
第一次使用 pi3d
,我们需要按照以下步骤下载和安装它:
-
pi3d
库使用 Pillow,这是与 Python 3 兼容的 Python 图像库版本,用于导入模型中使用的图形(例如纹理)以及背景)。
Pillow 的安装已在第三章的“准备就绪”部分中介绍,即使用 Python 进行自动化和生产效率提升。
安装命令显示在下面的代码中(如果您之前已经安装过,它将跳过它们并继续):
sudo apt-get update
sudo apt-get install python3-pip
sudo apt-get install libjpeg-dev
sudo pip-3.2 install pillow
- 我们现在可以使用
pip
通过以下命令安装pi3d
:
sudo pip-3.2 install pi3d
pi3d
团队正在持续开发和改进这个库;如果你遇到问题,可能意味着新版本与之前的版本不兼容。
您也可以在附录中的“硬件和软件列表”中查看确认您所使用的pi3d
版本,并在需要的情况下安装列表中列出的相同版本。或者,您可以通过 Google 群组联系pi3d
团队;他们将乐意提供帮助!
从 GitHub 网站获取pi3d
演示,如下命令行所示。您需要大约 90 MB 的空闲空间来下载和解压文件:
cd ~
wget https://github.com/pi3d/pi3d_demos/archive/master.zip
unzip master.zip
rm master.zip
你会发现演示已经解压到 pi3d_demos-master
。
默认情况下,演示文件预期位于 home/pi/pi3d
;因此,我们将重命名此目录为 pi3d
,如下命令所示:
mv pi3d_demos-master pi3d
-
最后,检查 Raspberry Pi 的内存分割。运行
raspi-config
(sudo raspi-config
) 并确保您的内存分割设置为 128 MB。(如果您之前更改过,则只需这样做,因为 128 MB 是默认值。)这确保您为 GPU 分配了足够的 RAM,以便在需要时能够处理大量的 3D 对象。 -
测试一切是否运行正常。你现在应该能够运行
pi3d_demos-master
目录中的任何脚本。有关它们如何工作的详细信息,请参阅pi3d
维基页面(pi3d.github.io/html/ReadMe.html
)。为了获得最佳性能,建议从命令提示符(不加载桌面)运行脚本:
cd pi3d
python3 Raspberry_Rain.py
pi3d.Keyboard
对象也支持通过 SSH 进行键盘控制(参见第一章,使用 Raspberry Pi 3 计算机入门中的通过 SSH 远程连接到 Raspberry Pi 并使用 X11 转发部分)。
配置您自己的脚本设置。由于我们将使用一些来自演示的纹理和模型,建议您在pi3d
目录内创建您的脚本。如果您有一个与默认 Pi 账户不同的用户名,您将需要调整/pi3d/demo.py
文件。通过编辑文件,将USERNAME
部分替换为您自己的用户名:
nano ~/pi3d/demo.py
import sys
sys.path.insert(1, '/home/USERNAME/pi3d')
如果你想要将文件移动到其他位置,请确保在包含所需资源文件正确路径的文件夹中添加demo.py
的副本。
如何做到这一点...
创建以下 3dObject.py
脚本:
#!/usr/bin/python3
""" Create a 3D space with a Tetrahedron inside and rotate the
view around using the mouse.
"""
from math import sin, cos, radians
import demo
import pi3d
KEY = {'ESC':27,'NONE':-1}
DISPLAY = pi3d.Display.create(x=50, y=50)
#capture mouse and key presses
mykeys = pi3d.Keyboard()
mymouse = pi3d.Mouse(restrict = False)
mymouse.start()
def main():
CAMERA = pi3d.Camera.instance()
tex = pi3d.Texture("textures/stripwood.jpg")
flatsh = pi3d.Shader("uv_flat")
#Define the coordinates for our shape (x,y,z)
A = (-1.0,-1.0,-1.0)
B = (1.0,-1.0,1.0)
C = (-1.0,-1.0,1.0)
D = (-1.0,1.0,1.0)
ids = ["A","B","C","D"]
coords = [A,B,C,D]
myTetra = pi3d.Tetrahedron(x=0.0, y=0.0, z=0.0,
corners=(A,B,C,D))
myTetra.set_draw_details(flatsh,[tex])
# Load ttf font and set the font to black
arialFont = pi3d.Font("fonts/FreeMonoBoldOblique.ttf",
"#000000")
mystring = []
#Create string objects to show the coordinates
for i,pos in enumerate(coords):
mystring.append(pi3d.String(font=arialFont,
string=ids[i]+str(pos),
x=pos[0], y=pos[1],z=pos[2]))
mystring.append(pi3d.String(font=arialFont,
string=ids[i]+str(pos),
x=pos[0], y=pos[1],z=pos[2], ry=180))
for string in mystring:
string.set_shader(flatsh)
camRad = 4.0 # radius of camera position
rot = 0.0 # rotation of camera
tilt = 0.0 # tilt of camera
k = KEY['NONE']
omx, omy = mymouse.position()
# main display loop
while DISPLAY.loop_running() and not k == KEY['ESC']:
k = mykeys.read()
mx, my = mymouse.position()
rot -= (mx-omx)*0.8
tilt += (my-omy)*0.8
omx = mx
omy = my
CAMERA.reset()
CAMERA.rotate(-tilt, rot, 0)
CAMERA.position((camRad * sin(radians(rot)) *
cos(radians(tilt)),
camRad * sin(radians(tilt)),
-camRad * cos(radians(rot)) *
cos(radians(tilt))))
#Draw the Tetrahedron
myTetra.draw()
for string in mystring:
string.draw()
try:
main()
finally:
mykeys.close()
mymouse.stop()
DISPLAY.destroy()
print("Closed Everything. END")
#End
运行脚本,请使用 python3 3dObject.py
。
它是如何工作的...
我们导入math
模块(用于计算角度,以便根据鼠标移动来控制视图)。我们还导入了 demo 模块,它仅提供此示例中着色器和纹理的路径。
我们首先定义一些关键元素,这些元素将由 pi3d
用于生成和显示我们的对象。我们将放置对象的空间是 pi3d.Display
对象;这定义了空间的大小,并初始化屏幕以生成和显示 OpenGL ES 图形。
接下来,我们定义一个pi3d.Camera
对象,这将允许我们定义如何查看
在我们的空间内的对象。为了渲染我们的对象,我们定义了一个要应用到表面的纹理和一个将纹理应用到对象上的着色器。着色器用于将所有效果和光照应用到对象上,并且它被编码为使用 GPU 的 OpenGL ES 核心而不是 ARM 处理器。
我们使用 pi3d.keyboard()
和 pi3d.mouse()
定义了 keyboard
和 mouse
对象,以便我们可以响应键盘和鼠标输入。mouse
对象的 restrict
标志允许鼠标的绝对位置超出屏幕限制(这样我们就可以连续旋转我们的 3D 对象)。当主循环运行时,会检查是否按下了 Esc 键,然后关闭所有内容(包括调用 DISPLAY.destroy()
释放屏幕)。我们使用 try: finally:
方法来确保正确关闭显示,即使在 main()
中发生异常也是如此。
鼠标移动是通过mymouse.position()
在主显示循环中收集的,该函数返回x和y坐标。x和y移动的差值用于围绕对象旋转。
鼠标移动决定了摄像机的位置和角度。对鼠标前后位置的任何调整都用于将其移动到物体上方或下方,并改变摄像机的角度(使用倾斜
),使其始终指向物体。同样,任何侧向移动都将使用CAMERA.reset()
函数使摄像机围绕物体移动。这确保了显示更新摄像机视图以显示新位置,CAMERA.rotate()
用于改变角度,并使用CAMERA.position()
将摄像机移动到距离物体中心camRad
单位的某个位置。
我们将绘制一个称为四面体的三维物体,它是由四个三角形组成的形状,形成一个底面为三角形的金字塔。该形状的四个角(三个在底部周围,一个在顶部)将由三维坐标A、B、C和D定义,如下所示图所示:
将四面体放置在三维坐标系中
pi3d.Tetrahedron
对象通过指定四个点的 x、y 和 z 坐标来定位其在空间中的位置,然后指定将连接形成构成该形状的四个三角形的角。
使用 set_draw_details(flatsh,[text])
,我们应用我们希望使用的着色器(们)和对象的纹理(们)。在我们的例子中,我们只使用了一个纹理,但某些着色器可以使用多个纹理以产生复杂效果。
为了帮助突出坐标的位置,我们将通过设置字符串文本来指定 ID 和坐标,并在它们旁边放置一些pi3d.String
对象,将其放置在所需的位置。对于每个位置,我们将创建两个字符串对象,一个朝前,另一个朝后(ry=180
在Y轴上旋转对象 180 度)。pi3d.String
对象是单面的,所以如果我们只有一面朝前,当视图旋转时,从后面看就不会可见,而且会直接消失(此外,如果可见,文本也会是反的)。再次强调,我们使用flatsh
着色器通过set_shader()
字符串对象来渲染它。
现在剩下的工作就是绘制我们的四面体和绳子对象,同时检查任何键盘事件。每次while
循环完成时,都会调用DISPLAY.loop_running()
,这将根据需要更新显示,调整相机设置。
还有更多...
除了介绍如何在三维空间中绘制基本对象之外,前面的示例还使用了以下在三维图形编程中使用的四个关键元素。
相机
照相机代表我们在三维空间中的视角;探索和查看更多空间的一种方法是通过移动照相机。Camera
类定义如下:
pi3d.Camera.Camera(at=(0, 0, 0), eye=(0, 0, -0.1),
lens=None, is_3d=True, scale=1.0)
摄像机通过提供两个位置来定义,一个是要看的对象(通常是我们希望看到的对象,由at
定义),另一个是观察的位置(对象的方位,由eye
定义)。摄像机的其他特性,如其视野(lens
)等,可以通过默认设置进行调整或使用。
如果我们没有在我们的显示中定义相机,系统将创建一个默认相机,该相机指向原点(即显示的中心,也就是0,0,0
),并稍微位于其前方(0,0,-0.1
)。
查看有关相机模块的pi3d
文档以获取更多详细信息。
着色器
着色器非常有用,因为它们允许将将纹理和光照应用到物体上的复杂工作卸载到更强大的 Raspberry Pi GPU 上。Shader
类定义如下:
class pi3d.Shader.Shader(shfile=None, vshader_source=None,
fshader_source=None)
这允许你在文件中指定一个着色器文件(shfile
)以及所需的特定顶点和片段着色器(如果需要的话)。
pi3d
库中包含几个着色器,其中一些允许使用多个纹理来实现反射、近距离细节和透明效果。着色器的实现方式将决定光线和纹理如何应用于对象(在某些情况下,例如 uv_flat
,着色器将忽略任何光照效果)。
着色器文件列在 pi3dshaders
目录中。尝试使用不同的着色器进行实验,例如 mat_reflect
,它将忽略纹理/字体但仍然应用光照效果,或者 uv_toon
,它将对纹理应用卡通效果。
每个着色器由两个文件组成,vs
(顶点着色器)和fs
(片段着色器),使用类似 C 语言的代码编写。它们协同工作以将所需的效果应用到对象上。顶点着色器负责将顶点的 3D 位置映射到 2D 显示。片段着色器(有时也称为像素着色器)负责将光照和纹理效果应用到像素本身。这些着色器的构建和操作超出了本章的范围,但在pi3dshaders
目录中,你可以比较、更改和实验几个示例着色器。
灯光
在 3D 世界中,光照非常重要;它可以从简单的泛光照明(如我们示例中使用的)到从不同方向提供不同强度和颜色的多个光源。光照与物体相互作用以及它们产生的效果将由用于渲染它们的纹理和着色器决定。
灯光通过其方向、颜色和亮度来定义,同时通过环境光来定义背景(非方向性)光。Light
类的定义如下:
class pi3d.Light (lightpos=(10, -10, 20),
lightcol=(1.0, 1.0, 1.0),
lightamb=(0.1, 0.1, 0.2))
默认情况下,显示将定义具有以下属性的光:
-
lightpos=(10, -10, 20)
: 这是一个从空间前方(靠近左上角)向下照射到空间后方的灯光(向右方) -
lightcol=(1.0, 1.0, 1.0)
: 这是一个明亮、白色的方向性光源(方向由前一个维度定义,颜色由 RGB 值1.0, 1.0, 1.0
定义) -
ightamb=(0.1, 0.1, 0.2)
: 这是一个整体略显暗淡、略带蓝色的光
纹理
纹理能够通过将精细细节应用于物体的表面来为物体增添真实感;这可能是用于墙面的砖块图像或显示在角色上的人脸。当着色器使用纹理时,它通常可以被重新缩放,并且可以添加反射效果;一些着色器甚至允许你应用表面细节。
我们可以将多种纹理应用于一个物体,将它们组合起来产生不同的效果;具体如何应用将由着色器来决定。
创建和导入 3D 模型
直接从代码创建复杂形状通常会很繁琐且耗时。幸运的是,您可以将预构建的模型导入到您的 3D 空间中。
甚至可以使用图形 3D 建模程序来生成模型,然后将其导出为适合您使用的格式。此示例生成一个纽威尔茶壶模型
在树莓派主题中,如下截图所示:
新 ell 树莓派茶壶
准备就绪
pi3dmodels
.
建模茶壶是显示Hello World的 3D 传统等效。计算机图形学研究员马丁·纽厄尔(Martin Newell)于 1975 年首次创建了纽厄尔茶壶(Newell Teapot),作为他工作的基本测试模型。纽厄尔茶壶很快成为了快速检查 3D 渲染系统是否正常工作的标准模型(它甚至出现在了玩具总动员和辛普森一家的 3D 集中)。
其他模型可在pi3dmodels
目录中找到(monkey.obj/mdl
,后来在书中资源文件中使用过)。
如何做到这一点...
创建并运行以下3dModel.py
脚本:
#!/usr/bin/python3
""" Wavefront obj model loading. Material properties set in
mtl file. Uses the import pi3d method to load *everything*
"""
import demo
import pi3d
from math import sin, cos, radians
KEY = {'ESC':27,'NONE':-1}
# Setup display and initialise pi3d
DISPLAY = pi3d.Display.create()
#capture mouse and key presses
mykeys = pi3d.Keyboard()
mymouse = pi3d.Mouse(restrict = False)
mymouse.start()
def main():
#Model textures and shaders
shader = pi3d.Shader("uv_reflect")
bumptex = pi3d.Texture("textures/floor_nm.jpg")
shinetex = pi3d.Texture("textures/stars.jpg")
# load model
#mymodel = pi3d.Model(file_string='models/teapot.obj', z=10)
mymodel = pi3d.Model(file_string='models/monkey.obj', z=10)
mymodel.set_shader(shader)
mymodel.set_normal_shine(bumptex, 4.0, shinetex, 0.5)
#Create environment box
flatsh = pi3d.Shader("uv_flat")
ectex = pi3d.loadECfiles("textures/ecubes","sbox")
myecube = pi3d.EnvironmentCube(size=900.0, maptype="FACES",
name="cube")
myecube.set_draw_details(flatsh, ectex)
CAMERA = pi3d.Camera.instance()
rot = 0.0 # rotation of camera
tilt = 0.0 # tilt of camera
k = KEY['NONE']
omx, omy = mymouse.position()
while DISPLAY.loop_running() and not k == KEY['ESC']:
k = mykeys.read()
#Rotate camera - camera steered by mouse
mx, my = mymouse.position()
rot -= (mx-omx)*0.8
tilt += (my-omy)*0.8
omx = mx
omy = my
CAMERA.reset()
CAMERA.rotate(tilt, rot, 0)
#Rotate object
mymodel.rotateIncY(2.0)
mymodel.rotateIncZ(0.1)
mymodel.rotateIncX(0.3)
#Draw objects
mymodel.draw()
myecube.draw()
try:
main()
finally:
mykeys.close()
mymouse.stop()
DISPLAY.destroy()
print("Closed Everything. END")
#End
它是如何工作的...
类似于 3dObject.py
示例,我们定义了 DISPLAY
着色器(这次使用 uv_reflect
)和一些额外的纹理——bumptex
(floor_nm.jpg
) 和 shinetex
(stars.jpg
)——以便稍后使用。我们定义了一个想要导入的模型,将其放置在 z=10
(如果没有给出坐标,它将被放置在 (0,0,0)
)。由于我们没有指定相机位置,默认情况下它将被放置在视图中(有关相机的详细信息,请参阅相关章节)。
我们使用 set_shader()
函数应用着色器。接下来,我们使用 bumptex
作为表面纹理(按 4
缩放)添加一些纹理和效果。我们通过 set_normal_shine()
函数应用额外的闪亮效果,并设置反射强度为 0.5
(强度范围从 0.0,最弱,到 1.0,最强)。如果你仔细观察模型的表面,bumptex
纹理提供了额外的表面细节,而 shinetex
纹理则可以看作是表面上的反射。
为了在比默认蓝色空间更有趣的环境中展示我们的模型,我们将创建一个EnvironmentCube
对象。这定义了一个内部空间应用了特殊纹理的大空间(在这个例子中,它将从texturesecubes
目录中加载sbox_front/back/bottom/left
和sbox_right
图像),因此它有效地包围了其中的对象。结果是,你得到了一个令人愉悦的背景来展示你的对象。
再次,我们定义了一个默认的 CAMERA
对象,并带有 rot
和 tilt
变量来控制视图。在 DISPLAY.loop_running()
部分中,我们可以控制视图。
使用鼠标操作CAMERA
对象,并绕其轴以不同速度旋转模型,以便
旋转并展示其所有侧面(使用RotateIncX/Y/Z()
函数来指定旋转速度)。最后,我们通过绘制模型和环境立方体来确保DISPLAY
被更新。
还有更多...
我们可以在模拟环境中创建各种对象进行放置。pi3d
提供了导入我们自己的模型并将多种纹理应用于它们的方法。
创建或加载您自己的对象
如果你想在这个示例中使用自己的模型,你需要创建一个正确格式的模型;pi3d
支持obj(波前对象文件)和egg(Panda3D)。
一款优秀且免费的 3D 建模程序叫做Blender(可在www.blender.org
获取)。他们的网站上有很多示例和教程,可以帮助你开始学习基本的建模(www.blender.org/education-help/tutorials
)。
pi3d
模型支持有限,不会支持 Blender 可以嵌入导出模型中的所有功能,例如可变形网格。因此,只支持基本的多部件模型。需要几个步骤来简化模型,以便它可以由pi3d
加载。
将 .obj
模型转换为与 pi3d
一起使用,请按照以下步骤操作:
-
在 Blender 中创建或加载一个模型——在尝试更复杂的模型之前,先从简单的对象开始。
-
选择每个对象并切换到编辑模式(按Tab键)。
-
选择所有顶点(按 A 键)并对其进行 uv 映射(按 U 键然后选择 Unwrap)。
-
返回对象模式(按Tab键)。
-
导出为 obj 文件 – 从顶部菜单的文件菜单中选择导出,然后选择 Wavefront (.obj)。确保在左下角的选项列表中也勾选了 Include Normals。
-
点击保存,并将
.obj
和.mtl
文件放置在pi3dmodels
目录中,同时确保您更新脚本以包含模型的文件名,如下所示:
mymodel = pi3d.Model(file_string='models/monkey.obj',
name='monkey', z=4)
当你运行更新后的脚本时,你将在 3D 空间中看到你的模型。例如,monkey.obj
模型在下面的屏幕截图中显示:
使用 Blender 创建并由 pi3d 展示的猴子头模型
更改对象的纹理和 .mtl 文件
模型表面所应用的纹理包含在模型的 .mtl
文件中。此文件定义了纹理及其应用方式,这些方式由建模软件设定。复杂的模型可能包含多个纹理,用于对象的各个部分。
如果没有定义材质,则使用着色器中的第一个纹理(在我们的例子中,这是bumptex
纹理)。要向对象添加新的纹理,请在.mtl
文件中添加(或编辑)以下行(即使用water.jpg
):
map_Kd ../textures/water.jpg
关于.mtl
文件和.obj
文件的更多信息可以在以下链接找到
维基百科链接:zh.wikipedia.org/wiki/Wavefront_.obj 文件
捕获屏幕截图
pi3d
库包含一个有用的截图功能,可以捕获屏幕并将其保存为 .jpg
或 .png
文件。我们可以添加一个新的按键事件来触发它,并调用 pi3d.screenshot("filename.jpg")
来保存图片(或使用计数器来拍摄多张截图),如下面的代码所示:
shotnum = 0 #Set counter to 0
while DISPLAY.loop_running()
...
if inputs.key_state("KEY_P"):
while inputs.key_state("KEY_P"):
inputs.do_input_events() # wait for key to go up
pi3d.screenshot("screenshot%04d.jpg"%( shotnum))
shotnum += 1
...
创建一个可探索的 3D 世界
现在我们能够在我们的 3D 空间内创建模型和对象,以及
生成背景时,我们可能想要在内部创建一个更有趣的环境
哪里放置它们。
3D 地形图提供了一种优雅的方式来定义非常复杂的地形。地形是通过使用灰度图像来设定土地的高程来定义的。以下示例展示了我们如何定义自己的景观并模拟在其上飞越,甚至在其表面行走:
由地形图生成的 3D 景观
准备就绪
您需要将 Map.png
文件(可在书籍资源文件中找到)放置在 pi3d
库的 pi3d/textures
目录下。或者,您可以使用已经存在的其中一个高程图——将 Map.png
的引用替换为另一个高程图,例如 testislands.jpg
。
如何做到这一点...
创建以下 3dWorld.py
脚本:
#!/usr/bin/python3
from __future__ import absolute_import, division
from __future__ import print_function, unicode_literals
""" An example of generating a 3D environment using a elevation map
"""
from math import sin, cos, radians
import demo
import pi3d
KEY = {'R':114,'S':115,'T':116,'W':119,'ESC':27,'NONE':-1}
DISPLAY = pi3d.Display.create(x=50, y=50)
#capture mouse and key presses
mykeys = pi3d.Keyboard()
mymouse = pi3d.Mouse(restrict = False)
mymouse.start()
def limit(value,min,max):
if (value < min):
value = min
elif (value > max):
value = max
return value
def main():
CAMERA = pi3d.Camera.instance()
tex = pi3d.Texture("textures/grass.jpg")
flatsh = pi3d.Shader("uv_flat")
# Create elevation map
mapwidth,mapdepth,mapheight = 200.0,200.0,50.0
mymap = pi3d.ElevationMap("textures/Map.png",
width=mapwidth, depth=mapdepth, height=mapheight,
divx=128, divy=128, ntiles=20)
mymap.set_draw_details(flatsh, [tex], 1.0, 1.0)
rot = 0.0 # rotation of camera
tilt = 0.0 # tilt of camera
height = 20
viewhight = 4
sky = 200
xm,ym,zm = 0.0,height,0.0
k = KEY['NONE']
omx, omy = mymouse.position()
onGround = False
# main display loop
while DISPLAY.loop_running() and not k == KEY['ESC']:
CAMERA.reset()
CAMERA.rotate(-tilt, rot, 0)
CAMERA.position((xm,ym,zm))
mymap.draw()
mx, my = mymouse.position()
rot -= (mx-omx)*0.8
tilt += (my-omy)*0.8
omx = mx
omy = my
#Read keyboard keys
k = mykeys.read()
if k == KEY['W']:
xm -= sin(radians(rot))
zm += cos(radians(rot))
elif k == KEY['S']:
xm += sin(radians(rot))
zm -= cos(radians(rot))
elif k == KEY['R']:
ym += 2
onGround = False
elif k == KEY['T']:
ym -= 2
ym -= 0.1 #Float down!
#Limit the movement
xm = limit(xm,-(mapwidth/2),mapwidth/2)
zm = limit(zm,-(mapdepth/2),mapdepth/2)
if ym >= sky:
ym = sky
#Check onGround
ground = mymap.calcHeight(xm, zm) + viewhight
if (onGround == True) or (ym <= ground):
ym = mymap.calcHeight(xm, zm) + viewhight
onGround = True
try:
main()
finally:
mykeys.close()
mymouse.stop()
DISPLAY.destroy()
print("Closed Everything. END")
#End
它是如何工作的...
一旦我们定义了将要使用的显示、相机、纹理和着色器,我们就可以定义ElevationMap
对象。
它通过根据图像所选点的像素值为地形图像分配高度来工作。例如,图像的单行将提供ElevationMap
对象的切片和 3D 表面上的一个高程点行:
将地图.png 像素色调映射到地形高度
我们通过提供用于梯度信息的图像文件名(textures/Map.png
)来创建一个ElevationMap
对象,同时我们也创建了地图的尺寸(width
、depth
和height
——这是白色空间与黑色空间相比的高度):
高程图对象
Map.png
纹理提供了一个示例地形图,该图被转换成
三维曲面。
我们还指定了 divx
和 divy
,它们决定了地形图细节的使用程度(使用地形图中的多少点来创建高程表面)。最后,ntiles
指定使用的纹理将被缩放到覆盖表面的 20 倍
。
在主DISPLAY.loop_running()
部分,我们将控制相机,绘制高程图
,响应用户输入,并限制我们在空间中的移动。
如前所述,我们使用一个Keyboard
对象来捕捉鼠标移动并将它们转换为控制相机。我们还将使用mykeys.read()
来确定是否按下了W、S、R和T键,这允许我们前进和后退,以及上升和下降。
为了方便在返回值和它们等效意义之间进行转换,我们将使用一个 Python 字典:
词典提供了一种简单的方法在给定的值和结果字符串之间进行转换。要访问键的值,我们使用 KEY['W']
。我们还在第三章,《使用 Python 进行自动化和生产效率》,中使用了一个字典来在图像 EXIF 标签名称和 ID 之间进行转换。
为了确保我们不会穿过ElevationMap
对象表面
我们移动到它上面,可以使用 mymap.calcHeight()
来提供其高度
在特定位置(x,y,z
)的地形。我们可以通过确保相机设置为等于这个值来跟随地面,或者通过确保我们从未低于它来飞行于空中。当我们检测到我们在地面上时,我们确保我们保持在地面直到
我们按R键再次升起。
构建三维地图和迷宫
我们已经看到,pi3d
库可以用来创建许多有趣的对象和环境。通过使用一些更复杂的类(或者通过构建我们自己的类),可以为用户设计出整个定制的空间供其探索。
在以下示例中,我们使用一个名为Building的特殊模块,该模块已被设计为允许您使用单个图像文件来提供布局,从而构建整个建筑:
探索迷宫并找到标记出口的球体
准备就绪
您需要确保在pi3d/textures
目录中拥有以下文件:
-
squareblocksred.png
-
floor.png
-
inside_map0.png
,inside_map1.png
,inside_map2.png
这些文件作为书籍资源的一部分提供,位于Chapter07resourcesource_filestextures
。
如何做到这一点...
让我们按照以下步骤运行以下3dMaze.py
脚本:
- 首先,我们使用以下代码设置模型的键盘、鼠标、显示器和设置:
#!/usr/bin/python3
"""Small maze game, try to find the exit
"""
from math import sin, cos, radians
import demo
import pi3d
from pi3d.shape.Building import Building, SolidObject
from pi3d.shape.Building import Size, Position
KEY = {'A':97,'D':100,'H':104,'R':114,'S':115,'T':116,
'W':119,'ESC':27,'APOST':39,'SLASH':47,'NONE':-1}
# Setup display and initialise pi3d
DISPLAY = pi3d.Display.create()
#capture mouse and key presses
mykeys = pi3d.Keyboard()
mymouse = pi3d.Mouse(restrict = False)
#Load shader
shader = pi3d.Shader("uv_reflect")
flatsh = pi3d.Shader("uv_flat")
# Load textures
ceilingimg = pi3d.Texture("textures/squareblocks4.png")
wallimg = pi3d.Texture("textures/squareblocksred.png")
floorimg = pi3d.Texture("textures/dunes3_512.jpg")
bumpimg = pi3d.Texture("textures/mudnormal.jpg")
startimg = pi3d.Texture("textures/rock1.jpg")
endimg = pi3d.Texture("textures/water.jpg")
# Create elevation map
mapwidth = 1000.0
mapdepth = 1000.0
#We shall assume we are using a flat floor in this example
mapheight = 0.0
mymap = pi3d.ElevationMap(mapfile="textures/floor.png",
width=mapwidth, depth=mapdepth, height=mapheight,
divx=64, divy=64)
mymap.set_draw_details(shader,[floorimg, bumpimg],128.0, 0.0)
levelList = ["textures/inside_map0.png","textures/inside_map1.png",
"textures/inside_map2.png"]
avhgt = 5.0
aveyelevel = 4.0
MAP_BLOCK = 15.0
aveyeleveladjust = aveyelevel - avhgt/2
PLAYERHEIGHT = (mymap.calcHeight(5, 5) + avhgt/2)
#Start the player in the top-left corner
startpos = [(8*MAP_BLOCK),PLAYERHEIGHT,(8*MAP_BLOCK)]
endpos = [0,PLAYERHEIGHT,0] #Set the end pos in the centre
person = SolidObject("person", Size(1, avhgt, 1),
Position(startpos[0],startpos[1],startpos[2]), 1)
#Add spheres for start and end, end must also have a solid object
#so we can detect when we hit it
startobject = pi3d.Sphere(name="start",x=startpos[0],
y=startpos[1]+avhgt,z=startpos[2])
startobject.set_draw_details(shader, [startimg, bumpimg],
32.0, 0.3)
endobject = pi3d.Sphere(name="end",x=endpos[0],
y=endpos[1],z=endpos[2])
endobject.set_draw_details(shader, [endimg, bumpimg], 32.0, 0.3)
endSolid = SolidObject("end", Size(1, avhgt, 1),
Position(endpos[0],endpos[1],endpos[2]), 1)
mazeScheme = {"#models": 3,
(1,None): [["C",2]], #white cell : Ceiling
(0,1,"edge"): [["W",1]], #white cell on edge next
# black cell : Wall
(1,0,"edge"): [["W",1]], #black cell on edge next
# to white cell : Wall
(0,1):[["W",0]]} #white cell next
# to black cell : Wall
details = [[shader, [wallimg], 1.0, 0.0, 4.0, 16.0],
[shader, [wallimg], 1.0, 0.0, 4.0, 8.0],
[shader, [ceilingimg], 1.0, 0.0, 4.0, 4.0]]
arialFont = pi3d.Font("fonts/FreeMonoBoldOblique.ttf",
"#ffffff", font_size=10)
- 我们随后创建函数以便我们使用以下代码重新加载关卡并向玩家显示消息:
def loadLevel(next_level):
print(">>> Please wait while maze is constructed...")
next_level=next_level%len(levelList)
building = pi3d.Building(levelList[next_level], 0, 0, mymap,
width=MAP_BLOCK, depth=MAP_BLOCK, height=30.0,
name="", draw_details=details, yoff=-15, scheme=mazeScheme)
return building
def showMessage(text,rot=0):
message = pi3d.String(font=arialFont, string=text,
x=endpos[0],y=endpos[1]+(avhgt/4),
z=endpos[2], sx=0.05, sy=0.05,ry=-rot)
message.set_shader(flatsh)
message.draw()
- 在主函数中,我们使用以下代码设置 3D 环境和绘制所有对象:
def main():
#Load a level
level=0
building = loadLevel(level)
lights = pi3d.Light(lightpos=(10, -10, 20),
lightcol =(0.7, 0.7, 0.7),
lightamb=(0.7, 0.7, 0.7))
rot=0.0
tilt=0.0
#capture mouse movements
mymouse.start()
omx, omy = mymouse.position()
CAMERA = pi3d.Camera.instance()
while DISPLAY.loop_running() and not
inputs.key_state("KEY_ESC"):
CAMERA.reset()
CAMERA.rotate(tilt, rot, 0)
CAMERA.position((person.x(), person.y(),
person.z() - aveyeleveladjust))
#draw objects
person.drawall()
building.drawAll()
mymap.draw()
startobject.draw()
endobject.draw()
#Apply the light to all the objects in the building
for b in building.model:
b.set_light(lights, 0)
mymap.set_light(lights, 0)
#Get mouse position
mx, my = mymouse.position()
rot -= (mx-omx)*0.8
tilt += (my-omy)*0.8
omx = mx
omy = my
xm = person.x()
ym = person.y()
zm = person.z()
- 最后,我们监控按键,处理与物体的任何碰撞,并在迷宫中按照以下方式移动:
#Read keyboard keys
k = mykeys.read()
if k == KEY['APOST']: #' Key
tilt -= 2.0
elif k == KEY['SLASH']: #/ Key
tilt += 2.0
elif k == KEY['A']:
rot += 2.0
elif k == KEY['D']:
rot -= 2.0
elif k == KEY['H']:
#Use point_at as help - will turn the player to face
# the direction of the end point
tilt, rot = CAMERA.point_at([endobject.x(), endobject.y(),
endobject.z()])
elif k == KEY['W']:
xm -= sin(radians(rot))
zm += cos(radians(rot))
elif k == KEY['S']:
xm += sin(radians(rot))
zm -= cos(radians(rot))
NewPos = Position(xm, ym, zm)
collisions = person.CollisionList(NewPos)
if collisions:
#If we reach the end, reset to start position!
for obj in collisions:
if obj.name == "end":
#Required to remove the building walls from the
# solidobject list
building.remove_walls()
showMessage("Loading Level",rot)
DISPLAY.loop_running()
level+=1
building = loadLevel(level)
showMessage("")
person.move(Position(startpos[0],startpos[1],
startpos[2]))
else:
person.move(NewPos)
try:
main()
finally:
mykeys.close()
mymouse.stop()
DISPLAY.destroy()
print("Closed Everything. END")
#End
它是如何工作的...
我们定义了在前面示例中使用到的许多元素,例如显示、纹理、着色器、字体和光照。我们还定义了对象,如建筑本身、ElevationMap
对象,以及迷宫的起点和终点。此外,我们还使用 SolidObjects 来帮助检测空间内的移动。有关更多信息,请参阅此食谱中 There's more... 部分的 *Using SolidObjects to detect collisions 子部分。
最后,我们根据选定的地图图像创建实际的 Building
对象(使用 loadLevel()
函数),并将摄像机(代表我们的第一人称视角)定位在起始位置。有关更多信息,请参阅此食谱中 There's more... 部分的 The Building module 子节。
在main
循环中,我们绘制我们空间中的所有对象并应用光照效果。我们还将监控鼠标(以控制相机的倾斜和旋转)或键盘的移动来移动玩家(或退出/提供帮助)。
控制方式如下:
-
鼠标移动:这会改变相机的倾斜和旋转。
-
' 或 / 键:这会将相机倾斜向上或向下。
-
A 或 D:这会将相机旋转从左到右或反之。
-
W 或 S: 这将使玩家向前或向后移动。
-
H: 这通过旋转玩家使其面对迷宫的终点来帮助玩家。有用的
CAMERA.point_at()
函数用于快速旋转和倾斜摄像机的视角,使其指向提供的坐标(终点位置)。
每当玩家移动时,我们检查新位置(NewPos
)是否与另一个SolidObject
发生碰撞,使用CollisionList(NewPos)
。该函数将返回与提供的坐标重叠的任何其他SolidObject
的列表。
如果没有障碍物挡道,我们就让玩家移动;否则,我们检查是否有 SolidObject 的名称是end
对象,在这种情况下,我们就到达了迷宫的尽头。
当玩家到达终点时,我们从旧的Building
对象中移除墙壁并显示加载信息。如果我们不移除墙壁,属于前一个Building
的所有 SolidObjects 仍然会保留,从而在下一级中创建不可见的障碍物。
我们将使用showMessage()
函数来通知用户下一级关卡即将加载(因为构建对象可能需要一段时间)。我们需要确保在绘制消息后调用DISPLAY.loop_running()
,这样可以确保在开始加载关卡之前(在此之后,玩家将无法移动,因为正在加载)消息已经在屏幕上显示。我们需要确保无论玩家的哪一侧与end
对象发生碰撞,消息总是面向玩家,通过使用摄像机的旋转(rot
)来调整其角度:
当找到出口球时,加载下一级
当列表中的下一级被加载(或者在所有级别都完成后再次加载第一级时),我们将消息替换为一个空白消息以移除它,并将该人的位置重置回起始位置。
您可以通过创建额外的地图文件(20 x 20 的 PNG 文件,用黑色像素标记墙壁,用白色表示通道)并在levelList
中列出它们来设计和添加自己的关卡。玩家将从地图的左上角开始,出口放置在中心。
你会注意到加载关卡可能需要相当长的时间;这是相对较慢的树莓派 ARM 处理器在执行构建迷宫和定位所有组件所需的所有计算。一旦迷宫构建完成,更强大的 GPU 接管,这使得玩家探索空间时图形快速且流畅。
本食谱展示了原始树莓派处理器与树莓派 2 之间的区别。树莓派 2 加载第一级需要大约 1 分 20 秒,而原始树莓派可能需要长达 4 分 20 秒。树莓派 3 加载同一级仅需惊人的 4 秒。
还有更多...
上述示例为玩家创建了一个可以探索和交互的建筑。为了实现这一点,我们使用pi3d
的Building
模块来创建建筑,并使用SolidObject
来检测碰撞。
建筑模块
pi3d.Building
模块允许您使用地图文件定义建筑物的整个楼层或楼层。就像前一个示例中使用的地形图一样,像素的颜色将被转换为不同部分的楼层。在我们的例子中,黑色用于墙壁,白色用于通道和走廊,并配有天花板:
建筑布局由图像中的像素定义
由Building
对象构建的章节由所使用的Scheme
定义。Scheme
由两个部分定义,首先是模型的数量,然后是针对模型各个方面的定义,如下面的代码所示:
mazeScheme = {"#models": 3,
(1,None): [["C",2]], #white cell : Ceiling
(0,1,"edge"): [["W",1]], #white cell on edge by black cell : Wall
(1,0,"edge"): [["W",1]], #black cell on edge by white cell : Wall
(0,1):[["W",0]]} #white cell next to black cell : Wall
第一个元组定义了所选模型应应用于哪种类型的单元格/方块。由于地图中有两种像素颜色,方块将是黑色(0
)或白色(1
)。通过确定特定单元格/方块的位置和类型,我们可以定义我们想要应用哪些模型(墙壁、天花板或屋顶)。
我们可以定义三种主要的单元格/方块位置类型:
-
一个完整的平方(1,None):这是一个代表开放空间的白色单元格。
这座建筑。
-
相邻的两个单元格,在边缘(0,1,"edge"):这是地图边缘旁边的一个黑色单元格和一个白色单元格。这还包括
(1,0,"edge")
。这将会代表建筑的外墙。 -
紧挨着白色细胞(0,1)的任何黑色细胞:这代表建筑物的所有内部墙壁。
接下来,我们分配一种(或多种)对象类型以应用于该类型(W
或 C
):
-
墙 (W): 这是一个放置在指定单元格之间(例如黑色和白色单元格之间)的垂直墙。
-
天花板(C):这是覆盖当前单元格的水平天花板部分。
-
屋顶 (R): 这是一个放置在天花板稍上方的额外水平部分,用于提供屋顶效果。它通常用于可能需要从外部观看的建筑(在我们的示例中不使用)。
-
天花板边缘 (CE): 这用于将天花板部分连接到建筑物的屋顶边缘(在我们的示例中不使用,因为我们的模型是室内模型)。
最后,我们指定每个对象将使用的模型。在这个示例中我们使用了三种模型(普通墙壁、边缘上的墙壁和天花板),因此我们可以通过指定0
、1
或2
来定义所使用的模型。
每个模型都在details
数组中定义,这使得我们可以为每个模型设置所需的纹理和着色器(这包含通常由.set_draw_details()
函数设置的相同信息),如下面的代码所示:
details = [[shader, [wallimg], 1.0, 0.0, 4.0, 16.0],
[shader, [wallimg], 1.0, 0.0, 4.0, 8.0],
[shader, [ceilingimg], 1.0, 0.0, 4.0, 4.0]]
在我们的示例中,内部墙壁分配给了wallimg
纹理(textures/squareblocksred.png
),而天花板分配给了ceilingimg
纹理(textures/squareblocks4.png
)。您可能可以从下面的屏幕截图中发现,我们可以将不同的纹理模型(在我们的情况下,稍微不同的缩放)应用于不同类型的方块。迷宫外部的墙壁(带有边缘标识符)将使用wallimg
模型纹理,缩放为 4 x 8(details[1]
),而对于内部墙壁,相同的模型纹理将缩放为 4 x 16(details[0]
):
不同缩放的墙壁
当创建pi3d.Building
对象时,scheme
和draw_details
都被设置,如下面的代码所示:
building = pi3d.Building(levelList[next_level], 0, 0, mymap,
width=MAP_BLOCK, depth=MAP_BLOCK, height=30.0, name="",
draw_details=details, yoff=-15, scheme=mazeScheme)
使用地图文件(levelList[next_level
]),方案(mazeScheme
)和绘制细节(details
),整个建筑在环境中被创建出来:
我们创建的 3D 迷宫的俯视图
尽管在这个例子中我们只使用了黑白两种颜色,但也可以使用其他彩色像素来定义额外的方块类型(以及如果需要的话,不同的纹理)。如果添加了另一种颜色(例如灰色),颜色映射的索引就会移动,使得黑色方块被引用为0
,新的彩色方块为1
,白色方块为2
。有关详细信息,请参阅pi3d
演示中的Silo示例。
我们还需要定义一个 ElevationMap
对象:mymap
。pi3d.Building
模块利用 ElevationMap
对象的 calcHeight()
函数来正确地将墙壁放置在 ElevationMap
对象的表面之上。在这个例子中,我们将使用 textures/floor.png
应用一个基本的 ElevationMap
对象,这将生成一个平面表面,Building
对象将被放置在这个表面上。
使用 SolidObjects 检测碰撞
除了Building
对象之外,我们还将定义一个玩家对象,并定义两个对象来标记迷宫的起点和终点。尽管玩家的视角是第一人称视角(也就是说,我们实际上看不到他们,因为视角实际上是透过他们的眼睛),但我们仍需要定义一个SolidObject来代表他们。
SolidObject 是一种特殊类型的不可见对象,可以通过检查来确定一个 SolidObject 将要占据的空间是否与另一个对象重叠。这将使我们能够使用person.CollisionList(NewPos)
来获取任何其他 SolidObjects 的列表,这些 SolidObjects 将在NewPos
位置与person
对象接触。由于Building
类为Building
对象的各个部分定义了 SolidObjects,因此我们能够检测到玩家试图穿过墙壁(或者,出于某种原因,屋顶/天花板)并阻止他们穿过。
我们还在迷宫中使用 SolidObjects 来定义起点和终点。玩家开始的位置被设置为地图的左上角(地图左上角的空白像素)并由startpos
对象标记(一个带有rock1.jpg
纹理的小pi3d.Sphere
,放置在人物头部上方)。迷宫的终点由另一个pi3d.Sphere
对象(带有water.jpg
纹理)标记,位于地图中心。我们还定义了另一个 SolidObject 在终点,这样我们就可以检测到玩家到达它并与它碰撞(并加载下一级)。
第八章:构建人脸检测和人脸识别应用
本章介绍了以下食谱:
-
面部识别系统简介
-
构建人脸检测应用程序
-
构建人脸识别应用程序
-
面部识别系统的应用
简介
近年来,人脸识别已成为最热门的研究领域之一。人脸识别系统是一种具有检测和识别人脸能力的计算机程序。为了识别一个人,它考虑其独特的面部特征。最近,它已被应用于多个安全和监控安装中,以确保高风险区域、住宅区、私人及公共建筑等地的安全。
构建人脸检测应用程序
在本节中,我们讨论如何从网络摄像头图像中检测人脸。要实现实时人脸检测,需要将 USB 网络摄像头连接到 Raspberry Pi 3。
如何操作...
- 导入必要的包:
import cv2
import numpy as np
- 加载人脸级联文件:
frontalface_cascade= cv2.CascadeClassifier('haarcascade_frontalface_alt.xml')
- 检查是否已加载人脸级联文件:
if frontalface_cascade.empty():
raiseIOError('Unable to load the face cascade classifier xml file')
- 初始化视频捕获对象:
capture = cv2.VideoCapture(0)
- 定义缩放因子:
scale_factor = 0.5
- 执行操作,直到按下Esc键:
# Loop until you hit the Esc key
while True:
- 捕获当前帧并调整其大小:
ret, frame = capture.read()
frame = cv2.resize(frame, None, fx=scale_factor, fy=scale_factor,
interpolation=cv2.INTER_AREA)
- 将图像帧转换为灰度:
gray_image = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
- 在灰度图像上运行人脸检测器:
face_rectangle = frontalface_cascade.detectMultiScale(gray_image, 1.3, 5)
- 绘制矩形框:
for (x,y,w,h) in face_rectangle:
cv2.rectangle(frame, (x,y), (x+w,y+h), (0,255,0), 3)
- 显示输出图像:
cv2.imshow('Face Detector', frame)
- 检查是否按下了Esc键以终止操作:
a = cv2.waitKey(1)
if a == 10:
break
- 停止视频捕获并终止操作:
capture.release()
cv2.destroyAllWindows()
人类面部检测系统得到的结果如下所示:
构建人脸识别应用程序
人脸识别是在人脸检测之后执行的技术。检测到的人脸与数据库中存储的图像进行比较。它从输入图像中提取特征,并将这些特征与数据库中存储的人类特征进行匹配。
如何操作...
- 导入必要的包:
import cv2
import numpy as np
from sklearn import preprocessing
- 加载编码和解码任务操作符:
class LabelEncoding(object):
# Method to encode labels from words to numbers
def encoding_labels(self, label_wordings):
self.le = preprocessing.LabelEncoder()
self.le.fit(label_wordings)
- 实现输入标签的单词转数字功能:
def word_to_number(self, label_wordings):
return int(self.le.transform([label_wordings])[0])
- 将输入标签从数字转换为文字:
def number_to_word(self, label_number):
return self.le.inverse_transform([label_number])[0]
- 从输入路径提取图像和标签:
def getting_images_and_labels(path_input):
label_wordings = []
- 对输入路径进行迭代并附加文件:
for roots, dirs, files in os.walk(path_input):
for fname in (x for x in files if x.endswith('.jpg')):
fpath = os.path.join(roots, fname)
label_wordings.append(fpath.split('/')[-2])
- 初始化变量并解析输入寄存器:
images = []
le = LabelEncoding()
le.encoding_labels(label_wordings)
labels = []
# Parse the input directory
for roots, dirs, files in os.walk(path_input):
for fname in (x for x in files if x.endswith('.jpg')):
fpath = os.path.join(roots, fname)
- 读取灰度图像:
img = cv2.imread(fpath, 0)
- 提取标签:
names = fpath.split('/')[-2]
- 执行人脸检测:
face = faceCascade.detectMultiScale(img, 1.1, 2, minSize=(100,100))
- 使用人脸矩形重复该过程:
for (x, y, w, h) in face:
images.append(img[y:y+h, x:x+w])
labels.append(le.word_to_number(names))
return images, labels, le
if __name__=='__main__':
path_cascade = "haarcascade_frontalface_alt.xml"
train_img_path = 'faces_dataset/train'
path_img_test = 'faces_dataset/test'
- 加载人脸级联文件:
faceCascade = cv2.CascadeClassifier(path_cascade)
- 使用局部二值模式初始化人脸检测:
face_recognizer = cv2.createLBPHFaceRecognizer()
- 从训练人脸数据集中提取人脸特征:
imgs, labels, le = getting_images_and_labels(train_img_path)
- 训练人脸检测系统:
print "nTraining..."
face_recognizer.train(imgs, np.array(labels))
- 测试人脸检测系统:
print 'nPerforming prediction on test images...'
flag_stop = False
for roots, dirs, files in os.walk(path_img_test):
for fname in (x for x in files if x.endswith('.jpg')):
fpath = os.path.join(roots, fname)
- 验证人脸识别系统:
predicting_img = cv2.imread(fpath, 0)
# Detect faces
face = faceCascade.detectMultiScale(predicting_img, 1.1,
2, minSize=(100,100))
# Iterate through face rectangles
for (x, y, w, h) in face:
# Predict the output
index_predicted, config = face_recognizer.predict(
predicting_img[y:y+h, x:x+w])
# Convert to word label
person_predicted = le.number_to_word(index_predicted)
# Overlay text on the output image and display it
cv2.putText(predicting_img, 'Prediction: ' + person_predicted,
(10,60), cv2.FONT_HERSHEY_SIMPLEX, 2, (255,255,255), 6)
cv2.imshow("Recognizing face", predicting_img)
a = cv2.waitKey(0)
if a == 27:
flag = True
break
if flag_stop:
break
在这里展示了获取到的面部识别输出:
它是如何工作的...
面部识别系统被广泛用于实现个人安全系统。读者可以参考文章《基于 OpenCV 的面部检测系统》ieeexplore.ieee.org/document/6242980/
。
参见
请参阅以下文章:
面部识别系统的应用
面部识别在安全、医疗和营销领域得到广泛应用。各行业正在开发使用深度学习技术的创新面部识别系统,以识别欺诈行为、区分人脸与照片之间的差异等。在医疗领域,面部识别与其他计算机视觉算法相结合,用于检测面部皮肤疾病。
第九章:使用 Python 驱动硬件
在本章中,我们将涵盖以下主题:
-
控制 LED
-
响应按钮
-
控制关机按钮
-
GPIO 键盘输入
-
多路复用彩色 LED
-
使用视觉暂留原理编写信息
简介
树莓派电脑的一个关键特性,使其与其他大多数家用/办公电脑区别开来的是,它能够直接与其他硬件进行接口。树莓派上的通用输入/输出(GPIO)引脚可以控制一系列低级电子设备,从发光二极管(LEDs)到开关、传感器、电机、伺服机构和甚至额外的显示屏。
本章将专注于将树莓派与一些简单的电路连接起来,并掌握使用 Python 来控制和响应连接组件的方法。
树莓派硬件接口由位于板边一侧的 40 个引脚组成。
GPIO 引脚及其布局将根据您所拥有的特定型号略有不同。
Raspberry Pi 3、Raspberry Pi 2 和 Raspberry Pi B+都拥有相同的 40 针布局。
较早的 Raspberry Pi 1 型号(非 plus 型号)有一个 26 针引脚,这与较新型号的 1-26 针相同。
Raspberry Pi 2、Raspberry Pi B+和 Raspberry Pi Model Plus 的 GPIO 引脚(引脚功能)
连接器的布局显示在先前的图中;引脚编号是从 GPIO 引脚 1 处看到的。
引脚 1位于最靠近 SD 卡的一端,如图所示:
树莓派 GPIO 引脚位置
使用 GPIO 引脚时需小心,因为它还包括电源引脚(3V3 和 5V),以及地(GND)引脚。所有的 GPIO 引脚都可以用作标准 GPIO,但其中一些也具有特殊功能;这些引脚被标记并用不同颜色突出显示。
工程师们通常会在电路图中使用 3V3 这样的记法来指定值,以避免使用容易被遗漏的小数点(使用 33V 而不是 3.3V 会对电路造成严重损坏)。同样的方法也适用于其他组件的值,例如,1.2K 欧姆可以写成 1K2 欧姆。
TX 和 RX 引脚用于串行通信,借助电压等级转换器,信息可以通过串行电缆传输到另一台计算机或设备。
我们还有 SDA 和 SCL 引脚,它们能够支持一种称为 I²C 的双线总线通信协议(在 Raspberry Pi 3 和 Model Plus 板上,有两个 I²C 通道:通道 1 ARM,用于通用,以及 通道 0 VC,通常用于识别 顶部连接的硬件(HAT)模块)。还有 SPI MOSI、SPI MISO、SPI SCLK、SPI CE0 和 SPI CE1 引脚,它们支持另一种称为 SPI 的总线协议,用于高速数据传输。最后,我们还有 PWM0/1 引脚,它允许生成 脉冲宽度调制 信号,这对于伺服系统和生成模拟信号非常有用。
然而,在本章中我们将专注于仅使用标准的 GPIO 功能。GPIO 引脚布局如图所示:
树莓派 GPIO 引脚头(GPIO.BOARD 和 GPIO.BCM)
Raspberry Pi Rev 2(2014 年 7 月之前)与 Raspberry Pi 2 GPIO 布局相比有以下不同之处:
-
26-GPIO 引脚排针(与第一个 26 个引脚相匹配)。
-
位于引脚头旁边的额外一组八个孔(P5)。具体细节如下:
Raspberry Pi Rev 2 P5 GPIO 头引脚
- 原始的 Raspberry Pi Rev 1(2012 年 10 月之前)总共只有 26 个 GPIO 引脚,(与当前 Raspberry Pi 的前 26 个引脚相匹配,除了以下细节:
Raspberry Pi Rev 1 GPIO 头部差异
RPi.GPIO
库可以使用两种系统之一来引用 Raspberry Pi 上的引脚。中间显示的数字代表引脚的物理位置,也是 RPi.GPIO
库在 GPIO.BOARD 模式下所引用的数字。外部的数字(GPIO.BCM)是处理器物理端口的实际参考编号,它们指示哪些引脚是连接的(这就是为什么它们没有特定的顺序)。当模式设置为 GPIO.BCM 时,它们被用来控制 GPIO 头部引脚以及连接到其他 GPIO 线的任何外围设备。这包括连接到 BCM GPIO 4 的附加摄像头上的 LED 以及板上的状态 LED。然而,这也可以包括用于读取/写入 SD 卡的 GPIO 线,如果被干扰,将导致严重错误。
如果你使用其他编程语言来访问 GPIO 引脚,编号方案可能不同,因此了解 BCM GPIO 引用会有所帮助,这些引用指的是处理器的物理 GPIO 端口。
一定要查看附录,硬件和软件列表,其中列出了本章中使用的所有物品以及你可以从中获取它们的地方。
控制 LED
硬件上的hello world
相当于一个 LED 闪烁,这是一个很好的测试,以确保一切正常工作并且你已经正确地连接了线路。为了让它更有趣,我建议使用一个红色、蓝色和绿色(RGB)的 LED,但如果这是你唯一可用的 LED,也请随意使用。
准备就绪
你需要以下设备:
-
4 x 杜邦公对母贴片线
-
小型面包板(170 个接线点)或更大型的面包板
-
RGB LED(共阴极)/3 标准 LED(理想情况下为红色、绿色和蓝色)
-
面板线(实心芯)
-
3 x 470 欧姆电阻
上述每个组件的成本都不应超过几美元,并且在之后的项目中可以重复使用。面包板是一个特别有用的物品,它允许你在不需要焊接的情况下尝试自己的电路:
RGB LED、标准 LED 和 RGB 电路的图示
以下图表展示了面包板电路:
连接到 GPIO 头部的 RGB LED/标准 LED 的接线
市面上有多种不同类型的 RGB LED 可供选择,因此请检查您组件的数据表以确认您所拥有的引脚顺序和类型。有些是 RGB 类型的,所以请确保您正确接线或调整代码中的RGB_
引脚设置。您还可以获得常见的正极版本,这需要将正极连接到 3V3(GPIO 引脚 1)才能点亮(并且它们还需要将RGB_ENABLE
和RGB_DISABLE
分别设置为0
和1
)。
本书中的面包板和组件图都是使用一款名为Fritzing的免费工具制作的(www.fritzing.org);它非常适合规划您自己的树莓派项目。
如何做到这一点...
- 创建
ledtest.py
脚本如下:
#!/usr/bin/python3
#ledtest.py
import time
import RPi.GPIO as GPIO
# RGB LED module
#HARDWARE SETUP
# GPIO
# 2[======XRG=B==]26[=======]40
# 1[=============]25[=======]39
# X=GND R=Red G=Green B=Blue
#Setup Active States
#Common Cathode RGB-LED (Cathode=Active Low)
RGB_ENABLE = 1; RGB_DISABLE = 0
#LED CONFIG - Set GPIO Ports
RGB_RED = 16; RGB_GREEN = 18; RGB_BLUE = 22
RGB = [RGB_RED,RGB_GREEN,RGB_BLUE]
def led_setup():
#Setup the wiring
GPIO.setmode(GPIO.BOARD)
#Setup Ports
for val in RGB:
GPIO.setup(val,GPIO.OUT)
def main():
led_setup()
for val in RGB:
GPIO.output(val,RGB_ENABLE)
print("LED ON")
time.sleep(5)
GPIO.output(val,RGB_DISABLE)
print("LED OFF")
try:
main()
finally:
GPIO.cleanup()
print("Closed Everything. END")
#End
RPi.GPIO
库访问 GPIO 引脚硬件需要sudo
权限,因此您需要使用以下命令来运行脚本:
sudo python3 ledtest.py
当你运行脚本时,你应该看到 LED(或如果你使用的是单独的 LED,则是每个 LED)的红、绿、蓝部分依次点亮。如果没有,请检查你的接线是否正确,或者通过临时将红、绿或蓝线连接到 3V3 引脚(GPIO 引脚的 1 号引脚)来确认 LED 是否工作。
sudo
命令对于大多数与硬件相关的脚本来说是必需的,因为用户通常不会在如此低级别直接控制硬件。例如,设置或清除 SD 卡控制器的一部分控制引脚可能会损坏正在写入的数据。因此,出于安全考虑,需要超级用户权限以防止程序意外(或恶意意图)使用硬件。
它是如何工作的...
要使用 Python 访问 GPIO 引脚,我们需要导入RPi.GPIO
库,该库允许通过模块函数直接控制引脚。我们还需要time
模块来暂停程序,使其暂停设定的时间数秒。
我们随后为 LED 的布线和激活状态定义值(参见本食谱“更多内容...”部分中的控制 GPIO 电流部分)。
在程序使用 GPIO 引脚之前,我们需要通过指定编号方法—GPIO.BOARD
—和方向—GPIO.OUT
或GPIO.IN
(在这种情况下,我们将所有 RGB 引脚设置为输出)来设置它们。如果一个引脚被配置为输出,我们将能够设置引脚状态;同样地,如果它被配置为输入,我们将能够读取引脚状态。
接下来,我们通过使用 GPIO.output()
函数来控制引脚,指定 GPIO 引脚的编号和我们希望其处于的状态(1
= 高/开,0
= 低/关)。我们依次打开每个 LED,等待五秒钟,然后将其关闭。
最后,我们使用 GPIO.cleanup()
将 GPIO 引脚恢复到它们原始的默认状态,并释放对引脚的控制权,以便其他程序使用。
还有更多...
在使用树莓派的 GPIO 引脚时必须小心谨慎,因为这些引脚直接连接到树莓派的主处理器,没有任何额外的保护措施。必须小心操作,因为任何错误的接线都可能损坏树莓派处理器,并导致其完全停止工作。
或者,您可以使用许多可直接插入 GPIO 引脚头部的模块之一(从而降低布线的错误率):
例如,Pi-Stop 是一块简单的预构建 LED 板,它模拟了一套交通信号灯,旨在为那些对控制硬件感兴趣但希望避免损坏他们的 Raspberry Pi 的用户提供一个过渡。在掌握基础知识之后,它也成为一个出色的指示器,有助于调试。
确保您已将 ledtest.py
脚本中的 LED CONFIG
引脚引用更新为引用您所使用的硬件的引脚布局和位置。
请参阅附录,硬件和软件列表,以获取树莓派硬件零售商的列表。
控制 GPIO 电流
每个 GPIO 引脚在烧毁之前只能处理一定电流(单个引脚最大为 16 mA,总电流为 30 mA),同样地,RGB LED 也应该限制在不超过 100 mA。通过在 LED 之前或之后添加一个电阻,我们就能限制通过它的电流,并控制其亮度(电流越大,LED 越亮)。
由于我们可能希望同时点亮多个 LED,我们通常的目标是将电流设置得尽可能低,同时仍然提供足够的电力来点亮 LED。
我们可以使用欧姆定律来告诉我们需要使用多少电阻来提供特定的电流。该定律如图所示:
欧姆定律:电流、电阻和电压在电路中的关系
我们的目标是确保电流最小(3 mA)和最大(16 mA),同时从每个 LED 中发出足够明亮的光。为了获得 RGB LED 的平衡输出,我测试了不同的电阻,直到它们提供了接近白色的光(通过卡片观察)。每个 LED 选择了 470 欧姆的电阻(你的 LED 可能略有不同):
需要使用电阻来限制通过 LED 的电流
电阻两端的电压等于 GPIO 电压(Vgpio = 3.3V)减去特定 LED 上的电压降(Vfwd);然后我们可以利用这个电阻来计算每个 LED 所使用的电流,如下公式所示:
我们可以计算每个 LED 所吸取的电流
响应按钮
许多使用树莓派的程序都需要在不连接键盘和屏幕的情况下激活动作。GPIO 引脚为树莓派通过您自己的按钮和开关进行控制提供了一种极好的方式,无需鼠标/键盘和屏幕。
准备就绪
你需要以下设备:
-
2 x 杜邦公对母贴片线
-
小型面包板(170 个接线点)或更大型的面包板
-
按钮开关(瞬时闭合)或通过电线连接来闭合/断开电路
-
面板线(实心芯线)
-
1K 欧姆电阻
开关的配置如图所示:
按钮开关和其他类型的开关
在以下示例中使用的开关是单刀单掷(SPST),瞬时闭合,按钮开关。单刀(SP)意味着有一组触点可以建立连接。在本例中使用的推开关的情况下,每侧的腿通过中间的单刀开关连接在一起。双刀(DP)开关的作用就像一个 SP 开关,只不过两侧在电气上是分离的,允许你同时切换两个独立组件的开启/关闭。
单掷(ST)表示开关只会与一个位置建立连接;另一侧将保持断开。双掷(DT)表示开关的两个位置都会连接到不同的部分。
瞬间关闭意味着按钮在被按下时会关闭开关,并在释放时自动打开。一个锁定的按钮开关将保持关闭状态,直到再次被按下。
尝试使用树莓派连接扬声器或耳机
按钮电路的布局
在本例中,我们将使用声音,因此您还需要将扬声器或耳机连接到树莓派的音频插孔。
您需要使用以下命令安装名为 flite
的程序,这将使我们能够让树莓派说话:
sudo apt-get install flite
安装完成后,您可以使用以下命令进行测试:
sudo flite -t "hello I can talk"
如果声音有点太安静(或者太响),你可以使用以下命令来调整音量(0-100 百分比):
amixer set PCM 100%
如何做到这一点...
创建 btntest.py
脚本如下:
#!/usr/bin/python3
#btntest.py
import time
import os
import RPi.GPIO as GPIO
#HARDWARE SETUP
# GPIO
# 2[==X==1=======]26[=======]40
# 1[=============]25[=======]39
#Button Config
BTN = 12
def gpio_setup():
#Setup the wiring
GPIO.setmode(GPIO.BOARD)
#Setup Ports
GPIO.setup(BTN,GPIO.IN,pull_up_down=GPIO.PUD_UP)
def main():
gpio_setup()
count=0
btn_closed = True
while True:
btn_val = GPIO.input(BTN)
if btn_val and btn_closed:
print("OPEN")
btn_closed=False
elif btn_val==False and btn_closed==False:
count+=1
print("CLOSE %s" % count)
os.system("flite -t '%s'" % count)
btn_closed=True
time.sleep(0.1)
try:
main()
finally:
GPIO.cleanup()
print("Closed Everything. END")
#End
它是如何工作的...
如前一道菜谱中所述,我们按照要求设置了 GPIO 引脚,但这次将其设置为输入模式,并且我们还启用了内部上拉电阻(有关更多信息,请参阅本菜谱“更多内容...”部分的上拉和下拉电阻电路),使用以下代码:
GPIO.setup(BTN,GPIO.IN,pull_up_down=GPIO.PUD_UP)
在设置好 GPIO 引脚后,我们创建一个循环,该循环将不断使用GPIO.input()
检查BTN
的状态。如果返回的值是false
,则表示引脚通过开关连接到了 0V(地),每次按钮被按下时,我们将使用flite
为我们大声计数。
由于我们在try
/finally
条件内部调用了主函数,即使我们使用Ctrl + Z关闭程序,它仍然会调用GPIO.cleanup()
。
我们在循环中使用短暂的延迟;这确保了开关接触上的任何噪音都被忽略。这是因为当我们按下按钮时,并不总是能够完美接触,当我们按下或释放它时,如果再次按下得太快,可能会产生几个触发信号。这被称为软件去抖动;我们在这里忽略信号的抖动。
还有更多...
Raspberry Pi 的 GPIO 引脚必须谨慎使用;用于输入的电压应该是
在特定范围内,并且从它们中抽取的任何电流都应该被最小化使用
保护电阻。
安全电压
我们必须确保我们只连接介于 0(地线)和 3V3 之间的输入。一些处理器使用 0V 到 5V 之间的电压,因此需要额外的组件才能安全地与它们接口。除非你确定它是安全的,否则不要连接使用 5V 的输入或组件,否则你会损坏树莓派的 GPIO 端口。
拉上和下拉电阻电路
之前的代码将 GPIO 引脚设置为使用内部上拉电阻。如果没有在 GPIO 引脚上使用上拉电阻(或下拉电阻),电压将在 3V3 和 0V 之间自由浮动,实际的逻辑状态将无法确定(有时为 1,有时为 0)。
树莓派的内部上拉电阻为 50K 欧姆-65K 欧姆,下拉电阻也是 50K 欧姆-65K 欧姆。在 GPIO 电路中(如下所示图),通常使用 10K 欧姆或更大的外部上拉/下拉电阻,出于类似的原因(当它们不活跃时,电流消耗非常小)。
拉伸电阻允许少量电流通过 GPIO 引脚,当开关未被按下时,将提供高电压。当按下开关时,小电流被流向 0V 的大电流所取代,因此 GPIO 引脚上得到的是低电压。开关在按下时为低电平,逻辑 0。其工作原理如下图中所示:
拉伸电阻电路
拉下电阻器的工作原理相同,只是开关是高电平激活(当按下时 GPIO 引脚为逻辑 1)。其工作原理如图所示:
拉下拉电阻电路
保护电阻
除了开关之外,电路还包括一个与开关串联的电阻,以保护 GPIO 引脚,如下所示图示:
一个 GPIO 保护限流电阻
保护电阻的作用是在 GPIO 引脚意外设置为输出而不是输入时对其进行保护。想象一下,例如,我们的开关连接在 GPIO 和地之间。现在 GPIO 引脚被设置为输出并打开(驱动它到 3V3),当我们按下开关时,如果没有电阻,GPIO 引脚将直接连接到 0V。GPIO 仍然会尝试将其驱动到 3V3;这将导致 GPIO 引脚烧毁(因为它将使用过多的电流来驱动引脚到高电平状态)。如果我们在这里使用一个 1K 欧姆的电阻,引脚就能够使用可接受的电流量被驱动到高电平(I = V/R = 3.3/1K = 3.3 mA)。
一个受控的关机按钮
Raspberry Pi 应始终正确关机,以避免 SD 卡在执行写入操作时因断电而损坏。如果你没有连接键盘或屏幕(你可能正在运行自动化程序或通过网络远程控制它并忘记关闭),这可能会造成问题,因为你无法输入命令或看到你在做什么。通过添加我们自己的按钮和 LED 指示灯,我们可以轻松地发出关机和重置命令,然后再次启动以指示系统何时处于活动状态。
准备就绪
你需要以下设备:
-
3 x 杜邦公对母贴片线
-
小型面包板(170 个接线点)或更大型的面包板
-
按钮式开关(瞬时闭合)
-
通用 LED
-
2 x 470 欧姆电阻
-
面板线(实心线)
关断电路的整体布局将如图所示:
控制关机电路布局
如何做到这一点...
- 按照以下步骤创建
shtdwn.py
脚本:
#!/usr/bin/python3
#shtdwn.py
import time
import RPi.GPIO as GPIO
import os
# Shutdown Script
DEBUG=True #Simulate Only
SNDON=True
#HARDWARE SETUP
# GPIO
# 2[==X==L=======]26[=======]40
# 1[===1=========]25[=======]39
#BTN CONFIG - Set GPIO Ports
GPIO_MODE=GPIO.BOARD
SHTDWN_BTN = 7 #1
LED = 12 #L
def gpio_setup():
#Setup the wiring
GPIO.setmode(GPIO_MODE)
#Setup Ports
GPIO.setup(SHTDWN_BTN,GPIO.IN,pull_up_down=GPIO.PUD_UP)
GPIO.setup(LED,GPIO.OUT)
def doShutdown():
if(DEBUG):print("Press detected")
time.sleep(3)
if GPIO.input(SHTDWN_BTN):
if(DEBUG):print("Ignore the shutdown (<3sec)")
else:
if(DEBUG):print ("Would shutdown the RPi Now")
GPIO.output(LED,0)
time.sleep(0.5)
GPIO.output(LED,1)
if(SNDON):os.system("flite -t 'Warning commencing power down 3 2 1'")
if(DEBUG==False):os.system("sudo shutdown -h now")
if(DEBUG):GPIO.cleanup()
if(DEBUG):exit()
def main():
gpio_setup()
GPIO.output(LED,1)
while True:
if(DEBUG):print("Waiting for >3sec button press")
if GPIO.input(SHTDWN_BTN)==False:
doShutdown()
time.sleep(1)
try:
main()
finally:
GPIO.cleanup()
print("Closed Everything. END")
#End
- 要使此脚本自动运行(一旦我们测试过它),我们可以将脚本放置在
~/bin
目录下(如果我们只想复制它,可以使用cp
代替mv
)并使用以下代码将其添加到crontab
:
mkdir ~/bin
mv shtdwn.py ~/bin/shtdwn.py
crontab -e
- 在文件末尾,我们添加以下代码:
@reboot sudo python3 ~/bin/shtdwn.py
它是如何工作的...
这次,当我们设置 GPIO 引脚时,我们将连接到关机按钮的引脚定义为输入,将连接到 LED 的引脚定义为输出。我们打开 LED 以指示系统正在运行。
通过将DEBUG
标志设置为True
,我们可以在不实际关闭的情况下测试脚本的功能(通过读取终端消息);我们只需确保在使用脚本进行实际操作时将DEBUG
设置为False
即可。
我们进入一个while
循环,每秒检查一次引脚,看 GPIO 引脚是否设置为LOW
(即检查是否按下了开关);如果是,我们进入doShutdown()
函数。
程序将等待三秒钟,然后再次测试以查看按钮是否仍在被按下。如果按钮不再被按下,我们将返回到之前的while
循环。然而,如果在三秒后按钮仍然被按下,程序将闪烁 LED 并触发关机(并且使用flite
提供音频警告)。
当我们对脚本的运行情况感到满意时,我们可以禁用DEBUG
标志(将其设置为False
)并将脚本添加到crontab
。crontab
是一个特殊的程序,它在后台运行,并允许我们在系统启动时(在特定时间、日期或周期性地)安排程序和动作(@reboot
)。这允许脚本在每次树莓派开机时自动启动。当我们按下并保持关机按钮超过三秒钟时,它会安全地关闭系统并进入低功耗状态(在此之前的 LED 灯熄灭,表示可以在稍后安全地移除电源)。要重启树莓派,我们短暂地移除电源;这将重启系统,当树莓派加载完成后,LED 灯会亮起。
还有更多...
我们可以通过使用重置头进一步扩展这个示例,通过添加额外功能并利用额外的 GPIO 连接(如果可用)。
重置和重启树莓派
树莓派上设有用于安装复位跳线的孔(在树莓派 3/2 上标记为RUN,在树莓派 1 型号 A 和型号 B Rev 2 上标记为P6)。复位引脚允许通过按钮来重置设备,而不是每次都需要拔掉微型 USB 连接器。
周转电力时间:
树莓派重置引脚 - 在左侧,树莓派型号 A/B(Rev2),在右侧,树莓派 3
要使用它,您需要将一根电线或引脚头焊接在树莓派上,并将一个按钮连接到它(或者每次短暂触摸两个孔之间的电线)。或者,我们可以扩展我们之前的电路,如下面的图所示:
控制关机电路布局和复位按钮
我们可以向电路中添加这个额外的按钮,该按钮可以连接到复位引脚头(这是在 Raspberry Pi 3 上靠近中间的孔,在其他型号上则靠近边缘)。当这个引脚通过连接到地(例如旁边的孔或另一个地线点,如 GPIO 引脚头的 6 号引脚)临时被拉低时,它将重置 Raspberry Pi,并允许它在关机后再次启动。
添加额外功能
由于我们现在有脚本持续监控关机按钮,我们可以添加额外的按钮/开关/跳线同时进行监控。这将使我们能够通过改变输入来触发特定的程序或设置特定的状态。以下示例允许我们轻松地在自动 DHCP 网络(默认网络设置)和使用直接 IP 地址之间切换,正如在第一章,“使用 Raspberry Pi 3 计算机入门”中直接连接到笔记本电脑或计算机的食谱中所述,用于直接 LAN 连接。
将以下组件添加到之前的电路中:
-
一个 470 欧姆的电阻
-
两个带有跳线连接器(或可选的开关)的引脚头
-
面板线(实心芯)
在添加了前面的组件之后,我们的控制关机电路现在看起来如下所示:
控制关机电路布局、复位按钮和跳线引脚
在之前的脚本中,我们添加了一个额外的输入来检测LAN_SWA
引脚的状态(我们添加到电路中的跳线引脚)使用以下代码:
LAN_SWA = 11 #2
确保在gpio_setup()
函数中将它设置为输入(带有上拉电阻)使用以下代码:
GPIO.setup(LAN_SWA,GPIO.IN,pull_up_down=GPIO.PUD_UP)
添加一个新功能以在局域网模式之间切换并读取新的 IP 地址。doChangeLAN()
函数检查自上次调用以来 LAN_SWA
引脚的状态是否已更改,如果是,则将网络适配器设置为 DHCP 或相应地设置直接局域网设置(如果可用,则使用 flite
语音新 IP 设置)。最后,当设置为直接连接时,该模式激活期间 LED 会缓慢闪烁。使用以下代码来完成此操作:
def doChangeLAN(direct):
if(DEBUG):print("Direct LAN: %s" % direct)
if GPIO.input(LAN_SWA) and direct==True:
if(DEBUG):print("LAN Switch OFF")
cmd="sudo dhclient eth0"
direct=False
GPIO.output(LED,1)
elif GPIO.input(LAN_SWA)==False and direct==False:
if(DEBUG):print("LAN Switch ON")
cmd="sudo ifconfig eth0 169.254.69.69"
direct=True
else:
return direct
if(DEBUG==False):os.system(cmd)
if(SNDON):os.system("hostname -I | flite")
return direct
添加另一个函数,flashled()
,每次调用时只会切换 LED 的状态。该函数的代码如下:
def flashled(ledon):
if ledon:
ledon=False
else:
ledon=True
GPIO.output(LED,ledon)
return ledon
最后,我们将主循环调整为同时调用 doChangeLAN()
并使用其结果来决定是否使用 ledon
调用 flashled()
以跟踪每次 LED 的先前状态。现在应将 main()
函数更新如下:
def main():
gpio_setup()
GPIO.output(LED,1)
directlan=False
ledon=True
while True:
if(DEBUG):print("Waiting for >3sec button press")
if GPIO.input(SHTDWN_BTN)==False:
doShutdown()
directlan= doChangeLAN(directlan)
if directlan:
flashled(ledon)
time.sleep(1)
GPIO 按键输入
我们已经看到如何通过 GPIO 监控输入来启动应用程序和控制树莓派;然而,有时我们需要控制第三方程序。使用uInput
库,我们可以模拟键盘(甚至鼠标移动)的按键操作,从而通过我们自己的定制硬件来控制任何程序。
如需了解有关使用 uInput
的更多信息,请访问 tjjr.fi/sw/python-uinput/
.
准备就绪
执行以下步骤以安装 uInput
:
- 首先,我们需要下载
uInput
。
您需要使用以下命令从 GitHub 下载uInput
Python 库(约 50 KB):
wget https://github.com/tuomasjjrasanen/python-uinput/archive/master.zip
unzip master.zip
图书馆将解压到一个名为 python-uinput-master
的目录中。
- 一旦完成,您可以使用以下命令删除 ZIP 文件:
rm master.zip
- 使用以下命令安装所需的软件包(如果您已经安装了它们,
apt-get
命令将忽略它们):
sudo apt-get install python3-setuptools python3-dev
sudo apt-get install libudev-dev
- 使用以下命令编译和安装
uInput
:
cd python-uinput-master
sudo python3 setup.py install
- 最后,我们使用以下命令加载新的
uinput
内核模块:
sudo modprobe uinput
为了确保它在启动时加载,我们可以使用以下命令将uinput
添加到modules
文件中:
sudo nano /etc/modules
在文件中把uinput
放在新的一行,并保存它(Ctrl + X, Y)。
-
使用以下设备创建以下电路:
-
面板(半尺寸或更大)
-
7 x 杜邦公对母贴片线
-
六个按钮
-
6 个 470 欧姆电阻
-
面板布线(实心线)
-
GPIO 键盘电路布局
键盘电路也可以通过将元件焊接到一个 Vero 原型板(也称为条形板)上而构建成一个永久电路,如下面的照片所示:
GPIO 键盘 Pi 硬件模块
此电路可作为自焊套件从 PiHardware.com 购得。
- 将电路连接到树莓派的 GPIO 引脚,通过将相应的按钮与相应的引脚匹配,如下表所示:
按钮 | GPIO 引脚 | |
---|---|---|
GND | 6 | |
v | B_DOWN | 22 |
< | B_LEFT | 18 |
^ | B_UP | 15 |
> | B_RIGHT | 13 |
1 | B_1 | 11 |
2 | B_2 | 7 |
如何做到...
创建一个 gpiokeys.py
脚本,如下所示:
#!/usr/bin/python3
#gpiokeys.py
import time
import RPi.GPIO as GPIO
import uinput
#HARDWARE SETUP
# GPIO
# 2[==G=====<=V==]26[=======]40
# 1[===2=1>^=====]25[=======]39
B_DOWN = 22 #V
B_LEFT = 18 #<
B_UP = 15 #^
B_RIGHT = 13 #>
B_1 = 11 #1
B_2 = 7 #2
DEBUG=True
BTN = [B_UP,B_DOWN,B_LEFT,B_RIGHT,B_1,B_2]
MSG = ["UP","DOWN","LEFT","RIGHT","1","2"]
#Setup the DPad module pins and pull-ups
def dpad_setup():
#Set up the wiring
GPIO.setmode(GPIO.BOARD)
# Setup BTN Ports as INPUTS
for val in BTN:
# set up GPIO input with pull-up control
#(pull_up_down can be:
# PUD_OFF, PUD_UP or PUD_DOWN, default PUD_OFF)
GPIO.setup(val, GPIO.IN, pull_up_down=GPIO.PUD_UP)
def main():
#Setup uinput
events = (uinput.KEY_UP,uinput.KEY_DOWN,uinput.KEY_LEFT,
uinput.KEY_RIGHT,uinput.KEY_ENTER,uinput.KEY_ENTER)
device = uinput.Device(events)
time.sleep(2) # seconds
dpad_setup()
print("DPad Ready!")
btn_state=[False,False,False,False,False,False]
key_state=[False,False,False,False,False,False]
while True:
#Catch all the buttons pressed before pressing the related keys
for idx, val in enumerate(BTN):
if GPIO.input(val) == False:
btn_state[idx]=True
else:
btn_state[idx]=False
#Perform the button presses/releases (but only change state once)
for idx, val in enumerate(btn_state):
if val == True and key_state[idx] == False:
if DEBUG:print (str(val) + ":" + MSG[idx])
device.emit(events[idx], 1) # Press.
key_state[idx]=True
elif val == False and key_state[idx] == True:
if DEBUG:print (str(val) + ":!" + MSG[idx])
device.emit(events[idx], 0) # Release.
key_state[idx]=False
time.sleep(.1)
try:
main()
finally:
GPIO.cleanup()
#End
它是如何工作的...
首先,我们导入uinput
并定义键盘按钮的接线。对于BTN
中的每个按钮,我们将其设置为输入,并启用内部上拉电阻。
接下来,我们设置uinput
,定义我们想要模拟的按键并将它们添加到uinput.Device()
函数中。我们等待几秒钟以允许uinput
初始化,设置初始按钮和按键状态,并启动我们的main
循环。
主循环分为两个部分:第一部分检查按钮并记录状态到btn_state
中,第二部分将btn_state
与当前的key_state
数组进行比较。这样,我们可以检测到btn_state
的变化并调用device.emit()
来切换按键的状态。
为了让我们能够在后台运行这个脚本,我们可以通过使用&
来运行它,如下所示:
以下命令:
sudo python3 gpiokeys.py &
&
字符允许命令在后台运行,因此我们可以继续使用命令行来运行其他程序。你可以使用fg
将其调回前台,或者如果你有多个命令正在运行,可以使用%1
、%2
等等。使用jobs
来获取列表。
您甚至可以通过按Ctrl + Z将进程/程序挂起,然后使用bg
(这将允许它在后台运行)来恢复它。
您可以使用在第五章“创建滚动游戏”中创建的游戏来测试按键,该章节位于“创建游戏和图形”部分,您现在可以使用 GPIO 方向垫来控制它。别忘了,如果您是通过远程连接到树莓派,那么任何按键操作都只会激活本地连接的屏幕。
还有更多...
我们可以使用 uinput
为其他程序提供硬件控制,包括那些需要鼠标输入的程序。
生成其他密钥组合
你可以在你的文件中创建几个不同的键映射来支持不同的程序。例如,events_z80
键映射对于模拟器,如 Fuse(浏览至 raspi.tv/2012/how-to-install-fuse-zx-spectrum-emulator-on-raspberry-pi
获取更多详情)非常有用。events_omx
键映射适合通过以下命令控制通过 OMXPlayer 播放的视频:
omxplayer filename.mp4
您可以通过使用 -k
参数来获取 omxplayer
支持的键列表。
将定义events
列表的行替换为新的键映射,并通过以下代码将不同的映射分配给事件:
events_dpad = (uinput.KEY_UP,uinput.KEY_DOWN,uinput.KEY_LEFT,
uinput.KEY_RIGHT,uinput.KEY_ENTER,uinput.KEY_ENTER)
events_z80 = (uinput.KEY_Q,uinput.KEY_A,uinput.KEY_O,
uinput.KEY_P,uinput.KEY_M,uinput.KEY_ENTER)
events_omx = (uinput.KEY_EQUAL,uinput.KEY_MINUS,uinput.KEY_LEFT,
uinput.KEY_RIGHT,uinput.KEY_P,uinput.KEY_Q)
你可以在input.h
文件中找到所有的KEY
定义;你可以使用less
命令查看它(按Q键退出),如下所示命令:
less /usr/include/linux/input.h
模拟鼠标事件
uinput
库可以模拟鼠标和游戏手柄事件,以及键盘按键。要使用按钮模拟鼠标,我们可以调整脚本以使用鼠标事件(以及定义 mousemove
来设置移动的步长)使用以下代码:
MSG = ["M_UP","M_DOWN","M_LEFT","M_RIGHT","1","Enter"]
events_mouse=(uinput.REL_Y,uinput.REL_Y, uinput.REL_X,
uinput.REL_X,uinput.BTN_LEFT,uinput.BTN_RIGHT)
mousemove=1
我们还需要修改按钮处理以提供连续移动,因为我们不需要跟踪鼠标按键的状态。为此,请使用以下代码:
#Perform the button presses/releases
#(but only change state once)
for idx, val in enumerate(btn_state):
if MSG[idx] == "M_UP" or MSG[idx] == "M_LEFT":
state = -mousemove
else:
state = mousemove
if val == True:
device.emit(events[idx], state) # Press.
elif val == False:
device.emit(events[idx], 0) # Release.
time.sleep(0.01)
多路复用彩色 LED
本章的下一个示例演示了,如果用软件控制,一些看似简单的硬件可以产生一些令人印象深刻的结果。为此,我们将回到使用 RGB LED。我们将使用五个 RGB LED,它们被连接得只需要使用八个 GPIO 引脚,通过一种称为硬件复用的方法来控制它们的红、绿和蓝元素(参见本食谱“更多内容...”部分的硬件复用小节)。
准备就绪
你将需要以下图片中所示的 RGB LED 模块:
来自 PiHardware.com 的 RGB LED 模块
如您在前面的照片中所见,来自pihardware.com/
的 RGB LED 模块配备了 GPIO 引脚和一根杜邦公对公电缆,用于连接它。尽管有两个从 1 到 5 标记的引脚组,但只需连接其中一边即可。
或者,你可以使用以下电路来重新创建自己的电路,该电路包括五个常见的负极 RGB LED、3 个 470 欧姆电阻和一个 Vero 原型板(或大型面包板)。电路将如下图中所示:
RGB LED 模块的电路图
严格来说,在这个电路中我们应该使用 15 个电阻(每个 RGB LED 元件一个),这样可以避免 LED 共享同一个电阻时产生的干扰,并且如果同时开启,还能延长 LED 本身的使用寿命。然而,使用这种方法的优势微乎其微,尤其是考虑到我们打算独立驱动每个 RGB LED,以实现多彩效果。
您需要按照以下方式将电路连接到树莓派的 GPIO 引脚头:
RGB LED | 1 | 2 | 3 | 4 | |||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
树莓派 GPIO 引脚 | 2 | 4 | 6 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 |
树莓派 GPIO 引脚 | 1 | 3 | 5 | 7 | 9 | 11 | 13 | 15 | 17 | 19 | 21 | 23 | 25 | 27 | 29 | 31 | 33 | 35 | 37 |
RGB LED | 5 | R | G | B |
如何做到这一点...
创建 rgbled.py
脚本并执行以下步骤:
- 导入所有必需的模块并使用以下代码定义要使用的值:
#!/usr/bin/python3
#rgbled.py
import time
import RPi.GPIO as GPIO
#Setup Active states
#Common Cathode RGB-LEDs (Cathode=Active Low)
LED_ENABLE = 0; LED_DISABLE = 1
RGB_ENABLE = 1; RGB_DISABLE = 0
#HARDWARE SETUP
# GPIO
# 2[=====1=23=4==]26[=======]40
# 1[===5=RGB=====]25[=======]39
#LED CONFIG - Set GPIO Ports
LED1 = 12; LED2 = 16; LED3 = 18; LED4 = 22; LED5 = 7
LED = [LED1,LED2,LED3,LED4,LED5]
RGB_RED = 11; RGB_GREEN = 13; RGB_BLUE = 15
RGB = [RGB_RED,RGB_GREEN,RGB_BLUE]
#Mixed Colors
RGB_CYAN = [RGB_GREEN,RGB_BLUE]
RGB_MAGENTA = [RGB_RED,RGB_BLUE]
RGB_YELLOW = [RGB_RED,RGB_GREEN]
RGB_WHITE = [RGB_RED,RGB_GREEN,RGB_BLUE]
RGB_LIST = [RGB_RED,RGB_GREEN,RGB_BLUE,RGB_CYAN,
RGB_MAGENTA,RGB_YELLOW,RGB_WHITE]
- 定义函数以设置 GPIO 引脚,请使用以下代码:
def led_setup():
'''Setup the RGB-LED module pins and state.'''
#Set up the wiring
GPIO.setmode(GPIO.BOARD)
# Setup Ports
for val in LED:
GPIO.setup(val, GPIO.OUT)
for val in RGB:
GPIO.setup(val, GPIO.OUT)
led_clear()
- 定义我们的效用函数以帮助控制 LED,使用以下代码:
def led_gpiocontrol(pins,state):
'''This function will control the state of
a single or multiple pins in a list.'''
#determine if "pins" is a single integer or not
if isinstance(pins,int):
#Single integer - reference directly
GPIO.output(pins,state)
else:
#if not, then cycle through the "pins" list
for i in pins:
GPIO.output(i,state)
def led_activate(led,color):
'''Enable the selected led(s) and set the required color(s)
Will accept single or multiple values'''
#Enable led
led_gpiocontrol(led,LED_ENABLE)
#Enable color
led_gpiocontrol(color,RGB_ENABLE)
def led_deactivate(led,color):
'''Deactivate the selected led(s) and set the required
color(s) will accept single or multiple values'''
#Disable led
led_gpiocontrol(led,LED_DISABLE)
#Disable color
led_gpiocontrol(color,RGB_DISABLE)
def led_time(led, color, timeon):
'''Switch on the led and color for the timeon period'''
led_activate(led,color)
time.sleep(timeon)
led_deactivate(led,color)
def led_clear():
'''Set the pins to default state.'''
for val in LED:
GPIO.output(val, LED_DISABLE)
for val in RGB:
GPIO.output(val, RGB_DISABLE)
def led_cleanup():
'''Reset pins to default state and release GPIO'''
led_clear()
GPIO.cleanup()
- 创建一个测试函数以展示模块的功能:
def main():
'''Directly run test function.
This function will run if the file is executed directly'''
led_setup()
led_time(LED1,RGB_RED,5)
led_time(LED2,RGB_GREEN,5)
led_time(LED3,RGB_BLUE,5)
led_time(LED,RGB_MAGENTA,2)
led_time(LED,RGB_YELLOW,2)
led_time(LED,RGB_CYAN,2)
if __name__=='__main__':
try:
main()
finally:
led_cleanup()
#End
它是如何工作的...
首先,我们通过定义根据所使用的 RGB LED(共阴极)类型所需的启用和禁用LED 的状态来定义硬件设置。如果你使用的是共阳极设备,只需反转启用和禁用状态。
接下来,我们定义 GPIO 到引脚的映射,以匹配我们之前所做的布线。
我们还通过将红色、绿色和/或蓝色组合在一起来定义一些基本的颜色组合,如下所示:
LED 颜色组合
我们定义了一系列有用的函数,第一个是 led_setup()
,它将设置 GPIO 编号为 GPIO.BOARD
并定义所有要作为输出使用的引脚。我们还调用了一个名为 led_clear()
的函数,该函数将引脚设置为默认状态,所有引脚均被禁用。
这意味着 LED 引脚 1-5(每个 LED 的公共阴极)被设置为HIGH
,而 RGB 引脚(每个颜色的独立阳极)被设置为LOW
。
我们创建了一个名为 led_gpiocontrol()
的函数,它将允许我们设置一个或多个引脚的状态。isinstance()
函数使我们能够测试一个值以查看它是否与特定类型匹配(在这种情况下,一个单独的整数);然后我们可以设置该单个引脚的状态,或者遍历引脚列表并设置每个引脚。
接下来,我们定义了两个函数,led_activate()
和 led_deactivate()
,它们将启用和禁用指定的 LED 和颜色。最后,我们定义了 led_time()
函数,它将允许我们指定一个 LED、颜色和时间来开启它。
我们还创建了led_cleanup()
函数来将引脚(和 LED)重置为默认值,并调用GPIO.cleanup()
来释放正在使用的 GPIO 引脚。
此脚本旨在成为一个库文件,因此我们将使用if __name__=='__main__'
检查来确保仅在直接运行文件时执行我们的测试代码:
通过检查__name__
的值,我们可以确定该文件是直接运行(它将等于__main__
)还是被另一个 Python 脚本导入。
这允许我们定义一个特殊的测试代码,该代码仅在直接加载并运行文件时执行。如果我们将此文件作为模块包含在其他脚本中,那么此代码将不会执行。
如前所述,我们将使用 try
/finally
来确保我们总是能够执行清理操作,即使我们在早期退出。
为了测试脚本,我们将依次设置 LED 灯以显示不同的颜色。
还有更多...
我们可以通过同时打开一个或多个 RGB LED 的部分来创建几种不同的颜色。然而,通过一些巧妙的编程,我们可以创建出整个光谱的颜色。此外,我们还可以在每个 LED 上显示不同的颜色,看起来像是同时进行的。
硬件复用
一个 LED 需要在阳极侧施加高电压,在阴极侧施加低电压才能点亮。电路中使用的 RGB LED 是共阴极的,因此我们必须在 RGB 引脚上施加高电压(3V3),在阴极引脚上施加低电压(0V),这些阴极引脚(每个 LED 的 1 到 5 号引脚)都连接在一起。
阴极和 RGB 引脚状态如下:
阴极和 RGB 引脚状态
因此,我们可以启用一个或多个 RGB 引脚,同时仍然控制哪些 LED 灯被点亮。我们启用想要点亮的 LED 的引脚,并禁用不需要的引脚。这使我们能够使用比单独控制 15 条 RGB 线所需的引脚数量少得多。
显示随机图案
我们可以向我们的库中添加新的功能以产生不同的效果,例如生成随机颜色。以下函数使用 randint()
来获取介于 1 和颜色数量之间的值。我们忽略任何超过可用颜色数量的值,这样我们可以控制 LED 灯关闭的频率。执行以下步骤以添加所需的函数:
- 将
random
模块中的randint()
函数添加到rgbled.py
脚本中,使用以下代码:
from random import randint
- 现在添加
led_rgbrandom()
函数,使用以下代码:
def led_rgbrandom(led,period,colors):
''' Light up the selected led, for period in seconds,
in one of the possible colors. The colors can be
1 to 3 for RGB, or 1-6 for RGB plus combinations,
1-7 includes white. Anything over 7 will be set as
OFF (larger the number more chance of OFF).'''
value = randint(1,colors)
if value < len(RGB_LIST):
led_time(led,RGB_LIST[value-1],period)
-
在
main()
函数中使用以下命令创建一系列闪烁的 LED 灯:
for i in range(20):
for j in LED:
#Select from all, plus OFF
led_rgbrandom(j,0.1,20)
混合多种颜色
到目前为止,我们只在一或多个 LED 上一次显示一种颜色。如果你考虑电路是如何连接的,你可能会想知道我们如何让一个 LED 显示一种颜色,而另一个同时显示不同的颜色。简单的答案是,我们不需要这么做——我们只是做得很快!
我们需要做的只是每次显示一种颜色,但快速地交替变换,使得颜色看起来像是两种(甚至三种红色/绿色/蓝色 LED)的混合。幸运的是,像树莓派这样的计算机可以非常容易地做到这一点,甚至允许我们将 RGB 元素组合起来,在所有五个 LED 上制作出多种颜色的阴影。执行以下步骤来混合颜色:
- 在
rgbled.py
脚本的顶部添加组合颜色定义,在混合颜色定义之后,使用以下代码:
#Combo Colors
RGB_AQUA = [RGB_CYAN,RGB_GREEN]
RGB_LBLUE = [RGB_CYAN,RGB_BLUE]
RGB_PINK = [RGB_MAGENTA,RGB_RED]
RGB_PURPLE = [RGB_MAGENTA,RGB_BLUE]
RGB_ORANGE = [RGB_YELLOW,RGB_RED]
RGB_LIME = [RGB_YELLOW,RGB_GREEN]
RGB_COLORS = [RGB_LIME,RGB_YELLOW,RGB_ORANGE,RGB_RED,
RGB_PINK,RGB_MAGENTA,RGB_PURPLE,RGB_BLUE,
RGB_LBLUE,RGB_CYAN,RGB_AQUA,RGB_GREEN]
上述代码将提供创建我们阴影所需的颜色组合,其中RGB_COLORS
提供了通过阴影的平滑过渡。
- 接下来,我们需要创建一个名为
led_combo()
的函数来处理单色或多色。该函数的代码如下:
def led_combo(pins,colors,period):
#determine if "colors" is a single integer or not
if isinstance(colors,int):
#Single integer - reference directly
led_time(pins,colors,period)
else:
#if not, then cycle through the "colors" list
for i in colors:
led_time(pins,i,period)
- 现在我们可以创建一个新的脚本,
rgbledrainbow.py
,以利用我们rgbled.py
模块中的新功能。rgbledrainbow.py
脚本将如下所示:
#!/usr/bin/python3
#rgbledrainbow.py
import time
import rgbled as RGBLED
def next_value(number,max):
number = number % max
return number
def main():
print ("Setup the RGB module")
RGBLED.led_setup()
# Multiple LEDs with different Colors
print ("Switch on Rainbow")
led_num = 0
col_num = 0
for l in range(5):
print ("Cycle LEDs")
for k in range(100):
#Set the starting point for the next set of colors
col_num = next_value(col_num+1,len(RGBLED.RGB_COLORS))
for i in range(20): #cycle time
for j in range(5): #led cycle
led_num = next_value(j,len(RGBLED.LED))
led_color = next_value(col_num+led_num,
len(RGBLED.RGB_COLORS))
RGBLED.led_combo(RGBLED.LED[led_num],
RGBLED.RGB_COLORS[led_color],0.001)
print ("Cycle COLORs")
for k in range(100):
#Set the next color
col_num = next_value(col_num+1,len(RGBLED.RGB_COLORS))
for i in range(20): #cycle time
for j in range(5): #led cycle
led_num = next_value(j,len(RGBLED.LED))
RGBLED.led_combo(RGBLED.LED[led_num],
RGBLED.RGB_COLORS[col_num],0.001)
print ("Finished")
if __name__=='__main__':
try:
main()
finally:
RGBLED.led_cleanup()
#End
main()
函数首先会遍历 LED,将 RGB_COLORS
数组中的每种颜色设置到所有 LED 上。然后,它会遍历这些颜色,在 LED 上创建出彩虹效果:
在五个 RGB LED 上循环多种颜色
使用视觉暂留原理编写信息
视觉持久性(POV)显示屏可以产生几乎神奇的效果,通过快速前后移动或绕圈移动一串 LED 灯,在空中显示图像。这种效果之所以有效,是因为你的眼睛无法快速调整以分离出单个的光闪,因此你观察到的是一个合并的图像(显示的消息或图片):
使用 RGB LED 实现视觉持久性
准备就绪
这个菜谱使用了之前菜谱中使用的 RGB LED 套件;你还需要以下额外的物品:
-
面板(半尺寸或更大)
-
2 x 杜邦公对母贴片线
-
倾斜开关(球轴承类型适用)
-
1 x 470 欧姆电阻 (R_Protect)
-
面板线(实心芯)
倾斜开关应该添加到 RGB LED(如多路复用彩色 LED配方中的准备就绪部分所述)。倾斜开关的接线方式如下:
倾斜开关连接到 GPIO 输入(GPIO 引脚 24)和地(GPIO 引脚 6)
要重现 POV 图像,你需要能够快速移动 LED 灯并来回倾斜开关。注意倾斜开关是如何以角度安装在侧面的,因此当向左移动时开关会打开。建议将硬件安装在一根木头或类似的设备上。你甚至可以使用便携式 USB 电池组和 Wi-Fi 适配器,通过远程连接来供电和控制 Raspberry Pi(有关详细信息,请参阅第一章中的通过 SSH(以及 X11 转发)在网络中远程连接到 Raspberry Pi配方,使用 Raspberry Pi 3 计算机入门):
视觉持久性硬件设置
您还需要完成的 rgbled.py
文件,我们将在 如何操作... 部分对其进行进一步扩展。
如何做到这一点...
- 创建一个名为
tilt.py
的脚本以报告倾斜开关的状态:
#!/usr/bin/python3
#tilt.py
import RPi.GPIO as GPIO
#HARDWARE SETUP
# GPIO
# 2[===========T=]26[=======]40
# 1[=============]25[=======]39
#Tilt Config
TILT_SW = 24
def tilt_setup():
#Setup the wiring
GPIO.setmode(GPIO.BOARD)
#Setup Ports
GPIO.setup(TILT_SW,GPIO.IN,pull_up_down=GPIO.PUD_UP)
def tilt_moving():
#Report the state of the Tilt Switch
return GPIO.input(TILT_SW)
def main():
import time
tilt_setup()
while True:
print("TILT %s"% (GPIO.input(TILT_SW)))
time.sleep(0.1)
if __name__=='__main__':
try:
main()
finally:
GPIO.cleanup()
print("Closed Everything. END")
#End
- 您可以通过以下命令直接运行脚本来测试它:
sudo python3 tilt.py
- 将以下
rgbled_pov()
函数添加到我们之前创建的rgbled.py
脚本中;这将使我们能够显示图像的单行:
def rgbled_pov(led_pattern,color,ontime):
'''Disable all the LEDs and re-enable the LED pattern in the required color'''
led_deactivate(LED,RGB)
for led_num,col_num in enumerate(led_pattern):
if col_num >= 1:
led_activate(LED[led_num],color)
time.sleep(ontime)
- 我们现在将创建以下文件,名为
rgbledmessage.py
,以执行显示我们消息所需的操作。首先,我们将导入所需的模块:更新的rgbled
模块、新的tilt
模块,以及 Python 的os
模块。最初,我们将DEBUG
设置为True
,这样在脚本运行时 Python 终端将显示额外的信息:
#!/usr/bin/python3
# rgbledmessage.py
import rgbled as RGBLED
import tilt as TILT
import os
DEBUG = True
- 添加一个
readMessageFile()
函数来读取letters.txt
文件的内容,然后添加processFileContent()
函数来生成每个字母的 LED 模式Python 字典:
def readMessageFile(filename):
assert os.path.exists(filename), 'Cannot find the message file: %s' % (filename)
try:
with open(filename, 'r') as theFile:
fileContent = theFile.readlines()
except IOError:
print("Unable to open %s" % (filename))
if DEBUG:print ("File Content START:")
if DEBUG:print (fileContent)
if DEBUG:print ("File Content END")
dictionary = processFileContent(fileContent)
return dictionary
def processFileContent(content):
letterIndex = [] #Will contain a list of letters stored in the file
letterList = [] #Will contain a list of letter formats
letterFormat = [] #Will contain the format of each letter
firstLetter = True
nextLetter = False
LETTERDIC={}
#Process each line that was in the file
for line in content:
# Ignore the # as comments
if '#' in line:
if DEBUG:print ("Comment: %s"%line)
#Check for " in the line = index name
elif '"' in line:
nextLetter = True
line = line.replace('"','') #Remove " characters
LETTER=line.rstrip()
if DEBUG:print ("Index: %s"%line)
#Remaining lines are formatting codes
else:
#Skip firstLetter until complete
if firstLetter:
firstLetter = False
nextLetter = False
lastLetter = LETTER
#Move to next letter if needed
if nextLetter:
nextLetter = False
LETTERDIC[lastLetter]=letterFormat[:]
letterFormat[:] = []
lastLetter = LETTER
#Save the format data
values = line.rstrip().split(' ')
row = []
for val in values:
row.append(int(val))
letterFormat.append(row)
LETTERDIC[lastLetter]=letterFormat[:]
#Show letter patterns for debugging
if DEBUG:print ("LETTERDIC: %s" %LETTERDIC)
if DEBUG:print ("C: %s"%LETTERDIC['C'])
if DEBUG:print ("O: %s"%LETTERDIC['O'])
return LETTERDIC
- 添加一个
createBuffer()
函数,该函数将消息转换为一系列代表每个字母的 LED 图案(假设字母由letters.txt
文件定义):
def createBuffer(message,dictionary):
buffer=[]
for letter in message:
try:
letterPattern=dictionary[letter]
except KeyError:
if DEBUG:print("Unknown letter %s: use _"%letter)
letterPattern=dictionary['_']
buffer=addLetter(letterPattern,buffer)
if DEBUG:print("Buffer: %s"%buffer)
return buffer
def addLetter(letter,buffer):
for row in letter:
buffer.append(row)
buffer.append([0,0,0,0,0])
buffer.append([0,0,0,0,0])
return buffer
- 接下来,我们定义一个
displayBuffer()
函数,用于使用rgbled_pov()
函数在rgbled
模块中显示 LED 图案:
def displayBuffer(buffer):
position=0
while(1):
if(TILT.tilt_moving()==False):
position=0
elif (position+1)<len(buffer):
position+=1
if DEBUG:print("Pos:%s ROW:%s"%(position,buffer[position]))
RGBLED.rgbled_pov(buffer[position],RGBLED.RGB_GREEN,0.001)
RGBLED.rgbled_pov(buffer[position],RGBLED.RGB_BLUE,0.001)
-
最后,我们创建一个
main()
函数来执行每个所需的步骤:-
设置硬件组件(RGB LED 灯和倾斜开关)。
-
读取
letters.txt
文件。 -
定义 LED 字母图案的字典。
-
生成一个缓冲区来表示所需的消息。
-
使用
rgbled
模块显示缓冲区,并用tilt
模块控制它:
-
def main():
RGBLED.led_setup()
TILT.tilt_setup()
dict=readMessageFile('letters.txt')
buffer=createBuffer('_COOKBOOK_',dict)
displayBuffer(buffer)
if __name__=='__main__':
try:
main()
finally:
RGBLED.led_cleanup()
print("Closed Everything. END")
#End
- 创建以下名为
letters.txt
的文件,用于定义显示示例消息'_COOKBOOK_'
所需的 LED 模式。请注意,此文件只需为消息中的每个唯一字母或符号定义一个模式:
#COOKBOOK
"C"
0 1 1 1 0
1 0 0 0 1
1 0 0 0 1
"O"
0 1 1 1 0
1 0 0 0 1
1 0 0 0 1
0 1 1 1 0
"K"
1 1 1 1 1
0 1 0 1 0
1 0 0 0 1
"B"
1 1 1 1 1
1 0 1 0 1
0 1 0 1 0
"_"
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
它是如何工作的...
第一个函数 readMessageFile()
将会打开并读取指定文件的內容。然后,它会使用 processFileContent()
函数来返回一个包含文件中定义的字母对应模式的 Python 字典。文件中的每一行都会被处理,忽略包含 #
字符的行,并检查 "
字符以指示随后的 LED 模式的名称。在文件被处理后,我们最终得到一个包含 '_'
、'C'
、'B'
、'K'
和 'O'
字符的 LED 模式的 Python 字典:
'_': [[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]
'C': [[0, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1]]
'B': [[1, 1, 1, 1, 1], [1, 0, 1, 0, 1], [0, 1, 0, 1, 0]]
'K': [[1, 1, 1, 1, 1], [0, 1, 0, 1, 0], [1, 0, 0, 0, 1]]
'O': [[0, 1, 1, 1, 0], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [0, 1, 1, 1, 0]]
现在我们有了一组可供选择的字母,我们可以使用createBuffer()
函数来创建一个 LED 图案序列。正如其名所示,该函数将通过逐个查找消息中的每个字母,并逐行添加相关的图案行来构建一个 LED 图案缓冲区。如果一个字母在字典中找不到,那么将使用空格代替。
最后,我们现在有一份准备显示的 LED 图案列表。为了控制何时开始序列,我们将使用 TILT 模块并检查倾斜开关的状态:
旋转开关在未移动(左侧)和移动(右侧)时的位置
倾斜开关由一个装在空心、绝缘圆柱体内的微型球轴承组成;当球轴承位于圆柱体底部时,两个引脚之间的连接是闭合的。当球轴承移动到圆柱体的另一端,与引脚失去接触时,倾斜开关处于开启状态:
开关关闭和开关打开的倾斜开关电路
之前展示的倾斜开关电路将允许当开关关闭时,GPIO 引脚 24 连接到地。然后,如果我们读取该引脚,当它处于静止状态时,它将返回False
。通过将 GPIO 引脚设置为输入并启用内部上拉电阻,当倾斜开关打开时,它将报告True
。
如果倾斜开关处于开启状态(报告True
),那么我们将假设设备正在移动,并开始显示 LED 序列,每次显示 LED 图案的一行时,都会增加当前位置。为了使图案更加多彩(因为我们能这么做!),我们将每一行重复一遍,用另一种颜色。一旦TILT.tilt_moving()
函数报告我们已经停止移动或者正在向相反方向移动,我们将重置当前位置,准备从头开始整个图案:
消息通过 RGB LED 显示 - 这里,我们正在一起使用绿色和蓝色
当 RGB LED 模块和倾斜开关来回移动时,我们应该看到空中显示的信息!
尝试使用不同的颜色组合、速度和手臂摆动幅度进行实验,看看你能产生什么效果。你甚至可以在轮子上创建一个类似的设置,以产生连续的 POV 效果。
第十章:感知与显示真实世界数据
在本章中,我们将涵盖以下主题:
-
使用带有 I²C 总线的设备
-
使用模数转换器读取模拟数据
-
记录和绘制数据
-
使用 I/O 扩展器扩展树莓派 GPIO
-
在 SQLite 数据库中捕获数据
-
查看来自您自己的网络服务器的数据
-
感知并发送数据到在线服务
简介
在本章中,我们将学习如何从现实世界收集模拟数据,并对其进行处理,以便我们能够显示、记录、绘图和共享数据,并在我们的程序中利用它。
我们将通过与模拟-数字转换器(ADCs)、LCD 数码显示屏和数字端口扩展器使用 Raspberry Pi 的 GPIO 连接来扩展 Raspberry Pi 的功能。
一定要查看附录,硬件和软件列表,其中列出了本章中使用的所有物品以及您可以从哪里获取它们。
使用带有 I2C 总线的设备
树莓派可以支持多种高级协议,使得各种设备能够轻松连接。在本章中,我们将重点关注最常用的总线,称为I-squared-C(I²C)。它提供了一种中速总线,用于通过两根线与设备进行通信。在本节中,我们将使用 I²C 来与一个 8 位模数转换器(ADC)进行接口。该设备将测量一个模拟信号,将其转换为介于 0 到 255 之间的相对值,并将该值作为数字信号(由 8 位表示)通过 I²C 总线发送到树莓派。
I²C 的优势可以概括如下:
-
即使在总线上连接了众多设备,也能保持低引脚/信号数量
-
适应不同从属设备的需要
-
支持多主节点
-
集成 ACK/NACK 功能以改进错误处理
准备就绪
I²C 总线并非在所有树莓派镜像中都启用;因此,我们需要启用该模块并安装一些支持工具。Raspbian 的新版本使用 设备树 来处理硬件外设和驱动程序。
为了使用 I²C 总线,我们需要在 bootconfig.txt
文件中启用 ARM I²C。
您可以使用以下命令自动完成此操作:
sudo raspi-config
从菜单中选择高级选项,然后选择 I²C,如图下截图所示。当询问时,选择是以启用该接口,然后点击是默认加载模块:
Raspberry Pi 配置菜单
从菜单中选择 I2C,然后选择是来启用接口并加载模块
默认情况下。
raspi-config
程序通过修改 /boot/config.txt
文件以包含 dtparam=i2c_arm=on
来启用 I2C_ARM
接口。其他总线(I2C_VC)通常保留用于与 Raspberry Pi HAT 扩展板进行接口连接(从板载存储设备读取配置信息);然而,您可以使用 dtparam=i2c_vc=on
来启用此功能。
如果您愿意,也可以通过raspi-config
列表启用 SPI,这是一种另一种类型的总线(我们将在第十三章,与技术接口)中更详细地探讨这一点)。
接下来,我们应该在启动树莓派时包含要加载的 I²C 模块,如下所示:
sudo nano /etc/modules
在单独的行上添加以下内容并保存(Ctrl + X, Y, Enter):
i2c-dev
i2c-bcm2708
同样,我们也可以通过添加 spi-bcm2708
来启用 SPI 模块。
接下来,我们将安装一些工具,以便可以直接从命令行使用 I²C 设备,如下所示:
sudo apt-get update
sudo apt-get install i2c-tools
最后,在连接硬件之前关闭 Raspberry Pi,以便应用更改,具体操作如下:
sudo halt
您需要一个 PCF8591 模块(这些模块的零售商列在附录中,硬件和软件列表),或者您可以单独获取 PCF8591 芯片并自行搭建电路(有关电路的详细信息,请参阅更多内容...部分):
来自 dx.com 的 PCF8591 模数转换器(ADC)和传感器模块
将 GND、VCC、SDA 和 SCL 引脚按照以下方式连接到树莓派 GPIO 引脚头上:
树莓派 GPIO 引脚上的 I2C 连接
您可以通过研究设备的规格说明书来找到需要发送/读取的消息以及用于控制您的设备的寄存器,从而使用相同的 I²C 工具/代码与其他 I²C 设备进行操作。
如何做到这一点...
i2cdetect
命令用于检测 I²C 设备(--y
选项跳过任何关于可能与其他硬件(可能连接到 I²C 总线)发生干扰的警告)。以下命令用于扫描两个总线:
sudo i2cdetect -y 0
sudo i2cdetect -y 1
- 根据您的树莓派板型修订版,设备的地址应列在总线 0(适用于 B 型 Rev1 板)或总线 1(适用于树莓派 2 和 3,以及树莓派 1 Model A 和 Model B 修订版 2)。默认情况下,PCF8591 的地址是
0x48
:
要使用的 I²C 总线编号 | 总线 00 | 总线 11 |
---|---|---|
树莓派 2 和 3 | HAT ID (I2C_VC) | GPIO (I2C_ARM) |
模型 A 和模型 B 修订版 2 | P5 | GPIO |
模型 B 修订版 1 | GPIO | N/A |
- 以下截图显示了
i2cdetect
的输出:
PCF8591 地址(48)在此处显示在总线 1 上
如果没有列出任何内容,请关闭并仔细检查您的连接(来自www.dx.com的 ADC 模块在供电时会点亮一个红色 LED)。
如果你收到一个错误信息,指出 /dev/i2c1
总线不存在,你可以执行以下检查:
- 确保文件
/etc/modprobe.d/raspi-blacklist.conf
为空(即,模块没有被列入黑名单),可以使用以下命令查看该文件:
sudo nano /etc/modprobe.d/raspi-blacklist.conf
-
如果文件中存在任何内容(例如
blacklist i2c-bcm2708
),请将其删除并保存 -
检查
/boot/config
文件,确保其中没有包含device_tree_param=
的行(这将禁用对新设备树配置的支持,并禁用对某些 Raspberry Pi HAT 扩展板的兼容性支持) -
使用
lsmod
检查模块是否已加载,并查找i2c-bcm2708
和i2c_dev
- 使用检测到的总线编号(
0
或1
)和设备地址(0x48
),使用i2cget
从设备中读取数据(在电源开启或通道更改后,您需要读取设备两次以查看最新值),如下所示:
sudo i2cget -y 1 0x48
sudo i2cget -y 1 0x48
- 要从通道
1
(这是模块上的温度传感器)读取,我们可以使用i2cset
将0x01
写入 PCF8591 控制寄存器。再次,使用两次读取来从通道1
获取新的样本,如下所示:
sudo i2cset -y 1 0x48 0x01
sudo i2cget -y 1 0x48
sudo i2cget -y 1 0x48
- 要循环遍历每个输入通道,请使用
i2cset
将控制寄存器设置为0x04
,具体操作如下:
sudo i2cset -y 1 0x48 0x04
- 我们也可以使用以下命令来控制 AOUT 引脚,将其完全打开(点亮 LED D1):
sudo i2cset -y 1 0x48 0x40 0xff
- 最后,我们可以使用以下命令将其完全关闭(关闭 LED D1):
sudo i2cset -y 1 0x48 0x40 0x00
它是如何工作的...
设备开启后的第一次读取将返回 0x80
,并触发从通道 0 的新样本。如果你再次读取,它将返回之前读取的样本并生成一个新的样本。每次读取都将是一个 8 位值(范围从 0
到 255
),代表电压到 VCC(在这种情况下,0 V 到 3.3 V)。在 www.dx.com 模块上,通道 0 连接到一个光传感器,因此如果你用手遮住模块并重新发送命令,你会观察到值的变化(变暗表示更高的值,变亮表示更低的值)。你会发现读取值总是落后一步;这是因为,当它返回之前的样本时,它会捕获下一个样本。
我们使用以下命令来指定要读取的特定频道:
sudo i2cset -y 1 0x48 0x01
这将读取通道更改为通道 1(在模块上标记为AIN1)。记住,你需要在看到新选择通道的数据之前执行两次读取。以下表格显示了通道和引脚名称,以及哪些跳线连接器启用/禁用每个传感器:
通道 | 0 | 1 | 2 | 3 |
---|---|---|---|---|
引脚名称 | AIN0 | AIN1 | AIN2 | AIN3 |
传感器 | 光敏电阻 | 热敏电阻 | 外部引脚 | 滑动变阻器 |
跳跃者 | P5 | P4 | P6 |
接下来,我们通过设置控制寄存器的模拟输出使能标志(位 6)来控制 AOUT 引脚,并使用下一个值来设置模拟电压(0V-3.3V,0x00-0xFF),具体如下:
sudo i2cset -y 1 0x48 0x40 0xff
最后,你可以将位 2(0x04
)设置为自动递增并循环遍历输入通道,如下所示:
sudo i2cset -y 1 0x48 0x04
每次运行 i2cget -y 1 0x48
,下一个通道将被选中,从通道 AIN0 开始,然后运行到 AIN1,再到 AIN3,最后回到 AIN0。
要了解如何在数值中设置特定的位,查看该数字的二进制表示形式会有所帮助。8 位值0x04
可以写作二进制b0000 0100
(0x
表示该值是以十六进制(或十六进制)形式书写的,而 b 表示这是一个二进制数)。
二进制数中的位从右到左计数,从 0 开始 - 也就是说,最高有效位(MSB)7 6 5 4 3 2 1 0 最低有效位(LSB)。
第 7 位被称为最高有效位(MSB),而第 0 位被称为最低有效位(LSB)。因此,通过设置第 2 位,我们得到b0000 0100
(即0x04
)。
还有更多...
I²C 总线使我们能够仅使用几根线就轻松连接多个设备。PCF8591 芯片可用于将您自己的传感器连接到模块或仅仅是芯片。
使用多个 I2C 设备
I²C 总线上的所有命令都是针对特定的 I²C 设备(许多设备有选项设置一些引脚为高电平或低电平以选择额外的地址,并允许多个设备存在于同一总线上)。每个设备都必须有一个唯一的地址,这样在任何时候只有一个设备会响应。PCF8591 的起始地址是0x48
,通过三个地址引脚可选择到0x4F
。这允许在同一总线上使用多达八个 PCF8591 设备。
如果你决定使用位于 GPIO 引脚 27 和 28(或在 A 型和 B 型 2 修订版设备上的 P5 引脚)的 I2C_VC 总线,你可能需要在 I²C 线和 3.3V 之间添加一个 1k8 欧姆的上拉电阻。这些电阻已经在 GPIO 连接器上的 I²C 总线上存在。然而,一些 I²C 模块,包括 PCF8591 模块,已经配备了它们自己的电阻,因此无需额外的电阻即可工作。
I2C 总线与电平转换
I²C 总线由两根线组成,一根是数据线(SDA),另一根是时钟线(SCL)。这两根线都通过上拉电阻被动地拉到 VCC(在树莓派上,这是 3.3 V)。树莓派将通过在每个周期中将时钟线拉低来控制时钟,数据线可以通过树莓派拉低来发送命令,或者由连接的设备拉低以响应数据:
树莓派的 I²C 引脚包括 SDA 和 SCL 上的上拉电阻
由于从设备只能将数据线拉至GND,因此设备可以在没有风险地驱动 GPIO 引脚过高(记住树莓派的 GPIO 无法处理超过 3.3 V 的电压)的情况下,由 3.3 V 甚至 5 V 供电。只要设备的 I²C 总线能够识别 3.3 V 的逻辑最大值而不是 5 V,这应该就可以工作。I²C 设备不得配备自己的上拉电阻,因为这会导致 GPIO 引脚被拉至 I²C 设备的供电电压。
注意到本章中使用的 PCF8591 模块已配备电阻;因此,我们只能使用VCC = 3V3。可以使用双向逻辑电平转换器来解决任何逻辑电平问题。以下图像展示了一个这样的设备,即Adafruit I²C 双向逻辑电平转换模块:
Adafruit I²C 双向电平转换模块
除了确保您所使用的设备中任何逻辑电压都处于合适的水平之外,它还将允许总线通过更长的线缆进行扩展(电平转换器还将充当总线中继器)。
仅使用 PCF8591 芯片或添加替代传感器
如下图中所示为 PCF8591 模块不带传感器的电路图:
PCF8591 模块不带传感器连接的电路图
如您所见,除了传感器外,只有五个额外的组件。我们有一个电源滤波电容(C1)和一个带有限流电阻(R5)的电源指示 LED(D2),所有这些都是可选的。
注意,该模块包含两个 10K 上拉电阻(R8 和 R9)用于 SCL 和 SDA 信号。然而,由于树莓派的 GPIO I²C 连接也包含上拉电阻,因此在该模块上不需要这些电阻(并且可以移除)。这也意味着我们应仅将此模块连接到 VCC = 3.3 V(如果我们使用 5 V,那么 SCL 和 SDA 上的电压将约为 3.56 V,这对树莓派的 GPIO 引脚来说太高了)。
PCF891 模块上的传感器都是电阻式的,因此随着传感器电阻的变化,模拟输入上的电压水平将在GND和VCC之间变化:
一个电位分压器电路。这提供与传感器电阻成比例的电压。
该模块使用一个称为分压器的电路。顶部的电阻平衡底部传感器的电阻,以提供一个介于VCC和GND之间的电压。
分压器输出电压(V[out])的计算公式如下:
R[t]和 R[b]分别是顶部和底部的电阻值,VCC 是供电电压。
该模块中的电位器电阻为 10K 欧姆,上下两端之间分配,具体取决于调节器的位置。因此,在中点位置,每侧有 5K 欧姆,输出电压为 1.65V;四分之一位置(顺时针方向),我们有 2.5K 欧姆和 7.5K 欧姆,产生 0.825V。
我还没有展示 AOUT 电路,它是一个电阻和 LED。然而,正如你将发现的,LED 并不适合用来指示模拟输出(除了显示开关状态)。
对于更敏感的电路,你可以使用更复杂的电路,例如一个惠斯通电桥(它允许检测电阻的非常小的变化),或者你可以使用基于其读数输出模拟电压的专用传感器(例如TMP36温度传感器)。PCF891 还支持差分输入模式,其中一个通道的输入可以与另一个通道的输入进行比较(最终的读数将是两者之间的差值)。
关于 PCF8591 芯片的更多信息,请参阅www.nxp.com/documents/data_sheet/PCF8591.pdf
的数据表。
使用模数转换器读取模拟数据
I²C 工具(在上一节中使用)在命令行中调试 I²C 设备非常有用,但它们在 Python 中使用并不实用,因为它们会非常慢,并且使用时需要大量的开销。幸运的是,有几个 Python 库提供了 I²C 支持,允许高效地使用 I²C 与连接的设备进行通信,并提供简便的操作。
我们将使用这样一个库来创建我们自己的 Python 模块,该模块将使我们能够快速轻松地从 ADC 设备获取数据,并在我们的程序中使用它。该模块设计得如此之好,以至于其他硬件或数据源可以替换它而不会影响剩余的示例。
准备就绪
要使用 Python 3 的 I²C 总线,我们将使用 戈登·亨德森 的 WiringPi2(更多详情请参阅 wiringpi.com/
)。
安装wiringpi2
最简单的方法是使用 Python 3 的pip
。pip
是 Python 的一个包管理器,其工作方式与apt-get
类似。您希望安装的任何包都将自动从在线仓库下载并安装。
要安装pip
,请使用以下命令:
sudo apt-get install python3-dev python3-pip
然后,使用以下命令安装 wiringpi2
:
sudo pip-3.2 install wiringpi2
安装完成后,你应该看到以下内容,表示成功:
成功安装了 WiringPi2
您需要将 PCF8591 模块按照之前在树莓派的 I²C 连接中使用的样子连接好:
PCF8591 模块与 Raspberry Pi GPIO 连接器的引脚连接
如何做到这一点...
在下一节中,我们将编写一个脚本,以便我们能够收集数据,然后在本章的后续部分使用这些数据。
创建以下脚本,data_adc.py
,如下所示:
- 首先,导入我们将使用的模块并创建变量,如下所示:
#!/usr/bin/env python3
#data_adc.py
import wiringpi2
import time
DEBUG=False
LIGHT=0;TEMP=1;EXT=2;POT=3
ADC_CH=[LIGHT,TEMP,EXT,POT]
ADC_ADR=0x48
ADC_CYCLE=0x04
BUS_GAP=0.25
DATANAME=["0:Light","1:Temperature",
"2:External","3:Potentiometer"]
- 创建一个带有构造函数以初始化它的
device
类,如下所示:
class device:
# Constructor:
def __init__(self,addr=ADC_ADR):
self.NAME = DATANAME
self.i2c = wiringpi2.I2C()
self.devADC=self.i2c.setup(addr)
pwrup = self.i2c.read(self.devADC) #flush powerup value
if DEBUG==True and pwrup!=-1:
print("ADC Ready")
self.i2c.read(self.devADC) #flush first value
time.sleep(BUS_GAP)
self.i2c.write(self.devADC,ADC_CYCLE)
time.sleep(BUS_GAP)
self.i2c.read(self.devADC) #flush first value
- 在类中,定义一个函数以提供频道名称列表,如下所示:
def getName(self):
return self.NAME
- 定义另一个函数(仍然作为类的一部分),用于返回来自 ADC 通道的新样本集,如下所示:
def getNew(self):
data=[]
for ch in ADC_CH:
time.sleep(BUS_GAP)
data.append(self.i2c.read(self.devADC))
return data
- 最后,在设备类之后,创建一个测试函数来测试我们的新
device
类,如下所示。这仅在脚本直接执行时运行:
def main():
ADC = device(ADC_ADR)
print (str(ADC.getName()))
for i in range(10):
dataValues = ADC.getNew()
print (str(dataValues))
time.sleep(1)
if __name__=='__main__':
main()
#End
您可以使用以下命令运行此模块的测试函数:
sudo python3 data_adc.py
它是如何工作的...
我们首先导入wiringpi2
,以便稍后与我们的 I²C 设备进行通信。我们将创建一个类来包含控制 ADC 所需的功能。当我们创建这个类时,我们可以初始化wiringpi2
,使其准备好使用 I²C 总线(使用wiringpi2.I2C()
),并且我们将使用芯片的总线地址设置一个通用的 I²C 设备(使用self.i2c.setup(0x48)
)。
wiringpi2
还有一个专门用于与 PCF8591 芯片配合使用的类;然而,在这种情况下,使用标准的 I²C 功能来展示如何使用 wiringpi2
控制任何 I²C 设备更为有用。通过参考设备数据表,你可以使用类似的命令与任何连接的 I²C 设备进行通信(无论它是否直接支持)。
如前所述,我们执行设备读取并配置 ADC 循环遍历通道,但不是使用i2cget
和i2cset
,而是使用I2C
对象的wiringpi2
读写函数。一旦初始化完成,设备将准备好读取每个通道上的模拟信号。
该类还将有两个成员函数。第一个函数,getName()
,返回一个频道名称列表(我们可以用它来关联我们的数据与其来源),第二个函数,getNew()
,返回所有频道的新数据集。数据是通过使用i2c.read()
函数从 ADC 读取的,并且由于我们已经将其置于循环模式,每次读取都将来自下一个频道。
由于我们计划稍后重用这个类,我们将使用 if __name__
测试来允许我们在直接执行文件时定义一段代码。在我们的 main()
函数中,我们创建了 ADC,它是我们新设备类的一个实例。如果我们需要,可以选择一个非默认地址;否则,将使用芯片的默认地址。我们使用 getName()
函数来打印出通道的名称,然后我们可以从 ADC
(使用 getNew()
)收集数据并显示它们。
还有更多...
以下允许我们在 data_adc.py
中定义设备类的替代版本,以便它可以替代 ADC 模块使用。这将使得本章剩余部分可以在不需要任何特定硬件的情况下进行尝试。
无硬件收集模拟数据
如果您没有可用的 ADC 模块,您可以从 Raspberry Pi 内部获取大量数据来替代使用。
创建 data_local.py
脚本如下:
#!/usr/bin/env python3
#data_local.py
import subprocess
from random import randint
import time
MEM_TOTAL=0
MEM_USED=1
MEM_FREE=2
MEM_OFFSET=7
DRIVE_USED=0
DRIVE_FREE=1
DRIVE_OFFSET=9
DEBUG=False
DATANAME=["CPU_Load","System_Temp","CPU_Frequency",
"Random","RAM_Total","RAM_Used","RAM_Free",
"Drive_Used","Drive_Free"]
def read_loadavg():
# function to read 1 minute load average from system uptime
value = subprocess.check_output(
["awk '{print $1}' /proc/loadavg"], shell=True)
return float(value)
def read_systemp():
# function to read current system temperature
value = subprocess.check_output(
["cat /sys/class/thermal/thermal_zone0/temp"],
shell=True)
return int(value)
def read_cpu():
# function to read current clock frequency
value = subprocess.check_output(
["cat /sys/devices/system/cpu/cpu0/cpufreq/"+
"scaling_cur_freq"], shell=True)
return int(value)
def read_rnd():
return randint(0,255)
def read_mem():
# function to read RAM info
value = subprocess.check_output(["free"], shell=True)
memory=[]
for val in value.split()[MEM_TOTAL+
MEM_OFFSET:MEM_FREE+
MEM_OFFSET+1]:
memory.append(int(val))
return(memory)
def read_drive():
# function to read drive info
value = subprocess.check_output(["df"], shell=True)
memory=[]
for val in value.split()[DRIVE_USED+
DRIVE_OFFSET:DRIVE_FREE+
DRIVE_OFFSET+1]:
memory.append(int(val))
return(memory)
class device:
# Constructor:
def __init__(self,addr=0):
self.NAME=DATANAME
def getName(self):
return self.NAME
def getNew(self):
data=[]
data.append(read_loadavg())
data.append(read_systemp())
data.append(read_cpu())
data.append(read_rnd())
memory_ram = read_mem()
data.append(memory_ram[MEM_TOTAL])
data.append(memory_ram[MEM_USED])
data.append(memory_ram[MEM_FREE])
memory_drive = read_drive()
data.append(memory_drive[DRIVE_USED])
data.append(memory_drive[DRIVE_FREE])
return data
def main():
LOCAL = device()
print (str(LOCAL.getName()))
for i in range(10):
dataValues = LOCAL.getNew()
print (str(dataValues))
time.sleep(1)
if __name__=='__main__':
main()
#End
上述脚本使我们能够使用以下命令从树莓派收集系统信息(subprocess
模块允许我们捕获结果并处理它们):
- CPU 速度:
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq
- CPU 负载:
awk '{print $1}' /proc/loadavg
- 核心温度(按 1,000 倍缩放):
cat /sys/class/thermal/thermal_zone0/temp
- 驱动信息:
df
- RAM 信息:
free
每个数据项都使用其中一个函数进行采样。在驱动器和 RAM 信息的情况下,我们将响应拆分为一个列表(由空格分隔)并选择我们想要监控的项目(例如可用内存和已用驱动器空间)。
这一切都被打包起来,以便与data_adc.py
文件和device
类以相同的方式工作(因此你可以在以下示例中选择使用其中之一,只需将data_adc
包含文件替换为data_local
即可)。
记录和绘制数据
现在我们能够采样和收集大量数据,因此能够捕捉和分析这些数据变得尤为重要。为此,我们将利用一个名为 matplotlib
的 Python 库,它包含了许多用于操作、绘图和分析数据的实用工具。我们将使用 pyplot
(它是 matplotlib
的一部分)来生成我们捕获数据的图表。有关 pyplot
的更多信息,请访问 matplotlib.org/users/pyplot_tutorial.html
。
这是一个用于 Python 的 MATLAB 风格数据可视化框架。
准备就绪
要使用 pyplot
,我们需要安装 matplotlib
。
由于matplotlib
安装程序的问题,使用pip-3.2
进行安装并不总是正确工作。以下方法将通过手动执行pip
所做的一切步骤来克服这个问题;然而,这可能需要超过 30 分钟才能完成。
为了节省时间,您可以尝试使用pip
安装,这会更快。如果不起作用,您可以使用前面提到的手动方法进行安装。
使用以下命令尝试使用 pip
安装 matplotlib
:
sudo apt-get install tk-dev python3-tk libpng-dev
sudo pip-3.2 install numpy
sudo pip-3.2 install matplotlib
您可以通过运行 python3
并尝试从 Python 终端导入它来确认 matplotlib
是否已安装,如下所示:
import matplotlib
如果安装失败,它将响应以下内容:
导入错误:没有名为 matplotlib 的模块
否则,将不会有错误。
使用以下步骤手动安装 matplotlib
:
- 按照以下步骤安装支持包:
sudo apt-get install tk-dev python3-tk python3-dev libpng-dev
sudo pip-3.2 install numpy
sudo pip-3.2 install matplotlib
- 从 Git 仓库下载源文件(命令应为一行)如下:
wget https://github.com/matplotlib/matplotlib/archive/master.zip
- 解压并打开创建的
matplotlib-master
文件夹,操作如下:
unzip master.zip
rm master.zip
cd matplotlib-master
- 运行设置文件进行构建(这将需要一段时间)并按照以下步骤安装:
sudo python3 setup.py build
sudo python3 setup.py install
- 以与自动化安装相同的方式进行安装测试。
我们可能需要 PCF8591 ADC 模块(以及之前安装的wiringpi2
),或者我们可以使用上一节中的data_local.py
模块(只需在脚本的导入部分将data_adc
替换为data_local
)。我们还需要确保data_adc.py
和data_local.py
与新的脚本位于同一目录中,具体取决于你使用哪个模块。
如何做到这一点...
- 创建一个名为
log_adc.py
的脚本:
#!/usr/bin/python3
#log_adc.c
import time
import datetime
import data_adc as dataDevice
DEBUG=True
FILE=True
VAL0=0;VAL1=1;VAL2=2;VAL3=3 #Set data order
FORMATHEADER = "t%st%st%st%st%s"
FORMATBODY = "%dt%st%ft%ft%ft%f"
if(FILE):f = open("data.log",'w')
def timestamp():
ts = time.time()
return datetime.datetime.fromtimestamp(ts).strftime(
'%Y-%m-%d %H:%M:%S')
def main():
counter=0
myData = dataDevice.device()
myDataNames = myData.getName()
header = (FORMATHEADER%("Time",
myDataNames[VAL0],myDataNames[VAL1],
myDataNames[VAL2],myDataNames[VAL3]))
if(DEBUG):print (header)
if(FILE):f.write(header+"n")
while(1):
data = myData.getNew()
counter+=1
body = (FORMATBODY%(counter,timestamp(),
data[0],data[1],data[2],data[3]))
if(DEBUG):print (body)
if(FILE):f.write(body+"n")
time.sleep(0.1)
try:
main()
finally:
f.close()
#End
- 创建一个名为
log_graph.py
的第二个脚本,如下所示:
#!/usr/bin/python3
#log_graph.py
import numpy as np
import matplotlib.pyplot as plt
filename = "data.log"
OFFSET=2
with open(filename) as f:
header = f.readline().split('t')
data = np.genfromtxt(filename, delimiter='t', skip_header=1,
names=['sample', 'date', 'DATA0',
'DATA1', 'DATA2', 'DATA3'])
fig = plt.figure(1)
ax1 = fig.add_subplot(211)#numrows, numcols, fignum
ax2 = fig.add_subplot(212)
ax1.plot(data['sample'],data['DATA0'],'r',
label=header[OFFSET+0])
ax2.plot(data['sample'],data['DATA1'],'b',
label=header[OFFSET+1])
ax1.set_title("ADC Samples")
ax1.set_xlabel('Samples')
ax1.set_ylabel('Reading')
ax2.set_xlabel('Samples')
ax2.set_ylabel('Reading')
leg1 = ax1.legend()
leg2 = ax2.legend()
plt.show()
#End
它是如何工作的...
第一段脚本 log_adc.py
允许我们收集数据并将其写入日志文件。
我们可以通过将data_adc
导入为dataDevice
来使用 ADC 设备,或者导入data_local
来使用系统数据。分配给VAL0
至VAL3
的数字使我们能够改变通道的顺序(如果使用data_local
设备,还可以选择其他通道)。我们还可以定义日志文件中头部和每行的格式字符串(以创建用制表符分隔数据的文件),使用%s
、%d
和%f
来允许我们替换字符串、整数和浮点值,如下表所示:
从 ADC 传感器模块捕获的数据表
当登录到文件(当FILE=True
时),我们使用'w'
选项以写入模式打开data.log
(这将覆盖任何现有文件;要追加到文件,请使用'a'
)。
作为我们数据日志的一部分,我们使用 time
和 datetime
模块生成 timestamp
,通过 time.time()
命令获取当前的纪元时间(这是自 1970 年 1 月 1 日以来的毫秒数)。我们使用 strftime()
函数将这个值转换为更友好的 年-月-日 时:分:秒
格式。
main()
函数首先创建我们 device
类的一个实例(我们在前面的示例中创建了它),该实例将提供数据。我们从 data
设备中获取通道名称并构建 header
字符串。如果 DEBUG
设置为 True
,数据将被打印到屏幕上;如果 FILE
设置为 True
,它将被写入文件。
在主循环中,我们使用设备的getNew()
函数来收集数据并将其格式化以在屏幕上显示或记录到文件中。main()
函数是通过try: finally:
命令调用的,这将确保当脚本被终止时,文件将被正确关闭。
第二个脚本 log_graph.py
允许我们读取日志文件并生成记录数据的图表,如下所示:
由 light 和 temperature 传感器生成的 log_graph.py 产生的图
我们首先打开日志文件并读取第一行;这包含了标题信息(我们随后可以用它来识别数据)。接下来,我们使用numpy
,这是一个专业的 Python 库,它扩展了我们对数据和数字的操作方式。在这种情况下,我们用它来读取文件中的数据,根据制表符分隔符将其拆分,并为每个数据通道提供标识符。
我们定义一个图形来容纳我们的图表,添加两个子图(位于 2 x 1 网格中,位置为网格中的 1 和 2 - 由值211
和212
设置)。接下来,我们定义我们想要绘制的值,提供x
值(data['sample']
),y
值(data['DATA0']
),color
值('r'
代表红色或'b'
代表蓝色),以及label
(设置为之前从文件顶部读取的标题文本)。
最后,我们为每个子图设置标题以及x
和y
标签,启用图例(以显示标签),并显示图表(使用plt.show()
)。
还有更多...
现在我们有了查看我们所收集数据的能力,我们可以在采样时将其显示出来,从而更进一步。这将使我们能够立即看到数据如何对环境或刺激的变化做出反应。我们还可以校准我们的数据,以便我们可以分配适当的缩放比例,以产生以实际单位进行的测量。
实时数据绘图
除了从文件中绘制数据,我们还可以使用 matplotlib
来绘制按采样得到的传感器数据。为了实现这一点,我们可以使用 plot-animation
功能,该功能会自动调用一个函数来收集新数据并更新我们的图表。
创建以下脚本,命名为 live_graph.py
:
#!/usr/bin/python3
#live_graph.py
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import data_local as dataDevice
PADDING=5
myData = dataDevice.device()
dispdata = []
timeplot=0
fig, ax = plt.subplots()
line, = ax.plot(dispdata)
def update(data):
global dispdata,timeplot
timeplot+=1
dispdata.append(data)
ax.set_xlim(0, timeplot)
ymin = min(dispdata)-PADDING
ymax = max(dispdata)+PADDING
ax.set_ylim(ymin, ymax)
line.set_data(range(timeplot),dispdata)
return line
def data_gen():
while True:
yield myData.getNew()[1]/1000
ani = animation.FuncAnimation(fig, update,
data_gen, interval=1000)
plt.show()
#End
我们首先定义我们的dataDevice
对象,并创建一个空数组dispdata[]
,它将保存所有收集到的数据。接下来,我们定义我们的子图以及我们要绘制的线条。
FuncAnimation()
函数允许我们通过定义一个更新函数和一个生成器函数来更新一个图形(fig
)。生成器函数(data_gen()
)将每隔一个间隔(1,000 毫秒)被调用一次,并产生一个数据值。
本例使用核心温度读数,当其除以 1,000 时,得到实际的温度单位为degC
:
要使用 ADC 数据,请将 dataDevice
的导入更改为 data_adc
,并调整以下行以使用除 [1]
之外的其他通道,并应用不同于 1,000 的缩放系数:
yield myData.getNew()[1]/1000
Raspberry Pi 实时绘图
数据值传递给update()
函数,这使得我们可以将其添加到我们的dispdata[]
数组中,该数组将包含要在图中显示的所有数据值。我们调整x轴的范围,使其接近数据的min
和max
值。同时,我们也调整y轴,以便在继续采样更多数据时增长。
FuncAnimation()
函数需要 data_gen()
对象是一个特殊类型的函数,称为 generator
。一个 generator
函数每次被调用时都会产生一系列连续的值,如果需要,甚至可以使用其先前状态来计算下一个值。这用于执行绘图时的连续计算;这就是为什么在这里使用它的原因。在我们的情况下,我们只想连续运行相同的采样函数(new_data()
),以便每次调用时都会产生一个新的样本。
最后,我们使用dispdata[]
数组(通过set_data()
函数)更新x轴和y轴的数据,这将绘制我们的样本与采样秒数的对比图。若要使用其他数据,或绘制来自 ADC 的数据,请调整dataDevice
的导入,并在data_gen()
函数中选择所需的通道(以及缩放)。
数据的缩放和校准
你可能已经注意到,有时解读从模数转换器(ADC)读取的数据可能会有点困难,因为其值只是一个数字。一个数字本身并不提供太多帮助;它只能告诉你环境比上一个样本稍微热一点或暗一点。然而,如果你可以使用另一个设备提供可比较的值(例如当前房间温度),那么你可以校准你的传感器数据以提供更有用的现实世界信息。
为了获得粗略的校准,我们将使用两个样本来创建一个线性拟合模型,然后可以使用该模型来估计其他 ADC 读数的实际世界值(这假设传感器本身的响应大部分是线性的)。以下图表显示了使用 25 度和 30 摄氏度两个读数创建的线性拟合图,为其他温度提供了估计的 ADC 值:
样本用于线性校准温度传感器的读数
我们可以使用以下函数来计算我们的模型:
def linearCal(realVal1,readVal1,realVal2,readVal2):
#y=Ax+C
A = (realVal1-realVal2)/(readVal1-readVal2)
C = realVal1-(readVal1*A)
cal = (A,C)
return cal
这将返回cal
,其中将包含模型斜率(A
)和偏移量(C
)。
我们可以使用以下函数,通过使用该通道计算出的cal
值来计算任何读数的值:
def calValue(readVal,cal = [1,0]):
realVal = (readVal*cal[0])+cal[1]
return realVal
为了提高准确性,您可以取几个样本,并在这些值之间进行线性插值(或者在需要的情况下,将数据拟合到其他更复杂的数学模型)。
使用 I/O 扩展器扩展树莓派 GPIO
正如我们所见,利用高级总线协议可以让我们快速且容易地连接到更复杂的硬件。通过使用 I²C 来扩展树莓派上的可用 I/O,以及提供额外的电路保护(在某些情况下,还能提供额外的电力来驱动更多硬件),I²C 可以发挥很大的作用。
有许多设备可以通过 I²C 总线(以及 SPI)提供 I/O 扩展,但最常用的设备是一个 28 引脚的设备,MCP23017,它提供了 16 个额外的数字输入/输出引脚。作为一个 I²C 设备,它只需要两个信号(SCL 和 SDA 连接,加上地线和电源)就可以愉快地与其他在同一总线上运行的 I²C 设备协同工作。
我们将看到 Adafruit I²C 16x2 RGB LCD Pi 板如何利用这些芯片之一来通过 I²C 总线控制 LCD 数字显示屏和键盘(如果没有 I/O 扩展器,这通常需要多达 15 个 GPIO 引脚)。
其他制造商的板子也可以使用。一个 16x2 液晶模块和 I²C 到串行接口模块可以组合成我们自己的低成本 I²C 液晶模块。
准备就绪
您将需要 Adafruit I²C 16x2 RGB LCD Pi Plate(该板还包含五个按键),如下照片所示:
Adafruit I²C 16x2 RGB LCD Pi Plate 直接连接到 Raspberry Pi 的 GPIO 接口。
如前所述,我们可以使用 PCF8591 ADC 模块,或者使用上一节中的data_local.py
模块(在脚本的导入部分使用data_adc
或data_local
)。data_adc.py
和data_local.py
文件应与新的脚本位于同一目录下。
LCD Pi 板只需要四个引脚(SDA、SCL、GND 和 5V);它通过整个 GPIO 引脚头连接。如果我们想与其他设备一起使用它,例如 PCF8591 ADC 模块,那么可以使用类似 PiBorg 的 TriBorg(将 GPIO 端口分成三个)的东西来添加端口。
如何做到这一点...
- 创建以下名为
lcd_i2c.py
的脚本:
#!/usr/bin/python3
#lcd_i2c.py
import wiringpi2
import time
import datetime
import data_local as dataDevice
AF_BASE=100
AF_E=AF_BASE+13; AF_RW=AF_BASE+14; AF_RS=AF_BASE+15
AF_DB4=AF_BASE+12; AF_DB5=AF_BASE+11; AF_DB6=AF_BASE+10
AF_DB7=AF_BASE+9
AF_SELECT=AF_BASE+0; AF_RIGHT=AF_BASE+1; AF_DOWN=AF_BASE+2
AF_UP=AF_BASE+3; AF_LEFT=AF_BASE+4; AF_BACK=AF_BASE+5
AF_GREEN=AF_BASE+6; AF_BLUE=AF_BASE+7; AF_RED=AF_BASE+8
BNK=" "*16 #16 spaces
def gpiosetup():
global lcd
wiringpi2.wiringPiSetup()
wiringpi2.mcp23017Setup(AF_BASE,0x20)
wiringpi2.pinMode(AF_RIGHT,0)
wiringpi2.pinMode(AF_LEFT,0)
wiringpi2.pinMode(AF_SELECT,0)
wiringpi2.pinMode(AF_RW,1)
wiringpi2.digitalWrite(AF_RW,0)
lcd=wiringpi2.lcdInit(2,16,4,AF_RS,AF_E,
AF_DB4,AF_DB5,AF_DB6,AF_DB7,0,0,0,0)
def printLCD(line0="",line1=""):
wiringpi2.lcdPosition(lcd,0,0)
wiringpi2.lcdPrintf(lcd,line0+BNK)
wiringpi2.lcdPosition(lcd,0,1)
wiringpi2.lcdPrintf(lcd,line1+BNK)
def checkBtn(idx,size):
global run
if wiringpi2.digitalRead(AF_LEFT):
idx-=1
printLCD()
elif wiringpi2.digitalRead(AF_RIGHT):
idx+=1
printLCD()
if wiringpi2.digitalRead(AF_SELECT):
printLCD("Exit Display")
run=False
return idx%size
def main():
global run
gpiosetup()
myData = dataDevice.device()
myDataNames = myData.getName()
run=True
index=0
while(run):
data = myData.getNew()
printLCD(myDataNames[index],str(data[index]))
time.sleep(0.2)
index = checkBtn(index,len(myDataNames))
main()
#End
- 连接好 LCD 模块后,按照以下步骤运行脚本:
sudo python3 lcd_i2c.py
使用左右按钮选择您想要显示的数据通道,然后按下 SELECT 按钮退出。
它是如何工作的...
wiringpi2
库对 I/O 扩展芯片有出色的支持,例如用于 Adafruit LCD 字符模块的芯片。要使用 Adafruit 模块,我们需要设置 MCP23017 端口 A 所有引脚的引脚映射,如下表所示(然后,我们使用偏移量 100
设置 I/O 扩展引脚):
姓名 | 选择 | 右 | 下 | 上 | 左 | 绿色 | 蓝色 | 红色 |
---|---|---|---|---|---|---|---|---|
MCP23017 端口 A | A0 | A1 | A2 | A3 | A4 | A6 | A7 | A8 |
WiringPi 引脚 | 100 | 101 | 102 | 103 | 104 | 106 | 107 | 108 |
MCP23017 端口 B 的所有引脚的引脚映射如下:
姓名 | DB7 | DB6 | DB5 | DB4 | E | RW | RS |
---|---|---|---|---|---|---|---|
MCP23017 端口 B | B1 | B2 | B3 | B4 | B5 | B6 | B7 |
WiringPi 引脚 | 109 | 110 | 111 | 112 | 113 | 114 | 115 |
要设置 LCD 屏幕,我们首先初始化wiringPiSetup()
和 I/O 扩展器mcp23017Setup()
。然后,我们指定 I/O 扩展器的引脚偏移和总线地址。接下来,我们将所有硬件按钮设置为输入(使用pinMode(pin number,0)
),并将 LCD 的 RW 引脚设置为输出。wiringpi2
LCD 库期望 RW 引脚被设置为LOW
(强制进入只读模式),因此我们设置该引脚为LOW
(使用digitalWrite(AF_RW,0)
)。
我们通过定义屏幕的行数和列数以及声明是否使用 4 位或 8 位数据模式(我们使用 8 条数据线中的 4 条,因此我们将使用 4 位模式)来创建一个lcd
对象。我们还提供了我们使用的引脚映射(最后四条设置为0
,因为我们只使用四条数据线)。
现在,我们将创建一个名为 PrintLCD()
的函数,它将允许我们向显示屏的每一行发送字符串。我们使用 lcdPosition()
函数来设置 lcd
对象上每一行的光标位置,然后打印每一行的文本。我们还在每一行的末尾添加一些空白空间,以确保整行被完全覆盖。
下一个函数 checkBtn()
简单地检查左右选择按钮是否被按下(使用 digitalRead()
函数)。如果左右按钮被按下,则索引设置为数组中的前一个/下一个项目。如果按下 SELECT 按钮,则将 run
标志设置为 False
(这将退出主循环,允许脚本完成)。
main()
函数调用 gpiosetup()
来创建我们的 lcd
对象;然后,我们创建我们的 dataDevice
对象并获取数据名称。在主循环中,我们获取新的数据;然后,我们使用我们的 printLCD()
函数在顶部行显示数据名称,在第二行显示数据值。最后,我们检查按钮是否被按下,并根据需要设置数据索引。
还有更多...
使用像 MCP23017 这样的扩展芯片提供了一种极好的方法来增加 Raspberry Pi 的硬件连接数量,同时提供额外的保护层(更换扩展芯片 Raspberry Pi 更便宜)。
I/O 扩展器电压和限制
端口扩展器在使用时仅消耗少量电力,但如果您使用 3.3 V 电源为其供电,那么您从所有引脚中总共只能抽取最大 50 mA 的电流。如果您抽取过多电力,那么您可能会遇到系统冻结或 SD 卡上的读写损坏。
如果你使用 5V 电源为扩展器供电,那么你可以获取扩展器所能支持的最大功率(大约每个引脚最大 25 mA,总共 125 mA),只要你的 USB 电源足够强大。
我们必须记住,如果扩展器使用 5 V 电源供电,输入/输出和中断线也将是 5 V,绝不应该直接连接回树莓派(除非使用电平转换器将电压降至 3.3 V)。
通过改变扩展芯片上的地址引脚(A0、A1 和 A2)的接线,可以在同一 I²C 总线上同时使用多达八个模块。为了确保每个模块都有足够的电流供应,我们需要使用一个单独的 3.3V 电源。一个如 LM1117-3.3 的线性稳压器就非常合适(这将在 3.3V 下提供高达 800mA 的电流,每个模块 100mA),并且只需要以下简单的电路:
LM1117 线性电压稳压器电路
以下图表展示了如何将稳压器连接到 I/O 扩展器(或其他设备)以提供更多电流来驱动额外硬件:
使用电压调节器与树莓派配合
输入电压(Vin)由树莓派(例如,来自 GPIO 引脚头,如 5 V 引脚 2)提供。然而,只要在 4.5 V 至 15 V 之间并且能够提供足够的电流,Vin 也可以由任何其他电源(或电池组)提供。重要的是要确保树莓派的接地连接(GND)、电源(如果使用单独的电源)、稳压器和 I/O 扩展器的接地都连接在一起(作为公共接地)。
使用您自己的 I/O 扩展模块
您可以使用以下电路中可用的 I/O 扩展模块之一(或者直接使用 MCP23017 芯片)来控制大多数与 HD44780 兼容的 LCD 显示屏:
I/O 扩展器和兼容 HD44780 的显示屏
D-Pad 电路,使用 Python-to-drive 硬件,也可以连接到扩展器的剩余端口 A 引脚(PA0
连接到按钮 1,PA1
连接到右,PA2
连接到下,PA3
连接到上,PA4
连接到左,以及 PA5
连接到按钮 2)。与前面的示例一样,按钮将连接到 PA0
到 PA4
(WiringPi 引脚编号 100 到 104);除此之外,我们还添加了第二个按钮连接到 PA5
(WiringPi 引脚编号 105)。
直接控制液晶数字显示屏
或者,您也可以使用以下连接直接从树莓派驱动屏幕:
我们在这里没有使用 I²C 总线。
LCD | VSS | VDD | V0 | RS | RW | E | DB4 | DB5 | DB6 | DB7 |
---|---|---|---|---|---|---|---|---|---|---|
LCD 引脚 | 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | 13 | 14 |
树莓派 GPIO | 6 (GND) | 2 (5V) | 对比度 | 11 | 13 (GND) | 15 | 12 | 16 | 18 | 22 |
前面的表格列出了树莓派与 HD44780 兼容的字母数字显示模块之间所需连接。
对比针(V0)可以像之前一样连接到一个可变电阻(一边连接到 5V 电源,另一边连接到 GND);尽管如此,根据屏幕的不同,你可能发现你可以直接连接到 GND/5V 以获得最大对比度。
wiringpi2
LCD 库假设 RW 引脚连接到 GND(只读);这避免了 LCD 如果直接连接到 Raspberry Pi 时发送数据的风险(这会成为一个问题,因为屏幕由 5 V 供电,并将使用 5 V 逻辑发送数据)。
确保您使用新的 AF_XX
引用更新代码,并在 gpiosetup()
函数内更改设置以通过物理引脚号进行引用。我们还可以跳过 MCP23017 设备的设置。
查看以下命令:
wiringpi2.wiringPiSetup()
wiringpi2.mcp23017Setup(AF_BASE,0x20)
将前面的命令替换为以下命令:
wiringpi.wiringPiSetupPhys()
你可以看到,我们只需更改引脚引用即可在是否使用 I/O 扩展器之间切换,这显示了 wiringpi2
实现是多么方便。
在 SQLite 数据库中捕获数据
数据库是存储大量结构化数据的同时保持访问和搜索特定数据能力的完美方式。结构化查询语言(SQL)是一组标准的命令,用于更新和查询数据库。在此示例中,我们将使用 SQLite(一个轻量级、自包含的 SQL 数据库系统实现)。
在本章中,我们将从我们的 ADC(或本地数据源)收集原始数据,并构建我们自己的数据库。然后我们可以使用一个名为sqlite3
的 Python 库向数据库添加数据,并对其进行查询:
## Timestamp 0:Light 1:Temperature 2:External 3:Potentiometer
0 2015-06-16 21:30:51 225 212 122 216
1 2015-06-16 21:30:52 225 212 148 216
2 2015-06-16 21:30:53 225 212 113 216
3 2015-06-16 21:30:54 225 212 137 216
4 2015-06-16 21:30:55 225 212 142 216
5 2015-06-16 21:30:56 225 212 115 216
6 2015-06-16 21:30:57 225 212 149 216
7 2015-06-16 21:30:58 225 212 128 216
8 2015-06-16 21:30:59 225 212 123 216
9 2015-06-16 21:31:02 225 212 147 216
准备就绪
为了在我们的数据库中捕获数据,我们将安装 SQLite,以便它可以与 Python 的内置模块sqlite3
一起使用。使用以下命令安装 SQLite:
sudo apt-get install sqlite3
接下来,我们将使用 SQLite 执行一些基本操作,以了解如何使用 SQL 查询。
直接运行 SQLite,使用以下命令创建一个新的 test.db
数据库文件:
sqlite3 test.db
SQLite version 3.7.13 2012-06-11 02:05:22
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite>
这将打开一个 SQLite 控制台,在其中我们可以直接输入 SQL 命令。例如,以下命令将创建一个新表,添加一些数据,显示内容,然后删除该表:
CREATE TABLE mytable (info TEXT, info2 TEXT,);
INSERT INTO mytable VALUES ("John","Smith");
INSERT INTO mytable VALUES ("Mary","Jane");
John|Smith
Mary|Jane
DROP TABLE mytable;
.exit
你需要与之前食谱中详细说明的相同硬件设置,如使用 I²C 总线的设备食谱的准备就绪部分所述。
如何做到这一点...
创建以下脚本,命名为 mysqlite_adc.py
:
#!/usr/bin/python3
#mysql_adc.py
import sqlite3
import datetime
import data_adc as dataDevice
import time
import os
DEBUG=True
SHOWSQL=True
CLEARDATA=False
VAL0=0;VAL1=1;VAL2=2;VAL3=3 #Set data order
FORMATBODY="%5s %8s %14s %12s %16s"
FORMATLIST="%5s %12s %10s %16s %7s"
DATEBASE_DIR="/var/databases/datasite/"
DATEBASE=DATEBASE_DIR+"mydatabase.db"
TABLE="recordeddata"
DELAY=1 #approximate seconds between samples
def captureSamples(cursor):
if(CLEARDATA):cursor.execute("DELETE FROM %s" %(TABLE))
myData = dataDevice.device()
myDataNames=myData.getName()
if(DEBUG):print(FORMATBODY%("##",myDataNames[VAL0],
myDataNames[VAL1],myDataNames[VAL2],
myDataNames[VAL3]))
for x in range(10):
data=myData.getNew()
for i,dataName in enumerate(myDataNames):
sqlquery = "INSERT INTO %s (itm_name, itm_value) " %(TABLE) +
"VALUES('%s', %s)"
%(str(dataName),str(data[i]))
if (SHOWSQL):print(sqlquery)
cursor.execute(sqlquery)
if(DEBUG):print(FORMATBODY%(x,
data[VAL0],data[VAL1],
data[VAL2],data[VAL3]))
time.sleep(DELAY)
cursor.commit()
def displayAll(connect):
sqlquery="SELECT * FROM %s" %(TABLE)
if (SHOWSQL):print(sqlquery)
cursor = connect.execute (sqlquery)
print(FORMATLIST%("","Date","Time","Name","Value"))
for x,column in enumerate(cursor.fetchall()):
print(FORMATLIST%(x,str(column[0]),str(column[1]),
str(column[2]),str(column[3])))
def createTable(cursor):
print("Create a new table: %s" %(TABLE))
sqlquery="CREATE TABLE %s (" %(TABLE) +
"itm_date DEFAULT (date('now','localtime')), " +
"itm_time DEFAULT (time('now','localtime')), " +
"itm_name, itm_value)"
if (SHOWSQL):print(sqlquery)
cursor.execute(sqlquery)
cursor.commit()
def openTable(cursor):
try:
displayAll(cursor)
except sqlite3.OperationalError:
print("Table does not exist in database")
createTable(cursor)
finally:
captureSamples(cursor)
displayAll(cursor)
try:
if not os.path.exists(DATEBASE_DIR):
os.makedirs(DATEBASE_DIR)
connection = sqlite3.connect(DATEBASE)
try:
openTable(connection)
finally:
connection.close()
except sqlite3.OperationalError:
print("Unable to open Database")
finally:
print("Done")
#End
如果您没有 ADC 模块硬件,可以通过将dataDevice
模块设置为data_local
来捕获本地数据。请确保您在同一目录下有data_local.py
(来自更多内容...部分,在使用模数转换器读取模拟数据菜谱中),如下脚本所示:
导入本地数据作为 dataDevice
这将捕获本地数据(RAM、CPU 活动、温度等)并存储到 SQLite 数据库中,而不是 ADC 样本。
它是如何工作的...
当脚本首次运行时,它将创建一个名为 mydatabase.db
的新 SQLite 数据库文件,并将添加一个名为 recordeddata
的表。该表由 createTable()
函数生成,该函数执行以下 SQLite 命令:
CREATE TABLE recordeddata
(
itm_date DEFAULT (date('now','localtime')),
itm_time DEFAULT (time('now','localtime')),
itm_name,
itm_value
)
新表格将包含以下数据项:
姓名 | 描述 |
---|---|
itm_date |
用于存储数据样本的日期。当创建数据记录时,将应用当前日期(使用 date('now','localtime') )作为默认值。 |
itm_time |
用于存储数据样本的时间。当数据记录创建时,当前时间(使用 time('now','localtime') )被应用为默认值。 |
itm_name |
用于记录样本的名称。 |
itm_value |
用于保存采样值。 |
我们随后使用与之前在记录和绘图数据配方中相同的方法从 ADC 捕获 10 个数据样本(如captureSamples()
函数所示)。然而,这次,我们将捕获到的数据添加到我们新的 SQLite 数据库表中,使用以下 SQL 命令(通过cursor.execute(sqlquery)
应用):
INSERT INTO recordeddata
(itm_name, itm_value) VALUES ('0:Light', 210)
当前日期和时间将在创建每条记录时默认添加。最终我们得到一组 40 条记录(每个 ADC 采样周期捕获 4 条记录),这些记录现在存储在 SQLite 数据库中:
已捕获并存储了八个 ADC 样本到 SQLite 数据库中
在记录创建完成后,我们必须记得调用 cursor.commit()
,这将把所有新记录保存到数据库中。
脚本的最后一部分调用displayAll()
,它将使用以下 SQL 命令:
SELECT * FROM recordeddata
这将选择recordeddata
表中的所有数据记录,我们使用cursor.fetch()
来提供作为列表的所选数据,我们可以遍历这个列表:
for x,column in enumerate(cursor.fetchall()):
print(FORMATLIST%(x,str(column[0]),str(column[1]),
str(column[2]),str(column[3])))
这使我们能够打印出数据库的全部内容,显示捕获的数据。
注意,在此脚本中我们使用了try
、except
和finally
结构来尝试处理用户在运行脚本时可能遇到的大部分情况。
首先,我们确保如果数据库目录不存在,就创建它。接下来,我们尝试打开数据库文件;如果数据库文件尚未存在,这个过程将自动创建一个新的数据库文件。如果这些初始步骤中的任何一个失败(例如,因为它们没有读写权限),我们就无法继续,因此我们报告无法打开数据库,并简单地退出脚本。
接下来,我们尝试在数据库中打开所需的表并显示它。如果数据库文件是全新的,这个操作将始终失败,因为它将是空的。然而,如果发生这种情况,我们只需捕获异常,在继续脚本以将我们的样本数据添加到表中并显示之前创建该表。
这使得脚本能够优雅地处理潜在问题,采取纠正措施,然后继续平稳运行。下次运行脚本时,数据库和表已经存在,因此我们不需要再次创建它们,并且可以将样本数据附加到同一数据库文件内的表中。
还有更多...
可用的 SQL 服务器版本有很多(例如 MySQL、Microsoft SQL Server 和 PostgreSQL),然而它们至少应该具备以下基本命令(或等效命令):
CREATE, INSERT, SELECT, WHERE, UPDATE, SET, DELETE, and DROP
你应该会发现,即使你选择使用与这里使用的 SQLite 不同的 SQL 服务器,SQL 命令也将相对相似。
CREATE TABLE 命令
CREATE TABLE
命令用于通过指定列名(以及如果需要还可以设置默认值)来定义一个新的表:
CREATE TABLE table_name (
column_name1 TEXT,
column_name2 INTEGER DEFAULT 0,
column_name3 REAL )
之前的 SQL 命令将创建一个名为 table_name
的新表,包含三个数据项。一列将包含文本,其他列包含整数(例如,1,3,-9),最后,一列将包含实数(例如,5.6,3.1749,1.0)。
INSERT 命令
INSERT
命令会将特定条目添加到数据库中的表中:
INSERT INTO table_name (column_name1name1, column_name2name2, column_name3)name3)
VALUES ('Terry'Terry Pratchett', 6666, 27.082015)082015)
这将把提供的值输入到表格中相应的列。
SELECT 命令
SELECT
命令允许我们指定数据库表中特定的列或列,返回包含数据的记录列表:
SELECT column_name1, column_name2 FROM table_name
它还可以让我们使用此命令选择所有项目:
SELECT * FROM table_name
WHERE 命令
WHERE
命令用于指定要选择、更新或删除的特定条目:
SELECT * FROM table_name
WHERE column_name1= 'Terry Pratchett'
这将 SELECT
任何 column_name1
字段匹配 'Terry Pratchett'
的记录。
UPDATE 命令
UPDATE
命令将允许我们更改(SET
)指定列中数据的值。我们还可以结合使用 WHERE
命令来限制更改应用到的记录:
UPDATE table_name
SET column_name2=49name2=49,column_name3=30name3=30.111997
WHERE column_name1name1= 'Douglas Adams'Adams';
DELETE 命令
DELETE
命令允许使用 WHERE
语句选中的任何记录从指定的表中删除。然而,如果选择整个表,使用 DELETE * FROM table_name
将会删除表中的全部内容:
DELETE FROM table_name
WHERE columncolumn_name2=9999
DROP 命令
DROP
命令允许将表完全从数据库中删除:
DROP table_name
警告:这将永久删除指定表中存储的所有数据及其结构。
查看来自您自己的网络服务器的数据
将信息和数据收集到数据库中非常有帮助,但如果它们被锁在数据库或文件中,那么它们就没什么用处了。然而,如果我们允许通过网页查看存储的数据,那么它将更容易获取;我们不仅可以从其他设备查看数据,还可以与同一网络上的其他人共享数据。
我们将创建一个本地网络服务器来查询和显示捕获的 SQLite 数据,并允许通过 PHP 网络界面进行查看。这将使得数据不仅可以通过树莓派上的网络浏览器查看,还可以在本地网络上的其他设备,如手机或平板电脑上查看:
通过网页显示的 SQLite 数据库中捕获的数据
使用网络服务器来输入和显示信息是一种强大的方式,可以让广泛的用户与您的项目进行交互。以下示例演示了一个可以针对各种用途进行定制的网络服务器设置。
准备就绪
确保你已经完成了之前的食谱,以便传感器数据已经被收集并存储在 SQLite 数据库中。我们需要安装一个网络服务器(Apache2)并启用 PHP 支持以允许 SQLite 访问。
使用以下命令来安装一个 Web 服务器和 PHP:
sudo apt-get update
sudo aptitude install apache2 php5 php5-sqlite
/var/www/
目录由 web 服务器使用;默认情况下,它将加载 index.html
(或 index.php
)——否则,它将仅显示目录内文件的链接列表。
要测试网页服务器是否正在运行,创建一个默认的index.html
页面。为此,您需要使用sudo
权限创建文件(/var/www/
目录受到普通用户更改的保护)。请使用以下命令:
sudo nano /var/www/index.html
使用以下内容创建 index.html
:
<h1>It works!</h1>
关闭并保存文件(使用 Ctrl + X,Y 和 Enter)。
如果你正在使用带有屏幕的树莓派,你可以通过加载桌面来检查它是否正常工作:
startx
然后,打开网页浏览器(epiphany-browser)并输入http://localhost
作为地址。你应该会看到以下测试页面,表明网络服务器正在运行:
树莓派浏览器显示的测试页面,位于 http://localhost
如果你正在远程使用树莓派或者它已经连接到你的网络,你也应该能够在网络上的另一台电脑上查看该页面。首先,确定树莓派的 IP 地址(使用sudo hostname -I
命令),然后将其作为地址在你的网页浏览器中使用。你甚至可能发现可以使用树莓派的实际主机名(默认情况下,这是http://raspberrypi/
)。
如果您无法从另一台电脑上看到网页,请确保您没有启用可能阻止它的防火墙(在电脑本身上,或在您的路由器上)。
接下来,我们可以测试 PHP 是否运行正常。我们可以创建一个名为 test.php
的网页,并确保它位于 /var/www/ 目录
:
<?php
phpinfo();
?>;
用于在 SQLite 数据库中查看数据的 PHP 网页具有以下详细信息:
在 http://localhost/test.php 查看 test.php 页面
我们现在准备编写自己的 PHP 网页来查看 SQLite 数据库中的数据。
如何做到这一点...
-
创建以下 PHP 文件并将它们保存在名为
/var/www/./
的 Web 服务器目录中。 -
使用以下命令创建 PHP 文件:
sudo nano /var/www/show_data_lite.php
show_data_lite.php
文件应包含以下内容:
<head>
<title>DatabaseDatabase Data</title>
<meta http-equiv="refresh" content="10" >
</head>
<body>
Press button to remove the table data
<br>
<input type="button" onclick="location.href = 'del_data_lite.php';" value="Delete">
<br><br>
<b>Recorded Data</b><br>
<?php
$db = new PDO("sqlite:/var/databases/datasitedatasite/mydatabase.db");
//SQL query
$strSQL = "SELECT * FROM recordeddatarecordeddata WHERE itmitm_name LIKE '%'%temp%'";
//Execute the query
$response = $db->query($strSQL);
//Loop through the response
while($column = $response->fetch())
{
//Display the content of the response
echo $column[0] . " ";
echo $column[1] . " ";
echo $column[2] . " ";
echo $column[3] . "<br />";
}
?>
Done
</body>
</html>
- 使用以下命令创建 PHP 文件:
sudo nano /var/www/del_data_lite.php
<html>
<body>
Remove all the data in the table.
<br>
<?php
$db = new PDO("sqlite:/var/databases/datasitedatasite/mydatabase.db");
//SQL query
$strSQL = "DROPDROP TABLErecordeddata recordeddata";
//ExecuteExecute the query
$response = $db->query($strSQL);
if ($response == 1)
{
echo "Result: DELETED DATA";
}
else
{
echo "Error: Ensure table exists and database directory is owned
by www-data";
}
?>
<br><br>
Press button to return to data display.
<br>
<input type="button" onclick="location.href = 'show'show_data_lite.php';" value="Return">
</body>
</html>
为了让 PHP 代码能够删除数据库中的表,它需要被 Web 服务器可写。使用以下命令来允许它可写:
sudo chown www-data /var/databases/datasite -R
- 如果您使用以下地址在网页浏览器中打开
show_data_lite.php
文件,它将显示为一个网页:
http://localhost/showshow_data_lite.php
- 或者,您可以通过引用树莓派的 IP 地址来打开网页(如果您愿意,可以在您网络中的另一台计算机上打开):
http://192.168.1.101/showshow_data_lite.php
您可能可以使用主机名来代替(默认情况下,这将使地址变为http://raspberrypi/show_data_lite.php
)。然而,这可能会取决于您的网络设置。
如果没有数据,请确保运行mysqlite_adc.py
脚本来捕获额外的数据。
- 要使
show_data_lite.php
页面在访问您的树莓派网页地址时自动显示(而不是显示 It works! 页面),我们可以将index.html
改为以下内容:
<meta http-equiv="refresh" content="0; URL='show_data_lite.php' " />
这将自动将浏览器重定向到加载我们的show_data_lite.php
页面。
它是如何工作的...
show_data_lite.php
文件应显示存储在 SQLite 数据库中的温度数据(无论是来自 ADC 样本还是本地数据源)。
show_data_lite.php
文件由标准的 HTML 代码以及一个特殊的 PHP 代码部分组成。HTML 代码将 ACD Data
设置为页面头部部分的标题,并使用以下命令使页面每 10 秒自动刷新:
<meta http-equiv="refresh" content="10" >
接下来,我们定义一个删除
按钮,当点击时将加载del_data_lite.php
页面:
<input type="button" onclick="location.href = 'del_data_lite.php';" value="Delete">
最后,我们使用 PHP 代码部分来加载 SQLite 数据库并显示通道 0 的数据。
我们使用以下 PHP 命令来打开我们之前存储数据的 SQLite 数据库(位于/var/databases/testsites/mydatabase.db
):
$db = new PDO("sqlite:/var/databases/testsite/mydatabase.db");
接下来,我们使用以下 SQLite 查询来选择所有文本中包含 0:
的条目(例如,0:Light
):
SELECT * FROM recordeddatarecordeddata WHERE itm_namename LIKE '%temp%''
注意,尽管我们现在使用的是 PHP,但我们与 SQLite 数据库使用的查询与使用sqlite3
Python 模块时相同。
我们现在将查询结果收集到$response
变量中:
$response = $db->query($strSQL);
Allowing us to use fetch() (like we used cursor.fetchall() previously) to list all the data columns in each of the data entries within the response.
while($column = $response->fetch())
{
//Display the content of the response
echo $column[0] . " ";
echo $column[1] . " ";
echo $column[2] . " ";
echo $column[3] . "<br />";
}
?>
del_data_lite.php
文件相当相似;它首先像之前一样重新打开 mydatabase.db
文件。然后执行以下 SQLite 查询:
DROP TABLE recordeddata
如在 更多内容... 部分所述,这将从数据库中删除 recordeddata
表。如果 response
不等于 1,则操作未完成。最可能的原因是包含 mydatabase.db
文件的目录无法被 Web 服务器写入(参见 如何操作... 部分的备注,关于将文件所有者更改为 www-data
)。
最后,我们提供了一个按钮,点击后会将用户带回到show_data_lite.php
页面(该页面将显示已记录的数据已被清除):
Show_data_lite.php
还有更多...
你可能已经注意到,这个菜谱更多地关注了 HTML 和 PHP 而不是 Python(是的,看看封面——这仍然是一本针对 Python 程序员的书籍!)。然而,重要的是要记住,工程的关键部分是整合和结合不同的技术以产生预期的结果。
设计上,Python 非常适合这类任务,因为它允许轻松定制并与大量其他语言和模块集成。我们完全可以只用 Python 来完成所有工作,但为什么不利用现有的解决方案呢?毕竟,它们通常有很好的文档记录,经过广泛的测试,并且通常符合行业标准。
安全
SQL 数据库被广泛应用于存储各种信息,从产品信息到客户详情。在这种情况下,用户可能需要输入信息,这些信息随后会被形成 SQL 查询。在一个实施不当的系统里,恶意用户可能能够在他们的响应中包含额外的 SQL 语法,从而允许他们危害 SQL 数据库(可能通过访问敏感信息、修改它或简单地删除它)。
例如,当在网页中请求用户名时,用户可以输入以下文本:
John; DELETE FROM Orders
如果直接使用它来构建 SQL 查询,我们最终会得到以下结果:
SELECT * FROM Users WHERE UserName = John; DELETE FROM CurrentOrders
我们刚刚允许攻击者删除了CurrentOrders
表中的所有内容!
使用用户输入作为 SQL 查询的一部分意味着我们必须小心允许执行哪些命令。在这个例子中,用户可能能够删除可能非常重要的信息,这可能会对公司的声誉造成很大的损失。
这种技术被称为 SQL 注入,可以通过使用 SQLite execute()
函数的参数选项轻松防范。我们可以将我们的 Python SQLite 查询替换为一个更安全的版本,如下所示:
sqlquery = "INSERT INTO %s (itm_name, itm_value) VALUES(?, ?)" %(TABLE)
cursor.execute(sqlquery, (str(dataName), str(data[i]))
与盲目构建 SQL 查询不同,SQLite 模块首先会检查提供的参数是否是数据库中可接受的合法值。然后,它会确保将这些参数插入到命令中不会导致额外的 SQL 操作。最后,dataName
和 data[i]
参数的值将被用来替换 ?
字符,以生成最终的安全的 SQLite 查询。
使用 MySQL 代替
SQLite,本菜谱中使用的数据库,只是众多 SQL 数据库中的一种。它适用于只需要相对较小的数据库和最少资源的简单项目。然而,对于需要额外功能(如用户账户以控制访问和额外的安全性)的大型项目,您可以使用替代品,例如 MySQL。
要使用不同的 SQL 数据库,您需要调整我们用来捕获条目的 Python 代码,使用合适的 Python 模块。
对于 MySQL (mysql-server
),我们可以使用一个与 Python-3 兼容的库,称为 PyMySQL 来与之接口。有关如何使用此库的更多信息,请参阅 PyMySQL 网站 (github.com/PyMySQL/PyMySQL
)。
要使用 PHP 与 MySQL,你还需要 PHP MySQL (php5-mysql
);更多详细信息,请参阅 W3 Schools 上的优秀资源(www.w3schools.com/php/php_mysql_connect.asp
)。
你会注意到,尽管不同的 SQL 实现之间存在一些细微的差别,但无论你选择哪一种,一般的概念和命令现在都应该对你来说很熟悉了。
感知并发送数据到在线服务
在本节中,我们将使用一个名为 Xively 的在线服务。该服务使我们能够在线连接、传输和查看数据。Xively 使用一种常见的协议,称为表示状态转移(REST),用于通过 HTTP 传输信息。许多服务,如 Facebook 和 Twitter,都使用各种密钥和访问令牌,以确保数据在授权应用程序和验证网站之间安全传输。
您可以使用名为 requests
的 Python 库手动执行大多数 REST 操作(例如 POST
、GET
、SET
等方法)(docs.python-requests.org
).
然而,通常使用你打算使用的服务的特定库会更容易。它们将处理授权过程并提供访问函数,如果服务发生变化,可以更新库而不是你的代码。
我们将使用xively-python
库,它提供了 Python 函数,使我们能够轻松地与该网站交互。
关于xively-python
库的详细信息,请参阅xively.github.io/xively-python/
。
Xively 收集的数据显示在以下截图:
Xively 收集并绘制使用 REST 传输的数据
准备就绪
您需要在www.xively.com创建一个账户,我们将使用该账户接收我们的数据。访问该网站并注册一个免费的开发者账户:
注册并创建一个 Xively 账户
一旦您注册并验证了您的账户,您就可以按照指示进行试驾示例,这将展示您如何从智能手机(陀螺仪数据、位置等)链接数据,这将让您体验一下我们可以用树莓派做到的事情。
当您登录时,您将被带到开发设备仪表板(位于 WebTools 下拉菜单中):
添加新设备
选择+添加设备并填写详细信息,给你的设备起一个名字,并将设备设置为私有。
您现在将看到您远程设备的控制页面,其中包含您连接设备所需的所有信息,以及数据将显示的位置:
示例 API 密钥和订阅号(这将为您的设备唯一)
尽管这个页面上有许多信息,但你只需要两个关键信息点:
- API 密钥(即
API Keys
部分中的长代码),如下所示:
API_KEY = CcRxJbP5TuHp1PiOGVrN2kTGeXVsb6QZRJU236v6PjOdtzze
- 饲料编号(在
API 密钥
部分提及,并在页面顶部列出),如下所示:
FEED_ID = 399948883
现在我们已经拥有了连接 Xively 所需的所有细节,我们可以专注于 Raspberry Pi 这一方面的事情。
我们将使用 pip-3.2
来安装 Xively,具体步骤如下:
sudo pip-3.2 install xively-python
确保以下内容得到报告:
Successfully installed xively-python requests
你现在可以准备从你的树莓派发送一些数据了。
如何做到这一点...
创建以下名为 xivelyLog.py
的脚本。确保在代码中设置 FEED_ID
和 API_KEY
,以匹配您创建的设备:
#!/usr/bin/env python3
#xivelylog.py
import xively
import time
import datetime
import requests
from random import randint
import data_local as dataDevice
# Set the FEED_ID and API_KEY from your account
FEED_ID = 399948883
API_KEY = "CcRxJbP5TuHp1PiOGVrN2kTGeXVsb6QZRJU236v6PjOdtzze"
api = xively.XivelyAPIClient(API_KEY) # initialize api client
DEBUG=True
myData = dataDevice.device()
myDataNames=myData.getName()
def get_datastream(feed,name,tags):
try:
datastream = feed.datastreams.get(name)
if DEBUG:print ("Found existing datastream")
return datastream
except:
if DEBUG:print ("Creating new datastream")
datastream = feed.datastreams.create(name, tags=tags)
return datastream
def run():
print ("Connecting to Xively")
feed = api.feeds.get(FEED_ID)
if DEBUG:print ("Got feed" + str(feed))
datastreams=[]
for dataName in myDataNames:
dstream = get_datastream(feed,dataName,dataName)
if DEBUG:print ("Got %s datastream:%s"%(dataName,dstream))
datastreams.append(dstream)
while True:
data=myData.getNew()
for idx,dataValue in enumerate(data):
if DEBUG:
print ("Updating %s: %s" % (dataName,dataValue))
datastreams[idx].current_value = dataValue
datastreams[idx].at = datetime.datetime.utcnow()
try:
for ds in datastreams:
ds.update()
except requests.HTTPError as e:
print ("HTTPError({0}): {1}".format(e.errno, e.strerror))
time.sleep(60)
run()
#End
它是如何工作的...
首先,我们初始化 Xively API 客户端,并向其提供API_KEY
(这授权我们向之前创建的Xively
设备发送数据)。接下来,我们使用FEED_ID
将我们链接到我们想要发送数据的特定数据流。最后,我们请求数据流以连接到(如果它尚未存在于数据流中,get_datastream()
函数将为我们创建一个)。
对于每个输入数据流,我们提供一个name
函数和tags
(这些是帮助我们识别数据的关键词;我们可以用我们的数据名称来使用这些)。
一旦我们定义了我们的数据流,我们就进入main
循环。在这里,我们从dataDevice
收集我们的数据值。然后我们设置每个数据项的current_value
函数以及数据的时间戳,并将它们应用到我们的数据流对象中。
最后,当所有数据准备就绪时,我们更新每个数据流,数据被发送到 Xively,并在几分钟内出现在设备的仪表板上。
我们可以使用标准网页浏览器登录我们的 Xively 账户,查看实时传入的数据。这为我们提供了在任何地方(如果需要,也许是从几个树莓派同时)发送数据和远程监控数据的方法。该服务甚至支持创建触发器,当某些项目超出预期范围、达到特定值或匹配设定标准时,可以发送额外的消息。触发器反过来可以用来控制其他设备或触发警报,等等。它们也可以用于其他平台,例如 ThingSpeak 或 plot.ly。
参见
AirPi 空气质量与天气项目(airpi.es
)展示了如何添加您自己的传感器或使用他们的 AirPi 套件来创建您自己的空气质量与天气站(并将数据记录到您的 Xively 账户)。该网站还允许您与世界各地的其他人共享您的 Xively 数据流。
第十一章:为光学字符识别构建神经网络模块
本章介绍了以下食谱:
-
使用光学字符识别(OCR)系统
-
使用软件进行光学字符可视化
-
使用神经网络构建光学字符识别器
-
OCR 系统应用
简介
OCR 系统用于将文本图像转换为字母、单词和句子。它在各个领域被广泛使用,用于从图像中转换/提取信息。它还用于签名识别、自动数据评估和安全系统。在商业上,OCR 用于验证数据记录、护照文件、发票、银行对账单、电子收据、名片、静态数据的打印输出等。OCR 是模式识别、人工智能和计算机视觉研究的一个领域。
可视化光学字符
光学字符可视化是将印刷文本数字化的一种常见方法,以便此类文本可以电子编辑、搜索、紧凑存储并在网上显示。目前,它们在认知计算、机器翻译、文本转语音转换、文本挖掘等领域得到广泛应用。
如何操作...
- 导入以下包:
import os
import sys
import cv2
import numpy as np
- 加载输入数据:
in_file = 'words.data'
- 定义可视化参数:
scale_factor = 10
s_index = 6
e_index = -1
h, w = 16, 8
- 循环直到遇到Esc键:
with open(in_file, 'r') as f:
for line in f.readlines():
information = np.array([255*float(x) for x in line.split('t')[s_index:e_index]])
image = np.reshape(information, (h,w))
image_scaled = cv2.resize(image, None, fx=scale_factor, fy=scale_factor)
cv2.imshow('Image', image_scaled)
a = cv2.waitKey()
if a == 10:
break
- 输入
python visualize_character.py
以执行代码:
- 执行
visualize_character.py
时得到的结果如下所示:
使用神经网络构建光学字符识别器
本节描述了基于神经网络的字符识别方案。
如何做这件事...
- 导入以下包:
import numpy as np
import neurolab as nl
- 读取输入文件:
in_file = 'words.data'
- 考虑使用 20 个数据点构建基于神经网络的系统:
# Number of datapoints to load from the input file
num_of_datapoints = 20
- 表示不同的字符:
original_labels = 'omandig'
# Number of distinct characters
num_of_charect = len(original_labels)
- 使用 90%的数据来训练神经网络,剩余的 10%用于测试:
train_param = int(0.9 * num_of_datapoints)
test_param = num_of_datapoints - train_param
- 定义数据集提取参数:
s_index = 6
e_index = -1
- 构建数据集:
information = []
labels = []
with open(in_file, 'r') as f:
for line in f.readlines():
# Split the line tabwise
list_of_values = line.split('t')
- 实现一个错误检查来确认字符:
if list_of_values[1] not in original_labels:
continue
- 提取标签并将其附加到主列表:
label = np.zeros((num_of_charect , 1))
label[original_labels.index(list_of_values[1])] = 1
labels.append(label)
- 提取字符并将其添加到主列表中:
extract_char = np.array([float(x) for x in list_of_values[s_index:e_index]])
information.append(extract_char)
- 一旦加载所需数据集,就退出循环:
if len(information) >= num_of_datapoints:
break
- 将信息和标签转换为 NumPy 数组:
information = np.array(information)
labels = np.array(labels).reshape(num_of_datapoints, num_of_charect)
- 提取维度数量:
num_dimension = len(information[0])
- 创建并训练神经网络:
neural_net = nl.net.newff([[0, 1] for _ in range(len(information[0]))], [128, 16, num_of_charect])
neural_net.trainf = nl.train.train_gd
error = neural_net.train(information[:train_param,:], labels[:train_param,:], epochs=10000, show=100, goal=0.01)
- 预测测试输入的输出:
p_output = neural_net.sim(information[train_param:, :])
print "nTesting on unknown data:"
for i in range(test_param):
print "nOriginal:", original_labels[np.argmax(labels[i])]
print "Predicted:", original_labels[np.argmax(p_output[i])]
- 执行
optical_character_recognition.py
时得到的结果如下截图所示:
它是如何工作的...
基于神经网络的光学字符识别系统被构建出来,用于从图像中提取文本。这个过程包括使用字符数据集对神经网络系统进行训练、测试和验证。
读者可以参考文章《基于神经网络的字符识别系统》来了解 OCR 背后的基本原理:ieeexplore.ieee.org/document/6419976/
.
参见
请参考以下内容:
OCR 系统应用
光学字符识别系统(OCR)被广泛用于从图像中转换/提取文本(字母和数字)。OCR 系统在验证商业文件、自动车牌识别以及从文档中提取关键字符方面得到广泛应用。它还被用于使印刷文档的电子图像可搜索,并为盲人和视力受损用户提供辅助技术。
第十二章:构建机器人
在本章中,我们将涵盖以下主题:
-
使用前向驱动电机构建 Rover-Pi 机器人
-
使用高级电机控制
-
构建一个六足 Pi-Bug 机器人
-
直接使用 ServoBlaster 控制伺服电机
-
避免物体和障碍物
-
获得方向感
简介
一台拥有“像行星一样大的大脑”的小型电脑(引用自道格拉斯·亚当斯所著的《银河系漫游指南》),作为您自己机器人创造的头脑将非常完美。现实中,树莓派可能提供的处理能力远超过一个小型机器人或探测车所需的;然而,其小巧的尺寸、优秀的连接性和相对较低的能量需求意味着它非常适合。
本章将重点探讨我们可以结合电机或伺服机构以产生机器人运动的各种方法,使用传感器来收集信息,并允许我们的创造物对其做出反应。
一定要查看附录,硬件和软件列表;它列出了本章中使用的所有物品以及你可以从哪里获取它们。
使用前向驱动电机构建 Rover-Pi 机器人
制作机器人不必是一项昂贵的爱好。可以使用家用物品来构建一个小型、漫游车型的机器人底盘(即所有部件都连接到的基座),并且可以使用几个小型驱动电机来移动它。
Rover-Pi 机器人是一种小型、四足型机器人,它有两个轮子和一个在前端的滑轮或万向轮,以便它能够转向。以下图片展示了一个这样的机器人:
一个自制的 Rover-Pi 机器人
虽然它可能无法与火星探测车相提并论,正如您将看到的,您有很多可以实验的内容。
您还可以购买众多价格低廉的机器人套件之一,这些套件包含您所需的大部分组件,只需一个包装(参见本例末尾的更多内容...部分)。
准备就绪
我们将要建造的探测器需要包含以下图中所示的各种元素:
Rover-Pi 机器人的各个部分
元素将按以下方式详细讨论:
- 底盘: 本例使用了一个经过修改的、电池供电的推式夜灯(尽管可以使用任何合适的平台)。请记住,你的机器人越大、越重,驱动电机就需要越强大才能移动它。或者,你也可以使用在“更多内容...”部分列出的底盘套件之一。以下照片展示了一个合适的推式夜灯:
这个推动式夜灯构成了 Rover-Pi 机器人的基本底盘
-
前滑或转向轮: 这可以简单到将一个大的回形针(76 mm/3 英寸)弯曲成形状,或者使用一个小转向轮。滑块在光滑表面上工作时效果最佳,但可能会在地毯上卡住。转向轮在所有表面上都能很好地工作,但有时可能会在转向时出现问题。
-
轮子、电机、齿轮:Rover-Pi 机器人的轮子运动是电机、齿轮和轮子的组合。齿轮很有帮助,因为它们允许高速旋转的电机以较慢的速度和更大的力量(扭矩)转动轮子;这将使我们能够更好地控制我们的机器人。以下照片展示了将轮子、电机和齿轮组合成单个单元的单元:
这些带有内置齿轮电机的轮子非常适合小型漫游车
- 电池/电源: Rover-Pi 机器人将使用四个 AA 电池供电,这些电池被安装到底盘的凹槽中。另外,可以使用标准的电池夹,或者甚至是一根连接到合适电源的长电线。建议您使用与 Raspberry Pi 独立的电源为电机供电。这有助于避免在驱动电机时,由于电机需要大电流跳跃来移动,导致 Raspberry Pi 突然断电的情况。另外,您也可以使用 5V 稳压器用电池为 Raspberry Pi 供电。以下图片展示了一个带有四个 AA 电池的底盘:
四节 AA 电池为驱动轮提供电源
- 电机驱动/控制器:电机需要比 GPIO 能够处理的电压和电流更大的电压和电流。因此,我们将使用达林顿阵列模块(该模块使用ULN2003芯片)。请参阅本例末尾的更多内容...部分,以获取有关该特定模块如何工作的更多详细信息。以下照片显示了一个达林顿阵列模块:
这款达尔顿阵列模块可在 http://www.dx.com 购买,可用于驱动小型电机
- 小型电缆扎带或线扎带:这将使我们能够将诸如电机或控制器之类的物品固定在底盘上。以下照片展示了电缆扎带的使用:
我们使用电缆扎带将电机和轮子固定在底盘上
- 树莓派连接:最简单的设置是将控制线通过长线连接到树莓派上,这样您就可以通过连接的屏幕和键盘轻松直接控制您的机器人。稍后,您可以考虑将树莓派安装在机器人上,并远程(或者甚至自主地,如果您包括传感器和智能来理解它们)控制它。
在本章中,我们将使用wiringpi2
Python 库来控制 GPIO;有关如何使用 Python 包管理器(pip
)安装它的详细信息,请参阅第十章,感知和显示现实世界数据。
如何做到这一点...
按照以下步骤创建一个小型 Rover-Pi 机器人:
-
在底盘前方,您需要通过弯曲来安装滑块
将回形针/电线弯成 V 形。通过在底盘两侧钻小孔,将回形针/电线固定在底盘前方,通过孔将电缆扎带穿过电线周围,并拉紧以固定。安装好的电线滑块应与以下照片中所示类似:
安装在 Rover-Pi 机器人前端的电缆滑轨
-
在安装车轮之前,你需要计算出底盘的大致重心位置(在底盘内安装电池时进行此操作,因为它们会影响平衡)。通过尝试用两只手指分别平衡底盘,并找出底盘向前或向后倾斜的距离来感受重心的位置。对于我的设备,这个位置大约在中心后 1 厘米(大约三分之一英寸)。你应该将车轮轴稍微放在这个位置之后,这样漫游车就会稍微向前倾斜在滑板上。在底盘上标记车轮的位置。
-
在每侧钻三个孔,使用电缆扎带安装车轮。如果电缆扎带不够长,你可以通过将一个扎带的末端穿过另一个扎带的末端来连接它们(只拉到扎带能够抓住的程度,以便延长扎带)。以下图示展示了如何使用电缆扎带:
将电机牢固地固定在底盘上
- 接下来,通过将电池插入单元来测试电机;然后,断开原本连接到灯泡的电线,并将它们接触到电机触点上。确定电机上哪个连接应该是正极,哪个应该是负极,以便电机使机器人向前移动(当机器人面向前方时,轮子的顶部应该向前移动)。将红色和黑色电线连接到电机上(在我的电机上,黑色在电机顶部代表负极,红色在底部代表正极),确保电线足够长,可以到达底盘上的任何位置(大约 14 厘米,即约 5.5 英寸,对于夜灯来说已经足够了)。
罗伏-派机器人组件应按照以下图示进行接线:
Rover-Pi 机器人的布线布局
为了建立联系,请执行以下步骤:
-
将电机的黑色线连接到达林顿模块的OUT 1(左侧)和OUT 2(右侧)输出,并将红色线连接到最后一个引脚(COM 连接)。
-
接下来,将电池线连接到模块底部的GND/V-和V+连接处。
-
最后,将 GPIO 连接器(引脚 6)的GND连接到相同的GND连接。
-
通过将 3.3V(GPIO Pin 1)连接到IN1或IN2来测试电机控制,以模拟 GPIO 输出。当你满意时,将 GPIO Pin 16连接到IN1(用于左侧)和 GPIO Pin 18连接到IN2(用于右侧)。
线路现在应与以下表格中给出的细节相匹配:
树莓派 GPIO | 达林顿模块 |
---|---|
引脚 16:左侧 | IN1 |
引脚 18:右 | IN2 |
引脚 6: GND | GND/V-(标记为 -) |
4 x AA 电池达林顿模块 | |
电池的正面 | V+(标记为+) |
电池的负面 | GND/V-(标记为-) |
电机 | |
左电机:黑色电线 | OUT 1(白色插座中的顶部插针) |
右侧电机:黑色电线 | OUT 2(白色插座中的第二个插孔) |
两个电机:红色电线 | COM(白色插座中的最后一个引脚) |
使用以下 rover_drivefwd.py
脚本进行控制测试:
#!/usr/bin/env python3
#rover_drivefwd.py
#HARDWARE SETUP
# GPIO
# 2[==X====LR====]26[=======]40
# 1[=============]25[=======]39
import time
import wiringpi2
ON=1;OFF=0
IN=0;OUT=1
STEP=0.5
PINS=[16,18] # PINS=[L-motor,R-motor]
FWD=[ON,ON]
RIGHT=[ON,OFF]
LEFT=[OFF,ON]
DEBUG=True
class motor:
# Constructor
def __init__(self,pins=PINS,steptime=STEP):
self.pins = pins
self.steptime=steptime
self.GPIOsetup()
def GPIOsetup(self):
wiringpi2.wiringPiSetupPhys()
for gpio in self.pins:
wiringpi2.pinMode(gpio,OUT)
def off(self):
for gpio in self.pins:
wiringpi2.digitalWrite(gpio,OFF)
def drive(self,drive,step=STEP):
for idx,gpio in enumerate(self.pins):
wiringpi2.digitalWrite(gpio,drive[idx])
if(DEBUG):print("%s:%s"%(gpio,drive[idx]))
time.sleep(step)
self.off()
def cmd(self,char,step=STEP):
if char == 'f':
self.drive(FWD,step)
elif char == 'r':
self.drive(RIGHT,step)
elif char == 'l':
self.drive(LEFT,step)
elif char == '#':
time.sleep(step)
def main():
import os
if "CMD" in os.environ:
CMD=os.environ["CMD"]
INPUT=False
print("CMD="+CMD)
else:
INPUT=True
roverPi=motor()
if INPUT:
print("Enter CMDs [f,r,l,#]:")
CMD=input()
for idx,char in enumerate(CMD.lower()):
if(DEBUG):print("Step %s of %s: %s"%(idx+1,len(CMD),char))
roverPi.cmd(char)
if __name__=='__main__':
try:
main()
finally:
print ("Finish")
#End
记住在使用本章中的脚本之前应该安装wiringpi2
(参见第十章,感知和显示现实世界数据)。
使用以下命令运行前面的代码:
sudo python3 rover_drivefwd.py
脚本将提示您以下信息:
Enter CMDs [f,r,l,#]:
您可以输入一系列命令来执行;例如:
ffrr#ff#llff
前述命令将指导 Rover-Pi 机器人执行一系列动作:前进(f
)、向右(r
)、暂停(#
)和向左(l
)。
它是如何工作的...
一旦你搭建好机器人并将轮子连接到电机控制器,你就可以研究如何控制它。
首先导入time
模块(这将允许你在电机控制中添加暂停)和wiringpi2
模块(以允许控制 GPIO 引脚)。在这里使用wiringpi2
,因为它使得使用 I/O 扩展器和其他 I²C 设备变得更加容易,如果你以后需要的话。
定义用于设置引脚ON
/OFF
、方向IN
/OUT
以及每个电机STEP
持续时间的值。同时,定义哪些PINS
连接到电机控制,以及我们的移动,FWD
、RIGHT
和LEFT
。移动的定义方式是,通过同时开启两个电机,你会向前移动,而只开启一个电机,你会转向。通过在文件开头使用变量设置这些值,我们的代码更容易维护和理解。
我们定义了一个motor
类,这将允许我们在其他代码中重用它,或者轻松地将其与替代的motor
类进行交换,以便如果我们想使用其他硬件时可以使用。我们设置了我们正在使用的默认引脚和我们的steptime
值(steptime
对象定义了在每一步中我们驱动电机(s)的时间)。然而,如果需要,这两个值在初始化对象时仍然可以指定。
接下来,我们调用 GPIOsetup()
;它选择物理引脚编号模式(因此我们可以按照它们在板上的位置来引用引脚)。我们还设置我们使用的所有引脚为输出模式。
最后,对于电机
类,我们定义了以下三个函数:
-
我们定义的第一个函数(称为
off()
)将允许我们关闭电机,因此我们将遍历引脚列表并将每个 GPIO 引脚设置为低电平(从而关闭电机)。 -
drive()
函数允许我们提供一组驱动动作列表(每个 GPIO 引脚的ON
和OFF
组合)。同样,我们遍历每个引脚,并将它们设置为相应的驱动动作,等待步进时间,然后使用off()
函数关闭电机。 -
我们定义的最后一个函数(称为
cmd()
)仅仅允许我们发送char
(单个字符)并使用它来选择我们想要使用的驱动动作集合(FWD
、RIGHT
或LEFT
,或者等待(#
))。
对于测试,main()
允许我们使用以下命令从命令行指定需要执行的一系列操作:
sudo CMD=f#lrr##fff python3 rover_drivefwd.py
使用 os.environ
(通过导入 os
模块以便使用它),我们可以在命令中检查 CMD
,并将其用作我们的驱动动作列表。如果没有提供 CMD
命令,我们可以使用 input()
函数直接提示输入驱动动作列表。要使用 motor
类,我们设置 roverPi=motor()
;这允许我们使用驱动动作列表中的每个字符调用(电机类的)cmd()
函数。
还有更多...
你的机器人应该只受限于你自己的创造力。有许多合适的底盘选项、其他电机、轮子和控制及驱动轮子的方式。你应该进行实验和测试,以确定哪些组合效果最佳。这些都是乐趣的一部分!
达尔林顿阵列电路
达尔林顿晶体管是一种驱动高功率设备(如电机或甚至继电器)的低成本方法。它们由两个串联排列的晶体管组成,其中一个晶体管向另一个晶体管供电(允许电流增益相乘)。也就是说,如果第一个晶体管的增益为 20,第二个晶体管的增益也为 20,那么它们共同将提供 400 的总增益。
这意味着在以下图中,基极引脚(1)上的 1 mA 电流可以使你通过达林顿晶体管驱动高达 400 mA 的电流。达林顿晶体管的电气符号在以下图中显示:
达林顿晶体管的电气符号展示了两个晶体管是如何封装在一起的
ULN2003 芯片在之前的模块中使用,并提供七个 NPN 达林顿晶体管(如果需要更多输出或与两个步进电机一起使用,还可以使用八通道版本,即 ULN2803)。以下图示展示了如何使用达林顿阵列来驱动电机:
使用达林顿阵列驱动两个小型电机
芯片每个输出端可提供最大 500 mA 的电流,电压高达 50V(足以驱动大多数小型电机)。然而,随着使用时间的延长,芯片可能会过热,因此在驱动大型电机时建议使用散热片。芯片内部集成了用于保护的二极管,每个达林顿晶体管之间都连接了一个。这是必要的,因为当电机在没有驱动的情况下移动时(这可能是由于电机的自然惯性引起的),它会像一个发电机一样工作。会产生一个称为反电动势的逆电压,如果不通过二极管耗散,将会损坏晶体管。
芯片的一个缺点是正电源电压必须始终连接到公共引脚(COM),因此每个输出只能吸收电流。也就是说,它只能驱动电机单向运行,COM 引脚上为正电压,OUT 引脚上为负电压。因此,如果我们希望以不同的方向驱动我们的 Rover-Pi 机器人,我们需要一个不同的解决方案(参见使用高级电机控制菜谱中的下一个示例)。
这些芯片也可以用来驱动某些类型的步进电机。来自www.dx.com
的一个模块将步进电机作为套件的一部分。尽管齿轮是为了非常缓慢的运动设计的,大约每转 12 秒(对于漫游车来说太慢了),但对于时钟来说仍然很有趣(或许)。
晶体管和继电器电路
继电器能够处理功率更高的电机,因为它们是由电磁线圈控制的机械开关,该线圈会物理地移动触点。然而,它们需要相当大的电流才能开启——通常超过 3.3V。为了切换即使是小型的继电器,我们需要大约 60 mA 的 5V 电流(这超过了 GPIO 提供的电流),因此我们仍然需要使用一些额外的组件来切换它。
我们可以使用达林顿阵列(如之前所述)或一个小型晶体管(任何小型晶体管,例如 2N2222,都将是合适的)来提供切换所需的电流和电压。以下电路将使我们能够做到这一点:
用于驱动外部电路的晶体管和继电器电路
就像电机一样,继电器也可以产生电磁感应峰值,因此需要一个保护二极管来避免晶体管上出现任何反向电压。
这是一个非常有用的电路,不仅适用于驱动电机,而且适用于任何外部电路;物理开关允许它与控制它的 Raspberry Pi 独立且电隔离。
只要正确选择继电器,您就可以通过它驱动直流或交流设备。
您可以使用一些继电器来控制由主电源供电的设备。然而,这只能非常谨慎地进行,并且需要适当的电气培训。主电源的电力可能致命或造成严重伤害。
PiBorg 拥有一个名为 PicoBorg 的现成模块,该模块允许切换多达四个继电器。它使用被称为 金属氧化物半导体场效应晶体管 (MOSFETs) 的设备,这些设备本质上是在之前讨论过的相同原理下工作的晶体管的高功率版本。
绑定或非绑定机器人
在设计自己的 Rover-Pi 机器人时,一个重要的选择是决定你是否希望使其完全独立,或者你是否愿意有一个连接(一根连接到 Rover-Pi 的长控制/电源电缆)。使用连接线,你可以降低 Rover-Pi 机器人的重量,这意味着小型电机可以轻松地移动单元。这将允许你将 Raspberry Pi 与主单元分开,以便它可以连接到屏幕和键盘,便于编程和调试。主要的缺点是,你将需要一个长长的脐带状连接到你的 Rover-Pi 机器人(每个控制信号都有一根线),这可能会阻碍其移动。然而,正如我们稍后将会看到的,你可能只需要三根或四根线就能提供你所需要的所有控制(参见下一道菜谱中的使用 I/O 扩展器部分)。
如果你打算直接将 Raspberry Pi 安装在 Rover-Pi 机器人上,你需要一个合适的电源,例如手机充电电池组。如果电池组有两个 USB 端口,那么你可能能够将其用作电源来驱动 Raspberry Pi 和电机。该单元必须能够独立维持供应,因为驱动电机可能引起的任何电源尖峰都可能导致 Raspberry Pi 重置。
记住,如果树莓派现在连接到了机器人,你需要一种控制它的方法。这可能是一个允许通过 SSH 远程连接的 USB Wi-Fi 外置设备(等等),或者是一个使用 RF/蓝牙的无线键盘,甚至可以是来自第九章, 使用 Python 驱动硬件中的 GPIO D-Pad,它可以用来进行直接控制。
然而,你安装在底盘上的东西越多,电机就需要更努力地工作才能移动。你可能发现需要更强的电机,而不是这里使用的那些小电机。以下照片展示了一个由 USB 电池组供电的 Rover-Pi 机器人:
一款由电池供电的 Raspberry Rover-Pi 机器人,可通过 Wi-Fi(可选电缆管理)进行控制
探索者套件
如果你不想自己制作底盘,也有许多现成的漫游车底盘可供选择。具体如下:
-
来自
www.sparkfun.com/
的 2WD 魔术师机器人底盘 -
4-Motor Smart Car Chassis from
www.dx.com/
-
来自
www.dx.com/
的 2-Wheel 智能汽车模型
TiddlyBot 展示了如何在单个平台上集成多个组件,正如我在修改版本中所展示的那样
一个特别出色的机器人设置是 TiddlyBot(来自www.PiBot.org
),它集成了多个传感器、连续伺服系统、内置电池组和 Raspberry Pi 摄像头。SD 卡已设置,使 TiddlyBot 充当 Wi-Fi 热点,提供带有远程控制界面的简单拖放编程平台。
这展示了如何将本章描述的简单组件组合成一个完整的系统:
TiddlyBot 图形用户界面提供了一个跨平台的拖放界面,以及 Python 支持
一定要查看附录,硬件和软件列表;它列出了本章中使用的所有物品以及你可以从哪里获取它们。
使用高级电机控制
之前的驱动电路不适用于驱动多方向运行的电机(因为它们只能控制电机的开启或关闭)。然而,使用名为 H 桥的电路,你还可以切换和控制电机的方向。
开关组合在以下图中展示:
电机方向可以通过不同的开关组合来控制
通过使用不同的切换组合,我们可以通过切换电机的正负电源来改变电机的方向(SW1 和 SW4 激活电机,而 SW2 和 SW3 使电机反转)。然而,我们不仅需要为每个电机配备四个切换装置,而且由于 ULN2X03 设备和 PiBorg 的 PicoBorg 模块只能吸收电流,因此还需要等效的设备来提供电流(以构成开关的上部部分)。
幸运的是,存在专门设计的 H 桥芯片,例如 L298N,它们内部包含之前的电路,从而提供了一种强大且便捷的方式来控制电机。
准备就绪
我们将用以下图片中显示的 H 桥电机控制器替换之前的达林顿阵列模块:
H 桥电机控制器允许对电机进行方向控制
L298N 的数据表可在www.st.com/resource/en/datasheet/l298.pdf
获取。
如何做到这一点...
该单元需要按照以下方式接线(这与其他类型的 H-bridge 控制器类似,但如有疑问,请查阅相关数据表)。
以下表格展示了电机和电机电源如何连接到 H 桥控制器模块:
模块的电机侧 – 连接到电池和电机 |
---|
电机 A |
左电机红色线 |
以下表格展示了 H 桥控制器模块如何连接到树莓派:
模块控制端 – 连接到树莓派 GPIO 引脚头 |
---|
ENA |
无 |
建议您保持上拉电阻跳线(UR1-UR4)开启状态,并允许电机电源为板载电压调节器供电,该调节器反过来将为 L298N 控制器(跳线 5V_EN)供电。板载调节器(78M05 器件)可提供高达 500 mA 的电流,足以满足 L298N 控制器以及任何额外的电路,例如 I/O 扩展器(更多信息请参阅更多内容...部分)。ENA 和 ENB 引脚应断开连接(默认情况下,电机输出将保持启用状态)。
您需要对之前的 rover_drivefwd.py
脚本进行以下修改(您可以将其保存为 rover_drive.py
)。
在文件顶部,重新定义PINS
,如下所示:
PINS=[15,16,18,22] # PINS=[L_FWD,L_BWD,R_FWD,R_BWD]
并更新控制模式,如下所示:
FWD=[ON,OFF,ON,OFF]
BWD=[OFF,ON,OFF,ON]
RIGHT=[OFF,ON,ON,OFF]
LEFT=[ON,OFF,OFF,ON]
接下来,我们需要将反向命令添加到cmd()
函数中,如下所示:
def cmd(self,char,step=STEP):
if char == 'f':
self.drive(FWD,step)
elif char == 'b':
self.drive(BWD,step)
elif char == 'r':
self.drive(RIGHT,step)
elif char == 'l':
self.drive(LEFT,step)
elif char == '#':
time.sleep(step)
最后,我们可以在main()
函数内部的提示中添加b
(反向)作为一个选项,如下所示:
print("Enter CMDs [f,b,r,l,#]:")
它是如何工作的...
H 桥电机控制器通过增加额外的电路来重现先前的开关电路,以确保电子开关不会造成短路(通过不允许SW1和SW3或SW2和SW4同时启用)。
H-桥电机控制器的开关电路如下所示:
H-桥开关电路的近似(在电机关闭状态下)
输入(IN1 和 IN2)将在电机上产生以下动作:
IN1****IN2 | 0 | 1 |
---|---|---|
0 | 电机关闭 | 电机倒退 |
1 | 电机前进 | 电机关闭 |
正如我们在上一道菜谱中所做的那样,我们可以通过同时驱动两个电机向前移动;然而,现在我们还可以将它们都向后驱动(以向后移动),以及以相反的方向驱动(使我们能够原地转动 Rover-Pi 机器人)。
还有更多...
我们可以使用脉冲宽度调制(PWM)信号来对电机实现更精细的控制,并使用 I/O 扩展器来扩展可用的输入/输出。
使用 PWM 控制进行电机速度控制
目前,Rover-Pi 机器人的电机是通过开关控制来控制的;然而,如果机器人移动速度过快(例如,如果你安装了更大的电机或使用了更高的齿轮比),我们可以利用控制器上的ENA和ENB输入。如果这些设置较低,则禁用电机输出,如果设置为高,则再次启用。因此,通过用 PWM 信号驱动它们,我们可以控制电机的速度。我们甚至可以设置略微不同的 PWM 速率(如果需要),以补偿电机/车轮或表面的任何差异,从而以略微不同的速度驱动它们,如下面的图示所示:
PWM 信号控制着开启和关闭时间的比例
PWM 信号是一种数字式的开/关信号,其开时间与关时间不同。使用 50:50 的开:关
信号的电机驱动将使电机以 100%开信号功率的一半运行,因此运行速度会较慢。通过使用不同的比率,我们可以使电机以不同的速度运行。
我们可以使用树莓派的硬件 PWM(GPIO 引脚 12 可以使用 PWM 驱动器)。
PWM 驱动器通常提供模拟音频输出的一个音频通道。有时,这会产生干扰;因此,建议您断开连接到模拟音频插座的任何设备。
在wiringpi2
中通过将引脚模式设置为2
(即PWM
的值)并指定开启时间(表示为ON_TIME
)如下启用硬件 PWM 功能:
PWM_PIN=12; PWM=2; ON_TIME=512 #0-1024 Off-On
def GPIOsetup(self):
wiringpi2.wiringPiSetupPhys()
wiringpi2.pinMode(PWM_PIN,PWM)
wiringpi2.pwmWrite(PWM_PIN,ON_TIME)
for gpio in self.pins:
wiringpi2.pinMode(gpio,OUT)
然而,这仅适用于联合 PWM 电机控制(因为它连接到 ENA 和 ENB),因为只有一个可用的硬件 PWM 输出。
另一个选择是使用wiringpi2
的软件 PWM 功能。这通过软件生成一个粗略的 PWM 信号;根据您的需求,这可能是可以接受的。在 GPIO 引脚 7 和 GPIO 引脚 11 上生成软件 PWM 信号的代码如下:
PWM_PIN_ENA=7;PWM_PIN_ENA=11;RANGE=100 #0-100 (100Hz Max)
ON_TIME1=20; ON_TIME2=75 #0-100
ON_TIME1=20 #0-100
def GPIOsetup(self):
wiringpi2.wiringPiSetupPhys()
wiringpi2.softPwmCreate(PWM_PIN_ENA,ON_TIME1,RANGE)
wiringpi2.softPwmCreate(PWM_PIN_ENB,ON_TIME2,RANGE)
for gpio in self.pins:
wiringpi2.pinMode(gpio,OUT)
之前的代码将两个引脚都设置为 100 Hz,其中 GPIO 引脚 7 设置为开启时间为 2 ms(关闭时间为 8 ms),GPIO 引脚 11 设置为 7.5 ms/2.5 ms。
要调整 PWM 定时,请使用wiringpi2.softPwmWrite(PWM_PIN_ENA,ON_TIME2)
。
PWM 信号的精度可能会被其他系统进程中断,但它可以控制一个小型微服,即使它略有抖动。
使用 I/O 扩展器
如我们之前所见(在第十章,感知与显示现实世界数据),wiringpi2
允许我们轻松调整代码以利用 I²C 使用 I/O 扩展器。在这种情况下,添加额外的电路可能很有用,例如传感器和 LED 状态指示器,甚至可能还有显示屏和控制按钮,以帮助你在开发过程中调试和控制 Rover-Pi 机器人。
如果您打算将其用作固定连接的设备,这将特别有用,因为您只需要三根线就可以连接回树莓派(I²C 数据 GPIO 引脚 3、I²C 时钟 GPIO 引脚 5 和地 GPIO 引脚 6),I²C VCC 由电机控制器的 5V 输出提供。
如前例所示,添加 I²C 地址和引脚基的宏定义,如下所示:
IO_ADDR=0x20
AF_BASE=100
然后,在 gpiosetup()
函数中,使用以下代码设置 MCP23017 设备:
wiringpi2.mcp23017Setup(AF_BASE,IO_ADDR)
确保您所引用的任何引脚编号为 100-115(以引用 I/O 扩展器的引脚 A0-7 和 B0-7),并添加AF_BASE
(这是 I/O 扩展器的引脚偏移量)。
构建一个六足 Pi-Bug 机器人
控制电机对于创建类似车辆的机器人非常有用,但创建更自然行为的机器人组件(例如伺服电机)可以提供出色的结果。有许多类似昆虫的机器人创意设计,甚至还有双足设计(具有类似人腿的设计),它们使用伺服电机提供自然的关节运动。本例中的设计使用了三个伺服电机,但这些原则和概念可以轻松应用于更复杂的设计,以控制使用多个伺服电机的腿/手臂。以下照片展示了 Pi-Bug 机器人:
六足 Pi-Bug 机器人使用伺服驱动器来控制三个伺服电机,在周围爬行
准备工作
您需要以下硬件:
-
PWM 驱动模块:需要一个驱动模块,例如 Adafruit 16-Channel 12-bit PWM/Servo 驱动器。这个模块使用 PCA9685 设备;有关详细信息,请参阅
www.adafruit.com/datasheets/PCA9685.pdf
数据表。 -
三个微型伺服电机:MG90S 9g 金属齿轮伺服电机以低廉的成本提供合理的扭矩。
-
粗重的导线:这将形成腿的部分;三个巨大的回形针(76 毫米/3 英寸)非常适合这个用途。
-
轻质线缆夹具:这些将用于将腿部连接到伺服电机,并将伺服电机安装到主板上。
-
一小块胶合板或纤维板:可以在其上钻孔,并将伺服电机安装在其上。
您需要安装wiringpi2
来控制 PWM 模块,并且安装 I²C 工具进行调试将非常有用。有关如何安装wiringpi2
和 I²C 工具的详细信息,请参阅第十章,“感知和显示现实世界数据”*。以下图表显示了 I²C 连接:
Raspberry Pi GPIO 头上的 I²C 连接
如何做到这一点...
Pi-Bug 机器人使用三个伺服电机,两侧各一个,中间一个。通过在伺服电机身体两侧各钻一个孔,将电线或电缆绑带穿过孔中,然后拉紧以固定伺服电机。
将纸夹的金属丝弯曲成合适的形状以形成 Pi-Bug 机器人的腿部,并添加一个小弯折,这样你可以将腿部安全地连接到伺服臂上。建议你在将伺服臂固定到位之前,首先运行程序,并将 Pi-Bug 机器人设置为初始位置 h
。这将确保腿部位于中间位置。
以下图表展示了 Pi-Bug 机器人的组件:
Pi-Bug 机器人的组件布局
创建以下用于控制伺服电机的 servoAdafruit.py
脚本:
#!/usr/bin/env python3
#servoAdafruit.py
import wiringpi2
import time
#PWM Registers
MODE1=0x00
PRESCALE=0xFE
LED0_ON_L=0x06
LED0_ON_H=0x07
LED0_OFF_L=0x08
LED0_OFF_H=0x09
PWMHZ=50
PWMADR=0x40
class servo:
# Constructor
def __init__(self,pwmFreq=PWMHZ,addr=PWMADR):
self.i2c = wiringpi2.I2C()
self.devPWM=self.i2c.setup(addr)
self.GPIOsetup(pwmFreq,addr)
def GPIOsetup(self,pwmFreq,addr):
self.i2c.read(self.devPWM)
self.pwmInit(pwmFreq)
def pwmInit(self,pwmFreq):
prescale = 25000000.0 / 4096.0 # 25MHz / 12-bit
prescale /= float(pwmFreq)
prescale = prescale - 0.5 #-1 then +0.5 to round to
# nearest value
prescale = int(prescale)
self.i2c.writeReg8(self.devPWM,MODE1,0x00) #RESET
mode=self.i2c.read(self.devPWM)
self.i2c.writeReg8(self.devPWM,MODE1,
(mode & 0x7F)|0x10) #SLEEP
self.i2c.writeReg8(self.devPWM,PRESCALE,prescale)
self.i2c.writeReg8(self.devPWM,MODE1,mode) #restore mode
time.sleep(0.005)
self.i2c.writeReg8(self.devPWM,MODE1,mode|0x80) #restart
def setPWM(self,channel, on, off):
on=int(on)
off=int(off)
self.i2c.writeReg8(self.devPWM,
LED0_ON_L+4*channel,on & 0xFF)
self.i2c.writeReg8(self.devPWM,LED0_ON_H+4*channel,on>>8)
self.i2c.writeReg8(self.devPWM,
LED0_OFF_L+4*channel,off & 0xFF)
self.i2c.writeReg8(self.devPWM,LED0_OFF_H+4*channel,off>>8)
def main():
servoMin = 205 # Min pulse 1ms 204.8 (50Hz)
servoMax = 410 # Max pulse 2ms 409.6 (50Hz)
myServo=servo()
myServo.setPWM(0,0,servoMin)
time.sleep(2)
myServo.setPWM(0,0,servoMax)
if __name__=='__main__':
try:
main()
finally:
print ("Finish")
#End
创建以下bug_drive.py
脚本以控制 Pi-Bug 机器人:
#!/usr/bin/env python3
#bug_drive.py
import time
import servoAdafruit as servoCon
servoMin = 205 # Min pulse 1000us 204.8 (50Hz)
servoMax = 410 # Max pulse 2000us 409.6 (50Hz)
servoL=0; servoM=1; servoR=2
TILT=10
MOVE=30
MID=((servoMax-servoMin)/2)+servoMin
CW=MID+MOVE; ACW=MID-MOVE
TR=MID+TILT; TL=MID-TILT
FWD=[TL,ACW,ACW,TR,CW,CW]#[midL,fwd,fwd,midR,bwd,bwd]
BWD=[TR,ACW,ACW,TL,CW,CW]#[midR,fwd,fwd,midL,bwd,bwd]
LEFT=[TR,ACW,CW,TL,CW,ACW]#[midR,fwd,bwd,midL,bwd,fwd]
RIGHT=[TL,ACW,CW,TR,CW,ACW]#[midL,fwd,bwd,midR,bwd,fwd]
HOME=[MID,MID,MID,MID,MID,MID]
PINS=[servoM,servoL,servoR,servoM,servoL,servoR]
STEP=0.2
global DEBUG
DEBUG=False
class motor:
# Constructor
def __init__(self,pins=PINS,steptime=STEP):
self.pins = pins
self.steptime=steptime
self.theServo=servoCon.servo()
def off(self):
#Home position
self.drive(HOME,step)
def drive(self,drive,step=STEP):
for idx,servo in enumerate(self.pins):
if(drive[idx]==servoM):
time.sleep(step)
self.theServo.setPWM(servo,0,drive[idx])
if(drive[idx]==servoM):
time.sleep(step)
if(DEBUG):print("%s:%s"%(gpio,drive[idx]))
def cmd(self,char,step=STEP):
if char == 'f':
self.drive(FWD,step)
elif char == 'b':
self.drive(BWD,step)
elif char == 'r':
self.drive(RIGHT,step)
elif char == 'l':
self.drive(LEFT,step)
elif char == 'h':
self.drive(HOME,step)
elif char == '#':
time.sleep(step)
def main():
import os
DEBUG=True
if "CMD" in os.environ:
CMD=os.environ["CMD"]
INPUT=False
print("CMD="+CMD)
else:
INPUT=True
bugPi=motor()
if INPUT:
print("Enter CMDs [f,b,r,l,h,#]:")
CMD=input()
for idx,char in enumerate(CMD.lower()):
if(DEBUG):print("Step %s of %s: %s"%(idx+1,len(CMD),char))
bugPi.cmd(char)
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print ("Finish")
#End
它是如何工作的...
我们通过探索如何使用 PWM 来控制伺服电机来解释之前的脚本函数。接下来,我们将看到伺服类提供了哪些方法来控制 PCA9685 设备。最后,我们将研究三个伺服电机的运动如何组合,为 Pi-Bug 机器人本身产生前进和转向动作。
控制伺服电机
为了控制用于 Pi-Bug 机器人的伺服电机,我们需要一个特殊的控制信号,该信号将决定伺服电机需要移动到的角度。我们将向伺服电机发送一个 PWM 信号,其中开启时间的持续时间将允许我们控制伺服臂的角度(从而,允许我们控制 Pi-Bug 机器人的腿部)。以下图示展示了如何使用 PWM 信号来控制伺服电机的角度:
伺服电机的角度由 PWM 信号的上升时间持续时间控制
大多数伺服电机的角度范围大约为 180 度,中间位置为 90 度。50 Hz 的 PWM 频率将有一个周期为 20 ms,90 度的中间位置通常对应于上升时间为 1.5 ms,对于接近 0 度和接近 180 度的范围,其变化范围为±0.5 ms 至 0.4 ms。每种类型的伺服电机可能会有所不同,但如果需要,你应该能够调整代码以适应。以下图表显示了您如何使用不同的 PWM 上升时间来控制伺服电机的角度:
伺服角度通过发送 1 毫秒到 2 毫秒之间的 PWM 上升时间来控制
另一种伺服类型被称为连续伺服(此处未使用)。它允许您控制旋转速度而不是角度,并且会根据已应用的 PWM 信号以恒定速度旋转。这两种伺服类型都具有内部反馈回路,会持续驱动伺服直到达到所需的角或速度。
虽然从理论上讲可以使用软件生成这些信号,但你将发现系统上其他进程的任何微小中断都会干扰信号时序;反过来,这会导致伺服机构产生不规律的反应。这就是我们使用硬件 PWM 控制器的原因,它只需要设置特定的上升和下降时间,然后就能自动为我们生成所需的信号。
伺服类
伺服代码基于 Adafruit 为他们模块使用的 PWM 驱动器;然而,它并不兼容 Python 3,因此我们需要创建自己的版本。我们将使用wiringpi2
I²C 驱动器来初始化和控制 I²C PWM 控制器。我们定义了我们将需要使用的寄存器(请参阅 PCA9685 设备的数据表),以及其默认总线地址,0x40
(PWMADR),以及 50 Hz(PWMHZ)的 PWM 频率。
在我们的伺服类中,我们在 wiringpi2
中初始化 I²C 驱动器,并在总线上设置我们的 devPWM
设备。接下来,我们初始化 PWM 设备本身(使用 pwmInit()
)。我们必须计算设备所需的预分频器,以便将板载 25 MHz 时钟转换为 50 Hz 信号,以生成我们需要的 PWM 频率;我们将使用以下公式:
预缩放寄存器值使用 12 位值缩放 25 MHz 时钟来设置 PWM 频率
预缩放值被加载到设备中,并触发设备重置以启用它。
接下来,我们创建一个函数来允许控制 PWM 的ON
和OFF
时间。ON
和OFF
时间是 12 位值(0-4096),因此每个值被分成高字节和低字节(每个 8 位),这些字节需要加载到两个寄存器中。对于L
(低)寄存器,我们使用&0xFF
屏蔽掉高 8 位,而对于H
(高)寄存器,我们向下移位 8 位以提供高 8 位。每个 PWM 通道将有两个寄存器用于ON
时间和两个用于OFF
时间,因此我们可以将第一个 PWM 通道寄存器的地址乘以 4 和通道号,以获取其他任何通道的地址。
为了测试我们的伺服
类,我们定义了伺服器的最小和最大范围,计算方法如下:
-
50 Hz 的 PWM 频率的周期为 20 ms (T=1/f)
-
开关时间范围从 0-4,096(即 0 毫秒到 20 毫秒)
现在,我们可以计算 0 度(1 毫秒)和 180 度(2 毫秒)的控制值
如下所示:
-
1 毫秒(伺服最小值)等于 4,096/20 毫秒,即 204.8
-
2 毫秒(伺服最大值)等于 4,096/10 毫秒,即 409.6
我们将数值四舍五入到最接近的整数。
学习走路
Pi-Bug 机器人采用了一种常见的设计,允许使用三个伺服电机来创建一个小型、六足的机器人。两端伺服电机提供前进和后退运动,而中间的伺服电机提供控制。以下照片展示了安装好的伺服电机:
伺服电机被安装在电路板的底部,方向是颠倒的
以下表格假设左右伺服电机安装在电路板的底部,且方向颠倒,中间伺服电机垂直安装。如果安装方式不同,您将需要调整代码。
以下表格展示了用于前进的伺服运动:
方向 | 中间(伺服 M) | 左侧(伺服 L) | 右侧(伺服 R) |
---|---|---|---|
主页 | MID /中间 |
MID /中间 |
MID /中间 |
fwdStep1 | TR /向上 |
ACW /腿向前 |
ACW /腿向后 |
fwdStep2 | TL /左侧向上 |
CW /腿向后 |
CW /腿向前 |
以下图表展示了运动如何使 Pi-Bug 机器人向前迈步:
Pi-Bug 机器人向前移动
虽然一开始可能有些令人困惑,但当你看到机器人移动时,应该会更容易理解。
对于第一次前进步,我们将中间伺服器(servoM
)顺时针移动,这样 Pi-Bug 机器人的左侧就能通过剩余中间腿的运动离开地面。然后,我们可以移动左侧伺服器(servoL
)来将左侧的腿向前移动(为后续移动做准备;此时它们并未接触地面)。现在,通过移动右侧伺服器(servoR
),我们可以将右侧的腿向后移动(允许 Pi-Bug 机器人在那一侧被推向前方)。
第二个前进步骤与之前相同,只是我们使用中间伺服器(servoM
)将右侧抬起离地。同样,我们将离地的腿向前移动(为下一次做准备),然后将另一侧的腿向后移动(允许 Pi-Bug 机器人的那一侧向前移动)。通过重复前进步骤,Pi-Bug 机器人将向前移动;或者,通过交换由中间伺服器(servoM
)抬起的两侧,它将向后移动。结果是相当像虫子一样快速地爬行!
要使 Pi-Bug 机器人转向,我们执行类似动作,只不过就像 Rover-Pi 机器人的高级电机控制一样,我们让机器人的一侧前进,另一侧后退。下表显示了用于向右转动的伺服器运动:
方向 | 中间(伺服 M) | 左侧(伺服 L) | 右侧(伺服 R) |
---|---|---|---|
主页 | MID /中间 |
MID /中间 |
MID /中间 |
rightStep1 | TL /左侧向上 |
CW /向后腿 |
ACW /向后腿 |
rightStep2 | TR /向上 |
ACW /腿向前 |
CW /腿向前 |
将 Pi-Bug 机器人转向右侧的步骤如下所示:
Pi-Bug 机器人向右转
要向右转,我们需要将 Pi-Bug 机器人的左侧抬起离地,但这次,我们将两边的腿向后移动。这允许 Pi-Bug 机器人的右侧向前移动。步子的后半部分抬起右侧离地,然后我们向前移动腿(这将推动 Pi-Bug 机器人的左侧向后)。这样,虫子就会在行走时转向;再次强调,只需交换被抬起的两侧,我们就可以改变 Pi-Bug 机器人将转向的方向。
Pi-Bug 行走代码
Pi-Bug 机器人的代码被设计成提供与 Rover-Pi 机器人相同的接口,以便它们可以轻松互换。你应该注意到每个类都包含相同的四个函数(__init__()
, off()
, drive()
, 和 cmd()
)。__init__()
函数定义了我们将要控制的引脚集合,行走动作的steptime
值(这次,动作之间的间隔),以及之前定义的服务器模块。
再次强调,我们有一个 off()
函数,它提供了一个可以被调用的函数来设置伺服电机到中间位置(这在需要将腿部调整到位时非常有用,正如之前在初始位置中所述)。off()
函数使用 drive()
函数将每个伺服电机设置到 MID
位置。MID
值位于 servoMin
和 servoMax
之间的一半(1.5 ms 以给出 90 度的位置)。
drive()
函数与之前的电机控制版本类似;它按照我们在之前讨论的各种运动模式(FWD
、BWD
、LEFT
和 RIGHT
)中定义的,循环执行每个伺服所需的动作。然而,为了重现所需的运动模式,我们在移动中间伺服(servoM
)时,会将其循环两次,并在每次移动中间伺服时插入一个小的延迟。这为伺服移动并给出必要的倾斜,以便在允许它们移动之前将其他腿从地面上抬起提供了时间。
我们将每个伺服命令定义为伺服臂的顺时针(CW)或逆时针/逆时(ACW)运动。由于伺服器是倒置安装的,左伺服器(servoL
)的 ACW(如果从上方看是 CW)运动会使腿部向前移动,而在右伺服器(servoR
)上相同方向的运动会使腿部向后移动(这在之前的图中是 fwdStep1)。这样,每种模式都可以被定义。
一次又一次,我们使用以下命令提供了一个测试函数,该命令允许从命令行或直接在提示符中定义一系列指令:
sudo CMD=fffll##rr##bb##h python3 bug_drive.py
这包括如果需要,添加 h
以返回 home
位置。
直接使用 ServoBlaster 控制伺服电机
之前的配方展示了使用专门的伺服控制器来处理 Pi-Bug 所使用的伺服电机的控制。这具有优势,即 Raspberry Pi 上正在进行的处理中的任何干扰都不会干扰微妙的伺服控制(因为控制器将继续发送正确的信号)。
然而,树莓派也具备直接伺服控制的能力。为了实现这一点,我们将利用理查德·赫斯特的 ServoBlaster,这是一个多伺服驱动器。
在这个菜谱中,我们将控制连接到 MeArm 的四个伺服电机,这是一个简单的激光切割机器人臂;然而,你可以选择将伺服电机安装到你喜欢的任何设备上:
MeArm 是一个由四个微型伺服电机驱动的简单机械臂
准备就绪
大多数伺服电机将有三根线和三个引脚的连接器,如下所示:
黑色/棕色 | 红色 | 橙色/白色/黄色/蓝色 |
---|---|---|
地线 | 正电源(通常为小型伺服电机的 5V) | 信号 |
虽然通常可以直接从 Raspberry Pi 的 GPIO 引脚上的 5V 引脚为伺服器供电,但在移动时它们会消耗相当大的电流。除非你有一个非常好的电源,否则这可能会导致 Raspberry Pi 意外重启,从而风险损坏 SD 卡。因此,建议你为它们单独供电;例如,使用一个额外的 USB 电源和连接到地线和正电源的电缆。
默认情况下,伺服电机可以按照以下方式接线:
伺服 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 所有 GND | 所有 电源 |
---|---|---|---|---|---|---|---|---|---|---|
树莓派 GPIO 引脚 | ||||||||||
7 | 11 | 12 | 13 | 15 | 16 | 19 | 22 | 6 | 无连接 | |
5V 电源 | GND | +5V |
我们将假设我们正在控制四个伺服电机(0、1、2 和 3),这些伺服电机将在稍后安装到 MeArm 或类似设备上:
要安装 ServoBlaster,首先从 Git 仓库下载源文件:
cd ~
wget https://github.com/richardghirst/PiBits/archive/master.zip
解压并打开matplotlib-master
文件夹,操作如下:
unzip master.zip
rm master.zip
cd PiBits-master/ServoBlaster/user
我们将使用位于用户目录中的用户空间守护进程(称为servod
)。在我们能够使用它之前,我们应该使用以下命令来编译它:
make servod
应该没有错误,显示以下文本:
gcc -Wall -g -O2 -o servod servod.c mailbox.c -lm
使用信息,请使用以下命令:
./servod --help
现在我们可以测试一个伺服电机;首先,启动servod
守护进程(设置超时时间为 2,000 毫秒,以便伺服电机移动后关闭):
sudo servod --idle-timeout=2000
您可以将伺服电机的位置移动到伺服电机范围的 0%。
echo 0=0% > /dev/servoblaster
现在,将伺服电机更新到 50%,使伺服电机旋转到 90 度(伺服中点):
echo 0=50% > /dev/servoblaster
如 MeArm 构建说明中所建议,在构建机械臂之前,应该连接并校准伺服电机,以确保每个伺服电机能够在其正确范围内移动机械臂。这是通过确保每个伺服电机都接通电源并被指令移动到中点位置(50%/90 度),并且伺服电机臂被安装到预期的方向上完成的:
在将伺服电机安装到 MeArm 之前,每个伺服电机都应该在正确的位置进行校准。
您现在可以在构建和将它们安装到完成的手臂之前,将每个 MeArm 伺服电机(0、1、2 和 3)设置为它们的中间位置(通过依次命令每个电机到 50%)。
伺服电机可以用来控制除 MeArm 之外的各种替代设备,但你的伺服电机可能需要以类似的方式进行校准:
伺服电机的精度控制意味着它们可以用于各种应用,例如,控制模拟手
如何做到这一点...
- 创建以下
servo_control.py
脚本:
#!/usr/bin/env python3
#servo_control.py
import curses
import os
#HARDWARE SETUP
# GPIO
# 2[=VX==2=======]26[=======]40
# 1[===013=======]25[=======]39
# V=5V X=Gnd
# Servo 0=Turn 1=Shoulder 2=Elbow 3=Claw
name=["Turn","Shoulder","Elbow","Claw"]
CAL=[90,90,90,90]
MIN=[0,60,40,60]; MAX=[180,165,180,180]
POS=list(CAL)
KEY_CMD=[ord('c'),ord('x')]
#Keys to rotate counter-clockwise
KEY_LESS={ord('d'):0,ord('s'):1,ord('j'):2,ord('k'):3}
#Keys to rotate clockwise
KEY_MORE={ord('a'):0,ord('w'):1,ord('l'):2,ord('i'):3}
STEP=5; LESS=-STEP; MORE=STEP #Define control steps
DEG2MS=1.5/180.0; OFFSET=1 #mseconds
IDLE=2000 #Timeout servo after command
SERVOD="/home/pi/PiBits-mater/ServoBlaster/user/servod" #Location of servod
DEBUG=True
text="Use a-d, w-s, j-l and i-k to control the MeArm. c=Cal x=eXit"
def initialize():
cmd=("sudo %s --idle-timeout=%s"%(SERVOD, IDLE))
os.system(cmd)
def limitServo(servo,value):
global text
if value > MAX[servo]:
text=("Max %s position %s:%s"%(name[servo],servo,POS[servo]))
return MAX[servo]
elif value < MIN[servo]:
text=("Min %s position %s:%s"%(name[servo],servo,POS[servo]))
return MIN[servo]
else:
return value
def updateServo(servo,change):
global text
POS[servo]=limitServo(servo,POS[servo]+change)
setServo(servo,POS[servo])
text=str(POS)
def setServo(servo,position):
ms=OFFSET+(position*DEG2MS)
os.system("echo %d=%dus > /dev/servoblaster" %(servo, ms/1000))
def calibrate():
global text
text="Calibrate 90deg"
for i,value in enumerate(CAL):
POS[i]=value
setServo(i,value)
def main(term):
term.nodelay(1)
term.addstr(text)
term.refresh()
while True:
term.move(1,0)
c = term.getch()
if c != -1:
if c in KEY_MORE:
updateServo(KEY_MORE[c],MORE)
elif c in KEY_LESS:
updateServo(KEY_LESS[c],LESS)
elif c in KEY_CMD:
if c == ord('c'):
calibrate()
elif c == ord('x'):
exit()
if DEBUG:term.addstr(text+" ")
if __name__=='__main__':
initialize()
curses.wrapper(main)
#End
- 运行脚本:
python3 servo_control.py
- 您可以按照提示控制安装在 MeArm(或您所使用的任何设备)上的伺服电机:
Use a-d, w-s, j-l and i-k to control the MeArm. c=Cal x=eXit
它是如何工作的...
脚本首先导入curses
和os
模块。标准的 Python input()
命令需要在每次按键后按下Enter键,我们才能对其做出反应。然而,正如我们很快就会看到的,curses
模块仅仅允许我们扫描键盘按键并立即对其做出响应。我们使用os
模块来调用 ServoBlaster 命令,就像我们在终端中做的那样。
首先,我们定义我们的设置,例如伺服映射、校准位置、最小/最大范围、我们的控制键,以及每个控制命令的STEP
大小(以度为单位)。我们还定义了我们的参数,用于计算所需角度(以度为单位)到目标 PWM 信号持续时间(以毫秒为单位)的计算。
对于这些特定的伺服电机,1 毫秒的运行时间等于 0 度,而 2.5 毫秒等于 180 度,因此我们有一个 1 毫秒的偏移量(OFFSET
)和 180 度/1.5 毫秒的缩放比例(DEG2MS
)。
因此,我们所需的正常运行时间(以毫秒为单位)可以计算为 OFFSET + (degreesDEG2MS)*。最后,我们定义 SERVOD
命令行和伺服 IDLE
超时以初始化 ServoBlaster 用户守护进程。在 initialize ()
函数中,我们使用 os.system()
启动 servod
守护进程,就像我们之前做的那样。
为了检测按键,我们调用脚本中的main()
函数,通过curses.wrapper()
,使term
能够控制终端的输入和输出。我们使用term.nodelay(1)
,这样当我们检查是否有按键按下(使用term.getch()
)时,执行将继续正常进行。我们使用term.addstr(text)
向用户显示控制键,然后通过term.refresh()
更新显示。剩余的脚本会检查终端是否有按键按下,并将结果赋值给c
。如果没有按键按下,则term.getch()
返回-1
;否则,返回 ASCII 等效值,我们可以在我们为控制键定义的每个字典中检查它。我们将使用KEY_MORE
和KEY_LESS
来改变伺服位置,使用KEY_CMD
(c
或x
)来允许我们将所有伺服设置到校准位置或干净地退出。最后,如果DEBUG
设置为True
,我们使用term.addstr()
显示任何有用的调试信息,并确保它在终端的(1,0)位置显示(从顶部向下数一行)。
对于常规控制,伺服电机的位置将通过updateServo()
函数进行控制,该函数通过所需的改变(无论是+STEP
还是-STEP
)调整当前位置(存储在 POS 数组中)。我们确保新位置在定义的 MAX/MIN 限制内,并报告是否已达到这些限制。然后,伺服电机被指令通过setServo()
移动到所需位置,指定所需的 PWM 上升时间(以微秒为单位)。
最后一个函数,当按下 C 键时调用 calibrate()
,它简单地设置每个伺服电机到 CAL
数组中定义的角度(使用 setServo()
),并确保当前位置保持最新。
使用红外遥控器与你的树莓派配合使用
远程控制机器人通常很有用。添加额外输入的一个简单方法是通过制作
使用一个红外(IR)接收器和标准遥控器。幸运的是,接收器是
得到充分支持。
我们将使用一个名为 lirc
的模块来捕获和解码来自标准
遥控器。
准备中
LIRC 支持许多类型的红外探测器,例如 Energenie 的 PiMote 红外板;然而,由于我们只需要接收红外信号,我们可以使用一个简单的(TSOP38238)红外探测器:
TSOP38238 红外接收器的三个引脚可以直接安装在 Raspberry Pi 的引脚头上
使用apt-get
命令安装以下软件包:
sudo apt-get install lirc lirc-x
将以下内容添加到 /boot/config.txt
文件中。这将启用驱动程序并定义接收器所安装的引脚(BCM GPIO24):
dtoverlay=lirc-rpi,gpio_in_pin=23
重启 Raspberry Pi 以使配置生效:
sudo reboot
我们现在应该会发现红外设备位于 /dev/lirc0
。如果我们用遥控器指向它并在使用以下命令后按一些按钮,我们可以观察到接收器的输出(使用 Ctrl + Z 退出):
mode2 -d /dev/lirco0
lirc0
资源可能报告为忙碌:
mode2: 无法打开 /dev/lirc0
mode2: default_init(): 设备或资源忙碌
然后我们需要停止lirc
服务:
sudo /etc/init.d/lirc stop
它将给出以下响应:
[ ok ] 停止 lirc(通过 systemctl):lirc.service
当你准备好后,你可以再次启动服务:
sudo /etc/init.d/lirc start
这将给出以下响应:
[ ok ] 启动 lirc (通过 systemctl): lirc.service
你将看到类似以下输出(如果不是,请确保你已经将接收器连接到 Raspberry Pi GPIO 的正确引脚上):
space 16300
pulse 95
space 28794
pulse 80
space 19395
pulse 83
...etc...
现在我们知道我们的设备正在工作,我们可以对其进行配置。
如何做到这一点...
全球 LIRC 配置存储在 /etc/lirc
目录中。我们关注以下文件:
-
hardware.conf
: 定义了我们的红外传感器安装位置以及传感器的整体设置。 -
lircd.conf
: 遥控器配置文件;此文件包含您遥控器按键的记录输出,并将它们映射到特定的按键符号。您通常可以从 lirc.sourceforge.net/remotes 获取预录制的文件,或者您可以记录一个自定义的,如下所示。 -
lircrc
: 此文件提供了每个按键符号到特定命令或键盘映射的映射。
存储在 /etc/lirc
中的所有 LIRC 配置对所有用户都可用;然而,如果需要,可以通过将它们放置在特定的家目录中(例如,/home/pi/.config/
)为每个用户定义不同的配置,从而覆盖默认设置。
设置传感器有三个步骤,每个步骤对应一个 LIRC 配置文件:
- 首先,确保
hardware.conf
文件已设置。对于我们的传感器,我们必须确保以下设置正确:
LIRCD_ARGS="--uinput"
DRIVER="default"
DEVICE="/dev/lirc0"
MODULES="lirc_rpi"
-
接下来,获取一个
lircd.conf
文件;或者,如果您没有为您的遥控器准备一个,我们可以为您生成。接下来的过程将引导您检测遥控器上的每个单独的按键。为了本菜谱的目的,我们只需要映射八个按键(以控制前一个菜谱中的四个伺服器)。 -
如果你想映射额外的键,请使用以下命令来查找所有有效键符号的完整列表:
irrecord --list-namespace
KEY_UP |
KEY_RIGHT |
KEY_VOLUMEUP |
KEY_CHANNELUP |
---|---|---|---|
KEY_DOWN |
KEY_LEFT |
KEY_VOLUMEDOWN |
KEY_CHANNELDOWN |
我们可以使用这个 Goodman 遥控器上的音量、通道和方向按钮作为我们的 MeArm 控制器
首先,我们需要停止lirc
服务,如果它正在运行,则会使用/dev/lirc0
设备:
sudo /etc/init.d/lirc stop
接下来,使用以下命令开始捕获过程:
irrecord -d /dev/lirc0 ~/lircd.conf
使用 irrecord 工具记录遥控器上的每个按钮
现在我们已经捕获了所需的密钥,我们确保远程设备的名称已设置(默认情况下,当捕获按钮时,它将设置为lirc.conf
文件的名称):
nano ~/lircd.conf
在文件中设置远程名称;例如,Goodmans
:
...
begin remote
name Goodmans
bits 16
...
最后,我们可以替换 /etc/lirc
文件夹中的配置:
sudo cp ~/lircd.conf /etc/lirc/lirc.conf
我们可以使用irw
程序来确认映射到遥控器的关键符号,如下所示:
irw
这将报告按键细节和远程控制,其定义如下:
0000000000fe7a85 00 KEY_UP Goodmans
0000000000fe7a85 01 KEY_UP Goodmans
0000000000fe6a95 00 KEY_DOWN Goodmans
0000000000fe6a95 01 KEY_DOWN Goodmans
...
现在,我们可以将键映射到特定的命令;在这种情况下,我们将它们映射到我们用于控制 MeArm 伺服电机的键上。创建一个新的/etc/lirc/lircrc
文件:
sudo nano /etc/lirc/lircrc
用以下内容替换它:
begin
prog=irxevent
button=KEY_UP
config=Key w CurrentWindow
end
begin
prog=irxevent
button=KEY_DOWN
config=Key s CurrentWindow
end
begin
prog=irxevent
button=KEY_LEFT
config=Key a CurrentWindow
end
begin
prog=irxevent
button=KEY_RIGHT
config=Key d CurrentWindow
end
begin
prog=irxevent
button=KEY_VOLUMEUP
config=Key i CurrentWindow
end
begin
prog=irxevent
button=KEY_VOLUMEDOWN
config=Key k CurrentWindow
end
begin
prog=irxevent
button=KEY_CHANNELUP
config=Key l CurrentWindow
end
begin
prog=irxevent
button=KEY_CHANNELDOWN
config=Key j CurrentWindow
end
要应用配置,您可能需要重新启动服务(或者,如果这不起作用,尝试重新启动树莓派):
sudo /etc/init.d/lirc restart
当我们在上一道菜谱中运行servo_control.py
脚本时,遥控器应能直接控制机械臂。
还有更多...
LIRC 支持多个辅助程序,其中irxevent
只是其中之一:
remote |
默认情况下,LIRC 支持一些简单的控制;例如: prog=remote`` button=KEY_UP`` config=UP 这将提供从遥控器(向上、向下、向左、向右,以及也包含 ENTER)的简单光标控制,非常适合简单的菜单控制。 |
---|---|
www.lirc.org/html/configure.html#lircrc_format |
|
irxevent |
模拟在 X 应用程序中的按钮点击和按键操作。您可以指定按键事件发生在 CurrentWindow 或特定名称的窗口中,即 leafpad 。这仅在您从图形桌面环境运行(或使用 X 转发)时才有效。 |
www.lirc.org/html/irxevent.html |
|
irpty |
将红外遥控命令转换为控制特定程序的按键:rog=irpty button=KEY_EXIT config=x 通过指定lircrc 配置和要控制程序启动它:irpty /etc/lirc/lircrc -- leafpad |
www.lirc.org/html/irpty.html |
|
irexec |
允许直接从遥控器运行命令:prog=irexec button=KEY_POWER config=sudo halt #关机 |
www.lirc.org/html/irexec.html |
您可以使用带有所需prog
的ircat
来测试lircrc
文件的任何部分:
ircat irxevent
上述命令将报告以下内容:
Key k CurrentWindow
Key i CurrentWindow
最后,如果您已连接合适的红外发射器 LED(包括保护电阻/开关晶体管),您也可以使用 LIRC 从 Raspberry Pi 发送红外信号。为此,您可以使用irsend
命令,例如:
irsend SEND_ONCE Goodmans KEY_PROGRAMUP
红外输出通道在/boot/config.txt
文件中已启用(假设连接到 GPIO 引脚 19):
dtoverlay=lirc-rpi,gpio_in_pin=24,gpio_out_pin=19
避免物体和障碍物
为了避免障碍,你可以在机器人的周围放置传感器,以便在遇到物体时自动激活。根据你希望机器人如何行动,一种避免策略是简单地反转最后采取的任何动作(对于前进/后退动作,还需要额外转向),以使其中一个传感器被激活。
准备就绪
当与物体发生碰撞时,你需要一些微动开关来触发。根据你拥有的类型,你需要放置足够的开关来检测周围任何物体(如果需要,你可以使用额外的电线长度来扩展开关的感应范围)。以下照片展示了两种可能的传感器,当弹簧或金属臂撞击物体时,这些传感器将导致开关激活。你需要确定哪个开关的触点会打开或关闭电路(这取决于设备):
小型微动开关可以用作碰撞传感器
如何做到这一点...
使用与我们在第九章使用 Python 驱动硬件中使用的方法类似的方法,将开关连接到 GPIO,用于 D-Pad 控制器。以下是开关的电路图:
开关应包括限流电阻(1K 欧姆是理想的)
你如何连接到树莓派的 GPIO 将取决于你的电机/伺服驱动器的接线方式。例如,一个配备 H 桥电机控制器的 Rover-Pi 机器人可以这样接线:
模块控制端 – 连接到树莓派 GPIO 引脚头 |
---|
ENA |
无 |
四个额外的接近/碰撞传感器可以连接到树莓派的 GPIO
如下所示:
接近/碰撞传感器 – 连接到 Raspberry Pi 的 GPIO 引脚头 |
---|
R_FWD |
引脚 7 |
如果你以不同的方式连接,你可以在代码中调整引脚编号,以满足需求。如果你需要额外的引脚,那么任何多功能引脚,例如 RS232 RX/TX(引脚 8 和 10)或 SPI/I²C,也可以用作正常的 GPIO 引脚;只需将它们设置为输入或输出,就像平常一样。通常,我们只是避免使用它们,因为它们通常对扩展和其他用途更有用,所以有时保持它们可用是有用的。
如果你只是使用以下示例代码,甚至可以用单个 GPIO 引脚来连接所有传感器,因为无论哪个传感器被触发,动作都是相同的。然而,通过为每个传感器单独布线,你可以根据障碍物在机器人周围的位置调整你的策略,或者提供有关哪个传感器被触发的额外调试信息。
创建以下 avoidance.py
脚本:
#!/usr/bin/env python3
#avoidance.py
import rover_drive as drive
import wiringpi2
import time
opCmds={'f':'bl','b':'fr','r':'ll','l':'rr','#':'#'}
PINS=[7,11,12,13] # PINS=[L_FWD,L_BWD,R_FWD,R_BWD]
ON=1;OFF=0
IN=0;OUT=1
PULL_UP=2;PULL_DOWN=1
class sensor:
# Constructor
def __init__(self,pins=PINS):
self.pins = pins
self.GPIOsetup()
def GPIOsetup(self):
wiringpi2.wiringPiSetupPhys()
for gpio in self.pins:
wiringpi2.pinMode(gpio,IN)
wiringpi2.pullUpDnControl(gpio,PULL_UP)
def checkSensor(self):
hit = False
for gpio in self.pins:
if wiringpi2.digitalRead(gpio)==False:
hit = True
return hit
def main():
myBot=drive.motor()
mySensors=sensor()
while(True):
print("Enter CMDs [f,b,r,l,#]:")
CMD=input()
for idx,char in enumerate(CMD.lower()):
print("Step %s of %s: %s"%(idx+1,len(CMD),char))
myBot.cmd(char,step=0.01)#small steps
hit = mySensors.checkSensor()
if hit:
print("We hit something on move: %s Go: %s"%(char,
opCmds[char]))
for charcmd in opCmds[char]:
myBot.cmd(charcmd,step=0.02)#larger step
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print ("Finish")
#End
它是如何工作的...
我们导入rover_drive
来控制机器人(如果我们使用 Pi-Bug 机器人,可以使用bug_drive
)以及wiringpi2
,以便我们可以使用 GPIO 来读取传感器(定义为PINS
)。我们定义了opCmds
,它使用 Python 字典来根据原始命令分配新的命令(使用opCmds[char]
,其中char
是原始命令)。
我们创建了一个名为 sensor
的新类,并将每个开关设置为 GPIO 输入(每个都设置了内部上拉电阻)。现在,每当进行移动(如之前所述,从 main()
函数中请求的命令列表),我们都会检查是否有任何开关被触发(通过调用 mySensor.checkSensor()
)。
如果开关被跳闸,我们停止当前的运动,然后向相反方向移动。然而,如果我们向前移动时其中一个传感器被触发,我们就向后移动,然后转向。这允许机器人逐渐避开阻挡其路径的物体,并继续向另一个方向移动。同样,如果我们向后移动并且传感器被触发,我们就向前移动,然后转向。通过结合简单的物体避障和方向信息,可以指挥机器人按照所需的方向导航。
还有更多...
也有方法可以检测到机器人附近的对象,而无需与它们进行实际的身体接触。其中一种方法就是使用超声波传感器,这种传感器通常用于车辆倒车/停车传感器。
超声波倒车传感器
超声波传感器提供了一种测量机器人与障碍物距离的绝佳方法(提供 2 厘米到 20 厘米的测量范围),并且大多数电子爱好者商店都有销售(见附录,硬件和软件列表)。超声波模块通过发送一短串超声波脉冲,然后测量接收器检测回波所需的时间来工作。模块随后在回波输出端产生一个脉冲,其时间等于测量的时间。这个时间等于行进距离除以声速(340.29 米/秒或 34,029 厘米/秒),即从传感器到物体再返回的距离。以下照片展示了超声波模块:
HC-SR04 超声波传感器模块
该传感器需要 5V 电压供电;它有一个输入端用于接收触发脉冲,以及一个输出端用于发送回波脉冲。虽然该模块可以使用 3.3V 触发脉冲工作,但在回波线上它会以 5V 信号响应;因此,需要一些额外的电阻来保护树莓派的 GPIO。
以下电路图显示了传感器输出的连接:
传感器回波输出必须通过分压器连接到树莓派
电阻器 R[t] 和 R[b] 构成一个分压器;目的是将回声电压从 5V 降低到大约 3V(但不少于 2.5V)。使用来自 第十章, 感知与显示现实世界数据 的以下公式来获取输出电压:
使用此公式计算分压器的输出电压(Vout)
这意味着我们应该将R[t]到R[b]的比率设定为 2:3 以获得 3V(且不低于 1:1,这将给出 2.5V);也就是说,R[t]等于 2K 欧姆,R[b]等于 3K 欧姆,或者 330 欧姆和 470 欧姆都是可以的。
如果你有一个电压表,你可以检查它(其他所有设备都断开连接)。将分压器的顶部连接到 GPIO 引脚 2(5V),底部连接到 GPIO 引脚 6(GND),然后测量中间的电压(应该大约是 3V)。
创建以下 sonic.py
脚本:
#!/usr/bin/python3
#sonic.py
import wiringpi2
import time
import datetime
ON=1;OFF=0; IN=0;OUT=1
TRIGGER=15; ECHO=7
PULSE=0.00001 #10us pulse
SPEEDOFSOUND=34029 #34029 cm/s
def gpiosetup():
wiringpi2.wiringPiSetupPhys()
wiringpi2.pinMode(TRIGGER,OUT)
wiringpi2.pinMode(ECHO,IN)
wiringpi2.digitalWrite(TRIGGER,OFF)
time.sleep(0.5)
def pulse():
wiringpi2.digitalWrite(TRIGGER,ON)
time.sleep(PULSE)
wiringpi2.digitalWrite(TRIGGER,OFF)
starttime=time.time()
stop=starttime
start=starttime
while wiringpi2.digitalRead(ECHO)==0 and start<starttime+2:
start=time.time()
while wiringpi2.digitalRead(ECHO)==1 and stop<starttime+2:
stop=time.time()
delta=stop-start
print("Start:%f Stop:%f Delta:%f"%(start,stop,delta))
distance=delta*SPEEDOFSOUND
return distance/2.0
def main():
global run
gpiosetup()
while(True):
print("Sample")
print("Distance:%.1f"%pulse())
time.sleep(2)
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print ("Finish")
#End
首先,我们定义了TRIGGER
和ECHO
引脚,触发脉冲的长度,以及声速(340.29 m/s)。TRIGGER
引脚被设置为输出,而ECHO
引脚作为输入(由于模块已经内置了上拉或下拉电阻,我们不需要额外的上拉或下拉电阻)。
pulse()
函数将发送一个短暂的触发脉冲(10 微秒);然后它将测量回声脉冲的持续时间。我们随后通过将持续时间除以声速(到物体的距离只是这个值的一半)来计算总行程距离。
不幸的是,传感器可能会对某些类型的物体产生混淆;它要么会检测到反射回来的物体附近的回声,要么无法拾取狭窄的物品,例如椅子腿。然而,结合局部碰撞传感器,超声波传感器可以帮助进行一般导航和避开大型物体。
对这个设置的一个改进是将超声波传感器安装在伺服电机上,这样你就可以对机器人的周围环境进行传感器扫描。通过进行多次扫描,测量距离,并跟踪伺服电机的角度,你可以构建机器人周围环境的内部地图。
获得方向感
为了在环境中导航你的机器人,你需要跟踪机器人面向的方向。你可以通过测量在固定时间段内机器人转过的角度来估计机器人转动的角度。对于轮式机器人,你也可以使用旋转编码器(一种提供轮子旋转计数的设备)来测量每个轮子的旋转。然而,随着机器人进行多次转弯,机器人面向的方向变得越来越不确定,因为表面和轮子或腿的抓地力差异导致机器人转动的角度不同。
幸运的是,我们可以使用指南针的电子版本;它通过提供一个从磁北方向的角度,使我们能够确定机器人面向的方向。如果我们知道机器人面向哪个方向,我们就可以接收请求特定角度的命令,并确保机器人朝那个方向移动。这使机器人能够进行受控运动并按要求进行导航。
当给定一个目标角度时,我们可以确定需要转向哪个方向,直到达到它。
准备就绪
您需要一个磁力计设备,例如 PiBorg 的 XLoBorg 模块(它是一个
结合了 I²C 磁力计和加速度计)。在这个例子中,我们将只关注磁力计(左侧较小的芯片)的输出。XLoBorg 模块在以下照片中展示:
PiBorg XLoBorg 模块包含一个三轴磁力计和加速度计
此设备可用于两种类型的机器人,并且从模块接收到的角度信息可以用来确定机器人需要移动的方向。
该模块设计为直接连接到 GPIO 引脚头,这将阻塞所有剩余的引脚。因此,为了使用其他 GPIO 设备,可以使用 GPIO 分线器(例如 PiBorg TriBorg)。或者,您可以使用杜邦公对母排线仅连接 I²C 引脚。以下表格显示了需要建立的连接:
将 XLoBorg 模块手动连接到 Raspberry Pi(使用标准 I²C 连接)的连接
从底部看,PiBorg XLoBorg 引脚与 Raspberry Pi GPIO 引脚相比是镜像的。
如何做到这一点...
使用 wiringpi2
创建一个兼容 Python 3 的 XLoBorg 库版本 (XLoBorg3.py
),具体如下:
#!/usr/bin/env python3
#XLoBorg3.py
import wiringpi2
import struct
import time
def readBlockData(bus,device,register,words):
magData=[]
for i in range(words):
magData.append(bus.readReg16(device,register+i))
return magData
class compass:
def __init__(self):
addr = 0x0E #compass
self.i2c = wiringpi2.I2C()
self.devMAG=self.i2c.setup(addr)
self.initCompass()
def initCompass(self):
# Acquisition mode
register = 0x11 # CTRL_REG2
data = (1 << 7) # Reset before each acquisition
data |= (1 << 5) # Raw mode, do not apply user offsets
data |= (0 << 5) # Disable reset cycle
self.i2c.writeReg8(self.devMAG,register,data)
# System operation
register = 0x10 # CTRL_REG1
data = (0 << 5) # Output data rate
# (10 Hz when paired with 128 oversample)
data |= (3 << 3) # Oversample of 128
data |= (0 << 2) # Disable fast read
data |= (0 << 1) # Continuous measurement
data |= (1 << 0) # Active mode
self.i2c.writeReg8(self.devMAG,register,data)
def readCompassRaw(self):
#x, y, z = readCompassRaw()
self.i2c.write(self.devMAG,0x00)
[status, xh, xl, yh, yl,
zh, zl, who, sm, oxh, oxl,
oyh, oyl, ozh, ozl,
temp, c1, c2] = readBlockData(self.i2c,self.devMAG, 0, 18)
# Convert from unsigned to correctly signed values
bytes = struct.pack('BBBBBB', xl, xh, yl, yh, zl, zh)
x, y, z = struct.unpack('hhh', bytes)
return x, y, z
if __name__ == '__main__':
myCompass=compass()
try:
while True:
# Read the MAG Data
mx, my, mz = myCompass.readCompassRaw()
print ("mX = %+06d, mY = %+06d, mZ = %+06d" % (mx, my, mz))
time.sleep(0.1)
except KeyboardInterrupt:
print("Finished")
#End
它是如何工作的...
该脚本基于适用于 XLoBorg 模块的 XLoBorg 库,除了我们使用 Python 3 兼容的 WiringPi2 来执行 I²C 操作之外。就像我们的电机/伺服驱动器一样,我们也将其定义为类,这样我们就可以将其放入我们的代码中,并在需要时轻松地用其他设备替换它。
我们导入wiringpi2
、time
以及一个名为struct
的库(该库允许我们将从设备读取的数据块快速解包成单独的项目)。
我们创建了compass
类,该类将包括__init__()
、initCompass()
和readCompassRaw()
函数。readCompassRaw()
函数等同于他们库中提供的标准 XLoBorg ReadCompassRaw()
函数。
__init__()
函数使用 wiringpi2
设置 I²C 总线,并在总线地址 0x0E
上注册 degMAG
设备。initCompass()
函数设置设备的 CTRL_REG1
和 CTRL_REG2
寄存器,以所需的设置从设备快速获取原始读数。
更多关于 MAG3110 寄存器的详细信息可在www.freescale.com/files/sensors/doc/data_sheet/MAG3110.pdf
找到。
readCompassRaw()
函数以单个块的形式(使用自定义函数 readBlockData()
)读取设备的数据寄存器。它读取设备中的所有 18 个寄存器(从 0x00
到 0x11
)。我们需要的传感器读数包含在寄存器 0x01
到 0x06
中,这些寄存器包含 x、y 和 z 读数,分为高字节和低字节(8 位值)。struct.pack()
和 struct.unpack()
函数提供了一种简单的方法将它们打包在一起,并重新拆分为单独的字(16 位值)。
我们可以通过从compass
类创建一个myCompass
对象,并使用myCompass.readCompassRaw()
来读取传感器值来测试我们的脚本。您将看到来自设备的原始x、y和z值,就像您从标准的 XLoBorg 库中看到的那样。
正如您会发现的那样,这些值本身并没有太大的用处,因为它们未经校准
只为你提供磁力计的原始读数。我们需要的是一个相对于磁北的更实用的角度(有关如何操作的详细信息,请参阅以下 还有更多... 部分)。
还有更多...
到目前为止,基本库使我们能够看到传感器周围三个轴(上下、左右和前后)上磁场的强度。虽然我们可以看到这些值会随着我们移动传感器而变化,但这还不足以引导我们的机器人。首先,我们需要校准传感器,然后根据x轴和y轴的读数确定机器人的方向。
校准指南针
罗盘需要校准以便报告中心化和均衡的值。这是必要的,因为周围存在磁场;通过校准传感器,我们可以消除任何局部磁场的影响。
通过测量所有轴向上的指南针读数,我们可以确定最小
并且为每个轴设置最大值。这将使我们能够计算读数的中间值,以及缩放比例,以便每个轴在朝向相同方向时都能读取相同的值。
在文件顶部添加以下代码(在import
语句之后):
CAL=100 #take CAL samples
将以下代码添加到compass
类的__init__(self)
方法中:
self.offset,self.scaling=self.calibrateCompass()
if DEBUG:print("offset:%s scaling:%s"%(str(self.offset),
str(self.scaling)))
在compass
类中添加一个名为calibrateCompass()
的新函数,如下所示:
def calibrateCompass(self,samples=CAL):
MAXS16=32768
SCALE=1000.0
avg=[0,0,0]
min=[MAXS16,MAXS16,MAXS16];max=[-MAXS16,-MAXS16,-MAXS16]
print("Rotate sensor around axis (start in 5 sec)")
time.sleep(5)
for calibrate in range(samples):
for idx,value in enumerate(self.readCompassRaw()):
avg[idx]+=value
avg[idx]/=2
if(value>max[idx]):
max[idx]=value
if(value<min[idx]):
min[idx]=value
time.sleep(0.1)
if DEBUG:print("#%d min=[%+06d,%+06d,%+06d]"
%(calibrate,min[0],min[1],min[2])
+" avg[%+06d,%+06d,%+06d]"
%(avg[0],avg[1],avg[2])
+" max=[%+06d,%+06d,%+06d]"
%(max[0],max[1],max[2]))
offset=[]
scaling=[]
for idx, value in enumerate(min):
magRange=max[idx]-min[idx]
offset.append((magRange/2)+min[idx])
scaling.append(SCALE/magRange)
return offset,scaling
在compass
类中添加另一个名为readCompass()
的新函数,如下所示:
def readCompass(self):
raw = self.readCompassRaw()
if DEBUG:print("mX = %+06d, mY = %+06d, mZ = %+06d"
% (raw[0],raw[1],raw[2]))
read=[]
for idx,value in enumerate(raw):
adj=value-self.offset[idx]
read.append(adj*self.scaling[idx])
return read
如果你仔细观察读数(如果你使用readCompass()
),你现在会发现所有的读数都具有相同的范围,并且都集中在相同的数值周围。
计算罗盘方位
XLoBorg 库仅提供对 MAG3110 设备的原始值访问,该设备可以测量每个轴上磁场的强度。为了确定传感器的方向,我们可以使用x轴和y轴的读数(假设我们已经水平安装并校准了传感器)。x轴和y轴的读数与传感器周围每个方向的磁场成正比,如下所示图示:
磁强计测量每个轴向上的磁场强度
我们偏离北方的角度可以通过以下图中所示的公式进行计算:
我们所指的方向角(相对于磁北)可以通过测量 Rx 和 Ry 来计算
我们现在可以通过将readCompassAngle()
函数添加到我们的compass
类中,来获取指南针
角度,如下所示:
def readCompassAngle(self,cal=True):
if cal==True:
read = self.readCompass()
else:
read = self.readCompassRaw()
angle = math.atan2 (read[1],read[0]) # cal angle in radians
if (angle < 0):
angle += (2 * math.pi) # ensure positive
angle = (angle * 360)/(2*math.pi); #report in degrees
return angle
我们还需要添加以下import
语句,与其它import
语句一起:
import math
我们使用数学函数 math.atan2()
来计算我们的角度(atan2
将返回相对于坐标 read[1]
和 read[2]
的 x 轴的角度——即我们想要的角度)。角度以弧度为单位,这意味着一个完整的旋转被定义为 2Pi,而不是 360 度。我们通过将其乘以 360 并除以 2Pi 来将其转换回度。由于我们希望角度在 0 到 360 度的范围内(而不是 -180 到 180 度),我们必须通过向任何负值添加相当于一个完整圆(2Pi)的值来确保它是正的。
传感器校准完成且角度计算后,我们现在应该已经拥有了在机器人上使用的正确罗盘方位。为了比较,您可以通过调用函数readCompassAngle (cal=False)
来查看使用未校准值计算的结果。
保存校准
在当前位置对传感器进行一次校准后,每次运行机器人时都需要重新校准将会很不方便。因此,你可以将以下代码添加到你的库中,以自动保存你的校准并在下次运行你的机器人时从文件中读取它。要创建一个新的校准,你可以删除或重命名mag.cal
(它位于你的脚本相同的文件夹中),或者创建你的compass
对象compass(newCal=True)
。
在文件顶部附近添加以下代码(在导入
语句之后):
FILENAME="mag.cal"
将 __init__(self)
改为 __init__(self,newCal=False)
.
还要考虑以下行:
self.offset,self.scaling=self.calibrateCompass()
将上一行改为以下行:
self.offset,self.scaling=self.readCal(newCal)
将 readCal()
添加到 compass
类中,如下所示:
def readCal(self,newCal=False,filename=FILENAME):
if newCal==False:
try:
with open(FILENAME,'r') as magCalFile:
line=magCalFile.readline()
offset=line.split()
line=magCalFile.readline()
scaling=line.split()
if len(offset)==0 or len(scaling)==0:
raise ValueError()
else:
offset=list(map(float, offset))
scaling=list(map(float, scaling))
except (OSError,IOError,TypeError,ValueError) as e:
print("No Cal Data")
newCal=True
pass
if newCal==True:
print("Perform New Calibration")
offset,scaling=self.calibrateCompass()
self.writeCal(offset,scaling)
return offset,scaling
将 writeCal()
方法添加到 compass
类中,如下所示:
def writeCal(self,offset,scaling):
if DEBUG:print("Write Calibration")
if DEBUG:print("offset:"+str(offset))
if DEBUG:print("scaling:"+str(scaling))
with open(FILENAME,'w') as magCalFile:
for value in offset:
magCalFile.write(str(value)+" ")
magCalFile.write("n")
for value in scaling:
magCalFile.write(str(value)+" ")
magCalFile.write("n")
使用指南针驾驶机器人
现在,我们剩下的任务就是利用指南针的方位来引导我们的机器人到达期望的角度。
创建以下 compassDrive.py
脚本:
#!/usr/bin/env python3
#compassDrive.py
import XLoBorg3 as XLoBorg
import rover_drive as drive
import time
MARGIN=10 #turn until within 10degs
LEFT="l"; RIGHT="r"; DONE="#"
def calDir(target, current, margin=MARGIN):
target=target%360
current=current%360
delta=(target-current)%360
print("Target=%f Current=%f Delta=%f"%(target,current,delta))
if delta <= margin:
CMD=DONE
else:
if delta>180:
CMD=LEFT
else:
CMD=RIGHT
return CMD
def main():
myCompass=XLoBorg.compass()
myBot=drive.motor()
while(True):
print("Enter target angle:")
ANGLE=input()
try:
angleTarget=float(ANGLE)
CMD=LEFT
while (CMD!=DONE):
angleCompass=myCompass.readCompassAngle()
CMD=calDir(angleTarget,angleCompass)
print("CMD: %s"%CMD)
time.sleep(1)
myBot.cmd(CMD)
print("Angle Reached!")
except ValueError:
print("Enter valid angle!")
pass
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print ("Finish")
#End
我们导入之前创建的模块:XLoBorg3
、rover_drive
(用于 Rover-Pi 机器人,或者根据需要使用替代的bug_drive
),以及time
。接下来,我们创建一个函数,该函数将根据给定的目标角度(由用户请求)和当前角度(从compass
类读取)返回LEFT
、RIGHT
或DONE
。如果指南针角度在目标角度 180 度以内,则我们转向LEFT
。同样,如果它在 180 度以内,我们转向RIGHT
。最后,如果指南针角度在误差范围内(+10 度/-10 度),则我们完成DONE
。通过使用angle%360
(它给出了角度除以 360 的余数),我们确保所有角度都在 0-360 度(即-90 度变为 270 度)。
对于main()
函数,我们创建了myCompass
(一个XLoBorg.compass
对象)和myBot
(一个drive.motor()
对象);这些使我们能够确定我们面对的方向,并为我们提供了一种按照期望方向行驶的方法。在main
循环中,我们要求输入目标角度,找到我们的机器人当前面对的角度,然后继续转向所需的角度,直到达到该角度(或者达到接近该角度的地方)。
第十三章:与技术接口
在本章中,我们将涵盖以下主题:
-
使用远程控制插座自动化您的家庭
-
使用 SPI 控制 LED 矩阵
-
使用串行接口进行通信
-
使用蓝牙控制树莓派
-
控制 USB 设备
简介
树莓派区别于普通计算机的关键特性之一是它能够与硬件进行交互和控制。在本章中,我们使用树莓派远程控制带电插座,从另一台计算机通过串行连接发送命令,并远程控制 GPIO。我们利用 SPI(另一个有用的协议)来驱动 8 x 8 LED 矩阵显示屏。
我们还使用蓝牙模块与智能手机连接,允许设备之间无线传输信息。最后,我们通过访问通过 USB 发送的命令来控制 USB 设备。
一定要查看附录中的“硬件清单”部分,即“硬件和软件清单”;它列出了本章中使用的所有物品及其获取地点。
使用远程控制插座自动化您的家庭
树莓派可以通过提供精确的时间控制、响应命令、按钮输入、环境传感器或来自互联网的消息的能力,成为家庭自动化的优秀工具。
准备就绪
在控制使用市电的设备时必须格外小心,因为通常涉及高压和电流。
永远不要在没有适当培训的情况下尝试修改或改变连接到主电源的设备。你绝对不能直接将任何自制设备连接到主电源。所有电子产品都必须经过严格的安全测试,以确保在发生故障的情况下不会对人员或财产造成风险或伤害。
在本例中,我们将使用遥控射频(RF)插头插座;这些插座使用一个独立的遥控单元发送特定的射频信号来控制任何连接到它的电器设备的开关。这使我们能够修改遥控器并使用树莓派安全地激活开关,而不会干扰危险的电压:
遥控和远程主电源插座
本例中使用的特定遥控器上有六个按钮,可以直接切换三个不同的插座的开或关,并由 12V 电池供电。它可以切换到四个不同的频道,这使得您能够控制总共 12 个插座(每个插座都有一个类似的选择器,将用于设置它将响应的信号):
在遥控器内部
当按下遥控按钮时,将广播一个特定的射频信号(本设备使用传输频率为 433.92 MHz)。这将触发设置为相应通道(A、B、C 或 D)和编号(1、2 或 3)的任何插座。
内部,每个按钮将两个独立的信号连接到地,编号(1,2,
或者 3),并说明(开启或关闭)。这会触发遥控器要发出的正确广播:
将电线连接到遥控器 PCB 板上的 ON 和 OFF,1,2 和 3,以及 GND 合适的位置(图中只连接了 ON,OFF,1 和 GND)
建议您不要将任何可能因开关而造成危险的物品连接到您的插座上。遥控器发送的信号不是唯一的(只有四个不同的频道可用)。因此,这使附近有类似插座组合的人在不经意间激活/关闭您的其中一个插座成为可能。建议您选择除默认频道 A 以外的频道,这将略微降低他人意外使用相同频道的机会。
为了让树莓派模拟遥控器的按钮按下,我们需要五个
中继器使我们能够选择数字(1、2 或 3)和状态(开启或关闭):
预制继电器模块可用于切换信号
或者,可以使用第十二章“构建机器人”中的晶体管和继电器电路来模拟按钮的按下。
将继电器控制引脚连接到树莓派的 GPIO,并将插座遥控器连接到每个继电器输出端,具体连接方式如下:
插座遥控电路
尽管远程套接字需要同时指定数字(1、2 或 3)和状态(开启或关闭)来激活套接字,但真正激活射频传输的是状态信号。为了避免耗尽远程设备的电池,我们必须确保我们已经关闭了状态信号。
如何做到这一点...
- 创建以下
socketControl.py
脚本:
#!/usr/bin/python3
# socketControl.py
import time
import RPi.GPIO as GPIO
#HARDWARE SETUP
# P1
# 2[V=G====XI====]26[=======]40
# 1[=====321=====]25[=======]39
#V=5V G=Gnd
sw_num=[15,13,11]#Pins for Switch 1,2,3
sw_state=[16,18]#Pins for State X=Off,I=On
MSGOFF=0; MSGON=1
SW_ACTIVE=0; SW_INACTIVE=1
class Switch():
def __init__(self):
self.setup()
def __enter__(self):
return self
def setup(self):
print("Do init")
#Setup the wiring
GPIO.setmode(GPIO.BOARD)
for pin in sw_num:
GPIO.setup(pin,GPIO.OUT)
for pin in sw_state:
GPIO.setup(pin,GPIO.OUT)
self.clear()
def message(self,number,state):
print ("SEND SW_CMD: %s %d" % (number,state))
if state==MSGON:
self.on(number)
else:
self.off(number)
def on(self,number):
print ("ON: %d"% number)
GPIO.output(sw_num[number-1],SW_ACTIVE)
GPIO.output(sw_state[MSGON],SW_ACTIVE)
GPIO.output(sw_state[MSGOFF],SW_INACTIVE)
time.sleep(0.5)
self.clear()
def off(self,number):
print ("OFF: %d"% number)
GPIO.output(sw_num[number-1],SW_ACTIVE)
GPIO.output(sw_state[MSGON],SW_INACTIVE)
GPIO.output(sw_state[MSGOFF],SW_ACTIVE)
time.sleep(0.5)
self.clear()
def clear(self):
for pin in sw_num:
GPIO.output(pin,SW_INACTIVE)
for pin in sw_state:
GPIO.output(pin,SW_INACTIVE)
def __exit__(self, type, value, traceback):
self.clear()
GPIO.cleanup()
def main():
with Switch() as mySwitches:
mySwitches.on(1)
time.sleep(5)
mySwitches.off(1)
if __name__ == "__main__":
main()
#End
端口控制脚本通过将第一个端口开启 5 秒钟然后再次关闭来进行快速测试。
- 要控制其余的插槽,创建如下 GUI 菜单:
远程开关 GUI
- 创建以下
socketMenu.py
脚本:
#!/usr/bin/python3
#socketMenu.py
import tkinter as TK
import socketControl as SC
#Define Switches ["Switch name","Switch number"]
switch1 = ["Living Room Lamp",1]
switch2 = ["Coffee Machine",2]
switch3 = ["Bedroom Fan",3]
sw_list = [switch1,switch2,switch3]
SW_NAME = 0; SW_CMD = 1
SW_COLOR=["gray","green"]
class swButtons:
def __init__(self,gui,sw_index,switchCtrl):
#Add the buttons to window
self.msgType=TK.IntVar()
self.msgType.set(SC.MSGOFF)
self.btn = TK.Button(gui,
text=sw_list[sw_index][SW_NAME],
width=30, command=self.sendMsg,
bg=SW_COLOR[self.msgType.get()])
self.btn.pack()
msgOn = TK.Radiobutton(gui,text="On",
variable=self.msgType, value=SC.MSGON)
msgOn.pack()
msgOff = TK.Radiobutton(gui,text="Off",
variable=self.msgType,value=SC.MSGOFF)
msgOff.pack()
self.sw_num=sw_list[sw_index][SW_CMD]
self.sw_ctrl=switchCtrl
def sendMsg(self):
print ("SW_CMD: %s %d" % (self.sw_num,
self.msgType.get()))
self.btn.configure(bg=SW_COLOR[self.msgType.get()])
self.sw_ctrl.message(self.sw_num,
self.msgType.get())
root = TK.Tk()
root.title("Remote Switches")
prompt = "Control a switch"
label1 = TK.Label(root, text=prompt, width=len(prompt),
justify=TK.CENTER, bg='lightblue')
label1.pack()
#Create the switch
with SC.Switch() as mySwitches:
#Create menu buttons from sw_list
for index, app in enumerate(sw_list):
swButtons(root,index,mySwitches)
root.mainloop()
#End
它是如何工作的...
第一个脚本定义了一个名为 Switch
的类;它设置了控制五个继电器所需的 GPIO 引脚(在 setup
函数中)。它还定义了 __enter__
和 __exit__
函数,这些是 with..as
语句使用的特殊函数。当使用 with..as
创建一个类时,它使用 __enter__
来执行任何额外的初始化或设置(如果需要),然后通过调用 __exit__
来执行任何清理工作。当 Switch
类执行完毕后,所有继电器都会关闭以保护遥控器的电池,并调用 GPIO.cleanup()
来释放 GPIO 引脚。__exit__
函数的参数(type
、value
和 traceback
)允许处理在 with..as
语句中执行类时可能发生的任何特定异常(如果需要)。
要控制插座,创建两个函数,这些函数将切换相关的继电器以开或关,从而激活远程控制发送所需的信号到插座。然后,稍后使用 clear()
再次关闭继电器。为了使控制开关更加简便,创建一个 message
函数,该函数将允许指定开关编号和状态。
我们通过创建一个 Tkinter GUI 菜单来使用socketControl.py
脚本。该菜单由三组控制(每个开关一组)组成,这些控制由swButtons
类定义。
swButtons
类创建了一个 Tkinter
按钮 和两个 Radiobutton
控件。每个 swButtons
对象都会分配一个索引和一个对 mySwitches
对象的引用。这使得我们可以为按钮设置一个名称,并在按下时控制特定的开关。通过调用 message()
函数来激活/停用套接字,所需的开关号和状态由 Radiobutton
控件设置。
还有更多...
之前的例子允许您重新布线大多数遥控插座的遥控器,但另一个选择是模拟信号以直接控制它。
直接发送射频控制信号
你无需重新布线遥控器,可以使用与你的插座相同频率的发射器来复制遥控器的射频信号(这些特定的设备使用 433.94 MHz)。这取决于特定的插座,有时也取决于你的位置——一些国家禁止使用某些频率——在你自行发射之前,你可能需要获得认证:
433.94 MHz 射频发射器(左侧)和接收器(右侧)
由 433Utils 创建的射频遥控器发送的信号可以被重新创建,其中 433Utils 是由
ninjablocks.com
. 433Utils 使用 WiringPi,并以 C++ 编写,允许高速捕获和复制射频信号。
使用以下命令获取代码:
cd ~
wget https://github.com/ninjablocks/433Utils/archive/master.zip
unzip master.zip
接下来,我们需要将我们的射频发射器(以便我们可以控制开关)和射频接收器(以便我们可以确定控制代码)连接到树莓派上。
发射器(较小的方形模块)有三个引脚,分别是电源(VCC)、地(GND)和数据输出(DATA)。电源引脚上提供的电压将决定传输范围(我们将使用来自树莓派的 5V 电源,但你也可以将其替换为 12V,只要确保将地引脚连接到你的 12V 电源和树莓派)。
尽管接收器有四个引脚,但其中有一个电源引脚(VCC)、一个地线引脚(GND)和两个数据输出引脚(DATA),这些引脚是连接在一起的,因此我们只需要连接三根线到树莓派:
射频发送 | RPi GPIO 引脚 | 射频接收 | RPi GPIO 引脚 |
---|---|---|---|
VCC (5V) | 2 | VCC (3V3) | 1 |
数据输出 | 11 | 数据输入 | 13 |
GND | 6 | GND | 9 |
在我们使用RPi_Utils
中的程序之前,我们将进行一些调整以确保我们的 RX 和 TX 引脚设置正确。
在 433Utils-master/RPi_utils/
目录下定位 codesend.cpp
文件以进行必要的修改:
cd ~/433Utils-master/RPi_utils
nano codesend.cpp -c
将int PIN = 0;
(位于大约第 24 行)更改为int PIN = 11;
(RPi 物理
验证码(pin number)。
将 wiringPi
改为使用物理引脚编号(位于第 27 行)通过将 wiringPiSetup()
替换为 wiringPiSetupPhy()
。否则,默认为 wiringPi
GPIO 编号;更多详情,请参阅wiringpi.com/reference/setup/
。找到以下行:
if (wiringPiSetup () == -1) return 1;
改成这个:
if (wiringPiSetupPhys () == -1) return 1;
使用 Ctrl + X, Y 保存并退出 nano
。
对 RFSniffer.cpp
进行类似的调整:
nano RFSniffer.cpp -c
找到以下行(位于大约第 25 行):
int PIN = 2;
改成这个:
int PIN = 13; //RPi physical pin number
找到以下行(位于大约第 27 行):
if(wiringPiSetup() == -1) {
改成这个:
if(wiringPiSetupPhys() == -1) {
使用 Ctrl + X, Y 保存并退出 nano
。
使用以下命令构建代码:
make all
这应该可以无错误地构建,如下所示:
g++ -c -o codesend.o codesend.cpp
g++ RCSwitch.o codesend.o -o codesend -lwiringPi
g++ -c -o RFSniffer.o RFSniffer.cpp
g++ RCSwitch.o RFSniffer.o -o RFSniffer -lwiringPi
现在我们已经将 RF 模块连接到树莓派,并且代码已经准备就绪,我们可以从我们的遥控器捕获控制信号。运行以下命令并注意报告的输出:
sudo ./RFSniffer
通过将按钮 1 关闭,并将遥控器设置为频道 A 来获取输出(注意我们可能会接收到一些随机噪音):
Received 1381716
Received 1381716
Received 1381716
Received 1381717
Received 1398103
我们现在可以使用sendcode
命令发送信号来切换套接字关闭(1381716
)和开启(1381719
):
sendcode 1381716
sendcode 1381719
你甚至可以设置树莓派,使用接收模块来检测来自遥控器(在未使用的频道上)的信号,并根据这些信号启动进程、控制其他硬件,或者可能触发软件的关机/重启。
扩展射频发射机的范围
当发射器由 5V 供电且没有附加天线时,其传输范围非常有限。然而,在做出任何修改之前,测试一切是值得的。
简单的线状天线可以用 25 厘米的单芯线制成,17 毫米的一端连接到天线焊接点,然后绕 16 圈(使用细螺丝刀柄或类似物品制作),剩余的线在上面(大约 53 毫米):
使用简单的天线,发射器的范围得到了极大的提升
确定遥控码的结构
记录每个按钮的代码,我们可以确定每个按钮的代码(并分解其结构):
开启 | 关闭 | 开启 | 关闭 | 开启 | 关闭 | |
---|---|---|---|---|---|---|
A | 0x15 15 57 (1381719) |
0x15 15 54 (1381716) |
0x15 45 57 (1394007) |
0x15 45 54 (1394004) |
0x15 51 57 (1397079) |
0x15 51 54 (1397076) |
B | 0x45 15 57 (4527447) |
0x45 15 54 (4527444) |
0x45 45 57 (4539735) |
0x45 45 54 (4539732) |
0x45 51 57 (4542807) |
0x45 51 54 (4542804) |
C | 0x51 15 57 (5313879) |
0x51 15 54 (5313876) |
0x51 45 57 (5326167) |
0x51 45 54 (5326164) |
0x51 51 57 (5329239) |
0x51 51 54 (5329236) |
D | 0x54 15 57 (5510487) |
0x54 15 57 (5510487) |
0x54 45 57 (5522775) |
0x54 45 54 (5522772) |
0x54 51 57 (5525847) |
0x54 51 54 (5526612) |
01 | 01 | 01 | 01 | 01 | 01 | 01 |
不同的代码以十六进制格式显示,以便您查看其结构;sendcode
命令使用十进制格式(括号内显示)
要选择通道 A、B、C 或 D,将两个位设置为 00。同样,对于按钮 1、2 或 3,将两个位设置为 00 以选择该按钮。最后,将最后两个位设置为 11 以表示开启或 00 以表示关闭。
请参阅arduinodiy.wordpress.com/2014/08/12/433-mhz-system-for-your-arduino/
,该页面分析了这些以及其他类似的射频遥控器。
使用 SPI 控制 LED 矩阵
在第十章《感知与显示现实世界数据》中,我们使用了一种名为 I²C 的总线协议连接到设备。树莓派还支持另一种称为串行外设接口(SPI)的芯片间协议。SPI 总线与 I²C 的不同之处在于它使用两条单向数据线(而 I²C 使用一条双向数据线)。
尽管 SPI 需要更多的线(I²C 使用两条总线信号,SDA 和 SCL),但它支持数据的同步发送和接收,并且比 I²C 具有更高的时钟速度:
SPI 设备与树莓派的通用连接
SPI 总线由以下四个信号组成:
-
SCLK: 这允许时钟边缘在输入/输出线上读写数据;它由主设备驱动。当时钟信号从一个状态变化到另一个状态时,SPI 设备将检查 MOSI 信号的状态以读取一个比特。同样地,如果 SPI 设备正在发送数据,它将使用时钟信号边缘来同步设置 MISO 信号状态的时刻。
-
CE:这指的是芯片使能(通常,在总线上为每个从设备使用一个单独的芯片使能)。主设备会将芯片使能信号设置为低电平,以便与它想要通信的设备通信。当芯片使能信号设置为高电平时,它会忽略总线上的任何其他信号。这个信号有时被称为芯片选择(CS)或从设备选择(SS)。
-
主输出,从输入(MOSI):它连接到主设备的数据输出和从设备的数据输入。
-
主输入从输出(MISO):它提供从从设备(slave)的响应。
以下图表显示了每个信号:
SPI 信号:SCLK(1)、CE(2)、MOSI(3)和 MISO(4)
之前的范围跟踪显示了通过 SPI 发送的两个字节。每个字节都通过SCLK (1)信号被时钟到 SPI 设备中。一个字节由八个时钟周期的一阵(SCLK (1)信号上的低电平和随后的高电平)表示,当时钟状态改变时读取特定位的值。确切的采样点由时钟模式决定;在下面的图中,它是在时钟从低电平变为高电平时:
Raspberry Pi 通过 MOSI(3) 信号发送的第一个数据字节
发送的第一个字节是 0x01(所有位都是低电平,除了位 0)和第二个发送的是 0x03(只有位 1和位 0是高电平)。同时,MOSI (4) 信号从 SPI 设备返回数据——在这种情况下,0x08(位 3是高电平)和 0x00(所有位都是低电平)。SCLK (1) 信号用于同步一切,甚至包括从 SPI 设备发送的数据。
当数据正在发送到特定 SPI 设备以指示其监听MOSI (4)信号时,CE (2)信号被保持低电平。当CE (2)信号再次设置为高电平时,它向 SPI 设备指示传输已完成。
以下是一个由SPI 总线控制的 8 x 8 LED 矩阵的图像:
一个显示字母 K 的 8 x 8 LED 模块
准备就绪
我们之前用于 I²C 的 wiringPi
库也支持 SPI。请确保已安装 wiringPi
(详情请见第十章,感知和显示现实世界数据),以便我们在此处使用。
接下来,如果我们之前在启用 I²C 时没有这样做,我们需要启用 SPI:
sudo nano /boot/config.txt
移除#
前的#dtparam=spi=on
以启用它,使其读取,并保存(Ctrl + X,Y,Enter):
dtparam=spi=on
您可以通过以下命令列出所有正在运行的模块,并定位到 spi_bcm2835
来确认 SPI 是否处于活动状态:
lsmod
您可以使用以下 spiTest.py
脚本测试 SPI:
#!/usr/bin/python3
# spiTest.py
import wiringpi
print("Add SPI Loopback - connect GPIO Pin19 and Pin21")
print("[Press Enter to continue]")
input()
wiringpi.wiringPiSPISetup(1,500000)
buffer=str.encode("HELLO")
print("Buffer sent %s" % buffer)
wiringpi.wiringPiSPIDataRW(1,buffer)
print("Buffer received %s" % buffer)
print("Remove the SPI Loopback")
print("[Press Enter to continue]")
input()
buffer=str.encode("HELLO")
print("Buffer sent %s" % buffer)
wiringpi.wiringPiSPIDataRW(1,buffer)
print("Buffer received %s" % buffer)
#End
将输入19和21连接起来以创建用于测试的 SPI 环回:
SPI 环回测试
你应该得到以下结果:
Buffer sent b'HELLO'
Buffer received b'HELLO'
Remove the SPI Loopback
[Press Enter to continue]
Buffer sent b'HELLO'
Buffer received b'x00x00x00x00x00'
下面的例子使用了一个由 LED 8 x 8 矩阵显示器,该显示器正在被驱动
SPI 控制的MAX7219 LED 驱动器:
MAX7219 LED 控制器引脚图、LED 矩阵引脚图以及 LED 矩阵内部连接(从左到右)
尽管该设备被设计用来控制八个独立的七段 LED 数码管,但我们仍可以用它来制作我们的 LED 矩阵显示屏。当用于数码管时,每个七段(加上小数点)都连接到一个 SEG 引脚上,每个数码管的 COM 连接则连接到 DIG 引脚上。控制器随后根据需要打开每个段,同时将相关数码管的 COM 设置为低电平以启用它。控制器可以通过快速切换 DIG 引脚来快速循环每个数码管,以至于所有八个数码管看起来同时点亮:
一个七段 LED 数码管使用段 A 到 G,加上小数点 DP(decimal place)
我们以类似的方式使用控制器,除了每个 SEG 引脚将连接到矩阵中的一列,而 DIG 引脚将启用/禁用一行。
我们使用一个 8 x 8 模块,如下连接到 MAX7219 芯片:
驱动 8 x 8 LED 矩阵显示屏的 MAX7219 LED 控制器
如何做到这一点...
- 要控制连接到 SPI MAX7219 芯片的 LED 矩阵,创建以下
matrixControl.py
脚本:
#!/usr/bin/python3
# matrixControl.py
import wiringpi
import time
MAX7219_NOOP = 0x00
DIG0=0x01; DIG1=0x02; DIG2=0x03; DIG3=0x04
DIG4=0x05; DIG5=0x06; DIG6=0x07; DIG7=0x08
MAX7219_DIGIT=[DIG0,DIG1,DIG2,DIG3,DIG4,DIG5,DIG6,DIG7]
MAX7219_DECODEMODE = 0x09
MAX7219_INTENSITY = 0x0A
MAX7219_SCANLIMIT = 0x0B
MAX7219_SHUTDOWN = 0x0C
MAX7219_DISPLAYTEST = 0x0F
SPI_CS=1
SPI_SPEED=100000
class matrix():
def __init__(self,DEBUG=False):
self.DEBUG=DEBUG
wiringpi.wiringPiSPISetup(SPI_CS,SPI_SPEED)
self.sendCmd(MAX7219_SCANLIMIT, 8) # enable outputs
self.sendCmd(MAX7219_DECODEMODE, 0) # no digit decode
self.sendCmd(MAX7219_DISPLAYTEST, 0) # display test off
self.clear()
self.brightness(7) # brightness 0-15
self.sendCmd(MAX7219_SHUTDOWN, 1) # start display
def sendCmd(self, register, data):
buffer=(register<<8)+data
buffer=buffer.to_bytes(2, byteorder='big')
if self.DEBUG:print("Send byte: 0x%04x"%
int.from_bytes(buffer,'big'))
wiringpi.wiringPiSPIDataRW(SPI_CS,buffer)
if self.DEBUG:print("Response: 0x%04x"%
int.from_bytes(buffer,'big'))
return buffer
def clear(self):
if self.DEBUG:print("Clear")
for row in MAX7219_DIGIT:
self.sendCmd(row + 1, 0)
def brightness(self,intensity):
self.sendCmd(MAX7219_INTENSITY, intensity % 16)
def letterK(matrix):
print("K")
K=(0x0066763e1e366646).to_bytes(8, byteorder='big')
for idx,value in enumerate(K):
matrix.sendCmd(idx+1,value)
def main():
myMatrix=matrix(DEBUG=True)
letterK(myMatrix)
while(1):
time.sleep(5)
myMatrix.clear()
time.sleep(5)
letterK(myMatrix)
if __name__ == '__main__':
main()
#End
运行脚本(python3 matrixControl.py
)会显示字母K
。
- 我们可以使用图形用户界面(GUI)通过
matrixMenu.py
来控制 LED 矩阵的输出:
#!/usr/bin/python3
#matrixMenu.py
import tkinter as TK
import time
import matrixControl as MC
#Enable/Disable DEBUG
DEBUG = True
#Set display sizes
BUTTON_SIZE = 10
NUM_BUTTON = 8
NUM_LIGHTS=NUM_BUTTON*NUM_BUTTON
MAX_VALUE=0xFFFFFFFFFFFFFFFF
MARGIN = 2
WINDOW_H = MARGIN+((BUTTON_SIZE+MARGIN)*NUM_BUTTON)
WINDOW_W = WINDOW_H
TEXT_WIDTH=int(2+((NUM_BUTTON*NUM_BUTTON)/4))
LIGHTOFFON=["red4","red"]
OFF = 0; ON = 1
colBg = "black"
def isBitSet(value,bit):
return (value>>bit & 1)
def setBit(value,bit,state=1):
mask=1<<bit
if state==1:
value|=mask
else:
value&=~mask
return value
def toggleBit(value,bit):
state=isBitSet(value,bit)
value=setBit(value,bit,not state)
return value
class matrixGUI(TK.Frame):
def __init__(self,parent,matrix):
self.parent = parent
self.matrix=matrix
#Light Status
self.lightStatus=0
#Add a canvas area ready for drawing on
self.canvas = TK.Canvas(parent, width=WINDOW_W,
height=WINDOW_H, background=colBg)
self.canvas.pack()
#Add some "lights" to the canvas
self.light = []
for iy in range(NUM_BUTTON):
for ix in range(NUM_BUTTON):
x = MARGIN+MARGIN+((MARGIN+BUTTON_SIZE)*ix)
y = MARGIN+MARGIN+((MARGIN+BUTTON_SIZE)*iy)
self.light.append(self.canvas.create_rectangle(x,y,
x+BUTTON_SIZE,y+BUTTON_SIZE,
fill=LIGHTOFFON[OFF]))
#Add other items
self.codeText=TK.StringVar()
self.codeText.trace("w", self.changedCode)
self.generateCode()
code=TK.Entry(parent,textvariable=self.codeText,
justify=TK.CENTER,width=TEXT_WIDTH)
code.pack()
#Bind to canvas not tk (only respond to lights)
self.canvas.bind('<Button-1>', self.mouseClick)
def mouseClick(self,event):
itemsClicked=self.canvas.find_overlapping(event.x,
event.y,event.x+1,event.y+1)
for item in itemsClicked:
self.toggleLight(item)
def setLight(self,num):
state=isBitSet(self.lightStatus,num)
self.canvas.itemconfig(self.light[num],
fill=LIGHTOFFON[state])
def toggleLight(self,num):
if num != 0:
self.lightStatus=toggleBit(self.lightStatus,num-1)
self.setLight(num-1)
self.generateCode()
def generateCode(self):
self.codeText.set("0x%016x"%self.lightStatus)
def changedCode(self,*args):
updated=False
try:
codeValue=int(self.codeText.get(),16)
if(codeValue>MAX_VALUE):
codeValue=codeValue>>4
self.updateLight(codeValue)
updated=True
except:
self.generateCode()
updated=False
return updated
def updateLight(self,lightsetting):
self.lightStatus=lightsetting
for num in range(NUM_LIGHTS):
self.setLight(num)
self.generateCode()
self.updateHardware()
def updateHardware(self):
sendBytes=self.lightStatus.to_bytes(NUM_BUTTON,
byteorder='big')
print(sendBytes)
for idx,row in enumerate(MC.MAX7219_DIGIT):
response = self.matrix.sendCmd(row,sendBytes[idx])
print(response)
def main():
global root
root=TK.Tk()
root.title("Matrix GUI")
myMatrixHW=MC.matrix(DEBUG)
myMatrixGUI=matrixGUI(root,myMatrixHW)
TK.mainloop()
if __name__ == '__main__':
main()
#End
- 矩阵 GUI 允许我们通过点击每个方块(或直接输入十六进制值)来切换每个 LED 的开关状态,以创建所需的图案:
使用 Matrix GUI 控制 8 x 8 LED 矩阵
它是如何工作的...
最初,我们为 MAX7219 设备使用的每个控制寄存器定义了地址。查看数据表以获取更多信息:
datasheets.maximintegrated.com/en/ds/MAX7219-MAX7221.pdf
.
我们创建了一个名为 matrix
的类,这将使我们能够控制该模块。__init__()
函数设置了树莓派的 SPI(使用 SPI_CS
作为引脚 26 的 CS1,并将 SPI_SPEED
设置为 100 kHz)。
我们matrix
类中的关键功能是sendCmd()
函数;它使用wiringpi.wiringPiSPIDataRW(SPI_CS,buff)
来通过 SPI 总线发送buffer
(这是我们想要发送的原始字节数据),在传输发生时也将SPI_CS
引脚设置为低电平。每个命令由两个字节组成:第一个字节指定寄存器的地址,第二个字节设置需要放入的数据。为了显示一排灯光,我们发送一个ROW
寄存器(MC.MAX7219_DIGIT
)的地址以及我们想要显示的位模式(作为一个字节)。
在调用wiringpi.wiringPiSPIDataRW()
函数之后,buffer
变量包含了从 MISO 引脚(在数据通过 MOSI 引脚发送的同时读取)接收到的任何结果。如果连接了,这将是对 LED 模块的输出(发送数据的延迟副本)。有关如何使用芯片输出的信息,请参阅以下更多内容...部分,了解有关串行外设接口(SPI)配置的菊花链设置。
要初始化 MAX7219,我们需要确保它配置在正确的模式。首先,我们将扫描限制字段设置为 7
(这将启用所有 DIG0 - DIG7 输出)。接下来,我们禁用内置的数字解码,因为我们正在使用原始输出进行显示(并且不希望它尝试显示数字)。我们还想确保 MAX7219_DISPLAYTEST
寄存器被禁用(如果启用,它将点亮所有 LED)。
我们通过调用自己的clear()
函数来确保显示被清除,该函数发送0
向每个MAX7219_DIGIT
寄存器发送指令以清除每一行。最后,我们使用MAX7219_INTENSITY
寄存器来设置 LED 的亮度。亮度通过 PWM 输出进行控制,以使 LED 根据所需的亮度显得更亮或更暗。
在main()
函数中,我们通过发送一组 8 个字节(0x0066763e1e366646
)来执行快速测试,以在网格上显示字母 K:
每个 8 x 8 图案由 8 个字节的 8 位组成(每个列对应一个位,使得每个字节成为显示屏中的一行)
matrixGUI
类创建了一个画布对象,该对象填充了一个矩形对象的网格,以表示我们想要控制的 8 x 8 LED 网格(这些保存在 self.light
中)。我们还添加了一个文本输入框来显示我们将发送到 LED 矩阵模块的结果字节。然后我们将 <Button-1>
鼠标事件绑定到画布上,以便在画布区域内发生鼠标点击时调用 mouseClick
。
我们使用特殊的 Python 函数 trace
将一个名为 changedCode()
的函数附加到 codeText
变量上,这允许我们监控特定的变量或函数。如果我们使用 trace
函数的 'w'
值,Python 系统将在值被写入时调用 callback
函数。
当调用 mouseClick()
函数时,我们使用 event.x
和 event.y
坐标来识别位于那里的对象。如果检测到项目,则使用项目的 ID(通过 toggleLight()
)来切换 self.lightStatus
值中的相应位,并且显示中的灯光颜色相应地改变(通过 setLight()
)。同时,codeText
变量也会更新为 lightStatus
值的新十六进制表示。
changeCode()
函数允许我们使用 codeText
变量并将其转换为整数。这使我们能够检查它是否是一个有效的值。由于在这里可以自由输入文本,我们必须对其进行验证。如果我们无法将其转换为整数,则使用 lightStatus
值刷新 codeValue
文本。否则,我们检查它是否过大,在这种情况下,我们通过 4 位位移操作将其除以 16,直到它在有效范围内。我们更新 lightStatus
值、GUI 灯光、codeText
变量,以及硬件(通过调用 updateHardware()
)。
updateHardware()
函数利用了使用 MC.matrix
类创建的 myMatrixHW
对象。我们逐字节将想要显示的字节发送到矩阵硬件(同时附带相应的 MAX7219_DIGIT
值以指定行)。
还有更多...
SPI 总线允许我们通过使用芯片来控制同一总线上的多个设备。
启用信号。一些设备,例如 MAX7219,还允许使用所谓的
菊链 SPI 配置。
Daisy-chain SPI 配置
你可能已经注意到,当我们通过 MOSI 线发送数据时,matrix
类也会返回一个字节。这是从 DOUT 连接上的 MAX7219 控制器输出的数据。实际上,MAX7219 控制器会将所有的 DIN 数据传递到 DOUT,这比 DIN 数据晚一组指令。通过这种方式,MAX7219 可以通过 DOUT 连接到下一个 DIN,从而实现菊花链(每个 DOUT 连接到下一个 DIN)。通过保持 CE 信号低,可以通过相互传递数据来加载多个控制器。
当 CE 设置为低时,数据将被忽略;只有当我们再次将其设置为高时,输出才会改变。这样,你可以为链中的每个模块记录所有数据,然后设置 CE 为高以更新它们:
菊链 SPI 配置
我们需要为每一行我们希望更新的行(或者如果我们想保持当前行不变,可以使用MAX7219_NOOP
)执行此操作。这被称为菊花链 SPI 配置,一些 SPI 设备支持该配置,其中数据通过 SPI 总线上的每个设备传递到下一个设备,这允许使用三个总线控制信号来控制多个设备。
使用串行接口进行通信
传统上,串行协议如 RS232 是连接打印机、扫描仪以及游戏手柄和鼠标等设备到计算机的常见方式。现在,尽管被 USB 所取代,许多外围设备仍然使用此协议进行组件间的内部通信、数据传输和固件更新。对于电子爱好者来说,RS232 是一种非常实用的协议,用于调试和控制其他设备,同时避免了 USB 的复杂性。
本例中的两个脚本允许控制 GPIO 引脚,以展示我们如何通过串行端口远程控制 Raspberry Pi。串行端口可以连接到 PC、另一个 Raspberry Pi 设备,甚至嵌入式微控制器(例如 Arduino、PIC 或类似设备)。
准备就绪
通过串行协议连接到树莓派的 easiest way 将取决于您的计算机是否内置了串行端口。串行连接、软件以及测试设置将在以下三个步骤中描述:
- 在您的计算机和树莓派之间创建一个 RS232 串行连接。为此,您需要以下配置之一:
-
-
如果您的计算机有可用的内置串行端口,您可以使用
一条带有 RS232 到 USB 适配器的 Null-Modem 线,用于连接到树莓派:
-
RS232-to-USB 适配器
Null-Modem 是一种串行电缆/适配器,其 TX 和 RX 线已交叉连接,使得一边连接到串行端口的 TX 引脚,另一边连接到 RX 引脚:
通过 Null-Modem 电缆和 RS232-to-USB 适配器连接到 Raspberry Pi 的 PC 串行端口,用于 RS232 适配器
支持的 USB-to-RS232 设备列表可在以下链接中找到:
elinux.org/RPi_VerifiedPeripherals#USB_UART_and_USB_to_Serial_.28RS-232.29_adapters
.
请参阅 更多内容... 部分以获取如何设置它们的详细信息。
如果您的电脑没有内置串行端口,您可以使用另一个 USB-to-RS232 适配器连接到 PC/笔记本电脑,将 RS232 转换为更常见的 USB 连接。
如果你在树莓派上没有可用的 USB 端口,你可以直接使用 GPIO 串行引脚,通过串行控制线或蓝牙串行模块(有关详细信息,请参阅更多内容...部分)。这两种方法都需要进行一些额外的设置。
在所有情况下,您可以使用 RS232 环回测试来确认一切工作正常并且设置正确(再次,参考更多内容...部分)。
- 接下来,准备您为此示例所需的软件。
您需要安装 pyserial
,这样我们才能使用 Python 的串行端口。
- 使用以下命令安装
pyserial
(你还需要安装pip
;有关详细信息,请参阅第三章,使用 Python 进行自动化和生产效率):
sudo pip-3.2 install pyserial
请参考pySerial
网站以获取更多文档信息:
pyserial.readthedocs.io/en/latest/
.
为了演示 RS232 串行控制,你需要一些连接到树莓派 GPIO 引脚的示例硬件。
serialMenu.py
脚本允许通过串行端口发送的命令来控制 GPIO 引脚。为了全面测试这一点,你可以将合适的输出设备(例如 LED)连接到每个 GPIO 引脚。你可以通过为每个 LED 使用 470 欧姆的电阻来确保总电流保持较低,这样就不会超过 Raspberry Pi 可以提供的最大 GPIO 电流:
用于通过串行控制测试 GPIO 输出的测试电路
如何做到这一点...
- 创建以下
serialControl.py
脚本:
#!/usr/bin/python3
#serialControl.py
import serial
import time
#Serial Port settings
SERNAME="/dev/ttyUSB0"
#default setting is 9600,8,N,1
IDLE=0; SEND=1; RECEIVE=1
def b2s(message):
'''Byte to String'''
return bytes.decode(message)
def s2b(message):
'''String to Byte'''
return bytearray(message,"ascii")
class serPort():
def __init__(self,serName="/dev/ttyAMA0"):
self.ser = serial.Serial(serName)
print (self.ser.name)
print (self.ser)
self.state=IDLE
def __enter__(self):
return self
def send(self,message):
if self.state==IDLE and self.ser.isOpen():
self.state=SEND
self.ser.write(s2b(message))
self.state=IDLE
def receive(self, chars=1, timeout=5, echo=True,
terminate="r"):
message=""
if self.state==IDLE and self.ser.isOpen():
self.state=RECEIVE
self.ser.timeout=timeout
while self.state==RECEIVE:
echovalue=""
while self.ser.inWaiting() > 0:
echovalue += b2s(self.ser.read(chars))
if echo==True:
self.ser.write(s2b(echovalue))
message+=echovalue
if terminate in message:
self.state=IDLE
return message
def __exit__(self,type,value,traceback):
self.ser.close()
def main():
try:
with serPort(serName=SERNAME) as mySerialPort:
mySerialPort.send("Send some data to me!rn")
while True:
print ("Waiting for input:")
print (mySerialPort.receive())
except OSError:
print ("Check selected port is valid: %s" %serName)
except KeyboardInterrupt:
print ("Finished")
if __name__=="__main__":
main()
#End
确保您要使用的串行端口中的serName
元素是正确的(例如,对于 GPIO 引脚使用/dev/ttyAMA0
,对于 USB RS232 适配器使用/dev/ttyUSB0
)。
将另一端连接到您的笔记本电脑或计算机的串行端口(串行端口可以是另一个 USB-to-RS232 适配器)。
使用串行程序如 Windows 的 HyperTerminal 或 RealTerm 或 OS X 的 Serial Tools 来监控您计算机上的串行端口。您需要确保已正确设置 COM 端口,并设置波特率为 9,600 bps(奇偶校验=None
,数据位=8
,停止位=1
,以及硬件流控制=None
)。
脚本将向用户发送数据请求并等待响应。
要将数据发送到树莓派,在另一台计算机上输入一些文本,然后按Enter键将其发送到树莓派。
- 你将在树莓派终端中看到类似以下输出的内容:
通过 USB-to-RS232 线缆从连接的电脑发送了“开启 LED 1”的文本
- 你也将在串行监控程序中看到类似以下输出:
RealTerm 显示连接的串行端口典型的输出
-
在树莓派上按 Ctrl + C 停止脚本。
-
现在,创建一个 GPIO 控制菜单。创建
serialMenu.py
:
#!/usr/bin/python3
#serialMenu.py
import time
import RPi.GPIO as GPIO
import serialControl as SC
SERNAME = "/dev/ttyUSB0"
running=True
CMD=0;PIN=1;STATE=2;OFF=0;ON=1
GPIO_PINS=[7,11,12,13,15,16,18,22]
GPIO_STATE=["OFF","ON"]
EXIT="EXIT"
def gpioSetup():
GPIO.setmode(GPIO.BOARD)
for pin in GPIO_PINS:
GPIO.setup(pin,GPIO.OUT)
def handleCmd(cmd):
global running
commands=cmd.upper()
commands=commands.split()
valid=False
print ("Received: "+ str(commands))
if len(commands)==3:
if commands[CMD]=="GPIO":
for pin in GPIO_PINS:
if str(pin)==commands[PIN]:
print ("GPIO pin is valid")
if GPIO_STATE[OFF]==commands[STATE]:
print ("Switch GPIO %s %s"% (commands[PIN],
commands[STATE]))
GPIO.output(pin,OFF)
valid=True
elif GPIO_STATE[ON]==commands[STATE]:
print ("Switch GPIO %s %s"% (commands[PIN],
commands[STATE]))
GPIO.output(pin,ON)
valid=True
elif commands[CMD]==EXIT:
print("Exit")
valid=True
running=False
if valid==False:
print ("Received command is invalid")
response=" Invalid:GPIO Pin#(%s) %srn"% (
str(GPIO_PINS), str(GPIO_STATE))
else:
response=" OKrn"
return (response)
def main():
try:
gpioSetup()
with SC.serPort(serName=SERNAME) as mySerialPort:
mySerialPort.send("rn")
mySerialPort.send(" GPIO Serial Controlrn")
mySerialPort.send(" -------------------rn")
mySerialPort.send(" CMD PIN STATE "+
"[GPIO Pin# ON]rn")
while running==True:
print ("Waiting for command...")
mySerialPort.send(">>")
cmd = mySerialPort.receive(terminate="rn")
response=handleCmd(cmd)
mySerialPort.send(response)
mySerialPort.send(" Finished!rn")
except OSError:
print ("Check selected port is valid: %s" %serName)
except KeyboardInterrupt:
print ("Finished")
finally:
GPIO.cleanup()
main()
#End
- 当你运行脚本(
sudo python3 serialMenu.py
),在串行监控程序中输入控制信息:
GPIO 串行控制菜单
- 树莓派上的终端输出将类似于以下截图,LED 灯应该相应地做出反应:
GPIO 串行控制菜单
树莓派验证从串行连接接收到的命令,并切换连接到 GPIO 引脚 7 和 11 的 LED 灯,然后打开再关闭。
它是如何工作的...
第一段脚本,serialControl.py
,为我们提供了一个serPort
类。我们使用以下函数定义该类:
-
__init__(self,serName="/dev/ttyAMA0")
: 此函数将使用serName
创建一个新的串行设备 – 默认的/dev/ttyAMA0
是 GPIO 串行引脚的 ID(参见 更多内容... 部分)。初始化后,将显示设备信息。 -
__enter__(self)
: 这是一个虚拟函数,允许我们使用with...as
方法。 -
send(self,message)
: 这用于检查串行端口是否已打开且未被占用;如果是这种情况,它将发送一条消息(在将其转换为原始字节后使用s2b()
函数)。 -
receive(self, chars=1, echo=True, terminate="r")
: 在检查串口是否打开且未被占用后,此函数随后通过串口等待数据。函数将收集数据,直到检测到终止字符,然后返回完整消息。 -
__exit__(self,type,value,traceback)
: 这个函数在serPort
对象不再需要通过with...as
方法时被调用,因此我们可以在这一点关闭端口。
脚本中的main()
函数通过串行端口向连接的计算机发送数据提示,然后等待输入,输入之后将跟随终止字符(们)。
下一个脚本 serialMenu.py
允许我们使用 serPort
类。
main()
函数设置 GPIO 引脚为输出(通过 gpioSetup()
),创建一个新的 serPort
对象,并最终等待来自串行端口的命令。每当接收到新的命令时,使用 handleCmd()
函数来解析消息,以确保在执行之前它是正确的。
该脚本将根据通过串行端口使用GPIO
命令关键字发出的指令来切换特定的 GPIO 引脚的开关状态。我们可以添加任意数量的命令关键字,并控制(或读取)我们连接到树莓派上的任何设备(或多个设备)。现在,我们有了使用通过串行链路连接的任何设备来控制树莓派的一种非常有效的方法。
还有更多...
除了串行发送和接收之外,RS232 串行标准还包括几个其他控制信号。为了测试它,你可以使用串行环回来确认串行端口
已正确设置。
为树莓派配置 USB-to-RS232 设备
一旦将 USB-to-RS232 设备连接到树莓派,请检查是否
通过输入以下命令列出新的串行设备:
dmesg | grep tty
dmesg
命令列出了系统上发生的事件;使用 grep
,我们可以过滤出提及 tty
的任何消息,如下面的代码所示:
[ 2409.195407] usb 1-1.2: pl2303 converter now attached to ttyUSB0
这表明基于 PL2303 的 USB-RS232 设备已连接(启动后 2,409 秒)并分配了ttyUSB0
标识。您将看到在/dev/
目录下(通常是/dev/ttyUSB0
或类似名称)已添加了一个新的串行设备。
如果设备未被检测到,您可以尝试与第一章中使用的步骤类似的步骤,即使用 Raspberry Pi 3 计算机入门,以定位和安装合适的驱动程序(如果可用)。
RS232 信号和连接
RS232 串行标准有很多变体,并包括六个额外的控制信号。
树莓派 GPIO 串行驱动程序(以及以下示例中使用的蓝牙 TTL 模块)仅支持 RX 和 TX 信号。如果您需要支持其他信号,例如常用于在编程 AVR/Arduino 设备之前重置的 DTR 信号,那么可能需要其他 GPIO 串行驱动程序来通过其他 GPIO 引脚设置这些信号。大多数 RS232 到 USB 转换器支持标准信号;然而,请确保您连接的任何设备都能处理标准 RS232 电压:
RS232 9 针 D 型连接器引脚排列和信号
想要了解更多关于 RS232 串行协议的细节以及了解这些信号的使用方法,请访问以下链接:
串行端口.
使用 GPIO 内置的串行引脚
标准的 RS232 信号可以从-15V 到+15V,因此你绝对不能直接将任何 RS232 设备连接到 GPIO 串行引脚。你必须使用 RS232 到 TTL 电压级别转换器(例如 MAX232 芯片)或使用 TTL 级别信号的设备(例如另一个微控制器或 TTL 串行控制台电缆):
USB-to-TTL 串行控制台电缆(电压等级为 3V)
树莓派在 GPIO 引脚上具有 TTL 级别的串行引脚,这允许连接 TTL 串行 USB 线缆。线缆将连接到树莓派的 GPIO 引脚,而 USB 将插入到您的计算机上,并像标准 RS232-to-USB 线缆一样被检测到:
将 USB-to-TTL 串行控制台线缆连接到树莓派 GPIO
可以从 USB 端口为 5V 引脚供电;然而,这将绕过内置的熔断器,因此不建议一般使用(只需将 5V 线断开,并像正常使用一样通过 micro USB 供电)。
默认情况下,这些引脚被设置为允许远程终端访问,使您能够连接
通过 PuTTY 连接到 COM 端口并创建一个串行 SSH 会话。
如果您想在未连接显示器的 Raspberry Pi 上使用它,串行 SSH 会话可能会有所帮助。
然而,串行 SSH 会话仅限于纯文本终端访问,因为它不支持 X10 转发,正如第一章中“使用 SSH(以及 X11 转发)远程连接到 Raspberry Pi”部分所述,在《Raspberry Pi 3 计算机入门》一书中。
为了将其用作标准串行连接,我们必须禁用串行控制台,以便我们可以使用它。
首先,我们需要编辑 /boot/cmdline.txt
文件以移除第一个 console
和 kgboc
选项(不要移除其他的 console=tty1
选项,这是您开启时默认的终端):
sudo nano /boot/cmdline.txt
dwc_otg.lpm_enable=0 console=ttyAMA0,115200 kgdboc=ttyAMA0,115200 console=tty1 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline rootwait
之前的命令行变为以下内容(确保这仍然是一个单独的
命令行():
dwc_otg.lpm_enable=0 console=tty1 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline rootwait
我们还必须通过使用#
注释掉运行getty
命令的任务(处理串行连接文本终端的程序),将其移除。这已在/etc/inittab
中设置如下:
sudo nano /etc/inittab
T0:23:respawn:/sbin/getty -L ttyAMA0 115200 vt100
之前的命令行变为以下:
#T0:23:respawn:/sbin/getty -L ttyAMA0 115200 vt100
在我们的脚本中引用 GPIO 串行端口,我们使用其名称,/dev/ttyAMA0
。
RS232 环回测试
您可以使用以下方法检查串行端口连接是否正常工作:
串行环回。
简单的环回连接由将 RXD 和 TXD 连接在一起组成。这些是 Raspberry Pi GPIO 接头上的第 8 和第 10 脚,或者在 USB-RS232 适配器上标准 RS232 D9 连接器上的第 2 和第 3 脚:
连接到测试树莓派 GPIO(左侧)和 RS232 9 针 D 型连接器(右侧)的串行环回连接
一条 RS232 全环回电缆也连接了 RS232 适配器上的 4 号引脚(DTR)和 6 号引脚(DSR),以及 7 号引脚(RTS)和 8 号引脚(CTS)。然而,在大多数情况下,这并不是必需的,除非使用这些信号。默认情况下,树莓派上没有专门为这些额外信号分配引脚:
RS232 全环回
创建以下 serialTest.py
脚本:
#!/usr/bin/python3
#serialTest.py
import serial
import time
WAITTIME=1
serName="/dev/ttyAMA0"
ser = serial.Serial(serName)
print (ser.name)
print (ser)
if ser.isOpen():
try:
print("For Serial Loopback - connect GPIO Pin8 and Pin10")
print("[Type Message and Press Enter to continue]")
print("#:")
command=input()
ser.write(bytearray(command+"rn","ascii"))
time.sleep(WAITTIME)
out=""
while ser.inWaiting() > 0:
out += bytes.decode(ser.read(1))
if out != "":
print (">>" + out)
else:
print ("No data Received")
except KeyboardInterrupt:
ser.close()
#End
当环回连接时,你会观察到信息被回显到屏幕上(当移除时,将显示无数据接收
):
在 GPIO 串行引脚上进行的 RS232 环回测试
如果我们需要非默认设置,它们可以在初始化串行端口时定义(pySerial 文档提供了所有选项的完整详情),如下面的代码所示:
ser = serial.Serial(port=serName, baudrate= 115200,
timeout=1, parity=serial.PARITY_ODD,
stopbits=serial.STOPBITS_TWO,
bytesize=serial.SEVENBITS)
使用蓝牙控制树莓派
通过连接支持串行端口配置文件(SPP)的 HC-05 蓝牙模块到 GPIO 串行 RX/TX 引脚,串行数据也可以通过蓝牙发送。这使得串行连接变为无线,从而可以使用 Android 平板电脑或智能手机来控制事物并从树莓派读取数据:
TLL 串行用的 HC-05 蓝牙模块
虽然使用 USB 蓝牙适配器也能实现类似的效果,但根据所使用的适配器类型,可能需要进行额外的配置。TTL 蓝牙模块可以作为一个物理电缆的直接替代品,只需要非常少的额外配置。
准备就绪
确保串行控制台已被禁用(参见前面的 还有更多... 部分)。
该模块应使用以下引脚进行连接:
连接到 TLL 串行接口的蓝牙模块
如何做到这一点...
配置并连接蓝牙模块后,我们可以将模块与笔记本电脑或智能手机配对,以无线方式发送和接收命令。蓝牙 spp pro 提供了一种简单的方法,通过蓝牙使用串行连接来控制或监控 Android 设备的 Raspberry Pi。
或者,您可能能够在您的 PC/笔记本电脑上设置一个蓝牙 COM 端口,并像之前的有线示例一样使用它:
- 当设备首次连接时,LED 灯快速闪烁以表示它正在等待配对。请启用您设备上的蓝牙并选择 HC-05 设备:
可在蓝牙 spp pro 中查看的 HC-05 蓝牙模块
- 点击配对按钮开始配对过程并输入设备的 PIN 码(默认为
1234
):
将蓝牙设备与 PIN 码(1234)配对
- 如果配对成功,您将能够连接到设备,并向 Raspberry Pi 发送和接收消息:
连接到设备并选择控制方法
- 在键盘模式下,您可以定义每个按钮的动作,以便在按下时发送合适的命令。
例如,可以将 Pin12 ON 设置为发送gpio 12 on
,而将 Pin12 OFF 设置为发送gpio 12 off
。
-
确保您通过菜单选项将结束标志设置为
rn
。 -
确保将
menuSerial.py
设置为使用 GPIO 串行连接:
serName="/dev/ttyAMA0"
- 运行
menuSerial.py
脚本(连接上 LED 灯):
sudo python3 menuSerial.py
- 检查蓝牙串行应用是否显示与以下截图所示的
GPIO 串行控制
菜单:
通过蓝牙进行 GPIO 控制
我们可以从下面的截图输出中看到,命令已被接收,连接到引脚 12 的 LED 灯已按需开启和关闭:
树莓派通过蓝牙接收 GPIO 控制
它是如何工作的...
默认情况下,蓝牙模块被设置为类似于 TTL 串行从设备,因此我们可以直接将其插入 GPIO RX 和 TX 引脚。一旦模块与设备配对,它将通过蓝牙连接传输串行通信。这使得我们可以通过蓝牙发送命令和接收数据,并使用智能手机或 PC 来控制 Raspberry Pi。
这意味着你可以将第二个模块连接到另一个设备(例如 Arduino)上
拥有 TTL 串行引脚,并使用树莓派(通过与其配对或通过其他方式)来控制它
TTL 蓝牙模块或适当配置一个 USB 蓝牙适配器)。如果该模块是
将其设置为从设备后,您需要重新配置它以作为主设备(请参阅更多内容...部分)。
还有更多...
现在,让我们了解如何配置蓝牙设置。
配置蓝牙模块设置
通过 KEY 引脚,可以将蓝牙模块设置为两种不同的模式之一。
在正常操作中,串行消息通过蓝牙发送;然而,如果我们需要更改蓝牙模块本身的设置,我们可以通过将 KEY 引脚连接到 3V3 并将它置于 AT 模式来实现。
AT 模式允许我们直接配置模块,使我们能够更改波特率、配对码、设备名称,甚至将其设置为主/从设备。
您可以使用 pySerial 的一部分miniterm
来发送所需的消息,如下面的代码所示:
python3 -m serial.tools.miniterm
当启动 miniterm
程序时,它会提示您输入要使用的端口号:
Enter port name: /dev/ttyAMA0
您可以发送以下命令(您需要快速完成此操作,或者粘贴它们,因为如果出现间隔,模块将超时并返回错误信息):
-
AT
: 此命令应响应“OK”。 -
AT+UART?
: 此命令将报告当前设置,格式为UART=<Param1>,<Param2>,<Param3>
。此命令的输出将是 OK。 -
要更改当前设置,请使用
AT+UART=<Param1>,<Param2>,<Param3>
,即AT+UART=19200,0,0
。
HC-05 AT 模式 AT+UART 命令参数
Zak Kemble 撰写了一篇优秀的指南,介绍了如何配置模块作为成对的从主设备(例如,在两个 Raspberry Pi 设备之间)。该指南可在以下链接找到:
关于 HC-05 模块的附加文档,请访问以下链接:
www.robotshop.com/media/files/pdf/rb-ite-12-bluetooth_hc05.pdf
.
控制 USB 设备
通用串行总线(USB)被计算机广泛用于通过一个通用的标准连接提供额外的外围设备和扩展。我们将使用
pyusb
Python 库用于通过 USB 向连接的设备发送自定义命令。
以下示例控制一个 USB 玩具导弹发射器,反过来它可以通过我们的 Python 控制面板进行控制。我们可以看到,同样的原理可以应用于其他 USB 设备,例如使用类似技术的机械臂,并且可以通过连接到树莓派 GPIO 的传感器来激活控制:
USB Tenx 技术 SAM 导弹发射器
准备就绪
我们需要使用pip-3.2
来为 Python 3 安装pyusb
,具体操作如下:
sudo pip-3.2 install pyusb
您可以通过运行以下命令来测试pyusb
是否已正确安装:
python3
> import usb
> help (usb)
> exit()
这应该允许您查看包信息,如果它被正确安装的话。
如何做到这一点...
我们将创建以下missileControl.py
脚本,其中将包含两个类和一个默认的main()
函数以进行测试:
- 按照以下方式导入所需的模块:
#!/usr/bin/python3
# missileControl.py
import time
import usb.core
- 定义
SamMissile()
类,该类提供了 USB 设备的特定命令,如下所示:
class SamMissile():
idVendor=0x1130
idProduct=0x0202
idName="Tenx Technology SAM Missile"
# Protocol control bytes
bmRequestType=0x21
bmRequest=0x09
wValue=0x02
wIndex=0x01
# Protocol command bytes
INITA = [ord('U'), ord('S'), ord('B'), ord('C'),
0, 0, 4, 0]
INITB = [ord('U'), ord('S'), ord('B'), ord('C'),
0, 64, 2, 0]
CMDFILL = [ 8, 8,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0]#48 zeros
STOP = [ 0, 0, 0, 0, 0, 0]
LEFT = [ 0, 1, 0, 0, 0, 0]
RIGHT = [ 0, 0, 1, 0, 0, 0]
UP = [ 0, 0, 0, 1, 0, 0]
DOWN = [ 0, 0, 0, 0, 1, 0]
LEFTUP = [ 0, 1, 0, 1, 0, 0]
RIGHTUP = [ 0, 0, 1, 1, 0, 0]
LEFTDOWN = [ 0, 1, 0, 0, 1, 0]
RIGHTDOWN = [ 0, 0, 1, 0, 1, 0]
FIRE = [ 0, 0, 0, 0, 0, 1]
def __init__(self):
self.dev = usb.core.find(idVendor=self.idVendor,
idProduct=self.idProduct)
def move(self,cmd,duration):
print("Move:%s %d sec"% (cmd,duration))
self.dev.ctrl_transfer(self.bmRequestType,
self.bmRequest,self.wValue,
self.wIndex, self.INITA)
self.dev.ctrl_transfer(self.bmRequestType,
self.bmRequest,self.wValue,
self.wIndex, self.INITB)
self.dev.ctrl_transfer(self.bmRequestType,
self.bmRequest, self.wValue,
self.wIndex, cmd+self.CMDFILL)
time.sleep(duration)
self.dev.ctrl_transfer(self.bmRequestType,
self.bmRequest, self.wValue,
self.wIndex, self.INITA)
self.dev.ctrl_transfer(self.bmRequestType,
self.bmRequest, self.wValue,
self.wIndex, self.INITB)
self.dev.ctrl_transfer(self.bmRequestType,
self.bmRequest, self.wValue,
self.wIndex, self.STOP+self.CMDFILL)
- 定义
Missile()
类,该类允许你检测 USB 设备并提供命令功能,具体如下:
class Missile():
def __init__(self):
print("Initialize Missiles")
self.usbDevice=SamMissile()
if self.usbDevice.dev is not None:
print("Device Initialized:" +
" %s" % self.usbDevice.idName)
#Detach the kernel driver if active
if self.usbDevice.dev.is_kernel_driver_active(0):
print("Detaching kernel driver 0")
self.usbDevice.dev.detach_kernel_driver(0)
if self.usbDevice.dev.is_kernel_driver_active(1):
print("Detaching kernel driver 1")
self.usbDevice.dev.detach_kernel_driver(1)
self.usbDevice.dev.set_configuration()
else:
raise Exception("Missile device not found")
def __enter__(self):
return self
def left(self,duration=1):
self.usbDevice.move(self.usbDevice.LEFT,duration)
def right(self,duration=1):
self.usbDevice.move(self.usbDevice.RIGHT,duration)
def up(self,duration=1):
self.usbDevice.move(self.usbDevice.UP,duration)
def down(self,duration=1):
self.usbDevice.move(self.usbDevice.DOWN,duration)
def fire(self,duration=1):
self.usbDevice.move(self.usbDevice.FIRE,duration)
def stop(self,duration=1):
self.usbDevice.move(self.usbDevice.STOP,duration)
def __exit__(self, type, value, traceback):
print("Exit")
- 最后,创建一个
main()
函数,如果文件直接运行,它将为我们提供对missileControl.py
模块的快速测试,如下所示:
def main():
try:
with Missile() as myMissile:
myMissile.down()
myMissile.up()
except Exception as detail:
time.sleep(2)
print("Error: %s" % detail)
if __name__ == '__main__':
main()
#End
- 当使用以下命令运行脚本时,你应该看到导弹发射器向下移动然后再向上移动:
sudo python3 missileControl.py
- 为了轻松控制设备,创建以下图形用户界面:
导弹指挥 GUI
虽然这里使用了简单的命令,但如果需要,你也可以使用一系列预设的命令。
- 为
missileMenu.py
导弹命令创建 GUI:
#!/usr/bin/python3
#missileMenu.py
import tkinter as TK
import missileControl as MC
BTN_SIZE=10
def menuInit():
btnLeft = TK.Button(root, text="Left",
command=sendLeft, width=BTN_SIZE)
btnRight = TK.Button(root, text="Right",
command=sendRight, width=BTN_SIZE)
btnUp = TK.Button(root, text="Up",
command=sendUp, width=BTN_SIZE)
btnDown = TK.Button(root, text="Down",
command=sendDown, width=BTN_SIZE)
btnFire = TK.Button(root, text="Fire",command=sendFire,
width=BTN_SIZE, bg="red")
btnLeft.grid(row=2,column=0)
btnRight.grid(row=2,column=2)
btnUp.grid(row=1,column=1)
btnDown.grid(row=3,column=1)
btnFire.grid(row=2,column=1)
def sendLeft():
print("Left")
myMissile.left()
def sendRight():
print("Right")
myMissile.right()
def sendUp():
print("Up")
myMissile.up()
def sendDown():
print("Down")
myMissile.down()
def sendFire():
print("Fire")
myMissile.fire()
root = TK.Tk()
root.title("Missile Command")
prompt = "Select action"
label1 = TK.Label(root, text=prompt, width=len(prompt),
justify=TK.CENTER, bg='lightblue')
label1.grid(row=0,column=0,columnspan=3)
menuInit()
with MC.Missile() as myMissile:
root.mainloop()
#End
它是如何工作的...
控制脚本由两个类组成:一个名为Missile
的类,它为控制提供了一个通用接口,另一个名为SamMissile
的类,它提供了正在使用的特定 USB 设备的所有详细信息。
为了驱动 USB 设备,我们需要大量关于该设备的信息,例如其 USB 标识、其协议以及它需要用于控制的控制消息。
Tenx Technology SAM 导弹设备的 USB ID 由厂商确定
ID (0x1130
) 和产品 ID (0x0202
)。这是相同的识别信息
你会在 Windows 的设备管理器中看到这些 ID。这些 ID 通常在 www.usb.org 进行注册;因此,每个设备应该是唯一的。再次提醒,你可以使用 dmesg | grep usb
命令来发现这些 ID。
我们使用设备 ID 通过usb.core.find
找到 USB 设备;然后,我们可以使用ctrl_transfer()
发送消息。
USB 信息包含五个部分:
-
请求类型(0x21):这定义了消息请求的类型,例如消息方向(主机到设备)、其类型(供应商)以及接收者(接口)。
-
请求 (0x09): 这是设置配置。
-
值 (0x02): 这是配置值。
-
索引 (0x01): 这是我们要发送的命令。
-
数据:这是我们想要发送的命令(如后续所述)。
SamMissile
设备需要以下命令来移动:
-
它需要两个初始化消息(
INITA
和INITB
)。 -
它还要求控制信息。这包括
CMD
,其中包含一个被设置为1
的控制字节,用于所需的组件。然后,CMD
被添加到CMDFILL
中以完成信息。
你会发现其他导弹装置和机械臂(见下文 还有更多... 部分)具有类似的消息结构。
对于每个设备,我们创建了__init__()
和move()
函数,并为每个有效的命令定义了值,当调用missile
类的left()
、right()
、up()
、down()
、fire()
和stop()
函数时,missile
类将使用这些值。
对于我们导弹发射器的控制 GUI,我们创建了一个带有五个按钮的小 Tkinter 窗口,每个按钮都会向导弹设备发送一个命令。
我们导入missileControl
并创建一个名为myMissile
的missile
对象,该对象将由每个按钮控制。
还有更多...
该示例仅展示了如何控制一个特定的 USB 设备;然而,有可能将其扩展以支持多种类型的导弹设备,甚至是一般意义上的其他 USB 设备。
控制类似的导弹型装置
USB 导弹型设备的变体有好几种,每种都有自己的 USB ID 和 USB 命令。我们可以通过定义它们自己的类别来处理这些其他设备,从而为这些设备添加支持。
使用 lsusb -vv
命令来确定与您的设备匹配的供应商和产品 ID。
对于Chesen Electronics/Dream Link
,我们必须添加以下代码:
class ChesenMissile():
idVendor=0x0a81
idProduct=0x0701
idName="Chesen Electronics/Dream Link"
# Protocol control bytes
bmRequestType=0x21
bmRequest=0x09
wValue=0x0200
wIndex=0x00
# Protocol command bytes
DOWN = [0x01]
UP = [0x02]
LEFT = [0x04]
RIGHT = [0x08]
FIRE = [0x10]
STOP = [0x20]
def __init__(self):
self.dev = usb.core.find(idVendor=self.idVendor,
idProduct=self.idProduct)
def move(self,cmd,duration):
print("Move:%s"%cmd)
self.dev.ctrl_transfer(self.bmRequestType,
self.bmRequest,
self.wValue, self.wIndex, cmd)
time.sleep(duration)
self.dev.ctrl_transfer(self.bmRequestType,
self.bmRequest, self.wValue,
self.wIndex, self.STOP)
对于 Dream Cheeky Thunder
,我们需要以下代码:
class ThunderMissile():
idVendor=0x2123
idProduct=0x1010
idName="Dream Cheeky Thunder"
# Protocol control bytes
bmRequestType=0x21
bmRequest=0x09
wValue=0x00
wIndex=0x00
# Protocol command bytes
CMDFILL = [0,0,0,0,0,0]
DOWN = [0x02,0x01]
UP = [0x02,0x02]
LEFT = [0x02,0x04]
RIGHT = [0x02,0x08]
FIRE = [0x02,0x10]
STOP = [0x02,0x20]
def __init__(self):
self.dev = usb.core.find(idVendor=self.idVendor,
idProduct=self.idProduct)
def move(self,cmd,duration):
print("Move:%s"%cmd)
self.dev.ctrl_transfer(self.bmRequestType,
self.bmRequest, self.wValue,
self.wIndex, cmd+self.CMDFILL)
time.sleep(duration)
self.dev.ctrl_transfer(self.bmRequestType,
self.bmRequest, self.wValue,
self.wIndex, self.STOP+self.CMDFILL)
最后,将脚本调整为使用所需的类,如下所示:
class Missile():
def __init__(self):
print("Initialize Missiles")
self.usbDevice = ThunderMissile()
机器人手臂
另一种可以用类似方式控制的设备是带有 USB 接口的 OWI 机器人手臂:
带有 USB 接口的 OWI 机器人手臂(图片由 Chris Stagg 提供)
这在 《The MagPi》杂志 中多次被提及,多亏了 Stephen Richards 的贡献。
关于 Skutter 的文章;USB 控制已在第 3 期(第 14 页)中详细解释
在issuu.com/themagpi/docs/the_magpi_issue_3_final/14
。也可以在www.raspberrypi.org/magpi/issues/3/
找到。
机器人臂可以通过以下类进行控制。记住,在调用move()
函数时,你还需要调整命令,例如UP
、DOWN
等,如下面的代码所示:
class OwiArm():
idVendor=0x1267
idProduct=0x0000
idName="Owi Robot Arm"
# Protocol control bytes
bmRequestType=0x40
bmRequest=0x06
wValue=0x0100
wIndex=0x00
# Protocol command bytes
BASE_CCW = [0x00,0x01,0x00]
BASE_CW = [0x00,0x02,0x00]
SHOLDER_UP = [0x40,0x00,0x00]
SHOLDER_DWN = [0x80,0x00,0x00]
ELBOW_UP = [0x10,0x00,0x00]
ELBOW_DWN = [0x20,0x00,0x00]
WRIST_UP = [0x04,0x00,0x00]
WRIST_DOWN = [0x08,0x00,0x00]
GRIP_OPEN = [0x02,0x00,0x00]
GRIP_CLOSE = [0x01,0x00,0x00]
LIGHT_ON = [0x00,0x00,0x01]
LIGHT_OFF = [0x00,0x00,0x00]
STOP = [0x00,0x00,0x00]
将 USB 控制进一步深化
用于 USB 导弹装置的控制理论和方法的适用范围很广,甚至可以应用于像 Xbox 360 的 Kinect(Xbox 游戏控制台的一个特殊 3D 摄像头附加装置)这样的非常复杂的设备。
Adafruit 的网站上有一篇由Limor Fried(也称为Ladyada)撰写的非常有趣的教程,介绍了如何分析和调查 USB 命令;您可以通过learn.adafruit.com/hacking-the-kinect
访问它。
如果你打算逆向工程其他 USB 设备,这绝对值得一看。在本章中,我们使用了树莓派来远程控制主电源插座,从另一台计算机通过串行连接发送命令,以及远程控制 GPIO。我们还使用了 SPI 来驱动一个 8 x 8 的 LED 矩阵显示屏。
第十四章:我能为您推荐一部电影吗?
在本章中,我们将介绍以下菜谱:
-
欧几里得距离得分计算
-
皮尔逊相关系数计算
-
如何在数据集中找到相似的用户
-
如何开发电影推荐模块
-
推荐系统应用
简介
电影推荐是根据用户的兴趣预测电影。数据库中的内容会被过滤,并为用户推荐合适的电影。推荐合适的电影增加了用户购买电影的概率。协同过滤被用于构建电影推荐系统。它考虑了当前用户过去的行为。它还考虑了其他用户给出的评分。协同过滤包括在数据集中寻找和计算欧几里得距离、皮尔逊相关性和寻找相似用户。
计算欧几里得距离得分
构建推荐引擎的第一步包括在数据库中寻找相似用户。欧几里得距离得分是寻找相似性的一个度量标准。
准备就绪
NumPy(数值 Python)需要在 Raspberry Pi 3 上安装以计算欧几里得距离。读者可以在 Raspberry Pi 3 终端中输入以下命令来安装numpy
:
sudo apt-get -y install python-numpy
如何做到这一点...
- 我们将创建一个新的 Python 文件并将以下包导入到其中:
import json
import numpy as np
- 要计算两个用户之间的欧几里得得分,我们将定义一个新的函数。让我们检查数据库中用户的存活性:
# The following code will return the Euclidean distance score between user1 and user2:
def euclidean_dist_score(dataset, FirstUser, SecondUser):
if FirstUser not in dataset:
raiseTypeError('User ' + FirstUser + ' not present in the dataset')
if SecondUser not in dataset:
raiseTypeError('User ' + SecondUser + ' not present in the dataset')
- 我们现在将提取那些被用户评分的电影。然后我们将计算得分:
# Movies rated by both FirstUser and SecondUser
Both_User_rated = {}
for element in dataset[FirstUser]:
if element in dataset[SecondUser]:
Both_User_rated[element] = 1
- 没有共同的电影表示第一位和第二位用户之间没有相似性。(否则无法在数据库中计算评分):
# Score 0 indicate no common movies
if len(Both_User_rated) == 0:
return 0
- 如果评分是常见的,计算平方差的和,然后计算结果的平方根,最后进行归一化。此时,分数将在零和一之间:
SquareDifference = []
for element in dataset[FirstUser]:
if element in dataset[SecondUser]:
SquareDifference.append(np.square(dataset[FirstUser][element] -
dataset[SecondUser][element]))
return 1 / (1 + np.sqrt(np.sum(SquareDifference)))
如果两个用户评分相同,那么平方差的和将是一个较小的值。因此,得分将会很高。这正是这里的宗旨。
- 我们将命名我们的数据文件为
movie_rates.json
。现在我们来加载它:
if __name__=='__main__':
data_file = 'movie_rates.json'
with open(data_file, 's') as m:
data = json.loads(m.read())
- 让我们计算两个随机用户之间的欧几里得距离得分:
FirstUser = 'Steven Ferndndes'
SecondUser = 'Ramesh Nayak'
print "nEuclidean score:"
print euclidean_dist_score(data, FirstUser, SecondUser)
- 上述代码将在终端中打印欧几里得距离得分:
它是如何工作的...
读者可以参考文章《相似性与推荐系统》来了解欧几里得距离是如何工作的:
www.inf.ed.ac.uk/teaching/courses/inf2b/learnnotes/inf2b-learn-note02-2up.pdf
还有更多...
读者可以参考文章《协同过滤推荐系统中使用的各种指标比较》来了解更多关于推荐系统中使用的各种指标:
ieeexplore.ieee.org/document/7346670/
参见
- Python 构建推荐引擎快速指南:
www.analyticsvidhya.com/blog/2016/06/quick-guide-build-recommendation-engine-python/
计算皮尔逊相关系数
欧几里得距离假设样本点以球形方式分布在样本均值周围,这并不总是成立。因此,使用皮尔逊相关系数分数而不是欧几里得距离分数。接下来将解释皮尔逊相关系数分数的计算方法。
如何做到这一点...
- 我们将创建一个新的 Python 文件并导入以下包:
import json
import numpy as np
- 要计算两个用户之间的皮尔逊相关系数,我们将定义一个新的函数。让我们检查数据库中用户的存在情况:
# Returns the Pearson correlation score between user1 and user2
def pearson _dist_score(dataset, FirstUser, SecondUser):
if FirstUser not in dataset:
raise TypeError('User ' + FirstUser + ' not present in the dataset')
if SecondUser not in dataset:
raise TypeError('User ' + SecondUser + ' not present in the dataset')
- 我们现在将提取那些被用户评分的电影:
# Movies rated by both FirstUser and SecondUser
Both_User_rated = {}
for item in dataset[FirstUser]:
if item in dataset[SecondUser]:
both_User_rated[element] = 1
rating_number= len(both_User_rated)
- 没有共同的电影表示第一个用户和第二个用户之间没有相似性;因此,我们返回零:
# Score 0 indicate no common movies
if rating_number == 0:
return 0
- 计算常见电影评分的平方和:
# Calculate the sum of ratings of all the common preferences
FirstUser_sum= np.sum([dataset[FirstUser][ element] for item in both_User_rated])
SecondUser_sum=np.sum([dataset[SecondUser][element] for item in both_User_rated])
- 计算所有共同电影评分的平方和:
# Calculate the sum of squared ratings of all the common preferences
FirstUser_squared_sum = np.sum([np.square(dataset[FirstUser][element]) for element in
both_User_rated])
SecondUser_squared_sum= np.sum([np.square(dataset[SecondUser][element]) for element inboth_User_rated])
- 现在,计算乘积之和:
# Calculate the sum of products of the common ratings
sum_product = np.sum([dataset[FirstUser][element] * dataset[SecondUser][element] for item inboth_User_rated])
- 计算计算皮尔逊相关系数所需的各种变量:
# Pearson correlation calculation
PSxy = sum_product - (FirstUser_sum* SecondUser_sum/rating_number)
PSxx = FirstUser_squared_sum- np.square(FirstUser_sum) / rating_number
PSyy = SecondUser_squared_sum - np.square(SecondUser_sum) / rating_number
- 我们需要关注分母变为零的问题:
if PSxx * PSyy == 0:
return 0
- 返回皮尔逊相关系数:
return PSxy / np.sqrt(PSxx * PSyy)
- 定义
main
函数并计算两个用户之间的皮尔逊相关系数:
if __name__=='__main__':
data_file = 'movie_rates.json'
with open(data_file, 's') as m:
data = json.loads(m.read())
FirstUser = 'StevenFerndndes'
SecondUser = 'Rameshnayak'
print "nPearson score:"
print pearson _dist_score(data, FirstUser, SecondUser)
- 上述代码将在终端中打印皮尔逊相关系数:
它是如何工作的...
读者可以参考 皮尔逊相关系数 - 简易教程 来了解皮尔逊相关系数是如何计算的:www.spss-tutorials.com/pearson-correlation-coefficient/
还有更多...
读者可以在此参考两种不同的皮尔逊相关系数变体:
- 相关系数:简单定义、公式、易行步骤:
- 一种用于提高协同过滤准确性的新用户相似度模型:
www.sciencedirect.com/science/article/pii/S0950705113003560
参见
- 基于用户偏好模型的协同过滤新相似度度量:
ieeexplore.ieee.org/document/7279353/
- 人工免疫系统在电影推荐系统中的应用:
ieeexplore.ieee.org/document/6846855/
在数据集中寻找相似用户
在数据集中找到相似用户是电影推荐中的关键步骤,这个过程将在下文中进行解释。
如何做到这一点...
- 我们将创建一个新的 Python 文件并导入以下包:
import json
import numpy as np
from pearson _dist_score import pearson _dist_score
- 首先,为输入用户定义一个函数,用于找到相似用户。为此,需要三个参数:相似用户的数量、输入用户和数据库。检查用户是否存在于数据库中。如果存在,计算数据库中存在的用户与输入用户之间的皮尔逊相关系数:
# Finds a specified number of users who are similar to the input user
def search_similar_user (dataset, input_user, users_number):
if input_user not in dataset:
raiseTypeError('User ' + input_user + ' not present in the dataset')
# Calculate Pearson scores for all the users
scores = np.array([[x, pearson _dist_score(dataset, input_user, i)] for i in dataset if
user != i])
- 现在将获得的分数按降序排列:
# Based on second column, sort the score
sorted_score= np.argsort(scores[:, 1])
# Sorting in decreasing order (highest score first)
dec_sorted_score= sorted_score[::-1]
- 我们将选择前
k
个分数:
# Pick top 'k' elements
top_q= dec_sorted_score[0:users_number]
return scores[top_q]
- 我们定义
main
函数并加载输入数据库:
if __name__=='__main__':
data_file = ''movie_rates.json'
with open(data_file, 's') as m:
data = json.loads(m.read())
- 我们发现三个相似的用户:
user = 'JohnCarson'
print "nUsers similar to " + input_user + ":n"
similar_one = search_similar_user(data, input_user, 3)
print "input_usertttSimilarity scoren"
for element in similar_one:
print element[0], 'tt', round(float(element[1]), 2)
参见
- 使用 YAGO 和 IMDB 推荐电影和明星:
ieeexplore.ieee.org/document/5474144/
开发电影推荐模块
我们现在准备构建电影推荐引擎。我们将使用在前面的菜谱中构建的所有功能。让我们看看它是如何实现的。
如何做到这一点...
- 我们将创建一个新的 Python 文件并导入以下包:
import json
import numpy as np
from euclidean_score import euclidean_score
from pearson_score import pearson_score
from search_similar_user import search_similar_user
- 对于给定用户的电影推荐,我们首先定义一个函数。我们现在检查该用户是否已经存在:
# Generate recommendations for a given user
def recommendation_generated(dataset, user):
if user not in dataset:
raiseTypeError('User ' + user + ' not present in the dataset')
- 计算当前用户的个人得分:
sumofall_scores= {}
identical_sums= {}
for u in [x for x in dataset if x != user]:
identical_score= pearson_score(dataset, user, u)
if identical_score<= 0:
continue
- 查找用户尚未评分的电影:
for element in [x for x in dataset[u] if x not in dataset[user] or
dataset[user][x] == 0]:
sumofall_scores.update({item: dataset[u][item] * identical_sums})
identical_sums.update({item: identical_score})
- 如果用户已经看过数据集中的所有电影?那么将不会有任何推荐:
if len(sumofall_scores) == 0:
return ['No recommendations possible']
- 我们现在有一份这些分数的列表。让我们创建一个电影排名的标准化列表:
# Create the normalized list
rank_of_movie= np.array([[total/ identical_sums[element], element]
for element, total in sumofall_scores.element()])
- 根据分数,按降序排列列表:
# Based on first column, sort in decreasing order
rank_of_movie = rank_of_movie[np.argsort(rank_of_movie[:, 0])[::-1]]
- 我们终于准备好提取电影推荐了:
# Recommended movies needs to be extracted
recommended = [movie for _, movie in movie_ranks]
return recommended
- 定义
main
函数并加载数据集:
if __name__=='__main__':
data_file = rating_of_miovie.json'
with open(data_file, 'r') as f:
data = json.loads(f.read())
- 让我们为
史蒂文·费尔南德斯
生成推荐:
user = ' Steven Ferndndes '
print "nRecommendations for " + user + ":"
movies = recommendation_generated(data, user)
for i, movie in enumerate(movies):
print str(i+1) + '. ' + movie
- 用户
Ramesh Nayak
观看了所有电影。因此,如果我们尝试为他生成推荐,应该显示零推荐:
user = ' Ramesh Nayak '
print "nRecommendations for " + user + ":"
movies = recommendation_generated(data, user)
for i, movie in enumerate(movies):
print str(i+1) + '. ' + movie
- 上述代码将在终端中打印电影推荐:
参见
- 推荐系统详解:
- 推荐系统算法:
blog.statsbot.co/recommendation-system-algorithms-ba67f39ac9a3
推荐系统的应用
推荐系统目前被广泛应用于各个领域。它们发挥着非常突出的作用,并在包括音乐、电影、书籍、新闻、搜索查询、社交标签、研究文章以及一般产品在内的多个领域得到应用。此外,还有针对餐厅、专家、合作伙伴、金融服务、笑话、服装、Twitter 页面和人寿保险的推荐系统。
第十五章:硬件和软件清单
在本章中,我们将涵盖以下主题:
-
通用组件来源
-
硬件清单
-
软件列表
简介
这本书使用多种硬件来展示可以取得的成果
以各种方式结合硬件和软件。为了最大限度地利用这本书,强烈建议您尝试一些硬件项目。我觉得观察从您的编码努力中产生的物理结果尤其有成就感,这正是树莓派与普通计算机的不同之处。
常见的问题是在不花费巨额资金的情况下找到适合项目的正确组件。本书中使用的所有硬件组件都专注于使用低成本物品,这些物品通常可以从各种供应商那里购买,在大多数情况下,只需花费几美元即可。
为了帮助您找到合适的物品,本附录将列出章节中使用的每个硬件组件,并提供获取它们的位置链接。此列表并非详尽无遗,且这些物品(及其价格)的可用性可能会随时间变化,因此每次购买时,请务必四处寻找最佳价值。本书的章节中已经提供了足够实用的细节,以便您自行寻找组件并构建自己的模块。
本附录还包括书中提到的所有软件和 Python 模块的完整列表,包括所使用的具体版本。如果书中使用的软件得到更新和改进,某些模块可能会失去向后兼容性。因此,如果你发现最新安装的版本无法按预期工作,可能需要安装一个较旧的版本(如何操作的详细信息在更多内容...部分的软件列表菜谱中提供)。
通用组件来源
一旦你完成了这本书中的一些基于硬件的食谱,你可能发现你想要尝试其他组件。有很多地方你可以以合理的价格购买到通用电子组件和附加模块,特别是针对树莓派或其他基于电子的爱好。这个列表并不全面,但它包含了我过去订购过并且性价比高的几个地方。
通用电子元件零售商
你可能会发现以下列表中提到的每个零售商都有自己的本地化网站,提供全球服务,或者拥有本地分销服务:
-
Farnell/element14/Newark:
www.newark.com
-
RS Components:
www.rs-components.com
-
亚马逊:
www.amzon.com
-
eBay:
www.ebay.com
-
Tandy UK:
www.tandyonline.co.uk
-
Maplin UK:
www.maplin.co.uk
制造商、爱好者以及树莓派专家
有许多公司专门销售可用于计算机和设备(如树莓派)的模块和附加组件,这些产品主要面向爱好者。以下是一些例子:
-
Adafruit Industries:
www.adafruit.com
-
SparkFun 电子:
www.sparkfun.com
-
Mouser Electronics:
www.mouser.com
-
Banggood:
www.banggood.com
-
DealExtreme:
dx.com
-
Pimoroni:
shop.pimoroni.com
-
Pi 供应:
www.pi-supply.com
-
PiBorg:
www.piborg.com
-
Hobbyking:
www.hobbyking.com
-
ModMyPi:
www.modmypi.com
-
Quick2Wire:
quick2wire.com
-
GeekOnFire:
www.geekonfire.com
-
Ciseco:
shop.ciseco.co.uk
您还可以查看我的个人网站,该网站专注于教育套件和教程:
- Pi 硬件:
PiHardware.com
硬件清单
本节中提到了本书各章节所使用的硬件的概述。
第一章
本节中提到了本书各章节所使用的硬件的概述。
本章描述了树莓派的设置;提到的项目包括以下内容:
-
树莓派及其电源
-
HDMI 显示器和 HDMI 线/模拟电视和模拟视频线
-
键盘
-
鼠标
-
网络电缆/Wi-Fi 适配器
第二章 – 第七章
在这些章节中,没有使用任何额外的硬件,因为它们纯粹讨论
软件食谱。
第八章
本章仅使用 USB 网络摄像头硬件。
第九章
本章所使用的组件在大多数电子元件零售商处均有售(例如在通用电子元件零售商部分之前列出的那些)。它们也可以从Pi 硬件处购买成套组件;在文本中,那些可以从特定零售商处购买的项目会被突出显示。
控制 LED 的套件包括以下设备:
-
四根杜邦公对母插头线(Pimoroni Jumper Jerky)
-
一个迷你面包板(170 个接线点)或一个更大的面包板(Pimoroni)
-
RGB LED(共阴极)或 3 个标准 LED(理想情况下为红/绿/蓝)
-
面包板走线(实心线)
-
三个 470 欧姆电阻
应对按钮的套件包括以下设备:
-
两个杜邦公对母贴片线(Pimoroni Jumper Jerky)
-
一个迷你面包板(170 个接线点)或一个更大的面包板(Pimoroni)
-
一个用于切换和瞬时开关的按钮(或一个电线连接)
接通/断开电路)
-
面包板用导线(实心芯线)
-
1K 欧姆电阻
用于控制关机按钮的物品如下:
-
三条杜邦公对母插头排线(Pimoroni Jumper Jerky)
-
一个迷你面包板(170 个接线点)或更大尺寸的(Pimoroni)
-
按钮式开关(瞬时闭合)
-
一个普通 LED(红色)
-
两个 470 欧姆电阻
-
面包板走线(实心线)
在食谱中“还有更多...”部分使用的附加项目,包括以下内容:
-
一个按钮
-
一个 470 欧姆的电阻
-
一个引脚头和两个带有跳线连接器(或可选的开关)的引脚
-
一根面包板线(实心芯线)
-
两个 4 针排针
用于 GPIO 键盘输入的组件如下:
-
面板:半尺寸或更大(Pimoroni)
-
七杜邦公对母插头排线(Pimoroni Jumper Jerky)
-
六个按键
-
六个 470 欧姆电阻
-
或者,一个自焊 DPad 套件(Pi 硬件)
用于复用式彩色 LED 的元件如下:
-
五种常见的共阴极 RGB LED
-
三个 470 欧姆电阻
-
Vero-原型板或大型面包板(Tandy)
-
一套自焊 RGB-LED 套件(Pi 硬件)
用于撰写信息的物品需要与前面的食谱相同的物品,再加上以下内容:
-
一根支撑杆、橡皮筋、USB Wi-Fi、便携式 USB 电池等等
-
倾斜开关(球轴承类型适合)(4-Tronix)
第十章
本章使用以下硬件:
-
PCF8591 芯片或模块(DealExtreme SKU: 150190 或 Quick2Wire I2C)
模拟板套件)
-
Adafruit I2C 双向电平转换器 (Adafruit ID: 757)
第十一章
本章未使用任何额外硬件,因为它们讨论的是纯软件配方。
第十二章
Pi-Rover 需要以下硬件或类似硬件:
-
一个巨大的回形针(76 毫米/3 英寸)或一个滚轮
-
电机和齿轮轮(ModMyPi 或 PiBorg)
-
电池/电源
-
底盘:推动夜灯
-
电机驱动/控制器:达林顿阵列模块 ULN2003(DealExtreme SKU - 153945)
-
小型电缆扎带或线扎带
以下列表也出现在更多内容...部分:
-
PicoBorg 电机控制器(PiBorg PicoBorg)
-
魔术师机器人底盘(Sparkfun ID: 10825)
-
4 电机智能车底盘(** DealExtreme ** SKU: 151803)
-
2 轮智能汽车模型(DealExtreme SKU: 151803)
高级电机控制示例使用以下项目:
- H 桥电机控制器(DealExtreme SKU: 120542 或 GeekOnFire SKU: A2011100407)
Hex Pod Pi-Bug 需要以下硬件或类似设备:
-
Adafruit I2C 16-通道 12 位 PWM/伺服驱动器 (Adafruit ID: 815)
-
MG90S 9g 金属齿轮伺服器(HobbyKing)
-
三个巨型回形针(76mm/3 英寸)
-
轻型规格线/电缆扎带
-
一小块胶合板或纤维板
基本伺服驱动的机械臂用于 ServoBlaster 示例(4-Tronix MeArm)。
红外遥控器示例使用以下组件:
- TSOP38238 (法恩莱尔 2251359)
在剩余的部分中,以下硬件被用于扩展可用输入/输出,避免障碍物,并确定机器人的方向:
-
MCP23017 输入输出扩展器(Ciseco SKU: K002)
-
微型开关
-
HC-SR04 超声波传感器(DealExtreme SKU: 133696)
-
超声波传感器使用一个 2K 欧姆电阻和一个 3K 欧姆电阻
-
XLoBorg: MAG3110 指南针模块(PiBorg XLoBorg)
可选地,可以使用四根雌到雄 Dupont 线连接到 XLoBorg(Pimoroni Jumper Jerky)
第十三章
本章使用以下硬件:
-
遥控主电源插座(Maplin/Amazon)
-
中继模块(Banggood 8 路 SKU075676)
-
替代方案是使用 433MHz 射频发射/接收器(Banggood SKU075671)
-
LED 8x8 SPI 矩阵模块 MAX7219(Banggood 自焊套件 SKU072955)
-
RS-232 转 USB 线缆(亚马逊/通用电脑配件)
-
RS-232 空调制解调器电缆/适配器(亚马逊/通用电脑配件)
-
RS-232 TTL USB 控制台电缆(Adafruit ID: 70)
-
HC-05 蓝牙主从模块带 PCB 背板(Banggood SKU078642)
-
USB Tenx Technology SAM 导弹发射器
-
带 USB 接口的 OWI 机器人手臂(Maplin/Amazon)
第十四章
本章未使用任何额外硬件,因为它们讨论的是纯软件配方。
软件列表
本书使用一系列软件包来扩展预装软件的功能。
PC 软件工具
在大多数情况下,应使用可用的最新软件版本(如果后续版本中存在兼容性问题,版本列表将列出)。使用的软件列表是
如下所示:
-
Notepad ++: www.notepad-plus-plus.org (版本 7.5.6)
-
PuTTY: www.putty.org (版本 0.62)
-
VNC Viewer: www.realvnc.com (版本 6.2.1)
-
Xming: www.straightrunning.com/XmingNotes (版本 6.9.0.31 公共领域发布)
-
MobaXterm: mobaxterm.mobatek.net (版本 8.6)
-
SD 格式化工具: www.sdcard.org/downloads/formatter_4 (版本 5.0)
-
RealTerm: realterm.sourceforge.net (版本 2.0.0.70)
树莓派软件包
本节以以下格式列出书中各章节所使用的每个软件包(版本列出是为了以防后续版本发布时出现兼容性问题):
- 包名(版本)
支持网站
:
Install command
第一章
-
本章描述了硬件设置,因此以下包是可选的(或必要时特定的硬件驱动程序):
-
TightVNC (版本 1.3.9-6.5):
www.tightvnc.com
sudo apt-get install tightvncserver
- Samba (版本 2:4.2.10):
www.samba.org
sudo apt-get install samba
第二章
以下是在第二章中使用的命令,分割文本数据和构建文本分类器:
sudo apt-get install geany
sudo apt-get -y install python-pip
sudo apt-get -y install python-git
sudo apt-get -y install python-numpy
sudo apt-get -y install python-scipy
sudo pip install --upgrade cython
sudo pip install -U scikit-learn
sudo pip install imutils
sudo apt-get -y install python-sklearn
sudo apt-get -y install python-skimage
第三章
- Tkinter(版本 3.4.2-1):
wiki.python.org/moin/TkInter
sudo apt-get install python3-tk
- pip-3.2(版本 1.5.6-5):
pip.pypa.io/en/latest
sudo apt-get install python3-pip
- libjpeg-dev (版本 1:1.3.1-12):
libjpeg.sourceforge.net
sudo apt-get install libjpeg-dev
- Pillow (版本 2.1.0):
pillow.readthedocs.io/en/latest
sudo pip-3.2 install pillow
第四章
以下是在第四章,“预测词语中的情感”中使用的命令:
sudo apt-get install geany
sudo apt-get -y install python-pip
sudo apt-get -y install python-git
sudo apt-get -y install python-numpy
sudo apt-get -y install python-scipy
sudo pip install --upgrade cython
sudo pip install -U scikit-learn
sudo pip install imutils
sudo apt-get -y install python-sklearn
sudo apt-get -y install python-skimage
第五章
- Tkinter (版本 3.4.2-1):
wiki.python.org/moin/TkInter
sudo apt-get install python3-tk
第六章
以下是在第六章中使用的命令,图像中的边缘和轮廓检测:
sudo apt-get install geany
sudo apt-get -y install python-pip
sudo apt-get -y install python-opencv
sudo apt-get -y install python-numpy
sudo apt-get -y install python-scipy
sudo pip install --upgrade cython
sudo pip install -U scikit-learn
sudo pip install imutils
sudo apt-get -y install python-sklearn
sudo apt-get -y install python-skimage
第七章
- pip-3.2 (版本 1.1-3):
www.pip-installer.org/en/latest
sudo apt-get install python3-pip
- Pi3D (版本 2.13):
pi3d.github.iosudo
pip-3.2 install pi3d
此外,也看看使用 Pi3D 的 3D 图形:
paddywwoof.github.io/pi3d_book/_build/latex/pi3d_book.pdf
第八章
以下是在第八章,“构建人脸检测和人脸识别应用”中使用的命令:
sudo apt-get install geany
sudo apt-get -y install python-pip
sudo apt-get -y install python-opencv
sudo apt-get -y install python-numpy
sudo apt-get -y install python-scipy
sudo pip install --upgrade cython
sudo pip install -U scikit-learn
sudo pip install imutils
sudo apt-get -y install python-sklearn
sudo apt-get -y install python-skimage
第九章
- RPi.GPIO 通常预安装在 Raspbian(版本 0.6.2~jessie-1)上:
sourceforge.net/p/raspberry-gpio-python/wiki/BasicUsage
sudo apt-get install python3-rpi.gpio
- flite (版本 1.4 发布-12):
www.festvox.org/flite
sudo apt-get install flite
- uInput (版本 0.11.2):
tjjr.fi/sw/python-uinput
安装说明提供在第九章,使用 Python 驱动硬件:
第十章
- i2c-tools (版本 3.1.1+svn-2):
www.lm-sensors.org/wiki/I2CTools
sudo apt-get install i2c-tools
- pip-3.2(版本 1.5-6-5):
www.pip-installer.org/en/latest
sudo apt-get install python3-pip
- python3-dev (版本 3.4.2-2): 为某些软件所需的 Python 的头文件和静态库
sudo apt-get install python3-dev
- wiringpi2 (版本 2.32.3):
wiringpi.com
sudo pip-3.2 install wiringpi2
第十一章
以下是在第十一章中使用的命令,构建用于光学字符识别的神经网络模块:
sudo apt-get install geany
sudo apt-get -y install python-pip
sudo apt-get -y install python-opencv
sudo apt-get -y install python-numpy
sudo apt-get -y install python-scipy
sudo pip install --upgrade cython
sudo pip install -U scikit-learn
sudo pip install imutils
sudo apt-get -y install python-sklearn
sudo apt-get -y install python-skimage
sudo pip install -U nltk
sudo pip install neurolab
第十二章
- wiringpi2 (版本 2.32.3):
wiringpi.com
sudo pip-3.2 install wiringpi2
- ServoBlaster(版本 2.32.3):
github.com/richardghirst/PiBits
sudo pip-3.2 install wiringpi2
第十三章
-
RPi.GPIO 通常预安装在 Raspbian(版本 0.6.2~jessie-1)上:
sudo apt-get install python3-rpi.gpio
- Tkinter (版本 3.4.2-1):
wiki.python.org/moin/TkInter
sudo apt-get install python3-tk
- wiringpi2 (版本 2.32.2):
wiringpi.com
sudo pip-3.2 install wiringpi2
- minicom (版本 2.7-1):
linux.die.net/man/1/minicom
sudo apt-get install minicom
- pyserial (版本 2.6):
pyserial.sourceforge.net
sudo pip-3.2 install pyserial
- pyusb (版本 1.0.0):
github.com/walac/pyusb
sudo pip-3.2 install pyusb
第十四章
以下是在第十四章“我能为您推荐一部电影吗?”中使用的命令:
sudo apt-get install geany
sudo apt-get -y install python-pip
sudo apt-get -y install python-opencv
sudo apt-get -y install python-numpy
sudo apt-get -y install python-scipy
sudo pip install --upgrade cython
sudo pip install -U scikit-learn
sudo pip install imutils
sudo apt-get -y install python-sklearn
sudo apt-get -y install python-skimage
还有更多...
本书所使用的绝大多数 Raspberry Pi 软件包都已安装
使用 apt-get
和 pip
进行配置。以下各节中已给出每个命令的有用说明。
APT 命令
以下是在 APT(这是 Raspbian 默认预安装的)中使用的有用命令:
-
在安装软件包之前,请始终使用
sudo apt-get update
命令更新软件包列表以获取最新版本和程序 -
通过搜索包含以下内容的任何软件包来查找软件
在包名或描述中使用
<searchtext>
命令sudo apt-cache search <seachtext>
-
使用特定的
<packagename>
安装软件sudo apt-get install <packagename>
-
使用以下命令卸载特定的软件包
sudo apt-get remove <packagename>
-
使用以下命令显示已安装的软件包的当前版本
sudo apt-cache showpkg <packagename>
如果你想要安装特定版本的软件包,请使用 sudo apt-get install <package name>=<version>
如果您需要在没有互联网访问的系统上使用这些软件包,您可以使用以下命令将软件包(及其依赖项)下载到指定的目录:
sudo apt-get -o dir::cache::archives="<目标目录>" -d -y 安装 <软件包名称>
您可以通过运行 sudo apt-get
和 sudo apt-cache
来查看附加命令的详细信息。或者,您可以通过使用 man
命令、man apt-get
命令和 man apt-cache
命令来阅读手册页面来查看它们。
Pip Python 包管理器命令
Pip 的有用命令(这通常不在 Raspbian 上预安装)如下列出:
-
要安装 Pip 或 Python 3,请使用
sudo apt-get install python3-pip
命令 -
使用
sudo pip-3.2 install <packagename>
安装所需的软件包 -
使用
sudo pip-3.2 uninstall
` 卸载特定的软件包 -
要查找已安装软件包的版本,请使用
pip-3.2 freeze | grep <packagename>
-
使用以下命令安装特定版本的软件包
sudo pip-3.2 install <packagename>==<version>
例如,要检查您系统上安装的 Pi3D 版本,请使用
pip-3.2 freeze | grep pi3d
.
要替换已安装的 Pi3D 版本为 2.13,请使用
sudo pip-3.2 uninstall pi3d
和 sudo pip-3.2 install pi3d==2.13
.