Electrónica y Programación en Personal
«Si no se soluciona con un script en Python entonces no es viable»

por Yeison Cardona el 01 de marzo del 2013 a las 12:07 am UTC

Descripción del protocolo

El protocolo permite enviar dos datos: una dirección y un comando, cada uno de un byte de longitud.
La modulación se hace mediante ráfagas de pulsos de 38kHz, cada ráfaga dura 560μs, la información se extrae de una ráfaga y la distancia hasta la siguiente ráfaga, siendo un «1» lógico una ráfaga y una distancia que dura 1.69ms y el «0» lógico una ráfaga y una distancia de 560μs, así:



Cada paquete de datos (la dirección y el comando) se envían separados por una ráfaga larga de 9ms y un espacio de 4.5ms, luego los bits de la dirección (como se explicó previamente) y nuevamente los bits de la dirección pero esta vez negados, luego los bits del comando y los bits del comando negados, para finalizar, se envía una ráfaga de 560μs, así:
  • Ráfaga larga de 9.5ms
  • Espacio de 4.5ms
  • Byte (8 bits) de la dirección
  • Byte de la dirección negada
  • Byte del comando
  • Byte del comando negado
  • Ráfaga de 560μs


En esta imagen se muestra el envío a la dirección 0b10011010 del comando 0b01101000, o en hexadecimal: 0x9a a 0x68,.
El concepto de dirección es casi irrelevante, es igual verlo como otro comando más.

Decodificando el protocolo con Pinguino

Dado que el protocolo trabaja con ráfagas a «altas» frecuencias entonces resulta «en teoría» un poco complicado saber qué se está leyendo, puede que en un hecho fortuito (o infortuito) que cada que hagamos unas cuantas lecturas las respuestas sean un cero lógico ya que estamos leyendo justo en un cero de la ráfaga, sin embargo esto es casi imposible (teorías probabilistas que no vienen al caso), entonces podemos suponer que las ráfagas serán simplemente un «1» lógico, de esta manera nos resulta un poco mas fácil de realizar el decodificador.

Ya sabemos que cada dato inicia con «1» lógico (una ráfaga pulsos de 38kHz) de 9ms, luego se envían los bytes, entonces la primera tarea es detectar este «inicio», de esta manera:
u8 startBit(){
    cont = 0;
    while (!READ_IRpin) {
        cont=cont+RESOLUTION;
        delayMicroseconds(RESOLUTION);
        }
    while (READ_IRpin) {}
    if (cont > 8000) return 1;
    else return 0;
    }
Esta función retornará un «verdadero» cuando se ha terminado de enviar una ráfaga de 9ms, READ_IRpin simplemente hace una lectura digital (del pin cero en éste caso), éste método es más rápido que digitalRead, por eso en el inicio del código se ha definido:
#define READ_IRpin (PORTB & 1<<0)
Ahora es necesario capturar todos los bits que vienen a continuación, se hace de la misma manera que en el anterior código:
u8 getBit(){
    while (!READ_IRpin) {}
    cont = 0;
    while (READ_IRpin) {
        cont=cont+RESOLUTION;
        delayMicroseconds(RESOLUTION);
        }
    if (cont > 1000) return 1;
    else return 0;
    }
Éste método que usa ciclos y pausas para medir un tiempo es muy útil sobre todo cuando se están trabajando con unidades de tiempo muy pequeñas, además nos permite incrementar el tiempo basado en una resolución definida, lo cual no hace más fácil la tarea a la hora se calcular duración ya que estamos trabajando sobre dos duraciones finales diferentes (para uno y cero), otro beneficio es aligerar un poco la tarea del procesador ya que hace menos mediciones por unidad de tiempo.

Finalmente la forma de poner todo esto a funcionar es muy simple, ya que en el loop sólo tendremos:
void loop() {  
    
    if (startBit()){
        for (i=0 ;i<32 ;i++) CDC.printf("%d",getBit());
        CDC.print("\n");
        }
    }
Lo que dice (y hace) es básicamente esto: espere hasta lea una ráfaga de inicio, listo, entonces lea y envíe los siguientes 32 bits (8 dirección + 8 dirección negada + 8 comando + 8 comando negado) y termine la línea; espere hasta lea otra ráfaga de inicio...

Ahora queda la tarea de procesar dicha señal, como la estamos enviando por CDC entonces eso significa que la vamos a leer des el computados y ya lo suponen, con Python...

