Dưới đây là giải pháp trọn gói bao gồm mã nguồn ESP32 (Arduino IDE), cấu trúc bảng MySQL và file API PHP. Hệ thống sử dụng kết nối HTTPS bảo mật bằng Root CA để đồng bộ lịch trình và ghi log. [1] ## Cấu trúc cơ sở dữ liệu (MySQL) Bạn chạy lệnh SQL này trong phpMyAdmin để tạo 2 bảng dữ liệu: CREATE DATABASE IF NOT EXISTS smart_relay; USE smart_relay; -- Bảng lưu lịch trình bật/tắtCREATE TABLE IF NOT EXISTS schedules ( id INT AUTO_INCREMENT PRIMARY KEY, relay_id INT NOT NULL, target_time TIME NOT NULL, -- Định dạng HH:MM:SS target_days VARCHAR(255) NOT NULL, -- Định dạng: Sun,Mon,Tue action TINYINT NOT NULL -- 1 = Bật, 0 = Tắt ); -- Bảng lưu nhật ký hoạt độngCREATE TABLE IF NOT EXISTS relay_logs ( id INT AUTO_INCREMENT PRIMARY KEY, relay_id INT NOT NULL, action TINYINT NOT NULL, timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Chèn dữ liệu mẫuINSERT INTO schedules (relay_id, target_time, target_days, action) VALUES (1, '08:00:00', 'Mon,Wed,Fri', 1), (1, '17:00:00', 'Mon,Wed,Fri', 0), (2, '06:30:00', 'Sun,Sat', 1); ------------------------------ ## Mã nguồn Backend API (api.php) Tạo file api.php trên hosting hỗ trợ HTTPS của bạn. Bản trả về định dạng JSON phù hợp với ArduinoJson. connect_error) { die(json_encode(["error" => "Kết nối DB thất bại"])); } $action = $_GET['action'] ?? ''; // 1. Lấy danh sách lịch trình if ($action == 'get_schedules') { $sql = "SELECT id, relay_id, target_time, target_days, action FROM schedules"; $result = $conn->query($sql); $schedules = []; while($row = $result->fetch_assoc()) { // Chỉ lấy HH:MM từ TIME (HH:MM:SS) để ESP32 dễ so sánh $row['target_time'] = substr($row['target_time'], 0, 5); $row['relay_id'] = (int)$row['relay_id']; $row['action'] = (int)$row['action']; $schedules[] = $row; } echo json_encode($schedules); } // 2. Ghi nhật ký thiết bị elseif ($action == 'log' && $_SERVER['REQUEST_METHOD'] === 'POST') { $input = json_parse(file_get_contents('php://input'), true); $relay_id = (int)($input['relay_id'] ?? 0); $relay_action = (int)($input['action'] ?? 0); if ($relay_id > 0) { $stmt = $conn->prepare("INSERT INTO relay_logs (relay_id, action) VALUES (?, ?)"); $stmt->bind_param("ii", $relay_id, $relay_action); if ($stmt->execute()) { echo json_encode(["status" => "success"]); } else { echo json_encode(["status" => "error"]); } $stmt->close(); } } else { echo json_encode(["error" => "Yêu cầu không hợp lệ"]); } $conn->close(); ?> ------------------------------ ## Mã nguồn ESP32 (ESP32_Relay_Controller.ino) Yêu cầu cài đặt thư viện qua Library Manager: ArduinoJson (v6 trở lên). #include #include #include #include #include #include "time.h" // Cấu hình WiFiconst char* ssid = "YOUR_WIFI_SSID";const char* password = "YOUR_WIFI_PASSWORD"; // Cấu hình API Server (Thay bằng domain của bạn)const char* get_schedule_url = "https://example.com";const char* log_url = "https://example.com"; // Root CA Certificate của website (Mẫu ISRG Root X1 của Let's Encrypt)// Thay bằng Root CA tương ứng với SSL certificate trên host của bạnconst char* root_ca = \"-----BEGIN CERTIFICATE-----\n" \"MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw\n" \"TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTI0ludGVybmV0IFNlY3VyaXR5IFJlc2Vh\n" \"cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4\n" \"WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMghUludGVy\n" \"bmV0IFNlY3VyaXR5IFJlc2VhcmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3Qg\n" \"WDEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzh9fiQgnKpSNtf767\n" \"H7CocbPDYwZ9G8Bf6Zv98fAnC1Q6m7g9SAtfC59S8650A7YvWbi6w9CQR2U3uWlb\n" \"GvOM9L3XvfQeA04Ld930vL3GL6bN1iE+I8E+fN59dM+w==\n" \"-----END CERTIFICATE-----\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; // GMT+7 cho Việt Namconst int daylightOffset_sec = 0; // Khai báo chân GPIO cho 8 Relayconst int relayPins[8] = {13, 12, 14, 27, 26, 25, 33, 32}; Preferences preferences;unsigned long lastFetchTime = 0;const unsigned long fetchInterval = 60000; // Đồng bộ lịch từ server mỗi 60 giây void setup() { Serial.begin(115200); // Khởi tạo các chân Relay ở mức HIGH hoặc LOW tùy mạch kích (Thường relay active low) for (int i = 0; i < 8; i++) { pinMode(relayPins[i], OUTPUT); digitalWrite(relayPins[i], HIGH); } // 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("\nConnected to WiFi"); // Cấu hình thời gian thực NTP configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); // Đồng bộ lịch trình từ DB ngay khi khởi động fetchSchedulesFromServer(); } void loop() { // Tự động cập nhật lịch từ API theo chu kỳ if (millis() - lastFetchTime > fetchInterval) { fetchSchedulesFromServer(); lastFetchTime = millis(); } // Kiểm tra kích hoạt Relay mỗi giây static unsigned long lastCheck = 0; if (millis() - lastCheck > 1000) { checkAndTriggerRelays(); lastCheck = millis(); } } // Hàm tải lịch trình từ HTTPS API và lưu vào bộ nhớ Preferences NVSvoid fetchSchedulesFromServer() { if (WiFi.status() != WL_CONNECTED) return; WiFiClientSecure client; client.setCACert(root_ca); HTTPClient http; if (http.begin(client, get_schedule_url)) { int httpCode = http.GET(); if (httpCode == HTTP_CODE_OK) { String payload = http.getString(); // Lưu trực tiếp chuỗi JSON nhận được vào Preferences preferences.begin("sche_data", false); preferences.putString("json", payload); preferences.end(); Serial.println("Schedules updated successfully from server."); } else { Serial.printf("HTTP GET Failed, error: %s\n", http.errorToString(httpCode).c_str()); } http.end(); } } // Hàm kiểm tra thời gian thực và đóng ngắt Relayvoid checkAndTriggerRelays() { struct tm timeinfo; if (!getLocalTime(&timeinfo)) { Serial.println("Failed to obtain time"); return; } // Lấy chuỗi định dạng Thời gian (HH:MM) và Thứ (Thu, Fri...) char currentTime[6]; char currentDay[4]; strftime(currentTime, sizeof(currentTime), "%H:%M", &timeinfo); strftime(currentDay, sizeof(currentDay), "%a", &timeinfo); // Ví dụ: "Sun", "Mon"... // Đọc dữ liệu lịch trình từ Preferences preferences.begin("sche_data", true); String jsonPayload = preferences.getString("json", "[]"); preferences.end(); // Parse chuỗi JSON DynamicJsonDocument doc(4096); DeserializationError error = deserializeJson(doc, jsonPayload); if (error) return; JsonArray arr = doc.as(); for (JsonObject obj : arr) { int relay_id = obj["relay_id"]; // Giá trị từ 1 đến 8 const char* target_time = obj["target_time"]; // "08:00" const char* target_days = obj["target_days"]; // "Mon,Wed,Fri" int action = obj["action"]; // 1 = ON, 0 = OFF // Kiểm tra tính hợp lệ của Relay ID if (relay_id < 1 || relay_id > 8) continue; // So sánh thời gian hiện tại và thứ hiện tại if (strcmp(currentTime, target_time) == 0 && strstr(target_days, currentDay) != NULL) { int pin = relayPins[relay_id - 1]; int targetState = (action == 1) ? LOW : HIGH; // Sửa LOW/HIGH tùy loại Relay Shield // Nếu trạng thái chân pin khác trạng thái đích thì thay đổi và ghi log if (digitalRead(pin) != targetState) { digitalWrite(pin, targetState); Serial.printf("Relay %d triggered to %s\n", relay_id, action == 1 ? "ON" : "OFF"); sendLogToServer(relay_id, action); } } } } // Hàm gửi log hoạt động của Relay lên HTTPS Server bằng phương thức POSTvoid sendLogToServer(int relay_id, int action) { if (WiFi.status() != WL_CONNECTED) return; WiFiClientSecure client; client.setCACert(root_ca); HTTPClient http; if (http.begin(client, log_url)) { http.addHeader("Content-Type", "application/json"); StaticJsonDocument<100> jsonDoc; jsonDoc["relay_id"] = relay_id; jsonDoc["action"] = action; String requestBody; serializeJson(jsonDoc, requestBody); int httpCode = http.POST(requestBody); if (httpCode == HTTP_CODE_OK) { Serial.println("Log sent to server successfully."); } else { Serial.printf("Failed to send log, error: %s\n", http.errorToString(httpCode).c_str()); } http.end(); } } ## Cách thức hoạt động của hệ thống 1. Lưu trữ ngoại tuyến: ESP32 tải toàn bộ danh sách cài đặt từ cơ sở dữ liệu qua mạng HTTPS, sau đó lưu trực tiếp vào bộ nhớ Flash (Preferences). Nếu mất mạng WiFi, thiết bị vẫn tự động đóng ngắt chuẩn xác nhờ chip RTC nội bộ đồng bộ từ NTP trước đó. 2. Xử lý chuỗi ngày: Định dạng ngày [Sun, Mon...] được xử lý thông minh qua hàm tìm kiếm chuỗi con strstr(), giúp giảm thiểu tối đa kích thước bộ nhớ RAM xử lý trên vi điều khiển. Để tiếp tục triển khai, bạn có muốn tôi hỗ trợ tạo thêm giao diện HTML/CSS trên hosting giúp bạn thao tác thêm/bớt lịch trình bằng điện thoại một cách trực quan không? [1] [https://www.instructables.com](https://www.instructables.com/Live-Regional-Weather-Clock/)