2026年5月31日 星期日

Galois LFSR

 在密碼學與數位訊號處理中,LFSR(線性回饋移位暫存器,Linear Feedback Shift Register) 是用來產生偽隨機序列(也就是串流加密中所需的金鑰流)最核心的硬體架構。

LFSR 主要分為兩種實現架構:Fibonacci(斐波那契)Galois(伽羅瓦)。其中,Galois LFSR 因為將互斥或(XOR)閘分散在各個暫存器之間,訊號不需要經過長串的級聯邏輯,因此在硬體電路中運作速度極快、延遲極低。

為了直觀說明 Galois LFSR 的運作原理,我們可以使用 Python 的 tkinter 套件來製作一個可視化的動態模擬器。

Galois LFSR 運作原理

一個 $n$ 階的 Galois LFSR 包含:

  1. 暫存器狀態(Registers): 一組儲存 01 的位元陣列(通常記為 $X_0, X_1, X_2 \dots$)。

  2. 抽頭(Tap): 決定哪些位置的暫存器需要參與 XOR 運算。這可以用一個特徵多項式來表示。

【移動規律】:

在每一個時脈脈衝(Clock)到來時:

  • 輸出: 最末端(最小位元)的數值輸出,作為隨機位元。

  • 回饋: 檢視這個輸出的值是 0 還是 1

    • 如果輸出是 0:所有暫存器的值單純向右移一格。

    • 如果輸出是 1:所有暫存器的值向右移一格,但有抽頭(Tap)的位置必須與 1 進行 XOR 運算(也就是反轉訊號)

    • 最左端(最高位元)則補入該輸出值。

我們使用一個經典的 4 階多項式:f(x) = x^4 + x^1 + 1 (抽頭在第 1 位與第 4 位),初始狀態(Seed)設為 1 0 1 1

使用特徵多項式 f(x) = X^4 + X^1 + 1 (抽頭在 X_1 的輸入端,也就是當 X_0 輸出 1 時,會與X_2 移向 X_1 的資料進行 XOR)。






import customtkinter as ctk
import tkinter as tk

# 設定外觀風格與主題
ctk.set_appearance_mode("System")  
ctk.set_default_color_theme("blue") 

