Saltar al contenido
Nivel avanzado

Monitorear el invernadero 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. 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 invernadero. 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 a la vez: es el microcontrolador que ya está midiendo la temperatura y la humedad y manejando el aviso, y, además, 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):

  • temperatura: recibirá el valor de temperatura en grados Celsius.
  • humedad: recibirá el valor de humedad relativa del aire, en porcentaje.

Luego, desde la sección Dashboards, creamos un nuevo panel con el nombre y la descripción que quieras. Dentro del panel agregamos, tal como propone el original, dos bloques del tipo Line Chart (gráfico de línea), uno por feed: nos permiten ver el historial de los cambios en la temperatura y en la humedad a lo largo del tiempo. Cada bloque permite elegir un título y sus colores, y luego se puede ajustar su tamaño y ubicación. Al finalizar, el panel replicará a distancia la evolución de las condiciones del invernadero.

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 unos segundos 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 y actualiza, 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.

Este programa introduce, además, un cambio en el aviso: el LED deja de parpadear y pasa a quedar encendido fijo mientras la temperatura supera el umbral (y apagado cuando está por debajo), resuelto con un if/else. Es exactamente como evoluciona el original al llegar a este nivel: con pantalla y monitoreo, un estado estable se lee mejor que un titileo.

Ver el código completo · invernadero-no-bloqueante.ino
invernadero-no-bloqueante.ino Arduino UNO
// Invernadero SIN delay(): el DHT11 mide temperatura y humedad, el LCD 16x2
// las muestra y el LED de ventilación queda ENCENDIDO FIJO mientras la
// temperatura supera el umbral (apagado si está por debajo). Todo con
// millis() en lugar de delay(): cada vuelta de loop() pregunta si ya pasó
// el intervalo y, si no, deja el procesador libre.
//
// Cambio de comportamiento del LED respecto a los niveles anteriores: acá
// el aviso pasa de parpadeo a encendido fijo, tal como evoluciona el
// original al llegar al nivel avanzado (con pantalla y monitoreo, un estado
// estable se lee mejor que un titileo). Esta es además la base directa de
// invernadero-iot.ino: sin código bloqueante, el ESP32 puede sumar WiFi y
// MQTT sin desatender el sensor.
//
// Plataforma: Arduino UNO.
// Conexión (la del esquema original): dato del DHT11 → pin 2; LED → pin 13;
// display LCD Keypad Shield en modo paralelo por los pines 8, 9, 4, 5, 6, 7.
//
// Nota del original: en el LCD no usar tildes ni la letra Ñ.
//
// Basado en: Fragmentos de código del Nivel Superior (Paso 5, patrón millis,
// con LED encendido/apagado por if/else y sin la parte IoT) — Invernadero
// Inteligente, Saberes Digitales (INET/EDUCAR 2019)
// Modernización: constantes nombradas; millisAnterior como unsigned long
// (el long del original desborda la cuenta); librería DHT de Adafruit con
// dht.begin(); una sola lectura por ciclo reutilizada para LED y display.

#include "DHT.h"
#include <LiquidCrystal.h>

// Pin de datos del sensor y tipo de sensor (DHT11).
const int PIN_DHT = 2;
#define DHTTYPE DHT11

// LED indicador de "hay que ventilar".
const int PIN_LED_VENTILACION = 13;

// Umbral del original: por encima de 25 °C hay que ventilar.
const float UMBRAL_TEMP_C = 25.0;

// El DHT11 no admite más de una lectura por segundo; medimos cada 2 s.
const unsigned long INTERVALO_MEDICION_MS = 2000;

// Objeto del sensor y display paralelo (RS, E, D4, D5, D6, D7).
DHT dht(PIN_DHT, DHTTYPE);
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

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

