Saltar al contenido
Volver a la ficha del proyecto

Manual del proyecto · Proyecto 01

Sistema de Semaforización

Diseño y construcción de una maqueta/prototipo de un sistema de semáforos para la circulación ordenada y óptima del tráfico vehicular.

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

Saberes Digitales

INET / EDUCAR — Ministerio de Educación, Cultura, Ciencia y Tecnología de la Nación Argentina

2019

Contenido del manual

  1. 01 Materiales y equipamiento
  2. 02 Construir y programar un semáforo Nivel inicial
  3. 03 Dos semáforos sincronizados en una intersección Nivel intermedio
  4. 04 Monitoreo remoto con Internet de las Cosas Nivel avanzado

Materiales y equipamiento

Insumos

Imagen Insumo Cantidad Plataforma
Foto de LED 5 mm LED rojo 5 mm 2 Todas
Foto de LED 5 mm LED amarillo 5 mm 2 Todas
Foto de LED 5 mm LED verde 5 mm 2 Todas
Foto de Resistencia Resistencia de 220 ohm 10 Todas
Foto de Cables dupont macho-macho Cable dupont macho-hembra 20 Todas
Foto de Cables dupont macho-macho Cable dupont macho-macho 20 Todas
Foto de Protoboard Protoboard 1 Todas
Filamento PLA para impresora 3D Para imprimir la maqueta del semáforo (paquete de fabricación del proyecto). según modelo Todas
Foto de Arduino UNO R3 Arduino UNO R3 El original de 2019 sumaba un módulo IoT OBLOQ para el nivel avanzado; ese módulo está discontinuado. Con UNO, el nivel IoT se sigue de manera conceptual y la práctica se realiza con la variante ESP32. 1 Arduino UNO R3
Foto de Placa ESP32 DevKit Placa ESP32 DevKit v1 Reemplaza al conjunto Arduino UNO + módulo OBLOQ del original: el ESP32 trae WiFi integrado, necesario para el nivel avanzado (IoT). 1 ESP32 DevKit
Foto de Cable USB tipo B Cable USB tipo B 1 Arduino UNO R3
Foto de Cable micro USB Cable micro USB (con línea de datos) 1 ESP32 DevKit
Foto de Fuente de alimentación Fuente de 9 V 1 A (plug centro positivo, 5,5 x 2,1 mm) (opcional) Para alimentar la maqueta sin la computadora. El ESP32 se alimenta directamente por USB con una fuente de 5 V. 1 Arduino UNO R3

Equipamiento del taller

  • Computadora
  • Impresora 3D
  • Soldador
  • Estaño
  • Alicate
  • Pinza de punta
  • Pinza brusela

Plano de conexionado en protoboard

Arduino UNO Conexionado en protoboard · Semaforización de dos vías: 6 LEDs (verde/amarillo/rojo por semáforo) sobre pines digitales.

Deslizá horizontalmente para ver el plano completo

Arduino UNO
Semáforo 1 · verde
AK
Semáforo 1 · amarillo
AK
Semáforo 1 · rojo
AK
Semáforo 2 · verde
AK
Semáforo 2 · amarillo
AK
Semáforo 2 · rojo
AK
ESP32 DevKit v1 Conexionado en protoboard · Semaforización de dos vías: 6 LEDs (verde/amarillo/rojo por semáforo) sobre pines digitales.

Deslizá horizontalmente para ver el plano completo

ESP32 DevKit v1
Semáforo 1 · verde
AK
Semáforo 1 · amarillo
AK
Semáforo 1 · rojo
AK
Semáforo 2 · verde
AK
Semáforo 2 · amarillo
AK
Semáforo 2 · rojo
AK
Arduino UNOConexionado en protoboard
Conexiones · Arduino UNO. Convención de colores: cada cable de señal repite el color del LED que enciende (rojo → LED rojo, amarillo → LED amarillo, verde → LED verde); GND = negro, el retorno común de las resistencias de cada LED. No hay bus + porque los LED se alimentan directo desde el pin digital de la placa.
Desde Hacia Color de cable
Arduino UNO · GNDBus − de la protoboardnegro
Semáforo 1 · LED verde (ánodo)Arduino UNO · 11verde
Semáforo 1 · LED verde (cátodo)Resistencia 220 Ω (verde 1)negro
Resistencia 220 Ω (verde 1)Bus − de la protoboardnegro
Semáforo 1 · LED amarillo (ánodo)Arduino UNO · 12amarillo
Semáforo 1 · LED amarillo (cátodo)Resistencia 220 Ω (amarillo 1)negro
Resistencia 220 Ω (amarillo 1)Bus − de la protoboardnegro
Semáforo 1 · LED rojo (ánodo)Arduino UNO · 13rojo
Semáforo 1 · LED rojo (cátodo)Resistencia 220 Ω (rojo 1)negro
Resistencia 220 Ω (rojo 1)Bus − de la protoboardnegro
Semáforo 2 · LED verde (ánodo)Arduino UNO · 8verde
Semáforo 2 · LED verde (cátodo)Resistencia 220 Ω (verde 2)negro
Resistencia 220 Ω (verde 2)Bus − de la protoboardnegro
Semáforo 2 · LED amarillo (ánodo)Arduino UNO · 9amarillo
Semáforo 2 · LED amarillo (cátodo)Resistencia 220 Ω (amarillo 2)negro
Resistencia 220 Ω (amarillo 2)Bus − de la protoboardnegro
Semáforo 2 · LED rojo (ánodo)Arduino UNO · 10rojo
Semáforo 2 · LED rojo (cátodo)Resistencia 220 Ω (rojo 2)negro
Resistencia 220 Ω (rojo 2)Bus − de la protoboardnegro
ESP32 DevKit v1Conexionado en protoboard
Conexiones · ESP32 DevKit v1. Convención de colores: cada cable de señal repite el color del LED que enciende (rojo → LED rojo, amarillo → LED amarillo, verde → LED verde); GND = negro, el retorno común de las resistencias de cada LED. No hay bus + porque los LED se alimentan directo desde el pin digital de la placa.
Desde Hacia Color de cable
ESP32 DevKit v1 · GNDBus − de la protoboardnegro
Semáforo 1 · LED verde (ánodo)ESP32 DevKit v1 · GPIO16verde
Semáforo 1 · LED verde (cátodo)Resistencia 220 Ω (verde 1)negro
Resistencia 220 Ω (verde 1)Bus − de la protoboardnegro
Semáforo 1 · LED amarillo (ánodo)ESP32 DevKit v1 · GPIO17amarillo
Semáforo 1 · LED amarillo (cátodo)Resistencia 220 Ω (amarillo 1)negro
Resistencia 220 Ω (amarillo 1)Bus − de la protoboardnegro
Semáforo 1 · LED rojo (ánodo)ESP32 DevKit v1 · GPIO18rojo
Semáforo 1 · LED rojo (cátodo)Resistencia 220 Ω (rojo 1)negro
Resistencia 220 Ω (rojo 1)Bus − de la protoboardnegro
Semáforo 2 · LED verde (ánodo)ESP32 DevKit v1 · GPIO19verde
Semáforo 2 · LED verde (cátodo)Resistencia 220 Ω (verde 2)negro
Resistencia 220 Ω (verde 2)Bus − de la protoboardnegro
Semáforo 2 · LED amarillo (ánodo)ESP32 DevKit v1 · GPIO21amarillo
Semáforo 2 · LED amarillo (cátodo)Resistencia 220 Ω (amarillo 2)negro
Resistencia 220 Ω (amarillo 2)Bus − de la protoboardnegro
Semáforo 2 · LED rojo (ánodo)ESP32 DevKit v1 · GPIO22rojo
Semáforo 2 · LED rojo (cátodo)Resistencia 220 Ω (rojo 2)negro
Resistencia 220 Ω (rojo 2)Bus − de la protoboardnegro
Nivel inicial

