/*******************************************************************************************\
|*                                                                                         *|
|*                Horloge LCD 4x20 pilotée par un serveur NTP (Liaison I2C).               *|
|*                                                                                         *|
\*******************************************************************************************/


/*******************************************************************************************\
|* Liste des Bibliothèques.                                                                *|
\*******************************************************************************************/

#include <WiFi.h> // Bibliothèque de raccordement au réseau WiFi
#include <LiquidCrystal_I2C.h> // Bibliothèque de gestion des afficheurs LCD alphanumériques
#include "time.h" // Bibliothèque de gestion du temps
#include "esp_sntp.h" // Bibliothèque du serveur NTP


/*******************************************************************************************\
|* Paramètrage de l'afficheur LCD : bus I2C, adresse 0x27, 20 caractères et 4 lignes       *|
\*******************************************************************************************/

  LiquidCrystal_I2C LCD(0x27,20,4) ; // Paramètrage de l'afficheur LCD : 0x27 d'adresse, 20 caractères et 4 lignes


/*******************************************************************************************\
 * Liste des Variables.                                                                    *|
\*******************************************************************************************/

// Variables d'Entrées/Sorties
  const int8_t BP_backlight = 14 ; // Bouton poussoir de commande du rétroéclairage de l'afficheur LCD
  bool status_BP_backlight = false ; // État du bouton poussoir de commande du rétroéclairage de l'afficheur LCD
  bool LCD_backlight = true ; // État du rétroéclairage de l'afficheur LCD

// Variables de communication
  const char ssid[32] = "Wokwi-GUEST" ; // Nom du réseau WiFi
  const char password[32] = "" ; // Mot de passe du réseau WiFi
  const char hostname[32] = "Horloge ESP32" ; // Nom de l'hôte sur le réseau WiFi
  const char ntpServer[32] = "pool.ntp.org" ; // Adresse du serveur NTP
  const char timeZone[64] = "CET-1CEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00" ; // Définition du fuseau horaire (voir https://sites.google.com/a/usapiens.com/opnode/time-zones)

// Variables temporelles
  const int16_t NTPmaj = 20 ; // Période de mise à jour sur le serveur NTP (en secondes)
  struct tm timeinfo ; // Structure des informations du temps récupéré sur le serveur NTP
  int16_t Heure, Minute, Seconde, JourdelaSemaine, Jour, Mois, Annee ; // Variables date et heures
  String JourdelaSemaine_en_Texte[7] = {"LUNDI","MARDI","MERCREDI","JEUDI","VENDREDI","SAMEDI", "DIMANCHE"} ;
  String Mois_en_Texte[12] = {"JANVIER","FEVRIER","MARS","AVRIL","MAI","JUIN","JUILLET","AOUT","SEPTEMBRE","OCTOBRE","NOVEMBRE","DECEMBRE"} ;
  uint64_t TON1 = 0 ; // Valeur à atteindre par la temporisation n° 1
  uint64_t TON2 = 0 ; // Valeur à atteindre par la temporisation n° 2

// Caractères customisés
  byte x10[8] = {0x07,0x07,0x07,0x00,0x00,0x00,0x00,0x00} ;
  byte x11[8] = {0x1F,0x1F,0x1F,0x00,0x00,0x00,0x00,0x00} ;
  byte x12[8] = {0x1C,0x1C,0x1C,0x00,0x00,0x00,0x00,0x00} ;
  byte x13[8] = {0x07,0x07,0x07,0x07,0x07,0x07,0x07,0x07} ;
  byte x14[8] = {0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C} ;
  byte x15[8] = {0x1F,0x1F,0x1F,0x07,0x07,0x07,0x07,0x07} ;
  byte x16[8] = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00} ;
  byte x17[8] = {0x00,0x00,0x00,0x00,0x0E,0x0E,0x0E,0x00} ;

// Variables d'affichage
  String Date ; // Date à afficher sur le LCD
  String Date_Old ; // Date affichée précédemment
  int16_t iCaractere = 0 ; // Premier caractère à afficher
  bool notSyncNTP = false ; // Notification de synchronisation au serveur NTP


/*******************************************************************************************\
 * Programme d'initialisation à n'exécuter qu'une seule fois.                              *|
\*******************************************************************************************/

