Compare commits

...

8 Commits

Author SHA1 Message Date
ptrinchini 9ece9d51ff Invert motor pins 2 weeks ago
ptrinchini ccd7b725d9 Refactor 3 weeks ago
ptrinchini 1f3292a329 Fix OTA password 3 weeks ago
ptrinchini 68c1406148 Non blocking 3 weeks ago
ptrinchini f6e61ffb7c OTA Update 3 weeks ago
ptrinchini 08e71b96e2 Changed input and output pin 2 months ago
ptrinchini 54bee65185 Internal pullup 2 months ago
ptrinchini b779613fc0 Fix libraries 2 months ago
  1. 17
      .vscode/extensions.json
  2. 21
      platformio.ini
  3. 8
      src/credentials.h
  4. 208
      src/main.cpp
  5. 13
      src/webpage.cpp
  6. 120
      src/webpage.css
  7. 10
      src/webpage.h
  8. 24
      src/webpage.html
  9. 65
      src/webpage.js

17
.vscode/extensions.json

@ -1,7 +1,10 @@
{ {
// See http://go.microsoft.com/fwlink/?LinkId=827846 // See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format // for the documentation about the extensions.json format
"recommendations": [ "recommendations": [
"platformio.platformio-ide" "platformio.platformio-ide"
] ],
} "unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
]
}

21
platformio.ini

@ -8,10 +8,23 @@
; Please visit documentation for the other options and examples ; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html ; https://docs.platformio.org/page/projectconf.html
[env:nodemcuv2] [platformio]
platform = espressif8266 default_envs = com, ota
board = nodemcuv2
[env:com]
platform = espressif32
board = esp32dev
framework = arduino framework = arduino
upload_protocol = esptool upload_protocol = esptool
lib_deps = lib_deps =
knolleary/PubSubClient@^2.8 knolleary/PubSubClient@^2.8
[env:ota]
platform = espressif32
board = esp32dev
framework = arduino
upload_protocol = espota
upload_port = 192.168.1.235
upload_flags = --auth=P@ssword
lib_deps =
knolleary/PubSubClient@^2.8

8
src/credentials.h

@ -0,0 +1,8 @@
#define WLAN_SSID "TP-LINK-OUT"
#define WLAN_PASS "6JM54UG4EX"
#define MQTT_USER "admin"
#define MQTT_PASS "22pa0799"
#define OTA_USER "admin"
#define OTA_PASS "P@ssword"

208
src/main.cpp