Construir y programar un semáforo

Realizar el montaje sencillo de un circuito en una protoboard

Colocamos un LED verde en la protoboard, como indica el esquema.

Esquema de conexión: placa Arduino UNO conectada a una protoboard con un LED verde y una resistencia de 220 ohm. Un cable rojo une el pin 13 con la pata larga del LED y un cable negro une GND con la línea de la resistencia.
Esquema 1 — Un LED verde en la protoboard, con su resistencia de 220 ohm (figura original del proyecto).

En un LED, la pata larga siempre es la pata positiva y es donde se conecta nuestro pin. La pata corta es la negativa; también podés reconocerla por el lado plano del encapsulado. Si lo conectás con la polaridad invertida no se va a romper, pero tampoco va a encender.

La resistencia de 220 ohm limita la cantidad de corriente que circula por esa rama del circuito: sin ella, el LED podría quemarse por exceso de corriente. Va conectada entre la pata corta del LED y la línea de GND de la protoboard.

Conectamos la pata larga del LED al pin 13 de la placa Arduino UNO, y la línea de GND de la protoboard a uno de los pines GND de la placa.

En la placa ESP32 el armado es el mismo, pero los pines se llaman GPIO y usamos otros números (los GPIO 0, 2, 12 y 15 se reservan porque intervienen en el arranque de la placa). Conectá la pata larga del LED al GPIO que indica la constante PIN_LED al comienzo del código del paso siguiente, y la línea de GND de la protoboard a un pin GND de la placa.

Antes de pasar al programa, podés explorar el circuito completo del semáforo en el simulador. Es el mismo esquema que vas a armar sobre la mesa, y responde al comportamiento real del código:

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.

Escribir tu primer programa

Ahora vamos a escribir el programa que hace parpadear el LED que acabamos de conectar. La programación la realizaremos directamente en el lenguaje de Arduino (C++), usando el Arduino IDE en su versión 2.x, que se descarga gratis desde arduino.cc/en/software. IDE proviene de la sigla en inglés de entorno de desarrollo integrado: es el programa donde escribimos el código, lo compilamos y lo enviamos a la placa.

Un programa de Arduino se llama sketch y tiene una estructura mínima de dos bloques:

  1. Un bloque de código que se ejecuta por única vez al inicializarse el dispositivo. Este bloque está contenido dentro de la función setup().
  2. Un bloque de código que se ejecuta repetidamente luego de setup(). Este bloque está contenido dentro de la función loop().

Después de // se incluyen comentarios para el lector, que no tienen ningún efecto en el programa. Estos comentarios sirven para clarificar el código y que sea más fácil de interpretar para otras personas.

Con esa estructura, nuestro primer programa necesita tres ideas nuevas, una por línea:

  • pinMode(pin, OUTPUT) le avisa a la placa, dentro de setup(), que ese pin va a usarse como salida.
  • digitalWrite(pin, HIGH) enciende la salida (y con LOW la apaga).
  • delay(1000) detiene el programa durante 1000 milisegundos, es decir, un segundo.
Ver el código completo · parpadeo.ino
parpadeo.ino Arduino UNO
// Parpadeo de un LED: el "Hola Mundo" de la electrónica.
// Enciende y apaga un LED cada 1 segundo.
//
// Plataforma: Arduino UNO. El pin 13 tiene un LED integrado en la placa,
// así que este programa funciona incluso antes de conectar el LED externo.
//
// Basado en: Fragmento de código 1 — Sistema de Semaforización,
// Saberes Digitales (INET/EDUCAR 2019)
// Modernización: se reemplazó el código generado por mBlock3 (_delay, _loop)
// por C++ idiomático de Arduino, con constantes nombradas para pin y tiempos.

