Introduction
Motion sensors are all around us, quietly keeping spaces secure and responsive without us even noticing. From automatic hallway lights that switch on as you walk by, to alarm systems that detect unexpected movement, to motion-activated security camera networks - PIR (Passive Infrared) sensors play a major role in everyday automation and security. Once found mostly in commercial products, as DIY electronics projects became more capable, components like PIR sensors are now available as simple plug-and-play modules available to anyone.
This article will walk you through what PIR sensors are, how do they detect motion, and finally how you can set your microcontroller to perform a certain task such as turning on a light or sending a notification based on whether motion was detected.
How does it work?
A Passive Infrared sensor is an electronic device that detects a moving object as it enters its field of view, causing a change in infrared levels. All objects and people that radiate heat also give off a low level of infrared radiation, relative to their temperature. The hotter the object, the more radiation it emits.
These devices are centered around two balanced strips of pyroelectric material which are placed parallel to one another, thus creating a pyroelectric sensor. This detects thermal energy in the surrounding area. In addition, an infrared filter is also included to block out all other light wavelengths. So, once a change to the signal differential between two pyroelectric elements is detected, the PIR sensor then triggers and sends a signal.
Delay time
One important concept about PIR motion sensors is the dwell time (sometimes called “delay time”), which is the amount of time the sensor’s output stays HIGH after detecting motion. In more simpler terms, it’s how long the sensor keeps reporting “motion detected” before returning to LOW.

Some PIR sensors models can have a small onboard potentiometer that lets you adjust the sensor delay. This is important because it helps avoid false triggers or rapid on/off cycling, it is also useful in lightning or security systems where you would want a stable “motion detected” signal.

Detecting movement
After understanding the basics, we’ll create a simple example that will print out a message to the serial monitor when motion is detected. Our PIR movement sensor board comes in two variations:
-
Standard version - uses DOUT and SOUT pins for output
-
I2C version - uses Qwiicy/easyC connector for easy connection (default address 0x30)
Example: Simple motion detection with delayed output
#define SOUT_PIN 2
#define DOUT_PIN 4
// Variables that hold the time since the last measurement for specific pins
unsigned long dout_delay = 0;
unsigned long sout_delay = 0;
void setup()
{
// Set the pins as input
pinMode(DOUT_PIN, INPUT);
pinMode(SOUT_PIN, INPUT);
// Initialize serial communication
Serial.begin(115200);
Serial.println("Warming up sensor...");
// Wait 20s so the sensor can warm up
delay(20000);
Serial.println("Warmup done!");
}
void loop()
{
// If the pin is set to HIGH and at least 4s have passed since the last measurement, print detection
if (digitalRead(DOUT_PIN) && millis() - dout_delay >= 4000)
{
Serial.println("DOUT Motion detected!");
dout_delay = millis();
}
// If the pin is set to HIGH and at least 4s have passed since the last measurement, print detection
if (digitalRead(SOUT_PIN) && millis() - sout_delay >= 4000)
{
Serial.println("SOUT Motion detected!");
sout_delay = millis();
}
}
|

