Đây là giải pháp toàn diện bao gồm thiết kế cấu trúc database MySQL/phpMyAdmin, mã nguồn backend PHP API, và mã nguồn firmware ESP32 (Arduino IDE) sử dụng thư viện Preferences.h và ArduinoJson.h để tải lịch trình, kiểm tra thời gian thực (NTP) theo ngày trong tuần [Sun, Mon, Tue, Wed, Thu, Fri, Sat], điều khiển 8 relay và gửi log trạng thái ngược lên server qua giao thức HTTPS SSL bảo mật (Root CA). [1] ------------------------------ ## 1. Cấu trúc Database (MySQL / phpMyAdmin) Tạo 2 bảng trong cơ sở dữ liệu của bạn để lưu cấu hình lịch trình bật/tắt relay và lưu vết lịch sử (log). -- 1. Bảng lưu cấu hình lịch trình của 8 RelayCREATE TABLE `relay_schedules` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `relay_id` INT NOT NULL, -- Định danh từ 1 đến 8 `selected_time` TIME NOT NULL, -- Ví dụ: '08:30:00' hoặc '18:00:00' `selected_days` VARCHAR(50) NOT NULL, -- Chuỗi phân tách bằng dấu phẩy, ví dụ: 'Mon,Wed,Fri' hoặc 'Sun,Sat' `active` TINYINT(1) DEFAULT 1, -- 1: Kích hoạt lịch, 0: Tạm dừng `action` ENUM('ON', 'OFF') NOT NULL DEFAULT 'ON' -- Hành động khi đến giờ ); -- 2. Bảng lưu vết trạng thái hoạt động (Relay Log)CREATE TABLE `relay_log` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `relay_id` INT NOT NULL, `status` ENUM('ON', 'OFF') NOT NULL, `changed_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- Dữ liệu mẫu (Data Examples) cho lịch trìnhINSERT INTO `relay_schedules` (`relay_id`, `selected_time`, `selected_days`, `active`, `action`) VALUES (1, '06:00:00', 'Mon,Tue,Wed,Thu,Fri', 1, 'ON'), (1, '18:00:00', 'Mon,Tue,Wed,Thu,Fri', 1, 'OFF'), (2, '08:30:00', 'Sun,Sat', 1, 'ON'), (2, '22:00:00', 'Sun,Sat', 1, 'OFF'); ------------------------------ ## 2. Backend API Backend (PHP - api.php) File script xử lý đồng thời 2 tác vụ: Trả lời lịch trình dạng JSON (GET) và tiếp nhận log trạng thái từ ESP32 gửi lên (POST). connect_error) { die(json_encode(["error" => "Database connection failed"])); } $method = $_SERVER['REQUEST_METHOD']; // TÁC VỤ 1: ESP32 lấy danh sách lịch trình (GET) if ($method === 'GET') { $sql = "SELECT relay_id, selected_time, selected_days, active, action FROM relay_schedules WHERE active = 1"; $result = $conn->query($sql); $schedules = []; while($row = $result->fetch_assoc()) { $schedules[] = [ "relay_id" => (int)$row['relay_id'], "time" => substr($row['selected_time'], 0, 5), // Lấy định dạng "HH:MM" "days" => explode(',', $row['selected_days']), // Chuyển chuỗi 'Mon,Wed' thành mảng ["Mon", "Wed"] "action" => $row['action'] ]; } echo json_encode(["schedules" => $schedules]); } // TÁC VỤ 2: ESP32 ghi log trạng thái (POST JSON) if ($method === 'POST') { $input = file_get_contents('php://input'); $data = json_decode($input, true); if (isset($data['relay_id']) && isset($data['status'])) { $relay_id = (int)$data['relay_id']; $status = $conn->real_escape_string($data['status']); $stmt = $conn->prepare("INSERT INTO relay_log (relay_id, status) VALUES (?, ?)"); $stmt->bind_param("is", $relay_id, $status); if ($stmt->execute()) { echo json_encode(["success" => true, "message" => "Log saved"]); } else { echo json_encode(["success" => false, "message" => "Insert failed"]); } $stmt->close(); } else { echo json_encode(["success" => false, "message" => "Invalid data format"]); } } $conn->close(); ?> ------------------------------ ## 3. Mã nguồn ESP32 (Arduino IDE) Đoạn mã cấu hình chân cho 8 Relay, tự động lấy thời gian từ Internet qua máy chủ NTP, đồng bộ lịch trình qua HTTPS (Root CA), lưu cấu hình tạm thời vào bộ nhớ Flash bằng Preferences.h đề phòng mất điện, xử lý dữ liệu JSON động bằng ArduinoJson.h. [2, 3, 4, 5, 6] ## Yêu cầu chuẩn bị thư viện: * * Mở Arduino IDE -> Library Manager -> Cài đặt thư viện ArduinoJson (phiên bản v6 hoặc v7). * Thư viện Preferences.h, WiFi.h, HTTPClient.h và WiFiClientSecure.h đã được tích hợp sẵn trong ESP32 Core. [3, 6, 7, 8, 9] * #include #include #include #include #include #include "time.h" // Cấu hình mạng WiFiconst char* ssid = "YOUR_WIFI_SSID";const char* password = "YOUR_WIFI_PASSWORD"; // Endpoint API PHP (Chuyển sang domain HTTPS của bạn)const char* api_url = "https://your-secure-domain.com"; // Chuỗi Root CA SSL / TLS của Server chứa API (Thay bằng Root CA tương ứng của bạn)// Có thể lấy bằng cách chạy command: openssl s_client -showcerts -connect your-secure-domain.com:443const char* root_ca = \"-----BEGIN CERTIFICATE-----\n" \"MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw\n" \"... (Hãy thay thế toàn bộ mã chứng chỉ Root CA thực tế tại đây) ...\n" \"-----END CERTIFICATE-----\n"; // Khai báo mảng 8 chân GPIO điều khiển Relay tương ứngconst int RELAY_PINS[8] = {13, 12, 14, 27, 26, 25, 33, 32}; // Trạng thái hiện tại của 8 relay (false = OFF, true = ON)bool relay_states[8] = {false, false, false, false, false, false, false, false}; // Cấu hình NTP Server lấy thời gian thựcconst char* ntpServer = "pool.ntp.org";const long gmtOffset_sec = 25200; // Múi giờ Việt Nam (ICT = UTC+7): 7 * 3600 = 25200const int daylightOffset_sec = 0; Preferences preferences;unsigned long lastCheckTime = 0;unsigned long lastSyncSchedule = 0; // Mảng tra cứu tên thứ trong tuần định dạng chuỗiconst char* daysOfWeek[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; void setup() { Serial.begin(115200); // Khởi tạo các chân GPIO output cho Relay for (int i = 0; i < 8; i++) { pinMode(RELAY_PINS[i], OUTPUT); digitalWrite(RELAY_PINS[i], LOW); // Mặc định tắt (LOW) khi khởi động } // Khởi tạo bộ nhớ Preferences lưu cấu hình dự phòng preferences.begin("relay_config", false); // Kết nối WiFi WiFi.begin(ssid, password); Serial.print("Connecting to WiFi"); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("\nWiFi Connected!"); // Cấu hình thời gian thực từ NTP configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); Serial.println("Synchronizing time..."); struct tm timeinfo; while(!getLocalTime(&timeinfo)){ delay(500); Serial.print("."); } Serial.println("\nTime Synchronized successfully."); // Tải lịch trình từ server ngay khi khởi động fetchSchedulesFromServer(); } void loop() { unsigned long currentMillis = millis(); // Kiểm tra kích hoạt lịch trình mỗi 10 giây một lần (Tránh trùng lặp trong cùng 1 phút) if (currentMillis - lastCheckTime >= 10000) { lastCheckTime = currentMillis; checkAndExecuteSchedules(); } // Tự động cập nhật / đồng bộ lại bộ lịch trình mới từ database sau mỗi 30 phút if (currentMillis - lastSyncSchedule >= 1800000 || lastSyncSchedule == 0) { lastSyncSchedule = currentMillis; fetchSchedulesFromServer(); } } // Hàm tải dữ liệu JSON lịch trình từ HTTPS Server và lưu vào bộ nhớ Preferencesvoid fetchSchedulesFromServer() { if (WiFi.status() == WL_CONNECTED) { WiFiClientSecure *client = new WiFiClientSecure; if(client) { client->setCACert(root_ca); // Đính kèm Root CA SSL kiểm tra bảo mật { HTTPClient https; Serial.println("[HTTPS] Fetching schedules..."); if (https.begin(*client, api_url)) { int httpCode = https.GET(); if (httpCode == HTTP_CODE_OK) { String payload = https.getString(); Serial.println("Schedules received successfully."); // Lưu chuỗi JSON trực tiếp vào Flash thông qua Preferences để phòng mất điện đột ngột preferences.putString("json_data", payload); } else { Serial.printf("[HTTPS] GET failed, error: %s\n", https.errorToString(httpCode).c_str()); } https.end(); } } delete client; } } } // Hàm phân tích JSON, đối chiếu thời gian thực hiện tại để kích hoạt đóng/ngắt Relayvoid checkAndExecuteSchedules() { struct tm timeinfo; if(!getLocalTime(&timeinfo)){ Serial.println("Failed to obtain local time"); return; } // Định dạng thời gian hiện tại char currentTimeStr[6]; // Định dạng "HH:MM" sprintf(currentTimeStr, "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min); const char* currentDayStr = daysOfWeek[timeinfo.tm_wday]; // Lấy thứ hiện tại: Sun, Mon... // Đọc dữ liệu lịch trình cũ từ Flash Preferences String jsonStr = preferences.getString("json_data", ""); if (jsonStr == "") return; // Khởi tạo DynamicJsonDocument để bóc tách mảng DynamicJsonDocument doc(4096); DeserializationError error = deserializeJson(doc, jsonStr); if (error) { Serial.print("deserializeJson() failed: "); Serial.println(error.c_str()); return; } JsonArray schedules = doc["schedules"].as(); for (JsonObject sched : schedules) { int relay_id = sched["relay_id"]; // 1 đến 8 const char* sched_time = sched["time"]; // "HH:MM" const char* action = sched["action"]; // "ON" hoặc "OFF" JsonArray days = sched["days"].as(); // Kiểm tra xem giờ hiện tại có khớp với giờ cài đặt không if (strcmp(currentTimeStr, sched_time) == 0) { // Kiểm tra ngày trong tuần có nằm trong danh sách được chọn không bool dayMatched = false; for (JsonVariant day : days) { if (strcmp(day.as(), currentDayStr) == 0) { dayMatched = true; break; } } if (dayMatched) { int pinIndex = relay_id - 1; // Chuyển từ relay_id (1-8) sang chỉ mục mảng chân (0-7) if (pinIndex >= 0 && pinIndex < 8) { bool targetState = (strcmp(action, "ON") == 0) ? true : false; // Tránh gửi log lặp đi lặp lại nếu trạng thái chân relay đã đúng mục tiêu if (relay_states[pinIndex] != targetState) { relay_states[pinIndex] = targetState; digitalWrite(RELAY_PINS[pinIndex], targetState ? HIGH : LOW); Serial.printf("Relay %d trigger status to %s\n", relay_id, action); // Gửi log sự kiện lên database thông qua giao thức HTTPS POST sendLogToServer(relay_id, action); } } } } } } // Hàm gửi log sự kiện ON/OFF lên DB qua HTTPS POST JSONvoid sendLogToServer(int relay_id, const char* status) { if (WiFi.status() == WL_CONNECTED) { WiFiClientSecure *client = new WiFiClientSecure; if(client) { client->setCACert(root_ca); { HTTPClient https; if (https.begin(*client, api_url)) { https.addHeader("Content-Type", "application/json"); // Tạo chuỗi JSON payload DynamicJsonDocument doc(256); doc["relay_id"] = relay_id; doc["status"] = status; String requestBody; serializeJson(doc, requestBody); int httpCode = https.POST(requestBody); if (httpCode > 0) { Serial.printf("[HTTPS] POST log response code: %d\n", httpCode); } else { Serial.printf("[HTTPS] POST log failed, error: %s\n", https.errorToString(httpCode).c_str()); } https.end(); } } delete client; } } } ------------------------------ ## 4. Tài nguyên GitHub tham khảo thêm Nếu cần mở rộng thêm các tính năng nâng cao (như giao diện Web cài đặt tại chỗ hoặc cơ chế lập lịch phức tạp), bạn có thể tìm kiếm mã nguồn trên GitHub qua các dự án mã nguồn mở tiêu biểu sau: * * [bblanchon/ArduinoJson](https://github.com/bblanchon/ArduinoJson): Trang gốc hướng dẫn tối ưu hóa bộ nhớ RAM khi parse mảng JSON lớn của ESP32. * [zirconchem/weekly-smart-timer](https://github.com/zirconchem/weekly-smart-timer): Dự án mẫu quản lý chu kỳ hẹn giờ Timer hàng tuần toàn diện sử dụng chip ESP32. * asimzulfiqar/esp_cron: Thư viện định dạng cú pháp hẹn giờ chuẩn CRON-expression cho ESP32 cực kỳ chính xác. [6, 7, 10, 11] * Bạn có gặp khó khăn trong việc trích xuất chuỗi mã hóa Root CA SSL của web server hoặc muốn bổ sung thêm tính năng điều khiển nút bấm tay (Manual Switch) dự phòng không? [1] [https://randomnerdtutorials.com](https://randomnerdtutorials.com/esp32-esp8266-run-daily-task/) [2] [https://randomnerdtutorials.com](https://randomnerdtutorials.com/esp32-https-requests/) [3] [https://zbotic.in](https://zbotic.in/esp32-https-rest-api-secure-data-post-to-cloud-servers/) [4] [https://www.youtube.com](https://www.youtube.com/watch?v=2pXzOjdhPbs) [5] [https://randomnerdtutorials.com](https://randomnerdtutorials.com/esp32-save-data-permanently-preferences/) [6] [https://github.com](https://github.com/mathieucarbou/MycilaESPConnect) [7] [https://github.com](https://github.com/bblanchon/ArduinoJson) [8] [https://zbotic.in](https://zbotic.in/esp32-configurable-web-portal-wifimanager-auto-setup/) [9] [https://zbotic.in](https://zbotic.in/esp32-https-rest-api-secure-data-post-to-cloud-servers/) [10] [https://registry.platformio.org](https://registry.platformio.org/libraries/asimzulfiqar/esp_cron) [11] [https://github.com](https://github.com/zirconchem/weekly-smart-timer)