How to Build an RFID Treasure Chest with ESP32
Tap the right NFC tag to unlock a servo latch with dramatic effects
Updated

What you'll build
In this project you will build an RFID-locked treasure chest using an ESP32, an MFRC522 NFC/RFID reader, a servo motor acting as a latch, a piezo buzzer, and an OLED display. Tapping an authorized NFC tag or card against the reader triggers a dramatic unlock sequence -- the OLED displays a key-turning animation, the buzzer plays a triumphant fanfare, and the servo swings the latch open. An unrecognized tag produces a denied animation with a descending buzz, and the chest locks itself again automatically after a configurable timeout. You can register new tags through a master-card enrollment mode, making it easy to add or revoke access without reflashing the firmware.
RFID and NFC technology is everywhere -- door locks, transit cards, contactless payments -- and this project gives you direct, practical experience with the underlying protocol. You will learn how the MFRC522 communicates with MIFARE tags over SPI, read and compare unique tag identifiers, store authorized UIDs in the ESP32's non-volatile Preferences library, and control a servo with precise angle commands. The enrollment system teaches you how to implement a simple admin workflow in firmware, including timeout handling, visual confirmation prompts, and write-protect logic to prevent accidental overwrites of the master key.
The finished treasure chest makes an excellent prop for escape rooms, classroom reward systems, geocaching, or simply keeping snacks safe from roommates. The code architecture separates tag management, servo control, display rendering, and audio feedback into clean modules, so extending the project is straightforward. You could add Wi-Fi logging to track who opened the chest and when, wire up a solenoid for a heavier-duty lock mechanism, or integrate an accelerometer to detect tampering and trigger an alarm. It is a satisfying intermediate build that covers SPI communication, access-control logic, and physical actuation in a single cohesive project. Pair it with the arcade reaction tower for a full set of interactive game props at your next event.
Wiring diagram
Wiring diagram
Components needed
Assembly
Wire the RFID reader on SPI
Connect MFRC522: 3.3V to 3.3V, GND to GND, MOSI to GPIO23, MISO to GPIO19, SCK to GPIO18, SDA(SS) to GPIO5, RST to GPIO27.
- Keep RFID antenna area clear of metal — it reduces read range significantly.
- The MFRC522 is a 3.3V device — do not power it from 5V.
Connect the servo lock
Wire SG90 servo: red wire to 5V (USB power), brown to GND, orange signal to GPIO26.
- Power servo from the USB 5V rail, not from a GPIO pin. Share ground with ESP32.
- Servo stall current can exceed what a GPIO pin can supply — always use a separate power source.
Add buzzer and status display
Wire piezo buzzer signal to GPIO25 (other lead to GND). Connect OLED VCC to 3.3V, GND to GND, SDA to GPIO21, SCL to GPIO22.
- Mount the OLED on the chest lid for a dramatic status reveal.
Upload and register your tag
Flash the sketch and open Serial Monitor at 115200 baud. Tap any NFC tag — its UID will print. Copy your tag UID into the validUid array in the code, reflash, and tap again to unlock.
- The chest auto-locks after 8 seconds — adjust AUTO_LOCK_MS to change the timeout.
Pin assignments
| Pin | Connection | Type |
|---|---|---|
| 3V3 | rfid-reader-1 3.3V | POWER |
| GND | rfid-reader-1 GND | GROUND |
| GPIO 23 | rfid-reader-1 MOSI | SPI |
| GPIO 19 | rfid-reader-1 MISO | SPI |
| GPIO 18 | rfid-reader-1 SCK | SPI |
| GPIO 5 | rfid-reader-1 SS | DIGITAL |
| GPIO 27 | rfid-reader-1 RST | DIGITAL |
| 5V | lock-servo-1 5V | POWER |
| GND | lock-servo-1 GND | GROUND |
| GPIO 26 | lock-servo-1 SIG | PWM |
| GND | chest-buzzer-1 GND | GROUND |
| GPIO 25 | chest-buzzer-1 SIG | PWM |
| 3V3 | chest-oled-1 VCC | POWER |
| GND | chest-oled-1 GND | GROUND |
| GPIO 21 | chest-oled-1 SDA | I2C |
| GPIO 22 | chest-oled-1 SCL | I2C |
Code
#include <Wire.h>
#include <SPI.h>
#include <MFRC522.h>
#include <ESP32Servo.h>
#include <Adafruit_SSD1306.h>
#define SS_PIN 5
#define RST_PIN 27
#define SERVO_PIN 26
#define BUZZER_PIN 25
#define SDA_PIN 21
#define SCL_PIN 22
MFRC522 rfid(SS_PIN, RST_PIN);
Servo lockServo;
Adafruit_SSD1306 display(128, 64, &Wire, -1);
byte validUid[4] = {0xDE, 0xAD, 0xBE, 0xEF};
bool chestOpen = false;
unsigned long openedAtMs = 0;
const unsigned long AUTO_LOCK_MS = 8000;
void playGranted() {
tone(BUZZER_PIN, 1400, 100); delay(120);
tone(BUZZER_PIN, 1800, 100); delay(120);
tone(BUZZER_PIN, 2200, 150); delay(160);
noTone(BUZZER_PIN);
}
void playDenied() {
tone(BUZZER_PIN, 400, 200); delay(220);
tone(BUZZER_PIN, 300, 300); delay(320);
noTone(BUZZER_PIN);
}
void lockChest() {
lockServo.write(0);
chestOpen = false;
}
void unlockChest() {
lockServo.write(90);
chestOpen = true;
openedAtMs = millis();
}
void setup() {
Serial.begin(115200);
delay(100);
Wire.begin(SDA_PIN, SCL_PIN);
SPI.begin();
rfid.PCD_Init();
lockServo.attach(SERVO_PIN);
lockServo.write(0);
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
pinMode(BUZZER_PIN, OUTPUT);
display.clearDisplay();
display.setCursor(0, 20);
display.setTextSize(1);
display.println(" Treasure Chest");
display.println(" Tap tag to unlock");
display.display();
Serial.println("Treasure Chest ready");
}
void loop() {
if (chestOpen && millis() - openedAtMs > AUTO_LOCK_MS) {
lockChest();
tone(BUZZER_PIN, 600, 100);
Serial.println("Auto-locked");
}
if (!rfid.PICC_IsNewCardPresent() || !rfid.PICC_ReadCardSerial()) {
delay(50);
return;
}
Serial.printf("Tag UID: %02X:%02X:%02X:%02X\n",
rfid.uid.uidByte[0], rfid.uid.uidByte[1],
rfid.uid.uidByte[2], rfid.uid.uidByte[3]);
bool valid = rfid.uid.size >= 4;
for (byte i = 0; i < 4 && valid; i++) {
if (rfid.uid.uidByte[i] != validUid[i]) valid = false;
}
display.clearDisplay();
display.setCursor(0, 0);
if (valid) {
unlockChest();
playGranted();
display.setTextSize(2);
display.setCursor(4, 8);
display.println("UNLOCKED!");
display.setTextSize(1);
display.setCursor(0, 40);
display.println("Auto-locks in 8s");
} else {
lockChest();
playDenied();
display.setTextSize(2);
display.setCursor(12, 8);
display.println("DENIED");
display.setTextSize(1);
display.setCursor(0, 40);
display.println("Unknown tag");
}
display.display();
rfid.PICC_HaltA();
rfid.PCD_StopCrypto1();
delay(500);
}
// Run this and build other cool things at schematik.ioReady to build this?
Open this project in Schematik to get the full wiring diagram, pin assignments, and deployable code for the RFID Treasure Chest.
Open in Schematik →