Python 树莓派项目(二)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

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

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

文件 I/O

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

  • 在 Linux 操作系统世界中,一切都是文件。与树莓派上的外围设备交互类似于从文件中读取/写入。例如:在 第四章 通信接口 中,我们讨论了串口通信。你应该能够观察到串口通信就像文件读写操作一样。
  • 我们在每一个项目中都会以某种形式使用文件 I/O。例如:将传感器数据写入 CSV 文件或读取为网络服务器预配置的选项,等等。

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

从文件中读取

让我们创建一个简单的文本文件,read_file.txt,内容如下:我正在使用树莓派零学习 Python 编程,并将其保存到代码示例目录(或您选择的任何位置)。

要从文件中读取内容,我们需要使用 Python 内置的函数:open 来打开文件。让我们快速查看一个代码片段,它演示了如何打开一个文本文件来读取其内容并将其打印到屏幕上:

if __name__ =="__main__":# open text file to readfile=open('read_line.txt','r')# read from file and store it to data data =file.read()print(data)file.close()

让我们详细讨论这个代码片段:

  1. 读取文本文件内容的第一步是使用内置函数 open 打开文件。需要将文件作为参数传递,并附带一个标志 r,表示我们正在打开文件以读取内容(我们将在讨论每个读取/写入文件时讨论其他标志选项。)
  2. 在打开文件时,open 函数返回一个指针(文件对象的地址),该指针存储在 file 变量中。
file=open('read_line.txt','r')
  1. 这个文件指针用于读取文件内容并将其打印到屏幕:
 data =file.read()print(data)
  1. 在读取文件内容后,通过调用 close() 函数关闭文件。

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

 I am learning Python Programming using the Raspberry Pi Zero 

读取行

有时,有必要逐行读取文件内容。在 Python 中,有两种方法可以实现这一点:readline()readlines()

  • readline(): 如其名所示,这个内置函数允许一次读取一行。让我们用一个例子来回顾一下:
if __name__ =="__main__":# open text file to readfile=open('read_line.txt','r')# read a line from the file data =file.readline()print(data)# read another line from the file  data =file.readline()print(data)file.close()

当执行前面的代码片段(作为 read_line_from_file.py 的一部分,与本章一起提供)时,readline() 函数打开 read_line.txt 文件,并返回一个单独的行。这行被存储在变量 data 中。由于在这个程序中该函数被调用了两次,所以输出如下:

 I am learning Python Programming using the Raspberry Pi Zero. This is the second line.

每次调用 readline 函数时,都会返回一个新行,当达到文件末尾时,它返回一个空字符串。

  • readlines(): 这个函数读取文件的全部内容,按行存储,并将每一行存储到一个列表中:
if __name__ =="__main__":# open text file to readfile=open('read_lines.txt','r')# read a line from the file data =file.readlines()for line in data:print(line)file.close()

由于文件行被存储为列表,可以通过遍历列表来检索:

 data =file.readlines()for line in data:print(line)

前面的代码片段作为 read_lines_from_file.py 可以下载,与本章一起提供。

写入文件

按以下步骤顺序写入文件:

  1. 向文件写入的第一步是使用写标志打开文件:w。如果传递给参数的文件名不存在,则会创建一个新文件:
file=open('write_file.txt','w')
  1. 文件打开后,下一步是将要写入的字符串作为参数传递给 write() 函数:
file.write('I am excited to learn Python using Raspberry Pi Zero')
  1. 让我们将代码组合起来,我们将字符串写入一个文本文件,关闭它,重新打开文件,并将文件内容打印到屏幕上:
if __name__ =="__main__":# open text file to writefile=open('write_file.txt','w')# write a line from the filefile.write('I am excited to learn Python using Raspberry Pi Zero \n')file.close()file=open('write_file.txt','r') data =file.read()print(data)file.close()
  1. 前面的代码片段作为 write_to_file.py 的一部分,与本章一起提供。
  2. 当执行前面的代码片段时,输出如下所示:
 I am excited to learn Python using Raspberry Pi Zero 

向文件追加

每次使用写标志 w 打开文件时,文件的内容都会被删除,并重新打开以写入数据。有一个替代标志 a,它允许将数据追加到文件末尾。如果传递给 open 函数的文件不存在,此标志也会创建一个新文件。让我们考虑下面的代码片段,其中我们将一行追加到前一个部分中的文本文件 write_file.txt

if __name__ =="__main__":# open text file to appendfile=open('write_file.txt','a')# append a line from the filefile.write('This is a line appended to the file\n')file.close()file=open('write_file.txt','r') data =file.read()print(data)file.close()

当执行前面的代码片段(作为 append_to_file.py 的一部分,与本章一起提供)时,字符串 This is a line appended to the file 被追加到文件的文本末尾。文件的内容将包括以下内容:

 I am excited to learn Python using Raspberry Pi Zero This is a line appended to the file

seek

一旦文件被打开,用于文件输入/输出的文件指针将从文件开始移动到文件末尾。可以将指针移动到特定位置并从该位置读取数据。这在我们需要关注文件中的特定行时特别有用。让我们考虑前一个例子中的文本文件 write_file.txt。文件的内容包括:

 I am excited to learn Python using Raspberry Pi Zero This is a line appended to the file

让我们尝试使用 seek 跳过第一行,只读取第二行:

if __name__ =="__main__":# open text file to readfile=open('write_file.txt','r')# read the second line from the filefile.seek(53) data =file.read()print(data)file.close()

在前面的示例(与本章一起提供下载,文件名为seek_in_file.py)中,seek函数用于将指针移动到字节53,这是第一行的末尾。然后读取文件的内容并将其存储到变量中。当这个代码片段执行时,输出如下:

 This is a line appended to the file

因此,seek允许将文件指针移动到特定位置。

读取 n 个字节

seek函数允许将指针移动到特定位置,并从该位置读取一个字节或n个字节。让我们重新阅读write_file.txt,并尝试从句子I am excited to learn Python using Raspberry Pi Zero中读取单词excited

if __name__ =="__main__":# open text file to read and write file=open('write_file.txt','r')# set the pointer to the desired position file.seek(5) data =file.read(1)print(data)# rewind the pointerfile.seek(5) data =file.read(7)print(data)file.close()

上述代码可以按以下步骤解释:

  1. 在第一步中,使用read标志打开文件,并将文件指针设置到第五字节(使用seek)——即文本文件内容中字母e的位置。
  2. 现在,我们通过将文件作为read函数的参数传递来从文件中读取一个字节。当传递整数作为参数时,read函数返回文件中的相应字节数。当不传递参数时,它读取整个文件。如果文件为空,read函数返回空字符串:
file.seek(5) data =file.read(1)print(data)
  1. 在第二部分,我们尝试从文本文件中读取单词excited。我们将指针的位置回滚到第五字节。然后从文件中读取七个字节(单词excited的长度)。
  2. 当代码片段执行时(与本章一起提供下载,文件名为seek_to_read.py),程序应打印字母e和单词excited
file.seek(5) data =file.read(7)print(data)

r+

我们讨论了使用rw标志对文件进行读写操作。还有一个叫做r+的标志。这个标志允许对文件进行读写操作。让我们回顾一个示例,以便我们能够理解这个标志。

让我们再次回顾write_file.txt的内容:

 I am excited to learn Python using Raspberry Pi Zero This is a line appended to the file

让我们修改第二行,使其读取为:This is a line that was modified。代码示例与本章一起提供下载,文件名为seek_to_write.py

if __name__ =="__main__":# open text file to read and write file=open('write_file.txt','r+')# set the pointer to the desired position file.seek(68)file.write('that was modified \n')# rewind the pointer to the beginning of the filefile.seek(0) data =file.read()print(data)file.close()

让我们回顾这个示例是如何工作的:

  1. 在本例中,第一步是使用r+标志打开文件。这允许对文件进行读写操作。
  2. 下一步是将文件移动到第 68 字节。
  3. that was modified字符串写入文件的这个位置。字符串末尾的空格用于覆盖第二句话的原始内容。
  4. 现在,文件指针被设置到文件的开始,并读取其内容。
  5. 当执行前面的代码片段时,修改后的文件内容将按以下方式打印到屏幕上:
 I am excited to learn Python using Raspberry Pi Zero This is a line that was modified 

还有一个a+标志,它允许同时向文件末尾追加数据并读取。我们将把这个留给读者,让他们使用前面讨论的示例来找出答案。

我们讨论了在 Python 中读取和写入文件的不同示例。如果没有足够的编程经验,可能会感到不知所措。我们强烈建议通过本章提供的不同代码示例进行练习。

读者挑战

使用 a+ 标志打开 write_file.txt 文件(在不同示例中讨论过),并向文件追加一行。使用 seek 设置文件指针并打印其内容。你可以在程序中只打开文件一次。

with 关键字

到目前为止,我们讨论了可以用于以不同模式打开文件的不同的标志。我们讨论的示例遵循了一个常见的模式——打开文件,执行读写操作,然后关闭文件。使用 with 关键字与文件交互有一种优雅的方式。

如果在执行与文件交互的代码块期间出现任何错误,with 关键字将确保在退出代码块时关闭文件并清理相关资源。一如既往,让我们通过一个示例来回顾 with 关键字:

if __name__ =="__main__":withopen('write_file.txt','r+')asfile:# read the contents of the file and print to the screen print(file.read())file.write("This is a line appended to the file")#rewind the file and read its contents file.seek(0)print(file.read())# the file is automatically closed at this point print("Exited the with keyword code block")

在前面的示例(with_keyword_example)中,我们跳过了关闭文件,因为 with 关键字会在缩进代码块执行完成后关闭文件。with 关键字还会在代码块因错误而离开时关闭文件。这确保了在任何情况下都能正确清理资源。从现在开始,我们将使用 with 关键字进行文件 I/O。

configparser

让我们讨论一些在开发树莓派应用程序时特别有帮助的 Python 编程方面。其中一个工具是 Python 中的 configparserconfigparser 模块(docs.python.org/3.4/library/configparser.html)用于读取/写入应用程序的配置文件。

在软件开发中,配置文件通常用于存储常量,例如访问凭证、设备 ID 等。在树莓派的环境中,可以使用 configparser 来存储所有使用的 GPIO 引脚列表、通过 I²C 接口连接的传感器的地址等。让我们讨论三个示例,了解如何利用 configparser 模块。在第一个示例中,我们将使用 configparser 创建一个 config 文件。在第二个示例中,我们将使用 configparser 读取配置值;在第三个示例中,我们将讨论修改配置文件。

示例 1

在第一个示例中,让我们创建一个配置文件,该文件存储有关设备 ID、使用的 GPIO 引脚、传感器接口地址、调试开关和访问凭证的信息:

import configparser if __name__ =="__main__":# initialize ConfigParser  config_parser = configparser.ConfigParser()# Let's create a config file withopen('raspi.cfg','w')as config_file:#Let's add a section called ApplicationInfo  config_parser.add_section('AppInfo')#let's add config information under this section  config_parser.set('AppInfo','id','123') config_parser.set('AppInfo','gpio','2') config_parser.set('AppInfo','debug_switch','True') config_parser.set('AppInfo','sensor_address','0x62')#Let's add another section for credentials  config_parser.add_section('Credentials') config_parser.set('Credentials','token','abcxyz123') config_parser.write(config_file)print("Config File Creation Complete")

