Leyendo desde API de Wikipedia

Por Erwin Agüero y Daniela Núñez

En este artículo explicamos nuestro proyecto final del ramo Laboratorio de Arquitectura de Computadores (conocido también por la sigla  IIC2345) de la Pontificia Universidad Católica de Chile. El objetivo de este curso es analizar los componentes más relevantes que conforman un computador y cómo éstos se organizan, a través de una labor experimental a bajo nivel con elementos como memorias, buses, microcontroladores y periféricos, aprendiendo, además, los principales elementos del desarrollo de sistemas embebidos.

A lo largo del semestre, utilizamos como herramienta de trabajo el microcontrolador MSP430f5529 de Texas Instruments, que posee una serie de componentes que nos permitieron hacer diversos experimentos, programándolos haciendo uso de una librería en C que existe para la placa. Además, para este proyecto utilizamos el dispositivo Raspberry Pi (con sistema operativo Raspbian), que corresponde a un ordenador de placa reducida de bajo costo. Conectamos ambas placas como se observa en la foto y las comunicamos utilizando el protocolo UART, con tal de realizar consultas en la API de Wikipedia.

labarqui_proyecto

Descripción del proyecto

El objetivo que nos propuso el curso fue la comunicación serial de la placa MSP430 con la placa Raspberry Pi. La primera debía, por lo menos recibir datos desde la segunda, que a su vez tenía que obtener “algo interesante” desde alguna API disponible en internet.

Para cumplir con el objetivo anterior, lo que decidimos hacer fue convertir a la placa MSP430 en un terminal de consulta y a la Raspberry Pi en el de respuesta. Para eso, creamos un menú navegable en la placa MSP430, del que se puede elegir una opción, enviársela a la Raspberry Pi y recibir y mostrar su respuesta que procesa desde la API de Wikipedia.

Menú de palabras disponibles en la placa MSP430

labarqui_choosing

Con el botón S1 la enviamos a la placa Raspberry Pi

labarqui_choosing2

La placa MSP430 espera la respuesta de la Raspberry Pi

labarqui_sending

La placa Raspberry Pi recibe la consulta, busca algo relacionado en la API de Wikipedia y se lo envía a la placa MSP430labarqui_raspberry

La placa MSP430 recibe lo que le envió la Raspberry Pi y lo muestra en pantalla  

labarqui_response

Implementación necesaria en la placa MSP430

Desde la perspectiva de la placa MSP430, nuestro proyecto tiene 4 grandes partes. La comunicación UART, el potenciómetro, I/O del usuario (pantalla, botones y leds) y el main que orquesta todo lo demás. Entraremos en detalles sólo de la comunicación UART y del main, dado que el funcionamiento del potenciómetro, la pantalla y los botones se da por sabido, ya que fue trabajado en otras experiencias durante el semestre.

Lo primero fue, entonces, inicializar los parámetros necesarios para que la placa pudiera comunicarse por UART usando los buffers de lectura y escritura UCA1. Hacemos notar que como el recibir un dato gatilla una interrupción, parte de la inicialización es que el flag que cambia en la interrupción (RX_FLAG) parta apagado.

void uart_init(void)
{
        // Flag que indica si se recibió o no alguna lectura en el buffer correspondiente (se cambia en la interrupción)
        RX_FLAG = 0;

        // Inicialización de la comunicación UART usando UCA1, según el familyguide
        // Se considera además un baudrate compatible con la placa Raspberry Pi
        P4SEL |= BIT4 | BIT5;
        P4MAP4 = PM_UCA1TXD;
        P4MAP5 = PM_UCA1RXD;
        UCA1CTL1 |= UCSWRST;
        UCA1CTL0 = 0x00;
        UCA1CTL1 = UCSSEL__SMCLK | UCSWRST;
        UCA1BR1 = (109 & 0xFF00) >> 8;                         
        UCA1BR0 = 109 & 0x00FF;
        UCA1MCTL = UCBRS_2 | UCBRF_0;
        UCA1CTL1 &= ~UCSWRST;
        UCA1IE |= UCRXIE;

        //Debemos darle tiempo al protocolo para que termine de inicializarse, antes de poder usarlo y que entregue datos correctos en ambas direcciones
        __delay_cycles(1000);
}

Sobre la comunicación utilizando el protocolo UART, tuvimos que tener dos consideraciones más. La primera es que la transmisión de datos no es instantánea en ningún sentido, por lo que hay que esperar que termine antes de leer o escribir algo. Así que eso hay que incorporarlo en los métodos de lectura y escritura, por ejemplo, con el siguiente while:

int uart_putchar(int tx_byte)
{
    while(!(UCA1IFG & UCTXIFG));
    UCA1TXBUF = (uint8_t) tx_byte;
    return 0;
}

La segunda, también visible en el ejemplo de arriba, es el protocolo envía sólo de a un byte a la vez, por lo que tuvimos que crear un buffer que nos permitiera enviar y recibir strings en vez de caracteres sueltos. Aparte del buffer como estructura, usamos el flag RX_FLAG en un while para saber cuando la placa había terminado de recibir bytes.

//En uart.c
volatile uint8_t buff_int[BUFF_SIZE];
volatile uint16_t buff_count = 0;
...
interrupt(USCI_A1_VECTOR) USCI_A1_ISR(void)
{
        if(UCA1IFG & UCRXIFG)
        {
                buff_int[buff_count++] = (char)(UCA1RXBUF);
                buff_count %= BUFF_SIZE;
                RX_FLAG = 1;
                led_state(1,ON);
        }
}

En cuanto a la rutina principal, como es de esperar parte con las inicializaciones correspondientes (prender la placa, inicialización de leds, botones, pantalla LCD, potencióimetro y comunicación UART), para luego ingresar al ciclo principal, el que está compuesto de dos flujos posibles: qué pasa cuando se está recibiendo datos por UART y qué hacer mientras no.

El bloque de código siguiente mostramos qué es lo que hace la placa cuando se gatilla la interrupción de recepción de datos por UART. La placa MSP430 se da cuenta con el flag RX_FLAG que está recibiendo algo por UART y se queda esperando hasta que dicho flag se apaga (con un “delay cicles” en cada espera, con tal de darle tiempo a las placas para que se terminen de comunicar). Todo lo que haya recibido quedará en el buffer que revisamos en el bloque de código anterior y debe ser copiado a un buffer local para poder ser impreso en pantalla.

//...
uint8_t buff_local[BUFF_SIZE + 1];
uint16_t buff_count_local = 0;
extern volatile uint8_t buff_int[BUFF_SIZE];
extern volatile uint16_t buff_count;
//...
int main (void)
{
  //... Inicializaciones

  //Ciclo principal
  while(true) { 
    //cuando recibe datos
    if(RX_FLAG)
    {
      LCD_printLine("Receiving...");
      while(RX_FLAG)
      {
        RX_FLAG = 0;
        led_toggle(2);
        __delay_cycles(10000);
      }
      __disable_interrupt();
      buff_count_local = buff_count;
      buff_count = 0;
      memcpy ((char *) buff_local, (const char *) buff_int, buff_count_local);
      __enable_interrupt();
      buff_local[buff_count_local] = '';
      LCD_clear();
      led_state(1,OFF);
      led_state(2,OFF);
      led_state(3,ON);
      LCD_printf("%s\n", buff_local);
    }

//...continúa en el siguiente bloque

La otra posibilidad es que la placa no esté recibiendo datos. En este caso, se preocupará de estar atento al input del usuario a través del menú de palabras en pantalla y el potenciómetro. Esto se logra muestrando continuamente lo que está pasando con “la ruedita”. Si se detecta un cambio lo suficientemente significativo (tiene que haber un umbral porque, como el potenciómetro entrega un input análogo, éste es inestable), se revisa en qué dirección del menú se quiso mover el usuario, se cambia la opción seleccionada según eso y se refresca la pantalla, tarea de la que se encarga el método printOptions() cuando optionsChanged es ‘true’.

//...viene del bloque anterior
     //mientras no está recibiendo datos
      else
      {
        printOptions();
        uint16_t sample = pot_take_sample();
        double tolerance = 0.01*POT_MAX_VALUE;
        if(sample > old_sample + tolerance)
        {
          selected++;
          if(selected > WORDS_COUNT-1)
            selected=WORDS_COUNT-1;
          else
            optionsChanged = true;
        }
        else if(sample + tolerance < old_sample)
        {
          selected--;
          if(selected < 0) 
            selected = 0;
          else
            optionsChanged = true;
        }  
        old_sample =sample;    
      }
    }
  }
}

API de Wikipedia

Como se mencionó anteriormente , la api que escogimos fue Wikipedia. Esta pieza de software actúa como un wrapper de los artículos y páginas este sitio.

Para poder hacer uso de los métodos se debe incorporar el package instalado gracias a pip (para mayor detalle ver installation de la documentación oficial) de la siguiente manera:

import wikipedia

Con ello se logra acceder a los siguientes métodos usados en este proyecto:

  • wikipedia.search(str) : Que sugiere un query un arreglo de consultas para realizar en el sitio a partir de un string base.

