07 enero 2013

Seguro programable con Arduino y Raspberry PI (Parte II)

En la primera parte de esta entrada, hablamos de la programación del módulo Arduino para simular el seguro de una caja fuerte. En esta segunda entrada nos centraremos en el funcionamiento de la Raspberry PI y en la creación de una pequeña herramienta que nos permita realizar monitoreo remoto del módulo.

Antes de continuar les recomiendo leer las siguientes entradas (si aún no lo han hecho):


La idea es que al terminar podamos hacer uso de SQLite para ir almacenando los diferentes eventos que genera nuestro Arduino y de esta manera poder consultarlos luego haciendo uso de una pequeña aplicación web.

Un daemon para monitorearlo todo


Explicando el término "daemon"


En terminología unix, un Daemon es un pequeño programa que se mantiene esperando por que ocurra algo (como que un cliente acceda a una página web) o se mantiene realizando una tarea específica, esto lo realiza en segundo plano y sin esperar intervención de un usuario.

Sucede que no sabemos en que momento nuestro Arduino va a generar algún evento, solo sabemos que ocurrirá, así que... ¿Por qué no crear un daemon que se encargue de monitorear la línea serial?

Hay otra razón muy importante por la cual nos interesa utilizar un "daemon" que actúe como intermediario. Hasta este punto nuestro Arduino es un "terminal tonto", simplemente envía comandos en base al estado del seguro y las únicas funciónes interactivas vía serial son la de cambiar la clave y autorizar usuarios. Este "daemon" nos permitirá realizar operaciones de más alto nivel, ayudándonos a procesar los comandos crudos y convertirlos en un registro con hora y fecha en una base de datos que podemos consultar luego desde cualquier otro programa.

Para programar nuestro Daemon utilizaremos Python. La razón de utilizar este lenguaje es que primero es un lenguaje interpretado de alto nivel y hay muchas bibliotecas a nuestra disposición para realizar cosas como acceder al puerto serial.

La mayoría de daemons en unix estan escritos en C, pero para fines didácticos y considerando que la aplicación es bastante simple Python realiza su trabajo de manera excelente.

Instalando el software necesario


La distribución raspian para nuestra Raspberry PI ya incluye una versión de Python que podemos utilizar para realizar nuestros programas, sin embargo no se incluye ninguna biblioteca para acceder al puerto Serial.

También tenemos que instalar una biblioteca que nos permita acceder a bases de datos SQLite la cual utilizaremos para almacenar todos los eventos.

Para instalar la biblioteca "pyserial" y el soporte para sqlite debemos de ejecutar el siguiente comando en nuestra Raspberry PI:

$ sudo apt-get install python-serial python-sqlite2

Leyendo el puerto serial


Antes de continuar debemos verificar que nuestra cuenta "pi" tenga acceso al puerto serial, para ello debe de pertenecer al grupo "dialout". Si no realizamos este paso siempre podremos acceder al puerto serial pero debemos de ejecutar los comandos con "sudo".

Para agregar el usuario "pi" al grupo "dialout" ejecutamos el siguiente comando:

$ sudo usermod pi -a -G dialout

Nota: En esta entrada voy a realizar una aproximación diferente al código. En la entrada anterior utilizamos una aproximación "de abajo hacia arriba", explicando primero las funciones "ayudantes" y luego examinábamos el código en un nivel de abstracción más alto. Esta vez voy a realizar lo contrario, vamos a realizar una aproximación "de arriba hacia abajo" examinando primero el funcionamiento de la herramienta primero y luego examinaremos el código y las funciones subyacentes.

Nuestro pequeño "daemon" realiza la tarea de monitorear el puerto serial y guardar los eventos que detecte en una base de datos sqlite, antes de invocarlo podemos leer la ayuda escribiendo el siguiente comando:

$ python serialreader.py

La salida es la siguiente:
Usage:
python serialreader.py start
python serialreader.py stop
python serialreader.py foreground

El funcionamiento es bastante sencillo:
  • "start" el daemon se iniciará como tarea en segundo plano con lo que podemos dejarlo corriendo permanentemente aunque cerremos la sesión. 
  • "stop" por otra parte envia por medio de una señalización muy sencilla, un comando de parada al daemon para que detenga su ejecución.
  • "foreground" es un comando que no utilizaremos directamente, su funcion es ejecutar el daemon en primer plano, puede resultar útil para tareas de "debug".

