Saltar al contenido
Volver a la ficha del proyecto

Manual del proyecto · Proyecto 05

Estación Meteorológica

Diseño, construcción y programación de una estación meteorológica automatizada: un sistema que solo mide y muestra las condiciones del clima. Con un sensor DHT11 (temperatura y humedad del aire) y un sensor de lluvia informa el estado del tiempo en un display LCD; luego suma la presión atmosférica con un sensor BMP180 y, por último, monitorea todos esos datos de forma remota por Internet de las Cosas (IoT).

Arduino UNO R3 ESP32 DevKit
Dificultad:
Nivel educativo:
Secundario. Ciclo Básico.
Electrónica Programación Internet de las Cosas

Saberes Digitales

INET / EDUCAR — Ministerio de Educación, Cultura, Ciencia y Tecnología de la Nación Argentina

2019

Contenido del manual

  1. 01 Materiales y equipamiento
  2. 02 Medir temperatura, humedad y lluvia en un display LCD Nivel inicial
  3. 03 Agregar la presión atmosférica con un sensor BMP180 Nivel intermedio
  4. 04 Monitorear la estación con Internet de las Cosas Nivel avanzado

Materiales y equipamiento

Insumos

Imagen Insumo Cantidad Plataforma
Foto de Sensor DHT11 Sensor DHT11 (humedad y temperatura) Sensor digital: mide temperatura y humedad del aire y entrega los datos por un pin digital (no analógico, no va a A0). En el esquema original se conecta al pin 2 del UNO; en el ESP32, al GPIO 4. No admite más de una lectura por segundo. 1 Todas
Foto de Sensor de lluvia Sensor de lluvia (módulo colector + comparador, tipo YL-83) Detecta la presencia de lluvia. La placa colectora se moja y, junto al comparador, entrega una salida DIGITAL (llueve / no llueve) que va a un pin digital (el 3 en el original). Se calibra el umbral con su potenciómetro. Usamos la salida digital (DO), no la analógica (AO). 1 Todas
Foto de Sensor de presión BMP180 Sensor de presión BMP180 Mide la presión atmosférica (en hPa) y también temperatura. Se comunica por el bus I2C: en el UNO por A4 (SDA) y A5 (SCL); en el ESP32 por GPIO 21 (SDA) y GPIO 22 (SCL). Requiere la librería Adafruit BMP085 (compatible con el BMP180). 1 Todas
Foto de Cables dupont macho-macho Cable dupont macho-hembra 20 Todas
Foto de Cables dupont hembra-hembra Cable dupont hembra-hembra Los módulos de sensores y el LCD I2C traen pines macho; para llegar a la protoboard o a la placa suelen usarse cables con punta hembra. 20 Todas
Foto de Protoboard Protoboard 1 Todas
Foto de Arduino UNO R3 Arduino UNO R3 El original de 2019 sumaba un módulo IoT OBLOQ para el nivel avanzado; ese módulo está discontinuado. Con UNO, el nivel IoT se sigue de manera conceptual y la práctica se realiza con la variante ESP32. 1 Arduino UNO R3
Foto de Cable USB tipo B Cable USB tipo B 1 Arduino UNO R3
Foto de LCD Keypad Shield Display LCD Keypad Shield Pantalla de 16x2 con formato de shield: se apila sobre el UNO y se maneja en modo paralelo (6 pines). Por su formato Arduino UNO, NO se enchufa en un ESP32. 1 Arduino UNO R3
Foto de Fuente de alimentación Fuente de 9 V 1 A (plug centro positivo, 5,5 x 2,1 mm) (opcional) Para alimentar el sistema sin la computadora. El ESP32 se alimenta directamente por USB con una fuente de 5 V. 1 Arduino UNO R3
Foto de Placa ESP32 DevKit Placa ESP32 DevKit v1 Reemplaza al conjunto Arduino UNO + módulo OBLOQ del original: el ESP32 trae WiFi integrado, necesario para el nivel avanzado (IoT). Trabaja a 3,3 V (no 5 V como el UNO). 1 ESP32 DevKit
Foto de Display LCD 1602 Display LCD 1602 con módulo I2C La alternativa del shield para el ESP32: la misma pantalla de 16x2, pero con un módulo I2C que se conecta con solo dos cables de señal (SDA y SCL) y la librería LiquidCrystal_I2C, en lugar de los 6 pines del shield paralelo del UNO. Comparte el bus I2C con el BMP180. 1 ESP32 DevKit
Foto de Cable micro USB Cable micro USB (con línea de datos) 1 ESP32 DevKit
Filamento PLA para impresora 3D (opcional) Para imprimir los soportes de los sensores de la estación, cuando esté disponible el paquete de fabricación. 1 Todas

Equipamiento del taller

  • Computadora
  • Soldador
  • Estaño
  • Alicate
  • Pinza de punta
  • Pinza brusela

Plano de conexionado en protoboard

Arduino UNO Conexionado en protoboard · Estación meteorológica: DHT11 + sensor de lluvia + BMP180 (I2C) + LCD 1602.

Deslizá horizontalmente para ver el plano completo

Arduino UNO
Sensor DHT11
DHT11
VCCDATAGND
Sensor de lluvia
VCCDOGND
BMP180 (I2C)
VCCGNDSDASCL
LCD 1602 (I2C)
VCCGNDSDASCL
ESP32 DevKit v1 Conexionado en protoboard · Estación meteorológica: DHT11 + sensor de lluvia + BMP180 (I2C) + LCD 1602.

Deslizá horizontalmente para ver el plano completo

