|
|
|
@ -5,6 +5,7 @@ |
|
|
|
#include <WebServer.h> |
|
|
|
#include <PubSubClient.h> |
|
|
|
#include "credentials.h" |
|
|
|
#include "webpage.h" |
|
|
|
|
|
|
|
#define MQTT_CLID "ValveControl" |
|
|
|
#define MQTT_HOST "192.168.1.146" |
|
|
|
@ -27,16 +28,18 @@ void OnOpen(); |
|
|
|
void OnClose(); |
|
|
|
void OnStatus(); |
|
|
|
void OnRestart(); |
|
|
|
void OnStyle(); |
|
|
|
void OnScript(); |
|
|
|
void OnNotFound(); |
|
|
|
|
|
|
|
String SendHTML(); |
|
|
|
String GetStatusJSON(); |
|
|
|
|
|
|
|
// Actuator control variables
|
|
|
|
ActuatorState currentState = IDLE; |
|
|
|
ActuatorState desiredState = IDLE; |
|
|
|
|
|
|
|
unsigned long operationStartTime = 0; |
|
|
|
const unsigned long OPERATION_DURATION = 60000; // 60 seconds
|
|
|
|
const unsigned long OPERATION_DURATION = 45000; // 45 seconds
|
|
|
|
|
|
|
|
bool ValveState = LOW; |
|
|
|
|
|
|
|
@ -114,6 +117,8 @@ void setup_mqtt() { |
|
|
|
void setup_http() { |
|
|
|
Serial.println("Starting HTTP server..."); |
|
|
|
server.on("/", OnConnect); |
|
|
|
server.on("/style.css", OnStyle); |
|
|
|
server.on("/app.js", OnScript); |
|
|
|
server.on("/open", OnOpen); |
|
|
|
server.on("/close", OnClose); |
|
|
|
server.on("/status", OnStatus); |
|
|
|
@ -125,7 +130,7 @@ void setup_http() { |
|
|
|
|
|
|
|
void setup() { |
|
|
|
|
|
|
|
Serial.begin(115200); |
|
|
|
Serial.begin(9600); |
|
|
|
while(!Serial) { |
|
|
|
delay(100); |
|
|
|
} |
|
|
|
@ -150,8 +155,6 @@ void loop() { |
|
|
|
} |
|
|
|
client.loop(); |
|
|
|
|
|
|
|
server.handleClient(); |
|
|
|
|
|
|
|
// Update actuator state machine (non-blocking)
|
|
|
|
updateActuator(); |
|
|
|
|
|
|
|
@ -166,10 +169,13 @@ void loop() { |
|
|
|
if(buttonCloseState == LOW) { |
|
|
|
closeValve(); |
|
|
|
} |
|
|
|
|
|
|
|
server.handleClient(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
void OnConnect() { |
|
|
|
server.send(200, "text/html", SendHTML()); |
|
|
|
server.send_P(200, "text/html", webpage_html); |
|
|
|
} |
|
|
|
|
|
|
|
void OnOpen() { |
|
|
|
@ -196,6 +202,14 @@ void OnRestart() { |
|
|
|
ESP.restart(); |
|
|
|
} |
|
|
|
|
|
|
|
void OnStyle() { |
|
|
|
server.send_P(200, "text/css", webpage_css); |
|
|
|
} |
|
|
|
|
|
|
|
void OnScript() { |
|
|
|
server.send_P(200, "application/javascript", webpage_js); |
|
|
|
} |
|
|
|
|
|
|
|
void OnNotFound(){ |
|
|
|
server.send(404, "text/plain", "Not found"); |
|
|
|
} |
|
|
|
@ -247,118 +261,6 @@ void updateActuator() { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
String SendHTML() { |
|
|
|
String ptr = "<!DOCTYPE html>\n"; |
|
|
|
ptr += "<html>\n"; |
|
|
|
ptr += "<head>\n"; |
|
|
|
ptr += " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\">\n"; |
|
|
|
ptr += " <title>Linear Actuator Control</title>\n"; |
|
|
|
ptr += " <style>\n"; |
|
|
|
ptr += " * { margin: 0; padding: 0; box-sizing: border-box; }\n"; |
|
|
|
ptr += " html { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; display: flex; height: 100vh; }\n"; |
|
|
|
ptr += " body { display: flex; align-items: center; justify-content: center; width: 100%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); margin: 0; }\n"; |
|
|
|
ptr += " .container { background: white; padding: 40px; border-radius: 12px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); text-align: center; max-width: 400px; }\n"; |
|
|
|
ptr += " h1 { color: #333; margin-bottom: 30px; font-size: 28px; }\n"; |
|
|
|
ptr += " .status-display { background: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 30px; border-left: 4px solid #667eea; }\n"; |
|
|
|
ptr += " .status-text { font-size: 18px; color: #555; margin-bottom: 10px; }\n"; |
|
|
|
ptr += " .status-value { font-size: 24px; font-weight: bold; color: #667eea; }\n"; |
|
|
|
ptr += " .progress-bar { width: 100%; height: 8px; background: #e0e0e0; border-radius: 4px; overflow: hidden; margin-top: 10px; display: none; }\n"; |
|
|
|
ptr += " .progress-bar.active { display: block; }\n"; |
|
|
|
ptr += " .progress-fill { height: 100%; background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); width: 0%; transition: width 0.2s ease; }\n"; |
|
|
|
ptr += " .button { display: inline-block; padding: 15px 40px; font-size: 18px; font-weight: bold; border: none; border-radius: 8px; cursor: pointer; transition: all 0.3s ease; color: white; margin-top: 20px; width: 100%; }\n"; |
|
|
|
ptr += " .button.open { background: linear-gradient(135deg, #1abc9c 0%, #16a085 100%); }\n"; |
|
|
|
ptr += " .button.open:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 10px 20px rgba(26, 188, 156, 0.3); }\n"; |
|
|
|
ptr += " .button.close { background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); }\n"; |
|
|
|
ptr += " .button.close:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 10px 20px rgba(231, 76, 60, 0.3); }\n"; |
|
|
|
ptr += " .button:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }\n"; |
|
|
|
ptr += " .info-text { font-size: 12px; color: #999; margin-top: 15px; }\n"; |
|
|
|
ptr += " </style>\n"; |
|
|
|
ptr += "</head>\n"; |
|
|
|
ptr += "<body>\n"; |
|
|
|
ptr += " <div class=\"container\">\n"; |
|
|
|
ptr += " <h1>Linear Actuator</h1>\n"; |
|
|
|
ptr += " <div class=\"status-display\">\n"; |
|
|
|
ptr += " <div class=\"status-text\">Current Status</div>\n"; |
|
|
|
ptr += " <div class=\"status-value\" id=\"status\">CLOSED</div>\n"; |
|
|
|
ptr += " <div class=\"progress-bar\" id=\"progressBar\">\n"; |
|
|
|
ptr += " <div class=\"progress-fill\" id=\"progressFill\"></div>\n"; |
|
|
|
ptr += " </div>\n"; |
|
|
|
ptr += " </div>\n"; |
|
|
|
ptr += " <button class=\"button open\" id=\"actionButton\" onclick=\"handleAction()\">OPEN</button>\n"; |
|
|
|
ptr += " <div class=\"info-text\">Operation time: <span id=\"opTime\">0</span>s / 60s</div>\n"; |
|
|
|
ptr += " </div>\n"; |
|
|
|
ptr += "\n"; |
|
|
|
ptr += " <script>\n"; |
|
|
|
ptr += " let isOperating = false;\n"; |
|
|
|
ptr += " let statusCheckInterval;\n"; |
|
|
|
ptr += "\n"; |
|
|
|
ptr += " function updateStatus() {\n"; |
|
|
|
ptr += " fetch('/status')\n"; |
|
|
|
ptr += " .then(response => response.json())\n"; |
|
|
|
ptr += " .then(data => {\n"; |
|
|
|
ptr += " const statusEl = document.getElementById('status');\n"; |
|
|
|
ptr += " const btnEl = document.getElementById('actionButton');\n"; |
|
|
|
ptr += " const progBar = document.getElementById('progressBar');\n"; |
|
|
|
ptr += " const progFill = document.getElementById('progressFill');\n"; |
|
|
|
ptr += " const opTimeEl = document.getElementById('opTime');\n"; |
|
|
|
ptr += "\n"; |
|
|
|
ptr += " if(data.state === 'OPENING' || data.state === 'CLOSING') {\n"; |
|
|
|
ptr += " isOperating = true;\n"; |
|
|
|
ptr += " btnEl.disabled = true;\n"; |
|
|
|
ptr += " progBar.classList.add('active');\n"; |
|
|
|
ptr += " statusEl.textContent = data.state;\n"; |
|
|
|
ptr += " const progress = (data.elapsedTime / data.totalTime) * 100;\n"; |
|
|
|
ptr += " progFill.style.width = progress + '%';\n"; |
|
|
|
ptr += " opTimeEl.textContent = Math.round(data.elapsedTime / 1000);\n"; |
|
|
|
ptr += " } else {\n"; |
|
|
|
ptr += " isOperating = false;\n"; |
|
|
|
ptr += " btnEl.disabled = false;\n"; |
|
|
|
ptr += " progBar.classList.remove('active');\n"; |
|
|
|
ptr += " progFill.style.width = '0%';\n"; |
|
|
|
ptr += " opTimeEl.textContent = '0';\n"; |
|
|
|
ptr += " \n"; |
|
|
|
ptr += " if(data.valveState === 1) {\n"; |
|
|
|
ptr += " statusEl.textContent = 'OPEN';\n"; |
|
|
|
ptr += " btnEl.textContent = 'CLOSE';\n"; |
|
|
|
ptr += " btnEl.className = 'button close';\n"; |
|
|
|
ptr += " } else {\n"; |
|
|
|
ptr += " statusEl.textContent = 'CLOSED';\n"; |
|
|
|
ptr += " btnEl.textContent = 'OPEN';\n"; |
|
|
|
ptr += " btnEl.className = 'button open';\n"; |
|
|
|
ptr += " }\n"; |
|
|
|
ptr += " }\n"; |
|
|
|
ptr += " })\n"; |
|
|
|
ptr += " .catch(err => console.error('Status update failed:', err));\n"; |
|
|
|
ptr += " }\n"; |
|
|
|
ptr += "\n"; |
|
|
|
ptr += " function handleAction() {\n"; |
|
|
|
ptr += " if(isOperating) return;\n"; |
|
|
|
ptr += " \n"; |
|
|
|
ptr += " const btnEl = document.getElementById('actionButton');\n"; |
|
|
|
ptr += " const endpoint = btnEl.textContent === 'OPEN' ? '/open' : '/close';\n"; |
|
|
|
ptr += " \n"; |
|
|
|
ptr += " fetch(endpoint)\n"; |
|
|
|
ptr += " .then(() => {\n"; |
|
|
|
ptr += " isOperating = true;\n"; |
|
|
|
ptr += " btnEl.disabled = true;\n"; |
|
|
|
ptr += " updateStatus();\n"; |
|
|
|
ptr += " })\n"; |
|
|
|
ptr += " .catch(err => console.error('Action failed:', err));\n"; |
|
|
|
ptr += " }\n"; |
|
|
|
ptr += "\n"; |
|
|
|
ptr += " // Initial status check\n"; |
|
|
|
ptr += " updateStatus();\n"; |
|
|
|
ptr += "\n"; |
|
|
|
ptr += " // Poll status every 250ms when operating, every 1s when idle\n"; |
|
|
|
ptr += " statusCheckInterval = setInterval(() => {\n"; |
|
|
|
ptr += " updateStatus();\n"; |
|
|
|
ptr += " }, 250);\n"; |
|
|
|
ptr += " </script>\n"; |
|
|
|
ptr += "</body>\n"; |
|
|
|
ptr += "</html>\n"; |
|
|
|
return ptr; |
|
|
|
} |
|
|
|
|
|
|
|
String GetStatusJSON() { |
|
|
|
String json = "{" |
|
|
|
"\"state\":\""; |
|
|
|
|