Keypad lock
A keypad lock is a whole access-control system in three parts: a matrix keypad to take a code, a servo whose horn is the bolt, and a status light that says locked or open. Type the right four digits and the servo swings to 90° for three seconds; a wrong code shakes the horn and flashes amber.
Every part is in the shop:
| Part | Price | Job |
|---|---|---|
| 4×4 keypad | $4 | sixteen keys, scanned as four rows × four columns |
| Servo | $5 | the bolt: 0° locked, 90° open |
| RGB LED | $2 | red locked, green open, amber wrong code |
| 220 Ω resistor × 3 | 3 × $0.25 | one per colour die |
Power the servo from 5 V (pi.V5), never from 3V3 or a GPIO: it needs 4.8–6 V, and a moving servo pulls about 250 mA against the 3V3 rail's 50 mA. The servo page has the current figures.
The fast version swaps the RGB LED and its three resistors for the RGB LED module kit ($4.50). No kit covers the keypad or the servo — those stay hand-wired — but the lamp is where the loose-parts wiring usually goes wrong, so that is the half worth buying.
The cheap version
The keypad has no power pins and no chip: all eight lines are GPIOs, four rows driven low one at a time and four columns read back through their internal pull-ups. The servo gets its signal from a PWM GPIO, and each RGB die gets a resistor before it.
from wiring import *
pi = Pi()
keypad = Keypad()
servo = Servo()
rgb = RGBLED("cathode")
keypad.r1 >> pi.GPIO17
keypad.r2 >> pi.GPIO27
keypad.r3 >> pi.GPIO22
keypad.r4 >> pi.GPIO23
keypad.c1 >> pi.GPIO24
keypad.c2 >> pi.GPIO25
keypad.c3 >> pi.GPIO5
keypad.c4 >> pi.GPIO6
servo.v_plus >> pi.V5 # 5 V: a moving servo draws hundreds of mA
servo.gnd >> pi.GND
servo.sig >> pi.GPIO18
pi.GPIO12 >> Resistor(220) >> rgb.red
pi.GPIO13 >> Resistor(220) >> rgb.green
pi.GPIO16 >> Resistor(220) >> rgb.blue
rgb.common >> pi.GNDimport RPi.GPIO as GPIO
from gpiozero import AngularServo, RGBLED
from time import monotonic, sleep
CODE = "1234"
UNLOCK_S = 3.0
ROWS, COLS = [17, 27, 22, 23], [24, 25, 5, 6]
KEYS = ["123A", "456B", "789C", "*0#D"]
GPIO.setmode(GPIO.BCM)
GPIO.setup(ROWS, GPIO.OUT, initial=GPIO.HIGH)
GPIO.setup(COLS, GPIO.IN, pull_up_down=GPIO.PUD_UP)
def scan():
for r, row in enumerate(ROWS):
GPIO.output(row, GPIO.LOW)
for c, col in enumerate(COLS):
if GPIO.input(col) == GPIO.LOW:
GPIO.output(row, GPIO.HIGH)
return KEYS[r][c]
GPIO.output(row, GPIO.HIGH)
return None
servo = AngularServo(18, min_angle=0, max_angle=180, min_pulse_width=0.0005, max_pulse_width=0.0024)
status = RGBLED(red=12, green=13, blue=16)
def show_locked():
servo.angle = 0
status.color = (1, 0, 0)
def shake():
for angle in (75, 105, 75, 105):
servo.angle = angle
sleep(0.1)
show_locked()
entered = ""
last = None
unlocked_until = 0.0
while True:
key = scan()
if key is not None and key != last:
entered = (entered + key)[-len(CODE):]
print("keys:", "*" * len(entered))
if entered == CODE:
print("unlocked")
servo.angle = 90
status.color = (0, 1, 0)
unlocked_until = monotonic() + UNLOCK_S
entered = ""
elif len(entered) == len(CODE):
print("wrong code")
status.color = (1, 1, 0)
shake()
show_locked()
entered = ""
last = key
if unlocked_until and monotonic() >= unlocked_until:
print("locked")
show_locked()
unlocked_until = 0.0
sleep(0.05)The fast version
The module kit is the same common-cathode RGB LED with its three 220 Ω resistors already fitted, so the four wires to the Pi replace the LED plus three resistors plus three hand-placed joints. The keypad and the servo keep the wiring and the code from the cheap version — there is no keypad or servo kit today, so nothing changes there.
from wiring import *
pi = Pi()
keypad = Keypad()
servo = Servo()
status = kits.RGBModule()
keypad.r1 >> pi.GPIO17
keypad.r2 >> pi.GPIO27
keypad.r3 >> pi.GPIO22
keypad.r4 >> pi.GPIO23
keypad.c1 >> pi.GPIO24
keypad.c2 >> pi.GPIO25
keypad.c3 >> pi.GPIO5
keypad.c4 >> pi.GPIO6
servo.v_plus >> pi.V5 # 5 V: a moving servo draws hundreds of mA
servo.gnd >> pi.GND
servo.sig >> pi.GPIO18
pi.GPIO12 >> status.red
pi.GPIO13 >> status.green
pi.GPIO16 >> status.blue
status.gnd >> pi.GNDimport RPi.GPIO as GPIO
from gpiozero import AngularServo, RGBLED
from time import monotonic, sleep
CODE = "1234"
UNLOCK_S = 3.0
ROWS, COLS = [17, 27, 22, 23], [24, 25, 5, 6]
KEYS = ["123A", "456B", "789C", "*0#D"]
GPIO.setmode(GPIO.BCM)
GPIO.setup(ROWS, GPIO.OUT, initial=GPIO.HIGH)
GPIO.setup(COLS, GPIO.IN, pull_up_down=GPIO.PUD_UP)
def scan():
for r, row in enumerate(ROWS):
GPIO.output(row, GPIO.LOW)
for c, col in enumerate(COLS):
if GPIO.input(col) == GPIO.LOW:
GPIO.output(row, GPIO.HIGH)
return KEYS[r][c]
GPIO.output(row, GPIO.HIGH)
return None
servo = AngularServo(18, min_angle=0, max_angle=180, min_pulse_width=0.0005, max_pulse_width=0.0024)
status = RGBLED(red=12, green=13, blue=16)
def show_locked():
servo.angle = 0
status.color = (1, 0, 0)
def shake():
for angle in (75, 105, 75, 105):
servo.angle = angle
sleep(0.1)
show_locked()
entered = ""
last = None
unlocked_until = 0.0
while True:
key = scan()
if key is not None and key != last:
entered = (entered + key)[-len(CODE):]
print("keys:", "*" * len(entered))
if entered == CODE:
print("unlocked")
servo.angle = 90
status.color = (0, 1, 0)
unlocked_until = monotonic() + UNLOCK_S
entered = ""
elif len(entered) == len(CODE):
print("wrong code")
status.color = (1, 1, 0)
shake()
show_locked()
entered = ""
last = key
if unlocked_until and monotonic() >= unlocked_until:
print("locked")
show_locked()
unlocked_until = 0.0
sleep(0.05)How it works
scan()drives one row low at a time and reads the four columns. With no key pressed every column floats high on its internal pull-up; a pressed key joins its row to its column, so the column of that crossing reads low. The keypad page walks through the matrix.key != lastturns held keys into one event per press — otherwise the loop would count the same key dozens of times — andenteredkeeps only the last four keys.- On a correct code the servo goes to 90° (green light) and
unlocked_untilis set three seconds ahead on the simulated clock (monotonic()), so the same loop relocks the door on time without a second thread. - A wrong four-digit code lights amber, then
shake()sweeps the horn 75°–105°–75°–105° before re-arming red. The servo holds every angle becauseAngularServokeeps the pulse train running; the part's own 0.5–2.4 ms pulse range is passed in so 0–180° uses the full travel. - The signal comes from GPIO18 (a hardware-PWM pin), but the horn's power comes from 5 V. That split — signal on a GPIO, current on
V+— is why the servo is wired pin by pin rather than chained.
Try it
Press Run: the status light is red and the console is quiet until you click a key. Click the keypad on the board (or set the keypad feed's Type by itself field to 1234) and the console prints keys: , keys: *, …; on the fourth match it prints unlocked, the servo swings to 90° and the light goes green, then after three seconds it prints locked and returns. Type a wrong code and the horn shakes over the amber light.
As a contest submission the judge types the code with key stimuli and checks what follows: the servo's angle (a state check) or the console lines, usually against a wrong code in between. Keep scan() fast — sleeping one loop at a time — so presses are not missed, and power the servo from 5 V or the judge's checks will see a weak, warning horn instead of a working lock.
Variations
- Submit with
#: keepenteredshort and only compare when#arrives, so a fat-fingered fourth digit does not shake the door before you finish. - Lockout: count wrong attempts and refuse input for ten seconds after three of them — the same
monotonic()deadline pattern as the unlock timer. - Beep on open: drive an active buzzer for a short pulse in the unlock branch, or a passive one with a two-tone chime.
Going deeper
- Input: the 4×4 keypad and the pull-up primer.
- Output: the servo (5 V power, pulse widths) and gpiozero's
AngularServoandRGBLED. - The scan loop is written in RPi.GPIO.
- Status light: RGB LED, PWM mixing and the RGB module kit.
- How a run works: circuit first, then
main.py.