ESP32 DevKit v1
Sensor DHT11
DHT11
VCCDATAGND
Sensor de lluvia
VCCDOGND
BMP180 (I2C)
VCCGNDSDASCL
LCD 1602 (I2C)
VCCGNDSDASCL
Arduino UNOConexionado en protoboard
Conexiones · Arduino UNO. Convención de colores: alimentación VCC = rojo, GND = negro; dato del DHT11 = amarillo; salida digital del sensor de lluvia = violeta; bus I2C del BMP180 (y, en el ESP32, también del LCD, que comparte el mismo bus) = verde (SDA) / naranja (SCL); en el UNO el LCD es un shield apilado (blanco).
Desde Hacia Color de cable
Arduino UNO · 5VBus + de la protoboardrojo
Arduino UNO · GNDBus − de la protoboardnegro
Sensor DHT11 · VCCBus + de la protoboardrojo
Sensor DHT11 · GNDBus − de la protoboardnegro
Sensor DHT11 · DATAArduino UNO · 2amarillo
Módulo sensor de lluvia · VCCBus + de la protoboardrojo
Módulo sensor de lluvia · GNDBus − de la protoboardnegro
Módulo sensor de lluvia · DOArduino UNO · 3violeta
BMP180 · VCCBus + de la protoboardrojo
BMP180 · GNDBus − de la protoboardnegro
BMP180 · SDAArduino UNO · A4verde
BMP180 · SCLArduino UNO · A5naranja
LCD Keypad ShieldArduino UNO · apilado sobre los pines digitales (RS 8, E 9, D4-D7 4/5/6/7)blanco
ESP32 DevKit v1Conexionado en protoboard
Conexiones · ESP32 DevKit v1. Convención de colores: alimentación VCC = rojo, GND = negro; dato del DHT11 = amarillo; salida digital del sensor de lluvia = violeta; bus I2C del BMP180 (y, en el ESP32, también del LCD, que comparte el mismo bus) = verde (SDA) / naranja (SCL); en el UNO el LCD es un shield apilado (blanco).
Desde Hacia Color de cable
ESP32 DevKit v1 · 3V3Bus + de la protoboardrojo
ESP32 DevKit v1 · GNDBus − de la protoboardnegro
Sensor DHT11 · VCCBus + de la protoboardrojo
Sensor DHT11 · GNDBus − de la protoboardnegro
Sensor DHT11 · DATAESP32 DevKit v1 · GPIO4amarillo
Módulo sensor de lluvia · VCCBus + de la protoboardrojo
Módulo sensor de lluvia · GNDBus − de la protoboardnegro
Módulo sensor de lluvia · DOESP32 DevKit v1 · GPIO14violeta
BMP180 · VCCBus + de la protoboardrojo
BMP180 · GNDBus − de la protoboardnegro
BMP180 · SDAESP32 DevKit v1 · GPIO21verde
BMP180 · SCLESP32 DevKit v1 · GPIO22naranja
LCD 1602 I2C · VCCBus + de la protoboardrojo
LCD 1602 I2C · GNDBus − de la protoboardnegro
LCD 1602 I2C · SDAESP32 DevKit v1 · GPIO21verde
LCD 1602 I2C · SCLESP32 DevKit v1 · GPIO22naranja
Nivel inicial

Medir temperatura, humedad y lluvia en un display LCD

Conectar el display LCD

Empezamos por la pantalla donde vamos a ver las mediciones: un display LCD de 16 columnas por 2 filas. Acá aparece la primera diferencia entre las dos plataformas de este proyecto, y vale la pena entenderla, porque es muy didáctica: el display se conecta de una manera distinta en cada una.

Esquema de conexión: a la izquierda un LCD Keypad Shield (placa verde con pantalla azul de dos filas y botones abajo); a la derecha, en una protoboard, un módulo DHT11 azul. Cables de alimentación (rojo y negro) y un cable de datos amarillo llevan la señal del DHT11 hacia la placa.
El display LCD y el sensor DHT11 en la variante Arduino UNO (figura original): el LCD Keypad Shield se apila sobre la placa; el módulo DHT11 va en la protoboard con su cable de datos y su alimentación.

En el Arduino UNO usamos el LCD Keypad Shield del original, un shield que tiene el formato de la placa y se apila directamente encima del UNO, encastrando en sus pines: no hay que cablear nada en la protoboard. Al apilarlo, el display queda conectado en modo paralelo, usando seis pines de datos de la placa (los pines 8, 9, 4, 5, 6 y 7). Es importante no forzar las piezas: todos los pines deben coincidir perfectamente. Como el shield ocupa varios pines, los sensores se cablean a los pines que quedan libres.

El LCD Keypad Shield tiene el formato del Arduino UNO y no se enchufa en un ESP32. Por eso, en el ESP32 usamos un LCD 1602 con módulo I2C: la misma pantalla de 16x2, pero con una plaquita (el módulo I2C) pegada atrás que reduce el cableado a solo dos cables de señal. Conectamos: VCC a 3,3 V, GND a GND, SDA al GPIO 21 y SCL al GPIO 22 (los pines I2C por defecto del ESP32). Guardá en la memoria este bus I2C: en el nivel intermedio el sensor de presión se va a colgar de estos mismos dos cables.

La programación la haremos directamente en el lenguaje de Arduino (C++), con el Arduino IDE en su versión 2.x, que se descarga gratis desde arduino.cc/en/software. Un programa de Arduino se llama sketch y tiene dos bloques: setup(), que se ejecuta una sola vez al encender, y loop(), que se ejecuta repetidamente después. Todo lo que sigue a // es un comentario: no afecta al programa, sirve para que el código sea más fácil de leer.

Un detalle a tener siempre presente con estas pantallas: el LCD no representa bien las tildes ni la letra Ñ, así que los textos se escriben sin ellas (por eso, más adelante, T:, H: y Lluvia:).

Si al probar el display no se lee (pantalla en blanco o todo encendido), ajustá el contraste con el potenciómetro azul (RP1) de la esquina superior izquierda del shield, con un destornillador chico.

Conectar el sensor de temperatura y humedad (DHT11)

Ahora sumamos el sensor que mide el aire: el sensor DHT11, que registra la temperatura y la humedad en un mismo integrado.

Un dato importante: el DHT11 es un sensor digital. A diferencia de un sensor analógico (que se conecta a una entrada A0–A5), el DHT11 tiene electrónica interna que digitaliza la medición y la transmite a la placa como un paquete de datos por un pin digital. Por eso no va a A0: va a un pin digital, y para leerlo hace falta una librería que gestione esa comunicación (la instalamos en un momento).

El DHT11 tiene tres conexiones útiles: alimentación (VCC y GND) y datos.

En el Arduino UNO seguimos el esquema original: el pin de datos del DHT11 va al pin digital 2 (la constante PIN_DHT del código), VCC al pin 5V y GND a GND.

En el ESP32 la conexión es equivalente, con un cuidado propio de la placa: el pin de datos del DHT11 va al GPIO 4 (la constante PIN_DHT del código) y la alimentación se toma de 3,3 V (no de 5 V) y GND. El DHT11 funciona sin problemas a 3,3 V.

Para poder leer el sensor necesitamos instalar su librería. En el Arduino IDE 2.x abrimos el Gestor de bibliotecas (el ícono de los libritos en la barra lateral), buscamos “DHT sensor library” de Adafruit y la instalamos; el IDE ofrecerá instalar también su dependencia “Adafruit Unified Sensor”: aceptá, porque el sensor no funciona sin ella. Esta librería reemplaza a la vieja DHT.h genérica del original, que ya no se consigue igual.

