ESP32 and Arduino

ESP32 PIR Motion Sensor MQTT Alerts with Debounce and Last-Motion Time

Detect motion with an HC-SR501 or AM312 PIR on an ESP32, debounce noisy triggers, publish motion events over MQTT and keep a retained last-motion timestamp for dashboards and automations.

HC-SR501 PIR module wired to an ESP32 sending motion alerts to the cloud in an esp32 pir motion sensor mqtt project
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

An ESP32 PIR motion sensor MQTT node turns a cheap infrared detector into an event source any dashboard or automation can react to. In this tutorial you will wire an HC-SR501 or AM312, catch motion with an interrupt, debounce noisy triggers, publish events to broker.connectmq.com, and keep a retained last-motion timestamp that tells new subscribers exactly when something last moved.

How PIR Sensors Behave

A passive infrared (PIR) sensor detects changes in infrared radiation across its field of view, typically a warm body moving. The module turns that into a simple digital output: high while motion is detected, low otherwise. The details matter for MQTT, because the output is not a clean one-pulse-per-event signal.

Module Supply Output Adjustable Notes
HC-SR501 5 V (VIN pin) 3.3 V high Sensitivity, hold time, trigger mode jumper Large lens, a few metres range, needs warm-up
AM312 2.7 to 12 V 3.3 V high None Tiny, fixed hold time of a couple of seconds, good for 3.3 V builds
HC-SR505 4.5 to 20 V 3.3 V high None Compact, fixed hold time of several seconds

Things to know before writing code:

  • Warm-up. After power-on, PIR modules output random triggers for up to a minute while the sensor stabilises. Ignore motion during that window.
  • Hold time. The output stays high for a configured time after the last detected movement. On the HC-SR501 a potentiometer sets it from a few seconds to several minutes.
  • Retrigger mode. With the HC-SR501 jumper in the "H" (repeat) position, continued movement keeps the output high. In "L" mode the output drops after the hold time even if movement continues, then fires again. "H" is usually what you want.
  • False triggers. Sunlight, heaters, air vents and Wi-Fi antennas placed right next to the sensor can all cause spurious pulses.

Wiring

For the HC-SR501: VCC to the ESP32 5V (VIN) pin, GND to GND, OUT to GPIO 27. The output is 3.3 V, so it connects directly to the ESP32 input. For the AM312: VCC to 3V3, GND to GND, OUT to GPIO 27. Keep the sensor a little away from the ESP32 antenna; close placement is a known source of phantom triggers.

Event Design: Debounce, Cooldown and Topics

Publishing every edge of the PIR output floods subscribers and wastes the per-client rate on the public broker. The sketch turns the raw signal into two clean states:

  1. Motion detected when the output goes high and the previous event is older than a cooldown period.
  2. Motion clear when the output has been low for a quiet period.
Topic Retained Payload
connectmq/demo/pir9d4k/motion Yes {"state":"detected","ts":"2026-10-16T18:03:11Z","count":14}
connectmq/demo/pir9d4k/motion/last Yes 2026-10-16T18:03:11Z
connectmq/demo/pir9d4k/status Yes online / offline

The motion/last topic is the key idea: any client that subscribes, even hours later, instantly receives the time of the last detection. The timestamps come from NTP; the ESP32 NTP MQTT timestamp guide explains the time-sync details used here.

The ESP32 PIR Motion Sensor MQTT Sketch

#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 your own random id

const uint8_t  PIR_PIN      = 27;
const uint32_t WARMUP_MS    = 60000;   // ignore triggers after power-on
const uint32_t COOLDOWN_MS  = 10000;   // min time between "detected" events
const uint32_t CLEAR_MS     = 30000;   // output low this long -> "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;              // not synced yet
  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) {               // discard warm-up noise
    edgeSeen = false;
    return;
  }

  // Rising edge (caught by the interrupt) or output still high -> motion
  if (edgeSeen || pinHigh) {
    edgeSeen = false;
    if (!motionActive && now - lastDetectedMs >= COOLDOWN_MS) {
      motionActive = true;
      lastDetectedMs = now;
      motionCount++;
      publishState("detected");
    }
  }

  // Output low long enough -> clear
  if (motionActive && !pinHigh && now - lastHighMs >= CLEAR_MS) {
    motionActive = false;
    publishState("clear");
  }
}

The interrupt service routine only sets a flag and a time. Never publish from inside an interrupt: networking code is not interrupt-safe and the ESP32 will crash. The flag guarantees that even a very short pulse from an AM312 is not missed while mqtt.loop() is busy.

Testing ESP32 PIR Motion Sensor MQTT Events

