ESP32 Smart Door Lock

Smart HomeBeginnerIntermediateAdvanced

Create a keypad-controlled door lock with an ESP32 and servo motor, starting from a single-PIN beginner build through RFID and MQTT-connected multi-user access control.

Overview

In this beginner build you will wire a 4x4 matrix keypad and a servo motor to an ESP32 to create a PIN-code door lock. Entering the correct four-digit code rotates the servo to the unlock position and lights a green LED. A wrong PIN lights a red LED and activates a buzzer. The project teaches matrix keypad scanning, servo PWM control, and basic string comparison without any cloud connectivity.

Components
  • 1× ESP32 DevKit V1
  • 1× 4x4 matrix keypad membrane — 16-key, 8-pin ribbon cable
  • 1× SG90 servo motor — 180-degree rotation, 5 V
  • 1× Green LED 5 mm — Unlock indicator
  • 1× Red LED 5 mm — Wrong PIN indicator
  • 1× Active buzzer 5 V — Error alert
  • 2× 220 ohm resistor — Current limiting for LEDs
  • 1× Breadboard and jumper wires
Wiring
Component PinESP32 PinNotes
Keypad rows R1-R4GPIO 13,12,14,27Row pins driven LOW during scan
Keypad cols C1-C4GPIO 26,25,33,32Column pins with pull-up
Servo signal wireGPIO 18PWM; yellow wire on servo
Servo powerVin (5 V)Servo draws up to 250 mA
Green LED anodeGPIO 4Through 220 ohm resistor
Red LED anodeGPIO 2Through 220 ohm resistor
Buzzer +GPIO 15Active buzzer; drive HIGH to sound
Arduino Code
esp32-smart-door-lock_beginner.ino
// ESP32 Smart Door Lock - Beginner
// 4x4 Keypad + SG90 servo + LED feedback
#include <Keypad.h>
#include <ESP32Servo.h>

const char* CORRECT_PIN = "1234";
const byte ROWS=4, COLS=4;
char keys[4][4]={{"1","2","3","A"},{"4","5","6","B"},
                  {"7","8","9","C"},{"*","0","#","D"}};
byte rowPins[4]={13,12,14,27};
byte colPins[4]={26,25,33,32};
Keypad kpad=Keypad(makeKeymap(keys),rowPins,colPins,ROWS,COLS);

Servo lockServo;
const int LED_G=4, LED_R=2, BUZ=15, SERVO_PIN=18;
String entered="";

void unlock(){
  lockServo.write(90);
  digitalWrite(LED_G,HIGH);
  Serial.println("UNLOCKED");
  delay(3000);
  lockServo.write(0);
  digitalWrite(LED_G,LOW);
}

void deny(){
  digitalWrite(LED_R,HIGH);
  digitalWrite(BUZ,HIGH);
  delay(1000);
  digitalWrite(LED_R,LOW);
  digitalWrite(BUZ,LOW);
}

void setup(){
  Serial.begin(115200);
  pinMode(LED_G,OUTPUT);
  pinMode(LED_R,OUTPUT);
  pinMode(BUZ,OUTPUT);
  lockServo.attach(SERVO_PIN);
  lockServo.write(0);
}

void loop(){
  char k=kpad.getKey();
  if(!k) return;
  if(k=="#"){
    if(entered==CORRECT_PIN) unlock();
    else deny();
    entered="";
  } else if(k=="*"){
    entered="";
  } else {
    entered+=k;
    Serial.print("*");
  }
}
How It Works
01

Matrix Keypad Scanning: The Keypad library drives each row pin LOW in turn and reads the four column pins. When a key is pressed, the row and column intersection identifies which key was pressed.

02

PIN Accumulation: Each digit is appended to a String variable. Pressing # submits the PIN for comparison. Pressing * clears the buffer so the user can retype from scratch.

03

Servo Lock Control: The servo sits at 0 degrees in the locked position. A correct PIN rotates it to 90 degrees to retract the latch. After 3 seconds it returns to 0 degrees and locks automatically.

04

LED and Buzzer Feedback: A correct PIN lights the green LED for 3 seconds. A wrong PIN lights the red LED and activates the buzzer for 1 second, providing clear physical feedback without any display.

Applications
  • Home office door lock with PIN code entry
  • Cabinet lock for storing electronics components
  • Safe box release mechanism for workshops
  • STEM demonstration of electromechanical security
Troubleshooting

Keypad registers phantom key presses

Check that all 8 ribbon cable wires are fully seated. The Keypad library enables internal pull-ups on column pins automatically; verify no external resistors are conflicting.

Servo jitters or does not hold position

Power the servo from the Vin 5 V pin, not 3.3 V. Add a 10 uF capacitor between servo power and GND to absorb inrush current spikes.

PIN comparison always fails

Print the entered String to Serial Monitor and verify the characters. Check that the # key is mapped correctly in the keys[][] array definition.

Green LED does not light on correct PIN

Check LED polarity (flat side = cathode = GND). Verify the 220 ohm resistor is on the anode side between GPIO 4 and the LED.

Upgrades
  • Store the PIN in NVS flash so it survives power loss
  • Add an OLED display to show entry prompts and remaining attempts
  • Add a reed switch to detect if the door is open after unlocking
  • Add a second PIN slot for a family member using a different code
FAQ

You need an ESP32 DevKit, TODO: sensor, Keypad rows R1-R4, 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 Home Automation. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.

Overview

The intermediate build supports up to ten user PINs stored in ESP32 NVS flash, an OLED display for prompts and unlock history, a tamper lockout after three consecutive wrong attempts, and a local web page to add or delete PINs. All access events are timestamped using NTP and logged to Serial for a simple audit trail.

Components
  • 1× ESP32 DevKit V1
  • 1× 4x4 matrix keypad
  • 1× SG90 servo motor
  • 1× SSD1306 OLED 128x64 I2C — Shows prompts and last event
  • 1× Active buzzer — Tamper alert
  • 1× Green and red LED
  • 1× Wi-Fi router or access point — For NTP and web admin
Wiring
Component PinESP32 PinNotes
Keypad rows and colsSame as beginner
Servo signalGPIO 18
OLED SDAGPIO 21
OLED SCLGPIO 22
Green LEDGPIO 4220 ohm resistor
Red LEDGPIO 2220 ohm resistor
BuzzerGPIO 15
Arduino Code
esp32-smart-door-lock_intermediate.ino
// ESP32 Smart Door Lock - Intermediate
// Multi-PIN NVS + OLED + tamper lockout + web admin
#include <Keypad.h>
#include <ESP32Servo.h>
#include <Preferences.h>
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <WiFi.h>
#include <WebServer.h>
#include <time.h>

const char* SSID="YourSSID";
const char* PASS="YourPassword";

Preferences prefs;
Adafruit_SSD1306 oled(128,64,&Wire,-1);
WebServer server(80);
Servo lockServo;

const byte ROWS=4,COLS=4;
char keys[4][4]={{"1","2","3","A"},{"4","5","6","B"},
                  {"7","8","9","C"},{"*","0","#","D"}};
byte rp[4]={13,12,14,27}, cp[4]={26,25,33,32};
Keypad kpad=Keypad(makeKeymap(keys),rp,cp,ROWS,COLS);

String entered="";
int wrongCount=0;
bool lockedOut=false;
unsigned long lockEnd=0;

bool checkPIN(const String &pin){
  prefs.begin("pins",true);
  int n=prefs.getInt("count",0);
  for(int i=0;i<n;i++){
    if(prefs.getString(String(i).c_str(),"")==pin){
      prefs.end(); return true;
    }
  }
  prefs.end(); return false;
}

void logEvent(const String &msg){
  time_t now=time(nullptr);
  String ts=ctime(&now); ts.trim();
  oled.clearDisplay(); oled.setCursor(0,0);
  oled.println(msg); oled.println(ts); oled.display();
  Serial.println(msg+" "+ts);
}

void setup(){
  Serial.begin(115200);
  Wire.begin(21,22);
  oled.begin(SSD1306_SWITCHCAPVCC,0x3C);
  oled.setTextSize(1); oled.setTextColor(WHITE);
  lockServo.attach(18); lockServo.write(0);
  pinMode(4,OUTPUT);pinMode(2,OUTPUT);pinMode(15,OUTPUT);
  WiFi.begin(SSID,PASS);
  while(WiFi.status()!=WL_CONNECTED) delay(500);
  configTime(0,0,"pool.ntp.org");
  server.on("/",[](){
    server.send(200,"text/html",
      "<h2>Lock Admin</h2>"
      "<form method=POST action=/add><input name=pin><button>Add</button></form>"
      "<form method=POST action=/del><input name=pin><button>Del</button></form>");
  });
  server.begin();
  oled.clearDisplay(); oled.setCursor(0,0);
  oled.print("IP: "); oled.println(WiFi.localIP()); oled.display();
}