>>> wikipedia.search("Barack")
 # [u'Barak (given name)', u'Barack Obama', u'Barack (brandy)', u'Presidency of Barack Obama', u'Family of Barack Obama', u'First inauguration of Barack Obama', u'Barack Obama presidential campaign, 2008', u'Barack Obama, Sr.', u'Barack Obama citizenship conspiracy theories', u'Presidential transition of Barack Obama'
  • wikipedia.summary(query,sentences): Entrega la información a partir de una query limitada por el número de oraciones solicitadas en sentences.

wikipedia.summary("Apple III", sentences=1)
 # u'The Apple III (often rendered as Apple ///) is a business-oriented personal computer produced and released by Apple Computer that was intended as the successor to the Apple II series, but largely considered a failure in the market. '

Otro métodos disponibles en la API son:

  • wikipedia.suggest(str) : Entrega una query apropiada dado un string base.

>>> wikipedia.suggest("Barak Obama")
 # u'Barack Obama'
  • wikipedia.page(title, auto_suggest=True, redirect=True, preload=False) : Obtiene un objeto que permite navegar una página completa de wikipedia. Es el método que entrega más información de la API y permite consultar cada elemento de la página de manera más individual.

El API cuenta con una amplia documentación, por lo mismo si estás familiarizado con python, el uso de ella no debería ser complicado. Te sugerimos visualizar primero QuickStart de la API para tener un entendimiento global de cómo funciona este wrapper, para luego profundizar en los mares de páginas de la documentación oficial.

Implementación en la placa Raspberry Pi: Wrapper de la API

El menú mencionado en la sección de la placa MSP430 permite seleccionar una palabra clave que se envía hacia la RaspberryPi, la que genera consulta relacionada a la palabra provista y devuelve el resultado para ser visualizado en el LCD del primer dispositivo. Para lograr esta forma de comunicación se implementó en la Raspberry Pi dos clases, denominadas wikiWrapper y serialConnection, que implementan un patrón singleton.

La clase serialConnection, se preocupa de ser la interfaz de comunicación con la placa msp430 y se conecta a través del protocolo UART. La configuración de esto se logra de la siguiente forma:

serial.Serial('/dev/ttyAMA0', baudrate = 9600, bytesize = serial.EIGHTBITS,parity = serial.PARITY_NONE,stopbits = serial.STOPBITS_ONE, timeout = 0.2)

Donde serial corresponde al package python que actúa como interfaz de comunicación.(Para mayor información, ver documentación  oficial del package pyserial). La implementación completa de serialConnection es la siguiente:

@Singleton
class serialConnection:
    connection = None

    def get_connection(self):
        if self.connection is None:
            self.connection =  serial.Serial('/dev/ttyAMA0', baudrate = 9600, bytesize = serial.EIGHTBITS,parity = serial.PARITY_NONE,stopbits = serial.STOPBITS_ONE, timeout = 0.2)
            if not self.connection.isOpen:
                self.connection.open()

        if not self.connection.isOpen:
            self.connection.open()

        return self.connection

    def close_connection(self):

        if self.connection.isOpen:
            self.connection.close()

Donde @Singleton es un meta-tag que permite a serialConnection utilizar el patrón mencionado anteriormente. Este patrón permite mantener una comunicación única y que se abre sólo la primera vez. Así se logra transparencia en la comunicación y no existe inferencia por abrir una conexión cada vez que se requiera.

Por otro lado, la clase wikiWrapper cuenta con lo siguiente métodos para su funcionamiento:

  • wikiWrapper.fillQueryInteractiveMode() : Incorpora 30 queries válidas en el atributo queries_interactive de la clase wikiWrapper. El objetivo de esto es tener una base de queries válidas al inicio del programa.

  • wikiWrapper.getSummary(query=”holi”) : Permite la comunicación con API wikipedia para obtener el resumen dada una query válida. La query por defecto es “holi”.

  • wikiWrapper.interactive() : Método que se mantiene escuchando a la placa msp430 y retorna el resultado obtenido desde Wikipedia a la misma.

WikiWrapper funciona del siguiente módo:

  1. Al momento de inicializarse completa un arreglo de consultas válidas para hacer al sitio por cada palabra en el menú descrito en el LCD. Esto permite tener un preprocesamiento de consultas.

    self.seeds_interactive = ["cat","penguin","dog","sorry","Wikipedia","Chile","Usa","England"]
  2. Luego se espera por input desde la placa msp430. Cuando se obtiene, se ocupa el método search mencionado anteriomente, el cual obtiene queries válidas y se incorparan al set ya acumulado. Este proceso cada vez que se obtiene una palabra obtiene un nuevo set de consultas para incorporarlo al ya establecido, así se aumenta la base de consultas y se puede obtener información más variada.

    while(True):
                    caracter = self.connection.get_connection().read()
                    if caracter == '-':
                        break
                    else:
                        option += caracter
    
                if option != "":
                    print "[Received] {opt}".format(opt=option)
                    query = self.getQueryInteractiveMode(option)
                    ...
  3. Una vez creadas las consultas, se escoge una al azar para obtener información de wikipedia.

                    ...
                    message = self.getSummary(query)
                    if not message is None:
                        message = message.encode('utf-8')
                        message = message[:127]
                        ...
  4. Para finalizar, se envía a la placa msp430 el string de información obtenido a través del método write que provee el api serial de python.

                        ...
                        print "[Sending] {m}\n".format(m=message)
                        self.connection.get_connection().write(str(message))

