ESP32 e Arduino

MicroPython umqtt no ESP32: Publish e Subscribe com umqtt.simple e umqtt.robust

Conecte um ESP32 com MicroPython a um broker MQTT usando umqtt.simple, adicione reconexão automática com umqtt.robust, receba comandos sem bloquear e entenda o que esses clientes mínimos não conseguem fazer.

Placa ESP32 executando um script MicroPython umqtt que publica leituras de sensores
Rode isso no seu próprio broker MQTTTenha um broker MQTT privado e gerenciado no plano Free e comece com exemplos de código prontos.
Teste grátis Exemplos de código Sem cartão de crédito

O MicroPython umqtt no ESP32 oferece MQTT em poucas linhas de Python, sem etapa de compilação e com um prompt interativo para depuração. Este tutorial conecta um ESP32 ao servidor MQTT público broker.connectmq.com com umqtt.simple, adiciona reconexão automática com umqtt.robust, trata comandos recebidos sem bloquear e explica os limites que você precisa conhecer antes de confiar nesses clientes em produção.

Por que MicroPython para dispositivos MQTT

C++ com o core Arduino é mais rápido e tem mais bibliotecas, mas o MicroPython ganha em velocidade de iteração. Você edita um arquivo, copia para a placa e executa em segundos. O REPL permite testar Wi-Fi, sensores e MQTT de forma interativa, o que é inestimável quando você está tentando descobrir por que algo não funciona.

O pacote umqtt da micropython-lib fornece dois módulos:

  • umqtt.simple: um cliente MQTT 3.1.1 mínimo. Conectar, publicar, assinar, ping, last will. Nada acontece automaticamente.
  • umqtt.robust: uma subclasse fina de simple que captura erros de socket em publish(), wait_msg() e algumas outras chamadas e reconecta automaticamente.

Ambos são pequenos o suficiente para caber com folga em qualquer ESP32.

Preparando a placa

  1. Grave um firmware MicroPython atual para o seu ESP32 a partir da página oficial de downloads, usando esptool.py.
  2. Instale o mpremote no seu computador: pip install mpremote.
  3. Verifique se o umqtt já está presente. Alguns builds de firmware o incluem, outros não. Se import umqtt.simple falhar no REPL, instale-o com o mip:
mpremote connect auto mip install umqtt.simple
mpremote connect auto mip install umqtt.robust

O mip precisa que a placa esteja online. Se ela ainda não estiver conectada ao Wi-Fi, execute a instalação a partir de um script que conecte primeiro, ou copie os arquivos do módulo manualmente com mpremote cp. A documentação de gerenciamento de pacotes do MicroPython descreve todas as opções.

Conectando ao Wi-Fi

Coloque isto em boot.py para que seja executado a cada inicialização:

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()

Publicando com umqtt.simple

