LoRaWan – Pluviometro Archivo.ino

Las instrucciones para control del pluviómetro y sensores complementarios se encuentran segmentadas por bloques:

  • Bloque principal
  • Sensores
  • Envío de tramas
  • Recepción de tramas

[ sensores ] [ envía ] [ recibe ] [ principal ]

Sensores

El bloque de sensores se simplifica por partes de control y comunicación acorde al  tipo de sensor:

  • pulsos: Pluviómetro
  • Analógicos: batería
  • I2C: humedad, temperatura y presión atmosférica

Pulsos: pluviómetro

El pluviómetro se centra en medir el vaciado de la «doble cubeta basculante».

Doble Cubeta BasculantePara la detección se usa un interruptor magnético que marca el cambio de estado mediante un pulso (pulse) conectado al pin asignado (pluvioPin). Para que la detección sea independiente del estado del dispositivo se usa una interrupción de instrucciones (pulse_tip)  que cuenta el pulso (pulse) en flanco de subida (RISING)y marca el evento (pulse_flag).

attachInterrupt(pluvioPin, pulse_tip, RISING);

El dispositivo cuenta  los pulsos generados durante un minuto  (sample_period) y los almacena en una lista (pulse_list) hasta la transmisión de los datos  hacia el gateway LoRa.

pulse_tip muestreo

Analógicos: Batería

El sensor de estado de batería es un ADC que toma una lectura del estado de la batería antes de cada transmisión LoRa.

I2C: humedad, temperatura y presión atmosférica

Los sensores de humedad, temperatura y presión son de tipo I2C que se activan solo al ser leídos en cada ciclo de transmisión mediante el pin Vext, Posterior a la lectura apagan para optimizar el ahorro de energía.

Antes de cada lectura, se realiza una verificación de estado de conexión al sensor optimizando el tiempo de éste proceso en caso de fallas del sensor expuesto a condiciones ambientales.

LoRaWan pluviometro sensores bloque

// sensors block
// pluviometer pulse counter at interrupt
void pulse_tip(){
  if (pulse<=255){pulse = pulse + 1;}
  pulse_flag = true;
}
void pulse_check(){
  if (pulse_flag==true){
    if (pulse>255){pulse=255;}
    pulse_flag = false;
    tipfree = 0;
    if (serial_msg){
      Serial.print("_|_: ");Serial.println(pulse);
    }
    turnOnRGB(128,200);turnOffRGB();
  }
}

void minute_check(){
  int i = 0; 
  pulse_list[i_minute] = pulse;
  if (i_minute>=n_minute){
    // pluviometer update
    for (i = 0; i < n_minute; i = i + 1) {
      pluviometer[i] = pulse_list[i];
      pulse_list[i] = 0;
    }
    i_minute = 0;
    loop_0 = false;
    sensor_flag = true;
    tx_done = false;
  }

  if (serial_msg){
    Serial.print("i_minute:");Serial.print(i_minute);
    Serial.print(" ; pulse_list:");
    for (i = 0; i < n_minute; i = i + 1) {
      Serial.print(" "); Serial.print(pulse_list[i]);
    }
    Serial.print(" ; pluviometer:");
    for (i = 0; i < n_minute; i = i + 1) {
      Serial.print(" "); Serial.print(pluviometer[i]);
    }
    Serial.print(" ; tipfree "); Serial.println(tipfree);
  }
}