Procesando la línea de comandos en el Daemon

Procesar los comandos en python es una tarea bastante sencilla, simplemente tenemos que revisar el arreglo "sys.arvg", este arreglo guarda los argumentos pasados por línea de comandos al momento de la ejecución del script, por convención siempre tiene en su primer elemento sys.argv[0]  el nombre del script que estamos ejecutando, en este caso "serialreader.py". Para procesar los argumentos nuestro daemon revisará el tamaño de este arreglo y luego actuará acorde al texto del segundo argumento almacenado en sys.argv[1].

pidfile = '/var/run/serialreader.pid'
datafile = '/var/db/serialreader/serial_events.db'
commPort = '/dev/ttyAMA0'


if __name__ == '__main__':
  if len(sys.argv)>1 and sys.argv[1]=='stop':
    if os.path.exists(pidfile):
      sys.stdout.write("Stopping main thread...")
      os.remove(pidfile)
      print " Stopped."
    else:
      print "No running process found."
  elif len(sys.argv)>1 and sys.argv[1]=='start':
    if os.path.exists(pidfile):
      print "Main thread already started."
      print "To stop run:"
      print "python serialreader.py stop"
    else:
      print "Starting daemon"
      os.system("/usr/bin/python serialreader.py foreground &");
  elif len(sys.argv)>1 and sys.argv[1]=='foreground':
    ser = serial.Serial(
      commPort,
      baudrate=9600,
      interCharTimeout=None
    )

    threading.Thread(target=receiving,args=(ser,)).start()
  else:
    print "Usage:"
    print "python serialreader.py start"
    print "python serialreader.py stop"
    print "python serialreader.py foreground"

El primer if, simplemente revisa si el script de python se encuentra en el hilo de ejecución principal.

Para detener el proceso en vez de utilizar algún método de comunicación entre-procesos, vamos a utilizar una sencilla técnica que hace uso de un "pidfile". Un pidfile es un sencillo archivo de texto que guarda el identificador de proceso (PID) del daemon. El "loop" que se encarga de leer el puerto serial se mantiene corriendo mientras exista el pidfile, en el momento que no lo encuentre este se detiene.

Si revisan el "if" asociado al comando stop, su único trabajo es eliminar el pidfile del daemon. Si no encuentra un pidfile simplemente nos advierte que no ha encontrado ningún pidfile lo que implica que el daemon no está corriendo. Al inicar con el comando "start" la lógica es bastante similar, primero verificamos que no exista el pidfile y con el comando "os.system" ejecutamos una nueva instancia del daemon en segundo plano. Si por el contrario existe un pidfile mostramos la advertencia de que ya hay un daemon corriendo.

La tercera y tal vez la más importante es la que se "conecta" al puerto serial y lanza un nuevo hilo de ejecución.

El loop principal

Con la función Thread generamos un nuevo hilo de ejecución que se encarga de monitorear continuamente el puerto serial hasta que encuentre una orden de parada (para nuestro caso que no encuentre el pidfile).

El código que maneja esta lectura es el siguiente:

last_received = ''
def receiving(ser):
  global last_received,pidfile,datafile
  # Create pidfile
  f = open(pidfile,'w')
  f.write(str(os.getpid()))
  f.close()

  conn = sqlite3.connect(datafile)
  buffer = ''

  while os.path.exists(pidfile):
    buffer += ser.read(ser.inWaiting())
    if '\n' in buffer:
      lines = buffer.split('\n')
      last_received = lines[-2]
      buffer = lines[-1]
      storeCode(conn,last_received.strip())

  ser.close()
  print "Ending main thread"

Este pequeño código es una pequeña modificación a una respuesta encontrada en StackOverflow para la lectura de "el último" carácter en una línea "serial" en Python.

Al revisar un poco el código podemos ver que la primera operación es la creación del "pidfile", la función os.getpid() nos permite obtener el identificador del proceso actual. El siguiente paso es la apertura de nuestra base de datos SQLite y por último, en lo que estamos más interesados, mientras exista el pidfile vamos a llamar a la función "inWaiting()" del puerto serial. Esta funcion realiza la "espera" de datos y devuelve los caracteres que se hayan leido una vez esten disposibles. Luego, si detectamos un fin de línea en lo que se ha leido dividimos la línea en el "fin de línea" y el comando corresponderá a lo que se encuentra antes del fin de línea. Todo lo que se encuentre después corresponderá al siguiente comando y por ello lo agregamos al buffer.

