GUI - USB-EC3883-III-2019/EC3883-G09 GitHub Wiki

Comunicación serial

La comunicación serial se realizó utilizando la librería Pyserial. Esta librería provee de una API que hace fácil el manejo de la data obtenida por puerto serial. Utiliza la clase "bytes" para representar y operar sobre data adquirida desde un puerto serial. Sobre esta clase es posible realiza operaciones bit a bit (bitwise) lo que permite realizar fácilmente el desentramado de la data recibida.

Desentramado

El desentramado se realiza utilizando la rutina "receiveData". Esta rutina adquiere la data a través de la instancia del puerto serial provista por la libreria Pyserial y lee el tamaño de una trama completa. Antes de empezar a desentramar, se verifica que la trama obtenida este alineada correctamente. Si esto es así, se procede a realizar el desentramado y asignación de los valores para la posición, sonar y lidar. Por el contrario, si no esta alineada, se procede a llamar la rutina "synchronize" para realizar el alineamiento.

def receiveData(dataSerial):
    
    ... 
    f = dataSerial.read(4) # lectura de la trama
    ...


    if f[0] & 0x80 == 0: #verificacion de aliniamiento 
        ...

    else:
        synchronize(dataSerial)

    return (pos,s,l)

La rutina "synchronize" se encarga de sincronizar la data adquiriendo un byte por vez hasta que obtiene un byte con el primer bit en 0. Una vez esto ocurre se realiza la lectura de 3 bytes adicionales, con el proposito de desechar la trama parcialmente leida, y se retorna el control de la comunicación a la rutina "receiveData".

def synchronize(dataSerial):
    """ Synchronize data received from serial port
    """
    while 1:
        f = dataSerial.read(1) # 1 byte por vez 
        if f[0] & 0x80 == 0: #verificacion
            dataSerial.read(3) # resto de la trama 
            break

Interfaz gráfica

Para realizar la interfaz gráfica se utilizó la libreria PyQt5 en conjunto con Matplotlib y Numpy para realizar la parte de los gráficos.

La librería utiliza una superclase QMainWindow que tiene el proposito de ser heredada a la clase que formará la estructura de la interfaz. La clase "Window" en este caso es una subclase de la clase QMainWindow y en cuyo constructor de clase ( el metodo __init__() de la clase) se definen todos los elementos que van a estar contenidos dentro de la interfaz. Entre estos elementos estan los botones, menus desplegables, e incluyendo la instancia del grafico que se va a mostrar.

class Window(QMainWindow): 
    def __init__(self):
        super().__init__()

        #Status bar 
        self.file_menu = QtWidgets.QMenu('&File', self)
        self.file_menu.addAction('&Quit', self.fileQuit,
                                 QtCore.Qt.CTRL + QtCore.Qt.Key_Q)
        self.file_menu.addAction('&Save As', self.fileSave,
                                 QtCore.Qt.CTRL + QtCore.Qt.Key_S)
        self.menuBar().addMenu(self.file_menu)
        
        #Help option
        self.help_menu = QtWidgets.QMenu('&Help', self)
        self.menuBar().addSeparator()
        self.menuBar().addMenu(self.help_menu)

        #create canvas object 
        self.canvas = MplCanvas(self)

        ....

Se muestra parcialmente el metodo __init__() de la clase Window() especificado dentro del archivo interface.py. En esta parte del código se agregan los menus de la parte superior de la interfaz que sirven para desplegar las opciones "File", "Help" y "Save As". Asi mismo, se muestra como se inicializa el objeto MplCanvas que se utiliza para poder graficar internamente haciendo uso de Matplotlib.

El objeto MplCanvas crea la instancia del gráfico utilizando la librería "matplotlib.backends.backend_qt5agg". A partir de esta se importa "FigureCanvasQTAgg", que es una superclase que sirve de "wraper" para la clase MplCanvas, y se importa con el alias "FigureCanvas". Dentro del constructor de la clase se realizan las preconfiguraciones necesarias para poder comenzar a graficar.

class MplCanvas(FigureCanvas):
    """ 
    
        Matplotlib Canvas object for creating plotting window
    """

    def __init__(self, parent=None, width=1, height=1, dpi=100):

        #Plot config
        self.fig = plt.figure()  
        FigureCanvas.__init__(self, self.fig)
        self.axes = self.fig.add_subplot(111, projection="polar")
        self.axes.set_thetamin(-30)
        self.axes.set_thetamax(210)
        #hide x's and y's labels 
        self.axes.set_xticklabels(np.linspace(0,360,10))
        self.axes.set_yticklabels([])

En esta figura se observa como se utiliza el constructor de la superclase para poder crear el gráfico dentro de la interfaz con FigureCanvas.__init(...). Luego, una vez creada la figura es posible modificarla como se muestra.

La lógica de la interfaz radica en el uso de los timers provistos por la clase QTimer. Y la cual cada cierto intervalo de tiempo refresca el gráfico que tiene MplCanvas. Este timer se utiliza dentro la función plot() de la clase Window. Esta función plot() llama a la función plot() dentro de la clase MplCanvas que adquiere los datos desde el puerto serial y grafica nuevamente los datos en el gráfico. A continuación se muestra la función plot() de la clase Window.

 def plot(self):
        
        """ 
            Sets timer and calls canvas plot internal function
        """
        
        if self.timer: #if timer is running stop and restart a new one
            self.timer.stop()
            self.timer.deleteLater()

        self.timer = QtCore.QTimer(self)
        self.timer.timeout.connect(self.canvas.plot(self.channels))
        self.timer.start(16) 

Para poder adquirir los datos e irlos actualizando cada vez que se adquieren se utilizaron diccionarios o dict() que asociaban la posición con la data de cada sensor. En la función update_figure() que es llamada a partir de la función plot() en la clase MplCanvas se define la actualización de estas variables de tipo dictionary.

    def update_figure(self, channels):
        self.axes.clear()
        
         #empty array to receive data from DEM0QE
        data = receiveData(self.dataSerial)
        self.sonar[data[0]] = data[1]
        self.lidar[data[0]] = data[2]

A partir de estos diccionarios es posible acceder, verificar y modificar de manera eficiente para la data recibida del puerto serial.

La siguiente imagen muestra la interfaz al momento de iniciar el programa en donde la escala radial varía de a 10cm. En ella se muestran cinco botones:

  • Start: comienza a mostrar los datos de la pantalla.
  • Stop: detiene el proceso de muestra de los datos.
  • LIDAR: muesta u oculta la data medida por el lidar en color rojo
  • SONAR: muesta u oculta la data medida por el sonar en color azul
  • SOLINDAR: muesta u oculta la data resultante de la fusión de los sensores en color amarillo

Algoritmo de fusión

El algoritmo de fusión implementado, consta de hacer la suma de los datos de ambos sensores, ponderada por sus varianzas, las cuales se obtuvieron durante el proceso de caracterización.

⚠️ **GitHub.com Fallback** ⚠️