Tutoriales

MQTT con MicroPython en ESP32 y Pico W (umqtt)

Conecta un ESP32 o Pico W a un broker MQTT con MicroPython: umqtt.simple frente a umqtt.robust, Wi-Fi, pings de keep-alive, reconexión y advertencias de TLS.

Actualizado · 9 min de lectura

MQTT con MicroPython en ESP32 y Pico W (umqtt)

MicroPython incluye un cliente MQTT diminuto llamado umqtt que funciona en un ESP32, un ESP8266 o una Raspberry Pi Pico W con unos pocos kilobytes de RAM. Viene en dos variantes: umqtt.simple, un cliente síncrono mínimo, y umqtt.robust, que añade reconexión automática encima. Este tutorial instala ambos, se conecta por Wi-Fi, publica y se suscribe, mantiene viva la conexión, se recupera de las caídas y explica qué vigilar con TLS.

El código está pensado para MicroPython 1.22 o superior y el umqtt.simple actual de micropython-lib. Funciona sin cambios en placas ESP32 y en la Pico W; solo cambian los números de pin si lees sensores.

umqtt de un vistazo

umqtt.simpleumqtt.robust
Versión MQTT3.1.13.1.1
QoS0 y 1 (publish y subscribe)0 y 1
Reconexión automáticaNoSí, ante errores de socket
Reconexión Wi-FiNoNo
Resuscripción tras reconectarNoNo (depende de una sesión persistente)
Pings de keep-aliveping() manualping() manual
TLSSí, mediante un contexto sslIgual que simple

Ambos son síncronos: wait_msg() bloquea hasta que llega un mensaje y check_msg() vuelve de inmediato. No hay hilo en segundo plano, así que tu bucle principal se encarga de todo.

1. Instala umqtt

Algunas builds del firmware ya incluyen umqtt (el port de ESP8266 suele hacerlo). En ESP32 y Pico W, instálalo desde micropython-lib con mpremote desde tu ordenador:

Instalar con mpremote
pip install mpremote
mpremote mip install umqtt.simple
mpremote mip install umqtt.robust

O, cuando la placa ya tenga Wi-Fi, desde el REPL:

REPL
import mip
mip.install("umqtt.simple")
mip.install("umqtt.robust")

Compruébalo con from umqtt.simple import MQTTClient: si no aparece ImportError, ya puedes empezar.

2. Conéctate al Wi-Fi

El módulo network es el mismo en ESP32 y Pico W. Ponlo en wifi.py para reutilizarlo desde otros archivos:

wifi.py
import network, time

def connect(ssid, password, timeout=20):
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    if not wlan.isconnected():
        wlan.connect(ssid, password)
        start = time.time()
        while not wlan.isconnected():
            if time.time() - start > timeout:
                raise OSError("Wi-Fi timeout, status=%s" % wlan.status())
            time.sleep(0.5)
    print("Wi-Fi OK", wlan.ifconfig()[0])
    return wlan

Ambos chips solo admiten redes de 2,4 GHz. Si el estado se queda en "conectando" para siempre, revisa la banda y la contraseña antes de sospechar de MQTT.

3. Publicar y suscribirse con umqtt.simple

main.py
import time, machine, ubinascii
from umqtt.simple import MQTTClient
import wifi

wifi.connect("your-ssid", "your-wifi-password")

CLIENT_ID = b"upy-" + ubinascii.hexlify(machine.unique_id())
BASE = b"testmqtt/upy/" + CLIENT_ID
KEEPALIVE = 60

def on_message(topic, msg):
    print("recibido", topic, msg)
    if msg == b"led/on":
        machine.Pin("LED", machine.Pin.OUT).on()   # Pico W; en ESP32 usa tu pin

client = MQTTClient(CLIENT_ID, "broker.connectmq.com", port=1883,
                    user="username", password="password",
                    keepalive=KEEPALIVE)
client.set_last_will(BASE + b"/status", b"offline", retain=True, qos=1)
client.set_callback(on_message)          # debe definirse antes de subscribe()
client.connect()
client.publish(BASE + b"/status", b"online", retain=True, qos=1)
client.subscribe(BASE + b"/cmd", qos=1)
print("conectado como", CLIENT_ID)

