ESP32 e Arduino

Atualização OTA do ESP32 via MQTT: Dispare Atualizações de Firmware por HTTPS e Reporte o Status

Dispare atualizações de firmware do ESP32 com um comando MQTT, baixe o binário por HTTPS com o HTTPUpdate e publique o progresso e os resultados em um tópico de status retido, para saber sempre qual versão cada dispositivo está rodando.

Dispositivos ESP32 recebendo um comando de firmware em um fluxo de atualização OTA do ESP32 via MQTT com servidor de download HTTPS
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

Uma atualização OTA do ESP32 via MQTT permite atualizar dispositivos em campo, como sensores espalhados por uma fazenda ou por uma mineradora, sem abrir o gabinete: você publica um comando, o dispositivo baixa o novo firmware por HTTPS e reporta o progresso de volta em um tópico de status. Este tutorial monta esse fluxo passo a passo com a biblioteca HTTPUpdate do Arduino-ESP32 e mostra as salvaguardas necessárias antes de usá-lo em hardware real.

Por que combinar MQTT e HTTPS para OTA

É tentador enviar o próprio binário do firmware pelo MQTT, mas raramente é uma boa ideia. Imagens de firmware têm de centenas de kilobytes a alguns megabytes, muitos brokers e bibliotecas cliente limitam o tamanho das mensagens bem abaixo disso, e dividir um binário em pedaços significa escrever sua própria lógica de remontagem, ordenação e checksum.

A divisão mais limpa é deixar cada protocolo fazer aquilo em que é bom:

  • O MQTT carrega as pequenas mensagens de controle: "atualize para a versão 1.4.0 a partir desta URL", além dos relatórios de progresso e resultado. Os dispositivos já estão conectados, então o comando chega instantaneamente, mesmo atrás de NAT.
  • O HTTPS carrega o binário grande a partir de um servidor web ou de um object storage, com TLS, requisições por faixa e cache já resolvidos.

O dispositivo nunca precisa de uma porta de entrada aberta, e você mantém no broker um registro ao vivo de cada tentativa de atualização.

O design dos tópicos

Tópico Direção Payload Retido
connectmq/demo/e47b10/<device>/ota/cmd Operador publica {"version":"1.4.0","url":"https://..."} Não
connectmq/demo/e47b10/<device>/ota/status ESP32 publica {"state":"downloading","progress":42} Sim
connectmq/demo/e47b10/<device>/info ESP32 publica {"version":"1.3.2"} Sim

Reter os tópicos de status e info significa que qualquer ferramenta que se inscreva depois, como um dashboard ou um script que verifica um rollout, vê imediatamente a versão atual e o último resultado de OTA de cada dispositivo.

Pré-requisitos

  • Um ESP32 com pelo menos 4 MB de flash e um esquema de partições com dois slots de aplicação OTA. O esquema padrão do Arduino tem isso; o "Huge APP (no OTA)" não tem.
  • Core Arduino-ESP32 2.x ou 3.x, que inclui o HTTPUpdate.
  • As bibliotecas PubSubClient e ArduinoJson.
  • Um servidor HTTPS que sirva o seu arquivo .bin. Qualquer host de arquivos estáticos serve.

Exporte o binário com Sketch > Export Compiled Binary na Arduino IDE, ou pegue o .pio/build/<env>/firmware.bin no PlatformIO.

Sketch completo de atualização OTA do ESP32 via MQTT

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPUpdate.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>

#define FW_VERSION "1.3.2"

const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* ALLOWED_HOST = "https://firmware.example.com/";   // only accept URLs under this prefix

// Root CA of your firmware host in PEM format
const char* FIRMWARE_CA = R"EOF(
-----BEGIN CERTIFICATE-----
...paste the root CA of your download server here...
-----END CERTIFICATE-----
)EOF";

WiFiClient mqttNet;
PubSubClient mqtt(mqttNet);
String deviceId, topicCmd, topicStatus, topicInfo;

volatile bool otaRequested = false;
String otaUrl, otaVersion;
int lastReported = -1;

void publishStatus(const char* state, int progress = -1, const char* error = nullptr) {
  JsonDocument doc;
  doc["state"] = state;
  doc["from"] = FW_VERSION;
  if (otaVersion.length()) doc["to"] = otaVersion;
  if (progress >= 0) doc["progress"] = progress;
  if (error) doc["error"] = error;
  char buf[192];
  size_t n = serializeJson(doc, buf, sizeof(buf));
  mqtt.publish(topicStatus.c_str(), (const uint8_t*)buf, n, true);
}

