IoT
Wi-Fi, HTTP y MQTT en la ESP32 con MicroPython
Wi-Fi con network.WLAN, HTTP con requests y MQTT con umqtt.simple en la ESP32: estados de la conexión, errores de la placa y cómo probarlo en el simulador.
Por Prof. Rodrigo Faustino · Publicado el · actualizado el · 13 min de lectura
Métricas de este artículo
- Palabras
- 2430
- Caracteres
- 14.418
- Tiempo de lectura
- ~13 min
Hasta aquí, la ESP32 conversó con el mundo por los pines: LEDs, botones, sensores y pantallas. Lo que la diferencia de muchas placas de microcontrolador, sin embargo, es la radio Wi-Fi integrada en el chip. Con ella, la placa busca el pronóstico del tiempo en internet, envía lecturas de sensores a un panel, recibe comandos del celular. Es la base de lo que se llama Internet de las Cosas (IoT).
Este artículo recorre las tres capas de ese camino en MicroPython: el Wi-Fi con el módulo network, las solicitudes HTTP con el módulo requests y los mensajes MQTT con umqtt.simple. En cada una, los comportamientos de la placa que sorprenden a los principiantes (la conexión que no es instantánea, los códigos de error, el ADC2 que deja de funcionar) y cómo PyPlaca simula todo eso con seguridad, sin que la placa tenga que salir del navegador.
El Wi-Fi como interfaz de red
En MicroPython, el Wi-Fi es la clase WLAN del módulo network. La ESP32 tiene dos interfaces Wi-Fi:
- STA (station, estación): la placa se conecta a un router, como un celular. Es el modo de casi todos los proyectos;
- AP (access point, punto de acceso): la placa crea su propia red, y otros aparatos se conectan a ella.
network.WLAN(network.STA_IF) devuelve la interfaz de estación, y siempre el mismo objeto, por más veces que se llame. Antes de cualquier cosa, la interfaz tiene que activarse con active(True), que enciende la radio. Solo entonces funciona connect(); llamado antes, da un OSError con el motivo en texto (como Wifi Not Started).
import network
import time
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect('laboratorio', 'esp32lab')
inicio = time.ticks_ms()
while not wlan.isconnected():
if time.ticks_diff(time.ticks_ms(), inicio) > 10000:
print('no conectó; status =', wlan.status())
break
time.sleep_ms(100)
else:
print('¡conectado! IP:', wlan.ifconfig()[0])En PyPlaca, la placa simulada "ve" una red llamada laboratorio, con la contraseña esp32lab. El botón Wi-Fi del banco de trabajo muestra el estado de la conexión y deja que el profesor cambie el nombre y la contraseña de esa red. Abre el ejemplo y ejecútalo: después de un instante, el Serial muestra la IP que el "router" del laboratorio le dio a la placa.
La conexión no es instantánea
El punto más importante del ejemplo es el bucle de espera. El connect() vuelve al instante: solo le pide al sistema de la radio que empiece a conectar. La conexión de verdad (encontrar la red, negociar la contraseña, recibir una IP por DHCP) ocurre después, en segundo plano. Un programa que llama a connect() e inmediatamente intenta usar la red falla.
Por eso todo programa de Wi-Fi espera a que isconnected() sea verdadero. Y espera con límite de tiempo: si la contraseña es incorrecta o la red no existe, el bucle sin límite se quedaría preso para siempre. El ejemplo usa ticks_diff, la misma técnica del artículo Programar sin colgarse, y el else del while solo se ejecuta si el bucle termina sin break.
En el simulador, la conexión tarda cerca de 1,5 s de tiempo virtual. Es un valor de referencia elegido para la simulación: el tiempo real depende del router y todavía se va a medir en una placa del laboratorio.
Los estados de la conexión
wlan.status() dice en qué punto está la conexión, con las constantes de MicroPython (los números son los de ESP-IDF, el sistema de Espressif que hay debajo de MicroPython):
| Constante | Valor | Significado |
|---|---|---|
network.STAT_IDLE | 1000 | parado, sin intentar conectar |
network.STAT_CONNECTING | 1001 | conectando |
network.STAT_GOT_IP | 1010 | conectado y con IP |
network.STAT_NO_AP_FOUND | 201 | la red no se encontró |
network.STAT_WRONG_PASSWORD | 202 | contraseña incorrecta |
network.STAT_CONNECT_FAIL | 203 | falló por otro motivo |
wlan.status() en el MicroPython de la ESP32: el connect() vuelve al instante, y la conexión ocurre después; por eso el programa espera el STAT_GOT_IP.Dos detalles del MicroPython real que el simulador reproduce: el motivo de un fallo no se pone a cero solo por llamar de nuevo a connect(); y después de disconnect() el status vuelve a STAT_IDLE. Cambia la contraseña del ejemplo por una incorrecta y ejecuta de nuevo para ver el 202.
Buscar redes
wlan.scan() lista las redes al alcance. Cada elemento es una tupla con el nombre (en bytes), la dirección del router, el canal, la fuerza de la señal (RSSI, en dBm: cuanto más cerca de cero, mejor), el tipo de seguridad y si la red es oculta. La lista viene de la señal más fuerte a la más débil, y el scan() bloquea el programa mientras busca.
import network
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
for ssid, bssid, canal, rssi, seguranca, oculta in wlan.scan():
print(ssid.decode(), 'canal', canal, 'señal', rssi, 'dBm')El ADC2 deja de funcionar
Un efecto colateral del Wi-Fi sorprende a mucha gente: con la radio encendida, los pines del ADC2 (GPIO 0, 2, 4, 12 a 15 y 25 a 27) ya no pueden hacer lecturas analógicas, porque el sistema del Wi-Fi se queda con ese conversor. La lectura da OSError: [Errno 116] ETIMEDOUT, y el simulador muestra el aviso adc2-wifi. La regla, explicada en el artículo sobre el ADC de la ESP32, es simple: los proyectos con Wi-Fi leen los sensores analógicos en el ADC1, los GPIO 32 a 39.
La IA que recuerda tu vida y vive en tu ordenador
- Memoria a largo plazo con la fuente de cada respuesta
- Funciona sin conexión: tus datos se quedan en tu PC
- Lee PDF, diapositivas, fotos y grabaciones de reuniones
Plan Esencial gratis para siempre
HTTP con requests
Con la placa en la red, el camino más común para buscar datos es el HTTP, el mismo protocolo de las páginas web. El firmware de MicroPython para la ESP32 trae el módulo requests (también importable como urequests), de la biblioteca oficial micropython-lib. El simulador ejecuta el mismo archivo original, sin adaptaciones, sobre una pila de red simulada.
import network
import time
import requests
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect('laboratorio', 'esp32lab')
while not wlan.isconnected():
time.sleep_ms(100)
r = requests.get('http://api.laboratorio.test/clima')
print('status:', r.status_code)
dados = r.json()
print('temperatura:', dados['temperatura'], '°C')
print('humedad:', dados['umidade'], '%')
r.close()La respuesta tiene status_code (200 es "salió bien"), text (el cuerpo como texto), content (como bytes) y json(), que transforma el cuerpo en diccionario cuando el servidor envía JSON. El close() del final devuelve la conexión: la ESP32 tiene poca memoria y pocos sockets, y un programa que hace muchas solicitudes sin cerrar acaba quedándose sin ellos.
Para enviar datos, requests.post() acepta un diccionario en el argumento json=:
r = requests.post('http://api.laboratorio.test/leituras', json={'temperatura': 23.4})
print(r.status_code) # 201: creado
r.close()Qué es "internet" en el simulador
El código del alumno no puede salir a buscar cualquier dirección de internet desde el navegador: sería un riesgo para el alumno y para la plataforma. Por eso la red del simulador funciona así:
- respuestas listas (mocks): direcciones como
api.laboratorio.testresponden con datos fijos, sin salir del navegador. Cada solicitud aparece en una línea corta en el Serial, con el método, la dirección y el status; - lista permitida: solo unos pocos dominios aprobados por la plataforma se consultan de verdad, y solo cuando la actividad lo permite;
- todo lo demás: el nombre no se encuentra, como en una red sin internet. El
getaddrinfode la placa devuelveOSError: -202y el simulador muestra el aviso rede-fora-da-lista.
El mismo -202 aparece si el programa intenta usar un nombre antes de que el Wi-Fi conecte: es la placa real diciendo "no pude resolver ese nombre". Si el servidor tarda más que el tiempo límite del socket, el error es OSError: [Errno 116] ETIMEDOUT.
HTTP y programas que no se cuelgan
Un requests.get() bloquea el programa hasta que llega la respuesta. En un proyecto que además hace parpadear LEDs y lee botones, eso significa que todo se detiene durante la solicitud. Dos prácticas ayudan: hacer solicitudes pocas veces (cada minuto, no en cada vuelta del bucle), controladas por ticks_diff, y preferir MQTT cuando la placa necesita intercambiar mensajes con frecuencia.
MQTT: mensajes por topics
El HTTP funciona con el modelo "pregunta y respuesta": la placa pregunta, el servidor responde. Para IoT, eso tiene un problema: el servidor no puede avisarle nada a la placa; ella tendría que preguntar todo el tiempo. MQTT lo resuelve con otro modelo, el de publicar y suscribirse:
- existe un servidor central, el broker;
- cada dispositivo se conecta al broker y mantiene la conexión abierta;
- quien quiere enviar algo publica un mensaje en un topic, un nombre como
lab/sala1/temperatura; - quien quiere recibir se suscribe al topic, y el broker entrega el mensaje a todos los suscriptores.
Nadie habla directamente con nadie. La placa que publica la temperatura no sabe si quien lee es un celular, un panel o diez dispositivos más, y no necesita saberlo.
Topics y comodines
Los topics son jerárquicos, separados por /. Al suscribirse, se pueden usar comodines:
+sustituye un nivel:lab/+/temperaturarecibelab/sala1/temperaturaylab/sala2/temperatura;#sustituye todos los niveles siguientes:lab/#recibe todo lo que empieza porlab/.
Al publicar, no se permiten comodines. Un mensaje puede ser retenido (retain=True): el broker guarda el último mensaje del topic y se lo entrega a quien se suscriba después; es útil para "el estado actual del LED", que un panel recién abierto necesita saber.
umqtt.simple en la práctica
El firmware de la ESP32 trae el cliente umqtt.simple, también de micropython-lib, y el simulador usa el original. El ejemplo de abajo conecta al Wi-Fi, se suscribe al topic lab/led para recibir comandos y publica un contador en lab/contador cada 2 segundos, sin colgarse, con check_msg() y ticks_diff:
import network
import time
from machine import Pin
from umqtt.simple import MQTTClient
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect('laboratorio', 'esp32lab')
while not wlan.isconnected():
time.sleep_ms(100)
led = Pin(2, Pin.OUT)
def chegou(topico, msg):
print('llegó en', topico, ':', msg)
if msg == b'liga':
led.on()
elif msg == b'desliga':
led.off()
cliente = MQTTClient('esp32-aluno', 'broker.laboratorio.test')
cliente.set_callback(chegou)
cliente.connect()
cliente.subscribe(b'lab/led')
ultimo = time.ticks_ms()
n = 0
while n < 15:
cliente.check_msg() # mira si llegó un mensaje, sin esperar
if time.ticks_diff(time.ticks_ms(), ultimo) >= 2000:
ultimo = time.ticks_add(ultimo, 2000)
n += 1
cliente.publish(b'lab/contador', str(n).encode())
time.sleep_ms(50)
cliente.disconnect()Los pasos son siempre los mismos: crear el MQTTClient con un identificador único y la dirección del broker, set_callback() con la función que recibe los mensajes, connect(), subscribe() y, en el bucle, check_msg(). El callback recibe el topic y el mensaje como bytes; por eso la comparación es con b'liga', y no con 'liga'.
check_msg() es la versión que no bloquea: si no llegó nada, vuelve al instante. Existe también wait_msg(), que se queda parado hasta que llega un mensaje: práctico en un programa que solo espera comandos, desastroso en uno que además tiene que hacer otras cosas.
El broker del simulador
En PyPlaca, el broker corre en el propio navegador y habla MQTT 3.1.1 de verdad, byte a byte: conexión, publicación con QoS 0, 1 y 2, suscripciones con comodines, mensajes retenidos, sesión persistente y "última voluntad". La dirección broker.laboratorio.test apunta a él, y los brokers públicos que usan los tutoriales (test.mosquitto.org, broker.hivemq.com, broker.emqx.io y mqtt.eclipseprojects.io) también: no se contacta ningún broker de internet. Así, un programa copiado de un tutorial corre igual, y ningún mensaje de alumno acaba en un servidor público.
La pestaña MQTT del banco de trabajo muestra los dispositivos conectados, a qué se suscribió cada uno, los topics con el último mensaje y los mensajes recientes. También tiene el formulario Publicar como otro dispositivo: ejecuta el ejemplo, publica liga en el topic lab/led y verás encenderse el LED azul de la placa.

