Decodificar Aiken Biphase con Perl

Antes de nada quería mandar un saludo a Explorer de perlenespanol.com. Porque, sin conocernos previamente, se ve que le gustan mis artículos. Pues casi desde que empecé con el blog todo lo que escribo que tenga que ver con Perl acaba reseñado en su foro.

Ya hemos codificado y decodificado una señal digital otras veces para extraer información. Hemos decodificado señales NRZ, Manchester, etc. Hoy vamos a hablar de un tipo de señal llamado FM1, Biphase Mark Code (BMC) o también Aiken Biphase. Es un tipo de FSK ampliamente utilizado. Vamos a ver uno de los sitios donde se usa y lo usaremos como ejemplo para construir un programita que lo decodifique.

El soporte de datos que vamos a usar como ejemplo nos exigirá hacer uso de la característica de auto sincronía (self clocking) de la señal. Si seguís leyendo ya veréis el motivo.


Self Clocking

En la mayoría de circuitos digitales hay un mecanismo de coordinación de todo el esquema. Un reloj. No es más que oscilador de onda cuadrada a una frecuencia fija, pero hace que los componentes actúen como un todo. Y lo que es más importante, puedan interactuar con otros sistemas digitales.

Pero los osciladores no son tan exactos como nos gustaría. Los relojes se atrasan y se adelantan, los sistemas se descoordinan y la transmisión de información falla. Pero hay una forma fácil de evitarlo: hacer que en cada bit, ya sea uno o cero, la señal cambie de estado. Así el receptor reconoce fácilmente cuando empieza y cuando acaba cada bit, y si el transmisor tiene una deriva temporal puede adaptarse a ella.

Vamos a ver un ejemplo de esto que os digo. En una codificación Aiken Biphase la frecuencia del cero es la mitad que la del uno. En el gráfico siguiente hay un pico de señal en cada transición. Es decir, dos picos juntos es un uno, dos picos separados es un cero. Veremos esto más despacio pues por ahora lo que me interesa que veáis es cómo hacia el final se van juntando los impulsos.


Pinchad para ampliar la imagen. Fijaos que lo que hay al comienzo y al final son ceros. Y entre medias hay algunos unos. Pero los ceros del final están más juntos que los del principio. Es decir que la trnasmisión se ha ido acelerando en el tiempo. Y si el receptor no se adapta, e insiste en interpretar la señal del final con el mismo patrón que la del principio acabará detectando los ceros más juntos como unos.

Como ya lo sabemos, nuestro programa tiene que irse adaptando a la velocidad de la señal en tiempo real, tanto si se acelera como si desacelera, dentro de unos márgenes, porque si no lo más probable es que falle.

¿Pero por qué tanto insistir en este aspecto? Un oscilador puede tener cierta deriva temporal, pero no tanto. Pasará desapercibido a menos que sean velocidades muy altas. El problema viene cuando mezclamos lo digital con lo analógico.

La señal del gráfico anterior es la lectura de una banda magnética de una tarjeta de identificación. Obtenida al recorrer la banda con un cabezal lector de un casete (un fonocaptor magnético para ser pedantes). La aceleración viene por el hecho de que es muy difícil mover el brazo a una velocidad constante; aunque con práctica se consigue, sería de vagos no aprovechar el self clocking de la señal que precisamente está para eso.


Bandas magnéticas

Por si os ha sorprendido: sí, se puede. Con un cabezal de cinta se pueden leer y hasta grabar tarjetas de identificación o de crédito, billetes de aparcamiento y cualquier otro soporte que use banda magnética para almacenar datos. Hay mucha información en Internet y experimentos. Si os interesa el tema os recomiendo especialmente una página que de haberla descubierto antes me habría ahorrado un montón de trabajo: http://www.gae.ucm.es/~padilla/extrawork/stripe.html.

Casi todas las tarjetas se adhieren a un formato concreto. Por la sencilla razón de que es más fácil implementar una norma que ya está hecha y se usa, que diseñar un sistema desde cero. No obstante también los hay que usan sus propios formatos no estándar. Por lo general casi siempre se usa Aiken Biphase pero no es el único sistema.

Doy brevemente unas pinceladas sobre cómo se graba la información en una banda magnética para que entendáis de dónde viene la forma de la señal que vamos a tratar. El siguiente dibujo está sacado de http://www.gae.ucm.es/~padilla/extrawork/card-o-rama.txt. La cabeza grabadora/lectora es simplemente un anillo de un metal ferromagnético pero no cerrado, sino con un hueco diminuto en la pate que se expone a la cinta. Hay un cable que está enrollado al anillo, con muchas muchas espiras, y es con el que captaremos la señal, o la grabaremos.

                        | |  <----cables al amplificador      
                        | |       (se enrollan al anillo)
                      /-|-|-\
                     /       \
                     |       | <----solenoide (acaba de cambiar la polaridad)                           
                     \       /
                      \ N S / <---hueco en el anillo
N----------------------SS-N-------------------------S
                       ^^  
             <<<<<-la banda se mueve en esta dirección

Imaginaos la banda como millones de micro-imanes todos seguidos. Si no hay nada grabado todos los imanes están orientados en la misma dirección. N es el polo norte del imán y S el polo sur. Así:

N---------------------------------------------------S

Para grabar información digital lo que hacemos recorrer la banda con el solenoide e ir cambiando la polaridad cuando nos interese invertir el flujo.

N---------S S-------N N----------S S---------N N----S

Cuando recorremos la banda con la cabeza de lectura los imanes cierran el anillo y fijan una dirección del flujo magnético, que se establece de acuerdo al dominio magnético con el que esté en contacto el cabezal en ese momento.

            /-|-|-\
           /       \
           |       | <----solenoide 
           \       /
            \ N S / 
N--------------------S S--------------N N-----------S

Ahora avanzamos por la cinta y nos encontramos con un cambio en la polaridad magnética. Como el flujo que atraviesa el anillo lo fija la banda magnética, se invierte, y pasa de ser N-S a S-N.

                           /-|-|-\
                          /       \
                          |       | <----solenoide 
                          \       /
                           \ S N /  (ha cambiado la dirección)
N--------------------S S--------------N N-----------S

Y ya sabíamos que al cambiar el flujo magnético en un solenoide se induce una corriente eléctrica. En este caso va a ser muy débil, pero suficiente para detectarla. Cuando avanzamos más vuelve a cambiar el flujo y se induce una corriente en el otro sentido.

