Community project

ESP32 Smart Door Security System

Arduino
Photo of ESP32 Smart Door Security System
Generated with AI

Jascodm

Last updated October 31, 2026

This project builds a tabletop sample sorter that uses weight and distance measurements to automatically classify objects into three categories and route them to the correct output gate. The system combines a load cell with HX711 amplifier for precise weighing, an ultrasonic distance sensor for height detection, infrared beam pairs for entry and exit sensing, and an inductive metal detector for material identification—all controlled by an Arduino with servo-driven mechanical gates and real-time feedback on a 16x2 LCD display.

The guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions for mounting the rotating classification mechanism, integrating all sensors with proper electrical isolation, and programming the Arduino firmware. Builders will learn to calibrate the weight profiles for three sample groups, set safe mechanical travel limits, and troubleshoot common jams and sensor failures through the built-in diagnostic display.

Wiring diagram

Loading diagram…

Gather all the parts

QtyComponent
1

LCD 16x2 I2C

16x2 character LCD display with I2C backpack

1

Certified enclosed regulated 12V 0.5A DC adapter

Use supplied insulated DC connector; never expose mains. Only low-voltage wiring in prototype.

1

Certified enclosed regulated 5V 6A DC supply

Use supplied insulated DC connector; never expose mains. Only low-voltage wiring in prototype.

1

5A inline fuse and holder

Protects servo distribution wiring; use wiring/connectors rated above 6A.

1

Load Cell

Four-wire strain-gauge straight-bar load cell, common 5 kg hobby scale sensor. It produces a small bridge signal and should be read through a load-cell ADC/amplifier such as HX711 or NAU7802 rather than directly by a microcontroller GPIO.

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

MG996R Servo Motor

High-torque digital metal gear RC servo motor by TowerPro. Operating voltage 4.8V-7.2V (nominal 5V-6V external supply). Standard RC servo control: 50 Hz signal with roughly 500-2500 us pulse width for 0-180 degree rotation. Three-wire interface: Brown = GND, Red = VCC, Orange = signal. Stall current can reach about 2.5A at 6V, so use a dedicated external servo supply and common ground with the controller. Add bulk capacitance across the servo rail when driving one or more servos. Torque: 9.4 kg.cm (4.8V) / 11 kg.cm (6V).

1

MG996R Servo Motor

High-torque digital metal gear RC servo motor by TowerPro. Operating voltage 4.8V-7.2V (nominal 5V-6V external supply). Standard RC servo control: 50 Hz signal with roughly 500-2500 us pulse width for 0-180 degree rotation. Three-wire interface: Brown = GND, Red = VCC, Orange = signal. Stall current can reach about 2.5A at 6V, so use a dedicated external servo supply and common ground with the controller. Add bulk capacitance across the servo rail when driving one or more servos. Torque: 9.4 kg.cm (4.8V) / 11 kg.cm (6V).

1

HX711 5V-compatible breakout

Use breakout explicitly rated 5V; bridge and logic both supplied at 5V. Catalog generic has a 3.3V nominal voltage; this design selects a 5V module.

1

Adafruit 2168 IR breakbeam receiver

Receiver half of pair. Red 5V, black GND, white/yellow OUT. INPUT_PULLUP; LOW means interrupted beam. Test with every clear bottle; optical transmission can cause missed detection.

1

Adafruit 2168 IR breakbeam transmitter

Transmitter half of pair; internal LED current limiting. Red 5V, black GND. Align across narrow chute.

1

Adafruit 2168 IR breakbeam receiver

Receiver half of pair. Red 5V, black GND, white/yellow OUT. INPUT_PULLUP; LOW means interrupted beam. Test with every clear bottle; optical transmission can cause missed detection.

1

Adafruit 2168 IR breakbeam transmitter

Transmitter half of pair; internal LED current limiting. Red 5V, black GND. Align across narrow chute.

1

LJ12A3-4-Z/BX NPN normally-open inductive sensor

6–36V supply, selected 12V. Black sinks on metal; aluminum range less than nominal 4mm steel range. NOT aluminum-specific. Only admitted metal sample is aluminum can.

1

Sharp PC817 optocoupler

DIP pins 1 anode, 2 cathode, 3 emitter, 4 collector. Isolates 12V sensor output from D3.

1

