2025年6月13日 星期五

ESP32 雙核心 RFID 讀取與 MQTT 發佈

 

ESP32 雙核心 RFID 讀取與 MQTT 發佈

這個程式將實現:

  • Core 0 (PRO_CPU):專門負責 MQTT 連線管理和數據發佈
  • Core 1 (APP_CPU):專門負責 MFRC522 RFID 卡片讀取

1. 硬體連接

與單核心版本相同:

MFRC522 PinESP32 Pin (通常)說明
SDA (SS/CS)GPIO21SPI Slave Select (片選) 腳位 (在程式碼中定義為 SS_PIN)
SCKGPIO18SPI Clock (時鐘) 腳位
MOSIGPIO23SPI Master Out Slave In (主出從入) 腳位
MISOGPIO19SPI Master In Slave Out (主入從出) 腳位
RST (Reset)GPIO22重置腳位 (在程式碼中定義為 RST_PIN)
GNDGND地線
VCC3.3V電源 (MFRC522 通常需要 3.3V 電源)

請再次確認:您的 ESP32 開發板的 GPIO 腳位定義。MFRC522 VCC 務必連接到 3.3V


2. 軟體設定 (Arduino IDE)

確保您已經安裝了以下函式庫:

  1. ESP32 Board 支援
  2. MFRC522 函式庫 (MFRC522 by Udo Klein 或其他相容版本)
  3. PubSubClient 函式庫 (PubSubClient by Nick O'Leary)

3. 程式碼

Arduino
#include <SPI.h>              // MFRC522 依賴的 SPI 函式庫
#include <MFRC522.h>          // MFRC522 RFID 讀卡器函式庫
#include <WiFi.h>             // ESP32 Wi-Fi 函式庫
#include <PubSubClient.h>     // MQTT 客戶端函式庫
#include <Arduino.h>          // 確保 FreeRTOS 相關函數可用

/********************************** 常數定義 **********************************/

// Wi-Fi 網路設定
const char* ssid = "YOUR_WIFI_SSID";     // 替換成您的 Wi-Fi 名稱
const char* password = "YOUR_WIFI_PASSWORD"; // 替換成您的 Wi-Fi 密碼

// MFRC522 模組設定 (SPI 連接)
#define RST_PIN    22         // MFRC522 重置腳位 (RST/Reset)
#define SS_PIN     21         // MFRC522 選擇腳位 (SDA/SS/CS)

// MQTT Broker 設定
const char* mqtt_server = "mqttgo.io";    // MQTT Broker 位址
const int mqtt_port = 1883;               // MQTT 端口
const char* mqtt_client_id = "ESP32_RFID_DualCore_Client"; // MQTT 客戶端 ID
const char* mqtt_topic = "alex9ufo/rfidUID"; // MQTT 發佈主題

// 任務堆疊大小 (根據任務複雜度調整)
#define TASK_STACK_SIZE 4096

// 發佈頻率控制 (避免重複發送相同的 UID)
const long publishInterval = 2000; // 2000 毫秒 = 2 秒

/********************************** 全域變數與物件 **********************************/

// MFRC522 讀卡器物件
MFRC522 mfrc522(SS_PIN, RST_PIN);

// WiFiClient 物件
WiFiClient espClient;

// PubSubClient 物件
PubSubClient mqttClient(espClient); // 注意這裡改名為 mqttClient 以區別

// 任務句柄
TaskHandle_t TaskRFIDRead = NULL;
TaskHandle_t TaskMQTTPublish = NULL;

// 使用 FreeRTOS 的 Queue 來在兩個任務之間傳遞 UID
// 佇列可儲存一個 String 物件,長度為 1
QueueHandle_t rfidUIDQueue;

// 保護共享資源 (lastUID 和 lastPublishTime) 的互斥鎖
SemaphoreHandle_t xSemaphore;
String lastUID = "";
unsigned long lastPublishTime = 0;

/********************************** 輔助函數 **********************************/

// 連接到 Wi-Fi 網路的函數
void setup_wifi() {
  delay(10);
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

  int retries = 0;
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    retries++;
    if (retries > 20) { // 最多等待 10 秒
      Serial.println("\nWiFi connection failed! Retrying...");
      retries = 0;
      WiFi.disconnect();
      WiFi.begin(ssid, password);
    }
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
}

// MQTT 訊息回調函數 (此程式主要發佈數據,接收功能可根據需求擴展)
void mqtt_callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
  }
  Serial.println();
}

