diff --git a/README.md b/README.md new file mode 100644 index 0000000..f06450f --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# Reflow Oven ESP8266 + +Ce projet est un contrôleur de four de refusion basé sur ESP8266, avec interface web et contrôle PID. + +## Fonctionnalités +- Contrôle manuel et automatique du four +- Interface web dynamique (température, consigne, puissance SSR) +- Configuration des profils de chauffe (Preheat, Soak, Reflow) +- Visualisation en temps réel via WebSocket +- Stockage des paramètres dans `conf.json` +- Configuration WiFi client +- Mise à jour des paramètres PID (Kp, Ki, Kd) et WiFi via la page de configuration + +## Structure du projet +``` +├── data/ +│ ├── index.html # Interface principale +│ ├── app.js # Logique client web +│ ├── style.css # Styles +│ ├── conf.html # Page de configuration +│ ├── conf.json # Paramètres persistants +├── src/ +│ └── main.cpp # Firmware ESP8266 +├── lib/ # Librairies PID et MAX6675 +├── platformio.ini # Configuration PlatformIO +``` + +## Démarrage +1. Flasher le firmware avec PlatformIO +2. Uploader le système de fichiers (dossier `data/`) +3. Connecter l'ESP8266 au WiFi : + - Par défaut, le module tente de se connecter au réseau défini dans `conf.json` (modifiable via la page de configuration). + - Si la connexion échoue ou si SSID/mot de passe sont vides, le module bascule automatiquement en mode Point d'Accès (AP) avec SSID `ReflowOven` et mot de passe `12345678`. + - L'IP par défaut en mode AP est `192.168.4.1`. +4. Accéder à l'interface web via l'adresse IP affichée dans le terminal (WiFi ou AP). + +## Utilisation +- **Chauffe manuelle** : Choisir la consigne avec le slider, valider, puis ajuster en temps réel +- **Chauffe automatique** : Définir les paramètres du profil, valider +- **Configuration** : Modifier SSID, mot de passe, PID, profils via `conf.html` + +## Dépendances +- [ArduinoJson](https://arduinojson.org/) +- [ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer) +- [PID_v1](https://playground.arduino.cc/Code/PIDLibrary/) +- [MAX6675](https://github.com/adafruit/MAX6675-library) + +## Auteurs +- Christophe (utilisateur) +- GitHub Copilot (assistance IA) + +## Licence +Ce projet est open source, licence MIT. diff --git a/data/conf.html b/data/conf.html index 0a8a106..1ae3ddb 100644 --- a/data/conf.html +++ b/data/conf.html @@ -16,7 +16,7 @@
- +
@@ -30,8 +30,7 @@
- - - + + diff --git a/data/conf.js b/data/conf.js new file mode 100644 index 0000000..2b4af5b --- /dev/null +++ b/data/conf.js @@ -0,0 +1,36 @@ +document.addEventListener('DOMContentLoaded', () => { + // Charger les valeurs actuelles + fetch('conf.json') + .then(r => r.json()) + .then(conf => { + for (const key in conf) { + const el = document.getElementById(key.toLowerCase()); + if (el) el.value = conf[key]; + } + }); + + // Sauvegarder à l'appui sur VALIDER + document.getElementById('validate').onclick = function() { + const data = {}; + ['ssid','password','kp','ki','kd'].forEach(key => { + const el = document.getElementById(key); + if (el) data[key] = el.value; + }); + fetch('/save_config', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(data) + }).then(r => { + if (r.ok) alert('Configuration enregistrée !'); + else alert('Erreur lors de l\'enregistrement'); + }); + }; +}); +fetch('conf.json') + .then(r => r.json()) + .then(conf => { + for (const key in conf) { + const el = document.getElementById(key); + if (el) el.value = conf[key]; + } + }); \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 1dce636..d0f9050 100644 --- a/platformio.ini +++ b/platformio.ini @@ -18,6 +18,7 @@ lib_deps = ESP32Async/ESPAsyncWebServer ESP32Async/ESPAsyncTCP LittleFS@^0.1.0 + ArduinoJson ; Partitionnement LittleFS (2MB pour fichiers, 1MB SPIFFS par défaut) board_build.filesystem = littlefs diff --git a/src/main.cpp b/src/main.cpp index e8e75de..eebcfcd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,10 +4,12 @@ #include #include #include +#include + +// Wifi credentials (mis à jour depuis le fichier conf.json au démarrage) +char ssid[32] = ""; +char password[64] = ""; -// Wifi credentials -const char* ssid = ""; -const char* password = ""; AsyncWebServer server(80); AsyncWebSocket ws("/ws"); @@ -17,10 +19,11 @@ 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; +double Kp = 2, Ki = 5, Kd = 1;//mises à jour depuis le fichier conf.json au démarrage PID myPID(&input, &output, &setpoint, Kp, Ki, Kd, DIRECT); // Variables globales pour le profil +volatile bool wifiReconfigPending = false; int preheatTemp = 0, preheatTime = 0; int soakTemp = 0, soakTime = 0; int reflowTemp = 0, reflowTime = 0; @@ -34,19 +37,107 @@ 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 loadConfig() { + if (!LittleFS.exists("/conf.json")) { + Serial.println("conf.json absent, création fichier par défaut"); + File f = LittleFS.open("/conf.json", "w"); + f.print("{\"ssid\":\"Linksys11539\",\"password\":\"etraxbbgxr\",\"Kp\":2,\"Ki\":5,\"Kd\":1}"); + f.close(); + } + File file = LittleFS.open("/conf.json", "r"); + if (!file) { + Serial.println("Impossible d'ouvrir conf.json"); + return; + } + JsonDocument doc; + DeserializationError err = deserializeJson(doc, file); + file.close(); + if (err) { + Serial.println("Erreur JSON conf.json"); + return; + } + if (doc["ssid"]) { + strncpy(ssid, doc["ssid"], sizeof(ssid)-1); + ssid[sizeof(ssid)-1] = '\0'; + } + if (doc["password"]) { + strncpy(password, doc["password"], sizeof(password)-1); + password[sizeof(password)-1] = '\0'; + } + if (doc["Kp"]) Kp = doc["Kp"].as(); + if (doc["Ki"]) Ki = doc["Ki"].as(); + if (doc["Kd"]) Kd = doc["Kd"].as(); + myPID.SetTunings(Kp, Ki, Kd); + Serial.printf("Config chargée : ssid=%s, kp=%.2f, ki=%.2f, kd=%.2f\n", ssid, Kp, Ki, Kd); +} + 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"); - }); + + 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); + + // Initialisation LittleFS + if (!LittleFS.begin()) { + Serial.println("Erreur LittleFS"); + return; + } + Serial.println("LittleFS monté"); + + // Charger la config + loadConfig(); + + // Route POST pour enregistrer la configuration depuis le client web (AsyncWebServer, version sans crash) + static String confBody; + server.on("/save_config", HTTP_POST, + [](AsyncWebServerRequest *request){ + if (confBody.length() > 0) { + // Sauvegarder l'ancienne config pour comparaison + String oldSsid = ssid; + String oldPwd = password; + File f = LittleFS.open("/conf.json", "w"); + if (!f) { + request->send(500, "text/plain", "Erreur écriture fichier"); + confBody = ""; + return; + } + f.print(confBody); + f.close(); + request->send(200, "text/plain", "OK"); + Serial.println("Configuration enregistrée via POST /conf.json"); + loadConfig(); + // Si ssid ou password ont changé, reconfigurer le WiFi + if (oldSsid != String(ssid) || oldPwd != String(password)) { + Serial.println("Changement SSID/PWD détecté, reconfiguration WiFi demandée..."); + wifiReconfigPending = true; + } + confBody = ""; + } else { + request->send(400, "text/plain", "Aucun corps reçu"); + } + }, + NULL, + [](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { + if (index == 0) confBody = ""; + for (size_t i = 0; i < len; i++) confBody += (char)data[i]; + } + ); + + // 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){ @@ -89,29 +180,36 @@ void setup() { 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()); + // Connexion WiFi ou démarrage AP si ssid/pwd vides + if (strlen(ssid) == 0 || strlen(password) == 0) { + Serial.println("SSID ou mot de passe vide, démarrage en mode AP"); + WiFi.mode(WIFI_AP); + WiFi.softAP("ReflowOven", "12345678"); + Serial.print("Point d'accès démarré : SSID=ReflowOven, PWD=12345678, IP=192.168.4.1"); + Serial.println(WiFi.softAPIP()); } else { - Serial.println("Échec connexion WiFi"); + 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, bascule en mode AP"); + WiFi.disconnect(); + delay(100); + WiFi.mode(WIFI_AP); + WiFi.softAP("ReflowOven", "12345678"); + Serial.print("Point d'accès démarré : SSID=ReflowOven, PWD=12345678, IP=192.168.4.1"); + Serial.println(WiFi.softAPIP()); + } } // Initialisation LittleFS @@ -133,6 +231,49 @@ void setup() { } void loop() { + // Reconfiguration WiFi non bloquante + static unsigned long wifiReconfigStart = 0; + static bool wifiReconfigInProgress = false; + if (wifiReconfigPending && !wifiReconfigInProgress) { + wifiReconfigInProgress = true; + wifiReconfigStart = millis(); + Serial.println("Début reconfiguration WiFi..."); + WiFi.disconnect(); + delay(100); + if (strlen(ssid) == 0 || strlen(password) == 0) { + WiFi.mode(WIFI_AP); + WiFi.softAP("ReflowOven", "12345678"); + Serial.print("Point d'accès démarré : SSID=ReflowOven, PWD=12345678, IP="); + Serial.println(WiFi.softAPIP()); + Serial.println("IP par défaut : 192.168.4.1"); + wifiReconfigPending = false; + wifiReconfigInProgress = false; + } else { + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + Serial.print("Connexion au WiFi : "); + Serial.println(ssid); + } + } + if (wifiReconfigInProgress) { + if (WiFi.status() == WL_CONNECTED) { + Serial.print("Connecté, IP : "); + Serial.println(WiFi.localIP()); + wifiReconfigPending = false; + wifiReconfigInProgress = false; + } else if (millis() - wifiReconfigStart > 10000) { // timeout 10s + Serial.println("Échec connexion WiFi, bascule en mode AP"); + WiFi.disconnect(); + delay(100); + WiFi.mode(WIFI_AP); + WiFi.softAP("ReflowOven", "12345678"); + Serial.print("Point d'accès démarré : SSID=ReflowOven, PWD=12345678, IP="); + Serial.println(WiFi.softAPIP()); + Serial.println("IP par défaut : 192.168.4.1"); + wifiReconfigPending = false; + wifiReconfigInProgress = false; + } + } ws.cleanupClients(); unsigned long now = millis();