O primeiro script se conecta ao servidor MQTT público em broker.connectmq.com e publica uma leitura a cada cinco segundos. O client ID é montado a partir do ID único do chip, para que nunca fique vazio nem colida com outra 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   # leitura aproximada do sensor interno
    payload = '{"temp":%.1f,"uptime":%d}' % (temp_c, time.ticks_ms() // 1000)
    client.publish(PREFIX + b"/telemetry", payload.encode())
    time.sleep(5)

Em variantes do ESP32 sem raw_temperature(), substitua essa linha pela leitura de um sensor real. Acompanhe as mensagens pelo seu computador:

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

Observe que topics e payloads são bytes. O umqtt aceita str em muitos lugares, mas usar bytes de forma consistente evita surpresas, principalmente nos callbacks, que sempre recebem bytes.

Recebendo comandos sem bloquear

Para receber mensagens, registre um callback e faça a assinatura. Depois, você precisa chamar regularmente wait_msg(), que bloqueia até chegar uma mensagem, ou check_msg(), que retorna imediatamente se não houver nada esperando. Use check_msg() em um loop que também faz outras tarefas:

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()                       # trata qualquer mensagem pendente

    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()                        # mantém a conexão viva quando ociosa
        last_ping = now

    time.sleep_ms(50)

Teste:

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

Dois detalhes passam despercebidos com facilidade. Primeiro, o umqtt.simple não envia pings de keepalive por conta própria. Se você define um keepalive e publica com menos frequência que isso, precisa chamar ping(), como faz o loop acima, ou o broker vai derrubar a conexão. Segundo, set_callback() deve ser chamado antes de subscribe().

Reconexão automática com umqtt.robust

O umqtt.simple lança OSError quando a conexão cai, e seu script morre se você não tratar o erro. O umqtt.robust envolve as chamadas principais e reconecta com um pequeno intervalo entre as tentativas. A única mudança é o import:

from umqtt.robust import MQTTClient

Há uma pegadinha. O cliente robust reconecta o socket, mas não refaz as assinaturas para você, e com uma clean session o broker esquece suas assinaturas. Um padrão confiável é encapsular a etapa de conexão e chamá-la tanto na inicialização quanto após qualquer erro que você mesmo capture:

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()
        # ... publicação 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 autônomos, como um sensor em uma fazenda distante do escritório, adicione machine.WDT(timeout=30000) e alimente-o no loop, para que um travamento total reinicie a placa.

Estruturando um projeto MicroPython umqtt no ESP32

Quando um script passa de algumas dezenas de linhas, divida-o como faria em qualquer projeto Python. Uma organização que funciona bem na placa:

  • boot.py conecta o Wi-Fi e nada mais, para que um bug na aplicação nunca impeça seu acesso ao REPL.
  • config.py guarda host do broker, porta, prefixo de topic e credenciais. Mantenha-o fora do controle de versão e copie-o para cada placa separadamente.
  • main.py importa config, cria o cliente e executa o loop principal dentro de um try/except que registra a exceção e chama machine.reset() após um breve intervalo.

Dois hábitos economizam muito tempo de depuração. Estruture o loop principal de modo que pressionar Ctrl+C no REPL o interrompa de forma limpa com KeyboardInterrupt, em vez de ser capturado pelo seu tratador genérico de erros. E chame gc.collect() periodicamente se você monta strings de payload no loop; o heap do MicroPython é pequeno e a fragmentação aparece como MemoryError depois de horas de execução, não de minutos.

Publicar JSON é mais fácil com o módulo nativo json: client.publish(topic, json.dumps({"temp": 22.4})). A interpretação de comandos funciona do mesmo jeito com json.loads(msg) dentro do callback, envolto em um try para que uma mensagem malformada não derrube o dispositivo.

Limitações do umqtt que você precisa conhecer

Limitação Impacto Alternativa
Apenas MQTT 3.1.1 Sem propriedades MQTT 5, reason codes ou recursos de assinatura compartilhada Use outro cliente se precisar de MQTT 5
Apenas QoS 0 e 1 QoS 2 não é suportado QoS 1 basta para a maioria das telemetrias e comandos
Sem pings de keepalive automáticos Conexões ociosas são derrubadas Chame ping() periodicamente
Chamadas de socket bloqueantes connect() e wait_msg() bloqueiam o script Use check_msg(); considere um cliente baseado em asyncio para dispositivos complexos
Sem reassinatura na reconexão Os comandos deixam de chegar após uma reconexão Refaça as assinaturas na sua própria função de reconexão
Sem fila de mensagens offline Mensagens publicadas enquanto desconectado são perdidas Armazene as leituras em buffer por conta própria, se necessário

Para dispositivos que precisam fazer várias coisas ao mesmo tempo, existem clientes MQTT da comunidade baseados em asyncio que cuidam de keepalives e reconexão em segundo plano. Para sensores e atuadores simples, o umqtt continua sendo a opção mais leve.

Usando TLS na porta 8883

Em um broker privado, criptografe a conexão. Versões recentes do umqtt.simple aceitam um argumento ssl que recebe um SSLContext; versões mais antigas usam ssl=True com um dicionário ssl_params, então confira a versão que você instalou.

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 raiz do 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()

Copie antes o certificado ISRG Root X1 para a placa com mpremote cp. O TLS exige uma boa quantidade de RAM; placas com PSRAM lidam com ele com mais folga. Use exatamente o client ID, o usuário e a senha gerados para o seu broker, porque o ConnectMQ vincula o client ID às credenciais emitidas.

Guias relacionados

Se você prefere trabalhar em C++, o guia de projeto MQTT com ESP32 e PlatformIO mostra uma configuração Arduino bem estruturada, e o guia de Last Will MQTT no ESP32 explica o padrão de status usado no primeiro script. O tutorial de IoT com ESP32 cobre o caminho completo do dispositivo ao dashboard e, se você é novo no protocolo, comece por o que faz um broker MQTT.

Conecte seus dispositivos MicroPython ao ConnectMQ

Todos os exemplos aqui funcionam com o servidor MQTT público (gratuito), sem cadastro, mas ele serve para testes e protótipos, não para dados de produção. Quando for além dos experimentos, o ConnectMQ oferece um broker MQTT gerenciado privado, com TLS na porta 8883 e um cliente MQTT no navegador para verificar o que sua placa envia; o guia de broker MQTT na nuvem compara as opções de hospedagem. Crie o seu no plano gratuito Serverless em app.connectmq.com, sem cartão de crédito.

Perguntas frequentes

O umqtt suporta MQTT 5?

Não. Tanto o umqtt.simple quanto o umqtt.robust implementam MQTT 3.1.1. Brokers que suportam MQTT 5, como o ConnectMQ, continuam aceitando conexões 3.1.1.

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

Você definiu um keepalive, mas não está enviando nada dentro desse intervalo. O umqtt não faz ping automaticamente, então chame client.ping() regularmente ou publique com mais frequência que o keepalive.

Qual é a diferença entre wait_msg() e check_msg()?

wait_msg() bloqueia até chegar uma mensagem. check_msg() processa uma mensagem pendente, se houver, e retorna imediatamente, o que é ideal para loops que também leem sensores.

O umqtt pode usar QoS 2?

Não. Ele suporta QoS 0 e 1, o que cobre a maioria dos casos de uso em dispositivos.

Artigos relacionados

Exemplo de MQTT com ESP-IDF: Monte um Cliente esp-mqtt do Zero

Monte um projeto ESP-IDF a partir de uma pasta vazia com o componente esp-mqtt: configuração do Wi-Fi em modo station, o handler de eventos MQTT, publicação e inscrição, propriedades do MQTT 5 e TLS com o bundle de certificados.

8 min de leitura

Usando os Recursos do MQTT 5 do esp-mqtt no ESP32 com ESP-IDF

Um projeto ESP-IDF prático que ativa o MQTT 5.0 no esp-mqtt e usa user properties, expiração de mensagens, topic aliases, expiração de sessão e response topics com um broker MQTT 5 real.

8 min de leitura

Todos os artigos →

Seu broker MQTT privado está a poucos cliques de distância

Crie uma conta, receba as credenciais do seu broker e conecte seu primeiro dispositivo — sem servidores para instalar, atualizar ou escalar.