20 RFID Hands-On Labs and Assessment
20.2 Learning Objectives
By the end of this chapter, you will be able to:
- Construct Access Control Systems: Assemble and program a complete RFID door lock with LCD feedback, LED indicators, buzzer alerts, and circular-buffer logging
- Implement Inventory Dashboards: Develop a Flask-based web interface that processes real-time RFID scans, stores items in SQLite, and displays live statistics
- Critique Security Vulnerabilities: Assess why MIFARE Classic Crypto1 is fundamentally broken and justify migration to AES-based alternatives like DESFire EV3
- Justify Frequency Band Selection: Defend the choice of LF, HF, or UHF for specific scenarios using tissue penetration, range, and standard compliance criteria
- Evaluate Transit Payment Architectures: Analyze why NFC dominates contactless transit and predict failure modes if UHF were substituted
- Diagnose RFID Failures: Isolate root causes of poor read rates including metal interference, tag orientation, and RF collision
What is this chapter? Complete hands-on projects and knowledge assessment for RFID systems.
Before starting labs:
- Review RFID Hardware Integration
- Gather required hardware components
- Set up development environment (Arduino IDE or Python)
Hardware for Labs:
| Lab | Hardware Required | Estimated Cost |
|---|---|---|
| Lab 1 | ESP32 + RC522 + LCD + LEDs | $15-20 |
| Lab 2 | Raspberry Pi + RC522 | $40-50 |
Assessment Focus:
- RFID frequency selection
- Security vulnerabilities
- Payment system design
20.3 Prerequisites
Before diving into this chapter, you should be familiar with:
- RFID Hardware Integration: Arduino and Python programming for RFID readers
- RFID Industry Applications: Real-world deployment scenarios
- RFID Security and Privacy: Understanding vulnerabilities and secure tag types
20.4 Lab 1: ESP32 RFID Access Control System
Build a complete access control system with LCD display, buzzer feedback, and access logging.
Hardware Required:
- ESP32 Development Board
- RC522 RFID Reader Module (13.56 MHz)
- I2C LCD Display (16x2 or 20x4)
- Buzzer
- Green and Red LEDs
- RFID cards/tags
- Resistors (220Ω for LEDs, 1kΩ for buzzer)
Wiring:
| Component | ESP32 Pin |
|---|---|
| RC522 SDA | GPIO 5 |
| RC522 SCK | GPIO 18 |
| RC522 MOSI | GPIO 23 |
| RC522 MISO | GPIO 19 |
| RC522 RST | GPIO 4 |
| RC522 3.3V | 3.3V |
| RC522 GND | GND |
| LCD SDA | GPIO 21 |
| LCD SCL | GPIO 22 |
| Green LED | GPIO 25 (via 220Ω resistor) |
| Red LED | GPIO 26 (via 220Ω resistor) |
| Buzzer | GPIO 27 (via 1kΩ resistor) |
Complete Code:
#include <SPI.h>
#include <MFRC522.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
// Pin definitions
#define SS_PIN 5
#define RST_PIN 4
#define GREEN_LED 25
#define RED_LED 26
#define BUZZER 27
// Initialize components
MFRC522 rfid(SS_PIN, RST_PIN);
LiquidCrystal_I2C lcd(0x27, 16, 2); // I2C address 0x27, 16 cols, 2 rows
// Authorized UIDs (add your card UIDs here)
String authorizedUIDs[] = {
"04521AB2", // Employee card 1
"04A1B2C3D4E5F6", // Employee card 2
"D3A512C4" // Admin card
};
int numAuthorized = sizeof(authorizedUIDs) / sizeof(authorizedUIDs[0]);
// Access log structure
struct AccessLog {
String uid;
String timestamp;
bool granted;
};
AccessLog accessLog[100]; // Store last 100 access attempts
int logIndex = 0;
void setup() {
Serial.begin(115200);
// Initialize SPI and RFID
SPI.begin();
rfid.PCD_Init();
// Initialize LCD
lcd.init();
lcd.backlight();
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("RFID Access");
lcd.setCursor(0, 1);
lcd.print("Control Ready");
// Initialize LEDs and buzzer
pinMode(GREEN_LED, OUTPUT);
pinMode(RED_LED, OUTPUT);
pinMode(BUZZER, OUTPUT);
// Test sequence
digitalWrite(GREEN_LED, HIGH);
delay(200);
digitalWrite(GREEN_LED, LOW);
digitalWrite(RED_LED, HIGH);
delay(200);
digitalWrite(RED_LED, LOW);
tone(BUZZER, 1000, 100);
Serial.println("\n=== RFID Access Control System ===");
Serial.println("System initialized. Waiting for cards...\n");
delay(2000);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Scan Your Card");
}
void loop() {
// Look for new cards
if (!rfid.PICC_IsNewCardPresent())
return;
// Select one of the cards
if (!rfid.PICC_ReadCardSerial())
return;
// Read card UID
String uid = "";
for (byte i = 0; i < rfid.uid.size; i++) {
uid += String(rfid.uid.uidByte[i] < 0x10 ? "0" : "");
uid += String(rfid.uid.uidByte[i], HEX);
}
uid.toUpperCase();
// Check authorization
bool authorized = false;
for (int i = 0; i < numAuthorized; i++) {
if (uid.equals(authorizedUIDs[i])) {
authorized = true;
break;
}
}
// Display result and log access
if (authorized) {
grantAccess(uid);
} else {
denyAccess(uid);
}
// Halt card and stop encryption
rfid.PICC_HaltA();
rfid.PCD_StopCrypto1();
// Return to ready state after 3 seconds
delay(3000);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Scan Your Card");
digitalWrite(GREEN_LED, LOW);
digitalWrite(RED_LED, LOW);
}
void grantAccess(String uid) {
Serial.println("✓ ACCESS GRANTED");
Serial.println(" UID: " + uid);
Serial.println(" Time: " + getTimestamp());
// LCD display
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("ACCESS GRANTED");
lcd.setCursor(0, 1);
lcd.print("Welcome!");
// Green LED and success beep
digitalWrite(GREEN_LED, HIGH);
tone(BUZZER, 2000, 100);
delay(100);
tone(BUZZER, 2500, 100);
// Log access
logAccess(uid, true);
}
void denyAccess(String uid) {
Serial.println("✗ ACCESS DENIED");
Serial.println(" UID: " + uid);
Serial.println(" Time: " + getTimestamp());
Serial.println(" Reason: Unauthorized card");
// LCD display
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("ACCESS DENIED");
lcd.setCursor(0, 1);
lcd.print("Unauthorized!");
// Red LED and error beep
digitalWrite(RED_LED, HIGH);
tone(BUZZER, 500, 200);
delay(250);
tone(BUZZER, 500, 200);
delay(250);
tone(BUZZER, 500, 200);
// Log access attempt
logAccess(uid, false);
}
void logAccess(String uid, bool granted) {
accessLog[logIndex].uid = uid;
accessLog[logIndex].timestamp = getTimestamp();
accessLog[logIndex].granted = granted;
logIndex = (logIndex + 1) % 100; // Circular buffer
}
String getTimestamp() {
// In real system, use RTC module for accurate timestamps
// For now, return milliseconds since boot
unsigned long ms = millis();
unsigned long seconds = ms / 1000;
unsigned long minutes = seconds / 60;
unsigned long hours = minutes / 60;
return String(hours % 24) + ":" +
String(minutes % 60) + ":" +
String(seconds % 60);
}
void printAccessLog() {
// Call this function via Serial command or button press
Serial.println("\n=== ACCESS LOG (Last 10 Entries) ===");
int start = max(0, logIndex - 10);
for (int i = start; i < logIndex; i++) {
Serial.print(accessLog[i].timestamp);
Serial.print(" | ");
Serial.print(accessLog[i].uid);
Serial.print(" | ");
Serial.println(accessLog[i].granted ? "GRANTED" : "DENIED");
}
Serial.println();
}Expected Output (Serial Monitor):
=== RFID Access Control System ===
System initialized. Waiting for cards...
✓ ACCESS GRANTED
UID: 04521AB2
Time: 0:5:12
✗ ACCESS DENIED
UID: F7A9B2C1
Time: 0:5:45
Reason: Unauthorized card
✓ ACCESS GRANTED
UID: E2801160600002A1
Time: 0:6:03
Expected Output (LCD Display):
[When ready]
Scan Your Card
[On authorized card]
ACCESS GRANTED
Welcome!
[On unauthorized card]
ACCESS DENIED
Unauthorized!
20.5 Lab 2: Python RFID Inventory Dashboard with Real-Time Monitoring
Build a complete inventory management dashboard with web interface using Flask.
Requirements:
pip install Flask mfrc522 RPi.GPIO sqlite3Web Dashboard Template (templates/dashboard.html):
<!DOCTYPE html>
<html>
<head>
<title>RFID Inventory Dashboard</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: Arial, sans-serif; background: #f5f5f5; padding: 20px; }
.container { max-width: 1400px; margin: 0 auto; }
h1 { color: #333; margin-bottom: 20px; }
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.stat-card h3 { color: #666; font-size: 14px; margin-bottom: 10px; }
.stat-card .value { font-size: 32px; font-weight: bold; color: #4CAF50; }
.sections {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.section {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.section h2 { margin-bottom: 15px; color: #333; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f9f9f9; font-weight: 600; color: #666; }
.live-scan {
background: #E3F2FD;
padding: 15px;
border-radius: 8px;
border-left: 4px solid #2196F3;
margin-bottom: 20px;
}
.live-scan.active {
background: #C8E6C9;
border-left-color: #4CAF50;
}
.timestamp { color: #999; font-size: 12px; }
</style>
</head>
<body>
<div class="container">
<h1>RFID Inventory Dashboard</h1>
<div class="live-scan" id="liveScan">
<strong>Last Scan:</strong> <span id="lastScanInfo">Waiting for tags...</span>
</div>
<div class="stats">
<div class="stat-card">
<h3>Total Items</h3>
<div class="value" id="totalItems">0</div>
</div>
<div class="stat-card">
<h3>Scans Today</h3>
<div class="value" id="scansToday">0</div>
</div>
<div class="stat-card">
<h3>Categories</h3>
<div class="value" id="categories">0</div>
</div>
</div>
<div class="sections">
<div class="section">
<h2>Inventory Items</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>Category</th>
<th>Last Seen</th>
</tr>
</thead>
<tbody id="itemsTable">
<tr><td colspan="3">Loading...</td></tr>
</tbody>
</table>
</div>
<div class="section">
<h2>Recent Activity</h2>
<table>
<thead>
<tr>
<th>Item</th>
<th>Location</th>
<th>Time</th>
</tr>
</thead>
<tbody id="activityTable">
<tr><td colspan="3">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
// Update statistics
function updateStats() {
fetch('/api/statistics')
.then(r => r.json())
.then(data => {
document.getElementById('totalItems').textContent = data.total_items;
document.getElementById('scansToday').textContent = data.scans_today;
document.getElementById('categories').textContent =
Object.keys(data.by_category).length;
});
}
// Update items table
function updateItems() {
fetch('/api/items')
.then(r => r.json())
.then(items => {
const tbody = document.getElementById('itemsTable');
tbody.innerHTML = items.map(item => `
<tr>
<td><strong>${item.name}</strong><br>
<span class="timestamp">${item.rfid_uid}</span></td>
<td>${item.category}</td>
<td class="timestamp">${item.last_seen || 'Never'}</td>
</tr>
`).join('');
});
}
// Update recent scans
function updateActivity() {
fetch('/api/recent_scans')
.then(r => r.json())
.then(scans => {
const tbody = document.getElementById('activityTable');
tbody.innerHTML = scans.slice(0, 10).map(scan => `
<tr>
<td>${scan.item_name}</td>
<td>${scan.location}</td>
<td class="timestamp">${new Date(scan.timestamp).toLocaleTimeString()}</td>
</tr>
`).join('');
});
}
// Check for new scans (live updates)
let lastScanUID = null;
function checkLastScan() {
fetch('/api/last_scan')
.then(r => r.json())
.then(data => {
if (data.uid && data.uid !== lastScanUID) {
lastScanUID = data.uid;
const liveScan = document.getElementById('liveScan');
const scanInfo = document.getElementById('lastScanInfo');
liveScan.classList.add('active');
scanInfo.textContent = `${data.name} (${data.uid}) - ${new Date(data.timestamp).toLocaleTimeString()}`;
// Remove highlight after 3 seconds
setTimeout(() => liveScan.classList.remove('active'), 3000);
// Refresh data
updateStats();
updateItems();
updateActivity();
}
});
}
// Initial load
updateStats();
updateItems();
updateActivity();
// Periodic updates
setInterval(checkLastScan, 1000); // Check for new scans every second
setInterval(updateStats, 5000); // Update stats every 5 seconds
setInterval(updateItems, 10000); // Update items every 10 seconds
setInterval(updateActivity, 5000); // Update activity every 5 seconds
</script>
</body>
</html>Running the Dashboard:
python3 rfid_dashboard.pyExpected Output:
Starting RFID Inventory Dashboard...
RFID scanner thread started...
Access dashboard at: http://localhost:5000
* Running on http://0.0.0.0:5000
Tag detected: 1234567890
Item: Arduino Uno R3
Tag detected: 9876543210
Item: Raspberry Pi 4
20.6 Knowledge Check
Test your understanding of RFID technology and applications.
The Sensor Squad decided to build their own RFID door lock! Max the Microcontroller was the brain of the operation. “I’ll check every card that comes near the reader,” he said.
Sammy the Sensor (playing the RC522 reader) stood by the door: “I’ll sense the radio waves from each card and tell Max the secret number.”
Lila the LED had two jobs: “I’ll glow GREEN when a friend’s card is scanned, and RED when a stranger tries to get in!” She practiced flipping between colors.
Bella the Battery made sure everyone had power: “Without me, none of this works! I keep the ESP32, the reader, the screen, and the LEDs all running.”
When their teacher’s card was scanned – BEEP! Green light! “ACCESS GRANTED” appeared on the tiny screen. When a random card was tried – BUZZ BUZZ BUZZ! Red light! “ACCESS DENIED!”
The lesson: You can build real security systems with just a few parts: a microcontroller, an RFID reader, some LEDs, and a bit of code. Start simple, then add features like logging and displays!
When choosing an RFID reader platform for a production deployment, three factors dominate: scale (how many tags per second), environment (networked vs standalone), and budget (cost per reader). This framework helps you avoid overbuilding (wasting money on Arduino for simple tasks) or underbuilding (using ESP32 when you need industrial reliability).
Decision Matrix:
| Requirement | Arduino Uno | ESP32 | Raspberry Pi | Commercial Reader |
|---|---|---|---|---|
| Tags/sec | 5-10 | 20-50 | 100+ | 500+ |
| Network | Serial only | Wi-Fi/BLE | Ethernet/Wi-Fi | Ethernet/PoE |
| Power | 5V USB/barrel | 3.3V micro-USB | 5V 3A USB-C | 12-48V PoE |
| Storage | 32 KB flash | 4 MB flash | 32 GB SD | Enterprise DB |
| Cost | $25 | $10 | $45 | $200-2,000 |
| Reliability | Hobby | Prototype | Production-lite | Industrial |
| Best For | Learning labs | Wi-Fi prototypes | Edge gateways | 24/7 operations |
Step-by-Step Selection Process:
1. Define Your Throughput:
- Under 10 tags/sec (e.g., door access with 1 scan every 10 seconds) → Arduino or ESP32
- 10-100 tags/sec (e.g., inventory spot-checks with handheld scanner) → Raspberry Pi
- Over 100 tags/sec (e.g., conveyor belt with bulk scanning) → Commercial UHF reader
2. Network Requirements:
- No network (standalone logger) → Arduino with SD card shield
- Wi-Fi cloud sync (periodic uploads) → ESP32 with MQTT
- Always-connected (real-time dashboard) → Raspberry Pi with Ethernet or commercial reader with PoE
- Multi-reader coordination (zone tracking) → Commercial readers with central RFID middleware
3. Power and Mounting:
- Battery-powered (mobile/handheld) → ESP32 with 18650 LiPo (runs 8+ hours)
- USB-powered (desk/bench) → Arduino or Raspberry Pi
- PoE installation (ceiling-mounted fixed readers) → Commercial reader with IEEE 802.3af PoE
4. Software Ecosystem:
- Custom firmware (C/C++ embedded) → Arduino or ESP32 (FreeRTOS)
- Python scripting (rapid prototyping) → Raspberry Pi
- Enterprise integration (EPCIS, SAP, WMS) → Commercial reader with LLRP protocol
5. Budget Constraints:
- Under $50 per reader: Arduino ($25 + RC522 $5) or ESP32 ($10 + RC522 $5)
- $50-$100 per reader: Raspberry Pi Zero W ($15) or Pi 4 ($45) + MFRC522 HAT
- Over $100 per reader: Evaluate commercial readers (Zebra FX series, Impinj R700, Nordic ID Sampo)
Real-World Example:
A library implements self-checkout kiosks: - Throughput: 5 books per checkout, 1 checkout every 30 seconds = ~0.2 tags/sec - Network: Wi-Fi for cloud inventory sync - Power: USB-powered from kiosk PC - Budget: $50 per station for 20 stations = $1,000 total
Selection: ESP32 + RC522 ($15 per station). Arduino Uno works but ESP32’s built-in Wi-Fi eliminates the need for separate Ethernet shields ($20 each). Raspberry Pi is overkill – the library does not need 100 tags/sec throughput or Python flexibility.
Total cost: 20 × $15 = $300. Saved $700 versus Pi-based solution, $39,000 versus commercial readers.
The Lesson: Match the reader platform to actual requirements. Commercial readers provide enterprise-grade reliability (99.9% uptime, industrial temperature range, vendor support) but cost 10-100× more than DIY solutions. For prototypes and low-scale production (under 50 readers), ESP32 or Raspberry Pi is often the sweet spot. For mission-critical 24/7 operations, pay for commercial reliability.
Deep Dives:
- RFID Fundamentals and Standards - Operating principles and ISO standards
- RFID Security and Privacy - Cryptographic authentication and privacy protocols
- NFC Architecture - Near-field communication as HF RFID extension
Comparisons:
- NFC vs RFID - When to use NFC versus traditional RFID
- Technology Comparison - RFID in context of IoT technology stack
Learning:
- Quizzes Hub - Test your RFID knowledge
- Knowledge Gaps - Identify areas for deeper study
Common Pitfalls
Tags with incorrect or factory-default EPCs produce misleading inventory results in lab exercises. Fix: programme fresh tags with known EPCs at the start of every lab session and verify the EPC before using tags in experiments.
Placing 5 tags vs 50 tags in the field produces dramatically different read rates but is often not held constant between experimental conditions. Fix: control the number of tags in the field as an independent variable and measure read rate separately for 5, 10, 20, and 50 tags.
A tag oriented perpendicular to the polarisation of the reader antenna reads very weakly. RSSI measurements that assume the worst case may be overly pessimistic; those that assume best-case orientation are too optimistic. Fix: measure RSSI at multiple tag orientations (0°, 45°, 90°) and report the minimum (worst-case) for deployment planning.
20.7 Summary
This chapter provided hands-on labs and assessment for RFID systems:
- Lab 1: ESP32 Access Control: Complete door lock system with RC522, LCD display, LED indicators, buzzer feedback, and access logging using circular buffer
- Lab 2: Python Dashboard: Flask-based web interface with real-time RFID scanning, inventory tracking, and periodic AJAX updates
- Security Assessment: MIFARE Classic Crypto1 vulnerability (broken 2008) vs DESFire EV3 AES-128 security
- Frequency Selection: LF for tissue penetration (pet microchips), HF/NFC for payments (<10cm range), UHF for inventory (1-12m range)
- Transit Systems: NFC is universal for payments (Apple Pay, Google Pay, EMV) - UHF’s long range causes accidental charges
20.8 Concept Relationships
Builds On:
- RFID Hardware Integration - Lab 1 extends Arduino RC522 examples to production features
- RFID Industry Applications - Lab 2 implements warehouse inventory patterns
Enables:
- RFID Security and Privacy - Upgrading from UID to challenge-response authentication
- Production RFID deployments with proper security and logging
Related Concepts:
- ESP32 circular buffer logging scales to SD card storage for production systems
- Flask dashboards provide real-time visibility into RFID inventory events
- LF 134.2 kHz is mandatory for pet microchips due to tissue penetration physics
20.9 See Also
Lab Extensions:
- ESP32 SD Card Tutorial - Replace circular buffer with persistent logging
- Flask-SocketIO Real-Time Updates - WebSocket for live dashboard updates
- SQLite Python Tutorial - Database for inventory tracking
Hardware Upgrades:
- RTC Module (DS3231) - Add accurate timestamps to access logs
- Relay Module Tutorial - Control actual door locks
- OLED Display (SSD1306) - Upgrade from LCD
Security Enhancements:
- MIFARE DESFire Tutorial - Implement AES-128 authentication
- NTAG 424 DNA Guide - Secure tag with SUN authentication
- Adafruit PN532 Security - DESFire programming examples
20.10 Try It Yourself
Lab 1 Enhancement: Add an SD card module to the ESP32 access control system. Log each access attempt as a CSV line: timestamp, UID, result (granted/denied), location. Implement a web server (ESP32WebServer) that serves the log file via HTTP for download and analysis.
Lab 2 Enhancement: Extend the Flask inventory dashboard with category-based filtering. Add a search bar that filters items by name or UID. Implement export functionality: “Download as CSV” button that generates a timestamped inventory report. Add bar charts showing items per category.
Cross-Lab Integration: Combine Lab 1 and Lab 2. ESP32 access control publishes to MQTT (same pattern as Python smart home). The Flask dashboard subscribes to the access MQTT topic and displays real-time access events alongside inventory scans. Create a unified dashboard showing both systems.
Production Deployment: Build a complete RFID asset tracking system for 20 items (tools, equipment, books). Tag each item with NTAG213. Create a Python Flask app with: (1) item registration (scan tag, enter name/category), (2) check-out/check-in logging, (3) “missing items” report (items not scanned in 7 days), (4) utilization statistics (most/least frequently used items).
20.11 What’s Next
| Chapter | Focus | Link |
|---|---|---|
| RFID Security and Privacy | Cryptographic authentication, privacy protocols, secure deployment | rfid-security-and-privacy.html |
| RFID Design and Deployment | Production system design, RF site surveys, tag selection | rfid-design-and-deployment.html |
| NFC Programming and Tags | NDEF records, tag writing, smartphone NFC integration | nfc-apps-programming-tags.html |
| RFID Troubleshooting | Diagnosing read failures, metal interference, and antenna tuning | rfid-troubleshooting.html |