Monitorear el sistema con Internet de las Cosas
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.
Introducción a Internet de las Cosas (IoT)
Internet de las Cosas (en inglés Internet of Things, abreviado IoTIoT (Internet de las Cosas)Interconexión digital de objetos cotidianos con internet. Permite, por ejemplo, enviar los datos de un sensor a una central y visualizarlos desde un dispositivo móvil. Requiere un dispositivo capaz de conectarse a internet y un servidor que reciba y aloje los datos.) es un concepto que refiere a la interconexión digital de objetos cotidianos con internet. Esta interconexión puede tener diversas funciones. Por ejemplo, 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:
- Un dispositivo capaz de conectarse a internet.
- Un servidor que reciba y aloje los datos.
Para el servidor usaremos Adafruit IOAdafruit IOPlataforma online de uso gratuito, orientada al uso educativo, que centraliza y gestiona la información que reportan los dispositivos IoT. Permite crear feeds y paneles de control para monitorear los datos desde cualquier dispositivo., 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 feedFeedFuente de datos en la que se puede publicar y a la que uno puede suscribirse. Permite enviar datos para que se almacenen en el tiempo, y también leerlos recibiendo las actualizaciones de quienes publican allí. es una fuente de datos en la que uno puede publicar 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,bombaydosificador: 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
// 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();
}
} // 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();
}
} 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.
// 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();
} 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.
De prendido/apagado a datos útiles
Los Indicator dicen “encendido” o “apagado”, pero un dato más rico ayudaría a cuidar el sistema. Tomen la extensión sugerida en la ficha del proyecto: agreguen un sensor de calidad del agua (temperatura, turbiedad o pH) y publiquen su lectura en un feed nuevo, con un bloque Gauge en el panel. ¿Qué haría falta cambiar en el código para sumar esa medición sin romper el control no bloqueante?
Verificá
Con el sistema andando, mirá el panel: al arrancar el ciclo, los tres Indicator encienden; a los pocos segundos se apagan el del dosificador y el de la bomba, más tarde el del aireador, y al reiniciarse el ciclo vuelven a encender. Si no llega nada, revisá las credenciales y el nombre de la red; si llegan datos pero un bloque no reacciona, revisá a qué feed está asociado y su condición.
Cierre del nivel
Al terminar este nivel, 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).