Energy Management System : real & factor power, délesteur, ESP32

Le compteur Linky n’affiche pas la puissance active, ni la puissance réinjectée tant que la demande spécifique n’est pas faite à ENEDIS.

Le compteur Linky affiche donc la puissance apparente positive qui est saturé à 0.

Le compteur Linky n’a pas de wi fi donc non communicant pour gérer de l’autoconsommation avec du photovoltaïque, ni la recharge d’une batterie domestique, ni la recharge d’un véhicule.

Par conséquent, le compteur linky ne permet pas de communiquer pour faire un délestage. Les applications cloud des energy meter vendu dans le commerce n’ont pas de période d’échantillonnage correcte pour faire varier le chargement d’un véhicule via l’autoconsommation photovoltaïque lors de couverture nuageuse variable, ni de faire la commande de délestage.

Donc, on propose de faire EMS (Energy Management System) pour mesurer la puissance monophasée de 8KVA (30A, 230V) avec un wifi d’où l’utilisation d’un ESP32 avec monitoring.

Il y a déjà pleins de choses sur internet avec des méthodes de programmations et des capteurs différents mais sans donner la précision des mesures. Certains utilisent des Library de capteur qui doivent être calibré, d’autres des circuits spécialisés tel que ATM90E26, PZEM-004T,….)

Notre choix a été d’utiliser des capteurs basiques pour tester plusieurs méthodes tout en isolant la tension secteur du micro tel que zmpt101b sensor voltage (0.5V d’amplitude de sortie) pour 311V et le sensor current SCT-013-050 qui passe de 50A d’amplitude à 1V. les 2 permettent d’être lu directement par l’ESP32.

Mais pour que le micro puisse mesurer le facteur de puissance positif et négatif, il faut une période d’échantillonnage convenable et avoir l’ondulation globale donc avec une tension d’offset de 1.65V

Le rapport de transformation zmpt101b 2mA de la tension secondaire dépend du rapport des résistances d’entrée et de sortie

https://www.alldatasheet.fr/datasheet-pdf/view/1821973/MICRO-TRANSFORMER/ZMPT101B.html

Avec le schéma électrique suivant la tension théorique sur la résistance R1 sera égale à la tension suivante avec une tension secteur de 240V

U2=240V*100/820k=0.029V

Puis, il y a 2 filtres inverseurs avec LM358 en cascades avec un gain de 10 chacun en théorie.

Mais, pour une amplitude secteur 340V (240V RMS), l’amplitude est de 0.55V avec un offset de 2.25V étant alimenté en 4.85V donc avec l’ESP 32 la précision de la mesure sera de 2V.

Pour une alimentation en 3.3V, l’amplitude de sortie est de 0.25V avec 1.65V d’offset.

Une fréquence d’échantillonnage de 0.2ms va être utilisé pour avoir 100 échantillons de la période secteur 20ms ce qui permettra d’avoir une mesure de précision du déphasage de 3.6° entre la tension et le courant. Mais, la méthode de la mesure du facteur de puissance par la puissance moyenne divisé par la puissance apparente mais il faudra calibrer le déphasage du capteur

Mais dans un premier temps, le sensor voltage va être testé sans routine d’interruption sur 1000 échantillon donc 10 périodes

Les méthodes de mesures sont bien expliquées sur ce lien

Voici le programme pour lire la tension secteur

// ZMPT101B sensor voltage
// Mesure de tension AC RMS

const int pinZMPT = 12;  //240V 50Hz
const int nombreEchantillons = 1000;   //10 periodes
float facteurCalibration = 1.00;
  float somme = 0;
  float sommeCarres = 0;

  int valeurADC;
  float tensionADC;
  float offset=3103;
  float signal1;
  float rmsADC;
  float tensionRMS;

  float valeurMax;  //
  float valeurMin;
  float amplitude;

void setup() {
  Serial.begin(115200);

  analogReadResolution(12);       // ADC 12 bits : 0 à 4095
  analogSetAttenuation(ADC_11db); // Plage ADC environ 0 à 3,3 V
}

void loop() {

  valeurMax = 0;
  valeurMin = 4095;

sommeCarres = 0;
  //  calcul des sommes   verif offset   2.5V=>3103
  for (int i = 0; i < nombreEchantillons; i++) {
    valeurADC = analogRead(pinZMPT);
    somme += valeurADC;            //
       
    if (valeurADC > valeurMax)   valeurMax = valeurADC;
//    if (valeurADC < valeurMin)   valeurMin = valeurADC;
        
    signal1 = valeurADC - offset;      // Suppression de l'offset pour RMS
    sommeCarres += signal1 * signal1;   // Carré du signal
    delayMicroseconds(200);        //pour 20ms secteur  mesure sur 100echantillon par periode
  }
// calcul de l'offset
  offset = somme / nombreEchantillons;
  somme=0;
// determination amplitude
  amplitude=((valeurMax-offset)*3.3*facteurCalibration)/4095;


  // Calcul RMS du signal alternatif
   rmsADC = sqrt(sommeCarres / nombreEchantillons);

   tensionADC = (rmsADC / 4095.0) * 3.3;
   tensionRMS = tensionADC * facteurCalibration;

  // Affichage monitor serie

  Serial.print("Offset=");
  Serial.print(offset, 1);

  Serial.print(" | RMS=");
  Serial.print(tensionRMS, 3);

  Serial.print(" | ampl=");    //peu precis
  Serial.println(amplitude, 3);

}

Dans la copie d’écran, le facteur de calibration est de 1 donc il faudra le passer à 582 en réel.
On remarquera que la mesure de l’amplitude n’est pas précise. De plus, le capteur déforme un peu le signal sinusoïdal, donc le rapport de racine de 2 entre l’amplitude de la valeur efficace ne fonctionne pas.

La suite des explications pour le teste du capteur de courant avec son offset avec la méthode de mesure de puissance et facteur de puissance ce sera pour bientôt.

Si l’on désire vérifier le déphasage entre la tension et le courant secteur avec une période d’échantillonnage rapide, le serial plotter ne peut pas être utilisé car l’envoie d’un float met 200us avec 115200 bauds.

De plus, le serial.print bloque la routine d’interruption.

Evidemment, un oscilloscope peut faire le job, mais utiliser le micro permet de vérifier l’acquisition.

Donc la solution est d’enregistrer les données dans une table tous les 4 secondes ici et envoie les données en bloquant les acquisitions.

Le programme utilise une routine d’interruption pour acquérir les 50 échantillons de la tension secteur avec si la période d’échantillonnage est de 400us, une période est visualisable.

Si l’on choisit une routine d’interruption de 800us alors 2 périodes seront visualisables

Voici le programme

//TTGO dispaly   ZMPT101B sensor voltage
// Mesure de tension et intensity  AC RMS 

#include <TFT_eSPI.h>
#include <SPI.h>

hw_timer_t * timer = NULL;

TFT_eSPI tft = TFT_eSPI();  //library   1,4" color 135, 240


const int nbrEchantillons = 2500;   // periodes
const float facteurCalibrationV = 582.00;
const float facteurCalibrationI = 1.00;
 int sampler=0;
 bool flag;
 bool flagenvoie;
  float sommeV = 0;
  float sommeI=0;
  float sommeCarres = 0;
  float sommeCarresI = 0;

  int valeurADC;  // tension,
  int valeurI;    //current

const int nbrsampler=50;
  int16_t tableV[nbrsampler];
  int16_t tableI[nbrsampler];
  byte indexTable=0;



  int offsetV=3103;
  int offsetI=2048;
  int signalV;
  int signalI;

  float rmsV;
  float tensionRMS;

  float rmsI;
  float currentRMS;

  float MaxV;  //
  float minV;
  float amplitudeV;

  float MaxI;  //
  float amplitudeI;

void IRAM_ATTR timer_isr() {    //routine interruption 1 seconde
 if (flagenvoie==1) {flag=0;} else  {flag=1;}    //bloque l'acquisition lors d'envoie des data
  sampler++;     //10 periode de 20ms
     
}//fin isr




void setup() {

  Serial.begin(115200);

  pinMode(22, OUTPUT);
  tft.init();                 //135 × 240.
  tft.setRotation(0); //0 format portrait a l'endroit  2 180°

  tft.fillScreen(TFT_BLACK);   
   
  tft.setCursor(0, 0, 2);  //// (x,y,taille)
  tft.setTextColor(TFT_GREEN,TFT_BLACK); //(couleur police,    background police)
  tft.println("IUT GEII SOISSONS");

  analogReadResolution(12);       // ADC 12 bits : 0 à 4095
  analogSetAttenuation(ADC_11db); // Plage ADC environ 0 à 3,3 V

uint8_t timer_id = 1;     
  uint16_t prescaler = 80; // Between 0 and 65 535
  int threshold = 800; // 800us

  timer = timerBegin(timer_id, prescaler, true);
  timerAttachInterrupt(timer, &timer_isr, true);
  timerAlarmWrite(timer, threshold, true);
  timerAlarmEnable(timer);

}

void loop() {

if (sampler>nbrEchantillons)  {   //400us*10000=4s   800us*2500=2
  flagenvoie=1;
  
  offsetV = sommeV / nbrEchantillons;
  offsetI = sommeI / nbrEchantillons;

  for (uint16_t i = 0; i < nbrsampler; i++)
    {
      Serial.print(tableV[i]);
      Serial.print(",");
      Serial.println(tableI[i]);
    }


// determination amplitude  
  amplitudeV=((MaxV-offsetV)*0.47);
  amplitudeI=((MaxI-offsetI)*0.0035);

  // Calcul RMS tension alternatif
   rmsV = sqrt(sommeCarres / nbrEchantillons);

   tensionRMS = (rmsV *0.47);           //3.3*582/4095=0.47
   

   rmsI = sqrt(sommeCarresI / nbrEchantillons);

  currentRMS= (rmsI*0.0035);            //3.3* /4095=0.004


tft.setCursor(0, 20, 4);// (x,y,taille)
tft.setTextColor(TFT_GREEN,TFT_BLACK);
tft.print("V:  ");
tft.print(tensionRMS,0);
tft.print("   ");

tft.setCursor(0, 50, 4);// (x,y,taille)
tft.setTextColor(TFT_GREEN,TFT_BLACK);
tft.print("aV:  ");
tft.print(amplitudeV,0);
tft.print("   ");

tft.setCursor(0, 80, 4);// (x,y,taille)
tft.setTextColor(TFT_RED,TFT_BLACK);
tft.print("I: ");
tft.print(currentRMS,3);
tft.print("   ");    

tft.setCursor(0, 110, 4);// (x,y,taille)
tft.setTextColor(TFT_RED,TFT_BLACK);
tft.print("aI:  ");
tft.print(amplitudeI,3);
tft.print("   ");

sommeCarres=0;
sommeCarresI=0;

  sommeV=0;
  sommeI=0;

    MaxV = 0;
  minV = 4095;
    MaxI = 0;

      sampler=0;
  flagenvoie=0;
  }  //end sampler



 if (flag==1)   {
   flag=0;
    if (digitalRead(22)==0)   {digitalWrite(22,1); } else {digitalWrite(22,0); }
    valeurADC = analogRead(27);      //pinZMPT sensor voltage
    sommeV += valeurADC;            //
       
    if (valeurADC > MaxV)   {MaxV = valeurADC;}
        
    signalV = valeurADC - offsetV;      // Suppression de l'offset pour RMS
    
    valeurI = analogRead(26);      //pin intensity
    sommeI += valeurI;
    signalI = valeurI - offsetI;   
    if (valeurI > MaxI)   {MaxI = valeurI;}

    sommeCarres += signalV * signalV;    // Carré du signal
    sommeCarresI += signalI * signalI;   // Carré du signal

    tableV[indexTable] = signalV;
    tableI[indexTable] = signalI;
    indexTable++;
    if  (indexTable>=nbrsampler)    {indexTable=0; }
                                                }  //flag==1


}   //end loop


On peut remarquer que le calcul de l’offset est légèrement décalé (amplitude positive n’est pas identique à l’amplitude négative), le ZMT provoque quelques distorsions du signal secteur ainsi que l’ADC.
Le courant a des distorsions souvent à cause des systèmes électroniques qui utilisent des redresseurs filtrés en tension mais aussi si le courant est faible.

Maintenant que la phase tension est bien synchro avec le courant et que les facteurs d’échelles sont bien calibrés avec une précision de la tension à 0.47V et celui du courant 0.0035A.

Alors la puissance apparente et active peut être programmer.

Sur des petits courants, de 0.3A AC, avec un capteur de 1V pour 50A SCT-013-050 , la mesure du courant est assez bruitée comme on peut l’observer sur la figure suivante.

D’ailleurs, ce bruit est mesurable car lorsqu’il n’y a pas de courant qui est de 0.034A.
Ce courant n’est pas significatif par rapport à un système qui peut mesurer 50A.
Il faudrait un amplificateur avec différents calibres pour améliorer cette précision pour les petits courants

Pour un courant efficace de 3.1A, on peut observer le déphase entre la tension et le courant qui est environ 45° donne un facteur de puissance de 0.72

Par contre, la précision de la mesure de la puissance active et apparente est correcte même pour les petits courants en utilisant la puissance instante divisée par 2500 échantillons toutes les 2 secondes

La puissance est bien nulle lorsqu’il n’y en a pas de courant

Voici le programme de base utilisant la somme de puissance instantanée divisée par 2500 échantillons pour avoir la puissance moyenne donc le calcul de l’énergie se fait sur l’intégration de la puissance tous les 2s.

/TTGO dispaly   ZMPT101B sensor voltage
// Mesure de tension et intensity  AC RMS   power energy

#include <TFT_eSPI.h>
#include <SPI.h>

hw_timer_t * timer = NULL;

TFT_eSPI tft = TFT_eSPI();  //library   1,4" color 135, 240


const int nbrEchantillons = 2500;   // periodes

 int sampler=0;
 bool flag;
 bool flagenvoie;
  float sommeV = 0;
  float sommeI=0;
  float sommeCarres = 0;
  float sommeCarresI = 0;

  float sommePower;

  int valeurADC;  // tension,
  int valeurI;    //current

const int nbrsampler=50;
const float calibreI=0.019;
const float calibreV=0.5;

  int16_t tableV[nbrsampler];
  int16_t tableI[nbrsampler];
  byte indexTable=0;

  int offsetV=3103;
  int offsetI=2048;
  int signalV;
  int signalI;

  float rmsV;
  float tensionRMS;

  float rmsI;
  float currentRMS;

  float MaxV;  //
  float minV;
  float amplitudeV;

  float MaxI;  //
  float amplitudeI;

  float Power;
  float S;      //power appararente
  float FP;     //factor power

  float joule2;  
  float Energy;

void IRAM_ATTR timer_isr() {    //routine interruption 800u seconde
 if (flagenvoie==1) {flag=0;} else  {flag=1;}    //bloque l'acquisition lors d'envoie des data
  sampler++;     //100 periode de 20ms
     
}//fin isr




void setup() {

  Serial.begin(115200);

  pinMode(22, OUTPUT);
  tft.init();                 //135 × 240.
  tft.setRotation(0); //0 format portrait a l'endroit  2 180°

  tft.fillScreen(TFT_BLACK);   
   
  tft.setCursor(0, 0, 2);  //// (x,y,taille)
  tft.setTextColor(TFT_GREEN,TFT_BLACK); //(couleur police,    background police)
  tft.println("IUT GEII SOISSONS");

  analogReadResolution(12);       // ADC 12 bits : 0 à 4095
  analogSetAttenuation(ADC_11db); // Plage ADC environ 0 à 3,3 V

uint8_t timer_id = 1;     
  uint16_t prescaler = 80; // Between 0 and 65 535
  int threshold = 800; // 800us

  timer = timerBegin(timer_id, prescaler, true);
  timerAttachInterrupt(timer, &timer_isr, true);
  timerAlarmWrite(timer, threshold, true);
  timerAlarmEnable(timer);

}

void loop() {

if (sampler>nbrEchantillons)  {   //400us*10000=4s   800us*2500=2
  flagenvoie=1;
  
  offsetV = sommeV / nbrEchantillons;
  offsetI = sommeI / nbrEchantillons;

  for (uint16_t i = 0; i < nbrsampler; i++)
    {
      Serial.print(tableV[i]);
      Serial.print(",");
      Serial.println(tableI[i]*4);
    }


// determination amplitude  
  amplitudeV=((MaxV-offsetV)*calibreV);
  amplitudeI=((MaxI-offsetI)*calibreI);

  // Calcul RMS tension alternatif
   rmsV = sqrt(sommeCarres / nbrEchantillons);
   tensionRMS = (rmsV *calibreV);           //3.3*582/4095=0.47
   
   rmsI = sqrt(sommeCarresI / nbrEchantillons);
   currentRMS= (rmsI*calibreI);            //3.3* /4095=0.004

   
tft.setCursor(0, 20, 4);// (x,y,taille)
tft.setTextColor(TFT_GREEN,TFT_BLACK);
tft.print("V:  ");
tft.print(tensionRMS,0);
tft.print("   ");

tft.setCursor(0, 50, 4);// (x,y,taille)
tft.setTextColor(TFT_GREEN,TFT_BLACK);
tft.print("aV:  ");
tft.print(amplitudeV,0);
tft.print("   ");

tft.setCursor(0, 80, 4);// (x,y,taille)
tft.setTextColor(TFT_RED,TFT_BLACK);
tft.print("I: ");
tft.print(currentRMS,3);
tft.print("   ");    


/*tft.setCursor(0, 110, 4);// (x,y,taille)
tft.setTextColor(TFT_RED,TFT_BLACK);
tft.print("aI:  ");
tft.print(amplitudeI,3);
tft.print("   ");
*/

Power=(sommePower*calibreV*calibreI)/ nbrEchantillons;    //power real
S=currentRMS*tensionRMS  ;         //power apparent
if (currentRMS>0.05) {FP=Power/S;  }   else {FP=1;  }   //factor power

joule2=joule2+Power;     //W*2s
Energy=(joule2*2)/3600;   //wh

tft.setCursor(0, 110, 4);// (x,y,taille)
tft.setTextColor(TFT_RED,TFT_BLACK);
tft.print("E:  ");
tft.print(Energy,0);
tft.print("   ");
                          
tft.setCursor(0, 140, 4);// (x,y,taille)
tft.setTextColor(TFT_BLUE,TFT_BLACK);
tft.print("P:  ");
tft.print(Power,0);
tft.print("    ");

tft.setCursor(0, 170, 4);// (x,y,taille)
tft.setTextColor(TFT_BLUE,TFT_BLACK);
tft.print("S:  ");
tft.print(S,0);
tft.print("   ");

tft.setCursor(0, 200, 4);// (x,y,taille)
tft.setTextColor(TFT_BLUE,TFT_BLACK);
tft.print("FP:  ");
tft.print(FP,2);
tft.print("   ");

sommePower=0;

sommeCarres=0;
sommeCarresI=0;

  sommeV=0;
  sommeI=0;

    MaxV = 0;
  minV = 4095;
    MaxI = 0;

      sampler=0;
  flagenvoie=0;
  }  //end sampler



 if (flag==1)   {
   flag=0;
    if (digitalRead(22)==0)   {digitalWrite(22,1); } else {digitalWrite(22,0); }
    valeurADC = analogRead(27);      //pinZMPT sensor voltage
    sommeV += valeurADC;            //
       
    if (valeurADC > MaxV)   {MaxV = valeurADC;}
        
    signalV = valeurADC - offsetV;      // Suppression de l'offset pour RMS
    
    valeurI = analogRead(26);      //pin intensity
    sommeI += valeurI;
    signalI = valeurI - offsetI;   
    if (valeurI > MaxI)   {MaxI = valeurI;}

    sommeCarres += signalV * signalV;    // Carré du signal
    sommeCarresI += signalI * signalI;   // Carré du signal
    
    sommePower  += signalI * signalV;

    tableV[indexTable] = signalV;
    tableI[indexTable] = signalI;
    indexTable++;
    if  (indexTable>=nbrsampler)    {indexTable=0; }
                                                }  //flag==1


}   //end loop

Sachant que la puissance instantanée contient une ondulation du double de la puissance secteur donc à 100 Hz. Donc pour avoir la puissance moyenne, il est possible d’utiliser une filtre IIR passe pas de fréquence de coupure de 1s qui donnera une puissance plus lissée.

Dans un premier temps, un filtre du premier ordre sera utilisé qui donne une atténuation de 0.016 donc 1.6% pour une fréquence de coupure de 0.16Hz et une constante de temps de 1s.
Donc l’énergie peut être déterminée toutes les 800us.

