Community project

Smart Temperature Locker

Photo of Smart Temperature Locker
Generated with AI

Lân Phạm Vũ

Published November 10, 2026

The Smart Temperature Locker combines an ESP32 microcontroller with environmental sensing and servo-controlled locking to create a temperature-aware storage system. It monitors temperature and humidity with a DHT22 sensor, detects proximity with an HC-SR04 ultrasonic sensor, and displays real-time status on a 16x2 LCD screen. The system can lock or unlock via a servo motor and provides audio feedback through a piezo buzzer.

This guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions for building the locker on a breadboard. The included firmware adds WiFi connectivity with admin authentication, allowing remote status monitoring and control through a web interface. Builders will learn how to integrate multiple sensors, manage separate power rails for 3.3V logic and 5V peripherals using a level shifter, and implement a secure OTP-based access system.

Wiring diagram

Loading diagram…

Gather all the parts

QtyComponent
1

DHT22

Digital temperature and humidity sensor

1

Resistor

4.7k ohm

Through-hole resistor (current-limiting in series with an LED)

1

SG90 Servo

Micro servo motor (SG90)

1

Breadboard-friendly SPDT Slide Switch

Compact through-hole SPDT slide switch with three pins for direct breadboard insertion.

1

USB-C 5V Adapter

USB-C wall adapter delivering regulated 5 V to the board's USB or VBUS rail. Default wired power source for desktop / stationary projects.

1

Electrolytic Capacitor

1000uF 10V

Radial aluminium electrolytic capacitor bought by value and voltage rating, such as 10 µF, 100 µF or 1000 µF 10 V. It is polarised: the + lead goes to the higher voltage and the - lead, marked by the stripe on the sleeve, to the lower voltage or ground. It is mostly used for bulk filtering across a power rail, with a voltage rating comfortably above the rail.

1

BSS138 4-channel level shifter

BSS138 4-channel level shifter; explicit physical terminals.

1

Breadboard 830 points

A full-size solderless breadboard with 830 tie points (MB-102 size), with socket grids on both sides of a centre channel and red and blue power rails along the long edges. LEDs, buttons, resistors, potentiometers, buzzers and jumper wires push into it without soldering. In a generated housing it rests in a screwless tray and carries those small parts inside the case.

1

HC-SR04

HC-SR04 ultrasonic distance sensor module with separate 40 kHz transmitter and receiver transducers, measuring 2 to 400 cm. A 10 us pulse on TRIG starts a measurement and ECHO stays high for the echo's round-trip time. This HC-SR04+ board runs from 3.3 V or 5 V.

1

Piezo Buzzer

Small passive piezo buzzer (TDK PS1240) for beeps, tones and alerts. It has no built-in oscillator, so drive one lead with a 3-30 V square wave from a microcontroller pin using tone() or PWM and connect the other lead to ground. It is loudest around 4 kHz and works from about 2 to 10 kHz; driving both leads in antiphase roughly doubles the volume.

1

LCD 16x2 I2C

16x2 character LCD display with I2C backpack

1

1N4001 Flyback Diode

General-purpose silicon rectifier diode used as a flyback / freewheeling diode to suppress back-EMF voltage spikes from inductive loads (DC motors, relays). Rated 50 V reverse voltage and 1 A forward current. Connected in parallel across the motor terminals with cathode (striped band) to the motor positive (+) / 5 V supply side and anode to the motor negative (−) / transistor collector side. Requires no firmware or library — purely passive protection component. Higher-voltage 1N400x rectifier variants are compatible substitutes when the current and package fit the design.

1

Resistor

1k ohm

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

2k ohm

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

220 ohm

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

220 ohm

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

10k ohm

Through-hole resistor (current-limiting in series with an LED)

1

LED

blue

Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.

1

LED

red

Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.

1

DFRobot DFR0017 relay HIGH active

DFRobot DFR0017 relay HIGH active; explicit physical terminals.

1

Adafruit 711 two-wire 130 motor

Adafruit 711 two-wire 130 motor; explicit physical terminals.

1

BSS138 4-channel level shifter

BSS138 4-channel level shifter; explicit physical terminals.

Assemble it in 6 steps

1. Bố trí quanh breadboard

Đặt breadboard nằm ngang ở giữa, ESP32 bên trái với cổng USB quay ra ngoài. DHT22 và HC-SR04 phía trên, LCD phía trên bên phải. Relay và motor phía phải, servo phía dưới phải. LED, công tắc và buzzer phía dưới. Hai bộ chuyển mức nằm gần LCD và các dây tín hiệu 5V. Đi dây dọc mép, rẽ vuông góc và dùng dây ngắn; cách bố trí này là hướng dẫn lắp, không phải tọa độ 3D được cố định.

