Interfaz Gráfica en Python «Gráfica», Pic C Compiler

Interfaz gráfica de usuario (GUI), programada en Python y VS Code para establecer comunicación USB mediante la computadora y el microcontrolador PIC, el objetivo es recibir el valor de los sensores conectados al microcontrolador para mostrar la gráfica de las señales en la interfaz gráfica.

En el siguiente link encontraras lo necesario para instalar los programas y librerías que se instalaron anteriormente.

Interfaz Gráfica 

Procedimiento

Instalar la librería «matplotlib».


pip install matplotlib

Inicialmente se importan las librerías y módulos necesarios.


import tkinter as tk
from tkinter import ttk
import serial
from serial.tools import list_ports
import threading

Se crea la ventana principal de la interfaz gráfica, se especifica el nombre de la ventana, e tamaño y el color de fondo.


# Crea la ventana de la interfaz gráfica
ventana = tk.Tk(className=" Gráfica")
ventana.geometry("1000x500")
ventana.config(bg="#405158")

Colocamos una etiqueta como titulo de la ventana.


# Etiqueta para mostrar el titulo del proyecto
etiqueta = tk.Label(ventana, text="Graficar señales", font=("Arial", 14), bg="#5C6B72", fg="white")
etiqueta.pack(side=tk.TOP, fill="both")

Se coloca un «ComboBox» para mostrar los puertos COM conectados a la computadora.


# Crear ComboBox para mostrar los puertos COM disponibles
combo_puertos = ttk.Combobox(ventana, state='readonly')
combo_puertos.place(x=5, y=35)

Se crea un botón para actualizar los puertos COM conectados, ejecutando la función «mostrar_puertos_com».


# Botón para actualizar la lista de puertos COM disponibles
btn_actualizar = tk.Button(ventana, text="Actualizar", command=mostrar_puertos_com)
btn_actualizar.place(x=235, y=30)

Se crea un botón para conectarse o desconectarse del puerto COM seleccionado en el «ComboBox», ejecutando la función  «conectar_o_desconectar».


# Botón para conectarse al puerto COM seleccionado
btn_conectar = tk.Button(ventana, text="Conectar", command=conectar_o_desconectar)
btn_conectar.place(x=155, y=30)

La función «obtener_puerto_com» obtiene la lista de los puertos COM conectados a la computadora y los muestra el en «ComboBox».


def mostrar_puertos_com():
    combo_puertos['values'] = obtener_puertos_com()
    combo_puertos.set("")

La función «conectar_puerto» se conecta al puerto COM seleccionado en el «ComboBox», y retorna la comunicación serial, si no se establece la conexión, retorna «None».


def conectar_puerto(puerto):
    try:
        conectar = serial.Serial(puerto, baudrate=9600, stopbits=1, parity='N', bytesize=8)
        return conectar
    except serial.SerialException as e:
        return None

La función «desconectar_puerto» se desconecta de la conexión establecida con el pureto COM seleccionado.


def desconectar_puerto(conectar):
    if conectar:
        conectar.close()

La función «recibir_datos» se ejecuta en segundo plano para recibir constantemente los datos enviados por el microcontroalador y cuando se reciben los datos se envían a la función «procesar_datos».


def recibir_datos():
    global conectar
    while conectar:
        try:
            datos_recibidos = conectar.readline().decode("utf-8")
            procesar_datos(datos_recibidos)
        except serial.SerialException:
            conectar = None
            btn_conectar.config(text="Conectar")
            break

La función «conectar_o_desconectar», simplemente se conecta o desconecta del puerto COM seleccionado ejecutando las diferentes funciones. Si no existe comunicación serial con un puerto «COM», la computadora se conectara al puerto COM seleccionado y el texto del botón «btn_conectar» se cambiara a «desconectar» y se utiliza un una función «threading.Thread» para  ejecutar la función «recibir_datos» en segundo plano, en caso contrario, si existe una comunicación serial con un puerto COM, la computadora se desconectara del puerto COM y el texto del botón «btn_conectar» se cambiara a «conectar».


