Herramientas de usuario

Herramientas del sitio


estacion_meteorologica_y_sistema_de_riego_autonomo_con_d1_mini

Estación meteorológica y sistema de riego autónomo con D1 Mini

21/03/2023

Descripción

Hoy mostraré este «pequeño» aunque a la vez uno de los más útiles montajes que he hecho nunca desde que me dedico a la «cosa maker». Se trata de una mini-estación meteorológica que transmite los datos de temperatura, humedad y presión atmosférica a un servidor MQTT, a la vez que gestiona el riego autónomo de una maceta en función de la humedad del suelo medida por un sensor llamado «higrómetro» activando una bomba de agua.

El higrómetro (de tipo capacitivo, más eficiente y con más duración que los de tipo resistivo) irá ubicado en la tierra de una maceta, como se puede ver en la foto, y el termómetro digital BME280 debería ubicarse en un sitio protegido de la lluvia y la humedad, a la sombra y a la vez expuesto al medio, para que las medidas sean correctas.


Esquema

En el esquema se puede observar que el conjunto recibe la alimentación de un módulo que nos proporciona pines con la misma corriente de entrada (12V), y a la vez nos proporciona pines de 3.3V y 5V, como puede verse en la imagen de la izquierda. Es un módulo muy interesante porque nos permite distribuir diferentes buses de corriente para alimentar a otros módulos con voltajes distintos. Por ejemplo, el relé se activará con una corriente de 5V (se pueden usar relés de otros voltajes, como 3.3V o 12V, pero hay que tener en cuenta que entonces habrá que conectarlo a la salida correspondiente de ese voltaje en el módulo alimentador), pero cerrará un circuito de 12V para que funcione la bomba de agua, y todo procederá del mismo módulo alimentador. El módulo de alimentación también proporcionará los 3.3V que necesita el termómetro digital BME280 y los 5V que necesita el medidor de humedad del suelo. Casi se podría decir que este módulo es la pieza más importante de todo el conjunto. Chulo, ¿verdad?

El módulo BME280 es de tipo I2C, y se conectará, como aparece en el esquema, a la alimentación de 3.3V y a los pines SDA (D2) y SDL (D1) del microcontrolador D1 Mini.

Por su parte, el higrómetro puede ser alimentado con 3.3V o 5V, y se conectará a la entrada analógica A0 del microcontrolador D1 Mini.

La bomba de agua (en mi caso es como la de la foto de la derecha, de 12V y 5W de potencia) se conectará con su polaridad correcta, obviamente, a la salida de 12V controlada por el relé. Este montaje está pensado para una bomba de agua pequeña. En caso de necesitar una bomba de agua de más potencia hay que tener en cuenta que el módulo alimentador no podrá suministrarle la energía necesaria, y habrá que tener también en cuenta las especificaciones del relé que instalemos, que puede ser un módulo de relé externo como el de la foto de la derecha. Con este tipo de relé podríamos alimentar una bomba de hasta 220V y 10A (cosa que recomiendo encarecidamente que no hagáis, por más que lo digan las especificaciones. Para estos casos, mejor instalar un contactor además del relé, que son caros, pero mucho más seguros).


Código

Aunque debido a lo extenso que es no voy a comentar todo el código, sí me voy a centrar en algunas partes del mismo que me parecen más importantes. En primer lugar, en la estructura de datos que grabaremos en la EEPROM del D1 Mini para que sean datos persistentes que no se pierdan en un reinicio o un corte de electricidad:

// EEPROM
struct datosEeprom {
  int NTPOffset = 3600;
  int emitir_timer = 10;
  int conectar_timer = 60;
  // Datos específicos del controlador "palmerita"
  int tiempo_medida = 5;               // Intervalo entre medidas 5 segundos
  int tiempo_riego = 10;               // 10 segundos de riego
  int tiempo_intervalo_riego = 43200;  // Cada 12 horas como mínimo
  int umbral_humedad = 350;            // Umbral mínumo de humedad para efectuar el riego
  bool modo = true;                    // true: modo automático ; false: modo manual
};
datosEeprom eepromData;

Entre estas variables hay que destacar «NTPOffset», que controla el horario de verano/invierno. Uno de mis bots en python emite por MQTT cada 15 minutos el offset horario correcto, y cuando éste cambia, más adelante veremos cómo se recibe la orden de modificar el NTPOffset y grabar de nuevo la EEPROM para que el cambio sea permanente. En este caso no es esencial que el controlador disponga de la hora correcta, pero son funciones que incluyo en cada uno de los sketches para tenerlas disponibles en caso necesario.

