Manual del proyecto · Proyecto 02
Sistema de Tanques
Diseño y construcción de una maqueta/prototipo de un sistema de abastecimiento de agua domiciliario, con control automático de llenado por sensor ultrasónico, electroválvula y bomba sumergible.
- Dificultad:
- Nivel educativo:
- Secundario. Ciclo Básico.
Contenido del manual
- 01 Materiales y equipamiento
- 02 Controlar el llenado de un tanque Nivel inicial
- 03 Un segundo tanque con bomba sumergible Nivel intermedio
- 04 Monitorear los tanques con Internet de las Cosas Nivel avanzado
Materiales y equipamiento
Insumos
| Imagen | Insumo | Cantidad | Plataforma |
|---|---|---|---|
| | Módulo relé de 5 V Uno para la electroválvula (nivel inicial) y otro para la bomba (nivel intermedio). Con ESP32, verificá que el módulo se dispare con señal de 3,3 V (los módulos con optoacoplador suelen hacerlo) o alimentalo con 5 V desde el pin VIN. | 2 | Todas |
| | Sensor de distancia ultrasónico (HC-SR04) Uno por tanque (el segundo se usa desde el nivel intermedio). Con ESP32, el pin Echo entrega 5 V: interponé un divisor de dos resistencias hacia el GPIO para no dañar la placa. | 2 | Todas |
| Electroválvula de baja tensión (12 V) La ficha técnica del original omitía la electroválvula, aunque el desarrollo la usa desde el nivel inicial (inconsistencia documentada en los desvíos). El original además la proponía de 220 V, tipo lavarropas; por seguridad la práctica usa una de 12 V. | 1 | Todas | |
| Bomba de agua sumergible de baja tensión (5 V o 12 V) Se usa desde el nivel intermedio. Debe trabajar siempre sumergida: funcionar en seco la arruina. La bomba de 220 V del original queda como extensión exclusiva del docente. | 1 | Todas | |
| | Fuente de 12 V 1 A Alimenta la electroválvula (y la bomba, si es de 12 V) a través de los bornes de los relés. | 1 | Todas |
| | 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 |
| | 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). | 1 | ESP32 DevKit |
| | Cable USB tipo B | 1 | Arduino UNO R3 |
| | Cable micro USB (con línea de datos) | 1 | ESP32 DevKit |
| | Fuente de 9 V 1 A (plug centro positivo, 5,5 x 2,1 mm) (opcional) Para alimentar la maqueta sin la computadora. El ESP32 se alimenta directamente por USB con una fuente de 5 V. | 1 | Arduino UNO R3 |
| | Cable dupont macho-hembra | 20 | Todas |
| Cables eléctricos Para el cableado de la electroválvula y la bomba hacia los bornes de los relés y la fuente de 12 V. | según instalación | Todas | |
| Recipientes para agua con canillas de desagote Hacen de tanques de la maqueta. Con tapa, y mejor si son translúcidos: se puede ver el nivel del agua desde afuera. | 2 | Todas | |
| Mangueras | según maqueta | Todas |
Equipamiento del taller
- Computadora
- Soldador
- Estaño
- Alicate
- Pinza de punta
- Pinza brusela
Plano de conexionado en protoboard
Deslizá horizontalmente para ver el plano completo
Deslizá horizontalmente para ver el plano completo
| Desde | Hacia | Color de cable |
|---|---|---|
| Arduino UNO · 5V | Bus + de la protoboard | rojo |
| Arduino UNO · GND | Bus − de la protoboard | negro |
| HC-SR04 · Cisterna · VCC | Bus + de la protoboard | rojo |
| HC-SR04 · Cisterna · GND | Bus − de la protoboard | negro |
| HC-SR04 · Cisterna · Trig | Arduino UNO · 8 | amarillo |
| HC-SR04 · Cisterna · Echo | Arduino UNO · 9 | azul |
| HC-SR04 · Tanque elevado · VCC | Bus + de la protoboard | rojo |
| HC-SR04 · Tanque elevado · GND | Bus − de la protoboard | negro |
| HC-SR04 · Tanque elevado · Trig | Arduino UNO · 10 | amarillo |
| HC-SR04 · Tanque elevado · Echo | Arduino UNO · 11 | azul |
| Relé válvula · VCC | Bus + de la protoboard | rojo |
| Relé válvula · GND | Bus − de la protoboard | negro |
| Relé válvula · IN | Arduino UNO · 4 | verde |
| Fuente 12 V (+) | Relé válvula · COM | naranja |
| Relé válvula · NO | Electroválvula · borne 1 | naranja |
| Electroválvula · borne 2 | Fuente 12 V (−) | naranja |
| Relé bomba · VCC | Bus + de la protoboard | rojo |
| Relé bomba · GND | Bus − de la protoboard | negro |
| Relé bomba · IN | Arduino UNO · 5 | verde |
| Fuente 12 V (+) | Relé bomba · COM | naranja |
| Relé bomba · NO | Bomba sumergible · borne 1 | naranja |
| Bomba sumergible · borne 2 | Fuente 12 V (−) | naranja |
| Desde | Hacia | Color de cable |
|---|---|---|
| ESP32 DevKit v1 · 3V3 | Bus + de la protoboard | rojo |
| ESP32 DevKit v1 · GND | Bus − de la protoboard | negro |
| HC-SR04 · Cisterna · VCC | Bus + de la protoboard | rojo |
| HC-SR04 · Cisterna · GND | Bus − de la protoboard | negro |
| HC-SR04 · Cisterna · Trig | ESP32 DevKit v1 · GPIO5 | amarillo |
| HC-SR04 · Cisterna · Echo | ESP32 DevKit v1 · GPIO18 | azul |
| HC-SR04 · Tanque elevado · VCC | Bus + de la protoboard | rojo |
| HC-SR04 · Tanque elevado · GND | Bus − de la protoboard | negro |
| HC-SR04 · Tanque elevado · Trig | ESP32 DevKit v1 · GPIO19 | amarillo |
| HC-SR04 · Tanque elevado · Echo | ESP32 DevKit v1 · GPIO21 | azul |
| Relé válvula · VCC | Bus + de la protoboard | rojo |
| Relé válvula · GND | Bus − de la protoboard | negro |
| Relé válvula · IN | ESP32 DevKit v1 · GPIO25 | verde |
| Fuente 12 V (+) | Relé válvula · COM | naranja |
| Relé válvula · NO | Electroválvula · borne 1 | naranja |
| Electroválvula · borne 2 | Fuente 12 V (−) | naranja |
| Relé bomba · VCC | Bus + de la protoboard | rojo |
| Relé bomba · GND | Bus − de la protoboard | negro |
| Relé bomba · IN | ESP32 DevKit v1 · GPIO26 | verde |
| Fuente 12 V (+) | Relé bomba · COM | naranja |
| Relé bomba · NO | Bomba sumergible · borne 1 | naranja |
| Bomba sumergible · borne 2 | Fuente 12 V (−) | naranja |
Controlar el llenado de un tanque
Preparar el tanque
Para comenzar, es necesario conseguir un recipiente que cumpla la función del tanque. Este debe tener una tapa, un agujero en la parte superior para el ingreso del agua, y otro en la parte inferior donde se colocará la llave de paso que controlará la distribución del agua.
Instalar el sensor ultrasónico y conectar la electroválvula
En este proyecto la ubicación de los distintos elementos en el prototipo es fundamental para el buen funcionamiento del mismo. Por lo tanto, organizamos la disposición de todos los componentes antes de realizar la conexión a la placa de control. Esta placa puede recibir datos de un sensor, así como enviar datos a un actuador, por medio de sus pines analógicos y digitales.
Para controlar el ingreso de agua y evitar que el tanque rebalse, colocamos en su entrada una electroválvulaElectroválvulaActuador que permite o interrumpe el paso de un fluido. Se acciona a través de su propia alimentación, por lo que tiene únicamente dos pines: sus estados posibles son activada o no, y se puede controlar mediante una salida digital y un relé. (válvula controlada electrónicamente, del tipo de las que suelen encontrarse en los lavarropas). Esta se abrirá cuando queramos llenar el tanque y se cerrará cuando el agua haya alcanzado un nivel determinado por nosotros. El original de 2019 usaba una válvula de 220 V; en esta reedición usamos una de 12 V, que enseña exactamente lo mismo sin meter tensión de red en la maqueta (el desvío está documentado en la ficha del proyecto).
Sujetamos el sensor ultrasónicoSensor de distancia ultrasónicoSensor que mide distancias emitiendo un pulso de sonido ultrasónico y midiendo el tiempo hasta que el eco regresa tras rebotar en un obstáculo. Tiene cuatro pines: VCC y GND para la alimentación, Trigger para disparar el pulso y Echo para detectar su llegada. en la parte interior de la tapa del tanque. Es importante que este apunte de manera perpendicular al fondo del tanque, para que las paredes no interfieran en la medición. Además, hay que asegurarnos de que el sensor no se moje cuando ingresa el agua. El sensor medirá la distancia a la que se encuentra el agua y, en función de eso, se activará o no la electroválvula.
Fijate en el esquema la lógica de la medición: el sensor mira al agua desde arriba, así que a menor distancia medida, más lleno está el tanque. Esa inversión va a aparecer todo el tiempo en el código y conviene tenerla clara desde ahora.
Verificá
Con el tanque armado: el sensor quedó firme bajo la tapa, apuntando perpendicular al fondo (no inclinado hacia una pared), fuera del chorro de entrada de agua; y la electroválvula quedó instalada en la entrada, con sus mangueras ajustadas. Todavía no hay nada conectado a la placa: eso llega en el paso 4.
Establecer la altura de corte
Para evitar que el tanque rebalse, el agua no puede superar la altura de ingreso. Por lo tanto, debemos detener la entrada de agua antes de que se alcance dicho nivel. Para asegurarnos de que eso suceda, es recomendable que el tanque se deje de llenar cuando el agua esté a unos 5 o 10 cm del ingreso. Una vez determinado este nivel, hay que medir la distancia desde este punto hasta la tapa donde se encuentra el sensor, y establecer este valor como la distancia mínima de desactivación de la válvula.
Ese número es la altura de corte de tu maqueta y es el que vas a cargar en la constante
DISTANCIA_MINIMA_CM del programa del paso 5. La guía original usa 40 cm como ejemplo para
un tanque real; en un recipiente de aula el valor va a ser mucho menor, y por eso se mide,
no se copia.
El margen de seguridad
Discutan en el grupo: ¿por qué la guía recomienda cortar 5 a 10 cm antes del ingreso y no justo en el borde? Piensen qué pasa con el agua que sigue entrando mientras la válvula termina de cerrarse, y qué pasaría si una medición del sensor viniera con error. Anoten sus hipótesis: el programa del paso 5 tiene una constante pensada exactamente para uno de esos problemas.
Conectar sensor, válvula y relé a la placa
Una vez que tengamos ubicados los distintos elementos del sistema, conectaremos el sensor ultrasónico, la electroválvula y el reléReléDispositivo electromagnético que funciona como un interruptor controlado por un circuito eléctrico: un electroimán abre o cierra contactos de otros circuitos independientes. Permite controlar con bajo voltaje dispositivos que utilizan mayor voltaje. a la placa de control, como indica la figura.
¡Atención! Agua y electricidad no se llevan bien. Todas las conexiones eléctricas van fuera del tanque y por encima del nivel del agua, los cables entran al tanque solo donde es imprescindible (el sensor, bajo la tapa), y siempre que manipules el circuito hacelo con las manos secas y con la maqueta desenchufada. El original trabajaba directamente con 220 V; en esta reedición la válvula es de 12 V, pero los hábitos de seguridad se practican igual, porque son los que te protegen cuando la instalación es real.
La conexión tiene dos partes:
- El circuito de potencia (12 V): la fuente de 12 V, el relé y la electroválvula. Conectamos un cable de la fuente de 12 V a la bornera COM del relé, un segundo cable desde la bornera NO del relé hasta un borne de la electroválvula, y un tercer cable que vuelve del otro borne de la electroválvula al otro polo de la fuente. Así, la válvula solo recibe energía cuando el relé cierra el contacto.
- El circuito de control (5 V): el sensor y la señal del relé hacia la placa, con cables dupont macho-hembra.
En el Arduino UNO, seguimos el esquema original: conectamos los pines Vcc, GND,
Trig y Echo del sensor a los pines 5V, GND, 8 y 9 de la placa,
respectivamente (en el código, los pines 8 y 9 son las constantes PIN_TRIG1 y
PIN_ECHO1). Del módulo relé, el pin + va a 5V, el pin - a GND y el pin
S al pin digital 4 (la constante PIN_RELE_VALVULA).
En el ESP32 el armado es el mismo, pero los pines se llaman GPIO y usamos otros números
(los GPIO 0, 2, 12 y 15 se reservan porque intervienen en el arranque de la placa). Los
números exactos están en las constantes PIN_TRIG1, PIN_ECHO1 y PIN_RELE_VALVULA al
comienzo del código del paso siguiente: conectá cada cable al GPIO que indica su constante.
Dos cuidados propios de esta placa: el pin Echo del HC-SR04 entrega 5 V, así que
interponé un divisor de dos resistencias antes del GPIO; y alimentá el módulo relé desde el
pin VIN (5 V del USB) si no conmuta con la señal de 3,3 V.
Para el aula
Este es el paso donde el docente marca la cancha en seguridad, aunque la válvula sea de 12 V: conexiones lejos y por encima del agua, manos secas para tocar el circuito, y la maqueta se modifica siempre desenchufada. Si más adelante se quisiera mostrar la versión de 220 V del original (válvula de lavarropas real), esa instalación es EXCLUSIVA del docente, supervisada y sin participación de estudiantes, tal como quedó registrado en los desvíos de la ficha. Vale también repetir la advertencia del original: las conexiones de potencia del relé nunca van sobre una protoboard.
Verificá
Antes de energizar, repasá cable por cable contra el esquema: los cuatro pines del sensor (Vcc, GND, Trig, Echo), los tres del relé (+, -, S) y el lazo de potencia fuente → COM → NO → válvula → fuente. La válvula todavía no va a moverse: falta el programa.
Programar el funcionamiento del dispositivo
La programación la realizaremos directamente en el lenguaje de Arduino (C++), usando 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 inicializarse
el dispositivo, y loop(), que se ejecuta repetidamente después. Los comentarios (todo lo
que sigue a //) no tienen efecto en el programa: están para que el código sea más fácil
de interpretar.
La lógica que queremos programar es la que definimos en los pasos anteriores: medir la distancia entre el sensor y la superficie del agua y, mientras esa distancia sea mayor que la altura de corte, mantener abierta la válvula para que se cargue el tanque; cuando el agua llega a la altura de corte, cerrarla.
Para medir, el programa dispara desde el pin Trig un pulso de sonido ultrasónico y mide
en el pin Echo cuánto tarda en volver el rebote: como el sonido recorre ida y vuelta
unos 58 microsegundos por cada centímetro, dividiendo el tiempo por ese valor se obtiene la
distancia. Esa cuenta vive en la función medirDistanciaCm() del código, comentada línea
por línea.
Ver el código completo · control-llenado.ino
// Control de llenado de un tanque: el sensor ultrasónico HC-SR04 mide la
// distancia entre la tapa y la superficie del agua; cuando el agua llega
// a la altura de corte, un relé cierra la electroválvula de entrada.
// Menor distancia = tanque más lleno: el sensor mira al agua desde arriba.
//
// SEGURIDAD: agua y electricidad no se llevan bien. Todas las conexiones
// eléctricas van fuera y POR ENCIMA del nivel del agua, y el sensor no se
// debe mojar. La práctica se realiza con una electroválvula de baja
// tensión (12 V); la conexión a 220 V de la guía original queda reservada
// al docente o a una instalación supervisada, y nunca sobre la protoboard
// (desvío pedagógico acordado).
//
// Plataforma: Arduino UNO.
// Conexión (la del esquema original): TRIG del sensor → pin 8, ECHO → pin 9,
// entrada "S" del módulo relé de la válvula → pin 4.
//
// Basado en: Fragmento de código 1 — Sistema de Tanques,
// Saberes Digitales (INET/EDUCAR 2019)
// Modernización: (1) se reemplazó el código generado por mBlock3 (_delay,
// _loop, pinMode dentro de la función de medición) por C++ idiomático con
// constantes nombradas y pinMode en setup(). (2) Se agregó una histéresis
// de 5 cm: el original abría y cerraba la válvula en el mismo valor exacto
// (40 cm), y el ruido de medición del HC-SR04 (±1 cm típico) haría
// traquetear el relé justo en el borde.
// Sensor ultrasónico del tanque (TRIG dispara el pulso, ECHO lo recibe).
const int PIN_TRIG1 = 8;
const int PIN_ECHO1 = 9;
// Relé que controla la electroválvula de entrada de agua.
const int PIN_RELE_VALVULA = 4;
// 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 RELE_ENCENDIDO = HIGH; // válvula abierta: entra agua
const int RELE_APAGADO = LOW; // válvula cerrada
// Altura de corte del original: si el agua queda a menos de 40 cm del
// sensor, el tanque está lleno. Este valor NO se inventa: sale de medir
// en tu maqueta la distancia entre el sensor y el nivel máximo deseado
// (5 a 10 cm por debajo del ingreso de agua, dice la guía).
const float DISTANCIA_MINIMA_CM = 40.0;
// Colchón de la histéresis: la válvula cierra a 40 cm pero recién vuelve
// a abrir cuando el agua baja más allá de 45 cm. Sin este margen, una
// medición que oscile entre 39.5 y 40.5 abriría y cerraría el relé sin
// parar (modernización, ver encabezado).
const float MARGEN_HISTERESIS_CM = 5.0;
// El original medía una vez por segundo (_delay(1)).
const unsigned long INTERVALO_MEDICION_MS = 1000;
// El eco tarda 58 microsegundos por cada centímetro de distancia (ida y
// vuelta del sonido a ~343 m/s). El timeout evita quedar esperando para
// siempre un eco que no llega.
const float MICROSEGUNDOS_POR_CM = 58.0;
const unsigned long TIMEOUT_ECO_US = 30000;
// Mide la distancia en cm entre el sensor y la superficie del agua.
// Si en 30 ms no llega el eco (sensor desconectado o fuera de rango),
// pulseIn devuelve 0 y la distancia da 0: el programa lo interpreta como
// "tanque lleno" y cierra la válvula. Es una falla SEGURA: ante la duda,
// que no entre agua.
float medirDistanciaCm(int pinTrig, int pinEcho) {
digitalWrite(pinTrig, LOW);
delayMicroseconds(2);
digitalWrite(pinTrig, HIGH); // pulso de disparo de 10 microsegundos
delayMicroseconds(10);
digitalWrite(pinTrig, LOW);
return pulseIn(pinEcho, HIGH, TIMEOUT_ECO_US) / MICROSEGUNDOS_POR_CM;
}
void setup() {
pinMode(PIN_TRIG1, OUTPUT);
pinMode(PIN_ECHO1, INPUT);
pinMode(PIN_RELE_VALVULA, OUTPUT);
// Arrancamos con la válvula cerrada hasta la primera medición:
// el estado seguro de un tanque es "no entra agua".
digitalWrite(PIN_RELE_VALVULA, RELE_APAGADO);
}
void loop() {
float distancia = medirDistanciaCm(PIN_TRIG1, PIN_ECHO1);
if (distancia < DISTANCIA_MINIMA_CM) {
// El agua llegó a la altura de corte: cerrar la válvula.
digitalWrite(PIN_RELE_VALVULA, RELE_APAGADO);
} else if (distancia > DISTANCIA_MINIMA_CM + MARGEN_HISTERESIS_CM) {
// Hay lugar de sobra: abrir la válvula para cargar el tanque.
digitalWrite(PIN_RELE_VALVULA, RELE_ENCENDIDO);
}
// Entre 40 y 45 cm no escribimos nada: el relé conserva su último
// estado. Esa franja "muerta" es la histéresis que evita el traqueteo.
delay(INTERVALO_MEDICION_MS);
} // Control de llenado de un tanque: el sensor ultrasónico HC-SR04 mide la
// distancia entre la tapa y la superficie del agua; cuando el agua llega
// a la altura de corte, un relé cierra la electroválvula de entrada.
// Menor distancia = tanque más lleno: el sensor mira al agua desde arriba.
//
// SEGURIDAD: agua y electricidad no se llevan bien. Todas las conexiones
// eléctricas van fuera y POR ENCIMA del nivel del agua, y el sensor no se
// debe mojar. La práctica se realiza con una electroválvula de baja
// tensión (12 V); la conexión a 220 V de la guía original queda reservada
// al docente o a una instalación supervisada, y nunca sobre la protoboard
// (desvío pedagógico acordado).
//
// Plataforma: ESP32 (DevKit v1).
// Conexión: TRIG → GPIO 5, ECHO → GPIO 18 (¡leer nota!), relé de la
// válvula → GPIO 25.
// NOTA ELÉCTRICA (¡importante!): el ESP32 trabaja a 3,3 V. El HC-SR04
// clásico se alimenta con 5 V y su pin ECHO devuelve el eco a 5 V: eso
// dañaría el GPIO. Es OBLIGATORIO un divisor de tensión en ECHO
// (ECHO —1 kΩ— GPIO 18 —2 kΩ— GND), que baja los 5 V a ~3,3 V. Es la
// misma idea del divisor del LDR: dos resistencias que reparten la
// tensión. TRIG no necesita nada: los 3,3 V del GPIO alcanzan para
// dispararlo. Alternativa sin divisor: el modelo HC-SR04P, que puede
// alimentarse directamente con 3,3 V.
//
// Basado en: Fragmento de código 1 — Sistema de Tanques,
// Saberes Digitales (INET/EDUCAR 2019)
// Modernización: (1) se reemplazó el código generado por mBlock3 (_delay,
// _loop, pinMode dentro de la función de medición) por C++ idiomático con
// constantes nombradas y pinMode en setup(); se portó a ESP32 (el original
// usaba los pines 8/9/4 del UNO). (2) Se agregó una histéresis de 5 cm:
// el original abría y cerraba la válvula en el mismo valor exacto (40 cm),
// y el ruido de medición del HC-SR04 (±1 cm típico) haría traquetear el
// relé justo en el borde.
// Sensor ultrasónico del tanque (TRIG dispara el pulso, ECHO lo recibe).
const int PIN_TRIG1 = 5;
const int PIN_ECHO1 = 18; // con divisor de tensión 1 kΩ / 2 kΩ (ver nota)
// Relé que controla la electroválvula de entrada de agua.
const int PIN_RELE_VALVULA = 25;
// 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 RELE_ENCENDIDO = HIGH; // válvula abierta: entra agua
const int RELE_APAGADO = LOW; // válvula cerrada
// Altura de corte del original: si el agua queda a menos de 40 cm del
// sensor, el tanque está lleno. Este valor NO se inventa: sale de medir
// en tu maqueta la distancia entre el sensor y el nivel máximo deseado
// (5 a 10 cm por debajo del ingreso de agua, dice la guía).
const float DISTANCIA_MINIMA_CM = 40.0;
// Colchón de la histéresis: la válvula cierra a 40 cm pero recién vuelve
// a abrir cuando el agua baja más allá de 45 cm. Sin este margen, una
// medición que oscile entre 39.5 y 40.5 abriría y cerraría el relé sin
// parar (modernización, ver encabezado).
const float MARGEN_HISTERESIS_CM = 5.0;
// El original medía una vez por segundo (_delay(1)).
const unsigned long INTERVALO_MEDICION_MS = 1000;
// El eco tarda 58 microsegundos por cada centímetro de distancia (ida y
// vuelta del sonido a ~343 m/s). El timeout evita quedar esperando para
// siempre un eco que no llega.
const float MICROSEGUNDOS_POR_CM = 58.0;
const unsigned long TIMEOUT_ECO_US = 30000;
// Mide la distancia en cm entre el sensor y la superficie del agua.
// Si en 30 ms no llega el eco (sensor desconectado o fuera de rango),
// pulseIn devuelve 0 y la distancia da 0: el programa lo interpreta como
// "tanque lleno" y cierra la válvula. Es una falla SEGURA: ante la duda,
// que no entre agua.
float medirDistanciaCm(int pinTrig, int pinEcho) {
digitalWrite(pinTrig, LOW);
delayMicroseconds(2);
digitalWrite(pinTrig, HIGH); // pulso de disparo de 10 microsegundos
delayMicroseconds(10);
digitalWrite(pinTrig, LOW);
return pulseIn(pinEcho, HIGH, TIMEOUT_ECO_US) / MICROSEGUNDOS_POR_CM;
}
void setup() {
pinMode(PIN_TRIG1, OUTPUT);
pinMode(PIN_ECHO1, INPUT);
pinMode(PIN_RELE_VALVULA, OUTPUT);
// Arrancamos con la válvula cerrada hasta la primera medición:
// el estado seguro de un tanque es "no entra agua".
digitalWrite(PIN_RELE_VALVULA, RELE_APAGADO);
}
void loop() {
float distancia = medirDistanciaCm(PIN_TRIG1, PIN_ECHO1);
if (distancia < DISTANCIA_MINIMA_CM) {
// El agua llegó a la altura de corte: cerrar la válvula.
digitalWrite(PIN_RELE_VALVULA, RELE_APAGADO);
} else if (distancia > DISTANCIA_MINIMA_CM + MARGEN_HISTERESIS_CM) {
// Hay lugar de sobra: abrir la válvula para cargar el tanque.
digitalWrite(PIN_RELE_VALVULA, RELE_ENCENDIDO);
}
// Entre 40 y 45 cm no escribimos nada: el relé conserva su último
// estado. Esa franja "muerta" es la histéresis que evita el traqueteo.
delay(INTERVALO_MEDICION_MS);
} Antes de cargarlo, acordate de reemplazar el valor de DISTANCIA_MINIMA_CM por la altura
de corte que mediste en el paso 3.
Hay un detalle del código que la guía original no tenía: la constante
MARGEN_HISTERESIS_CM. El original abría y cerraba la válvula comparando contra el mismo
valor exacto (40 cm), pero las mediciones del sensor tienen un pequeño ruido (±1 cm es
típico): justo en el borde, el relé quedaría abriendo y cerrando sin parar. Con el margen,
la válvula corta al llegar a la altura de corte y recién reabre cuando el agua bajó 5 cm
más: esa franja “muerta” es la histéresis, y es la respuesta al desafío del paso 3.
Podés ver el comportamiento completo —el tanque llenándose, el sensor midiendo y la válvula cortando en la altura de corte— en el simulador, antes de tocar el agua 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 el aula
El material original de 2019 enseñaba este paso con bloques en mBlock3, hoy discontinuado. La función didáctica de los bloques —bajar la barrera de entrada— la cumple acá la combinación de un código corto con constantes nombradas, comentado línea por línea, y el simulador en el navegador. Sugerencia de lectura en el aula: taparle el “porqué” a cada línea y pedir a los grupos que lo reconstruyan; la estructura si/si no del control de la válvula es una oportunidad ideal para el primer condicional serio de la colección.
Verificá
En el Arduino IDE, apretá el botón de verificar (el tilde ✓). Si abajo aparece “Compilación completada” sin mensajes en rojo, tu programa está bien escrito. Si aparece un error, revisá los punto y coma al final de cada línea y que las llaves abran y cierren.
Subir el código a la placa
Para subir el código de nuestro programa a la placa, necesitamos:
- Conectar la placa a la computadora con el cable USB.
- Chequear que en el selector de placa del Arduino IDE esté seleccionada la placa correcta.
- Seleccionar el puertoPuerto COMPuerto de comunicaciones por el cual el sistema operativo se comunica con un dispositivo externo, como la placa. Se asigna automáticamente al conectarla por USB y puede cambiar de número; en el IDE hay que indicar cuál usar. al que está conectada la placa.
- Cliquear el botón Subir (la flecha →) en la parte superior de la interfaz.
En el Arduino IDE 2.x, la placa y el puerto se eligen juntos en el menú desplegable de la barra superior: al conectar la placa suele aparecer directamente como opción. Si no aparece, entrá a “Select other board and port” y buscala por nombre.
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.
Al terminar de subir nuestro código, veremos el mensaje de carga completada en la barra inferior del IDE.
Verificá
Podés probar el sistema en seco, sin agua: con el tanque vacío (distancia grande), el LED del módulo relé tiene que indicar la válvula abierta. Acercale al sensor una superficie plana (un cuaderno) a menos de la altura de corte: el relé tiene que conmutar con un clic y cerrar la válvula; alejala más allá del margen de histéresis y tiene que reabrir. Recién después probá con agua de verdad: la válvula tiene que cortar cuando el nivel llega a la altura de corte que mediste.
Cierre del nivel
Al terminar este nivel, el tanque de Camilo se llena solo y sin rebalsar: el sensor ultrasónico mide desde la tapa la distancia hasta el agua, y la placa cierra la electroválvula cuando se alcanza la altura de corte que vos mismo mediste, con el margen de histéresis evitando el traqueteo del relé. Además, ya sabés subir programas desde el Arduino IDE 2.x, una herramienta que te va a acompañar el resto del proyecto. En el nivel intermedio el sistema crece a la escala de un edificio: un segundo tanque, una bomba sumergible y una regla nueva —la bomba jamás trabaja en seco—.
Evidencia de cierre
Al terminar este nivel, esto tiene que estar funcionando:
- El tanque tiene el sensor ultrasónico fijado bajo la tapa, apuntando perpendicular al fondo y protegido del agua, y la electroválvula instalada en su entrada.
- Mediste la altura de corte de tu tanque y ese valor está cargado en la constante DISTANCIA_MINIMA_CM del programa.
- La placa cierra la válvula cuando el agua llega a la altura de corte y la reabre cuando el nivel baja más allá del margen de histéresis.
- 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.
Un segundo tanque con bomba sumergible
Situación problema
La prima de Camilo, Melina, quiere instalar un sistema como el de su primo, que también mida y regule el nivel del tanque de agua con un sensor ultrasónico. Ella vive en el mismo barrio que Camilo, pero no en una casa sino en un edificio de 6 pisos que tiene 12 departamentos, y que recibe agua desde la misma planta potabilizadora.
La presión y el caudal de agua entregados desde la planta a cada construcción dependen de la altura de la edificación, de la distancia a la que se encuentre de la planta, y del consumo que haya en la red. La presión suministrada por esta red es suficiente para abastecer el tanque de una casa como la de Camilo, pero no para llenar los tanques ubicados en los pisos superiores de los edificios más altos, como el de Melina.
En estos casos se utiliza un tanque extra llamado cisterna, que se ubica en el sótano. El agua de la red llega a este tanque, que luego envía el agua al tanque de arriba por medio de una bomba. El desafío en este caso será construir un sistema de tanques para el edificio de Melina.
En esta instancia del proyecto, habrá que agregar un segundo tanque con una bomba sumergible que reciba el agua y la envíe al tanque principal.
Preparar la instalación del segundo tanque
Para simular el sistema de tanque cisterna y elevado, tomaremos la maqueta realizada en la etapa anterior y agregaremos un tanque extra con otro sensor de distanciaSensor de distancia ultrasónicoSensor que mide distancias emitiendo un pulso de sonido ultrasónico y midiendo el tiempo hasta que el eco regresa tras rebotar en un obstáculo. Tiene cuatro pines: VCC y GND para la alimentación, Trigger para disparar el pulso y Echo para detectar su llegada. y una bomba sumergible. Los roles quedan así:
- El tanque del nivel inicial pasa a ser la cisterna: conserva su sensor y su electroválvulaElectroválvulaActuador que permite o interrumpe el paso de un fluido. Se acciona a través de su propia alimentación, por lo que tiene únicamente dos pines: sus estados posibles son activada o no, y se puede controlar mediante una salida digital y un relé. (el agua de la red sigue entrando por ahí) y recibe adentro la bomba sumergible.
- El tanque nuevo es el tanque elevado: lleva el segundo sensor bajo su tapa, montado igual que en el nivel inicial (perpendicular al fondo y protegido del agua), y recibe por su entrada la manguera que sube desde la bomba.
Las bombas sumergibles no pueden funcionar si no están por debajo del nivel del agua; por lo tanto, es muy importante asegurarnos de que el tanque cisterna sea lo suficientemente grande como para que el agua tape la bomba.
Además, programaremos la placa de modo que, cuando el nivel del agua sea inferior al de la bomba, no envíe agua al tanque elevado y espere hasta que se haya llenado lo suficiente para enviar el agua.
Realizar las conexiones a la placa
¡Atención! Agua y electricidad no se llevan bien. En este paso la regla del nivel inicial vale doble, porque ahora hay un cable que sí entra al agua: el de la bomba sumergible. Ese cable debe entrar al tanque por arriba, con su aislación sana en todo el recorrido; el resto de las conexiones queda fuera de los tanques y por encima del nivel del agua, y el circuito se manipula siempre con las manos secas y con la maqueta desenchufada. El original trabajaba con válvula y bomba de 220 V; en esta reedición ambas son de baja tensión (el desvío está documentado en la ficha), pero los hábitos se practican como si fueran de red.
Conectamos los componentes como indica la figura, agregando el sensor ultrasónico del segundo tanque y la bomba de agua con su reléReléDispositivo electromagnético que funciona como un interruptor controlado por un circuito eléctrico: un electroimán abre o cierra contactos de otros circuitos independientes. Permite controlar con bajo voltaje dispositivos que utilizan mayor voltaje.:
- Sensor del tanque elevado: cuatro cables dupont macho-hembra, de los pines Vcc, GND, Trig y Echo del sensor a la alimentación y a las dos entradas nuevas de la placa.
- Bomba con su relé: un cable de la fuente de baja tensión a la bornera COM del segundo relé, de la bornera NO a un cable de la bomba, y el otro cable de la bomba de vuelta al otro polo de la fuente (el mismo lazo que armaste para la válvula).
- Control del segundo relé: el pin + a 5V, el pin - a GND y el pin S a la salida digital nueva de la placa.
En el Arduino UNO seguimos el esquema original: los pines Trig y Echo del segundo
sensor van a los pines 10 y 11 (las constantes PIN_TRIG2 y PIN_ECHO2 del
código), y el pin S del relé de la bomba, al pin digital 5 (la constante
PIN_RELE_BOMBA). El sensor y el relé del nivel inicial no se tocan: siguen en 8, 9 y 4.
En el ESP32, el segundo sensor y el relé de la bomba van a GPIO de propósito general
seguros. Los números exactos están en las constantes PIN_TRIG2, PIN_ECHO2 y
PIN_RELE_BOMBA al comienzo del código del paso siguiente: conectá cada cable al GPIO que
indica su constante. Igual que en el nivel inicial, el pin Echo de este segundo sensor
también necesita su divisor de dos resistencias hacia el GPIO.
El armado de este circuito tiene como función conseguir que el sensor del tanque elevado envíe una señal a la placa para activar la bomba sumergible del tanque cisterna cuando el nivel de llenado sea inferior al máximo. Si en el tanque cisterna hay agua suficiente, se activará la bomba para enviar agua al tanque elevado. La activación se mantendrá hasta que se llene el tanque elevado o hasta que el nivel de agua del tanque cisterna sea inferior al mínimo.
Para el aula
Antes de dar por bueno el cableado, conviene hacer una ronda de seguridad por grupo con tres preguntas: ¿la bomba queda completamente tapada por agua antes de energizar?, ¿el cable de la bomba entra por arriba y su aislación está sana?, ¿alguna conexión eléctrica quedó a la altura o por debajo del nivel del agua? La bomba trabajando siempre bajo agua no es solo cuidado del motor (en seco se arruina en segundos): es también la ocasión de instalar el hábito de revisar la instalación completa antes de conectarla, con las manos secas. Y como advertía el original: las conexiones de potencia de los relés, nunca sobre una protoboard.
Verificá
Repasá contra el esquema los dos circuitos de control (cuatro cables por sensor, tres por relé) y los dos lazos de potencia (fuente → COM → NO → carga → fuente, uno para la válvula y otro para la bomba). Con el programa del nivel inicial todavía cargado, el primer sensor y la válvula tienen que seguir funcionando igual que antes: si algo dejó de andar, lo desarmaste al cablear lo nuevo.
Programar el funcionamiento del dispositivo
Editamos nuestro programa para que contemple este tanque extra, así como su llenado,
siempre que el primer tanque esté en condiciones de activar la bomba. La lógica completa
tiene ahora dos decisiones por vuelta de loop():
- La válvula (igual que en el nivel inicial, pero ahora cuida a la cisterna): si el agua de la cisterna llegó a su altura de corte, se cierra; si bajó más allá del margen de histéresis, se abre.
- La bomba (la decisión nueva, con doble condición): si el tanque elevado está lleno, la bomba se apaga. Si necesita agua, antes de encenderla el programa revisa la cisterna: cuando su nivel está demasiado bajo —la distancia medida supera la distancia máxima—, la bomba quedaría al descubierto y funcionar en seco la arruina, así que también se apaga. Solo si el elevado necesita agua y la cisterna tiene suficiente, la bomba se enciende.
La guía original usa como ejemplo 40 cm de altura de corte para ambos tanques y 140 cm de distancia máxima para la cisterna: igual que en el nivel inicial, esos valores no se copian, se miden en tu maqueta y se cargan en las constantes de distancia del comienzo del código.
Ver el código completo · dos-tanques.ino
// Sistema de dos tanques: cisterna (tanque 1) + tanque elevado (tanque 2).
// La electroválvula carga la cisterna desde la red; una bomba sumergible,
// ubicada dentro de la cisterna, sube el agua al tanque elevado.
// Cada tanque tiene su sensor ultrasónico mirando el agua desde arriba
// (menor distancia = tanque más lleno).
//
// Reglas (las del original):
// - Válvula: cierra cuando la cisterna está llena; abre cuando hay lugar.
// - Bomba: se apaga si el tanque elevado está lleno, O si la cisterna
// está tan vacía que la bomba quedaría fuera del agua (una bomba
// sumergible que trabaja en seco se arruina). Solo bombea si hay
// lugar arriba Y agua suficiente abajo.
//
// SEGURIDAD: agua y electricidad no se llevan bien. Todas las conexiones
// eléctricas van fuera y POR ENCIMA del nivel del agua, los sensores no
// se deben mojar y la bomba sumergible solo se enciende estando bajo el
// agua. La práctica se realiza con válvula y bomba de baja tensión (12 V);
// la conexión a 220 V de la guía original queda reservada al docente o a
// una instalación supervisada, y nunca sobre la protoboard (desvío
// pedagógico acordado).
//
// Plataforma: Arduino UNO.
// Conexión (la del esquema original): sensor cisterna TRIG 8 / ECHO 9,
// relé de la válvula → pin 4; sensor tanque elevado TRIG 10 / ECHO 11,
// relé de la bomba → pin 5.
//
// Basado en: Fragmento de código 2 — Sistema de Tanques,
// Saberes Digitales (INET/EDUCAR 2019)
// Modernización: (1) constantes nombradas y pinMode en setup() en lugar
// del C generado por mBlock3. (2) Histéresis de 5 cm en las tres
// comparaciones: el original conmutaba válvula y bomba en el valor exacto
// del umbral y el ruido del HC-SR04 haría traquetear los relés.
// Sensor del tanque 1 (cisterna) y relé de la electroválvula.
const int PIN_TRIG1 = 8;
const int PIN_ECHO1 = 9;
const int PIN_RELE_VALVULA = 4;
// Sensor del tanque 2 (elevado) y relé de la bomba sumergible.
const int PIN_TRIG2 = 10;
const int PIN_ECHO2 = 11;
const int PIN_RELE_BOMBA = 5;
// Módulos relé activo-alto, como en el original (HIGH = actuador andando).
// Si tu módulo es activo-bajo, invertí solo estas dos constantes.
const int RELE_ENCENDIDO = HIGH;
const int RELE_APAGADO = LOW;
// Distancias del ejemplo original (se ajustan midiendo en TU maqueta):
// - MINIMA1: agua a menos de 40 cm del sensor 1 = cisterna llena.
// - MAXIMA1: agua a más de 140 cm del sensor 1 = cisterna casi vacía,
// la bomba quedaría al aire.
// - MINIMA2: agua a menos de 40 cm del sensor 2 = tanque elevado lleno.
const float DISTANCIA_MINIMA1_CM = 40.0;
const float DISTANCIA_MAXIMA1_CM = 140.0;
const float DISTANCIA_MINIMA2_CM = 40.0;
// Colchón anti-traqueteo (modernización, ver encabezado): cada umbral
// tiene una franja de 5 cm en la que no se toca el relé.
const float MARGEN_HISTERESIS_CM = 5.0;
// El original medía una vez por segundo (_delay(1)).
const unsigned long INTERVALO_MEDICION_MS = 1000;
// 58 microsegundos de eco por cada cm de distancia; el timeout evita
// esperar para siempre un eco que no llega.
const float MICROSEGUNDOS_POR_CM = 58.0;
const unsigned long TIMEOUT_ECO_US = 30000;
// Mide la distancia en cm entre un sensor y la superficie del agua.
// Si no llega eco, pulseIn devuelve 0 y la distancia da 0: se interpreta
// como "tanque lleno" y todo se apaga. Falla segura.
float medirDistanciaCm(int pinTrig, int pinEcho) {
digitalWrite(pinTrig, LOW);
delayMicroseconds(2);
digitalWrite(pinTrig, HIGH); // pulso de disparo de 10 microsegundos
delayMicroseconds(10);
digitalWrite(pinTrig, LOW);
return pulseIn(pinEcho, HIGH, TIMEOUT_ECO_US) / MICROSEGUNDOS_POR_CM;
}
void setup() {
pinMode(PIN_TRIG1, OUTPUT);
pinMode(PIN_ECHO1, INPUT);
pinMode(PIN_TRIG2, OUTPUT);
pinMode(PIN_ECHO2, INPUT);
pinMode(PIN_RELE_VALVULA, OUTPUT);
pinMode(PIN_RELE_BOMBA, OUTPUT);
// Estado inicial seguro: válvula cerrada y bomba apagada hasta que
// las primeras mediciones digan otra cosa.
digitalWrite(PIN_RELE_VALVULA, RELE_APAGADO);
digitalWrite(PIN_RELE_BOMBA, RELE_APAGADO);
}
void loop() {
float distancia1 = medirDistanciaCm(PIN_TRIG1, PIN_ECHO1); // cisterna
float distancia2 = medirDistanciaCm(PIN_TRIG2, PIN_ECHO2); // elevado
// --- Válvula de la cisterna (misma lógica del nivel inicial) ---
if (distancia1 < DISTANCIA_MINIMA1_CM) {
// Cisterna llena: cerrar la válvula.
digitalWrite(PIN_RELE_VALVULA, RELE_APAGADO);
} else if (distancia1 > DISTANCIA_MINIMA1_CM + MARGEN_HISTERESIS_CM) {
// Hay lugar: abrir la válvula.
digitalWrite(PIN_RELE_VALVULA, RELE_ENCENDIDO);
}
// En la franja intermedia el relé conserva su último estado (histéresis).
// --- Bomba sumergible (el razonamiento escalonado del original) ---
if (distancia2 < DISTANCIA_MINIMA2_CM) {
// El tanque elevado está lleno: no hay que bombear más.
digitalWrite(PIN_RELE_BOMBA, RELE_APAGADO);
} else if (distancia1 > DISTANCIA_MAXIMA1_CM) {
// La cisterna está casi vacía: apagar la bomba ANTES de que quede
// fuera del agua. Esta condición protege al hardware y por eso se
// evalúa aunque arriba haga falta agua.
digitalWrite(PIN_RELE_BOMBA, RELE_APAGADO);
} else if (distancia2 > DISTANCIA_MINIMA2_CM + MARGEN_HISTERESIS_CM
&& distancia1 < DISTANCIA_MAXIMA1_CM - MARGEN_HISTERESIS_CM) {
// Hay lugar arriba Y agua suficiente abajo: bombear.
digitalWrite(PIN_RELE_BOMBA, RELE_ENCENDIDO);
}
// En las franjas intermedias la bomba conserva su estado (histéresis).
delay(INTERVALO_MEDICION_MS);
} // Sistema de dos tanques: cisterna (tanque 1) + tanque elevado (tanque 2).
// La electroválvula carga la cisterna desde la red; una bomba sumergible,
// ubicada dentro de la cisterna, sube el agua al tanque elevado.
// Cada tanque tiene su sensor ultrasónico mirando el agua desde arriba
// (menor distancia = tanque más lleno).
//
// Reglas (las del original):
// - Válvula: cierra cuando la cisterna está llena; abre cuando hay lugar.
// - Bomba: se apaga si el tanque elevado está lleno, O si la cisterna
// está tan vacía que la bomba quedaría fuera del agua (una bomba
// sumergible que trabaja en seco se arruina). Solo bombea si hay
// lugar arriba Y agua suficiente abajo.
//
// SEGURIDAD: agua y electricidad no se llevan bien. Todas las conexiones
// eléctricas van fuera y POR ENCIMA del nivel del agua, los sensores no
// se deben mojar y la bomba sumergible solo se enciende estando bajo el
// agua. La práctica se realiza con válvula y bomba de baja tensión (12 V);
// la conexión a 220 V de la guía original queda reservada al docente o a
// una instalación supervisada, y nunca sobre la protoboard (desvío
// pedagógico acordado).
//
// Plataforma: ESP32 (DevKit v1).
// Conexión: sensor cisterna TRIG GPIO 5 / ECHO GPIO 18, relé de la
// válvula → GPIO 25; sensor elevado TRIG GPIO 19 / ECHO GPIO 21, relé de
// la bomba → GPIO 26.
// NOTA ELÉCTRICA (¡importante!): el ESP32 trabaja a 3,3 V. El HC-SR04
// clásico se alimenta con 5 V y su pin ECHO devuelve el eco a 5 V: eso
// dañaría el GPIO. Es OBLIGATORIO un divisor de tensión en CADA ECHO
// (ECHO —1 kΩ— GPIO —2 kΩ— GND), que baja los 5 V a ~3,3 V. TRIG no
// necesita nada: los 3,3 V del GPIO alcanzan para dispararlo.
// Alternativa sin divisores: el modelo HC-SR04P, que puede alimentarse
// directamente con 3,3 V.
//
// Basado en: Fragmento de código 2 — Sistema de Tanques,
// Saberes Digitales (INET/EDUCAR 2019)
// Modernización: (1) constantes nombradas y pinMode en setup() en lugar
// del C generado por mBlock3; portado a ESP32 (el original usaba los
// pines 8/9/4 y 10/11/5 del UNO). (2) Histéresis de 5 cm en las tres
// comparaciones: el original conmutaba válvula y bomba en el valor exacto
// del umbral y el ruido del HC-SR04 haría traquetear los relés.
// Sensor del tanque 1 (cisterna) y relé de la electroválvula.
const int PIN_TRIG1 = 5;
const int PIN_ECHO1 = 18; // con divisor 1 kΩ / 2 kΩ (ver nota)
const int PIN_RELE_VALVULA = 25;
// Sensor del tanque 2 (elevado) y relé de la bomba sumergible.
const int PIN_TRIG2 = 19;
const int PIN_ECHO2 = 21; // con divisor 1 kΩ / 2 kΩ (ver nota)
const int PIN_RELE_BOMBA = 26;
// Módulos relé activo-alto, como en el original (HIGH = actuador andando).
// Si tu módulo es activo-bajo, invertí solo estas dos constantes.
const int RELE_ENCENDIDO = HIGH;
const int RELE_APAGADO = LOW;
// Distancias del ejemplo original (se ajustan midiendo en TU maqueta):
// - MINIMA1: agua a menos de 40 cm del sensor 1 = cisterna llena.
// - MAXIMA1: agua a más de 140 cm del sensor 1 = cisterna casi vacía,
// la bomba quedaría al aire.
// - MINIMA2: agua a menos de 40 cm del sensor 2 = tanque elevado lleno.
const float DISTANCIA_MINIMA1_CM = 40.0;
const float DISTANCIA_MAXIMA1_CM = 140.0;
const float DISTANCIA_MINIMA2_CM = 40.0;
// Colchón anti-traqueteo (modernización, ver encabezado): cada umbral
// tiene una franja de 5 cm en la que no se toca el relé.
const float MARGEN_HISTERESIS_CM = 5.0;
// El original medía una vez por segundo (_delay(1)).
const unsigned long INTERVALO_MEDICION_MS = 1000;
// 58 microsegundos de eco por cada cm de distancia; el timeout evita
// esperar para siempre un eco que no llega.
const float MICROSEGUNDOS_POR_CM = 58.0;
const unsigned long TIMEOUT_ECO_US = 30000;
// Mide la distancia en cm entre un sensor y la superficie del agua.
// Si no llega eco, pulseIn devuelve 0 y la distancia da 0: se interpreta
// como "tanque lleno" y todo se apaga. Falla segura.
float medirDistanciaCm(int pinTrig, int pinEcho) {
digitalWrite(pinTrig, LOW);
delayMicroseconds(2);
digitalWrite(pinTrig, HIGH); // pulso de disparo de 10 microsegundos
delayMicroseconds(10);
digitalWrite(pinTrig, LOW);
return pulseIn(pinEcho, HIGH, TIMEOUT_ECO_US) / MICROSEGUNDOS_POR_CM;
}
void setup() {
pinMode(PIN_TRIG1, OUTPUT);
pinMode(PIN_ECHO1, INPUT);
pinMode(PIN_TRIG2, OUTPUT);
pinMode(PIN_ECHO2, INPUT);
pinMode(PIN_RELE_VALVULA, OUTPUT);
pinMode(PIN_RELE_BOMBA, OUTPUT);
// Estado inicial seguro: válvula cerrada y bomba apagada hasta que
// las primeras mediciones digan otra cosa.
digitalWrite(PIN_RELE_VALVULA, RELE_APAGADO);
digitalWrite(PIN_RELE_BOMBA, RELE_APAGADO);
}
void loop() {
float distancia1 = medirDistanciaCm(PIN_TRIG1, PIN_ECHO1); // cisterna
float distancia2 = medirDistanciaCm(PIN_TRIG2, PIN_ECHO2); // elevado
// --- Válvula de la cisterna (misma lógica del nivel inicial) ---
if (distancia1 < DISTANCIA_MINIMA1_CM) {
// Cisterna llena: cerrar la válvula.
digitalWrite(PIN_RELE_VALVULA, RELE_APAGADO);
} else if (distancia1 > DISTANCIA_MINIMA1_CM + MARGEN_HISTERESIS_CM) {
// Hay lugar: abrir la válvula.
digitalWrite(PIN_RELE_VALVULA, RELE_ENCENDIDO);
}
// En la franja intermedia el relé conserva su último estado (histéresis).
// --- Bomba sumergible (el razonamiento escalonado del original) ---
if (distancia2 < DISTANCIA_MINIMA2_CM) {
// El tanque elevado está lleno: no hay que bombear más.
digitalWrite(PIN_RELE_BOMBA, RELE_APAGADO);
} else if (distancia1 > DISTANCIA_MAXIMA1_CM) {
// La cisterna está casi vacía: apagar la bomba ANTES de que quede
// fuera del agua. Esta condición protege al hardware y por eso se
// evalúa aunque arriba haga falta agua.
digitalWrite(PIN_RELE_BOMBA, RELE_APAGADO);
} else if (distancia2 > DISTANCIA_MINIMA2_CM + MARGEN_HISTERESIS_CM
&& distancia1 < DISTANCIA_MAXIMA1_CM - MARGEN_HISTERESIS_CM) {
// Hay lugar arriba Y agua suficiente abajo: bombear.
digitalWrite(PIN_RELE_BOMBA, RELE_ENCENDIDO);
}
// En las franjas intermedias la bomba conserva su estado (histéresis).
delay(INTERVALO_MEDICION_MS);
} Podés ver el sistema completo funcionando —la cisterna cargándose por la válvula, la bomba subiendo agua al tanque elevado y las protecciones actuando— 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.
Las cuatro combinaciones
Cada tanque puede estar “con lugar” o “lleno”, así que el sistema tiene cuatro combinaciones posibles. Armen en el pizarrón la tabla completa (cisterna llena/vacía × elevado lleno/con lugar) y decidan, para cada caso, qué deberían hacer la válvula y la bomba. Después contrasten su tabla con el código: ¿coincide en los cuatro casos? ¿Qué caso sería el más peligroso si el programa estuviera mal?
Verificá
Probalo primero en seco, acercando un cuaderno a cada sensor: tapando el sensor del elevado (distancia corta = “lleno”), la bomba tiene que apagarse; destapándolo con la cisterna “vacía” (los dos sensores lejos de todo), la bomba también tiene que quedar apagada. Con agua: la válvula corta cuando la cisterna llega a su altura de corte, la bomba sube agua solo mientras el elevado tiene lugar, y si la cisterna se vacía por debajo del nivel de la bomba, esta se detiene antes de quedar al descubierto.
Cierre del nivel
Al terminar este nivel, la maqueta reproduce el sistema de agua de un edificio real: la electroválvula carga la cisterna desde la red, la bomba sumergible sube el agua al tanque elevado cuando este lo necesita, y los dos sensores ultrasónicos vigilan los niveles. La regla de oro quedó escrita en el código: la bomba solo trabaja sumergida, gracias a la doble condición que podés señalar y explicar. En el nivel avanzado, los vecinos de Melina van a poder mirar todo esto desde el celular: vamos a publicar los niveles y los estados por Internet de las Cosas.
Evidencia de cierre
Al terminar este nivel, esto tiene que estar funcionando:
- La maqueta tiene los dos tanques conectados: la cisterna (con la electroválvula en su entrada y la bomba sumergible adentro) y el tanque elevado, cada uno con su sensor ultrasónico.
- La bomba solo se enciende cuando el tanque elevado necesita agua Y la cisterna tiene agua suficiente para mantenerla sumergida.
- Podés explicar por qué la bomba nunca debe funcionar en seco y señalar en el código la condición que lo evita.
Monitorear los tanques con Internet de las Cosas
Situación problema
Luego de haber terminado la instalación del tanque y la bomba, los vecinos de Melina le plantearon que sería muy útil poder tener acceso a la información que toma el sensor respecto al nivel del agua. Hoy en día, si quisieran saber cuánta agua hay en el tanque, deberían subir al techo, acceder al tanque y sacarle la tapa, lo cual resulta muy complicado.
Si pudieran registrar los datos que toma el sensor y monitorearlos desde sus casas, podrían saber cuánta agua hay disponible en el tanque para cuidar este recurso entre todo el consorcio.
En esta tercera instancia se programará el envío de los datos obtenidos a un dispositivo móvil a través de internet (IoT).
Introducción a Internet de las Cosas (IoT)
Internet de las Cosas (en inglés Internet of Things, abreviado IoTIoT (Internet de las Cosas)Interconexión digital de objetos cotidianos con internet. Permite, por ejemplo, enviar los datos de un sensor a una central y visualizarlos desde un dispositivo móvil. Requiere un dispositivo capaz de conectarse a internet y un servidor que reciba y aloje los datos.) es un concepto que refiere a la interconexión digital de objetos cotidianos con internet. Esta interconexión puede tener diversas funciones. Por ejemplo, puede utilizarse para monitorear la temperatura de un ambiente, enviando los datos obtenidos por un sensor a una central donde se recopile la información. De esta manera podría visualizarse en 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:
- Un dispositivo capaz de conectarse a internet.
- Un servidor que reciba y aloje los datos.
Existen diversas formas de lograr el cometido de registrar y almacenar los datos del sistema de tanques construido. En este caso, se detallará cómo hacerlo con el WiFi integrado de la placa ESP32ESP32Placa con un microcontrolador de Espressif que trae WiFi (y Bluetooth) integrados. Se programa desde el Arduino IDE igual que un Arduino, con pines llamados GPIO. En este proyecto reemplaza al conjunto Arduino UNO + módulo OBLOQ para el nivel IoT. y con los servidores de Adafruit IOAdafruit IOPlataforma online de uso gratuito, orientada al uso educativo, que centraliza y gestiona la información que reportan los dispositivos IoT. Permite crear feeds y paneles de control para monitorear los datos desde cualquier dispositivo..
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 (los pasos 2 y 3 funcionan 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 los tanques y manejando la válvula y la bomba, 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
En primer lugar, 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 se encarga de centralizar la información que reportan los dispositivos. Luego, se verá cómo vincular los controles del panel con los datos que se intercambian con el dispositivo. Primero debemos crear una cuenta de usuario en io.adafruit.com.
Una vez que ingresamos con nuestro usuario, el trabajo tiene dos partes: crear los feeds donde se publicarán los datos y armar el panel que los muestre.
Un feedFeedFuente de datos en la que se puede publicar y a la que uno puede suscribirse. Permite enviar datos para que se almacenen en el tiempo, y también leerlos recibiendo las actualizaciones de quienes publican allí. es una fuente de datos en la que uno puede publicar, así como también se puede suscribir para recibir los datos de cierto feed. 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):
tanque1ytanque2: recibirán el nivel de cada tanque. Lo que se publica es la distancia en centímetros que mide cada sensor entre la tapa y el agua, así que vale la lógica que ya conocés: a menor valor, tanque más lleno.valvulaybomba: recibirán el estado de cada actuador, con la convención 1 = encendida/abierta y 0 = apagada/cerrada.
Luego, desde la sección Dashboards, creamos un nuevo panel con el nombre y la descripción que quieras. Dentro del panel agregamos cuatro bloques, uno por feed:
- Dos bloques Gauge (indicador de aguja), asociados a
tanque1ytanque2, para mostrar el nivel de cada tanque. Como valor máximo del Gauge poné la profundidad de tu tanque en centímetros: la aguja cerca de cero significará tanque lleno (el agua pegada al sensor) y cerca del máximo, tanque vacío. - Dos bloques Indicator (luz virtual), asociados a
valvulaybomba, con la condición “igual a 1”: encendidos cuando la válvula está abierta o la bomba trabajando, apagados cuando no.
Cada bloque permite elegir un título y sus colores, y luego se puede ajustar el tamaño y la ubicación de los bloques dentro del panel. Al finalizar, el panel replicará a distancia el estado completo del sistema: cuánta agua hay en cada tanque y qué están haciendo la válvula y la bomba.
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.
Verificá
Para probar el panel sin la placa, entrá al feed tanque1 y agregale valores a mano
(Add Data): la aguja del Gauge tiene que moverse con cada valor. Cargale un 1 al feed
valvula y su Indicator tiene que encenderse; con 0, apagarse. Si un bloque no
reacciona, revisá su condición y que esté asociado al feed correcto.
Programar sin código bloqueante
Antes de comenzar a utilizar IoT debemos hacer una aclaración con respecto a la función
delay() que figura en el código que usamos hasta ahora (nuestro programa espera un
segundo entre medición y medición). Esta función brinda un tiempo de espera al sistema que
puede utilizarse con varios fines. Suele utilizarse bastante en las primeras aproximaciones
a la programación, ya que su comportamiento resulta fácil de comprender y su programación
no requiere más que una línea de código.
Sin embargo, esta función tiene una complicación, dado que genera un
código bloqueanteCódigo bloqueanteCódigo que detiene todo el procesamiento hasta que se cumple una condición, como delay(), que deja al programa 'colgado' durante el tiempo indicado. En IoT es conflictivo: impide atender tareas simultáneas como la publicación de datos y la conexión a internet.. Esto significa que, cuando el
programa entra en esa función, se detiene todo el procesamiento hasta que se cumpla el
tiempo indicado. En otras palabras, cuando el programa entra al delay() queda “colgado”
por el período de tiempo establecido.
Al utilizar IoT, es conflictivo utilizar código bloqueante, ya que al detenerse el procesamiento se impide también que el sistema realice otras operaciones que funcionan en simultáneo. Por ejemplo, las tareas de publicación y el mantenimiento constante de la conexión a internet.
Para evitar estos problemas, se puede utilizar una alternativa de código no bloqueante,
como la función millis(). Esta función arroja un valor sobre un conteo de tiempo, que se
realiza desde el momento en que se inicia el sistema. Es decir, funciona como un cronómetro
(en milisegundos) que, cada vez que es consultada desde el código, “devuelve” el valor en
el que se encuentra. De esta manera podemos pedirle al sistema que informe cuánto tiempo
transcurrió desde el inicio de las operaciones para dar indicaciones temporales sobre una
tarea, sin detener todas las 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.
Si la diferencia alcanzó el intervalo de medición, el programa mide los tanques y decide
sobre la válvula y la bomba, y anota en millisAnterior el nuevo valor, 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 de los dos tanques del nivel intermedio quitando el código bloqueante. De paso adopta el ritmo del programa IoT del original: se mide cada 3 segundos en lugar de cada 1 —el agua se mueve despacio y medir más seguido no aporta nada—:
Ver el código completo · dos-tanques-no-bloqueante.ino
// Sistema de dos tanques SIN delay(): la misma lógica del nivel intermedio
// (válvula de la cisterna + bomba al tanque elevado), pero no bloqueante.
// En vez de detener el programa entre mediciones, cada vuelta de loop()
// pregunta con millis() si ya pasaron 3 segundos desde la última medición;
// el resto del tiempo el procesador queda libre para otras tareas (esta es
// la base que en ESP32 permite sumar IoT en tanques-iot.ino).
//
// SEGURIDAD: agua y electricidad no se llevan bien. Todas las conexiones
// eléctricas van fuera y POR ENCIMA del nivel del agua, los sensores no
// se deben mojar y la bomba sumergible solo se enciende estando bajo el
// agua. La práctica se realiza con válvula y bomba de baja tensión (12 V);
// la conexión a 220 V de la guía original queda reservada al docente o a
// una instalación supervisada, y nunca sobre la protoboard (desvío
// pedagógico acordado).
//
// Plataforma: Arduino UNO.
// Conexión (la del esquema original): sensor cisterna TRIG 8 / ECHO 9,
// relé de la válvula → pin 4; sensor tanque elevado TRIG 10 / ECHO 11,
// relé de la bomba → pin 5.
//
// Basado en: Fragmentos de código 3 (patrón millis) y 4 (medición y
// control cada 3 segundos, sin la parte IoT) — Sistema de Tanques,
// Saberes Digitales (INET/EDUCAR 2019)
// Modernización: (1) constantes nombradas; millisAnterior como unsigned
// long (el long del original desborda la cuenta). (2) Histéresis de 5 cm
// en los umbrales para que el ruido del HC-SR04 no haga traquetear los
// relés (el original conmutaba en el valor exacto).
// Sensor del tanque 1 (cisterna) y relé de la electroválvula.
const int PIN_TRIG1 = 8;
const int PIN_ECHO1 = 9;
const int PIN_RELE_VALVULA = 4;
// Sensor del tanque 2 (elevado) y relé de la bomba sumergible.
const int PIN_TRIG2 = 10;
const int PIN_ECHO2 = 11;
const int PIN_RELE_BOMBA = 5;
// Módulos relé activo-alto, como en el original (HIGH = actuador andando).
// Si tu módulo es activo-bajo, invertí solo estas dos constantes.
const int RELE_ENCENDIDO = HIGH;
const int RELE_APAGADO = LOW;
// Distancias del ejemplo original (se ajustan midiendo en TU maqueta).
const float DISTANCIA_MINIMA1_CM = 40.0; // cisterna llena
const float DISTANCIA_MAXIMA1_CM = 140.0; // cisterna casi vacía (bomba al aire)
const float DISTANCIA_MINIMA2_CM = 40.0; // tanque elevado lleno
// Colchón anti-traqueteo (modernización, ver encabezado).
const float MARGEN_HISTERESIS_CM = 5.0;
// El Fragmento de código 4 del original mide cada 3 segundos: el agua se
// mueve despacio y medir más seguido no aporta nada.
const unsigned long INTERVALO_MEDICION_MS = 3000;
// 58 microsegundos de eco por cada cm; timeout para ecos que no llegan.
const float MICROSEGUNDOS_POR_CM = 58.0;
const unsigned long TIMEOUT_ECO_US = 30000;
// Momento (ms desde el arranque) de la última medición.
unsigned long millisAnterior = 0;
// Mide la distancia en cm entre un sensor y la superficie del agua.
// Si no llega eco, pulseIn devuelve 0 y la distancia da 0: se interpreta
// como "tanque lleno" y todo se apaga. Falla segura.
// (pulseIn puede esperar el eco hasta 30 ms; junto con los microsegundos
// del disparo, es la única espera que queda en el programa, y es parte
// de la medición misma, no un delay() de relleno.)
float medirDistanciaCm(int pinTrig, int pinEcho) {
digitalWrite(pinTrig, LOW);
delayMicroseconds(2);
digitalWrite(pinTrig, HIGH); // pulso de disparo de 10 microsegundos
delayMicroseconds(10);
digitalWrite(pinTrig, LOW);
return pulseIn(pinEcho, HIGH, TIMEOUT_ECO_US) / MICROSEGUNDOS_POR_CM;
}
void setup() {
pinMode(PIN_TRIG1, OUTPUT);
pinMode(PIN_ECHO1, INPUT);
pinMode(PIN_TRIG2, OUTPUT);
pinMode(PIN_ECHO2, INPUT);
pinMode(PIN_RELE_VALVULA, OUTPUT);
pinMode(PIN_RELE_BOMBA, OUTPUT);
// Estado inicial seguro: válvula cerrada y bomba apagada hasta que
// las primeras mediciones digan otra cosa.
digitalWrite(PIN_RELE_VALVULA, RELE_APAGADO);
digitalWrite(PIN_RELE_BOMBA, RELE_APAGADO);
}
void loop() {
unsigned long millisActual = millis();
// ¿Ya pasaron 3 segundos desde la última medición? Si no, no hacemos
// nada: eso es NO bloquear.
if (millisActual - millisAnterior >= INTERVALO_MEDICION_MS) {
millisAnterior = millisActual;
float distancia1 = medirDistanciaCm(PIN_TRIG1, PIN_ECHO1); // cisterna
float distancia2 = medirDistanciaCm(PIN_TRIG2, PIN_ECHO2); // elevado
// --- Válvula de la cisterna ---
if (distancia1 < DISTANCIA_MINIMA1_CM) {
// Cisterna llena: cerrar la válvula.
digitalWrite(PIN_RELE_VALVULA, RELE_APAGADO);
} else if (distancia1 > DISTANCIA_MINIMA1_CM + MARGEN_HISTERESIS_CM) {
// Hay lugar: abrir la válvula.
digitalWrite(PIN_RELE_VALVULA, RELE_ENCENDIDO);
}
// En la franja intermedia el relé conserva su estado (histéresis).
// --- Bomba sumergible ---
if (distancia2 < DISTANCIA_MINIMA2_CM) {
// El tanque elevado está lleno: no hay que bombear más.
digitalWrite(PIN_RELE_BOMBA, RELE_APAGADO);
} else if (distancia1 > DISTANCIA_MAXIMA1_CM) {
// La cisterna está casi vacía: apagar la bomba ANTES de que
// quede fuera del agua (protección del hardware).
digitalWrite(PIN_RELE_BOMBA, RELE_APAGADO);
} else if (distancia2 > DISTANCIA_MINIMA2_CM + MARGEN_HISTERESIS_CM
&& distancia1 < DISTANCIA_MAXIMA1_CM - MARGEN_HISTERESIS_CM) {
// Hay lugar arriba Y agua suficiente abajo: bombear.
digitalWrite(PIN_RELE_BOMBA, RELE_ENCENDIDO);
}
// En las franjas intermedias la bomba conserva su estado (histéresis).
}
// En este punto el procesador queda libre para realizar otras tareas.
} // Sistema de dos tanques SIN delay(): la misma lógica del nivel intermedio
// (válvula de la cisterna + bomba al tanque elevado), pero no bloqueante.
// En vez de detener el programa entre mediciones, cada vuelta de loop()
// pregunta con millis() si ya pasaron 3 segundos desde la última medición;
// el resto del tiempo el procesador queda libre. Este programa es la base
// directa de tanques-iot.ino: sin código bloqueante, el ESP32 puede
// mantener el WiFi y publicar por MQTT sin desatender los tanques.
//
// SEGURIDAD: agua y electricidad no se llevan bien. Todas las conexiones
// eléctricas van fuera y POR ENCIMA del nivel del agua, los sensores no
// se deben mojar y la bomba sumergible solo se enciende estando bajo el
// agua. La práctica se realiza con válvula y bomba de baja tensión (12 V);
// la conexión a 220 V de la guía original queda reservada al docente o a
// una instalación supervisada, y nunca sobre la protoboard (desvío
// pedagógico acordado).
//
// Plataforma: ESP32 (DevKit v1).
// Conexión: sensor cisterna TRIG GPIO 5 / ECHO GPIO 18, relé de la
// válvula → GPIO 25; sensor elevado TRIG GPIO 19 / ECHO GPIO 21, relé de
// la bomba → GPIO 26.
// NOTA ELÉCTRICA (¡importante!): el ESP32 trabaja a 3,3 V. El HC-SR04
// clásico se alimenta con 5 V y su pin ECHO devuelve el eco a 5 V: eso
// dañaría el GPIO. Es OBLIGATORIO un divisor de tensión en CADA ECHO
// (ECHO —1 kΩ— GPIO —2 kΩ— GND), que baja los 5 V a ~3,3 V. TRIG no
// necesita nada. Alternativa sin divisores: el modelo HC-SR04P, que
// puede alimentarse directamente con 3,3 V.
//
// Basado en: Fragmentos de código 3 (patrón millis) y 4 (medición y
// control cada 3 segundos, sin la parte IoT) — Sistema de Tanques,
// Saberes Digitales (INET/EDUCAR 2019)
// Modernización: (1) constantes nombradas; millisAnterior como unsigned
// long (el long del original desborda la cuenta); portado a ESP32.
// (2) Histéresis de 5 cm en los umbrales para que el ruido del HC-SR04
// no haga traquetear los relés (el original conmutaba en el valor exacto).
// Sensor del tanque 1 (cisterna) y relé de la electroválvula.
const int PIN_TRIG1 = 5;
const int PIN_ECHO1 = 18; // con divisor 1 kΩ / 2 kΩ (ver nota)
const int PIN_RELE_VALVULA = 25;
// Sensor del tanque 2 (elevado) y relé de la bomba sumergible.
const int PIN_TRIG2 = 19;
const int PIN_ECHO2 = 21; // con divisor 1 kΩ / 2 kΩ (ver nota)
const int PIN_RELE_BOMBA = 26;
// Módulos relé activo-alto, como en el original (HIGH = actuador andando).
// Si tu módulo es activo-bajo, invertí solo estas dos constantes.
const int RELE_ENCENDIDO = HIGH;
const int RELE_APAGADO = LOW;
// Distancias del ejemplo original (se ajustan midiendo en TU maqueta).
const float DISTANCIA_MINIMA1_CM = 40.0; // cisterna llena
const float DISTANCIA_MAXIMA1_CM = 140.0; // cisterna casi vacía (bomba al aire)
const float DISTANCIA_MINIMA2_CM = 40.0; // tanque elevado lleno
// Colchón anti-traqueteo (modernización, ver encabezado).
const float MARGEN_HISTERESIS_CM = 5.0;
// El Fragmento de código 4 del original mide cada 3 segundos: el agua se
// mueve despacio y medir más seguido no aporta nada.
const unsigned long INTERVALO_MEDICION_MS = 3000;
// 58 microsegundos de eco por cada cm; timeout para ecos que no llegan.
const float MICROSEGUNDOS_POR_CM = 58.0;
const unsigned long TIMEOUT_ECO_US = 30000;
// Momento (ms desde el arranque) de la última medición.
unsigned long millisAnterior = 0;
// Mide la distancia en cm entre un sensor y la superficie del agua.
// Si no llega eco, pulseIn devuelve 0 y la distancia da 0: se interpreta
// como "tanque lleno" y todo se apaga. Falla segura.
// (pulseIn puede esperar el eco hasta 30 ms; junto con los microsegundos
// del disparo, es la única espera que queda en el programa, y es parte
// de la medición misma, no un delay() de relleno.)
float medirDistanciaCm(int pinTrig, int pinEcho) {
digitalWrite(pinTrig, LOW);
delayMicroseconds(2);
digitalWrite(pinTrig, HIGH); // pulso de disparo de 10 microsegundos
delayMicroseconds(10);
digitalWrite(pinTrig, LOW);
return pulseIn(pinEcho, HIGH, TIMEOUT_ECO_US) / MICROSEGUNDOS_POR_CM;
}
void setup() {
pinMode(PIN_TRIG1, OUTPUT);
pinMode(PIN_ECHO1, INPUT);
pinMode(PIN_TRIG2, OUTPUT);
pinMode(PIN_ECHO2, INPUT);
pinMode(PIN_RELE_VALVULA, OUTPUT);
pinMode(PIN_RELE_BOMBA, OUTPUT);
// Estado inicial seguro: válvula cerrada y bomba apagada hasta que
// las primeras mediciones digan otra cosa.
digitalWrite(PIN_RELE_VALVULA, RELE_APAGADO);
digitalWrite(PIN_RELE_BOMBA, RELE_APAGADO);
}
void loop() {
unsigned long millisActual = millis();
// ¿Ya pasaron 3 segundos desde la última medición? Si no, no hacemos
// nada: eso es NO bloquear.
if (millisActual - millisAnterior >= INTERVALO_MEDICION_MS) {
millisAnterior = millisActual;
float distancia1 = medirDistanciaCm(PIN_TRIG1, PIN_ECHO1); // cisterna
float distancia2 = medirDistanciaCm(PIN_TRIG2, PIN_ECHO2); // elevado
// --- Válvula de la cisterna ---
if (distancia1 < DISTANCIA_MINIMA1_CM) {
// Cisterna llena: cerrar la válvula.
digitalWrite(PIN_RELE_VALVULA, RELE_APAGADO);
} else if (distancia1 > DISTANCIA_MINIMA1_CM + MARGEN_HISTERESIS_CM) {
// Hay lugar: abrir la válvula.
digitalWrite(PIN_RELE_VALVULA, RELE_ENCENDIDO);
}
// En la franja intermedia el relé conserva su estado (histéresis).
// --- Bomba sumergible ---
if (distancia2 < DISTANCIA_MINIMA2_CM) {
// El tanque elevado está lleno: no hay que bombear más.
digitalWrite(PIN_RELE_BOMBA, RELE_APAGADO);
} else if (distancia1 > DISTANCIA_MAXIMA1_CM) {
// La cisterna está casi vacía: apagar la bomba ANTES de que
// quede fuera del agua (protección del hardware).
digitalWrite(PIN_RELE_BOMBA, RELE_APAGADO);
} else if (distancia2 > DISTANCIA_MINIMA2_CM + MARGEN_HISTERESIS_CM
&& distancia1 < DISTANCIA_MAXIMA1_CM - MARGEN_HISTERESIS_CM) {
// Hay lugar arriba Y agua suficiente abajo: bombear.
digitalWrite(PIN_RELE_BOMBA, RELE_ENCENDIDO);
}
// En las franjas intermedias la bomba conserva su estado (histéresis).
}
// En este punto el procesador queda libre para realizar otras tareas
// (en tanques-iot.ino, acá se atiende la conexión WiFi/MQTT).
} Teniendo un programa de funcionamiento de los tanques con un código no bloqueante (es
decir, sin usar la función delay()) estamos en condiciones de incorporar IoT a nuestro
proyecto.
Para el aula
Este es el concepto más transferible del nivel: la diferencia entre esperar deteniendo el
mundo y consultar un reloj sin detenerlo. Funciona muy bien teatralizarlo: un estudiante
“bloqueante” que se congela contando hasta tres sin poder responder nada, contra uno “no
bloqueante” que sigue atendiendo consultas y cada tanto mira el reloj de la pared. Cuando
el grupo puede predecir cuál de los dos puede, además, “atender el teléfono de internet”,
el paso 4 se explica solo. Si el curso ya hizo el proyecto de Semaforización, este paso es
un reencuentro: allá la máquina de estados del semáforo, acá el mismo millis() cuidando
dos tanques.
Verificá
Cargá el programa: el sistema tiene que comportarse igual que en el nivel intermedio —la válvula corta en la altura de corte, la bomba respeta su doble condición—, con la única diferencia visible de que ahora reacciona en rondas de 3 segundos. Que por fuera se vean casi idénticos y por dentro sean tan distintos es, justamente, el punto de este paso.
Programar IoT
Con el panel listo y el código ya libre de bloqueos, falta el último eslabón: que el sistema publique los niveles y los estados en cada ronda de medición.
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 y
bomba cada vez que un relé conmuta, y las distancias en tanque1 y tanque2 cuando
difieren en 2 cm o más de lo último publicado. El original publicaba los cuatro feeds en
cada medición; publicar solo los cambios muestra lo mismo en el panel sin agotar el cupo
de la cuenta gratuita de Adafruit IO (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, los tanques siguen controlados, y al volver la conexión
el panel se pone al día solo.
Ver el código completo · tanques-iot.ino
Nota: Este código está disponible para ESP32. Cambiá la plataforma con el selector del encabezado para verlo y practicarlo.
// Sistema de dos tanques con monitoreo IoT vía Adafruit IO (MQTT).
// La lógica no bloqueante de dos-tanques-no-bloqueante.ino sigue manejando
// válvula y bomba; además se publican cuatro feeds para verlos desde el
// Panel de Control de Adafruit (dos Gauge y dos Indicator, como en la guía):
//
// "tanque1" y "tanque2": nivel medido, en cm de distancia entre el
// sensor y el agua (número más chico = tanque más lleno, porque el
// sensor mira desde arriba).
// "valvula" y "bomba": 0 = apagada, 1 = encendida.
//
// SEGURIDAD: agua y electricidad no se llevan bien. Todas las conexiones
// eléctricas van fuera y POR ENCIMA del nivel del agua, los sensores no
// se deben mojar y la bomba sumergible solo se enciende estando bajo el
// agua. La práctica se realiza con válvula y bomba de baja tensión (12 V);
// la conexión a 220 V de la guía original queda reservada al docente o a
// una instalación supervisada, y nunca sobre la protoboard (desvío
// pedagógico acordado).
//
// Plataforma: ESP32 (DevKit v1) — usa su WiFi integrado.
// Conexión: sensor cisterna TRIG GPIO 5 / ECHO GPIO 18, relé de la
// válvula → GPIO 25; sensor elevado TRIG GPIO 19 / ECHO GPIO 21, relé de
// la bomba → GPIO 26.
// NOTA ELÉCTRICA (¡importante!): el ESP32 trabaja a 3,3 V. El HC-SR04
// clásico se alimenta con 5 V y su pin ECHO devuelve el eco a 5 V: eso
// dañaría el GPIO. Es OBLIGATORIO un divisor de tensión en CADA ECHO
// (ECHO —1 kΩ— GPIO —2 kΩ— GND). Alternativa: el modelo HC-SR04P, que
// puede alimentarse directamente con 3,3 V.
//
// Librería requerida (Library Manager del IDE de Arduino):
// "Adafruit MQTT Library" (de Adafruit) — la misma elección que en el
// proyecto del semáforo: cada feed es un objeto y publish() acepta
// números directamente, igual que el olq.publish() del original.
//
// Basado en: Fragmento de código 4 — Sistema de Tanques,
// 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 los cuatro feeds
// en CADA medición (cada 3 s = 80 publicaciones por minuto, muy por encima
// del límite gratuito de Adafruit IO de 30/min); acá se publica solo
// cuando algo cambia: los estados al conmutar, y los niveles cuando la
// diferencia con lo último publicado es de 2 cm o más. (3) Histéresis de
// 5 cm en los umbrales de control (el original conmutaba en el valor
// exacto y el ruido del sensor haría traquetear los relés).
#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;
// Sensor del tanque 1 (cisterna) y relé de la electroválvula.
const int PIN_TRIG1 = 5;
const int PIN_ECHO1 = 18; // con divisor 1 kΩ / 2 kΩ (ver nota)
const int PIN_RELE_VALVULA = 25;
// Sensor del tanque 2 (elevado) y relé de la bomba sumergible.
const int PIN_TRIG2 = 19;
const int PIN_ECHO2 = 21; // con divisor 1 kΩ / 2 kΩ (ver nota)
const int PIN_RELE_BOMBA = 26;
// Módulos relé activo-alto, como en el original (HIGH = actuador andando).
// Si tu módulo es activo-bajo, invertí solo estas dos constantes.
const int RELE_ENCENDIDO = HIGH;
const int RELE_APAGADO = LOW;
// Distancias del ejemplo original (se ajustan midiendo en TU maqueta).
const float DISTANCIA_MINIMA1_CM = 40.0; // cisterna llena
const float DISTANCIA_MAXIMA1_CM = 140.0; // cisterna casi vacía (bomba al aire)
const float DISTANCIA_MINIMA2_CM = 40.0; // tanque elevado lleno
// Colchón anti-traqueteo de los relés (modernización, ver encabezado).
const float MARGEN_HISTERESIS_CM = 5.0;
// El Fragmento de código 4 del original mide cada 3 segundos.
const unsigned long INTERVALO_MEDICION_MS = 3000;
// Un nivel se re-publica solo si difiere en 2 cm o más de lo último
// publicado: los milímetros que bailan entre medición y medición no le
// aportan nada a la central y consumen el cupo gratuito de Adafruit IO.
const int UMBRAL_PUBLICACION_CM = 2;
// Si se cae la conexión MQTT, reintentamos cada tanto SIN bloquear:
// los tanques deben seguir controlados aunque no haya internet.
const unsigned long INTERVALO_REINTENTO_MQTT_MS = 5000;
// 58 microsegundos de eco por cada cm; timeout para ecos que no llegan.
const float MICROSEGUNDOS_POR_CM = 58.0;
const unsigned long TIMEOUT_ECO_US = 30000;
// 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 feedTanque1(&mqtt, IO_USERNAME "/feeds/tanque1");
Adafruit_MQTT_Publish feedTanque2(&mqtt, IO_USERNAME "/feeds/tanque2");
Adafruit_MQTT_Publish feedValvula(&mqtt, IO_USERNAME "/feeds/valvula");
Adafruit_MQTT_Publish feedBomba(&mqtt, IO_USERNAME "/feeds/bomba");
// 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ó nada: no hay que publicar niveles inventados si
// la conexión MQTT llega antes que la primera medición.
bool hayMedicion = false;
// Estado actual del sistema (lo que muestran los relés y los sensores)...
int estadoValvula = 0; // 0 = cerrada, 1 = abierta
int estadoBomba = 0; // 0 = apagada, 1 = encendida
int nivelCm1 = 0; // última medición de la cisterna, en cm
int nivelCm2 = 0; // última medición del tanque elevado, en cm
// ...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 estadoBombaPublicado = -1;
int nivelPublicado1 = -1000; // imposible: fuerza la primera publicación
int nivelPublicado2 = -1000;
// Mide la distancia en cm entre un sensor y la superficie del agua.
// Si no llega eco, pulseIn devuelve 0 y la distancia da 0: se interpreta
// como "tanque lleno" y todo se apaga. Falla segura.
float medirDistanciaCm(int pinTrig, int pinEcho) {
digitalWrite(pinTrig, LOW);
delayMicroseconds(2);
digitalWrite(pinTrig, HIGH); // pulso de disparo de 10 microsegundos
delayMicroseconds(10);
digitalWrite(pinTrig, LOW);
return pulseIn(pinEcho, HIGH, TIMEOUT_ECO_US) / MICROSEGUNDOS_POR_CM;
}
// 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 control de los tanques:
// 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
// (los niveles, solo si cambiaron 2 cm o más). Si publish() falla o no
// hay conexión, no se actualiza el registro: se reintenta en la próxima
// vuelta de loop().
void publicarCambios() {
if (!mqtt.connected()) {
return; // sin internet los tanques siguen; la central se pone al día al volver
}
if (!hayMedicion) {
return; // solo se publica lo que realmente se midió
}
if (estadoValvula != estadoValvulaPublicado
&& feedValvula.publish((int32_t)estadoValvula)) {
estadoValvulaPublicado = estadoValvula;
}
if (estadoBomba != estadoBombaPublicado
&& feedBomba.publish((int32_t)estadoBomba)) {
estadoBombaPublicado = estadoBomba;
}
if (diferenciaAbsoluta(nivelCm1, nivelPublicado1) >= UMBRAL_PUBLICACION_CM
&& feedTanque1.publish((int32_t)nivelCm1)) {
nivelPublicado1 = nivelCm1;
}
if (diferenciaAbsoluta(nivelCm2, nivelPublicado2) >= UMBRAL_PUBLICACION_CM
&& feedTanque2.publish((int32_t)nivelCm2)) {
nivelPublicado2 = nivelCm2;
}
}
void setup() {
Serial.begin(115200);
pinMode(PIN_TRIG1, OUTPUT);
pinMode(PIN_ECHO1, INPUT);
pinMode(PIN_TRIG2, OUTPUT);
pinMode(PIN_ECHO2, INPUT);
pinMode(PIN_RELE_VALVULA, OUTPUT);
pinMode(PIN_RELE_BOMBA, OUTPUT);
// Estado inicial seguro: válvula cerrada y bomba apagada hasta que
// las primeras mediciones digan otra cosa.
digitalWrite(PIN_RELE_VALVULA, RELE_APAGADO);
digitalWrite(PIN_RELE_BOMBA, RELE_APAGADO);
// Conexión WiFi. Esperar acá con delay() es aceptable porque el control
// de los tanques 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, cada 3 segundos (como el original) ---
if (millisActual - millisAnterior >= INTERVALO_MEDICION_MS) {
millisAnterior = millisActual;
float distancia1 = medirDistanciaCm(PIN_TRIG1, PIN_ECHO1); // cisterna
float distancia2 = medirDistanciaCm(PIN_TRIG2, PIN_ECHO2); // elevado
// Los feeds publican el nivel en cm enteros, como el original.
nivelCm1 = (int)distancia1;
nivelCm2 = (int)distancia2;
hayMedicion = true; // desde ahora, publicarCambios() tiene datos reales
// Válvula de la cisterna.
if (distancia1 < DISTANCIA_MINIMA1_CM) {
// Cisterna llena: cerrar la válvula.
digitalWrite(PIN_RELE_VALVULA, RELE_APAGADO);
estadoValvula = 0;
} else if (distancia1 > DISTANCIA_MINIMA1_CM + MARGEN_HISTERESIS_CM) {
// Hay lugar: abrir la válvula.
digitalWrite(PIN_RELE_VALVULA, RELE_ENCENDIDO);
estadoValvula = 1;
}
// En la franja intermedia el relé conserva su estado (histéresis).
// Bomba sumergible.
if (distancia2 < DISTANCIA_MINIMA2_CM) {
// El tanque elevado está lleno: no hay que bombear más.
digitalWrite(PIN_RELE_BOMBA, RELE_APAGADO);
estadoBomba = 0;
} else if (distancia1 > DISTANCIA_MAXIMA1_CM) {
// La cisterna está casi vacía: apagar la bomba ANTES de que
// quede fuera del agua (protección del hardware).
digitalWrite(PIN_RELE_BOMBA, RELE_APAGADO);
estadoBomba = 0;
} else if (distancia2 > DISTANCIA_MINIMA2_CM + MARGEN_HISTERESIS_CM
&& distancia1 < DISTANCIA_MAXIMA1_CM - MARGEN_HISTERESIS_CM) {
// Hay lugar arriba Y agua suficiente abajo: bombear.
digitalWrite(PIN_RELE_BOMBA, RELE_ENCENDIDO);
estadoBomba = 1;
}
// En las franjas intermedias la bomba conserva su estado (histéresis).
}
// --- 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();
} Así queda funcionando el tablero que pedían los vecinos de Melina. Podés ver el sistema completo —los tanques trabajando más 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.
De centímetros a litros
El Gauge muestra centímetros de distancia, y “43 cm” no le dice mucho a un vecino. Tomen la extensión sugerida en la ficha del proyecto: midan su tanque, calculen cuántos litros representa cada centímetro y modifiquen el programa para publicar la capacidad disponible en litros (o el porcentaje de llenado). ¿Qué hay que cambiar en el panel para que el Gauge siga siendo coherente con el nuevo valor?
Verificá
Con el programa cargado y la placa conectada a tu red WiFi, abrí el panel de Adafruit IO en el celular o la computadora: los Indicator tienen que encenderse y apagarse junto con la válvula y la bomba en cada conmutación, y los Gauge, actualizarse a medida que el nivel cambia (recordá que se publica por cambios: con el agua quieta, que no lleguen datos nuevos es lo esperado; movela y van a aparecer). Todo con una pequeña demora de red. Si no llega nada, revisá las credenciales y el nombre de la red; si llegan datos pero un bloque no reacciona, revisá a qué feed está asociado y su condición.
Cierre del nivel
Al terminar este nivel, tu sistema de tanques dejó de ser solo una maqueta hidráulica: 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 cuánta agua hay
en cada tanque y qué están haciendo la válvula y la bomba. Con esto queda completo el
recorrido del proyecto. Si el grupo quiere seguir, en la ficha del proyecto está la
extensión sugerida: convertir la lectura del sensor en litros disponibles y publicar
ese dato, que es exactamente lo que un consorcio querría ver.
Evidencia de cierre
Al terminar este nivel, esto tiene que estar funcionando:
- El sistema de tanques 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 los niveles de los dos tanques y el estado de la válvula y la bomba publicados por el dispositivo (variante ESP32).