在智能宠物喂食机的第1部分“使用Arduino Uno制作智能自动宠物喂食机”中,我们建立了一个自动化平台,可以判断您的宠物是否被喂食以及计算下一次喂食的时间。在第2部分中,我们将尝试通过MIT App Inventor开发一个应用程序并添加语音识别功能,使系统更加“智能”。在之前的教程中,我们已经使用过MIT App Inventor来创建应用程序。App Inventor是一种易于使用的基于块的语言,用于设计Android应用程序。
硬件
- • Arduino UNO
- • RFID RC522
- • HC-05 蓝牙模块
沿用第1部分的硬件:
- • Arduino Uno
- • 光传感器 TEMT6000
- • 距离传感器 Sharp GP2Y0A21YK
- • RFID RC522
- • 蜂鸣器
- • 电机 SG90
- • RTC DS1307
软件
- Arduino IDE
- GitHub上的PetFeeder
- MIT App Inventor
步骤1:使用带有RFID标签的EEPROM
我们在第1部分中设置的用于EEPROM的标签—在该存储器中,标签将被保存到我们将其清除为止。此功能有助于我们将自己的宠物与其他宠物分开来,只为带有指定标签的宠物提供食物。
我们在第1部分中设置了两个标签,并使用EEPROM来存储数据。RFID标签有助于识别您的宠物并将其与其他人的宠物区分开,从而仅向带有指定标签的宠物提供食物。使用EEPROM可以确保数据在系统重新启动后也可以安全地存储在内存中。您可以使用以下代码从EEPROM更改标签信息:
#include <EEPROM.h>
#include <SPI.h>
#include <MFRC522.h>
boolean match = false;
boolean programMode = false;
int isOurPet;
byte storedCard[4];
byte readCard[4];
byte masterCard[4];
#define SS_PIN 10
#define RST_PIN 9
MFRC522 mfrc522(SS_PIN, RST_PIN);
void setup() {
Serial.begin(9600);
SPI.begin();
mfrc522.PCD_Init();
if (EEPROM.read(1) != 143) {
do {
isOurPet = findOurPet();
}
while (!isOurPet);
for ( int j = 0; j < 4; j++ ) {
EEPROM.write( 2 + j, readCard[j] );
}
EEPROM.write(1, 143);
}
for ( int i = 0; i < 4; i++ ) {
masterCard[i] = EEPROM.read(2 + i);
}
}
void loop () {
do {
isOurPet = findOurPet();
}
while (!isOurPet);
if ( master(readCard)) {
programMode = true;
Serial.println(F("Our Pet - Green Tag"));
int count = EEPROM.read(0);
}
else {
Serial.println(F("Not our pet - Purple Tag"));
}
}
int findOurPet() {
if ( ! mfrc522.PICC_IsNewCardPresent()) {
return 0;
}
if ( ! mfrc522.PICC_ReadCardSerial()) {
return 0;
}
for (int i = 0; i < 4; i++) {
readCard[i] = mfrc522.uid.uidByte[i];
}
mfrc522.PICC_HaltA(); // Stop reading
return 1;
}
void readCollar( int number ) {
int start = (number * 4 ) + 2;
for ( int i = 0; i < 4; i++ ) {
storedCard[i] = EEPROM.read(start + i);
}
}
boolean EEpromCheck ( byte a[], byte b[] ) {
if ( a[0] != NULL )
match = true;
for ( int k = 0; k < 4; k++ ) {
if ( a[k] != b[k] )
match = false;
}
if ( match ) {
return true;
}
else {
return false;
}
}
boolean master( byte test[] ) {
if ( EEpromCheck( test, masterCard ) )
return true;
else
return false;
}
上次,我们将宠物指定为红色标签。这次,我们将标签更改为绿色。
步骤2:怎样控制伺服
伺服电机通过来自微控制器的PWM(脉冲宽度调节)来更改其位置。伺服需要进行校准,并将其阀门开度角度设置为90度。
为了控制伺服,我们将使用ArduinoSweep 代码。该代码中伺服电机轴可以旋转180度。我们将把旋转角度从0-180度更改为10-170度。
#include <Servo.h>
Servo myservo; // create servo object to control a servo
int pos = 0; // variable to store the servo position
void setup()
{
myservo.attach(9); // attaches the servo on pin 9 to the servo object
}
void loop()
{
for(pos = 10; pos <= 170; pos += 1) // goes from 10 degrees to 170 degrees
// in steps of 1 degree
{
myservo.write(pos); // tell servo to go to position in variable ‘pos’
delay(15); // waits 15ms for the servo to reach the position
}
for(pos = 170; pos>=10; pos -= 1) // goes from 170 degrees to 10 degrees
{
myservo.write(pos); // tell servo to go to position in variable ‘pos’
delay(15); // waits 15ms for the servo to reach the position
}
}
关于伺服的注意事项:
您的伺服在某些时候可能会行为异常。当您由于Arduino自行重启而无法执行指令时,可能是因为USB端口没有提供足够的电源来驱动伺服。在这种情况下,Arduino会进行重置,应用程序也无法工作。用以下两种方法可以避免此问题的产生:
- 1. 您可以在面包板上的GND和5V之间添加一个大容值电容器(470uF或更大)。当Arduino没有足够的电源来维持电流时,该电容器可以用作临时电源。较长的端子必须连接到VDD=5V,而较短的端子必须连接到GND。
- 2. 您可以通过USB对开发板进行编程,之后断开连接。然后,您可以使用手机充电器通过插头来为装置供电,因为它具有更大的电流容量。
让我们简要地看一下<Servo.h> 库的工作方式。
#include <Servo.h>
必须含有该指令才能使用Servo.h库。Servo库中给出的两个示例是“Knob”和“Sweep”。这两个控件对伺服的测试很有用。使用Knob,您就可以通过电位计将伺服转动到特定角度。使用Sweep,您就可以在180度的范围内来回扫动伺服轴。
Servo servo;
这是一个类型的声明。它定义了servo类型的变量Servo。它与其他用于伺服的类型相似,如int(整型)和float(浮点型)。
servo.attach(servoPin);
在设置代码块时,您需要将伺服分配给特定的引脚。该指令用于将伺服变量分配引脚。
servo.write(angle);
此指令将设置伺服轴角度(在0到180度范围内),然后将伺服转动至该角度。
步骤3:添加HC-05蓝牙模块
———-
关于蓝牙HC-05 – 用户使用手册
蓝牙串行模块的运行不需要进行驱动,并且可以与具有串行接口的其他蓝牙设备进行通信。两个蓝牙模块之间实现通信至少需要两个条件:
(1) 必须在主设备和从设备之间进行。
(2) 密码必须正确。
该模块的属性:
- • 可以在主模式和从模式之间切换
- • 蓝牙名称: HC-05
- • 密码: 1234
配对:主设备不仅可以与指定的蓝牙地址配对(如手机、计算机适配器、从设备),还可以自动搜索从设备并与之配对。
典型方法:在某些特定条件下,主设备和从设备可以自动配对(这是默认方法)。
———-
在本项目中,我们选择了蓝牙连接,因为这种方式易于配置。蓝牙模块用作Arduino的串行端子,将被连接到TX和RX引脚。
想要通过蓝牙进行数据传输需要遵循一些规则。我们需要有:
- • (通常值为逻辑0)
- • 数据位
- • 校验位(数据位的和;我们将会比较从头到尾所有位的和)
- • 停止位(大多数情况下值为逻辑1)
引脚连接:
- 1.将HC-05的TX引脚连接到Arduino的RX引脚。
- 2.将HC-05的RX引脚连接到Arduino的TX引脚。
- 3.将GND引脚连接在一起。
在我们之前的教程 制作您自己的Arduino RFID门锁—第2部分:用智能手机解锁中对HC-05设置进行了详细说明。如果您在连接蓝牙模块时遇到问题,请参考上述教程。
蓝牙传输代码:
#include <SD.h>
#include <Wire.h>
#include <Time.h>
#include <TimeLib.h>
#include <DS1307RTC.h>
#include <Servo.h>
#include <EEPROM.h>
#include <SPI.h>
#include <MFRC522.h>
String voice;
#define SS_PIN 10
#define RST_PIN 9
Servo myservo;
boolean match = false;
boolean programMode = false;
boolean replaceMaster = false;
int lightSensor = 0;
int distanceSensor=1;
int pos = 0;
int successRead;
byte storedCard[4];
byte readCard[4];
byte masterCard[4];
MFRC522 mfrc522(SS_PIN, RST_PIN);
void setup() {
pinMode(8, OUTPUT);
setSyncProvider(RTC.get);
myservo.attach(9);
Serial.begin(9600);
SPI.begin();
mfrc522.PCD_Init();
if (EEPROM.read(1) != 143) {
do
{
successRead = getID();
}
while (!successRead);
for ( int j = 0; j < 4; j++ )
{
EEPROM.write( 2 + j, readCard[j] );
}
EEPROM.write(1, 143);
}
for ( int i = 0; i < 4; i++ )
{
masterCard[i] = EEPROM.read(2 + i);
Serial.print(masterCard[i], HEX);
Serial.println("");
}
}
void loop()
{
int valueFromLightSensor = analogRead(lightSensor);
//Serial.print("Light Value= ");
//Serial.println(valueFromLightSensor);
//Serial.println("");
//Serial.print("Distance Value= ");
int valueFromDistanceSensor = analogRead(distanceSensor);
int distance= 4800/(valueFromDistanceSensor - 20);
//Serial.println(distance);
//Serial.print("Hour= ");
// Serial.println(hour());
while (Serial.available())
{
delay(10);
char c = Serial.read();
voice += c;
}
if (voice.length() > 0)
{
Serial.println(voice);
if(voice == "feed")
{
myservo.write(130);
delay(1000);
myservo.write(50);
delay(1000);
myservo.write(130);
delay(1000);
myservo.write(50);
delay(1000);
digitalClockDisplay();
}
if(voice == "feed2")
{
myservo.write(130);
delay(1000);
myservo.write(50);
delay(1000);
digitalClockDisplay();
}
if(voice == "feed1")
{
myservo.write(130);
delay(1000);
myservo.write(50);
delay(1000);
myservo.write(130);
delay(1000);
myservo.write(50);
delay(1000);
myservo.write(130);
delay(1000);
myservo.write(50);
delay(1000);
digitalClockDisplay();
}
voice="";
}
do {
successRead = getID();
}
while (!successRead);
if (programMode) {
if ( isMaster(readCard) ) {
programMode = false;
return;
}
else {
if ( findID(readCard) ) {
}
}
}
else {
if ( isMaster(readCard)) {
programMode = true;
int count = EEPROM.read(0);
}
else {
if ( findID(readCard) ) {
if ((hour()>=8) && (hour()<=12 )){
if (distance>=20){
// Serial.println(distance);
myservo.write(130);
delay(100);
myservo.write(50);
delay(100);
myservo.write(130);
delay(100);
myservo.write(50);
delay(100);
digitalClockDisplay();
}
delay(300);
}
if ((hour()>=12) && (hour()<=16 )){
if (distance>=20){
// Serial.println(distance);
myservo.write(130);
delay(100);
myservo.write(50);
delay(100);
myservo.write(130);
delay(100);
myservo.write(50);
delay(100);
digitalClockDisplay();
}
delay(300);
}
if ((hour()>=16) && (hour()<=20 )){
if (distance>=20){
// Serial.println(distance);
myservo.write(130);
delay(100);
myservo.write(50);
delay(100);
myservo.write(130);
delay(100);
myservo.write(50);
delay(100);
digitalClockDisplay();
}
delay(300);
}
if ((hour()>=20) && (hour()<=8 )){
if (distance>=20){
// Serial.println(distance);
myservo.write(130);
delay(100);
myservo.write(50);
delay(100);
myservo.write(130);
delay(100);
myservo.write(50);
delay(100);
digitalClockDisplay();
}
delay(300);
}
}
else { // If not, show that the ID was not valid
Serial.println(F("You shall not pass"));
}
}
}
}
int getID() {
if ( ! mfrc522.PICC_IsNewCardPresent()) {
return 0;
}
if ( ! mfrc522.PICC_ReadCardSerial()) {
return 0;
}
// Serial.println(F("Scanned PICC's UID:"));
for (int i = 0; i < 4; i++) { //
readCard[i] = mfrc522.uid.uidByte[i];
// Serial.print(readCard[i], HEX);
}
// Serial.println("");
mfrc522.PICC_HaltA(); // Stop reading
return 1;
}
void readID( int number ) {
int start = (number * 4 ) + 2;
for ( int i = 0; i < 4; i++ ) {
storedCard[i] = EEPROM.read(start + i);
}
}
boolean checkTwo ( byte a[], byte b[] ) {
if ( a[0] != NULL )
match = true;
for ( int k = 0; k < 4; k++ ) {
if ( a[k] != b[k] )
match = false;
}
if ( match ) {
return true;
}
else {
return false;
}
}
int findIDSLOT( byte find[] ) {
int count = EEPROM.read(0);
for ( int i = 1; i <= count; i++ ) {
readID(i);
if ( checkTwo( find, storedCard ) ) {
return i;
break;
}
}
}
boolean findID( byte find[] ) {
int count = EEPROM.read(0);
for ( int i = 1; i <= count; i++ ) {
readID(i);
if ( checkTwo( find, storedCard ) ) {
return true;
break;
}
else {
}
}
return false;
}
boolean isMaster( byte test[] ) {
if ( checkTwo( test, masterCard ) )
return true;
else
return false;
}
void digitalClockDisplay()
{
Serial.print(hour());
printDigits(minute());
//printDigits(second());
Serial.print(" ");
Serial.print(day());
Serial.print(" ");
Serial.print(month());
Serial.print(" ");
Serial.print(year());
Serial.println();
}
void printDigits(int digits){
// utility function for digital clock display: prints preceding colon and leading 0
Serial.print(":");
if(digits < 10)
Serial.print('0');
Serial.print(digits);
}
该代码中的算法非常简单:我们对串口进行初始化,然后等待端口打开。我们将通过该代码发送指令。如果代码不可用,程序将不会执行,“喂食”指令将不会发送到微控制器进行相应处理。
该程序还将比较来自“voice(声音)”变量的字符串和串口读取的字符串。如果两者相同,则会向电机发送一个指令来触发SG90伺服电机。
步骤3:设计应用程序
现在,让我们来创建一个应用程序吧!和以前一样,我们将使用MIT App Inventor。我们的最终目标是创建一个对所连接的多种设备进行集成的组合型应用程序(例如,集成了多个所连接设备的智能家居应用程序)。
有关MIT App Inventor的设置指南,请参考上一教程制作您自己的Arduino RFID门锁—第2部分:用智能手机解锁 (步骤3:应用程序)。本教程将分步指导您使用App Inventor创建自己的应用程序。
我们所创建的应用程序将会具有一个简单的界面,其中包含以下功能:
- • 连接到蓝牙
- • 使您可以远程喂食宠物
- • 存储:将数据(以文件形式)存储在手机中
- • 显示日期:在手机屏幕上显示日期信息
该程序的模块图非常简单易懂:
- •第一个模块:第一个模块用于蓝牙按钮
- • ListPicker – MIT App Inventor
单击该按钮,将显示文本列表供用户选择。可以通过Designer或Blocks Editor来指定文本内容,方法是将ElementsFromString属性设置为文本的拆分字符串级联(例如,选择1,选择2,选择3)或者将Elements属性设置为一个Blocks editor中的列表。
- • ListPicker – MIT App Inventor
-
- • ListPicker 显示所有可用的蓝牙;该功能在您选择某一个蓝牙前有效。
- • 第二个模块:通过调用BluetoothClient1.Connect address现应用程序与客户端之间的连接。您手机中的蓝牙将搜索附近的设备,并将其显示在ListPicker中。您可以选择要配对的设备。
同时还有一个标签,在建立连接后标签上会显示相关消息。如果设备已经成功连接,您将在屏幕上看到“已连接(Connected)”的消息。
- • 第三个模块:仅用于连接完成时通过客户端发送一个消息。该文本以串行通信的方式,通过蓝牙从一个设备发送到另一个设备。这就像在Arduino的串行监视器中键入文本一样。
当我们从串行读取数据时,就是在对用户的输入与Arduino内存中存储的字符串进行比较。这就是算法的工作原理。
我们接下来看看另一组模块图:
- • 第一个模块:将蓝牙传输的日期保存到存储在手机内存里的.txt文件中。
- • 第二个模块:当按下按钮时,将会读取保存在文本文件中的数据。
- • 第三个模块:将喂食的时间和标签写入屏幕。这个信息很有用,因为它可以帮助我们对喂食时间进行记录,如果我们不想对宠物喂食过多,可以查看时间来确认。
- • 第四个模块:发生错误时,该模块将删除.txt文件中的所有数据。这一功能很重要,因为一旦执行,将不再显示以前的信息。
该应用程序的第二部分提供了不同的喂食模式:正常喂食模式,用于宠物宝宝的喂食模式和用于成年宠物的喂食模式。这也为您提供了需要为宠物喂食多少食物量的有关信息。其中最酷的功能之一是语音识别模式。我们将在下文中讨论有关该功能的更多内容。
如果想要查找喂食的日期和时间,可以按“显示日期”按钮。该应用程序是以精简模式制作的,因为并不是每个人都希望看到所有信息。如图所示,日期和时间显示不正确。为了获得确切的日期和时间,我们需要使用Arduino IDE中的Set Time 示例。现在,RTC模块将指示正确的日期和时间。
Set Time code:
#include <Wire.h>
#include <TimeLib.h>
#include <DS1307RTC.h>
const char *monthName[12] = {
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};
tmElements_t tm;
void setup() {
bool parse=false;
bool config=false;
// get the date and time the compiler was run
if (getDate(__DATE__) && getTime(__TIME__)) {
parse = true;
// and configure the RTC with this info
if (RTC.write(tm)) {
config = true;
}
}
Serial.begin(9600);
while (!Serial) ; // wait for Arduino Serial Monitor
delay(200);
if (parse && config) {
Serial.print("DS1307 configured Time=");
Serial.print(__TIME__);
Serial.print(", Date=");
Serial.println(__DATE__);
} else if (parse) {
Serial.println("DS1307 Communication Error :-{");
Serial.println("Please check your circuitry");
} else {
Serial.print("Could not parse info from the compiler, Time=\"");
Serial.print(__TIME__);
Serial.print("\", Date=\"");
Serial.print(__DATE__);
Serial.println("\"");
}
}
void loop() {
}
bool getTime(const char *str)
{
int Hour, Min, Sec;
if (sscanf(str, "%d:%d:%d", &Hour, &Min, &Sec) != 3) return false;
tm.Hour = Hour;
tm.Minute = Min;
tm.Second = Sec;
return true;
}
bool getDate(const char *str)
{
char Month[12];
int Day, Year;
uint8_t monthIndex;
if (sscanf(str, "%s %d %d", Month, &Day, &Year) != 3) return false;
for (monthIndex = 0; monthIndex < 12; monthIndex++) {
if (strcmp(Month, monthName[monthIndex]) == 0) break;
}
if (monthIndex >= 12) return false;
tm.Day = Day;
tm.Month = monthIndex + 1;
tm.Year = CalendarYrToTm(Year);
return true;
}
图14显示了该应用程序的最终版本:
应用程序概述:
- • 需要连接到蓝牙。
- • 根据宠物的年龄采用不同的喂食模式。
- • 要激活语音识别模式,只需单击“语音识别喂食”按钮并讲话即可。
- • 仅当正确说出指令时,喂食器才能运行。如果关键字不正确(无法正确识别),则只会在标签上显示而不执行指令。
- • 在语音识别的模式下,当两次说出/重复“喂食”指令(比如您没有等待几秒钟的时间让系统对信息进行处理)时,指令就会变成“喂食喂食”,这不是有效的指令。它将保留在标签上而不执行。
- • 如果语音识别上的“喂食”指令正常工作,标签上将会打印出时间。
- • 还包括一个喂食指南,您可以根据宠物的体重查找有关所应提供食物量的信息。
所有指令都可以在Arduino IDE的串行监视器上找到。这有助于我们在必要时对应用程序进行调试。
对于语音识别,我们需要一个按钮来激活该模式。我们可以使用App Inventor中已经提供的SpeechRecognizer组件。
有了这两个组件后,将它们连接起来非常简单。您需要处理来自讲话者的文本。这是通过调用SpeechRecognizer.GetText来完成的。之后,您需要有一个标签来显示所说的内容,也可以没有这个标签,但是如果没有标签您将无法看到自己是否说了正确的指令。在程序循环中,您还需要通过蓝牙将语音指令传输到Arduino,需要使用SentText text程序。
对于每种模式,您都需要有相对应的按钮。每个按钮对应不同的指令,该指令将会被发送到Arduino,然后据此喂食不同量的食物。