# ESP32_PZEM004T (改DC供電) | ESP32_PZEM004T | |:---------------------------------------------------:| |![image](https://hackmd.io/_uploads/rk3M1kVhkx.jpg)| ## 簡介 這是一個測量電流的專案,主要功能如下 * 測量 1~100A 的交流電流 * 透過 Wifi 與 MQTT 定時上傳 * 使用 TTGO T-Display,TFT 螢幕即時顯示 * 透過開發板上的兩顆按鈕,做簡易的測量校正 * 本程式由 grok3 協助撰寫 ## 材料與電路 * TTGO T-Display (esp32) * PZEM-004T 與 CT * SMA01L-12 (1W DC-DC Unregulated Single Output Converter) * 1K 電阻 * 1 * 接線如下圖 | 接線圖 | |:---------------------------------------------------------------------------------------------------------------:| |![image](https://hackmd.io/_uploads/HyA9nC72kx.png)| | 訂正:圖中 26 與 RX 之間不需要電阻,否則通訊不穩定) | | 圖中 27 與 TX 之間的電阻,是用比較簡單的方式配合 ESP32 3.3V 的電平 | | 為什麼用5V而不是3.3V?這樣就不需要那顆電阻。因為稍後要用 5V 轉 12V 的模組,我一時買不到 3.3V 轉 12V 的模組。| | PZEM-004T | |:---------------------------------------------------:| | ![image](https://hackmd.io/_uploads/B1ehU3X3yg.png) | | PZEM-004T 原理圖 (我只找到這板,與實體略有不同) | |:---------------------------------------------------:| | ![image](https://hackmd.io/_uploads/rkgrwhXhkx.png) | ## 我的特殊需求 * PZEM-004T 預設使用 AC 供電。但在我的需求中,==不允許用 AC 供電==,所以必須把它改成 DC 供電。ESP32 使用電池供電。 * 由原理圖與實際測量得知,AC 電源經過 12V 穩壓二極體,在經過 7133 LDO,轉換成 3.3V 供晶片使用。 * 我將使用一個 DC-DC Converter (5V轉12V),直接飛線到 LDO 的 VIN,提供 DC 12V 的電源 | DC DC Converter | |:---------------------------------------------------:| | ![image](https://hackmd.io/_uploads/SypArnX2Je.png) | | 魔改版 PZEM-004T | |:---------------------------------------------------:| |![image](https://hackmd.io/_uploads/BkypcnXhJx.png)| |![image](https://hackmd.io/_uploads/ryJTn3m2yg.jpg)| |![image](https://hackmd.io/_uploads/B1Fshh7n1g.jpg)| ## 程式碼 :::spoiler TTGO_PZEM004T.ino ```cpp= // Used library Version Path // WiFi 2.0.0 /home/chihhaolai/.arduino15/packages/esp32/hardware/esp32/2.0.14/libraries/WiFi // Wire 2.0.0 /home/chihhaolai/.arduino15/packages/esp32/hardware/esp32/2.0.14/libraries/Wire // TFT_eSPI 2.5.43 /home/chihhaolai/Arduino/libraries/TFT_eSPI // SPI 2.0.0 /home/chihhaolai/.arduino15/packages/esp32/hardware/esp32/2.0.14/libraries/SPI // FS 2.0.0 /home/chihhaolai/.arduino15/packages/esp32/hardware/esp32/2.0.14/libraries/FS // SPIFFS 2.0.0 /home/chihhaolai/.arduino15/packages/esp32/hardware/esp32/2.0.14/libraries/SPIFFS // PubSubClient 2.8 /home/chihhaolai/Arduino/libraries/pubsubclient-master // EEPROM 2.0.0 /home/chihhaolai/.arduino15/packages/esp32/hardware/esp32/2.0.14/libraries/EEPROM // Button2 2.3.4 /home/chihhaolai/Arduino/libraries/Button2-master // PZEM004Tv30 1.2.1 /home/chihhaolai/Arduino/libraries/PZEM004Tv30-master // // Used platform Version Path // esp32:esp32 2.0.14 /home/chihhaolai/.arduino15/packages/esp32/hardware/esp32/2.0.14 #include <WiFi.h> #include <esp_wifi.h> #include <TFT_eSPI.h> #include <PubSubClient.h> #include <EEPROM.h> #include <Button2.h> #include "wifi_config.h" // 引用外部 WiFi 配置文件 #include <PZEM004Tv30.h> #define ENABLE_WIFI true #define PZEM_RX_PIN 26 #define PZEM_TX_PIN 25 #define PZEM_SERIAL Serial2 PZEM004Tv30 pzem(PZEM_SERIAL, PZEM_RX_PIN, PZEM_TX_PIN); #define ADC_PIN 36 #define RANDOM_SEED_PIN 32 TFT_eSPI tft = TFT_eSPI(); const char* VERSION = "2025-03-16a"; const char* topicBase = "ESP32_PZEM/"; char mqttTopic[30]; char equipmentId[6] = "Eq01"; char clientId[30] = "ESP32_PZEM"; unsigned long lastMsgDisplay = 0; // 記錄最後一次成功顯示的時間 WiFiClient espClient; PubSubClient client(espClient); uint8_t newMACAddress[] = {0x30, 0xC6, 0xF7, 0x51, 0xB3, 0xF8}; float ratio = 1.000; float global_current = 0.0; float global_voltage = 0.0; float global_power = 0.0; int voltageIndex = 1; float voltages[] = {110.0, 208.0, 220.0}; #define STATUS_BLOCK_WIDTH 60 #define STATUS_BLOCK_HEIGHT 20 #define WIFI_BLOCK_X 0 #define WIFI_BLOCK_Y 0 #define MQTT_BLOCK_X (STATUS_BLOCK_WIDTH + 5) #define MQTT_BLOCK_Y 0 #define VOLTAGE_BLOCK_X MQTT_BLOCK_X + (STATUS_BLOCK_WIDTH + 5) #define VOLTAGE_BLOCK_Y 0 #define BLINK_INTERVAL 500 #define MQTT_UPLOAD_INTERVAL 20000 //20秒上傳一次 #define MSG_TIMEOUT 5000 // 底部訊息顯示時間 #define WIFI_ICON_X (tft.width() - 32) #define WIFI_ICON_Y 0 #define WIFI_ICON_WIDTH 32 #define WIFI_ICON_HEIGHT 20 #define TFT_GRAY tft.color565(128, 128, 128) #define TFT_SKYBLUE tft.color565(0, 191, 255) #define STATUS_BAR_HEIGHT 24 #define ERROR_BAR_HEIGHT 20 #define DATA_AREA_TOP STATUS_BAR_HEIGHT #define DATA_AREA_BOTTOM (tft.height() - ERROR_BAR_HEIGHT) // 135 - 20 = 115 #define EEPROM_SIZE 14 #define EEPROM_ADDR 0 #define RATIO_EEPROM_ADDR (EEPROM_ADDR + 5) // 在 equipmentId 後儲存 ratio #define RATIO_MIN 0.5 #define RATIO_MAX 1.5 #define RATIO_STEP 0.005 // 增大步長 Button2 button1(35); // GPIO 35 Button2 button2(0); // GPIO 0 volatile bool wifiConnected = false; volatile bool mqttConnected = false; TaskHandle_t mqttTaskHandle = NULL; TaskHandle_t wifiTaskHandle = NULL; TaskHandle_t currentTaskHandle = NULL; SemaphoreHandle_t wifiMutex = NULL; SemaphoreHandle_t serialMutex = NULL; SemaphoreHandle_t dataMutex = NULL; unsigned long lastFastUpdate = 0; unsigned long lastSlowUpdate = 0; unsigned long lastMsgTime = 0; void mySerialPrintln(const String &message) { if (xSemaphoreTake(serialMutex, portMAX_DELAY) == pdTRUE) { Serial.println(message); xSemaphoreGive(serialMutex); } } void mySerialPrint(const String &message) { if (xSemaphoreTake(serialMutex, portMAX_DELAY) == pdTRUE) { Serial.print(message); xSemaphoreGive(serialMutex); } } void displayData() { static float lastCurrent = -1; static float lastPower = -1; if (xSemaphoreTake(dataMutex, portMAX_DELAY) == pdTRUE) { // 計算每個區域的高度 int halfHeight = (DATA_AREA_BOTTOM - DATA_AREA_TOP - 2) / 2; // 45 // 設定文字置中並調大字體 tft.setTextSize(3); tft.setTextDatum(MC_DATUM); // Middle Center 置中對齊 // 上方:電流 (Current) if (global_current != lastCurrent) { tft.fillRect(0, DATA_AREA_TOP, tft.width(), halfHeight, TFT_BLACK); // Y=24 to 69 tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.drawString(String(global_current, 2) + " A", tft.width() / 2, DATA_AREA_TOP + halfHeight / 2); // X=120, Y=47 lastCurrent = global_current; } // 下方:功率 (Power) if (global_power != lastPower) { tft.fillRect(0, DATA_AREA_TOP + halfHeight, tft.width(), halfHeight, TFT_BLACK); // Y=69 to 114 tft.setTextColor(TFT_YELLOW, TFT_BLACK); if (global_power >= 1000) { float powerKW = global_power / 1000.0; tft.drawString(String(powerKW, 3) + " kW", tft.width() / 2, DATA_AREA_TOP + halfHeight + halfHeight / 2); // X=120, Y=92 } else { tft.drawString(String(global_power, 2) + " W", tft.width() / 2, DATA_AREA_TOP + halfHeight + halfHeight / 2); // X=120, Y=92 } lastPower = global_power; } // 恢復預設文字大小和對齊方式 tft.setTextSize(2); tft.setTextDatum(TL_DATUM); xSemaphoreGive(dataMutex); } } void displayError(String message, uint16_t color = TFT_RED) { tft.fillRect(0, DATA_AREA_BOTTOM + 1, tft.width(), ERROR_BAR_HEIGHT - 2, TFT_BLACK); tft.setTextDatum(MC_DATUM); tft.setTextColor(color, TFT_BLACK); tft.drawString(message, tft.width() / 2, DATA_AREA_BOTTOM + ERROR_BAR_HEIGHT / 2 + 1); tft.setTextDatum(TL_DATUM); } void displayMsg(String message, uint16_t color = TFT_SKYBLUE) { displayError(message, color); // 直接顯示,無時間限制 lastMsgTime = millis(); // 更新最後顯示時間 } void loadEEPROM() { // 載入 equipmentId String storedId = ""; for (int i = 0; i < 5; i++) { char c = EEPROM.read(EEPROM_ADDR + i); if (c == 0 || c == 255) break; storedId += c; } if (storedId.length() == 4 && storedId.startsWith("Eq")) { storedId.toCharArray(equipmentId, 6); } else { strcpy(equipmentId, "Eq01"); saveEquipmentId(); } // 載入 ratio float storedRatio; EEPROM.get(RATIO_EEPROM_ADDR, storedRatio); if (storedRatio >= RATIO_MIN && storedRatio <= RATIO_MAX) { ratio = storedRatio; } else { ratio = 1.0; saveRatio(); } } void saveRatio() { EEPROM.put(RATIO_EEPROM_ADDR, ratio); EEPROM.commit(); } void saveEquipmentId() { for (int i = 0; i < 5; i++) { if (i < strlen(equipmentId)) { EEPROM.write(EEPROM_ADDR + i, equipmentId[i]); } else { EEPROM.write(EEPROM_ADDR + i, 0); } } EEPROM.commit(); } void setEquipmentIdFromSerial() { if (Serial.available() > 0) { String input = Serial.readStringUntil('\n'); input.trim(); if (input.length() == 4 && input.startsWith("Eq") && isDigit(input[2]) && isDigit(input[3])) { int num = input.substring(2).toInt(); if (num >= 1 && num <= 20) { input.toCharArray(equipmentId, 6); saveEquipmentId(); mySerialPrintln("Equipment ID set to: " + String(equipmentId)); updateMqttTopicAndClientId(); } else { mySerialPrintln("Invalid number! Please enter Eq01 to Eq20."); } } else { mySerialPrintln("Invalid format! Please enter in format 'EqXX' (e.g., Eq01 to Eq20)."); } } } void updateMqttTopicAndClientId() { // 從 PIN32 讀取模擬值並用作隨機數種子 int analogValue = analogRead(RANDOM_SEED_PIN); // 讀取模擬值 srand(analogValue); int randomSuffix = rand() % 1000000; // 生成0到999999的隨機數 snprintf(clientId, sizeof(clientId), "ESP32_PZEM_%s_%06d", equipmentId, randomSuffix); // 格式化字串 snprintf(mqttTopic, sizeof(mqttTopic), "%s%s", topicBase, equipmentId); mySerialPrintln("Client ID updated to: " + String(clientId)); mySerialPrintln("MQTT Topic updated to: " + String(mqttTopic)); } void connectToWiFiTask(void *pvParameters) { int failedAttempts = 0; while (true) { if (xSemaphoreTake(wifiMutex, portMAX_DELAY) == pdTRUE) { if (WiFi.status() != WL_CONNECTED) { wifiConnected = false; mqttConnected = false; mySerialPrintln("Wi-Fi not connected, attempting to reconnect..."); WiFi.disconnect(); vTaskDelay(1000 / portTICK_PERIOD_MS); mySerialPrintln("Connecting to SSID: " + String(ssid)); WiFi.begin(ssid, password); int attempts = 0; while (WiFi.status() != WL_CONNECTED && attempts < 20) { vTaskDelay(500 / portTICK_PERIOD_MS); mySerialPrint("."); attempts++; } mySerialPrintln(""); if (WiFi.status() == WL_CONNECTED) { wifiConnected = true; mySerialPrintln("Wi-Fi Connected"); mySerialPrint("IP Address: "); mySerialPrintln(WiFi.localIP().toString()); failedAttempts = 0; } else { mySerialPrint("Wi-Fi Connection Failed: "); mySerialPrint("Status Code: "); mySerialPrintln(String(WiFi.status())); wifiConnected = false; mqttConnected = false; WiFi.disconnect(); failedAttempts++; } } else { wifiConnected = true; } xSemaphoreGive(wifiMutex); } vTaskDelay(10000 / portTICK_PERIOD_MS); } } void connectToMqtt() { mqttConnected = false; client.disconnect(); client.setServer(mqttServer, mqttPort); client.setSocketTimeout(10); if (client.connect(clientId, mqttUser, mqttPassword)) { mqttConnected = true; mySerialPrintln("Connected to MQTT broker"); } else { mqttConnected = false; mySerialPrintln("MQTT Connect failed: " + String(client.state())); } } void mqttPublishTask(void *pvParameters) { char payload[128]; while (true) { if (xSemaphoreTake(wifiMutex, portMAX_DELAY) == pdTRUE) { if (WiFi.status() == WL_CONNECTED) { if (!client.connected()) { connectToMqtt(); } if (client.connected()) { if (xSemaphoreTake(dataMutex, portMAX_DELAY) == pdTRUE) { snprintf(payload, sizeof(payload), "{\"voltage\":%.2f,\"current\":%.2f,\"power\":%.2f}", global_voltage, global_current, global_power); if (client.publish(mqttTopic, payload)) { mqttConnected = true; displayMsg("Data upload OK!"); mySerialPrintln("Data published to MQTT: " + String(payload)); } else { mqttConnected = false; displayError("Data upload Fail!"); mySerialPrintln("Publish failed"); client.disconnect(); } xSemaphoreGive(dataMutex); } } } else { wifiConnected = false; mqttConnected = false; client.disconnect(); } xSemaphoreGive(wifiMutex); } vTaskDelay(MQTT_UPLOAD_INTERVAL / portTICK_PERIOD_MS); } } void measureCurrentTask(void *pvParameters) { while (true) { float rawCurrent = pzem.current(); if (xSemaphoreTake(dataMutex, portMAX_DELAY) == pdTRUE) { global_current = rawCurrent * ratio; if (voltageIndex == 1) { // 208V 對應三相 global_power = global_voltage * global_current * sqrt(3.0); } else{ global_power = global_voltage * global_current ; } xSemaphoreGive(dataMutex); } // Serial.printf("%f A\n", rawCurrent); vTaskDelay(100 / portTICK_PERIOD_MS); } } void changeRatio(float value){ ratio += value; if (ratio > RATIO_MAX) ratio = RATIO_MAX; if (ratio < RATIO_MIN) ratio = RATIO_MIN; mySerialPrint("Ratio: "); mySerialPrintln(String(ratio,3)); saveRatio(); // 儲存到 EEPROM displayMsg("Ratio: " + String(ratio, 3)); } void setup() { Serial.begin(115200); while (!Serial); serialMutex = xSemaphoreCreateMutex(); if (serialMutex == NULL) { Serial.println("Failed to create serial mutex"); while (1); } wifiMutex = xSemaphoreCreateMutex(); if (wifiMutex == NULL) { mySerialPrintln("Failed to create WiFi mutex"); while (1); } dataMutex = xSemaphoreCreateMutex(); if (dataMutex == NULL) { mySerialPrintln("Failed to create data mutex"); while (1); } mySerialPrintln("System starting..."); mySerialPrintln("VERSION: " + String(VERSION)); EEPROM.begin(EEPROM_SIZE); loadEEPROM(); updateMqttTopicAndClientId(); pinMode(ADC_PIN, INPUT); // 配置 GPIO 36 為輸入 pinMode(RANDOM_SEED_PIN, INPUT); // 配置 GPIO 32 為輸入 global_voltage = voltages[voltageIndex]; // Button2 初始化 static unsigned long lastButton1PressTime = 0; static unsigned long lastButton2PressTime = 0; button1.setPressedHandler([](Button2& btn) { unsigned long currentTime = millis(); if (currentTime - lastButton1PressTime < 200) return; lastButton1PressTime = currentTime; if (button2.isPressed()) { voltageIndex = (voltageIndex + 1) % 3; if (xSemaphoreTake(dataMutex, portMAX_DELAY) == pdTRUE) { global_voltage = voltages[voltageIndex]; if (voltageIndex == 1) { global_power = global_voltage * global_current * sqrt(3.0); } else { global_power = global_voltage * global_current; } xSemaphoreGive(dataMutex); } } else { changeRatio(RATIO_STEP); // 使用 changeRatio 增加 ratio } displayData(); }); button2.setPressedHandler([](Button2& btn) { unsigned long currentTime = millis(); if (currentTime - lastButton2PressTime < 200) return; lastButton2PressTime = currentTime; if (button1.isPressed()) { voltageIndex = (voltageIndex + 1) % 3; if (xSemaphoreTake(dataMutex, portMAX_DELAY) == pdTRUE) { global_voltage = voltages[voltageIndex]; if (voltageIndex == 1) { global_power = global_voltage * global_current * sqrt(3.0); } else { global_power = global_voltage * global_current; } xSemaphoreGive(dataMutex); } } else { changeRatio(-RATIO_STEP); // 使用 changeRatio 減少 ratio } displayData(); // 更新顯示 }); tft.init(); tft.setRotation(3); tft.fillScreen(TFT_BLACK); pinMode(4, OUTPUT); digitalWrite(4, HIGH); tft.setTextSize(2); tft.setTextDatum(TL_DATUM); // 左上對齊 // 畫兩條白線 tft.drawFastHLine(0, STATUS_BAR_HEIGHT - 1, tft.width(), TFT_GRAY); // Y=23 tft.drawFastHLine(0, DATA_AREA_BOTTOM - 2, tft.width(), TFT_GRAY); // Y=113 // 初始顯示 updateStatusBlocks(); displayData(); displayMsg(VERSION); if(ENABLE_WIFI){ WiFi.mode(WIFI_STA); esp_wifi_set_mac(WIFI_IF_STA, newMACAddress); // 設定 MAC (開發階段使用) mySerialPrintln("MAC Address Changed to: 30:C6:F7:51:B3:F8"); if (xTaskCreatePinnedToCore(connectToWiFiTask, "WiFi Task", 8192, NULL, 2, &wifiTaskHandle, 1) != pdPASS) { mySerialPrintln("Failed to create WiFi Task"); while (1); } client.setServer(mqttServer, mqttPort); if (xTaskCreatePinnedToCore(mqttPublishTask, "MQTT Publish Task", 8192, NULL, 1, &mqttTaskHandle, 1) != pdPASS) { mySerialPrintln("Failed to create MQTT Task"); while (1); } } if (xTaskCreatePinnedToCore(measureCurrentTask, "Current Task", 4096, NULL, 2, &currentTaskHandle, 1) != pdPASS) { mySerialPrintln("Failed to create Current Task"); while (1); } } void loop() { if (client.connected() && WiFi.status() == WL_CONNECTED) { client.loop(); } setEquipmentIdFromSerial(); button1.loop(); button2.loop(); // static unsigned long lastCheck = 0; // if (millis() - lastCheck > 10000) { // mySerialPrintln("wifiTaskHandle Remaining: " + String(uxTaskGetStackHighWaterMark(wifiTaskHandle)) + "/8192"); // mySerialPrintln("mqttTaskHandle Remaining: " + String(uxTaskGetStackHighWaterMark(mqttTaskHandle)) + "/8192"); // mySerialPrintln("currentTaskHandle Remaining: " + String(uxTaskGetStackHighWaterMark(currentTaskHandle))+ "/4096"); // lastCheck = millis(); // } unsigned long currentTime = millis(); // 清空底部訊息 if (lastMsgTime > 0 && (currentTime - lastMsgTime >= MSG_TIMEOUT)) { tft.fillRect(0, DATA_AREA_BOTTOM + 1, tft.width(), ERROR_BAR_HEIGHT - 2, TFT_BLACK); // 清空錯誤訊息區域 lastMsgTime = 0; } if (currentTime - lastFastUpdate >= BLINK_INTERVAL) { updateStatusBlocks(); updateWiFiSignalIcon(); lastFastUpdate = currentTime; } if (currentTime - lastSlowUpdate >= 333) { displayData(); lastSlowUpdate = currentTime; } } void updateWiFiSignalIcon() { static int lastSignalLevel = -1; int signalLevel = getWiFiSignalLevel(); if (signalLevel == lastSignalLevel) return; tft.fillRect(WIFI_ICON_X, WIFI_ICON_Y, WIFI_ICON_WIDTH + 1, WIFI_ICON_HEIGHT + 1, TFT_BLACK); if (signalLevel == 0) { for (int i = 0; i < 5; i++) { int barHeight = (i + 1) * 4; // 調整為 4 的倍數以適應 20px 高度 int barY = WIFI_ICON_Y + WIFI_ICON_HEIGHT - barHeight; int barWidth = 4; int barX = WIFI_ICON_X + i * 6; tft.fillRect(barX, barY, barWidth, barHeight, TFT_GRAY); } int thickness = 5; for (int i = 0; i < thickness; i++) { tft.drawLine(WIFI_ICON_X + i, WIFI_ICON_Y, WIFI_ICON_X + WIFI_ICON_WIDTH + i, WIFI_ICON_Y + WIFI_ICON_HEIGHT, TFT_RED); tft.drawLine(WIFI_ICON_X + WIFI_ICON_WIDTH - i, WIFI_ICON_Y, WIFI_ICON_X + i, WIFI_ICON_Y + WIFI_ICON_HEIGHT, TFT_RED); } } else { for (int i = 0; i < signalLevel; i++) { int barHeight = (i + 1) * 4; // 同樣調整為 4 的倍數 int barY = WIFI_ICON_Y + WIFI_ICON_HEIGHT - barHeight; int barWidth = 4; int barX = WIFI_ICON_X + i * 6; tft.fillRect(barX, barY, barWidth, barHeight, TFT_GREEN); } } lastSignalLevel = signalLevel; } int getWiFiSignalLevel() { int rssi = WiFi.RSSI(); if (rssi == 0) return 0; if (rssi > -50) return 5; if (rssi > -60) return 4; if (rssi > -70) return 3; if (rssi > -80) return 2; if (rssi > -90) return 1; return 0; } void updateStatusBlocks() { static int lastWifiStatus = -1; static bool lastMqttConnected = false; static int lastVoltageIndex = -1; // 新增:追蹤電壓變化 static bool firstRun = true; int currentWifiStatus = WiFi.status(); bool mqttStatusChanged = (mqttConnected != lastMqttConnected); bool voltageChanged = (voltageIndex != lastVoltageIndex); // 新增:檢查電壓是否改變 static bool blinkState = false; blinkState = !blinkState; tft.setTextSize(2); tft.setTextDatum(MC_DATUM); // WiFi 狀態 int WIFI_BLOCK_TEXT_X = WIFI_BLOCK_X + STATUS_BLOCK_WIDTH/2 + 2; // X=25 int WIFI_BLOCK_TEXT_Y = WIFI_BLOCK_Y + STATUS_BLOCK_HEIGHT/2 + 1; // Y=13 if (firstRun || currentWifiStatus != lastWifiStatus) { if (currentWifiStatus == WL_CONNECTED) { tft.fillRect(WIFI_BLOCK_X, WIFI_BLOCK_Y, STATUS_BLOCK_WIDTH, STATUS_BLOCK_HEIGHT, TFT_GREEN); tft.setTextColor(TFT_BLACK, TFT_GREEN); if (currentWifiStatus != lastWifiStatus) mySerialPrintln("WiFi Status: Connected"); } else { tft.fillRect(WIFI_BLOCK_X, WIFI_BLOCK_Y, STATUS_BLOCK_WIDTH, STATUS_BLOCK_HEIGHT, blinkState ? TFT_YELLOW : TFT_GRAY); tft.setTextColor(TFT_BLACK, blinkState ? TFT_YELLOW : TFT_GRAY); if (currentWifiStatus != lastWifiStatus) mySerialPrintln("WiFi Status: Connecting"); } tft.drawString("WIFI", WIFI_BLOCK_TEXT_X, WIFI_BLOCK_TEXT_Y); } else if (currentWifiStatus != WL_CONNECTED) { tft.fillRect(WIFI_BLOCK_X, WIFI_BLOCK_Y, STATUS_BLOCK_WIDTH, STATUS_BLOCK_HEIGHT, blinkState ? TFT_YELLOW : TFT_GRAY); tft.setTextColor(TFT_BLACK, blinkState ? TFT_YELLOW : TFT_GRAY); tft.drawString("WIFI", WIFI_BLOCK_TEXT_X, WIFI_BLOCK_TEXT_Y); } // MQTT 狀態 int MQTT_BLOCK_TEXT_X = MQTT_BLOCK_X + STATUS_BLOCK_WIDTH/2 + 2; // X=77 int MQTT_BLOCK_TEXT_Y = MQTT_BLOCK_Y + STATUS_BLOCK_HEIGHT/2 + 1; // Y=13 if (firstRun || mqttStatusChanged) { if (mqttConnected) { tft.fillRect(MQTT_BLOCK_X, MQTT_BLOCK_Y, STATUS_BLOCK_WIDTH, STATUS_BLOCK_HEIGHT, TFT_GREEN); tft.setTextColor(TFT_BLACK, TFT_GREEN); mySerialPrintln("MQTT Status: Connected"); } else { tft.fillRect(MQTT_BLOCK_X, MQTT_BLOCK_Y, STATUS_BLOCK_WIDTH, STATUS_BLOCK_HEIGHT, blinkState ? TFT_YELLOW : TFT_GRAY); tft.setTextColor(TFT_BLACK, blinkState ? TFT_YELLOW : TFT_GRAY); mySerialPrintln("MQTT Status: Connecting"); } tft.drawString("MQTT", MQTT_BLOCK_TEXT_X, MQTT_BLOCK_TEXT_Y); } else if (!mqttConnected) { tft.fillRect(MQTT_BLOCK_X, MQTT_BLOCK_Y, STATUS_BLOCK_WIDTH, STATUS_BLOCK_HEIGHT, blinkState ? TFT_YELLOW : TFT_GRAY); tft.setTextColor(TFT_BLACK, blinkState ? TFT_YELLOW : TFT_GRAY); tft.drawString("MQTT", MQTT_BLOCK_TEXT_X, MQTT_BLOCK_TEXT_Y); } // 新增:電壓狀態 (Voltage) int VOLTAGE_BLOCK_TEXT_X = VOLTAGE_BLOCK_X + STATUS_BLOCK_WIDTH/2 + 2; // X=127 int VOLTAGE_BLOCK_TEXT_Y = VOLTAGE_BLOCK_Y + STATUS_BLOCK_HEIGHT/2 + 1; // Y=13 if (firstRun || voltageChanged) { tft.fillRect(VOLTAGE_BLOCK_X, VOLTAGE_BLOCK_Y, STATUS_BLOCK_WIDTH, STATUS_BLOCK_HEIGHT, TFT_GREEN); tft.setTextColor(TFT_RED, TFT_GREEN); tft.drawString(String(global_voltage, 0) + "V", VOLTAGE_BLOCK_TEXT_X, VOLTAGE_BLOCK_TEXT_Y); } lastWifiStatus = currentWifiStatus; lastMqttConnected = mqttConnected; lastVoltageIndex = voltageIndex; // 更新電壓狀態 firstRun = false; tft.setTextDatum(TL_DATUM); } ``` ::: :::spoiler wifi_config.h ```cpp= #ifndef WIFI_CONFIG_H #define WIFI_CONFIG_H const char* ssid = "*****"; const char* password = "*****"; const char* mqttServer = "*****"; const int mqttPort = 1883; const char* mqttUser = "*****"; const char* mqttPassword = "*****"; #endif ``` :::