Overview
In this beginner project you build a single-card RFID door lock. The ESP32 reads a card's unique ID (UID) from an RC522 reader connected over SPI. One authorised UID is hardcoded in the sketch. When the matching card is presented, a green LED lights up and a relay unlocks a door strike or electric solenoid for 3 seconds. Any other card triggers a red LED and a short buzzer tone.
The RC522 module communicates via SPI (Serial Peripheral Interface) — a fast four-wire protocol where the ESP32 is the master and the RC522 is the slave. You will learn SPI wiring, the MFRC522 Arduino library, reading card UIDs, and comparing byte arrays. You will also learn how to use a relay for an electric door strike — the most common type of electronic lock used in offices and apartments.
This project is a genuine real-world security component. After completing it you will understand exactly how tap-to-enter systems work — the same technology in your office badge reader and hotel key cards.
Components
- 1× ESP32 DevKit V1
- 1× RC522 RFID Reader Module — Operates at 3.3 V — do NOT use 5 V
- 2× RFID Cards or Key Fobs (13.56 MHz) — Most RC522 kits include 1 card + 1 fob
- 1× 5 V Relay Module — For door strike or solenoid lock
- 1× LED (green) — Access granted indicator
- 1× LED (red) — Access denied indicator
- 2× 220 Ω Resistor — Current limiting for LEDs
- 1× Active Buzzer (5 V) — Optional audio feedback
- 1× Jumper wires + breadboard
Wiring
| Component Pin | ESP32 Pin | Notes |
|---|---|---|
| RC522 SDA (SS) | GPIO 5 | Chip Select / Slave Select |
| RC522 SCK | GPIO 18 | SPI Clock — hardware SPI bus |
| RC522 MOSI | GPIO 23 | Master Out Slave In |
| RC522 MISO | GPIO 19 | Master In Slave Out |
| RC522 RST | GPIO 27 | Reset — any spare GPIO works |
| RC522 3.3 V | 3.3 V | CRITICAL: 3.3 V only — 5 V destroys RC522 |
| RC522 GND | GND | |
| Relay IN | GPIO 26 | |
| Green LED (+ 220 Ω) | GPIO 33 | LED anode via 220 Ω to GPIO, cathode to GND |
| Red LED (+ 220 Ω) | GPIO 32 | Same wiring pattern |
| Buzzer + | GPIO 25 | Active buzzer: + to GPIO, - to GND |
Arduino Code
/*
* ESP32 RFID Access Control — Beginner
* Single authorised card UID stored in flash.
* Green LED + relay = access granted (3 s).
* Red LED + buzzer = access denied.
*
* Library: "MFRC522" by GithubCommunity (Library Manager)
*/
#include <SPI.h>
#include <MFRC522.h>
#define SS_PIN 5
#define RST_PIN 27
#define RELAY_PIN 26
#define LED_GREEN 33
#define LED_RED 32
#define BUZZER 25
#define DOOR_MS 3000 // How long to hold relay open (ms)
MFRC522 rfid(SS_PIN, RST_PIN);
// ── Authorised UID — replace with YOUR card's UID ───────────────
// Scan a card and read the UID from Serial Monitor, then paste it here.
byte AUTH_UID[] = { 0xDE, 0xAD, 0xBE, 0xEF };
byte AUTH_LEN = 4; // Most MIFARE Classic cards: 4 bytes
void grantAccess() {
Serial.println("ACCESS GRANTED");
digitalWrite(LED_GREEN, HIGH);
digitalWrite(RELAY_PIN, HIGH);
delay(DOOR_MS);
digitalWrite(RELAY_PIN, LOW);
digitalWrite(LED_GREEN, LOW);
}
void denyAccess() {
Serial.println("ACCESS DENIED");
for (int i = 0; i < 3; i++) {
digitalWrite(LED_RED, HIGH);
digitalWrite(BUZZER, HIGH);
delay(150);
digitalWrite(LED_RED, LOW);
digitalWrite(BUZZER, LOW);
delay(100);
}
}
void setup() {
Serial.begin(115200);
SPI.begin();
rfid.PCD_Init();
rfid.PCD_DumpVersionToSerial(); // Prints firmware version — confirms wiring
pinMode(RELAY_PIN, OUTPUT); digitalWrite(RELAY_PIN, LOW);
pinMode(LED_GREEN, OUTPUT); pinMode(LED_RED, OUTPUT);
pinMode(BUZZER, OUTPUT);
Serial.println("Waiting for card...");
}
void loop() {
if (!rfid.PICC_IsNewCardPresent() || !rfid.PICC_ReadCardSerial()) return;
// Print scanned UID to Serial (useful for learning what UID your card has)
Serial.print("Card UID: ");
for (byte i = 0; i < rfid.uid.size; i++) {
Serial.printf("%02X ", rfid.uid.uidByte[i]);
}
Serial.println();
// Compare UID
bool match = (rfid.uid.size == AUTH_LEN);
if (match) {
for (byte i = 0; i < AUTH_LEN; i++) {
if (rfid.uid.uidByte[i] != AUTH_UID[i]) { match = false; break; }
}
}
match ? grantAccess() : denyAccess();
rfid.PICC_HaltA();
rfid.PCD_StopCrypto1();
}How It Works
SPI Communication: SPI uses four wires: SCK (clock), MOSI (data to slave), MISO (data from slave), and SS/SDA (chip select). The ESP32 drives SCK and MOSI while reading MISO. SS going LOW tells the RC522 the bus is for it — allowing multiple SPI devices on the same bus using different SS pins.
RFID Card Detection: rfid.PICC_IsNewCardPresent() sends a 13.56 MHz radio pulse and listens for a response. When a card enters the ~3 cm field, its internal antenna harvests power from the field and sends back its UID. PICC_ReadCardSerial() reads and validates the full UID.
UID Comparison: The UID is stored as a byte array. The loop compares each byte of the scanned card against the hardcoded AUTH_UID[]. A single byte mismatch sets match = false. Comparing byte-by-byte is safer than converting to a string, which can introduce encoding issues.
Relay Timing: On access grant, the relay energises for DOOR_MS milliseconds (default 3000 = 3 seconds). During this window the LED stays on. When the door catches and closes, the relay drops and the LED turns off. Adjust DOOR_MS to suit your electric strike's release time.
Learning Your Card UID: Scan your card before setting AUTH_UID. The sketch prints the full UID to Serial Monitor in hex format (e.g. DE AD BE EF). Copy each byte into AUTH_UID[] and set AUTH_LEN to the number of bytes. Most MIFARE Classic 1K cards are 4 bytes; MIFARE DESFire cards are 7 bytes.
Applications
- Home office or workshop door entry system
- Filing cabinet or safe electronic lock
- Makerspace machine-enable interlock (lathe, laser cutter)
- Prototype for apartment building entry systems
- Attendance logger combined with a server-side database
Troubleshooting
rfid.PCD_DumpVersionToSerial() prints 0x00 or 0xFF
This means the ESP32 cannot communicate with the RC522. Check: 1) RC522 is on 3.3 V, not 5 V. 2) SPI pins match exactly (SS→GPIO5, SCK→GPIO18, MOSI→GPIO23, MISO→GPIO19). 3) RST pin is connected. 4) All GND lines are common.
Card is detected but UID bytes are always 00 00 00 00
The card is not being fully read. Ensure the card is held flat and parallel to the RC522 antenna, within 1-3 cm. Some plastic card holders block RFID — test without them first.
Access is always denied even for the correct card
Open Serial Monitor and scan the card to see the actual UID bytes printed. Compare them to the AUTH_UID[] array in your sketch — they must match exactly including byte order.
Upgrades
- Store multiple authorised UIDs in an array and loop through them
- Add a keypad so the system requires RFID + PIN (two-factor access)
- Log every access attempt with timestamp to Serial or SD card
- Add a push-exit button on the inside of the door
FAQ
You need an ESP32 DevKit, TODO: sensor, RC522 SCK, a breadboard, jumper wires, and a USB cable for power and programming.
Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.
Start with Beginner if you are new to Security Projects. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.
Overview
At the intermediate level you expand to a multi-card system with persistent storage. Authorised UIDs are stored in ESP32 NVS flash using the Preferences library, so you can add and remove cards without reflashing. A small serial command interface lets you enrol new cards, list authorised cards, and revoke access — all through the Arduino Serial Monitor or any terminal.
You also add an access attempt log that records the last 20 events (card UID, grant/deny result, timestamp from NTP) in circular buffer memory, viewable via the serial command log. This intermediate level mirrors the logic inside a real commercial access controller.
Components
- 1× ESP32 DevKit V1
- 1× RC522 RFID Module
- 2+× RFID Cards / Fobs — One for admin enrolment, one per user
- 1× 5 V Relay Module
- 1× LEDs (green + red) + 220 Ω resistors
- 1× Active Buzzer
Wiring
| Component Pin | ESP32 Pin | Notes |
|---|---|---|
| RC522 SDA/SCK/MOSI/MISO/RST | GPIO 5/18/23/19/27 | Same as beginner |
| Relay IN / LEDs / Buzzer | GPIO 26 / 33 / 32 / 25 | Same as beginner |
Arduino Code
/*
* ESP32 RFID Access Control — Intermediate
* Multi-card NVS storage + serial admin commands + access log
*
* Serial commands (115200 baud):
* enrol — present a card to add it
* revoke — present a card to remove it
* list — list all authorised UIDs
* log — show last 20 access events
* clear — wipe ALL authorised UIDs (careful!)
*/
#include <SPI.h>
#include <MFRC522.h>
#include <Preferences.h>
#include <WiFi.h>
#include <time.h>
#define SS_PIN 5
#define RST_PIN 27
#define RELAY_PIN 26
#define LED_GREEN 33
#define LED_RED 32
#define BUZZER 25
#define MAX_CARDS 20
#define LOG_SIZE 20
const char* SSID = "YOUR_WIFI_SSID";
const char* PASSWORD = "YOUR_WIFI_PASSWORD";
MFRC522 rfid(SS_PIN, RST_PIN);
Preferences prefs;
// Access log (circular buffer)
struct LogEntry { String uid; bool granted; String ts; };
LogEntry accessLog[LOG_SIZE];
int logIdx = 0;
String uidToString(byte* uid, byte len) {
String s;
for (byte i = 0; i < len; i++) { if(i) s+=":"; s+=String(uid[i],HEX); }
s.toUpperCase(); return s;
}
String nowString() {
struct tm t; if(getLocalTime(&t)){
char b[20]; strftime(b,sizeof(b),"%H:%M:%S",&t); return String(b);
} return "??:??:??";
}
bool isAuthorised(String uid) {
for (int i = 0; i < MAX_CARDS; i++) {
if (prefs.getString(("c"+String(i)).c_str(),"") == uid) return true;
}
return false;
}
bool enrollCard(String uid) {
for (int i = 0; i < MAX_CARDS; i++) {
String key = "c"+String(i);
if (prefs.getString(key.c_str(),"").isEmpty()) {
prefs.putString(key.c_str(), uid); return true;
}
}
return false; // storage full
}
void revokeCard(String uid) {
for (int i = 0; i < MAX_CARDS; i++) {
String key = "c"+String(i);
if (prefs.getString(key.c_str(),"") == uid) { prefs.remove(key.c_str()); return; }
}
}
void addLog(String uid, bool granted) {
accessLog[logIdx % LOG_SIZE] = {uid, granted, nowString()};
logIdx++;
}
enum Mode { NORMAL, ENROL, REVOKE };
Mode mode = NORMAL;
void setup() {
Serial.begin(115200);
SPI.begin(); rfid.PCD_Init();
pinMode(RELAY_PIN,OUTPUT); digitalWrite(RELAY_PIN,LOW);
pinMode(LED_GREEN,OUTPUT); pinMode(LED_RED,OUTPUT); pinMode(BUZZER,OUTPUT);
prefs.begin("rfid", false);
WiFi.begin(SSID, PASSWORD);
for (int i=0;i<20&&WiFi.status()!=WL_CONNECTED;i++) delay(500);
if(WiFi.status()==WL_CONNECTED) configTime(0,0,"pool.ntp.org");
Serial.println("RFID Access Control ready. Commands: enrol | revoke | list | log | clear");
}
void loop() {
// Handle serial commands
if (Serial.available()) {
String cmd = Serial.readStringUntil('\n'); cmd.trim();
if (cmd == "enrol") { mode=ENROL; Serial.println("Present card to ENROL..."); }
if (cmd == "revoke") { mode=REVOKE; Serial.println("Present card to REVOKE..."); }
if (cmd == "list") {
Serial.println("Authorised UIDs:");
for(int i=0;i<MAX_CARDS;i++){
String u=prefs.getString(("c"+String(i)).c_str(),"");
if(!u.isEmpty()) Serial.printf(" [%d] %sn",i,u.c_str());
}
}
if (cmd == "log") {
Serial.println("Access Log (newest last):");
for(int i=0;i<min(logIdx,LOG_SIZE);i++){
auto& e=accessLog[i];
Serial.printf(" %s %s %sn",e.ts.c_str(),e.uid.c_str(),e.granted?"GRANT":"DENY");
}
}
if (cmd == "clear") { prefs.clear(); Serial.println("All UIDs cleared."); }
}
if (!rfid.PICC_IsNewCardPresent() || !rfid.PICC_ReadCardSerial()) return;
String uid = uidToString(rfid.uid.uidByte, rfid.uid.size);
Serial.print("Card: "); Serial.println(uid);
if (mode == ENROL) {
enrollCard(uid) ? Serial.println("Enrolled.") : Serial.println("Storage full.");
mode = NORMAL;
} else if (mode == REVOKE) {
revokeCard(uid); Serial.println("Revoked.");
mode = NORMAL;
} else {
bool ok = isAuthorised(uid);
addLog(uid, ok);
if (ok) {
digitalWrite(LED_GREEN,HIGH); digitalWrite(RELAY_PIN,HIGH);
delay(3000);
digitalWrite(RELAY_PIN,LOW); digitalWrite(LED_GREEN,LOW);
} else {
for(int i=0;i<3;i++){
digitalWrite(LED_RED,HIGH); digitalWrite(BUZZER,HIGH); delay(150);
digitalWrite(LED_RED,LOW); digitalWrite(BUZZER,LOW); delay(100);
}
}
}
rfid.PICC_HaltA(); rfid.PCD_StopCrypto1();
}How It Works
NVS Slot Allocation: Preferences stores up to MAX_CARDS UIDs as string key-value pairs (c0, c1, … c19). enrollCard() scans for the first empty slot (empty string value) and writes the UID there. revokeCard() finds the matching slot and calls prefs.remove() to clear it.
Admin Mode via Serial: A simple state machine (enum Mode) switches between NORMAL, ENROL, and REVOKE modes based on serial commands. When a card is presented in ENROL mode, it is added rather than checked. This is the same pattern commercial access controllers use with a physical admin card or master key.
Circular Log Buffer: accessLog[] is a fixed array of LOG_SIZE structs. logIdx is an ever-increasing integer; the actual array index is logIdx % LOG_SIZE. When logIdx exceeds LOG_SIZE, old entries are overwritten — a classic circular buffer pattern requiring no dynamic memory.
Applications
- Field trial with visible OLED feedback
- Manual override for maintenance or testing
- Calibrated setup for daily use
Troubleshooting
Card is enrolled but still denied
Run the "list" command to confirm the UID is stored. Compare the format: the sketch stores colon-separated uppercase hex (e.g. DE:AD:BE:EF). If the beginner-level UID used spaces (DE AD BE EF), the formats won't match — they must be identical.
Time shows ??:??:?? in the log
The ESP32 did not connect to Wi-Fi or NTP. Check SSID/PASSWORD. NTP requires working internet access from the ESP32's network, not just local network access.
Upgrades
- Add a 16×2 LCD to show "Welcome, User" on grant instead of just a green LED
- Store user names mapped to UIDs for human-readable log entries
- Add a door-open sensor (magnetic reed switch) to detect if the door was actually opened after grant
FAQ
You need an ESP32 DevKit, TODO: sensor, RC522 SCK, a breadboard, jumper wires, and a USB cable for power and programming.
Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.
Start with Beginner if you are new to Security Projects. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.
Overview
The advanced level adds a full web-based admin panel served directly by the ESP32. From any browser on your network you can view all authorised cards by name, enrol or revoke access, set per-user access schedules (time-of-day restrictions), and download the complete access log as CSV. The system also publishes every access event to MQTT so your home automation can react — for example, turning on the entry hall lights when a specific card is presented.
Card UIDs are stored with associated user names, giving meaningful log entries ("Alice granted at 08:32") instead of raw hex strings. The web panel uses a minimal REST API (JSON over HTTP) with a small JS front-end, making this an excellent introduction to building ESP32-hosted web applications beyond simple dashboards.
Components
- 1× ESP32 DevKit V1
- 1× RC522 RFID Module
- 1× 5 V Relay Module
- 2+× RFID Cards / Fobs
- 1× MQTT Broker (local or cloud) — For event publishing
Wiring
| Component Pin | ESP32 Pin | Notes |
|---|---|---|
| RC522 SDA/SCK/MOSI/MISO/RST | GPIO 5/18/23/19/27 | Same as previous levels |
| Relay IN / LEDs / Buzzer | GPIO 26/33/32/25 | Same as previous levels |
Arduino Code
/*
* ESP32 RFID Access Control — Advanced
* Web admin panel + named users + MQTT events + CSV log export
*
* Endpoints:
* GET / — Admin panel HTML
* GET /api/users — List all users (JSON)
* POST /api/enrol — Enrol card: present card within 10s after request
* POST /api/revoke?uid=XX:XX — Revoke by UID
* GET /api/log — Access log (JSON)
* GET /api/log/csv — Download log as CSV
*
* Libraries: MFRC522, PubSubClient, WebServer (built-in)
*/
#include <SPI.h>
#include <MFRC522.h>
#include <WiFi.h>
#include <WebServer.h>
#include <PubSubClient.h>
#include <Preferences.h>
#include <time.h>
const char* SSID = "YOUR_WIFI_SSID";
const char* PASSWORD = "YOUR_WIFI_PASSWORD";
const char* BROKER = "192.168.1.100";
#define SS_PIN 5
#define RST_PIN 27
#define RELAY_PIN 26
#define LED_GREEN 33
#define LED_RED 32
#define BUZZER 25
MFRC522 rfid(SS_PIN, RST_PIN);
WebServer server(80);
WiFiClient net;
PubSubClient mqtt(net);
Preferences prefs;
struct User { String uid; String name; };
struct LogEntry { String uid; String name; bool granted; String ts; };
#define MAX_USERS 20
#define LOG_SIZE 50
User users[MAX_USERS];
LogEntry log_[LOG_SIZE];
int userCount=0, logHead=0;
bool enrolPending=false;
unsigned long enrolDeadline=0;
String enrolName;
String nowStr(){
struct tm t; char b[20]="";
if(getLocalTime(&t)) strftime(b,sizeof(b),"%Y-%m-%d %H:%M:%S",&t);
return String(b);
}
String uidStr(byte* u,byte l){
String s; for(byte i=0;i<l;i++){if(i)s+=":";s+=String(u[i],HEX);}
s.toUpperCase(); return s;
}
void loadUsers(){
userCount=prefs.getInt("count",0);
for(int i=0;i<userCount;i++){
users[i].uid = prefs.getString(("u"+String(i)+"uid").c_str(),"");
users[i].name = prefs.getString(("u"+String(i)+"nam").c_str(),"User");
}
}
void saveUsers(){
prefs.putInt("count",userCount);
for(int i=0;i<userCount;i++){
prefs.putString(("u"+String(i)+"uid").c_str(),users[i].uid);
prefs.putString(("u"+String(i)+"nam").c_str(),users[i].name);
}
}
void publishEvent(String uid,String name,bool ok){
if(!mqtt.connected()) return;
String j="{"uid":""+uid+"","user":""+name+"","granted":"+(ok?"true":"false")+","ts":""+nowStr()+""}";
mqtt.publish("esp32engine/rfid/events",j.c_str());
}
void addLog(String uid,String name,bool ok){
log_[logHead%LOG_SIZE]={uid,name,ok,nowStr()};
logHead++;
publishEvent(uid,name,ok);
}
// ── Web Handlers ────────────────────────────────────────────────
void handleUsers(){
String j="[";
for(int i=0;i<userCount;i++){
if(i) j+=",";
j+="{"uid":""+users[i].uid+"","name":""+users[i].name+""}";
}
j+="]"; server.send(200,"application/json",j);
}
void handleEnrol(){
enrolName=server.hasArg("name")?server.arg("name"):"Unknown";
enrolPending=true; enrolDeadline=millis()+10000;
server.send(200,"text/plain","Present card within 10 seconds");
}
void handleRevoke(){
String uid=server.arg("uid");
for(int i=0;i<userCount;i++){
if(users[i].uid==uid){
users[i]=users[--userCount]; saveUsers();
server.send(200,"text/plain","Revoked"); return;
}
}
server.send(404,"text/plain","Not found");
}
void handleLog(){
String j="["; int count=min(logHead,LOG_SIZE);
for(int i=0;i<count;i++){
auto& e=log_[i]; if(i) j+=",";
j+="{"uid":""+e.uid+"","user":""+e.name+"","granted":"+(e.granted?"true":"false")+","ts":""+e.ts+""}";
}
j+="]"; server.send(200,"application/json",j);
}
void handleLogCSV(){
String csv="UID,User,Granted,Timestampn";
for(int i=0;i<min(logHead,LOG_SIZE);i++){
auto& e=log_[i];
csv+=e.uid+","+e.name+","+(e.granted?"YES":"NO")+","+e.ts+"n";
}
server.sendHeader("Content-Disposition","attachment; filename=access_log.csv");
server.send(200,"text/csv",csv);
}
void setup(){
Serial.begin(115200);
SPI.begin(); rfid.PCD_Init();
pinMode(RELAY_PIN,OUTPUT); digitalWrite(RELAY_PIN,LOW);
pinMode(LED_GREEN,OUTPUT); pinMode(LED_RED,OUTPUT); pinMode(BUZZER,OUTPUT);
prefs.begin("rfid2",false); loadUsers();
WiFi.begin(SSID,PASSWORD);
while(WiFi.status()!=WL_CONNECTED) delay(400);
configTime(0,0,"pool.ntp.org");
mqtt.setServer(BROKER,1883);
mqtt.connect("rfid-controller");
server.on("/api/users",HTTP_GET,handleUsers);
server.on("/api/enrol",HTTP_POST,handleEnrol);
server.on("/api/revoke",HTTP_POST,handleRevoke);
server.on("/api/log",HTTP_GET,handleLog);
server.on("/api/log/csv",HTTP_GET,handleLogCSV);
server.begin();
Serial.printf("Admin panel: http://%s/n",WiFi.localIP().toString().c_str());
}
void loop(){
server.handleClient();
if(!mqtt.connected()) mqtt.connect("rfid-controller");
mqtt.loop();
if(!rfid.PICC_IsNewCardPresent()||!rfid.PICC_ReadCardSerial()) return;
String uid=uidStr(rfid.uid.uidByte,rfid.uid.size);
if(enrolPending && millis()<enrolDeadline){
if(userCount<MAX_USERS){
users[userCount++]={uid,enrolName}; saveUsers();
Serial.printf("Enrolled: %s -> %sn",uid.c_str(),enrolName.c_str());
}
enrolPending=false;
rfid.PICC_HaltA(); rfid.PCD_StopCrypto1(); return;
}
enrolPending=false;
String name="Unknown";
bool ok=false;
for(int i=0;i<userCount;i++){
if(users[i].uid==uid){ ok=true; name=users[i].name; break; }
}
addLog(uid,name,ok);
if(ok){
digitalWrite(LED_GREEN,HIGH); digitalWrite(RELAY_PIN,HIGH); delay(3000);
digitalWrite(RELAY_PIN,LOW); digitalWrite(LED_GREEN,LOW);
} else {
for(int i=0;i<3;i++){
digitalWrite(LED_RED,HIGH);digitalWrite(BUZZER,HIGH);delay(150);
digitalWrite(LED_RED,LOW); digitalWrite(BUZZER,LOW); delay(100);
}
}
rfid.PICC_HaltA(); rfid.PCD_StopCrypto1();
}How It Works
REST API Design: The web server exposes five JSON endpoints. The admin HTML page calls these via browser fetch() — no server-side rendering required. This separates data (API) from presentation (HTML), the same architecture used by modern web applications.
Pending Enrolment Pattern: When POST /api/enrol is called, the server sets a flag (enrolPending) and a 10-second deadline. The next card scan within that window is enrolled rather than authenticated. This is a common pattern in physical security systems where you press an enrol button then present the card.
MQTT Event Publishing: Every access event (grant or deny) is published to esp32engine/rfid/events as JSON. A Node-RED flow or Home Assistant automation can consume this stream: turn on lights when Alice is granted, send a push notification when an unknown card is denied, or update a Google Sheet for audit compliance.
CSV Export: The /api/log/csv endpoint sends a Content-Disposition header with filename=access_log.csv, causing the browser to download the data as a file. This gives building managers a format compatible with Excel for compliance reporting.
Applications
- Remote monitoring from phone or laptop
- Automated alerts when limits are crossed
- Long-term trend logging for optimization
Troubleshooting
Enrol endpoint returns 200 but card is not stored after scanning
The 10-second enrolment window may have expired before the card was presented. Check that enrolPending is true and enrolDeadline is still in the future when the card scan triggers. Increase the 10000 ms deadline if needed.
MQTT events not appearing on broker
Call mqtt.connect() in loop() as a reconnect handler — the advanced sketch shows a minimal version. Verify the broker IP with a mosquitto_sub -h BROKER_IP -t "esp32engine/rfid/#" command on the broker machine.
Upgrades
- Add per-user access schedules: deny card X on weekends or outside 08:00–18:00
- Integrate with Telegram Bot API to send push notifications on denied access attempts
- Add a camera (ESP32-CAM as a separate node) to photograph every access attempt
FAQ
You need an ESP32 DevKit, TODO: sensor, RC522 SCK, a breadboard, jumper wires, and a USB cable for power and programming.
Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.
Start with Beginner if you are new to Security Projects. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.