Siddhant Kumar
Project 051 · Security

RFID Access Control.

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.

Intermediate 12–18 hours 34 min read RFIDAccessLogs
Jump to source Bill of materials
RFID Access Control — reference build illustration MCU VCC · GND · SIG · NC
Difficulty
Intermediate
Build time
12–18 hours
Indicative cost
₹3,500 – ₹5,500
Platform
ESP32 DevKit V1 (ESP-WROOM-32)
Category
Security
Last updated
28 July 2026
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.

An ESP32 development board with the ESP-WROOM-32 module and USB connector
A door controller reads a card and decides access by role and schedule — not just whether a number is on a list. Photograph sourced from Wikimedia Commons — ESP32 Espressif ESP-WROOM-32 Dev Board.jpg. Reused under the licence stated on that page; please check it before republishing.

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

SettingHow it is used
Office / lab / server-room doorsRole-based entry where different staff have different rights, with an audit trail for compliance and incident review.
Society / apartment common areasResident cards for gates, gyms and clubhouses with schedules and logs, and easy revocation of lost cards.
Makerspaces / shared facilitiesAccess to rooms and machines gated by training/role, logged per user.
Small business premisesControlled 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

AttributeValue
Difficulty levelIntermediate
Estimated completion time12–18 hours
Indicative build cost₹3,500 – ₹5,500
Primary disciplineSecurity
Reference platformESP32 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.

ComponentKey specificationQtyApprox. 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 DAC1₹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 SPI1₹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 input1₹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²C1₹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 diameter1₹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-backed1₹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 out1₹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 recommended1₹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 door1₹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 sensorREX button and reed contact for safe egress and door-ajar/forced logging1₹300
Tamper switch + backup powerEnclosure tamper and battery backup so power loss is handled by design1₹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

PartSpecificationSupplyInterfaceReference
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 DAC3.3 V logic / 5 V USBUART, SPI, I²C, I²S, CAN, PWMDatasheet
MFRC522 13.56 MHz RFID reader + card + fobISO/IEC 14443A, MIFARE Classic 1K, 0–60 mm range, up to 10 Mbit/s SPI2.5–3.6 VSPI (also I²C/UART)Datasheet
5 V single-channel opto-isolated relay moduleSPDT contacts rated 10 A @ 250 VAC / 10 A @ 30 VDC, opto-isolated input5 V coilDigital (active-low)Datasheet
0.96″ SSD1306 OLED display128 × 64 monochrome, 1.3–3.3 V logic, 100 kHz–400 kHz I²C3.3–5 VI²C (0x3C)Datasheet
Active piezo buzzer 5 V85 dB at 10 cm, 2.3 kHz resonance, 12 mm diameter3–5 VDigital / PWMDatasheet
4×4 matrix membrane keypad16 keys, 8-wire matrix, 35 × 76 mm adhesive-backedlogic level8 GPIO matrix scanDatasheet
DS3231 precision RTC + CR2032 backup±2 ppm (±1 min/year), temperature-compensated crystal, alarms, 32 kHz out2.3–5.5 VI²C (0x68)Datasheet
microSD card 32 GB A1 classA1 rated, 10 MB/s random write, UHS-I, endurance-grade recommended3.3 VSDIO / SPIDatasheet

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.

LoadSupply railTypical current (mA)Notes
ESP32 DevKit V1 (ESP-WROOM-32)3.3 V logic / 5 V USB160Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly.
MFRC522 13.56 MHz RFID reader + card + fob2.5–3.6 V26Strictly 3.3 V — 5 V on the SPI pins kills the chip.
5 V single-channel opto-isolated relay module5 V coil70Cut the JD-VCC jumper and feed the coil separately for true isolation.
0.96″ SSD1306 OLED display3.3–5 V20Static images burn in — invert or scroll the screen periodically.
Active piezo buzzer 5 V3–5 V30Active buzzers make tone on DC; passive ones need a PWM carrier.
4×4 matrix membrane keypadlogic level1Debounce in software — 20 ms is enough for membrane keys.
DS3231 precision RTC + CR2032 backup2.3–5.5 V0.2Vastly better than DS1307; some boards trickle-charge a non-rechargeable cell — cut that resistor.
microSD card 32 GB A1 class3.3 V100For 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.json under 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 dialout group: sudo usermod -aG dialout $USER and 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

LibraryWhy it is neededInstall
WiFi (ESP32 core) bundledStation/AP connection management for the ESP32.Bundled with the ESP32 Arduino core
PubSubClient 2.8Lightweight MQTT 3.1.1 client for constrained devices.Library Manager → "PubSubClient" by Nick O'Leary
MFRC522 1.4.xSPI driver and MIFARE authentication for the RC522 reader.Library Manager → "MFRC522" by GithubCommunity
Adafruit SSD1306 + GFX 2.5.xFramebuffer and text/graphics primitives for the OLED.Library Manager → "Adafruit SSD1306"
NTPClient / configTime bundledWall-clock time from an NTP server for timestamping.Bundled (`configTime()` on ESP32)
Preferences (NVS) bundledWear-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.

