Browse Source

Refactor

esp32ota3
ptrinchini 3 weeks ago
parent
commit
ccd7b725d9
  1. 2
      platformio.ini
  2. 138
      src/main.cpp
  3. 13
      src/webpage.cpp
  4. 120
      src/webpage.css
  5. 10
      src/webpage.h
  6. 24
      src/webpage.html
  7. 65
      src/webpage.js

2
platformio.ini

@ -25,6 +25,6 @@ board = esp32dev
framework = arduino framework = arduino
upload_protocol = espota upload_protocol = espota
upload_port = 192.168.1.235 upload_port = 192.168.1.235
upload_flags = --auth=P@ssw0rd upload_flags = --auth=P@ssword
lib_deps = lib_deps =
knolleary/PubSubClient@^2.8 knolleary/PubSubClient@^2.8

138
src/main.cpp

@ -5,6 +5,7 @@
#include <WebServer.h> #include <WebServer.h>
#include <PubSubClient.h> #include <PubSubClient.h>
#include "credentials.h" #include "credentials.h"
#include "webpage.h"
#define MQTT_CLID "ValveControl" #define MQTT_CLID "ValveControl"
#define MQTT_HOST "192.168.1.146" #define MQTT_HOST "192.168.1.146"
@ -27,16 +28,18 @@ void OnOpen();
void OnClose(); void OnClose();
void OnStatus(); void OnStatus();
void OnRestart(); void OnRestart();
void OnStyle();
void OnScript();
void OnNotFound(); void OnNotFound();
String SendHTML();
String GetStatusJSON(); String GetStatusJSON();
// Actuator control variables // Actuator control variables
ActuatorState currentState = IDLE; ActuatorState currentState = IDLE;
ActuatorState desiredState = IDLE; ActuatorState desiredState = IDLE;
unsigned long operationStartTime = 0; unsigned long operationStartTime = 0;
const unsigned long OPERATION_DURATION = 60000; // 60 seconds const unsigned long OPERATION_DURATION = 45000; // 45 seconds
bool ValveState = LOW; bool ValveState = LOW;
@ -114,6 +117,8 @@ void setup_mqtt() {
void setup_http() { void setup_http() {
Serial.println("Starting HTTP server..."); Serial.println("Starting HTTP server...");
server.on("/", OnConnect); server.on("/", OnConnect);
server.on("/style.css", OnStyle);
server.on("/app.js", OnScript);
server.on("/open", OnOpen); server.on("/open", OnOpen);
server.on("/close", OnClose); server.on("/close", OnClose);
server.on("/status", OnStatus); server.on("/status", OnStatus);
@ -125,7 +130,7 @@ void setup_http() {
void setup() { void setup() {
Serial.begin(115200); Serial.begin(9600);
while(!Serial) { while(!Serial) {
delay(100); delay(100);
} }
@ -150,8 +155,6 @@ void loop() {
} }
client.loop(); client.loop();
server.handleClient();
// Update actuator state machine (non-blocking) // Update actuator state machine (non-blocking)
updateActuator(); updateActuator();
@ -166,10 +169,13 @@ void loop() {
if(buttonCloseState == LOW) { if(buttonCloseState == LOW) {
closeValve(); closeValve();
} }
server.handleClient();
} }
void OnConnect() { void OnConnect() {
server.send(200, "text/html", SendHTML()); server.send_P(200, "text/html", webpage_html);
} }
void OnOpen() { void OnOpen() {
@ -196,6 +202,14 @@ void OnRestart() {
ESP.restart(); ESP.restart();
} }
void OnStyle() {
server.send_P(200, "text/css", webpage_css);
}
void OnScript() {
server.send_P(200, "application/javascript", webpage_js);
}
void OnNotFound(){ void OnNotFound(){
server.send(404, "text/plain", "Not found"); 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 GetStatusJSON() {
String json = "{" String json = "{"
"\"state\":\""; "\"state\":\"";

13
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";

120
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;
}

10
src/webpage.h

@ -0,0 +1,10 @@
#ifndef WEBPAGE_H
#define WEBPAGE_H
#include <Arduino.h>
extern const char webpage_html[] PROGMEM;
extern const char webpage_css[] PROGMEM;
extern const char webpage_js[] PROGMEM;
#endif // WEBPAGE_H

24
src/webpage.html

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Linear Actuator Control</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<div class="container">
<h1>Linear Actuator</h1>
<div class="status-display">
<div class="status-text">Current Status</div>
<div class="status-value" id="status">CLOSED</div>
<div class="progress-bar" id="progressBar">
<div class="progress-fill" id="progressFill"></div>
</div>
</div>
<button class="button open" id="actionButton" onclick="handleAction()">OPEN</button>
<div class="info-text">Operation time: <span id="opTime">0</span>s / 60s</div>
</div>
<script src="/app.js"></script>
</body>
</html>

65
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);
Loading…
Cancel
Save