Tutoriais

MQTT com MicroPython no ESP32 e no Pico W (umqtt)

Conecte um ESP32 ou Pico W a um broker MQTT com MicroPython: umqtt.simple x umqtt.robust, Wi-Fi, pings de keep-alive, loops de reconexão e cuidados com TLS.

Atualizado · 9 min de leitura

MQTT com MicroPython no ESP32 e no Pico W (umqtt)

O MicroPython traz um cliente MQTT minúsculo chamado umqtt que roda num ESP32, num ESP8266 ou num Raspberry Pi Pico W com poucos kilobytes de RAM. Ele vem em dois sabores: umqtt.simple, um cliente síncrono mínimo, e umqtt.robust, que acrescenta reconexão automática por cima. Este tutorial instala os dois, conecta via Wi-Fi, publica e assina, mantém a conexão viva, se recupera de quedas e explica o que observar com TLS.

O código mira o MicroPython 1.22 ou mais novo e o umqtt.simple atual da micropython-lib. Roda sem mudanças em placas ESP32 e no Pico W; só os números de pino mudam se você ler sensores.

umqtt em resumo

umqtt.simpleumqtt.robust
Versão do MQTT3.1.13.1.1
QoS0 e 1 (publish e subscribe)0 e 1
Reconexão automáticaNãoSim, em erros de socket
Reconexão do Wi-FiNãoNão
Reassinatura após reconectarNãoNão (depende de uma sessão persistente)
Pings de keep-aliveping() manualping() manual
TLSSim, via contexto sslIgual ao simple

Os dois são síncronos: wait_msg() bloqueia até chegar uma mensagem, check_msg() retorna na hora. Não existe thread em segundo plano, então o seu loop principal é responsável por tudo.

1. Instale o umqtt

Alguns builds de firmware já incluem o umqtt (o port do ESP8266 geralmente inclui). No ESP32 e no Pico W, instale a partir da micropython-lib com o mpremote no seu computador:

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

Ou, quando a placa já estiver no Wi-Fi, pelo REPL:

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

Confira com from umqtt.simple import MQTTClient: sem ImportError, está tudo pronto.

2. Conecte ao Wi-Fi

O módulo network é o mesmo no ESP32 e no Pico W. Coloque isto em wifi.py para reutilizar em outros arquivos:

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

Os dois chips só suportam redes de 2,4 GHz. Se o status ficar em "conectando" para sempre, verifique a banda e a senha antes de desconfiar do MQTT.

3. Publicar e assinar com 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("recebido", topic, msg)
    if msg == b"led/on":
        machine.Pin("LED", machine.Pin.OUT).on()   # Pico W; no ESP32 use o seu pino

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)          # precisa vir 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()                   # não bloqueia: roda on_message se houver dados
    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)

Pontos principais:

  • Topics e payloads são bytes. Strings costumam funcionar, mas bytes evitam surpresas, e on_message sempre recebe bytes.
  • set_callback() antes de subscribe(): caso contrário, o umqtt lança um erro de asserção.
  • check_msg() x wait_msg(): wait_msg() bloqueia até algo chegar, o que serve para um receptor de comandos puro, mas trava o seu loop de sensores. Use check_msg() quando também publica.
  • Keep-alive é com você. Com keepalive=60, o broker derruba o cliente depois de cerca de 90 segundos de silêncio. O loop acima faz ping a cada 30 segundos. Veja MQTT keep alive para as regras exatas de tempo.
  • Client ID único a partir de machine.unique_id(), para que duas placas rodando o mesmo arquivo não derrubem uma à outra.

Teste

Abra o cliente MQTT online, conecte em broker.connectmq.com e assine testmqtt/upy/#. Você deve ver a mensagem status retida e um valor de uptime a cada 10 segundos. Publique led/on em testmqtt/upy/<seu-client-id>/cmd e o REPL imprime recebido ....

Veja a placa publicando no navegador

4. Reconectando do jeito certo

O Wi-Fi cai, roteadores reiniciam, brokers reiniciam. Com o umqtt.simple, toda falha de rede aparece como um OSError (tipicamente [Errno 104] ECONNRESET, [Errno 113] ECONNABORTED ou [Errno 110] ETIMEDOUT) na chamada que tocou o socket. Envolva o loop, refaça a conexão e assine de novo:

main.py — loop de reconexão
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)  # numa nova clean session as assinaturas somem
    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)                # só termina lançando OSError
    except OSError as e:
        print("conexão perdida:", e, "nova tentativa em", delay, "s")
        try:
            client.disconnect()
        except Exception:
            pass
        time.sleep(delay)
        delay = min(delay * 2, 60)      # backoff exponencial com teto

O backoff exponencial importa quando você tem muitos dispositivos: se o broker reinicia, uma frota tentando a cada segundo, todos ao mesmo tempo, cria uma tempestade de reconexões. O padrão é explicado em boas práticas de reconexão MQTT.

