Browse Source

Initial commit

master
scayac 2 weeks ago
commit
6f56a5fe6f
  1. 4
      .gitignore
  2. 127
      data/app.js
  3. 37
      data/conf.html
  4. 47
      data/index.html
  5. 71
      data/style.css
  6. BIN
      enclosure/ReflowOven-Capot.stl
  7. BIN
      enclosure/ReflowOven-Corps.stl
  8. BIN
      enclosure/ReflowOven.20251207-141003.FCBak
  9. BIN
      enclosure/ReflowOven.FCStd
  10. 37
      include/README
  11. 1
      lib/Arduino-PID-Library
  12. 1
      lib/MAX6675-library
  13. 24
      platformio.ini
  14. 194
      src/main.cpp
  15. 128
      src/main.cpp.saved
  16. 11
      test/README

4
.gitignore vendored

@ -0,0 +1,4 @@
.pio
.vscode
.git
data/conf.json

127
data/app.js

@ -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()
});
}
};
}
});

37
data/conf.html

@ -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>

47
data/index.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>

71
data/style.css

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

BIN
enclosure/ReflowOven-Capot.stl

Binary file not shown.

BIN
enclosure/ReflowOven-Corps.stl

Binary file not shown.

BIN
enclosure/ReflowOven.20251207-141003.FCBak

Binary file not shown.

BIN
enclosure/ReflowOven.FCStd

Binary file not shown.

37
include/README

@ -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

1
lib/Arduino-PID-Library

@ -0,0 +1 @@
Subproject commit 524a4268fc01e6ea397e7fc5b5d820741e9b662f

1
lib/MAX6675-library

@ -0,0 +1 @@
Subproject commit 6a7d05d22769d7f48e486ee734e4ed0a4714ab02

24
platformio.ini

@ -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

194
src/main.cpp

@ -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);
}
}

128
src/main.cpp.saved

@ -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);
}
}

11
test/README

@ -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…
Cancel
Save