def conectar_o_desconectar():
    global conectar
    if conectar is None:
        puerto_seleccionado = combo_puertos.get()
        conectar = conectar_puerto(puerto_seleccionado)
        if conectar:
            btn_conectar.config(text="Desconectar")
            recibir_datos_thread = threading.Thread(target=recibir_datos)
            recibir_datos_thread.start()
    else:
        desconectar = conectar
        conectar = None
        desconectar_puerto(desconectar)
        btn_conectar.config(text="Conectar")

Una vez colocado el código correspondiente a la comunicación serial entre la computadora y el microcontrolador, se realiza el código, en este caso para recibir los datos del microcontrolador y mostrar los valores en una gráfica.

En la función «procesar_datos»  se identifica el comando y el valor correspondiente separando la cadena de caracteres recibida en «id_sen» y «numero», si se recibe sel identificador «SEN01» el valor correspondiente se guarda en «num_sen01» y si se recibe el identificador «SEN02» el valor correspondiente se guarda en «num_sen02», lo siguiente es enviar el valor al método «graficar» de la clase «Grafica_sensores» mediante la instancia «grafica». 


def procesar_datos(datos_recibidos):
    global num_sen01, num_sen02
    try:
        id_sen = datos_recibidos[0:5] # Obtiene los caracteres del 0 al 4
        numero = int(datos_recibidos[5:9]) # Obtiene los caracteres del 5 al 8

        if id_sen == "SEN01":
            num_sen01 = numero        
        if id_sen == "SEN02":
            num_sen02 = numero      
        else:
            return
        
        grafica.graficar(num_sen01, num_sen02)
    except: 
        pass

La clase «Grafica_sensores» esta diseñada para crear y mostrar la gráfica.


class Grafica_sensores:
def __init__(self, ventana):

def param_grafica(self):

def graficar(self, y_num, y_num2): 

def limpiar_grafica(self):

En el constructor de la clase «__init__» toma el argumento «ventana» que representa la ventana donde se mostrara la gráfica y se almacena en el atributo «self.ventana».

Para diseñar la gráfica se crea la figura «self.fig» y la instancia «self.ax» utilizando «plt.subplots» donde se definen las propiedades de la gráfica como la resolución (dpi), el tamaño (figzise), y el color de fondo de (facecolor) y los parámetros de la gráfica se definen en el método «param_grafica».

Se crea el lienzo «self.canvas» utilizando «FigureCanvasTkAgg» para mostrar la gráfica en la ventana colocando la figura «self.fig»  y la ubicación donde se mostrara en lienzo con la figura «master=self.ventana» que en este caso es en la ventana principal.

Se crea un botón que ejecuta el método «limpiar_gráfica».

Se crean las listas que contendrán puntos a graficar «self_puntos».

La variable «self.num_puntos» establece el limite de puntos que se mostraran en gráfica.

Se crea una variable «self.x_num» que se utilizara como el incremento del eje x de la gráfica.


    def __init__(self, ventana):
        # Configuracion de la ventana
        self.ventana = ventana

        # Diseño de la gráfica, resolución (dpi), tamaño (figzise) y color de fondo (facecolor) de la figura
        self.fig, self.ax = plt.subplots(dpi=80, figsize=(7, 5), facecolor="#8F9192")
        self.param_grafica() 

        # Crear el lienzo y inserta el diseño de la gráfica
        self.canvas = FigureCanvasTkAgg(self.fig, master=self.ventana)
        self.canvas.get_tk_widget().place(x=350, y=30)

        # Botón para limpiar la gráfica
        self.btn_limpiar_grafica = tk.Button(ventana, text="Limpiar gráfica", command=self.limpiar_grafica)
        self.btn_limpiar_grafica.place(x=350, y=440)

        # Lista para almacenar los valores de x_num e y_num
        self.puntos = []
        self.puntos2 = []

        # Establece el numero de puntos en la gráfica
        self.num_puntos = 100

        # Contador para el eje x
        self.x_num = 0