Evidemment, un filtre du second ordre aurait pu être utilisée. Mais avec une fréquence de coupure de 1Hz avec 800us d’échantillonnage, l’atténuation sera de 0.04 mais la précision des coefficients du filtre demandé ne pourra être réaliser avec 7 chiffres significatifs
a1 = -1.99289;
a2 = 0.99291;

Par contre, il serait possible de mettre 2 filtres du premier ordre en cascade si l’on veut améliorer la précision.

Voici le programme avec la puissance moyenne par le filtre

/TTGO dispaly   ZMPT101B sensor voltage
// Mesure de tension et intensity  AC RMS  filtre IIR power energy

#include <TFT_eSPI.h>
#include <SPI.h>

hw_timer_t * timer = NULL;

TFT_eSPI tft = TFT_eSPI();  //library   1,4" color 135, 240


const int nbrEchantillons = 2500;   // periodes

 int sampler=0;
 bool flag;
 bool flagenvoie;
  float sommeV = 0;
  float sommeI=0;
  float sommeCarres = 0;
  float sommeCarresI = 0;

  int valeurADC;  // tension,
  int valeurI;    //current

const int nbrsampler=50;
const float calibreI=0.019;
const float calibreV=0.5;

  int16_t tableV[nbrsampler];
  int16_t tableI[nbrsampler];
  byte indexTable=0;

  int offsetV=3103;
  int offsetI=2048;
  int signalV;
  int signalI;

  float rmsV;
  float tensionRMS;

  float rmsI;
  float currentRMS;

  float MaxV;  //
  float minV;
  float amplitudeV;

  float MaxI;  //
  float amplitudeI;

  float Powerinstant;
  float Power;
  float Powern;
  float Powerf;
  float S;      //power appararente
  float FP;     //factor power

  float joule2;  
  float Energy;

//  const float alpha = 1.0f - expf(-0.0008 / cstemps);    //0.0016    cstemps=1
const float alpha=0.0016;

void IRAM_ATTR timer_isr() {    //routine interruption 800u seconde
 if (flagenvoie==1) {flag=0;} else  {flag=1;}    //bloque l'acquisition lors d'envoie des data
  sampler++;     //100 periode de 20ms
     
}//fin isr




void setup() {

  Serial.begin(115200);

  pinMode(22, OUTPUT);
  tft.init();                 //135 × 240.
  tft.setRotation(0); //0 format portrait a l'endroit  2 180°

  tft.fillScreen(TFT_BLACK);   
   
  tft.setCursor(0, 0, 2);  //// (x,y,taille)
  tft.setTextColor(TFT_GREEN,TFT_BLACK); //(couleur police,    background police)
  tft.println("IUT GEII SOISSONS");

  analogReadResolution(12);       // ADC 12 bits : 0 à 4095
  analogSetAttenuation(ADC_11db); // Plage ADC environ 0 à 3,3 V

uint8_t timer_id = 1;     
  uint16_t prescaler = 80; // Between 0 and 65 535
  int threshold = 800; // 800us

  timer = timerBegin(timer_id, prescaler, true);
  timerAttachInterrupt(timer, &timer_isr, true);
  timerAlarmWrite(timer, threshold, true);
  timerAlarmEnable(timer);

}

void loop() {

if (sampler>nbrEchantillons)  {   //400us*10000=4s   800us*2500=2secondes affichage et bilan
  flagenvoie=1;
  
  offsetV = sommeV / nbrEchantillons;
  offsetI = sommeI / nbrEchantillons;

  for (uint16_t i = 0; i < nbrsampler; i++)
    {
      Serial.print(tableV[i]);
      Serial.print(",");
      Serial.println(tableI[i]*4);
    }


// determination amplitude  
  amplitudeV=((MaxV-offsetV)*calibreV);
  amplitudeI=((MaxI-offsetI)*calibreI);

  // Calcul RMS tension alternatif
   rmsV = sqrt(sommeCarres / nbrEchantillons);
   tensionRMS = (rmsV *calibreV);           //3.3*582/4095=0.47
   
   rmsI = sqrt(sommeCarresI / nbrEchantillons);
   currentRMS= (rmsI*calibreI);            //3.3* /4095=0.004

   
tft.setCursor(0, 20, 4);// (x,y,taille)
tft.setTextColor(TFT_GREEN,TFT_BLACK);
tft.print("V:  ");
tft.print(tensionRMS,0);
tft.print("   ");

tft.setCursor(0, 50, 4);// (x,y,taille)
tft.setTextColor(TFT_GREEN,TFT_BLACK);
tft.print("aV:  ");
tft.print(amplitudeV,0);
tft.print("   ");

tft.setCursor(0, 80, 4);// (x,y,taille)
tft.setTextColor(TFT_RED,TFT_BLACK);
tft.print("I: ");
tft.print(currentRMS,3);
tft.print("   ");    


/*tft.setCursor(0, 110, 4);// (x,y,taille)
tft.setTextColor(TFT_RED,TFT_BLACK);
tft.print("aI:  ");
tft.print(amplitudeI,3);
tft.print("   ");
*/

Power=Powerf*calibreV*calibreI;

S=currentRMS*tensionRMS  ;         //power apparent
if (currentRMS>0.05) {FP=Power/S;  }   else {FP=1;  }   //factor power


tft.setCursor(0, 110, 4);// (x,y,taille)
tft.setTextColor(TFT_RED,TFT_BLACK);
tft.print("E:  ");
tft.print(Energy,0);
tft.print("   ");
                          
tft.setCursor(0, 140, 4);// (x,y,taille)
tft.setTextColor(TFT_BLUE,TFT_BLACK);
tft.print("P:  ");
tft.print(Power,0);
tft.print("    ");

tft.setCursor(0, 170, 4);// (x,y,taille)
tft.setTextColor(TFT_BLUE,TFT_BLACK);
tft.print("S:  ");
tft.print(S,0);
tft.print("   ");

tft.setCursor(0, 200, 4);// (x,y,taille)
tft.setTextColor(TFT_BLUE,TFT_BLACK);
tft.print("FP:  ");
tft.print(FP,2);
tft.print("   ");


sommeCarres=0;
sommeCarresI=0;

  sommeV=0;
  sommeI=0;

    MaxV = 0;
  minV = 4095;
    MaxI = 0;

      sampler=0;
  flagenvoie=0;
  }  //end sampler



 if (flag==1)   {
   flag=0;
    if (digitalRead(22)==0)   {digitalWrite(22,1); } else {digitalWrite(22,0); }
    valeurADC = analogRead(27);      //pinZMPT sensor voltage
    sommeV += valeurADC;            //
       
    if (valeurADC > MaxV)   {MaxV = valeurADC;}
        
    signalV = valeurADC - offsetV;      // Suppression de l'offset pour RMS
    
    valeurI = analogRead(26);      //pin intensity
    sommeI += valeurI;
    signalI = valeurI - offsetI;   
    if (valeurI > MaxI)   {MaxI = valeurI;}

    sommeCarres += signalV * signalV;    // Carré du signal
    sommeCarresI += signalI * signalI;   // Carré du signal
    
    Powerinstant = signalI * signalV;      
    Powerf = Powern + alpha * (Powerinstant - Powern);  //puissance flitrée
    Powern = Powerf;           //n-1

Power=Powerf*calibreV*calibreI;
joule2=joule2+Power;     //
Energy=(joule2*0.0008)/3600;   //wh

    tableV[indexTable] = signalV;    //enregiste les 50 derniers mesures
    tableI[indexTable] = signalI;
    indexTable++;
    if  (indexTable>=nbrsampler)    {indexTable=0; }
                                                }  //flag==1


}   //end loop

La méthode du filtre IIR passe bas peut être utilisée aussi sur la mesure du courant RMS et pour enlever la composante continue un filtre passe haut donc plus besoin de déterminer l’offset

ibrut[n]→filtre passe-haut →i[n]→i^2[n]→IIR RMS

mais ce sera pour plus tard

Donc pour mesurer la tension et le courant un filtre passe haut du premier ordre a été utilisé avec une fréquence de coupure de 2Hz pour retirer la composante continue 0Hz mais gardant la composante alternative de 50Hz.
Puis un filtre passe bas du signal pour avoir la valeur efficace RMS du signal

Le temps de calcul pour avoir le courant, la tension et la puissance est de 250us.

Il n’est pas possible de mettre ce temps de calcul dans la routine d’interruption qui est pourtant de 800us car cela bug.

Par contre avec 1000us de la routine cela passe.

mais, on a refait un flag dans la fonction loop pour garder 800us

L’avantage du filtre passe bas est d’avoir un affichage stable qui évolue progressivement donc de façon moins abrupte surtout au niveau du courant qui est assez variable.

Voici le programme ou il n’y a pas besoin de mettre les données dans une table, c’est juste une vérification.

/TTGO dispaly   ZMPT101B sensor voltage
// Mesure de tension et intensity  AC RMS  filtre IIR passe haut et passe bas 

#include <TFT_eSPI.h>
#include <SPI.h>

hw_timer_t * timer = NULL;

TFT_eSPI tft = TFT_eSPI();  //library   1,4" color 135, 240


const int nbrEchantillons = 2500;   // pour affichage
volatile int sampler=0;
volatile  bool flag;
volatile  bool flagenvoie;


  float valeurADC,valeurADCn ;  // tension,
  float valeurI,valeurIn ;    //current

const int nbrsampler=50;       
const float calibreI=0.019;
const float calibreV=0.5;

  int16_t tableV[nbrsampler];
  int16_t tableI[nbrsampler];
  byte indexTable=0;

  float signalV,signalVn;
  float signalI,signalIn;

  float rmsV, rmsVn;
  float tensionRMS;

  float rmsI, rmsIn;
  float currentRMS;

  float MaxV;  //
  float minV;
  float amplitudeV;

  float MaxI;  //
  float amplitudeI;

  float Powerinstant;
  float Power;
  float Powern;
  float Powerf;
  float S;      //power appararente
  float FP;     //factor power

  float joule2;  
  float Energy;

//  const float alpha = 1.0f - expf(-0.0008 / cstemps);    //0.0016    cstemps=1
const float alphaPB=0.0016;
const float alphaHP =0.9901;

void IRAM_ATTR timer_isr() {    //routine interruption 800u seconde
if (flagenvoie==1) {flag=0;} else  {flag=1;} 
 sampler++;     //affichage tous les 2s


}//fin isr




void setup() {

  Serial.begin(115200);

  pinMode(25, OUTPUT);
  tft.init();                 //135 × 240.
  tft.setRotation(0); //0 format portrait a l'endroit  2 180°

  tft.fillScreen(TFT_BLACK);   
   
  tft.setCursor(0, 0, 2);  //// (x,y,taille)
  tft.setTextColor(TFT_GREEN,TFT_BLACK); //(couleur police,    background police)
  tft.println("IUT GEII SOISSONS");

  analogReadResolution(12);       // ADC 12 bits : 0 à 4095
  analogSetAttenuation(ADC_11db); // Plage ADC environ 0 à 3,3 V

uint8_t timer_id = 1;     
  uint16_t prescaler = 80; // Between 0 and 65 535
  int threshold = 800; // 800us

  timer = timerBegin(timer_id, prescaler, true);
  timerAttachInterrupt(timer, &timer_isr, true);
  timerAlarmWrite(timer, threshold, true);
  timerAlarmEnable(timer);
}




void loop() {

if (flag==1)  {flag=0;
// if (digitalRead(25)==0)   {digitalWrite(25,1); } else {digitalWrite(25,0); }
digitalWrite(25,1);    //temps de calcul du flag 250us
valeurADC = analogRead(27);      //pinZMPT sensor voltage     
    signalV = alphaHP * (signalVn + valeurADC - valeurADCn);  //filtre passe haut
    signalVn=signalV;       //valeur precedente
    valeurADCn=valeurADC;    
    rmsV = rmsVn + alphaPB * (signalV*signalV - rmsVn);  //filtre passe bas
    rmsVn=rmsV;             //valeur precedente
tensionRMS=(sqrt(rmsV))*calibreV;

    valeurI = analogRead(26);      //pin intensity
    signalI = alphaHP * (signalIn + valeurI - valeurIn);  //filtre passe haut
    signalIn=signalI;       //valeur precedente
    valeurIn=valeurI;    
    rmsI = rmsIn + alphaPB * (signalI*signalI - rmsIn);  //filtre passe bas
    rmsIn=rmsI;             //valeur precedente
currentRMS= (sqrt(rmsI))*calibreI; 
    
    Powerinstant = signalI * signalV;      
    Powerf = Powern + alphaPB * (Powerinstant - Powern);  //puissance flitrée
    Powern = Powerf;           //n-1

Power=Powerf*calibreV*calibreI;
joule2=joule2+Power;     //
Energy=(joule2*0.0008)/3600;   //wh


    tableV[indexTable] = signalV;    //enregiste les 50 derniers mesures
    tableI[indexTable] = signalI;
    indexTable++;
    if  (indexTable>=nbrsampler)    {indexTable=0; }

digitalWrite(25,0);

}// fin flag




if (sampler>2500)  {   //  800us*2500=2secondes affichage et bilan
 flagenvoie=1;

  for (uint16_t i = 0; i < nbrsampler; i++)
    {
      Serial.print(tableV[i]);
      Serial.print(",");
      Serial.println(tableI[i]*5);    //facteur d'echelle
    } 

tft.setCursor(0, 20, 4);// (x,y,taille)
tft.setTextColor(TFT_GREEN,TFT_BLACK);
tft.print("V:  ");
tft.print(tensionRMS,0);
tft.print("   ");

tft.setCursor(0, 50, 4);// (x,y,taille)
tft.setTextColor(TFT_GREEN,TFT_BLACK);
tft.print("aV:  ");
tft.print(amplitudeV,0);
tft.print("   ");

tft.setCursor(0, 80, 4);// (x,y,taille)
tft.setTextColor(TFT_RED,TFT_BLACK);
tft.print("I: ");
tft.print(currentRMS,3);
tft.print("   ");    

S=currentRMS*tensionRMS  ;         //power apparent
if (currentRMS>0.05) {FP=Power/S;  }   else {FP=1;  }   //factor power

tft.setCursor(0, 110, 4);// (x,y,taille)
tft.setTextColor(TFT_RED,TFT_BLACK);
tft.print("E:  ");
tft.print(Energy,0);
tft.print("   ");
                          
tft.setCursor(0, 140, 4);// (x,y,taille)
tft.setTextColor(TFT_BLUE,TFT_BLACK);
tft.print("P:  ");
tft.print(Power,0);
tft.print("    ");

tft.setCursor(0, 170, 4);// (x,y,taille)
tft.setTextColor(TFT_BLUE,TFT_BLACK);
tft.print("S:  ");
tft.print(S,0);
tft.print("   ");

tft.setCursor(0, 200, 4);// (x,y,taille)
tft.setTextColor(TFT_BLUE,TFT_BLACK);
tft.print("FP:  ");
tft.print(FP,2);
tft.print("   ");
sampler=0;
flagenvoie=0;
}  //fin flagaencoie et affichage

}   //end loop

Pourrait-on utiliser le double core de l’ESP32 entre l’affichage et la mesure ?

Exemple : Core 0 → mesure, Core 1 → TFT avec une synchronisation entre les 2 core

Evidement on peut utiliser les 2 core…

Pour simplifier le code de l’utilisation des 2 core avec la routine d’interruption, seule la tension est affichée.
Core 0 → mesure et calculs, Core 1 → TFT avec une synchronisation entre les 2

Voici le code

/TTGO display   double core
//Core 0 → mesure, Core 1 → TFT avec une synchronisation entre les 2
//isr de 500us et affichage tous les 1.25 s

#include <Arduino.h>
#include <TFT_eSPI.h>

TFT_eSPI tft;

#define PIN_ZMPT        27
#define N_ECHANTILLONS  2500
#define PERIODE_US      500
#define calibreV        0.5f

hw_timer_t *timer = NULL;

TaskHandle_t mesureTaskHandle = NULL;  // HANDLE DE LA TACHE


struct Mesure   // DONNEES PARTAGEES CORE 0 / CORE 1
{
  float tensionRMS;
  float moyenne;
  float moyenCarre;  
};

volatile Mesure resultat;
volatile bool nouvelleMesure ;

portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;  // PROTECTION DES DONNEES PARTAGEES


void IRAM_ATTR timerISR()   // INTERRUPTION TIMER
{
  BaseType_t xHigherPriorityTaskWoken = pdFALSE;

  // Réveille la tâche de mesure sur Core 0
  vTaskNotifyGiveFromISR(  mesureTaskHandle,  &xHigherPriorityTaskWoken  );

  if (xHigherPriorityTaskWoken)    {  portYIELD_FROM_ISR();  }
}



// CORE 0 : MESURE
// =====================================================

void mesureCore0(void *parameter)
{
const float alphaPB=0.0016;
const float alphaHP =0.9901;
float valeurADC,valeurADCn;
float signalV, signalVn;
float rmsV,rmsVn;
uint16_t echantillon;
float tensionRMS;


  while (true)   //loopcore0
  {
 if (digitalRead(25)==0)   {digitalWrite(25,1); } else {digitalWrite(25,0); }
// Attente de l'interruption 500 us
    ulTaskNotifyTake( pdTRUE,portMAX_DELAY   );

 valeurADC = analogRead(27);      //pinZMPT sensor voltage     
    signalV = alphaHP * (signalVn + valeurADC - valeurADCn);  //filtre passe haut
    signalVn=signalV;       //valeur precedente
    valeurADCn=valeurADC;    
    rmsV = rmsVn + alphaPB * (signalV*signalV - rmsVn);  //filtre passe bas
    rmsVn=rmsV;             //valeur precedente
   tensionRMS=(sqrt(rmsV))*calibreV;
   echantillon++;
      // =================================================
    if (echantillon >= N_ECHANTILLONS)   // Attente de l'interruption 800 us
    {   // ENVOI CORE 0 -> CORE 1
      portENTER_CRITICAL(&mux);
        resultat.tensionRMS = tensionRMS;
        nouvelleMesure =1;
      portEXIT_CRITICAL(&mux);

      echantillon = 0;  // Remise à zéro
    }
  }
}



void setup()
{
  Serial.begin(115200);

  analogReadResolution(12);
  analogSetAttenuation(ADC_11db);
  pinMode(PIN_ZMPT, INPUT);
  pinMode(25, OUTPUT);

  tft.init();
  tft.setRotation(0);

  tft.fillScreen(TFT_BLACK);
  tft.setTextColor(TFT_GREEN, TFT_BLACK);
  tft.setCursor(0, 0, 2);
  tft.println("IUT GEII SOISSONS");

  xTaskCreatePinnedToCore(      // TACHE MESURE -> CORE 0
    mesureCore0,          // fonction
    "MESURE",             // nom
    4096,                 // stack
    NULL,                 // paramètre
    2,                    // priorité
    &mesureTaskHandle,    // handle
    0                     // Core 0
  );

  uint8_t timer_id = 1;     // TIMER
  uint16_t prescaler = 80;

  timer = timerBegin( timer_id,   prescaler,   true   );
  timerAttachInterrupt( timer, &timerISR, true );
  timerAlarmWrite( timer, PERIODE_US, true   );
  timerAlarmEnable(timer);
}


// =====================================================
float V;

void loop()    // CORE 1 : TFT
{
  if (nouvelleMesure==1)   {
//      if (digitalRead(25)==0)   {digitalWrite(25,1); } else {digitalWrite(25,0); }
      
    portENTER_CRITICAL(&mux);      // Lecture protégée
      V = resultat.tensionRMS;
      nouvelleMesure = 0;
    portEXIT_CRITICAL(&mux);

    tft.setTextColor(TFT_GREEN, TFT_BLACK);
    tft.setCursor(0, 50, 4);
    tft.print("V = ");
    tft.print(V, 1);

    Serial.print("Vrms = ");      // Moniteur série
    Serial.println(V, 2);

  }//end new mesure


  delay(10);
}//en loop

Avec l’utilisation des 2 cores, il est possible de diminuer le temps de la routine d’interruption.

Attention, si on diminue la routine d’interruption, on modifie les fréquences de coupures du filtre passe haut et passe bas.

Un autre avantage d’utilisation du filtre c’est que la racine de la tension avec l’offset retiré ne peut etre négative par rapport au calcul de la somme

Maintenant, il faut faire la communication

Au niveau de la communication de ESP 32 de l’energy metre et commander d’autres ESP32 pour faire du délestage via le wifi ou bluetooth, il y a plusieurs possibilités mais je n’ai pas de recul pour savoir ce qui est le mieux et je ne connais pas les limites des possibilités de chacun tout en restant gratuit.