Las siguientes variables, «emitir_timer» y «conectar_timer» indican los intervalos entre transmisiones al servidor MQTT y entre verificaciones de la conexión al wifi. Los valores por defecto son 10 y 60 segundos, respectivamente.

«tiempo_medida», «tiempo_riego» y «tiempo_intervalo_riego» indican los intervalos para tomar medidas, el tiempo de riego efectivo en cada proceso de regado y el intervalo a esperar entre un proceso de riego y el siguiente. Por defecto, 5, 10 y 43.200 segundos (12 horas).

«umbral_humedad» indica el mínimo de humedad necesario para que se inicie un proceso de riego. Si la humedad tiene valores inferiores a 350 (más humedad), el proceso no se iniciará.

«modo» Es una variable booleana (true o false) para determinar si el sistema se encuentra en modo automático (riego autónomo) o en modo manual (riego según órdenes). Si el sistema se encuentra en modo manual, el sistema no regará aunque se rebase el umbral de humedad del suelo.

Y con esto terminan las variables que serán almacenadas en EEPROM para que la programación del sistema no se altere tras un reinicio o un corte de corriente.


La función «conectar()», como es habitual en mis sketches, no se queda esperando a tener una conexión wifi, sino que lo intenta diez veces como máximo cada minuto (o el tiempo definido en «eepromData.conectar_timer») para que el sistema pueda seguir funcionando incluso en el caso de que el wifi falle durante un tiempo.

int contador = 0;
while (WiFi.status() != WL_CONNECTED && contador < 10) {
    conectado_wifi = false;
    Serial.print("Conectando a WiFi... ");
    Serial.println(contador);
    contador++;
    delay(800);
  }

Una vez conseguida la conexión wifi, se procederá a conectarse (en caso de haber quedado desconectado previamente) al servidor MQTT. También hacemos que este proceso tenga un número de intentos limitado para no caer en un bucle sin fin que bloquee el sistema.

// Iniciamos el proceso de intento de conexión a MQTT
    if (!client.connected() || !conectado_mqtt) {
      contador = 0;
      conectado_mqtt = false;
      while (!client.connect(clientID, mqtt_username, mqtt_password) && contador < 10) {
        Serial.print("Conectando a MQTT... ");
        Serial.println(contador);
        contador++;
        delay(500);
      }
    }

En cuanto a la gestión de las órdenes recibidas a través de MQTT, el dispositivo se suscribe a «casa/ordenes/#» ,de manera que recibe todas las órdenes que se transmitan a través de este topic a todos los dispositivos de la casa. Como he comentado antes, cada 15 minutos se actualiza el offset horario, que es transmitivo a través del topic «casa/ordenes/offset» y gracias al cual todos los dispositivos cambian el horario de verano o invierno según dicho offset.

Además, el dispositivo reaccionará a órdenes dirigidas sólo a él a través del topic «casa/ordenes/palmerita»:

  * E - Graba la configuración actual en EEPROM
  * D - Devuelve los datos e configuración actuales
  * M - Cambia el modo de automático a manual
  * R - Ejecuta un ciclo de riego incondicional
  * T xx - Cambia el tiempo de riego
  * I xx - Cambia el intervalo de tiempo mínimo entre riegos
  * U xx - Cambia el umbral de humedad necesario para ejecutar el riego

El código completo del sketch (a falta de los datos de configuración de wifi y MQTT, es el siguiente:

#include <EEPROM.h>
#include <ESP8266WiFi.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <PubSubClient.h>
#include <Adafruit_BME280.h>
 
// EEPROM
struct datosEeprom {
  int NTPOffset = 3600;
  int emitir_timer = 10;
  int conectar_timer = 60;
  // Datos específicos del controlador "palmerita"
  int tiempo_medida = 5;               // Intervalo entre medidas 5 segundos
  int tiempo_riego = 10;               // 10 segundos de riego
  int tiempo_intervalo_riego = 43200;  // Cada 12 horas como mínimo
  int umbral_humedad = 350;            // Umbral mínumo de humedad para efectuar el riego
  bool modo = true;                    // true: modo automático ; false: modo manual
};
datosEeprom eepromData;
 
// Datos de conexión WiFi
char* ssid = "SSID_de_tu_wifi";
char* passwd = "password_de_tu_wifi";
WiFiClient wifiClient;
bool conectado_wifi = false;
time_t conectar_contador = 0;
 
// Datos de conexión MQTT
char* mqtt_server = "192.168.1.118"; // Dirección IP o URL del servidor MQTT
int mqtt_port = 1883;
char* mqtt_username = "usuario_de_tu_servidor_MQTT";
char* mqtt_password = "password_de_tu_servidor_MQTT";
char* topic_ordenes_general = "casa/ordenes/#";
char* clientID = "palmerita";
char* topic_respuesta = "casa/palmerita/respuesta";
char* topic_ordenes_especifico = "casa/ordenes/palmerita";
char* topic_temperatura = "casa/palmerita/temperatura";
char* topic_humedad = "casa/palmerita/humedad";
char* topic_presion = "casa/palmerita/presion";
char* topic_suelo = "casa/palmerita/suelo";
PubSubClient client(mqtt_server, mqtt_port, wifiClient);
bool conectado_mqtt = false;
time_t emitir_contador = 0;
 
// Datos de conexión NTP
const char* servidor_NTP = "192.168.1.118"; // Dirección IP o URL de un servidor NTP público o privado
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, servidor_NTP, eepromData.NTPOffset);
time_t tiempoUNIX = 0;
time_t tiempoUNIX_UTC = 0;
struct tm fechaHora;
 
// Termómetro digital BME
Adafruit_BME280 bme;
float temperatura = 0.0;
float humedad = 0.0;
float presion = 0.0;
 
// Medidor de humedad del suelo
int humedad_suelo = 0;
 
// Pines de E/S
int circuito_rele = D5;
 
// Contadores
time_t contador_medida = 0;
time_t contador_riego = 0;
time_t contador_intervalo_riego = 0;
// Banderas de estado
bool regando = false;
 
// Graba los datos de la struct eepromData en memoria eeprom
void grabarEeprom(datosEeprom d) {
  Serial.println("Iniciando grabación de EEPROM...");
  EEPROM.begin(4096);
  EEPROM.put(0, d);
  EEPROM.end();
  Serial.println("EEPROM grabada");
}
 
// Lee los datos de la struct eepromData desde memoria eeprom
datosEeprom leerEeprom() {
  datosEeprom d;
  Serial.println("Iniciando lectura de EEPROM...");
  EEPROM.begin(4096);
  EEPROM.get(0, d);
  EEPROM.end();
  Serial.println("EEPROM leída");
  return d;
}
 
// Intenta conectar los servicios de WiFi, NTP y MQTT
void conectar() {
  // Iniciamos el proceso de intento de conexión
  int contador = 0;
  while (WiFi.status() != WL_CONNECTED && contador < 10) {
    conectado_wifi = false;
    Serial.print("Conectando a WiFi... ");
    Serial.println(contador);
    contador++;
    delay(800);
  }
  // Finalizado el proceso, comprobamos el estado de la conexión
  if (WiFi.status() == WL_CONNECTED) {
    // Si venimos de estado desconectado, avisar de la nueva conexión
    if (!conectado_wifi) {
      Serial.println("Conectado con éxito a WiFi");
    }
    conectado_wifi = true;
    // Iniciamos el proceso de intento de conexión a MQTT
    if (!client.connected() || !conectado_mqtt) {
      contador = 0;
      conectado_mqtt = false;
      while (!client.connect(clientID, mqtt_username, mqtt_password) && contador < 10) {
        Serial.print("Conectando a MQTT... ");
        Serial.println(contador);
        contador++;
        delay(500);
      }
    }
    if (client.connected()) {
      // Si venimos de estado desconectado, avisar de la nueva conexión
      if (!conectado_mqtt) {
        Serial.println("Conectado con éxito a MQTT");
      }
      conectado_mqtt = true;
      client.subscribe(topic_ordenes_general);
      client.loop();
    } else {
      Serial.println("***Error de conexión MQTT");
    }
  } else {
    Serial.println("***Error de conexión WiFi");
  }
}
 
// Emite un dato al servidor MQTT en el topic indicado
void emitirDato(char* dato_topic, String payload, bool retain) {
  if (dato_topic == topic_respuesta) {
    payload = timeClient.getFormattedTime() + " - " + payload;
  }
  if (conectado_mqtt) {
    int contador = 0;
    conectar();
    while (!client.publish(dato_topic, payload.c_str(), retain) && contador < 10) {
      delay(100);
      contador++;
    }
  } else {
    Serial.println("***Imposible transmitir datos: MQTT desconectado");
  }
  emitir_contador = tiempoUNIX_UTC;
}
 