En la función «param_grafica» se definen los parámetros de la gráfica, titulo (set_title) , etiquetas (set_xlabel) y (set_ylabel), color (facecolor), escala y lineas externas (tick_params), lineas internas (grid) y si se desea se puede colocar el limite de la escala con (set_ylim) o (set_yticks). Los parametros se asignan a la instancia «self.ax».


    def param_grafica(self):
        self.ax.set_title("Gráfica Sensores", fontdict={"family": "Arial", "size": 14, "color": "white"}, pad=10)
        self.ax.set_xlabel("Eje x", fontdict={"family": "Arial", "size": 14, "color": "white"}, labelpad=1)
        self.ax.set_ylabel("Eje y", fontdict={"family": "Arial", "size": 14, "color": "white"}, labelpad=1)
        self.ax.set_facecolor("black")
        self.ax.tick_params(direction="out", length=5, width=0.5, color="white", labelcolor="white", labelsize=12)
        self.ax.grid(axis="both", color="white", linestyle="--", linewidth=0.5)
        #self.ax.set_ylim(0, 1025) # Escala en el eje y (min, max)
        #self.ax.set_yticks(range(0, 1025, 100)) # Escala en el eje y (min, max, intervalo)

El método «graficar» toma los valores «y_num» y «y_num2», para agregarlos al final de cada lista.
En la lista «self.puntos» se utiliza el utiliza el método «append», para agregar el valor de «y_num» y «x_num» para formar un punto.
En la lista «self.puntos2» se utiliza el utiliza el método «append», para agregar el valor de «y_num2» y «x_num» para formar un punto.

Se actualiza el valor de la variable «self.x_num» que se incrementa por cada valor recibido.

Para definir la cantidad de puntos a graficar se actualizan las listas «self.puntos» conservando la cantidad de puntos definida en la variable «self.num_puntos».

La clase zip itera todos los puntos y_num y x_num de la lista «self.puntos» para guardarlos en las variables xs e ys.

El método «self.ax.clear()» elimina todos los elementos de la instancia permitiendo actualizar los parámetros de la gráfica y mostrar los nuevos puntos. 

La función «self.ax.plot» recibe los puntos de xs e ys que se graficaran, también se definen los parámetros de las lineas que se trazan entre cada punto. 

El método  «self.canva.draw()» muestra la gráfica de los puntos.


    def graficar(self, y_num, y_num2):     
        # Agregar los puntos al final de la lista
        self.puntos.append((self.x_num, y_num))
        self.puntos2.append((self.x_num, y_num2))

        # Actualizar el contador x_num
        self.x_num += 1

        # Conserva los ultimos "num_puntos" puntos en la lista  
        self.puntos = self.puntos[-self.num_puntos:]
        self.puntos2 = self.puntos2[-self.num_puntos:]

        # Separa los puntos x e y de la lista en variables separadas
        xs, ys = zip(*self.puntos)
        xs2, ys2 = zip(*self.puntos2)

        # Limpia la gráfica
        self.ax.clear()  

        # Actualiza los parametros
        self.param_grafica()

        # Establece los puntos a mostrar y sus parametros
        self.ax.plot(xs, ys, marker=',', linestyle='-', color='yellow')
        self.ax.plot(xs2, ys2, marker=',', linestyle='-', color='red')
        
        # Muestra los puntos y parametros en la grafica
        self.canvas.draw()

El método «limpiar_grafica» borra los puntos de las listas y limpia la gráfica.


    def limpiar_grafica(self):
        # Borra los puntos de las listas y limpia los puntos de la gráfica
        self.x_num = 0
        self.data_points.clear()
        self.data_points2.clear()
        self.ax.clear()
        self.param_grafica()
        self.canvas.draw()
    

Se crea una instancia de la clase «Grafica_sensores» y se le pasa el argumento ventana.


grafica = Grafica_sensores(ventana) 

Código completo Python


import tkinter as tk
from tkinter import ttk
import serial
from serial.tools import list_ports
import threading
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg


conectar = None


def obtener_puertos_com():
    puertos_com = [port.device for port in list_ports.comports()]
    return puertos_com

    
def mostrar_puertos_com():
    combo_puertos['values'] = obtener_puertos_com()
    combo_puertos.set("")


def conectar_puerto(puerto):
    try:
        conectar = serial.Serial(puerto, baudrate=9600, stopbits=1, parity='N', bytesize=8)
        return conectar
    except serial.SerialException as e:
        return None


def desconectar_puerto(conectar):
    if conectar:
        conectar.close()


