Um nó com sensor de movimento PIR no ESP32 e MQTT transforma um detector infravermelho barato em uma fonte de eventos à qual qualquer dashboard ou automação pode reagir. Neste tutorial, você vai ligar um HC-SR501 ou AM312, capturar o movimento com uma interrupção, filtrar disparos ruidosos, publicar eventos no broker.connectmq.com e manter um timestamp retido do último movimento, que informa aos novos assinantes exatamente quando algo se moveu pela última vez.
Como os sensores PIR se comportam
Um sensor infravermelho passivo (PIR) detecta variações de radiação infravermelha no seu campo de visão, normalmente um corpo quente em movimento. O módulo converte isso em uma saída digital simples: nível alto enquanto há movimento, baixo caso contrário. Os detalhes importam para o MQTT, porque a saída não é um sinal limpo de um pulso por evento.
| Módulo | Alimentação | Saída | Ajustável | Observações |
|---|---|---|---|---|
| HC-SR501 | 5 V (pino VIN) | Nível alto de 3,3 V | Sensibilidade, tempo de retenção, jumper de modo de disparo | Lente grande, alcance de alguns metros, precisa de aquecimento |
| AM312 | 2,7 a 12 V | Nível alto de 3,3 V | Nenhum | Minúsculo, tempo de retenção fixo de alguns segundos, bom para montagens em 3,3 V |
| HC-SR505 | 4,5 a 20 V | Nível alto de 3,3 V | Nenhum | Compacto, tempo de retenção fixo de vários segundos |
O que saber antes de escrever o código:
- Aquecimento. Após ligados, os módulos PIR geram disparos aleatórios por até um minuto enquanto o sensor se estabiliza. Ignore movimentos nessa janela.
- Tempo de retenção. A saída permanece alta por um tempo configurado após o último movimento detectado. No HC-SR501, um potenciômetro o ajusta de alguns segundos a vários minutos.
- Modo de redisparo. Com o jumper do HC-SR501 na posição "H" (repetição), o movimento contínuo mantém a saída alta. No modo "L", a saída cai após o tempo de retenção mesmo que o movimento continue e depois dispara de novo. "H" costuma ser o que você quer.
- Disparos falsos. Luz solar, aquecedores, saídas de ar e antenas Wi-Fi posicionadas logo ao lado do sensor podem causar pulsos espúrios.
Ligação
Para o HC-SR501: VCC no pino 5V (VIN) do ESP32, GND no GND, OUT no GPIO 27. A saída é de 3,3 V, então vai diretamente à entrada do ESP32. Para o AM312: VCC no 3V3, GND no GND, OUT no GPIO 27. Mantenha o sensor um pouco afastado da antena do ESP32; a proximidade é uma fonte conhecida de disparos fantasmas.
Design de eventos: debounce, cooldown e topics
Publicar cada borda da saída do PIR inunda os assinantes e desperdiça a taxa por cliente no servidor MQTT público (gratuito). O sketch transforma o sinal bruto em dois estados limpos:
- Movimento detectado quando a saída vai a nível alto e o evento anterior é mais antigo que um período de cooldown.
- Movimento encerrado quando a saída está em nível baixo por um período de silêncio.
| Topic | Retido | Payload |
|---|---|---|
connectmq/demo/pir9d4k/motion |
Sim | {"state":"detected","ts":"2026-10-16T18:03:11Z","count":14} |
connectmq/demo/pir9d4k/motion/last |
Sim | 2026-10-16T18:03:11Z |
connectmq/demo/pir9d4k/status |
Sim | online / offline |
O topic motion/last é a ideia central: qualquer cliente que assine, mesmo horas depois, recebe instantaneamente o horário da última detecção. Os timestamps vêm do NTP; o guia de timestamp NTP no ESP32 para MQTT explica os detalhes de sincronização usados aqui.
O sketch do sensor de movimento PIR no ESP32 com MQTT
#include <WiFi.h>
#include <PubSubClient.h>
#include <time.h>
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const char* PREFIX = "connectmq/demo/pir9d4k"; // use seu próprio id aleatório
const uint8_t PIR_PIN = 27;
const uint32_t WARMUP_MS = 60000; // ignora disparos após ligar
const uint32_t COOLDOWN_MS = 10000; // tempo mínimo entre eventos "detected"
const uint32_t CLEAR_MS = 30000; // saída baixa por este tempo -> "clear"
WiFiClient net;
PubSubClient mqtt(net);
String clientId, tMotion, tLast, tStatus;
volatile bool edgeSeen = false;
volatile uint32_t lastEdgeMs = 0;
bool motionActive = false;
uint32_t lastDetectedMs = 0;
uint32_t lastHighMs = 0;
uint32_t motionCount = 0;
void IRAM_ATTR onPirRise() {
edgeSeen = true;
lastEdgeMs = millis();
}
bool isoNow(char* buf, size_t len) {
time_t now = time(nullptr);
if (now < 1700000000) return false; // ainda não sincronizado
struct tm utc;
gmtime_r(&now, &utc);
strftime(buf, len, "%Y-%m-%dT%H:%M:%SZ", &utc);
return true;
}
void publishState(const char* state) {
char ts[24] = "unknown";
bool synced = isoNow(ts, sizeof(ts));
char payload[128];
snprintf(payload, sizeof(payload), "{\"state\":\"%s\",\"ts\":\"%s\",\"count\":%lu}",
state, ts, (unsigned long)motionCount);
mqtt.publish(tMotion.c_str(), payload, true);
if (strcmp(state, "detected") == 0 && synced) {
mqtt.publish(tLast.c_str(), ts, true);
}
Serial.println(payload);
}
void connectWifi() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) delay(300);
Serial.printf("Wi-Fi OK, IP %s\n", WiFi.localIP().toString().c_str());
}
void connectMqtt() {
while (!mqtt.connected()) {
if (mqtt.connect(clientId.c_str(), tStatus.c_str(), 1, true, "offline")) {
mqtt.publish(tStatus.c_str(), "online", true);
Serial.println("MQTT connected");
} else {
Serial.printf("MQTT failed, state %d\n", mqtt.state());
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
pinMode(PIR_PIN, INPUT_PULLDOWN);
attachInterrupt(digitalPinToInterrupt(PIR_PIN), onPirRise, RISING);
clientId = "esp32-pir-" + String((uint32_t)ESP.getEfuseMac(), HEX);
tMotion = String(PREFIX) + "/motion";
tLast = String(PREFIX) + "/motion/last";
tStatus = String(PREFIX) + "/status";
connectWifi();
configTime(0, 0, "pool.ntp.org", "time.cloudflare.com");
mqtt.setServer(MQTT_HOST, MQTT_PORT);
connectMqtt();
Serial.println("Warming up PIR sensor...");
}
void loop() {
if (WiFi.status() != WL_CONNECTED) connectWifi();
if (!mqtt.connected()) connectMqtt();
mqtt.loop();
uint32_t now = millis();
bool pinHigh = digitalRead(PIR_PIN) == HIGH;
if (pinHigh) lastHighMs = now;
if (now < WARMUP_MS) { // descarta o ruído do aquecimento
edgeSeen = false;
return;
}
// Borda de subida (capturada pela interrupção) ou saída ainda alta -> movimento
if (edgeSeen || pinHigh) {
edgeSeen = false;
if (!motionActive && now - lastDetectedMs >= COOLDOWN_MS) {
motionActive = true;
lastDetectedMs = now;
motionCount++;
publishState("detected");
}
}
// Saída baixa por tempo suficiente -> clear
if (motionActive && !pinHigh && now - lastHighMs >= CLEAR_MS) {
motionActive = false;
publishState("clear");
}
}
A rotina de serviço de interrupção apenas define uma flag e um horário. Nunca publique de dentro de uma interrupção: o código de rede não é seguro para interrupções e o ESP32 vai travar. A flag garante que mesmo um pulso muito curto de um AM312 não seja perdido enquanto mqtt.loop() está ocupado.
Testando os eventos MQTT do sensor PIR no ESP32
Assine seu prefixo e acene diante do sensor após o minuto de aquecimento:
mosquitto_sub -h broker.connectmq.com -p 1883 -i pir-view-9d4k -t "connectmq/demo/pir9d4k/#" -v
Você vê um estado detected e depois clear cerca de 30 segundos após o ambiente ficar parado. Pare o assinante, espere e inicie-o de novo: o valor retido motion/last chega imediatamente. O servidor MQTT público é compartilhado e pode ser lido por todos, então serve só para testes e protótipos, não para uma casa ou escritório de verdade. Dados de movimento revelam quando um lugar está vazio e, ligados a pessoas identificáveis, são dados pessoais sob a LGPD.
Ajustando os tempos
As três constantes controlam o comportamento:
- COOLDOWN_MS limita a frequência com que
detectedpode disparar. Dez segundos servem para a luz de um corredor; para um log de segurança, você pode querer registrar cada nova presença após um intervalo curto. - CLEAR_MS decide quando o ambiente conta como vazio. Some a ele o tempo de retenção do próprio módulo: com um tempo de retenção de 5 segundos no HC-SR501 e CLEAR_MS de 30 segundos,
clearchega cerca de 35 segundos após o último movimento. - WARMUP_MS deve cobrir o tempo de estabilização do módulo. Sessenta segundos é seguro para o HC-SR501.
Gire o potenciômetro de tempo de retenção do HC-SR501 para o mínimo e deixe o firmware cuidar da temporização. A temporização por software é mais fácil de mudar remotamente e consistente entre módulos.
Reagindo aos eventos de movimento
O broker entrega os eventos; outra coisa decide o que fazer com eles. Consumidores típicos são o Home Assistant com um binary sensor MQTT, um fluxo do Node-RED que acende uma luz e envia uma notificação, ou uma câmera que captura um quadro quando há movimento; o projeto ESP32-CAM com MQTT combina naturalmente com este sensor. Cada consumidor assina motion como um cliente MQTT comum.
A mensagem will retida offline é igualmente importante em usos de segurança. Um sensor de movimento que morre em silêncio parece exatamente um ambiente vazio. O tutorial de Last Will MQTT no ESP32 explica como o broker a publica em seu nome.
Solução de problemas
- Disparos constantes. O sensor está perto demais da antena do ESP32 ou voltado para uma fonte de calor. Mude-o de lugar, reduza a sensibilidade e verifique a lógica de aquecimento.
- Nenhum disparo. Confira se o HC-SR501 está alimentado com 5 V, e não 3,3 V, e se o OUT vai ao pino configurado.
- Detectado, mas nunca encerrado. Modo de redisparo com movimento constante, ou tempo de retenção maior que o esperado. Reduza o potenciômetro de retenção.
- O ESP32 reinicia com movimento. Você está processando dentro do handler de interrupção. Limite-o a definir flags.
- Timestamp mostra unknown. O NTP ainda não sincronizou; libere a saída UDP 123.
Configuração para produção
Para qualquer local real, use um broker autenticado e criptografado. Crie um em app.connectmq.com, mude para WiFiClientSecure na porta 8883 com o certificado ISRG Root X1 e conecte com YOUR_CLIENT_ID, YOUR_USERNAME e YOUR_PASSWORD. No portal do ConnectMQ, você pode acompanhar o topic motion no explorador de topics ao vivo enquanto ajusta o sensor.
Se você usa vários sensores, dê a cada cômodo seu próprio nível de prefixo, como site/floor1/hallway/motion, em vez de um único topic plano. Assim, os consumidores podem assinar um cômodo ou usar um wildcard para o andar inteiro, e uma regra de alerta por limite na contagem de movimentos de um cômodo sensível oferece um aviso simples fora do horário sem código de backend extra.
Rode seus alertas de movimento no ConnectMQ
Alertas de movimento só são úteis se o caminho do sensor até o celular for confiável e privado. O ConnectMQ é um broker MQTT gerenciado que roda como broker MQTT na nuvem, com TLS, mensagens retidas, suporte a last will e um plano gratuito Serverless que não exige cartão de crédito. Conecte este nó, integre sua ferramenta de automação preferida e encontre mais projetos de dispositivos na coleção tutorial de IoT com ESP32.
Perguntas frequentes
Por que meu sensor PIR dispara logo após ser ligado?
Os módulos PIR precisam de até um minuto para se estabilizar e geram saídas aleatórias nesse período. Ignore os disparos em uma janela de aquecimento, como o sketch faz.
Eventos de movimento devem ser mensagens retidas?
Retenha o estado atual e o timestamp do último movimento, para que novos assinantes tenham contexto imediatamente. Não retenha notificações pontuais que devem disparar apenas uma vez.
Posso usar deep sleep com um sensor PIR?
Sim. Configure a saída do PIR como fonte de despertar ext0, conecte ao Wi-Fi após acordar, publique o evento e volte a dormir. Espere alguns segundos de atraso até a mensagem chegar.
Uma interrupção é realmente necessária?
Para um HC-SR501 com tempo de retenção longo, a leitura periódica basta. Módulos de pulso curto como o AM312 podem ser perdidos enquanto o código de rede roda, então a flag de interrupção torna a detecção confiável.



