Breadboard
docs
Example project · Builds & projects

Keypad door lock (keypad + OLED)

Click 1 2 3 4 on the keypad, then # to unlock (the OLED prompts you and * clears a mistake). Behind the scenes it is a real embedded project: an ESP32 scans a 4x4 matrix keypad, edge-detects new presses, and drives an OLED lock screen showing UNLOCKED or DENIED — all in MicroPython.

The Keypad door lock (keypad + OLED) circuit as rendered by the simulator

How it works

A matrix keypad scanned the way every keypad ever is: four row pins (G0–G3) driven high one at a time, four column pins (G4–G7) read back, so 8 pins cover 16 keys. The script scans continuously, edge-detects fresh presses (canvas keys latch until clicked again, so it must), and builds an entry string: digits append, * clears, # submits. The right PIN (1234) prints UNLOCKED and swaps the OLED lock screen; anything else earns a DENIED.

What's on the bench

  • Battery
  • ESP32-C3
  • Keypad 4×4
  • OLED 128×64

How it's wired

  • Battery · posESP32-C3 · vin
  • Battery · negESP32-C3 · gnd
  • ESP32-C3 · g0Keypad 4×4 · r0
  • ESP32-C3 · g1Keypad 4×4 · r1
  • ESP32-C3 · g2Keypad 4×4 · r2
  • ESP32-C3 · g3Keypad 4×4 · r3
  • ESP32-C3 · g4Keypad 4×4 · c0
  • ESP32-C3 · g5Keypad 4×4 · c1
  • ESP32-C3 · g6Keypad 4×4 · c2
  • ESP32-C3 · g7Keypad 4×4 · c3
  • ESP32-C3 · 3v3OLED 128×64 · vcc
  • ESP32-C3 · gnd2OLED 128×64 · gnd
  • ESP32-C3 · g9OLED 128×64 · scl
  • ESP32-C3 · g8OLED 128×64 · sda

The code

This MicroPython script runs on the emulated board every boot; edit it in the Code tab.

from machine import I2C, Pin
import framebuf, time
i = I2C(0, scl=Pin(9), sda=Pin(8))
buf = bytearray(1024)
fb = framebuf.FrameBuffer(buf, 128, 64, framebuf.MONO_VLSB)
for c in b'\xae\xd5\x80\xa8\x3f\xd3\x00\x40\x8d\x14\x20\x00\xa1\xc8\xda\x12\x81\xcf\xd9\xf1\xdb\x40\xa4\xa6\xaf':
    i.writeto(0x3c, bytes([0, c]))
def show():
    i.writeto(0x3c, b'\x00\x21\x00\x7f\x22\x00\x07')
    i.writeto(0x3c, b'\x40' + buf)

# TRY IT: click 1 2 3 4 on the keypad, then # to unlock (* clears).
# 4x4 matrix keypad: rows drive, columns read (active-high scan).
rows = [Pin(n, Pin.OUT) for n in (0, 1, 2, 3)]
cols = [Pin(n, Pin.IN) for n in (4, 5, 6, 7)]
KEYS = ['123A', '456B', '789C', '*0#D']
def scan():
    pressed = set()
    for r in range(4):
        for x in range(4):
            rows[x].value(1 if x == r else 0)
        time.sleep_ms(2)  # let the row drive settle before reading
        for c in range(4):
            if cols[c].value():
                pressed.add(KEYS[r][c])
    return pressed

PIN = '1234'
entry = ''
msg = 'TRY 1234 THEN #'
prev = set()
def render():
    fb.fill(0)
    fb.text('SECURE LOCK', 20, 2)
    fb.text('CODE: ' + '*' * len(entry), 8, 24)
    fb.text(msg, 8, 46)
    show()
render()
print('lock ready -- click 1 2 3 4 on the keypad, then # to unlock (* clears)')
while True:
    cur = scan()
    for k in cur - prev:  # keys newly pressed since the last scan
        if k == '#':
            msg = 'UNLOCKED' if entry == PIN else 'DENIED'
            print('submit', entry, '->', msg)
            if msg == 'DENIED':
                print('hint: the code is 1234, then # to submit')
            entry = ''
        elif k == '*':
            entry, msg = '', 'CLEARED'
        elif k in '0123456789':
            entry = (entry + k)[:8]
            msg = 'THEN # TO SUBMIT' if entry else 'TRY 1234 THEN #'
    prev = cur
    render()
    time.sleep_ms(40)

Try this

  • Click 1 2 3 4 then # on the keypad: UNLOCKED, on the OLED and in serial.
  • Type something wrong and submit with # for the DENIED screen; * clears a fumbled entry.
  • Change PIN in the script to your own code and re-run.
  • Remember the keys latch: click a stuck key again to release it before the next entry.

Related projects