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()