Saltar al contenido
Volver a la ficha del proyecto

Manual del proyecto · Proyecto 14

Sistema Acuapónico

Automatización de un sistema acuapónico: un aireador oxigena la pecera, una bomba recircula el agua hacia las plantas y un dosificador impreso en 3D alimenta a los peces, todo comandado por tiempo con una placa Arduino o ESP32, con monitoreo remoto por IoT en el nivel avanzado.

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

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 Automatizar la recirculación del agua y la aireación Nivel inicial
  3. 03 Un dosificador de alimento impreso en 3D Nivel intermedio
  4. 04 Monitorear el sistema 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 el aireador y otro para la bomba, ambos del nivel inicial. Conmutan cargas de baja tensión (en el original, de 220 V): usá módulos con optoacoplador. Con ESP32, alimentá el módulo con 5 V desde VIN y compartí GND; la señal la toma del GPIO de 3,3 V. 2 Todas
Aireador (oxigenador) para peceras La carga que mantiene oxigenada el agua de la pecera. El original la conecta a 220 V; por seguridad, en el aula se usa una versión de baja tensión conmutada por el relé (ver desvíos). 1 Todas
Bomba de agua Recircula el agua desde la pecera hacia la batea de las plantas. Debe trabajar siempre sumergida: funcionar en seco la quema. Baja tensión en el aula; la de 220 V queda como extensión exclusiva del docente. 1 Todas
ULN2003
Motor DC con reducción Mueve el mecanismo del dosificador de alimento (nivel intermedio). La reducción le da fuerza para girar la pieza impresa. 1 Todas
ULN2003
Módulo motor ULN2003 Driver del motor del dosificador: un motor no puede conectarse directo a un pin de la placa. Es un componente nuevo respecto del proyecto de tanques. 1 Todas
Foto de Fuente de alimentación Fuente de baja tensión (12 V) para el aireador y la bomba Alimenta las cargas de baja tensión a través de los bornes de los relés. El original usaba directamente la red de 220 V; en esta reedición se trabaja con baja tensión (ver desvíos). 1 Todas
Foto de Protoboard Protoboard Para el cableado de control (señales de baja tensión). Las conexiones de potencia del relé van FUERA de la protoboard. 1 Todas
Rollo de filamento PLA Para imprimir en 3D las piezas del dosificador (nivel intermedio). 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 placa 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 Para llevar las señales de los módulos (relés, ULN2003) a los pines de la placa. 20 Todas
Foto de Cables dupont macho-macho Cable dupont macho-macho La ficha original listaba «20 cables dupont macho-hembra» DOS VECES; corregimos la segunda entrada a macho-macho, que son los que se necesitan para interconectar puntos sobre la protoboard (ver desvíos). 20 Todas
Cables eléctricos Para el cableado de las cargas (aireador y bomba) hacia los bornes de los relés y la fuente de baja tensión. según instalación Todas

Equipamiento del taller

  • Computadora
  • Impresora 3D (para las piezas del dosificador)

Plano de conexionado en protoboard

Arduino UNO Conexionado en protoboard · Sistema acuapónico: relé de aireador + relé de bomba + motor dosificador (ULN2003).

Deslizá horizontalmente para ver el plano completo

Arduino UNO
Relé aireador
RELAY 5V
INVCCGNDCOMNONC
Relé bomba
RELAY 5V
INVCCGNDCOMNONC
Motor dosificador (ULN2003)
ULN2003
IN1VCCGND
ESP32 DevKit v1 Conexionado en protoboard · Sistema acuapónico: relé de aireador + relé de bomba + motor dosificador (ULN2003).

Deslizá horizontalmente para ver el plano completo

ESP32 DevKit v1
Relé aireador
RELAY 5V
INVCCGNDCOMNONC
Relé bomba
RELAY 5V
INVCCGNDCOMNONC
Motor dosificador (ULN2003)
ULN2003
IN1VCCGND
Arduino UNOConexionado en protoboard
Conexiones · Arduino UNO. Convención de colores: alimentación VCC = rojo, GND = negro; señal IN del relé del aireador = azul, señal IN del relé de la bomba = verde, señal IN1 del ULN2003 del dosificador = amarillo; circuito de potencia (fuente aparte ↔ relé/ULN2003 ↔ carga), fuera de la protoboard = naranja.
Desde Hacia Color de cable
Arduino UNO · 5VBus + de la protoboardrojo
Arduino UNO · GNDBus − de la protoboardnegro
Relé aireador · VCCBus + de la protoboardrojo
Relé aireador · GNDBus − de la protoboardnegro
Relé aireador · INArduino UNO · 2azul
Fuente de carga (+)Relé aireador · COMnaranja
Relé aireador · NOAireadornaranja
Relé bomba · VCCBus + de la protoboardrojo
Relé bomba · GNDBus − de la protoboardnegro
Relé bomba · INArduino UNO · 3verde
Fuente de carga (+)Relé bomba · COMnaranja
Relé bomba · NOBomba sumergiblenaranja
ULN2003 · IN1Arduino UNO · 4amarillo
Fuente aparte del motor (+)ULN2003 · alimentación de motornaranja
Fuente aparte del motor (−)Bus − de la protoboardnaranja
ULN2003 · OUT1Motor DC del dosificadornaranja
ESP32 DevKit v1Conexionado en protoboard
Conexiones · ESP32 DevKit v1. Convención de colores: alimentación VCC = rojo, GND = negro; señal IN del relé del aireador = azul, señal IN del relé de la bomba = verde, señal IN1 del ULN2003 del dosificador = amarillo; circuito de potencia (fuente aparte ↔ relé/ULN2003 ↔ carga), fuera de la protoboard = naranja.
Desde Hacia Color de cable
ESP32 DevKit v1 · 3V3Bus + de la protoboardrojo
ESP32 DevKit v1 · GNDBus − de la protoboardnegro
Relé aireador · VCCBus + de la protoboardrojo
Relé aireador · GNDBus − de la protoboardnegro
Relé aireador · INESP32 DevKit v1 · GPIO25azul
Fuente de carga (+)Relé aireador · COMnaranja
Relé aireador · NOAireadornaranja
Relé bomba · VCCBus + de la protoboardrojo
Relé bomba · GNDBus − de la protoboardnegro
Relé bomba · INESP32 DevKit v1 · GPIO26verde
Fuente de carga (+)Relé bomba · COMnaranja
Relé bomba · NOBomba sumergiblenaranja
ULN2003 · IN1ESP32 DevKit v1 · GPIO27amarillo
Fuente aparte del motor (+)ULN2003 · alimentación de motornaranja
Fuente aparte del motor (−)Bus − de la protoboardnaranja
ULN2003 · OUT1Motor DC del dosificadornaranja
Nivel inicial

