Niveau d'eau dans la citerne

Voici une petite réalisation destinée à répondre à la question : Combien d’eau reste-t-il dans ma citerne à eau de pluie ?

Le printemps étant là, et les mois plus secs vont bientôt arriver et la question risque de se poser à nouveau. Et comme la cuve est enterrée, ce n’est pas tous les jours que l’on va soulever le couvercle en béton pour sonder.


Plusieurs solutions

J’ai envisagé plusieurs solutions :

  • Utiliser une série de contacts, espacés verticalement, et commandés par un flotteur => Pas forcément très précis.
  • Utiliser un détecteur de distance ultrasonore pour mesurer la distance de la flottaison par rapport au haut de la cuve.
  • Mesurer la pression au fond de la cuve.

Ensuite, l’environnement à l’intérieur de la citerne présente une très forte humidité. Cet environnement est, le moins que l’on puisse dire, hostile à tout ce qui est électricité et électronique. Une solution “sans fils” serait donc souhaitable. Ici, par “sans fils”, il faut comprendre sans fils électrique ni composants électroniques. On oublie donc les 2 premières solutions, reste à trouver le moyen de mesurer une pression sans mettre le capteur sous l’eau.


La solution est dans l’air ;-)

La solution théorique retenue consiste à souffler de l’air dans un tuyau qui descend jusqu’au fond de la cuve et à mesurer la pression dans ce tuyau. A partir du moment où l’air va s’échapper du tuyau, la pression dans le tuyau ne va plus augmenter. Cette solution, je ne l’ai pas inventée, elle est présentée sur le site arduino.cc. Par contre, sa mise en œuvre m’a posé quelques problèmes.

La pompe

Ne sachant pas initialement où j’allais l’installer, je voulais un modèle basse tension, si possible 5 Volt, comme l’Arduino, sinon, 12 Volt. Ne trouvant aucun modèle 5 Volt, j’ai dû essayer plusieurs modèles avant de trouver une pompe capable de souffler de l’air sous au moins 2 mètres d’eau. La première à fonctionner correctement (photo ci-dessus) demandait beaucoup de puissance, était très bruyante et vibrait énormément. Elle fonctionne bien avec une batterie automobile, mais ce genre de batterie s’intègre très mal dans un tableau électrique. Finalement, j’ai trouvé et conservé celle-ci. Petite et pas trop bruyante.

Lire la pression

Le capteur utilisé est le Freescale MPX5050DP qui permet de mesurer jusqu’à des pressions équivalentes à une hauteur d’eau de 5 mètres. Il fonctionne à merveille. Par contre, le placer près de la pompe n’est vraiment pas une bonne idée, surtout avec la première pompe utilisée, car des ondes de pression sont générées par la pompe, ce qui donne des valeurs très erratiques. Positionner le capteur près de la cuve semblait résoudre le problème. En partie seulement, car cette fois ci c’est l’échappement des bulles qui perturbe la lecture. Mais ayant décidé de ne pas mettre d’électronique dehors, j’ai dû me résoudre à utiliser un second tuyau. Un tuyau pour l’aller, et un autre pour le retour. Et là, vu la longueur du tuyau (40 mètres aller + 40 mètres retour) les parasites ont pratiquement disparus, amortis par la distance !


Les composants nécessaires

L’ensemble a été installé dans une petite armoire électrique d’une rangée de 13 modules. Et toute la rangée va être utilisée.

Liste des composants utilisés, de gauche à droite :

  • Un disjoncteur comme celui-ci qui fait principalement office de coupe circuit.
  • Une alimentation 5 Volt pour alimenter l’Arduino et le relai. Disponible également ici.
  • Une carte Arduino Uno comme celle-ci ou celle-la.
  • Un shield Ethernet comme celui-ci ou celui-la.
  • Un relai pour déclencher la pompe.
  • Une alimentation 12 Volt pour alimenter la pompe à air. Disponible également ici.
  • Une pompe à air pas trop bruyante et pas trop gourmande en énergie.
  • Un capteur de pression Freescale MPX5050DP.
  • Une LED, dont la fréquence de clignotement donnera une indication sur le fonctionnement de l'ensemble.
  • Un petit écran monochrome oled affichant 128x64 pixels de 0.96" de diagonale via une interface i2c. C'est petit mais suffisant !
  • Un coffret électrique 13 modules de ce type pour emballer le tout.
  • Deux fois 50 mètres de tuyau 4 mm pour faire l’aller et le retour. Il existe en 15 m et 25 m, mais j’avais besoin d’au moins 30 mètres. De plus, la longueur facilite la mesure.
  • Des pièces imprimées en 3D qui servent de support à différents composants pour les fixer sur le rail DIN.

L’ensemble revient quand même à prêt de 110 €.


