#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <DHT.h>
// --- 1. CẤU HÌNH WIFI WOKWI (BẮT BUỘC) ---
const char* ssid = "Wokwi-GUEST"; // Wifi ảo của Wokwi không cần pass
const char* password = "";
// --- 2. CẤU HÌNH MQTT HIVEMQ CLOUD ---
const char* mqtt_server = "dff8f7471d7745a6907092c74b9267e6.s1.eu.hivemq.cloud";
const int mqtt_port = 8883;
const char* mqtt_user = "Project220251";
const char* mqtt_pass = "Project220251";
WiFiClientSecure espClient;
PubSubClient client(espClient);
// --- 3. CẤU HÌNH CHÂN (ESP32 WOKWI) ---
#define DHTPIN 15 // Chân DATA của DHT22
#define RELAY_PIN 23 // Chân IN của Relay (Máy bơm)
#define SOIL_PIN 34 // Chân SIG của Biến trở (Giả lập cảm biến đất)
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
// --- 4. CẤU HÌNH NGƯỠNG & BIẾN ---
// ESP32 ADC là 12-bit (0 - 4095).
// Biến trở Wokwi: 0V -> 3.3V tương ứng 0 -> 4095
const int DRY = 4095; // Giả sử vặn hết cỡ là KHÔ
const int WET = 0; // Vặn về 0 là ƯỚT
const int START_PUMP = 40; // Dưới 40% thì bơm
const int STOP_PUMP = 80; // Trên 80% thì tắt (để khoảng rộng cho dễ test)
bool isAuto = true; // Mặc định chế độ Tự động
bool pumpState = false; // Trạng thái bơm
unsigned long lastMsg = 0;
// --- HÀM ĐỌC ĐỘ ẨM ĐẤT (GIẢ LẬP) ---
int readSoilPercent() {
int raw = analogRead(SOIL_PIN);
// Map giá trị 0-4095 sang 0-100%
// Lưu ý: Nếu vặn biến trở lên cao mà % giảm thì đảo vị trí DRY, WET trong hàm map
int pct = map(raw, WET, DRY, 100, 0);
return constrain(pct, 0, 100);
}
// --- HÀM XỬ LÝ TIN NHẮN MQTT ĐẾN ---
void callback(char* topic, byte* payload, unsigned int length) {
String message;
for (int i = 0; i < length; i++) {
message += (char)payload[i];
}
Serial.print("📩 Nhận lệnh ["); Serial.print(topic); Serial.print("]: ");
Serial.println(message);
// 1. Xử lý chuyển chế độ (AUTO / MANUAL)
if (String(topic) == "iot/cmd/mode") {
if (message == "AUTO") {
isAuto = true;
Serial.println("=> Chế độ: TỰ ĐỘNG");
} else if (message == "MANUAL") {
isAuto = false;
Serial.println("=> Chế độ: THỦ CÔNG");
}
// Gửi xác nhận ngược lại cho Web cập nhật UI
client.publish("iot/mode", isAuto ? "AUTO" : "MANUAL");
}
// 2. Xử lý bật tắt bơm (Chỉ hoạt động khi ở chế độ MANUAL)
if (String(topic) == "iot/cmd/pump") {
if (!isAuto) { // Chỉ cho phép điều khiển khi đang tắt Auto
if (message == "ON") {
pumpState = true;
Serial.println("=> BẬT BƠM (Thủ công)");
} else if (message == "OFF") {
pumpState = false;
Serial.println("=> TẮT BƠM (Thủ công)");
}
digitalWrite(RELAY_PIN, pumpState ? HIGH : LOW);
client.publish("iot/pump", pumpState ? "ON" : "OFF");
} else {
Serial.println("⚠️ Đang ở chế độ AUTO, không thể điều khiển thủ công!");
}
}
}
void reconnect() {
while (!client.connected()) {
Serial.print("Đang kết nối MQTT...");
// Tạo ID ngẫu nhiên để server không đá kết nối cũ
String clientId = "ESP32-Wokwi-" + String(random(0xffff), HEX);
if (client.connect(clientId.c_str(), mqtt_user, mqtt_pass)) {
Serial.println("Thành công!");
// Đăng ký nhận lệnh từ Web
client.subscribe("iot/cmd/mode");
client.subscribe("iot/cmd/pump");
} else {
Serial.print("Lỗi, rc=");
Serial.print(client.state());
Serial.println(" thử lại sau 5s");
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
pinMode(RELAY_PIN, OUTPUT);
pinMode(SOIL_PIN, INPUT);
dht.begin();
// Kết nối Wifi
Serial.print("Kết nối Wifi Wokwi...");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println(" Đã kết nối!");
// Cấu hình SSL cho Wokwi (Bỏ qua chứng chỉ vì giả lập không hỗ trợ root CA đầy đủ)
espClient.setInsecure();
client.setServer(mqtt_server, mqtt_port);
client.setCallback(callback);
}
void loop() {
if (!client.connected()) reconnect();
client.loop();
unsigned long now = millis();
// --- LOGIC ĐIỀU KHIỂN BƠM (Thời gian thực) ---
int soilPct = readSoilPercent();
if (isAuto) {
// Chế độ Tự động: Đất khô (<40%) thì bơm, Đất ẩm (>80%) thì tắt
if (soilPct < START_PUMP && !pumpState) {
pumpState = true;
Serial.println("💧 Đất khô -> Tự động BẬT BƠM");
digitalWrite(RELAY_PIN, HIGH);
client.publish("iot/pump", "ON");
}
else if (soilPct > STOP_PUMP && pumpState) {
pumpState = false;
Serial.println("✅ Đất ẩm -> Tự động TẮT BƠM");
digitalWrite(RELAY_PIN, LOW);
client.publish("iot/pump", "OFF");
}
}
// Chế độ Manual: Giữ nguyên trạng thái do người dùng bấm nút (xử lý ở callback)
// --- GỬI DỮ LIỆU ĐỊNH KỲ (5 giây/lần) ---
if (now - lastMsg > 5000) {
lastMsg = now;
float h = dht.readHumidity();
float t = dht.readTemperature();
// Kiểm tra lỗi cảm biến
if (isnan(h) || isnan(t)) {
Serial.println("❌ Lỗi đọc DHT sensor!");
return;
}
// In ra Serial để debug
Serial.printf("[Sensor] Temp: %.1f°C | Hum: %.1f%% | Soil: %d%%\n", t, h, soilPct);
// Gửi lên MQTT
client.publish("iot/temp", String(t, 1).c_str());
client.publish("iot/hum", String(h, 1).c_str());
client.publish("iot/soil", String(soilPct).c_str());
// Gửi lại trạng thái bơm và mode để Web đồng bộ (nếu lỡ web mới vào)
client.publish("iot/pump", pumpState ? "ON" : "OFF");
client.publish("iot/mode", isAuto ? "AUTO" : "MANUAL");
}
}