让我们详细讨论前面的代码示例(与本章一起提供下载,作为 config_parser_write.py):

  1. 第一步是导入 configparser 模块并创建 ConfigParser 类的一个实例。这个实例将被命名为 config_parser
 config_parser = configparser.ConfigParser()
  1. 现在,我们使用 with 关键字打开一个名为 raspi.cfg 的配置文件。由于文件不存在,将创建一个新的配置文件。
  2. 配置文件将包含两个节,即 AppInfoCredentials
  3. 可以使用 add_section 方法创建两个节,如下所示:
 config_parser.add_section('AppInfo') config_parser.add_section('Credentials')
  1. 每个节将包含一组不同的常量。每个常量都可以使用 set 方法添加到相关节中。传递给 set 方法的参数包括节名(参数/常量将要位于的节),参数/常量的名称及其对应值。例如:可以将 id 参数添加到 AppInfo 节,并赋予其值为 123,如下所示:
 config_parser.set('AppInfo','id','123')
  1. 最后一步是将这些配置值保存到文件中。这是通过使用 config_parser 方法的 write 实现的。一旦程序退出 with 关键字下的缩进块,文件就会被关闭:
 config_parser.write(config_file)

我们强烈建议您亲自尝试代码片段,并将这些片段作为参考。通过犯错,您可能会得到比这里讨论的更好的解决方案。

当执行前面的代码片段时,会创建一个名为 raspi.cfg 的配置文件。配置文件的内容将包括以下所示的内容:

[AppInfo]id=123 gpio =2 debug_switch =True sensor_address =0x62[Credentials] token = abcxyz123 

示例 2

让我们讨论一个示例,其中我们从前面示例中创建的配置文件中读取配置参数:

import configparser if __name__ =="__main__":# initialize ConfigParser  config_parser = configparser.ConfigParser()# Let's read the config file  config_parser.read('raspi.cfg')# Read config variables  device_id = config_parser.get('AppInfo','id') debug_switch = config_parser.get('AppInfo','debug_switch') sensor_address = config_parser.get('AppInfo','sensor_address')# execute the code if the debug switch is true if debug_switch =="True":print("The device id is "+ device_id)print("The sensor_address is "+ sensor_address)

如果配置文件以所示格式创建,ConfigParser 类应该能够解析它。实际上并不一定需要使用 Python 程序创建配置文件。我们只是想展示程序化创建配置文件,因为同时为多个设备程序化创建配置文件更容易。

前面的示例可以与本章一起下载(config_parser_read.py)。让我们讨论这个代码示例是如何工作的:

  1. 第一步是初始化一个名为 config_parserConfigParser 类的实例。
  2. 第二步是使用实例方法 read 加载和读取配置文件。
  3. 由于我们知道配置文件的结构,让我们继续读取 AppInfo 部分下可用的某些常量。可以使用 get 方法读取配置文件参数。所需的参数包括配置参数所在的节以及参数的名称。例如:配置 id 参数位于 AppInfo 节下。因此,传递给方法所需的参数包括 AppInfoid
 device_id = config_parser.get('AppInfo','id')
  1. 现在配置参数已经读入变量中,让我们在我们的程序中使用它。例如:让我们测试 debug_switch 变量(一个用于确定程序是否处于调试模式的开关)并打印从文件中检索到的其他配置参数:
if debug_switch =="True":print("The device id is "+ device_id)print("The sensor_address is "+ sensor_address)

示例 3

让我们讨论一个例子,其中我们想要修改现有的配置文件。这在需要在对固件进行更新后更新配置文件中的固件版本号的情况下特别有用。

以下代码片段可以作为config_parser_modify.py下载,与本章一起:

import configparser if __name__ =="__main__":# initialize ConfigParser  config_parser = configparser.ConfigParser()# Let's read the config file  config_parser.read('raspi.cfg')# Set firmware version  config_parser.set('AppInfo','fw_version','A3')# write the updated config to the config file withopen('raspi.cfg','w')as config_file: config_parser.write(config_file)

让我们讨论它是如何工作的:

  1. 如往常一样,第一步是初始化ConfigParser类的实例。使用read方法加载配置文件:
# initialize ConfigParser  config_parser = configparser.ConfigParser()# Let's read the config file  config_parser.read('raspi.cfg')
  1. 使用set方法(在先前的示例中讨论过)更新所需的参数:
# Set firmware version  config_parser.set('AppInfo','fw_version','A3')
  1. 使用write方法将更新的配置保存到配置文件中:
withopen('raspi.cfg','w')as config_file: config_parser.write(config_file)

向读者挑战

以示例 3 为参考,将配置参数debug_switch更新为值False。重复示例 2 并查看发生了什么。

读取/写入 CSV 文件

在本节中,我们将讨论读取/写入 CSV 文件。此模块(docs.python.org/3.4/library/csv.html)在数据记录应用中很有用。由于我们将在下一章讨论数据记录,让我们回顾一下读取/写入 CSV 文件。

向 CSV 文件中写入

让我们考虑一个场景,其中我们从不同的传感器读取数据。这些数据需要记录到一个 CSV 文件中,其中每一列对应于特定传感器的读取值。我们将讨论一个例子,其中我们在 CSV 文件的第一行记录值123456789,而第二行将包括值RedGreenBlue

  1. 向 CSV 文件写入的第一步是使用with关键字打开 CSV 文件:
withopen("csv_example.csv",'w')as csv_file:
  1. 下一步是初始化 CSV 模块的writer类的实例:
 csv_writer = csv.writer(csv_file)
  1. 现在,每一行都是通过创建一个包含需要添加到行中的所有元素的列表来添加到文件中的。例如:第一行可以按如下方式添加到列表中:
 csv_writer.writerow([123,456,789])
  1. 将所有内容整合在一起,我们得到:
import csv if __name__ =="__main__":# initialize csv writer withopen("csv_example.csv",'w')as csv_file: csv_writer = csv.writer(csv_file) csv_writer.writerow([123,456,789]) csv_writer.writerow(["Red","Green","Blue"])
  1. 当执行上述代码片段(作为csv_write.py与本章一起下载)时,在本地目录中创建了一个包含以下内容的 CSV 文件:
123,456,789 Red,Green,Blue 

从 CSV 文件中读取

让我们讨论一个例子,其中我们将读取上一节创建的 CSV 文件的正文:

  1. 读取 CSV 文件的第一步是以读取模式打开它:
withopen("csv_example.csv",'r')as csv_file:
  1. 接下来,我们初始化 CSV 模块的reader类的实例。CSV 文件的正文被加载到对象csv_reader中:
 csv_reader = csv.reader(csv_file)
  1. 现在 CSV 文件的正文已加载,可以按如下方式检索 CSV 文件的每一行:
for row in csv_reader:print(row)
  1. 将所有内容整合在一起:
import csv if __name__ =="__main__":# initialize csv writer withopen("csv_example.csv",'r')as csv_file: csv_reader = csv.reader(csv_file)for row in csv_reader:print(row)
  1. 当执行前面的代码片段(与本章一起作为csv_read.py下载)时,文件的内容按行打印,其中每一行都是一个包含逗号分隔值的列表:
['123','456','789']['Red','Green','Blue']

Python 实用工具

Python 随带了一些工具,可以用来与其他文件和操作系统本身交互。我们已经确定了我们在过去的项目中使用过的所有 Python 工具。让我们讨论不同的模块及其用途,因为我们可能会在本书的最终项目中使用它们。

os 模块

如其名所示,此模块(docs.python.org/3.1/library/os.html)允许与操作系统交互。让我们通过示例讨论其一些应用。

检查文件的存在

可以使用 os 模块检查特定目录中是否存在文件。例如:我们广泛使用了 write_file.txt 文件。在打开此文件进行读取或写入之前,我们可以检查文件的存在:

import os if __name__ =="__main__":# Check if file existsif os.path.isfile('/home/pi/Desktop/code_samples/write_file.txt'):print('The file exists!')else:print('The file does not exist!')

在前面的代码片段中,我们使用了 os.path 模块中可用的 isfile() 函数。当文件位置作为参数传递给函数时,如果文件位于该位置,则函数返回 True。在这个例子中,由于 write_file.txt 文件位于代码示例目录中,函数返回 True。因此,屏幕上打印出“文件存在”的消息:

if os.path.isfile('/home/pi/Desktop/code_samples/write_file.txt'):print('The file exists!')else:print('The file does not exist!')

检查文件夹的存在

os.path.isfile() 类似,还有一个名为 os.path.isdir() 的函数。如果特定位置存在文件夹,它将返回 True。我们一直在审查位于树莓派桌面上名为 code_samples 的文件夹中的所有代码示例。其存在可以通过以下方式确认:

# Confirm code_samples' existence if os.path.isdir('/home/pi/Desktop/code_samples'):print('The directory exists!')else:print('The directory does not exist!')

删除文件

os 模块还通过 remove() 函数允许删除文件。任何作为函数参数传递的文件都将被删除。在 文件输入/输出 部分中,我们讨论了使用文本文件 read_file.txt 从文件中读取。让我们通过将文件作为参数传递给 remove() 函数来删除该文件:

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

杀死进程

可以通过将进程 pid 传递给 kill() 函数来杀死在树莓派上运行的应用程序。在上一章中,我们讨论了作为树莓派后台进程运行的 light_scheduler 示例。为了演示杀死进程,我们将尝试杀死该进程。我们需要确定 light_scheduler 进程的进程 pid(你可以选择你作为用户启动的应用程序,不要触碰 root 进程)。进程 pid 可以通过以下命令从命令行终端检索:

 ps aux 

它会输出当前在树莓派上运行的进程(如下图所示)。light_scheduler 应用程序的进程 pid 为 1815:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_06_001.png

light_scheduler 守护进程的 PID

假设我们知道需要被杀死的应用程序的进程 pid,让我们回顾一下使用 kill() 函数来杀死进程的方法。杀死进程所需的参数包括进程 pid 和需要发送给进程以杀死应用程序的信号(signal.SIGKILL):

import os import signal if __name__ =="__main__":#kill the applicationtry: os.kill(1815, signal.SIGKILL)except OSError as error:print("OS Error "+str(error))

signal模块(docs.python.org/3/library/signal.html)包含表示可以用来停止应用程序的信号的常量。在这个代码片段中,我们使用了SIGKILL信号。尝试运行ps命令(ps aux),你会注意到light_scheduler应用程序已被终止。

监控进程

在前面的例子中,我们讨论了使用kill()函数终止应用程序。你可能已经注意到,我们使用了try/except关键字来尝试终止应用程序。我们将在下一章详细讨论这些关键字。

还可以使用kill()函数和try/except关键字来监控应用程序是否正在运行。在介绍使用try/except关键字捕获异常的概念之后,我们将讨论使用kill()函数监控进程。

本章中讨论的所有os模块的示例都可以作为os_utils.py一起下载。

glob模块

glob模块(docs.python.org/3/library/glob.html)允许识别具有特定扩展名或具有特定模式的文件。例如,可以列出文件夹中的所有 Python 文件,如下所示:

# List all filesforfilein glob.glob('*.py'):print(file)

glob()函数返回一个包含.py扩展名的文件列表。使用for循环遍历列表并打印每个文件。当执行前面的代码片段时,输出包含属于本章的所有代码示例的列表(输出已截断以供展示):

read_from_file.py config_parser_read.py append_to_file.py read_line_from_file.py config_parser_modify.py python_utils.py config_parser_write.py csv_write.py 

此模块在列出具有特定模式的文件时特别有用。例如:让我们考虑一个场景,你想要上传由实验的不同试验创建的文件。你只对以下格式的文件感兴趣:file1xx.txt,其中x代表09之间的任何数字。这些文件可以按如下方式排序和列出:

# List all files of the format 1xx.txtforfilein glob.glob('txt_files/file1[0-9][0-9].txt'):print(file)

在前面的例子中,[0-9]表示文件名可以包含09之间的任何数字。由于我们正在寻找file1xx.txt格式的文件,传递给glob()函数的搜索模式是file1[0-9][0-9].txt

当执行前面的代码片段时,输出包含指定格式的所有文本文件:

txt_files/file126.txt txt_files/file125.txt txt_files/file124.txt txt_files/file123.txt txt_files/file127.txt 

我们发现了一篇解释使用表达式对文件进行排序的文章:www.linuxjournal.com/content/bash-extended-globbing。同样的概念可以扩展到使用glob模块进行文件搜索。

对读者的挑战

glob 模块一起讨论的示例作为 glob_example.py 可以下载。在其中一个示例中,我们讨论了列出特定格式的文件。你将如何列出以下格式的文件:filexxxx.*?(其中 x 代表 09 之间的任何数字。* 代表任何文件扩展名。)

shutil 模块

shutil 模块(docs.python.org/3/library/shutil.html)通过 move()copy() 方法在文件夹之间移动和复制文件。在前一节中,我们列出了 txt_files 文件夹中的所有文本文件。让我们使用 move() 将这些文件移动到当前目录(代码正在执行的目录)中,然后在 txt_files 中再次复制这些文件,最后从当前目录中删除文本文件:

import glob import shutil import os if __name__ =="__main__":# move files to the current directoryforfilein glob.glob('txt_files/file1[0-9][0-9].txt'): shutil.move(file,'.')# make a copy of files in the folder 'txt_files' and delete themforfilein glob.glob('file1[0-9][0-9].txt'): shutil.copy(file,'txt_files') os.remove(file)

在前面的示例(作为 shutil_example.py 一起提供下载)中,通过指定源和目标作为第一个和第二个参数,文件正在从源移动到目标以及进行复制。

使用 glob 模块识别要移动(或复制)的文件。然后,使用它们对应的方法移动或复制每个文件。

子进程模块

我们在上一章中简要介绍了这个模块。subprocess 模块(docs.python.org/3.2/library/subprocess.html)允许在 Python 程序中启动另一个程序。subprocess 模块中常用的一个函数是 Popen。任何需要在程序中启动的过程都需要作为列表参数传递给 Popen 函数:

import subprocess if __name__ =="__main__": subprocess.Popen(['aplay','tone.wav'])

在前面的示例中,tone.wav(需要播放的 WAVE 文件)和需要运行的命令作为列表参数传递给函数。subprocess 模块中还有其他几个具有类似功能的命令。我们将它们留给您去探索。

sys 模块

sys 模块(docs.python.org/3/library/sys.html)允许与 Python 运行时解释器交互。sys 模块的一个功能是解析程序输入提供的命令行参数。让我们编写一个程序,读取并打印作为程序参数传递的文件的内容:

import sys if __name__ =="__main__":withopen(sys.argv[1],'r')as read_file:print(read_file.read())

尝试按照以下方式运行前面的示例:

python3 sys_example.py read_lines.txt 

前面的示例作为 sys_example.py 一起提供下载。在运行程序时传递的命令行参数列表作为 argv 列表在 sys 模块中可用。argv[0] 通常代表 Python 程序的名称,而 argv[1] 通常是将第一个参数传递给函数。

当使用 read_lines.txt 作为参数执行 sys_example.py 时,程序应打印文本文件的内容:

I am learning Python Programming using the Raspberry Pi Zero. This is the second line. Line 3. Line 4. Line 5. Line 6. Line 7.

摘要

在本章中,我们讨论了文件 I/O——读取和写入文件,以及用于读取、写入和追加到文件的不同的标志。我们讨论了将文件指针移动到文件中的不同位置以检索特定内容或覆盖文件特定位置的文件内容。我们还讨论了 Python 中的ConfigParser模块及其在存储/检索应用程序配置参数以及在 CSV 文件中读取和写入中的应用。

最后,我们讨论了不同可能在我们的项目中使用的 Python 实用工具。在我们的最终项目中,我们将广泛使用文件 I/O 和讨论过的 Python 实用工具。我们强烈建议在继续阅读本书中讨论的最终项目之前,熟悉本章中讨论的概念。

在接下来的章节中,我们将讨论将存储在 CSV 文件中的传感器数据上传到云以及记录应用程序执行过程中遇到的错误。下一章见!

第七章:请求和 Web 框架

本章的主要主题是 Python 中的请求和 Web 框架。我们将讨论使从网络检索数据(例如,获取天气更新)、上传数据到远程服务器(例如,记录传感器数据)或控制本地网络上的设备成为可能的库和框架。我们还将讨论有助于学习本章核心主题的话题。

try/except 关键字

到目前为止,我们假设理想条件,即程序执行将不会遇到错误,来审查和测试了所有我们的示例。相反,应用程序有时会因外部因素(例如无效的用户输入和差的互联网连接)或程序员造成的程序逻辑错误而失败。在这种情况下,我们希望程序报告/记录错误的性质,并在退出程序之前继续执行或清理资源。try/except关键字提供了一种在程序执行过程中捕获错误并采取补救措施的方法。由于可以在代码的关键部分捕获和记录错误,因此try/except关键字在调试应用程序时特别有用。

通过比较两个示例来了解try/except关键字。让我们构建一个简单的猜数字游戏,用户被要求猜测一个介于 0 到 9 之间的数字:

  1. 使用 Python 的random模块生成一个随机数(介于 0 到 9 之间)。如果用户猜对了生成的数字,Python 程序宣布用户为赢家并退出游戏。
  2. 如果用户输入是字母x,程序将退出游戏。
  3. 用户输入通过int()函数转换为整数。进行合理性检查以确定用户输入是否在 0 到 9 之间。
  4. 整数与一个随机数进行比较。如果它们相同,用户被宣布为赢家,程序退出游戏。

让我们观察当我们故意向这个程序提供错误输入时会发生什么(这里显示的代码片段可以与本章一起作为guessing_game.py下载):

import random if __name__ =="__main__":whileTrue:# generate a random number between 0 and 9 rand_num = random.randrange(0,10)# prompt the user for a number value =input("Enter a number between 0 and 9: ")if value =='x':print("Thanks for playing! Bye!")break input_value =int(value)if input_value <0or input_value >9:print("Input invalid. Enter a number between 0 and 9.")if input_value == rand_num:print("Your guess is correct! You win!")breakelse:print("Nope! The random value was %s"% rand_num)

让我们执行前面的代码片段,并将输入hello提供给程序:

 Enter a number between 0and9: hello Traceback (most recent call last): File "guessing_game.py", line 12,in<module> input_value =int(value) ValueError: invalid literal forint()with base 10:'hello'

在前面的例子中,程序在尝试将用户输入hello转换为整数时失败。程序执行以异常结束。异常突出了错误发生的位置。在这种情况下,它发生在第 10 行:

 File "guessing_game.py", line 12,in<module> input_value =int(value)

错误的性质也在异常中得到了强调。在这个例子中,最后一行表明抛出的异常是ValueError

 ValueError: invalid literal forint()with base 10:'hello'

让我们讨论相同的示例(可以与本章一起作为try_and_except.py下载),它使用了try/except关键字。在捕获此异常并将其打印到屏幕后,可以继续玩游戏。我们有以下代码:

import random if __name__ =="__main__":whileTrue:# generate a random number between 0 and 9 rand_num = random.randrange(0,10)# prompt the user for a number value =input("Enter a number between 0 and 9: ")if value =='x':print("Thanks for playing! Bye!")try: input_value =int(value)except ValueError as error:print("The value is invalid %s"% error)continueif input_value <0or input_value >9:print("Input invalid. Enter a number between 0 and 9.")continueif input_value == rand_num:print("Your guess is correct! You win!")breakelse:print("Nope! The random value was %s"% rand_num)

让我们讨论一下使用try/except关键字时相同的示例是如何工作的:

  1. 从前面的示例中,我们知道当用户提供错误的输入(例如,0 到 9 之间的字母而不是数字)时,异常会在第 10 行(用户输入转换为整数的地方)发生,错误的性质被命名为ValueError
  2. 可以通过将这段代码包裹在try...except块中来避免程序执行的中断:
try: input_value =int(value)except ValueError as error:print("The value is invalid %s"% error)
  1. 在接收到用户输入后,程序尝试在try块中将用户输入转换为整数。
  2. 如果发生了ValueErrorerror会被except块捕获,并且实际错误信息会与以下信息一起打印到屏幕上:
except ValueError as error:print("The value is invalid %s"% error)
  1. 尝试执行代码示例,并尝试提供一个无效的输入。你会注意到程序会打印出错误信息(包括错误的性质),然后回到游戏循环的顶部并继续寻找有效的用户输入:
 Enter a number between 0and9:3 Nope! The random value was 5 Enter a number between 0and9: hello The value is invalid invalid literal forint()with base 10:'hello' Enter a number between 0and10:4 Nope! The random value was 6

try...except块伴随着大量的处理能力成本。因此,保持try...except块尽可能短是很重要的。因为我们知道错误发生在我们尝试将用户输入转换为整数的那一行,所以我们将其包裹在try...except块中以捕获错误。

因此,try/except关键字用于防止程序执行过程中由于错误导致的任何异常行为。它允许记录错误并采取补救措施。类似于try...except块,也存在try...except...elsetry...except...else代码块。让我们通过几个示例快速回顾这些选项。

try…except…else

try...except...else块在当我们只想在没有引发异常的情况下执行特定代码块时特别有用。为了演示这个概念,让我们使用这个块重写猜数字游戏示例:

try: input_value =int(value)except ValueError as error:print("The value is invalid %s"% error)else:if input_value <0or input_value >9:print("Input invalid. Enter a number between 0 and 9.")elif input_value == rand_num:print("Your guess is correct! You win!")breakelse:print("Nope! The random value was %s"% rand_num)

修改后的猜数字游戏示例,它使用了try...except...else块,可以与本章一起下载,文件名为try_except_else.py。在这个示例中,程序只有在接收到有效的用户输入时才会将用户输入与随机数进行比较。否则,它会跳过else块,并回到循环的顶部以接受下一个用户输入。因此,当我们在try块中没有因为代码而引发异常时,会使用try...except...else

try…except…else…finally

如其名所示,finally块用于在离开try块时执行一段代码。即使在抛出异常之后,这段代码也会被执行。这在我们需要在进入下一阶段之前清理资源并释放内存的情况下非常有用。

让我们通过我们的猜谜游戏来演示finally块的功能。为了理解finally关键字的工作原理,让我们使用一个名为count的计数变量,它在finally块中递增,以及另一个名为valid_count的计数变量,它在else块中递增。以下是我们的代码:

count =0 valid_count =0whileTrue:# generate a random number between 0 and 9 rand_num = random.randrange(0,10)# prompt the user for a number value =input("Enter a number between 0 and 9: ")if value =='x':print("Thanks for playing! Bye!")try: input_value =int(value)except ValueError as error:print("The value is invalid %s"% error)else:if input_value <0or input_value >9:print("Input invalid. Enter a number between 0 and 9.")continue valid_count +=1if input_value == rand_num:print("Your guess is correct! You win!")breakelse:print("Nope! The random value was %s"% rand_num)finally: count +=1print("You won the game in %d attempts "\ "and %d inputs were valid"%(count, valid_count))

上述代码片段来自try_except_else_finally.py代码示例(与本章一起提供下载)。尝试执行代码示例并玩游戏。你会注意到赢得游戏所需的尝试总数以及有效输入的数量:

 Enter a number between 0and9: g The value is invalid invalid literal forint()with base 10:'g' Enter a number between 0and9:3 Your guess is correct! You win! You won the game in9 attempts and8 inputs were valid 