// 連接到 MQTT Broker 的函數
void reconnect_mqtt() {
  while (!mqttClient.connected()) {
    Serial.print("Attempting MQTT connection...");
    if (mqttClient.connect(mqtt_client_id)) {
      Serial.println("connected");
      // 如果需要訂閱主題,請在這裡添加
      // mqttClient.subscribe("your/subscribe/topic");
    } else {
      Serial.print("failed, rc=");
      Serial.print(mqttClient.state());
      Serial.println(" try again in 5 seconds");
      delay(5000); // 等待 5 秒後重試
    }
  }
}

// 將 UID 轉換為十六進位字串的函數
String uid_to_string(byte *buffer, byte bufferSize) {
  String uidString = "";
  for (byte i = 0; i < bufferSize; i++) {
    if (buffer[i] < 0x10) { // 如果是個位數,前面補 0
      uidString += "0";
    }
    uidString += String(buffer[i], HEX); // 轉換為十六進位
  }
  uidString.toUpperCase(); // 轉換為大寫
  return uidString;
}

/********************************** 核心任務函數 **********************************/

// Core 1 (APP_CPU) 上的 RFID 讀取任務
void TaskRFIDReader(void *pvParameters) {
  Serial.println("RFID Reader Task running on Core 1");
  SPI.begin();           // 啟動 SPI 介面
  mfrc522.PCD_Init();    // 初始化 MFRC522
  Serial.println("MFRC522 initialized on Core 1.");

  for (;;) { // 無限循環
    // 檢查是否有新的 RFID 卡片感應到
    if (mfrc522.PICC_IsNewCardPresent() && mfrc522.PICC_ReadCardSerial()) {
      String currentUID = uid_to_string(mfrc522.uid.uidByte, mfrc522.uid.size);
      Serial.print("[Core1] Card detected! UID: ");
      Serial.println(currentUID);

      // 嘗試獲取互斥鎖,保護共享變數
      if (xSemaphoreTake(xSemaphore, (TickType_t)10) == pdTRUE) {
        unsigned long currentMillis = millis();
        // 檢查 UID 是否與上次的相同,並控制發佈頻率
        if (currentUID != lastUID || (currentMillis - lastPublishTime >= publishInterval)) {
          // 將 UID 放入佇列,讓 MQTT 任務處理
          if (xQueueSend(rfidUIDQueue, &currentUID, (TickType_t)10) == pdPASS) {
            Serial.println("[Core1] UID sent to MQTT Queue.");
            // 只有在成功發送給 MQTT 任務後才更新 lastUID 和 lastPublishTime
            lastUID = currentUID;
            lastPublishTime = currentMillis;
          } else {
            Serial.println("[Core1] Failed to send UID to MQTT Queue (queue full).");
          }
        } else {
          Serial.println("[Core1] Same card, not sending (within interval).");
        }
        xSemaphoreGive(xSemaphore); // 釋放互斥鎖
      } else {
        Serial.println("[Core1] Failed to take semaphore.");
      }

      // 停止 PICC 和 PCD 以節省電力並防止重複讀取
      mfrc522.PICC_HaltA();
      mfrc522.PCD_StopCrypto1();
    }
    vTaskDelay(pdMS_TO_TICKS(50)); // 每次循環延遲 50ms,避免過度佔用 CPU
  }
}