def recibir_datos():
    global conectar
    while conectar:
        try:
            datos_recibidos = conectar.readline().decode("utf-8")
            procesar_datos(datos_recibidos)
        except serial.SerialException:
            conectar = None
            btn_conectar.config(text="Conectar")
            break
    

def conectar_o_desconectar():
    global conectar
    if conectar is None:
        puerto_seleccionado = combo_puertos.get()
        conectar = conectar_puerto(puerto_seleccionado)
        if conectar:
            btn_conectar.config(text="Desconectar")
            recibir_datos_thread = threading.Thread(target=recibir_datos)
            recibir_datos_thread.start()
    else:
        desconectar = conectar
        conectar = None
        desconectar_puerto(desconectar)
        btn_conectar.config(text="Conectar")


# Crea la ventana de la interfaz gráfica
ventana = tk.Tk(className=" Gráfica")
ventana.geometry("1000x500")
ventana.config(bg="#405158")

# Etiqueta para mostrar el titulo del proyecto
etiqueta = tk.Label(ventana, text="Graficar señales", font=("Arial", 14), bg="#5C6B72", fg="white")
etiqueta.pack(side=tk.TOP, fill="both")

combo_puertos = ttk.Combobox(ventana, state='readonly')
combo_puertos.place(x=5, y=35)

# Botón para actualizar la lista de puertos COM disponibles
btn_actualizar = tk.Button(ventana, text="Actualizar", command=mostrar_puertos_com)
btn_actualizar.place(x=235, y=30)

# Botón para conectarse al puerto COM seleccionado
btn_conectar = tk.Button(ventana, text="Conectar", command=conectar_o_desconectar)
btn_conectar.place(x=155, y=30)

# Actualizar la lista de puertos COM disponibles al abrir la ventana
mostrar_puertos_com()

################################################################################################
num_sen01 = None
num_sen02 = None

def procesar_datos(datos_recibidos):
    global num_sen01, num_sen02
    try:
        id_sen = datos_recibidos[0:5] # Obtiene los caracteres del 0 al 4
        numero = int(datos_recibidos[5:9]) # Obtiene los caracteres del 5 al 8

        if id_sen == "SEN01":
            num_sen01 = numero        
        if id_sen == "SEN02":
            num_sen02 = numero      
        else:
            return
        
        grafica.graficar(num_sen01, num_sen02)
    except: 
        pass


class Grafica_sensores:
    def __init__(self, ventana):
        # Configuracion de la ventana
        self.ventana = ventana

        # Diseño de la gráfica, resolución (dpi), tamaño (figzise) y color de fondo (facecolor) de la figura
        self.fig, self.ax = plt.subplots(dpi=80, figsize=(7, 5), facecolor="#8F9192")
        self.param_grafica() 

        # Crear el lienzo y inserta el diseño de la gráfica
        self.canvas = FigureCanvasTkAgg(self.fig, master=self.ventana)
        self.canvas.get_tk_widget().place(x=350, y=30)

        # Botón para limpiar la gráfica
        self.btn_limpiar_grafica = tk.Button(ventana, text="Limpiar gráfica", command=self.limpiar_grafica)
        self.btn_limpiar_grafica.place(x=350, y=440)

        # Lista para almacenar los valores de x_num e y_num
        self.puntos = []
        self.puntos2 = []

        # Establece el numero de puntos en la gráfica
        self.num_puntos = 100

        # Contador para el eje x
        self.x_num = 0
    

    def param_grafica(self):
        self.ax.set_title("Gráfica Sensores", fontdict={"family": "Arial", "size": 14, "color": "white"}, pad=10)
        self.ax.set_xlabel("Eje x", fontdict={"family": "Arial", "size": 14, "color": "white"}, labelpad=1)
        self.ax.set_ylabel("Eje y", fontdict={"family": "Arial", "size": 14, "color": "white"}, labelpad=1)
        self.ax.set_facecolor("black")
        self.ax.tick_params(direction="out", length=5, width=0.5, color="white", labelcolor="white", labelsize=12)
        self.ax.grid(axis="both", color="white", linestyle="--", linewidth=0.5)
        #self.ax.set_ylim(0, 1025) # Escala en el eje y (min, max)
        #self.ax.set_yticks(range(0, 1025, 100)) # Escala en el eje y (min, max, intervalo)


    def graficar(self, y_num, y_num2):     
        # Agregar los puntos al final de la lista
        self.puntos.append((self.x_num, y_num))
        self.puntos2.append((self.x_num, y_num2))

        # Actualizar el contador x_num
        self.x_num += 1

        # Conserva los ultimos "num_puntos" puntos en la lista  
        self.puntos = self.puntos[-self.num_puntos:]
        self.puntos2 = self.puntos2[-self.num_puntos:]

        # Separa los puntos x e y de la lista en variables separadas
        xs, ys = zip(*self.puntos)
        xs2, ys2 = zip(*self.puntos2)

        # Limpia la gráfica
        self.ax.clear()  

        # Actualiza los parametros
        self.param_grafica()

        # Establece los puntos a mostrar y sus parametros
        self.ax.plot(xs, ys, marker=',', linestyle='-', color='yellow')
        self.ax.plot(xs2, ys2, marker=',', linestyle='-', color='red')
        
        # Muestra los puntos y parametros en la grafica
        self.canvas.draw()
        

    def limpiar_grafica(self):
        # Borra los puntos de las listas y limpia los puntos de la gráfica
        self.x_num = 0
        self.puntos.clear()
        self.puntos2.clear()
        self.ax.clear()
        self.param_grafica()
        self.canvas.draw()
    

