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->RxSlot?"RXWIN2":"RXWIN1",
              mcpsIndication->BufferSize,
              mcpsIndication->Port);
  lora_printf("+REV DATA:");
  
  actuador_pin(mcpsIndication->Buffer[0],
                mcpsIndication->Buffer[1]);

  for(uint8_t i=0;i<mcpsIndication->BufferSize;i++)  {
    lora_printf("%02X",mcpsIndication->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

static void prepareTxFrame( uint8_t port ){
  uint16_t BateriaV = SensorBateria();
  
  appDataSize = 4 ;
  appData[3] = 0 ;
  appData[2] = 0 ;
  appData[1] = highByte(BateriaV);
  appData[0] = lowByte(BateriaV);
  
  if (serial_msg){
    Serial.print("trama datos Byte: ");
    Serial.print(appData[1]);
    Serial.print("  ");
    Serial.println(appData[0]);
  }
}

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;
}

LoRaWan – Interruptor temporizado con Relé

Un actuador con red LoRaWan puede usarse con varios elementos, por ejemplo para el control de una válvula de paso para riego.

El esquema básico del circuito empieza con tener un pin de salida o control. El primer paso sería controlar el LED del módulo de desarrollo que para el ejemplo es el pin 25. Con esto se puede realizar las pruebas de conectividad de LoRaWan, Mqtt y Home Assistant.

 

El pin de salida puede ser conectado aun circuito para usar un relé para adaptarlo a diferentes niveles de voltaje o controlar otros circuitos de mayor voltaje y corriente.

LoRaWan – Estado de bateria. Gateway y Broker

El dispositivo envía los datos del sensor de bateria como un entero en dos bytes con valores entre [0,4096]. La conversión del valor a voltaje se realiza a partir del diagrama de circuito Heltec, donde se muestra que el módulo tiene un divisor de voltaje incorporado.

El divisor de voltaje y su equivalente de la ecuación es:

\frac{R12}{R10+R12} V_{BAT} = \frac{ADC}{4096}(3.3-0.7) V_{BAT} = \Big[ \frac{ADC}{4096}(3.3-0.7) \Big] \frac{R10+R12}{R12} V_{BAT} = 0.00203125*ADC

Decodificador de Trama – ChirpStack/Device-Profile

function Decode(fPort, bytes, variables) {
  
  // usando entero
  var unalectura = (bytes[1] << 8) |(bytes[0]);
  unalectura = unalectura*0.00203125;
  unalectura = +unalectura.toFixed(2);
  var appData = {'Bateria_V': unalectura}

  return appData;
}

En la salida de monitor-Serie se obtiene: Voltaje Batería (V): 2136

que decodificado en applications de chirpstack_ es: Bateria_V: 4.34

que es el valor que se envia por Mqtt.

Configuración en Home-Assistant

La configuración se realiza en el archivo configuration.yaml con la instrucción:

sudo nano /home/homeassistant/.homeassistant/configuration.yaml

al archivo se añade en la sección de sensores las instrucciones de cada lectura.

El tópico a observar se construye tomando los datos de chirpstack para cada dispositivo, observando el número de aplicación y el identificador de dispositivo.

Los valores de los sensores se decodificaron en un diccionario, que se transfirió como un valor tipo texto, por lo que primero se lo convierte en un diccionario antes de seleccionar el valor a usar. La selección del valor se realiza en value_template.

  - platform: mqtt
    name: 'Bateria_S02'
    unit_of_measurement: 'V'
    state_topic: 'application/2/device/c5db5e2b64eefcae/event/up'
    value_template: "{% set valores = value_json.objectJSON |from_json %} {{valores.Bateria_V}}"

Referencia: WiFi_Kit_series/SchematicDiagram/WiFi_LoRa_32(V2)/WIFI_LoRa_32_V2(868-915).PDF. https://github.com/Heltec-Aaron-Lee/WiFi_Kit_series/blob/master/SchematicDiagram/WiFi_LoRa_32(V2)/WIFI_LoRa_32_V2(868-915).PDF

Heltec Wifi LoRa V2 battery management. http://community.heltec.cn/t/heltec-wifi-lora-v2-battery-management/147/36

LoRaWan – Estado de bateria. Archivo.ino

El módulo de desarrollo Heltec Wireless Stick Lite tiene el circuito de sensor y alimentación por baterías. El diagrama de pines muestra que el pin 13 se encuentra conectado como detector de energía, que se incorpora a las instrucciones como PowerDetection. En los nuevos modelos, el detector de energía es el pin 37.

Preparación de trama

El valor del sensor ADC para bateria es de tipo entero, entrega la cuantificación en el rango [0, 4096], por lo que un entero de 2 bytes es suficiente para los datos. El proceso de conversión a voltaje se realiza en el decodificador en Application-Server del Broker ChirpStack.

  uint16_t BateriaV = SensorBateria();
  
  appDataSize = 4 ;
  appData[3] = 0 ;
  appData[2] = 0 ;
  appData[1] = highByte(BateriaV);
  appData[0] = lowByte(BateriaV);

Intrucciones Principales

/* Adaptado a partir de:
 * 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/tree/master/examples/OTAA_Battery_power
*/
#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[] = { 0xc5, 0xdb, 0x5e, 0x2b, 0x64, 0xee, 0xfc, 0xae };
uint8_t AppEui[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
uint8_t AppKey[] = { 0xf2, 0x1f, 0xff, 0x5c, 0x6f, 0xf2, 0x4c, 0xa0, 0x74, 0x73, 0xf5, 0xef, 0xf7, 0x39, 0x39, 0x10 };

/* ABP parametros*/
uint8_t NwkSKey[] = { 0x15, 0xb1, 0xd0, 0xef, 0xa4, 0x63, 0xdf, 0xbe, 0x3d, 0x11, 0x18, 0x1e, 0x1e, 0xc7, 0xda,0x85 };
uint8_t AppSKey[] = { 0xd7, 0x2c, 0x78, 0x75, 0x8c, 0xdc, 0xca, 0xbf, 0x55, 0xee, 0x4a, 0x77, 0x8d, 0x16, 0xef,0x67 };
uint32_t DevAddr =  ( uint32_t )0x007e6ae1;

/*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 = 15000;       /*15000; en [ms]*/
bool overTheAirActivation = true;      /*OTAA or ABP*/
bool loraWanAdr = true;                /*ADR enable*/
bool isTxConfirmed = true;             /*confirmed or unconfirmed messages */
uint8_t appPort = 2;                   /* 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;

// variables de sensor/actuador
// 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);

  //sensor de bateria
  adcAttachPin(PowerDetection);
  analogSetClockDiv(255); // 1338mS
    pinMode(Vext, OUTPUT);
    
  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;
      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;
    }
  }
}

Preparación de trama

// Trama - integra datos
static void prepareTxFrame( uint8_t port ){
  
  uint16_t BateriaV = SensorBateria();
  
  appDataSize = 4 ;
  appData[3] = 0 ;
  appData[2] = 0 ;
  appData[1] = highByte(BateriaV);
  appData[0] = lowByte(BateriaV);
}

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);

  Serial.print("Voltaje Batería (V): ");
  Serial.println(ADC_voltage);

  return ADC_voltage;
}

Referencia: Heltec LoRa ESP32, https://github.com/HelTecAutomation/ESP32_LoRaWAN/blob/master/examples/OTAA_Battery_power/OTAA_Battery_power.ino

Heltec LoRa ESP32 Wireless Stick Lite, diagrama de pintes- https://resource.heltec.cn/download/Wireless_Stick_Lite/Wireless_Stick_Lite.pdf

LoRaWan – Estado de bateria y carga con panel solar

Para dispositivos remotos donde es posible solo alimentación de energía mediante baterías es conveniente incluir el estado de la batería entre los datos de sensores.

Los mensajes LoRa son cortos, poco frecuentes y de bajo consumo de energía, por lo que un pequeño panel solar puede ser suficiente para mantener la batería del dispositivo con suficiente carga.

En un esquema básico, usando elementos disponibles, se usa un módulo HELTEC LoRa Wireless Stick Lite que dispone de un conector para alimentación con baterías a 3.7 V, además del circuito sensor de estado. Para recarga se conecta un panel solar de 5 a 6V (~1W) con un módulo TP4056 que contiene un circuito de protección se sobrevoltaje y polaridad inversa.

Esquema del circuito

El módulo TP4056 regula y controla la carga de la batería, pues el voltaje de carga del panel solar varía según la intensidad de luz solar recibida. El módulo tambien controla la salida de la batería hacia el módulo LoRa, previene la descarga de la batería por debajo de niveles que la dañen.

Los módulos de desarrollo para LoRa de Heltec incorporan el circuito y un puerto para conectar la batería de 3.7V. El modelo CubeCell incorpora el circuito de carga y pines de conexión para un panel solar, lo que simplifica aun más el esquema, tener encuenta el modelo a usar para el caso particular a implementar.

Referencias: TP 4056, https://www.componentsinfo.com/tp4056-module-pinout-datasheet-details/

CubeCell Dev-Board Plus, https://heltec.org/project/htcc-ab02/

Power ESP32/ESP8266 with Solar Panels, https://randomnerdtutorials.com/power-esp32-esp8266-solar-panels-battery-level-monitoring/

 

LoRaWan – Sensor Humedad y Temperatura. Gateway y Broker

El dispositivo que envía el mensaje con los datos de Humedad y temperatura debe registrarse en ChirpStack. La trama del mensaje se decodifica en al perfil del dispositivo (Device-profile) que permite al servidor de aplicaciones (Application-Server) integrar el mensaje en Home Assistant.

Decodificador de Trama – ChirpStack/Device-Profile

En el menú «Device-profile» se crea el perfil del dispositivo con datos principales semejantes a los usados en el dispositivo básico. La sección que cambia es la de CODEC que se describe en Java Script.

La decodificación se realiza usando LSB, es decir el orden es byte menos significativo primero. En este caso los datos se reconvierten a número real (float) y se trunca a dos decimales (+NumReal.toFixed(2)).

Con los datos decodificados se integran como un diccionario de resultados.

// Decode decodes an array of bytes into an object.
//  - fPort contains the LoRaWAN fPort number
//  - bytes is an array of bytes, e.g. [225, 230, 255, 0]
//  - variables contains the device variables e.g. {"calibration": "3.5"} (both the key / value are of type string)
// The function must return an object, e.g. {"temperature": 22.5}
//function Decode(fPort, bytes, variables) {
//  return {};
//}
function Decode(fPort, bytes, variables) {

  // usando LSB  (least significant byte first)
  var unalectura = bytes[3]<<24 | bytes[2]<<16 | bytes[1]<<8 | bytes[0]; var unsigno = (unalectura>>>31 === 0) ? 1.0 : -1.0;
  var exponente = unalectura>>>23 & 0xff;
  var mantisa = (exponente === 0) ? (unalectura & 0x7fffff)<<1 : (unalectura & 0x7fffff) | 0x800000;
  var NumReal = unsigno * mantisa * Math.pow(2, exponente - 150);
  NumReal = +NumReal.toFixed(2);
  
  // usando LSB  (least significant byte first)
  var unalectura = bytes[7]<<24 | bytes[6]<<16 | bytes[5]<<8 | bytes[4]; var unsigno = (unalectura>>>31 === 0) ? 1.0 : -1.0;
  var exponente = unalectura>>>23 & 0xff;
  var mantisa = (exponente === 0) ? (unalectura & 0x7fffff)<<1 : (unalectura & 0x7fffff) | 0x800000;
  var NumReal2 = unsigno * mantisa * Math.pow(2, exponente - 150);
  NumReal2 = +NumReal2.toFixed(2);
  
  var appData = {'temperaturaC': NumReal, 'humedad': NumReal2};
  return appData;
}

Configuración en Home-Assistant

La configuración se realiza en el archivo configuration.yaml con la instrucción:

sudo nano /home/homeassistant/.homeassistant/configuration.yaml

al archivo se añade en la sección de sensores las instrucciones de cada lectura.

El tópico a observar se construye tomando los datos de chirpstack para cada dispositivo, observando el número de aplicación y el identificador de dispositivo.

Los valores de los sensores se decodificaron en un diccionario, que se transfirió como un valor tipo texto, por  lo que primero se lo convierte en un diccionario antes de seleccionar el valor a usar. La selección del valor se realiza en value_template.

  - platform: mqtt
    name: 'DHT_S01_temperatura'
    unit_of_measurement: '°C'
    state_topic: 'application/1/device/01200893df803774/event/up'
    value_template: "{% set valores = value_json.objectJSON |from_json %} {{valores.temperaturaC}}"    
    #json_attributes_topic: 'application/1/device/01200893df803774/event/up'

  - platform: mqtt
    name: 'DHT_S01_humedad'
    unit_of_measurement: '%'
    state_topic: 'application/1/device/01200893df803774/event/up'
    value_template: "{% set valores = value_json.objectJSON |from_json %} {{valores.humedad}}"

  - platform: mqtt
    name: 'DHT_S01_rssi'
    unit_of_measurement: 'dB'
    state_topic: 'application/1/device/01200893df803774/event/up'
    value_template: "{{ value_json.rxInfo[0].rssi}}"

  - platform: mqtt
    name: 'DHT_S01_snr'
    unit_of_measurement: 'dB'
    state_topic: 'application/1/device/01200893df803774/event/up'
    value_template: "{{ value_json.rxInfo[0].loRaSNR}}"

El resultado se observa como:

LoRaWan – Sensor Humedad y Temperatura. Archivo.ino

El desarrollo del ejercicio se simplifica al tratarse como una extensión del ejemplo base de comunicación Lora al que se añade el Sensor DHT11.

ChirpStack – Añadir un dispositivo

El archivo de instrucciones se presenta en tres secciones o pestañas. La primera contiene la declaración de variables, la parte de configuración y el lazo principal. Las siguientes dos secciones son procedimientos para preparación de trama y lectura de sensor.

Preparación de trama

El valor de temperatura se almacena en tipo real (float) que usa 4 bytes. En la trama se separan los bytes en orden LSB (Byte menos significativo primero, detalle que hay que recordar para decodificar en ChirpStack.

    unsigned char *puc;
    puc = (unsigned char *)(&temperaturaC);
    appDataSize = 8;//AppDataSize max value is 64
    appData[0] = puc[0];
    appData[1] = puc[1];
    appData[2] = puc[2];
    appData[3] = puc[3];

De utilizar el mismo formato para el valor de Humedad, el formato es muy semejante.

Intrucciones Principales

 /*
 * 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"
#include <DHT.h>

 /*licencia Heltec ESP32 LoRaWan http: / /resource.heltec.cn /search * /
uint32_t  license[4] = {0xBE21335B, 0xAEC3C5CE, 0xCC0A1CF4, 0xB836F981};

 /* OTAA parametros* /
uint8_t DevEui[] = { 0x01, 0x20, 0x08, 0x93, 0xdf, 0x80, 0x37, 0x74 };
uint8_t AppEui[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
uint8_t AppKey[] = { 0x05, 0x8e, 0xeb, 0xff, 0x24, 0xf1, 0x01, 0x84, 0xd0, 0x07, 0xbe, 0xd4, 0x65, 0xe7, 0x6b, 0xb5 };

 /* ABP parametros* /
uint32_t DevAddr =  ( uint32_t )0x0174b1fd;
uint8_t NwkSKey[] = { 0xc1, 0x45, 0x31, 0x28, 0x5f, 0xb2, 0x56, 0x3b, 0x9d, 0x5f, 0x27, 0x15, 0xed, 0x3a, 0x0e, 0xbc}; 
uint8_t AppSKey[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};

 / /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 = 300000;         /*15000; en [ms]* /
bool overTheAirActivation = true;        /*OTAA or ABP* /
bool loraWanAdr = true;                  /*ADR enable* /
bool isTxConfirmed = true;               /*confirmed or unconfirmed messages * /
uint8_t appPort = 2;                     /* 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;

 / / variables de sensor /actuador
uint8_t contador = 0;  
float humedad = 0;
float temperaturaC = 0;
float temperaturaF = 0;

 / / Mensajes por Puerto Serial
volatile boolean serial_msg = true;

 / / Sensor humedad y temperatura
#define DHTPIN 4
#define DHTTYPE DHT11
DHT dht(DHTPIN, DHTTYPE);

void setup(){
  if (serial_msg){
    Serial.begin(115200);
    while (!Serial);
  }
  SPI.begin(SCK,MISO,MOSI,SS);
  Mcu.init(SS,RST_LoRa,DIO0,DIO1,license);
  deviceState = DEVICE_STATE_INIT;

   / / SENSOR Temperatura&Humedad
  dht.begin();
}
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;
      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;
    }
  }
}

Preparación de trama

// Trama - integra datos
static void prepareTxFrame( uint8_t port ){
    contador = contador + 1;
    // Sensor de Temperatura y Humedad
    SensorTempHum();

    if (isnan(humedad) || isnan(temperaturaC) || isnan(temperaturaF)) {
      humedad = 0;
      temperaturaC = 0;
      temperaturaF = 0;
    }
    unsigned char *puc;
    puc = (unsigned char *)(&temperaturaC);
    appDataSize = 8;//AppDataSize max value is 64
    appData[0] = puc[0];
    appData[1] = puc[1];
    appData[2] = puc[2];
    appData[3] = puc[3];

    puc = (unsigned char *)(&humedad);
    appData[4] = puc[0];
    appData[5] = puc[1];
    appData[6] = puc[2];
    appData[7] = puc[3];
    if (serial_msg){
      Serial.print("tx: "+String(temperaturaC)+"  ");
      Serial.print(appData[0]);
      Serial.print(appData[1]);
      Serial.print(appData[2]);
      Serial.println(appData[3]);
      Serial.print("tx: "+String(humedad)+"  ");
      Serial.print(appData[4]);
      Serial.print(appData[5]);
      Serial.print(appData[6]);
      Serial.println(appData[7]);
    }
}

Lectura de sensor

void SensorTempHum(){
  // SENSOR Temperatura&Humedad
  dht.begin(); // regresa de sleep
  delay(2000); // espera minina entre lecturas de sensor
  humedad = dht.readHumidity();
  temperaturaC = dht.readTemperature();
  temperaturaF = dht.readTemperature(true);

  if (isnan(humedad) || isnan(temperaturaC) || isnan(temperaturaF)) {
    if (serial_msg){
      Serial.println(F("Failed to read from DHT sensor!"));
    }
    // espera minina entre lecturas de sensor
    delay(100); 
    return;
  }

  float hif = dht.computeHeatIndex(temperaturaF, humedad);
  float hic = dht.computeHeatIndex(temperaturaC, humedad, false);

  if (serial_msg){
    Serial.print(F(" Humidity: "));
    Serial.print(humedad);
    Serial.print(F("%  \n Temperature: "));
    Serial.print(temperaturaC);
    Serial.print(F("°C "));
    Serial.print(temperaturaF);
    Serial.print(F("°F \n Heat index: "));
    Serial.print(hic);
    Serial.print(F("°C "));
    Serial.print(hif);
    Serial.println(F("°F"));
  }
}

LoRaWan – Sensor Humedad y Temperatura con DHT11

Para disponer de los valores de Humedad y Temperatura de una ubicación mas lejana a la cobertura de Wifi, se propone implementar un dispositivo con LoRaWan y el sensor DHT11. Se lo implementa como primer ejercicio para mostrar la integración de dos redes LoRa y Ethernet, junto a los gateways y brokers.

La propuesta para redes WiFi se realizó con ESP8266 que se describe en el enlace: WiFi Sensor Temperatura-Humedad ESP-07

En LoRaWan, la base del dispositivo es un módulo de desarrollo ESP32-LoRa modelo HELTEC wireless stick-lite al que se añade el sensor DHT11.

Primero se desarrolla la parte básica de captura de datos del sensor, luego se arma la trama de datos, para enviarla por la red LoRa hacia el Gateway controlado por ChirpStack. En ChripStack se acondicionan los datos para una mejor lectura con «Decoder» y se la envía al Broker Home Assistant para mostrarla en una página web.

 

 

2.3 Wifi-Conectando ESP8266 – ESP01 Esquemático

Un ESP01 que es el más pequeño de los diferentes módulos ESP8266, requiere añadir algunos componentes para conectarse y programar u operar el módulo.

Esquemático con ESP01

Al módulo se añade: una fuente de alimentaicón a 3.3V, un módulo de conexión USB-TTL para conectar a una computadora y unas botoneras para controlar el modo de operación y reinicio reset).

La alimentación del circuito presentada en el esquema, sigue las indicaciones de la sección: Fuente de alimentación 3.3VDC.

El control de modo de «operación/Programación» y «reset» se implementa con  botoneras.

El modo predeterminado de inicio es «operación» por medio de GPIO0 a estado HIGH (+VCC ) con una resistencia (pullup), al presionar la botonera se cambia el estado a LOW (GND).

Para programar se presiona la botonera «programar» y luego se añade un pulso a «reset», el módulo queda  listo para «subir» instrucciones desde IDE Arduino.

En la versión ESP-01 el LED interno se encuentra conectado al pin de TX usado para la comunicación Serial. Usar mensajes por puerto serial causaría un conflicto en el modo de operación con el parpadeo de led, asunto a tomar encuenta en las instrucciones.

Una forma de implementación funcional, es no usar los mensajes por puerto serial. Otra forma es cambiar el LED a otro pin y configurarlo como salida, ej: GPIO2. El nuevo pin permite conectar Tx y Rx y observar los mensajes del proceso de conexión. Adicionalmente GPIO2 es el led interno de otros modelos de ESP8266.

En caso de usar los componentes junto a un protoboard se muetra tambien el diagrama de conexión como referencia

la programación del módulo se realiza en la siguiente sección.

6.1 Puntos de prueba vs Modelo de propagación

Una revisión del modelo de propagación encontrado con los puntos de entrenamiento se puede realizar con puntos de prueba. Las lecturas de los puntos de prueba se realizaron dos semanas después de los puntos de entrenamiento.

Los puntos de prueba tienen una lectura a cada baliza, a diferencia de al menos cien de los puntos de entrenamiento.

En la revisión se encuentra que los puntos de prueba se encuentran en el rango de las mediciones del modelo, sin embargo los puntos se encuentran distribuidos en la gráfica desplazados hacia arriba del modelo, indicando que es necesario incorporar más variables para ajustar el modelo, por ejemplo la temperatura del entorno u otra variable que se determinaría en una siguiente fase del experimento.

Para cuantificar el desplazamiento Δrssi, se calcula el error entre cada punto y la ecuación modelo. Bajo el concepto que los errores se distribuyen equitativamente sobre y debajo la ecuación, se obtiene el valor de la mediana que indica el desplazamiento más optimo como ajuste al modelo bajo las nuevas condiciones.

Revisando la aplicabilidad del modelo, se obtiene el coeficiente de correlación para los puntos de prueba clasificados en gtwFIEC, que es de -0.88 que permite estimar que se puede aplicar relación lineal con pendiente negativa en el intervalo.

gtwFIEC s0 r0  error_mediana: 5.99 correlacion: -0.88
gtwFIEC s0 r1  error_mediana: 7.14 correlacion: -0.68
gtwFIEC s0 r2  error_mediana: 1.98 correlacion: -0.76

Se puede proseguir con los otros puntos de prueba para las otras balizas, encontrando que en la baliza gtwRECT los mejores valores de correlación con los datos disponibles tan dado en el área de vegetación sin sombra.  El modelo estimado en vegetación para la zona con sombra debe ser revisados pues hay un valores cercanos a cero en correlación, por lo que la linealización con los datos puede ser insuficiente para estimar que hay una relación lineal entre las dos variables. Se requiere en ese caso expandir la toma de muestra y revisar con mas datos el modelo.

Desplazamientos de puntos de prueba
 respecto al modelo:
[baliza,sector,intervalo,drssi]
gtwRECT s0 r0  error_mediana: 7.8 correlacion: -0.54
gtwRECT s0 r1  error_mediana: 5.55 correlacion: -0.13
gtwRECT s0 r2  error_mediana: 9.3 correlacion: -0.47
gtwRECT s1 r0  error_mediana: 3.7 correlacion: 0.13
gtwRECT s1 r1  error_mediana: 3.7 correlacion: 0.13
gtwFIEC s0 r0  error_mediana: 5.99 correlacion: -0.88
gtwFIEC s0 r1  error_mediana: 7.14 correlacion: -0.68
gtwFIEC s0 r2  error_mediana: 1.98 correlacion: -0.76
gtwFCNM s0 r0  error_mediana: 8.56 correlacion: -0.76
gtwFCNM s0 r1  error_mediana: 6.59 correlacion: -0.75
gtwFCNM s0 r2  error_mediana: 9.98 correlacion: -0.29

Algoritmo en Python

Se adjunta el algoritmo correspondiente:

# LoRa-Multipunto, Revisa grafica Rssi vs distancia
# con puntos de prueba 'CIRC'
# linealización Rssi vs log10(distancia) por mínimos cuadrados
# Graficas 2D y 3D
# Girni 2020-01-30 propuesta: edelros@espol.edu.ec

import numpy as np
import pandas as pd
import json
import matplotlib.pyplot as plt
import girni_lora_libreria as girni

# INGRESO
# archivos de entrada
modo       = 'rx'
medida     = 'rssi'
descriptor = 'mean'
arch_ecuaciones  = 'rsmP07_ecuacionSector01.json'
arch_medidaubica = 'rsmP06_'+medida+'UbicaCirc01.txt'
# archivos de salida
arch_ecuaciones2  = 'rsmP07_ecuacionSector02.json'


baliza = {'d1':'gtwRECT',
          'd2':'gtwFIEC',
          'd3':'gtwFCNM'}

# Parámetros de grafica
tipograf      = '2D'  # '2D','3D'
escala        = 'log' # 'normal','log'
escalabase    = 10    # 10, np.exp()
casicero   = 1e-4
precision  = 2
intersectar = 0 # 0:Falso, 1: Verdadero

# Referencias de gráfica
grupo   = ['FIEC' ,'FCNM'  ,'RECT','CIRC']
colores = ['green','orange','grey','magenta']
tipo    = ['punto','1m' ,'gtw','dispositivo']
marcas  = [    'o','D'  ,'D'  ,'*' ]

mostrargrpeti = ['FIEC','FCNM','RECT']
mostrartipeti = ['1m','gtw']

# PROCEDIMIENTO
# leer datos
with open(arch_ecuaciones) as json_file: 
    ecuacion = json.load(json_file) 
tabla = pd.read_csv(arch_medidaubica, index_col='etiqueta')
tabla = pd.DataFrame(tabla)

baliza_key = list(baliza.keys())
baliza_val = list(baliza.values())
eq_graf  = {}

# Revisa un sector_estimado
for cualbaliza in baliza:
    sector_baliza = 'sector_'+cualbaliza
    tabla[sector_baliza] = 0
for cadapunto in tabla.index:
    for cualbaliza in baliza:
        # evalua en sector
        sector_baliza = 'sector_'+cualbaliza
        donde = baliza_key.index(cualbaliza)
        # coordenadas baliza para identificar ángulo
        b_este  = tabla['c_este'][baliza[cualbaliza]]
        b_norte = tabla['c_norte'][baliza[cualbaliza]]
        # coordenadas del punto
        p_este  = tabla['c_este'][cadapunto]
        p_norte = tabla['c_norte'][cadapunto]
        dx = p_este-b_este
        dy = p_norte-b_norte
        theta = np.arctan2(dy,dx)
        if theta<0:
            theta = theta + 2*np.pi
        sectores   =  ecuacion[baliza[cualbaliza]]['sector_rad']
        nsectores  = len(sectores)
        otrosector = ''
        for i in range(0,nsectores-1,1):
            a = sectores[i]
            b = sectores[i+1]
            if theta>=a and theta<b:
                otrosector = i+1
        if otrosector !='':
            tabla.loc[cadapunto,'sector_'+cualbaliza] = otrosector

# analiza datos hacia una baliza
for unabaliza in ecuacion:
    donde = baliza_val.index(unabaliza)
    cualbaliza = baliza_key[donde]
    
    eq_graf[unabaliza] = {}
    for unsector in list(ecuacion[unabaliza].keys()):  
        if unsector != 'sector_rad':
            eq_graf[unabaliza][unsector] = {}
            for i_eq in list(ecuacion[unabaliza][unsector].keys()):
                eq_graf[unabaliza][unsector][i_eq] = {} 

    # puntos de tabla
    for unsector in list(ecuacion[unabaliza].keys()):
        if unsector != 'sector_rad':
            xi_p = [] ; yi_p = []
            xi = [] ; yi = [] ; etiqueta = []
            columna = medida+'_'+modo+'_'+cualbaliza
            for cadapunto in tabla.index:
                p_rssi = np.round(tabla[columna][cadapunto], precision)
                dist   = np.round(tabla['dist_'+cualbaliza][cadapunto], precision)
                ensector = 's'+str(tabla['sector_'+cualbaliza][cadapunto])
                LOS = tabla['LOS_'+cualbaliza][cadapunto]
                if not(np.isnan(p_rssi)) and dist>1 and LOS==1 and unsector==ensector:
                    yi_p.append(p_rssi)
                    xi_p.append(dist)
                    etiqueta.append(cadapunto)
                    if not(dist in xi):
                        xi.append(dist)
                        yi.append(p_rssi)
                    
            xi_p = np.array(xi_p)
            yi_p = np.array(yi_p)
            ordenar = np.argsort(xi_p)
            xi_p = list(xi_p[ordenar])
            yi_p = list(yi_p[ordenar])
            etiqueta = np.array(etiqueta)
            etiqueta = etiqueta[ordenar]

            xi = np.array(xi)
            yi = np.array(yi)
            ordenar = np.argsort(xi)
            xi = xi[ordenar]
            yi = yi[ordenar]

            i_eq = 'r0' # todos los puntos
            eq_graf[unabaliza][unsector][i_eq]['xi'] = xi_p.copy()
            eq_graf[unabaliza][unsector][i_eq]['yi'] = yi_p.copy()
            eq_graf[unabaliza][unsector][i_eq]['etiqueta'] = etiqueta.copy()
                
            
            # ecuacion linealizada
            for i_eq in list(ecuacion[unabaliza][unsector].keys()):
                a = ecuacion[unabaliza][unsector][i_eq]['intervalox'][0]
                b = ecuacion[unabaliza][unsector][i_eq]['intervalox'][1]
                
                alpha = ecuacion[unabaliza][unsector][i_eq]['alpha']
                beta  = ecuacion[unabaliza][unsector][i_eq]['beta']
                std   = ecuacion[unabaliza][unsector][i_eq]['error_std']
                fdist = lambda d: -10*alpha*(np.log10(d))+beta

                subintervalo = (xi >= a) & (xi <= b)
                xi_s = xi[subintervalo]
                yi_s = yi[subintervalo]

                # correlacion de los puntos
                correlacion = np.corrcoef(xi_s,yi_s)[0,1]
                
                # dibujar la línea en intervalo
                xi_sub = xi[subintervalo]
                if not(a in xi_sub):
                    xi_sub = np.concatenate(([a],xi_sub),axis=0)
                if not(b in xi_sub):
                    xi_sub = np.concatenate((xi_sub,[b]),axis=0)
                yi_sub = fdist(xi_sub)

                eq_graf[unabaliza][unsector][i_eq]['xi_graf'] = xi_sub.copy()
                eq_graf[unabaliza][unsector][i_eq]['yi_graf'] = yi_sub.copy()

                
                # Errores en subintervalo
                mediana = 0
                if intersectar == 0:
                    yie_s = yi_s - fdist(xi_s)
                    mediana = np.median(yie_s)
                else:
                    if i_eq=='r0':
                        yie_s = yi_s - fdist(xi_s)
                        mediana = np.median(yie_s)
                    else:
                        mediana = ecuacion[unabaliza][unsector]['r0']['desplaza']
                                    
                eq_graf[unabaliza][unsector][i_eq]['desplaza'] = mediana
                eq_graf[unabaliza][unsector][i_eq]['correlacion'] = correlacion
                ecuacion[unabaliza][unsector][i_eq]['desplaza'] = mediana
                

# SALIDA
print('Desplazamientos de puntos de prueba')
print(' respecto al modelo:')
print('[baliza,sector,intervalo,drssi]')
for unabaliza in ecuacion:
    for unsector in list(ecuacion[unabaliza].keys()):  
        if unsector != 'sector_rad':
            for i_eq in list(ecuacion[unabaliza][unsector].keys()):
                mediana = ecuacion[unabaliza][unsector][i_eq]['desplaza']
                mediana = np.round(mediana,precision)
                correlacion = eq_graf[unabaliza][unsector][i_eq]['correlacion']
                correlacion = np.round(correlacion,precision)
                texto = unabaliza + ' '+unsector + ' ' + i_eq + ' '
                texto = texto + ' error_mediana: '+ str(mediana)
                texto = texto + ' correlacion: '+str(correlacion)
                print(texto)

# salida hacia archivo
with open(arch_ecuaciones2, 'w') as outfile:
    json.dump(ecuacion, outfile) 

# GRAFICA
# Referencias para gráfica
grupo   = ['FIEC' ,'FCNM'  ,'RECT','CIRC']
colores = ['green','orange','grey','magenta']
tipo    = ['punto','1m' ,'gtw','dispositivo']
marcas  = [    'o','D'  ,'D'  ,'*' ]

mostrargrpeti = ['FIEC','FCNM','RECT']
mostrartipeti = ['1m','gtw']

if tipograf=='2D':
    for unabaliza in eq_graf:
        for unsector in eq_graf[unabaliza]:
            figura,grafica = plt.subplots()
            if escala == 'log':
                grafica.set_xscale(escala,base=escalabase)
                
            eq_graf2 = {}
            # todos los puntos
            unintervalo = 'r0'
            xi_p = eq_graf[unabaliza][unsector][unintervalo]['xi']
            yi_p = eq_graf[unabaliza][unsector][unintervalo]['yi']
            etiqueta = eq_graf[unabaliza][unsector][unintervalo]['etiqueta']
            grafica.scatter(xi_p,yi_p,marker='.')
            m = len(xi_p)
            for i in range(0,m,1):
                grafica.annotate(etiqueta[i],
                                (xi_p[i],yi_p[i]))
                
            # linea con todos los puntos
            for i_eq in list(eq_graf[unabaliza][unsector].keys()):
                fdtxt   = ecuacion[unabaliza][unsector][i_eq]['eq_latex']
                xi_graf = eq_graf[unabaliza][unsector][i_eq]['xi_graf']
                yi_graf = eq_graf[unabaliza][unsector][i_eq]['yi_graf']
                a = np.round(ecuacion[unabaliza][unsector][i_eq]['intervalox'][0],precision)
                b = np.round(ecuacion[unabaliza][unsector][i_eq]['intervalox'][1],precision)
                desplaza = eq_graf[unabaliza][unsector][i_eq]['desplaza']
                desplaza = np.round(desplaza,precision)
                estilo = 'solid'
                if i_eq =='r0':
                    estilo = 'dashed'
                eq_texto = i_eq+': '+fdtxt+' ; ['+str(a)+','+str(b)+']'
                grafica.plot(xi_graf,yi_graf,
                             label = eq_texto,
                             linestyle = estilo)
                # para linea desplazada
                eq_graf2[i_eq] = {'xi':xi_graf,
                              'yi':yi_graf + desplaza,
                              'desplaza': desplaza}
                
                # lineas de frontera
                grafica.axvline(a, color='lightblue')
                valor_frontera = str(np.round(a,precision))
                grafica.annotate(valor_frontera,
                                 (a,np.max(yi_p)),
                                 color='lightblue')
                grafica.axvline(b, color='lightblue')
                valor_frontera = str(np.round(b,precision))
                grafica.annotate(valor_frontera,
                                 (b,np.max(yi_p)),
                                 color='lightblue')
            for i_eq in list(ecuacion[unabaliza][unsector].keys()):
                estilo = 'solid'
                if i_eq =='r0':
                    estilo = 'dashed'
                xi = eq_graf2[i_eq]['xi']
                yi = eq_graf2[i_eq]['yi']
                desplaza = eq_graf2[i_eq]['desplaza']
                texto = i_eq + ' + $\Delta $Rssi'
                texto = texto + ' ; $ \Delta rssi$= '+ str(desplaza)
                grafica.plot(xi,yi, label = texto,
                             linestyle = estilo)
        
            # etiquetas y títulos
            grafica.legend()
            grafica.set_ylabel(medida+'_'+modo)
            grafica.set_xlabel('distancia')
            grafica.grid(True,linestyle='dotted',
                         axis='x', which='both')
            
            untitulo = unabaliza+' '+unsector+': Puntos de prueba desplazados'
            grafica.set_title(untitulo)
            
            plt.show()