El código completo sería el siguiente:
/*----------------------------------------------------- 
Author: Yeison Cardona --<yeison.eng@gmail.com>
Date: Fri Sep 28 18:56:57 2012
Description: Decodificador IR con Pinguino

-----------------------------------------------------*/

#define READ_IRpin (PORTB & 1<<0)
#define RESOLUTION 20

u16 pulses[50]; 
u32 con_pulse, i, d0;
u32 cont;
u16 total_pulse=0;

void setup() {
    pinMode(IRpin,INPUT);
    pinMode(10,OUTPUT);
    }
    
u8 startBit(){
    cont = 0;
    while (!READ_IRpin) {
        cont=cont+RESOLUTION;
        delayMicroseconds(RESOLUTION);
        }
    while (READ_IRpin) {}
    if (cont > 8000) return 1;
    else return 0;
    }
    
u8 getBit(){
    while (!READ_IRpin) {}
    cont = 0;
    while (READ_IRpin) {
        cont=cont+RESOLUTION;
        delayMicroseconds(RESOLUTION);
        }
    if (cont > 1000) return 1;
    else return 0;
    }
    

void loop() {  
    
    if (startBit()){
        for (i=0 ;i<32 ;i++) CDC.printf("%d",getBit());
        CDC.print("\n");
        }
    }

Lectura, Verificación e Interpretación de los comando con Python

Vamos a leer cada línea enviada, una línea es un paquete de datos de 32 bits, luego vamos a verificar que los datos sean válidos, para eso las tramas negadas, obtener la dirección y el comando (en hexadecimal).
    def getData(self, data):
        address = data[:8]
        address_ = data[8:16]
        command = data[16:24]
        command_ = data[24:]
        
        verify = map(lambda x, y: x == y, address, address_)
        if verify.count(True) > 0: return False
        
        verify = map(lambda x, y: x == y, command, command_)
        if verify.count(True) > 0: return False
        
        address = hex(eval('0b'+address))
        command = hex(eval('0b'+command))
        return address, command
Esta función se encarga de todo lo que habíamos acordado, recibe la trama de 32bits, la separa, comprueba los bits con sus respectivos bits negados, de haber una violación al protocolo entonces descarta el dato, termina y retorna False, si todo va bien entonces obtiene y retorna los valores hexadecimales de la dirección y el comando.

Durante el proceso anterior debemos leer cada línea disponible en el puerto serie, asegurarnos de que sea de 32 caracteres de longitud y si es válido ejecutar un comando:
    def readCode(self):
        data = self.pinguino.readline().replace("\n", "")
        self.pinguino.flushInput()
        if len(data) == 32:
            key = self.getData(data)
            if key != False:
                self.execute(key)
Los comandos están almacenados en diccionarios conde las claves son cada par de dirección-comando que hemos recibido y validado, si lo que se recibió esta dentro de las claves de dicho diccionario, entonces ejecutará el comando asociado a la clave:
    def execute(self, key):
        address = key[0]
        command = key[1]
        print address, command
        if (address, command) in self.exe.keys():
            os.system(self.exe[(address, command)])
El resto del código consiste en conectarse al puerto serial y en definir una clase, también he agregado una lista de comandos que ha funcionado para mi, aquí está:
#!/usr/bin/env python
#-*- coding: utf-8 -*-

import sys, os
import serial