class GaloisLFSRVisualizer(ctk.CTk):
    def __init__(self):
        super().__init__()
        
        # =============================================================
        # 核心修正:強力繞過 CustomTkinter 內部與 Thonny/底層環境的相容性 Bug
        # =============================================================
        # 1. 繞過標題列顏色設定錯誤
        self._windows_set_titlebar_color = lambda appearance_mode: None
        
        # 2. 繞過背景縮放追蹤器 (Scaling Tracker) 的無限迴圈崩潰
        try:
            from customtkinter.windows.widgets.scaling.scaling_tracker import ScalingTracker
            ScalingTracker.check_dpi_scaling = lambda *args, **kwargs: None
        except Exception:
            pass
        # =============================================================

        self.title("Galois LFSR 現代化動態模擬器")
        self.geometry("800x520")
        
        # 4位元 Galois LFSR 參數設定
        self.num_bits = 4
        self.state = [1, 0, 1, 1]          # 初始種子 (Seed)
        self.taps = [True, False, True, False] # [X3_in, X2_in, X1_in, X0_in] 是否有抽頭
        self.clock_count = 0
        self.is_playing = False
        self.output_history = []

        self.setup_ui()
        
        # 確保 Canvas 佈局完成後再進行第一次繪製
        self.update_idletasks()
        self.draw_lfsr()

    def setup_ui(self):
        # 1. 標題與說明面板
        self.title_frame = ctk.CTkFrame(self, fg_color="transparent")
        self.title_frame.pack(pady=15, fill=ctk.X)
        
        self.lbl_title = ctk.CTkLabel(self.title_frame, text="Galois LFSR 運作原理動態模擬", font=ctk.CTkFont(family="Helvetica", size=20, weight="bold"))
        self.lbl_title.pack()
        
        self.lbl_formula = ctk.CTkLabel(self.title_frame, text="特徵多項式: f(x) = X⁴ + X¹ + 1  (抽頭設於 X₁ 的輸入端)", font=ctk.CTkFont(family="Courier", size=13, weight="normal"))
        self.lbl_formula.pack(pady=2)

        # 2. 畫布面板 (放置傳統 Canvas 用於繪製電路圖)
        self.canvas_frame = ctk.CTkFrame(self, width=740, height=190, corner_radius=10)
        self.canvas_frame.pack(pady=10, padx=20, fill=ctk.BOTH, expand=True)
        
        self.canvas = tk.Canvas(self.canvas_frame, bg="#ffffff", highlightthickness=0)
        self.canvas.pack(padx=5, pady=5, fill=tk.BOTH, expand=True)

        # 3. 狀態與數據顯示面板
        self.info_frame = ctk.CTkFrame(self, corner_radius=10)
        self.info_frame.pack(pady=10, padx=20, fill=ctk.X)
        
        self.lbl_clock = ctk.CTkLabel(self.info_frame, text="目前時脈 (Clock): 0", font=ctk.CTkFont(size=13, weight="bold"), anchor="w")
        self.lbl_clock.pack(side=ctk.TOP, fill=ctk.X, padx=15, pady=(8, 2))

        self.lbl_status = ctk.CTkLabel(self.info_frame, text="準備就緒。請點擊單步執行或自動播放。", font=ctk.CTkFont(size=13), text_color="#1f77b4", anchor="w")
        self.lbl_status.pack(side=ctk.TOP, fill=ctk.X, padx=15, pady=2)

        self.lbl_output = ctk.CTkLabel(self.info_frame, text="產生的偽隨機序列 (Output): ", font=ctk.CTkFont(family="Courier", size=14, weight="bold"), text_color="#d62728", anchor="w")
        self.lbl_output.pack(side=ctk.TOP, fill=ctk.X, padx=15, pady=(2, 8))

        # 4. 控制按鈕面板
        self.control_frame = ctk.CTkFrame(self, fg_color="transparent")
        self.control_frame.pack(pady=15)

        self.btn_next = ctk.CTkButton(self.control_frame, text="單步執行 (Next)", width=150, command=self.step_lfsr)
        self.btn_next.grid(row=0, column=0, padx=10)

        self.btn_toggle = ctk.CTkButton(self.control_frame, text="自動播放", width=150, fg_color="#2ca02c", hover_color="#238223", command=self.toggle_play)
        self.btn_toggle.grid(row=0, column=1, padx=10)

        self.btn_reset = ctk.CTkButton(self.control_frame, text="重設 (Reset)", width=150, fg_color="#7f7f7f", hover_color="#636363", command=self.reset_lfsr)
        self.btn_reset.grid(row=0, column=2, padx=10)

    def draw_lfsr(self, feedback_active=None):
        """利用 Canvas 繪製動態的 Galois LFSR 架構圖"""
        self.canvas.delete("all")
        
        canvas_width = self.canvas.winfo_width()
        if canvas_width < 10:
            canvas_width = 730
        
        box_width, box_height = 60, 50
        gap = 80  
        start_x = (canvas_width - ((self.num_bits - 1) * gap + box_width)) // 2 - 20
        start_y = 50
        
        # 1. 畫出主回饋線路
        out_x = start_x + (self.num_bits - 1) * gap + box_width
        out_y = start_y + box_height // 2
        
        fb_color = "#1f77b4" if feedback_active == 1 else "#b0b0b0"
        fb_width = 3 if feedback_active == 1 else 1.5
        
        self.canvas.create_line(out_x, out_y, out_x + 30, out_y, fill=fb_color, width=fb_width) 
        self.canvas.create_line(out_x + 20, out_y, out_x + 20, out_y + 65, fill=fb_color, width=fb_width) 
        self.canvas.create_line(out_x + 20, out_y + 65, start_x - 45, out_y + 65, fill=fb_color, width=fb_width) 
        self.canvas.create_line(start_x - 45, out_y + 65, start_x - 45, out_y, fill=fb_color, width=fb_width) 
        self.canvas.create_line(start_x - 45, out_y, start_x, out_y, arrow=tk.LAST, fill=fb_color, width=fb_width) 
        
        self.canvas.create_text(out_x + 50, out_y - 15, text="Output", fill="#d62728", font=("Helvetica", 11, "bold"))

        # 2. 依序繪製各個暫存器方塊 (從左到右為 X3, X2, X1, X0)
        for i in range(self.num_bits):
            x1 = start_x + i * gap
            y1 = start_y
            x2 = x1 + box_width
            y2 = y1 + box_height
            
            reg_name = f"X{self.num_bits - 1 - i}"
            
            self.canvas.create_rectangle(x1, y1, x2, y2, fill="#e1f5fe", outline="#0288d1", width=2)
            self.canvas.create_text((x1 + x2)//2, (y1 + y2)//2, text=str(self.state[i]), font=("Helvetica", 20, "bold"), fill="#212121")
            self.canvas.create_text((x1 + x2)//2, y1 - 15, text=reg_name, font=("Helvetica", 11, "bold"), fill="#555555")
            
            # 3. 處理暫存器之間的連接與 XOR 閘
            if i < self.num_bits - 1:
                next_x1 = start_x + (i + 1) * gap
                mid_y = y1 + box_height // 2
                
                if self.taps[i + 1]:
                    xor_x = x2 + (gap - box_width) // 2
                    
                    self.canvas.create_oval(xor_x - 11, mid_y - 11, xor_x + 11, mid_y + 11, fill="#fff3e0", outline="#ffb74d", width=2)
                    self.canvas.create_text(xor_x, mid_y, text="⊕", font=("Helvetica", 14, "bold"), fill="#f57c00")
                    
                    self.canvas.create_line(x2, mid_y, xor_x - 11, mid_y, fill="#666666", width=1.5)
                    self.canvas.create_line(xor_x + 11, mid_y, next_x1, mid_y, arrow=tk.LAST, fill="#666666", width=1.5)
                    self.canvas.create_line(xor_x, mid_y + 65, xor_x, mid_y + 11, arrow=tk.LAST, fill=fb_color, width=fb_width)
                else:
                    self.canvas.create_line(x2, mid_y, next_x1, mid_y, arrow=tk.LAST, fill="#666666", width=1.5)

    def step_lfsr(self):
        """執行一個 Clock 的標準 Galois 移位與 XOR 邏輯"""
        self.clock_count += 1
        
        output_bit = self.state[-1]
        self.output_history.append(str(output_bit))
        
        self.draw_lfsr(feedback_active=output_bit)
        
        next_state = [0] * self.num_bits
        next_state[0] = output_bit
        
        status_msg = f"時脈 {self.clock_count}: 輸出位元 = {output_bit}。 "
        if output_bit == 1:
            status_msg += "回饋為 1 → 抽頭處 (X₁ 輸入端) 將與 1 進行 XOR 反轉。"
        else:
            status_msg += "回饋為 0 → 抽頭處不受影響,資料正常右移。"

        for i in range(1, self.num_bits):
            if self.taps[i]:
                next_state[i] = self.state[i-1] ^ output_bit
            else:
                next_state[i] = self.state[i-1]
                
        self.state = next_state
        
        self.after(200, self.update_ui_text, status_msg)

    def update_ui_text(self, status_msg):
        self.lbl_clock.configure(text=f"目前時脈 (Clock): {self.clock_count}")
        self.lbl_status.configure(text=status_msg)
        self.lbl_output.configure(text=f"產生的偽隨機序列 (Output): {' '.join(self.output_history)}")
        self.draw_lfsr()

    def toggle_play(self):
        """切換自動播放 / 暫停"""
        if self.is_playing:
            self.is_playing = False
            self.btn_toggle.configure(text="自動播放", fg_color="#2ca02c", hover_color="#238223")
        else:
            self.is_playing = True
            self.btn_toggle.configure(text="暫停", fg_color="#d62728", hover_color="#b32021")
            self.auto_play()

    def auto_play(self):
        if self.is_playing:
            self.step_lfsr()
            self.after(1000, self.auto_play)

    def reset_lfsr(self):
        self.state = [1, 0, 1, 1]
        self.clock_count = 0
        self.is_playing = False
        self.output_history = []
        self.btn_toggle.configure(text="自動播放", fg_color="#2ca02c", hover_color="#238223")
        self.lbl_clock.configure(text="目前時脈 (Clock): 0")
        self.lbl_status.configure(text="已重設。請點擊單步執行或自動播放。", text_color="#1f77b4")
        self.lbl_output.configure(text="產生的偽隨機序列 (Output): ")
        self.draw_lfsr()

if __name__ == "__main__":
    app = GaloisLFSRVisualizer()
    app.mainloop()

2026年5月29日 星期五

IoT 智能控制面板 (Python Tkinter)

 IoT 智能控制面板  (Python Tkinter)

customtkinter

為了在 tkinter 中達到如同網頁般的圓角平滑陰影視覺效果,最頂級且優雅的作法是使用 CustomTkinter 庫。







安裝 Python MQTT 套件

 paho-mqtt   與  customtkinter


Python 與 Wokwi 的主題變數必須一字不差:

  • LED 控制:alex9ufo/home/led

  • 溫度:alex9ufo/home/temperature

  • 濕度:alex9ufo/home/humidity


Python程式

import customtkinter as ctk

import paho.mqtt.client as mqtt

import threading

import json


# --- 基礎視覺風格設定 ---

ctk.set_appearance_mode("Light")

ctk.set_default_color_theme("green")


# --- MQTT 參數設定(必須與 Wokwi 完全一致) ---

MQTT_BROKER = "broker.emqx.io"

MQTT_PORT = 1883

TOPIC_LED = "alex9ufo/home/led"

TOPIC_TEMP = "alex9ufo/home/temperature"

TOPIC_HUM = "alex9ufo/home/humidity"


class EnvironmentDashboard(ctk.CTk):

    def __init__(self):

        super().__init__()


        # 視窗設定

        self.title("IoT 智能控制面板 (Python MQTT 連動版)")

        self.geometry("540x420")

        self.configure(fg_color="#F5F5F7")


        # 建立 UI 介面

        self.setup_ui()


        # 啟動後台 MQTT 連線

        self.start_mqtt()


    def setup_ui(self):

        # 1. 頂部標題欄與狀態 (Header)

        self.header_frame = ctk.CTkFrame(self, fg_color="transparent")

        self.header_frame.pack(fill="x", padx=40, pady=(40, 20))


        self.title_label = ctk.CTkLabel(

            self.header_frame, text="環境主控台", 

            font=ctk.CTkFont(family="Arial", size=26, weight="bold"), text_color="#1D1D1F"

        )

        self.title_label.pack(side="left")


        self.status_frame = ctk.CTkFrame(self.header_frame, fg_color="transparent")

        self.status_frame.pack(side="right")


        self.status_dot = ctk.CTkLabel(self.status_frame, text="●", font=ctk.CTkFont(size=14), text_color="#FF3B30") # 預設紅色斷線

        self.status_dot.pack(side="left", padx=(0, 6))


        self.status_text = ctk.CTkLabel(self.status_frame, text="連線中...", font=ctk.CTkFont(family="Arial", size=14, weight="bold"), text_color="#86868B")

        self.status_text.pack(side="left")


        # 2. 數據卡片區域 (Grid Layout)

        self.grid_frame = ctk.CTkFrame(self, fg_color="transparent")

        self.grid_frame.pack(fill="x", padx=40, pady=10)

        self.grid_frame.grid_columnconfigure(0, weight=1)

        self.grid_frame.grid_columnconfigure(1, weight=1)


        # 🌡️ 溫度卡片

        self.temp_card = ctk.CTkFrame(self.grid_frame, fg_color="#FFFFFF", corner_radius=16)

        self.temp_card.grid(row=0, column=0, padx=(0, 10), sticky="nsew")

        self.temp_title = ctk.CTkLabel(self.temp_card, text="🌡️ 當前溫度", font=ctk.CTkFont(family="Arial", size=14, weight="bold"), text_color="#86868B")

        self.temp_title.pack(anchor="w", padx=20, pady=(20, 5))

        self.temp_value = ctk.CTkLabel(self.temp_card, text="--.- °C", font=ctk.CTkFont(family="Arial", size=28, weight="bold"), text_color="#1D1D1F")

        self.temp_value.pack(anchor="w", padx=20, pady=(0, 20))


        # 💧 濕度卡片

        self.hum_card = ctk.CTkFrame(self.grid_frame, fg_color="#FFFFFF", corner_radius=16)

        self.hum_card.grid(row=0, column=1, padx=(10, 0), sticky="nsew")

        self.hum_title = ctk.CTkLabel(self.hum_card, text="💧 當前濕度", font=ctk.CTkFont(family="Arial", size=14, weight="bold"), text_color="#86868B")

        self.hum_title.pack(anchor="w", padx=20, pady=(20, 5))

        self.hum_value = ctk.CTkLabel(self.hum_card, text="--.- %", font=ctk.CTkFont(family="Arial", size=28, weight="bold"), text_color="#1D1D1F")

        self.hum_value.pack(anchor="w", padx=20, pady=(0, 20))


        # 3. LED 控制卡片區域

        self.control_card = ctk.CTkFrame(self, fg_color="#FFFFFF", corner_radius=16)

        self.control_card.pack(fill="x", padx=40, pady=20)


        self.control_info_frame = ctk.CTkFrame(self.control_card, fg_color="transparent")

        self.control_info_frame.pack(side="left", padx=24, pady=20)


        self.control_title = ctk.CTkLabel(self.control_info_frame, text="遠端 LED 節點", font=ctk.CTkFont(family="Arial", size=18, weight="bold"), text_color="#1D1D1F")

        self.control_title.pack(anchor="w")


        self.control_status = ctk.CTkLabel(self.control_info_frame, text="目前狀態:關閉", font=ctk.CTkFont(family="Arial", size=13), text_color="#86868B")

        self.control_status.pack(anchor="w", pady=(2, 0))


        self.led_switch = ctk.CTkSwitch(

            self.control_card, text="", command=self.on_switch_toggle,

            onvalue="ON", offvalue="OFF", progress_color="#34C759"

        )

        self.led_switch.pack(side="right", padx=24)

        self.led_switch.configure(state="disabled") # 連線前禁用開關


    # --- MQTT 核心邏輯 ---

    def start_mqtt(self):

        # 初始化 paho-mqtt 客戶端

        self.client = mqtt.Client()

        self.client.on_connect = self.on_mqtt_connect

        self.client.on_message = self.on_mqtt_message

        self.client.on_disconnect = self.on_mqtt_disconnect


        # 使用獨立線程執行 MQTT 循環,避免造成 Tkinter 視窗凍結卡死

        mqtt_thread = threading.Thread(target=self.mqtt_loop_runner, daemon=True)

        mqtt_thread.start()


    def mqtt_loop_runner(self):

        try:

            self.client.connect(MQTT_BROKER, MQTT_PORT, 60)

            self.client.loop_forever() # 持續監聽

        except Exception as e:

            print(f"MQTT 連線失敗: {e}")


    def on_mqtt_connect(self, client, userdata, flags, rc):

        if rc == 0:

            print("Python 成功連線至 MQTT Broker!")

            # 切換 UI 狀態為已連線 (綠燈)

            self.status_dot.configure(text_color="#34C759")

            self.status_text.configure(text="已連線")

            self.led_switch.configure(state="normal")

            

            # 訂閱 Wokwi 的溫濕度主題

            self.client.subscribe(TOPIC_TEMP)

            self.client.subscribe(TOPIC_HUM)

        else:

            print(f"連線錯誤,錯誤碼: {rc}")


    def on_mqtt_disconnect(self, client, userdata, rc):

        print("與 MQTT Broker 斷開連線")

        self.status_dot.configure(text_color="#FF3B30")

        self.status_text.configure(text="已斷線")

        self.led_switch.configure(state="disabled")


    def on_mqtt_message(self, client, userdata, msg):

        topic = msg.topic

        payload = msg.payload.decode("utf-8")

        print(f"收到 Wokwi 數據 -> 主題: {topic} | 內容: {payload}")


        # 即時更新 Tkinter 介面上的數字

        if topic == TOPIC_TEMP:

            self.temp_value.configure(text=f"{float(payload):.1f} °C")

        elif topic == TOPIC_HUM:

            self.hum_value.configure(text=f"{float(payload):.1f} %")


    def on_switch_toggle(self):

        # 取得目前開關狀態 (ON / OFF)

        command = self.led_switch.get()

        self.control_status.configure(text=f"目前狀態:{'開啟' if command == 'ON' else '關閉'}")

        

        # 發送指令給 Wokwi

        self.client.publish(TOPIC_LED, command)

        print(f"已發送控制指令給 Wokwi -> {command}")


if __name__ == "__main__":

    app = EnvironmentDashboard()

    app.mainloop()




#include <WiFi.h>
#include <PubSubClient.h>
#include <DHTesp.h>  // 使用 DHTesp 函式庫



// --- 網路與 MQTT 伺服器設定 ---
const char* ssid = "Wokwi-GUEST"; // Wokwi 專用虛擬 WiFi
const char* password = "";
const char* mqtt_server = "broker.emqx.io";
const int mqtt_port = 1883;

// 主題 (Topic) 設定 - 建議加上個人化後綴避免與他人衝突
//const char* topic_led = "alex9ufo/esp32/led";
//const char* topic_temp = "alex9ufo/esp32/temp";
//const char* topic_hum = "alex9ufo/esp32/hum";


// 定義 MQTT 主題 (已更新為 alex9ufo 專屬代稱)
const char* topic_led = "alex9ufo/home/led";
const char* topic_temp = "alex9ufo/home/temperature";
const char* topic_hum = "alex9ufo/home/humidity";

#define LED_PIN 2
#define DHTPIN 15

DHTesp dht;          // 宣告 DHTesp 物件
WiFiClient espClient;
PubSubClient client(espClient);

unsigned long lastMsg = 0;

void setup_wifi() {
  delay(10);
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi connected");
}

// 接收來自 EasyBuilder Pro 的指令
void callback(char* topic, byte* payload, unsigned length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
 
  String message;
  for (int i = 0; i < length; i++) {
    message += (char)payload[i];
  }
  Serial.println(message);

  if (String(topic) == topic_led) {
    if (message == "ON") {
      digitalWrite(LED_PIN, HIGH);
    } else if (message == "OFF") {
      digitalWrite(LED_PIN, LOW);
    }
  }
}

void reconnect() {
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // 隨機生成 Client ID 避免與他人衝突導致頻繁斷線
    String clientId = "ESP32Client-" + String(random(0, 0xffff), HEX);
    if (client.connect(clientId.c_str())) {
      Serial.println("connected");
      client.subscribe(topic_led); // 訂閱 LED 控制主題
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      delay(5000);
    }
  }
}

void setup() {
  pinMode(LED_PIN, OUTPUT);
  Serial.begin(115200);
  setup_wifi();
  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(callback);
 
  // 修正:DHTesp 的初始化寫法
  dht.setup(DHTPIN, DHTesp::DHT22);
}

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

  // 每 2 秒發送一次溫濕度
  unsigned long now = millis();
  if (now - lastMsg > 2000) {
    lastMsg = now;
   
    // 修正:DHTesp 讀取溫濕度的寫法
    float h = dht.getHumidity();
    float t = dht.getTemperature();

    if (!isnan(h) && !isnan(t)) {
      client.publish(topic_temp, String(t, 1).c_str()); // 格式化為小數點後1位
      client.publish(topic_hum, String(h, 1).c_str());
     
      Serial.print("Temp: "); Serial.print(t, 1);
      Serial.print("°C | Hum: "); Serial.print(h, 1); Serial.println("%");
    } else {
      Serial.println("Failed to read from DHT sensor!");
    }
  }
}


