Build a digital safe with a keypad, LCD and servo

A safe with a 4-digit PIN: type it on the keypad, watch the LCD, and a servo opens the latch. Change the PIN yourself, and three wrong tries lock the safe for 30 seconds.
  • Level:Intermediate
  • Duration: 2 h
  • Ages 12+
  • Platform: Arduino

Tools for this lesson

Helpful for this build. Open a tool to see tips and more projects that use it.

Build a digital safe with a keypad, LCD and servo – EEMENTOR STEM™ lesson

Hotel safes have a keypad, a small screen and a motor that moves the bolt. In this capstone you build all three. The LCD guides the user, the keypad takes the PIN, and a servo is the latch. The PIN is stored in the UNO's permanent memory, you can change it from the keypad, and three wrong guesses lock the safe for 30 seconds. Build it into a shoebox for a real hiding place.

What you'll learn

  • How to read a keypad with the Keypad library
  • How to collect key presses into a PIN and compare it
  • How to store a PIN in EEPROM and change it safely
  • How to use analog pins as extra digital pins when you run out

What you need

  • UNO R3 board and USB cable (UNO R3 Starter Kit)
  • 4×4 membrane keypad (UNO R3 Starter Kit)
  • 1602 LCD display, 10 kΩ potentiometer and 1 resistor, 220 Ω (UNO R3 Starter Kit)
  • Servo motor (UNO R3 Starter Kit)
  • Optional: a shoebox, and a straw or ice-lolly stick as the latch
  • Breadboard and jumper wires (UNO R3 Starter Kit)

Before you start

The LCD has 16 connections in a row. If yours has no pins, solder 16 pins from the 40-pin male header in the kit first, so it plugs into the breadboard. Ask an adult to help if you're new to soldering.

The keypad, the LCD and the servo need 15 pins, but the UNO has only 12 free digital pins (pins 0 and 1 are used by USB). The trick: the analog pins A0 to A5 also work as ordinary digital pins, so the keypad's four row wires go there.

Wiring

Breadboard wiring diagram: Build a digital safe with a keypad, LCD and servo
Breadboard view. Check each connection against the wiring table below.
From To
LCD VSS, RW and K GND
LCD VDD 5V
LCD V0 Middle pin (W) of the potentiometer
LCD RS UNO pin 12
LCD E UNO pin 11
LCD D4, D5, D6, D7 UNO pins 5, 4, 3, 2
LCD D0 to D3 Not connected
LCD A (backlight +) 220 Ω resistor, then 5V
Potentiometer outer pins 5V and GND
Keypad R1, R2, R3, R4 UNO pins A0, A1, A2, A3
Keypad C1, C2, C3, C4 UNO pins 10, 8, 7, 6
Servo orange / yellow wire UNO pin 9
Servo red wire 5V
Servo brown / black wire GND

The keypad's eight wires are in the order R1, R2, R3, R4, C1, C2, C3, C4 when you look at the keys from the front with the cable at the bottom.

How to use the safe

Key What it does
0 to 9 Type a digit
# Confirm (like Enter)
* Delete what you typed
A Change the PIN: type the old PIN, then the new one twice
C Close the latch when the safe is open

The first PIN is 1234. Change it straight away.

The sketch

Install the Keypad library (by Mark Stanley and Alexander Brevig) in the Arduino IDE. LiquidCrystal, Servo and EEPROM are built in.

#include <Keypad.h>
#include <LiquidCrystal.h>
#include <Servo.h>
#include <EEPROM.h>

// LCD pins: RS, E, D4, D5, D6, D7
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
Servo latch;

const byte ROWS = 4, COLS = 4;
char keys[ROWS][COLS] = {
  {'1', '2', '3', 'A'},
  {'4', '5', '6', 'B'},
  {'7', '8', '9', 'C'},
  {'*', '0', '#', 'D'}
};
byte rowPins[ROWS] = {A0, A1, A2, A3};
byte colPins[COLS] = {10, 8, 7, 6};
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);

const int SERVO_PIN = 9;
const int LOCKED_ANGLE = 0;
const int OPEN_ANGLE = 90;
const int MAGIC = 42;              // marks that a PIN has been saved

char pin[5] = "1234";
int wrongTries = 0;
bool isOpen = false;

void loadPin() {
  if (EEPROM.read(0) == MAGIC) {
    for (int i = 0; i < 4; i++) pin[i] = EEPROM.read(1 + i);
  }
}

void savePin(const char* newPin) {
  for (int i = 0; i < 4; i++) {
    pin[i] = newPin[i];
    EEPROM.update(1 + i, newPin[i]);   // writes only if the value changed
  }
  EEPROM.update(0, MAGIC);
}

void moveLatch(int angle) {
  latch.attach(SERVO_PIN);
  latch.write(angle);
  delay(500);
  latch.detach();
}

void message(const char* line1, const char* line2) {
  lcd.clear();
  lcd.print(line1);
  lcd.setCursor(0, 1);
  lcd.print(line2);
}

// Lets the user type 4 digits and press #. Returns false if they cancel (* on an empty line) or wait 20 s.
bool readPin(const char* prompt, char* result) {
  int count = 0;
  unsigned long start = millis();
  message(prompt, "");
  while (millis() - start < 20000) {
    char key = keypad.getKey();
    if (!key) continue;
    start = millis();
    if (key >= '0' && key <= '9' && count < 4) {
      result[count++] = key;
      lcd.setCursor(count - 1, 1);
      lcd.print('*');                 // show stars, not the digits
    } else if (key == '*') {
      if (count == 0) return false;   // * on an empty line cancels
      count = 0;
      message(prompt, "");
    } else if (key == '#' && count == 4) {
      result[4] = '\0';
      return true;
    }
  }
  return false;
}

void lockout() {
  for (int s = 30; s > 0; s--) {
    char line[17];
    snprintf(line, sizeof(line), "Wait %2d seconds", s);
    message("Too many tries!", line);
    delay(1000);
  }
  wrongTries = 0;
}

void changePin() {
  char oldPin[5], newPin[5], again[5];
  if (!readPin("Old PIN + #", oldPin)) return;
  if (strcmp(oldPin, pin) != 0) {
    message("Wrong PIN", "Not changed");
    delay(1500);
    return;
  }
  if (!readPin("New PIN + #", newPin)) return;
  if (!readPin("Repeat + #", again)) return;
  if (strcmp(newPin, again) == 0) {
    savePin(newPin);
    message("PIN changed", "");
  } else {
    message("PINs differ", "Not changed");
  }
  delay(1500);
}

void setup() {
  lcd.begin(16, 2);
  loadPin();
  moveLatch(LOCKED_ANGLE);
}

void loop() {
  if (isOpen) {
    message("Safe is OPEN", "C = close");
    char key = keypad.waitForKey();
    if (key == 'C') {
      moveLatch(LOCKED_ANGLE);
      isOpen = false;
    }
    return;
  }

  message("Enter PIN + #", "A = change PIN");
  char key = keypad.waitForKey();
  if (key == 'A') {
    changePin();
    return;
  }
  if (key < '0' || key > '9') return;

  // The first digit was already pressed; read the rest
  char attempt[5];
  attempt[0] = key;
  int count = 1;
  lcd.clear();
  lcd.print("Enter PIN + #");
  lcd.setCursor(0, 1);
  lcd.print('*');
  unsigned long start = millis();
  while (millis() - start < 20000) {
    char k = keypad.getKey();
    if (!k) continue;
    start = millis();
    if (k >= '0' && k <= '9' && count < 4) {
      attempt[count++] = k;
      lcd.print('*');
    } else if (k == '*') {
      return;                         // start again
    } else if (k == '#' && count == 4) {
      attempt[4] = '\0';
      if (strcmp(attempt, pin) == 0) {
        wrongTries = 0;
        message("Welcome!", "Opening...");
        moveLatch(OPEN_ANGLE);
        isOpen = true;
      } else {
        wrongTries++;
        message("Wrong PIN", wrongTries >= 3 ? "" : "Try again");
        delay(1500);
        if (wrongTries >= 3) lockout();
      }
      return;
    }
  }
}

Build it in stages

  1. Screen. Wire the LCD and the potentiometer and upload. The screen shows "Enter PIN + #". Turn the potentiometer until the text is sharp.
  2. Keypad. Connect the keypad. Each digit you press appears as a star. If the wrong keys respond, see the keypad lesson to check the wire order.
  3. Latch. Add the servo. Type 1234 and # to hear it open, then press C to close.
  4. Change the PIN. Press A and follow the screen.

How it works

The keypad is a grid of 4 rows and 4 columns. The Keypad library switches the rows one at a time and checks which column answers, which tells it exactly which key is down. getKey() returns a key once per press, or nothing, so the sketch can keep checking the clock while it waits. waitForKey() simply waits until a key is pressed.

The PIN is kept as text: four characters plus an invisible end marker '\0', which is how C strings end. strcmp() compares two strings and returns 0 when they are equal. The PIN is stored in EEPROM at addresses 1 to 4, with the value 42 at address 0 as a sign that a PIN has been saved. A brand-new UNO has random values there, so without that sign the safe would load a random PIN. EEPROM.update() writes only when the value changes, which saves the memory from wearing out.

Three wrong PINs start a 30-second countdown. That makes guessing slow: trying all 10,000 PINs would take more than 27 hours.

Try this next

  • Add the active buzzer: a short beep for each key and a long one for a wrong PIN.
  • Double the lockout time after every three more wrong tries.
  • Let the safe close by itself 30 seconds after it was opened.
  • Store a second "master" PIN that also works.

Troubleshooting

  • The screen lights up but shows only blocks or nothing: turn the potentiometer slowly from one end to the other.
  • The wrong digits appear: the keypad wires are in the wrong order. Check the order against the wiring table.
  • No keys respond: check that the row wires go to A0 to A3 and the column wires to 10, 8, 7 and 6.
  • The UNO restarts when the servo moves: use another USB port or a powered hub.
  • You forgot your PIN: upload this one-line sketch to clear it, then upload the safe again: #include <EEPROM.h> void setup() { EEPROM.write(0, 0); } void loop() {}. The PIN is back to 1234.

Build it yourself

Everything used in this lesson comes in one kit, with parts tested by our engineers.

UNO R3 Starter Kit | Arduino IDE Compatible | Free Tutorials $56.00

Shop the kit