5V active piezo sounder, two terminals, maximum 30mA

Self-oscillating active PIEZO sounder; continuous DC produces sound. Not a passive piezo or magnetic coil. Low-side NPN driver.

1

NPN Transistor (2N2222 / BC547)

General-purpose NPN bipolar junction transistor (BJT) in TO-92 package. Used as a low-side switch to drive loads (e.g. buzzers) whose current exceeds ESP32 GPIO source capability (~12 mA). A ~1 kΩ base resistor is placed between the ESP32 GPIO and the transistor base. The collector connects to the load negative terminal (buzzer –), and the emitter connects to GND. When the GPIO goes HIGH (3.3 V), the transistor saturates and switches the load on. Compatible with both 2N2222/2N2222A and BC547/BC547B variants. Add a flyback diode (e.g. 1N4148) across inductive load terminals.

1

Momentary normally-open start button

D5 INPUT_PULLUP; software debounce.

1

2.2k ohm 0.25W resistor

2.2k ohm 0.25W

1

10k ohm resistor

10k ohm

1

1k ohm resistor

1k ohm

1

10k ohm resistor

10k ohm

1

2200uF 10V electrolytic capacitor

Bulk capacitance directly across servo distribution terminals. Observe polarity.

Assemble it in 10 steps

1. Build a guarded tabletop sorter

Use three fixed bins and fixed receiving chutes in a 180-degree arc: blue bottle at 45 degrees, red can at 90 degrees, green paper at 135 degrees. Home is 90 degrees. The rotating tube needs an offset or angled outlet: rotating a straight tube around its own center would not change where trash falls. Put the bottom gate above a short angled outlet whose mouth sweeps the three receiving chutes. Only the tube, gate, and short outlet rotate; bins and receiving chutes stay fixed. Size for one empty dry bottle, empty aluminum can, or paper ball at a time. Use a removable loading cover and guards; this is a supervised prototype, not a public-use machine. Never put hands in the moving mechanism.

  • No automatic system can guarantee safe movement without guards. No liquids, glass, batteries, sharp objects, or heavy trash.

2. Mount the weighed rotating assembly

Bolt one end of the 5kg load cell to the frame. Bolt a small floating platform to the other end with spacers, allowing the bar to flex without touching the frame. Mount the tube's bearing, rotation servo, gate and gate servo on this platform so all item weight passes through the cell. A bearing takes the tube load; do not support its weight on the servo shaft alone. Keep total assembly plus trash below 5kg, and use mechanical overload stops. Servo 1 drives tube yaw through a horn/linkage; Servo 2 travels with the tube and drives its bottom gate. Leave soft cable loops so wires neither constrain the scale nor wind up across 45–135 degrees. Do not add supports that bypass the scale.

  • Gate must open freely under gravity and not pinch an object when closing. Light, empty demonstration items only. Verify servo torque with a generous margin; avoid operating near stall.

3. Fit the identification sensors

Place HC-SR04 on a stationary bracket above the home-position tube, aimed down at the item. The empty gate distance must be 10–60cm, with every sample surface at least 2cm from the transducers. A narrow guide holds bottles and cans upright at repeatable positions. Mount the LJ12A3 sensing face flush in a nonmetal wall beside the base of the sample, with no metal gate or bracket in its field. Bring the aluminum can wall within about 1mm and verify detection repeatedly; aluminum range is shorter than its advertised steel range. The sensor detects many metals, not aluminum uniquely. Only admit aluminum cans as the metal sample category. Sensor operates at home only.

4. Fit both infrared beam pairs

IR 1 uses an Adafruit 2168 transmitter and receiver across the holding region at home. IR 2 uses another pair on the rotating short outlet, below the bottom gate and above the fixed bin chutes. A passing item must fully interrupt then clear IR 2. Align beams through narrow black shrouds. Test every bottle: transparent plastic may transmit infrared, so position the beam through the cap, label or opaque base. If the exit beam misses an item, the machine deliberately awards no points. Keep hands away from the exit; an optical sensor cannot distinguish a hand from trash.

5. Connect low-voltage logic