这演示了try-except-else-finally块的工作原理。当关键的代码块(在try关键字下)成功执行时,任何在else关键字下的代码都会执行,而finally关键字下的代码块在退出try...except块时执行(在退出代码块时清理资源很有用)。

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

连接到互联网 - 网络请求

现在我们已经讨论了try/except关键字,让我们利用它来构建一个简单的应用程序,该程序可以连接到互联网。我们将编写一个简单的应用程序,从互联网获取当前时间。我们将使用 Python 的requests库(requests.readthedocs.io/en/master/#)。

requests模块允许连接到网络并检索信息。为了做到这一点,我们需要使用requests模块中的get()方法来发送请求:

import requests response = requests.get('http://nist.time.gov/actualtime.cgi')

在前面的代码片段中,我们将一个 URL 作为参数传递给get()方法。在这种情况下,它是返回 Unix 格式当前时间的 URL(en.wikipedia.org/wiki/Unix_time)。

让我们使用try/except关键字来请求获取当前时间:

#!/usr/bin/python3import requests if __name__ =="__main__":# Source for link: http://stackoverflow.com/a/30635751/822170try: response = requests.get('http://nist.time.gov/actualtime.cgi')print(response.text)except requests.exceptions.ConnectionError as error:print("Something went wrong. Try again")

在前面的示例(与本章一起提供下载,文件名为internet_access.py)中,请求是在try块中发出的,并且响应(由response.text返回)被打印到屏幕上。

如果在检索当前时间时发生错误,将引发ConnectionErrorrequests.readthedocs.io/en/master/user/quickstart/#errors-and-exceptions)。这个错误可能是由缺少互联网连接或错误的 URL 引起的。这个错误被except块捕获。尝试运行示例,它应该从time.gov返回当前时间:

<timestamp time="1474421525322329" delay="0"/>

requests库的应用 - 获取天气信息

让我们使用requests模块来检索旧金山市的天气信息。我们将使用OpenWeatherMap API (openweathermap.org)来检索天气信息:

  1. 为了使用 API,注册一个 API 账户并获取一个 API 密钥(免费):

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_07_001.png

一个来自 openweathermap.org 的 API 密钥

  1. 根据 API 文档(openweathermap.org/current),可以使用http://api.openweathermap.org/data/2.5/weather?zip=SanFrancisco&appid=API_KEY&units=imperial作为 URL 来检索一个城市的天气信息。
  2. API_KEY替换为您账户中的密钥,并在浏览器中用它来检索当前的天气信息。您应该能够以以下格式检索天气信息:
{"coord":{"lon":-122.42,"lat":37.77},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"base":"stations","main":{"temp":71.82,"pressure":1011,"humidity":50,"temp_min":68,"temp_max":75.99},"wind":{"speed":13.04,"deg":291},"clouds":{"all":0},"dt":1474505391,"sys":{"type":3,"id":9966,"message":0.0143,"country":"US","sunrise":1474552682,"sunset":1474596336},"id":5391959,"name":"San Francisco","cod":200}

天气信息(如前所述)以 JSON 格式返回。JavaScript 对象表示法JSON)是一种广泛用于在网络上交换数据的数据格式。JSON 格式的优点是它是一种可读的格式,许多流行的编程语言都支持以 JSON 格式封装数据。如前所述的代码片段所示,JSON 格式允许以可读的名称/值对的形式交换信息。

让我们回顾一下使用requests模块检索天气并解析 JSON 数据的过程:

  1. 将前面例子(internet_access.py)中的 URL 替换为本文中讨论的 URL。这将返回 JSON 格式的天气信息。
  2. 请求模块提供了一个解析 JSON 数据的方法。响应可以按照以下方式解析:
 response = requests.get(URL) json_data = response.json()
  1. json()函数解析来自 OpenWeatherMap API 的响应,并返回一个包含不同天气参数及其值的字典(json_data)。
  2. 由于我们知道 API 文档中的响应格式,我们可以按照以下方式从解析的响应中检索当前温度:
print(json_data['main']['temp'])
  1. 将所有内容整合,我们得到以下内容:
#!/usr/bin/python3import requests # generate your own API key APP_ID ='5d6f02fd4472611a20f4ce602010ee0c' ZIP =94103 URL ="""http://api.openweathermap.org/data/2.5/weather?zip={} &appid={}&units=imperial""".format(ZIP, APP_ID)if __name__ =="__main__":# API Documentation: http://openweathermap.org/ current#current_JSONtry:# encode data payload and post it response = requests.get(URL) json_data = response.json()print("Temperature is %s degrees Fahrenheit"% json_data['main']['temp'])except requests.exceptions.ConnectionError as error:print("The error is %s"% error)

前面的例子可以作为本章的附件下载,名为weather_example.py。该示例应显示以下当前温度:

 Temperature is68.79 degrees Fahrenheit 

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

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

在此示例中,我们将使用requests将这些事件发布到 Slack。每当发生类似猫门开启的传感器事件时,我们都会给自己发送一条直接消息。我们需要一个 URL 来将这些传感器事件发布到 Slack。让我们回顾一下如何生成一个 URL 以发布传感器事件到 Slack:

  1. 生成 URL 的第一步是创建一个incoming webhook。Webhook 是一种请求类型,可以将作为有效载荷的消息发布到像 Slack 这样的应用程序。
  2. 如果你是名为TeamX的 Slack 团队的一员,请在浏览器中打开您的团队应用目录,即teamx.slack.com/apps

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_07_002.png

启动您的团队应用目录

  1. 在您的应用目录中搜索incoming webhooks并选择第一个选项,即 Incoming WebHooks(如下面的截图所示):

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_07_003.png

选择“incoming webhooks”

  1. 点击添加配置:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_07_004.png

添加配置

  1. 当发生事件时,让我们给自己发送一条私密消息。选择“私下发送给你”作为选项,并通过点击添加 Incoming WebHooks 集成来创建 webhook:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_07_005.png

选择“私下发送给你”

  1. 我们已经生成了一个用于发送关于传感器事件的直接消息的 URL(URL 部分被隐藏):

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_07_006.png

生成的 URL

  1. 现在,我们可以使用之前提到的 URL 直接在 Slack 上给自己发送消息。传感器事件可以作为 JSON 有效载荷发布到 Slack。让我们回顾一下如何将传感器事件发布到 Slack。
  2. 例如,让我们考虑在猫门打开时发布一条消息。第一步是为消息准备 JSON 有效载荷。根据 Slack API 文档(api.slack.com/custom-integrations),消息有效载荷需要以下格式:
 payload ={"text":"The cat door was just opened!"}
  1. 为了发布此事件,我们将使用requests模块中的post()方法。在发布时,数据有效载荷需要以 JSON 格式编码:
 response = requests.post(URL, json.dumps(payload))
  1. 将所有这些放在一起,我们得到这个:
#!/usr/bin/python3import requests import json # generate your own URL URL ='https://hooks.slack.com/services/'if __name__ =="__main__": payload ={"text":"The cat door was just opened!"}try:# encode data payload and post it response = requests.post(URL, json.dumps(payload))print(response.text)except requests.exceptions.ConnectionError as error:print("The error is %s"% error)
  1. 在发布消息时,请求返回ok作为响应。这表示发布成功。
  2. 生成您自己的 URL 并执行前面的示例(作为本章的附件slack_post.py提供下载)。您将在 Slack 上收到一条直接消息:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_07_007.png

Slack 上的直接消息

现在,尝试将传感器连接到 Raspberry Pi Zero(在前面章节中讨论过)并将传感器事件发布到 Slack。

还可以将传感器事件发布到 Twitter,并让您的 Raspberry Pi Zero 检查新电子邮件等。请查看本书的网站以获取更多示例。

Flask Web 框架

在我们的最后一节中,我们将讨论 Python 中的 Web 框架。我们将讨论 Flask 框架 (flask.pocoo.org/)。基于 Python 的框架允许使用 Raspberry Pi Zero 将传感器连接到网络。这使得可以在网络内的任何地方控制电器和读取传感器的数据。让我们开始吧!

安装 Flask

第一步是安装 Flask 框架。可以按照以下步骤进行:

 sudo pip3 install flask 

构建我们的第一个示例

Flask 框架文档解释了如何构建第一个示例。按照以下方式修改文档中的示例:

#!/usr/bin/python3from flask import Flask app = Flask(__name__)@app.route("/")defhello():return"Hello World!"if __name__ =="__main__": app.run('0.0.0.0')

启动此示例(与本章一起提供下载,名为 flask_example.py),它应该在 Raspberry Pi Zero 上启动一个对网络可见的服务器。在另一台计算机上,启动浏览器并输入 Raspberry Pi Zero 的 IP 地址以及端口号 5000 作为后缀(如下面的快照所示)。它应该带您到显示消息 Hello World! 的服务器索引页面:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_07_008.png

基于 Flask 框架的 Raspberry Pi Zero 上的 Web 服务器

您可以使用命令行终端上的 ifconfig 命令找到您的 Raspberry Pi Zero 的 IP 地址。

使用 Flask 框架控制电器

让我们尝试使用 Flask 框架在家中的电器上打开/关闭。在之前的章节中,我们使用了 PowerSwitch Tail II 通过 Raspberry Pi Zero 控制台灯。让我们尝试使用 Flask 框架来控制它。按照以下图示连接 PowerSwitch Tail:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_07_009.png

使用 Flask 框架控制台灯

根据 Flask 框架文档,可以将 URL 路由到特定的函数。例如,可以使用 route()/lamp/<control> 绑定到 control() 函数:

@app.route("/lamp/<control>")defcontrol(control):if control =="on": lights.on()elif control =="off": lights.off()return"Table lamp is now %s"% control 

在前面的代码片段中,<control> 是一个可以作为参数传递给绑定函数的变量。这使得我们能够控制灯的开关。例如,<IP 地址>:5000/lamp/on 会打开灯,反之亦然。将所有这些放在一起,我们得到如下:

#!/usr/bin/python3 from flask import Flask from gpiozero import OutputDevice app = Flask(__name__) lights = OutputDevice(2)@app.route("/lamp/<control>")defcontrol(control):if control =="on": lights.on()elif control =="off": lights.off()return"Table lamp is now %s"% control if __name__ =="__main__": app.run('0.0.0.0')

前面的示例作为 appliance_control.py 与本章一起提供下载。启动基于 Flask 的 Web 服务器,并在另一台计算机上打开一个 Web 服务器。为了打开灯,输入 <Raspberry Pi Zero 的 IP 地址>:5000/lamp/on 作为 URL:

这应该会打开灯:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_07_010.png

因此,我们已经构建了一个简单的框架,该框架能够控制网络内的电器。可以在 HTML 页面中添加按钮并将它们路由到特定的 URL 以执行特定功能。Python 中还有其他几个框架可以用来开发 Web 应用程序。我们只是向您介绍了 Python 可能实现的不同应用。我们建议您查看本书的网站以获取更多示例,例如使用 Flask 框架控制万圣节装饰和其他节日装饰。

摘要

在本章中,我们讨论了 Python 中的try/except关键字。我们还讨论了开发从互联网检索信息的应用程序以及将传感器事件发布到互联网的应用程序。我们还讨论了 Python 的 Flask Web 框架,并演示了在网络内控制电器。在下一章中,我们将讨论 Python 的一些高级主题。

第八章:使用 Python 可以开发的一些酷炫事物

在本章中,我们将讨论 Python 的一些高级主题。我们还将讨论某些独特主题(如图像处理),这些主题可以让您开始使用 Python 进行应用程序开发。

使用 Raspberry Pi Zero 进行图像处理

Raspberry Pi Zero 是一块由 1 GHz 处理器供电的廉价硬件。虽然它不足以运行某些高级图像处理操作,但它可以帮助您在 25 美元的预算下学习基础知识(Raspberry Pi Zero 和摄像头的成本)。

