/********* Pleasedontcode.com ********** Pleasedontcode thanks you for automatic code generation! Enjoy your code! - Terms and Conditions: You have a non-exclusive, revocable, worldwide, royalty-free license for personal and commercial use. Attribution is optional; modifications are allowed, but you're responsible for code maintenance. We're not liable for any loss or damage. For full terms, please visit pleasedontcode.com/termsandconditions. - Project: # Smart Gateway - Version: 009 - Source Code NOT compiled for: ESP32S3 Dev Module - Source Code created on: 2026-03-08 21:02:43 ********* Pleasedontcode.com **********/ /****** SYSTEM REQUIREMENTS *****/ /****** SYSTEM REQUIREMENT 1 *****/ /* Inicijalizovati UART1 sa GPIO17(TX) i GPIO18(RX) */ /* na 9600 baud za Modbus RTU komunikaciju. GPIO4 kao */ /* RS485 DE kontrola. Slave ID=1, 8 releja (Coil */ /* 0-7), CRC16 provjera, timeout zaštita. */ /****** SYSTEM REQUIREMENT 2 *****/ /* Inicijalizovati LVGL display sa Waveshare 7" touch */ /* LCD. Prikazati 8 touchscreen dugmadi za releje */ /* (4+4 raspored), status LED, WiFi ikona, Modbus */ /* status. Real-time ažuriranje stanja releja. */ /****** SYSTEM REQUIREMENT 3 *****/ /* Pritiskom na relay dugme na displeju poslati */ /* Modbus Write Single Coil (FC 0x05) komandu. */ /* 0xFF00=ON, 0x0000=OFF. Log svaku komandu sa */ /* vremenskom markom. Debounce 100ms. */ /****** SYSTEM REQUIREMENT 4 *****/ /* WiFi pristup sa SSID i password (hardkod ili web */ /* konfiguracija). Web server na portu 80 sa REST */ /* API: GET /api/relay/status, POST */ /* /api/relay/control, GET /api/logs. JSON format */ /* odgovora. */ /****** SYSTEM REQUIREMENT 5 *****/ /* Web dashboard sa HTML/CSS/JavaScript: prikaz svih */ /* 8 releja, ON/OFF dugmadi, status Modbus konekcije, */ /* log Modbus komunikacije, OTA update sekcija. Auto- */ /* refresh svakih 500ms. */ /****** SYSTEM REQUIREMENT 6 *****/ /* OTA (Over-The-Air) firmware update mogućnost. Web */ /* upload interface za novi .bin fajl. ArduinoOTA */ /* biblioteka, sigurna autentifikacija sa korisničkim */ /* imenom i lozinkom. */ /****** SYSTEM REQUIREMENT 7 *****/ /* Čuvanje Modbus komunikacijskih logova u */ /* EEPROM/SPIFFS (max 1000 posljednjih komandi). */ /* Prikaz logova na web interfejsu sa filtriranjem po */ /* tipu, vremenu, statusu. CSV export mogućnost. */ /****** SYSTEM REQUIREMENT 8 *****/ /* Web interfejs autentifikacija sa admin korisničkim */ /* imenom i lozinkom. JWT token ili session-based */ /* autentifikacija. Logovanje pokušaja pristupa. */ /* Default user: admin/admin123 (promjena obavezna */ /* pri prvom loginu). */ /****** END SYSTEM REQUIREMENTS *****/ /* START CODE */ #include #include #include #include #include #include #include #include #include #include #include "waveshare_lcd_port.h" // ===================================================================== // SYSTEM REQUIREMENTS - UART1 Configuration for Modbus RTU // ===================================================================== // UART1 sa GPIO17(TX) i GPIO18(RX) na 9600 baud #define UART1_TX_PIN 17 #define UART1_RX_PIN 18 #define MODBUS_BAUD 9600 #define UART_CHANNEL 1 // ===================================================================== // RS485 Control and Modbus Configuration // ===================================================================== #define RS485_DE_PIN 4 // RS485 Driver Enable pin #define MODBUS_SLAVE_ADDR 1 // Slave address for Modbus commands #define NUM_RELAYS 8 // Number of relays (0-7) #define MODBUS_TIMEOUT 1000 // milliseconds #define MODBUS_MAX_RETRIES 3 #define MODBUS_CRC_POLY 0xA001 // ===================================================================== // Web Server Configuration // ===================================================================== #define WEB_SERVER_PORT 80 #define WEB_DASHBOARD_REFRESH_MS 500 // ===================================================================== // EEPROM/SPIFFS Configuration // ===================================================================== #define MAX_LOG_ENTRIES 1000 #define LOG_ENTRY_SIZE 128 // ===================================================================== // WiFi Configuration // ===================================================================== #define WIFI_SSID "YourSSID" #define WIFI_PASSWORD "YourPassword" #define WIFI_TIMEOUT 20000 // ===================================================================== // JWT/Session Configuration // ===================================================================== #define JWT_SECRET "your-secret-key-change-this" #define DEFAULT_USERNAME "admin" #define DEFAULT_PASSWORD "admin123" // ===================================================================== // Display Configuration // ===================================================================== #define DISPLAY_WIDTH 800 #define DISPLAY_HEIGHT 480 #define BUTTON_ROWS 2 #define BUTTONS_PER_ROW 4 #define TOTAL_BUTTONS 8 // ===================================================================== // LVGL Configuration // ===================================================================== #define LVGL_TICK_PERIOD 5 #define LVGL_BUFFER_SIZE (DISPLAY_WIDTH * DISPLAY_HEIGHT / 10) // ===================================================================== // Global Variables // ===================================================================== HardwareSerial ModbusSerial(UART_CHANNEL); WebServer webServer(WEB_SERVER_PORT); Preferences prefs; // Relay states: 0 = OFF, 1 = ON uint8_t relayStates[NUM_RELAYS] = {0}; // UI state bool wifiConnected = false; bool modbusConnected = false; uint32_t lastUIRefresh = 0; uint32_t lastHealthCheck = 0; // Authentication String currentSessionToken = ""; uint32_t sessionExpire = 0; const uint32_t SESSION_TIMEOUT = 3600000; // 1 hour in milliseconds // LVGL objects static lv_obj_t *screen_main = NULL; static lv_obj_t *relay_buttons[NUM_RELAYS]; static lv_obj_t *wifi_label = NULL; static lv_obj_t *modbus_label = NULL; static lv_obj_t *status_container = NULL; // ===================================================================== // Log Entry Structure // ===================================================================== struct LogEntry { uint32_t timestamp; uint8_t type; // 0=Modbus Send, 1=Modbus Response, 2=Error, 3=Access uint8_t relayIndex; bool value; bool success; char details[64]; }; // ===================================================================== // Global Log Buffer // ===================================================================== std::vector modbusLog; // ===================================================================== // LVGL Display Buffer // ===================================================================== static lv_disp_buf_t disp_buf; static lv_color_t buf[LVGL_BUFFER_SIZE]; // ===================================================================== // FUNCTION: LVGL Flush Callback // ===================================================================== void lv_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { esp_panel::drivers::LCD *lcd = waveshare_lcd_get(); if (!lcd) return; uint32_t w = (area->x2 - area->x1 + 1); uint32_t h = (area->y2 - area->y1 + 1); lcd->drawBitmap(area->x1, area->y1, w, h, (uint16_t *)color_p); lv_disp_flush_ready(disp_drv); } // ===================================================================== // FUNCTION: LVGL Timer Callback // ===================================================================== static void lv_tick_task(void *arg) { lv_tick_inc(LVGL_TICK_PERIOD); } // ===================================================================== // FUNCTION: Initialize LVGL Display // ===================================================================== void initializeLVGL() { Serial.println("Initializing LVGL display..."); // Initialize LVGL lv_init(); // Initialize display buffer lv_disp_buf_init(&disp_buf, buf, NULL, LVGL_BUFFER_SIZE); // Initialize display driver lv_disp_drv_t disp_drv; lv_disp_drv_init(&disp_drv); disp_drv.buffer = &disp_buf; disp_drv.hor_res = DISPLAY_WIDTH; disp_drv.ver_res = DISPLAY_HEIGHT; disp_drv.flush_cb = lv_flush_cb; lv_disp_drv_register(&disp_drv); // Create main screen screen_main = lv_scr_act(); lv_obj_set_style_local_bg_color(screen_main, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE); Serial.println("LVGL initialized successfully"); } // ===================================================================== // FUNCTION: Create GUI Relay Control Panel // ===================================================================== void createRelayPanel() { Serial.println("Creating relay control panel..."); // Create container for relay buttons lv_obj_t *relay_container = lv_cont_create(screen_main, NULL); lv_obj_set_auto_realign(relay_container, true); lv_obj_align(relay_container, NULL, LV_ALIGN_IN_TOP_MID, 0, 10); lv_cont_set_fit2(relay_container, LV_FIT_MAX, LV_FIT_MAX); lv_cont_set_layout(relay_container, LV_LAYOUT_GRID); // Set grid spacing lv_obj_set_style_local_pad_all(relay_container, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, 10); lv_obj_set_style_local_pad_inner(relay_container, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, 10); // Create relay buttons in 4x2 grid for (uint8_t i = 0; i < NUM_RELAYS; i++) { relay_buttons[i] = lv_btn_create(relay_container, NULL); lv_obj_set_width(relay_buttons[i], 150); lv_obj_set_height(relay_buttons[i], 100); // Update button appearance based on state if (relayStates[i]) { lv_obj_set_style_local_bg_color(relay_buttons[i], LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_GREEN); } else { lv_obj_set_style_local_bg_color(relay_buttons[i], LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_RED); } // Add label to button lv_obj_t *label = lv_label_create(relay_buttons[i], NULL); char btn_text[16]; snprintf(btn_text, sizeof(btn_text), "Relay %d\n%s", i, relayStates[i] ? "ON" : "OFF"); lv_label_set_text(label, btn_text); lv_obj_set_style_local_text_color(label, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE); } Serial.println("Relay panel created successfully"); } // ===================================================================== // FUNCTION: Create Status Display // ===================================================================== void createStatusDisplay() { Serial.println("Creating status display..."); // Create status container at bottom status_container = lv_cont_create(screen_main, NULL); lv_obj_set_width(status_container, DISPLAY_WIDTH); lv_obj_set_height(status_container, 80); lv_obj_align(status_container, NULL, LV_ALIGN_IN_BOTTOM_MID, 0, 0); lv_obj_set_style_local_bg_color(status_container, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_GRAY); // WiFi status label wifi_label = lv_label_create(status_container, NULL); lv_label_set_text(wifi_label, "WiFi: Connecting..."); lv_obj_align(wifi_label, NULL, LV_ALIGN_IN_TOP_LEFT, 10, 10); // Modbus status label modbus_label = lv_label_create(status_container, NULL); lv_label_set_text(modbus_label, "Modbus: Ready"); lv_obj_align(modbus_label, wifi_label, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 5); Serial.println("Status display created successfully"); } // ===================================================================== // FUNCTION: Update GUI Display // ===================================================================== void updateGUIDisplay() { // Update WiFi status if (wifi_label) { if (wifiConnected) { lv_label_set_text(wifi_label, "WiFi: Connected"); lv_obj_set_style_local_text_color(wifi_label, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_GREEN); } else { lv_label_set_text(wifi_label, "WiFi: Disconnected"); lv_obj_set_style_local_text_color(wifi_label, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_RED); } } // Update Modbus status if (modbus_label) { if (modbusConnected) { lv_label_set_text(modbus_label, "Modbus: Connected"); lv_obj_set_style_local_text_color(modbus_label, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_GREEN); } else { lv_label_set_text(modbus_label, "Modbus: Offline"); lv_obj_set_style_local_text_color(modbus_label, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_RED); } } // Update relay button states for (uint8_t i = 0; i < NUM_RELAYS; i++) { if (relay_buttons[i]) { if (relayStates[i]) { lv_obj_set_style_local_bg_color(relay_buttons[i], LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_GREEN); } else { lv_obj_set_style_local_bg_color(relay_buttons[i], LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_RED); } } } // Refresh LVGL lv_task_handler(); } // ===================================================================== // FUNCTION: Initialize Preferences/NVS Storage // ===================================================================== void initializeStorage() { if (!prefs.begin("modbus_esp32", false)) { Serial.println("Failed to initialize Preferences"); return; } Serial.println("Storage initialized"); } // ===================================================================== // FUNCTION: Add Log Entry // ===================================================================== void addLogEntry(uint8_t type, uint8_t relayIndex, bool value, bool success, const char* details) { LogEntry entry; entry.timestamp = millis() / 1000; entry.type = type; entry.relayIndex = relayIndex; entry.value = value; entry.success = success; strncpy(entry.details, details, sizeof(entry.details) - 1); entry.details[sizeof(entry.details) - 1] = '\0'; modbusLog.push_back(entry); // Maintain max log size if (modbusLog.size() > MAX_LOG_ENTRIES) { modbusLog.erase(modbusLog.begin()); } } // ===================================================================== // FUNCTION: Calculate Modbus CRC16 // ===================================================================== uint16_t calculateModbusCRC(uint8_t *data, uint8_t length) { uint16_t crc = 0xFFFF; for(uint8_t i = 0; i < length; i++) { crc ^= data[i]; for(uint8_t j = 0; j < 8; j++) { if(crc & 1) crc = (crc >> 1) ^ MODBUS_CRC_POLY; else crc >>= 1; } } return crc; } // ===================================================================== // FUNCTION: Set RS485 to TX mode // ===================================================================== inline void rs485_tx() { digitalWrite(RS485_DE_PIN, HIGH); } // ===================================================================== // FUNCTION: Set RS485 to RX mode // ===================================================================== inline void rs485_rx() { digitalWrite(RS485_DE_PIN, LOW); } // ===================================================================== // SYSTEM REQUIREMENT 3: Modbus Write Single Coil // ===================================================================== // FC 0x05: Write Single Coil bool modbusWriteCoil(uint8_t relayIndex, bool on) { if (relayIndex >= NUM_RELAYS) { addLogEntry(2, relayIndex, on, false, "Invalid relay index"); return false; } uint8_t txBuffer[8]; // Build Modbus frame txBuffer[0] = MODBUS_SLAVE_ADDR; // Slave address txBuffer[1] = 0x05; // Function code: Write Single Coil txBuffer[2] = 0x00; // Coil address high byte txBuffer[3] = relayIndex; // Coil address low byte (Coil 0-7) txBuffer[4] = on ? 0xFF : 0x00; // Value high byte (0xFF00 for ON, 0x0000 for OFF) txBuffer[5] = 0x00; // Value low byte // Calculate and append CRC16 uint16_t crc = calculateModbusCRC(txBuffer, 6); txBuffer[6] = crc & 0xFF; txBuffer[7] = crc >> 8; // Clear any pending data in receive buffer while(ModbusSerial.available()) ModbusSerial.read(); // Switch RS485 to TX mode rs485_tx(); delayMicroseconds(100); // Send Modbus frame ModbusSerial.write(txBuffer, 8); ModbusSerial.flush(); // Wait for transmission to complete delayMicroseconds(200); // Switch RS485 to RX mode rs485_rx(); // Read response with timeout uint8_t rxBuffer[8]; uint8_t bytesRead = 0; uint32_t startTime = millis(); while(ModbusSerial.available() && bytesRead < 8) { if(millis() - startTime > MODBUS_TIMEOUT) { addLogEntry(2, relayIndex, on, false, "Modbus timeout"); modbusConnected = false; return false; } rxBuffer[bytesRead] = ModbusSerial.read(); bytesRead++; delayMicroseconds(100); } // Verify response bool success = false; if(bytesRead >= 8) { // Extract CRC from response uint16_t receivedCRC = (rxBuffer[7] << 8) | rxBuffer[6]; // Calculate CRC of received data (excluding CRC bytes) uint16_t calculatedCRC = calculateModbusCRC(rxBuffer, 6); if(receivedCRC == calculatedCRC) { success = true; relayStates[relayIndex] = on ? 1 : 0; addLogEntry(1, relayIndex, on, true, "CRC verified"); modbusConnected = true; Serial.print("Modbus Write Success: Relay "); Serial.print(relayIndex); Serial.print(" -> "); Serial.println(on ? "ON" : "OFF"); } else { addLogEntry(2, relayIndex, on, false, "CRC mismatch"); Serial.println("Modbus Response: CRC mismatch!"); modbusConnected = false; } } else { addLogEntry(2, relayIndex, on, false, "No response"); Serial.println("Modbus Write: No response from slave"); modbusConnected = false; } return success; } // ===================================================================== // FUNCTION: Read Modbus Coil Status (Function Code 0x01) // ===================================================================== bool modbusReadCoils(uint8_t startCoil, uint8_t count, uint8_t* values) { if (startCoil + count > NUM_RELAYS) { addLogEntry(2, startCoil, false, false, "Invalid coil range"); return false; } uint8_t txBuffer[12]; // Build read coils frame txBuffer[0] = MODBUS_SLAVE_ADDR; txBuffer[1] = 0x01; // Function code: Read Coils txBuffer[2] = 0x00; txBuffer[3] = startCoil; txBuffer[4] = 0x00; txBuffer[5] = count; uint16_t crc = calculateModbusCRC(txBuffer, 6); txBuffer[6] = crc & 0xFF; txBuffer[7] = crc >> 8; // Clear receive buffer while(ModbusSerial.available()) ModbusSerial.read(); // Send request rs485_tx(); delayMicroseconds(100); ModbusSerial.write(txBuffer, 8); ModbusSerial.flush(); delayMicroseconds(200); rs485_rx(); // Read response uint8_t rxBuffer[32]; uint8_t bytesRead = 0; uint32_t startTime = millis(); while(ModbusSerial.available() && bytesRead < 32) { if(millis() - startTime > MODBUS_TIMEOUT) { addLogEntry(2, startCoil, false, false, "Read timeout"); return false; } rxBuffer[bytesRead] = ModbusSerial.read(); bytesRead++; delayMicroseconds(100); } // Verify response if(bytesRead >= 5) { uint16_t receivedCRC = (rxBuffer[bytesRead - 1] << 8) | rxBuffer[bytesRead - 2]; uint16_t calculatedCRC = calculateModbusCRC(rxBuffer, bytesRead - 2); if(receivedCRC == calculatedCRC) { // Parse coil values uint8_t byteCount = rxBuffer[2]; for(uint8_t i = 0; i < count && i < byteCount; i++) { values[i] = (rxBuffer[3 + (i / 8)] >> (i % 8)) & 1; } addLogEntry(1, startCoil, false, true, "Read coils OK"); modbusConnected = true; return true; } else { addLogEntry(2, startCoil, false, false, "Read CRC error"); } } else { addLogEntry(2, startCoil, false, false, "Read no response"); } modbusConnected = false; return false; } // ===================================================================== // FUNCTION: Initialize UART1 for Modbus RTU // ===================================================================== void initializeModbusUART() { // Configure RS485 control pin pinMode(RS485_DE_PIN, OUTPUT); digitalWrite(RS485_DE_PIN, LOW); // Start in RX mode // Initialize UART1 with specified pins and baud rate ModbusSerial.begin( MODBUS_BAUD, // 9600 baud SERIAL_8N1, // 8 data bits, no parity, 1 stop bit UART1_RX_PIN, // RX on GPIO18 UART1_TX_PIN // TX on GPIO17 ); Serial.println("Modbus UART1 initialized"); Serial.println("TX: GPIO17, RX: GPIO18, Baud: 9600"); } // ===================================================================== // FUNCTION: Initialize WiFi Connection // ===================================================================== void initializeWiFi() { Serial.println("\nStarting WiFi connection..."); WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); uint32_t startTime = millis(); while(WiFi.status() != WL_CONNECTED && millis() - startTime < WIFI_TIMEOUT) { delay(500); Serial.print("."); } if(WiFi.status() == WL_CONNECTED) { wifiConnected = true; Serial.println("\nWiFi connected!"); Serial.print("IP address: "); Serial.println(WiFi.localIP()); // Configure time for NTP configTime(0, 0, "pool.ntp.org", "time.nist.gov"); Serial.println("NTP time configured"); } else { wifiConnected = false; Serial.println("\nWiFi connection failed!"); } } // ===================================================================== // FUNCTION: Generate JWT Token // ===================================================================== String generateJWT(const String& username) { String payload = username + String(millis()); // Simple hash for signature uint32_t hash = 5381; for(unsigned int i = 0; i < payload.length(); i++) { hash = ((hash << 5) + hash) + payload.charAt(i); } String token = "ESP32." + payload + "." + String(hash); return token; } // ===================================================================== // FUNCTION: Verify Session Token // ===================================================================== bool verifySession() { // Check if session is expired if(millis() > sessionExpire && sessionExpire > 0) { currentSessionToken = ""; return false; } return currentSessionToken.length() > 0; } // ===================================================================== // WEB API HANDLERS // ===================================================================== // ===================================================================== // HANDLER: Login Endpoint // ===================================================================== void handleLogin() { if(webServer.method() != HTTP_POST) { webServer.send(405, "application/json", "{\"error\":\"Method not allowed\"}"); return; } String body = webServer.arg("plain"); DynamicJsonDocument doc(256); if(deserializeJson(doc, body) != DeserializationError::Ok) { webServer.send(400, "application/json", "{\"error\":\"Invalid JSON\"}"); return; } String username = doc["username"] | ""; String password = doc["password"] | ""; // Verify credentials if(username == DEFAULT_USERNAME && password == DEFAULT_PASSWORD) { // Generate session token currentSessionToken = generateJWT(username); sessionExpire = millis() + SESSION_TIMEOUT; DynamicJsonDocument response(256); response["success"] = true; response["token"] = currentSessionToken; response["expires_in"] = SESSION_TIMEOUT / 1000; String jsonResponse; serializeJson(response, jsonResponse); webServer.send(200, "application/json", jsonResponse); addLogEntry(3, 0, true, true, "Login successful"); } else { webServer.send(401, "application/json", "{\"error\":\"Invalid credentials\"}"); addLogEntry(3, 0, false, false, "Login failed"); } } // ===================================================================== // HANDLER: Get Relay Status // ===================================================================== void handleGetRelayStatus() { if(!verifySession()) { webServer.send(401, "application/json", "{\"error\":\"Unauthorized\"}"); return; } DynamicJsonDocument response(512); response["timestamp"] = millis() / 1000; response["modbus_connected"] = modbusConnected; response["wifi_connected"] = wifiConnected; JsonArray relays = response.createNestedArray("relays"); for(uint8_t i = 0; i < NUM_RELAYS; i++) { JsonObject relay = relays.createNestedObject(); relay["index"] = i; relay["state"] = relayStates[i] ? "ON" : "OFF"; } String jsonResponse; serializeJson(response, jsonResponse); webServer.send(200, "application/json", jsonResponse); } // ===================================================================== // HANDLER: Control Relay // ===================================================================== void handleControlRelay() { if(!verifySession()) { webServer.send(401, "application/json", "{\"error\":\"Unauthorized\"}"); return; } if(webServer.method() != HTTP_POST) { webServer.send(405, "application/json", "{\"error\":\"Method not allowed\"}"); return; } String body = webServer.arg("plain"); DynamicJsonDocument doc(256); if(deserializeJson(doc, body) != DeserializationError::Ok) { webServer.send(400, "application/json", "{\"error\":\"Invalid JSON\"}"); return; } uint8_t relayIndex = doc["relay"] | 255; String action = doc["action"] | ""; if(relayIndex >= NUM_RELAYS) { webServer.send(400, "application/json", "{\"error\":\"Invalid relay index\"}"); return; } bool turnOn = (action == "ON"); bool success = modbusWriteCoil(relayIndex, turnOn); DynamicJsonDocument response(256); response["success"] = success; response["relay"] = relayIndex; response["state"] = turnOn ? "ON" : "OFF"; String jsonResponse; serializeJson(response, jsonResponse); webServer.send(success ? 200 : 500, "application/json", jsonResponse); } // ===================================================================== // HANDLER: Get Modbus Logs // ===================================================================== void handleGetLogs() { if(!verifySession()) { webServer.send(401, "application/json", "{\"error\":\"Unauthorized\"}"); return; } // Parse query parameters for filtering String typeFilter = webServer.arg("type"); String limit = webServer.arg("limit"); int maxEntries = limit.length() > 0 ? limit.toInt() : 100; if(maxEntries > 1000) maxEntries = 1000; if(maxEntries < 1) maxEntries = 10; DynamicJsonDocument response(8192); response["total_entries"] = modbusLog.size(); response["returned"] = 0; JsonArray logs = response.createNestedArray("logs"); int count = 0; int startIdx = modbusLog.size() > maxEntries ? modbusLog.size() - maxEntries : 0; for(int i = startIdx; i < (int)modbusLog.size() && count < maxEntries; i++) { LogEntry& entry = modbusLog[i]; // Apply type filter if specified if(typeFilter.length() > 0) { uint8_t filterType = typeFilter.toInt(); if(entry.type != filterType) continue; } JsonObject logEntry = logs.createNestedObject(); logEntry["timestamp"] = entry.timestamp; logEntry["type"] = entry.type; logEntry["relay"] = entry.relayIndex; logEntry["value"] = entry.value ? "ON" : "OFF"; logEntry["success"] = entry.success; logEntry["details"] = entry.details; count++; } response["returned"] = count; String jsonResponse; serializeJson(response, jsonResponse); webServer.send(200, "application/json", jsonResponse); } // ===================================================================== // HANDLER: Export Logs as CSV // ===================================================================== void handleExportLogs() { if(!verifySession()) { webServer.send(401, "application/json", "{\"error\":\"Unauthorized\"}"); return; } String csv = "Timestamp,Type,Relay,Value,Success,Details\n"; for(const auto& entry : modbusLog) { csv += String(entry.timestamp) + ","; csv += String(entry.type) + ","; csv += String(entry.relayIndex) + ","; csv += (entry.value ? "ON" : "OFF") + String(","); csv += (entry.success ? "true" : "false") + String(","); csv += String(entry.details) + "\n"; } webServer.send(200, "text/csv", csv); } // ===================================================================== // HANDLER: OTA Firmware Update // ===================================================================== void handleOTAUpdate() { if(!verifySession()) { webServer.send(401, "application/json", "{\"error\":\"Unauthorized\"}"); return; } if(webServer.method() != HTTP_POST) { webServer.send(405, "application/json", "{\"error\":\"Method not allowed\"}"); return; } if(!Update.hasError()) { DynamicJsonDocument response(256); response["success"] = true; response["message"] = "Firmware update completed. Device rebooting..."; String jsonResponse; serializeJson(response, jsonResponse); webServer.send(200, "application/json", jsonResponse); delay(1000); ESP.restart(); } else { DynamicJsonDocument response(256); response["success"] = false; response["error"] = Update.errorString(); String jsonResponse; serializeJson(response, jsonResponse); webServer.send(500, "application/json", jsonResponse); } } // ===================================================================== // HANDLER: Web Dashboard HTML // ===================================================================== void handleDashboard() { String html = R"rawliteral( ESP32-S3 Modbus Master Dashboard )rawliteral"; webServer.send(200, "text/html", html); } // ===================================================================== // HANDLER: Not Found // ===================================================================== void handleNotFound() { webServer.send(404, "application/json", "{\"error\":\"Not found\"}"); } // ===================================================================== // FUNCTION: Setup Web Server Routes // ===================================================================== void setupWebServer() { webServer.on("/", HTTP_GET, handleDashboard); webServer.on("/api/login", HTTP_POST, handleLogin); webServer.on("/api/relay/status", HTTP_GET, handleGetRelayStatus); webServer.on("/api/relay/control", HTTP_POST, handleControlRelay); webServer.on("/api/logs", HTTP_GET, handleGetLogs); webServer.on("/api/logs/export", HTTP_GET, handleExportLogs); webServer.on("/api/ota/update", HTTP_POST, handleOTAUpdate); webServer.onNotFound(handleNotFound); webServer.begin(); Serial.println("Web server started on port " + String(WEB_SERVER_PORT)); } // ===================================================================== // FUNCTION: Display Relay Status (Console) // ===================================================================== void displayRelayStatus() { Serial.println("\n====== RELAY STATUS ======"); for(uint8_t i = 0; i < NUM_RELAYS; i++) { Serial.print("Relay "); Serial.print(i); Serial.print(": "); Serial.println(relayStates[i] ? "ON" : "OFF"); } Serial.println("=========================\n"); } // ===================================================================== // FUNCTION: Periodic Modbus Health Check // ===================================================================== void performModbusHealthCheck() { const uint32_t HEALTH_CHECK_INTERVAL = 30000; // 30 seconds if(millis() - lastHealthCheck < HEALTH_CHECK_INTERVAL) return; lastHealthCheck = millis(); Serial.println("Performing Modbus health check..."); uint8_t coilValues[NUM_RELAYS]; if(modbusReadCoils(0, NUM_RELAYS, coilValues)) { Serial.println("Health check: Slave is responsive"); // Update relay states based on read values for(uint8_t i = 0; i < NUM_RELAYS; i++) { relayStates[i] = coilValues[i]; } modbusConnected = true; } else { Serial.println("Health check: Slave is not responding"); modbusConnected = false; } } // ===================================================================== // FUNCTION: Initialize System // ===================================================================== void setup() { // Initialize debug serial Serial.begin(115200); delay(1000); Serial.println("\n\n========== ESP32-S3 MODBUS RTU MASTER WITH LVGL DISPLAY =========="); Serial.println("Production-Ready Firmware v1.0"); Serial.println("Features:"); Serial.println(" - Modbus RTU Master on UART1 (GPIO17/18)"); Serial.println(" - ST7262 Driver 800x480 Touch LCD via I2C (GPIO8/9)"); Serial.println(" - LVGL GUI with 8 Relay Control Buttons"); Serial.println(" - WiFi Web Dashboard"); Serial.println(" - REST API with JWT Authentication"); Serial.println(" - OTA Firmware Update"); Serial.println(" - Modbus Logging (1000 entries)"); Serial.println(" - Error Handling & Recovery"); Serial.println("====================================================================\n"); // Initialize storage Serial.println("Initializing storage..."); initializeStorage(); // Initialize LCD display Serial.println("Initializing ST7262 display..."); waveshare_lcd_init(); // Initialize LVGL initializeLVGL(); // Create GUI elements createRelayPanel(); createStatusDisplay(); // Initialize UART1 for Modbus RTU communication Serial.println("Initializing Modbus UART1..."); initializeModbusUART(); // Initialize WiFi Serial.println("Initializing WiFi..."); initializeWiFi(); // Setup web server Serial.println("Setting up web server..."); setupWebServer(); Serial.println("\nSystem initialized successfully!"); Serial.println("Web Dashboard: http://" + WiFi.localIP().toString()); Serial.println("Default Login: admin / admin123"); Serial.println("====================================================================\n"); // Display initial relay status displayRelayStatus(); } // ===================================================================== // FUNCTION: Main Loop // ===================================================================== void loop() { // Handle HTTP requests webServer.handleClient(); // Update LVGL display updateGUIDisplay(); // Perform periodic health checks performModbusHealthCheck(); // Update UI if interval exceeded if(millis() - lastUIRefresh > WEB_DASHBOARD_REFRESH_MS) { lastUIRefresh = millis(); } // Small delay to prevent watchdog timeout delay(10); } /* END CODE */