A real-time system that tracks how many people are on each floor of the library using ESP32 microcontrollers, IR break-beam sensors, and a Next.js + Firebase web dashboard.
Each floor has one Main Board and optionally one or more Support Boards. Each board reads two IR break-beam sensors mounted in a doorway to detect whether someone is entering or exiting based on which beam breaks first. Support Boards send their counts to the Main Board every 10 seconds via ESP-NOW (a peer-to-peer WiFi protocol that doesn't require a router). The Main Board aggregates all counts and writes the floor occupancy to Firebase Realtime Database every 10 seconds over HTTPS. The Next.js frontend subscribes to Firebase with an onValue() listener and updates the display the instant new data arrives.
IR Sensors β Support Board ββESP-NOWβββ Main Board ββHTTPSβββ Firebase ββonValue()βββ Frontend
IR Sensors β Main Board βββββββββββββββββββββββββββββββββββββββββββββββ
USF Library Tracker/
βββ esp32/
β βββ config/
β β βββ Config.h # All credentials and settings β edit this before flashing
β βββ Get_MAC_Address/
β β βββ Get_MAC_Address.ino # One-time utility to read ESP32 MAC address
β βββ Main_Board/
β β βββ Main_Board.ino # Flash to the initiator board on each floor
β β βββ Config.h # Copy of Config.h
β βββ Support_Board/
β βββ Support_Board.ino # Flash to all other boards on the same floor
β βββ Config.h # Copy of Config.h
βββ library-occupancy-tracker/ # Next.js web app
βββ src/
β βββ app/
β β βββ page.tsx # Frontend dashboard
β β βββ layout.tsx
β β βββ api/
β β βββ update-floor/
β β βββ route.ts # Optional API route (not used in current build)
β βββ lib/
β β βββ firebase.ts # Firebase initialization
β βββ components/
β βββ FloorCard.tsx # Per-floor occupancy card
βββ .env.local # Firebase credentials for Next.js (never commit)
floors/
Floor_1/
current_occupancy: 42
max_capacity: 100
total_entries: 310
total_exits: 268
last_updated: 823042 β ms since board boot
Floor_2/
...
current_occupancy is clamped between 0 and max_capacity. total_entries and total_exits are cumulative since last board boot and are useful for detecting sensor drift over time.
- 2Γ IR break-beam sensor pairs (emitter + receiver)
- 2x 1k Ohm resistors (for pull up resistor)
- 1Γ ESP32 development board
- Jumper wires
| Sensor | ESP32 Pin | Notes |
|---|---|---|
| Sensor A (corridor side) | GPIO 18 | INPUT_PULLUP β LOW when beam broken |
| Sensor B (floor side) | GPIO 19 | INPUT_PULLUP β LOW when beam broken |
Mount sensors in sequence across the doorway. Sensor A must face the corridor (outside), Sensor B must face the floor interior (inside). The order of which beam breaks first determines direction:
- A breaks first β entry
- B breaks first β exit
All settings live in Config.h. Copy this file into each sketch folder before flashing.
// Config.h
// Shared β both Main Board and Support Board
#define FLOOR_ID "Floor_1" // Must match Firebase path key exactly (case-sensitive)
#define MAX_CAPACITY 120 // Max occupancy for this floor
#define WIFI_CHANNEL 1 // Must match your router's channel β see Setup below
// MAC address of the Main Board for this floor
// Get this by flashing Get_MAC_Address.ino and reading Serial output
uint8_t MAIN_BOARD_MAC[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
// Main Board only
#define WIFI_SSID "your_network"
#define WIFI_PASSWORD "your_password"
#define FIREBASE_HOST "your-project-default-rtdb.firebaseio.com"
#define FIREBASE_AUTH "your_database_secret"Firebase Console β Project Settings β Service Accounts β Database secrets β Show or Add secret. This is a long random string, not the Web API Key (AIzaSy...).
Flash Main_Board.ino first. Check Serial Monitor β it will print [WiFi] Channel: X. Put that number in WIFI_CHANNEL in Config.h before flashing Support Boards.
Arduino IDE with the ESP32 board package installed is required.
Install ESP32 boards: File β Preferences β addhttps://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.jsonto Additional Board Manager URLs, then Tools β Board β Boards Manager β search "esp32" by Espressif β Install.
- ArduinoJson by Benoit Blanchon
- Open
Get_MAC_Address/Get_MAC_Address.ino - Select board: Tools β Board β ESP32 Arduino β ESP32 Dev Module
- Select port: Tools β Port β (your ESP32's COM port)
- Upload β open Serial Monitor at 115200 baud
- Record the MAC address printed β this is your Main Board's MAC
- Repeat for any other boards if needed
Fill in all values in Config.h:
- Paste the Main Board MAC into
MAIN_BOARD_MAC - Add your WiFi credentials
- Add your Firebase host and auth secret
- Leave
WIFI_CHANNELas1for now (you'll confirm it after first flash)
Copy Config.h into both Main_Board/ and Support_Board/ folders.
- Open
Main_Board/Main_Board.ino - Select the Main Board's COM port
- Upload
- Open Serial Monitor at 115200 baud
- Note the channel printed:
[WiFi] Channel: X - Update
WIFI_CHANNELinConfig.hif it differs from what you set
- Update
WIFI_CHANNELinConfig.hto match what Main Board reported - Copy updated
Config.hintoSupport_Board/ - Open
Support_Board/Support_Board.ino - Select the Support Board's COM port
- Upload
Main Board Serial Monitor should show:
[WiFi] Connected. IP: ...
[ESP-NOW] Ready. Channel=1
[Boot] max_capacity write HTTP 200
[Boot] Main Board ready.
[ESP-NOW] Received β entries: 0 exits: 0 β Support Board packets arriving
[Firebase] Write successful.
Support Board Serial Monitor should show:
[Boot] Support Board ready. Channel=1
[ESP-NOW] Packet queued. entries=0 exits=0
Note:
onDataSentmay report FAILED on the Support Board even when data arrives correctly. This is a known ESP32 quirk β when the Main Board is connected to WiFi, it briefly hops channels for beacon frames, causing the ACK to be missed. As long as the Main Board shows[ESP-NOW] Received, the system is working correctly.
For development, set your rules to open in Firebase Console β Realtime Database β Rules:
{
"rules": {
".read": true,
".write": true
}
}Before going live in the library, tighten these rules to only allow writes authenticated with your database secret.
cd library-occupancy-tracker
npm installCreate .env.local in the project root:
NEXT_PUBLIC_FIREBASE_API_KEY=
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=
NEXT_PUBLIC_FIREBASE_DATABASE_URL=
NEXT_PUBLIC_FIREBASE_PROJECT_ID=All values are found in Firebase Console β Project Settings β General β Your apps β Firebase SDK snippet.
npm run devApp runs at http://localhost:3000. The dashboard reads from Firebase in real time using onValue() β no polling needed. The display updates the instant the Main Board writes new data.
Each floor card shows:
- Current occupancy percentage (
current_occupancy / max_capacity) - Last updated timestamp
| Setting | File | Variable | Default |
|---|---|---|---|
| How often Main Board writes to Firebase | Main_Board.ino |
SEND_INTERVAL_MS |
10000 (10s) |
| How often Support Board sends to Main Board | Support_Board.ino |
SEND_INTERVAL_MS |
10000 (10s) |
| Sensor direction debounce window | Both | DEBOUNCE_WINDOW_MS |
500 (500ms) |
Keep Support Board interval shorter than Main Board interval so the Main Board always has fresh aggregated data before it writes to Firebase.
- Get MAC address of the new floor's Main Board via
Get_MAC_Address.ino - Create a new
Config.hwith the newFLOOR_ID(e.g."Floor_2") and the new board's MAC - Flash
Main_Board.inoto the initiator board andSupport_Board.inoto any responder boards - Firebase will auto-create the
floors/Floor_2/node on first boot - Add the new floor to your frontend β add
"Floor_2"wherever your floor list is defined
last_updatedstores milliseconds since board boot, not a real timestamp. For real timestamps, NTP time sync would need to be added toMain_Board.ino.- Occupancy resets to 0 on board reboot. A daily scheduled reset (Firebase Cloud Function or Next.js cron) is recommended for production to prevent drift accumulating across days.
- If the library has more than one entrance per floor, one sensor pair per entrance is needed, each on its own board.
setInsecure()is used for HTTPS β acceptable for an internal tool, but a proper root CA certificate should be used for a production deployment.