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

Mostrando entradas con la etiqueta triggerhappy. Mostrar todas las entradas
Mostrando entradas con la etiqueta triggerhappy. Mostrar todas las entradas

jueves, 1 de diciembre de 2022

Interruptor casero USB.

Vamos a ver como construir un interruptor casero que se conecta por USB y que al pulsarse permite lanzar un evento en el ordenador, ejecutando un script.

Lo primero es construir el interruptor: ¿que usar si no tenemos ni idea de electrónica?. Pues no hace falta nada especial ya que con un ratón de ordenador es suficiente. Todos tenemos ratones USB rotos o antiguos que podemos reciclar. La idea es desmontar el ratón, sacar la placa, arrancar la luz led y verificar que el botón central (el asociado al click de la rueda) funciona y manda señal por el cable USB. Da igual el estado del resto de mecanismos.

Cogemos el ratón, lo metemos dentro de una cajita del tamaño adecuado y lo pegamos usando la pistola térmica de silicona que tenemos todos para las manualidades. En mi caso tenía una cajita que venía perfecta:
Nótese como buscamos que el interruptor del botón quede justo en el centro (marcado con el círculo amarillo). Luego he puesto una tapa de plástico agujereada para permitir llegar al interruptor y forrada con goma eva. Por último hago un interruptor externo con un tapón y un poco de espuma de embalaje. La idea es que el interruptor externo presione al interruptor interno del ratón.
Una vez pegado y montado todo:
Creo que es resultón para lo que yo quiero, pero si queremos hacerlo mas formal siempre podemos usar la impresora 3D para generar una cajita y botón con aspecto mas profesional.

Ya tenemos el hardware, vamos al software. Pinchamos el cable en un puerto USB y el Linux lo detecta como un ratón, pero como esta casi todo inutilizado tan solo recibirá eventos del botón central. Lo primero es encontrar el generador de eventos asociado al nuevo ratón:
# ls -l /dev/input/by-id
total 0
lrwxrwxrwx 1 root root  9 oct 27 09:34 usb-0461_USB_Optical_Mouse-event-mouse -> ../event2
lrwxrwxrwx 1 root root  9 oct 27 09:34 usb-0461_USB_Optical_Mouse-mouse -> ../mouse0
lrwxrwxrwx 1 root root  9 oct 27 09:34 usb-CHICONY_HP_Basic_USB_Keyboard-event-kbd -> ../event3
lrwxrwxrwx 1 root root 10 nov 29 13:32 usb-Logitech_USB-PS_2_Optical_Mouse-event-mouse -> ../event14
lrwxrwxrwx 1 root root  9 nov 29 13:32 usb-Logitech_USB-PS_2_Optical_Mouse-mouse -> ../mouse1
En nuestro caso sería /dev/input/event14. El paso siguiente es verificar que se detectan los eventos de pulsación. Para ello usamos triggerhappy:
# apt-get install triggerhappy
# thd --dump /dev/input/event14
Y pulsamos varias veces el interruptor para ver si se detecta. En consola debería mostrarse:
EV_KEY	BTN_MIDDLE	1	/dev/input/event14
# BTN_MIDDLE	1	command
EV_KEY	BTN_MIDDLE	0	/dev/input/event14
# BTN_MIDDLE	0	command
EV_KEY	BTN_MIDDLE	1	/dev/input/event14
# BTN_MIDDLE	1	command
thd --dump /dev/input/event14
EV_KEY	BTN_MIDDLE	0	/dev/input/event14
# BTN_MIDDLE	0	command
Perfecto: el evento generado por la pulsación del botón es "BTN_MIDDLE 1". Conviene recalcar en este momento que la pulsación del botón central del ratón puede ser también interceptado por nuestro entorno de escritorio Linux y provocar algún efecto asociado (por ejemplo, pegar el texto que hay en el portapapeles). Para evitar efectos colaterales recomendamos desactivar ese ratón en la configuración del entorno de escritorio:

Una vez tenemos el evento localizado, creamos la configuración que asocia dicho evento al lanzamiento de un script:
# cat /etc/triggerhappy/triggers.d/redbutton.conf
BTN_MIDDLE    1       /root/scripts/redbutton.sh
Después de este cambio de configuración se debe reiniciar el servicio triggerhappy, pero antes debemos configurarlo para que se ejecute como root (normalmente lo hace como nobody) y escuche solo los eventos que vengan de /dev/input/event14.

