// ============================================================
// SYSTÈME DE TRAÇABILITÉ INTELLIGENTE - HUILERIE D'OLIVE
// Version : SIMULATION WOKWI
// Plateforme : ESP32 DevKit V1 (38 pins)
// Date : 2026
// ============================================================
// ─── BIBLIOTHÈQUES ──────────────────────────────────────────
#include <WiFi.h>
#include <HTTPClient.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <LiquidCrystal_I2C.h>
// ─── CONFIGURATION WIFI & THINGSPEAK ────────────────────────
const char* WIFI_SSID = "Wokwi-GUEST"; // Réseau simulé Wokwi (pas de mot de passe)
const char* WIFI_PASSWORD = "";
const char* TS_API_KEY = "VOTRE_WRITE_API_KEY";
const char* TS_URL = "http://api.thingspeak.com/update";
// ThingSpeak désactivé en simulation (pas d'accès internet réel)
#define THINGSPEAK_ACTIF false
// ─── BROCHES ESP32 ──────────────────────────────────────────
#define PIN_DS18B20_MALAXAGE 4 // DS18B20 malaxage → D4
#define PIN_DS18B20_STOCKAGE 5 // DS18B20 stockage → D5
#define PIN_DEBIT 18 // YF-S201 débit → D18
#define PIN_POT_POIDS 34 // Potentiomètre poids → D34
#define PIN_POT_OFFSET 35 // Potentiomètre offset → D35
#define PIN_BTN_ARRIVEE 15 // Bouton arrivée → D15
#define PIN_BTN_CYCLE 14 // Bouton cycle → D14
#define PIN_BTN_EXTRACTION 13 // Bouton extraction → D13
#define PIN_LED_ROUGE 25 // LED rouge alarme → D25
#define PIN_LED_VERTE 26 // LED verte machine → D26
#define PIN_LED_BLEUE 27 // LED bleue WiFi → D27
#define PIN_BUZZER 32 // Buzzer → D32
#define PIN_POMPE 33 // Pompe (LED blanche simulée) → D33
// ─── CONSTANTES MÉTIER ──────────────────────────────────────
#define SEUIL_TEMP_MALAXAGE 27.0f
#define HYST_BASSE 26.5f
#define SEUIL_TEMP_STOCKAGE 20.0f
#define DUREE_MALAXAGE_MIN 45UL
#define DUREE_MALAXAGE_MAX 60UL
#define K_DEBIT 450.0f
#define INTERVALLE_THINGSPEAK 15000UL
// ─── OBJETS MATÉRIELS ───────────────────────────────────────
OneWire ow_malaxage(PIN_DS18B20_MALAXAGE);
OneWire ow_stockage(PIN_DS18B20_STOCKAGE);
DallasTemperature capteur_malaxage(&ow_malaxage);
DallasTemperature capteur_stockage(&ow_stockage);
LiquidCrystal_I2C lcd(0x27, 16, 2);
// ─── ÉTAT DU LOT EN COURS ───────────────────────────────────
struct Lot {
String id;
String producteur;
float poids_kg;
float temp_malaxage;
float volume_huile_L;
float rendement_pct;
float temp_stockage;
unsigned long horodatage;
bool actif;
};
Lot lot_courant;
int compteur_lots = 0;
const String PRODUCTEURS[] = {
"Ahmed Ben Ali",
"Fatima Trabelsi",
"Mohamed Khamassi",
"Sonia Gharbi",
"Karim Mansour"
};
const int NB_PRODUCTEURS = 5;
int index_producteur = 0;
// ─── ÉTAT DES PHASES ────────────────────────────────────────
bool phase_malaxage_active = false;
bool alarme_temperature = false;
bool pompe_active = false;
unsigned long debut_malaxage = 0;
unsigned long derniere_mesure = 0;
unsigned long dernier_envoi = 0;
// ─── COMPTEUR DÉBIT (interruption) ──────────────────────────
volatile unsigned long nb_impulsions = 0;
void IRAM_ATTR ISR_debit() { nb_impulsions++; }
// ─── FILTRAGE TEMPÉRATURE ────────────────────────────────────
float historique_temp[5] = {25.0f, 25.0f, 25.0f, 25.0f, 25.0f};
int idx_historique = 0;
// ─── NIVEAU DE STOCKAGE ──────────────────────────────────────
float stock_cuve_L = 0.0f;
float cuve_max_L = 5000.0f;
// ─── PROTOTYPES ──────────────────────────────────────────────
void connexion_wifi();
void lcd_afficher(String ligne1, String ligne2);
void afficher_lcd_principal(float temp_m, float temp_s, float poids, float vol);
void debug_serie(float temp_m, float temp_s, float poids, float vol);
void envoyer_thingspeak();
float lire_temperature_malaxage();
float lire_temperature_stockage();
float lire_poids_simule();
float calculer_volume_debit();
float calculer_rendement(float volume_L, float poids_kg);
void gerer_alarme_temperature(float temp);
void gerer_duree_malaxage(unsigned long maintenant);
void phase_reception();
void demarrer_malaxage();
void arreter_malaxage();
void phase_extraction();
void mise_en_stockage(float volume_L);
void archiver_lot_final();
void gerer_bouton_arrivee();
void gerer_bouton_cycle();
void gerer_bouton_extraction();
String generer_id_lot();
// ============================================================
// SETUP
// ============================================================
void setup() {
Serial.begin(115200);
Serial.println("\n=== HUILERIE DIGITALE — MODE WOKWI ===");
pinMode(PIN_LED_ROUGE, OUTPUT);
pinMode(PIN_LED_VERTE, OUTPUT);
pinMode(PIN_LED_BLEUE, OUTPUT);
pinMode(PIN_BUZZER, OUTPUT);
pinMode(PIN_POMPE, OUTPUT);
pinMode(PIN_BTN_ARRIVEE, INPUT_PULLUP);
pinMode(PIN_BTN_CYCLE, INPUT_PULLUP);
pinMode(PIN_BTN_EXTRACTION, INPUT_PULLUP);
pinMode(PIN_DEBIT, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(PIN_DEBIT), ISR_debit, RISING);
lcd.init();
lcd.backlight();
lcd_afficher("Huilerie", "Wokwi v1.0");
delay(2000);
capteur_malaxage.begin();
capteur_stockage.begin();
lot_courant.actif = false;
lot_courant.poids_kg = 0.0f;
lot_courant.temp_malaxage = 0.0f;
lot_courant.temp_stockage = 0.0f;
lot_courant.volume_huile_L = 0.0f;
lot_courant.rendement_pct = 0.0f;
lot_courant.horodatage = 0;
connexion_wifi();
lcd_afficher("Systeme pret", "Attente lot...");
Serial.println("[INFO] Appuyer sur les boutons pour simuler les phases");
Serial.println("[INFO] BTN VERT = Arrivee producteur");
Serial.println("[INFO] BTN JAUNE = Demarrer/Arreter malaxage");
Serial.println("[INFO] BTN ROUGE = Extraction + Archivage");
}
// ============================================================
// BOUCLE PRINCIPALE
// ============================================================
void loop() {
unsigned long maintenant = millis();
if (maintenant - derniere_mesure >= 1000) {
derniere_mesure = maintenant;
float temp_m = lire_temperature_malaxage();
float temp_s = lire_temperature_stockage();
float poids = lire_poids_simule();
float volume = calculer_volume_debit();
if (lot_courant.actif) {
lot_courant.temp_malaxage = temp_m;
lot_courant.temp_stockage = temp_s;
lot_courant.volume_huile_L = volume;
lot_courant.rendement_pct = calculer_rendement(volume, lot_courant.poids_kg);
}
gerer_alarme_temperature(temp_m);
if (phase_malaxage_active) gerer_duree_malaxage(maintenant);
afficher_lcd_principal(temp_m, temp_s, poids, volume);
debug_serie(temp_m, temp_s, poids, volume);
}
if (maintenant - dernier_envoi >= INTERVALLE_THINGSPEAK) {
dernier_envoi = maintenant;
envoyer_thingspeak();
}
gerer_bouton_arrivee();
gerer_bouton_cycle();
gerer_bouton_extraction();
delay(10);
}
// ============================================================
// PHASES
// ============================================================
void phase_reception() {
Serial.println("\n--- PHASE 1 : RÉCEPTION ---");
index_producteur = (index_producteur + 1) % NB_PRODUCTEURS;
compteur_lots++;
lot_courant.id = generer_id_lot();
lot_courant.producteur = PRODUCTEURS[index_producteur];
lot_courant.poids_kg = lire_poids_simule();
lot_courant.horodatage = millis();
lot_courant.actif = true;
lot_courant.volume_huile_L = 0.0f;
lot_courant.rendement_pct = 0.0f;
lot_courant.temp_malaxage = 0.0f;
lot_courant.temp_stockage = 0.0f;
Serial.println(" Lot ID : " + lot_courant.id);
Serial.println(" Producteur: " + lot_courant.producteur);
Serial.println(" Poids : " + String(lot_courant.poids_kg, 1) + " kg");
lcd_afficher("Lot: " + lot_courant.id, lot_courant.producteur.substring(0, 16));
delay(3000);
lcd_afficher(String(lot_courant.poids_kg, 1) + " kg recus", "Lot cree OK");
delay(2000);
digitalWrite(PIN_LED_VERTE, HIGH);
}
void demarrer_malaxage() {
if (!lot_courant.actif) {
Serial.println("[ERREUR] Aucun lot actif !");
lcd_afficher("ERREUR", "Creer un lot");
return;
}
phase_malaxage_active = true;
debut_malaxage = millis();
alarme_temperature = false;
nb_impulsions = 0;
Serial.println("\n--- PHASE 3 : MALAXAGE DÉMARRÉ ---");
lcd_afficher("Malaxage ON", "Seuil: 27.0C");
digitalWrite(PIN_LED_VERTE, HIGH);
}
void arreter_malaxage() {
phase_malaxage_active = false;
unsigned long duree_min = (millis() - debut_malaxage) / 60000UL;
Serial.println("--- MALAXAGE ARRÊTÉ — Durée : " + String(duree_min) + " min ---");
if (duree_min < DUREE_MALAXAGE_MIN) {
lcd_afficher("ATTENTION!", "Duree courte");
delay(3000);
} else {
lcd_afficher("Malaxage OK", String(duree_min) + " min");
}
digitalWrite(PIN_LED_VERTE, LOW);
}
void gerer_alarme_temperature(float temp) {
if (!alarme_temperature && temp > SEUIL_TEMP_MALAXAGE && phase_malaxage_active) {
alarme_temperature = true;
digitalWrite(PIN_LED_ROUGE, HIGH);
digitalWrite(PIN_BUZZER, HIGH);
Serial.println("[ALARME] T=" + String(temp, 2) + "C > " + String(SEUIL_TEMP_MALAXAGE));
lcd_afficher("!!! ALARME !!!", "T=" + String(temp, 1) + "C > 27");
}
if (alarme_temperature && temp < HYST_BASSE) {
alarme_temperature = false;
digitalWrite(PIN_LED_ROUGE, LOW);
digitalWrite(PIN_BUZZER, LOW);
Serial.println("[OK] T revenue : " + String(temp, 2) + "C");
lcd_afficher("Alarme OFF", "T=" + String(temp, 1) + "C OK");
}
}
void gerer_duree_malaxage(unsigned long maintenant) {
if ((maintenant - debut_malaxage) / 60000UL >= DUREE_MALAXAGE_MAX) {
Serial.println("[INFO] Durée max atteinte, arrêt auto.");
arreter_malaxage();
}
}
void phase_extraction() {
if (!lot_courant.actif) return;
Serial.println("\n--- PHASE 4 : EXTRACTION ---");
lot_courant.rendement_pct = calculer_rendement(lot_courant.volume_huile_L, lot_courant.poids_kg);
Serial.println(" Rendement : " + String(lot_courant.rendement_pct, 1) + " %");
lcd_afficher("Rendement:", String(lot_courant.rendement_pct, 1) + " %");
delay(3000);
}
void mise_en_stockage(float volume_L) {
stock_cuve_L += volume_L;
if (stock_cuve_L > cuve_max_L) stock_cuve_L = cuve_max_L;
float niv = (stock_cuve_L / cuve_max_L) * 100.0f;
Serial.println("\n--- PHASE 5 : STOCKAGE — " + String(stock_cuve_L, 0) + "L (" + String(niv, 1) + "%) ---");
lcd_afficher("Stock:" + String(stock_cuve_L, 0) + "L", "Niv:" + String(niv, 0) + "%");
delay(2000);
}
void archiver_lot_final() {
Serial.println("\n--- PHASE 7 : ARCHIVAGE ---");
Serial.println(" ID : " + lot_courant.id);
Serial.println(" Producteur : " + lot_courant.producteur);
Serial.println(" Poids : " + String(lot_courant.poids_kg, 1) + " kg");
Serial.println(" Temp. max : " + String(lot_courant.temp_malaxage, 2) + " °C");
Serial.println(" Volume huile : " + String(lot_courant.volume_huile_L, 2) + " L");
Serial.println(" Rendement : " + String(lot_courant.rendement_pct, 1) + " %");
envoyer_thingspeak();
Serial.println("\n[CSV] id,producteur,poids_kg,temp_malaxage,volume_huile_L,rendement_pct");
Serial.println("[CSV] " + lot_courant.id + "," + lot_courant.producteur + "," +
String(lot_courant.poids_kg, 1) + "," + String(lot_courant.temp_malaxage, 2) + "," +
String(lot_courant.volume_huile_L, 2) + "," + String(lot_courant.rendement_pct, 1));
lot_courant.actif = false;
lcd_afficher("Lot archive!", "Lot: " + lot_courant.id);
delay(3000);
lcd_afficher("Systeme pret", "Attente lot...");
}
// ============================================================
// CAPTEURS
// ============================================================
float lire_temperature_malaxage() {
capteur_malaxage.requestTemperatures();
float t = capteur_malaxage.getTempCByIndex(0);
float offset = ((float)analogRead(PIN_POT_OFFSET) / 4095.0f) * 10.0f - 5.0f;
t += offset;
if (t < -50.0f || t > 100.0f) t = 25.0f + offset;
historique_temp[idx_historique] = t;
idx_historique = (idx_historique + 1) % 5;
float somme = 0.0f;
for (int i = 0; i < 5; i++) somme += historique_temp[i];
return somme / 5.0f;
}
float lire_temperature_stockage() {
capteur_stockage.requestTemperatures();
float t = capteur_stockage.getTempCByIndex(0);
if (t < -50.0f || t > 100.0f) t = 18.5f;
return t;
}
float lire_poids_simule() {
return ((float)analogRead(PIN_POT_POIDS) / 4095.0f) * 500.0f;
}
float calculer_volume_debit() {
noInterrupts();
unsigned long imp = nb_impulsions;
interrupts();
return (float)imp / K_DEBIT;
}
float calculer_rendement(float volume_L, float poids_kg) {
if (poids_kg < 1.0f) return 0.0f;
return (volume_L * 0.916f / poids_kg) * 100.0f;
}
// ============================================================
// BOUTONS
// ============================================================
unsigned long dernier_appui_arrivee = 0;
unsigned long dernier_appui_cycle = 0;
unsigned long dernier_appui_extraction = 0;
const unsigned long DELAI_REBOND = 300UL;
void gerer_bouton_arrivee() {
if (digitalRead(PIN_BTN_ARRIVEE) == LOW && millis() - dernier_appui_arrivee > DELAI_REBOND) {
dernier_appui_arrivee = millis();
phase_reception();
}
}
void gerer_bouton_cycle() {
if (digitalRead(PIN_BTN_CYCLE) == LOW && millis() - dernier_appui_cycle > DELAI_REBOND) {
dernier_appui_cycle = millis();
if (!phase_malaxage_active) demarrer_malaxage();
else arreter_malaxage();
}
}
void gerer_bouton_extraction() {
if (digitalRead(PIN_BTN_EXTRACTION) == LOW && millis() - dernier_appui_extraction > DELAI_REBOND) {
dernier_appui_extraction = millis();
phase_extraction();
mise_en_stockage(lot_courant.volume_huile_L);
archiver_lot_final();
}
}
// ============================================================
// LCD
// ============================================================
void lcd_afficher(String ligne1, String ligne2) {
lcd.clear();
lcd.setCursor(0, 0); lcd.print(ligne1.substring(0, 16));
lcd.setCursor(0, 1); lcd.print(ligne2.substring(0, 16));
}
void afficher_lcd_principal(float temp_m, float temp_s, float poids, float vol) {
static unsigned long dernier_switch = 0;
static bool ecran_A = true;
if (millis() - dernier_switch > 4000UL) { dernier_switch = millis(); ecran_A = !ecran_A; }
if (ecran_A) {
lcd_afficher("Malaxage:" + String(temp_m, 1) + "C", "Stockage:" + String(temp_s, 1) + "C");
} else {
if (lot_courant.actif) lcd_afficher("Lot:" + lot_courant.id, "Rdmt:" + String(lot_courant.rendement_pct, 1) + "%");
else lcd_afficher("En attente", "Pas de lot actif");
}
}
// ============================================================
// THINGSPEAK (désactivé en simulation)
// ============================================================
void envoyer_thingspeak() {
if (!THINGSPEAK_ACTIF) {
Serial.println("[ThingSpeak] Simulation — envoi désactivé");
return;
}
}
// ============================================================
// WIFI
// ============================================================
void connexion_wifi() {
Serial.print("[WiFi] Connexion Wokwi-GUEST ");
lcd_afficher("WiFi...", "Wokwi-GUEST");
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
int t = 0;
while (WiFi.status() != WL_CONNECTED && t < 20) { delay(500); Serial.print("."); t++; }
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\n[WiFi] Connecté ! IP : " + WiFi.localIP().toString());
lcd_afficher("WiFi OK", WiFi.localIP().toString());
digitalWrite(PIN_LED_BLEUE, HIGH); delay(1500); digitalWrite(PIN_LED_BLEUE, LOW);
} else {
Serial.println("\n[WiFi] ÉCHEC — mode hors ligne");
lcd_afficher("WiFi ECHEC", "Mode hors ligne");
}
}
// ============================================================
// UTILITAIRES
// ============================================================
String generer_id_lot() {
String id = "L";
if (compteur_lots < 10) id += "00";
else if (compteur_lots < 100) id += "0";
return id + String(compteur_lots);
}
void debug_serie(float temp_m, float temp_s, float poids, float vol) {
Serial.print("[DATA] T.Malaxage="); Serial.print(temp_m, 2);
Serial.print(" | T.Stockage="); Serial.print(temp_s, 2);
Serial.print(" | Poids="); Serial.print(poids, 0);
Serial.print("kg | Vol="); Serial.print(vol, 3);
Serial.print("L | Rdmt="); Serial.print(lot_courant.rendement_pct, 1);
Serial.print("% | Stock="); Serial.print(stock_cuve_L, 0);
Serial.println("L");
}