// Core 0 (PRO_CPU) 上的 MQTT 發佈任務
void TaskMQTTPublisher(void *pvParameters) {
  Serial.println("MQTT Publisher Task running on Core 0");
  mqttClient.setServer(mqtt_server, mqtt_port);
  mqttClient.setCallback(mqtt_callback);

  for (;;) { // 無限循環
    if (!mqttClient.connected()) {
      reconnect_mqtt();
    }
    mqttClient.loop(); // 處理 MQTT 數據包和保持連線

    String uidToPublish;
    // 嘗試從佇列接收 UID,等待 100ms
    if (xQueueReceive(rfidUIDQueue, &uidToPublish, (TickType_t)100) == pdPASS) {
      Serial.print("[Core0] Received UID from Queue: ");
      Serial.println(uidToPublish);

      // 發佈 UID 到 MQTT 主題
      if (mqttClient.publish(mqtt_topic, uidToPublish.c_str())) {
        Serial.println("[Core0] UID published successfully to MQTT!");
      } else {
        Serial.println("[Core0] Failed to publish UID to MQTT.");
        // 如果發佈失敗,可以考慮將 UID 再次放入佇列或記錄下來以便後續重試
      }
    }
    vTaskDelay(pdMS_TO_TICKS(10)); // 每次循環延遲 10ms,讓出 CPU 給其他任務
  }
}

/********************************** Setup & Loop (主要執行緒) **********************************/

void setup() {
  Serial.begin(115200);
  delay(100);

  // 創建互斥鎖
  xSemaphore = xSemaphoreCreateMutex();
  if (xSemaphore == NULL) {
    Serial.println("Failed to create mutex semaphore.");
    // 處理錯誤,可能重啟或其他錯誤指示
  }

  // 創建 RFID UID 佇列
  rfidUIDQueue = xQueueCreate(1, sizeof(String)); // 佇列深度為 1,每次只存一個 UID
  if (rfidUIDQueue == NULL) {
    Serial.println("Failed to create RFID UID queue.");
    // 處理錯誤
  }

  // 設定 Wi-Fi (通常在 Core 0 執行,但因為是 FreeRTOS,最好在任務啟動前完成)
  setup_wifi();

  // 創建 RFID 讀取任務,並將其固定在 Core 1 (APP_CPU)
  xTaskCreatePinnedToCore(
    TaskRFIDReader,     // 任務函數
    "RFID_Reader",      // 任務名稱
    TASK_STACK_SIZE,    // 任務堆疊大小
    NULL,               // 任務參數
    1,                  // 任務優先級 (0 最低,portTICK_RATE_MS 相關)
    &TaskRFIDRead,      // 任務句柄
    1                   // 運行在 Core 1
  );

  // 創建 MQTT 發佈任務,並將其固定在 Core 0 (PRO_CPU)
  xTaskCreatePinnedToCore(
    TaskMQTTPublisher,  // 任務函數
    "MQTT_Publisher",   // 任務名稱
    TASK_STACK_SIZE,    // 任務堆疊大小
    NULL,               // 任務參數
    1,                  // 任務優先級
    &TaskMQTTPublish,   // 任務句柄
    0                   // 運行在 Core 0
  );

  Serial.println("Tasks created. Entering main loop...");
}

void loop() {
  // 主循環中不執行任何阻塞操作,僅讓 FreeRTOS 調度器運行
  // 所有的 RFID 讀取和 MQTT 發佈都在各自的任務中進行
  vTaskDelete(NULL); // 刪除主任務,釋放 Core 1 (預設主任務在 Core 1)
                     // 如果您希望主任務持續運行但無所事事,可以只放一個 vTaskDelay
}

4. 運行與測試

  1. 接線:按照硬體連接說明,將 MFRC522 模組連接到您的 ESP32。
  2. 修改程式碼
    • YOUR_WIFI_SSIDYOUR_WIFI_PASSWORD 替換為您的 Wi-Fi 網路名稱和密碼。
    • 檢查 RST_PINSS_PIN 的定義是否與您的 MFRC522 實際連接的 ESP32 腳位一致。
  3. 上傳程式碼
    • 在 Arduino IDE 中,選擇正確的 ESP32 開發板 (例如 "ESP32 Dev Module") 和正確的 COM 埠。
    • 點擊 "上傳" 按鈕將程式碼燒錄到 ESP32。
  4. 開啟序列埠監控器
    • 燒錄完成後,打開 "工具" -> "序列埠監控器",設定鮑率為 115200
    • 您將會看到兩個核心啟動任務的訊息,Wi-Fi 連線進度,MQTT 連線狀態。
    • 當感應到卡片時,您會看到類似 [Core1] Card detected! UID: ...[Core0] Received UID from Queue: ...[Core0] UID published successfully to MQTT! 的訊息。
  5. MQTT 客戶端監控
    • 使用任何 MQTT 客戶端 (例如 MQTTX, Mosquitto Client, Node-RED 等) 連接到 mqttgo.io Broker,並訂閱 alex9ufo/rfidUID 主題。
    • 您將會看到 ESP32 發佈的卡片 UID。



製作一個軟體模組 (或稱 模擬器 / 樁函式),來模擬 MFRC522 的讀取行為。這樣,您就可以在 Wokwi 上驗證您的 MQTT 發佈邏輯和雙核心任務分配是否正確,而不需要實際的 MFRC522 硬體。

這個軟體模組會:

  1. 替代 MFRC522 函式庫的呼叫:當您的程式碼嘗試讀取卡片時,它會返回預設的或模擬的 UID。
  2. 模擬卡片感應行為:您可以設定一個時間間隔,讓它像真的感應到卡片一樣"觸發" UID 的讀取和發送。

模擬 MFRC522 的軟體模組程式碼

這個版本會用一個簡單的計數器來模擬不同的 RFID 卡片 UID,並將其整合到之前的雙核心架構中。




#include <WiFi.h>             // ESP32 Wi-Fi 函式庫
#include <PubSubClient.h>     // MQTT 客戶端函式庫
#include <Arduino.h>          // 確保 FreeRTOS 相關函數可用

// 注意:這裡不再需要 MFRC522 相關函式庫,因為我們將模擬其功能

/********************************** 常數定義 **********************************/

// Wi-Fi 網路設定
const char* ssid = "Wokwi-GUEST";     // 替換成您的 Wi-Fi 名稱 (例如 Wokwi 的 "Wokwi-GUEST")
const char* password = "";            // 替換成您的 Wi-Fi 密碼 (例如 Wokwi 的空字串 "")

// MQTT Broker 設定
const char* mqtt_server = "mqttgo.io";    // MQTT Broker 位址
const int mqtt_port = 1883;               // MQTT 端口
const char* mqtt_client_id = "MQTTGO-9527419059"; // MQTT 客戶端 ID (建議獨一無二)
const char* mqtt_topic = "alex9ufo/rfidUID"; // MQTT 發佈主題

// 任務堆疊大小 (根據任務複雜度調整)
#define TASK_STACK_SIZE 4096

// 發佈頻率控制 (避免重複發送相同的 UID)
const long publishInterval = 2000; // 2000 毫秒 = 2 秒,每 2 秒最多發送一次數據

// MFRC522 模擬模組設定
const long RFID_SIM_INTERVAL = 3000; // 每 3 秒模擬一次 RFID 卡片感應
unsigned long lastRFIDSimTime = 0;
int simulatedUIDCounter = 0; // 用於生成不同的模擬 UID

/********************************** 全域變數與物件 **********************************/

// 創建 WiFiClient 物件,用於 Wi-Fi 連線
WiFiClient espClient;

// 創建 PubSubClient 物件,用於 MQTT 通訊
PubSubClient mqttClient(espClient);

// 任務句柄
TaskHandle_t TaskRFIDSimulate = NULL;
TaskHandle_t TaskMQTTPublish = NULL;

// 使用 FreeRTOS 的 Queue 來在兩個任務之間傳遞 UID
// 佇列可儲存一個 String 物件,長度為 1
QueueHandle_t rfidUIDQueue;

// 保護共享資源 (lastUID 和 lastPublishTime) 的互斥鎖
SemaphoreHandle_t xSemaphore;
String lastUID = "";
unsigned long lastPublishTime = 0;

/********************************** 輔助函數 **********************************/

// 連接到 Wi-Fi 網路的函數
void setup_wifi() {
  delay(10);
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

  int retries = 0;
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    retries++;
    if (retries > 20) { // 最多等待 10 秒
      Serial.println("\nWiFi connection failed! Retrying...");
      retries = 0;
      WiFi.disconnect();
      WiFi.begin(ssid, password);
    }
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
}

// MQTT 訊息回調函數 (此程式主要發佈數據,接收功能可根據需求擴展)
void mqtt_callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
  }
  Serial.println();
}

// 連接到 MQTT Broker 的函數
void reconnect_mqtt() {
  while (!mqttClient.connected()) {
    Serial.print("Attempting MQTT connection...");
    if (mqttClient.connect(mqtt_client_id)) {
      Serial.println("connected");
      // 如果需要訂閱主題,請在這裡添加
      // mqttClient.subscribe("your/subscribe/topic");
    } else {
      Serial.print("failed, rc=");
      Serial.print(mqttClient.state());
      Serial.println(" try again in 5 seconds");
      delay(5000); // 等待 5 秒後重試
    }
  }
}

/********************************** 核心任務函數 **********************************/

// Core 1 (APP_CPU) 上的 RFID 模擬讀取任務
void TaskRFIDSimulator(void *pvParameters) {
  Serial.println("RFID Simulator Task running on Core 1");

  for (;;) { // 無限循環
    unsigned long currentMillis = millis();

    // 每 RFID_SIM_INTERVAL 毫秒模擬一次卡片感應
    if (currentMillis - lastRFIDSimTime >= RFID_SIM_INTERVAL) {
      lastRFIDSimTime = currentMillis;
     
      // 使用亂數產生 UID,格式為六個十六進位字符
      char uidBuffer[7]; // 6 個字符 + 1 個 null 終止符
      // 產生一個 24 位元(6 個十六進位字符)的亂數
      uint32_t randomUID = random(0xFFFFFF + 1); // random(max) 會產生 0 到 max-1 的數
                                                 // 所以 max+1 才能包含 0xFFFFFF
      sprintf(uidBuffer, "%06X", randomUID); // 格式化為六位大寫十六進位,不足補零
      String simulatedUID = String(uidBuffer);
      simulatedUID.toUpperCase(); // 轉換為大寫

      Serial.print("[Core1] Simulated Card Detected! UID: ");
      Serial.println(simulatedUID);

      // 嘗試獲取互斥鎖,保護共享變數
      if (xSemaphoreTake(xSemaphore, (TickType_t)10) == pdTRUE) {
        // 檢查模擬 UID 是否與上次的相同,並控制發佈頻率 (這部分在模擬中可能較不重要,但保留以保持結構一致)
        if (simulatedUID != lastUID || (currentMillis - lastPublishTime >= publishInterval)) {
          // 將模擬 UID 放入佇列,讓 MQTT 任務處理
          if (xQueueSend(rfidUIDQueue, &simulatedUID, (TickType_t)10) == pdPASS) {
            Serial.println("[Core1] Simulated UID sent to MQTT Queue.");
            // 只有在成功發送給 MQTT 任務後才更新 lastUID 和 lastPublishTime
            lastUID = simulatedUID;
            lastPublishTime = currentMillis;
          } else {
            Serial.println("[Core1] Failed to send simulated UID to MQTT Queue (queue full).");
          }
        } else {
          Serial.println("[Core1] Same simulated card, not sending (within interval).");
        }
        xSemaphoreGive(xSemaphore); // 釋放互斥鎖
      } else {
        Serial.println("[Core1] Failed to take semaphore.");
      }
    }
    vTaskDelay(pdMS_TO_TICKS(50)); // 每次循環延遲 50ms,避免過度佔用 CPU
  }
}

// Core 0 (PRO_CPU) 上的 MQTT 發佈任務
void TaskMQTTPublisher(void *pvParameters) {
  Serial.println("MQTT Publisher Task running on Core 0");
  mqttClient.setServer(mqtt_server, mqtt_port);
  mqttClient.setCallback(mqtt_callback);

  for (;;) { // 無限循環
    if (!mqttClient.connected()) {
      reconnect_mqtt();
    }
    mqttClient.loop(); // 處理 MQTT 數據包和保持連線

    String uidToPublish;
    // 嘗試從佇列接收 UID,等待 100ms
    if (xQueueReceive(rfidUIDQueue, &uidToPublish, (TickType_t)100) == pdPASS) {
      Serial.print("[Core0] Received UID from Queue: ");
      Serial.println(uidToPublish);

      // 發佈 UID 到 MQTT 主題
      if (mqttClient.publish(mqtt_topic, uidToPublish.c_str())) {
        Serial.println("[Core0] UID published successfully to MQTT!");
      } else {
        Serial.println("[Core0] Failed to publish UID to MQTT.");
        // 如果發佈失敗,可以考慮將 UID 再次放入佇列或記錄下來以便後續重試
      }
    }
    vTaskDelay(pdMS_TO_TICKS(10)); // 每次循環延遲 10ms,讓出 CPU 給其他任務
  }
}

/********************************** Setup & Loop (主要執行緒) **********************************/

void setup() {
  Serial.begin(115200);
  delay(100);

  // 創建互斥鎖
  xSemaphore = xSemaphoreCreateMutex();
  if (xSemaphore == NULL) {
    Serial.println("Failed to create mutex semaphore.");
    // 處理錯誤,可能重啟或其他錯誤指示
  }

  // 創建 RFID UID 佇列
  rfidUIDQueue = xQueueCreate(1, sizeof(String)); // 佇列深度為 1,每次只存一個 UID
  if (rfidUIDQueue == NULL) {
    Serial.println("Failed to create RFID UID queue.");
    // 處理錯誤
  }

  // 設定 Wi-Fi
  setup_wifi();

  // 創建 RFID 模擬讀取任務,並將其固定在 Core 1 (APP_CPU)
  xTaskCreatePinnedToCore(
    TaskRFIDSimulator,  // 任務函數
    "RFID_Simulator",   // 任務名稱
    TASK_STACK_SIZE,    // 任務堆疊大小
    NULL,               // 任務參數
    1,                  // 任務優先級
    &TaskRFIDSimulate,  // 任務句柄
    1                   // 運行在 Core 1
  );

  // 創建 MQTT 發佈任務,並將其固定在 Core 0 (PRO_CPU)
  xTaskCreatePinnedToCore(
    TaskMQTTPublisher,  // 任務函數
    "MQTT_Publisher",   // 任務名稱
    TASK_STACK_SIZE,    // 任務堆疊大小
    NULL,               // 任務參數
    1,                  // 任務優先級
    &TaskMQTTPublish,   // 任務句柄
    0                   // 運行在 Core 0
  );

  Serial.println("Tasks created. Entering main loop...");
}

void loop() {
  // 主循環中不執行任何阻塞操作,僅讓 FreeRTOS 調度器運行
  // 所有的 RFID 模擬讀取和 MQTT 發佈都在各自的任務中進行
  vTaskDelete(NULL); // 刪除主任務,釋放 Core 1 (預設主任務在 Core 1)
}

沒有留言:

張貼留言

ESP32 (ESP-IDF in VS Code) MFRC522 + MQTT + PYTHON TKinter +SQLite

 ESP32 (ESP-IDF in VS Code) MFRC522 + MQTT + PYTHON TKinter +SQLite  ESP32 VS Code 程式 ; PlatformIO Project Configuration File ; ;   Build op...