Inocente de mi, probé a modificar tanto /etc/default/triggerhappy como /etc/init.d/triggerhappy sin éxito. Seguia ejecutándose con el usario nobody y escuchando en /dev/input/event*. Esto lo podemos verificar haciendo:
# ps aux | grep thd
nobody   12523  0.1  0.0  43844  2852 ?        Ss   21:32   0:00 /usr/sbin/thd --triggers /etc/triggerhappy/triggers.d/ --socket /run/thd.socket --user nobody --deviceglob /dev/input/event*
La causa de esto es que en Ubuntu 18 el demonio triggerhappy se lanza realmente desde /lib/systemd/system/triggerhappy.service. Los otros dos ficheros están de adorno. No entiendo esta manía de tener servicios que están init.d y en systemd a la vez, es como si hubiesen quedado el trabajo a medias y se hubiesen ido de cañas.

Editamos /lib/systemd/system/triggerhappy.service y lo quedamos:
[Unit]
Description=triggerhappy global hotkey daemon
After=local-fs.target

[Service]
Type=notify
ExecStart=/usr/sbin/thd --triggers /etc/triggerhappy/triggers.d/ --socket /run/thd.socket --user root --deviceglob /dev/input/event14

[Install]
WantedBy=multi-user.target
Luego reiniciamos el servicio:
# service triggerhappy restart
Y bueno ahora nos falta escribir el script. Aqui podemos poner cualquier cosa, yo simplemente voy a hacer que se oiga un audio (descargado de Internet o generado con pico2wave) en el escritorio:
# cat /root/scripts/redbutton.sh 
#!/bin/bash

#Subimos el volumen al máximo en el alsamixer
amixer set Master unmute
amixer set Master 100%

#Si hay un usuario logado, conectamos con su pulseaudio para subir el volumen al máximo
user=$(who | grep "(:0)" | head -1 | cut -f1 -d" "); 
if  [ -n "$user" ]
then
  su $user -c "DISPLAY=:0 pactl set-sink-volume 0 150%"
  su $user -c "DISPLAY=:0 pactl set-sink-mute 0 0"
  cp -f /root/scripts/no.wav /tmp/no.wav
  su $user -c  "DISPLAY=:0 aplay /tmp/no.wav"
else
  cp -f /root/scripts/no.wav /tmp/no.wav
  DISPLAY=:0 aplay /tmp/no.wav
fi

exit 0
Con esto ya tenemos todo funcionando. Bien, y ahora la pregunta: ¿Para qué carajos quiero esto?. Bueno, pues aparte de para enredar, para conectarlo a una Raspberry Pi o un OpenWRT sin teclado/ratón y poder lanzar eventos sobre ellos: apagar/encender la wifi, hacer una foto con la webcam, reiniciar un servicio o una aplicación, etc.

En mi caso tengo una Rasbperry Pi mostrando una página web en modo kiosko con información en tiempo real. A veces el navegador se bloquea y hay que cerrarlo y abrirlo de nuevo, teniendo que conectarme por VNC y hacer el proceso a mano. Con un botón de este tipo puedo lanzar un script que permite a cualquiera reiniciar el navegador en todo momento.

Out!

viernes, 4 de agosto de 2017

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

Hay momentos en que en el centro se forman colas bastante largas, especialmente en época de matriculaciones con la gente de pie durante un largo tiempo. Nos pareció buena idea implementar un sistema de colas con número que tienen en otros sitios, de tal forma que los conserjes irán dando números en orden de llegada, una pantalla visible dirá por que número se va atendiendo y el personal de secretaría podrá incrementar dicho número cuando se quiera pasar al siguiente. La gente podrá estar sentada o salir a dar una vuelta sin tener que estar pendientes de guardar cola.

Empezar desde cero era costoso, pero casi siempre hay alguien que ha abierto camino antes y que nos da todo casi hecho. En este caso es este magnífico blog sobre OpenWrt y otros proyectos interesantes: http://openwrt.tuinstituto.es/sistema-de-gestion-de-colas-de-espera-simple-con-openwrt-y-mqtt.

