Back to Projects

ESP32 Auto-Lock

Addressing the laziness factor: Enforcing physical security when humans fail.

The Human Flaw

Corporate security teams obsess over encryption and firewalls, yet the most common vulnerability is a simple human trait: laziness. Employees frequently step away from their desks without locking their screens, leaving sensitive data exposed to anyone walking by.

No matter how "secure" a company claims to be, these small operational mistakes undermine the entire infrastructure. This project removes the need for human discipline by using an ESP32 and ultrasonic sensors to detect when a desk is abandoned and automatically locking the workstation.

Implementation Code

The solution consists of an ESP32 publishing distance data via MQTT and a Python client monitoring that data to trigger the OS lock command.

1. Firmware (C++)

// ESP32 Sensor Logic
void loop() {
  long duration, distance;
  digitalWrite(trigPin, LOW);
  delayMicroseconds(2);
  digitalWrite(trigPin, HIGH);
  delayMicroseconds(10);
  digitalWrite(trigPin, LOW);

  duration = pulseIn(echoPin, HIGH);
  distance = (duration / 2) / 29.1;

  if (distance > 0 && distance < 400) {
    client.publish("office/presence/distance", String(distance).c_str());
  }
  delay(500);
}

2. Desktop Client (Python)

This full implementation includes a system tray icon, countdown GUI, and auto-lock logic.

import time
import threading
import collections
import os
import ctypes
import tkinter as tk
from tkinter import messagebox, simpledialog
import paho.mqtt.client as mqtt

import pystray
from pystray import MenuItem as item
from PIL import Image, ImageDraw

# ===================== CONFIG =====================

MQTT_BROKER = "192.168.1.xxx"
MQTT_TOPIC = "MQTT Topic"

DISTANCE_THRESHOLD_CM = 60      # CHANGE HERE
LOCK_DELAY_SEC = 10             # CHANGE HERE
EXTEND_TIME_SEC = 120

STARTUP_GRACE_SEC = 15
LOCK_COOLDOWN_SEC = 120
NO_DATA_WARNING_AFTER = 15

# ==================================================

distance_buffer = collections.deque()
last_data_time = 0
last_lock_time = 0
auto_lock_enabled = True
paused = False

unattended_since = None
startup_time = time.time()

# ===================== WINDOWS LOCK =====================

def lock_windows():
    os.system("rundll32.exe user32.dll,LockWorkStation")

# ===================== MQTT =====================

def on_message(client, userdata, msg):
    global last_data_time, unattended_since

    try:
        distance = float(msg.payload.decode())
    except ValueError:
        return

    now = time.time()
    last_data_time = now

    distance_buffer.append((now, distance))

def mqtt_thread():
    client = mqtt.Client()
    client.on_message = on_message
    client.connect(MQTT_BROKER, 1883)
    client.subscribe(MQTT_TOPIC)
    client.loop_forever()

# ===================== LOGIC =====================

def person_present():
    now = time.time()

    while distance_buffer and now - distance_buffer[0][0] > 10:
        distance_buffer.popleft()

    if len(distance_buffer) < 3:
        return None  # unknown

    distances = [d for _, d in distance_buffer]
    far = sum(d > DISTANCE_THRESHOLD_CM for d in distances)

    return far <= 2

# ===================== GUI =====================

root = tk.Tk()
root.title("Desk Presence Monitor")
root.geometry("420x260")
root.resizable(False, False)

status_label = tk.Label(root, text="", font=("Segoe UI", 14))
status_label.pack(pady=10)

icon_label = tk.Label(root, text="", font=("Segoe UI", 48))
icon_label.pack()

countdown_label = tk.Label(root, text="", font=("Segoe UI", 12))
countdown_label.pack(pady=5)

extend_btn = tk.Button(root, text="Extend lock by 120s")

def extend_lock():
    global unattended_since
    unattended_since = time.time()

extend_btn.config(command=extend_lock)

# ===================== MENU =====================

menu_bar = tk.Menu(root)

def change_distance():
    global DISTANCE_THRESHOLD_CM
    v = simpledialog.askinteger("Distance", "Distance threshold (cm):", initialvalue=DISTANCE_THRESHOLD_CM)
    if v:
        DISTANCE_THRESHOLD_CM = v

def change_delay():
    global LOCK_DELAY_SEC
    v = simpledialog.askinteger("Lock delay", "Lock delay (seconds):", initialvalue=LOCK_DELAY_SEC)
    if v:
        LOCK_DELAY_SEC = v

def toggle_lock():
    global auto_lock_enabled
    auto_lock_enabled = not auto_lock_enabled

settings_menu = tk.Menu(menu_bar, tearoff=0)
settings_menu.add_command(label="Change distance", command=change_distance)
settings_menu.add_command(label="Change lock delay", command=change_delay)
settings_menu.add_command(label="Enable / Disable auto-lock", command=toggle_lock)

menu_bar.add_cascade(label="Settings", menu=settings_menu)
root.config(menu=menu_bar)

# ===================== TRAY ICON =====================

def create_image():
    img = Image.new("RGB", (64, 64), "black")
    d = ImageDraw.Draw(img)
    d.ellipse((16, 8, 48, 40), fill="white")
    d.rectangle((28, 40, 36, 60), fill="white")
    return img

def on_restore(icon, item):
    root.after(0, root.deiconify)

def on_exit(icon, item):
    icon.stop()
    root.destroy()

def on_pause(icon, item):
    global paused
    paused = not paused

tray_icon = pystray.Icon(
    "DeskPresence",
    create_image(),
    menu=pystray.Menu(
        item("Restore", on_restore),
        item("Pause / Resume", on_pause),
        item("Exit", on_exit)
    )
)

def hide_to_tray():
    root.withdraw()
    threading.Thread(target=tray_icon.run, daemon=True).start()

root.protocol("WM_DELETE_WINDOW", hide_to_tray)

# ===================== UPDATE LOOP =====================

def update_ui():
    global unattended_since, last_lock_time

    now = time.time()

    if now - startup_time < STARTUP_GRACE_SEC:
        status_label.config(text="Starting up…")
        icon_label.config(text="⏳")
        countdown_label.config(text="")
        root.after(500, update_ui)
        return

    if now - last_data_time > NO_DATA_WARNING_AFTER:
        status_label.config(text="No presence data")
        icon_label.config(text="⚠️")
        countdown_label.config(text="Auto-lock disabled")
        root.after(1000, update_ui)
        return

    presence = person_present()

    if presence is None:
        status_label.config(text="Waiting for data…")
        icon_label.config(text="…")
        countdown_label.config(text="")
        root.after(500, update_ui)
        return

    if presence:
        unattended_since = None
        status_label.config(text="Person detected")
        icon_label.config(text="🧑‍💻")
        countdown_label.config(text="")
        extend_btn.pack_forget()
    else:
        if unattended_since is None:
            unattended_since = now

        remaining = LOCK_DELAY_SEC - int(now - unattended_since)

        status_label.config(text="PC unattended")
        icon_label.config(text="🥷💻")
        countdown_label.config(text=f"Locking in {max(0, remaining)}s")

        extend_btn.pack(pady=5)

        if remaining <= 0 and auto_lock_enabled and not paused:
            if now - last_lock_time > LOCK_COOLDOWN_SEC:
                lock_windows()
                last_lock_time = now
                unattended_since = None

    root.after(500, update_ui)

# ===================== START =====================

threading.Thread(target=mqtt_thread, daemon=True).start()
update_ui()
root.mainloop()