Surveillance de relais avec module ESP32-LoRa

Genèse du projet

Lors des discussions et réflexions au sein de l'ADRASEC73, face aux risques de délestages électriques de l'hiver 2022, est revenu sur la table la surveillance de nos transpondeurs. En effet, pour le moment nous découvrons que nous les avons exploités via l'alimentation secourue que quand ils ne répondaient plus suite à la décharge des batteries.

=== J'ai donc réfléchi à un moyen de supervision permettant de nous remonter l'état de l'alimentation des équipements en cas de passage en mode secouru afin d'adapter notre usage dans un but d'économie d'énergie et donc de longévité de nos capacités de transmission.

Cahier des charges

Voici, en vrac, les éléments que j'ai inscrit au cahier des charges afin de guider le développement du projet :

  • Surveillance de la présence/absence d'alimentation électrique par le secteur.
  • Envoi des infos par une "notification" sur la voie VHF du transpondeur et par une trame détaillée via un autre moyen de transmission.
  • Suivi de la tension batterie lors de la décharge et du début de la recharge.
  • La répétition de trames reçues émanant des autres transpondeurs afin de maximiser les chances de réception de l'info même si ça les doublonnes.
  • Possibilité, via une gateway, de recevoir des notifications plus détaillées mais moins essentielles.

Partie hardware

1. sur site

J'ai retenu le choix d'utiliser une platine TTGO comprenant un ESP32 pour la partie logique, un module LoRa pour la partie transmission et des GPIO pour l’interfaçage et le relevé d'informations. De plus, le protocole LoRa est robuste et peu consommateur d'énergie.

Il est complété d'un relai permettant le changement d'état d'un GPIO à la perte de l'alimentation secteur, d'un pont diviseur de tension afin de pouvoir mesurer la tension des batteries de manière convenable pour l'ESP32, d'un cordon pour piloter les Motorola GM340 des transpondeurs sur un canal avec un bip de fin de transmission (indiquant le passage sur batteries) et d'un abaisseur de tension afin de fournir les 5V nécessaire à l'ESP32.

Voici l'ensemble sur la platine de développement : photo_2023-06-03_21-10-54

Et le montage final avec la mise en boitier :

Partie software

Je n'ai aucune connaissance en C++, mais en me documentant un peu j'ai quand même décidé de me lancer. Mon code est certainement très améliorable, je je serais ravi d'avoir des retours afin de l'améliorer.

J'ai donc développé l'ensemble du code sur Visual Studio et PlatformIO.

1. sur site

Voici les différentes briques de mon code. Je commence par déclarer les différentes librairies nécessaires ainsi que les variables globales :

#include <Wire.h>
#include <SSD1306Wire.h>
#include <SPI.h>
#include <LoRa.h>

SSD1306Wire display(0x3c, SDA, SCL); //Déclaration des pins de l'écran

const int pinRelais230 = 15;                    // GPIO relais présence 230V à surveiller
const int pinTension = 36;                       //GPIO Voltmetre
const int pinCmdTX = 25;                       //GPIO cmd TX
bool Secteur = true;
const long frequency = 430600000;     //Fréquence LoRa : 430.600MHz
#define SCK     5                                  // GPIO5  -- SX1278's SCK
#define MISO    19                               // GPIO19 -- SX1278's MISnO
#define MOSI    27                               // GPIO27 -- SX1278's MOSI
#define SS      18                                 // GPIO18 -- SX1278's CS
#define RST     13.8                            // GPIO13.8 -- SX1278's RESET
#define DI0     26                                 // GPIO26 -- SX1278's IRQ(Interrupt Request)

float TensionBatRead = 0;
float LastTension = 0;

Ensuite, je vais créer quelques fonctions que je pourrai appeler dans la suite du code. La première gère l'affichage sur le LCD en prenant en entré les différentes valeurs mesurées :

void Affichage(String ValSecteur, String ValTension) {
  display.clear();
  display.setFont(ArialMT_Plain_16);
  display.drawString(0, 0, "Cime Caron");
  display.setFont(ArialMT_Plain_10);
  display.drawString(0, 30, ValSecteur);
  display.drawString(0, 50, "Tension bat :" + ValTension + "V");
  display.display();
}

La suivante gère l'envoi des trames LoRa :

void LoRa_sendMessage(String message) {
  LoRa.beginPacket();               // Début du paquet
  LoRa.print(message);             // ajout des infos dans la trame
  LoRa.endPacket();                 //fin du paquet et envoi
}

Cette fonction mesure la tension en entrée sur la pin choisie de l'ESP32 qui ne peut varier que de 0 à 3,3V (d'où la présence du pont diviseur de tension). Je la convertie donc logiciellement pour la ramener à la vraie valeur de la batterie (aux environs de 12V). de plus, pour éviter des fausses valeurs, je fais une série de 10 mesures pour ne retourner que la plus élevée car, sur la série, j'obtiens plusieurs fois 0V.

float TensionBat() {
  float TensionBat = 0;
  float TableauMesures[] = {0};
  for (size_t i = 0; i < 10; i++)
  {
    TensionBatRead = analogRead(pinTension) * (13.8 / 4095);
    TableauMesures[i] = {TensionBatRead};
    if (TableauMesures[i]>TensionBat)
    {
      TensionBat = TableauMesures[i];
    } 
    delay(100);
  }
  return(TensionBat);  
}

Vient ensuite, la fonction d'initialisation (setup). Celle-ci s’exécute une seule fois au démarrage de l'ESP32. Les commentaires sont normalement assez explicites :

void setup() {
  Serial.begin(115200); //J'initialise la vitesse du port console qui me permet de suivre le déroulement de l’exécution du code
    digitalWrite(pinCmdTX, LOW); //Initialisation à OFF du GPIO de changement de canal

  //Pin relais présence 230V
  pinMode(pinRelais230, INPUT_PULLUP);     //mode INPUT_PULLUP
  pinMode(pinCmdTX, OUTPUT);                     //mode OUTPUT

  //voltmetre
  pinMode(pinTension,INPUT_PULLUP);         //mode INPUT_PULLUP

  //Ecran de démarrage
  Serial.println("Démarrage");
  display.init();
  display.clear();
  display.flipScreenVertically();
  display.setTextAlignment(TEXT_ALIGN_LEFT);
  display.setFont(ArialMT_Plain_16);
  display.drawString(0, 0, "Démarrage suveillance secteur");
  display.drawString(0, 35, "Cime Caron");
  display.display();
  delay(3000);

  //Module Lora - démarrage
  SPI.begin(SCK,MISO,MOSI,SS);
  LoRa.setPins(SS,RST,DI0);
  if (!LoRa.begin(frequency)) {
    Serial.println("Starting LoRa failed!");
    while (1);
  }
  LoRa.setTxPower(20);
  LoRa.receive(); //par défaut, en réception
}

La fonction loop va être exécutée en boucle, c'est donc ici que va se trouver le "coeur" de la supervision.

void loop() {
  Serial.begin(115200);
  int Presence230; // Déclaration de la variable du dernier état enregistré
  int CompteurTemps; //Déclaration de la variable pour stoker le temps passé
  String  TxtPresenceSecteur; // Déclaration de la variable contenant le texte

  //Vérification de la présence de 230V et actions en fonction de l'état retourné.
  Presence230 = digitalRead(pinRelais230); 

  if (Presence230 == 0) { // Si on perd l'alim 230V
    CompteurTemps = 0; //Initialisation du compteur de temps depuis la perte du 230V
    Secteur = false;
    digitalWrite(pinCmdTX, HIGH); //On passe sur le canal secours sur le TX 
    while (Presence230 == 0) //tant qu'on est sur batterie
    {
      String CTps_string = String(CompteurTemps);
      TxtPresenceSecteur = "Sur Batterie : " + CTps_string + " min.";
      Serial.println(TxtPresenceSecteur); //affichage sur la console
      Affichage(TxtPresenceSecteur, String(TensionBat())); //affichage sur l'écran LCD
      //Transmission LoRa
      String MessageLora = "Caron\n" + TxtPresenceSecteur + "\nTension Batterie : " + String(TensionBat()) + "V.";
      if ((CompteurTemps % 15) == 0) //si on est 1/4h plus tard
      {
        LoRa_sendMessage(MessageLora); //nouvelle transmission LoRa
      }
      Serial.println("TX Lora : " + MessageLora);
      CompteurTemps++; //on incrémente le compteur de temps
      delay(60000); //on attend 1 min 
      Presence230 = digitalRead(pinRelais230);  //on lit a nouveau l'état de l'alimentation
    }
  }
  else { //Si on retrouve l'alim 230V
    digitalWrite(pinCmdTX, LOW); //On passe sur le canal normal sur le TX 
    TxtPresenceSecteur = "Sur Secteur 230V";
    Serial.println(TxtPresenceSecteur);
    Affichage(TxtPresenceSecteur, String(TensionBat()));
    //on regarde le dernier état de la présence secteur (Secteur). Si on était sur batterie, on envoie 
    //un message Lora pour dire que le courant est revenu et d'autres 15, 30, 45 et 60min plus tard. Sinon rien.
    if (Secteur == false)
    {
      for (size_t i = 0; i < 4; i++)
      {
        if (Presence230 == 1)
        {
          /* envoie message retour secteur + tension baterie toutes les 15min pendant 1h */
          String MessageLora = "Caron\n" + TxtPresenceSecteur + "\nTension Batterie : " + TensionBat() + "V.";
          LoRa_sendMessage(MessageLora);
          Serial.println("TX Lora : " + MessageLora);
          Affichage(TxtPresenceSecteur, String(TensionBat()));

          for (size_t i = 0; i < 4; i++) // et on vérifie qu'on n'a pas une coupure entre 2 messages Lora
          {
            Presence230 = digitalRead(pinRelais230);
            Affichage(TxtPresenceSecteur, String(TensionBat()));
            if (Presence230 == 0) //Si coupure on sort de la boucle
            {
              break;
            }            
            delay(60000); //sinon on attend 1min
          }          
          if (Presence230 == 0) //si on est sur secteur, on sort de la boucle
          {
            break;
          }
        }
        else {
          //on sort de la boucle for
          break;
        }
      }
      Secteur = true;
      Affichage(TxtPresenceSecteur, String(TensionBat()));
    }    
    delay(60000); //on attend 1 min
  }

  //Code digipeater pour  les autres supervisions de relais
  //Si on reçoit une trame d'un autre transpondeur, on la ré-émet afin de maximiser les chances de réception par la gateway 
  //et on ajoute le chemin de la trame
  int packetSize = LoRa.parsePacket();
  if (packetSize) {
    while (LoRa.available()) {
      String messageRX = "via C Caron>>  " + LoRa.readString(); //ajout du site qui à répété
      Serial.println(messageRX);
      display.setTextAlignment(TEXT_ALIGN_RIGHT);
      display.drawString(0, 0, "RX>>TX"); //affichage sur l'écran LCD d'une ré-émission
      display.display();
      delay(1000);
      LoRa_sendMessage(messageRX);
    }
  }
}

Mise en place sur sites

à venir après la mise en coffret

La Gateway

Elle fera l'objet du prochain article : Réception de la surveillance de relais

Article précédent Article suivant