last_pub = last_ping = time.ticks_ms()
while True:
    client.check_msg()                   # no bloquea: ejecuta on_message si hay datos
    now = time.ticks_ms()
    if time.ticks_diff(now, last_pub) > 10_000:
        client.publish(BASE + b"/uptime", str(time.ticks_ms() // 1000))
        last_pub = now
    if time.ticks_diff(now, last_ping) > KEEPALIVE * 1000 // 2:
        client.ping()
        last_ping = now
    time.sleep_ms(50)

Puntos clave:

  • Topics y payloads son bytes. Las cadenas suelen funcionar, pero con bytes evitas sorpresas, y on_message siempre recibe bytes.
  • set_callback() antes de subscribe(): si no, umqtt lanza un error de aserción.
  • check_msg() frente a wait_msg(): wait_msg() bloquea hasta que llega algo, lo cual vale para un receptor de comandos puro, pero detiene tu bucle de sensores. Usa check_msg() si además publicas.
  • El keep-alive es cosa tuya. Con keepalive=60, el broker expulsa al cliente tras unos 90 segundos de silencio. El bucle de arriba hace ping cada 30 segundos. Consulta MQTT keep alive para las reglas exactas de tiempos.
  • Client ID único a partir de machine.unique_id(), para que dos placas con el mismo archivo no se expulsen mutuamente.

Pruébalo

Abre el cliente MQTT online, conéctate a broker.connectmq.com y suscríbete a testmqtt/upy/#. Deberías ver el mensaje status retenido y un valor uptime cada 10 segundos. Publica led/on en testmqtt/upy/<tu-client-id>/cmd y el REPL mostrará recibido ....

Ver lo que publica tu placa en el navegador

4. Reconectar como es debido

El Wi-Fi se cae, los routers se reinician, los brokers se reinician. Con umqtt.simple, cada fallo de red aparece como un OSError (normalmente [Errno 104] ECONNRESET, [Errno 113] ECONNABORTED o [Errno 110] ETIMEDOUT) en la llamada que tocó el socket. Envuelve el bucle, reconstruye la conexión y vuelve a suscribirte:

main.py — bucle de reconexión
def mqtt_connect():
    c = MQTTClient(CLIENT_ID, "broker.connectmq.com", user="username",
                   password="password", keepalive=KEEPALIVE)
    c.set_last_will(BASE + b"/status", b"offline", retain=True, qos=1)
    c.set_callback(on_message)
    c.connect()
    c.publish(BASE + b"/status", b"online", retain=True, qos=1)
    c.subscribe(BASE + b"/cmd", qos=1)  # con una nueva clean session las suscripciones desaparecen
    return c

def run_loop(c):
    last_pub = last_ping = time.ticks_ms()
    while True:
        c.check_msg()
        now = time.ticks_ms()
        if time.ticks_diff(now, last_pub) > 10_000:
            c.publish(BASE + b"/uptime", str(time.ticks_ms() // 1000))
            last_pub = now
        if time.ticks_diff(now, last_ping) > KEEPALIVE * 1000 // 2:
            c.ping()
            last_ping = now
        time.sleep_ms(50)

client = None
delay = 1
while True:
    try:
        wifi.connect("your-ssid", "your-wifi-password")
        client = mqtt_connect()
        delay = 1
        run_loop(client)                # solo termina lanzando OSError
    except OSError as e:
        print("conexión perdida:", e, "reintento en", delay, "s")
        try:
            client.disconnect()
        except Exception:
            pass
        time.sleep(delay)
        delay = min(delay * 2, 60)      # backoff exponencial con tope

El backoff exponencial importa cuando tienes muchos dispositivos: si el broker se reinicia, una flota que reintenta cada segundo a la vez provoca una tormenta de reconexiones. El patrón se explica en buenas prácticas de reconexión MQTT.

Para dispositivos que nunca deben quedarse colgados, añade un watchdog de hardware: wdt = machine.WDT(timeout=30000) y llama a wdt.feed() una vez por iteración del bucle. Si el código se bloquea en una llamada de socket, la placa se reinicia sola.

¿Y umqtt.robust?

umqtt.robust.MQTTClient tiene la misma API. Cuando publish(), wait_msg() o check_msg() encuentran un OSError, llama a reconnect(), que reintenta connect(False) indefinidamente con un retardo creciente. Dos limitaciones:

  1. Nunca toca el Wi-Fi. Si el punto de acceso ha desaparecido, reintenta un socket que no puede funcionar.
  2. Se reconecta con clean_session=False y no vuelve a suscribirse. Eso solo restaura tus suscripciones si el broker conservó la sesión: un broker privado normalmente lo hace, pero los endpoints de prueba compartidos como broker.connectmq.com no guardan sesiones persistentes. Prueba el comportamiento de las sesiones persistentes contra tu propia instancia de broker.

umqtt.robust vale para un prototipo rápido. Para cualquier cosa que funcione desatendida, el bucle explícito de arriba es más fácil de razonar.

5. Advertencias sobre TLS

El puerto 1883 envía tu usuario y contraseña en texto plano. Para cualquier cosa más allá de una prueba en la mesa, conéctate al 8883 con TLS. El umqtt.simple actual acepta un ssl.SSLContext:

TLS con verificación de certificado
import ssl, ntptime
ntptime.settime()                        # la validez del certificado necesita un reloj real

ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.verify_mode = ssl.CERT_REQUIRED
with open("ca.der", "rb") as f:
    ctx.load_verify_locations(cadata=f.read())

client = MQTTClient(CLIENT_ID, "broker.connectmq.com", port=8883,
                    user="username", password="password",
                    keepalive=KEEPALIVE, ssl=ctx)
client.connect()

Lo que suele dar problemas:

  • Versiones antiguas de umqtt usaban ssl=True, ssl_params={...} en lugar de un contexto. Si obtienes un TypeError en el constructor, comprueba qué versión hay en la placa.
  • No hay almacén de CA en el dispositivo. MicroPython no incluye certificados raíz. Copia a la placa la CA raíz que firmó el certificado de tu broker (convierte PEM a DER con openssl x509 -in ca.pem -outform der -out ca.der y súbelo con mpremote cp ca.der :).
  • CERT_NONE es solo para depurar. Cifra el tráfico, pero acepta cualquier servidor, incluido el de un atacante.
  • Memoria. Un handshake TLS necesita decenas de kilobytes de heap libre. ESP32 y Pico W se las arreglan; el ESP8266 suele quedarse corto. Llama a gc.collect() antes de connect() y conéctate pronto, antes de que tu aplicación llene el heap.
  • Reloj. Sin ntptime.settime() la placa cree que está en el año 2000 y puede rechazar un certificado válido por no ser válido todavía.

Probar una conexión TLS muestra cómo revisar la cadena de certificados del broker desde tu portátil antes de depurarla en el dispositivo.

Errores habituales

ErrorSignificadoSolución
MQTTException: 5CONNACK "not authorized"Usuario incorrecto o la ACL deniega al cliente
MQTTException: 4Usuario o contraseña incorrectosRevisa las credenciales
MQTTException: 2Identifier rejectedClient ID vacío, demasiado largo o vetado
OSError: [Errno 104] ECONNRESETEl broker cerró el socketKeep-alive perdido, client ID duplicado, TLS en un puerto sin TLS
OSError: -2 o -202Falló la resolución DNS (código según el port)Wi-Fi aún no listo o nombre de host incorrecto
MemoryError al conectar con TLSNo hay heap suficientegc.collect(), conectar antes, buffers más pequeños

Los números tras MQTTException son códigos de retorno CONNACK de MQTT 3.1.1; la herramienta de reason codes explica cada uno. Para un recorrido más a fondo, consulta errores MQTT en MicroPython.

umqtt es lo bastante pequeño como para leerlo de una sentada, y esa es su fuerza: en cuanto añades tus propios pings de keep-alive, un bucle de reconexión con backoff, la resuscripción y TLS con una CA real, es un cliente fiable para sensores que cuidan la batería. Haz el prototipo contra el endpoint compartido y luego lleva el mismo código a un broker privado cambiando el host y las credenciales.

Preguntas frecuentes

¿Uso umqtt.simple o umqtt.robust?

umqtt.robust es una subclase mínima de umqtt.simple que se reconecta sola cuando ocurre un error de socket. No reconecta el Wi-Fi ni vuelve a suscribirse por ti, así que muchos proyectos usan umqtt.simple con su propio bucle de reconexión, que da control total sobre Wi-Fi, suscripciones y backoff.

¿umqtt soporta QoS 2 o MQTT 5?

No. umqtt.simple implementa MQTT 3.1.1 solo con QoS 0 y QoS 1. Si necesitas propiedades de MQTT 5 o QoS 2 en un microcontrolador, busca otro cliente o un gateway que traduzca para el dispositivo.

¿Por qué mi cliente MQTT en MicroPython se desconecta al cabo de un minuto?

umqtt no envía pings de keep-alive por sí mismo. Si defines keepalive y luego no publicas nada durante más de 1,5 veces ese valor, el broker cierra la conexión. Llama a client.ping() periódicamente desde tu bucle principal, o usa keepalive=0 si tu broker lo permite.