void read_sensors(){
  batteryVoltage = getBatteryVoltage();

  // sensors check i2c on
  byte sensor_error, address;
  digitalWrite(Vext, LOW); delay(300);
  Wire.begin();
  
  humitemp_active = false;
  Wire.beginTransmission(0x40);
  sensor_error = Wire.endTransmission();
  if (sensor_error == 0){humitemp_active = true;}

  prestemp_active = false;
  Wire.beginTransmission(0x77);
  sensor_error = Wire.endTransmission();
  if (sensor_error == 0){prestemp_active = true;}

  if (humitemp_active){hdc1080.begin(0x40);}
  if (prestemp_active){bmp.begin();}
    
  if (humitemp_active){
    ht_temp = hdc1080.readTemperature();
    ht_humi = hdc1080.readHumidity();
  }

  float bar_alti = 0, bar_sea_pres = 0, bar_sea_alt = 0;
  if (prestemp_active){
    bar_temp = bmp.readTemperature();
    bar_pres = bmp.readPressure();
    bar_alti = bmp.readAltitude();
    bar_sea_pres = bmp.readSealevelPressure();
    bar_sea_alt  = bmp.readAltitude(100800);
  }

  // sensors I2C off
  Wire.end();
  digitalWrite(Vext, HIGH);

  if (serial_msg){
    Serial.print("Battery = ");Serial.print(batteryVoltage);
    
    Serial.print(" ; --- sensor ");
    Serial.print("hum_temp: "); Serial.print(humitemp_active);
    Serial.print(" ; bar_temp: "); Serial.println(prestemp_active);
    
    if (humitemp_active){
      Serial.print("HDC1080 Temperature = ");
      Serial.print(ht_temp); Serial.print(" C ; Humidity = ");
      Serial.print(ht_humi);Serial.println(" %");
    }
    if (prestemp_active){
      Serial.print("BMP180  Temperature = ");
      Serial.print(bar_temp);Serial.println(" C");
      
      Serial.print("  Pressure =      ");
      Serial.print(bar_pres);Serial.print(" Pa ; Altitude = ");
      Serial.print(bar_alti);Serial.println(" meters");

      Serial.print("  SeaLevel(calc)= ");
      Serial.print(bar_sea_pres); Serial.print(" Pa ; Altitude = ");
      Serial.print(bar_sea_alt); Serial.println(" meters");
    }
  Serial.println("");
  }
}

void onSleep(){
  Serial.print("\n  i_minute:");Serial.print(i_minute);
  Serial.printf(" ;Going into lowpower mode, %d ms later wake up.\r\n",timetillwakeup);
  lowpower = 1;
  //timetillwakeup ms later wake up;
  timetillwakeup = sample_period*1000 - (millis()-sample_ti);
  TimerSetValue( &wakeUp, timetillwakeup );
  TimerStart( &wakeUp );
}
void onWakeUp(){
  Serial.println("  ... Woke up by time");//, %d ms later into lowpower mode.\r\n",timetillsleep);
	lowpower = 0;
  sleep_flag = false;
  sleep_done = true;
	//timetillsleep ms later into lowpower mode;
	//TimerSetValue( &sleep, timetillsleep );
	//TimerStart( &sleep );
}

Adicionalmente existen dos procedimientos usados para el control del modo de ahorro de energía: onSleep() y onWakeup(), que junto a la instrucción lowPowerHandler() permiten un mayor ahorro de energía en los ciclos donde no se han producido pulsos del pluviómetro. Esto minimiza el uso de batería ante los periodos sin lluvia y manteniendo aún el registro por minuto durante largos periodos.

Básicamente, se usa el modo de ahorro de energía si no han ocurrido eventos de conteo del pluviómetro durante al menos un ciclo de n_minute, caso contrario se mantiene el controlador activo.

[ sensores ] [ envía ] [ recibe ] [ principal ]

LoRaWan – Envia trama

La trama se configura usando los parámetros obtenidos en el dispositivo para cada sensor.

  • Rssi Downlink (1 byte)
  • Snr Downlink (1 byte)
  • Datarate Downlink (1 byte)
  • Voltaje de batería (2 bytes)
  • sensores (15 bytes)

LoRaWan pluviometro envia

