Automatizar la recirculación del agua y la aireación
Recorrido del nivel
Esa barra de arriba te dice en qué paso estás. Tocala para ver el índice completo y saltar a cualquier paso.
Conectar el aireador al módulo relé
Para mantener aireada el agua de la pecera, vamos a encender y apagar el aireador mediante un 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. en intervalos de tiempo determinados. El relé es un interruptor que la placa abre y cierra con una señal de baja tensión, pero que del otro lado puede manejar una carga de mayor tensión, como el aireador.
Conectamos el aireador y el relé a la placa como muestra el esquema. El aireador no tiene polaridad, así que sus dos terminales pueden ir indistintamente a uno u otro borne del relé. Donde el esquema dice “ENCHUFE 220V” se hace referencia a la conexión de la carga a su fuente de alimentación.
¡Atención! Agua y electricidad no se llevan bien, y esta carga puede ser de 220 V. El código que subiremos a la placa controla la apertura y el cierre del relé y, con eso, el encendido y apagado del aireador; pero la parte de potencia hay que armarla con cuidado. Si usás protoboard, las conexiones al relé y a la carga van FUERA de la protoboard. Todas las conexiones eléctricas van lejos y por encima del nivel del agua, y el circuito se manipula siempre con las manos secas y desenchufado.
Programar los tiempos de encendido del aireador
La programación la realizamos 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 idea más simple sería: encender el aireador, esperar un rato, apagarlo, esperar otro rato
y repetir. Para “esperar” existe la instrucción delay(), que detiene el programa la
cantidad de tiempo que le indiquemos. Con una sola carga, alcanza. Vamos a ver en el paso 4
que, en cuanto sumemos la bomba, esa idea se nos complica, y en el paso 5 la resolvemos bien.
Por ahora quedate con la idea de que el programa va a encender y apagar una salida digital
por tiempo.
Subir el código a la placa
Para subir un 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, veremos el mensaje de carga completada en la barra inferior del IDE. Con el aireador conectado, debería empezar a encenderse y apagarse siguiendo los intervalos que programamos.
Incluir la bomba de agua
Para que las plantas reciban el agua con los nutrientes que generan los peces y las bacterias, hace falta llevar el agua de la pecera hasta la batea de las plantas. Eso lo hace una bomba de agua instalada en el fondo de la pecera, con una manguera que llega hasta arriba.
El funcionamiento de la bomba es igual al del aireador: se enciende y se apaga con un relé. Conectamos entonces un segundo módulo relé para la bomba, cableado de la misma forma que el del aireador (la señal a otro pin de la placa, y la bomba en los bornes de salida).
Verificá
Antes de seguir, repasá el armado: el relé del aireador con su señal a un pin, el relé de la bomba con su señal a otro pin, cada carga en los bornes de salida de su relé, y toda la potencia fuera de la protoboard y lejos del agua.
¡Atención! La bomba no puede encenderse fuera del agua: se quema. Conviene además probar manualmente los tiempos de encendido y apagado hasta encontrar los que hacen que la batea de las plantas no rebalse. Esos tiempos son lo que llamaremos régimen de funcionamiento de la bomba.
Para el aula
Dos reglas para instalar como hábito, no como dato: la bomba siempre sumergida (en seco se arruina en segundos) y el régimen de la bomba se mide, no se copia (depende del caudal de la bomba y del tamaño de la batea de cada grupo). Es una buena oportunidad para que cada grupo pruebe, cronómetro en mano, cuántos segundos de bomba llenan su batea sin desbordarla.
Programar sin código bloqueante
Ahora que tenemos que controlar dos cargas a la vez, aparece un problema. Supongamos estos regímenes de funcionamiento, que coinciden en un ciclo de 60 segundos:
- Aireador: 20 segundos encendido y 40 segundos apagado.
- Bomba: 5 segundos encendida y 55 segundos apagada.
Si programáramos esto con delay() —encender, esperar, apagar, esperar—, tendríamos un
problema: mientras el programa “espera” los 40 segundos apagados del aireador, no puede
atender a la bomba, y al revés. Es decir, el código que temporiza el aireador bloquea el
de la bomba. Esto se conoce como 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. y su
problema es que impide realizar varias tareas en simultáneo.
¿Cómo se resuelve? En lugar de detener el programa con delay(), usamos un cronómetro.
En cada vuelta de loop() consultamos cuánto tiempo lleva el ciclo (con millis()) y
decidimos, para cada carga por separado, si en ese instante le toca estar encendida o
apagada:
- Si el cronómetro marca menos de 20 s, el aireador está encendido; si no, apagado.
- Si el cronómetro marca menos de 5 s, la bomba está encendida; si no, apagada.
- Al cumplirse los 60 s, reiniciamos el cronómetro y el ciclo vuelve a empezar.
Así, los regímenes del aireador y de la bomba se ejecutan en simultáneo en función del mismo cronómetro, y el programa nunca queda “clavado” esperando.
Ver el código completo · acuaponico-temporizado.ino
// Sistema acuapónico automatizado — nivel inicial (SIN código bloqueante).
// Dos cargas, cada una con su relé, dentro de un ciclo de 60 s que se
// repite indefinidamente:
// - Aireador de la pecera : 20 s encendido / 40 s apagado.
// - Bomba de recirculación: 5 s encendida / 55 s apagada.
// Estos regímenes de funcionamiento son los del ejemplo de la guía (se
// ajustan a mano en TU maqueta hasta que la batea no rebalse).
//
// ¿POR QUÉ NO USAR delay()? Con una sola carga alcanzaría con encender,
// esperar y apagar. Pero acá el aireador y la bomba tienen ciclos DISTINTOS
// que corren AL MISMO TIEMPO: si el programa se queda "esperando" (delay)
// los 40 s apagados del aireador, no puede atender a la bomba en ese rato.
// Eso se llama "código bloqueante" y es exactamente el problema que plantea
// el Paso 5 del original. La solución: un CRONÓMETRO. En cada vuelta de
// loop() miramos cuánto tiempo lleva el ciclo (millis() - inicioCiclo) y,
// para CADA carga por separado, decidimos si en este instante le toca estar
// encendida o apagada. El procesador nunca queda clavado esperando: esta es
// la base no bloqueante sobre la que después se apoyará el IoT (ESP32).
//
// SEGURIDAD (220 V + agua): el aireador y la bomba son cargas de 220 V
// conmutadas por un relé. En el aula se trabaja con cargas de BAJA
// TENSIÓN; la conexión real a 220 V queda reservada al docente o a una
// instalación supervisada, NUNCA sobre la protoboard, y con todas las
// conexiones eléctricas fuera y por encima del nivel del agua. La BOMBA
// NO DEBE FUNCIONAR EN SECO: si se enciende fuera del agua, se quema
// (por eso primero se llena la pecera y recién ahí se prueba).
//
// Plataforma: Arduino UNO.
// Conexión (contrato de la reedición): entrada "S" del relé del aireador
// → pin 2; entrada "S" del relé de la bomba → pin 3.
//
// Basado en: Fragmento de código 2 (Nivel Inicial, "Programar sin código
// bloqueante") — Sistema Acuapónico Automatizado, Saberes Digitales
// (INET/EDUCAR 2019).
// Modernización: se reemplazó el código generado por mBlock3 (getLastTime
// con double de millis()/1000.0, _delay/_loop, angle_rad/deg) por C++
// idiomático con constantes nombradas y aritmética de unsigned long en
// milisegundos (evita el desborde de la resta que arrastraba el original).
// Relés de las dos cargas de 220 V (ver advertencia de SEGURIDAD arriba).
const int PIN_RELE_AIREADOR = 2;
const int PIN_RELE_BOMBA = 3;
// Módulos relé activo-alto, como en el original (HIGH = carga encendida).
// Si tu módulo es activo-bajo, invertí solo estas dos constantes.
const int RELE_ENCENDIDO = HIGH;
const int RELE_APAGADO = LOW;
// Umbrales de encendido dentro del ciclo (los del ejemplo original). Cada
// carga arranca encendida al comenzar el ciclo y se apaga al cruzar su
// umbral; a los 60 s el ciclo se reinicia.
const unsigned long AIREADOR_ON_MS = 20000UL; // aireador ON los primeros 20 s
const unsigned long BOMBA_ON_MS = 5000UL; // bomba ON los primeros 5 s
const unsigned long CICLO_TOTAL_MS = 60000UL; // el ciclo dura 60 s
// Marca de tiempo (ms desde el arranque) en la que empezó el ciclo actual.
// Es nuestro "cronómetro": el tiempo transcurrido es millis() - inicioCiclo.
unsigned long inicioCiclo = 0;
void setup() {
pinMode(PIN_RELE_AIREADOR, OUTPUT);
pinMode(PIN_RELE_BOMBA, OUTPUT);
// Estado inicial seguro: ambas cargas apagadas hasta la primera vuelta.
digitalWrite(PIN_RELE_AIREADOR, RELE_APAGADO);
digitalWrite(PIN_RELE_BOMBA, RELE_APAGADO);
inicioCiclo = millis();
}
void loop() {
// Tiempo transcurrido desde el inicio del ciclo. Con unsigned long, la
// resta funciona bien aun cuando millis() desborda (~cada 50 días).
unsigned long transcurrido = millis() - inicioCiclo;
// Cada carga decide su estado mirando el cronómetro. Nada de delay():
// esta función entra y sale al instante y las dos se atienden "a la vez".
digitalWrite(PIN_RELE_AIREADOR,
transcurrido < AIREADOR_ON_MS ? RELE_ENCENDIDO : RELE_APAGADO);
digitalWrite(PIN_RELE_BOMBA,
transcurrido < BOMBA_ON_MS ? RELE_ENCENDIDO : RELE_APAGADO);
// Cumplidos los 60 s, reiniciamos el cronómetro y el ciclo vuelve a cero.
if (transcurrido >= CICLO_TOTAL_MS) {
inicioCiclo = millis();
}
} // Sistema acuapónico automatizado — nivel inicial (SIN código bloqueante).
// Dos cargas, cada una con su relé, dentro de un ciclo de 60 s que se
// repite indefinidamente:
// - Aireador de la pecera : 20 s encendido / 40 s apagado.
// - Bomba de recirculación: 5 s encendida / 55 s apagada.
// Estos regímenes son los del ejemplo de la guía (se ajustan a mano en TU
// maqueta hasta que la batea no rebalse).
//
// ¿POR QUÉ NO USAR delay()? El aireador y la bomba tienen ciclos DISTINTOS
// que corren AL MISMO TIEMPO: si el programa se queda "esperando" (delay)
// los 40 s apagados del aireador, no puede atender a la bomba en ese rato.
// Eso es "código bloqueante", el problema que plantea el Paso 5 del
// original. La solución es un CRONÓMETRO: en cada vuelta de loop() miramos
// cuánto lleva el ciclo (millis() - inicioCiclo) y, para CADA carga por
// separado, decidimos si le toca estar encendida o apagada. El procesador
// nunca queda clavado esperando: esta es la base no bloqueante sobre la que
// se apoya el IoT del nivel avanzado.
//
// SEGURIDAD (220 V + agua): el aireador y la bomba son cargas de 220 V
// conmutadas por un relé. En el aula se trabaja con cargas de BAJA
// TENSIÓN; la conexión real a 220 V queda reservada al docente o a una
// instalación supervisada, NUNCA sobre la protoboard, y con todas las
// conexiones eléctricas fuera y por encima del nivel del agua. La BOMBA
// NO DEBE FUNCIONAR EN SECO: si se enciende fuera del agua, se quema.
//
// Plataforma: ESP32 (DevKit v1).
// Conexión (contrato de la reedición): entrada "S" del relé del aireador
// → GPIO 25; entrada "S" del relé de la bomba → GPIO 26.
// NOTA ELÉCTRICA: la lógica del ESP32 es de 3,3 V. La entrada de señal de
// la mayoría de los módulos relé la reconoce, pero el módulo se ALIMENTA
// con 5 V (VCC al 5V/VIN de la placa) y la masa (GND) debe ser común. Para
// conmutar 220 V, usar sí o sí un módulo relé con optoacoplador.
//
// Basado en: Fragmento de código 2 (Nivel Inicial, "Programar sin código
// bloqueante") — Sistema Acuapónico Automatizado, Saberes Digitales
// (INET/EDUCAR 2019).
// Modernización: C++ idiomático con constantes nombradas en vez del código
// de mBlock3; portado del UNO (pines 2/3) al ESP32 (GPIO 25/26); aritmética
// de unsigned long en ms (evita el desborde de la resta del original).
// Relés de las dos cargas de 220 V (ver advertencia de SEGURIDAD arriba).
const int PIN_RELE_AIREADOR = 25;
const int PIN_RELE_BOMBA = 26;
// Módulos relé activo-alto, como en el original (HIGH = carga encendida).
// Si tu módulo es activo-bajo, invertí solo estas dos constantes.
const int RELE_ENCENDIDO = HIGH;
const int RELE_APAGADO = LOW;
// Umbrales de encendido dentro del ciclo (los del ejemplo original). Cada
// carga arranca encendida al comenzar el ciclo y se apaga al cruzar su
// umbral; a los 60 s el ciclo se reinicia.
const unsigned long AIREADOR_ON_MS = 20000UL; // aireador ON los primeros 20 s
const unsigned long BOMBA_ON_MS = 5000UL; // bomba ON los primeros 5 s
const unsigned long CICLO_TOTAL_MS = 60000UL; // el ciclo dura 60 s
// Marca de tiempo (ms desde el arranque) en la que empezó el ciclo actual.
// Es nuestro "cronómetro": el tiempo transcurrido es millis() - inicioCiclo.
unsigned long inicioCiclo = 0;
void setup() {
pinMode(PIN_RELE_AIREADOR, OUTPUT);
pinMode(PIN_RELE_BOMBA, OUTPUT);
// Estado inicial seguro: ambas cargas apagadas hasta la primera vuelta.
digitalWrite(PIN_RELE_AIREADOR, RELE_APAGADO);
digitalWrite(PIN_RELE_BOMBA, RELE_APAGADO);
inicioCiclo = millis();
}
void loop() {
// Tiempo transcurrido desde el inicio del ciclo. Con unsigned long, la
// resta funciona bien aun cuando millis() desborda (~cada 50 días).
unsigned long transcurrido = millis() - inicioCiclo;
// Cada carga decide su estado mirando el cronómetro. Nada de delay():
// esta función entra y sale al instante y las dos se atienden "a la vez".
digitalWrite(PIN_RELE_AIREADOR,
transcurrido < AIREADOR_ON_MS ? RELE_ENCENDIDO : RELE_APAGADO);
digitalWrite(PIN_RELE_BOMBA,
transcurrido < BOMBA_ON_MS ? RELE_ENCENDIDO : RELE_APAGADO);
// Cumplidos los 60 s, reiniciamos el cronómetro y el ciclo vuelve a cero.
if (transcurrido >= CICLO_TOTAL_MS) {
inicioCiclo = millis();
}
} Antes de cargarlo, acordate de ajustar los tiempos (AIREADOR_ON_MS, BOMBA_ON_MS) a los
que probaste en tu maqueta, sobre todo el de la bomba para que la batea no rebalse.
Podés ver el comportamiento completo —el ciclo de 60 segundos con el aireador y la bomba encendiéndose y apagándose en sus tiempos— 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.
¿Y si fueran tres?
En el nivel intermedio vamos a sumar un tercer actuador (el dosificador de alimento) al mismo
ciclo de 60 segundos. Discutan en el grupo: si tuviéramos que hacer esto con delay(), ¿por
qué se complicaría todavía más al agregar una tercera carga? ¿Y por qué con el cronómetro
alcanza con agregar una línea más? Anoten su respuesta: en el próximo nivel la van a poder
contrastar con el código.
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. Este es, además, el primer encuentro serio de la colección con el concepto de
código no bloqueante: conviene teatralizarlo (un estudiante “bloqueante” que se congela
contando hasta 40 sin poder responder, contra uno que mira el reloj y sigue atendiendo). Si
el curso ya hizo Semaforización o Tanques, es el mismo millis() que ya vieron.
Verificá
Con el programa cargado, mirá los LED de los dos módulos relé: al arrancar el ciclo, los dos encienden; a los 5 segundos se apaga el de la bomba; a los 20, el del aireador; y a los 60 el ciclo vuelve a empezar. Si tenés las cargas conectadas (de baja tensión), el aireador y la bomba tienen que seguir esos mismos tiempos. Probá primero en seco (sin agua) y recién después con la bomba sumergida.
Cierre del nivel
Al terminar este nivel, el sistema acuapónico recircula el agua y airea la pecera solo: la placa enciende y apaga el aireador y la bomba en un ciclo de 60 segundos, cada uno con su propio régimen, gracias a un cronómetro que evita el código bloqueante. Ese concepto —no detener el programa para poder atender varias cosas a la vez— es la base de todo lo que sigue. En el nivel intermedio vamos a sumar un tercer actuador: un dosificador de alimento impreso en 3D, movido por un motor, para que los peces coman sin que nadie tenga que estar presente.
Evidencia de cierre
Al terminar este nivel, esto tiene que estar funcionando:
- El aireador y la bomba están conectados, cada uno a su módulo relé, y los relés se comandan desde la placa (pines 2 y 3 en el UNO).
- La placa enciende y apaga el aireador (20 s sí / 40 s no) y la bomba (5 s sí / 55 s no) en un ciclo de 60 segundos que se repite solo.
- Podés explicar por qué dos cargas con ciclos distintos que funcionan a la vez no se pueden temporizar con delay() (código bloqueante) y cómo lo resuelve el cronómetro.
- 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.