// Constantes con nombre en lugar de "números mágicos": si mañana movemos
// el LED a otro pin o queremos otro ritmo, tocamos UNA línea acá arriba
// y no hace falta revisar el resto del programa. Este hábito es la base
// de todos los programas que siguen en la guía.
const int PIN_LED = 13; // LED integrado del UNO

const unsigned long DURACION_ENCENDIDO_MS = 1000;
const unsigned long DURACION_APAGADO_MS   = 1000;

void setup() {
  // Un pin digital puede leer o escribir. Acá declaramos que este pin
  // va a ESCRIBIR (encender y apagar el LED).
  pinMode(PIN_LED, OUTPUT);
}

void loop() {
  // delay() detiene TODO el programa durante ese tiempo. Para un primer
  // programa es ideal: el código se lee de arriba hacia abajo igual que
  // la secuencia real de la luz. En el nivel avanzado veremos por qué
  // esto se vuelve un problema (código "bloqueante") y cómo resolverlo
  // con millis().
  digitalWrite(PIN_LED, HIGH); // encender
  delay(DURACION_ENCENDIDO_MS);
  digitalWrite(PIN_LED, LOW);  // apagar
  delay(DURACION_APAGADO_MS);
}
parpadeo.ino ESP32
// Parpadeo de un LED: el "Hola Mundo" de la electrónica.
// Enciende y apaga un LED cada 1 segundo.
//
// Plataforma: ESP32 (DevKit v1). El GPIO 2 tiene un LED azul integrado
// en la placa, así que este programa funciona incluso antes de conectar
// el LED externo.
// Nota eléctrica: el ESP32 trabaja a 3,3 V (no 5 V como el UNO). Con la
// misma resistencia de 220 ohm el LED enciende un poco más tenue, y NUNCA
// hay que inyectar 5 V en un GPIO del ESP32: se puede dañar la placa.
//
// Basado en: Fragmento de código 1 — Sistema de Semaforización,
// Saberes Digitales (INET/EDUCAR 2019)
// Modernización: se reemplazó el código generado por mBlock3 (_delay, _loop)
// por C++ idiomático de Arduino con constantes nombradas, y se portó la
// guía (pensada para Arduino UNO) a ESP32.

// Constantes con nombre en lugar de "números mágicos": si mañana movemos
// el LED a otro pin o queremos otro ritmo, tocamos UNA línea acá arriba
// y no hace falta revisar el resto del programa. Este hábito es la base
// de todos los programas que siguen en la guía.
const int PIN_LED = 2; // LED integrado del DevKit v1

const unsigned long DURACION_ENCENDIDO_MS = 1000;
const unsigned long DURACION_APAGADO_MS   = 1000;

void setup() {
  // Un pin digital puede leer o escribir. Acá declaramos que este pin
  // va a ESCRIBIR (encender y apagar el LED).
  pinMode(PIN_LED, OUTPUT);
}

void loop() {
  // delay() detiene TODO el programa durante ese tiempo. Para un primer
  // programa es ideal: el código se lee de arriba hacia abajo igual que
  // la secuencia real de la luz. En el nivel avanzado veremos por qué
  // esto se vuelve un problema (código "bloqueante") y cómo resolverlo
  // con millis().
  digitalWrite(PIN_LED, HIGH); // encender
  delay(DURACION_ENCENDIDO_MS);
  digitalWrite(PIN_LED, LOW);  // apagar
  delay(DURACION_APAGADO_MS);
}
Primer programa: el LED parpadea en intervalos de un segundo.

Leelo en voz alta como si fuera una receta: “encendé la luz, esperá un segundo, apagala, esperá un segundo… y volvé a empezar”. Eso es exactamente lo que loop() repite sin descanso mientras la placa tenga energía. Antes de cargarlo en la placa, podés probar el mismo comportamiento en el simulador del paso anterior.

Subir el código a la placa

Para subir el código de nuestro programa a la placa, necesitamos:

  1. Conectar la placa a la computadora con el cable USB.
  2. Chequear que en el selector de placa del Arduino IDE esté seleccionada la placa correcta.
  3. Seleccionar el puerto al que está conectada la placa.
  4. Cliquear el botón Subir (la flecha →) en la parte superior de la interfaz.

En el Arduino IDE 2.x, la placa y el puerto se eligen juntos en el menú desplegable de la barra superior: al conectar la placa suele aparecer directamente como opción. Si no aparece, entrá a “Select other board and port” y buscala por nombre.

Para el Arduino UNO: usá el cable USB tipo B, elegí la placa Arduino UNO y el puerto que se agregó al conectarla (en Windows es un puerto COM numerado; en Linux suele llamarse /dev/ttyACM0 o similar).

Para el ESP32 hay dos pasos previos que se hacen una sola vez: instalar el soporte de placas ESP32 desde el gestor de placas del IDE (buscá “esp32” e instalá el paquete de Espressif) y, si el puerto no aparece, instalar el driver del conversor USB-serie de tu placa (generalmente CP210x o CH340). Después, elegí la placa ESP32 Dev Module (o “DOIT ESP32 DEVKIT V1”) y el puerto correspondiente. Si al subir el IDE queda esperando en “Connecting…”, mantené apretado el botón BOOT de la placa hasta que empiece la carga.

Al terminar de subir nuestro código, veremos el mensaje de carga completada en la barra inferior del IDE.

Expandir el circuito a tres LED