void onMessage(char* topic, byte* payload, unsigned int length) {
  JsonDocument doc;
  if (deserializeJson(doc, payload, length)) return;
  String url = doc["url"] | "";
  String version = doc["version"] | "";
  if (!url.startsWith(ALLOWED_HOST) || version.length() == 0) {
    publishStatus("rejected", -1, "url or version invalid");
    return;
  }
  if (version == FW_VERSION) {
    publishStatus("up_to_date");
    return;
  }
  otaUrl = url;
  otaVersion = version;
  otaRequested = true;              // do the work in loop(), not here
}

void onProgress(int current, int total) {
  int pct = total > 0 ? (current * 100) / total : 0;
  if (pct / 10 != lastReported / 10) {   // report every 10 %
    lastReported = pct;
    publishStatus("downloading", pct);
    mqtt.loop();
  }
}

void runOta() {
  otaRequested = false;
  lastReported = -1;
  publishStatus("starting");

  WiFiClientSecure https;
  https.setCACert(FIRMWARE_CA);

  httpUpdate.rebootOnUpdate(false);
  httpUpdate.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
  httpUpdate.onProgress(onProgress);

  t_httpUpdate_return result = httpUpdate.update(https, otaUrl);

  switch (result) {
    case HTTP_UPDATE_OK:
      publishStatus("success", 100);
      mqtt.loop();
      delay(500);
      ESP.restart();
      break;
    case HTTP_UPDATE_NO_UPDATES:
      publishStatus("no_update");
      break;
    case HTTP_UPDATE_FAILED:
    default:
      publishStatus("failed", -1, httpUpdate.getLastErrorString().c_str());
      break;
  }
}