Subscribe to your prefix and wave a hand in front of the sensor after the one-minute warm-up:

mosquitto_sub -h broker.connectmq.com -p 1883 -i pir-view-9d4k -t "connectmq/demo/pir9d4k/#" -v

You see a detected state, then clear about 30 seconds after the room goes quiet. Stop the subscriber, wait, and start it again: the retained motion/last value arrives immediately. The public area is shared and readable by everyone, so do not use it for a real home or office. Motion data reveals when a place is empty. See the public MQTT test broker page for its intended use.

Tuning the Timing

The three constants control behaviour:

  • COOLDOWN_MS limits how often detected can fire. Ten seconds suits a hallway light; for a security log you may want every new presence after a short gap.
  • CLEAR_MS decides when the room counts as empty. Add the module's own hold time to it: with an HC-SR501 hold time of 5 seconds and CLEAR_MS of 30 seconds, clear arrives about 35 seconds after the last movement.
  • WARMUP_MS should cover the module's settle time. Sixty seconds is safe for the HC-SR501.

Turn the HC-SR501 hold-time potentiometer to its minimum and let the firmware do the timing. Software timing is easier to change remotely and consistent across modules.

Reacting to Motion Events

The broker delivers the events; something else decides what to do with them. Typical consumers are Home Assistant with an MQTT binary sensor, a Node-RED flow that switches a light and sends a notification, or a camera that captures a frame when motion is detected; the ESP32-CAM MQTT project pairs naturally with this sensor. Each consumer subscribes to motion as an ordinary MQTT client.

The retained offline will message is equally important for security uses. A motion sensor that silently dies looks exactly like a quiet room. The ESP32 MQTT last will tutorial explains how the broker publishes it on your behalf.

Troubleshooting

  1. Constant triggers. The sensor is too close to the ESP32 antenna or facing a heat source. Move it, lower sensitivity, and check the warm-up logic.
  2. No triggers at all. Check that the HC-SR501 is powered from 5 V, not 3.3 V, and that OUT goes to the configured pin.
  3. Detected but never clear. Retrigger mode with constant movement, or a hold time longer than expected. Reduce the hold potentiometer.
  4. ESP32 resets on motion. You are doing work in the interrupt handler. Keep it to setting flags.
  5. Timestamp shows unknown. NTP has not synced yet; allow outbound UDP 123.

Production Setup

For any real premises, use an authenticated, encrypted broker. Create one at app.connectmq.com, switch to WiFiClientSecure on port 8883 with the ISRG Root X1 certificate, and connect with YOUR_CLIENT_ID, YOUR_USERNAME and YOUR_PASSWORD. In the ConnectMQ portal you can watch the motion topic in the live topic explorer while you tune the sensor.

If you run several sensors, give each room its own prefix level, such as site/floor1/hallway/motion, rather than one flat topic. Consumers can then subscribe to one room or use a wildcard for the whole floor, and a threshold alert rule on the motion count of a sensitive room gives you a simple out-of-hours warning without extra backend code.

Run Your Motion Alerts on ConnectMQ

Motion alerts are only useful if the path from sensor to phone is reliable and private. ConnectMQ is a cloud MQTT broker with TLS, retained messages, last will support and a free Serverless plan that needs no credit card. Connect this node, wire up your automation tool of choice, and find more device projects in the ESP32 IoT tutorial collection.

Frequently Asked Questions

Why does my PIR sensor trigger right after power-on?

PIR modules need up to a minute to stabilise and produce random output during that time. Ignore triggers in a warm-up window, as the sketch does.

Should motion events be retained messages?

Retain the current state and the last-motion timestamp so new subscribers get context immediately. Do not retain one-off notifications that should only fire once.

Can I use deep sleep with a PIR sensor?

Yes. Configure the PIR output as an ext0 wake source, connect to Wi-Fi after waking, publish the event and go back to sleep. Expect a few seconds of delay before the message arrives.

Is an interrupt really necessary?

For an HC-SR501 with a long hold time, polling is enough. Short-pulse modules like the AM312 can be missed while the network code runs, so the interrupt flag makes detection reliable.

Related articles

ESP-IDF MQTT Example: Build an esp-mqtt Client from Scratch

Build an ESP-IDF project from an empty folder with the esp-mqtt component: Wi-Fi station setup, the MQTT event handler, publish and subscribe, MQTT 5 properties and TLS with the certificate bundle.

8 min read

Using esp-mqtt MQTT 5 Features on ESP32 with ESP-IDF

A hands-on ESP-IDF project that enables MQTT 5.0 in esp-mqtt and uses user properties, message expiry, topic aliases, session expiry and response topics against a real MQTT 5 broker.

7 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.