Ya os habéis dado cuenta de que sólo se induce cuando cambia el flujo, y ese cambio va a ser casi instantáneo. Así que esperamos ver picos puntuales. Pero ya sabéis que en la naturaleza la línea recta no existe, y en la electrónica menos aún. Luego lo que vamos a ver es esto:


Ya veis la que la regla del Aiken Biphase es sencilla, os pego esta imagen de la Wikipedia.




Preamplificador de entrada

Hemos dicho que no se necesita electrónica ninguna para conectar el lector a la tarjeta de sonido. Salvo un condensador, porque si recordáis como es la entrada de una tarjeta de sonido sabréis que si lo conectáis tal cual hay una corriente continua atravesando el lector. Con tan mala suerte que es bastante para borrar el contenido de la banda magnética. Así que no sólo no leeréis nada sino que borraréis lo que hubiera escrito.

Aunque no es necesario un amplificador, yo sí voy a utilizarlo porque facilita captar correctamente la señal. Este es el circuito, que como veis no es más que un amplificador inversor que habíamos explicado ya. Este tiene una ganancia de 7.5 veces aproximadamente. Los componentes no son críticos, sirve casi cualquier operacional y las resistencias que tengáis por el taller.


El condensador de entrada merece mención aparte. Si usamos capacidades muy bajas, por debajo de 47nF vamos a hacer que las frecuencias bajas estén muy atenuadas. Y el valle que hay entre los picos se deformará apareciendo un pico secundario a modo de rebote. Y el lector se va a confundir con esos picos. En cambio si usamos una capacidad alta por encima de 10uF la forma de la onda no se va a deformar apenas, y los picos aparecerán claros y contundentes. Pero también se van a colar ruidos de baja frecuencia que hacen que el tren de pulsos suba y baje como en una montaña rusa. Aunque el algoritmo adaptativo que os presento tolera esas variaciones, si podemos es mejor que nos las quitemos. Yo he hecho las pruebas con una capacidad de 470nF y funciona aceptablemente.


Tratamiento digital previo

Conectando la cabeza lectora a la entrada de micro de la tarjeta ya llega con suficiente señal para detectarla bien. Aunque para las pruebas he utilizado un pequeño amplificador operacional a la entrada.

Os cuento ahora algunos pasos que damos en la detección para que entendáis mejor el programa que sigue. Lo primero que vamos a hacer es tomar el valor absoluto de la señal recibida. Porque sólo nos interesa saber cuando hay un pico. Si es positivo o negativo no nos importa. Lo segundo es pensar si filtramos la señal. Como siempre que nos interesa detectar picos, una solución socorrida es elevar la señal a alguna potencia para aumentar la relación señal-ruido. Esta es la señal en valor absoluto:


La imagen que sigue es al cuadrado. Vemos que el ruido ha disminuido y los picos están más claros. Observad el cambio de escala porque estamos elevando valores menores que uno.


Pero hay que tener cuidado con esta técnica. Primero porque en un ordenador es muy fácil elevar a una potencia, pero si usamos un integrado tipo PIC o DSP es bastante más chungo. Y segundo porque tiene sus desventajas. Esta es la misma señal a la cuarta potencia:


Mirad como se amplifica la diferencia entre los picos. Esta técnica viene muy bien si lo que hacemos es fijar un umbral y todo lo que hay por encima decimos que es un pico.

Una de las decisiones más difíciles es dónde colocar el umbral del ruido. Cuando tenemos el archivo completo delante de nuestras narices lo vemos clarísimo, pero de alguna manera tiene que saber el programa que empieza una lectura y no es ruido.

En el programa de debajo vamos a usar un método adaptativo que tiene en cuenta dos cosas. Por un lado hay un umbral de ruido que tiene un cierto valor que nosotros le damos. Más o menos en función de lo ruidoso que sea el ambiente y de la ganancia del preamplificador. Y por el otro lado hay una variable que llega hasta 0.6 veces el valor del pico anterior. Fijamos el umbral en el más alto de esos valores. Así cuando hay una señal fuerte el umbral se sube automáticamente y evitamos captar nada que no sean picos.

Nuestra mala suerte es que ese algoritmo de ajuste no funciona bien si hacemos lo de elevar al cuadrado. Así que no alteraremos la señal de entrada.


El algoritmo

Ya hemos nombrado antes dos parámetros que varían: uno es la distancia entre picos (o la frecuencia de reloj, o la velocidad, o como queráis llamarlo); y otro la amplitud de la señal. Francamente nos interesa mucho más el primero porque el segundo es cuestión de fijar un umbral a mano y ya está. El tiempo como tal no nos interesa, lo que vamos a mirar es la distancia en muestras. Si la frecuencia de de muestreo es de 44100Hz quiere decir que cada 44100 muestras habrá transcurrido un segundo, pero eso también nos da igual.

Para controlar el tiempo no podemos (o no debemos) fiarnos de la última medida, porque pudiéramos estar en un error y alterar los bits siguientes. Así que lo que hacemos es usar una media móvil exponencial de los últimos bits leídos. Es una especie de filtro IIR paso bajos para que una mala lectura no cause estragos. En cuanto leemos un bit y determinamos si es un CERO o un UNO realimentamos la variable. Como las aceleraciones o deceleraciones no son bruscas sino que son graduales, a la media móvil le da tiempo de adaptarse bit a bit. Aunque los bits finales estén mucho más juntos o mucho más separados que los del principio, como el cambio se ha hecho poco a poco el tiempo entre UNOS (que es como llamamos a la variable) se ha ido adaptando progresivamente.

Ya hemos visto antes cómo decodificarlo. Pero yo lo voy a hacer de otra manera aprovechando que tengo un ordenador con un lenguaje de alto nivel y no un integrado. Llamadme vago. Voy a considerar que es un UNO cuando el impulso siguiente venga separado una distancia (en muestras) parecida a Tuno y que es un cero cuando venga a una distancia parecida al doble de Tuno. La desventaja es que los UNOS me salen duplicados, porque un UNO son dos picos juntos, que yo detectaré como dos unos separados. Pero no os preocupéis que lo arreglamos después. Lo he hecho así porque viene muy bien para leer bandas con formato desconocido, que pueden no ser Aiken Biphase.

