Um projeto ESP32 PlatformIO MQTT pode ser muito mais do que um único sketch com credenciais fixas no código. Neste guia, você vai montar um projeto PlatformIO com um ambiente de desenvolvimento apontando para o broker.connectmq.com e um ambiente de produção com TLS e credenciais privadas, mantendo os segredos fora do Git e as versões das bibliotecas fixadas para builds reproduzíveis.
Por que PlatformIO para firmware MQTT
A Arduino IDE serve bem para um primeiro sketch. Assim que um projeto tem mais de um arquivo, mais de um alvo ou mais de um desenvolvedor, o PlatformIO compensa:
- Builds reproduzíveis. As versões de plataforma e bibliotecas ficam no
platformio.ini, então um colega ou um servidor de CI compila exatamente o que você compilou. - Múltiplos ambientes. Um único projeto pode gerar firmware para um broker de testes e um broker de produção, ou para placas diferentes, a partir do mesmo código.
- Estrutura de projeto de verdade. Headers em
include/, código dividido em arquivos emsrc/, módulos reutilizáveis emlib/e testes emtest/. - Builds pela linha de comando.
pio runfunciona igual no VS Code, em um terminal e em um pipeline de CI.
Organização do projeto
Crie um novo projeto para um ESP32 DevKit com o framework Arduino e organize-o assim:
esp32-mqtt/
platformio.ini
secrets.ini (not committed)
secrets.example.ini (committed template)
.gitignore
include/
config.h
src/
main.cpp
mqtt_link.cpp
mqtt_link.h
O .gitignore mantém os segredos locais e a saída de build fora do repositório:
.pio/
secrets.ini
O platformio.ini com ambientes de dev e produção
[platformio]
default_envs = dev
extra_configs = secrets.ini
[env]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
monitor_filters = esp32_exception_decoder
lib_deps =
knolleary/PubSubClient @ ^2.8
bblanchon/ArduinoJson @ ^7.0.0
build_flags =
-D WIFI_SSID=\"${secrets.wifi_ssid}\"
-D WIFI_PASS=\"${secrets.wifi_pass}\"
-D FW_VERSION=\"1.0.0\"
[env:dev]
build_flags =
${env.build_flags}
-D MQTT_HOST=\"broker.connectmq.com\"
-D MQTT_PORT=1883
-D MQTT_USE_TLS=0
-D TOPIC_PREFIX=\"connectmq/demo/6d1f3c\"
-D CORE_DEBUG_LEVEL=3
[env:prod]
build_flags =
${env.build_flags}
-D MQTT_HOST=\"broker.connectmq.com\"
-D MQTT_PORT=8883
-D MQTT_USE_TLS=1
-D MQTT_CLIENT_ID=\"${secrets.mqtt_client_id}\"
-D MQTT_USER=\"${secrets.mqtt_user}\"
-D MQTT_PASS=\"${secrets.mqtt_pass}\"
-D TOPIC_PREFIX=\"factory1/line1/sensor01\"
-D CORE_DEBUG_LEVEL=1
A seção compartilhada [env] reúne tudo o que é comum. Cada ambiente nomeado estende build_flags com suas próprias configurações de broker. extra_configs = secrets.ini mescla um segundo arquivo à configuração, de modo que ${secrets.wifi_ssid} resolve para um valor que nunca entra no controle de versão.
O arquivo secrets.ini:
[secrets]
wifi_ssid = YOUR_WIFI
wifi_pass = YOUR_WIFI_PASSWORD
mqtt_client_id = YOUR_CLIENT_ID
mqtt_user = YOUR_USERNAME
mqtt_pass = YOUR_PASSWORD
Faça commit de um secrets.example.ini com as mesmas chaves e valores de exemplo, para que novos colaboradores saibam o que preencher. Os valores de produção vêm do portal do ConnectMQ, onde cada broker recebe um client ID, usuário e senha gerados.
No CI, você pode dispensar o arquivo e ler variáveis de ambiente com ${sysenv.MQTT_PASS}. Lembre-se de que build flags acabam virando strings dentro do binário, então qualquer pessoa com o arquivo de firmware consegue extraí-las. Para dispositivos distribuídos a terceiros, prefira credenciais por dispositivo provisionadas em tempo de execução, por exemplo com o portal WiFiManager MQTT para ESP32.
config.h: um único lugar para os padrões
O header dá a cada configuração um padrão seguro e faz o build falhar se faltar algo essencial:
#pragma once
#ifndef MQTT_HOST
#define MQTT_HOST "broker.connectmq.com"
#endif
#ifndef MQTT_PORT
#define MQTT_PORT 1883
#endif
#ifndef MQTT_USE_TLS
#define MQTT_USE_TLS 0
#endif
#ifndef TOPIC_PREFIX
#error "TOPIC_PREFIX must be set in platformio.ini"
#endif
#if MQTT_USE_TLS && !defined(MQTT_PASS)
#error "Production builds need MQTT credentials from secrets.ini"
#endif
#ifndef FW_VERSION
#define FW_VERSION "dev"
#endif
Um #error é muito melhor do que um firmware que compila, é distribuído e depois se conecta silenciosamente ao lugar errado.
O módulo MQTT
Manter a lógica MQTT em um arquivo próprio deixa o main.cpp curto e permite reutilizar o módulo entre projetos.
src/mqtt_link.h:
#pragma once
#include <Arduino.h>
namespace mqtt_link {
void begin();
void loop();
bool publish(const char* subtopic, const char* payload, bool retain = false);
bool connected();
}
src/mqtt_link.cpp:
#include "mqtt_link.h"
#include "config.h"
#include <WiFi.h>
#include <PubSubClient.h>
#if MQTT_USE_TLS
#include <WiFiClientSecure.h>
#include "isrg_root_x1.h" // const char ISRG_ROOT_X1[] = R"EOF(...PEM...)EOF";
#endif
namespace {
#if MQTT_USE_TLS
WiFiClientSecure net;
#else
WiFiClient net;
#endif
PubSubClient client(net);
String clientId;
String statusTopic;
unsigned long nextAttempt = 0;
unsigned long backoffMs = 1000;
void onMessage(char* topic, byte* payload, unsigned int length) {
Serial.printf("[mqtt] %s: %.*s\n", topic, (int)length, (const char*)payload);
}
void tryConnect() {
bool ok;
#if MQTT_USE_TLS
ok = client.connect(MQTT_CLIENT_ID, MQTT_USER, MQTT_PASS,
statusTopic.c_str(), 1, true, "offline");
#else
ok = client.connect(clientId.c_str(), statusTopic.c_str(), 1, true, "offline");
#endif
if (ok) {
client.publish(statusTopic.c_str(), "online", true);
client.subscribe((String(TOPIC_PREFIX) + "/cmd").c_str());
backoffMs = 1000;
Serial.println("[mqtt] connected");
} else {
Serial.printf("[mqtt] failed, state=%d\n", client.state());
nextAttempt = millis() + backoffMs;
backoffMs = min(backoffMs * 2, 60000UL);
}
}
} // namespace
namespace mqtt_link {
void begin() {
#if MQTT_USE_TLS
net.setCACert(ISRG_ROOT_X1);
clientId = MQTT_CLIENT_ID;
#else
clientId = "esp32-pio-" + String((uint32_t)ESP.getEfuseMac(), HEX);
#endif
statusTopic = String(TOPIC_PREFIX) + "/status";
client.setServer(MQTT_HOST, MQTT_PORT);
client.setBufferSize(512);
client.setSocketTimeout(5);
client.setCallback(onMessage);
}
void loop() {
if (WiFi.status() != WL_CONNECTED) return;
if (client.connected()) {
client.loop();
} else if (millis() >= nextAttempt) {
tryConnect();
}
}
bool publish(const char* subtopic, const char* payload, bool retain) {
String topic = String(TOPIC_PREFIX) + "/" + subtopic;
return client.publish(topic.c_str(), payload, retain);
}
bool connected() { return client.connected(); }
} // namespace mqtt_link
Crie include/isrg_root_x1.h com o certificado ISRG Root X1 do Let's Encrypt como uma string PEM. Ele só é compilado nos builds de produção.
src/main.cpp:
#include <Arduino.h>
#include <WiFi.h>
#include <ArduinoJson.h>
#include "config.h"
#include "mqtt_link.h"
unsigned long lastPublish = 0;
void setup() {
Serial.begin(115200);
Serial.printf("Firmware %s, broker %s:%d\n", FW_VERSION, MQTT_HOST, MQTT_PORT);
WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(true);
WiFi.begin(WIFI_SSID, WIFI_PASS);
mqtt_link::begin();
}
void loop() {
mqtt_link::loop();
if (mqtt_link::connected() && millis() - lastPublish > 10000) {
lastPublish = millis();
JsonDocument doc;
doc["fw"] = FW_VERSION;
doc["uptime"] = millis() / 1000;
doc["rssi"] = WiFi.RSSI();
char buf[128];
serializeJson(doc, buf, sizeof(buf));
mqtt_link::publish("telemetry", buf);
}
}
Compilando, gravando e monitorando
# build de desenvolvimento contra a área pública de testes
pio run -e dev -t upload
pio device monitor -e dev
# build de produção com TLS e credenciais do portal
pio run -e prod -t upload
# compila todos os ambientes, como o CI faria
pio run
Enquanto o build de dev roda, acompanhe seus topics pelo computador:
mosquitto_sub -h broker.connectmq.com -p 1883 -i pc-6d1f3c -t 'connectmq/demo/6d1f3c/#' -v
Configurações de ambiente em resumo
| Configuração | dev | prod |
|---|---|---|
| Broker | Servidor MQTT público broker.connectmq.com | Seu broker ConnectMQ privado |
| Porta e transporte | 1883, TCP simples | 8883, TLS com ISRG Root X1 |
| Client ID | Derivado do MAC | Client ID emitido pelo portal |
| Credenciais | Nenhuma | Usuário e senha do secrets.ini |
| Prefixo de topic | connectmq/demo/<random> |
Sua própria hierarquia, como um caminho ISA-95 |
| Nível de log | Info | Somente erros |
Por que essa estrutura compensa
Dividir o código dessa forma não é organização por si só. Cada parte tem uma função que faz diferença real quando o projeto sai da sua bancada.
Os ambientes expressam a intenção. Ninguém precisa lembrar de mudar uma porta, trocar WiFiClient por WiFiClientSecure e colar credenciais antes de um release. Escolher prod faz tudo isso de forma consistente, e as verificações #error no config.h barram um build configurado pela metade antes que ele chegue a um dispositivo. A mesma ideia se estende naturalmente: adicione um ambiente staging apontando para um segundo broker privado, ou um ambiente por tipo de placa com uma linha board = diferente.
O módulo MQTT esconde o transporte. O main.cpp nunca sabe se está falando por TCP simples ou TLS, nem qual client ID está em uso. Quando você trocar o PubSubClient por um cliente assíncrono ou mover os topics para uma nova hierarquia, a mudança fica restrita a um arquivo. O módulo também cuida do backoff de reconexão, então todo projeto que o reutiliza ganha reconexão não bloqueante de graça.
O topic de status e o Last Will já vêm prontos. Como tryConnect() sempre registra um will offline retido e publica online após conectar, todo dispositivo construído a partir desse modelo informa sua disponibilidade sem código extra.
Dependências fixadas dão sentido ao CI. Um pipeline que roda pio run a cada commit só prova algo se compilar com as mesmas versões de bibliotecas que os desenvolvedores usam. Com faixas com circunflexo como ^2.8, você recebe correções de bugs, mas não versões major incompatíveis.
Se quiser verificações automáticas além da compilação, o executor de testes unitários do PlatformIO pode rodar testes na própria placa ou no host com um ambiente native. Lógica pura, como montagem de topics ou formatação de payload, é fácil de testar nativamente quando fica em um módulo próprio.
Armadilhas comuns no PlatformIO
- Macros de string sem aspas escapadas.
-D MQTT_HOST=broker.connectmq.comdefine um token solto, não uma string, e não compila. Use\"...\"como mostrado. - Sobrescrever em vez de estender as flags. Escrever
build_flags =em um ambiente sem${env.build_flags}descarta as flags compartilhadas, incluindo as credenciais de Wi-Fi. - Bibliotecas sem versão fixada. Um nome de biblioteca sem versão puxa a mais recente, e uma atualização major como ArduinoJson 6 para 7 pode quebrar seu build da noite para o dia. Fixe com
@ ^x.y.z. secrets.iniausente. Referências a uma seção indefinida falham de maneiras confusas. O arquivo de exemplo versionado evita isso.- Builds desatualizados após mudar flags. O PlatformIO normalmente recompila quando as flags mudam, mas se o comportamento parecer errado, rode
pio run -t clean. - Decodificação de crashes. Mantenha
monitor_filters = esp32_exception_decoderpara que os backtraces mostrem arquivo e linha, o que é inestimável em problemas como resets do task watchdog no ESP32.
Próximos passos
Com uma estrutura limpa, adicionar recursos é simples. Coloque o tratamento de JSON em um módulo próprio seguindo o guia de ArduinoJson e MQTT no ESP32, adicione entrega remota de firmware com atualizações OTA do ESP32 via MQTT e veja o tutorial de IoT com ESP32 para o projeto completo do dispositivo ao dashboard. Para entender por que os brokers de dev e prod se comportam de forma diferente, leia sobre o servidor MQTT público (gratuito) e seus limites.
Publique seu firmware PlatformIO com o ConnectMQ
O ambiente de dev funciona imediatamente com o servidor MQTT público. Para o ambiente de prod, crie um broker privado no ConnectMQ, um broker MQTT gerenciado com MQTT 3.1.1 e 5.0, TLS na porta 8883 e certificado Let's Encrypt. Cadastre-se no plano gratuito Serverless em app.connectmq.com, sem cartão de crédito, copie as credenciais geradas para o secrets.ini e rode pio run -e prod -t upload.
Perguntas frequentes
Como manter senhas MQTT fora do Git no PlatformIO?
Coloque-as em um arquivo secrets.ini listado no .gitignore e mesclado com extra_configs, ou leia-as de variáveis de ambiente com ${sysenv.NAME} no CI.
Um projeto PlatformIO pode compilar para vários brokers?
Sim. Defina um ambiente por broker ou estágio de implantação, cada um estendendo as build_flags compartilhadas com seu próprio host, porta e credenciais, e selecione-o com pio run -e <name>.
Credenciais em build flags ficam seguras dentro do firmware?
Elas são compiladas no binário como strings simples, então qualquer pessoa com o arquivo consegue extraí-las. Para dispositivos entregues a terceiros, provisione as credenciais em tempo de execução e armazene-as na NVS.
Quais bibliotecas usar para MQTT no PlatformIO?
O PubSubClient é o cliente síncrono mais usado, e o AsyncMqttClient é uma alternativa orientada a eventos. Adicione qualquer um deles ao lib_deps com uma versão fixada.