void setup() {
  pinMode(PIN_LED_VENTILACION, OUTPUT);
  dht.begin();
  lcd.begin(16, 2);
  lcd.clear();
}

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;

    float temperatura = dht.readTemperature(); // grados Celsius
    float humedad     = dht.readHumidity();    // humedad relativa en %

    // Aviso de ventilación como encendido fijo (if/else): encendido = hay
    // que ventilar, apagado = temperatura en orden.
    if (temperatura > UMBRAL_TEMP_C) {
      digitalWrite(PIN_LED_VENTILACION, HIGH);
    } else {
      digitalWrite(PIN_LED_VENTILACION, LOW);
    }

    // Fila 0: temperatura. Fila 1: humedad. Sin tildes ni Ñ (limitación LCD).
    lcd.setCursor(0, 0);
    lcd.print("Temp.: ");
    lcd.print(temperatura);
    lcd.setCursor(0, 1);
    lcd.print("Hum.: ");
    lcd.print(humedad);
  }

  // En este punto el procesador queda libre para realizar otras tareas.
}
invernadero-no-bloqueante.ino ESP32
// Invernadero SIN delay(): el DHT11 mide temperatura y humedad, el LCD 16x2
// (I2C) las muestra y el LED de ventilación queda ENCENDIDO FIJO mientras la
// temperatura supera el umbral (apagado si está por debajo). Todo con
// millis(): cada vuelta de loop() pregunta si ya pasó el intervalo y, si no,
// deja el procesador libre.
//
// El aviso pasa de parpadeo (niveles anteriores) a encendido fijo, como
// evoluciona el original. Este programa es la base directa de
// invernadero-iot.ino: sin código bloqueante, el ESP32 puede sumar WiFi y
// MQTT sin desatender el sensor.
//
// Plataforma: ESP32 (DevKit v1).
// Conexión: dato del DHT11 → GPIO 4; LED externo → GPIO 16; display LCD 1602
// por I2C: SDA → GPIO 21, SCL → GPIO 22.
//
// ADAPTACIÓN DE PLATAFORMA (ver README): el original usa un LCD Keypad
// Shield paralelo (6 pines) que no entra en un ESP32; acá usamos un LCD 1602
// con módulo I2C (2 cables, librería LiquidCrystal_I2C, dirección típica
// 0x27; probá 0x3F si no muestra nada).
// Nota eléctrica: el ESP32 trabaja a 3,3 V.
//
// Nota del original: en el LCD no usar tildes ni la letra Ñ.
//
// Librerías requeridas (Library Manager): "DHT sensor library" (Adafruit) +
// "Adafruit Unified Sensor", y "LiquidCrystal_I2C".
//
// Basado en: Fragmentos de código del Nivel Superior (Paso 5, patrón millis,
// LED por if/else, sin la parte IoT) — Invernadero Inteligente, Saberes
// Digitales (INET/EDUCAR 2019)
// Modernización: constantes nombradas; millisAnterior como unsigned long;
// DHT de Adafruit con dht.begin(); LCD paralelo del original adaptado a I2C;
// una sola lectura por ciclo reutilizada para LED y display.

#include "DHT.h"
#include <Wire.h>
#include <LiquidCrystal_I2C.h>

// Pin de datos del sensor y tipo de sensor (DHT11).
const int PIN_DHT = 4;
#define DHTTYPE DHT11

// LED externo indicador de "hay que ventilar".
const int PIN_LED_VENTILACION = 16;

// Dirección I2C y tamaño del display (16 columnas x 2 filas).
const uint8_t LCD_DIRECCION_I2C = 0x27;
const uint8_t LCD_COLUMNAS      = 16;
const uint8_t LCD_FILAS         = 2;

// Umbral del original: por encima de 25 °C hay que ventilar.
const float UMBRAL_TEMP_C = 25.0;

// El DHT11 no admite más de una lectura por segundo; medimos cada 2 s.
const unsigned long INTERVALO_MEDICION_MS = 2000;

// Objeto del sensor y display por I2C.
DHT dht(PIN_DHT, DHTTYPE);
LiquidCrystal_I2C lcd(LCD_DIRECCION_I2C, LCD_COLUMNAS, LCD_FILAS);

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

void setup() {
  pinMode(PIN_LED_VENTILACION, OUTPUT);
  dht.begin();
  lcd.init();
  lcd.backlight();
  lcd.clear();
}

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;

    float temperatura = dht.readTemperature(); // grados Celsius
    float humedad     = dht.readHumidity();    // humedad relativa en %

    // Aviso de ventilación como encendido fijo (if/else).
    if (temperatura > UMBRAL_TEMP_C) {
      digitalWrite(PIN_LED_VENTILACION, HIGH);
    } else {
      digitalWrite(PIN_LED_VENTILACION, LOW);
    }

    // Fila 0: temperatura. Fila 1: humedad. Sin tildes ni Ñ (limitación LCD).
    lcd.setCursor(0, 0);
    lcd.print("Temp.: ");
    lcd.print(temperatura);
    lcd.setCursor(0, 1);
    lcd.print("Hum.: ");
    lcd.print(humedad);
  }

  // En este punto el procesador queda libre para realizar otras tareas
  // (en invernadero-iot.ino, acá se atiende la conexión WiFi/MQTT).
}
El mismo invernadero del nivel intermedio, ahora sin delay(): el programa consulta millis() y solo mide cuando pasó el intervalo. El aviso pasa a LED encendido fijo (encendido = ventilar, apagado = todo en orden).