void setup() // Programme d'initialisation à n'exécuter qu'une seule fois
{
  // Mise en service de la communication série vers la console
    Serial.begin(115200) ; // Vitesse de communication : 115 200 Bauds

  // Initialisation des entrées/sorties
    pinMode (BP_backlight, INPUT_PULLUP) ; // Entrée du bouton poussoir de commande du rétroéclairage de l'afficheur LCD

  // Configuration de l'afficheur LCD
    LCD_config() ; // Configuration de l'afficheur LCD
    
  // Connexion au réseau WiFi
    WiFi_connect() ; // Connexion au réseau WiFi

  // Initialisation de la synchronisation avec le serveur NTP
    NTP_init() ; // Initialisation de la synchronisation avec le serveur NTP
}


/*******************************************************************************************\
|* Programme principal à exécuter en boucle répétitive                                     *|
\*******************************************************************************************/

void loop() // Programme principal à exécuter en boucle répétitive
{
  // Affichage de la notification de synchronisation au serveur NTP
    if (notSyncNTP) printSyncNTP() ; // Affichage de la notification de synchronisation au serveur NTP
     
  // Détermination et affichage de la date et de l'heure locale
    if (Tempo1(250)) // Période de 0,25 secondes
    {
      // Détermination de la date et de l'heure locale
        localTime() ; // Détermination de l'heure et de la date locale
      // Affichage de la date et de l'heure
        printLocalTime() ; // Affichage de la date et de l'heure
  }

  // Gestion de l'allumage et de l'extinction du rétroéclairage de l'afficheur LCD
    if (backlightLCD()) LCD.backlight() ; // Allumage du rétroéclairage de l'afficheur LCD
    else LCD.noBacklight() ; // Extinction du rétroéclairage de l'afficheur LCD
}


/*******************************************************************************************\
| Fonction de configuration de l'afficheur LCD.                                            *|
\*******************************************************************************************/

void LCD_config() // Fonction de configuration de l'afficheur LCD 
{ 
  // Initilisation de l'afficheur
    LCD.init() ; // Initialisation de l'afficheur LCD
    LCD.backlight() ; // Activation du rétro-éclairage
    LCD.clear() ; // Effacement de l'afficheur LCD
  
  // Transfert des caractères customisés
    LCD.createChar(0,x10) ; // Transfert du caractère customisé n° 0
    LCD.createChar(1,x11) ; // Transfert du caractère customisé n° 1
    LCD.createChar(2,x12) ; // Transfert du caractère customisé n° 2
    LCD.createChar(3,x13) ; // Transfert du caractère customisé n° 3
    LCD.createChar(4,x14) ; // Transfert du caractère customisé n° 4
    LCD.createChar(5,x15) ; // Transfert du caractère customisé n° 5
    LCD.createChar(6,x16) ; // Transfert du caractère customisé n° 6
    LCD.createChar(7,x17) ; // Transfert du caractère customisé n° 7
}


/*******************************************************************************************\
| Fonction de connexion au réseau WiFi.                                                    *|
\*******************************************************************************************/

void WiFi_connect() // Fonction de connexion au réseau WiFi
{
  // Initialisation du WiFi
    WiFi.mode(WIFI_STA) ; // Définition du mode station du WiFi
    WiFi.setHostname(hostname) ; // Définition du nom de l'hôte sur le réseau WiFi
  
  // Connexion au réseau WiFi
    Serial.printf("Connexion au : %s", ssid) ; // Affichage du SSID de connexion
    Serial.println() ;
    LCD.setCursor(0,0) ; LCD.print("Connexion au : ") ; LCD.setCursor(0,1) ; LCD.print(ssid) ; // Affichage LCD du réseau à connecter
    WiFi.begin (ssid, password) ; // Connexion au réseau WiFi
    LCD.setCursor(0,2) ; // Passage à la ligne suivante du LCD
    int i = 0 ;
    while (WiFi.status() != WL_CONNECTED) // Attente de connexion
    {
      Serial.print("*") ; // Affichage LCD d'une *
      if (i > 20) // Le curseur est en bout de ligne ?
      {
        LCD.setCursor(0,2) ;
        for(int j = 0 ; j < 20 ; j ++) LCD.print(" ") ; // Effacement de la ligne        
        LCD.setCursor(0,2) ;
        i = 0 ;
      }
      else i ++ ;      
      delay(100) ; // Attente de 0,1 secondes
    }
  
  delay(1000) ; // Attente de 1 seconde
  
  // Affichage, sur le Terminal Série, des informations de connexion    
    Serial.println("") ;
    Serial.printf(R"(
Détails de la connexion :
-----------------------------------------------------
  Nom du Réseau WiFi   : %s
  Nom de l'Hôte        : %s
  Adresse IP           : %s
  Adresse Mac          : %s
  Force du signal WiFi : %d %%

      )", WiFi.SSID().c_str(),
          WiFi.getHostname(),
          WiFi.localIP().toString().c_str(),
          WiFi.macAddress().c_str(),
          WiFi.RSSI()) ;
  
  // Affichage LCD des informations de connexion   
    LCD.clear() ; // Effacement de l'afficheur LCD
    LCD.setCursor(0,0) ; LCD.print(WiFi.getHostname()) ; // Affichage LCD de la connexion WiFi
    LCD.setCursor(0,1) ; LCD.print("        est connecte") ; 
    LCD.setCursor(0,2) ; LCD.printf("WiFi : %s", WiFi.SSID()) ; // Affichage LCD du nom du réseau
    LCD.setCursor(0,3) ; LCD.printf("IP : %s", WiFi.localIP().toString().c_str()) ; // Affichage LCD de l'adresse IP
  
  delay(5000) ; // Attente de 5 secondes
  LCD.clear() ; // Effacement de l'afficheur LCD
}


/*******************************************************************************************\
| Fonction d'initialisation de la synchronisation avec le serveur NTP.                     *|
\*******************************************************************************************/

void NTP_init() // Initialisation de la synchronisation avec le serveur NTP
{
  // Affichage LCD des informations de synchronisation avec le serveur NTP
    LCD.setCursor(0,0) ; LCD.print("Synchronisation avec") ; // Affichage LCD de la synchronisation NTP
    LCD.setCursor(0,1) ; LCD.print("le serveur NTP") ; // Affichage LCD de la synchronisation NTP
  
  // Initialisation de la synchronisation au serveur NTP
    sntp_set_time_sync_notification_cb(time_sync_notification) ; // Notification de synchronisation au serveur NTP terminée
    sntp_set_sync_interval(NTPmaj * 1000UL) ; // Définition de la période de synchronisation au serveur NTP
    configTzTime(timeZone, ntpServer) ; // Synchronisation au serveur NTP
  
  delay(2000); // Attente de 2 secondes
}


/*******************************************************************************************\
| Fonction de notification de synchronisation au serveur NTP terminée.                     *|
\*******************************************************************************************/

void time_sync_notification(struct timeval *tv) // Fonction de notification de synchronisation au serveur NTP terminée
{
  notSyncNTP = true ;
}


/*******************************************************************************************\
| Fonction d'affichage de la notification de synchronisation au serveur NTP.               *|
\*******************************************************************************************/

void printSyncNTP() // Fonction d'affichage de la notification de synchronisation au serveur NTP
{
  LCD.clear() ; // Effacement de l'afficheur LCD
  LCD.setCursor(0, 1) ;
  LCD.print("Mise a jour du temps") ; // Affichage LCD du texte de mise à jour du temps
  localTime() ; // Acquisition de la date et de l'heure locale
  Serial.println("\nMise a jour du temps") ; // Affichage sur le Terminal Série de la mise à jour du temps
  Serial.printf("le %d/%02d/%d a %d:%02d:%02d", Jour, Mois, Annee, Heure, Minute, Seconde) ; // Affichage sur le Terminal Série de la date et l'heure locale
  Serial.println() ;
  delay(2000) ; // Attente de 2 secondes
  LCD.clear() ; // Effacement de l'afficheur LCD
  notSyncNTP = false ;
}


/*******************************************************************************************\
| Fonction d'acquisition de la date et de l'heure locale.                                  *|
\*******************************************************************************************/

void localTime() // Fonction d'acquisition de la date et de l'heure locale
{
  // Acquisition de la date et de l'heure locale
    getLocalTime(&timeinfo) ; // Acquisition de la date et de l'heure locale
  // Extraction de la date et de l'heure
    Heure = timeinfo.tm_hour ; // Extraction de l'heure
    Minute = timeinfo.tm_min ; // Extraction des minutes
    Seconde = timeinfo.tm_sec ; // Extaction des secondes
    if (timeinfo.tm_wday == 0) JourdelaSemaine = 7 ; // Extaction du jour de la semaine
    else JourdelaSemaine = timeinfo.tm_wday ; 
    Jour = timeinfo.tm_mday ; // Extaction du jour
    Mois = timeinfo.tm_mon + 1 ; // Extaction du mois
    Annee = timeinfo.tm_year + 1900 ; // Extaction de l'année
}

/*******************************************************************************************\
| Fonction d'inscription de la date et de l'heure sur l'afficheur LCD.                     *|
\*******************************************************************************************/

void printLocalTime() // Fonction d'inscription de la date et de l'heure sur l'afficheur LCD
{
  // Écriture sur l'afficheur LCD de l'heure locale
    if ((Heure/10)==0) doNumber(10,0,0) ; // Affichage d'un triple espace
    else doNumber((Heure/10),0,0) ; // Affichage du chiffre des dizaines d'heure
    doNumber((Heure%10),0,3) ; // Affichage du chiffre des unités d'heure
    doNumber(11,0,6) ; // Affichage des deux points
    doNumber((Minute/10),0,7) ; // Affichage du chiffre des dizaines de minute
    doNumber((Minute%10),0,10) ; // Affichage du chiffre des unités de minute
    doNumber(11,0,13) ; // Affichage des deux points
    doNumber((Seconde/10),0,14) ; // Affichage du chiffre des dizaines de seconde
    doNumber((Seconde%10),0,17) ; // Affichage du chiffre des unités de seconde
  
  // Écriture sur l'afficheur LCD de la date
    Date = " * "
         + JourdelaSemaine_en_Texte[(JourdelaSemaine - 1)]
         + " "
         + Jour 
         + " " 
         + Mois_en_Texte[(Mois - 1)] 
         + " " 
         + Annee ;
    // La date a changée ?
      if (Date != Date_Old)
      {
        iCaractere = 0 ; // Réécriture depuis le premier caractère
        Date_Old = Date ;
      }
    // Initialisation du texte à afficher sur le LCD
      String D = "" ;
    // Construction de la chaîne de caractères à afficher sur l'afficheur LCD
      for (int i = 0 ; i < 20 ; i ++) {D += Date.charAt((iCaractere + i) % Date.length()) ;}
    // Détermination du prochain premier caractère à afficher
      if (iCaractere < Date.length()) iCaractere ++ ;
      else iCaractere = 0 ;
    // Affichage du Texte sur le LCD (le texte à afficher est une chaîne de caractères nommée D)
      LCD.setCursor(0,3) ; LCD.print(D) ; // Envoi de la chaîne de caractères sur le LCD
}


/*******************************************************************************************\
|* Fonction d'allumage et d'extinction du rétroéclairage de l'afficheur LCD.               *|
|*                                                                                         *|
|* Retourne "true" si le rétroéclairage doit être allumé.                                  *|
|* Retourne "false" si le rétroéclairage doit être éteint.                                 *|
\*******************************************************************************************/

bool backlightLCD() // Fonction d'allumage et d'extinction du rétroéclairage de l'afficheur LCD    
{ 
  // Si front montant du bouton de poussoir, alors inversion de l'état du rétroéclairage
    if (!status_BP_backlight && !digitalRead(BP_backlight)) // Front montant du bouton de poussoir du rétroéclairage ?
    {
      LCD_backlight = !LCD_backlight ; // Inversion de l'état du rétroéclairage
      status_BP_backlight = true ; // Mémorisation de l'état du bouton poussoir
    }
  
  // Si bouton poussoir relaché, alors remise à zéro de la mémorisation de l'état du bouton poussoir
    if (digitalRead(BP_backlight)) // Bouton poussoir relaché ?
    {
      status_BP_backlight = false ; // Mémorisation de l'état du bouton poussoir
    }

  // Renvoi de l'état du rétroéclairage
    return(LCD_backlight) ; // Renvoi de l'état du rétroéclairage
}