void connectMqtt() {
  while (!mqtt.connected()) {
    if (mqtt.connect(deviceId.c_str(), topicStatus.c_str(), 1, true,
                     "{\"state\":\"offline\"}")) {
      mqtt.subscribe(topicCmd.c_str());
      String info = String("{\"version\":\"") + FW_VERSION + "\"}";
      mqtt.publish(topicInfo.c_str(), info.c_str(), true);
    } else {
      delay(3000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) delay(250);

  deviceId = "esp32-ota-" + String((uint32_t)ESP.getEfuseMac(), HEX);
  String base = "connectmq/demo/e47b10/" + deviceId;
  topicCmd = base + "/ota/cmd";
  topicStatus = base + "/ota/status";
  topicInfo = base + "/info";

  mqtt.setServer("broker.connectmq.com", 1883);
  mqtt.setBufferSize(512);
  mqtt.setCallback(onMessage);
  Serial.printf("Device %s running %s\n", deviceId.c_str(), FW_VERSION);
}

void loop() {
  if (!mqtt.connected()) connectMqtt();
  mqtt.loop();
  if (otaRequested) runOta();
}

Após a primeira gravação via USB, leia o ID do dispositivo no monitor serial e dispare uma atualização:

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

mosquitto_pub -h broker.connectmq.com -p 1883 -i pc-e47b10-pub \
  -t connectmq/demo/e47b10/esp32-ota-1a2b3c4d/ota/cmd \
  -m '{"version":"1.4.0","url":"https://firmware.example.com/sensor-1.4.0.bin"}'

Você verá starting, uma série de mensagens downloading, success e depois uma mensagem info com a nova versão quando o dispositivo reiniciar e reconectar.

Como as peças se encaixam

O callback apenas registra o pedido. httpUpdate.update() bloqueia durante todo o download. Executá-lo dentro do callback MQTT bloquearia o mqtt.loop() de dentro dele mesmo, então o sketch marca uma flag e deixa o loop() iniciar a atualização. Isso também evita que o dispositivo dispare o task watchdog; resets do task watchdog do ESP32 em projetos MQTT explica por quê.

O progresso mantém a conexão MQTT viva. Durante um download longo, o callback de progresso publica a cada 10 % e chama mqtt.loop(), o que também envia pings de keepalive para que o broker não derrube a sessão no meio do caminho.

rebootOnUpdate(false) dá a você a última palavra. Por padrão, o HTTPUpdate reinicia assim que a imagem é gravada. Desativar isso permite que o dispositivo publique success primeiro e só então reinicie.

O Last Will reporta travamentos. A chamada de conexão registra uma mensagem de will offline retida no tópico de status, então um dispositivo que morre no meio da atualização aparece como offline em vez de ficar parado em "downloading". O guia de Last Will MQTT no ESP32 aborda esse padrão em detalhes.

Salvaguardas de segurança que você não pode pular

Atualizações remotas são a coisa mais poderosa que seu dispositivo pode fazer, o que torna o comando de atualização o alvo mais atraente. Antes de levar isso para perto de uma implantação real:

  1. Nunca dispare OTA a partir de um tópico público em produção. O servidor MQTT público (gratuito) do ConnectMQ pode ser lido e escrito por qualquer pessoa, o que serve para a demo com uma URL fictícia, mas não para dispositivos reais. Use um broker privado com credenciais.
  2. Restrinja o host de download a uma lista permitida. O sketch rejeita qualquer URL que não comece com o seu próprio prefixo HTTPS, então um comando malicioso não consegue apontar o dispositivo para um binário qualquer.
  3. Valide o certificado TLS. Carregue a CA raiz do seu host de firmware com setCACert(). Com setInsecure(), qualquer um no caminho de rede poderia substituir a imagem.
  4. Assine suas imagens. Para garantias mais fortes, o ESP-IDF suporta secure boot e verificação de aplicação assinada, de modo que o bootloader rejeita firmware que você não assinou. Isso exige um build com ESP-IDF ou uma configuração Arduino personalizada.
  5. Verifique as versões. Ignore comandos para a versão já instalada e considere recusar downgrades, a menos que estejam explicitamente sinalizados.

Rollout para uma frota

Passo O que fazer
1 Envie o novo binário com um nome de arquivo versionado; nunca sobrescreva o antigo
2 Atualize um dispositivo de teste e acompanhe o tópico de status até que info mostre a nova versão
3 Atualize um grupo pequeno e fique atento a dispositivos que permanecem offline
4 Faça o rollout para o restante em lotes, não todos de uma vez
5 Mantenha o binário anterior disponível para poder enviá-lo de volta, se necessário

Enviar os comandos dispositivo por dispositivo, em vez de para um tópico compartilhado no qual todos se inscrevem, dá a você esse controle. Um pequeno script em Python ou Node.js que lê uma lista de IDs de dispositivos, publica o comando e espera cada success normalmente é toda a ferramenta de que você precisa.

Solução de problemas

  • HTTP_UPDATE_FAILED com "Not Enough Space": o binário é maior que o slot OTA. Escolha um esquema de partições com partições de aplicação maiores.
  • "Wrong HTTP Code" com redirecionamento: alguns hosts redirecionam para uma CDN. O sketch segue redirecionamentos no modo estrito; garanta que a CA carregada também corresponda ao host final.
  • O handshake TLS falha: CA errada ou heap insuficiente. Libere buffers grandes antes de chamar runOta().
  • O dispositivo reinicia no firmware antigo: a nova imagem não conseguiu inicializar. Teste o binário via USB primeiro.
  • O broker derruba o dispositivo durante o download: o callback de progresso não está rodando com frequência suficiente para chamar mqtt.loop(). Reduza o passo de reporte.

Rode seu fluxo de OTA no ConnectMQ

Teste o fluxo inteiro no servidor MQTT público em broker.connectmq.com, feito para testes e protótipos, e depois coloque os tópicos de comando atrás de credenciais do ConnectMQ. O ConnectMQ é um broker MQTT gerenciado com isolamento multi-tenant, client IDs vinculados às credenciais emitidas e TLS na 8883, para que apenas as suas ferramentas possam enviar comandos de atualização aos seus dispositivos. Crie um broker no plano Serverless gratuito em app.connectmq.com, sem cartão de crédito, e use o explorador de tópicos ao vivo para acompanhar o tópico de status de cada dispositivo durante um rollout. Para o projeto completo de dispositivo em torno disso, veja o tutorial de IoT com ESP32.

Perguntas frequentes

Posso enviar o próprio binário do firmware via MQTT?

É possível, mas raramente vale a pena. Os binários são grandes, o tamanho das mensagens é limitado e você precisaria de sua própria divisão em pedaços e verificação. Envie uma URL via MQTT e baixe o binário por HTTPS.

O que acontece se a energia cair durante uma atualização OTA?

O ESP32 grava a nova imagem no slot OTA inativo e só troca a partição de boot após uma gravação completa e verificada. Uma queda de energia no meio do download mantém o firmware antigo no lugar.

Por que meu dispositivo mostra "downloading" para sempre?

Provavelmente ele travou ou perdeu energia sem uma desconexão limpa. Com um Last Will retido no tópico de status, o broker substitui esse estado por "offline" após o timeout de keepalive.

Preciso de QoS 1 para o comando de OTA?

QoS 1 é uma boa escolha para comandos, porque garante a entrega enquanto o dispositivo está conectado. Torne o handler idempotente ignorando comandos para a versão que já está instalada.

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.