Continuamos agregando tres LED, uno de cada color (verde, amarillo y rojo), con sus respectivas resistencias de 220 ohm, como indica el circuito.

Esquema de conexión: Arduino UNO con tres LED (verde, amarillo y rojo) en la protoboard, cada uno con su resistencia de 220 ohm hacia la línea de GND.
Esquema 2 — El circuito expandido a tres LED, uno por cada luz del semáforo (figura original del proyecto).

Colocamos todas las resistencias hacia la línea de GND de la protoboard, que se conecta al pin GND de la placa.

Conectamos el LED rojo al pin 13, el amarillo al pin 12 y el verde al pin 11.

En el ESP32 usamos tres GPIO de propósito general seguros para salidas. Los números exactos están en las constantes PIN_LED_ROJO, PIN_LED_AMARILLO y PIN_LED_VERDE al comienzo del código: conectá cada LED al GPIO que indica su constante.

Ahora modificamos el programa anterior para que realice ciclos de encendido y apagado para las luces de los tres colores, con los intervalos de demora de un semáforo real: verde 5 segundos, amarillo 1 segundo, rojo 5 segundos y, antes de volver al verde, rojo y amarillo juntos durante 1 segundo (el aviso de “preparate para avanzar”).

Ver el código completo · semaforo-simple.ino
semaforo-simple.ino Arduino UNO
// Semáforo simple de tres luces (un solo semáforo).
// Secuencia: verde 5 s → amarillo 1 s → rojo 5 s → rojo+amarillo 1 s,
// y vuelve a empezar. La fase rojo+amarillo avisa que está por venir
// el verde, como en los semáforos reales de varios países.
//
// Plataforma: Arduino UNO.
// Conexión: LED verde → pin 11, LED amarillo → pin 12, LED rojo → pin 13,
// cada uno con su resistencia de 220 ohm a GND.
//
// Basado en: Fragmento de código 2 — Sistema de Semaforización,
// Saberes Digitales (INET/EDUCAR 2019)
// Modernización: se reemplazó el código generado por mBlock3 (_delay, _loop)
// por C++ idiomático de Arduino, con constantes nombradas para pines y tiempos.

// Pines del semáforo 1 (los mismos del esquema original de la guía).
const int PIN_SEM1_VERDE    = 11;
const int PIN_SEM1_AMARILLO = 12;
const int PIN_SEM1_ROJO     = 13;

// Tiempos de cada fase. Son los MISMOS en toda la guía (y en el simulador):
// si acá se cambian, el comportamiento deja de coincidir con el resto.
const unsigned long DURACION_VERDE_MS         = 5000;
const unsigned long DURACION_AMARILLO_MS      = 1000;
const unsigned long DURACION_ROJO_MS          = 5000;
const unsigned long DURACION_ROJO_AMARILLO_MS = 1000;

void setup() {
  pinMode(PIN_SEM1_VERDE, OUTPUT);
  pinMode(PIN_SEM1_AMARILLO, OUTPUT);
  pinMode(PIN_SEM1_ROJO, OUTPUT);
}

void loop() {
  // En cada fase escribimos las TRES luces, aunque alguna no cambie:
  // así cada bloque describe el estado completo del semáforo y no hay
  // que "acordarse" de qué quedó encendido en la fase anterior.

  // Fase 1: VERDE — los autos circulan.
  digitalWrite(PIN_SEM1_VERDE, HIGH);
  digitalWrite(PIN_SEM1_AMARILLO, LOW);
  digitalWrite(PIN_SEM1_ROJO, LOW);
  delay(DURACION_VERDE_MS);

  // Fase 2: AMARILLO — atención, prepararse para frenar.
  digitalWrite(PIN_SEM1_VERDE, LOW);
  digitalWrite(PIN_SEM1_AMARILLO, HIGH);
  digitalWrite(PIN_SEM1_ROJO, LOW);
  delay(DURACION_AMARILLO_MS);

  // Fase 3: ROJO — detenerse.
  digitalWrite(PIN_SEM1_VERDE, LOW);
  digitalWrite(PIN_SEM1_AMARILLO, LOW);
  digitalWrite(PIN_SEM1_ROJO, HIGH);
  delay(DURACION_ROJO_MS);

  // Fase 4: ROJO + AMARILLO — aviso de que está por venir el verde.
  digitalWrite(PIN_SEM1_VERDE, LOW);
  digitalWrite(PIN_SEM1_AMARILLO, HIGH);
  digitalWrite(PIN_SEM1_ROJO, HIGH);
  delay(DURACION_ROJO_AMARILLO_MS);

  // Al terminar la fase 4, loop() vuelve a ejecutarse desde arriba:
  // ese "volver a empezar" automático es lo que hace el ciclo infinito.
}
semaforo-simple.ino ESP32
// Semáforo simple de tres luces (un solo semáforo).
// Secuencia: verde 5 s → amarillo 1 s → rojo 5 s → rojo+amarillo 1 s,
// y vuelve a empezar. La fase rojo+amarillo avisa que está por venir
// el verde, como en los semáforos reales de varios países.
//
// Plataforma: ESP32 (DevKit v1).
// Conexión: LED verde → GPIO 16, LED amarillo → GPIO 17, LED rojo → GPIO 18,
// cada uno con su resistencia de 220 ohm a GND.
// Nota eléctrica: el ESP32 trabaja a 3,3 V (no 5 V como el UNO). Con la
// misma resistencia de 220 ohm los LED encienden un poco más tenues, y
// NUNCA hay que inyectar 5 V en un GPIO del ESP32.
//
// Basado en: Fragmento de código 2 — Sistema de Semaforización,
// Saberes Digitales (INET/EDUCAR 2019)
// Modernización: se reemplazó el código generado por mBlock3 (_delay, _loop)
// por C++ idiomático de Arduino con constantes nombradas, y se portó a ESP32
// usando GPIO seguros (16/17/18, lejos de los pines de arranque o "strapping").

