Autor: Edison Del Rosario

  • 3. Coordenadas - GPS a utm y distancias a gateway

    Las coordenadas de cada punto donde se ubica un dispositivo se obtienen usando un GPS.

    Cada nodo se identifica con tres letras y dos dígitos por ejemplo usando cultivo y numeración ascendente al alejarse del gateway. Por ejemplo, para el cultivo de Cacao , radial "A" desde el gateway y punto en ubicación 04  el identificador de ubicación será CA04.

    Las coordenadas se registran obteniendo el archivo kml del equipo GPS y contienen los valores de Latitud y Longitud. Por facilidad de uso y ubicación las coordenadas se usan en formato utm (Universal Transverse Mercator).

    El archivo KML contiene los datos de los puntos, básicamente en el siguiente formato:

    <Placemark>
            <name>CA04
            <styleUrl>#msn_pink-diamond</styleUrl>
            <Point>
                    <coordinates>-79.96257337394334,
                                 -2.139534989113919,
                                 65.76000000000001</coordinates>
            </Point>
    </Placemark>
    

    Se adjunta un ejemplo del archivo para pruebas.

    Granja2021_LOS.kml

    El objetivo es generar un archivo con distancias al gateway y alturas del perfil del terreno semejante al ejemplo mostrado:

           altitud  altitud_gps  dist_Gw03   latitud   longitud  utm_este  utm_norte utm_zLetra utm_zNum
    punto                                                                                               
    Gw03      65.0        68.30       0.00 -2.140439 -79.962474  615377.0  9763377.0          M       17
    LOS01     64.0        66.16      24.74 -2.140222 -79.962528  615371.0  9763401.0          M       17
    LOS02     59.0        66.16      75.93 -2.139770 -79.962627  615360.0  9763451.0          M       17
    LOS03     58.0        66.16      99.64 -2.139553 -79.962636  615359.0  9763475.0          M       17
    LOS04     58.0        66.06     126.44 -2.139309 -79.962645  615358.0  9763502.0          M       17
    ...
    

    En cada punto, la altitud_gps se puede mejorar usando el perfil del terreno desde google earth. Las instrucciones se muestran más adelante en "Perfil de alturas de terreno".

    Coordenadas en utm

    Para la conversión a utm, se usa la librería en Python con el mismo nombre. En caso de no disponer de la librería, puede ser instalada con la instrucción pip desde una ventana de comandos:

    pip install utm

    con lo que es posible hacer las conversiones a UTM desde latitud y longitud y viceversa.

    Las coordenadas en formato UTM para el ejemplo dado, se encuentran ubicadas en la zona «17 M». Revisar para cada caso la zona

    Perfil de alturas de terreno

    Los datos de alturas del gps usado presentan variaciones irregulares de altura, por lo que para mejores valores se usa el perfil desde google earth usando las marcas de las coordenadas del archivo.kml

    Para visualizar el perfil se agrega una línea como una "ruta" entre el punto del Gateway (Gw3) y el último punto de observación. Los datos del perfil se pueden observar al seleccionar la ruta y usar la opción de "mostrar perfil de elevación". La opción se obtiene con "click derecho" del mouse. Se tiene adicionalmente como referencia, las distancias desde el gateway obtenidas en el proceso de conversión a utm. Los valores leidos de alturas para cada punto se guardan en un archivo .csv

    El archivo obtenido en el ejemplo de alturas se muestra en:

    Granja2021_alturas.csv


    Instrucciones en Python

    El algoritmo integra los resultados de la conversión de coordenadas a utm, incluye las distancias al gateway e integra las alturas obtenidas de forma externa para tener como resultado el archivo.csv.

    Granja2021_LOS.csv

    Una revisión para generar el archivo es evitar sobreescribir un archivo anterior. Por lo que si se requiere procesar nuevamente los datos se pide borrar el archivo anterior antes de ejecutar el algoritmo.

    El algoritmo se divide en procesos identificados por un número ascendente. La variable 'procesoCompletado' permite dar indicaciones de avances en el algoritmo en caso que se produzcan errores como haber escrito el nombre del archivo de datos con errores, que el nombre del gateway debe empezar con "Gw", etc.

    # Procesa puntos de coordenadas, a utm
    # girni-fiec-espol Revision:20220724
    # http://blog.espol.edu.ec/girni/ 
    
    import geopandas as gpd
    import fiona
    import utm
    import numpy as np
    import pandas as pd
    import os
    
    # INGRESO
    archivokml  = 'Granja2021_LOS.kml' 
    carp_coord  = 'Coordenadas'
    
    # archivo de alturas tomadas de google
    arch_altura = 'Granja2021_alturas.csv'
    
    # zona UTM
    zonaNum   = 17
    zonaLetra = 'M'
    
    precision = 2
    
    # PROCEDIMIENTO
    procesoCompleto = 0
    
    # Lectura de archivo .KML
    if len(carp_coord)>0:
        carp_coord = carp_coord+'/'
    archivo_ruta = carp_coord+archivokml
    if os.path.exists(archivo_ruta):
        gpd.io.file.fiona.drvsupport.supported_drivers['KML'] = 'rw'
        puntos = gpd.read_file(archivo_ruta, driver='KML')
        puntos = puntos.sort_values(by=['Name'])
        
        procesoCompleto = 1
    
    if procesoCompleto == 1:
        # tabla en UTM
        tabla = pd.DataFrame()
        n_Gw = '';
        for i in puntos.index:
            # en latitud, longitud y altura
            punto_nombre = puntos.loc[i]['Name']
            longitud = puntos.loc[i]['geometry'].x
            latitud  = puntos.loc[i]['geometry'].y
            altitud  = puntos.loc[i]['geometry'].z
            altitud  = np.round(altitud,precision)
            #convierte lat_lon en UTM
            coord_utm = utm.from_latlon(latitud,longitud,
                                        zonaNum,zonaLetra)
            utm_este  = np.round(coord_utm[0],precision)
            utm_norte = np.round(coord_utm[1],precision)
            if punto_nombre.startswith('Gw'):
                n_Gw = punto_nombre
                x1 = utm_este
                y1 = utm_norte
                    # añade coordenadas UTM a tabla
            tabla.at[i,'punto']      = punto_nombre
            tabla.at[i,'utm_este']   = utm_este
            tabla.at[i,'utm_norte']  = utm_norte
            tabla.at[i,'utm_zNum']   = str(zonaNum)
            tabla.at[i,'utm_zLetra'] = zonaLetra
            tabla.at[i,'altitud']    = 0
            tabla.at[i,'altitud_gps']= altitud
            tabla.at[i,'latitud']    = latitud
            tabla.at[i,'longitud']   = longitud
        procesoCompleto = 2
    
    if procesoCompleto==2:
        # gateway encontrado
        if len(n_Gw)>0:
            procesoCompleto = 3
    
    if procesoCompleto == 3:
        # distancias a Gateways
        for i in puntos.index:
            dist = 0.0
            x2 = tabla.at[i,'utm_este']
            y2 = tabla.at[i,'utm_norte']
            dist = np.sqrt((x2-x1)**2 + (y2-y1)**2)
            dist = np.round(dist,precision)
            tabla.at[i,'dist_'+n_Gw] = dist
    
        # indice por nombre de punto
        tabla = tabla.set_index('punto')
        # ordena por indice
        tabla = tabla.sort_index(axis=1)
    
        procesoCompleto = 4
    
    if procesoCompleto == 4:
        # añade alturas desde archivo
        if len(arch_altura)>0:
            arch_rutaA = carp_coord + arch_altura
            if os.path.exists(arch_rutaA):
                tablaAltura = pd.read_csv(arch_rutaA)
                tablaAltura = tablaAltura.set_index('punto')
                for cadapunto in tabla.index:
                    if cadapunto in tablaAltura.index:
                        encontrada = tablaAltura.loc[cadapunto]['altitud']
                        tabla.at[cadapunto,'altitud'] = encontrada
    
                procesoCompleto = 5
            
    # Para escritura de archivo
    archivocsv   = archivokml.split('.')[0] + '.csv'
    archivo_ruta = carp_coord+archivocsv
    
    # SALIDA
    if procesoCompleto == 5:
        print(tabla)
        # Escribe archivo: tabla de resultados
        if os.path.exists(archivo_ruta):
            print('El archivo existe: ',archivo_ruta)
            print('Esta acción borraría el archivo último actualizado')
            print('Puede borrarlo manualmente antes de reintentar')
        else:
            tabla.to_csv(archivo_ruta)
    else:
        print('proceso completado hasta:', procesoCompleto)
        print('Revisar errores en proceso:', procesoCompleto+1)
        print('   Procesos:')
        print('1: abrir archivo .KML',archivokml)
        print('2: crear tabla de datos')
        print('3: encontrar gateway:', n_Gw)
        print('4: calcula distancia cada punto a gateway')
        print('5: añadir alturas desde archivo: ', arch_rutaA)
    

    Referencia: Bidirectional UTM-WGS84 converter for python. https://pypi.org/project/utm/0.4.2/

  • 2.3. Descriptor estadístico de un punto o dispositivo

    Los datos registrados para un punto, tabulados en el proceso anterior requieren un análisis básico para obtener descriptores estadísticos que los representen. A partir del archivo ejemplo mostrado, se pueden obtener los siguientes resultados:

    data_LOS22.csv


    Por ejemplo, para analizar el nivel de señal (rssi_up, rssi_down) se requiere de los parámetos de media, desviación estándar, medias móviles, función de probabilidad de masa (pmf), etc. Los parámetros se pueden resumir en tablas y gráficas semejantes a la mostrada.

    descriptor estadístico para los datos de la gráfica

    descriptor
                   rssi_up  rssi_down
    count        98.000000   98.00000
    mean        -86.336735  -77.44898
    std           3.782608    1.14083
    min         -97.000000  -81.00000
    25%         -89.000000  -78.00000
    50%         -86.000000  -77.00000
    75%         -84.000000  -77.00000
    max         -78.000000  -75.00000
    error_trama   0.000000    0.00000
    

    Para el proceso se requiere el nombre del punto, la carpeta o directorio donde se encuentra, la medida a observar (ej: rssi).

    Un parámetro como la media puede se insuficiente para observar el comportamiento, por lo que se añaden las medias móviles.

    Para la media móvil se debe indicar cada cuántas muestras se obtendrá el promedio, por lo que al usar varias se darán varias de ellas mediante la variable movAvg_cual.

    Errores en una trama

    Se consideran errores de medida a los valores fuera de un intervalo expresado en "medida_normal". Al indicar si un dato tiene un error es posible determinar la tasa de error en de trama, para el canal de subida y de bajada.

    Registros a procesar

    Los registros que se procesarán serán libres de errores de trama, donde se calculan la media, desviación estándar, mínimo, máximo. Los resultados son los mostrados en la tabla.

    Funciones de Probabilidad de masa (pmf)

    Para cada valor de medida (rssi), se obtiene la frecuencia relativa que se presentan en una gráfica pmf.

    Archivos de resultado

    Los resultados se guardan en archivos.csv y graficos.pmf para el registro del análisis de cada punto. Loa archivos se agrupan en una carpeta de resultados para su posterior análisis en conjunto.

    describe_LOS22.csv


    Instrucciones en Python

    # Descriptores estadisticos de los datos.csv
    # de un dispositivo, revisa media, desviació estándar, pmf
    # graba unreporte.csv con pandas y genera gráficas
    # http://blog.espol.edu.ec/girni/
    
    import numpy as np
    import json
    import pandas as pd
    import datetime as dt
    import os
    import matplotlib.pyplot as plt
    import matplotlib.dates as mdates
    import matplotlib.units as munits
    
    # INGRESO
    cualpunto   = "data_m_LOS22.csv"
    carpeta_rsm = "resultado_Test"
    
    medida         = "rssi"
    medida_unidad  = "dBm"
    medida_normal  = [-250,-1]
    medida_grafica = [-100,-60]
    
    movAvg_cual  = [8,16] #cada cuantas muestras
    movAvg_color = ['lightgreen','orange']
    precision   = 2
    guarda      = True
    
    # PROCEDIMIENTO
    unmodelo = cualpunto.strip('.csv').split('_')[1]
    unubicado   = cualpunto.strip('.csv').split('_')[2]
    codigopunto = unmodelo+'_'+unubicado
    
    # Leer archivo
    archivopunto = carpeta_rsm + '/' + cualpunto
    tabla = pd.read_csv(archivopunto)
    tabla = tabla.drop(columns='Unnamed: 0')
    tabla = pd.DataFrame(tabla)
    
    fechaformato = "%Y-%m-%d %H:%M:%S.%f"
    # medida intervalo
    medida_min = np.min(medida_normal)
    medida_max = np.max(medida_normal)
    
    # fechas series a datetime
    tabla['publishedAt'] = pd.to_datetime(tabla['publishedAt'],
                                          format=fechaformato)
    tabla['created'] = pd.to_datetime(tabla['publishedAt'],
                                      format=fechaformato)
    
    # revisa errores de medida
    tabla["error_up"]   = 0
    tabla["error_down"] = 0
    for undato in tabla['publishedAt'].keys():   
        medida_up = tabla[medida+'_up'][undato]
        enrango = (medida_up>=np.min(medida_normal))
        enrango = (enrango and medida_up<=np.max(medida_normal))
        if not(enrango):
            tabla.at[undato,"error_up"] = 1
        
        medida_down = tabla[medida+'_down'][undato]
        enrango = (medida_down>=np.min(medida_normal))
        enrango = (enrango and medida_down<=np.max(medida_normal))
        if not(enrango):
            tabla.at[undato,"error_down"] = 1      
    
    # tasa error trama
    leidos = len(tabla)
    if leidos > 0:
        error_up   = np.sum(tabla['error_up'])
        error_up   = error_up/leidos
        error_down = np.sum(tabla['error_down'])
        error_down = error_down/leidos
    
    # descriptor estadístico, datos sin errores
    condicion_up = (tabla['error_up']==0)
    condicion_down = (tabla['error_down']==0)
    
    medida_up = tabla[condicion_up][medida+'_up']
    describe_up = medida_up.describe()
    describe_up['error_trama'] = error_up
    
    medida_down = tabla[condicion_down][medida+'_down']
    describe_down = medida_down.describe()
    describe_down['error_trama'] = error_down
    
    descriptor = describe_up.copy()
    descriptor = pd.concat([descriptor,describe_down],axis=1)
    descriptor['dispositivo'] = tabla['dispositivo'][0]
    
    # función de probabilidad de masa pmf
    def medida_pmf(valores,undescriptor):
        pmin   = np.min(valores)
        pmax   = np.max(valores)
        tramo  = int(pmax-pmin)
        conteo = np.zeros(tramo+1,dtype=int)
        intervalo = np.arange(pmin,pmax+1,1)
        for valor in valores:
            donde = np.where(intervalo == valor)
            conteo[donde] = conteo[donde] + 1
        freq_relativa = np.array(conteo)/np.sum(conteo)
        unpmf = {'intervalo' : list(intervalo),
                 'freq_relativa' : list(freq_relativa)}
        return(unpmf)
    
    pmf_up   = medida_pmf(medida_up,describe_up)
    pmf_down = medida_pmf(medida_down,describe_down)
    
    pmf_punto = {'pmf':{'pmf_up'   : pmf_up,
                        'pmf_down' : pmf_down}}
    pmf_punto = pd.DataFrame(pmf_punto)
    pmf_punto['dispositivo'] = tabla['dispositivo'][0]
    
    # Para gráficas
    # medias moviles en movAvg_cual[]
    serie_up  = pd.Series(medida_up)
    movAvg_up_mean = []
    movAvg_up_std = []
    m = len(movAvg_cual)
    for j in range(0,m,1):
        k = movAvg_cual[j]
        movAvg_up_mean.append(list(serie_up.rolling(k).mean()))
        movAvg_up_std.append(list(serie_up.rolling(k).std()))
        
    serie_down = pd.Series(medida_down)
    movAvg_down_mean = []
    movAvg_down_std = []
    for j in range(0,m,1):
        k = movAvg_cual[j]
        movAvg_down_mean.append(list(serie_down.rolling(k).mean()))
        movAvg_down_std.append(list(serie_down.rolling(k).std()))
    
    movAvgData ={'movAvg_cual'   : movAvg_cual,
                 'movAvg_color'  : movAvg_color,
                 'movAvg_up_mean'  : movAvg_up_mean,
                 'movAvg_down_mean': movAvg_down_mean,
                 'movAvg_up_std'   : movAvg_up_std,
                 'movAvg_down_std' : movAvg_down_std
                 }
    
    grafData ={'codigopunto' : codigopunto,
               'medida' : medida,
               'precision': precision,
               'medida_unidad' : medida_unidad,
               'medida_grafica': medida_grafica
               }
    
    # SALIDA --------------------------
    print('descriptor')
    print(descriptor)
    
    # guarda el reporte en csv
    unarchivo = carpeta_rsm+'/describe_'+codigopunto+'.csv'
    descriptor.to_csv(unarchivo)
    
    unarchivo = carpeta_rsm+'/pmf_'+codigopunto+'.json'
    pmf_punto.to_json(unarchivo)
    
    unarchivo = carpeta_rsm+'/movavg_'+codigopunto+'.json'
    with open(unarchivo, "w") as outfile:
        json.dump(movAvgData, outfile)
    
    unarchivo = carpeta_rsm+'/grfdata_'+codigopunto+'.json'
    with open(unarchivo, "w") as outfile:
        json.dump(grafData, outfile)
    
    
    # GRAFICA -----
    def graf_puntos_serie(tabla,descriptor,movAvgData,grafData):
        ''' grafica la serie de tiempo de cada punto
            añade medias móviles
        '''
        # ajuste de formato de fecha para eje x
        converter = mdates.ConciseDateConverter()
        munits.registry[np.datetime64] = converter
        munits.registry[dt.date] = converter
        munits.registry[dt.datetime] = converter
    
        # datos para grafica
        precision   = grafData['precision']
        medida = grafData['medida']
        medida_unidad  = grafData['medida_unidad']
        medida_grafica = grafData['medida_grafica']
        
        movAvg_cual  = movAvgData['movAvg_cual']
        movAvg_color = movAvgData['movAvg_color']
        
        media_up    = descriptor[medida+'_up']['mean']
        std_up      = descriptor[medida+'_up']['std']
        media_down  = descriptor[medida+'_down']['mean']
        std_down    = descriptor[medida+'_down']['std']
    
        # ajuste de intervalo eje y
        y_min = np.min([np.min(medida_grafica),
                        media_up - 2*std_up,
                        media_down - 2*std_down])
        y_max = np.max([np.max(medida_grafica),
                        media_up + 2*std_up,
                        media_down + 2*std_down])
        
        # selecciona sin error
        condicion_up = (tabla['error_up']==0)
        condicion_down = (tabla['error_down']==0)
    
        # grafica
        fig_serie,(graf_up,graf_down) = plt.subplots(2,1)
        
        # medida_up -----
        graf_up.plot(tabla[condicion_up]['publishedAt'],
                     tabla[condicion_up][medida+'_up'],
                     color='blue',marker ='.',
                     linestyle='')
        
        # medida_up, medias y std
        etiq_up = str(np.round(media_up,precision))+' +/- '
        etiq_up = etiq_up + str(np.round(std_up,precision))
        graf_up.axhline(media_up,
                        color='blue',label=etiq_up)
        graf_up.axhline(media_up-std_up,
                        color='blue',linestyle='dotted')
        graf_up.axhline(media_up+std_up,
                        color='blue',linestyle='dotted')
        
        # medida_up, medias móviles
        m = len(movAvg_cual)
        for j in range(0,m,1):
            k = str(movAvg_cual[j])
            graf_up.plot(tabla[condicion_up]['publishedAt'],
                         movAvgData['movAvg_up_mean'][j],
                         label='movAvg_'+k,
                         color=movAvg_color[j])
        
        graf_up.set_ylim(y_min,y_max)
        graf_up.set_ylabel(medida+'_up ('+medida_unidad+')',
                           color='blue')
        graf_up.legend()
        graf_up.grid(True,linestyle='dotted',
                     axis='x',which='both')
    
        # medida_down -------
        graf_down.plot(tabla[condicion_down]['publishedAt'],
                       tabla[condicion_down][medida+'_down'],
                       color='brown',marker ='.',
                       linestyle='')
    
        # medida_down, medias y std
        etiq_down = str(np.round(media_down,precision))+' +/- '
        etiq_down = etiq_down + str(np.round(std_down,precision))
        graf_down.axhline(media_down,
                          color='brown',label=etiq_down)
        graf_down.axhline(media_down+std_down,
                          color='brown',linestyle='dotted')
        graf_down.axhline(media_down-std_down,
                          color='brown',linestyle='dotted')
        
        # medida_down, medias moviles
        for j in range(0,m,1):
            k = str(movAvg_cual[j])
            graf_down.plot(tabla[condicion_down]['publishedAt'],
                           movAvgData['movAvg_down_mean'][j],
                           label='movAvg_'+k,
                           color=movAvg_color[j])
        
        graf_down.set_ylim(y_min,y_max)
        graf_down.set_xlabel('fecha')
        graf_down.set_ylabel(medida+'_down ('+medida_unidad+')',
                             color='brown')
        graf_down.legend()
        graf_down.grid(True,linestyle='dotted',
                       axis='x', which='both')
        graf_up.set_title('Serie: '+grafData['codigopunto']+' '+ medida)
        plt.tight_layout()
        return(fig_serie)
    
    def graf_puntos_pmf(pmf_punto,descriptor,grafData):
        ''' grafica función de probabilida de masa
            para cada punto, media +/- std
        '''
        # datos para grafica
        x_pmfup   = pmf_punto['pmf']['pmf_up']['intervalo']
        y_pmfup   = pmf_punto['pmf']['pmf_up']['freq_relativa']
        x_pmfdown = pmf_punto['pmf']['pmf_down']['intervalo']
        y_pmfdown = pmf_punto['pmf']['pmf_down']['freq_relativa']
        
        precision   = grafData['precision']
        medida = grafData['medida']
        medida_unidad  = grafData['medida_unidad']
        medida_grafica = grafData['medida_grafica']
        
        media_up   = descriptor[medida+'_up']['mean']
        std_up     = descriptor[medida+'_up']['std']
        media_down = descriptor[medida+'_down']['mean']
        std_down   = descriptor[medida+'_down']['std']
    
        prob_max = 0.40
        # ajuste de intervalo eje y
        y_min = np.min([np.min(medida_grafica),
                        media_up - 2*std_up,
                        media_down - 2*std_down])
        y_max = np.max([np.max(medida_grafica),
                        media_up + 2*std_up,
                        media_down + 2*std_down])
        # grafica
        fig_pmf,graf_pmf = plt.subplots()
        etiq_up = str(np.round(media_up,precision)) +' +/- '
        etiq_up = etiq_up + str(np.round(std_up,precision))
        graf_pmf.plot(x_pmfup,y_pmfup,
                      label='media_up '+etiq_up,
                      color='blue')
        graf_pmf.axvline(media_up,color='blue')
        graf_pmf.axvline(media_up+std_up,
                         linestyle='dotted',color='blue')
        graf_pmf.axvline(media_up-std_up,
                         linestyle='dotted',color='blue')
    
        etiq_down = str(np.round(media_down,precision))+' +/- '
        etiq_down = etiq_down + str(np.round(std_down,precision))
        graf_pmf.plot(x_pmfdown,y_pmfdown,
                      label='media_down '+etiq_down,
                      color='brown')
        graf_pmf.axvline(media_down,color='brown')
        graf_pmf.axvline(media_down+std_down,
                         linestyle='dotted',color='brown')
        graf_pmf.axvline(media_down-std_down,
                         linestyle='dotted',color='brown')
    
        graf_pmf.set_title('pmf: '+grafData['codigopunto']+' '+medida)
        graf_pmf.set_xlim(y_min,y_max)
        graf_pmf.set_ylim(0,prob_max)
        graf_pmf.set_xlabel(medida+' ('+medida_unidad+')')
        graf_pmf.set_ylabel('frecuencia relativa')
        graf_pmf.legend()
        graf_pmf.grid(True,linestyle='dotted',
                      axis='x', which='both')
        return(fig_pmf)
    
    def graf_puntos_std(tabla,descriptor,movAvgData,grafData):
        ''' grafica serie de std usando medias moviles
            para cada punto, media_std
        '''
        # ajuste de formato de fecha para eje x
        converter = mdates.ConciseDateConverter()
        munits.registry[np.datetime64] = converter
        munits.registry[dt.date] = converter
        munits.registry[dt.datetime] = converter
        
        # datos para grafica
        precision = grafData['precision']
        medida    = grafData['medida']
        medida_unidad = grafData['medida_unidad']
    
        movAvg_cual = movAvgData['movAvg_cual']
        movAvg_color = movAvgData['movAvg_color']
    
        # selecciona sin error
        condicion_up   = (tabla['error_up']==0)
        condicion_down = (tabla['error_down']==0)
        
        # ajuste de intervalo eje y
        y_min = 0
        y_max = np.max([2, 2*descriptor[medida+'_up']['std'],
                        2*descriptor[medida+'_down']['std']])
        # grafica
        fig_std,(graf_stdUp,graf_stdDown) = plt.subplots(2,1)
        
        # std up
        std_up = np.round(descriptor[medida+'_up']['std'],precision)
        graf_stdUp.axhline(std_up,label='std '+str(std_up),
                           color='blue')
        m = len(movAvg_cual)
        for j in range(0,m,1):
            k = str(movAvg_cual[j])
            graf_stdUp.plot(tabla[condicion_up]['publishedAt'],
                            movAvgData['movAvg_up_std'][j],
                            label='movAvg_'+k,
                            color=movAvg_color[j])
        graf_stdUp.set_ylim(y_min,y_max)
        graf_stdUp.set_ylabel('std_up ('+medida_unidad+')',
                              color='blue')
        graf_stdUp.legend()
        graf_stdUp.grid(True,linestyle='dotted',
                        axis='x', which='both')
        graf_stdUp.set_title('std: '+grafData['codigopunto']+' '+ medida)
    
        # std down
        std_down = np.round(descriptor[medida+'_down']['std'],precision)
        graf_stdDown.axhline(std_down,label='std '+str(std_down),
                             color='brown')
        for j in range(0,m,1):
            k = str(movAvg_cual[j])
            graf_stdDown.plot(tabla[condicion_down]['publishedAt'],
                              movAvgData['movAvg_down_std'][j],
                              label='movAvg_'+k,color=movAvg_color[j])
        graf_stdDown.set_ylim(y_min,y_max)
        graf_stdDown.set_xlabel('fecha')
        graf_stdDown.set_ylabel('std_down ('+medida_unidad+')',
                                color='brown')
        graf_stdDown.legend()
        graf_stdDown.grid(True,linestyle='dotted',
                          axis='x', which='both')
        plt.tight_layout()
        return(fig_std)
    
    fig_serie = graf_puntos_serie(tabla,descriptor,movAvgData,grafData)
    fig_pmf   = graf_puntos_pmf(pmf_punto,descriptor,grafData)
    fig_std   = graf_puntos_std(tabla,descriptor,movAvgData,grafData)
    
    if guarda==True:
        unarchivo = carpeta_rsm+'/serie_'+codigopunto+'.png'
        fig_serie.savefig(unarchivo)
        unarchivo = carpeta_rsm+'/pmf_'+codigopunto+'.png'
        fig_pmf.savefig(unarchivo)
        unarchivo = carpeta_rsm+'/std_'+codigopunto+'.png'
        fig_std.savefig(unarchivo)
    plt.show()
    

    Referencia: pmf, cdf en una señal de sonido. http://blog.espol.edu.ec/estg1003/senal-de-sonido-pmf-cdf/

  • 2.2. Lectura de registros en states.json de un Punto con Python

    El procesamiento de los datos históricos de "un sensor" en una ubicación se realiza con Python desde el archivo generado en la sección anterior. Esta forma se obtiene un archivo nuevo y de menor tamaño para procesar con los datos de solo el dispositivo que se propone analizar.

    states20211022.json

    El formato de la tabla es el mismo que el observado en "DB Browser".

    DB Browser 04

    Los campos a usar son principalmente:

    • "entity_id" para la selección de un sensor,
    • "attributes" donde previamente se configuró para guardar  el mensaje mqtt de cada transmisión
    • "created" que contine la fecha y hora de creación del registro. referenciada a GMT=0.

    Parametros guardados en attributes

    El campo "attributes" de cada registro contiene varios parámetros posibles a usar en el formato del mensaje. Un ejemplo del contenido se indica en el recuadro en rojo mostrado en lala figura anterior, y se presenta a continuación.

    {"applicationID":"4",
     "applicationName":"Leer_RssiSNR",
     "deviceName":"cc11Pruebas",
     "devEUI":"pT7GFa7ePxE=",
     "rxInfo":[{"gatewayID":"uCfr//4dylc=",
                "time":null,"timeSinceGPSEpoch":null,
                "rssi":-63,
                "loRaSNR":10.8,
                "channel":3,
                "rfChain":0,
                "board":0,
                "antenna":0,
                "location":{"latitude":-2.1418242484279535,
                            "longitude":-79.96711134910585,
                            "altitude":20,
                            "source":"UNKNOWN",
                            "accuracy":0},
                "fineTimestampType":"NONE",
                "context":"CmMVZA==",
                "uplinkID":"I7NWhENCQyaFtYoZIDw3KQ==",
                "crcStatus":"CRC_OK"}],
     "txInfo":{"frequency":902900000,
               "modulation":"LORA",
               "loRaModulationInfo":{"bandwidth":125,
                                     "spreadingFactor":10,
                                     "codeRate":"4/5",
                                     "polarizationInversion":false}
               },
     "adr":true,
     "dr":0,
     "fCnt":778,
     "fPort":4,
     "data":"KwcK9A8=",
     "objectJSON":"{\"Down_datarate\":10,\"Down_rssi\":-43,\"Down_snr\":7,\"bateria_V\":4.08}",
     "tags":{},
     "confirmedUplink":true,
     "devAddr":"AVqCdA==",
     "publishedAt":"2021-10-21T16:28:36.265518462Z",
     "unit_of_measurement":"dBm",
     "friendly_name":"rssi_up_cc11"
     }
    

    El formato del ejemplo mostrado se realizó con el editor de Python para facilidad de lectura,

    Procesamiento de datos para un sensor

    El procesamiento de cada registro se realiza separando los campos a usar, en una tabla los campos representan las  columnas a usar.

    La selección del dispositivos se realiza comparando con "entity_id".

    El intervalo de fechas permite usar ventanas de tiempo de observación. La fecha y hora registrada se encuentra referenciadas a GMT=0, dado que en el pais donde se realizan las muestras usa GMT=-5, se realiza un desplazamiento en las horas para facilitar la identificación de las franjas horarias.

    Los identificadores de gateways se simplifican con el diccionario gateway, que deben ser previamente identificados buscando en el archivo.json como "gatewayID". También se revisa el número de trama (fCnt) para registrar las recepción de tramas repetidas en cada gateway, registrando los valores en una lista, para posteriormente, seleccionar el valor mayor, menor, promedio).

    Los parámetros de recepción en el gateway se encuentran en "rx_datos" en el siguiente orden: rssi, loRaSNR, channel, rfChain, frequency

    El resultado de la selección de los datos se guarda en un nuevo archivo en formato.csv para facilitar la lectura de los datos con otros programas.

    Otra forma de guardar los datos en en formato.json, que puede ser leida con la librería Pandas.

    Instrucciones en Python para tabla CSV

    # Lectura de archivo.json hacia una tabla0
    # selecciona registros de "un sensor"
    # graba unreporte.csv con pandas
    # http://blog.espol.edu.ec/girni/
    
    import numpy as np
    import json
    import pandas as pd
    import datetime as dt
    import os
    
    # INGRESO
    # archivo de entrada
    unarchivoDB = "states20211022.json"
    carpeta_DB  = "BaseDatosJSON"
    
    # fecha intervalo
    fechainicio = "2021-10-21 11:00:00.0"
    fechafin    = "2021-10-22 11:50:00.0"
    zonaGMT     = -5
    
    unsensor    = "sensor.rssi_up_cc12"
    modelo_disp = "modulo" # capsula, modulo desarrollo
    ubicado     = "LOS22"
    carpeta_rsm = "resultado_Test"
    
    # gatewayID simplifica identificador 
    gatewayDB   = {'uCfr//4dylc=':'Gw03'}
    
    # PROCEDIMIENTO -----
    campos = ['domain','entity_id','state','attributes','created']
    
    # Lectura de archivo json, cambia a formato DataFrame
    tabla0 = pd.DataFrame()
    archivo_ruta  = carpeta_DB + '/' + unarchivoDB
    archivoexiste = os.path.exists(archivo_ruta)
    if archivoexiste:
        tabla0 = pd.read_json(archivo_ruta)
        tabla0 = pd.DataFrame(tabla0,columns=campos)
    else:
        print(' ERROR: NO se encuentra el archivo...')
        print('        revise el nombre de archivo. ')
    
    # Revisa cada registro 
    leidos = 0
    tabla  = {}
    if archivoexiste:
        # Intervalo de fecha como datetime
        hora_desplaza  = dt.timedelta(hours = abs(zonaGMT))
        fechaformatoDB = '%Y-%m-%dT%H:%M:%S.%f'
        fechaformato   = '%Y-%m-%d %H:%M:%S.%f'
        fechatxt     = fechainicio[0:np.min([26,len(fechainicio)])]
        fechainicio  = dt.datetime.strptime(fechatxt,fechaformato)
        fechatxt     = fechafin[0:np.min([26,len(fechafin)])]
        fechafin     = dt.datetime.strptime(fechatxt,fechaformato)
    
        # datos de trama
        for cadaregistro in tabla0.index:
            unatrama   = tabla0['attributes'][cadaregistro]
            trama_mqtt = json.loads(unatrama)
            
            # selecciona registro por sensor, en fecha y con datos
            cualsensor = (tabla0['entity_id'][cadaregistro] == unsensor)
            
            unafecha = tabla0['created'][cadaregistro]
            unafecha = unafecha[0:np.min([26,len(unafecha)])]
            unafecha = dt.datetime.strptime(unafecha,fechaformato)
            unafecha = unafecha - hora_desplaza
            
            enfecha  = (unafecha>=fechainicio) and (unafecha<=fechafin)
            condatos = 'applicationID' in trama_mqtt.keys()
            
            selecciona = cualsensor and condatos and enfecha
            if (selecciona == True):        
                # datos en la mensaje MQTT
                publishedAt = trama_mqtt["publishedAt"]
                publishedAt = publishedAt[0:np.min([26,len(publishedAt)])]
                publishedAt = dt.datetime.strptime(publishedAt,fechaformatoDB)
                publishedAt = publishedAt - hora_desplaza
                
                friendly_name = trama_mqtt["friendly_name"]
                deviceName    = trama_mqtt["deviceName"]
                dispositivo   = friendly_name.split('_')[2]
                
                dr      = trama_mqtt["dr"]
                fCnt    = trama_mqtt["fCnt"]
                fPort   = trama_mqtt["fPort"]
                freq_tx = trama_mqtt["txInfo"]["frequency"]
                bandwidth = trama_mqtt["txInfo"]["loRaModulationInfo"]["bandwidth"]
                spreadingFactor = trama_mqtt["txInfo"]["loRaModulationInfo"]["spreadingFactor"]
    
                objectJSON = trama_mqtt["objectJSON"]
                datosensor = json.loads(objectJSON)
                     
                datostrama = {"publishedAt": publishedAt,
                              "dispositivo": dispositivo,
                              "fCnt": fCnt}
                
                # revisa gateway en la red LoRaWan
                tamano = len(trama_mqtt["rxInfo"])
                i = 0
                while i<tamano:
                    gatewayID = trama_mqtt["rxInfo"][i]["gatewayID"]
                    rssi      = trama_mqtt["rxInfo"][i]["rssi"]
                    loRaSNR   = trama_mqtt["rxInfo"][i]["loRaSNR"]
                    channel   = trama_mqtt["rxInfo"][i]["channel"]
                    rfChain   = trama_mqtt["rxInfo"][i]["rfChain"]
                    gtwNum    = gatewayDB[gatewayID]
                    
                    # registra en tabla, incluyendo tramas repetidas
                    datostrama['rssi_up']    = rssi
                    datostrama['snr_up']     = loRaSNR
                    datostrama['channel_up'] = channel
                    datostrama['rfChain_up'] = rfChain
                    datostrama['gtw_rx']     = gtwNum
                    i = i + 1
    
                # dato del sensor
                equivale = {'Down_datarate': 'dr_down',
                            'Down_rssi'    : 'rssi_down',
                            'Down_snr'     : 'snr_down',
                            'bateria_V'    : 'bateria_V'}
                for undato in datosensor:
                    if undato in equivale.keys():
                        datostrama[equivale[undato]] = datosensor[undato]
                    else:
                        datostrama[undato] = datosensor[undato]
    
                # datos, otros valores
                datostrama["frequency"] = freq_tx
                datostrama["bandwidth"] = bandwidth
                datostrama["spreadingFactor"] = spreadingFactor
                datostrama["fPort"]   = fPort
                datostrama["dr"]      = dr
                datostrama["created"] = unafecha
    
                leidos = leidos + 1
                
                # revisa registro repetido
                repetido = False
                if leidos>1:
                    trama_num  = (fCnt == tabla[leidos-1]['fCnt'])
                    fecha_pub  = (publishedAt == tabla[leidos-1]['publishedAt'])
                    gtwNum_rep = (gtwNum == tabla[leidos-1]['gtw_rx'])
                    repetido   = (trama_num and fecha_pub and gtwNum_rep)
                if not(repetido):
                    tabla[leidos] = datostrama
                else:
                    leidos = leidos - 1
    
        # convierte diccionario a DataFrame
        if len(tabla)>0:
            tabla = pd.DataFrame(tabla)
            tabla = tabla.T
            # añade ubicación y modelo (capsula o modulo desarrollo)
            tabla.insert(1,'ubicado',ubicado)
            tabla.insert(2,'modelo_disp',modelo_disp)
            
    # directorio de resultados
    if len(carpeta_rsm) > 0:
        if not(os.path.exists(carpeta_rsm)):
            os.makedirs(carpeta_rsm)
        carpeta_rsm = carpeta_rsm + '/'
    
    # SALIDA
    print('registros leidos:     ', len(tabla0))
    print('registros procesados: ', leidos)
    print(tabla)
    
    # guarda el reporte en csv
    if len(tabla)>0:
        unreporte = carpeta_rsm+'data_'+ubicado+'.csv'
        tabla.to_csv(unreporte)
    

    el resultado de puede visualiza como:

    registros leidos:      7090
    registros procesados:  98
                      publishedAt ubicado modelo_disp dispositivo  ... spreadingFactor fPort dr                    created
    1  2021-10-21 11:34:07.079188   LOS22      modulo        cc12  ...              10     4  0 2021-10-21 11:34:07.105036
    2  2021-10-21 11:49:07.862255   LOS22      modulo        cc12  ...              10     4  0 2021-10-21 11:49:07.876219
    3  2021-10-21 12:04:07.931128   LOS22      modulo        cc12  ...              10     4  0 2021-10-21 12:04:07.943781
    4  2021-10-21 12:19:08.489097   LOS22      modulo        cc12  ...              10     4  0 2021-10-21 12:19:08.504848
    5  2021-10-21 12:34:08.479274   LOS22      modulo        cc12  ...              10     4  0 2021-10-21 12:34:08.494229
    ..                        ...     ...         ...         ...  ...             ...   ... ..                        ...
    94 2021-10-22 10:49:46.742877   LOS22      modulo        cc12  ...              10     4  0 2021-10-22 10:49:46.756601
    95 2021-10-22 11:04:47.636628   LOS22      modulo        cc12  ...              10     4  0 2021-10-22 11:04:47.652299
    96 2021-10-22 11:19:48.310535   LOS22      modulo        cc12  ...              10     4  0 2021-10-22 11:19:48.331143
    97 2021-10-22 11:34:49.209626   LOS22      modulo        cc12  ...              10     4  0 2021-10-22 11:34:49.224717
    98 2021-10-22 11:49:49.638194   LOS22      modulo        cc12  ...              10     4  0 2021-10-22 11:49:49.651398
    
    [98 rows x 20 columns]
    >>>

    y el archivo resultante se presenta como:

    data_LOS22.csv


    Instrucciones en Python para tabla JSON

    # Lectura de archivo.json hacia una tabla1
    # selecciona registros de "un sensor"
    # graba unreporte.json con pandas
    
    import numpy as np
    import json
    import pandas as pd
    import datetime as dt
    
    # INGRESO
    
    # archivo de entrada
    unarchivo   = 'states20211022.json'
    
    fechainicio = '2021-10-21 19:00:00.0'
    fechafin    = '2021-10-22 17:10:00.0'
    
    unsensor    = 'sensor.rssi_up_cc22'
    # archivo de salida
    unreporte   = 'tramas20211022_cc12.json'
    
    # PROCEDIMIENTO -----
    # columnas o campos a usar
    campos    = ['domain','entity_id','state',
                 'attributes','created']
    # gatewayID simplifica identificador 
    gateway   = {'uCfr//5zvhk=':'Gw01',
                 'uCfr//4dylc=':'Gw03'}
    
    # Lectura de archivo json
    tabla0 = pd.read_json(unarchivo)
    # Cambia a formato DataFrame
    tabla0 = pd.DataFrame(tabla0,columns=campos)
    
    # Revisa cada registro 
    leidos = 0
    tabla  = {}
    
    # Intervalo de fecha como datetime
    formato     = '%Y-%m-%d %H:%M:%S.%f'
    fechainicio = fechainicio[0:np.min([26,len(fechainicio)])]
    fechainicio = dt.datetime.strptime(fechainicio,formato)
    fechafin    = fechafin[0:np.min([26,len(fechafin)])]
    fechafin    = dt.datetime.strptime(fechafin,formato)
    
    # datos de trama
    for cadaregistro in tabla0.index:
        unatrama   = tabla0['attributes'][cadaregistro]
        trama_mqtt = json.loads(unatrama)
        
        # selecciona registro por sensor, en fecha y con datos
        cualsensor = (tabla0['entity_id'][cadaregistro] == unsensor)
        
        unafecha = tabla0['created'][cadaregistro]
        unafecha = unafecha[0:np.min([26,len(unafecha)])]
        unafecha = dt.datetime.strptime(unafecha,formato)
        
        enfecha  = (unafecha>=fechainicio) and (unafecha<=fechafin)
        condatos   = 'applicationID' in trama_mqtt.keys()
        
        selecciona = cualsensor and condatos and enfecha
        if (selecciona == True):
            leidos =  leidos + 1
            
            # datos en la mensaje MQTT
            publishedAt = trama_mqtt["publishedAt"]
            friendly_name = trama_mqtt["friendly_name"]
            deviceName    = trama_mqtt["deviceName"]
            dr    = trama_mqtt["dr"]
            fCnt  = trama_mqtt["fCnt"]
            fPort = trama_mqtt["fPort"]
    
            objectJSON  = trama_mqtt["objectJSON"]
            datosensor  = json.loads(objectJSON)
            
            frequency   = trama_mqtt["txInfo"]["frequency"]
            bandwidth   = trama_mqtt["txInfo"]["loRaModulationInfo"]["bandwidth"]
            spreadingFactor = trama_mqtt["txInfo"]["loRaModulationInfo"]["spreadingFactor"]
                 
            datostrama = {"publishedAt":publishedAt,
                          "fPort":fPort, "dr": dr,"fCnt": fCnt,
                          "frequency":  frequency,
                          "bandwidth":  bandwidth,
                          "spreadingFactor": spreadingFactor,
                          "datosensor":datosensor}
            
            # revisa por cada gateway en la red LoRaWan
            tamano = len(trama_mqtt["rxInfo"])
            i = 0
            while i<tamano:
                gatewayID = trama_mqtt["rxInfo"][i]["gatewayID"]
                rssi      = trama_mqtt["rxInfo"][i]["rssi"]
                loRaSNR   = trama_mqtt["rxInfo"][i]["loRaSNR"]
                channel   = trama_mqtt["rxInfo"][i]["channel"]
                rfChain   = trama_mqtt["rxInfo"][i]["rfChain"]
                gtwNum    = gateway[gatewayID]
                i = i+1
                
                # registra en tabla, incluyendo tramas repetidas
                rx_datos = [rssi, loRaSNR,
                            channel, rfChain,frequency/1e6]  
                if not(gtwNum in datostrama.keys()):
                    datostrama[gtwNum] = [rx_datos]
                else:
                    if not(rx_datos in datostrama[gtwNum]):
                        datostrama[gwNum].append(rx_datos)
            dispositivo = friendly_name.split('_')[2]
    
            # revisa registro repetido
            disp_existe = dispositivo in tabla
            if not(disp_existe):
                tabla[dispositivo] = {leidos:datostrama}
            else:
                trama_num = (fCnt == tabla[dispositivo][leidos-1]['fCnt'])
                fecha_pub = (publishedAt == tabla[dispositivo][leidos-1]['publishedAt'])
                repetido  = (trama_num and fecha_pub)
                if not(repetido):
                    tabla[dispositivo][leidos] = datostrama
                else:
                    leidos = leidos - 1
    
    # convierte diccionario en DataFrame
    tabla = pd.DataFrame(tabla)
    
    # SALIDA
    print('registros leidos:     ',len(tabla0))
    print('registros procesados: ', leidos)
    
    # guarda el reporte en json
    tabla.to_json(unreporte)
    

    El procesamiento de los datos se puede realizar con librerias como Pandas.

    En el procesamiento de registros, se descarta el registro de "join".

    Una muestra del reporte es:

    registros leidos:      7090
    registros procesados:  98
    >>> tabla.keys()
    Index(['cc12'], dtype='object')
    >>> tabla['cc12'].keys()
    Int64Index([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17,
                18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34,
                35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51,
                52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68,
                69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85,
                86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98],
               dtype='int64')
    >>> tabla['cc12'][2].keys()
    dict_keys(['publishedAt', 'fPort', 'dr', 'fCnt',
     'frequency', 'bandwidth', 'spreadingFactor', 'datosensor',
     'Gw03'])
    >>> 
    
    >>> tabla['cc12'][2]['datosensor'].keys()
    dict_keys(['Down_datarate', 'Down_rssi', 'Down_snr',
     'bateria_V'])
    
    >>> tabla['cc12'][2]['Gw03']
    [[-84, 8.5, 1, 0, 902.5]]
    

    Esta forma se obtiene un nuevo y mas pequeño archivo para procesar con los datos de solo el dispositivo que se propone analizar.

    tramas20211022_cc12.json

    Referencia: Archivos.json – Pandas en blog de Fundamentos de Programación

  • 2.1 Home-assistant.db a JSON o CSV

    Descarga remota de archivo home-assistant_v2.db

    Para facilitar el procesamiento de los registros, se obtiene una copia de forma remota del archivo de la base de datos, usando una instruccion en la línea de comandos de windows.

    scp pi@192.168.xx.xx:/home/homeassistant/.homeassistant/home-assistant_v2.db D:\Downloads

    En la primera parte se indica el usuario 'pi' en la dirección '192.168.xx.xx', la última parte de la instrucción indica el directorio de descarga local en la máquina desde que se conecta.

    Al ejecutar la instrucción se requerirá la contraseña asignada al usuario 'pi' u otro asignado. De ser aceptada la conexión, se inicia el proceso de descarga del archivo.

    Cambio de formato.db a archivo.json con DB Browser

    Referencia: https://sqlitebrowser.org/

    Para facilitar lectura del archivo se puede usar un programa como "DB Browser" que permite abrir el archivo.db y observar los registros de la tabla de estados ('states'), asi como exportar en formato ".csv" o ".json".

    Si se ha almacenado en atributos los datos de cada trama, se prefiere usar el formato 'json' y la librería json de Python para leer, seleccionar  procesar los registros.

    Seleccione la tabla "states" para disponer de los registro de estado de sensores en un nuevo archivo.json.

    El resultado debe ser un archivo semejante al presentado como ejemplo

    states20211022.json

    Referencia: Base de datos de Home-assistant. https://www.home-assistant.io/docs/backend/database/

  • 2. LoRaWan - Registro de los datos de sensor

    Los datos de dispositivo sensor o actuador son enviados hacia un gestor de datos como Home-assistant. Dadas las condiciones de limitado acceso a internet en en el sector agrícola, es posible descargar los registros para su posterior análisis desde un computador conectado a la misma red de datos ‘LAN’.

    Los datos de sensor se registran en Home-Assistant en la base de datos SQL.

    En la situación presentada sin acceso a internet, el acceso a los datos se realiza de forma local y periódica copiando los datos del archivo Home-assistant_v2.db. La copia del archivo se puede realizar:

    - usando una memoria USB en el servidor "Raspberry Pi" o

    - mediante una conexión para copia desde una laptop conectada en la misma red Lan, por ejemplo mediante WiFi.

    La lectura del archivo Home-assistant_v2.db. puede realizarse usando un software como DBbrowser y exportarlo en formato csv o json.

    El proceso de análisis de datos se lo realiza usando una hoja electrónica o un algoritmo en Python.

    En las siguientes secciones se desarrollan los detalles de los pasos.

  • 1. LoRaWan - Esquema abierto en entorno agrícola

    Los entornos agrícolas se determinan como un área de interés para minimizar el uso de recursos limitados como el agua. El control y automatización de uso de los recursos puede ser elemento diferenciador en las cosechas.

    El sector de agrícola en los países en vias de desarrollo debe considerar el entorno que:

    • Se encuentra conformado por áreas extensas de vegetación (cultivos) y con niveles de terreno irregulares.
    • La adopción tecnologías para control y automatización es muy bajo, por el costo elevado (relativo) y difusión.
    • El acceso a internet se encuentra limitado por la cobertura en áreas de baja densidad poblacional.

    En tales condiciones, IoT con esquemas abiertos de software y hardware conforman una alternativa en la forma que permita:

    • Escalabilidad al iniciar pruebas de algunos componentes que luego pueden ser ampliados en cantidad y cobertura.
    • Integración de componentes de software y hardware abierto.
    • Flexibilidad en la adopción y aprendizaje para los usuarios, operadores, que va de la mano con desarrolladores e integradores

    Esquema agrícola basado en IoT

    Las plataformas IoT son un punto de partida para este tipo de tecnologías de control y monitoreo en el área agrícola al utilizar los mismos elementos de gestión y control.

    La gestión de datos, mensajería y redes de datos del esquema básico IoT se extiende usando otras redes inalámbricas.

    En las sección de Broker/Gestión-IoT se detallan las instrucciones para implementar estos componentes usando Raspberry Pi. Éstos componentes permiten la gestión de IoT de forma 'local' , en condiciones donde el acceso a internet es limitado o nulo,

    LoRa ofrece una amplia cobertura y bajo consumo energético que se adapta a las condiciones de un entorno agrícola. La tecnología LoRa se incorpora al esquema IoT como LoRaWan mediante el uso de gateways.

    La gestión de gateways  en esquema abierto es posible con el software ChirpStack. La gestión de datos es posible usando Home-Assistant al interconectar los  mensajes de datos con Mosquitto-MQTT.

    En esquemas abiertos, el gateway se implementa usando módulos para gateways, tales como Heltec HT-M01.

  • LoRaWan - Enlaces Up/Down Decodificador en Chirpstack y HA

    Las instrucciones para interpretar la trama en el gestor de gateways  se realizan en JavaScript, siguiendo el mismo orden de bytes realizado en el dispositivo.

    Con esta parte se habilita la lectura de los parámetros para gestionar los datos en HomeAssistant mediante un mensaje Mqtt.

    function Decode(fPort, bytes, variables) {
      var Down_rssi = -1*parseInt(bytes[0]);
      var Down_snr = bytes[1];
      var Down_datarate = bytes[2];
      // usando entero
      var unalectura = (bytes[4] << 8) |(bytes[3]);
      unalectura = (unalectura/1000)
      unalectura = +unalectura.toFixed(2);
      var appData = {'Down_rssi':Down_rssi,
                     'Down_snr':Down_snr,
                     'Down_datarate':Down_datarate,
                     'bateria_V': unalectura}
      return appData;
    }
    

    función de codificación hacia el dispositivo, Reenvía al dispositivo el Rssi de Uplink del paquete anterior

    function Encode(fPort, obj) {
      var data = new Array();
      data[0] = -1*parseInt(obj["Up_rssi"])
      return data;
    }
    

    Automatización en Home-Assistant

    para reenviar el Rssi de UpLink hacia el dispositivo para registrar valores en el punto de muestra:

    alias: cc01UpRssi_reenviar
    description: reenviar cc01 Up_rssi al dispositivo
    trigger:
      - platform: state
        entity_id: sensor.rssi_up_cc01
    action:
      - service: mqtt.publish
        data_template:
          topic: application/1/device/a53ec615aede3f01/command/down
          payload_template: >-
            {"confirmed":false,"fPort":3,"object":{"Up_rssi":{{
            trigger.to_state.state }}}}
    
  • LoRaWan - Enlaces Up/Down Archivo.ino

    Preparación de trama

    La trama se configura usando los parametros obtenidos en el dispositivo con una trama de confirmación de subida (Ack). El primer valor enviado será cero, puesto que no se dispone de parámetros iniciales.

    • Rssi Downlink (1 byte)
    • Snr Downlink (1 byte)
    • Datarate Downlink (1 byte)
    • Voltaje de bateria (2 bytes)
    // Ack parametros de recepción
    uint8_t confirmaRssi = 0;
    uint8_t confirmaSnr = 0;
    uint8_t confirmaDatarate = 0;
    

    A Septiembre del 2021, se usan las librerías publicadas para el módulo HELTEC Cubecell Board-Plus HTCC-AB02. Para obtener los parámetros de la trama de confirmación de recibido (Ack) el gateway para una trama de subida de datos (Uplink) se sustituye el procedimiento:

    downLinkAckHandle(McpsIndication_t *mcpsIndication)

    Las instrucciones para el manejo de LoraWan se pueden revisar en:

    https://github.com/HelTecAutomation/CubeCell-Arduino/blob/master/libraries/LoRa/src/LoRaWan_APP.cpp

    El archivo LoRaWan_APP.cpp se encuentra instalado en el directorio de windows:

    C:\Users\MiUsuario\AppData\Local\Arduino15\packages
      \CubeCell\hardware\CubeCell\1.3.0\libraries\LoRa\src

    desde donde es posible complementar las instrucciones usando un editor de texto en las líneas corespondientes:

    void __attribute__((weak)) downLinkAckHandle()
    {
    	//printf("ack received\r\n");
    }
    

    para indicar los parámetros a usar al recibir la trama Ack:

    void __attribute__((weak)) downLinkAckHandle(McpsIndication_t *mcpsIndication)
    {
    	//printf("ack received\r\n");
    }
    

    adicionalmente, en el mismo archivo, más adelante:

    static void McpsIndication( McpsIndication_t *mcpsIndication )
    ....
    	if(mcpsIndication->AckReceived)
    	{
    		downLinkAckHandle(mcpsIndication);
    	}

    con lo que es posible usar las instrucciones del dispositivo para obtener los parámetros indicados para la trama de confirmación de recibo (Ack)

    Parámetros de trama de confirmación de recibido - Ack

    LoRaWan Enlace UpDown Parametros Ack

    void downLinkAckHandle(McpsIndication_t *mcpsIndication){
      confirmaRssi = uint8_t(abs(mcpsIndication->Rssi));
      confirmaSnr  = uint8_t(mcpsIndication->Snr);
      confirmaDatarate = uint8_t(mcpsIndication->RxDoneDatarate);
      Serial.print(" ack received(rssi,snd,datarate): -");
      Serial.print(confirmaRssi);Serial.print(" ,");
      Serial.print(confirmaSnr);Serial.print(" ,");
      Serial.println(confirmaDatarate);
    }
    

    con los parámetros obtenidos y añadiendo el voltaje de la batería, se conforma la trama a enviar.

    LoRaWan Enlace UpDown Enviar

    /* Prepares the payload of the frame */
    static void prepareTxFrame( uint8_t port ) {
      // enciende sensor
      pinMode(Vext, OUTPUT);
      digitalWrite(Vext, LOW);
      
      //Lectura de Sensor
    
      // apaga sensor
      digitalWrite(Vext, HIGH);
      
      // lectura de bateria  
      uint16_t batteryVoltage = getBatteryVoltage();
      unsigned char *puc;
    
      // trama
      appDataSize = 5;
      appData[0] = confirmaRssi; //Ack leido en dispositivo
      appData[1] = confirmaSnr;
      appData[2] = confirmaDatarate;
      appData[3] = (uint8_t)batteryVoltage;
      appData[4] = (uint8_t)(batteryVoltage>>8);
    
      Serial.print("%, Bateria = ");
      Serial.println(batteryVoltage);
    }
    

    Instrucciones Principales

    LoRaWan Enlace UpDown Principal

    #include "LoRaWan_APP.h"
    #include "Arduino.h"
    
    /* set LoraWan_RGB to Active,the RGB active in loraWan
     * red   |sending;   purple | joined done;
     * blue  |RxWindow1; yellow | means RxWindow2;
     * green | received done;
     */
    /* Conexión LoRa: OTAA parametros*/
    uint8_t devEui[] = { 0xa6, 0x17, 0x74, 0xe9, 0x5c, 0x1c, 0x98, 0xbd };
    uint8_t appEui[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
    uint8_t appKey[] = { 0x76, 0x9d, 0x1b, 0xc3, 0xf9, 0xe6, 0x7b, 0xbd,
                         0xa3, 0x4d, 0xe3, 0xcf, 0xbc, 0x8e, 0x35, 0x8f };
    /* ABP parametros*/
    uint8_t nwkSKey[] = { 0x15, 0xb1, 0xd0, 0xef, 0xa4, 0x63, 0xdf, 0xbe,
                          0x3d, 0x11, 0x18, 0x1e, 0x1e, 0xc7, 0xda,0x85 };
    uint8_t appSKey[] = { 0x47, 0xdc, 0xac, 0x5f, 0xc2, 0x32, 0x24, 0x31, 
                          0xdf, 0xf1, 0xff, 0xf9, 0x46, 0xe5, 0x2e, 0x17 };
    uint32_t devAddr =  ( uint32_t )0x007bc4af;
    /*LoraWan channelsmask, default channels 0-7*/ 
    uint16_t userChannelsMask[6]={ 0x00FF,0x0000,0x0000,0x0000,0x0000,0x0000 };
    
    /*Select in arduino IDE tools*/
    LoRaMacRegion_t loraWanRegion = ACTIVE_REGION;
    DeviceClass_t  loraWanClass = LORAWAN_CLASS;
    bool overTheAirActivation = LORAWAN_NETMODE;
    bool loraWanAdr = LORAWAN_ADR;
    bool keepNet = LORAWAN_NET_RESERVE;
    bool isTxConfirmed = LORAWAN_UPLINKMODE;
    
    uint32_t appTxDutyCycle = 1*15*1000;
    uint8_t appPort = 2; /* Application port */
    /* trials to transmit frame, if didn't receive ack.
     * The MAC performs a datarate adaptation,
     * Tx nb|Data Rate
     * -----|----------
     * 1    |DR           * 5    | max(DR-2,0)
     * 2    |DR           * 6    | max(DR-2,0)
     * 3    |max(DR-1,0)  * 7    | max(DR-3,0)
     * 4    |max(DR-1,0)  * 8    | max(DR-3,0)
    */
    uint8_t confirmedNbTrials = 4;
    
    // Ack parametros de recepción
    uint8_t confirmaRssi = 0;
    uint8_t confirmaSnr = 0;
    uint8_t confirmaDatarate = 0;
    
    uint8_t itera = 0;
    
    void setup() {
    	Serial.begin(115200);
    #if(AT_SUPPORT)
    	enableAt();
    #endif
      LoRaWAN.displayMcuInit();
    	deviceState = DEVICE_STATE_INIT;
    	LoRaWAN.ifskipjoin(); //if joinned,skip
    }
    
    void loop() {
      Serial.print(".");
      itera = itera + 1;
      if (itera>6){
        itera = 0;
        Serial.println(" ");
      }
    	switch( deviceState ) {
    		case DEVICE_STATE_INIT: {
    #if(LORAWAN_DEVEUI_AUTO)
    			LoRaWAN.generateDeveuiByChipID();
    #endif
    #if(AT_SUPPORT)
    			getDevParam();
    #endif
    			printDevParam();
    			LoRaWAN.init(loraWanClass,loraWanRegion);
    			deviceState = DEVICE_STATE_JOIN;
    			break;
    		}
    		case DEVICE_STATE_JOIN: {
          LoRaWAN.displayJoining();
    			LoRaWAN.join();
    			break;
    		}
    		case DEVICE_STATE_SEND:	{
          LoRaWAN.displaySending();
    			prepareTxFrame( appPort );
    			LoRaWAN.send();
    			deviceState = DEVICE_STATE_CYCLE;
    			break;
    		}
    		case DEVICE_STATE_CYCLE: {
    			// Schedule next packet transmission
    			txDutyCycleTime = appTxDutyCycle + randr( 0, APP_TX_DUTYCYCLE_RND );
    			LoRaWAN.cycle(txDutyCycleTime);
    			deviceState = DEVICE_STATE_SLEEP;
    			break;
    		}
    		case DEVICE_STATE_SLEEP: {
          LoRaWAN.displayAck();
    			LoRaWAN.sleep();
    			break;
    		}
    		default: {
    			deviceState = DEVICE_STATE_INIT;
    			break;
    		}
    	}
    }
    

    y en el caso de recibir instrucciones para el dispositivo, se dispone de un ejemplo:

    //downlink data handle function example
    void downLinkDataHandle(McpsIndication_t *mcpsIndication){
      // revisa parametros
      Serial.print("\nLLEGo un mensaje para dispositivo...");
      Serial.print("Rssi: ");
      Serial.println(mcpsIndication->Rssi);
      
      Serial.printf("+REV DATA:%s,RXSIZE %d,PORT %d\r\n",
      mcpsIndication->RxSlot?"RXWIN2":"RXWIN1",
      mcpsIndication->BufferSize,mcpsIndication->Port);
      Serial.print("+REV DATA:");
      for(uint8_t i=0;i<mcpsIndication->BufferSize;i++)
      {
        Serial.printf("%02X",mcpsIndication->Buffer[i]);
      }
      Serial.println();
      uint32_t color=mcpsIndication->Buffer[0]<<16|mcpsIndication->Buffer[1]<<8|mcpsIndication->Buffer[2];
    #if(LoraWan_RGB==1)
      turnOnRGB(color,5000);
      turnOffRGB();
    #endif
    }
    
  • LoRaWan - Enlaces Up/Down Parámetros

    Para realizar pruebas de conectividad y calidad del enlace inalámbrico, se propone usar dispositivos con una configuración básica que registre los parámetros del enlace de subida y bajada.

    Los parámetros para subida (Uplink) se obtienen desde el gateway, de los parámetros regulares que registra el gateway.

    Los parámetros de bajada (Downlink) se obtienen de la trama de confirmación (Acknowlegment) que son Rssi, Snr y datarate. Estos parámetros se usan en el próximo envío de datos (Uplink) por lo que presentan un atraso equivalente al intervalo de lectura de datos del dispositivo.

    El dispositivo para la prueba se implementa con un módulo de desarrollo HELTEC Cubecell Board-Plus HTCC-AB02.

  • 1. IDE Arduino con HELTEC CubeCell LoRa

    Referencia: LoRa Heltec CubeCell https://heltec-automation-docs.readthedocs.io/en/latest/cubecell/quick_start.html

    1. Lora HELTEC CubeCell–  Incluir entre las tarjetas del IDE

    Lora Heltec CubeCell 01

    La placa de desarrollo incluye el circuito para usar un panel solar y una batería, facilitando la fase de desarrollo de dispositivos.

    Además como controlador PSoC® 4000 series MCU (ARM® Cortex® M0+ Core) que es de menor consumo energético que un ESP32.

    1.1 Semejante al procedimiento para añadir la versión ESP32+LoRa, se debe añadir en la sección Archivo/Preferencias/“Gestor de URL’s Adicionales de Tarjetas” la dirección:

    https://github.com/HelTecAutomation/CubeCell-Arduino/releases/download/V1.3.0/package_CubeCell_index.json

    añadir en gestor de placas arduino IDE

    1.2 En el menú de Herramientas, Placas, Gestor de tarjetas, se añade “CubeCell Development Framework”,

    añadir Cubecell development framework

     

    con lo que será posible seleccionar la placa y configurar los parámetros de uso, como LoRaWan_Region y LoraWan_Class necesarias para la configuración y operación del dispositivo.

    gestor de placas IDE Cubecell development framework

    una vez seleccionada la placa, se muestra en el menú los detalles de la placa:

    Arduino IDE Gestor Placas 04

    Con lo que se está listo para trabajar con la placa y las instrucciones para añadir.