原文:
zh.annas-archive.org/md5/ab90cd9c9a6a245c854f01db41f21e93译者:飞龙
第十三章:制作 Jarvis 物联网启用版
曾经,我们曾想象用指尖控制世界。现在,这个想象已经变成了现实。随着智能手机的出现,我们一直在做十年前只能想象的事情。随着手机变得智能,行业和企业也尽力跟上颠覆性的变革。然而,还有一部分仍然落后。那是哪一部分呢? 你的家!
想想你能用智能手机控制家里哪些东西?没多少吧!有一些设备可以打开或关闭一串设备,比如你的空调。然而,这个列表是详尽的。所以,在前面章节中获得的所有知识和我们手中的强大硬件面前,为什么我们不成为潮流的引领者和颠覆者,创造出仍然只是我们想象中的一部分的东西呢。
本章将涵盖以下主题:
-
物联网(IoT)基础
-
消息队列遥测传输(MQTT)协议
-
设置 MQTT 代理
-
制作基于物联网的入侵检测器
-
控制家庭
物联网基础
在本章中,我们将使用智能手机控制家里的设备,但在这样做之前,我们应该了解这项技术的基础。本章的第一个主题是物联网——现代世界中被过度使用的术语。它是每个人都想了解但没有人真正了解的东西。物联网可以与一种技术相关联,比如你的冰箱会告诉你哪些物品供应不足,并自动为你订购。可怜的家伙!这项技术需要一段时间才能侵入我们的家庭。但物联网不仅仅意味着这一点。物联网是一个非常广泛的概念,几乎可以应用于所有地方进行优化。那么,物联网究竟是什么呢?
让我们来分解这个缩写词,物联网有时也被称为网络物理系统。现在,什么是事物?任何能够无需人工干预收集或接收数据的电子设备都可以称为事物。所以这个事物可以是你的手机、起搏器、健康监测设备等等。唯一的条件是它应该连接到互联网,并且具有收集和/或接收数据的能力。第二个术语是互联网;互联网指的是互联网,嗯!现在,所有这些物联网设备都从云或中央计算机发送和接收数据。它之所以这样做的原因是,任何物联网设备,无论大小,都被视为资源受限的环境。也就是说,资源,如计算能力,要少得多。这是因为物联网设备必须简单且便宜。想象一下,你必须把物联网传感器安装在所有的路灯上以监控交通。如果设备成本为 500 美元,那么安装这种设备就不切实际了。然而,如果它可以用 5-10 美元的价格制造出来,那么没有人会眨一下眼。这就是物联网设备的特点;它们极其便宜。然而,这个故事的反面是,它们没有太多的计算能力。因此,为了平衡这个方程,它们不是在自己的处理器上计算原始数据,而是简单地发送这些数据到云计算设备或服务器,在那里这些数据被计算,并提取出有意义的结论。所以,这解决了我们所有的问题。嗯,不!这些设备的第二个问题是,它们可以是电池供电的,一次性使用设备。例如,在森林中安装的所有温度传感器;在这种情况下,没有人,绝对没有人会每周去更换电池。因此,这些设备被设计成消耗很少甚至没有电力,这使得编程变得非常棘手。
现在我们已经了解了物联网的概念,在本章中,我们将使我们的家庭物联网化。这意味着,我们将能够从我们的家庭传感器接收和收集数据,并在我们的移动设备上查看,如果需要,我们还可以通过智能手机控制这些设备。不过有一件事,我们不会在云上计算,而是将所有数据上传到云上,仅访问这些数据或从云上发送我们的数据,以便可以访问。我们将另开一册书来讨论云计算方面,因为这可以是一个全新的维度,并且超出了本书的范围。
MQTT 协议
MQTT 是一个 ISO 认证的协议,并且被广泛使用。这个协议有趣的地方在于,它是由 Andy Stanford 和 Arlen Nipper 在 1999 年开发的,用于通过沙漠监控石油管道。正如你可以想象的那样,在沙漠的中间,他们开发的协议必须既节能又带宽高效。
这种协议是如何工作的非常有趣。它有一个发布-订阅架构。这意味着,它有一个中央服务器,我们通常称之为代理。任何设备都可以注册到这个代理并发布任何有意义的到它。现在,正在发布的数据应该有一个主题,例如,空气温度。
这些主题特别重要。为什么你会问?对于代理来说,可以连接一个或多个设备。随着连接的建立,它们还需要订阅一个主题。假设它们订阅了主题*Air-*Temperature。现在,每当有新的数据到来时,它将被发布到订阅的设备。
有一个重要的事情需要了解,那就是从代理获取数据不需要像 HTTP 中那样发出请求。相反,每当接收到数据时,它将被推送到订阅了该主题的设备。很明显,在整个过程中,TCP 协议也将处于运行状态,并且与代理相关的端口将始终保持连接,以便实现无缝的数据传输。然而,如果数据出现中断,代理将缓冲所有数据,并在连接恢复时将数据发送给订阅者。
如您所见,运动传感器和温度传感器通过特定的主题将数据发送给 MQTT 服务器,这些主题分别是温度和运动。订阅了这些主题的用户将从这个设备获取读数。因此,实际传感器和移动设备之间不需要直接通信。
整个架构的好处是,可以连接无限数量的设备,并且不需要考虑可扩展性问题。此外,该协议相对简单,易于处理,即使处理大量数据也是如此。因此,它成为物联网的首选协议,因为它提供了一个简单、可扩展且无缝的数据生产者和数据接收者之间的链接。
设置 MQTT 代理
记得那个老套的视觉处理更新过程吗?你怎么能忘记?我们在这里也要做同样的事情。但幸运的是,这次它并不长。那么,让我们看看我们为了设置这个服务器需要做什么。打开你的命令行,输入以下这些行:
sudo apt-get update
sudo apt-get upgrade
你知道这一行的作用。如果你对它的记忆不是很清楚,那么请参考第九章,视觉处理。一旦更新和升级过程完成,继续安装以下包:
sudo apt-get install mosquitto -y
这将在你的 Raspberry Pi 上安装 Mosquitto 代理。这个代理将负责所有的数据传输:
sudo apt-get install mosquitto-clients -y
现在,这一行将安装客户端包。正如你所想象的那样,Raspberry Pi 本身将是代理的一个客户端。因此,它将负责所需的一切。
我们现在已经安装了包;是的,就是那么简单。现在,我们只需要配置 Mosquitto 代理。为此,你需要输入以下命令:
sudo nano etc/mosquitto/mosquitto.conf
现在,这个命令将打开保存 Mosquitto 文件配置的文件。为了配置它,你需要到达文件末尾,在那里你会看到以下内容:
include_dir/etc/mosquitto/conf.d
现在,你可以通过在行前添加 # 来注释掉前面的代码行。完成后,继续添加以下行:
allow_anonymous false
password_file /etc/mosquitto/pwfile
listener 1883
让我们看看我们在这里做了什么。allow_anonymous false 这一行告诉代理不是每个人都可以访问数据。下一行,password_file /etc/mosquitto/pwfile 告诉代理密码文件的存放位置,该文件位于 /etc/mosquitto/pwfile。最后,我们将使用 listener 1883 命令定义这个代理的端口,该端口是 1883。
因此,最终,我们在我们的 Raspberry Pi 上完成了 MQTT 客户端的设置。现在我们准备好继续使用它来为物联网化的家庭服务。
制作一个基于物联网的入侵检测器
现在,Raspberry Pi 已经设置好,我们准备将其变为物联网设备,让我们看看我们将如何将系统连接到互联网并使其工作。首先,我们需要将 Raspberry Pi 连接到我们想要使用物联网技术控制的设备。所以,请继续使用以下图表来建立连接:
一旦设置好所有组件,让我们继续上传以下代码:
import time
import paho.mqtt.client as mqtt
import RPi.gpio as gpio
pir = 23
gpio.setmode(gpio.BCM)
gpio.setup(pir, gpio.IN)
client = mqtt.Client()
broker="broker.hivemq.com"
port = 1883
pub_topic = "IntruderDetector_Home"
def SendData():
client.publish(pub_topic,"WARNING : SOMEONE DETECTED AT YOUR PLACE")
def on_connect(client, userdata, flag,rc):
print("connection returned" + str(rc))
SendData()
while True:
client.connect(broker,port)
client.on_connect = on_connect
if gpio.output(pir) == gpio.HIGH :
SendData()
client.loop_forever()
与我们之前看到的其他代码块不同,这段代码对你来说将相当新颖。所以,我将解释它的每一部分,除了几个显而易见的部分。那么,让我们看看这里有什么:
import paho.mqtt.client as mqtt
在这部分,我们导入 pho.mqtt.client 库作为 mqtt。所以每当需要访问这个库时,我们只需使用 mqtt 这一行,而不是整个库的名称。
client = mqtt.Client()
我们使用 mqtt 库的客户端方法定义了一个客户端。这可以通过 client 变量来调用。
broker="broker.hivemq.com"
因此,我们在程序中定义了代理。对于这个程序,我们使用代理 broker.hivemq.com,它为我们提供代理服务。
port = 1883
现在我们已经像之前做的那样,将再次定义协议将工作的端口,在我们的例子中是 1883。
pub_topic = "IntuderDetector_Home"
在这里,我们正在定义一个名为 pub_topic 的变量的值,它是 IntruderDetector_Home。这将是在代码运行后可以订阅的最终主题。
def SendData():
client.publish(pub.topic, "WARNING : SOMEONE DETECTED AT YOUR PLACE")
在这里,我们定义了一个名为 SendData() 的函数,将数据 Warning : SOMEONE DETECTED AT YOUR PLACE 发布到我们之前声明的主题的代理。
def on_message(client, userdata, message):
print('message is : ')
print(str(message.payload))
在这一行,我们正在定义一个名为on_message()的函数,该函数将打印出message is:值,后跟数据。这将通过print(str(message.payload))行来完成。这个操作是在打印函数参数中传递的任何内容。
def on_connect(client, userdata, flag,rc):
print("connection returned" + str(rc))
SendData()
在这一行,我们正在定义on_connect()函数,该函数将打印出connection returned行,后跟rc的值。rc代表返回码。因此,每当消息送达时,都会生成一个代码,即使没有错误发生,也会返回特定的代码来通知错误。所以,这可以被视为一个确认。完成此操作后,我们将使用之前定义的SendData()函数将数据发送到代理。
client.connect(broker,port)
connect()是 MQTT 库中的一个函数,用于将客户端连接到代理。这样做非常简单。我们只需要传递我们想要连接的代理的参数以及将要使用的端口。在我们的例子中,broker = broker.hivemq.com和port = 1883。因此,当我们调用该函数时,树莓派就会连接到我们的代理。
client.on_connect = on_connect
这是程序的核心。client.on_connect函数所做的就是在树莓派每次连接到代理时,它都会执行我们定义的on_connect函数。这将连续每 5 秒将数据发送到代理,这正是我们在函数中定义的方式。这个过程也被称为回调,这使得它是事件驱动的。也就是说,如果没有连接,它不会尝试将数据发送到代理。
if gpio.output(pir) == HIGH :
sendData()
当 PIR 传感器检测到高电平或检测到运动时,sendData()函数会被调用,将带有警告信息“有人在你这里被检测到”的消息发送到代理。
client.loop_forever()
这是我最喜欢的函数,尤其是因为它有一个可爱的名字。正如你所预期的,client.loop_forver()函数将不断寻找任何事件,一旦检测到,就会触发将数据发送到代理。现在,我们将看到这些数据。为此,如果你正在运行 iOS,我们需要从 App Store 下载MyMQTT应用;如果你正在运行 Android,则需要从 Playstore 下载。
一旦启动应用,你将看到前面的屏幕。你需要填写代理 URL 的名称,在我们的例子中是broker.hivemq.com。然后,填写端口,在我们的例子中是1883。
完成这些操作后,你将看到类似于以下屏幕的屏幕:
简单地添加你需要订阅的名称,即IntruderDetector_Home。完成这些操作后,你将看到魔法!
你也可以应用我们在第十章中使用的相同逻辑,即制作一个守卫机器人。所以,请继续尝试;我会在这里等你。
在下一节中,我们将根据物联网来控制事物;到时候见。
控制家庭
最后,使用以下图表,建立连接并上传以下代码:
import time
import paho.mqtt.client as paho
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setup(14,GPIO.OUT)
broker="broker.hivemq.com"
sub_topic = light/control
client = paho.Client()
def on_message(client, userdata, message):
print('message is : ')
print(str(message.payload))
data = str(message.payload)
if data == "on":
GPIO.output(3,GPIO.HIGH)
elif data == "off":
GPIO.output(3,GPIO.LOW)
def on_connect(client,userdata, flag, rc):
print("connection returned" + str(rc))
client.subscribe(sub_topic)
client.connect(broker,port)
client.on_connect = on_connect
client.on_message=on_message
client.loop_forever()
现在,在这段代码中,我没有什么要告诉你的;它相当直接。我们发送数据的方式和上次一样。然而,这次我们使用了一个新的函数。那么,让我们看看这段代码是关于什么的:
def on_message(client, userdata, message):
print('message is : ')
print(str(message.payload))
data = str(message.payload)
if data == "on":
GPIO.output(3,GPIO.HIGH)
elif data == "off":
GPIO.output(3,GPIO.LOW)
在这里,我们定义了on_message()函数的作用。该函数有三个参数,消息将在这三个参数上工作。这包括我们之前已经声明的client;userdata,我们现在没有使用;最后是message,我们将通过智能手机通过互联网发送它。
一旦你查看程序内部,这个函数将使用print('message is : ')和print(str(message.payload))这些行打印消息。一旦完成,data的值将被设置为订阅者发送的消息。
这份数据将由我们的条件进行评估。如果数据保持on,那么 GPIO 端口号3将被设置为HIGH,如果字符串是off,那么 GPIO 端口号3将被设置为LOW——简单来说,就是打开或关闭你的设备。
def on_connect(client,userdata, flag, rc):
print("connection returned" + str(rc))
client.subscribe(sub_topic)
我们之前已经定义了on_connect()函数。然而,这次它略有不同。我们不仅打印出带有rc值的连接返回值,还使用了一个名为client.subscribe(sub_topic)的另一个函数,这将使我们能够连接到我们在程序中之前定义的特定主题的代理。
client.on_message=on_message
如我们所知,整个算法基于事件驱动系统,这个client.on_message函数将一直等待接收消息。一旦接收到,它将执行on_message函数。这将决定是否打开或关闭设备。
要使用它,只需根据主题发送数据,它就会被你的树莓派接收。
一旦接收到,决策函数on_message()将决定 MyMQTT 应用接收到的数据是什么。如果接收到的数据是on,那么灯就会打开。如果接收到的数据是off,那么灯就会关闭。就这么简单。
摘要
在本章中,我们了解了物联网的基本知识以及 MQTT 服务器的工作原理。我们还创建了一个入侵检测系统,无论你在世界上的任何地方,只要有人进入你的家,它就会发出警报。最后,我们还创建了一个系统,可以通过简单的手机命令在家中的设备上打开。在下一章中,我们将让 Jarvis 能够让你根据声音与系统交互。
第十四章:给 Jarvis 赋予声音
你是否想过使用机器人来完成我们的工作是否可能?嗯,是的!当然在一些高科技小说、漫威电影甚至漫画书中是可能的。所以,系好你的安全带,准备好进入这个令人惊叹的章节,你将实际实施我刚才提到的事情。
本章将涵盖以下主题:
-
基本安装
-
自动应答机
-
制作一个交互式门应答机器人
-
让 Jarvis 理解我们的声音
基本安装
有许多方法和途径可以控制我们的智能家居 Jarvis,其中一些我们在之前已经探索过,例如通过控制它。因此,为了开始,我们需要准备我们的系统以便能够进行语音合成;为了做到这一点,让我们执行以下过程。
首先,转到终端并输入以下命令:
sudo apt-get install alsa-utils
这将安装依赖项alsa-utils。alsa-utils包包含各种有用的工具,可用于控制你的声音驱动程序。
一旦完成,你需要编辑文件。为了做到这一点,我们需要打开文件。使用以下命令:
sudo nano /etc/modules
一旦完成,一个文件将打开;在该文件的底部,你需要添加以下行:
snd_bcm2835
你不需要太深入地了解我们为什么要做这件事。它只是用来设置一些基础。我可以给你一个解释;然而,我不希望在这么激动人心的时刻让你感到无聊。
此外,如果你很幸运的话,有时你可能会发现这一行已经存在。如果是这样,那就让它在那里,不要去动它。
现在,为了播放 Jarvis 需要说的声音,我们需要一个音频播放器。不,不是你家里有的那个。我们说的是能够播放它的软件。
要安装播放器,我们需要运行以下命令:
sudo apt-get install mplayer
好的,我们已经完成了音频播放器的设置;让我们看看接下来是什么。现在,再次,我们需要编辑媒体播放器的文件。我们将使用相同的步骤来打开和编辑文件:
sudo nano /etc/mplayer/mplayer.conf
这将打开文件。和之前一样,只需添加以下行:
nolirc=yes
最后,我们需要给它一些声音,所以运行以下命令:
sudo apt-get install festvox-rablpc16k
这将为 Jarvis 安装一个 16 kHz、英国男性声音。我们喜欢英国口音,不是吗?
完美。一旦我们完成了之前提到的所有步骤,我们就可以开始了。为了测试声音,只需将 USB 扬声器连接到树莓派,并运行以下代码:
import os
from time import sleep
os.system('echo "hello! i am raspberry pi robot"|festival --tts ')
sleep(2)
os.system('echo "how are you?"| festival --tts ')
sleep(2)
os.system('echo "I am having fun."| festival --tts ')
sleep(2)
好吧,让我们看看我们实际上已经做了什么:
import os
如你所想,我们正在导入名为os的库。这个库提供了一种使用操作系统依赖功能的方法:
os.system('echo "Hello from the other side"|festival --tts ')
在这里,我们使用了一个名为 system() 的方法;这个方法的作用是执行一个 shell 命令。你可能想知道这是什么。shell 命令是用户用来访问系统功能并与系统交互的命令。所以现在,我们想要将我们的文本转换为语音,我们将向这个函数提供两个参数。首先,文本是什么?在我们的例子中,它是 Hello from the other side;这里的第二个参数是 festival --tts。现在 festival 是一个库,而 tts 代表文本到语音转换。所以当我们传递给参数时,系统将知道传递给参数的文本需要从文本转换为语音。
就这样!是的,就是这样。这就是我们让树莓派说话所需要做的全部。
自动投递应答机
这些天,我们都在网上订购东西。然而,无论亚马逊的过程多么自动化,当我们谈到 2018 年时,我们仍然有人将包裹送到我们的家门口。有时,你希望他们知道一些关于在哪里留下包裹的事情。现在我们变得越来越自动化,那些可能在你门口留下便条的日子已经过去了。是时候用我们的技术做一些真正有趣的事情了。为了做到这一点,我们几乎不需要做什么严肃的事情。我们只需要按照以下图示连接组件:
PIR 传感器必须放置在门周围有移动时给出逻辑高电平的位置。
一旦完成,请上传以下代码:
import RPi.GPIO as GPIO
import time
Import os
GPIO.setmode(GPIO.BCM)
PIR = 13
GPIO.setup(PIR,GPIO.IN)
while True:
if GPIO.input(PIR) == 1 :
os.system('echo "Hello, welcome to my house"|festival --tts ')
time.sleep(0.2)
os.system('echo "If you are a delivery agent then please leave the package here"|festival --tts ')
time.sleep(0.2)
os.system('echo "If you are a guest then I'm sorry I have to leave I will be back after 7pm"|festival --tts ')
time.sleep(0.2)
os.system('echo "also Kindly don't step over the grass, its freshly grown and needs some time"|festival --tts ')
time.sleep(1)
os.system('echo "Thank you !"|festival --tts ')
现在我们所做的是非常简单的。一旦 PIR 传感器给出逻辑高电平,就会发出一定的指令。这里不需要解释。如果你需要任何澄清,可以参考之前的代码。
制作一个交互式门应答机器人
在上一章中,我们使用 PIR 传感器来感应任何人类活动,然而,这个传感器的问题在于,无论谁来或离开,它都会传达相同的信息。这基本上意味着,即使你在漫长的一天后回到家,它也会提出同样的问题。不是很愚蠢吗?
因此,在本章中,我们将使用之前的仓库,将视觉和语音集成在一起,以创造一个惊人的组合。在这里,摄像头将识别门上是谁,并识别是否是人或陌生人,如果是的话,它将传达你想要传达的信息。另一方面,如果你是那个人,它将简单地用一个简单的问候让你通过。然而,如果检测到人脸但没有识别出来,它将向站在摄像头前的人给出一系列指令。
要实现这一切,你只需要在门上设置一个带有 PIR(被动红外探测器)的摄像头。PIR 基本上是用来激活摄像头的。换句话说,只有在检测到没有运动时,摄像头才会被激活。这个设置非常简单,不需要使用任何 GPIO。只需固定摄像头和 PIR,然后上传以下代码:
import RPi.GPIO as GPIO
import time
Import os
import cv2
import numpy as np
import cv2
faceDetect = cv2.CascadeClassifier('haarcascade_frontalface_default.xml')
cam = cv2.VideoCapture(0)
rec = cv2.face.LBPHFaceRecognizer_create()
rec.read("recognizer/trainningData.yml")
id = 0
while True:
GPIO.setmode(GPIO.BCM)
PIR = 13
GPIO.setup(PIR, GPIO.IN)
if GPIO.input(PIR) == 1:
ret, img = cam.read()
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
faces = faceDetect.detectMultiScale(gray, 1.3, 5)
for (x, y, w, h) in faces:
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 2)
id, conf = rec.predict(gray[y: y + h, x: x + w])
if id == 1:
id = "BEN"
os.system('echo "Hello, welcome to the house BEN"|festival --tts ')
time, sleep(0.2)
else :
os.system('echo "If you are a delivery agent then please leave the package here"|festival --tts ')
time, sleep(0.2)
os.system('echo "If you are a guest then I'
m sorry I have to leave I will be back after 7 pm "|festival --tts ')
time, sleep(0.2)
os.system('echo "also Kindly don'
t step over the grass, its freshly grown and needs some time "|festival --tts ')
time.sleep(1)
os.system('echo "Thank you !"|festival --tts ') cv2.imshow("face", img) if cv2.waitKey(1) == ord('q'):
break cam.release()
cv2.destroyAllWindows()
faceDetect = cv2.CascadeClassifier('haarcascade_frontalface_default.xml')
在前面的代码中,我们正在使用CascadeClassifier方法创建一个级联分类器,以便摄像头可以检测到人脸。
cam = cv2.VideoCapture(0)
rec = cv2.face.LBPHFaceRecognizer_create()
在前面的代码中,我们正在使用cv2的VideoCapture(0)方法从摄像头读取帧。同时,正在创建人脸识别器以识别特定的人脸。
ret, img = cam.read()
现在,使用cam.read()从摄像头读取数据,就像在之前的代码中所做的那样。
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
faces = faceDetect.detectMultiScale(gray,1.3,5)
图像被转换为灰色。然后,faceDetect.detectMultiScale()将使用灰度转换后的图像。
for (x,y,w,h) in faces:
cv2.rectangle(img, (x,y), (x+w, y+h), (0,0,255), 2)
id, conf = rec.predict(gray[y:y+h, x:x+w])
if id==1:
id = "BEN"
os.system('echo "Hello, welcome to my house BEN"|festival --tts ')
time, sleep(0.2)
当检测到人脸时,包含人脸的图像部分将被转换为灰色并传递给预测函数。这种方法将告诉人脸是否为人所知,如果人脸被识别,它还会返回 ID。假设这个人是BEN,那么 Jarvis 会说“你好,欢迎来到我的家,BEN”。现在BEN可以告诉 Jarvis 打开灯光,当唤醒词 Jarvis 被激活时,Jarvis 会做出响应。如果人脸没有被识别,那么可能是一个快递员。然后,以下命令将被执行:
os.system('echo "If you are a delivery agent then please leave the package here"|festival --tts ')
time, sleep(0.2)
os.system('echo "If you are a guest then I'm sorry I have to leave I will be back after 7pm"|festival --tts ')
time, sleep(0.2)
os.system('echo "also Kindly don't step over the grass, its freshly grown and needs some time"|festival --tts ')
time.sleep(1)
os.system('echo "Thank you !"|festival --tts ')
让 Jarvis 理解我们的声音
声音是沟通的精髓。它帮助我们非常快速地在短时间内传输大量数据。它当然比打字快且容易。因此,越来越多的公司正在努力开发能够理解人类声音和语言并据此工作的系统。这当然不容易,因为语言中存在巨大的变化;然而,我们已经取得了相当大的进步。所以,让我们在不浪费太多时间的情况下,让我们的系统准备好识别我们的声音。
因此,这里我们将使用 Google Voice 的 API。正如你可能知道的,谷歌在理解你说的话方面真的很擅长。比如,非常字面地。所以使用他们的 API 是有意义的。现在,它的工作方式非常简单。我们捕捉声音,并将其转换为文本。然后,我们比较文本是否与我们配置文件中定义的内容相似。如果匹配,则执行与之关联的 bash 命令。
首先,我们需要检查麦克风是否已连接。为此,运行以下命令:
lsusb
此命令将显示连接到 USB 的设备列表。如果你在列表中看到了你的设备,那么恭喜你,你走对了路。否则,尝试通过连接找到它,或者也许尝试使用其他硬件。
我们还需要将录音音量设置为高。为此,请在串行上输入以下命令:
alsamixer
现在,一旦 GUI 出现在屏幕上,就可以使用箭头键切换音量。
最好是亲自听一下录制的声音,而不是直接将其传给树莓派。为此,我们首先需要录制我们的声音,所以我们需要运行以下命令:
arecord -l
这将检查摄像头是否在列表中。然后,输入以下命令进行录制:
arecord -D plughw:1,0 First.wav
声音将以以下名称记录,First.wav。
现在我们还想听听我们刚才录制的声音。最简单的方法是输入以下命令:
aplay test.wav
检查声音是否正确。如果不正确,那么你可以自由地对系统进行任何调整。
一旦我们检查完声音和麦克风,就是时候安装实际的工作软件了。有一些简单的方法可以做到这一点。以下是你需要运行的命令列表:
wget –- no-check-certificate “http://goo.gl/KrwrBa” -O PiAUISuite.tar.gz
tar -xvzf PiAUISuite.tar.gz
cd PiAUISuite/Install/
sudo ./InstallAUISuite.sh
现在当你运行这个程序时,一些非常有趣的事情将会开始发生。它将开始问你各种问题。其中一些将是直接的。你可以用你的理智以是或否的形式回答它。其他问题可能非常技术性。由于这些问题可能会随时间而变化,似乎没有必要明确指出需要填写的答案,但作为一个一般性的规则——除非你真的想说不,否则就给它一个肯定回答。
完美,我们已经安装了软件。现在在你进一步使用该软件之前,让我们先写下以下程序:
import RPi.GPIO as GPIO
import time
import os
GPIO.setmode(GPIO.BCM)
LIGHT = 2
GPIO.setup(LIGHT,GPIO.OUT)
GPIO.output(LIGHT, GPIO.HIGH)
os.system('echo "LIGHTS TURNED ON "|festival --tts')
无论何时运行此程序,连接在 PIN 编号2上的灯将会点亮。同时,它将读出LIGHTS TURNED ON。将此文件保存为lighton.py:
import RPi.GPIO as GPIO
import time
import os
GPIO.setmode(GPIO.BCM)
LIGHT = 23
GPIO.setup(LIGHT,GPIO.OUT)
GPIO.output(LIGHT, GPIO.LOW)
os.system('echo "LIGHTS TURNED OFF "|festival --tts')
同样,在这个程序中,灯将会关闭,并且它会读出LIGHTS TURNED OFF。将其保存为lightoff.py:
import RPi.GPIO as GPIO
import time
Import os
GPIO.setmode(GPIO.BCM)
FAN = 22
GPIO.setup(FAN,GPIO.OUT)
GPIO.output(LIGHT, GPIO.HIGH)
os.system('echo "FAN TURNED ON "|festival --tts')
现在我们也对风扇做了同样的事情。在这个程序中,风扇将会打开;将其保存为fanon.py:
import RPi.GPIO as GPIO
import time
Import os
GPIO.setmode(GPIO.BCM)
FAN = 22
GPIO.setup(FAN,GPIO.OUT)
GPIO.output(LIGHT, GPIO.LOW)os.system('echo "FAN TURNED OFF "|festival --tts')
我不需要解释同样的事情,对吧?正如你所猜想的,保存为fanoff.py。
好的!当这一切都完成之后,然后输入以下命令来检查软件是否正确安装:
voicecommand -c
树莓派对唤醒词pi做出响应;让我们将其更改为jarvis。所有这些更改都可以在通过以下命令打开配置文件后进行:
voicecommand -e.
在那个文件中,输入你自己的命令。这里,让我们添加以下代码:
LIGHT_ON
LIGHT_OFF
FAN_ON
FAN_OFF
现在对于每个命令,定义动作。动作将是运行包含切换灯和风扇开关代码的 Python 文件。代码是基本且易于理解的。将以下内容添加到文件中:
LIGHT ON = sudo python lighton.py
LIGHT OFF = sudo python lightoff.py
FAN ON = sudo python fanon.py
FAN OFF = sudo python fanoff.py
现在,让我们看看我们完成了什么。每当你说 <q>Jarvis,开灯</q>,它就会将你的速度转换为文本,将其与它必须运行的相应程序进行比较,并执行程序中的任何操作。因此,在这个程序中,每当我说 <q>开灯</q>,灯光就会打开,其余的命令也是如此。记得让它倾听你说的话。你将不得不说出单词 <q>Jarvis</q>,这将让它对命令保持警觉并准备好倾听。
摘要
在本章中,我们了解了如何交互并让 Jarvis 根据我们的需求工作。如果本章是关于口头交流,那么下一章就是关于手势识别,在那里,通过使用先进的电容技术,你只需挥动手臂就能控制你的自动化系统。
第十五章:手势识别
从时间的开始,人类就通过手势相互交流,甚至在还没有正式语言之前。手势是主要的交流方式,这在全世界发现的古代雕塑中也是显而易见的,这些符号已经是一种非常高效地传输大量数据的方式,有时甚至比语言本身还要高效。
手势是自然的,它们可以作为一种对特定情况的反射而出现。它也可能在我们不知道的情况下无意识地发生。因此,它成为与各种设备进行交流的理想方式。然而,问题仍然存在,那就是如何?
我们在前几章中使用了视觉处理,我们可以确信,如果我们谈论手势,那么我们肯定需要在视频中识别手势进行大量的编程;此外,它还需要巨大的处理能力来实现。因此,这是不可能的。我们可以使用一组接近传感器构建一些基本的 gesture-recognition 系统。然而,可识别的手势范围将非常有限,并且使用的端口数量将是多倍的。
因此,我们需要找到一个易于操作且成本不超过其提供价值的解决方案。
本章将涵盖以下主题:
-
电场感应
-
使用 Flick HAT
-
基于手势识别的自动化
电场感应
近场感应是一个非常有趣的感应领域。准备好迎接一些有趣的内容。如果你感到有点困倦,或者注意力不集中,那么喝点咖啡吧,因为这个系统的原理可能会有些新颖。
每当有电荷存在时,都会伴随着一个相关的电场。这些电荷在空间中传播并围绕一个物体移动。当这种情况发生时,与之相关的电场具有特定的特性。这个特性将一直持续到它周围的空环境。
对于我们使用的手势识别板,它周围的感应场只有大约几厘米,所以超出这个范围的任何东西都可以忽略不计。如果附近没有东西,那么我们可以安全地假设感应到的电场模式将保持不变。然而,每当有像我们的手这样的物体进入附近,这些波就会发生畸变。这种畸变直接与物体的位置及其位置有关。通过这种畸变,我们可以感知手指的位置,并通过持续感应,我们看到正在执行的运动类型。所讨论的板看起来像这样:
板上的中心交叉区域是发射器,而在极端两侧有四个矩形结构。这些是传感元件。它们能够感知空间中波形的模式。基于此,它们可以推导出物体的 x、y 和 z 坐标。这是由名为 MGC 3130 的芯片供电的。它执行所有计算并将原始读数交付给用户,关于坐标。
使用 Flick HAT。
Flick HAT 以盾牌的形式出现,你可以简单地将其插入 Raspberry Pi 并开始使用。然而,一旦你这样做,你将不会剩下任何 GPIO 引脚。因此,为了避免这个问题,我们将使用公对母线将其连接。这将使我们能够访问其他 GPIO 引脚,然后我们可以尽情玩耍。
所以,按照以下步骤进行连接。以下是 Flick 板的引脚图:
此后,按照以下步骤进行连接:
连接完成后,只需上传此代码并查看会发生什么:
import signal
import flicklib
import time
def message(value):
print value
@flicklib.move()
def move(x, y, z):
global xyztxt
xyztxt = '{:5.3f} {:5.3f} {:5.3f}'.format(x,y,z)
@flicklib.flick()
def flick(start,finish):
global flicktxt
flicktxt = 'FLICK-' + start[0].upper() + finish[0].upper()
message(flicktxt)
def main():
global xyztxt
global flicktxt
xyztxt = ''
flicktxt = ''
flickcount = 0
while True:
xyztxt = ''
if len(flicktxt) > 0 and flickcount < 5:
flickcount += 1
else:
flicktxt = ''
flickcount = 0
main()
现在你已经上传了代码,让我们来了解一下这段代码实际上在做什么。
我们使用一个名为 import flicklib 的库,这是该板的制造商提供的。这个库中的函数将在本章中用于与 Flick 板通信并获取数据。
def message(value):
print value
在这里,我们定义了一个名为 message(value) 的函数,它的作用是简单地打印传递给函数的任何值:
@flicklib.move()
这有一个特殊的装饰器概念。根据定义,装饰器是一个函数,它接受另一个函数并扩展后者的行为,而不需要显式修改它。在代码的前一行,我们声明它是一个装饰器 @。
它有一个特殊的工作:动态定义程序中的任何函数。用简单的话说,使用这种方法定义的函数可以根据用户如何定义它而有所不同。
move() 函数将进一步由其后定义的函数补充。这类函数被称为嵌套函数。也就是说,函数在函数内部:
def move(x, y, z):
global xyztxt
xyztxt = '{:5.3f} {:5.3f} {:5.3f}'.format(x,y,z)
在这里,我们定义了一个名为 move() 的函数,它具有 x、y 和 z 作为参数。在函数内部,我们定义了一个名为 xyztxt 的全局变量;现在,xyztxt 的值将是一个五位数字,小数点后有三位。我们是如何知道这个的?正如你所见,我们使用了一个名为 format() 的函数。这个函数的作用是按照用户请求的方式格式化给定变量的值。我们在这里声明值为 {:5.3f}。其中 :5 表示它将是一个五位数字,而 3f 表示小数点将在三位数字之后。因此,格式将是 xxx.xx:
def flick(start,finish):
global flicktxt
flicktxt = 'FLICK-' + start[0].upper() + finish[0].upper()
message(flicktxt)
在这里,我们定义了一个名为flick(start, finish)的函数。它有两个参数:start和finish。使用flicktxt = 'FLICK-' + start[0].upper() + finish[0].upper()这一行,这是根据手势板识别的字符进行切片。如果检测到南-北滑动,那么start将得到南,而finish是北。现在我们只使用单词的第一个字符:
global xyztxt
global flicktxt
我们再次在全局范围内定义了名为xyztxt和flicktxt的变量。之前,我们是在函数中定义它们的。因此,在主程序中定义它们对我们来说非常重要:
if len(flicktxt) > 0 and flickcount < 5:
flickcount += 1
else:
flicktxt = ''
flickcount = 0
当检测到手势时,flicktxt变量将获得与手势对应的值。如果没有检测到手势,则flicktxt将被留空。一个名为flickcount的变量将计算其被滑动的次数。如果值超出了指定的范围,则使用flicktxt = ''这一行将flicktxt清空为空字符串,并将flickcount设置为 0。
最终的输出将是一个提供给用户的文本,说明手是从哪个方向滑动的。
基于手势识别的自动化
现在我们已经根据以下图示进行了连接:
让我们继续上传以下代码:
import signal
import flicklib
import time
import RPi.GPIO as GPIO
GIPO.setmode(GPIO.BCM)
GPIO.setup(light, GPIO.OUT)
GPIO.setup(fan,GPIO.OUT)
pwm = GPIO.PWM(fan,100)
def message(value):
print value
@flicklib.move()
def move(x, y, z):
global xyztxt
xyztxt = '{:5.3f} {:5.3f} {:5.3f}'.format(x,y,z)
@flicklib.flick()
def flick(start,finish):
global flicktxt
flicktxt = 'FLICK-' + start[0].upper() + finish[0].upper()
message(flicktxt)
def main():
global xyztxt
global flicktxt
xyztxt = ''
flicktxt = ''
flickcount = 0
dc_inc = 0
dc_dec = 0
while True:
pwm.start(0)
xyztxt = ' '
if len(flicktxt) > 0 and flickcount < 5:
flickcount += 1
else:
flicktxt = ''
flickcount = 0
if flicktxt ==”FLICK-WE”:
GPIO.output(light,GPIO.LOW)
if flicktxt ==”FLICK-EW”:
GPIO.output(light,GPIO.HIGH)
if flicktxt ==”FLICK-SN”:
if dc_inc < 100:
dc_inc = dc_inc + 10
pwm.changeDutyCycle(dc_inc)
else:
Dc_inc = 10
if flicktxt ==”FLICK-NS”:
if dc_inc >0:
dc_dec = dc_dec - 10
pwm.changeDutyCycle(dc_dec)
main()
这个程序是在之前程序的基础上增加的,因为我们总是有一些使用通过手势板接收到的数据来打开或关闭灯光的附加功能。
与之前的程序类似,我们正在以滑动方向的形式接收板上的手势,并使用简单的条件来关闭或打开灯光。那么,让我们看看有哪些新增内容:
if flicktxt ==”FLICK-WE”:
GPIO.output(light,GPIO.LOW)
第一个条件很简单。我们正在将flicktxt的值与一个给定变量进行比较,在我们的例子中是FLICK-WE,其中WE代表从西到东。所以当我们从西向东滑动,或者说,当我们从左向右滑动时,灯光将会关闭:
if flicktxt ==”FLICK-EW”:
GPIO.output(light,GPIO.HIGH)
与之前一样,我们再次接收一个名为FLICK-EW的变量,它代表从东向西的手势。它的作用是,每当我们将手从东向西或从右向左滑动时,灯光将会打开:
if flicktxt ==”FLICK-SN”:
if dc_inc <= 100:
dc_inc = dc_inc + 20
pwm.changeDutyCycle(dc_inc)
现在我们已经将调光器与风扇一起放置,以控制风扇的速度;因此,我们需要给出一个与所需驱动速度相对应的 PWM 信号。现在,每当用户将手从南向北或从下向上滑动时,条件if dc_inc <100将检查dc_inc的值是否小于或等于100。如果是,那么它将dc_inc的值增加20个单位。通过使用ChangeDutyCycle()函数,我们向调光器提供不同的占空比,从而改变风扇的整体速度。每次向上滑动风扇值时,它将增加 20%。
else:
Dc_inc = 10
if flicktxt ==”FLICK-NS”:
if dc_inc >0:
dc_dec = dc_dec - 10
pwm.changeDutyCycle(dc_dec)
摘要
在本章中,我们能够理解通过电场检测来识别手势的概念。我们还了解到使用手势控制板控制家居用品是多么简单。我们将在下一章介绍机器学习部分。
第十六章:机器学习
从其原始时期到如今,机器人和计算机都在被编程执行一系列活动。这些活动可能非常庞大。因此,为了开发复杂的程序,需要大量的软件工程师日夜工作以实现特定的功能。当问题定义得很好时,这是可行的。但是,当问题也非常复杂时怎么办呢?
学习是我们人类之所以成为我们的原因。我们的经验塑造了我们以更好地、更有效地适应情况。每次我们做某事,我们就知道得更多。这使得我们在一段时间内更好地完成这项任务。俗话说,熟能生巧,而通过反复做事情来学习使我们变得更好。
然而,让我们退一步来定义什么是学习?我想引用谷歌的定义,它是通过学习、经验或被教导获得的知识。因此,学习基本上是一种从我们的周围环境中获取信息以理解过程及其本质的方式。
现在,你可能正在想,等等,我们不是在制作守卫机器人的前几章中使我们的系统学习了很多视觉数据吗?你的想法完全正确。然而,学习可以通过不同的方式进行。可能适用于一种类型的问题的方法可能对其他类型的问题毫无用处。因此,存在各种类型的机器学习算法及其原理。在本章中,我们将重点关注一个名为k-最近邻的算法。它被称为懒惰算法。我个人非常喜欢这个算法用于分类。为什么?因为从技术上讲,没有训练阶段。怎么呢?
k-最近邻实际上是一个智能算法。它不是对提供的数据进行回归计算并进行大量的数学计算,而是简单地从提供的数据集中获取结构化数据。每当有新的数据用于预测时,它就会根据用户提供的数据的分类,在数据库中简单地搜索与用户提供的数据最接近的k个匹配项。因此,在本章中,我们将学习这个算法是如何工作的,以及我们如何使用它来使我们的家庭智能化。
在本章中,我们将涵盖以下主题:
-
制作数据集
-
使用数据集进行预测
-
使你的家庭学会学习
-
家庭学习和自动化
制作数据集
就像在第十章制作守卫机器人中一样,我们使用了多张图片来训练模型,以确定图像中的物体是人还是其他东西。以非常相似的方式,我们不得不制作一个虚拟数据集,这样机器学习算法就可以根据这些数据预测应该做什么。
要创建一个数据集,我们需要了解正在考虑哪些数据。在本章中,我们将基于时间和温度创建一个机器学习算法,以预测风扇应该开启还是关闭。因此,至少有两件事应该由我们提供给系统,一个是Temperature,另一个是Time,以便进行预测。但有一点要记住,我们正在谈论一个监督学习算法,因此为了训练模型,我们还需要提供Temperature和Time的输出到风扇的状态。在这里,风扇的状态将是开启或关闭。因此,我们可以用0或1来表示它。现在让我们自己创建一个数据集。
现在,要创建一个数据集,你只需打开 Microsoft Excel 并按照以下方式开始编写数据集:
拥有一个包含超过 20 组数据的数据集总是更好的。此外,数据必须具有独特的特征,而不是随机数据。例如,在前面的例子中,你可以看到当温度为28时,在12.44时风扇是开启的;然而,在相同的时间,当时间是12.13且温度为21时,风扇是关闭的。
一旦你创建了一个数据集,你必须以dataset的名称将其保存为 CSV 格式。可能会有一些用户不会使用 Microsoft Excel,在这种情况下,你可以在文本编辑器中以相同的格式写入数据,最后以 CSV 格式保存。
一旦你有了dataset.csv文件,你必须将它们复制到将要保存即将到来的代码的地方。一旦你完成,我们就可以继续下一步。
记住,数据的质量越好,学习过程就越好。所以你可能需要花一些时间,仔细制作你的数据集,以确保它有意义。
使用数据集进行预测
不多说了,让我们看看下面的代码:
import numpy as np
import pandas as pd
from sklearn.neighbors import KNeighborsClassifier
knn = KNeighborsClassifier(n_neighbors=5)
data = pd.read_csv('dataset.csv')
x = np.array(data[['Time', 'Temp']])
y = np.array(data[['State']]).ravel()
knn.fit(x,y)
time = raw_input("Enter time")
temp = raw_input("Enter temp")
data =. []
data.append(float(time))
data.append(float(temp))
a = knn.predict([data])
print(a[0])}
那么,让我们看看我们在做什么:
import numpy as np
我们将numpy导入到我们的程序中;这有助于我们处理列表和矩阵:
import pandas as pd
这里,我们导入了一个名为pandas的库;这有助于我们读取以逗号分隔的值文件,换句话说,CSV 文件。我们将使用 CSV 文件来存储我们的数据,并在学习过程中访问它:
from sklearn.neighbors import KNeighborsClassifier
这里,我们从sklearn库中导入KneighborsClassifier。sklearn本身是一个庞大的库;因此,我们只导入其中的一部分,因为我们不会在这个程序中使用它:
knn = KNeighborsClassifier(n_neighbors=5)
这里,我们给变量knn赋值,其值将是KNeighborsClassifer(n_neighbors =5);这意味着我们正在使用带有参数n_neighbors=5的KneighborsClassifer()函数。这个参数告诉KneighborsClassifer函数,在算法中我们将有五个邻居。进一步来说,使用这个声明,整个函数可以通过knn来调用:
data = pd.read_csv('dataset.csv')
在这里,我们正在为名为data的变量赋值,传递的值是pd.read_csv('dataset.csv');这意味着每当调用data时,就会调用来自pandas库的pd.read_csv()函数。该函数的目的是从 CSV 文件中读取数据。在这里,传递的参数是dataset.csv;因此,它指示函数将读取哪些数据。在我们的例子中,它将从一个名为dataset.csv的文件中读取:
x = np.array(data[['Time', 'Temp']])
在下一行中,我们正在将值传递给变量x,传递的值是np.array(data[['Time', 'Temp']])。现在,使用numpy库的np.array函数创建一个数组。这个数组将按Time和Temp的名称存储数据:
y = np.array(data[['State']]).ravel()
就像之前一样,我们使用numpy库的.ravel()函数将State存储在一个数组中。这样做是为了使数学函数可以在两个数组——x和y之间进行操作:
knn.fit(x,y)
在这短短的一行中,我们使用knn库中的fit()函数,它的作用是使用x作为主要数据,y作为输出结果数据来拟合模型:
time = raw_input("Enter time")
temp = raw_input("Enter temp")
在这一行中,我们请求用户的数据。在第一行,我们将打印Enter time,然后等待用户输入时间。用户输入时间后,它将被存储在名为time的变量中。一旦完成,然后就会进入下一行;代码将打印Enter temp。一旦提示用户,它将等待收集数据。一旦用户收集到数据,它将把数据存储在名为temp的变量中:
data =. []
在这里,我们创建了一个名为data的空列表;这个列表将用于计算输出结果的状态。因为所有的机器学习算法都是在列表数据类型上工作的。因此,输入必须以列表的形式给出,以便进行决策:
data.append(float(time))
data.append(float(temp))
在这里,我们将数据添加到我们刚刚创建的名为data的列表中。首先,添加time,然后是temp:
a = knn.predict([data])
一旦完成,将使用名为predict的knn算法中的函数来根据名为data的列表预测输出。预测算法的输出被检索到一个名为a的变量中:
print(a[0])
最后,一旦完成预测,我们就会读取a的值,并记住所有数据输入/输出都是以列表的形式发生的。因此,预测算法给出的数据输出也将以列表格式。因此,我们正在打印列表的第一个元素。
这个输出将预测根据用户提供的数据集,风扇将处于哪种状态。所以,请继续提供一个温度和时间,让系统为您预测结果。看看它是否工作正常。如果不正常,那么尝试向 CSV 文件中添加更多的数据集,或者看看数据集中的值是否有意义。我相信你最终会得到一个出色的预测系统。
让你的家学会
一旦完成这一宪法,就可以按照下面的图示进行接线:
一旦设置好,就轮到我们将在我们的树莓派上编写以下代码:
import Adafruit_DHT
import datetime
import RPi.GPIO as GPIO
import time
import numpy as np
import pandas as pd
import Adafruit_DHT
from sklearn.neighbors import KNeighborsClassifier
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
fan = 22
light = 23
sw1 = 13
sw2 = 14
GPIO.setup(led1,GPIO.OUT)
GPIO.setup(led2,GPIO.OUT)
GPIO.setup(sw1,GPIO.IN)
GPIO.setup(sw2,GPIO.IN)
sensor = 11
pin = 2
f = open("dataset.csv","a+")
count = 0
while count < 50:
data = ""
H = datetime.datetime.now().strftime('%H')
M = datetime.datetime.now().strftime('%M')
data = str(H)+"."+str(M)
humidity,temperature = Adafruit_DHT.read_retry(sensor,pin)
data = data + "," + str(temperature)
prev_state = state
if (GPIO.input(sw1) == 0) and (GPIO.input(sw2) == 0):
state = 0
GPIO.output(light,GPIO.LOW)
GPIO.output(fan,GPIO.LOW)
elif (GPIO.input(sw1) == 0) and (GPIO.input(sw2) == 1):
state = 1
GPIO.output(light,GPIO.HIGH)
GPIO.output(fan,GPIO.LOW)
elif (GPIO.input(sw1) == 1) and (GPIO.input(sw2) == 0):
state = 2
GPIO.output(light,GPIO.LOW)
GPIO.output(fan,GPIO.HIGH)
elif (GPIO.input(sw1) == 1) and (GPIO.input(sw2) == 1):
state = 3
GPIO.output(light,GPIO.HIGH)
GPIO.output(fan,GPIO.HIGH)
data = ","+str(state)
if prev_state =! state:
f.write(data)
count = count+1
f.close()
现在,让我们看看我们在这里做了什么:
f = open("dataset.csv","a+")
在这一行代码中,我们将open("dataset.csv", "a+")的值赋给了变量f。之后,open()函数将打开传递给其参数的文件,在我们的例子中是dataset.csv;参数a+表示在 CSV 文件的末尾追加值。因此,这一行将打开dataset.csv文件,并添加我们稍后传递的值:
data = ""
我们通过名为data的名称声明了一个空字符串:
data = str(H)+"."+str(M)
我们正在将小时和分钟的值添加到字符串中,中间用点分隔以示区别。因此,数据将看起来像HH.MM:
humidity,temperature = Adafruit_DHT.read_retry(sensor,pin)
我们使用这一行来读取 DHT 11 传感器的湿度和温度读数,并将这些值传递给变量humidity和temperature:
data = data + "," + str(temperature)
一旦读取数据,我们将温度也添加到变量data中。因此,现在数据将看起来像这样HH.MM和TT.TT:
if (GPIO.input(sw1) == 0) and (GPIO.input(sw2) == 0):
state = 0
elif (GPIO.input(sw1) == 0) and (GPIO.input(sw2) == 1):
state = 1
elif (GPIO.input(sw1) == 1) and (GPIO.input(sw2) == 0):
state = 2
elif (GPIO.input(sw1) == 1) and (GPIO.input(sw2) == 1):
state = 3
在这里,我们定义了不同类型的与开关组合相对应的状态。相应的表格如下:
| 开关 1 | 开关 2 | 状态 |
|---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 2 |
1 | 1 | 3 |
因此,通过状态值,我们可以理解哪个开关会被打开,哪个会被关闭:
data = ","+str(state)
最后,将状态值也添加到名为data的变量中。现在,数据将看起来像HH.MM,TT.TT和S:
f.write(data)
现在,使用write()函数,我们将数据的值写入我们之前通过值f定义的文件。
因此,每当单个开关打开或关闭时,数据都会被收集,并且值会记录在该文件中的时间戳。然后,可以使用这些数据来预测在任何给定时间家里的状态,而不需要任何干预:
if prev_state =! state:
f.write(data)
count = count+1
在这里,我们正在将状态与prev_state进行比较,正如你在我们的程序中看到的那样。上一个状态是在我们程序开始时计算的。所以,如果系统状态有任何变化,那么prev_state和state的值就会不同。这将导致if语句为真。当这种情况发生时,数据将通过write()函数写入我们的文件。传递的参数是需要写入的值。最后,计数器的值增加1。
一旦这个程序运行几个小时或者可能是几天,它就会收集一些关于你的灯光和风扇开关模式的有用数据。然后,这些数据可以被检索到之前的程序中,它将能够根据时间和温度做出自己的决定。
家庭学习和自动化
既然在前一节我们已经了解了学习是如何工作的,现在是时候使用这个概念来制作一个能够自动理解我们如何运作和做决定的机器人了。基于我们的决定,系统将判断应该做什么。但这次,我们不是通过用户提供一组数据,而是让这个程序自己生成数据。一旦数据看起来足够它自己运作,那么,不多做解释,让我们直接进入正题:
import Adafruit_DHT
import datetime
import RPi.GPIO as GPIO
import time
import numpy as np
import pandas as pd
from sklearn.neighbors import KNeighborsClassifier
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
light = 22
fan = 23
sw1 = 13
sw2 = 14
GPIO.setup(light,GPIO.OUT)
GPIO.setup(fan,GPIO.OUT)
GPIO.setup(sw1,GPIO.IN)
GPIO.setup(sw2,GPIO.IN)
sensor = 11
pin = 2
f = open("dataset.csv","a+")
count = 0
while count < 200:
data = ""
H = datetime.datetime.now().strftime('%H')
M = datetime.datetime.now().strftime('%M')
data = str(H)+"."+str(M)
humidity,temperature = Adafruit_DHT.read_retry(sensor,pin)
data = data + "," + str(temperature)
prev_state = state
if (GPIO.input(sw1) == 0) and (GPIO.input(sw2) == 0):
state = 0
GPIO.output(light,GPIO.LOW)
GPIO.output(fan,GPIO.LOW)
elif (GPIO.input(sw1) == 0) and (GPIO.input(sw2) == 1):
state = 1
GPIO.output(light,GPIO.HIGH)
GPIO.output(fan,GPIO.LOW)
elif (GPIO.input(sw1) == 1) and (GPIO.input(sw2) == 0):
state = 2
GPIO.output(light,GPIO.LOW)
GPIO.output(fan,GPIO.HIGH)
elif (GPIO.input(sw1) == 1) and (GPIO.input(sw2) == 1):
state = 3
GPIO.output(light,GPIO.HIGH)
GPIO.output(fan,GPIO.HIGH)
data = ","+str(state)
if prev_state =! state:
f.write(data)
count = count+1
Test_set = []
knn = KNeighborsClassifier(n_neighbors=5)
data = pd.read_csv('dataset.csv')
X = np.array(data[['Time', 'Temp']])
y = np.array(data[['State']]).ravel()
knn.fit(X,y)
While Count > 200:
time = ""
H = datetime.datetime.now().strftime('%H')
M = datetime.datetime.now().strftime('%M')
time = float(str(H)+"."+str(M))
humidity, temperature = Adafruit_DHT.read_retry(sensor, pin)
temp = int(temperature)
test_set.append(time)
test_set.append(temp)
a = knn.predict([test_set]])
Out = a[0]
If out == 0:
GPIO.output(light,GPIO.LOW)
GPIO.output(fan,GPIO.LOW)
If out == 1:
GPIO.output(light,GPIO.LOW)
GPIO.output(fan,GPIO.HIGH)
If out == 2:
GPIO.output(light,GPIO.HIGH)
GPIO.output(fan,GPIO.LOW)
If out == 3:
GPIO.output(light,GPIO.HIGH)
GPIO.output(fan,GPIO.HIGH)
现在我们来看看我们在这里做了什么。在这个程序中,while count < 200:条件下的程序的第一部分与我们之前所做的代码完全相同。所以它只是在按照用户的要求做事,同时,它正在从用户那里获取值以了解他们的工作行为:
while count > 200:
此后,当计数器超过200时,将开始执行代码的第二部分,这部分代码位于前面的循环中:
time = ""
在这一行中,我们创建了一个名为time的空字符串,我们将在这里存储时间的值:
H = datetime.datetime.now().strftime('%H')
M = datetime.datetime.now().strftime('%M')
我们将时间的值存储在名为H和M的变量中:
time = float(str(H)+"."+str(M))
我们现在将时间的值存储在字符串time中。这将包括小时和分钟:
temp = int(temperature)
为了方便计算和减少系统计算负载,我们正在减小温度变量的尺寸。我们通过删除小数点来实现这一点。为了做到这一点,TT.TT;我们只是消除了小数点,将其转换为整数。这是通过名为int()的函数完成的。温度的整数值将存储在名为temp的变量中:
test_set.append(time)
test_set.append(temp)
在这里,我们将时间和温度的值添加到名为test_set的列表中,如果你查看程序,你会看到程序中间声明了一个空集合。所以,现在这个test_set包含了time和temp的值,这些值可以被预测算法进一步用于预测状态:
a = knn.predict([test_set]])
使用knn函数中的简单函数predict(),我们可以预测状态值。我们所需做的只是将数据或test_set列表传递给预测函数。该函数的输出将是一个列表,将被存储在一个名为a的变量中:
Out = a[0]
Out的值将被设置为列表a的第一个元素:
If out == 0:
GPIO.output(light,GPIO.LOW)
GPIO.output(fan,GPIO.LOW)
If out == 1:
GPIO.output(light,GPIO.LOW)
GPIO.output(fan,GPIO.HIGH)
If out == 2:
GPIO.output(light,GPIO.HIGH)
GPIO.output(fan,GPIO.LOW)
If out == 3:
GPIO.output(light,GPIO.HIGH)
GPIO.output(fan,GPIO.HIGH)
使用前面的代码块,我们能够根据算法预测的状态来选择性地打开灯和风扇。因此,使用这种方法,程序能够在没有你的干预下自动预测并打开或关闭灯和风扇。
摘要
在本章中,我们了解了即使不学习也能如何进行机器学习。我们了解了如何提供数据集,以及我们可以如何使用现有系统创建一个新的数据集。最后,我们了解了系统如何无缝地收集数据、从数据中学习,并最终提供输入。想要构建一个轮式自平衡机器人?那么,下一章见!
第十七章:手势控制机器人车辆
在前面的几章中,你已经学到了很多关于传感器的内容——它们的作用以及它们是如何工作的。这个星球上的传感器数量简直无法计数。它一直在增加,当你阅读这本书的时候,可能某个国家正在推出一些新的传感器。这可能会让你想,你是否需要每天学习市场上出现的所有这些传感器?
幸运的是,正如你所学的,这些传感器在某种程度上是相似的,并且基于有限的协议集。这使得与新的传感器合作变得容易。你可能认为我们已经涵盖了大部分的基本传感器。然而,这并不准确——因为我们还没有学习到与机器人平衡最重要的传感器。这些是加速度计和陀螺仪。这两个传感器不仅存在于所有与平衡有关的机器人中,而且它们也进入了我们的移动设备。所以,现在让我们来了解这些传感器的基本知识。
加速度计和陀螺仪
让我们先从加速度计开始。正如其名所示,这些设备是用来测量任何物体上的加速度的。因此,每当任何物体上有加速度时,加速度计就有工作来感知这种加速度并提供相应的读数。
陀螺仪是一种利用地球重力帮助确定机器人方向的设备。加速度计是一种设计用来测量非重力加速度的设备。每当有加速度时,加速度计中的晶体就会激发并提供与加速度相对应的电压。
这些设备之间的主要区别很简单:陀螺仪可以感知旋转,而加速度计则不能。从某种意义上说,加速度计可以测量静止物体相对于地球表面的方向。当向特定方向加速时,加速度计无法区分这是由于地球的重力提供的加速度还是由于加速本身。
在本章中,我们将使用一个名为 MPU 6050 的传感器,这是具有加速度计和陀螺仪的常见传感器之一,它们集成在一个单一包中。
上图是它的照片。正如你所见,它有SCL和SDA线。因此,你可以确信这个传感器是基于 I2C 协议工作的。这很酷,因为我们已经与 I2C 类型的传感器有了良好的合作经验。
接口惯性测量单元
MPU 6050 是一个三轴加速度计和三轴陀螺仪。它基于 I2C 协议工作,但这个传感器有趣的地方在于它为不同的传感器有不同的地址。你刚才说什么?不同传感器的不同地址。难道这不是一个单一的传感器吗?
嗯,是的,但是如果你看到传感器的内部结构,你会发现对于每个不同的轴,数据都存储在不同的寄存器中,我们可以从那里获取所需的数据。因此,如果你只需要读取加速度计的 x 轴,你可以这样做,而不是获取一长串不同的值。现在连接这个传感器非常简单。你所要做的就是使用 Raspberry Pi 给它供电,并简单地连接 I2C 引脚,如图所示:
一旦完成接线,继续上传以下代码。一旦这样做,运行并查看你得到的结果:
import smbus
from time import sleep
PWR_MGMT_1 = 0x6B
SMPLRT_DIV = 0x19
CONFIG = 0x1A
GYRO_CONFIG = 0x1B
INT_ENABLE = 0x38
ACCEL_XOUT_H = 0x3B
ACCEL_YOUT_H = 0x3D
ACCEL_ZOUT_H = 0x3F
GYRO_XOUT_H = 0x43
GYRO_YOUT_H = 0x45
GYRO_ZOUT_H = 0x47
def MPU_Init():
bus.write_byte_data(Device_Address, SMPLRT_DIV, 7)
bus.write_byte_data(Device_Address, PWR_MGMT_1, 1)
bus.write_byte_data(Device_Address, CONFIG, 0)
bus.write_byte_data(Device_Address, GYRO_CONFIG, 24)
bus.write_byte_data(Device_Address, INT_ENABLE, 1)
def read_raw_data(addr):
high = bus.read_byte_data(Device_Address, addr)
low = bus.read_byte_data(Device_Address, addr+1)
value = ((high << 8) | low)
if(value > 32768):
value = value - 65536
return value
bus = smbus.SMBus(1)
Device_Address = 0x68
MPU_Init()
print (" Reading Data of Gyroscope and Accelerometer")
while True:
Ax = read_raw_data(ACCEL_XOUT_H)
Ay = read_raw_data(ACCEL_YOUT_H)
Az = read_raw_data(ACCEL_ZOUT_H)
Gx = read_raw_data(GYRO_XOUT_H)
Gy = read_raw_data(GYRO_YOUT_H)
Gz = read_raw_data(GYRO_ZOUT_H)
print(“Ax=”+str(Ax)+”Ay=”+str(Ay)+"Az="+str(Az)+“Gx=”+str(Gx)+”Gy=”+str(Gy)+"Gz="+str(Gz))
现在是时候看看我们做了什么:
def MPU_Init():
bus.write_byte_data(Device_Address, SMPLRT_DIV, 7)
bus.write_byte_data(Device_Address, PWR_MGMT_1, 1)
bus.write_byte_data(Device_Address, CONFIG, 0)
bus.write_byte_data(Device_Address, GYRO_CONFIG, 24)
bus.write_byte_data(Device_Address, INT_ENABLE, 1)
在这些行中,我们定义了一个名为MPU_Init()的函数。这个函数将被用来初始化惯性测量单元(IMU)传感器。在这个函数中,我们使用来自bus库的write_byte_data()方法。它的作用是将数据写入在参数中声明的特定寄存器。使用此函数写入的数据大小为字节。现在我们声明了Device_Address。也就是说,写入的数据将位于连接的设备地址上。第二个参数SMPLRT_DIV将告诉寄存器的地址是十六进制的。因此,SMPLRT_DIV的值将用作地址。最后一个参数是需要写入的数据,在我们的例子中是7。同样,以下是将传递给设备的值:
-
PWR_MGMT_1:1 -
CONFIG:0 -
GYRO_CONFIG:24 -
INT_ENABLE:1
这些值是设置传感器所必需的。这些值的作用对我们目前来说并不重要。然而,如果你想知道的话,你可以使用 IMU MPU 6050 的数据表,以详细了解所有这些寄存器都在做什么:
def read_raw_data(addr):
high = bus.read_byte_data(Device_Address, addr)
low = bus.read_byte_data(Device_Address, addr+1)
在这里,我们定义了一个名为read_raw_data(addr)的函数,它从设备读取地址,然后我们读取地址的寄存器是addr用于high变量,addr+1用于low变量。
value = ((high << 8) | low)
一旦获取了数据,它就会通过以下语句value = ((high << 8) | low)进行处理,这个操作是将high变量的值左移,因此如果high变量的二进制数据是10111011,(high << 8)会将这个值转换为1011101100000000。完成这个操作后,与操作符会将八个数字的值添加到新计算出的high变量的值上。使得输出成为一个 16 位整数。现在你可能想知道我们为什么要这样做?
MPU 6050 为任何读数提供的值是 16 位的。然而,MPU 6050 的寄存器大小是 8 位的。现在 MPU 6050 所做的是根据位的位置将任何传感器的值存储在寄存器中。也就是说,最重要的 8 位将存储在第一个地址,其余的最不重要的 8 位将存储在下一个地址,即addr+1:
if(value > 32768):
value = value - 65535
return value
正如我们在上一行所理解的,传感器的输出数据将以 16 位格式。当传感器处于正常位置时,这个读数将位于中间某个位置。如果值超过中间读数,我们将通过65535减去该值,因此为每个超过中间读数的值提供一个负值。这将给我们一个方向感,而不需要做太多的脑力劳动。如果值是正的,倾斜方向是向一个方向,如果是负的,则是向另一个方向。
acc_x = read_raw_data(ACCEL_XOUT_H)
acc_y = read_raw_data(ACCEL_YOUT_H)
acc_z = read_raw_data(ACCEL_ZOUT_H)
gyro_x = read_raw_data(GYRO_XOUT_H)
gyro_y = read_raw_data(GYRO_YOUT_H)
gyro_z = read_raw_data(GYRO_ZOUT_H)
现在我们已经定义了名为read_raw_data()的函数,我们可以开始从不同的传感器读取数据。这些传感器会将数据保存到不同的地址。我们之前提到的就是这些地址。因此,通过将这些数据传递给函数,我们可以了解每个传感器的读数。所有这些读数都在被计算,并且数据被存储在一个变量中。
print(“Ax=”+str(Ax)+”Ay=”+str(Ay)+"Az="+str(Az)+“Gx=”+str(Gx)+”Gy=”+str(Gy)+"Gz="+str(Gz))
在这一行,我们只是简单地打印出每个传感器对应的每个变量的值。
现在你运行这段代码,你会看到每个传感器的值都会显示出来,并且当你改变方向或加速度时,这些值会发生变化。探索这些读数以及它们如何随着运动而变化。我相信这会给你关于传感器的很多想法。所以,继续前进,享受吧!
手势控制汽车
我们已经看到了如何从 IMU 中提取数据。现在是时候将数据投入使用。在本章中,我们将通过手的倾斜来控制我们的机器人车辆。所以本质上,它将是一个手势控制的机器人车辆。现在为了做到这一点,让我们继续并按照以下方式连接 Raspberry Pi:
确保为传感器连接足够长的线,在任何地方都不要超过 1 米,并将其用作你车辆的遥控器。一旦连接,上传以下代码:
import smbus
from time import sleep
import RPi.GPIO as GPIO
int1 = 12
int2 = 16
int3 = 18
int4 = 15
GPIO.setup(int1, GPIO.OUT)
GPIO.setup(int2, GPIO.OUT)
GPIO.setup(int3, GPIO.OUT)
GPIO.setup(int4, GPIO.OUT)
PWM1 = GPIO.PWM(12, 100)
PWM2 = GPIO.PWM(16, 100)
PWM3 = GPIO.PWM(18, 100)
PWM4 = GPIO.PWM(15, 100)
PWM1.start(0)
PWM2.start(0)
PWM3.start(0)
PWM4.start(0)
PWR_MGMT_1 = 0x6B
SMPLRT_DIV = 0x19
CONFIG = 0x1A
GYRO_CONFIG = 0x1B
INT_ENABLE = 0x38
ACCEL_XOUT_H = 0x3B
ACCEL_YOUT_H = 0x3D
ACCEL_ZOUT_H = 0x3F
GYRO_XOUT_H = 0x43
GYRO_YOUT_H = 0x45
GYRO_ZOUT_H = 0x47
def MPU_Init():
bus.write_byte_data(Device_Address, SMPLRT_DIV, 7)
bus.write_byte_data(Device_Address, PWR_MGMT_1, 1)
bus.write_byte_data(Device_Address, CONFIG, 0)
bus.write_byte_data(Device_Address, GYRO_CONFIG, 24)
bus.write_byte_data(Device_Address, INT_ENABLE, 1)
def read_raw_data(addr):
high = bus.read_byte_data(Device_Address, addr)
low = bus.read_byte_data(Device_Address, addr+1)
value = ((high << 8) | low)
if(value > 32768):
value = value - 65536
return value
bus = smbus.SMBus(1)
Device_Address = 0x68
MPU_Init()
while True:
acc_x = read_raw_data(ACCEL_XOUT_H)
acc_y = read_raw_data(ACCEL_YOUT_H)
acc_z = read_raw_data(ACCEL_ZOUT_H)
gyro_x = read_raw_data(GYRO_XOUT_H)
gyro_y = read_raw_data(GYRO_YOUT_H)
gyro_z = read_raw_data(GYRO_ZOUT_H)
Ax = (gyro_x/327)
Ay = (gyro_y/327)
for Ax > 20:
PWM1.changeDutyCycle(Ax)
PWM3.changeDutyCycle(Ax)
for Ax < -20:
PWM2.changeDutyCycle(Ax)
PWM4.changeDutyCycle(Ax)
for Ay > 20:
PWM1.changeDutyCycle(Ax)
PWM4.changeDutyCycle(Ax)
for Ay < -20:
PWM2.changeDutyCycle(Ax)
PWM3.changeDutyCycle(Ax)
正如你所看到的,代码在到达while True循环之前几乎是一样的。之后我们做了一点小技巧。那么让我们看看它是什么。
Ax = (gyro_x/327)
Ay = (gyro_y/327)
当加速度计处于中间或水平放置的位置时,其值将接近32768,这将是一个中心读取值。因此,为了找出我们向前或向后倾斜了多少百分比,我们使用这一行。为了做到这一点,我们将其除以327。它所做的就是,在0至100之间给出一个读取值。例如,如果原始读取值是gryo_x = 21000,则gyro_x/327 = 64.22。现在64.22将是倾斜的百分比读取值。这对于我们来说是一个重要的步骤,因为这个读取值将帮助我们确定必须提供给电机驱动器的DutyCycle。
for Ax < -20:
PWM2.changeDutyCycle(Ax)
PWM4.changeDutyCycle(Ax)
这一步非常简单,我们所做的是为读取值设置一个阈值,超过这个阈值就会向电机提供 PWM 信号。提供给电机的 PWM 的DutyCycle将与倾斜角度成正比。
for Ay < -20:
PWM2.changeDutyCycle(Ax)
PWM3.changeDutyCycle(Ax)
就像我们之前做的那样,如果倾斜方向相反,那么对应后向方向的电机驱动引脚将被置为高电平,使其向后移动。速度与之前一样,将成正比于倾斜角度。
for Ay > 20:
PWM1.changeDutyCycle(Ax)
PWM4.changeDutyCycle(Ax)
在这一行,我们使机器人车辆向一个方向转弯。和之前一样,由于倾斜的百分比值,DutyCycle会改变,从而改变转弯速度。正如你所看到的,机器人的车轮会相对转动,因此转弯将在其轴上完成。
有时在使用各种库时,原始输出可能会变化。为了确保你的代码运行良好,首先查看你得到的原始读取值。一旦你得到读取值,就在一张纸上写下这些值:平稳线读取值、最大读取值和最小读取值。这些读取值可能会根据传感器的材质而变化。(有许多假冒产品不会给出相同的读取值。)一旦你看到了整个读取范围,你就可以对算法进行所需的调整,使其工作。
试试看,看看你如何仅通过手势来控制它。
使其更加先进
在上一章中,我们看到了我们如何仅基于我们的手势使汽车向任何方向转弯,然而之前的代码存在一个问题。首先,汽车一次只能向一个方向移动,即它要么向前或向后移动,要么向左或向右转弯。
它本身无法根据手势进行转弯。为了使其能够这样做,我们需要使代码更加智能。与机器人的整体连接将完全相同。但是代码会有所不同,让我们看看具体是什么:
import smbus
from time import sleep
import RPi.GPIO as GPIO
int1 = 12
int2 = 16
int3 = 18
int4 = 15
GPIO.setup(int1, GPIO.OUT)
GPIO.setup(int2, GPIO.OUT)
GPIO.setup(int3, GPIO.OUT)
GPIO.setup(int4, GPIO.OUT)
PWM1 = GPIO.PWM(12, 100)
PWM2 = GPIO.PWM(16, 100)
PWM3 = GPIO.PWM(18, 100)
PWM4 = GPIO.PWM(15, 100)
PWM1.start(0)
PWM2.start(0)
PWM3.start(0)
PWM4.start(0)
PWR_MGMT_1 = 0x6B
SMPLRT_DIV = 0x19
CONFIG = 0x1A
GYRO_CONFIG = 0x1B
INT_ENABLE = 0x38
ACCEL_XOUT_H = 0x3B
ACCEL_YOUT_H = 0x3D
ACCEL_ZOUT_H = 0x3F
GYRO_XOUT_H = 0x43
GYRO_YOUT_H = 0x45
GYRO_ZOUT_H = 0x47
def MPU_Init():
bus.write_byte_data(Device_Address, SMPLRT_DIV, 7)
bus.write_byte_data(Device_Address, PWR_MGMT_1, 1)
bus.write_byte_data(Device_Address, CONFIG, 0)
bus.write_byte_data(Device_Address, GYRO_CONFIG, 24)
bus.write_byte_data(Device_Address, INT_ENABLE, 1)
def read_raw_data(addr):
high = bus.read_byte_data(Device_Address, addr)
low = bus.read_byte_data(Device_Address, addr+1)
value = ((high << 8) | low)
if(value > 32768):
value = value - 65536
return value
bus = smbus.SMBus(1)
Device_Address = 0x68
MPU_Init()
while True:
acc_x = read_raw_data(ACCEL_XOUT_H)
acc_y = read_raw_data(ACCEL_YOUT_H)
acc_z = read_raw_data(ACCEL_ZOUT_H)
gyro_x = read_raw_data(GYRO_XOUT_H)
gyro_y = read_raw_data(GYRO_YOUT_H)
gyro_z = read_raw_data(GYRO_ZOUT_H)
Ax = (gyro_x/160)- 16384
Ay = (gyro_y/160)-16384
if Ax > 20:
if Ay > 20:
dc1 = Ax - Ay
PWM1.changeDutyCycle(dc1)
dc3 = Ax + Ay
PWM3.changeDutyCycle(dc3)
elif Ay <- 20:
dc2 = Ax + Ay
PWM1.changeDutyCycle(dc)
dc4 = Ax - Ay
PWM3.changeDutyCycle(dc4)
else:
dc1=Ax
PWM1.changeDutyCycle(dc)
dc3=Ax
PWM3.changeDutyCycle(dc)
if Ax < -20:
if Ay > 20:
dc1 = Ax - Ay
dc3 = Ax + Ay
PWM1.changeDutyCycle(dc1)
PWM3.changeDutyCycle(dc3)
if Ay <- 20:
dc2 = Ax + Ay
dc4 = Ax - Ay
PWM2.changeDutyCycle(dc2)
PWM4.changeDutyCycle(dc4)
else:
dc2=Ax
dc4=Ax
PWM2.changeDutyCycle(dc2)
PWM4.changeDutyCycle(dc4)
现在我们来看看我们对代码所做的所有更改。整个传感机制保持不变,但是数据处理进行了重大改进。那么,让我们看看具体是什么:
If Ax > 20:
if Ay > 20:
dc1 = Ax - Ay
PWM1.changeDutyCycle(dc1)
dc3 = Ax + Ay
PWM3.changeDutyCycle(dc3)
现在我们正在比较Ax的值。如果Ax > 20,则下面的代码将运行。我们之所以这样做,是因为加速度计极其敏感,可以感知到最微小的振动。因此,可能会出现错误输出。所以为了过滤它,我们有一个20的阈值。也就是说,直到加速度计倾斜 20%,这个代码都不会生效。同样,我们也在 y 轴上这样做。一旦完成,这个百分比值就给了dc1 = Ax - Ay这一行。这个操作是在 x 轴的倾斜度上,也就是前进轴,减去Y的移动。在第二行,我们对电机的另一侧做同样的操作,然而,我们不是减去Y的值,而是加上它。因此,它将产生电机两端速度的差异。这个速度差异将直接与Y轴的角倾斜度相关。因此,倾斜度越大,速度变化越大,转弯角度也越大。
elif Ay <- 20:
dc2 = Ax + Ay
PWM1.changeDutyCycle(dc)
dc4 = Ax - Ay
PWM3.changeDutyCycle(dc4)
在下一行,我们为加速度计另一侧的倾斜度设置了一个条件,通过检测Ay < -20。如果值小于-20,则以下算法将生效。
在这里,这些行完全相同。然而,数学运算符已经被反转。因此,对于第一个电机,我们不再减去Y的值,而是现在加上它。另一方面,对于第二个电机,我们不是加值,而是减去它。
Else:
dc1=Ax
PWM1.changeDutyCycle(dc)
dc3=Ax
PWM3.changeDutyCycle(dc)
最后,如果Ax的值大于20,但Ay的值在-20到+20之间,那么我们将假设汽车需要直行。因此,Ax的值将直接传递给两个电机,使其完全直行。
摘要
在本章中,我们已经了解了加速度计和陀螺仪的工作原理。我们使用了陀螺仪和加速度计的原始数据,并将其用于制作一个手势控制机器人。最后,进入书的下一章和最后一章,我们将继续我们的旅程,这一章是你最期待的精彩章节。
第十八章:制作机械臂
最后,我们来到了大多数人在这本书开始时就希望达到的地方。制作一个机械臂!在本章中,我们将学习机械臂工作原理背后的概念。毫无疑问,我们也将制作一个供个人使用的机械臂,它可以为我们做无数的事情。
机械臂的基本原理
如果你看到人体,那么使我们能够与其他大多数物种不同的最显著部分之一就是手臂。它是我们用来做大部分工作的身体部位。
人类手臂是一个非常复杂的关节和肌肉机制,这些关节和肌肉协同工作,赋予了我们所熟知的灵活性。以我们的肩关节为例。如果你仔细观察,你会发现它能够上下移动,左右移动,甚至可以绕着自己的轴线旋转,而这一切都只靠一个单一的关节,我们称之为球关节。
当我们谈论机器人上的机械臂时,我们无疑是在谈论一个复杂的执行器排列,与身体(即底盘)一起,以在三维空间中获得所需的运动。
现在,让我们了解任何机械臂的基本组成部分。首先是执行器。我们可以使用电机来控制机械臂;然而,正如我们之前所研究的,使用我们之前使用的电机并不是理想的选择,因为它无法保持其位置,也没有反馈机制。因此,我们只剩下一个选择,那就是使用伺服电机。正如我们所知,它们具有相当大的扭矩,并且能够知道自己的位置,并且可以保持我们想要的任何位置。
机器人的第二部分是底盘,即那个将所有电机连接在一起并提供机器人结构支撑的部分。它必须以这种方式制作,以便为任何给定的关节提供所有期望的轴运动。这一点很重要,因为单个伺服电机只能在一个轴上提供运动。然而,有多个地方可以使用复杂的排列来使机器人在多个轴上移动。此外,底盘应该是刚性的,这一点非常重要。众所周知,地球上所有的材料都有一定的柔韧性。此外,材料的构造取决于材料的非顺应性。这服务于一个非常重要的重复性目的。
那么,什么是重复性?正如你可能已经在工业或任何制造单位中看到的那样,机器人被安装并重复执行相同的任务。这是可能的,因为机器人被编程在特定情况下执行一组特定的功能。现在,让我们假设机器人的底盘不是刚性的。在这种情况下,即使伺服系统 100%精确并且每次都能到达完全相同的位置,机器人实际上可能与其目标位置有所不同。这是因为底盘可能存在一些灵活性,这就是为什么最终位置可能不同。因此,一个合适的底盘是必不可少的。当我们谈论大型机器人时,这一点变得更加重要,因为即使是微小的变形也可能导致手臂最终位置发生很大的变化。
在谈论机械臂时,我们经常使用的一个术语是末端执行器。这基本上是机械臂的末端,它将为我们完成所有最终工作。在真实的人臂的情况下,末端执行器可以被认为是手。这是手臂的顶部,手臂的所有运动基本上是为了在三维空间中定位手的位置。此外,正是手拿起物体或执行必要的物理动作。因此,我们称之为末端执行器。
现在,随着机械臂在三维空间中移动,确定运动发生的轴线成为一个真正的大问题。因此,我们通常不是使用轴线来定义运动,而是使用正在执行的运动类型,这给我们一个关于运动是什么以及它可能在哪个轴上的现实概念。为了分析运动,我们使用偏航、俯仰和滚转(YPR)的概念。
上述图示将消除关于 YPR 的大部分疑问。这个概念通常用于飞机;然而,它也是机械手的一个基本组成部分。所以,正如你从上述图中可以看到的,当飞机的鼻部向上或向下移动时,这将被认为是俯仰运动。同样,如果飞机改变航向,那么偏航可以被认为是相应地改变——偏航就是飞机在y轴上的运动。最后,我们还有被称为滚转的东西。它用于理解旋转角度。正如你所看到的,这三个实体彼此独立,追逐任何一个都不会对其他产生影响。这个概念也很有用,因为无论飞机的朝向如何,YPR 都会保持不变,并且非常容易理解。因此,我们将这个概念直接从飞机应用到我们的机器人上。
最后,我们怎能忘记处理单元呢?它是控制所有执行器并进行协调和决策的单元。在我们的案例中,这个处理单元是树莓派,它将指挥所有执行器。所有这些先前的组件共同构成了一个机械臂。
自由度
并非每个机械臂都是相同的。它们有不同的负载能力,即末端执行器可以承受的最大负载,速度和范围,即末端执行器可以达到多远。然而,机械臂的一个非常重要的部分是它拥有的电机数量。因此,对于每个轴,你需要至少一个电机来使机器人沿该轴移动。例如,人类的肩关节具有三维自由度。因此,为了模仿该关节,你需要每个轴上的一个电机,也就是说,手臂要在所有三个轴上独立移动,至少需要三个电机。同样,当我们谈论我们手部的肘关节时,它只能在两个维度上移动。那就是手臂的闭合和打开,以及最后手臂的旋转——肘关节在第三维度上不移动。因此,为了复制其运动,我们需要至少两个电机,这样我们才能在w轴上移动机器人。
从我们目前所理解的来看,我们可以安全地假设,电机数量越多,机器人也会越灵活。这通常是这种情况;然而,你可能使用多个电机使机器人本身在一个轴上转动。在这种情况下,通过计算执行器的数量来确定机器人灵活性的基本概念将不再适用。那么,我们如何确定机器人的灵活性呢?
我们有一个称为自由度(DOF)的概念。如果按照标准定义来说,那么我可以非常确信你会对它实际意味着什么感到困惑。如果你还不信服,那么你自己尝试在谷歌上查找。用非常简单和直白的话来说,一个自由度是一个可以在任何给定轴上独立移动的关节。例如,如果我们谈论肩关节,那么它在三个轴上都有运动。因此,自由度是三个。现在,让我们考虑我们手臂的肘关节。因为它只能在俯仰和滚转方向上移动,所以我们最终得到两个自由度。如果我们把肩关节和肘关节连接起来,那么自由度将相加,整个系统将被称为具有六个自由度。请记住,这个定义是一个非常简化的定义。如果你选择深入研究,你会遇到多个复杂性。
现在,你将遇到的大多数机械臂都会有接近六个自由度。尽管你可能认为这比人类的臂少,但实际上,它完成了大部分工作。显然,自由度越少,意味着需要的电机数量越少,从而成本更低,编程的复杂性也显然更低。因此,我们尽量使用尽可能少的自由度。
现在,在先前的图中,你可以看到一个典型的六自由度(DOFs)机械臂。标记为数字1的底座执行器提供了滚动和改变俯仰的自由度。标记为数字2的肘部执行器只为机器人增加了一个俯仰的自由度。此外,编号为3的关节具有在俯仰和滚动中移动的能力。最后,我们这里有一个末端执行器作为夹爪;夹爪本身有一个自由度。因此,总的来说,我们可以称这个机器人为一个六自由度机器人。
电池组
我们在所有项目中都使用了一个单位,但我想在本章中强调它。这个单位是功率单位。我们之所以谈论它,是因为在本章中我们将控制多个伺服电机。当我们谈论多个伺服电机时,自然会谈到大量的功耗。在机械臂中,我们有六个伺服电机。现在,根据电机的品牌和型号,功耗会有所不同。但为了安全起见,假设每个伺服电机的功耗约为 1 安培是个不错的想法。你使用的多数电源可能无法提供这么大的瞬时电流。那么我们应该怎么办?
我们可以采取简单的方法,选择更高的功率输出。但相反,我们可以走一条非传统的路线。我们可以有一个在需要时可以提供这么多功率的电池。但问题是,任何电池都能满足我们的需求吗?显然,答案是否定的。
存在多种类型的电池。这些电池可以根据以下参数进行区分:
-
电压
-
容量
-
功率重量比
-
最大充放电速率
-
化学成分
这些内容将在接下来的小节中详细说明。
电压
电压是电池可以产生的整体电位差。每个电池都有其特定的电压输出。有一点需要记住的是,这个电压会根据电池的充电状态略有变化。也就是说,当 12V 电池完全充电时,它可能输出 12.6V。然而,当它完全放电时,它可能达到 11.4V。因此,电池电压的含义是电池将提供的标称电压。
容量
现在的第二个参数是容量。通常,当你购买一个电池时,你会看到它的容量以毫安时(mAh)或安培时(Ah)表示。这是一个非常简单的术语。让我用一个例子来解释这个术语。假设你有一个 5 Ah 容量的电池。现在,如果我连续 1 小时抽取 5 安培的电流,那么电池将完全放电。相反,如果我连续抽取 10 安培的电流,那么电池将在半小时内放电。通过这个,我们也可以使用以下简单的公式推导出电池的总功率:总功率(瓦)= 电池标称电压 x 电池总容量(安培)。
因此,如果你有一个 12V、容量为 10 Ah 的电池,那么总容量将是 120 瓦。
功率与重量比
在上一章中,我们已经了解到重量在机器人技术中起着非常重要的作用,如果我们增加机器人的重量,那么移动它所需的力可以指数级增加。因此,功率与重量比的概念就应运而生。我们总是偏好那种极轻且相对于重量能提供大量功率的电池。功率与重量比的方程可以定义为以下:功率与重量比(瓦时/公斤)= 最大功率(瓦)/ 电池总重量(公斤)。
现在,假设一个电池提供 500 瓦的功率,重量为 5 公斤,那么功率与重量比将是 100 Wh/kg。功率与重量比越高,电池越好。
最大充放电速率
这可能是电池最重要的部分之一。通常,电池能够使机器人运行 1 小时。然而,机器人的功耗并不恒定。假设 90%的时间里,我们的机械臂消耗 2 安培的功率,那么电池容量是 2 Ah。然而,在操作过程中的一些时刻,机器人需要所有电机以最大功率工作。机器人的峰值功耗约为 6 安培。现在的问题是,2 Ah 的电池能否为机器人提供 6 安培的功率?
这是一个非常实际的挑战。你可能会说,使用比 2 Ah 电池更大的电池会更好。但是,正如你所知,这将极大地增加重量。那么解决方案是什么呢?
有一种叫做峰值放电电流的东西。这用C值表示。所以,如果我们的电池是 1C 的充电率,那么 2Ah 的电池在任何时候只能提供最大 2Ah 的电源。然而,如果电池是 10C 的充电率,那么它应该能够提供高达 20 安培的瞬间电源。如今,您可以找到能够提供高达 100C 甚至更多瞬间电源的电池。我们之所以有这个,是因为机器人的峰值功率消耗可能比它们的恒定功率消耗指数级更高。如果在任何时候电池无法提供足够的电力,那么机器人可能会出现错误行为,甚至可能关闭。
这个故事的第二部分是充电率。这是您可以提供给电池的最大充电电流。它也用相同的 C 值表示。所以,如果 C 值是 0.5,那么您可以向 2Ah 的电池提供最大 1 安培的充电电流。
换句话说,您最快可以在 2 小时内给电池充电。
化学成分
市场上可以找到不同类型的电池,这些电池主要根据它们的化学成分进行分类。所有这些电池都有它们自己的优缺点。因此,我们不能说哪一个比另一个更好。这总是在各种因素之间的权衡。以下是在市场上可以找到的电池列表,以及它们的优缺点:
| 电池 | 峰值功率输出 | 功率重量比 | 价格 |
|---|---|---|---|
| 湿电池 | 低 | 极低 | 最低 |
| 镍氢 | 中等 | 低 | 便宜 |
| 锂离子 | 高 | 好 | 高 |
| 锂聚合物 | 极高 | 极好 | 极高 |
如您从这张表中可以看到,峰值功率输出是我们非常想要的,同样好的功率重量比也是;因此,在锂聚合物电池上投入大量资金是有意义的。
这些电池至少有 20C 的充电率,功率重量比比普通湿电池高约五倍。然而,它们的成本可能是普通湿电池的 10 倍以上。
现在我们知道了哪些电池适合那些更高的电流需求。一款 11.1V、2200mAh 的锂聚合物电池不会让您花费超过 20 美元,并且会为您提供您可能永远不需要的巨大功率。因此,我们已经解决了电源问题。现在是时候继续前进,使机器人手变得可用。
寻找极限
机器人手臂套件在 eBay 或 Amazon 上购买相当容易。组装起来并不困难,需要几个小时来准备。有些机器人手臂套件可能不包含伺服电机,在这种情况下,您可能需要单独订购。我会建议选择包含伺服电机的套件,因为如果您选择单独订购伺服电机,可能会出现兼容性问题。
如你所知,这些伺服将使用 PWM 工作,控制它们也不难。所以,让我们直接进入它,看看我们能做什么。一旦你组装好机器人手臂套件,按照以下方式连接伺服的电线:
现在,首先,我们需要知道我们机器人上连接的每个伺服的最大物理限制是什么。有各种技术可以做到这一点。最基本的方法是进行物理测量。这种方法可能很好,但你将无法充分利用伺服电机的全部潜力,因为你会有一定程度的测量误差。因此,你放入伺服的值将略小于你认为它可以达到的值。第二种方法是手动输入数据并找出确切的角度。所以,让我们继续使用第二种方法,并上传以下代码:
import RPi.GPIO as GPIO
import time
GPIO.setmode(GPIO.BCM)
GPIO.setup(14,GPIO.OUT)
GPIO.setup(16,GPIO.OUT)
GPIO.setup(18,GPIO.OUT)
GPIO.setup(20,GPIO.OUT)
GPIO.setup(21,GPIO.OUT)
GPIO.setup(22,GPIO.OUT)
GPIO.setwarnings(False)
pwm1 = GPIO.PWM(14, 50)
pwm2 = GPIO.PWM(16, 50)
pwm3 = GPIO.PWM(18, 50)
pwm4 = GPIO.PWM(20, 50)
pwm5 = GPIO.PWM(21, 50)
pwm6 = GPIO.PWM(22, 50)
pwm1.start(0)
pwm2.start(0)
pwm3.start(0)
pwm4.start(0)
pwm5.start(0)
pwm6.start(0)
def cvt_angle(angle):
dc = float(angle/90) + 0.5
return dc
while 1:
j = input('select servo')
if j == 1:
i = input('select value to rotate')
pwm1.ChangeDutyCycle(cvt_angle(i))
time.sleep(2)
pwm1.ChangeDutyCycle(cvt_angle(90))
elif j ==2:
i = input('select value to rotate')
pwm2.ChangeDutyCycle(cvt_angle(i))
time.sleep(2)
pwm2.ChangeDutyCycle(cvt_angle(90))
elif j ==3:
i = input('select value to rotate')
pwm3.ChangeDutyCycle(cvt_angle(i))
time.sleep(2)
pwm3.ChangeDutyCycle(cvt_angle(90))
elif j ==4:
i = input('select value to rotate')
pwm4.ChangeDutyCycle(cvt_angle(i))
time.sleep(2)
pwm4.ChangeDutyCycle(cvt_angle(90))
elif j ==5:
i = input('select value to rotate')
pwm5.ChangeDutyCycle(cvt_angle(i))
time.sleep(2)
pwm5.ChangeDutyCycle(cvt_angle(90))
elif j ==6:
i = input('select value to rotate')
pwm6.ChangeDutyCycle(cvt_angle(i))
time.sleep(2)
pwm6.ChangeDutyCycle(cvt_angle(90)) }
现在,让我们看看这段代码在做什么。这段代码看起来可能相当复杂,但它所做的是极其简单的。
j = input('select servo from 1-6')
使用前面的代码行,我们向用户打印了从 1-6 选择伺服的语句。当用户输入伺服的值时,这个值将被存储在变量j中:
if j == 1:
i = input('select value to rotate')
pwm1.ChangeDutyCycle(cvt_angle(i))
time.sleep(2)
pwm1.ChangeDutyCycle(cvt_angle(90))
这里的if条件用于检查j的值。如果在这一行中j=1,那么它将运行对应伺服编号1的代码。在这个代码内部,第一行将打印选择旋转的值。一旦完成,程序将等待用户输入。一旦用户输入任何值,它将被存储在一个名为I的变量中。之后,使用cvt_angle(i)函数,用户输入到系统中的值将被转换为相应的占空比值。这个占空比值将被用于pwm1.ChangeDutyCycle()参数,从而给机器人提供你想要特定关节的非常确定的角度。由于time.sleep(2)函数,伺服将等待执行下一行。之后,我们使用pwm1.ChangeDutyCycle(cvt_angle(90))这一行,这将将其恢复到 90 度。
你可能会问,我们为什么要这样做?这是出于一个非常重要的原因。假设你给它一个超出其物理限制的命令。如果是这样,那么伺服将不断尝试朝那个方向移动,不管发生什么。然而,由于物理限制,它将无法继续前进。一旦发生这种情况,然后,在几秒钟内,你将看到从伺服中冒出蓝色烟雾,这表明它已经死亡。问题是,犯这种错误很容易,损失也很明显。因此,为了防止这种情况,我们迅速将其恢复到中央位置,这样它就没有燃烧的可能性了。
现在,根据前面的代码,通过机器人对伺服 1-6 进行了相同的操作。既然你已经知道了发生了什么,是时候拿起笔和纸,开始为伺服器提供角度值了。请记住,这个代码的最终目标是找出最大限制。所以,让我们从 90 度开始做起来。在两侧给出一个值,直到你能够取到的值。在纸上列一个清单,因为我们需要它来编写下一部分的代码。
使机器人安全
在本章的前一部分,通过我们的多次尝试,我们已经能够找到每个伺服器的最大位置。现在是我们使用这些值的时候了。在这一章中,我们将为伺服器的绝对最大值编程。在这个程序中,我们将确保伺服器不会超出定义参数的任何一侧,甚至一度。如果用户给出的值超出这个范围,它将简单地选择忽略用户输入,而不是造成自我损害。
那么,让我们看看如何完成它。在这个程序的一些部分,数值被加粗了。这些是需要用我们在本章前一部分记录的值来替换的值。例如,对于伺服器 1,记录的值是23和170作为两侧的最大值。因此,代码的变化将从if a[0] < 160 and a[0] > 30变为if a[0] < 170 and a[0] > 23。同样,对于每个伺服器,都必须遵循相同的程序:
import RPi.GPIO as GPIO
import time
GPIO.setmode(GPIO.BCM)
GPIO.setup(14,GPIO.OUT)
GPIO.setup(16,GPIO.OUT)
GPIO.setup(18,GPIO.OUT)
GPIO.setup(20,GPIO.OUT)
GPIO.setup(21,GPIO.OUT)
GPIO.setup(22,GPIO.OUT)
GPIO.setwarnings(False)
pwm1 = GPIO.PWM(14, 50)
pwm2 = GPIO.PWM(16, 50)
pwm3 = GPIO.PWM(18, 50)
pwm4 = GPIO.PWM(20, 50)
pwm5 = GPIO.PWM(21, 50)
pwm6 = GPIO.PWM(22, 50)
pwm1.start(cvt_angle(90))
pwm2.start(cvt_angle(90))
pwm3.start(cvt_angle(90))
pwm4.start(cvt_angle(90))
pwm5.start(cvt_angle(90))
pwm6.start(cvt_angle(90))
def cvt_angle(angle):
dc = float(angle/90) + 0.5
return dc
while True:
a = raw_input("enter a list of 6 values")
if a[0] < 160 and a[0] > 30:
pwm1.ChangeDutyCycle(cvt_angle(a[0]))
if a[1] < 160 and a[1] > 30:
pwm2.ChangeDutyCycle(cvt)angle(a[1]))
if a[0] < 160 and a[0] > 30:
pwm3.ChangeDutyCycle(cvt_angle(a[2]))
if a[0] < 160 and a[0] > 30:
pwm4.ChangeDutyCycle(cvt_angle(a[3]))
if a[0] < 160 and a[0] > 30:
pwm5.ChangeDutyCycle(cvt_angle(a[4]))
if a[0] < 160 and a[0] > 30:
pwm6.ChangeDutyCycle(cvt_angle(a[5]))}
现在,在这段代码中,我们做了一些非常基础的工作。你可以安全地说,我们所做的只是将ChangeDutyCycle()函数放入一个if语句中。这个if语句将决定伺服器是移动还是保持在它自己的位置。对一些人来说,这个程序在特殊部分中可能看起来非常天真。但是,相信我,它不是。这个语句现在将被用作从现在开始每个程序的一部分。所有为伺服器运动编写的代码都必须通过这个if语句检查要发送到伺服器的最终值;因此,代码的基本可视化是极其必要的。
现在解释已经完成,是时候给你不同的命令,看看它们是否在安全工作范围内工作。
编程多个框架
在上一章中,我们学习了如何确保机器人在安全限制下工作的基础知识。在这一章中,我们将探讨如何通过点击按钮让机器人执行不同的活动,而不是逐个输入值。
要做到这一点,我们需要理解一些运动的高级概念。每当你观看任何视频或玩游戏时,你肯定遇到过“每秒帧数”(FPS)这个术语。如果你没有听说过这个术语,那么让我为你解释一下。现在制作的所有视频实际上都是由静态图像组成的。这些静态图像是由每秒点击 25-30 次的相机捕捉的。当这些图像以与捕捉时相同的速率在屏幕上播放时,就形成了一个平滑的视频。
类似地,在机器人中,我们确实有框架的概念。然而,这些框架并不是图像,而是机器人为了实现特定运动必须遵循的多个步骤。在一个简单的机器人程序中,可能只有两个框架,即初始框架和最终框架。这两个框架将对应初始位置或最终位置。
然而,在现实世界中,这并不总是可能的,因为每当机器人直接从初始位置移动到最终位置时,它通常会遵循一条具有特定曲率的特定路径。然而,这条路径上可能会有障碍,或者这条路径可能不是所期望的,因为需要遵循的路径可能是不同的。因此,我们需要框架。这些框架不仅定义了机器人从初始位置到最终位置的运动,还将这两个位置之间的过渡分解成多个步骤,使机器人遵循所需的路径。
这可以被称为帧编程,我们将在本章中介绍。需要注意的是,帧数越多,机器人的运行越平滑。你还记得我们看到的监控录像吗?我们可以说是不够平滑,有很多颠簸。这是由于监控摄像机的帧率低。他们不是在 30 FPS 下工作,而是在 15 FPS 下工作。这是为了减少视频的存储空间。然而,如果你看最新的视频,有些游戏和视频的帧率比正常情况下要高得多。我们最新的相机工作在 60 FPS,使视频更加平滑,观看起来更加愉快。机器人也是如此。帧数越多,运动越平滑、越受控。但是,请确保不要过度追求。
现在,要从一个位置移动到另一个位置,我们将在一开始就设置每个伺服电机的角度值。一旦获取,它将自动依次执行这些值。要这样做,请继续编写以下代码:
import RPi.GPIO as GPIO
import time
GPIO.setmode(GPIO.BCM)
GPIO.setup(14,GPIO.OUT)
GPIO.setup(16,GPIO.OUT)
GPIO.setup(18,GPIO.OUT)
GPIO.setup(20,GPIO.OUT)
GPIO.setup(21,GPIO.OUT)
GPIO.setup(22,GPIO.OUT)
GPIO.setwarnings(False)
pwm1 = GPIO.PWM(14, 50)
pwm2 = GPIO.PWM(16, 50)
pwm3 = GPIO.PWM(18, 50)
pwm4 = GPIO.PWM(20, 50)
pwm5 = GPIO.PWM(21, 50)
pwm6 = GPIO.PWM(22, 50)
pwm1.start(0)
pwm2.start(0)
pwm3.start(0)
pwm4.start(0)
pwm5.start(0)
pwm6.start(0)
def cvt_angle(angle):
dc = float(angle/90) + 0.5
return dc
prev0 = 90
prev1 = 90
prev2 = 90
prev3 = 90
prev4 = 90
prev5 = 90
while True:
a = raw_input("enter a list of 6 values for motor 1")
b = raw_input("enter a list of 6 values for motor 2")
c = raw_input("enter a list of 6 values for motor 3")
d = raw_input("enter a list of 6 values for motor 4")
e = raw_input("enter a list of 6 values for motor 5")
f = raw_input("enter a list of 6 values for motor 6")
for i in range(6):
if a[i] > 10 and a[i]< 180 :
pwm1.ChangeDutyCycle(cvt_angle(a[i]))
if b[i] > 10 and b[i] < 180:
pwm2.ChangeDutyCycle(cvt_angle(b[i]))
if c[i] > 10 and c[i] < 180:
pwm3.ChangeDutyCycle(cvt_angle(c[i]))
if d[i] > 10 and d[i] < 180:
pwm4.ChangeDutyCycle(cvt_angle(d[i]))
if e[i] > 10 and e[i] < 180:
pwm5.ChangeDutyCycle(cvt_angle(e[i]))
if f[i] > 10 and f[i] < 180:
pwm6.ChangeDutyCycle(cvt_angle(f[i]))
在这个程序中,你可以看到我们复制了之前的程序,只是做了一些非常小的改动。那么,让我们看看这些改动是什么:
a = raw_input("enter a list of 6 values for motor 1")
b = raw_input("enter a list of 6 values for motor 2")
c = raw_input("enter a list of 6 values for motor 3")
d = raw_input("enter a list of 6 values for motor 4")
e = raw_input("enter a list of 6 values for motor 5")
f = raw_input("enter a list of 6 values for motor 6")
在这里,我们正在为每个伺服系统获取输入值并将其存储在不同的列表中。对于伺服 1,将使用列表a;同样,b将用于伺服 2,以此类推,直到f。在代码的前几行中,机器人将提示用户填写motor 1的六个帧值。然后,它将询问motor 2的六个值,以此类推,直到motor 6:
for i in range(6):
给伺服系统提供 PWM 的整个程序都集中在这个 for 循环中。这个循环将检查i的值,并在每次迭代时增加它。i的值将从1开始,循环将运行并增加i的值,直到它达到6。
if a[i] > 10 and a[i]< 180 :
pwm1.ChangeDutyCycle(cvt_angle(a[i]))
在程序的这一行中,列表中的值是根据1的值来命名的。所以,第一次它会读取a[1]的值,这将是列表a[]的第一个值。这个值应该在安全工作范围内,因此有if循环。如果它在安全工作范围内,那么if条件下的程序将工作,否则不会。在if循环内部,我们有一个简单的语句:pwm1.ChangeDutyCycle(cvt_angle(a[I]))。这会将a[1]的值转换为相应的 PWM 值,并将其提取到ChangeDutyCycle()函数中,这将改变伺服 1 的 PWM。
对于从伺服 1 到伺服 6 的其余伺服系统,也制作了类似的程序。因此,所有这些都将逐个读取它们对应列表的值,并按照用户编程的方式改变伺服的角度。此外,随着循环的执行,i的值将增加,从而使程序读取列表中提取的不同值。列表中伺服的每个值都对应于不同的帧,因此机器人通过它进行解析。
所以,继续前进,享受制作机器人做一些惊人的动作吧。只是要注意要温柔对待它!
速度控制
制作一个机器人手臂竟然如此简单,而且只需一点代码,我们现在就能按照我们想要的方式控制它。然而,你可能已经注意到一个问题,那就是机器人移动的方式是我们想要的,但不是我们想要的速度。在使用基于数字 PWM 的伺服系统时,这是一个非常常见的问题。
这些伺服系统没有内置的速度控制。它们的控制系统被编程为尽可能快地移动伺服系统以到达目标位置。因此,为了控制速度,我们不得不自己调整程序,并给它一个平滑的线性进展。
速度控制可以通过几种不同的技术来完成。所以,不多说,让我们看看代码。在你写之前,先读一读,然后看一遍代码,然后看看下面的解释。这样,你将更好地了解我们在做什么。这将使编写代码更快、更容易。所以,让我们看看它:
import RPi.GPIO as GPIO
import time
GPIO.setmode(GPIO.BCM)
GPIO.setup(14,GPIO.OUT)
GPIO.setup(16,GPIO.OUT)
GPIO.setup(18,GPIO.OUT)
GPIO.setup(20,GPIO.OUT)
GPIO.setup(21,GPIO.OUT)
GPIO.setup(22,GPIO.OUT)
GPIO.setwarnings(False)
pwm1 = GPIO.PWM(14, 50)
pwm2 = GPIO.PWM(16, 50)
pwm3 = GPIO.PWM(18, 50)
pwm4 = GPIO.PWM(20, 50)
pwm5 = GPIO.PWM(21, 50)
pwm6 = GPIO.PWM(22, 50)
pwm1.start(0)
pwm2.start(0)
pwm3.start(0)
pwm4.start(0)
pwm5.start(0)
pwm6.start(0)
def cvt_angle(angle):
dc = float(angle/90) + 0.5
return dc
prev0 = 90
prev1 = 90
prev2 = 90
prev3 = 90
prev4 = 90
prev5 = 90
pwm1.ChangeDutyCycle(cvt_angle(prev0))
pwm2.ChangeDutyCycle(cvt_angle(prev1))
pwm3.ChangeDutyCycle(cvt_angle(prev2))
pwm4.ChangeDutyCycle(cvt_angle(prev3))
pwm5.ChangeDutyCycle(cvt_angle(prev4))
pwm6.ChangeDutyCycle(cvt_angle(prev5))
while True:
a = raw_input("enter a list of 6 values for motor 1")
b = raw_input("enter a list of 6 values for motor 2")
c = raw_input("enter a list of 6 values for motor 3")
d = raw_input("enter a list of 6 values for motor 4")
e = raw_input("enter a list of 6 values for motor 5")
f = raw_input("enter a list of 6 values for motor 6")
speed = raw_input("enter one of the following speed 0.1, 0.2, 0.5, 1")
for i in range(6):
while prev0 =! a[i] and prev1 =! b[i] and prev2 =! c[i] and prev3 =! d[i] and prev4 =! e[i] and prev 5 =! f[i]
if a[i] > 10 and a[i]< 180 :
if prev0 > a[i]
prev0 = prev0 - speed
if prev0 < a[i]
prev0 = prev0 + speed
if prev0 = a[i]
prev0 = prev0
pwm1.ChangeDutyCycle(cvt_angle(prev0))
if b[i] > 10 and b[i] < 180:
if prev2 > b[i]
prev2 = prev2 - speed
if prev2 < b[i]
prev2 = prev2 + speed
if prev2 = b[i]
prev2 = prev2
pwm2.ChangeDutyCycle(cvt_angle(b[i]))
if c[i] > 10 and c[i] < 180:
if prev3 > c[i]
prev3 = prev3 - speed
if prev3 < c[i]
prev3 = prev3 + speed
if prev3 = c[i]
prev3 = prev3
pwm3.ChangeDutyCycle(cvt_angle(c[i]))
if d[i] > 10 and d[i] < 180:
if prev4 > d[i]
prev4 = prev4 - speed
if prev4 < d[i]
prev4 = prev4 + speed
if prev4 = d[i]
prev4 = prev4
pwm4.ChangeDutyCycle(cvt_angle(d[i]))
if e[i] > 10 and e[i] < 180:
if prev5 > e[i]
prev5 = prev5 - speed
if prev0 < e[i]
prev5 = prev5 + speed
if prev5 = e[i]
prev5 = prev5
pwm5.ChangeDutyCycle(cvt_angle(e[i]))
if f[i] > 10 and f[i] < 180:
if prev6 > f[i]
prev6 = prev6 - speed
if prev6 < f[i]
prev6 = prev6 + speed
if prev6 = f[i]
prev6 = prev6
pwm6.ChangeDutyCycle(cvt_angle(f[i]))
flag = 0
在这个程序中,有很多东西。我们应该逐一了解它们,以便理解它。所以,让我们看看我们在做什么:
prev0 = 90
prev1 = 90
prev2 = 90
prev3 = 90
prev4 = 90
prev5 = 90
在这里,我们定义了六个新的变量,名称为prev0到prev5,并且它们都被赋予了90的值。这里的prev代表之前,所以这将用来指示前一个值。
while prev0 =! a[i] and prev1 =! b[i] and prev2 =! c[i] and prev3 =! d[i] and prev4 =! e[i] and prev 5 =! f[i]
在代码行for i in range 6之后,我们有前一行代码,它基本上是在检查a[i]的值是否与prev0相等。同样,它也在检查b[i]的值是否与prev1相等,依此类推。直到所有这些都不成立时,while循环才会成立,并且会循环程序内的代码,直到条件不成立。也就是说,所有的prev值都恰好等于列表中相应值的值。
再次,这对你来说可能有点奇怪,但请相信我,它将会非常有用,我们很快就会看到:
if a[i] > 10 and a[i]< 180 :
if prev0 > a[i]
prev0 = prev0 - speed
if prev0 < a[i]
prev0 = prev0 + speed
if prev0 = a[i]
prev0 = prev0
pwm1.ChangeDutyCycle(cvt_angle(prev0))
现在,这里才是真正的关键。这是控制伺服器速度的主要程序。在这个程序中,第一行很简单;它会检查给它提供的值是否有效,即是否在安全范围内。一旦完成这个检查,它将检查a[Ii]的值是否小于或大于前一个值。如果它大于a[i],那么它将取前一个值并减去用户指定的速度。如果它小于a[i]的值,那么它将以前一个值为基础增加用户指定的速度。
因此,如果你看的话,代码只是在while循环运行时简单地增加或减少前一个值。现在,while循环将一直运行,直到prev的值等于对应列表的值。也就是说,循环将一直增加值,直到达到指定的位置。
因此,速度值越低,每次增加的值就越低,从而整体降低速度。
这也是所有其他伺服器将要经历的过程。这听起来可能非常复杂,但事实并非如此!编程很简单,每次你将其分解成小块并逐一理解时,它都会保持简单!
摘要
在这一章中,我们了解了机器人的基本知识,它的电源和编程。通过一个非常简单的程序,我们能够找出伺服器的限制,并将这些限制应用到确保伺服器不会损坏自己的程序中。我们对框架有了基本的了解,并基于框架进行了一些编程。最后,我们还通过自己的程序在基本层面上控制了伺服器的速度。
2万+



被折叠的 条评论
为什么被折叠?



