#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>
#include <ArduinoJson.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
// --- 硬體定義 ---
#define DHTPIN 2
#define DHTTYPE DHT22
#define LED1_PIN 4
#define LED2_PIN 5
#define LED3_PIN 6
#define LED4_PIN 7
#define I2C_SDA 8
#define I2C_SCL 9
DHT dht(DHTPIN, DHTTYPE);
WiFiClient espClient;
PubSubClient client(espClient);
LiquidCrystal_I2C lcd(0x27, 16, 2);
// --- LED 陣列與狀態管理 ---
const int ledPins[4] = {LED1_PIN, LED2_PIN, LED3_PIN, LED4_PIN};
enum Mode { IDLE, FLASHING, TIMER_COUNTDOWN };
Mode ledModes[4] = {IDLE, IDLE, IDLE, IDLE};
unsigned long targetTimes[4] = {0, 0, 0, 0};
unsigned long lastFlashTimes[4] = {0, 0, 0, 0};
const int flashInterval = 300;
// 動作定義
enum LedAction { ACT_UNKNOWN, ACT_ON, ACT_OFF, ACT_FLASH, ACT_TIMER };
unsigned long lastMsgTime = 0;
// --- MQTT 設定 ---
const char* ssid = "Wokwi-GUEST";
const char* password = "";
const char* mqtt_server = "mqttgo.io";
const char* topics[] = {
"wokwi/led/control", // LED 訂閱 (接收控制)
"wokwi/dht/temperature", // 溫度發布
"wokwi/dht/humidity" // 濕度發布
};
// --- 輔助函式:將字串動作轉為 Enum ---
LedAction getActionType(String actionStr) {
actionStr.toLowerCase();
if (actionStr == "on" || actionStr == "1") return ACT_ON;
if (actionStr == "off" || actionStr == "0") return ACT_OFF;
if (actionStr == "flash" || actionStr == "2") return ACT_FLASH;
if (actionStr == "timer" || actionStr == "3") return ACT_TIMER;
return ACT_UNKNOWN;
}
// --- 核心邏輯:套用動作到 LED ---
void applyActionToLED(int index, LedAction action, int timerValue) {
if (index < 0 || index >= 4) return;
switch (action) {
case ACT_ON:
ledModes[index] = IDLE;
digitalWrite(ledPins[index], HIGH);
break;
case ACT_OFF:
ledModes[index] = IDLE;
digitalWrite(ledPins[index], LOW);
break;
case ACT_FLASH:
ledModes[index] = FLASHING;
break;
case ACT_TIMER:
digitalWrite(ledPins[index], HIGH);
targetTimes[index] = millis() + (timerValue * 1000);
ledModes[index] = TIMER_COUNTDOWN;
break;
default:
break;
}
}
// 🌟 共用的純字串指令處理器
void handleDirectCommand(String cmd) {
cmd.toLowerCase();
LedAction finalAction = ACT_UNKNOWN;
int targetLed = 0; // 0=全部, 1-4=個別
int timerVal = 5; // 預設倒數 5 秒
if (cmd.startsWith("all")) {
targetLed = 0;
finalAction = getActionType(cmd.substring(3));
} else {
int splitIndex = 0;
while (splitIndex < cmd.length() && isDigit(cmd[splitIndex])) {
splitIndex++;
}
if (splitIndex > 0) {
targetLed = cmd.substring(0, splitIndex).toInt();
finalAction = getActionType(cmd.substring(splitIndex));
}
}
// 執行動作
if (finalAction == ACT_UNKNOWN) {
Serial.println(" ❌ 錯誤: 無法辨識的動作字串");
return;
}
if (targetLed == 0) {
for (int i = 0; i < 4; i++) applyActionToLED(i, finalAction, timerVal);
Serial.println(" ➡️ 狀態已更新: 全部 LED");
} else if (targetLed >= 1 && targetLed <= 4) {
applyActionToLED(targetLed - 1, finalAction, timerVal);
Serial.printf(" ➡️ 狀態已更新: LED %d\n", targetLed);
} else {
Serial.printf(" ❌ 錯誤: 無效的 LED 編號 %d\n", targetLed);
}
}
// --- MQTT 訊息回呼 ---
void callback(char* topic, byte* payload, unsigned int length) {
String rawMsg = "";
for (int i = 0; i < length; i++) rawMsg += (char)payload[i];
rawMsg.trim();
Serial.printf("\n📩 [MQTT 收到指令]: %s\n", rawMsg.c_str());
StaticJsonDocument<200> doc;
DeserializationError error = deserializeJson(doc, payload, length);
if (!error) {
Serial.println(" 📦 解析格式: JSON");
LedAction finalAction = getActionType(doc["action"] | "");
int targetLed = doc["led"] | 0;
int timerVal = doc["value"] | 5;
// 如果是 JSON 格式,直接在這裡套用動作
if (finalAction != ACT_UNKNOWN) {
if (targetLed == 0) {
for (int i = 0; i < 4; i++) applyActionToLED(i, finalAction, timerVal);
Serial.println(" ➡️ 狀態已更新: 全部 LED");
} else if (targetLed >= 1 && targetLed <= 4) {
applyActionToLED(targetLed - 1, finalAction, timerVal);
Serial.printf(" ➡️ 狀態已更新: LED %d\n", targetLed);
} else {
Serial.printf(" ❌ 錯誤: 無效的 LED 編號 %d\n", targetLed);
}
} else {
Serial.println(" ❌ 錯誤: 無法辨識的動作字串");
}
} else {
Serial.println(" 📝 解析格式: 純字串");
handleDirectCommand(rawMsg);
}
}
void setup() {
Serial.begin(115200);
for (int i = 0; i < 4; i++) {
pinMode(ledPins[i], OUTPUT);
digitalWrite(ledPins[i], LOW); // 依據繼電器觸發方式調整 HIGH/LOW
}
dht.begin();
Wire.begin(I2C_SDA, I2C_SCL);
lcd.init();
lcd.backlight();
lcd.setCursor(0, 0);
lcd.print("MQTT Connect...");
lcd.setCursor(0, 1);
lcd.print("NCUT-DB212211");
setup_wifi();
client.setServer(mqtt_server, 1883);
client.setCallback(callback);
}
void setup_wifi() {
delay(10);
Serial.print("📡 Wi-Fi 連線中: "); Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\n✅ Wi-Fi 已連線");
configTime(8 * 3600, 0, "pool.ntp.org", "time.nist.gov");
struct tm timeinfo;
while (!getLocalTime(&timeinfo)) { delay(500); }
Serial.println("✅ NTP 時間已同步");
}
void reconnect() {
while (!client.connected()) {
String clientId = "ESP32_NCUT_" + String(random(0xffff), HEX);
if (client.connect(clientId.c_str())) {
client.subscribe(topics[0]);
Serial.println("✅ MQTT 已連線");
} else {
Serial.print("❌ MQTT 失敗, rc="); Serial.print(client.state());
delay(5000);
}
}
}
void loop() {
if (!client.connected()) reconnect();
client.loop();
unsigned long now = millis();
// 監聽 Serial 鍵盤輸入指令
if (Serial.available() > 0) {
String serialCmd = Serial.readStringUntil('\n');
serialCmd.trim();
if (serialCmd.length() > 0) {
Serial.printf("\n⌨️ [Serial 手動輸入]: %s\n", serialCmd.c_str());
handleDirectCommand(serialCmd);
}
}
// 1. 每 1 秒執行一次的區塊 (更新 DHT、LCD、以及印出倒數計時)
if (now - lastMsgTime > 1000) {
lastMsgTime = now;
// 印出 LED 倒數計時
for (int i = 0; i < 4; i++) {
if (ledModes[i] == TIMER_COUNTDOWN && targetTimes[i] > now) {
int remainSec = (targetTimes[i] - now) / 1000 + 1;
Serial.printf("⏳ LED %d 倒數中: %d 秒\n", i + 1, remainSec);
}
}
struct tm timeinfo;
if (getLocalTime(&timeinfo)) {
char dateStr[11], timeStr[9];
strftime(dateStr, sizeof(dateStr), "%y/%m/%d", &timeinfo);
strftime(timeStr, sizeof(timeStr), "%H:%M:%S", &timeinfo);
// 讀取溫濕度
float h = dht.readHumidity();
float t = dht.readTemperature();
// 如果讀取成功 (不是 NaN)
if (!isnan(h) && !isnan(t)) {
static float last_t = -999.0;
static float last_h = -999.0;
static unsigned long last_publish_time = 0; // 記錄上次發送 MQTT 的時間
bool valueChanged = (t != last_t || h != last_h);
// 觸發發送的條件:1. 數值改變了 或 2. 距離上次發送超過 10 秒 (心跳機制)
if (valueChanged || (now - last_publish_time > 10000)) {
if (valueChanged) {
Serial.printf("[%s]*檢測到環境變動* 溫度: %.1f °C | 濕度: %.1f %%\n", timeStr, t, h);
last_t = t;
last_h = h;
} else {
// 這是強制發送的狀態,你可以選擇要不要印出來
// Serial.println("💓 [MQTT 心跳] 發送當前環境數值");
}
// 🌟 將發送動作移到這裡,只有符合條件時才發送 MQTT
client.publish(topics[1], String(t, 1).c_str());
client.publish(topics[2], String(h, 1).c_str());
last_publish_time = now; // 更新最後發送時間
}
// 更新 LCD 畫面 (不管數值有沒有變,LCD 都會維持每秒刷新時間)
lcd.setCursor(0, 0); lcd.print(dateStr); lcd.print(" T:");
lcd.print(t, 1); lcd.write((uint8_t)223);
lcd.setCursor(0, 1); lcd.print(timeStr); lcd.print(" H:");
lcd.print(h, 1); lcd.print("% ");
} else {
Serial.println("⚠️ 無法從 DHT 感測器讀取資料!");
}
}
}
// 2. LED 非阻塞狀態機
for (int i = 0; i < 4; i++) {
switch (ledModes[i]) {
case FLASHING:
if (now - lastFlashTimes[i] >= flashInterval) {
lastFlashTimes[i] = now;
digitalWrite(ledPins[i], !digitalRead(ledPins[i]));
}
break;
case TIMER_COUNTDOWN:
if (now >= targetTimes[i]) {
digitalWrite(ledPins[i], LOW); // 時間到關燈 (視繼電器高低電位調整)
ledModes[i] = IDLE;
Serial.printf("✅ LED %d 計時結束,已自動關閉\n", i + 1);
}
break;
case IDLE:
// 無動作
break;
}
}
}GND
LED
1
LED
2
LED
3
LED
4