2026年5月2日 星期六

使用一個 .exe 檔案在 Windows 10-11 上輕鬆安裝 Node-Red


安裝 Node-RED 主要分兩步:

1. 安裝 Node.js(推薦 LTS 版本);

2. 使用 NPM 指令 npm install -g --unsafe-perm node-red 進行全域安裝。

安裝後,在終端機輸入 node-red 啟動,並在瀏覽器開啟 http://localhost:1880 

http://127.0.0.1:1880

=====================


詳細安裝步驟 (Windows/macOS/Linux)
  1. 安裝 Node.js (必須)
    • 前往 Node.js 官方網站 下載並安裝最新的 LTS(長期支援)版本。
    • 安裝完成後,開啟命令提示字元 (cmd) 或終端機 (Terminal),輸入以下指令驗證版本:
      node -v
      npm -v
      

  2. 安裝 Node-RED
    • 在終端機中執行以下指令來全域安裝 Node-RED:

      npm install -g --unsafe-perm node-red
      

使用一個 .exe 檔案在 Windows 10-11 上輕鬆安裝 Node-Red


源自於 https://www.reddit.com/r/nodered/comments/1i9qiss/easy_installation_of_nodered_on_windows_1011_with/?tl=zh-hant


 Windows 10 和 11 建立了一個安裝程式和一個可移植版本的 Node-Red,

對於在 VM 中進行新安裝以及將整個專案及其相依性移至另一台伺服器非常有用。

範例影片連結: https://youtu.be/gV2FfDJb9UE

下載網站: https://vitormiao.com/Node-RED-Installer





2026年4月28日 星期二

WOKWI board-mfrc522 RFID Reader

WOKWI board-mfrc522 RFID Reader




 #include <SPI.h>

#include <MFRC522.h>

#define SS_PIN  5
#define RST_PIN 21

MFRC522 rfid(SS_PIN, RST_PIN);

void setup() {
  Serial.begin(115200);
  SPI.begin();
  rfid.PCD_Init();
  Serial.println("MFRC522 Ready");
}

void loop() {
  delay(10);
  if (!rfid.PICC_IsNewCardPresent()) {
    return;
  }

  if (!rfid.PICC_ReadCardSerial()) {
    return;
  }

  Serial.print("UID:");
  for (byte i = 0; i < rfid.uid.size; i++) {
    Serial.print(rfid.uid.uidByte[i] < 0x10 ? ":0" : ":");
    Serial.print(rfid.uid.uidByte[i], HEX);
  }
  Serial.println();

  rfid.PICC_HaltA();
  delay(1000);
}


board-mfrc522 RFID Reader

MFRC522 RFID/NFC reader module with SPI interface for reading 13.56 MHz MIFARE cards and tags.

board-mfrc522

Pin names

NameDescription
3.3VVoltage supply (3.3V)
RSTReset (active low)
GNDGround
IRQInterrupt request (active low)
MISOSPI data out
MOSISPI data in
SCKSPI clock
SDASPI chip select (active low)

Attributes

NameDescriptionDefault value
uidCustom UID (for Blue Card only)""

UID format

The UID can be specified in the following lengths:

  • 4 bytes: 01:02:03:04 (MIFARE Classic)
  • 7 bytes: 04:11:22:33:44:55:66 (MIFARE Ultralight)

Each byte must be two hexadecimal characters (00-FF), separated by colons.

Card presets

The simulator provides built-in card presets for quick testing. Click on the MFRC522 to open the control panel where you can select a card:

IndexPresetColorUIDCard Type
0Blue CardBlue01:02:03:04MIFARE Classic 1K
1Green CardGreen11:22:33:44MIFARE Classic 1K
2Yellow CardYellow55:66:77:88MIFARE Classic 1K
3Red CardRedAA:BB:CC:DDMIFARE Classic 1K
4NFC TagGray04:11:22:33:44:55:66MIFARE Ultralight
5Key FobOrangeC0:FF:EE:99MIFARE Mini

The uid attribute only affects the Blue Card (index 0). Other cards always use their preset UIDs. This lets you customize your main card while keeping the others for multi-user testing.

2026年4月27日 星期一

ESP32 RFID User Management System (修正 新增/比對 模式)

ESP32 RFID User Management System (修正  新增/比對 模式)












系統邏輯設計

  • 模式切換:在 Python 後端設定一個變數來紀錄當前模式(比對或新增)。

  • 新增模式:ESP32 讀取 UID 後發送給伺服器,伺服器將 UID 傳送到網頁表單,並啟動 5 秒倒數,若無人存檔則設為 Unknown。

  • 比對模式:即原本的邏輯,檢查白名單並回傳信號控制繼電器。

Python 後端程式整合了 MQTT 監聽Flask 網頁伺服器模式切換 API 以及 Tkinter 監控視窗

請將此程式存為 User Management.py


import paho.mqtt.client as mqtt
from flask import Flask, render_template, request, redirect, url_for, Response, jsonify
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"

# 系統全局狀態
current_mode = "compare"  # 模式:'compare' (比對) 或 'add' (新增)
latest_uid = ""           # 暫存新增模式下感應到的 UID

# 初始化檔案
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")

# 從 users.txt 查詢角色
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.lower(): return r
    return "Unknown"

# --- MQTT 邏輯 ---
def on_message(client, userdata, msg):
    global latest_uid, current_mode
    uid = msg.payload.decode().lower().strip()
    now = datetime.now()
    
    if current_mode == "add":
        # 新增模式:將 UID 傳給網頁,不觸發繼電器
        latest_uid = uid
        client.publish(TOPIC_CONTROL, "MODE_ADD")
        log_msg = f"模式:新增 | 偵測 UID: {uid} (等待網頁存檔)"
    else:
        # 比對模式:檢查白名單
        role = get_role(uid)
        if role != "Unknown":
            client.publish(TOPIC_CONTROL, "ON")  # 通知 ESP32 開鎖
            log_msg = f"模式:比對 | UID: {uid} | 角色: {role} -> 准許進入"
        else:
            client.publish(TOPIC_CONTROL, "OFF") # 通知 ESP32 拒絕/警報
            log_msg = f"模式:比對 | UID: {uid} | 角色: 未知 -> 拒絕進入"
        
        # 紀錄日誌到 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)

    # 更新 GUI 視窗
    if 'gui_log' in globals():
        gui_log.insert(tk.END, f"[{now.strftime('%H:%M:%S')}] {log_msg}\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_page():
    return render_template('add-user.html')

@app.route('/manage-users')
def manage_users():
    return render_template('manage-users.html')

@app.route('/set_mode')
def set_mode():
    global current_mode
    mode = request.args.get('mode')
    if mode in ['add', 'compare']:
        current_mode = mode
    return jsonify({"status": "success", "mode": current_mode})

@app.route('/get_latest_uid')
def get_uid():
    global latest_uid
    # 網頁端會持續輪詢此路徑
    temp = latest_uid
    latest_uid = "" # 讀取後清除,避免重複觸發倒數
    return temp

@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} 權限: {role}")

    # 處理刪除邏輯
    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 GUI 視窗 ---
def run_gui():
    global gui_log
    root = tk.Tk()
    root.title("RFID 系統監控後端")
    root.geometry("500x450")

    tk.Label(root, text="系統即時狀態紀錄", font=("Arial", 12, "bold")).pack(pady=5)
    gui_log = scrolledtext.ScrolledText(root, width=60, height=20)
    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", font=("Arial", 10, "underline"))
    link_label.pack(pady=10)
    link_label.bind("<Button-1>", lambda e: webbrowser.open_new(url))

    root.mainloop()

# --- 主程式啟動 ---
if __name__ == '__main__':
    init_files()
    # 啟動 MQTT 執行緒
    threading.Thread(target=mqtt_client.loop_forever, daemon=True).start()
    # 啟動 Flask 執行緒
    threading.Thread(target=lambda: app.run(port=5000, debug=False, use_reloader=False), daemon=True).start()
    # 啟動 GUI (主執行緒)
    run_gui()

程式邏輯說明:

  1. /set_mode API:讓前端網頁的按鈕可以切換 current_mode

  2. /get_latest_uid API:前端網頁透過此 API 獲取剛才在 ESP32 刷下的卡號。

  3. on_message 判斷

    • 新增模式 下,它只會將卡號存入 latest_uid 並通知 ESP32 顯示「模式:新增中」,不會去檢查白名單或開鎖。

    • 比對模式 下,它會比對 users.txt。若 UID 存在則發送 ON (觸發繼電器),不存在則發送 OFF (警報)。

  4. Tkinter 視窗:提供管理員即時查看後端正在進行「比對」還是「新增」,並顯示刷卡結果。


ESP32 程式修正 (控制繼電器與警示)

修改 callback 函式來處理繼電器與警示聲(假設繼電器在 GPIO 4)。


#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 RELAY_PIN 4
#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);

void callback(char* topic, byte* payload, unsigned int length) {
  String msg = "";
  for (int i = 0; i < length; i++) msg += (char)payload[i];
 
  Serial.print("收到指令: "); Serial.println(msg);
  lcd.setCursor(0, 1);
 
  if (msg == "ON") {
    digitalWrite(RELAY_PIN, HIGH); // 打開電動鎖
    digitalWrite(LED_PIN, HIGH);
    lcd.print("Access: Granted ");
    delay(3000);
    digitalWrite(RELAY_PIN, LOW); // 自動關鎖
    digitalWrite(LED_PIN, LOW);
  } else if (msg == "OFF") {
    //digitalWrite(BUZZER_PIN, HIGH); // 報警聲
    lcd.print("Access: Denied  ");
    delay(500);
    //digitalWrite(BUZZER_PIN, LOW);
  } else if (msg == "MODE_ADD") {
    lcd.print("Mode: Registering");
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
  pinMode(RELAY_PIN, OUTPUT);
  //pinMode(BUZZER_PIN, OUTPUT);
 
  Wire.begin(I2C_SDA, I2C_SCL);
  lcd.init(); lcd.backlight();
 
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
  lcd.clear(); lcd.print("Ready to Scan");

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

void reconnect() {
  while (!client.connected()) {
    if (client.connect("ESP32_RFID_alex9ufo")) {
      client.subscribe(topic_control);
    } else { delay(5000); }
  }
}

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

  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.setCursor(0, 0);
    lcd.print("UID: " + uid);
    Serial.println("偵測 UID: " + uid);
    client.publish(topic_detect, uid.c_str());
    mfrc522.PICC_HaltA();
    delay(2000);
  }
}

系統運作說明:

  1. 新增模式:管理員在網頁點擊「進入新增模式」,此時刷卡 UID 會自動跳轉到網頁的輸入框,網頁啟動 5 秒倒數。若管理員沒選 User1~6,時間一到網頁會自動以 Unknown 角色存檔。

  2. 比對模式:系統預設狀態。刷卡後 Python 檢查 users.txt,若 UID 存在,發送 ON 給 ESP32。

  3. 硬體執行:ESP32 收到 ON 後將 RELAY_PIN 設為高電位(打開門鎖);若收到 OFF 則觸發 BUZZER_PIN 警報。


前端網頁介面 (add-user.html)

我們在表單上方加入「模式切換按鈕」,並利用 JavaScript 實作 5 秒倒數功能。

HTML
<section class="main-section">
    <h2>🎮 系統控制面板</h2>
    
    <div class="mode-selector" style="margin-bottom: 25px; padding: 15px; background: #eee; border-radius: 8px;">
        <p><strong>切換運作模式:</strong></p>
        <button onclick="changeMode('compare')" id="btn-compare" class="button" style="background-color: #555;">🔍 比對模式 (監控中)</button>
        <button onclick="changeMode('add')" id="btn-add" class="button" style="background-color: #28a745;">➕ 進入新增模式</button>
        <p id="current-status" style="margin-top: 10px; color: blue;">當前狀態:比對模式</p>
    </div>

    <div id="timer-box" style="color: red; font-weight: bold; font-size: 1.2rem; height: 30px;"></div>

    <form action="get" id="add-user-form" class="user-form">
        <label for="uid">感應到的 UID</label>
        <input type="text" id="uid" name="uid" placeholder="請在新增模式下刷卡..." readonly 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>
            <option value="unknown">Unknown</option>
        </select>
        <button type="submit" style="background-color: #007bff;">✅ 確認存檔</button>
    </form>
</section>

<script>
    let countdownTimer;

    // 1. 切換模式
    async function changeMode(mode) {
        await fetch(`/set_mode?mode=${mode}`);
        const statusText = document.getElementById('current-status');
        if(mode === 'add') {
            statusText.innerText = "當前狀態:新增模式 (請到設備刷卡)";
            statusText.style.color = "green";
        } else {
            statusText.innerText = "當前狀態:比對模式";
            statusText.style.color = "blue";
            document.getElementById('timer-box').innerText = "";
            clearInterval(countdownTimer);
        }
    }

    // 2. 輪詢後端是否有新的 UID 刷入
    setInterval(async () => {
        const response = await fetch('/get_latest_uid');
        const uid = await response.text();
        if (uid && uid.length > 0) {
            document.getElementById('uid').value = uid;
            startAutoSaveTimer();
        }
    }, 1000);

    // 3. 5秒倒數邏輯
    function startAutoSaveTimer() {
        let seconds = 5;
        clearInterval(countdownTimer);
        const timerBox = document.getElementById('timer-display');
        
        countdownTimer = setInterval(() => {
            document.getElementById('timer-box').innerText = `⏱️ 偵測到新卡片!將在 ${seconds} 秒後自動存為 Unknown...`;
            if (seconds <= 0) {
                clearInterval(countdownTimer);
                // 如果管理員沒改選,就設為 unknown 並自動送出
                const roleSelect = document.getElementById('role');
                if (roleSelect.value === "admin") { // 假設預設是 admin,沒動就變 unknown
                    roleSelect.value = "unknown";
                }
                document.getElementById('add-user-form').submit();
            }
            seconds--;
        }, 1000);
    }
</script>

3. 操作流程說明

  1. 進入新增模式:在網頁點擊「進入新增模式」按鈕。

  2. 刷卡感應:拿一張新卡片去感應 ESP32。

  3. 自動填入:網頁會透過每秒一次的 fetch 抓到這個 UID,並自動填入 input 欄位。

  4. 倒數與決策

    • 手動操作:管理員有 5 秒鐘時間可以從下拉選單選擇 User1User6,然後按「確認存檔」。

    • 自動超時:如果 5 秒內沒人理它,JavaScript 會強行把 Role 改成 Unknown 並自動觸發 submit() 存入 users.txt

  5. 返回比對:存檔成功後,點擊「返回比對模式」,系統恢復正常的門禁開鎖功能。

這樣您的「控制面板」就真正具備了遠端操控硬體行為的能力。

使用一個 .exe 檔案在 Windows 10-11 上輕鬆安裝 Node-Red

安裝 Node-RED 主要分兩步: 1. 安裝  Node.js (推薦 LTS 版本); 2. 使用 NPM 指令  npm install -g --unsafe-perm node-red  進行全域安裝。 安裝後,在終端機輸入  node-red  啟動,並在瀏覽器開啟...