ESP32 e Arduino

Alertas MQTT com Sensor de Movimento PIR no ESP32, com Debounce e Horário do Último Movimento

Detecte movimento com um PIR HC-SR501 ou AM312 no ESP32, filtre disparos ruidosos, publique eventos de movimento via MQTT e mantenha um timestamp retido do último movimento para dashboards e automações.

Módulo PIR HC-SR501 ligado a um ESP32 enviando alertas de movimento para a nuvem em um projeto de sensor de movimento PIR com 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 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:

  1. Movimento detectado quando a saída vai a nível alto e o evento anterior é mais antigo que um período de cooldown.
  2. 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 detected pode 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, clear chega 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

  1. 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.
  2. 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.
  3. 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.
  4. O ESP32 reinicia com movimento. Você está processando dentro do handler de interrupção. Limite-o a definir flags.
  5. 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.

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.