/*
   Programme pc_rm1 : régleur de montre - chronocomparateur
   Auteur : P. Chour - 2018-12-31 - V1.3
   Matériel : carte Arduino pro micro, afficheur I2C 2 lignes 16 caractères, 2 LEDS, une entrée "signal", un contacteur rotatif avec poussoir
   Différences entre V1.0 et V1.1
    - amélioration de la gestion du bouton poussoir
    - correction bug sur signe avance/retard par heure
   Différences entre V1.1 et V1.2
    - passage en 64 bits
    - test durée d'affichage (53ms pour deux lignes). Alternance de l'affichage pour diminuer cette durée.
    - amélioration du comptage de la durée à long terme
    - Affichage avance-retard sur 24 heures
   Différences entre V1.1 et V1.2
    - Ajout battement 19800, 25200

*/


#include <limits.h>
#include <PinChangeInterrupt.h>
#include <PinChangeInterruptBoards.h>
#include <PinChangeInterruptPins.h>
#include <PinChangeInterruptSettings.h>
#include <EEPROM.h>

#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <TimerThree.h>

typedef struct TCompteur TCompteur;

//
// Constantes, define et variables pour la mesure.
//
#define MaxMesures 20                 // Taille des buffer circulaires pour le calcul des durées
struct TCompteur
{
    int IndexMesure = 0;              // Index du buffer circulaire
    long NbCumuls = 0;       // nb de Cumuls pour le buffer courant (IndexMesure)
    uint64_t Mesure[MaxMesures];      // Buffer circulaire pour conserver les durées des Tic et des Tac
  long MesureLongTerme;               // Pour les mesures à long terme des écarts des durées de Tic et de Tac, valeur signée
};

TCompteur CompteurTic;
TCompteur CompteurTac;
struct TCompteur* PCompteur;
uint64_t DureeLongTerme;     // Pour mesure précise des périodes. Limité à 1 heure. En µS.
uint64_t NbDureeLongTerme;   // Nombre de mesures DureeLongTerme


#define MaxBattements   7             // Nombre de valeurs dans Battements[] (et idem pour Periode, demi periode et Aberrant)
// Les constantes qui suivent dépendent de Battements. Elles sont précalculées pour éviter de perdre du temps lors des traitements.
// Si on souhaite ajouter un nouveau battement, on l'ajoute dans le tableau Battement à la position i
// On calcule la période correspondante que l'on ajoute dans le tableau Periode à la position i
// On calcule la demi période que l'on ajoute dans le tableau DemiPeriode à la position i
// On calcule une valeur de durée de demi période que l'on considère comme aberrante. Pour ma part, c'est en général 1/4 de période. On l'ajoute dans
// le tableau Aberrant à la position i
// On met à jour la valeur MaxBattements qui correspond aux nombres de battements que l'on a défini.
const uint64_t Battements[MaxBattements] = {18000, 19800, 21600, 25200, 28800, 36000, 3600};             // en battements par heures
const uint64_t Periode[MaxBattements] = {400000, 363636, 333333, 285714, 250000, 200000, 1000000};       // periode en µS
const uint64_t DemiPeriode[MaxBattements] = {200000, 181818, 166666, 142857, 125000, 100000, 500000};    // demi période en µS
const uint64_t Aberrant[MaxBattements] = {100000, 90909, 83333, 71428, 62500, 50000, 500000};            // valeur de durée aberrantes en dessous de laquelle on ne prend pas en compte la mesure (1/4 de période en général) en µS
int IndexBattements = 0;                  // Valeur du battement courant dans le tableau Battements (et Periode et Aberrant).
uint64_t TempsTicTac;                // Durée d'un Tic ou d'un Tac

boolean ITTic = false;                    // Vrai si un Tic ou un Tac a été détecté (génération interruption). Doit être remis à faux par l'utilisateur




// Pin du processeur et usage
//
#define SignalEntree        9             // PIN Signal d'entrée de la mesure
#define LED_tic     4                     // PIN LED rouge (Tic)
#define LED_tac     5                     // PIN LED verte (Tac)
#define poussoir    8                     // PIN bouton poussoir
#define droite      7                     // PIN contact "droite" du contacteur
#define gauche      6                     // PIN contact "gauche du contacteur

// Etats de l'automate
//
#define E_selection                   0
#define E_mesure                      1
#define E_selection_trans             2
#define E_mesure_trans                3
#define E_pause                       4
#define E_pause_in_trans              5
#define E_pause_out_trans             6
#define E_mesure_redemarre_trans      7
#define E_selection_change_trans      8
#define E_calcule                     9

byte EtatAutomate = E_selection_trans;  // Etat courant de l'automate

// gestion du contacteur rotatif
//
#define AntiRebondPoussoir 50             // durée en millisecondes de l'anti rebond
unsigned long TimerPoussoir = 0;          // Timer pour antirebond
#define DureeDouble 700                   // Durée max d'un double appui en ms
unsigned long TimerDouble = 0;            // Timer pour double appui
boolean Gauche = false;                   // Contacteur va à gauche
boolean Droite = false;                   // contacteur va à droite
byte GaucheSeq = 0;                       // séquence courante de lecture du contacteur rotatif
byte DroiteSeq = 0;
boolean GauchePrec = false;               // Ancienne valeur du contacteur rotatif
boolean DroitePrec = false;
#define AntiRebondRotation 5              // durée en millisecondes de l'anti rebond
unsigned long TimerRotation = 0;          // timer pour anti rebond de la rotation


LiquidCrystal_I2C lcd(0x3f, 16, 2); // Initialisation de l'afficheur : adresse 0x27, 16 caractères, 2 lignes



/*
   Initialisation du programme
*/
void setup()
{
  int i;
  lcd.init();                       // Initialisation de l'afficheur
  lcd.clear();
  lcd.print("Initialisation");      // Un petit texte pour faire patienter
  lcd.setCursor(0, 1);
  lcd.print("P. Chour V1.3");       // C'est moi, c'est moi, c'est moi !

  pinMode(poussoir, INPUT);         // La pin où est connecté le bouton poussoir en entrée.
  pinMode(droite, INPUT);           // La pin où est connecté le contact "droite"
  pinMode(gauche, INPUT);           // La pin où est connecté le contact "gauche"
  digitalWrite(poussoir, HIGH);     // active résistance pull up
  digitalWrite(droite, HIGH);       // active résistance pull up
  digitalWrite(gauche, HIGH);       // active résistance pull down
  pinMode(SignalEntree, INPUT);     // La pin où est connecté le signal à mesurer
  digitalWrite(SignalEntree, LOW);  // active résistance pull up
  pinMode(LED_tic, OUTPUT);         // La pin où est connecté la LED rouge
  pinMode(LED_tac, OUTPUT);         // La pin où est connecté la LED verte
  for (i = 0; i < 5; i++) {
    lcd.noBacklight();              // Allumage rétro éclairage afficheur
    digitalWrite(LED_tic, HIGH);
    digitalWrite(LED_tac, LOW);
    delay(200);
    lcd.backlight();                // Allumage rétro éclairage afficheur
    digitalWrite(LED_tic, LOW);
    digitalWrite(LED_tac, HIGH);
    delay(200);
  }
  digitalWrite(LED_tac, LOW);
  attachPinChangeInterrupt(digitalPinToPinChangeInterrupt(SignalEntree), TicTacIT, RISING); // Signal d'entrée sous interruption.
  enablePinChangeInterrupt(digitalPinToPinChangeInterrupt(SignalEntree)); // On active l'interruption du signal d'entrée
  attachPinChangeInterrupt(digitalPinToPinChangeInterrupt(poussoir), PoussoirIT, FALLING); // Signal poussoir sous interruption.
  enablePinChangeInterrupt(digitalPinToPinChangeInterrupt(poussoir)); // On active l'interruption du poussoir
  sei();
}

/*
   Procédure sous interruption
   Déclenchée si signal en entrée (tic-tac)

*/

void TicTacIT()
{
  TempsTicTac = micros(); // Sauve la valeur du compteur courant
  ITTic = true;           // Indique qu'un Tic ou un Tac a été détecté
}

/*
   Procédure sous interruption
   Déclenchée lorsque l'on appuie sur le poussoir

*/

void PoussoirIT()
{
  unsigned long courante;
  courante = millis();                                            // valeur courante de l'horloge systeme
  if (DureeVrai(TimerPoussoir, courante) >= AntiRebondPoussoir) { // Prend-on en compte l'impulsion (antireboond) ?
    TimerPoussoir = courante;
    switch (EtatAutomate) {
      case E_mesure:                                              // On est dans l'état E_mesure. On passe en pause
        EtatAutomate = E_pause_in_trans;
        TimerDouble = courante;
        break;
      case E_pause:
        EtatAutomate = E_pause_out_trans;
        TimerDouble = courante;
      break;
      case E_pause_in_trans:                                      // On est en transition vers la pause : si deux appui rapide, on passe en sélection
      case E_pause_out_trans:
        if (DureeVrai(TimerDouble, courante) <= DureeDouble) {
          EtatAutomate = E_selection_trans;
        }
      break;
      case E_selection:                                           // On est dans l'état E_selection. On passe dans l'état E_mesure
        EtatAutomate = E_mesure_trans;
        break;
      default:                                                    // On peut être dans l'exécution d'une transition. Dans ce cas, on ne fait rien
      break;
    }
  }

}

//
// La fonction ci-dessous mesure le temps en prenant en compte un éventuel bouclage de la fonction millis ou micro
// On lui donne la valeur de départ de la valeur à mesurer et la valeur courante de millis ou micro. Elle rend la durée écoulée en millisecondes ou microseconde.
// DureeVrai = courante-dernière
//
unsigned long DureeVrai(unsigned long derniere, unsigned long courante) {
  if (courante < derniere) {  // on est passé par zéro
    return (0xffffffffu - derniere) + courante;
  } else {
    return courante - derniere;
  }
}

// La fonction ci-dessous convertit un nombre de 64 bits en string
// En entrée : N = nombre de 64 bits
// En sortie : string contenant le nombre
String Uint64ToString(uint64_t N) {
  String S;  
  if (N <= ULONG_MAX) {
    return String((unsigned long)N);
  } else {
    S = "";
    while (N > ULONG_MAX) {
      S = String((unsigned long)(N%10))+S;
      N = N / 10;
    }
    if (N > 0) {
      S = String(((unsigned long)N)) + S;
    }
    if (S == "") {S = "0";}
    return S;
  }
}

//
// La fonction qui suit teste le contacteur rotatif et détermine s'il y a eu
// rotation ou pas. Elle fonctionne par test d'état (donc pas très performante).
// Positionne les variables d'état Gauche, Droite
// La fonction rend vrai si il y a eu rotation et que le sens a pu être déterminé
//
void TestRotation() {
  unsigned long courante;
  courante = millis();                   // valeur courante de l'horloge systeme
  // Lit les signaux présents sur les pins droite et gauche du contacteur
  boolean GaucheVal = digitalRead(gauche);
  boolean DroiteVal = digitalRead(droite);
  if ((GauchePrec != GaucheVal) || (DroitePrec != DroiteVal)) {     // il y a eu changement d'état
    if (DureeVrai(TimerRotation, courante) >= AntiRebondRotation) { // Prend-on en compte l'impulsion (antirebond) ?
      TimerRotation = courante;
      GaucheSeq <<= 1;
      GaucheSeq |= GaucheVal;
      GaucheSeq &= 0b00001111;
      GauchePrec = GaucheVal;
      DroiteSeq <<= 1;
      DroiteSeq |= DroiteVal;
      DroiteSeq &= 0b00001111;
      DroitePrec = DroiteVal;

      // Mask the MSB four bits
      // Compare the recorded sequence with the expected sequence
      if ((GaucheSeq == 0b00001001) && (DroiteSeq == 0b00000011)) {
        Gauche = true;
      }
      if ((GaucheSeq == 0b00000011) && (DroiteSeq == 0b00001001)) {
        Droite = true;
      }
      if (Droite || Gauche) {EtatAutomate = E_selection_change_trans;}
    }
  }
}
//
// Effectue une différence entre deux valeurs (V1 et V2) non signées et formatte le résultat pour affichage
// V1 = valeur en µS
// V2 = valeur en µS
// L = longueur
// Signe = boolean si un signe doit être mis. Si V1>V2, signe +. Sinon -
//
String FormatEcart(uint64_t V1, uint64_t V2, int L, boolean Signe) {
String S;
    S = "";
    if (V1 > V2) {
      V1 = V1-V2;
      if (Signe) {S = "+";}
    } else {
      V1 = V2-V1;
      if (Signe) {S = "-";}
    }
    return FormatS(V1,L,S,4);
}

//
// Formatte une valeur pour affichage
// V1 = valeur
// L = longueur
// Signe = Signe à afficher
// Unite de la valeur= 4=µS, 3=ms, 2=s, 1=mn, 0=h (unité dans laquelle est le nombre)
// Rend "***" si dépassement capacité ou affichage impossible
// Affichage => unité sur deux caractères + signe sur 1 (+ ou -) ou 0 (pas de signe) caractère + 1 chiffre => L >= 3 si pas de signe et L >= 4 si signe
//
String FormatS(uint64_t V1, int L, String Signe, byte Unite) {
const String Depassement = "***";
String S;
uint64_t VDec;
uint64_t VTemp;
int LPartieEntiere;
unsigned int Diviseur;
int i;
    if ((L < 3) || ((L == 4) && (Signe.length() == 1)) || (Signe.length() > 1)) {return Depassement;} // dépassement ou erreurs de certains paramètres
    S = "";
    VDec = 0;
    LPartieEntiere = 0;
    while (S == "") {
      switch (Unite) {
        case 4:
        case 3: Diviseur = 1000; break;
        case 2:
        case 1: Diviseur = 60; break;
        //default: S = "> " + Signe + "1h"; break;
      }
      if ((V1 < Diviseur) && (Unite > 0)) {
        S = Signe+Uint64ToString(V1);
        LPartieEntiere = S.length();
        if (VDec != 0) {
          S = S+"."+Uint64ToString(VDec);
          for (i=S.length(); i < L-2; i++) {S = S+"0";}
        }
      } else {
        VTemp = V1;
        V1 = V1 / Diviseur;
        VDec = VTemp-V1*Diviseur;
        Unite = Unite-1;
      }
    }
    if (S.length() > L-2) {
      S.remove(L-2,S.length()-L-2);             // On tronque le résultat pour une longueur L (+ longueur des unités)
    }
    if (S.length() < LPartieEntiere) {return Depassement;}   // Si la longueur du résultat est inférieure à la partie entière du nombre, alors, dépassement, on sort
    switch (Unite) {                            // On accole les unités
      case 4: S = S+"us";break;
      case 3: S = S+"ms";break;
      case 2: S = S+" s";break;
      case 1: S = S+"mn";break;
      case 0: S = S+" h";break;
      default: S = "***";break;
    }
    for (i=S.length(); i < L; i++) {            // On complète le résultat avec des espaces en tête si besoin
      S = " "+S;
    }
    return S;
}

//
// Affichage des mesures
// AffFlip : affichage 1ère ligne (true) ou deuxième ligne (false)
//
void AffMesure(boolean AffFlip)
{
  int i;
  String S;
  String Signe;
  long MoyenneTic;
  long MoyenneTac;
  uint64_t VTempU;
/* Test durée exécution affichage  
  unsigned long ENTREE;
  unsigned long SORTIE;
  ENTREE =  millis();
 Fin test durée exécution affichage */  
// Affichage période. On n'affiche que si l'on a au moins MaxMesures mesures

  if ((CompteurTic.NbCumuls >= MaxMesures) &&  (CompteurTac.NbCumuls >= MaxMesures)) {
    if (AffFlip) {
      // Affichage 1ère ligne
      MoyenneTic = 0;
      MoyenneTac = 0;
      for (i=0;i < MaxMesures; i++) {
        MoyenneTic = MoyenneTic+CompteurTic.Mesure[i];
        MoyenneTac = MoyenneTac+CompteurTac.Mesure[i];
      }
      MoyenneTic = MoyenneTic / MaxMesures;
      MoyenneTac = MoyenneTac / MaxMesures;
      lcd.setCursor(0,0);
      S = FormatEcart(MoyenneTic+MoyenneTac, Periode[IndexBattements], 8, true);
      S = S+" "+FormatEcart(MoyenneTic, MoyenneTac, 7, false);
      lcd.print(S);

    } else {
      // Affichage 2ème ligne. MoyenneTic sert de variable temporaire
      lcd.setCursor(0,1);
      MoyenneTic = (CompteurTic.MesureLongTerme/CompteurTic.NbCumuls)+(CompteurTac.MesureLongTerme/CompteurTac.NbCumuls); // Moyenne des différence entre la durée d'un TicTac par rapport à la durée théorique attendue
      if (MoyenneTic < 0) { // Si la durée d'un Tic Tac est négative, c'est qu'elle est plus courte que la durée théorique attendue
        MoyenneTic = 0-MoyenneTic;
        Signe = "-"; // On affichera -X ms (ou µs ou s...) => La montre avance
      } else {
        Signe = "+"; // La durée d'un TicTac est plus grande que la durée théorique attendue, on affichera + X ms (ou µs ou s...) => La montre retarde
      }
      S = FormatS((uint64_t)MoyenneTic,8,Signe,4);
  
      VTempU = DemiPeriode[IndexBattements]*NbDureeLongTerme; // Durée attendue
      if (VTempU < DureeLongTerme) {                          // Si durée attendue est inférieure à durée mesurée
        VTempU = (DureeLongTerme-VTempU)/NbDureeLongTerme;    // Alors, la montre retarde
        Signe = "-";
      } else {
        VTempU = (VTempU-DureeLongTerme)/NbDureeLongTerme;    // Sinon, la montre avance
        Signe = "+";
      }
      S = S+" "+FormatS(VTempU*Battements[IndexBattements]*(uint64_t)24,7,Signe,4);
      lcd.print(S);
    }

/* Test durée exécution affichage    
    SORTIE = millis();
    lcd.setCursor(0,0);  
    lcd.print(String(DureeVrai(ENTREE,SORTIE))+"      ");
 Fin test durée exécution affichage */    
  }
}


/**********************************
   APPLICATION
**********************************/

void loop()
{
  const String NonInitialise = "------    ------";
  int i;
  boolean LEDFlip = true;   // Pour alterner l'allumage des LED
  boolean AffFlip = true;   // pour affichage alterné des indications
  int Passe;                // Pour ne pas prendre en compte les deux premières ITTic
  unsigned long AffTimer;
  uint64_t TempsTic;
  uint64_t TempsTac;
  uint64_t DiffTicTac;
  
  AffTimer = millis();
  while (1) {
    if (ITTic) { // Action faite quelque soit l'état de l'atomate s'il y a eu un Tic
      LEDFlip = !LEDFlip;   // Pour affichage alterné des LED
      if (LEDFlip) {
        TempsTic = TempsTicTac;
        digitalWrite(LED_tic, HIGH);
        digitalWrite(LED_tac, LOW);
      } else {
        TempsTac = TempsTicTac;
        digitalWrite(LED_tic, LOW);
        digitalWrite(LED_tac, HIGH);
      }
      ITTic = false; // Prise en compte du Tic
      if (EtatAutomate == E_mesure) {EtatAutomate = E_calcule;} // Traitement du Tic si on est dans l'état de mesure
    }

    switch (EtatAutomate) {

      //
      // Passage en mode sélection, on efface l'écran et on passe en changement de battement
      //
      case E_selection_trans:
        lcd.clear();
        lcd.print("Selection");
        for (i=0; i < MaxMesures;i++) {
          CompteurTic.Mesure[i] = 0;
          CompteurTac.Mesure[i] = 0;
        }
        CompteurTic.IndexMesure = 0;
        CompteurTac.IndexMesure = 0;
        CompteurTic.MesureLongTerme = 0;
        CompteurTac.MesureLongTerme = 0;
        CompteurTic.NbCumuls = 0;
        CompteurTac.NbCumuls = 0;
        DureeLongTerme = 0;
        NbDureeLongTerme = 0;
        Passe = 2;
        ITTic = false;

      //
      // Transition de changement de battement
      //
      case E_selection_change_trans:
        if (Gauche) {
          Gauche=false;
          IndexBattements--;
          if (IndexBattements < 0) {IndexBattements = MaxBattements-1;}
        }
        if (Droite) {
          Droite=false;
          IndexBattements++;
          if (IndexBattements >= MaxBattements) {IndexBattements = 0;}
        }
        lcd.setCursor(0, 1);
        lcd.print(Uint64ToString(Battements[IndexBattements]) + " Bat/h   ");
        EtatAutomate = E_selection;
      break;

      //
      // Passage en mode mesure. On affiche les valeurs courantes de la mesure
      //
      case E_mesure_trans:
        EtatAutomate = E_mesure;
        lcd.clear();
        lcd.print(NonInitialise);
        lcd.setCursor(0, 1);
        lcd.print(NonInitialise);
      break;

      //
      // passage en pause
      //
      case E_pause_in_trans: // on n'était pas dans l'état E_pause
        if (DureeVrai(TimerDouble,  millis()) > DureeDouble) {
          EtatAutomate = E_pause;
          lcd.setCursor(9, 1);
          lcd.print(" PAUSE  ");
        }
      break;

      case E_pause_out_trans: // on était dans l'état E_pause
        if (DureeVrai(TimerDouble, millis()) > DureeDouble) {
          EtatAutomate = E_mesure_redemarre_trans;
          lcd.setCursor(9, 1);
          lcd.print("        ");
        }
      break;

      //
      // En pause, on attend un événement (bouton poussoir)
      //
      case E_pause:
        delay(1);
      break;

      //
      // Si on n'a pas eu de double appui, alors, on repars en mesure
      //
      case E_mesure_redemarre_trans:
        lcd.setCursor(10, 1);
        lcd.print("     ");
        Passe = 2;
        EtatAutomate = E_mesure;
      break;

      case E_calcule:
        if (Passe > 0) {
          Passe = Passe-1;
        } else {
          if (LEDFlip) {
            PCompteur = &CompteurTic;
          } else {
            PCompteur = &CompteurTac;
          }
          // Calcule de la dérive à court terme (MaxMesures) et à long terme
          if (TempsTic > TempsTac) {DiffTicTac = TempsTic-TempsTac;} else {DiffTicTac = TempsTac-TempsTic;}
/* Test Durée mesurée brute
          lcd.setCursor(0,0);
          lcd.print(String(DiffTicTac));
*/
          if ((DiffTicTac > Periode[IndexBattements]) || (DiffTicTac < Aberrant[IndexBattements])) {
            Passe = 2; // si valeur aberrante, on ne prend pas en compte la mesure et on laisse passer 2 battements
          } else {
            PCompteur->Mesure[PCompteur->IndexMesure] = DiffTicTac; // On conserve les durées des Tic ou Tac
            PCompteur->MesureLongTerme = PCompteur->MesureLongTerme+(PCompteur->Mesure[PCompteur->IndexMesure]-DemiPeriode[IndexBattements]); // On ne conserve que les écarts par rapport à la valeur attendue (valeru signée)
            PCompteur->NbCumuls = PCompteur->NbCumuls+1;
            if (DureeLongTerme < 0x0FFFFFFFFFFFFFFF) {
              DureeLongTerme = DureeLongTerme + DiffTicTac;
              NbDureeLongTerme = NbDureeLongTerme+1;
            }
            PCompteur->IndexMesure = PCompteur->IndexMesure+1;
            if (PCompteur->IndexMesure >= MaxMesures) {PCompteur->IndexMesure = 0;}
          }
        }
        EtatAutomate = E_mesure;
      break;

      case E_mesure:
        if (DureeVrai(AffTimer, millis()) > 1000) {
          AffTimer = millis();
          AffMesure(AffFlip);
          AffFlip = !AffFlip;
        }
      break;
//
// Dans l'état E_selection, on ne fait que choisir le battement de la montre.
// La gestion du contacteur rotatif est fait par test d'état car on n'a que ça à faire.
// On sort de l'état E_selection par appui sur le bouton poussoir du contacteur. Cet appui est
// détecté par interruption
//
      case E_selection:
        TestRotation();
      break;


//
// L'état default correspond à une erreur d'automate (pas d'état connu).
// En cas d'erreur, on retourne dans l'état E_Selection
//
      default:
        EtatAutomate = E_selection_trans;
        lcd.clear();
        lcd.print("Erreur");
        delay(1000);
       break;
    }


  }
}