With all supplies disconnected, use a breadboard and jumper wires for low-current signals only. Uno is powered by USB. HC-SR04 TRIG to D7 (trigger), ECHO to D6 (distance timing). IR 1 receiver OUT to D2 (item), IR 2 receiver OUT to D4 (exit); their red leads and transmitter red leads to Uno 5V, black leads to GND. LCD SDA to A4 (data), SCL to A5 (clock), VCC to 5V, GND to GND. HX711 DT to A0 (weight data), SCK to A1 (weight clock), VCC to 5V, GND to GND; use a breakout explicitly rated for 5V. Match the cell's actual excitation and signal labels to HX711 E+, E-, A+, A-; wire colors vary. Button between D5 and GND; for a four-leg button use opposite switched contacts, not two internally joined legs.

6. Isolate the 12V metal sensor

Use the exact NPN normally-open BX version, not a PNP BY version. Brown to 12V adapter positive; blue to that adapter negative. Brown also to the 2.2k 0.25W resistor, resistor to PC817 pin 1 anode, PC817 pin 2 cathode to black sensor output. On the Uno side PC817 pin 3 emitter to GND, pin 4 collector to D3. Connect the separate 10k pull-up between collector and Uno 5V. Keep the 12V adapter negative separate from Uno ground: the optocoupler bridges the two electrically isolated sides. D3 should read HIGH without metal and LOW with the sample can. Never connect brown or black directly to a Uno pin.

  • A different sensor output or accidentally joining its 12V wire to logic can destroy the Uno.

7. Wire servo power and the active sounder

Use an enclosed regulated 5V 6A supply. Supply positive through the 5A inline fuse to a screw-terminal servo distribution point; supply negative to servo grounds and Uno GND. Red servo leads to fused 5V, brown to common ground, signals to D9 for rotation and D10 for gate. Add the 2200uF capacitor at these terminals, positive to servo 5V and negative stripe to ground. Never join external servo 5V to Uno 5V or route servo current through a breadboard. Use suitably rated short power wires and connectors. For the maximum-30mA 5V active piezo sounder, positive to Uno 5V, negative to transistor collector, emitter to GND, D8 through 1k to base, and 10k from base to emitter. Verify the exact transistor's B/C/E lead order from its supplier; 2N2222 and BC547 packages vary. This is an active piezo sounder, not a magnetic buzzer or passive piezo.

  • Never expose mains wiring. Unplug all supplies before changing wires. A reversed electrolytic capacitor can rupture.

8. Set safe mechanical travel

First test with servo horns disconnected and the tube empty. Firmware commands gate closed 0 degrees, open 90 degrees, home 90 degrees, and blue/red/green 45/90/135 degrees. Change CLOSED, OPEN, HOME and ANGLES in the firmware to match your mechanics before fitting horns. Fit horns with power disconnected in the corresponding commanded positions. Test closed-gate alignment and collision-free travel using light samples and guards. Rotation waits 1.8 seconds before gate opening and gate waits 1 second before return; increase these constants for a slower assembly. These are timed movements, not measured position feedback. A stalled or disconnected servo cannot be detected.

9. Teach the three sample groups

First boot automatically enters calibration. To recalibrate later, hold Start during power-on for at least 2 seconds, then release. Begin with the tube empty and click once to tare; place a known 100g calibration weight on the closed gate and click. Remove the weight. For each prompted Bottle, Can, and Paper group, insert one representative sample and click; remove and replace between the three readings. Use three examples covering your allowed sample variation. Keep fingers clear while measuring. Firmware learns stable weight/height envelopes and checks inductive response; it rejects overlapping bottle/paper envelopes and asks you to reteach. After Calibration OK, remove everything and click. Profiles persist in EEPROM. Keep the tube empty whenever powering up; startup tares the complete assembly. USB serial at 115200 logs measured grams, height and metal readings for troubleshooting.

  • Weight and height do not identify material universally. Only use the taught, restricted sample set. An unrelated item with the same measurements can be misclassified. If bottle/paper envelopes overlap, this mechanism is unsuitable for those samples; do not widen limits to force a decision.

10. Run and check failures

