Medir el consumo del gas con Arduino

Modificado por última vez hace 2 meses

Los que son habituales del blog, ya saben que me gusta medirlo todo, y el gas no podía ser menos. Llevo años midiendo el consumo del gas con Arduino, pero lo importante no es eso, lo importante es que ese Arduino lleva casi seis años enviando los datos inalámbricamente con solo dos pilas AA, y sigue funcionando…

Una cosa que no mucha gente sabe, es que suele ser muy fácil de medir porque los contadores tienen una «salida magnética» que puede ser leída fácilmente.

La dificultad está, más que otra cosa, en cómo enviar los datos desde el contador, que suele estar inaccesible y sin tomas cercanas de corriente.

Durante un tiempo estuve leyendo el consumo de gas mediante un Jeenode (una especie de Arduino con un módulo de radiofrecuencia para aplicaciones de bajo consumo). El sistema funcionó muy bien durante unos meses hasta que, bueno, hasta que ¡me lo robaron del cuadro del contador!

Un buen día simplemente dejaron de llegar lecturas, fui a ver qué pasaba, y mi querido aparatito había desaparecido.

Llevo ya casi un año sin leer el gas, y con los cambios que he hecho este año en la caldera y en el sistema de control de la calefacción me vendría bien volver a tener datos en tiempo real para poder optimizar su funcionamiento y saber si los cambios que voy implementando se traducen en un ahorro de gas.

Medir el consumo del gas no es difícil, en la mayoría de los casos.

Afortunadamente, los contadores de gas suelen tener un imán que se mueve con los numeritos esos que dan vueltas, de manera que podemos detectar cada vuelta del número y calcular el consumo de esa manera.

En principio quería hacer el sistema de medición mediante un ESP8266 y MQTT (y lo preparé) el problema es que en las inmediaciones del contador de gas no dispongo de una toma eléctrica cercana y el ESP8266 es un poco hambriento, energéticamente hablando, como para alimentarlo con batería.

Por otra parte, los JeeNodes son un poco caros, para lo que hay por ahí hoy en día.

Como quiera que ya tengo un receptor para los JeeNodes instalado e integrado a través de Nodered (mi sistema de lectura de consumo eléctrico era un EmonTX, hasta que lo sustituí por un IoTaWatt, que se basa y es compatible con JeeNode), y todos los componentes para hacerme un JeeNode casero por poco dinero, he decidido hacerlo así.

Con un Arduino Pro Mini, de los que se pueden conseguir en AliExpress por menos de 2€, debidamente modificado para mejorar su consumo (quitando el led de encendido, que consume mucho para esta aplicación y eliminando el regulador de tensión, ya que lo voy a utilizar con pilas y espero que duren varios meses), y un módulo de radio RFM12B que ya tenía, me he construido un JeeNode casero en un par de horas.

El mayor problema es el sketch que hay que cargar en el Arduino, y es que, tras un par de años desde que escribí el viejo, no sé dónde está, no lo encuentro, ni vivo ni muerto, por lo que he tenido que escribir uno nuevo. El sketch, en sí, es muy sencillo, la dificultad viene porque hay que optimizarlo para que consuma lo mínimo posible, lo que supone algún quebradero de cabeza adicional.

Como en la versión anterior, he utilizado la estupenda librería de Arduino Jeelib. Esta librería es una maravilla para implementar módulos de bajo consumo con comunicación vía radio. Es ya un poco antigua, pero es justo lo que necesito y estoy familiarizado con ella.

La verdad es que la optimización de su consumo me ha llevado mucho más tiempo del que pensaba. Al final me he pasado una semana de pruebas, de medidas y de cambios en el código hasta conseguir lo que quería. 

Este es el dispositivo, una vez terminado:

Medidor Gas Arduino

Edito: Incluyo este gráfico un año después (febrero de 2018), donde se puede ver que el invento funciona espectacularmente bien. Tras un año de uso ininterrumpido, la batería (dos pilas AA de 1.5 Voltios) sigue estando por encima de 3 Voltios, por lo que se prevén muchos más meses de funcionamiento antes de tener que cambiar las pilas

Edito 2: Incluyo este gráfico tres años después (febrero de 2021).