Le plan de montage

  • Le secteur alimente le disjoncteur qui sert en fait de coupe circuit général.
  • Le disjoncteur alimente l’alimentation 5 Volt, ainsi que l’alimentation 12 Volt, par l’intermédiaire du relai.
  • L’Arduino coordonne l’ensemble des opérations:
    • Commander le relai pour alimenter la pompe.
    • Lire la mesure de la pression.
    • Envoyer la mesure réalisée vers un serveur de datalogging.
    • Afficher la dernière mesure ou les opérations en cours.
    • Interroger un serveur NTP pour savoir quand effectuer les mesures.
  • Le relai est commandé par l’Arduino, pour mettre l’alimentation 12 Volt sous tension afin de faire fonctionner la pompe à air
  • L’alimentation 12 Volt pour la pompe à air.
  • Il y a donc 2 tuyaux qui vont jusqu’à la citerne:
    • Un pour l’aller qui part de la pompe à air jusqu’à l’entrée de la citerne. Il sert à mettre l’ensemble en pression au moment de la mesure.
    • Un pour le retour, qui va de l’entrée de la citerne au capteur de pression, et qui va transmettre la pression en atténuant toutes les ondes parasites.
  • Enfin, un T permet de relier le tuyau “aller”, au tuyau “retour”, ainsi qu’au tuyau qui plonge au fond de la citerne. Pour maintenir l’extrémité du tuyau au fond de la citerne, j’ai percé un petit bloc de schiste de plusieurs trous de 6 mm dans lesquels j’ai fait passer le tuyau en zigzag.

Les pièces 3D

Il y a 6 pièces imprimée en 3D afin de peaufiner et de fixer l’ensemble. Ces pièces sont disponibles sur GitHub

  • Support de la carte Arduino.
  • Support du relai.
  • Support de la pompe à air
  • Support du capteur de pression
  • Cache transparent qui recouvre l’Arduino et qui permet de voir les LED
  • Cache opaque à placer devant la pompe et le capteur de pression, mais avec une fenêtre et un support pour y placer l'écran d'affichage.

Support de l’Arduino

Cette pièce et son utilisation sont décrites ici. Le fichier source est disponible ici.

Support du relai

Le fichier source est disponible ici. Il est conçu pour qu'on puisse y glisser le relai, et que les borniers restent accessibles.

Support de la pompe à air

Le fichier source est disponible ici. La pompe est fixée avec 2 petits colliers de serrage qui sont passées dans des tunnels prévus à cet effet.

Support du capteur de pression

Le fichier source est disponible ici. Le capteur est fixé avec 2 vis M4. Un emplacement pour les écrous est prévu à l'arrière.

Cache transparent

Il recouvre l’Arduino et qui permet de voir les LED. Le fichier source est disponible ici. Le fichier .scad est également disponible. Cela peut permettre d’imprimer des caches de différentes tailles.

Cache opaque

Il recouvre la pompe et le capteur de pression et sert de support à l'écran d'affichage. L'écran est simplement clipser au cache grâce à 4 cylindres prévus à l'emplacement des trous de fixation. Le fichier source est disponible ici. Le fichier .scad est également disponible.


Mesurer la pression

Pour prendre la mesure de la pression, lire juste une fois la tension aux bornes du capteur ne suffit pas. La précision n’est pas au rendez-vous. Pour améliorer la mesure, la solution adoptée est la classique série de mesure, dont on ne garde que la moyenne. Ensuite, quand faut-il mesurer ? En effet, lorsque la pompe se met en marche, on constate une augmentation de la pression. Puis, en fonction de la hauteur d’eau, la pression diminue, jusqu’à se stabiliser.


Afficher le taux de remplissage

Plutôt que d'afficher la hauteur d'eau mesurée, qui ne signifie pas grans chose en elle-même, j'ai préféré afficher le taux de remplissage, sous la forme d'un pourcentage, allant d'une valeur négative à 100%. Ce pourcentage est affiché en gros, visible de loin. Mais en s'approchant, il est quand même possible de lire la hauteur d'eau.

Pour gérer l'affichage, j'utilise la librairie U8glib. Celle-ci est disponible ici.


Câblage

Et voici le schéma de câblage général.

L’ensemble est isolable du secteur 220 Volt AC par un disjoncteur coupe-circuit. Le 220 volt alimente un bloc d’alimentation 5 Volt, ainsi qu’un bloc d’alimentation 12 Volt via un relai commandé par la broche 2 de l’Arduino.

Le bloc d’alimentation 5 Volt alimente directement :

  • L’Arduino via ses broches +5v et GND.
  • Le relai, par ailleurs commandé par la broche 2 de l’Arduino
  • Le capteur de pression, dont la mesure est lue par la broche A0 de l’Arduino

