30 de mayo de 2014

Código morse

En una entrada anterior elaboramos un proyecto relativamente sencillo en el que usamos la Raspberry Pi (RPi) para prender y apagar un LED dentro de un ciclo infinito. En esta ocasión vamos a utilizar la misma configuración de hardware de dicho proyecto, pero ahora con un programa más interesante que permita enviar mensajes utilizando código morse. Este es otro proyecto clásico para los recién iniciados en la RPi y otras plataformas electrónicas afines.

Creadores del código morse:
Samuel Morse y Alfred Vail

La Wikipedia explica los detalles que necesitamos conocer sobre el código morse:
Fue desarrollado por Alfred Vail mientras colaboraba en 1830 con Samuel Morse en la invención del telégrafo eléctrico. Vail creó un método según el cual cada letra o número era transmitido de forma individual con un código consistente en rayas y puntos, es decir, señales telegráficas que se diferencian en el tiempo de duración de la señal activa. La duración del punto es la mínima posible. Una raya tiene una duración de aproximadamente tres veces la del punto. Entre cada par de símbolos de una misma letra existe una ausencia de señal con duración aproximada a la de un punto. Entre las letras de una misma palabra, la ausencia es de aproximadamente tres puntos. Para la separación de palabras transmitidas el tiempo es de aproximadamente tres veces el de la raya.

El telégrafo original de Samuel Morse.
De la colección del museo Nacional Smithsoniano
de Historia de los Estados Unidos.

El alfabeto morse (junto con símbolos de numeración y puntuación) se muestra en la siguiente tabla:

Código morse

Si deseamos, por ejemplo, enviar el mensaje “hola” usando el código morse, tendríamos la siguiente secuencia de rayas y puntos:
····  −−−  ·−··  ·−
Comúnmente, los puntos y rayas son generados a partir de señales audibles o el encendido y apagado de una fuente de luz. En nuestro proyecto usaremos nuestro LED para parpadear los puntos y rayas del mensaje que deseamos enviar. En las siguientes secciones iremos construyendo nuestro programa poco a poco.

Usando diccionarios

La tabla de código morse puede ser convertida a un diccionario de Python. Un diccionario es una estructura de datos que permite asociar una cierta llave a un valor determinado. Por ejemplo:
d = { 'pollito': 'chicken',
      'gallina': 'hen',
      'lapiz':   'pencil',
      'pluma':   'pen'
    }
En este caso, las cadenas 'pollito', 'gallina', 'lapiz' y 'pluma' son las llaves del diccionario contenido en la variable d. Decimos que la llave 'pollito' está asociada al valor 'chicken', y así con cada una de las llaves. Es importante notar en la sintaxis del diccionario el uso del carácter de dos puntos (:). A la izquierda de este carácter va la llave, y a la derecha está su valor asociado. Las parejas de llave-valor están delimitadas entre sí usando comas. Otro detalle importante a notar es que las llaves deben ser siempre objetos inmutables (por ejemplo números o cadenas de caracteres).

La expresión:
d['lapiz']
devuelve la cadena 'pencil', ya que la llave 'lapiz' está asociada al valor 'pencil' dentro del diccionario d.

Por otro lado, la expresión:
d['ventana']
produce un error (arroja la excepción KeyError) ya que la llave 'ventana' no existe en el diccionario d.

Si deseamos determinar si una llave existe en el diccionario podemos utilizar el operador in:
'lapiz' in d
En este caso, la expresión anterior devolvería True. Si la llave no hubiera existido, el resultado devuelto habría sido False.

Hay muchas más operaciones que se pueden efectuar sobre un diccionario. La documentación oficial describe todos los detalles de este tipo de dato. Cabe también mencionar que los diccionarios de Python están implementados usando tablas de hash, por lo que los tiempos de acceso e inserción son bastante rápidos.

El diccionario que necesitamos para nuestro proyecto quedaría así:
codigo = {
    'A': '.-',     'B': '-...',    'C': '-.-.',
    'D': '-..',    'E': '.',       'F': '..-.',
    'G': '--.',    'H': '....',    'I': '..',
    'J': '.---',   'K': '-.-',     'L': '.-..',
    'M': '--',     'N': '-.',      'O': '---',
    'P': '.--.',   'Q': '--.-',    'R': '.-.',
    'S': '...',    'T': '-',       'U': '..-',
    'V': '...-',   'W': '.--',     'X': '-..-',
    'Y': '-.--',   'Z': '--..',    '1': '.----',
    '2': '..---',  '3': '...--',   '4': '....-',
    '5': '.....',  '6': '-....',   '7': '--...',
    '8': '---..',  '9': '----.',   '0': '-----',
    '.': '.-.-.-', ',': '--..--',  ':': '---...',
    ';': '-.-.-.', '?': '..--..',  '!': '-.-.--',
    '"': '.-..-.', "'": '.----.',  '+': '.-.-.',
    '-': '-....-', '/': '-..-.',   '=': '-...-',
    '_': '..--.-', '$': '...-..-', '@': '.--.-.',
    '&': '.-...',  '(': '-.--.',   ')': '-.--.-'
}