Programar la lectura del DHT11 y mostrarla en el display

Con la pantalla y el sensor conectados, la primera versión del programa lee el DHT11 y muestra sus valores en el LCD. Aprovechamos las dos filas del display: en la primera escribimos la temperatura, precedida por el rótulo T:, y a su lado la humedad con el rótulo H:. En cada vuelta de loop() el programa lee el sensor una vez y actualiza esos números.

Hay un detalle del sensor que marca el ritmo del programa: el DHT11 no admite más de una lectura por segundo. Por eso, entre medición y medición el programa espera con delay() el tiempo de la constante INTERVALO_MEDICION_MS, que está en 2 segundos (un margen seguro; la librería de Adafruit, además, guarda la última lectura durante 2 segundos). En este primer nivel usar delay() es lo más simple y hace que el código se lea igual que su comportamiento; en el nivel avanzado lo cambiaremos.

En el próximo paso agregamos el sensor de lluvia y vemos el programa completo del nivel inicial.

Agregar el sensor de lluvia

Falta el tercer sensor de esta etapa: el sensor de lluvia. Con él, en la segunda fila del display vamos a mostrar Lluvia: SI o Lluvia: NO, según corresponda.

El sensor de lluvia consta de una placa colectora (la que se moja) recorrida por dos pistas conductoras, y una plaquita con un comparador. Cuando cae agua, las pistas se conectan y el módulo lo detecta. Lo importante para programarlo: su salida digital (DO) indica presencia o ausencia de lluvia, así que se conecta a un pin digital (el pin de la constante PIN_LLUVIA del código, el 3 del original) y se lee con digitalRead().

Esquema de conexión: a la izquierda el LCD Keypad Shield; en la protoboard, el módulo DHT11 azul y el módulo del sensor de lluvia (plaquita con potenciómetro y pines AO, DO, GND, VCC); arriba, la placa colectora del sensor de lluvia etiquetada YL-83, unida a la plaquita por dos cables.
El sistema del nivel inicial en la variante Arduino UNO (figura original): DHT11 y sensor de lluvia sobre la protoboard, junto al LCD Keypad Shield. La placa colectora (arriba, YL-83) es la parte que se expone a la lluvia.

Antes de medir conviene calibrar el umbral con el potenciómetro azul del módulo. En la calibración del original: cuando el sensor detecta agua, su salida digital queda en estado BAJO (0 V); cuando no detecta lluvia, queda en estado ALTO (5 V). El programa lee ese estado con digitalRead(): si está en alto, muestra Lluvia: NO; si está en bajo, Lluvia: SI.

En el Arduino UNO, la salida digital (DO) del sensor de lluvia va al pin 3 (PIN_LLUVIA), y su alimentación a 5V y GND.

En el ESP32, la salida digital (DO) del sensor de lluvia va a un pin digital (PIN_LLUVIA, el GPIO indicado en el código) y su alimentación a 3,3 V y GND.

Este es el programa completo del nivel inicial: lee el DHT11 y el sensor de lluvia, y muestra en el LCD la temperatura, la humedad y el estado de la lluvia. Lo único que cambia entre plataformas es cómo se maneja el display (LiquidCrystal en el UNO, LiquidCrystal_I2C en el ESP32); la lógica de los sensores es idéntica.

Ver el código completo · estacion-basica.ino
estacion-basica.ino Arduino UNO
// Estación meteorológica básica: mide temperatura y humedad del aire con un
// sensor DHT11 y detecta presencia de lluvia con un sensor digital, y muestra
// todo en un display LCD 16x2. Fila 0: temperatura y humedad. Fila 1: si
// llueve o no.
//
// CONVENCIÓN DEL SENSOR DE LLUVIA (¡importante!, activo-bajo): este módulo
// (tipo FC-37 / YL-83 con comparador LM393) cierra circuito cuando se moja.
// Su salida digital D0 vale BAJO (0 V) cuando DETECTA agua y ALTO (5 V)
// cuando está SECO. El original lo confirma: "al detectar agua... la salida
// digital D0 se debe encontrar en el estado BAJO (0V); cuando no se detecta
// lluvia... en el estado ALTO (5V)". Por eso comparamos contra LOW.
// La sensibilidad se calibra con el potenciómetro azul del módulo.
//
// Plataforma: Arduino UNO.
// Conexión (la del esquema original): dato del DHT11 → pin 2; salida digital
// D0 del sensor de lluvia → pin 3; display LCD Keypad Shield en modo paralelo
// por los pines 8, 9, 4, 5, 6, 7 (se apila sobre el UNO; su potenciómetro RP1
// ajusta el contraste).
//
// Nota del original: en el LCD no usar tildes ni la letra Ñ.
// Nota física: la placa de contactos del sensor de lluvia se moja a propósito,
// pero la electrónica (la placa Arduino y el módulo comparador) debe quedar
// protegida del agua. Es todo de baja tensión (5 V).
//
// Basado en: Fragmento de código del Nivel Inicial (Paso 4) — Estación
// Meteorológica Automatizada, Saberes Digitales (INET/EDUCAR 2019)
// Modernización: (1) C++ idiomático con constantes nombradas en vez del C de
// mBlock3. (2) Librería "DHT sensor library" de Adafruit (instalar con su
// dependencia "Adafruit Unified Sensor"), que reemplaza la vieja "DHT.h" y
// requiere dht.begin(). (3) El original no pausaba el loop; acá medimos cada
// 2 s porque el DHT11 no admite más de una lectura por segundo.

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

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

// Salida digital D0 del sensor de lluvia.
const int PIN_LLUVIA = 3;

// Nivel de la salida del sensor cuando HAY lluvia (activo-bajo: ver encabezado).
const int LLUVIA_DETECTADA_NIVEL = LOW;