// Actualiza la hora
void actualizarHora() {
  timeClient.update();
  tiempoUNIX = timeClient.getEpochTime();
  tiempoUNIX_UTC = tiempoUNIX - eepromData.NTPOffset;
  struct tm ts;
  ts = *localtime(&tiempoUNIX);
  fechaHora.tm_year = ts.tm_year + 1900;
  fechaHora.tm_mon = ts.tm_mon + 1;
  fechaHora.tm_mday = ts.tm_mday;
  fechaHora.tm_hour = ts.tm_hour;
  fechaHora.tm_min = ts.tm_min;
  fechaHora.tm_sec = ts.tm_sec;
}
 
// Gestiona las órdenes recibidas desde el servidor MQTT
void gestionarOrdenes(String to, String st) {
  bool error = true;
  // El offset horario es transmitido por el servidor para todos los controladores
  if (to == "casa/ordenes/offset") {
    int nuevo_offset = st.toInt();
    if (nuevo_offset != eepromData.NTPOffset && (nuevo_offset == 3600 || nuevo_offset == 7200)) {
      emitirDato(topic_respuesta, "Cambiando valor de offset horario a " + st, true);
      eepromData.NTPOffset = st.toInt();
      timeClient.setTimeOffset(eepromData.NTPOffset);
      datosEeprom actualEeprom = eepromData;
      eepromData = leerEeprom();
      eepromData.NTPOffset = actualEeprom.NTPOffset;
      grabarEeprom(eepromData);
      eepromData = actualEeprom;
      error = false;
    }
  }
  // Gestionar órdenes específicas para este controlador
  if (to == String(topic_ordenes_especifico)) {
    String orden = st;
    String respuesta = "";
    String comando[3] = { "", "", "" };
    int parametros = 0;
    int t = 0;
    if (orden != "") {
      for (int i = 0; i < orden.length() - 1; i++) {
        if (orden[i] == ' ') {
          comando[parametros] = orden.substring(t, i);
          t = i + 1;
          parametros++;
          if (parametros > 2) {
            break;
          }
        }
      }
      comando[parametros] = orden.substring(t, orden.length());
 
      switch (parametros) {
        // Órdenes sin parámetro
        case 0:
          switch (comando[0][0]) {
            case 'E':
              grabarEeprom(eepromData);
              emitirDato(topic_respuesta, "Configuración grabada en EEPROM", true);
              error = false;
              break;
            case 'D':
              emitirDato(topic_respuesta, "Time offset: " + String(eepromData.NTPOffset) + 
                                          " - Intervalo de emisión: " + String(eepromData.emitir_timer) +
                                          "s - Tiempo de riego: " + String(eepromData.tiempo_riego) +
                                          "s - Intervalo entre riegos: " + String(eepromData.tiempo_intervalo_riego/3600) +
                                          "h - Umbral de humedad: " + String(eepromData.umbral_humedad), true);
              error = false;
              break;
            case 'M': {
              eepromData.modo = !eepromData.modo;
              grabarEeprom(eepromData);
              String m = "auto";
              if (!eepromData.modo) {
                m = "manual";       
              }
              emitirDato(topic_respuesta, "Modo de funcionamiento cambiado a "+m, true);
              error = false;
              break;
            }
            // Orden de lanzar riego. Funcionará en cualquiera de los modos independientemente de la humedad y de los temporizadores.
            case 'R':
              contador_intervalo_riego = tiempoUNIX + eepromData.tiempo_intervalo_riego;
              contador_riego = tiempoUNIX + eepromData.tiempo_riego;
              digitalWrite(circuito_rele, HIGH);
              Serial.println("Regando...");
              regando = true;
              error = false;
              break;
          }
          break;
        // Órdenes con un parámetro
        case 1:
          int p1 = comando[1].toInt();
          switch (comando[0][0]) {
            case 'T':
              if (p1 > 0 && p1 <= 20) {
                eepromData.tiempo_riego = p1;
                emitirDato(topic_respuesta, "Tiempo de riego cambiado a " + String(p1) + " segundos", true);
                grabarEeprom(eepromData);
                error = false;
                break;
              }
            case 'I':
              if (p1 > 0 && p1 <= 48) {
                eepromData.tiempo_intervalo_riego = p1 * 3600;
                emitirDato(topic_respuesta, "Intervalo entre riegos cambiado a " + String(p1) + " horas", true);
                grabarEeprom(eepromData);
                error = false;
                break;
              }
            case 'U':
              if (p1 > 0 && p1 <= 1024) {
                eepromData.umbral_humedad = p1;
                emitirDato(topic_respuesta, "Umbral de humedad cambiado a " + String(p1), true);
                grabarEeprom(eepromData);
                error = false;
                break;
              }                            
          }
          break;
      }
    }
  }
  if (error) {
    emitirDato(topic_respuesta, "Comando o parámetros incorrectos", true);
  }
}
 
// Función que gestiona la recepción de los topics suscritos
void callback(char* topic, byte* payload, unsigned int length) {
  String to = String(topic);
  String st = "";
  for (int i = 0; i < length; i++) {
    st = st + (char)payload[i];
  }
  gestionarOrdenes(to, st);
}
 
// Mide datos del sensor BME y del higrómetro. Si se cumplen los requisitos, inicia el proceso de riego
void medir() {
  temperatura = bme.readTemperature();
  humedad = bme.readHumidity();
  presion = bme.readPressure() / 100.0F;
  humedad_suelo = analogRead(A0);
  /*
  Condiciones para el inicio del riego:
  1. Que la humedad del suelo supere el umbral
  2. Que el contador de intervalo de riego haya vencido
  3. Que no esté ya regando
  4. Que el modo esté en auto (true)
  */  
  if (humedad_suelo > eepromData.umbral_humedad && contador_intervalo_riego < tiempoUNIX && !(regando) && eepromData.modo) {
    contador_intervalo_riego = tiempoUNIX + eepromData.tiempo_intervalo_riego;
    contador_riego = tiempoUNIX + eepromData.tiempo_riego;
    digitalWrite(circuito_rele, HIGH);
    Serial.println("Regando...");
    regando = true;
  }
}
 
// Gestiona los temporizadores
void actualizarContadores() {
  // Disparador del evento de conexión
  if (conectar_contador + eepromData.conectar_timer < tiempoUNIX_UTC) {
    conectar();
    conectar_contador = tiempoUNIX_UTC;
  }
  // Disparador del evento de emitir datos
  if (emitir_contador + eepromData.emitir_timer < tiempoUNIX_UTC) {
    emitirDato(topic_respuesta, "Ok", true);
    // Emitimos los datos del termómetro y el higrómetro
    emitirDato(topic_temperatura, String(temperatura), true);
    emitirDato(topic_humedad, String(humedad), true);
    emitirDato(topic_presion, String(presion), true);
    emitirDato(topic_suelo, String(humedad_suelo), true);
  }
  // Disparador del evento de medida
  if (contador_medida < tiempoUNIX) {
    contador_medida = tiempoUNIX + eepromData.tiempo_medida;
    medir();
  }
  // Disparador del evento de finalización de riego
  if (contador_riego < tiempoUNIX && regando) {
    contador_riego = 0;
    digitalWrite(circuito_rele, LOW);
    Serial.println("Riego finalizado");
    regando = false;
  }
}
 
// Inicialización general del sketch
void setup() {
  Serial.begin(9600);
  /*
  Hay que grabar la Eeprom por defecto al menos una vez al instalar este sketch,
  y luego comentar la orden para que no se reescriba la eeprom por defecto en
  cada reinicio. Por lo tanto, esta orden debe estar comentada en la subida final
  */
  // grabarEeprom(eepromData);
  eepromData = leerEeprom();
  WiFi.begin(ssid, passwd);
  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(callback);
  conectar();
  timeClient.begin();
  actualizarHora();
 
  // Datos específicos del controlador "palmerita"
  // Inicializamos el termómetro digital
  bme.begin(0x76);
  // Configuramos el pin del relé y lo ponemos en OFF
  pinMode(circuito_rele, OUTPUT);
  digitalWrite(circuito_rele, LOW);
}
 
// Bucle principal
void loop() {
  client.subscribe(topic_ordenes_general);
  client.loop();
  actualizarHora();
  actualizarContadores(); 
  delay(1000);
}
estacion_meteorologica_y_sistema_de_riego_autonomo_con_d1_mini.txt · Última modificación: 2023/03/22 12:57 por hispa

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki