/* ============================================================================
Smart-Soja (Unité de Pretraitement du Soja) - Code ESP32 avec GRAFCET (FreeRTOS et MQTT)
=========================================================================== */
#include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/queue.h>
#include <freertos/timers.h>
#include <WiFi.h>
#include <WiFiClient.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <ArduinoJson.h>
#include <ESP32Servo.h>
#include <SD.h>
#include <SPI.h>
// Objets Servo
Servo servoTremie;
Servo servoVanne;
// Pins SD Card
const int SD_CS = 5;
// Variables de vitesse (0-255) lues depuis les potentiomètres
volatile uint16_t vitesse_tamis_val = 255;
volatile uint16_t vitesse_vis_val = 255;
/* ============================================================================
WIFI ET MQTT (Cloud HiveMQ)
============================================================================ */
const char *ssid = "Wokwi-GUEST";
const char *password = "";
const char *mqtt_server = "broker.hivemq.com";
const int mqtt_port = 1883;
WiFiClient espClient;
PubSubClient client(espClient);
String unit_id = "";
/* ============================================================================
AFFICHEUR LCD I2C
============================================================================ */
LiquidCrystal_I2C lcd(0x27, 16, 2);
/* ============================================================================
ÉNUMÉRATIONS ET STRUCTURES (Francisées)
============================================================================ */
enum etape_grafcet_t
{
ETAPE_0_VEILLE,
ETAPE_1_VERIF_SOJA,
ETAPE_2_ALIMENTATION,
ETAPE_3_NETTOYAGE,
ETAPE_4_MESURE_HUMIDITE,
ETAPE_5_DECISION,
ETAPE_5_SECHAGE_ACTIF,
ETAPE_6_TRANSFERT,
ETAPE_7_PESEE,
ETAPE_8_FIN_LOT,
ETAPE_9_SECURITE
};
enum evenement_grafcet_t
{
EVT_T0_DEMARRAGE,
EVT_T1_IR1_DETECTE,
EVT_T2_FIN_ALIMENTATION,
EVT_T3_FIN_NETTOYAGE,
EVT_T4_MESURE_FAITE,
EVT_T5A_HUMIDITE_OK,
EVT_T5B_SURCHAUFFE,
EVT_T5C_TIMEOUT,
EVT_T6_FIN_TRANSFERT,
EVT_T7_POIDS_STABLE,
EVT_T8_IR2_SAC_RETIRE,
EVT_T9_REARMEMENT,
EVT_ARRET_URGENCE,
EVT_STOP_MARCHE
};
enum commande_actionneur_t
{
CMD_TOUT_OFF,
CMD_OUVRIR_TREMIE,
CMD_FERMER_TREMIE,
CMD_OUVRIR_VANNE,
CMD_FERMER_VANNE,
CMD_TAMIS_ON,
CMD_TAMIS_OFF,
CMD_VENTILATEUR_ON,
CMD_VENTILATEUR_OFF,
CMD_CHAUFFAGE_ON,
CMD_CHAUFFAGE_OFF,
CMD_VIS_ON,
CMD_VIS_OFF,
CMD_FIN_SECHAGE,
CMD_URGENCE_OFF
};
/* ============================================================================
MODE ET PARAMÈTRES CONFIGURABLES (Modifiables via WiFi/MQTT)
============================================================================ */
volatile bool mode_automatique = true; // true = GRAFCET, false = Contrôle Manuel IHM
// Temporisations (ms)
#define DELAI_ANTI_REBOND 200 // Délai anti-rebond pour les capteurs/boutons
int temps_t1_ms = 2000; // Alimentation servo
int temps_t2_ms = 5000; // Nettoyage tamis
int temps_t3_ms = 1000; // Mesure humidité
int temps_t4_ms = 4000; // Transfert vers pesée
int timeout_sechage_ms = 30000; // Timeout séchage sécurité
// Seuils
uint16_t seuil_humidite = 120; // 12% (valeur ADC)
uint16_t seuil_temperature = 800; // Temp max
int32_t poids_cible = 5000; // Poids cible (g)
int32_t seuil_stabilite = 10; // Tolérance stabilité
uint8_t nb_mesures_stables = 3; // Nb mesures stables requises
/* ============================================================================
PINS ESP32
============================================================================ */
// Capteurs (Source: Schéma KiCad)
const int BROCHE_IR1 = 32; // Label IR1
const int BROCHE_IR2 = 33; // Label IR2
const int BROCHE_SOIL1 = 36; // Label SENSOR_VP
const int BROCHE_SOIL2 = 39; // Label SENSOR_VN
const int BROCHE_TEMP = 25; // Label DHT_DATA
const int BROCHE_HX711_DT = 4; // Label HX711_DO
const int BROCHE_HX711_SCK = 16; // Label HX711_SCK
// Potentiomètres de vitesse (Source: Schéma KiCad CTRL_M1/M2)
const int BROCHE_POT_TAMIS = 35; // Label CTRL_M1
const int BROCHE_POT_VIS = 34; // Label CTRL_M2
// Actionneurs (Source: Schéma KiCad)
const int BROCHE_SERVO_TREMIE = 26; // Label SERVO_CTRL (M1)
const int BROCHE_SERVO_VANNE = 2; // Signal piqué sur LED_RGB
const int BROCHE_MOTEUR_TAMIS = 14; // Label MOTOR1_CTRL (Q2)
const int BROCHE_VIS_SANS_FIN = 12; // Label MOTOR2_CTRL (Q1)
const int BROCHE_VENTILATEUR = 13; // Label FAN_CTRL (Q3)
const int BROCHE_RESISTANCE = 27; // Label HEATER_CTRL (Q4)
const int BROCHE_LED_ERR = 2; // Partagée avec Servo Vanne (ou à déplacer)
const int BROCHE_BUZZER = 17; // Label BUZZER_CTRL (Q5)
// Boutons (Source: Schéma KiCad)
const int BROCHE_START = 15; // Label SWITCH_ON-OFF
const int BROCHE_RESET = 0; // Pin Boot (ou EN via hardware)
const int BROCHE_URGENCE = 1; // Pin TX0 (Piquée pour libérer la SD)
/* ============================================================================
COMMUNICATION INTER-TÂCHES
============================================================================ */
QueueHandle_t xQueue_evenements;
QueueHandle_t xQueue_actionneurs;
QueueHandle_t xQueue_humidite;
QueueHandle_t xQueue_poids;
volatile etape_grafcet_t etape_actuelle_ui = ETAPE_0_VEILLE;
volatile uint16_t derniere_humidite_ui = 0;
volatile uint16_t derniere_temperature_ui = 0;
volatile int32_t dernier_poids_ui = 0;
TimerHandle_t xTimer_t2, xTimer_t3, xTimer_t4, xTimer_sechage;
volatile bool arret_urgence_actif = false;
volatile uint32_t dernier_temps_ir1 = 0, dernier_temps_ir2 = 0, dernier_temps_start = 0;
/* ============================================================================
PROTOTYPES
============================================================================ */
void tache_Grafcet(void *pvParameters);
void tache_Capteurs(void *pvParameters);
void tache_Actionneurs(void *pvParameters);
void tache_Pesee(void *pvParameters);
void tache_Securite(void *pvParameters);
void tache_LCD(void *pvParameters);
void tache_MQTT(void *pvParameters);
void callback_timer_t2(TimerHandle_t xTimer);
void callback_timer_t3(TimerHandle_t xTimer);
void callback_timer_t4(TimerHandle_t xTimer);
void callback_timer_sechage(TimerHandle_t xTimer);
void callback_mqtt(char *topic, byte *payload, unsigned int length);
void activer_arret_urgence(void);
void envoyer_evenement(evenement_grafcet_t evt);
void configurer_wifi_mqtt();
String obtenir_id_machine();
// Fonctions Stockage Offline (SD Card)
void sauvegarder_donnee_sd(String topic, String payload) {
File logFile = SD.open("/offline.log", FILE_APPEND);
if (logFile) {
logFile.println(topic + "|" + payload);
logFile.close();
Serial.println("💾 [SD] Donnée sauvegardée localement (Offline)");
}
}
void synchroniser_donnees_sd() {
if (!SD.exists("/offline.log")) return;
Serial.println("🔄 [SD] Début de la synchronisation des données stockées...");
File logFile = SD.open("/offline.log", FILE_READ);
if (logFile) {
while (logFile.available()) {
String line = logFile.readStringUntil('\n');
line.trim();
if (line.length() > 0) {
int separatorIndex = line.indexOf('|');
if (separatorIndex != -1) {
String topic = line.substring(0, separatorIndex);
String payload = line.substring(separatorIndex + 1);
if (client.publish(topic.c_str(), payload.c_str())) {
Serial.println("✅ [SD] Synchronisation réussie pour une ligne");
} else {
logFile.close();
return; // Arrêt si échec d'envoi
}
}
}
}
logFile.close();
SD.remove("/offline.log");
Serial.println("✨ [SD] Synchronisation terminée. Fichier vidé.");
}
}
/* ============================================================================
SETUP
============================================================================ */
void setup()
{
Serial.begin(115200);
lcd.init();
lcd.backlight();
lcd.print("SMART-SOJA Init");
// Init Carte SD
if (!SD.begin(SD_CS)) {
Serial.println("❌ [SD] Erreur d'initialisation !");
lcd.setCursor(0, 1);
lcd.print("SD Error!");
} else {
Serial.println("✅ [SD] Carte prête.");
}
// Init broches
pinMode(BROCHE_IR1, INPUT);
pinMode(BROCHE_IR2, INPUT);
pinMode(BROCHE_POT_TAMIS, INPUT); // Potentiomètre
pinMode(BROCHE_POT_VIS, INPUT); // Potentiomètre
pinMode(BROCHE_START, INPUT_PULLUP);
pinMode(BROCHE_RESET, INPUT_PULLUP);
pinMode(BROCHE_URGENCE, INPUT_PULLUP);
// Initialisation Servos
servoTremie.attach(BROCHE_SERVO_TREMIE);
servoVanne.attach(BROCHE_SERVO_VANNE);
servoTremie.write(0); // Fermé par défaut
servoVanne.write(0); // Fermé par défaut
pinMode(BROCHE_MOTEUR_TAMIS, OUTPUT);
pinMode(BROCHE_VENTILATEUR, OUTPUT);
pinMode(BROCHE_RESISTANCE, OUTPUT);
pinMode(BROCHE_VIS_SANS_FIN, OUTPUT);
pinMode(BROCHE_LED_ERR, OUTPUT);
pinMode(BROCHE_BUZZER, OUTPUT);
configurer_wifi_mqtt();
xQueue_evenements = xQueueCreate(10, sizeof(evenement_grafcet_t));
xQueue_actionneurs = xQueueCreate(20, sizeof(commande_actionneur_t));
xQueue_humidite = xQueueCreate(1, sizeof(uint16_t));
xQueue_poids = xQueueCreate(1, sizeof(int32_t));
xTimer_t2 = xTimerCreate("T2", pdMS_TO_TICKS(temps_t2_ms), pdFALSE, NULL, callback_timer_t2);
xTimer_t3 = xTimerCreate("T3", pdMS_TO_TICKS(temps_t3_ms), pdFALSE, NULL, callback_timer_t3);
xTimer_t4 = xTimerCreate("T4", pdMS_TO_TICKS(temps_t4_ms), pdFALSE, NULL, callback_timer_t4);
xTimer_sechage = xTimerCreate("TSec", pdMS_TO_TICKS(timeout_sechage_ms), pdFALSE, NULL, callback_timer_sechage);
xTaskCreatePinnedToCore(tache_Grafcet, "Control", 4096, NULL, 3, NULL, 1);
xTaskCreatePinnedToCore(tache_Capteurs, "Capteurs", 2048, NULL, 2, NULL, 1);
xTaskCreatePinnedToCore(tache_Actionneurs, "Actionneurs", 2048, NULL, 2, NULL, 1);
xTaskCreatePinnedToCore(tache_Pesee, "Pesee", 2048, NULL, 2, NULL, 1);
xTaskCreatePinnedToCore(tache_Securite, "Securite", 2048, NULL, 4, NULL, 0);
xTaskCreatePinnedToCore(tache_LCD, "IHM_LCD", 2048, NULL, 1, NULL, 1);
xTaskCreatePinnedToCore(tache_MQTT, "Reseau_MQTT", 4096, NULL, 1, NULL, 0); // Stack + large pour JSON
}
void loop() { vTaskDelay(pdMS_TO_TICKS(1000)); }
/* ============================================================================
WIFI / MQTT ET CALLBACK DE CONFIGURATION WEB
============================================================================ */
String obtenir_id_machine()
{
String mac = WiFi.macAddress();
mac.replace(":", "");
return "SS-" + mac.substring(mac.length() - 6);
}
void configurer_wifi_mqtt()
{
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
}
unit_id = obtenir_id_machine();
client.setServer(mqtt_server, mqtt_port);
client.setCallback(callback_mqtt);
Serial.println("Connexion WiFi...");
Serial.println("WiFi connecte !");
}
void callback_mqtt(char *topic, byte *payload, unsigned int length)
{
String msg;
for (int i = 0; i < length; i++)
{
msg += (char)payload[i];
}
Serial.printf("[MQTT] Commande reçue : %s\n", msg.c_str());
StaticJsonDocument<512> doc;
DeserializationError error = deserializeJson(doc, msg);
if (error)
{
Serial.println("[MQTT] Erreur parsing JSON");
return;
}
// Fonction de test LED intégrée (ping-pong avec l'interface)
if (doc.containsKey("test_led"))
{
bool etat_led = doc["test_led"];
digitalWrite(BROCHE_LED_ERR, etat_led ? HIGH : LOW);
Serial.printf("--> Test LED intégrée : %s\n", etat_led ? "ALLUMEE" : "ETEINTE");
}
// Règlage Mode
if (doc.containsKey("mode_automatique"))
{
mode_automatique = doc["mode_automatique"];
Serial.printf("--> Changement MODE: %s\n", mode_automatique ? "AUTO" : "MANUEL");
}
// Commandes Manuelles (Seulement si en mode manuel)
if (!mode_automatique && doc.containsKey("commande_manuel"))
{
String cmdMode = doc["commande_manuel"].as<String>();
commande_actionneur_t cmd;
if (cmdMode == "TAMIS_ON")
cmd = CMD_TAMIS_ON;
else if (cmdMode == "TAMIS_OFF")
cmd = CMD_TAMIS_OFF;
else if (cmdMode == "TREMIE_OUVRIR")
cmd = CMD_OUVRIR_TREMIE;
else if (cmdMode == "TREMIE_FERMER")
cmd = CMD_FERMER_TREMIE;
// Ajouter les autres selon l'interface web...
xQueueSend(xQueue_actionneurs, &cmd, 0);
}
// Mise à jour Configuration
if (doc.containsKey("config"))
{
if (doc["config"].containsKey("temps_t1_ms"))
temps_t1_ms = doc["config"]["temps_t1_ms"];
if (doc["config"].containsKey("poids_cible"))
poids_cible = doc["config"]["poids_cible"];
if (doc["config"].containsKey("seuil_humidite"))
seuil_humidite = doc["config"]["seuil_humidite"];
Serial.println("--> Configuration mise à jour !");
// Mise à jour des Timers FreeRTOS si nécessaire
xTimerChangePeriod(xTimer_t2, pdMS_TO_TICKS(temps_t2_ms), 0);
xTimerChangePeriod(xTimer_t3, pdMS_TO_TICKS(temps_t3_ms), 0);
xTimerChangePeriod(xTimer_t4, pdMS_TO_TICKS(temps_t4_ms), 0);
xTimerChangePeriod(xTimer_sechage, pdMS_TO_TICKS(timeout_sechage_ms), 0);
}
}
void verifier_reconnexion_mqtt()
{
if (!client.connected() && WiFi.status() == WL_CONNECTED)
{
String clientId = "SmartSoja_Core_" + unit_id;
if (client.connect(clientId.c_str()))
{
String topicCommands = "smart-soja/commands/" + unit_id;
client.subscribe(topicCommands.c_str());
}
}
}
void envoyer_evenement(evenement_grafcet_t evt) { xQueueSend(xQueue_evenements, &evt, 0); }
void activer_arret_urgence()
{
digitalWrite(BROCHE_SERVO_TREMIE, LOW);
digitalWrite(BROCHE_MOTEUR_TAMIS, LOW);
digitalWrite(BROCHE_VENTILATEUR, LOW);
digitalWrite(BROCHE_RESISTANCE, LOW);
digitalWrite(BROCHE_VIS_SANS_FIN, LOW);
digitalWrite(BROCHE_LED_ERR, HIGH);
digitalWrite(BROCHE_BUZZER, HIGH);
arret_urgence_actif = true;
}
/* ================== CALLBACKS TIMERS ================== */
void callback_timer_t2(TimerHandle_t x) { envoyer_evenement(EVT_T2_FIN_ALIMENTATION); }
void callback_timer_t3(TimerHandle_t x) { envoyer_evenement(EVT_T3_FIN_NETTOYAGE); }
void callback_timer_t4(TimerHandle_t x) { envoyer_evenement(EVT_T6_FIN_TRANSFERT); }
void callback_timer_sechage(TimerHandle_t x) { envoyer_evenement(EVT_T5C_TIMEOUT); }
/* ============================================================================
TÂCHE 1 : CONTRÔLE GRAFCET (Coeur 1, Prio 3)
============================================================================ */
void tache_Grafcet(void *pvParameters)
{
etape_grafcet_t etape_actuelle = ETAPE_0_VEILLE;
evenement_grafcet_t evt;
BaseType_t statutRCV;
uint16_t humidite = 0;
for (;;)
{
// En mode manuel, le GRAFCET se gèle et observe
if (!mode_automatique)
{
vTaskDelay(pdMS_TO_TICKS(100));
continue;
}
etape_actuelle_ui = etape_actuelle;
switch (etape_actuelle)
{
case ETAPE_0_VEILLE:
{
commande_actionneur_t off = CMD_TOUT_OFF;
xQueueSend(xQueue_actionneurs, &off, 0);
statutRCV = xQueueReceive(xQueue_evenements, &evt, portMAX_DELAY);
if (statutRCV == pdTRUE && evt == EVT_T0_DEMARRAGE)
etape_actuelle = ETAPE_1_VERIF_SOJA;
}
break;
case ETAPE_1_VERIF_SOJA:
{
statutRCV = xQueueReceive(xQueue_evenements, &evt, portMAX_DELAY);
if (statutRCV == pdTRUE) {
if (evt == EVT_STOP_MARCHE) etape_actuelle = ETAPE_0_VEILLE;
else if (evt == EVT_T1_IR1_DETECTE) etape_actuelle = ETAPE_2_ALIMENTATION;
}
}
break;
case ETAPE_2_ALIMENTATION:
{
commande_actionneur_t cmd = CMD_OUVRIR_TREMIE;
xQueueSend(xQueue_actionneurs, &cmd, 0);
vTaskDelay(pdMS_TO_TICKS(temps_t1_ms));
cmd = CMD_FERMER_TREMIE;
xQueueSend(xQueue_actionneurs, &cmd, 0);
xTimerStart(xTimer_t2, 0);
statutRCV = xQueueReceive(xQueue_evenements, &evt, portMAX_DELAY);
if (statutRCV == pdTRUE) {
if (evt == EVT_STOP_MARCHE) etape_actuelle = ETAPE_0_VEILLE;
else if (evt == EVT_T2_FIN_ALIMENTATION) etape_actuelle = ETAPE_3_NETTOYAGE;
}
}
break;
case ETAPE_3_NETTOYAGE:
{
commande_actionneur_t cmd = CMD_TAMIS_ON;
xQueueSend(xQueue_actionneurs, &cmd, 0);
xTimerStart(xTimer_t3, 0);
statutRCV = xQueueReceive(xQueue_evenements, &evt, portMAX_DELAY);
if (statutRCV == pdTRUE) {
if (evt == EVT_STOP_MARCHE) etape_actuelle = ETAPE_0_VEILLE;
else if (evt == EVT_T3_FIN_NETTOYAGE) etape_actuelle = ETAPE_4_MESURE_HUMIDITE;
}
}
break;
case ETAPE_4_MESURE_HUMIDITE:
{
statutRCV = xQueueReceive(xQueue_evenements, &evt, pdMS_TO_TICKS(2000));
if (statutRCV == pdTRUE) {
if (evt == EVT_STOP_MARCHE) etape_actuelle = ETAPE_0_VEILLE;
// Note: humidité est lue via xQueueReceive(xQueue_humidite)
}
if (xQueueReceive(xQueue_humidite, &humidite, 0) == pdTRUE) {
derniere_humidite_ui = humidite;
etape_actuelle = ETAPE_5_DECISION;
}
}
break;
case ETAPE_5_DECISION:
{
if (humidite > seuil_humidite) {
etape_actuelle = ETAPE_5_SECHAGE_ACTIF;
xTimerStart(xTimer_sechage, 0);
} else {
etape_actuelle = ETAPE_6_TRANSFERT;
}
// Check for stop even here (non-blocking)
if (xQueueReceive(xQueue_evenements, &evt, 0) == pdTRUE && evt == EVT_STOP_MARCHE)
etape_actuelle = ETAPE_0_VEILLE;
}
break;
case ETAPE_5_SECHAGE_ACTIF:
{
static bool actions_envoyees = false;
if (!actions_envoyees) {
commande_actionneur_t c1 = CMD_VENTILATEUR_ON, c2 = CMD_CHAUFFAGE_ON, c3 = CMD_VIS_ON;
xQueueSend(xQueue_actionneurs, &c1, 0);
xQueueSend(xQueue_actionneurs, &c2, 0);
xQueueSend(xQueue_actionneurs, &c3, 0);
actions_envoyees = true;
}
statutRCV = xQueueReceive(xQueue_evenements, &evt, pdMS_TO_TICKS(100));
if (statutRCV == pdTRUE) {
if (evt == EVT_STOP_MARCHE) {
actions_envoyees = false;
xTimerStop(xTimer_sechage, 0);
etape_actuelle = ETAPE_0_VEILLE;
}
else if (evt == EVT_T5A_HUMIDITE_OK) {
actions_envoyees = false;
xTimerStop(xTimer_sechage, 0);
commande_actionneur_t c = CMD_FIN_SECHAGE;
xQueueSend(xQueue_actionneurs, &c, 0);
etape_actuelle = ETAPE_6_TRANSFERT;
}
else if (evt == EVT_T5B_SURCHAUFFE || evt == EVT_T5C_TIMEOUT) {
actions_envoyees = false;
etape_actuelle = ETAPE_9_SECURITE;
}
}
if (xQueueReceive(xQueue_humidite, &humidite, 0) == pdTRUE) {
derniere_humidite_ui = humidite;
if (humidite <= seuil_humidite)
envoyer_evenement(EVT_T5A_HUMIDITE_OK);
}
}
break;
case ETAPE_6_TRANSFERT:
{
commande_actionneur_t cmd = CMD_OUVRIR_VANNE;
xQueueSend(xQueue_actionneurs, &cmd, 0);
xTimerStart(xTimer_t4, 0);
statutRCV = xQueueReceive(xQueue_evenements, &evt, portMAX_DELAY);
if (statutRCV == pdTRUE) {
if (evt == EVT_STOP_MARCHE) etape_actuelle = ETAPE_0_VEILLE;
else if (evt == EVT_T6_FIN_TRANSFERT) {
cmd = CMD_FERMER_VANNE;
xQueueSend(xQueue_actionneurs, &cmd, 0);
etape_actuelle = ETAPE_7_PESEE;
}
}
}
break;
case ETAPE_7_PESEE:
{
statutRCV = xQueueReceive(xQueue_evenements, &evt, portMAX_DELAY);
if (statutRCV == pdTRUE) {
if (evt == EVT_STOP_MARCHE) etape_actuelle = ETAPE_0_VEILLE;
else if (evt == EVT_T7_POIDS_STABLE) etape_actuelle = ETAPE_8_FIN_LOT;
}
}
break;
case ETAPE_8_FIN_LOT:
{
commande_actionneur_t cmd = CMD_TOUT_OFF;
xQueueSend(xQueue_actionneurs, &cmd, 0);
statutRCV = xQueueReceive(xQueue_evenements, &evt, portMAX_DELAY);
if (statutRCV == pdTRUE) {
if (evt == EVT_STOP_MARCHE || evt == EVT_T8_IR2_SAC_RETIRE)
etape_actuelle = ETAPE_0_VEILLE;
}
}
break;
case ETAPE_9_SECURITE:
{
activer_arret_urgence();
statutRCV = xQueueReceive(xQueue_evenements, &evt, portMAX_DELAY);
if (statutRCV == pdTRUE && evt == EVT_T9_REARMEMENT)
{
digitalWrite(BROCHE_LED_ERR, LOW);
digitalWrite(BROCHE_BUZZER, LOW);
arret_urgence_actif = false;
etape_actuelle = ETAPE_0_VEILLE;
}
}
break;
}
vTaskDelay(pdMS_TO_TICKS(20));
}
}
/* ============================================================================
TÂCHES DU SYSTÈME D'ACQUISITION ET ACTIONNEURS
============================================================================ */
void tache_Capteurs(void *pvParameters)
{
uint16_t s1, s2, h;
for (;;)
{
uint32_t ms = millis();
if (digitalRead(BROCHE_IR1) == HIGH && (ms - dernier_temps_ir1) > DELAI_ANTI_REBOND)
{
dernier_temps_ir1 = ms;
envoyer_evenement(EVT_T1_IR1_DETECTE);
}
if (digitalRead(BROCHE_IR2) == HIGH && (ms - dernier_temps_ir2) > DELAI_ANTI_REBOND)
{
dernier_temps_ir2 = ms;
envoyer_evenement(EVT_T8_IR2_SAC_RETIRE);
}
static bool dernier_etat_start = HIGH;
bool etat_actuel_start = digitalRead(BROCHE_START);
if (etat_actuel_start == LOW && dernier_etat_start == HIGH && (ms - dernier_temps_start) > 500) {
if (etape_actuelle_ui == ETAPE_0_VEILLE) {
envoyer_evenement(EVT_T0_DEMARRAGE);
Serial.println("[Bouton] Appui détecté : DEMARRAGE");
} else {
envoyer_evenement(EVT_STOP_MARCHE);
Serial.println("[Bouton] Appui détecté : ARRET");
}
dernier_temps_start = ms;
}
dernier_etat_start = etat_actuel_start;
if (digitalRead(BROCHE_RESET) == LOW)
envoyer_evenement(EVT_T9_REARMEMENT);
// Lecture des potentiomètres de vitesse (0-4095 -> 0-255)
vitesse_tamis_val = map(analogRead(BROCHE_POT_TAMIS), 0, 4095, 0, 255);
vitesse_vis_val = map(analogRead(BROCHE_POT_VIS), 0, 4095, 0, 255);
s1 = analogRead(BROCHE_SOIL1);
s2 = analogRead(BROCHE_SOIL2);
h = (s1 + s2) / 2;
xQueueOverwrite(xQueue_humidite, &h);
derniere_temperature_ui = analogRead(BROCHE_TEMP);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void tache_Actionneurs(void *pvParameters)
{
commande_actionneur_t cmd;
for (;;)
{
if (xQueueReceive(xQueue_actionneurs, &cmd, portMAX_DELAY) == pdTRUE)
{
switch (cmd)
{
case CMD_TOUT_OFF:
case CMD_FIN_SECHAGE:
analogWrite(BROCHE_MOTEUR_TAMIS, 0);
digitalWrite(BROCHE_VENTILATEUR, LOW);
digitalWrite(BROCHE_RESISTANCE, LOW);
analogWrite(BROCHE_VIS_SANS_FIN, 0);
servoTremie.write(0); // Fermé
servoVanne.write(0); // Fermé
break;
case CMD_TAMIS_ON:
analogWrite(BROCHE_MOTEUR_TAMIS, vitesse_tamis_val);
break;
case CMD_TAMIS_OFF:
analogWrite(BROCHE_MOTEUR_TAMIS, 0);
break;
case CMD_VENTILATEUR_ON:
digitalWrite(BROCHE_VENTILATEUR, HIGH);
break;
case CMD_VENTILATEUR_OFF:
digitalWrite(BROCHE_VENTILATEUR, LOW);
break;
case CMD_OUVRIR_TREMIE:
servoTremie.write(90); // Angle d'ouverture
break;
case CMD_FERMER_TREMIE:
servoTremie.write(0);
break;
case CMD_OUVRIR_VANNE:
servoVanne.write(90);
break;
case CMD_FERMER_VANNE:
servoVanne.write(0);
break;
case CMD_CHAUFFAGE_ON:
digitalWrite(BROCHE_RESISTANCE, HIGH);
break;
case CMD_CHAUFFAGE_OFF:
digitalWrite(BROCHE_RESISTANCE, LOW);
break;
case CMD_VIS_ON:
analogWrite(BROCHE_VIS_SANS_FIN, vitesse_vis_val);
break;
case CMD_VIS_OFF:
analogWrite(BROCHE_VIS_SANS_FIN, 0);
break;
}
}
}
}
void tache_Pesee(void *pvParameters)
{
int32_t p_courant = 0;
for (;;)
{
// En vrai: utiliser scale.get_units(), ici c'est de l'aléatoire simulé
p_courant = (etape_actuelle_ui == ETAPE_7_PESEE) ? random(poids_cible - 5, poids_cible + 5) : 0;
xQueueOverwrite(xQueue_poids, &p_courant);
dernier_poids_ui = p_courant;
if (mode_automatique && etape_actuelle_ui == ETAPE_7_PESEE && p_courant >= poids_cible)
{
envoyer_evenement(EVT_T7_POIDS_STABLE);
}
vTaskDelay(pdMS_TO_TICKS(200));
}
}
void tache_Securite(void *pvParameters)
{
for (;;)
{
uint16_t temp = analogRead(BROCHE_TEMP);
if (temp > seuil_temperature && !arret_urgence_actif)
{
envoyer_evenement(EVT_T5B_SURCHAUFFE);
activer_arret_urgence(); // sécurité absolue même en manuel
}
if (digitalRead(BROCHE_URGENCE) == LOW && !arret_urgence_actif)
{
envoyer_evenement(EVT_ARRET_URGENCE);
activer_arret_urgence();
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
/* ============================================================================
TÂCHES AFFICHAGE LCD & COMMUNICATION MQTT
============================================================================ */
void tache_LCD(void *pvParameters)
{
for (;;)
{
lcd.setCursor(0, 0);
if (!mode_automatique)
{
lcd.print("! MODE MANUEL ! ");
}
else
{
switch (etape_actuelle_ui)
{
case ETAPE_0_VEILLE:
lcd.print("1. En Veille ");
break;
case ETAPE_1_VERIF_SOJA:
lcd.print("2. Verif Soja ");
break;
case ETAPE_2_ALIMENTATION:
lcd.print("2. Alimentation ");
break;
case ETAPE_3_NETTOYAGE:
lcd.print("3. Nettoyage.. ");
break;
case ETAPE_4_MESURE_HUMIDITE:
lcd.print("4. Analyse Hum. ");
break;
case ETAPE_5_DECISION:
lcd.print("5. Decision... ");
break;
case ETAPE_5_SECHAGE_ACTIF:
lcd.print("5. Sechage Actif");
break;
case ETAPE_6_TRANSFERT:
lcd.print("6. Transfert ");
break;
case ETAPE_7_PESEE:
lcd.print("7. Pesee grains ");
break;
case ETAPE_8_FIN_LOT:
lcd.print("8. Fin! Sac pret");
break;
case ETAPE_9_SECURITE:
lcd.print("! ALARME SECUR !");
break;
}
}
lcd.setCursor(0, 1);
lcd.printf("H:%02d%% | P:%04dg", derniere_humidite_ui, dernier_poids_ui);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void tache_MQTT(void *pvParameters)
{
unsigned long timer_telemetrie = 0;
for (;;)
{
bool wasConnected = client.connected();
verifier_reconnexion_mqtt();
// Si on vient de se reconnecter, on vide la SD vers le cloud
if (!wasConnected && client.connected()) {
synchroniser_donnees_sd();
}
client.loop();
unsigned long now = millis();
// 1. Télémétrie en direct (Toutes les 5 secondes)
if (now - timer_telemetrie > 5000)
{
timer_telemetrie = now;
StaticJsonDocument<256> docNode;
docNode["unitId"] = unit_id;
docNode["mode"] = mode_automatique ? "auto" : "manuel";
docNode["step"] = (int)etape_actuelle_ui;
docNode["humidity"] = derniere_humidite_ui;
docNode["temperature"] = derniere_temperature_ui;
docNode["weight"] = dernier_poids_ui;
docNode["alarm"] = arret_urgence_actif;
String payloadTel;
serializeJson(docNode, payloadTel);
String topicTel = "smart-soja/telemetry/" + unit_id;
if (client.connected()) {
client.publish(topicTel.c_str(), payloadTel.c_str());
} else {
sauvegarder_donnee_sd(topicTel, payloadTel);
}
}
// 2. Traçabilité complète du lot (Seulement en fin de lot auto)
if (mode_automatique && etape_actuelle_ui == ETAPE_8_FIN_LOT)
{
StaticJsonDocument<256> docLot;
docLot["unitId"] = unit_id;
docLot["status"] = "TERMINE";
docLot["humidity_final"] = derniere_humidite_ui;
docLot["weight_final"] = dernier_poids_ui;
docLot["temperature_max"] = derniere_temperature_ui;
docLot["target_weight"] = poids_cible;
String payloadLot;
serializeJson(docLot, payloadLot);
String topicLog = "smart-soja/traceabilite/" + unit_id;
if (client.connected()) {
client.publish(topicLog.c_str(), payloadLot.c_str());
} else {
sauvegarder_donnee_sd(topicLog, payloadLot);
}
Serial.println("[MQTT] Donnée de traçabilité traitée (Cloud ou SD) !");
vTaskDelay(pdMS_TO_TICKS(5000)); // Éviter l'envoi en boucle
}
vTaskDelay(pdMS_TO_TICKS(100)); // Pour plus de réactivité de reception MQTT
}
}