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 Pin | ESP32 Pin | Notes |
|---|---|---|
| Keypad rows R1-R4 | GPIO 13,12,14,27 | Row pins driven LOW during scan |
| Keypad cols C1-C4 | GPIO 26,25,33,32 | Column pins with pull-up |
| Servo signal wire | GPIO 18 | PWM; yellow wire on servo |
| Servo power | Vin (5 V) | Servo draws up to 250 mA |
| Green LED anode | GPIO 4 | Through 220 ohm resistor |
| Red LED anode | GPIO 2 | Through 220 ohm resistor |
| Buzzer + | GPIO 15 | Active buzzer; drive HIGH to sound |
Arduino Code
// 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
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.
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.
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.
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 Pin | ESP32 Pin | Notes |
|---|---|---|
| Keypad rows and cols | Same as beginner | |
| Servo signal | GPIO 18 | |
| OLED SDA | GPIO 21 | |
| OLED SCL | GPIO 22 | |
| Green LED | GPIO 4 | 220 ohm resistor |
| Red LED | GPIO 2 | 220 ohm resistor |
| Buzzer | GPIO 15 |
Arduino Code
// 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
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.
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.
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.
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 Pin | ESP32 Pin | Notes |
|---|---|---|
| MFRC522 SDA/CS | GPIO 5 | SPI chip select |
| MFRC522 SCK | GPIO 18 | SPI clock |
| MFRC522 MOSI | GPIO 23 | SPI data out |
| MFRC522 MISO | GPIO 19 | SPI data in |
| MFRC522 RST | GPIO 27 | Reset pin |
| MFRC522 3.3 V | 3V3 only | Never connect to 5 V |
| Keypad + servo + OLED | Same as intermediate | GPIOs 18/19/23 shared with RFID SPI |
Arduino Code
// 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
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.
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.
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.
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.