Para dispositivos que nunca podem travar, adicione um watchdog de hardware: wdt = machine.WDT(timeout=30000) e chame wdt.feed() uma vez por iteração do loop. Se o código bloquear numa chamada de socket, a placa se reinicia sozinha.

E o umqtt.robust?

umqtt.robust.MQTTClient tem a mesma API. Quando publish(), wait_msg() ou check_msg() encontram um OSError, ele chama reconnect(), que tenta connect(False) para sempre com um atraso crescente. Duas limitações:

  1. Ele nunca mexe no Wi-Fi. Se o ponto de acesso sumiu, ele insiste num socket que não tem como funcionar.
  2. Ele reconecta com clean_session=False e não refaz as assinaturas. Isso só restaura as assinaturas se o broker guardou a sessão: um broker privado normalmente guarda, mas endpoints de teste compartilhados como broker.connectmq.com não mantêm sessões persistentes. Teste o comportamento de sessões persistentes na sua própria instância de broker.

O umqtt.robust serve para um protótipo rápido. Para qualquer coisa que rode sem supervisão, o loop explícito acima é mais fácil de entender.

5. Cuidados com TLS

A porta 1883 envia usuário e senha em texto puro. Para qualquer coisa além de um teste na bancada, conecte na 8883 com TLS. O umqtt.simple atual recebe um ssl.SSLContext:

TLS com verificação de certificado
import ssl, ntptime
ntptime.settime()                        # a validade do certificado exige um relógio 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()

O que costuma pegar as pessoas:

  • Versões antigas do umqtt usavam ssl=True, ssl_params={...} em vez de um contexto. Se aparecer TypeError no construtor, confira qual versão está na placa.
  • Nenhum repositório de CAs no dispositivo. O MicroPython não traz certificados raiz. Copie para a placa a CA raiz que assinou o certificado do seu broker (converta PEM para DER com openssl x509 -in ca.pem -outform der -out ca.der e envie com mpremote cp ca.der :).
  • CERT_NONE é só para depuração. Criptografa o tráfego, mas aceita qualquer servidor, inclusive o de um atacante.
  • Memória. Um handshake TLS precisa de dezenas de kilobytes de heap livre. ESP32 e Pico W dão conta; o ESP8266 costuma ficar apertado. Chame gc.collect() antes de connect() e conecte cedo, antes que a aplicação encha o heap.
  • Relógio. Sem ntptime.settime() a placa acha que está no ano 2000 e pode rejeitar um certificado válido como ainda não válido.

Testar uma conexão TLS mostra como verificar a cadeia de certificados do broker pelo notebook antes de depurar no dispositivo.

Erros comuns

ErroSignificadoCorreção
MQTTException: 5CONNACK "not authorized"Usuário errado ou ACL nega o cliente
MQTTException: 4Usuário ou senha inválidosVerifique as credenciais
MQTTException: 2Identifier rejectedClient ID vazio, longo demais ou bloqueado
OSError: [Errno 104] ECONNRESETO broker fechou o socketKeep-alive perdido, client ID duplicado, TLS numa porta sem TLS
OSError: -2 ou -202Falha na resolução DNS (código depende do port)Wi-Fi ainda não pronto ou hostname errado
MemoryError ao conectar com TLSHeap insuficientegc.collect(), conectar mais cedo, buffers menores

Os números depois de MQTTException são códigos de retorno CONNACK do MQTT 3.1.1; a ferramenta de reason codes explica cada um. Para um passo a passo mais profundo, veja erros MQTT no MicroPython.

O umqtt é pequeno o bastante para ser lido de uma vez, e essa é a sua força: assim que você adiciona os próprios pings de keep-alive, um loop de reconexão com backoff, a reassinatura e TLS com uma CA de verdade, ele vira um cliente confiável para sensores que poupam bateria. Faça o protótipo no endpoint compartilhado e depois leve o mesmo código para um broker privado trocando o host e as credenciais.

Perguntas frequentes

Devo usar umqtt.simple ou umqtt.robust?

umqtt.robust é uma subclasse fina do umqtt.simple que reconecta automaticamente quando ocorre um erro de socket. Ela não reconecta o Wi-Fi nem refaz as assinaturas por você, por isso muitos projetos usam umqtt.simple com um loop de reconexão próprio, que dá controle total sobre Wi-Fi, assinaturas e backoff.

O umqtt suporta QoS 2 ou MQTT 5?

Não. O umqtt.simple implementa MQTT 3.1.1 apenas com QoS 0 e QoS 1. Se você precisa de propriedades do MQTT 5 ou QoS 2 num microcontrolador, procure outro cliente ou um gateway que faça a tradução para o dispositivo.

Por que meu cliente MQTT em MicroPython desconecta depois de cerca de um minuto?

O umqtt não envia pings de keep-alive sozinho. Se você define keepalive e depois não publica nada por mais de 1,5 vez esse valor, o broker fecha a conexão. Chame client.ping() periodicamente no loop principal, ou use keepalive=0 se o seu broker permitir.