ESP32 y Arduino

MicroPython umqtt en ESP32: publicar y suscribirse con umqtt.simple y umqtt.robust

Conecta un ESP32 con MicroPython a un broker MQTT usando umqtt.simple, añade reconexión automática con umqtt.robust, recibe comandos sin bloquear y conoce lo que estos clientes mínimos no pueden hacer.

Placa ESP32 ejecutando un script de MicroPython umqtt en ESP32 que publica lecturas de sensores
Pruébalo en tu propio broker MQTTConsigue un broker MQTT privado y gestionado con el plan Free y empieza con ejemplos de código listos para usar.
Pruébalo gratis Ejemplos de código Sin tarjeta

MicroPython umqtt en ESP32 te da MQTT en unas pocas líneas de Python, sin paso de compilación y con un prompt interactivo para depurar. Este tutorial conecta un ESP32 a broker.connectmq.com, el servidor MQTT público gratuito de ConnectMQ, con umqtt.simple, añade reconexión automática con umqtt.robust, gestiona comandos entrantes sin bloquear y explica los límites que debes conocer antes de confiar en estos clientes en producción.

Por qué MicroPython para dispositivos MQTT

C++ con el core de Arduino es más rápido y tiene más librerías, pero MicroPython gana en velocidad de iteración. Editas un archivo, lo copias a la placa y lo ejecutas en segundos. El REPL te permite probar Wi-Fi, sensores y MQTT de forma interactiva, algo muy valioso cuando intentas averiguar por qué algo no funciona.

El paquete umqtt de micropython-lib ofrece dos módulos:

  • umqtt.simple: un cliente MQTT 3.1.1 mínimo. Conectar, publicar, suscribirse, ping, last will. Nada ocurre de forma automática.
  • umqtt.robust: una subclase ligera de simple que captura errores de socket en publish(), wait_msg() y algunas otras llamadas y se reconecta automáticamente.

Ambos son lo bastante pequeños como para caber con holgura en cualquier ESP32.

Preparar la placa

  1. Graba un firmware de MicroPython actual para tu ESP32 desde la página oficial de descargas, usando esptool.py.
  2. Instala mpremote en tu ordenador: pip install mpremote.
  3. Comprueba si umqtt ya está presente. Algunas compilaciones del firmware lo incluyen y otras no. Si import umqtt.simple falla en el REPL, instálalo con mip:
mpremote connect auto mip install umqtt.simple
mpremote connect auto mip install umqtt.robust

mip necesita que la placa esté conectada a internet. Si todavía no está conectada al Wi-Fi, ejecuta la instalación desde un script que se conecte primero, o copia los archivos del módulo manualmente con mpremote cp. La documentación de gestión de paquetes de MicroPython describe todas las opciones.

Conectarse al Wi-Fi

Pon esto en boot.py para que se ejecute en cada arranque:

import network
import time

WIFI_SSID = "YOUR_WIFI"
WIFI_PASS = "YOUR_WIFI_PASSWORD"

def connect_wifi(timeout=15):
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    if not wlan.isconnected():
        wlan.connect(WIFI_SSID, WIFI_PASS)
        start = time.time()
        while not wlan.isconnected():
            if time.time() - start > timeout:
                raise RuntimeError("Wi-Fi connection timed out")
            time.sleep(0.25)
    print("Wi-Fi OK", wlan.ifconfig()[0])
    return wlan

connect_wifi()

Publicar con umqtt.simple

El primer script se conecta al área pública de broker.connectmq.com y publica una lectura cada cinco segundos. El client ID se construye a partir del ID único del chip, de modo que nunca está vacío ni coincide con el de otra placa.

import time
import ubinascii
import machine
import esp32
from umqtt.simple import MQTTClient

BROKER = "broker.connectmq.com"
PREFIX = b"connectmq/demo/2b7e95"
CLIENT_ID = b"mpy-" + ubinascii.hexlify(machine.unique_id())

client = MQTTClient(CLIENT_ID, BROKER, port=1883, keepalive=30)
client.set_last_will(PREFIX + b"/status", b"offline", retain=True, qos=1)
client.connect()
client.publish(PREFIX + b"/status", b"online", retain=True, qos=1)
print("Connected as", CLIENT_ID)