我们建议使用 16 GB 的卡(或更高)来安装本节讨论的图像处理工具集。

例如,您可以使用 Raspberry Pi Zero 来追踪您后院的一只鸟。在本章中,我们将讨论在 Raspberry Pi Zero 上开始图像处理的不同方法。

为了测试本节中使用的摄像头示例,需要一台 Raspberry Pi Zero v1.3 或更高版本。检查您的 Raspberry Pi Zero 的背面以验证板子版本:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_08_001.png

识别您的 Raspberry Pi Zero 版本

OpenCV

OpenCV 是一个开源工具箱,由为图像处理开发的多个软件工具组成。OpenCV 是一个跨平台工具箱,它支持不同的操作系统。由于 OpenCV 在开源许可下可用,全球的研究人员通过开发工具和技术对其增长做出了贡献。这使得开发应用程序相对容易。OpenCV 的应用包括人脸识别和车牌识别。

由于其有限的处理能力,完成框架的安装可能需要几个小时。在我们的终端上,大约花费了 10 个小时。

我们遵循了从 www.pyimagesearch.com/2015/10/26/how-to-install-opencv-3-on-raspbian-jessie/ 安装 OpenCV 到 Raspberry Pi Zero 的说明。我们特别遵循了使用 Python 3.x 绑定安装 OpenCV 的说明,并验证了安装过程。我们大约花费了 10 个小时来完成在 Raspberry Pi Zero 上安装 OpenCV。我们出于不重复造轮子的考虑,没有重复这些说明。

安装验证

让我们确保 OpenCV 的安装及其 Python 绑定正常工作。启动命令行终端并确保您已通过执行 workon cv 命令启动了 cv 虚拟环境(您可以通过检查确认您是否处于 cv 虚拟环境):

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_08_002.png

验证您是否处于 cv 虚拟环境

现在,让我们确保我们的安装正确无误。从命令行启动 Python 解释器并尝试导入 cv2 模块:

>>>import cv2 >>> cv2.__version__ '3.0.0'

这证明了 OpenCV 已安装在 Raspberry Pi Zero 上。让我们写一个涉及 OpenCV 的“hello world”示例。在这个例子中,我们将打开一个图像(这可以是 Raspberry Pi Zero 桌面上任何颜色的图像)并在将其转换为灰度后显示它。我们将使用以下文档来编写我们的第一个示例:docs.opencv.org/3.0-beta/doc/py_tutorials/py_gui/py_image_display/py_image_display.html

根据文档,我们需要使用imread()函数读取图像文件的内容。我们还需要指定我们想要读取图像的格式。在这种情况下,我们将以灰度格式读取图像。这是通过将cv2.IMREAD_GRAYSCALE作为函数的第二个参数传递来指定的:

import cv2 img = cv2.imread('/home/pi/screenshot.jpg',cv2.IMREAD_GRAYSCALE)

现在图像已经以灰度格式加载并保存到img变量中,我们需要在新窗口中显示它。这可以通过imshow()函数实现。根据文档,我们可以通过指定窗口名称作为第一个参数和图像作为第二个参数来显示图像:

cv2.imshow('image',img)

在这种情况下,我们将打开一个名为image的窗口,并显示我们在上一步中加载的img的内容。我们将显示图像,直到接收到按键。这是通过使用cv2.waitKey()函数实现的。根据文档,waitkey()函数监听键盘事件:

cv2.waitKey(0)

0参数表示我们将无限期地等待按键。根据文档,当以毫秒为单位的持续时间作为参数传递时,waitkey()函数会监听指定时间段的按键。当按下任何键时,窗口将通过destroyAllWindows()函数关闭:

cv2.destroyAllWindows()

将所有这些放在一起,我们得到以下内容:

import cv2 img = cv2.imread('/home/pi/screenshot.jpg',cv2.IMREAD_GRAYSCALE) cv2.imshow('image',img) cv2.waitKey(0) cv2.destroyAllWindows()

上述代码示例作为opencv_test.py与本章一起提供下载。一旦你完成 OpenCV 库的安装,请尝试按照此示例加载图像。它应该以灰度形式加载图像,如下所示:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_08_003.jpg

以灰度加载的 Raspberry Pi 桌面

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

对读者的挑战

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

将相机安装到 Raspberry Zero

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

名称来源
Raspberry Pi Zero 相机适配器thepihut.com/products/raspberry-pi-zero-camera-adapter
树莓派相机thepihut.com/products/raspberry-pi-camera-module

执行以下步骤将相机安装到树莓派 Zero 上:

  1. 第一步是将相机连接到树莓派 Zero。相机适配器可以按照以下图示安装。抬起连接器卡扣,滑动相机适配器并轻轻按下连接器:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_08_004.jpg

  1. 我们需要在树莓派 Zero 上启用相机接口。在你的桌面上,转到“首选项”并启动树莓派配置。在树莓派配置的“接口”选项卡下启用相机,并保存配置:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_08_005.png

启用相机接口

  1. 让我们通过在命令行终端运行以下命令来测试相机:
 raspistill -o /home/pi/Desktop/test.jpg 
  1. 它应该会拍照并将照片保存到你的树莓派桌面上。验证相机是否正常工作。如果你无法使相机工作,我们建议查看树莓派基金会发布的故障排除指南:www.raspberrypi.org/documentation/raspbian/applications/camera.md

相机线缆有点难以操控,在尝试拍照时可能会造成困难。我们建议使用相机支架。我们发现这个支架很有用(如图所示),可以在a.co/hQolR7O找到:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_08_006.png

为你的树莓派相机使用挂载

让我们尝试使用树莓派相机,并配合 OpenCV 库一起使用:

  1. 我们将使用相机拍照,并使用 OpenCV 框架显示它。为了在 Python 中访问相机,我们需要picamera包。可以按照以下方式安装:
 pip3 install picamera 
  1. 让我们通过一个简单的程序来确保包按预期工作。picamera包的文档可在picamera.readthedocs.io/en/release-1.12/api_camera.html找到。
  2. 第一步是初始化PiCamera类。然后是沿垂直轴翻转图像。这仅因为相机在支架上安装是颠倒的。在其他支架上可能不需要这样做:
with PiCamera()as camera: camera.vflip =True
  1. 在拍照之前,我们可以使用start_preview()方法预览将要捕获的图片:
 camera.start_preview()
  1. 在拍照之前,让我们预览 10 秒钟。我们可以使用capture()方法拍照:
 sleep(10) camera.capture("/home/pi/Desktop/desktop_shot.jpg") camera.stop_preview()
  1. capture()方法需要一个文件位置作为参数(如前文所示)。完成后,我们可以使用stop_preview()关闭相机预览。
  2. 将所有这些放在一起,我们得到以下内容:
from picamera import PiCamera from time import sleep if __name__ =="__main__":with PiCamera()as camera: camera.vflip =True camera.start_preview() sleep(10) camera.capture("/home/pi/Desktop/desktop_shot.jpg") camera.stop_preview()

上述代码示例作为本章的一部分提供下载,名为picamera_test.py。以下图显示了使用相机拍摄的照片快照:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_08_007.png

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

  1. 让我们将这个示例与上一个示例结合起来——将此图像转换为灰度并显示,直到按下键。请确保您仍在 cv 虚拟环境工作区中。
  2. 让我们按以下方式将捕获的图像转换为灰度:
 img = cv2.imread("/home/pi/Desktop/desktop_shot.jpg", cv2.IMREAD_GRAYSCALE)

以下是在捕获时转换的图像:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_08_008.png

捕获时转换为灰度的图像

  1. 现在我们可以按以下方式显示灰度图像:
 cv2.imshow("image", img) cv2.waitKey(0) cv2.destroyAllWindows()

修改后的示例作为 picamera_opencvtest.py 可以下载。

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

语音识别

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

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

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

让我们回顾使用 wit.ai 在 Python 中构建语音识别应用程序的过程(其文档在此处提供github.com/wit-ai/pywit)。为了执行语音识别和识别语音命令,我们需要一个麦克风。然而,我们将演示使用现成的音频样本。我们将使用由研究出版物提供的音频样本(可在ecs.utdallas.edu/loizou/speech/noizeus/clean.zip获取)。

wit.ai API 许可证声明该工具可免费使用,但上传到他们服务器的音频将用于调整他们的语音转录工具。

我们现在将尝试转录 sp02.wav 音频样本,执行以下步骤:

  1. 第一步是在 wit.ai 上注册账户。注意以下截图中的 API:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_08_009.png

  1. 第一步是安装 requests 库。它可以按以下方式安装:
 pip3 install requests 
  1. 根据wit.ai文档,我们需要在我们的请求中添加自定义头信息,包括 API 密钥(将$TOKEN替换为您的账户中的令牌)。我们还需要在头信息中指定文件格式。在这种情况下,是一个.wav文件,采样频率为 8000 Hz:
import requests if __name__ =="__main__": url ='https://api.wit.ai/speech?v=20161002' headers ={"Authorization":"Bearer $TOKEN","Content-Type":"audio/wav"}
  1. 为了转录音频样本,我们需要在请求体中附加音频样本:
 files =open('sp02.wav','rb') response = requests.post(url, headers=headers, data=files)print(response.status_code)print(response.text)
  1. 将所有这些放在一起,我们得到以下内容:
#!/usr/bin/python3 import requests if __name__ =="__main__": url ='https://api.wit.ai/speech?v=20161002' headers ={"Authorization":"Bearer $TOKEN","Content-Type":"audio/wav"} files =open('sp02.wav','rb') response = requests.post(url, headers=headers, data=files)print(response.status_code)print(response.text)

之前的代码示例可以作为本章的wit_ai.py一起下载。尝试执行之前的代码示例,它应该能够转录音频样本:sp02.wav。我们有以下代码:

200{"msg_id":"fae9cc3a-f7ed-4831-87ba-6a08e95f515b","_text":"he knew the the great young actress","outcomes":[{"_text":"he knew the the great young actress","confidence":0.678,"intent":"DataQuery","entities":{"value":[{"confidence":0.7145905790744499,"type":"value","value":"he","suggested": true },{"confidence":0.5699616515542044,"type":"value","value":"the","suggested": true },{"confidence":0.5981701138805214,"type":"value","value":"great","suggested": true },{"confidence":0.8999612482250062,"type":"value","value":"actress","suggested": true }]}}],"WARNING":"DEPRECATED"}

音频样本包含以下录音:他深知那位年轻女演员的技艺。根据wit.ai API,转录结果是他深知那位年轻女演员。单词错误率为 22%(en.wikipedia.org/wiki/Word_error_rate)。

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

自动化路由任务

在本节中,我们将讨论在 Python 中自动化路由任务。我们选取了两个示例,以展示 Raspberry Pi Zero 作为个人助理的能力。第一个示例涉及改善通勤,而第二个示例则有助于提高词汇量。让我们开始吧。

改善日常通勤

许多城市和公共交通系统已经开始与公众共享数据,以实现透明度和提高运营效率。交通系统已经开始通过 API 向公众共享警告和交通信息。这使得任何人都可以开发移动应用程序,为通勤者提供信息。有时,这有助于缓解公共交通系统中的拥堵。

这个例子是受一个追踪旧金山共享单车站自行车可用性的朋友所启发。在旧金山湾区,有一个自行车共享项目,使通勤者可以从交通中心租用自行车到工作地点。在像旧金山这样拥挤的城市,特定站点的自行车可用性会根据一天中的时间而波动。

