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:
- 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.
- 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.
- Valide o certificado TLS. Carregue a CA raiz do seu host de firmware com
setCACert(). ComsetInsecure(), qualquer um no caminho de rede poderia substituir a imagem. - 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.
- 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_FAILEDcom "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.