El sistema ¡sigue funcionando tras más de cuatro años funcionando ininterrumpidamente con las mismas pilas (que ni siquiera eran nuevas cuando las puse para probar por primera vez)!

Como ves, el voltaje de las pilas todavía está sobre los 2.92V. Nada mal…

Edito 3: Incluyo este gráfico otros casi tres años después (diciembre de 2023).

El sistema ¡SIGUE FUNCIONADO TRAS MÁS DE SEIS AÑOS funcionando ininterrumpidamente con las mismas pilas (que ni siquiera eran nuevas cuando las puse para probar por primera vez)!

El voltaje de las pilas todavía está sobre los 2.8V, por lo que queda batería para rato. ¿Hasta dónde llegará?

El código para Arduino

Aunque no estoy seguro si esta es la última versión del código (lo perdí hace años y lo acabo de encontrar), puede ser un buen punto de partida si tienes curiosidad por saber cómo funciona o si quieres replicarlo.

// (c) eMariete 2016-2023 https://emariete.com/medir-consumo-gas-arduino/
// Este sketch para Arduino se llama ContadorReedDelGas.ino:
// Se utiliza para contar los pulsos de un sensor de gas de tipo reed y enviar los datos a través de RFM12B a un nodo de registro.
// El código se basa en el código de ejemplo de JeeLib y se ha modificado para adaptarse a las necesidades de este proyecto.
// También está basado en código encontrado en el foro de Open Energy Monitor pero han pasado años y no localizo nombre ni autor (lo siento)

/* Asignación recomendada de ID de nodo
------------------------------------------------------------------------------------------------------------
-ID-  - Tipo de Nodo
0     - Asignación especial en el controlador JeeLib RFM12: reservado para uso OOK
1-4   - Nodos de control
5-10  - Nodos de monitoreo de energía
11-14 - No asignado--
15-16 - Estación base y nodos de registro
17-30 - Nodos de detección ambiental (temperatura, humedad, etc.)
31    - Asignación especial en el controlador JeeLib RFM12: el Nodo31 puede comunicarse con nodos en cualquier grupo de red
-------------------------------------------------------------------------------------------------------------
*/

#define RF_freq RF12_868MHZ                                                // Frecuencia del módulo RF12B, puede ser RF12_433MHZ, RF12_868MHZ o RF12_915MHZ.
const int nodeID = 18;                                                     // ID del nodo Jeenode
const int networkGroup = 212;                                              // Grupo de red inalámbrica Jeenode RFM12B
const int UNO = 1;                                                         // Establecer en 0 si no se está utilizando el cargador de arranque UNO (es decir, utilizando Duemilanove)

#include <avr/wdt.h>                                                       // El cargador de arranque UNO
#include <JeeLib.h>                                                        // Descargar JeeLib: http://github.com/jcw/jeelib

ISR(WDT_vect) {
  Sleepy::watchdogEvent();
}
#include <avr/power.h>
#include <avr/sleep.h>
#include <PinChangeInt.h>                                                  // Interrupciones de cambio de pin.

typedef struct {
  unsigned int pulse, totalpulse, battery;
} PayloadGas;
PayloadGas gas;                                                          // Forma ordenada de empaquetar datos para la comunicación RF

const  unsigned long WDT_PERIOD = 1000;                                    // milisegundos.
const  unsigned long WDT_MAX_NUMBER = 10;                                  // Datos enviados después de WDT_MAX_NUMBER periodos de  WDT_PERIOD ms sin pulsos
//                              O
const  unsigned long PULSE_MAX_NUMBER = 5;                                 // Datos enviados después de PULSE_MAX_NUMBER pulsos
const  unsigned long PULSE_MAX_DURATION = 10;                              // El sensor se apaga durante PULSE_MAX_DURATION milisegundos después de un pulso para desactivar el rebote.

const int BATT_ADC = A0;
const int magPin = 3;                                                      // Sensor magnético
const int ledPin = 13;                                                     // LED de depuración.

// Objetos
PCintPort PCintPort();

volatile unsigned long pulseCount ;
volatile unsigned long elapsed_time;
unsigned long WDT_number;

boolean  debug = true;
boolean  p;                                                                // Bandera para nuevo pulso

