2026年1月22日 星期四

經由MQTT協定的2個WOKWI ESP32 雙向通訊 (ESP32 to ESP32 MQTT Communication )

 經由MQTT協定的2個WOKWI ESP32 雙向通訊 (ESP32  to ESP32 MQTT Communication )



使用兩個 ESP32 建立一個遠端控制系統。

MQTT Broker: mqtt-dashboard.com
Topic (主題): alex9ufo/switchPB

訊息內容: on, off, flash, timer

發送: 按鈕去彈跳並循環發送 on/off/flash/timeralex9ufo/switchPB
接收: 訂閱 alex9ufo/DHT22,解析 JSON 格式並顯示於 LCD。
執行: 訂閱 alex9ufo/switchPB 控制 LED 動作。
發送: 每 2 秒讀取 DHT22 數據並發行 JSON 到 alex9ufo/DHT22

引腳連接提醒:

  • Button: GPIO 34 (外部下拉電阻)。

  • LCD: SDA=21, SCL=22。

  • LED: GPIO 2。

  • DHT22: GPIO 15。




發送端:按鈕 + LCD (含序列埠監控)

#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <ArduinoJson.h>

const int buttonPin = 34;
LiquidCrystal_I2C lcd(0x27, 16, 2);
const char* mqtt_server = "mqtt-dashboard.com";

WiFiClient espClient;
PubSubClient client(espClient);

volatile int stateCounter = 0;
volatile bool sendFlag = false;
float sharedTemp = 0, sharedHum = 0;
volatile bool dataUpdated = false;

TaskHandle_t Task0;

void Task0code(void * pvParameters) {
  for (;;) {
    if (WiFi.status() != WL_CONNECTED) {
      WiFi.begin("Wokwi-GUEST", "");
      while (WiFi.status() != WL_CONNECTED) delay(500);
      Serial.println("[Core 0] WiFi Reconnected");
    }
    if (!client.connected()) {
      if (client.connect("ESP32_Button_Core0")) {
        client.subscribe("alex9ufo/DHT22");
        Serial.println("[Core 0] MQTT Connected & Subscribed to DHT22");
      }
    }
    if (sendFlag) {
      const char* cmds[] = {"on", "off", "flash", "timer"};
      const char* msg = cmds[stateCounter - 1];
      client.publish("alex9ufo/switchPB", msg);
      // 新增:顯示發行的訊息
      Serial.printf("[Core 0] ---> Published to switchPB: %s\n", msg);
      sendFlag = false;
    }
    client.loop();
    delay(10);
  }
}

void callback(char* topic, byte* payload, unsigned int length) {
  StaticJsonDocument<200> doc;
  deserializeJson(doc, payload, length);
  sharedTemp = doc["temp"];
  sharedHum = doc["hum"];
  dataUpdated = true;
  // 新增:顯示收到的訂閱訊息
  Serial.printf("[Core 0] <--- Received DHT22: Temp=%.1f, Hum=%.1f\n", sharedTemp, sharedHum);
}

void setup() {
  Serial.begin(115200);
  pinMode(buttonPin, INPUT);
  lcd.init();
  lcd.backlight();
  client.setServer(mqtt_server, 1883);
  client.setCallback(callback);
  xTaskCreatePinnedToCore(Task0code, "Task0", 10000, NULL, 1, &Task0, 0);
  Serial.println("System Initialized (Sender)");
}

void loop() {
  static int lastButtonState = LOW;
  int reading = digitalRead(buttonPin);
  if (reading == HIGH && lastButtonState == LOW) {
    delay(50);
    stateCounter = (stateCounter % 4) + 1;
    sendFlag = true;
    Serial.printf("[Core 1] Button Pressed. State: %d\n", stateCounter);
  }
  lastButtonState = reading;

  if (dataUpdated) {
    lcd.setCursor(0, 0);
    lcd.printf("Temp: %.1f C  ", sharedTemp);
    lcd.setCursor(0, 1);
    lcd.printf("Hum : %.1f %%  ", sharedHum);
    dataUpdated = false;
  }
  delay(10);
}