Teniendo un programa 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 temperatura y la humedad 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, 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 actualizar el display cuando toca, el programa mantiene viva la conexión con el servidor y publica los cambios. Para no gastar el cupo de la cuenta gratuita de Adafruit IO (30 publicaciones por minuto), un valor se re-publica solo si cambió lo suficiente respecto del último enviado: al menos 0,5 °C la temperatura y al menos 1 % la humedad. El original publicaba en cada medición; como el DHT11 tiene resolución de 1 °C y 1 %, en la práctica se publica ante cualquier cambio real de la medición (el desvío está documentado en la ficha). Por esto es importante que el 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 aviso y el display siguen funcionando, y al volver la conexión el panel se pone al día solo.

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

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

invernadero-iot.ino ESP32
// Invernadero con monitoreo IoT vía Adafruit IO (MQTT).
// La lógica no bloqueante de invernadero-no-bloqueante.ino sigue midiendo
// con el DHT11, mostrando en el LCD (I2C) y encendiendo el LED de
// ventilación; además publica dos feeds para ver las condiciones del
// invernadero desde el Panel de Control de Adafruit (como en la guía):
//
//   "temperatura": grados Celsius.
//   "humedad": humedad relativa en %.
//
// Plataforma: ESP32 (DevKit v1) — usa su WiFi integrado.
// Conexión: dato del DHT11 → GPIO 4; LED externo → GPIO 16; display LCD 1602
// por I2C: SDA → GPIO 21, SCL → GPIO 22.
//
// ADAPTACIÓN DE PLATAFORMA (ver README): el original usa un LCD Keypad
// Shield paralelo (6 pines) que no entra en un ESP32; acá usamos un LCD 1602
// con módulo I2C (2 cables, librería LiquidCrystal_I2C, dirección típica
// 0x27; probá 0x3F si no muestra nada).
// Nota eléctrica: el ESP32 trabaja a 3,3 V.
//
// Nota del original: en el LCD no usar tildes ni la letra Ñ.
//
// Librerías requeridas (Library Manager): "DHT sensor library" (Adafruit) +
// "Adafruit Unified Sensor", "LiquidCrystal_I2C" y "Adafruit MQTT Library".
//
// Basado en: Fragmento de código del Nivel Superior (Paso 6) — Invernadero
// Inteligente, 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 temperatura y humedad en
// CADA medición; acá se publican solo cuando cambian de forma apreciable
// (temperatura ≥ 0,5 °C; humedad ≥ 1 %) respecto de lo último publicado, para
// no gastar el cupo gratuito de Adafruit IO (30 publicaciones por minuto).
// Como el DHT11 tiene resolución de 1 °C y 1 %, en la práctica se publica
// ante cualquier cambio real de la medición. (3) Se ignora una lectura
// fallida del DHT11 (isnan): si el sensor devuelve "no numérico" (pasa cada
// tanto), se saltea el ciclo en vez de mostrar/publicar basura. (4) El LCD
// paralelo del original se adapta a I2C.

#include <WiFi.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include "DHT.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;

// Pin de datos del sensor y tipo de sensor (DHT11).
const int PIN_DHT = 4;
#define DHTTYPE DHT11

// LED externo indicador de "hay que ventilar".
const int PIN_LED_VENTILACION = 16;

// Dirección I2C y tamaño del display (16 columnas x 2 filas).
const uint8_t LCD_DIRECCION_I2C = 0x27;
const uint8_t LCD_COLUMNAS      = 16;
const uint8_t LCD_FILAS         = 2;

// Umbral del original: por encima de 25 °C hay que ventilar.
const float UMBRAL_TEMP_C = 25.0;

// El DHT11 no admite más de una lectura por segundo; medimos cada 2 s.
const unsigned long INTERVALO_MEDICION_MS = 2000;

// Un valor se re-publica solo si cambió al menos esto respecto del último
// publicado: evita gastar el cupo gratuito de Adafruit IO con repeticiones.
const float UMBRAL_PUBLICACION_TEMP_C = 0.5;
const float UMBRAL_PUBLICACION_HUM    = 1.0;

// Si se cae la conexión MQTT, reintentamos cada tanto SIN bloquear.
const unsigned long INTERVALO_REINTENTO_MQTT_MS = 5000;

// Objetos de sensor, display, WiFi y MQTT.
DHT dht(PIN_DHT, DHTTYPE);
LiquidCrystal_I2C lcd(LCD_DIRECCION_I2C, LCD_COLUMNAS, LCD_FILAS);
WiFiClient clienteWifi;
Adafruit_MQTT_Client mqtt(&clienteWifi, SERVIDOR_MQTT, PUERTO_MQTT,
                          IO_USERNAME, IO_KEY);
Adafruit_MQTT_Publish feedTemperatura(&mqtt, IO_USERNAME "/feeds/temperatura");
Adafruit_MQTT_Publish feedHumedad(&mqtt, IO_USERNAME "/feeds/humedad");

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

// Últimas mediciones válidas, y lo último que llegó a publicarse. Separarlos
// permite publicar solo los cambios y ponerse al día tras una desconexión.
// Se arranca con valores imposibles para forzar la primera publicación.
float temperaturaPublicada = -1000.0;
float humedadPublicada     = -1000.0;
bool  hayMedicion          = false;

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

// Mantiene viva la conexión MQTT sin bloquear el sensor: 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ó lo suficiente desde la última publicación
// exitosa. Si publish() falla o no hay conexión, no se actualiza el registro:
// se reintenta en la próxima vuelta.
void publicarCambios(float temperatura, float humedad) {
  if (!mqtt.connected() || !hayMedicion) {
    return; // sin internet o sin datos reales todavía: no se publica
  }

  if (diferenciaAbsoluta(temperatura, temperaturaPublicada)
        >= UMBRAL_PUBLICACION_TEMP_C
      && feedTemperatura.publish(temperatura)) {
    temperaturaPublicada = temperatura;
  }

  if (diferenciaAbsoluta(humedad, humedadPublicada) >= UMBRAL_PUBLICACION_HUM
      && feedHumedad.publish(humedad)) {
    humedadPublicada = humedad;
  }
}

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

  pinMode(PIN_LED_VENTILACION, OUTPUT);
  dht.begin();
  lcd.init();
  lcd.backlight();
  lcd.clear();

  // Conexión WiFi. Esperar acá con delay() es aceptable porque el ciclo de
  // medición 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, aviso y display, cada 2 s (respeta el DHT11) ---
  if (millisActual - millisAnterior >= INTERVALO_MEDICION_MS) {
    millisAnterior = millisActual;

    float temperatura = dht.readTemperature(); // grados Celsius
    float humedad     = dht.readHumidity();    // humedad relativa en %

    // Una lectura fallida del DHT11 devuelve "no numérico": la salteamos
    // para no mostrar ni publicar basura.
    if (!isnan(temperatura) && !isnan(humedad)) {
      hayMedicion = true;

      // Aviso de ventilación como encendido fijo (if/else).
      if (temperatura > UMBRAL_TEMP_C) {
        digitalWrite(PIN_LED_VENTILACION, HIGH);
      } else {
        digitalWrite(PIN_LED_VENTILACION, LOW);
      }

      // Fila 0: temperatura. Fila 1: humedad. Sin tildes ni Ñ.
      lcd.setCursor(0, 0);
      lcd.print("Temp.: ");
      lcd.print(temperatura);
      lcd.setCursor(0, 1);
      lcd.print("Hum.: ");
      lcd.print(humedad);

      // Publicamos (solo si cambió lo suficiente).
      publicarCambios(temperatura, humedad);
    }
  }

  // --- Mantenimiento de la conexión IoT, en cada vuelta ---
  // Gracias a que la medición no bloquea, esto se atiende continuamente:
  // esta es la razón de fondo por la que la guía abandona delay() antes de IoT.
  mantenerConexionMqtt();
}
El invernadero IoT completo: control no bloqueante + WiFi del ESP32 + publicación de la temperatura y la humedad en Adafruit IO, solo cuando cambian.

Así queda funcionando el tablero que permite monitorear el invernadero a distancia. Podés ver el sistema completo —el sensor midiendo, el aviso, el display 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 invernadero 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 la temperatura y la humedad del invernadero. 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 “Sistema de Riego Automatizado”, para que el invernadero controle tanto el ambiente como el riego de las plantas.

Evidencia de cierre

Al terminar este nivel, esto tiene que estar funcionando:

  • El sistema funciona con código no bloqueante basado en millis(): no queda ningún delay() que detenga el programa entre mediciones, y el LED de aviso pasó de parpadear a quedar encendido fijo mientras la temperatura supera el umbral.
  • 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 temperatura y la humedad del invernadero publicadas por el dispositivo (variante ESP32).