Un nodo con sensor de movimiento PIR, ESP32 y MQTT convierte un detector infrarrojo barato en una fuente de eventos a la que puede reaccionar cualquier dashboard o automatización. En este tutorial conectarás un HC-SR501 o un AM312, capturarás el movimiento con una interrupción, filtrarás los disparos ruidosos, publicarás eventos en broker.connectmq.com y conservarás una marca de tiempo retenida del último movimiento que indica a los nuevos suscriptores exactamente cuándo se movió algo por última vez.
Cómo se comportan los sensores PIR
Un sensor infrarrojo pasivo (PIR) detecta cambios en la radiación infrarroja dentro de su campo de visión, normalmente un cuerpo caliente en movimiento. El módulo lo convierte en una salida digital sencilla: alta mientras detecta movimiento y baja en caso contrario. Los detalles importan para MQTT, porque la salida no es una señal limpia de un pulso por evento.
| Módulo | Alimentación | Salida | Ajustable | Notas |
|---|---|---|---|---|
| HC-SR501 | 5 V (pin VIN) | 3,3 V en alto | Sensibilidad, tiempo de retención, jumper de modo de disparo | Lente grande, alcance de unos metros, necesita calentamiento |
| AM312 | 2,7 a 12 V | 3,3 V en alto | Nada | Diminuto, tiempo de retención fijo de un par de segundos, bueno para montajes a 3,3 V |
| HC-SR505 | 4,5 a 20 V | 3,3 V en alto | Nada | Compacto, tiempo de retención fijo de varios segundos |
Lo que debes saber antes de escribir código:
- Calentamiento. Tras el encendido, los módulos PIR generan disparos aleatorios durante hasta un minuto mientras el sensor se estabiliza. Ignora el movimiento durante ese periodo.
- Tiempo de retención. La salida permanece alta durante un tiempo configurado tras el último movimiento detectado. En el HC-SR501, un potenciómetro lo ajusta desde unos segundos hasta varios minutos.
- Modo de redisparo. Con el jumper del HC-SR501 en la posición "H" (repetición), el movimiento continuado mantiene la salida alta. En modo "L" la salida baja tras el tiempo de retención aunque el movimiento continúe y luego vuelve a dispararse. "H" suele ser lo que quieres.
- Falsos disparos. La luz solar, los calefactores, las rejillas de ventilación y las antenas Wi-Fi colocadas justo al lado del sensor pueden provocar pulsos espurios.
Conexiones
Para el HC-SR501: VCC al pin 5V (VIN) del ESP32, GND a GND y OUT a GPIO 27. La salida es de 3,3 V, así que se conecta directamente a la entrada del ESP32. Para el AM312: VCC a 3V3, GND a GND y OUT a GPIO 27. Mantén el sensor algo alejado de la antena del ESP32; colocarlo muy cerca es una causa conocida de disparos fantasma.
Diseño de eventos: antirrebote, enfriamiento y topics
Publicar cada flanco de la salida del PIR satura a los suscriptores y malgasta el límite por cliente del servidor MQTT público gratuito. El sketch convierte la señal bruta en dos estados limpios:
- Movimiento detectado cuando la salida pasa a alto y el evento anterior es más antiguo que un periodo de enfriamiento.
- Movimiento despejado cuando la salida lleva en bajo un periodo de calma.
| Topic | Retenido | Payload |
|---|---|---|
connectmq/demo/pir9d4k/motion |
Sí | {"state":"detected","ts":"2026-10-16T18:03:11Z","count":14} |
connectmq/demo/pir9d4k/motion/last |
Sí | 2026-10-16T18:03:11Z |
connectmq/demo/pir9d4k/status |
Sí | online / offline |
El topic motion/last es la idea clave: cualquier cliente que se suscriba, aunque sea horas después, recibe al instante la hora de la última detección. Las marcas de tiempo vienen de NTP; la guía de timestamp NTP en MQTT con ESP32 explica los detalles de sincronización horaria que se usan aquí.
El sketch del sensor de movimiento PIR con ESP32 y 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"; // usa tu propio id aleatorio
const uint8_t PIR_PIN = 27;
const uint32_t WARMUP_MS = 60000; // ignora disparos tras el encendido
const uint32_t COOLDOWN_MS = 10000; // tiempo mínimo entre eventos "detected"
const uint32_t CLEAR_MS = 30000; // salida baja durante este tiempo -> "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; // aún sin sincronizar
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 el ruido del calentamiento
edgeSeen = false;
return;
}
// Flanco de subida (capturado por la interrupción) o salida aún alta -> movimiento
if (edgeSeen || pinHigh) {
edgeSeen = false;
if (!motionActive && now - lastDetectedMs >= COOLDOWN_MS) {
motionActive = true;
lastDetectedMs = now;
motionCount++;
publishState("detected");
}
}
// Salida baja el tiempo suficiente -> despejado
if (motionActive && !pinHigh && now - lastHighMs >= CLEAR_MS) {
motionActive = false;
publishState("clear");
}
}
La rutina de servicio de interrupción solo activa una bandera y guarda una hora. Nunca publiques desde dentro de una interrupción: el código de red no es seguro en interrupciones y el ESP32 se bloqueará. La bandera garantiza que ni siquiera un pulso muy corto de un AM312 se pierda mientras mqtt.loop() está ocupado.
Probar los eventos del sensor de movimiento PIR con ESP32 y MQTT
Suscríbete a tu prefijo y mueve la mano delante del sensor tras el minuto de calentamiento:
mosquitto_sub -h broker.connectmq.com -p 1883 -i pir-view-9d4k -t "connectmq/demo/pir9d4k/#" -v
Verás un estado detected y luego clear unos 30 segundos después de que la habitación quede en calma. Detén el suscriptor, espera y vuelve a iniciarlo: el valor retenido de motion/last llega de inmediato. El área pública es compartida y cualquiera puede leerla, así que no la uses para una vivienda u oficina reales. Los datos de movimiento revelan cuándo un lugar está vacío. Consulta la página del servidor público para conocer su uso previsto: pruebas y prototipos, no datos de producción.
Ajustar la temporización
Tres constantes controlan el comportamiento:
- COOLDOWN_MS limita la frecuencia con la que puede dispararse
detected. Diez segundos van bien para la luz de un pasillo; para un registro de seguridad quizá quieras cada nueva presencia tras una breve pausa. - CLEAR_MS decide cuándo se considera vacía la habitación. Súmale el tiempo de retención del propio módulo: con un tiempo de retención del HC-SR501 de 5 segundos y un CLEAR_MS de 30 segundos,
clearllega unos 35 segundos después del último movimiento. - WARMUP_MS debe cubrir el tiempo de estabilización del módulo. Sesenta segundos es un valor seguro para el HC-SR501.
Gira el potenciómetro de tiempo de retención del HC-SR501 al mínimo y deja que el firmware se encargue de la temporización. La temporización por software es más fácil de cambiar en remoto y coherente entre módulos.
Reaccionar a los eventos de movimiento
El broker entrega los eventos; otra cosa decide qué hacer con ellos. Los consumidores típicos son Home Assistant con un sensor binario MQTT, un flujo de Node-RED que enciende una luz y envía una notificación, o una cámara que captura una imagen cuando se detecta movimiento; el proyecto ESP32-CAM con MQTT combina de forma natural con este sensor. Cada consumidor se suscribe a motion como un cliente MQTT normal.
El mensaje will retenido offline es igual de importante en usos de seguridad. Un sensor de movimiento que muere en silencio se parece exactamente a una habitación tranquila. El tutorial de Last Will MQTT en ESP32 explica cómo el broker lo publica en tu nombre.
Solución de problemas
- Disparos constantes. El sensor está demasiado cerca de la antena del ESP32 o mira hacia una fuente de calor. Muévelo, baja la sensibilidad y revisa la lógica de calentamiento.
- Ningún disparo. Comprueba que el HC-SR501 se alimenta a 5 V, no a 3,3 V, y que OUT va al pin configurado.
- Detectado pero nunca despejado. Modo de redisparo con movimiento constante, o un tiempo de retención mayor de lo esperado. Reduce el potenciómetro de retención.
- El ESP32 se reinicia con el movimiento. Estás haciendo trabajo en el gestor de interrupciones. Limítalo a activar banderas.
- La marca de tiempo muestra unknown. NTP aún no se ha sincronizado; permite el UDP 123 saliente.
Configuración para producción
Para cualquier instalación real, usa un broker autenticado y cifrado. Crea uno en app.connectmq.com, cambia a WiFiClientSecure en el puerto 8883 con el certificado ISRG Root X1 y conéctate con YOUR_CLIENT_ID, YOUR_USERNAME y YOUR_PASSWORD. En el portal de ConnectMQ puedes observar el topic motion en el explorador de topics en vivo mientras ajustas el sensor.
Si tienes varios sensores, da a cada estancia su propio nivel de prefijo, como site/floor1/hallway/motion, en lugar de un único topic plano. Así los consumidores pueden suscribirse a una estancia o usar un comodín para toda la planta, y una regla de alerta por umbral sobre el recuento de movimientos de una estancia sensible te da un aviso sencillo fuera del horario laboral sin código de backend adicional.
Gestiona tus alertas de movimiento con ConnectMQ
Las alertas de movimiento solo son útiles si el camino del sensor al móvil es fiable y privado. ConnectMQ es un broker MQTT gestionado en la nube con TLS, retained messages, soporte de last will y un plan gratuito Serverless que no requiere tarjeta de crédito. Conecta este nodo, enlaza la herramienta de automatización que prefieras y encuentra más proyectos con dispositivos en la colección del tutorial de IoT con ESP32.
Preguntas frecuentes
¿Por qué mi sensor PIR se dispara justo después del encendido?
Los módulos PIR necesitan hasta un minuto para estabilizarse y generan una salida aleatoria durante ese tiempo. Ignora los disparos en una ventana de calentamiento, como hace el sketch.
¿Deben ser retained messages los eventos de movimiento?
Retén el estado actual y la marca de tiempo del último movimiento para que los nuevos suscriptores tengan contexto de inmediato. No retengas notificaciones puntuales que solo deban dispararse una vez.
¿Puedo usar deep sleep con un sensor PIR?
Sí. Configura la salida del PIR como fuente de despertar ext0, conéctate al Wi-Fi tras despertar, publica el evento y vuelve a dormir. Cuenta con unos segundos de retraso antes de que llegue el mensaje.
¿Es realmente necesaria una interrupción?
Para un HC-SR501 con un tiempo de retención largo, basta con sondear el pin. Los módulos de pulso corto como el AM312 pueden pasar desapercibidos mientras se ejecuta el código de red, así que la bandera de interrupción hace fiable la detección.