接收端:LED + DHT22 (含序列埠監控)

#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>
#include <ArduinoJson.h>

#define DHTPIN 15
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);

const int ledPin = 2;
const char* mqtt_server = "mqtt-dashboard.com";

WiFiClient espClient;
PubSubClient client(espClient);

volatile int currentModeCode = 0;
unsigned long myTimerStart = 0;
float t_val = 0, h_val = 0;

TaskHandle_t Task0_Comm;

void Task0_CommCode(void * pvParameters) {
  for (;;) {
    if (!client.connected()) {
      if (client.connect("ESP32_LED_Core0")) {
        client.subscribe("alex9ufo/switchPB");
        Serial.println("[Core 0] MQTT Connected & Subscribed to switchPB");
      }
    }
    static unsigned long lastUpdate = 0;
    if (millis() - lastUpdate > 5000) { // 每5秒發行一次
      StaticJsonDocument<200> doc;
      doc["temp"] = t_val;
      doc["hum"] = h_val;
      char buffer[128];
      serializeJson(doc, buffer);
      client.publish("alex9ufo/DHT22", buffer);
      // 新增:顯示發行的訊息
      Serial.printf("[Core 0] ---> Published DHT Data: %s\n", buffer);
      lastUpdate = millis();
    }
    client.loop();
    delay(10);
  }
}

void callback(char* topic, byte* payload, unsigned int length) {
  String msg = "";
  for (int i = 0; i < length; i++) msg += (char)payload[i];
 
  // 新增:顯示收到的訂閱訊息
  Serial.printf("[Core 0] <--- Received switchPB: %s\n", msg.c_str());

  if (msg == "off") currentModeCode = 0;
  else if (msg == "on") currentModeCode = 1;
  else if (msg == "flash") currentModeCode = 2;
  else if (msg == "timer") {
    currentModeCode = 3;
    myTimerStart = millis();
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(ledPin, OUTPUT);
  dht.begin();
  WiFi.begin("Wokwi-GUEST", "");
  while (WiFi.status() != WL_CONNECTED) delay(500);
  client.setServer(mqtt_server, 1883);
  client.setCallback(callback);
  xTaskCreatePinnedToCore(Task0_CommCode, "CommTask", 10000, NULL, 1, &Task0_Comm, 0);
  Serial.println("System Initialized (Receiver)");
}

void loop() {
  t_val = dht.readTemperature();
  h_val = dht.readHumidity();

  // 根據 currentModeCode 執行 LED 動作
  if (currentModeCode == 1) digitalWrite(ledPin, HIGH);
  else if (currentModeCode == 0) digitalWrite(ledPin, LOW);
  else if (currentModeCode == 2) digitalWrite(ledPin, (millis() / 500) % 2);
  else if (currentModeCode == 3) {
    if (millis() - myTimerStart < 5000) digitalWrite(ledPin, HIGH);
    else {
      digitalWrite(ledPin, LOW);
      currentModeCode = 0;
      Serial.println("[Core 1] Timer Finished. LED Off.");
    }
  }
  delay(100);
}

2026年1月21日 星期三

BPSK (Binary Phase Shift Keying) 調變與 NRZ-L 編碼。

BPSK (Binary Phase Shift Keying) 調變與 NRZ-L 編碼 

ISO 14443-B 標準中,從標籤(PICC)到讀取器(PCD)的反向傳輸使用 847 kHz 副載波 (Subcarrier),並採用 BPSK (Binary Phase Shift Keying) 調變與 NRZ-L 編碼。

核心原理

  1. NRZ-L 編碼:邏輯 '1' 與 '0' 分別對應不同的電平(通常在調變中對應不同的相位)。

  2. BPSK 調變

    • 當數據為 '0' 時,副載波相位為 0

    • 當數據為 '1' 時,副載波相位翻轉為 180 (相位切換)。

  3. 847 kHz 副載波:這是由 13.56  MHz / 16 得到的頻率。

  • BPSK 理輯實作

    • 我們使用相位偏移來實作調變:np.cos(2 * np.pi * f * t + phase)

    • 當數據是 0 時,phase = 0

    • 當數據是 1 時,phase = π (180°)

    • 在波形圖中,你會發現在 Bit 切換(例如從 0 變 1)的地方,正弦波的連續性會被打斷,產生一個明顯的「尖點」或翻轉,這就是相位調變的特徵。

  • 頻率與速率

    • 副載波頻率固定為 847.5 kHz

    • 位元速率設定為 106 kbps(這是 ISO 14443-B 最基礎的速率)。這意味著每個位元週期內包含約 8 個副載波週期 (847.5 / 106 ≈  8 )。

  • 在實際的 ISO 14443-B 物理層中,847 kHz 的副載波並不是獨立存在的,它是透過「負載調變」(Load Modulation)掛載在 13.56 MHz 的載波 之上。

    當標籤(PICC)切換其內部負載時,13.56 MHz 載波的振幅會隨著 847 kHz 的頻率產生微小的變化。這在頻譜上看起來像是距離中心頻率   847  kHz  的邊帶(Sidebands)。

  • 第一層 (NRZ Data):原始數位資料(0 與 1)。

  • 第二層 (BPSK Subcarrier):847 kHz 的副載波,根據資料切換相位。

  • 第三層 (Load Modulation):將 BPSK 訊號調變至 13.56 MHz 載波上。



  • import tkinter as tk

    from tkinter import messagebox

    import numpy as np

    import matplotlib.pyplot as plt

    from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg


    class ISO14443B_Full_Simulator:

        def __init__(self, root):

            self.root = root

            self.root.title("ISO 14443-B 完整負載調變模擬器")

            

            # UI 佈局

            control_frame = tk.Frame(root)

            control_frame.pack(side=tk.TOP, fill=tk.X, padx=10, pady=10)


            tk.Label(control_frame, text="Bit Stream (PICC to PCD):").pack(side=tk.LEFT)

            self.bit_entry = tk.Entry(control_frame, width=20)

            self.bit_entry.insert(0, "1011")

            self.bit_entry.pack(side=tk.LEFT, padx=5)


            self.btn_plot = tk.Button(control_frame, text="生成三級波形", command=self.plot_all, bg="#fff3cd")

            self.btn_plot.pack(side=tk.LEFT, padx=5)


            # 建立三個子圖

            self.fig, (self.ax1, self.ax2, self.ax3) = plt.subplots(3, 1, figsize=(9, 8), sharex=True)

            self.canvas = FigureCanvasTkAgg(self.fig, master=root)

            self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)


        def plot_all(self):

            bits_str = self.bit_entry.get().strip()

            if not all(b in '01' for b in bits_str) or not bits_str:

                messagebox.showerror("錯誤", "請輸入二進位位元流")

                return


            bits = [int(b) for b in bits_str]

            

            # 為了觀察方便,我們縮小頻率比例但保持物理意義

            # 實際: 13.56MHz / 847kHz = 16 倍關係

            f_sub = 847500      # 847.5 kHz

            f_carrier = f_sub * 8  # 模擬載波 (簡化為8倍以利肉眼觀察波形)

            bit_rate = 106000   # 106 kbps

            

            fs = 2000 # 取樣率

            t = np.linspace(0, len(bits), len(bits) * fs)

            

            # 1. NRZ 訊號

            nrz_signal = np.repeat(bits, fs)


            # 2. BPSK 副載波 (847 kHz)

            phases = np.repeat([0 if b == 0 else np.pi for b in bits], fs)

            subcarrier = np.cos(2 * np.pi * (f_sub/bit_rate) * t + phases)


            # 3. 負載調變 (13.56 MHz 載波 + 副載波調變)

            # 調變深度通常很淺 (例如 10%)

            mod_depth = 0.15 

            carrier_1356 = np.sin(2 * np.pi * (f_carrier/bit_rate) * t)

            # 負載調變公式:Carrier * (1 + depth * subcarrier)

            load_mod_signal = carrier_1356 * (1 + mod_depth * subcarrier)


            # --- 繪圖 ---

            for ax in [self.ax1, self.ax2, self.ax3]: ax.clear()


            # 波形 1: NRZ

            self.ax1.step(t, nrz_signal, where='post', color='red')

            self.ax1.set_title("1. PICC Data (NRZ-L)")

            self.ax1.set_ylim(-0.2, 1.2)


            # 波形 2: BPSK Subcarrier

            self.ax2.plot(t, subcarrier, color='blue', linewidth=0.8)

            self.ax2.set_title("2. BPSK Subcarrier (847.5 kHz)")

            self.ax2.legend(["Phase 0°/180°"], loc='lower right', fontsize='x-small')


            # 波形 3: 13.56MHz Carrier with Load Modulation

            self.ax3.plot(t, load_mod_signal, color='green', linewidth=0.5)

            self.ax3.set_title("3. Load Modulation (13.56 MHz Carrier)")

            self.ax3.set_xlabel("Bit Index")

            self.ax3.legend(["ASK-modulated Carrier"], loc='lower right', fontsize='x-small')


            self.fig.tight_layout()

            self.canvas.draw()


    if __name__ == "__main__":

        root = tk.Tk()

        root.geometry("1000x850")

        app = ISO14443B_Full_Simulator(root)

        app.plot_all()

        root.mainloop()


    IOS14443B 讀取器至電子標籤 調變及編碼方式

     IOS14443B  讀取器至電子標籤 調變及編碼方式

    根據 ISO 14443-B 標準,ASK 10% 的調變指數實務上通常落在 8% 到 14% 之間。

    核心原理解析

    1. NRZ 編碼 (Non-Return-to-Zero):逻辑 '1' 代表高電位,逻辑 '0' 代表低電位。在位元持續時間內,電平保持不變。

    2. ASK 10% 調變:載波(Carrier)在邏輯低位時不會歸零,而是下降到最大振幅的約 90%。

      • 計算公式:m =  {A - B} / {A + B},其中 A  是最大振幅, B 是最小振幅。

      • m = 10%,則 B  ≈ 0.818  *   A




    Python 實作程式碼

    import tkinter as tk

    from tkinter import messagebox

    import numpy as np

    import matplotlib.pyplot as plt

    from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg


    class ISO14443BSimulator:

        def __init__(self, root):

            self.root = root

            self.root.title("ISO 14443-B ASK 10% 模擬器")

            

            # --- UI 介面佈局 ---

            control_frame = tk.Frame(root)

            control_frame.pack(side=tk.TOP, fill=tk.X, padx=10, pady=10)


            tk.Label(control_frame, text="二進位位元流 (Bit Stream):").pack(side=tk.LEFT)

            self.bit_entry = tk.Entry(control_frame, width=25)

            self.bit_entry.insert(0, "101101")

            self.bit_entry.pack(side=tk.LEFT, padx=5)


            self.btn_plot = tk.Button(control_frame, text="繪製波形", command=self.plot_waveform, bg="#e1e1e1")

            self.btn_plot.pack(side=tk.LEFT, padx=5)


            # Matplotlib 畫布設定

            # tight_layout 確保標籤不會被切到

            self.fig, self.ax = plt.subplots(figsize=(8, 4.5), dpi=100)

            self.canvas = FigureCanvasTkAgg(self.fig, master=root)

            self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)


        def plot_waveform(self):

            bits_str = self.bit_entry.get().strip()

            

            # 檢查輸入是否合法

            if not bits_str or not all(b in '01' for b in bits_str):

                messagebox.showerror("錯誤", "請輸入有效的二進位字串 (僅限 0 與 1)")

                return


            bits = [int(b) for b in bits_str]

            

            # 模擬參數

            fs = 500  # 每個 bit 的取樣點數

            carrier_freq = 10  # 載波頻率

            total_points = len(bits) * fs

            t = np.linspace(0, len(bits), total_points)

            

            # 1. 生成 NRZ 信號 (用於包絡線)

            # ISO 14443-B: 1 = 高振幅 (A), 0 = 低振幅 (B)

            # 若指數為 10%, 則 B ≈ 0.82 * A

            A = 1.0

            B = 0.818 

            envelope = np.repeat([A if b == 1 else B for b in bits], fs)

            

            # 2. 生成載波並進行 ASK 調變

            carrier = np.sin(2 * np.pi * carrier_freq * t)

            ask_signal = envelope * carrier


            # --- 開始繪圖 ---

            self.ax.clear()

            

            # 繪製 ASK 調變波形 (藍色實線)

            self.ax.plot(t, ask_signal, color='#1f77b4', label='ASK 10% (ISO 14443-B)', linewidth=1)

            

            # 繪製原始 NRZ 數據參考線 (紅色虛線)

            # 為了美觀,將 0/1 映射到對應的振幅高度

            nrz_display = np.repeat([A if b == 1 else B for b in bits], fs)

            self.ax.plot(t, nrz_display, color='red', linestyle='--', alpha=0.7, label='NRZ Data')


            # 設定圖表格式

            self.ax.set_ylim(-1.3, 1.5)

            self.ax.set_title(f"ISO 14443-B Modulation Simulation", fontsize=12)

            self.ax.set_xlabel("Bit Period")

            self.ax.set_ylabel("Amplitude")

            self.ax.grid(True, which='both', linestyle=':', alpha=0.6)


            # --- 關鍵修改:將圖例移至右下角 ---

            # loc='lower right' 指定位置

            # frameon=True 加上外框以利辨識

            self.ax.legend(loc='lower right', frameon=True, shadow=True, fontsize='small')

            

            self.fig.tight_layout()

            self.canvas.draw()


    if __name__ == "__main__":

        root = tk.Tk()

        # 設定視窗初始大小

        root.geometry("850x550")

        app = ISO14443BSimulator(root)

        # 啟動時先畫一次預設值

        app.plot_waveform()

        root.mainloop()


    Python 控制 Wokwi ESP32 LED 跑馬燈動畫

     Python 控制 Wokwi ESP32  LED 跑馬燈動畫

    在 ESP32 上利用 FreeRTOS 的雙核特性,我們可以將 Core 0 專門用於處理 MQTT 通訊與 WiFi 維護,而 Core 1 則專門負責 LED 跑馬燈動畫。


  • Core 0 (通訊核心)

    • 負責 client.loop()。這非常重要,因為 WiFi 協議疊(Protocol Stack)預設在 Core 0 運行。這樣可以減少核心間的上下文切換開關(Context Switch)。

    • 使用 vTaskDelay 而不是 delay(),讓出 CPU 時間給系統背景進程(如 WiFi 管理)。

  • Core 1 (應用核心)

    • 負責 LED 的跑馬燈計算與輸出。即便動畫中有 vTaskDelay,也不會卡死 MQTT 的接收。

    • 使用 volatile int currentMode 確保當 Core 0 修改數值時,Core 1 能立刻讀取到最新的指令。

  • #include <WiFi.h>
    #include <PubSubClient.h>

    // --- 設定區 ---
    const char* ssid = "Wokwi-GUEST";
    const char* password = "";
    const char* mqtt_server = "mqtt-dashboard.com";
    const char* mqtt_topic = "alex9ufo/LEDcommand";

    // LED 接腳
    int ledPins[] = {19, 18, 5, 4, 2, 27, 26, 25, 33, 32};
    int numLeds = 10;

    // 共享變數 (使用 volatile 確保跨核心讀取正確)
    volatile int currentMode = 0;

    WiFiClient espClient;
    PubSubClient client(espClient);

    // 任務句柄
    TaskHandle_t LEDTask;

    // --- MQTT 接收回調 (執行於 Core 0) ---
    void callback(char* topic, byte* payload, unsigned int length) {
      char message[length + 1];
      memcpy(message, payload, length);
      message[length] = '\0';
     
      int newMode = atoi(message);
      currentMode = newMode; // 更新共享變數
      Serial.printf("Core %d 收到指令: %d\n", xPortGetCoreID(), currentMode);
    }

    // --- Core 0 任務:處理 MQTT 通訊 ---
    void MQTTTaskCode(void * pvParameters) {
      Serial.printf("MQTT 任務運行於 Core %d\n", xPortGetCoreID());
     
      for (;;) {
        if (!client.connected()) {
          String clientId = "ESP32-Client-" + String(random(0xffff), HEX);
          if (client.connect(clientId.c_str())) {
            client.subscribe(mqtt_topic);
          }
        }
        client.loop();
        vTaskDelay(10 / portTICK_PERIOD_MS); // 稍微休息,釋放資源給系統
      }
    }

    // --- Core 1 任務:處理 LED 動畫 (原本的 Loop 內容) ---
    void LEDTaskCode(void * pvParameters) {
      Serial.printf("LED 任務運行於 Core %d\n", xPortGetCoreID());
     
      static int step = 0;
      static bool direction = true;
      static unsigned long lastToggle = 0;
      static bool flashState = false;

      for (;;) {
        // 0.5s 閃爍狀態計算
        if (millis() - lastToggle >= 500) {
          lastToggle = millis();
          flashState = !flashState;
        }

        // 根據模式執行動畫
        switch (currentMode) {
          case 1: // 上到下
            for (int i=0; i<numLeds; i++) {
              for(int j=0; j<numLeds; j++) digitalWrite(ledPins[j], i==j);
              vTaskDelay(100 / portTICK_PERIOD_MS);
              if(currentMode != 1) break;
            }
            break;
          case 2: // 下到上
            for (int i=numLeds-1; i>=0; i--) {
              for(int j=0; j<numLeds; j++) digitalWrite(ledPins[j], i==j);
              vTaskDelay(100 / portTICK_PERIOD_MS);
              if(currentMode != 2) break;
            }
            break;
          case 3: // 彈跳
            for(int j=0; j<numLeds; j++) digitalWrite(ledPins[j], step==j);
            if (direction) step++; else step--;
            if (step >= numLeds - 1 || step <= 0) direction = !direction;
            vTaskDelay(80 / portTICK_PERIOD_MS);
            break;
          case 4: // 奇數閃爍
            for (int i=0; i<numLeds; i++) digitalWrite(ledPins[i], (flashState && (i%2==0)));
            vTaskDelay(50 / portTICK_PERIOD_MS);
            break;
          case 5: // 偶數閃爍
            for (int i=0; i<numLeds; i++) digitalWrite(ledPins[i], (flashState && (i%2!=0)));
            vTaskDelay(50 / portTICK_PERIOD_MS);
            break;
          case 6: // 奇偶交替
            for (int i=0; i<numLeds; i++) {
              if (flashState) digitalWrite(ledPins[i], i%2==0);
              else digitalWrite(ledPins[i], i%2!=0);
            }
            vTaskDelay(50 / portTICK_PERIOD_MS);
            break;
          case 0: // 全滅
            for (int i=0; i<numLeds; i++) digitalWrite(ledPins[i], LOW);
            vTaskDelay(200 / portTICK_PERIOD_MS);
            break;
        }
      }
    }

    void setup() {
      Serial.begin(115200);
      for (int i = 0; i < numLeds; i++) pinMode(ledPins[i], OUTPUT);

      // WiFi 連線
      WiFi.begin(ssid, password);
      while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
      Serial.println("\nWiFi Connected");

      client.setServer(mqtt_server, 1883);
      client.setCallback(callback);

      // 分配任務到不同的核心
      // xTaskCreatePinnedToCore(函式, 任務名稱, 堆疊大小, 參數, 優先度, 句柄, 核心ID)
     
      xTaskCreatePinnedToCore(
        MQTTTaskCode, "MQTT_Task", 10000, NULL, 1, NULL, 0  // Core 0 負責通訊
      );

      xTaskCreatePinnedToCore(
        LEDTaskCode, "LED_Task", 10000, NULL, 1, &LEDTask, 1 // Core 1 負責動畫
      );
    }

    void loop() {
      // 雙核心架構下,Arduino 的 loop 可以留空或處理次要事務
      vTaskDelete(NULL);
    }



    import tkinter as tk import paho.mqtt.client as mqtt # --- 設定區 --- MQTT_SERVER = "mqtt-dashboard.com" MQTT_TOPIC = "alex9ufo/LEDcommand" class LEDController: def __init__(self, root): self.root = root self.root.title("ESP32 LED 遠端控制") self.root.geometry("300x450") # 連線狀態 self.status_label = tk.Label(root, text="MQTT 狀態: 正在連線...", fg="orange", font=("Arial", 10)) self.status_label.pack(pady=10) # 按鈕標題 tk.Label(root, text="選擇 LED 模式", font=("Arial", 12, "bold")).pack(pady=5) # 模式按鈕定義 modes = [ ("1. 從上到下", "1"), ("2. 從下到上", "2"), ("3. 上下來回", "3"), ("4. 1,3,5,7,9 亮", "4"), ("5. 2,4,6,8,10 亮", "5"), ("6. 奇偶交替閃爍", "6"), ("全部熄滅", "0") ] for text, cmd in modes: btn = tk.Button(root, text=text, width=20, height=2, command=lambda c=cmd: self.send_command(c)) btn.pack(pady=5) # MQTT 初始化 self.client = mqtt.Client() self.client.on_connect = self.on_connect try: self.client.connect(MQTT_SERVER, 1883, 60) self.client.loop_start() except: self.status_label.config(text="MQTT 狀態: 連線失敗", fg="red") def on_connect(self, client, userdata, flags, rc): if rc == 0: self.status_label.config(text="MQTT 狀態: 已連線", fg="green") else: self.status_label.config(text=f"MQTT 狀態: 錯誤({rc})", fg="red") def send_command(self, cmd): self.client.publish(MQTT_TOPIC, cmd) print(f"發送指令: {cmd}") if __name__ == "__main__": root = tk.Tk() app = LEDController(root) root.mainloop()

    2026年1月20日 星期二

    Python 控制 WOKWI ESP32 10個LED 閃爍

     Python 控制 WOKWI ESP32 10個LED 閃爍

    分為兩個部分:ESP32 接收端程式(負責接收指令並執行 LED 跑馬燈)以及 Python Tkinter 控制端程式(負責發送指令)。

    1. ESP32 端程式碼 (Wokwi)

    這段程式會連線到 MQTT Broker 並訂閱 alex9ufo/LEDcommand 主題。當收到數字指令時,會切換對應的 LED 效果。

    2. Python Tkinter 控制端程式碼

    這支程式提供按鈕來發送 1~6 的指令,並顯示 MQTT 連線狀態。




    #include <WiFi.h>
    #include <PubSubClient.h>

    // --- 設定區 ---
    const char* ssid = "Wokwi-GUEST";
    const char* password = "";
    const char* mqtt_server = "mqtt-dashboard.com";
    const char* mqtt_topic = "alex9ufo/LEDcommand";

    // LED 接腳定義
    int ledPins[] = {19, 18, 5, 4, 2, 27, 26, 25, 33, 32};
    int numLeds = 10;
    int currentMode = 0; // 目前的模式編號

    WiFiClient espClient;
    PubSubClient client(espClient);

    void allOff() {
      for (int i = 0; i < numLeds; i++) digitalWrite(ledPins[i], LOW);
    }

    // MQTT 接收回調函式
    void callback(char* topic, byte* payload, unsigned int length) {
      String message = "";
      for (int i = 0; i < length; i++) message += (char)payload[i];
     
      Serial.print("收到指令: ");
      Serial.println(message);
     
      // 更新模式編號
      currentMode = message.toInt();
      allOff(); // 切換模式時先全滅
    }

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

    void reconnect() {
      while (!client.connected()) {
        Serial.print("Attempting MQTT connection...");
        if (client.connect("ESP32_LED_Client")) {
          Serial.println("connected");
          client.subscribe(mqtt_topic);
        } else {
          delay(5000);
        }
      }
    }

    void setup() {
      Serial.begin(115200);
      for (int i = 0; i < numLeds; i++) pinMode(ledPins[i], OUTPUT);
      setup_wifi();
      client.setServer(mqtt_server, 1883);
      client.setCallback(callback);
    }

    void runMode(int mode) {
      switch (mode) {
        case 1: // 從上到下
          for (int i = 0; i < numLeds; i++) { allOff(); digitalWrite(ledPins[i], HIGH); delay(100); client.loop(); if(currentMode != 1) return; }
          break;
        case 2: // 從下到上
          for (int i = numLeds - 1; i >= 0; i--) { allOff(); digitalWrite(ledPins[i], HIGH); delay(100); client.loop(); if(currentMode != 2) return; }
          break;
        case 3: // 上下來回
          for (int i = 0; i < numLeds; i++) { allOff(); digitalWrite(ledPins[i], HIGH); delay(70); client.loop(); }
          for (int i = numLeds - 2; i > 0; i--) { allOff(); digitalWrite(ledPins[i], HIGH); delay(70); client.loop(); }
          break;
        case 4: // 1,3,5,7,9 亮
          allOff();
          for (int i = 0; i < numLeds; i += 2) digitalWrite(ledPins[i], HIGH);
          break;
        case 5: // 2,4,6,8,10 亮
          allOff();
          for (int i = 1; i < numLeds; i += 2) digitalWrite(ledPins[i], HIGH);
          break;
        case 6: // 奇偶交替
          allOff();
          for (int i = 0; i < numLeds; i += 2) digitalWrite(ledPins[i], HIGH);
          delay(300); client.loop();
          allOff();
          for (int i = 1; i < numLeds; i += 2) digitalWrite(ledPins[i], HIGH);
          delay(300);
          break;
      }
    }

    void loop() {
      if (!client.connected()) reconnect();
      client.loop();
      if (currentMode != 0) runMode(currentMode);
    }





    import tkinter as tk

    import paho.mqtt.client as mqtt


    # --- 設定區 ---

    MQTT_SERVER = "mqtt-dashboard.com"

    MQTT_TOPIC = "alex9ufo/LEDcommand"


    class LEDController:

        def __init__(self, root):

            self.root = root

            self.root.title("ESP32 LED 遠端控制")

            self.root.geometry("300x450")


            # 連線狀態

            self.status_label = tk.Label(root, text="MQTT 狀態: 正在連線...", fg="orange", font=("Arial", 10))

            self.status_label.pack(pady=10)


            # 按鈕標題

            tk.Label(root, text="選擇 LED 模式", font=("Arial", 12, "bold")).pack(pady=5)


            # 模式按鈕定義

            modes = [

                ("1. 從上到下", "1"),

                ("2. 從下到上", "2"),

                ("3. 上下來回", "3"),

                ("4. 1,3,5,7,9 亮", "4"),

                ("5. 2,4,6,8,10 亮", "5"),

                ("6. 奇偶交替閃爍", "6"),

                ("全部熄滅", "0")

            ]


            for text, cmd in modes:

                btn = tk.Button(root, text=text, width=20, height=2, 

                                command=lambda c=cmd: self.send_command(c))

                btn.pack(pady=5)


            # MQTT 初始化

            self.client = mqtt.Client()

            self.client.on_connect = self.on_connect

            try:

                self.client.connect(MQTT_SERVER, 1883, 60)

                self.client.loop_start()

            except:

                self.status_label.config(text="MQTT 狀態: 連線失敗", fg="red")


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

            if rc == 0:

                self.status_label.config(text="MQTT 狀態: 已連線", fg="green")

            else:

                self.status_label.config(text=f"MQTT 狀態: 錯誤({rc})", fg="red")


        def send_command(self, cmd):

            self.client.publish(MQTT_TOPIC, cmd)

            print(f"發送指令: {cmd}")


    if __name__ == "__main__":

        root = tk.Tk()

        app = LEDController(root)

        root.mainloop()


    經由MQTT協定的2個WOKWI ESP32 雙向通訊 (ESP32 to ESP32 MQTT Communication )

     經由MQTT協定的2個WOKWI ESP32 雙向通訊  (ESP32  to ESP32 MQTT Communication ) 使用兩個 ESP32 建立一個遠端控制系統。 MQTT Broker: mqtt-dashboard.com Topic (主題): ale...