// Pines del semáforo 1.
const int PIN_SEM1_VERDE    = 16;
const int PIN_SEM1_AMARILLO = 17;
const int PIN_SEM1_ROJO     = 18;

// Tiempos de cada fase. Son los MISMOS en toda la guía (y en el simulador):
// si acá se cambian, el comportamiento deja de coincidir con el resto.
const unsigned long DURACION_VERDE_MS         = 5000;
const unsigned long DURACION_AMARILLO_MS      = 1000;
const unsigned long DURACION_ROJO_MS          = 5000;
const unsigned long DURACION_ROJO_AMARILLO_MS = 1000;

void setup() {
  pinMode(PIN_SEM1_VERDE, OUTPUT);
  pinMode(PIN_SEM1_AMARILLO, OUTPUT);
  pinMode(PIN_SEM1_ROJO, OUTPUT);
}

void loop() {
  // En cada fase escribimos las TRES luces, aunque alguna no cambie:
  // así cada bloque describe el estado completo del semáforo y no hay
  // que "acordarse" de qué quedó encendido en la fase anterior.

  // Fase 1: VERDE — los autos circulan.
  digitalWrite(PIN_SEM1_VERDE, HIGH);
  digitalWrite(PIN_SEM1_AMARILLO, LOW);
  digitalWrite(PIN_SEM1_ROJO, LOW);
  delay(DURACION_VERDE_MS);

  // Fase 2: AMARILLO — atención, prepararse para frenar.
  digitalWrite(PIN_SEM1_VERDE, LOW);
  digitalWrite(PIN_SEM1_AMARILLO, HIGH);
  digitalWrite(PIN_SEM1_ROJO, LOW);
  delay(DURACION_AMARILLO_MS);

  // Fase 3: ROJO — detenerse.
  digitalWrite(PIN_SEM1_VERDE, LOW);
  digitalWrite(PIN_SEM1_AMARILLO, LOW);
  digitalWrite(PIN_SEM1_ROJO, HIGH);
  delay(DURACION_ROJO_MS);

  // Fase 4: ROJO + AMARILLO — aviso de que está por venir el verde.
  digitalWrite(PIN_SEM1_VERDE, LOW);
  digitalWrite(PIN_SEM1_AMARILLO, HIGH);
  digitalWrite(PIN_SEM1_ROJO, HIGH);
  delay(DURACION_ROJO_AMARILLO_MS);

  // Al terminar la fase 4, loop() vuelve a ejecutarse desde arriba:
  // ese "volver a empezar" automático es lo que hace el ciclo infinito.
}
El semáforo completo: la secuencia de las tres luces con sus tiempos.

Fijate que el programa es una estructura secuencial: las órdenes se ejecutan una detrás de la otra, en el orden exacto en que están escritas, y al llegar al final loop() vuelve a empezar. Ese orden es la “coreografía” del semáforo.

Podés volver al simulador del paso 1 para comparar el comportamiento esperado con el de tu circuito real.

Soldar y ensamblar la maqueta

En este momento se procederá a construir la maqueta del semáforo. Para ello se debe soldar cada componente e imprimir el modelo 3D del semáforo que alojará el circuito de las luces. Tené en cuenta que si se pretende seguir con el nivel intermedio y agregar un segundo semáforo, puede resultar más conveniente saltar este paso y continuar con los siguientes, para programar la conexión de los dos semáforos en la protoboard antes del armado final.

Render del modelo 3D del cuerpo del semáforo: una pieza vertical con tres alojamientos circulares para los LED y una base de encastre.
El modelo 3D original del semáforo, diseñado para alojar los tres LED (figura original del proyecto).

Para armar la maqueta, se deben imprimir las piezas en la impresora 3D. El modelo original del semáforo se distribuía de forma libre y gratuita en el repositorio Saberes Digitales; sus archivos están en re-publicación como paquete de fabricación de este proyecto (vas a encontrarlo en la ficha, junto con un rediseño de la maqueta para corte láser en MDF de 3 mm). Cuando estén listas todas las piezas, las ensamblamos para construir el semáforo.

En caso de querer realizar una modificación en el modelo, independientemente del programa de modelado que utilicemos, debemos exportar nuestras piezas en formato .stl. El .stl es un formato de archivo de diseño asistido por computadora (CAD) que define la geometría de objetos sólidos 3D; es el más popular a la hora de intercambiar digitalmente modelos para ser impresos en 3D. El programa laminador de la impresora convierte ese .stl en código G: las instrucciones de movimiento que tu impresora ejecuta para materializar la pieza.

Una vez que las conexiones han sido probadas y el sistema de luces funciona correctamente, podemos pasar a armar los circuitos de la maqueta prescindiendo de la protoboard. Para hacerlo, soldamos cada LED con su cable y su resistencia. Luego, lo colocamos dentro de la pieza correspondiente del semáforo.

Render translúcido del modelo 3D del semáforo donde se ven los tres LED (rojo, amarillo y verde) alojados en su interior con sus cables bajando por el poste.
Los tres LED soldados y alojados dentro del cuerpo del semáforo; los cables bajan por el poste hacia la placa (figura original del proyecto).

Cierre del nivel

Al terminar este nivel, tu semáforo tiene que estar funcionando solo: el circuito de tres LED montado con sus resistencias, el programa cargado desde el Arduino IDE, y la secuencia verde → amarillo → rojo → rojo + amarillo repitiéndose en ciclo con sus tiempos. Si además ensamblaste la maqueta impresa en 3D, ya tenés un prototipo digno de una esquina de verdad. En el nivel intermedio ese semáforo deja de estar solo: vamos a sumarle el segundo semáforo de la intersección y a sincronizarlos.

