La transformación digital en educación no es solo una tendencia, sino una necesidad imperativa en el siglo XXI. En esta serie de tutoriales, exploraremos cómo el ESP32, un microcontrolador versátil y económico, puede convertirse en una herramienta poderosa para enseñar IoT (Internet de las Cosas) de manera práctica y accesible.
🎯 ¿Qué aprenderás en este tutorial?
Al finalizar esta primera parte, serás capaz de:
- Comprender por qué el ESP32 es el estándar educativo para proyectos IoT.
- Identificar los 3 componentes clave de una arquitectura IoT básica.
- Interactuar con un simulador de hardware (Wokwi) para enviar datos de sensores.
- Ejecutar un script en Python para capturar datos en tiempo real mediante el protocolo MQTT.
1. El Cerebro del Proyecto: ¿Por qué ESP32 para la Educación?
Antes de programar, debemos entender nuestra herramienta. El ESP32 es un microcontrolador de bajo costo que se ha ganado su lugar en las aulas porque ofrece:
- Conectividad nativa: WiFi y Bluetooth integrados en la misma placa.
- Potencia: 240 MHz de velocidad de procesamiento (mucho más rápido que un Arduino tradicional).
- Versatilidad: Capacidad de trabajar con múltiples protocolos de Internet (MQTT, HTTP, WebSocket).
- Accesibilidad: Compatibilidad total con Arduino IDE y una comunidad de soporte gigante.
💡 Nota del Profesor: Estas características permiten a estudiantes y educadores crear proyectos reales de IoT sin necesidad de invertir presupuestos gigantescos en hardware industrial.
2. Entendiendo la Arquitectura del Sistema
Para que un dato viaje desde un sensor hasta nuestra pantalla, necesitamos una arquitectura. En este proyecto, utilizaremos el modelo Cliente-Servidor (Publicador-Suscriptor) a través del protocolo MQTT.
Nuestro sistema tiene tres componentes principales:
- Dispositivo IoT (El Emisor): Nuestro ESP32 simulado en Wokwi. Leerá los datos de temperatura y humedad, y los "publicará".
- Servidor / Broker MQTT (El Mensajero): Un servidor intermedio que recibe los mensajes del ESP32 y los reparte a quien esté interesado.
- Cliente Python (El Receptor): Un programa en nuestra computadora que se "suscribe" para escuchar al servidor y mostrar los datos en tiempo real.
┌─────────────────────┐ MQTT (pub/sub) ┌──────────────────────┐
│ ESP32 + DHT22 │ <───────────────────────────>│ cliente_iot.py │
│ (firmware en C++) │ Broker: crystalmq │ (menú en terminal) │
│ │ │ │
│ - Lee temp/humedad │ │ - Envía ON/OFF │
│ - Enciende/apaga LED│ │ - Muestra temp/hum │
└─────────────────────┘ └──────────────────────┘
3. Nuestro Laboratorio Virtual: Simulando en Wokwi
Para esta serie, utilizaremos un proyecto simulado llamado mqtt_temperatura_humedad_led. El simulador nos permite prototipar sin hardware físico, eliminando barreras y facilitando el aprendizaje.
🛠️ Actividad Práctica:
- Interactúa con el simulador a continuación.
- Inicia la simulación presionando el botón de "Play".
- Observa la consola serial en la parte inferior: verás cómo el ESP32 se conecta al WiFi y comienza a enviar datos.
- Interactúa con el sensor DHT22 en la pantalla para cambiar la temperatura manualmente.
Puede ver el proyecto demo en Wokwi https://wokwi.com/projects/461265097276998657
4. Manos a la Obra: Capturando Datos con Python
Ahora que nuestro ESP32 virtual está enviando datos al mundo, vamos a atraparlos usando Python.
Para facilitar el aprendizaje y fomentar las buenas prácticas de desarrollo, he preparado un repositorio en GitHub con todo el código fuente estructurado y listo para usar.
⚠️ Importante: En este primer tutorial utilizaremos el broker MQTT público de Bevywise (
crystalmq.bevywise.com) para que puedas ver resultados inmediatos. En la Parte 2, aprenderemos a montar nuestro propio servidor local seguro.
Paso 1: Descargar el Proyecto desde GitHub
Abre tu terminal o símbolo del sistema y clona el repositorio oficial del proyecto. Esto descargará todos los archivos necesarios directamente a tu computadora:
Bash
git clone https://github.com/hernanramirez/MqttLedDTH22.git
cd MqttLedDTH22
Paso 2: Configurar el Entorno Virtual
Para mantener tu sistema limpio, crearemos un entorno virtual aislado donde instalaremos las librerías necesarias (como paho-mqtt):
Bash
# En Windows (CMD o PowerShell):
python -m venv venv
venv\Scripts\activate
pip install paho-mqtt
# En macOS y Linux:
python3 -m venv venv
source venv/bin/activate
pip install paho-mqtt
(Sabrás que funcionó porque verás el prefijo (venv) al inicio de tu terminal).
Paso 3: Analizando el Código (cliente_iot.py)
Si abres la carpeta que acabamos de descargar, encontrarás el archivo principal: cliente_iot.py. Este script es el encargado de conectarse al broker y escuchar lo que dice nuestro ESP32.
Analicemos cómo funciona internamente:
Python
"""
cliente_iot.py
================
Cliente de escritorio (línea de comandos) para controlar un ESP32 por MQTT.
Este script NO se conecta directamente al ESP32. Ambos (este script y el
ESP32) se conectan a un servidor intermediario llamado "broker" MQTT
(en este caso `crystalmq.bevywise.com`), y se comunican publicando y
suscribiéndose a "tópicos" (canales con nombre). Es el mismo patrón que
usan por ejemplo las apps de chat: nadie te escribe directo a tu teléfono,
todo pasa por un servidor central.
Patrón usado: publish/subscribe (pub/sub)
- "Publicar" (publish) = enviar un mensaje a un tópico.
- "Suscribirse" (subscribe) = pedirle al broker que nos avise cada vez
que llegue un mensaje nuevo a un tópico.
Este cliente:
- Se SUSCRIBE a los tópicos de temperatura, humedad y estado del LED
(para poder mostrarlos en el menú).
- PUBLICA en el tópico de comando del LED (para encenderlo/apagarlo).
Ver esp32_mqtt_led_dth22.c para el firmware que corre del otro lado.
CONFIGURACIÓN REQUERIDA ANTES DE EJECUTAR:
Este script NO contiene ningún usuario/contraseña en el código. Las
credenciales se leen desde variables de entorno (usando un archivo ".env"
local, que nunca se sube al repositorio). Pasos:
1) Copiá ".env.example" y renombrá la copia a ".env".
2) Completá tus propios datos (ver instrucciones dentro del archivo y
en el README.md, sección "Configuración de credenciales").
3) Instalá las dependencias: pip install -r requirements.txt
"""
import os
import paho.mqtt.client as mqtt
import json
from datetime import datetime
import time # Para pausas breves y medir tiempos de espera
import threading # Para esperar la conexión sin "bloquear a ciegas" con sleep
from dotenv import load_dotenv # Carga variables desde un archivo .env local
# 1. CONFIGURACIÓN DEL BROKER Y TEMAS
# -------------------------------------------------------------------------
# Estos valores DEBEN coincidir exactamente con los del firmware del ESP32
# (ver esp32_mqtt_led_dth22.c). Si cambiás un nombre de tópico acá, tenés
# que cambiarlo también allá, o dejarán de "escucharse".
load_dotenv() # Lee el archivo .env (si existe) y carga sus valores como variables de entorno
BROKER = os.environ.get("MQTT_BROKER", "crystalmq.bevywise.com")
PORT = int(os.environ.get("MQTT_PORT", "1883")) # Puerto MQTT sin cifrar (TLS sería el 8883). Ver README, sección de seguridad.
TOPIC_TEMP = "topic_sensor_temperature"
TOPIC_HUM = "topic_sensor_humidity"
TOPIC_LED = "esp32/led" # Acá publicamos comandos ON/OFF
TOPIC_LED_STATUS = "esp32/led/status" # Acá el ESP32 confirma el estado real
# 1.5 CREDENCIALES DE AUTENTICACIÓN
# NOTA PEDAGÓGICA: en un proyecto real las contraseñas NUNCA deberían estar
# escritas directamente en el código ("hardcodeadas"), porque cualquiera
# que vea el código (o el historial de git) las vería también. Por eso acá
# se leen desde variables de entorno, típicamente cargadas desde un
# archivo ".env" que está en el .gitignore.
USERNAME = os.environ.get("MQTT_USERNAME")
PASSWORD = os.environ.get("MQTT_PASSWORD")
if not USERNAME or not PASSWORD:
print("[✗] Faltan credenciales del broker MQTT.")
print(" 1) Copiá '.env.example' y renombralo a '.env'.")
print(" 2) Completá MQTT_USERNAME y MQTT_PASSWORD con los datos de tu")
print(" cuenta de https://crystalmq.bevywise.com")
print(" Ver README.md, sección 'Configuración de credenciales'.")
raise SystemExit(1)
# --- VARIABLE GLOBAL PARA EL ESTADO ---
# Aquí guardaremos el último dato que llegue de forma silenciosa, en
# segundo plano, cada vez que el broker nos entregue un mensaje nuevo.
ultima_lectura = {
"temperatura": "N/A",
"humedad": "N/A",
"estado_led": "N/A",
"timestamp": "Ninguna lectura aún"
}
# threading.Event funciona como un semáforo binario: empieza "bajo" (no
# activado) y se pone "alto" (activado) cuando llamamos a .set(). Otro hilo
# puede quedarse esperando con .wait() hasta que eso pase, en vez de dormir
# un tiempo fijo "a ciegas" con time.sleep() y esperar que haya alcanzado.
conectado = threading.Event()
# 2. FUNCIONES DE RESPUESTA (Callbacks)
# -------------------------------------------------------------------------
# Un "callback" es una función que vos escribís pero que NO llamás vos
# directamente: se la entregás a la librería (paho-mqtt) y es ELLA quien la
# ejecuta automáticamente cuando ocurre cierto evento (conectar, desconectar,
# recibir un mensaje). Por eso estas funciones corren en un hilo aparte,
# manejado internamente por la librería (ver cliente.loop_start() más abajo).
def on_connect(client, userdata, flags, rc, properties=None):
"""Se ejecuta automáticamente cuando el cliente logra (o falla en)
conectarse al broker. `rc` ("return code") indica el resultado."""
if rc == 0:
print("\n[✓] Conectado exitosamente al broker MQTT")
# Al conectar, nos suscribimos a los canales de temperatura, humedad y estado del LED.
# A partir de acá, on_message() se disparará solo cuando llegue algo nuevo.
client.subscribe(TOPIC_TEMP)
client.subscribe(TOPIC_HUM)
client.subscribe(TOPIC_LED_STATUS)
conectado.set() # Avisamos al hilo principal que ya podemos operar
else:
error_codes = {1: "Versión de protocolo", 2: "ID rechazado", 3: "Servidor no disponible", 4: "Usuario/Clave incorrecta", 5: "No autorizado"}
print(f"\n[✗] Error de conexión: {error_codes.get(rc, rc)}")
def on_disconnect(client, userdata, rc, properties=None):
"""Se ejecuta si la conexión con el broker se corta (wifi caído, broker
reiniciado, etc.). paho-mqtt reintentará reconectar solo (ver
reconnect_delay_set más abajo)."""
conectado.clear()
def on_message(client, userdata, msg):
"""Se ejecuta SILENCIOSAMENTE en segundo plano cada vez que llega un
mensaje a cualquiera de los tópicos a los que estamos suscritos.
`msg.topic` nos dice de cuál tópico vino, y `msg.payload` trae el
contenido en bytes (por eso hay que decodificarlo a texto)."""
global ultima_lectura
try:
# Decodificar el mensaje (El ESP32 envía texto plano, no JSON)
payload = msg.payload.decode('utf-8')
# Actualizamos el timestamp
ultima_lectura["timestamp"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Verificamos de qué tópico vino el mensaje para saber qué campo actualizar
if msg.topic == TOPIC_TEMP:
ultima_lectura["temperatura"] = payload
elif msg.topic == TOPIC_HUM:
ultima_lectura["humedad"] = payload
elif msg.topic == TOPIC_LED_STATUS:
ultima_lectura["estado_led"] = payload
except Exception as e:
pass # Ignoramos errores de lectura en segundo plano para no ensuciar la consola
# 3. INICIAR EL CLIENTE Y CONECTAR
# -------------------------------------------------------------------------
# Compatibilidad con paho-mqtt 1.x y 2.x: a partir de la versión 2, la
# librería exige indicar explícitamente qué "versión de API de callbacks"
# se va a usar. VERSION1 mantiene la firma clásica de on_connect/on_message
# que usamos en este script. Si tenés instalada la versión 1.x (más vieja),
# mqtt.CallbackAPIVersion directamente no existe, así que probamos y si
# falla creamos el cliente "a la vieja usanza".
try:
cliente = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1)
except AttributeError:
cliente = mqtt.Client()
cliente.username_pw_set(username=USERNAME, password=PASSWORD)
# Le decimos a la librería qué función llamar para cada evento.
# Ojo: acá NO se están "ejecutando" las funciones, solo se están registrando
# para que paho-mqtt las llame por nosotros más adelante.
cliente.on_connect = on_connect
cliente.on_disconnect = on_disconnect
cliente.on_message = on_message
cliente.reconnect_delay_set(min_delay=1, max_delay=30) # reintentos automáticos si se corta la conexión
try:
print(f"[→] Conectando a {BROKER}...")
cliente.connect(BROKER, PORT, keepalive=60)
# CAMBIO CLAVE: Iniciamos el loop en un HILO EN SEGUNDO PLANO.
# loop_start() crea un hilo aparte que se encarga de mantener la
# conexión viva y de llamar a nuestros callbacks (on_connect,
# on_message, etc.) cada vez que corresponda, sin que nosotros
# tengamos que estar pendientes. Así el hilo principal queda libre
# para mostrar el menú y leer lo que el usuario tipea.
cliente.loop_start()
# En vez de "dormir 1.5 segundos y confiar en que ya estamos
# conectados", esperamos de verdad a que on_connect() nos avise
# (con conectado.set()), con un máximo de 5 segundos de espera.
if not conectado.wait(timeout=5):
print("[✗] No se pudo confirmar la conexión al broker en 5s. Continuando de todas formas...")
# 4. MENÚ INTERACTIVO
while True:
print("\n" + "="*35)
print(" PANEL DE CONTROL ESP32")
print("="*35)
# Mostramos el estado del LED directamente en la cabecera del menú
print(f" ESTADO DEL LED : {ultima_lectura['estado_led']}")
print("-" * 35)
print(" 1. Encender el LED")
print(" 2. Apagar el LED")
print(" 3. Leer Temperatura y Humedad")
print(" 4. Salir")
print("="*35)
opcion = input("Elige una opción (1-4): ")
if opcion in ('1', '2'):
comando = "ON" if opcion == '1' else "OFF"
accion = "ENCENDIDO" if opcion == '1' else "APAGADO"
if not cliente.is_connected():
print("\n[✗] No hay conexión con el broker, no se puede enviar el comando.")
time.sleep(1.5)
continue
print(f"\n[!] Enviando comando de {accion}...")
# publish() manda el mensaje al broker, que lo reenvía a todo
# cliente suscrito a TOPIC_LED (en este proyecto, el ESP32).
cliente.publish(TOPIC_LED, comando)
# El ESP32 recibe el comando, cambia el LED físico y publica la
# confirmación real en TOPIC_LED_STATUS. Ese mensaje nos llega
# de forma asíncrona a on_message(), así que esperamos un ratito
# a que ultima_lectura se actualice en vez de asumir que ya
# cambió apenas mandamos el comando.
for _ in range(20): # 20 * 0.1s = hasta ~2 segundos de espera
if ultima_lectura["estado_led"] == comando:
break
time.sleep(0.1)
if ultima_lectura["estado_led"] == comando:
print(f"[✓] LED confirmado en estado: {comando}")
else:
print(f"[!] Comando enviado, esperando confirmación del dispositivo (estado actual: {ultima_lectura['estado_led']})")
elif opcion == '3':
print("\n[✓] --- ÚLTIMOS DATOS DEL DISPOSITIVO ---")
print(f" Hora de lectura : {ultima_lectura['timestamp']}")
# Quitamos el '°C' manual porque el código del ESP32 ya se lo envía así: "25.00C"
print(f" Temperatura : {ultima_lectura['temperatura']}")
print(f" Humedad : {ultima_lectura['humedad']} %")
print(f" Estado LED : {ultima_lectura['estado_led']}")
print("-----------------------------------------")
input("Presiona ENTER para volver al menú...")
elif opcion == '4':
print("\n[!] Cerrando el programa...")
break # Rompe el bucle while infinito
else:
print("\n[✗] Opción no válida. Intenta nuevamente.")
time.sleep(1)
except KeyboardInterrupt:
print("\n[!] Interrumpido por el usuario.")
except Exception as e:
print(f"\n[✗] Ocurrió un error inesperado: {e}")
finally:
# Aseguramos un cierre limpio al terminar o si hay error:
# loop_stop() detiene el hilo en segundo plano y disconnect() avisa
# correctamente al broker que nos vamos (en vez de simplemente cortar
# la conexión de golpe).
cliente.loop_stop()
cliente.disconnect()
print("[✓] Cliente MQTT desconectado de forma segura.")
🔒 Nota de Seguridad IoT: En proyectos de la vida real, los datos nunca se envían en texto plano ni a brokers públicos sin protección. Esto lo abordaremos más adelante en la serie.
Paso 4: Ejecución
Con tu entorno virtual activado y dentro de la carpeta del proyecto, ejecuta el script:
Bash
# En Windows:
python cliente_iot.py
# En macOS y Linux:
python3 cliente_iot.py
Si todo está correcto, empezarás a ver en tu terminal los cambios de temperatura que hagas en el simulador de Wokwi en tiempo real. ¡Has conectado la nube con tu entorno local!
Conclusión y Próximos Pasos
El ESP32 abre un mundo de posibilidades para la transformación digital en las aulas. Hoy has dado el primer paso: entender la arquitectura y lograr que el hardware (simulado) hable con tu computadora usando tecnologías reales de la industria.
🚀 Reto del día: Modifica el código de Python para que te muestre una advertencia en la consola si la temperatura supera los 30°C.
En los próximos tutoriales de esta serie cubriremos:
- Parte 2: Configuración de tu propio Servidor MQTT (Mosquitto)
- Parte 3: Programación del ESP32 en C++ (Arduino IDE)
- Parte 4: Construcción de un Dashboard Web interactivo con Node.js
- Parte 5: Casos de uso y proyectos reales en educación
¿Estás listo para dominar el Internet de las Cosas? ¡Sígueme en mis redes sociales para no perderte la próxima entrega!