grafica = Grafica_sensores(ventana) 

################################################################################################
def cerrar_grafica():
    if conectar:
        ventana.destroy()
    else:
        ventana.quit() 
    
ventana.protocol("WM_DELETE_WINDOW", cerrar_grafica)

ventana.mainloop()

Circuito de conexión USB

Procedimiento

Inicialmente en el archivo .h se define el oscilador del microcontrolador, y se habilita la velocidad de la comunicación USB.


#use delay(clock=48MHz,crystal=20MHz,USB_FULL)

Se establece la corriente de funcionamiento necesaria para que el microcontrolador se conecte mediante USB.


#define USB_CONFIG_BUS_POWER 500 //500ma corriente de entrada

Para la comunicación USB se utiliza la siguiente librería.


#include "usb_cdc.h"

Se inicializa la comunicación USB.


   usb_init();

Lo siguiente es enviar los datos a la computadora, en este caso se envía el valor de los pines analógicos.

Se habilita la conversión analógica digital.


setup_adc(ADC_CLOCK_INTERNAL); 

Para obtener el valor de los pines analógicos se habilita la lectura del canal analógico con «set_adc_channel()» y después se realiza la lectura del valor con «read_adc()», para finalmente guardarlo en la variable declarada.

Para enviar datos por el puerto USB, se utiliza la función «printf()» para generar una cadena de caracteres y utilizando la función «usb_cdc_putc» se enviara la cadena de caracteres por el puerto USB, colocando un identificador para el valor «SEN01», el valor «%04lu» y «\r\n» para establecer el final de los datos.


void enviar_sensor1()
{
   int16 sensor1;

   set_adc_channel (0);//Lectura Vout del potenciometro en PIN AN0
   delay_us(10);
   sensor1 = read_adc();// lectura del valor

   printf(usb_cdc_putc,"SEN01%04lu\r\n",sensor1);
}

Para enviar el valor del pin «AN1» se realiza el mismo procedimiento y en la cadena de caracteres se coloca un identificador para el valor «SEN02», el valor «%04lu» y «\r\n» para establecer el final de los datos.


void enviar_sensor2()
{
   int16 sensor2;

   set_adc_channel (1);//Lectura Vout del potenciometro en PIN AN1
   delay_us(10);
   sensor2 = read_adc();// lectura del valor

   printf(usb_cdc_putc,"SEN02%04lu\r\n",sensor2);
}

En la función «main» se coloca una sentencia «if» donde se ejecuta cada subrutina para enviar los datos de cada pin analógico cuando se asigne un puerto COM al microcontrolador.


      if(usb_enumerated()) //esta asignado a un puerto COM?
      {
         enviar_sensor1();
         delay_ms(50);
         enviar_sensor2();
         delay_ms(50);
      }

Código completo USB


#FUSES NOMCLR

#define USB_CONFIG_BUS_POWER 500 //500ma corriente de entrada

#include "usb_cdc.h"

void enviar_sensor1()
{
   int16 sensor1;

   set_adc_channel (0);//Lectura Vout del potenciometro en PIN AN0
   delay_us(10);
   sensor1 = read_adc();// lectura del valor

   printf(usb_cdc_putc,"SEN01%04lu\r\n",sensor1);
}

void enviar_sensor2()
{
   int16 sensor2;

   set_adc_channel (1);//Lectura Vout del potenciometro en PIN AN0
   delay_us(10);
   sensor2 = read_adc();// lectura del valor

   printf(usb_cdc_putc,"SEN02%04lu\r\n",sensor2);
}

void main()
{
   usb_init();

   setup_adc(ADC_CLOCK_INTERNAL);

   while(TRUE)
   {
      if(usb_enumerated()) //esta asignado a un puerto COM?
      {
         enviar_sensor1();
         delay_ms(50);
         enviar_sensor2();
         delay_ms(50);
      }
   }
}

Circuito de conexión RS232

Módulo convertidor de USB a RS232 FT232RL

Procedimiento

Inicialmente se establecen los parámetros de la comunicación RS232.

  • baud = Velocidad de transmisión.
  • stop = Bit de paro.
  • parity = Bit de paridad. 
  • xmit = Pin de transmisión.
  • rcv = Pin de recepción. 
  • bits = Datos de transmisión.
  • stream = identificación de transmisión.

#use rs232(baud=9600, stop=1, parity=N, xmit=PIN_C6, rcv=PIN_C7, bits=8)

Lo siguiente es enviar los datos a la computadora, en este caso se envía el valor de los pines analógicos.

Se habilita la conversión analógica digital.


setup_adc(ADC_CLOCK_INTERNAL); 

Para obtener el valor de los pines analógicos se habilita la lectura del canal analógico con «set_adc_channel()» y después se realiza la lectura del valor con «read_adc()», para finalmente guardarlo en la variable asignada.

Para enviar datos por el puerto RS232, se utiliza la función «printf()», colocando en la cadena de caracteres un identificador para el valor «SEN01», el valor «%04lu» y «\r\n» para establecer el final de los datos.


 void enviar_sensor1()
{
   int16 sensor1;

   set_adc_channel (0);//Lectura Vout del potenciometro en PIN AN0
   delay_us(10);
   sensor1 = read_adc();// lectura del valor

   printf("SEN01%04lu\r\n",sensor1);
}

Para enviar el valor del pin «AN1» se realiza el mismo procedimiento y en la cadena de caracteres se coloca un identificador para el valor «SEN02», el valor «%04lu» y «\r\n» para establecer el final de los datos.


void enviar_sensor2()
{
   int16 sensor2;

   set_adc_channel (1);//Lectura Vout del potenciometro en PIN AN1
   delay_us(10);
   sensor2 = read_adc();// lectura del valor

   printf("SEN02%04lu\r\n",sensor2);
}

En la función «main» se coloca una sentencia «if» donde se ejecutara cada subrutina para enviar los datos por el puerto RS232 a la computadora.


      enviar_sensor1();
      delay_ms(50);
      enviar_sensor2();
      delay_ms(50);

Código completo RS232


#FUSES NOMCLR

#use rs232(baud=9600, stop=1, parity=N, xmit=PIN_C6, rcv=PIN_C7, bits=8)

void enviar_sensor1()
{
   int16 sensor1;

   set_adc_channel (0);//Lectura Vout del potenciometro en PIN AN0
   delay_us(10);
   sensor1 = read_adc();// lectura del valor

   printf("SEN01%04lu\r\n",sensor1);
}

void enviar_sensor2()
{
   int16 sensor2;

   set_adc_channel (1);//Lectura Vout del potenciometro en PIN AN1
   delay_us(10);
   sensor2 = read_adc();// lectura del valor

   printf("SEN02%04lu\r\n",sensor2);
}

void main()
{  
   setup_adc(ADC_CLOCK_INTERNAL); 
   while(TRUE)
   {
      enviar_sensor1();
      delay_ms(50);
      enviar_sensor2();
      delay_ms(50);
   }
}
Scroll al inicio