Capítulo 6. Software
Contenido de este capítulo
Introducción
Este capítulo trata sobre la implementación del software, que es la aplicación
utilizada en el PC para controlar el osciloscopio. Incluye información sobre el
diseño, selección de las herramientas, métodos de programación empleados,
estructura del código e funcionamiento del software.
Selección de las herramientas
La primera decisión a la hora de desarrollar la aplicación para controlar el osciloscopio fue la selección de las herramientas a utilizar, en particular:
- el lenguaje de programación a usar
- el toolkit gráfico (librería para el manejo de ventanas y creación de la interfaz gráfica)
- las librerías a utilizar para controlar diversos aspectos de la aplicación
- el entorno de desarrollo a utilizar para crear la aplicación gráfica
Lenguaje de programación
Para tomar la decisión del lenguaje a utilizar para la elaboración del sofate del software gráfico que se instalará en el PC para el control del osciloscopio desarrollado se han tenido en cuenta los siguientes requisitos:
Los siguientes fueron los requisitos tenidos en cuenta:
- Debe tener interfaz gráfica
- Debe ser multi-plataforma (Windows, Linux, Mac OS X)
- Debe ser de rápido desarrollo
- Debe poder comunicarse con puertos virtuales de Windows (para hablar con el driver RS232-USB de CDC de Microchip) de forma sencilla
- Debe ser un lenguaje gratuito que no requiera de licencias para compilar o distribuir el código compilado
El lenguaje que seleccionas para programar la aplicación fue python, ya que es
un lenguaje robusto y extremadamente portable (multi-plataforma,
requisito 2)
puesto que existen interpretadores para todas las plataformas, dos de las
cuales (Linux y Mac OS X) ya lo traen incluido dentro del propio sistema
operativo. Además, es un lenguaje interpretado lo cual elimina los tediosos
ciclos de edición-compilación-enlazado-ejecución y permite un desarrollo rápido
y ágil (
requisito 3). Por eso es comúnmente utilizado en el prototipado de
aplicaciones, y lo hace ideal para nuestro caso.
Además python cuenta con una extensión llamada pySerial que permite el acceso transparente a puertos series (virtuales o reales) de Windows y, a la vez, a puertos series en linux, lo cual nos permite desentendernos del problema de la comunicación (
requisito 4).
Otras aplicaciones como Java, si bien son portables, exigen más tiempo de diseño e implementación (pues el lenguaje es más estricto) y requieren tener instalada una máquina virtual Java (JRE o JDK) en la máquina del cliente donde se vaya a correr la aplicación. Al hacerlo en python se puede evitar esa restricción compilando la aplicación para que corra nativamente en cada arquitectura y sistema operativo.
La plataforma .Net de Microsoft no es portable para sistemas operativos fuera de windows y además su licencia tiene algunas restricciones respecto al código generado con ella, por lo cual fue descartada inmediatamente.
Toolkit gráfico
En cuanto al toolkit gráfico a decisión no fue tan sencilla.
Las bindings más populares disponibles para python son:
- tkInter - para trabajar con el toolkit tk
- GTK+ - utilizando los bindings PyGTK
- qt - utilizando los bindings PyQt
- wxWidgets - utilizando los bindings wxPython
tkinter
Tk es el toolkit nativo de python para trabajar con interfaces gráficas, pero
la razón de esto es que era el mejorcito que existía en el momento que python
fue diseñado, y de eso hace mucho tiempo. Tk tiene dos grandes desventajas:
requiere mucho código para implementar cosas simples y ademas no es muy
presentable. Por estas razones, tkinter fue descartado inmediatamente.
GTK+
A través de los bindings PyGTK se puede utilizar el popular toolkit GTK+ de la Free Software Foundation. Sin embargo lo descartamos por las siguientes razones:
- GTK no es muy lindo estéticamente en Windows
- GTK no tiene implementación para Mac OS X
- GTK no tiene un buen entorno rápido de desarrollo (RAD) para el diseño de ventanas
qt
Qt es muy portable (Windows, Linux, Mac) y estéticamente muy prolijo. Sin embargo, lo descartamos puesto que su licencia es ligeramente restrictiva para aplicaciones Windows (no así para Linux/Mac) y además la versión disponible actualmente (8 Feb 2006) de PyQt (los bindings de qt para python) no soporta la última versión de qt (4.x) sino que solo soporta hasta la 3.x, y además no soporta python 2.4 (hasta 2.3).
wxWidgets
wxPython usa el toolkit wxWidgets que es bastante prolijo y muy portable (Windows, Linux y Mac). Además tiene las siguientes ventajas:
- cuenta un muy buen diseñador rápido de ventanas (llamado Boa Constructor) que es gratuito y libre
- tiene una excelente documentación
- tiene una gran popularidad entre los toolkit GUI para python, lo cual significa que tiene un gran soporte comunitario (foros, etc)
Por estas razones es que decidimos adoptar wxPython como toolkit gráfico para la aplicación del osciloscopio.
Librerías
Otras librerías a utilizar son las siguientes:
pySerial
La librería pySerial permite comunicarse con los puertos de la PC de forma transparente, sencilla y (lo que es más importante) independiente de la plataforma.
pyWin32
La librería pyWin32 son bindings para poder acceder a la API de win32 desde python. La razón por la cual la usamos es que el pySerial la necesita para acceder a los puertos serie en Windows.
NumPy
La librería NumPy provee funciones para calculo de transformada discreta de Fourier (FFT) la cual es usada para mostrar el espectro de la señal en el software.
Entorno de desarrollo
El entorno de desarrollo utilizado para crear la aplicación gráfica fue el Boa
Constructor. Entre sus características se encuentra el desarrollo rápido (drag
and drop) de ventanas y sus componentes (botones, barras de estado, etc), como
así también la posibilidad de depurar el programa gráfico utilizando
breakpoints y las herramientas típicas de depuración.
Un dato curioso es que, además de ser un entorno de desarrollo para wxPython, el Boa Constructor también está escrito en python utilizando wxPython.
Compilador
Para la plataforma Windows en particular se utilizó el compilador py2exe para
compilar la aplicación a un archivo ejecutable (EXE) que no dependiera de
ninguna otra librería externa brindándole así al osciloscopio una gran
portabilidad y facilitando su distribución.
Para los sistemas operativos Linux y Mac esto no es necesario puesto que dichos
sistemas operativos ya vienen con python integrado y puede correr
cualquier programa escrito en ese lenguaje, sin necesidad de compilarlo
previamente. Esto incluso es una gran ventaja puesto que, al no tener que ser
compilado, la aplicación es independiente de la arquitectura. De esta forma,
tenemos una aplicación que es, a la vez, multi-plataforma e independiente de la
arquitectura. Todo gracias a python.
Resumen
Este es un resumen del lenguaje, librerías y herramientas utilizadas para desarrollar el software:
| Lenguaje | python |
| Librerías | pySerial NumPy pyWin32 (solo windows) |
| Entorno de desarrollo | Boa Constructor |
| Compilador | py2exe (solo Windows) |
Tabla 6.1 Herramientas de desarrollo del software
Estructura del software
Clases
Dado que python es un lenguaje preferentemente orientado a objetos, el software
se compone de varias clases, las cuales a su vez contienen métodos que son las
funciones encargadas de implementar las diferentes funcionalidades. A
continuación se presenta un resumen de las mismas.
Las clases que componen el software son las siguientes:
Osc
La clase Osc es la encargada de la comunicación con el osciloscopio. Implementa
el protocolo de comunicación (como se encuentra especificado en el capítulo 7)
proveyendo primitivas para el envío y recepción de comandos al osciloscopio.
El constructor de la clase acepta como parámetro el puerto serie donde se
encuentra el osciloscopio.
Las funciones de esta clase son:
- Clase Osc
-
connect() intenta conectarse al osciloscopio. Devuelve TRUE en caso de lograrlo, o FALSE en caso contrario.
-
getVersion() devuelve la versión de firmware del osciloscopio
-
getPort() devuelve el puerto por el cual se encuentra conectado el osciloscopio
-
getData() devuelve los datos obtenidos por el último comando de datos (ej. AQHI, AQME, AQLO)
-
getSample() se bloquea hasta recibir un byte del osciloscopio y, una vez recibido, lo devuelve. Utilizado para capturas de baja velocidad en tiempo real
-
stop() envía un comando STOP al osciloscopio y limpia el buffer de entrada
-
acquire(type, chan, vdv1, vdv2, dual, chop) solicita una captura de datos al osciloscopio. Una vez finalizada la misma retorna TRUE (si la captura fue exitosa) o FALSE si hubo algún error. Para obtener los datos capturas usar la función getData(). Los parámetros que acepta esta función son los siguientes:
-
type - el tipo de captura a realizar (AQHI, AQME, AQLO)
-
chan - el canal a utilizar (1 ó 2)
-
vdv1 - el divisor vertical a usar en el canal 1 (0, 1, 2 ó 3)
-
vdv2 - el divisor vertical a usar en el canal 2 (0, 1, 2 ó 3)
-
dual - habilita (1) o deshabilita (0) el modo dual
-
chop - habilita (1) o deshabilita (0) el modo chop
-
debug() escribe en un archivo el texto pasado como parámetro. Utilizado para dejar un registro de la comunicación con el osciloscopio con fines depurativos.
-
send(cmd, param, retry) envía un comando al osciloscopio. Los parámetros que acepta son:
-
cmd - el comando a enviar (string de 4 caracteres, obligatorio)
-
param - el parámetro del comando (número entero, opcional)
-
retry - indica si debe reintentar enviar el comando en caso de error (por defecto es TRUE).
-
disconnect() se desconecta del osciloscopio
Frame1
La clase Frame1 es la encargada de desplegar la ventana del osciloscopio en
pantallas, así como también de llevar a cabo todas las funciones de interacción
con el usuario.
Las funciones de esta clase son las siguientes:
-
cleantrigger() aplica el trigger por software descartando las muestras irrelevantes del principio
-
triggerlevelup() rutina que implementa el algoritmo de trigger por software. Es utilizada por la función cleantrigger.
-
updatevalues() actualiza los parámetros numéricos de la señal (voltaje medio, voltaje pico-pico y voltaje eficaz)
-
updatefft() actualiza los análisis espectrales de las señales
-
updatedivs() actualiza los valores seleccionables de divisiones de voltaje para la vista actual (en modo dual separado dichos valores se duplican)
-
updatetriggers() actualiza los valores seleccionable de los triggers de voltaje
-
updatezeros() actualiza el nivel de los ceros en el display del osciloscopio. Dicho nivel depende del modo dual y de si se está utilizando etapa de entrada
-
savetofile() guarda las muestras recibidas en el archivo especificado en la interfaz gráfica
-
acqstart() inicia el proceso de captura de datos. Es ejecutada al activar el botón de captura.
-
acqstop() detiene el proceso de captura. Es ejecutada al desactivar el botón de captura.
-
connectosc() intenta conectarse al osciloscopio barriendo todos los puertos disponibles en la PC. Devuelve TRUE si pudo conectarse, o FALSE en caso contrario.
-
doconnect() se conecta al osciloscopio utilizando la función connectosc() y actualiza los controles de la aplicación según la conexión allá sido exitosa o no.
Además de las funciones ya mencionadas esta clase tiene otras rutinas especiales que son disparadas por eventos de la aplicación, desde la pulsación de un botón hasta la llegada de los datos del osciloscopio. En particular, las más importantes son las siguiente:
-
OnOscDisplayPaint() es disparada cuando el display necesita actualizarse, ya sea porque la ventana se movió o porque llegaron nuevas muestras del osciloscopio
-
OnAqcuireData() es disparada cuando llegan nuevos datos del osciloscopio. Se encarga de actualizar todos los displays y valores a través de las funciones update*
AcquireThread
La clase AcquireThread es la encargada de llevar a cabo la captura de datos. Corre en forma paralela a la clase Frame1 y se comunica con ésta a enviándole mensaje, los cuales también son clases (AcquireEvent).
Esta clase tiene una única función (
run()) en donde se realiza la comunicación con el osciloscopio (a través de la clase Osc) y se ejecuta toda la lógica necesaria para procesara una nueva secuencia de muestras del osciloscopio.
AcquireEvent
La clase AcquireEvent es el tipo de objeto utilizado para el transporte de las muestras del osciloscopio entre la clase AcquireThread y la clase Frame1. Por consiguiente, carece de funciones.
Archivos
La estructura del código fuente es bastante simple y consta de los siguientes archivos:
-
oscapp.py - este es el código que dispara toda la aplicación. Es el que llama a la ventana (ver clase Frame1) para controlar el programa
-
oscframe.py - aquí está el código que define el comportamiento, la apariencia y el funcionamiento de la ventana de la aplicación. En este archivo se encuentran implementadas las clases: Frame1, AcquireThread y AcquireEvent
-
osc.py - aquí se encuentra el código de comunicación con el osciloscopio. En este archivo está implementada la clase Osc
-
setup.py - este archivo no es código del progama, sino que son las directivas utilizadas para compilar el programa en Windows utilizando el compilador py2exe
Funcionamiento del software
Una programa gráfico en wxPython consta de una
Aplicación y varios
Frames,
que pertenecen a ella. Estos frames son justamente las diferentes ventanas de
la aplicación. Como en nuestro caso el software tiene una sola ventana, el
mismo tiene un solo
Frame.
Al disparar la aplicación (
oscusb.py), ésta abre el frame por defecto
(
oscframe.py) que es la ventana que se ve cuando se ejecuta el programa.
El comunicación con el osciloscopio se realiza a través del driver que se
encuentra implementado en el archivo
oscctrl.py como una clase de python y es
utilizado desde oscframe.py para enviar comando y recibir datos.
Control de la interfaz gráfica
La creación de la ventana se realiza (al igual que en cualquier aplicación
gráfica) creando un cuadro (o
Frame) el cual controla el funcionamiento de la
ventana de la aplicación gráfica. A dicho cuadro se le asignan controles
(botones, etiquetas, cuadro para ingresar texto, etc) en coordenadas
específicas. Las coordenadas pueden darse en pixels o en proporciones, lo cual
permite que la ventana puede mantener su aspecto al ser maximizada o cambiada
de tamaño.
A los diferentes controles (por ejemplo, botones) se les define un
comportamiento a través de eventos que son disparados cuando se realiza una
acción sobre ellos (por ejemplo, pulsar un botón).
Al ser disparados, dichos eventos llaman a una función especificada predefinida
al crear el Frame.
Asimismo, otros controles son de salida (por ejemplo, el display del
osciloscopio) los cuales pueden ser modificados arbitrariamente desde el código
aplicación a través de métodos que éstos proveen (por ejemplo, dibujar una
línea, cambiar el color de fondo, etc).
Conexión con el osciloscopio
La comunicación con el osciloscopio se realiza a través de la clase provista
por el driver (
oscctrl.py) al dispararse ciertos eventos.
Actualmente la aplicación sigue el siguiente mecanismo para conectarse con el osciloscopio:
- Intenta conectarse al primer puerto serie disponible en el PC (COM1 en caso de Windows, /dev/ttyACM0 en caso de linux)
- Si logra conectarse envía un comando VERS (ver Capítulo 7 - protocolo de comunicación), de lo contrario prueba con el siguiente puerto disponible que encuentra
- Si obtiene respuesta al comando VERS, entonces considera que la comunicación con osciloscopio se ha realizado exitosamente y despliega la versión de firmware del mismo (la cual fue obtenida en la respuesta del comando VERS).
Este este mecanismo de barrido fue por un razón de comodidad ya que, debido a
que osciloscopio genera dinámicamente un puerto serie virtual cada vez que se
conecta, éste no siempre era el mismo, aún cuando se conectase el osciloscopio
en el mismo puerto USB. Por lo tanto, el barrido nos ahorró el tiempo de estar
buscando y configurando el puerto virtual del osciloscopio.
Sin embargo, a pesar de contar con dichas ventajas, reconocemos que el
mecanismo de barrido debe ser sustituido en el producto final puesto que, al
enviar información a todos los puertos, puede interferir con el funcionamiento
de algún dispositivo conectado al PC por puerto serie.
Para este problema existen dos soluciones, una trivial y una prolija:
- la solución es trivial consiste simplemente en colocar (en la aplicación) un selector del puerto serie a utilizar para el osciloscopio.
- la solución prolija consiste en desarrollar un driver USB personalizado para el osciloscopio que no involucre puertos serie virtuales de por medio.
El único otro momento donde el programa se comunica con el osciloscopio es al
pulsar el botón
Capturar en el cual envía un comando de captura, precedido
por los comandos de configuración de los parámetros de captura (HDIV, DUAL,
etc).
Desplegado de muestras en pantalla
La graficación de las muestras recibidas del osciloscopio es realizada a través
del objeto
wxDC de la librería wxWidgets el cual (a diferencia de usar el API
de win32, por ejemplo) lo hace independiente de la plataforma.
A continuación se muestra un esbozo reducido de la rutina de graficación:
size = dc.GetSize()
dc.SetPen(wx.GREEN_PEN)
top = min(len(data), size.x)
lastx, lasty = 0, 0
vdiv = vdivs[self.vdivch.GetSelection()]
for i in range(0, top):
val = (data[i] - 128) / 255 / vdiv
x = i
y = int(size.y/2*val)
if x > 0:
dc.DrawLine(lastx, lasty, x, y)
lastx = x
lasty = y
dc.SetPen(wx.NullPen)
Se puede observar que se utilizan las funciones
SetPen y
DrawLine del
objeto wxWC que permiten seleccionan el color del trazo y dibujar una línea
entre dos puntos, respectivamente.
Comunicación con el osciloscopio
En particular, resulta de especial importancia comentar sobre el uso de hilos
para la comunicación con el osciloscopio, ya que de lo contrario la aplicación
funcionaría muy lenta y poco responsiva. Esto es porque, al activar la captura
el programa está continuamente enviando comandos de captura al osciloscopio y
recibiendo sus datos. Entonces, si esto se hace en primer plano (que es la
forma trivial de hacerla) el programa queda colgado esperando los datos del
osciloscopio hasta que estos llegan, para finalmente los graficarlos. El
problema es que, como el tiempo de transferir los datos del osciloscopio al PC
es mucho mayor comparado con el resto de los tiempos, el programa está
continuamente esperando datos del osciloscopio y mientras esto sucede la
ventana no responde lo cual resulta en una efecto extremadamente molesto para
la usuario y deja la aplicación inusable.
Para solucionar este problema se utilizó la tecnología de hilos en el cual, al
presionar el botón de captura la aplicación dispara una especie de sub-programa
(llamado hilo) que corre paralelamente a la aplicación y se encarga de
comunicarse con el osciloscopio para adquirir los datos y, una vez que los
obtiene, notifica del suceso al programa principal a través de un evento, con
el cual también le transfiere los datos. La aplicación principal, al recibir
el evento con los datos, lo único que tiene que hacer es extraer los datos y
graficarlos, lo cual es un proceso muy rápido y por lo tanto la ventana no
queda congelada.
Como ya se mencionó anteriormente, python es un lenguaje fuertemente orientado
a objetos. Por lo cual, en el escenario de captura antes descripto, cada rol es
cumplido por una clase. Ellas son:
- el funcionamiento del programa principal está a cargo de la clase
Frame1
- el sub-programa que se corre en un hilo corre es la clase
AcquireThread
- el evento que genera el hilo (
AquireThread) y lo envía al programa principal (Frame1) es la clase AcquireEvent
- finalmente, la clase encargada de comunicarse con el osciloscopio es
Osc
En el siguiente diagrama se muestra la interacción que ocurre entras las
diferentes clases del software, para llevar a cabo la adquisición de datos del
osciloscopio.

