- Introducción
- Estructura del repositorio
- Scripts de respaldo
- Scripts principales
- Pendientes
El presente repositorio contiene múltiples scripts que se crearon con el objetivo de poder graficar información proveniente de mediciones de material particulado de arreglos de sensores PurpleAir a través de una superficies. En este aspecto el repositorio contiene 4 scripts clave, los cuales seran descritos de manera específica más adelante. De forma general los scripts tienen las siguientes funciones:
- TSClasses.py: Script que contiene las funciones necesarias para obtener los datos de un canal de Thingspeak
- LivePlot.py: Script que permite graficar en tiempo real los datos recibidos de un Sensor PurpleAir (utilizando las funciones de TSclasses.py)
- AvgFunctions.py: Script con las funciones necesarias para hacer distintos tipos de gráficas y animaciones a partir de datos recibidos de canales de thingspeak (utilizando funciones de TSclasses.py) o archivos CSV
- GUI.py: Script para generar una interfaz de usuario que permita de manera fácil y rápida realizar el graficado de datos a partir de las funciones de AvgFunctions.py
Nota: Estos scripts únicamente han sido probados con Python 3.7 en Windows 10.
El repositorio está conformado por los scripts previamente mencionados, los cuales no se encuentran en ninguna carpeta. Durante el desarrollo de este proyecto se crearon varios scripts con el objetivo de comprender como descargar datos y proponer distintas maneras de graficarlos. La mayoría de estos scripts fueron simplificados y se integraron a alguno de los 4 scripts clave (principalmente AvgFunctions.py), por lo que se volvieron obsoletos. Sin embargo, en caso de ser necesarios revisarlos estos se colocaron en la carpeta Backup. Adicionalmente, en este repositorio se encuentran dos carpetas con configuraciones. Por un lado, la carpeta .idea contiene las configuraciones de PyCharm al ejecutar este repositorio (esta carpeta no es necesaria y puede ser eliminada sin causar problemas en el repositorio. Por otro lado, la carpeta pycache contiene archivos que son versiones simplificadas de algunos scripts que permiten ejecutarlos de manera rápida al ser llamados por otro script (por ejemplo, cuando el script GUI.py llama una función de AvgFunctions.py). Finalmente, se añadió la carpeta media para colocar archivos media para el repositorio.
Como se mencionó en la sección anterior, varios scripts fueron de prueba. Al volverse obsoletos estos se transfirieron la carpeta Backup. A través de esta sección se describirá en que consistieron dichos scripts. Sin embargo, es importante mencionar que la mayoría de estos scripts no fueron comentados.
- CSV_read.py: Script que abre un archivo CSV (correspondiente a un sensor), lee su contenido y grafica la concentración de PM a través del tiempo. De manera más específica, grafica las concentraciones de PM1.0, PM2.5 y PM10.0.
- PlotAnimation.py: Script para graficar las mediciones de sensores PurpleAir distribuidos en cierto lugar durante una cantidad de tiempo. Los datos se obtienen de canales de thingspeak.
- PurpleAirReadTest.py: Este script usa las funciones del script TSClasses para abrir obtener los datos de un canal de thingspeak y posteriormente graficar la concentración de PM1.0, PM2.5 y PM10.0 contra el tiempo.
- ReadMultiplePA.py: Script capaz de obtener los datos de 3 sensores PurpleAir, a partir de su canales de thingspeak, para después graficar la información de PM1.0, PM2.5 y PM10.0 con respecto al tiempo.
- ReadMultiplePACSV.py: Script diseñado para graficar los datos de la primera campaña de monitoreo (llevada a cabo en el Parque de Investigación e Innovación Tecnológica). De manera más específica, este script usa 6 archivos CSV con datos de mediciones. Después se guardan en listas los datos de concentraciones PM1.0 para cada sensor, se hace un promedio de las mediciones de cada sensor y se grafican los datos en un espacio tridimensional. Donde el eje Y y X describen la ubicación de los sensores y el eje Z describe la concentración de PM en un rango de tiempo.
- ReadMultiplePACSV_AVG.py: Script muy similar al anterior. Sin embargo, se le realizaron algunas modificaciones para graficar los datos obtenidos de un segunda campaña de monitoreo (en el Tec de Monterrey) donde se utilizó una mayor cantidad de sensores.
- ReadMultiplePACSV_AVG_10_MIN.py: Script similar a ReadMultiplePACSV.py. Sin embargo, en vez de hacer un promedio de todos los datos en cada archivo csv, este script (a partir de un hora de inicio) hace una gráfica de un promedio de 10 minutos.
- Test.csv: Archivo de prueba para validar el graficado de datos a partir de un archivo CSV.
- testtime.py: Script para comprender como generar formatos de fecha en Python.
Los sensores PurpleAir suben información a la nube, la cual puede ser accedida usando el API de PurpleAir o el API de Thingspeak. Debido a que en un inicio se desconocía como obtener las llaves para utilizar el API de PurpleAir se optó por usar el API de Thingspeak. En este aspecto, esta API genera 4 canales por cada sensor PurpleAir, esto debido a que cada uno de estos dispositivos de PurpleAir a su vez cuentan con dos sensores (sensor A y B) los cuales reportan la misma información y únicamente permiten validar que ambos sensores funcionen de manera correcta. Los datos de cada uno de estos sensores dentro del dispositivo se entregan en 2 partes (un canal primario y uno secundario) resultando así en los 4 canales por dispositivo PurpleAir. Para acceder a los datos de los sensores se tenía que acceder a los canales previamente mencionados. Con este objetivo en mente se generó un script con funciones capaces de acceder a un canal (a partir de un ID y una llave) y llamar un cantidad definida de datos de dicho canal. Es importante mencionar que dichas funciones pueden ser utilizadas por otros scripts.
Nota: Adicionalmente, es importante mencionar que para obtener el ID y las llaves de un dispositivo PurpleAir (considerando que este ya fue registrado en el sitio web de PurpleAir), se debe acceder a este enlace (el cual contiene la lista de todos los sensores registrados en PurpleAir), buscar el dispositivo por su nombre, copiar el número de dispositivo y acceder a:
https://www.purpleair.com/json?show=SEN_NUM
Donde SEN_NUM es remplazado por el número de dispositivo. Este último enlace contendrá los IDs y llaves para todos los canales del dispositivo PurpleAir, los cuales llevan por nombre "THINGSPEAK_PRIMARY_ID", "THINGSPEAK_PRIMARY_ID_READ_KEY","THINGSPEAK_SECONDARY_ID" y "THINGSPEAK_SECONDARY_ID_READ_KEY". Por ejemplo, si primero buscamos el dispositivo con nombre GIECC_UCMEXUS_1 encontraremos que su número es 104774 y al utilizar:
https://www.purpleair.com/json?show=104774
encontramos los siguientes IDs y llaves:
- Sensor A:
- "THINGSPEAK_PRIMARY_ID":"1367948"
- "THINGSPEAK_PRIMARY_ID_READ_KEY":"TMTVNTYUXGGT7MK3"
- "THINGSPEAK_SECONDARY_ID":"1367949"
- "THINGSPEAK_SECONDARY_ID_READ_KEY":"N35ZTFXU25M0A6CA"
- Sensor B:
- "THINGSPEAK_PRIMARY_ID":"1367950"
- "THINGSPEAK_PRIMARY_ID_READ_KEY":"UJUFOPEW3TOQWV8W"
- "THINGSPEAK_SECONDARY_ID":"1367951"
- "THINGSPEAK_SECONDARY_ID_READ_KEY":"S26M6JT22KN1VIDK"
Con el objetivo de poder monitorear un sensor en tiempo real y validar que sus mediciones sean correctas se creó este código. Este script utiliza el API de thingspeak para acceder a un canal, obtener una cantidad definida de datos de ese canal graficar dichas concentraciones de PM contra el tiempo y cada 2.1 minutos verificar si se han recibido nuevos datos para actualizar el grafico. Esta validación de nuevos datos se hace a través de una función llamada animate() donde se obtiene la última medición añadida a un canal de thingspeak, para posteriormente checar si el tiempo en el que se obtuvo dicha medición coincide con una que ya se tenía. En caso de ser así pues este no es un dato nuevo. Caso contrario, es una nueva medición y se guardan en listas la estampa de tiempo y la medición. Estas listas son las que se utiliza para hacer el grafico que se actualiza periódicamente. Finalmente, para modificar el ID, llave, tipo de PM y numero de resultados se deben modificar las líneas 15, 16, 30 y 44 del script respectivamente.
Para evitar tener en un mismo script la interfaz de usuario y las funciones a ejecutar al presionar botones específicos de la interfaz de usuario, se optó por separar estos dos. Este código contiene las funciones a ejecutar por el GUI. De manera más específica este script contiene 7 funciones las cuales se describirán a continuación:
- avg(): esta función permite sacar un promedio de los elementos de una lista. Por lo que únicamente es necesario proveerle un lista y esta función regresara el promedio obtenido.
- GraphAvg(): función que intenta acceder a una cantidad definida de archivos CSV (correspondientes a sensores PurpleAir) para después buscar mediciones para cierto tipo de PM a partir de cierta hora y hasta cierta hora en cada uno de los archivos CSV. En este aspecto, es importante mencionar que la función itera a través de cada uno de los archivos CSV, por lo que estos deben tener un nombre especifico, correspondiente al número de sensor ("S1","S2","S3","S4",...). Las mediciones de cada sensor se añaden a una lista, para después obtener el valor promedio usando la función avg(). Los promedios de cada uno de los sensores se añaden a una lista (que representa los valores del eje z). Seguido de esto, se genera un archivo de texto reportando información general sobre las mediciones. Posteriormente, a partir del arreglo definido para los sensores y la distancia entre estos se generan do listas mas una que define las posiciones en el eje X de cada sensor y otra que define las posiciones en el eje Y. Adicionalmente, es importante mencionar que el primer se ubica en la coordenada (0,0). Después, se define el formato de la gráfica, se convierten las listas con los datos de todos los ejes coordenados a arreglos numpy para poder hacer interpolación entre los sensores y obtener una gráfica con curvas suaves. Finalmente se genera el grafico.
- LateralAvg(): Funciona de manera similar a la función anterior. Sin embargo, una vez obtenida la lista de promedios para cada sensor, se itera a través de cada fila y se obtiene un valor promedio para las columnas de esa fila los resultados se añaden posteriormente a una lista. Después, únicamente se genera una lista describiendo la posición de los sensores en el eje Y (a partir de la distancia entre filas). Posteriormente, se definen las configuraciones del grafico para después generar un gráfico 2D a partir de archivos CSV. es importante mencionar que en este caso el promedio de la primera fila de sensores se encuentra en Y = 0.
- animate(): Función llamada para animar un gráfico. Esta función recibe las mediciones de un arreglo de sensores en un periodo de tiempo determinado. Al mismo tiempo, esta función recibe la iteración actual (el número de iteraciones está definido por la cantidad de datos recolectados por cada sensor) por lo que esta función itera a través de todos los sensores y obtiene la medición de cierto tipo de PM en un instante especifico de tiempo (definido por la iteración actual) para cada sensor, estos valores se guardan en una lista que define los valores del grafico en el eje Z. Por otro lado, los valores en los ejes X y Y se le entregan directamente a la función en forma de listas. Las listas de todos los ejes se convierten a arreglos numpy para poder interpolar y suavizar las curvas del gráfico. Adicionalmente, se definen las configuraciones del gráfico y se crea el grafico o se actualiza en caso de que ya existiese. Esta función se llama periódicamente al desear crear una animación, con cada llamada el valor de la iteración actual cambia, por lo que se obtienen los valores medidos por los sensores en otros instantes de tiempo, generando así una animación.
- AnimationCSV(): Esta función es muy similar a GraphAvg(). La única diferencia radica en que al final de la función no se configura ni genera un gráfico, sino que se define una animación utilizando matplotlib.animation.FuncAnimation() lo cual genera una gráfica que se actualiza periódicamente utilizando una función (que en nuestro caso es animate()). Esta función tiene que cumplir cierto número de iteraciones (frames) antes de que se cumpla un intervalo de tiempo (interval), ambos valores se define al llamar la función FuncAnimation(). Es importante mencionar que, al finalizar el intervalo de tiempo, la animación se reinicia.
- AnimationPA(): Esta función primero genera las listas para los ejes X y Y. Seguido de esto, se utilizan las funciones de TSClasses.py para iterar a través de una cantidad de sensores cuyos IDs y llaves (únicamente sensor A y canal primario) se encuentran previamente definidos en las líneas 49 y 53 del script. Al iterar a través de estos sensores se obtienen una cantidad definida de mediciones de un tipo específico de PM, las mediciones de cada sensor son añadidas a un lista, que a su vez después se añade a un lista que contiene listas con las mediciones de cada sensor. Finalmente, se utiliza matplolib.animation.FuncAnimation() al igual que en la función pasada para llamar a la función animate() y definir el interval y frames.
- GraphAvgPA(): Esta función es una combinación de AnimationPA() y GraphAvg(), ya que primero obtiene mediciones de un tipo de material particulado por una cantidad definida de tiempo de forma similar a como lo hace AnimationPA(). Por otro lado, la generación de reportes, las configuraciones de la listas de los ejes X y Y, la interpolación para la suavización del gráfico, la configuración del gráfico y la generación del grafico se hace de manera similar a como lo hace GraphAvg(). Adicionalmente, se debe reconocer que en caso de que los sensores utilizados para la campaña de monitoreo no sean los suficientes para llenar las dimensiones del arreglo definido en la interfaz de usuario, se debe cambiar/añadir los IDs y llaves del sensor más próximo para añadir información a ese punto que no la tiene (Esta última nota también aplica para AnimationPA()).
Este script es el que genera la interfaz de usuario, para lograr esto se usó la librería PySimpleGUI. Lo primero que hace el script es definir las diferentes páginas de la interfaz:
- layout1: Pagina que se abre al ejecutar el script, esta contiene 3 botones, de los cuales uno es para cerrar el programa. Por otro lado, los otros dos botones permiten seleccionar de donde se obtendrán los datos a graficar. Uno es para graficar a partir de datos de archivos de CSV, el otro es para graficar a partir de canales de thingspeak.
- layout2: Esta página se accede cuando se oprime el botón del layout1 correspondiente al graficado a partir de datos de archivos CSV. En esta página existen 3 botones. Igual que en la página anterior, uno es para cerrar la interfaz. Por otro lado, uno de los botones restantes es para generar graficas promedio, mientras que el otro es para generar animaciones.
- layout3: Esta página es accedida cuando se oprime el botón del layout2 correspondiente al graficado de promedio de datos. En esta interfaz se encuentran definidos diferentes espacios de entrada para colocar el número de sensores, distancia entre sensores, tipo de PM, ubicación del folder con archivos CSV, entre otros. Asimismo, debajo de estos espacios de entrada se encuentran 3 botones uno de salida y dos para generar gráficas, uno crea una tridimensional de las mediciones usando la función previamente descrita GraphAvg(), por otro lado el otro botón genera un grafica lateral utilizando LateralAvg().
- layout4: Esta página se accede cuando se oprime el botón del layout2 correspondiente a la creación de una animación. De manera similar a layout3, este contiene diferentes parámetros de entrada. Adicionalmente, contiene dos botones en la parte inferior, uno para salir y el otro para crear una animación a partir de los datos proveídos en los parámetros de entrada y utilizando las funciones AnimationCSV() y animate().
- layout5: Pagina que se abre al presionar el botón "Online" en la página inicial (Layout1). Esta página contiene 3 botones, entre los cuales hay uno para salir del programa. Por otro lado, los botones restantes se utilizan para seleccionar si se hará una gráfica promedio o una animación a partir de canales de thingspeak.
- layout6: Similar a las pagina 4 (layout4), esta es accedida cuando se oprime el botón de animación en el layout5. La página contiene diferentes espacios de entrada los cuales proveen la información necesaria a las funciones AnimationPA() y animate() para generar una animación a partir de mediciones de sensores PurpleAir almacenadas en canales de thingspeak. Para generar la animación se debe oprimir un botón ubicado en la parte inferior de la página (lo cual llama a las funciones previamente mencionadas). Adicionalmente, existe otro botón el cual sirve para salir de la GUI.
- layout7: Esta página se accede cuando se presiona el botón de promedio en el layout5. De manera similar a la página interior, existen varios parámetros de entrada y al final se encuentran 2 botones. El primero permite generar un gráfico 3D de mediciones de sensores almacenadas en canales de Thingspeak, esto se logra a partir de la función GraphAvgPA(). Por otro lado, el otro botón únicamente permite salir de la aplicación.
Después de definir todas las páginas, se establece el orden de las mismas y cual esta activada. para posteriormente, generar la GUI. Seguido de esto, se monitorean continuamente los eventos en la GUI y se guardan los eventos que sucedan, cada vez que sucede un evento. Posteriormente, utilizando una serie de condiciones IF/ELIF se verifica que evento sucedió (es decir, que botón se apreto) para realizar la acción correspondiente, ya sea cambiar de página o llamar funciones de AvgFunctions.py.
- la GUI debe permitir definir hora de inicio para el graficado de datos (en PurpleAir no se pone hora, busca de la última medición a cierta cantidad de valores en el pasado).
- la GUI debe permitir definir la duración de tiempo para hacer el promedio.
- Generar un gráfico 3D del promedio de mediciones en un espacio y tiempo definido.
- El grafico 3D debe permitir visualizar los datos "desde arriba" generando un mapa de calor 2D, que muestre ejes X y Y.
- Promediar valores a lo largo del eje Y (una fila) para generar un segundo gráfico en 2D, que muestra la concentración promedio de PM a medida que nos alejamos de la vía, mostrando ejes X y Z (Pendiente para datos de obtenidos de thingspeak).
- la GUI debe permitir definir duración de tiempo total para la animación.
- la GUI debe permitir definir duración real de la animación. Es decir, tenemos datos de "x" horas, pero ¿en cuánto tiempo se despliega la animación?
- Permitir animaciones parciales, es decir que se puedan hacer animaciones con promedios de "x" cantidad de minutos y no únicamente con los datos de cada 2 minutos.
- Hacer una animación de un promedio de valores a lo largo del eje Y (una fila) para generar un segundo gráfico en 2D, que muestra la concentración promedio de PM a medida que nos alejamos de la vía, mostrando ejes X y Z.
- la GUI debe permitir definir la cantidad de sensores en eje Y y en eje X.
- la GUI debe permitir definir la distancia entre sensores en eje Y y en eje X.
- la GUI debe permitir declarar de dónde se sacará la información (Internet o MicroSD).
- las gráficas se deben presentar como una gráfica de calor (esto no lo define el usuario, pero nos servirá para identificar concentraciones mayores a los límites permitidos/norma de calidad del aire).
- Después de ejecutar los scripts de promedio a lo largo del tiempo y espacio se debe generar un archivo de texto que incluya hora de inicio y fin de mediciones y Cantidad de datos tomados por cada sensor, esto para identificar algún error y poder hacer las correcciones necesarias.
- STDV: sería bueno tomar la desviación estándar (stdv) de cada fila, pues en teoría todos los sensores dentro de una misma fila en Y deberían reportar valores similares.
- ¿Qué pasa si perdemos datos intermedios en algún sensor?, por ejemplo S1 y S3 tienen 30 datos, pero S2 (en medio de S1 y S3) solo reporta 15. En ese caso, para el sensor S2, podemos tomar un promedio considerando solo los 15 datos o podríamos ajustar los datos faltantes como un promedio entre los valores entregados por S1 y S3.
- la GUI debe permitir definir el tamaño de partícula a utilizar.
- la GUI debe permitir definir la ruta a seguir para obtener los datos de la MicroSD
- Checar si PA conectado a la Micro SD genera un archivo por día, es decir si lo conectamos una vez, y dura 3 días ¿genera 3 archivos?
- Guardar todos los ids de los 30 sensores para que el programa tenga la info local, y solo te pida el número del sensor que quieres usar.
- Utilizar los dos canales de cada sensor.
- Poder ver el # del sensor al pasar el cursor sobre la gráfica.
- Asignar colores de acuerdo al AQI de la EPA.
- Agregar a la interfaz la opción de checar lo que monitoreó un sensor en específico (Crear una función basada en LivePlot.py que pueda ser integrada a AvgFunctions.py).