ESP32 e Arduino

Projeto MQTT com ESP32 e PlatformIO: Estrutura Limpa com Configurações de Broker por Ambiente

Estruture um projeto ESP32 PlatformIO MQTT com ambientes separados de desenvolvimento e produção, configurações de broker injetadas na compilação, segredos fora do controle de versão e versões de bibliotecas fixadas.

VS Code com PlatformIO mostrando ambientes de desenvolvimento e produção de um projeto ESP32 PlatformIO MQTT
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

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 em src/, módulos reutilizáveis em lib/ e testes em test/.
  • Builds pela linha de comando. pio run funciona 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

  1. Macros de string sem aspas escapadas. -D MQTT_HOST=broker.connectmq.com define um token solto, não uma string, e não compila. Use \"...\" como mostrado.
  2. 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.
  3. 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.
  4. secrets.ini ausente. Referências a uma seção indefinida falham de maneiras confusas. O arquivo de exemplo versionado evita isso.
  5. 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.
  6. Decodificação de crashes. Mantenha monitor_filters = esp32_exception_decoder para 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.

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.