2. Tách hai đường nguồn

Rãnh dương trên lấy 3V3 ESP32. Rãnh dương dưới lấy nguồn adapter 5V 2A riêng. Nối các rãnh GND với GND ESP32 và adapter. Kiểm tra rãnh nguồn có bị ngắt ở giữa bằng đồng hồ. Tụ 1000uF nối cực cộng vào 5V, cực trừ vào GND gần servo. Dây cấp motor và servo đi trực tiếp từ nguồn, không dồn dòng qua tiếp điểm breadboard.

  • Không nối nguồn 5V riêng với VIN khi ESP32 đang cắm USB. Không nối hai rãnh dương 3.3V và 5V với nhau. Tụ ngược cực có thể hỏng.

3. Đấu hai cảm biến

DHT22 VCC vào 3V3, GND vào GND, DATA vào GPIO4; điện trở 4.7k nối DATA lên 3V3. HC-SR04 VCC vào 5V, GND chung. GPIO25 qua LV1/HV1 của controlLevels đến TRIG. ECHO vào điện trở 1k rồi đến nút GPIO18; điện trở 2k từ nút này xuống GND, tạo khoảng 3.33V khi ECHO là 5V. Chỉ dùng nguồn 5V ổn áp, điện trở 1 phần trăm. Các chân NC của DHT để hở.

4. Đấu chuyển mức và màn hình

Cả hai module BSS138 có LV vào 3V3, HV vào 5V, GND chung. Trên levels, LV1 vào GPIO21, HV1 vào SDA LCD; LV2 vào GPIO22, HV2 vào SCL LCD. LCD cấp 5V. LV3 vào GPIO13, HV3 đến tín hiệu servo. LV4 vào GPIO14, HV4 vào IN relay. Điện trở 10k kéo GPIO14 xuống GND. Kênh còn lại của controlLevels để hở.

  • Không nối SDA hoặc SCL có kéo lên 5V trực tiếp vào ESP32.

5. Đấu motor và khóa

Relay DFR0017 kích mức HIGH, VCC vào 5V và GND chung. COM vào 5V, NO vào cực cộng motor, cực trừ motor vào GND. Diode mắc song song motor với đầu có vạch vào cực cộng, đầu không vạch vào cực trừ. NC để hở. Servo cấp 5V và GND chung; thử góc 10 độ khóa và 90 độ mở khi chưa gắn tay đòn.

  • Chỉ dùng tải DC 5V cho mô hình
  • không đấu điện lưới. Che trục quay và tránh kẹt servo.

6. Đấu báo trạng thái

GPIO23 qua 220 ohm đến anode LED xanh, cathode xuống GND; GPIO19 qua 220 ohm đến anode LED đỏ, cathode xuống GND. Công tắc COM vào GPIO27, A xuống GND, B để hở; gạt nối đất nghĩa là cửa đóng. Buzzer piezo thụ động SIG vào GPIO26, GND xuống GND.

  • Hai LED chỉ mô phỏng làm mát và làm ấm
  • không có sò Peltier thật. Quạt không làm lạnh dưới nhiệt độ phòng.

Review all connections

1. Connections between "levels" and "ESP32"

FunctionlevelsESP32
powerLV3V3
powerHV5V
groundGNDGND
dataLV1GPIO 21
dataHV1 → LCD 16x2 I2C SDAEXT
dataLV2GPIO 22
dataHV2 → LCD 16x2 I2C SCLEXT
dataLV3GPIO 13
dataHV3 → SG90 Servo SIGNALEXT
dataLV4GPIO 14
dataHV4 → DFRobot DFR0017 relay HIGH active INEXT

2. Connections between "controlLevels" and "ESP32"

FunctioncontrolLevelsESP32
powerLV3V3
powerHV5V
groundGNDGND
dataLV1GPIO 25
dataHV1 → HC-SR04 TRIGEXT

3. Connections between "temp" and "ESP32"

FunctiontempESP32
powerVCC3V3
groundGNDGND
dataDATAGPIO 4

4. Connections between "sonar" and "ESP32"

FunctionsonarESP32
powerVCC5V
groundGNDGND
digitalECHO → Resistor P1EXT

5. Connections between "lock" and "ESP32"

FunctionlockESP32
powerVCC5V
groundGNDGND

6. Connections between "lcd" and "ESP32"

FunctionlcdESP32
powerVCC5V
groundGNDGND

7. Connections between "relay" and "ESP32"

FunctionrelayESP32
powerVCC5V
groundGNDGND
powerCOM5V
powerNO → Adafruit 711 two-wire 130 motor PLUSEXT

8. Connections between "supply" and "ESP32"

FunctionsupplyESP32
power+5V5V
groundGNDGND

9. Connections between "bulk" and "ESP32"

FunctionbulkESP32
power+5V
ground-GND

10. Connections between "pullup" and "ESP32"

FunctionpullupESP32
digitalP1 → DHT22 DATAEXT
powerP23V3

11. Connections between "echoTop" and "ESP32"

FunctionechoTopESP32
digitalP2GPIO 18

12. Connections between "echoBottom" and "ESP32"

FunctionechoBottomESP32
digitalP1 → Resistor P2EXT
groundP2GND

13. Connections between "relayPull" and "ESP32"

FunctionrelayPullESP32
digitalP1 → BSS138 4-channel level shifter LV4EXT
groundP2GND

14. Connections between "door" and "ESP32"

FunctiondoorESP32
digitalCOMGPIO 27
groundAGND

15. Connections between "buzzer" and "ESP32"

FunctionbuzzerESP32
pwmSIGGPIO 26
groundGNDGND

16. Connections between "blueR" and "ESP32"

FunctionblueRESP32
digitalP1GPIO 23
digitalP2 → LED ANODEEXT

17. Connections between "blue" and "ESP32"

FunctionblueESP32
groundGNDGND

18. Connections between "redR" and "ESP32"

FunctionredRESP32
digitalP1GPIO 19
digitalP2 → LED ANODEEXT

19. Connections between "red" and "ESP32"

FunctionredESP32
groundGNDGND

20. Connections between "motor" and "ESP32"

FunctionmotorESP32
groundMINUSGND

21. Connections between "diode" and "ESP32"

FunctiondiodeESP32
passiveCathode → Adafruit 711 two-wire 130 motor PLUSEXT
passiveAnode → Adafruit 711 two-wire 130 motor MINUSEXT

Deploy the firmware

