commit
6f56a5fe6f
16 changed files with 682 additions and 0 deletions
@ -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 @@ |
|||||||
|
<!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 @@ |
|||||||
|
<!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 @@ |
|||||||
|
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 @@ |
|||||||
|
|
||||||
|
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,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 @@ |
|||||||
|
#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 @@ |
|||||||
|
#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 @@ |
|||||||
|
|
||||||
|
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