La infraestructura que necesitamos es:

  • Una pantalla de ordenador en un sitio visible, donde se mostrará el número actual. Dicha pantalla estará conectada a un PC (vale un PC antiguo, una Raspberry PI o una Android Box) capaz de abrir un navegador web, mostrando algo similar a:


  • Un ratón adicional conectado por USB a uno de los PC que ya hay en secretaría. Ese ratón permitirá manipular el número de la cola (botón derecho incrementará el número y botón izquierdo lo decrementará). En el blog de Tuinstituto usan un dispositivo con OpenWRT al que enchufan el ratón para esto, pero yo me lo ahorro y usamos un PC que ya está funcionando al que le ponemos un segundo ratón.
  • Un PC intermedio donde estará la cola que comunica ambos dispostivos y almacena físicamente el número de orden. Este PC no es estrictamente necesario y puede ser tanto el PC de la pantalla como el que maneja la cola, pero en mi caso lo he puesto aparte en uno de los servidores del centro por hacer más completo el sistema y mantener total disponibilidad.

Lo que hace más estimulante para mí esta tarea es el uso de dos prometedoras herramientas que desconocía:

  • mqtt: un protocolo ligerísimo pensado para el uso en IoT (Internet de las Cosas) que permite mandar mensajes mediante colas entre dos puntos de Internet/red local.
  • triggerhappy: un demonio ligerito que permite disparar eventos ante pulsaciones en teclados, ratones y otros dispositivos de entrada.

Parte 1: Implementación de la cola mqtt.

Necesitamos un ordenador donde implementar el servidor mqtt (llamado broker mqtt). Podemos usar servidores públicos que hay en Internet, como los de esta lista para hacer las pruebas, pero es mas correcto implantarlo en un equipo de nuestra confianza.

En Debian el protocolo mqtt se implementa con el paquete mosquitto. La versión que viene de serie con Jessie es un poco anticuada y no soporta que el cliente sea una página web y se comunique por Websockets. Es mas adecuado usar la última versión directamente de su repositorio:
# cat /etc/apt/sources.list.d/mosquitto-jessie.list
deb http://repo.mosquitto.org/debian jessie main
# apt-get update
E instalamos los paquetes:
# apt-get install libmosquitto1 mosquitto mosquitto-clients
# dpkg -l | grep mosqui
ii  libmosquitto1:amd64                   1.4.12-0mosquitto1                   amd64        MQTT version 3.1/3.1.1 client library
ii  mosquitto                             1.4.12-0mosquitto1                   amd64        MQTT version 3.1/3.1.1 compatible message broker
ii  mosquitto-clients                     1.4.12-0mosquitto1                   amd64        Mosquitto command line MQTT clients
Nótese como es la versión 1.4, no la 1.3.4 que viene con Jessie. Ahora ponemos está configuración de puertos y reiniciamos el servidor:
# cat /etc/mosquitto/mosquitto.conf

# Place your local configuration in /etc/mosquitto/conf.d/
#
# A full description of the configuration file is at
# /usr/share/doc/mosquitto/examples/mosquitto.conf.example

pid_file /var/run/mosquitto.pid

listener 1883
protocol mqtt

listener 8000
protocol websockets 

#port 8000

persistence true
persistence_location /var/lib/mosquitto/

log_dest file /var/log/mosquitto/mosquitto.log

include_dir /etc/mosquitto/conf.d
Ponemos a andar el sistema:
# /etc/init.d/mosquitto restart
Vamos a probarlo: nos suscribimos a una cola llamada "numero" indicando la IP del PC donde hemos instalado el Broker mqtt:
# mosquitto_sub -v -h 172.19.X.Y -t "numero"
El sistema queda esperando a que aparezca algo en la cola. Ahora, desde otro terminal u otro PC hacemos:
# mosquitto_pub -h 172.19.X.Y -t "numero" -m "A-02"
El código "A-02" se enviará a la cola "numero" del broker y todos los que estén suscritos recibirán dicho código de forma inmediata. Podremos verlo si nos vamos al terminal donde ejecutamos mosquitto_sub.

Parte 2: Visor de la cola.

Ahora debemos configurar un visor de la cola que se suscriba a ella, lea su contenido y lo muestre en una página web. Básicamente es un html con código javacript conecta con el broker mediante WebSockets (por eso instalamos la versión 1.4 de mosquitto) y muestra el último valor de la cola:
# cat /var/www/turnos/Suturno.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>Su Turno</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: 20%;
color: white;
font-style: normal;
text-align: center;
}

</style>
    <script src="mqttws31.js" type="text/javascript"></script>
    <script src="jquery.min.js" type="text/javascript"></script>
    <script src="config.js" type="text/javascript"></script>

    <script type="text/javascript">

    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;

        $('#cuerpo').html(payload);
    };


    $(document).ready(function() {
        MQTTconnect();
    });
    </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">SU TURNO</div>
