I2C problème d'affichage sur 1ière ligne

Bonjour,
Sur l'écran LCD20x4 présente un problème d'affichage sur la première ligne.
Une partie de l'affichage de la deuxième ligne se répète sur la première.

La première ligne affiche _1410461.84 * alors qu'elle devrait afficher Spaduino V1.84
2e ligne : Lun_18/05/26_14:10 c'est OK
3e ligne : EAU:---Cons:32C
C'est OK
4e ligne : Ch:N_Pv:N_GV:N
C'est OK

Je ne trouve pas l'erreur dans le code ci-joint.

Merçi de votre aide.

// ═══════════════════════════════════════════════════════════════════════════
// SpaDuino v1.84
// ═══════════════════════════════════════════════════════════════════════════
// Corrections et adaptations v1.84 :
//   - Capteur crépusculaire déplacé de A5 vers A0 (conflit I2C corrigé)
//   - Broche 4 (résistance) retirée — pas de chauffage électrique
//   - Bulleur retiré — manuel
//   - Lampe intérieure (A1) retirée — pas installée
//   - Lampe extérieure spa (A2) retirée — pas installée
//   - Lampe jardin (A3) gardée mais désactivée — future installation
//   - Chauffage via Pompe GV uniquement (chaleur moteur)
//   - Pompe GV et PV jamais ensemble — GV remplace PV
//   - Plages filtration : 10h00→10h30 et 18h30→19h00
//   - Circulation minimale : Pompe PV 20 min si aucune pompe depuis 3 heures
//   - Chrono 3 heures remis à zéro si GV ou PV tourne
//   - Lampe jardin : crépusculaire + télécommande (quand installée)
//   - Alimentation générale via horloge mécanique externe 8h00→22h00
// ═══════════════════════════════════════════════════════════════════════════

#include <DallasTemperature.h>             // Sonde température DS18B20
#include <SoftwareSerial.h>                // Module HC-06 BlueTooth
#include <Wire.h>                          // Communications I2C
#include <RTClib.h>                        // Horloge temps réel DS3231
#include <hd44780.h>                       // Bibliothèque LCD hd44780
#include <hd44780ioClass/hd44780_I2Cexp.h> // Contrôle LCD via I2C
#include <OneWire.h>                       // Sonde température OneWire
#include <IRremote.h>                      // ⚠️ Garder la VERSION 3.9.0

hd44780_I2Cexp ecran;

// ═══════════════════════════════════════════════════════════════════════════
// BROCHES
// ═══════════════════════════════════════════════════════════════════════════

const int BROCHE_SONDE_TEMP    = 6;
const int BROCHE_IR            = 9;
const int BROCHE_BT_RX         = 10;
const int BROCHE_BT_TX         = 11;
const int BROCHE_POMPE_PV      = 2;   // Relais N1 → contacteur pompe PV
const int BROCHE_POMPE_GV      = 3;   // Relais N2 → contacteur pompe GV
// Broche 4 retirée — pas de résistance électrique
const int BROCHE_COMMUN_POMPE  = 5;   // Alimentation commune relais
const int BROCHE_CREPUSCULAIRE = A0;  // ✅ Déplacé de A5 — conflit I2C corrigé
// A1 retiré — lampe intérieure non installée
// A2 retiré — lampe extérieure spa non installée
const int BROCHE_LAMPE_JARDIN  = A3;  // Futur — désactivé pour l'instant
// A4 = SDA réservé I2C
// A5 = SCL réservé I2C

// ═══════════════════════════════════════════════════════════════════════════
// CONFIGURATION
// ═══════════════════════════════════════════════════════════════════════════

const int   SEUIL_CREPUSCULAIRE    = 300;
const int   TEMP_MAXI              = 37;
const int   TEMP_MINI              = 28;
const int   TEMP_CONSIGNE_DEFAUT   = 32;
const int   DUREE_FORCEE_POMPE_MIN = 20;  // minutes — jets massage
const int   DUREE_FORCEE_LAMPE_H   = 2;   // heures — lampe jardin forcée
const int   DELAI_FILTRE_JET_J     = 7;   // jours
const int   DELAI_FILTRE_HP_J      = 21;  // jours
const int   HEURE_FIN_ECLAIRAGE    = 23;

// Plages de filtration : 10h00→10h30 et 18h30→19h00
// Format : { heure_debut, minute_debut, heure_fin, minute_fin }
const int PLAGE_FILTRATION[2][4] = {
  { 10,  0, 10, 30 },
  { 18, 30, 19,  0 }
};

// Durée sans activité avant circulation minimale : 3 heures en secondes
const unsigned long DUREE_INACTIVITE_S = 3UL * 3600UL;

// Durée de la circulation minimale : 20 minutes en secondes
const unsigned long DUREE_CIRCULATION_MIN_S = 20UL * 60UL;

const float VERSION = 1.84;

// Codes IR télécommande
const unsigned long IR_MASSAGE1   = 0xFD00FF;
const unsigned long IR_TEMP_PLUS  = 0xFD8877;
const unsigned long IR_TEMP_MOINS = 0xFD9867;
const unsigned long IR_MENU       = 0xFD609F;
const unsigned long IR_JARDIN     = 0xFD50AF; // Lampe jardin (future)
const unsigned long IR_POMPE_PV   = 0xFD30CF;
const unsigned long IR_POMPE_GV   = 0xFD708F;
const unsigned long IR_OK         = 0xFDA857;
const unsigned long IR_ECRAN      = 0xFD40BF;
const unsigned long IR_BUG        = 0xFFFFFFFF;

// ═══════════════════════════════════════════════════════════════════════════
// OBJETS MATÉRIELS
// ═══════════════════════════════════════════════════════════════════════════

OneWire           busTemp(BROCHE_SONDE_TEMP);
DallasTemperature sondeTemp(&busTemp);
SoftwareSerial    bluetooth(BROCHE_BT_RX, BROCHE_BT_TX);
RTC_DS3231        horloge;
IRrecv            recepteurIR(BROCHE_IR);
decode_results    signalIR;

const int LCD_COLS   = 20;
const int LCD_LIGNES = 4;

// ═══════════════════════════════════════════════════════════════════════════
// NOMS DE JOURS EN MÉMOIRE FLASH (PROGMEM)
// ═══════════════════════════════════════════════════════════════════════════

const char j0[] PROGMEM = "Dimanche";
const char j1[] PROGMEM = "Lundi";
const char j2[] PROGMEM = "Mardi";
const char j3[] PROGMEM = "Mercredi";
const char j4[] PROGMEM = "Jeudi";
const char j5[] PROGMEM = "Vendredi";
const char j6[] PROGMEM = "Samedi";
const char* const nomJours[] PROGMEM = { j0, j1, j2, j3, j4, j5, j6 };
char bufJour[10];

void afficherNomJour(uint8_t indexJour) {
  strcpy_P(bufJour, (char*)pgm_read_word(&nomJours[indexJour]));
  ecran.print(bufJour);
}

// ═══════════════════════════════════════════════════════════════════════════
// VARIABLES GLOBALES
// ═══════════════════════════════════════════════════════════════════════════

DateTime maintenant;

int  tempActuelle     = 0;
int  consigneTemp     = TEMP_CONSIGNE_DEFAUT;
int  valeurCrepuscule = 0;

bool chauffageActif   = false; // Pompe GV active pour chauffer
bool eclairageActif   = false;
bool retroEclairage   = true;

// Lampe jardin — future installation
bool lampeForceeJardin = false;
DateTime finMarcheForceeLampe;

// Pompes
bool pompeGvActive    = false;
bool pompePvActive    = false;
bool pompeGvForcee    = false; // jets massage via télécommande
bool pumpPvForcee     = false;
DateTime finMarcheForceePompe;

// Filtration
bool filtrationActive  = false;
bool alerteFiltreJet   = false;
bool alerteFiltreHP    = false;
DateTime prochainNettoyageJet;
DateTime prochainNettoyageHP;

// Circulation minimale (3 heures sans activité)
// dermierActivite : moment où une pompe a tourné pour la dernière fois
DateTime derniereActivite;
bool     circulationMinActive  = false;
DateTime finCirculationMin;

// Délais chauffage sans blocking (millis)
unsigned long tsChauffageAllumage   = 0;
unsigned long tsChauffageExtinction = 0;
bool          enAttenteAllumage     = false;
bool          enAttenteExtinction   = false;

String messageRecu;

// ═══════════════════════════════════════════════════════════════════════════
// UTILITAIRES
// ═══════════════════════════════════════════════════════════════════════════

void afficherDeuxChiffres(int valeur) {
  if (valeur <= 9) ecran.print(F("0"));
  ecran.print(valeur);
}

void afficherHeure(DateTime dt) {
  afficherDeuxChiffres(dt.hour());
  ecran.print(F(":"));
  afficherDeuxChiffres(dt.minute());
  ecran.print(F(":"));
  afficherDeuxChiffres(dt.second());
}

void afficherDateCourte(DateTime dt) {
  if (dt.day()   <= 9) ecran.print('0');
  ecran.print(dt.day());
  ecran.print('/');
  if (dt.month() <= 9) ecran.print('0');
  ecran.print(dt.month());
}

void nettoyerIR() {
  Serial.print(F("nettoyage IR : "));
  Serial.println(signalIR.value, HEX);
  recepteurIR.resume();
  signalIR.value = 0;
  delay(100);
}

void lireBluetooth() {
  messageRecu = "";
  while (bluetooth.available()) {
    delay(3);
    messageRecu += (char)bluetooth.read();
  }
  if (messageRecu.length() > 0)
    Serial.println(messageRecu);
}

void envoyerBluetooth() {
  bluetooth.print("*T" + String(tempActuelle) + "*");
  bluetooth.print("*C" + String(consigneTemp) + "*");
  bluetooth.print(chauffageActif ? "*RR255G0B0*" : "*RR255G255B255*");

  if      (alerteFiltreJet)      bluetooth.print(F("*MNettoyage filtre Jet*"));
  else if (alerteFiltreHP)       bluetooth.print(F("*MNettoyage filtre HP*"));
  else if (circulationMinActive) bluetooth.print(F("*MCirculation minimale*"));
  else if (filtrationActive)     bluetooth.print(F("*MFiltration en cours*"));
  else if (chauffageActif)       bluetooth.print(F("*MChauffage en cours*"));
  else                           bluetooth.print(F("*MSpaduino*"));
}

void lireTemperature() {
  sondeTemp.requestTemperatures();
  tempActuelle = (int)sondeTemp.getTempCByIndex(0);
}

// ═══════════════════════════════════════════════════════════════════════════
// AFFICHAGE LCD
// ═══════════════════════════════════════════════════════════════════════════

// Complète une ligne LCD avec des espaces jusqu'à 20 caractères
// Empêche les résidus de l'affichage précédent de rester visibles
void effacerFinLigne(int nbCaracteresEcrits) {
  for (int i = nbCaracteresEcrits; i < 20; i++)
    ecran.print(' ');
}

// Efface l'ecran et reecrit immediatement le texte fixe ligne 1
// A utiliser partout a la place de ecran.clear()
void effacerEcran() {
  ecran.clear();
  ecran.setCursor(0, 0);
  ecran.print(F("* SpaDuino v1.84  *"));
}

void afficherEtatGeneral() {
  // Ligne physique 1 — texte fixe, jamais modifie ici
  // Gere par effacerEcran() et setup()
}

void afficherDateHeure() {
  // Ligne physique 2 = setCursor(0, 1)
  // "Lun 18/05/26 08:21 " = 19 car. — mise a jour 1x/minute
  static int derniereMinute = -1;
  if (maintenant.minute() == derniereMinute) return;
  derniereMinute = maintenant.minute();

  ecran.setCursor(0, 1);
  strcpy_P(bufJour, (char*)pgm_read_word(&nomJours[maintenant.dayOfTheWeek()]));
  ecran.print(bufJour[0]);
  ecran.print(bufJour[1]);
  ecran.print(bufJour[2]);
  ecran.print(F(" "));
  afficherDeuxChiffres(maintenant.day());
  ecran.print(F("/"));
  afficherDeuxChiffres(maintenant.month());
  ecran.print(F("/"));
  afficherDeuxChiffres(maintenant.year() - 2000);
  ecran.print(F(" "));
  afficherDeuxChiffres(maintenant.hour());
  ecran.print(F(":"));
  afficherDeuxChiffres(maintenant.minute());
  ecran.print(F(" "));
}

void afficherTemperatures() {
  // Ligne 2 — format exact : "Eau:20C  Cons:32C   " = 20 car.
  // "Eau:"(4) + temp(1-3) + "C"(1) + espaces + "Cons:"(5) + cons(2) + "C"(1)
  ecran.setCursor(0, 2);
  ecran.print(F("Eau:"));
  int nbCar = 4; // "Eau:" = 4 caracteres
  if (tempActuelle == -127) {
    ecran.print(F("--- "));
    nbCar += 4;
  } else if (tempActuelle < 10) {
    ecran.print(tempActuelle);
    ecran.print(F("C   "));
    nbCar += 5;
  } else if (tempActuelle < 100) {
    ecran.print(tempActuelle);
    ecran.print(F("C  "));
    nbCar += 5;
  } else {
    ecran.print(tempActuelle);
    ecran.print(F("C "));
    nbCar += 6;
  }
  ecran.print(F("Cons:"));
  ecran.print(consigneTemp);
  ecran.print(F("C   "));
}

void afficherActionneurs() {
  // Ligne 3 — format exact : "Ch:O PV:O GV:O      " = 20 car.
  // Utilise O/N au lieu de OUI/NON pour tenir en 20 colonnes
  ecran.setCursor(0, 3);
  ecran.print(F("Ch:"));
  ecran.print(chauffageActif ? F("O") : F("N"));
  ecran.print(F(" PV:"));
  ecran.print(pompePvActive  ? F("O") : F("N"));
  ecran.print(F(" GV:"));
  ecran.print(pompeGvActive  ? F("O") : F("N"));
  ecran.print(F("            ")); // complete jusqu'a 20 colonnes
}

void afficherEcran() {
  if (maintenant.hour() == 0 && maintenant.minute() == 0 && maintenant.second() == 0)
    effacerEcran();
  afficherEtatGeneral();
  afficherDateHeure();
  afficherTemperatures();
  afficherActionneurs();
}

void afficherDebugSerie() {
  Serial.print(F("Heure : "));
  afficherDeuxChiffres(maintenant.hour());
  Serial.print(F(":"));
  afficherDeuxChiffres(maintenant.minute());
  Serial.print(F(":"));
  afficherDeuxChiffres(maintenant.second());
  Serial.println();
  Serial.print(F("Temperature : "));
  Serial.print(tempActuelle);
  Serial.print(F("C / Consigne : "));
  Serial.print(consigneTemp);
  Serial.println(F("C"));
  Serial.print(F("Pompe PV : "));
  Serial.print(pompePvActive ? F("OUI") : F("NON"));
  Serial.print(F("  Pompe GV : "));
  Serial.print(pompeGvActive ? F("OUI") : F("NON"));
  Serial.print(F("  Chauffage : "));
  Serial.println(chauffageActif ? F("OUI") : F("NON"));
  Serial.print(F("Crepusculaire : "));
  Serial.println(valeurCrepuscule);
}

// ═══════════════════════════════════════════════════════════════════════════
// GESTION FILTRATION
// Plages : 10h00→10h30 et 18h30→19h00
// ═══════════════════════════════════════════════════════════════════════════

void gererAlerteFiltre() {
  if (maintenant.unixtime() >= prochainNettoyageHP.unixtime())
    alerteFiltreHP = true;
  if (maintenant.unixtime() >= prochainNettoyageJet.unixtime())
    alerteFiltreJet = true;
}

void gererFiltration() {
  // Vérifie si on est dans une plage de filtration
  // Plage 1 : 10h00 → 10h30
  bool plage1 = (maintenant.hour() == PLAGE_FILTRATION[0][0]
                 && maintenant.minute() >= PLAGE_FILTRATION[0][1]
                 && maintenant.minute() <  PLAGE_FILTRATION[0][3])
                || (maintenant.hour() == PLAGE_FILTRATION[0][2]
                    && maintenant.minute() < PLAGE_FILTRATION[0][3]);

  // Correction plage 1 : heure début = heure fin (10h), on compare les minutes
  bool dansPlage1 = (maintenant.hour() == 10
                     && maintenant.minute() >= 0
                     && maintenant.minute() < 30);

  // Plage 2 : 18h30 → 19h00
  bool dansPlage2 = (maintenant.hour() == 18 && maintenant.minute() >= 30)
                    || (maintenant.hour() == 19 && maintenant.minute() < 0);

  // Correction plage 2 : 18h30 → 19h00
  dansPlage2 = (maintenant.hour() == 18 && maintenant.minute() >= 30)
               || (maintenant.hour() == 19 && maintenant.minute() == 0
                   && maintenant.second() == 0);

  filtrationActive = dansPlage1 || dansPlage2;
}

// ═══════════════════════════════════════════════════════════════════════════
// GESTION ÉCLAIRAGE
// Lampe jardin : crépusculaire automatique + télécommande
// Lampe intérieure et extérieure spa : non installées
// ═══════════════════════════════════════════════════════════════════════════

void gererEclairage() {
  valeurCrepuscule = analogRead(BROCHE_CREPUSCULAIRE);

  bool nuit = (valeurCrepuscule <= SEUIL_CREPUSCULAIRE
               && maintenant.hour() >= 17
               && maintenant.hour() < HEURE_FIN_ECLAIRAGE);

  // ── Lampe jardin (A3) ────────────────────────────────────────────────────
  // DÉSACTIVÉE pour l'instant — décommenter quand installée :
  // bool allumerJardin = nuit || lampeForceeJardin;
  // digitalWrite(BROCHE_LAMPE_JARDIN, allumerJardin ? LOW : HIGH);
  // eclairageActif = allumerJardin;

  // Arrêt automatique lampe jardin forcée
  if (maintenant.unixtime() >= finMarcheForceeLampe.unixtime() && lampeForceeJardin) {
    // digitalWrite(BROCHE_LAMPE_JARDIN, HIGH); // décommenter quand installée
    lampeForceeJardin = false;
  }

  eclairageActif = false; // mettre à jour quand lampe jardin installée
}

// ═══════════════════════════════════════════════════════════════════════════
// GESTION POMPES
// Règle : Pompe GV et PV jamais ensemble — GV remplace PV
// ═══════════════════════════════════════════════════════════════════════════

// Démarre la Pompe PV — vérifie que GV ne tourne pas
void demarrerPompePV() {
  if (pompeGvActive) return; // GV prioritaire — PV ne démarre pas
  pompePvActive = true;
  digitalWrite(BROCHE_POMPE_PV,     HIGH);
  digitalWrite(BROCHE_COMMUN_POMPE, HIGH);
  derniereActivite = maintenant; // remet le chrono d'inactivité à zéro
}

// Arrête la Pompe PV
void arreterPompePV() {
  pompePvActive = false;
  digitalWrite(BROCHE_POMPE_PV,     LOW);
  // Ne coupe pas COMMUN_POMPE si GV pourrait démarrer ensuite
  if (!pompeGvActive)
    digitalWrite(BROCHE_COMMUN_POMPE, LOW);
}

// Démarre la Pompe GV — arrête PV si elle tournait
void demarrerPompeGV() {
  if (pompePvActive) arreterPompePV(); // GV remplace PV
  pompeGvActive = true;
  chauffageActif = false; // sera réévalué dans gererChauffage
  digitalWrite(BROCHE_POMPE_GV,     HIGH);
  digitalWrite(BROCHE_COMMUN_POMPE, HIGH);
  derniereActivite = maintenant; // remet le chrono d'inactivité à zéro
}

// Arrête la Pompe GV
void arreterPompeGV() {
  pompeGvActive  = false;
  pompeGvForcee  = false;
  chauffageActif = false;
  digitalWrite(BROCHE_POMPE_GV,     LOW);
  digitalWrite(BROCHE_COMMUN_POMPE, LOW);
}

void gererPompes() {
  // Arrêt automatique marche forcée (jets massage)
  if (maintenant.unixtime() >= finMarcheForceePompe.unixtime()
      && (pumpPvForcee || pompeGvForcee)) {
    pumpPvForcee  = false;
    pompeGvForcee = false;
    if (!filtrationActive) arreterPompeGV();
  }

  // Filtration — Pompe PV si pas de GV active
  if (filtrationActive && !pompeGvActive && !pompePvActive)
    demarrerPompePV();
  if (!filtrationActive && pompePvActive && !pumpPvForcee)
    arreterPompePV();
}

// ═══════════════════════════════════════════════════════════════════════════
// GESTION CHAUFFAGE
// Chauffage = Pompe GV (chaleur moteur)
// GV démarre si temp < consigne-1, s'arrête si temp > consigne+1
// ═══════════════════════════════════════════════════════════════════════════

void gererChauffage() {
  // Demande de chauffage — démarrer Pompe GV après 3 secondes
  // Sonde invalide (-127) — on ne chauffe pas
  if (tempActuelle == -127) return;
  if (tempActuelle <= consigneTemp - 1 && !pompeGvActive && !enAttenteAllumage
      && !pompeGvForcee) {
    enAttenteAllumage   = true;
    tsChauffageAllumage = millis();
  }

  if (enAttenteAllumage && (millis() - tsChauffageAllumage >= 3000UL)) {
    enAttenteAllumage = false;
    demarrerPompeGV();
    chauffageActif = true;
    Serial.println(F("Chauffage : Pompe GV démarrée"));
  }

  // Fin de chauffage — arrêter Pompe GV après 5 secondes
  if (tempActuelle >= consigneTemp + 1 && chauffageActif && !enAttenteExtinction
      && !pompeGvForcee) {
    enAttenteExtinction   = true;
    tsChauffageExtinction = millis();
  }

  if (enAttenteExtinction && (millis() - tsChauffageExtinction >= 5000UL)) {
    enAttenteExtinction = false;
    chauffageActif      = false;
    if (!pompeGvForcee) {
      arreterPompeGV();
      Serial.println(F("Chauffage : Pompe GV arretee"));
    }
  }
}

