Tutoriales

MQTT en CircuitPython con MiniMQTT

Publica y suscríbete desde placas CircuitPython con Adafruit MiniMQTT: secretos en settings.toml, TLS en ESP32-S2/S3 y Pico W, callbacks, loop() y reconexión.

Actualizado · 8 min de lectura

MQTT en CircuitPython con MiniMQTT

Adafruit MiniMQTT (adafruit_minimqtt) es el cliente MQTT de CircuitPython. Funciona en placas con Wi-Fi como ESP32-S2, ESP32-S3 y Raspberry Pi Pico W, usa la pila TLS integrada de la placa y sigue un estilo de callbacks que te resultará familiar si has usado paho-mqtt. Este tutorial lo instala, se conecta de forma segura, publica una lectura cada 10 segundos, reacciona a comandos y sobrevive a las caídas del Wi-Fi.

Los ejemplos están pensados para CircuitPython 9.x con una versión actual de MiniMQTT del bundle de Adafruit. Como CircuitPython recarga code.py cada vez que guardas, puedes iterar rápido; y también verás por qué un dispositivo desplegado necesita un bucle de gestión de errores de verdad.

Lo que necesitas

ElementoNotas
Placa CircuitPython con Wi-FiFeather o QT Py ESP32-S2/S3, Pico W o una placa AirLift
CircuitPython 9.xDescárgalo de circuitpython.org para tu placa exacta
Biblioteca adafruit_minimqttMás sus dependencias, instaladas por circup
Un broker MQTTLos ejemplos usan broker.connectmq.com (8883 TLS)

Instala la biblioteca con circup, que copia los archivos .mpy correctos y sus dependencias a la unidad CIRCUITPY:

Instalar MiniMQTT
pip install circup
circup install adafruit_minimqtt
circup list          # muestra las versiones instaladas y marca las desactualizadas

También puedes copiar a mano la carpeta adafruit_minimqtt del bundle en lib/; circup comete menos errores porque elige el bundle que corresponde a tu versión de CircuitPython.

1. Secretos en settings.toml

CircuitPython lee settings.toml desde la raíz de la unidad. Las dos claves CIRCUITPY_WIFI_* hacen además que la placa se una al Wi-Fi automáticamente al arrancar.

settings.toml
CIRCUITPY_WIFI_SSID = "your-ssid"
CIRCUITPY_WIFI_PASSWORD = "your-wifi-password"
MQTT_BROKER = "broker.connectmq.com"
MQTT_PORT = 8883
MQTT_USER = "username"
MQTT_PASS = "password"

Léelas con os.getenv(). Tener las credenciales aquí permite compartir o versionar code.py sin filtrar nada.

2. Un publicador y suscriptor completo

code.py
import os, time
import microcontroller, ssl, socketpool, wifi
import adafruit_minimqtt.adafruit_minimqtt as MQTT

if not wifi.radio.connected:
    wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"),
                       os.getenv("CIRCUITPY_WIFI_PASSWORD"))
print("Wi-Fi:", wifi.radio.ipv4_address)

DEVICE = "cpy-" + microcontroller.cpu.uid.hex()
BASE = f"testmqtt/cpy/{DEVICE}"

def on_connect(client, userdata, flags, rc):
    print("conectado, rc =", rc)
    client.subscribe(f"{BASE}/cmd", qos=1)

def on_disconnect(client, userdata, rc):
    print("desconectado, rc =", rc)

def on_message(client, topic, message):
    print(f"{topic}: {message}")
    if message == "ping":
        client.publish(f"{BASE}/pong", str(time.monotonic()))

pool = socketpool.SocketPool(wifi.radio)
mqtt = MQTT.MQTT(
    broker=os.getenv("MQTT_BROKER"),
    port=os.getenv("MQTT_PORT"),
    username=os.getenv("MQTT_USER"),
    password=os.getenv("MQTT_PASS"),
    client_id=DEVICE,
    is_ssl=True,
    keep_alive=60,
    socket_pool=pool,
    ssl_context=ssl.create_default_context(),
)
mqtt.on_connect = on_connect
mqtt.on_disconnect = on_disconnect
mqtt.on_message = on_message
mqtt.will_set(f"{BASE}/status", "offline", retain=True, qos=1)