</div>
</body>
</html>
Existe un fichero config.js con:
/var/www/turnos# cat config.js 
//host = 'broker.hivemq.com'; // hostname or IP address
//port = 8000;                    // Puerto de tipo Websocket
host = '172.19.X.Y';    // hostname or IP address
port = 8000;                    // Puerto de tipo Websocket

topic = 'numero';  // topic to subscribe to
useTLS = false;
username = null;
password = null;
cleansession = true;
// path = '';
El contenido completo de /var/www/turnos es:
# tree /var/www/turnos/
/var/www/turnos/
├── Antonio-Bold.ttf
├── Antonio-Light.ttf
├── Antonio-Regular.ttf
├── config.js
├── img
│   ├── logo_200_original.png
│   ├── logo_guadalupe.jpg
│   └── logo_guadalupe.png
├── jquery.min.js
├── mosquitto-repo.gpg.key
├── mqttws31.js
└── Suturno.html
Y lo descargaremos de este enlace (habrá que adaptar texto y logo). Este código html estará en cualquier máquina que haga de servidor web en la red del centro. Yo he usado el mismo servidor donde está el broker mqtt, pero podría ser cualquier otro.

En la pantalla que muestra el número simplemente habrá un navegador cargado (que soporte javascript, claro) con la siguiente página abierta:
http://servidor-web/turnos/Suturno.html
Yo tengo un Debian con autologin y con el Firefox abriéndose en la página al iniciar sesión.

Parte 3. Controlador de la cola.

Vamos a la parte final: el PC que controla la cola. Empezamos con 3 scripts que incrementarán, decrementarán y resetearan la cola a 0 (no olviden adaptar la IP).
# cat suma_turno.sh
#!/bin/bash

servidor="172.19.X.Y"
topic="numero"

# Obtenemos el ultimo valor guardado C 1 lee el valor y corta la subscripcion
info=$(mosquitto_sub -t $topic -h $servidor -C 1)

# Metemos en un array la letra y el numero
IFS='-' read -r -a array <<< "$info"
letra="${array[0]}"
num="${array[1]}"

