Project Overview
This project creates an occupancy counter that tracks how many people are in a room. Using two PIR sensors placed at a doorway, the system detects direction of movement (entry vs. exit) and maintains a count of current occupants. The count is displayed on an LCD and can be sent to a server for occupancy monitoring.
Difficulty: Intermediate
Estimated time: 3-4 hours
Estimated cost: $30-40
How It Works
Two PIR sensors are placed at the top and bottom of a doorway (or left and right). When a person passes through, the order in which the sensors trigger indicates direction:
- Sensor A then Sensor B = entry (count +1)
- Sensor B then Sensor A = exit (count -1)
The system maintains a running count of people in the room, displayed on an LCD. An ESP32 can send occupancy data to a server or cloud platform.
Materials Needed
- Arduino Uno or ESP32 (1)
- HC-SR501 PIR sensors (2)
- LCD display (16×2 with I2C)
- Push buttons (for resetting count)
- LEDs (red and green for status)
- Resistors (220Ω for LEDs, 10k for buttons)
- Jumper wires
- Power supply (5V 1A)
- Project enclosure
Circuit Diagram
Connection Table
| Component | Pin | Arduino Pin |
|---|---|---|
| Sensor A (Entry) | VCC | 5V |
| Sensor A (Entry) | GND | GND |
| Sensor A (Entry) | OUT | Digital Pin 2 |
| Sensor B (Exit) | VCC | 5V |
| Sensor B (Exit) | GND | GND |
| Sensor B (Exit) | OUT | Digital Pin 3 |
| LCD (I2C) | VCC | 5V |
| LCD (I2C) | GND | GND |
| LCD (I2C) | SDA | A4 (SDA) |
| LCD (I2C) | SCL | A5 (SCL) |
| Reset Button | One pin | Digital Pin 4 (with 10k pull-up) |
| Reset Button | Other pin | GND |
| Green LED | Anode | Digital Pin 5 (through 220Ω) |
| Green LED | Cathode | GND |
| Red LED | Anode | Digital Pin 6 (through 220Ω) |
| Red LED | Cathode | GND |
Arduino Code
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
// Pin definitions
const int sensorA = 2; // Entry sensor (outside)
const int sensorB = 3; // Exit sensor (inside)
const int resetButton = 4;
const int ledGreen = 5;
const int ledRed = 6;
// LCD (16x2, I2C address 0x27)
LiquidCrystal_I2C lcd(0x27, 16, 2);
// Variables
int occupancy = 0;
bool sensorATriggered = false;
bool sensorBTriggered = false;
unsigned long sensorATime = 0;
unsigned long sensorBTime = 0;
unsigned long lastEventTime = 0;
const unsigned long eventWindow = 2000; // 2 seconds to pair triggers
const int maxOccupancy = 50; // Safety limit
void setup() {
Serial.begin(9600);
// Initialize pins
pinMode(sensorA, INPUT);
pinMode(sensorB, INPUT);
pinMode(resetButton, INPUT_PULLUP);
pinMode(ledGreen, OUTPUT);
pinMode(ledRed, OUTPUT);
// Initialize LCD
lcd.init();
lcd.backlight();
lcd.setCursor(0, 0);
lcd.print("Occupancy Counter");
lcd.setCursor(0, 1);
lcd.print("Initializing...");
Serial.println("Occupancy Counter Starting...");
Serial.println("Waiting 60 seconds for sensor warm-up...");
delay(60000);
updateDisplay();
Serial.println("System Ready");
}
void updateDisplay() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("People in room:");
lcd.setCursor(0, 1);
lcd.print(" ");
lcd.setCursor(5, 1);
lcd.print(occupancy);
// Update LEDs
if (occupancy > 0) {
digitalWrite(ledGreen, HIGH);
digitalWrite(ledRed, LOW);
} else {
digitalWrite(ledGreen, LOW);
digitalWrite(ledRed, HIGH);
}
Serial.print("Occupancy: ");
Serial.println(occupancy);
}
void processEntry() {
occupancy++;
if (occupancy > maxOccupancy) occupancy = maxOccupancy;
Serial.println("Entry detected (+1)");
updateDisplay();
}
void processExit() {
occupancy--;
if (occupancy < 0) occupancy = 0;
Serial.println("Exit detected (-1)");
updateDisplay();
}
void checkTriggers() {
unsigned long now = millis();
// Check sensor A (entry)
if (digitalRead(sensorA) == HIGH && !sensorATriggered) {
sensorATriggered = true;
sensorATime = now;
Serial.println("Sensor A triggered");
}
// Check sensor B (exit)
if (digitalRead(sensorB) == HIGH && !sensorBTriggered) {
sensorBTriggered = true;
sensorBTime = now;
Serial.println("Sensor B triggered");
}
// Determine direction if both triggered within window
if (sensorATriggered && sensorBTriggered) {
if (abs((long)(sensorATime - sensorBTime)) < eventWindow) {
if (sensorATime < sensorBTime) {
// A first, then B = entry
processEntry();
} else {
// B first, then A = exit
processExit();
}
}
// Reset trigger flags
sensorATriggered = false;
sensorBTriggered = false;
}
// Timeout: clear single triggers after window
if (sensorATriggered && (now - sensorATime > eventWindow)) {
sensorATriggered = false;
Serial.println("Sensor A timeout - ignored");
}
if (sensorBTriggered && (now - sensorBTime > eventWindow)) {
sensorBTriggered = false;
Serial.println("Sensor B timeout - ignored");
}
}
void loop() {
// Check reset button
if (digitalRead(resetButton) == LOW) {
delay(50);
if (digitalRead(resetButton) == LOW) {
occupancy = 0;
updateDisplay();
Serial.println("Occupancy reset to 0");
while (digitalRead(resetButton) == LOW) {
delay(10);
}
}
}
checkTriggers();
delay(50);
}
ESP32 Version with Wi-Fi Data Logging
For remote monitoring, use an ESP32 and send occupancy data to a server:
#include <WiFi.h>
#include <HTTPClient.h>
const char* ssid = "YourWiFiSSID";
const char* password = "YourWiFiPassword";
const char* serverURL = "http://yourserver.com/api/occupancy";
void sendOccupancyData() {
if (WiFi.status() == WL_CONNECTED) {
HTTPClient http;
http.begin(serverURL);
http.addHeader("Content-Type", "application/json");
String payload = "{\"occupancy\":" + String(occupancy) + "}";
int httpCode = http.POST(payload);
http.end();
}
}
// Call sendOccupancyData() after each occupancy change
Installation Steps
- Position sensors: Mount one sensor on each side of the doorway at 1.5-2m height. Point them slightly downward and toward the doorway center. Ensure fields overlap.
- Adjust sensor settings: Set both sensors to minimum hold time (5 seconds) and maximum sensitivity.
- Connect circuit: Assemble on breadboard and test sensor pairing.
- Upload code: Load code to Arduino and open Serial Monitor to debug.
- Calibrate: Walk through doorway several times to verify direction detection is accurate.
- Adjust event window: If people move quickly through the door, reduce
eventWindow. If slowly, increase. - Final mounting: Secure sensors and enclosure, route wires neatly.
Calibration Tips
- Sensor spacing: Place sensors 30-50cm apart for clear direction detection.
- Avoid overlapping fields: Ensure sensors trigger sequentially, not simultaneously.
- Test with groups: People walking together may be counted as one; adjust sensitivity to detect individuals.
- Test with children: Ensure sensors are positioned to detect children as well as adults.
Troubleshooting
- Direction detection inaccurate: Adjust sensor positions and event window. Ensure sensors are not triggered simultaneously.
- Multiple counts for one person: Reduce sensor hold time to minimum. Add debouncing in code.
- Missed counts: Increase sensor sensitivity. Ensure sensors cover the entire doorway width.
- Count drifting over time: Implement periodic reset or add a confirmation mechanism (e.g., door contact sensor).
- False triggers from pets: Use pet-immune lenses or mount sensors higher.
Project Extensions
- Door contact sensor: Add a magnetic reed switch to only count when the door is open, reducing false counts from motion near door.
- Real-time clock: Log occupancy by time of day to analyze peak usage patterns.
- Thermal printer: Add a thermal printer to print occupancy reports on demand.
- Traffic light indicator: Add a traffic light outside the room to indicate occupancy status (green = available, red = full).
- Email alerts: Send email when occupancy reaches maximum capacity.
- Integration with reservation system: Connect to booking platform to show real-time room availability.
Conclusion
This occupancy counter provides accurate, real-time room occupancy data. It’s ideal for meeting rooms, shared workspaces, and any area where capacity management is important. With Wi-Fi integration, occupancy data can be accessed remotely and used to optimize space utilization.