Hemos hablado antes de una duración parecida a Tuno o al doble de Tuno ¿Pero cuánto es parecida? Bueno pues vamos a tomar un criterio sencillo. Tomamos tres valores:
el valor es        Tuno
su mitad es    1/2*Tuno
y su doble es    2*Tuno

Al principio Tuno puede ser la duración del CERO o del UNO (hablamos de esto en el párrafo siguiente). Si recibimos una duración entre pulsos equivalente a Tuno diremos que es el mismo carácter que tiene Tuno. Si sabemos que eran UNOS pues diremos que llega un UNO. Si llega una duración equivalente al doble, diremos que hemos recibido un CERO. Pero si llega una duración equivalente a la mitad de Tuno pueden pasar dos cosas: durante la inicialización servirá para discriminar que Tuno era en realidad el tiempo del CERO y no del UNO. Pero pasada la etapa de inicialización se tratará de un error. Lo mismo que si la medida supera el doble. Así dado un tiempo t tenemos 5 intervalos:
error si            t < 1/4*Tuno
mitad si 1/4*Tuno < t <= 3/4*Tuno
igual si 3/4*Tuno < t <= 3/2*Tuno
doble si 3/2*Tuno < t <= 5/2*Tuno
error si 5/2*Tuno < t


Pero ¡un momento! Habrá que inicializar Tuno de alguna manera. Generalmente al principio de la lectura se repite mucho uno de los dos bits, que suele ser CERO para que el receptor se entere de la velocidad de transmisión. Vamos a intentar dar una vuelca de tuerca y a hacer que nuestro decodificador sea inteligente y sepa cuándo los caracteres iniciales sean CEROS y cuando UNOS. Pero eso no lo puede saber hasta que no encuentre un bit diferente. Si este dura la mitad es que lo de antes eran CEROS. Pero si dura el doble es que lo de antes eran UNOS. Por eso al empezar a leer estamos leyendo caracteres "T", que no sabemos si son UNOS o CEROS hasta leer otro diferente para poder comparar.

Cuando ya tenemos una cadena de unos y ceros se la pasamos a las rutinas que decodifican los formatos conocidos. No voy a entrar en cómo calcular la paridad ni el bit de LRC. Si os interesa hay mucha información. Lo que me gustaría es que os fijarais en que se usa paridad IMPAR. Y es por una razón muy sencilla si lo pensáis. Con paridad PAR un byte que sea 0, o sea todo CEROS, su bit de paridad también es CERO. Con lo que si tenemos muchos bytes 0 seguidos nos encontramos con una cadena de bit 0 todos iguales. En cambio usando paridad IMPAR el bit de paridad es 1, así que se obliga a que por lo menos uno de los bits del grupo sea distinto. Y así se favorece la sincronía. Un ejemplo con grupos de 5 bits + 1 de paridad.


Paridad par (siempre el mismo bit):
000000 000000 000000 000000 000000

Paridad impar (se obliga a un bit distinto):
000001 000001 000001 000001 000001


El programa

Pego el programa, parece largo pero quiero dejarlo íntegro porque está muy comentado. Os doy unas pinceladas breves y si decidís que os interesa ampliáis información leyendo en los comentarios en el código.

Así por encima, distinguimos cuatro partes:
  • Inicio, hasta la linea 67. Donde abrimos el dispositivo del que leeremos las muestras, ya sea un archivo de disco o la tarjeta de sonido.
  • Bucle principal. Hay una rutina nada más empezar el bucle que se para hasta detectar un pico. Es decir que el bucle sólo reacciona a los picos de señal. Fijaos cómo los primeros picos sólo se usan para inicializar variables y a medida que llegan más picos vamos avanzando y llegando más adentro en el bucle. Hasta que del tercer pico en adelante ya pasamos a la parte donde se discrimina la duración para ver si es un UNO o un CERO.
  • Rutinas de la señal. Aproximadamente entre las líneas 199 y 324. Tratan diversos aspectos de la señal que aún es sonido.
  • Rutinas de decodificación. Cuando la señal ya no es sonido sino una hilera de bits, entonces pasamos a decodificarla.

Tenemos una variable que activa o desactiva el modo depuración. Cuando el script está en modo depuración escribe por pantalla abundante información sobre cuándo ha detectado un pico, de qué duración, en qué muestra, etc.

Como ejercicio si te interesa, intenta buscar la respuesta a estas preguntas (casi todo está en los comentarios):
  • ¿Cual es el criterio de intervalos para Tuno y por qué se ha hecho así?
  • ¿Donde se hace la realimentación de las medias móviles y por qué sólo ahí?
  • ¿Donde se aplica la histéresis al Umbral de ruido?

#!/usr/bin/perl 
#===============================================================================
#
#         FILE:  decodifica.pl
#
#        USAGE:  ./decodifica.pl [fichero.wav]
#                Si no se da fichero.wav se lee del dispositivo de grabación hw:0,0.
#
#  DESCRIPTION:  Recibe un archivo de sonido e intenta decodificar la información que 
#                contiene la banda magnética.
#
#                Esta utiliza los picos y medias móviles para adaptarse a la velocidad
#                de lectura variable.
#
#      OPTIONS:  ---
# REQUIREMENTS:  sox
#         BUGS:  ---
#        NOTES:  ---
#       AUTHOR:  Reinoso Guzman
#      VERSION:  1.0
#      CREATED:  14/11/10 12:16:09
#===============================================================================

use strict;
use warnings;
use List::Util qw(max);

my $alphaVpico = 0.33;    # Para la media móvil del nivel de pico.
                          # Si la sigue mucho se va hasta el nivel de ruido
           # y no corta la lectura.        
my $alphaTuno  = 0.33;    # Para la media móvil del intervalo del UNO.
my $umbralInicial = 0.4;

my $debug = 0;


my $file = $ARGV[0];
if ($file and ! -e $file) {
 die "El fichero $file no existe o no se puede leer.\n";
}

my $data;
if ($file) {
 open $data, "sox $file -t dat - |" or die "Error: $!\n";
}
else {
    $ENV{AUDIODEV} = "hw:0,0";
 open $data, "rec -q -r 48000 -t alsa hw:0,0  -t dat - |" or die "Error: $!\n";
}


my $muestra;     # No usamos el tiempo sino cuantas muestras,
                 # así no depende de la velocidad de lectura.
                 # Esta variable cuenta por qué muestra vamos.

