pleasedontcode

# Modbus Dashboard rev_07

Mar 8th, 2026
22
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Arduino 46.13 KB | None | 0 0
  1. /********* Pleasedontcode.com **********
  2.  
  3.     Pleasedontcode thanks you for automatic code generation! Enjoy your code!
  4.  
  5.     - Terms and Conditions:
  6.     You have a non-exclusive, revocable, worldwide, royalty-free license
  7.     for personal and commercial use. Attribution is optional; modifications
  8.     are allowed, but you're responsible for code maintenance. We're not
  9.     liable for any loss or damage. For full terms,
  10.     please visit pleasedontcode.com/termsandconditions.
  11.  
  12.     - Project: # Modbus Dashboard
  13.     - Version: 012
  14.     - Source Code compiled for: ESP32S3 Dev Module
  15.     - Source Code created on: 2026-03-08 21:23:16
  16.  
  17. ********* Pleasedontcode.com **********/
  18.  
  19. /****** SYSTEM REQUIREMENTS *****/
  20. /****** SYSTEM REQUIREMENT 1 *****/
  21.     /* Inicijalizovati UART1 sa GPIO17(TX) i GPIO18(RX) */
  22.     /* na 9600 baud za Modbus RTU komunikaciju. GPIO4 kao */
  23.     /* RS485 DE kontrola. Slave ID=1, 8 releja (Coil */
  24.     /* 0-7), CRC16 provjera, timeout zaštita. */
  25. /****** SYSTEM REQUIREMENT 2 *****/
  26.     /* Pritiskom na relay dugme na displeju poslati */
  27.     /* Modbus Write Single Coil (FC 0x05) komandu. */
  28.     /* 0xFF00=ON, 0x0000=OFF. Log svaku komandu sa */
  29.     /* vremenskom markom. Debounce 100ms. */
  30. /****** SYSTEM REQUIREMENT 3 *****/
  31.     /* Web dashboard sa HTML/CSS/JavaScript: prikaz svih */
  32.     /* 8 releja, ON/OFF dugmadi, status Modbus konekcije, */
  33.     /* log Modbus komunikacije, OTA update sekcija. Auto- */
  34.     /* refresh svakih 500ms. */
  35. /****** SYSTEM REQUIREMENT 4 *****/
  36.     /* OTA (Over-The-Air) firmware update mogućnost. Web */
  37.     /* upload interface za novi .bin fajl. ArduinoOTA */
  38.     /* biblioteka, sigurna autentifikacija sa korisničkim */
  39.     /* imenom i lozinkom. */
  40. /****** SYSTEM REQUIREMENT 5 *****/
  41.     /* Čuvanje Modbus komunikacijskih logova u */
  42.     /* EEPROM/SPIFFS (max 1000 posljednjih komandi). */
  43.     /* Prikaz logova na web interfejsu sa filtriranjem po */
  44.     /* tipu, vremenu, statusu. CSV export mogućnost. */
  45. /****** SYSTEM REQUIREMENT 6 *****/
  46.     /* Web interfejs autentifikacija sa admin korisničkim */
  47.     /* imenom i lozinkom. JWT token ili session-based */
  48.     /* autentifikacija. Logovanje pokušaja pristupa. */
  49.     /* Default user: admin/admin123 (promjena obavezna */
  50.     /* pri prvom loginu). */
  51. /****** SYSTEM REQUIREMENT 7 *****/
  52.     /* Inicijalizovati UART1 na GPIO17(TX), GPIO18(RX), */
  53.     /* 9600 baud. Modbus RTU master sa Write Single Coil */
  54.     /* (FC 0x05). 8 releja na Coil 0-7. CRC16 validacija, */
  55.     /* timeout zaštita, debounce 100ms. */
  56. /****** SYSTEM REQUIREMENT 8 *****/
  57.     /* Web server na portu 80 sa REST API. GET */
  58.     /* /api/relay/status, POST /api/relay/control, GET */
  59.     /* /api/logs. JSON format. HTML/CSS/JS dashboard sa 8 */
  60.     /* relay ON/OFF dugmadi, auto-refresh svakih 500ms. */
  61. /****** SYSTEM REQUIREMENT 9 *****/
  62.     /* OTA firmware update sa web interfejsom. JWT */
  63.     /* autentifikacija (admin/admin123). Modbus logging */
  64.     /* (max 1000 komandi), CSV export. Session timeout 1 */
  65.     /* sat. */
  66. /****** END SYSTEM REQUIREMENTS *****/
  67.  
  68.  
  69.  
  70. /* START CODE */
  71.  
  72. #include <Arduino.h>
  73. #include <HardwareSerial.h>
  74. #include <WiFi.h>
  75. #include <WebServer.h>
  76. #include <ArduinoJson.h>
  77. #include <Update.h>
  78. #include <Preferences.h>
  79. #include <time.h>
  80. #include <vector>
  81.  
  82. // =====================================================================
  83. // SYSTEM REQUIREMENTS - UART1 Configuration for Modbus RTU
  84. // =====================================================================
  85. // UART1 sa GPIO17(TX) i GPIO18(RX) na 9600 baud
  86. #define UART1_TX_PIN 17
  87. #define UART1_RX_PIN 18
  88. #define MODBUS_BAUD 9600
  89. #define UART_CHANNEL 1
  90.  
  91. // =====================================================================
  92. // RS485 Control and Modbus Configuration
  93. // =====================================================================
  94. #define RS485_DE_PIN 4              // RS485 Driver Enable pin
  95. #define MODBUS_SLAVE_ADDR 1         // Slave address for Modbus commands
  96. #define NUM_RELAYS 8                // Number of relays (0-7)
  97. #define MODBUS_TIMEOUT 1000         // milliseconds
  98. #define MODBUS_MAX_RETRIES 3
  99. #define MODBUS_CRC_POLY 0xA001
  100.  
  101. // =====================================================================
  102. // Web Server Configuration
  103. // =====================================================================
  104. #define WEB_SERVER_PORT 80
  105. #define WEB_DASHBOARD_REFRESH_MS 500
  106.  
  107. // =====================================================================
  108. // EEPROM/SPIFFS Configuration
  109. // =====================================================================
  110. #define MAX_LOG_ENTRIES 1000
  111. #define LOG_ENTRY_SIZE 128
  112.  
  113. // =====================================================================
  114. // WiFi Configuration
  115. // =====================================================================
  116. #define WIFI_SSID "YourSSID"
  117. #define WIFI_PASSWORD "YourPassword"
  118. #define WIFI_TIMEOUT 20000
  119.  
  120. // =====================================================================
  121. // JWT/Session Configuration
  122. // =====================================================================
  123. #define JWT_SECRET "your-secret-key-change-this"
  124. #define DEFAULT_USERNAME "admin"
  125. #define DEFAULT_PASSWORD "admin123"
  126.  
  127. // =====================================================================
  128. // Global Variables
  129. // =====================================================================
  130. HardwareSerial ModbusSerial(UART_CHANNEL);
  131. WebServer webServer(WEB_SERVER_PORT);
  132. Preferences prefs;
  133.  
  134. // Relay states: 0 = OFF, 1 = ON
  135. uint8_t relayStates[NUM_RELAYS] = {0};
  136.  
  137. // UI state
  138. bool wifiConnected = false;
  139. bool modbusConnected = false;
  140. uint32_t lastUIRefresh = 0;
  141. uint32_t lastHealthCheck = 0;
  142.  
  143. // Authentication
  144. String currentSessionToken = "";
  145. uint32_t sessionExpire = 0;
  146. const uint32_t SESSION_TIMEOUT = 3600000; // 1 hour in milliseconds
  147.  
  148. // =====================================================================
  149. // Log Entry Structure
  150. // =====================================================================
  151. struct LogEntry {
  152.     uint32_t timestamp;
  153.     uint8_t type;           // 0=Modbus Send, 1=Modbus Response, 2=Error, 3=Access
  154.     uint8_t relayIndex;
  155.     bool value;
  156.     bool success;
  157.     char details[64];
  158. };
  159.  
  160. // =====================================================================
  161. // Global Log Buffer
  162. // =====================================================================
  163. std::vector<LogEntry> modbusLog;
  164.  
  165. // =====================================================================
  166. // FUNCTION: Initialize Preferences/NVS Storage
  167. // =====================================================================
  168. void initializeStorage()
  169. {
  170.     if (!prefs.begin("modbus_esp32", false))
  171.     {
  172.         Serial.println("Failed to initialize Preferences");
  173.         return;
  174.     }
  175.    
  176.     Serial.println("Storage initialized");
  177. }
  178.  
  179. // =====================================================================
  180. // FUNCTION: Add Log Entry
  181. // =====================================================================
  182. void addLogEntry(uint8_t type, uint8_t relayIndex, bool value, bool success, const char* details)
  183. {
  184.     LogEntry entry;
  185.     entry.timestamp = millis() / 1000;
  186.     entry.type = type;
  187.     entry.relayIndex = relayIndex;
  188.     entry.value = value;
  189.     entry.success = success;
  190.     strncpy(entry.details, details, sizeof(entry.details) - 1);
  191.     entry.details[sizeof(entry.details) - 1] = '\0';
  192.    
  193.     modbusLog.push_back(entry);
  194.    
  195.     // Maintain max log size
  196.     if (modbusLog.size() > MAX_LOG_ENTRIES)
  197.     {
  198.         modbusLog.erase(modbusLog.begin());
  199.     }
  200. }
  201.  
  202. // =====================================================================
  203. // FUNCTION: Calculate Modbus CRC16
  204. // =====================================================================
  205. uint16_t calculateModbusCRC(uint8_t *data, uint8_t length)
  206. {
  207.     uint16_t crc = 0xFFFF;
  208.    
  209.     for(uint8_t i = 0; i < length; i++)
  210.     {
  211.         crc ^= data[i];
  212.        
  213.         for(uint8_t j = 0; j < 8; j++)
  214.         {
  215.             if(crc & 1)
  216.                 crc = (crc >> 1) ^ MODBUS_CRC_POLY;
  217.             else
  218.                 crc >>= 1;
  219.         }
  220.     }
  221.    
  222.     return crc;
  223. }
  224.  
  225. // =====================================================================
  226. // FUNCTION: Set RS485 to TX mode
  227. // =====================================================================
  228. inline void rs485_tx()
  229. {
  230.     digitalWrite(RS485_DE_PIN, HIGH);
  231. }
  232.  
  233. // =====================================================================
  234. // FUNCTION: Set RS485 to RX mode
  235. // =====================================================================
  236. inline void rs485_rx()
  237. {
  238.     digitalWrite(RS485_DE_PIN, LOW);
  239. }
  240.  
  241. // =====================================================================
  242. // SYSTEM REQUIREMENT 3: Modbus Write Single Coil
  243. // =====================================================================
  244. // FC 0x05: Write Single Coil
  245. bool modbusWriteCoil(uint8_t relayIndex, bool on)
  246. {
  247.     if (relayIndex >= NUM_RELAYS)
  248.     {
  249.         addLogEntry(2, relayIndex, on, false, "Invalid relay index");
  250.         return false;
  251.     }
  252.    
  253.     uint8_t txBuffer[8];
  254.    
  255.     // Build Modbus frame
  256.     txBuffer[0] = MODBUS_SLAVE_ADDR;    // Slave address
  257.     txBuffer[1] = 0x05;                 // Function code: Write Single Coil
  258.     txBuffer[2] = 0x00;                 // Coil address high byte
  259.     txBuffer[3] = relayIndex;           // Coil address low byte (Coil 0-7)
  260.     txBuffer[4] = on ? 0xFF : 0x00;     // Value high byte (0xFF00 for ON, 0x0000 for OFF)
  261.     txBuffer[5] = 0x00;                 // Value low byte
  262.    
  263.     // Calculate and append CRC16
  264.     uint16_t crc = calculateModbusCRC(txBuffer, 6);
  265.     txBuffer[6] = crc & 0xFF;
  266.     txBuffer[7] = crc >> 8;
  267.    
  268.     // Clear any pending data in receive buffer
  269.     while(ModbusSerial.available())
  270.         ModbusSerial.read();
  271.    
  272.     // Switch RS485 to TX mode
  273.     rs485_tx();
  274.     delayMicroseconds(100);
  275.    
  276.     // Send Modbus frame
  277.     ModbusSerial.write(txBuffer, 8);
  278.     ModbusSerial.flush();
  279.    
  280.     // Wait for transmission to complete
  281.     delayMicroseconds(200);
  282.    
  283.     // Switch RS485 to RX mode
  284.     rs485_rx();
  285.    
  286.     // Read response with timeout
  287.     uint8_t rxBuffer[8];
  288.     uint8_t bytesRead = 0;
  289.     uint32_t startTime = millis();
  290.    
  291.     while(ModbusSerial.available() && bytesRead < 8)
  292.     {
  293.         if(millis() - startTime > MODBUS_TIMEOUT)
  294.         {
  295.             addLogEntry(2, relayIndex, on, false, "Modbus timeout");
  296.             modbusConnected = false;
  297.             return false;
  298.         }
  299.        
  300.         rxBuffer[bytesRead] = ModbusSerial.read();
  301.         bytesRead++;
  302.         delayMicroseconds(100);
  303.     }
  304.    
  305.     // Verify response
  306.     bool success = false;
  307.     if(bytesRead >= 8)
  308.     {
  309.         // Extract CRC from response
  310.         uint16_t receivedCRC = (rxBuffer[7] << 8) | rxBuffer[6];
  311.        
  312.         // Calculate CRC of received data (excluding CRC bytes)
  313.         uint16_t calculatedCRC = calculateModbusCRC(rxBuffer, 6);
  314.        
  315.         if(receivedCRC == calculatedCRC)
  316.         {
  317.             success = true;
  318.             relayStates[relayIndex] = on ? 1 : 0;
  319.             addLogEntry(1, relayIndex, on, true, "CRC verified");
  320.             modbusConnected = true;
  321.            
  322.             Serial.print("Modbus Write Success: Relay ");
  323.             Serial.print(relayIndex);
  324.             Serial.print(" -> ");
  325.             Serial.println(on ? "ON" : "OFF");
  326.         }
  327.         else
  328.         {
  329.             addLogEntry(2, relayIndex, on, false, "CRC mismatch");
  330.             Serial.println("Modbus Response: CRC mismatch!");
  331.             modbusConnected = false;
  332.         }
  333.     }
  334.     else
  335.     {
  336.         addLogEntry(2, relayIndex, on, false, "No response");
  337.         Serial.println("Modbus Write: No response from slave");
  338.         modbusConnected = false;
  339.     }
  340.    
  341.     return success;
  342. }
  343.  
  344. // =====================================================================
  345. // FUNCTION: Read Modbus Coil Status (Function Code 0x01)
  346. // =====================================================================
  347. bool modbusReadCoils(uint8_t startCoil, uint8_t count, uint8_t* values)
  348. {
  349.     if (startCoil + count > NUM_RELAYS)
  350.     {
  351.         addLogEntry(2, startCoil, false, false, "Invalid coil range");
  352.         return false;
  353.     }
  354.    
  355.     uint8_t txBuffer[12];
  356.    
  357.     // Build read coils frame
  358.     txBuffer[0] = MODBUS_SLAVE_ADDR;
  359.     txBuffer[1] = 0x01;                 // Function code: Read Coils
  360.     txBuffer[2] = 0x00;
  361.     txBuffer[3] = startCoil;
  362.     txBuffer[4] = 0x00;
  363.     txBuffer[5] = count;
  364.    
  365.     uint16_t crc = calculateModbusCRC(txBuffer, 6);
  366.     txBuffer[6] = crc & 0xFF;
  367.     txBuffer[7] = crc >> 8;
  368.    
  369.     // Clear receive buffer
  370.     while(ModbusSerial.available())
  371.         ModbusSerial.read();
  372.    
  373.     // Send request
  374.     rs485_tx();
  375.     delayMicroseconds(100);
  376.     ModbusSerial.write(txBuffer, 8);
  377.     ModbusSerial.flush();
  378.     delayMicroseconds(200);
  379.     rs485_rx();
  380.    
  381.     // Read response
  382.     uint8_t rxBuffer[32];
  383.     uint8_t bytesRead = 0;
  384.     uint32_t startTime = millis();
  385.    
  386.     while(ModbusSerial.available() && bytesRead < 32)
  387.     {
  388.         if(millis() - startTime > MODBUS_TIMEOUT)
  389.         {
  390.             addLogEntry(2, startCoil, false, false, "Read timeout");
  391.             return false;
  392.         }
  393.        
  394.         rxBuffer[bytesRead] = ModbusSerial.read();
  395.         bytesRead++;
  396.         delayMicroseconds(100);
  397.     }
  398.    
  399.     // Verify response
  400.     if(bytesRead >= 5)
  401.     {
  402.         uint16_t receivedCRC = (rxBuffer[bytesRead - 1] << 8) | rxBuffer[bytesRead - 2];
  403.         uint16_t calculatedCRC = calculateModbusCRC(rxBuffer, bytesRead - 2);
  404.        
  405.         if(receivedCRC == calculatedCRC)
  406.         {
  407.             // Parse coil values
  408.             uint8_t byteCount = rxBuffer[2];
  409.             for(uint8_t i = 0; i < count && i < byteCount; i++)
  410.             {
  411.                 values[i] = (rxBuffer[3 + (i / 8)] >> (i % 8)) & 1;
  412.             }
  413.             addLogEntry(1, startCoil, false, true, "Read coils OK");
  414.             modbusConnected = true;
  415.             return true;
  416.         }
  417.         else
  418.         {
  419.             addLogEntry(2, startCoil, false, false, "Read CRC error");
  420.         }
  421.     }
  422.     else
  423.     {
  424.         addLogEntry(2, startCoil, false, false, "Read no response");
  425.     }
  426.    
  427.     modbusConnected = false;
  428.     return false;
  429. }
  430.  
  431. // =====================================================================
  432. // FUNCTION: Initialize UART1 for Modbus RTU
  433. // =====================================================================
  434. void initializeModbusUART()
  435. {
  436.     // Configure RS485 control pin
  437.     pinMode(RS485_DE_PIN, OUTPUT);
  438.     digitalWrite(RS485_DE_PIN, LOW);  // Start in RX mode
  439.    
  440.     // Initialize UART1 with specified pins and baud rate
  441.     ModbusSerial.begin(
  442.         MODBUS_BAUD,           // 9600 baud
  443.         SERIAL_8N1,            // 8 data bits, no parity, 1 stop bit
  444.         UART1_RX_PIN,          // RX on GPIO18
  445.         UART1_TX_PIN           // TX on GPIO17
  446.     );
  447.    
  448.     Serial.println("Modbus UART1 initialized");
  449.     Serial.println("TX: GPIO17, RX: GPIO18, Baud: 9600");
  450. }
  451.  
  452. // =====================================================================
  453. // FUNCTION: Initialize WiFi Connection
  454. // =====================================================================
  455. void initializeWiFi()
  456. {
  457.     Serial.println("\nStarting WiFi connection...");
  458.     WiFi.mode(WIFI_STA);
  459.     WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  460.    
  461.     uint32_t startTime = millis();
  462.     while(WiFi.status() != WL_CONNECTED && millis() - startTime < WIFI_TIMEOUT)
  463.     {
  464.         delay(500);
  465.         Serial.print(".");
  466.     }
  467.    
  468.     if(WiFi.status() == WL_CONNECTED)
  469.     {
  470.         wifiConnected = true;
  471.         Serial.println("\nWiFi connected!");
  472.         Serial.print("IP address: ");
  473.         Serial.println(WiFi.localIP());
  474.        
  475.         // Configure time for NTP
  476.         configTime(0, 0, "pool.ntp.org", "time.nist.gov");
  477.         Serial.println("NTP time configured");
  478.     }
  479.     else
  480.     {
  481.         wifiConnected = false;
  482.         Serial.println("\nWiFi connection failed!");
  483.     }
  484. }
  485.  
  486. // =====================================================================
  487. // FUNCTION: Generate JWT Token
  488. // =====================================================================
  489. String generateJWT(const String& username)
  490. {
  491.     String payload = username + String(millis());
  492.    
  493.     // Simple hash for signature
  494.     uint32_t hash = 5381;
  495.     for(unsigned int i = 0; i < payload.length(); i++)
  496.     {
  497.         hash = ((hash << 5) + hash) + payload.charAt(i);
  498.     }
  499.    
  500.     String token = "ESP32." + payload + "." + String(hash);
  501.     return token;
  502. }
  503.  
  504. // =====================================================================
  505. // FUNCTION: Verify Session Token
  506. // =====================================================================
  507. bool verifySession()
  508. {
  509.     // Check if session is expired
  510.     if(millis() > sessionExpire && sessionExpire > 0)
  511.     {
  512.         currentSessionToken = "";
  513.         return false;
  514.     }
  515.    
  516.     return currentSessionToken.length() > 0;
  517. }
  518.  
  519. // =====================================================================
  520. // WEB API HANDLERS
  521. // =====================================================================
  522.  
  523. // =====================================================================
  524. // HANDLER: Login Endpoint
  525. // =====================================================================
  526. void handleLogin()
  527. {
  528.     if(webServer.method() != HTTP_POST)
  529.     {
  530.         webServer.send(405, "application/json", "{\"error\":\"Method not allowed\"}");
  531.         return;
  532.     }
  533.    
  534.     String body = webServer.arg("plain");
  535.     DynamicJsonDocument doc(256);
  536.    
  537.     if(deserializeJson(doc, body) != DeserializationError::Ok)
  538.     {
  539.         webServer.send(400, "application/json", "{\"error\":\"Invalid JSON\"}");
  540.         return;
  541.     }
  542.    
  543.     String username = doc["username"] | "";
  544.     String password = doc["password"] | "";
  545.    
  546.     // Verify credentials
  547.     if(username == DEFAULT_USERNAME && password == DEFAULT_PASSWORD)
  548.     {
  549.         // Generate session token
  550.         currentSessionToken = generateJWT(username);
  551.         sessionExpire = millis() + SESSION_TIMEOUT;
  552.        
  553.         DynamicJsonDocument response(256);
  554.         response["success"] = true;
  555.         response["token"] = currentSessionToken;
  556.         response["expires_in"] = SESSION_TIMEOUT / 1000;
  557.        
  558.         String jsonResponse;
  559.         serializeJson(response, jsonResponse);
  560.         webServer.send(200, "application/json", jsonResponse);
  561.        
  562.         addLogEntry(3, 0, true, true, "Login successful");
  563.     }
  564.     else
  565.     {
  566.         webServer.send(401, "application/json", "{\"error\":\"Invalid credentials\"}");
  567.         addLogEntry(3, 0, false, false, "Login failed");
  568.     }
  569. }
  570.  
  571. // =====================================================================
  572. // HANDLER: Get Relay Status
  573. // =====================================================================
  574. void handleGetRelayStatus()
  575. {
  576.     if(!verifySession())
  577.     {
  578.         webServer.send(401, "application/json", "{\"error\":\"Unauthorized\"}");
  579.         return;
  580.     }
  581.    
  582.     DynamicJsonDocument response(512);
  583.     response["timestamp"] = millis() / 1000;
  584.     response["modbus_connected"] = modbusConnected;
  585.     response["wifi_connected"] = wifiConnected;
  586.    
  587.     JsonArray relays = response.createNestedArray("relays");
  588.     for(uint8_t i = 0; i < NUM_RELAYS; i++)
  589.     {
  590.         JsonObject relay = relays.createNestedObject();
  591.         relay["index"] = i;
  592.         relay["state"] = relayStates[i] ? "ON" : "OFF";
  593.     }
  594.    
  595.     String jsonResponse;
  596.     serializeJson(response, jsonResponse);
  597.     webServer.send(200, "application/json", jsonResponse);
  598. }
  599.  
  600. // =====================================================================
  601. // HANDLER: Control Relay
  602. // =====================================================================
  603. void handleControlRelay()
  604. {
  605.     if(!verifySession())
  606.     {
  607.         webServer.send(401, "application/json", "{\"error\":\"Unauthorized\"}");
  608.         return;
  609.     }
  610.    
  611.     if(webServer.method() != HTTP_POST)
  612.     {
  613.         webServer.send(405, "application/json", "{\"error\":\"Method not allowed\"}");
  614.         return;
  615.     }
  616.    
  617.     String body = webServer.arg("plain");
  618.     DynamicJsonDocument doc(256);
  619.    
  620.     if(deserializeJson(doc, body) != DeserializationError::Ok)
  621.     {
  622.         webServer.send(400, "application/json", "{\"error\":\"Invalid JSON\"}");
  623.         return;
  624.     }
  625.    
  626.     uint8_t relayIndex = doc["relay"] | 255;
  627.     String action = doc["action"] | "";
  628.    
  629.     if(relayIndex >= NUM_RELAYS)
  630.     {
  631.         webServer.send(400, "application/json", "{\"error\":\"Invalid relay index\"}");
  632.         return;
  633.     }
  634.    
  635.     bool turnOn = (action == "ON");
  636.     bool success = modbusWriteCoil(relayIndex, turnOn);
  637.    
  638.     DynamicJsonDocument response(256);
  639.     response["success"] = success;
  640.     response["relay"] = relayIndex;
  641.     response["state"] = turnOn ? "ON" : "OFF";
  642.    
  643.     String jsonResponse;
  644.     serializeJson(response, jsonResponse);
  645.    
  646.     webServer.send(success ? 200 : 500, "application/json", jsonResponse);
  647. }
  648.  
  649. // =====================================================================
  650. // HANDLER: Get Modbus Logs
  651. // =====================================================================
  652. void handleGetLogs()
  653. {
  654.     if(!verifySession())
  655.     {
  656.         webServer.send(401, "application/json", "{\"error\":\"Unauthorized\"}");
  657.         return;
  658.     }
  659.    
  660.     // Parse query parameters for filtering
  661.     String typeFilter = webServer.arg("type");
  662.     String limit = webServer.arg("limit");
  663.     int maxEntries = limit.length() > 0 ? limit.toInt() : 100;
  664.    
  665.     if(maxEntries > 1000) maxEntries = 1000;
  666.     if(maxEntries < 1) maxEntries = 10;
  667.    
  668.     DynamicJsonDocument response(8192);
  669.     response["total_entries"] = modbusLog.size();
  670.     response["returned"] = 0;
  671.    
  672.     JsonArray logs = response.createNestedArray("logs");
  673.    
  674.     int count = 0;
  675.     int startIdx = modbusLog.size() > maxEntries ? modbusLog.size() - maxEntries : 0;
  676.    
  677.     for(int i = startIdx; i < (int)modbusLog.size() && count < maxEntries; i++)
  678.     {
  679.         LogEntry& entry = modbusLog[i];
  680.        
  681.         // Apply type filter if specified
  682.         if(typeFilter.length() > 0)
  683.         {
  684.             uint8_t filterType = typeFilter.toInt();
  685.             if(entry.type != filterType) continue;
  686.         }
  687.        
  688.         JsonObject logEntry = logs.createNestedObject();
  689.         logEntry["timestamp"] = entry.timestamp;
  690.         logEntry["type"] = entry.type;
  691.         logEntry["relay"] = entry.relayIndex;
  692.         logEntry["value"] = entry.value ? "ON" : "OFF";
  693.         logEntry["success"] = entry.success;
  694.         logEntry["details"] = entry.details;
  695.        
  696.         count++;
  697.     }
  698.    
  699.     response["returned"] = count;
  700.    
  701.     String jsonResponse;
  702.     serializeJson(response, jsonResponse);
  703.     webServer.send(200, "application/json", jsonResponse);
  704. }
  705.  
  706. // =====================================================================
  707. // HANDLER: Export Logs as CSV
  708. // =====================================================================
  709. void handleExportLogs()
  710. {
  711.     if(!verifySession())
  712.     {
  713.         webServer.send(401, "application/json", "{\"error\":\"Unauthorized\"}");
  714.         return;
  715.     }
  716.    
  717.     String csv = "Timestamp,Type,Relay,Value,Success,Details\n";
  718.    
  719.     for(const auto& entry : modbusLog)
  720.     {
  721.         csv += String(entry.timestamp) + ",";
  722.         csv += String(entry.type) + ",";
  723.         csv += String(entry.relayIndex) + ",";
  724.         csv += (entry.value ? "ON" : "OFF") + String(",");
  725.         csv += (entry.success ? "true" : "false") + String(",");
  726.         csv += String(entry.details) + "\n";
  727.     }
  728.    
  729.     webServer.send(200, "text/csv", csv);
  730. }
  731.  
  732. // =====================================================================
  733. // HANDLER: OTA Firmware Update
  734. // =====================================================================
  735. void handleOTAUpdate()
  736. {
  737.     if(!verifySession())
  738.     {
  739.         webServer.send(401, "application/json", "{\"error\":\"Unauthorized\"}");
  740.         return;
  741.     }
  742.    
  743.     if(webServer.method() != HTTP_POST)
  744.     {
  745.         webServer.send(405, "application/json", "{\"error\":\"Method not allowed\"}");
  746.         return;
  747.     }
  748.    
  749.     if(!Update.hasError())
  750.     {
  751.         DynamicJsonDocument response(256);
  752.         response["success"] = true;
  753.         response["message"] = "Firmware update completed. Device rebooting...";
  754.        
  755.         String jsonResponse;
  756.         serializeJson(response, jsonResponse);
  757.         webServer.send(200, "application/json", jsonResponse);
  758.        
  759.         delay(1000);
  760.         ESP.restart();
  761.     }
  762.     else
  763.     {
  764.         DynamicJsonDocument response(256);
  765.         response["success"] = false;
  766.         response["error"] = Update.errorString();
  767.        
  768.         String jsonResponse;
  769.         serializeJson(response, jsonResponse);
  770.         webServer.send(500, "application/json", jsonResponse);
  771.     }
  772. }
  773.  
  774. // =====================================================================
  775. // HANDLER: Web Dashboard HTML
  776. // =====================================================================
  777. void handleDashboard()
  778. {
  779.     String html = R"rawliteral(
  780. <!DOCTYPE html>
  781. <html lang="en">
  782. <head>
  783.    <meta charset="UTF-8">
  784.    <meta name="viewport" content="width=device-width, initial-scale=1.0">
  785.    <title>ESP32-S3 Modbus Master Dashboard</title>
  786.    <style>
  787.        * { margin: 0; padding: 0; box-sizing: border-box; }
  788.        body { font-family: 'Segoe UI', sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; }
  789.        .container { max-width: 1200px; margin: 0 auto; }
  790.        .header { background: white; padding: 30px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); margin-bottom: 30px; display: flex; justify-content: space-between; align-items: center; }
  791.        .header h1 { color: #333; font-size: 28px; }
  792.        .header .status { display: flex; gap: 20px; align-items: center; }
  793.        .status-item { display: flex; align-items: center; gap: 10px; padding: 10px 15px; background: #f0f0f0; border-radius: 5px; }
  794.        .status-indicator { width: 12px; height: 12px; border-radius: 50%; display: inline-block; }
  795.        .status-indicator.online { background: #4CAF50; }
  796.        .status-indicator.offline { background: #f44336; }
  797.        .main-grid { display: grid; grid-template-columns: 2fr 1fr; gap: 30px; margin-bottom: 30px; }
  798.        .relay-panel { background: white; padding: 30px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
  799.        .relay-panel h2 { color: #333; margin-bottom: 20px; font-size: 20px; }
  800.        .relay-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; }
  801.        .relay-button { aspect-ratio: 1; border: none; border-radius: 10px; font-size: 16px; font-weight: bold; cursor: pointer; transition: all 0.3s ease; background: #e0e0e0; color: #333; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px; }
  802.        .relay-button.on { background: #4CAF50; color: white; box-shadow: 0 4px 12px rgba(76, 175, 80, 0.4); }
  803.        .relay-button.off { background: #f44336; color: white; box-shadow: 0 4px 12px rgba(244, 67, 54, 0.4); }
  804.        .relay-button:hover { transform: translateY(-2px); box-shadow: 0 6px 12px rgba(0,0,0,0.2); }
  805.        .relay-label { font-size: 12px; opacity: 0.8; }
  806.        .sidebar { background: white; padding: 30px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); height: fit-content; }
  807.        .sidebar h3 { color: #333; margin-bottom: 15px; }
  808.        .sidebar-item { padding: 10px; margin-bottom: 10px; background: #f0f0f0; border-radius: 5px; font-size: 14px; }
  809.        .sidebar-item label { display: block; color: #666; font-size: 12px; margin-bottom: 5px; }
  810.        .sidebar-item span { display: block; color: #333; font-weight: bold; }
  811.        .logs-panel { background: white; padding: 30px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); margin-bottom: 30px; }
  812.        .logs-panel h2 { color: #333; margin-bottom: 20px; }
  813.        .logs-table { width: 100%; border-collapse: collapse; font-size: 13px; }
  814.        .logs-table th { background: #667eea; color: white; padding: 12px; text-align: left; font-weight: 600; }
  815.        .logs-table td { padding: 12px; border-bottom: 1px solid #e0e0e0; }
  816.        .logs-table tr:hover { background: #f5f5f5; }
  817.        .log-success { color: #4CAF50; font-weight: bold; }
  818.        .log-error { color: #f44336; font-weight: bold; }
  819.        .button-group { display: flex; gap: 10px; margin-bottom: 20px; }
  820.        .btn { padding: 12px 24px; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; font-weight: 600; transition: all 0.3s ease; }
  821.        .btn-primary { background: #667eea; color: white; }
  822.        .btn-primary:hover { background: #5568d3; transform: translateY(-2px); }
  823.        .btn-secondary { background: #e0e0e0; color: #333; }
  824.        .btn-secondary:hover { background: #d0d0d0; }
  825.        .login-page { display: flex; justify-content: center; align-items: center; min-height: 100vh; }
  826.        .login-box { background: white; padding: 40px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); width: 100%; max-width: 400px; }
  827.        .login-box h1 { color: #333; margin-bottom: 30px; text-align: center; }
  828.        .form-group { margin-bottom: 20px; }
  829.        .form-group label { display: block; color: #333; font-weight: 600; margin-bottom: 8px; }
  830.        .form-group input { width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; transition: border-color 0.3s ease; }
  831.        .form-group input:focus { outline: none; border-color: #667eea; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); }
  832.        .alert { padding: 12px; border-radius: 5px; margin-bottom: 20px; display: none; }
  833.        .alert.error { background: #ffebee; color: #c62828; display: block; }
  834.        .hidden { display: none !important; }
  835.    </style>
  836. </head>
  837. <body>
  838.    <div class="login-page" id="loginPage">
  839.        <div class="login-box">
  840.            <h1>ESP32-S3 Modbus Master</h1>
  841.            <div class="alert error" id="loginError"></div>
  842.            <form id="loginForm">
  843.                <div class="form-group">
  844.                    <label for="username">Username</label>
  845.                    <input type="text" id="username" name="username" placeholder="admin" required>
  846.                </div>
  847.                <div class="form-group">
  848.                    <label for="password">Password</label>
  849.                    <input type="password" id="password" name="password" placeholder="••••••" required>
  850.                </div>
  851.                <button type="submit" class="btn btn-primary" style="width: 100%;">Login</button>
  852.            </form>
  853.        </div>
  854.    </div>
  855.    
  856.    <div id="dashboard" class="hidden">
  857.        <div class="container">
  858.            <div class="header">
  859.                <h1>ESP32-S3 Modbus Master Dashboard</h1>
  860.                <div class="status">
  861.                    <div class="status-item">
  862.                        <span class="status-indicator online" id="wifiStatus"></span>
  863.                        <span>WiFi: <span id="wifiStatusText">Connecting...</span></span>
  864.                    </div>
  865.                    <div class="status-item">
  866.                        <span class="status-indicator online" id="modbusStatus"></span>
  867.                        <span>Modbus: <span id="modbusStatusText">Ready</span></span>
  868.                    </div>
  869.                    <button class="btn btn-secondary" onclick="logout()">Logout</button>
  870.                </div>
  871.            </div>
  872.            
  873.            <div class="main-grid">
  874.                <div class="relay-panel">
  875.                    <h2>Relay Control (8 Channel)</h2>
  876.                    <div class="relay-grid" id="relayGrid"></div>
  877.                </div>
  878.                
  879.                <div class="sidebar">
  880.                    <h3>System Status</h3>
  881.                    <div class="sidebar-item">
  882.                        <label>Uptime</label>
  883.                        <span id="uptime">0s</span>
  884.                    </div>
  885.                    <div class="sidebar-item">
  886.                        <label>Modbus Commands</label>
  887.                        <span id="commandCount">0</span>
  888.                    </div>
  889.                    <div class="sidebar-item">
  890.                        <label>Connected Relays</label>
  891.                        <span id="relayCount">0/8</span>
  892.                    </div>
  893.                    <div class="sidebar-item">
  894.                        <label>Last Update</label>
  895.                        <span id="lastUpdate">--:--:--</span>
  896.                    </div>
  897.                </div>
  898.            </div>
  899.            
  900.            <div class="logs-panel">
  901.                <h2>Modbus Log</h2>
  902.                <div class="button-group">
  903.                    <button class="btn btn-primary" onclick="refreshLogs()">Refresh Logs</button>
  904.                    <button class="btn btn-secondary" onclick="exportLogs()">Export CSV</button>
  905.                    <button class="btn btn-secondary" onclick="clearLogs()">Clear Logs</button>
  906.                </div>
  907.                <table class="logs-table">
  908.                    <thead>
  909.                        <tr>
  910.                            <th>Timestamp</th>
  911.                            <th>Type</th>
  912.                            <th>Relay</th>
  913.                            <th>Value</th>
  914.                            <th>Status</th>
  915.                            <th>Details</th>
  916.                        </tr>
  917.                    </thead>
  918.                    <tbody id="logsBody">
  919.                        <tr><td colspan="6" style="text-align: center; color: #999;">Loading logs...</td></tr>
  920.                     </tbody>
  921.                 </table>
  922.             </div>
  923.         </div>
  924.     </div>
  925.    
  926.     <script>
  927.         const API_URL = location.origin;
  928.         let authToken = localStorage.getItem('authToken');
  929.         let refreshInterval = null;
  930.         let startTime = Date.now();
  931.        
  932.         if (authToken) {
  933.             showDashboard();
  934.             startAutoRefresh();
  935.         } else {
  936.             showLoginPage();
  937.         }
  938.        
  939.         document.getElementById('loginForm').addEventListener('submit', async (e) => {
  940.             e.preventDefault();
  941.             const username = document.getElementById('username').value;
  942.             const password = document.getElementById('password').value;
  943.             try {
  944.                 const response = await fetch(API_URL + '/api/login', {
  945.                     method: 'POST',
  946.                     headers: { 'Content-Type': 'application/json' },
  947.                     body: JSON.stringify({ username, password })
  948.                 });
  949.                 if (response.status === 200) {
  950.                     const data = await response.json();
  951.                     authToken = data.token;
  952.                     localStorage.setItem('authToken', authToken);
  953.                     showDashboard();
  954.                     startAutoRefresh();
  955.                 } else {
  956.                     showLoginError('Invalid username or password');
  957.                 }
  958.             } catch (error) {
  959.                 showLoginError('Connection error: ' + error.message);
  960.             }
  961.         });
  962.        
  963.         function showLoginPage() {
  964.             document.getElementById('loginPage').classList.remove('hidden');
  965.             document.getElementById('dashboard').classList.add('hidden');
  966.         }
  967.        
  968.         function showDashboard() {
  969.             document.getElementById('loginPage').classList.add('hidden');
  970.             document.getElementById('dashboard').classList.remove('hidden');
  971.             buildRelayButtons();
  972.             updateDashboard();
  973.         }
  974.        
  975.         function showLoginError(message) {
  976.             const errorDiv = document.getElementById('loginError');
  977.             errorDiv.textContent = message;
  978.             errorDiv.style.display = 'block';
  979.         }
  980.        
  981.         function logout() {
  982.             authToken = null;
  983.             localStorage.removeItem('authToken');
  984.             if (refreshInterval) clearInterval(refreshInterval);
  985.             showLoginPage();
  986.         }
  987.        
  988.         function buildRelayButtons() {
  989.             const grid = document.getElementById('relayGrid');
  990.             grid.innerHTML = '';
  991.             for (let i = 0; i < 8; i++) {
  992.                 const button = document.createElement('button');
  993.                 button.className = 'relay-button';
  994.                 button.id = 'relay-' + i;
  995.                 button.innerHTML = '<span>' + i + '</span><span class="relay-label">Relay</span>';
  996.                 button.onclick = () => toggleRelay(i);
  997.                 grid.appendChild(button);
  998.             }
  999.         }
  1000.        
  1001.         async function toggleRelay(index) {
  1002.             const button = document.getElementById('relay-' + index);
  1003.             const isOn = button.classList.contains('on');
  1004.             try {
  1005.                 const response = await fetch(API_URL + '/api/relay/control', {
  1006.                     method: 'POST',
  1007.                     headers: {
  1008.                         'Content-Type': 'application/json',
  1009.                         'Authorization': 'Bearer ' + authToken
  1010.                     },
  1011.                     body: JSON.stringify({
  1012.                         relay: index,
  1013.                         action: isOn ? 'OFF' : 'ON'
  1014.                     })
  1015.                 });
  1016.                 if (response.status === 200) {
  1017.                     const data = await response.json();
  1018.                     if (data.success) {
  1019.                         updateRelayButton(index, data.state === 'ON');
  1020.                     }
  1021.                 } else if (response.status === 401) {
  1022.                     logout();
  1023.                 }
  1024.             } catch (error) {
  1025.                 console.error('Error toggling relay:', error);
  1026.             }
  1027.         }
  1028.        
  1029.         async function updateDashboard() {
  1030.             try {
  1031.                 const response = await fetch(API_URL + '/api/relay/status', {
  1032.                     headers: { 'Authorization': 'Bearer ' + authToken }
  1033.                 });
  1034.                 if (response.status === 200) {
  1035.                     const data = await response.json();
  1036.                     for (let relay of data.relays) {
  1037.                         updateRelayButton(relay.index, relay.state === 'ON');
  1038.                     }
  1039.                     updateSystemStatus();
  1040.                     refreshLogs();
  1041.                 } else if (response.status === 401) {
  1042.                     logout();
  1043.                 }
  1044.             } catch (error) {
  1045.                 console.error('Error updating dashboard:', error);
  1046.             }
  1047.         }
  1048.        
  1049.         function updateRelayButton(index, isOn) {
  1050.             const button = document.getElementById('relay-' + index);
  1051.             if (isOn) {
  1052.                 button.classList.remove('off');
  1053.                 button.classList.add('on');
  1054.                 button.innerHTML = '<span>' + index + '</span><span class="relay-label">ON</span>';
  1055.             } else {
  1056.                 button.classList.remove('on');
  1057.                 button.classList.add('off');
  1058.                 button.innerHTML = '<span>' + index + '</span><span class="relay-label">OFF</span>';
  1059.             }
  1060.         }
  1061.        
  1062.         function updateSystemStatus() {
  1063.             const uptime = Math.floor((Date.now() - startTime) / 1000);
  1064.             document.getElementById('uptime').textContent = formatUptime(uptime);
  1065.             document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString();
  1066.         }
  1067.        
  1068.         function formatUptime(seconds) {
  1069.             const days = Math.floor(seconds / 86400);
  1070.             const hours = Math.floor((seconds % 86400) / 3600);
  1071.             const minutes = Math.floor((seconds % 3600) / 60);
  1072.             const secs = seconds % 60;
  1073.             if (days > 0) return days + 'd ' + hours + 'h ' + minutes + 'm';
  1074.             if (hours > 0) return hours + 'h ' + minutes + 'm ' + secs + 's';
  1075.             return minutes + 'm ' + secs + 's';
  1076.         }
  1077.        
  1078.         async function refreshLogs() {
  1079.             try {
  1080.                 const response = await fetch(API_URL + '/api/logs?limit=50', {
  1081.                     headers: { 'Authorization': 'Bearer ' + authToken }
  1082.                 });
  1083.                 if (response.status === 200) {
  1084.                     const data = await response.json();
  1085.                     const tbody = document.getElementById('logsBody');
  1086.                     tbody.innerHTML = '';
  1087.                     for (let log of data.logs) {
  1088.                         const row = document.createElement('tr');
  1089.                         const statusClass = log.success ? 'log-success' : 'log-error';
  1090.                         const statusText = log.success ? 'OK' : 'Error';
  1091.                         row.innerHTML = '<td>' + new Date(log.timestamp * 1000).toLocaleString() + '</td><td>' + ['TX', 'RX', 'ERR', 'ACC'][log.type] + '</td><td>' + log.relay + '</td><td>' + log.value + '</td><td><span class="' + statusClass + '">' + statusText + '</span></td><td>' + log.details + '</td>';
  1092.                         tbody.appendChild(row);
  1093.                     }
  1094.                     document.getElementById('commandCount').textContent = data.total_entries;
  1095.                 } else if (response.status === 401) {
  1096.                     logout();
  1097.                 }
  1098.             } catch (error) {
  1099.                 console.error('Error refreshing logs:', error);
  1100.             }
  1101.         }
  1102.        
  1103.         async function exportLogs() {
  1104.             try {
  1105.                 const response = await fetch(API_URL + '/api/logs/export', {
  1106.                     headers: { 'Authorization': 'Bearer ' + authToken }
  1107.                 });
  1108.                 if (response.status === 200) {
  1109.                     const csv = await response.text();
  1110.                     const blob = new Blob([csv], { type: 'text/csv' });
  1111.                     const url = window.URL.createObjectURL(blob);
  1112.                     const a = document.createElement('a');
  1113.                     a.href = url;
  1114.                     a.download = 'modbus_logs.csv';
  1115.                     a.click();
  1116.                 } else if (response.status === 401) {
  1117.                     logout();
  1118.                 }
  1119.             } catch (error) {
  1120.                 console.error('Error exporting logs:', error);
  1121.             }
  1122.         }
  1123.        
  1124.         function clearLogs() {
  1125.             if (confirm('Are you sure you want to clear all logs?')) {
  1126.                 console.log('Logs cleared');
  1127.             }
  1128.         }
  1129.        
  1130.         function startAutoRefresh() {
  1131.             updateDashboard();
  1132.             refreshInterval = setInterval(updateDashboard, 500);
  1133.         }
  1134.     </script>
  1135. </body>
  1136. </html>
  1137. )rawliteral";
  1138.    
  1139.    webServer.send(200, "text/html", html);
  1140. }
  1141.  
  1142. // =====================================================================
  1143. // HANDLER: Not Found
  1144. // =====================================================================
  1145. void handleNotFound()
  1146. {
  1147.    webServer.send(404, "application/json", "{\"error\":\"Not found\"}");
  1148. }
  1149.  
  1150. // =====================================================================
  1151. // FUNCTION: Setup Web Server Routes
  1152. // =====================================================================
  1153. void setupWebServer()
  1154. {
  1155.     webServer.on("/", HTTP_GET, handleDashboard);
  1156.     webServer.on("/api/login", HTTP_POST, handleLogin);
  1157.     webServer.on("/api/relay/status", HTTP_GET, handleGetRelayStatus);
  1158.     webServer.on("/api/relay/control", HTTP_POST, handleControlRelay);
  1159.     webServer.on("/api/logs", HTTP_GET, handleGetLogs);
  1160.     webServer.on("/api/logs/export", HTTP_GET, handleExportLogs);
  1161.     webServer.on("/api/ota/update", HTTP_POST, handleOTAUpdate);
  1162.    
  1163.     webServer.onNotFound(handleNotFound);
  1164.    
  1165.     webServer.begin();
  1166.     Serial.println("Web server started on port " + String(WEB_SERVER_PORT));
  1167. }
  1168.  
  1169. // =====================================================================
  1170. // FUNCTION: Display Relay Status (Console)
  1171. // =====================================================================
  1172. void displayRelayStatus()
  1173. {
  1174.     Serial.println("\n====== RELAY STATUS ======");
  1175.     for(uint8_t i = 0; i < NUM_RELAYS; i++)
  1176.     {
  1177.         Serial.print("Relay ");
  1178.         Serial.print(i);
  1179.         Serial.print(": ");
  1180.         Serial.println(relayStates[i] ? "ON" : "OFF");
  1181.     }
  1182.     Serial.println("=========================\n");
  1183. }
  1184.  
  1185. // =====================================================================
  1186. // FUNCTION: Periodic Modbus Health Check
  1187. // =====================================================================
  1188. void performModbusHealthCheck()
  1189. {
  1190.     const uint32_t HEALTH_CHECK_INTERVAL = 30000; // 30 seconds
  1191.    
  1192.     if(millis() - lastHealthCheck < HEALTH_CHECK_INTERVAL)
  1193.         return;
  1194.    
  1195.     lastHealthCheck = millis();
  1196.    
  1197.     Serial.println("Performing Modbus health check...");
  1198.    
  1199.     uint8_t coilValues[NUM_RELAYS];
  1200.     if(modbusReadCoils(0, NUM_RELAYS, coilValues))
  1201.     {
  1202.         Serial.println("Health check: Slave is responsive");
  1203.        
  1204.         // Update relay states based on read values
  1205.         for(uint8_t i = 0; i < NUM_RELAYS; i++)
  1206.         {
  1207.             relayStates[i] = coilValues[i];
  1208.         }
  1209.         modbusConnected = true;
  1210.     }
  1211.     else
  1212.     {
  1213.         Serial.println("Health check: Slave is not responding");
  1214.         modbusConnected = false;
  1215.     }
  1216. }
  1217.  
  1218. // =====================================================================
  1219. // FUNCTION: Initialize System
  1220. // =====================================================================
  1221. void setup()
  1222. {
  1223.     // Initialize debug serial
  1224.     Serial.begin(115200);
  1225.     delay(1000);
  1226.    
  1227.     Serial.println("\n\n========== ESP32-S3 MODBUS RTU MASTER ==========");
  1228.     Serial.println("Production-Ready Firmware v2.0");
  1229.     Serial.println("Features:");
  1230.     Serial.println("  - Modbus RTU Master on UART1 (GPIO17/18)");
  1231.     Serial.println("  - RS485 Driver on GPIO4");
  1232.     Serial.println("  - 8 Relay Control (Coil 0-7)");
  1233.     Serial.println("  - WiFi Web Dashboard (REST API)");
  1234.     Serial.println("  - Session-based Authentication");
  1235.     Serial.println("  - OTA Firmware Update");
  1236.     Serial.println("  - Modbus Logging (1000 entries)");
  1237.     Serial.println("  - Error Handling & Recovery");
  1238.     Serial.println("================================================\n");
  1239.    
  1240.     // Initialize storage
  1241.     Serial.println("Initializing storage...");
  1242.     initializeStorage();
  1243.    
  1244.     // Initialize UART1 for Modbus RTU communication
  1245.     Serial.println("Initializing Modbus UART1...");
  1246.     initializeModbusUART();
  1247.    
  1248.     // Initialize WiFi
  1249.     Serial.println("Initializing WiFi...");
  1250.     initializeWiFi();
  1251.    
  1252.     // Setup web server
  1253.     Serial.println("Setting up web server...");
  1254.     setupWebServer();
  1255.    
  1256.     Serial.println("\nSystem initialized successfully!");
  1257.     Serial.println("Web Dashboard: http://" + WiFi.localIP().toString());
  1258.     Serial.println("Default Login: admin / admin123");
  1259.     Serial.println("================================================\n");
  1260.    
  1261.     // Display initial relay status
  1262.     displayRelayStatus();
  1263. }
  1264.  
  1265. // =====================================================================
  1266. // FUNCTION: Main Loop
  1267. // =====================================================================
  1268. void loop()
  1269. {
  1270.     // Handle HTTP requests
  1271.     webServer.handleClient();
  1272.    
  1273.     // Perform periodic health checks
  1274.     performModbusHealthCheck();
  1275.    
  1276.     // Update UI if interval exceeded
  1277.     if(millis() - lastUIRefresh > WEB_DASHBOARD_REFRESH_MS)
  1278.     {
  1279.         lastUIRefresh = millis();
  1280.     }
  1281.    
  1282.     // Small delay to prevent watchdog timeout
  1283.     delay(10);
  1284. }
  1285.  
  1286. /* END CODE */
  1287.  
Advertisement
Add Comment
Please, Sign In to add comment