Automatizar la recirculación del agua y la aireación

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

Esquema de conexión: una placa Arduino UNO con los pines 5V y GND llevados al módulo relé (rotulado FE_SRly) y el pin digital 2 llevado a la entrada de señal del relé. En los bornes de salida del relé (NC/NO) se conecta el aireador, y entre el relé y la red se indica un enchufe de 220 V.
Conexión del aireador por relé en el nivel inicial (figura original del proyecto). La señal del relé va al pin 2 del Arduino; la carga se conmuta entre el enchufe y el aireador. En esta reedición ese enchufe es de baja tensión, no de 220 V.

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

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

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

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 bloqueante 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
acuaponico-temporizado.ino Arduino UNO
// 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();
  }
}
acuaponico-temporizado.ino ESP32
// 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();
  }
}
Control no bloqueante del aireador y la bomba: en cada vuelta el cronómetro decide el estado de cada relé, y a los 60 s el ciclo se reinicia.

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.

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

Un dosificador de alimento impreso en 3D

Imprimir las partes

El dosificador se fabrica con impresión 3D. Si querés modificar el modelo, sea cual sea el programa de modelado que uses, tenés que exportar las piezas en formato .stl: es el formato que abre el software de impresión (el laminador o slicer) para, desde ahí, generar el código G que la impresora ejecuta capa por capa.

Render 3D de una de las piezas del dosificador: una caja rectangular alargada, con un orificio circular en la cara superior (por donde entra el alimento) y una abertura circular grande en un extremo (donde va el motor), con perforaciones para los tornillos de sujeción.
Una de las piezas del dosificador para imprimir en 3D: el cuerpo que aloja el alimento y el motor (modelo original del proyecto).

Armar el mecanismo

Con las piezas listas, armamos el mecanismo del dosificador. La idea es que el motor DC con reducción haga girar la parte móvil del dosificador (un tambor o tornillo con alimento): en cada giro cae una porción de comida a la pecera. El motor se aloja en el cuerpo impreso y se fija de modo que su eje mueva la pieza dosificadora.

La reducción del motor (su caja de engranajes) es importante acá: baja la velocidad y aumenta la fuerza, que es justo lo que necesita un mecanismo que empuja alimento, no algo que gire rápido.

Conectar el motor del dosificador a la placa

Acá aparece un componente nuevo. El motor no se puede conectar directo a un pin de la placa: un motor consume más corriente de la que un pin es capaz de entregar. Por eso usamos un ULN2003, un circuito integrado que actúa de intermediario: recibe la señal débil del pin en su entrada IN1 y, con ella, deja pasar hacia el motor la corriente que necesita, tomada de una fuente aparte.

Lo usamos como un simple interruptor de motor: con el pin en alto (HIGH) el motor gira y dosifica; en bajo (LOW), se detiene. El cableado es: pin de la placa → IN1 del ULN2003, salida OUT1 → un borne del motor, el otro borne del motor → la fuente del motor, y la masa (GND) en común entre la placa, el ULN2003 y la fuente.

Esquema de conexión completo: una placa Arduino UNO conectada a dos módulos relé (rotulados aireador y bomba, cada uno con su carga y un enchufe de 220 V) y a un módulo ULN2003 que maneja un motor DC. Las señales de los relés y del ULN2003 van a pines digitales de la placa; el motor se conecta a las salidas del ULN2003.
Sistema completo del nivel intermedio (figura original del proyecto): los relés del aireador y la bomba más el ULN2003 que mueve el motor del dosificador. La señal del ULN2003 va al pin 4 del Arduino.

En el Arduino UNO, siguiendo el esquema original: el relé del aireador va al pin 2 y el de la bomba al pin 3 (como en el nivel inicial), y la entrada IN1 del ULN2003 al pin 4 (la constante PIN_DOSIFICADOR del código).

En el ESP32, los relés del aireador y la bomba van a GPIO 25 y GPIO 26, y la entrada IN1 del ULN2003 a GPIO 27 (la constante PIN_DOSIFICADOR). Recordá alimentar los módulos relé con 5 V (VCC al 5V/VIN) y compartir GND; la señal la toman del GPIO de 3,3 V.

¡Atención! La advertencia de seguridad del nivel inicial sigue vigente: el aireador y la bomba son cargas que pueden ser de 220 V, con el relé fuera de la protoboard y las conexiones lejos y por encima del agua; la bomba nunca en seco. El motor del dosificador, en cambio, es de baja tensión y no maneja 220 V.

Programar los tiempos del dosificador

Queremos que el dosificador se encienda 1 segundo por cada ciclo de 60 segundos y que se mantenga lo que ya programamos en el nivel inicial. Los tres regímenes quedan así:

  • Aireador: 20 segundos encendido y 40 segundos apagado.
  • Bomba: 5 segundos encendida y 55 segundos apagada.
  • Dosificador: 1 segundo encendido y 59 segundos apagado.

