Pluviometro LoRaWan con open hardware y open software
Componentes de Hardware
Pluviómetro de cubo basculante
El componente principal es el colector de lluvia con cubo basculante. El colector adquirir solo o en kit con componentes para velocidad y dirección de viento..
Sensor digital de Presión atmosférica y temperatura BMP180. Comunicación tipo I2C, compatible con Arduino con la librería
#include "BMP180.h"
Placa de desarrollo LoRa
Placa de desarrollo LoRa que incluye controlador ARM y módulo de Radio, conexión USB, puerto para batería y panel solar. Por normativa para Ecuador se usa la versión con US915.
El desarrollo es compatible con otros módulos Lora de la serie CubeCell, se ha probado con el modelo "capsule" para la primera versión.
Las instrucciones para interpretar la trama en el gestor de gateways Chirpstack se realizan en JavaScript, siguiendo el mismo orden de bytes transmitido desde el dispositivo.
Para el caso de valores de temperatura se usan números enteros de 2 bytes con signo con dos decimales, según lo permite el sensor usado. El valor antes de transmitir se multiplica por 100. En la recepción se restaura el formato del número al dividirlo para 100.
Al final se añade el acumulado del pluviómetro de los últimos 5 minutos como pluviosum para fines generales.
Con esta parte se habilita la lectura de los parámetros para gestionar los datos en HomeAssistant mediante un mensaje Mqtt.
ChirpStack
// 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) {
var Down_rssi = -1*parseInt(bytes[0]);
var Down_snr = bytes[1];
var Down_datarate = bytes[2];
// usando entero sin signo
var bateria = (bytes[4] << 8) |(bytes[3]);
bateria = (bateria/1000);
bateria = +bateria.toFixed(2);
// usando entero con signo
var unalectura = (bytes[6] << 8) |(bytes[5]); var unsigno = (unalectura>>>15 === 0) ? 1.0 : -1.0;
var unvalor = unalectura & 0x7fff;
if (unsigno<0){unvalor = unvalor - 32768;}
var ht_temp = unvalor/100;
ht_temp = +ht_temp.toFixed(2);
var ht_humi = (bytes[8] << 8) |(bytes[7]);
ht_humi = ht_humi/100;
ht_humi = +ht_humi.toFixed(2);
var unalectura = (bytes[10] << 8) |(bytes[9]); var unsigno = (unalectura>>>15 === 0) ? 1.0 : -1.0;
var unvalor = unalectura & 0x7fff;
if (unsigno<0){unvalor = unvalor - 32768;}
var bar_temp = unvalor/100;
bar_temp = +bar_temp.toFixed(2);
var unalectura = (bytes[14] << 24) | (bytes[13]<< 16) | (bytes[12] << 8) |(bytes[11]); 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);
var bar_press = NumReal;
var pluvi0 = bytes[15];
var pluvi1 = bytes[16];
var pluvi2 = bytes[17];
var pluvi3 = bytes[18];
var pluvi4 = bytes[19];
var pluviosum = pluvi0 + pluvi1 + pluvi2 + pluvi3 + pluvi4;
var appData = {'Down_rssi':Down_rssi,'Down_snr':Down_snr,
'Down_datarate':Down_datarate,
'bateria_V':bateria,
'ht_temp':ht_temp,'ht_humi':ht_humi,
'bar_temp':bar_temp,'bar_press':bar_press,
'pluvi0':pluvi0,'pluvi1':pluvi1,'pluvi2':pluvi2,'pluvi3':pluvi3,'pluvi4':pluvi4,
'pluviosum':pluviosum,
}
return appData;
}
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".
Para 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.
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.
// sensors block// pluviometer pulse counter at interruptvoidpulse_tip(){if(pulse<=255){pulse=pulse+1;}pulse_flag=true;}voidpulse_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();}}voidminute_check(){inti=0;pulse_list[i_minute]=pulse;if(i_minute>=n_minute){// pluviometer updatefor(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);}}voidread_sensors(){batteryVoltage=getBatteryVoltage();// sensors check i2c onbytesensor_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();}floatbar_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 offWire.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("");}}voidonSleep(){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);}voidonWakeUp(){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.
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)
/* Prepares the payload of the frame */staticvoidprepareTxFrame(uint8_tport){unsignedchar*puc;signedchar*pucs;// tramaappDataSize=15+n_minute;// 15+pluvio_list sizeappData[0]=ack_rssi;//Ack leido en dispositivoappData[1]=ack_snr;appData[2]=ack_datarate;appData[3]=(uint8_t)batteryVoltage;appData[4]=(uint8_t)(batteryVoltage>>8);// convierte float a bytesintht_temp_int=round(ht_temp*100);pucs=(signedchar*)(&ht_temp_int);appData[5]=pucs[0];appData[6]=pucs[1];// convierte float a bytesintht_humi_int=round(ht_humi*100);puc=(unsignedchar*)(&ht_humi_int);appData[7]=puc[0];appData[8]=puc[1];// convierte float a bytesintbar_temp_int=round(bar_temp*100);pucs=(signedchar*)(&bar_temp_int);appData[9]=pucs[0];appData[10]=pucs[1];// convierte float a bytespuc=(unsignedchar*)(&bar_pres);appData[11]=puc[0];appData[12]=puc[1];appData[13]=puc[2];appData[14]=puc[3];for(inti=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(inti=0;i<n_minute;i=i+1){pluviometer[i]=0;}}
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 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_tdevEui[]={0x2e,0x4f,0xa4,0xdd,0xf0,0x2f,0x06,0xeb};uint8_tappEui[]={0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00};uint8_tappKey[]={0x13,0xb9,0xd1,0x66,0x30,0x2a,0xeb,0x53,0x46,0x6c,0x0d,0x2d,0xa2,0x31,0x6b,0xf0};/* ABP parameters*/uint8_tnwkSKey[]={0xe2,0x28,0x89,0xe0,0x73,0x22,0xcb,0xd1,0xa7,0x95,0x64,0x2e,0xdb,0xe5,0x94,0x42};uint8_tappSKey[]={0x1a,0xfc,0x10,0xc5,0x6f,0xb8,0xba,0x86,0x0d,0xf3,0xcf,0xc5,0xd2,0xdb,0x44,0xb8};uint32_tdevAddr=(uint32_t)0x01d06174;/*LoraWan channelsmask, default channels 0-7*/uint16_tuserChannelsMask[6]={0x00FF,0x0000,0x0000,0x0000,0x0000,0x0000};/*Select in arduino IDE tools*/LoRaMacRegion_tloraWanRegion=ACTIVE_REGION;DeviceClass_tloraWanClass=LORAWAN_CLASS;booloverTheAirActivation=LORAWAN_NETMODE;boolloraWanAdr=LORAWAN_ADR;boolkeepNet=LORAWAN_NET_RESERVE;boolisTxConfirmed=LORAWAN_UPLINKMODE;// sample perioduint8_tsample_min=0;uint8_tsample_seg=5;uint32_tsample_period=(sample_min*60+sample_seg);uint32_tappTxDutyCycle=(sample_min*60+sample_seg)*1000;// min*seg*msuint8_tappPort=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_tconfirmedNbTrials=4;// Ack reception parametersuint8_tack_rssi=0;uint8_tack_snr=0;uint8_tack_datarate=0;uint8_tUp_rssi=0;// serial print messagesboolserial_msg=true;// time variables and control checkunsignedlongsample_t0=0;unsignedlongsample_ti=0;unsignedlongsample_dt=0;boolpluvio_flag=false;boolsensor_flag=false;unsignedlongtx_t0=0;booltx_flag=false;booltx_done=false;uint8_ttipfree=0;boolloop_0=true;boolsleep_flag=false;boolsleep_done=true;uint8_ti_minute=0;constuint8_tn_minute=5;uint8_tpulse_list[n_minute];uint8_tpluviometer[n_minute];uint8_tpulse=0;boolpulse_flag=false;constuint8_tpluvioPin=GPIO5;#definetimetillsleep60*60*1000inttimetillwakeup=sample_period*1000;staticTimerEvent_tsleep;staticTimerEvent_twakeUp;uint8_tlowpower=1;//sensors librariesHDC1080hdc1080;// humidity and temperature sensorBMP085bmp;// barometer and temperature sensor// sensors I2C active check and value variablesboolhumitemp_active=false;boolprestemp_active=false;floatht_temp=0,ht_humi=0;floatbar_temp=0,bar_pres=0;uint16_tbatteryVoltage=0;voidsetup(){Serial.begin(115200);#if(AT_SUPPORT)enableAt();#endif// OLED display status//LoRaWAN.displayMcuInit();pinMode(pluvioPin,INPUT);//OUTPUT_PULLUP);attachInterrupt(pluvioPin,pulse_tip,RISING);for(inti=0;i<n_minute;i=i+1){pulse_list[i]=0;pluviometer[i]=0;}// sensors I2C Vcc pin controlpinMode(Vext,OUTPUT);read_sensors();// time variables iniciatedsample_t0=millis();sample_ti=sample_t0;tx_t0=sample_t0;deviceState=DEVICE_STATE_INIT;LoRaWAN.ifskipjoin();//if joinned,skip// lowpower modeRadio.Sleep();TimerInit(&sleep,onSleep);TimerInit(&wakeUp,onWakeUp);onWakeUp();}voidloop(){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 overflowif(tipfree<=250){tipfree=tipfree+1;}if(tipfree>250){tipfree=1;}}pulse=0;minute_check();}// read I2C sensors at i_minute=0if(i_minute==0&&sensor_flag==true){Serial.println(" reading sensors ...");read_sensors();sensor_flag=false;}// tx checkif(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 checkif(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){caseDEVICE_STATE_INIT:{#if(LORAWAN_DEVEUI_AUTO)LoRaWAN.generateDeveuiByChipID();#endif#if(AT_SUPPORT)getDevParam();#endifprintDevParam();LoRaWAN.init(loraWanClass,loraWanRegion);deviceState=DEVICE_STATE_JOIN;break;}caseDEVICE_STATE_JOIN:{//LoRaWAN.displayJoining();LoRaWAN.join();break;}caseDEVICE_STATE_SEND:{//LoRaWAN.displaySending();prepareTxFrame(appPort);LoRaWAN.send();deviceState=DEVICE_STATE_CYCLE;break;}caseDEVICE_STATE_CYCLE:{// Schedule next packet transmission//txDutyCycleTime = appTxDutyCycle + randr( 0, APP_TX_DUTYCYCLE_RND );//LoRaWAN.cycle(txDutyCycleTime);deviceState=DEVICE_STATE_SLEEP;break;}caseDEVICE_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;}}}