########################################################################
class IR_controller:
    """"""
    #----------------------------------------------------------------------
    def __init__(self):
        self.pinguino = False
        

        ##A cada comando se le asocia en un diccionario un comando de consola
        self.exe = {("0xff", "0x98"): "xdotool key 'XF86AudioPlay'",
                    ("0x20", "0x22"): "xdotool key 'XF86AudioPlay'",
                    ("0x76", "0x10"): "xdotool key 'XF86AudioPlay'",
                    ("0xff", "0x40"): "xdotool key 'XF86AudioStop'",
                    ("0x76", "0x80"): "xdotool key 'XF86AudioStop'",
                    ("0xff", "0x20"): "xdotool key 'XF86AudioNext'",
                    ("0xff", "0x60"): "xdotool key 'XF86AudioPrev'",
                    ("0x76", "0xc0"): "xdotool key 'XF86AudioNext'",
                    ("0x76", "0x40"): "xdotool key 'XF86AudioPrev'",
                    
                    ("0x20", "0x0"): "xdotool key n",
                    ("0xc8", "0xc0"): "xdotool key n",
                    ("0x20", "0x80"): "xdotool key p",
                    ("0xc8", "0xe0"): "xdotool key p",
                    
                    ("0x76", "0xe4"): "xdotool key 'Shift+Right'",
                    ("0x76", "0x14"): "xdotool key 'Alt+Right'",
                    ("0x76", "0x94"): "xdotool key 'Ctrl+Right'",
                    ("0x76", "0xb4"): "xdotool key 'Ctrl+Shift+Right'",
                    #("0x76", "0x54"): "xdotool key ''",
                    #("0x76", "0x74"): "xdotool key ''",
                    #("0x76", "0xd4"): "xdotool key ''",
                    
                    ("0x76", "0x34"): "xdotool key 'Shift+Left'",
                    ("0x76", "0xa4"): "xdotool key 'Alt+Left'",
                    ("0x76", "0x24"): "xdotool key 'Ctrl+Left'",
                    ("0x76", "0xc4"): "xdotool key 'Ctrl+Shift+Left'",
                    #("0x76", "0x84"): "xdotool key ''",
                    #("0x76", "0xf4"): "xdotool key ''",
                    #("0x76", "0x4"): "xdotool key ''",
                    
                    
                    ("0xff", "0xc8"): "xdotool key 'XF86AudioLowerVolume'",
                    ("0x20", "0x40"): "xdotool key 'XF86AudioRaiseVolume'",
                    ("0xc8", "0xd0"): "xdotool key 'XF86AudioRaiseVolume'",
                    ("0x20", "0xc0"): "xdotool key 'XF86AudioLowerVolume'",
                    ("0xc8", "0xf0"): "xdotool key 'XF86AudioLowerVolume'",
                    ("0xff", "0x38"): "xdotool key 'XF86AudioRaiseVolume'",
                    ("0xff", "0xd8"): "xdotool key 'XF86AudioMute'",
                    ("0xc8", "0xe4"): "xdotool key 'XF86AudioMute'",
                    ("0x20", "0x90"): "xdotool key 'XF86AudioMute'",
                    ("0x76", "0x68"): "xdotool key super",
                    ("0x76", "0xfa"): "xdotool click --repeat 2 -delay 1 1",
                    ("0x76", "0xf8"): "xdotool key 'BackSpace'",
                    ("0x76", "0xe5"): "xdotool key 'Alt'+'F4'",
                    
                    ("0x20", "0xd8"): "xdotool key 'Enter'",
                    
                    ("0x20", "0x10"): "xdotool key 'Escape'",
                    ("0xc8", "0xc4"): "xdotool key 'Escape'",
                    
                    
                    
                    }
        
        
        #Enlazamos Pinguino
        self.conectar()
        if self.pinguino == False:  #si se puede
            raw_input("No hay dispositivos")
            sys.exit()
    
    #----------------------------------------------------------------------
    def conectar(self):
        #Conectamos al primer puerto serie que se encuentre
        for i in range(20):
            try:
                self.pinguino = serial.Serial("/dev/ttyACM%d"%i, timeout=1)
                return self.pinguino
            except: self.pinguino = False
        return False
    
    #----------------------------------------------------------------------
    def readCode(self):
        data = self.pinguino.readline().replace("\n", "")
        self.pinguino.flushInput()
        if len(data) == 32:
            key = self.getData(data)
            print key
            if key != False:
                self.execute(key)
            
    #----------------------------------------------------------------------
    def getData(self, data):
        address = data[:8]
        address_ = data[8:16]
        command = data[16:24]
        command_ = data[24:]
        
        verify = map(lambda x, y: x == y, address, address_)
        if verify.count(True) > 0: return False
        
        verify = map(lambda x, y: x == y, command, command_)
        if verify.count(True) > 0: return False
        
        address = hex(eval('0b'+address))
        command = hex(eval('0b'+command))
        return address, command
        

    #----------------------------------------------------------------------
    def execute(self, key):
        address = key[0]
        command = key[1]
        if (address, command) in self.exe.keys():
            os.system(self.exe[(address, command)])

dec = IR_controller()
while True: dec.readCode()
Para agregar un nuevo comando sólo basta con apuntar lo que se imprime en pantalla y agregarlo a la lista con su respectivo comando, éste comando debe ser una orden de terminal válida.

Conclusiones

Es relativamente fácil añadir un control por infrarrojos a nuestros proyectos o bien sea a nuestro propio computador, y de esta manera reciclar un poco de electrónica vieja que de seguro tenemos en casa.
Hay varios protocolos similares al NEC, pero éste es el más común.

También podría interesarte:

Añadir un comentario:
Si desean una respuesta para su comentario sólo deben agregarme en G+ y hacer una mención a Yeison Cardona, así les podré responder lo antes posible.