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.

26 de mayo de 2014

El LED parpadeante

En la entrada anterior introduje brevemente a la Raspberry Pi (RPi). Ahora toca elaborar nuestro primer proyecto usando esta plataforma. Vamos a programar en Python uno de los pines de GPIO (Entrada/Salida de Propósito General) para controlar el parpadeo de un LED. Este proyecto es considerado el “¡Hola Mundo!” de la electrónica.

El hardware

Además de la RPi correctamente configurada y funcional, requeriremos los siguientes componentes:
  • Un protoboard.
  • Un LED (diodo emisor de luz).
  • Una resistencia de 330Ω (bandas naranja, naranja, café).
  • Dos cables conectores.
Es muy importante conectar los componentes de forma correcta para evitar todo tipo de problemas. El ánodo del LED (la patita larga) se conecta al pin GPIO 24 de la RPi, mientras que el cátodo (la patita corta) se conecta a la resistencia. El otro extremo de la resistencia se conecta a tierra (cualquiera de los pines GND de la RPi). La siguiente figura, elaborada con Fritzing, muestra cómo se ven los componentes ya conectados:

Vista de protoboard Fritzing de un LED conectado a la RPi.

La resistencia se necesita con el fin de evitar que el LED reciba demasiada corriente y se dañe. Para no complicar demasiado las cosas, se recomienda utilizar una resistencia entre 270Ω y 330Ω (si se requiere ser más precisos al respecto, se debe aplicar la Ley de Ohm y conocer la tensión del LED así como la corriente que pasa por éste).

La siguiente foto muestra físicamente la forma en que se ven las conexiones de nuestros componentes electrónicos:

Vista física de un LED conectado a la RPi.

Hay que notar que en la foto se aprecia un GPIO breakout (la tarjeta de interfaz negra) que facilita conectar la RPi al protoboard a través de un cable plano. En este caso, los cables conectores a utilizar deben ser macho-macho. Si no se cuenta con el breakout, se pueden usar cables conectores macho-hembra para conectar directamente los pines GPIO de la RPi al protoboard.

El software

Ahora veremos la manera de escribir nuestro programa en Python. Primeramente necesitamos importar el módulo RPi.GPIO:
import RPi.GPIO as GPIO
De esta forma podemos referirnos al módulo simplemente usando el identificador GPIO. Las distribuciones recientes de Raspian traen pre-instalado este módulo para Python 2.7.

Como segundo paso, necesitamos indicar la manera en que estaremos haciendo referencia a los números de pin de la RPi. El módulo RPi.GPIO permite hacerlo de dos formas:
  • BOARD: Los pines se numeran usando la distribución física de la cabecera de expansión P1 de 26 pines.
  • BCM: Los pines se numeran según la designación de canales de Broadcom SoC (System-on-a-chip). En otras palabras, se usan los números GPIO.
La siguiente figura (adaptada de elinux.org) muestra la correspondencia entre la numeración de los pines BOARD (fondo blanco) y los pines BCM (fondo naranja):

Numeración de pines BOARD (blanco) y BCM (naranja),
para la RPi modelo B, revisión 2.

Por ejemplo, el pin GPIO 24 corresponde al pin físico 18, mientras que el pin GPIO 17 corresponde al pin físico 11.

La función GPIO.setmode() recibe como argumento GPIO.BOARD o GPIO.BCM para indicar el esquema de numeración a utilizar:
GPIO.setmode(GPIO.BCM)
En nuestro proyecto usaremos la numeración BCM. Hay que notar que se debe tener algo de cuidado con este esquema, ya que hay algunas diferencias sutiles entre las revisiones 1 y 2 del modelo B de la RPi. Sin embargo, el proyecto que estamos construyendo debe funcionar sin problemas con cualquiera de las dos revisiones. En nuestro programa de Python podemos utilizar el valor de la constante simbólica GPIO.RPI_REVISION para averiguar el número de revisión.

El tercer paso consiste en indicar la dirección (entrada o salida, usando GPIO.IN o GPIO.OUT, respectivamente) que usaremos con el pin 24:
GPIO.setup(24, GPIO.OUT)
La documentación le llama a lo anterior “establecer un canal”.

Ahora, para controlar nuestro LED solo tenemos que usar la función GPIO.output(), indicando como argumentos el número de pin y GPIO.HIGH si queremos prender el LED o GPIO.LOW si lo queremos apagar. Por ejemplo:
GPIO.output(24, GPIO.HIGH)
Para producir un efecto de parpadeo necesitamos prender el LED, pausar por un tiempo, apagar el LED, volver a pausar, y repetir lo anterior cuantas veces queramos. Para pausar un momento nuestro programa en Python podemos usar la función sleep() del módulo time, la cual recibe como argumento la cantidad de segundos que se debe detener.

Finalmente, se recomienda invocar a la función GPIO.cleanup() para reiniciar todos los canales de GPIO y con esto asegurarnos que el LED quede apagado y también evitar algunos mensajes de advertencia que pudieran aparecer la siguiente vez que se corra nuestro programa. Dado que deseamos que siempre ocurra esto al final del programa (sin importar si éste terminó de forma normal o a raíz de una excepción), lo más conveniente es colocar la instrucción GPIO.cleanup() como parte de la cláusula finally de una instrucción try.

El siguiente programa completo integra lo que se discutió anteriormente:
# Archivo: parpadea.py

import RPi.GPIO as GPIO
import time

# Constantes
TIEMPO_PAUSA = 0.5     # Las pausas duran medio segundo.
PIN_LED = 24           # Pin donde esta conectado
                       # el anodo del LED.

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

try:
    # Ciclo infinito.
    # Para terminar el programa de debe presionar Ctrl-C.
    while True:
        GPIO.output(PIN_LED, GPIO.HIGH)
        time.sleep(TIEMPO_PAUSA)
        GPIO.output(PIN_LED, GPIO.LOW)
        time.sleep(TIEMPO_PAUSA)
finally:
    GPIO.cleanup()
Para correr el programa, requerimos ejecutar el siguiente comando desde una terminal en el mismo directorio donde radica el archivo parpadea.py:
sudo python parpadea.py
El comando debe empezar con sudo debido a que el programa de Python necesita privilegios de administrador para poder manipular los puertos de GPIO.

Cuando corremos el programa, el LED se prende por medio segundo, y luego se apaga durante otro medio segundo. Esto se repite indefinidamente hasta que  presionemos Ctrl-C para terminar el programa. Podemos cambiar el valor de la constante simbólica TIEMPO_PAUSA para hacer que el LED parpadee más lento o más rápido.

Usamos la constante simbólica PIN_LED para indicar el número de pin GPIO al que conectamos el ánodo del LED. Si después queremos usar otro número de pin, solo tenemos que cambiar este dato en un solo lugar de nuestro programa y no en múltiples sitios.

Si por fin logramos hacer funcionar este proyecto, entonces estamos listos para hacer proyectos más complicados e interesantes. ¡Yupi!