这个朋友想要根据最近共享单车站的自行车可用性来规划他的日程。如果站内剩余的自行车非常少,这位朋友更愿意早点离开去租一辆自行车。他正在寻找一个简单的技巧,当自行车数量低于某个阈值时,能够向他的手机发送通知。旧金山的共享单车项目在feeds.bayareabikeshare.com/stations/stations.json提供了这些数据。

让我们回顾一下构建一个简单的示例,该示例将允许向移动设备发送推送通知。为了发送移动推送通知,我们将使用If This Then ThatIFTTT)——一种允许将您的项目连接到第三方服务的服务。

在这个例子中,我们将解析 JSON 格式的数据,检查特定站点的可用自行车数量,如果它低于指定的阈值,就会在您的移动设备上触发通知。

让我们开始吧:

  1. 第一步是从自行车共享服务中检索自行车可用性。这些数据以 JSON 格式提供,可在feeds.bayareabikeshare.com/stations/stations.json找到。数据包括整个网络中的自行车可用性。
  2. 每个站点的自行车可用性都提供了参数,例如站点 ID、站点名称、地址、可用自行车数量等。
  3. 在这个例子中,我们将检索旧金山Townsend at 7th站点的自行车可用性。站点 ID 是65(在浏览器中打开前面提到的链接以找到id)。让我们编写一些 Python 代码来检索自行车可用性数据并解析这些信息:
import requests BIKE_URL = http://feeds.bayareabikeshare.com/stations /stations.json # fetch the bike share information  response = requests.get(BIKE_URL) parsed_data = response.json()

第一步是使用GET请求(通过requests模块)获取数据。requests模块提供了一个内置的 JSON 解码器。可以通过调用json()函数来解析 JSON 数据。

  1. 现在,我们可以遍历站点字典,通过以下步骤找到Townsend at 7th的自行车可用性:
  2. 在检索到的数据中,每个站点的数据都附有一个 ID。所讨论的站点 ID 是65(在浏览器中打开前面提供的早期数据馈送 URL 以了解数据格式;以下截图显示了数据片段):

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_08_010.png

使用浏览器获取的自行车共享数据片段

  1. 我们需要遍历值并确定站点id是否与Townsend at 7th匹配:
 station_list = parsed_data['stationBeanList']for station in station_list:if station['id']==65and station['availableBikes']<2:print("The available bikes is %d"% station ['availableBikes'])
    1. 如果站点上的自行车可用量少于2辆,我们将向我们的移动设备推送移动通知。
  1. 为了接收移动通知,您需要安装IF by IFTTT应用程序(适用于苹果和安卓设备)。
  2. 我们还需要在 IFTTT 上设置一个食谱来触发移动通知。在ifttt.com/注册一个账户。

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

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

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_08_011.png

在 IFTTT 上开始创建一个菜谱

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_08_012.png

点击此处

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_08_013.png

选择 Maker Webhooks 通道

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_08_014.png

选择接收 Web 请求

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_08_015.png

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

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_08_016.png

点击那个

  1. 选择通知:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_08_017.png

选择通知

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_08_018.png

设置设备通知

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_08_019.png

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_08_020.png

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_08_021.png

创建一个菜谱

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

触发器可以在这里找到:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_08_022.png

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_08_023.png

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

  1. 在进行请求(如 IFTTT 文档中所述)时,如果我们包括请求的 JSON 体中的自行车数量(使用Value1),它可以在移动通知中显示。
  2. 让我们回顾一下 Python 示例,当自行车数量低于某个阈值时,进行 Web 请求。将IFTTT URL 和您的 IFTTT 访问密钥(从您的 IFTTT 账户中获取)保存到您的代码中如下所示:
 IFTTT_URL = "https://maker.ifttt.com/trigger/mobile_notify/with/key/$KEY" 
  1. 当自行车数量低于某个阈值时,我们需要在 JSON 体中编码自行车信息并发送一个POST请求:
for station in station_list:if station['id']==65and station['availableBikes']<3:print("The available bikes is %d"% station['availableBikes']) payload ={"value1": station['availableBikes']} response = requests.post(IFTTT_URL, json=payload)if response.status_code ==200:print("Notification successfully triggered")
  1. 在前面的代码片段中,如果少于三辆自行车,将使用requests模块发送一个POST请求。可用自行车的数量使用键value1进行编码:
 payload ={"value1": station['availableBikes']}
  1. 将所有这些放在一起,我们得到这个:
#!/usr/bin/python3 import requests import datetime BIKE_URL = "http://feeds.bayareabikeshare.com/stations/ stations.json" # find your key from ifttt  IFTTT_URL = "https://maker.ifttt.com/trigger/mobile_notify/with/key/$KEY" if __name__ =="__main__":# fetch the bike share information  response = requests.get(BIKE_URL) parsed_data = response.json() station_list = parsed_data['stationBeanList']for station in station_list:if station['id']==65and station['availableBikes']<10:print("The available bikes is %d"% station ['availableBikes']) payload ={"value1": station['availableBikes']} response = requests.post(IFTTT_URL, json=payload)if response.status_code ==200:print("Notification successfully triggered")

上述代码示例作为bike_share.py与本章一起提供下载。在设置 IFTTT 配方后尝试执行它。如有必要,调整可用自行车的阈值。你应该会在你的设备上收到移动通知:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_08_024.png

移动设备上的通知

对读者的挑战

在这个例子中,自行车信息被检索和解析,并在必要时触发通知。你会如何修改这个代码示例以确保它在一天中的特定时间执行?(提示:使用datetime模块)。

你会如何构建一个作为视觉辅助的桌面显示屏?

项目挑战

尝试找出你所在地区的交通系统是否向用户提供此类数据。你将如何利用这些数据帮助通勤者节省时间?例如,你将如何使用这些数据向你的朋友/同事提供交通系统建议?

书完成后,我们将发布一个使用旧金山湾区快速交通(BART)的数据的类似示例。

提高你的词汇量

使用 Python 提高你的词汇量是可能的!想象一下设置一个大型显示屏,安装在显眼的位置,并每天更新。我们将使用wordnik API(在www.wordnik.com/signup注册 API 密钥):

  1. 第一步是安装wordnik API 客户端的python3版本:
 git clone https://github.com/wordnik/wordnik-python3.git cd wordnik-python3/ sudo python3 setup.py install 

对 wordnik API 的使用有限制。请参阅 API 文档以获取更多详细信息。

  1. 让我们回顾一下使用wordnik Python 客户端编写的第一个示例。为了获取每日一词,我们需要初始化WordsApi类。根据 API 文档,可以这样做:
# sign up for an API key  API_KEY ='API_KEY' apiUrl ='http://api.wordnik.com/v4' client = swagger.ApiClient(API_KEY, apiUrl) wordsApi = WordsApi.WordsApi(client)
  1. 现在已经初始化了WordsApi类,让我们继续获取每日一词:
 example = wordsApi.getWordOfTheDay()
  1. 这返回一个WordOfTheDay对象。根据wordnik Python 客户端文档,该对象包含不同的参数,包括单词、同义词、来源、用法等。每日一词及其同义词可以按以下方式打印:
print("The word of the day is %s"% example.word)print("The definition is %s"%example.definitions[0].text)
  1. 将所有内容整合在一起,我们得到如下:
#!/usr/bin/python3 from wordnik import*# sign up for an API key  API_KEY ='API_KEY' apiUrl ='http://api.wordnik.com/v4'if __name__ =="__main__": client = swagger.ApiClient(API_KEY, apiUrl) wordsApi = WordsApi.WordsApi(client) example = wordsApi.getWordOfTheDay()print("The word of the day is %s"% example.word)print("The definition is %s"%example.definitions[0].text)

上述代码片段作为wordOfTheDay.py与本章一起提供下载。注册 API 密钥,你应该能够检索每日一词:

 The word of the day is transpare The definition is To be,or cause to be, transparent; to appear,or cause to appear,or be seen, through something.

对读者的挑战

你会如何使这个应用程序成为守护进程,以便每日一词能够每天更新?(提示:cronjob 或datetime)。

项目挑战

使用wordnik API 构建一个单词游戏是可能的。想象一下一个既有趣又能帮助你提高词汇量的单词游戏。你将如何构建一个提示玩家问题并接受答案输入的东西?

尝试在显示屏上显示每日一词。你会如何实现这一点?

记录

这是一个将在接下来的两章中非常有用的主题。记录日志 (docs.python.org/3/library/logging.html) 有助于解决问题。它通过追踪应用程序记录的事件序列来确定问题的根本原因。虽然我们将在接下来的两章中广泛使用日志记录,但让我们通过一个简单的应用程序来回顾日志记录。为了回顾日志记录,让我们通过发送一个 POST 请求来查看它:

  1. 日志的第一步是设置日志文件位置和日志级别:
 logging.basicConfig(format='%(asctime)s :%(levelname)s :%(message)s', filename='log_file.log', level=logging.INFO)

在初始化 logging 类时,我们需要指定将日志信息、错误等写入文件的格式。在这种情况下,格式如下:

format='%(asctime)s : %(levelname)s : %(message)s'

日志消息的格式如下:

2016-10-2520:28:07,940: INFO : Starting new HTTPS connection (1): maker.ifttt.com 

日志消息被保存到名为 log_file.log 的文件中。

日志级别决定了我们应用程序所需的日志记录级别。不同的日志级别包括 DEBUGINFOWARNERROR

在这个例子中,我们将日志级别设置为 INFO。因此,属于 INFOWARNINGERROR 级别的任何日志消息都将保存到文件中。

如果将日志级别设置为 ERROR,则只有那些日志消息会被保存到文件中。

  1. 基于对 POST 请求结果的输出记录一条消息:
 response = requests.post(IFTTT_URL, json=payload)if response.status_code ==200: logging.info("Notification successfully triggered")else: logging.error("POST request failed")
  1. 将所有这些放在一起,我们得到如下:
#!/usr/bin/python3 import requests import logging # find your key from ifttt  IFTTT_URL = "https://maker.ifttt.com/trigger/rf_trigger/with/key/$key" if __name__ =="__main__":# fetch the bike share information  logging.basicConfig(format='%(asctime)s :%(levelname)s :%(message)s', filename='log_file.log', level=logging.INFO) payload ={"value1":"Sample_1","value2":"Sample_2"} response = requests.post(IFTTT_URL, json=payload)if response.status_code ==200: logging.info("Notification successfully triggered")else: logging.error("POST request failed")

以下代码示例(logging_example.py)与本章一起提供下载。这是对 Python 中日志记录概念的非常温和的介绍。我们将使用日志记录来排查我们项目中可能出现的任何错误。

在最后一章中,我们将讨论日志记录的最佳实践。

Python 中的多线程

在本节中,我们将讨论 Python 中的多线程概念。我们将在下一章中使用多线程。线程允许同时运行多个进程。例如,我们可以在监听传感器传入事件的同时运行电机。让我们用一个例子来演示这一点。

我们将模拟一个我们想要处理相同类型传感器的事件的情况。在这个例子中,我们只是将一些内容打印到屏幕上。我们需要定义一个函数来监听每个传感器的事件:

defsensor_processing(string):for num inrange(5): time.sleep(5)print("%s: Iteration: %d"%(string, num))

我们可以使用前面的函数,通过 Python 中的 threading 模块同时监听来自三个不同传感器的传感器事件:

thread_1 = threading.Thread(target=sensor_processing, args=("Sensor 1",)) thread_1.start() thread_2 = threading.Thread(target=sensor_processing, args=("Sensor 2",)) thread_2.start() thread_3 = threading.Thread(target=sensor_processing, args=("Sensor 3",)) thread_3.start()

将所有这些放在一起,我们得到如下:

import threading import time defsensor_processing(string):for num inrange(5): time.sleep(5)print("%s: Iteration: %d"%(string, num))if __name__ =='__main__': thread_1 = threading.Thread(target=sensor_processing, args=("Sensor 1",)) thread_1.start() thread_2 = threading.Thread(target=sensor_processing, args=("Sensor 2",)) thread_2.start() thread_3 = threading.Thread(target=sensor_processing, args=("Sensor 3",)) thread_3.start()

以下代码示例(作为 threading_example.py 可以下载)启动了三个线程,同时监听来自三个传感器的事件。输出看起来像这样:

Thread 1: Iteration:0 Thread 2: Iteration:0 Thread 3: Iteration:0 Thread 2: Iteration:1 Thread 1: Iteration:1 Thread 3: Iteration:1 Thread 2: Iteration:2 Thread 1: Iteration:2 Thread 3: Iteration:2 Thread 1: Iteration:3 Thread 2: Iteration:3 Thread 3: Iteration:3 Thread 1: Iteration:4 Thread 2: Iteration:4 Thread 3: Iteration:4

在下一章中,我们将使用多线程根据传感器输入控制机器人的电机。

Python 的 PEP8 风格指南

PEP8是 Python 的风格指南,有助于程序员编写可读的代码。遵循某些约定对于使我们的代码可读非常重要。以下是一些编码约定的示例:

  • 内联注释应以开头,并后跟一个空格。
  • 变量应遵循以下约定:first_var
  • 避免每行尾随空格。例如,if name == "test":后面不应跟空格。

您可以在www.python.org/dev/peps/pep-0008/#block-comments阅读整个 PEP8 标准。

验证 PEP8 指南

有工具可以验证代码的 PEP8 标准。在编写代码示例后,确保您的代码遵循 PEP8 标准。这可以通过使用pep8包来完成。它可以按照以下方式安装:

 pip3 install pep8 

让我们检查我们的代码示例是否按照 PEP8 约定编写。这可以通过以下方式完成:

 pep8 opencv_test.py 

检查显示以下错误:

 opencv_test.py:5:50: E231 missing whitespace after ',' opencv_test.py:6:19: E231 missing whitespace after ','

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_08_025.png

逗号后缺少尾随空格

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

摘要

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

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

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

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_001.png

由 Raspberry Pi Zero 供电的机器人

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_002.png

识别您的 Raspberry Pi Zero 版本

机器人的组件

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_003.png

机器人的组件

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

  • Raspberry Pi Zero 通过电机驱动电路(堆叠在 Raspberry Pi Zero 上)控制机器人的移动
  • 机器人的电机连接到电机驱动电路
  • 使用 USB 电池组为 Raspberry Pi Zero 供电。使用单独的 AA 电池组驱动电机
  • 机器人还配备了一个摄像头模块,有助于控制机器人的移动

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

组件来源数量价格(美元)
底盘www.adafruit.com/products/294319.95
底盘顶板www.adafruit.com/products/294414.95
一套 M2.5 螺母、垫圈和螺母a.co/dpdmb1B111.99
伺服电机中的直流电机www.adafruit.com/products/294123.50
轮子www.adafruit.com/products/274422.50
万向轮www.adafruit.com/products/294211.95
Raspberry Pi Zerowww.adafruit.com/products/340015.00
Raspberry Pi Zero 摄像头模块a.co/07iFhxC124.99
Raspberry Pi Zero 摄像头适配器www.adafruit.com/products/315715.95
Raspberry Pi Zero 电机驱动电路www.adafruit.com/products/2348122.50
USB 电池组a.co/9vQLx2t15.09
AA 电池组(4 节电池)a.co/hVPxfzD15.18
AA 电池NA4N.A.
Raspberry Pi 摄像头模块支架www.adafruit.com/products/143414.95

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

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

设置远程登录

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

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

更改密码

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

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

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_004.png

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

启用 SSH 访问

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_005.png

在接口选项卡下启用 SSH

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

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

底盘设置

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

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

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

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_006.jpg

为机器人准备底盘

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_007.png

组装电机和安装万向轮

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_008.png

将车轮组装到伺服电机上

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_009.png

将车轮锁紧在轴上

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

电机驱动器和电机选择

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

准备电机驱动器电路

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_010.png

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

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_011.png

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

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_012.png

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

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_013.png

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

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_014.png

焊接 3.5 mm 端子

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_015.png

电机驱动器已准备好使用

Raspberry Pi Zero 和电机驱动器组装

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

Raspberry Pi Zero 和电机驱动器组装

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

  1. 为了将 Raspberry Pi Zero 安装到底盘上,我们需要 4 个 M2.5 螺丝和螺母(安装孔规格可在www.raspberrypi.org/documentation/hardware/raspberrypi/mechanical/rpi-zero-v1_2_dimensions.pdf找到)。
  2. 我们选择的底盘带有插槽,可以直接将 Raspberry Pi Zero 安装到底盘上。根据您的底盘设计,您可能需要钻孔以安装 Raspberry Pi Zero。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_016.png

将 Raspberry Pi Zero 安装到底盘上

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

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

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

  1. 将电机驱动器堆叠在 Raspberry Pi Zero 上方(如前节所示)。
  2. 机器人的两个电机需要连接到 Raspberry Pi Zero:
  3. 电机驱动器带有 M1 至 M4 的电机端子。让我们将左边的直流电机连接到 M1,右边的直流电机连接到 M2。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_017.png

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

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

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

机器人电源设置

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

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

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_018.png

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

550mA * 2 hours = 1100 mAh

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

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

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

1100 mAh /0.7 = 1571.42 mAh

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_019.png

2200 mAh 5V USB 电池组

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

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_020.png

2200 mAh 5V USB 电池组

  1. 我们需要验证当使用电池组时 Raspberry Pi Zero 是否可以启动。
  2. 插入 HDMI 线(连接到监视器),使用非常短的微型 USB 线,尝试启动 Raspberry Pi Zero 并确保一切正常启动。

设置电机电源

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

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_021.png

电机驱动器电源端子

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

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

使用 Raspberry Pi Zero 的 5V 电源

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

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_022.png

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

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_023.png

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

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_024.png

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

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

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

使用外部电源

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

  1. 我们将使用由 4 节 AA 电池组成的电池组来驱动电机(电池组可在a.co/hVPxfzD购买)。
  2. 我们需要安装电池组,以便其引线可以连接到电机驱动器的电源端子。
  3. 机器人底盘套件附带了一个额外的铝制板,可用于安装电池组(如下图中所示):

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_025.png

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

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_026.png

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

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_027.png

固定铝制板

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_028.png

安装在铝制板上的电池

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

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

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

测试电机

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

电机驱动器检测

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

方法 1:从桌面启动

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_029.png

启用 I²C 接口

方法 2:从命令行启动

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

  1. 通过ssh登录到你的 Raspberry Pi Zero(有关ssh访问教程,请参阅第十一章,技巧与窍门)。
  2. 登录后,按照以下方式启动raspi-config
 sudo raspi-config.
  1. 应该启动配置选项菜单(如下截图所示):

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_030.png

raspi-config 菜单

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_031.png

选择 I²C 接口

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_032.png

启用 I²C 接口

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

检测电机驱动器

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

 sudo i2cdetect -y 1

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

0123456789 a b c d e f 00:--------------------------10:--------------------------------20:--------------------------------30:--------------------------------40:--------------------------------50:--------------------------------60:60------------------------------70:70--------------

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

电机测试

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

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

依赖项

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

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

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

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

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

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

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

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

  1. 如往常一样,第一步是导入MotorHAT模块:
from Adafruit_MotorHAT import Adafruit_MotorHAT, Adafruit_DCMotor 
  1. 下一步是创建MotorHAT类的实例并与电机驱动器建立接口(如前一小节所述,电机驱动器的 7 位地址是0x60)。
  2. 机器人的电机连接到通道 1 和 2。因此,我们需要初始化两个Adafruit_DCMotor类的实例,分别代表机器人的左右电机:
 left_motor = motor_driver.getMotor(1) right_motor = motor_driver.getMotor(2)
  1. 下一步是设置电机速度和方向。电机速度可以使用介于0255之间的整数设置(这对应于电机额定 rpm 的 0%和 100%)。让我们将电机速度设置为 100%:
 left_motor.setSpeed(255) right_motor.setSpeed(255)
  1. 让我们以正向方向旋转电机:
 left_motor.run(Adafruit_MotorHAT.FORWARD) right_motor.run(Adafruit_MotorHAT.FORWARD)
  1. 让我们以正向方向旋转两个电机 5 秒钟,然后降低速度:
 left_motor.setSpeed(200) right_motor.setSpeed(200)
  1. 现在,让我们以相反的方向旋转电机:
 left_motor.run(Adafruit_MotorHAT.BACKWARD) right_motor.run(Adafruit_MotorHAT.BACKWARD)
  1. 当我们完成将电机反向旋转 5 秒后,让我们关闭电机:
 left_motor.run(Adafruit_MotorHAT.RELEASE) right_motor.run(Adafruit_MotorHAT.RELEASE)

整合起来:

from Adafruit_MotorHAT import Adafruit_MotorHAT, Adafruit_DCMotor from time import sleep if __name__ =="__main__": motor_driver = Adafruit_MotorHAT(addr=0x60) left_motor = motor_driver.getMotor(1) right_motor = motor_driver.getMotor(2) left_motor.setSpeed(255) right_motor.setSpeed(255) left_motor.run(Adafruit_MotorHAT.FORWARD) right_motor.run(Adafruit_MotorHAT.FORWARD) sleep(5) left_motor.setSpeed(200) right_motor.setSpeed(200) left_motor.run(Adafruit_MotorHAT.BACKWARD) right_motor.run(Adafruit_MotorHAT.BACKWARD) sleep(5) left_motor.run(Adafruit_MotorHAT.RELEASE) right_motor.run(Adafruit_MotorHAT.RELEASE)

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

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

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

相机设置

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

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

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

让我们开始吧:

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_033.png

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

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_034.png

小心滑动相机界面的标签

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_035.png

相机适配器放置

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_036.png

在另一侧插入适配器

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_037.png

Raspberry Pi 相机模块安装

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_038.png

机器人前部的摄像头

  1. 通过 ssh 登录到你的 Raspberry Pi Zero 桌面以启用并测试摄像头接口。
  2. 启用摄像头接口与本章前面讨论的启用 I²C 接口类似。使用 raspi-config 命令启动 Raspberry Pi 配置:
 sudo raspi-config 

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_039.png

Raspberry Pi 配置屏幕的截图

  1. 重启你的 Raspberry Pi Zero!

摄像头功能验证

  1. 重启完成后,从命令提示符运行以下命令:
 raspistill -o test_picture 
  1. 由于你的机器人已经完全组装好,你的 Raspberry Pi Zero 的 HDMI 端口可能无法访问。你应该使用 scp 命令检索文件。

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

 scp [email protected]:/home/pi/test_output.
  1. 检查使用 Raspberry Pi 摄像头模块拍摄的图片以验证其功能

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_040.png

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

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

Web 接口

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

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

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

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

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

  1. 打开名为 robot.py 的文件以实现 Robot 类。
  2. 为了控制机器人的移动,机器人在初始化时需要使用(以驱动电机)的电机驱动器通道作为输入。
  3. 因此,Robot 类的 __init__() 函数可能如下所示:
import time from Adafruit_MotorHAT import Adafruit_MotorHAT classRobot(object):def__init__(self, left_channel, right_channel): self.motor = Adafruit_MotorHAT(0x60) self.left_motor = self.motor.getMotor(left_channel) self.right_motor = self.motor.getMotor(right_channel)
  1. 在前面的代码片段中,__init__() 函数需要作为参数传递给连接左右电机到电机驱动器板的通道。
  2. 当创建 Robot 类的实例时,电机驱动器(Adafruit_MotorHAT)被初始化,电机通道也被初始化。
  3. 让我们编写方法来使机器人向前和向后移动:
defforward(self, duration): self.set_speed() self.left_motor.run(Adafruit_MotorHAT.FORWARD) self.right_motor.run(Adafruit_MotorHAT.FORWARD) time.sleep(duration) self.stop()defreverse(self, duration): self.set_speed() self.left_motor.run(Adafruit_MotorHAT.BACKWARD) self.right_motor.run(Adafruit_MotorHAT.BACKWARD) time.sleep(duration) self.stop()
  1. 让我们也编写方法来使机器人向左和向右移动。为了使机器人向左转,我们需要关闭左电机并保持右电机开启,反之亦然。这会产生一个转向力矩,使机器人向该方向转动:
defleft(self, duration): self.set_speed() self.right_motor.run(Adafruit_MotorHAT.FORWARD) time.sleep(duration) self.stop()defright(self, duration): self.set_speed() self.left_motor.run(Adafruit_MotorHAT.FORWARD) time.sleep(duration) self.stop()
  1. 因此,我们实现了一个 Robot 类,该类可以驱动机器人在四个方向上移动。让我们实现一个简单的测试,以便在我们将其用于主程序之前测试 Robot 类:
if __name__ =="__main__":# create an instance of the robot class with channels 1 and 2  robot = Robot(1,2)print("Moving forward...") robot.forward(5)print("Moving backward...") robot.reverse(5) robot.stop()

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

Web 界面的摄像头设置

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

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

 sudo apt-get install motion 

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

  1. 编辑 /etc/motion/motion.conf 中的以下参数:
 daemon on threshold 99999 framerate 90 stream_maxrate 100 stream_localhost off 
  1. /etc/default/motion 中包含以下参数:
 start_motion_daemon=yes 
  1. 按照以下方式编辑 /etc/init.d/motion
 start)if check_daemon_enabled ; then if ! [-d /var/run/motion ]; then mkdir /var/run/motion fi chown motion:motion /var/run/motion sudo modprobe bcm2835-v4l2 chmod 777/var/run/motion sleep 30 log_daemon_msg "Starting $DESC""$NAME"
  1. 重启你的树莓派 Zero。
  2. 下一步假设你已经安装了 Flask 框架并尝试了 第七章 中的基本示例,请求和 Web 框架
  3. 在你的 flask 框架所在的文件夹中创建一个名为 templates 的文件夹:Robot 类文件位于)并在该文件夹中创建一个名为 index.html 的文件,内容如下:
<!DOCTYPE html><html><head><title>Raspberry Pi Zero Robot</title></head><body><iframe id="stream" src="img/?action=stream" width="320" height="240"></iframe></body></html>
  1. 在前面的代码片段中,包括你的树莓派 Zero 的 IP 地址并将其保存为 index.html
  2. 创建一个名为 web_interface.py 的文件,并在模板文件夹中提供 index.html
from flask import Flask, render_template app = Flask(__name__)@app.route("/")defhello():return render_template('index.html')if __name__ =="__main__": app.run('0.0.0.0')
  1. 使用以下命令运行 Flask 服务器:
 python3 web_interface.py 
  1. 在你的笔记本电脑上打开浏览器,并访问你的树莓派 Zero 的 IP 地址(端口 5000),以查看树莓派摄像头模块的实时流。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_041.png

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

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

机器人控制按钮

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

  1. 第一步是向index.html添加四个按钮。我们将使用 HTML 表格添加四个按钮(代码片段已缩短以节省篇幅,有关 HTML 表格的更多信息,请参阅www.w3schools.com/html/html_tables.asp):
<table style="width:100%; max-width: 500px; height:300px;"><tr><td><form action="/forward" method="POST"><inputtype="submit" value="forward" style="float: left; width:80%;"></br></form></td>...</table>
  1. web_interface.py中,我们需要实现一个方法来接受来自按钮的POST请求。例如,可以按照以下方式实现接受来自/forward的请求的方法:
@app.route('/forward', methods =['POST'])defforward(): my_robot.forward(0.25)return redirect('/')
  1. 将所有这些放在一起,web_interface.py看起来如下所示:
from flask import Flask, render_template, request, redirect from robot import Robot app = Flask(__name__) my_robot = Robot(1,2)@app.route("/")defhello():return render_template('index.html')@app.route('/forward', methods =['POST'])defforward(): my_robot.forward(0.25)return redirect('/')@app.route('/reverse', methods =['POST'])defreverse(): my_robot.reverse(0.25)return redirect('/')@app.route('/left', methods =['POST'])defleft(): my_robot.left(0.25)return redirect('/')@app.route('/right', methods =['POST'])defright(): my_robot.right(0.25)return redirect('/')if __name__ =="__main__": app.run('0.0.0.0')

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

 python3 /<path_to_webserver_file>/web_interface.py 

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

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-prog-raspi/img/image_09_042.png

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

故障排除技巧

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

  • 在组装摄像头模块时,我们损坏了 Raspberry Pi Zero 的摄像头接口标签。我们不得不更换 Raspberry Pi Zero。
  • 我们在电机驱动电路中遇到了一些幽灵问题。在某些情况下,我们无法检测到电机驱动器。我们不得不更换电机驱动器的电源。当我们找到这个问题的根本原因时,我们将保持本书的网站更新。
  • 在为浏览器设置网络流时,我们遇到了很多问题。我们不得不调整很多设置才能使其工作。我们找到了一些文章来修复这个问题。我们已经在本书的参考文献部分分享了它们。

项目增强

  • 考虑对网络界面进行增强,以便你可以改变机器人的速度。
  • 如果你计划构建一个在户外条件下运行的机器人,你可能会添加一个 GPS 传感器。大多数 GPS 传感器通过 UART 接口传输数据。我们建议阅读第四章*,通信接口*以获取示例。
  • 可以使用这个传感器测量障碍物的距离:www.adafruit.com/products/3317。这可能在遥测应用中很有帮助。
  • *在本书中,我们使用摄像头来驱动机器人。使用这个图像理解工具可以拍照并理解场景中的物体:cloud.google.com/vision/

摘要

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

学习资源

  • 差速转向机构: https://www.robotix.in/tutorial/mechanical/drivemechtut/
  • 差速转向机构的视频讲座: https://www.coursera.org/learn/mobile-robot/lecture/GnbnD/differential-drive-robots
  • 《Make 杂志:构建你自己的底盘》: https://makezine.com/projects/designing-a-robot-chassis/
  • 机器人协会:构建你自己的底盘指南: http://www.societyofrobots.com/mechanics_chassisconstruction.shtml
  • Adafruit 的电机驱动器文档: https://learn.adafruit.com/adafruit-dc-and-stepper-motor-hat-for-raspberry-pi
  • Adafruit 电机选择指南: https://learn.adafruit.com/adafruit-motor-selection-guide
  • Adafruit 关于构建基于 Raspberry Pi 的简单机器人的指南: https://learn.adafruit.com/simple-raspberry-pi-robot/overview
  • Flask 框架和表单提交: http://opentechschool.github.io/python-flask/core/form-submission.html
  • Raspberry Pi 摄像头设置用于网络流媒体: http://jamespoole.me/2016/04/29/web-controlled-robot-with-video-stream/

Read more

OpenClaw dashboard命令后,无法登录web控制面板(在systemd服务无法启动的一些虚拟机里会碰到)

OpenClaw dashboard命令后,无法登录web控制面板(在systemd服务无法启动的一些虚拟机里会碰到)

先上结论 执行OpenClaw dashboard命令后,无法登录web控制面板,是因为OpenClaw的gateway服务没有起来。原来小龙虾OpenClaw 的命令没有学明白,先弄清楚命令: openclaw onboard 是配置 openclaw dashboard是显示web控制面板登录信息 openclaw gateway --verbose 是启动网关 openclaw gateway start是启动网关服务 问题就是因为这台系统的systemd没有起作用,导致openclaw的gateway服务没有起来,所以控制面板无法登录。 OpenClaw status Overview ┌─────────────────┬───────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Item │ Value │ ├─────────────────┼────────────────────────────────────

By Ne0inhk
哈希的介绍

哈希的介绍

1. unordered系列关联式容器     下面来看哈希,首先看关联式容器unorder_map和unorder_set,它们底层是哈希表,用法和map set一样。下面浅浅过一下,它是单向迭代器,因为没有rbegin和rend。也就是红黑树和哈希表实现的map和set用法几乎相同,区别是:1.unorder系列是单向迭代器。2.unorder系列遍历出来不是有序的。下面演示一下: 它只能去重,不能排序,它也是有multi版本的。再演示一下unorder_map: 2.哈希     下面正式看哈希,什么是哈希呢?我们以前遇到的搜索有这样几类:首先是暴力查找,在一个数组里都查,这样非常慢。于是有人衍生出了有序数组的二分查找,但它的前提是排序,而且增删查改不方便,过程中为了保证有序会涉及大量的数据挪动。因此衍生出了平衡搜索树,此时基础上又出现了新的搜索,这种搜索叫哈希(散列)。它的本质是存储的值跟存储位置建立出一个映射关系,什么意思呢,先来看一个计数排序的样例: 有上面这样的一组值,最小的值是15,最大的值是30,总共开了16个空间。然后存映射关系(次数),15映射第一个位

By Ne0inhk
音乐播放器实现:前端HTML,CSS,JavaScript综合大项目

音乐播放器实现:前端HTML,CSS,JavaScript综合大项目

音乐播放器实现:前端HTML,CSS,JavaScript综合大项目 * 项目概述 * 项目视图效果 * 一、侧边栏相关代码 * (一)HTML代码 * (二)css代码 * 二、登录页面 * (一)HTML代码 * (二)css代码 * (三)js代码 * 三、剩余代码以及所有源代码Gitee地址 项目概述 在当今数字化时代,音乐已然成为人们生活中不可或缺的一部分。本次带来的音乐播放器 HTML 项目,旨在打造一个具备基础且实用功能的音乐播放平台。通过 HTML、CSS 和 JavaScript 等前端技术的巧妙融合,实现一个界面美观、操作便捷的音乐播放器,满足用户在本地浏览音乐库、播放音乐等多样化需求。 提示!!!! 由于项目代码太多,代码全部内容放置在我的Gitee码云中,需要的小伙伴们自取 我的码云链接https://gitee.com/srte-7719/project-experience/tree/master/

By Ne0inhk
Flutter for OpenHarmony: Flutter 三方库 directed_graph 在鸿蒙应用中优雅处理复杂的拓扑排序与依赖关系(算法级工具)

Flutter for OpenHarmony: Flutter 三方库 directed_graph 在鸿蒙应用中优雅处理复杂的拓扑排序与依赖关系(算法级工具)

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 在进行 OpenHarmony 的复杂业务逻辑设计时,我们经常会遇到“依赖关联”问题。例如: 1. 任务调度:任务 A 依赖于任务 B 和 C,任务 B 依赖于 D。你应该按什么顺序运行它们? 2. 数据流建模:在鸿蒙分布式节点中,数据是如何从一个端点流向另一个端点的?是否存在循环引用(Cycle)? 3. 资源加载器:一个大型鸿蒙 HAP 包内的资源加载优先级排序。 directed_graph 是一款纯粹的、算法级别的 Dart 库。它提供了标准的数据结构模型,能帮你极其高效地处理这些复杂的拓扑(Topology)关系。 一、有向图逻辑模型 该库支持对图节点进行深度遍历、

By Ne0inhk