diff --git a/platformio.ini b/platformio.ini index f423ca6..8e68c2c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -25,6 +25,6 @@ board = esp32dev framework = arduino upload_protocol = espota upload_port = 192.168.1.235 -upload_flags = --auth=P@ssw0rd +upload_flags = --auth=P@ssword lib_deps = knolleary/PubSubClient@^2.8 diff --git a/src/main.cpp b/src/main.cpp index 9b252eb..7f64328 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,6 +5,7 @@ #include #include #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 = "\n"; - ptr += "\n"; - ptr += "\n"; - ptr += " \n"; - ptr += " Linear Actuator Control\n"; - ptr += " \n"; - ptr += "\n"; - ptr += "\n"; - ptr += "
\n"; - ptr += "

Linear Actuator

\n"; - ptr += "
\n"; - ptr += "
Current Status
\n"; - ptr += "
CLOSED
\n"; - ptr += "
\n"; - ptr += "
\n"; - ptr += "
\n"; - ptr += "
\n"; - ptr += " \n"; - ptr += "
Operation time: 0s / 60s
\n"; - ptr += "
\n"; - ptr += "\n"; - ptr += " \n"; - ptr += "\n"; - ptr += "\n"; - return ptr; -} - String GetStatusJSON() { String json = "{" "\"state\":\""; diff --git a/src/webpage.cpp b/src/webpage.cpp new file mode 100644 index 0000000..b70e5c9 --- /dev/null +++ b/src/webpage.cpp @@ -0,0 +1,13 @@ +#include "webpage.h" + +const char webpage_css[] PROGMEM = R"rawliteral( +#include "webpage.css" +)rawliteral"; + +const char webpage_js[] PROGMEM = R"rawliteral( +#include "webpage.js" +)rawliteral"; + +const char webpage_html[] PROGMEM = R"rawliteral( +#include "webpage.html" +)rawliteral"; diff --git a/src/webpage.css b/src/webpage.css new file mode 100644 index 0000000..fcd13ba --- /dev/null +++ b/src/webpage.css @@ -0,0 +1,120 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + display: flex; + height: 100vh; +} + +body { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + margin: 0; +} + +.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; +} + +h1 { + color: #333; + margin-bottom: 30px; + font-size: 28px; +} + +.status-display { + background: #f8f9fa; + padding: 20px; + border-radius: 8px; + margin-bottom: 30px; + border-left: 4px solid #667eea; +} + +.status-text { + font-size: 18px; + color: #555; + margin-bottom: 10px; +} + +.status-value { + font-size: 24px; + font-weight: bold; + color: #667eea; +} + +.progress-bar { + width: 100%; + height: 8px; + background: #e0e0e0; + border-radius: 4px; + overflow: hidden; + margin-top: 10px; + display: none; +} + +.progress-bar.active { + display: block; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); + width: 0%; + transition: width 0.2s ease; +} + +.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%; +} + +.button.open { + background: linear-gradient(135deg, #1abc9c 0%, #16a085 100%); +} + +.button.open:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 10px 20px rgba(26, 188, 156, 0.3); +} + +.button.close { + background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); +} + +.button.close:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 10px 20px rgba(231, 76, 60, 0.3); +} + +.button:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +.info-text { + font-size: 12px; + color: #999; + margin-top: 15px; +} \ No newline at end of file diff --git a/src/webpage.h b/src/webpage.h new file mode 100644 index 0000000..e55443a --- /dev/null +++ b/src/webpage.h @@ -0,0 +1,10 @@ +#ifndef WEBPAGE_H +#define WEBPAGE_H + +#include + +extern const char webpage_html[] PROGMEM; +extern const char webpage_css[] PROGMEM; +extern const char webpage_js[] PROGMEM; + +#endif // WEBPAGE_H diff --git a/src/webpage.html b/src/webpage.html new file mode 100644 index 0000000..2e00f16 --- /dev/null +++ b/src/webpage.html @@ -0,0 +1,24 @@ + + + + + Linear Actuator Control + + + +
+

Linear Actuator

+
+
Current Status
+
CLOSED
+
+
+
+
+ +
Operation time: 0s / 60s
+
+ + + + diff --git a/src/webpage.js b/src/webpage.js new file mode 100644 index 0000000..c1dcfe9 --- /dev/null +++ b/src/webpage.js @@ -0,0 +1,65 @@ +let isOperating = false; +let statusCheckInterval; + +function updateStatus() +{ + fetch('/status') + .then(response => response.json()) + .then(data => + { + const statusEl = document.getElementById('status'); + const btnEl = document.getElementById('actionButton'); + const progBar = document.getElementById('progressBar'); + const progFill = document.getElementById('progressFill'); + const opTimeEl = document.getElementById('opTime'); + + if (data.state === 'OPENING' || data.state === 'CLOSING') { + isOperating = true; + btnEl.disabled = true; + progBar.classList.add('active'); + statusEl.textContent = data.state; + const progress = (data.elapsedTime / data.totalTime) * 100; + progFill.style.width = progress + '%'; + opTimeEl.textContent = Math.round(data.elapsedTime / 1000); + } else { + isOperating = false; + btnEl.disabled = false; + progBar.classList.remove('active'); + progFill.style.width = '0%'; + opTimeEl.textContent = '0'; + + if (data.valveState === 1) { + statusEl.textContent = 'OPEN'; + btnEl.textContent = 'CLOSE'; + btnEl.className = 'button close'; + } else { + statusEl.textContent = 'CLOSED'; + btnEl.textContent = 'OPEN'; + btnEl.className = 'button open'; + } + } + }) + .catch(err => console.error('Status update failed:', err)); +} + +function handleAction() +{ + if (isOperating) return; + const btnEl = document.getElementById('actionButton'); + const endpoint = btnEl.textContent === 'OPEN' ? '/open' : '/close'; + + fetch(endpoint) + .then(() => + { + isOperating = true; + btnEl.disabled = true; + updateStatus(); + }) + .catch(err => console.error('Action failed:', err)); +} + +updateStatus(); +statusCheckInterval = setInterval(() => +{ + updateStatus(); +}, 250);