As you can see from the serial monitor output, the DOUT pin passes the 4 second delay threshold which we set, this is because the pin gives a HIGH (“motion detected”) reading for an additional 2 seconds when the onboard potentiometer is set to the highest delay (counter-clockwise). Adjusting this value can be useful for projects when creating a motion detector that is not too sensitive to small movements.
PIR WiFi Alert project
Components used:
- ESP32
- PIR sensor module
- LED
- 330 Ohm resistor
This project uses WiFi capabilities of ESP32 to send an email alert notification when movement was detected, the email contains date and time the movement was detected. Additionally, notifications can be turned ON/OFF from a web server built from ESP32. To access it, first make sure you are connected on the same network as ESP, then just type in the IP address printed on the serial monitor in your web browser URL.
#include "web_ui.h"
#include "time.h"
#include <ESP_Mail_Client.h>
// PIR sensor pins
#define SOUT_PIN 2
#define DOUT_PIN 4
/* The smtp host name (smtp.gmail.com for GMail) and SMTP SSL port */
#define SMTP_HOST "smtp.gmail.com"
#define SMTP_PORT 465
/* The sign in credentials */
#define AUTHOR_EMAIL ""
#define AUTHOR_PASSWORD ""
/* Recipient's email */
#define RECIPIENT_EMAIL ""
SMTPSession smtp;
/* Set the session config */
Session_Config config;
/* Initialize web server object */
WebServer server(80);
// ISR Flag for motion detection
volatile bool motionDetected = false;
unsigned long lastHandledMS = 0;
// Network details
const char* SSID = "";
const char* PASSWORD = "";
// NTP server setup
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 3600;
const int daylightOffset_sec = 3600;
// Variable to keep track of notification state
bool notificationsEnabled = true;
// Local time keeping structure
struct tm timeInfo;
// Function declarations
void HandleRoot();
void UpdateNotificationStatus();
void updateTime();
void IRAM_ATTR onPIRDetection();
void smtpCallback(SMTP_Status status);
bool ensureSmtp();
void sendMotionNotification(const String &textMsg);
void setup()
{
Serial.begin(115200);
// Set PIR pins as input
pinMode(DOUT_PIN, INPUT);
pinMode(SOUT_PIN, INPUT);
attachInterrupt(digitalPinToInterrupt(SOUT_PIN), onPIRDetection, RISING);
// PIR Sensor warmup
Serial.println("Warming up sensor...");
delay(20000);
Serial.println("Warmup done!");
// Connect to specified WiFi network
connect_to_wifi(SSID, PASSWORD);
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
// Set up the web server to handle different routes and then start it
server.on("/", HandleRoot);
server.on("/toggle/off", UpdateNotificationStatus);
server.on("/toggle/on", UpdateNotificationStatus);
server.begin();
Serial.println("HTTP server started");
MailClient.networkReconnect(true);
smtp.debug(1);
/* Set the callback function to get the sending results */
smtp.callback(smtpCallback);
// Config parameters
config.server.host_name = SMTP_HOST;
config.server.port = SMTP_PORT;
config.login.email = AUTHOR_EMAIL;
config.login.password = AUTHOR_PASSWORD;
config.login.user_domain = "";
config.time.ntp_server = F("pool.ntp.org");
config.time.gmt_offset = 1;
config.time.day_light_offset = 0;
}
void loop()
{
// Handle incoming client requests
server.handleClient();
// If motion is detected and at least 4 seconds have passed from last detection, update time and send email notification
if (motionDetected)
{
noInterrupts();
motionDetected = false;
interrupts();
const unsigned long now = millis();
if (now - lastHandledMS >= 4000)
{
lastHandledMS = now;
Serial.println("MOTION DETECTED");
updateTime();
print_last_detection_time();
// If notifications are enabled send alert email
if (notificationsEnabled)
{
sendMotionNotification("Movement detected on " + print_last_detection_time() + "!!");
}
}
}
}
// Main page handler
void HandleRoot()
{
server.send(200, "text/html", ui_HandleRoot(notificationsEnabled));
}
// Set new notification status
void UpdateNotificationStatus()
{
notificationsEnabled = !notificationsEnabled;
if (notificationsEnabled) { Serial.println("Notifications: ENABLED"); }
else { Serial.println("Notifications: DISABLED"); }
HandleRoot();
}
// Update global timeInfo variable
void updateTime()
{
if (!getLocalTime(&timeInfo))
{
Serial.println("Failed to obtain time");
return;
}
}
void IRAM_ATTR onPIRDetection()
{
motionDetected = true;
}
void smtpCallback(SMTP_Status status)
{
/* Print the current status */
Serial.println(status.info());
/* Print the sending result */
if (status.success())
{
Serial.println("----------------");
ESP_MAIL_PRINTF("Message sent success: %d\n", status.completedCount());
ESP_MAIL_PRINTF("Message sent failed: %d\n", status.failedCount());
Serial.println("----------------\n");
for (size_t i = 0; i < smtp.sendingResult.size(); i++)
{
/* Get the result item */
SMTP_Result result = smtp.sendingResult.getItem(i);
ESP_MAIL_PRINTF("--- Message Info ---\n");
ESP_MAIL_PRINTF("Status: %s\n", result.completed ? "success" : "failed");
ESP_MAIL_PRINTF(
"Date/Time: %s\n",
MailClient.Time.getDateTimeString(result.timestamp, "%B %d, %Y %H:%M:%S").c_str()
);
ESP_MAIL_PRINTF("Recipient: %s\n", result.recipients.c_str());
ESP_MAIL_PRINTF("Subject: %s\n", result.subject.c_str());
}
Serial.println("----------------\n");
// Clear sending result as the memory usage will grow up.
smtp.sendingResult.clear();
}
}
bool ensureSmtp()
{
/* Connect to the server */
if (!smtp.connect(&config))
{
ESP_MAIL_PRINTF(
"Connection error, Status Code: %d, Error Code: %d, Reason: %s",
smtp.statusCode(), smtp.errorCode(), smtp.errorReason().c_str()
);
return false;
}
if (!smtp.isLoggedIn())
{
Serial.println("\nNot yet logged in.");
}
else
{
if (smtp.isAuthenticated())
{
Serial.println("\nSuccessfully logged in.");
return true;
}
else
{
Serial.println("\nConnected with no Auth.");
return false;
}
}
return false;
}
void sendMotionNotification(const String &textMsg)
{
/* Declare the message class */
SMTP_Message message;
/* Set the message headers */
message.sender.name = F("ESP");
message.sender.email = AUTHOR_EMAIL;
message.subject = F("Movement Detection");
message.addRecipient(F("USER"), RECIPIENT_EMAIL);
// Send raw text message
message.text.content = textMsg.c_str();
message.text.charSet = "us-ascii";
message.text.transfer_encoding = Content_Transfer_Encoding::enc_7bit;
message.priority = esp_mail_smtp_priority::esp_mail_smtp_priority_low;
message.response.notify = esp_mail_smtp_notify_success |
esp_mail_smtp_notify_failure |
esp_mail_smtp_notify_delay;
if (ensureSmtp())
{
/* Start sending Email and close the session */
if (!MailClient.sendMail(&smtp, &message))
{
ESP_MAIL_PRINTF(
"Error, Status Code: %d, Error Code: %d, Reason: %s",
smtp.statusCode(), smtp.errorCode(), smtp.errorReason().c_str()
);
}
}
}
|
The main loop logic first checks whether any device has made an HTTP request to the ESP32 board. Then, if motion was detected and at least 4 seconds have passed since last time motion was handled, it updates the timestamp, prints a corresponding message and sends a motion-alert email if notifications are enabled.
Web page + email notification:

Successful message output:

You can find full code example for this project on
GitHub.