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();
}

