# Sistema de Control de Temperatura El Sistema de Control de Temperatura es un diseño basado en la recopilación de datos de la temperatura de dos transistores a partir de sensores que permiten medir esta variable y utilizando Arduino UNO como microcontrolador. Con la ayuda de pasta térmica, la energía del transistor al calentarse es transferida por conducción y convección al sensor de temperatura. Además, este proyecto dispone de una interfaz desarrollada en Python para visualizar la información y monitorear el estado de los dispositivos. ## Funcionamiento Básico La información acerca de la temperatura registrada por el termistor es reunida por el microcontrolador para posteriormente ser enviada a través de comunicación serial a una computadora; mediante el código desarrollado y cargado al Arduino UNO, el monitor serial del software Arduino recibe estos datos, los cuales inmediatamente son expedidos al programa diseñado en Python para mostrar en una interfaz los resultados obtenidos en tiempo real tanto de manera numérica como gráfica, además de permitir controlar el encendido y apagado de los transistores. ## Programas Requeridos En primer lugar se tiene que disponer del software de *Arduino* para poder comunicarse con el microcontrolador, cargar el programa en el mismo y recibir la información en el monitor serial. Para la descarga e instalación se **recomienda** seguir los pasos especificados en los siguientes enlaces: ### Para Windows: * [Como instalar Arduino en Windows](https://arduino.cl/como-instalar-arduino-en-windows/). ### Para Ubuntu: * [Cómo instalar Arduino IDE en las últimas versiones de Ubuntu](https://ubunlog.com/arduino-ide-en-ubuntu/). De igual manera, se requiere del IDE de python para poder ejecutar el código de la interfaz que mostrará la información enviada a través de Arduino. Para la descarga e instalación se **recomienda** seguir los pasos especificados en los siguientes enlaces: ### Para Windows: * [Download the latest version for Windows](https://www.python.org/downloads/). * [Download Spyder for Windows](https://www.spyder-ide.org). ### Para Ubuntu: * [IDLE Python, un entorno integrado de desarrollo para el aprendizaje](https://ubunlog.com/idle-python-entorno-desarrollo-aprendizaje/). *Nota: es importante mencionar que los anteriores tutoriales de las páginas web recomendadas no son de nuestra autoría.* ## Cargar y ejecutar los Programas Primeramente se debe cargar el programa a la tarjeta de *Arduino UNO*, para ello hay que seguir las siguientes instrucciones: 1. **Conectar la tarjeta Arduino.** La placa de desarrollo debe de ser conectada a la computadora al puerto USB. El LED de encendio de la placa debería de iluminarse. 2. **Ejecutar el IDE de Arduino** Ir al escritorio o buscar la aplicación de Arduino. 3. **Abrir el archivo correspondiente** Para ello únicamente hay que ir al menú *Archivo > Abrir > Buscar el archivo lecturaArduino* 4. **Cargar el programa en la tarjeta** Es necesario indicar el dispositivo con el que se está trabajando, en *Herramientas > Placas > Seleccionar Arduino Uno* También el puerto en el que se encuentra conectada la placa, en *Herramientas > Puerto > Seleccionar el puerto adecuado* Ahora simplemente hay que dar click en *subir* A continuación, ya es posible ejecutar el código de Python para disponer de la interfaz para entablar la comunicación y comenzar a recibir los datos de Arduino. ### Instalación de librerías de Python Es altamente probable que, la primera vez que se ejecute el programa en el sistema operativo del usuario, se muestren ciertos errores debido a la ausencia de algunas librería que se importan en el programa necesarias para la ejecución del mismo y sus funciones. Por lo que, si este es el caso deben instalarse todos los módulos faltantes; antes es preciso verificar algunas cuestiones como: * **Versión de Python del sistema** Para conocer cuál es la versión de python que tenemos instalada solo hace falta escribir en la terminal la siguiente instrucción: > python --version Es probable que sea alguna de las versiones de python3 (no importa cual), si no es así es recomendable llevar a cabo esta actualización puesto que algunas librerías suelen no ser compatibles entre la versión 2 y 3 de Python. * **Orden pip instalada** Una vez realizado esto, hay que comprobar también si se encuentra instalado el paquete pip, este es un sistema de gestión de paquetes utilizado para instalar y administrar paquetes de software escritos en Python. Para ello escribimos en la terminal: > pip list Si el resultado es un error, advertencia o mensaje de que no se ha encontrado la orden especificada, hay que ejecutar en la terminal la instrucción y esperar a que finalice la instalación: > sudo apt install python3-pip Si ya se dispone de este paquete, ya se puede comenzar con la instalación de las librerías. Algunas de los posibles errores emitidos por Python al ejecutar el programa acerca de los módulos ausentes son: * **No module named 'matplotlib'** Para su instalación se ejecutar la siguiente línea en la terminal: > pip install matplotlib * **No module named 'serial'** Para su instalación se ejecutar la siguiente línea en la terminal: > pip install serial * **No module named 'Tkinter'** Para su instalación se ejecutar la siguiente línea en la terminal: > pip install tkinter * **cannot import name 'imageTk' from 'PIL'** Este error es un poco distinto y suele ser por problemas de compatibilidad con el paquete pillow, se puede solucionar ejecutando la siguiente instrucción en la terminal: > sudo apt-get install python3-pil python3-pil.imagetk *Notas: es posible que al usar otro IDE de Python no se requiera solucionar estos errores por que los módulos ya vienen instalados con el programa; también puede que, si ocurren estos errores, el procedimiento de instalación de los módulos sea diferente al trabajar en un entorno distinto; además, si es el caso de que falta algún otro módulo de librería solo hay que ejecutar la instalación del mismo con la instrucción "pip install"; información adicional sobre otro tipo de errores no mencionados aquí se puede encontrar en diversas páginas web* ## Código en Arduino A continuación, se presenta una breve explicación acerca del funcionamiento del programa diseñado en el IDE de Arduino: En primer lugar, se establecen los puertos a utilizar del microcontrolador, para ello se definen 2 constantes correspondientes a la entrada de datos analógicos (pines A0 Y A1) a partir de los sensores de temperatura y otras 2 para las salidas que encenderán a los transistores (pines 6 y 7); además de ello, se declaran las variables que guardan los datos de las lecturas recibidas y las que almacenan el valor convertido de la temperatura en grado centígrados, por último una variable que indica el estado proporcionado desde Python para encender o apagar los transistores: ``` #define in1 A0 #define in2 A1 #define out1 6 #define out2 7 float temp1, lectura1, temp2, lectura2; String estado; ``` Ahora, en la función *setup()* se indica el uso del monitor serial a una velocidad de comunicación de 9600 baudios, además de el modo de funcionamiento de los pines de entrada y salida: ``` Serial.begin(9600); pinMode(in1, INPUT); pinMode(in2, INPUT); pinMode(out1, OUTPUT); pinMode(out2, OUTPUT); ``` En el ciclo prrincipal *loop()*, primeramente se establecen las instrucciones para la lectura de datos. las variables de lectura almacenan un valor analógico que consta de valor entre 0 a 1023; posterior a ello se utiliza una ecuación donde la parte que corresponde a **(lecturax * 5000 / 1024)** convierte la medición realizada a voltaje entre 0 a 5000mV; ahora, el fabricante del sensor TMP36 indica que la salida de tensión será de 10 mV (mili voltios) por cada grado de temperatura, por lo que el resultado del voltaje se divide entre 10 para realizar la conversión a temperatura; se imprimen los resultados en el monitor serial para poder leerlos desde Python: ``` lectura1 = analogRead(in1); lectura2 = analogRead(in2); temp1 = (lectura1 * 5000 / 1024 / 10) - 33; temp2 = (lectura2 * 5000 / 1024 / 10) - 37; Serial.println(temp1); Serial.println(temp2); ``` Finalmente, se codifican las líneas para cambiar el estado del transistor dependiendo de la información recibida desde Python en el monitor serial, por lo que antes que nada se debe de verificar los datos disponibles en este, para luego realizar la lectura de la variable en este monitor y enviarla a la placa de Arduino para encender o apagar el transistor indicado: ``` if(Serial.available()>0){ estado = Serial.readString(); if(estado == "on1"){ digitalWrite(out1, HIGH); } else if(estado == "off1"){ digitalWrite(out1, LOW); } else if(estado == "on2"){ digitalWrite(out2, HIGH); } else if(estado == "off2"){ digitalWrite(out2, LOW); } } ``` Se establece también un retardo de 100 milisegundos para repetir el ciclo. ## Código en Python A continuación, se presenta una breve explicación acerca del funcionamiento del programa para la interfaz de usuario diseñada en Python: En primer lugar, se importan todas las librerías o módulos necesarios, previamente instalados, de no ser así ir a la sección de **Instalación de librerías de Python**. ``` from threading import Thread import collections import matplotlib.pyplot as plt import matplotlib.animation as animation import time import serial from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import tkinter as tk from matplotlib.lines import Line2Ds ``` Aquí se encuentran las funcionaes necesarias para: crear hilos con `threading`, hacer una colección o lista con los datos a graficar con `collections`, dibujar los datos en una gráfica animada con líneas 2D y visualizarla en la interfaz utilizando los módulos de la librería de `matplotlib`, establecer la comunicación serial con Arduino usando `serial`, el diseño de la interfaz se hace con el paquete `tkinter` y finalmente, se importa `time` para ocupar algunos retardos de tiempo. A continuación, el código dispone de una serie de funciones que son llamadas entre sí y se utilizan también en los distintos objetivos de la interfaz para mostrar la información recibida de Arduino y mantener la interacción con el usuario: Para comenzar, se declaran ciertas variables, aquellas cuyo valor es modificado dentro de alguna función se definen como globales, el resto se inicializan de manera normal. ``` global isRun global on1 global on2 global conected conected = False on1 = False on2 = False isReceiving = False isRun = False numData = 2 #Número de datos a recibir de Arduino datos = 0.0 #Variable de dato a leer desde Arduino dato = 0.0 #Variable del dato convertido a float serialPort = 'COM3' #Puerto al que está conectado el Arduino baudRate = 9600 #Baudios configurados en Arduino estado = "off" #Variable de estado que se enviará al serial ``` La variable `isRun` representa si la función de lectura y graficación de datos está corriendo, `on1` y `on2` son los estados de encendido o apagado en que se encuentran los transistores, mientras que `isReceiving` confirma que se han recibido datos correctamente. `numData` hace referencia a que se recibirán 2 datos desde Arduino, correspondientes al sensor 1 y 2, `datos` es la información leída desde Arduino en forma de string, `dato` es la variable dato leído convertido a flotante, `serialPort` es el puerto al que se encuentra conectado el micrcontrolador, `baudRate` es la velocidad de comunicación a la que está trabajando el monitor serial de Arduino, `estado` representa el dato que se envía a Arduino para encender o apagar alguno de los transistores. Ahora bien, la función `conectar_serial` se utliza para entablar la comunicación con el monitor serial de Arduino. ``` def conectar_serial(): global arduino global conected try: arduino = serial.Serial(serialPort, baudRate) arduino.timeout = 0.2 time.sleep(0.5) print("CONECTADO") btnStart.config(state = "normal") btnConectar.config(state = "disabled") btnManual.config(state = "normal") btnManual2.config(state = "normal") conected = True except: print("Error de conexión") ``` `arduino` es el objeto encargado de establecer la conexión, sus atributos son el puerto serie y la tasa de baudios, `timeout` es el tiempo en que se están leyendo los datos constantemente, además se requiere de un pequeño retardo para lograr correctamente la comunicación de datos en primera instancia. Posterior a ello, únicamente se habilitan y deshabilitan botones para su uso en la interfaz. La siguiente función `leer_datos` se usa para comenzar a recibir los datos de Arduino una vez lista la comunicación. ``` def leer_datos(): time.sleep(1.0) arduino.reset_input_buffer() print("leyendo") while(conected): if(isRun == True): global isReceiving global dato global data for i in range(numData): datos = arduino.readline().decode("utf-8").strip() print(datos) if(datos != ''): print("RECIBIENDO..." + str(i)) dato = float(datos) data[i].append(dato) if(i == 0): var.set("TEMPERATURE 1: " + str(dato) + " °C") else: var2.set("TEMPERATURE 2: " + str(dato) + " °C") else: break; isReceiving = True ``` Esta, a partir de un ciclo while al que se puede entrar una vez iniciada la conexión con Arduino, se encarga de leer y guardar en una variable `datos` la información decodificada en forma de cadena desde el monitor serie para, una vez asegurado que se ha leído un dato y no es información vacía, hacer su conversión a flotante y agregar este dato a la colección que se graficará posteriormente, además de poder visualizar el valor de la temperatura en la interfaz mediante una etiqueta ya sea que se trate del sensor 1 o 2. Cabe mencionar que cada que se muestra un `print` es para poder visualizar en la consola de python algún error o la parte del código donde nos encontramos. Puesto que la lectura de datos se debe efectuar simultaneamente a la graficación de los mismos, es necesario correr la función de `leer_datos` en un hilo a parte: ``` def iniciar_hilo(): global thread global isRun btnStart.config(state = "disabled") isRun = True thread = Thread(target=leer_datos) thread.start() #Inicio de la lectura btnStart.config(state = "disabled") btnPause.config(state = "normal") ``` Ahora, la siguiente función `iniciarGrafica` es la que se va a encargar de graficar mediante líeas 2D la colección de datos que recibe, pero para ello ocupa ser llamada posteriomente. ``` def iniciarGrafica(self, muestras,lines): global dato lines[0].set_data(range(muestras), data[0]) lines[1].set_data(range(muestras), data[1]) ``` Para poder guardar la gráfica que se muestra actualmente en la interfaz se utiliza la función: ``` def guardarGrafica(): plt.savefig('miFigura.png') ``` En control de los transistores se efectua a partir de las siguientes dos funciones: ``` def control1(): global on1 global estado global arduino if on1 == False: on1 = True btnManual.config(bg = "#55DE1E", text = "ON") estado = "on1" arduino.write(estado.encode()) else: on1 = False btnManual.config(bg = "red", text = "OFF") estado = "off1" arduino.write(estado.encode()) ``` ``` def control2(): global on2 global estado global arduino if on2 == False: on2 = True btnManual2.config(bg = "#55DE1E", text = "ON") estado = "on2" arduino.write(estado.encode()) else: on2 = False btnManual2.config(bg = "red", text = "OFF") estado = "off2" arduino.write(estado.encode()) ``` Según sea el botón de la interfaz con el que se interactúa, se hace la función de on/off de los transistores, para ello se hace uso de las variables `on1` y `on2` que indican el estado actual de estos dispositivos, de manera que si se presiona algún botón se ingresa a la función y se comprueba su estado actual, si el valor booleano actual de la variable `on` es *False* se enciende el transistor y se envía este cambio de estado al serial de Arduino, de lo contrario si el valor actual es *True* se apaga el transistor correspondiente e igualmente se envía la información a Arduino. El botón cambia de color y texto según el estado en que se encuentre el dispositivo, además se modifica el valor booleano de la variable `on1` u `on2`. Para pausar la obtención de datos y detener la gráfica se utiliza la función `pausar`, la cual interrumpe el evento de la animación de las líneas y cierra la conexión con Arduino, además el botón de `Resume` se habilita y el de `pause` se deshabilita: ``` def pausar(): global isRun global arduino isRun = False anim.event_source.stop() arduino.close() btnResume.config(state = "normal") btnPause.config(state = "disabled") ``` De manera similar, mediante la función `reanudar` se entabla nuevamente la conexión para recibir la temperatura de los transistores y se grafican nuevamente estos datos: ``` def reanudar(): global arduino global isRun conectar_serial() arduino.reset_input_buffer() isRun = True anim.event_source.start() btnResume.config(state = "disabled") btnPause.config(state = "normal") ``` Con la función `desconectar_serial` se detiene completamente la comunicación con Arduino, asimismo se deshabilitan los botnes para interactuar con la interfaz, esto con el propósito de finalizar la ejecución del programa. ``` def desconectar_serial(): global isRun global conected anim.event_source.stop() isRun = False conected = False arduino.close() btnPause.config(state = "disabled") btnResume.config(state = "disabled") btnManual.config(state = "disabled") btnManual2.config(state = "disabled") ``` Ahora bien, ya conociendo la labor de cada una de las funciones, lo siguiente corresponde a la parte del diseño de la interfaz que verá el usuario: Primeramente, se declaran ciertas variables ncesarias para graficar los datos provenientes de Arduino: ``` muestras = 100 tiempoMuestreo = 100 data = [] lines = [] for i in range(numData): data.append(collections.deque([0] * muestras, maxlen = muestras)) lines.append(Line2D([], [], color = "blue")) ``` `muestras` indica la cantidad de datos que se van a almacenar y visualizar, se crea una lista para guardar las lecturas de temperatura de los sensores y, de igual manera, `lines` que son las líneas 2D de cada figura para graficar los datos. Con las siguiente instrucciones se crea una nueva figura donde se van a visualizar las gráficas en la interfaz, posteriormente se agregan los respectivos subplots `ax1` para el sensor 1 y `ax2` para el sensor 2; en este caso se establece que se van a distribuir en la pantalla con un tamaño de 2 filas por 1 columna y se establecen sus límites de `xlim` indicando de 0 a 100 muestras y `ylim` para ver un rango de 0 a 150°C; también se le incorpora un título a cada subplot y etiquetas a los ejes; finalmente, y muy importante, indicar la línea que va a graficarse en cada subplot: ``` fig = plt.figure(facecolor = '0.94') #creación de la gráfica (figura) ax1 = fig.add_subplot(2, 1, 1, xlim=(0,100), ylim=(0, 150)) #Rango de ejes ax1.title.set_text("Sensor 1 - Arduino") ax1.set_ylabel("Voltaje") ax1.add_line(lines[0]) ax2 = fig.add_subplot(2, 1, 2, xlim=(0,100), ylim=(0, 150)) #Rango de ejes ax2.title.set_text("Sensor 2 - Arduino") ax2.set_xlabel("Muestras") ax2.set_ylabel("Voltaje") ax2.add_line(lines[1]) ``` Todo lo que se muestra a continuación corresponde a la aplicación de funciones y objetos que forman parte de la paquetería de tkinter para haces el diseño y la distribución de los elementos de la interfaz: ``` root= tk.Tk() root.title("Sistema de calentamiento") var = tk.StringVar() var2 = tk.StringVar() frame = tk.Frame(root, bd=2) frame.grid(column=0, row=3, columnspan=2, sticky="nsew") frame1 = tk.Frame(root) frame1.grid(column=0, row=1, columnspan=2, sticky="EW") frame2 = tk.Frame(root) frame2.grid(column=0, row=2, columnspan=2, sticky="EW") frame0 = tk.Frame(root) frame0.grid(column=0, row=0, columnspan=2, sticky="EW") root.columnconfigure(0, weight=1) root.columnconfigure(1, weight=1) root.rowconfigure(3, weigh=5) canvas = FigureCanvasTkAgg(fig, master=frame) canvas.get_tk_widget().pack(padx=0, pady=0, expand=True, fill='both') labelBlank = tk.Label(frame1, text="") labelBlank.grid(row=0, column=4, pady=2, padx=25) btnManual = tk.Button(frame1, text = "OFF", command = control1, bg = "red", state = "disabled") btnManual.grid(row=0, column=6, pady=2, padx=10) labelState = tk.Label(frame1, text="Transistor 1 State:") labelState.grid(row=0, column=5, pady=2, padx=5) labelBlank2 = tk.Label(frame1, text="") labelBlank2.grid(row=0, column=7, pady=2, padx=125) btnManual2 = tk.Button(frame1, text = "OFF", command = control2, bg = "red", state = "disabled") btnManual2.grid(row=0, column=9, pady=2, padx=10) labelState2 = tk.Label(frame1, text="Transistor 2 State:") labelState2.grid(row=0, column=8, pady=2, padx=5) btnConectar = tk.Button(frame1, text = "Connect", command = conectar_serial, bg="#00F1FC") btnConectar.grid(row=0, column=0, pady=2, padx=10) btnStart = tk.Button(frame1, text = "Start", command = iniciar_hilo, bg="#008C17", state="disabled") btnStart.grid(row=0, column=1, pady=2, padx=10) btnPause = tk.Button(frame1, text = "Pause", command = pausar, bg="#E2E200", state="disabled") btnPause.grid(row=0, column=2, pady=2, padx=10) btnResume = tk.Button(frame1, text = "Resume", command = reanudar, bg="#00F428", state="disabled") btnResume.grid(row=0, column=3, pady=2, padx=10) btnDesconectar = tk.Button(frame2, text='Disconnect', command = desconectar_serial, bg="#FE5E5E") btnDesconectar.grid(row=0, column=0, pady=2, padx=10) labelData = tk.Label(frame2, textvariable=var, font="Helvetica 10 bold") labelData.grid(row=1, column=1, pady=2, padx=230) labelData2 = tk.Label(frame2, textvariable=var2, font="Helvetica 10 bold") labelData2.grid(row=1, column=2, pady=2, padx=1) barraMenu = tk.Menu(frame0) barra1 = tk.Menu(barraMenu) barra1.add_command(label="Guardar gráfica", command=guardarGrafica) barraMenu.add_cascade(label="Archivo", menu=barra1) root.config(menu=barraMenu) ``` `root` es el objeto que representa la ventana de la interfaz, esta es la base o el lienzo donde se van a acomodar todos los componentes a visualizar; `var` y `var2` son textos variable que almacenan el valor de la temperatura de ambos sensores según este cambie; `frame`, `frame0`, `frame1`y `frame2` son secciones o espacios para distribuir de una manera más sencilla y limpia las etiquetas, botones y gráficas; se declara la variale `canvas` para poder visualizar con tkinter las gráficas en la interfaz; al presionar los botones `btnManual` y `btnManual2` hacen llamar a las funciones de control1 y control2 anteriormente descritas; el botón `btnConectar` sirve para iniciar la comunicación con Arduino, llama a conectar_serial; mientras que `btnDesconectar` finaliza la conexión; los botones de `btnStart`, `btnPause` y `btnResume` se utilizan para iniciar, pausar y continuar la graficación de datos, haciendo la llamada a las funciones iniciar_hilo, pausar y reanudar, respectivamente; las etiquetas `labelData` y `labelData2` muestran el valor que contienen las variables `var` y `var2`; finalmente, en `barraMenu` se incluye un menú Archivo para disponer de la opción de guardar la gráfica en la carpeta donde se encuentra el programa. Por último, con el módulo de `animation` se lleva a cabo la actualización continua de la gráfica, para ello se le dan de argumentos la figura declarada, la función de `inicarGrafica` ya descrita, los argumentos de esta función que son el número de muestras y las líneas 2D a graficar, el tiempo de muestro y, finalmente, cache_frame_data controla si los datos del frame se almacenan en caché, el valor predeterminado es Verdadero, deshabilitar el caché puede ser útil cuando los frames contienen objetos grandes, hacer esto es opcional comúnmente en Windows pero suele ser necesario en Linux en caso de algún error. Con esto, ya es posible ejecutar la ventana `root` con la función de mainloop de tkinter. ``` anim = animation.FuncAnimation(fig, iniciarGrafica, fargs=(muestras, lines), interval = tiempoMuestreo, cache_frame_data=False) root.geometry('1000x600') root.mainloop() ``` ![](http://gmarxcc.com:8088/MSP430/GUI-Heater-System/raw/branch/master/Esquematico%20Conexiones/Esquematico_Sistema_de_Calentamiento.png)