La pompe à air est directement alimentée par le bloc d’alimentation 12 Volt Une LED est commandée par la broche 5. L'afficheur est alimentée par la broche 3.3V de l'Arduino


Le programme

Voici le code, expurgé des commentaires du début.

L’intégralité est disponible à la fin de cet article sous forme de fichier archive téléchargeable. Egalement disponible sur GitHub.

Ce programme effectue deux mesures par jour, entre 00h00 et 00h05, ainsi que entre 12h00 et 12h05. Afin de connaitre l’heure, un server NTP est interrogé régulièrement. Il est aussi possible de provoquer une mesure à la demande, grâce à l’ordre ReadWaterLevel. 2 autres codes sont aussi implémentés : AbortProcess pour stopper un processus de lecture et Reset pour réinitialiser la carte Arduino.

Toutes les mesures sont envoyées vers le serveur de datalogging.

Les broches suivantes sont utilisées :

  • A0: Capteur de pression
  • 2: Relais alimentation électrique de la pompe
  • 4: carte Ethernet - SS for SD card
  • 5: LED
  • 10: carte Ethernet - SS for ethernet controller / LCD - Backlit control (broche pliée)
  • 11: carte Ethernet - SPI bus : MOSI
  • 12: carte Ethernet - SPI bus : MISO
  • 13: carte Ethernet - SPI bus : SCK

Le programme:

Attention Avec l'utilisation des polices, le croquis dépasse la taille maximale s'il est généré avec une version récente du logiciel Arduino. Avec la version 1.6.5, la taille du croquis est de 32 118 octets. Pour rappel, le maximum est de 32 256 octets.

/*************************************************************
*   Includes
*/

#include <avr/wdt.h>               // Pour le WatchDog
#include <SPI.h>                   // Pour la carte Ethernet
#include <Ethernet.h>              // Pour la carte Ethernet
#include <EthernetServer.h>        // Pour la communication Ethernet
#include <EthernetClient.h>        // Pour la communication Ethernet
#include <EthernetUdp.h>           // Pour le protocole NTP qui permet de récupérer l'heure
#include <U8glib.h>                // Pour l'afficheur oLed

/*************************************************************
*   Defines & constantes
*/
const int printPause = 50;           // Pause après envoi sur le terminal série, pour laisser le temps au destinataire de traiter l'information
const int pinRelayPump = 2;          // Broche du relai d'alimentation électrique de la pompe à air
const int pinPressure = A0;          // Broche reliée au capteur de pression
const int pinInternalLED = 13;       // Led interne sur la carte Arduino - Ne pas utiliser avec le shield ethernet
const int pinLED = 5;                // Led du montage 

const int relayOn = 0;
const int relayOff = 255;

const int httpMaxChar = 25;          // Nombre de caractère maxi à traiter
//const long millisDay = 86400000L;    // Nombre de millisecondes dans une journée
const int nbConsReads = 5;           // Nombre de mesures consécutives à observer pour obtenir un résultat stable
const int nLoop = 100;               // Nombre de lectures à effectuer pour avoir une mesure
const int maxCycles = 100;           // Nombre de cycle maxi avant mise en défaut
const int ntpEveryMinutes = 10;      // Demande NTP (heure) toutes les 10 minutes
const unsigned long seventyYears = 2208988800UL;
                                     // Unix time starts on Jan 1 1970. In seconds, that's 2208988800:

// Déclaration des états
const int statusUnknown = 0;         // Etat inconnu ou indéfini
const int statusWaiting = 1;         // En attende de déclencher une lecture de pression
const int statusOperatingPhase1 = 2; // Une lecture de pression est en cours, en phase 1
const int statusOperatingPhase2 = 3; // Une lecture de pression est en cours, en phase 2
const int statusDefault = 99;        // Mise en défaut car hauteur d'eau non lue correctement

const int ntpPacketSize = 48;        // NTP time stamp is in the first 48 bytes of the message

/*************************************************************
*   Déclaration des variables globales
*/

int loopStatus = statusUnknown;      // Etat en cours
int loopLastStatus = statusUnknown;  // Etat précédent
int ledStatus = LOW;

float waterLevel = 0;
float reads[nbConsReads-1];          // Enregistrement des lectures successives
int cycles = 0;                      // Nombre de cycle depuis le démarrage de la pompe

// Déclaration relatives à la config réseau
byte netMac[6] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06};  // Adresse MAC de la carte ethernet arduino
byte netIp[4] = { 192, 168, 1, 2 };                      // Adresse IP de la carte Arduino
byte netGateway[4] = { 192, 168, 1, 200 };               // Adresse IP de la passerelle
byte netMask[4] = { 255, 255, 255, 0 };                  // Masque sous-réseau
byte sqlServer[4] = { 192, 168, 1, 1 };                  // Adresse IP vers laquelle envoyer les données
unsigned int sqlPort = 8080;                             // Port HTTP sur lequel écoute le server SQL
unsigned int udpPort = 8888;                             // Port pour écouter les packets UDP
char timeServer[] = "time.nist.gov"; // time.nist.gov NTP server
//char timeServer[] = "192.168.2.200"; // server sur le réseau local