if [ $num = "99" ]; then
nuevo_numero="00"
else # Incrementamos en 1 el valor usando base 10
nuevo_numero=$((10#$num + 1))
fi

# Formatea el numero con ceros delante.
n=`printf %02d $nuevo_numero`

# Publica de forma persistente -r la letra-numero
mosquitto_pub -t $topic -h $servidor -m "$letra-$n" -r

exit 0

# cat resta_turno.sh

#!/bin/bash

servidor="172.19.X.Y"
topic="numero"

# Obtenemos el ultimo valor guardado C 1 lee el valor y corta la subscripcion
info=$(mosquitto_sub -t $topic -h $servidor -C 1)

# Metemos en un array la letra y el numero
IFS='-' read -r -a array <<< "$info"
letra="${array[0]}"
num="${array[1]}"

if [ $num = "00" ]; then
nuevo_numero="99"
else # Incrementamos en 1 el valor usando base 10
nuevo_numero=$((10#$num - 1))
fi

# Formatea el numero con ceros delante.
n=`printf %02d $nuevo_numero`
# Publica de forma persistente -r la letra-numero
mosquitto_pub -t $topic -h $servidor -m "$letra-$n" -r

exit 0
# cat reset_turno.sh

#!/bin/bash

servidor="172.19.X.Y"
topic="numero"

mosquitto_pub -t $topic -h $servidor -m "A-00" -r

exit 0
Notemos que hemos llamado a mosquito_sub con "-C 1": esto se hace asi para leer un valor y desconectar, de lo contrario el script quedaría bloqueado eternamente suscrito a la cola.

Estos scripts podríamos ponerlos como accesos directos en el escritorio del PC de secretaría que controlará el sistema de turno. Pero tal como hacen los compañeros de http://openwrt.tuinstituto.es/ queda mucho más práctico y rápido usar un dispositivo dedicado solo para controlar al menos el incremento/decremento.

Esto se consigue con un ratón adicional y triggerhappy. Pinchamos un ratón adicional en el puerto usb y hacemos:
# ls -l /dev/input/by-id
total 0
lrwxrwxrwx 1 root root  9 jun  8 10:22 usb-0461_USB_Optical_Mouse-event-mouse -> ../event3
lrwxrwxrwx 1 root root  9 jun  8 10:22 usb-0461_USB_Optical_Mouse-mouse -> ../mouse0
lrwxrwxrwx 1 root root  9 jun  8 10:22 usb-CHICONY_HP_Basic_USB_Keyboard-event-kbd -> ../event4
lrwxrwxrwx 1 root root 10 jul 10 13:40 usb-PixArt_HP_USB_Optical_Mouse-event-mouse -> ../event16
lrwxrwxrwx 1 root root  9 jul 10 13:40 usb-PixArt_HP_USB_Optical_Mouse-mouse -> ../mouse1
lrwxrwxrwx 1 root root 10 jun  8 10:22 usb-Sonix_Technology_Co.__Ltd._USB_2.0_Camera-event-if00 -> ../event15
Este listado anterior nos muestra los dispositivos de entrada conectados al sistema. El ratón extra que hemos metido aparece en negrita. Debemos por tanto monitorizar /dev/input/event16 (o /dev/input/by-id/usb-PixArt_HP_USB_Optical_Mouse-event-mouse) y eso lo haremos con el paquete triggerhappy, tal como muestran aquí. Instalamos el paquete:
# apt-get install triggerhappy
Y probamos a mano si funciona:
# thd --dump /dev/input/event16
Moviendo el ratón y pulsando botones veremos como salta la ristra de eventos, lo cual nos sirve para identificar cada evento para luego asociar a él un "trigger". Ahora asociamos los eventos que nos interesan (pulsación de ambos botones)
# cat /etc/triggerhappy/triggers.d/raton.conf
BTN_LEFT    1  /usr/bin/suma_turno.sh
BTN_RIGHT   1  /usr/bin/resta_turno.sh
Esto funcionaría para cualquier ratón conectado al sistema, así que tenemos que filtrar para que solo se haga para el ratón adicional, modificando la configuración del demonio tal como se muestra aquí:
# cat /etc/default/triggerhappy
# Defaults for triggerhappy initscript
# sourced by /etc/init.d/triggerhappy
# installed at /etc/default/triggerhappy by the maintainer scripts

#
# This is a POSIX shell fragment
#

# Additional options that are passed to the Daemon.

#Solo captura eventos del raton HP
DAEMON_ARGS="--daemon --triggers /etc/triggerhappy/triggers.d/ --socket /var/run/thd.socket --pidfile $PIDFILE --user nobody /dev/input/by-id/usb-PixArt_HP_USB_Optical_Mouse-event-mouse"

DAEMON_OPTS=""

# The Triggerhappy daemon (thd) drops its root privileges after
# startup and becomes "nobody". If you want it to retain its root
# status (e.g. to run commands only accessible to the system user),
# uncomment the following line or specifiy the user option yourself:
#
# DAEMON_OPTS="--user root"
Con esto, reiniciando el demonio triggerhappy ya podemos comprobar que al pulsar los botones del ratón añadido se incrementa o decrementa el valor de la cola. Esto tiene un reflejo automático en la pantalla de visualización que está suscrita a ella.

Queda un pequeño detalle: el ratón adicional es fiel a su naturaleza y se comporta como un ratón normal, con lo que al moverlo o pulsarlo tendrá un efecto molesto (ya que se moverá el cursor) en el escritorio del PC al que está conectado. Lo ideal es que solo se dedique a controlar la cola y no tenga efecto sobre el cursor y el escritorio. ¿Podemos desvincularlo del escritorio? Si se puede con este script:
# cat /usr/bin/anula_raton_turnos.sh
#!/bin/bash

id=$(xinput list --id-only "PixArt HP USB Optical Mouse")

if [  ! -z "${id##*[!0-9]*}"  ]   #compruebo si es un número
then
  xinput set-int-prop $id "Device Enabled" 8 0  #Lo desactivo
fi
exit 0
Busca el ratón llamado "PixArt HP USB Optical Mouse" y lo desactiva como dispositivo de las X. Aquí habría que adaptar al nombre del ratón que tenga cada cual. Solo queda hacer que esto se ejecute al iniciar sesión y ese ratón dejará de molestar y quedará para su funcion primordial:
# cat # /etc/xdg/autostart/anula_raton_turnos.desktop
[Desktop Entry]
Encoding=UTF-8
Version=1.0
Type=Application
Exec=/usr/bin/anula_raton_turnos.sh
Icon=
Terminal=false
Name[es_ES]=Anula Raton Turnos
Name=controlIES
Comment[es_ES]=
Coment=
Categories=Application;Education;
Y con esto dejamos el verano fluir y ya si escribo algo será puntual que no quiera dejar pasar. A pasarlo bien...