Fig 6.1 Interacción entre las clases del software
Los números rojos indican (de forma ordenada) todos los pasos ejecutados por
cada una de las clases, para obtener una secuencia de muestras del osciloscopio
y desplegarlas en pantalla.
Esto ocurre cuando el usuario activa el botón Capturar de la interfaz y
continúa corriendo ininterrumpidamente hasta que el botón es desactivado. Por
lo tanto, mientras el botón Capturar esté activado, los pasos del 1 al 8 se
ejecutan cíclicamente.
Como puede verse también en el diagrama, la clase
Frame1 (que es la controla
el funcionamiento de la interfaz) sigue interactuando con el usuario
independientemente del resto de las clases. Solo interrumpe brevemente el
control de la interfaz cuando llega el evento
AquireThread, del cual extrae
los datos y los grafica en la pantalla. Si el botón Capturar sigue activado,
inmediatamente dispara un nuevo hilo de captura (
AquireThread) y regresa a
su tarea de controlar la interfaz. Como el proceso de atender el evento y
desplegar los datos ocurre muy rápido, el usuario no se percata de la demora y
obtiene la impresión de que la interfaz nunca se cuelga, que es justamente el
objetivo de usar los hilos.
Hilo de captura
Todo el funcionamiento mencionado en la sección anterior se realiza utilizando
la clase
Thread de python y extendiéndola. Esto es lo que hace la clase
AcquireThread, que hereda de la clase
Thread.
El código de la clase
AcquireThread es muy simple y se muestra continuación:
class AcquireThread(Thread):
def __init__(self, notify_window):
Thread.__init__(self)
self._notify_window = notify_window
def run(self):
win = self._notify_window
osc = win.osc
hd = win.hdivch.GetSelection()
size = 512
if win.osc.acquire(count=size):
chardata = win.osc.getData()
data = []
for c in chardata:
data.append(ord(c))
wx.PostEvent(self._notify_window, AcquireEvent(data))
else:
wx.PostEvent(self._notify_window, AcquireEvent(None))
De particular interés son las lineas
wx.PostEvent donde se dispara el evento
una vez terminada la captura de datos. En caso de haber algún error (segunda
línea de
wx.PostEvent) el hilo envía None en lugar de los datos lo cual
notifica al programa principal que hubo un error al capturar los datos.
Para la notificación y comunicación de datos entre el hilo de captura y la
aplicación se utiliza la clase
wx.pyEvent de wxPython, extendiéndola para que
permita enviar los datos dentro de si misma. El código de dicha clase se
presenta a continuación:
class AcquireEvent(wx.PyEvent):
def __init__(self, data):
wx.PyEvent.__init__(self)
self.SetEventType(EVT_RESULT_ID)
self.data = data
Notar que aquí la única extensión que se le hizo a la clase base (
wx.PyEvent)
fue la de agregarle un campo de datos (
data) el cual es usado para
transportar los datos del osciloscopio.
Manual de uso
El manual de uso del software se encuentra en el capítulo 9.
Código fuente
El código fuente del software se encuentra disponible para bajar en la siguiente página:
http://pablohoffman.com/oscusb/software/
Referencias
- Lenguaje python
- Librería pySerial
- Librería NumPy
- Librería PyQt
- Librería PyGTK
- Toolkit qt
- Toolkit GTK+
- GNU
- Boa Constructor (entorno de desarrollo)
- Librería wxPython
- Librería wxWidgets
- Librería pywin32
- Compilador py2exe
- Instrucciones para empaquetado multiplataforma