IoT 智能控制面板

 IoT 智能控制面板





測試你的成果!

  1. 啟動 Wokwi 模擬: 點擊 Wokwi 的播放鍵,觀察序列埠(Serial Monitor),確認它成功連上 Wokwi-GUEST WiFi 並且顯示 嘗試 MQTT 連線...已連線!

  2. 開啟網頁: 在電腦上雙擊打開修改好的 index.html

  3. 測試控制: 點擊網頁上的 LED 開關,你會看見 Wokwi 視窗裡的 ESP32 藍色燈(或外接 LED)同步亮起或熄滅!

  4. 測試接收: 在 Wokwi 中點擊 DHT22 元件,手動拉動滑桿改變溫度或濕度,約 5 秒內,你網頁上原本寫死的 Figma 數字就會即時更新。

這個架構就是現代智慧家居(如 Home Assistant)或工業物聯網(IIoT)的核心基礎。

網頁程式碼 (index.html)

請將以下內容完整複製,儲存為 index.html,直接雙擊檔案用瀏覽器開啟即可:

<!DOCTYPE html>

<html lang="zh-TW">

<head>

    <meta charset="UTF-8">

    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>IoT 智能控制面板 (Figma 實作版)</title>

    <style>

        /* 模擬 Figma 的基礎畫布與字型設定 */

        :root {

            --bg-color: #F5F5F7;

            --card-bg: #FFFFFF;

            --text-main: #1D1D1F;

            --text-sub: #86868B;

            --primary-green: #34C759;

            --alert-red: #FF3B30;

        }


        body {

            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

            background-color: var(--bg-color);

            color: var(--text-main);

            margin: 0;

            padding: 40px 20px;

            display: flex;

            justify-content: center;

            align-items: center;

            min-height: 100vh;

            box-sizing: border-box;

        }


        /* 面板主外殼 (Figma Frame) */

        .dashboard {

            width: 100%;

            max-width: 480px;

            background: var(--card-bg);

            border-radius: 24px;

            padding: 32px;

            box-shadow: 0px 10px 30px rgba(0, 0, 0, 0.05);

        }


        /* 標題欄與連線狀態 */

        .header {

            display: flex;

            justify-content: space-between;

            align-items: center;

            margin-bottom: 32px;

        }


        .header h1 {

            font-size: 24px;

            font-weight: 600;

            margin: 0;

        }


        .status-badge {

            display: flex;

            align-items: center;

            font-size: 14px;

            color: var(--text-sub);

            font-weight: 500;

        }


        .status-dot {

            width: 8px;

            height: 8px;

            background-color: var(--alert-red);

            border-radius: 50%;

            margin-right: 8px;

            transition: background-color 0.3s ease;

        }


        .status-dot.connected {

            background-color: var(--primary-green);

        }


        /* 數據卡片佈局 (Grid) */

        .grid-container {

            display: grid;

            grid-template-columns: 1fr 1fr;

            gap: 16px;

            margin-bottom: 24px;

        }


        .card {

            background: var(--bg-color);

            border-radius: 16px;

            padding: 20px;

            display: flex;

            flex-direction: column;

        }


        .card-title {

            font-size: 14px;

            color: var(--text-sub);

            margin-bottom: 8px;

            font-weight: 500;

        }


        .card-value {

            font-size: 28px;

            font-weight: 700;

            margin: 0;

        }


        /* 控制卡片 (全寬) */

        .control-card {

            background: var(--bg-color);

            border-radius: 16px;

            padding: 24px;

            display: flex;

            justify-content: space-between;

            align-items: center;

        }


        .control-info h3 {

            margin: 0 0 4px 0;

            font-size: 18px;

            font-weight: 600;

        }


        .control-info p {

            margin: 0;

            font-size: 13px;

            color: var(--text-sub);

        }


        /* Figma 風格的 Toggle Switch 開關 */

        .switch {

            position: relative;

            display: inline-block;

            width: 60px;

            height: 34px;

        }


        .switch input {

            opacity: 0;

            width: 0;

            height: 0;

        }


        .slider {

            position: absolute;

            cursor: pointer;

            top: 0; left: 0; right: 0; bottom: 0;

            background-color: #D1D1D6;

            transition: .3s cubic-bezier(0.4, 0, 0.2, 1);

            border-radius: 34px;

        }


        .slider:before {

            position: absolute;

            content: "";

            height: 26px;

            width: 26px;

            left: 4px;

            bottom: 4px;

            background-color: white;

            transition: .3s cubic-bezier(0.4, 0, 0.2, 1);

            border-radius: 50%;

            box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.15);

        }


        input:checked + .slider {

            background-color: var(--primary-green);

        }


        input:checked + .slider:before {

            transform: translateX(26px);

        }

    </style>