Standby shows Click to Begin and the current points. Press and release once, then insert exactly one item. Button presses while busy are discarded. Processing keeps the sounder on, identifies a unique taught group, rotates with gate closed, settles, then opens. Only an exit interruption followed by 200ms clear earns exactly two points, once, after gate closure and return home. Verify each bin and perform an unknown-item test. Unknown Trash keeps the gate closed at home: remove the sample by hand with power off if needed, then click to acknowledge when clear; another new press is required to begin. If exit is blocked or never confirms passage within 8 seconds, a latched fault awards zero points and stops motion; a released gate stays open rather than closing on a jam. Disconnect servo power before clearing; empty the assembly before restart. Points are held in RAM and reset to zero on reboot, unlike calibration. Three bins remain fixed.

  • Exit passage confirms departure from the tube, not arrival in the correct bin. This design has no bin sensors, servo feedback, safety interlock, or fraud protection.

Review all connections

1. Connections between "sonar" and "Arduino"

FunctionsonarArduino
powerVCC5V
groundGNDGND
digitalTRIGGPIO 7
digitalECHOGPIO 6

2. Connections between "lcd" and "Arduino"

FunctionlcdArduino
powerVCC5V
groundGNDGND
i2cSDAGPIO 18
i2cSCLGPIO 19

3. Connections between "scale" and "Arduino"

FunctionscaleArduino
powerVCC5V
groundGNDGND
digitalDTGPIO 14
digitalSCKGPIO 15

4. Connections between "entry" and "Arduino"

FunctionentryArduino
powerVCC5V
groundGNDGND
digitalOUTGPIO 2

5. Connections between "exit" and "Arduino"

FunctionexitArduino
powerVCC5V
groundGNDGND
digitalOUTGPIO 4

6. Connections between "entryTx" and "Arduino"

FunctionentryTxArduino
powerVCC5V
groundGNDGND

7. Connections between "exitTx" and "Arduino"

FunctionexitTxArduino
powerVCC5V
groundGNDGND

8. Connections between "button" and "Arduino"

FunctionbuttonArduino
digital1GPIO 5
ground2GND

9. Connections between "cell" and "Arduino"

FunctioncellArduino
powerE+ → HX711 5V-compatible breakout E+EXT
groundE- → HX711 5V-compatible breakout E-EXT
analogA+ → HX711 5V-compatible breakout A+EXT
analogA- → HX711 5V-compatible breakout A-EXT

10. Connections between "psu5" and "Arduino"

Functionpsu5Arduino
powerPOS → 5A inline fuse and holder 1EXT
groundNEGGND

11. Connections between "fuse" and "Arduino"

FunctionfuseArduino
power25V

12. Connections between "cap" and "Arduino"

FunctioncapArduino
powerPOS5V
groundNEGGND

13. Connections between "rotate" and "Arduino"

FunctionrotateArduino
powerVCC5V
groundGNDGND
pwmSIGGPIO 9

14. Connections between "gate" and "Arduino"

FunctiongateArduino
powerVCC5V
groundGNDGND
pwmSIGGPIO 10

15. Connections between "psu12" and "Arduino"

Functionpsu12Arduino
powerPOS → LJ12A3-4-Z/BX NPN normally-open inductive sensor BROWNEXT
groundNEG → LJ12A3-4-Z/BX NPN normally-open inductive sensor BLUEEXT

16. Connections between "sensorR" and "Arduino"

FunctionsensorRArduino
data1 → LJ12A3-4-Z/BX NPN normally-open inductive sensor BROWNEXT
data2 → Sharp PC817 optocoupler AEXT

17. Connections between "iso" and "Arduino"

FunctionisoArduino
digitalK → LJ12A3-4-Z/BX NPN normally-open inductive sensor BLACKEXT
groundEGND
digitalCGPIO 3

18. Connections between "pull" and "Arduino"

FunctionpullArduino
power15V
data2 → Sharp PC817 optocoupler CEXT

19. Connections between "buzzer" and "Arduino"

FunctionbuzzerArduino
powerPOS5V
groundNEG → NPN Transistor (2N2222 / BC547) COLLECTOREXT

20. Connections between "driver" and "Arduino"

FunctiondriverArduino
groundEMITTERGND

21. Connections between "baseR" and "Arduino"

FunctionbaseRArduino
data1GPIO 8
data2 → NPN Transistor (2N2222 / BC547) BASEEXT

22. Connections between "basePull" and "Arduino"

FunctionbasePullArduino
data1 → NPN Transistor (2N2222 / BC547) BASEEXT
ground2GND

Deploy the firmware

