Community project
Connected Multi-Lock Security System
This connected multi-lock security system uses an ESP32 to control two 12 V solenoid bolts via logic-level MOSFETs, adding smart access control to gates or doors. The system authenticates users through RFID cards and fingerprint scanning, with optional visitor detection via ultrasonic sensor and manual override capabilities. A reed switch monitors bolt position while status LEDs provide visual feedback.
The guide provides a complete wiring diagram, detailed parts list, and step-by-step assembly instructions covering power supply configuration, solenoid and diode installation, sensor integration, and mechanical testing. Firmware is included with support for multiple security modes (Home, Away, Night, Alarm, Smoke) and configurable authentication windows, allowing builders to deploy a flexible access control system tailored to residential or commercial gate security.
Wiring diagram

Gather all the parts
Assemble it in 9 steps
1. Build the fused 12 V supply
Connect the adapter positive lead to the 5 A fuse input, then take the fuse output as the 12 V rail. Put the 1000 uF capacitor across that rail: capacitor + to fused 12 V and capacitor - to the shared ground. (power protection)
- The electrolytic capacitor has polarity: reversing it can make it overheat or burst.
- Do not connect USB and either 5 V buck output to the C6 at the same time. Unplug both buck outputs before flashing by USB.
2. Set the two 5 V supplies
Connect both LM2596 inputs to fused 12 V and shared ground. Before connecting any electronics, use a multimeter to adjust each output to exactly 5.0 V. Use 5V-A for the C6 5V pin and ESP32-CAM 5V pin; use 5V-B only for the HC-SR04. Join every ground. (power)
- Do not power the RC522 or AS608 from 5 V; both use the C6 3V3 rail.
3. Fit the two spring bolts and manual release
Mount one bolt at the top and one at the bottom of the gate. Each bolt must slide freely toward its keeper under its own compression spring. Join both bolt pull links to one inside lever and the cam-lock key bar, so either manual control pulls both bolts in without electrical power. (mechanical emergency release)
- The inside lever and key must open the gate even when every wire is disconnected; the firmware is not part of the exit path.
4. Wire each solenoid and its diode
For the top bolt, connect fused 12 V to solenoid + and the top driver LOAD- terminal to solenoid -. Put the 1N5819 directly across that solenoid: striped cathode to solenoid + and unstriped anode to solenoid -. Repeat the same arrangement for the bottom bolt and bottom driver. Connect both driver ground terminals to the shared ground. (solenoid power and spike protection)
- A diode with its stripe toward the solenoid negative creates a short circuit; its stripe must face the fused +12 V side.
- Use D4184/AOD4184A drivers with a 3.3 V-compatible SIG input. Never substitute an IRF520 board or put 5 V/12 V onto a C6 GPIO.
5. Wire the Gate C6 signals
On the WeAct ESP32-C6-MiNi, wire top driver SIG to GPIO19 (top bolt control) and bottom driver SIG to GPIO23 (bottom bolt control). Wire the gate reed from GPIO0 to GND, the series bolt-out switch pair from GPIO1 to GND, and the inside-lever switch from GPIO3 to GND. The code uses the board's internal pull-up resistors. (signals)
- The two bolt position switches are wired in series: the GPIO1 circuit closes only when both bolts are fully out.
6. Wire the RFID and fingerprint readers
Wire the RC522 VCC and RST to C6 3V3, GND to shared ground, SCK to GPIO6, MOSI to GPIO7, MISO to GPIO2, and SDA/CS to GPIO18. Wire AS608 VCC to 3V3, GND to ground, AS608 TX to GPIO10 and AS608 RX to GPIO11. (identity signals)
- The RC522 is a 3.3 V-only module; connecting it to 5 V can damage it.
- GPIO2, GPIO6 and GPIO7 are intentionally routed SPI lines. Check these exact header labels rather than relying on a DevKitC diagram.
7. Wire the visitor detector and camera trigger
Power the HC-SR04 from 5V-B and ground. Connect TRIG to GPIO20. Make the echo divider in this order: HC-SR04 ECHO to the 1 kohm resistor, resistor far end to GPIO21, then the 2 kohm resistor from that same GPIO21 junction to GND. Connect C6 GPIO22 to ESP32-CAM GPIO13, and share ground. Power the camera from 5V-A. (visitor distance and photo trigger)
- HC-SR04 ECHO is 5 V. The 1 kohm / 2 kohm divider is required before GPIO21; bypassing it can damage the C6.
8. Fit the status LEDs
Connect GPIO4 through the green 330 ohm resistor to the green LED long leg, then connect its short leg to GND. Connect GPIO5 through the red 330 ohm resistor to the red LED long leg, then its short leg to GND. (locked and unlocked status)
- Do not omit the 330 ohm resistors; without them the LEDs or C6 pins can be damaged.
9. Test the mechanics before locking the gate
With the gate open and the keeper plates clear, test that each bolt springs out when solenoid power is removed, that the inside lever retracts both bolts, and that the key works with all power removed. Then align the two keepers so the slanted bolt tips latch as the gate swings shut. (safe mechanical check)
- Do not first test this system with anyone trapped behind a locked gate. Confirm manual inside release at every stage.
Review all connections
1. Connections between "gate_power_adapter" and "ESP32"
2. Connections between "gate_fuse" and "ESP32"
3. Connections between "gate_bulk_cap" and "ESP32"
4. Connections between "gate_5v_a_buck" and "ESP32"
5. Connections between "gate_5v_b_buck" and "ESP32"
6. Connections between "gate_top_driver" and "ESP32"
7. Connections between "gate_bottom_driver" and "ESP32"
8. Connections between "gate_top_solenoid" and "ESP32"
9. Connections between "gate_bottom_solenoid" and "ESP32"
10. Connections between "gate_top_diode" and "ESP32"
11. Connections between "gate_bottom_diode" and "ESP32"
12. Connections between "gate_rfid" and "ESP32"
13. Connections between "gate_fingerprint" and "ESP32"
14. Connections between "gate_ultrasonic" and "ESP32"
15. Connections between "gate_echo_r1" and "ESP32"
16. Connections between "gate_echo_r2" and "ESP32"
17. Connections between "gate_reed" and "ESP32"
18. Connections between "gate_bolts_switch" and "ESP32"
19. Connections between "gate_lever_switch" and "ESP32"
20. Connections between "gate_green_resistor" and "ESP32"
21. Connections between "gate_green_led" and "ESP32"
22. Connections between "gate_red_resistor" and "ESP32"
23. Connections between "gate_red_led" and "ESP32"
24. Connections between "gate_camera" and "ESP32"
Deploy the firmware
#include <Arduino.h>
#include <SPI.h>
#include <MFRC522.h>
#include <Adafruit_Fingerprint.h>
#include "OThread.h"
#include "OThreadCLI.h"
#define NIVASA_DATASET ""
constexpr uint8_t RFID_SCK=6, RFID_MOSI=7, RFID_MISO=2, RFID_CS=18, FINGER_RX=10, FINGER_TX=11, TOP_SOL=19, BOTTOM_SOL=23, ULTRA_TRIG=20, ULTRA_ECHO=21, REED=0, BOLTS_OUT=1, LEVER=3, CAMERA_TRIGGER=22, GREEN_LED=4, RED_LED=5;
constexpr uint32_t PWM_HZ=20000,PULLIN_MS=200,OPEN_TIMEOUT_MS=5000,GATE_OPEN_SETTLE_MS=2000,HOLD_LIMIT_MS=15000,JAM_WAIT_MS=2000,STATE_MS=30000,PRESENCE_MS=1000,PHOTO_COOLDOWN_MS=30000,AUTH_WINDOW_MS=10000,FORCE_GRACE_MS=10000,WRONG_WINDOW_MS=60000,FINGER_SCAN_MS=250,CARD_COOLDOWN_MS=1200;
constexpr uint8_t PWM_FULL=255,PWM_HOLD=77;
const byte AUTHORISED_UID[]={0xDE,0xAD,0xBE,0xEF};
enum Mode{HOME,AWAY,NIGHT,ALARM,SMOKE}; enum BoltDrive{BOLT_RELEASED,BOLT_PULLIN,BOLT_HOLD}; Mode mode=HOME; BoltDrive boltDrive=BOLT_RELEASED;
MFRC522 rfid(RFID_CS,255); HardwareSerial fingerUart(1); Adafruit_Fingerprint finger(&fingerUart); String cliLine,lastState;
uint32_t driveStartedAt=0,reedOpenedAt=0,releasedAt=0,lastStateAt=0,authorisedAt=0,lastPhotoAt=0,presenceStartedAt=0,wrongWindowAt=0,lastFingerScanAt=0,lastCardAt=0; uint8_t wrongCount=0; bool reedLast,boltsLast,leverLast,presenceReported=false,pendingSecondFactor=false;
void logLine(const String&t){Serial.printf("%lu %s\n",millis(),t.c_str());} void ot(const String&c){OThreadCLI.println(c);logLine("OT> "+c);} void sendNv(const String&t){ot("udp send ff03::1 1234 "+t);logLine("TX.THREAD "+t);} void eventNv(const char*e){sendNv(String("NV.GATE.EVT.")+e);} bool gateShut(){return digitalRead(REED)==LOW;} bool boltsOut(){return digitalRead(BOLTS_OUT)==LOW;} bool leverPressed(){return digitalRead(LEVER)==LOW;}
void updateLeds(){bool locked=boltsOut();digitalWrite(GREEN_LED,(mode==HOME||!locked)?HIGH:LOW);digitalWrite(RED_LED,(mode==ALARM||locked)?HIGH:LOW);} void sendState(bool force=false){String now=String(boltsOut()?"LOCKED":"UNLOCKED")+"."+(gateShut()?"SHUT":"OPEN");if(force||now!=lastState||millis()-lastStateAt>=STATE_MS){lastState=now;lastStateAt=millis();sendNv("NV.GATE.STATE."+now);}updateLeds();}
void setSolenoidDuty(uint8_t d){ledcWrite(TOP_SOL,d);ledcWrite(BOTTOM_SOL,d);} void releaseBolts(const char*r){if(boltDrive==BOLT_RELEASED)return;setSolenoidDuty(0);boltDrive=BOLT_RELEASED;releasedAt=millis();logLine(String("ACT.SOLENOID.RELEASE.")+r);sendState(true);} void startOpening(const char*r){if(mode==ALARM&&strcmp(r,"MODE_SMOKE")!=0){logLine(String("OPEN.BLOCKED.ALARM.")+r);return;}if(boltDrive==BOLT_RELEASED){setSolenoidDuty(PWM_FULL);boltDrive=BOLT_PULLIN;driveStartedAt=millis();reedOpenedAt=0;logLine(String("ACT.SOLENOID.PULLIN.200MS.")+r);}authorisedAt=millis();}
void triggerCamera(const char*r){if(millis()-lastPhotoAt<PHOTO_COOLDOWN_MS)return;digitalWrite(CAMERA_TRIGGER,HIGH);delay(200);digitalWrite(CAMERA_TRIGGER,LOW);lastPhotoAt=millis();logLine(String("ACT.CAMERA.TRIGGER.200MS.")+r);} void registerWrong(const char*r){logLine(String("AUTH.FAIL.")+r);eventNv("AUTH_FAIL");if(!wrongWindowAt||millis()-wrongWindowAt>WRONG_WINDOW_MS){wrongWindowAt=millis();wrongCount=0;}if(++wrongCount>=3){wrongCount=0;wrongWindowAt=millis();logLine("AUTH.TAMPER.THREE_WRONG_IN_1MIN");eventNv("TAMPER");triggerCamera("TAMPER");}}
void authorised(const char*m){logLine(String("AUTH.OK.")+m);eventNv("AUTH_OK");startOpening(m);} bool uidMatches(const MFRC522::Uid&u){if(u.size!=sizeof(AUTHORISED_UID))return false;for(byte i=0;i<u.size;i++)if(u.uidByte[i]!=AUTHORISED_UID[i])return false;return true;}
void processCard(){if(!rfid.PICC_IsNewCardPresent()||!rfid.PICC_ReadCardSerial())return;if(millis()-lastCardAt<CARD_COOLDOWN_MS){rfid.PICC_HaltA();rfid.PCD_StopCrypto1();return;}lastCardAt=millis();bool known=uidMatches(rfid.uid);rfid.PICC_HaltA();rfid.PCD_StopCrypto1();logLine(known?"SENSOR.RFID.KNOWN_CARD":"SENSOR.RFID.UNKNOWN_CARD");if(mode==ALARM){logLine("AUTH.IGNORED.ALARM.CARD");return;}if(!known){registerWrong("CARD");return;}if(mode==HOME){authorised("RFID");return;}if(mode==AWAY||mode==NIGHT){if(pendingSecondFactor&&millis()-authorisedAt<=AUTH_WINDOW_MS){pendingSecondFactor=false;authorised("RFID_AFTER_FINGER");}else{pendingSecondFactor=true;authorisedAt=millis();logLine("AUTH.CARD.OK.WAITING_FINGER_10S");}}}
void processFinger(){if(millis()-lastFingerScanAt<FINGER_SCAN_MS)return;lastFingerScanAt=millis();uint8_t p=finger.getImage();if(p==FINGERPRINT_NOFINGER)return;if(p!=FINGERPRINT_OK){logLine("SENSOR.FINGER.READ_ERROR");return;}p=finger.image2Tz();if(p!=FINGERPRINT_OK){registerWrong("FINGER_TEMPLATE");return;}p=finger.fingerFastSearch();bool known=p==FINGERPRINT_OK;logLine(known?"SENSOR.FINGER.MATCH":"SENSOR.FINGER.NO_MATCH");if(mode==ALARM){logLine("AUTH.IGNORED.ALARM.FINGER");return;}if(!known){registerWrong("FINGER");return;}if(mode==HOME){authorised("FINGER");return;}if(mode==AWAY||mode==NIGHT){if(pendingSecondFactor&&millis()-authorisedAt<=AUTH_WINDOW_MS){pendingSecondFactor=false;authorised("FINGER_AFTER_RFID");}else{pendingSecondFactor=true;authorisedAt=millis();logLine("AUTH.FINGER.OK.WAITING_CARD_10S");}}}
void setMode(const String&v){if(v=="HOME")mode=HOME;else if(v=="AWAY")mode=AWAY;else if(v=="NIGHT")mode=NIGHT;else if(v=="ALARM")mode=ALARM;else if(v=="SMOKE")mode=SMOKE;else return;pendingSecondFactor=false;logLine("MODE."+v);if(mode==SMOKE)startOpening("MODE_SMOKE");sendState(true);} void parseMessage(const String&m){if(!m.startsWith("NV."))return;logLine("RX.THREAD "+m);int a=m.indexOf('.',3),b=a<0?-1:m.indexOf('.',a+1);if(a<0||b<0)return;String from=m.substring(3,a),kind=m.substring(a+1,b),rest=m.substring(b+1);if(from!="CORE")return;if(kind=="MODE"){setMode(rest);return;}if(kind!="CMD")return;int d=rest.indexOf('.');if(d<0)return;String target=rest.substring(0,d),action=rest.substring(d+1);if(target!="GATE"&&target!="ALL")return;logLine("CMD."+action);if(action=="RELEASE"||action=="UNLOCK")startOpening("REMOTE");}
void pollThread(){while(OThreadCLI.available()){char c=OThreadCLI.read();if(c=='\n'){int at=cliLine.indexOf("NV.");if(at>=0){String m=cliLine.substring(at);int sp=m.indexOf(' ');if(sp>0)m=m.substring(0,sp);m.trim();parseMessage(m);}cliLine="";}else if(c!='\r'&&cliLine.length()<300)cliLine+=c;}}
long distanceCm(){digitalWrite(ULTRA_TRIG,LOW);delayMicroseconds(2);digitalWrite(ULTRA_TRIG,HIGH);delayMicroseconds(10);digitalWrite(ULTRA_TRIG,LOW);unsigned long us=pulseIn(ULTRA_ECHO,HIGH,25000);return us?(long)(us/58):-1;} void serviceSensors(){bool shut=gateShut(),out=boltsOut(),lever=leverPressed();if(shut!=reedLast){logLine(String("SENSOR.REED.")+(shut?"SHUT":"OPEN"));if(!shut){if(boltsOut()&&millis()-authorisedAt>FORCE_GRACE_MS&&!lever){eventNv("FORCED");triggerCamera("FORCED");}reedOpenedAt=millis();eventNv("OPENED");}else eventNv("CLOSED");reedLast=shut;sendState(true);}if(out!=boltsLast){logLine(String("SENSOR.BOLTS.")+(out?"BOTH_OUT":"NOT_BOTH_OUT"));boltsLast=out;sendState(true);}if(lever&&!leverLast){logLine("SENSOR.INSIDE_LEVER");authorisedAt=millis();eventNv("LEVER");}leverLast=lever;}
void serviceBolts(){if(boltDrive==BOLT_PULLIN&&millis()-driveStartedAt>=PULLIN_MS){setSolenoidDuty(PWM_HOLD);boltDrive=BOLT_HOLD;logLine("ACT.SOLENOID.HOLD.30PCT");}if(boltDrive!=BOLT_RELEASED){if(mode==SMOKE&&millis()-driveStartedAt>=HOLD_LIMIT_MS){releaseBolts("SMOKE_HOLD_LIMIT");eventNv("THERMAL");}else if(mode!=SMOKE&&((reedOpenedAt&&millis()-reedOpenedAt>=GATE_OPEN_SETTLE_MS)||millis()-driveStartedAt>=OPEN_TIMEOUT_MS))releaseBolts(reedOpenedAt?"GATE_OPEN_PLUS_2S":"NO_GATE_OPEN_5S");}if(releasedAt&&!boltsOut()&&millis()-releasedAt>=JAM_WAIT_MS){releasedAt=0;logLine("ACT.SOLENOID.JAM.BOLTS_NOT_OUT");eventNv("JAM");}}
void servicePresence(){static uint32_t lastSample=0;if(millis()-lastSample<120)return;lastSample=millis();long cm=distanceCm();bool close=cm>0&&cm<80;if(close){if(!presenceStartedAt){presenceStartedAt=millis();presenceReported=false;logLine("SENSOR.ULTRASONIC.UNDER_80CM");}if(!presenceReported&&millis()-presenceStartedAt>=PRESENCE_MS){presenceReported=true;logLine("SENSOR.PRESENCE.OVER_1S");eventNv("AT_GATE");triggerCamera("AT_GATE");}}else{presenceStartedAt=0;presenceReported=false;}}
void setup(){Serial.begin(115200);delay(250);pinMode(REED,INPUT_PULLUP);pinMode(BOLTS_OUT,INPUT_PULLUP);pinMode(LEVER,INPUT_PULLUP);pinMode(ULTRA_TRIG,OUTPUT);pinMode(ULTRA_ECHO,INPUT);pinMode(CAMERA_TRIGGER,OUTPUT);pinMode(GREEN_LED,OUTPUT);pinMode(RED_LED,OUTPUT);digitalWrite(CAMERA_TRIGGER,LOW);ledcAttach(TOP_SOL,PWM_HZ,8);ledcAttach(BOTTOM_SOL,PWM_HZ,8);setSolenoidDuty(0);SPI.begin(RFID_SCK,RFID_MISO,RFID_MOSI,RFID_CS);rfid.PCD_Init();fingerUart.begin(57600,SERIAL_8N1,FINGER_RX,FINGER_TX);finger.begin(57600);logLine(finger.verifyPassword()?"FINGERPRINT.READY":"FINGERPRINT.NOT_FOUND");reedLast=gateShut();boltsLast=boltsOut();leverLast=leverPressed();logLine("NIVASA.GATE.BOOT.MODE.HOME");OThread.begin(false);OThreadCLI.begin();OThreadCLI.setTimeout(100);ot("thread stop");ot("ifconfig down");if(strlen(NIVASA_DATASET)){ot("dataset set active " NIVASA_DATASET);logLine("THREAD.DATASET.CONFIGURED");}else logLine("THREAD.DATASET.MISSING");ot("ifconfig up");ot("thread start");ot("udp close");ot("udp open");ot("udp bind :: 1234");sendState(true);}
void loop(){pollThread();serviceSensors();processCard();processFinger();if(pendingSecondFactor&&millis()-authorisedAt>AUTH_WINDOW_MS){pendingSecondFactor=false;registerWrong("SECOND_FACTOR_TIMEOUT");}serviceBolts();servicePresence();sendState();delay(5);}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.




