Connexion iot coupee par lcd

Bonjour a tous ,j’espére avoir mis sur le bon post c’est mon 2eme projet à 70ans mon premier est la régulation des ventilo additionnels RGB et PWM de mon système de refroidissement pour mon ONDULEUR VICTRON QUADRO plus . Mon 2ème projet c’est celui que j’ai un problème , c’est un contrôle de niveau cuve FUEL aidé je l’avoue avec des IA , je comprends à peu prés la logique , mais j’ai un soucis… Voilà je suis sur IOT ARDUINO CLOUD , j’ai tout qui fonctionne, peut-être des améliorations , ou condensation . Mon IOT est à 600km et je voudrais même sur place mettre en veilles (extinction) mes Leds de niveaux et mon écran LCD I2C . J’ai bien créer 2 Buttons différents mais lorsque je simule en déconnectant le+5V des Leds ou de l’écran ….Plus de DASHBOARD et je pense une déconnection du WIFI et CLOUD .. .J’avoue je patauge dans la semoule . Question y a t-il une solution simple car mon code est déjà compliqué ……Merci d’avance , comme dit au début c’est la première fois que je demande l’aide ‘‘‘HUMAINE””. et que j’utilise le site ,alors si je réponds pas de suite ne m’en voulez pas… Pas d’urgence .. Passez tous de bonnes fêtes… Vincenzo M

/* ESP IOT URSULA – VERSION 1.9.8-upgraded – 21/12/2025
   =============================================================
   - Conso détectée uniquement après 3 mesures stables
   - Plus d'incrémentation fantôme dans les buckets
   - KY040 corrigé, écran stable, code complet
   ============================================================= */

#include <Arduino.h>
#include <Wire.h>
#include <DHT.h>
#include <EEPROM.h>
#include <WiFi.h>
#include <time.h>
#include <esp_task_wdt.h>
#include <algorithm>
#include "thingProperties.h"

const char* ntpServer = "pool.ntp.org";
const long  gmtOffset_sec = 7200;
const int   daylightOffset_sec = 0;

#define DHTPIN  4
#define DHTTYPE DHT11
DHT dht(DHTPIN, DHTTYPE);

#define LED_VERTE1  12
#define LED_VERTE2  14
#define LED_JAUNE1  27
#define LED_JAUNE2  26
#define LED_ORANGE1 25
#define LED_ROUGE1  33
#define LED_ROUGE2  32
#define BUZZER      13
#define LED_WIFI    18
#define BTN_ERASE_EEPROM 0
#define SDA 21
#define SCL 22
#define KY040_CLK 16
#define KY040_DT  17
#define KY040_SW  19
#define WDT_TIMEOUT 30

const int HAUTEUR  = 160;
const int CAPACITE = 3200;
const int LITRES_CM = CAPACITE / HAUTEUR;

#define SEUIL_CONSOMMATION 2.0f
#define SEUIL_MIN_REMPLISSAGE_MANUEL 5.0f
#define DELAI_REMPLISSAGE_MANUEL_MIN 10
#define DELAI_REMPLISSAGE_MAX 120
#define DELAI_REMPLISSAGE_MIN 5
#define CHENILLARD_INTERVAL_MS 200
#define CHENILLARD_ALTERNANCE_MIN 5

#define NB_MESURES 100
#define MIN_MESURES_INITIALISATION 5
#define TRIGGER 15
#define ECHO    2
#define MAX_DISTANCE 160
#define TIMEOUT_US (MAX_DISTANCE * 100)

#define LCD_ADDR 0x27
#define LCD_COLS 20
#define LCD_ROWS 4

#define EEPROM_SIZE 2048
#define ADDR_BUCKET_MAGIC 1050
#define NB_BUCKETS 400
#define ADDR_BUCKETS 1060
#define ADDR_REMPLISSAGE_TIME 2000
#define TEMPS_SORTIE_AUTO_MIN 5
#define BTN_EEPROM_LONG_PRESS 5000

// Commande des relais ALLUMAGE LCD/LEDS
const int RelaisLCD  = 35;
const int RelaisLEDS = 34;
const int BpLCD = 3;
const int BpLEDS = 1;
const int allumageTemps = 30000;
unsigned long allumageTimer = 0;


struct BucketHoraire {
  uint16_t annee;
  uint8_t  mois, jour, heure;
  float    conso_L;
};
BucketHoraire buckets[NB_BUCKETS];
int idxBucket = 0;
const uint32_t bucketMagic = 0xCAFE2025;

float ancien_niveau = -1;
float tableau_mesures[NB_MESURES] = {0};
float tableau_temp[NB_MESURES]   = {0};
float tableau_hum[NB_MESURES]    = {0};
int index_mesure = 0;
int nb_mesures_valides = 0;
bool tableau_plein = false;
bool systeme_initialise = false;
float dcc = 2.05f;

unsigned long lastPing = 0, lastDisplay = 0, lastWifiBlink = 0, lastRouge2Blink = 0, cycleStartTime = 0;
bool showingWifiStatus = true;
const unsigned long CYCLE_DURATION = 60000;
const unsigned long WIFI_BLINK_INTERVAL = 1000;
const unsigned long CLOUD_BLINK_INTERVAL = 100;

int displayMode = 0, currentDisplayMode = -1;
const unsigned long DISPLAY_DURATION = 10000;

float conso_detectee = 0;
bool  flag_conso_detectee = false;
unsigned long debut_affichage_conso = 0;
char date_conso[12], heure_conso[10];

bool alerte_sonde = false;
uint8_t mauvaises_lectures_consecutives = 0;
const uint8_t SEUIL_MAUVAISES_LECTURES = 25;

#define NB_REMPLISSAGES 10
struct Remplissage {
  char  date[12];
  char  heure[10];
  float volumeIntroduit;
};
Remplissage remplissages[NB_REMPLISSAGES];
int idxRemplissage = 0;
int nb_remplissages_reels = 0;
unsigned long lastScrollRemplissage = 0;
int affichageRemplissageIdx = 0;

String lastLine[4] = {"", "", "", ""};

bool inMenu = false;
int menuItem = 0;
const char* menuItems[12] = {
  "Effacer alarmes", "Effacer conso", "Effacer remplis.",
  "Effacer stats", "Remplissage", "Effacer EEPROM",
  "Voir consos horaires", "Voir consos journalieres",
  "Voir consos mensuelles", "Voir consos annuelles",
  "Voir alarmes", "Voir remplissages"
};

int lastCLKState = HIGH;
unsigned long lastRotationTime = 0;
const unsigned long DEBOUNCE_ROTATION = 200;

unsigned long swPressStart = 0;
int swLastState = HIGH;
const unsigned long LONG_PRESS_DURATION = 1500;

bool inRemplissageMode = false;
float volumeReferenceRemplissage = 0.0f;
unsigned long debutRemplissageMode = 0;
bool remplissageDetecte = false;
float volumeMaxDetecte = 0.0f;
int tempsRemplissageMinutes = DELAI_REMPLISSAGE_MANUEL_MIN;

bool reglageTempsActif = false;
int tempsReglageProvisoire = 0;

unsigned long lastChenillardUpdate = 0;
int chenillardPosition = 0;
bool chenillardActif = false;
bool afficherNiveauActuel = true;
unsigned long derniereAlternance = 0;

unsigned long btnEepromPressStart = 0;
int btnEepromLastState = HIGH;

#define ALERTE_ORANGE_PCT      30.0f
#define ALERTE_ROUGE_PCT       20.0f
#define ALERTE_CRITIQUE_PCT    10.0f
#define ALERTE_TRES_CRITIQUE_PCT 5.0f

bool alerte_fuel_orange = false;
bool alerte_fuel_rouge = false;
bool alerte_fuel_critique = false;
bool alerte_fuel_tres_critique = false;

unsigned long lastAlerteBeep = 0, lastAlerteBlink = 0;
bool alerteBlinkState = false;
bool conso_en_cours = false;

// ====================== LCD ======================
#define LCD_BACKLIGHT 0x08
#define En B00000100
#define Rs B00000001

void lcd_send(uint8_t data, uint8_t mode) {
  uint8_t high = data & 0xF0;
  uint8_t low  = (data << 4) & 0xF0;
  auto write4 = [](uint8_t d) -> void {
    Wire.beginTransmission(LCD_ADDR);
    Wire.write(d | LCD_BACKLIGHT);
    Wire.endTransmission();
    Wire.beginTransmission(LCD_ADDR);
    Wire.write(d | En | LCD_BACKLIGHT);
    Wire.endTransmission();
    delayMicroseconds(1);
    Wire.beginTransmission(LCD_ADDR);
    Wire.write((d & ~En) | LCD_BACKLIGHT);
    Wire.endTransmission();
    delayMicroseconds(50);
  };
  write4(high | mode);
  write4(low  | mode);
}

void lcd_init() {
  Wire.begin(SDA, SCL);
  delay(50);
  auto cmd = [](uint8_t c) { delayMicroseconds(c >= 0x10 ? 4500 : 150); lcd_send(c, 0); };
  cmd(0x03); cmd(0x03); cmd(0x03); cmd(0x02);
  lcd_send(0x28, 0);
  lcd_send(0x08 | 0x04, 0);
  lcd_send(0x06, 0);
  lcd_clear();
}

void lcd_clear() { lcd_send(0x01, 0); delay(2); }

void lcd_setCursor(uint8_t c, uint8_t r) {
  const uint8_t offsets[4] = {0x00, 0x40, 0x14, 0x54};
  lcd_send(0x80 | (c + offsets[r]), 0);
}

void lcd_print(const char* s) { while (*s) lcd_send(*s++, Rs); }
void lcd_print(String s) { for (size_t i = 0; i < s.length(); ++i) lcd_send(s[i], Rs); }

void lcd_createChar(uint8_t loc, uint8_t map[]) {
  lcd_send(0x40 | (loc << 3), 0);
  for (int i = 0; i < 8; ++i) lcd_send(map[i], Rs);
}

void updateLine(int line, String text) {
  if (text == lastLine[line]) return;
  lcd_setCursor(0, line);
  lcd_print("                    ");
  lcd_setCursor(0, line);
  lcd_print(text);
  lastLine[line] = text;
}

void displayPreciseBargraph(float pc) {
  int tot = (int)(pc * 100);
  if (tot < 0) tot = 0; else if (tot > 10000) tot = 10000;
  int full = tot / 500;
  int rest = (tot % 500) / 100;
  String bar = "";
  for (int i = 0; i < 20; ++i) {
    if (i < full) bar += char(4);
    else if (i == full && rest > 0) bar += char(rest - 1);
    else bar += ' ';
  }
  updateLine(3, bar);
}

void resetLineCache() { for (auto& l : lastLine) l = ""; }

// ====================== EEPROM & BUCKETS ======================
void effacer_totale_eeprom() {
  EEPROM.begin(EEPROM_SIZE);
  for (int i = 0; i < EEPROM_SIZE; ++i) EEPROM.write(i, 0xFF);
  EEPROM.commit(); EEPROM.end();
}

void chargerTempsRemplissage() {
  EEPROM.begin(EEPROM_SIZE);
  int tempsLu; 
  EEPROM.get(ADDR_REMPLISSAGE_TIME, tempsLu);
  if (tempsLu >= DELAI_REMPLISSAGE_MIN && tempsLu <= DELAI_REMPLISSAGE_MAX) {
    tempsRemplissageMinutes = tempsLu;
  } else {
    tempsRemplissageMinutes = DELAI_REMPLISSAGE_MANUEL_MIN;
  }
  EEPROM.end();
}

void sauvegarderTempsRemplissage(int temps) {
  EEPROM.begin(EEPROM_SIZE);
  EEPROM.put(ADDR_REMPLISSAGE_TIME, temps);
  EEPROM.commit(); 
  EEPROM.end();
}

void initBuckets() { memset(buckets, 0, sizeof(buckets)); idxBucket = 0; }

void chargerBuckets() {
  EEPROM.begin(EEPROM_SIZE);
  uint32_t magic; EEPROM.get(ADDR_BUCKET_MAGIC, magic);
  if (magic != bucketMagic) { EEPROM.end(); initBuckets(); sauvegarderBuckets(); return; }
  int addr = ADDR_BUCKETS;
  for (int i = 0; i < NB_BUCKETS; ++i) { EEPROM.get(addr, buckets[i]); addr += sizeof(BucketHoraire); }
  idxBucket = 0;
  for (int i = 0; i < NB_BUCKETS; ++i) if (buckets[i].annee >= 2020) idxBucket = (i + 1) % NB_BUCKETS;
  EEPROM.end();
}

void sauvegarderBuckets() {
  EEPROM.begin(EEPROM_SIZE);
  EEPROM.put(ADDR_BUCKET_MAGIC, bucketMagic);
  int addr = ADDR_BUCKETS;
  for (int i = 0; i < NB_BUCKETS; ++i) { EEPROM.put(addr, buckets[i]); addr += sizeof(BucketHoraire); }
  EEPROM.commit(); EEPROM.end();
}

void forcerBucketCourant() {
  struct tm ti; if (!getLocalTime(&ti)) return;
  uint16_t a = ti.tm_year + 1900;
  uint8_t  m = ti.tm_mon + 1, j = ti.tm_mday, h = ti.tm_hour;
  int cur = (idxBucket == 0 ? NB_BUCKETS - 1 : idxBucket - 1);
  if (buckets[cur].annee != a || buckets[cur].mois != m || buckets[cur].jour != j || buckets[cur].heure != h) {
    buckets[idxBucket] = {a, m, j, h, 0.0f};
    idxBucket = (idxBucket + 1) % NB_BUCKETS;
    sauvegarderBuckets();
  }
}

void ajouterConsoDansBucket(float q) {
  forcerBucketCourant();
  int cur = (idxBucket == 0 ? NB_BUCKETS - 1 : idxBucket - 1);
  buckets[cur].conso_L += q;
  sauvegarderBuckets();
}

float getConso(bool horaire = true, bool jour = false, bool mois = false, bool annee = false) {
  forcerBucketCourant();
  struct tm ti; if (!getLocalTime(&ti)) return 0.0f;
  uint16_t a = ti.tm_year + 1900;
  uint8_t  m = ti.tm_mon + 1, j = ti.tm_mday, h = ti.tm_hour;
  float total = 0.0f;
  for (int i = 0; i < NB_BUCKETS; ++i) {
    if ((horaire && buckets[i].annee == a && buckets[i].mois == m && buckets[i].jour == j && buckets[i].heure == h) ||
        (jour   && buckets[i].annee == a && buckets[i].mois == m && buckets[i].jour == j) ||
        (mois   && buckets[i].annee == a && buckets[i].mois == m) ||
        (annee  && buckets[i].annee == a))
      total += buckets[i].conso_L;
  }
  return total;
}

// ====================== CHENILLARD LED ======================
void updateLEDs(float v) {
  if (chenillardActif && !afficherNiveauActuel) return;
  digitalWrite(LED_ROUGE2, v >= 400 ? HIGH : ((millis() - lastRouge2Blink >= 500) ? (lastRouge2Blink = millis(), !digitalRead(LED_ROUGE2)) : digitalRead(LED_ROUGE2)));
  digitalWrite(LED_ROUGE1, v >= 500 ? HIGH : LOW);
  digitalWrite(LED_ORANGE1, v >= 700 ? HIGH : LOW);
  digitalWrite(LED_JAUNE2, v >= 800 ? HIGH : LOW);
  digitalWrite(LED_JAUNE1, v >= 900 ? HIGH : LOW);
  digitalWrite(LED_VERTE2, v >= 1000 ? HIGH : LOW);
  digitalWrite(LED_VERTE1, v >= 1200 ? HIGH : LOW);
}

void updateChenillardLED() {
  if (!chenillardActif) return;
  unsigned long now = millis();
  if (now - derniereAlternance >= CHENILLARD_ALTERNANCE_MIN * 60000UL) {
    afficherNiveauActuel = !afficherNiveauActuel;
    derniereAlternance = now;
    if (afficherNiveauActuel) { updateLEDs(moyenne(tableau_mesures)); return; }
  }
  if (afficherNiveauActuel) return;
  if (now - lastChenillardUpdate >= CHENILLARD_INTERVAL_MS) {
    lastChenillardUpdate = now;
    digitalWrite(LED_VERTE1, LOW); digitalWrite(LED_VERTE2, LOW);
    digitalWrite(LED_JAUNE1, LOW); digitalWrite(LED_JAUNE2, LOW);
    digitalWrite(LED_ORANGE1, LOW); digitalWrite(LED_ROUGE1, LOW); digitalWrite(LED_ROUGE2, LOW);
    switch (chenillardPosition) {
      case 0: digitalWrite(LED_VERTE1, HIGH); break;
      case 1: digitalWrite(LED_VERTE2, HIGH); break;
      case 2: digitalWrite(LED_JAUNE1, HIGH); break;
      case 3: digitalWrite(LED_JAUNE2, HIGH); break;
      case 4: digitalWrite(LED_ORANGE1, HIGH); break;
      case 5: digitalWrite(LED_ROUGE1, HIGH); break;
      case 6: digitalWrite(LED_ROUGE2, HIGH); break;
    }
    chenillardPosition = (chenillardPosition + 1) % 7;
  }
}

void demarrerChenillard() {
  chenillardActif = true; chenillardPosition = 0; lastChenillardUpdate = 0;
  derniereAlternance = millis(); afficherNiveauActuel = false;
}

void arreterChenillard() {
  chenillardActif = false; updateLEDs(moyenne(tableau_mesures));
}

// ====================== ÉCRAN REGLAGE TEMPS 4 LIGNES ======================
void ecran_reglage_temps() {
  unsigned long lastRefresh = 0;
  unsigned long startTime = millis();
  int localCLKState = HIGH;
  int localSWState = HIGH;
  unsigned long localSWPress = 0;

  while (true) {
    esp_task_wdt_reset();
    ArduinoCloud.update();
    if (millis() - startTime > 30000) return;

    if (millis() - lastRefresh >= 150) {
      lastRefresh = millis();
      lcd_clear(); resetLineCache();

      lcd_setCursor(6, 0); lcd_print("CONFIG TEMPS");

      char bufVal[10];
      snprintf(bufVal, 10, "%3d min", tempsReglageProvisoire);
      lcd_setCursor(20 - strlen(bufVal), 1);
      lcd_print(bufVal);

      lcd_setCursor(2, 2); lcd_print("Plage: 5-120 min");
      lcd_setCursor(1, 3); lcd_print("KY040: +/-  OK: val");
    }

    int clk = digitalRead(KY040_CLK);
    int dt  = digitalRead(KY040_DT);
    int sw  = digitalRead(KY040_SW);

    if (clk != localCLKState && clk == LOW) {
      if (millis() - lastRotationTime > DEBOUNCE_ROTATION) {
        if (dt == LOW) tempsReglageProvisoire = min(tempsReglageProvisoire + 5, DELAI_REMPLISSAGE_MAX);
        else tempsReglageProvisoire = max(tempsReglageProvisoire - 5, DELAI_REMPLISSAGE_MIN);
        lastRotationTime = millis();
        beep();
      }
    }
    localCLKState = clk;

    if (sw == LOW && localSWState == HIGH) localSWPress = millis();
    else if (sw == HIGH && localSWState == LOW) {
      unsigned long dur = millis() - localSWPress;
      if (dur > 50 && dur < LONG_PRESS_DURATION) {
        tempsRemplissageMinutes = tempsReglageProvisoire;
        sauvegarderTempsRemplissage(tempsRemplissageMinutes);
        lcd_clear(); resetLineCache();
        lcd_setCursor(4, 1); lcd_print("TEMPS ENREGISTRE");
        char buf[21];
        snprintf(buf, 21, "%d minutes", tempsRemplissageMinutes);
        lcd_setCursor(4, 2); lcd_print(buf);
        beep_long();
        delay(2000);
        return;
      }
    }
    localSWState = sw;
    delay(10);
  }
}

// ====================== FONCTIONS D'ÉCRAN ======================
bool confirmer_action(const char* titre) {
  int choix = 0;
  const char* options[3] = {"Oui", "Non", "Sortie"};
  unsigned long lastRefresh = 0;
  unsigned long startTime = millis();
  int localCLKState = HIGH;
  int localSWState = HIGH;
  unsigned long localSWPress = 0;

  while (true) {
    esp_task_wdt_reset();
    ArduinoCloud.update();
    if (millis() - startTime > TEMPS_SORTIE_AUTO_MIN * 60000UL) return false;
    if (millis() - lastRefresh >= 200) {
      lastRefresh = millis();
      lcd_clear(); resetLineCache();
      lcd_setCursor(1, 0); lcd_print(titre);
      for (int i = 0; i < 3; ++i) {
        lcd_setCursor(0, 1 + i);
        lcd_print(i == choix ? ">" : " ");
        lcd_print(options[i]);
      }
      lcd_setCursor(0, 3); lcd_print("Appui long = Sortie");
    }
    int clk = digitalRead(KY040_CLK);
    int dt  = digitalRead(KY040_DT);
    int sw  = digitalRead(KY040_SW);
    if (clk != localCLKState && clk == LOW) {
      if (millis() - lastRotationTime > DEBOUNCE_ROTATION) {
        choix = (choix + (dt == LOW ? 1 : -1) + 3) % 3;
        lastRotationTime = millis();
      }
    }
    localCLKState = clk;
    if (sw == LOW && localSWState == HIGH) localSWPress = millis();
    else if (sw == HIGH && localSWState == LOW) {
      unsigned long dur = millis() - localSWPress;
      if (dur > 50 && dur < LONG_PRESS_DURATION) return (choix == 0);
      else if (dur >= LONG_PRESS_DURATION) return false;
    }
    localSWState = sw;
    delay(100);
  }
}

void afficher_fuel_opti(float vol, float pc, float dcorr, float hfuel, const char* titre = "FUEL RESTANT") {
  if (currentDisplayMode != 0) {
    lcd_clear(); resetLineCache();
    lcd_setCursor((LCD_COLS - strlen(titre)) / 2, 0);
    lcd_print(titre);
    currentDisplayMode = 0;
  }
  char buf[21];
  snprintf(buf, 21, "%.2f L %.2f %%", vol, pc); updateLine(1, buf);
  snprintf(buf, 21, "%.3f cm %.2f cm", dcorr, hfuel); updateLine(2, buf);
  displayPreciseBargraph(pc);
}

void afficher_remplissage_en_cours() {
  float vol = moyenne(tableau_mesures);
  float pc  = vol * 100.0f / CAPACITE;
  float dc  = distance;
  float hf  = HAUTEUR - (dc - dcc);
  if (currentDisplayMode != 0) {
    lcd_clear(); resetLineCache();
    lcd_setCursor(2, 0); lcd_print("REMPLISSAGE EN COURS");
    currentDisplayMode = 0;
  }
  char buf[21];
  snprintf(buf, 21, "%.2f L %.2f %%", vol, pc); updateLine(1, buf);
  snprintf(buf, 21, "%.3f cm %.2f cm", dc, hf); updateLine(2, buf);
  displayPreciseBargraph(pc);
}

void afficher_infos_capteurs_opti() {
  if (currentDisplayMode != 1) { lcd_clear(); resetLineCache(); currentDisplayMode = 1; }
  char d[12], h[10]; lire_date_heure(d, h);
  float t = moyenne(tableau_temp), hum = moyenne(tableau_hum);
  char buf[21];
  snprintf(buf, 21, "%s %s", d, h); updateLine(0, buf);
  snprintf(buf, 21, "Temp: %.1f C", t); updateLine(1, buf);
  snprintf(buf, 21, "Hum : %.1f %%", hum); updateLine(2, buf);
  snprintf(buf, 21, " WiFi:%s Cloud:%s", WiFi.status() == WL_CONNECTED ? "OK" : "NO", ArduinoCloud.connected() ? "OK" : "NO");
  updateLine(3, buf);
}

void afficher_conso_opti() {
  if (currentDisplayMode != 2) { lcd_clear(); resetLineCache(); lcd_setCursor(2, 0); lcd_print("CONSO CALENDAIRE"); currentDisplayMode = 2; }
  char buf[21];
  snprintf(buf, 21, "Heure : %.2f L", getConso(true)); updateLine(1, buf);
  snprintf(buf, 21, "Jour  : %.2f L", getConso(false, true)); updateLine(2, buf);
  snprintf(buf, 21, "Mois  : %.2f L", getConso(false, false, true)); updateLine(3, buf);
}

void afficher_conso_annuelle_opti() {
  if (currentDisplayMode != 3) { lcd_clear(); resetLineCache(); lcd_setCursor(2, 0); lcd_print(" CONSO ANNUELLE"); currentDisplayMode = 3; }
  float ca = getConso(false, false, false, true);
  char buf[21];
  snprintf(buf, 21, "%.2f L/an", ca); updateLine(2, buf);
  snprintf(buf, 21, "%.3f m3/an", ca / 1000.0f); updateLine(3, buf);
}

void ecran_consommation_opti() {
  if (currentDisplayMode != 9) { lcd_clear(); resetLineCache(); lcd_setCursor(4, 0); lcd_print("CONSOMMATION"); currentDisplayMode = 9; }
  char buf[21];
  snprintf(buf, 21, "Date : %s", date_conso); updateLine(1, buf);
  snprintf(buf, 21, "Heure: %s", heure_conso); updateLine(2, buf);
  snprintf(buf, 21, "Conso: -%.2f L", conso_detectee); updateLine(3, buf);
}

void displayMenu() {
  lcd_clear(); resetLineCache();
  lcd_setCursor(5, 0); lcd_print("*** MENU ***");
  lcd_setCursor(0, 1); lcd_print(">"); lcd_print(menuItems[menuItem]);
  lcd_setCursor(0, 3); lcd_print("Tourner: select Bref: OK");
}

// ====================== LED / WIFI / EEPROM BUTTONS ======================
void gerer_led_wifi_cloud() {
  if (millis() - cycleStartTime >= CYCLE_DURATION) {
    cycleStartTime = millis(); showingWifiStatus = !showingWifiStatus; digitalWrite(LED_WIFI, LOW);
  }
  bool connected = showingWifiStatus ? (WiFi.status() == WL_CONNECTED) : ArduinoCloud.connected();
  unsigned long interval = showingWifiStatus ? WIFI_BLINK_INTERVAL : CLOUD_BLINK_INTERVAL;
  if (connected && millis() - lastWifiBlink >= interval) { lastWifiBlink = millis(); digitalWrite(LED_WIFI, !digitalRead(LED_WIFI)); }
  else if (!connected) digitalWrite(LED_WIFI, LOW);
}

void gerer_bouton_eeprom() {
  int state = digitalRead(BTN_ERASE_EEPROM);
  if (state != btnEepromLastState) {
    if (state == LOW) btnEepromPressStart = millis();
    btnEepromLastState = state;
  }
  if (state == LOW) {
    unsigned long dur = millis() - btnEepromPressStart;
    if (dur >= BTN_EEPROM_LONG_PRESS) {
      lcd_clear(); lcd_setCursor(1,0); lcd_print("EFFACEMENT TOTAL");
      lcd_setCursor(3,1); lcd_print("EEPROM EN COURS");
      lcd_setCursor(5,2); lcd_print("PATIENTEZ...");
      for (int i = 0; i < 3; ++i) { beep_long(); delay(200); }
      effacer_totale_eeprom();
      lcd_clear(); lcd_setCursor(2,1); lcd_print("EEPROM EFFACEE");
      lcd_setCursor(3,2); lcd_print("REDEMARRAGE...");
      delay(2000); ESP.restart();
    } else if (dur >= 1000 && dur % 1000 < 100) {
      lcd_clear(); lcd_setCursor(1,0); lcd_print("MAINTENIR POUR");
      lcd_setCursor(1,1); lcd_print("EFFACER EEPROM");
      char buf[21]; snprintf(buf, 21, "%d secondes...", (BTN_EEPROM_LONG_PRESS - dur) / 1000 + 1);
      lcd_setCursor(5,2); lcd_print(buf); beep();
    }
  }
}

// ====================== MODE REMPLISSAGE AVEC REGLAGE TEMPS ======================
void mode_remplissage_avec_reglage_temps() {
  tempsReglageProvisoire = tempsRemplissageMinutes;
  reglageTempsActif = true;
  ecran_reglage_temps();
  reglageTempsActif = false;

  volumeReferenceRemplissage = moyenne(tableau_mesures);
  volumeMaxDetecte = 0.0f;
  debutRemplissageMode = millis();
  remplissageDetecte = false;
  currentDisplayMode = -1;
  resetLineCache();
  demarrerChenillard();

  int localCLKState = HIGH;
  int localSWState = HIGH;
  unsigned long localSWPress = 0;

  while (true) {
    esp_task_wdt_reset(); ArduinoCloud.update();
    gerer_led_wifi_cloud(); updateChenillardLED();

    if (millis() - lastPing >= 300) { lastPing = millis(); mesure_et_cloud(); }

    afficher_remplissage_en_cours();

    unsigned long tempsEcoule = millis() - debutRemplissageMode;
    unsigned long tempsRestant = (tempsRemplissageMinutes * 60000UL) - tempsEcoule;
    int minutesRestantes = max(0, (int)(tempsRestant / 60000UL));
    char buf[21];
    snprintf(buf, 21, "%.2f L %.2f %%", moyenne(tableau_mesures), moyenne(tableau_mesures) * 100.0f / CAPACITE);
    updateLine(1, buf);
    char buf2[8];
    snprintf(buf2, 8, "%3d", minutesRestantes);
    lcd_setCursor(17, 1); lcd_print(buf2);

    float volActuel = moyenne(tableau_mesures);
    float gain = volActuel - volumeReferenceRemplissage;
    if (gain > volumeMaxDetecte) volumeMaxDetecte = gain;
    if (gain >= SEUIL_MIN_REMPLISSAGE_MANUEL) remplissageDetecte = true;

    int clk = digitalRead(KY040_CLK);
    int sw  = digitalRead(KY040_SW);
    if (clk != localCLKState && clk == LOW) localCLKState = clk;
    if (sw == LOW && localSWState == HIGH) localSWPress = millis();
    else if (sw == HIGH && localSWState == LOW) {
      unsigned long dur = millis() - localSWPress;
      if (dur > 50 && dur < LONG_PRESS_DURATION) {
        if (gain >= SEUIL_MIN_REMPLISSAGE_MANUEL) {
          if (confirmer_action("Valider remplissage ?")) {
            char d[12], h[10]; lire_date_heure(d, h);
            strcpy(remplissages[idxRemplissage].date, d);
            strcpy(remplissages[idxRemplissage].heure, h);
            remplissages[idxRemplissage].volumeIntroduit = gain;
            idxRemplissage = (idxRemplissage + 1) % NB_REMPLISSAGES;
            if (nb_remplissages_reels < NB_REMPLISSAGES) ++nb_remplissages_reels;
            beep_long();
            lcd_clear(); resetLineCache();
            lcd_setCursor(2,1); lcd_print("REMPLISSAGE");
            lcd_setCursor(2,2); lcd_print("ENREGISTRE !");
            delay(2000);
          }
        }
        break;
      }
    }
    localSWState = sw;

    if (millis() - debutRemplissageMode >= tempsRemplissageMinutes * 60000UL) {
      if (!remplissageDetecte || volumeMaxDetecte < SEUIL_MIN_REMPLISSAGE_MANUEL) {
        lcd_clear(); resetLineCache();
        lcd_setCursor(1,1); lcd_print("VOLUME TROP FAIBLE");
        lcd_setCursor(3,2); lcd_print("MIN: 5L");
        beep_long(); delay(3000);
      }
      break;
    }
    delay(10);
  }

  inRemplissageMode = false;
  arreterChenillard();
  ancien_niveau = moyenne(tableau_mesures);
  currentDisplayMode = -1;
  resetLineCache();
}

// ====================== SETUP ======================
void setup() {
  Serial.begin(115200);
// Allumage

  pinMode(3,OUTPUT);
  pinMode(BpLCD, INPUT_PULLUP);
   digitalWrite(RelaisLCD, LOW);
   
  
  pinMode(1,OUTPUT);
  pinMode(BpLEDS, INPUT_PULLUP);   
   digitalWrite(RelaisLEDS, LOW);
  
  // ALLUMAGE
  pinMode(BUZZER, OUTPUT); digitalWrite(BUZZER, LOW);
  pinMode(LED_WIFI, OUTPUT); pinMode(TRIGGER, OUTPUT); pinMode(ECHO, INPUT);
  digitalWrite(TRIGGER, LOW);
  pinMode(KY040_CLK, INPUT_PULLUP); pinMode(KY040_DT, INPUT_PULLUP); pinMode(KY040_SW, INPUT_PULLUP);
  pinMode(BTN_ERASE_EEPROM, INPUT_PULLUP);
  for (int p : {LED_VERTE1,LED_VERTE2,LED_JAUNE1,LED_JAUNE2,LED_ORANGE1,LED_ROUGE1,LED_ROUGE2}) pinMode(p, OUTPUT);
  esp_task_wdt_init(WDT_TIMEOUT, true); esp_task_wdt_add(NULL);
  Wire.begin(SDA, SCL); lcd_init();
  byte bars[5][8] = {
    {0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10},
    {0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18},
    {0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C},
    {0x1E,0x1E,0x1E,0x1E,0x1E,0x1E,0x1E,0x1E},
    {0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F}
  };
  for (int i = 0; i < 5; ++i) lcd_createChar(i, bars[i]);
  lcd_clear(); lcd_setCursor(3,0); lcd_print("CUVE FUEL 1.9.8");
  lcd_setCursor(3,1); lcd_print("INITIALISATION...");
  lcd_setCursor(3,2); lcd_print("ATTENTE MESURES");
  lcd_setCursor(2,3); lcd_print("VINCENT MAGLIOLO");
  chargerTempsRemplissage();
  dht.begin();
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
  WiFi.begin();
  unsigned long t = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - t < 15000) { esp_task_wdt_reset(); delay(100); }
  chargerBuckets();
  initProperties(); ArduinoCloud.begin(ArduinoIoTPreferredConnection);
  setDebugMessageLevel(4); ArduinoCloud.printDebugInfo();
  swLastState = digitalRead(KY040_SW);
  delay(2000); lcd_clear(); currentDisplayMode = -1;
  cycleStartTime = millis(); showingWifiStatus = true;
}

// ====================== MESURE ET CLOUD ======================
void mesure_et_cloud() {
  float temp = dht.readTemperature(), hum = dht.readHumidity();
  if (isnan(temp)) temp = 20.0f; if (isnan(hum)) hum = 50.0f;
  temperature = temp; humidity = hum;
  tableau_temp[index_mesure] = temp; tableau_hum[index_mesure] = hum;
  unsigned int dcm = 0;
  for (int i = 0; i < 3 && !dcm; ++i) dcm = lire_distance_median();
  bool def = (dcm == 0 || dcm > HAUTEUR || dcm < 5);
  if (def) {
    if (++mauvaises_lectures_consecutives >= SEUIL_MAUVAISES_LECTURES && !alerte_sonde) { alerte_sonde = true; beep_long(); }
  } else {
    mauvaises_lectures_consecutives = 0;
    if (alerte_sonde) alerte_sonde = false;
  }
  if (alerte_sonde) return;
  float corr = getCorrectionTempHum(moyenne(tableau_temp), moyenne(tableau_hum));
  float dcorr = dcm + corr;
  distance = dcorr + dcc;
  float hfuel = HAUTEUR - dcorr;
  float vol = hfuel * LITRES_CM;
  if (vol > 0 && vol <= CAPACITE) {
    fuel = vol;
    tableau_mesures[index_mesure] = vol;
    beep();
    if (++index_mesure >= NB_MESURES) { index_mesure = 0; tableau_plein = true; }
    if (++nb_mesures_valides >= MIN_MESURES_INITIALISATION && !systeme_initialise) {
      float init = moyenne(tableau_mesures);
      for (int i = 0; i < NB_MESURES; ++i) tableau_mesures[i] = init;
      ancien_niveau = init; systeme_initialise = true;
    }
  }
  if (systeme_initialise && tableau_plein && index_mesure == 0) {
    float vol_moy = moyenne(tableau_mesures);
    pourcentage = vol_moy * 100.0f / CAPACITE;

    // 🔒 Attendre au moins 3 mesures stables avant de détecter une conso
    static int mesures_stables = 0;
    float diff = vol_moy - ancien_niveau;

    if (abs(diff) <= SEUIL_CONSOMMATION) {
      mesures_stables++;
    } else {
      mesures_stables = 0;
    }

    if (diff < -SEUIL_CONSOMMATION && mesures_stables >= 3) {
      float delta_conso = -diff;
      if (!conso_en_cours) {
        conso_detectee = delta_conso;
        lire_date_heure(date_conso, heure_conso);
        conso_en_cours = true;
      } else {
        conso_detectee += delta_conso;
      }
      ajouterConsoDansBucket(delta_conso);
      flag_conso_detectee = true;
      debut_affichage_conso = millis();
      beep();
      ancien_niveau = vol_moy;
      mesures_stables = 0;
    } else if (conso_en_cours && abs(diff) <= SEUIL_CONSOMMATION) {
      conso_en_cours = false;
    }
    tableau_plein = false;
  }
  ch = getConso(true);
  cj = getConso(false, true);
  float pc = pourcentage;
  alerte_fuel_orange      = (pc <= ALERTE_ORANGE_PCT && pc > ALERTE_ROUGE_PCT);
  alerte_fuel_rouge       = (pc <= ALERTE_ROUGE_PCT && pc > ALERTE_CRITIQUE_PCT);
  alerte_fuel_critique    = (pc <= ALERTE_CRITIQUE_PCT && pc > ALERTE_TRES_CRITIQUE_PCT);
  alerte_fuel_tres_critique = (pc <= ALERTE_TRES_CRITIQUE_PCT);
}

// ====================== LOOP ======================
void loop() {
  esp_task_wdt_reset();
  ArduinoCloud.update();
  forcerBucketCourant();
  gerer_bouton_eeprom();

// ALLUMAGE EXTINCTION DU LCD 
// Si le bouton 35 avec RELAIS 03 est pressé et l'allumage n'est pas en fonction
	if (digitalRead(BpLCD) == LOW && allumageTimer == 0)
	{
		delay(250);     // "anti-rebonds" du bouton
		digitalWrite(RelaisLCD, HIGH);     // Allumer le LCD
		allumageTimer = millis();     // Démarrage du chrono
	} 

	// Si allumage en fonction et allumageTemps échu
	if (allumageTimer != 0 && (millis()-allumageTimer >= allumageTemps))
	{
		digitalWrite(RelaisLCD, LOW);     // Eteindre le LCD
		allumageTimer = 0;     // Arrêt du chrono
	}
			
	// Si le bouton est pressé et l'allumage est en fonction
	if (digitalRead(BpLCD) == LOW && allumageTimer != 0)
	{
		delay(250);     // "anti-rebonds" du bouton
		digitalWrite(RelaisLCD, LOW);     // Eteindre le LCD
		allumageTimer = 0;     // Arrêt du chrono
	}

  // ALLUMAGE EXTINCTION DES LEDS 
// Si le bouton 34 et RELAIS  1  est pressé et l'allumage n'est pas en fonction  
	if (digitalRead(BpLEDS) == LOW && allumageTimer == 0)
	{
		delay(250);     // "anti-rebonds" du bouton
		digitalWrite(RelaisLEDS, HIGH);     // Allumer les LEDS
		allumageTimer = millis();     // Démarrage du chrono
	} 

	// Si allumage en fonction et allumageTemps échu
	if (allumageTimer != 0 && (millis()-allumageTimer >= allumageTemps))
	{
		digitalWrite(RelaisLEDS, LOW);     // Eteindre les LEDS
		allumageTimer = 0;     // Arrêt du chrono
	}
			
	// Si le bouton est pressé et l'allumage est en fonction
	if (digitalRead(BpLEDS) == LOW && allumageTimer != 0)
	{
		delay(250);     // "anti-rebonds" du bouton
		digitalWrite(RelaisLEDS, LOW);     // Eteindre les LEDS
		allumageTimer = 0;     // Arrêt du chrono
	}
  


  int clk = digitalRead(KY040_CLK);
  int dt  = digitalRead(KY040_DT);
  int sw  = digitalRead(KY040_SW);




  

  if (!inRemplissageMode && !reglageTempsActif) {
    if (clk != lastCLKState) {
      if (clk == LOW && millis() - lastRotationTime > DEBOUNCE_ROTATION) {
        if (dt != clk) menuItem = (menuItem + 1) % 12;
        else menuItem = (menuItem - 1 + 12) % 12;
        if (inMenu) displayMenu();
        lastRotationTime = millis();
      }
      lastCLKState = clk;
    }
  }

  if (sw == LOW && swLastState == HIGH) swPressStart = millis();
  else if (sw == HIGH && swLastState == LOW) {
    unsigned long dur = millis() - swPressStart;
    if (dur > 50 && dur < LONG_PRESS_DURATION) {
      if (inMenu) {
        switch (menuItem) {
          case 0: if (confirmer_action("Effacer alarmes ?")) { alerte_sonde = false; mauvaises_lectures_consecutives = 0; flag_conso_detectee = false; lcd_clear(); resetLineCache(); lcd_setCursor(2,1); lcd_print("ALARMES EFFACEES"); beep_long(); delay(1500); } break;
          case 1: if (confirmer_action("Effacer conso ?")) { flag_conso_detectee = false; lcd_clear(); resetLineCache(); lcd_setCursor(2,1); lcd_print("CONSO EFFACEE"); beep_long(); delay(1500); } break;
          case 2: if (confirmer_action("Effacer remplis. ?")) { memset(remplissages, 0, sizeof(remplissages)); idxRemplissage = nb_remplissages_reels = 0; lcd_clear(); resetLineCache(); lcd_setCursor(1,1); lcd_print("REMPLISSAGES EFFACES"); beep_long(); delay(1500); } break;
          case 3: if (confirmer_action("Effacer stats ?")) { initBuckets(); sauvegarderBuckets(); lcd_clear(); resetLineCache(); lcd_setCursor(2,1); lcd_print("STATS EFFACEES"); beep_long(); delay(1500); } break;
          case 4:
            if (inRemplissageMode) {
              lcd_clear(); resetLineCache(); lcd_setCursor(1,1); lcd_print("REMPLISSAGE ACTIF"); lcd_setCursor(2,2); lcd_print("ATTENDRE FIN..."); beep_long(); delay(2000);
            } else {
              inRemplissageMode = true;
              inMenu = false;
              mode_remplissage_avec_reglage_temps();
            }
            break;
          case 5: if (confirmer_action("Effacer EEPROM ?")) { effacer_totale_eeprom(); ESP.restart(); } break;
          case 6: inMenu = false; /* afficher_conso_horaire_7j(); */ break;
          case 7: inMenu = false; /* afficher_conso_journaliere_60j(); */ break;
          case 8: inMenu = false; /* afficher_conso_mensuelle_24m(); */ break;
          case 9: inMenu = false; /* afficher_conso_annuelle_10a(); */ break;
          case 10: inMenu = false; /* afficher_alarmes(); */ break;
          case 11: inMenu = false; /* ecran_remplissages_opti(); */ break;
        }
        inMenu = false; currentDisplayMode = -1;
      } else {
        inMenu = true;
        menuItem = 0;
        displayMenu();
      }
    } else if (dur >= LONG_PRESS_DURATION) {
      inMenu = false;
      if (inRemplissageMode) {
        inRemplissageMode = false;
        arreterChenillard();
        ancien_niveau = moyenne(tableau_mesures);
        currentDisplayMode = -1;
        resetLineCache();
      }
    }
  }
  swLastState = sw;

  if (inRemplissageMode || reglageTempsActif) return;

  if (inMenu) { gerer_led_wifi_cloud(); return; }
  if (alerte_sonde) { gerer_led_wifi_cloud(); return; }
  if (flag_conso_detectee && millis() - debut_affichage_conso < 6000) { ecran_consommation_opti(); gerer_led_wifi_cloud(); return; }

  if (millis() - lastPing >= 300) { lastPing = millis(); mesure_et_cloud(); }

  if (millis() - lastDisplay >= DISPLAY_DURATION) {
    displayMode = (displayMode + 1) % 5;
    lastDisplay = millis();
    currentDisplayMode = -1;
  }

  switch (displayMode) {
    case 0: {
      float pc = pourcentage;
      float dc = distance;
      float hf = HAUTEUR - (dc - dcc);
      afficher_fuel_opti(fuel, pc, dc, hf);
      updateLEDs(fuel);
    } break;
    case 1: afficher_infos_capteurs_opti(); break;
    case 2: afficher_conso_opti(); break;
    case 3: afficher_conso_annuelle_opti(); break;
    case 4: ecran_consommation_opti(); break;
  }

  gerer_led_wifi_cloud();
}

// ====================== UTILITAIRES ======================
float moyenne(float* tab) {
  float s = 0; int c = 0;
  for (int i = 0; i < NB_MESURES; ++i) if (tab[i] > 0) { s += tab[i]; ++c; }
  return c ? s / c : (tab == tableau_temp ? 20.0f : (tab == tableau_hum ? 50.0f : 0.0f));
}

void beep() { digitalWrite(BUZZER, HIGH); delayMicroseconds(80000); digitalWrite(BUZZER, LOW); }
void beep_long() { digitalWrite(BUZZER, HIGH); delayMicroseconds(500000); digitalWrite(BUZZER, LOW); }

void lire_date_heure(char* d, char* h) {
  struct tm ti;
  if (getLocalTime(&ti)) {
    strftime(d, 12, "%d/%m/%Y", &ti);
    strftime(h, 10, "%H:%M:%S", &ti);
  } else {
    strcpy(d, "--/--/----"); strcpy(h, "--:--:--");
  }
}

unsigned int lire_distance_median() {
  unsigned int m[5];
  for (int i = 0; i < 5; ++i) {
    digitalWrite(TRIGGER, LOW); delayMicroseconds(25);
    digitalWrite(TRIGGER, HIGH); delayMicroseconds(120);
    digitalWrite(TRIGGER, LOW); delayMicroseconds(25);
    unsigned long dur = pulseIn(ECHO, HIGH, TIMEOUT_US);
    m[i] = (dur == 0) ? 0 : dur / 58;
    if (m[i] < 2 || m[i] > MAX_DISTANCE) m[i] = 0;
    delay(10);
  }
  std::sort(m, m + 5);
  return m[2];
}

float getCorrectionTempHum(float t, float h) {
  return (t - 20.0f) * 0.017f + (h - 50.0f) * 0.0018f;
}

// ====================== CLOUD CALLBACKS ======================
void onFuelChange() {}
void onPourcentageChange() {}
void onDistanceChange() {}
void onCjChange() {}
void onChChange() {}

/* =============================================================
   FIN – VERSION 1.9.8-upgraded – Conso fiable + 3 mesures stables
   ============================================================= */

D’abord bienvenue sur le forum.

Si je comprends votre texte, vous éprouvez des difficultés à éveillez un module a été mis en veille. Est-ce bien cela ?

Quel est le micro-contrôleur utilisé?

Bonjour patriboom ,pardon je n'ai pas ou plutôt oublié que j'avais un esp32 dev module sans aucune veille . C'est vraiment en decoonectant l'alimentation du LCD ou des leds que j'ai la déconnection du cloud…j'ai créer un.skecht sans lcd et leds et là pas de problème.. je pense que c'est peut l'ordre dans la boucle qui me fait le zonzon…qu'en pensez vous.. merci pour bienveillance

Je vous souhaite un joyeux Noël :santa_claus: :santa_claus: :santa_claus:

Quand c'est dû à une connexion/déconnexion sur une alimentation, on se préoccupe en priorité de l'organisation de la distribution électrique.
La perturbation peut très bien faire rebooter l'ESP32 et faire perdre la connexion.

Donc tu commences par faire sur une feuille de papier le bilan des consommations de chaque module, plus un schéma, sans oublier les caractéristiques de "ce qui sert" d'alimentation, et tu publies le tout.

Les LEDS, il suffit de ne pas les allumer pour qu'elles ne consomment pas.
Je vois que tu as une ligne qui te permet de le faireif (chenillardActif && !afficherNiveauActuel) return; donc, je dirais qu'il n'y a pas besoin de relais. Il suffit d'exploiter ce booléen.


Pour le LCD, c'est son rétroéclairage qui consomme le plus. Le plus simple serait de couper le rétroéclairage et pas l'alimentation du module. En effet si tu coupes le +5V du module, il va tirer du courant par ses connexions avec ton CPU et ça ce n'est vraiment pas une bonne chose.

Merci à vous tous pour les réponses je testerai cela après le papa Noël... les petits enfants sont là on va s'occuper d'eux avant tout . Une fois les tests exécutés ,je reviens vers vous . "" Amicalement vôtre"". Passez tous un magnifique Noël en famille. Et sincèrement merci

Ps étant electrotechnicien de métier pardon ayant été….j'ai séparé la commande et la puissance P de 15 w soit 3 amperes les leds sont des 3mm les couleurs différentes , égales conso differentes et 2 relais 6v qui marchent en 5 toutefois les leds relais buzzer sont commander indivuellement par la base de Npn bc547… et aussi Électronicien autodidacte depuis toujours.. Mais je vais essayer tout merci je n’ai pas fais de dessin mais je sais qu’en fermant les yeux vous l’avez dans la tête..

Vincenzo

Bonjour à tous , avant tout mes meilleurs voeux à tous…

Concernant mon problème de blocage de code par une déconnection du lcd (+5v) j’ai appris que lors de cette déconnection on tirait I2C par le bas d’ou la panique à bord en glanant dans les tutos sur I2C et LCD , j’ai eu des lignes de code que j’ai repris en testant le lcd et le test de la reconnexion.. Je pense que j’ai mal exposé mon problème et mon projet ..

En effet je voulais un capteur étanche JSN-04RT les valeurs pour ce capteur vous semble bizarre, c’est par tâtonnement que j’ai réussi à faire marcher celui-ci ( c’est surement dû au réglage de l’inductance sur la platine ). mais j’ai une précision du 1/10 mm.