/* Prepares the payload of the frame */
static void prepareTxFrame( uint8_t port ) {
  unsigned char *puc;
  signed char *pucs;
  // trama
  appDataSize = 15 + n_minute; // 15+pluvio_list size
  appData[0] = ack_rssi; //Ack leido en dispositivo
  appData[1] = ack_snr;
  appData[2] = ack_datarate;
  appData[3] = (uint8_t)batteryVoltage;
  appData[4] = (uint8_t)(batteryVoltage>>8);

  // convierte float a bytes
  int ht_temp_int = round(ht_temp*100);
  pucs = (signed char *)(&ht_temp_int);
  appData[5] = pucs[0];
  appData[6] = pucs[1];

  // convierte float a bytes
  int ht_humi_int = round(ht_humi*100);
  puc = (unsigned char *)(&ht_humi_int);
  appData[7] = puc[0];
  appData[8] = puc[1];

  // convierte float a bytes
  int bar_temp_int = round(bar_temp*100);
  pucs = (signed char *)(&bar_temp_int);
  appData[9] = pucs[0];
  appData[10] = pucs[1];

  // convierte float a bytes
  puc = (unsigned char *)(&bar_pres);
  appData[11] = puc[0];
  appData[12] = puc[1];
  appData[13] = puc[2];
  appData[14] = puc[3];

  for (int i = 0; i < n_minute; i = i + 1) {
    appData[15+i] = pluviometer[i];
  }

  // if (serial_msg){
  //   Serial.print("send pluviometer:");
  //   for (int i = 0; i < n_minute; i = i + 1) {
  //     Serial.print(" ");
  //     Serial.print(pluviometer[i]);
  //   }
  //   Serial.println("");
  // }
  for (int i = 0; i < n_minute; i = i + 1) {
    pluviometer[i]=0;
  }
}

[ sensores ] [ envía ] [ recibe ] [ principal ]

LoRaWan – Recibe trama

Manejo de tramas recibidas por el dispositivo para control o recibo de recibidos (Ack)

//downlink data handle and downLink Ack Handle functions
void downLinkDataHandle(McpsIndication_t *mcpsIndication) {
  // revisa parametros
  Serial.print("\nLlegó 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]);
  // }

  // parametros de recepcion
  ack_rssi = uint8_t(abs(mcpsIndication->Rssi));
  ack_snr  = uint8_t(mcpsIndication->Snr);
  ack_datarate = uint8_t(mcpsIndication->RxDoneDatarate);
  // recibido de trama
  Up_rssi = uint8_t(mcpsIndication->Buffer[0]);

  //
  Serial.print("Rx ack_Rssi:-"); Serial.print(ack_rssi);
  Serial.print(", ack_Snr:");Serial.print(ack_snr);
  Serial.print(", ack_Datarate: ");Serial.println(ack_datarate);
  Serial.print(" Up_rssi:");
  Serial.print(-1*Up_rssi);
  Serial.printf(" +REV DATA:%s,RXSIZE %d,PORT %d\r",mcpsIndication->RxSlot?"RXWIN2":"RXWIN1",mcpsIndication->BufferSize,mcpsIndication->Port);
  Serial.println();
}

void downLinkAckHandle(McpsIndication_t *mcpsIndication){
  ack_rssi = uint8_t(abs(mcpsIndication->Rssi));
  ack_snr  = uint8_t(mcpsIndication->Snr);
  ack_datarate = uint8_t(mcpsIndication->RxDoneDatarate);
  tx_done = true;
  // if (serial_msg){
  //   Serial.println(' ');
  //   Serial.print(" ack received(rssi,snr,datarate): -");
  //   Serial.print(ack_rssi);Serial.print(" ,");
  //   Serial.print(ack_snr);Serial.print(" ,");
  //   Serial.println(ack_datarate);
  // }
}

[ sensores ] [ envía ] [ recibe ] [ principal ]

LoRaWan – Bloque principal

El bloque principal se basa en el esquema básico de LoRaWan usado para sensores, cambiando la parte de ahorro de energía entre transmisiones DEVICE_STATE_SLEEP a una controlada por periodos de muestreo del pluviómetro con los procedimientos onSleep() y onWakeUp() descritos en la parte de sensores. Se controla las actividades de Lectura de sensores y modos de ahorro de energía por la sucesión de cada evento.

LoRaWan pluviometro bloque principal

// LoRaWan Pluviometer, temperature, humidity and barometric pressure
// 2023 April
// http://blog.espol.edu.ec/girni/lorawan-pluviometro-ino/
#include "LoRaWan_APP.h"
#include "Arduino.h"
#include <Wire.h>
#include <HDC1080.h>
#include <BMP180.h>

/* set LoraWan_RGB to Active,the RGB active in loraWan
 * red   |sending;   purple | joined done;
 * blue  |RxWindow1; yellow | means RxWindow2;
 * green | received done;
 */
/* LoRaWan: OTAA parameters*/
uint8_t devEui[] = { 0x2e, 0x4f, 0xa4, 0xdd, 0xf0, 0x2f, 0x06, 0xeb };
uint8_t appEui[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
uint8_t appKey[] = { 0x13, 0xb9, 0xd1, 0x66, 0x30, 0x2a, 0xeb, 0x53,
                     0x46, 0x6c, 0x0d, 0x2d, 0xa2, 0x31, 0x6b, 0xf0 };
/* ABP parameters*/
uint8_t nwkSKey[] = { 0xe2, 0x28, 0x89, 0xe0, 0x73, 0x22, 0xcb, 0xd1,
                      0xa7, 0x95, 0x64, 0x2e, 0xdb, 0xe5, 0x94, 0x42 };
uint8_t appSKey[] = { 0x1a, 0xfc, 0x10, 0xc5, 0x6f, 0xb8, 0xba, 0x86,
                      0x0d, 0xf3, 0xcf, 0xc5, 0xd2, 0xdb, 0x44 ,0xb8 };
uint32_t devAddr =  ( uint32_t )0x01d06174;
/*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;

// sample period
uint8_t sample_min = 0; 
uint8_t sample_seg = 5; 
uint32_t sample_period = (sample_min*60 + sample_seg);

uint32_t appTxDutyCycle = (sample_min*60 + sample_seg)*1000; // min*seg*ms

uint8_t appPort = 4; /* 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 reception parameters
uint8_t ack_rssi = 0;
uint8_t ack_snr = 0;
uint8_t ack_datarate = 0;

uint8_t Up_rssi = 0;

// serial print messages
bool serial_msg = true;

// time variables and control check
unsigned long sample_t0 = 0;
unsigned long sample_ti = 0;
unsigned long sample_dt = 0;
bool pluvio_flag = false;
bool sensor_flag = false;
unsigned long tx_t0 = 0;
bool tx_flag = false;
bool tx_done = false;
uint8_t tipfree = 0;
bool loop_0 = true;
bool sleep_flag = false;
bool sleep_done = true;

uint8_t i_minute = 0;
const uint8_t n_minute = 5;
uint8_t pulse_list[n_minute];
uint8_t pluviometer[n_minute];
uint8_t pulse = 0;
bool pulse_flag = false;
const uint8_t pluvioPin = GPIO5;

#define timetillsleep 60*60*1000
int timetillwakeup = sample_period*1000;
static TimerEvent_t sleep;
static TimerEvent_t wakeUp;
uint8_t lowpower=1;

//sensors libraries
HDC1080 hdc1080;  // humidity and temperature sensor
BMP085 bmp; // barometer and temperature sensor

// sensors I2C active check and value variables
bool humitemp_active = false;
bool prestemp_active = false;
float ht_temp = 0, ht_humi = 0;
float bar_temp = 0, bar_pres = 0;

uint16_t batteryVoltage = 0;

void setup() {
  Serial.begin(115200);

#if(AT_SUPPORT)
	enableAt();
#endif

  // OLED display status
  //LoRaWAN.displayMcuInit();

  pinMode(pluvioPin, INPUT); //OUTPUT_PULLUP);
  attachInterrupt(pluvioPin, pulse_tip, RISING);
  
  for (int i = 0; i < n_minute; i = i + 1) {
    pulse_list[i] = 0;
    pluviometer[i] = 0;
  }
  
  // sensors I2C Vcc pin control
  pinMode(Vext, OUTPUT);
  read_sensors();

  // time variables iniciated
  sample_t0 = millis();
  sample_ti = sample_t0;
  tx_t0 = sample_t0;
  
  deviceState = DEVICE_STATE_INIT;
	LoRaWAN.ifskipjoin(); //if joinned,skip

  // lowpower mode
  Radio.Sleep( );
  TimerInit( &sleep, onSleep );
  TimerInit( &wakeUp, onWakeUp );
  onWakeUp();
}

void loop() {
  if(lowpower){lowPowerHandler();}

  pulse_check();
  sample_t0 = millis();
  sample_dt = abs(sample_t0-sample_ti);
  if (sample_dt>=sample_period*1000){
    sample_ti = sample_t0;
    i_minute = i_minute + 1;
    if (pulse==0){ // tipfree counter do not overflow
      if (tipfree<=250){tipfree = tipfree + 1;}
      if (tipfree>250){tipfree = 1;}
    }
    pulse = 0;
    minute_check();
  }
  
  // read I2C sensors at i_minute=0
  if (i_minute==0 && sensor_flag == true){
    Serial.println("  reading sensors ...");
    read_sensors();
    sensor_flag = false;
    }

  // tx check
  if (i_minute==0 && tx_flag==false && tx_done == false && loop_0==false){
    tx_t0 = sample_t0;
    deviceState = DEVICE_STATE_SEND;
    tx_flag = true;
    tx_done = false;
    }
  if (i_minute >= n_minute){tx_flag = false;}
  
  // sleep check
  if (i_minute==0 && tipfree>=n_minute && sleep_flag==false && sleep_done==true && tx_done==true){
    sleep_flag = true;
    sleep_done = false;
    lowpower = 1;
    //timetillsleep ms later into lowpower mode;
    TimerSetValue( &sleep, 1 );
    TimerStart( &sleep );
    }
  if (i_minute > 0 && tipfree>=n_minute && sleep_flag==false && sleep_done==true){
    sleep_flag = true;
    sleep_done = false;
    lowpower = 1;
    //timetillsleep ms later into lowpower mode;
    TimerSetValue( &sleep, 1 );
    TimerStart( &sleep );
    }
  
	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();
      if (sleep_flag==true && sleep_done==false){ 
      //   if (serial_msg){
      //     Serial.print(" --- Sleep mode ; deviceState : ");
      //     Serial.print(deviceState);
      //     Serial.print(" ; i_minute : "); Serial.println(i_minute);
      //     }
        //LoRaWAN.sleep();
        sleep_done = true;
      }
			break;
		}
		default: {
			deviceState = DEVICE_STATE_INIT;
			break;
		}
	}
}

[ sensores ] [ envía ] [ recibe ] [ principal ]

LoRaWan – Probador con OLED en Chirpstack y HomeAssistant

ChirpStack

DECODER

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

ENCODER

function Encode(fPort, obj, variables) {
  var UP_rssi = obj["UP_rssi"];
  // var Up_snr = obj["UP_snr"];
  var mensaje = [UP_rssi];
  return mensaje;
}

Home Assistant

configuration.yaml

mqtt:
  sensor:
    - name: "rssi_up_cc50"
      unique_id: cc27rssiup
      state_topic: "application/1/device/a53ec615aede3f50/event/up"
      unit_of_measurement: "dBm"
      value_template: "{{ value_json.rxInfo[0].rssi}}"
      #availability:
      #  - topic: "home/sensor1/status"
      payload_available: "online"
      payload_not_available: "offline"
      json_attributes_topic: "application/1/device/a53ec615aede3f50/event/up"

Automation

Desencadenante

platform: mqtt
topic: application/1/device/a53ec615aede3f50/event/up

Accion

service: mqtt.publish
data:
  qos: 0
  retain: false
  topic: application/1/device/a53ec615aede3f50/command/down
  payload: '{"confirmed":false,"fPort":4,"object":{"UP_rssi":{{(states("sensor.rssi_up_cc50") | int)*(-1)}}}}'

LoRaWan – Probador de campo con OLED Archivo.ino

Pantalla OLED con parametros RSSI UP/DOWN

Probador de Rssi de subida y bajada

Hardware: Heltec cubecell AB02, con pantalla OLED incorporada.

OLED_pantalla.ino

void displayconectando(){
	display2.setFont(ArialMT_Plain_16);
	display2.setTextAlignment(TEXT_ALIGN_CENTER);
	display2.clear();
	display2.drawString(58, 22, "Joining...");
	display2.display();
}
void displayConectado(){
	display2.clear();
	display2.drawString(64, 22, "Conectado_OK");
	display2.display();
	delay(500);
}
void displayTransmitiendo(){
	digitalWrite(Vext,LOW);
	display2.init();
	display2.setFont(ArialMT_Plain_16);
	display2.setTextAlignment(TEXT_ALIGN_CENTER);
	display2.clear();
	display2.drawString(58, 22, "Tx...");
	display2.display();
	delay(500);
}
void displayPaqRecibido(){
  char temp[25];
	display2.clear();
  display2.setFont(ArialMT_Plain_16);
  display2.setTextAlignment(TEXT_ALIGN_LEFT);
  sprintf(temp,"Rx snr:%d,dr:%d",Down_snr, Down_datarate);
	display2.drawString(0, 0, temp); //22
	sprintf(temp,"rssiDw: -%d",Down_rssi);
	display2.setFont(ArialMT_Plain_24);
	display2.setTextAlignment(TEXT_ALIGN_RIGHT);
	display2.drawString(128, 16, temp);
  sprintf(temp,"rssiUp: -%d",Up_rssi);
	display2.setFont(ArialMT_Plain_24);
	display2.setTextAlignment(TEXT_ALIGN_RIGHT);
	display2.drawString(128, 40, temp);

  display2.drawString(0,0, temp);
	if(loraWanClass==CLASS_A)	{
		display2.setFont(ArialMT_Plain_10);
		display2.setTextAlignment(TEXT_ALIGN_LEFT);
		display2.drawString(28, 50, "Into deep sleep in 2S");
	}
	display2.display();
  delay(10000);
  display2.clear();
	if(loraWanClass==CLASS_A){
		digitalWrite(Vext,HIGH);
		display2.stop();
	}
}

void displayAckRecibido(){
  char temp[25];
	display2.clear();
  display2.setFont(ArialMT_Plain_16);
  display2.setTextAlignment(TEXT_ALIGN_LEFT);
  sprintf(temp,"ACK snr:%d,dr:%d",confirmaSnr, confirmaDatarate);
	display2.drawString(0, 0, temp); //22
	sprintf(temp,"rssiDw: -%d",confirmaRssi);
	display2.setFont(ArialMT_Plain_24);
	display2.setTextAlignment(TEXT_ALIGN_RIGHT);
	display2.drawString(128, 16, temp);
	if(loraWanClass==CLASS_A)	{
		display2.setFont(ArialMT_Plain_10);
		display2.setTextAlignment(TEXT_ALIGN_LEFT);
		display2.drawString(28, 50, "Into deep sleep in 2S");
	}
	display2.display();
  delay(10000);
	if(loraWanClass==CLASS_A)	{
		digitalWrite(Vext,HIGH);
		display2.stop();
	}
}

CubeCell_Practicas2023OLED.ino

// Lectura de Rssi Snr, datarate Up/Downlink
// Datos Downlink de la trama de confirmación anterior
// http://blog.espol.edu.ec/girni/lorawan-enlaces-up-down-archivo-ino/
#include "LoRaWan_APP.h"
#include "Arduino.h"
#include <Wire.h>               
#include "HT_SH1107Wire.h"

SH1107Wire  display2(0x3c, 500000, SDA, SCL ,GEOMETRY_128_64,GPIO10); // addr, freq, sda, scl, resolution, rst

/* 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[] = { 0xa5, 0x3e, 0xc6, 0x15, 0xae, 0xde, 0x3f, 0x50 };
uint8_t appEui[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
uint8_t appKey[] = { 0x88, 0xbe, 0x25, 0xca, 0x2c, 0xcf, 0x31, 0x85,
                     0x51, 0x2d, 0xee, 0xe2, 0x80, 0x31, 0x8e, 0x01 };
/* 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 )0x007bc4150;
/*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;

uint8_t duermemin = 0; //15
uint8_t duermeseg = 300; //0

uint32_t appTxDutyCycle = (duermemin*60 + duermeseg)*1000; // min*seg*ms

uint8_t appPort = 4; /* 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;

// Ack parametros de recepción
uint8_t Down_rssi = 0;
uint8_t Down_snr = 0;
uint8_t Down_datarate = 0;

uint8_t Up_rssi = 0;

uint8_t itera = 0;
uint8_t estado = 0; //0x00, 0x01,"OFF","ON"

void setup() {
	Serial.begin(115200);
 
#if(AT_SUPPORT)
	enableAt();
#endif

  // OLED display status
  //LoRaWAN.displayMcuInit();
  
	deviceState = DEVICE_STATE_INIT;
	//LoRaWAN.ifskipjoin(); //if joinned,skip

  display2.init();
  display2.setFont(ArialMT_Plain_10);
}

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();
      displayconectando();
			LoRaWAN.join();
			break;
		}
		case DEVICE_STATE_SEND:	{
      //LoRaWAN.displaySending();
      displayTransmitiendo();
			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;
		}
	}
}

LoRaWanEnvia.ino

/* 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);
}

LoRaWanRecibe.ino

//downlink data handle function example
void downLinkDataHandle(McpsIndication_t *mcpsIndication){
  // parametros de recepcion
  Down_rssi = uint8_t(abs(mcpsIndication->Rssi));
  Down_snr  = uint8_t(mcpsIndication->Snr);
  Down_datarate = uint8_t(mcpsIndication->RxDoneDatarate);
  // recibido de trama
  Up_rssi = uint8_t(mcpsIndication->Buffer[0]);

  //
  Serial.print("Rx Down_rssi:-"); Serial.print(Down_rssi);
  Serial.print(", Down_snr:");Serial.print(Down_snr);
  Serial.print(", Down_datarate: ");Serial.println(Down_datarate);
  Serial.print(" UP_rssi:");
  Serial.print(-1*Up_rssi);
  Serial.printf(" +REV DATA:%s,RXSIZE %d,PORT %d\r",mcpsIndication->RxSlot?"RXWIN2":"RXWIN1",mcpsIndication->BufferSize,mcpsIndication->Port);
  Serial.println();
  
  displayPaqRecibido();
}

LoRaWanRecibeConfirma.ino

void downLinkAckHandle(McpsIndication_t *mcpsIndication){
  // ACK parametros de recepcion
  confirmaRssi = uint8_t(abs(mcpsIndication->Rssi));
  confirmaSnr  = uint8_t(mcpsIndication->Snr);
  confirmaDatarate = uint8_t(mcpsIndication->RxDoneDatarate);

  Serial.println("");
  Serial.print(" ack received(rssi,snd,datarate): -");
  Serial.print(confirmaRssi);Serial.print(" ,");
  Serial.print(confirmaSnr);Serial.print(" ,");
  Serial.println(confirmaDatarate);
  
  displayAckRecibido();
}

LoRaWan – Enlaces Up/Down Decodificador en Chirpstack y HA

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

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

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

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

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

Automatización en Home-Assistant

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

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

LoRaWan – Enlaces Up/Down Archivo.ino

Preparación de trama

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

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

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

downLinkAckHandle(McpsIndication_t *mcpsIndication)

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

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

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

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

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

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

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

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

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

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

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

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

LoRaWan Enlace UpDown Parametros Ack

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

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

LoRaWan Enlace UpDown Enviar

/* Prepares the payload of the frame */
static void prepareTxFrame( uint8_t port ) {
  // enciende sensor
  pinMode(Vext, OUTPUT);
  digitalWrite(Vext, LOW);
  
  //Lectura de Sensor

  // apaga sensor
  digitalWrite(Vext, HIGH);
  
  // lectura de bateria  
  uint16_t batteryVoltage = getBatteryVoltage();
  unsigned char *puc;

  // trama
  appDataSize = 5;
  appData[0] = confirmaRssi; //Ack leido en dispositivo
  appData[1] = confirmaSnr;
  appData[2] = confirmaDatarate;
  appData[3] = (uint8_t)batteryVoltage;
  appData[4] = (uint8_t)(batteryVoltage>>8);

  Serial.print("%, Bateria = ");
  Serial.println(batteryVoltage);
}

Instrucciones Principales

LoRaWan Enlace UpDown Principal

#include "LoRaWan_APP.h"
#include "Arduino.h"

/* set LoraWan_RGB to Active,the RGB active in loraWan
 * red   |sending;   purple | joined done;
 * blue  |RxWindow1; yellow | means RxWindow2;
 * green | received done;
 */
/* Conexión LoRa: OTAA parametros*/
uint8_t devEui[] = { 0xa6, 0x17, 0x74, 0xe9, 0x5c, 0x1c, 0x98, 0xbd };
uint8_t appEui[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
uint8_t appKey[] = { 0x76, 0x9d, 0x1b, 0xc3, 0xf9, 0xe6, 0x7b, 0xbd,
                     0xa3, 0x4d, 0xe3, 0xcf, 0xbc, 0x8e, 0x35, 0x8f };
/* ABP parametros*/
uint8_t nwkSKey[] = { 0x15, 0xb1, 0xd0, 0xef, 0xa4, 0x63, 0xdf, 0xbe,
                      0x3d, 0x11, 0x18, 0x1e, 0x1e, 0xc7, 0xda,0x85 };
uint8_t appSKey[] = { 0x47, 0xdc, 0xac, 0x5f, 0xc2, 0x32, 0x24, 0x31, 
                      0xdf, 0xf1, 0xff, 0xf9, 0x46, 0xe5, 0x2e, 0x17 };
uint32_t devAddr =  ( uint32_t )0x007bc4af;
/*LoraWan channelsmask, default channels 0-7*/ 
uint16_t userChannelsMask[6]={ 0x00FF,0x0000,0x0000,0x0000,0x0000,0x0000 };

/*Select in arduino IDE tools*/
LoRaMacRegion_t loraWanRegion = ACTIVE_REGION;
DeviceClass_t  loraWanClass = LORAWAN_CLASS;
bool overTheAirActivation = LORAWAN_NETMODE;
bool loraWanAdr = LORAWAN_ADR;
bool keepNet = LORAWAN_NET_RESERVE;
bool isTxConfirmed = LORAWAN_UPLINKMODE;

uint32_t appTxDutyCycle = 1*15*1000;
uint8_t appPort = 2; /* Application port */
/* trials to transmit frame, if didn't receive ack.
 * The MAC performs a datarate adaptation,
 * Tx nb|Data Rate
 * -----|----------
 * 1    |DR           * 5    | max(DR-2,0)
 * 2    |DR           * 6    | max(DR-2,0)
 * 3    |max(DR-1,0)  * 7    | max(DR-3,0)
 * 4    |max(DR-1,0)  * 8    | max(DR-3,0)
*/
uint8_t confirmedNbTrials = 4;

// Ack parametros de recepción
uint8_t confirmaRssi = 0;
uint8_t confirmaSnr = 0;
uint8_t confirmaDatarate = 0;

uint8_t itera = 0;

void setup() {
	Serial.begin(115200);
#if(AT_SUPPORT)
	enableAt();
#endif
  LoRaWAN.displayMcuInit();
	deviceState = DEVICE_STATE_INIT;
	LoRaWAN.ifskipjoin(); //if joinned,skip
}

void loop() {
  Serial.print(".");
  itera = itera + 1;
  if (itera>6){
    itera = 0;
    Serial.println(" ");
  }
	switch( deviceState ) {
		case DEVICE_STATE_INIT: {
#if(LORAWAN_DEVEUI_AUTO)
			LoRaWAN.generateDeveuiByChipID();
#endif
#if(AT_SUPPORT)
			getDevParam();
#endif
			printDevParam();
			LoRaWAN.init(loraWanClass,loraWanRegion);
			deviceState = DEVICE_STATE_JOIN;
			break;
		}
		case DEVICE_STATE_JOIN: {
      LoRaWAN.displayJoining();
			LoRaWAN.join();
			break;
		}
		case DEVICE_STATE_SEND:	{
      LoRaWAN.displaySending();
			prepareTxFrame( appPort );
			LoRaWAN.send();
			deviceState = DEVICE_STATE_CYCLE;
			break;
		}
		case DEVICE_STATE_CYCLE: {
			// Schedule next packet transmission
			txDutyCycleTime = appTxDutyCycle + randr( 0, APP_TX_DUTYCYCLE_RND );
			LoRaWAN.cycle(txDutyCycleTime);
			deviceState = DEVICE_STATE_SLEEP;
			break;
		}
		case DEVICE_STATE_SLEEP: {
      LoRaWAN.displayAck();
			LoRaWAN.sleep();
			break;
		}
		default: {
			deviceState = DEVICE_STATE_INIT;
			break;
		}
	}
}

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

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

LoRaWan – Enlaces Up/Down Parámetros

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

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

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

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

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/