Le net donne 3 possibilités : ESP32 → Wi-Fi → box → Vlan → INTERNET → PC/téléphone

Qu’est ce qui serait le mieux ?? il va falloir tester

Dans un premier temps, une communication locale sera effectuée et permetra detester la valeur du signal le wifi autour de la box

ESP32 → Wi-Fi → box → INTERNET locale → PC/téléphone

En locale, il faut que le PC ou le telephone ne soit pas loin de la box

Il est possible de se balader avec son ESP32 pour connaitre la valeur du signal dans une maison est de savoir si cela va passer ou pas.

· 30 à -50 dBm : Signal excellent. L'ESP32 est à côté de la box (routeur)

· -60 à -67 dBm : Signal très bon.

· -70 à -75 dBm : Signal correct. La connexion est stable, mais les obstacles commencent à l'atténuer.

· -80 à -85 dBm : Signal faible. Risque de déconnexions fréquentes et de pertes de paquets de données.

· -90 dBm et au-delà : Signal critique. La connexion est instable ou impossible à maintenir (au dela de 10m)

Sinon, il faut un wifi extender pour améliorer la communication

Pour ne pas compliquer le code, voici le programme qui envoie une valeur en temps réel comme la température en page HTML directement sur l’adresse IP que l’ESP32 a envoyé dans la liaison série.

//TTGO wifi server
#include <WiFi.h>
#include <WebServer.h>
#include <TFT_eSPI.h>

const char* ssid = "          ";
const char* password = "            ";

TFT_eSPI tft = TFT_eSPI();

WebServer server(80);  //crée le serveur Web.

float temperature = 23.5;   // à remplacer par votre capteur

void handleRoot()  //Quand la page web PC demande /, exécute la fonction handleRoot(). »
{
String page = "<!DOCTYPE html>";
page += "<html>";
page += "<head>";

page += "<meta charset='UTF-8'>";
page += "<meta name='viewport' content='width=device-width, initial-scale=1.0'>";
page += "<meta http-equiv='refresh' content='4'>";   //refraissement 4s

page += "<title>ESP32 Temperature</title>";  //affiché dans les résultats des moteurs de recherche.
page += "</head>";

page += "<body>";      //ouvre l'ecriture
page += "<h1>ESP32</h1>";
page += "<p>Temperature : " + String(temperature) + " &deg;C</p>";   //<p>  paragraphe
page += "<p>Signal Wi-Fi : " + String(WiFi.RSSI()) + " dBm</p>";

page += "</body>";
page += "</html>";
  server.send(200, "text/html", page);  //requete  ok
}

void setup()
{
  Serial.begin(115200);

  // Ecran
  tft.init();
  tft.setRotation(0);
  tft.fillScreen(TFT_BLACK);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.setTextSize(2);

  tft.setCursor(10, 20);
  tft.println("Connexion WiFi...");

  // Wi-Fi
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }

  Serial.println();
  Serial.println("WiFi connecte !");
  Serial.print("Adresse IP : ");     //indique l'adresse IP  locale de l'ESP32
  Serial.println(WiFi.localIP());    //sinon regarder dans la box


  // Serveur Web
  server.on("/", handleRoot);
  server.begin();

  Serial.println("Serveur Web demarre");
}

void loop()
{
  server.handleClient();  //traiter les demandes reçues.

temperature=temperature+0.1;  //simuulation

  if (WiFi.status() == WL_CONNECTED)
  {
    tft.fillScreen(TFT_BLACK);

    tft.setCursor(0, 20);
    tft.print("dBm: ");
    tft.print(WiFi.RSSI());  //indique la puissance du signal

    tft.setCursor(0, 130);
    tft.print("T: ");
    tft.print(temperature, 1);
    tft.print(" C");
  }

delay (2000);  
}

Voilà ce que donne le script du code précèdent en temps réel

Si l’on désire envoyer une table de valeur glissante des 10 dernières minutes et voir jusque 1 heure en fichier .csv via le wifi.
L’objectif de ce fichier est d’avoir la connaissance de dynamique des données

Voici le code

//TTGO dispaly  wifi server table buffer circulaire   .CSV

#include <WiFi.h>
#include <WebServer.h>
#include <TFT_eSPI.h>

const char* ssid = "      ";    // name box WIFI
const char* password = "            ";  //password

TFT_eSPI tft = TFT_eSPI();
WebServer server(80);

float temperature = 0;   // A remplacer par le vrai capteur

const int TAILLE_TAMPON = 300;   // 300 mesures x 2 secondes = 600 secondes =>  10 minutes

float bufferTemperature[TAILLE_TAMPON];
unsigned long bufferTemps[TAILLE_TAMPON];

int indexActuel = 0;
int nombreValeurs = 0;

unsigned long derniereMesure = 0;
unsigned long tempsEnregistrement = 0;

// Intervalle entre deux mesures
const unsigned long INTERVALLE = 2000;   // 2 secondes

void handleRoot()   // PAGE WEB PRINCIPALE
{
  String page = "";
  page += "<!DOCTYPE html>";
  page += "<html>";
  page += "<head>";

  page += "<meta charset='UTF-8'>";
  page += "<meta name='viewport' content='width=device-width, initial-scale=1.0'>";

  page += "<meta http-equiv='refresh' content='2'>";    // Actualisation de la page toutes les 2 secondes
  page += "<title>ESP32 Temperature</title>";
  page += "<style>";

  page += "body {";
  page += "font-family: Arial;";
  page += "margin: 20px;";
  page += "background-color: #eeeeee;";
  page += "}";

  page += "h1 { color: #333333; }";

  page += ".temperature {";
  page += "font-size: 30px;";
  page += "color: red;";
  page += "font-weight: bold;";
  page += "}";

  page += ".csv {";
  page += "background-color: white;";
  page += "border: 1px solid #999;";
  page += "padding: 10px;";
  page += "font-family: monospace;";
  page += "white-space: pre;";
  page += "overflow-x: auto;";
  page += "height: 400px;";
  page += "overflow-y: scroll;";
  page += "}";

  page += "button {";
  page += "font-size: 18px;";
  page += "padding: 10px 20px;";
  page += "}";

  page += "</style>";
  page += "</head>";
  page += "<body>";
  // ----------------------------------------------------------
  page += "<h1>ESP32</h1>";
  page += "<p>Temperature en temps reel :";
  page += "<div class='temperature'>";
  page += String(temperature, 1);
  page += " &deg;C";
  page += "</div>";

//  page += "<h2>Buffer</h2>";    // INFORMATIONS BUFFER
//  page += "<p>"+= "buffer : " += String(nombreValeurs);+= " / "+= String(TAILLE_TAMPON) "</p>";   // INFORMATIONS BUFFER
/*
  page += "<h2>Donnees CSV</h2>";     // AFFICHAGE CSV

  page += "<div class='csv'>";

  // En-tête CSV
  page += "Temps(s);Temperature(°C)\n";

  if (nombreValeurs > 0)    {int debut;
    if (nombreValeurs < TAILLE_TAMPON)      {  debut = 0;    }
    else     { debut = indexActuel;    }

    // Affichage dans l'ordre chronologique
    for (int i = 0; i < nombreValeurs; i++)
    {
      int position = (debut + i) % TAILLE_TAMPON;

      page += String(bufferTemps[position]);
      page += ";";
      page += String(bufferTemperature[position], 1);

      page += "\n";
    }
  }
  page += "</div>";
*/
  // ----------------------------------------------------------
  page += "<br>";      // BOUTON TELECHARGEMENT

  page += "<a href='/download'>";
  page += "<button>Télécharger le fichier CSV</button>";
  page += "</a>";

  page += "</body>";
  page += "</html>";

  server.send(200, "text/html", page);
}

// ============================================================
void handleDownload()     // CREATION ET TELECHARGEMENT DU CSV
{
  String csv = "";
  csv += "Temps(s);Temperature(°C)\r\n";

  // ORDRE CHRONOLOGIQUE

  if (nombreValeurs > 0)    { int debut;

    if (nombreValeurs < TAILLE_TAMPON)   { debut = 0; }   else    { debut = indexActuel;   }

    for (int i = 0; i < nombreValeurs; i++)
    {
      int position = (debut + i) % TAILLE_TAMPON;

      csv += String(bufferTemps[position]);
      csv += ";";
      csv += String(bufferTemperature[position], 1);
      csv += "\r\n";
    }
  }

  // ----------------------------------------------------------
  server.sendHeader(         // ENVOI DU FICHIER
    "Content-Disposition",
    "attachment; filename=temperature.csv");

  server.send( 200, "text/csv", csv );
}


// ============================================================
void setup()   {
  Serial.begin(115200);

  tft.init();
  tft.setRotation(0);
  tft.fillScreen(TFT_BLACK);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.setTextSize(2);
  tft.setCursor(10, 20);

  tft.println("Connexion WiFi...");

  WiFi.mode(WIFI_STA);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED)    { delay(500);    Serial.print(".");  }
  Serial.println();
  Serial.println("WiFi connecte !");
  Serial.print("Adresse IP : ");

  Serial.println(WiFi.localIP());

  server.on("/", handleRoot);     // ENVOI DU FICHIER
  server.on("/download", handleDownload);
  server.begin();
  Serial.println("Serveur Web demarre");

  derniereMesure = millis();      // INITIALISATION TEMPS
  tempsEnregistrement = 0;
}  //fin set up

// ============================================================
void loop()   {

  server.handleClient();     // SERVEUR WEB

  unsigned long maintenant = millis();      // NOUVELLE MESURE TOUTES LES 2 SECONDES
  if (maintenant - derniereMesure >= INTERVALLE)     { derniereMesure = maintenant;

    temperature = temperature + 0.1;       // SIMULATION de data TEMPERATURE
    bufferTemps[indexActuel] = tempsEnregistrement;       // ENREGISTREMENT DU TEMPS
    bufferTemperature[indexActuel] = temperature;
    tempsEnregistrement += 2;        // INCREMENT DU TEMPS

    indexActuel++;  
    if (indexActuel >= TAILLE_TAMPON)    {indexActuel = 0;    }
    if (nombreValeurs < TAILLE_TAMPON)   {nombreValeurs++;    }

      tft.fillScreen(TFT_BLACK);
      tft.setCursor(0, 20);
      tft.setTextSize(2);
      tft.print("T:");
      tft.print(temperature, 1);
      tft.print(" C");
    
  }  //fin intervalle
}  //End loop

Le fichier s’enregistre directement dans le dossier download

Si l’on desire passer via la 4G, il faudra utiliser MQTT :
│ESP32 IP : 192.168.1.34│ → publish → │Wi-Fi TCP / réseau IP box│ → Broker MQTT mosquito → subscribe (ecoute) → │4G téléphone Home Assistant│

Mais d’abord, on va rajouter dans le code du wifi local, la mesure de la tension et du courant pour avoir la puissance active, et le facteur de puissance

J’ai demandé à une IA de fusionner le programme du wifi local et le programme 2 core avec toutes les mesures (tension et courant, puissance et facteur de puissance)
L’IA donne meme l’algo


Par contre, l’IA rajoute des trucs qu’on ne lui demande pas.

Par exemple des lignes de debug, des decalages dans l’ecran, mais des explications.
Je n’aime pas sa façon d’ecrire.
Mais elle a fait le taf

Voici le programme fusionné

//TTGO dispaly  wifi server table buffer circulaire   .CSV

// ================================================================
// ESP32 TTGO - DOUBLE CORE
//
// CORE 0 :
//   - Acquisition tension/courant
//   - Echantillonnage toutes les 800 us
//   - Filtre passe-haut
//   - Calcul RMS filtré
//   - Calcul puissance active filtrée
//
// CORE 1 :
//   - Ecran TFT
//   - Serveur Web WiFi
//   - Enregistrement d'une mesure toutes les 2 secondes
//   - Buffer circulaire de 10 minutes
//   - Téléchargement CSV
//
// 300 mesures x 2 secondes = 600 secondes = 10 minutes
//
// CSV :
// Temps(s);Tension_RMS(V);Courant_RMS(A);Puissance_Active(W);FP
// ================================================================

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <TFT_eSPI.h>


const char* ssid     = "      ";       // NOM WIFI
const char* password = "     "; // MOT DE PASSE

WebServer server(80);

TFT_eSPI tft = TFT_eSPI();


#define PIN_ZMPT       32       // Capteur tension
#define PIN_intensity  33       // SCT-013-050
#define PIN_LED        25       // Test acquisition


#define N_ECHANTILLONS 2500   // PARAMETRES ACQUISITION  
#define PERIODE_US     800    // 2500 x 800 us = 2 secondes

// 2500 x 800 us = 2 secondes
//
// Une mesure complète est donc calculée toutes les 2 secondes.

#define calibreV  0.5f    // CALIBRATION
#define calibreI  0.019f


const int TAILLE_TAMPON = 3600;   //1heure/1800

// 300 mesures x 2 secondes = 600 secondes = 10 minutes
float bufferTension[TAILLE_TAMPON];
float bufferCourant[TAILLE_TAMPON];
float bufferPuissance[TAILLE_TAMPON];
float bufferFP[TAILLE_TAMPON];

unsigned long bufferTemps[TAILLE_TAMPON];

int indexActuel = 0;
int nombreValeurs = 0;

unsigned long tempsEnregistrement = 0;

hw_timer_t *timer = NULL;
TaskHandle_t mesureTaskHandle = NULL;


struct Mesure   // STRUCTURE DES DONNEES PARTAGEES CORE 0 -> CORE 1
{
  float tensionRMS;
  float currentRMS;
  float Power;
  float FP;
};

volatile Mesure resultat;
volatile bool nouvelleMesure = false;

// Protection des données partagées
portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;

// ================================================================
// INTERRUPTION TIMER
// ================================================================

void IRAM_ATTR timerISR()
{
  BaseType_t xHigherPriorityTaskWoken = pdFALSE;

  // Réveille la tâche de mesure sur Core 0
  vTaskNotifyGiveFromISR(
    mesureTaskHandle,
    &xHigherPriorityTaskWoken
  );

  if (xHigherPriorityTaskWoken)
  {
    portYIELD_FROM_ISR();
  }
}

// ================================================================
// CORE 0 : TACHE DE MESURE
// ================================================================

void mesureCore0(void *parameter)
{

  const float alphaPB = 0.0016f;     // FILTRES
  const float alphaHP = 0.9901f;

  // --------------------------------------------------------------
  // TENSION
  // --------------------------------------------------------------

  float valeurADC  = 0.0f;
  float valeurADCn = 0.0f;

  float signalV  = 0.0f;
  float signalVn = 0.0f;

  float rmsV  = 0.0f;
  float rmsVn = 0.0f;

  float tensionRMS = 0.0f;

  // --------------------------------------------------------------
  // COURANT
  // --------------------------------------------------------------

  float valeurI  = 0.0f;
  float valeurIn = 0.0f;

  float signalI  = 0.0f;
  float signalIn = 0.0f;

  float rmsI  = 0.0f;
  float rmsIn = 0.0f;

  float currentRMS = 0.0f;

  // --------------------------------------------------------------
  // PUISSANCE
  // --------------------------------------------------------------

  float Powerinstant = 0.0f;

  float Powerf = 0.0f;
  float Powern = 0.0f;

  float Power = 0.0f;

  float S  = 0.0f;
  float FP = 0.0f;

  uint16_t echantillon = 0;      // COMPTEUR ECHANTILLONS

  // ==============================================================
  // BOUCLE DE MESURE
  // ==============================================================

  while (true)     {      // Attente de l'interruption timer

    ulTaskNotifyTake( pdTRUE,  portMAX_DELAY  );


 //   digitalWrite(PIN_LED, !digitalRead(PIN_LED));   // LED de test

    // ============================================================
    // MESURE TENSION
    // ============================================================
    valeurADC = analogRead(PIN_ZMPT);

    // Filtre passe-haut
    signalV =
      alphaHP *
      (signalVn + valeurADC - valeurADCn);

    signalVn   = signalV;
    valeurADCn = valeurADC;

    // RMS filtré
    rmsV =
      rmsVn +
      alphaPB *
      (signalV * signalV - rmsVn);

    rmsVn = rmsV;

    tensionRMS = sqrt(rmsV) * calibreV;

    // MESURE COURANT
    // ============================================================

    valeurI = analogRead(PIN_intensity);

    // Filtre passe-haut
    signalI =
      alphaHP *
      (signalIn + valeurI - valeurIn);

    signalIn = signalI;
    valeurIn = valeurI;

    // RMS filtré
    rmsI =
      rmsIn +
      alphaPB *
      (signalI * signalI - rmsIn);

    rmsIn = rmsI;

    currentRMS = sqrt(rmsI) * calibreI;

    Powerinstant = signalV * signalI;          // PUISSANCE ACTIVE

    // Filtrage passe-bas de la puissance instantanée
    Powerf =
      Powern +
      alphaPB *
      (Powerinstant - Powern);

    Powern = Powerf;
    Power = Powerf * calibreV * calibreI;

    echantillon++;       // COMPTEUR

    // ============================================================
    // FIN DES 2500 ECHANTILLONS    2500 x 800 us = 2 secondes
    // ============================================================

    if (echantillon >= N_ECHANTILLONS)     {

      S = tensionRMS * currentRMS;

      if (S > 0.05f)   { FP = Power / S;         // Facteur de puissance
        if (FP > 1.0f)   FP = 1.0f;
        if (FP < -1.0f)  FP = -1.0f;
                      }   else   { FP = 0.0f;   }



      portENTER_CRITICAL(&mux);    // ENVOI CORE 0 -> CORE 1

      resultat.tensionRMS  = tensionRMS;
      resultat.currentRMS  = currentRMS;
      resultat.Power       = Power;
      resultat.FP          = FP;

      nouvelleMesure = true;

      portEXIT_CRITICAL(&mux);

      echantillon = 0;   // Remise à zéro du compteur
    }
  }
}

// ================================================================

void handleRoot() {  // PAGE WEB PRINCIPALE

 float V;
  float I;
  float P;
  float FP;
  float tempsbuffer;

  portENTER_CRITICAL(&mux);

  V  = resultat.tensionRMS;
  I  = resultat.currentRMS;
  P  = resultat.Power;
  FP = resultat.FP;

  portEXIT_CRITICAL(&mux);


  // PAGE HTML
  // --------------------------------------------------------------
unsigned long debut = millis();  
  String page = "";

  page += "<!DOCTYPE html>";
  page += "<html>";
  page += "<head>";

  page += "<meta charset='UTF-8'>";
  page += "<meta name='viewport' content='width=device-width, initial-scale=1.0'>";

  // Actualisation toutes les 2 secondes
  page += "<meta http-equiv='refresh' content='2'>";

  page += "<title>ESP32 - Mesure électrique</title>";

  page += "<style>";

  page += "body {";
  page += "font-family: Arial;";
  page += "margin: 20px;";
  page += "background-color: #eeeeee;";
  page += "}";

  page += "h1 {";
  page += "color: #333333;";
  page += "}";

  page += ".mesure {";
  page += "background-color: white;";
  page += "border-radius: 10px;";
  page += "padding: 15px;";
  page += "margin-bottom: 10px;";
  page += "font-size: 25px;";
  page += "font-weight: bold;";
  page += "}";

  page += ".tension { color: green; }";
  page += ".courant { color: red; }";
  page += ".puissance { color: blue; }";
  page += ".fp { color: purple; }";

  page += "button {";
  page += "font-size: 18px;";
  page += "padding: 10px 20px;";
  page += "}";

  page += "</style>";

  page += "</head>";

  page += "<body>";

  // --------------------------------------------------------------
  // TITRE
  // --------------------------------------------------------------

  page += "<h1>ESP32 - Mesure électrique</h1>";

  // --------------------------------------------------------------
  // TENSION
  // --------------------------------------------------------------

  page += "<div class='mesure tension'>";
  page += "Tension RMS : ";
  page += String(V, 1);
  page += " V";
  page += "</div>";

  // --------------------------------------------------------------
  // COURANT
  // --------------------------------------------------------------

  page += "<div class='mesure courant'>";
  page += "Courant RMS : ";
  page += String(I, 3);
  page += " A";
  page += "</div>";

  // --------------------------------------------------------------
  // PUISSANCE
  // --------------------------------------------------------------

  page += "<div class='mesure puissance'>";
  page += "Puissance active : ";
  page += String(P, 1);
  page += " W";
  page += "</div>";

  // --------------------------------------------------------------
  // FACTEUR DE PUISSANCE
  // --------------------------------------------------------------

  page += "<div class='mesure fp'>";
  page += "Facteur de puissance : ";
  page += String(FP, 2);
  page += "</div>";

  // --------------------------------------------------------------
  // INFORMATIONS BUFFER
  // --------------------------------------------------------------
  page += "<p>";
  page += "Mesures enregistrees : ";
  page += String(nombreValeurs);
  page += " / ";
  page += String(TAILLE_TAMPON);
  page += "</p>";

  page += "<p>";
  page += "Duree maximale du fichier : 2 heures";   //   TAILLE_TAMPON*2s/3600
  page += "</p>";

  // --------------------------------------------------------------
  // BOUTON CSV
  // --------------------------------------------------------------

  page += "<br>";

  page += "<a href='/download'>";
  page += "<button>";
  page += "Telecharger le fichier CSV";
  page += "</button>";
  page += "</a>";

  page += "</body>";
  page += "</html>";

  server.send( 200, "text/html",  page  );

unsigned long duree = millis() - debut;

  Serial.print("Duree requete (ms) : ");
  Serial.println(duree);                    //5ms

}

// ================================================================
// TELECHARGEMENT CSV
// ================================================================

void handleDownload()  {
  String csv = "";

  csv += "Temps(s);Tension_RMS(V);Courant_RMS(A);Puissance_Active(W);FP\r\n";     // EN-TETE CSV


  if (nombreValeurs > 0)   {      // ORDRE CHRONOLOGIQUE
    int debut;

    if (nombreValeurs < TAILLE_TAMPON)   { debut = 0;  }
    else   { debut = indexActuel;   }

    // Parcours des données
    // ------------------------------------------------------------
    for (int i = 0; i < nombreValeurs; i++)   {
      int position =
        (debut + i) % TAILLE_TAMPON;

      csv += String(bufferTemps[position]); csv += ";";

      csv += String(bufferTension[position], 2);  csv += ";";

      csv += String(bufferCourant[position], 3);  csv += ";";

      csv += String(bufferPuissance[position], 2);  csv += ";";

      csv += String(bufferFP[position], 3);

      csv += "\r\n";
    }
  }   

  // NOM DU FICHIER
  // --------------------------------------------------------------
  server.sendHeader(  "Content-Disposition",  "attachment; filename=mesures_2H.csv"  );

  server.send(  200,  "text/csv", csv  );
}  //end handleDownload()


// ================================================================
void setup()
{
  Serial.begin(115200);

  analogReadResolution(12);
  analogSetAttenuation(ADC_11db);

  pinMode(PIN_ZMPT, INPUT);
  pinMode(PIN_intensity, INPUT);

  pinMode(PIN_LED, OUTPUT);
  digitalWrite(PIN_LED, LOW);

  tft.init();
  tft.setRotation(0);

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid,  password  );
  while (WiFi.status() != WL_CONNECTED)   { delay(500);  Serial.print(".");  }

  server.on( "/", handleRoot  );     // SERVEUR WEB
  server.on("/download",  handleDownload );
  server.begin();

  Serial.println("Serveur Web demarre");

  resultat.tensionRMS = 0;
  resultat.currentRMS = 0;
  resultat.Power      = 0;
  resultat.FP         = 0;

  nouvelleMesure = false;

  indexActuel = 0;
  nombreValeurs = 0;

  tempsEnregistrement = 0;

  // CREATION TACHE MESURE SUR CORE 0
  // ==============================================================

  xTaskCreatePinnedToCore(
    mesureCore0,          // fonction
    "MESURE",             // nom
    4096,                 // stack
    NULL,                 // parametre
    2,                    // priorité
    &mesureTaskHandle,    // handle
    0                     // CORE 0
  );

  uint8_t timer_id = 1;      // // 1 tick = 1 us
  uint16_t prescaler = 80;   //80 MHz / 80 = 1 MHz

  timer = timerBegin( timer_id, prescaler,  true  );
  timerAttachInterrupt( timer, &timerISR, true );
  timerAlarmWrite(  timer, PERIODE_US, true  );
  timerAlarmEnable(timer);


  tft.fillScreen(TFT_BLACK);
  tft.setCursor(0, 0, 2);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.println("MESURE ELECTRIQUE");

  Serial.println("Systeme demarre");
}


// ================================================================
float V  = 0;   // VARIABLES CORE 1
float I  = 0;
float P  = 0;
float FP = 0;
float E, joule;


void loop()   {    // LOOP CORE 1


  server.handleClient();     // SERVEUR WEB   1ms   

  if (nouvelleMesure == true)    {
 //   digitalWrite(25,1);      //durée 5ms
    portENTER_CRITICAL(&mux);      // Lecture protégée des données
    V  = resultat.tensionRMS;
    I  = resultat.currentRMS;
    P  = resultat.Power;
    FP = resultat.FP;
    nouvelleMesure = false;
    portEXIT_CRITICAL(&mux);

      // ENREGISTREMENT DANS LE BUFFER CIRCULAIRE
    // ============================================================

    bufferTemps[indexActuel] =tempsEnregistrement;
    bufferTension[indexActuel] = V;
    bufferCourant[indexActuel] = I;
    bufferPuissance[indexActuel] = P;
    bufferFP[indexActuel] =   FP;

    tempsEnregistrement += 2;
    indexActuel++;

    if (indexActuel >= TAILLE_TAMPON)   { indexActuel = 0;    }       // Avancement buffer circulaire
    if (nombreValeurs < TAILLE_TAMPON)    {nombreValeurs++;   }      // Nombre de valeurs enregistrées

    tft.setCursor(0, 25, 4);

    tft.setTextColor( TFT_GREEN,  TFT_BLACK );
    tft.print("V: ");        // TENSION
    tft.print(V, 1);
    tft.print(" V");

    tft.setCursor(0, 55, 4);
    tft.setTextColor( TFT_RED,  TFT_BLACK );
    tft.print("I: ");
    tft.print(I, 2);       // COURANT
    tft.print(" A ");

    tft.setCursor(0, 85, 4);
    tft.setTextColor( TFT_BLUE, TFT_BLACK );
    tft.print("P:");
    tft.print(P, 0);        // PUISSANCE ACTIVE
    tft.print("   ");

    tft.setCursor(0, 115, 4);
    tft.setTextColor( TFT_WHITE,  TFT_BLACK   );
    tft.print("FP:");       // FACTEUR DE PUISSANCE
    tft.print(FP, 2);
    tft.print("  ");

    joule=joule+P;   //
E=(joule*2)/3600;   //wh

    tft.setCursor(0, 145, 4);
    tft.setTextColor( TFT_WHITE,  TFT_BLACK   );
    tft.print("E:");       // FACTEUR DE PUISSANCE
    tft.print(E,0);
   tft.print("  ");

//  digitalWrite(25,0); 
  }

  delay(5);     // Petite pause pour laisser le serveur WiFi respirer
}

Avec les cinq tableaux de 300 valeurs de float 4 octet (10minutes), le compilateur indique que 15% de la mémoire SRAM de 520k de l’ESP32 est utilisé et que Le programme prend 62%.

Avec 1800 valeurs de buffer (1 heure) , (1800*5*4oxter=36koctet), le compilateur indique que la SRAM utilise 24%

Avec 3600 valeurs de buffer (2 heures) , la compilation indique que la SRAM utilise 35%

Avec 5400 valeurs de buffer, le compilateur ne veut plus transmettre le programme à cause du string+csv qui doit prendre pas mal de RAM


Pour faire une étude énergétique poussée et de sauvegarder les données du fichier .CSV automatiquement toutes les 2 heures.
Il est possible de changer le script HTML de l’ESP32 (quand le buffer est plein) vers Google Drive/cloud, par MQTT, par e-mail sans avoir à appuyer sur « Télécharger »

J’ai fait des tests pour faire une comparaison avec du matériel (relais, compteur, disjoncteur numérique) existant que l’on m’a prêté….

Le KWS-303WF ne donne pas la puissance injectée car elle n’est pas bidirectionnelle sur cet appareil !
Il y a une application sur smartphone du KWS-303WF qui trace la puissance et le facteur de puissance mais toutes les heures donc ne permet pas d’étudier un réseau électrique (consommation) ni faire du délestage.
Ce n’est pas pertinent d’avoir un graphique avec une mesure de puissance toutes les heures !
Le taux de rafraîchissement des données de l’application par le wifi est de 1 minute minimum.
Mais, avec l’application, on peut couper ce qu’il alimente manuellement.
J’ai un doute sur la mesure du facteur de puissance qui est trop volatile.

Je n’ai pas essayé la version KWS-303 RS485


Dans le même genre, j’ai testé le TOMPD-63LW qui lui est bidirectionnel et indique la puissance négative sur l’écran, mais pas sur l’application ????
il mesure le courant différentiel en permanence et on peut savoir quel système a un défaut d’isolement.
Le rafraichissement des données par le wifi de l’application est toutes les 2s.

Impossible de récupérer les données via l’adresse IP en wifi local via la plateforme Tuya car le Vlan est bloqué et il faut obligatoirement passer par le cloud tuya payant.

Sur la plateform tuya avec TOMPD-63LW, la data de la puissance n’est pas négative non plus ????

Sur le net, on peut observer que les anciennes versions TOMPD-63LW était plus libre mais s’il faut changer le firmware, c’est pénible. Et certains ont essayé sans succès ou pour avoir une écriture en mandarin ou un ecran tout noir, ce n’est pas gagné.

Etant donné que la puissance négative n’est pas prise en compte, l’energie consommée totale ne prend pas en compte la réinjection dans l’application.
Lorsqu’il y a réinjection, la tension réseau augmente légèrement de 1V par rapport à la chute de la tension dans les câbles.

Des systèmes domotiques existent tel que Shelly Pro 3EM 120A – Smart Meter (130€) via des contacteurs modulaires (50€), il va commander des appareils en tout ou rien. Sachant que la priorité du délestage se ferait avec l’application android (Shelly Smart Control) mais cela n’est pas bien expliqué ????

Il existe plusieurs architectures de l’utilisation des panneaux photovoltaïque avec batterie de stockage, mais là plus utilisé est la suivante à cause que si la batterie de stockage est pleine le surplus ira dans le réseau

Panneaux PV → onduleur hybride ↔ batterie stockage et maison ↔ réseau

Mais cela peut se compliquer à donner des priorités en fonction de metéo futur…

Panneaux PV → mesure du surplus (energy meter) → contacteur (pompe arrosage) → contacteurs (energy meter) → (chargeur vélo elec, batterie de stockage, contacteur ballon d’eau chaude,….)

Bref, pas facile de gérer l’énergie et faire du délestage autoconsommation d’énergie photovoltaïque ou éolien, les systèmes deviennent très vite propriétaire, les manuels de ces appareils n’explique pas tout donc ce n’est pas la « smartlife ».

Voici le programme qui envoie les données toutes les 2 heures de façon automatique, ce qui permet de faire autres choses sur son PC sans louper d’enregistrer.

On croit toujours que c’est simple à faire mais c’est toujours plus compliqué à que l’on ne le pense !

//TTGO dispaly  wifi server table buffer circulaire  .CSV
//envoie auto quand le buffer est plein

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <TFT_eSPI.h>

const char* ssid     = "      ";       // NOM WIFI
const char* password = "      "; // MOT DE PASSE

WebServer server(80);

TFT_eSPI tft = TFT_eSPI();

#define PIN_ZMPT       32       // Capteur tension
#define PIN_intensity  33       // SCT-013-050
#define PIN_LED        25       // Test acquisition

#define N_ECHANTILLONS 2500   // PARAMETRES ACQUISITION  
#define PERIODE_US     800    // 2500 x 800 us = 2 secondes

// 2500 x 800 us = 2 secondes
// Une mesure complète est donc calculée toutes les 2 secondes.

#define calibreV  0.48f    // CALIBRATION
#define calibreI  0.019f

const int TAILLE_TAMPON = 300;   //300/10minutes   1800/1heure

// 300 mesures x 2 secondes = 600 secondes = 10 minutes
float bufferTension[TAILLE_TAMPON];
float bufferCourant[TAILLE_TAMPON];
float bufferPuissance[TAILLE_TAMPON];
float bufferFP[TAILLE_TAMPON];

unsigned long bufferTemps[TAILLE_TAMPON];

int indexActuel = 0;
int nombreValeurs = 0;
unsigned long generationBuffer = 0;  //nouveau fichier
unsigned long tempsEnregistrement = 0;

hw_timer_t *timer = NULL;
TaskHandle_t mesureTaskHandle = NULL;


struct Mesure {  // STRUCTURE DES DONNEES PARTAGEES CORE 0 -> CORE 1
  float tensionRMS;
  float currentRMS;
  float Power;
  float FP;
};

volatile Mesure resultat;
volatile bool nouvelleMesure = false;

// Protection des données partagées
portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;

// ================================================================
// INTERRUPTION TIMER
// ================================================================

void IRAM_ATTR timerISR()   {
  BaseType_t xHigherPriorityTaskWoken = pdFALSE;

  // Réveille la tâche de mesure sur Core 0
  vTaskNotifyGiveFromISR (mesureTaskHandle,   &xHigherPriorityTaskWoken  );

  if (xHigherPriorityTaskWoken)    { portYIELD_FROM_ISR(); }
}

// ================================================================
// CORE 0 : TACHE DE MESURE
// ================================================================

void mesureCore0(void *parameter)   {
  const float alphaPB = 0.0016f;     // FILTRES
  const float alphaHP = 0.9901f;

  // --------------------------------------------------------------
  // TENSION
  // --------------------------------------------------------------

  float valeurADC  = 0.0f;
  float valeurADCn = 0.0f;

  float signalV  = 0.0f;
  float signalVn = 0.0f;

  float rmsV  = 0.0f;
  float rmsVn = 0.0f;

  float tensionRMS = 0.0f;


  // COURANT
  // --------------------------------------------------------------

  float valeurI  = 0.0f;
  float valeurIn = 0.0f;

  float signalI  = 0.0f;
  float signalIn = 0.0f;

  float rmsI  = 0.0f;
  float rmsIn = 0.0f;

  float currentRMS = 0.0f;


  // PUISSANCE
  // --------------------------------------------------------------

  float Powerinstant = 0.0f;

  float Powerf = 0.0f;
  float Powern = 0.0f;

  float Power = 0.0f;

  float S  = 0.0f;
  float FP = 0.0f;

  uint16_t echantillon = 0;      // COMPTEUR ECHANTILLONS

  // ==============================================================
  // BOUCLE DE MESURE
  // ==============================================================

  while (true)     {      // Attente de l'interruption timer

    ulTaskNotifyTake( pdTRUE,  portMAX_DELAY  );

 //   digitalWrite(PIN_LED, !digitalRead(PIN_LED));   // LED de test

    // ============================================================
    // MESURE TENSION
    // ============================================================
    valeurADC = analogRead(PIN_ZMPT);

    // Filtre passe-haut
    signalV = alphaHP *(signalVn + valeurADC - valeurADCn);
    signalVn   = signalV;
    valeurADCn = valeurADC;

    // RMS filtré
    rmsV = rmsVn +  alphaPB *(signalV * signalV - rmsVn);
    rmsVn = rmsV;
    tensionRMS = sqrt(rmsV) * calibreV;

    // MESURE COURANT
    // ============================================================

    valeurI = analogRead(PIN_intensity);

    // Filtre passe-haut
    signalI =alphaHP * (signalIn + valeurI - valeurIn);
    signalIn = signalI;
    valeurIn = valeurI;

    // RMS filtré
    rmsI =rmsIn +alphaPB *(signalI * signalI - rmsIn);
    rmsIn = rmsI;
    currentRMS = sqrt(rmsI) * calibreI;

    Powerinstant = signalV * signalI;          // PUISSANCE ACTIVE

    // Filtrage passe-bas de la puissance instantanée
    Powerf =Powern + alphaPB * (Powerinstant - Powern);
    Powern = Powerf;
    Power = Powerf * calibreV * calibreI;

    echantillon++;       // COMPTEUR

      // FIN DES 2500 ECHANTILLONS    2500 x 800 us = 2 secondes
    // ============================================================

    if (echantillon >= N_ECHANTILLONS)     {
      S = tensionRMS * currentRMS;

      if (S > 0.05f)   { FP = Power / S;         // Facteur de puissance
        if (FP > 1.0f)   FP = 1.0f;
        if (FP < -1.0f)  FP = -1.0f;
                      }   else   { FP = 0.0f;   }



      portENTER_CRITICAL(&mux);    // ENVOI CORE 0 -> CORE 1

      resultat.tensionRMS  = tensionRMS;
      resultat.currentRMS  = currentRMS;
      resultat.Power       = Power;
      resultat.FP          = FP;

      nouvelleMesure = true;

      portEXIT_CRITICAL(&mux);

      echantillon = 0;   // Remise à zéro du compteur
    }
  }
}

// ================================================================

void handleRoot() {  // PAGE WEB PRINCIPALE

 float V;
  float I;
  float P;
  float FP;
  float tempsbuffer;

  portENTER_CRITICAL(&mux);

  V  = resultat.tensionRMS;
  I  = resultat.currentRMS;
  P  = resultat.Power;
  FP = resultat.FP;

  portEXIT_CRITICAL(&mux);

  // PAGE HTML
  // --------------------------------------------------------------
  String page = "";

  page += "<!DOCTYPE html>";
  page += "<html>";
  page += "<head>";

  page += "<meta charset='UTF-8'>";
  page += "<meta name='viewport' content='width=device-width, initial-scale=1.0'>";

  // Actualisation toutes les 2 secondes
//  page += "<meta http-equiv='refresh' content='2'>";

  page += "<title>ESP32 - Mesure électrique</title>";

  page += "<style>";

  page += "body {";
  page += "font-family: Arial;";
  page += "margin: 20px;";
  page += "background-color: #eeeeee;";
  page += "}";

  page += "h1 {";
  page += "color: #333333;";
  page += "}";

  page += ".mesure {";
  page += "background-color: white;";
  page += "border-radius: 10px;";
  page += "padding: 15px;";
  page += "margin-bottom: 10px;";
  page += "font-size: 25px;";
  page += "font-weight: bold;";
  page += "}";

  page += ".tension { color: green; }";
  page += ".courant { color: red; }";
  page += ".puissance { color: blue; }";
  page += ".fp { color: purple; }";

  page += "button {";
  page += "font-size: 18px;";
  page += "padding: 10px 20px;";
  page += "}";

  page += "</style>";

  page += "</head>";
  page += "<body>";

  // TITRE
  // --------------------------------------------------------------
  page += "<h1>ESP32 - Mesure électrique</h1>";

  // TENSION
  // --------------------------------------------------------------

  page += "<div class='mesure tension'>";
page += "Tension RMS : ";
page += "<span id='tension'>";
page += String(V, 1);
page += "</span> V";
  page += "</div>";


  // COURANT
  // --------------------------------------------------------------

  page += "<div class='mesure courant'>";
page += "Courant RMS : ";
page += "<span id='courant'>";
page += String(I, 2);
page += "</span> A";
  page += "</div>";

  // PUISSANCE
  // --------------------------------------------------------------
  page += "<div class='mesure puissance'>";
page += "Puissance active : ";
page += "<span id='puissance'>";
page += String(P, 1);
page += "</span> W";
  page += "</div>";

  // FACTEUR DE PUISSANCE
  // --------------------------------------------------------------

  page += "<div class='mesure fp'>";
page += "Facteur de puissance : ";
page += "<span id='fp'>";
page += String(FP, 2);
page += "</span>";
  page += "</div>";

  // --------------------------------------------------------------
  // INFORMATIONS BUFFER
  // --------------------------------------------------------------
page += "<p>";
page += "Mesures enregistrees : ";

page += "<span id='progression'>";
page += String(nombreValeurs);
page += " / ";
page += String(TAILLE_TAMPON);
page += "</span>";

page += "</p>";

  // BOUTON CSV
  // --------------------------------------------------------------

  page += "<br>";
  page += "<a href='/download'>";
  page += "<button>";
  page += "Telecharger le fichier CSV";
  page += "</button>";
  page += "</a>";

  // --envoie des data automatique---------------------------------
page += "<script>";
page += "let derniereGeneration = -1;";
page += "async function actualiserMesures() {";
page += "try {";

page += "const reponse = await fetch('/data');";
page += "const data = await reponse.json();";

page += "document.getElementById('tension').innerText = data.V.toFixed(1);";
page += "document.getElementById('courant').innerText = data.I.toFixed(3);";
page += "document.getElementById('puissance').innerText = data.P.toFixed(1);";
page += "document.getElementById('fp').innerText = data.FP.toFixed(2);";

page += "} catch (erreur) {";
page += "console.log('Erreur mesures :', erreur);";
page += "}";
page += "}";
page += "async function surveillerBuffer() {";

page += "try {";

page += "const reponse = await fetch('/status');";
page += "const data = await reponse.json();";

page += "document.getElementById('progression').innerText = ";
page += "data.nombre + ' / ' + data.taille;";

page += "if (data.nombre >= data.taille && ";
page += "data.generation != derniereGeneration) {";

page += "derniereGeneration = data.generation;";

page += "console.log('BUFFER PLEIN - téléchargement automatique - génération : ' + data.generation);";

page += "const lien = document.createElement('a');";

page += "lien.href = '/download';";
page += "lien.download = 'mesures_2H.csv';";

page += "document.body.appendChild(lien);";
page += "lien.click();";
page += "document.body.removeChild(lien);";
page += "}";
page += "} catch (erreur) {";
page += "console.log('Erreur buffer :', erreur);";
page += "}";
page += "}";

page += "setInterval(actualiserMesures, 2000);";
page += "setInterval(surveillerBuffer, 2000);";

page += "actualiserMesures();";
page += "surveillerBuffer();";

page += "</script>";

page += "</body>";
page += "</html>";

  page += "</body>";
  page += "</html>";

    server.send( 200, "text/html",  page  );
}

void handleStatus() {

  String json = "";
  json += "{";

  json += "\"nombre\":";
  json += String(nombreValeurs);  

  json += ",";

  json += "\"taille\":";
  json += String(TAILLE_TAMPON);

  json += ",";

  json += "\"generation\":";
  json += String(generationBuffer);

  json += "}";
  server.send( 200, "application/json", json);
}

void handleData() {

  float V;
  float I;
  float P;
  float FP;

  portENTER_CRITICAL(&mux);

  V  = resultat.tensionRMS;
  I  = resultat.currentRMS;
  P  = resultat.Power;
  FP = resultat.FP;

  portEXIT_CRITICAL(&mux);

  String json = "{";

  json += "\"V\":";
  json += String(V, 1);

  json += ",\"I\":";
  json += String(I, 3);

  json += ",\"P\":";
  json += String(P, 1);

  json += ",\"FP\":";
  json += String(FP, 2);

  json += "}";

  server.send(200, "application/json", json);
}



// ================================================================
// TELECHARGEMENT CSV
// ================================================================

void handleDownload()  {
  String csv = "";

  csv += "Temps(s);Tension_RMS(V);Courant_RMS(A);Puissance_Active(W);FP\r\n";     // EN-TETE CSV

  if (nombreValeurs > 0)   {      // ORDRE CHRONOLOGIQUE
    int debut;

    if (nombreValeurs < TAILLE_TAMPON)   { debut = 0;  }
    else   { debut = indexActuel;   }

    // Parcours des données
    // ------------------------------------------------------------
    for (int i = 0; i < nombreValeurs; i++)   {
      int position =
        (debut + i) % TAILLE_TAMPON;

      csv += String(bufferTemps[position]); csv += ";";

      csv += String(bufferTension[position], 2);  csv += ";";

      csv += String(bufferCourant[position], 3);  csv += ";";

      csv += String(bufferPuissance[position], 2);  csv += ";";

      csv += String(bufferFP[position], 3);

      csv += "\r\n";
    }
  }   

  // NOM DU FICHIER
  // --------------------------------------------------------------
  server.sendHeader(  "Content-Disposition",  "attachment; filename=mesures_2H.csv"  );

// Remise à zéro uniquement de la progression
if (nombreValeurs >= TAILLE_TAMPON) {nombreValeurs = 0;}

// Envoi du CSV
server.send(200, "text/csv", csv);  // Envoi du CSV

}  //end handleDownload()


// ================================================================
void setup()
{
  Serial.begin(115200);

  analogReadResolution(12);
  analogSetAttenuation(ADC_11db);

  pinMode(PIN_ZMPT, INPUT);
  pinMode(PIN_intensity, INPUT);

  pinMode(PIN_LED, OUTPUT);
  digitalWrite(PIN_LED, LOW);

  tft.init();
  tft.setRotation(0);

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid,  password  );
  while (WiFi.status() != WL_CONNECTED)   { delay(500);  Serial.print(".");  }

server.on("/", handleRoot);
server.on("/download", handleDownload);
server.on("/status", handleStatus);
server.on("/data", handleData);

  server.begin();

  Serial.println("Serveur Web demarre");

  resultat.tensionRMS = 0;
  resultat.currentRMS = 0;
  resultat.Power      = 0;
  resultat.FP         = 0;

  nouvelleMesure = false;

  indexActuel = 0;
  nombreValeurs = 0;

  tempsEnregistrement = 0;
  generationBuffer = 0;

  // CREATION TACHE MESURE SUR CORE 0
  // ==============================================================

  xTaskCreatePinnedToCore(
    mesureCore0,          // fonction
    "MESURE",             // nom
    4096,                 // stack
    NULL,                 // parametre
    2,                    // priorité
    &mesureTaskHandle,    // handle
    0                     // CORE 0
  );

  uint8_t timer_id = 1;      // // 1 tick = 1 us
  uint16_t prescaler = 80;   //80 MHz / 80 = 1 MHz

  timer = timerBegin( timer_id, prescaler,  true  );
  timerAttachInterrupt( timer, &timerISR, true );
  timerAlarmWrite(  timer, PERIODE_US, true  );
  timerAlarmEnable(timer);


  tft.fillScreen(TFT_BLACK);
  tft.setCursor(0, 0, 2);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.println("MESURE ELECTRIQUE");

  Serial.println("Systeme demarre");
}


// ================================================================
float V  = 0;   // VARIABLES CORE 1
float I  = 0;
float P  = 0;
float FP = 0;
float E, joule;


void loop()   {    // LOOP CORE 1


  server.handleClient();     // SERVEUR WEB   1ms   


  if (nouvelleMesure == true)    {
 //   digitalWrite(25,1);      //durée 5ms
    portENTER_CRITICAL(&mux);      // Lecture protégée des données
    V  = resultat.tensionRMS;
    I  = resultat.currentRMS;
    P  = resultat.Power;
    FP = resultat.FP;
    nouvelleMesure = false;
    portEXIT_CRITICAL(&mux);

      // ENREGISTREMENT DANS LE BUFFER CIRCULAIRE
    // ============================================================

    bufferTemps[indexActuel] =tempsEnregistrement;
    bufferTension[indexActuel] = V;
    bufferCourant[indexActuel] = I;
    bufferPuissance[indexActuel] = P;
    bufferFP[indexActuel] =   FP;

    tempsEnregistrement += 2;
    indexActuel++;

    if (indexActuel >= TAILLE_TAMPON)   { indexActuel = 0; generationBuffer++;   }       // Avancement buffer circulaire
    if (nombreValeurs < TAILLE_TAMPON)    {nombreValeurs++;   }      // Nombre de valeurs enregistrées

    tft.setCursor(0, 25, 4);

    tft.setTextColor( TFT_GREEN,  TFT_BLACK );
    tft.print("V: ");        // TENSION
    tft.print(V, 1);
    tft.print(" V");

    tft.setCursor(0, 55, 4);
    tft.setTextColor( TFT_RED,  TFT_BLACK );
    tft.print("I: ");
    tft.print(I, 2);       // COURANT
    tft.print(" A ");

    tft.setCursor(0, 85, 4);
    tft.setTextColor( TFT_BLUE, TFT_BLACK );
    tft.print("P:");
    tft.print(P, 0);        // PUISSANCE ACTIVE
    tft.print("   ");

    tft.setCursor(0, 115, 4);
    tft.setTextColor( TFT_WHITE,  TFT_BLACK   );
    tft.print("FP:");       // FACTEUR DE PUISSANCE
    tft.print(FP, 2);
    tft.print("  ");

    joule=joule+P;   //
E=(joule*2)/3600;   //wh

    tft.setCursor(0, 145, 4);
    tft.setTextColor( TFT_WHITE,  TFT_BLACK   );
    tft.print("E:");       // FACTEUR DE PUISSANCE
    tft.print(E,0);
   tft.print("  ");

//  digitalWrite(25,0); 
  }

  delay(5);     // Petite pause pour laisser le serveur WiFi respirer
}

On m’a posé la question pourquoi ne pas utiliser ESP home ?
Parce ce que je ne le connais pas bien.

Si la mise en réseau semble assez simple, je n’ai en revanche jamais testé l’imposition du temps d’échantillonnage des mesures !

Bref, avec le programme précèdent, l’ESP32 commandera d’autres ESP32 avec Wi-Fi mais sans passer par la box ce qui sera plus simple mais il ne faudra pas qu’il soit trop éloignée 25m en intérieur et ? m en exterieur

Juste une remarque, dans une information plutôt pédagogique il est préférable d'utiliser nullptr à null en C++ cela peut toujours éviter des surprises désagréables et difficilement compréhensibles pour les débutants en C++

Bonjour,
Attention au post #9 et #7, le réseau et son mot de passe est visible en clair je crois.
Votre post est très intéressant par ailleurs. Je lui suis avec intérêt :slight_smile:

Bonnes bidouilles,
Pandaroux007

Merci pour le mot de passe

Pour un vieux enseignant, je manque de rigueur…pour la bidouille

Pour continuer sur la bidouille voici les programmes de communication en wi ifi entre 2 ESP32

Sur le net, il y a une foule d’explication sur les différents modes de wifi avec l’ESP32 et de commande.

Mais pour simplifier, on va faire un programme simple avec 1 ESP32 en mode serveur qui sera le compteur d'énergie et qui va communiquer directement avec ESP NOW vers d'autres ESP32 qui feront contacteur tout ou rien pour delester.


En effet, ESP NOW a été développé pour cela avec 250 octets de transmission possible.

L’abréviation MAC signifie Media Access Control) gère la façon dont les données se déplacent dans le wifi. Le mode broadcast (diffusion générale) désigne l'envoi d'un même message à tous les appareils à portée qui écoutent sur le même canal adresse mac (FF:FF:FF:FF:FF:FF).

Si on désire une communication qu’a 1 seul ESP32 client, il faut aller lire l’adresse du récepteur.

Tout est bien expliqué sur ce lien

J’utilise l’ESP32 Arduino core 2.0.17 et pas les dernières versions ou les fonctions de la routine d’interruption avait changé avec les nouvelles versions, mais il y a aussi des fonctions avec ESP NOW qui ont changé.

Voici le code pour le TTGO serveur, juste pour une mesure de temperature simulé par simplification

// ============================================================
// ESP32 SERVEUR - TTGO
//
// Arduino ESP32 Core 2.0.17
//
// WIFI :
// ESP32 serveur -> Livebox -> PC
//
// ESP-NOW :
// ESP32 serveur -> BROADCAST -> ESP32 client
//
// Le serveur envoie les commandes ON / OFF en broadcast.
//
// Le PC commande la LED avec la page Web.
// ============================================================

#include <WiFi.h>
#include <WebServer.h>
#include <TFT_eSPI.h>
#include <esp_now.h>
#include <esp_wifi.h>

const char* ssid = "       ";
const char* password = "       ";

TFT_eSPI tft = TFT_eSPI();

WebServer server(80);

float temperature = 23.5;

bool ledState = false;   // ETAT LED DISTANTE

int wifiRSSI = 0;   // decibel RSSI WIFI DU SERVEUR

int wifiChannel = 6;

// ============================================================
// STRUCTURE DU MESSAGE ESP-NOW

typedef struct { uint8_t type; bool led;} Message;

#define MSG_LED_COMMAND 1

// ADRESSE BROADCAST
uint8_t broadcastAddress[] = {  0xFF,  0xFF,  0xFF,  0xFF,  0xFF,  0xFF};

// ============================================================
// ENVOI COMMANDE LED

void sendLedCommand(bool state)
{
 Message msg;

 msg.type = MSG_LED_COMMAND;
 msg.led = state;

 esp_err_t result;

 result = esp_now_send( broadcastAddress,  (uint8_t*)&msg,  sizeof(msg)  );

 // ----------------------------------------------------------
 // Résultat

 if (result == ESP_OK)   {
   Serial.print(  "Commande ESP-NOW envoyee : "  );

   if (state)    {Serial.println("ON");  }
   else    { Serial.println("OFF");   }
                       }
 else
 {  Serial.print( "Erreur ESP-NOW : "  );
   Serial.println(   result  );  }}

void handleRoot()   // PAGE WEB   HTML
{
 String page = "";

 page += "<!DOCTYPE html>";
 page += "<html lang='fr'>";
 page += "<head>";
 page += "<meta charset='UTF-8'>";
 page += "<meta name='viewport' ";
 page += "content='width=device-width, initial-scale=1.0'>";

 page += "<title>ESP32 TTGO</title>";

 page += "<style>";     // STYLE

 page += "body{";
 page += "font-family:Arial;";
 page += "text-align:center;";
 page += "background:#eeeeee;";
 page += "margin:20px;";
 page += "}";

 page += ".bloc{";
 page += "background:white;";
 page += "padding:20px;";
 page += "margin:15px auto;";
 page += "max-width:450px;";
 page += "border-radius:15px;";
 page += "box-shadow:0 2px 8px #999;";
 page += "}";

 page += "button{";
 page += "font-size:28px;";
 page += "padding:15px 45px;";
 page += "margin:10px;";
 page += "border:0;";
 page += "border-radius:10px;";
 page += "color:white;";
 page += "cursor:pointer;";
 page += "}";

 page += ".on{";
 page += "background:#00aa44;";
 page += "}";

 page += ".off{";
 page += "background:#dd2222;";
 page += "}";

 page += ".valeur{";
 page += "font-size:30px;";
 page += "}";

 page += "</style>";
 page += "</head>";

 page += "<body>";     // BODY

 page += "<h1>ESP32 TTGO</h1>";

 page += "<div class='bloc'>";
 page += "<h2>Temperature</h2>";       // TEMPERATURE
 page += "<div class='valeur' id='temperature'>";

 page += String( temperature,  1 );
 page += " &deg;C";
 page += "</div>";
 page += "</div>";

 // ==========================================================
 // WIFI SERVEUR

 page += "<div class='bloc'>";
 page += "<h2>Wi-Fi vers la box</h2>";
 page += "<p>Signal :</p>";
 page += "<div class='valeur' id='wifi'>";
 page += String( WiFi.RSSI()
 );

 page += " dBm";
 page += "</div>";
 page += "<p>Canal : ";

 page += String(
   WiFi.channel()
 );

 page += "</p>";

 page += "</div>";

 // ==========================================================
 // ESP-NOW

 page += "<div class='bloc'>";
 page += "<h2>ESP-NOW</h2>";
 page += "<p>";
 page += "Mode broadcast";
 page += "</p>";

 page += "<p>Canal : ";

 page += String(
   WiFi.channel()
 );

 page += "</p>";
 page += "</div>";
 // ==========================================================
 // LED
 page += "<div class='bloc'>";
 page += "<h2>LED distante</h2>";
page += "<p id='led'>";

 if (ledState)   { page += "LED : ON"; }
 else  {    page += "LED : OFF";  }

 page += "</p>";

 // ----------------------------------------------------------
 // BOUTON ON

 page += "<button class='on' ";
 page += "onclick='ledOn()'>ON</button>";

 // ----------------------------------------------------------
 // BOUTON OFF

 page += "<button class='off' ";
 page += "onclick='ledOff()'>OFF</button>";
 page += "</div>";

 // ==========================================================
 // JAVASCRIPT
 // ==========================================================

 page += "<script>";

 page += "function ledOn(){";     // ON
 page += "fetch('/led/on')";
 page += ".then(r=>r.text())";
 page += ".then(t=>update());";
 page += "}";

 page += "function ledOff(){";       // OFF
 page += "fetch('/led/off')";
 page += ".then(r=>r.text())";
 page += ".then(t=>update());";
 page += "}";

 // ----------------------------------------------------------
 // MISE A JOUR
 // ----------------------------------------------------------

 page += "function update(){";
 page += "fetch('/data')";
 page += ".then(r=>r.json())";
 page += ".then(d=>{";
 page += "document.getElementById('temperature').innerHTML=";
 page += "d.temperature.toFixed(1)+' &deg;C';";
 page += "document.getElementById('wifi').innerHTML=";
 page += "d.wifi+' dBm';";
 page += "document.getElementById('led').innerHTML=";
 page += "d.led ? 'LED : ON' : 'LED : OFF';";
 page += "});";
 page += "}";

 // ----------------------------------------------------------
 // ACTUALISATION
 // ----------------------------------------------------------

 page += "setInterval(update,2000);";
 page += "</script>";
 page += "</body>";
 page += "</html>";

 // ----------------------------------------------------------
 // ENVOI PAGE
 // ----------------------------------------------------------

 server.send(  200,  "text/html",  page );}

// ============================================================
// BOUTON ON


void handleLedOn()   {
 ledState = true;
 sendLedCommand(  true );
 server.send( 200, "text/plain", "ON" );
}

// ============================================================
// BOUTON OFF

void handleLedOff(){
 ledState = false;
 sendLedCommand(  false );
 server.send( 200, "text/plain",  "OFF" );
}


// ============================================================
void handleData()   {   // DONNEES POUR LA PAGE WEB
   wifiRSSI = WiFi.RSSI();
 String json = "{";
 json += "\"temperature\":";
 json += String( temperature,  1 );
 json += ",";
 json += "\"wifi\":";
 json += String(  wifiRSSI  );
 json += ",";
 json += "\"led\":";
 if (ledState)    {  json += "true"; }
 else     {  json += "false";  }
 json += "}";

 server.send(   200,   "application/json",   json  );
}

// ============================================================

void setup()   {

 Serial.begin(  115200 );

 delay(500);

 Serial.println();
 Serial.println( "ESP32 SERVEUR - TTGO" );
 Serial.println( "ESP-NOW BROADCAST"  );
 Serial.println( "Arduino Core 2.0.17"  );

 tft.init();

 tft.setRotation( 0 );
 tft.fillScreen(TFT_BLACK  );
 tft.setTextColor( TFT_WHITE, TFT_BLACK );
 tft.setTextSize(2);
 tft.setCursor(  10,  20 );
 tft.println( "Connexion WiFi..."  );


 WiFi.mode( WIFI_STA );
 WiFi.begin( ssid, password );
 Serial.print( "Connexion a la box" );

 while ( WiFi.status() != WL_CONNECTED  )     // ATTENTE WIFI
 {    delay(500);   Serial.print( "."  );  }

 Serial.println();
 Serial.println(  "WiFi connecte !" );

 Serial.print( "Adresse IP : " );
 Serial.println(  WiFi.localIP() );

 wifiRSSI = WiFi.RSSI();

 Serial.print( "Signal WiFi : " );
 Serial.print(  wifiRSSI );
 Serial.println( " dBm"  );

 // ==========================================================

 Serial.println();
 Serial.println( "Initialisation ESP-NOW..." );

 if ( esp_now_init() != ESP_OK  )
 {    Serial.println( "ERREUR initialisation ESP-NOW"  );
   tft.fillScreen( TFT_BLACK );
   tft.setTextColor( TFT_RED, TFT_BLACK );
   tft.setCursor( 10, 50 );
   tft.println( "Erreur ESP-NOW" );
   return;
 }

 Serial.println(  "ESP-NOW initialise" );


 esp_now_peer_info_t peerInfo = {};    // communication direct
 memcpy(   peerInfo.peer_addr,  broadcastAddress,  6 );
 peerInfo.channel =  wifiChannel;
 peerInfo.encrypt = false;

 if (  esp_now_add_peer( &peerInfo ) != ESP_OK)
 {  Serial.println( "Erreur ajout peer broadcast"  );  }
 else   {  Serial.println( "Broadcast ESP-NOW active" );  }


 server.on(    "/",  handleRoot );
 server.on(  "/led/on",  handleLedOn );
 server.on( "/led/off", handleLedOff );
 server.on( "/data", handleData );
 server.begin();

 Serial.println( "Serveur Web demarre" );
 Serial.println("SERVEUR PRET" );
 Serial.print( "PC : http://");
 Serial.println( WiFi.localIP() );

}


// ============================================================
void loop()  {

 server.handleClient();    // SERVEUR WEB

 static unsigned long previousMillis = 0;

 if ( millis() - previousMillis >= 2000 )      // Mise a jour toutes les 2 secondes
 {    previousMillis =  millis();

   temperature += 0.1;      // Température simulation

   wifiRSSI = WiFi.RSSI();

   tft.fillScreen( TFT_BLACK );
   tft.setTextColor( TFT_GREEN, TFT_BLACK  );
   tft.setCursor(  10,  20 );

   tft.println( "SERVEUR OK"  );

   tft.setTextColor( TFT_WHITE, TFT_BLACK  );
   tft.setCursor( 10,   55  );
   tft.print( "dbm: ");
   tft.print(   wifiRSSI  );

    tft.setCursor( 10, 125 );
   tft.print( "T:");
   tft.print(temperature, 1 );

   tft.println(" C");

   tft.setCursor( 10, 160);

   tft.print("LED: " );

   if (ledState)  {
     tft.setTextColor( TFT_GREEN, TFT_BLACK );
     tft.println( "ON" );   }
   else    {  tft.setTextColor( TFT_RED, TFT_BLACK );
              tft.println( "OFF"  );  }

  }
}

Voici la page HTML

Voici le code pour le TTGO client recepteur
Lors d’un envoie, la valeur en dbm est affichée pour tester la distance de fonctionnement du wifi pour savoir si cela va fonctionner ou pas.
En Europe, la puissance est limité à 89 mW donc par rapport au milliwatt cela donne +19.5mdB qui est poissble à parametrer
WiFi.setTxPower(WIFI_POWER_19_5dBm); //
La portée va dépendre de l’antenne. Avec une petite antenne low tech on peut gagner 10dbm.
Mais sur le display TTGO ce n’est pas facile à souder

// ============================================================
// ESP32 CLIENT / ESCLAVE
//
// Reçoit les commandes du TTGO SERVEUR par ESP-NOW
// Commande une LED   sans DE CONNEXION A LA BOX
// Affiche l'état de la LED et le RSSI en dBm
//
// Compatible ESP32 Arduino Core 2.0.17
//
// ============================================================

#include <WiFi.h>
#include <esp_now.h>
#include <esp_wifi.h>
#include <TFT_eSPI.h>

TFT_eSPI tft = TFT_eSPI();

#define LED_PIN 2

#define WIFI_CHANNEL 6   //notre choix

#define MSG_LED_COMMAND 1

// ============================================================
// STRUCTURE DU MESSAGE


typedef struct  {  uint8_t type;  bool led;} Message;
Message message;

// Indique qu'un nouveau message ESP-NOW est arrivé
volatile bool nouveauMessage = false;

// RSSI de la dernière trame WiFi reçue
volatile int dernierRSSI = -100;

// ============================================================
// CALLBACK WIFI PROMISCUOUS

void wifiPromiscuousCallback(  void *buf,  wifi_promiscuous_pkt_type_t type)
{
  if (buf == NULL)    { return; }

  if (  type == WIFI_PKT_DATA ||    type == WIFI_PKT_MGMT ||   type == WIFI_PKT_CTRL  )    {
    wifi_promiscuous_pkt_t *packet;
    packet = ( wifi_promiscuous_pkt_t * )buf;
    dernierRSSI = packet->rx_ctrl.rssi;
  }
}

// ============================================================
// CALLBACK ESP-NOW
void onDataReceive(  const uint8_t *mac,  const uint8_t *data,  int len)  {

  if (len != sizeof(Message))   { return;  }    // Vérification taille

  memcpy( &message,  data,  sizeof(message)  );     // Copie du message

  nouveauMessage = true;    // Nouveau message disponible
}

// ============================================================
// AFFICHAGE LED + RSSI
// ============================================================

void afficherLED()  {
  tft.fillScreen( TFT_BLACK );
  tft.setTextDatum( MC_DATUM );

  // ==========================================================
  // ETAT LED

  if (message.led)    {

    tft.setTextColor( TFT_GREEN, TFT_BLACK  );
    tft.setTextSize(4);
    tft.drawString(   "ON",  tft.width() / 2,   65  );
  }
  else    {

    tft.setTextColor(  TFT_RED,  TFT_BLACK );
    tft.setTextSize(4);
    tft.drawString(  "OFF", tft.width() / 2,  65  );
  }

  // ==========================================================
  // TEXTE LED
  // ==========================================================

  tft.setTextColor(TFT_WHITE, TFT_BLACK  );
  tft.setTextSize(2);
  tft.drawString( "LED", tft.width() / 2, 115  );

  // ==========================================================
  // RSSI

  tft.setTextColor( TFT_CYAN,  TFT_BLACK );
  tft.setTextSize(2);
  tft.drawString(    "Signal",  tft.width() / 2,    160  );
  char texteRSSI[30];
  sprintf( texteRSSI,  "%d dBm", dernierRSSI  );
  tft.drawString(texteRSSI, tft.width() / 2, 185);
 // tft.setTextDatum( TL_DATUM );     // Retour alignement normal
}

// ============================================================

void setup()
{
  pinMode( LED_PIN,  OUTPUT  );

  Serial.begin(115200 );
  delay(500);
  Serial.println(   "ESP32 CLIENT / ESCLAVE"  );
  Serial.println( "ESP-NOW + RSSI"    );

  digitalWrite( LED_PIN,   LOW );

  message.type = MSG_LED_COMMAND;
  message.led =    false;

  tft.init();
  tft.setRotation( 0);
  tft.fillScreen(TFT_BLACK );
  tft.setTextColor(TFT_WHITE, TFT_BLACK  );
  tft.setTextSize(2);
  tft.setTextDatum( TL_DATUM );
  tft.setCursor(   10, 20 );
  tft.println(  "ESP32 CLIENT"  );
  tft.setCursor(  10,  50 );
  tft.println( "Initialisation..."    );

  WiFi.mode( WIFI_STA   );

//  Serial.print( "Canal WiFi : " );
//  Serial.println( WIFI_CHANNEL  );

  esp_err_t resultatCanal;

  resultatCanal =    esp_wifi_set_channel(   WIFI_CHANNEL,  WIFI_SECOND_CHAN_NONE  );

  if (   resultatCanal != ESP_OK  )    { Serial.println(  "ERREUR canal WiFi"   );

    tft.fillScreen( TFT_BLACK  );
    tft.setTextColor(TFT_RED, TFT_BLACK  );
    tft.setCursor( 10, 50 );
    tft.println( "Erreur canal WiFi"   );

    return;
  }

  // ==========================================================
  // CALLBACK PROMISCUOUS POUR RSSI

  esp_wifi_set_promiscuous(true );
  esp_wifi_set_promiscuous_rx_cb(    &wifiPromiscuousCallback  );
  Serial.println( "Lecture RSSI active"  );
  Serial.println(    "Initialisation ESP-NOW..."  );

  if (  esp_now_init() != ESP_OK  )  {
    Serial.println(  "ERREUR ESP-NOW"  );
    tft.fillScreen(  TFT_BLACK  );
    tft.setTextColor( TFT_RED, TFT_BLACK );
    tft.setCursor(  10,   50 );
    tft.println(  "Erreur ESP-NOW"   );
    return;  }

  Serial.println( "ESP-NOW initialise" );

  // ==========================================================
  // CALLBACK ESP-NOW

  if (  esp_now_register_recv_cb(  onDataReceive  ) != ESP_OK )
  {    Serial.println(  "ERREUR callback ESP-NOW"  );
    tft.fillScreen(TFT_BLACK  );
    tft.setTextColor(  TFT_RED,   TFT_BLACK  );
    tft.setCursor(10, 50 );
    tft.println(  "Erreur callback"  );
    return;
  }

  Serial.println(  "Callback ESP-NOW OK" );

  afficherLED();

  Serial.println();
  Serial.println(  "================================" );
  Serial.println(  "CLIENT PRET"  );
  Serial.println(  "Attente ON / OFF..."  );
  Serial.println(  "================================" );
}


// ============================================================

void loop()  {

  if (nouveauMessage)  {     // Nouveau message reçu ?
    nouveauMessage = false;      // Désactivation du drapeau

    // --------------------------------------------------------
    // Vérification type

    if (  message.type == MSG_LED_COMMAND  )    {      // Vérification type
      if (message.led)   {digitalWrite( LED_PIN,  HIGH  );
        Serial.print(  "Commande recue : ON" );
        Serial.print( " | RSSI : "     );
        Serial.print(  dernierRSSI   );
        Serial.println( " dBm"   );
      }

      // ======================================================
      // LED OFF
       else
      { digitalWrite(LED_PIN,   LOW   );
        Serial.print( "Commande recue : OFF"  );
        Serial.print(    " | RSSI : "   );
        Serial.print( dernierRSSI);
        Serial.println(" dBm" );
      }

      afficherLED();          // Mise à jour écran
    }


    else   {  Serial.print(   "Type inconnu : "  );
      Serial.println(   message.type  );
    }
  }

  delay(1);
}

Voici ce que cela donne sur les affichages des 2 TTGO

Sur la page HTML du serveur, 1 seul bouton commande manuelle ou automatique avec 1 seul bouton on/off serait intéressant à faire pour commander automatiquement ou pas le delestage.
De plus, une perte du wifi à l’ESP32 serveur devrait etre signalé en interrogeant regulierement l’ESP 32 récepteur.

En faisant des essais, on a remarqué qu’entre le capteur de tension ZMPT et le capteur de courant, il y a un déphasage de 28° sur le signal transmis de la puissance avec la courbe bleu correspondant à la tension secteur, la courbe rouge le courant, la courbe verte la puissance instantanée alors que la charge était purement résistive donc déphasage nulle. Sachant que les filtres numériques entrainent aussi de petits déphasages.
on a repris le programme du poste 4
Par conséquent, on a décalé de 2 échantillons, la mesure de la tension pour avoir la courbe suivante
On a fait aussi 3 tours dans le capteur de courant pour améliorer la précision de la mesure du courant
De plus, on a rajouté un filtre passe bas avec une fréquence de coupure de 100Hz sur la mesure de tension et de courant pour minimiser le bruit sur le courant mais cela atténué très légèrement la tension et le courant.

Donc, on a du ajusté le calibre de la tension et du courant et cela fonctionne tres bien à 0.01prés pour le facteur de puissance.

Voici le signal qui donne tension, courant, puissance instantanée pour 1000W pour 232V RMS qui sont bien en phase pour la determination de la puissance instannée


Pour vérifier le progrmme, on a voulu tester une autre méthode qui détermine le facteur de puissance

La précision théorique de cette méthode avec 2 périodes est de 7.2° de déphasage, avec 4 périodes de 3.6° etc.

Mais cette méthode donne de moins bons résultats à 0.1 près lorsque le facteur de puissance est inferieure à 0.6.

Voici le nouveau programme de l’énergie mètre avec les 2 méthodes pour mesurer le déphasage

//TTGO dispaly   ZMPT101B sensor voltage
// Mesure de tension et intensity  AC RMS  filtre IIR passe haut et passe bas 

#define PIN_ZMPT       32       // Capteur tension
#define PIN_intensity  33       // SCT-013-050

#include <TFT_eSPI.h>
#include <SPI.h>

hw_timer_t * timer = NULL;

TFT_eSPI tft = TFT_eSPI();  //library   1,4" color 135, 240


const int nbrEchantillons = 2500;   // pour affichage
volatile int sampler=0;
volatile  bool flag;
volatile  bool flagenvoie;


  float valeurADC,valeurADCn ;  // tension,
  float valeurI,valeurIn ;    //current


const float PeS = 0.0008;       //periode sampler 800us  1250Hz
const int nbrsampler=100;       
const float calibreI=0.0385/3 ;   //3 tours
const float calibreV=0.482;

const float fmain = 50.0;   //frequency reseau

float Ucos = 0;
float Usin = 0;

float Icos = 0;
float Isin = 0;

  int16_t tableV[nbrsampler];
  int16_t tableI[nbrsampler];
  float tablePowerinstant[nbrsampler];

  byte indexTable=0;

  float signalV,signalVn;
  float signalVf, signalVfn;
float signalVf1, signalVf2,signalVf3; 

  float signalI,signalIn;
  float signalIf,signalIfn;

  float rmsV, rmsVn;
  float tensionRMS;

  float rmsI, rmsIn;
  float currentRMS;

  float MaxV;  //
  float minV;
  float amplitudeV;

  float MaxI;  //
  float amplitudeI;

  float Powerinstant;
  float Power;
  float Powern;
  float Powerf;
  float S;      //power appararente
  float FP;     //factor power

  float joule2;  
  float Energy;

//  const float alpha = 1.0f - expf(-0.0008 / cstemps);    //0.0016    cstemps=1
const float alphaPB=0.0016;     //flitre passe bas pour la mesure de la puissance moyenne
const float alphaLP = 0.6;    //     0.33 filtre low pas pour retirer le bruit au dessus de 100Hz     0.314  pour 75Hz
const float alphaHP =0.9901;    //flitre passe haut pour retirer la composante continue

void IRAM_ATTR timer_isr() {    //routine interruption 800u seconde
if (flagenvoie==1) {flag=0;} else  {flag=1;} 
 sampler++;     //affichage tous les 2s

}//fin isr


void setup() {

  Serial.begin(115200);

  pinMode(25, OUTPUT);
  tft.init();                 //135 × 240.
  tft.setRotation(0); //0 format portrait a l'endroit  2 180°

  tft.fillScreen(TFT_BLACK);   
   

  analogReadResolution(12);       // ADC 12 bits : 0 à 4095
  analogSetAttenuation(ADC_11db); // Plage ADC environ 0 à 3,3 V

uint8_t timer_id = 1;     
  uint16_t prescaler = 80; // Between 0 and 65 535
  int threshold = 800; // 800us

  timer = timerBegin(timer_id, prescaler, true);
  timerAttachInterrupt(timer, &timer_isr, true);
  timerAlarmWrite(timer, threshold, true);
  timerAlarmEnable(timer);
}


void loop() {

if (flag==1)  {flag=0;
// if (digitalRead(25)==0)   {digitalWrite(25,1); } else {digitalWrite(25,0); }
digitalWrite(25,1);    //temps de calcul du flag 250us
valeurADC = analogRead(PIN_ZMPT);      //pinZMPT sensor voltage   
valeurI = analogRead(PIN_intensity);      //pin intensity  
    signalV = alphaHP * (signalVn + valeurADC - valeurADCn);  //filtre passe haut
    signalVn=signalV;       //valeur precedente
        valeurADCn=valeurADC;

    signalVf2=signalVf1;  //retard 2 echantillon
    signalVf1=signalVf;   //retard 1 echantillon     14°
    signalVf = signalVfn + alphaLP * (signalV - signalVfn);   //filtre passe pas 100Hz
    signalVfn = signalVf;

    rmsV = rmsVn + alphaPB * (signalV*signalV - rmsVn);  //filtre passe bas
    rmsVn=rmsV;             //valeur precedente
tensionRMS=(sqrt(rmsV))*calibreV;

    signalI = alphaHP * (signalIn + valeurI - valeurIn);  //filtre passe haut 2Hz
    signalIn=signalI;       //valeur precedente
    valeurIn=valeurI;   

    signalIf = signalIfn + alphaLP * (signalI - signalIfn);  //filtre passe pas 100Hz
    signalIfn = signalIf;


    rmsI = rmsIn + alphaPB * (signalIf*signalIf - rmsIn);  //filtre passe bas
    rmsIn=rmsI;             //valeur precedente
currentRMS= (sqrt(rmsI))*calibreI; 
    
    Powerinstant = signalIf * signalVf2;     //decalage de 30°
    Powerf = Powern + alphaPB * (Powerinstant - Powern);  //puissance filtrée
    Powern = Powerf;           //n-1

Power=Powerf*calibreV*calibreI;
joule2=joule2+Power;     //
Energy=(joule2*0.0008)/3600;   //wh


    tableV[indexTable] = signalVf2;    //enregiste les 50 derniers mesures  retard2
    tableI[indexTable] = signalIf;      //signalIf
    tablePowerinstant[indexTable]=Powerinstant*calibreV*calibreI;
    indexTable++;
    if  (indexTable>=nbrsampler)    {indexTable=0; }

digitalWrite(25,0);

}// fin flag



if (sampler>2500)  {   //  800us*2500=2secondes affichage et bilan
 flagenvoie=1;

for (int n = 0; n < nbrsampler; n++)
{
    float angle = 2.0 * PI * fmain * n *PeS;    //2.0 * PI * fmain * n *PeS

    Ucos += tableV[n] * cos(angle);
    Usin += tableV[n] * sin(angle);

    Icos += tableI[n] * cos(angle);
    Isin += tableI[n] * sin(angle);
} 

float phaseU = atan2(Usin, Ucos);
float phaseI = atan2(Isin, Icos);

float dephasage = phaseU - phaseI;

float FPsomme=cos(dephasage);

  for (uint16_t i = 0; i < nbrsampler; i++)
    {
      Serial.print(tableV[i]);
      Serial.print(",");
      Serial.print(tableI[i]);    //facteur d'echelle
      Serial.print(",");
      Serial.println(tablePowerinstant[i]);
    } 

tft.setCursor(0, 0, 4);// (x,y,taille)
tft.setTextColor(TFT_GREEN,TFT_BLACK);
tft.print("V:  ");
tft.print(tensionRMS,0);
tft.print("   ");


tft.setCursor(0, 30, 4);// (x,y,taille)
tft.setTextColor(TFT_RED,TFT_BLACK);
tft.print("I: ");
tft.print(currentRMS,2);
tft.print("   ");    

S=currentRMS*tensionRMS  ;         //power apparent
if (currentRMS>0.05) {FP=Power/S;  }   else {FP=1;  }   //factor power

tft.setCursor(0, 60, 4);// (x,y,taille)
tft.setTextColor(TFT_RED,TFT_BLACK);
tft.print("E:  ");
tft.print(Energy,0);
tft.print("   ");
                          
tft.setCursor(0, 90, 4);// (x,y,taille)
tft.setTextColor(TFT_BLUE,TFT_BLACK);
tft.print("P:  ");
tft.print(Power,0);
tft.print("    ");

tft.setCursor(0, 120, 4);// (x,y,taille)
tft.setTextColor(TFT_BLUE,TFT_BLACK);
tft.print("S:  ");
tft.print(S,0);
tft.print("   ");

tft.setCursor(0, 150, 4);// (x,y,taille)
tft.setTextColor(TFT_BLUE,TFT_BLACK);
tft.print("FP:  ");
tft.print(FP,2);
tft.print("   ");

tft.setCursor(0, 180, 4);// (x,y,taille)
tft.setTextColor(TFT_CYAN,TFT_BLACK);
tft.print("FP:  ");
tft.print(FPsomme,2);
tft.print("   ");

sampler=0;
flagenvoie=0;
}  //fin flagaenvoie et affichage


}   //end loop





Les améliorations de la mesure avec filtre passe haut et passe bas pour minimiser le bruit pour donner un facteur de puissance fiable, avec la fusion du programme utilisant les 2 core de l’ESP 32 a été fait avec l’envoie des données automatiques toutes les 2 heures.

J’avais un doute sur la mesure du facteur de puissance du Tuya TOMPD-63LW, mais l’ESP32 et ce module donne les mêmes valeurs

Les appareils électroniques faibles puissances qui ont du redressement avec filtrage capacitif ont des facteurs de puissances autour de 0.5 (téléviseur, chargeur de smartphone, chargeur de vélo, chargeur nettoyeur laveur) mais même les alimentations des microcontrôleurs des laves vaisselles, machine à laver….

Tout ce qui est grosse puissance 2000W (résistance four électrique, plaque vitro céramique, chauffage eau machine à laver) sont commandés en tout ou rien…

Aspirateur traineau qui a encore un moteur série DC avec son réglage triac par angle de phase provoque un mauvais facteur de puissance mais en générale, on est à fond.

Est-ce que les gens aurait compris la variabilité de leurs de consommation de leur maison avec le compteur linky qui enverrait tous les 2 secondes les données sur leur box ?

Car on peut observer la grande fluctuation de puissance active

De plus avec la réinjection du photovoltaïque, ces données fluctuent de positif à négatif.


Le facteur fluctue donc aussi de positif à négatif en fonction des charges tout ou rien. Il n’y a pas de batterie qui permettrait de lisser la consommation.

Par contre au niveau graphique, il est possible de faire une enveloppe moyenne et vectorielle pour minimiser les datas ou de faire la derivée de l'energie.

Voici le dernier code qui ne fait que mesurer le réseau électrique en positif et négatif sans commander d’autres ESP32

//TTGO dispaly  wifi server table buffer circulaire  .CSV
//envoie auto quand le buffer est plein
//filtre passe haut 2Hz retire offset, filtre pas bas 200hz retire le bruit, RMS, filtre passe bas

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <TFT_eSPI.h>

const char* ssid     = "      ";       // NOM WIFI
const char* password = "                    "; // MOT DE PASSE

WebServer server(80);

TFT_eSPI tft = TFT_eSPI();

#define PIN_ZMPT       32       // Capteur tension
#define PIN_intensity  33       // SCT-013-050
#define PIN_LED        25       // Test acquisition

#define N_ECHANTILLONS 2500   // PARAMETRES ACQUISITION  
#define PERIODE_US     800    // 2500 x 800 us = 2 secondes

// 2500 x 800 us = 2 secondes
// Une mesure complète est donc calculée toutes les 2 secondes.

#define calibreV  0.482f    // CALIBRATION
#define calibreI  0.0385f/3 ////3 tours

const int TAILLE_TAMPON = 1800;   //300/10minutes   1800/1heure

// 300 mesures x 2 secondes = 600 secondes = 10 minutes
float bufferTension[TAILLE_TAMPON];
float bufferCourant[TAILLE_TAMPON];
float bufferPuissance[TAILLE_TAMPON];
float bufferFP[TAILLE_TAMPON];

unsigned long bufferTemps[TAILLE_TAMPON];

int indexActuel = 0;
int nombreValeurs = 0;
unsigned long generationBuffer = 0;  //nouveau fichier
unsigned long tempsEnregistrement = 0;

hw_timer_t *timer = NULL;
TaskHandle_t mesureTaskHandle = NULL;


struct Mesure {  // STRUCTURE DES DONNEES PARTAGEES CORE 0 -> CORE 1
  float tensionRMS;
  float currentRMS;
  float Power;
  float FP;
};

volatile Mesure resultat;
volatile bool nouvelleMesure = false;

// Protection des données partagées
portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;

// ================================================================
// INTERRUPTION TIMER

void IRAM_ATTR timerISR()   {
  BaseType_t xHigherPriorityTaskWoken = pdFALSE;

  // Réveille la tâche de mesure sur Core 0
  vTaskNotifyGiveFromISR (mesureTaskHandle,   &xHigherPriorityTaskWoken  );

  if (xHigherPriorityTaskWoken)    { portYIELD_FROM_ISR(); }
}

// ================================================================
// CORE 0 : TACHE DE MESURE

void mesureCore0(void *parameter)   {
  const float alphaPB = 0.0016f;     // flitre passe bas pour la mesure de la puissance moyenne et RMS
  const float alphaLP = 0.6;         // 0.33 filtre low pas pour retirer le bruit au dessus de 100Hz     0.314  pour 75Hz
  const float alphaHP = 0.9901f;     //filtre passe haut pour retirer la composante continue

  float valeurADC  = 0.0f;     // TENSION
  float valeurADCn = 0.0f;

  float signalV,signalVn;
  float signalVf, signalVfn;
  float signalVf1, signalVf2; 

  float rmsV  = 0.0f;
  float rmsVn = 0.0f;

  float tensionRMS = 0.0f;

  float valeurI,valeurIn ;   // COURANT
  float signalI,signalIn;  
  float signalIf,signalIfn;
 
  float rmsI  = 0.0f;
  float rmsIn = 0.0f;

  float currentRMS = 0.0f;

  float Powerinstant = 0.0f;    // PUISSANCE
  float Powerf = 0.0f;
  float Powern = 0.0f;

  float Power = 0.0f;

  float S  = 0.0f;
  float FP = 0.0f;

  uint16_t echantillon = 0;      // COMPTEUR ECHANTILLONS

  // ==============================================================
  // BOUCLE DE MESURE
 
  while (true)     {      // Attente de l'interruption timer

    ulTaskNotifyTake( pdTRUE,  portMAX_DELAY  );
 //   digitalWrite(PIN_LED, !digitalRead(PIN_LED));   // LED de test

    valeurADC = analogRead(PIN_ZMPT);        // MESURE TENSION
    valeurI = analogRead(PIN_intensity);      // MESURE COURANT

    // Filtre passe-haut voltage
    signalV = alphaHP *(signalVn + valeurADC - valeurADCn);
    signalVn   = signalV;
    valeurADCn = valeurADC;

    signalVf2=signalVf1;  //retard 2 echantillon
    signalVf1=signalVf;   //retard 1 echantillon     14°
    signalVf = signalVfn + alphaLP * (signalV - signalVfn);   //filtre passe pas 100Hz
    signalVfn = signalVf;

    // RMS filtré
    rmsV = rmsVn +  alphaPB *(signalV * signalV - rmsVn);
    rmsVn = rmsV;
    tensionRMS = sqrt(rmsV) * calibreV;
    // ============================================================
    // Filtre passe-haut  COURANT
    signalI =alphaHP * (signalIn + valeurI - valeurIn);
    signalIn = signalI;
    valeurIn = valeurI;

    signalIf = signalIfn + alphaLP * (signalI - signalIfn);  //filtre passe pas 100Hz
    signalIfn = signalIf;

    // RMS filtré
    rmsI =rmsIn +alphaPB *(signalI * signalI - rmsIn);
    rmsIn = rmsI;
    currentRMS = sqrt(rmsI) * calibreI;

    Powerinstant = signalIf * signalVf2;     //decalage de 30°

    // Filtrage passe-bas de la puissance instantanée
    Powerf =Powern + alphaPB * (Powerinstant - Powern);
    Powern = Powerf;
    Power = Powerf * calibreV * calibreI;

    echantillon++;       // COMPTEUR

    // FIN DES 2500 ECHANTILLONS    2500 x 800 us = 2 secondes
    // ============================================================

    if (echantillon >= N_ECHANTILLONS)     {
      S = tensionRMS * currentRMS;

      if (S > 0.05f)   { FP = Power / S;         // Facteur de puissance
        if (FP > 1.0f)   FP = 1.0f;
        if (FP < -1.0f)  FP = -1.0f;
                      }   else   { FP = 0.0f;   }



      portENTER_CRITICAL(&mux);    // ENVOI CORE 0 -> CORE 1

      resultat.tensionRMS  = tensionRMS;
      resultat.currentRMS  = currentRMS;
      resultat.Power       = Power;
      resultat.FP          = FP;

      nouvelleMesure = true;

      portEXIT_CRITICAL(&mux);

      echantillon = 0;   // Remise à zéro du compteur
    }
  }
}

// ================================================================
void handleRoot() {  // PAGE WEB PRINCIPALE
  float V;
  float I;
  float P;
  float FP;
  float tempsbuffer;

  portENTER_CRITICAL(&mux);

  V  = resultat.tensionRMS;
  I  = resultat.currentRMS;
  P  = resultat.Power;
  FP = resultat.FP;

  portEXIT_CRITICAL(&mux);

  // PAGE HTML
  // --------------------------------------------------------------
  String page = "";

  page += "<!DOCTYPE html>";
  page += "<html>";
  page += "<head>";

  page += "<meta charset='UTF-8'>";
  page += "<meta name='viewport' content='width=device-width, initial-scale=1.0'>";

  // Actualisation toutes les 2 secondes
//  page += "<meta http-equiv='refresh' content='2'>";

  page += "<title>ESP32 - Mesure électrique</title>";

  page += "<style>";

  page += "body {";
  page += "font-family: Arial;";
  page += "margin: 20px;";
  page += "background-color: #eeeeee;";
  page += "}";

  page += "h1 {";
  page += "color: #333333;";
  page += "}";

  page += ".mesure {";
  page += "background-color: white;";
  page += "border-radius: 10px;";
  page += "padding: 15px;";
  page += "margin-bottom: 10px;";
  page += "font-size: 25px;";
  page += "font-weight: bold;";
  page += "}";

  page += ".tension { color: green; }";
  page += ".courant { color: red; }";
  page += ".puissance { color: blue; }";
  page += ".fp { color: purple; }";

  page += "button {";
  page += "font-size: 18px;";
  page += "padding: 10px 20px;";
  page += "}";

  page += "</style>";

  page += "</head>";
  page += "<body>";

  // TITRE
  // --------------------------------------------------------------
  page += "<h1>ESP32 - Mesure électrique</h1>";

  // TENSION
  // --------------------------------------------------------------

  page += "<div class='mesure tension'>";
page += "Tension RMS : ";
page += "<span id='tension'>";
page += String(V, 1);
page += "</span> V";
  page += "</div>";


  // COURANT
  // --------------------------------------------------------------

  page += "<div class='mesure courant'>";
page += "Courant RMS : ";
page += "<span id='courant'>";
page += String(I, 2);
page += "</span> A";
  page += "</div>";

  // PUISSANCE
  // --------------------------------------------------------------
  page += "<div class='mesure puissance'>";
page += "Puissance active : ";
page += "<span id='puissance'>";
page += String(P, 1);
page += "</span> W";
  page += "</div>";

  // FACTEUR DE PUISSANCE
  // --------------------------------------------------------------

  page += "<div class='mesure fp'>";
page += "Facteur de puissance : ";
page += "<span id='fp'>";
page += String(FP, 2);
page += "</span>";
  page += "</div>";

  // --------------------------------------------------------------
  // INFORMATIONS BUFFER
  // --------------------------------------------------------------
page += "<p>";
page += "Mesures enregistrees : ";

page += "<span id='progression'>";
page += String(nombreValeurs);
page += " / ";
page += String(TAILLE_TAMPON);
page += "</span>";

page += "</p>";

  // BOUTON CSV
  // --------------------------------------------------------------

  page += "<br>";
  page += "<a href='/download'>";
  page += "<button>";
  page += "Telecharger le fichier CSV";
  page += "</button>";
  page += "</a>";

  // --envoie des data automatique---------------------------------
page += "<script>";
page += "let derniereGeneration = -1;";
page += "async function actualiserMesures() {";
page += "try {";

page += "const reponse = await fetch('/data');";
page += "const data = await reponse.json();";

page += "document.getElementById('tension').innerText = data.V.toFixed(1);";
page += "document.getElementById('courant').innerText = data.I.toFixed(3);";
page += "document.getElementById('puissance').innerText = data.P.toFixed(1);";
page += "document.getElementById('fp').innerText = data.FP.toFixed(2);";

page += "} catch (erreur) {";
page += "console.log('Erreur mesures :', erreur);";
page += "}";
page += "}";
page += "async function surveillerBuffer() {";

page += "try {";

page += "const reponse = await fetch('/status');";
page += "const data = await reponse.json();";

page += "document.getElementById('progression').innerText = ";
page += "data.nombre + ' / ' + data.taille;";

page += "if (data.nombre >= data.taille && ";
page += "data.generation != derniereGeneration) {";

page += "derniereGeneration = data.generation;";

page += "console.log('BUFFER PLEIN - téléchargement automatique - génération : ' + data.generation);";

page += "const lien = document.createElement('a');";

page += "lien.href = '/download';";
page += "lien.download = 'mesures_2H.csv';";

page += "document.body.appendChild(lien);";
page += "lien.click();";
page += "document.body.removeChild(lien);";
page += "}";
page += "} catch (erreur) {";
page += "console.log('Erreur buffer :', erreur);";
page += "}";
page += "}";

page += "setInterval(actualiserMesures, 2000);";
page += "setInterval(surveillerBuffer, 2000);";

page += "actualiserMesures();";
page += "surveillerBuffer();";

page += "</script>";

page += "</body>";
page += "</html>";

  page += "</body>";
  page += "</html>";

    server.send( 200, "text/html",  page  );
}

void handleStatus() {

  String json = "";
  json += "{";

  json += "\"nombre\":";
  json += String(nombreValeurs);  

  json += ",";

  json += "\"taille\":";
  json += String(TAILLE_TAMPON);

  json += ",";

  json += "\"generation\":";
  json += String(generationBuffer);

  json += "}";
  server.send( 200, "application/json", json);
}

void handleData() {

  float V;
  float I;
  float P;
  float FP;

  portENTER_CRITICAL(&mux);

  V  = resultat.tensionRMS;
  I  = resultat.currentRMS;
  P  = resultat.Power;
  FP = resultat.FP;

  portEXIT_CRITICAL(&mux);

  String json = "{";

  json += "\"V\":";
  json += String(V, 1);

  json += ",\"I\":";
  json += String(I, 3);

  json += ",\"P\":";
  json += String(P, 1);

  json += ",\"FP\":";
  json += String(FP, 2);

  json += "}";

  server.send(200, "application/json", json);
}


// ================================================================
// TELECHARGEMENT CSV
// ================================================================
void handleDownload()  {
  String csv = "";

  csv += "Temps(s);Tension_RMS(V);Courant_RMS(A);Puissance_Active(W);FP\r\n";     // EN-TETE CSV

  if (nombreValeurs > 0)   {      // ORDRE CHRONOLOGIQUE
    int debut;

    if (nombreValeurs < TAILLE_TAMPON)   { debut = 0;  }
    else   { debut = indexActuel;   }

    // Parcours des données
    // ------------------------------------------------------------
    for (int i = 0; i < nombreValeurs; i++)   {
      int position =
        (debut + i) % TAILLE_TAMPON;

      csv += String(bufferTemps[position]); csv += ";";
      csv += String(bufferTension[position], 2);  csv += ";";
      csv += String(bufferCourant[position], 3);  csv += ";";
      csv += String(bufferPuissance[position], 2);  csv += ";";
      csv += String(bufferFP[position], 3);
      csv += "\r\n";
    }
  }   

  // NOM DU FICHIER
  // --------------------------------------------------------------
  server.sendHeader(  "Content-Disposition",  "attachment; filename=mesures_2H.csv"  );

// Remise à zéro uniquement de la progression
if (nombreValeurs >= TAILLE_TAMPON) {nombreValeurs = 0;}

// Envoi du CSV
server.send(200, "text/csv", csv);  // Envoi du CSV

}  //end handleDownload()


// ================================================================
void setup()  {
  Serial.begin(115200);

  analogReadResolution(12);
  analogSetAttenuation(ADC_11db);

  pinMode(PIN_ZMPT, INPUT);
  pinMode(PIN_intensity, INPUT);

  pinMode(PIN_LED, OUTPUT);
  digitalWrite(PIN_LED, LOW);

  tft.init();
  tft.setRotation(0);

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid,  password  );
  while (WiFi.status() != WL_CONNECTED)   { delay(500);  Serial.print(".");  }

server.on("/", handleRoot);
server.on("/download", handleDownload);
server.on("/status", handleStatus);
server.on("/data", handleData);

  server.begin();

  Serial.println("Serveur Web demarre");

  resultat.tensionRMS = 0;
  resultat.currentRMS = 0;
  resultat.Power      = 0;
  resultat.FP         = 0;

  nouvelleMesure = false;

  indexActuel = 0;
  nombreValeurs = 0;

  tempsEnregistrement = 0;
  generationBuffer = 0;

  // CREATION TACHE MESURE SUR CORE 0
  // ==============================================================

  xTaskCreatePinnedToCore(
    mesureCore0,          // fonction
    "MESURE",             // nom
    4096,                 // stack
    NULL,                 // parametre
    2,                    // priorité
    &mesureTaskHandle,    // handle
    0                     // CORE 0
  );

  uint8_t timer_id = 1;      // // 1 tick = 1 us
  uint16_t prescaler = 80;   //80 MHz / 80 = 1 MHz

  timer = timerBegin( timer_id, prescaler,  true  );
  timerAttachInterrupt( timer, &timerISR, true );
  timerAlarmWrite(  timer, PERIODE_US, true  );
  timerAlarmEnable(timer);


  tft.fillScreen(TFT_BLACK);
  tft.setCursor(0, 0, 2);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.println("MESURE ELECTRIQUE");

  Serial.println("Systeme demarre");
}


// ================================================================
float V  = 0;   // VARIABLES CORE 1
float I  = 0;
float P  = 0;
float FP = 0;
float Econso, joule;
float Einjec, jouleinjec;


void loop()   {    // LOOP CORE 1

  server.handleClient();     // SERVEUR WEB   1ms   

  if (nouvelleMesure == true)    {
 //   digitalWrite(25,1);      //durée 5ms
    portENTER_CRITICAL(&mux);      // Lecture protégée des données
    V  = resultat.tensionRMS;
    I  = resultat.currentRMS;
    P  = resultat.Power;
    FP = resultat.FP;
    nouvelleMesure = false;
    portEXIT_CRITICAL(&mux);

      // ENREGISTREMENT DANS LE BUFFER CIRCULAIRE
    // ============================================================

    bufferTemps[indexActuel] =tempsEnregistrement;
    bufferTension[indexActuel] = V;
    bufferCourant[indexActuel] = I;
    bufferPuissance[indexActuel] = P;
    bufferFP[indexActuel] =   FP;

    tempsEnregistrement += 2;
    indexActuel++;

    if (indexActuel >= TAILLE_TAMPON)   { indexActuel = 0; generationBuffer++;   }       // Avancement buffer circulaire
    if (nombreValeurs < TAILLE_TAMPON)    {nombreValeurs++;   }      // Nombre de valeurs enregistrées

    tft.setCursor(0, 25, 4);

    tft.setTextColor( TFT_GREEN,  TFT_BLACK );
    tft.print("V: ");        // TENSION
    tft.print(V, 1);
    tft.print(" V");

    tft.setCursor(0, 55, 4);
    tft.setTextColor( TFT_RED,  TFT_BLACK );
    tft.print("I: ");
    tft.print(I, 2);       // COURANT
    tft.print(" A ");

    tft.setCursor(0, 85, 4);
    tft.setTextColor( TFT_BLUE, TFT_BLACK );
    tft.print("P:");
    tft.print(P, 0);        // PUISSANCE ACTIVE
    tft.print("   ");

    tft.setCursor(0, 115, 4);
    tft.setTextColor( TFT_WHITE,  TFT_BLACK   );
    tft.print("FP:");       // FACTEUR DE PUISSANCE
    tft.print(FP, 2);
    tft.print("  ");

//    joule=joule+P;   //
//    E=(joule*2)/3.6;   //kwh

if (P>0)   {  joule=joule+P;   //puissance consommée
              Econso=(joule*2)/3.6; }  //kwh

 if (P<0)   {  jouleinjec=jouleinjec+P;   //puissance injectée
              Einjec=(jouleinjec*2)/3.6;    }             


    tft.setCursor(0, 145, 4);
    tft.setTextColor( TFT_WHITE,  TFT_BLACK   );
    tft.print("E:");       // FACTEUR DE PUISSANCE
    tft.print(Econso,0);
   tft.print("  ");

//  digitalWrite(25,0); 
  }

  delay(5);     // Petite pause pour laisser le serveur WiFi respirer
}

Remarques :

Le facteur de puissance n’est pas obligé d’être mémorisé dans le code car le traceur peut faire le calcul avec la tension, le courant et la puissance active.

En autoconsommation, étant donné que la réinjection est cadeau à cause que les centrales qui n’arriverait pas à faire baisser leur production rapidement et qu’il n’y a pas de volonté politiques à réguler le marché correctement.

Donc, dans le programme l’énergie consommée et réinjecté a été séparée comme dans le compteur linky

Pour faire du délestage, une période d’échantillonnage tous les 2 secondes est un minimum ainsi que l’affichage

Par contre pour avoir une dynamique de la puissance active sur une grande période de plus d’une heure pour avoir la puissance moyenne, on peut utiliser un filtre passe bas du premier ordre avec une fréquence de coupure de 1/600s donc de constante de temps de 10minutes comme on peut l’observer sur la figure suivante.

Ces 10 minutes correspondent aux montées en température et à l’inertie thermique de nos appareils électro-ménagers


Par conséquent, un enregistrement tous les minutes est amplement suffisant pour voir l’évolution de la puissance.

Le facteur de puissance négatif n’est pas très pertinent. Car celui-ci présente le rapport entre le courant actif et apparent.

On peut observer qu’il y a une différence entre la détermination le facteur de puissance qui est déterminé avec la puissance active en valeur absolue et l’apparente moyenne (courbe bleu) et le facteur de puissance déterminer avec le filtre passe bas en valeur absolue (courbe orange).

La dernière méthode est moins sensible lorsque la puissance est faible


Par conséquent, voici le nouveau programme avec filtre sur des datas puissances et buffer circulaire avec un enregistrement toutes les 1 minutes

 //TTGO dispaly  wifi server table buffer circulaire  .CSV
//envoie auto quand le buffer est plein
//filtre passe haut 2Hz retire offset, filtre pas bas 200hz retire le bruit, RMS, filtre passe bas
//filtrage enregistrement des données toutes les 1minutes

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <TFT_eSPI.h>

const char* ssid     = "Livebox";       // NOM WIFI
const char* password = "         "; // MOT DE PASSE

WebServer server(80);

TFT_eSPI tft = TFT_eSPI();

#define PIN_ZMPT       32       // Capteur tension
#define PIN_intensity  33       // SCT-013-050
#define PIN_LED        25       // Test acquisition

#define N_ECHANTILLONS 2500   // PARAMETRES ACQUISITION  
#define PERIODE_US     800    // 2500 x 800 us = 2 secondes

// 2500 x 800 us = 2 secondes
// Une mesure complète est donc calculée toutes les 2 secondes.

#define calibreV  0.482f    // CALIBRATION
#define calibreI  0.0385f/3 ////3 tours

const int TAILLE_TAMPON = 1440;   //1440*1minutes=24heures

float bufferTension[TAILLE_TAMPON];
float bufferCourant[TAILLE_TAMPON];
float bufferPuissance[TAILLE_TAMPON];
float bufferFP[TAILLE_TAMPON];

unsigned long bufferTemps[TAILLE_TAMPON];

int indexActuel = 0;
int nombreValeurs = 0;
unsigned long generationBuffer = 0;  //nouveau fichier
unsigned long tempsEnregistrement = 0;

hw_timer_t *timer = NULL;
TaskHandle_t mesureTaskHandle = NULL;


struct Mesure {  // STRUCTURE DES DONNEES PARTAGEES CORE 0 -> CORE 1
  float tensionRMS;
  float currentRMS;
  float Power;
  float FP;
};

volatile Mesure resultat;
volatile bool nouvelleMesure = false;

// Protection des données partagées
portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;

// ================================================================
// INTERRUPTION TIMER

void IRAM_ATTR timerISR()   {
  BaseType_t xHigherPriorityTaskWoken = pdFALSE;

  // Réveille la tâche de mesure sur Core 0
  vTaskNotifyGiveFromISR (mesureTaskHandle,   &xHigherPriorityTaskWoken  );

  if (xHigherPriorityTaskWoken)    { portYIELD_FROM_ISR(); }
}

// ================================================================
// CORE 0 : TACHE DE MESURE

void mesureCore0(void *parameter)   {
  const float alphaPB = 0.0016f;     // filtre passe bas pour la mesure de la puissance moyenne et RMS
  const float alphaLP = 0.6;         // 0.33 filtre low pas pour retirer le bruit au dessus de 100Hz     0.314  pour 75Hz
  const float alphaHP = 0.9901f;     //filtre passe haut pour retirer la composante continue

  float valeurADC  = 0.0f;     // TENSION
  float valeurADCn = 0.0f;

  float signalV,signalVn;
  float signalVf, signalVfn;
  float signalVf1, signalVf2; 

  float rmsV  = 0.0f;
  float rmsVn = 0.0f;

  float tensionRMS = 0.0f;

  float valeurI,valeurIn ;   // COURANT
  float signalI,signalIn;  
  float signalIf,signalIfn;
 
  float rmsI  = 0.0f;
  float rmsIn = 0.0f;

  float currentRMS = 0.0f;

  float Powerinstant = 0.0f;    // PUISSANCE
  float Powerf = 0.0f;
  float Powern = 0.0f;

  float Power = 0.0f;

  float S  = 0.0f;
  float FP = 0.0f;

  uint16_t echantillon = 0;      // COMPTEUR ECHANTILLONS

  // ==============================================================
  // BOUCLE DE MESURE
 
  while (true)     {      // Attente de l'interruption timer

    ulTaskNotifyTake( pdTRUE,  portMAX_DELAY  );
 //   digitalWrite(PIN_LED, !digitalRead(PIN_LED));   // LED de test

    valeurADC = analogRead(PIN_ZMPT);        // MESURE TENSION
    valeurI = analogRead(PIN_intensity);      // MESURE COURANT

    // Filtre passe-haut voltage
    signalV = alphaHP *(signalVn + valeurADC - valeurADCn);
    signalVn   = signalV;
    valeurADCn = valeurADC;

    signalVf2=signalVf1;  //retard 2 echantillon
    signalVf1=signalVf;   //retard 1 echantillon     14°
    signalVf = signalVfn + alphaLP * (signalV - signalVfn);   //filtre passe pas 100Hz
    signalVfn = signalVf;

    // RMS filtré
    rmsV = rmsVn +  alphaPB *(signalV * signalV - rmsVn);
    rmsVn = rmsV;
    tensionRMS = sqrt(rmsV) * calibreV;
    // ============================================================
    // Filtre passe-haut  COURANT
    signalI =alphaHP * (signalIn + valeurI - valeurIn);
    signalIn = signalI;
    valeurIn = valeurI;

    signalIf = signalIfn + alphaLP * (signalI - signalIfn);  //filtre passe pas 100Hz
    signalIfn = signalIf;

    // RMS filtré
    rmsI =rmsIn +alphaPB *(signalI * signalI - rmsIn);
    rmsIn = rmsI;
    currentRMS = sqrt(rmsI) * calibreI;

    Powerinstant = signalIf * signalVf2;     //decalage de 30°

    // Filtrage passe-bas de la puissance instantanée
    Powerf =Powern + alphaPB * (Powerinstant - Powern);
    Powern = Powerf;
    Power = Powerf * calibreV * calibreI;

    echantillon++;       // COMPTEUR

    // FIN DES 2500 ECHANTILLONS    2500 x 800 us = 2 secondes
    // ============================================================

    if (echantillon >= N_ECHANTILLONS)     {
      S = tensionRMS * currentRMS;

      if (S > 0.05f)   { FP = Power / S;         // Facteur de puissance
        if (FP > 1.0f)   FP = 1.0f;
        if (FP < -1.0f)  FP = -1.0f;
                      }   else   { FP = 0.0f;   }



      portENTER_CRITICAL(&mux);    // ENVOI CORE 0 -> CORE 1

      resultat.tensionRMS  = tensionRMS;
      resultat.currentRMS  = currentRMS;
      resultat.Power       = Power;
      resultat.FP          = FP;

      nouvelleMesure = true;

      portEXIT_CRITICAL(&mux);

      echantillon = 0;   // Remise à zéro du compteur
    }
  }
}

// ================================================================
void handleRoot() {  // PAGE WEB PRINCIPALE
  float V;
  float I;
  float P;
  float FP;
  float tempsbuffer;

  portENTER_CRITICAL(&mux);

  V  = resultat.tensionRMS;
  I  = resultat.currentRMS;
  P  = resultat.Power;
  FP = resultat.FP;

  portEXIT_CRITICAL(&mux);

  // PAGE HTML
  // --------------------------------------------------------------
  String page = "";

  page += "<!DOCTYPE html>";
  page += "<html>";
  page += "<head>";

  page += "<meta charset='UTF-8'>";
  page += "<meta name='viewport' content='width=device-width, initial-scale=1.0'>";

  // Actualisation toutes les 2 secondes
//  page += "<meta http-equiv='refresh' content='2'>";

  page += "<title>ESP32 - Mesure électrique</title>";

  page += "<style>";

  page += "body {";
  page += "font-family: Arial;";
  page += "margin: 20px;";
  page += "background-color: #eeeeee;";
  page += "}";

  page += "h1 {";
  page += "color: #333333;";
  page += "}";

  page += ".mesure {";
  page += "background-color: white;";
  page += "border-radius: 10px;";
  page += "padding: 15px;";
  page += "margin-bottom: 10px;";
  page += "font-size: 25px;";
  page += "font-weight: bold;";
  page += "}";

  page += ".tension { color: green; }";
  page += ".courant { color: red; }";
  page += ".puissance { color: blue; }";
  page += ".fp { color: purple; }";

  page += "button {";
  page += "font-size: 18px;";
  page += "padding: 10px 20px;";
  page += "}";

  page += "</style>";

  page += "</head>";
  page += "<body>";

  // TITRE
  // --------------------------------------------------------------
  page += "<h1>ESP32 - Mesure électrique</h1>";

  // TENSION
  // --------------------------------------------------------------

  page += "<div class='mesure tension'>";
page += "Tension RMS : ";
page += "<span id='tension'>";
page += String(V, 1);
page += "</span> V";
  page += "</div>";


  // COURANT
  // --------------------------------------------------------------

  page += "<div class='mesure courant'>";
page += "Courant RMS : ";
page += "<span id='courant'>";
page += String(I, 2);
page += "</span> A";
  page += "</div>";

  // PUISSANCE
  // --------------------------------------------------------------
  page += "<div class='mesure puissance'>";
page += "Puissance active : ";
page += "<span id='puissance'>";
page += String(P, 1);
page += "</span> W";
  page += "</div>";

  // FACTEUR DE PUISSANCE
  // --------------------------------------------------------------

  page += "<div class='mesure fp'>";
page += "Facteur de puissance : ";
page += "<span id='fp'>";
page += String(FP, 2);
page += "</span>";
  page += "</div>";

  // --------------------------------------------------------------
  // INFORMATIONS BUFFER
  // --------------------------------------------------------------
page += "<p>";
page += "Mesures enregistrees : ";

page += "<span id='progression'>";
page += String(nombreValeurs);
page += " / ";
page += String(TAILLE_TAMPON);
page += "</span>";

page += "</p>";

  // BOUTON CSV
  // --------------------------------------------------------------

  page += "<br>";
  page += "<a href='/download'>";
  page += "<button>";
  page += "Telecharger le fichier CSV";
  page += "</button>";
  page += "</a>";

  // --envoie des data automatique---------------------------------
page += "<script>";
page += "let derniereGeneration = -1;";
page += "async function actualiserMesures() {";
page += "try {";

page += "const reponse = await fetch('/data');";
page += "const data = await reponse.json();";

page += "document.getElementById('tension').innerText = data.V.toFixed(1);";
page += "document.getElementById('courant').innerText = data.I.toFixed(3);";
page += "document.getElementById('puissance').innerText = data.P.toFixed(1);";
page += "document.getElementById('fp').innerText = data.FP.toFixed(2);";

page += "} catch (erreur) {";
page += "console.log('Erreur mesures :', erreur);";
page += "}";
page += "}";
page += "async function surveillerBuffer() {";

page += "try {";

page += "const reponse = await fetch('/status');";
page += "const data = await reponse.json();";

page += "document.getElementById('progression').innerText = ";
page += "data.nombre + ' / ' + data.taille;";

page += "if (data.nombre >= data.taille && ";
page += "data.generation != derniereGeneration) {";

page += "derniereGeneration = data.generation;";

page += "console.log('BUFFER PLEIN - téléchargement automatique - génération : ' + data.generation);";

page += "const lien = document.createElement('a');";

page += "lien.href = '/download';";
page += "lien.download = 'mesures_2H.csv';";

page += "document.body.appendChild(lien);";
page += "lien.click();";
page += "document.body.removeChild(lien);";
page += "}";
page += "} catch (erreur) {";
page += "console.log('Erreur buffer :', erreur);";
page += "}";
page += "}";

page += "setInterval(actualiserMesures, 2000);";
page += "setInterval(surveillerBuffer, 2000);";

page += "actualiserMesures();";
page += "surveillerBuffer();";

page += "</script>";

page += "</body>";
page += "</html>";

  page += "</body>";
  page += "</html>";

    server.send( 200, "text/html",  page  );
}

void handleStatus() {

  String json = "";
  json += "{";

  json += "\"nombre\":";
  json += String(nombreValeurs);  

  json += ",";

  json += "\"taille\":";
  json += String(TAILLE_TAMPON);

  json += ",";

  json += "\"generation\":";
  json += String(generationBuffer);

  json += "}";
  server.send( 200, "application/json", json);
}

void handleData() {

  float V;
  float I;
  float P;
  float FP;

  portENTER_CRITICAL(&mux);

  V  = resultat.tensionRMS;
  I  = resultat.currentRMS;
  P  = resultat.Power;
  FP = resultat.FP;

  portEXIT_CRITICAL(&mux);

  String json = "{";

  json += "\"V\":";
  json += String(V, 1);

  json += ",\"I\":";
  json += String(I, 3);

  json += ",\"P\":";
  json += String(P, 1);

  json += ",\"FP\":";
  json += String(FP, 2);

  json += "}";

  server.send(200, "application/json", json);
}


// ================================================================
// TELECHARGEMENT CSV
// ================================================================
void handleDownload()  {
  String csv = "";

  csv += "Temps(s);Tension_RMS(V);Courant_RMS(A);Puissance_Active(W);FP\r\n";     // EN-TETE CSV

  if (nombreValeurs > 0)   {      // ORDRE CHRONOLOGIQUE
    int debut;

    if (nombreValeurs < TAILLE_TAMPON)   { debut = 0;  }
    else   { debut = indexActuel;   }

    // Parcours des données
    // ------------------------------------------------------------
    for (int i = 0; i < nombreValeurs; i++)   {
      int position =
        (debut + i) % TAILLE_TAMPON;

      csv += String(bufferTemps[position]); csv += ";";
      csv += String(bufferTension[position], 2);  csv += ";";
      csv += String(bufferCourant[position], 3);  csv += ";";
      csv += String(bufferPuissance[position], 2);  csv += ";";
      csv += String(bufferFP[position], 3);
      csv += "\r\n";
    }
  }   

  // NOM DU FICHIER
  // --------------------------------------------------------------
  server.sendHeader(  "Content-Disposition",  "attachment; filename=mesures_2H.csv"  );

// Remise à zéro uniquement de la progression
if (nombreValeurs >= TAILLE_TAMPON) {nombreValeurs = 0;}

server.send(200, "text/csv", csv);  // Envoi du CSV

}  //end handleDownload()



// ================================================================
void setup()  {
  Serial.begin(115200);

  analogReadResolution(12);
  analogSetAttenuation(ADC_11db);

  pinMode(PIN_ZMPT, INPUT);
  pinMode(PIN_intensity, INPUT);

  pinMode(PIN_LED, OUTPUT);
  digitalWrite(PIN_LED, LOW);

  tft.init();
  tft.setRotation(0);

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid,  password  );
  while (WiFi.status() != WL_CONNECTED)   { delay(500);  Serial.print(".");  }

server.on("/", handleRoot);
server.on("/download", handleDownload);
server.on("/status", handleStatus);
server.on("/data", handleData);

  server.begin();

  Serial.println("Serveur Web demarre");

  resultat.tensionRMS = 0;
  resultat.currentRMS = 0;
  resultat.Power      = 0;
  resultat.FP         = 0;

  nouvelleMesure = false;

  indexActuel = 0;
  nombreValeurs = 0;

  tempsEnregistrement = 0;
  generationBuffer = 0;

  // CREATION TACHE MESURE SUR CORE 0
  // ==============================================================

  xTaskCreatePinnedToCore(
    mesureCore0,          // fonction
    "MESURE",             // nom
    4096,                 // stack
    NULL,                 // parametre
    2,                    // priorité
    &mesureTaskHandle,    // handle
    0                     // CORE 0
  );

  uint8_t timer_id = 1;      // // 1 tick = 1 us
  uint16_t prescaler = 80;   //80 MHz / 80 = 1 MHz

  timer = timerBegin( timer_id, prescaler,  true  );
  timerAttachInterrupt( timer, &timerISR, true );
  timerAlarmWrite(  timer, PERIODE_US, true  );
  timerAlarmEnable(timer);


  tft.fillScreen(TFT_BLACK);
  tft.setCursor(0, 0, 2);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.println("MESURE ELECTRIQUE");

  Serial.println("Systeme demarre");
}


// ================================================================
float V  = 0;   // VARIABLES CORE 1
float I  = 0;
float P  = 0;
float FP = 0;
float Econso, joule;
float Einjec, jouleinjec;

const float alphaPB10= 0.02073;       //fitre passe bas 10minutes
float Imoy,Imoyn ;

float Pmoy,Pmoyn; 
float FPmoy,FPmoyn;
bool mesure1minutes;
int  index1minutes;

void loop()   {    // LOOP CORE 1

  server.handleClient();     // SERVEUR WEB   1ms   

if (mesure1minutes==true)  {     // ENREGISTREMENT DANS LE BUFFER CIRCULAIRE 1minutes
mesure1minutes=false;

    bufferTemps[indexActuel] =tempsEnregistrement;
    bufferTension[indexActuel] = V;          
    bufferCourant[indexActuel] = Imoy;
    bufferPuissance[indexActuel] = Pmoy;
    bufferFP[indexActuel] =   FPmoy;
    tempsEnregistrement += 1;
    indexActuel++;

    if (indexActuel >= TAILLE_TAMPON)   { indexActuel = 0; generationBuffer++;   }       // Avancement buffer circulaire
    if (nombreValeurs < TAILLE_TAMPON)    {nombreValeurs++;   }      // Nombre de valeurs enregistrées

}



  if (nouvelleMesure == true)    {     //echantillon tous les 2 secondes
 //   digitalWrite(25,1);      //durée 5ms
    portENTER_CRITICAL(&mux);      // Lecture protégée des données
    V  = resultat.tensionRMS;
    I  = resultat.currentRMS;
    P  = resultat.Power;
    FP = resultat.FP;
    nouvelleMesure = false;
    portEXIT_CRITICAL(&mux);

    // FILTRE PASSE-BAS 10 minutes
    // ==========================================
    Imoy = Imoyn +  alphaPB10 *(I - Imoyn);   //filtre passe bas 10minutes
    Imoyn = Imoy;

    Pmoy = Pmoyn +  alphaPB10 *(P - Pmoyn);
    Pmoyn = Pmoy;

    FPmoy = FPmoyn +  alphaPB10 *(abs(FP) - FPmoyn);
    FPmoyn = FPmoy;


    index1minutes++;
     if (index1minutes >= 30)   { mesure1minutes =true ;index1minutes = 0;}  


    tft.setCursor(0, 25, 4);

    tft.setTextColor( TFT_GREEN,  TFT_BLACK );
    tft.print("V: ");        // TENSION
    tft.print(V, 1);
    tft.print(" V");

    tft.setCursor(0, 55, 4);
    tft.setTextColor( TFT_RED,  TFT_BLACK );
    tft.print("I: ");
    tft.print(I, 2);       // COURANT
    tft.print(" A ");

    tft.setCursor(0, 85, 4);
    tft.setTextColor( TFT_BLUE, TFT_BLACK );
    tft.print("P:");
    tft.print(P, 0);        // PUISSANCE ACTIVE
    tft.print("   ");

    tft.setCursor(0, 115, 4);
    tft.setTextColor( TFT_WHITE,  TFT_BLACK   );
    tft.print("FP:");       // FACTEUR DE PUISSANCE
    tft.print(FP, 2);
    tft.print("  ");

if (P>=0)   {  joule=joule+P*2;
               Econso=joule/3600000; }    //puissance consommée kwh 

 if (P<0)  {  jouleinjec=jouleinjec+P*2;  
              Einjec=jouleinjec/3600000;    }    //puissance injectée 
 
    tft.setCursor(0, 145, 4);
    tft.setTextColor( TFT_WHITE,  TFT_BLACK   );
    tft.print("Ec:");       // FACTEUR DE PUISSANCE
    tft.print(Econso,0);
    tft.print("  ");

    tft.setCursor(0, 175, 4);
    tft.setTextColor( TFT_WHITE,  TFT_BLACK   );
    tft.print("Ei:");       // FACTEUR DE PUISSANCE
    tft.print(Einjec,0);
    tft.print("  ");

//  digitalWrite(25,0); 
  }  //2s

  delay(5);     // Petite pause pour laisser le serveur WiFi respirer
}  //end loop
 

Est-ce que le compteur Linky aurait du donner ces informations ?

Est-ce que les usagers auraient compris ces données ?

Les usagers comprennent tout de suite les données de la production photovoltaique des applications de leurs onduleurs donc ils auraient compris les infos de leurs consos

Dans ce programme, il manque la valeur max de la puissance apparente par jour, pour indiquer une disjonction par rapport à la puissance souscrite. Mais l’abonnement ne devrait plus exister depuis longtemps !

Etant donné que les données sont enregistrées dans des tables du TTGO, l’écran du PC peut être éteint avec le navigateur qui fonctionne pour récupérer le fichier .CSV de façon automatique tous les heures ou à tout moment.

Voici les données de puissance active et de son facteur d’une journée de ma maison avec du photovoltaïque d’où la valeur négative de la puissance….et les pics de puissance sont filtrées mais visible quand même malgré la mémorisation des échantillons toutes les minutes. On pourrait même faire toutes les 2 minutes.


On peut observer la mesure du facteur de puissance ou la précision n’est pas pertinente lorsque la puissance active est faible

1 Like