Dưới đây là hướng dẫn toàn diện và mã nguồn đầy đủ cho ESP32 sử dụng Arduino IDE để điều khiển 8 relay theo lịch trình (thời gian, các ngày trong tuần), lưu trạng thái vào Preferences.h, đồng bộ dữ liệu cấu hình và gửi log lên server PHP/MySQL sử dụng ArduinoJson.h. ## Cấu trúc bảng MySQL Bạn cần tạo hai bảng trong cơ sở dữ liệu MySQL (qua phpMyAdmin) như sau: -- Bảng chứa lịch trình bật/tắt relayCREATE TABLE `relay_schedules` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `relay_id` INT NOT NULL, -- Từ 1 đến 8 `selected_time` TIME NOT NULL, -- Định dạng HH:MM:SS `selected_days` VARCHAR(50) NOT NULL, -- Định dạng: Sun,Mon,Tue `action` VARCHAR(10) NOT NULL -- "ON" hoặc "OFF" ); -- Bảng lưu trữ lịch sử hoạt động (Logs)CREATE TABLE `relay_logs` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `relay_id` INT NOT NULL, `status` VARCHAR(10) NOT NULL, -- "ON" hoặc "OFF" `timestamp` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Thêm dữ liệu mẫu cho lịch trìnhINSERT INTO `relay_schedules` (`relay_id`, `selected_time`, `selected_days`, `action`) VALUES (1, '08:00:00', 'Mon,Wed,Fri', 'ON'), (1, '17:00:00', 'Mon,Wed,Fri', 'OFF'), (2, '06:30:00', 'Sun,Sat', 'ON'); ------------------------------ ## Mã nguồn Backend (API PHP) Tạo một file PHP tên là api.php trên server của bạn để ESP32 tương tác. connect_error) { die(json_encode(["error" => "Kết nối thất bại"])); } $method = $_SERVER['REQUEST_METHOD']; // 1. ESP32 LẤY LỊCH TRÌNH (GET) if ($method === 'GET') { $sql = "SELECT relay_id, selected_time, selected_days, action FROM relay_schedules"; $result = $conn->query($sql); $schedules = []; while($row = $result->fetch_assoc()) { $schedules[] = $row; } echo json_encode($schedules); } // 2. ESP32 GỬI LOG TRẠNG THÁI (POST) if ($method === 'POST') { $input = json_decode(file_get_contents('php://input'), true); if (isset($input['relay_id']) && isset($input['status'])) { $relay_id = intval($input['relay_id']); $status = $conn->real_escape_string($input['status']); $sql = "INSERT INTO relay_logs (relay_id, status) VALUES ($relay_id, '$status')"; if ($conn->query($sql)) { echo json_encode(["success" => true]); } else { echo json_encode(["success" => false, "error" => $conn->error]); } } } $conn->close(); ?> ------------------------------ ## Mã nguồn Full cho ESP32 (Arduino C++) Thư viện cần cài đặt trước: ArduinoJson (phiên bản 6 hoặc 7). #include #include #include #include #include "time.h" // Cấu hình WiFi và APIconst char* ssid = "YOUR_WIFI_SSID";const char* password = "YOUR_WIFI_PASSWORD";const char* api_url = "http://YOUR_SERVER_IP/api.php"; // Thay bằng IP server của bạn // Cấu hình NTP Server để lấy thời gian thựcconst char* ntpServer = "pool.ntp.org";const long gmtOffset_sec = 7 * 3600; // Múi giờ Việt Nam (UTC+7)const int daylightOffset_sec = 0; // Khai báo chân GPIO cho 8 Relayconst int relayPins[8] = {23, 22, 21, 19, 18, 5, 17, 16}; // Khởi tạo Preferences (Lưu bộ nhớ Flash)Preferences preferences; // Cấu trúc dữ liệu lịch trìnhstruct Schedule { int relay_id; int hour; int minute; String days; String action; bool active; };#define MAX_SCHEDULES 20Schedule schedules[MAX_SCHEDULES];int scheduleCount = 0; // Biến điều khiển thời gian bằng millis() thay cho delay()unsigned long lastNTPCheck = 0;unsigned long lastFetchSchedules = 0;const unsigned long fetchInterval = 60000; // Cập nhật lịch trình từ server mỗi 60 giây void setup() { Serial.begin(115200); // Cấu hình các chân Relay preferences.begin("relay_status", false); for (int i = 0; i < 8; i++) { pinMode(relayPins[i], OUTPUT); // Đọc trạng thái cũ đã lưu trước khi mất điện (mặc định tắt: HIGH hoặc LOW tùy mạch relay) bool lastState = preferences.getBool(String(i + 1).c_str(), false); digitalWrite(relayPins[i], lastState ? LOW : HIGH); // Giả định Active Low (LOW là BẬT) } // 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 NTP configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); // Tải lịch trình lần đầu tiên fetchSchedulesFromServer(); } void loop() { unsigned long currentMillis = millis(); // Kiểm tra lịch trình để kích hoạt relay mỗi giây một lần static unsigned long lastActionCheck = 0; if (currentMillis - lastActionCheck >= 1000) { lastActionCheck = currentMillis; checkAndTriggerSchedules(); } // Định kỳ đồng bộ lại lịch trình mới từ Server MySQL if (currentMillis - lastFetchSchedules >= fetchInterval) { lastFetchSchedules = currentMillis; fetchSchedulesFromServer(); } } // Hàm lấy dữ liệu lịch trình JSON từ PHP và phân tíchvoid fetchSchedulesFromServer() { if (WiFi.status() != WL_CONNECTED) return; HTTPClient http; http.begin(api_url); int httpCode = http.GET(); if (httpCode == HTTP_CODE_OK) { String payload = http.getString(); // Khởi tạo tài liệu Json (Tùy biến kích thước theo số lịch trình) DynamicJsonDocument doc(4096); DeserializationError error = deserializeJson(doc, payload); if (!error) { JsonArray arr = doc.as(); scheduleCount = 0; for (JsonObject obj : arr) { if (scheduleCount >= MAX_SCHEDULES) break; schedules[scheduleCount].relay_id = obj["relay_id"]; // Tách chuỗi thời gian HH:MM:SS thành Giờ và Phút String timeStr = obj["selected_time"].as(); schedules[scheduleCount].hour = timeStr.substring(0, 2).toInt(); schedules[scheduleCount].minute = timeStr.substring(3, 5).toInt(); schedules[scheduleCount].days = obj["selected_days"].as(); schedules[scheduleCount].action = obj["action"].as(); schedules[scheduleCount].active = true; scheduleCount++; } Serial.printf("Đã cập nhật %d lịch trình từ Server.\n", scheduleCount); } } http.end(); } // Hàm kiểm tra thời gian thực và thực thi Relayvoid checkAndTriggerSchedules() { struct tm timeinfo; if (!getLocalTime(&timeinfo)) { Serial.println("Lỗi lấy thời gian NTP"); return; } // Mảng chuyển đổi thứ trong tuần từ cấu trúc `tm` sang định dạng chuỗi của bạn const char* daysOfWeek[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; String currentDay = daysOfWeek[timeinfo.tm_wday]; int currentHour = timeinfo.tm_hour; int currentMinute = timeinfo.tm_min; int currentSecond = timeinfo.tm_sec; // Chỉ kích hoạt chính xác tại giây thứ 00 để tránh trùng lặp lệnh liên tục trong 1 phút if (currentSecond != 0) return; for (int i = 0; i < scheduleCount; i++) { if (schedules[i].hour == currentHour && schedules[i].minute == currentMinute) { // Kiểm tra ngày hiện tại có nằm trong chuỗi cấu hình hay không (ví dụ "Mon,Wed" có chứa "Mon") if (schedules[i].days.indexOf(currentDay) >= 0) { int rId = schedules[i].relay_id; String act = schedules[i].action; if (rId >= 1 && rId <= 8) { bool targetState = (act == "ON"); int pin = relayPins[rId - 1]; // Đọc trạng thái hiện tại (đổi ngược lại logic do Relay active LOW) bool currentState = (digitalRead(pin) == LOW); if (currentState != targetState) { digitalWrite(pin, targetState ? LOW : HIGH); // Thực thi lệnh // Lưu trạng thái mới vào Preferences chống mất điện preferences.putBool(String(rId).c_str(), targetState); // Gửi dữ liệu Log lên Server sendLogToServer(rId, act); Serial.printf("Kích hoạt Relay %d -> %s thành công!\n", rId, act.c_str()); } } } } } } // Hàm POST dữ liệu log trạng thái lên MySQL thông qua API PHPvoid sendLogToServer(int relay_id, String status) { if (WiFi.status() != WL_CONNECTED) return; HTTPClient http; http.begin(api_url); http.addHeader("Content-Type", "application/json"); StaticJsonDocument<200> doc; doc["relay_id"] = relay_id; doc["status"] = status; String requestBody; serializeJson(doc, requestBody); int httpCode = http.POST(requestBody); if (httpCode > 0) { Serial.println("Đã gửi log lên Server thành công."); } http.end(); } ## Các điểm mấu chốt trong mã nguồn: 1. Preferences.h: Trạng thái của từng relay được lưu vào bộ nhớ Flash theo cặp Key-Value (ví dụ Key là "1", Value là true). Khi khởi động lại hoặc mất điện, ESP32 tự nạp lại trạng thái cuối cùng. 2. millis() thay cho delay(): Toàn bộ chu kỳ kiểm tra thời gian thực và đồng bộ dữ liệu API từ server đều chạy độc lập không làm nghẽn tiến trình của chip. 3. Xử lý chuỗi ngày: Hàm sử dụng tính năng .indexOf(currentDay) để kiểm tra ngày hiện tại (ví dụ: "Wed") có tồn tại trong danh sách chọn "Mon,Wed,Fri" hay không một cách linh hoạt. Bạn muốn tôi hướng dẫn chi tiết thêm về cách cấu hình mạch phần cứng kích mức thấp/cao (Active Low/High) hay bổ sung tính năng điều khiển thủ công qua nút nhấn không?