2026年4月27日 星期一

ESP32 RFID User Management System

 

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 寫入系統。


運作流程圖解

  1. 刷卡階段

    • 使用者刷卡 $\rightarrow$ ESP32 讀取 UID $\rightarrow$ 透過 MQTT 發布 alex9ufo/rfid/detect

  2. 判斷階段

    • Python 接收到 UID $\rightarrow$ 查詢 users.txt $\rightarrow$ 判斷 Role (Admin/UserX/Unknown)。

  3. 執行與紀錄階段

    • Python 寫入 log.txt $\rightarrow$ 發布 ONOFFalex9ufo/rfid/control

    • 同時 Python 的 Tkinter 視窗顯示偵測紀錄。

  4. 硬體回饋階段

    • ESP32 收到 ON $\rightarrow$ LED 亮起、LCD 顯示 Welcome。

  5. 管理階段

    • 管理員打開網頁查看 log.txt 的統計結果。


資料夾結構複習

為了讓這個架構跑起來,您的檔案必須按此邏輯存放:

  • 主目錄.py 程式、.txt 資料檔。

  • templates/:存放所有的 .html 檔案。

  • static/:存放 style.css


在此架構中,MQTT 扮演了硬體與軟體之間的橋樑。





ESP32 Arduino 程式 (負責感應與 MQTT 通訊)

此程式會讀取 RFID 標籤,並透過 MQTT 主題alex9ufo/rfid/detect 發送 UID。同時監聽 alex9ufo/rfid/control 以接收來自 Python 端的回饋(例如開門或 LED 提示)。

#include <SPI.h>
#include <MFRC522.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>

// 腳位與設定
#define I2C_SDA   17
#define I2C_SCL   16
#define LED_PIN   2
#define SS_PIN    5
#define RST_PIN   22

LiquidCrystal_I2C lcd(0x27, 16, 2);
MFRC522 mfrc522(SS_PIN, RST_PIN);

const char* ssid = "Wokwi-GUEST";
const char* password = "";
const char* mqtt_server = "broker.emqx.io";
const char* topic_detect = "alex9ufo/rfid/detect";
const char* topic_control = "alex9ufo/rfid/control";

WiFiClient espClient;
PubSubClient client(espClient);

// 接收來自 Python 的指令
void callback(char* topic, byte* payload, unsigned int length) {
  String msg = "";
  for (int i = 0; i < length; i++) msg += (char)payload[i];
 
  // 輸出到 Serial Monitor
  Serial.print("收到控制指令: ");
  Serial.println(msg);

  lcd.setCursor(0, 1);
  if (msg == "ON") {
    digitalWrite(LED_PIN, HIGH);
    lcd.print("Access: Granted ");
    Serial.println("狀態: 允許進入 (LED亮)");
  } else {
    digitalWrite(LED_PIN, LOW);
    lcd.print("Access: Denied  ");
    Serial.println("狀態: 拒絕進入 (LED滅)");
  }
}

void setup() {
  Serial.begin(115200);
  Serial.println("系統初始化中...");
 
  pinMode(LED_PIN, OUTPUT);
  Wire.begin(I2C_SDA, I2C_SCL);
  lcd.init();
  lcd.backlight();
 
  lcd.print("WiFi Connecting");
  Serial.print("正在連接 WiFi: ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
 
  Serial.println("\nWiFi 連線成功!");
  Serial.print("IP 地址: ");
  Serial.println(WiFi.localIP());

  lcd.clear();
  lcd.print("WiFi Connected!");
  delay(1000);
  lcd.clear();
  lcd.print("Ready to Scan");
  Serial.println("等待 RFID 感應...");

  SPI.begin();
  mfrc522.PCD_Init();
  client.setServer(mqtt_server, 1883);
  client.setCallback(callback);
}

void reconnect() {
  while (!client.connected()) {
    Serial.print("嘗試連接 MQTT Broker...");
    if (client.connect("ESP32_RFID_alex9ufo")) {
      Serial.println("成功連線!");
      client.subscribe(topic_control);
      Serial.print("已訂閱主題: ");
      Serial.println(topic_control);
    } else {
      Serial.print("失敗, rc=");
      Serial.print(client.state());
      Serial.println(" 5秒後重試");
      delay(5000);
    }
  }
}

void loop() {
  if (!client.connected()) reconnect();
  client.loop();

  // 偵測 RFID 卡片
  if (mfrc522.PICC_IsNewCardPresent() && mfrc522.PICC_ReadCardSerial()) {
    String uid = "";
    for (byte i = 0; i < mfrc522.uid.size; i++) {
      uid += String(mfrc522.uid.uidByte[i] < 0x10 ? "0" : "");
      uid += String(mfrc522.uid.uidByte[i], HEX);
    }
   
    // 輸出到 LCD
    lcd.setCursor(0, 0);
    lcd.print("UID: " + uid);
   
    // 輸出到 Serial Monitor
    Serial.print("\n偵測到卡片! UID: ");
    Serial.println(uid);
   
    // 發送到 MQTT
    Serial.print("發送至 MQTT: ");
    Serial.println(topic_detect);
    client.publish(topic_detect, uid.c_str());
   
    mfrc522.PICC_HaltA();
    delay(2000); // 避免短時間重複感應
  }
}


這套系統已經完整整合了 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)





D:\2026 RFID\ESP32 RFID User Management System\
├── User Management.py (Python的程式)
├── log.txt
├── users.txt
├── static/
│   └── style.css
└── templates/
    ├── full-log.html
    ├── add-user.html
    ├── manage-users.html
    └── get.html



Copy the following to the full-log.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>📄 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()












新增 Users

<!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="user1">User1</option>
    <option value="user2">User2</option>
    <option value="user3">User3</option>
    <option value="user4">User4</option>
    <option value="user5">User5</option>
    <option value="user6">User6</option>
    </select>
                <button type="submit">✅ Save</button>
            </form>
        </section>
    </div>
</body>
</html>



沒有留言:

張貼留言

ESP32 RFID User Management System

  ESP32 RFID User Management System 參考來源 : https://randomnerdtutorials.com/esp32-rfid-user-management-web-server/ 這個系統採用了 「邊緣感應、雲端/中控邏輯、網頁管理...