</head>

<body>


    <div class="dashboard">

        <!-- 頂部狀態 -->

        <div class="header">

            <h1>環境主控台</h1>

            <div class="status-badge">

                <span id="status-dot" class="status-dot"></span>

                <span id="status-text">連線中...</span>

            </div>

        </div>


        <!-- DHT22 溫濕度顯示區 -->

        <div class="grid-container">

            <div class="card">

                <span class="card-title">🌡️ 當前溫度</span>

                <p id="temp-text" class="card-value">--.- °C</p>

            </div>

            <div class="card">

                <span class="card-title">💧 當前濕度</span>

                <p id="hum-text" class="card-value">--.- %</p>

            </div>

        </div>


        <!-- LED 控制區 -->

        <div class="control-card">

            <div class="control-info">

                <h3>遠端 LED 節點</h3>

                <p id="led-status-text">目前狀態:關閉</p>

            </div>

            <label class="switch">

                <input type="checkbox" id="led-toggle" disabled>

                <span class="slider"></span>

            </label>

        </div>

    </div>


    <!-- 核心:引入 MQTT.js 函式庫 -->

    <script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>

    <script>

        /*****************************************

         * 1. 參數設定 (已更新為 alex9ufo 專屬代稱)

         *****************************************/

        const MQTT_BROKER = 'wss://broker.emqx.io:8084/mqtt';

        const TOPIC_LED  = 'alex9ufo/home/led';

        const TOPIC_TEMP = 'alex9ufo/home/temperature';

        const TOPIC_HUM  = 'alex9ufo/home/humidity';


        // 取得 DOM 元件

        const statusDot = document.getElementById('status-dot');

        const statusText = document.getElementById('status-text');

        const tempText = document.getElementById('temp-text');

        const humText = document.getElementById('hum-text');

        const ledToggle = document.getElementById('led-toggle');

        const ledStatusText = document.getElementById('led-status-text');


        /*****************************************

         * 2. 初始化 MQTT 連線

         *****************************************/

        console.log('正在嘗試連線至 MQTT Broker...');

        const client = mqtt.connect(MQTT_BROKER);


        // 連線成功事件

        client.on('connect', () => {

            console.log('成功連線至 EMQX Broker!');

            statusDot.classList.add('connected');

            statusText.innerText = '已連線';

            ledToggle.disabled = false; // 開放按鈕操控


            // 訂閱來自 Wokwi ESP32 的數據主題

            client.subscribe([TOPIC_TEMP, TOPIC_HUM], (err) => {

                if (!err) {

                    console.log('已成功訂閱溫濕度主題');

                }

            });

        });


        // 連線斷開事件

        client.on('close', () => {

            console.log('與 Broker 連線斷開');

            statusDot.classList.remove('connected');

            statusText.innerText = '連線斷開';

            ledToggle.disabled = true;

        });


        /*****************************************

         * 3. 接收數據 (接收從 Wokwi 傳來的 DHT22 資料)

         *****************************************/

        client.on('message', (topic, message) => {

            const rawData = message.toString();

            console.log(`收到主題 [${topic}]: ${rawData}`);


            if (topic === TOPIC_TEMP) {

                // 更新溫度

                tempText.innerText = parseFloat(rawData).toFixed(1) + " °C";

            } else if (topic === TOPIC_HUM) {

                // 更新濕度

                humText.innerText = parseFloat(rawData).toFixed(1) + " %";

            }

        });


        /*****************************************

         * 4. 發送控制 (點擊網頁開關時傳送給 Wokwi)

         *****************************************/

        ledToggle.addEventListener('change', (event) => {

            const command = event.target.checked ? 'ON' : 'OFF';

            

            client.publish(TOPIC_LED, command, { qos: 1 }, (err) => {

                if (!err) {

                    console.log(`指令 [${command}] 已成功發送至主題: ${TOPIC_LED}`);

                    ledStatusText.innerText = `目前狀態:${command === 'ON' ? '開啟' : '關閉'}`;

                } else {

                    console.error('指令發送失敗:', err);

                }

            });

        });

    </script>