// ═══════════════════════════════════════════════════════════════════════════
// CIRCULATION MINIMALE
// Si aucune pompe n'a tourné depuis 3 heures → Pompe PV pendant 20 minutes
// Le chrono repart à zéro dès qu'une pompe tourne
// ═══════════════════════════════════════════════════════════════════════════

void gererCirculationMinimale() {
  // Si une pompe tourne, on n'a pas besoin de circulation minimale
  if (pompePvActive || pompeGvActive) {
    derniereActivite     = maintenant; // remet le chrono à zéro
    circulationMinActive = false;
    return;
  }

  // Vérifier si 3 heures se sont écoulées sans activité
  unsigned long inactivite = maintenant.unixtime() - derniereActivite.unixtime();

  if (!circulationMinActive && inactivite >= DUREE_INACTIVITE_S) {
    // Démarrer circulation minimale
    circulationMinActive = true;
    finCirculationMin    = maintenant + TimeSpan(0, 0, (int)DUREE_CIRCULATION_MIN_S / 60, 0);
    demarrerPompePV();
    Serial.println(F("Circulation minimale : Pompe PV démarrée"));
  }

  // Arrêter circulation minimale après 20 minutes
  if (circulationMinActive && maintenant.unixtime() >= finCirculationMin.unixtime()) {
    circulationMinActive = false;
    arreterPompePV();
    derniereActivite = maintenant; // remet le chrono à zéro
    Serial.println(F("Circulation minimale : Pompe PV arretee"));
  }
}

// ═══════════════════════════════════════════════════════════════════════════
// MENU FILTRE
// ═══════════════════════════════════════════════════════════════════════════

void afficherMenuFiltre() {
  ecran.setCursor(0, 0);
  ecran.print(F("  Nettoyage Filtre  "));
  ecran.setCursor(0, 1);
  ecran.print(F("Jet:"));
  afficherDateCourte(prochainNettoyageJet);
  ecran.print(F(" - HP:"));
  afficherDateCourte(prochainNettoyageHP);
  ecran.setCursor(0, 2);
  ecran.print(F("1:Jet 2:HP - V"));
  ecran.print(VERSION);
  ecran.setCursor(0, 3);
  ecran.print(F("3:Ecran  OK:Exit    "));
}

void afficherConfirmationRAZ() {
  effacerEcran();
  ecran.setCursor(0, 0);
  ecran.print(F("Confirmer RAZ?      "));
  ecran.setCursor(0, 1);
  ecran.print(F("OK=Oui  0=Annuler   "));
}

bool attendreConfirmation() {
  bool reponse   = false;
  bool enAttente = true;
  while (enAttente) {
    lireBluetooth();
    if (recepteurIR.decode(&signalIR) || messageRecu.length() > 0) {
      if (signalIR.value == IR_OK || messageRecu == "OK") {
        reponse   = true;
        enAttente = false;
      } else if (signalIR.value == IR_BUG) {
        nettoyerIR();
      } else {
        reponse   = false;
        enAttente = false;
      }
    }
    delay(100);
  }
  delay(100);
  recepteurIR.resume();
  effacerEcran();
  return reponse;
}

void menuFiltre() {
  maintenant = horloge.now();
  effacerEcran();
  afficherMenuFiltre();
  nettoyerIR();

  bool dansMenu = true;
  while (dansMenu) {
    lireBluetooth();
    if (recepteurIR.decode(&signalIR) || messageRecu.length() > 0) {

      if (signalIR.value == IR_MASSAGE1 || messageRecu == "1") {
        nettoyerIR();
        afficherConfirmationRAZ();
        if (attendreConfirmation()) {
          alerteFiltreJet      = false;
          prochainNettoyageJet = maintenant + TimeSpan(DELAI_FILTRE_JET_J, 0, 0, 0);
          Serial.println(F("RAZ filtre Jet OK"));
        }
        dansMenu = false;

      } else if (signalIR.value == 0xFD807F || messageRecu == "2") {
        nettoyerIR();
        afficherConfirmationRAZ();
        if (attendreConfirmation()) {
          alerteFiltreHP      = false;
          prochainNettoyageHP = maintenant + TimeSpan(DELAI_FILTRE_HP_J, 0, 0, 0);
          Serial.println(F("RAZ filtre HP OK"));
        }
        dansMenu = false;

      } else if (signalIR.value == IR_OK || messageRecu == "OK") {
        nettoyerIR();
        effacerEcran();
        dansMenu = false;

      } else if (signalIR.value == IR_ECRAN || messageRecu == "3") {
        nettoyerIR();
        retroEclairage = !retroEclairage;
        ecran.setBacklight(retroEclairage ? HIGH : LOW);
        effacerEcran();
        dansMenu = false;

      } else if (signalIR.value == IR_BUG) {
        nettoyerIR();
      }
    }
    delay(100);
  }
}

// ═══════════════════════════════════════════════════════════════════════════
// PROGRAMME MASSAGE (Jets — Pompe GV forcée 20 minutes)
// ═══════════════════════════════════════════════════════════════════════════

void arreterMassage() {
  if (!chauffageActif) arreterPompeGV();
  pompeGvForcee = false;
  effacerEcran();
  Serial.println(F("Massage termine"));
}

void programmeMassage1() {
  const int CONSIGNE_TEMP_MASSAGE = 35;

  messageRecu = "";
  maintenant  = horloge.now();
  DateTime heureFinMassage = maintenant + TimeSpan(0, 0, DUREE_FORCEE_POMPE_MIN, 0);

  effacerEcran();
  ecran.setCursor(0, 0);
  ecran.print(F("**Cycle Massage 1***"));

  // Démarrer Pompe GV pour les jets
  demarrerPompeGV();
  pompeGvForcee = true;

  bool massageEnCours = true;
  while (massageEnCours) {
    lireTemperature();
    maintenant = horloge.now();

    ecran.setCursor(0, 1);
    ecran.print(F("Il est : "));
    afficherHeure(maintenant);
    ecran.setCursor(0, 2);
    ecran.print(F("Fin a  : "));
    afficherHeure(heureFinMassage);
    ecran.setCursor(0, 3);
  ecran.print(F("Eau:"));
  if (tempActuelle == -127) {
    ecran.print(F("Err "));
  } else {
    ecran.print(tempActuelle);
    ecran.print(F("C   "));
  }
  ecran.print(F("Cons:"));
  ecran.print(consigneTemp);
  ecran.print(F("C   "));

    lireBluetooth();

    // Fin par le temps
    if (maintenant.unixtime() >= heureFinMassage.unixtime())
      massageEnCours = false;

    // Annulation télécommande
    if (recepteurIR.decode(&signalIR) || messageRecu.length() > 0) {
      if (signalIR.value == IR_MASSAGE1 || messageRecu == "massage1") {
        delay(100);
        recepteurIR.resume();
        massageEnCours = false;
      } else if (signalIR.value == IR_BUG) {
        nettoyerIR();
      }
    }

    messageRecu = "";
    delay(10);
  }

  arreterMassage();
}

// ═══════════════════════════════════════════════════════════════════════════
// GESTION DES COMMANDES IR / BLUETOOTH
// ═══════════════════════════════════════════════════════════════════════════

void gererCommandes() {
  lireBluetooth();
  if (!recepteurIR.decode(&signalIR) && messageRecu.length() == 0) return;

  Serial.print(F("Code IR : 0x"));
  Serial.println(signalIR.value, HEX);
  Serial.print(F("Message BT : "));
  Serial.println(messageRecu);

  // ── Massage / Jets ───────────────────────────────────────────────────────
  if (signalIR.value == IR_MASSAGE1 || messageRecu == "massage1") {
    effacerEcran();
    recepteurIR.resume();
    programmeMassage1();
  }

  // ── Consigne température ─────────────────────────────────────────────────
  if (signalIR.value == IR_TEMP_PLUS || messageRecu == "temp+") {
    if (consigneTemp < TEMP_MAXI) {
      consigneTemp++;
      Serial.print(F("Consigne temp : "));
      Serial.println(consigneTemp);
    }
  }
  if (signalIR.value == IR_TEMP_MOINS || messageRecu == "temp-") {
    if (consigneTemp > TEMP_MINI) {
      consigneTemp--;
      Serial.print(F("Consigne temp : "));
      Serial.println(consigneTemp);
    }
  }

  // ── Menu filtre ──────────────────────────────────────────────────────────
  if (signalIR.value == IR_MENU || messageRecu == "menu") {
    effacerEcran();
    recepteurIR.resume();
    menuFiltre();
  }

  // ── Lampe jardin (future installation) ───────────────────────────────────
  // DÉSACTIVÉ — décommenter quand lampe jardin installée sur A3 :
  /*
  if (signalIR.value == IR_JARDIN || messageRecu == "lampeJardin") {
    lampeForceeJardin = !lampeForceeJardin;
    if (lampeForceeJardin) {
      finMarcheForceeLampe = maintenant + TimeSpan(0, DUREE_FORCEE_LAMPE_H, 0, 0);
      digitalWrite(BROCHE_LAMPE_JARDIN, LOW);
    } else {
      digitalWrite(BROCHE_LAMPE_JARDIN, HIGH);
    }
  }
  */

  // ── Pompe petite vitesse forcée ──────────────────────────────────────────
  if (signalIR.value == IR_POMPE_PV || messageRecu == "pompePV") {
    if (!pumpPvForcee && !pompeGvActive) {
      finMarcheForceePompe = maintenant + TimeSpan(0, 0, DUREE_FORCEE_POMPE_MIN, 0);
      pumpPvForcee = true;
      demarrerPompePV();
      Serial.println(F("Pompe PV forcee ON"));
    } else {
      pumpPvForcee = false;
      if (!filtrationActive) arreterPompePV();
      Serial.println(F("Pompe PV forcee OFF"));
    }
  }

  // ── Pompe grande vitesse forcée (Jets) ───────────────────────────────────
  if (signalIR.value == IR_POMPE_GV || messageRecu == "pompeGV") {
    if (!pompeGvForcee) {
      finMarcheForceePompe = maintenant + TimeSpan(0, 0, DUREE_FORCEE_POMPE_MIN, 0);
      pompeGvForcee = true;
      demarrerPompeGV();
      Serial.println(F("Pompe GV forcee ON"));
    } else {
      pompeGvForcee = false;
      if (!chauffageActif) arreterPompeGV();
      Serial.println(F("Pompe GV forcee OFF"));
    }
  }

  // ── Rétroéclairage LCD ───────────────────────────────────────────────────
  if (signalIR.value == IR_ECRAN || messageRecu == "ecran") {
    retroEclairage = !retroEclairage;
    ecran.setBacklight(retroEclairage ? HIGH : LOW);
  }

  recepteurIR.resume();
  nettoyerIR();
  messageRecu = "";
}

