Saltar al contenido
Volver a la ficha del proyecto

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.

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 Controlar el llenado de un tanque Nivel inicial
  3. 03 Un segundo tanque con bomba sumergible Nivel intermedio
  4. 04 Monitorear los tanques con Internet de las Cosas Nivel avanzado

Materiales y equipamiento

Insumos

Imagen Insumo Cantidad Plataforma
Foto de Módulo relé 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
Foto de Sensor ultrasónico HC-SR04 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
Foto de Fuente de alimentación 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
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 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). 1 ESP32 DevKit
Foto de Cable USB tipo B Cable USB tipo B 1 Arduino UNO R3
Foto de Cable micro USB Cable micro USB (con línea de datos) 1 ESP32 DevKit
Foto de Fuente de alimentación 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
Foto de Cables dupont macho-macho 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

Arduino UNO Conexionado en protoboard · Sistema de tanques: 2 sensores HC-SR04 (nivel) + relé de válvula + relé de bomba.

Deslizá horizontalmente para ver el plano completo

Arduino UNO
HC-SR04 · Cisterna
HC-SR04
VCCTRIGECHOGND
HC-SR04 · Tanque elevado
HC-SR04
VCCTRIGECHOGND
Relé válvula
RELAY 5V
INVCCGNDCOMNONC
Relé bomba
RELAY 5V
INVCCGNDCOMNONC
ESP32 DevKit v1 Conexionado en protoboard · Sistema de tanques: 2 sensores HC-SR04 (nivel) + relé de válvula + relé de bomba.

Deslizá horizontalmente para ver el plano completo

ESP32 DevKit v1
HC-SR04 · Cisterna
HC-SR04
VCCTRIGECHOGND
HC-SR04 · Tanque elevado
HC-SR04
VCCTRIGECHOGND
Relé válvula
RELAY 5V
INVCCGNDCOMNONC
Relé bomba
RELAY 5V
INVCCGNDCOMNONC
Arduino UNOConexionado en protoboard
Conexiones · Arduino UNO. Convención de colores: alimentación VCC = rojo, GND = negro; Trig (pulso de la placa al sensor) = amarillo, Echo (eco del sensor a la placa) = azul; señal IN de cada relé = verde; circuito de potencia (fuente 12 V ↔ relé ↔ electroválvula/bomba), fuera de la protoboard = naranja.
Desde Hacia Color de cable
Arduino UNO · 5VBus + de la protoboardrojo
Arduino UNO · GNDBus − de la protoboardnegro
HC-SR04 · Cisterna · VCCBus + de la protoboardrojo
HC-SR04 · Cisterna · GNDBus − de la protoboardnegro
HC-SR04 · Cisterna · TrigArduino UNO · 8amarillo
HC-SR04 · Cisterna · EchoArduino UNO · 9azul
HC-SR04 · Tanque elevado · VCCBus + de la protoboardrojo
HC-SR04 · Tanque elevado · GNDBus − de la protoboardnegro
HC-SR04 · Tanque elevado · TrigArduino UNO · 10amarillo
HC-SR04 · Tanque elevado · EchoArduino UNO · 11azul
Relé válvula · VCCBus + de la protoboardrojo
Relé válvula · GNDBus − de la protoboardnegro
Relé válvula · INArduino UNO · 4verde
Fuente 12 V (+)Relé válvula · COMnaranja
Relé válvula · NOElectroválvula · borne 1naranja
Electroválvula · borne 2Fuente 12 V (−)naranja
Relé bomba · VCCBus + de la protoboardrojo
Relé bomba · GNDBus − de la protoboardnegro
Relé bomba · INArduino UNO · 5verde
Fuente 12 V (+)Relé bomba · COMnaranja
Relé bomba · NOBomba sumergible · borne 1naranja
Bomba sumergible · borne 2Fuente 12 V (−)naranja
ESP32 DevKit v1Conexionado en protoboard
Conexiones · ESP32 DevKit v1. Convención de colores: alimentación VCC = rojo, GND = negro; Trig (pulso de la placa al sensor) = amarillo, Echo (eco del sensor a la placa) = azul; señal IN de cada relé = verde; circuito de potencia (fuente 12 V ↔ relé ↔ electroválvula/bomba), fuera de la protoboard = naranja.
Desde Hacia Color de cable
ESP32 DevKit v1 · 3V3Bus + de la protoboardrojo
ESP32 DevKit v1 · GNDBus − de la protoboardnegro
HC-SR04 · Cisterna · VCCBus + de la protoboardrojo
HC-SR04 · Cisterna · GNDBus − de la protoboardnegro
HC-SR04 · Cisterna · TrigESP32 DevKit v1 · GPIO5amarillo
HC-SR04 · Cisterna · EchoESP32 DevKit v1 · GPIO18azul
HC-SR04 · Tanque elevado · VCCBus + de la protoboardrojo
HC-SR04 · Tanque elevado · GNDBus − de la protoboardnegro
HC-SR04 · Tanque elevado · TrigESP32 DevKit v1 · GPIO19amarillo
HC-SR04 · Tanque elevado · EchoESP32 DevKit v1 · GPIO21azul
Relé válvula · VCCBus + de la protoboardrojo
Relé válvula · GNDBus − de la protoboardnegro
Relé válvula · INESP32 DevKit v1 · GPIO25verde
Fuente 12 V (+)Relé válvula · COMnaranja
Relé válvula · NOElectroválvula · borne 1naranja
Electroválvula · borne 2Fuente 12 V (−)naranja
Relé bomba · VCCBus + de la protoboardrojo
Relé bomba · GNDBus − de la protoboardnegro
Relé bomba · INESP32 DevKit v1 · GPIO26verde
Fuente 12 V (+)Relé bomba · COMnaranja
Relé bomba · NOBomba sumergible · borne 1naranja
Bomba sumergible · borne 2Fuente 12 V (−)naranja
Nivel inicial

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álvula (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ónico 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.

Esquema del tanque: recipiente con el sensor ultrasónico fijado bajo la tapa emitiendo ondas hacia la superficie del agua, la electroválvula en la entrada de agua superior derecha y la manguera de salida en la parte inferior izquierda.
El tanque del nivel inicial: el sensor ultrasónico mide desde la tapa la distancia hasta el agua, y la electroválvula controla la entrada (figura original del proyecto).

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.

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.

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é a la placa de control, como indica la figura.

Esquema de conexión: placa Arduino UNO unida a un sensor ultrasónico HC-SR04 rotulado Nivel Tanque 1 (VCC a 5V, GND a GND, Trig y Echo a los pines 8 y 9) y a un módulo relé cuyo pin de señal va al pin 4; en los bornes del relé, la electroválvula 1 y un enchufe de 220 V.
Esquema de conexión del nivel inicial (figura original del proyecto). Atención: el original alimentaba la válvula desde un enchufe de 220 V; en esta reedición ese cableado se reemplaza por una fuente de 12 V.

¡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.

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-llenado.ino Arduino UNO
// 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-llenado.ino ESP32
// 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);
}
El control de llenado: si el agua está más cerca que DISTANCIA_MINIMA_CM, la válvula se cierra; si está más lejos que la altura de corte más el margen de histéresis, se abre.

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.