Para almacenar el comando llamamos a la función "storeCode()" que toma como parámetros la conexión a la base SQLite y el comando sin espacios ni retornos de línea que obtenemos con la función "strip()";

Guardando el código en SQLite


La función "storeCode" se encarga de guardar el código en SQLite, esta no resulta demasiado complicada ya que es una simple consulta SQL. Sin embargo debo hacer notar algo: En SQLite vamos a asumir que las tablas en la base no existen. A diferencia de un gestor relacional de toda la vida, con SQLite podemos crear tablas "al vuelo", en este caso vamos a asumir que nuestra tabla no existe y si no existe simplemente la vamos a crear con una segunda función ayudante "initDB", luego "revisamos" que el código a ingresar exista con la función "checkValue" y por último guardamos la hora local del evento más el código del comando correspondiente.

Consulta SQL:
INSERT INTO
  events(
    unixtime,
    code_id
  )
  VALUES(
    (?),
    (
      SELECT
        codes.id
      FROM
        codes
      WHERE
        name=(?)
    )
  )

Código en Python:
def storeCode(connection,evt):
  initDB(connection)
  checkValue(connection,evt)
  c=connection.execute(' \
INSERT INTO \
  events( \
    unixtime, \
    code_id \
  ) \
  VALUES( \
    (?), \
    ( \
      SELECT \
        codes.id \
      FROM \
        codes \
      WHERE \
        name=(?) \
    ) \
  ) \
  ',(time.mktime(time.localtime()),evt))
  connection.commit()


La función mktime() genera un "entero largo" que representa el número de segundos transcurridos desde el "EPOCH". Esta es una forma sencilla de guardar las fechas, es el estándar en sistemas unix y luego podemos procesarlo en PHP para desplegar la fecha en un formato más amigable.

Luego tenemos la función ayudante "initDB" que se encarga de crear las tablas si no existen e ingresar los comandos correspondientes en una tabla secundaria:

Consulta SQL:
-- Tabla de codigos
CREATE TABLE IF NOT EXISTS
  codes(
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT
  )
-- Tabla de eventos
CREATE TABLE IF NOT EXISTS
  events(
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    unixtime INTEGER,
    code_id INTEGER
  )

Código en Python:
def initDB(connection):
  connection.execute('\
CREATE TABLE IF NOT EXISTS \
  codes( \
    id INTEGER PRIMARY KEY AUTOINCREMENT, \
    name TEXT \
  ) \
  ');
  commands = (
    'ST',
    'HR',
    'DO',
    'BC',
    'DC',
    'KSOK',
    'KSER',
    'IDOK',
    'IDER'
    )
  for (i,code) in enumerate(commands):
    checkValue(connection,code)
  connection.execute('\
CREATE TABLE IF NOT EXISTS\
  events(\
    id INTEGER PRIMARY KEY AUTOINCREMENT,\
    unixtime INTEGER,\
    code_id INTEGER\
  )\
  ')

Por último la función "checkCode()" simplemente se encarga de verificar si el comando existe y si no, genera un código en la tabla de códigos correspondiente:

Consulta SQL:
-- Verificacion de existencia de codigo
SELECT
  COUNT(*) as TOTAL
FROM
  codes
WHERE
  name=(?)
-- Insercion de codigo nuevo
INSERT INTO
  codes(
    name
  )
  VALUES(
    (?)
  )

Código en Python:
def checkValue(connection,code):
  c = connection.execute('\
SELECT \
  COUNT(*) as TOTAL \
FROM \
  codes \
WHERE \
  name=(?) \
  ',(code,))
  row = c.fetchone()
  if row[0]==0:
    connection.execute('\
INSERT INTO \
  codes( \
    name \
  ) \
  VALUES( \
    (?) \
  ) \
  ',(code,))

Instalando nuestro script como servicio


Originalmente había pensado dejar el script para ser invocado de forma manual, sin embargo resulta muchísimo más sencillo de utilizar si lo instalamos como un servicio en Debian.