Evidencia de cierre

Al terminar este nivel, esto tiene que estar funcionando:

  • El circuito de tres LED (rojo, amarillo y verde) está montado en la protoboard, cada uno con su resistencia de 220 ohm y con la polaridad correcta.
  • La placa ejecuta el programa del semáforo: verde 5 segundos, amarillo 1 segundo, rojo 5 segundos y rojo + amarillo 1 segundo, en un ciclo que se repite solo.
  • Sabés subir un programa desde el Arduino IDE 2.x: elegir la placa, elegir el puerto y verificar que el código quedó cargado.
  • La maqueta del semáforo está ensamblada con los LED soldados (o el paso quedó planificado para después del nivel intermedio).
Nivel intermedio

Dos semáforos sincronizados en una intersección

Armar el circuito de la intersección

Para armar la intersección con dos semáforos debemos conectar a la protoboard tres LED más que en el circuito anterior (rojo, amarillo y verde), con sus respectivas resistencias de 220 ohm hacia la línea de GND.

Esquema de conexión: Arduino UNO con seis LED en la protoboard formando dos semáforos, rotulados Semáforo 1 y Semáforo 2, cada LED con su resistencia de 220 ohm.
Esquema de la intersección: los seis LED forman el semáforo 1 y el semáforo 2, controlados por la misma placa (figura original del proyecto).

El primer semáforo conserva sus pines del nivel inicial (rojo 13, amarillo 12, verde 11). Para el segundo semáforo conectaremos el LED rojo al pin 10, el amarillo al pin 9 y el verde al pin 8.

En el ESP32, cada uno de los seis LED va a un GPIO de salida seguro. Los números exactos están en las constantes del comienzo del código (PIN_S1_ROJO, PIN_S1_AMARILLO, PIN_S1_VERDE, PIN_S2_ROJO, PIN_S2_AMARILLO, PIN_S2_VERDE): conectá cada LED al GPIO que indica su constante.

Podés recorrer el circuito completo, con los dos semáforos funcionando en la esquina, 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.

Programar la secuencia para dos semáforos

En este momento vamos a sumar la secuencia de nuestro segundo semáforo al código previamente armado. La planificación de la secuencia para dos semáforos es más compleja, por lo que requiere que se le dedique un poco más de tiempo a su armado y comprensión.

Para pensar cómo armar el código, es posible imaginar esta secuencia como viñetas de un cómic en donde hay una serie de acciones a realizar y una demora entre cuadro y cuadro. Cada viñeta muestra la esquina congelada en un instante: qué luz tiene cada semáforo y cuánto tiempo dura ese cuadro antes de pasar al siguiente.

ViñetaSemáforo 1Semáforo 2Demora
Nº 1VerdeRojo5 s
Nº 2AmarilloRojo1 s
Nº 3RojoRojo1 s
Nº 4RojoRojo + amarillo1 s
Nº 5RojoVerde5 s
Nº 6RojoAmarillo1 s
Nº 7RojoRojo1 s
Nº 8Rojo + amarilloRojo1 s
Nº 9VerdeRojo

Como se puede ver en la tabla, el cuadro Nº 9 es igual al cuadro Nº 1: es en ese punto donde comienza a repetirse la secuencia. Por lo tanto, en total tenemos ocho pasos que se repiten y una determinada demora en cada paso.

Dos detalles de esta coreografía merecen atención:

  • Nunca las dos calles tienen verde a la vez: cuando un semáforo habilita el paso, el otro está en rojo.
  • En las viñetas Nº 3 y Nº 7 ambos semáforos están en rojo durante un segundo. Ese respiro le da tiempo al último vehículo a despejar la bocacalle antes de habilitar a la calle transversal.

Traducimos las ocho viñetas al programa: cada viñeta es un grupo de órdenes de encendido y apagado seguido de su demora, en estructura secuencial.

Ver el código completo · interseccion.ino
interseccion.ino Arduino UNO
// Intersección con dos semáforos sincronizados (6 LED).
// Recorre los 8 cuadros del "cómic" de la Fig. 10 de la guía original:
//
//   Cuadro | 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
//
// El cuadro 9 es igual al 1: ahí la secuencia vuelve a empezar.
// Los cuadros 3 y 7 dejan TODO en rojo un instante: es el margen de
// seguridad para que la intersección se vacíe antes de habilitar la
// otra calle. Ningún semáforo real pasa de verde a verde sin ese margen.
//
// Plataforma: Arduino UNO.
// Conexión: semáforo 1 → verde 11, amarillo 12, rojo 13;
//           semáforo 2 → verde 8, amarillo 9, rojo 10 (como en la guía).
//
// Basado en: Fragmento de código 3 — Sistema de Semaforización,
// Saberes Digitales (INET/EDUCAR 2019)
// Modernización: se reemplazó el código generado por mBlock3 (_delay, _loop)
// por C++ idiomático de Arduino con constantes nombradas. Además, cada
// cuadro escribe las SEIS luces completas (el original solo escribía los
// pines que cambiaban): así cada bloque de código se corresponde 1 a 1
// con un cuadro del cómic y puede leerse de forma aislada.

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

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);
}