RFID Access Control — system block diagramFunctional block diagram of the RFID Access Control system. PresentCard/smart-cardRC522PIN (opt)second factorDecideESP32role + schedulePermissionslocal copyAct + recordLock relayfail mode setAudit logtamper-evidentManageServerusers + logsrightrightnone
RFID Access Control — system block diagram

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.

RFID Access Control — wiring schematicConnection schematic showing which controller pin drives each peripheral. Sensors / InputsControllerActuators / OutputsESP32 DevKit V1(ESP-WROOM-32)3.3 V logic / 5 V USBRC522 readerGPIO 18/19/23/5Card read (SPI)RC522GPIO 4Reader resetKeypad (PIN)GPIO matrixOptional secondfactorDoor sensor / REXGPIO 34/35Door contact +request-to-exitLock relayGPIO 26Actuatestrike/maglockDS3231 RTCGPIO 21/22Timestamps for thelogmicroSDshared SPI + CSLocal permissions +audit logOLED + buzzerGPIO 21-22 / 27Prompt + grant/denyfeedback
RFID Access Control — wiring schematic
PeripheralPeripheral pinController pinSignal
RC522 readerSPIGPIO 18/19/23/5Card read (SPI)
RC522RSTGPIO 4Reader reset
Keypad (PIN)rows/colsGPIO matrixOptional second factor
Door sensor / REXinGPIO 34/35Door contact + request-to-exit
Lock relayINGPIO 26Actuate strike/maglock
DS3231 RTCSDA/SCLGPIO 21/22Timestamps for the log
microSDSPIshared SPI + CSLocal permissions + audit log
OLED + buzzerI²C / INGPIO 21-22 / 27Prompt + 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.
A small monochrome OLED display module
An OLED gives clear grant/deny feedback while the event is written to a tamper-evident audit log. Photograph sourced from Wikimedia Commons — OLED display module.jpg. Reused under the licence stated on that page; please check it before republishing.

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.

RFID Access Control — architecture stackLayered architecture from hardware to user interface. Hardware layerESP32 DevKit V1 (ESP-WROOM-32) · MFRC522 13.56 MHz RFID reader + card +fobDriver layerwifi · pubsub · mfrc522 · ssd1306Application logicsampling loop · filtering · thresholds · state machineTransport layerWi-Fi + MQTT → access-management server (local-first) · TLS · retry andbackoffPresentation layerdashboard · mobile notifications · historical charts
RFID Access Control — architecture stack

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

plainAuthorisation 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

plainCloneability 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

plainFail 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.

RFID Access Control — firmware flowchartControl flow through the main program loop. Card presentedCredential known?Check role + scheduleDeny + logCheck role + scheduleAllowed here and now (andPIN if required)?Unlock + log grantDeny + logUnlock + log grantDeny + logRelock; sync log when online
RFID Access Control — firmware flowchart

Assembly Instructions

Build on a breadboard first and only commit to solder once the whole system has run for an hour without a fault.

  1. 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.

  2. 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.

  3. 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.

  1. 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.

  2. 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.ino
    struct 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.
  3. 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.

