Monitoreo remoto 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, puede utilizarse para monitorear la temperatura de un ambiente, enviando los datos obtenidos por un sensor a una central donde se recopile la información. De esta manera podría visualizarse en un dispositivo móvil la temperatura de un laboratorio, de un invernadero o de una sala de un hospital.
Para poder incorporar IoT a nuestro proyecto es necesario:
- Un dispositivo capaz de conectarse a internet.
- Un servidor que reciba y aloje los datos.
Existen diversas formas de lograr el cometido de registrar y almacenar los datos de nuestro sistema de semáforos. En este caso, se detallará cómo hacerlo con el WiFi integrado de la placa ESP32ESP32Placa con un microcontrolador de Espressif que trae WiFi (y Bluetooth) integrados. Se programa desde el Arduino IDE igual que un Arduino, con pines llamados GPIO. En este proyecto reemplaza al conjunto Arduino UNO + módulo OBLOQ para el nivel IoT. y con los servidores de Adafruit IOAdafruit IOPlataforma online de uso gratuito, orientada al uso educativo, que centraliza y gestiona la información que reportan los dispositivos IoT. Permite crear feeds y paneles de control para monitorear los datos desde cualquier dispositivo..
El material original de 2019 resolvía este punto agregando al Arduino UNO un módulo externo llamado OBLOQ, un adaptador WiFi-serie hoy discontinuado. Como el UNO clásico no tiene conectividad propia, en esta reedición la práctica de IoT se realiza con la variante ESP32 del proyecto: podés seguir este nivel de manera conceptual con tu UNO (los pasos 2 y 3 funcionan perfectamente en él) y cambiar al ESP32 con el selector de plataforma para la parte práctica de conexión, que es idéntica en lo didáctico y más simple en el circuito.
El ESP32 cumple los dos roles del “dispositivo capaz de conectarse a internet”: es el microcontrolador que maneja los LED del semáforo 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.
Programar sin código bloqueante
Antes de comenzar a utilizar IoT debemos hacer una aclaración con respecto a la función
delay() que figura en el código que usamos hasta ahora. Esta función brinda un tiempo de
espera al sistema que puede utilizarse con varios fines. Suele utilizarse bastante en las
primeras aproximaciones a la programación, ya que su comportamiento resulta fácil de
comprender y su programación no requiere más que una línea de código.
Sin embargo, esta función tiene una complicación, dado que genera un
código bloqueanteCódigo bloqueanteCódigo que detiene todo el procesamiento hasta que se cumple una condición, como delay(), que deja al programa 'colgado' durante el tiempo indicado. En IoT es conflictivo: impide atender tareas simultáneas como la publicación de datos y la conexión a internet.. Esto significa que, cuando el
programa entra en esa función, se detiene todo el procesamiento hasta que se cumpla el
tiempo indicado. En otras palabras, cuando el programa entra al delay() queda “colgado”
por el período de tiempo establecido.
Al utilizar IoT, es conflictivo utilizar código bloqueante, ya que al detenerse el procesamiento se impide también que el sistema realice otras operaciones que funcionan en simultáneo. Por ejemplo, las tareas de publicación y el mantenimiento constante de la conexión a internet.
Para evitar estos problemas, se puede utilizar una alternativa de código no bloqueante,
como la función millis(). Esta función arroja un valor sobre un conteo de tiempo, que se
realiza desde el momento en que se inicia el sistema. Es decir, funciona como un cronómetro
(en milisegundos) que, cada vez que es consultada desde el código, “devuelve” el valor en
el que se encuentra. De esta manera podemos pedirle al sistema que informe cuánto tiempo
transcurrió desde el inicio de las operaciones para dar indicaciones temporales sobre una
tarea, sin detener todas las demás.
A continuación se presenta un ejemplo de cómo se puede programar la intermitencia de un
LED que se prenda y apague cada un segundo (expresado en 1000 milisegundos) sin utilizar
código bloqueante, usando millis() para consultar cuánto tiempo pasó:
Ver el código completo · parpadeo-sin-delay.ino
// Parpadeo de un LED SIN delay(): código no bloqueante con millis().
//
// ¿Por qué? delay() "cuelga" el procesador: mientras espera, no puede
// atender botones, sensores ni la conexión a internet (clave para IoT).
// millis() funciona como un cronómetro que corre desde el encendido:
// en vez de detenernos a esperar, cada vuelta de loop() PREGUNTAMOS
// cuánto tiempo pasó y solo actuamos cuando corresponde.
//
// Plataforma: Arduino UNO (pin 13, LED integrado).
//
// Basado en: Fragmento de código 4 — Sistema de Semaforización,
// Saberes Digitales (INET/EDUCAR 2019)
// Modernización: constantes nombradas para pin e intervalo, y
// "millisAnterior" declarado como unsigned long (el original usaba long,
// que a los ~25 días de funcionamiento continuo desborda y rompe la cuenta;
// con unsigned long la resta millisActual - millisAnterior sigue siendo
// correcta incluso cuando millis() da la vuelta a los ~49 días).
const int PIN_LED = 13; // LED integrado del UNO
const unsigned long INTERVALO_PARPADEO_MS = 1000;
// Estado actual del LED (LOW = apagado, HIGH = encendido).
int estadoLed = LOW;
// Momento (en ms desde el arranque) en que conmutamos el LED por última vez.
unsigned long millisAnterior = 0;
void setup() {
pinMode(PIN_LED, OUTPUT);
}
void loop() {
unsigned long millisActual = millis();
// ¿Ya pasó un segundo desde la última conmutación? Si no pasó,
// no hacemos nada y loop() vuelve a empezar: eso es NO bloquear.
if (millisActual - millisAnterior >= INTERVALO_PARPADEO_MS) {
// Conmutar el estado del LED.
if (estadoLed == LOW) {
estadoLed = HIGH;
} else {
estadoLed = LOW;
}
digitalWrite(PIN_LED, estadoLed);
// Guardamos "cuándo fue la última vez": así la próxima espera
// se mide desde este momento y no desde el arranque.
millisAnterior = millisActual;
}
// En este punto el procesador queda libre para realizar otras tareas
// (leer sensores, atender la red, etc.) en cada vuelta de loop().
} // Parpadeo de un LED SIN delay(): código no bloqueante con millis().
//
// ¿Por qué? delay() "cuelga" el procesador: mientras espera, no puede
// atender botones, sensores ni la conexión a internet (clave para IoT,
// y en el ESP32 el WiFi es justamente el plato fuerte). millis() funciona
// como un cronómetro que corre desde el encendido: en vez de detenernos
// a esperar, cada vuelta de loop() PREGUNTAMOS cuánto tiempo pasó y solo
// actuamos cuando corresponde.
//
// Plataforma: ESP32 (DevKit v1), GPIO 2 (LED azul integrado).
// Nota eléctrica: el ESP32 trabaja a 3,3 V (no 5 V como el UNO).
//
// Basado en: Fragmento de código 4 — Sistema de Semaforización,
// Saberes Digitales (INET/EDUCAR 2019)
// Modernización: portado a ESP32; constantes nombradas para pin e
// intervalo, y "millisAnterior" declarado como unsigned long (el original
// usaba long, que desborda la cuenta; con unsigned long la resta
// millisActual - millisAnterior sigue siendo correcta incluso cuando
// millis() da la vuelta a los ~49 días).
const int PIN_LED = 2; // LED integrado del DevKit v1
const unsigned long INTERVALO_PARPADEO_MS = 1000;
// Estado actual del LED (LOW = apagado, HIGH = encendido).
int estadoLed = LOW;
// Momento (en ms desde el arranque) en que conmutamos el LED por última vez.
unsigned long millisAnterior = 0;
void setup() {
pinMode(PIN_LED, OUTPUT);
}
void loop() {
unsigned long millisActual = millis();
// ¿Ya pasó un segundo desde la última conmutación? Si no pasó,
// no hacemos nada y loop() vuelve a empezar: eso es NO bloquear.
if (millisActual - millisAnterior >= INTERVALO_PARPADEO_MS) {
// Conmutar el estado del LED.
if (estadoLed == LOW) {
estadoLed = HIGH;
} else {
estadoLed = LOW;
}
digitalWrite(PIN_LED, estadoLed);
// Guardamos "cuándo fue la última vez": así la próxima espera
// se mide desde este momento y no desde el arranque.
millisAnterior = millisActual;
}
// En este punto el procesador queda libre para realizar otras tareas
// (leer sensores, atender la red WiFi, etc.) en cada vuelta de loop().
} En el ejemplo se puede observar que, para tomar el valor de millis(), se define un valor
inicial al que llamamos millisAnterior, que es igual a cero. Luego, para saber el tiempo
transcurrido, se calcula la diferencia entre el valor de millisActual y el de
millisAnterior. Como queremos generar una intermitencia de 1 segundo, evaluamos si esta
diferencia es mayor o igual a 1000. En caso de que haya transcurrido más de un segundo, el
sistema modificará el estado de la luz; si ha transcurrido menos tiempo, el estado se
mantendrá estable.
En última instancia se establece que, si esta diferencia es mayor o igual a 1000, se le
asigne a millisAnterior el valor de millisActual. De esta manera, la diferencia entre
ambas vuelve a ser cero hasta que vuelva a transcurrir otro segundo.
Verificá
Cargá el programa: el LED tiene que parpadear cada un segundo, exactamente igual que la
versión con delay() del nivel inicial. Que por fuera se vean idénticos y por dentro sean
tan distintos es, justamente, el punto de este paso.
Una máquina de estados para el semáforo
En el parpadeo del paso anterior había dos estados: LOW (bajo) y HIGH (alto). En el
caso del semáforo podemos considerar los siguientes:
- Poner en verde
- Esperando en verde
- Poner en amarillo
- Esperando en amarillo
- Poner en rojo
- Esperando en rojo
- Poner en rojo y amarillo
- Esperando en rojo y amarillo
Esta forma de organizar el programa se llama máquina de
estadosMáquina de estadosForma de organizar un programa donde una variable guarda el estado actual del sistema y, en cada vuelta del loop, solo se ejecuta lo que corresponde a ese estado. El semáforo no bloqueante usa ocho estados que se recorren según el tiempo transcurrido.: una variable guarda en qué estado está el semáforo, y en cada vuelta de
loop() el programa hace solo lo que corresponde a ese estado. Los estados “Poner en…”
encienden las luces y anotan la hora; los estados “Esperando en…” solo consultan millis()
y, cuando pasó el tiempo de esa luz, saltan al estado siguiente. Así queda el semáforo del
nivel inicial quitando el código bloqueante:
Ver el código completo · semaforo-no-bloqueante.ino
// Intersección completa (6 LED) SIN delay(): máquina de estados con millis().
// Misma secuencia de 8 cuadros del nivel intermedio, pero no bloqueante:
// el procesador queda libre en cada vuelta de loop() para otras tareas
// (esta es la base sobre la que el nivel IoT agrega la conexión a internet).
//
// Estado | Semáforo 1 | Semáforo 2 | Duración
// -------+-----------------+-----------------+---------
// 1 | VERDE | ROJO | 5 s
// 2 | AMARILLO | ROJO | 1 s
// 3 | ROJO | ROJO | 1 s (todo rojo)
// 4 | ROJO | ROJO + AMARILLO | 1 s
// 5 | ROJO | VERDE | 5 s
// 6 | ROJO | AMARILLO | 1 s
// 7 | ROJO | ROJO | 1 s (todo rojo)
// 8 | ROJO + AMARILLO | ROJO | 1 s → vuelve al 1
//
// Plataforma: Arduino UNO.
// Conexión: semáforo 1 → verde 11, amarillo 12, rojo 13;
// semáforo 2 → verde 8, amarillo 9, rojo 10.
//
// Basado en: Fragmento de código 5 — Sistema de Semaforización,
// Saberes Digitales (INET/EDUCAR 2019)
// Modernización: (1) el original aplicaba la máquina de estados a UN solo
// semáforo; acá se extiende a la intersección completa de 6 LED manteniendo
// el estilo (if por estado + millisAnterior). (2) El original separaba cada
// paso en dos estados ("poner" y "esperando"); acá cada estado enciende sus
// luces AL ENTRAR (dentro del if de transición), lo que deja exactamente
// 8 estados: uno por cuadro del cómic de la Fig. 10. (3) unsigned long para
// los tiempos y una función auxiliar aplicarLuces() para escribir las seis
// luces de un cuadro completo de una sola vez.
// Semáforo 1 (calle principal).
const int PIN_SEM1_VERDE = 11;
const int PIN_SEM1_AMARILLO = 12;
const int PIN_SEM1_ROJO = 13;
// Semáforo 2 (calle transversal).
const int PIN_SEM2_VERDE = 8;
const int PIN_SEM2_AMARILLO = 9;
const int PIN_SEM2_ROJO = 10;
// Tiempos de la secuencia (los mismos de toda la guía y del simulador).
const unsigned long DURACION_VERDE_MS = 5000;
const unsigned long DURACION_AMARILLO_MS = 1000;
const unsigned long DURACION_TODO_ROJO_MS = 1000; // margen de seguridad
const unsigned long DURACION_ROJO_AMARILLO_MS = 1000;
// Estado actual de la máquina (1 a 8, un estado por cuadro del cómic).
int estado = 1;
// Momento (ms desde el arranque) en que entramos al estado actual.
unsigned long millisAnterior = 0;
// Escribe las seis luces de un cuadro completo. Que cada estado defina
// TODAS las luces (y no solo las que cambian) evita que un cuadro dependa
// de lo que dejó encendido el anterior: cada estado se entiende solo.
// Orden de parámetros: semáforo 1 (verde, amarillo, rojo),
// semáforo 2 (verde, amarillo, rojo).
void aplicarLuces(int sem1Verde, int sem1Amarillo, int sem1Rojo,
int sem2Verde, int sem2Amarillo, int sem2Rojo) {
digitalWrite(PIN_SEM1_VERDE, sem1Verde);
digitalWrite(PIN_SEM1_AMARILLO, sem1Amarillo);
digitalWrite(PIN_SEM1_ROJO, sem1Rojo);
digitalWrite(PIN_SEM2_VERDE, sem2Verde);
digitalWrite(PIN_SEM2_AMARILLO, sem2Amarillo);
digitalWrite(PIN_SEM2_ROJO, sem2Rojo);
}
void setup() {
pinMode(PIN_SEM1_VERDE, OUTPUT);
pinMode(PIN_SEM1_AMARILLO, OUTPUT);
pinMode(PIN_SEM1_ROJO, OUTPUT);
pinMode(PIN_SEM2_VERDE, OUTPUT);
pinMode(PIN_SEM2_AMARILLO, OUTPUT);
pinMode(PIN_SEM2_ROJO, OUTPUT);
// Encendemos las luces del estado inicial (cuadro 1): S1 verde / S2 rojo.
aplicarLuces(HIGH, LOW, LOW, LOW, LOW, HIGH);
}
void loop() {
unsigned long millisActual = millis();
// Cada if pregunta: "¿estoy en el estado N y ya se cumplió su tiempo?"
// Si la respuesta es sí, enciende las luces del estado siguiente,
// guarda el momento del cambio y avanza. Si es no, no hace nada:
// por eso este código es no bloqueante.
// Estado 1 (S1 verde / S2 rojo): tras 5 s pasa al estado 2.
if (estado == 1 && millisActual - millisAnterior >= DURACION_VERDE_MS) {
aplicarLuces(LOW, HIGH, LOW, LOW, LOW, HIGH); // cuadro 2: S1 amarillo / S2 rojo
millisAnterior = millisActual;
estado = 2;
}
// Estado 2 (S1 amarillo / S2 rojo): tras 1 s pasa al estado 3.
if (estado == 2 && millisActual - millisAnterior >= DURACION_AMARILLO_MS) {
aplicarLuces(LOW, LOW, HIGH, LOW, LOW, HIGH); // cuadro 3: todo rojo
millisAnterior = millisActual;
estado = 3;
}
// Estado 3 (todo rojo): tras 1 s pasa al estado 4.
if (estado == 3 && millisActual - millisAnterior >= DURACION_TODO_ROJO_MS) {
aplicarLuces(LOW, LOW, HIGH, LOW, HIGH, HIGH); // cuadro 4: S2 rojo+amarillo
millisAnterior = millisActual;
estado = 4;
}
// Estado 4 (S1 rojo / S2 rojo+amarillo): tras 1 s pasa al estado 5.
if (estado == 4 && millisActual - millisAnterior >= DURACION_ROJO_AMARILLO_MS) {
aplicarLuces(LOW, LOW, HIGH, HIGH, LOW, LOW); // cuadro 5: S2 verde
millisAnterior = millisActual;
estado = 5;
}
// Estado 5 (S1 rojo / S2 verde): tras 5 s pasa al estado 6.
if (estado == 5 && millisActual - millisAnterior >= DURACION_VERDE_MS) {
aplicarLuces(LOW, LOW, HIGH, LOW, HIGH, LOW); // cuadro 6: S2 amarillo
millisAnterior = millisActual;
estado = 6;
}
// Estado 6 (S1 rojo / S2 amarillo): tras 1 s pasa al estado 7.
if (estado == 6 && millisActual - millisAnterior >= DURACION_AMARILLO_MS) {
aplicarLuces(LOW, LOW, HIGH, LOW, LOW, HIGH); // cuadro 7: todo rojo
millisAnterior = millisActual;
estado = 7;
}
// Estado 7 (todo rojo): tras 1 s pasa al estado 8.
if (estado == 7 && millisActual - millisAnterior >= DURACION_TODO_ROJO_MS) {
aplicarLuces(LOW, HIGH, HIGH, LOW, LOW, HIGH); // cuadro 8: S1 rojo+amarillo
millisAnterior = millisActual;
estado = 8;
}
// Estado 8 (S1 rojo+amarillo / S2 rojo): tras 1 s vuelve al estado 1.
if (estado == 8 && millisActual - millisAnterior >= DURACION_ROJO_AMARILLO_MS) {
aplicarLuces(HIGH, LOW, LOW, LOW, LOW, HIGH); // cuadro 1: S1 verde / S2 rojo
millisAnterior = millisActual;
estado = 1;
}
// En este punto el procesador queda libre para realizar otras tareas.
} // Intersección completa (6 LED) SIN delay(): máquina de estados con millis().
// Misma secuencia de 8 cuadros del nivel intermedio, pero no bloqueante:
// el procesador queda libre en cada vuelta de loop() para otras tareas.
// Este programa es la base directa de semaforo-iot.ino: sin código
// bloqueante, el ESP32 puede mantener el WiFi y publicar por MQTT
// sin que el semáforo se "cuelgue".
//
// Estado | Semáforo 1 | Semáforo 2 | Duración
// -------+-----------------+-----------------+---------
// 1 | VERDE | ROJO | 5 s
// 2 | AMARILLO | ROJO | 1 s
// 3 | ROJO | ROJO | 1 s (todo rojo)
// 4 | ROJO | ROJO + AMARILLO | 1 s
// 5 | ROJO | VERDE | 5 s
// 6 | ROJO | AMARILLO | 1 s
// 7 | ROJO | ROJO | 1 s (todo rojo)
// 8 | ROJO + AMARILLO | ROJO | 1 s → vuelve al 1
//
// Plataforma: ESP32 (DevKit v1).
// Conexión: semáforo 1 → verde GPIO 16, amarillo GPIO 17, rojo GPIO 18;
// semáforo 2 → verde GPIO 19, amarillo GPIO 21, rojo GPIO 22.
// Nota eléctrica: el ESP32 trabaja a 3,3 V (no 5 V como el UNO); nunca
// inyectar 5 V en un GPIO.
//
// Basado en: Fragmento de código 5 — Sistema de Semaforización,
// Saberes Digitales (INET/EDUCAR 2019)
// Modernización: (1) el original aplicaba la máquina de estados a UN solo
// semáforo; acá se extiende a la intersección completa de 6 LED manteniendo
// el estilo (if por estado + millisAnterior). (2) El original separaba cada
// paso en dos estados ("poner" y "esperando"); acá cada estado enciende sus
// luces AL ENTRAR (dentro del if de transición), lo que deja exactamente
// 8 estados: uno por cuadro del cómic de la Fig. 10. (3) unsigned long para
// los tiempos, GPIO seguros de ESP32 y una función auxiliar aplicarLuces()
// para escribir las seis luces de un cuadro completo de una sola vez.
// Semáforo 1 (calle principal).
const int PIN_SEM1_VERDE = 16;
const int PIN_SEM1_AMARILLO = 17;
const int PIN_SEM1_ROJO = 18;
// Semáforo 2 (calle transversal).
const int PIN_SEM2_VERDE = 19;
const int PIN_SEM2_AMARILLO = 21;
const int PIN_SEM2_ROJO = 22;
// Tiempos de la secuencia (los mismos de toda la guía y del simulador).
const unsigned long DURACION_VERDE_MS = 5000;
const unsigned long DURACION_AMARILLO_MS = 1000;
const unsigned long DURACION_TODO_ROJO_MS = 1000; // margen de seguridad
const unsigned long DURACION_ROJO_AMARILLO_MS = 1000;
// Estado actual de la máquina (1 a 8, un estado por cuadro del cómic).
int estado = 1;
// Momento (ms desde el arranque) en que entramos al estado actual.
unsigned long millisAnterior = 0;
// Escribe las seis luces de un cuadro completo. Que cada estado defina
// TODAS las luces (y no solo las que cambian) evita que un cuadro dependa
// de lo que dejó encendido el anterior: cada estado se entiende solo.
// Orden de parámetros: semáforo 1 (verde, amarillo, rojo),
// semáforo 2 (verde, amarillo, rojo).
void aplicarLuces(int sem1Verde, int sem1Amarillo, int sem1Rojo,
int sem2Verde, int sem2Amarillo, int sem2Rojo) {
digitalWrite(PIN_SEM1_VERDE, sem1Verde);
digitalWrite(PIN_SEM1_AMARILLO, sem1Amarillo);
digitalWrite(PIN_SEM1_ROJO, sem1Rojo);
digitalWrite(PIN_SEM2_VERDE, sem2Verde);
digitalWrite(PIN_SEM2_AMARILLO, sem2Amarillo);
digitalWrite(PIN_SEM2_ROJO, sem2Rojo);
}
void setup() {
pinMode(PIN_SEM1_VERDE, OUTPUT);
pinMode(PIN_SEM1_AMARILLO, OUTPUT);
pinMode(PIN_SEM1_ROJO, OUTPUT);
pinMode(PIN_SEM2_VERDE, OUTPUT);
pinMode(PIN_SEM2_AMARILLO, OUTPUT);
pinMode(PIN_SEM2_ROJO, OUTPUT);
// Encendemos las luces del estado inicial (cuadro 1): S1 verde / S2 rojo.
aplicarLuces(HIGH, LOW, LOW, LOW, LOW, HIGH);
}
void loop() {
unsigned long millisActual = millis();
// Cada if pregunta: "¿estoy en el estado N y ya se cumplió su tiempo?"
// Si la respuesta es sí, enciende las luces del estado siguiente,
// guarda el momento del cambio y avanza. Si es no, no hace nada:
// por eso este código es no bloqueante.
// Estado 1 (S1 verde / S2 rojo): tras 5 s pasa al estado 2.
if (estado == 1 && millisActual - millisAnterior >= DURACION_VERDE_MS) {
aplicarLuces(LOW, HIGH, LOW, LOW, LOW, HIGH); // cuadro 2: S1 amarillo / S2 rojo
millisAnterior = millisActual;
estado = 2;
}
// Estado 2 (S1 amarillo / S2 rojo): tras 1 s pasa al estado 3.
if (estado == 2 && millisActual - millisAnterior >= DURACION_AMARILLO_MS) {
aplicarLuces(LOW, LOW, HIGH, LOW, LOW, HIGH); // cuadro 3: todo rojo
millisAnterior = millisActual;
estado = 3;
}
// Estado 3 (todo rojo): tras 1 s pasa al estado 4.
if (estado == 3 && millisActual - millisAnterior >= DURACION_TODO_ROJO_MS) {
aplicarLuces(LOW, LOW, HIGH, LOW, HIGH, HIGH); // cuadro 4: S2 rojo+amarillo
millisAnterior = millisActual;
estado = 4;
}
// Estado 4 (S1 rojo / S2 rojo+amarillo): tras 1 s pasa al estado 5.
if (estado == 4 && millisActual - millisAnterior >= DURACION_ROJO_AMARILLO_MS) {
aplicarLuces(LOW, LOW, HIGH, HIGH, LOW, LOW); // cuadro 5: S2 verde
millisAnterior = millisActual;
estado = 5;
}
// Estado 5 (S1 rojo / S2 verde): tras 5 s pasa al estado 6.
if (estado == 5 && millisActual - millisAnterior >= DURACION_VERDE_MS) {
aplicarLuces(LOW, LOW, HIGH, LOW, HIGH, LOW); // cuadro 6: S2 amarillo
millisAnterior = millisActual;
estado = 6;
}
// Estado 6 (S1 rojo / S2 amarillo): tras 1 s pasa al estado 7.
if (estado == 6 && millisActual - millisAnterior >= DURACION_AMARILLO_MS) {
aplicarLuces(LOW, LOW, HIGH, LOW, LOW, HIGH); // cuadro 7: todo rojo
millisAnterior = millisActual;
estado = 7;
}
// Estado 7 (todo rojo): tras 1 s pasa al estado 8.
if (estado == 7 && millisActual - millisAnterior >= DURACION_TODO_ROJO_MS) {
aplicarLuces(LOW, HIGH, HIGH, LOW, LOW, HIGH); // cuadro 8: S1 rojo+amarillo
millisAnterior = millisActual;
estado = 8;
}
// Estado 8 (S1 rojo+amarillo / S2 rojo): tras 1 s vuelve al estado 1.
if (estado == 8 && millisActual - millisAnterior >= DURACION_ROJO_AMARILLO_MS) {
aplicarLuces(HIGH, LOW, LOW, LOW, LOW, HIGH); // cuadro 1: S1 verde / S2 rojo
millisAnterior = millisActual;
estado = 1;
}
// En este punto el procesador queda libre para realizar otras tareas
// (en semaforo-iot.ino, acá se atiende la conexión WiFi/MQTT).
} Teniendo un programa de funcionamiento de semáforos con un código no bloqueante (es decir,
sin usar la función delay()) estamos en condiciones de incorporar IoT a nuestro proyecto.
Verificá
El semáforo tiene que comportarse exactamente igual que en el nivel inicial: verde 5 s, amarillo 1 s, rojo 5 s, rojo + amarillo 1 s, en ciclo. Si alguna luz queda “pegada”, revisá que cada estado “Poner en…” cambie al estado de espera correspondiente y que cada espera compare contra el tiempo correcto.
Crear un panel de control en Adafruit IO
Ahora vamos a preparar la “central de monitoreo”. 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 con posibilidad de uso gratuito, orientada al uso educativo, que se encarga de centralizar la información que reportan los dispositivos. Primero debemos crear una cuenta de usuario en io.adafruit.com.
Una vez que ingresamos con nuestro usuario, el trabajo tiene dos partes: crear los feeds donde se publicarán los datos y armar el panel que los muestre.
Un feedFeedFuente de datos en la que se puede publicar y a la que uno puede suscribirse. Permite enviar datos para que se almacenen en el tiempo, y también leerlos recibiendo las actualizaciones de quienes publican allí. es una fuente de datos en la que uno puede publicar, así
como también se puede suscribir para recibir los datos de cierto feed. Desde la sección
Feeds, creamos un feed llamado semaforo1: en él publicaremos, desde nuestro
dispositivo, la información sobre el estado del primer semáforo (si más adelante sumás el
segundo semáforo, su feed será semaforo2).
Este feed tendrá el estado del semáforo expresado con la siguiente convención:
- Cuando el valor sea 1, estará en verde.
- Cuando el valor sea 2, estará en amarillo.
- Cuando el valor sea 3, estará en rojo.
Durante la fase de rojo y amarillo (el cuadro previo al verde) también se publica
3: la luz roja sigue encendida, así el panel refleja siempre un estado real.
(La guía original publicaba un cuarto valor, 4, que su propio panel nunca mostraba;
lo simplificamos — el desvío está documentado en la ficha del proyecto.)
Luego, desde la sección Dashboards, creamos un nuevo panel con el nombre y la descripción que quieras. Dentro del panel, agregamos bloques del tipo Indicator (indicador): son luces virtuales que se encienden cuando se cumple una condición sobre un feed. Para nuestro semáforo, podemos ubicar tres indicadores con diferentes colores, uno abajo del otro, como una columna de semáforo virtual:
- El indicador rojo se asocia al feed
semaforo1con la condición “igual a 3”: estará encendido (rojo) cuando el feed valga 3, y apagado (gris) cuando valga otra cosa. - El indicador amarillo, con la condición “igual a 2”.
- El indicador verde, con la condición “igual a 1”.
Cada bloque permite elegir un título, un color para el estado activado y otro para el desactivado, y luego se puede ajustar el tamaño y la ubicación de los bloques dentro del panel. Al finalizar, la columna de indicadores replicará a distancia el estado del semáforo real.
Por último, necesitamos las credenciales que le permitirán a nuestro dispositivo publicar en la cuenta: en Adafruit IO, tocá el ícono de la llave (API Key). Vas a ver dos datos, el nombre de usuario (Username) y la clave (Active Key): anotalos, porque los vamos a copiar dentro del código en el paso siguiente. La clave es secreta: cualquiera que la tenga puede publicar en tus feeds.
Verificá
Para probar el panel sin la placa, entrá al feed semaforo1 y agregale un valor a mano
(Add Data): con 3, el indicador rojo del panel tiene que encenderse; con 1, el
verde. Si los indicadores no reaccionan, revisá la condición de cada bloque y que estén
asociados al feed correcto.
Programación IoT
Con el panel listo y el código ya libre de bloqueos, falta el último eslabón: que el semáforo publique su estado cada vez que cambia de luz.
Esta parte práctica corresponde a la variante ESP32: el Arduino UNO clásico no tiene WiFi (el original lo resolvía con el módulo OBLOQ, hoy discontinuado). Cambiá la plataforma a ESP32 con el selector para ver el código y las instrucciones de conexión. Todo lo conceptual —cuándo publicar, la convención de valores, el porqué del código no bloqueante— ya lo tenés resuelto con tu UNO en los pasos anteriores.
El programa final integra tres cosas que ya conocés: la máquina de estados del paso 3, la conexión WiFi nativa del ESP32 y la publicación en Adafruit IO (mediante el protocolo MQTT, usando la biblioteca Adafruit MQTT Library, que se instala desde el gestor de bibliotecas del Arduino IDE junto con sus dependencias).
Antes de subirlo, tenés que completar cuatro datos al comienzo del código: el nombre (SSID) y la contraseña de tu red WiFi, y el usuario y la clave que copiaste de Adafruit IO en el paso anterior.
La publicación reutiliza la máquina de estados: en cada estado “Poner en…”, además de
encender las luces, el programa publica en el feed semaforo1 el valor de la convención
(1 = verde, 2 = amarillo, 3 = rojo; en rojo y amarillo también 3, como vimos en el
paso 4). Y en cada vuelta de loop() se
llama a la función de actualización de la biblioteca, que mantiene viva la conexión con el
servidor. Por esto es importante que nuestro código no sea bloqueante: si el programa
quedara colgado en un delay(), la conexión no podría atenderse y las publicaciones se
perderían.
Ver el código completo · semaforo-iot.ino
Nota: Este código está disponible para ESP32. Cambiá la plataforma con el selector del encabezado para verlo y practicarlo.
// Intersección completa (6 LED) con monitoreo IoT vía Adafruit IO (MQTT).
// La máquina de estados de semaforo-no-bloqueante.ino sigue manejando las
// luces; además, cada vez que un semáforo cambia de color, se publica su
// nuevo estado en un "feed" de Adafruit IO para verlo desde el Panel de
// Control (la "central de monitoreo") en cualquier dispositivo con internet.
//
// Convención de códigos publicados: 1 = verde, 2 = amarillo, 3 = rojo.
// Durante rojo + amarillo también se publica 3 (ver constantes más abajo).
// Feeds: "semaforo1" (calle principal) y "semaforo2" (calle transversal).
//
// Plataforma: ESP32 (DevKit v1) — usa su WiFi integrado.
// Conexión: semáforo 1 → verde GPIO 16, amarillo GPIO 17, rojo GPIO 18;
// semáforo 2 → verde GPIO 19, amarillo GPIO 21, rojo GPIO 22.
// Nota eléctrica: el ESP32 trabaja a 3,3 V (no 5 V como el UNO); nunca
// inyectar 5 V en un GPIO.
//
// Librería requerida (Library Manager del IDE de Arduino):
// "Adafruit MQTT Library" (de Adafruit). Se eligió porque cada feed se
// representa como un objeto y publish() acepta números directamente,
// igual que el olq.publish() del original; con PubSubClient habría que
// armar a mano los topics y convertir los números a texto.
//
// Basado en: Fragmento de código 6 — Sistema de Semaforización,
// Saberes Digitales (INET/EDUCAR 2019)
// Modernización: (1) el módulo OBLOQ y la librería ObloqAdafruit quedaron
// discontinuados; se reemplazan por el WiFi nativo del ESP32 (WiFi.h) y un
// cliente MQTT estándar contra io.adafruit.com. (2) La máquina de estados
// se extiende del semáforo único del original a la intersección completa
// de 6 LED con dos feeds. (3) Se publica SOLO cuando un semáforo cambia de
// color: publicar en cada vuelta de loop() saturaría el límite gratuito de
// Adafruit IO (30 publicaciones por minuto; este ciclo genera 8 cada 16 s,
// justo al límite — si Adafruit avisa "throttled", alargá las duraciones).
#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;
// Semáforo 1 (calle principal).
const int PIN_SEM1_VERDE = 16;
const int PIN_SEM1_AMARILLO = 17;
const int PIN_SEM1_ROJO = 18;
// Semáforo 2 (calle transversal).
const int PIN_SEM2_VERDE = 19;
const int PIN_SEM2_AMARILLO = 21;
const int PIN_SEM2_ROJO = 22;
// Tiempos de la secuencia (los mismos de toda la guía y del simulador).
const unsigned long DURACION_VERDE_MS = 5000;
const unsigned long DURACION_AMARILLO_MS = 1000;
const unsigned long DURACION_TODO_ROJO_MS = 1000; // margen de seguridad
const unsigned long DURACION_ROJO_AMARILLO_MS = 1000;
// Si se cae la conexión MQTT, reintentamos cada tanto SIN bloquear:
// el semáforo debe seguir funcionando aunque no haya internet.
const unsigned long INTERVALO_REINTENTO_MQTT_MS = 5000;
// Códigos de estado que se publican (convención del original: 1/2/3).
// Modernización: el original publicaba 4 en rojo+amarillo, pero su panel
// solo mapeaba 1/2/3 y quedaba gris. Como el rojo sigue encendido en esa
// fase, publicamos 3 y el panel refleja siempre un estado real.
const int CODIGO_VERDE = 1;
const int CODIGO_AMARILLO = 2;
const int CODIGO_ROJO = 3;
const int CODIGO_ROJO_AMARILLO = CODIGO_ROJO;
// 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 feedSemaforo1(&mqtt, IO_USERNAME "/feeds/semaforo1");
Adafruit_MQTT_Publish feedSemaforo2(&mqtt, IO_USERNAME "/feeds/semaforo2");
// Estado actual de la máquina (1 a 8, un estado por cuadro del cómic).
int estado = 1;
// Momento (ms desde el arranque) en que entramos al estado actual.
unsigned long millisAnterior = 0;
// Momento del último intento de conexión MQTT.
unsigned long millisUltimoIntentoMqtt = 0;
// Color actual de cada semáforo y último color que llegó a publicarse.
// Separarlos permite publicar solo los CAMBIOS y, si la conexión estaba
// caída durante un cambio, ponerse al día apenas vuelva.
int codigoSem1 = CODIGO_VERDE;
int codigoSem2 = CODIGO_ROJO;
int codigoSem1Publicado = 0; // 0 = todavía no se publicó nada
int codigoSem2Publicado = 0;
// Escribe las seis luces de un cuadro completo (ver semaforo-no-bloqueante.ino).
// Orden: semáforo 1 (verde, amarillo, rojo), semáforo 2 (verde, amarillo, rojo).
void aplicarLuces(int sem1Verde, int sem1Amarillo, int sem1Rojo,
int sem2Verde, int sem2Amarillo, int sem2Rojo) {
digitalWrite(PIN_SEM1_VERDE, sem1Verde);
digitalWrite(PIN_SEM1_AMARILLO, sem1Amarillo);
digitalWrite(PIN_SEM1_ROJO, sem1Rojo);
digitalWrite(PIN_SEM2_VERDE, sem2Verde);
digitalWrite(PIN_SEM2_AMARILLO, sem2Amarillo);
digitalWrite(PIN_SEM2_ROJO, sem2Rojo);
}
// Mantiene viva la conexión MQTT sin bloquear el semáforo: si está caída,
// intenta reconectar como mucho una vez cada INTERVALO_REINTENTO_MQTT_MS.
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 en cada feed SOLO si el color cambió desde la última publicación
// exitosa. Si publish() falla (o no hay conexión), no actualizamos el
// registro: se reintentará en la próxima vuelta de loop().
void publicarCambios() {
if (!mqtt.connected()) {
return; // sin internet el semáforo sigue; la central se actualiza al volver
}
if (codigoSem1 != codigoSem1Publicado) {
if (feedSemaforo1.publish((int32_t)codigoSem1)) {
codigoSem1Publicado = codigoSem1;
}
}
if (codigoSem2 != codigoSem2Publicado) {
if (feedSemaforo2.publish((int32_t)codigoSem2)) {
codigoSem2Publicado = codigoSem2;
}
}
}
void setup() {
Serial.begin(115200);
pinMode(PIN_SEM1_VERDE, OUTPUT);
pinMode(PIN_SEM1_AMARILLO, OUTPUT);
pinMode(PIN_SEM1_ROJO, OUTPUT);
pinMode(PIN_SEM2_VERDE, OUTPUT);
pinMode(PIN_SEM2_AMARILLO, OUTPUT);
pinMode(PIN_SEM2_ROJO, OUTPUT);
// Encendemos las luces del estado inicial (cuadro 1): S1 verde / S2 rojo.
aplicarLuces(HIGH, LOW, LOW, LOW, LOW, HIGH);
// Conexión WiFi. Esperar acá con delay() es aceptable porque el semáforo
// todavía no arrancó su ciclo; una vez dentro de loop(), nada bloquea.
Serial.print("Conectando a la red WiFi");
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.print("WiFi conectado, IP: ");
Serial.println(WiFi.localIP());
}
void loop() {
unsigned long millisActual = millis();
// --- Máquina de estados de la intersección (8 cuadros del cómic) ---
// Cada transición enciende las luces del estado siguiente y anota qué
// color muestra ahora cada semáforo; la publicación se hace más abajo.
// Estado 1 (S1 verde / S2 rojo): tras 5 s pasa al estado 2.
if (estado == 1 && millisActual - millisAnterior >= DURACION_VERDE_MS) {
aplicarLuces(LOW, HIGH, LOW, LOW, LOW, HIGH); // cuadro 2: S1 amarillo / S2 rojo
codigoSem1 = CODIGO_AMARILLO;
codigoSem2 = CODIGO_ROJO;
millisAnterior = millisActual;
estado = 2;
}
// Estado 2 (S1 amarillo / S2 rojo): tras 1 s pasa al estado 3.
if (estado == 2 && millisActual - millisAnterior >= DURACION_AMARILLO_MS) {
aplicarLuces(LOW, LOW, HIGH, LOW, LOW, HIGH); // cuadro 3: todo rojo
codigoSem1 = CODIGO_ROJO;
codigoSem2 = CODIGO_ROJO;
millisAnterior = millisActual;
estado = 3;
}
// Estado 3 (todo rojo): tras 1 s pasa al estado 4.
if (estado == 3 && millisActual - millisAnterior >= DURACION_TODO_ROJO_MS) {
aplicarLuces(LOW, LOW, HIGH, LOW, HIGH, HIGH); // cuadro 4: S2 rojo+amarillo
codigoSem1 = CODIGO_ROJO;
codigoSem2 = CODIGO_ROJO_AMARILLO;
millisAnterior = millisActual;
estado = 4;
}
// Estado 4 (S1 rojo / S2 rojo+amarillo): tras 1 s pasa al estado 5.
if (estado == 4 && millisActual - millisAnterior >= DURACION_ROJO_AMARILLO_MS) {
aplicarLuces(LOW, LOW, HIGH, HIGH, LOW, LOW); // cuadro 5: S2 verde
codigoSem1 = CODIGO_ROJO;
codigoSem2 = CODIGO_VERDE;
millisAnterior = millisActual;
estado = 5;
}
// Estado 5 (S1 rojo / S2 verde): tras 5 s pasa al estado 6.
if (estado == 5 && millisActual - millisAnterior >= DURACION_VERDE_MS) {
aplicarLuces(LOW, LOW, HIGH, LOW, HIGH, LOW); // cuadro 6: S2 amarillo
codigoSem1 = CODIGO_ROJO;
codigoSem2 = CODIGO_AMARILLO;
millisAnterior = millisActual;
estado = 6;
}
// Estado 6 (S1 rojo / S2 amarillo): tras 1 s pasa al estado 7.
if (estado == 6 && millisActual - millisAnterior >= DURACION_AMARILLO_MS) {
aplicarLuces(LOW, LOW, HIGH, LOW, LOW, HIGH); // cuadro 7: todo rojo
codigoSem1 = CODIGO_ROJO;
codigoSem2 = CODIGO_ROJO;
millisAnterior = millisActual;
estado = 7;
}
// Estado 7 (todo rojo): tras 1 s pasa al estado 8.
if (estado == 7 && millisActual - millisAnterior >= DURACION_TODO_ROJO_MS) {
aplicarLuces(LOW, HIGH, HIGH, LOW, LOW, HIGH); // cuadro 8: S1 rojo+amarillo
codigoSem1 = CODIGO_ROJO_AMARILLO;
codigoSem2 = CODIGO_ROJO;
millisAnterior = millisActual;
estado = 8;
}
// Estado 8 (S1 rojo+amarillo / S2 rojo): tras 1 s vuelve al estado 1.
if (estado == 8 && millisActual - millisAnterior >= DURACION_ROJO_AMARILLO_MS) {
aplicarLuces(HIGH, LOW, LOW, LOW, LOW, HIGH); // cuadro 1: S1 verde / S2 rojo
codigoSem1 = CODIGO_VERDE;
codigoSem2 = CODIGO_ROJO;
millisAnterior = millisActual;
estado = 1;
}
// --- Parte IoT ---
// Gracias a que la máquina de estados no bloquea, estas dos tareas se
// atienden en CADA vuelta de loop(): esta es la razón de fondo por la
// que la guía enseña a abandonar delay() antes de llegar a IoT.
mantenerConexionMqtt();
publicarCambios();
} Así queda funcionando nuestra central de monitoreo en miniatura. Podés ver el sistema completo —la intersección más el panel de la central reflejando cada publicación— en el simulador:
Simulación del comportamiento
La misma máquina de estados del firmware, portada a TypeScript, maneja el
comportamiento del dispositivo. No se ejecuta el binario real: la ejecución
del .hex con avr8js está en el roadmap.
Simulación lista. Presioná «Iniciar» para arrancar la secuencia.
La central completa
Para simplificar el proceso, implementamos IoT para un solo semáforo. Extendé el sistema
para abarcar los dos semáforos de la intersección: creá el feed semaforo2, duplicá la
columna de indicadores en el panel y publicá el estado del segundo semáforo en cada cambio
de luz. Pregunta para discutir en grupo: ¿hace falta publicar los dos, o el estado de uno
alcanza para deducir el del otro?
Verificá
Con el programa cargado y la placa conectada a tu red WiFi, abrí el panel de Adafruit IO en el celular o la computadora: los indicadores tienen que ir siguiendo al semáforo real (verde, amarillo, rojo) con cada cambio de luz, con una pequeña demora de red. Si no llegan datos, revisá las credenciales y el nombre de la red; si llegan pero los indicadores no se encienden, revisá la convención de valores en las condiciones de los bloques.
Cierre del nivel
Al terminar este nivel, tu semáforo dejó de ser solo un circuito: es un dispositivo IoT.
El programa funciona con millis() y una máquina de estados, sin ningún delay() que lo
cuelgue; sabés explicar por qué eso es imprescindible cuando hay que atender internet en
simultáneo; y la central de monitoreo —tu panel de Adafruit IO— muestra desde cualquier
dispositivo el estado del semáforo en cada cambio de luz. Con esto queda completo el
recorrido del proyecto. Si el grupo quiere seguir, en la ficha del proyecto están las
extensiones sugeridas: semáforo peatonal, señal sonora accesible y pulsador de cruce
manejado desde el circuito o desde IoT.
Evidencia de cierre
Al terminar este nivel, esto tiene que estar funcionando:
- El semáforo funciona con código no bloqueante basado en millis() y una máquina de estados: no queda ningún delay() en el programa.
- Podés explicar con tus palabras por qué el código bloqueante es un problema cuando el dispositivo tiene que atender internet en simultáneo.
- El panel de control en Adafruit IO refleja en tiempo casi real el estado del semáforo (verde, amarillo o rojo) publicado por el dispositivo (variante ESP32).