Saltar al contenido
Nivel avanzado

Monitoreo remoto con Internet de las Cosas

Introducción a Internet de las Cosas (IoT)

Internet de las Cosas (en inglés Internet of Things, abreviado IoT) es un concepto que refiere a la interconexión digital de objetos cotidianos con internet. Esta interconexión puede tener diversas funciones. Por ejemplo, puede utilizarse para monitorear la temperatura de un ambiente, enviando los datos obtenidos por un sensor a una central donde se recopile la información. De esta manera podría visualizarse en un dispositivo móvil la temperatura de un laboratorio, de un invernadero o de una sala de un hospital.

Para poder incorporar IoT a nuestro proyecto es necesario:

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

Existen diversas formas de lograr el cometido de registrar y almacenar los datos de nuestro sistema de semáforos. En este caso, se detallará cómo hacerlo con el WiFi integrado de la placa ESP32 y con los servidores de Adafruit IO.

El material original de 2019 resolvía este punto agregando al Arduino UNO un módulo externo llamado OBLOQ, un adaptador WiFi-serie 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 bloqueante. Esto significa que, cuando el programa entra en esa función, se detiene todo el procesamiento hasta que se cumpla el tiempo indicado. En otras palabras, cuando el programa entra al delay() queda “colgado” por el período de tiempo establecido.

Al utilizar IoT, es conflictivo utilizar código bloqueante, ya que al detenerse el procesamiento se impide también que el sistema realice otras operaciones que funcionan en simultáneo. Por ejemplo, las tareas de publicación y el mantenimiento constante de la conexión a internet.

Para evitar estos problemas, se puede utilizar una alternativa de código no bloqueante, como la función millis(). Esta función arroja un valor sobre un conteo de tiempo, que se realiza desde el momento en que se inicia el sistema. Es decir, funciona como un cronómetro (en milisegundos) que, cada vez que es consultada desde el código, “devuelve” el valor en el que se encuentra. De esta manera podemos pedirle al sistema que informe cuánto tiempo transcurrió desde el inicio de las operaciones para dar indicaciones temporales sobre una tarea, sin detener todas las demás.

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-sin-delay.ino Arduino UNO
// 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-sin-delay.ino ESP32
// 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().
}
El mismo parpadeo del nivel inicial, ahora sin delay(): el procesador queda libre entre conmutación y conmutación.

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.

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:

  1. Poner en verde
  2. Esperando en verde
  3. Poner en amarillo
  4. Esperando en amarillo
  5. Poner en rojo
  6. Esperando en rojo
  7. Poner en rojo y amarillo
  8. Esperando en rojo y amarillo

Esta forma de organizar el programa se llama máquina de estados: 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
semaforo-no-bloqueante.ino Arduino UNO
// 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.
}
semaforo-no-bloqueante.ino ESP32
// 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).
}
El semáforo como máquina de estados: la misma secuencia y los mismos tiempos, sin ningún delay().

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.

Crear un panel de control en Adafruit IO

Ahora vamos a preparar la “central de monitoreo”. Usaremos Adafruit IO, una plataforma online con posibilidad de uso gratuito, orientada al uso educativo, que se encarga de centralizar la información que reportan los dispositivos. Primero debemos crear una cuenta de usuario en io.adafruit.com.

Una vez que ingresamos con nuestro usuario, el trabajo tiene dos partes: crear los feeds donde se publicarán los datos y armar el panel que los muestre.

Un feed es una fuente de datos en la que uno puede publicar, así como también se puede suscribir para recibir los datos de cierto feed. Desde la sección Feeds, creamos 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 semaforo1 con 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.

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.

semaforo-iot.ino ESP32
// 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();
}
El semáforo IoT completo: máquina de estados + WiFi del ESP32 + publicación en Adafruit IO.

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.

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