DIY ELECTRONICSTR

← All projects

Code Door Lock

Intermediate50 min

A door lock that opens with a code typed on a 4x4 keypad: the LCD says what to do and shows the digits as *, and when the code is right the servo turns the latch and opens the door for 5 seconds while the screen counts down. After 3 wrong codes in a row the keypad is locked for 30 seconds. While the door is open, A changes the code; the new code is typed twice and saved in the Arduino's EEPROM memory, so it is kept even if the power goes. It teaches reading a keypad as a matrix, splitting a program into states (locked, open, waiting, changing the code), listening to keys while timing with millis(), and using memory that lasts. Install "Keypad" and "LiquidCrystal I2C" from the Library Manager; Servo and EEPROM come with the Arduino IDE. This is a learning project; do not use it as a real door's only lock.

Build it step by step in the app

What you need

Wiring steps

  1. 1

    Connect the power rails (5V and GND)

    Connect the power rails (5V and GND)

    Plug a red male-male jumper wire into the 5V pin on the Arduino's power header and connect its other end to the breadboard's top red (+) rail. Plug a black wire into one of the GND pins on the same header and connect its other end to the top blue (−) rail. The Uno has a single 5V pin; these two rails share 5V and GND between the LCD and the servo. That is all the breadboard is used for here.

    • Arduino 5V → the top red (+) rail (red wire)
    • Arduino GND → the top blue (−) rail (black wire)
  2. 2

    Connect the keypad

    Connect the keypad

    Push 8 male-male wires into the 8-hole socket at the end of the keypad's cable. With the keys facing you, the socket's first 4 pins from the left are the rows and the last 4 are the columns. Connect pin 1 to the Arduino's pin 9, pin 2 to 8, pin 3 to 7 and pin 4 to 6 (rowPins); pin 5 to 5, pin 6 to 4, pin 7 to 3 and pin 8 to 2 (colPins). The keypad needs no power of its own: the Keypad library scans the rows in turn and finds which key is pressed. Pins 0 and 1 stay free, because the Serial Monitor uses them over USB.

    • Arduino pin 9 → keypad socket pin 1 (green wire)
    • Arduino pin 8 → keypad socket pin 2 (green wire)
    • Arduino pin 7 → keypad socket pin 3 (green wire)
    • Arduino pin 6 → keypad socket pin 4 (green wire)
    • Arduino pin 5 → keypad socket pin 5 (blue wire)
    • Arduino pin 4 → keypad socket pin 6 (blue wire)
    • Arduino pin 3 → keypad socket pin 7 (blue wire)
    • Arduino pin 2 → keypad socket pin 8 (blue wire)
  3. 3

    Connect the LCD

    Connect the LCD

    Do not plug the screen into the breadboard; push the female ends of male-female jumper wires onto the four pins of the I2C module on its back. Connect GND to the top (−) rail and VCC to the top (+) rail. Connect SDA to the Arduino's A4 pin with a yellow wire and SCL to A5 with a green wire.

    • LCD GND → the top blue (−) rail (black wire)
    • LCD VCC → the top red (+) rail (red wire)
    • Arduino A4 → LCD SDA (yellow wire)
    • Arduino A5 → LCD SCL (green wire)
  4. 4

    Connect the servo and make a latch

    Connect the servo and make a latch

    Push three male-male wires into the three-hole plug at the end of the SG90's cable. Connect the one facing the brown (GND) wire to the top (−) rail, the one facing the red (5V) wire to the top (+) rail, and the one facing the orange (signal) wire straight to the Arduino's pin 10 (SERVO_PIN 10). Fit the servo's single arm onto its shaft after the code has turned the servo to 0° at start-up: at 0° the arm should hold the latch shut, at 90° it should free it. To try it, tape the servo inside the lid of a cardboard box; while the arm catches on the box's edge, the lid stays shut.

    • servo brown wire (GND) → the top blue (−) rail (black wire)
    • servo red wire (5V) → the top red (+) rail (red wire)
    • Arduino pin 10 → servo orange wire (signal) (orange wire)

Arduino sketch

// Install from the Library Manager:
// "Keypad" and "LiquidCrystal I2C"
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Keypad.h>
#include <Servo.h>
#include <EEPROM.h>