my $nbits;       # Para contar cuantos picos van.
my $Tuno;        # Tiempo del 1, el del 0 será el doble (media movil).
my $Vpico;       # Valor de pico (media movil).

my $last_pico;   # Para calcular el tiempo entre transiciones
my $string;      # Cadena leída, por ahora vacía.
my $trailChar;   # Caracter inicial a priori no sabemos si es 0 o 1
my $umbral;      # Lo inicializamos más adelante
my $leyendo;     # Indica si estamos en mitad de una lectura

inicializar();

while (1) { # sale cuando get_sample se quede sin datos
 my $intervalo; # duración entre el pico anterior y el proximo que encontremos.

 espera_senal() or next;
 my ($pos_pico, $valPico) = procesa_pico($data);

 # Estamos en el primer pico, aún no tenemos la mitad de las variables
 # definidas. Definimos los valores iniciales y no hacemos nada más.
 if (not defined $last_pico) {
  $last_pico = $pos_pico;
  $Vpico     = $valPico;
  print "\n------------- Comienza nueva lectura en la muestra $muestra.\n";
  $leyendo = 1;

  # Bajamos el umbral en cuanto llega el primer impulso para
  # intentar captar los demás.
  $umbral = 0.70*$umbralInicial;
  # No seguimos.
  next;
 }

 # Está definido last_pico, luego ya ha habido un pico anterior y
 # este puede ser del segundo en adelante. Ya podemos hablar de intervalo
 # entre dos picos.
 $intervalo = $pos_pico - $last_pico;
 print "Pico en $pos_pico.   Duración: $intervalo.   Valor: $valPico.\n" if $debug;
 
 # Si el pico dura muy poco (menos que 3 muestras) es sospechoso de ser debido
 # al ruido. Si además el valor es muy próximo al umbral lo ignoramos.
 # Consideramos corta duración si es menor que 1/4 de Tuno, pero puede no estar
 # definido Tuno. En ese caso que dure menos de 1/4*24 = 6 muestras.
 if ($intervalo < 1/4*($Tuno||24) and $valPico <= 1.1 * $umbral) {
  print "Pico ingnorado\n" if $debug;
  next;
 }

 # Si hemos llegado aquí se trata de un pico válido. Actualizamos. 
 $last_pico = $pos_pico;

 # Si aún no está definido $Tuno Estamos en el segundo pico: 
 # lo inicializamos al primer intervalo que pillemos. Y luego veremos si eran
 # cero o eran unos.
 if (not defined $Tuno) {
  $Tuno = $intervalo;
  $string .= $trailChar; # no sabemos cual es el caracter inicial.
  next;
 }


 # Este es a partir del tercer pico, ya tenemos una referencia con la que comparar.
 # Juzgamos si es de la misma duración que el anterior del doble o de la mitad.

 # La duración t puede ser:
 #   1/2T   si  1/4T < t <= 3/4T
 #      T   si  3/4T < t <= 3/2T
 #     2T   si  3/2T < t <= 5/2T
 #   indefinida en otros casos.
 
 # Es de la misma duración que Tuno
 if ($intervalo > 3/4*$Tuno and $intervalo <= 3/2*$Tuno) {
  # Si aún no hemos recibido nada diferente para comparar no sabemos
  # si Tuno es la duración del UNO o del CERO.
  if ($trailChar eq "T") {
   $string .= $trailChar;
  }
  # Si ya sabemos que Tuno es del uno pues lo ponemos
  else {
   $string .= "1";
  }
  
  $Tuno  = $alphaTuno  * $intervalo + (1-$alphaTuno)  * $Tuno;
  $Vpico = $alphaVpico * $valPico   + (1-$alphaVpico) * $Vpico;
 }

 # Recibimos un símbolo de duración el doble: un CERO
 elsif ($intervalo > 3/2*$Tuno and $intervalo <= 5/2*$Tuno) {
  # Confirmamos, si no lo sabíamos, que Tuno es el tiempo del UNO
  # porque acabamos de recibir un CERO.
  if ($trailChar eq "T") {
   $trailChar = "1";
   $string =~ s/T/$trailChar/g;
  }
  # Y añadimos el CERO recién recibido a la vez que realimentamos
  # la media móvil que lleva el tiempo del UNO.
  $string .= "0";
  $Tuno  = $alphaTuno  * $intervalo/2 + (1-$alphaTuno)  * $Tuno;
  $Vpico = $alphaVpico * $valPico     + (1-$alphaVpico) * $Vpico;
 }

 # Recibimos un símbolo de duración la mitad: un UNO
 elsif ($intervalo > 1/4*$Tuno and $intervalo <= 3/4*$Tuno) {
  # Si no sabíamos cual era el caracter inicial eso quiere decir que son 
  # CEROS y no UNOS. Porque acabamos de recibir el primer UNO.
  # Así que rectificamos la cadena y la duración del UNO.
  if ($trailChar eq "T") {
   $trailChar = 0;
   $string =~ s/T/$trailChar/g;
   $Tuno = $Tuno / 2; # Rectificamos la duración
  
   $string .= "1";
   $Tuno  = $alphaTuno  * $intervalo + (1-$alphaTuno)  * $Tuno;
   $Vpico = $alphaVpico * $valPico   + (1-$alphaVpico) * $Vpico;
  }

  # Pero si ya habíamos determinado la duración del UNO y nos llega un
  # pulso que dura la mitad, es que hay algo que está mal. 
  # Se tratará de un error.
  else {
   $string .= "M";
  }
 }

 # Dura más o menos de lo esperado, se trata de un error, hemos perdido algo.
 # Evitamos alimentar $Tuno con una medida errónea y con suerte el resto de
 # bits los recibiremos correctamente.
 elsif ($intervalo < 1/4*$Tuno) {
  $string .= "_";
 }

 elsif ($intervalo > 5/2*$Tuno) {
  $string .= "^";
 }

 print "Tuno = $Tuno      Vpico = $Vpico      Umbral = ". max($umbral, 0.7*($Vpico||0))."\n" if $debug;
}


print "\n";



