Por: Josue Soriano Publicado el: 2017-08-12 16:36:41, 4968
Hace poco termine un proyecto de automatizacion que encontre en la pagina de Instructables, sin embargo la documentacion sobre el mismo es muy pobre en cuanto a la parte de la configuracion del sistema operativo en la Raspberry Pi y por el paso de las actualizaciones de algunas librerias ha quedado un poco obsoletas.
Explicando un poco el objetico de este proyecto, cosiste en hacer un dispositivo que me permita alimentar a mis mascotas de manera remota utilizando un correo electronico de GMail. El alimentador automatizado me permite consultar por medio de un correo electronico con el Subject: Cuando, este me contestara el correo un mensaje que indica cuando sera la proxima alimentacion adjuntando una foto del plato de comida para ver si ya esta o no vacio y tambien me permite enviar un e-mail con el Subject: Alimentar lo cual, si ya se cumplio un tiempo de 8 horas antes de la ultima alimentacion, comenzara a dispensar la comida; una vez terminada la alimentacion automatica me contestara el correo indicando que se ha dispensado correctamente la comida adjuntando una foto del plato de comida para corroborar que en efecto se haya llenado.
Se necesita armar un pequeño circuito que activa un Motor de Alto torque, y como mis conocimientos de electronica son muy pero muy escasos me tuve que pasar un par de dias aprendiendo las cosas basicas, asi que les recomiendo que vean los canales de Youtube de TerrazoCultor y sobre todo vean los videos de electornica basica de Charly Labs
El Script de automatizacion original esta escrito en Python, nunca habia usado este lenguaje de programacion pero la verdad no es tan diferente de los demas, lo he modificado un poco para que se adapte mejor a las nuevas librerias de Python y el proceso de automatizacion junto con las configuraciones del sistema operativo permiten parametrizar lo siguiente:
Materiales Utilizados
Todos los materiales anteriores a excepcion de los ultimos dos no se pueden conseguir localmente (o por lo menos no sabia donde conseguirlos), sin embargo la varilla y el acoplador probablemente se encuntren en algun taller de soldadura pero al ser la primera vez que hago algo asi, no tenia idea de como debia ser para que se acoplara al motor asi que lo pedi a la pagina de los links descritos anteriomente; acontinuacion los materiales que pude comprar en tiendas locales en San Pedro Sula:
Alguinos materiales micelaneos que se pueden conseguir en ferreterias o posiblemente ya tengamos:
Armando la estructura
La idea principal es sustituir la manija dispensadora que viene en el ZEVRO por la varilla D que luego se conectara al Motor por medio del Acoplador. El dispensador se sujetara de la caja de madera y la caja de madera a la Pared. Como no se mucho de electrónica no utilicé ninguna placa de baquelita para montar el circuito así que use la protoboard para poder poner todos los componentes así que en el fondo de la caja va sujeta con tornillos la Rapberry Pi y la protoboard la cual ya tenía un adhesivo en la parte de atrás así que solo fue de pegarla. La caja debe de tener tres botones los cuales tendrán la función de Reiniciar el temporizador, activar el alimentador y el último será un paso directo para activar el motor sin pasar por el circuito. El motor se sujetara dentro de la caja por medio del braket por lo que, de la caja, solo sobresaldrá la varilla D que se conecta al dispensador, en la parte de abajo de la caja fije y ajuste la webcam para que en cada correo de consulta y confirmacion me envie una foto de como esta el plato, esto para no sobre alimentar en caso de que no se hayan terminado la ultima tanda de comida.
De la parte inferior del dispensador se colocara el tubo que bajara hasta el plato y en la base se colocara el codo de PVC, yo coloque un poco de Tape Industrial en la salida para minimizar la velocidad del flujo de comida e hice una base dispensadora para evitar que la comida salga disparada por todos lados. Para fijar el tubo a la pared utilice abrazaderas metálicas que fije a la pared con los tornillos para taco S8.
En la puerta de la caja sujete la placa de la pantalla LCD y utilice los pares trenzados del cable UTP para llevarlos directamente a la Raspberry, en el otro extremo de los cables empalme las puntas hembras de los jumpers para hacer más fácil la conexión en los puertos GPIO de la Raspberry. Este sería el diagrama de circuitos. Voy a tratar de explicarlo con lo poco lo que logre captar de Electrónica.
El motor se conecta directamente al polo positivo del transformador de 12 voltios pero para que la corriente fluya debe pasar por el circuito en el polo neutro del motor, para ello se utiliza el transistor N2222. Los transistores tienen normalmente 3 patitas las cuales corresponden a un colector, una base y un emisor, dependiendo del modelo del transistor la ubicación de estas patitas pueden variar; este transistor hace la función de Switch aquí es donde conectamos el polo neutro del motor al colector del transistor, el pin #19 de la rapberry se conecta a la base por medio de una resistencia de 270Ω y el emisor se conecta a la terminal neutra del transformador de 12V junto con uno de los polos Tierra de la raspberry; el transistor dejara fluir la corriente entre colector y emisor siempre y cuando la base se estimule con suficiente voltaje; mas adelante programaremos las rapberry para que según ciertas ordenes el puerto 19 emitirá 3.3 voltios, lo suficiente para que el circuito continúe y active el motor.
Para la pantalla LCD se utiliza otra parte de la protobard en un circuito diferente donde conecte el potenciómetro de 10KΩ el cual recula el contraste del texto que aparece en la pantalla LCD, así que si no aparece nada en la pantalla es porque probablemente el potenciómetro este totalmente cerrado; en mi caso yo lo deje totalmente abierto para que se visualice mejor el texto. Al final las conexiones en la protoboard quedarían de la siguiente manera.
Configuración de la Raspberry
Para configurar la rapberry utilice la versión Lite del Rapbian (no necesitamos la carcasa grafica) y aplique la configuración básica que muestra el raspbian una vez que se instala o utilizando el comando raspi-config: expandi el espacio al 100% y habilite el SSH pero sobre todo lo mas importante es cambiar la contraseña y el nombre de usuario al usuario pi que viene por defecto (De lo contrario, como me paso a mi, pueden sufrir un ataque por el puerto 22 si tienen una IP publica direccionada a la raspberry). Aparte de eso la configuracion para que se conecte automaticamente a mi red WiFi (Suponiendo que me red se llama "BlogSoriano" y mi password es "$eguridad123!") generamos y guardamos la clave en el archivo de conexiones Wireless con el siguiente comando:
$ sudo wpa_passphrase "BlogSoriano" "$eguridad123!" | sudo tee -a /etc/wpa_supplicant/wpa_supplicant.conf > /dev/null
lo siguiente es actualizar e instalar el sistema de instalacion de python "pip" para ello vamos a loguearnos como root, actualizar e instalar los paquetes necesarios:
$ sudo -i
$ apt-get update
$ apt-get install build-essential python-dev python-smbus python-pip
Con esto ya tenemos acceso a la libreria de clases de python, para el script que vamos a usar se necesita instala lo siguiente:
$ pip install IMAPClient RPi.GPIO Adafruit-CharLCD httplib2 html2text netifaces wireless
Una vez terminada la instalacion de dependencias vamos a escribir el siguiente script, yo lo guarde en la ruta /opt/petfeeder.py el archivo lo pueden crear mediante el comando nano /opt/petfeeder.py y dentro de este archivo pegamos el siguiente codigo, aclaro que este codigo no lo escribi yo ya que nunca habia programado en Python, unicamene lo modifique para que funcione con las nuevas librerias y con la pantalla LCD de 20x4; tambien inclui una funcion para leer tweets random desde algunas cuentas que yo sigo desde una API que programe en PHP la cual compartire mas tarde:
###########################################################################################
# petfeeder.py v 2.0
#
# Author: Krish Sivakumar
# Modificador: Josue Soriano
# Creado en Dic. 2015
# Actualizado Jul 2017
#
# Program to automate on-demand pet feeding through the internet
# Uses Raspberry Pi as a controller
#
# Distributed under Creative Commons Attribution-NonCommercial-Sharealike licensing
# https://creativecommons.org/licenses/by-nc-sa/3.0/us/
#
############################################################################################
from imapclient import IMAPClient, SEEN
import time
import smtplib
from email.MIMEMultipart import MIMEMultipart
from email.MIMEBase import MIMEBase
from email.MIMEText import MIMEText
from email import Encoders
import os
import sys
import RPi.GPIO as GPIO
from Adafruit_CharLCD import Adafruit_CharLCD
import httplib2
import json
import html2text
import cv2
import netifaces as ni
from wireless import Wireless
DEBUG = False
MOTORON = True
CHUCKNORRIS = True #activa para que adjunte un chiste de Chuck Norris en ingles en los correos
NUMBERTRIVIA = True #active para que mande dentro de los correos una trivia de un numero aleatorio.
TWEETS = False #implementacion propia para adjuntar Tweets Random desde un API personal.
TWEETSAPI = "" #URL del API que me retorna tweets de cuentas que sigo.
# Here is our logfile
LOGFILE = "/tmp/petfeeder.log"
# Variables for checking email
GMAILHOSTNAME = 'imap.gmail.com' # Insert your mailserver here - Gmail uses 'imap.gmail.com'
MAILBOX = 'Inbox' # Insert the name of your mailbox. Gmail uses 'Inbox'
GMAILUSER = '@gmail.com'# Insert your email username
GMAILPASSWD = 'mipassword'# Insert your email password
EMAILNOTIFICATIONS = 'correopersonal@gmail.com' #correo al que llegaran las notificaciones
NEWMAIL_OFFSET = 0
lastEmailCheck = time.time()
MAILCHECKDELAY = 30 # Don't check email too often since Gmail will complain
# GPIO pins for feeder control
MOTORCONTROLPIN = 19
FEEDBUTTONPIN = 6
RESETBUTTONPIN = 13
# GPIO pins for 20x4 HD44780
lcd_rs = 27
lcd_en = 22
lcd_d4 = 25
lcd_d5 = 24
lcd_d6 = 23
lcd_d7 = 18
lcd_bl = 4
lcd_cols = 20
lcd_rows = 4
# Variables for feeding information
readyToFeed = False # not used now but for future use
feedInterval = 28800 # This translates to 8 hours in seconds
FEEDFILE="/tmp/lastfeed.log"
cupsToFeed = 1.5
motorTime = cupsToFeed * 27 # It takes 27 seconds of motor turning (~1.75 rotations) to get 1 cup of feed
PHOTOFILE = '/tmp/foodstatus.jpg'
# Function that gets Chuck Norris jokes from the internet. It uses an HTTP GET and then a JSON parser
def getChuckNorrisQuote():
# The database where the jokes are stored
ICNDB="http://api.icndb.com/jokes/random"
# Doing a HTTP request to get the response (resp) and content (content)
resp, content = httplib2.Http().request(ICNDB)
# The content is in the following JSON format and needs to be parsed
# {u'type': u'success', u'value' : {u'joke': 'Text of the joke', u'id': 238, u'categories': []}}
parsed_content = json.loads(content)
joke = "\n\n** Chiste Random de Chuck Norris **:\n" + html2text.html2text(parsed_content['value']['joke'])
return joke
# Function that gets a number trivia from the internet. It uses an HTTP GET and then a JSON parser
def getNumberTrivia():
# The database where the trivia are stored
NUMDB="http://numbersapi.com/random/trivia?json"
# Doing a HTTP request to get the response (resp) and content (content)
resp, content = httplib2.Http().request(NUMDB)
# The content is in the following JSON format and needs to be parsed
# {u'text': u'Text of trivia', u'type' : u'trivia, u'number': , u'found': True}
parsed_content = json.loads(content)
trivia = "\n\n** Curiosidad del Numero " + str(parsed_content['number']) + " **\n"
trivia = trivia + parsed_content['text']
return trivia
def getTweets():
global TWEETSAPI
# Doing a HTTP request to get the response (resp) and content (content)
resp, content = httplib2.Http().request(TWEETSAPI)
# The content is in the following JSON format and needs to be parsed
# {u'text': u'Text of trivia', u'type' : u'trivia, u'number': , u'found': True}
parsed_content = json.loads(content)
tweets = "\n\n*** No Hay Tweets Disponibles ***"
if parsed_content['usuario'] is not None:
tweets = "\n\n*** Tweet de Random de " + html2text.html2text(parsed_content['usuario']) + "\n"
tweets = tweets + html2text.html2text(parsed_content['tweet'])
if parsed_content['mediaUrl'] is not None:
tweets = tweets + "\n" + html2text.html2text(parsed_content['mediaUrl'])
return tweets
# Function to check email
def checkmail():
global lastEmailCheck
global lastFeed
global feedInterval
if (time.time() > (lastEmailCheck + MAILCHECKDELAY)): # Make sure that that atleast MAILCHECKDELAY time has passed
lastEmailCheck = time.time()
server = IMAPClient(GMAILHOSTNAME, use_uid=True, ssl=True) # Create the server class from IMAPClient with HOSTNAME mail server
server.login(GMAILUSER, GMAILPASSWD)
server.select_folder(MAILBOX)
# See if there are any messages with subject "When" that are unread
whenMessages = server.search([u'UNSEEN', u'SUBJECT', u'Cuando'])
# Respond to the when messages
if whenMessages:
for msg in whenMessages:
msginfo = server.fetch([msg], ['BODY[HEADER.FIELDS (FROM)]'])
fromAddress = str(msginfo[msg].get('BODY[HEADER.FIELDS (FROM)]')).split('<')[1].split('>')[0]
msgBody = "La ultima alimentacion se realizo el " + time.strftime("%b %d at %I:%M %P", time.localtime(lastFeed))
if (time.time() - lastFeed) > feedInterval:
msgBody = msgBody + "\nListo para alimentar ahorita!"
else:
msgBody = msgBody + "\nLa siguiente alimentacion comenzara el " + time.strftime("%b %d at %I:%M %P", time.localtime(lastFeed + feedInterval))
if NUMBERTRIVIA:
msgBody = msgBody + getNumberTrivia()
if CHUCKNORRIS:
msgBody = msgBody + getChuckNorrisQuote()
if TWEETS:
msgBody = msgBody + getTweets()
getPhoto()
sendemail(fromAddress, "Gracias por la consulta de Alimentacion", msgBody, PHOTOFILE)
server.add_flags(whenMessages, [SEEN])
# See if there are any messages with subject "Feed" that are unread
feedMessages = server.search([u'UNSEEN', u'SUBJECT', u'Alimentar'])
# Respond to the feed messages and then exit
if feedMessages:
for msg in feedMessages:
msginfo = server.fetch([msg], ['BODY[HEADER.FIELDS (FROM)]'])
fromAddress = str(msginfo[msg].get('BODY[HEADER.FIELDS (FROM)]')).split('<')[1].split('>')[0]
msgBody = "La ultima alimentacion se realizo el " + time.strftime("%b %d at %I:%M %P", time.localtime(lastFeed))
if (time.time() - lastFeed) > feedInterval:
msgBody = msgBody + "\nLa alimentacion comenzara en breves momentos"
else:
msgBody = msgBody + "\nLa proxima alimentacion se realizara el " + time.strftime("%b %d at %I:%M %P", time.localtime(lastFeed + feedInterval))
if NUMBERTRIVIA:
msgBody = msgBody + getNumberTrivia()
if CHUCKNORRIS:
msgBody = msgBody + getChuckNorrisQuote()
if TWEETS:
msgBody = msgBody + getTweets()
sendemail(fromAddress, "Gracias por tu solicitud de alimentacion", msgBody)
server.add_flags(feedMessages, [SEEN])
return True
return False
def sendemail(to, subject, text, attach=None):
msg = MIMEMultipart()
msg['From'] = GMAILUSER
msg['To'] = to
msg['Subject'] = subject
msg.attach(MIMEText(text.encode('utf-8'), 'plain', 'utf-8'))
if attach:
part = MIMEBase('application', 'octet-stream')
part.set_payload(open(attach, 'rb').read())
Encoders.encode_base64(part)
part.add_header('Content-Disposition', 'attachment; filename="%s"' % os.path.basename(attach))
msg.attach(part)
mailServer = smtplib.SMTP("smtp.gmail.com", 587)
mailServer.ehlo()
mailServer.starttls()
mailServer.ehlo()
mailServer.login(GMAILUSER, GMAILPASSWD)
mailServer.sendmail(GMAILUSER, to, msg.as_string())
mailServer.close()
def buttonpressed(PIN):
# Check if the button is pressed
global GPIO
# Cheap (sleep) way of controlling bounces / rapid presses
time.sleep(0.2)
button_state = GPIO.input(PIN)
if button_state == False:
return True
else:
return False
def remotefeedrequest():
# At this time we are only checking for email
# Other mechanisms for input (e.g. web interface or iOS App) is a TO-DO
return checkmail()
def printlcd(row, col, LCDmesg):
# Set the row and column for the LCD and print the message
global logFile
global lcd
lcd.set_cursor(row, col)
lcd.message(LCDmesg)
lcd.set_cursor(0,2)
lcd.message("Wifi: " + getSSID())
lcd.set_cursor(0,3)
lcd.message("IP: " + getIP())
def feednow():
# Run the motor for motorTime, messages in the LCD during the feeeding
global GPIO
global MOTORCONTROLPIN
global motorTime
global lastFeed
global GMAILUSER
global lastFeed
lcd.clear()
printlcd(0,0,"Alimentando.....")
#print "Alimentando...."
if MOTORON:
GPIO.output(MOTORCONTROLPIN, True)
time.sleep(motorTime)
GPIO.output(MOTORCONTROLPIN, False)
lastFeed = time.time()
saveLastFeed()
printlcd(0,1, "\nAlimentacion Terminada!")
msgBody = "Alimentacion terminada exitosamente!"
if NUMBERTRIVIA:
msgBody = msgBody + getNumberTrivia()
if CHUCKNORRIS:
msgBody = msgBody + getChuckNorrisQuote()
if TWEETS:
msgBody = msgBody + getTweets()
getPhoto()
sendemail(EMAILNOTIFICATIONS, "Alimentacion exitosa el " + time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(lastFeed)), msgBody, PHOTOFILE)
time.sleep(2)
return time.time()
def saveLastFeed():
global FEEDFILE
global lastFeed
with open(FEEDFILE, 'w') as feedFile:
feedFile.write(str(lastFeed))
feedFile.close()
def getPhoto():
global PHOTOFILE
camera_port = 0
camera = cv2.VideoCapture(camera_port)
time.sleep(0.1)
return_value, image = camera.read()
cv2.imwrite(PHOTOFILE, image)
del(camera)
def getIP():
global ni
try:
ni.ifaddresses('wlan0')
ip = ni.ifaddresses('wlan0')[ni.AF_INET][0]['addr']
except:
ip = "0.0.0.0"
return ip
def getSSID():
try:
wireless = Wireless()
ssid = wireless.current()
except:
ssid = "Ninguno"
return ssid
# This is is the main program, essentially runs in a continuous loop looking for button press or remote request
try:
print "Iniciando Servicio de Alimentacion"
#### Begin initializations #########################
####################################################
# Initialize the logfile
logFile = open(LOGFILE, 'a')
# Initialize the LCD
lcd = Adafruit_CharLCD(lcd_rs, lcd_en, lcd_d4, lcd_d5, lcd_d6, lcd_d7, lcd_cols, lcd_rows, lcd_bl)
# lcd.begin(16,2)
lcd.clear()
lcd.show_cursor(False)
# Initialize the GPIO system
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
# Initialize the pin for the motor control
GPIO.setup(MOTORCONTROLPIN, GPIO.OUT)
GPIO.output(MOTORCONTROLPIN, False)
# Initialize the pin for the feed and reset buttons
GPIO.setup(FEEDBUTTONPIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(RESETBUTTONPIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
# Initialize lastFeed
if os.path.isfile(FEEDFILE):
with open(FEEDFILE, 'r') as feedFile:
lastFeed = float(feedFile.read())
feedFile.close()
else:
lastFeed = time.time()
saveLastFeed()
#### End of initializations ########################
####################################################
#### The main loop ####
while True:
try:
#### If reset button pressed, then reset the counter
if buttonpressed(RESETBUTTONPIN):
lcd.clear()
print "Reiniciando... "
printlcd(0,0, "Reiniciando... ")
time.sleep(2)
lastFeed = time.time() - feedInterval + 5
saveLastFeed()
#### Check if we are ready to feed
if (time.time() - lastFeed) > feedInterval:
#lcd.clear()
printlcd(0,0, time.strftime("%m/%d %I:%M:%S%P", time.localtime(time.time())))
printlcd(0,1, "Listo para Alimentar")
#### See if the button is pressed
if buttonpressed(FEEDBUTTONPIN):
#print "Boton Presionado"
feednow()
#### Check if remote feed request is available
elif remotefeedrequest():
feednow()
#### Since it is not time to feed yet, keep the countdown going
else:
timeToFeed = (lastFeed + feedInterval) - time.time()
#lcd.clear()
printlcd(0,0, time.strftime("%m/%d %I:%M:%S%P", time.localtime(time.time())))
printlcd(0,1, 'Proxima: ' + time.strftime("%Hh %Mm %Ss", time.gmtime(timeToFeed)))
checkmail()
if buttonpressed(FEEDBUTTONPIN):
#print "Boton Presionado"
lcd.clear()
printlcd(0,0, "Ahora no, intenta el")
printlcd(0,1, time.strftime("%b/%d %H:%M", time.localtime(lastFeed + feedInterval)))
#print time.strftime("%b/%d %H:%M", time.localtime(lastFeed + feedInterval))
time.sleep(2)
time.sleep(.6)
except Exception as e:
logError = time.strftime("%m/%d %I:%M:%S%P", time.localtime(time.time())) + " Error inesperado: " + str(e) + "\n"
sendemail(EMAILNOTIFICATIONS, "Registro de error", logError)
logFile.write(logError)
#### Cleaning up at the end
except KeyboardInterrupt:
logFile.close()
lcd.clear()
GPIO.cleanup()
except SystemExit:
logFile.close()
lcd.clear()
GPIO.cleanup()
Una vez que tengamos el Script hay que cambiar los valores de las variables con los correos electronicos que se requieran, yo les recomiendo que usen gmail por su facilidad de conexion por medio del protocolo IMAP, con eso solo quedaria guardar el archivo y salir (con Ctrl+o se guarda y ctrl+x se sale del editor nano), solo nos hace falta que sea a prueba de reinicios; el script por si ya guarda la ultima vez que se activo el sistema de alimentacion, por lo que solo debemos hacer que el script se ejecute cada vez que se inicie el sistema operativo, para ello encontre una solucion muy particular con un programa llamado supervisor, el cual se instala desde los repositorios del Debian:
$ apt-get install supervisor
y una vez instalado solamente necesitamos crear un archivo de configuracion en /etc/supervisor/conf.d/petfeeder.conf, al igual que el caso anterior lo podemos crear y guardar con nano, este archivo tendra lo siguiente:
[program:petfeederd]
directory=/opt
command=python petfeeder.py
autostart=true
autorestart=true
Una vez guardado el archivo podemos usar el comando supervisorctl [start|stop|restart] petfeederd, como en este caso el servicio no se ha iniciado lo ejecutamos con start:
$ supervisorctl start petfeederd
Y con esto ya deberia funcionar el sistema, la pantalla LCD deberia mostrar informacion de cuando deberia ser la siguiente alimentacion o si ya esta listo para alimentar deberia decirlo. Tambien debe mostrar el nombre de la Red inalambrica a la que se esta conectado y la direccion IP que se esta utilizando por si necesitamos conectarnos por medio de SSH, comparto un pequeño video que hice, me disculpo por la calidad del mismo, no soy muy bueno es este tema de los vlogs.