Um controlador ESP32 com relé via MQTT permite acionar lâmpadas, bombas (como as de irrigação de uma fazenda), ventiladores ou contatores a partir de qualquer app, script ou dashboard capaz de publicar uma mensagem. Neste tutorial, você vai ligar um módulo relé a um ESP32, inscrever-se em um topic de comando, publicar o estado do relé como retained message e restaurar o último estado conhecido após uma reinicialização. Tudo é testado na área pública gratuita do broker.connectmq.com e depois migrado para credenciais em produção.
Como Funciona o Projeto ESP32 Relé MQTT
Um nó de relé é o atuador útil mais simples da IoT, mas também é onde muitos projetos dão errado. O erro comum é tratar o MQTT como um botão remoto: algo publica ON, o ESP32 aciona o relé e ninguém nunca sabe se o comando realmente chegou. Um projeto melhor separa o que alguém quer do que o dispositivo está realmente fazendo.
| Topic | Direção | Retido | Payload |
|---|---|---|---|
connectmq/demo/<id>/relay/1/set |
App para o ESP32 | Não | ON, OFF, TOGGLE |
connectmq/demo/<id>/relay/1/state |
ESP32 para os apps | Sim | ON ou OFF |
connectmq/demo/<id>/status |
ESP32 para os apps | Sim | online ou offline (Last Will) |
O topic de comando não é retido. Se fosse, cada reconexão repetiria um comando antigo, e um relé que alguém desligou manualmente poderia voltar a ligar de repente. O topic de estado é retido, então um dashboard aberto um minuto depois recebe imediatamente o estado atual, sem esperar pela próxima mudança. O topic status usa o Last Will do MQTT para que os assinantes saibam quando o nó sai da rede.
Se retained messages e Last Will são novidade para você, a visão geral sobre o que um broker MQTT faz explica os dois em poucos minutos.
Hardware e Ligações
Você vai precisar de:
- Qualquer placa de desenvolvimento ESP32 (DevKitC, NodeMCU-32S, módulos WROOM ou WROVER funcionam).
- Um módulo relé de 1, 2 ou 4 canais com entrada por optoacoplador. Módulos de 5 V com jumpers de "high/low level trigger" são os mais flexíveis.
- Jumpers e um cabo USB. Para qualquer coisa ligada à rede elétrica, um gabinete adequado e bornes.
Ligação típica para um módulo de 5 V:
| Pino do módulo relé | Pino do ESP32 | Observação |
|---|---|---|
| VCC | 5V (VIN) | Alimenta a bobina do relé |
| GND | GND | Terra comum |
| IN1 | GPIO 26 | Sinal de controle do canal 1 |
| IN2 | GPIO 27 | Sinal de controle do canal 2 (opcional) |
A maioria das placas de relé baratas é ativa em nível baixo: o relé é energizado quando a entrada é levada ao GND. O sketch abaixo tem uma única constante para inverter essa lógica. Evite os GPIOs 0, 2, 12 e 15 para relés, porque são pinos de strapping, e um relé estalando durante o boot é incômodo e, com algumas cargas, perigoso. Os GPIOs 34 a 39 são somente entrada e não vão funcionar.
Um aviso de segurança: se você for acionar tensão da rede elétrica, o módulo relé precisa ser dimensionado para a carga, o lado de alta tensão precisa ficar fechado em um gabinete e o trabalho deve seguir as normas elétricas locais. Enquanto desenvolve o firmware, acione uma fita de LED de baixa tensão, ou nada, e apenas escute o clique.
Instale as Bibliotecas
Na Arduino IDE 2.x, instale o pacote de placas esp32 da Espressif pelo Boards Manager e a biblioteca PubSubClient, de Nick O'Leary, pelo Gerenciador de Bibliotecas. Nenhuma outra biblioteca é necessária. Se você nunca montou um sketch MQTT no ESP32, o tutorial IoT com ESP32 mostra com mais detalhes a configuração da placa e a sua primeira publicação.
O Sketch Completo ESP32 Relé MQTT
O sketch abaixo suporta qualquer quantidade de relés, trata reconexões sem bloquear o loop e publica o estado retido após cada mudança.
#include <WiFi.h>
#include <PubSubClient.h>
// ---------- Configuração ----------
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const char* DEVICE_ID = "relay-7f3a91"; // torne-o único
const String BASE = String("connectmq/demo/") + DEVICE_ID;
const bool ACTIVE_LOW = true; // a maioria das placas de relé
const uint8_t RELAY_PINS[] = {26, 27};
const uint8_t RELAY_COUNT = sizeof(RELAY_PINS);
bool relayState[RELAY_COUNT] = {false};
bool restoreWindow = true; // aceita o estado retido uma vez no boot
unsigned long connectedAt = 0;
WiFiClient net;
PubSubClient mqtt(net);
unsigned long lastAttempt = 0;
void applyRelay(uint8_t i, bool on) {
relayState[i] = on;
digitalWrite(RELAY_PINS[i], (on ^ ACTIVE_LOW) ? HIGH : LOW);
}
void publishState(uint8_t i) {
String topic = BASE + "/relay/" + String(i + 1) + "/state";
mqtt.publish(topic.c_str(), relayState[i] ? "ON" : "OFF", true);
}
void onMessage(char* topic, byte* payload, unsigned int length) {
String t(topic);
String msg;
for (unsigned int i = 0; i < length; i++) msg += (char)payload[i];
msg.trim();
msg.toUpperCase();
// Esperado: <BASE>/relay/<n>/set ou <BASE>/relay/<n>/state
int start = BASE.length() + 7; // pula "/relay/"
int slash = t.indexOf('/', start);
if (slash < 0) return;
int n = t.substring(start, slash).toInt();
if (n < 1 || n > RELAY_COUNT) return;
uint8_t i = n - 1;
String leaf = t.substring(slash + 1);
if (leaf == "state") {
// Estado retido de antes da reinicialização: restaura uma vez e depois ignora
if (restoreWindow) applyRelay(i, msg == "ON");
return;
}
if (leaf != "set") return;
if (msg == "ON") applyRelay(i, true);
else if (msg == "OFF") applyRelay(i, false);
else if (msg == "TOGGLE") applyRelay(i, !relayState[i]);
else return; // ignora comandos desconhecidos
publishState(i);
Serial.printf("Relay %d -> %s\n", n, relayState[i] ? "ON" : "OFF");
}
bool connectMqtt() {
String willTopic = BASE + "/status";
bool ok = mqtt.connect(DEVICE_ID, nullptr, nullptr,
willTopic.c_str(), 1, true, "offline");
if (!ok) {
Serial.printf("MQTT connect failed, state=%d\n", mqtt.state());
return false;
}
mqtt.publish(willTopic.c_str(), "online", true);
for (uint8_t i = 0; i < RELAY_COUNT; i++) {
String base = BASE + "/relay/" + String(i + 1);
mqtt.subscribe((base + "/set").c_str(), 1);
if (restoreWindow) mqtt.subscribe((base + "/state").c_str(), 1);
}
connectedAt = millis();
Serial.println("MQTT connected");
return true;
}
void setup() {
Serial.begin(115200);
for (uint8_t i = 0; i < RELAY_COUNT; i++) {
pinMode(RELAY_PINS[i], OUTPUT);
applyRelay(i, false); // padrão seguro antes do Wi-Fi
}
WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(true);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setCallback(onMessage);
mqtt.setKeepAlive(30);
mqtt.setSocketTimeout(5);
}
void loop() {
if (WiFi.status() != WL_CONNECTED) return;
if (!mqtt.connected()) {
if (millis() - lastAttempt > 5000) {
lastAttempt = millis();
connectMqtt();
}
return;
}
mqtt.loop();
// Fecha a janela de restauração dois segundos após a primeira conexão
if (restoreWindow && millis() - connectedAt > 2000) {
restoreWindow = false;
for (uint8_t i = 0; i < RELAY_COUNT; i++) {
String s = BASE + "/relay/" + String(i + 1) + "/state";
mqtt.unsubscribe(s.c_str());
publishState(i); // confirma o estado real
}
}
}
O que a janela de restauração faz
Quando o ESP32 reinicia após uma queda de energia, os relés começam na posição segura OFF. Na primeira conexão, o sketch se inscreve por um instante nos seus próprios topics state retidos. O broker entrega imediatamente o último estado que tem guardado, o sketch o aplica e, depois de dois segundos, cancela a inscrição e publica novamente o estado real. Se você prefere que os relés sempre comecem desligados, defina restoreWindow como false. Para cargas críticas, como aquecedores, "sempre desligado após reiniciar" costuma ser a política mais segura.
Lembre-se de que isso depende de o broker ainda guardar a retained message. As retained messages existem enquanto o broker as mantém, então trate a restauração como uma conveniência, e não como uma garantia, e projete a carga de modo que OFF seja sempre um estado aceitável.
Teste o Relé a Partir do Seu Computador
Abra um terminal e acompanhe tudo sob o prefixo do seu dispositivo:
mosquitto_sub -h broker.connectmq.com -p 1883 -i watch-7f3a91 -v -t "connectmq/demo/relay-7f3a91/#"
Em um segundo terminal, envie comandos:
mosquitto_pub -h broker.connectmq.com -p 1883 -i cmd-7f3a91 -t "connectmq/demo/relay-7f3a91/relay/1/set" -m ON
mosquitto_pub -h broker.connectmq.com -p 1883 -i cmd-7f3a91 -t "connectmq/demo/relay-7f3a91/relay/1/set" -m TOGGLE
Você deve ouvir o clique e ver o estado retido ecoado de volta:
connectmq/demo/relay-7f3a91/status online
connectmq/demo/relay-7f3a91/relay/1/state ON
connectmq/demo/relay-7f3a91/relay/1/state OFF
Agora desconecte o ESP32. Em aproximadamente um intervalo e meio de keep alive, o broker publica o Last Will, e o assinante imprime status offline. Esse é exatamente o sinal de que um dashboard precisa para deixar o interruptor acinzentado. Como o broker.connectmq.com é um servidor MQTT público (gratuito), qualquer pessoa que adivinhe o seu topic pode acionar o seu relé, então nunca deixe uma carga real ligada à área pública.
Checklist de Solução de Problemas
- O relé estala no boot. Você está em um pino de strapping, ou o módulo é ativo em nível baixo e o pino fica flutuando antes de
setup()rodar. Passe para os GPIOs 26/27/32/33 e defina o nível do pino antes dopinModequando possível. - O relé nunca comuta, embora o LED do módulo acenda. Muitos módulos precisam de 5 V no VCC para a bobina; 3,3 V acende o LED, mas não consegue atracar os contatos. Algumas placas também precisam do jumper
JD-VCCinstalado. - Os comandos funcionam uma vez e depois param. Procure
state=-3(conexão perdida) no log serial. Usar o mesmo client ID em dois dispositivos faz com que um derrube o outro repetidamente. - O ESP32 reinicia quando o relé comuta. Cargas indutivas causam quedas na alimentação e ruído. Alimente a bobina do relé com uma fonte de 5 V separada, com terra compartilhado, e use um snubber ou proteção flyback no lado da carga.
- O topic de estado mostra o valor errado após reiniciar. Outra coisa está publicando retained messages no topic
state. Somente o próprio dispositivo deve publicar ali. - Nada chega. Os nomes de topics diferenciam maiúsculas de minúsculas, e uma barra sobrando no final cria um topic diferente.
Vá para Produção com Credenciais e TLS
Em uma instalação real, crie um broker no portal do ConnectMQ e use o usuário, a senha e o client ID gerados. Troque WiFiClient por WiFiClientSecure, conecte-se à porta 8883 e passe as credenciais para connect():
#include <WiFiClientSecure.h>
WiFiClientSecure net;
// no setup(): net.setCACert(ROOT_CA_PEM); // ISRG Root X1 (Let's Encrypt)
mqtt.setServer("broker.connectmq.com", 8883);
mqtt.connect("YOUR_CLIENT_ID", "YOUR_USERNAME", "YOUR_PASSWORD",
willTopic.c_str(), 1, true, "offline");
Como os client IDs ficam vinculados às credenciais emitidas, use o client ID que o portal gerou em vez de inventar um. A configuração completa do certificado, incluindo a sincronização de hora e as considerações de memória, está em ESP32 MQTT com SSL/TLS. Se o seu nó de relé precisa sobreviver a um Wi-Fi instável, também vale acrescentar a estratégia de backoff de lógica de reconexão MQTT no ESP32.
Um broker MQTT gerenciado importa mais para atuadores: um relé que você não consegue alcançar é pior do que um sensor que você não consegue ler. Com o ConnectMQ, você tem TLS na 8883, credenciais por dispositivo e um portal onde pode acompanhar relay/+/state no explorador de topics ao vivo ou publicar um comando de teste pelo cliente no navegador, sem abrir um terminal.
Ideias para Expandir o Nó de Relé
- Temporizadores. Aceite
{"state":"ON","seconds":600}em JSON e desligue automaticamente após dez minutos. Mantenha o temporizador no dispositivo, para que ele desligue mesmo se a rede cair. - Botão local. Adicione um botão que alterna o relé e publica o novo estado, para que o interruptor físico e o app nunca discordem.
- Intertravamentos. Para reversão de motores ou válvulas de duas vias, recuse ligar os dois canais ao mesmo tempo no firmware, e não no app.
- Projetos irmãos com servo ou LED. O mesmo padrão de comando/estado funciona para controle de servo via MQTT e fitas de LED endereçáveis.
Comece a Usar o ConnectMQ
Crie um broker Serverless gratuito em app.connectmq.com, sem precisar de cartão de crédito. Você recebe as credenciais em menos de um minuto, TLS pronto para uso e um explorador de topics ao vivo para ver seus relés comutando. Quando o plano gratuito ficar pequeno, o mesmo firmware passa para um plano pago ou dedicado sem mudar nada além das credenciais.
Perguntas frequentes
O topic de comando do relé deve ser retido?
Não. Retenha o topic de estado, não o de comando. Um comando retido seria repetido a cada reconexão e poderia acionar uma carga de forma inesperada.
Qual QoS devo usar para comandos de relé?
QoS 1 é a escolha prática: o comando é entregue pelo menos uma vez, e como ON e OFF são idempotentes, uma duplicata não causa problema. O PubSubClient pode se inscrever com QoS 0 ou 1.
Posso controlar vários relés com um ESP32?
Sim. Adicione os pinos a RELAY_PINS, e cada um ganha seus próprios topics relay/<n>/set e relay/<n>/state. Uma única inscrição com curinga em relay/+/set também funciona se você extrair o número do canal do topic.
Como sei se o ESP32 está offline?
Inscreva-se no topic status retido. O ESP32 publica online quando se conecta, e o broker publica a mensagem de Last Will offline se a conexão cair sem uma desconexão limpa.