# Sale en cuanto haya una señal.
# Mientras Vpico está si definir sólo cuenta el umbral
# Si pasa mucho tiempo con ruido decimos que es otra lectura diferente
sub espera_senal {
 my $muestras_ruido = 0;
 my $valor;
 while (defined ($valor = get_sample($data)) and $valor < max($umbral, 0.6*($Vpico||0))) {
  $muestras_ruido++;
  # El tiempo del cero tiene que ser como mucho el doble que el del uno
  # pero si pasa el triple asumimos que se ha terminado la lectura.
  if ($leyendo and defined $Tuno and $muestras_ruido > 3*$Tuno) {
   print "Ruido durante $muestras_ruido > 3*$Tuno\n" if $debug;
   fin_lectura(); # des-define Tuno y saldría del bucle
   return undef;
  }
  elsif ($leyendo and not defined $Tuno and $muestras_ruido > 100*44100/1000) {
   print "Ruido durante $muestras_ruido\n" if $debug;   
   fin_lectura();
   return undef;
  }
 }
 return $valor;
}


# Procesa lo que es un pico, devuelve la posición y el valor máximo
sub procesa_pico {
 my $maximo   = $umbral;  # Valor de pico
 my $posicion = $muestra; # Posición del pico
 my $valor    = 0; # Valor de la muestra actual
 print "Entra pico: $muestra " if $debug;
 while (($valor = get_sample($data)) > max($umbral, 0.6*($Vpico||0))) {
  if ($valor >= $maximo) {
   $posicion = $muestra;
   $maximo   = $valor;
  }
 }
 print "Sale pico: $muestra.    Valor: $maximo en $posicion\n" if $debug;
 return ($posicion, $maximo);
}


# Inicializa las variables para una nueva lectura
sub inicializar {
 print "Variables reseteadas.\n" if $debug;
 $last_pico = undef;
 $Tuno      = undef;
 $string    = "";
 $Vpico     = undef;
 $trailChar = "T";
 $leyendo   = 0;

 # El umbral tiene histéresis:
 #   Cuando no estamos leyendo es un 10% superior al fijado.
 #   Pero en cuanto se detecte la primera señal bajamos al 75%
 #   por si luego llegan señales débiles.
 $umbral    = $umbralInicial * 1.1;

 printf "Umbral de señal fijado en %3.4f\n", $umbral if $debug;
}

# Obtiene una muestra a partir del descriptor abierto
# Lee la línea y extrae la información arpopiada.
# Incrementa la posición contando una muestra más.
sub get_sample {
 my $data = shift;

 my $linea;
 while ($linea = <$data>) {
  next unless $linea;

  my (undef, $time, $valor) = split /\s+/, $linea;
  next unless $time =~ /\d+/; # Saltar las lineas no numéricas.
  
  # Preprocesamos la señal para hacer más evidentes lo picos.
  $valor = abs($valor);
  #$valor = 0.90*$valor + (1-0.90)*$valor_old;
  #$valor = $valor*$valor*$valor;

  $muestra++;
  return $valor if defined $valor;
 }

 
 print "Fin de los datos en la muestra $muestra\n";
 fin_lectura();
 exit; # Se acabaron los datos.
}


sub fin_lectura {
 # No hace nada si no estábamos leyendo.
 return undef unless $leyendo;

 # Si estábamos leyendo termina.
 print "Terminamos en la muestra $muestra\n";
 if ($string and length $string > 10) {
  #print "String: $string\n";
  print_data_stream($string);
  print "-------------------------------------------------------\n\n";
 }
 else {
  print "Lectura vacía.\n" if $debug;
 }

 inicializar();
}



sub print_data_stream {
 my $string = shift;

 $string =~ s/11/1/g; # apaño porque los 1 salen en parejas
 my $bits = length($string);
 print "Crudo: $string\n";
 print "Bits: $bits\n\n" if $bits;


 # Intentamos decodificarla con todo lo que podría ser.
 # Y comprobamos los bit de paridad, lrc, etc
 my ($decoded, $chars, $perrors, $LRCerror);
 
 ($decoded, $chars, $perrors, $LRCerror) = decode_ALPHA($string);
 if ($decoded) {
  printf "Formato:            ALPHA\n";
  printf "Caracteres:         $chars\n";
  printf "Errores de paridad: $perrors\n";
  printf "Comprobación LRC:   %s\n", $LRCerror ? "No válido." : "¡Correcto!";
  printf "Contenido:          %s\n", $decoded;

#  if ($chars and $chars > 10 and $perrors < 2) {
#   print "Anotada.\n";
#   open my $fh, ">> log";
#   print $fh "$string : $decoded\n";
#   close $fh;
#  }
 
  return;
 }

 ($decoded, $chars, $perrors, $LRCerror) = decode_BCD($string);
 if ($decoded) {
  printf "Formato:            BCD\n";
  printf "Caracteres:         $chars\n";
  printf "Errores de paridad: $perrors\n";
  printf "Comprobación LRC:   %s\n", $LRCerror ? "No válido." : "¡Correcto!";
  printf "Contenido:          %s\n", $decoded;

#  if ($chars and $chars > 10 and $perrors < 2) {
#   print "Anotada.\n";
#   open my $fh, ">> log";
#   print $fh "$string : $decoded\n";
#   close $fh;
#  }
  return;
 }
}


# Sustituye los grupos por su correspondiencia
sub decode_BCD {
 # http://www.gae.ucm.es/~padilla/extrawork/card-o-rama.txt
 my %BCD_table = (
  '00001' => '0',
  '10000' => '1',
  '01000' => '2',
  '11001' => '3',
  '00100' => '4',
  '10101' => '5',
  '01101' => '6',
  '11100' => '7',
  '00010' => '8',
  '10011' => '9',
  '01011' => ':',
  '11010' => ';', # Start Sentinel
  '00111' => '<',
  '10110' => '=', # Field Separator
  '01110' => '>',
  '11111' => '?', # End Sentinel
 );
 my $string  = shift;
 my $decoded = "";
 my $errores = 0;
 my $chars   = 0;
 my @LRC;
 
 ($string) = $string =~ /(11010([01\?]{5})+11111[01\?]{5})/;
 if (not $string) {
  print "DecodeBCD: Error de formato.\n" if $debug;
  return (undef);
 }

 for (my $i = 0; $i < length($string); $i+= 5) {
  my $grupo = substr ($string, $i, 5);
  if (exists $BCD_table{$grupo}) {
   $decoded .= $BCD_table{$grupo};
   $LRC[$_] = ($LRC[$_]||0) ^ substr($grupo, $_, 1) for (0..3);
  }
  else {
   $decoded .= "_";
   $errores++;
  }
  $chars ++;
 }

 return ($decoded, $chars, $errores, any(@LRC));
}

# Sustituye los grupos por su correspondiencia
sub decode_ALPHA {
 # http://www.gae.ucm.es/~padilla/extrawork/card-o-rama.txt
 my %ALPHA_table = (
  "0000001" => ' ', # (0H)   Special
  "1000000" => '!', # (1H)      "
  "0100000" => '"', # (2H)      "
  "1100001" => '#', # (3H)      "
  "0010000" => '$', # (4H)      "
  "1010001" => '%', # (5H)   Start Sentinel
  "0110001" => '&', # (6H)   Special
  "1110000" => "'", # (7H)      "
  "0001000" => '(', # (8H)      "
  "1001001" => ')', # (9H)      "
  "0101001" => '*', # (AH)      "
  "1101000" => '+', # (BH)      "
  "0011001" => ',', # (CH)      "
  "1011000" => '-', # (DH)      "
  "0111000" => '.', # (EH)      "
  "1111001" => '/', # (FH)      "

  "0000100" => '0', # (10H)    Data (numeric)
  "1000101" => '1', # (11H)     "
  "0100101" => '2', # (12H)     "
  "1100100" => '3', # (13H)     "
  "0010101" => '4', # (14H)     "
  "1010100" => '5', # (15H)     "
  "0110100" => '6', # (16H)     "
  "1110101" => '7', # (17H)     "
  "0001101" => '8', # (18H)     "
  "1001100" => '9', # (19H)     "

  "0101100" => ':', # (1AH)   Special
  "1101101" => ';', # (1BH)      "
  "0011100" => '<', # (1CH)      "
  "1011101" => '=', # (1DH)      "
  "0111101" => '>', # (1EH)      "
  "1111100" => '?', # (1FH)   End Sentinel
  "0000010" => '@', # (20H)   Special

  "1000011" => 'A', # (21H)   Data (alpha) 
  "0100011" => 'B', # (22H)     "
  "1100010" => 'C', # (23H)     "
  "0010011" => 'D', # (24H)     "
  "1010010" => 'E', # (25H)     "
  "0110010" => 'F', # (26H)     "
  "1110011" => 'G', # (27H)     "
  "0001011" => 'H', # (28H)     "
  "1001010" => 'I', # (29H)     "
  "0101010" => 'J', # (2AH)     "
  "1101011" => 'K', # (2BH)     "
  "0011010" => 'L', # (2CH)     "
  "1011011" => 'M', # (2DH)     "
  "0111011" => 'N', # (2EH)     "
  "1111010" => 'O', # (2FH)     "
  "0000111" => 'P', # (30H)     "
  "1000110" => 'Q', # (31H)     "
  "0100110" => 'R', # (32H)     "
  "1100111" => 'S', # (33H)     "
  "0010110" => 'T', # (34H)     "
  "1010111" => 'U', # (35H)     "
  "0110111" => 'V', # (36H)     "
  "1110110" => 'W', # (37H)     "
  "0001110" => 'X', # (38H)     "
  "1001111" => 'Y', # (39H)     "
  "0101111" => 'Z', # (3AH)     "

  "1101110" => '[', # (3BH)    Special
  "0011111" => '\\', # (3DH)   Special
  "1011110" => ']', # (3EH)    Special
  "0111110" => '^', # (3FH)    Field Separator
  "1111111" => '_', # (40H)    Special
 );
 my $string  = shift;
 my $decoded = "";
 my $errores = 0;
 my $chars   = 0;
 my @LRC;
 
 ($string) = $string =~ /(1010001([01\?]{7})+1111100[01\?]{7})/;
 if (not $string) {
  print "Decode ALPHA: Error de formato.\n" if $debug;
  return;
 }

 for (my $i = 0; $i < length($string); $i+= 7) {
  my $grupo = substr ($string, $i, 7);
  if (exists $ALPHA_table{$grupo}) {
   $decoded .= $ALPHA_table{$grupo};
   $LRC[$_] = ($LRC[$_]||0) ^ substr($grupo, $_, 1) for (0..5); # paridad no entra en LRC
  }
  else {
   $decoded .= "_";
   $errores++;
  }
  $chars++;
 }

 return ($decoded, $chars, $errores, any(@LRC));
}


# One argument is true
sub any { $_ && return 1 for @_; 0 }


Conclusiones

Y para terminar os acompaño una captura para que veais cómo una secuencia de dominios magnéticos apropiadamente colocados se traduce en unos y ceros, y esa información binaria se decodifica en información útil. La salida no es del programa de arriba sino de una versión anterior.


A veces cuesta un poco leer las pistas de alta densidad porque el lector que usamos es más estrecho para el ancho de la banda. Pero no quiere decir que no vayamos a poder leer, sino que cuesta más obtener una señal válida.

Ni que decir tiene que si has leído hasta aquí es porque estás interesado en la parte didáctica del artículo. Porque, si fueras un delincuente no estarías perdiendo el tiempo leyendo sobre electrónica digital y lectores de cintas de cassete sino que estarías clonando ya tarjetas con un grabador comercial comprado por Internet.
Artículo completo >>

Contraseña dinámica para acceder al PC de casa

En ocasiones necesitamos acceder a nuestro ordenador desde fuera de casa. No hay problema, instalamos un servidor SSH y desde cualquier ordenador con Linux, o con PuTTY o SecureCRT instalado nos podemos conectar y ejecutar comandos o ver el correo como si estuviéramos delante mismo de la consola en casita.

El problema viene cuando nos conectamos desde ordenadores no seguros. Qué se yo, un cyber-café, un puesto de acceso libre en alguna party, o el ordenador de un amigo o no tan amigo. Estos sitos no seguros pueden tener instalados algún tipo de troyano o programa semejante para capturar las contraseñas que la gente mete. Puesto que la contraseña de acceso siempre es la misma (salvo que la cambiemos) con que alguien nos la robe en un descuido ya puede andar por nuestro PC de casa sin problemas. Y si tenemos alguna de estas distribuciones con el sudo abierto pues puede organizar un desaguisado de mucho cuidado.

La solución pasa entonces por tener una clave que cambie cada día, o mejor aún que cada vez que accedamos sea una distinta. Así al intruso no le bastará con sólo capturar la clave una vez, porque no servirá para la próxima vez que intente entrar.