mqtt.connect()
mqtt.publish(f"{BASE}/status", "online", retain=True, qos=1)

last = 0
while True:
    mqtt.loop(timeout=1)                  # lee los paquetes entrantes y envía PINGREQ cuando toca
    if time.monotonic() - last >= 10:
        temp = microcontroller.cpu.temperature
        mqtt.publish(f"{BASE}/temperature", f"{temp:.1f}")
        last = time.monotonic()

Qué hacen las líneas importantes:

  • socket_pool y ssl_context conectan MiniMQTT con la pila de red de la placa. ssl.create_default_context() usa los certificados raíz incluidos en el firmware de CircuitPython, así que un broker con certificado de una CA pública se verifica sin archivos adicionales.
  • loop(timeout=1) debe ejecutarse a menudo. Procesa los mensajes entrantes, llama a on_message y envía los pings de keep-alive a su tiempo. El timeout no puede ser menor que el socket_timeout del cliente (1 segundo por defecto), o MiniMQTT lanza un error.
  • Suscribirse en on_connect hace que cada conexión correcta, la primera y cada reconexión, restaure la suscripción. Importa porque los endpoints de prueba compartidos como broker.connectmq.com no conservan sesiones entre conexiones.
  • microcontroller.cpu.uid da un client ID estable y único. Si prefieres IDs aleatorios para pruebas desechables, el generador de client ID crea IDs válidos.
  • microcontroller.cpu.temperature es un valor cómodo para la demo; en algunas placas devuelve None. Cámbialo por un driver de sensor real (BME280, SHT4x…) para lecturas de verdad.

Enrutado por topic

Cuando una placa se suscribe a varios topics, los callbacks por topic mantienen el código ordenado:

Callbacks por topic
def on_led(client, topic, message):
    led.value = message == "on"

mqtt.add_topic_callback(f"{BASE}/led", on_led)
mqtt.subscribe(f"{BASE}/led")

Los mensajes de ese topic van a on_led; todo lo demás sigue llegando al on_message general.

3. Pruébalo desde el navegador

Guarda code.py y abre la consola serie (Mu, Thonny o screen /dev/tty.usbmodem* 115200). Deberías ver la dirección IP y después conectado, rc = 0.

Ahora abre el cliente MQTT online, conéctate al mismo broker y suscríbete a testmqtt/cpy/#. Verás el estado online retenido y una temperatura cada 10 segundos. Publica ping en testmqtt/cpy/<device>/cmd y en menos de un segundo vuelve un pong.

Ver tu placa CircuitPython en el navegador

Para probar el Last Will, desenchufa la placa: tras aproximadamente un periodo y medio de keep-alive, el broker publica offline en el topic de estado.

4. Sobrevivir a las desconexiones

En desarrollo, una excepción simplemente detiene code.py y la corriges. En un dispositivo metido en un armario, una excepción no capturada significa que se queda mudo hasta que alguien lo desenchufa y lo vuelve a enchufar. Envuelve el bucle:

code.py — bucle principal resistente
def ensure_wifi():
    if not wifi.radio.connected:
        print("reconexión Wi-Fi")
        wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"),
                           os.getenv("CIRCUITPY_WIFI_PASSWORD"))

delay = 1
last = 0
while True:
    try:
        mqtt.loop(timeout=1)
        if time.monotonic() - last >= 10:
            mqtt.publish(f"{BASE}/temperature", f"{microcontroller.cpu.temperature:.1f}")
            last = time.monotonic()
        delay = 1
    except (OSError, RuntimeError, ConnectionError, MQTT.MMQTTException) as e:
        print("error:", e, "- reintento en", delay, "s")
        time.sleep(delay)
        delay = min(delay * 2, 60)
        try:
            ensure_wifi()
            mqtt.reconnect()              # on_connect se ejecuta de nuevo y vuelve a suscribirse
            mqtt.publish(f"{BASE}/status", "online", retain=True, qos=1)
        except Exception as e2:
            print("reconexión fallida:", e2)
            if delay >= 60:
                microcontroller.reset()   # último recurso tras fallos repetidos

El patrón es el mismo en cualquier plataforma: arregla primero la capa de red y luego MQTT, aplica backoff exponencial para que una flota no arrolle a un broker que se reinicia, y deja un reset duro como última red de seguridad. Buenas prácticas de reconexión MQTT explica el razonamiento con más detalle.

5. Notas sobre TLS y certificados

  • Los certificados de CA públicas funcionan sin más. CircuitPython incluye un paquete de certificados raíz y ssl.create_default_context() lo usa.
  • ¿Autofirmado o CA privada? Cárgalo explícitamente: ctx = ssl.create_default_context(); ctx.load_verify_locations(cadata=CA_PEM), donde CA_PEM es el texto PEM de tu certificado raíz.
  • La memoria es limitada. TLS necesita un buffer considerable. En placas sin PSRAM, conéctate a MQTT pronto en code.py y evita reservas grandes antes del handshake.
  • TCP sin cifrar en el 1883 funciona con is_ssl=False y port=1883, pero envía la contraseña en texto plano. Úsalo solo en una red de pruebas.

Si el handshake falla, descarta primero el lado del broker con una prueba de conexión TLS desde tu ordenador.

Errores que verás

MensajeCausa
ConnectionError: No network with that ssidSSID incorrecto o red solo de 5 GHz
ConnectionError: Authentication failureContraseña de Wi-Fi incorrecta
MMQTTException: Connection Refused - Incorrect username/passwordEl broker rechazó las credenciales (CONNACK 4)
MMQTTException: Connection Refused - UnauthorizedSin permiso para conectarse (CONNACK 5)
OSError: [Errno 116] ETIMEDOUTPuerto bloqueado, host incorrecto o broker inalcanzable
MemoryErrorNo hay heap suficiente para TLS o para un payload grande

Los códigos CONNACK son los códigos de retorno estándar de MQTT 3.1.1; la herramienta de reason codes los recoge todos. Para un montaje con sensores que va más allá (varios sensores, payloads JSON, un dashboard), consulta la estación meteorológica MQTT. Y si estás eligiendo plataforma, el tutorial de umqtt en MicroPython muestra las mismas tareas con un cliente de más bajo nivel.

MiniMQTT da a las placas CircuitPython un cliente MQTT completo con TLS en unas pocas decenas de líneas. Pon los secretos en settings.toml, suscríbete en on_connect, llama a loop() constantemente y envuélvelo todo en un bucle de reconexión: una placa de hobby se convierte en un dispositivo que puedes dejar funcionando durante meses.

Preguntas frecuentes

¿Qué placas pueden ejecutar Adafruit MiniMQTT?

Cualquier placa CircuitPython con pila de red: placas con Wi-Fi nativo como ESP32-S2, ESP32-S3 y la Raspberry Pi Pico W, y placas que usan un coprocesador ESP32 AirLift mediante la biblioteca esp32spi. El código MQTT es el mismo; solo cambia la configuración del socket pool.

¿MiniMQTT soporta MQTT 5 o QoS 2?

No. MiniMQTT habla MQTT 3.1.1 y soporta QoS 0 y QoS 1. Los brokers compatibles con MQTT 5 también aceptan clientes 3.1.1, así que funciona con ellos, pero no puedes usar propiedades de MQTT 5 desde la placa.

¿Por qué mis mensajes de MiniMQTT llegan como str y no como bytes?

Por defecto, MiniMQTT decodifica los payloads como UTF-8 y pasa un str a on_message. Para payloads binarios como CBOR o imágenes, crea el cliente con use_binary_mode=True y recibirás bytes.