Como se mencionó anteriormente, se utilizaron los métodos search y sumary de la API de Wikipedia, debido a que suggest entregaba en la mayoría de palabras testeadas resultados None, lo que indica que, si bien el API resulta muy útil por su implementación, el algoritmo de este método no es tan preciso. Otro problema encontrado en el API fue en cuanto a los resultados debido a que algunas consultas son reconocidas como “ambiguas” para la misma , por lo que se hizo un proceso de filtrado para completar el atributo queries_interactives de la clase wikiWrapper. Para mayor detalle de esta información, puedes ver los tipos de excepciones que tiene el API Wikipedia.

El conjunto de consultas en queries_interactives permite escoger un resumen o summary válido en wikipedia, el cual es cortado a sólo 127 caracteres por el tamaño del LCD de la placa MSP430.

Conexión entre placas

Obviamente, nada de lo anterior funcionaría si las placas no tuvieran una forma física de comunicarse entre sí. Esto se logra conectando los pines UART de cada una de la siguiente forma:

  • Tierra de la Raspberry con tierra de la MSP430 (cable negro)
  • Transmisión de la Raspberry  con recepción de la MSP430 (cable amarillo)
  • Recepción de la Raspberry  con transmisión de la MSP430 (cable azul)

Conexión y pinout de la placa msp430:

labarqui_connection_msp430labarqui_pinout_msp 

Conexión y pinout de la placa Raspberry Pi:

labarqui_connection_raspberry raspberry-pi-rev2-gpio-pinout

Conclusiones finales y recomendaciones para grupos de los próximos semestres

La realización de todo el proyecto aquí descrito significa una cantidad de líneas de código importante, pero que logramos sacar relativamente rápido porque pudimos reutilizar mucho trabajo de las experiencias/tareas que tuvimos durante el semestre. Esto fue posible porque nos preocupamos de que los manejos de cada uno de los componentes del hardware de la placa MSP430 (pantalla, leds, botones, potienciómetro, UART) quedaran escritos “como cajas negras” (exponiendo métodos fáciles e intuitivos de invocar), lo suficientemente genérica para poder ser incorporadas rápidamente a un nuevo proyecto. De esta forma, con pocas líneas podíamos prender un led o escuchar un botón:

#include "buttons_led/led.h"
...
led_init();
...
led_state(1,ON);
#include "buttons_led/buttons.h"
...
buttons_init();
...
#define REALIZAR_ACCION BUTTON1_PRESSED
extern volatile int REALIZAR_ACCION;
...
if(REALIZAR_ACCION)
    realizar_accion();

Otra consideración importante, es la revisión de la documentación. Sin ir más lejos, el protocolo UART tiene sus “mañas” y es necesario conocerlas para lograr que los dos dispositivos se comuniquen. Por ejemplo, el formato en que se mandan los datos, cuántos datos se mandan a la vez y cómo saber si se terminó la comunicación (con tal de no interrumpirla y corromper la información) son cosas que se deben tener en mente y tener cuidado de incluir en la lógica del programa, que incluye desde el tipo de variables a usar (unsigned vs signed, cantidad de bits, etc), hasta el control del flujo del software (whiles de espera e ifs que hay que poner). La misma idea es válida para el resto de los componentes de hardware de la placa MSP430.

Siguiendo en esa línea, revisar el funcionamiento de la API también fue muy importante, pero no tanto como probarla “en terreno”. Esto porque la naturaleza de ambas documentaciones es distinta. En lo que se refiere a la placa, la información que existe es mucho más oficial y académica, ya que estamos hablando de un producto por el que se paga, pero no así con la mayoría de las APIs. En el caso particular de la API de Wikipedia, nos encontramos con que, si bien está muy completa a la hora de buscar cómo resolver errores y excepciones, no siempre es fácil anticiparse a ellos. Por esta razón, implementar y probar el código que utilizaba la API desde un comienzo fue uno de nuestros factores de éxito.

Leave a comment