// El DHT11 no admite más de una lectura por segundo; medimos cada 2 s
// (margen seguro; la librería de Adafruit además cachea 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);

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

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

  // Fila 0: temperatura (col 0) y humedad (col 8). Sin tildes ni Ñ.
  lcd.setCursor(0, 0);
  lcd.print("T:");
  lcd.print(temperatura);
  lcd.setCursor(8, 0);
  lcd.print("H:");
  lcd.print(humedad);

  // Fila 1: estado de la lluvia. La salida en LOW significa agua detectada.
  lcd.setCursor(0, 1);
  if (digitalRead(PIN_LLUVIA) == LLUVIA_DETECTADA_NIVEL) {
    lcd.print("Lluvia:SI");
  } else {
    lcd.print("Lluvia:NO");
  }

  // En este nivel usamos delay(): es la herramienta que ya conocemos y hace
  // que el programa se lea de arriba hacia abajo igual que su comportamiento.
  delay(INTERVALO_MEDICION_MS);
}
estacion-basica.ino ESP32
// Estación meteorológica básica: mide temperatura y humedad del aire con un
// sensor DHT11 y detecta presencia de lluvia con un sensor digital, y muestra
// todo en un display LCD 16x2. Fila 0: temperatura y humedad. Fila 1: si
// llueve o no.
//
// CONVENCIÓN DEL SENSOR DE LLUVIA (¡importante!, activo-bajo): este módulo
// (tipo FC-37 / YL-83 con comparador LM393) cierra circuito cuando se moja.
// Su salida digital D0 vale BAJO (0 V) cuando DETECTA agua y ALTO cuando está
// SECO. El original lo confirma: "al detectar agua... la salida D0 en estado
// BAJO; cuando no se detecta lluvia... en ALTO". Por eso comparamos contra LOW.
// La sensibilidad se calibra con el potenciómetro azul del módulo.
//
// Plataforma: ESP32 (DevKit v1).
// Conexión: dato del DHT11 → GPIO 4; salida digital D0 del sensor de lluvia →
// GPIO 14; 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; el DHT11 y el módulo I2C del LCD
// funcionan a 3,3 V. El módulo de lluvia se alimenta a 3,3 V para que su
// salida no supere ese nivel en el GPIO.
// Nota física: la placa de contactos del sensor de lluvia se moja a propósito,
// pero la electrónica (placa y módulo comparador) debe quedar protegida del
// agua. Es todo de baja tensión.
//
// Librerías requeridas (Library Manager): "DHT sensor library" (Adafruit) +
// "Adafruit Unified Sensor", y "LiquidCrystal_I2C".
//
// Basado en: Fragmento de código del Nivel Inicial (Paso 4) — Estación
// Meteorológica Automatizada, Saberes Digitales (INET/EDUCAR 2019)
// Modernización: C++ idiomático con constantes nombradas; portado a ESP32;
// DHT de Adafruit con dht.begin(); LCD paralelo del original adaptado a I2C;
// pausa de 2 s (límite del DHT11; el original no pausaba el loop).

#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

// Salida digital D0 del sensor de lluvia.
const int PIN_LLUVIA = 14;

// Nivel de la salida del sensor cuando HAY lluvia (activo-bajo).
const int LLUVIA_DETECTADA_NIVEL = LOW;

// 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;

// 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);

void setup() {
  pinMode(PIN_LLUVIA, INPUT);
  dht.begin();
  // Con I2C, primero arranca el display; backlight enciende la luz de fondo.
  lcd.init();
  lcd.backlight();
  lcd.clear();
}

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

  // Fila 0: temperatura (col 0) y humedad (col 8). Sin tildes ni Ñ.
  lcd.setCursor(0, 0);
  lcd.print("T:");
  lcd.print(temperatura);
  lcd.setCursor(8, 0);
  lcd.print("H:");
  lcd.print(humedad);

  // Fila 1: estado de la lluvia. La salida en LOW significa agua detectada.
  lcd.setCursor(0, 1);
  if (digitalRead(PIN_LLUVIA) == LLUVIA_DETECTADA_NIVEL) {
    lcd.print("Lluvia:SI");
  } else {
    lcd.print("Lluvia:NO");
  }

  delay(INTERVALO_MEDICION_MS);
}
Estación básica: el DHT11 mide temperatura y humedad, el sensor de lluvia indica si llueve, y el LCD muestra los tres datos (T: y H: en la primera fila, Lluvia: en la segunda). Mide cada 2 segundos, respetando el límite del DHT11.

Podés ver el comportamiento —los valores actualizándose y el estado de la lluvia cambiando— en el simulador, antes de armar el circuito real:

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.

Para subir el código a la placa, conectala a la computadora con el cable USB, elegí en el Arduino IDE 2.x la placa y el puerto (en el IDE 2.x se eligen juntos en el menú desplegable de la barra superior) y apretá el botón Subir (la flecha →). Al terminar, aparece el mensaje de carga completada en la barra inferior.

Para el Arduino UNO: usá el cable USB tipo B, elegí la placa Arduino UNO y el puerto que se agregó al conectarla (en Windows es un puerto COM numerado; en Linux suele llamarse /dev/ttyACM0 o similar).

Para el ESP32 hay dos pasos previos que se hacen una sola vez: instalar el soporte de placas ESP32 desde el gestor de placas del IDE (buscá “esp32” e instalá el paquete de Espressif) y, si el puerto no aparece, instalar el driver del conversor USB-serie de tu placa (generalmente CP210x o CH340). Después, elegí la placa ESP32 Dev Module (o “DOIT ESP32 DEVKIT V1”) y el puerto correspondiente. Si al subir el IDE queda esperando en “Connecting…”, mantené apretado el botón BOOT de la placa hasta que empiece la carga.

Cierre del nivel

Al terminar este nivel, tu estación ya mide y muestra tres datos del clima: la temperatura y la humedad del aire (DHT11) y la presencia de lluvia (sensor de lluvia), todo en el display LCD. En el camino armaste un circuito con varios sensores y viste la primera diferencia real entre las plataformas: el LCD paralelo del UNO frente al LCD por I2C del ESP32. En el nivel intermedio vamos a sumar un dato más del tiempo —la presión atmosférica— con un sensor BMP180 que se comunica por el bus I2C.

Evidencia de cierre

Al terminar este nivel, esto tiene que estar funcionando:

  • El circuito está montado en la protoboard: el display LCD conectado (LCD Keypad Shield apilado en el UNO, o LCD 1602 por I2C en el ESP32), el sensor DHT11 en su pin de datos y el sensor de lluvia en su pin digital.
  • Sabés subir un programa desde el Arduino IDE 2.x: elegir la placa, elegir el puerto y verificar que el código quedó cargado.
  • El display muestra la temperatura y la humedad que mide el DHT11 y, en la segunda fila, «Lluvia: SI» o «Lluvia: NO» según el estado del sensor de lluvia.
  • Entendés que el DHT11 es digital (pin digital, no A0) y que el sensor de lluvia entrega una salida digital de umbral (llueve / no llueve).
Nivel intermedio

Agregar la presión atmosférica con un sensor BMP180

Conectar el sensor de presión BMP180

