"Después del juego es antes del juego"
Sepp Herberger

lunes, 23 de abril de 2018

Sistema de gestión de turnos y colas (II)

Se acerca la temporada de colas en el IES y desempolvamos nuestro sistema para dar gestionar las colas de espera para hacer algunas reformitas.

En su día creamos 3 scripts para incrementar, decrementar y poner a cero el contador de turnos. Hoy empezamos añadiendo un script nuevo que nos permitirá dar un valor concreto, que entrará como parámetro, al marcador:
# cat pon_turno.sh 
#!/bin/bash

num=$1
test "$num" == "" && num="00"

servidor="172.X.Y.Z"
topic="numero"

if num=$(printf "%02d" $num 2> /dev/null)
then  #Si $num no es un numero, falla el comando anterior
  mosquitto_pub -t $topic -h $servidor -m "A-$num" -r
fi
Esto será útil si hay cualquier fallo que descontrole el marcador y necesitemos ir hasta un número directamente.

Para continuar, se me planteó la siguiente necesidad: controlar los turnos desde una aplicación normal y corriente como alternativa al método de la entrada anterior del blog (la cual usaba un ratón y triggerhappy como sistema para incrementar/decrementar el control del marcador de turnos). Podría escribir una aplicación de escritorio en Python y GTK, ya que con bash sería mas complicado, pero al final me he ido a lo mas sencillo y he optado por usar una aplicación web con Javascript+Mosquitto, usando la misma tecnología con que hice la pantalla que muestra el sistema de turnos del post anterior.

El aspecto de la aplicación será:


Al pulsar "Definir Turno" nos permitirán introducir un turno nuevo directamente:


Todo esto se implementa con una página HTML con mucho CSS y Javascript, ahí va:
# cat /var/www/turnos/GestionTurnos.html 
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html><head>
  <meta content="text/html; charset=ISO-8859-1" http-equiv="content-type">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Sistema de Gestión de Turnos</title>

    <style type="text/css">

        html, body {
             margin: 0px;
             height: 100%;
             background-color: #000099;
        }
        @font-face{
             font-family: 'Antonio';
             src: url('Antonio-Regular.ttf') format('truetype');
        }
        #Pagina {
             font-family: 'Antonio', Arial, sans-serif;
             margin: 0px;
             height: 100%;
        }
        #cabecera {
             font-style: normal;
             font-weight: bold;
             background-color: #000099;
             font-size:7vw;
             width: 100%;
             color: white;
             text-align: center;
             background-image: url(img/logo_guadalupe.png);
             background-position: left center;
             background-repeat: no-repeat;
        }
        #cuerpo {
             color: yellow;
             text-align: center;
             background-color: black;
             font-size:22vw;
             font-weight: bold;
             width: 100%;
             height: 60%;
        }
        #pie {
             font-size:7vw;
             background-color: #000099;
             font-weight: bold;
             width: 100%;
             height: 15%;
             color: white;
             font-style: normal;
             text-align: center;
        }

 .botonAvanza {
  -moz-box-shadow:inset 0px -3px 7px 0px #29bbff;
  -webkit-box-shadow:inset 0px -3px 7px 0px #29bbff;
  box-shadow:inset 0px -3px 7px 0px #29bbff;
  background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #2dabf9), color-stop(1, #0688fa));
  background:-moz-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:-webkit-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:-o-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:-ms-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:linear-gradient(to bottom, #2dabf9 5%, #0688fa 100%);
  filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#2dabf9', endColorstr='#0688fa',GradientType=0);
  background-color:#2dabf9;
  -moz-border-radius:12px;
  -webkit-border-radius:12px;
  border-radius:12px;
  border:1px solid #0b0e07;
  display:inline-block;
  cursor:pointer;
  color:#ffffff;
  font-family:Arial;
  font-size:28px;
  padding:22px 48px;
  text-decoration:none;
  text-shadow:0px 1px 0px #263666;
                vertical-align:middle;
 }
 .botonAvanza:hover {
  background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #0688fa), color-stop(1, #2dabf9));
  background:-moz-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:-webkit-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:-o-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:-ms-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:linear-gradient(to bottom, #0688fa 5%, #2dabf9 100%);
  filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#0688fa', endColorstr='#2dabf9',GradientType=0);
  background-color:#0688fa;
 }
 .botonAvanza:active {
  position:relative;
  top:1px;
 }

 .botonRetrocede {
  -moz-box-shadow:inset 0px -3px 7px 0px #29bbff;
  -webkit-box-shadow:inset 0px -3px 7px 0px #29bbff;
  box-shadow:inset 0px -3px 7px 0px #29bbff;
  background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #2dabf9), color-stop(1, #0688fa));
  background:-moz-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:-webkit-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:-o-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:-ms-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:linear-gradient(to bottom, #2dabf9 5%, #0688fa 100%);
  filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#2dabf9', endColorstr='#0688fa',GradientType=0);
  background-color:#2dabf9;
  -moz-border-radius:12px;
  -webkit-border-radius:12px;
  border-radius:12px;
  border:1px solid #0b0e07;
  display:inline-block;
  cursor:pointer;
  color:#ffffff;
  font-family:Arial;
  font-size:28px;
  padding:22px 48px;
  text-decoration:none;
  text-shadow:0px 1px 0px #263666;
                vertical-align:middle;

 }
 .botonRetrocede:hover {
  background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #0688fa), color-stop(1, #2dabf9));
  background:-moz-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:-webkit-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:-o-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:-ms-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:linear-gradient(to bottom, #0688fa 5%, #2dabf9 100%);
  filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#0688fa', endColorstr='#2dabf9',GradientType=0);
  background-color:#0688fa;
 }
 .botonRetrocede:active {
  position:relative;
  top:1px;
 }

       .botonSet {
  -moz-box-shadow:inset 0px -3px 7px 0px #29bbff;
  -webkit-box-shadow:inset 0px -3px 7px 0px #29bbff;
  box-shadow:inset 0px -3px 7px 0px #29bbff;
  background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #2dabf9), color-stop(1, #0688fa));
  background:-moz-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:-webkit-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:-o-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:-ms-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:linear-gradient(to bottom, #2dabf9 5%, #0688fa 100%);
  filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#2dabf9', endColorstr='#0688fa',GradientType=0);
  background-color:#2dabf9;
  -moz-border-radius:12px;
  -webkit-border-radius:12px;
  border-radius:12px;
  border:1px solid #0b0e07;
  display:inline-block;
  cursor:pointer;
  color:#ffffff;
  font-family:Arial;
  font-size:28px;
  padding:22px 48px;
  text-decoration:none;
  text-shadow:0px 1px 0px #263666;
                vertical-align:middle;

 }
 .botonSet:hover {
  background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #0688fa), color-stop(1, #2dabf9));
  background:-moz-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:-webkit-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:-o-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:-ms-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:linear-gradient(to bottom, #0688fa 5%, #2dabf9 100%);
  filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#0688fa', endColorstr='#2dabf9',GradientType=0);
  background-color:#0688fa;
 }
 .botonSet:active {
  position:relative;
  top:1px;
 }

        .inputturno {
                -moz-box-shadow:inset 0px -3px 7px 0px #29bbff;
                -webkit-box-shadow:inset 0px -3px 7px 0px #29bbff;
                box-shadow:inset 0px -3px 7px 0px #29bbff;
                background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #2dabf9), color-stop(1, #0688fa));
                background:-moz-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
                background:-webkit-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
                background:-o-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
                background:-ms-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
                background:linear-gradient(to bottom, #2dabf9 5%, #0688fa 100%);
                filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#2dabf9', endColorstr='#0688fa',GradientType=0);
                background-color:#2dabf9;
                -moz-border-radius:12px;
                -webkit-border-radius:12px;
                border-radius:12px;
                border:1px solid #0b0e07;
                display:inline-block;
                cursor:pointer;
                color:#ffffff;
                font-family:Arial;
                font-size:28px;
                padding:22px 48px;
                text-decoration:none;
                text-shadow:0px 1px 0px #263666;
                vertical-align:middle;
                width: 30px;
        }

       .labelturno {
                display:inline-block;
                cursor:pointer;
                color:#ffffff;
                font-family:Arial;
                font-size:28px;
                padding:22px 2px;
                text-decoration:none;
                text-shadow:0px 1px 0px #263666;
                vertical-align:middle;
        }

    
    </style>

    <script src="http://tercero/turnos/mqttws31.js" type="text/javascript"></script>
    <script src="http://tercero/turnos/jquery.min.js" type="text/javascript"></script>
    <script src="http://tercero/turnos/config.js" type="text/javascript"></script>

    <script type="text/javascript">

    var valor;
    var mqtt;
    var reconnectTimeout = 2000;
    function MQTTconnect() {
 if (typeof path == "undefined") {
  path = '/mqtt';
 }
 mqtt = new Paho.MQTT.Client(
   host,
   port,
   path,
   "web_" + parseInt(Math.random() * 100, 10)
 );
        var options = {
            timeout: 3,
            useSSL: useTLS,
            cleanSession: cleansession,
            onSuccess: onConnect,
//            mqttVersion: 3, 
            onFailure: function (message) {
                $('#status').val("Connection failed: " + message.errorMessage + "Retrying");
                setTimeout(MQTTconnect, reconnectTimeout);
            }
        };

        mqtt.onConnectionLost = onConnectionLost;
        mqtt.onMessageArrived = onMessageArrived;

        if (username != null) {
            options.userName = username;
            options.password = password;
        }

        console.log("Host="+ host + ", port=" + port + ", path=" + path + " TLS = " + useTLS + " username=" + username + " password=" + password);
        mqtt.connect(options);
    }

    function onConnect() {
        $('#status').val('Connected to ' + host + ':' + port + path);
        // Connection succeeded; subscribe to our topic
        mqtt.subscribe(topic, {qos: 0});
        $('#topic').val(topic);
    }

    function onConnectionLost(response) {
        setTimeout(MQTTconnect, reconnectTimeout);
        $('#status').val("connection lost: " + responseObject.errorMessage + ". Reconnecting");

    };

    function onMessageArrived(message) {

        var topic = message.destinationName;
        var payload = message.payloadString;
        valor=payload;
        $('#cuerpo').html(payload);
    };


    $(document).ready(function() {
        MQTTconnect();
        var caja=$('#id_turno');
        caja.hide();
        var label=$('#id_label');
        label.hide();
    });

    function subirTurno () {

         var numero=valor.substring(2);
         if (numero==99) numero=0;
         else numero++;
          
         topicMessage = new Paho.MQTT.Message("A-"+("00" + numero).slice (-2));  //Para añadir padZero a la izquierda del numero
         topicMessage.destinationName = "numero";
         topicMessage.retained = true;
         mqtt.send(topicMessage);

    }

    function bajarTurno () {

         var numero=valor.substring(2);
         if (numero==0) numero=0;
         else numero--;

         topicMessage = new Paho.MQTT.Message("A-"+("00" + numero).slice (-2));  //Para añadir padZero a la izquierda del numero
         topicMessage.destinationName = "numero";
         topicMessage.retained = true;
         mqtt.send(topicMessage);

    }


   function verCaja () {

        var caja=$('#id_turno');
        caja.show();
        var numero=valor.substring(2);
        caja.val(numero); 
        var label=$('#id_label');
        label.show();
        var boton=$('#id_boton');
        boton.hide();

   }


   function ponerTurno () {

        var caja=$('#id_turno');
        var numero=caja.val();

        if ( !! numero && ! isNaN(numero) ) {            // !!numero-> string no vacio.
            caja.hide();
            var label=$('#id_label');
            label.hide();
            var boton=$('#id_boton');
            boton.show();muchomucho
            if ( numero<0 || numero > 99) numero=valor.substring(2);
            topicMessage = new Paho.MQTT.Message("A-"+("00" + numero).slice (-2));  //Para añadir padZero a la izquierda del numero
            topicMessage.destinationName = "numero";
            topicMessage.retained = true;
            mqtt.send(topicMessage);
        }

   }

    </script>
</head>
<body>
  <div id="Pagina">
    <input type='text' id='topic' disabled hidden/>
    <input type='text' id='status' size="80" disabled hidden/>
    <div id="cabecera">IES VIRGEN DE GUADALUPE</div>
    <div id="cuerpo">TURNO</div>
    <div id="pie">
       <a href="#" onclick="subirTurno();" class="botonAvanza">Avanza Turno</a>
       <a href="#" onclick="bajarTurno();" class="botonRetrocede">Retrocede Turno</a>
       <a href="#" onclick="verCaja();" class="botonSet" id="id_boton">Definir Turno</a>
       <label class="labelturno" id="id_label">Introduzca nuevo turno ==></label>
       <input id="id_turno" class="inputturno" onclick="this.select()" onKeyDown="if (event.keyCode==13) ponerTurno();" value="" />
    </div>
  </div>
</body>
</html>
Todos los ficheros necesarios ya los enlacé en entrada anterior del blog. Simplemente habría que añadir GestionTurnos.html en el mismo sitio web donde pusimos SuTurno.html

Al estar basada en Mosquitto la página web GestionTurnos.html se actualizará en tiempo real y si está abierta en varias ubicaciones (por ejemplo todos los PC de administración) cualquier cambio se propagará a todas los demás de forma inmediata.

Una vez la tenemos funcionando se me ocurre dar una vuelta de tuerca más y convertir la aplicación web en una aplicación independiente de escritorio. De esta manera correrá en una ventana autónoma del navegador que podrá minimizarse y maximizarse a voluntad y será mas sencilla de usar. Hay muchas herramientas para realizar esta conversión web->escritorio, pero me ha gustado por su sencillez Nativifier. Lo que haremos será:
# cat /etc/apt/sources.list.d/nodesource.list 
deb https://deb.nodesource.com/node_0.10 trusty main
deb-src https://deb.nodesource.com/node_0.10 trusty main
# apt-key adv --recv-keys --keyserver hkp://keyserver.ubuntu.com:80   1655A0AB68576280
# apt-get update
# apt-get install nodejs
# npm install nativefier -g
# nativefier -n "Sistema de Gestión de Turnos"  http://172.X.Y.Z/turnos/GestionTurnos.html
Esto crea un directorio con un fichero ejecutable estándar de Linux que al lanzarlo abre la aplicación Web de Gestión de Turnos como si fuera una aplicación de escritorio. Bastará con poner un acceso directo en el escritorio del usuario y quedará todo simple y accesible.

Si queremos que la aplicación sea usada por todos los usuarios de la máquina lo mas sencillo será llevarla a /opt y dar permisos 777 a todo el directorio donde está.

Bueno, pues ya solo nos falta hacer algún día una aplicación para el móvil...

10 comentarios:

  1. Eres el puto amo,

    Alfonso no me funciona la parte de subir, bajar y poner. Si mando mosquitto_pub desde consola si actualiza....

    Lo estoy haciendo en Raspberry aunque no creo que eso influya.

    Un saludo y gracias.

    ResponderEliminar
  2. Te voy comentando por si se te ocurre algo. Lo único que creo hay que configurar es el archivo config.js en el que pongo la Ip de la Raspberry y el puerto. Si pongo la Ip que le asigna el router, desde cualquier sitio puedo acceder a la web y ver la cola, pero no funcionan los botones de subir, bajar o poner. Si lo configuro con 127.0.0.1 solo funciona desde la propia Raspberry, pero tampoco funcionan los botones...

    Gracias por todo.

    ResponderEliminar
    Respuestas
    1. Hola Juan. En principio es solo configurar config.js, no tiene más.

      Lo único que se me ocurre es que sea un fallo de javascript. ¿Tienes la raspberry pi con GestionTurnos.html en una dirección pública a la que pueda acceder desde dentro de la red educativa?

      Eliminar
    2. Como estoy con las pruebas, la tengo en casa. Si quieres echarle un vistazo:
      shaserver2.ddns.net

      Puerto 80 y 22 hacia la Raspberry

      Eliminar
    3. OK. La variable "valor" toma valor "undefined". Eso quiere decir que no hay nada en la cola mqtt cuando carga la página web.

      Tendría que entrar por ssh para ver que pasa con la cola, pero no tengo contraseña para entrar ahí. ¿Me la haces llegar a alfonso.pastor(arroba)gmail.com?

      Eliminar
    4. Esperate. Antes de seguir he entrado en la depuración de Firefox y me salen 2 cosas.

      En ponerTurno tienes:

      boton.show();muchomucho

      Ese "muchomucho" rompe todo el código Javascript de la máquina.

      Luego, en la consola del depurador me dice:

      Firefox no puede establecer una conexión con el servidor en ws://192.168.10.111:8000/mqtt.

      Pero claro, mi navegador no puede acceder ahí ya que no alcanza esa máquina. Esto está pensado para ser ejecutado en una misma red todo, no en una página publicada a través de Internet, ya que el direccionamiento es diferente.






      Eliminar
  3. Como te he comentado por privado, funcionando.
    Eres una makina.

    Un saludo y te debo unos cafés con algo de picar.

    ResponderEliminar
    Respuestas
    1. Me alegro :-). A ver si rula bien, ya me cuentas cuando se ponga en funcionamiento en entorno real.

      Eliminar
  4. Funciona perfectamente, he creado cinco html (mesa A, mesa B, mesa C, mesa D) cada administrativo entra en su html correspondiente para bajar y subir número y cambiar letra. Por otro lado está el Index que es el que se muestra por pantalla sin botones.

    Muchísimas gracias.

    ResponderEliminar
    Respuestas
    1. Me alegro, es un placer que te sea útil. Me apunto lo de los html por mesas, aquí de momento al haber solo 2 mesas se coordinarán entre ellas pero nunca se sabe si tendremos que montarlo como tú.
      Saludos.

      Eliminar