Saltar al contenido
Nivel avanzado

Monitorear el riego con Internet de las Cosas

Introducción a Internet de las Cosas (IoT)

Internet de las Cosas (en inglés Internet of Things, abreviado IoT) es un concepto que refiere a la interconexión digital de objetos cotidianos con internet. Esta interconexión puede tener diversas funciones. Por ejemplo, monitorear la temperatura de un ambiente enviando los datos de un sensor a una central donde se recopile la información, para visualizarla desde un dispositivo móvil: la temperatura de un laboratorio, de un invernadero o de una sala de un hospital.

Para poder incorporar IoT a nuestro proyecto es necesario:

  1. Un dispositivo capaz de conectarse a internet.
  2. Un servidor que reciba y aloje los datos.

Existen diversas formas de registrar y almacenar los datos del sistema de riego. En este caso, se detallará cómo hacerlo con el WiFi integrado de la placa ESP32 y con los servidores de Adafruit IO.

El material original de 2019 resolvía este punto agregando al Arduino UNO un módulo externo llamado OBLOQ, un adaptador WiFi-serie de DFRobot hoy discontinuado (igual que su biblioteca ObloqAdafruit). Como el UNO clásico no tiene conectividad propia, en esta reedición la práctica de IoT se realiza con la variante ESP32 del proyecto: podés seguir este nivel de manera conceptual con tu UNO (el paso 3, sobre código no bloqueante, funciona perfectamente en él) y cambiar al ESP32 con el selector de plataforma para la parte práctica de conexión, que es idéntica en lo didáctico y más simple en el circuito.

El ESP32 cumple los dos roles del “dispositivo capaz de conectarse a internet”: es el microcontrolador que ya está midiendo la humedad y manejando la válvula y, a la vez, trae radio WiFi integrada. No hace falta ningún módulo adicional ni cableado extra: la conexión a internet se resuelve por completo desde el código.

Crear un panel de control en Adafruit IO

Primero se explicará cómo crear un panel de control en Adafruit IO, una plataforma online con posibilidad de uso gratuito, orientada al uso educativo, que centraliza la información que reportan los dispositivos. Empezamos creando una cuenta de usuario en io.adafruit.com.

Una vez que ingresamos, el trabajo tiene dos partes: crear los feeds donde se publicarán los datos y armar el panel que los muestre.

Un feed es una fuente de datos en la que uno puede publicar, así como también suscribirse para recibir sus datos. Desde la sección Feeds creamos dos, con estos nombres exactos (sin tildes, porque forman parte de la dirección con la que se publica):

  • humedad: recibirá la lectura del higrómetro. Recordá la convención del sensor: a mayor valor, tierra más seca.
  • valvula: recibirá el estado de la válvula, con la convención 1 = abierta (regando) y 0 = cerrada.

Luego, desde la sección Dashboards, creamos un nuevo panel con el nombre y la descripción que quieras. Dentro del panel agregamos dos bloques, uno por feed:

  • Un bloque Gauge (indicador de aguja) asociado a humedad, para ver la lectura del sensor. Como es una lectura inversa, tené presente al leerlo que la aguja alta significa tierra seca y la aguja baja, tierra húmeda.
  • Un bloque Indicator (luz virtual) asociado a valvula, con la condición “igual a 1”: encendido cuando la válvula está abierta (regando), apagado cuando está cerrada.

Cada bloque permite elegir un título y sus colores, y luego se puede ajustar su tamaño y ubicación dentro del panel. Al finalizar, el panel replicará a distancia el estado del sistema: qué tan seca está la tierra y si el riego está activo.

Por último, necesitamos las credenciales que le permitirán a nuestro dispositivo publicar en la cuenta: en Adafruit IO, tocá el ícono de la llave (API Key). Vas a ver dos datos, el nombre de usuario (Username) y la clave (Active Key): anotalos, porque los vamos a copiar dentro del código en el paso 4. La clave es secreta: cualquiera que la tenga puede publicar en tus feeds.

Programar sin código bloqueante

Antes de usar IoT debemos hacer una aclaración sobre la función delay() que venimos usando (nuestro programa espera un segundo entre medición y medición). Esta función brinda un tiempo de espera al sistema y suele usarse bastante en las primeras aproximaciones a la programación, porque su comportamiento es fácil de comprender y no requiere más que una línea de código.

Sin embargo, delay() genera un código bloqueante: cuando el programa entra en esa función, se detiene todo el procesamiento hasta que se cumple el tiempo indicado. En otras palabras, el programa queda “colgado” durante ese período.

Al utilizar IoT, el código bloqueante es un problema: mientras el procesamiento está detenido, el sistema no puede realizar otras operaciones que funcionan en simultáneo, como las tareas de publicación y el mantenimiento constante de la conexión a internet.

Para evitarlo, usamos una alternativa no bloqueante, la función millis(). Esta función devuelve un conteo de tiempo (en milisegundos) desde que se inició el sistema: funciona como un cronómetro que, cada vez que se lo consulta, informa en cuánto va. Con eso podemos preguntarle al sistema cuánto tiempo pasó desde la última medición, sin detener todo lo demás.

La receta tiene tres ingredientes, que vas a reconocer en el código: una variable millisAnterior, que guarda cuándo fue la última medición (arranca en cero); la consulta del valor actual con millis() en cada vuelta de loop(); y la comparación entre ambos contra el INTERVALO_MEDICION_MS. Si ya pasó el intervalo, el programa mide el higrómetro y decide sobre la válvula, y anota el nuevo valor en millisAnterior, para que la cuenta arranque de cero otra vez. Si todavía no pasó el tiempo, no espera: sigue de largo, con el procesador libre para otras tareas.

Así queda el programa del nivel intermedio quitando el código bloqueante. Sigue midiendo cada segundo, igual que el original, pero ahora sin colgarse:

Ver el código completo · riego-no-bloqueante.ino
riego-no-bloqueante.ino Arduino UNO
// Riego por humedad SIN delay(): la misma lógica del nivel intermedio
// (higrómetro que abre o cierra la electroválvula), pero no bloqueante.
// En vez de detener el programa un segundo entre mediciones, cada vuelta
// de loop() pregunta con millis() si ya pasó el intervalo; el resto del
// tiempo el procesador queda libre. Esta es la base que en ESP32 permite
// sumar IoT en riego-iot.ino sin que el riego se "cuelgue".
//
// CONVENCIÓN DEL HIGRÓMETRO: MAYOR lectura = tierra MÁS SECA (módulo
// resistivo). Por eso "seco" es el umbral alto (750) y "húmedo" el bajo
// (380), igual que en el nivel intermedio.
//
// HISTÉRESIS NATURAL DEL ORIGINAL (se preserva tal cual): dos ifs con dos
// umbrales; en la franja 380 a 750 la válvula conserva su estado, lo que
// evita el traqueteo cuando la lectura baila cerca de un borde.
//
// SEGURIDAD: agua y electricidad no se llevan bien. Conexiones eléctricas
// fuera y por encima del agua. Electroválvula de baja tensión (12 V) o
// simulación en el aula; 220 V solo docente/instalación supervisada, nunca
// sobre la protoboard (desvío pedagógico acordado).
//
// Plataforma: Arduino UNO.
// Conexión (la del esquema original): higrómetro → A0; relé de la
// electroválvula → pin 2.
//
// Basado en: Fragmentos de código 6 (patrón millis) y 7 (medición y
// control cada segundo, sin la parte IoT) — Sistema de Riego Automatizado,
// Saberes Digitales (INET/EDUCAR 2019)
// Modernización: constantes nombradas; millisAnterior como unsigned long
// (el long del original desborda la cuenta); una sola lectura del
// higrómetro por ciclo, reutilizada para imprimir y comparar.

const int PIN_HIGROMETRO   = A0;
const int PIN_RELE_VALVULA = 2;

// Módulo relé activo-alto, como en el original (HIGH = válvula abierta).
// Si tu módulo es activo-bajo, invertí solo estas dos constantes.
const int VALVULA_ABIERTA = HIGH;
const int VALVULA_CERRADA = LOW;

// Umbrales del original (escala del UNO: 0 a 1023). Ajustalos a TU suelo.
const int UMBRAL_SECO   = 750; // por encima: tierra seca → regar
const int UMBRAL_HUMEDO = 380; // por debajo: tierra húmeda → cortar

const long VELOCIDAD_SERIAL = 115200; // la del original

// El Fragmento de código 7 del original mide una vez por segundo.
const unsigned long INTERVALO_MEDICION_MS = 1000;

// Momento (ms desde el arranque) de la última medición.
unsigned long millisAnterior = 0;

void setup() {
  pinMode(PIN_HIGROMETRO, INPUT);
  pinMode(PIN_RELE_VALVULA, OUTPUT);
  // Estado inicial SEGURO: arrancamos con la válvula cerrada. Sin esta línea
  // el pin queda en LOW por defecto; si invertís las constantes para un módulo
  // activo-bajo, ese LOW pasaría a ser "abierta" y, si al encender la lectura
  // cae en la banda muerta (entre 380 y 750), la válvula regaría sin control.
  // Regla de oro: dejá SIEMPRE el actuador en estado seguro antes del loop.
  digitalWrite(PIN_RELE_VALVULA, VALVULA_CERRADA);
  Serial.begin(VELOCIDAD_SERIAL);
}

void loop() {
  unsigned long millisActual = millis();

  // ¿Ya pasó el intervalo desde la última medición? Si no, no hacemos
  // nada y loop() vuelve a empezar: eso es NO bloquear.
  if (millisActual - millisAnterior >= INTERVALO_MEDICION_MS) {
    millisAnterior = millisActual;

    int humedad = analogRead(PIN_HIGROMETRO);
    Serial.println(humedad);

    // Histéresis del original: dos ifs independientes, dos umbrales.
    if (humedad > UMBRAL_SECO) {
      // Tierra demasiado seca: abrir la válvula para regar.
      digitalWrite(PIN_RELE_VALVULA, VALVULA_ABIERTA);
    }
    if (humedad < UMBRAL_HUMEDO) {
      // Tierra suficientemente húmeda: cerrar la válvula.
      digitalWrite(PIN_RELE_VALVULA, VALVULA_CERRADA);
    }
    // Entre 380 y 750 la válvula queda como estaba.
  }

  // En este punto el procesador queda libre para realizar otras tareas.
}
riego-no-bloqueante.ino ESP32
// Riego por humedad SIN delay(): la misma lógica del nivel intermedio
// (higrómetro que abre o cierra la electroválvula), pero no bloqueante.
// En vez de detener el programa un segundo entre mediciones, cada vuelta
// de loop() pregunta con millis() si ya pasó el intervalo; el resto del
// tiempo el procesador queda libre. Este programa es la base directa de
// riego-iot.ino: sin código bloqueante, el ESP32 puede mantener el WiFi y
// publicar por MQTT sin desatender el riego.
//
// CONVENCIÓN DEL HIGRÓMETRO: MAYOR lectura = tierra MÁS SECA (módulo
// resistivo). Por eso "seco" es el umbral alto y "húmedo" el bajo.
//
// HISTÉRESIS NATURAL DEL ORIGINAL (se preserva tal cual): dos ifs con dos
// umbrales; en la franja intermedia la válvula conserva su estado, lo que
// evita el traqueteo cuando la lectura baila cerca de un borde.
//
// DIFERENCIA DE ADC (didáctica): el ESP32 mide 0 a 4095 (12 bits, 3,3 V),
// no 0 a 1023 (10 bits, 5 V) del UNO; los umbrales del original se
// reescalan en la misma proporción (750→3000, 380→1520).
//
// SEGURIDAD: agua y electricidad no se llevan bien. Conexiones eléctricas
// fuera y por encima del agua. Electroválvula de baja tensión (12 V) o
// simulación en el aula; 220 V solo docente/instalación supervisada, nunca
// sobre la protoboard (desvío pedagógico acordado).
//
// Plataforma: ESP32 (DevKit v1).
// Conexión: higrómetro → GPIO 34 (ADC1, solo entrada); relé de la
// electroválvula → GPIO 16. Nota eléctrica: el ESP32 trabaja a 3,3 V;
// alimentá el higrómetro con 3,3 V y nunca inyectes 5 V en un GPIO.
//
// Basado en: Fragmentos de código 6 (patrón millis) y 7 (medición y
// control cada segundo, sin la parte IoT) — Sistema de Riego Automatizado,
// Saberes Digitales (INET/EDUCAR 2019)
// Modernización: constantes nombradas; millisAnterior como unsigned long
// (el long del original desborda la cuenta); portado a ESP32 con umbrales
// reescalados; una sola lectura del higrómetro por ciclo, reutilizada para
// imprimir y comparar.

const int PIN_HIGROMETRO   = 34;
const int PIN_RELE_VALVULA = 16;

// Módulo relé activo-alto, como en el original (HIGH = válvula abierta).
// Si tu módulo es activo-bajo, invertí solo estas dos constantes.
const int VALVULA_ABIERTA = HIGH;
const int VALVULA_CERRADA = LOW;

// Umbrales del original reescalados al ADC de 12 bits del ESP32.
const int UMBRAL_SECO   = 3000; // por encima: tierra seca → regar   (750 en UNO)
const int UMBRAL_HUMEDO = 1520; // por debajo: tierra húmeda → cortar (380 en UNO)

const long VELOCIDAD_SERIAL = 115200; // la del original

// El Fragmento de código 7 del original mide una vez por segundo.
const unsigned long INTERVALO_MEDICION_MS = 1000;

// Momento (ms desde el arranque) de la última medición.
unsigned long millisAnterior = 0;

void setup() {
  pinMode(PIN_HIGROMETRO, INPUT);
  pinMode(PIN_RELE_VALVULA, OUTPUT);
  // Estado inicial SEGURO: arrancamos con la válvula cerrada. Sin esta línea
  // el pin queda en LOW por defecto; si invertís las constantes para un módulo
  // activo-bajo, ese LOW pasaría a ser "abierta" y, si al encender la lectura
  // cae en la banda muerta (entre 1520 y 3000), la válvula regaría sin control.
  // Regla de oro: dejá SIEMPRE el actuador en estado seguro antes del loop.
  digitalWrite(PIN_RELE_VALVULA, VALVULA_CERRADA);
  Serial.begin(VELOCIDAD_SERIAL);
}

void loop() {
  unsigned long millisActual = millis();

  // ¿Ya pasó el intervalo desde la última medición? Si no, no hacemos
  // nada y loop() vuelve a empezar: eso es NO bloquear.
  if (millisActual - millisAnterior >= INTERVALO_MEDICION_MS) {
    millisAnterior = millisActual;

    int humedad = analogRead(PIN_HIGROMETRO);
    Serial.println(humedad);

    // Histéresis del original: dos ifs independientes, dos umbrales.
    if (humedad > UMBRAL_SECO) {
      // Tierra demasiado seca: abrir la válvula para regar.
      digitalWrite(PIN_RELE_VALVULA, VALVULA_ABIERTA);
    }
    if (humedad < UMBRAL_HUMEDO) {
      // Tierra suficientemente húmeda: cerrar la válvula.
      digitalWrite(PIN_RELE_VALVULA, VALVULA_CERRADA);
    }
    // Entre el umbral húmedo y el seco la válvula queda como estaba.
  }

  // En este punto el procesador queda libre para realizar otras tareas
  // (en riego-iot.ino, acá se atiende la conexión WiFi/MQTT).
}
El mismo control de la válvula por humedad del nivel intermedio, ahora sin delay(): el programa consulta millis() y solo mide cuando pasó el intervalo.

Teniendo un programa de riego con código no bloqueante (es decir, sin delay()) estamos en condiciones de incorporar IoT.

Programar IoT

Con el panel listo y el código ya libre de bloqueos, falta el último eslabón: que el sistema publique la humedad y el estado de la válvula en Adafruit IO.

Esta parte práctica corresponde a la variante ESP32: el Arduino UNO clásico no tiene WiFi (el original lo resolvía con el módulo OBLOQ, hoy discontinuado). Cambiá la plataforma a ESP32 con el selector para ver el código y las instrucciones de conexión. Todo lo conceptual —qué se publica en cada feed, la convención de valores, el porqué del código no bloqueante— ya lo tenés resuelto con tu UNO en los pasos anteriores.

El programa final integra tres cosas que ya conocés: el control no bloqueante del paso 3, la conexión WiFi nativa del ESP32 y la publicación en Adafruit IO (mediante el protocolo MQTT, usando la Adafruit MQTT Library, que se instala desde el gestor de bibliotecas del Arduino IDE; reemplaza a la biblioteca ObloqAdafruit del original, discontinuada junto con su módulo).

Antes de subirlo, tenés que completar cuatro datos al comienzo del código: el nombre (SSID) y la contraseña de tu red WiFi, y el usuario y la clave que copiaste de Adafruit IO en el paso 2.

La publicación se engancha en el tiempo que el paso 3 le dejó libre al procesador: en cada vuelta de loop(), además de medir y decidir cuando toca, el programa mantiene viva la conexión con el servidor y publica los cambios: el estado (1 o 0) en valvula cada vez que la válvula conmuta, y la lectura en humedad cuando difiere de la última publicada. El original publicaba la humedad y el estado en cada medición, cada segundo; publicar solo los cambios muestra lo mismo en el panel sin agotar el cupo de la cuenta gratuita de Adafruit IO (30 publicaciones por minuto; el desvío está documentado en la ficha). Por esto es importante que nuestro código no sea bloqueante: si el programa quedara colgado en un delay(), la conexión no podría atenderse y las publicaciones se perderían. Bonus del mismo diseño: si internet se cae, el riego sigue funcionando, y al volver la conexión el panel se pone al día solo.

Ver el código completo · riego-iot.ino

Nota: Este código está disponible para ESP32. Cambiá la plataforma con el selector del encabezado para verlo y practicarlo.

riego-iot.ino ESP32
// Riego por humedad con monitoreo IoT vía Adafruit IO (MQTT).
// La lógica no bloqueante de riego-no-bloqueante.ino sigue controlando la
// electroválvula según el higrómetro; además se publican dos feeds para
// verlos desde el Panel de Control de Adafruit (como en la guía):
//
//   "humedad": lectura cruda del higrómetro (número más alto = más seco).
//   "valvula": 0 = cerrada, 1 = abierta.
//
// CONVENCIÓN DEL HIGRÓMETRO: MAYOR lectura = tierra MÁS SECA (módulo
// resistivo). Por eso "seco" es el umbral alto y "húmedo" el bajo.
//
// HISTÉRESIS NATURAL DEL ORIGINAL (se preserva tal cual): dos ifs con dos
// umbrales; en la franja intermedia la válvula conserva su estado.
//
// DIFERENCIA DE ADC (didáctica): el ESP32 mide 0 a 4095 (12 bits, 3,3 V),
// no 0 a 1023 del UNO; los umbrales se reescalan (750→3000, 380→1520).
//
// SEGURIDAD: agua y electricidad no se llevan bien. Conexiones eléctricas
// fuera y por encima del agua. Electroválvula de baja tensión (12 V) o
// simulación en el aula; 220 V solo docente/instalación supervisada, nunca
// sobre la protoboard (desvío pedagógico acordado).
//
// Plataforma: ESP32 (DevKit v1) — usa su WiFi integrado.
// Conexión: higrómetro → GPIO 34 (ADC1, solo entrada); relé de la
// electroválvula → GPIO 16. Nota eléctrica: el ESP32 trabaja a 3,3 V;
// alimentá el higrómetro con 3,3 V y nunca inyectes 5 V en un GPIO.
//
// Librería requerida (Library Manager del IDE de Arduino):
//   "Adafruit MQTT Library" (de Adafruit) — la misma elección que en los
//   demás proyectos: cada feed es un objeto y publish() acepta números
//   directamente, igual que el olq.publish() del original.
//
// Basado en: Fragmento de código 7 — Sistema de Riego Automatizado,
// Saberes Digitales (INET/EDUCAR 2019)
// Modernización: (1) el módulo OBLOQ y la librería ObloqAdafruit quedaron
// discontinuados; se reemplazan por el WiFi nativo del ESP32 (WiFi.h) y
// MQTT contra io.adafruit.com. (2) El original publicaba "humedad" en CADA
// medición (cada 1 s = 60 publicaciones por minuto, el doble del límite
// gratuito de Adafruit IO de 30/min); acá se publica solo cuando algo
// cambia: la válvula al conmutar y la humedad cuando difiere 50 o más del
// último valor publicado. (3) Umbrales reescalados al ADC de 12 bits.

#include <WiFi.h>
#include "Adafruit_MQTT.h"
#include "Adafruit_MQTT_Client.h"

// ---------------------------------------------------------------------------
// Credenciales: reemplazar los cuatro valores por los propios.
// El usuario y la key de Adafruit se copian desde io.adafruit.com
// haciendo clic en el ícono de la "llave" (igual que en la guía original).
// ---------------------------------------------------------------------------
#define WIFI_SSID     "TU_SSID"
#define WIFI_PASSWORD "TU_PASSWORD"
#define IO_USERNAME   "TU_USUARIO_ADAFRUIT"
#define IO_KEY        "TU_KEY_ADAFRUIT"

// Servidor MQTT de Adafruit IO.
const char SERVIDOR_MQTT[] = "io.adafruit.com";
const int  PUERTO_MQTT     = 1883;

const int PIN_HIGROMETRO   = 34;
const int PIN_RELE_VALVULA = 16;

// Módulo relé activo-alto, como en el original (HIGH = válvula abierta).
// Si tu módulo es activo-bajo, invertí solo estas dos constantes.
const int VALVULA_ABIERTA = HIGH;
const int VALVULA_CERRADA = LOW;

// Umbrales del original reescalados al ADC de 12 bits del ESP32.
const int UMBRAL_SECO   = 3000; // por encima: tierra seca → regar   (750 en UNO)
const int UMBRAL_HUMEDO = 1520; // por debajo: tierra húmeda → cortar (380 en UNO)

// La humedad se re-publica solo si difiere en 50 o más (sobre 0-4095) del
// último valor publicado: los saltitos de ruido entre medición y medición
// no le aportan nada a la central y consumen el cupo gratuito de Adafruit.
const int UMBRAL_PUBLICACION = 50;

const long VELOCIDAD_SERIAL = 115200;

// El original mide una vez por segundo.
const unsigned long INTERVALO_MEDICION_MS = 1000;

// Si se cae la conexión MQTT, reintentamos cada tanto SIN bloquear:
// el riego debe seguir funcionando aunque no haya internet.
const unsigned long INTERVALO_REINTENTO_MQTT_MS = 5000;

// Cliente MQTT sobre el WiFi del ESP32, y un objeto por feed.
WiFiClient clienteWifi;
Adafruit_MQTT_Client mqtt(&clienteWifi, SERVIDOR_MQTT, PUERTO_MQTT,
                          IO_USERNAME, IO_KEY);
Adafruit_MQTT_Publish feedHumedad(&mqtt, IO_USERNAME "/feeds/humedad");
Adafruit_MQTT_Publish feedValvula(&mqtt, IO_USERNAME "/feeds/valvula");

// Momento (ms desde el arranque) de la última medición y del último
// intento de conexión MQTT.
unsigned long millisAnterior = 0;
unsigned long millisUltimoIntentoMqtt = 0;

// Todavía no se midió: no hay que publicar humedad inventada si la
// conexión MQTT llega antes que la primera medición.
bool hayMedicion = false;

// Estado actual (lo que muestran la válvula y el higrómetro)...
int estadoValvula = 0; // 0 = cerrada, 1 = abierta
int humedadActual = 0; // última lectura cruda del higrómetro

// ...y lo último que llegó a publicarse. Separarlos permite publicar solo
// los CAMBIOS y, si la conexión estaba caída, ponerse al día al volver.
int estadoValvulaPublicado = -1;    // -1 = todavía no se publicó nada
int humedadPublicada        = -1000; // imposible: fuerza la 1ª publicación

// Diferencia sin signo entre dos enteros (para comparar contra el umbral).
int diferenciaAbsoluta(int a, int b) {
  if (a > b) {
    return a - b;
  }
  return b - a;
}

// Mantiene viva la conexión MQTT sin bloquear el riego: si está caída,
// reintenta como mucho una vez cada 5 segundos.
void mantenerConexionMqtt() {
  if (mqtt.connected()) {
    return;
  }

  unsigned long millisActual = millis();
  if (millisActual - millisUltimoIntentoMqtt < INTERVALO_REINTENTO_MQTT_MS) {
    return; // todavía no toca reintentar
  }
  millisUltimoIntentoMqtt = millisActual;

  Serial.print("Conectando a Adafruit IO... ");
  int8_t resultado = mqtt.connect(); // 0 = conexión exitosa
  if (resultado == 0) {
    Serial.println("conectado.");
  } else {
    Serial.println(mqtt.connectErrorString(resultado));
    mqtt.disconnect(); // deja el socket limpio para el próximo intento
  }
}

// Publica cada feed SOLO si cambió desde la última publicación exitosa
// (la humedad, solo si cambió 50 o más). Si publish() falla o no hay
// conexión, no se actualiza el registro: se reintenta en la próxima vuelta.
void publicarCambios() {
  if (!mqtt.connected() || !hayMedicion) {
    return; // sin internet o sin datos reales todavía: no se publica
  }

  if (estadoValvula != estadoValvulaPublicado
      && feedValvula.publish((int32_t)estadoValvula)) {
    estadoValvulaPublicado = estadoValvula;
  }

  if (diferenciaAbsoluta(humedadActual, humedadPublicada) >= UMBRAL_PUBLICACION
      && feedHumedad.publish((int32_t)humedadActual)) {
    humedadPublicada = humedadActual;
  }
}

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

  pinMode(PIN_HIGROMETRO, INPUT);
  pinMode(PIN_RELE_VALVULA, OUTPUT);

  // Estado inicial seguro: válvula cerrada hasta que la primera medición
  // diga otra cosa.
  digitalWrite(PIN_RELE_VALVULA, VALVULA_CERRADA);

  // Conexión WiFi. Esperar acá con delay() es aceptable porque el riego
  // todavía no arrancó; una vez dentro de loop(), nada bloquea.
  Serial.print("Conectando a la red WiFi");
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println();
  Serial.print("WiFi conectado, IP: ");
  Serial.println(WiFi.localIP());
}

void loop() {
  unsigned long millisActual = millis();

  // --- Medición y control, una vez por segundo (como el original) ---
  if (millisActual - millisAnterior >= INTERVALO_MEDICION_MS) {
    millisAnterior = millisActual;

    humedadActual = analogRead(PIN_HIGROMETRO);
    hayMedicion = true; // desde ahora, publicarCambios() tiene datos reales
    Serial.println(humedadActual);

    // Histéresis del original: dos ifs independientes, dos umbrales.
    if (humedadActual > UMBRAL_SECO) {
      // Tierra demasiado seca: abrir la válvula para regar.
      digitalWrite(PIN_RELE_VALVULA, VALVULA_ABIERTA);
      estadoValvula = 1;
    }
    if (humedadActual < UMBRAL_HUMEDO) {
      // Tierra suficientemente húmeda: cerrar la válvula.
      digitalWrite(PIN_RELE_VALVULA, VALVULA_CERRADA);
      estadoValvula = 0;
    }
    // Entre el umbral húmedo y el seco la válvula queda como estaba.
  }

  // --- Parte IoT ---
  // Gracias a que el control no bloquea, estas dos tareas se atienden en
  // CADA vuelta de loop(): esta es la razón de fondo por la que la guía
  // enseña a abandonar delay() antes de llegar a IoT.
  mantenerConexionMqtt();
  publicarCambios();
}
El sistema de riego IoT completo: control no bloqueante + WiFi del ESP32 + publicación de la humedad y el estado de la válvula en Adafruit IO.

Así queda funcionando el tablero que permite monitorear el riego a distancia. Podés ver el sistema completo —el higrómetro midiendo, la válvula regando y el panel reflejando cada publicación— en el simulador:

Simulación del comportamiento

La misma máquina de estados del firmware, portada a TypeScript, maneja el comportamiento del dispositivo. No se ejecuta el binario real: la ejecución del .hex con avr8js está en el roadmap.

Simulación lista. Presioná «Iniciar» para arrancar la secuencia.

Cierre del nivel

Al terminar este nivel, tu sistema de riego dejó de ser solo un dispositivo local: es un dispositivo IoT. El programa funciona con millis(), sin ningún delay() que lo cuelgue; sabés explicar por qué eso es imprescindible cuando hay que atender internet en simultáneo; y el panel de Adafruit IO muestra desde cualquier dispositivo qué tan seca está la tierra y si el sistema está regando. Con esto queda completo el recorrido del proyecto. Si el grupo quiere seguir, en la ficha del proyecto está la extensión sugerida: complementar con el proyecto “Invernadero inteligente”, sumando sensores de humedad de ambiente y de temperatura.

Evidencia de cierre

Al terminar este nivel, esto tiene que estar funcionando:

  • El sistema de riego funciona con código no bloqueante basado en millis(): no queda ningún delay() que detenga el programa entre mediciones.
  • Podés explicar con tus palabras por qué el código bloqueante es un problema cuando el dispositivo tiene que atender internet en simultáneo.
  • El panel de Adafruit IO muestra la lectura de humedad del suelo y el estado de la válvula publicados por el dispositivo (variante ESP32).