void setup()
{
  Serial.begin(57600);
  Serial.println("ContadorReedDelGas.ino V1.0c");
  Serial.print("Grupo Id: ");
  Serial.println(networkGroup);
  Serial.print("ID de nodo: ");
  Serial.println(nodeID);
  Serial.print("Enviando datos después de ");
  Serial.print(PULSE_MAX_NUMBER);
  Serial.println(" pulsos");
  Serial.print("  o  ");
  Serial.print(WDT_MAX_NUMBER);
  Serial.print(" periodos de ");
  Serial.print(WDT_PERIOD);
  Serial.println(" ms sin ningún pulso");
  Serial.println("Inicializando RF..");
  delay(100);
  rf12_initialize(nodeID, RF_freq, networkGroup);                          // Inicializar RF
  rf12_sleep(RF12_SLEEP);
  Serial.println("RF listo");
  delay(50);

  gas.pulse = 0;
  gas.totalpulse = 0;
  gas.battery = 0;
  pulseCount = 0;
  analogReference(INTERNAL);
  delay(50);

  WDT_number = 0;

  if (Serial) debug = 1; else debug = 0;                                   // Si la UART serie a USB está conectada, mostrar la salida de depuración. Si no, deshabilitar la serie.
  debug = true;
  p = 0;

  pinMode(magPin, INPUT);                                                  // Establecer el pin como entrada
  digitalWrite(magPin, HIGH);                                              // No utilizar la resistencia pull-up interna
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, !0);

  // attachInterrupt(1, onPulse, RISING);                                  // Interrupción de kWh adjunta a IRQ 0  = Digita 2 - conectado directamente al bloque de terminales
  PCintPort::attachInterrupt(magPin, onPulse, FALLING);

  gas.battery = (unsigned int)(analogRead(BATT_ADC) *   0.542);            // La primera lectura de DAC es inexacta
}

/**
 * The main loop function that runs repeatedly in the program.
 * It checks for incoming pulses, performs fading, and sends RF data.
 */
void loop()
{
  // Si acaba de llegar un pulso, esperar PULSE_MAX_DURATION ms para el desvanecimiento
  if (p) {
    Serial.flush;
    Sleepy::loseSomeTime(PULSE_MAX_DURATION);
    if (debug == debug) {
      flashLed(25);
    }
    p = 0;
  }

  Serial.flush;
  if (Sleepy::loseSomeTime(WDT_PERIOD) == 1) {                            // Verdadero si no ocurrió ningún pulso durante el WDT
    WDT_number++;                                                         // Número de WDT finalizados normalmente desde el último envío de RF
    if (debug) {
      Serial.print(".");
      delay(3);
    }
  }
  else if (debug)
  {
    Serial.print("INT ");
    delay(5);
    Serial.println(pulseCount);
    delay(5);
  }

  if (WDT_number >= WDT_MAX_NUMBER || pulseCount >= PULSE_MAX_NUMBER) {
    cli(); // Desactivar la interrupción en caso de que llegue un pulso mientras estamos actualizando el conteo
    gas.totalpulse += (unsigned int) pulseCount;
    gas.pulse = (unsigned int) pulseCount;
    pulseCount = 0;
    sei();

    gas.battery = (unsigned int)(readVcc());

    if (debug == 1) {
      Serial.print("ADC: "); delay(5); Serial.println(readVcc()); delay(5);
      Serial.print("Batería: "); delay(5); Serial.println(gas.battery); delay(5);
    }

    if (debug == 1) {
      Serial.println(""); delay(5);
      Serial.print("Enviando "); delay(5);
      Serial.print(gas.pulse); delay(5);
      Serial.print("/"); delay(5);
      Serial.print(gas.totalpulse); delay(5);
      Serial.print("/"); delay(5);
      Serial.print(gas.battery); delay(5);
      Serial.println(""); delay(5);
    }

    send_rf_data();                          // *ENVIAR DATOS RF* - 
    WDT_number = 0;
  }
}

// La rutina de interrupción - se ejecuta cada vez que se detecta un flanco descendente de un pulso
void onPulse()
{
  p = 1;                                     // Bandera para nuevo pulso establecida a verdadero
  pulseCount++;                              // Número de pulsos desde el último envío de RF
}

// Calcular el voltaje de la MCU (mV).
long readVcc() {
  long result;
  ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
  Sleepy::loseSomeTime(2);
  ADCSRA |= _BV(ADSC);
  while (bit_is_set(ADCSRA, ADSC));
  result = ADCL;
  result |= ADCH << 8;
  result = 1126400L / result;
  return result;
}

// Enviar datos a través del módulo RF
void send_rf_data()
{
  power_spi_enable();
  rf12_sleep(RF12_WAKEUP);
  rf12_sendNow(0, &gas, sizeof gas);   // Enviar datos de temperatura a través de RFM12B utilizando el wraper rf12_sendNow
  rf12_sendWait(2);
  rf12_sleep(RF12_SLEEP);
  power_spi_disable();
}

// Parpadear un LED con fines de depuración
void flashLed (int i) {
  digitalWrite(ledPin, !1);
  Sleepy::loseSomeTime(i);
  digitalWrite(ledPin, !0);
}

Este código de Arduino se utiliza para contar los pulsos de un sensor de gas de tipo reed y enviar los datos a través de un módulo RFM12B a un nodo de registro. Aquí hay una descripción general del código:

  1. Configuración de parámetros:
    • Se define la frecuencia del módulo RF12B (RF_freq) y otros parámetros como el ID del nodo (nodeID), el grupo de red inalámbrica (networkGroup), y la configuración para el cargador de arranque UNO (UNO).
    • Se especifican constantes para la duración y el número máximo de pulsos (PULSE_MAX_DURATION y PULSE_MAX_NUMBER) y el número máximo de periodos de tiempo sin pulsos (WDT_MAX_NUMBER y WDT_PERIOD).
  2. Librerías y estructuras:
    • Se incluyen las librerías necesarias como avr/wdt.h, JeeLib.h, y PinChangeInt.h.
    • Se define una estructura (PayloadGas) para empaquetar los datos a enviar por RF.
  3. Inicialización:
    • Se inicializan variables y objetos, como el objeto PCintPort para gestionar interrupciones por cambio de pin.
    • Se configuran pines de entrada y salida, y se establece la referencia interna del ADC.
  4. Rutina de interrupción y funciones auxiliares:
    • La rutina de interrupción (onPulse()) se ejecuta cuando se detecta un flanco descendente de un pulso en el pin del sensor magnético.
    • La función readVcc() se utiliza para medir el voltaje de la MCU.
    • send_rf_data() envía los datos a través del módulo RFM12B.
    • flashLed() parpadea un LED con fines de depuración.
  5. Loop principal:
    • Se ejecuta continuamente.
    • Espera pulsos y realiza un tiempo de espera después de cada pulso para evitar rebotes.
    • Contabiliza el número de pulsos y envía datos a través del módulo RFM12B después de un número específico de pulsos o periodos sin pulsos.
  6. Depuración:
    • Se utiliza la variable debug para controlar la salida de depuración a través de la interfaz serie.
    • Se parpadea un LED cuando se detecta un nuevo pulso.

Es importante tener en cuenta que el código utiliza la biblioteca JeeLib, por lo que deberías asegurarte de tenerla instalada para que el código funcione correctamente. Además, si planeas utilizar este código, asegúrate de comprender cómo funciona y adapta los parámetros según tus necesidades específicas.

4 comentarios en «Medir el consumo del gas con Arduino»

  1. Cómo se hace?. Me parece muy bien pero no se por donde empezar para hacer algo así y muchas pistas no das. Cómo lo hiciste en arduino.?

    Responder
    • Hola Fernando.

      Este es un proyecto muy antiguo que fue hecho como algo «rápido y sucio» para dar solución al problema.

      Actualmente estoy trabajando en una nueva versión con ESP32. No voy muy deprisa, pero sí que hay cierto avance. Lo publicaré cuando tenga terminada una primera versión.

      Un saludo.

      Responder
      • Nos vale que sea algo rápido y sucio para ver por dónde van los tiros 😉

        Sería interesante al menos, ver cómo lees el tema del imán y su algoritmo.

        Podrías poner en descarga o en esta misma web el código para la lectura del contador?

        muchas gracias!!

        Responder
        • Hola Juan Carlos,

          Aunque no estoy seguro de que sea la última versión, acabo de encontrar el código de Arduino para leer los pulsos con el contacto magnético. Lo he incluido en el post por si puede ayudar a alguien.

          Un saludo.

          Responder

Deja un comentario