Mes demandes c’est:

  Pour économie du LCD et des LEDS
  • Un affichage LCD qui fonctionne en tempo de 10 mn par appui d’un BP
  • Un affichage des LEDS NIVEAUX tempo 10mn par appui BP et le chenillard qui marche en fonction remplissage.
  • Une Led WIFI allumée en permanence
  • Une conso horaire, journalière, mensuelle et annuelle suivant des délais
  • Une alarme de défaut de distance
  • une alarme conso supérieure à x litres/heure
  • Un EEPROM performante
  • etc….

J’ai trouvé ceci en général et j’ai appliqué ces conditions et maintenant le code marche…

Pour éviter que le programme Arduino IoT Cloud se bloque lorsque l’écran LCD est débranché électriquement.

? Pourquoi ton code se bloque quand le LCD est déconnecté

La plupart des écrans LCD (I2C ou SPI) provoquent un blocage du microcontrôleur lorsqu’ils sont débranchés, car :

  • Les fonctions lcd.begin(), lcd.print(), etc. attendent une réponse du périphérique

  • Si le LCD n’est plus alimenté, le bus I2C reste tiré au niveau bas, ce qui gèle le programme

  • Arduino IoT Cloud continue de tourner, mais ton loop() ne s’exécute plus correctement

? Solution : vérifier si le LCD est présent avant d’écrire dessus, et ignorer les commandes si l’écran est absent.

:white_check_mark: Solution : détecter la présence du LCD avant utilisation

Voici un code type pour un LCD I2C (adresse 0x27 ou 0x3F).

? Étape 1 : fonction pour tester la présence du LCD

#include #include LiquidCrystal_I2C lcd(0x27, 16, 2); bool lcdPresent = false; bool detectLCD(uint8_t address) { Wire.beginTransmission(address); return (Wire.endTransmission() == 0); }

? Étape 2 : initialisation sécurisée

void setup() { Wire.begin(); if (detectLCD(0x27)) { lcdPresent = true; lcd.init(); lcd.backlight(); } else { lcdPresent = false; } }

? Étape 3 : utilisation sécurisée dans loop()