</body>

</html>




#include <WiFi.h>
#include <PubSubClient.h>
#include <DHTesp.h>  // 使用 DHTesp 函式庫



// --- 網路與 MQTT 伺服器設定 ---
const char* ssid = "Wokwi-GUEST"; // Wokwi 專用虛擬 WiFi
const char* password = "";
const char* mqtt_server = "broker.emqx.io";
const int mqtt_port = 1883;

// 主題 (Topic) 設定 - 建議加上個人化後綴避免與他人衝突
//const char* topic_led = "alex9ufo/esp32/led";
//const char* topic_temp = "alex9ufo/esp32/temp";
//const char* topic_hum = "alex9ufo/esp32/hum";


// 定義 MQTT 主題 (已更新為 alex9ufo 專屬代稱)
const char* topic_led = "alex9ufo/home/led";
const char* topic_temp = "alex9ufo/home/temperature";
const char* topic_hum = "alex9ufo/home/humidity";

#define LED_PIN 2
#define DHTPIN 15

DHTesp dht;          // 宣告 DHTesp 物件
WiFiClient espClient;
PubSubClient client(espClient);

unsigned long lastMsg = 0;

void setup_wifi() {
  delay(10);
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi connected");
}

// 接收來自 EasyBuilder Pro 的指令
void callback(char* topic, byte* payload, unsigned length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
 
  String message;
  for (int i = 0; i < length; i++) {
    message += (char)payload[i];
  }
  Serial.println(message);

  if (String(topic) == topic_led) {
    if (message == "ON") {
      digitalWrite(LED_PIN, HIGH);
    } else if (message == "OFF") {
      digitalWrite(LED_PIN, LOW);
    }
  }
}