/*******************************************************************************************\
 * Fonction retournant le nombre de millisecondes depuis le démarrage du programme.        *|
 *                                                                                         *|
 * Retourne le nombre de milliseconde depuis le démarrage du programme sous la forme d’un  *|
 * nombre entier non signé de 64 bits.                                                     *|
\*******************************************************************************************/

uint64_t superMillis() // Fonction retournant le nombre de millisecondes depuis le démarrage du programme
{  
	static uint32_t nbRollover = 0 ; // Nombre de dépacement de millis
	static uint32_t previousMillis = 0 ; // Sauvegarde du nombre de millis précédent
	uint32_t currentMillis = millis() ; // Sauvegarde du nombre de millis courant
  	
	if (currentMillis < previousMillis) {nbRollover ++ ;} // Dépassement de millis ?
	previousMillis = currentMillis ; // Sauvegarde de la valeur courante de millis

	uint64_t finalMillis = nbRollover ; // Construction de finalMillis
	finalMillis <<= 32 ; // Décallage de 32 bits vers la gauche
	finalMillis += currentMillis ; // Ajout de currentMillis
	return finalMillis ; // Retourne le nombre de millisecondes depuis le démarrage du programme
}

/*******************************************************************************************\
|* Fonctions réalisants des temporisations.                                                *|
|*                                                                                         *|
|* TONv : présélection de la temporisation en millisecondes (constante 32 bits).           *|
|* Retourne "true" si la temporisation est terminée.                                       *|
|* Retourne "false" si la temporisation est en cours.                                      *|
\*******************************************************************************************/

bool Tempo1(uint32_t TONv) // Fonction réalisant la temporisation n° 1
{ 
  uint64_t Temps = superMillis() ; // Sauvegarde du temps courant
  if (Temps > TON1) {TON1 = Temps + TONv ; return true ; } // Temporisation écoulée ?
  else return false ;
}

bool Tempo2(uint32_t TONv) // Fonction réalisant la temporisation n° 2
{ 
  uint64_t Temps = superMillis() ; // Sauvegarde du temps courant
  if (Temps > TON2) {TON2 = Temps + TONv ; return true ; } // Temporisation écoulée ?
  else return false ;
}

/*******************************************************************************************\
|* Fonction dessinant un chiffre en triple hauteur & triple largeur.                       *|
|*                                                                                         *|           
|* Le chiffre "num" est à positionner à la ligne "l" et à la colonne "c".                  *|
\*******************************************************************************************/

void doNumber(byte num, byte l, byte c) // Fonction dessinant un chiffre en triple hauteur & triple largeur
{
  switch(num)
  {
    case  0 : LCD.setCursor(c,l) ; LCD.write(byte(3)) ; LCD.write(byte(1)) ; LCD.write(byte(4)) ; // Affichage du chiffre "0"
              LCD.setCursor(c,l+1) ; LCD.write(byte(3)) ; LCD.write(byte(6)) ;
              LCD.write(byte(4)) ; LCD.setCursor(c,l+2) ; LCD.write(byte(0)) ;
              LCD.write(byte(1)) ; LCD.write(byte(2)) ;
              break ;
    case  1 : LCD.setCursor(c,l) ; LCD.write(byte(6)) ; LCD.write(byte(5)) ; LCD.write(byte(6)) ; // Affichage du chiffre "1"
              LCD.setCursor(c,l+1) ; LCD.write(byte(6)) ; LCD.write(byte(3)) ; LCD.write(byte(6)) ;
              LCD.setCursor(c,l+2) ; LCD.write(byte(6)) ; LCD.write(byte(0)) ; LCD.write(byte(6)) ;
              break ;
    case  2 : LCD.setCursor(c,l) ; LCD.write(byte(0)) ; LCD.write(byte(1)) ; LCD.write(byte(4)) ; // Affichage du chiffre "2"
              LCD.setCursor(c,l+1) ; LCD.write(byte(3)) ; LCD.write(byte(1)) ; LCD.write(byte(2)) ;
              LCD.setCursor(c,l+2) ; LCD.write(byte(0)) ; LCD.write(byte(1)) ; LCD.write(byte(2)) ;
              break ;
    case  3 : LCD.setCursor(c,l) ; LCD.write(byte(0)) ; LCD.write(byte(1)) ; LCD.write(byte(4)) ; // Affichage du chiffre "4"
              LCD.setCursor(c,l+1) ; LCD.write(byte(6)) ; LCD.write(byte(1)) ; LCD.write(byte(4)) ;
              LCD.setCursor(c,l+2) ; LCD.write(byte(0)) ; LCD.write(byte(1)) ; LCD.write(byte(2)) ;
              break ;
    case  4 : LCD.setCursor(c,l) ; LCD.write(byte(3)) ; LCD.write(byte(6)) ; LCD.write(byte(4)) ; // Affichage du chiffre "4"
              LCD.setCursor(c,l+1) ; LCD.write(byte(0)) ; LCD.write(byte(1)) ; LCD.write(byte(4)) ;
              LCD.setCursor(c,l+2) ; LCD.write(byte(6)) ; LCD.write(byte(6)) ; LCD.write(byte(2)) ;
              break ;
    case  5 : LCD.setCursor(c,l) ; LCD.write(byte(3)) ; LCD.write(byte(1)) ; LCD.write(byte(2)) ; // Affichage du chiffre "5"
              LCD.setCursor(c,l+1) ; LCD.write(byte(0)) ; LCD.write(byte(1)) ; LCD.write(byte(4)) ;
              LCD.setCursor(c,l+2) ; LCD.write(byte(0)) ; LCD.write(byte(1)) ; LCD.write(byte(2)) ;
              break ;
    case  6 : LCD.setCursor(c,l) ; LCD.write(byte(3)) ; LCD.write(byte(1)) ; LCD.write(byte(2)) ; // Affichage du chiffre "6"
              LCD.setCursor(c,l+1) ; LCD.write(byte(3)) ; LCD.write(byte(1)) ; LCD.write(byte(4)) ;
              LCD.setCursor(c,l+2) ; LCD.write(byte(0)) ; LCD.write(byte(1)) ; LCD.write(byte(2)) ;
              break ;
    case  7 : LCD.setCursor(c,l) ; LCD.write(byte(0)) ; LCD.write(byte(1)) ; LCD.write(byte(4)) ; // Affichage du chiffre "7"
              LCD.setCursor(c,l+1) ; LCD.write(byte(6)) ; LCD.write(byte(6)) ; LCD.write(byte(4)) ;
              LCD.setCursor(c,l+2) ; LCD.write(byte(6)) ; LCD.write(byte(6)) ; LCD.write(byte(2)) ;
              break ;
    case  8 : LCD.setCursor(c,l) ; LCD.write(byte(3)) ; LCD.write(byte(1)) ; LCD.write(byte(4)) ; // Affichage du chiffre "8"
              LCD.setCursor(c,l+1) ; LCD.write(byte(3)) ; LCD.write(byte(1)) ; LCD.write(byte(4)) ;
              LCD.setCursor(c,l+2) ; LCD.write(byte(0)) ; LCD.write(byte(1)) ; LCD.write(byte(2)) ;
              break ;
    case  9 : LCD.setCursor(c,l) ; LCD.write(byte(3)) ; LCD.write(byte(1)) ; LCD.write(byte(4)) ; // Affichage du chiffre "9"
              LCD.setCursor(c,l+1) ; LCD.write(byte(0)) ; LCD.write(byte(1)) ; LCD.write(byte(4)) ;
              LCD.setCursor(c,l+2) ; LCD.write(byte(0)) ; LCD.write(byte(1)) ; LCD.write(byte(2)) ;
              break ;
    case 10 : LCD.setCursor(c,l) ; LCD.write(byte(6)) ; LCD.write(byte(6)) ; LCD.write(byte(6)) ; // Affichage d'un triple espace
              LCD.setCursor(c,l+1) ; LCD.write(byte(6)) ; LCD.write(byte(6)) ; LCD.write(byte(6)) ;
              LCD.setCursor(c,l+2) ; LCD.write(byte(6)) ;  LCD.write(byte(6)) ; LCD.write(byte(6)) ;
              break ;
    case 11 : LCD.setCursor(c,l) ; LCD.write(byte(7)) ; // Affichage des deux points
              LCD.setCursor(c,l+1) ; LCD.write(byte(7)) ;
              LCD.setCursor(c,l+2) ; LCD.write(byte(6)) ;
              break ;
  }
}