byte packetBuffer[ ntpPacketSize];   // buffer to hold incoming and outgoing packets
unsigned long nextNtp;               // millis() à atteindre pour demander l'heure                   
int ntpHour = -1;
int ntpMinute = -1;
int ntpSecond = -1;

/*************************************************************
*   Déclaration des objets
*/

EthernetServer httpServer(80);       // on crée un objet serveur utilisant le port 80 (port HTTP par défaut)
EthernetClient sqlClient;            // objet client pour envoyer des données
EthernetUDP udp;

//U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_NONE);              // Afficheur sur broches SCL & SDA
U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_NO_ACK);            // Afficheur sur broches A4 & A5
const u8g_fntpgm_uint8_t *smallFont = u8g_font_courR10r;  // Petite police
const u8g_fntpgm_uint8_t *hugeFont = u8g_font_fub49n;     // Police géante

/**************************************************************************
**
**  setup()  ==>  Fonction obligatoire d'initialisation
**
**************************************************************************/

void setup() {

  // Initialisation du WatchDog
  wdt_enable(WDTO_8S);

  // Initialisation de la communication série
  Serial.begin(115200);  
  Serial.println("Starting V 1.0 ...");

  // Test de l'afficheur
  oledDrawText("Setup");
  delay(printPause);

  // Déclarer les broches comme "output" ou "input"
  pinMode(pinLED, OUTPUT); 
  pinMode(pinRelayPump, OUTPUT); 
  pinMode(pinPressure, INPUT);

  // Initialisation des broches pour que les relais ne soient pas actifs au reset
  digitalWrite(pinRelayPump, relayOff);

  Serial.println("Pins initialized.");
  delay(printPause);

  // Initialisation de la connexion Ethernet avec l'adresse MAC, l'adresse IP et le masque
  Ethernet.begin(netMac, netIp, netGateway, netMask);
  Serial.print("Ethernet initialized on IP ");
  for(int i = 0; i < 4; i++) {
    if (i > 0) {
      Serial.print(".");
    }
    Serial.print(netIp[i]);
  }
  udp.begin(udpPort);
  Serial.println("");
  delay(printPause);

  // Initialisation des variables
  for(int i=0; i<nbConsReads; i++) {
    reads[i] = NULL;
  }
  //getNtpTime();
  nextNtp = 0;

  // Initialisation du serveur interne, et commence à écouter les clients
  httpServer.begin();   

  flushSerial();   // Pour vider le port série des ordres ventuellement arriv lors de l'initialisation
  loopStatus = statusWaiting;
  Serial.println("Setup finish.");
  delay(printPause);

}

/**************************************************************************
**
**  loop()  ==>  Fonction obligatoire. Boucle infinie de fonctionnement
**
**************************************************************************/

void loop() {

  // Reset du watchDog : C'est reparti pour 8 secondes
  wdt_reset();

  // Lire les éventuelles commandes reçues sur les ports série ou réseau et y répondre
  serialCommandProcess();     
  httpCommandProcess();

  // Gérer la mesure de pression
  if (isOperating()) {
    oledDrawText("Operate");
    float p = pressureRead(pinPressure);
    addPressure(p);
    Serial.print(p, 2);
    Serial.println(" kPa");
    delay(printPause);
    int e = pressureCheckEvolution();
    if (e > 0) {
      // La pile est pleine, la phase 1 a bien commencée
      if ( (loopStatus == statusOperatingPhase1) && (e == 2) ) { 
        // Phase 1 ET que des diminutions
        loopStatusChange(statusOperatingPhase2);
      }
      if ( (loopStatus == statusOperatingPhase2) && (e == 1) ) {
        // Phase 2 ET une augmentation : On a trouvé une valeur
        airPumpStop(statusWaiting);
      }
    }
    cycles++;
    if (cycles >= maxCycles) {
      airPumpStop(statusDefault);
    }
  }

  // Gérer le temps - Toutes les 10 minutes environ, récupérer l'heure
  if ( (millis() > nextNtp) && !isOperating() ) {
    getNtpTime();
    nextNtp = calcNextNtp(nextNtp);
    if (checkTime()) {
      airPumpStart();
    }
  }

  // Faire clignoter la LED, en fonction de loopStatus
  toggleLedStatus();
  // Si pas en cours de lecture, faire une longue pause
  if (!isOperating()) {
    // Si en défaut, quelques éclats supplémentaires
    if (loopStatus == statusDefault) {
      oledDrawText("Default");
      for (int i=0; i<7; i++) {
        toggleLedStatus();
        delay(100);
      }
      delay(800);
    }
    else {
      delay(1500);
    }
  }
}

/**************************************************************************
**
**  Fonctions d'affichage oLed et calcul du pourcentage
**
**************************************************************************/

// A partir du niveau d'eau en mm, calcul le pourcentage de remplissage de la cuve
float perCentFill() {
  // 1600 mm ou plus => 100% (plein)
  // 200 mm ou moins => 0%  (vide)
  const float fillMax = 1600;
  const float fillMin = 200;
  return( (waterLevel - fillMin) / (fillMax - fillMin) * 100 );
}

// Affiche le texte passé en paramètre
void oledDrawText(String label) {
  u8g.firstPage();  
  do {
    u8g.setFont(smallFont);
    u8g.setFontPosTop();
    u8g.setScale2x2();
    u8g.drawStr( 0, 10, label.c_str());
    u8g.undoScale();
  } while( u8g.nextPage() );
}

// Affiche un pourcentage en gros
void oledDrawPCentLevel(float pc, float level) {
  int top = 0;
  char str[10];
  u8g.firstPage();  
  do {

    u8g.setColorIndex(1);
    u8g.drawBox(0, 0, 127, 63);
    u8g.setColorIndex(0);

    u8g.setFont(hugeFont); // OK
    u8g.setFontPosTop();
    // Cas du chiffre 100
    if (pc > 99.5) {
      u8g.drawStr(-7, top, "1"); 
      u8g.drawStr(24, top, "00"); 
    }
    else {
      if (pc > 9.5) {
        u8g.drawStr(20, top, dtostrf(pc, 2, 0, str));
      }
      else {
        if (pc < 0) {
          if (pc <= -9.5) {
            u8g.drawStr(0, top, dtostrf(pc, 3, 0, str));
          }
          else {
            u8g.drawStr(37, top, dtostrf(pc, 3, 0, str));
          }
        }
        else {
          u8g.drawStr(52, top, dtostrf(pc, 2, 0, str));
        }
      }
    }
    // Le pourcentage
    int xe = 100;
    u8g.drawStr(xe, top, "/"); 
    u8g.drawFilledEllipse(xe+8, top+6, 6, 8, U8G_DRAW_ALL);
    u8g.drawFilledEllipse(xe+20, top+45, 6, 8, U8G_DRAW_ALL);

    u8g.setFont(smallFont);
    u8g.setFontPosTop();
    //u8g.drawStr(104, top, "0"); 
    //u8g.drawStr(119, top + 42, "0"); 

    // le niveau
    int top2 = 52;
    u8g.drawStr(50, top2, dtostrf((int)level, 4, 0, str));
    u8g.drawStr(90, top2, "mm");

    u8g.setColorIndex(1);
    u8g.drawFilledEllipse(xe+8, top+6, 2, 3, U8G_DRAW_ALL);
    u8g.drawFilledEllipse(xe+20, top+45, 2, 3, U8G_DRAW_ALL);

  } while( u8g.nextPage() );
}

/**************************************************************************
**
**  Fonctions diverses
**
**************************************************************************/

// Vérifie si c'est l'heure de lire la hauteur d'eau
boolean checkTime() {
  // Entre respectivement 11h00 et 23h00
  if ( (ntpHour == 11) || (ntpHour == 23) ) {
    // et 11h10 et 23h10
    if (ntpMinute < ntpEveryMinutes) {
      return(true);
    }
  }
  return(false);
}

// Change l'état de la LED
void toggleLedStatus(void) {
  if (ledStatus == LOW) {
    ledStatus = HIGH;
  }
  else {
    ledStatus = LOW;
  }
  digitalWrite(pinLED, ledStatus);
}

// Vérifie si c'est une phase d'activité 
boolean isOperating() {
  return ( (loopStatus == statusOperatingPhase1) || (loopStatus == statusOperatingPhase2) );
}

// Changer le status courant
void loopStatusChange(int newStatus) {
  loopLastStatus = loopStatus;
  loopStatus = newStatus;
  Serial.print("loopStatus=");
  Serial.println(textStatus(loopStatus));
}

// Envoi des données vers le server de dataLogging
void ethernetSendData() {
  // Envoyer le rapport vers le server
  if (sqlClient.connect(sqlServer, sqlPort)) {
    sqlClient.print("GET /data/");
    sqlClient.print("WaterTank");
    sqlClient.print("/status=");
    sqlClient.print(textStatus(loopStatus));
    sqlClient.print("&level=");
    sqlClient.print(waterLevel, 0);
    sqlClient.println(" HTTP/1.0");
    sqlClient.println();
    delay(2);
    sqlClient.stop();
  }
}

// Envoie des données de lecture sur le port série
void serialSendData(void) {
  Serial.print("{\"status\":\"");
  Serial.print(textStatus(loopStatus));
  Serial.print("\"");
  jsonPrint("level", waterLevel, 0);
  Serial.println("}");
}

// Chaine du statut
String textStatus(int currentStatus) {
  if (currentStatus == statusWaiting) {
    return("Waiting");
  }
  else if (currentStatus == statusOperatingPhase1) {
    return("OperatingPhase1");
  }
  else if (currentStatus == statusOperatingPhase2) {
    return("OperatingPhase2");
  }
  else if (currentStatus == statusDefault) {
    return("Default");
  }
  return("Unknown");
}

//  Ajout d'une valeur au document jSon, après vérification
void jsonPrint(String label, float val, int precis) {
  if ( !isinf(val) && !isnan(val) && (val <= 4294967040.0) && (val >= -4294967040.0) ) {
    Serial.print(",\"");
    Serial.print(label);
    Serial.print("\":");
    Serial.print(val, precis);
  }
}

/**************************************************************************
**
**  Gestion des commandes reçues
**
**************************************************************************/

// Lire les éventuelles commandes reçues sur le port série et y répondre
void serialCommandProcess(void) {
  String commandText = String("");
  char byteInSerial;
  while (Serial.available() > 0) {
    byteInSerial = Serial.read();
    if ( (byteInSerial == 10) || (byteInSerial == 13) ) {
      //Serial.println("*");
      commandProcess(commandText);
    }
    else {
      //Serial.println("+");
      commandText += String(byteInSerial);
    }
  }
  if (commandText.length() > 0) {
    //Serial.println("-");
    commandProcess(commandText);
  }
}  

void commandProcess(String commandTxt) {
  Serial.println("commandProcess");
  // Test les différentes commandes possibles
  if (commandTxt.startsWith("Reset")) {
    commandReset();
  }
  if (commandTxt.startsWith("ReadWaterLevel")) {
    commandInitOperate();
  }
  if (commandTxt.startsWith("AbortProcess")) {
    commandAbortProcess();
  }
  else {
    commandUnknown(commandTxt);
  }
}

// Commande de reset
void commandReset() { 
  Serial.println("commandReset");
  delay(printPause);
  // Demander au watchdog de ne pas attendre plus que 15 millisecondes avant d'agir
  wdt_enable(WDTO_15MS);
  // Demander une longue attente pour déclencher le watchdog
  delay(10000);
}

// Commande d'initialisation de la lecture du niveau d'eau dans la cuve
void commandInitOperate(void) { 
  Serial.println("commandInitOperate");
  delay(printPause);
  airPumpStart();
}

// Commande AbortProcess
void commandAbortProcess(void) {
  Serial.println("commandAbortProcess");
  delay(printPause);
  digitalWrite(pinRelayPump, relayOff);
  loopStatusChange(statusWaiting);
}

// Commande inconnue !
void commandUnknown(String commandTxt) { 
  Serial.print("commandUnknown:");
  Serial.println(commandTxt);
  delay(printPause);
}

// Démarrage de la pompe à air - Démarrage du processus de lecture du niveau d'eau
void airPumpStart(void) {
  digitalWrite(pinRelayPump, relayOn);
  loopStatusChange(statusOperatingPhase1);
  cycles = 0;
}

// Arret de la pompe à air - Fin du processus de lecture du niveau d'eau
// Envoi des données de lecture
void airPumpStop(int newStatus) {
  waterLevel = pressureKpaToMm(reads[nbConsReads-1]);
  digitalWrite(pinRelayPump, relayOff);
  loopStatusChange(newStatus);
  serialSendData();
  oledDrawPCentLevel(perCentFill(), waterLevel);
  ethernetSendData();
}

// Pour vider le port série
void flushSerial(void) {
  while(1) {
    if (Serial.available() == 0)  {
      break;
    }
    else {
       Serial.read();
    }
  }
}

/**************************************************************************
**
**  Gestion du réseau
**
**************************************************************************/