// The keypad: 4 rows, 4 columns
const byte ROWS = 4;
const byte COLS = 4;
char keys[ROWS][COLS] = {
  {'1', '2', '3', 'A'},
  {'4', '5', '6', 'B'},
  {'7', '8', '9', 'C'},
  {'*', '0', '#', 'D'}
};
// The socket's 8 pins left to right: the
// 4 rows first, then the 4 columns
byte rowPins[ROWS] = {9, 8, 7, 6};
byte colPins[COLS] = {5, 4, 3, 2};

const int SERVO_PIN = 10;
// The servo's locked and open angles
const int LOCKED_ANGLE = 0;
const int OPEN_ANGLE = 90;

// How long the door stays open (ms)
const unsigned long OPEN_TIME = 5000;
// After this many wrong codes in a row,
// make them wait
const int MAX_TRIES = 3;
const unsigned long BLOCK_TIME = 30000;
// While changing the code, give up after
// this long without a key (ms)
const unsigned long IDLE_TIME = 20000;

// Codes are 4-8 digits; 1234 at first
const byte MIN_LEN = 4;
const byte MAX_LEN = 8;
// Forgot the code? Change this number
// and upload: the code is 1234 again
const byte EEPROM_MARK = 42;

// I2C address: 0x27 on most, 0x3F on some
LiquidCrystal_I2C lcd(0x27, 16, 2);
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);
Servo lock;

enum State { LOCKED, OPEN, BLOCKED, NEW_CODE, CONFIRM_CODE };
State state = LOCKED;

char code[MAX_LEN + 1] = "1234";
char entry[MAX_LEN + 1] = "";
char newCode[MAX_LEN + 1] = "";
byte entryLen = 0;
int wrongTries = 0;
unsigned long stateStart = 0;
int shownSeconds = -1;

// EEPROM: byte 0 a mark, byte 1 the
// length, then the digits
void loadCode() {
  if (EEPROM.read(0) != EEPROM_MARK) return;
  byte len = EEPROM.read(1);
  if (len < MIN_LEN || len > MAX_LEN) return;
  for (byte i = 0; i < len; i++) {
    code[i] = EEPROM.read(2 + i);
  }
  code[len] = '\0';
}

void saveCode() {
  byte len = strlen(code);
  EEPROM.update(0, EEPROM_MARK);
  EEPROM.update(1, len);
  for (byte i = 0; i < len; i++) {
    EEPROM.update(2 + i, code[i]);
  }
}

// Both lines of the screen at once
void show(const char *top, const char *bottom) {
  lcd.clear();
  lcd.print(top);
  lcd.setCursor(0, 1);
  lcd.print(bottom);
}

// Show a short notice and wait
void message(const char *text) {
  show(text, "");
  delay(1500);
}

// New state; clear what was typed
void prompt(State next) {
  state = next;
  entryLen = 0;
  entry[0] = '\0';
  if (next == NEW_CODE) {
    show("New code:", "");
  } else if (next == CONFIRM_CODE) {
    show("Repeat code:", "");
  } else {
    show("Enter code:", "");
  }
}

// Count down at the bottom line's end
void showRemaining(unsigned long total) {
  int left = (total - (millis() - stateStart) + 999) / 1000;
  if (left == shownSeconds) return;
  shownSeconds = left;
  lcd.setCursor(12, 1);
  if (left < 10) lcd.print(' ');
  lcd.print(left);
  lcd.print("s ");
}

void startTimer(State next) {
  state = next;
  stateStart = millis();
  shownSeconds = -1;
}

void openDoor() {
  wrongTries = 0;
  lock.write(OPEN_ANGLE);
  startTimer(OPEN);
  show("Door open", "Closes in:");
  Serial.println("Right code: door opened.");
}

void lockDoor() {
  lock.write(LOCKED_ANGLE);
  prompt(LOCKED);
  Serial.println("Door locked.");
}

// Give up changing the code
void cancelChange() {
  prompt(LOCKED);
  Serial.println("Code change cancelled.");
}

void wrongCode() {
  wrongTries++;
  Serial.print("Wrong code, try ");
  Serial.println(wrongTries);
  if (wrongTries >= MAX_TRIES) {
    startTimer(BLOCKED);
    show("Too many tries", "Wait:");
    Serial.println("Too many tries: keys locked.");
    return;
  }
  show("Wrong code!", "Tries left:");
  lcd.setCursor(11, 1);
  lcd.print(MAX_TRIES - wrongTries);
  delay(1500);
  prompt(LOCKED);
}

// On #, act on what was typed
void submit() {
  if (state == LOCKED) {
    if (strcmp(entry, code) == 0) {
      openDoor();
    } else {
      wrongCode();
    }
  } else if (entryLen < MIN_LEN) {
    message("Min 4 digits!");
    prompt(state);
  } else if (state == NEW_CODE) {
    strcpy(newCode, entry);
    prompt(CONFIRM_CODE);
  } else if (strcmp(entry, newCode) == 0) {
    strcpy(code, newCode);
    saveCode();
    Serial.println("Code changed and saved.");
    message("Code changed");
    prompt(LOCKED);
  } else {
    Serial.println("The codes did not match.");
    message("No match!");
    prompt(NEW_CODE);
  }
}

void setup() {
  Serial.begin(9600);
  lcd.init();
  lcd.backlight();
  lock.attach(SERVO_PIN);
  lock.write(LOCKED_ANGLE);
  loadCode();
  prompt(LOCKED);
  Serial.println("Lock ready.");
}

void loop() {
  char key = keypad.getKey();

  if (state == OPEN) {
    // Locks when time is up or on #
    if (key == '#' || millis() - stateStart >= OPEN_TIME) {
      lockDoor();
    } else if (key == 'A') {
      // Lock first, then ask for the code
      lock.write(LOCKED_ANGLE);
      startTimer(NEW_CODE);
      prompt(NEW_CODE);
    } else {
      showRemaining(OPEN_TIME);
    }
    return;
  }

  if (state == BLOCKED) {
    // Keys are ignored while waiting
    if (millis() - stateStart >= BLOCK_TIME) {
      wrongTries = 0;
      prompt(LOCKED);
      Serial.println("Wait over.");
    } else {
      showRemaining(BLOCK_TIME);
    }
    return;
  }

  // While changing the code, every key
  // starts the time over
  if (state != LOCKED) {
    if (key) {
      stateStart = millis();
    } else if (millis() - stateStart >= IDLE_TIME) {
      cancelChange();
      return;
    }
  }

  if (!key) return;
  if (key >= '0' && key <= '9') {
    // Digits show on screen as *
    if (entryLen < MAX_LEN) {
      entry[entryLen++] = key;
      entry[entryLen] = '\0';
      lcd.setCursor(entryLen - 1, 1);
      lcd.print('*');
    }
  } else if (key == '*') {
    // Delete the last digit
    if (entryLen > 0) {
      entryLen--;
      entry[entryLen] = '\0';
      lcd.setCursor(entryLen, 1);
      lcd.print(' ');
    }
  } else if (key == '#') {
    submit();
  } else if (key == 'D' && state != LOCKED) {
    cancelChange();
  }
}

What the Serial Monitor shows

SERIAL MONITOR · 9600 BAUD
Lock ready.
Wrong code, try 1
Right code: door opened.
Code changed and saved.
Wrong code, try 1
Wrong code, try 2
Wrong code, try 3
Too many tries: keys locked.
Wait over.
Right code: door opened.
Door locked.

What you will see

At start-up the servo turns to 0° and the screen says "Enter code:"; the first code is 1234. A * appears on the bottom line for each digit you press; the * key deletes the last digit and # confirms. When the code is right the servo turns to 90°, the screen shows "Door open" and counts down from 5; when time is up or you press #, the door locks again. A wrong code shows how many tries are left; after 3 wrong codes in a row the keypad is locked for 30 seconds while the screen counts down. To change the code, press A while the door is open (it locks at once), type the new code (4-8 digits), press #, type it once more and press #; D gives up, and so do 20 seconds without a key. The new code is saved in EEPROM: it still works after you unplug the Arduino. If you forget the code, change the EEPROM_MARK number in the code and upload it again; the code is back to 1234. If the screen lights up but shows no text, turn the small trimmer on the I2C module; if there is still nothing, change 0x27 in the code to 0x3F. If the key you press is not the one on the screen, check the order of the keypad's wires; on some keypads the socket runs the other way, so swap the numbers in the rowPins and colPins arrays. If the screen flickers or the Arduino restarts when the servo turns, the servo is not getting enough current from USB: make sure the arm is not stuck, or power the servo from a separate 5V supply and join the GNDs.

In the app, tick off every wire, match the parts to your inventory and pick up where you left off.

Build it step by step in the app