QoS, Keep Alive y conexiones que se caen
Tres conceptos de MQTT que aparecen enseguida en los primeros proyectos:
- QoS (calidad de servicio): 0 es "manda y olvida"; 1 garantiza que el mensaje llega al menos una vez (el broker confirma); 2 garantiza exactamente una vez, con más idas y venidas. Para lecturas de sensor, QoS 0 basta;
- Keep Alive: el cliente promete enviar algún paquete cada tantos segundos. Si pasa una vez y media ese tiempo sin noticias, el broker considera muerta la conexión y la corta;
- última voluntad (last will): un mensaje que el cliente deja acordado al conectar y que el broker publica si él se cae sin despedirse; es la manera clásica de que un panel muestre "placa fuera de línea".
Cuando la conexión se cae, las lecturas del socket empiezan a fallar. umqtt.simple no intenta reconectar; para eso existe umqtt.robust, también en el firmware, que reconecta solo.
Juntar sensores, red y tiempo
Un proyecto de IoT completo combina todo lo que mostró la serie: leer un sensor en el ADC1 o con el DHT22, organizar las tareas con ticks_diff para que nada se cuelgue, publicar las lecturas por MQTT cada cierto tiempo y reaccionar a los comandos que llegan por check_msg(). Un esqueleto típico:
- conectar al Wi-Fi con límite de tiempo y un mensaje claro si falla;
- conectar al broker y suscribirse a los topics de comando;
- en el bucle:
check_msg(), leer los sensores a su ritmo, publicar a su ritmo, pausa corta; - tratar el
OSErrorde la red (se cayó el Wi-Fi, el broker está fuera de línea) sin derribar el resto del programa.
Antes de llevarlo a la placa real, cambia el broker del laboratorio por la dirección del broker de tu escuela o de tu proyecto; el resto del código sigue igual. Y recuerda que, en la placa de verdad, la red es de verdad: las contraseñas no van a repositorios públicos, y los topics con nombres únicos evitan que tu placa reciba los mensajes de la placa del compañero.
Del programa al semestre planificado
- Plan de enseñanza y calendario montados solos
- Diario de clases listo para pegar en el SIGA
- Diapositivas y listas de ejercicios con solucionario
Versión Community gratis · Windows
Diagnosticar problemas de red
Cuando un programa de IoT no funciona, conviene seguir las capas de abajo hacia arriba, como en el banco de trabajo:
- ¿conectó el Wi-Fi? Imprime
wlan.status(): 201 es red no encontrada, 202 es contraseña incorrecta. En el simulador, el botón Wi-Fi del banco de trabajo muestra el estado y la IP; - ¿se resuelve el nombre del servidor? Un
OSError: -202antes de cualquier respuesta indica que no se encontró el nombre: Wi-Fi todavía desconectado, nombre mal escrito o, en el simulador, una dirección fuera de la lista de la actividad; - ¿respondió el servidor? Mira el
status_codedel HTTP antes de llamar ajson(). Un 404 tiene cuerpo, pero no el que esperabas; - en MQTT, ¿aceptó la conexión? El
connect()lanza un error si el broker la rechaza (usuario y contraseña incorrectos, por ejemplo); - ¿llegan los mensajes? Comprueba que el topic suscrito sea exactamente el publicado (mayúsculas, barras y espacios cuentan) y que el bucle llame a
check_msg()con frecuencia; - el callback recibe bytes: comparar
msg == 'liga'(texto) conb'liga'(bytes) es falso, y el comando parece ignorado.
En el simulador, cada solicitud HTTP aparece en el Serial y en la ventana de la red simulada, y la pestaña MQTT muestra quién está conectado y qué se publicó. Es el equivalente a tener un analizador de red junto al banco de trabajo.
Para continuar
Este artículo cierra la serie básica de ESP32, y las tres últimas rutas de la plataforma son su continuación directa. La ruta Wi-Fi y HTTP rehace la primera mitad de este texto con lecciones y desafíos corregidos automáticamente: conectar con límite de tiempo, entender el status(), buscar e interpretar JSON, enviar lecturas con POST y no quedarse esperando para siempre a un servidor lento. La ruta MQTT practica publicar y suscribirse, topics y comodines, recibir comandos sin colgarse con check_msg(), Keep Alive y última voluntad. Y el proyecto final lo junta todo en una estación meteorológica: DHT22, OLED, configuración por HTTP y lecturas en MQTT.
Después vienen cinco artículos de profundización: ESP-NOW, para que las placas conversen sin router; servidor web en la ESP32, para que la placa sirva su propia página; guardar datos en la ESP32; baterías y deep sleep; e IA con ESP32, que incluye la integración de la placa con un modelo de lenguaje.
Para elegir los pines correctos cuando el proyecto salga del simulador y vaya al banco de trabajo, consulta la ficha de la ESP32 DevKit V1 y el mapa de pines, y recuerda el ADC1 para los sensores analógicos.
Para practicar en PyPlaca
Rutas
- Ruta 8: Wi-Fi y HTTP
- Ruta 9: MQTT: publicar y suscribirse
- Ruta 10: Proyecto final: estación meteorológica IoT
Lecciones
- Wi-Fi: la placa entra en la red
- HTTP: la placa hace una petición, el servidor responde
- JSON: los datos que el programa entiende
- MQTT: publicar y suscribirse
- check_msg(): recibir sin trabarse
- El ADC: de tensión a número