void reconnect() {
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // 隨機生成 Client ID 避免與他人衝突導致頻繁斷線
    String clientId = "ESP32Client-" + String(random(0, 0xffff), HEX);
    if (client.connect(clientId.c_str())) {
      Serial.println("connected");
      client.subscribe(topic_led); // 訂閱 LED 控制主題
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      delay(5000);
    }
  }
}

void setup() {
  pinMode(LED_PIN, OUTPUT);
  Serial.begin(115200);
  setup_wifi();
  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(callback);
 
  // 修正:DHTesp 的初始化寫法
  dht.setup(DHTPIN, DHTesp::DHT22);
}

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

  // 每 2 秒發送一次溫濕度
  unsigned long now = millis();
  if (now - lastMsg > 2000) {
    lastMsg = now;
   
    // 修正:DHTesp 讀取溫濕度的寫法
    float h = dht.getHumidity();
    float t = dht.getTemperature();

    if (!isnan(h) && !isnan(t)) {
      client.publish(topic_temp, String(t, 1).c_str()); // 格式化為小數點後1位
      client.publish(topic_hum, String(h, 1).c_str());
     
      Serial.print("Temp: "); Serial.print(t, 1);
      Serial.print("°C | Hum: "); Serial.print(h, 1); Serial.println("%");
    } else {
      Serial.println("Failed to read from DHT sensor!");
    }
  }
}


程式碼是一個典型的 ESP32 物聯網(IoT)應用程式,運行於 Wokwi 模擬器環境中。它的核心功能是透過 Wi-Fi 連接到公共的 MQTT Broker (EMQX),將 DHT22 感測器量測到的溫度與濕度定時發送(Publish)出去,同時訂閱(Subscribe)控制主題,接收來自網頁端(Figma UI)的指令來切換 LED 的開關。

由於你傳送的文字中,程式碼重複貼了兩次,以下我將針對這份結構完整的程式碼進行逐行詳細解說

📑 1. 引入函式庫與定義全域變數

C++
#include <WiFi.h>
#include <PubSubClient.h>
#include <DHTesp.h>  // 使用 DHTesp 函式庫
  • #include <WiFi.h>:引入 ESP32 的 Wi-Fi 核心函式庫,負責處理無線網路連線。

  • #include <PubSubClient.h>:引入 MQTT 協定的函式庫,讓 ESP32 具備發布(Publish)與訂閱(Subscribe)訊息的功能。

  • #include <DHTesp.h>:引入專為 ESP32 優化的 DHT 溫濕度感測器函式庫。

C++
// --- 網路與 MQTT 伺服器設定 ---
const char* ssid = "Wokwi-GUEST"; // Wokwi 專用虛擬 WiFi
const char* password = "";
const char* mqtt_server = "broker.emqx.io";
const int mqtt_port = 1883;
  • ssid / password:設定 Wi-Fi 的帳號與密碼。Wokwi-GUEST 是 Wokwi 模擬環境提供的專用虛擬基地台,不需密碼。

  • mqtt_server:設定 MQTT 伺服器(Broker)的網址,這裡使用的是 EMQX 提供的免費公共測試伺服器。

  • mqtt_port:設定 MQTT 的通訊埠,標準非加密的 MQTT 服務埠號為 1883

C++
// 定義 MQTT 主題 (已更新為 alex9ufo 專屬代稱)
const char* topic_led = "alex9ufo/home/led";
const char* topic_temp = "alex9ufo/home/temperature";
const char* topic_hum = "alex9ufo/home/humidity";
  • 這三行定義了通訊的「頻道名稱」(Topic)。網頁前端與硬體端必須對應相同的名稱才能互通:

    • topic_led:接收網頁端傳來控制 LED 開關的頻道。

    • topic_temp:發送溫度數據的頻道。

    • topic_hum:發送濕度數據的頻道。

C++
#define LED_PIN 2
#define DHTPIN 15
  • 使用巨集定義硬體腳位。將 ESP32 的 GPIO 2 定義為 LED 控制腳(通常也是內建藍色 LED 腳位),將 GPIO 15 定義為 DHT22 的數據訊號腳。

C++
DHTesp dht;          // 宣告 DHTesp 物件
WiFiClient espClient;
PubSubClient client(espClient);
unsigned long lastMsg = 0;
  • DHTesp dht;:實例化一個溫濕度感測器物件。

  • WiFiClient espClient;:建立一個網路用戶端物件,負責底層的 TCP 連線。

  • PubSubClient client(espClient);:建立 MQTT 用戶端物件,並將剛才的 Wi-Fi 連線傳入,讓 MQTT 透過 Wi-Fi 傳輸資料。

  • lastMsg:宣告一個無號長整型變數,用來記錄上一次發送資料的時間(單位為毫秒),實現非阻塞式的定時器。

🌐 2. Wi-Fi 連線初始化函式

C++
void setup_wifi() {
  delay(10);
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi connected");
}
  • WiFi.begin(ssid, password);:啟動 ESP32 的 Wi-Fi 晶片並嘗試連線到指定的基地台。

  • while (WiFi.status() != WL_CONNECTED):使用迴圈檢查連線狀態。如果尚未連線成功,程式會每隔 0.5 秒(delay(500))在序列埠監控視窗印出一個點(.),直到成功連上為止。

📩 3. MQTT 訊息接收回呼函式(Callback)

當網頁端(Figma UI)發送指令到 ESP32 訂閱的 alex9ufo/home/led 主題時,MQTT 伺服器會將訊息推播過來,自動觸發此函式:

C++
void callback(char* topic, byte* payload, unsigned length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  
  String message;
  for (int i = 0; i < length; i++) {
    message += (char)payload[i];
  }
  Serial.println(message);
  • 參數說明topic 是接收到訊息的主題名稱;payload 是傳過來的原始資料(位元組陣列);length 是資料長度。

  • for (int i = 0; i < length; i++):因為傳進來的資料是 byte 陣列,所以透過迴圈逐一將字元(char)累加到字串變數 message 中,轉換成看得懂的文字(例如 "ON" 或 "OFF")。

C++
  if (String(topic) == topic_led) {
    if (message == "ON") {
      digitalWrite(LED_PIN, HIGH);
    } else if (message == "OFF") {
      digitalWrite(LED_PIN, LOW);
    }
  }
}
  • if (String(topic) == topic_led):確認收到的訊息確實是來自控制 LED 的頻道。

  • 控制邏輯:如果文字內容為 "ON",執行 digitalWrite(LED_PIN, HIGH) 將 GPIO 2 輸出高電位,點亮 LED;若為 "OFF",則輸出低電位,熄滅 LED。

🔄 4. MQTT 斷線重連函式

C++
void reconnect() {
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // 隨機生成 Client ID 避免與他人衝突導致頻繁斷線
    String clientId = "ESP32Client-" + String(random(0, 0xffff), HEX);
  • while (!client.connected()):如果與 MQTT 伺服器斷開連線,就會進入這個迴圈嘗試重連。

  • clientId:公共 MQTT 伺服器規定每個客戶端的 Client ID 必須唯一。這裡利用 random(0, 0xffff) 產生隨機數並轉成十六進位字串,避免你的 ESP32 與其他人的裝置撞名而被剔除連線。

C++
    if (client.connect(clientId.c_str())) {
      Serial.println("connected");
      client.subscribe(topic_led); // 訂閱 LED 控制主題
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      delay(5000);
    }
  }
}
  • client.connect(...):嘗試向 MQTT 伺服器註冊並連線。

  • 連線成功:印出 connected,並立刻執行 client.subscribe(topic_led),告訴伺服器「我要監聽 LED 控制頻道,有新訊息請發給我」。

  • 連線失敗:印出錯誤代碼(rc),並等待 5 秒後重新進入迴圈嘗試。

⚙️ 5. 硬體與初始化設定(Setup)

C++
void setup() {
  pinMode(LED_PIN, OUTPUT);
  Serial.begin(115200);
  setup_wifi();
  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(callback);
  
  // 修正:DHTesp 的初始化寫法
  dht.setup(DHTPIN, DHTesp::DHT22); 
}
  • pinMode(LED_PIN, OUTPUT);:將 GPIO 2 設定為輸出模式。

  • Serial.begin(115200);:初始化序列埠通訊,波特率設定為 115200,用於在電腦畫面上印出除錯訊息。

  • setup_wifi();:呼叫前面寫好的函式連接無線網路。

  • client.setServer(...):綁定 MQTT 伺服器位址與通訊埠。

  • client.setCallback(callback);:向 MQTT 庫註冊回呼函式,指定當收到訊息時,由 callback 函式來處理。

  • dht.setup(...):這是 DHTesp 函式庫特有的初始化方式,指定控制腳位為 15,感測器型態為 DHT22

🔄 6. 主程式循環(Loop)

C++
void loop() {
  if (!client.connected()) {
    reconnect();
  }
  client.loop();
  • if (!client.connected()):每次循環都檢查 MQTT 連線狀態,斷線就呼叫 reconnect()

  • client.loop();:這是 MQTT 庫的核心函式,必須在 loop() 裡頻繁執行。它負責維持與伺服器的連線心跳(KeepAlive),並檢查是否有訂閱的訊息送達。

C++
  // 每 2 秒發送一次溫濕度
  unsigned long now = millis();
  if (now - lastMsg > 2000) {
    lastMsg = now;
    
    // 修正:DHTesp 讀取溫濕度的寫法
    float h = dht.getHumidity();
    float t = dht.getTemperature();
  • 時間控制millis() 會回傳系統開機至今的毫秒數。利用 now - lastMsg > 2000 來判斷是否過去了 2 秒。這種做法比使用 delay(2000) 更好,因為它不會讓晶片暫停運作,確保 MQTT 訊息接收不會卡頓。

  • 讀取數據:呼叫 dht.getHumidity() 取得濕度,dht.getTemperature() 取得溫度。

C++
    if (!isnan(h) && !isnan(t)) {
      client.publish(topic_temp, String(t, 1).c_str()); // 格式化為小數點後1位
      client.publish(topic_hum, String(h, 1).c_str());
      
      Serial.print("Temp: "); Serial.print(t, 1);
      Serial.print("°C | Hum: "); Serial.print(h, 1); Serial.println("%");
    } else {
      Serial.println("Failed to read from DHT sensor!");
    }
  }
}
  • !isnan(h) && !isnan(t)isnan 代表 "Is Not a Number"。此處用來確認讀取出來的溫濕度數值是有效的,而非空值或錯誤訊號。

  • client.publish(...):將數值轉成字串(String(t, 1) 代表保留小數點後 1 位),然後轉成 C 語言格式字串(.c_str())發布到對應的 MQTT 頻道(topic_temp / topic_hum),網頁端便能即時接收並顯示。

  • 異常處理:若讀取失敗,則在序列埠印出錯誤訊息。

Galois LFSR

  在密碼學與數位訊號處理中, LFSR(線性回饋移位暫存器,Linear Feedback Shift Register) 是用來產生偽隨機序列(也就是串流加密中所需的金鑰流)最核心的硬體架構。 LFSR 主要分為兩種實現架構: Fibonacci(斐波那契) 與 Galo...