Contents — 26 sections
Project Overview
Card-based door access with per-person roles and a complete audit trail — built with the security realities of RFID (cloneable cards, fail-safe doors, offline operation) taken seriously.
An access-control system answers a deceptively simple question at every door: should this person be let in, right now? Getting that right is more than reading a card and clicking a relay. It means knowing who each credential belongs to and what they are allowed to open, keeping an audit trail of every entry and every refusal, deciding what happens to the door when the power or network fails, and — the part hobby projects usually skip — being honest that a plain RFID card's ID is trivially cloneable, so the design cannot treat "I saw a valid card number" as proof of identity. This project builds a real door controller with those realities front of mind.
At the door, an RFID reader presents a credential; the controller checks it against a list of authorised users and their roles (who may open which doors, and when), actuates the lock if permitted, and writes a timestamped record of the event — granted or denied — to storage that cannot be quietly edited. Roles matter because access is rarely all-or-nothing: a cleaner may enter the lobby but not the server room, a contractor only during working hours, a manager everywhere. The audit log matters because access control without accountability is half a system: when something goes wrong, the log is how you know who went where and when.
The security-honest details are what separate this from a toy. Simple RFID cards broadcast a fixed ID that a cheap cloner can copy, so for anything sensitive the design points to authenticated smart cards (which perform a cryptographic challenge instead of just announcing a number) and to defence-in-depth (card plus PIN, anti-passback, tamper detection). The door's fail mode is a deliberate choice — fail-secure (locked on power loss) for a secure store, fail-safe (unlocked on power loss) for a fire-egress door, and the code must make that explicit. And because a door must keep working when the network is down, the controller keeps a local copy of permissions and logs offline, syncing when the link returns. It is candid that a DIY controller is not a substitute for certified security hardware where that is required — but as a genuinely-architected access system, it teaches and does the real job: the right people through the right doors, with a record you can trust.
What this project does
- Reads RFID/smart-card credentials at the door
- Checks each credential against authorised users and their roles/schedules
- Actuates the lock only when access is permitted
- Writes a timestamped, tamper-evident audit log of every grant and denial
- Chooses a deliberate door fail mode (fail-safe vs fail-secure)
- Operates offline with a local permissions copy, syncing when online
- Supports defence-in-depth (card+PIN, anti-passback, tamper) and warns about cloneable cards
Real-World Applications
| Setting | How it is used |
|---|---|
| Office / lab / server-room doors | Role-based entry where different staff have different rights, with an audit trail for compliance and incident review. |
| Society / apartment common areas | Resident cards for gates, gyms and clubhouses with schedules and logs, and easy revocation of lost cards. |
| Makerspaces / shared facilities | Access to rooms and machines gated by training/role, logged per user. |
| Small business premises | Controlled entry with the right fail mode and offline resilience where a network drop must not lock everyone out (or in). |
Deployment contexts where a build of this kind earns its keep.
Features & Capabilities
- Role- and schedule-based permissions, not all-or-nothing access
- Complete, tamper-evident audit trail of entries and refusals
- Explicit fail-safe/fail-secure door behaviour
- Offline operation with local permissions + log sync
- Honest RFID security: authenticated smart cards and layered defences
- Anti-passback and tamper options
- Networked management with local autonomy
Difficulty, Time & Required Skills
| Attribute | Value |
|---|---|
| Difficulty level | Intermediate |
| Estimated completion time | 12–18 hours |
| Indicative build cost | ₹3,500 – ₹5,500 |
| Primary discipline | Security |
| Reference platform | ESP32 DevKit V1 (ESP-WROOM-32) |
Skills you should have (or will pick up)
- Reading RFID/smart cards (and understanding their cloneability)
- Designing role/schedule-based permissions
- Tamper-evident logging and offline sync
- Choosing and wiring fail-safe vs fail-secure locks
- Defence-in-depth (card+PIN, anti-passback, tamper)
Bill of Materials
Every part below is commonly available from Indian and international hobby-electronics suppliers. Prices are indicative 2026 retail figures in Indian rupees and will drift — treat them as a budgeting guide, not a quotation.
| Component | Key specification | Qty | Approx. cost |
|---|---|---|---|
| ESP32 DevKit V1 (ESP-WROOM-32) Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly. | Dual-core Xtensa LX6 @ 240 MHz, 520 KB SRAM, 4 MB flash, Wi-Fi 802.11 b/g/n + BLE 4.2, 34 GPIO, 18× 12-bit ADC, 2× 8-bit DAC | 1 | ₹450 |
| MFRC522 13.56 MHz RFID reader + card + fob Strictly 3.3 V — 5 V on the SPI pins kills the chip. | ISO/IEC 14443A, MIFARE Classic 1K, 0–60 mm range, up to 10 Mbit/s SPI | 1 | ₹180 |
| 5 V single-channel opto-isolated relay module Cut the JD-VCC jumper and feed the coil separately for true isolation. | SPDT contacts rated 10 A @ 250 VAC / 10 A @ 30 VDC, opto-isolated input | 1 | ₹90 |
| 0.96″ SSD1306 OLED display Static images burn in — invert or scroll the screen periodically. | 128 × 64 monochrome, 1.3–3.3 V logic, 100 kHz–400 kHz I²C | 1 | ₹250 |
| Active piezo buzzer 5 V Active buzzers make tone on DC; passive ones need a PWM carrier. | 85 dB at 10 cm, 2.3 kHz resonance, 12 mm diameter | 1 | ₹25 |
| 4×4 matrix membrane keypad Debounce in software — 20 ms is enough for membrane keys. | 16 keys, 8-wire matrix, 35 × 76 mm adhesive-backed | 1 | ₹70 |
| DS3231 precision RTC + CR2032 backup Vastly better than DS1307; some boards trickle-charge a non-rechargeable cell — cut that resistor. | ±2 ppm (±1 min/year), temperature-compensated crystal, alarms, 32 kHz out | 1 | ₹180 |
| microSD card 32 GB A1 class For 24/7 loggers buy a high-endurance card — normal cards die in months. | A1 rated, 10 MB/s random write, UHS-I, endurance-grade recommended | 1 | ₹450 |
| Electric door lock (fail-safe or fail-secure) Fail mode is a security AND life-safety decision | Electric strike or maglock chosen for the required fail mode and door | 1 | ₹1,200 |
| Authenticated smart-card reader/cards For real security; plain UID cards are cloneable | MIFARE DESFire / cards supporting cryptographic auth (vs plain UID) | 1 | ₹900 |
| Request-to-exit + door sensor | REX button and reed contact for safe egress and door-ajar/forced logging | 1 | ₹300 |
| Tamper switch + backup power | Enclosure tamper and battery backup so power loss is handled by design | 1 | ₹400 |
Estimated total: ₹4,495, excluding tools, shipping and consumables.
Tools and consumables
- Soldering iron (temperature controlled, 350 °C) with 0.8 mm 60/40 or lead-free solder
- Digital multimeter — continuity, DC volts and current ranges
- Wire strippers, flush cutters and a small set of precision screwdrivers
- Heat-shrink tubing and a heat gun (or a lighter, carefully)
- A laptop with a USB port and the toolchain listed above
Hardware Specifications
| Part | Specification | Supply | Interface | Reference |
|---|---|---|---|---|
| ESP32 DevKit V1 (ESP-WROOM-32) | Dual-core Xtensa LX6 @ 240 MHz, 520 KB SRAM, 4 MB flash, Wi-Fi 802.11 b/g/n + BLE 4.2, 34 GPIO, 18× 12-bit ADC, 2× 8-bit DAC | 3.3 V logic / 5 V USB | UART, SPI, I²C, I²S, CAN, PWM | Datasheet |
| MFRC522 13.56 MHz RFID reader + card + fob | ISO/IEC 14443A, MIFARE Classic 1K, 0–60 mm range, up to 10 Mbit/s SPI | 2.5–3.6 V | SPI (also I²C/UART) | Datasheet |
| 5 V single-channel opto-isolated relay module | SPDT contacts rated 10 A @ 250 VAC / 10 A @ 30 VDC, opto-isolated input | 5 V coil | Digital (active-low) | Datasheet |
| 0.96″ SSD1306 OLED display | 128 × 64 monochrome, 1.3–3.3 V logic, 100 kHz–400 kHz I²C | 3.3–5 V | I²C (0x3C) | Datasheet |
| Active piezo buzzer 5 V | 85 dB at 10 cm, 2.3 kHz resonance, 12 mm diameter | 3–5 V | Digital / PWM | Datasheet |
| 4×4 matrix membrane keypad | 16 keys, 8-wire matrix, 35 × 76 mm adhesive-backed | logic level | 8 GPIO matrix scan | Datasheet |
| DS3231 precision RTC + CR2032 backup | ±2 ppm (±1 min/year), temperature-compensated crystal, alarms, 32 kHz out | 2.3–5.5 V | I²C (0x68) | Datasheet |
| microSD card 32 GB A1 class | A1 rated, 10 MB/s random write, UHS-I, endurance-grade recommended | 3.3 V | SDIO / SPI | Datasheet |
Consolidated electrical and interface specifications for every active part in the build.
Power Budget & Supply Sizing
Add up the typical active current of every part, then size the supply with at least 50 % headroom so transmit bursts and motor inrush never brown out the controller.
| Load | Supply rail | Typical current (mA) | Notes |
|---|---|---|---|
| ESP32 DevKit V1 (ESP-WROOM-32) | 3.3 V logic / 5 V USB | 160 | Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly. |
| MFRC522 13.56 MHz RFID reader + card + fob | 2.5–3.6 V | 26 | Strictly 3.3 V — 5 V on the SPI pins kills the chip. |
| 5 V single-channel opto-isolated relay module | 5 V coil | 70 | Cut the JD-VCC jumper and feed the coil separately for true isolation. |
| 0.96″ SSD1306 OLED display | 3.3–5 V | 20 | Static images burn in — invert or scroll the screen periodically. |
| Active piezo buzzer 5 V | 3–5 V | 30 | Active buzzers make tone on DC; passive ones need a PWM carrier. |
| 4×4 matrix membrane keypad | logic level | 1 | Debounce in software — 20 ms is enough for membrane keys. |
| DS3231 precision RTC + CR2032 backup | 2.3–5.5 V | 0.2 | Vastly better than DS1307; some boards trickle-charge a non-rechargeable cell — cut that resistor. |
| microSD card 32 GB A1 class | 3.3 V | 100 | For 24/7 loggers buy a high-endurance card — normal cards die in months. |
Summed typical draw is 407.2 mA. With a 1.5× design margin the supply should deliver at least 700 mA continuously at the stated rail voltage.
Software Requirements & Development Environment
Reference toolchain: Arduino IDE 2.3.x with the ESP32 board package 3.x (or PlatformIO on VS Code). Anything newer normally works; anything older may lack the board definitions used here.
- Install the Arduino IDE 2.3.x (or PlatformIO if you prefer a real editor and dependency locking).
- Add
https://espressif.github.io/arduino-esp32/package_esp32_index.jsonunder File → Preferences → Additional Board Manager URLs, then install esp32 from the Boards Manager. - Set the correct port under Tools → Port. On Linux add yourself to the
dialoutgroup:sudo usermod -aG dialout $USERand log out and back in. - Open the Serial Monitor at 115200 baud — every sketch here logs its state there.
- Keep File → Preferences → Show verbose output during: compilation switched on while you are debugging build errors.
Required libraries
| Library | Why it is needed | Install |
|---|---|---|
| WiFi (ESP32 core) bundled | Station/AP connection management for the ESP32. | Bundled with the ESP32 Arduino core |
| PubSubClient 2.8 | Lightweight MQTT 3.1.1 client for constrained devices. | Library Manager → "PubSubClient" by Nick O'Leary |
| MFRC522 1.4.x | SPI driver and MIFARE authentication for the RC522 reader. | Library Manager → "MFRC522" by GithubCommunity |
| Adafruit SSD1306 + GFX 2.5.x | Framebuffer and text/graphics primitives for the OLED. | Library Manager → "Adafruit SSD1306" |
| NTPClient / configTime bundled | Wall-clock time from an NTP server for timestamping. | Bundled (`configTime()` on ESP32) |
| Preferences (NVS) bundled | Wear-levelled key/value storage in ESP32 flash for settings. | Bundled with the ESP32 core |
| SQLite 3.45+ | Zero-configuration embedded database for local logs. | Bundled with Python (`import sqlite3`) |
Block Diagram
The block diagram shows the functional decomposition of the system — what senses, what decides, what acts, and where the data ends up.
Circuit Diagram & Wiring
Every signal line in the build is shown below, followed by a pin-by-pin connection table you can work through with a multimeter in hand.
| Peripheral | Peripheral pin | Controller pin | Signal |
|---|---|---|---|
| RC522 reader | SPI | GPIO 18/19/23/5 | Card read (SPI) |
| RC522 | RST | GPIO 4 | Reader reset |
| Keypad (PIN) | rows/cols | GPIO matrix | Optional second factor |
| Door sensor / REX | in | GPIO 34/35 | Door contact + request-to-exit |
| Lock relay | IN | GPIO 26 | Actuate strike/maglock |
| DS3231 RTC | SDA/SCL | GPIO 21/22 | Timestamps for the log |
| microSD | SPI | shared SPI + CS | Local permissions + audit log |
| OLED + buzzer | I²C / IN | GPIO 21-22 / 27 | Prompt + grant/deny feedback |
Wire one row at a time and tick it off — most "it does not work" reports trace back to a single swapped pair.
Wiring explanation
- Wire the lock through a relay sized for it, and choose the strike/maglock and relay-normal state to give the intended fail mode: fail-secure (locked on power loss) or fail-safe (unlocked on power loss).
- Always provide a request-to-exit path and never let a fail-secure door trap people against fire-egress rules.
- Give the RTC a coin-cell backup so audit timestamps survive power loss — a log with wrong times is worthless.
- Add a tamper switch on the enclosure and battery backup so power/tamper events are handled and logged by design.
- For real security, use an authenticated smart-card reader; a plain UID reader is convenient for learning but cloneable.
System Architecture
Read the stack from the bottom up: physical hardware, the firmware that drives it, the transport that moves data off the device, and the software a human actually looks at.
Working Principle
Access control is an authentication-then-authorisation problem, and both halves have to be right. Authentication asks "is this really who they claim to be?"; authorisation asks "is this identity allowed through this door now?". A toy reads a card ID and, if it is in a list, opens the door — collapsing both questions into one weak check. A real system separates them: it first establishes the credential's identity as securely as the hardware allows, then evaluates that identity against a permissions model. Conflating the two is how systems get both insecure (any cloned card works) and inflexible (no roles, no schedules).
The uncomfortable truth about ordinary RFID is that it fails the authentication half. A basic card (a 125 kHz tag or a MIFARE Classic in UID mode) simply broadcasts a fixed serial number when energised, and a cheap handheld cloner can read and replay it — so treating a valid UID as proof of identity is like treating a shouted name as proof. That is fine for low-stakes convenience (a gym locker) and useless for a server room. The honest design therefore distinguishes stakes: for sensitive access it uses authenticated smart cards (e.g. DESFire) that never reveal a static secret but instead perform a cryptographic challenge-response the reader verifies, and/or adds a second factor (a PIN) so a cloned or stolen card alone is not enough. Naming this limitation, rather than hiding it, is part of building the system responsibly.
Authorisation is where roles and schedules live, because access is almost never binary. Each credential maps to a person with a role, and the role defines which doors they may open and when — a cleaner in the lobby but not the lab, a contractor only 9-to-5, a manager everywhere, a lost card revoked instantly. Modelling permissions this way (rather than a flat allow-list per door) makes the system both more secure (least privilege — people can open only what they need) and vastly more manageable (change a role, not every door). Layered options like anti-passback (a card cannot enter twice without exiting, defeating card-sharing) extend the same principle.
Finally, a door controller must be trustworthy in two operational senses. It must keep an audit trail — every grant and every denial, timestamped and tamper-evident — because access without accountability is only half a control, and the log is the evidence when an incident is investigated; that means append-only local storage with a backed-up clock, mirrored to a server. And it must handle failure deliberately: what does the door do when power or network is lost? A fail-secure lock stays locked (protecting a secure store), a fail-safe lock releases (allowing fire egress) — a choice dictated by security needs and life-safety law, and one the design must make explicit, never accidental. And because a network outage must not lock everyone out (or leave the door unmanaged), the controller holds its permissions and logs locally and operates offline, syncing when the link returns. Roles, audit, fail mode, offline resilience — these unglamorous properties are what make an access controller real rather than a relay that clicks on a card.
The maths behind it
Authorisation decision
grant if:
authenticated(credential) AND
role_of(credential) permits door D AND
now ∈ schedule(role, D) AND
(second_factor_ok if required) AND
anti_passback_ok(credential, D)
Else deny. Every path — grant or deny — is logged.
Cloneability of plain UID
Plain card: presents fixed UID U.
attacker reads U (proximity) → writes clone → replays U
system sees U → cannot distinguish clone from original.
Authenticated card: reader sends nonce N; card returns
MAC_k(N) using secret k it never reveals.
clone without k fails → replay/clone defeated.
Fail mode choice
Power/controller loss:
FAIL-SECURE lock stays LOCKED (security priority)
FAIL-SAFE lock RELEASES (life-safety egress priority)
Driven by: is this a secure store or a fire-egress route?
Life-safety egress must never be trapped — law governs this.
Program Flowchart
The firmware is a single cooperative loop. Nothing blocks for long, so networking, sensing and the user interface all stay responsive.
Assembly Instructions
Build on a breadboard first and only commit to solder once the whole system has run for an hour without a fault.
Wire the lock for the right fail mode
Select an electric strike or maglock and wire it through the relay so the door's behaviour on power loss matches its purpose — fail-secure for a secure room, fail-safe for a fire-egress door — and provide a request-to-exit path.
Add battery backup and a tamper switch so power and tamper events are handled by design.
Fit reader, clock and storage
Connect the RFID/smart-card reader, a real-time clock with coin-cell backup for accurate log timestamps, and local storage (SD/flash) for the permissions copy and the append-only audit log. Add a keypad if using a PIN second factor.
Set up feedback and networking
Add an OLED and buzzer for clear grant/deny feedback, and Wi-Fi to a management server for user/role administration and log sync — while keeping the door fully functional offline.
Step-by-Step Implementation Guide
Work through these in order. Each step ends in something you can observe, so a failure is always localised to the step you just finished.
Authenticate the credential
For plain cards, read the UID (and treat it as low-assurance); for smart cards, perform the card's challenge-response authentication so a clone without the secret fails.
Authorise against role, schedule and factors
Look up the credential's role, check it permits this door at this time, verify any required second factor and anti-passback, then decide — and log the decision either way.
cppauthorise.inostruct Cred { char id[24]; int role; }; struct Decision { bool grant; const char *reason; }; Decision authorise(const Cred &c, int door, time_t now, bool pinOk, bool pinRequired) { if (c.role < 0) return {false, "unknown credential"}; if (!rolePermitsDoor(c.role, door)) return {false, "role not allowed here"}; if (!withinSchedule(c.role, door, now)) return {false, "outside allowed hours"}; if (pinRequired && !pinOk) return {false, "second factor failed"}; if (!antiPassbackOk(c.id, door)) return {false, "anti-passback"}; return {true, "granted"}; } void handleCard(const Cred &c, int door, bool pinOk, bool pinRequired) { time_t now = timeNow(); Decision d = authorise(c, door, now, pinOk, pinRequired); logEvent(c.id, door, now, d.grant, d.reason); // append-only, always if (d.grant) unlockDoor(door); // per configured fail mode feedback(d.grant, d.reason); }if (c.role < 0) return {false, "unknown credential"}An unrecognised credential is denied and logged — the first authorisation gate, and itself a security-relevant event worth recording.if (!rolePermitsDoor(c.role, door))Authorisation is by role and door, enforcing least privilege so a person can open only what their role needs.if (!withinSchedule(c.role, door, now))Time-of-day schedules mean access can be granted only when appropriate — a contractor in hours, not at 2 a.m.logEvent(c.id, door, now, d.grant, d.reason); // append-only, alwaysEvery decision, grant or deny, is written to the tamper-evident log before the door acts — accountability is not optional.if (d.grant) unlockDoor(door); // per configured fail modeThe lock is actuated according to the door's deliberate fail mode, keeping security and life-safety behaviour explicit.Log tamper-evidently and sync
Append each event with a sequence number/timestamp to local storage, mirror to the server when online, and forward any offline backlog on reconnect.
Complete Source Code
The listing below is complete and compiles as written — there are no elided sections. Read the annotations under each block before you upload it.
/* ═══════════════════════════════════════════════════════════════
RFID Access Control — ESP32, role-based, audited, offline-capable
Authenticates a credential, authorises against role + schedule +
optional PIN and anti-passback, actuates the lock per a deliberate
fail mode, and writes a tamper-evident audit log (local + synced).
══════════════════════════════════════════════════════════════════ */
#include <WiFi.h>
#include <PubSubClient.h>
#include <SPI.h>
#include <MFRC522.h>
#include <Wire.h>
#include <RTClib.h>
#include <SD.h>
#include <Preferences.h>
#define RC522_SS 5
#define RC522_RST 4
#define PIN_LOCK 26
#define SD_CS 15
#define DOOR_ID 1
#define PIN_REQUIRED false // set true for two-factor doors
MFRC522 rfid(RC522_SS, RC522_RST);
RTC_DS3231 rtc;
Preferences perms; // local permissions cache
WiFiClient net; PubSubClient mqtt(net);
uint32_t logSeq = 0;
// Look up a credential's role from the local cache (synced from server).
int roleOf(const String &uid) {
perms.begin("acl", true);
int r = perms.getInt(uid.c_str(), -1); // -1 = unknown
perms.end();
return r;
}
bool rolePermitsDoor(int role, int door) { /* role→door map */ return role >= 0; }
bool withinSchedule(int role, int door, DateTime now) { /* schedule */ return true; }
bool antiPassbackOk(const String &uid, int door) { /* track in/out */ return true; }
void logEvent(const String &uid, bool grant, const char *reason, DateTime t) {
File f = SD.open("/access.csv", FILE_APPEND); // append-only
if (f) {
f.printf("%lu,%04d-%02d-%02d %02d:%02d:%02d,%s,%d,%d,%s\n",
(unsigned long)logSeq++, t.year(),t.month(),t.day(),
t.hour(),t.minute(),t.second(), uid.c_str(), DOOR_ID,
grant?1:0, reason);
f.close();
}
if (mqtt.connected()) { // mirror to server
char m[160];
snprintf(m,sizeof m,
"{\"uid\":\"%s\",\"door\":%d,\"grant\":%d,\"reason\":\"%s\"}",
uid.c_str(), DOOR_ID, grant?1:0, reason);
mqtt.publish("acl/door1/event", m);
}
}
void unlockDoor() {
digitalWrite(PIN_LOCK, HIGH); // energise per configured fail mode
delay(3000); // relock timeout
digitalWrite(PIN_LOCK, LOW);
}
void setup() {
Serial.begin(115200);
pinMode(PIN_LOCK, OUTPUT);
SPI.begin();
rfid.PCD_Init();
Wire.begin(21,22); rtc.begin();
SD.begin(SD_CS);
WiFi.begin(WIFI_SSID, WIFI_PASS);
mqtt.setServer(MQTT_HOST, 1883);
}
void loop() {
if (!mqtt.connected() && WiFi.status()==WL_CONNECTED) mqtt.connect("acl-1");
mqtt.loop();
if (!rfid.PICC_IsNewCardPresent() || !rfid.PICC_ReadCardSerial()) {
delay(50); return;
}
// NOTE: for plain cards this UID is cloneable — low assurance.
// For real security, do the smart-card challenge-response here instead.
String uid;
for (byte i = 0; i < rfid.uid.size; i++) {
char b[3]; snprintf(b,sizeof b,"%02X", rfid.uid.uidByte[i]); uid += b;
}
rfid.PICC_HaltA();
DateTime now = rtc.now();
int role = roleOf(uid);
bool pinOk = PIN_REQUIRED ? checkPin() : true;
bool grant = false; const char *reason;
if (role < 0) reason = "unknown credential";
else if (!rolePermitsDoor(role, DOOR_ID)) reason = "not allowed here";
else if (!withinSchedule(role, DOOR_ID, now)) reason = "outside hours";
else if (PIN_REQUIRED && !pinOk) reason = "second factor failed";
else if (!antiPassbackOk(uid, DOOR_ID)) reason = "anti-passback";
else { grant = true; reason = "granted"; }
logEvent(uid, grant, reason, now); // always log the decision
feedback(grant, reason);
if (grant) unlockDoor();
}
Configuration & Calibration
Configuration steps
- Define roles, the role→door map and schedules; sync them to each controller's local cache.
- Set PIN_REQUIRED per door and configure anti-passback where needed.
- Choose and wire the fail mode (fail-safe/fail-secure) appropriate to each door and local code.
- Configure the RTC/NTP, tamper-evident logging, and server sync.
Calibration procedure
An uncalibrated sensor produces confident, precise, wrong numbers. Do this once per physical unit and record the constants.
Clock
Sync the RTC (NTP when online) and confirm it holds time on its coin cell through a power-off, so audit timestamps are accurate.
Reader/credentials
Verify reliable reads at the intended range; for smart cards, confirm the challenge-response authenticates genuine cards and rejects clones.
Fail mode
Physically verify the door behaves as intended on power loss (locked vs released) and that egress is never trapped.
Network Architecture & Connectivity
Communication protocol
Doors authorise locally from a synced permissions cache and publish each event; the server distributes user/role/schedule updates and collects audit logs. Losing the network degrades management, not the door.
| Topic / endpoint | Direction | Payload |
|---|---|---|
acl/door1/event | node → server | uid, door, grant/deny, reason, time |
acl/door1/perms | server → node | role/schedule/revocation updates |
acl/door1/status | node → server | tamper, door-forced, battery, RTC health |
Message contract between the device and the broker.
Cloud platform configuration
A server manages users, roles, schedules and instant revocations, pushes them to controllers, and aggregates every door's audit log for compliance and incident review.
Dashboard setup
User/role management, per-door live event stream and searchable audit history, and door-status/tamper indicators.
Mobile app integration
Alerts on tamper, door-forced/held-open, or repeated denials at a door.
Security considerations
- Use TLS and authenticate controllers to the server; protect reader/card keys.
- Keep logs append-only and mirrored so records cannot be silently altered.
- Support instant revocation and least-privilege roles; keep the door functional and audited offline.
Testing Procedure & Expected Output
Test from the bottom up. Confirm power, then each sensor in isolation, then the integrated loop — the first failing step tells you exactly where to look.
| Test | What you should see |
|---|---|
| Present an authorised card in hours | Granted, door unlocks, event logged as grant |
| Present an authorised card out of hours | Denied with "outside hours"; logged |
| Present an unknown/revoked card | Denied with "unknown credential"; logged |
| Two-factor door without PIN | Denied "second factor failed"; logged |
| Pull the network | Door still authorises from local cache; log queues and syncs later |
| Cut power | Door assumes its configured fail mode; egress not trapped |
Bench-test checklist. If a row fails, stop and fix it before moving on.
Expected output
The management dashboard lists users/roles and shows a live audit log of grants and denials per door; the controller shows a grant/deny prompt locally.
seq,timestamp,uid,door,grant,reason
10231,2026-07-27 09:02:11,04A3F2C1,1,1,granted
10232,2026-07-27 21:44:03,04A3F2C1,1,0,outside hours
10233,2026-07-27 21:45:10,9F1177AA,1,0,unknown credential
The log records both successful entries and refusals with their reasons — the accountability that turns a door opener into an access-control system.
Troubleshooting: Common Errors & Fixes
Performance Optimisation
- Authorise from the local cache for instant, network-independent decisions.
- Keep the audit log append-only and rotate by day/month to stay fast and tamper-evident.
- Debounce reads and set a sensible relock timeout so the door is responsive but secure.
- Sync permissions and log backlogs in the background, never blocking a door decision.
- Replace every
delay()with amillis()comparison — blocking delays are the single most common cause of dropped readings. - Sample sensors on a fixed cadence and publish on a slower one; you almost never need to transmit at the sampling rate.
- Move networking into its own FreeRTOS task so a slow DNS lookup cannot stall the control loop.
- Use
uint8_t/uint16_twhere the range allows; on an 8-bit AVR a 32-bit add costs four times as much. - Batch several samples into one MQTT publish. Radio time, not CPU time, dominates the energy budget.
- Set the MQTT keep-alive to a value that matches your reporting interval so the broker does not churn reconnections.
- For battery builds use deep sleep between samples: an ESP32 drops from ~160 mA awake to about 10 µA asleep, which is the difference between days and months of runtime.
Safety Precautions
- Choose the door fail mode for both security and life-safety; never trap people against fire-egress requirements.
- A plain RFID UID is not proof of identity — use authenticated cards and/or a second factor for anything sensitive.
- A DIY controller is not certified security/fire hardware; use certified equipment where codes require it.
- Protect the audit log and card keys; an access system is only as trustworthy as its records and secrets.
- Mains voltage kills. Anything on the load side of the relay is at 230 V. Do not work on a powered circuit, and never leave exposed mains wiring on a bench where someone could touch it.
- Keep at least 6 mm of creepage between the mains and low-voltage sides of any board you make, and never route mains tracks under the microcontroller.
- Have a qualified electrician do the final installation into a consumer unit or wall fitting. In most jurisdictions this is a legal requirement, not a suggestion.
- Fit an RCD/RCBO upstream and fuse the load appropriately for its rating.
- Wear eye protection when soldering or cutting, and solder in a ventilated space — rosin flux fumes are a respiratory irritant.
- Power the circuit through a bench supply with a current limit while you are testing. A 300 mA limit turns a wiring mistake into a beep instead of a dead board.
- Disconnect power before changing any wiring. Hot-plugging a sensor onto a live bus is the fastest way to lose a controller.
Maintenance
- Review the audit log and revoke lost/stale credentials promptly.
- Replace the RTC coin cell on schedule; verify time accuracy.
- Test the fail mode, REX and tamper periodically; check battery backup.
- Rotate reader/card keys where supported and keep permission syncs current.
- Re-check every screw terminal and header after the first week — thermal cycling loosens connections that felt tight on day one.
- Rotate the microSD card annually and keep an image of the working system. Cards used as loggers wear out silently.
- Keep the broker and dashboard containers patched, and rotate device credentials at least once a year.
- Recalibrate at the interval given in the calibration section, and keep the constants in a text file next to the firmware — not only in flash.
- Keep a short logbook of firmware versions and what changed. Six months later you will not remember why that constant is 1.083.
Future Improvements & Upgrades
A working v1 is a platform, not a finish line. These are the upgrades that add the most capability for the least rework.
- Add biometric or mobile-credential (NFC phone) second factors.
- Add full anti-passback and occupancy tracking across doors.
- Cryptographically sign each log record for a court-defensible audit trail.
- Integrate with HR/identity systems for automatic provisioning and de-provisioning.
- Design a proper PCB. Once the breadboard version has run for a month, moving to a two-layer board removes the intermittent-contact failures that dominate prototype faults.
- Add over-the-air firmware updates so you never have to physically reach a deployed node again.
- Add persistent local storage (microSD or the on-chip flash) so a network outage does not create a hole in your data.
- Move configuration out of the source: a captive-portal setup page or a JSON config file makes the build reusable without a recompile.
- Add a battery and solar option so the unit survives a power cut and can be sited away from a socket.
- Write a small test harness that feeds synthetic sensor values through the decision logic, so you can validate thresholds without physically triggering the event.
Frequently Asked Questions
References & Learning Resources
These are the primary sources worth reading in full. Manufacturer datasheets always outrank forum posts when the two disagree.