Pero la pregunta es, si cada vez que entre la clave es distinta ¿cómo sé yo con qué clave tengo que entrar? Pues una solución sencilla podría ser que el ordenador nos mande un desafío y nosotros tengamos que responder siguiendo un algoritmo que hayamos programado previamente. Esa es la dinámica del programa que os quiero presentar hoy.



El algoritmo

En este caso la clave es el algoritmo. Al espía no se bastará capturar una clave o veinte, necesitará descubrir con qué algoritmo respondemos. Porque si intenta entrar y su respuesta es errónea, el programa nos alertará.

Hay algoritmos que aunque el intruso sepa el algoritmo sería incapaz de generar una clave válida a partir de una clave usada. Es el caso del intercambio de claves Diffie-Hellman basado en el problema del logaritmo discreto. Pero como no es cuestión de teclear varias decenas de cifras cada vez que nos conectemos, vamos a usar algoritmos un poco menos seguros.

Como cualquier móvil hoy en día tiene una calculadora básica tenemos muchas operaciones para elegir. Por ejemplo podríamos elegir las cuatro últimas cifras del cuadrado del número. Si el ordenador nos pasa el número 3465 nosotros responderíamos con 6225. Hay infinitos algoritmos para elegir. Algunos os habréis dado cuenta de que precisamente este no es muy inteligente.

Con las modernas calculadoras para móviles no es difícil hacer cualquier operación, a mi me gusta mucho esta: http://midp-calc.sourceforge.net/Calc.html. Además como es programable sólo tengo que pasarle los números y me devuelve el resultado.

Conviene escoger funciones que no sean lineales, o por lo menos no lo parezcan. Por ejemplo el módulo (he dicho no lo parezcan) va oscilando entre 0 y un valor máximo, igual que el seno o el coseno. Esta propiedad es estupenda para despistar.

El programa

Veamos el programa que permite esto. Se trata de un pequeño script en Perl que se coloca en lugar de la shell de nuestro usuario. Así cuando alguien meta la contraseña correcta se le presenta el desafío.

Desde el momento que se presenta el desafío el que entra tiene dos opciones:
  • Responder correctamente, con lo que se le dejará entrar.
  • Cualquier otra acción, ya sea cortar la conexión o responder equivocadamente disparará la rutina de error.

#!/usr/bin/perl 
#===============================================================================
#
#         FILE:  escudo.pl
#
#        USAGE:  ./escudo.pl  
#
#  DESCRIPTION:  Se pone como shell de usuario para proporcionar un nivel de
#                seguridad extra. Puede ser una contraseña dinámica que cambie
#                cada vez a modo de token. O que cambie según día.
#
#                Cuando un usuario se logee con nuetra cuenta y no sepa qué algoritmo
#                hemos puesto en el escudo fallará. Y nos llegará un correo avisando.
#                De esa forma sabremos que nuestra contraseña ha sido comprometida.
#
#                Como acciones posteriores puede cambiar la contraseña y enviar la nueva
#                a un correo seguro. O bloquear la IP origen.
#
#        NOTES:  Hay que modificar las funciones &parametros y &calcular para crear
#                el algoritmo que nosotros diseñemos.
#
#       AUTHOR:  Reinoso Guzmán
#      VERSION:  1.0
#      CREATED:  13/11/10 12:48:18
#     REVISION:  ---
#===============================================================================

use strict;
use warnings;
use Sys::Syslog;
use Net::SMTP;

openlog('escudo', 'cons,pid', 'user');

$| = 1;
# Si el usuario hace cualquier otra cosa que no sea meter la clave correcta,
# damos la alarma.
$SIG{TERM} = \&tomar_medidas;
$SIG{HUP}  = \&tomar_medidas;
$SIG{INT}  = \&tomar_medidas;
$SIG{CHLD} = \&tomar_medidas;


# Variable global con el nombre del usuario
my $usuario = $ENV{LOGNAME} || $ENV{USER} || "Perfecto desconocido";
$usuario = ucfirst($usuario);

# Actuamos si es shell remota por SSH.
# Los comandos no interactivos fallarán, pero es de lo que se trata.
if (not exists $ENV{SSH_CLIENT}) {
 do_shell();
}


# Le hacemos una pregunta al usuario con los parámetros
my @params = parametros();
print "\nHola $usuario.\nSi yo te digo @params, ¿tú que me contestas?\n";

# Esperar respuesta
my $respuesta = <>;
chomp $respuesta;

# Comprobamos la respuesta
if (calcular(@params) eq $respuesta) {
 do_shell();
}


# Tomar medidas en caso de que algo no funcione bien.
# La shell se lanza con exec, que no retorna. 
# Luego si de cualquier forma llegamos a esta función (ya sea por un fallo
# en exec, o por algún truco del intruso, tomamos medidas):
tomar_medidas();
exit(1);


##############################################################################


# Proporciona un array con los parámetros que se le dan al usuario.
sub parametros {
 my $param1 = 13 + int (rand(10000));
 my $param2 = 13 + int (rand(100));
 #$param1 = 7521;
 #$param2 = 77;
 return($param1, $param2);
}


# Devuelve 0 si la respuesta coincide con el número que se esperaría.
# Se le pasan los parámetros de &parametros.
sub calcular {
 my ($a, $b) = @_;
 my $dia = (localtime(time))[3];
 my $respuesta;

 # Inventar aquí el algoritmo: respuesta = f(a, b, c, ...)
 # Otra opción sería usar tokens:
 # (números aparentemente aleatorios pero con una estructura interna desconocida
 # para el atacante. Calculados de manera automática y de un sólo uso.
 # --------------------------
 $respuesta = abs (int (log($a + $b * $dia) * 10000));
 # --------------------------

 return $respuesta;
}


# Hemos comprobado que el usuario es legítimo y ejecutamos la shell.
sub do_shell {
 syslog('notice', 'Respuesta correcta, entra %s.', $usuario);

 # Reemplazando SHELL y llamando a exec de esa manera es como si el escudo nunca
 # hubiera existido por medio.
 $ENV{SHELL} = '/bin/bash';
 exec {"/bin/bash"} "-bash";
}

sub interr {
 tomar_medidas();
}


