Learning Goals 5 min
- Build a combination lock: three debounced buttons (A, B, C) plus a confirm button. Enter the correct N-button sequence to unlock; servo opens a latch on success.
- Store the 5-button combination in EEPROM so the lock survives power-cycles and can be re-programmed without re-flashing the sketch.
- Combine debouncing (L02-37), state machines (Cluster D / F), servo control (L02-26), and EEPROM (L02-39) into one ship-ready security device.
Warm-Up 10 min
You enter a combination on a real keypad lock every time you use a hotel room safe or a school locker. The Arduino version is fun because you control everything: what the combo is, what the lock does (servo opens? buzzer rewards? LED gives feedback?), how long the lockout lasts after three wrong tries. Today: build the simplest version that's actually usable.
Predict the state machine
Before coding, list the lock's states. What does it do while you're entering the combo? What happens after a wrong attempt? What happens after the right one?
One reasonable design
- LOCKED — servo at "closed" angle, status LED red, accepting button presses into a buffer.
- UNLOCKED — servo at "open" angle, status LED green, ignoring everything for 10 s before auto-relocking.
- WRONG — brief red flash + buzzer chirp, then back to LOCKED with the buffer cleared.
- LOCKOUT — after 3 wrong attempts in a row, a 30 s timeout where no input is accepted (red LED slow-blinks).
New Concept · Comparing sequences 20 min
The combination as an array
Store the secret as 5 numbers, one per button (A=1, B=2, C=3):
const int COMBO_LEN = 5;
byte combo[COMBO_LEN] = {1, 2, 3, 1, 2}; // A B C A B
byte buffer[COMBO_LEN]; // user input so far
int bufferLen = 0;Every button press pushes a number onto the buffer. When the buffer fills (5 entries) the lock compares it to the secret and decides.
The comparison
bool comboMatches() {
for (int i = 0; i < COMBO_LEN; i++) {
if (buffer[i] != combo[i]) return false;
}
return true;
}Loop, byte-by-byte, return false at the first mismatch. Beware: a careless implementation can leak timing information (a wrong first byte returns faster than a wrong last byte). For a school project this is fine; for production crypto you'd use constant-time comparison. We'll mention but not implement that.
EEPROM-backed secret
To allow re-programming the combination without re-flashing the sketch, store the secret in EEPROM:
const int COMBO_ADDR = 0; // 5 bytes starting at address 0
void loadCombo() {
for (int i = 0; i < COMBO_LEN; i++) {
combo[i] = EEPROM.read(COMBO_ADDR + i);
}
// First-boot check — if all bytes are 0xFF (unwritten), set a default
bool fresh = true;
for (int i = 0; i < COMBO_LEN; i++) if (combo[i] != 0xFF) fresh = false;
if (fresh) {
combo[0] = 1; combo[1] = 2; combo[2] = 3; combo[3] = 1; combo[4] = 2;
saveCombo();
}
}
void saveCombo() {
for (int i = 0; i < COMBO_LEN; i++) {
EEPROM.update(COMBO_ADDR + i, combo[i]);
}
}The "re-program" gesture
To set a new combo: long-press the confirm button while in UNLOCKED state. The next 5 presses get stored as the new combo. Saves a separate "program mode" switch.
Worked Example · The full lock 25 min
Step 1 — wiring
| Component | Pin |
|---|---|
| Button A (INPUT_PULLUP) | D2 |
| Button B | D3 |
| Button C | D4 |
| Confirm button (✓) | D5 |
| Status LED (red) | D6 |
| Status LED (green) | D9 |
| Buzzer | D8 |
| Servo signal | D11 |
Step 2 — the sketch
Save as combo-lock.ino:
#include <EEPROM.h>
#include <Servo.h>
const int BTN_A=2, BTN_B=3, BTN_C=4, BTN_OK=5;
const int RED=6, GREEN=9, BUZZ=8, SERVO_PIN=11;
const int LOCKED_ANGLE = 0, UNLOCKED_ANGLE = 90;
const int COMBO_LEN = 5;
const int COMBO_ADDR = 0;
byte combo[COMBO_LEN];
byte buffer[COMBO_LEN];
int bufferLen = 0;
int wrongAttempts = 0;
enum State { LOCKED, UNLOCKED, WRONG_STATE, LOCKOUT, PROGRAMMING };
State state = LOCKED;
unsigned long stateEnteredAt = 0;
Servo lock;
// ---- Debounced button (longPress flag included) ----
struct Btn {
int pin;
unsigned long lastChange, pressedAt;
int rawState, stableState, prevStable;
bool justPressed, longPressFired;
void begin(int p) {
pin = p; pinMode(p, INPUT_PULLUP);
rawState = stableState = prevStable = digitalRead(p);
lastChange = millis();
}
void tick() {
int n = digitalRead(pin);
if (n != rawState) { rawState = n; lastChange = millis(); }
justPressed = false;
if (millis() - lastChange >= 50 && rawState != stableState) {
prevStable = stableState; stableState = rawState;
if (stableState == LOW && prevStable == HIGH) {
justPressed = true; pressedAt = millis(); longPressFired = false;
}
}
if (stableState == LOW && !longPressFired && millis() - pressedAt >= 2000) {
longPressFired = true;
}
if (stableState == HIGH) longPressFired = false;
}
};
Btn a, b, c, ok;
void loadCombo() {
bool fresh = true;
for (int i = 0; i < COMBO_LEN; i++) {
combo[i] = EEPROM.read(COMBO_ADDR + i);
if (combo[i] != 0xFF) fresh = false;
}
if (fresh) {
// Set factory default 1-2-3-1-2
combo[0]=1; combo[1]=2; combo[2]=3; combo[3]=1; combo[4]=2;
for (int i = 0; i < COMBO_LEN; i++) EEPROM.update(COMBO_ADDR + i, combo[i]);
}
}
void enterState(State next) {
state = next;
stateEnteredAt = millis();
switch (next) {
case LOCKED:
bufferLen = 0;
lock.write(LOCKED_ANGLE);
digitalWrite(RED, HIGH); digitalWrite(GREEN, LOW);
Serial.println(">> LOCKED");
break;
case UNLOCKED:
wrongAttempts = 0;
lock.write(UNLOCKED_ANGLE);
digitalWrite(RED, LOW); digitalWrite(GREEN, HIGH);
tone(BUZZ, 1800, 100);
Serial.println(">> UNLOCKED");
break;
case WRONG_STATE:
tone(BUZZ, 300, 300);
digitalWrite(RED, HIGH);
Serial.println(">> WRONG");
break;
case LOCKOUT:
digitalWrite(RED, HIGH); digitalWrite(GREEN, LOW);
Serial.println(">> LOCKOUT (30 s)");
break;
case PROGRAMMING:
bufferLen = 0;
digitalWrite(GREEN, HIGH);
Serial.println(">> PROGRAMMING — enter new 5-press combo");
break;
}
}
void recordPress(int digit) {
if (state != LOCKED && state != PROGRAMMING) return;
if (bufferLen >= COMBO_LEN) return;
buffer[bufferLen++] = digit;
tone(BUZZ, 1500, 30);
if (bufferLen >= COMBO_LEN) {
if (state == PROGRAMMING) {
for (int i = 0; i < COMBO_LEN; i++) combo[i] = buffer[i];
for (int i = 0; i < COMBO_LEN; i++) EEPROM.update(COMBO_ADDR + i, combo[i]);
Serial.println(">> NEW COMBO SAVED");
enterState(LOCKED);
}
}
}
void checkSubmit() {
if (state != LOCKED || bufferLen != COMBO_LEN) {
enterState(WRONG_STATE);
return;
}
bool ok = true;
for (int i = 0; i < COMBO_LEN; i++) if (buffer[i] != combo[i]) ok = false;
if (ok) enterState(UNLOCKED);
else {
wrongAttempts++;
enterState(WRONG_STATE);
if (wrongAttempts >= 3) enterState(LOCKOUT);
}
}
void setup() {
pinMode(RED, OUTPUT); pinMode(GREEN, OUTPUT); pinMode(BUZZ, OUTPUT);
lock.attach(SERVO_PIN);
a.begin(BTN_A); b.begin(BTN_B); c.begin(BTN_C); ok.begin(BTN_OK);
Serial.begin(9600);
loadCombo();
enterState(LOCKED);
}
void loop() {
a.tick(); b.tick(); c.tick(); ok.tick();
if (a.justPressed) recordPress(1);
if (b.justPressed) recordPress(2);
if (c.justPressed) recordPress(3);
if (ok.justPressed) checkSubmit();
// long-press OK while UNLOCKED → enter PROGRAMMING
if (ok.longPressFired && state == UNLOCKED) {
ok.longPressFired = false;
enterState(PROGRAMMING);
}
// state timeouts
if (state == UNLOCKED && millis() - stateEnteredAt >= 10000)
enterState(LOCKED);
if (state == WRONG_STATE && millis() - stateEnteredAt >= 1500)
enterState(LOCKED);
if (state == LOCKOUT && millis() - stateEnteredAt >= 30000) {
wrongAttempts = 0;
enterState(LOCKED);
}
// LOCKOUT blink
if (state == LOCKOUT) {
bool on = ((millis() / 200) % 2) == 0;
digitalWrite(RED, on ? HIGH : LOW);
}
}Step 3 — try the factory default
On first boot the combo is 1-2-3-1-2. Press A, B, C, A, B, then OK. Servo opens, green LED, "UNLOCKED" in Serial. After 10 seconds it auto-relocks.
Step 4 — try a wrong combo
Press A, A, A, A, A, then OK. WRONG state, brief red flash, low buzzer. Try again, still wrong: WRONG again. Three wrongs in a row: LOCKOUT for 30 seconds, red LED blinks. No input accepted during lockout. After 30 s, back to LOCKED.
Step 5 — re-program the combo
Unlock normally (default combo). While in UNLOCKED state, hold OK for 2 seconds. State changes to PROGRAMMING. Enter a new 5-press sequence (e.g. C, C, A, B, A). After the 5th press, it saves and returns to LOCKED. Now the only way in is your new combo.
Step 6 — verify persistence
Unplug. Re-plug. Try the OLD combo → WRONG. Try the NEW combo → UNLOCKED. The EEPROM saved the new combo across the power cycle.
Try It Yourself 15 min
Goal: Add a "clear" button or a long-press of A to reset the buffer mid-entry (in case you press a wrong digit). The user shouldn't need to submit a wrong combo just to start over.
Hint
if (a.longPressFired && state == LOCKED) {
a.longPressFired = false;
bufferLen = 0;
Serial.println("(buffer cleared)");
}Goal: Add the LCD from Cluster E. Show the number of digits entered (0/5, 1/5, ... 5/5) and the lock state. Visual feedback while typing.
Hint
One row for state name ("LOCKED", "UNLOCKED", etc.), one for the progress bar. Use **--- style — stars for entered digits, dashes for remaining. Don't show the actual digits entered; that defeats the purpose.
Goal: Save the wrong-attempt count to EEPROM so it survives a power cycle. Otherwise an attacker can just unplug after 2 wrong attempts to dodge the LOCKOUT. Cumulative count → lockout still fires across reboots.
Hint
Reserve 1 byte at EEPROM address 10 for the attempt count. Save it every time you increment wrongAttempts. Load it in setup(). Reset it to 0 only after a successful unlock or after a completed LOCKOUT.
Mini-Challenge · Physical lock build 15 min
Mount the lock on a cardboard or wooden door. The servo's horn should physically prevent the door opening when in LOCKED_ANGLE and clear the way at UNLOCKED_ANGLE. Test it as a real device:
- Cut a small rectangular door from cardboard. Hinge it with tape.
- Mount the servo so its horn engages a notch on the door when at 0°.
- The door should be physically held shut while LOCKED and free to swing while UNLOCKED.
- Demo to a sibling/parent: give them a wrong combo, then the right combo. Time how long it takes them to figure out.
It's done when:
- The door actually resists being pulled open in LOCKED state.
- The lock's LEDs and beeps make the state obvious without any explanation.
- Three wrong attempts triggers a real cooldown that the tester has to wait through.
- Programming the combo via long-press works for a fresh user with no instructions beyond "press OK for 2 seconds while unlocked".
Recap 5 min
The Combination Lock is Cluster H's second integration build. The new architectural ideas are sequence buffer + comparison (the array of digits, the byte-by-byte equality check) and multi-state security UX (LOCKED / UNLOCKED / WRONG / LOCKOUT / PROGRAMMING with timeouts and gestures). Every primitive used today — debounced buttons (L02-37), state machine (L02-26), servo (L02-26), EEPROM (L02-39), buzzer (L01-14), LEDs (L01-08) — came from earlier lessons. The skill being learned is composing them into a coherent product. Tomorrow we'll build the third Cluster H integration: the Reaction-Tester Arcade with LCD scoreboard.
- Combination lock
- A device that opens only when the correct sequence of inputs is presented. Mechanical (rotating dial), electronic (keypad), or digital (button sequence as today).
- Sequence buffer
- The array of recent inputs that haven't yet been submitted for matching. Cleared after a successful or unsuccessful submission.
- Constant-time comparison
- A cryptographically safer comparison that takes the same number of operations regardless of where the mismatch occurs. Not used here (we're a teaching project), but standard practice in production crypto.
- Lockout
- A timed period after N failed attempts during which no input is accepted. Defeats brute-force guessing. 30 s is the consumer-product default; longer for high-security.
- Re-programmable secret
- The lock's combo lives in EEPROM and can be changed via a special gesture (here: long-press OK while unlocked). Without it, the only way to change the combo is to re-flash the sketch.
- Gesture-based UX
- Using button-press patterns (short, long, double) instead of dedicated mode switches. Saves hardware; requires the user to learn the gestures.
- Physical lock
- The actual mechanism that prevents physical access. Servo-driven cardboard is for learning; real locks use steel internals graded for the protection level required.
- Defence in depth
- The principle that security comes from multiple overlapping controls (combo, lockout, durability). One layer alone is rarely enough.
- State-locked input
- The lock only accepts presses in certain states (LOCKED, PROGRAMMING). In LOCKOUT or UNLOCKED, presses are ignored. Prevents users confusing the system in non-input modes.
Homework 5 min
Build & demo. Mount the lock on a real cardboard door as in the challenge. Then run a small UX experiment:
- Tell two siblings/friends/parents nothing about the lock except "here are the buttons; the combo is 1-2-3-1-2; unlock the door".
- Watch silently as they try.
- Note which buttons they pressed first, how long it took, whether they triggered LOCKOUT, whether they figured out the OK button.
- After 2 minutes (or success), debrief: ask what was confusing.
Then list three UX improvements you'd make based on the experiment. Examples: label the buttons, add a tutorial screen, change the buzzer pitch.
Bring back next class:
- Photo or short video of the working lock + door.
- Your two-tester observation notes.
- Your three UX-improvement ideas.
- Tomorrow: the Reaction-Tester Arcade — a polished version of L01-22 with LCD and EEPROM high score.