pleasedontcode

**Modbus Master** rev_05

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