while True:
    temp_c = (esp32.raw_temperature() - 32) / 1.8   # lectura aproximada del sensor interno
    payload = '{"temp":%.1f,"uptime":%d}' % (temp_c, time.ticks_ms() // 1000)
    client.publish(PREFIX + b"/telemetry", payload.encode())
    time.sleep(5)

En variantes de ESP32 sin raw_temperature(), sustituye esa línea por la lectura de un sensor real. Observa los mensajes desde tu ordenador:

mosquitto_sub -h broker.connectmq.com -p 1883 -i pc-2b7e95 -t 'connectmq/demo/2b7e95/#' -v

Ten en cuenta que los topics y los payloads son bytes. umqtt acepta str en muchos sitios, pero usar bytes de forma coherente evita sorpresas, sobre todo en los callbacks, que siempre reciben bytes.

Recibir comandos sin bloquear

Para recibir mensajes, registra un callback y suscríbete. Después debes llamar con regularidad a wait_msg(), que bloquea hasta que llega un mensaje, o a check_msg(), que retorna de inmediato si no hay nada pendiente. Usa check_msg() en un bucle que también haga otras tareas:

import time
import ubinascii
import machine
from umqtt.simple import MQTTClient

BROKER = "broker.connectmq.com"
PREFIX = b"connectmq/demo/2b7e95"
CLIENT_ID = b"mpy-" + ubinascii.hexlify(machine.unique_id())
led = machine.Pin(2, machine.Pin.OUT)

def on_message(topic, msg):
    print("Received", topic, msg)
    if topic == PREFIX + b"/led/set":
        led.value(1 if msg == b"ON" else 0)
        client.publish(PREFIX + b"/led/state", msg, retain=True)

client = MQTTClient(CLIENT_ID, BROKER, port=1883, keepalive=30)
client.set_callback(on_message)
client.connect()
client.subscribe(PREFIX + b"/led/set", qos=1)

last_ping = time.ticks_ms()
last_publish = time.ticks_ms()

while True:
    client.check_msg()                       # procesa cualquier mensaje pendiente

    now = time.ticks_ms()
    if time.ticks_diff(now, last_publish) > 10000:
        client.publish(PREFIX + b"/telemetry", b'{"alive":true}')
        last_publish = now
        last_ping = now
    elif time.ticks_diff(now, last_ping) > 20000:
        client.ping()                        # mantiene viva la conexión cuando está inactiva
        last_ping = now

    time.sleep_ms(50)

Pruébalo:

mosquitto_pub -h broker.connectmq.com -p 1883 -i pc-2b7e95-pub -t connectmq/demo/2b7e95/led/set -m ON

Hay dos detalles fáciles de pasar por alto. Primero, umqtt.simple no envía pings de keepalive por sí solo. Si configuras un keepalive y publicas con menos frecuencia, debes llamar a ping(), como hace el bucle anterior, o el broker te desconectará. Segundo, set_callback() debe llamarse antes de subscribe().

Reconexión automática con umqtt.robust

umqtt.simple lanza OSError cuando se cae la conexión, y tu script termina salvo que gestiones la excepción. umqtt.robust envuelve las llamadas principales y se reconecta con una breve espera entre intentos. El import es el único cambio:

from umqtt.robust import MQTTClient

Hay una trampa. El cliente robust reconecta el socket, pero no vuelve a suscribirse por ti, y con una sesión limpia el broker olvida tus suscripciones. Un patrón fiable es encapsular el paso de conexión y llamarlo tanto al arrancar como después de cualquier error que captures tú mismo:

def connect_and_subscribe():
    client.connect(clean_session=True)
    client.subscribe(PREFIX + b"/led/set", qos=1)
    client.publish(PREFIX + b"/status", b"online", retain=True, qos=1)

connect_and_subscribe()

while True:
    try:
        client.check_msg()
        # ... publicación periódica ...
    except OSError as e:
        print("MQTT error", e, "- reconnecting")
        time.sleep(5)
        try:
            connect_and_subscribe()
        except OSError:
            pass
    time.sleep_ms(50)

Para dispositivos realmente desatendidos, añade machine.WDT(timeout=30000) y aliméntalo en el bucle, de modo que un bloqueo total reinicie la placa.

Estructurar un proyecto MicroPython umqtt en ESP32

Cuando un script supera unas pocas decenas de líneas, divídelo como harías con cualquier proyecto de Python. Una estructura que funciona bien en la placa:

  • boot.py conecta el Wi-Fi y nada más, para que un fallo en la aplicación nunca te deje sin acceso al REPL.
  • config.py contiene el host del broker, el puerto, el prefijo de topic y las credenciales. Mantenlo fuera del control de versiones y cópialo a cada placa por separado.
  • main.py importa config, crea el cliente y ejecuta el bucle principal dentro de un try/except que registra la excepción y llama a machine.reset() tras una breve espera.

Dos hábitos ahorran mucho tiempo de depuración. Estructura el bucle principal de forma que pulsar Ctrl+C en el REPL lo detenga limpiamente con KeyboardInterrupt en lugar de que lo capture tu gestor de errores genérico. Y llama a gc.collect() periódicamente si construyes cadenas de payload en el bucle; el heap de MicroPython es pequeño y la fragmentación se manifiesta como MemoryError tras horas de funcionamiento, no minutos.

Publicar JSON es más sencillo con el módulo json integrado: client.publish(topic, json.dumps({"temp": 22.4})). Analizar comandos funciona igual con json.loads(msg) dentro del callback, envuelto en un try para que un mensaje mal formado no pueda bloquear el dispositivo.

Limitaciones de umqtt que debes conocer

Limitación Impacto Solución
Solo MQTT 3.1.1 Sin propiedades, reason codes ni funciones de suscripción compartida de MQTT 5 Usa otro cliente si necesitas MQTT 5
Solo QoS 0 y 1 QoS 2 no está soportado QoS 1 basta para la mayoría de telemetría y comandos
Sin pings de keepalive automáticos Las conexiones inactivas se cierran Llama a ping() periódicamente
Llamadas de socket bloqueantes connect() y wait_msg() bloquean el script Usa check_msg(); considera un cliente basado en asyncio para dispositivos complejos
Sin resuscripción al reconectar Los comandos dejan de llegar tras una reconexión Vuelve a suscribirte en tu propia función de reconexión
Sin cola de mensajes offline Los mensajes publicados durante la desconexión se pierden Almacena tú mismo las lecturas en búfer si lo necesitas

Para dispositivos que necesitan hacer varias cosas a la vez existen clientes MQTT comunitarios basados en asyncio que gestionan los keepalives y la reconexión en segundo plano. Para sensores y actuadores sencillos, umqtt sigue siendo la opción más ligera.

Usar TLS en el puerto 8883

Con un broker privado, cifra la conexión. Las versiones recientes de umqtt.simple aceptan un argumento ssl que recibe un SSLContext; las versiones antiguas usan en su lugar ssl=True con un diccionario ssl_params, así que comprueba la versión que has instalado.

import ssl
from umqtt.simple import MQTTClient

ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.verify_mode = ssl.CERT_REQUIRED
ctx.load_verify_locations(cafile="isrg-root-x1.der")   # CA raíz de Let's Encrypt

client = MQTTClient(
    b"YOUR_CLIENT_ID",
    "broker.connectmq.com",
    port=8883,
    user=b"YOUR_USERNAME",
    password=b"YOUR_PASSWORD",
    keepalive=30,
    ssl=ctx,
)
client.connect()

Copia primero el certificado ISRG Root X1 a la placa con mpremote cp. TLS necesita bastante RAM; las placas con PSRAM lo gestionan con más holgura. Usa exactamente el client ID, el usuario y la contraseña generados para tu broker, porque ConnectMQ vincula el client ID a las credenciales emitidas.

Guías relacionadas

Si prefieres trabajar en C++, la guía de proyectos MQTT con ESP32 y PlatformIO muestra una configuración de Arduino bien estructurada, y la guía de Last Will MQTT en ESP32 explica el patrón de estado usado en el primer script. El tutorial de IoT con ESP32 cubre el recorrido completo del dispositivo al dashboard y, si el protocolo es nuevo para ti, empieza por qué hace un broker MQTT.

Conecta tus dispositivos MicroPython a ConnectMQ

Todos los ejemplos funcionan con el servidor MQTT público gratuito sin necesidad de registro, pensado para pruebas y prototipos, no para datos de producción. Cuando vayas más allá de los experimentos, ConnectMQ te ofrece un broker MQTT gestionado, un broker MQTT en la nube privado con TLS en el puerto 8883 y un cliente MQTT en el navegador para comprobar lo que envía tu placa. Crea uno en el plan gratuito Serverless en app.connectmq.com, sin tarjeta de crédito.

Preguntas frecuentes

¿umqtt es compatible con MQTT 5?

No. Tanto umqtt.simple como umqtt.robust implementan MQTT 3.1.1. Los brokers compatibles con MQTT 5, como ConnectMQ, siguen aceptando conexiones 3.1.1.

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

Has configurado un keepalive pero no envías nada dentro de ese intervalo. umqtt no hace ping automáticamente, así que llama a client.ping() con regularidad o publica con más frecuencia que el keepalive.

¿Cuál es la diferencia entre wait_msg() y check_msg()?

wait_msg() bloquea hasta que llega un mensaje. check_msg() procesa un mensaje pendiente si lo hay y retorna de inmediato, lo que encaja con bucles que también leen sensores.

¿Puede umqtt usar QoS 2?

No. Admite QoS 0 y 1, lo que cubre la mayoría de casos de uso en dispositivos.

Artículos relacionados

Ejemplo de ESP-IDF MQTT: crea un cliente esp-mqtt desde cero

Construye un proyecto ESP-IDF desde una carpeta vacía con el componente esp-mqtt: configuración de Wi-Fi en modo estación, el manejador de eventos MQTT, publicación y suscripción, propiedades de MQTT 5 y TLS con el bundle de certificados.

9 min de lectura

Todos los artículos →

Tu broker MQTT privado, a unos pocos clics

Crea una cuenta, obtén las credenciales de tu broker y conecta tu primer dispositivo, sin servidores que instalar, parchear ni escalar.