En el nivel inicial armamos una estación que mide temperatura, humedad y presencia de lluvia. Ahora le sumamos un sensor BMP180, que mide la presión atmosférica: la fuerza que ejerce el aire sobre la superficie de la Tierra. La presión se expresa en hectopascales (hPa) (un valor típico ronda los 1013 hPa a nivel del mar).

El BMP180 no se lee por un pin cualquiera: se comunica por el bus I2C, un bus serial que usa solo dos líneas de señal, SDA (datos) y SCL (reloj), más su alimentación.

Esquema de conexión del sistema completo: a la izquierda el LCD Keypad Shield; en la protoboard, el módulo DHT11, el módulo del sensor de lluvia con su placa colectora YL-83 y, a la derecha, el pequeño módulo BMP180. Cables de señal amarillos y grises llevan las líneas SDA y SCL hacia la placa.
El sistema completo del nivel intermedio en la variante Arduino UNO (figura original): al DHT11 y al sensor de lluvia se suma el BMP180 (derecha), conectado por las líneas I2C. En el ESP32, el BMP180 y el LCD I2C comparten el mismo bus.

En el Arduino UNO, el bus I2C está en los pines A4 (SDA) y A5 (SCL). Conectamos ahí las líneas del BMP180, más su alimentación a 5V y GND, como indica el esquema.

En el ESP32, el bus I2C está en GPIO 21 (SDA) y GPIO 22 (SCL): exactamente los mismos pines donde ya conectamos el LCD I2C en el nivel inicial. Y acá viene el punto lindo: el I2C es un bus compartido. El LCD y el BMP180 se cuelgan de los mismos dos cables, y cada uno responde a su propia dirección: el LCD en 0x27 y el BMP180 en 0x77. No hay conflicto, porque cuando la placa «llama» a una dirección, solo contesta ese dispositivo. Conectamos, entonces, SDA y SCL del BMP180 en paralelo con los del LCD, y su alimentación a 3,3 V y GND.

Instalar la librería del BMP180

Como el BMP180 se comunica por I2C, necesitamos una librería que resuelva ese protocolo. Es la misma que usaba el original: la “Adafruit BMP085 Library” (el BMP085 y el BMP180 son compatibles, por eso sirve la misma librería). La instalamos desde el Gestor de bibliotecas del Arduino IDE 2.x: buscamos “Adafruit BMP085” y apretamos Instalar.

En el código, el sensor se declara con Adafruit_BMP085 bmp;, se inicia con bmp.begin() en el setup(), y la presión se lee con bmp.readPressure(). Esa función devuelve la presión en pascales (Pa); como es más habitual expresarla en hectopascales (hPa), el código la divide por 100 antes de mostrarla.

Programar y mostrar la presión en el display

Ahora completamos el programa: al código del nivel inicial le agregamos la lectura del BMP180 y mostramos la presión en el display, identificada con la letra P. El original la ubica junto al estado de la lluvia, en la segunda fila; por ejemplo, P: 1013.

El programa sigue midiendo cada INTERVALO_MEDICION_MS (2 segundos, por el límite del DHT11): en cada ciclo lee la temperatura y la humedad del DHT11, el estado del sensor de lluvia y la presión del BMP180, y vuelca los cuatro datos al LCD.

Ver el código completo · estacion-con-presion.ino
estacion-con-presion.ino Arduino UNO
// Estación meteorológica con presión: además de temperatura y humedad (DHT11)
// y presencia de lluvia (sensor digital), suma un sensor BMP180 que mide la
// presión atmosférica. Todo se muestra en el LCD 16x2. Fila 0: temperatura y
// humedad. Fila 1: estado de lluvia y presión (rótulo "P:").
//
// ¿Por qué importa la presión? El original lo explica: las bajas presiones
// suelen asociarse a tiempo inestable (y a veces lluvia) y las altas, a tiempo
// estable. Sumarla enriquece el pronóstico de la estación.
//
// CONVENCIÓN DEL SENSOR DE LLUVIA (activo-bajo): la salida digital D0 vale
// BAJO cuando DETECTA agua y ALTO cuando está SECO (ver detalle en el sketch
// del nivel inicial). Por eso comparamos contra LOW.
//
// Plataforma: Arduino UNO.
// Conexión (la del esquema original): dato del DHT11 → pin 2; lluvia D0 → pin
// 3; display LCD Keypad Shield paralelo por 8, 9, 4, 5, 6, 7; BMP180 por el bus
// I2C del UNO: SDA → A4, SCL → A5.
//
// 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 "Adafruit BMP085 Library" (sirve para BMP085 y
// BMP180, que son compatibles; es la que nombra el original).
//
// Basado en: Fragmento de código del Nivel Intermedio (Paso 3) — Estación
// Meteorológica Automatizada, Saberes Digitales (INET/EDUCAR 2019)
// Modernización: constantes nombradas; DHT de Adafruit con dht.begin();
// pausa de 2 s (el DHT11 no admite más de una lectura por segundo; el original
// no pausaba el loop).

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

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

// Salida digital D0 del sensor de lluvia.
const int PIN_LLUVIA = 3;

// Nivel de la salida del sensor cuando HAY lluvia (activo-bajo).
const int LLUVIA_DETECTADA_NIVEL = LOW;

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

// Objetos de sensores y display. El BMP180 habla por I2C (A4/A5 en el UNO).
DHT dht(PIN_DHT, DHTTYPE);
Adafruit_BMP085 bmp;
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

void setup() {
  pinMode(PIN_LLUVIA, INPUT);
  dht.begin();
  bmp.begin(); // inicializa el sensor de presión por I2C
  lcd.begin(16, 2);
  lcd.clear();
}