Subir el código a la placa

Para subir el código de nuestro programa a la placa, necesitamos:

  1. Conectar la placa a la computadora con el cable USB.
  2. Chequear que en el selector de placa del Arduino IDE esté seleccionada la placa correcta.
  3. Seleccionar el puerto al que está conectada la placa.
  4. 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.

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.
Nivel intermedio

Un segundo tanque con bomba sumergible

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 distancia y una bomba sumergible. Los roles quedan así:

  • El tanque del nivel inicial pasa a ser la cisterna: conserva su sensor y su electroválvula (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.

Esquema de los dos tanques: a la derecha, la cisterna con su sensor ultrasónico bajo la tapa, la bomba sumergible en su interior y la electroválvula en la entrada; a la izquierda, el tanque elevado con su propio sensor y la manguera de salida. Una manguera une la bomba con el tanque elevado.
El sistema completo del nivel intermedio: la cisterna (con electroválvula y bomba sumergible) alimenta al tanque elevado, y cada tanque tiene su sensor (figura original del proyecto).

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.

Esquema de conexión: Arduino UNO con dos sensores ultrasónicos HC-SR04 rotulados Nivel Tanque 1 y Nivel Tanque 2 (Trig y Echo a los pines 8-9 y 10-11) y dos módulos relé con señal a los pines 4 y 5; en los bornes de los relés, la electroválvula 1, la bomba de agua 1 y un enchufe de 220 V.
Esquema de conexión del nivel intermedio: se suman el segundo sensor y el relé de la bomba (figura original del proyecto). Atención: el enchufe de 220 V del original se reemplaza en esta reedición por la fuente de baja tensión.

Conectamos los componentes como indica la figura, agregando el sensor ultrasónico del segundo tanque y la bomba de agua con su relé:

  • 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.

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():

  1. 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.
  2. 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
dos-tanques.ino Arduino UNO
// 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);
}
dos-tanques.ino ESP32
// 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);
}
El sistema de dos tanques: la válvula cuida el llenado de la cisterna y la bomba solo trabaja si el elevado necesita agua y hay nivel suficiente para mantenerla sumergida.

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.

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.
Nivel avanzado

Monitorear los tanques con Internet de las Cosas

Introducción a Internet de las Cosas (IoT)

Internet de las Cosas (en inglés Internet of Things, abreviado IoT) es un concepto que refiere a la interconexión digital de objetos cotidianos con internet. Esta interconexión puede tener diversas funciones. Por ejemplo, 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:

  1. Un dispositivo capaz de conectarse a internet.
  2. 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 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 (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 feed 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):

  • tanque1 y tanque2: 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.
  • valvula y bomba: 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 tanque1 y tanque2, 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 valvula y bomba, 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.

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 bloqueante. 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
dos-tanques-no-bloqueante.ino Arduino UNO
// 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.
}
dos-tanques-no-bloqueante.ino ESP32
// 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).
}
El mismo control de válvula y bomba del nivel intermedio, ahora sin delay(): el programa consulta millis() y solo mide cuando toca.

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.

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.

tanques-iot.ino ESP32
// 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();
}
El sistema de tanques IoT completo: control no bloqueante + WiFi del ESP32 + publicación de niveles y estados en Adafruit IO.

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.

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).