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
| Elemento | Notas |
|---|---|
| Placa CircuitPython con Wi-Fi | Feather o QT Py ESP32-S2/S3, Pico W o una placa AirLift |
| CircuitPython 9.x | Descárgalo de circuitpython.org para tu placa exacta |
Biblioteca adafruit_minimqtt | Más sus dependencias, instaladas por circup |
| Un broker MQTT | Los 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:
pip install circup
circup install adafruit_minimqtt
circup list # muestra las versiones instaladas y marca las desactualizadasTambié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.
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
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_poolyssl_contextconectan 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 aon_messagey envía los pings de keep-alive a su tiempo. El timeout no puede ser menor que elsocket_timeoutdel cliente (1 segundo por defecto), o MiniMQTT lanza un error.- Suscribirse en
on_connecthace que cada conexión correcta, la primera y cada reconexión, restaure la suscripción. Importa porque los endpoints de prueba compartidos comobroker.connectmq.comno conservan sesiones entre conexiones. microcontroller.cpu.uidda un client ID estable y único. Si prefieres IDs aleatorios para pruebas desechables, el generador de client ID crea IDs válidos.microcontroller.cpu.temperaturees un valor cómodo para la demo; en algunas placas devuelveNone. 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:
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:
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 repetidosEl 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), dondeCA_PEMes 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.pyy evita reservas grandes antes del handshake. - TCP sin cifrar en el 1883 funciona con
is_ssl=Falseyport=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
| Mensaje | Causa |
|---|---|
ConnectionError: No network with that ssid | SSID incorrecto o red solo de 5 GHz |
ConnectionError: Authentication failure | Contraseña de Wi-Fi incorrecta |
MMQTTException: Connection Refused - Incorrect username/password | El broker rechazó las credenciales (CONNACK 4) |
MMQTTException: Connection Refused - Unauthorized | Sin permiso para conectarse (CONNACK 5) |
OSError: [Errno 116] ETIMEDOUT | Puerto bloqueado, host incorrecto o broker inalcanzable |
MemoryError | No 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.