void loop() {
  float temperatura = dht.readTemperature();      // grados Celsius
  float humedad     = dht.readHumidity();         // humedad relativa en %
  // readPressure() devuelve pascales (Pa); dividimos por 100 para tener
  // hectopascales (hPa), la unidad habitual del "1013 hPa" del clima.
  float presion     = bmp.readPressure() / 100.0;

  // Fila 0: temperatura (col 0) y humedad (col 8). Sin tildes ni Ñ.
  lcd.setCursor(0, 0);
  lcd.print("T:");
  lcd.print(temperatura);
  lcd.setCursor(8, 0);
  lcd.print("H:");
  lcd.print(humedad);

  // Fila 1: estado de la lluvia (col 0) y presión (col 10).
  lcd.setCursor(0, 1);
  if (digitalRead(PIN_LLUVIA) == LLUVIA_DETECTADA_NIVEL) {
    lcd.print("Lluvia:SI");
  } else {
    lcd.print("Lluvia:NO");
  }
  lcd.setCursor(10, 1);
  lcd.print("P:");
  lcd.print(presion);

  delay(INTERVALO_MEDICION_MS);
}
estacion-con-presion.ino ESP32
// Estación meteorológica con presión: además de temperatura y humedad (DHT11)
// y presencia de lluvia (sensor digital), suma un sensor BMP180 que mide la
// presión atmosférica. Todo se muestra en el LCD 16x2. Fila 0: temperatura y
// humedad. Fila 1: estado de lluvia y presión (rótulo "P:").
//
// ¿Por qué importa la presión? El original lo explica: las bajas presiones
// suelen asociarse a tiempo inestable (y a veces lluvia) y las altas, a tiempo
// estable.
//
// BUS I2C COMPARTIDO (¡buen punto didáctico!): en el ESP32, TANTO el LCD como
// el BMP180 se conectan al MISMO bus I2C (SDA GPIO 21, SCL GPIO 22). El I2C es
// un bus compartido: varios dispositivos conviven en los mismos dos cables
// porque cada uno tiene una DIRECCIÓN distinta. El LCD responde en 0x27 y el
// BMP180 en 0x77, así que no hay conflicto. Ambos cuelgan en paralelo de
// SDA/SCL.
//
// CONVENCIÓN DEL SENSOR DE LLUVIA (activo-bajo): la salida D0 vale BAJO cuando
// DETECTA agua y ALTO cuando está SECO (ver detalle en el nivel inicial).
//
// Plataforma: ESP32 (DevKit v1).
// Conexión: DHT11 → GPIO 4; lluvia D0 → GPIO 14; LCD y BMP180 al bus I2C
// (SDA GPIO 21, SCL GPIO 22).
//
// ADAPTACIÓN DE PLATAFORMA (ver README): el original usa un LCD Keypad Shield
// paralelo que no entra en un ESP32; acá el LCD es 1602 I2C (LiquidCrystal_I2C,
// 0x27; probá 0x3F si no muestra nada).
// Nota eléctrica: el ESP32 trabaja a 3,3 V; DHT11, BMP180 y LCD I2C andan 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", "Adafruit BMP085 Library" (BMP085/BMP180) y
// "LiquidCrystal_I2C".
//
// Basado en: Fragmento de código del Nivel Intermedio (Paso 3) — Estación
// Meteorológica Automatizada, Saberes Digitales (INET/EDUCAR 2019)
// Modernización: constantes nombradas; portado a ESP32; DHT de Adafruit con
// dht.begin(); LCD adaptado a I2C (comparte bus con el BMP180); pausa de 2 s.

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

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

// Salida digital D0 del sensor de lluvia.
const int PIN_LLUVIA = 14;

// Nivel de la salida del sensor cuando HAY lluvia (activo-bajo).
const int LLUVIA_DETECTADA_NIVEL = LOW;

// 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;

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

// Objetos de sensores y display. LCD (0x27) y BMP180 (0x77) comparten el bus.
DHT dht(PIN_DHT, DHTTYPE);
Adafruit_BMP085 bmp;
LiquidCrystal_I2C lcd(LCD_DIRECCION_I2C, LCD_COLUMNAS, LCD_FILAS);

void setup() {
  pinMode(PIN_LLUVIA, INPUT);
  dht.begin();
  bmp.begin(); // el BMP180 se inicializa por el mismo bus I2C que el LCD
  lcd.init();
  lcd.backlight();
  lcd.clear();
}

void loop() {
  float temperatura = dht.readTemperature();      // grados Celsius
  float humedad     = dht.readHumidity();         // humedad relativa en %
  float presion     = bmp.readPressure() / 100.0; // Pa → hPa

  // Fila 0: temperatura (col 0) y humedad (col 8). Sin tildes ni Ñ.
  lcd.setCursor(0, 0);
  lcd.print("T:");
  lcd.print(temperatura);
  lcd.setCursor(8, 0);
  lcd.print("H:");
  lcd.print(humedad);

  // Fila 1: estado de la lluvia (col 0) y presión (col 10).
  lcd.setCursor(0, 1);
  if (digitalRead(PIN_LLUVIA) == LLUVIA_DETECTADA_NIVEL) {
    lcd.print("Lluvia:SI");
  } else {
    lcd.print("Lluvia:NO");
  }
  lcd.setCursor(10, 1);
  lcd.print("P:");
  lcd.print(presion);

  delay(INTERVALO_MEDICION_MS);
}
Estación con presión: al DHT11 y al sensor de lluvia se suma el BMP180. El LCD muestra T: y H: en la primera fila, y Lluvia: y P: (en hPa) en la segunda. La única diferencia entre plataformas sigue siendo cómo se maneja el display.

Fijate, al comparar las dos plataformas con el selector, que el sensor de presión se lee igual en ambas: la librería resuelve el I2C por debajo. Lo único que cambia entre UNO y ESP32 es, otra vez, la parte del display.

Podés ver el sistema completo —los cuatro datos actualizándose en pantalla— 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 estación mide cuatro variables del clima —temperatura, humedad, lluvia y presión atmosférica— y las muestra todas en el display. Además, apareció una idea importante: el bus I2C, donde el BMP180 (y, en el ESP32, también el LCD) conviven en dos cables compartidos gracias a sus direcciones. En el nivel avanzado vamos a poder mirar todos estos datos a distancia: publicaremos la temperatura, la humedad, la presión y el estado de la lluvia por Internet de las Cosas para monitorear la estación desde el celular.

Evidencia de cierre

Al terminar este nivel, esto tiene que estar funcionando:

  • El sensor de presión BMP180 está conectado por el bus I2C: en el UNO por A4 (SDA) y A5 (SCL); en el ESP32 por GPIO 21 (SDA) y GPIO 22 (SCL), compartiendo el bus con el LCD I2C.
  • Instalaste la librería Adafruit BMP085 (compatible con el BMP180) desde el gestor de bibliotecas del Arduino IDE 2.x.
  • El display muestra ahora también la presión atmosférica en hectopascales (hPa), junto a la temperatura, la humedad y el estado de la lluvia.
Nivel avanzado

Monitorear la estación 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 de la estación. 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, la humedad, la lluvia y la presión, 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 cuatro, 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.
  • presion: recibirá el valor de presión atmosférica, en hectopascales (hPa).
  • lluvia: recibirá el estado de la lluvia como un 0 (no llueve) o un 1 (llueve).