// Smart Locker: 3.3V logic with isolated external 5V load rail.
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <DHT.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <ESP32Servo.h>
#include <Preferences.h>
#include <esp_system.h>
#include "locker_ui.h"
DHT sensor(4,DHT22);
LiquidCrystal_I2C lcd(0x27,16,2);
float distanceCm=NAN;
String previousLine1,previousLine2;
Servo latch;
WebServer server(80);
Preferences prefs;
const char* admin="locker-demo-2026";
String state="empty", otp="", category="normal";
float temperature=NAN, humidity=NAN, target=30;
bool fan=false, closed=false, rawClosed=false, lastRaw=false, openingSeen=false;
uint32_t edge=0, sample=0, issued=0, unlockedAt=0, blockedAt=0;
int failures=0;
bool pending=false, blocked=false;
void save(){prefs.putString("state",state); prefs.putString("category",category);}
void reply(int code,String text){server.send(code,"application/json",text);}
bool auth(){if(server.header("X-Admin-Key")==admin)return true;reply(403,"{\"error\":\"admin_key\"}");return false;}
void mint(){char b[7];snprintf(b,sizeof(b),"%06lu",(unsigned long)(100000+esp_random()%900000));otp=b;issued=millis();failures=0;}
void status(){
 String j="{\"state\":\""+state+"\",\"category\":\""+category+"\",\"temperature\":";
 j+=isnan(temperature)?String("null"):String(temperature,2);
 j+=",\"humidity\":"+(isnan(humidity)?String("null"):String(humidity,1));
 j+=",\"distanceCm\":"+(isnan(distanceCm)?String("null"):String(distanceCm,1));
 // Demo occupancy threshold: calibrate to the actual empty compartment.
 j+=",\"packagePresent\":"+(isnan(distanceCm)?String("null"):String(distanceCm<20?"true":"false"));
 j+=",\"target\":"+String(target)+",\"fan\":"+(fan?"true":"false")+",\"doorClosed\":"+(closed?"true":"false")+",\"sensorFault\":"+(isnan(temperature)?"true":"false")+"}";
 reply(200,j);
}
void setup(){
 Serial.begin(115200);pinMode(14,OUTPUT);digitalWrite(14,LOW);pinMode(23,OUTPUT);pinMode(19,OUTPUT);pinMode(26,OUTPUT);pinMode(25,OUTPUT);digitalWrite(25,LOW);pinMode(18,INPUT);pinMode(27,INPUT_PULLUP);
 closed=lastRaw=digitalRead(27)==LOW;
 latch.setPeriodHertz(50);latch.attach(13,500,2400);latch.write(closed?10:90);
 prefs.begin("locker",false);state=prefs.getString("state","empty");category=prefs.getString("category","normal");target=category=="ventilated"?28:30;
 // Interrupted transactions need admin inspection; never infer delivery from reboot.
 if(state!="empty" && state!="waiting")state="recovery";
 sensor.begin();Wire.begin(21,22);Wire.setClock(100000);lcd.init();lcd.backlight();
 WiFi.mode(WIFI_AP);WiFi.softAP("SmartLocker-Demo","LockerDemo2026");
 const char* headers[]={"X-Admin-Key"};server.collectHeaders(headers,1);
 server.on("/",HTTP_GET,[](){server.send_P(200,"text/html; charset=utf-8",LOCKER_UI);});
 server.on("/status",HTTP_GET,status);
 server.on("/deposit",HTTP_POST,[](){
  if(!auth())return;
  if(state!="empty"||!closed){reply(409,"{\"error\":\"not_empty_or_door_open\"}");return;}
  category=server.arg("category");if(category!="normal"&&category!="ventilated"){reply(400,"{\"error\":\"category\"}");return;}
  target=category=="ventilated"?28:30;state="loading";openingSeen=false;unlockedAt=millis();latch.write(90);save();status();
 });
 server.on("/otp",HTTP_POST,[](){if(!auth())return;if(state!="waiting"){reply(409,"{}");return;}mint();reply(200,"{\"otp\":\""+otp+"\",\"expiresSeconds\":300}");});
 server.on("/unlock",HTTP_POST,[](){
  if(blocked){reply(429,"{\"error\":\"wait_60_seconds\"}");return;}
  if(state!="waiting"||otp.length()!=6||millis()-issued>=300000){otp="";reply(403,"{\"error\":\"expired_or_missing\"}");return;}
  if(server.arg("otp")!=otp){if(++failures>=5){blocked=true;blockedAt=millis();otp="";}reply(403,"{\"error\":\"wrong_otp\"}");return;}
  otp="";state="collecting";openingSeen=false;unlockedAt=millis();latch.write(90);save();status();
 });
 server.on("/reset",HTTP_POST,[](){if(!auth())return;if(!closed){reply(409,"{}");return;}state="empty";otp="";latch.write(10);save();status();});
 server.begin();Serial.println("WiFi SmartLocker-Demo / LockerDemo2026; http://192.168.4.1");
}
void loop(){
 server.handleClient();uint32_t now=millis();
 rawClosed=digitalRead(27)==LOW;if(rawClosed!=lastRaw){lastRaw=rawClosed;edge=now;}if(now-edge>80)closed=rawClosed;
 if(blocked&&now-blockedAt>=60000){blocked=false;failures=0;}
 if(state=="loading"||state=="collecting"){
  if(!closed)openingSeen=true;
  if(closed && openingSeen){latch.write(10);state=state=="loading"?"waiting":"empty";save();}
  else if(closed && now-unlockedAt>30000){latch.write(10);state=state=="loading"?"empty":"waiting";save();}
 }
 if(now-sample>=2500){
  sample=now;temperature=sensor.readTemperature();humidity=sensor.readHumidity();
  digitalWrite(25,HIGH);delayMicroseconds(10);digitalWrite(25,LOW);
  unsigned long duration=pulseIn(18,HIGH,25000);distanceCm=duration?duration/58.0f:NAN;
 }
 bool next=fan;
 if(isnan(temperature))next=true;
 else if(temperature>=target+0.5f)next=true;
 else if(temperature<=target-0.5f)next=false;
 fan=next;digitalWrite(14,fan?HIGH:LOW);
 // LEDs simulate thermal modes only: no Peltier is connected.
 digitalWrite(23,!isnan(temperature)&&fan);
 digitalWrite(19,!isnan(temperature)&&temperature<target-0.5f);
 static uint32_t alarmAt=0;
 if((!closed && now-unlockedAt>30000)||isnan(temperature)){
  if(now-alarmAt>3000){tone(26,4000,100);alarmAt=now;}
 }
 String line1=isnan(temperature)?"TEMP ERROR":String(temperature,1)+"C "+(fan?"FAN ON":"FAN OFF");
 String line2=(closed?String("Closed "):String("Open "))+(isnan(distanceCm)?"--":String(distanceCm,0))+"cm";
 if(line1!=previousLine1){lcd.setCursor(0,0);lcd.print("                ");lcd.setCursor(0,0);lcd.print(line1);previousLine1=line1;}
 if(line2!=previousLine2){lcd.setCursor(0,1);lcd.print("                ");lcd.setCursor(0,1);lcd.print(line2);previousLine2=line2;}
}

// Pin mapping confirmed: TRIG25 ECHO18 servo13 relay14 door27 buzzer26 SDA21 SCL22.

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace, diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik