//
//  PROGRAMME POUR GESTION DE CHARGEUR DE BATTERIE AU PLOMB
//  Pascal Chour - 08/2017
//  Le schéma du chargeur se trouve sur www.pascalchour.fr
//
//  Carte Arduino pro micro pour la partie processeur.
//
//#define TEST true
//
// Affectation des pins du processeur
//
const int FaibleCourant = 19;     // Pin pour commande faible courant. Commande la LED Orange et commute le relais "Courant faible"
const int FortCourant = 18;       // Pin pour commande fort courant. Commande une LED Rouge et commute le relais "Courant fort"
const int BatterieCharge = 20;    // Pin pour commander la LED verte indiquant que la batterie est chargée.
const int ADCALire = 4;           // N° de l'ADC à lire (ADC4)
const int Erreur = 10;            // Pin pour comander une LED rouge qui sert à signaler les erreurs
const byte VREF_INTERNE = 1;      // Constante à utiliser si on veut mesurer par rapport à la référence de tension interne (2,56V). Voir InitADC
const byte Vcc_EXTERNE = 2;       // constante à utiliser si on veut mesurer par rapport à Vcc. Voir InitADC
//
// Quelques constantes concernant les tensions (toutes en mv)
//
const unsigned long VLimiteBasse = 7000;   // Valeur de tension en dessous de laquelle on considère qu'il n'y a pas de batterie connectée => Err
const unsigned long VLimiteHaute = 15000;  // Valeur de tension au dessus de laquelle on considère que la tension de batterie est trop élevée => Err
//
// ---- CONSTANTES A REGLER  ---- //
//
const unsigned long Facteur = 585;                   // Facteur multiplicatif pour calculer la tension réelle par rapport à la tension mesurée. 
//                                                   // Est en lien avec la valeur du diviseur de tension du montage (pont de résistance plus diode en série).
//                                                   // Tension réelle = (valeur ADC) * (Tension de référence) * Facteur) / 102300  où
//                                                   // Valeur ADC est la valeur lue dans le convertisseur, Tension de référence est la tension
//                                                   // d'alimentation ou la tension de référence ARef en mV selon la référence choisie dans InitADC. 
const unsigned long VAlimArduino = 4800;             // tension d'alimentation de l'Arduino réellement mesurée
//
// ---FIN CONSTANTES A REGLER--- //
//
const unsigned long VAref = 2560;                    // Tension ARef interne (2.56 sur 32U4)
const unsigned long VBasculeFaibleFort = 13000;      // Tension de basculement entre charge à courant élevé et charge à courant faible
const unsigned long VLimiteBatterieChargee  = 13500; // Tension à partir de laquelle on considère la batterie comme chargée
const unsigned long Hysteresis = 200;                // valeur de l'hystérésis
//
// Constantes de temps
//
const unsigned long PeriodeMesure = 600000;         // 600000 = Période de mesure de la tension réelle de la batterie (10 mn)
//
// Etat automate chargeur
//
const byte EtatInitial = 1;         // Etat initial de l'automate. Normalement, on n'y reste pas, sauf si erreur de programme !
const byte EtatErrFaible = 2;       // Tension trop faible. Clignotement lent
const byte EtatErrEleve = 3;        // Tension trop forte. Cligontement rapide
const byte EtatChargeFaible = 4;    // Charge faible de la batterie
const byte EtatChargeForte = 5;     // Charge forte de la batterie
const byte EtatBatterieCharge = 6;  // Batterie chargée
//
// Variables
//
byte EtatAutomate;                  // Etat courant automate
boolean Transition;                 // Indique si on est dans la transition d'un état vers l'autre (vrai) ou si on est dans l'état établi (faux)

boolean LEDAllume;                  // Utilisé pour le clignotement de la LED d'erreur. Vrai si la LED est allumée
unsigned long DureeClignotement;    // Pour mesurer la temps pour le clignotement de la LED d'erreur
unsigned int LEDDelai;              // Pour indiquer la période du clignotement. 0 = Pas de clignotement.
//
unsigned long DureeMesure;          // pour mesurer le temps entre deux mesures avec batterie déconnectée.


//-------------------------------------- INITIALISE ADCi
// Lecture de la tension d'alimentation selon la référence.
// En entrée :  N° de l'ADC à lire
//              Reference de tension à utiliser
// En sortie : valeur lue
//
void InitADC(byte ADCAlire, byte Reference)
{
  ADMUX = ADCAlire;         // n° de l'ADC
  if (Reference == VREF_INTERNE) {
    ADMUX |= (1 << REFS0);    // Utilisation référence interne
    ADMUX |= (1 << REFS1);    //
  } else {
    ADMUX |= (1 << REFS0);    // Utilisation référence Vcc
    ADMUX &= ~(1 << REFS1);    //
  }
  ADMUX &= ~(1 << ADLAR);   // Résolution de 10 bits
  delay(2);
  ADCSRA |= (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);    // 128 prescale for 8Mhz
  ADCSRA |= (1 << ADEN);    // Active l'ADC
}



//-------------------------------------- LECTURE ADCi
// Lecture de la tension d'alimentation selon la référence.
// InitADC doit avoir été appelé avant
// En sortie : valeur lue
//
word LectureADC()
{
  word ADCval;
  ADCSRA |= (1 << ADSC);    // Démarre la conversion
  while (ADCSRA & (1 << ADSC));     // Attente que la conversion soit terminée
  ADCval = ADCL;                    // Lecture d'ADCL en premier
  ADCval = (ADCH << 8) + ADCval;    // Lecture d'ADCH. ADC peut de nouveau être utilisé
  return ADCval;
}


//-------------------------------------- SETUP
// Initialisation du programme
void setup() {
  pinMode(FaibleCourant, OUTPUT);
  pinMode(FortCourant, OUTPUT);
  DesactiveCharge();                  // Ouvre tous les relais
  pinMode(BatterieCharge, OUTPUT);    
  IndicateurBatterieChargee(false);   // Eteint l'indicateur de batterie chargée
  pinMode(Erreur, OUTPUT);
  RAZErreur();                        // Pas d'erreur. Eteint la diode de signalement d'erreur
  InitADC(ADCALire, Vcc_EXTERNE);     // Par précaution, on initialise la référence de tension pour la mesure à Vcc. Le montage est ainsi protégé jusqu'à environ 30V avant diviseur de tension.

  EtatAutomate = EtatInitial;         // Etat de l'automate au lancement
  Transition = false;                 // Pas de changement d'état en cours

  // pour tests
#if defined(TEST)
  Serial.begin(9600);
#endif
}