Los tres primeros son los del original; el feed lluvia es una extensión de esta reedición: la estación ya mide la lluvia, así que tiene sentido publicarla también para completar el monitoreo remoto (queda registrado en la ficha del proyecto).

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, bloques del tipo Line Chart (gráfico de línea) para temperatura, humedad y presion: nos permiten ver el historial de esas variables a lo largo del tiempo. Para lluvia, que es un valor de 0 o 1, conviene un bloque más simple, como un indicador o un gráfico que muestre el estado actual. 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 clima que registra la estación.

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 el display, 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.

Ver el código completo · estacion-no-bloqueante.ino
estacion-no-bloqueante.ino Arduino UNO
// Estación meteorológica completa SIN delay(): DHT11 (temperatura y humedad),
// sensor de lluvia y BMP180 (presión), mostrando todo en el LCD 16x2, pero
// con millis() en lugar de delay(). Cada vuelta de loop() pregunta si ya pasó
// el intervalo de medición y, si no, deja el procesador libre.
//
// Este programa es la base directa de estacion-iot.ino: sin código bloqueante,
// el ESP32 puede sumar WiFi y MQTT sin desatender los sensores.
//
// CONVENCIÓN DEL SENSOR DE LLUVIA (activo-bajo): la salida digital D0 vale
// BAJO cuando DETECTA agua y ALTO cuando está SECO (ver detalle en el sketch
// del nivel inicial). Por eso comparamos contra LOW.
//
// Plataforma: Arduino UNO.
// Conexión (la del esquema original): dato del DHT11 → pin 2; lluvia D0 → pin
// 3; display LCD Keypad Shield paralelo por 8, 9, 4, 5, 6, 7; BMP180 por I2C:
// SDA → A4, SCL → A5.
//
// 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 "Adafruit BMP085 Library" (BMP085/BMP180).
//
// Basado en: Fragmentos de código del Nivel Avanzado (patrón millis, sin la
// parte IoT) — Estación Meteorológica Automatizada, Saberes Digitales
// (INET/EDUCAR 2019)
// Modernización: constantes nombradas; millisAnterior como unsigned long (el
// long del original desborda la cuenta); DHT de Adafruit con dht.begin().

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

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

// Salida digital D0 del sensor de lluvia.
const int PIN_LLUVIA = 3;

// Nivel de la salida del sensor cuando HAY lluvia (activo-bajo).
const int LLUVIA_DETECTADA_NIVEL = LOW;

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

// Objetos de sensores y display.
DHT dht(PIN_DHT, DHTTYPE);
Adafruit_BMP085 bmp;
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_LLUVIA, INPUT);
  dht.begin();
  bmp.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 %
    float presion     = bmp.readPressure() / 100.0; // Pa → hPa

    // Fila 0: temperatura (col 0) y humedad (col 8). Sin tildes ni Ñ.
    lcd.setCursor(0, 0);
    lcd.print("T:");
    lcd.print(temperatura);
    lcd.setCursor(8, 0);
    lcd.print("H:");
    lcd.print(humedad);

    // Fila 1: estado de la lluvia (col 0) y presión (col 10).
    lcd.setCursor(0, 1);
    if (digitalRead(PIN_LLUVIA) == LLUVIA_DETECTADA_NIVEL) {
      lcd.print("Lluvia:SI");
    } else {
      lcd.print("Lluvia:NO");
    }
    lcd.setCursor(10, 1);
    lcd.print("P:");
    lcd.print(presion);
  }

  // En este punto el procesador queda libre para realizar otras tareas.
}
estacion-no-bloqueante.ino ESP32
// Estación meteorológica completa SIN delay(): DHT11 (temperatura y humedad),
// sensor de lluvia y BMP180 (presión), mostrando todo en el LCD 16x2 (I2C),
// pero con millis() en lugar de delay(). Cada vuelta de loop() pregunta si ya
// pasó el intervalo de medición y, si no, deja el procesador libre.
//
// Este programa es la base directa de estacion-iot.ino: sin código bloqueante,
// el ESP32 puede sumar WiFi y MQTT sin desatender los sensores.
//
// BUS I2C COMPARTIDO: el LCD (0x27) y el BMP180 (0x77) conviven en el mismo
// bus I2C (SDA GPIO 21, SCL GPIO 22): distintas direcciones, sin conflicto.
//
// CONVENCIÓN DEL SENSOR DE LLUVIA (activo-bajo): la salida D0 vale BAJO cuando
// DETECTA agua y ALTO cuando está SECO (ver detalle en el nivel inicial).
//
// Plataforma: ESP32 (DevKit v1).
// Conexión: DHT11 → GPIO 4; lluvia D0 → GPIO 14; LCD y BMP180 al bus I2C
// (SDA GPIO 21, SCL GPIO 22).
//
// ADAPTACIÓN DE PLATAFORMA (ver README): LCD 1602 I2C (LiquidCrystal_I2C,
// 0x27; probá 0x3F si no muestra nada) en lugar del Keypad Shield paralelo
// del original.
// 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", "Adafruit BMP085 Library" y "LiquidCrystal_I2C".
//
// Basado en: Fragmentos de código del Nivel Avanzado (patrón millis, sin la
// parte IoT) — Estación Meteorológica Automatizada, Saberes Digitales
// (INET/EDUCAR 2019)
// Modernización: constantes nombradas; millisAnterior como unsigned long;
// portado a ESP32; DHT de Adafruit con dht.begin(); LCD adaptado a I2C.

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

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

// Salida digital D0 del sensor de lluvia.
const int PIN_LLUVIA = 14;

// Nivel de la salida del sensor cuando HAY lluvia (activo-bajo).
const int LLUVIA_DETECTADA_NIVEL = LOW;

// 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;

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

// Objetos de sensores y display por I2C.
DHT dht(PIN_DHT, DHTTYPE);
Adafruit_BMP085 bmp;
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_LLUVIA, INPUT);
  dht.begin();
  bmp.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 %
    float presion     = bmp.readPressure() / 100.0; // Pa → hPa

    // Fila 0: temperatura (col 0) y humedad (col 8). Sin tildes ni Ñ.
    lcd.setCursor(0, 0);
    lcd.print("T:");
    lcd.print(temperatura);
    lcd.setCursor(8, 0);
    lcd.print("H:");
    lcd.print(humedad);

    // Fila 1: estado de la lluvia (col 0) y presión (col 10).
    lcd.setCursor(0, 1);
    if (digitalRead(PIN_LLUVIA) == LLUVIA_DETECTADA_NIVEL) {
      lcd.print("Lluvia:SI");
    } else {
      lcd.print("Lluvia:NO");
    }
    lcd.setCursor(10, 1);
    lcd.print("P:");
    lcd.print(presion);
  }

  // En este punto el procesador queda libre para realizar otras tareas
  // (en estacion-iot.ino, acá se atiende la conexión WiFi/MQTT).
}
La misma estación del nivel intermedio, ahora sin delay(): el programa consulta millis() y solo mide y actualiza el display cuando pasó el intervalo. Por fuera se comporta igual; por dentro deja el procesador libre para, en el ESP32, atender internet.

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 sus mediciones 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 en los cuatro feeds (temperatura, humedad, presion y lluvia). 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, 1 % la humedad y 1 hPa la presión; la lluvia se publica cuando cambia de estado. 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 display sigue funcionando, y al volver la conexión el panel se pone al día solo.

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

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

estacion-iot.ino ESP32
// Estación meteorológica con monitoreo IoT vía Adafruit IO (MQTT).
// La lógica no bloqueante de estacion-no-bloqueante.ino sigue midiendo con el
// DHT11, el sensor de lluvia y el BMP180, y mostrando en el LCD (I2C); además
// publica cuatro feeds para verlos desde el Panel de Control de Adafruit:
//
//   "temperatura": grados Celsius.
//   "humedad": humedad relativa en %.
//   "presion": presión atmosférica en hPa.
//   "lluvia": 0 = seco, 1 = lloviendo.
//
// El original publicaba solo temperatura, humedad y presión. Acá se agrega el
// feed "lluvia" (extensión): el estado de lluvia ya se muestra en el LCD y es
// una variable meteorológica que también vale la pena monitorear a distancia.
//
// BUS I2C COMPARTIDO: el LCD (0x27) y el BMP180 (0x77) conviven en el mismo
// bus I2C (SDA GPIO 21, SCL GPIO 22): distintas direcciones, sin conflicto.
//
// CONVENCIÓN DEL SENSOR DE LLUVIA (activo-bajo): la salida D0 vale BAJO cuando
// DETECTA agua y ALTO cuando está SECO (ver detalle en el nivel inicial).
//
// Plataforma: ESP32 (DevKit v1) — usa su WiFi integrado.
// Conexión: DHT11 → GPIO 4; lluvia D0 → GPIO 14; LCD y BMP180 al bus I2C
// (SDA GPIO 21, SCL GPIO 22).
//
// ADAPTACIÓN DE PLATAFORMA (ver README): LCD 1602 I2C (0x27; probá 0x3F si no
// muestra nada) en lugar del Keypad Shield paralelo del original.
// 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", "Adafruit BMP085 Library", "LiquidCrystal_I2C" y
// "Adafruit MQTT Library".
//
// Basado en: Fragmento de código del Nivel Avanzado (Paso 4) — Estación
// Meteorológica Automatizada, 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 en cada medición (cada
// 10 s); acá se publica solo cuando un valor cambia de forma apreciable
// (temperatura ≥ 0,5 °C; humedad ≥ 1 %; presión ≥ 1 hPa; lluvia ante cualquier
// cambio de estado), para no gastar el cupo gratuito de Adafruit IO (30
// publicaciones/minuto). (3) Se ignora una lectura fallida del DHT11 (isnan).
// (4) LCD adaptado a I2C.

#include <WiFi.h>
#include <Wire.h>
#include <Adafruit_BMP085.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

// Salida digital D0 del sensor de lluvia.
const int PIN_LLUVIA = 14;

// Nivel de la salida del sensor cuando HAY lluvia (activo-bajo).
const int LLUVIA_DETECTADA_NIVEL = LOW;

// 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;

// 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;
const float UMBRAL_PUBLICACION_PRESION  = 1.0; // hPa

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

// Objetos de sensores, display, WiFi y MQTT.
DHT dht(PIN_DHT, DHTTYPE);
Adafruit_BMP085 bmp;
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");
Adafruit_MQTT_Publish feedPresion(&mqtt, IO_USERNAME "/feeds/presion");
Adafruit_MQTT_Publish feedLluvia(&mqtt, IO_USERNAME "/feeds/lluvia");

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

// Últimos valores publicados (arrancan imposibles para forzar la 1ª publicación).
float temperaturaPublicada = -1000.0;
float humedadPublicada     = -1000.0;
float presionPublicada     = -1000.0;
int   lluviaPublicada      = -1;
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 los sensores: 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, float presion, int lluvia) {
  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;
  }

  if (diferenciaAbsoluta(presion, presionPublicada) >= UMBRAL_PUBLICACION_PRESION
      && feedPresion.publish(presion)) {
    presionPublicada = presion;
  }

  if (lluvia != lluviaPublicada && feedLluvia.publish((int32_t)lluvia)) {
    lluviaPublicada = lluvia;
  }
}

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

  pinMode(PIN_LLUVIA, INPUT);
  dht.begin();
  bmp.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 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 %
    float presion     = bmp.readPressure() / 100.0; // Pa → hPa
    // 1 si la salida del sensor está en el nivel de "agua detectada", si no 0.
    int lluvia = (digitalRead(PIN_LLUVIA) == LLUVIA_DETECTADA_NIVEL) ? 1 : 0;

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

      // Fila 0: temperatura (col 0) y humedad (col 8). Sin tildes ni Ñ.
      lcd.setCursor(0, 0);
      lcd.print("T:");
      lcd.print(temperatura);
      lcd.setCursor(8, 0);
      lcd.print("H:");
      lcd.print(humedad);

      // Fila 1: estado de la lluvia (col 0) y presión (col 10).
      lcd.setCursor(0, 1);
      if (lluvia == 1) {
        lcd.print("Lluvia:SI");
      } else {
        lcd.print("Lluvia:NO");
      }
      lcd.setCursor(10, 1);
      lcd.print("P:");
      lcd.print(presion);

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

  // --- Mantenimiento de la conexión IoT, en cada vuelta ---
  mantenerConexionMqtt();
}
La estación meteorológica IoT completa: control no bloqueante + WiFi del ESP32 + publicación de la temperatura, la humedad, la presión y la lluvia en Adafruit IO, solo cuando cambian.

Así queda funcionando el tablero que permite monitorear la estación a distancia. Podés ver el sistema completo —los sensores midiendo, 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 estación 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, la humedad, la presión y la lluvia de tu estación. Con esto queda completo el recorrido del proyecto. Si el grupo quiere seguir, en la ficha del proyecto están las extensiones sugeridas: sumar una veleta, un anemómetro o un sensor de luz para medir aún más variables del clima.

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 la estación puede atender internet mientras sigue midiendo.
  • 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, la humedad, la presión y el estado de la lluvia publicados por el dispositivo (variante ESP32).