Acá se ve la ventaja de haber resuelto el código sin bloqueos en el nivel inicial: como el programa decide el estado de cada actuador mirando el mismo cronómetro, sumar el dosificador es agregar una sola línea (¿en qué momento del ciclo tiene que estar encendido?), sin tocar la lógica del aireador ni de la bomba.

Ver el código completo · acuaponico-dosificador.ino
acuaponico-dosificador.ino Arduino UNO
// Sistema acuapónico automatizado — nivel intermedio (dosificador de
// alimento). Se suma al aireador y la bomba un tercer actuador: un motor
// DC con reducción que hace girar un dosificador impreso en 3D para
// alimentar a los peces. Todo dentro del mismo ciclo de 60 s, resuelto con
// el mismo CRONÓMETRO no bloqueante del nivel inicial:
//   - Aireador de la pecera : 20 s encendido / 40 s apagado.
//   - Bomba de recirculación:  5 s encendida / 55 s apagada.
//   - Dosificador (motor)   :  1 s encendido / 59 s apagado.
//
// COMPONENTE NUEVO — ULN2003: el motor del dosificador NO puede conectarse
// directo a un pin del Arduino, porque un motor pide más corriente de la
// que un pin puede entregar. El ULN2003 es el "amplificador" intermedio
// (un arreglo de transistores Darlington): recibe la señal débil del pin
// y con ella deja pasar la corriente que el motor necesita desde una
// fuente aparte. Acá lo usamos como un simple INTERRUPTOR de motor
// UNIDIRECCIONAL (encendido/apagado): mientras el pin está en HIGH, el
// motor gira en un sentido y dosifica; en LOW, se detiene. Se cablea la
// entrada IN1 del ULN2003 al pin del Arduino, la salida OUT1 a un borne
// del motor, el otro borne del motor a la fuente, y la masa (GND) común.
//
// SEGURIDAD (220 V + agua): el aireador y la bomba son cargas de 220 V
// conmutadas por relé. En el aula se trabaja con 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 las conexiones eléctricas fuera y por
// encima del nivel del agua. La BOMBA NO DEBE FUNCIONAR EN SECO: se quema.
// (El motor del dosificador es de baja tensión y no maneja 220 V.)
//
// Plataforma: Arduino UNO.
// Conexión (contrato de la reedición): relé del aireador → pin 2, relé de
// la bomba → pin 3, entrada IN1 del ULN2003 (motor del dosificador) → pin 4.
//
// Basado en: Fragmento de código 3 (Nivel Intermedio) — Sistema Acuapónico
// Automatizado, Saberes Digitales (INET/EDUCAR 2019).
// Modernización: C++ idiomático con constantes nombradas en vez de mBlock3;
// aritmética de unsigned long en ms (el original llevaba la cuenta con un
// double de millis()/1000.0, con riesgo de desborde en la resta).

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

// Entrada de control del ULN2003 que mueve el motor del dosificador.
const int PIN_DOSIFICADOR = 4;

// Módulos relé activo-alto (HIGH = carga encendida). El ULN2003 también
// activa el motor con HIGH en su entrada, así que compartimos convención.
const int ENCENDIDO = HIGH;
const int APAGADO   = LOW;

// Umbrales de encendido dentro del ciclo (los del ejemplo original). Los
// tres actuadores arrancan encendidos al comenzar el ciclo y se apagan 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 DOSIFICADOR_ON_MS = 1000UL;  // dosificador ON el primer 1 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);
  pinMode(PIN_DOSIFICADOR, OUTPUT);

  // Estado inicial seguro: todo apagado hasta la primera vuelta del loop.
  digitalWrite(PIN_RELE_AIREADOR, APAGADO);
  digitalWrite(PIN_RELE_BOMBA, APAGADO);
  digitalWrite(PIN_DOSIFICADOR, 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 actuador decide su estado según el cronómetro. Nada de delay():
  // esta función entra y sale al instante, así que los tres se atienden
  // "a la vez" (y queda tiempo de procesador para el IoT del ESP32).
  digitalWrite(PIN_RELE_AIREADOR,
               transcurrido < AIREADOR_ON_MS ? ENCENDIDO : APAGADO);
  digitalWrite(PIN_RELE_BOMBA,
               transcurrido < BOMBA_ON_MS ? ENCENDIDO : APAGADO);
  digitalWrite(PIN_DOSIFICADOR,
               transcurrido < DOSIFICADOR_ON_MS ? ENCENDIDO : APAGADO);

  // Cumplidos los 60 s, reiniciamos el cronómetro y el ciclo vuelve a cero.
  if (transcurrido >= CICLO_TOTAL_MS) {
    inicioCiclo = millis();
  }
}
acuaponico-dosificador.ino ESP32
// Sistema acuapónico automatizado — nivel intermedio (dosificador de
// alimento). Se suma al aireador y la bomba un tercer actuador: un motor
// DC con reducción que mueve un dosificador impreso en 3D para alimentar
// a los peces. Todo dentro del mismo ciclo de 60 s, resuelto con el mismo
// CRONÓMETRO no bloqueante del nivel inicial:
//   - Aireador de la pecera : 20 s encendido / 40 s apagado.
//   - Bomba de recirculación:  5 s encendida / 55 s apagada.
//   - Dosificador (motor)   :  1 s encendido / 59 s apagado.
//
// COMPONENTE NUEVO — ULN2003: el motor del dosificador NO puede conectarse
// directo a un pin del ESP32, porque un motor pide más corriente de la que
// un pin puede entregar. El ULN2003 es el "amplificador" intermedio (un
// arreglo de transistores Darlington): recibe la señal débil del pin y con
// ella deja pasar la corriente que el motor necesita desde una fuente
// aparte. Acá lo usamos como interruptor de motor UNIDIRECCIONAL
// (encendido/apagado): con el pin en HIGH el motor gira y dosifica, en LOW
// se detiene. Cableado: pin del ESP32 → IN1 del ULN2003, OUT1 → un borne
// del motor, el otro borne del motor → fuente, y GND común.
//
// SEGURIDAD (220 V + agua): aireador y bomba son cargas de 220 V por relé.
// En el aula, BAJA TENSIÓN; los 220 V solo el docente / instalación
// supervisada, nunca sobre la protoboard, conexiones fuera y por encima
// del agua. La BOMBA NO DEBE FUNCIONAR EN SECO: se quema. (El motor del
// dosificador es de baja tensión.)
//
// Plataforma: ESP32 (DevKit v1).
// Conexión (contrato de la reedición): relé del aireador → GPIO 25, relé
// de la bomba → GPIO 26, entrada IN1 del ULN2003 (dosificador) → GPIO 27.
// NOTA ELÉCTRICA: los módulos relé se alimentan con 5 V (VCC al 5V/VIN),
// GND común, y la señal la toman del GPIO de 3,3 V. Para 220 V, usar
// módulos con optoacoplador.
//
// Basado en: Fragmento de código 3 (Nivel Intermedio) — Sistema Acuapónico
// Automatizado, Saberes Digitales (INET/EDUCAR 2019).
// Modernización: C++ idiomático en vez de mBlock3; portado del UNO
// (pines 2/3/4) al ESP32 (GPIO 25/26/27); aritmética de unsigned long en ms.

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

