commit
6f56a5fe6f
16 changed files with 682 additions and 0 deletions
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
.pio |
||||
.vscode |
||||
.git |
||||
data/conf.json |
||||
@ -0,0 +1,127 @@
@@ -0,0 +1,127 @@
|
||||
const btnPreheat = document.getElementById('validateChauffeManuelle'); |
||||
const btnProfil = document.getElementById('validateChauffeAuto'); |
||||
|
||||
// WebSocket pour affichage température en temps réel
|
||||
function connectWS() { |
||||
const ws = new WebSocket(`ws://${window.location.hostname}/ws`); |
||||
ws.onopen = () => console.log('WebSocket connecté'); |
||||
ws.onclose = () => setTimeout(connectWS, 2000); |
||||
ws.onmessage = e => { |
||||
const tempDiv = document.getElementById('temp'); |
||||
const powerBar = document.getElementById('power-bar'); |
||||
const powerBarValue = document.getElementById('power-bar-value'); |
||||
try { |
||||
const data = JSON.parse(e.data); |
||||
if (tempDiv) tempDiv.textContent = `${data.temp} °C /${data.setpoint}°C`; |
||||
if (powerBar && powerBarValue && typeof data.output !== 'undefined') { |
||||
let pct = Math.max(0, Math.min(100, Math.round(data.output))); |
||||
powerBar.value = pct; |
||||
powerBarValue.textContent = pct + '%'; |
||||
} |
||||
} catch { |
||||
if (tempDiv) tempDiv.textContent = e.data; |
||||
} |
||||
}; |
||||
} |
||||
|
||||
connectWS(); |
||||
|
||||
// Fonction utilitaire pour modifier l'aspect d'un bouton
|
||||
function updateButton(btn, isActive) { |
||||
if (isActive) { |
||||
btn.textContent = 'STOP'; |
||||
btn.className = 'stop'; |
||||
} else { |
||||
btn.textContent = 'VALIDER'; |
||||
btn.className = 'start'; |
||||
} |
||||
} |
||||
|
||||
// Initialisation : aucun mode actif
|
||||
updateButton(btnPreheat, false); |
||||
updateButton(btnProfil, false); |
||||
|
||||
btnPreheat.onclick = function() { |
||||
if (btnPreheat.className === 'stop') { |
||||
// Désactive tout
|
||||
updateButton(btnPreheat, false); |
||||
updateButton(btnProfil, false); |
||||
fetch('/action/stop', { method: 'POST' }); |
||||
return; |
||||
} |
||||
// Active le mode manuel
|
||||
const temp = document.getElementById('preheat-gauge').value; |
||||
const params = new URLSearchParams(); |
||||
params.append('temp', temp); |
||||
fetch('/action/preheat', { |
||||
method: 'POST', |
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'}, |
||||
body: params.toString() |
||||
}).then(r => { |
||||
if (r.ok){ |
||||
updateButton(btnPreheat, true); |
||||
updateButton(btnProfil, false); |
||||
console.log('Préchauffage lancé à ' + temp + '°C'); |
||||
} |
||||
else console.log('Erreur serveur'); |
||||
}); |
||||
}; |
||||
|
||||
// Chauffe profil : bouton VALIDER
|
||||
btnProfil.onclick = function() { |
||||
if (btnProfil.className === 'stop') { |
||||
// Désactive tout
|
||||
updateButton(btnPreheat, false); |
||||
updateButton(btnProfil, false); |
||||
fetch('/action/stop', { method: 'POST' }); |
||||
return; |
||||
} |
||||
// Active le mode automatique
|
||||
const preheatTemp = document.getElementById('preheat-temp').value; |
||||
const preheatTime = document.getElementById('preheat-time').value; |
||||
const soakTemp = document.getElementById('soak-temp').value; |
||||
const soakTime = document.getElementById('soak-time').value; |
||||
const reflowTemp = document.getElementById('reflow-temp').value; |
||||
const reflowTime = document.getElementById('reflow-time').value; |
||||
const params = new URLSearchParams(); |
||||
params.append('preheatTemp', preheatTemp); |
||||
params.append('preheatTime', preheatTime); |
||||
params.append('soakTemp', soakTemp); |
||||
params.append('soakTime', soakTime); |
||||
params.append('reflowTemp', reflowTemp); |
||||
params.append('reflowTime', reflowTime); |
||||
fetch('/action/auto', { |
||||
method: 'POST', |
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'}, |
||||
body: params.toString() |
||||
}).then(r => { |
||||
if (r.ok){ |
||||
updateButton(btnPreheat, false); |
||||
updateButton(btnProfil, true); |
||||
console.log('Profil lancé'); |
||||
} |
||||
else console.log('Erreur serveur'); |
||||
}); |
||||
}; |
||||
|
||||
// Jauge préchauffage : affichage dynamique
|
||||
document.addEventListener('DOMContentLoaded', () => { |
||||
const gauge = document.getElementById('preheat-gauge'); |
||||
const gaugeValue = document.getElementById('preheat-gauge-value'); |
||||
const btnPreheat = document.getElementById('validateChauffeManuelle'); |
||||
if (gauge && gaugeValue) { |
||||
gauge.oninput = function() { |
||||
gaugeValue.textContent = this.value + '°C'; |
||||
// Si le mode manuel est actif, on met à jour le setpoint côté serveur
|
||||
if (btnPreheat && btnPreheat.className === 'stop') { |
||||
const params = new URLSearchParams(); |
||||
params.append('temp', this.value); |
||||
fetch('/action/preheat', { |
||||
method: 'POST', |
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'}, |
||||
body: params.toString() |
||||
}); |
||||
} |
||||
}; |
||||
} |
||||
}); |
||||
@ -0,0 +1,37 @@
@@ -0,0 +1,37 @@
|
||||
<!DOCTYPE html> |
||||
<html lang="fr"> |
||||
<head> |
||||
<meta charset="UTF-8"> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
<title>Reflow Oven CONFIG</title> |
||||
<link rel="stylesheet" href="style.css"> |
||||
</head> |
||||
<body> |
||||
<div class="container"> |
||||
<h4>Configuration :</h4> |
||||
<div class="phase" id="chauffe-auto"> |
||||
<div class="row"> |
||||
<label style="width: 65px; display: inline-block;">SSID : </label> |
||||
<input type="text" id="ssid" > |
||||
</div> |
||||
<div class="row"> |
||||
<label style="width: 65px; display: inline-block;">PWD : </label> |
||||
<input type="text" id="pwd" > |
||||
</div> |
||||
<div class="row"> |
||||
<label style="width: 65px; display: inline-block;">Kp : </label> |
||||
<input type="number" id="kp" > |
||||
</div> |
||||
<div class="row"> |
||||
<label style="width: 65px; display: inline-block;">Ki : </label> |
||||
<input type="number" id="ki" > |
||||
</div> |
||||
<div class="row"> |
||||
<label style="width: 65px; display: inline-block;">Kd : </label> |
||||
<input type="number" id="kd" > |
||||
</div> |
||||
<button id="validate" class="start">VALIDER</button> |
||||
|
||||
<script src="app.js"></script> |
||||
</body> |
||||
</html> |
||||
@ -0,0 +1,47 @@
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html> |
||||
<html lang="fr"> |
||||
<head> |
||||
<meta charset="UTF-8"> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
<title>Reflow Oven</title> |
||||
<link rel="stylesheet" href="style.css"> |
||||
</head> |
||||
<body> |
||||
<div class="container"> |
||||
<div class="phase" id="chauffe-manuelle"> |
||||
<div class="temp" id="temp">--.- °C/0°C</div> |
||||
<div class="data" id="power-bar-container"> |
||||
<label for="power-bar">Puissance :</label> |
||||
<progress id="power-bar" value="0" max="100" style="width: 180px;"></progress> |
||||
<span id="power-bar-value">0%</span> |
||||
</div> |
||||
</div> |
||||
<div class="phase" id="chauffe-manuelle"> |
||||
<h4>Chauffe manuelle :</h4> |
||||
<div class="row"> |
||||
<input type="range" id="preheat-gauge" min="20" max="250" value="20"> |
||||
<span id="preheat-gauge-value">20°C</span> |
||||
</div> |
||||
<button id="validateChauffeManuelle" class="start">VALIDER</button> |
||||
</div> |
||||
<div class="phase" id="chauffe-auto"> |
||||
<h4>Chauffe automatique : <span id="auto-time"></span></h4> |
||||
<div class="row active"> |
||||
<label style="width: 65px; display: inline-block;">Preheat : </label> |
||||
<input type="number" id="preheat-temp" value="150" min="0" max="300">°C |
||||
<input type="number" id="preheat-time" value="180" min="1" max="600">s |
||||
</div> |
||||
<div class="row"> |
||||
<label style="width: 65px; display: inline-block;">Soak :</label> |
||||
<input type="number" id="soak-temp" value="180" min="0" max="300">°C |
||||
<input type="number" id="soak-time" value="120" min="1" max="600">s |
||||
</div> |
||||
<div class="row"> |
||||
<label style="width: 65px; display: inline-block;">Reflow : </label> |
||||
<input type="number" id="reflow-temp" value="230" min="0" max="300">°C |
||||
<input type="number" id="reflow-time" value="60" min="1" max="600">s |
||||
</div> |
||||
<button id="validateChauffeAuto" class="start">VALIDER</button> |
||||
<script src="app.js"></script> |
||||
</body> |
||||
</html> |
||||
@ -0,0 +1,71 @@
@@ -0,0 +1,71 @@
|
||||
body { |
||||
font-family: Arial, sans-serif; |
||||
background: #222222; |
||||
color: #eeeeee; |
||||
margin: 0; |
||||
padding: 0; |
||||
} |
||||
.container { |
||||
max-width: 400px; |
||||
margin: 2em auto; |
||||
background: #333333; |
||||
border-radius: 10px; |
||||
box-shadow: 0 0 10px #111111; |
||||
padding: 2em; |
||||
} |
||||
h1 { |
||||
text-align: center; |
||||
} |
||||
h4 { |
||||
margin: 0 0 0.5em 0; |
||||
} |
||||
.temp { |
||||
font-size: 2em; |
||||
text-align: center; |
||||
margin: 0.5em 0; |
||||
} |
||||
.data { |
||||
font-size: 1em; |
||||
text-align: center; |
||||
margin: 0.5em 0; |
||||
} |
||||
.phase { |
||||
padding: 0.8em; |
||||
border-radius: 5px; |
||||
background: #444444; |
||||
margin-bottom: 0.5em; |
||||
} |
||||
.actions { |
||||
text-align: center; |
||||
margin: 1.5em 0; |
||||
} |
||||
button.start { |
||||
font-size: 0.8em; |
||||
padding: 0.5em 0.5em; |
||||
border: none; |
||||
border-radius: 5px; |
||||
background: #4caf50; |
||||
color: #ffffff; |
||||
cursor: pointer; |
||||
} |
||||
button.stop { |
||||
font-size: 0.8em; |
||||
padding: 0.5em 0.5em; |
||||
border: none; |
||||
border-radius: 5px; |
||||
background: #f44336; |
||||
color: #ffffff; |
||||
cursor: pointer; |
||||
} |
||||
.row { |
||||
display: flex; |
||||
align-items: center; |
||||
gap: 1em; |
||||
padding: 0.5em 0.5em; |
||||
} |
||||
.row.active { |
||||
background: #1976d2; |
||||
color: #fff; |
||||
border-radius: 4px; |
||||
box-shadow: 0 0 6px #1976d2; |
||||
} |
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,37 @@
@@ -0,0 +1,37 @@
|
||||
|
||||
This directory is intended for project header files. |
||||
|
||||
A header file is a file containing C declarations and macro definitions |
||||
to be shared between several project source files. You request the use of a |
||||
header file in your project source file (C, C++, etc) located in `src` folder |
||||
by including it, with the C preprocessing directive `#include'. |
||||
|
||||
```src/main.c |
||||
|
||||
#include "header.h" |
||||
|
||||
int main (void) |
||||
{ |
||||
... |
||||
} |
||||
``` |
||||
|
||||
Including a header file produces the same results as copying the header file |
||||
into each source file that needs it. Such copying would be time-consuming |
||||
and error-prone. With a header file, the related declarations appear |
||||
in only one place. If they need to be changed, they can be changed in one |
||||
place, and programs that include the header file will automatically use the |
||||
new version when next recompiled. The header file eliminates the labor of |
||||
finding and changing all the copies as well as the risk that a failure to |
||||
find one copy will result in inconsistencies within a program. |
||||
|
||||
In C, the convention is to give header files names that end with `.h'. |
||||
|
||||
Read more about using header files in official GCC documentation: |
||||
|
||||
* Include Syntax |
||||
* Include Operation |
||||
* Once-Only Headers |
||||
* Computed Includes |
||||
|
||||
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html |
||||
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
Subproject commit 524a4268fc01e6ea397e7fc5b5d820741e9b662f |
||||
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
Subproject commit 6a7d05d22769d7f48e486ee734e4ed0a4714ab02 |
||||
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
; PlatformIO Project Configuration File |
||||
; |
||||
; Build options: build flags, source filter |
||||
; Upload options: custom upload port, speed and extra flags |
||||
; Library options: dependencies, extra library storages |
||||
; Advanced options: extra scripting |
||||
; |
||||
; Please visit documentation for the other options and examples |
||||
; https://docs.platformio.org/page/projectconf.html |
||||
|
||||
[env:esp12e] |
||||
platform = espressif8266 |
||||
board = esp12e |
||||
framework = arduino |
||||
monitor_speed = 115200 |
||||
|
||||
lib_deps = |
||||
ESP32Async/ESPAsyncWebServer |
||||
ESP32Async/ESPAsyncTCP |
||||
LittleFS@^0.1.0 |
||||
|
||||
; Partitionnement LittleFS (2MB pour fichiers, 1MB SPIFFS par défaut) |
||||
board_build.filesystem = littlefs |
||||
board_build.ldscript = eagle.flash.4m2m.ld |
||||
@ -0,0 +1,194 @@
@@ -0,0 +1,194 @@
|
||||
#include <PID_v1.h> |
||||
#include <Arduino.h> |
||||
#include <ESP8266WiFi.h> |
||||
#include <LittleFS.h> |
||||
#include <ESPAsyncWebServer.h> |
||||
#include <max6675.h> |
||||
|
||||
// Wifi credentials
|
||||
const char* ssid = ""; |
||||
const char* password = ""; |
||||
AsyncWebServer server(80); |
||||
AsyncWebSocket ws("/ws"); |
||||
|
||||
// SSR relay pin
|
||||
const int SSR_PIN = 12; // D1 (GPIO5) par exemple
|
||||
|
||||
// PID variables
|
||||
double setpoint = 20, input = 0, output = 0; |
||||
const int PWM_PERIOD = 2000; |
||||
double Kp = 2, Ki = 5, Kd = 1; |
||||
PID myPID(&input, &output, &setpoint, Kp, Ki, Kd, DIRECT); |
||||
|
||||
// Variables globales pour le profil
|
||||
int preheatTemp = 0, preheatTime = 0; |
||||
int soakTemp = 0, soakTime = 0; |
||||
int reflowTemp = 0, reflowTime = 0; |
||||
bool autoProfileActive = false; |
||||
unsigned long profileStartTime = 0; |
||||
int profileStep = 0; |
||||
|
||||
// Broches MAX6675 (exemple : SCK=14, CS=12, SO=13)
|
||||
const int thermoSO = 13; |
||||
const int thermoCS = 4; |
||||
const int thermoSCK = 5; |
||||
MAX6675 thermocouple(thermoSCK, thermoCS, thermoSO); |
||||
float lastTemp = NAN; |
||||
|
||||
unsigned long lastSend = 0; |
||||
unsigned long pwmStart = 0; |
||||
|
||||
void setup() { |
||||
// Action arrêt (setpoint à 0°C)
|
||||
server.on("/action/stop", HTTP_POST, [](AsyncWebServerRequest *request){ |
||||
setpoint = 0; |
||||
autoProfileActive = false; |
||||
profileStep = 0; |
||||
Serial.println("Arrêt demandé : setpoint PID à 0°C"); |
||||
request->send(200, "text/plain", "OK"); |
||||
}); |
||||
|
||||
// Action chauffe manuelle
|
||||
server.on("/action/preheat", HTTP_POST, [](AsyncWebServerRequest *request){ |
||||
if (!request->hasArg("temp")) { |
||||
request->send(400, "text/plain", "Paramètre 'temp' manquant"); |
||||
return; |
||||
} |
||||
// Désactive le mode automatique si actif
|
||||
if (autoProfileActive) { |
||||
autoProfileActive = false; |
||||
profileStep = 0; |
||||
Serial.println("Mode automatique désactivé (chauffe manuelle)"); |
||||
} |
||||
setpoint = request->arg("temp").toDouble(); |
||||
Serial.printf("Chauffe manuelle demandée : %.1f°C (setpoint PID fixé)\n", setpoint); |
||||
request->send(200, "text/plain", "OK"); |
||||
}); |
||||
|
||||
// Action chauffe auto
|
||||
server.on("/action/auto", HTTP_POST, [](AsyncWebServerRequest *request){ |
||||
if (!request->hasArg("preheatTemp") || !request->hasArg("preheatTime") || |
||||
!request->hasArg("soakTemp") || !request->hasArg("soakTime") || |
||||
!request->hasArg("reflowTemp") || !request->hasArg("reflowTime")) { |
||||
request->send(400, "text/plain", "Paramètres manquants"); |
||||
return; |
||||
} |
||||
preheatTemp = request->arg("preheatTemp").toInt(); |
||||
preheatTime = request->arg("preheatTime").toInt(); |
||||
soakTemp = request->arg("soakTemp").toInt(); |
||||
soakTime = request->arg("soakTime").toInt(); |
||||
reflowTemp = request->arg("reflowTemp").toInt(); |
||||
reflowTime = request->arg("reflowTime").toInt(); |
||||
// Démarrage du profil automatique
|
||||
autoProfileActive = true; |
||||
profileStep = 0; |
||||
profileStartTime = millis(); |
||||
setpoint = preheatTemp; |
||||
Serial.printf("Profil %s lancé : Preheat %d°C/%ds, Soak %d°C/%ds, Reflow %d°C/%ds\n", |
||||
request->arg("profile").c_str(), preheatTemp, preheatTime, soakTemp, soakTime, reflowTemp, reflowTime); |
||||
request->send(200, "text/plain", "OK"); |
||||
}); |
||||
|
||||
pinMode(SSR_PIN, OUTPUT); |
||||
digitalWrite(SSR_PIN, LOW); |
||||
myPID.SetMode(AUTOMATIC); |
||||
myPID.SetOutputLimits(0, PWM_PERIOD); // output = ms ON sur 2s
|
||||
Serial.begin(115200); |
||||
delay(100); |
||||
|
||||
// Connexion à un réseau WiFi défini dans les paramètres
|
||||
WiFi.mode(WIFI_STA); |
||||
WiFi.begin(ssid, password); |
||||
Serial.print("Connexion au WiFi : "); |
||||
Serial.println(ssid); |
||||
int wifiTimeout = 0; |
||||
while (WiFi.status() != WL_CONNECTED && wifiTimeout < 20) { |
||||
delay(500); |
||||
Serial.print("."); |
||||
wifiTimeout++; |
||||
} |
||||
if (WiFi.status() == WL_CONNECTED) { |
||||
Serial.print("Connecté, IP : "); |
||||
Serial.println(WiFi.localIP()); |
||||
} else { |
||||
Serial.println("Échec connexion WiFi"); |
||||
} |
||||
|
||||
// Initialisation LittleFS
|
||||
if (!LittleFS.begin()) { |
||||
Serial.println("Erreur LittleFS"); |
||||
return; |
||||
} |
||||
Serial.println("LittleFS monté"); |
||||
|
||||
// Route pour servir les fichiers statiques (html, js, css)
|
||||
server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html"); |
||||
|
||||
// WebSocket
|
||||
server.addHandler(&ws); |
||||
|
||||
// Démarrer le serveur web
|
||||
server.begin(); |
||||
Serial.println("Serveur web démarré sur port 80"); |
||||
} |
||||
|
||||
void loop() { |
||||
|
||||
ws.cleanupClients(); |
||||
unsigned long now = millis(); |
||||
// Gestion du profil automatique
|
||||
if (autoProfileActive) { |
||||
unsigned long elapsed = (millis() - profileStartTime) / 1000; |
||||
if (profileStep == 0) { // Preheat
|
||||
setpoint = preheatTemp; |
||||
if (elapsed >= (unsigned long)preheatTime) { |
||||
profileStep = 1; |
||||
profileStartTime = millis(); |
||||
Serial.println("Début Soak"); |
||||
} |
||||
} else if (profileStep == 1) { // Soak
|
||||
setpoint = soakTemp; |
||||
if (elapsed >= (unsigned long)soakTime) { |
||||
profileStep = 2; |
||||
profileStartTime = millis(); |
||||
Serial.println("Début Reflow"); |
||||
} |
||||
} else if (profileStep == 2) { // Reflow
|
||||
setpoint = reflowTemp; |
||||
if (elapsed >= (unsigned long)reflowTime) { |
||||
profileStep = 3; |
||||
autoProfileActive = false; |
||||
Serial.println("Profil terminé"); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// PID
|
||||
myPID.Compute(); |
||||
// PWM logiciel SSR (2s)
|
||||
if (now - pwmStart >= PWM_PERIOD) { |
||||
pwmStart = now; |
||||
} |
||||
if ((now - pwmStart) < (unsigned long)output) { |
||||
digitalWrite(SSR_PIN, HIGH); |
||||
} else { |
||||
digitalWrite(SSR_PIN, LOW); |
||||
} |
||||
// WebSocket température (1Hz)
|
||||
if (now - lastSend > 1000) { |
||||
lastSend = now; |
||||
// Lecture température
|
||||
float t = thermocouple.readCelsius(); |
||||
if (!isnan(t)) { |
||||
input = t; |
||||
} |
||||
int pct = 0; |
||||
if (output > 0) { |
||||
pct = (int)round((output * 100) / PWM_PERIOD); |
||||
if (pct > 100) pct = 100; |
||||
} |
||||
String json = String("{\"temp\":") + String(input, 1) + ",\"setpoint\":" + String(setpoint, 1) + ",\"output\":" + String(pct) + "}"; |
||||
Serial.println("Température: " + String(input, 1) + " °C | Setpoint: " + String(setpoint, 1) + " | Output PID: " + String(output) + " | Puissance: " + String(pct) + "%"); |
||||
ws.textAll(json); |
||||
} |
||||
} |
||||
@ -0,0 +1,128 @@
@@ -0,0 +1,128 @@
|
||||
#include <PID_v1.h> |
||||
#include <Arduino.h> |
||||
#include <ESP8266WiFi.h> |
||||
#include <LittleFS.h> |
||||
#include <ESPAsyncWebServer.h> |
||||
#include <max6675.h> |
||||
|
||||
// Wifi credentials |
||||
const char* ssid = "ReflowOven"; |
||||
const char* password = "12345678"; |
||||
AsyncWebServer server(80); |
||||
AsyncWebSocket ws("/ws"); |
||||
|
||||
// SSR relay pin |
||||
const int SSR_PIN = 5; // D1 (GPIO5) par exemple |
||||
|
||||
// PID variables |
||||
double setpoint = 150, input = 0, output = 0; |
||||
const int PWM_PERIOD = 2000; |
||||
double Kp = 2, Ki = 5, Kd = 1; |
||||
PID myPID(&input, &output, &setpoint, Kp, Ki, Kd, DIRECT); |
||||
|
||||
// Broches MAX6675 (exemple : SCK=14, CS=12, SO=13) |
||||
const int thermoSO = 13; |
||||
const int thermoCS = 12; |
||||
const int thermoSCK = 14; |
||||
MAX6675 thermocouple(thermoSCK, thermoCS, thermoSO); |
||||
float lastTemp = NAN; |
||||
|
||||
unsigned long lastSend = 0; |
||||
unsigned long pwmStart = 0; |
||||
|
||||
void setup() { |
||||
|
||||
// Action chauffe manuelle |
||||
server.on("/action/preheat", HTTP_POST, [](AsyncWebServerRequest *request){ |
||||
if (!request->hasArg("temp")) { |
||||
request->send(400, "text/plain", "Paramètre 'temp' manquant"); |
||||
return; |
||||
} |
||||
setpoint = request->arg("temp").toDouble(); |
||||
Serial.printf("Chauffe manuelle demandée : %.1f°C (setpoint PID fixé)\n", setpoint); |
||||
request->send(200, "text/plain", "OK"); |
||||
}); |
||||
|
||||
// Action chauffe profil |
||||
server.on("/action/profile", HTTP_POST, [](AsyncWebServerRequest *request){ |
||||
if (!request->hasArg("profile") || !request->hasArg("preheatTemp") || !request->hasArg("preheatTime") || |
||||
!request->hasArg("soakTemp") || !request->hasArg("soakTime") || |
||||
!request->hasArg("reflowTemp") || !request->hasArg("reflowTime")) { |
||||
request->send(400, "text/plain", "Paramètres manquants"); |
||||
return; |
||||
} |
||||
String profile = request->arg("profile"); |
||||
int preheatTemp = request->arg("preheatTemp").toInt(); |
||||
int preheatTime = request->arg("preheatTime").toInt(); |
||||
int soakTemp = request->arg("soakTemp").toInt(); |
||||
int soakTime = request->arg("soakTime").toInt(); |
||||
int reflowTemp = request->arg("reflowTemp").toInt(); |
||||
int reflowTime = request->arg("reflowTime").toInt(); |
||||
Serial.printf("Profil %s lancé : Preheat %d°C/%ds, Soak %d°C/%ds, Reflow %d°C/%ds\n", |
||||
profile.c_str(), preheatTemp, preheatTime, soakTemp, soakTime, reflowTemp, reflowTime); |
||||
request->send(200, "text/plain", "OK"); |
||||
}); |
||||
|
||||
pinMode(SSR_PIN, OUTPUT); |
||||
digitalWrite(SSR_PIN, LOW); |
||||
myPID.SetMode(AUTOMATIC); |
||||
myPID.SetOutputLimits(0, PWM_PERIOD); // output = ms ON sur 2s |
||||
Serial.begin(115200); |
||||
delay(100); |
||||
|
||||
// Configure ESP8266 as Access Point |
||||
WiFi.mode(WIFI_AP); |
||||
WiFi.softAP(ssid, password); |
||||
Serial.print("AP IP address: "); |
||||
Serial.println(WiFi.softAPIP()); |
||||
|
||||
// Initialisation LittleFS |
||||
if (!LittleFS.begin()) { |
||||
Serial.println("Erreur LittleFS"); |
||||
return; |
||||
} |
||||
Serial.println("LittleFS monté"); |
||||
|
||||
// Route pour servir les fichiers statiques (html, js, css) |
||||
server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html"); |
||||
|
||||
// WebSocket |
||||
server.addHandler(&ws); |
||||
|
||||
// Démarrer le serveur web |
||||
server.begin(); |
||||
Serial.println("Serveur web démarré sur port 80"); |
||||
} |
||||
|
||||
void loop() { |
||||
|
||||
ws.cleanupClients(); |
||||
unsigned long now = millis(); |
||||
|
||||
|
||||
|
||||
// PID |
||||
myPID.Compute(); |
||||
// PWM logiciel SSR (2s) |
||||
if (now - pwmStart >= PWM_PERIOD) { |
||||
pwmStart = now; |
||||
} |
||||
if ((now - pwmStart) < (unsigned long)output) { |
||||
digitalWrite(SSR_PIN, HIGH); |
||||
} else { |
||||
digitalWrite(SSR_PIN, LOW); |
||||
} |
||||
// WebSocket température (1Hz) |
||||
if (now - lastSend > 1000) { |
||||
lastSend = now; |
||||
// Lecture température |
||||
float t = thermocouple.readCelsius(); |
||||
|
||||
if (!isnan(t)) { |
||||
input = t; |
||||
} |
||||
String msg = String(input, 1); |
||||
Serial.println("Température: " + msg + " °C"+" | Output PID: "+ String(output)); |
||||
ws.textAll(msg); |
||||
} |
||||
} |
||||
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
|
||||
This directory is intended for PlatformIO Test Runner and project tests. |
||||
|
||||
Unit Testing is a software testing method by which individual units of |
||||
source code, sets of one or more MCU program modules together with associated |
||||
control data, usage procedures, and operating procedures, are tested to |
||||
determine whether they are fit for use. Unit testing finds problems early |
||||
in the development cycle. |
||||
|
||||
More information about PlatformIO Unit Testing: |
||||
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html |
||||
Loading…
Reference in new issue