void loop() { // Code IoT Cloud ArduinoCloud.update(); if (lcdPresent) { lcd.setCursor(0, 0); lcd.print("Hello !"); } delay(500); }

?￯ᄌマ Résultat

  • Si le LCD est branché → tout fonctionne normalement

  • Si le LCD est débranché → aucun blocage, le programme continue

  • Arduino IoT Cloud reste stable et connecté

? Option avancée : redétection automatique

Si tu veux que le LCD soit détecté même après avoir été rebranché :

unsigned long lastCheck = 0; void loop() { ArduinoCloud.update(); if (millis() - lastCheck > 2000) { lastCheck = millis(); lcdPresent = detectLCD(0x27); } if (lcdPresent) { lcd.setCursor(0, 0); lcd.print("LCD OK"); } }

et voici mon code totalement opérationnel que je fourni à la communauté pour utilisation … Si on peut y apprté des modifications sans problème . la prochaine étape serait un TFT GUILTON tactile , mais je crois qu’il faudrait passé en RASPERRY ??

Merci à tous et je n’ai pas fini de vous ennuyé Cordialement je ne sais pas fermé le sujet et le mettre en projet terminé …

/*
CAPTEUR NIVEAU FUEL PERFMOMANT AVEC LCD et LEDS
Vincent MAGLIOLO du 5 Janvier 2026 à 21h00
Avec 1096 lignes .....

ESP IOT URSULA – VERSION 2.1.1-upgradée delay supprimé au max par  millis  – 30/12/2025

Conso détectée uniquement après 3 mesures stables

Plus d'incrémentation fantôme dans les buckets

KY040 corrigé, écran stable, code complet

Remplacement des delay() bloquants par millis() 30/12/25

Coupure du Négatif du LCD il semblerait que  code non bloqué et WIFI/ CLOUD OK...à suivre

Reste à verifier les tempo allumage LCD et LEDS pendant 10 minutes et allumage par le ou les BP  sur dashboard à réaliser

TIMERS SÉPARÉS POUR LCD ET LEDS - 31/12/2025

FONCTION REMPLISSAGE AJOUTÉE - 31/12/2025 à tester lors du remplissage manuel si chenillard LED OK maxi 30 minutes un autre timer?

Voir pour alarme bi-ton 2 buzzers

PROTECTION LCD DÉCONNEXION ÉLECTRIQUE rajouté le  - 05/01/2026
============================================================= */

#include <Arduino.h>
#include <Wire.h>
#include <DHT.h>
#include <EEPROM.h>
#include <WiFi.h>
#include <time.h>
#include <esp_task_wdt.h>
#include 
#include "thingProperties.h"

const char* ntpServer = "pool.ntp.org";
const long  gmtOffset_sec = 7200;
const int   daylightOffset_sec = 0;

#define DHTPIN  4
#define DHTTYPE DHT11
DHT dht(DHTPIN, DHTTYPE);

#define LED_VERTE1  12
#define LED_VERTE2  14
#define LED_JAUNE1  27
#define LED_JAUNE2  26
#define LED_ORANGE1  25
#define LED_ROUGE1  33
#define LED_ROUGE2  32
#define BUZZER      13
#define LED_WIFI    18
#define BTN_ERASE_EEPROM 0
#define SDA 21
#define SCL 22
#define KY040_CLK 16
#define KY040_DT  17
#define KY040_SW  19
#define WDT_TIMEOUT 30

const int HAUTEUR  = 160;
const int CAPACITE = 3200;
const int LITRES_CM = CAPACITE / HAUTEUR;

#define SEUIL_CONSOMMATION 2.0f
#define SEUIL_MIN_REMPLISSAGE_MANUEL 5.0f
#define DELAI_REMPLISSAGE_MANUEL_MIN 10
#define DELAI_REMPLISSAGE_MAX 120
#define DELAI_REMPLISSAGE_MIN 5
#define CHENILLARD_INTERVAL_MS 200
#define CHENILLARD_ALTERNANCE_MIN 5

#define NB_MESURES 100
#define MIN_MESURES_INITIALISATION 5
#define TRIGGER 15
#define ECHO    2
#define MAX_DISTANCE 160
#define TIMEOUT_US (MAX_DISTANCE * 100)

#define LCD_ADDR 0x27
#define LCD_COLS 20
#define LCD_ROWS 4

#define EEPROM_SIZE 2048
#define ADDR_BUCKET_MAGIC 1050
#define NB_BUCKETS 400
#define ADDR_BUCKETS 1060
#define ADDR_REMPLISSAGE_TIME 2000
#define TEMPS_SORTIE_AUTO_MIN 5
#define BTN_EEPROM_LONG_PRESS 5000

// Commande des relais ALLUMAGE LCD/LEDS
const int RelaisLCD  = 35;
const int RelaisLEDS = 34;
const int BpLCD = 3;
const int BpLEDS = 1;
const int allumageTemps = 30000;

// TIMERS SÉPARÉS POUR LCD ET LEDS
unsigned long allumageTimerLCD = 0;
unsigned long allumageTimerLEDS = 0;

// Variables pour remplacer les delay() bloquants
unsigned long lastButtonCheckLCD = 0;
unsigned long lastButtonCheckLEDS = 0;
const unsigned long BUTTON_DEBOUNCE_MS = 250;

// VARIABLE POUR DÉTECTION LCD
bool lcdPresent = false;
unsigned long lastLCDCheck = 0;
const unsigned long LCD_CHECK_INTERVAL = 2000;

struct BucketHoraire {
uint16_t annee;
uint8_t  mois, jour, heure;
float    conso_L;
};
BucketHoraire buckets[NB_BUCKETS];
int idxBucket = 0;
const uint32_t bucketMagic = 0xCAFE2025;

float ancien_niveau = -1;
float tableau_mesures[NB_MESURES] = {0};
float tableau_temp[NB_MESURES]   = {0};
float tableau_hum[NB_MESURES]    = {0};
int index_mesure = 0;
int nb_mesures_valides = 0;
bool tableau_plein = false;
bool systeme_initialise = false;
float dcc = 2.05f;

unsigned long lastPing = 0, lastDisplay = 0, lastWifiBlink = 0, lastRouge2Blink = 0, cycleStartTime = 0;
bool showingWifiStatus = true;
const unsigned long CYCLE_DURATION = 60000;
const unsigned long WIFI_BLINK_INTERVAL = 1000;
const unsigned long CLOUD_BLINK_INTERVAL = 100;

int displayMode = 0, currentDisplayMode = -1;
const unsigned long DISPLAY_DURATION = 10000;

float conso_detectee = 0;
bool  flag_conso_detectee = false;
unsigned long debut_affichage_conso = 0;
char date_conso[12], heure_conso[10];

bool alerte_sonde = false;
uint8_t mauvaises_lectures_consecutives = 0;
const uint8_t SEUIL_MAUVAISES_LECTURES = 25;

#define NB_REMPLISSAGES 10
struct Remplissage {
char  date[12];
char  heure[10];
float volumeIntroduit;
};
Remplissage remplissages[NB_REMPLISSAGES];
int idxRemplissage = 0;
int nb_remplissages_reels = 0;
unsigned long lastScrollRemplissage = 0;
int affichageRemplissageIdx = 0;

String lastLine[4] = {"", "", "", ""};

bool inMenu = false;
int menuItem = 0;
const char* menuItems[12] = {
"Effacer alarmes", "Effacer conso", "Effacer remplis.",
"Effacer stats", "Remplissage", "Effacer EEPROM",
"Voir consos horaires", "Voir consos journalieres",
"Voir consos mensuelles", "Voir consos annuelles",
"Voir alarmes", "Voir remplissages"
};

int lastCLKState = HIGH;
unsigned long lastRotationTime = 0;
const unsigned long DEBOUNCE_ROTATION = 200;

unsigned long swPressStart = 0;
int swLastState = HIGH;
const unsigned long LONG_PRESS_DURATION = 1500;

bool inRemplissageMode = false;
float volumeReferenceRemplissage = 0.0f;
unsigned long debutRemplissageMode = 0;
bool remplissageDetecte = false;
float volumeMaxDetecte = 0.0f;
int tempsRemplissageMinutes = DELAI_REMPLISSAGE_MANUEL_MIN;

bool reglageTempsActif = false;
int tempsReglageProvisoire = 0;

unsigned long lastChenillardUpdate = 0;
int chenillardPosition = 0;
bool chenillardActif = false;
bool afficherNiveauActuel = true;
unsigned long derniereAlternance = 0;

unsigned long btnEepromPressStart = 0;
int btnEepromLastState = HIGH;

#define ALERTE_ORANGE_PCT      30.0f
#define ALERTE_ROUGE_PCT       20.0f
#define ALERTE_CRITIQUE_PCT    10.0f
#define ALERTE_TRES_CRITIQUE_PCT 5.0f

bool alerte_fuel_orange = false;
bool alerte_fuel_rouge = false;
bool alerte_fuel_critique = false;
bool alerte_fuel_tres_critique = false;

unsigned long lastAlerteBeep = 0, lastAlerteBlink = 0;
bool alerteBlinkState = false;
bool conso_en_cours = false;

// ====================== FONCTION DE DÉTECTION LCD ======================
bool detectLCD(uint8_t address) {
Wire.beginTransmission(address);
return (Wire.endTransmission() == 0);
}

// ====================== LCD ======================
#define LCD_BACKLIGHT 0x08
#define En B00000100
#define Rs B00000001

void lcd_send(uint8_t data, uint8_t mode) {
if (!lcdPresent) return; // Protection si LCD absent

uint8_t high = data & 0xF0;
uint8_t low  = (data << 4) & 0xF0;
auto write4 = [](uint8_t d) -> void {
if (!lcdPresent) return; // Vérification supplémentaire
Wire.beginTransmission(LCD_ADDR);
Wire.write(d | LCD_BACKLIGHT);
Wire.endTransmission();
Wire.beginTransmission(LCD_ADDR);
Wire.write(d | En | LCD_BACKLIGHT);
Wire.endTransmission();
delayMicroseconds(1);
Wire.beginTransmission(LCD_ADDR);
Wire.write((d & ~En) | LCD_BACKLIGHT);
Wire.endTransmission();
delayMicroseconds(50);
};
write4(high | mode);
write4(low  | mode);
}

void lcd_init() {
if (!lcdPresent) return; // Protection si LCD absent

Wire.begin(SDA, SCL);
delay(50);
auto cmd = [](uint8_t c) { delayMicroseconds(c >= 0x10 ? 4500 : 150); lcd_send(c, 0); };
cmd(0x03); cmd(0x03); cmd(0x03); cmd(0x02);
lcd_send(0x28, 0);
lcd_send(0x08 | 0x04, 0);
lcd_send(0x06, 0);
lcd_clear();
}

void lcd_clear() {
if (lcdPresent) lcd_send(0x01, 0);
delay(2);
}

void lcd_setCursor(uint8_t c, uint8_t r) {
if (!lcdPresent) return;
const uint8_t offsets[4] = {0x00, 0x40, 0x14, 0x54};
lcd_send(0x80 | (c + offsets[r]), 0);
}

void lcd_print(const char* s) {
if (!lcdPresent) return;
while (*s) lcd_send(*s++, Rs);
}

void lcd_print(String s) {
if (!lcdPresent) return;
for (size_t i = 0; i < s.length(); ++i) lcd_send(s[i], Rs);
}

void lcd_createChar(uint8_t loc, uint8_t map[]) {
if (!lcdPresent) return;
lcd_send(0x40 | (loc << 3), 0);
for (int i = 0; i < 8; ++i) lcd_send(map[i], Rs);
}

void updateLine(int line, String text) {
if (!lcdPresent) return; // Protection si LCD absent
if (text == lastLine[line]) return;
lcd_setCursor(0, line);
lcd_print("                    ");
lcd_setCursor(0, line);
lcd_print(text);
lastLine[line] = text;
}

void displayPreciseBargraph(float pc) {
if (!lcdPresent) return; // Protection si LCD absent
int tot = (int)(pc * 100);
if (tot < 0) tot = 0; else if (tot > 10000) tot = 10000;
int full = tot / 500;
int rest = (tot % 500) / 100;
String bar = "";
for (int i = 0; i < 20; ++i) {
if (i < full) bar += char(4);
else if (i == full && rest > 0) bar += char(rest - 1);
else bar += ' ';
}
updateLine(3, bar);
}

void resetLineCache() {
for (auto& l : lastLine) l = "";
}

// ====================== EEPROM & BUCKETS ======================
void effacer_totale_eeprom() {
EEPROM.begin(EEPROM_SIZE);
for (int i = 0; i < EEPROM_SIZE; ++i) EEPROM.write(i, 0xFF);
EEPROM.commit(); EEPROM.end();
}

void chargerTempsRemplissage() {
EEPROM.begin(EEPROM_SIZE);
int tempsLu;
EEPROM.get(ADDR_REMPLISSAGE_TIME, tempsLu);
if (tempsLu >= DELAI_REMPLISSAGE_MIN && tempsLu <= DELAI_REMPLISSAGE_MAX) {
tempsRemplissageMinutes = tempsLu;
} else {
tempsRemplissageMinutes = DELAI_REMPLISSAGE_MANUEL_MIN;
}
EEPROM.end();
}

void sauvegarderTempsRemplissage(int temps) {
EEPROM.begin(EEPROM_SIZE);
EEPROM.put(ADDR_REMPLISSAGE_TIME, temps);
EEPROM.commit();
EEPROM.end();
}

void initBuckets() { memset(buckets, 0, sizeof(buckets)); idxBucket = 0; }

void chargerBuckets() {
EEPROM.begin(EEPROM_SIZE);
uint32_t magic; EEPROM.get(ADDR_BUCKET_MAGIC, magic);
if (magic != bucketMagic) { EEPROM.end(); initBuckets(); sauvegarderBuckets(); return; }
int addr = ADDR_BUCKETS;
for (int i = 0; i < NB_BUCKETS; ++i) { EEPROM.get(addr, buckets[i]); addr += sizeof(BucketHoraire); }
idxBucket = 0;
for (int i = 0; i < NB_BUCKETS; ++i) if (buckets[i].annee >= 2020) idxBucket = (i + 1) % NB_BUCKETS;
EEPROM.end();
}

void sauvegarderBuckets() {
EEPROM.begin(EEPROM_SIZE);
EEPROM.put(ADDR_BUCKET_MAGIC, bucketMagic);
int addr = ADDR_BUCKETS;
for (int i = 0; i < NB_BUCKETS; ++i) { EEPROM.put(addr, buckets[i]); addr += sizeof(BucketHoraire); }
EEPROM.commit(); EEPROM.end();
}

void forcerBucketCourant() {
struct tm ti; if (!getLocalTime(&ti)) return;
uint16_t a = ti.tm_year + 1900;
uint8_t  m = ti.tm_mon + 1, j = ti.tm_mday, h = ti.tm_hour;
int cur = (idxBucket == 0 ? NB_BUCKETS - 1 : idxBucket - 1);
if (buckets[cur].annee != a || buckets[cur].mois != m || buckets[cur].jour != j || buckets[cur].heure != h) {
buckets[idxBucket] = {a, m, j, h, 0.0f};
idxBucket = (idxBucket + 1) % NB_BUCKETS;
sauvegarderBuckets();
}
}

void ajouterConsoDansBucket(float q) {
forcerBucketCourant();
int cur = (idxBucket == 0 ? NB_BUCKETS - 1 : idxBucket - 1);
buckets[cur].conso_L += q;
sauvegarderBuckets();
}

float getConso(bool horaire = true, bool jour = false, bool mois = false, bool annee = false) {
forcerBucketCourant();
struct tm ti; if (!getLocalTime(&ti)) return 0.0f;
uint16_t a = ti.tm_year + 1900;
uint8_t  m = ti.tm_mon + 1, j = ti.tm_mday, h = ti.tm_hour;
float total = 0.0f;
for (int i = 0; i < NB_BUCKETS; ++i) {
if ((horaire && buckets[i].annee == a && buckets[i].mois == m && buckets[i].jour == j && buckets[i].heure == h) ||
(jour   && buckets[i].annee == a && buckets[i].mois == m && buckets[i].jour == j) ||
(mois   && buckets[i].annee == a && buckets[i].mois == m) ||
(annee  && buckets[i].annee == a))
total += buckets[i].conso_L;
}
return total;
}

// ====================== CHENILLARD LED ======================
void updateLEDs(float v) {
if (chenillardActif && !afficherNiveauActuel) return;
digitalWrite(LED_ROUGE2, v >= 400 ? HIGH : ((millis() - lastRouge2Blink >= 500) ? (lastRouge2Blink = millis(), !digitalRead(LED_ROUGE2)) : digitalRead(LED_ROUGE2)));
digitalWrite(LED_ROUGE1, v >= 500 ? HIGH : LOW);
digitalWrite(LED_ORANGE1, v >= 700 ? HIGH : LOW);
digitalWrite(LED_JAUNE2, v >= 800 ? HIGH : LOW);
digitalWrite(LED_JAUNE1, v >= 900 ? HIGH : LOW);
digitalWrite(LED_VERTE2, v >= 1000 ? HIGH : LOW);
digitalWrite(LED_VERTE1, v >= 1200 ? HIGH : LOW);
}

void updateChenillardLED() {
if (!chenillardActif) return;
unsigned long now = millis();
if (now - derniereAlternance >= CHENILLARD_ALTERNANCE_MIN * 60000UL) {
afficherNiveauActuel = !afficherNiveauActuel;
derniereAlternance = now;
if (afficherNiveauActuel) { updateLEDs(moyenne(tableau_mesures)); return; }
}
if (afficherNiveauActuel) return;
if (now - lastChenillardUpdate >= CHENILLARD_INTERVAL_MS) {
lastChenillardUpdate = now;
digitalWrite(LED_VERTE1, LOW); digitalWrite(LED_VERTE2, LOW);
digitalWrite(LED_JAUNE1, LOW); digitalWrite(LED_JAUNE2, LOW);
digitalWrite(LED_ORANGE1, LOW); digitalWrite(LED_ROUGE1, LOW); digitalWrite(LED_ROUGE2, LOW);
switch (chenillardPosition) {
case 0: digitalWrite(LED_VERTE1, HIGH); break;
case 1: digitalWrite(LED_VERTE2, HIGH); break;
case 2: digitalWrite(LED_JAUNE1, HIGH); break;
case 3: digitalWrite(LED_JAUNE2, HIGH); break;
case 4: digitalWrite(LED_ORANGE1, HIGH); break;
case 5: digitalWrite(LED_ROUGE1, HIGH); break;
case 6: digitalWrite(LED_ROUGE2, HIGH); break;
}
chenillardPosition = (chenillardPosition + 1) % 7;
}
}

void demarrerChenillard() {
chenillardActif = true; chenillardPosition = 0; lastChenillardUpdate = 0;
derniereAlternance = millis(); afficherNiveauActuel = false;
}

void arreterChenillard() {
chenillardActif = false; updateLEDs(moyenne(tableau_mesures));
}

// ====================== ÉCRAN REGLAGE TEMPS 4 LIGNES ======================
void ecran_reglage_temps() {
if (!lcdPresent) return; // Protection si LCD absent

unsigned long lastRefresh = 0;
unsigned long startTime = millis();
int localCLKState = HIGH;
int localSWState = HIGH;
unsigned long localSWPress = 0;

while (true) {
esp_task_wdt_reset();
ArduinoCloud.update();
if (millis() - startTime > 30000) return;

if (millis() - lastRefresh >= 150) {
  lastRefresh = millis();
  lcd_clear(); resetLineCache();

  lcd_setCursor(6, 0); lcd_print("CONFIG TEMPS");

  char bufVal[10];
  snprintf(bufVal, 10, "%3d min", tempsReglageProvisoire);
  lcd_setCursor(20 - strlen(bufVal), 1);
  lcd_print(bufVal);

  lcd_setCursor(2, 2); lcd_print("Plage: 5-120 min");
  lcd_setCursor(1, 3); lcd_print("KY040: +/-  OK: val");
}

int clk = digitalRead(KY040_CLK);
int dt  = digitalRead(KY040_DT);
int sw  = digitalRead(KY040_SW);

if (clk != localCLKState && clk == LOW) {
  if (millis() - lastRotationTime > DEBOUNCE_ROTATION) {
    if (dt == LOW) tempsReglageProvisoire = min(tempsReglageProvisoire + 5, DELAI_REMPLISSAGE_MAX);
    else tempsReglageProvisoire = max(tempsReglageProvisoire - 5, DELAI_REMPLISSAGE_MIN);
    lastRotationTime = millis();
    beep();
  }
}
localCLKState = clk;

if (sw == LOW && localSWState == HIGH) localSWPress = millis();
else if (sw == HIGH && localSWState == LOW) {
  unsigned long dur = millis() - localSWPress;
  if (dur > 50 && dur < LONG_PRESS_DURATION) {
    tempsRemplissageMinutes = tempsReglageProvisoire;
    sauvegarderTempsRemplissage(tempsRemplissageMinutes);
    lcd_clear(); resetLineCache();
    lcd_setCursor(4, 1); lcd_print("TEMPS ENREGISTRE");
    char buf[21];
    snprintf(buf, 21, "%d minutes", tempsRemplissageMinutes);
    lcd_setCursor(4, 2); lcd_print(buf);
    beep_long();
    delay(2000);
    return;
  }
}
localSWState = sw;
delay(10);

}
}

// ====================== FONCTIONS D'ÉCRAN ======================
bool confirmer_action(const char* titre) {
if (!lcdPresent) return false; // Protection si LCD absent

int choix = 0;
const char* options[3] = {"Oui", "Non", "Sortie"};
unsigned long lastRefresh = 0;
unsigned long startTime = millis();
int localCLKState = HIGH;
int localSWState = HIGH;
unsigned long localSWPress = 0;

while (true) {
esp_task_wdt_reset();
ArduinoCloud.update();
if (millis() - startTime > TEMPS_SORTIE_AUTO_MIN * 60000UL) return false;
if (millis() - lastRefresh >= 200) {
lastRefresh = millis();
lcd_clear(); resetLineCache();
lcd_setCursor(1, 0); lcd_print(titre);
for (int i = 0; i < 3; ++i) {
lcd_setCursor(0, 1 + i);
lcd_print(i == choix ? ">" : " ");
lcd_print(options[i]);
}
lcd_setCursor(0, 3); lcd_print("Appui long = Sortie");
}
int clk = digitalRead(KY040_CLK);
int dt  = digitalRead(KY040_DT);
int sw  = digitalRead(KY040_SW);
if (clk != localCLKState && clk == LOW) {
if (millis() - lastRotationTime > DEBOUNCE_ROTATION) {
choix = (choix + (dt == LOW ? 1 : -1) + 3) % 3;
lastRotationTime = millis();
}
}
localCLKState = clk;
if (sw == LOW && localSWState == HIGH) localSWPress = millis();
else if (sw == HIGH && localSWState == LOW) {
unsigned long dur = millis() - localSWPress;
if (dur > 50 && dur < LONG_PRESS_DURATION) return (choix == 0);
else if (dur >= LONG_PRESS_DURATION) return false;
}
localSWState = sw;
delay(100);
}
}

void afficher_fuel_opti(float vol, float pc, float dcorr, float hfuel, const char* titre = "FUEL RESTANT") {
if (!lcdPresent) return; // Protection si LCD absent

if (currentDisplayMode != 0) {
lcd_clear(); resetLineCache();
lcd_setCursor((LCD_COLS - strlen(titre)) / 2, 0);
lcd_print(titre);
currentDisplayMode = 0;
}
char buf[21];
snprintf(buf, 21, "%.2f L %.2f %%", vol, pc); updateLine(1, buf);
snprintf(buf, 21, "%.3f cm %.2f cm", dcorr, hfuel); updateLine(2, buf);
displayPreciseBargraph(pc);
}

void afficher_remplissage_en_cours() {
if (!lcdPresent) return; // Protection si LCD absent

float vol = moyenne(tableau_mesures);
float pc  = vol * 100.0f / CAPACITE;
float dc  = distance;
float hf  = HAUTEUR - (dc - dcc);
if (currentDisplayMode != 0) {
lcd_clear(); resetLineCache();
lcd_setCursor(2, 0); lcd_print("REMPLISSAGE EN COURS");
currentDisplayMode = 0;
}
char buf[21];
snprintf(buf, 21, "%.2f L %.2f %%", vol, pc); updateLine(1, buf);
snprintf(buf, 21, "%.3f cm %.2f cm", dc, hf); updateLine(2, buf);
displayPreciseBargraph(pc);
}

void afficher_infos_capteurs_opti() {
if (!lcdPresent) return; // Protection si LCD absent

if (currentDisplayMode != 1) { lcd_clear(); resetLineCache(); currentDisplayMode = 1; }
char d[12], h[10]; lire_date_heure(d, h);
float t = moyenne(tableau_temp), hum = moyenne(tableau_hum);
char buf[21];
snprintf(buf, 21, "%s %s", d, h); updateLine(0, buf);
snprintf(buf, 21, "Temp: %.1f C", t); updateLine(1, buf);
snprintf(buf, 21, "Hum : %.1f %%", hum); updateLine(2, buf);
snprintf(buf, 21, " WiFi:%s Cloud:%s", WiFi.status() == WL_CONNECTED ? "OK" : "NO", ArduinoCloud.connected() ? "OK" : "NO");
updateLine(3, buf);
}

void afficher_conso_opti() {
if (!lcdPresent) return; // Protection si LCD absent

if (currentDisplayMode != 2) { lcd_clear(); resetLineCache(); lcd_setCursor(2, 0); lcd_print("CONSO CALENDAIRE"); currentDisplayMode = 2; }
char buf[21];
snprintf(buf, 21, "Heure : %.2f L", getConso(true)); updateLine(1, buf);
snprintf(buf, 21, "Jour  : %.2f L", getConso(false, true)); updateLine(2, buf);
snprintf(buf, 21, "Mois  : %.2f L", getConso(false, false, true)); updateLine(3, buf);
}

void afficher_conso_annuelle_opti() {
if (!lcdPresent) return; // Protection si LCD absent

if (currentDisplayMode != 3) { lcd_clear(); resetLineCache(); lcd_setCursor(2, 0); lcd_print(" CONSO ANNUELLE"); currentDisplayMode = 3; }
float ca = getConso(false, false, false, true);
char buf[21];
snprintf(buf, 21, "%.2f L/an", ca); updateLine(2, buf);
snprintf(buf, 21, "%.3f m3/an", ca / 1000.0f); updateLine(3, buf);
}

void ecran_consommation_opti() {
if (!lcdPresent) return; // Protection si LCD absent

if (currentDisplayMode != 9) { lcd_clear(); resetLineCache(); lcd_setCursor(4, 0); lcd_print("CONSOMMATION"); currentDisplayMode = 9; }
char buf[21];
snprintf(buf, 21, "Date : %s", date_conso); updateLine(1, buf);
snprintf(buf, 21, "Heure: %s", heure_conso); updateLine(2, buf);
snprintf(buf, 21, "Conso: -%.2f L", conso_detectee); updateLine(3, buf);
}

void displayMenu() {
if (!lcdPresent) return; // Protection si LCD absent

lcd_clear(); resetLineCache();
lcd_setCursor(5, 0); lcd_print("*** MENU ***");
lcd_setCursor(0, 1); lcd_print(">"); lcd_print(menuItems[menuItem]);
lcd_setCursor(0, 3); lcd_print("Tourner: select Bref: OK");
}

// ====================== LED / WIFI / EEPROM BUTTONS ======================
void gerer_led_wifi_cloud() {
if (millis() - cycleStartTime >= CYCLE_DURATION) {
cycleStartTime = millis(); showingWifiStatus = !showingWifiStatus; digitalWrite(LED_WIFI, LOW);
}
bool connected = showingWifiStatus ? (WiFi.status() == WL_CONNECTED) : ArduinoCloud.connected();
unsigned long interval = showingWifiStatus ? WIFI_BLINK_INTERVAL : CLOUD_BLINK_INTERVAL;
if (connected && millis() - lastWifiBlink >= interval) { lastWifiBlink = millis(); digitalWrite(LED_WIFI, !digitalRead(LED_WIFI)); }
else if (!connected) digitalWrite(LED_WIFI, LOW);
}

void gerer_bouton_eeprom() {
int state = digitalRead(BTN_ERASE_EEPROM);
if (state != btnEepromLastState) {
if (state == LOW) btnEepromPressStart = millis();
btnEepromLastState = state;
}
if (state == LOW) {
unsigned long dur = millis() - btnEepromPressStart;
if (dur >= BTN_EEPROM_LONG_PRESS) {
if (lcdPresent) { // Protection si LCD absent
lcd_clear(); lcd_setCursor(1,0); lcd_print("EFFACEMENT TOTAL");
lcd_setCursor(3,1); lcd_print("EEPROM EN COURS");
lcd_setCursor(5,2); lcd_print("PATIENTEZ...");
}
for (int i = 0; i < 3; ++i) { beep_long(); delay(200); }
effacer_totale_eeprom();
if (lcdPresent) { // Protection si LCD absent
lcd_clear(); lcd_setCursor(2,1); lcd_print("EEPROM EFFACEE");
lcd_setCursor(3,2); lcd_print("REDEMARRAGE...");
}
delay(2000); ESP.restart();
} else if (dur >= 1000 && dur % 1000 < 100) {
if (lcdPresent) { // Protection si LCD absent
lcd_clear(); lcd_setCursor(1,0); lcd_print("MAINTENIR POUR");
lcd_setCursor(1,1); lcd_print("EFFACER EEPROM");
char buf[21]; snprintf(buf, 21, "%d secondes...", (BTN_EEPROM_LONG_PRESS - dur) / 1000 + 1);
lcd_setCursor(5,2); lcd_print(buf); beep();
}
}
}
}

// ====================== MODE REMPLISSAGE AVEC REGLAGE TEMPS ======================
void mode_remplissage_avec_reglage_temps() {
if (!lcdPresent) return; // Protection si LCD absent

tempsReglageProvisoire = tempsRemplissageMinutes;
reglageTempsActif = true;
ecran_reglage_temps();
reglageTempsActif = false;

volumeReferenceRemplissage = moyenne(tableau_mesures);
volumeMaxDetecte = 0.0f;
debutRemplissageMode = millis();
remplissageDetecte = false;
currentDisplayMode = -1;
resetLineCache();
demarrerChenillard();

int localCLKState = HIGH;
int localSWState = HIGH;
unsigned long localSWPress = 0;

while (true) {
esp_task_wdt_reset(); ArduinoCloud.update();
gerer_led_wifi_cloud(); updateChenillardLED();

if (millis() - lastPing >= 300) { lastPing = millis(); mesure_et_cloud(); }

afficher_remplissage_en_cours();

unsigned long tempsEcoule = millis() - debutRemplissageMode;
unsigned long tempsRestant = (tempsRemplissageMinutes * 60000UL) - tempsEcoule;
int minutesRestantes = max(0, (int)(tempsRestant / 60000UL));
char buf[21];
snprintf(buf, 21, "%.2f L %.2f %%", moyenne(tableau_mesures), moyenne(tableau_mesures) * 100.0f / CAPACITE);
updateLine(1, buf);
char buf2[8];
snprintf(buf2, 8, "%3d", minutesRestantes);
lcd_setCursor(17, 1); lcd_print(buf2);

float volActuel = moyenne(tableau_mesures);
float gain = volActuel - volumeReferenceRemplissage;
if (gain > volumeMaxDetecte) volumeMaxDetecte = gain;
if (gain >= SEUIL_MIN_REMPLISSAGE_MANUEL) remplissageDetecte = true;

int clk = digitalRead(KY040_CLK);
int sw  = digitalRead(KY040_SW);
if (clk != localCLKState && clk == LOW) localCLKState = clk;
if (sw == LOW && localSWState == HIGH) localSWPress = millis();
else if (sw == HIGH && localSWState == LOW) {
  unsigned long dur = millis() - localSWPress;
  if (dur > 50 && dur < LONG_PRESS_DURATION) {
    if (gain >= SEUIL_MIN_REMPLISSAGE_MANUEL) {
      if (confirmer_action("Valider remplissage ?")) {
        char d[12], h[10]; lire_date_heure(d, h);
        strcpy(remplissages[idxRemplissage].date, d);
        strcpy(remplissages[idxRemplissage].heure, h);
        remplissages[idxRemplissage].volumeIntroduit = gain;
        idxRemplissage = (idxRemplissage + 1) % NB_REMPLISSAGES;
        if (nb_remplissages_reels < NB_REMPLISSAGES) ++nb_remplissages_reels;
        beep_long();
        lcd_clear(); resetLineCache();
        lcd_setCursor(2,1); lcd_print("REMPLISSAGE");
        lcd_setCursor(2,2); lcd_print("ENREGISTRE !");
        delay(2000);
      }
    }
    break;
  }
}
localSWState = sw;

if (millis() - debutRemplissageMode >= tempsRemplissageMinutes * 60000UL) {
  if (!remplissageDetecte || volumeMaxDetecte < SEUIL_MIN_REMPLISSAGE_MANUEL) {
    lcd_clear(); resetLineCache();
    lcd_setCursor(1,1); lcd_print("VOLUME TROP FAIBLE");
    lcd_setCursor(3,2); lcd_print("MIN: 5L");
    beep_long(); delay(3000);
  }
  break;
}
delay(10);

}

inRemplissageMode = false;
arreterChenillard();
ancien_niveau = moyenne(tableau_mesures);
currentDisplayMode = -1;
resetLineCache();
}

// ====================== SETUP ======================
void setup() {
Serial.begin(115200);

// Allumage
pinMode(3,OUTPUT);
pinMode(BpLCD, INPUT_PULLUP);
digitalWrite(RelaisLCD, LOW);

pinMode(1,OUTPUT);
pinMode(BpLEDS, INPUT_PULLUP);
digitalWrite(RelaisLEDS, LOW);

// ALLUMAGE
pinMode(BUZZER, OUTPUT); digitalWrite(BUZZER, LOW);
pinMode(LED_WIFI, OUTPUT); pinMode(TRIGGER, OUTPUT); pinMode(ECHO, INPUT);
digitalWrite(TRIGGER, LOW);
pinMode(KY040_CLK, INPUT_PULLUP); pinMode(KY040_DT, INPUT_PULLUP); pinMode(KY040_SW, INPUT_PULLUP);
pinMode(BTN_ERASE_EEPROM, INPUT_PULLUP);
for (int p : {LED_VERTE1,LED_VERTE2,LED_JAUNE1,LED_JAUNE2,LED_ORANGE1,LED_ROUGE1,LED_ROUGE2}) pinMode(p, OUTPUT);
esp_task_wdt_init(WDT_TIMEOUT, true); esp_task_wdt_add(NULL);

// DÉTECTION LCD AVANT INITIALISATION
Wire.begin(SDA, SCL);
lcdPresent = detectLCD(LCD_ADDR);

if (lcdPresent) {
lcd_init();
byte bars[5][8] = {
{0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10},
{0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18},
{0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C},
{0x1E,0x1E,0x1E,0x1E,0x1E,0x1E,0x1E,0x1E},
{0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F}
};
for (int i = 0; i < 5; ++i) lcd_createChar(i, bars[i]);
lcd_clear(); lcd_setCursor(3,0); lcd_print("CUVE FUEL 2.1.1");
lcd_setCursor(3,1); lcd_print("INITIALISATION...");
lcd_setCursor(3,2); lcd_print("ATTENTE MESURES");
lcd_setCursor(2,3); lcd_print("VINCENT MAGLIOLO");
}

chargerTempsRemplissage();
dht.begin();
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
WiFi.begin();
unsigned long t = millis();
while (WiFi.status() != WL_CONNECTED && millis() - t < 15000) { esp_task_wdt_reset(); delay(100); }
chargerBuckets();
initProperties(); ArduinoCloud.begin(ArduinoIoTPreferredConnection);
setDebugMessageLevel(4); ArduinoCloud.printDebugInfo();
swLastState = digitalRead(KY040_SW);
delay(2000);
if (lcdPresent) {
lcd_clear(); currentDisplayMode = -1;
}
cycleStartTime = millis(); showingWifiStatus = true;
}

// ====================== MESURE ET CLOUD ======================
void mesure_et_cloud() {
float temp = dht.readTemperature(), hum = dht.readHumidity();
if (isnan(temp)) temp = 20.0f; if (isnan(hum)) hum = 50.0f;
temperature = temp; humidity = hum;
tableau_temp[index_mesure] = temp; tableau_hum[index_mesure] = hum;
unsigned int dcm = 0;
for (int i = 0; i < 3 && !dcm; ++i) dcm = lire_distance_median();
bool def = (dcm == 0 || dcm > HAUTEUR || dcm < 5);
if (def) {
if (++mauvaises_lectures_consecutives >= SEUIL_MAUVAISES_LECTURES && !alerte_sonde) { alerte_sonde = true; beep_long(); }
} else {
mauvaises_lectures_consecutives = 0;
if (alerte_sonde) alerte_sonde = false;
}
if (alerte_sonde) return;
float corr = getCorrectionTempHum(moyenne(tableau_temp), moyenne(tableau_hum));
float dcorr = dcm + corr;
distance = dcorr + dcc;
float hfuel = HAUTEUR - dcorr;
float vol = hfuel * LITRES_CM;
if (vol > 0 && vol <= CAPACITE) {
fuel = vol;
tableau_mesures[index_mesure] = vol;
beep();
if (++index_mesure >= NB_MESURES) { index_mesure = 0; tableau_plein = true; }
if (++nb_mesures_valides >= MIN_MESURES_INITIALISATION && !systeme_initialise) {
float init = moyenne(tableau_mesures);
for (int i = 0; i < NB_MESURES; ++i) tableau_mesures[i] = init;
ancien_niveau = init; systeme_initialise = true;
}
}
if (systeme_initialise && tableau_plein && index_mesure == 0) {
float vol_moy = moyenne(tableau_mesures);
pourcentage = vol_moy * 100.0f / CAPACITE;

// 🔒 Attendre au moins 3 mesures stables avant de détecter une conso
static int mesures_stables = 0;
float diff = vol_moy - ancien_niveau;

if (abs(diff) <= SEUIL_CONSOMMATION) {
  mesures_stables++;
} else {
  mesures_stables = 0;
}

if (diff < -SEUIL_CONSOMMATION && mesures_stables >= 3) {
  float delta_conso = -diff;
  if (!conso_en_cours) {
    conso_detectee = delta_conso;
    lire_date_heure(date_conso, heure_conso);
    conso_en_cours = true;
  } else {
    conso_detectee += delta_conso;
  }
  ajouterConsoDansBucket(delta_conso);
  flag_conso_detectee = true;
  debut_affichage_conso = millis();
  beep();
  ancien_niveau = vol_moy;
  mesures_stables = 0;
} else if (conso_en_cours && abs(diff) <= SEUIL_CONSOMMATION) {
  conso_en_cours = false;
}
tableau_plein = false;

}
ch = getConso(true);
cj = getConso(false, true);
float pc = pourcentage;
alerte_fuel_orange      = (pc <= ALERTE_ORANGE_PCT && pc > ALERTE_ROUGE_PCT);
alerte_fuel_rouge       = (pc <= ALERTE_ROUGE_PCT && pc > ALERTE_CRITIQUE_PCT);
alerte_fuel_critique    = (pc <= ALERTE_CRITIQUE_PCT && pc > ALERTE_TRES_CRITIQUE_PCT);
alerte_fuel_tres_critique = (pc <= ALERTE_TRES_CRITIQUE_PCT);
}

// ====================== LOOP ======================
void loop() {
esp_task_wdt_reset();
ArduinoCloud.update();
forcerBucketCourant();
gerer_bouton_eeprom();

// DÉTECTION LCD PÉRIODIQUE
if (millis() - lastLCDCheck >= LCD_CHECK_INTERVAL) {
lastLCDCheck = millis();
lcdPresent = detectLCD(LCD_ADDR);
}

// ALLUMAGE EXTINCTION DU LCD - Gestion non-bloquante avec timer séparé
if (digitalRead(BpLCD) == LOW && (millis() - lastButtonCheckLCD >= BUTTON_DEBOUNCE_MS)) {
lastButtonCheckLCD = millis();
if (allumageTimerLCD == 0) {
digitalWrite(RelaisLCD, HIGH);
allumageTimerLCD = millis();
} else {
digitalWrite(RelaisLCD, LOW);
allumageTimerLCD = 0;
}
}

if (allumageTimerLCD != 0 && (millis() - allumageTimerLCD >= allumageTemps)) {
digitalWrite(RelaisLCD, LOW);
allumageTimerLCD = 0;
}

// ALLUMAGE EXTINCTION DES LEDS - Gestion non-bloquante avec timer séparé
if (digitalRead(BpLEDS) == LOW && (millis() - lastButtonCheckLEDS >= BUTTON_DEBOUNCE_MS)) {
lastButtonCheckLEDS = millis();
if (allumageTimerLEDS == 0) {
digitalWrite(RelaisLEDS, HIGH);
allumageTimerLEDS = millis();
} else {
digitalWrite(RelaisLEDS, LOW);
allumageTimerLEDS = 0;
}
}

if (allumageTimerLEDS != 0 && (millis() - allumageTimerLEDS >= allumageTemps)) {
digitalWrite(RelaisLEDS, LOW);
allumageTimerLEDS = 0;
}

int clk = digitalRead(KY040_CLK);
int dt  = digitalRead(KY040_DT);
int sw  = digitalRead(KY040_SW);

if (!inRemplissageMode && !reglageTempsActif) {
if (clk != lastCLKState) {
if (clk == LOW && millis() - lastRotationTime > DEBOUNCE_ROTATION) {
if (dt != clk) menuItem = (menuItem + 1) % 12;
else menuItem = (menuItem - 1 + 12) % 12;
if (inMenu) displayMenu();
lastRotationTime = millis();
}
lastCLKState = clk;
}
}

if (sw == LOW && swLastState == HIGH) swPressStart = millis();
else if (sw == HIGH && swLastState == LOW) {
unsigned long dur = millis() - swPressStart;
if (dur > 50 && dur < LONG_PRESS_DURATION) {
if (inMenu) {
switch (menuItem) {
case 0: if (confirmer_action("Effacer alarmes ?")) { alerte_sonde = false; mauvaises_lectures_consecutives = 0; flag_conso_detectee = false; if (lcdPresent) { lcd_clear(); resetLineCache(); lcd_setCursor(2,1); lcd_print("ALARMES EFFACEES"); } beep_long(); delay(1500); } break;
case 1: if (confirmer_action("Effacer conso ?")) { flag_conso_detectee = false; if (lcdPresent) { lcd_clear(); resetLineCache(); lcd_setCursor(2,1); lcd_print("CONSO EFFACEE"); } beep_long(); delay(1500); } break;
case 2: if (confirmer_action("Effacer remplis. ?")) { memset(remplissages, 0, sizeof(remplissages)); idxRemplissage = nb_remplissages_reels = 0; if (lcdPresent) { lcd_clear(); resetLineCache(); lcd_setCursor(1,1); lcd_print("REMPLISSAGES EFFACES"); } beep_long(); delay(1500); } break;
case 3: if (confirmer_action("Effacer stats ?")) { initBuckets(); sauvegarderBuckets(); if (lcdPresent) { lcd_clear(); resetLineCache(); lcd_setCursor(2,1); lcd_print("STATS EFFACEES"); } beep_long(); delay(1500); } break;
case 4:
if (inRemplissageMode) {
if (lcdPresent) { lcd_clear(); resetLineCache(); lcd_setCursor(1,1); lcd_print("REMPLISSAGE ACTIF"); lcd_setCursor(2,2); lcd_print("ATTENDRE FIN..."); }
beep_long(); delay(2000);
} else {
inRemplissageMode = true;
inMenu = false;
mode_remplissage_avec_reglage_temps();
}
break;
case 5: if (confirmer_action("Effacer EEPROM ?")) { effacer_totale_eeprom(); ESP.restart(); } break;
case 6: inMenu = false; /* afficher_conso_horaire_7j(); / break;
case 7: inMenu = false; / afficher_conso_journaliere_60j(); / break;
case 8: inMenu = false; / afficher_conso_mensuelle_24m(); / break;
case 9: inMenu = false; / afficher_conso_annuelle_10a(); / break;
case 10: inMenu = false; / afficher_alarmes(); / break;
case 11: inMenu = false; / ecran_remplissages_opti(); */ break;
}
inMenu = false; currentDisplayMode = -1;
} else {
inMenu = true;
menuItem = 0;
displayMenu();
}
} else if (dur >= LONG_PRESS_DURATION) {
inMenu = false;
if (inRemplissageMode) {
inRemplissageMode = false;
arreterChenillard();
ancien_niveau = moyenne(tableau_mesures);
currentDisplayMode = -1;
resetLineCache();
}
}
}
swLastState = sw;

if (inRemplissageMode || reglageTempsActif) return;

if (inMenu) { gerer_led_wifi_cloud(); return; }
if (alerte_sonde) { gerer_led_wifi_cloud(); return; }
if (flag_conso_detectee && millis() - debut_affichage_conso < 6000) { ecran_consommation_opti(); gerer_led_wifi_cloud(); return; }

if (millis() - lastPing >= 300) { lastPing = millis(); mesure_et_cloud(); }

if (millis() - lastDisplay >= DISPLAY_DURATION) {
displayMode = (displayMode + 1) % 5;
lastDisplay = millis();
currentDisplayMode = -1;
}

switch (displayMode) {
case 0: {
float pc = pourcentage;
float dc = distance;
float hf = HAUTEUR - (dc - dcc);
afficher_fuel_opti(fuel, pc, dc, hf);
updateLEDs(fuel);
} break;
case 1: afficher_infos_capteurs_opti(); break;
case 2: afficher_conso_opti(); break;
case 3: afficher_conso_annuelle_opti(); break;
case 4: ecran_consommation_opti(); break;
}

gerer_led_wifi_cloud();
}

// ====================== UTILITAIRES ======================
float moyenne(float* tab) {
float s = 0; int c = 0;
for (int i = 0; i < NB_MESURES; ++i) if (tab[i] > 0) { s += tab[i]; ++c; }
return c ? s / c : (tab == tableau_temp ? 20.0f : (tab == tableau_hum ? 50.0f : 0.0f));
}

void beep() { digitalWrite(BUZZER, HIGH); delayMicroseconds(80000); digitalWrite(BUZZER, LOW); }
void beep_long() { digitalWrite(BUZZER, HIGH); delayMicroseconds(500000); digitalWrite(BUZZER, LOW); }

void lire_date_heure(char* d, char* h) {
struct tm ti;
if (getLocalTime(&ti)) {
strftime(d, 12, "%d/%m/%Y", &ti);
strftime(h, 10, "%H:%M:%S", &ti);
} else {
strcpy(d, "--/--/----"); strcpy(h, "--:--:--");
}
}

unsigned int lire_distance_median() {
unsigned int m[5];
for (int i = 0; i < 5; ++i) {
digitalWrite(TRIGGER, LOW); delayMicroseconds(25);
digitalWrite(TRIGGER, HIGH); delayMicroseconds(120);
digitalWrite(TRIGGER, LOW); delayMicroseconds(25);
unsigned long dur = pulseIn(ECHO, HIGH, TIMEOUT_US);
m[i] = (dur == 0) ? 0 : dur / 58;
if (m[i] < 2 || m[i] > MAX_DISTANCE) m[i] = 0;
delay(10);
}
std::sort(m, m + 5);
return m[2];
}

float getCorrectionTempHum(float t, float h) {
return (t - 20.0f) * 0.017f + (h - 50.0f) * 0.0018f;
}

// ====================== CLOUD CALLBACKS ======================
void onFuelChange() {}
void onPourcentageChange() {}
void onDistanceChange() {}
void onCjChange() {}
void onChChange() {}

/* =============================================================
FIN – VERSION 2.1.1- Conso fiable + 3 mesures stables + delay() remplacés + TIMERS SÉPARÉS + FONCTION REMPLISSAGE + PROTECTION LCD DÉCONNEXION
============================================================= */