// ═══════════════════════════════════════════════════════════════════════════
// SETUP
// ═══════════════════════════════════════════════════════════════════════════

void setup() {
  Serial.begin(115200);

  int erreurLcd = ecran.begin(LCD_COLS, LCD_LIGNES);
  if (erreurLcd) {
    Serial.print(F("Erreur init LCD : "));
    Serial.println(erreurLcd);
    hd44780::fatalError(erreurLcd);
  }
  ecran.setBacklight(HIGH);
  effacerEcran();
  ecran.setCursor(0, 0);
  ecran.print(F("  SpaDuino v"));
  ecran.print(VERSION);
  ecran.setCursor(0, 1);
  ecran.print(F("  Initialisation..."));

  sondeTemp.begin();

  pinMode(BROCHE_POMPE_PV,     OUTPUT); digitalWrite(BROCHE_POMPE_PV,     LOW);
  pinMode(BROCHE_POMPE_GV,     OUTPUT); digitalWrite(BROCHE_POMPE_GV,     LOW);
  pinMode(BROCHE_COMMUN_POMPE, OUTPUT); digitalWrite(BROCHE_COMMUN_POMPE, LOW);
  // Lampe jardin — future installation
  // pinMode(BROCHE_LAMPE_JARDIN, OUTPUT); digitalWrite(BROCHE_LAMPE_JARDIN, HIGH);

  recepteurIR.enableIRIn();
  bluetooth.begin(9600);
  delay(1000);

  // Initialisation du DS3231 avec vérification
  // Si le DS3231 ne répond pas, on affiche un message clair au lieu de bloquer
  if (!horloge.begin()) {
    Serial.println(F("ERREUR : DS3231 non trouve !"));
    Serial.println(F("Verifiez SDA et SCL du DS3231."));
    ecran.setCursor(0, 2);
    ecran.print(F("ERREUR : Horloge!   "));
    ecran.setCursor(0, 3);
    ecran.print(F("Verif. SDA/SCL RTC  "));
    // On ne bloque pas — on continue avec une heure par défaut
    // pour que le reste du programme fonctionne quand même
  } else {
    Serial.println(F("DS3231 OK"));

    // Décommenter UNE SEULE FOIS pour remettre l'horloge à l'heure :
    // horloge.adjust(DateTime(F(__DATE__), F(__TIME__)));
  }

  maintenant       = horloge.now();
  derniereActivite = maintenant; // démarre le chrono d'inactivité

  prochainNettoyageJet = maintenant + TimeSpan(DELAI_FILTRE_JET_J, 0, 0, 0);
  prochainNettoyageHP  = maintenant + TimeSpan(DELAI_FILTRE_HP_J,  0, 0, 0);

  ecran.setCursor(0, 2);
  ecran.print(F("  Heure : "));
  afficherDeuxChiffres(maintenant.hour());
  ecran.print(F(":"));
  afficherDeuxChiffres(maintenant.minute());

  delay(2000);
  effacerEcran(); // efface + ecrit texte fixe ligne 1

  Serial.print(F("SpaDuino Version "));
  Serial.println(VERSION);
  Serial.println(F("Initialisation OK"));
}

// ═══════════════════════════════════════════════════════════════════════════
// LOOP
// ═══════════════════════════════════════════════════════════════════════════

void loop() {
  lireTemperature();
  envoyerBluetooth();

  maintenant = horloge.now();

  afficherEcran();
  afficherDebugSerie();

  gererCommandes();

  gererAlerteFiltre();
  gererFiltration();
  gererEclairage();
  gererPompes();
  gererChauffage();
  gererCirculationMinimale();
}

:warning:
Post mis dans la mauvaise section, on parle anglais dans les forums généraux. déplacé vers le forum francophone.

Merci de prendre en compte les recommandations listées dans Les bonnes pratiques du Forum Francophone

Bonjour,
On préfère généralement que vous postiez votre programme au format texte entre les balises codes (dans le menu d'édition d'un message, l'icone <CODE/> en haut), plutôt qu'en fichier (peu pratique sur smartphone et certains aidants refusent de télécharger par sécurité)

Le programme est assez long, quoique digeste - la partie intéressante est située L231 à L327, qui gère l'affichage... Vous n'utilisez la fonction effacerFinLigne nul part, peut-être qu'il s'agit d'un oubli (d'ailleurs afficherEtatGeneral ne sert à rien, elle est vide) ? Elle vous permettrait, si j'en crois le nom, de supprimer proprement les caractères résiduels après écrasement d'une ancienne ligne (en parallèle il faudrait checker où sont les clear, et à quel moment/sur quel menu ils sont appelés - c'est un peu un plat de spaghetti, on y verrait plus clair sur un exemple minimaliste avec uniquement l'affichage des menus et des valeurs fictives par exemple)