cpprfid-access-control.ino
/* ═══════════════════════════════════════════════════════════════
   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();
}
int roleOf(const String &uid)Permissions come from a local cache synced from the server, so the door authorises correctly even with the network down — offline resilience by design.
// NOTE: for plain cards this UID is cloneableThe code is explicit that a plain UID is low-assurance and points to where real smart-card authentication belongs — honesty about the security limit, in the code itself.
File f = SD.open("/access.csv", FILE_APPEND); // append-onlyEvents are appended with a sequence number and timestamp, never overwritten, so the audit trail is tamper-evident.
logEvent(uid, grant, reason, now); // always log the decisionBoth grants and denials are logged before any feedback or unlock, so every access attempt is accountable.
digitalWrite(PIN_LOCK, HIGH); // energise per configured fail modeThe lock is driven according to the door's chosen fail mode, keeping the security-versus-egress behaviour a deliberate configuration, not an accident.

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.

  1. 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.

  2. Reader/credentials

    Verify reliable reads at the intended range; for smart cards, confirm the challenge-response authenticates genuine cards and rejects clones.

  3. Fail mode

    Physically verify the door behaves as intended on power loss (locked vs released) and that egress is never trapped.

Network Architecture & Connectivity

RFID Access Control — network topologyPath taken by telemetry from field node to end user. Edge nodesGatewayCloudClientsDoor controllerESP32Other doorsmore controllersWi-Fi 2.4 GHzRouterlocal autonomyMQTT/TLSACL serverusers, roles, logsAdminmanage + auditSecuritylive events
RFID Access Control — network topology

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 / endpointDirectionPayload
acl/door1/eventnode → serveruid, door, grant/deny, reason, time
acl/door1/permsserver → noderole/schedule/revocation updates
acl/door1/statusnode → servertamper, 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.

TestWhat you should see
Present an authorised card in hoursGranted, door unlocks, event logged as grant
Present an authorised card out of hoursDenied with "outside hours"; logged
Present an unknown/revoked cardDenied with "unknown credential"; logged
Two-factor door without PINDenied "second factor failed"; logged
Pull the networkDoor still authorises from local cache; log queues and syncs later
Cut powerDoor 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.

plainaccess-log.csv
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.

Pallet racking in a distribution warehouse
Networked management administers users and roles and collects every door's audit trail, while doors work offline. Photograph sourced from Wikimedia Commons — Warehouse racking.jpg. Reused under the licence stated on that page; please check it before republishing.

Troubleshooting: Common Errors & Fixes

Cloned card opens the door

Likely cause. Plain UID trusted as identity

Fix. Use authenticated smart cards (challenge-response) and/or add a PIN; plain UID is low-assurance

Everyone locked out during a network drop

Likely cause. No local permissions cache

Fix. Hold permissions locally and authorise offline; sync logs later

Audit timestamps wrong

Likely cause. RTC coin cell missing/dead

Fix. Fit a fresh coin cell; re-sync from NTP; verify time survives power-off

Door traps people on power loss

Likely cause. Wrong fail mode on an egress door

Fix. Use fail-safe (release) on egress routes per code; verify physically

Log can be edited

Likely cause. Overwritable storage

Fix. Append-only with sequence numbers; mirror to a server; consider signing records

The sketch will not upload — "Failed to connect" or "avrdude: stk500_recv()"

Likely cause. The bootloader is not being reached: wrong port, wrong board, a serial monitor holding the port open, or a USB cable that only carries power.

Fix. Close every serial monitor, confirm Tools → Board and Port, and swap to a known data-capable USB cable. On an ESP32 hold BOOT while the IDE prints "Connecting…", then release. If a peripheral is wired to the UART pins (GPIO 1/3 on ESP32, D0/D1 on Uno) unplug it — it fights the programmer.

The board resets in a loop, or the serial monitor prints "Brownout detector was triggered"

Likely cause. The supply cannot deliver peak current. Wi-Fi transmit bursts, relay coils and servos all pull far more than their average draw.

Fix. Power peripherals from a separate regulated supply with a common ground rather than from the board 5 V pin. Add a 470–1000 µF electrolytic capacitor across the supply near the load, and use a real power adapter rather than a laptop USB port.

Serial monitor shows garbage characters

Likely cause. Baud rate mismatch between Serial.begin() and the monitor, or a floating/shared UART line.

Fix. Set the monitor to 115200 to match the sketch. If it still garbles, the crystal or the USB bridge is being confused by noise — shorten the cable and keep motor wiring away from the USB lead.

An I²C device is not detected

Likely cause. Wrong address, missing pull-ups, swapped SDA/SCL, or a bus too long for the pull-up value.

Fix. Run an I²C scanner sketch first — it should print the device address. Most breakout boards include 4.7 kΩ pull-ups, but if you have chained four of them the parallel resistance is too low; remove the pull-ups from all but one board. Keep the bus under 30 cm at 100 kHz.

Wi-Fi connects but MQTT never does (state -2)

Likely cause. Wrong broker address or port, a firewall in the way, or the broker requiring credentials the sketch is not sending.

Fix. Test from a laptop on the same network first: mosquitto_sub -h <broker> -t "#" -v. If that works, the problem is on the device — check the IP literal, port 1883 (or 8883 for TLS), and that client.setServer() runs before connect(). PubSubClient state codes are documented in its header.

Readings arrive for a while and then stop

Likely cause. The Wi-Fi or MQTT session dropped and the sketch never reconnects, or the broker dropped the client on keep-alive timeout.

Fix. Never assume the link stays up. Check WiFi.status() and client.connected() at the top of every loop and reconnect with exponential backoff. Add a watchdog so a wedged network stack reboots the device instead of going silent.

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 a millis() 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_t where 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

Aren't RFID cards easy to clone?

Plain UID cards are — they just broadcast a fixed number. That is why, for anything sensitive, this design uses authenticated smart cards (which prove a secret without revealing it) and/or a PIN, and it says so plainly rather than pretending a UID is secure.

What happens if the network goes down?

The door keeps working. Permissions are cached locally so it authorises offline, and it queues the audit log to sync when the network returns. A network drop must never lock everyone out.

Fail-safe or fail-secure — which do I use?

It depends on the door. Fail-secure (locked on power loss) protects a secure store; fail-safe (released on power loss) is required for fire-egress routes so people are never trapped. Local codes govern this.

Why log denials, not just entries?

Refusals are security-relevant: repeated denials at a door, or an unknown card, may indicate an attempt. A complete audit trail of grants and denials is what makes the system accountable.

What are roles for?

Least privilege and manageability. Roles let each person open only the doors they need, at the times they need, and let you change access by editing a role rather than every door.

References & Learning Resources

These are the primary sources worth reading in full. Manufacturer datasheets always outrank forum posts when the two disagree.

  1. RFID and card cloning (MIFARE Classic weaknesses)Reference
  2. Access control — authentication vs authorisationReference
  3. Fail-safe vs fail-secure locks and egressReference
  4. MFRC522 RFID reader (datasheet)NXP
  5. Role-based access control (RBAC)Reference