#include <Arduino.h>
#include <Wire.h>
#include <Servo.h>
#include <EEPROM.h>
#include <hd44780.h>
#include <hd44780ioClass/hd44780_I2Cexp.h>
#include <math.h>
// Controlled sample classifier, not chemical material identification.
const byte ENTRY=2,METAL=3,EXIT=4,BUTTON=5,ECHO=6,TRIG=7,BUZZER=8;
const byte ROTATE=9,GATE=10,DT=A0,HX_CLOCK=A1;
const byte HOME=90,CLOSED=0,OPEN=90;
const byte ANGLES[]={45,90,135};
const unsigned long ROTATE_MS=1800,GATE_MS=1000,EXIT_MS=8000;
Servo tube,gate;
hd44780_I2Cexp lcd;
struct Profile { float w0,w1,h0,h1; };
struct Calibration { uint32_t magic; float factor,emptyDistance; Profile p[3]; uint16_t check; } cal;
const uint32_t MAGIC=0x57535433UL;
long zeroRaw=0;
uint32_t points=0,entered=0,lastButtonChange=0;
byte buttonRaw=HIGH,buttonStable=HIGH;
bool calibrated=false,seenExit=false,exitCleared=false;
uint32_t clearSince=0;
int kind=-1;
enum State {STANDBY,WAIT_ITEM,DETECTED,IDENTIFIED,ROTATING,RELEASING,CLOSING,RETURNING,SUCCESS,TOTAL,UNKNOWN,JAM,CAL_EMPTY,CAL_WEIGHT,CAL_SAMPLE,CAL_DONE};
State state=STANDBY;
byte teachClass=0,teachRepeat=0;
void screen(const __FlashStringHelper *a,const __FlashStringHelper *b) {
 lcd.clear(); lcd.setCursor(0,0);lcd.print(a);lcd.setCursor(0,1);lcd.print(b);
}
void enter(State s){state=s;entered=millis();}
void standby(){digitalWrite(BUZZER,LOW);enter(STANDBY);lcd.clear();lcd.print(F("Click to Begin"));lcd.setCursor(0,1);lcd.print(F("Points: "));lcd.print(points);}
bool press(){
 byte r=digitalRead(BUTTON);
 if(r!=buttonRaw){buttonRaw=r;lastButtonChange=millis();}
 if(r!=buttonStable && millis()-lastButtonChange>=35){buttonStable=r;return r==LOW;}
 return false;
}
bool raw(long &v){
 uint32_t start=millis();while(digitalRead(DT)){if(millis()-start>250)return false;}
 unsigned long x=0;
 // Keep HX_CLOCK high under 60us even when the servo interrupt fires.
 for(byte i=0;i<24;i++){noInterrupts();digitalWrite(HX_CLOCK,HIGH);delayMicroseconds(1);x=(x<<1)|digitalRead(DT);digitalWrite(HX_CLOCK,LOW);interrupts();delayMicroseconds(1);}
 noInterrupts();digitalWrite(HX_CLOCK,HIGH);delayMicroseconds(1);digitalWrite(HX_CLOCK,LOW);interrupts(); // 25th clock selects A gain 128
 if(x&0x800000UL)x|=0xFF000000UL;v=(long)x;return true;
}
bool average(long &v){long x;int64_t sum=0;for(byte i=0;i<10;i++){if(!raw(x))return false;sum+=x;}v=(long)(sum/10);return true;}
float distance(){
 digitalWrite(TRIG,LOW);delayMicroseconds(3);digitalWrite(TRIG,HIGH);delayMicroseconds(10);digitalWrite(TRIG,LOW);
 unsigned long t=pulseIn(ECHO,HIGH,25000UL);return t?t/58.0f:-1;
}
bool measure(float &w,float &h,bool &metal){
 float wl=1e9,wh=-1e9,dl=1e9,dh=-1e9,ws=0,ds=0;byte metals=0;
 for(byte i=0;i<10;i++){
  long x;if(!raw(x))return false;float a=(x-zeroRaw)/cal.factor;float d=distance();if(d<2 || d>60)return false;
  wl=min(wl,a);wh=max(wh,a);dl=min(dl,d);dh=max(dh,d);ws+=a;ds+=d;
  if(digitalRead(METAL)==LOW)metals++;delay(65);
 }
 w=ws/10;h=(cal.emptyDistance-ds/10)*10;metal=metals>=9;
 Serial.print(F("grams="));Serial.print(w);Serial.print(F(" height_mm="));Serial.print(h);Serial.print(F(" metal_samples="));Serial.println(metals);
 return wh-wl<=max(1.0f,fabs(w)*0.08f) && dh-dl<=1.2f && (metals<=1 || metals>=9) && w>0.5 && h>5;
}
uint16_t checksum(){uint16_t s=0;byte *p=(byte*)&cal;for(unsigned i=0;i<sizeof(cal)-sizeof(cal.check);i++)s=(s*31)^p[i];return s;}
void unknown(){digitalWrite(BUZZER,LOW);enter(UNKNOWN);screen(F("Unknown Trash"),F("Remove; click"));}
void jam(){digitalWrite(BUZZER,LOW);enter(JAM);screen(F("Jam / Exit Fault"),F("Power off; clear"));}
void samplePrompt(){
 lcd.clear();lcd.print(teachClass==0?F("Bottle sample"):teachClass==1?F("Can sample"):F("Paper sample"));lcd.setCursor(0,1);lcd.print(teachRepeat+1);lcd.print(F("/3 Place; click"));
}
void setup(){
 pinMode(ENTRY,INPUT_PULLUP);pinMode(EXIT,INPUT_PULLUP);pinMode(METAL,INPUT_PULLUP);pinMode(BUTTON,INPUT_PULLUP);
 pinMode(BUZZER,OUTPUT);digitalWrite(BUZZER,LOW);pinMode(TRIG,OUTPUT);pinMode(ECHO,INPUT);pinMode(DT,INPUT);pinMode(HX_CLOCK,OUTPUT);digitalWrite(HX_CLOCK,LOW);
 Serial.begin(115200);Wire.begin();if(lcd.begin(16,2)){while(true){delay(100);}}
 // Set closed before attaching. Mechanical stops must be calibrated unloaded.
 gate.write(CLOSED);gate.attach(GATE);delay(GATE_MS);tube.write(HOME);tube.attach(ROTATE);delay(ROTATE_MS);
 EEPROM.get(0,cal);calibrated=cal.magic==MAGIC && cal.check==checksum() && fabs(cal.factor)>0.01f;
 bool teach=digitalRead(BUTTON)==LOW;
 if(teach){delay(2000);teach=digitalRead(BUTTON)==LOW;}
 buttonRaw=buttonStable=digitalRead(BUTTON);lastButtonChange=millis();
 if(teach || !calibrated){cal.magic=0;cal.factor=1;enter(CAL_EMPTY);screen(F("Calibration"),F("Empty; click"));return;}
 screen(F("Keep tube empty"),F("Taring scale..."));delay(1500);
 if(!average(zeroRaw) || digitalRead(ENTRY)==LOW || digitalRead(EXIT)==LOW){unknown();return;}
 standby();
}
void loop(){
 bool clicked=press();uint32_t now=millis();
 if(state==CAL_EMPTY && clicked){
  if(digitalRead(ENTRY)==LOW || digitalRead(EXIT)==LOW)return;
  if(!average(zeroRaw)){screen(F("Scale fault"),F("Check HX711"));return;}
  float d=distance();if(d<10 || d>60){screen(F("Sonar fault"),F("Check placement"));return;}
  cal.emptyDistance=d;enter(CAL_WEIGHT);screen(F("Place 100 grams"),F("Then click"));return;
 }
 if(state==CAL_WEIGHT && clicked){long x;if(!average(x) || labs(x-zeroRaw)<100){screen(F("Scale fault"),F("Retry 100 grams"));return;}cal.factor=(x-zeroRaw)/100.0f;teachClass=0;teachRepeat=0;enter(CAL_SAMPLE);samplePrompt();return;}
 if(state==CAL_SAMPLE && clicked){
  float w,h;bool m;if(!measure(w,h,m) || m!=(teachClass==1)){screen(F("Unstable / Metal"),F("Fix sample;click"));return;}
  Profile &p=cal.p[teachClass];float wm=max(0.8f,w*0.12f),hm=10;
  if(!teachRepeat){p={w-wm,w+wm,h-hm,h+hm};}else{p.w0=min(p.w0,w-wm);p.w1=max(p.w1,w+wm);p.h0=min(p.h0,h-hm);p.h1=max(p.h1,h+hm);}
  teachRepeat++;
  if(teachRepeat==3){teachRepeat=0;teachClass++;}
  if(teachClass<3){samplePrompt();return;}
  Profile a=cal.p[0],b=cal.p[2];
  if(a.w0<=b.w1 && b.w0<=a.w1 && a.h0<=b.h1 && b.h0<=a.h1){enter(CAL_EMPTY);screen(F("Samples overlap"),F("Empty; reteach"));return;}
  cal.magic=MAGIC;cal.check=checksum();EEPROM.put(0,cal);calibrated=true;enter(CAL_DONE);screen(F("Calibration OK"),F("Empty; click"));return;
 }
 if(state==CAL_DONE && clicked){if(digitalRead(ENTRY)==LOW || digitalRead(EXIT)==LOW)return;if(!average(zeroRaw))return;standby();return;}
 if(state==STANDBY && clicked){
  if(!calibrated || digitalRead(ENTRY)==LOW || digitalRead(EXIT)==LOW){unknown();return;}
  long x;if(!average(x) || fabs((x-zeroRaw)/cal.factor)>1.5f){unknown();return;}
  enter(WAIT_ITEM);screen(F("Insert Trash"),F("Ready to Sort"));return;
 }
 if(state==WAIT_ITEM){
  if(digitalRead(ENTRY)==LOW){digitalWrite(BUZZER,HIGH);enter(DETECTED);screen(F("Trash Detected"),F("On Processing"));}
  else if(now-entered>60000)standby();return;
 }
 if(state==DETECTED && now-entered>=1200){
  float w,h;bool m;if(!measure(w,h,m)){unknown();return;}
  int matches=0;kind=-1;
  for(byte i=0;i<3;i++){Profile p=cal.p[i];if(m==(i==1) && w>=p.w0 && w<=p.w1 && h>=p.h0 && h<=p.h1){matches++;kind=i;}}
  if(matches!=1){unknown();return;}
  enter(IDENTIFIED);screen(F("Trash Identified"),kind==0?F("Plastic Bottle"):kind==1?F("Aluminum Can"):F("Crumpled Paper"));return;
 }
 if(state==IDENTIFIED && now-entered>=1500){
  if(digitalRead(EXIT)==LOW){unknown();return;}
  gate.write(CLOSED);tube.write(ANGLES[kind]);enter(ROTATING);screen(F("Sorting Trash"),F("Please Wait..."));return;
 }
 if(state==ROTATING && now-entered>=ROTATE_MS){
  if(digitalRead(EXIT)==LOW){jam();return;}
  seenExit=false;exitCleared=false;clearSince=0;gate.write(OPEN);enter(RELEASING);return;
 }
 if(state==RELEASING){
  if(digitalRead(EXIT)==LOW){seenExit=true;exitCleared=false;clearSince=0;}else if(seenExit){if(!clearSince)clearSince=now;if(now-clearSince>=200)exitCleared=true;}
  if(exitCleared && now-entered>=GATE_MS){gate.write(CLOSED);enter(CLOSING);return;}
  if(now-entered>=EXIT_MS){jam();return;}
 }
 if(state==CLOSING && now-entered>=GATE_MS){tube.write(HOME);enter(RETURNING);return;}
 if(state==RETURNING && now-entered>=ROTATE_MS){
  digitalWrite(BUZZER,LOW);if(points<=4294967293UL)points+=2;else{jam();return;}
  enter(SUCCESS);screen(kind==0?F("Bottle Sorted!"):kind==1?F("Can Sorted!"):F("Paper Sorted!"),F("+2 Points"));return;
 }
 if(state==SUCCESS && now-entered>=1800){enter(TOTAL);lcd.clear();lcd.print(F("Sorting Success"));lcd.setCursor(0,1);lcd.print(F("Total: "));lcd.print(points);lcd.print(F(" Points"));return;}
 if(state==TOTAL && now-entered>=2200){standby();return;}
 if(state==UNKNOWN && clicked){
  if(!calibrated){enter(CAL_EMPTY);screen(F("Calibration"),F("Empty; click"));return;}
  long x;if(digitalRead(ENTRY)==HIGH && digitalRead(EXIT)==HIGH && average(x) && fabs((x-zeroRaw)/cal.factor)<1.5f)standby();
 }
 // JAM is latched. Never rotate with a potentially trapped item or close on it.
}

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