Smart RFID Door Lock + App Control
Intermediate1/8/2026- Author: IoTSpark Maker

Smart RFID Door Lock + App Control

An ESP32 with an RC522 RFID reader unlocks the door with a valid card, while also hosting a local HTTP API for remote unlocking from an app — both paths share the same lock logic and auto-relock after a few seconds.

ESP32RFIDRC522RelaySmart LockDoor Lock
0 steps5 components

This RFID door lock supports two ways to unlock: tapping a valid RFID card on the RC522 reader in person, or sending an unlock command remotely through a local HTTP API hosted right on the ESP32 — simulating an "app control" flow.

Detailed guide

An ESP32 with an RC522 RFID reader unlocks the door with a valid card, while also hosting a local HTTP API for remote unlocking from an app.

1. Introduction

This RFID door lock combines two unlock methods: (1) tapping a valid RFID card/tag on the RC522 reader in person, and (2) sending an unlock command remotely through a local HTTP API hosted right on the ESP32 (simulating an "app control" flow).

Once authentication succeeds (a valid card UID or the correct app token), the ESP32 triggers a relay that powers the electric lock for a configured duration and then automatically re-locks — no need for the user to remember to lock it manually.

2. Components Needed

Component

Qty

Notes

ESP32 DevKit V4

1

The main board, handling RFID (SPI) + WiFi + WebServer

RFID RC522 Reader/Writer (mod_rc522_rfid)

1

Reads 13.56MHz card/tag UIDs over SPI, powered at 3.3V only

Relay Module 1 kênh 5V (mod_relay_1ch)

1

Closes the power circuit to the electric lock on successful authentication

Van điện từ Solenoid 12V (mod_solenoid_valve)

1

Substitutes for a real 12V electric door lock — the catalog doesn't yet have a dedicated "electric door lock" entry; since it shares the same electrical characteristics (a 12V DC coil), it's used here to illustrate the solenoid load

Diode chỉnh lưu 1N4007 (comp_diode_1n4007)

1

A flyback diode wired in reverse parallel across the lock/solenoid coil to protect the relay

3. Wiring Diagram

Module pin

ESP32

RC522 VCC

3V3 (3.3V ONLY — do not use 5V)

RC522 GND

GND

RC522 RST

GPIO27

RC522 SDA/SS

GPIO5

RC522 MOSI

GPIO23

RC522 MISO

GPIO19

RC522 SCK

GPIO18

Relay VCC

5V

Relay GND

GND

Relay IN

GPIO26

Relay NO/COM

+12V external supply → Solenoid VCC (the electric lock)

Diode 1N4007 CATHODE

Solenoid VCC (+12V)

Diode 1N4007 ANODE

Solenoid GND

Important note: the RC522 only tolerates 3.3V on VCC — accidentally feeding it 5V will destroy the module. The 1N4007 diode is wired in reverse parallel across the lock/solenoid coil (cathode toward the positive supply) to absorb the back-EMF spike generated when the relay cuts power to an inductive load, protecting the relay's contacts from arcing.

4. Example #1 — Reading an RFID Card's UID via RC522 (SPI)

This snippet shows the function that converts a UID to a hex string, and the card-reading loop:

String uidToString(MFRC522::Uid uid) {
  String s = "";
  for (byte i = 0; i < uid.size; i++) {
    if (uid.uidByte[i] < 0x10) s += "0";
    s += String(uid.uidByte[i], HEX);
    if (i + 1 < uid.size) s += " ";
  }
  s.toUpperCase();
  return s;
}

// Trong loop():
if (rfid.PICC_IsNewCardPresent() && rfid.PICC_ReadCardSerial()) {
  String uid = uidToString(rfid.uid);
  Serial.println(uid); // vd "DE AD BE EF"
  rfid.PICC_HaltA();
  rfid.PCD_StopCrypto1();
}

5. Example #2 — Unlocking via an App Command (HTTP WebServer)

This snippet shows the /unlock route — the app sends a POST with the correct X-Api-Token header to unlock remotely:

void handleUnlock() {
  String token = server.header("X-Api-Token");
  if (token != APP_API_TOKEN) {
    server.send(401, "application/json", "{\"error\":\"invalid token\"}");
    return;
  }
  setLock(true, "app request");
  server.send(200, "application/json", "{\"status\":\"unlocked\"}");
}

// setup(): server.on("/unlock", HTTP_POST, handleUnlock);
//          server.on("/status", HTTP_GET, handleStatus);

6. Example #3 — The Complete Application: RFID + Relay + WebServer App Control

The full firmware: boots up, checks that the RC522's version register can be read at boot (degrading to logging "rfid=null" if it can't), reconnects WiFi with a timeout, and auto-relocks after 4 seconds:

/*
  Khóa cửa thông minh RFID + điều khiển qua app
  Board: ESP32 DevKit V4 (board_esp32_devkitc)
  - RFID RC522 (mod_rc522_rfid, SPI, VCC 3.3V CHỈ):
      RST  -> GPIO27
      SDA/SS -> GPIO5
      MOSI -> GPIO23
      MISO -> GPIO19
      SCK  -> GPIO18
      VCC  -> 3V3, GND -> GND
  - Relay 1 kênh (mod_relay_1ch) IN -> GPIO26, VCC -> 5V, GND -> GND
      NO/COM đóng mạch cấp nguồn 12V cho khóa điện từ / van solenoid (mod_solenoid_valve,
      dùng thay thế khóa điện từ 12V thật vì catalog hiện chưa có "khóa cửa điện từ" riêng
      - cùng đặc tính điện: cuộn dây DC 12V, cần diode chống dội flyback).
  - Diode 1N4007 (comp_diode_1n4007) mắc song song ngược cực với cuộn dây khóa để bảo vệ relay
    khỏi điện áp cảm ứng ngược khi ngắt tải.

  Tính năng "điều khiển qua app": ESP32 mở một WebServer HTTP nội bộ (cùng mạng LAN) nhận
  lệnh mở khóa từ xa (POST /unlock), ngoài luồng quét thẻ RFID tại chỗ.
*/

#include <WiFi.h>
#include <WebServer.h>
#include <SPI.h>
#include <MFRC522.h>

// ==== Cấu hình người dùng ====
const char *WIFI_SSID = "YOUR_WIFI_SSID";
const char *WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char *APP_API_TOKEN = "YOUR_SHARED_SECRET_TOKEN"; // app phải gửi kèm token này

// ==== Chân kết nối ====
const int PIN_RC522_RST = 27;
const int PIN_RC522_SS = 5;
const int PIN_RELAY_IN = 26;

const unsigned long UNLOCK_DURATION_MS = 4000; // giữ khóa mở 4s rồi tự khóa lại
const unsigned long WIFI_RETRY_INTERVAL_MS = 5000;
const unsigned long WIFI_CONNECT_TIMEOUT_MS = 10000;
const unsigned long TELEMETRY_INTERVAL_MS = 5000;

// Danh sách UID thẻ được phép (thay bằng UID thật khi triển khai)
const char *ALLOWED_UIDS[] = {"DE AD BE EF", "12 34 56 78"};
const size_t ALLOWED_UIDS_COUNT = sizeof(ALLOWED_UIDS) / sizeof(ALLOWED_UIDS[0]);

MFRC522 rfid(PIN_RC522_SS, PIN_RC522_RST);
WebServer server(80);

unsigned long lastWifiAttemptMs = 0;
unsigned long lastTelemetryMs = 0;
unsigned long unlockUntilMs = 0;
bool locked = true;
bool rfidReady = false;

void setLock(bool unlock, const char *reason) {
  digitalWrite(PIN_RELAY_IN, unlock ? LOW : HIGH); // active-low relay
  locked = !unlock;
  if (unlock) {
    unlockUntilMs = millis() + UNLOCK_DURATION_MS;
  }
  Serial.printf("[Lock] %s (ly do: %s)\n", unlock ? "MO KHOA" : "KHOA LAI", reason);
}

String uidToString(MFRC522::Uid uid) {
  String s = "";
  for (byte i = 0; i < uid.size; i++) {
    if (uid.uidByte[i] < 0x10) s += "0";
    s += String(uid.uidByte[i], HEX);
    if (i + 1 < uid.size) s += " ";
  }
  s.toUpperCase();
  return s;
}

bool isAllowedUid(const String &uid) {
  for (size_t i = 0; i < ALLOWED_UIDS_COUNT; i++) {
    if (uid.equalsIgnoreCase(ALLOWED_UIDS[i])) return true;
  }
  return false;
}

void handleUnlock() {
  String token = server.header("X-Api-Token");
  if (token != APP_API_TOKEN) {
    server.send(401, "application/json", "{\"error\":\"invalid token\"}");
    return;
  }
  setLock(true, "app request");
  server.send(200, "application/json", "{\"status\":\"unlocked\"}");
}

void handleStatus() {
  String json = String("{\"locked\":") + (locked ? "true" : "false") +
                ",\"rfidReady\":" + (rfidReady ? "true" : "false") + "}";
  server.send(200, "application/json", json);
}

void connectWiFiNonBlocking() {
  if (WiFi.status() == WL_CONNECTED) return;
  unsigned long now = millis();
  if (now - lastWifiAttemptMs < WIFI_RETRY_INTERVAL_MS) return;
  lastWifiAttemptMs = now;

  Serial.printf("[WiFi] Dang ket noi toi SSID \"%s\" ...\n", WIFI_SSID);
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  unsigned long start = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - start < WIFI_CONNECT_TIMEOUT_MS) {
    delay(200);
    Serial.print(".");
  }
  Serial.println();

  if (WiFi.status() == WL_CONNECTED) {
    Serial.printf("[WiFi] Da ket noi. IP: %s\n", WiFi.localIP().toString().c_str());
    server.begin();
    Serial.println("[HTTP] WebServer san sang: POST /unlock, GET /status");
  } else {
    Serial.println("[WiFi] Ket noi that bai, se thu lai sau.");
  }
}

void setup() {
  Serial.begin(115200);
  delay(300);
  Serial.println();
  Serial.println("========================================");
  Serial.println(" IoTLabs Maker - Khoa Cua RFID + App");
  Serial.println(" Board: ESP32 DevKit V4");
  Serial.println("========================================");

  pinMode(PIN_RELAY_IN, OUTPUT);
  setLock(false, "khoi dong");

  SPI.begin(); // SCK=GPIO18, MISO=GPIO19, MOSI=GPIO23 (mac dinh VSPI tren ESP32 DevKit)
  rfid.PCD_Init();
  byte version = rfid.PCD_ReadRegister(MFRC522::VersionReg);
  rfidReady = (version != 0x00 && version != 0xFF);
  if (rfidReady) {
    Serial.printf("[RC522] Khoi tao OK (version reg=0x%02X)\n", version);
  } else {
    Serial.println("[RC522] KHONG doc duoc module - kiem tra day noi SPI/nguon 3.3V.");
  }

  server.on("/unlock", HTTP_POST, handleUnlock);
  server.on("/status", HTTP_GET, handleStatus);

  connectWiFiNonBlocking();
}

void loop() {
  connectWiFiNonBlocking();
  if (WiFi.status() == WL_CONNECTED) {
    server.handleClient();
  }

  if (!locked && millis() > unlockUntilMs) {
    setLock(false, "het thoi gian giu khoa");
  }

  if (rfidReady && rfid.PICC_IsNewCardPresent() && rfid.PICC_ReadCardSerial()) {
    String uid = uidToString(rfid.uid);
    bool allowed = isAllowedUid(uid);
    Serial.printf("[RFID] The quet UID=%s -> %s\n", uid.c_str(), allowed ? "HOP LE" : "TU CHOI");
    if (allowed) {
      setLock(true, "the RFID hop le");
    }
    rfid.PICC_HaltA();
    rfid.PCD_StopCrypto1();
  }

  unsigned long now = millis();
  if (now - lastTelemetryMs >= TELEMETRY_INTERVAL_MS) {
    lastTelemetryMs = now;
    Serial.printf("[Telemetry] locked=%s rfid=%s wifi=%s\n",
                  locked ? "true" : "false",
                  rfidReady ? "ready" : "null",
                  WiFi.status() == WL_CONNECTED ? "connected" : "disconnected");
  }

  delay(50);
}

7. Common Issues

Issue

Cause

Fix

Serial prints "RC522 KHONG doc duoc module" (module unreadable)

Wrong SPI wiring, or 5V was mistakenly fed into VCC, damaging the module

Double-check the 4 SPI wires (MOSI/MISO/SCK/SS) are on the right pins, and measure VCC to confirm it's exactly 3.3V

Tapping a valid card doesn't unlock the door

The card's actual UID isn't in the ALLOWED_UIDS array, or the RC522 is hanging due to SPI noise from overly long wires

Print the UID to Serial when tapping to capture the real UID, then update the array; keep SPI wires under 20cm

The app's call to /unlock returns 401

The token sent by the app doesn't match the APP_API_TOKEN configured on the ESP32

Sync the token value between the app and the firmware; avoid hard-coding a real token in publicly shared sample code

The relay randomly clicks repeatedly even without a card being tapped

Missing flyback diode across the solenoid coil, causing back-EMF noise that disrupts the control circuit

Install the 1N4007 diode correctly in reverse parallel across the coil, as shown in section 3's diagram

Safety Notes

  • A door-lock control circuit falls into a critical safety category: a relay switching an inductive load (a solenoid) must have a flyback diode to prevent back-EMF spikes from damaging the relay or disturbing the MCU.

  • For a real door lock, always add a mechanical backup unlock mechanism (a physical key) to avoid being locked out during a power failure or firmware bug — this is a mandatory safety recommendation for any real electronic lock installation.

8. Summary

This project demonstrates two parallel authentication paths for an IoT door lock: in-person RFID and a remote app API, both sharing one central setLock() function to avoid conflicting states.

Possible extensions: store allowed UIDs on a server instead of hard-coding them, log the unlock history, or add two-factor authentication (RFID + PIN) for more sensitive areas.