Primero tenemos que crear algunos directorios y archivos, nuestro script va a "residir" en "/usr/local":

$ sudo mkdir /usr/local/serialreader

Luego tenemos que copiar nuestro script a la nueva carpeta:
$ sudo cp serialreader.py /usr/local/serialreader

Lo siguiente es crear una carpeta en /var donde guardaremos la base de datos, por el momento dejaremos los permisos de la carpeta en "777" indicando lectura, acceso y escritura por todos los usuarios:
$ sudo mkdir -p /var/db/serialreader
sudo chmod 777 /var/db/serialreader/serial_events.db

Luego crearemos un archivo vacío para nuestra base de datos, esto lo hacemos con el comando "touch" y cambiamos luego los permisos:
$ sudo touch /var/db/serialreader/serial_events.db
$ sudo chmod 666 /var/db/serialreader/serial_events.db

Por último deberán crear un "script" para init, pueden buscar en google una plantilla, esta es el que yo utilicé:
!/bin/bash
# serialreader daemon
# description: serialreader daemon
# processname: serialreader

DAEMON_PATH="/var/db/serialreader"

DAEMON="/usr/bin/python /usr/local/serialreader/serialreader.py"
DAEMONOPTS="foreground"

NAME=serialreader
DESC="Serial port monitor for Arduino"
PIDFILE=/var/run/$NAME.pid
SCRIPTNAME=/etc/init.d/$NAME

case "$1" in
start)
  printf "%-50s" "Starting $NAME..."
  cd $DAEMON_PATH
  PID=`$DAEMON $DAEMONOPTS > /dev/null 2>&1 & echo $!`
  #echo "Saving PID" $PID " to " $PIDFILE
        if [ -z $PID ]; then
            printf "%s\n" "Fail"
        else
            echo $PID > $PIDFILE
            printf "%s\n" "Ok"
        fi
;;
status)
        printf "%-50s" "Checking $NAME..."
        if [ -f $PIDFILE ]; then
            PID=`cat $PIDFILE`
            if [ -z "`ps axf | grep ${PID} | grep -v grep`" ]; then
                printf "%s\n" "Process dead but pidfile exists"
            else
                echo "Running"
            fi
        else
            printf "%s\n" "Service not running"
        fi
;;
stop)
        printf "%-50s" "Stopping $NAME"
            PID=`cat $PIDFILE`
            cd $DAEMON_PATH
        if [ -f $PIDFILE ]; then
            kill -HUP $PID
            printf "%s\n" "Ok"
            rm -f $PIDFILE
        else
            printf "%s\n" "pidfile not found"
        fi
;;

restart)
    $0 stop
    $0 start
;;

*)
        echo "Usage: $0 {status|start|stop|restart}"
        exit 1
esac

Notarán que los únicos cambios son las ubicaciones nuestro pidfile y el nombre del ejecutable, que apunta a /usr/local/serialreader/serialreader.py con el comando foreground, esta vez utilizo esa opción ya que el script de Init se encargará de mandar el proceso a segundo plano.

Copien este archivo a /etc/init.d y cambien los permisos a ejecutable:
$ sudo cp serialreader /etc/init.d
$ sudo chmod 755 /etc/init.d/serialreader

Una vez listo esto para iniciar o detener el monitoreo del puerto serial solo tienen que ejecutar:
$ sudo service serialreader start
$ sudo service serialreader stop

Este script sobreescribe el PIDfile que genera nuestro script al invocarlo de manera manual, esto no debe preocuparnos mucho, no quise modificar el script ya que quería mantener la funcionalidad manual o como servicio.

 Creando nuestra página web 


Nota: De aquí en adelante asumo que ya realizaron la instalación de Lighttpd + PHP con soporte para SQLite en su Raspberry PI.

Hasta ahora se han de preguntar ¿Por qué no accedo directamente al Arduino desde PHP?. La respuesta es simple: Los scripts de PHP están diseñados para web y están basados en la arquitectura de consultas cliente-servidor. Un script solo se ejecuta cuando recibe una petición de un cliente. Si no existiera un "daemon" que monitoreara permanentemente el puerto serial perdería todos los eventos en tanto no hubieran usuarios realizando solicitudes, la base de datos SQLiite me sirve de registro y caché para guardar los eventos. Adicionalmente puedo tener varios usuarios consultando la base al mismo tiempo lo que es ideal en el caso de aplicaciones web.

Habiendo dicho eso, una vez tenemos lista nuestra base de datos y nuestro Daemon corriendo, este se encargará de guardar todos los eventos que se generen por el arduino en la base de datos SQLite. Lo que viene después, es muchísimo más fácil. Simplemente trabajaremos una pequeña aplicación web que lea la información y la reporte en una página web.

Nuestra pequeña página tiene tres componentes:
  • Una página de autenticación.
  • Una página donde podremos visualizar los últimos diez eventos y cambiar la clave.
  • Una página que nos servirá para cerrar la sesión.
Para evitar el tener que hacer consultas al puerto serial cada vez que deseemos entrar a la página, vamos a crear una pequeñísima tabla que llamaremos "settings", esta tabla contendrá pares de "nombres" y "valores" en la que guardaremos información sobre nuestra aplicación.

Así que antes de continuar, vamos a crear una peqeña clase en PHP que nos ayudará a "ocultar" estas operaciones de bajo nivel.

Archivo dbFunctions.inc.php


<?php
class SimpleLock {

  private $db;
  private $commPort;

  function __construct($dataFile='serial_events.db',$commPort='/dev/ttyAMA0') {
    try {
      $this->commPort = $commPort;
      $this->db = new PDO("sqlite:$dataFile");
      $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
      $this->db->exec('CREATE TABLE IF NOT EXISTS settings(name TEXT, value TEXT)');
      $this->checkSetting('key','000');
      $this->checkSetting('lastLogin');
    } catch(Exception $e) {
      print_r($e);
    }
  }
  
  function checkSetting($name,$defaultValue=NULL) {
    $settingExists = false;
  
    $sth = $this->db->prepare('
      SELECT COUNT(*) AS total FROM settings WHERE name = :name
    ');
    $sth->execute(array(':name'=>$name));|
    $row = $sth->fetch();
    if($row['total']==0) {
      $sth = $this->db->prepare('
        INSERT INTO settings(name,value)
          VALUES(:name,:value)
      ');
      $sth->execute(array(
        ':name'=>$name,
        ':value'=>$defaultValue
      ));
      $settingExists = false;
    } else {
      $settingExists = true;
    }
    return $settingExists;
  }
  
  function getSetting($name) {
    $this->checkSetting($name);
    $sth = $this->db->prepare('
      SELECT
        value
      FROM
        settings
      WHERE
        name = :name
    ');
    $sth->execute(array(':name'=>$name));
    $row = $sth->fetch();
    return $row['value'];
  }
  
  function setSetting($name,$value) {
    $this->checkSetting($name);
    $sth = $this->db->prepare('
      UPDATE
        settings
      SET
        value = :value
      WHERE
        name = :name
    ');
    $sth->execute(array(
      ':value' => $value,
      ':name' => $name
    ));
  }
 
  function getDB() {
    return $this->db;
  }

  function changeKey($origKey,$newKey) {
    exec("/bin/echo -e \"SK$origKey$newKey\\n\" > $this->commPort");
    sleep(1); // We nee to give time to the daemon to write
    // to the database
    $row = $this->db->query('
SELECT
  unixtime
FROM
  events
WHERE
  code_id = (SELECT id FROM codes WHERE name = "KSOK")
ORDER BY unixtime DESC
LIMIT 1
   ')->fetch();
    if(!empty($row)) {
      if((time()-$row['unixtime'])<3) {
        $this->setSetting('key',$newKey);
        return TRUE;
      } else {
        return FALSE;
      }
    } else {
      return FALSE;
   }
  }
}


Los métodos que nos provee esta clase son las siguientes:
  • Constructor: Este método se encarga de abrir la base de datos e inicializar algunas la tabla de "settings" al crear una instancia de la clase. Adicionalmente creamos nuestro setting "key" que almacenará la llave. Se asume que el seguro en Arduino tiene el código por defecto.
  • checkSetting($name,$defaultValue): Se encarga de verificar si la opción de configuración existe y si no es así la crea con el valor por defecto especificado.
  • getSetting($name): Se encarga de devolver el valor almacenado en el setting especificado, si este no existe se crea "al vuelo".
  • setSetting($name,$value): Se encarga de actualizar un setting existente, esta función no hace nada si el setting no existe.
  • changeKey($origKey,$newKey): Se encarga de cambiar la llave en el Arduino conectado vía serial. Este comando se encarga de enviar el código de cambio de clave con el comando "echo" y espera a que se actualice la base de datos por el daemon para así verificar que se cambió la llave.

Archivo login.php


La página de login simplemente se encarga de consultar a la base de datos con la función getSetting('key') para obtener la llave actual:
<?php
include("config.php");
include("dbFunctions.inc.php");
session_start();

// If user already logged, redirect to index.
if(isset($_SESSION['key'])) {
  header("Location: index.php");
  exit();
}

$lock = new SimpleLock($dataFile);


$message = "";
if(isset($_POST['key'])) {
  if(preg_match('/^\d{3}$/',$_POST['key'])!=1) {
    // Verify that this is a valid code.
    $message = "Debe de especificar 3 números entre 0 y 9 como clave.";
  } else {
    $currentKey = $lock->getSetting('key');
    if($currentKey==$_POST['key']) {
      $_SESSION['key'] = $currentKey;
      header("Location: index.php");
      exit();
    } else {
      $message = "Clave inválida.";
    }
  }
}
header("Content-Type: text/html; charset=utf-8;");
?>
<!DOCTYPE html>
<html>
  <head>
    <title>Inicio de sesión</title>
    <link href="css/default.css" rel="stylesheet" type="text/css" />
  </head>
  <body>
    <h1>Ingrese su clave</h1>
<?php if($message!="") { ?>
      <div id="notice">
        <?php echo $message ?>
      </div>
<?php } ?>
    <form method="post" action="login.php">
      Clave: <input type="password" name="key" /><br />
      <input type="submit" value="Ingresar" />
    </form>
  </body>
</html>

En el login.php hacemos uso de la función "preg_match" para verificar que el usuario ha ingresado un formato correcto de llave.

Archivo index.php


La página principal se encarga de dos cosas, primero mostrar los eventos almacenados en la base de datos y segundo, en caso de recibir una nueva llave de enviar el comando correspondiente a la clase que definimos al principio y en caso de ser correcta actualizar el valor de la llave en la base de datos local de la aplicación:
<?php
include("config.php");
include("dbFunctions.inc.php");
session_start();

if(!isset($_SESSION['key'])) {
  header("HTTP/1.1 401 Unauthorized");
  header("Location: login.php");
  exit();
}

$lock = new SimpleLock($dataFile);

$message = '';
if(isset($_SESSION['message'])) {
  $message = $_SESSION['message'];
  unset($_SESSION['message']);
}

if(isset($_POST['newKey'])) {
  if(preg_match('/^\d{3}$/',$_POST['newKey'])!=1) {
    // Verify that this is a valid code.
    $_SESSION['message'] = "Debe de especificar 3 números entre 0 y 9 como clave.";
  } else {
    if($lock->changeKey($_SESSION['key'],$_POST['newKey'])) {
      $_SESSION['message'] = "Llave cambiada.";
      $_SESSION['key'] = $_POST['newKey'];
    } else {
      $_SESSION['message'] = "Hubo un problema al cambiar la llave.";
    }
  }
  header("Location: index.php");
  exit();
}

header("Content-Type: text/html; charset=utf-8");
?>
<!DOCTYPE html>
<html>
  <head>
    <title>Registro de eventos</title>
    <link href="css/default.css" rel="stylesheet" type="text/css" />
    <meta http-equiv="refresh" content="10" />
  </head>
  <body>
  <h2>Registro del seguro</h2>
  <table id="evt_table">
    <thead>
      <tr><th>Fecha y Hora</th><th>Evento</th></tr>
    </thead>
    <tbody>
<?php
try {
foreach($lock->getDB()->query("
SELECT
  unixtime,
  name
FROM events JOIN codes ON events.code_id=codes.id
ORDER BY events.id DESC
LIMIT 10
") as $row) {
?>
     <tr>
       <td><?php echo date('Y-m-d H:i:s',$row['unixtime']) ?></td>
       <td><?php echo $row['name'] ?></td>
     </tr>
<?php
}

?>
    </tbody>
  </table>
<?php
} catch(Exception $e) {
  print_r($e);
}
?>
  <br />
<?php if($message!="") { ?>
      <div id="notice">
        <?php echo $message ?>
      </div>
<?php } ?>
  <form method="POST">
  Nueva llave: <input type="password" name="newKey" /><br />
  <input type="submit" value="Cambiar Llave" />
  </form>
  <br />
  <a href="logout.php">Cerrar sesión</a>
  </body>
</html>

En index.php hacemos uso nuevamente de preg_match para validar la nueva llave, notarán que utilizamos la variable $_SESSION['message'], esta variable nos sirve para enviar mensajes que se mostrarán una sola vez luego de la redirección. Para los que han usado Ruby esta variable hace las veces de "flash".

Archivo logout.php


El código de logout es sumamente sencillo, esta página solo se encarga de cerrar la sesión actual y redireccionar a la página de login.
<?php
session_start();
session_unset();
header("Location: login.php");
exit();

Archivo config.php


Notarán además que hemos definido un pequeño archivo "config.php" donde guardamos las opciones de configuración de la aplicación web. Este archivo es sumamente sencillo:

<?php
$dataFile = "/var/db/serialreader/serial_events.db";

¿Cómo funciona?


Puede que en este momento se encuentren algo impacientes por verlo funcionar, así que he puesto una versión en línea en mi RasPI que pueden acceder con el código "000". En esta versión no tengo conectado el módulo Arduino, así que si intentan cambiar la llave recibirán un bonito mensaje de error indicando que la llave no ha podido ser modificada: 


Pueden descargar todos los archivos que hemos utilizado en esta entrada de la siguiente dirección:


Para descomprimir los archivos en las ubicaciones correctas pueden ejecutar el siguiente comando:

$ cd /
$ sudo tar -xzf <ruta_a_simplelock .tar.gz>

Les mostrará la siguiente salida:
usr/local/serialreader/serialreader.py
etc/init.d/serialreader
var/db/serialreader/serial_events.db
var/www/simplelock/config.php
var/www/simplelock/dbFunctions.inc.php
var/www/simplelock/index.php
var/www/simplelock/login.php
var/www/simplelock/logout.php
var/www/simplelock/css/default.css

Solo recuerden modificar los permisos de manera correspondientes antes de intentar ejecutar los archivos.

Recapitulando


Durante las últimas tres semanas hemos aprendido varias cosas que vale la pena listar:
  • Hemos desarrollado un pequeño módulo de pruebas que nos sirve para probar la funcionalidad de entradas y salidas digitales del Arduino, manejo de interrupciones y comunicación de datos vía puerto Serial.
  • Nuestra RasPI posee ahora un pequeño servidor web capaz de hospedar aplicaciones web sencillas.
  • Hemos desarrollado un pequeño daemon que monitorea eventos serial y que es compatible con el sistema de servicios de windows.
  • Creamos una pequeña aplicación web que nos permite interactuar con nuestro Arduino desde Internet.
¿Que más se puede hacer? Pues lo que ustedes se puedan imaginar, con estas herramientas puedan inventar muchísimas cosas, quién quita luego y veamos proyectos interesantes.

Con esta entrada termino la serie de artículos sobre Arduino y Raspberry PI. Espero pronto traerles nuevos proyectos interesantes en los que puedan darle uso a estos conocimientos.

No habiendo nada más que decir... ¡¡¡Hasta la próxima!!!

4 comentarios:

Jesús Alberto Pinedo dijo...

Muchas gracias por tu artículo.
Creo que necesito otro cafe para leer la segunda parte del mismo, de momento he llegado hasta la definición de la db jeje.
Seguiré leyendote.

IngOtonielFlores dijo...

Muy bueno !!! Por si alguno le interesa, he encontrado esto en la RED, venden arduinos en el salvador.

www.ingnac.com

Maurcio Rodriguez dijo...

Hola Mario de nuevo !!!

Pues estoy tratando de hacer un programa que lea usb por medio de una entrada por teclado y eso mandarlos a la salida serial del raspi .. si tienes una idea seria genial gracias

Carlos Rincon dijo...

Muy bueno tu trabajo