# Esta función toma las medidas que se prevean. Generalmente enviar un correo
# o bloquear la IP atacante al cabo de algunos intentos.
sub tomar_medidas {
# print "Password comprometida. ¡Fuera!\n¡Avisaré a $usuario!\n";
 my $conn = $ENV{SSH_CLIENT} || "localhost";
 syslog('notice',
     'Respuesta incorrecta al desafio para %s: acceso denegado.',
     $usuario);
 
 my $smtp = Net::SMTP->new('localhost');
    $smtp->mail($usuario.'@localhost');
    $smtp->to($usuario.'@localhost');
    $smtp->data();
    $smtp->datasend("To: $usuario\n");
    $smtp->datasend("From: root\n");
    $smtp->datasend("Subject: Clave de $usuario comprometida.\n");
    $smtp->datasend("\n");
    $smtp->datasend("Alguien intento entrar desde $conn con la clave de $usuario.\n");
    $smtp->dataend();
    $smtp->quit;

 closelog();

 # La última medida que se toma, es por supuesto, terminar la shell para tirar la sesión.
 exit(1);
}


El funcionamiento es sencillo. Lo primero que hacemos es capturar todas la interrupciones que pueden ocurrir. Para conseguir que una vez llamado, el programa sea una trampa: o se contesta bien, o se tomarán medidas.

Por otro lado no nos interesa que el escudo moleste cuando iniciemos sesión nosotros localmente. Así que justo después hay una condición para que si la conexión no es desde un terminal remoto por SSH nos presente la shell sin mediar palabra.

Una primera rutina nos genera unos parámetros dentro del rango adecuado a nuestro algoritmo. Estos números son los que nos presentará el escudo cuando intentemos entrar. Una segunda rutina se encarga de generar la respuesta que corresponde a esos parámetros. Como la calculadora en el móvil debe seguir el mismo que hay programado en el escudo las respuestas deben ser idénticas.

Si la respuesta era la esperada por el escudo, llamamos a la shell. Fijaos la forma de llamarla con exec, y cómo se reemplaza la variable de entorno SHELL. Así es como si el escudo nunca hubiera estado en medio.

La rutina tomar_medidas actúa según hayamos programado.
  • Evidentemente sale del programa. Terminando la shell y desconectando al posible intruso.
  • Puede enviar un correo a alguna dirección que se ponga para que nos avise de que alguien ha conseguido entrar con nuestra contraseña.
  • Podría cambiar la contraseña a otra que tengamos programado. Para el caso de que comprometan la primera podamos seguir entrando con la de respaldo.
  • Podría bloquear la IP al cabo de unos cuantos intentos infructuosos. Aunque haría falta un poco de maña, porque el escudo corre con los privilegios del usuario.


Observaciones

Es de cajón, pero hay que advertir no teclear en la calculadora de Windows o en la de Google la operación. Porque si realmente hay instalado un programa para capturar claves también descubrirá las operaciones que hacemos.

Yo he optado por sustituir a la shell del usuario. Pero otra opción es sustituir al programa loginpara que actúe sobre todos los usuarios. Este paso es peliagudo, porque login hace algunas cosas más a parte de autenticar al usuario y lo que es peor: se ejecuta con privilegios de root. Así que cualquier fallo es peligroso.

Si quisiéramos usarlo para varios usuarios lo mejor sería un fichero de configuración en el home del usuario. Bien con el código del algoritmo para ejecutarlo con eval, o bien con algunos números que sirvan de parámetros y poder personalizar el algoritmo para cada usuario.

Huelga decir que el código que presento hay que modificarlo si lo quieres usar. No dejes el algoritmo que pongo de ejemplo.
Artículo completo >>

Mando tipo ultraligero para simulador de vuelo

Hoy os quiero enseñar un invento que hice hace unos años para jugar al FlightGear Flight Simulator. Es muy sencillo y una forma de aprovechar un viejo ratón de bola.

Se trata de que controlar el avión por teclado es muy insípido. Hacerlo con un ratón pues también. Y hacerlo con un joystick tamaño normal deja mucho que desear. Así que vamos a construir un pseudo-joystick de talla XXL que asemeja bastante a las palancas de los ultraligeros. Un ejemplo en esta imagen.

No hay mucho que contar y yo no tengo mucho tiempo para escribir. Así que os explico mientras veis las fotos. La pinta hay que admitir que no es nada profesional. Digamos que si funciona, es suficiente.


Como otros buenos inventos españoles, como el chupachups o la fregona, este consiste tomar un ratón y ponerle un palo. Únicamente hay que fijar el ratón a la base y utilizar unos hilos para que el movimiento de la palanca se transmita a los rodillos.

En la foto de abajo vemos cómo hemos fijado los hilos con argollas. Es un detalle importante para que el chisme sea desmontable.

Observad que las argollas de los laterales están más arriba que las del frente y atrás. Mientras más arriba está el hilo, más se nota cuando movemos la palanca. Lo que conseguimos así es que el alabeo (movimiento lateral) del avión sea más sensible que el cabeceo. O dicho en otras palabras, tenemos más precisión para girar el avión que para levantar o bajar el morro.


Esos hilos los pasamos por unos ejes para guiarlo, en nuestro caso hechos con clavos doblados.


Y después rodeamos los rodillos del ratón de forma que el movimiento del hilo se transmita a ellos. Es preciso que el hilo quede tenso, aunque no demasiado. Si fuera necesario podríamos también ponerle un pequeño muelle en algún punto intermedio del hilo para que no se afloje.


Luego hay que sacar los interruptores del ratón para llevarlos a un lugar más accesible, cerca de la mano, por si tenemos que pulsarlos. Utilizaremos una clavija para poder conectar y desconectar el cable cuando los desmontemos.



Para que el eje pivote libremente pero no se salga de su posición podemos utilizar la base de un remache como vemos aquí:


Y finalmente un buen toque sería que la longitud de los hilos sea tal que además de engancharlos a las argollas, sirva igualmente para dejarlos enganchados en los ejes que habíamos dicho antes. Y que cuando lo desmontemos no queden hilos por ahí colgando.


Si el montaje está suficientemente tenso el eje se mantiene vertical por el rozamiento y no hace falta sujetarlo.

Como algunos os habréis dado cuenta, si inclinamos la palanca demasiado los hilos se destensan y se pueden salir de su sitio. Pero esto lo hemos planteado para ser el mando de un avión, y no es probable que necesitemos tanto ángulo para manejarlo.
Artículo completo >>