// Lire les éventuelles commandes reçues sur le port ethernet et y répondre
void httpCommandProcess(void) {
  // Exemple d'envoi de commande : http://192.168.1.2/ReadWaterLevel
  // Déclaration des variables
  String chaineRecue = ""; // Pour y mettre la chaine reçue
  int comptChar = 0;       // Pour compter les caractères reçus

  // crée un objet client basé sur le client connecté au serveur
  EthernetClient httpClient = httpServer.available();
  if (httpClient) { // si l'objet client n'est pas vide
    // Initialisation des variables utilisées pour l'échange serveur/client
    chaineRecue = "";        // Vide le String de reception
    comptChar = 0;           // Compteur de caractères en réception à 0  
    if (httpClient.connected()) {       // Si que le client est connecté
      while (httpClient.available()) {  // Tant que des octets sont disponibles en lecture
        char c = httpClient.read();     // Lit l'octet suivant reçu du client (pour vider le buffer au fur à mesure !)
        comptChar++;                    // Incrémente le compteur de caractère reçus
        if (comptChar <= httpMaxChar) { // Les 25 premiers caractères sont suffisants pour analyses la requête
          chaineRecue += c;             // Ajoute le caractère reçu au String pour les N premiers caractères
        }
      } 
      // Si la chaine recue fait au moins 25 caractères ET qu'elle commence par "GET"
      if ( (chaineRecue.length() >= httpMaxChar) && (chaineRecue.startsWith("GET")) ) { 
        // Extrait à partir du 6ème caractère
        httpSend(httpClient,"OK");
        commandProcess(chaineRecue.substring(5));
      }
    } // Fin while client connected
    delay(10);           // On donne au navigateur le temps de recevoir les données
    httpClient.stop();   // Fermeture de la connexion avec le client
  }
}  

void httpSend(EthernetClient httpClient, String txt) {
  // Envoi d'une entete standard de réponse http
  httpClient.print("HTTP/1.1 20");
  if (txt.length() == 0) {
    //httpClient.println("HTTP/1.1 204 OK");
    httpClient.print("4");
  }
  else {
    //httpClient.println("HTTP/1.1 200 OK");
    httpClient.print("0");
  }
  httpClient.println(" OK");
  //httpClient.println("HTTP/1.1 200 OK");
  httpClient.println("Content-Type: text/html");
  httpClient.println("Connection: close");
  httpClient.println();
  // Affiche chaines caractères simples
  if (txt.length() > 0) {
    httpClient.println(txt);
  }
  delay(200);
}

// Envoi une demande au server NTP dont l'adresse est en paramètre
unsigned long sendNtpPacket(char* address) {
  // Initialise le buffer à 0 par défaut
  memset(packetBuffer, 0, ntpPacketSize);
  // Initialisée la demande NTP
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;            // Stratum, or type of clock
  packetBuffer[2] = 6;            // Polling Interval
  packetBuffer[3] = 0xEC;         // Peer Clock Precision
  // 8 bytes of zero for Root Delay & Root Dispersion
  packetBuffer[12]  = 49;
  packetBuffer[13]  = 0x4E;
  packetBuffer[14]  = 49;
  packetBuffer[15]  = 52;

  // Envoi du paquet IP de demande NTP
  udp.beginPacket(address, 123);  // Les requetes NTP se font sur le port 123
  udp.write(packetBuffer, ntpPacketSize);
  udp.endPacket();
}

void getNtpTime(void) {
  // Envoyer un packet Ntp au server
  sendNtpPacket(timeServer);
  // attendre la réponse du server NTP. 100 ms pour un server local est suffisant. Prévoir plus pour un server Internet
  delay(100);
  if (udp.parsePacket()) {
    // Le server NTP a répondu. 
    // Lire lire données reçues dans le buffer
    udp.read(packetBuffer, ntpPacketSize); 
    // l(heure se trouve codée sur 4 octets à partir de l'octet 40
    // soit 2 'mots' à extraire
    unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
    unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
    // Combiner les 4 octets (2 'mots') dans un entier long pour avoir l'heure NTP (le nombre de secondes depuis le 1er janvier 1900)
    unsigned long secsSince1900 = highWord << 16 | lowWord;
    // Convertir l'heure NTP en temps usuel. Soustraire 70 ans pour avoir l'"epoch" (le temps UNIX)
    unsigned long epoch = secsSince1900 - seventyYears;

    // Récupérer Heures, Minutes et Secondes
    ntpHour = (epoch  % 86400L) / 3600;    // Il y a 86400 secondes par jour
    ntpMinute = (epoch  % 3600) / 60;      // il y a 3600 secondes par minute
    ntpSecond = epoch % 60;

    // Envoi de l'heure au terminal série
    Serial.print("l'heure GMT est ");      // GMT (Greenwich Meridian Time) ou heure TU, ou encore UTC
    Serial.print(ntpHour); // print the hour (86400 equals secs per day)
    Serial.print(':');
    if (ntpMinute < 10) {
      // Pour les 10 premières minutes, il faut plcer d'abord un "0"
      Serial.print('0');
    }
    Serial.print(ntpMinute); 
    Serial.print(':');
    if (ntpSecond < 10) {
      // Pour les 10 premières secondes, il faut plcer d'abord un "0"
      Serial.print('0');
    }
    Serial.println(ntpSecond); 
  }
}

