GIRNI

Grupo de Investigación de Redes de iNformación Inalámbricas

Autor: Edison Del Rosario

  • 2.2 Datos.csv - Analizar con Python

    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ámetros 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. https://blog.espol.edu.ec/algoritmos101/stp-aplica/senal-sonido-pmf-cdf/

  • 2.1 States.json - extraer datos.csv 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.

    Parámetros 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 país 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 leída 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 librerías 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 - csv, json con Pandas en Fundamentos de Programación

  • 2. Registros.db - Obtener desde Home Assistant



    2.1 Registro de datos del 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 con:

    - Una memoria USB conectada en el servidor "Raspberry Pi"

    - Copia del archivo por medio de la red LAN (Ethernet/Wifi)

    La lectura del archivo Home-assistant_v2.db. puede realizarse usando DBbrowser y luego 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.



    2.2 Copia 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 instrucción 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.



    2.3 Formato.db convertir a.json

    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'), así 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/



  • 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 vías 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 HT1303 o HT-M01.

  • LoRaWan - Parámetros Up/Down: 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] &lt;&lt; 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 - Parámetros Up/Down: Archivo.ino

    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.

    Preparación de trama

    La trama se configura usando los parámetros 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
    }
    
  • 1. Taller Lora - IDE Arduino con CubeCell

    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.

  • Proyectos

    Proyectos realizados o en desarrollo sobre tecnologías inalámbricas, plataformas tecnológicas.

     

  • LoRaWan - Interruptor temporizado: Gateway y Broker

    Gateway ChirpStack

    La función Encode se encarga de preparar los valores para ser enviados por la red LoRaWan. El ejemplo muestra el uso de las variables estado y duración:

    // Encode encodes the given object into an array of bytes.
    //  - fPort contains the LoRaWAN fPort number
    //  - obj is an object, e.g. {"temperature": 22.5}
    //  - variables contains the device variables e.g. {"calibration": "3.5"} (both the key / value are of type string)
    // The function must return an array of bytes, e.g. [225, 230, 255, 0]
    function Encode(fPort, obj, variables) {
      var estado = obj["estado"];
      var duracion = obj["duracion"];
      var mensaje = [50,20];
      if (estado == "ON") {
      	mensaje = [49,duracion];
      }
      if (estado == "OFF") {
      	mensaje = [50,20];
      }
      return mensaje;
    }
    

    Mensaje MQTT

    El mensaje Mqtt que activa la instrucción tiene un formato que requiere para el tema o tópico: identificador de aplicación del ChirpStack, deviceEUI,

    el mensaje contiene si se confirma la trama, el puerto de la aplicación y un diccionario "object" con las variables  sus valores correspondientes en texto o numérico.

    El ejemplo de la instruccion para el dispositivo es:

    mosquitto_pub -h "localhost" -u "usuarioprueba" -P "usuarioclave" -t "application/3/device/bc55318912bfd090/command/down" -m '{"confirmed":true,"fPort":3,"object":{"estado":"ON","duracion":46}}'

    Para observar el estado en mqtt para probar el envio del mensase se  usa la instrucción de suscripción:

    mosquitto_sub -h "localhost" -u "usuarioprueba" -P "usuarioclave" -t "application/3/device/bc55318912bfd090/command/down"

    Home Assitant - configuración

    Se realiza combinando una variable numérica (input_number) para la duración en segundos, y se envía como una instrucción de control usando una plantilla de foco dim (light).

    light:
      - platform: mqtt
        schema: template
        name: 'RegarPlanta'
        state_topic: "application/3/device/bc55318912bfd090/estado"
        command_topic: "application/3/device/bc55318912bfd090/command/down"
        command_on_template:  '{"confirmed":true,"fPort":3,"object":{"estado":"ON","duracion":{{states("input_number.regar_seg") | int}}}}'
        command_off_template: '{"confirmed":true,"fPort":3,"object":{"estado":"OFF","duracion":0}}'
    
    input_number:
      regar_seg:
        name: regar_segundos
        initial: 10
        min: 0
        max: 254
        step: 1
        mode: box
        icon: mdi:timer-outline
        unit_of_measurement: "s"
    

    Referencia: Send JSON command with MQTT?, https://community.home-assistant.io/t/send-json-command-with-mqtt/37663

    Input Number, https://www.home-assistant.io/integrations/input_number

    MQTT Light, https://www.home-assistant.io/integrations/light.mqtt/

  • LoRaWan - Interruptor temporizado: Archivo.ino

    Un dispositivo como actuador requiere recibir un mensaje de control desde el broker para establecer el estado del artefacto o cosa. En el caso de LoRaWan el mensaje debe gestionarse por el gateway para transformar un mensaje Mqtt enviado al formato de bytes usado en la red LoRa, es decir un Encoder.

    En el dispositivo LoRa se requiere al menos una función para recibir los mensajes, interpretación de los bytes recibidos y con éstos actualizar el estado de los pines de salidas usados como control.

    Datos de control

    Lo primordial en el dispositivo es el pin de control. Para el ejemplo inicial se usa el LED incorporado en el módulo de desarrollo Pin25.

    // actuador
    #define LEDPin 25  //LED light
    boolean estado = false;  //estado dispositivo
    

    Un dato complementario es la duración del actuador en estado encendido, es un factor necesario en el sistema de riego, pues se debería usar solo por periodos cortos de tiempo. Se aprovecha el modo de ahorro de energía del dispositivo en contraste con los largos periodos sin riego.

    Intrucciones Principales

    A las instrucciones principales se añaden las líneas para la declaración de las variables. El resto de operaciones se las añade como funciones.

    /* HelTec Automation(TM) LoRaWAN 1.0.2 OTAA example use OTAA, CLASS A
     * Solo ESP32+LoRa series boards con licencia http://www.heltec.cn/search/);
     *https://github.com/HelTecAutomation/ESP32_LoRaWAN
    */
    #include <ESP32_LoRaWAN.h>
    #include "Arduino.h"
    
    /*licencia Heltec ESP32 LoRaWan http://resource.heltec.cn/search */
    uint32_t  license[4] = { 0xC254CA22, 0xFB5646A9, 0xA23B184F, 0x8F613844};
    
    /* OTAA parametros*/
    uint8_t DevEui[] = { 0xbc, 0x55, 0x31, 0x89, 0x12, 0xbf, 0xd0, 0x90 };
    uint8_t AppEui<[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
    uint8_t AppKey[] = { 0x99, 0x42, 0xd4, 0x4b, 0xfc, 0x59, 0x85, 0x0a, 0xad, 0x76, 0x15, 0x3b, 0x7a, 0xdb, 0x50, 0x32 };
    
    /* ABP parametros*/
    uint8_t NwkSKey[] = { 0xd6, 0x28, 0x15, 0x0c, 0x99, 0x79, 0xbc, 0xec, 0xed, 0x11, 0x67, 0x75, 0x0b, 0x37, 0xb4, 0xe0 };
    uint8_t AppSKey[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
    uint32_t DevAddr  = ( uint32_t )0x00052815;
    
    //LoraWan channelsmask, default channels 0-7/
    uint16_t userChannelsMask[6]={ 0x00FF,0x0000,0x0000,0x0000,0x0000,0x0000 };
    
    DeviceClass_t  loraWanClass = CLASS_A;  /*Soporte de A and C*/
    uint32_t appTxDutyCycle = 30000;        /*15000; en [ms]*/
    bool overTheAirActivation = true;     /*OTAA or ABP*/
    bool loraWanAdr = true;               /*ADR enable*/
    bool isTxConfirmed = true;            /*confirmed or unconfirmed messages */
    uint8_t appPort = 3;                  /* Application port */
      
    /* reintentos de transmisión, en caso de no recibir ack */
    uint8_t confirmedNbTrials = 8;
    
    /* Seleccionado de Arduino IDE tools */
    uint8_t debugLevel = LoRaWAN_DEBUG_LEVEL; 
    LoRaMacRegion_t loraWanRegion = ACTIVE_REGION;
    
    // Mensajes por Puerto Serial
    volatile boolean serial_msg = true;
    
    // actuador
    #define LEDPin 25  //LED light
    boolean estado = false;  //estado dispositivo
    
    // Sensor de estado de bateria, revisar modelo de módulo
    // PowerDetection 13 en Wireless Stick lite, otros 37
    #define Vext 21
    #define PowerDetection 13
    
    void setup(){
      Serial.begin(115200);
      while (!Serial);
      SPI.begin(SCK,MISO,MOSI,SS);
      Mcu.init(SS,RST_LoRa,DIO0,DIO1,license);
      deviceState = DEVICE_STATE_INIT;
    }
    void loop(){
      switch( deviceState )  {
        case DEVICE_STATE_INIT:    {
          LoRaWAN.init(loraWanClass,loraWanRegion);
          break;
        }
        case DEVICE_STATE_JOIN:    {
          LoRaWAN.join();
          break;
        }
        case DEVICE_STATE_SEND:    {
          prepareTxFrame( appPort );
          LoRaWAN.send(loraWanClass);
          deviceState = DEVICE_STATE_CYCLE;
          if (serial_msg){
            Serial.print("  estado: ");
            Serial.println(estado);
          }
          break;
        }
        case DEVICE_STATE_CYCLE:    {
          // Schedule next packet transmission
          txDutyCycleTime = appTxDutyCycle + randr( -APP_TX_DUTYCYCLE_RND,
                                                    APP_TX_DUTYCYCLE_RND );
          LoRaWAN.cycle(txDutyCycleTime);
          deviceState = DEVICE_STATE_SLEEP;
          break;
        }
        case DEVICE_STATE_SLEEP:    {
          LoRaWAN.sleep(loraWanClass,debugLevel);
          break;
        }
        default:    {
          deviceState = DEVICE_STATE_INIT;
          break;
        }
      }
    }
    

    Recibe trama

    La recepción de los datos en la trana, "downlink", se realiza en la variable Buffer que es un arreglo de Bytes. La instrucción de control requiere dos valores: estado y duración , por lo que se usa el primer y segundo Byte. Este detalle se debe ajustar en cada situación para la cantidad de bytes que se requieran.

    Recibidos los datos, se invoca a la función para control del actuador "actuador_pin()".

    void  downLinkDataHandle(McpsIndication_t *mcpsIndication){
      lora_printf("+REV DATA:%s,RXSIZE %d,PORT %d\r\n",
                  mcpsIndication-&gt;RxSlot?"RXWIN2":"RXWIN1",
                  mcpsIndication-&gt;BufferSize,
                  mcpsIndication-&gt;Port);
      lora_printf("+REV DATA:");
      
      actuador_pin(mcpsIndication-&gt;Buffer[0],
                    mcpsIndication-&gt;Buffer[1]);
    
      for(uint8_t i=0;i&lt;mcpsIndication-&gt;BufferSize;i++)  {
        lora_printf("%02X",mcpsIndication-&gt;Buffer[i]);
      }
      lora_printf("\r\n");
    }
    
    

    Controla estado de actuador

    Con los dato recibidos en la función anterior, se selecciona la acción para cada caso de valor de estado.

    void actuador_pin(uint8_t estado, uint8_t duracion) {
      if (serial_msg){
        Serial.print("\n instruccion recibida: ");
        Serial.print(estado);
        Serial.print("  ");
        Serial.println(duracion);
      }
      switch(estado) {
        case 49: {
          pinMode(LEDPin,OUTPUT);
          digitalWrite(LEDPin, HIGH);
          estado = true;
          if (serial_msg){
            Serial.print("activador: ON ");
            Serial.print(" duración (s): ");
            Serial.print(duracion);
          }
          delay(duracion*1000); // en segundos
          digitalWrite(LEDPin, LOW);
          estado = false;
          break;
        }
        case 50: {
          pinMode(LEDPin,OUTPUT);
          digitalWrite(LEDPin, LOW);
          estado = false;
          if (serial_msg){
            Serial.println("activador: OFF");
          }
          delay(duracion*1000); // en segundos
          break;
        }
        default: {break;}
      }
    }
    
    

    Las siguientes funciones son las mismas usadas en el ejemplo de revisión de estado de batería.

    Prepara trama

    void actuador_pin(uint8_t estado, uint8_t duracion) {
      if (serial_msg){
        Serial.print("\n instruccion recibida: ");
        Serial.print(estado);
        Serial.print("  ");
        Serial.println(duracion);
      }
      switch(estado) {
        case 49: {
          pinMode(LEDPin,OUTPUT);
          digitalWrite(LEDPin, HIGH);
          estado = true;
          if (serial_msg){
            Serial.print("activador: ON ");
            Serial.print(" duración (s): ");
            Serial.print(duracion);
          }
          delay(duracion*1000); // en segundos
          digitalWrite(LEDPin, LOW);
          estado = false;
          break;
        }
        case 50: {
          pinMode(LEDPin,OUTPUT);
          digitalWrite(LEDPin, LOW);
          estado = false;
          if (serial_msg){
            Serial.println("activador: OFF");
          }
          delay(duracion*1000); // en segundos
          break;
        }
        default: {break;}
      }
    }
    

    Lectura de Sensor

    uint16_t SensorBateria(){
      // lectura ADC Voltaje de bateria 
      digitalWrite(Vext, LOW);
      delay(10);
      uint16_t ADC_voltage = analogRead(PowerDetection);
      digitalWrite(Vext, HIGH);
      
      if (serial_msg){
        Serial.print("Voltaje Batería (V): ");
        Serial.println(ADC_voltage);
      }
      return ADC_voltage;
    }