// Entrada de control del ULN2003 que mueve el motor del dosificador.
const int PIN_DOSIFICADOR = 27;

// Módulos relé activo-alto (HIGH = carga encendida). El ULN2003 también
// activa el motor con HIGH en su entrada, así que compartimos convención.
const int ENCENDIDO = HIGH;
const int APAGADO   = LOW;

// Umbrales de encendido dentro del ciclo (los del ejemplo original). Los
// tres actuadores arrancan encendidos al comenzar el ciclo y se apagan 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 DOSIFICADOR_ON_MS = 1000UL;  // dosificador ON el primer 1 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);
  pinMode(PIN_DOSIFICADOR, OUTPUT);

  // Estado inicial seguro: todo apagado hasta la primera vuelta del loop.
  digitalWrite(PIN_RELE_AIREADOR, APAGADO);
  digitalWrite(PIN_RELE_BOMBA, APAGADO);
  digitalWrite(PIN_DOSIFICADOR, 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 actuador decide su estado según el cronómetro. Nada de delay():
  // esta función entra y sale al instante, así que los tres se atienden
  // "a la vez" (y queda tiempo de procesador para el IoT del ESP32).
  digitalWrite(PIN_RELE_AIREADOR,
               transcurrido < AIREADOR_ON_MS ? ENCENDIDO : APAGADO);
  digitalWrite(PIN_RELE_BOMBA,
               transcurrido < BOMBA_ON_MS ? ENCENDIDO : APAGADO);
  digitalWrite(PIN_DOSIFICADOR,
               transcurrido < DOSIFICADOR_ON_MS ? ENCENDIDO : APAGADO);

  // Cumplidos los 60 s, reiniciamos el cronómetro y el ciclo vuelve a cero.
  if (transcurrido >= CICLO_TOTAL_MS) {
    inicioCiclo = millis();
  }
}
El sistema con dosificador: el mismo control no bloqueante del nivel inicial suma el motor (vía ULN2003), encendido 1 s por ciclo.

Podés ver el sistema completo funcionando —el aireador, la bomba y el dosificador en sus tiempos dentro del ciclo de 60 segundos— 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, el sistema ya se cuida solo también en la comida: además de recircular el agua y airear la pecera, un dosificador impreso en 3D y movido por un motor libera una porción de alimento una vez por minuto. En el camino apareció un componente clave —el ULN2003, que le da al motor la corriente que un pin no puede— y quedó claro por qué el código no bloqueante del nivel inicial se paga solo: sumar un tercer actuador fue trivial. En el nivel avanzado vamos a llevar todo esto a Internet: monitorear a distancia, desde un celular, si el aireador, la bomba y el dosificador están funcionando.

Evidencia de cierre

Al terminar este nivel, esto tiene que estar funcionando:

  • Imprimiste en 3D las piezas del dosificador y armaste el mecanismo con el motor DC.
  • El motor del dosificador está conectado a la placa a través del módulo ULN2003 (entrada IN1 al pin 4 en el UNO), con su fuente aparte y la masa en común.
  • La placa suma el dosificador al ciclo de 60 s: se enciende 1 segundo y se apaga 59, sin alterar los tiempos del aireador y la bomba.
  • Podés explicar por qué el motor no se conecta directo a un pin y qué hace el ULN2003 en el medio.
Nivel avanzado

Monitorear el sistema 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, monitorear la temperatura de un ambiente enviando los datos de un sensor a una central donde se recopilan; así podría verse desde un celular la temperatura de un laboratorio, de un invernadero o de una sala.

Para incorporar IoT a nuestro proyecto es necesario:

  1. Un dispositivo capaz de conectarse a internet.
  2. Un servidor que reciba y aloje los datos.

Para el servidor usaremos Adafruit IO, una plataforma online de uso gratuito orientada a lo educativo.

El material original de 2019 resolvía la conexión 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: podés seguir este nivel de manera conceptual con tu UNO y cambiar al ESP32 con el selector de plataforma para la parte práctica, 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á comandando el aireador, la bomba y el dosificador, 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

Vamos a crear un panel de control en Adafruit IO y a vincular sus controles con los datos que manda el dispositivo. Primero creamos una cuenta en io.adafruit.com.

El trabajo tiene dos partes: crear los feeds donde se publican los datos y armar el panel que los muestra. Un feed es una fuente de datos en la que uno puede publicar y a la que también se puede suscribir para recibir sus datos. Desde la sección Feeds creamos tres, con estos nombres exactos (sin tildes ni Ñ, porque forman parte de la dirección con la que se publica):

  • aireador, bomba y dosificador: cada uno recibirá el estado de su actuador, con la convención 1 = encendido y 0 = apagado.

Luego, desde la sección Dashboards, creamos un panel nuevo y le agregamos tres bloques Indicator (luz virtual), uno por feed, con la condición “igual a 1”: el indicador se enciende cuando el actuador está funcionando y se apaga cuando no. Cada bloque permite elegir título y colores, y se puede ajustar su tamaño y ubicación. Al terminar, el panel replicará a distancia si el aireador, la bomba y el dosificador están encendidos o apagados.

Por último, necesitamos las credenciales que le permitirán al dispositivo publicar: en Adafruit IO, tocá el ícono de la llave (API Key). Vas a ver el nombre de usuario (Username) y la clave (Active Key): anotalos, porque los vamos a copiar en el código en el paso 4. La clave es secreta: cualquiera que la tenga puede publicar en tus feeds.

Preparar el código no bloqueante como base del IoT

Antes de publicar, conviene mirar el código con la vista puesta en internet. El control del sistema ya es no bloqueante desde el nivel inicial (usa un cronómetro, no delay()), y eso es justamente lo que hace posible el IoT: si el programa se detuviera esperando, no podría mantener viva la conexión ni publicar. Por eso partimos de esa base.

Lo único que agregamos ahora es llevar el estado de cada actuador en una variable (estadoAireador, estadoBomba, estadoDosificador; 0 o 1). Todavía no publicamos nada, pero ese registro es la pieza que permite, en el paso siguiente, publicar solo los cambios: enviar a Adafruit “encendió” o “apagó” en el momento en que ocurre, en vez de repetir el mismo dato una y otra vez.

Ver el código completo · acuaponico-no-bloqueante.ino
acuaponico-no-bloqueante.ino Arduino UNO
// Sistema acuapónico automatizado — nivel avanzado (base para IoT).
// Mismo comportamiento y mismos regímenes de los niveles anteriores, con el
// mismo CRONÓMETRO no bloqueante, dentro de un ciclo de 60 s:
//   - Aireador de la pecera : 20 s encendido / 40 s apagado.
//   - Bomba de recirculación:  5 s encendida / 55 s apagada.
//   - Dosificador (motor)   :  1 s encendido / 59 s apagado.
//
// ¿QUÉ CAMBIA RESPECTO DEL NIVEL INTERMEDIO? El comportamiento es idéntico.
// Lo único nuevo es que ahora guardamos el estado de cada actuador en una
// variable (estadoAireador, estadoBomba, estadoDosificador; 0 = apagado,
// 1 = encendido). Acá esas variables "no hacen falta" para prender los
// relés... pero son la pieza clave del paso siguiente: en la variante ESP32
// (acuaponico-iot.ino) permiten publicar a internet SOLO cuando el estado
// CAMBIA, en vez de repetir el mismo dato una y otra vez. Este sketch es esa
// base, todavía SIN internet. En el Arduino UNO, que no tiene WiFi, este es
// el punto final del proyecto; el monitoreo IoT se realiza con el ESP32.
//
// COMPONENTE ULN2003: el motor del dosificador se maneja por un ULN2003
// (arreglo Darlington) que amplifica la señal del pin para dar al motor la
// corriente que necesita. HIGH en la entrada IN1 = gira y dosifica, LOW =
// detenido.
//
// SEGURIDAD (220 V + agua): aireador y bomba son cargas de 220 V por relé.
// En el aula, BAJA TENSIÓN; los 220 V solo el docente / instalación
// supervisada, nunca sobre la protoboard, conexiones fuera y por encima
// del agua. La BOMBA NO DEBE FUNCIONAR EN SECO: se quema.
//
// Plataforma: Arduino UNO.
// Conexión (contrato de la reedición): relé del aireador → pin 2, relé de
// la bomba → pin 3, entrada IN1 del ULN2003 (dosificador) → pin 4.
//
// Basado en: Fragmento de código 4 (Nivel Avanzado, sin la parte IoT) —
// Sistema Acuapónico Automatizado, Saberes Digitales (INET/EDUCAR 2019).
// Modernización: C++ idiomático en vez de mBlock3; aritmética de unsigned
// long en ms. Se conserva del original la idea de llevar el estado de cada
// carga en una variable (allá servía para publicar; acá deja el terreno
// listo para el IoT del ESP32).

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

// Entrada de control del ULN2003 que mueve el motor del dosificador.
const int PIN_DOSIFICADOR = 4;

// Relés / entrada del ULN2003 activos en alto (HIGH = actuador andando).
// Si tu módulo relé es activo-bajo, invertí solo estas dos constantes.
const int ENCENDIDO = HIGH;
const int APAGADO   = LOW;

// Umbrales de encendido dentro del ciclo (los del ejemplo original).
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 DOSIFICADOR_ON_MS = 1000UL;  // dosificador ON el primer 1 s
const unsigned long CICLO_TOTAL_MS    = 60000UL; // el ciclo dura 60 s

// Marca de tiempo (ms) en la que empezó el ciclo actual: es el cronómetro.
unsigned long inicioCiclo = 0;

// Estado actual de cada actuador (0 = apagado, 1 = encendido). Es el valor
// que la versión IoT publicará —solo cuando cambie— a Adafruit.
int estadoAireador    = 0;
int estadoBomba       = 0;
int estadoDosificador = 0;