void loop() {
  // Cuadro 1: Semáforo 1 VERDE / Semáforo 2 ROJO.
  digitalWrite(PIN_SEM1_VERDE, HIGH);
  digitalWrite(PIN_SEM1_AMARILLO, LOW);
  digitalWrite(PIN_SEM1_ROJO, LOW);
  digitalWrite(PIN_SEM2_VERDE, LOW);
  digitalWrite(PIN_SEM2_AMARILLO, LOW);
  digitalWrite(PIN_SEM2_ROJO, HIGH);
  delay(DURACION_VERDE_MS);

  // Cuadro 2: Semáforo 1 AMARILLO / Semáforo 2 ROJO.
  digitalWrite(PIN_SEM1_VERDE, LOW);
  digitalWrite(PIN_SEM1_AMARILLO, HIGH);
  digitalWrite(PIN_SEM1_ROJO, LOW);
  digitalWrite(PIN_SEM2_VERDE, LOW);
  digitalWrite(PIN_SEM2_AMARILLO, LOW);
  digitalWrite(PIN_SEM2_ROJO, HIGH);
  delay(DURACION_AMARILLO_MS);

  // Cuadro 3: TODO ROJO — la intersección se vacía antes del cambio.
  digitalWrite(PIN_SEM1_VERDE, LOW);
  digitalWrite(PIN_SEM1_AMARILLO, LOW);
  digitalWrite(PIN_SEM1_ROJO, HIGH);
  digitalWrite(PIN_SEM2_VERDE, LOW);
  digitalWrite(PIN_SEM2_AMARILLO, LOW);
  digitalWrite(PIN_SEM2_ROJO, HIGH);
  delay(DURACION_TODO_ROJO_MS);

  // Cuadro 4: Semáforo 1 ROJO / Semáforo 2 ROJO + AMARILLO (por arrancar).
  digitalWrite(PIN_SEM1_VERDE, LOW);
  digitalWrite(PIN_SEM1_AMARILLO, LOW);
  digitalWrite(PIN_SEM1_ROJO, HIGH);
  digitalWrite(PIN_SEM2_VERDE, LOW);
  digitalWrite(PIN_SEM2_AMARILLO, HIGH);
  digitalWrite(PIN_SEM2_ROJO, HIGH);
  delay(DURACION_ROJO_AMARILLO_MS);

  // Cuadro 5: Semáforo 1 ROJO / Semáforo 2 VERDE — ahora circula la otra calle.
  digitalWrite(PIN_SEM1_VERDE, LOW);
  digitalWrite(PIN_SEM1_AMARILLO, LOW);
  digitalWrite(PIN_SEM1_ROJO, HIGH);
  digitalWrite(PIN_SEM2_VERDE, HIGH);
  digitalWrite(PIN_SEM2_AMARILLO, LOW);
  digitalWrite(PIN_SEM2_ROJO, LOW);
  delay(DURACION_VERDE_MS);

  // Cuadro 6: Semáforo 1 ROJO / Semáforo 2 AMARILLO.
  digitalWrite(PIN_SEM1_VERDE, LOW);
  digitalWrite(PIN_SEM1_AMARILLO, LOW);
  digitalWrite(PIN_SEM1_ROJO, HIGH);
  digitalWrite(PIN_SEM2_VERDE, LOW);
  digitalWrite(PIN_SEM2_AMARILLO, HIGH);
  digitalWrite(PIN_SEM2_ROJO, LOW);
  delay(DURACION_AMARILLO_MS);

  // Cuadro 7: TODO ROJO — otra vez el margen de seguridad.
  digitalWrite(PIN_SEM1_VERDE, LOW);
  digitalWrite(PIN_SEM1_AMARILLO, LOW);
  digitalWrite(PIN_SEM1_ROJO, HIGH);
  digitalWrite(PIN_SEM2_VERDE, LOW);
  digitalWrite(PIN_SEM2_AMARILLO, LOW);
  digitalWrite(PIN_SEM2_ROJO, HIGH);
  delay(DURACION_TODO_ROJO_MS);

  // Cuadro 8: Semáforo 1 ROJO + AMARILLO / Semáforo 2 ROJO.
  digitalWrite(PIN_SEM1_VERDE, LOW);
  digitalWrite(PIN_SEM1_AMARILLO, HIGH);
  digitalWrite(PIN_SEM1_ROJO, HIGH);
  digitalWrite(PIN_SEM2_VERDE, LOW);
  digitalWrite(PIN_SEM2_AMARILLO, LOW);
  digitalWrite(PIN_SEM2_ROJO, HIGH);
  delay(DURACION_ROJO_AMARILLO_MS);

  // loop() vuelve a empezar: el cuadro 9 del cómic es igual al cuadro 1.
}
interseccion.ino ESP32
// Intersección con dos semáforos sincronizados (6 LED).
// Recorre los 8 cuadros del "cómic" de la Fig. 10 de la guía original:
//
//   Cuadro | 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
//
// El cuadro 9 es igual al 1: ahí la secuencia vuelve a empezar.
// Los cuadros 3 y 7 dejan TODO en rojo un instante: es el margen de
// seguridad para que la intersección se vacíe antes de habilitar la
// otra calle. Ningún semáforo real pasa de verde a verde sin ese margen.
//
// 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 3 — Sistema de Semaforización,
// Saberes Digitales (INET/EDUCAR 2019)
// Modernización: se reemplazó el código generado por mBlock3 (_delay, _loop)
// por C++ idiomático de Arduino con constantes nombradas y se portó a ESP32
// con GPIO seguros (se evitan los pines de arranque o "strapping"). Además,
// cada cuadro escribe las SEIS luces completas (el original solo escribía
// los pines que cambiaban): así cada bloque de código se corresponde 1 a 1
// con un cuadro del cómic y puede leerse de forma aislada.

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

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);
}

