In today’s maker world, combining electronics with custom enclosures is what transforms a prototype into a polished product. For my latest project, I built a web‑based RFID attendance system using the DFRobot Beetle ESP32‑C3 microcontroller and an RC522 RFID reader. To give it a professional finish, I partnered with JustWay 3D Printing Services, who helped me design and print a sleek, durable enclosure that protects the electronics while looking great on any desk or wall.
This blog will walk you through the hardware setup, software implementation, and enclosure design process, while also showcasing how Just Way’s 3D printing expertise elevates DIY projects into production‑ready solutions.
Hardware Components- DFRobot Beetle ESP32‑C3
- RC522 RFID Reader
- Jumper wires
- USB cable for programming
- JustWay 3D‑printed enclosure (custom designed for this project)
When it comes to compact IoT development boards, the DFRobot Beetle ESP32‑C3 stands out as a powerhouse in a tiny package. Designed for makers who need performance without sacrificing space, this board is ideal for projects like RFID attendance systems where both connectivity and form factor matter.
Key Features- Ultra‑Compact Design: Measuring just 25mm x 20mm, the Beetle ESP32‑C3 is one of the smallest ESP32 boards available, making it perfect for embedding inside custom enclosures.
- RISC‑V Core: Built on the efficient RISC‑V architecture, it delivers solid performance while keeping power consumption low.
- Connectivity: Equipped with Wi‑Fi (2.4GHz) and Bluetooth 5.0, it ensures seamless communication with web servers, mobile devices, or other IoT nodes.
- Low Power Consumption: Ideal for battery‑powered or energy‑sensitive applications.
- Rich GPIO Options: Despite its size, it provides multiple pins for SPI, I2C, UART, PWM, and ADC, enabling integration with sensors, displays, and actuators.
- Small footprint: Easily fits inside a 3D‑printed case without crowding.
- Built‑in Wi‑Fi: Allows the board to host a webserver for real‑time attendance tracking.
- Affordable and accessible: A cost‑effective choice for classrooms, labs, and small offices.
- Community support: Backed by DFRobot’s documentation and the broader ESP32 ecosystem, making development smoother.
The RC522 RFID reader is one of the most popular and affordable RFID modules for hobbyists and makers. It’s widely used in access control, attendance systems, and smart identification projects thanks to its simplicity and reliability.
Key Features- Operating Frequency: 13.56 MHz (ISO/IEC 14443 standard).
- Supported Cards/Tags: MIFARE Classic 1K, 4K, and other compatible RFID cards and key fobs.
- Communication Interface: SPI (default), with support for I²C and UART in some configurations.
- Range: Typically 2–5 cm, ideal for secure close‑range scanning.
- Low Power Consumption: Suitable for embedded IoT projects.
- Compact Size: Easy to integrate into small enclosures.
- Fast UID Detection: Quickly reads card IDs, making it practical for logging Punch‑In and Punch‑Out events.
- Affordable and accessible: Available from most electronics suppliers at low cost.
- Easy integration: Works seamlessly with microcontrollers like the ESP32, Arduino, and Raspberry Pi.
- Community support: Extensive tutorials and libraries (such as the MFRC522 Arduino library) make development straightforward.
The RC522 communicates via SPI. Here’s the wiring I used:
- RC522 VCC → Beetle 3.3V
- RC522 GND → Beetle GND
- RC522 SDA (SS) → Beetle GPIO1(chip select pin)
- RC522 RST → Beetle GPIO0(reset pin)
- RC522 SCK → Beetle SCK pin(default SPI clock)
- RC522 MOSI → Beetle MOSI pin(default SPI data out)
- RC522 MISO → Beetle MISO pin(default SPI data in)
The firmware was written in Arduino IDE using the MFRC522 library. The ESP32 hosts a webserver that displays attendance logs in real time.
/*
Beetle ESP32-C3 RFID Attendance Webserver
- Reads MFRC522 UIDs
- Alternates Punch1 / Punch2 per UID
- Serves a web UI with live clock, IP, and attendance table
*/
#include <WiFi.h>
#include <WebServer.h>
#include <SPI.h>
#include <MFRC522.h>
#include "time.h"
#include <vector>
#include <map>
// ---------- CONFIG ----------
const char* WIFI_SSID = "";
const char* WIFI_PASSWORD = "";
// RFID pins (use GPIO numbers you wired)
constexpr uint8_t SS_PIN = 1; // change to your SDA/SS GPIO
constexpr uint8_t RST_PIN = 0; // change to your RST GPIO
// NTP
const char* NTP_SERVER = "pool.ntp.org";
const long GMT_OFFSET_SEC = 19800; // IST +5:30 (change to your timezone)
const int DAYLIGHT_OFFSET_SEC = 0;
// debounce
const unsigned long SCAN_DEBOUNCE_MS = 2000UL;
// ---------- GLOBALS ----------
MFRC522 rfid(SS_PIN, RST_PIN);
WebServer server(80);
struct Entry {
String uid;
String name;
String punch1; // ISO timestamp or empty
String punch2; // ISO timestamp or empty
String note;
};
std::vector<Entry> entries;
std::map<String, String> uidToName; // friendly names
unsigned long lastScanMillis = 0;
String lastUID = "";
// ---------- HELPERS ----------
String nowIso() {
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
// fallback to millis
unsigned long ms = millis();
return String(ms);
}
char buf[32];
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &timeinfo);
return String(buf);
}
String uidToString(const MFRC522::Uid &uid) {
String s = "";
for (byte i = 0; i < uid.size; i++) {
if (uid.uidByte[i] < 0x10) s += "0";
s += String(uid.uidByte[i], HEX);
if (i < uid.size - 1) s += ":";
}
s.toUpperCase();
return s;
}
void addAttendance(const String &uid) {
String name = "";
auto it = uidToName.find(uid);
if (it != uidToName.end()) name = it->second;
// Find last entry for this UID
int idx = -1;
for (int i = (int)entries.size() - 1; i >= 0; --i) {
if (entries[i].uid == uid) { idx = i; break; }
}
String ts = nowIso();
if (idx == -1) {
// no previous entry -> create new with punch1
Entry e; e.uid = uid; e.name = name; e.punch1 = ts; e.punch2 = ""; e.note = "";
entries.push_back(e);
} else {
// if last entry has punch1 but no punch2 -> fill punch2
if (entries[idx].punch1.length() > 0 && entries[idx].punch2.length() == 0) {
entries[idx].punch2 = ts;
} else {
// otherwise start a new entry (new punch1)
Entry e; e.uid = uid; e.name = name; e.punch1 = ts; e.punch2 = ""; e.note = "";
entries.push_back(e);
}
}
}
// ---------- HTTP handlers ----------
void handleRoot() {
String html = R"rawliteral(
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>RFID Attendance</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
body{font-family:system-ui,Segoe UI,Roboto,Arial;background:#0b1220;color:#e6eef6;margin:0;padding:18px}
header{display:flex;justify-content:space-between;align-items:center;gap:12px}
h1{margin:0;font-size:20px;color:#4cc9f0}
.meta{font-size:13px;color:#94a3b8}
.card{background:#0f1724;padding:12px;border-radius:10px;margin-top:12px;box-shadow:0 6px 18px rgba(0,0,0,0.4)}
table{width:100%;border-collapse:collapse}
th,td{padding:8px 6px;text-align:left;border-bottom:1px solid #111827;font-size:13px}
th{color:#9fb7c9}
.status{display:flex;gap:12px;align-items:center}
.dot{width:10px;height:10px;border-radius:50%;background:#4caf50;display:inline-block}
.small{font-size:12px;color:#94a3b8}
.controls{display:flex;gap:8px;align-items:center}
.btn{background:#4cc9f0;color:#021018;padding:8px 10px;border-radius:8px;border:none;font-weight:700}
.note{color:#94a3b8;font-size:12px;margin-top:8px}
@media (max-width:600px){ h1{font-size:18px} th,td{font-size:12px} }
</style>
</head>
<body>
<header>
<div>
<h1>RFID Attendance</h1>
<div class="meta">Device IP: <span id="ip">...</span> • Time: <span id="clock">...</span></div>
</div>
<div class="status">
<div class="dot" id="wifiDot"></div>
<div class="small" id="wifiText">Connecting...</div>
</div>
</header>
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center">
<div><strong>Attendance</strong><div class="small">Latest scans appear here automatically</div></div>
<div class="controls">
<button class="btn" onclick="clearEntries()">Clear</button>
<button class="btn" onclick="downloadCSV()">Export CSV</button>
</div>
</div>
<div style="overflow:auto;margin-top:12px">
<table id="attTable">
<thead><tr><th>#</th><th>UID</th><th>Name</th><th>Punch 1</th><th>Punch 2</th><th>Note</th></tr></thead>
<tbody id="attBody"></tbody>
</table>
</div>
<div class="note">Scans alternate between Punch 1 and Punch 2 per UID. Refreshes every 2s.</div>
</div>
<script>
async function fetchStatus(){
try {
const r = await fetch('/api/status');
const j = await r.json();
document.getElementById('ip').innerText = j.ip || 'N/A';
document.getElementById('clock').innerText = j.time || '...';
document.getElementById('wifiText').innerText = j.wifi ? 'Wi‑Fi connected' : 'Wi‑Fi offline';
document.getElementById('wifiDot').style.background = j.wifi ? '#4caf50' : '#f44336';
} catch(e) {
document.getElementById('wifiText').innerText = 'Error';
document.getElementById('wifiDot').style.background = '#f44336';
}
}
async function fetchEntries(){
try {
const r = await fetch('/api/entries');
const j = await r.json();
const tbody = document.getElementById('attBody');
tbody.innerHTML = '';
j.entries.forEach((e,i)=>{
const tr = document.createElement('tr');
tr.innerHTML = `<td>${i+1}</td><td>${e.uid}</td><td>${e.name}</td><td>${e.punch1}</td><td>${e.punch2}</td><td>${e.note}</td>`;
tbody.appendChild(tr);
});
} catch(e) {
console.error(e);
}
}
async function clearEntries(){
if (!confirm('Clear all attendance entries?')) return;
await fetch('/api/clear', {method:'POST'});
fetchEntries();
}
async function downloadCSV(){
const r = await fetch('/api/entries');
const j = await r.json();
let csv = 'UID,Name,Punch1,Punch2,Note\n';
j.entries.forEach(e=>{
csv += `"${e.uid}","${e.name}","${e.punch1}","${e.punch2}","${e.note}"\n`;
});
const blob = new Blob([csv], {type:'text/csv'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = 'attendance.csv'; document.body.appendChild(a); a.click();
a.remove(); URL.revokeObjectURL(url);
}
// initial and periodic refresh
fetchStatus(); fetchEntries();
setInterval(fetchStatus, 1000); // clock and IP every second
setInterval(fetchEntries, 2000); // entries every 2s
</script>
</body>
</html>
)rawliteral";
server.send(200, "text/html", html);
}
void handleApiStatus() {
String ip = WiFi.localIP().toString();
String t = nowIso();
bool wifi = (WiFi.status() == WL_CONNECTED);
String out = "{";
out += "\"ip\":\"" + ip + "\",";
out += "\"time\":\"" + t + "\",";
out += "\"wifi\":" + String(wifi ? "true" : "false");
out += "}";
server.send(200, "application/json", out);
}
void handleApiEntries() {
// build JSON array
String out = "{ \"entries\": [";
for (size_t i = 0; i < entries.size(); ++i) {
const Entry &e = entries[i];
out += "{";
out += "\"uid\":\"" + e.uid + "\",";
out += "\"name\":\"" + e.name + "\",";
out += "\"punch1\":\"" + e.punch1 + "\",";
out += "\"punch2\":\"" + e.punch2 + "\",";
out += "\"note\":\"" + e.note + "\"";
out += "}";
if (i + 1 < entries.size()) out += ",";
}
out += "] }";
server.send(200, "application/json", out);
}
void handleClear() {
entries.clear();
server.send(200, "text/plain", "Cleared");
}
// ---------- SETUP ----------
void setup() {
Serial.begin(115200);
delay(100);
// friendly names (edit as needed)
uidToName["E4:3E:41:01"] = "Jack";
uidToName["33:1B:FC:2C"] = "David";
// Init SPI and RFID
SPI.begin(); // use default SPI pins for your core
rfid.PCD_Init();
Serial.println("MFRC522 initialized");
// Connect Wi-Fi
Serial.printf("Connecting to WiFi '%s' ", WIFI_SSID);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED && millis() - start < 15000) {
delay(300);
Serial.print(".");
}
Serial.println();
if (WiFi.status() == WL_CONNECTED) {
Serial.print("WiFi connected, IP: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("WiFi connect failed (will retry in loop)");
}
// NTP
configTime(GMT_OFFSET_SEC, DAYLIGHT_OFFSET_SEC, NTP_SERVER);
// HTTP routes
server.on("/", HTTP_GET, handleRoot);
server.on("/api/status", HTTP_GET, handleApiStatus);
server.on("/api/entries", HTTP_GET, handleApiEntries);
server.on("/api/clear", HTTP_POST, handleClear);
server.begin();
Serial.println("HTTP server started");
}
// ---------- LOOP ----------
void loop() {
server.handleClient();
// WiFi reconnect simple logic
if (WiFi.status() != WL_CONNECTED) {
static unsigned long lastTry = 0;
if (millis() - lastTry > 5000) {
Serial.println("Attempting WiFi reconnect...");
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
lastTry = millis();
}
}
// RFID scanning
if (!rfid.PICC_IsNewCardPresent()) {
delay(20);
return;
}
if (!rfid.PICC_ReadCardSerial()) {
delay(20);
return;
}
String uid = uidToString(rfid.uid);
uid.toUpperCase();
unsigned long now = millis();
// debounce duplicate reads
if (uid == lastUID && (now - lastScanMillis) < SCAN_DEBOUNCE_MS) {
Serial.println("Duplicate read ignored");
rfid.PICC_HaltA();
delay(100);
return;
}
lastUID = uid;
lastScanMillis = now;
Serial.print("Scanned UID: ");
Serial.println(uid);
addAttendance(uid);
rfid.PICC_HaltA();
delay(200);
}Key Features:- Punch‑In / Punch‑Out system: Each UID alternates between Punch 1 and Punch 2.
- Web UI: Accessible from any browser on the same Wi‑Fi network.
- Live clock & IP display: Powered by NTP.
- Export to CSV: Download attendance logs directly.
- Clear entries: Reset the table with one click.
This makes the system practical for small offices, labs, or classrooms.
Powered by JUSTWAY 3D Printing Service 🖨️For the prototype, I partnered with JUSTWAY 3D Printing Service — a reliable platform that brings ideas to life with precision and speed.
How to Order Your 3D Print 🖨️
For this project, I used JUSTWAY 3D Printing Service to bring my gate prototype to life. Ordering your own custom print is simple and beginner‑friendly:
- Create or download a 3D model (e.g.,. STL
.STLor.OBJfile). - Visit the JUSTWAY 3D Printing Service platform.
- Upload your design file directly through their interface.
- Select from options like PLA, ABS, PETG, or specialty filaments.Pick the color and finish that suits your prototype.
- The platform provides a real‑time cost estimate based on size, material, and complexity.
- Confirm your design and checkout securely.
- JUSTWAY handles the printing and ships the finished part to your address.
- Once delivered, integrate the printed gate with your servo and ESP32 P4 setup.
- You’ll have a professional‑looking prototype ready for demos.
Attach the RFID module into the enclosure.
The finished system is a compact RFID attendance terminal:
- Plug it in, connect to Wi‑Fi, and open the web interface.
- Scan a card to log Punch 1 and Punch 2 with timestamps.
- View logs in real time or export them for record‑keeping.
- All housed in a sleek Just Way 3D‑printed enclosure that looks great in any workspace.
If you’re building IoT devices, robotics projects, or any electronics prototype, Just Way 3D Printing Services can help you:
- Turn breadboard projects into polished products.
- Create enclosures that fit your exact hardware.
- Experiment with different materials and finishes.
- Scale from one‑off prototypes to small production runs.
This project shows how combining DFRobot’s Beetle ESP32‑C3 with Just Way’s 3D printing expertise results in a professional, functional RFID attendance system. The microcontroller provides the brains, while the enclosure provides the body — together making a complete solution.
Whether you’re a hobbyist, educator, or startup, don’t let your projects stop at the prototype stage. With JustWay3D Printing Services, you can give your electronics the enclosure they deserve.






Comments