PyRaspBeta

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:

PartPriceJob
4×4 keypad$4sixteen keys, scanned as four rows × four columns
Servo$5the bolt: 0° locked, 90° open
RGB LED$2red locked, green open, amber wrong code
220 Ω resistor × 33 × $0.25one 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.

circuit.py
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.GND
main.py
import 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.

circuit.py
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.GND
main.py
import 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 != last turns held keys into one event per press — otherwise the loop would count the same key dozens of times — and entered keeps only the last four keys.
  • On a correct code the servo goes to 90° (green light) and unlocked_until is 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 because AngularServo keeps 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 #: keep entered short 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