Filtrando el mensaje

El mensaje que deseamos enviar usando código morse estará contenido en una cadena de caracteres (string) de Python. Sin embargo dicha cadena puede tener algunos problemas. Por ejemplo, la cadena “¡Adiós Niños!” contiene caracteres que no están en nuestro diccionario. Específicamente aparecen tres casos que tenemos que considerar:
  1. Letras minúsculas.
  2. Letras acentuadas (ó) o con tilde (ñ).
  3. Símbolos especiales (¡).
Para el caso 2 definiremos la siguiente función, la cual elimina los acentos, tildes y diéresis que hay en ciertas letras que usamos en el idioma español:
import unicodedata

def convierte_ascii(c):
    return (unicodedata.normalize('NFD', c.decode('utf8'))
            .encode('ascii', 'ignore'))
Si invocamos esta función así:
convierte_ascii('¡Adiós Niños!')
nos devuelve la cadena 'Adios Ninos!'. Como se puede ver, la función se encarga parcialmente también del caso 3.  Cualquier carácter que no sea ASCII se elimina, como es el caso del signo de exclamación invertido (¡). Sin embargo, hay algunos caracteres ASCII (por ejemplo el asterisco o el signo de por ciento) que no son eliminados, por lo que tendrían que estar presentes como llaves en nuestro diccionario de código morse, pero no es así. Nos encargaremos de estos caracteres en la siguiente sección.

El caso 1 se resuelve de manera muy sencilla. Después de transformar las letras con acentos, diéresis y tildes, solo necesitamos convertir las letras resultantes a mayúsculas. Para ello usamos el método upper() sobre la cadena.

Traduciendo a código morse

Una vez que tenemos el diccionario de codigo morse y el mecanismo de filtrado, solo falta tomar cada carácter del mensaje de entrada y convertirlo a su equivalente en morse. Supongamos que la variable mensaje tiene el texto a traducir. El siguiente código se encarga de hacer la parte principal de la conversión:
resultado = []
for c in convierte_ascii(mensaje).upper():
    if c in codigo:
        resultado.append(codigo[c])
    elif c == ' ':
        resultado.append('  ')
Usamos la lista resultado para ir acumulando las cadenas resultantes de la traducción. El for trabaja sobre el mensaje filtrado, el cual solo contiene caracteres ASCII y letras mayúsculas. Para cada carácter del mensaje, si es un carácter que existe como llave en el diccionario, entonces se coloca su valor asociado (una cadena con la secuencia de puntos y rayas correspondiente) en la lista resultante. Como caso especial, si hay un espacio en el mensaje, éste se denota con dos espacios que representan un separador entre palabras. Cualquier otro carácter del mensaje de entrada es ignorado, y con ello nos encargamos completamente del caso 3 de la sección anterior.

Si el valor de la variable mensaje es '¡Adiós Niños!', después de ejecutar el ciclo for la variable resultado contiene lo siguiente:
['.-', '-..', '..', '---', '...', '  ',
 '-.', '..', '-.', '---', '...', '-.-.--']
Ahora, usaremos el método join() para convertir el contenido de esta lista en una gran cadena. A continuación tenemos un ejemplo del uso de este método:
'#'.join(['uno', 'dos', 'tres', 'cuatro'])
El método recibe una lista de cadenas y produce una sola cadena que resulta de concatenar todas las cadenas pero delimitándolas entre sí usando la cadena correspondiente al objeto receptor del método (la cadena '#' en el ejemplo). La expresión anterior produciría 'uno#dos#tres#cuatro'.

Regresando a nuestro proyecto, aquí usaremos un espacio para delimitar todas las cadenas contenidas en la lista resultado:
' '.join(resultado)
Con el mensaje '¡Adiós Niños!', la cadena de código morse después del proceso completo de  traducción sería la siguiente:
'.- -.. .. --- ...    -. .. -. --- ... -.-.--'
Hay que notar que quedó un espacio en blanco entre letras y cuatro espacios entre palabras.

Controlando el LED