@ -1,43 +1,64 @@
#include <Arduino.h> #include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h> #include <WiFi.h>
#include <ArduinoOTA.h>
#include <WebServer.h>
#include <PubSubClient.h> #include <PubSubClient.h>
#include "credentials.h"
#include "webpage.h"
#define WLAN_SSID "TP-LINK-RPT0" #define MQTT_CLID "ValveControl"
#define WLAN_PASS "6JM54UG4EY" #define MQTT_HOST "192.168.1.146"
#define MQTT_PORT 1883
#define MQTT_CLID "ValveControl" // Actuator state enum
#define MQTT_HOST "192.168.1.133" enum ActuatorState {
#define MQTT_PORT 1883 IDLE,
#define MQTT_USER "admin" OPENING,
#define MQTT_PASS "22pa0799" CLOSING
};
void openValve(); void openValve();
void closeValve(); void closeValve();
void stopValve(); void stopValve();
void updateActuator();
void OnConnect(); void OnConnect();
void OnOpen(); void OnOpen();
void OnClose(); void OnClose();
void OnStatus();
void OnRestart(); void OnRestart();
void OnStyle();
void OnScript();
void OnNotFound(); void OnNotFound();
String SendHTML(uint8_t ValveOpen); String GetStatusJSON();
// Actuator control variables
ActuatorState currentState = IDLE;
ActuatorState desiredState = IDLE;
unsigned long operationStartTime = 0;
const unsigned long OPERATION_DURATION = 45000; // 45 seconds
bool ValveOpen = false; bool ValveState = LOW;
int ButtonOpen = 14; int ButtonOpen = 16;
int ButtonClose = 12; int ButtonClose = 19;
int MotorPin1 = 0; int MotorPin1 = 25; // ritrae il braccio -> valvola aperta
int MotorPin2 = 2; int MotorPin2 = 26; // estende il braccio -> valvola chiusa
int duration = 10000;
ESP8266WebServer server(80); WebServer server(80);
WiFiClient espClient; WiFiClient espClient;
PubSubClient client(espClient); PubSubClient client(espClient);
void setup_ota() {
ArduinoOTA.setPassword(OTA_PASS);
ArduinoOTA.begin();
}
void setup_wifi() { void setup_wifi() {
Serial.println(); Serial.println();
Serial.print("Connecting to "); Serial.print("Connecting to ");
@ -49,8 +70,10 @@ void setup_wifi() {
} }
Serial.println(); Serial.println();
Serial.println("WiFi connected"); Serial.println("WiFi connected");
Serial.println("IP address: "); Serial.print("IP address: ");
Serial.println(WiFi.localIP()); Serial.println(WiFi.localIP());
Serial.print("Starting OTA...");
} }
void onMessage(char* topic, byte* payload, unsigned int length) { void onMessage(char* topic, byte* payload, unsigned int length) {
@ -94,8 +117,11 @@ 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("/restart", OnRestart); server.on("/restart", OnRestart);
server.onNotFound(OnNotFound); server.onNotFound(OnNotFound);
server.begin(); server.begin();
@ -104,67 +130,70 @@ void setup_http() {
void setup() { void setup() {
Serial.begin(115200); Serial.begin(9600);
while(!Serial) { while(!Serial) {
delay(100); delay(100);
} }
pinMode(ButtonOpen, INPUT); pinMode(ButtonOpen, INPUT_PULLUP);
pinMode(ButtonClose, INPUT); pinMode(ButtonClose, INPUT_PULLUP);
pinMode(MotorPin1, OUTPUT); pinMode(MotorPin1, OUTPUT);
pinMode(MotorPin2, OUTPUT); pinMode(MotorPin2, OUTPUT);
setup_wifi(); setup_wifi();
setup_ota();
setup_mqtt(); setup_mqtt();
setup_http(); setup_http();
} }
void loop() { void loop() {
ArduinoOTA.handle();
if (!client.connected()) { if (!client.connected()) {
reconnect(); reconnect();
} }
client.loop(); client.loop();
server.handleClient(); // Handle physical buttons
int buttonOpenState = digitalRead(ButtonOpen); int buttonOpenState = digitalRead(ButtonOpen);
int buttonCloseState = digitalRead(ButtonClose); int buttonCloseState = digitalRead(ButtonClose);
if(buttonOpenState == LOW) { if(buttonOpenState == LOW) {
buttonCloseState = !buttonOpenState; openValve();
digitalWrite(MotorPin1, LOW);
digitalWrite(MotorPin2, HIGH);
ValveOpen = true;
} }
if(buttonCloseState == LOW) { if(buttonCloseState == LOW) {
buttonOpenState = !buttonCloseState; closeValve();
digitalWrite(MotorPin1, HIGH);
digitalWrite(MotorPin2, LOW);
ValveOpen = false;
}
if(buttonOpenState == HIGH && buttonCloseState == HIGH) {
stopValve();
} }
// Update actuator state machine (non-blocking)
updateActuator();
server.handleClient();
} }
void OnConnect() { void OnConnect() {
server.send(200, "text/html", SendHTML(ValveOpen)); server.send_P(200, "text/html", webpage_html);
} }
void OnOpen() { void OnOpen() {
openValve(); if(currentState == IDLE) {
server.send(200, "text/html", SendHTML(ValveOpen)); openValve();
}
server.send(200, "application/json", GetStatusJSON());
} }
void OnClose() { void OnClose() {
closeValve(); if(currentState == IDLE) {
server.send(200, "text/html", SendHTML(ValveOpen)); closeValve();
}
server.send(200, "application/json", GetStatusJSON());
}
void OnStatus() {
server.send(200, "application/json", GetStatusJSON());
} }
void OnRestart() { void OnRestart() {
@ -173,26 +202,36 @@ 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");
} }
void openValve() { void openValve() {
Serial.println("Opening valve..."); if(currentState == IDLE) {
digitalWrite(MotorPin1, LOW); Serial.println("Opening valve...");
digitalWrite(MotorPin2, HIGH); digitalWrite(MotorPin1, LOW);
delay(duration); digitalWrite(MotorPin2, HIGH);
stopValve(); currentState = OPENING;
ValveOpen = HIGH; operationStartTime = millis();
}
} }
void closeValve() { void closeValve() {
Serial.println("Closing valve..."); if(currentState == IDLE) {
digitalWrite(MotorPin1, HIGH); Serial.println("Closing valve...");
digitalWrite(MotorPin2, LOW); digitalWrite(MotorPin1, HIGH);
delay(duration); digitalWrite(MotorPin2, LOW);
stopValve(); currentState = CLOSING;
ValveOpen = false; operationStartTime = millis();
}
} }
void stopValve() { void stopValve() {
@ -200,28 +239,45 @@ void stopValve() {
digitalWrite(MotorPin2, LOW); digitalWrite(MotorPin2, LOW);
} }
String SendHTML(uint8_t ValveOpen) { void updateActuator() {
String ptr = "<!DOCTYPE html> <html>\n"; // Check if operation is complete
ptr +="<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\">\n"; if(currentState != IDLE) {
ptr +="<title>Linear Actuator</title>\n"; unsigned long elapsedTime = millis() - operationStartTime;
ptr +="<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}\n";
ptr +="body{margin-top: 50px;} h1 {color: #444444;margin: 50px auto 30px;} h3 {color: #444444;margin-bottom: 50px;}\n"; if(elapsedTime >= OPERATION_DURATION) {
ptr +=".button {display: block;width: 80px;background-color: #1abc9c;border: none;color: white;padding: 13px 30px;text-decoration: none;font-size: 25px;margin: 0px auto 35px;cursor: pointer;border-radius: 4px;}\n"; // Operation complete
ptr +=".button-open {background-color: #1abc9c;}\n"; stopValve();
ptr +=".button-open:active {background-color: #16a085;}\n";
ptr +=".button-close {background-color: #34495e;}\n"; if(currentState == OPENING) {
ptr +=".button-close:active {background-color: #2c3e50;}\n"; ValveState = HIGH;
ptr +="p {font-size: 14px;color: #888;margin-bottom: 10px;}\n"; Serial.println("Valve fully open");
ptr +="</style>\n"; } else if(currentState == CLOSING) {
ptr +="</head>\n"; ValveState = LOW;
ptr +="<body>\n"; Serial.println("Valve fully closed");
ptr +="<h1>Linear Actuator</h1>\n"; }
if(ValveOpen) {
ptr +="<p>Valve is OPEN</p><a class=\"button button-close\" href=\"/close\">CLOSE</a>\n"; currentState = IDLE;
}
}
}
String GetStatusJSON() {
String json = "{"
"\"state\":\"";
if(currentState == OPENING) {
json += "OPENING";
} else if(currentState == CLOSING) {
json += "CLOSING";
} else { } else {
ptr +="<p>Valve is CLOSED</p><a class=\"button button-open\" href=\"/open\">OPEN</a>\n"; json += "IDLE";
} }
ptr +="</body>\n";
ptr +="</html>\n"; json += "\",";
return ptr; json += "\"valveState\":" + String(ValveState ? 1 : 0) + ",";
json += "\"elapsedTime\":" + String(millis() - operationStartTime) + ",";
json += "\"totalTime\":" + String(OPERATION_DURATION);
json += "}";
return json;
} }

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