Com parâmetros ESP32 WiFiManager MQTT, um dispositivo que você entrega a outra pessoa, como um sensor instalado na fazenda de um cliente no agronegócio, pode ser configurado pelo celular: ele abre um portal cativo, o usuário escolhe uma rede Wi-Fi e preenche as configurações do broker, e o dispositivo lembra de tudo entre reinicializações. Este tutorial monta esse fluxo com a biblioteca WiFiManager e com Preferences, usando o broker.connectmq.com, o servidor MQTT público (gratuito) do ConnectMQ, como padrão para que tudo funcione logo de cara.
O problema das configurações fixas no código
A maioria dos exemplos MQTT começa com linhas como const char* WIFI_SSID = "..." e const char* MQTT_PASS = "...". Isso funciona na sua bancada, mas cada rede nova ou troca de credencial significa recompilar e regravar. Significa também que o binário do firmware contém segredos e que você não pode entregar o mesmo binário a dois clientes.
Um portal de provisionamento resolve isso. No primeiro boot, ou quando o usuário segura um botão, o ESP32 inicia seu próprio ponto de acesso. Ao conectar nele com o celular, abre-se uma página web onde o usuário informa:
- A rede Wi-Fi e a senha.
- O host e a porta do broker MQTT.
- O client ID, o usuário e a senha do MQTT.
- Um prefixo de tópico para este dispositivo.
O dispositivo salva os valores no armazenamento não volátil, entra na rede Wi-Fi e conecta ao broker. O mesmo binário funciona para todos.
Bibliotecas e hardware
- WiFiManager, de tzapu (2.x), instalável pelo Library Manager da Arduino IDE ou pelo PlatformIO como
tzapu/WiFiManager. - PubSubClient para o MQTT.
- Preferences, que vem com o core Arduino-ESP32 e guarda pares chave–valor na flash NVS.
- Qualquer placa ESP32. O exemplo usa o botão BOOT (GPIO 0) da maioria dos DevKits para reabrir o portal.
Como funciona o fluxo ESP32 WiFiManager MQTT
| Situação | O que acontece |
|---|---|
| Primeiro boot, sem Wi-Fi salvo | O portal inicia como ponto de acesso Sensor-Setup-xxxx |
| Wi-Fi salvo acessível | O dispositivo conecta silenciosamente e carrega as configurações MQTT de Preferences |
| Wi-Fi salvo inacessível | O portal abre de novo depois que a tentativa de conexão falha |
| Botão BOOT pressionado por 3 s na inicialização | O portal abre sob demanda para que o usuário altere as configurações |
| Portal sem uso por 3 minutos | O dispositivo reinicia e tenta de novo, para nunca ficar travado para sempre |
O WiFiManager guarda as credenciais do Wi-Fi por conta própria. Os campos MQTT são parâmetros personalizados que você adiciona ao portal e persiste você mesmo.
Sketch completo
#include <WiFi.h>
#include <WiFiManager.h>
#include <Preferences.h>
#include <PubSubClient.h>
const int CONFIG_BUTTON = 0; // BOOT button on most ESP32 DevKits
Preferences prefs;
WiFiClient net;
PubSubClient mqtt(net);
char mqttHost[64] = "broker.connectmq.com";
char mqttPort[6] = "1883";
char mqttClient[48] = "";
char mqttUser[48] = "";
char mqttPass[64] = "";
char topicPrefix[64] = "connectmq/demo/d93f2a";
bool shouldSave = false;
unsigned long lastPublish = 0;
void loadSettings() {
prefs.begin("mqtt", true);
prefs.getString("host", mqttHost, sizeof(mqttHost));
prefs.getString("port", mqttPort, sizeof(mqttPort));
prefs.getString("client", mqttClient, sizeof(mqttClient));
prefs.getString("user", mqttUser, sizeof(mqttUser));
prefs.getString("pass", mqttPass, sizeof(mqttPass));
prefs.getString("prefix", topicPrefix, sizeof(topicPrefix));
prefs.end();
if (strlen(mqttClient) == 0) { // never connect with an empty client ID
snprintf(mqttClient, sizeof(mqttClient), "esp32-%08x", (uint32_t)ESP.getEfuseMac());
}
}
void saveSettings() {
prefs.begin("mqtt", false);
prefs.putString("host", mqttHost);
prefs.putString("port", mqttPort);
prefs.putString("client", mqttClient);
prefs.putString("user", mqttUser);
prefs.putString("pass", mqttPass);
prefs.putString("prefix", topicPrefix);
prefs.end();
}
void runWifiManager(bool forcePortal) {
WiFiManager wm;
WiFiManagerParameter pHost("host", "MQTT host", mqttHost, sizeof(mqttHost) - 1);
WiFiManagerParameter pPort("port", "MQTT port (1883 or 8883)", mqttPort, sizeof(mqttPort) - 1);
WiFiManagerParameter pClient("client", "MQTT client ID", mqttClient, sizeof(mqttClient) - 1);
WiFiManagerParameter pUser("user", "MQTT username (optional)", mqttUser, sizeof(mqttUser) - 1);
WiFiManagerParameter pPass("pass", "MQTT password (optional)", mqttPass, sizeof(mqttPass) - 1,
"type='password'");
WiFiManagerParameter pPrefix("prefix", "Topic prefix", topicPrefix, sizeof(topicPrefix) - 1);
wm.addParameter(&pHost);
wm.addParameter(&pPort);
wm.addParameter(&pClient);
wm.addParameter(&pUser);
wm.addParameter(&pPass);
wm.addParameter(&pPrefix);
wm.setSaveParamsCallback([]() { shouldSave = true; });
wm.setConfigPortalTimeout(180);
char apName[32];
snprintf(apName, sizeof(apName), "Sensor-Setup-%04x", (uint16_t)ESP.getEfuseMac());
bool connected = forcePortal ? wm.startConfigPortal(apName)
: wm.autoConnect(apName);
if (!connected) {
ESP.restart(); // portal timed out: try again from scratch
}
if (shouldSave) {
strlcpy(mqttHost, pHost.getValue(), sizeof(mqttHost));
strlcpy(mqttPort, pPort.getValue(), sizeof(mqttPort));
strlcpy(mqttClient, pClient.getValue(), sizeof(mqttClient));
strlcpy(mqttUser, pUser.getValue(), sizeof(mqttUser));
strlcpy(mqttPass, pPass.getValue(), sizeof(mqttPass));
strlcpy(topicPrefix, pPrefix.getValue(), sizeof(topicPrefix));
saveSettings();
loadSettings(); // re-applies the client ID fallback
}
}
bool buttonHeld(unsigned long ms) {
pinMode(CONFIG_BUTTON, INPUT_PULLUP);
unsigned long start = millis();
while (digitalRead(CONFIG_BUTTON) == LOW) {
if (millis() - start > ms) return true;
delay(10);
}
return false;
}
void connectMqtt() {
while (!mqtt.connected()) {
bool ok = strlen(mqttUser) > 0
? mqtt.connect(mqttClient, mqttUser, mqttPass)
: mqtt.connect(mqttClient);
if (ok) {
String cmd = String(topicPrefix) + "/cmd";
mqtt.subscribe(cmd.c_str());
} else {
Serial.printf("MQTT failed state=%d, retrying\n", mqtt.state());
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
loadSettings();
delay(200);
runWifiManager(buttonHeld(3000));
mqtt.setServer(mqttHost, atoi(mqttPort));
Serial.printf("Broker %s:%s as %s\n", mqttHost, mqttPort, mqttClient);
}
void loop() {
if (!mqtt.connected()) connectMqtt();
mqtt.loop();
if (millis() - lastPublish > 10000) {
lastPublish = millis();
String topic = String(topicPrefix) + "/telemetry";
String payload = "{\"uptime\":" + String(millis() / 1000) + "}";
mqtt.publish(topic.c_str(), payload.c_str());
}
}
Percorrendo o portal
- Grave o sketch e abra o monitor serial.
- No celular, conecte-se à rede Wi-Fi chamada
Sensor-Setup-xxxx. A maioria dos celulares abre o portal cativo automaticamente; caso contrário, acesse192.168.4.1no navegador. - Toque em Configure WiFi, escolha sua rede e informe a senha dela.
- Mantenha os valores padrão para usar o servidor MQTT público em broker.connectmq.com ou informe suas próprias configurações e salve.
- O dispositivo entra na sua rede, conecta ao broker e começa a publicar em
<prefix>/telemetry.
Verifique a partir de um computador:
mosquitto_sub -h broker.connectmq.com -p 1883 -i pc-d93f2a -t 'connectmq/demo/d93f2a/#' -v
Todo dispositivo que mantém o prefixo padrão publica no mesmo tópico, então, se configurar vários, dê a cada um seu próprio prefixo no portal.
Detalhes que garantem a confiabilidade
Carregue antes de montar o portal. Os parâmetros personalizados são criados com os valores atuais como padrão, então o portal mostra o que está armazenado, e não um formulário em branco. Quem edita um campo não perde os outros.
Nunca conecte com um client ID vazio. Se o usuário apagar o campo, loadSettings() recorre a um ID derivado do MAC do chip. Um client ID vazio ou duplicado é um dos principais motivos para dispositivos não conectarem ou derrubarem uns aos outros; veja por que o ESP32 não conecta ao broker MQTT para a lista completa.
Use o timeout do portal. Sem setConfigPortalTimeout(), um dispositivo cujo roteador está temporariamente fora fica no modo portal para sempre. Com um timeout, ele reinicia e tenta de novo a rede salva.
Guarde números como texto no portal e converta uma vez. Os parâmetros do WiFiManager são campos de texto. Mantenha a porta como string no armazenamento e converta com atoi() ao configurar o cliente.
Oculte o campo de senha. O argumento extra type='password' mascara a senha MQTT no portal. O valor ainda trafega pelo ponto de acesso aberto em HTTP simples, então só faça o provisionamento em um local confiável.
Validando o que o usuário digita
Um portal cativo aceita qualquer coisa que o usuário digite, então o firmware precisa lidar com erros. Algumas verificações após salvar evitam que o dispositivo se inutilize com configurações ruins:
- Porta: aceite apenas valores entre 1 e 65535. Se
atoi()retornar 0 porque o campo continha texto, volte para 1883. - Host: remova espaços e rejeite string vazia. Usuários costumam colar uma URL como
mqtt://broker.connectmq.com; retire o esquema e qualquer coisa depois do nome do host. - Prefixo de tópico: remova a barra final e rejeite curingas (
+e#), que não são permitidos em tópicos de publicação. - Credenciais: se houver usuário, mas a senha estiver vazia, emita um aviso no console serial, porque o broker vai recusar a conexão.
Quando a conexão MQTT falha repetidamente com um código mqtt.state() positivo (credenciais inválidas ou não autorizado), geralmente o problema é de configuração, e não de rede. Um bom padrão é contar essas falhas e reabrir o portal automaticamente após várias seguidas, para que o usuário possa corrigir os valores sem precisar de botão ou cabo serial.
Adicionando TLS para brokers de produção
A porta 1883 é texto puro e serve para testes no servidor MQTT público. Para um broker privado, mude para WiFiClientSecure quando o usuário escolher a porta 8883:
#include <WiFiClientSecure.h>
WiFiClientSecure secureNet;
void configureTransport() {
if (atoi(mqttPort) == 8883) {
secureNet.setCACert(ISRG_ROOT_X1); // Let's Encrypt root, stored as a PEM string
mqtt.setClient(secureNet);
} else {
mqtt.setClient(net);
}
mqtt.setServer(mqttHost, atoi(mqttPort));
}
Chame configureTransport() no lugar da linha simples de setServer() no setup(). Com o ConnectMQ, o usuário então informa no portal o client ID, o usuário e a senha gerados para o broker dele (YOUR_CLIENT_ID, YOUR_USERNAME, YOUR_PASSWORD). O client ID é vinculado a essas credenciais, então precisa ser digitado exatamente como foi emitido. O guia de ESP32 MQTT SSL/TLS explica como embutir o certificado raiz.
Solução de problemas
| Problema | Causa provável | Solução |
|---|---|---|
| O portal nunca aparece | O Wi-Fi salvo continua conectando | Segure o botão BOOT durante o reset ou chame wm.resetSettings() uma vez |
| Os parâmetros não são salvos | O callback de salvamento não foi acionado | Use setSaveParamsCallback() e toque em Save na página do portal |
| Valores truncados | Tamanho do campo menor que a entrada | Aumente o tamanho do buffer e o comprimento do parâmetro |
| Conecta ao Wi-Fi, mas não ao MQTT | Porta errada para o transporte ou credenciais erradas | Verifique mqtt.state() e a combinação de porta e TLS |
| O celular não abre o portal | Detecção de portal cativo bloqueada | Acesse manualmente 192.168.4.1 no navegador |
Próximos passos
Com as configurações ajustáveis, os próximos passos naturais são atualizações remotas e relatórios de status: atualizações OTA do ESP32 disparadas via MQTT mostra como enviar novo firmware sem USB, e o tutorial de IoT com ESP32 coloca o provisionamento dentro de um projeto de dispositivo completo.
Provisione dispositivos para o ConnectMQ
As configurações padrão apontam para o servidor MQTT público (gratuito), então um dispositivo novo funciona assim que entra no Wi-Fi. Ele serve para testes e protótipos, não para dados de produção. Para implantações, crie um broker privado no ConnectMQ e informe as credenciais dele no portal. O ConnectMQ é um broker MQTT gerenciado, então quem configura os dispositivos nunca precisa manter um servidor. Comece no plano Serverless gratuito em app.connectmq.com, sem cartão de crédito.
Perguntas frequentes
Onde o WiFiManager guarda a senha do Wi-Fi?
O driver Wi-Fi do ESP32 guarda as credenciais da rede na flash NVS. Neste sketch, os campos MQTT são guardados separadamente com Preferences, no namespace mqtt.
Como reabro o portal do WiFiManager depois?
Neste sketch, segure o botão BOOT por três segundos durante a inicialização, o que chama startConfigPortal(). Você também poderia acioná-lo por um comando MQTT, desde que o dispositivo continue acessível.
Posso usar o WiFiManager com o AsyncMqttClient?
Sim. O WiFiManager cuida apenas do provisionamento. Depois que ele retorna, use os valores armazenados para configurar o cliente MQTT de sua preferência.
O portal cativo é seguro?
O portal roda como um ponto de acesso aberto em HTTP simples, então qualquer pessoa por perto poderia observar os valores durante a configuração. Faça o provisionamento em um local confiável e considere definir uma senha no ponto de acesso de configuração.