//-------------------------------------- MESURE TENSION
// Mesure de la tension.
// En entrée :  MesureBatterie = vrai, mesure tension batterie avec déconnexion préalable du chargeur
//              MesureBatterie = faux, mesure tension batterie connectée ou déconnectée du chargeur
// En sortie :  V = Valeur mesurée
//              EtatAutomate = EtatErrFaible si tension trop faible. La fonction rend faux
//              EtatAutomate = EtatErrFort si tension trop forte. La fonction rend faux
//              EtatAutomate inchangé si autre cas. La fonction rend vrai.
//
// NOTE : si le chargeur a été déconnecté, il n'est pas reconnecté. C'est à l'appelant de savoir ce qu'il veut faire.
//  Le fait de ne pas reconnecter le chargeur dans cette fonction permet d'éviter d'éventuelles oscillations des relais
//
boolean MesureTension(unsigned long *V, boolean MesureBatterie) {
  int i;
  unsigned long moyenne;

  //
  // On commence par tester les tension grossièrement (par rapport à l'alimentation = 5V)
  //
  InitADC(ADCALire, Vcc_EXTERNE);                 // La référence de tension est celle de l'alimentation (environ 5V)
  for (i = 1; i < 5; i++) {                       // On fait quelques lectures bidons
    LectureADC();
  }
  moyenne = 0;                                    // On fait une moyenne sur 5 mesures de la lecture de l'ADC
  for (i = 1; i <= 5; i++) {                      // 
    moyenne = moyenne + LectureADC();
  }
  moyenne = moyenne / 5;                          // on espère que le compilo n'optimise pas cette opération
  *V = (VAlimArduino * moyenne * Facteur) / 102300 ;   // V contient le résultat de la mesure
  
#if defined(TEST)
  Serial.print(" TVcc ");
  Serial.print(*V);
  Serial.print(" - Vlu ");
  Serial.print(moyenne);
  Serial.print(" - Tension lue ");
  Serial.print((moyenne * VAlimArduino) / 1023);
#endif

  if (*V <= VLimiteBasse) {                       // V inférieur à la limite basse de tension acceptée ?
    Transition = (EtatAutomate != EtatErrFaible); // Oui ? Transition n'est positionné à vrai que s'il y a changement d'état
    EtatAutomate = EtatErrFaible;                 // Alors on passe dans l'état d'erreur (tension faible).
    return false;
  }
  
  if (*V >= VLimiteHaute) {                       // V inférieur à la limite haute de tension acceptée ?
    Transition = (EtatAutomate != EtatErrEleve);  // Oui ? Transition n'est positionné à vrai que s'il y a changement d'état
    EtatAutomate = EtatErrEleve;                  // Alors on passe dans l'état d'erreur (tension élevée).
    return false;
  }
  
  // Il n'y a pas d'erreur. La tension peut être mesurée préciséement.
  if (MesureBatterie) {                           // Si on doit mesurer la tension réelle de la batterie
    DesactiveCharge();                            // alors, il faut déconnecter le chargeur
  };
  
  InitADC(ADCALire, VREF_INTERNE);                // La référence de tension est ARef (cf. doc Atmel)
  for (i = 1; i < 5; i++) {
    LectureADC();
  }
  moyenne = 0;
  for (i = 1; i <= 5; i++) {                      
    moyenne = moyenne + LectureADC();
  }
  moyenne = moyenne / 5;                          // on espère que le compilo n'optimise pas cette opération
  *V = (VAref * moyenne * Facteur) / 102300;      // V contient le résultat de la mesure

#if defined(TEST)
  Serial.print(" - TVaref ");
  Serial.print(*V);
  Serial.print(" - Vlu ");
  Serial.print(moyenne);
  Serial.print(" - Tension lue ");
  Serial.print((moyenne * VAref) / 1023);
#endif

  InitADC(ADCALire, Vcc_EXTERNE);                // Par précaution, on remet la référence de tension à celle de l'alimentation.
  return true;
}

//-------------------------------------- INDICATEUR BATTERIE CHARGEE
// Allume/éteint la diode d'indication de batterie chargée
// En entrée : True, allume la diode, false, éteint la diode
// En sortie : Néant
//
void IndicateurBatterieChargee(boolean B) {
  if (B) {
    digitalWrite(BatterieCharge, LOW);
  } else {
    digitalWrite(BatterieCharge, HIGH);
  }
}

//-------------------------------------- ACTIVE COURANT FORT
// Active la Charge courant fort
// En entrée : néant
// En sortie : Néant
//
void ActiveCourantFort() {
  digitalWrite(FaibleCourant, HIGH);  // désactive courant faible
  IndicateurBatterieChargee(false);   // désactive batterie chargée
  delay(200);                         // Attend que le relai commute
  digitalWrite(FortCourant, LOW);     // active courant fort
}

//-------------------------------------- ACTIVE VOURANT FAIBLE
// Active la Charge courant faible
// En entrée : néant
// En sortie : Néant
//
void ActiveCourantFaible() {
  digitalWrite(FortCourant, HIGH);    // désactive courant fort
  IndicateurBatterieChargee(false);   // désactive batterie chargée
  delay(200);                         // Attend que le relai commute
  digitalWrite(FaibleCourant, LOW);   // active courant faible
}

//-------------------------------------- DESACTIVE CHARGE
// Désactive courant Fort et faible
// En entrée : néant
// En sortie : Néant
//
void DesactiveCharge() {
  digitalWrite(FortCourant, HIGH);    // désactive courant fort
  digitalWrite(FaibleCourant, HIGH);  // désactive courant faible
  delay(200);                         // attend que les relais commutent
}

//-------------------------------------- INIT DIODE ERREUR
// Initialisation de l'allumage et de la durée de clignotement de la diode
// de signalement d'erreur
// En entrée : RAPIDE = True => clignotement rapide => 100mS
//             RAPIDE = False => clignotement lent => 500ms
//
void InitDiodeErreur(boolean Rapide) {
  digitalWrite(Erreur, LOW);      // on allume la diode qui signale les erreurs
  LEDAllume = true;               // on se souvient de l'état de la diode
  if (Rapide) {                   // si clignotement rapide,
    LEDDelai = 100;               // la durée du timer est initialisé pour 100ms
  } else {                        // sinon
    LEDDelai = 700;               // pour 700ms
  }
  DureeClignotement = millis();   // on initialise le timer (variable globale DureeClignotement)
}

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

//-------------------------------------- DIODE ERREUR
// Gère le clignotement de la LED de signalement d'erreur
// En entrée : néant
//             néant
// Un appel à InitDiodeErreur doit avoir été effectué pour initialiser les variables globales
// si LEDDelai = 0, on considère qu'il n'y a pas de clignotement.
//
void DiodeErreur() {
  unsigned long i;
  if (LEDDelai == 0) {            // Si le timer n'est pas initialisé, on sort
    return;
  }
  i = millis();                   // valeur courante de l'horloge systeme
  if (DureeMilli(DureeClignotement, i) >= LEDDelai) { // calcule si délai timer dépassé
    if (LEDAllume) {              // si oui, alors, si la diode est allumée
      digitalWrite(Erreur, HIGH); // on l'éteint
    } else {                      // sinon,
      digitalWrite(Erreur, LOW);  // on l'allume
    }
    LEDAllume = !LEDAllume;       // on mémorise l'état de la diode
    DureeClignotement = i;        // et on réinitialise le timer
  }
}


//-------------------------------------- RAZ ERREUR
// Arrête le clignotement
//
void RAZErreur() {
  digitalWrite(Erreur, HIGH);    // Eteint la diode signalant les erreurs
  LEDDelai = 0;                  // LEDDelai = 0 => timer non initialisé
}

//-------------------------------------- INIT TIMER MESURE
// Initialise le timer pour la mesure de tension (variable globale DureeMesure)
//
void InitTimerMesure() {
  DureeMesure = millis();
}

//-------------------------------------- TIMER MESURE
// Vrai si le temps écoulé depuis le dernier InitTimer est supérieur à la période prévues pour la mesure
// En entrée : néant
//
boolean TimerMesure() {
  if (DureeMilli(DureeMesure, millis()) >= PeriodeMesure) { // si le timer est échu
    InitTimerMesure();    // alors, on le redémarre
    return true;          // et on retourne True (Timer échu)
  } else {                // sinon
    return false;         // on retourne faux (timer non échu)
  }
}



//-------------------------------------- LOOP
// Pour faire simple, dès que l'on n'est pas dans une condition d'erreur, on
// démarre par l'état charge faible. L'état de charge faible peut être considéré comme l'aiguillage de l'automate
// pour les changements d'états hors cas d'erreur.
//
void loop() {
  unsigned long TensionCourante;

  int ADCvalue;

#if defined(TEST)
  Serial.print(" - Etat Automate ");
  Serial.println(EtatAutomate);
  delay(500);
#endif

  switch (EtatAutomate) {

    case EtatErrFaible:         // Erreur, tension trop faible
      if (Transition) {         // si transition d'automate en cours (passage d'un état à l'autre)
        DesactiveCharge();      // on désactive le chargeur (au cas où)
        IndicateurBatterieChargee(false);   // l'indicateur de batterie chargée est éteint (au cas où)
        InitDiodeErreur(false); // On initialise le timer de la diode avec un clignotement lent
        Transition = false;     // on indique que la transition est terminée
      } else {                  // On est dans l'état (transition terminée)
        DiodeErreur();          // DiodeErreur gère le clignotement de la diode
        if (MesureTension(&TensionCourante, false)) { // on mensure la tension au cas où.
          EtatAutomate = EtatChargeFaible;            // Si une tension valide est mesurée, alors, on passe dans l'état Charge Faible
          Transition = true;    // et on positione l'indicateur de transition
        }
      }
      break;

    case EtatErrEleve:          // Erreur, tension trop élevée. Pour la suite, voir commentaires de l'état EtatErrFaible
      if (Transition) {
        DesactiveCharge();
        IndicateurBatterieChargee(false);
        InitDiodeErreur(true);  // On initialise le timer de la diode avec un clignotement rapide
        Transition = false;
      } else {
        DiodeErreur();
        if (MesureTension(&TensionCourante, false)) {
          EtatAutomate = EtatChargeFaible;
          Transition = true;
        }
      }
      break;

    case EtatChargeForte:       // Etat charge forte (5A par exemple)
      if (Transition) {         // Si le changement d'état est en cours
        ActiveCourantFort();    // On active le relai qui commande le courant fort
        RAZErreur();            // On efface les conditions d'erreurs (au cas où)
        IndicateurBatterieChargee(false); // On éteint la diode qui indique la batterie est chargée (au cas où)
        Transition = false;     // On indique que la transition est terminée
        InitTimerMesure;        // On initialise le timer pour la mesure de la tension sur la batterie
      } else {                  // Si on est dans l'état établi (pas dans la transition)
        if (TimerMesure()) {    // On vérifie si on doit mesurer la tension avec le chargeur déconnecté
          if (MesureTension(&TensionCourante, true)) {  // En sortie, le chargeur est déconnecté
            if (TensionCourante >= VBasculeFaibleFort + Hysteresis) { // Doit on passer en charge de maintien ?
              EtatAutomate = EtatChargeFaible;  // Si oui, on change d'état
              Transition = true;                // Transition en cours
              break;                            // On sort
            }
            ActiveCourantFort();    // on reconnecte le chargeur uniquement si on n'a pas changé d'état
          }
        } else {                // On mesure la tension pour vérifier s'il n'y a rien d'anormal (tension trop basse ou tension trop élevée)
          MesureTension(&TensionCourante, false);
        }
      }
      break;

    case EtatChargeFaible:    // Etat charge faible. C'est l'aiguillage de l'automate lorsqu'il n'y a pas d'erreur
      if (Transition) {       // Si le changement d'état est en cours
        RAZErreur();          // On efface les conditions d'erreurs (au cas où)
        IndicateurBatterieChargee(false);   // on éteint l'indicateur de batterie chargée (au cas où)
        if (MesureTension(&TensionCourante, true)) {  // on fait une mesure de la tension de batterie pour voir si on doit rester dans l'état courant
          if (TensionCourante >= VLimiteBatterieChargee + Hysteresis) {
            EtatAutomate = EtatBatterieCharge;
            break;
          }
          if (TensionCourante <= VBasculeFaibleFort - Hysteresis) {
            EtatAutomate = EtatChargeForte;
            break;
          }
        }
        ActiveCourantFaible();  // On arrive ici si on était bien dans le bon état
        Transition = false;     // La transition est terminée
        InitTimerMesure;        // On initialise le timer pour la prise de mesure de la tension de la batterie
      }
      if (TimerMesure()) {      // Si le timer pour la prise de mesure est échu
        if (MesureTension(&TensionCourante, true)) {  // Alors, on mesure la tension de la batterie et on voit si on doit rester dans cet état.
          if (TensionCourante >= VLimiteBatterieChargee + Hysteresis) {
            EtatAutomate = EtatBatterieCharge;
            Transition = true;
            break;
          }

          if (TensionCourante <= VBasculeFaibleFort - Hysteresis) {
            EtatAutomate = EtatChargeForte;
            Transition = true;
            break;
          }
          ActiveCourantFaible();    // on reconnecte le chargeur

        }
      } else {
        MesureTension(&TensionCourante, false);
      }
      break;

    case EtatBatterieCharge:
      if (Transition) {
        DesactiveCharge();
        RAZErreur();
        IndicateurBatterieChargee(true);    // active batterie chargée
        Transition = false;
      }
      if (MesureTension(&TensionCourante, false)) {   // la batterie est déja déconnectée
        if (TensionCourante <= VLimiteBatterieChargee - Hysteresis) {
          EtatAutomate = EtatChargeFaible;
          Transition = true;
        }
      }
      break;

    default: // état initial ou erreur d'automate !
      DesactiveCharge();
      RAZErreur();
      IndicateurBatterieChargee(false);
      if (MesureTension(&TensionCourante, false)) {
        EtatAutomate = EtatChargeFaible;
        Transition = true;
      }
      break;
  }
}