-- phpMyAdmin SQL Dump -- version 5.2.2 -- https://www.phpmyadmin.net/ -- -- Máy chủ: localhost:3306 -- Thời gian đã tạo: Th7 07, 2026 lúc 03:15 PM -- Phiên bản máy phục vụ: 10.11.18-MariaDB-cll-lve-log -- Phiên bản PHP: 8.4.22 SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; START TRANSACTION; SET time_zone = "+00:00"; /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; /*!40101 SET NAMES utf8mb4 */; -- -- Cơ sở dữ liệu: `nongngh8_hivemq` -- -- -------------------------------------------------------- -- -- Cấu trúc bảng cho bảng `relay_log` -- CREATE TABLE `relay_log` ( `id` int(11) NOT NULL, `relay_id` int(11) NOT NULL, `status` varchar(10) NOT NULL, `timestamp` timestamp NULL DEFAULT current_timestamp() ) ENGINE=MyISAM DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci; -- -- Đang đổ dữ liệu cho bảng `relay_log` -- INSERT INTO `relay_log` (`id`, `relay_id`, `status`, `timestamp`) VALUES (1, 1, 'ON', '2026-07-07 14:51:19'), (2, 1, 'OFF', '2026-07-07 14:51:25'), (3, 1, 'ON', '2026-07-07 14:54:02'), (4, 1, 'ON', '2026-07-07 14:54:38'), (5, 5, 'ON', '2026-07-07 14:57:55'); -- -------------------------------------------------------- -- -- Cấu trúc bảng cho bảng `relay_preferences` -- CREATE TABLE `relay_preferences` ( `relay_id` int(11) NOT NULL, `selected_time` time NOT NULL, `selected_days` set('Sun','Mon','Tue','Wed','Thu','Fri','Sat') NOT NULL, `active` tinyint(1) DEFAULT 1 ) ENGINE=MyISAM DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci; -- -- Đang đổ dữ liệu cho bảng `relay_preferences` -- INSERT INTO `relay_preferences` (`relay_id`, `selected_time`, `selected_days`, `active`) VALUES (1, '21:54:00', 'Sun,Mon,Tue,Wed,Thu,Fri,Sat', 1), (2, '09:30:00', 'Tue,Thu,Sat', 1), (3, '12:00:00', 'Sun,Sat', 1), (4, '17:00:00', 'Mon,Tue,Wed,Thu,Fri', 1), (5, '22:03:00', 'Sun,Mon,Tue,Wed,Thu,Fri,Sat', 0), (6, '22:00:00', 'Fri,Sat', 0), (7, '15:15:00', 'Wed', 1), (8, '18:45:00', 'Sun,Thu', 1); -- -------------------------------------------------------- -- -- Cấu trúc bảng cho bảng `sensor_log` -- CREATE TABLE `sensor_log` ( `id` int(11) NOT NULL, `sensor_id` int(11) NOT NULL, `flow_rate` float NOT NULL, `total_liters` float NOT NULL, `timestamp` timestamp NULL DEFAULT current_timestamp() ) ENGINE=MyISAM DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci; -- -- Đang đổ dữ liệu cho bảng `sensor_log` -- INSERT INTO `sensor_log` (`id`, `sensor_id`, `flow_rate`, `total_liters`, `timestamp`) VALUES (1, 1, 0, 0, '2026-07-07 14:42:05'); -- -- Chỉ mục cho các bảng đã đổ -- -- -- Chỉ mục cho bảng `relay_log` -- ALTER TABLE `relay_log` ADD PRIMARY KEY (`id`); -- -- Chỉ mục cho bảng `relay_preferences` -- ALTER TABLE `relay_preferences` ADD PRIMARY KEY (`relay_id`); -- -- Chỉ mục cho bảng `sensor_log` -- ALTER TABLE `sensor_log` ADD PRIMARY KEY (`id`); -- -- AUTO_INCREMENT cho các bảng đã đổ -- -- -- AUTO_INCREMENT cho bảng `relay_log` -- ALTER TABLE `relay_log` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6; -- -- AUTO_INCREMENT cho bảng `sensor_log` -- ALTER TABLE `sensor_log` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=209; COMMIT; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; -------------------- connect_error) { die(json_encode(["status" => "error", "message" => "Database connection failed"])); } $method = $_SERVER['REQUEST_METHOD']; if ($method === 'GET') { // Trả về cấu hình lịch trình cho ESP32 $sql = "SELECT relay_id, selected_time, selected_days, active FROM relay_preferences"; $result = $conn->query($sql); $preferences = []; while($row = $result->fetch_assoc()) { $row['relay_id'] = (int)$row['relay_id']; $row['active'] = (int)$row['active']; $preferences[] = $row; } echo json_encode($preferences); } elseif ($method === 'POST') { // Nhận log từ ESP32 $input = json_decode(file_get_contents('php://input'), true); if (!$input) { echo json_encode(["status" => "error", "message" => "Invalid JSON"]); exit; } if (isset($input['type'])) { if ($input['type'] === 'relay') { $stmt = $conn->prepare("INSERT INTO relay_log (relay_id, status) VALUES (?, ?)"); $stmt->bind_param("is", $input['relay_id'], $input['status']); $stmt->execute(); echo json_encode(["status" => "success"]); } elseif ($input['type'] === 'sensor') { $stmt = $conn->prepare("INSERT INTO sensor_log (sensor_id, flow_rate, total_liters) VALUES (?, ?, ?)"); foreach ($input['data'] as $sensor) { $stmt->bind_param("idd", $sensor['sensor_id'], $sensor['flow_rate'], $sensor['total_liters']); $stmt->execute(); } echo json_encode(["status" => "success"]); } } } $conn->close(); ?> -------------------- #include #include #include #include #include #include "time.h" // Cấu hình mạng WiFi const char* ssid = "XM"; const char* password = "79797979"; // Đường dẫn URL API HTTPS (Thay domain của bạn) const char* serverApiUrl = "https://hivemq.nongnghiep24h.com/api.php"; // Chứng chỉ SSL Root CA (Ví dụ mẫu, hãy thay bằng Root CA thực tế của Web Server của bạn) const char* ROOT_CA = "-----BEGIN CERTIFICATE-----\n" "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\n" "-----END CERTIFICATE-----"; // Khai báo chân GPIO cho 8 Relay const int relayPins[8] = {15, 2, 4, 5, 18, 19, 21, 22};//15, 2, 4, 5, 18, 19, 21, 22 // Khai báo chân GPIO cho 8 cảm biến lưu lượng YF-S201 (Cần các chân hỗ trợ Interrupt) const int sensorPins[8] = {13, 12, 14, 27, 26, 25, 33, 32}; // Biến lưu số xung đếm được từ Interrupt của cảm biến volatile unsigned long pulseCounts[8] = {0, 0, 0, 0, 0, 0, 0, 0}; // Cấu trúc lưu trữ lịch trình Relay struct RelaySchedule { int hour; int minute; String days; // Dạng chuỗi: "Sun,Mon,Tue" bool active; bool triggeredToday; }; RelaySchedule schedules[8]; Preferences preferences; unsigned long lastSensorReport = 0; unsigned long lastScheduleSync = 0; // Các hàm xử lý ngắt (ISR) cho 8 cảm biến độc lập void IRAM_ATTR isr_s1() { pulseCounts[0]++; } void IRAM_ATTR isr_s2() { pulseCounts[1]++; } void IRAM_ATTR isr_s3() { pulseCounts[2]++; } void IRAM_ATTR isr_s4() { pulseCounts[3]++; } void IRAM_ATTR isr_s5() { pulseCounts[4]++; } void IRAM_ATTR isr_s6() { pulseCounts[5]++; } void IRAM_ATTR isr_s7() { pulseCounts[6]++; } void IRAM_ATTR isr_s8() { pulseCounts[7]++; } void setup() { Serial.begin(115200); // Cấu hình chân Relay là OUTPUT và mặc định TẮT (HIGH/LOW tùy thuộc mạch relay kích mức nào) for (int i = 0; i < 8; i++) { pinMode(relayPins[i], OUTPUT); digitalWrite(relayPins[i], HIGH); // Thường là ngắt relay mức cao } // Cấu hình chân cảm biến dòng chảy với INPUT_PULLUP for (int i = 0; i < 8; i++) { pinMode(sensorPins[i], INPUT_PULLUP); } // Gắn Interrupt cho từng cảm biến attachInterrupt(digitalPinToInterrupt(sensorPins[0]), isr_s1, RISING); attachInterrupt(digitalPinToInterrupt(sensorPins[1]), isr_s2, RISING); attachInterrupt(digitalPinToInterrupt(sensorPins[2]), isr_s3, RISING); attachInterrupt(digitalPinToInterrupt(sensorPins[3]), isr_s4, RISING); attachInterrupt(digitalPinToInterrupt(sensorPins[4]), isr_s5, RISING); attachInterrupt(digitalPinToInterrupt(sensorPins[5]), isr_s6, RISING); attachInterrupt(digitalPinToInterrupt(sensorPins[6]), isr_s7, RISING); attachInterrupt(digitalPinToInterrupt(sensorPins[8]), isr_s8, 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 lấy thời gian thực từ NTP Server máy chủ Việt Nam/Quốc tế configTime(7 * 3600, 0, "pool.ntp.org", "time.nist.gov"); // Đọc lịch trình cũ đã lưu trong bộ nhớ Flash của ESP32 đề phòng mất mạng loadLocalSchedules(); // Đồng bộ lịch trình mới nhất từ Server qua HTTPS syncSchedulesFromServer(); } void loop() { // 1. Kiểm tra lịch trình Relay mỗi giây checkAndControlRelays(); // 2. Định kỳ mỗi 30 giây gửi log dữ liệu cảm biến nước lên Server if (millis() - lastSensorReport > 30000) { reportSensors(); lastSensorReport = millis(); } // 3. Định kỳ mỗi 15 phút đồng bộ lại lịch trình mới từ phpMyAdmin if (millis() - lastScheduleSync > 10000) { syncSchedulesFromServer(); lastScheduleSync = millis(); } } // Đồng bộ cấu hình từ Server và lưu vĩnh viễn vào bộ nhớ Preferences NVS void syncSchedulesFromServer() { if (WiFi.status() != WL_CONNECTED) return; WiFiClientSecure client; client.setCACert(ROOT_CA); HTTPClient http; if (http.begin(client, serverApiUrl)) { int httpCode = http.GET(); if (httpCode == HTTP_CODE_OK) { String payload = http.getString(); // Khởi tạo bộ nhớ Preferences preferences.begin("sched", false); StaticJsonDocument<2048> doc; DeserializationError error = deserializeJson(doc, payload); if (!error) { JsonArray arr = doc.as(); int i = 0; for (JsonObject repo : arr) { if(i >= 8) break; int r_id = repo["relay_id"]; String r_time = repo["selected_time"]; // "08:00:00" String r_days = repo["selected_days"]; // "Sun,Mon,Tue" bool active = repo["active"]; int hour = r_time.substring(0, 2).toInt(); int minute = r_time.substring(3, 5).toInt(); schedules[i] = {hour, minute, r_days, active, false}; // Lưu trữ lại vào Flash String key = "r" + String(i); preferences.putInt((key+"h").c_str(), hour); preferences.putInt((key+"m").c_str(), minute); preferences.putString((key+"d").c_str(), r_days); preferences.putBool((key+"a").c_str(), active); i++; } Serial.println("Schedules synced and stored to NVS."); } preferences.end(); } http.end(); } } // Đọc dữ liệu lịch trình cục bộ khi khởi động không có mạng void loadLocalSchedules() { preferences.begin("sched", true); for (int i = 0; i < 8; i++) { String key = "r" + String(i); schedules[i].hour = preferences.getInt((key+"h").c_str(), 0); schedules[i].minute = preferences.getInt((key+"m").c_str(), 0); schedules[i].days = preferences.getString((key+"d").c_str(), ""); schedules[i].active = preferences.getBool((key+"a").c_str(), false); schedules[i].triggeredToday = false; } preferences.end(); Serial.println("Local schedules loaded."); } // Kiểm tra thời gian thực và kích hoạt Relay theo ngày trong tuần void checkAndControlRelays() { struct tm timeinfo; if (!getLocalTime(&timeinfo)) return; char dayStr[4]; strftime(dayStr, sizeof(dayStr), "%a", &timeinfo); // Lấy định dạng: Sun, Mon, Tue... int currentHour = timeinfo.tm_hour; int currentMinute = timeinfo.tm_min; // Reset trạng thái kích hoạt vào lúc nửa đêm if (currentHour == 0 && currentMinute == 0) { for (int i = 0; i < 8; i++) schedules[i].triggeredToday = false; } for (int i = 0; i < 8; i++) { if (!schedules[i].active) continue; // Kiểm tra xem hôm nay có nằm trong danh sách các ngày được lựa chọn không if (schedules[i].days.indexOf(dayStr) >= 0) { if (currentHour == schedules[i].hour && currentMinute == schedules[i].minute && !schedules[i].triggeredToday) { // Kích hoạt Relay bật trong vòng 5 giây (Ví dụ tưới nước tự động) digitalWrite(relayPins[i], LOW); // Bật relay mức thấp sendLogToServer("relay", i + 1, "ON"); delay(5000); digitalWrite(relayPins[i], HIGH); // Tắt relay sendLogToServer("relay", i + 1, "OFF"); schedules[i].triggeredToday = true; } } } } // Gửi Log hoạt động Relay bằng HTTPS POST JSON void sendLogToServer(String type, int relayId, String status) { if (WiFi.status() != WL_CONNECTED) return; WiFiClientSecure client; client.setCACert(ROOT_CA); HTTPClient http; StaticJsonDocument<200> doc; doc["type"] = type; doc["relay_id"] = relayId; doc["status"] = status; String jsonStr; serializeJson(doc, jsonStr); if (http.begin(client, serverApiUrl)) { http.addHeader("Content-Type", "application/json"); int httpCode = http.POST(jsonStr); http.end(); } } // Đọc xung từ 8 cảm biến YF-S201, quy đổi sang lít và đẩy lên Server void reportSensors() { if (WiFi.status() != WL_CONNECTED) return; WiFiClientSecure client; client.setCACert(ROOT_CA); HTTPClient http; StaticJsonDocument<1500> doc; doc["type"] = "sensor"; JsonArray dataArr = doc.createNestedArray("data"); for (int i = 0; i < 8; i++) { // Đọc giá trị xung một cách an toàn bằng cách tạm ngắt Interrupt ngắn hạn noInterrupts(); unsigned long pulses = pulseCounts[i]; pulseCounts[i] = 0; // Reset số xung chuẩn bị cho chu kỳ tiếp theo interrupts(); // Công thức tính toán cho YF-S201: Tần số xung (Hz) = 7.5 * FlowRate (L/min) // Ở đây chu kỳ đo là 30 giây: Q = (Xung / 30) / 7.5 (L/min) float flowRate = (pulses / 30.0) / 7.5; float totalLiters = pulses / 450.0; // YF-S201 có thông số xấp xỉ 450 xung thu được cho 1 lít nước JsonObject sensorObj = dataArr.createNestedObject(); sensorObj["sensor_id"] = i + 1; sensorObj["flow_rate"] = flowRate; sensorObj["total_liters"] = totalLiters; } String jsonStr; serializeJson(doc, jsonStr); if (http.begin(client, serverApiUrl)) { http.addHeader("Content-Type", "application/json"); int httpCode = http.POST(jsonStr); http.end(); } }