Une question tout de même, avez-vous créé ce programme seul ? Entre autres choses, les commentaires et les émojis me font penser à de l'IA, mais je peux me tromper - sachez juste que dans ce cas, vous aurez du mal à faire autre chose qu'envoyer prompt sur prompt, ce qui, en plus d'être inefficace, ne vous apportera aucune connaissance pour modifier le programme vous même. Si c'est votre production, mea culpa :slight_smile:

Cdlt,
Pandaroux007

C’est sur que ce genre de remarque apporte une solution constructive à la question de @delium55.

  strcpy_P(bufJour, (char*)pgm_read_word(&nomJours[maintenant.dayOfTheWeek()]));
  ecran.print(bufJour[0]);
  ecran.print(bufJour[1]);
  ecran.print(bufJour[2]);

Ceci me parait bizarre.

  • bufjour contient une chaine de caractères
  • print envoie des caractères jusqu'à rencontrer un terminateur de chaine \0

==> pour moi les 3 print ci-dessus copient 3 fois la chaine.
Il serait plus logique de faire tout simplement un ecran.write(bufJour, 3); qui ferait imprimer les 3 premiers caractères de la chaine pointée par bufjour.


void afficherDebugSerie() {
  Serial.print(F("Heure : "));
  afficherDeuxChiffres(maintenant.hour());
  Serial.print(F(":"));
  afficherDeuxChiffres(maintenant.minute());
  Serial.print(F(":"));
  afficherDeuxChiffres(maintenant.second());

Dans afficherDebugSerie tu appelles afficherDeuxChiffres() qui peut afficher n'importe oú sur l'écran au lieu d'envoyer sur Serial.


Autres trucs pas très logique non plus c'est que tu définis des fonctions afficheHeure(), afficherDateCourte(), ... mais que tu ne les utilises pas et tu refais l'équivalent dans la mise à jour de l'afficheur.

Si on va par là, ta remarque n'est pas plus constructive ....

Au moins ca équilibre le débat :wink:

À UKHeliBob
Merçi pour le tranfert sur le bon forum.

Bonjour pandaroux007,

Effectivement le code aurait du être mis en les balises au départ, merçi.

Pour ce qui est du programme, au départ j'ai utilisé le programme de NLeclerc sur ce fils de discussion Gestion d'un Spa/Jacuzzi via un Arduino = SpaDuino ou JakUino - Français - Arduino Forum puis J-M-L Jackson m'a proposé un programme créé pa IA suivants ses directives. Contrôle moteur de spa - #30 by delium55

Le premier problème d'affichage en était de branchement entre le UNO, le 2C et le Ds3231 que j,ai réglé avec l'aide de Gimini (IA) et il reste ce problème de superposition d'affichage sur la ligne 1 qui demeure. Plusieurs choses ont été vérifié avec IA mais sans résultat. Voilà pour la mise en contexte.

Salutations,

Bonjour fdufnews,

C'est une partie du code que l'IA a modifié.
Le problème d'affichage est que sur la ligne1 affiche 2 fois l'heure 1315251131525* par dessus Spaduino_v1.85

Je me demande si c'est un défaut de fabrication de l'écran LCD20x4 car les 3 autres lignes s'affichent correctement.

AfficheDateCourte était dans le programme initial, je n'y ai pas touché, pourrait être éliminé si inutile.

Bonjour EGT59,

Cela m'aura permis de régler les problèmes de branchements entre les différentes composantes du panneau de contrôle.

Salutations,

Justement, si le reste de l'afficheur se comporte de manière standard, je serais plus enclin à me demander si le problème ne se situe pas entre la chaise et le clavier.


Et il reste toujours le problème que j'ai relevé dans afficherDebugSerie() qui fait des écritures dans l'afficheur au lieu de les faire sur Serial.

Bonjour fdufnews,

Encore là c'était une proposition de l'IA qui a modifié le code.

Oui mais il faut quand même analyser un peu le code fourni par l'IA pour éviter justement se genre de désagréments.

À fdufnews,

Je comprends, mais pour un débutant ce n'est pas évident surtout lorsque les explications semblent logiques.
Et puis le problème entre la chaise et le clavier c'est le que dernier cours d'informatique que j'ai suivi au début des années 70 (Introduction au langage Fortan avec des cartes perforées) ne m'apporte pas un grand support pour le langage C.

Bonjour fdufnews,

Je crois avoir trouvé après quelques recherches.
Sur le branchement du régulateur du LCD j'ai installé temporairement 2 résistances de 4.7k entre SCl et Vcc et entre SDA et Vcc et l'affichage sur la ligne 1 est devenue correcte.

Je me demande s'il y a une manière plus propre de le faire. Si vous avez une photo d'un montage adéquat je serais preneur.

Concernant le code, si vous avez des idées pour l'améliorer
je suis partant.

Merçi encore pour votre aide.

C'est clair, une bonne méthode est de demandé à l'IA, une analyse du code et si c'est la meilleur façon de faire et si elle respecte les bonnes pratiques dans le domaine.
L'IA n'a pas d'égo est peu tout a fait démonter son propre code :slight_smile:
Ne pas hésiter non plus a repartir d'un contexte neuf et reformuler la question, si les échanges s'éternise.

C'est alors que tu as oublié ce que tu avais appris, les deux langages reste quand même des langage fortement typé et procédurale, non ?

Hello, étrange, les lignes sda et scl sont habituellement tirées vers le haut...

Edit: en regardant ta photo, tes résistances sont soudees sur VCC .(Et non sur GND comme tu le dis en#16)

Merci dfgh,

Ils sont bien sur Vcc, j'ai corrigé la discussion #16.

À terwal,

Voilà, la mémoire est une faculté qui oublie.