USF Library Tracker — Project Story

Inspiration

Anyone who has studied at USF knows the frustration: you walk across campus to the library, climb to your favorite floor, and find every seat taken. You then try the next floor, and the next, burning 15 minutes just to find a place to sit. There's no way to know how busy a floor is before you get there.

That friction inspired this project. We wanted a system that gives students real-time visibility into floor occupancy before they leave their dorm — the same way Google Maps shows how busy a restaurant is before you drive there. Libraries are shared resources, and better information means students can use them more efficiently.


What it does

The USF Library Tracker monitors how many people are on each floor of the library in real time and displays that information on a live web dashboard.

Each floor has a network of ESP32 microcontrollers mounted at doorways. Pairs of IR break-beam sensors detect whether someone is entering or exiting a floor based on which beam breaks first. This directional logic is simple but effective: if Sensor A (corridor side) breaks before Sensor B (floor side), it's an entry; if B breaks before A, it's an exit.

The system tracks occupancy as a running count, updated every 20 seconds, and expresses it as a percentage of each floor's maximum capacity:

$$\text{Occupancy \%} = \frac{\text{current_occupancy}}{\text{max_capacity}} \times 100$$

The web dashboard color-codes each floor — green below 50%, amber from 50–79%, and red at 80% and above — so students can make an informed decision at a glance.


How we built it

The stack has three layers: embedded hardware, a cloud database, and a web frontend.

Hardware — Each floor runs one Main Board (ESP32 acting as the WiFi-connected initiator) and any number of Support Boards (ESP32s that only communicate wirelessly). All boards use IR break-beam sensor pairs for directional foot traffic detection. Support Boards transmit their entry/exit counts to the Main Board every 10 seconds using ESP-NOW, a peer-to-peer protocol built into the ESP32 that works without joining a WiFi network. The Main Board aggregates all counts and writes to Firebase every 20 seconds over HTTPS.

Database — Firebase Realtime Database stores a node per floor:

floors/
  Floor_1/
    current_occupancy: 42
    max_capacity:      100
    total_entries:     310
    total_exits:       268

Frontend — A Next.js app subscribes to Firebase using onValue(), which pushes updates to the browser the instant new data is written. No polling. The UI re-renders in real time every time the Main Board writes, so the displayed occupancy is never more than 20 seconds stale.

The Arduino modules were written in C++ using the Arduino framework with the ESP-IDF ESP-NOW API. The web app was bootstrapped with create-next-app and uses Tailwind CSS for styling and the Firebase JS SDK for the realtime listener.


Challenges we ran into

ESP-NOW and WiFi channel conflicts — The biggest technical hurdle. The ESP32 radio is shared between WiFi and ESP-NOW. When the Main Board connects to WiFi, it locks to the router's channel (e.g. channel 1). Support Boards default to channel 1 but the timing of channel switches during WiFi beacon frames caused ESP-NOW acknowledgments to be dropped. The onDataSent callback was consistently reporting FAILED even though the Main Board was successfully receiving every packet. The fix was to force the Support Board to the confirmed WiFi channel via esp_wifi_set_channel() and treat esp_now_send() returning ESP_OK as the reliable success signal rather than the ACK callback.

HTTPS from an ESP32 — The standard HTTPClient on ESP32 refuses HTTPS connections without explicit SSL handling. The initial Firebase writes returned connection refused until we switched to WiFiClientSecure with setInsecure() to bypass certificate verification for development.

Firebase auth confusion — The Firebase Web API Key (AIzaSy...) and the Database Secret are two completely different credentials. Using the Web API Key in the ?auth= query parameter on REST calls returns a 401. The correct credential is the legacy Database Secret found under Project Settings → Service Accounts.

Directional detection timing — A naive single-sensor approach can't distinguish entry from exit. The two-sensor sequencing approach works well, but required careful debounce logic: wait up to 500ms for the second sensor to fire, evaluate order, then reset. If only one sensor fires within the window (a partial pass or sensor noise), the event is discarded entirely.


Accomplishments that we're proud of

  • Built a working end-to-end IoT pipeline from physical sensors to a live web dashboard with no intermediary server — the ESP32 writes directly to Firebase.
  • The ESP-NOW mesh architecture means each floor is self-contained. Adding more sensor nodes to a floor requires no changes to the Main Board or the backend — just flash a Support Board with Config.h and it works.
  • The directional detection correctly handles entries and exits with a debounce window, rejecting partial passes and noise without any false counts during testing.
  • The frontend updates in under a second of a Firebase write landing, with zero polling thanks to Firebase's onValue() realtime listener.
  • The entire configuration for a floor — WiFi credentials, floor ID, MAC address, capacity — lives in a single Config.h file, making multi-floor deployment straightforward.

What we learned

  • ESP-NOW is powerful but finicky with WiFi coexistence. The channel synchronization requirement between ESP-NOW peers and the WiFi-connected board is not well-documented and cost significant debugging time.
  • Embedded HTTP clients need explicit TLS handling. The Arduino HTTPClient does not handle HTTPS transparently — WiFiClientSecure must be used and its lifecycle managed carefully to avoid stale connection state.
  • Interrupts over polling for sensors. The example code for the IR sensors used digitalRead() in a polling loop with delay(200). Switching to hardware interrupts on FALLING edge means no foot traffic is missed regardless of what else the loop is doing.
  • Separation of credentials from firmware. Centralizing all deployment-specific values in Config.h and keeping it out of version control is critical when the same codebase is flashed to multiple boards across multiple floors.
  • Firebase's onValue() eliminates the need for frontend polling entirely. The listener model is both simpler to implement and more responsive than any interval-based approach.

What's next for USF Library Tracker

  • NTP time sync — Replace millis() (ms since boot) with real UTC timestamps in the last_updated field so the frontend can display a meaningful "last updated at 2:34 PM" instead of a raw millisecond count.
  • Daily occupancy reset — A Firebase Cloud Function triggered on a schedule at library open time to zero current_occupancy, total_entries, and total_exits each morning, preventing drift from accumulating across days.
  • Drift detection — Surface an alert on the dashboard when $|\text{total_entries} - \text{total_exits}|$ grows unreasonably large, indicating a sensor misfire or missed event that needs attention.
  • Historical analytics — Log hourly snapshots to a Firebase time-series structure so students can see patterns like "Floor 3 is always packed between 11 AM and 2 PM on Wednesdays."
  • Tighter Firebase security rules — Replace open read/write rules with rules that restrict writes to authenticated requests only, before deploying in the actual library.
  • Physical enclosures — 3D-printed mounts for the ESP32 boards and sensor pairs that can be cleanly installed in library doorframes without exposed wiring.
  • Expansion to other campus buildings — The architecture is floor-agnostic. Any building with doorways and WiFi can run this system with no code changes — just a new Config.h per floor.

Built With

Share this project:

Updates