void loop(){
  server.handleClient();
  if(lockedOut){
    if(millis()<lockEnd) return;
    lockedOut=false; wrongCount=0;
  }
  char k=kpad.getKey();
  if(!k) return;
  if(k=="#"){
    if(checkPIN(entered)){
      wrongCount=0;
      logEvent("UNLOCKED");
      digitalWrite(4,HIGH); lockServo.write(90);
      delay(3000); lockServo.write(0); digitalWrite(4,LOW);
    } else {
      wrongCount++;
      logEvent("DENIED #"+String(wrongCount));
      digitalWrite(2,HIGH);digitalWrite(15,HIGH);
      delay(800); digitalWrite(2,LOW);digitalWrite(15,LOW);
      if(wrongCount>=3){ lockedOut=true; lockEnd=millis()+30000; }
    }
    entered="";
  } else if(k=="*"){ entered=""; }
  else { entered+=k; }
}
How It Works
01

NVS Multi-PIN Storage: PINs are stored as indexed keys (0, 1, 2...) in an NVS namespace "pins". The web admin page provides POST endpoints to add or delete PINs, updating a count key that controls the search loop.

02

NTP Timestamp Logging: configTime() syncs the ESP32 clock to pool.ntp.org over Wi-Fi. Each access event calls ctime() to format a human-readable timestamp that appears on the OLED and Serial log.

03

Tamper Lockout: After three wrong PINs the lockedOut flag is set and lockEnd is recorded 30 seconds ahead. The main loop skips keypad input until millis() passes lockEnd, then resets the wrong-count counter.

04

Web Admin Interface: WebServer on port 80 serves a minimal HTML form. Submitting a PIN to /add appends it to NVS. Submitting to /del searches NVS and removes the matching entry. The device IP is shown on the OLED at boot.

Applications
  • Rental property with per-guest PIN code management
  • Small office shared by multiple staff members
  • Home lab cabinet with an audit log of who accessed it
  • School storeroom with teacher-controlled PIN access
Troubleshooting

NVS PIN count resets unexpectedly

Confirm that prefs.begin() and prefs.end() are called in matching pairs everywhere PINs are read or written. Use the same namespace string "pins" in all calls.

Web admin page fails to load

Verify the ESP32 IP from the OLED and that the browser is on the same Wi-Fi subnet. Ensure WiFi.status()==WL_CONNECTED before starting the server.

NTP time shows 1970 epoch

Wait 5-10 seconds after Wi-Fi connects for NTP sync. Add a check: while(time(nullptr)<1000000000UL) delay(500); before logging events.

Upgrades
  • Add a DS3231 RTC for timekeeping when Wi-Fi is unavailable
  • Send a Telegram message on each unlock or lockout event
  • Add a door contact sensor and alert if the door stays open over 30 seconds
  • Implement HTTPS for the web admin page using a self-signed certificate
FAQ

You need an ESP32 DevKit, TODO: sensor, Keypad rows R1-R4, 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 Home Automation. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.

Overview

The advanced build adds an MFRC522 RFID reader alongside the keypad, supporting both card and PIN authentication. Access schedules restrict which cards are active during specific hours. All events are published to an MQTT broker for integration with Home Assistant or Node-RED. Remote lock and unlock commands are received on a dedicated MQTT topic, enabling app-based door control from anywhere on the local network.

Components
  • 1× ESP32 DevKit V1
  • 1× MFRC522 RFID reader module — SPI interface, 3.3 V logic only
  • 1× 4x4 matrix keypad
  • 1× MG996R servo motor — 11 kg/cm for heavier latches
  • 2× MIFARE Classic 1K RFID cards — 13.56 MHz, fixed 4-byte UID
  • 1× SSD1306 OLED 128x64
  • 1× Mosquitto MQTT broker — On Raspberry Pi or local server
Wiring
Component PinESP32 PinNotes
MFRC522 SDA/CSGPIO 5SPI chip select
MFRC522 SCKGPIO 18SPI clock
MFRC522 MOSIGPIO 23SPI data out
MFRC522 MISOGPIO 19SPI data in
MFRC522 RSTGPIO 27Reset pin
MFRC522 3.3 V3V3 onlyNever connect to 5 V
Keypad + servo + OLEDSame as intermediateGPIOs 18/19/23 shared with RFID SPI
Arduino Code
esp32-smart-door-lock_advanced.ino
// ESP32 Smart Door Lock - Advanced (RFID + MQTT + schedules)
#include <SPI.h>
#include <MFRC522.h>
#include <PubSubClient.h>
#include <WiFi.h>
#include <ArduinoJson.h>
#include <Preferences.h>
#include <time.h>

MFRC522 rfid(5, 27); // SS=5, RST=27

const char* SSID="YourSSID", *PASS="YourPass";
const char* MQTT_HOST="192.168.1.100";
const char* TOPIC_EVT="door/events";
const char* TOPIC_CMD="door/commands";

WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);
Preferences prefs;
bool remoteCmd=false;
bool remoteUnlock=false;

void publishEvent(const char* type, const char* id){
  StaticJsonDocument<128> doc;
  doc["type"]=type; doc["id"]=id; doc["ts"]=(long)time(nullptr);
  char buf[128]; serializeJson(doc,buf);
  mqtt.publish(TOPIC_EVT,buf);
}

void mqttCallback(char* topic, byte* payload, unsigned int len){
  String msg((char*)payload,len);
  remoteCmd=true;
  remoteUnlock=(msg=="UNLOCK");
}

bool cardAllowed(const String &uid){
  prefs.begin("rfid",true);
  int n=prefs.getInt("count",0);
  for(int i=0;i<n;i++){
    if(prefs.getString(String(i).c_str(),"")==uid){
      prefs.end(); return true;
    }
  }
  prefs.end(); return false;
}

void actuateServo(bool open){
  // servo.write(open ? 90 : 0);
  Serial.println(open ? "UNLOCK" : "LOCK");
}

void setup(){
  Serial.begin(115200);
  SPI.begin(); rfid.PCD_Init();
  WiFi.begin(SSID,PASS);
  while(WiFi.status()!=WL_CONNECTED) delay(500);
  configTime(0,0,"pool.ntp.org");
  mqtt.setServer(MQTT_HOST,1883);
  mqtt.setCallback(mqttCallback);
}

void loop(){
  if(!mqtt.connected()){
    mqtt.connect("ESP32DoorLock");
    mqtt.subscribe(TOPIC_CMD);
  }
  mqtt.loop();

  if(remoteCmd){
    actuateServo(remoteUnlock);
    publishEvent(remoteUnlock?"REMOTE_UNLOCK":"REMOTE_LOCK","app");
    remoteCmd=false;
  }

  if(rfid.PICC_IsNewCardPresent() && rfid.PICC_ReadCardSerial()){
    String uid="";
    for(byte i=0;i<rfid.uid.size;i++) uid+=String(rfid.uid.uidByte[i],HEX);
    rfid.PICC_HaltA();
    if(cardAllowed(uid)){
      actuateServo(true);
      publishEvent("RFID_UNLOCK",uid.c_str());
      delay(3000);
      actuateServo(false);
    } else {
      publishEvent("RFID_DENY",uid.c_str());
    }
  }
}
How It Works
01

RFID UID Reading: The MFRC522 communicates over SPI. PICC_IsNewCardPresent() detects a card in range and PICC_ReadCardSerial() fills rfid.uid.uidByte with the 4-7 byte unique identifier. The UID bytes are hex-formatted into a String for NVS lookup.

02

Allowed Card Registry: Authorised card UIDs are stored in NVS under namespace "rfid". cardAllowed() iterates through stored UIDs and returns true on a match. UIDs are added via a web endpoint or a registration mode triggered by a physical button.

03

MQTT Event Publishing: Every access attempt publishes a JSON payload to door/events containing the event type (RFID_UNLOCK, RFID_DENY, REMOTE_UNLOCK), credential ID, and Unix timestamp. Home Assistant or Node-RED can subscribe to trigger automations.

04

Remote Lock Commands: The ESP32 subscribes to door/commands. Receiving "UNLOCK" sets remoteCmd and remoteUnlock flags. The main loop calls actuateServo(true) and publishes a REMOTE_UNLOCK event for the audit log.

Applications
  • Smart home integration with Home Assistant MQTT
  • Airbnb property with per-stay RFID card provisioning
  • Office server room with employee badge access and logging
  • School lab with time-restricted student card schedules
Troubleshooting

RFID reader detects no card

Confirm 3.3 V supply (never 5 V). Verify SPI wiring: SCK=18, MOSI=23, MISO=19, SS=5, RST=27. Run the MFRC522 DumpInfo example to confirm the reader initialises.

MQTT connection drops repeatedly

Call mqtt.setKeepAlive(60) and ensure mqtt.loop() runs every loop iteration. Add a 30-second heartbeat publish to prevent broker-side idle disconnection.

Same card returns a different UID each tap

Some cards use UID randomisation. Use MIFARE Classic 1K cards with a fixed 4-byte UID for reliable registration.

Upgrades
  • Add an AS608 fingerprint sensor as a third authentication method
  • Integrate with Home Assistant via MQTT auto-discovery
  • Send a photo via Telegram on each access attempt using an ESP32-CAM
  • Implement time-of-day schedules to restrict certain cards to business hours only
FAQ

You need an ESP32 DevKit, TODO: sensor, Keypad rows R1-R4, 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 Home Automation. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.