Seguro programable con Arduino y Raspberry PI (Parte I)
Durante el mes pasado escribí algunas entradas que explicaban como construir un pequeño "módulo de pruebas" que nos permitiría utilizar las funciones más básicas del Arduino. La idea de esta serie de artículos era prepararnos para construir un mini-Proyecto que podamos conectar a la "Raspberry" y nos permitiera controlar nuestro Arduino vía Internet.
Si no han leído las entradas anteriores les recomiendo que lo hagan ahora ya que no me detendré demasiado en explicar los detalles del funcionamiento de cada parte del módulo y me centraré en el funcionamiento del mini proyecto.
- Conectando la Raspberry PI al Arduino vía Serial
- Fabricando un contador simple con Arduino y un display de 7 segmentos.
- Manejo de Interrupciones con Arduino
- Instalando Lighttpd+PHP 5+Sqlite en la Raspberry PI
Como una imagen dice más que mil palabras y un video dice más que mil imágenes, les dejo un pequeño video del funcionamiento del mini proyecto que denominaremos "Seguro programable":
Voy a dividir esta entrada en dos partes ya que una sola podría ser muy "pesada", en la primera vamos a examinar el funcionamiento del seguro con el Arduino y en la segunda vamos a explorar como monitorear los datos desde la raspberry, almacenarlos y procesarlos para accederlos vía Internet.
Nota: Ya esta disponible la segunda parte del Seguro Programable con Arduino y Rasberry PI.
Nota: Ya esta disponible la segunda parte del Seguro Programable con Arduino y Rasberry PI.
Examinando el código del seguro
Definiendo nuestras variables y constantes
Como pudieron ver en el video, nuestro seguro programable simula una caja fuerte de perilla. El código funciona monitoreando las interrupciones y cambiando el estado interno del programa en base a las mismas.
En esta entrada vamos a hacer uso de la memoria EEPROM, esta es una memoria no-volátil (se mantiene guardada incluso al desconectar la fuente), para almacenar el la combinación que abre el seguro.
#include <string.h> #include <EEPROM.h>
Notarán que también incluyo a <string.h>, esta bibiloteca de C contiene algunas funciones para el manejo de cadenas, algo que utilizaremos para leer fácilmente los comandos recibidos de la línea serial.
La constante EEPROM_keyBase se utiliza para definir en que posición de la memoria EEPROM utilizaremos como base para guardar la llave.
const int EEPROM_keyBase = 0;
Antes de comenzar con el algoritmo del seguro tenemos que definir las líneas de entrada y salida que utilizaremos:
- BTN1-3: Líneas de interrupción asignadas a los botones.
- D0-3: Líneas de salida para el display de 7 segmentos.
- RED: Led que indica que la puerta está cerrada.
- GRN: Led que indica que la puerta está abierta.
- RST_PIN: Línea utilizada para detectar el "reinicio por hardware".
const int BTN1 = 18; const int BTN2 = 19; const int BTN3 = 20; const int D0 = 4; const int D1 = 5; const int D2 = 6; const int D3 = 7; const int RED = 8; const int GRN = 9; const int RST_PIN = 10;
El algoritmo es un tanto sencillo, primero definamos algunas variables que van a determinar el estado del seguro:
- Almacenamos el código en un arreglo de enteros de 3 elementos al que denominaremos "key".
- Hemos definido algunas variables que guardan el "estado del programa":
- pos: Guarda el número seleccionado actual.
- i: Guarda la posición en el arreglo, esta variable nos sirve para desplazarnos por el arreglo de la llave cada vez que se cambia de "dirección de giro" y el número seleccionado es igual a la posición actual del arreglo.
- last: Guarda la última dirección de giro; Por convención: si es 0 la ultima operación fue incrementar y si es 1 la última operación fué decrementar.
- num: Guarda el número que se muestra en el display.
Si recuerdan la entrada sobre interrupciones, tenemos que definir este grupo de variables como "volatile" ya que van a ser accedidas por el código de manejo de interrupciones.
volatile int key[] = {0,0,0}; volatile int i; volatile int num; volatile int last; volatile int pos; volatile int doorOpen;
Adicionalmente vamos a utilizar la técnica de eliminación de rebote por software y como haremos uso del puerto serial y vamos a leer comandos, es necesario definir un pequeño "buffer" que almacenara los comandos para poder procesarlos:
volatile long lastInt; char buffer[32]; int buffSize;
Manejando los datos de la EEPROM
Como les mencioné la inicio idealmente queremos que nuestra llave se almacene en la EEPROM, de esta manera aunque desconectemos el Arduino el valor de la llave estará disponible para la próxima vez que lo encendamos.
Vamos a definir tres funciones que nos ayudaran a resetear la llave (en caso de recibir una petición de reseteo de hardware), cargar la llave de la EEPROM y guardar una llave nueva.
void resetKey() { Serial3.print("HR\n"); EEPROM.write(EEPROM_keyBase,0); EEPROM.write(EEPROM_keyBase+1,0); EEPROM.write(EEPROM_keyBase+2,0); } void loadKeyFromEEPROM() { key[0] = EEPROM.read(EEPROM_keyBase); key[1] = EEPROM.read(EEPROM_keyBase+1); key[2] = EEPROM.read(EEPROM_keyBase+2); } void changeKey(char command[]) { // ¡CUIDADO! // Esta funcion asume que el comando es valido // fue escrita para mejorar la lectura del codigo. key[0] = ((int)command[5] - 48); key[1] = ((int)command[6] - 48); key[2] = ((int)command[7] - 48); EEPROM.write(EEPROM_keyBase,key[0]); EEPROM.write(EEPROM_keyBase+1,key[1]); EEPROM.write(EEPROM_keyBase+2,key[2]); }
En el código anterior la función resetKey() simplemente guarda el código "000" en la memoria EEPROM, para nuestro caso estamos utilizando las direcciones 0, 1 y 2 de dicha memoria. Si luego quisiéramos mover la llave a otra posición de la memoria podemos modificar el valor de la variable EEPROM_keyBase.
La función loadKeyFromEEPROM() simplemente lee las direcciones correspondientes de la EEPROm y las guarda en nuestra "llave".
En C las cadenas de texto no son más que arreglos de caracteres, la función asume que ha recibido una cadena de la forma "SK000000", así los caracteres en las posiciones 5, 6 y 7 corresponden a la nueva llave. Hay que notar que esta función no realiza ninguna validación, sustraemos el número 48 al carácter ya que estamos asumiendo que el texto está en formato ASCII y los valores decimales correspondientes se almacenan desde la posición 48 en adelante. Más adelante cuando revisemos el procesamiento de los comandos recibidos vía serial realizaremos la validación del comando.
Inicializando el Arduino
Lo siguiente es ejecutar el código de incialización del Arduino por medio de "setup":
void setup() { buffSize = 0; // 7-Segment display pinMode(D0,OUTPUT); pinMode(D1,OUTPUT); pinMode(D2,OUTPUT); pinMode(D3,OUTPUT); // Door status (RED=closed,GREEN=open) pinMode(RED,OUTPUT); pinMode(GRN,OUTPUT); // Reset PIN pinMode(RST_PIN,INPUT_PULLUP); i = 0; num = 0; last = 1; pos = 0; doorOpen = 0; doorLed(); attachInterrupt(5,int5,RISING); attachInterrupt(4,int4,RISING); attachInterrupt(3,int3,RISING); writeTo7Seg(0); //Start serial reporting Serial3.begin(9600); clearBuffer(); Serial3.print("ST\n"); // Resets EEPROM code to 000 if(!digitalRead(RST_PIN)) { resetKey(); } else { loadKeyFromEEPROM(); } }
El código de inicialización es bastante sencillo. Tal vez la única parte que valga la pena mencionar es la del reseteo por hardware, este es un simple if que detecta si la línea RST_PIN está en 0 al momento de encender el Arduino, si es así entonces se establece el código en la EEPROM a 0, en caso contrario leemos la llave almacenada en la EEPROM. Cuando reseteamos la llave no se hace necesario leerla nuevamente ya que inicializamos la llave en 000 al declararla al inicio.
Al inicializar llamamos a otras funciones "ayudantes" como doorLed() que enciende el led correspondiente (cerrado al incializarse) y clearBuffer() que se encarga de "limpiar" los datos del buffer que almacena los comandos recibidos vía serial, no voy a explicar la función writeTo7Seg() porque la he explicado antes en los artículos previos.
void doorLed() { digitalWrite(RED,!doorOpen); digitalWrite(GRN,doorOpen); } void clearBuffer() { for(int i=0;i<32;i++) { buffer[i] = 0; } buffSize = 0; }
Manejando las interrupciones
Si recuerdan en el video nuestro seguro programable utiliza tres botones, uno para incrementar el conteo, otro para reducirlo y uno más para indicar que hemos cerrado la puerta nuevamente, el algoritmo del seguro es bastante sencillo y funciona de la siguiente manera:
- Si la última operación era diferente a la actual (incremento o decremento).
- Entonces: guardamos el numero actual en el display y verificamos el código.
- En caso contrario: incrementamos o reducimos el contador según el botón que se ha presionado.
Ambas funciones de manejo de interrupción tienen códigos similares, este codigo solo se ejecuta si la puerta esta cerrada, no voy a explicar las funciones increment() y decrement() ya que se explicaron en entradas anteriores, incluyendo el código de eliminación de rebote, las líneas de incremento y decremento quedan de la siguiente manera:
void int5() { if((millis()-lastInt)>200) { if(doorOpen == 1){ lastInt = millis(); return; } if(last == 0) { pos = num; checkCode(); } increment(); last = 1; lastInt = millis(); } } void int4() { if((millis()-lastInt)>200) { if(doorOpen == 1){ lastInt = millis(); return; } if(last == 1){ pos = num; checkCode(); } decrement(); last = 0; lastInt = millis(); } }
La función de verificación del código funciona bajo el siguiente algoritmo:
Pseudocódigo:
- Si el dígito actual es igual al valor almacenado en la posición actual del arreglo con la llave:
- Si ya revisamos los tres dígitos:
- Abrimos la puerta.
- En caso contrario:
- Movemos la posición actual del arreglo para revisar el siguiente dígito.
- En caso contrario:
- Regresamos la posición del arreglo al primer dígito.
- Hacemos que el led "parpadee" para indicar que se ha ingresado un código equivocado.
void checkCode() { if(key[i]==pos){ if(i==2){ openDoor(); } else { i+=1; } } else { i=0; wrongCode(); } } void openDoor() { doorOpen = 1; doorLed(); Serial3.print("DO\n"); } void wrongCode() { Serial3.print("BC\n"); digitalWrite(RED,0); delay(5000); doorLed(); delay(2000); digitalWrite(RED,0); delay(5000); doorLed(); }
El último código que revisaremos es el de la linea que maneja la señal de "puerta cerrada", este es mucho más sencillo ya que únicamente se encarga de reiniciar todas las variables a su estado original y "cerrar la puerta".
void int3() { if((millis()-lastInt)>200) { if(doorOpen == 0) { lastInt = millis(); return; } Serial3.print("DC\n"); i = 0; num = 0; last = 1; pos = 0; doorOpen = 0; doorLed(); writeTo7Seg(num); lastInt = millis(); } }
Comunicándonos por el puerto Serial
Una parte muy importante de este mini proyecto es la capacidad de comunicaciones por medio del puerto Serial, esta funcionalidad es la que utilizaremos para interconectar nuestro módulo con nuestra Raspberry PI.
Antes de entrar en los detalles de explicar el código tenemos que definir un pequeño "protocolo de comunicación" que nos permita enviar y recibir comandos, como no es mucha la información que transmitiremos y para no complicar demasiado el diseño vamos a transmitir comandos en formato ASCII, podríamos enviar los comandos como bytes directamente pero creanme, trabajar en serial con caracteres ASCII facilita muchísimo el trabajo.
El protocolo lo definiremos de la siguiente manera:
- Enviados por el Arduino:
- ST: "Start", el Arduino ha sido encendido y se está inicializando.
- HR: "Hardware Reset", el Arduino ha detectado un 0 lógico en la línea RST_PIN y ha reseteado la llave a "000".
- DO: "Door Opened", la puerta ha sido abierta después de recibir la combinación correcta.
- DC: "Door Closed", se ha recibido una interrupción de la línea que nos indica que la puerta se ha cerrado.
- BC: "Bad Code", se ha detectado una combinación incorrecta.
- Comandos recibidos por el Arduino y sus respuestas.
- SK<KKK><NNN>: "Set Key", este comando se envía al arduino para indicar que deseamos cambiar la llave, <KKK> es una combinación de tres dígitos que debe coincidir con la llave actual, <NNN> es una combinación de tres dígitos que corresponde a la nueva llave.
- En caso de que la llave sea correcta el Arduino responde con KSOK, "Key Set: OK".
- En caso de que la llave sea incorrecta el Arduino responde con KSER, "Key Set: Error".
- AU<KKK>: "Auth User", este comando sirve para validar un usuario, esta es una función utilitaria que decidí incluir para poder verificar una llave sin necesidad de cambiar la llave existente. <KKK> es una combinación de tres dígitos correspondiente a la llave actual
- En caso de que la llave sea la correcta el Arduino responde con IDOK, "Identification OK".
- En caso contrario responde con IDER, "Identification Error".
Si revisan el código que hemos revisado, desde el principio se cubren casi todos los envíos realizados por el Arduino. Para leer los comandos enviados hacia el Arduino vamos a hacer uso de otra interrupción llamada "serialEvent", esta interrupción se llama cada vez que hay datos en una línea serie, el Arduino Mega tiene 3 puertos serial, el puerto 1 es el que está asociado al USB y dos que son de proposito general.
Para acceder a estas funciones no necesitamos hacer uso de "attachInterrupt()" sino simplemente definir el código. En nuestro caso utilizaremos la función "serialEvent3()" ya que estaremos utilizando el puerto serie 3 para comunicarnos con la Raspberry PI:
void appendToBuffer(char c) { if(buffSize<32) { buffer[buffSize++] = c; } else { Serial3.print(buffer); clearBuffer(); buffer[buffSize++] = c; } } volatile char chr; void serialEvent3() { chr = (char)Serial3.read(); if(!(chr=='\r' || chr=='\n')) { appendToBuffer(chr); } else { if(strstr(buffer,"SK")!=NULL) { if(validCode(buffer)) { changeKey(buffer); Serial3.print("KSOK\n"); } else { Serial3.print("KSER\n"); } } else if(strstr(buffer,"AU")!=NULL) { if(authUser(buffer)) { Serial3.print("IDOK\n"); } else { Serial3.print("IDER\n"); } } clearBuffer(); } }
¿Cómo funciona el código? Primero asumimos que la función serialEvent3() se llama cada vez que hay datos nuevos disponibles en la línea serial. Lo primero que hacemos es obtener el primer carácter disponible, si este no es un fin de línea o retorno de carro (identificado por '\n' o '\r') adicionamos el carácter al buffer, una vez obtenemos un fin de línea intentamos procesar el comando almacenado en el buffer.
En otras palabras este código lee letra por letra y cuando encuentra un fin de línea o un retorno de línea intenta revisar si es un código válido.
La función strstr() esta incluida en la biblioteca string de C y nos permite saber si una cadena de texto está contenida en otra cadena de texto, para nuestro ejemplo necesitamos saber si el comando contiene "SK" o "AU" para llamar al código correspondiente.
Lo siguiente son las funciones de verificación y cambio de llave. Revisemos primero la función "validCode()":
boolean validCode(char command[]) { // Revisar si tiene el ancho correcto if(strlen(command)!=8) { return false; } // Revisar si la llave coincide if(!checkKey(command)) { return false; } boolean validNewKey = true; int curr = -1; curr = (int)command[5] - 48; if(curr<0 || curr>9) { validNewKey &= false; } curr = (int)command[6] - 48; if(validNewKey && (curr<0 || curr>9)) { validNewKey &= false; } curr = (int)command[7] - 48; if(validNewKey && (curr<0 || curr>9)) { validNewKey &= false; } return validNewKey; }
Tal vez parece un código un poco extenso, pero tiene la ventaja de que deja de ejecutarse al momento que encuentra un error, esto significa que "el peor de los casos" (computacionalmente hablando) es cuando el código es correcto, lo que significa que en promedio si recibe códigos incorrectos ejecutará menos instrucciones que cuando recibe códigos correctos.
La función de "checkKey()" es muchísimo más sencilla, esta asume que recibe un comando con el código en los caracteres 2, 3 y 4 de una cadena:
boolean checkKey(char command[]) { // Revisar si la llave coincide int curr = -1; curr = (int)command[2] - 48; if(curr<0 || curr>9 || curr!=key[0] ) { return false; } curr = (int)command[3] - 48; if(curr<0 || curr>9 || curr!=key[1] ) { return false; } curr = (int)command[4] - 48; if(curr<0 || curr>9 || curr!=key[2] ) { return false; } return true; }
Por último tenemos el código que "identifica al usuario", esta es una versión mas corta del que utilizamos para verificar el comando de cambio de llave, ya que solo verificamos que el comando tenga el largo correcto y luego verificamos la llave:
boolean authUser(char command[]) { // Revisar si tiene el ancho correcto if(strlen(command)!=5) { return false; } return checkKey(command); }
Este mini-proyecto no utiliza la función "loop" así que podemos dejarla vacía.
void loop() { }
Poniendo todo junto
Si han seguido las últimas entradas deberán tener su módulo de prueba completo y listo para funcionar, pueden descargar el sketch de Arduino desde la siguiente dirección:
El comportamiento debería ser igual al que se muestra en el video al inicio de esta entrada.
La próxima entrada revisaremos como utilizar Python para crear un pequeño "monitor" de línea serial y adicionalmente aprenderemos a usar SQLite y PHP para hacer una bonita interfaz para monitorear nuestro seguro programable en nuestra Raspberry PI.
Con esto concluyo la primera parte de nuestro mini-proyecto para Arduino y Raspberry PI y solo me queda decirles ¡¡Hasta la próxima!!
+Modificado 07/01/2013: En el afán de hacer un poco más leible el código lo modifiqué de tal manera que no permitía cambiar la llave vía serial. Ya he corregido el código y debería funcionar. Disculpas por el inconveniente.
Comentarios
Estoy muy interesado en la parte de comunicación serial entre mi raspberry y una leonardo y precisamente lo hago a traves de un pequeño scrip python que sube datos de temp, humedad, etc a Cosm.
Sin embargo tengo algunos problemas de lecturas falsas https://cosm.com/feeds/76405
Te sigo con atención.. Un saludo desde España.
¡Saludos y gracias por visitar mi pequeño blog!
Lamentablemente, no dejo de ser un aficionado, y cada paso que doy me supone mucho esfuerzo.
A mi modo de ver, raspberrypi sería la shield definitiva para arduino, ya que podría ser una ehernet shiel o una time shield, o etc, etc.. y todo pasa por una buena comunicación serial, que aunque está muy documentada, no me resulta facil de entender.
He encontrado varios proyectos de control web para manejar GPIO, pero el que realmente me interesa es este http://sourceforge.net/projects/envirocontrol/?source=dlp el cual estoy desarrollando en mi rpi http://raspberryblog.serveblog.net:8080/EnviroControl/index.php (default/default) con el cual se manejaría por web a arduino.
XD!.. Gracias.. Un saludo ;)
Primero, enhorabuena por tu blog. Es de una gran utilidad para los novatos como yo. Te escribo para ver si me puedes echar una mano porque ando algo perdido.
Estoy desarrollando un sistema de control de acceso a una instalación y había pensado en la siguiente combinación de sistemas:
- dispositivo con BT + app Blueterm
- arduino con BT
- raspberry
La idea sería que el usuario se conecta al arduino con la aplicación Blueterm vía Bluetooth. Ingresa la contraseña y el arduino la compara con la contraseña establecida previamente.
El problema que se me presenta es que quiero que esa contraseña se cambie cada hora del día. Es aquí donde creo que entraría en juego la raspberry Pi.
La rapsberry Pi almacenaría todas las contraseñas en una base de datos y mandaría la contraseña correcta al arduino dependiendo de la hora. Es aquí como no sé como interactuar entre la rapsberry y arduino.
He visto manuales que utilizan Python para escribir comandos, pero yo querría que el sistema estuviera totalmente automatizado.
¿Alguna idea que puedas darme? ¿Sería la solución más correcta para diseñar este control de acceso por horas o se te ocurre alguna otra manera mejor?
Muchas gracias por tu ayuda de antemano, saludos, Nacho.
¿Esto es para un sistema de producción o lo estás armando como una simple "prueba de concepto"?
Si es para una prueba de concepto, puedes fácilmente guardar una lista de passwords (en tanto no sean muy extensos) en la memoria del Arduino y utilizar un módulo de "reloj de tiempo real" para llevar registro de las horas y cambiar los passwords acorde a la hora del reloj.
La comunicación a través de un módulo bluetooth y serial no es demasiado compleja, puedes encontrar un ejemplo en la última entrada que publiqué en el blog
Ahora, si estás pensando en un sistema de acceso centralizado, que puedas manejar desde red o por medio de un nodo central. Te recomendaría mejor no utilizar Arduino y armarlo completamente en Raspberry Pi. Existen algunos módulos de expansión de la Raspberry Pi como el PiFace que te servirán para controlar actuadores (el seguro de la puerta) y puedes comprar un dongle bluetooth USB para poder establecer las conexiones.
Obviamente si eliges esto último tendrás que aprender un poco de python (que no es tán dificil como parece).
Espero esto te de ideas de como realizar tu proyecto.
¡Saludos!
Mario.
Muchas gracias por tu respuesta. Se trata de un proyecto "semi profesional" por lo que, dado tu consejo, me decantaré por utilizar la RSPI+PIFACE.
He visto tu entrada en el blog de bluetooth con arduino. Con la RSPI, ¿sería algo similar? ¿Podría realizar una app en android que se conecte via BT a la RSPI?
¿Me podrías aconsejar algún USB dongle BT?
Perdona por hacerte tantas preguntas.
¿Me podrías enviar un correo a igburmar@hotmail.com o decirme tu email para realizarte unas consultas en el ámbito privado profesional?
Saludos y gracias de antemano.
Muchas gracias por tu respuesta. Se trata de un proyecto "semi profesional" por lo que, dado tu consejo, me decantaré por utilizar la RSPI+PIFACE.
He visto tu entrada en el blog de bluetooth con arduino. Con la RSPI, ¿sería algo similar? ¿Podría realizar una app en android que se conecte via BT a la RSPI?
¿Me podrías aconsejar algún USB dongle BT?
Perdona por hacerte tantas preguntas.
¿Me podrías enviar un correo a igburmar@hotmail.com o decirme tu email para realizarte unas consultas en el ámbito privado profesional?
Saludos y gracias de antemano.