Community project

Offline RFID Attendance Tracker

Photo of Offline RFID Attendance Tracker
Generated with AI

Krishna Manojkumar

Published November 13, 2026

This project builds an offline RFID attendance tracker that logs when people enter and leave a space using RFID card taps. The ESP32 microcontroller reads RFID cards via the MFRC522 module, toggles an LED to show occupancy status, and provides audio feedback through a piezo buzzer. The system stores attendance records locally and serves them on a web interface accessible over WiFi, making it ideal for classrooms or small offices that need simple, reliable attendance tracking without cloud connectivity.

This guide provides a complete parts list, wiring diagram showing how to connect the RFID reader, LED, resistor, and buzzer to the ESP32, and step-by-step assembly instructions. The included firmware handles card detection, tracks entry and exit status for up to 100 unique cards, and displays real-time attendance data through a built-in web server. Builders will learn how to interface with RFID hardware, manage state tracking, and create a functional web interface on an embedded device.

Wiring diagram

Wiring diagram for Offline RFID Attendance Tracker
Loading diagram…

Gather all the parts

QtyComponent
1

MFRC522 RFID Module

13.56 MHz RFID reader/writer module based on the NXP MFRC522 IC. Communicates over SPI and is commonly sold as an RC522 breakout with an onboard antenna.

1

LED

green

Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.

1

Resistor

220 ohm

Through-hole resistor (current-limiting in series with an LED)

1

KPEG132 active piezo buzzer

Two-pin self-drive piezo buzzer, 3–28 V operating range, maximum 8 mA. Operated at 3.3 V; quieter than its 12 V rating. Do not substitute a higher-current magnetic buzzer without a transistor driver.

Assemble it in 5 steps

1. Place the board and prepare power rails

Disconnect USB. Place the ESP32 across the breadboard center gap with its USB socket accessible. If it leaves no usable holes, use two breadboards. Connect 3V3 to the positive rail and GND to the ground rail. Bridge split rails if necessary.

  • Do not connect USB 5 V to the 3.3 V rail.

2. Connect the RFID reader

Connect VCC (marked 3.3V) to 3V3 and GND to ground. Connect SDA/SS to GPIO21, SCK to GPIO18, MOSI to GPIO23, MISO to GPIO19 and RST to GPIO22. Leave IRQ disconnected. Keep wires short.

  • SDA is SPI chip select on this reader, not I2C data. GPIO21 replaces the requested boot-strapping GPIO5.

3. Add the occupied light

Connect GPIO4 to one end of the 220 ohm resistor. Connect its other end to the green LED long leg (anode). Connect the short leg (cathode, flat edge) to ground. Use separate breadboard rows for the two LED legs.

4. Add tap feedback

Connect KPEG132 buzzer positive to GPIO27 and negative to ground. GPIO27 replaces the requested boot-strapping GPIO15.

  • Use this low-current active piezo buzzer for direct GPIO drive. A generic higher-current buzzer needs a transistor driver; do not substitute one directly.

5. Check before powering

Check polarity, shared ground, and each GPIO label, not header position. Power only through the ESP32 micro-USB port with a regulated 5 V USB supply.

  • RC522 supply and signals must remain 3.3 V.

Review all connections

1. Connections between "reader" and "ESP32"

FunctionreaderESP32
spiSDAGPIO 21
spiSCKGPIO 18
spiMOSIGPIO 23
spiMISOGPIO 19
digitalRSTGPIO 22
powerVCC3V3
groundGNDGND

2. Connections between "led_resistor" and "ESP32"

Functionled_resistorESP32
digitalP1GPIO 4
digitalP2 → LED ANODEEXT

3. Connections between "occupied" and "ESP32"

FunctionoccupiedESP32
groundGNDGND

4. Connections between "feedback" and "ESP32"

FunctionfeedbackESP32
digitalPLUSGPIO 27
groundMINUSGND

Deploy the firmware

#include <Arduino.h>
#include <SPI.h>
#include <MFRC522.h>
#include <WiFi.h>
#include <WebServer.h>
constexpr int SS_PIN=21, RST_PIN=22, LED_PIN=4, BUZZER_PIN=27;
MFRC522 reader(SS_PIN,RST_PIN);
WebServer server(80);
struct Entry { String uid; bool inside=false; unsigned long changed=0; };
Entry entries[100];
int used=0;
unsigned long beepStart=0;
bool beeping=false;
String lastUid;
unsigned long lastTap=0;
void page(){
 String html="<!doctype html><html><head><meta name='viewport' content='width=device-width'><meta http-equiv='refresh' content='5'><title>Attendance</title></head><body><h1>Classroom attendance</h1><p>Times are seconds since startup. Records reset on restart.</p><table border='1'><tr><th>Card UID</th><th>Status</th><th>Last change (s)</th></tr>";
 int count=0;
 for(int i=0;i<used;i++){count+=entries[i].inside;html+="<tr><td>"+entries[i].uid+"</td><td>"+(entries[i].inside?String("Inside"):String("Outside"))+"</td><td>"+String(entries[i].changed/1000)+"</td></tr>";}
 html+="</table><p>People inside: "+String(count)+"</p><p>Remove your card before tapping again. Maximum 100 cards.</p></body></html>";
 server.send(200,"text/html",html);
}
void setup(){
 Serial.begin(115200);
 pinMode(LED_PIN,OUTPUT); digitalWrite(LED_PIN,LOW);
 pinMode(BUZZER_PIN,OUTPUT);digitalWrite(BUZZER_PIN,LOW);
 SPI.begin(18,19,23,SS_PIN);reader.PCD_Init();
 WiFi.mode(WIFI_AP);WiFi.softAP("Classroom-Attendance","Classroom123");
 server.on("/",page);server.begin();Serial.println(WiFi.softAPIP());
}
void loop(){
 server.handleClient();
 if(beeping && millis()-beepStart>=100){digitalWrite(BUZZER_PIN,LOW);beeping=false;}
 if(!reader.PICC_IsNewCardPresent() || !reader.PICC_ReadCardSerial())return;
 String uid;
 for(byte i=0;i<reader.uid.size;i++){if(reader.uid.uidByte[i]<16)uid+="0";uid+=String(reader.uid.uidByte[i],HEX);}uid.toUpperCase();
 reader.PICC_HaltA();reader.PCD_StopCrypto1();
 if(uid==lastUid && millis()-lastTap<1500)return;
 lastUid=uid;lastTap=millis();
 int index=0;while(index<used && entries[index].uid!=uid)index++;
 if(index==used){if(used>=100)return;entries[used++].uid=uid;}
 entries[index].inside=!entries[index].inside;entries[index].changed=millis();
 bool occupied=false;for(int i=0;i<used;i++)occupied|=entries[i].inside;
 digitalWrite(LED_PIN,occupied?HIGH:LOW);
 digitalWrite(BUZZER_PIN,HIGH);beeping=true;beepStart=millis();
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace, diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik