Community project

Presence Reader Multi-Protocol

Gian Luca Battini

Published July 14, 2026

ESP321 component7 assembly steps
Remix this project

This project builds a multi-protocol presence reader using an ESP32 and MFRC522 RFID module. The system reads RFID cards and tags, validates them against an authorized list, and logs attendance events. It communicates via BLE for mobile integration, WiFi for web access, and provides real-time feedback through a display, RGB LED, and speaker.

The guide includes a complete wiring diagram showing SPI connections between the MFRC522 and ESP32, a full parts list, the Arduino firmware with BLE and web server support, and step-by-step assembly instructions. Readers will learn how to set up the RFID reader, configure card authorization, and integrate wireless presence tracking into their projects.

Wiring diagram

Interactive · read-only
Wiring diagram for Presence Reader Multi-Protocol

Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.

Parts list

Bill of materials
ComponentQtyNotes
MFRC522 RFID Module113.56 MHz RFID reader/writer module based on the NXP MFRC522 IC. Communicates over SPI and is commonly sold as an RC522 breakout with an onboard antenna.

Assembly

7 steps
  1. Prepara la scheda

    Usa la ESP32-2432S028R alimentata dalla sua porta USB. Il display ILI9341, touch, LED RGB e speaker sono già integrati: non collegare moduli a quei segnali.

    • Tip: Tieni libera la microSD: non è usata da questo progetto.
    • Non alimentare mai l'MFRC522 a 5 V.
  2. Alimenta il lettore RFID

    Collega VCC del modulo MFRC522 al pin 3V3 della scheda e GND a GND.

    • Tip: Usa collegamenti corti, in particolare per alimentazione e SPI.
    • MFRC522 è un modulo a logica 3,3 V.
  3. Collega le linee SPI dell'MFRC522

    Collega SCK a GPIO0, MOSI a GPIO1, MISO a GPIO3 e SDA/SS a GPIO27.

    • Tip: SDA sul modulo MFRC522 è il chip-select SPI, non la linea I²C SDA.
    • GPIO0 è un pin di boot: il lettore non deve forzarlo a massa durante reset o accensione.
  4. Collega il reset RFID

    Collega RST dell'MFRC522 a GPIO22.

    • Tip: Il pin IRQ dell'MFRC522 non è necessario e resta scollegato.
  5. Avvio e verifica

    Con la scheda alimentata, premi Deploy in Schematik. Il display integrato mostra “AVVICINA” quando il lettore è pronto.

    • Tip: Avvicina una tessera NFC/RFID 13,56 MHz compatibile ISO 14443A al modulo MFRC522.
  6. Timbratura BLE

    Dal telefono cerca il dispositivo BLE “LettorePresenze”. Con un client BLE, scrivi un nome sulla caratteristica 19B10001-E8F2-537E-4F6C-D104768A1214 del servizio 19B10000-E8F2-537E-4F6C-D104768A1214.

    • Tip: Il nome scritto viene registrato come timbratura BLE e visualizzato sul display.
  7. Autorizza le tessere

    Leggi l'UID sul log seriale, quindi sostituisci o aggiungi l'UID nell'array authorizedUIDs del firmware e premi Deploy.

    • Tip: Invia L sul seriale per vedere il log RAM; invia C per cancellarlo.

Firmware

ESP32
firmware.cppDeploy to device
#include <Arduino.h>
#include <SPI.h>
#include <MFRC522.h>
#include <Arduino_GFX_Library.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>

// ESP32-2432S028R (CYD). MFRC522: SCK=0, MOSI=1, MISO=3, SS=27, RST=22.
#define RFID_SCK_PIN   0
#define RFID_MOSI_PIN  1
#define RFID_MISO_PIN  3
#define RFID_CS_PIN   27
#define RFID_RST_PIN  22
#define TFT_DC_PIN     2
#define TFT_CS_PIN    15
#define TFT_SCK_PIN   14
#define TFT_MOSI_PIN  13
#define TFT_MISO_PIN  12
#define TFT_BL_PIN    21
#define SPEAKER_PIN    26
#define RGB_RED_PIN     4
#define RGB_GREEN_PIN  16
#define RGB_BLUE_PIN   17

#define BLE_SERVICE_UUID "19B10000-E8F2-537E-4F6C-D104768A1214"
#define BLE_STAMP_UUID   "19B10001-E8F2-537E-4F6C-D104768A1214"
#define MAX_LOG_ENTRIES 50

// Hoisted type definitions
struct AttendanceEntry { char identity[24]; char source; unsigned long millisAtStamp; };


// Forward declarations
void setRgb(bool r, bool g, bool b);
void beepOk();
void beepDenied();
void showScreen(const String &status, const String &detail, uint16_t color);
void showReady();
void addLog(const char *id, char source);
String uidAsText(const MFRC522::Uid &uid);
bool uidIsAuthorized(const MFRC522::Uid &uid);
bool bleUserAllowed(String name);
void setupBle();
void handleRfid();
void handleBle();
String pageHtml();
void setupWeb();
void printLog();

const char *SETUP_AP_NAME = "Presenze-Setup";
const char *SETUP_AP_PASSWORD = "presenze2026";


Arduino_DataBus *displayBus = new Arduino_ESP32SPI(TFT_DC_PIN, TFT_CS_PIN, TFT_SCK_PIN, TFT_MOSI_PIN, TFT_MISO_PIN, HSPI);
Arduino_GFX *display = new Arduino_ILI9341(displayBus, GFX_NOT_DEFINED, 1, false);
MFRC522 rfid(RFID_CS_PIN, RFID_RST_PIN);
WebServer webServer(80);
Preferences preferences;
AttendanceEntry attendanceLog[MAX_LOG_ENTRIES];
uint8_t logHead = 0, logCount = 0;
volatile bool bleStampWaiting = false;
String blePayload;
String bleName = "LettorePresenze";
String blePin = "1234";
String bleUsers = ""; // empty = every name is accepted, if PIN is correct

const uint8_t authorizedUIDs[][7] = {{0xDE,0xAD,0xBE,0xEF,0,0,0},{0x01,0x02,0x03,0x04,0x05,0,0}};
const uint8_t authorizedUIDLengths[] = {4, 5};
const uint8_t authorizedCount = sizeof(authorizedUIDLengths) / sizeof(authorizedUIDLengths[0]);

void setRgb(bool r, bool g, bool b) { digitalWrite(RGB_RED_PIN, r ? LOW : HIGH); digitalWrite(RGB_GREEN_PIN, g ? LOW : HIGH); digitalWrite(RGB_BLUE_PIN, b ? LOW : HIGH); }
void beepOk() { tone(SPEAKER_PIN, 1800, 100); delay(130); }
void beepDenied() { for (uint8_t i=0;i<3;i++) { tone(SPEAKER_PIN, 500, 70); delay(130); } }
void showScreen(const String &status, const String &detail, uint16_t color) {
  display->fillScreen(BLACK); display->setTextColor(WHITE); display->setTextSize(3); display->setCursor(18, 50); display->println("PRESENZE");
  display->fillRoundRect(12, 108, 296, 84, 10, color); display->setTextColor(BLACK); display->setTextSize(2); display->setCursor(24, 136); display->println(status);
  display->setTextColor(WHITE); display->setTextSize(2); display->setCursor(18, 220); display->println(detail);
}
void showReady() { setRgb(false,false,false); showScreen("AVVICINA", "RFID/NFC o BLE", CYAN); }
void addLog(const char *id, char source) {
  strncpy(attendanceLog[logHead].identity, id, 23); attendanceLog[logHead].identity[23] = 0;
  attendanceLog[logHead].source=source; attendanceLog[logHead].millisAtStamp=millis(); logHead=(logHead+1)%MAX_LOG_ENTRIES; if(logCount<MAX_LOG_ENTRIES) logCount++;
}
String uidAsText(const MFRC522::Uid &uid) { String s; for(byte i=0;i<uid.size;i++){if(uid.uidByte[i]<16)s+='0';s+=String(uid.uidByte[i],HEX);if(i+1<uid.size)s+=':';} s.toUpperCase(); return s; }
bool uidIsAuthorized(const MFRC522::Uid &uid) {
  for(uint8_t u=0;u<authorizedCount;u++){ if(uid.size!=authorizedUIDLengths[u]) continue; bool ok=true; for(byte i=0;i<uid.size;i++)if(uid.uidByte[i]!=authorizedUIDs[u][i]){ok=false;break;} if(ok)return true; } return false;
}
bool bleUserAllowed(String name) {
  name.trim();
  if (bleUsers.isEmpty()) return true;
  String list = "," + bleUsers + ","; list.toLowerCase();
  String wanted = "," + name + ","; wanted.toLowerCase();
  return list.indexOf(wanted) >= 0;
}
class StampCallbacks : public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *c) override { std::string v=c->getValue(); if(!v.empty()){blePayload=String(v.c_str()); bleStampWaiting=true;} } };
void setupBle() {
  BLEDevice::init(bleName.c_str()); BLEServer *server=BLEDevice::createServer(); BLEService *service=server->createService(BLE_SERVICE_UUID);
  BLECharacteristic *stamp=service->createCharacteristic(BLE_STAMP_UUID, BLECharacteristic::PROPERTY_WRITE); stamp->setCallbacks(new StampCallbacks()); service->start();
  BLEAdvertising *advertising=BLEDevice::getAdvertising(); advertising->addServiceUUID(BLE_SERVICE_UUID); advertising->start();
}
void handleRfid() {
  if(!rfid.PICC_IsNewCardPresent() || !rfid.PICC_ReadCardSerial()) return;
  String uid=uidAsText(rfid.uid); Serial.printf("[RFID] UID: %s\n",uid.c_str());
  if(uidIsAuthorized(rfid.uid)){showScreen("ACCESSO OK",uid,GREEN);setRgb(false,true,false);beepOk();addLog(uid.c_str(),'R');}
  else {showScreen("NON AUTORIZZATO",uid,RED);setRgb(true,false,false);beepDenied();}
  rfid.PICC_HaltA(); rfid.PCD_StopCrypto1(); delay(1000); showReady();
}
void handleBle() {
  if(!bleStampWaiting) return; bleStampWaiting=false; blePayload.trim();
  int separator=blePayload.lastIndexOf('|');
  String name = separator >= 0 ? blePayload.substring(0,separator) : "";
  String pin = separator >= 0 ? blePayload.substring(separator+1) : "";
  name.trim(); pin.trim(); name=name.substring(0,23);
  if(name.isEmpty() || pin != blePin || !bleUserAllowed(name)) { Serial.println("[BLE] Timbratura rifiutata"); showScreen("BLE RIFIUTATO","Nome o codice",RED);setRgb(true,false,false);beepDenied();delay(1000);showReady();return; }
  Serial.printf("[BLE] Timbratura: %s\n",name.c_str()); showScreen("TIMBRATO BLE",name,GREEN);setRgb(false,true,false);beepOk();addLog(name.c_str(),'B');delay(1000);showReady();
}
String pageHtml() {
  String h = F("<!doctype html><html lang='it'><meta name='viewport' content='width=device-width,initial-scale=1'><style>body{font-family:Arial;background:#10202d;color:#fff;margin:24px;max-width:620px}input{box-sizing:border-box;width:100%;padding:12px;margin:6px 0 18px;border-radius:6px;border:0}button{background:#00bfa5;border:0;padding:13px 18px;border-radius:6px;font-weight:bold}small{color:#c7d5dd}</style><h1>Presenze BLE</h1><p>Configura le timbrature Bluetooth.</p><form method='POST' action='/save'><label>Nome visibile Bluetooth</label><input name='name' maxlength='20' value='");
  h += bleName; h += F("'><label>Codice di timbratura BLE</label><input name='pin' maxlength='16' value='"); h += blePin;
  h += F("'><label>Nomi autorizzati (separati da virgola)</label><input name='users' maxlength='180' value='"); h += bleUsers;
  h += F("'><small>Lascia vuoto l'elenco per accettare qualsiasi nome con il codice corretto. Dal client BLE invia: <b>Nome|Codice</b>, ad esempio <b>Mario Rossi|1234</b>.</small><p><button type='submit'>Salva e riavvia BLE</button></p></form><hr><small>Rete di configurazione: "); h += SETUP_AP_NAME; h += F(" &mdash; IP 192.168.4.1</small></html>"); return h;
}
void setupWeb() {
  WiFi.mode(WIFI_AP); WiFi.softAP(SETUP_AP_NAME, SETUP_AP_PASSWORD);
  webServer.on("/", HTTP_GET, [](){ webServer.send(200,"text/html; charset=utf-8",pageHtml()); });
  webServer.on("/save", HTTP_POST, [](){
    String name=webServer.arg("name"); String pin=webServer.arg("pin"); String users=webServer.arg("users"); name.trim();pin.trim();users.trim();
    if(name.isEmpty() || pin.isEmpty()){webServer.send(400,"text/plain; charset=utf-8","Nome BLE e codice sono obbligatori.");return;}
    preferences.putString("bleName",name.substring(0,20)); preferences.putString("blePin",pin.substring(0,16)); preferences.putString("bleUsers",users.substring(0,180));
    webServer.send(200,"text/html; charset=utf-8","<meta http-equiv='refresh' content='3;url=/'><h2>Salvato.</h2><p>Riavvio del Bluetooth in corso...</p>"); delay(500); ESP.restart();
  });
  webServer.begin(); Serial.printf("[WEB] Wi-Fi %s, apri http://%s\n",SETUP_AP_NAME,WiFi.softAPIP().toString().c_str());
}
void printLog() { Serial.println("--- LOG PRESENZE ---"); uint8_t first=(logCount<MAX_LOG_ENTRIES)?0:logHead; for(uint8_t i=0;i<logCount;i++){AttendanceEntry &e=attendanceLog[(first+i)%MAX_LOG_ENTRIES];Serial.printf("[%s] %s t=%lu ms\n",e.source=='B'?"BLE":"RFID",e.identity,e.millisAtStamp);} }
void setup() {
  Serial.begin(115200); preferences.begin("presenze",false); bleName=preferences.getString("bleName",bleName);blePin=preferences.getString("blePin",blePin);bleUsers=preferences.getString("bleUsers",bleUsers);
  pinMode(TFT_BL_PIN,OUTPUT);digitalWrite(TFT_BL_PIN,HIGH);pinMode(SPEAKER_PIN,OUTPUT);pinMode(RGB_RED_PIN,OUTPUT);pinMode(RGB_GREEN_PIN,OUTPUT);pinMode(RGB_BLUE_PIN,OUTPUT);
  display->begin();showReady(); SPI.begin(RFID_SCK_PIN,RFID_MISO_PIN,RFID_MOSI_PIN,RFID_CS_PIN);rfid.PCD_Init();rfid.PCD_DumpVersionToSerial(); setupWeb();setupBle();
  Serial.println("[SYS] Pronto. Seriale: L=log, C=cancella log.");
}
void loop() { webServer.handleClient(); if(Serial.available()){char c=(char)Serial.read();if(c=='L'||c=='l')printLog();if(c=='C'||c=='c'){logHead=0;logCount=0;Serial.println("[SYS] Log cancellato.");}} handleBle();handleRfid(); }

“Deploy to device” opens this project in Schematik, where you can flash it to your board over USB.

Remix this project

Make it yours in one click

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

Open in Schematik