A keypad box is a didactic lock that releases a latch when it recognizes a sequence of buttons.
This project is our first approach to Access Control Systems. The underlying logic is the same one you use to unlock your mobile phone with a PIN.
The technical challenge here is not turning on the motor, but information management:
- We need to store a “master password”.
- We need to listen to and temporarily save what the user presses (buffer).
- We need to compare both sequences and decide whether to open or deny access.
We will use a standard SG90 servomotor. The servo can hold a position while powered, but this assembly does not replace a security lock: a power cut, a plastic piece, or excessive force can defeat it.
The servo as a latch
To turn a cardboard or wooden box into a safe, we need a locking mechanism.
A Servomotor is ideal for this. Unlike a DC motor that spins freely, the servo moves to a specific angle (e.g., 0º or 90º) and stays there.
- 0º position (closed): The servo arm blocks the lid.
- 90º position (open): The arm retracts and releases the lid.
Components
- [1x|blue] Arduino Uno | The brain of the project
- [1x|green] Servomotor (SG90 or MG90S) | Locking mechanism
- [3x|orange] Pushbuttons | Enter the key
- [1x|green] Green LED | Indicates “Open”
- [1x|red] Red LED | Indicates “Error”
- [2x|gray] 220Ω Resistors | Limit LED current
- [Various|gray] Breadboard and wires | Connections
To enter the key, we will use 3 pushbuttons. This allows combinations like “1-1-2-3” or “3-2-1”.
Connection diagram
The assembly has two parts: the data input (buttons) and the output (Servo + LEDs).
Closing servo
The servo moves the latch. The signal goes to pin 9; the power can come from an external regulated source if the servo causes resets.
[ { “from”: “Servo Signal (Orange)”, “to”: “Pin 9”, “color”: “green” }, { “from”: “Servo VCC (Red)”, “to”: “External 5V source”, “color”: “red” }, { “from”: “Servo GND (Brown)”, “to”: “Source GND + Arduino GND”, “color”: “black” } ]
Keypad pushbuttons
The buttons use INPUT_PULLUP, so each pushbutton closes to GND.
[ { “from”: “Button 1”, “to”: “Pin 2”, “color”: “orange”, “note”: “INPUT_PULLUP, other side to GND” }, { “from”: “Button 2”, “to”: “Pin 3”, “color”: “orange”, “note”: “INPUT_PULLUP, other side to GND” }, { “from”: “Button 3”, “to”: “Pin 4”, “color”: “orange”, “note”: “INPUT_PULLUP, other side to GND” }, { “from”: “Other side of buttons”, “to”: “Arduino GND”, “color”: “black” } ]
Status LEDs
The LEDs show whether the key is correct or if an error has occurred.
[ { “from”: “Green LED + 220Ω”, “to”: “Pin 5”, “color”: “green” }, { “from”: “Red LED + 220Ω”, “to”: “Pin 6”, “color”: “red” }, { “from”: “LED cathodes”, “to”: “Arduino GND”, “color”: “black” } ]
The code
The critical part is the comparison logic. We cannot use a simple if to see if the correct button has been pressed, because a password is a sequence.
We will use two Arrays:
masterKey[]: The correct password.userAttempt[]: Where we will store what the user presses.
Each time the user presses a button, we save it in the attempt array and advance an index. When the user has pressed as many buttons as the key length, we compare the two arrays.
Configuration and constants
We need the Servo.h library to easily control the motor.
#include <Servo.h>
Servo myLock;
// --- Hardware Configuration ---
const int servoPin = 9;
const int greenLedPin = 5;
const int redLedPin = 6;
// Buttons to enter the key (Button 1, Button 2, Button 3)
const int buttonPins[] = {2, 3, 4};
const int NUM_BUTTONS = 3;
// --- Security Configuration ---
// Define the correct key: Button 1, Button 1, Button 3, Button 2
const int KEY_LENGTH = 4;
const int MASTER_KEY[KEY_LENGTH] = {0, 0, 2, 1};
// State variables
int userAttempt[KEY_LENGTH]; // Buffer to store what is pressed
int currentIndex = 0; // How many numbers have been pressed
bool isOpen = false;
setup
We initialize pins and place the servo in the “Closed” position at startup.
void setup() {
Serial.begin(9600);
// Configure LEDs
pinMode(redLedPin, OUTPUT);
pinMode(greenLedPin, OUTPUT);
// Configure Buttons with internal PULLUP
for(int i=0; i < NUM_BUTTONS; i++){
pinMode(buttonPins[i], INPUT_PULLUP);
}
// Configure servo and adopt initial closed state
myLock.attach(servoPin);
closeBox();
Serial.println("SYSTEM LOCKED. Enter key code.");
}
The main loop
In the loop, our mission is to detect presses. To prevent a single press from being registered multiple times (bouncing), we will use a small wait function.
void loop() {
if (!isOpen) {
// Only read buttons if the box is closed
readButtons();
} else {
// If open, wait for any button press to close
if (detectAnyButton()) {
closeBox();
}
}
}
void readButtons() {
for (int i = 0; i < NUM_BUTTONS; i++) {
// Read the button (LOW means pressed in Input Pullup)
if (digitalRead(buttonPins[i]) == LOW) {
// Feedback: quick flash of the red/yellow LED on press
digitalWrite(redLedPin, HIGH);
delay(100);
digitalWrite(redLedPin, LOW);
// Save the pressed button in the array (0, 1, or 2)
userAttempt[currentIndex] = i;
currentIndex++;
Serial.print("* "); // Print asterisk for security
// Wait for button release (Simple debounce)
while(digitalRead(buttonPins[i]) == LOW) { delay(10); }
// Have we reached the end of the key length?
if (currentIndex == KEY_LENGTH) {
verifyKey();
}
}
}
}
Verification logic
Here is where we compare the two arrays element by element.
void verifyKey() {
bool keyCorrect = true;
// Loop through both arrays comparing position by position
for (int i = 0; i < KEY_LENGTH; i++) {
if (userAttempt[i] != MASTER_KEY[i]) {
keyCorrect = false;
break; // If one fails, no need to keep checking
}
}
if (keyCorrect) {
openBox();
} else {
keyError();
}
// Reset the index for the next attempt
currentIndex = 0;
}
Open, close, and error actions
Finally, the functions that move the physical world.
void openBox() {
Serial.println(" -> KEY CORRECT. Opening...");
digitalWrite(greenLedPin, HIGH);
myLock.write(90); // Angle to unlock
isOpen = true;
delay(500); // Time for the servo to reach
}
void closeBox() {
Serial.println(" -> CLOSING...");
digitalWrite(greenLedPin, LOW);
myLock.write(0); // Angle to lock
isOpen = false;
currentIndex = 0; // Reset any half-finished attempt
delay(500);
}
void keyError() {
Serial.println(" -> ERROR. Incorrect key.");
// Error flashing
for(int i=0; i<3; i++){
digitalWrite(redLedPin, HIGH);
delay(100);
digitalWrite(redLedPin, LOW);
delay(100);
}
}
// Helper function to re-close
bool detectAnyButton() {
for (int i = 0; i < NUM_BUTTONS; i++) {
if (digitalRead(buttonPins[i]) == LOW) {
while(digitalRead(buttonPins[i]) == LOW) delay(10); // Wait for release
return true;
}
}
return false;
}
Code analysis
Notice an important detail: the initial logical state is closed. After configuring the outputs, setup() calls closeBox(). Even so, the program cannot confirm that the latch has physically reached its position; for that we would need a limit switch.
Furthermore, the use of arrays (userAttempt) acts as a buffer. We do not make the decision button by button, but accumulate the information until we have the complete package (the entire key) and then process it.
Possible improvements
This project is the perfect foundation to complicate it as much as you want:
- Temporary lockout: If you fail the key 3 times, ignore the buttons for 1 minute (to prevent brute force attacks… or annoying siblings!).
- Matrix Keypad: Replace the 3 buttons with a 4x4 numeric keypad to have real keys.
- Door sensor: Add a limit switch or magnetic sensor to know if the door is physically closed before turning the servo (and prevent the latch from hitting the frame).