Para controlar el LED desde nuestra RPi, definiremos dos constantes simbólicas que representan las duraciones en segundos de un punto y una raya:
TIEMPO_PUNTO = 0.2
TIEMPO_RAYA  = TIEMPO_PUNTO * 3
Ahora procesaremos con un for cada carácter del mensaje traducido a código morse. Los caracteres solo pueden ser puntos, rayas o espacios. Si encontramos un punto, prendemos el LED por la duración de un punto. Si encontramos una raya, prendemos el LED por la duración de una raya. Si encontramos un espacio, apagamos el LED por la duración de un punto. Al final de cada iteración del ciclo, sin importar el carácter que hayamos encontrado anteriormente, apagamos el LED por la duración de un punto.
for c in convierte_morse(mensaje):
    if c == '.':
        GPIO.output(PIN_LED, GPIO.HIGH)
        time.sleep(TIEMPO_PUNTO)
    elif c == '-':
        GPIO.output(PIN_LED, GPIO.HIGH)
        time.sleep(TIEMPO_RAYA)
    elif c == ' ':
        GPIO.output(PIN_LED, GPIO.LOW)
        time.sleep(TIEMPO_PUNTO)

    GPIO.output(PIN_LED, GPIO.LOW)
    time.sleep(TIEMPO_PUNTO)
Si analizamos con cuidado la lógica de todo nuestro código podremos percatarnos que el LED efectivamente queda apagado por la duración de un punto entre símbolos (punto y raya) de una misma letra, por la duración de tres puntos entre dos letras de una misma palabra, y por la duración de nueve puntos entre dos palabras del mismo mensaje, tal como lo especifica el artículo citado de Wikipedia.

Así queda el programa completo:
# coding: utf8

# Archivo: morse.py

# La primera línea del archivo especifica que estamos
# usando una codificación UTF-8 en nuestro archivo
# fuente. Es indispensable hacer esto en Python 2 con
# el fin de poder incluir en el archivo caracteres que
# no sean ASCII (por ejemplo letras con acentos y eñes).

import RPi.GPIO as GPIO
import time
import unicodedata

# Duración de un punto y una raya.
TIEMPO_PUNTO = 0.2
TIEMPO_RAYA  = TIEMPO_PUNTO * 3

# Pin donde está conectado el ánodo del LED.
PIN_LED = 24

GPIO.setmode(GPIO.BCM)
GPIO.setup(PIN_LED, GPIO.OUT)

codigo = {
    'A': '.-',     'B': '-...',    'C': '-.-.',
    'D': '-..',    'E': '.',       'F': '..-.',
    'G': '--.',    'H': '....',    'I': '..',
    'J': '.---',   'K': '-.-',     'L': '.-..',
    'M': '--',     'N': '-.',      'O': '---',
    'P': '.--.',   'Q': '--.-',    'R': '.-.',
    'S': '...',    'T': '-',       'U': '..-',
    'V': '...-',   'W': '.--',     'X': '-..-',
    'Y': '-.--',   'Z': '--..',    '1': '.----',
    '2': '..---',  '3': '...--',   '4': '....-',
    '5': '.....',  '6': '-....',   '7': '--...',
    '8': '---..',  '9': '----.',   '0': '-----',
    '.': '.-.-.-', ',': '--..--',  ':': '---...',
    ';': '-.-.-.', '?': '..--..',  '!': '-.-.--',
    '"': '.-..-.', "'": '.----.',  '+': '.-.-.',
    '-': '-....-', '/': '-..-.',   '=': '-...-',
    '_': '..--.-', '$': '...-..-', '@': '.--.-.',
    '&': '.-...',  '(': '-.--.',   ')': '-.--.-'
}

def convierte_ascii(c):
    """Convierte c a una cadena que únicamente contiene
    caracteres ASCII.
    
    Convierte caracteres con acento, diéresis o tilde al
    carácter simple correspondiente. Elimina cualquier otro
    carácter que no sea ASCII.
    """
    return (unicodedata.normalize('NFD', c.decode('utf8'))
            .encode('ascii', 'ignore'))

def convierte_morse(mensaje):
    """Convierte mensaje a una cadena de símbolos morse.
    
    Las letras se delimitan entre sí con un espacio. Las
    palabras se delimitan con cuatro espacios.
    """
    resultado = []
    for c in convierte_ascii(mensaje).upper():
        if c in codigo:
            resultado.append(codigo[c])
        elif c == ' ':
            resultado.append('  ')
    return ' '.join(resultado)

def parpadea_morse(mensaje):
    """Prende y apaga un LED para enviar mensaje usando
    código morse.
    """
    try:
        for c in convierte_morse(mensaje):
            if c == '.':
                GPIO.output(PIN_LED, GPIO.HIGH)
                time.sleep(TIEMPO_PUNTO)
            elif c == '-':
                GPIO.output(PIN_LED, GPIO.HIGH)
                time.sleep(TIEMPO_RAYA)
            elif c == ' ':
                GPIO.output(PIN_LED, GPIO.LOW)
                time.sleep(TIEMPO_PUNTO)

            GPIO.output(PIN_LED, GPIO.LOW)
            time.sleep(TIEMPO_PUNTO)
    finally:
        GPIO.cleanup()

# Coloca aquí tu mensaje.
parpadea_morse('Este es un mensaje en código morse.')
Para correr el programa, teclear en la terminal de la RPi:
sudo python morse.py
Si todo salió bien, se debe ver el LED parpadeando el mensaje indicado en código morse.

2 comentarios: