CREATE DATABASE IF NOT EXISTS smart_control; USE smart_control; -- 1. Bảng cấu hình lịch trình Relay CREATE TABLE IF NOT EXISTS relay_config ( id INT AUTO_INCREMENT PRIMARY KEY, relay_id INT NOT NULL, -- Từ 1 đến 8 mode ENUM('auto', 'manual') NOT NULL DEFAULT 'auto', selected_time TIME NOT NULL, -- Ví dụ: '08:30:00' selected_days VARCHAR(100) NOT NULL, -- Ví dụ: 'Sun,Mon,Wed' action ENUM('ON', 'OFF') NOT NULL, active TINYINT(1) NOT NULL DEFAULT 1 -- 1 = Kích hoạt, 0 = Tắt cấu hình ); -- 2. Bảng lưu lịch sử thay đổi trạng thái Relay CREATE TABLE IF NOT EXISTS relay_logs ( id INT AUTO_INCREMENT PRIMARY KEY, relay_id INT NOT NULL, status ENUM('ON', 'OFF') NOT NULL, changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 3. Bảng lưu dữ liệu cảm biến lưu lượng nước YF-S201 CREATE TABLE IF NOT EXISTS sensor_yfs201 ( id INT AUTO_INCREMENT PRIMARY KEY, sensor_id INT NOT NULL, -- Từ 1 đến 8 flow_rate FLOAT NOT NULL, -- Lít/phút (L/min) total_liters FLOAT NOT NULL, -- Tổng số lít đã chảy qua recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Thêm dữ liệu mẫu cho bảng cấu hình INSERT INTO relay_config (relay_id, mode, selected_time, selected_days, action, active) VALUES (1, 'auto', '06:00:00', 'Mon,Wed,Fri', 'ON', 1), (1, 'auto', '06:30:00', 'Mon,Wed,Fri', 'OFF', 1), (2, 'manual', '00:00:00', 'Sun,Mon,Tue,Wed,Thu,Fri,Sat', 'ON', 1); ------------------------------------------------------------------ connect_error) { die(json_encode(["error" => "Kết nối CSDL thất bại: " . $conn->connect_error])); } $method = $_SERVER['REQUEST_METHOD']; // TÁC VỤ 1: ESP32 lấy lịch trình cấu hình (GET Request) if ($method === 'GET') { $sql = "SELECT relay_id, mode, selected_time, selected_days, action FROM relay_config WHERE active = 1"; $result = $conn->query($sql); $configs = []; while($row = $result->fetch_assoc()) { $configs[] = $row; } echo json_encode(["schedules" => $configs]); } // TÁC VỤ 2: ESP32 gửi dữ liệu Logs và Sensors lên Server (POST Request) elseif ($method === 'POST') { $input = file_get_contents('php://input'); $data = json_decode($input, true); if (!$data) { echo json_encode(["status" => "error", "message" => "Dữ liệu JSON không hợp lệ"]); exit; } // Xử lý lưu Relay Logs if (isset($data['relay_logs']) && is_array($data['relay_logs'])) { $stmtLog = $conn->prepare("INSERT INTO relay_logs (relay_id, status) VALUES (?, ?)"); foreach ($data['relay_logs'] as $log) { $stmtLog->bind_param("is", $log['relay_id'], $log['status']); $stmtLog->execute(); } $stmtLog->close(); } // Xử lý lưu Dữ liệu cảm biến YF-S201 if (isset($data['sensor_data']) && is_array($data['sensor_data'])) { $stmtSensor = $conn->prepare("INSERT INTO sensor_yfs201 (sensor_id, flow_rate, total_liters) VALUES (?, ?, ?)"); foreach ($data['sensor_data'] as $sensor) { $stmtSensor->bind_param("idd", $sensor['sensor_id'], $sensor['flow_rate'], $sensor['total_liters']); $stmtSensor->execute(); } $stmtSensor->close(); } echo json_encode(["status" => "success", "message" => "Đã cập nhật dữ liệu thành công"]); } $conn->close(); ?> ------------------------------------------------------------------ #include #include #include #include #include "time.h" // Cấu hình WiFi & API const char* ssid = "XM"; const char* password = "79797979"; const char* apiUrl = "http://hivemq.nongnghiep24h.com/api.php"; // Cấu hình NTP Server const char* ntpServer = "pool.ntp.org"; const long gmtOffset_sec = 7 * 3600; // GMT+7 const int daylightOffset_sec = 0; // Cấu hình cấu trúc dữ liệu để lưu lịch trình Auto từ Web Server struct Schedule { int relay_id; char mode[10]; char selected_time[10]; // Định dạng "HH:MM" hoặc "HH:MM:SS" từ MySQL char selected_days[100]; // Dạng "Sun,Mon,Tue..." char action[5]; // "ON" hoặc "OFF" }; #define MAX_SCHEDULES 30 Schedule activeSchedules[MAX_SCHEDULES]; int scheduleCount = 0; // Khai báo chân GPIO cho 8 Relay const int relayPins[8] = {15, 2, 0, 4, 5, 18, 19, 21}; bool relayStates[8] = {false, false, false, false, false, false, false, false}; // Khai báo chân GPIO cho 8 cảm biến YF-S201 const int sensorPins[8] = {13, 12, 14, 26, 25, 33, 32, 35}; volatile unsigned long LoggedPulses[8] = {0, 0, 0, 0, 0, 0, 0, 0}; float flowRates[8] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; float totalLiters[8] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; // Quản lý thời gian bằng millis() unsigned long lastFlowMillis = 0; unsigned long lastSyncMillis = 0; unsigned long lastPostMillis = 0; unsigned long lastCheckScheduleMillis = 0; // Biến cờ ngăn chặn kích hoạt lặp lại nhiều lần trong cùng một phút int lastTriggeredMinute = -1; Preferences preferences; // Định nghĩa các hàm đọc xung ngắt ISR cho cảm biến dòng chảy void IRAM_ATTR isr0() { LoggedPulses[0]++; } void IRAM_ATTR isr1() { LoggedPulses[1]++; } void IRAM_ATTR isr2() { LoggedPulses[2]++; } void IRAM_ATTR isr3() { LoggedPulses[3]++; } void IRAM_ATTR isr4() { LoggedPulses[4]++; } void IRAM_ATTR isr5() { LoggedPulses[5]++; } void IRAM_ATTR isr6() { LoggedPulses[6]++; } void IRAM_ATTR isr7() { LoggedPulses[7]++; } void setup() { Serial.begin(115200); // Khởi tạo các chân Relay preferences.begin("relays", false); for (int i = 0; i < 8; i++) { pinMode(relayPins[i], OUTPUT); String key = "r" + String(i); relayStates[i] = preferences.getBool(key.c_str(), false); digitalWrite(relayPins[i], relayStates[i] ? HIGH : LOW); } // Khởi tạo chân cảm biến YF-S201 và đính kèm ngắt ISR void (*isrFunctions[8])() = {isr0, isr1, isr2, isr3, isr4, isr5, isr6, isr7}; for (int i = 0; i < 8; i++) { pinMode(sensorPins[i], INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(sensorPins[i]), isrFunctions[i], RISING); } // Kết nối WiFi WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("\nWiFi Connected!"); // Cấu hình thời gian thực mạng NTP configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); // Đồng bộ lịch trình từ server ngay khi khởi động fetchSchedules(); } void loop() { unsigned long currentMillis = millis(); // THỰC THI 1: Tính toán lưu lượng nước mỗi 1 giây if (currentMillis - lastFlowMillis >= 1000) { calculateFlow(); lastFlowMillis = currentMillis; } // THỰC THI 2: Đồng bộ hóa lịch trình tự động từ database mỗi 5 phút if (currentMillis - lastSyncMillis >= 10000) { fetchSchedules(); lastSyncMillis = currentMillis; } // THỰC THI 3: Kiểm tra điều kiện thời gian để kích hoạt Relay (Quét mỗi 1 giây) if (currentMillis - lastCheckScheduleMillis >= 1000) { checkTimeSchedules(); lastCheckScheduleMillis = currentMillis; } // THỰC THI 4: Gửi dữ liệu Sensor & Log lên HTTP Server định kỳ mỗi 30 giây if (currentMillis - lastPostMillis >= 1000) { sendDataToServer(); lastPostMillis = currentMillis; } } // Hàm tính lưu lượng nước cho 8 cảm biến YF-S201 void calculateFlow() { for (int i = 0; i < 8; i++) { noInterrupts(); unsigned long pulseCount = LoggedPulses[i]; LoggedPulses[i] = 0; interrupts(); // Công thức YF-S201: F (Hz) = 7.5 * Q (L/min) flowRates[i] = ((float)pulseCount) / 7.5; totalLiters[i] += (flowRates[i] / 60.0); if(pulseCount > 0) { Serial.printf("Sensor %d Flow Rate: %.2f L/min, Total: %.2f L\n", i+1, flowRates[i], totalLiters[i]); } } } // Lấy chuỗi viết tắt ngày trong tuần (Sun, Mon, Tue...) từ hệ thống NTP String getDayOfWeekString(struct tm timeinfo) { char dayStr[4]; strftime(dayStr, sizeof(dayStr), "%a", &timeinfo); return String(dayStr); } // Kiểm tra lịch trình thời gian thực (Xử lý chế độ AUTO) void checkTimeSchedules() { struct tm timeinfo; if(!getLocalTime(&timeinfo)){ Serial.println("Lỗi: Không lấy được thời gian từ NTP"); return; } // Lấy chuỗi Giờ:Phút để so sánh char currentTimeStr[6]; strftime(currentTimeStr, sizeof(currentTimeStr), "%H:%M", &timeinfo); String currentDay = getDayOfWeekString(timeinfo); // Chỉ cho phép kiểm tra lệnh một lần duy nhất khi bước sang phút mới if (timeinfo.tm_min == lastTriggeredMinute) { return; } bool triggeredAny = false; // Vòng lặp kiểm tra toàn bộ danh sách lịch trình đã lưu từ Database for (int i = 0; i < scheduleCount; i++) { if (strcmp(activeSchedules[i].mode, "auto") == 0) { // So sánh khớp chuỗi thời gian "HH:MM" (Chỉ so sánh 5 ký tự đầu để bỏ qua phần giây nếu có từ SQL) if (strncmp(activeSchedules[i].selected_time, currentTimeStr, 5) == 0) { // Kiểm tra xem hôm nay có nằm trong danh sách các ngày được chọn không String daysAllowed = String(activeSchedules[i].selected_days); if (daysAllowed.indexOf(currentDay) >= 0) { int r_idx = activeSchedules[i].relay_id - 1; // ID dạng 1-8 chuyển sang Index 0-7 if (r_idx >= 0 && r_idx < 8) { bool actionState = (strcmp(activeSchedules[i].action, "ON") == 0); Serial.printf("[AUTO TẬP LỆNH] Khớp lịch trình! Kích hoạt Relay %d -> %s vào lúc %s (%s)\n", activeSchedules[i].relay_id, activeSchedules[i].action, currentTimeStr, currentDay.c_str()); controlRelay(r_idx, actionState); triggeredAny = true; } } } } } // Nếu có bất kì lịch trình nào khớp và chạy thành công, khóa phút này lại if (triggeredAny) { lastTriggeredMinute = timeinfo.tm_min; } } // Hàm GET: Lấy toàn bộ lịch trình từ PHP Web Server lưu vào mảng Struct RAM void fetchSchedules() { if (WiFi.status() != WL_CONNECTED) return; HTTPClient http; http.begin(apiUrl); int httpCode = http.GET(); if (httpCode == HTTP_CODE_OK) { String payload = http.getString(); Serial.println("\n--- Syncing Schedules From Server ---"); Serial.println(payload); JsonDocument doc; DeserializationError error = deserializeJson(doc, payload); if (!error) { JsonArray schedules = doc["schedules"]; scheduleCount = 0; // Reset số lượng lịch trình cũ for (JsonObject item : schedules) { if (scheduleCount >= MAX_SCHEDULES) break; activeSchedules[scheduleCount].relay_id = item["relay_id"]; strlcpy(activeSchedules[scheduleCount].mode, item["mode"] | "auto", sizeof(activeSchedules[scheduleCount].mode)); strlcpy(activeSchedules[scheduleCount].selected_time, item["selected_time"] | "00:00", sizeof(activeSchedules[scheduleCount].selected_time)); strlcpy(activeSchedules[scheduleCount].selected_days, item["selected_days"] | "", sizeof(activeSchedules[scheduleCount].selected_days)); strlcpy(activeSchedules[scheduleCount].action, item["action"] | "OFF", sizeof(activeSchedules[scheduleCount].action)); // Nếu bản ghi ghi nhận chế độ "manual", ra lệnh đổi trạng thái lập tức if (strcmp(activeSchedules[scheduleCount].mode, "manual") == 0) { int r_idx = activeSchedules[scheduleCount].relay_id - 1; if (r_idx >= 0 && r_idx < 8) { bool newState = (strcmp(activeSchedules[scheduleCount].action, "ON") == 0); controlRelay(r_idx, newState); } } scheduleCount++; } Serial.printf("Đã nạp thành công %d lịch trình vào bộ nhớ.\n", scheduleCount); } else { Serial.print("Lỗi parse JSON: "); Serial.println(error.c_str()); } } else { Serial.printf("Lỗi kết nối HTTP GET: %d\n", httpCode); } http.end(); } // Điều khiển bật tắt relay vật lý và lưu vào bộ nhớ Flash qua Preferences.h void controlRelay(int index, bool state) { if (index < 0 || index >= 8) return; // Bảo vệ mảng băm bộ nhớ ngoài phạm vi if (relayStates[index] != state) { relayStates[index] = state; digitalWrite(relayPins[index], state ? HIGH : LOW); // Lưu vào bộ nhớ Flash non-volatile String key = "r" + String(index); preferences.putBool(key.c_str(), state); Serial.printf("[SYSTEM] Relay %d chuyển trạng thái -> %s\n", index + 1, state ? "ON" : "OFF"); } } // Hàm POST: Đóng gói Json gửi Log và dữ liệu cảm biến lưu lượng nước void sendDataToServer() { if (WiFi.status() != WL_CONNECTED) return; HTTPClient http; http.begin(apiUrl); http.addHeader("Content-Type", "application/json"); JsonDocument doc; // Đóng gói mảng relay logs JsonArray relayLogs = doc["relay_logs"].to(); for(int i = 0; i < 8; i++) { JsonObject logNode = relayLogs.add(); logNode["relay_id"] = i + 1; logNode["status"] = relayStates[i] ? "ON" : "OFF"; } // Đóng gói mảng dữ liệu cảm biến lưu lượng nước YF-S201 JsonArray sensorData = doc["sensor_data"].to(); for(int i = 0; i < 8; i++) { JsonObject sNode = sensorData.add(); sNode["sensor_id"] = i + 1; sNode["flow_rate"] = flowRates[i]; sNode["total_liters"] = totalLiters[i]; } String requestBody; serializeJson(doc, requestBody); Serial.println("\n--- Gửi Dữ Liệu Lên Server (POST) ---"); Serial.println(requestBody); int httpResponseCode = http.POST(requestBody); if(httpResponseCode > 0) {String response = http.getString(); Serial.print("Phản hồi từ Server: "); Serial.println(response); } else { Serial.printf("Lỗi gửi dữ liệu POST: %s\n", http.errorToString(httpResponseCode).c_str()); } http.end(); }