// Prochaine lecture de l'heure
unsigned long calcNextNtp(unsigned long currentMillis) {
  return(currentMillis + (ntpEveryMinutes * 60L * 1000L));
}

/**************************************************************************
**
**  Lecture de la pression
**
**************************************************************************/

/**************************************************************************
**
**  Lecture de la pression du composant Freescale MPX5050DP
**    Valeur d'isopression :  32  => O kPa
**    Valeur max :            1016 => 50 kPa
**    Valeur min :            11  => dépression
**
**    Max 3 mètres d'eau, soit 30 kPa
**
**    1 bar = 1OO kPa
**
**************************************************************************/
const float pressureBitMin = 32.0;      // Valeur brute en isopression
const float pressureBitMax = 1016.0;    // Valeur brute maxi
const float pressureKPaMin = 0.0;       // kPa en isopression
const float pressureKPaMax = 50.0;      // kPa maxi
const float pressureMmPerKpa = 100.0;   // Coefficient à appliquer aux kPa pour avoir des millimètres d'eau

// Vérifie l'évolution des mesures
// Renvoi:
//    0 -> pas assez de mesure
//    1 -> il y a au moins une augmentation dans la série
//    2 -> toutes les mesures montrent une diminution 
int pressureCheckEvolution(void) {
  if (reads[nbConsReads-1] == NULL) {
    //Serial.println(reads[nbConsReads-1]);
    return(0);  
  }
  for (int i=1; i<nbConsReads-1; i++) {
    if (reads[i-1] < reads[i]) {
      //Serial.println(i);
      //Serial.print(reads[i-1]);
      //Serial.print(" < ");
      //Serial.println(reads[i]);
      return(1);
    }
  }
  //Serial.println("OK");
  return(2);
}

// Ajoute une mesure, faite sur la broche passée en paramètre, dans la pile des mesures
void addPressure(float p) {
  // Dépiler si la pile est pleine
  if (reads[nbConsReads-1] != NULL) {
    for (int i=1; i<nbConsReads; i++) {
      reads[i-1] = reads[i];
    }
    reads[nbConsReads-1] = NULL;
  }
  // Placer la valeur dans la première case vide
  for (int i=0; i<nbConsReads; i++) {
    if (reads[i] == NULL) {
      reads[i] = p;
      break;
    }
  }
}

// Effectue une mesure sur la broche passée en paramètre et renvoi une valeur moyenne exprimée en kPa
float pressureRead(int pin) {
  long sumReads = 0;
  float avgReads = 0;
  for (int i=0; i<nLoop; i++) {
     sumReads += pressureReadAnalogPin(pin);
     delay(5);
  }
  avgReads = (float)sumReads / (float)nLoop;
  //Serial.println(avgReads, 2);
  return (pressureRawToKpa(avgReads));
}

// Lit la broche analogique passée en paramètre et renvoi une valeur brute
int pressureReadAnalogPin(int pin) {
  int r = analogRead(pin);
  //float p = (r - pressureBitMin) / (pressureBitMax - pressureBitMin) * (pressureKPaMax - pressureKPaMin);
  //return p;
  return (r);
}

// Convertir une valeur brute lue sur une broche en kPa
float pressureRawToKpa(float avgR) {
  return (avgR - pressureBitMin) / (pressureBitMax - pressureBitMin) * (pressureKPaMax - pressureKPaMin);
}

//Transforme les kPa en millimètres d'eau
float pressureKpaToMm(float kpa) {
  return kpa * pressureMmPerKpa; 
}

/**************************************************************************
**
**  MPX5050DP DataSheet
**    Pressure range      : From 0 to 50 kPa
**    Supply voltage (Vs) : 5 V
**    Min pressure offset : 0.2 V
**    Full scale ouput    : 4.7 V
**    Full scale span     : 4.5 V
**    Sensitivity         : 90 mV / kPa
**    Transfert function  : Vout = Vs * (P * 0.0018 + 0.04)
**    Valeur isopression  : 32
**
**************************************************************************/

/**************************************************************************
**
**  MPX5500DP DataSheet
**    Pressure range      : From 0 to 500 kPa
**    Supply voltage (Vs) : 5 V
**    Supply current (Io) : 10 mA max
**    Min pressure offset : 0.2 V
**    Full scale ouput    : 4.7 V
**    Full scale span     : 4.5 V
**    Sensitivity         : 9 mV / kPa
**    Transfert function  : Vout = Vs * (P * 0.0018 + 0.04)  ?
**    Valeur isopression  : 37
**
**************************************************************************/

Conclusion

Voici un petit montage qui fonctionne tout seul depuis 6 mois, avec l'inconvénient de devoir aller chercher l'info. Désormais, je la vois chaque fois que je passe dans le garage.


Publié le 25 mars 2016