void loop() {
  // Cuadro 1: Semáforo 1 VERDE / Semáforo 2 ROJO.
  digitalWrite(PIN_SEM1_VERDE, HIGH);
  digitalWrite(PIN_SEM1_AMARILLO, LOW);
  digitalWrite(PIN_SEM1_ROJO, LOW);
  digitalWrite(PIN_SEM2_VERDE, LOW);
  digitalWrite(PIN_SEM2_AMARILLO, LOW);
  digitalWrite(PIN_SEM2_ROJO, HIGH);
  delay(DURACION_VERDE_MS);

  // Cuadro 2: Semáforo 1 AMARILLO / Semáforo 2 ROJO.
  digitalWrite(PIN_SEM1_VERDE, LOW);
  digitalWrite(PIN_SEM1_AMARILLO, HIGH);
  digitalWrite(PIN_SEM1_ROJO, LOW);
  digitalWrite(PIN_SEM2_VERDE, LOW);
  digitalWrite(PIN_SEM2_AMARILLO, LOW);
  digitalWrite(PIN_SEM2_ROJO, HIGH);
  delay(DURACION_AMARILLO_MS);

  // Cuadro 3: TODO ROJO — la intersección se vacía antes del cambio.
  digitalWrite(PIN_SEM1_VERDE, LOW);
  digitalWrite(PIN_SEM1_AMARILLO, LOW);
  digitalWrite(PIN_SEM1_ROJO, HIGH);
  digitalWrite(PIN_SEM2_VERDE, LOW);
  digitalWrite(PIN_SEM2_AMARILLO, LOW);
  digitalWrite(PIN_SEM2_ROJO, HIGH);
  delay(DURACION_TODO_ROJO_MS);

  // Cuadro 4: Semáforo 1 ROJO / Semáforo 2 ROJO + AMARILLO (por arrancar).
  digitalWrite(PIN_SEM1_VERDE, LOW);
  digitalWrite(PIN_SEM1_AMARILLO, LOW);
  digitalWrite(PIN_SEM1_ROJO, HIGH);
  digitalWrite(PIN_SEM2_VERDE, LOW);
  digitalWrite(PIN_SEM2_AMARILLO, HIGH);
  digitalWrite(PIN_SEM2_ROJO, HIGH);
  delay(DURACION_ROJO_AMARILLO_MS);

  // Cuadro 5: Semáforo 1 ROJO / Semáforo 2 VERDE — ahora circula la otra calle.
  digitalWrite(PIN_SEM1_VERDE, LOW);
  digitalWrite(PIN_SEM1_AMARILLO, LOW);
  digitalWrite(PIN_SEM1_ROJO, HIGH);
  digitalWrite(PIN_SEM2_VERDE, HIGH);
  digitalWrite(PIN_SEM2_AMARILLO, LOW);
  digitalWrite(PIN_SEM2_ROJO, LOW);
  delay(DURACION_VERDE_MS);

  // Cuadro 6: Semáforo 1 ROJO / Semáforo 2 AMARILLO.
  digitalWrite(PIN_SEM1_VERDE, LOW);
  digitalWrite(PIN_SEM1_AMARILLO, LOW);
  digitalWrite(PIN_SEM1_ROJO, HIGH);
  digitalWrite(PIN_SEM2_VERDE, LOW);
  digitalWrite(PIN_SEM2_AMARILLO, HIGH);
  digitalWrite(PIN_SEM2_ROJO, LOW);
  delay(DURACION_AMARILLO_MS);

  // Cuadro 7: TODO ROJO — otra vez el margen de seguridad.
  digitalWrite(PIN_SEM1_VERDE, LOW);
  digitalWrite(PIN_SEM1_AMARILLO, LOW);
  digitalWrite(PIN_SEM1_ROJO, HIGH);
  digitalWrite(PIN_SEM2_VERDE, LOW);
  digitalWrite(PIN_SEM2_AMARILLO, LOW);
  digitalWrite(PIN_SEM2_ROJO, HIGH);
  delay(DURACION_TODO_ROJO_MS);

  // Cuadro 8: Semáforo 1 ROJO + AMARILLO / Semáforo 2 ROJO.
  digitalWrite(PIN_SEM1_VERDE, LOW);
  digitalWrite(PIN_SEM1_AMARILLO, HIGH);
  digitalWrite(PIN_SEM1_ROJO, HIGH);
  digitalWrite(PIN_SEM2_VERDE, LOW);
  digitalWrite(PIN_SEM2_AMARILLO, LOW);
  digitalWrite(PIN_SEM2_ROJO, HIGH);
  delay(DURACION_ROJO_AMARILLO_MS);

  // loop() vuelve a empezar: el cuadro 9 del cómic es igual al cuadro 1.
}
La secuencia de ocho estados para los dos semáforos de la intersección.

Cierre del nivel

Al terminar este nivel, la intersección completa funciona con una sola placa: los seis LED conectados, y los dos semáforos recorriendo en ciclo la secuencia de ocho estados, con sus momentos de “todo rojo” y de rojo + amarillo. Además, podés contar la secuencia cuadro a cuadro, como un cómic, que es la mejor prueba de que la entendiste. Si habías postergado la soldadura y el ensamblado de las maquetas, este es el momento de retomar el paso 5 del nivel inicial para las dos columnas. En el nivel avanzado, la central de tránsito va a poder mirar esta esquina desde internet: vamos a sumarle Internet de las Cosas.

Evidencia de cierre

Al terminar este nivel, esto tiene que estar funcionando:

  • Los seis LED de la intersección (dos semáforos completos) están conectados a una sola placa, cada uno con su resistencia.
  • Los dos semáforos recorren la secuencia completa de ocho estados en ciclo, sin habilitar nunca las dos calles al mismo tiempo.
  • Podés explicar la secuencia cuadro a cuadro, como las viñetas de un cómic, incluyendo los momentos en que ambos semáforos quedan en rojo.
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).