void setup() {
  pinMode(PIN_RELE_AIREADOR, OUTPUT);
  pinMode(PIN_RELE_BOMBA, OUTPUT);
  pinMode(PIN_DOSIFICADOR, OUTPUT);

  // Estado inicial seguro: todo apagado (coincide con las variables en 0).
  digitalWrite(PIN_RELE_AIREADOR, APAGADO);
  digitalWrite(PIN_RELE_BOMBA, APAGADO);
  digitalWrite(PIN_DOSIFICADOR, 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;

  // El cronómetro define el estado que le toca a cada actuador...
  estadoAireador    = (transcurrido < AIREADOR_ON_MS) ? 1 : 0;
  estadoBomba       = (transcurrido < BOMBA_ON_MS) ? 1 : 0;
  estadoDosificador = (transcurrido < DOSIFICADOR_ON_MS) ? 1 : 0;

  // ...y ese estado se refleja en los pines (nada de delay(): el loop entra
  // y sale al instante, listo para atender otras tareas —el IoT del ESP32—).
  digitalWrite(PIN_RELE_AIREADOR, estadoAireador ? ENCENDIDO : APAGADO);
  digitalWrite(PIN_RELE_BOMBA, estadoBomba ? ENCENDIDO : APAGADO);
  digitalWrite(PIN_DOSIFICADOR, estadoDosificador ? ENCENDIDO : APAGADO);

  // Cumplidos los 60 s, reiniciamos el cronómetro y el ciclo vuelve a cero.
  if (transcurrido >= CICLO_TOTAL_MS) {
    inicioCiclo = millis();
  }
}
acuaponico-no-bloqueante.ino ESP32
// Sistema acuapónico automatizado — nivel avanzado (base para IoT).
// Mismo comportamiento y mismos regímenes de los niveles anteriores, con el
// mismo CRONÓMETRO no bloqueante, dentro de un ciclo de 60 s:
//   - Aireador de la pecera : 20 s encendido / 40 s apagado.
//   - Bomba de recirculación:  5 s encendida / 55 s apagada.
//   - Dosificador (motor)   :  1 s encendido / 59 s apagado.
//
// ¿QUÉ CAMBIA RESPECTO DEL NIVEL INTERMEDIO? El comportamiento es idéntico.
// Lo único nuevo es que ahora guardamos el estado de cada actuador en una
// variable (estadoAireador, estadoBomba, estadoDosificador; 0 = apagado,
// 1 = encendido). Acá esas variables "no hacen falta" para prender los
// relés... pero son la pieza clave del paso siguiente: en acuaponico-iot.ino
// permiten publicar a internet SOLO cuando el estado CAMBIA, en vez de
// repetir el mismo dato una y otra vez. Este sketch es esa base, todavía
// SIN la parte de red: agregarle WiFi + Adafruit IO es el último paso.
//
// COMPONENTE ULN2003: el motor del dosificador se maneja por un ULN2003
// (arreglo Darlington) que amplifica la señal del pin para dar al motor la
// corriente que necesita. Interruptor de motor unidireccional: HIGH = gira
// y dosifica, LOW = detenido.
//
// SEGURIDAD (220 V + agua): aireador y bomba son cargas de 220 V por relé.
// En el aula, BAJA TENSIÓN; los 220 V solo el docente / instalación
// supervisada, nunca sobre la protoboard, conexiones fuera y por encima
// del agua. La BOMBA NO DEBE FUNCIONAR EN SECO: se quema.
//
// Plataforma: ESP32 (DevKit v1).
// Conexión (contrato de la reedición): relé del aireador → GPIO 25, relé
// de la bomba → GPIO 26, entrada IN1 del ULN2003 (dosificador) → GPIO 27.
// NOTA ELÉCTRICA: módulos relé alimentados con 5 V, GND común, señal desde
// el GPIO de 3,3 V; para 220 V, módulos con optoacoplador.
//
// Basado en: Fragmento de código 4 (Nivel Avanzado, sin la parte IoT) —
// Sistema Acuapónico Automatizado, Saberes Digitales (INET/EDUCAR 2019).
// Modernización: (1) C++ idiomático en vez de mBlock3; portado a ESP32.
// (2) El original contaba con un double de millis()/1000.0; acá usamos
// aritmética de unsigned long en milisegundos (más precisa y sin el riesgo
// de desborde de la resta que arrastraba el patrón 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;

// Entrada de control del ULN2003 que mueve el motor del dosificador.
const int PIN_DOSIFICADOR = 27;

// Módulos relé / entrada del ULN2003 activos en alto (HIGH = actuador andando).
const int ENCENDIDO = HIGH;
const int APAGADO   = LOW;

// Umbrales de encendido dentro del ciclo (los del ejemplo original).
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 DOSIFICADOR_ON_MS = 1000UL;  // dosificador ON el primer 1 s
const unsigned long CICLO_TOTAL_MS    = 60000UL; // el ciclo dura 60 s

// Marca de tiempo (ms) en la que empezó el ciclo actual: es el cronómetro.
unsigned long inicioCiclo = 0;

// Estado actual de cada actuador (0 = apagado, 1 = encendido). Es el valor
// que la versión IoT publicará —solo cuando cambie— a Adafruit.
int estadoAireador    = 0;
int estadoBomba       = 0;
int estadoDosificador = 0;

void setup() {
  pinMode(PIN_RELE_AIREADOR, OUTPUT);
  pinMode(PIN_RELE_BOMBA, OUTPUT);
  pinMode(PIN_DOSIFICADOR, OUTPUT);

  // Estado inicial seguro: todo apagado hasta la primera vuelta del loop.
  digitalWrite(PIN_RELE_AIREADOR, APAGADO);
  digitalWrite(PIN_RELE_BOMBA, APAGADO);
  digitalWrite(PIN_DOSIFICADOR, 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;

  // El cronómetro define el estado que le toca a cada actuador...
  estadoAireador    = (transcurrido < AIREADOR_ON_MS) ? 1 : 0;
  estadoBomba       = (transcurrido < BOMBA_ON_MS) ? 1 : 0;
  estadoDosificador = (transcurrido < DOSIFICADOR_ON_MS) ? 1 : 0;

  // ...y ese estado se refleja en los pines (nada de delay(): el loop entra
  // y sale al instante y queda libre para atender otras tareas —el IoT—).
  digitalWrite(PIN_RELE_AIREADOR, estadoAireador ? ENCENDIDO : APAGADO);
  digitalWrite(PIN_RELE_BOMBA, estadoBomba ? ENCENDIDO : APAGADO);
  digitalWrite(PIN_DOSIFICADOR, estadoDosificador ? ENCENDIDO : APAGADO);

  // Cumplidos los 60 s, reiniciamos el cronómetro y el ciclo vuelve a cero.
  if (transcurrido >= CICLO_TOTAL_MS) {
    inicioCiclo = millis();
  }
}
La base para el IoT: el mismo control no bloqueante de los niveles anteriores, ahora con una variable de estado por actuador. Todavía sin internet.

En el Arduino UNO, este es el punto final del proyecto: el UNO no tiene WiFi, así que la parte práctica de publicación se hace con el ESP32. Todo lo conceptual de los pasos siguientes —qué se publica, la convención de valores, por qué el código no bloqueante importa— lo podés seguir igual con tu UNO.

Programar la publicación a Adafruit IO

Esta parte práctica corresponde a la variante ESP32: el Arduino UNO clásico no tiene WiFi (el original lo resolvía con el módulo OBLOQ, hoy discontinuado). Cambiá la plataforma a ESP32 con el selector para ver el código y las instrucciones. Todo lo conceptual 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, completá 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 control no bloqueante le deja libre al procesador: en cada vuelta de loop(), además de comandar el aireador, la bomba y el dosificador, el programa mantiene viva la conexión con el servidor y publica los cambios: el estado (1 o 0) de cada actuador cada vez que conmuta. Como cada actuador cambia como mucho dos veces por ciclo de 60 s, son unas pocas publicaciones por minuto, muy por debajo del límite de la cuenta gratuita de Adafruit IO. El IoT del original ya estaba resuelto así (publicaba al conmutar) y se mantiene ese criterio. Por esto es importante que el código no sea bloqueante: si quedara colgado en un delay(), la conexión no podría atenderse y las publicaciones se perderían. Bonus del mismo diseño: si internet se cae, el sistema sigue funcionando, y al volver la conexión el panel se pone al día solo.

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

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

acuaponico-iot.ino ESP32
// Sistema acuapónico automatizado — nivel avanzado con monitoreo IoT.
// La lógica NO bloqueante de acuaponico-no-bloqueante.ino sigue manejando
// el aireador, la bomba y el dosificador; además se publican tres feeds a
// Adafruit IO para verlos desde el Panel de Control (tres Indicator, como
// en la guía):
//   "aireador", "bomba", "dosificador": 0 = apagado, 1 = encendido.
//
// PUBLICACIÓN POR CAMBIO (event-driven): NO publicamos en cada vuelta del
// loop. Cada feed se publica SOLO cuando su estado cambia respecto de lo
// último que se envió. Así el envío es económico y respeta el límite del
// plan gratuito de Adafruit IO (30 publicaciones/min): cada actuador
// cambia como mucho dos veces por ciclo de 60 s (enciende y apaga), o sea
// 3 actuadores x 2 = 6 publicaciones/min en total, muy por debajo del tope.
// El código IoT del original ya estaba resuelto así (publica al conmutar);
// se mantiene ese criterio.
//
// COMPONENTE ULN2003: el motor del dosificador se maneja por un ULN2003
// (arreglo Darlington) que amplifica la señal del pin. Interruptor de motor
// unidireccional: HIGH = gira y dosifica, LOW = detenido.
//
// SEGURIDAD (220 V + agua): aireador y bomba son cargas de 220 V por relé.
// En el aula, BAJA TENSIÓN; los 220 V solo el docente / instalación
// supervisada, nunca sobre la protoboard, conexiones fuera y por encima
// del agua. La BOMBA NO DEBE FUNCIONAR EN SECO: se quema.
//
// Plataforma: ESP32 (DevKit v1) — usa su WiFi integrado.
// Conexión (contrato de la reedición): relé del aireador → GPIO 25, relé
// de la bomba → GPIO 26, entrada IN1 del ULN2003 (dosificador) → GPIO 27.
// NOTA ELÉCTRICA: módulos relé alimentados con 5 V, GND común, señal desde
// el GPIO de 3,3 V; para 220 V, módulos con optoacoplador.
//
// Librería requerida (Library Manager del IDE de Arduino):
//   "Adafruit MQTT Library" (de Adafruit) — misma elección que en el resto
//   de la colección: cada feed es un objeto y publish() acepta números
//   directamente, igual que el olq.publish() del original.
//
// Basado en: Fragmento de código del Nivel Avanzado (IoT) — Sistema
// Acuapónico Automatizado, Saberes Digitales (INET/EDUCAR 2019).
// Modernización: 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 con la Adafruit MQTT Library. Se conserva la
// publicación por cambio de estado del original (event-driven).

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

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

// Entrada de control del ULN2003 que mueve el motor del dosificador.
const int PIN_DOSIFICADOR = 27;

// Módulos relé / entrada del ULN2003 activos en alto (HIGH = actuador andando).
const int ENCENDIDO = HIGH;
const int APAGADO   = LOW;

// Umbrales de encendido dentro del ciclo (los del ejemplo original).
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 DOSIFICADOR_ON_MS = 1000UL;  // dosificador ON el primer 1 s
const unsigned long CICLO_TOTAL_MS    = 60000UL; // el ciclo dura 60 s

// Si se cae la conexión MQTT, reintentamos cada tanto SIN bloquear: el
// control del acuapónico debe seguir aunque no haya internet.
const unsigned long INTERVALO_REINTENTO_MQTT_MS = 5000;

// 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 feedAireador(&mqtt, IO_USERNAME "/feeds/aireador");
Adafruit_MQTT_Publish feedBomba(&mqtt, IO_USERNAME "/feeds/bomba");
Adafruit_MQTT_Publish feedDosificador(&mqtt, IO_USERNAME "/feeds/dosificador");

// Marca de tiempo (ms) en la que empezó el ciclo actual: es el cronómetro.
unsigned long inicioCiclo = 0;
// Último intento de (re)conexión MQTT.
unsigned long millisUltimoIntentoMqtt = 0;

// Estado ACTUAL de cada actuador (lo que muestran los relés y el motor)...
int estadoAireador    = 0; // 0 = apagado, 1 = encendido
int estadoBomba       = 0;
int estadoDosificador = 0;

// ...y lo ÚLTIMO que llegó a publicarse en cada feed. Separarlos permite
// publicar solo los CAMBIOS y, si la conexión estaba caída, ponerse al día
// al reconectar (-1 = todavía no se publicó nada).
int aireadorPublicado    = -1;
int bombaPublicado       = -1;
int dosificadorPublicado = -1;

// Mantiene viva la conexión MQTT sin bloquear el control: 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 un feed SOLO si su estado cambió desde la última publicación
// exitosa. Si publish() falla, no se actualiza el registro: se reintenta
// en la próxima vuelta. Devuelve nada; actualiza *publicado por referencia.
void publicarSiCambio(Adafruit_MQTT_Publish &feed, int estado, int &publicado) {
  if (estado != publicado && feed.publish((int32_t)estado)) {
    publicado = estado;
  }
}

// Envía a Adafruit los cambios de estado (event-driven). Si no hay conexión
// no publica: el control local sigue y la central se pone al día al volver.
void publicarCambios() {
  if (!mqtt.connected()) {
    return;
  }
  publicarSiCambio(feedAireador, estadoAireador, aireadorPublicado);
  publicarSiCambio(feedBomba, estadoBomba, bombaPublicado);
  publicarSiCambio(feedDosificador, estadoDosificador, dosificadorPublicado);
}

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

  pinMode(PIN_RELE_AIREADOR, OUTPUT);
  pinMode(PIN_RELE_BOMBA, OUTPUT);
  pinMode(PIN_DOSIFICADOR, OUTPUT);

  // Estado inicial seguro: todo apagado hasta la primera vuelta del loop.
  digitalWrite(PIN_RELE_AIREADOR, APAGADO);
  digitalWrite(PIN_RELE_BOMBA, APAGADO);
  digitalWrite(PIN_DOSIFICADOR, APAGADO);

  // Conexión WiFi. Esperar acá con delay() es aceptable porque el control
  // 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());

  inicioCiclo = millis();
}

void loop() {
  // --- Control no bloqueante (idéntico a acuaponico-no-bloqueante.ino) ---
  unsigned long transcurrido = millis() - inicioCiclo;

  estadoAireador    = (transcurrido < AIREADOR_ON_MS) ? 1 : 0;
  estadoBomba       = (transcurrido < BOMBA_ON_MS) ? 1 : 0;
  estadoDosificador = (transcurrido < DOSIFICADOR_ON_MS) ? 1 : 0;

  digitalWrite(PIN_RELE_AIREADOR, estadoAireador ? ENCENDIDO : APAGADO);
  digitalWrite(PIN_RELE_BOMBA, estadoBomba ? ENCENDIDO : APAGADO);
  digitalWrite(PIN_DOSIFICADOR, estadoDosificador ? ENCENDIDO : APAGADO);

  if (transcurrido >= CICLO_TOTAL_MS) {
    inicioCiclo = millis();
  }

  // --- Parte IoT ---
  // Gracias a que el control no bloquea, estas dos tareas se atienden en
  // CADA vuelta del loop. publicarCambios() solo envía cuando un estado
  // cambió, así que el tráfico real es de unas pocas publicaciones por
  // minuto (event-driven).
  mantenerConexionMqtt();
  publicarCambios();
}
El sistema acuapónico IoT completo: control no bloqueante + WiFi del ESP32 + publicación del estado de los tres actuadores en Adafruit IO, solo cuando cambian.

Monitorear el sistema acuapónico a distancia

Con el programa cargado y el ESP32 conectado a tu red WiFi, abrí el panel de Adafruit IO en el celular o la computadora: los tres Indicator (aireador, bomba y dosificador) tienen que encenderse y apagarse siguiendo el ciclo de 60 segundos, con una pequeña demora de red. Así queda funcionando el monitoreo remoto que buscaban los alumnos: pueden ver, desde cualquier lado, si el sistema está haciendo su trabajo.

Podés ver el sistema completo —los tres actuadores trabajando más el panel reflejando cada cambio de estado— 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, el sistema acuapónico dejó de ser solo un dispositivo local: es un dispositivo IoT. El programa comanda el aireador, la bomba y el dosificador con un cronómetro no bloqueante, y en el tiempo que ese diseño le deja libre mantiene viva la conexión y publica cada cambio de estado en Adafruit IO, donde un panel lo muestra desde cualquier celular. Con esto queda completo el recorrido del proyecto. Si el grupo quiere seguir, en la ficha está la extensión sugerida: sumar sensores de calidad del agua (temperatura, turbiedad, oxígeno, pH) y publicar también esos datos, para cuidar mejor la vida del sistema.

Evidencia de cierre

Al terminar este nivel, esto tiene que estar funcionando:

  • El código del sistema lleva el estado de cada actuador (aireador, bomba, dosificador) en una variable y solo actúa cuando ese estado cambia.
  • Podés explicar por qué el código no bloqueante es imprescindible para atender internet en simultáneo con el control del sistema.
  • El panel de Adafruit IO muestra tres indicadores (aireador, bomba, dosificador) que reflejan el estado publicado por el dispositivo (variante ESP32).