ESP32 RFID User Management System
參考來源 :https://randomnerdtutorials.com/esp32-rfid-user-management-web-server/
這個系統採用了 「邊緣感應、雲端/中控邏輯、網頁管理」 的三層架構,利用 MQTT 協議實現硬體與軟體之間的非同步通訊。
這種架構最大的優點在於解耦(Decoupling):ESP32 不需要知道使用者名單,只需要負責讀取硬體訊號;所有的權限判斷、檔案讀寫與網頁顯示都交由效能更強的 Python 伺服器處理。
系統架構組成
1. 硬體邊緣端 (ESP32 + RFID + LCD)
這是系統的「眼睛」與「手」,負責第一線的物理交互。
感應 (RFID MFRC522):不斷掃描是否有卡片靠近,讀取 UID。
通訊 (MQTT):
發送:將讀取到的 UID 發布到
alex9ufo/rfid/detect。接收:訂閱
alex9ufo/rfid/control,接收來自 Python 的ON/OFF指令。
回饋 (LCD & LED):根據收到的指令,在 16x2 LCD 顯示「Access Granted」或「Denied」,並切換 GPIO 2 的 LED 亮滅。
2. 通訊中繼站 (MQTT Broker)
使用 broker.emqx.io 作為訊息交換中心。它就像一個郵局,負責把 ESP32 發出的 UID 傳遞給 Python,並把 Python 的控制指令傳回給 ESP32。
3. 後端管理中心 (Python Flask + Tkinter)
這是系統的「大腦」,負責運算與資料管理。
MQTT 監聽執行緒:全天候等待 UID 訊號。收到後去比對
users.txt,判斷權限後立即發回控制指令。Flask Web Server:
提供 API (
/view-log,/view-users) 讓前端讀取資料。處理邏輯 (
/get),執行新增用戶、刪除日誌等檔案操作。
資料儲存:使用簡單輕量、易於讀取的純文字檔 (
log.txt,users.txt)。Tkinter GUI:提供管理員一個即時監控視窗,並附帶一個快速開啟管理網頁的超連結。
4. 前端網頁介面 (HTML/CSS/JS)
這是使用者的「控制面板」。
即時日誌 (Full Log):透過 JavaScript 自動從後端抓取
log.txt並畫成表格。用戶管理 (Manage Users):顯示現有用戶清單,並提供刪除特定用戶的按鈕。
新增用戶 (Add User):提供下拉選單(Admin, User1~User6),將 UID 與 Role 寫入系統。
運作流程圖解
刷卡階段:
使用者刷卡 $\rightarrow$ ESP32 讀取 UID $\rightarrow$ 透過 MQTT 發布 alex9ufo/rfid/detect。
判斷階段:
Python 接收到 UID $\rightarrow$ 查詢 users.txt $\rightarrow$ 判斷 Role (Admin/UserX/Unknown)。
執行與紀錄階段:
Python 寫入 log.txt $\rightarrow$ 發布 ON 或 OFF 到 alex9ufo/rfid/control。
同時 Python 的 Tkinter 視窗顯示偵測紀錄。
硬體回饋階段:
ESP32 收到 ON $\rightarrow$ LED 亮起、LCD 顯示 Welcome。
管理階段:
管理員打開網頁查看 log.txt 的統計結果。
刷卡階段:
使用者刷卡 $\rightarrow$ ESP32 讀取 UID $\rightarrow$ 透過 MQTT 發布
alex9ufo/rfid/detect。
判斷階段:
Python 接收到 UID $\rightarrow$ 查詢
users.txt$\rightarrow$ 判斷 Role (Admin/UserX/Unknown)。
執行與紀錄階段:
Python 寫入
log.txt$\rightarrow$ 發布ON或OFF到alex9ufo/rfid/control。同時 Python 的 Tkinter 視窗顯示偵測紀錄。
硬體回饋階段:
ESP32 收到
ON$\rightarrow$ LED 亮起、LCD 顯示 Welcome。
管理階段:
管理員打開網頁查看
log.txt的統計結果。
資料夾結構複習
為了讓這個架構跑起來,您的檔案必須按此邏輯存放:
主目錄:
.py程式、.txt資料檔。templates/:存放所有的
.html檔案。static/:存放
style.css。
在此架構中,MQTT 扮演了硬體與軟體之間的橋樑。
ESP32 Arduino 程式 (負責感應與 MQTT 通訊)
此程式會讀取 RFID 標籤,並透過 MQTT 主題alex9ufo/rfid/detect 發送 UID。同時監聽 alex9ufo/rfid/control 以接收來自 Python 端的回饋(例如開門或 LED 提示)。
這套系統已經完整整合了 ESP32 (硬體端) 與 Python Flask + Tkinter (伺服器端)。
1. 檔案目錄結構 (重要)
請確保您的電腦資料夾結構如下,否則 Flask 會找不到網頁檔案:
📁
ESP32 RFID User Management System📄
User Management.py(下方的 Python 程式)📄
log.txt(自動產生)📄
users.txt(自動產生)📁
static📄
style.css(您上傳的 CSS 檔)
📁
templates📄
full-log.html(您上傳的第 1 個 HTML)📄
manage-users.html(您上傳的第 2 個 HTML)📄
add-user.html(您上傳的第 3 個 HTML)📄
get.html(您上傳的第 4 個 HTML)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Manage Users</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<nav>
<div class="nav-container">
<a href="/" class="brand">User Management</a>
<ul class="nav-menu">
<li><a href="/">📄 Full Log</a></li>
<li><a href="add-user">➕ Add User</a></li>
<li><a href="manage-users">👤 Manage Users</a></li>
</ul>
</div>
</nav>
<div class="main-container">
<section class="main-section">
<h2>📄 Full Access Log</h2>
<table id="tableData">
<thead>
<tr>
<th>Date</th>
<th>Time</th>
<th>UID</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<!-- Data from log.txt will be loaded here -->
</tbody>
</table>
</section>
</div>
<div class="main-container">
<a href="get?delete=log"><button class="button button-delete">🗑️ Delete log.txt File</button></a>
</div>
<script>
// JavaScript to load and parse log.txt
async function loadTableData() {
try {
const response = await fetch('view-log');
const data = await response.text();
const rows = data.trim().split('\n').slice(1); // Skip the header line
const tableBody = document.querySelector('#tableData tbody');
rows.forEach(row => {
const columns = row.split(',');
const tr = document.createElement('tr');
columns.forEach(column => {
const td = document.createElement('td');
td.textContent = column;
tr.appendChild(td);
});
tableBody.appendChild(tr);
});
} catch (error) {
console.error('Error loading log data:', error);
}
}
// Call the function to load log data
loadTableData();
</script>
</body>
</html>
Copy the following to the manage-users.html file.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Manage Users</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<nav>
<div class="nav-container">
<a href="/" class="brand">User Management</a>
<ul class="nav-menu">
<li><a href="/">📄 Full Log</a></li>
<li><a href="add-user">➕ Add User</a></li>
<li><a href="manage-users">👤 Manage Users</a></li>
</ul>
</div>
</nav>
<div class="main-container">
<section class="main-section">
<h2>👤 User Log</h2>
<table id="tableData">
<thead>
<tr>
<th>UID</th>
<th>Role</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
<!-- Data from users.txt will be loaded here -->
</tbody>
</table>
</section>
</div>
<div class="main-container">
<a href="get?delete=users"><button class="button button-delete">🗑️ Delete users.txt File</button></a>
</div>
<script>
// JavaScript to load and parse users.txt
async function loadTableData() {
try {
const response = await fetch('view-users');
const data = await response.text();
const rows = data.trim().split('\n').slice(1); // Skip the header line
const tableBody = document.querySelector('#tableData tbody');
rows.forEach((row, index) => {
const columns = row.split(',');
const tr = document.createElement('tr');
// Add remaining columns
columns.forEach(column => {
const td = document.createElement('td');
td.textContent = column;
tr.appendChild(td);
});
// Create and add row number cell with a delete link
const noCell = document.createElement('td');
const deleteLink = document.createElement('a');
deleteLink.href = `get?delete-user=${index + 1}`;
deleteLink.textContent = "❌ Delete User #" + (index + 1);
noCell.appendChild(deleteLink);
tr.appendChild(noCell);
tableBody.appendChild(tr);
});
} catch (error) {
console.error('Error loading log data:', error);
}
}
// Call the function to load log data
loadTableData();
</script>
</body>
</html>
Copy the following to the add-user.html file.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Add User</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<nav>
<div class="nav-container">
<a href="/" class="brand">User Management</a>
<ul class="nav-menu">
<li><a href="/">📄 Full Log</a></li>
<li><a href="add-user">➕ Add User</a></li>
<li><a href="manage-users">👤 Manage Users</a></li>
</ul>
</div>
</nav>
<div class="main-container">
<section class="main-section">
<h2>➕ Add User</h2>
<p>Enter the UID in lower case letters and no spaces.</p><br>
<form action="get" class="user-form">
<label for="uid">UID</label>
<input type="text" id="uid" name="uid" required>
<label for="role">Role</label>
<select id="role" name="role">
<option value="admin">Admin</option>
<option value="user">User</option>
</select>
<button type="submit">✅ Save</button>
</form>
</section>
</div>
</body>
</html>
Copy the following to the get.html file.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Add User</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<nav>
<div class="nav-container">
<a href="/" class="brand">User Management</a>
<ul class="nav-menu">
<li><a href="/">📄 Full Log</a></li>
<li><a href="add-user">➕ Add User</a></li>
<li><a href="manage-users">👤 Manage Users</a></li>
</ul>
</div>
</nav>
<div class="main-container">
<section class="main-section">
<p>%inputmessage%</p>
</section>
</div>
</body>
</html>
Copy the following to the style.css file.
/* General Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background-color: #f4f4f9;
color: #333;
display: flex;
flex-direction: column;
align-items: center;
height: 100vh;
margin: 0;
}
/* Navigation Bar Styles */
nav {
width: 100%;
background-color: #333;
padding: 1rem 0;
}
.nav-container {
max-width: 1200px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 1rem;
}
.brand {
color: #fff;
text-decoration: none;
font-size: 1.5rem;
font-weight: bold;
}
.nav-menu {
list-style-type: none;
display: flex;
}
.nav-menu li {
margin-left: 1.5rem;
}
.nav-menu a {
color: #fff;
text-decoration: none;
font-size: 1rem;
transition: color 0.3s;
}
.nav-menu a:hover, .nav-menu a.active {
color: #f4f4f9;
}
.main-container {
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
width: 100%;
}
.main-section {
max-width: 500px;
padding: 2rem;
background-color: #fff;
border-radius: 5px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
text-align: center;
}
.main-section h2 {
margin-bottom: 1rem;
color: #333;
}
.user-form label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
color: #333;
}
.user-form input, .user-form select {
width: 100%;
padding: 0.5rem;
margin-bottom: 1rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.user-form button {
width: 100%;
padding: 0.7rem;
background-color: #333;
color: #fff;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.3s;
}
.user-form button:hover {
background-color: #555;
}
.button {
display: inline-block;
padding: 10px 20px;
margin: 10px;
font-size: 16px;
border: none;
border-radius: 5px;
cursor: pointer;
transition-duration: 0.4s;
}
.button-delete {
background-color: #780320;
color: #fff;
}
.button-home {
background-color: #333;
color: #fff;
}
#tableData {
font-family: Arial, Helvetica, sans-serif;
border-collapse: collapse;
width: 100%;
}
#tableData td, #tableData th {
border: 1px solid #ddd;
padding: 8px;
}
#tableData tr:nth-child(even) {
background-color: #f2f2f2;
}
#tableData tr:hover {
background-color: #ddd;
}
#tableData th {
padding-top: 12px;
padding-bottom: 12px;
text-align: left;
background-color: #1f1f1f;
color: white;
}
2. Python 伺服器程式 (User Management.py)
這段程式整合了 MQTT 監聽、Flask 網頁伺服器,以及一個可點擊超連結的 Tkinter 視窗。
import paho.mqtt.client as mqtt
from flask import Flask, render_template, request, redirect, url_for, Response
import threading
import os
import webbrowser
from datetime import datetime
import tkinter as tk
from tkinter import scrolledtext, font as tkfont
# --- 基礎設定 ---
app = Flask(__name__)
LOG_FILE = "log.txt"
USER_FILE = "users.txt"
MQTT_BROKER = "broker.emqx.io"
TOPIC_DETECT = "alex9ufo/rfid/detect"
TOPIC_CONTROL = "alex9ufo/rfid/control"
# 初始化檔案
def init_files():
if not os.path.exists(LOG_FILE):
with open(LOG_FILE, "w", encoding="utf-8") as f: f.write("Date,Time,UID,Role\n")
if not os.path.exists(USER_FILE):
with open(USER_FILE, "w", encoding="utf-8") as f: f.write("UID,Role\n")
# 查詢使用者權限
def get_role(uid):
if os.path.exists(USER_FILE):
with open(USER_FILE, "r", encoding="utf-8") as f:
for line in f.readlines()[1:]:
if "," in line:
u, r = line.strip().split(',')
if u == uid: return r
return "Unknown"
# --- MQTT 邏輯 ---
def on_message(client, userdata, msg):
uid = msg.payload.decode().lower().strip()
role = get_role(uid)
now = datetime.now()
# 紀錄到 log.txt
log_entry = f"{now.strftime('%Y-%m-%d')},{now.strftime('%H:%M:%S')},{uid},{role}\n"
with open(LOG_FILE, "a", encoding="utf-8") as f: f.write(log_entry)
# 權限判斷回傳給 ESP32 (控制 LED)
# 只有在 users.txt 裡的已知用戶才發送 ON
response_msg = "ON" if role != "Unknown" else "OFF"
client.publish(TOPIC_CONTROL, response_msg)
# 更新 GUI 顯示
if 'gui_log' in globals():
gui_log.insert(tk.END, f"[{now.strftime('%H:%M:%S')}] 偵測: {uid} -> {role}\n")
gui_log.see(tk.END)
mqtt_client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
mqtt_client.on_message = on_message
mqtt_client.connect(MQTT_BROKER, 1883)
mqtt_client.subscribe(TOPIC_DETECT)
# --- Flask 路由 ---
@app.route('/')
def index(): return render_template('full-log.html')
@app.route('/add-user')
def add_user(): return render_template('add-user.html')
@app.route('/manage-users')
def manage_users(): return render_template('manage-users.html')
@app.route('/view-log')
def view_log():
with open(LOG_FILE, "r", encoding="utf-8") as f: return Response(f.read(), mimetype='text/plain')
@app.route('/view-users')
def view_users():
with open(USER_FILE, "r", encoding="utf-8") as f: return Response(f.read(), mimetype='text/plain')
@app.route('/get')
def handle_get():
# 處理新增用戶
uid = request.args.get('uid')
role = request.args.get('role')
if uid and role:
with open(USER_FILE, "a", encoding="utf-8") as f: f.write(f"{uid.lower().strip()},{role}\n")
return render_template('get.html', inputmessage=f"成功新增用戶: {uid}")
# 處理刪除功能
target = request.args.get('delete')
if target == 'log':
with open(LOG_FILE, "w", encoding="utf-8") as f: f.write("Date,Time,UID,Role\n")
return redirect(url_for('index'))
elif target == 'users':
with open(USER_FILE, "w", encoding="utf-8") as f: f.write("UID,Role\n")
return redirect(url_for('manage_users'))
# 處理單一用戶刪除
user_idx = request.args.get('delete-user')
if user_idx:
with open(USER_FILE, "r", encoding="utf-8") as f: lines = f.readlines()
idx = int(user_idx)
if 0 < idx < len(lines):
del lines[idx]
with open(USER_FILE, "w", encoding="utf-8") as f: f.writelines(lines)
return redirect(url_for('manage_users'))
return redirect(url_for('index'))
# --- Tkinter 視窗與超連結 ---
def run_gui():
global gui_log
root = tk.Tk()
root.title("RFID MQTT 監控系統")
root.geometry("450x400")
tk.Label(root, text="即時刷卡紀錄", font=("Arial", 12, "bold")).pack(pady=5)
gui_log = scrolledtext.ScrolledText(root, width=50, height=15)
gui_log.pack(padx=10, pady=10)
url = "http://127.0.0.1:5000"
link_label = tk.Label(root, text=f"點此開啟網頁管理介面\n{url}", fg="blue", cursor="hand2")
link_label.pack(pady=10)
# 點擊開啟瀏覽器
def open_url(event): webbrowser.open_new(url)
link_label.bind("<Button-1>", open_url)
# 字體底線效果
f = tkfont.Font(link_label, link_label.cget("font"))
f.configure(underline=True)
link_label.configure(font=f)
root.mainloop()
if __name__ == '__main__':
init_files()
threading.Thread(target=mqtt_client.loop_forever, daemon=True).start()
threading.Thread(target=lambda: app.run(port=5000, debug=False, use_reloader=False), daemon=True).start()
run_gui()














沒有留言:
張貼留言