Niveau d’eau dans la citerne

vendredi 25 mars 2016
par  Finizi
popularité : 100%

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.
La première version a été décrite ici.
Voici le seconde version, qui contient juste une amélioration : l’affichage directement sur le boitier, du pourcentage de remplissage et du niveau d’eau dans la cuve.


 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.
PNG - 32.9 ko
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.

JPEG - 85.2 ko

Liste des composants utilisés :

  • 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

PNG - 30.6 ko
  • 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.

JPEG - 59 ko

 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.

PNG - 60.6 ko

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.

  1.  
  2. /*************************************************************
  3. *   Includes
  4. */
  5.  
  6. #include <avr/wdt.h>               // Pour le WatchDog
  7. #include <SPI.h>                   // Pour la carte Ethernet
  8. #include <Ethernet.h>              // Pour la carte Ethernet
  9. #include <EthernetServer.h>        // Pour la communication Ethernet
  10. #include <EthernetClient.h>        // Pour la communication Ethernet
  11. #include <EthernetUdp.h>           // Pour le protocole NTP qui permet de récupérer l'heure
  12. #include <U8glib.h>                // Pour l'afficheur oLed
  13.  
  14.  
  15.  
  16. /*************************************************************
  17. *   Defines & constantes
  18. */
  19. const int printPause = 50;           // Pause après envoi sur le terminal série, pour laisser le temps au destinataire de traiter l'information
  20. const int pinRelayPump = 2;          // Broche du relai d'alimentation électrique de la pompe à air
  21. const int pinPressure = A0;          // Broche reliée au capteur de pression
  22. const int pinInternalLED = 13;       // Led interne sur la carte Arduino - Ne pas utiliser avec le shield ethernet
  23. const int pinLED = 5;                // Led du montage
  24.  
  25. const int relayOn = 0;
  26. const int relayOff = 255;
  27.  
  28. const int httpMaxChar = 25;          // Nombre de caractère maxi à traiter
  29. //const long millisDay = 86400000L;    // Nombre de millisecondes dans une journée
  30. const int nbConsReads = 5;           // Nombre de mesures consécutives à observer pour obtenir un résultat stable
  31. const int nLoop = 100;               // Nombre de lectures à effectuer pour avoir une mesure
  32. const int maxCycles = 100;           // Nombre de cycle maxi avant mise en défaut
  33. const int ntpEveryMinutes = 10;      // Demande NTP (heure) toutes les 10 minutes
  34. const unsigned long seventyYears = 2208988800UL;
  35.                                      // Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
  36.  
  37. // Déclaration des états
  38. const int statusUnknown = 0;         // Etat inconnu ou indéfini
  39. const int statusWaiting = 1;         // En attende de déclencher une lecture de pression
  40. const int statusOperatingPhase1 = 2; // Une lecture de pression est en cours, en phase 1
  41. const int statusOperatingPhase2 = 3; // Une lecture de pression est en cours, en phase 2
  42. const int statusDefault = 99;        // Mise en défaut car hauteur d'eau non lue correctement
  43.  
  44. const int ntpPacketSize = 48;        // NTP time stamp is in the first 48 bytes of the message
  45.  
  46.  
  47.  
  48. /*************************************************************
  49. *   Déclaration des variables globales
  50. */
  51.  
  52. int loopStatus = statusUnknown;      // Etat en cours
  53. int loopLastStatus = statusUnknown;  // Etat précédent
  54. int ledStatus = LOW;
  55.  
  56. float waterLevel = 0;
  57. float reads[nbConsReads-1];          // Enregistrement des lectures successives
  58. int cycles = 0;                      // Nombre de cycle depuis le démarrage de la pompe
  59.  
  60.  
  61. // Déclaration relatives à la config réseau
  62. byte netMac[6] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06};  // Adresse MAC de la carte ethernet arduino
  63. byte netIp[4] = { 192, 168, 1, 2 };                      // Adresse IP de la carte Arduino
  64. byte netGateway[4] = { 192, 168, 1, 200 };               // Adresse IP de la passerelle
  65. byte netMask[4] = { 255, 255, 255, 0 };                  // Masque sous-réseau
  66. byte sqlServer[4] = { 192, 168, 1, 1 };                  // Adresse IP vers laquelle envoyer les donn�es
  67. unsigned int sqlPort = 8080;                             // Port HTTP sur lequel écoute le server SQL
  68. unsigned int udpPort = 8888;                             // Port pour écouter les packets UDP
  69. char timeServer[] = "time.nist.gov"; // time.nist.gov NTP server
  70. //char timeServer[] = "192.168.2.200"; // server sur le réseau local
  71.  
  72. byte packetBuffer[ ntpPacketSize];   // buffer to hold incoming and outgoing packets
  73. unsigned long nextNtp;               // millis() à atteindre pour demander l'heure                  
  74. int ntpHour = -1;
  75. int ntpMinute = -1;
  76. int ntpSecond = -1;
  77.  
  78. /*************************************************************
  79. *   Déclaration des objets
  80. */
  81.  
  82. EthernetServer httpServer(80);       // on crée un objet serveur utilisant le port 80 (port HTTP par défaut)
  83. EthernetClient sqlClient;            // objet client pour envoyer des données
  84. EthernetUDP udp;
  85.  
  86. //U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_NONE);              // Afficheur sur broches SCL & SDA
  87. U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_NO_ACK);            // Afficheur sur broches A4 & A5
  88. const u8g_fntpgm_uint8_t *smallFont = u8g_font_courR10r;  // Petite police
  89. const u8g_fntpgm_uint8_t *hugeFont = u8g_font_fub49n;     // Police géante
  90.  
  91.  
  92.  
  93.  
  94. /**************************************************************************
  95. **
  96. **  setup()  ==>  Fonction obligatoire d'initialisation
  97. **
  98. **************************************************************************/
  99.  
  100. void setup() {
  101.  
  102.   // Initialisation du WatchDog
  103.   wdt_enable(WDTO_8S);
  104.  
  105.   // Initialisation de la communication série
  106.   Serial.begin(115200);  
  107.   Serial.println("Starting V 1.0 ...");
  108.  
  109.   // Test de l'afficheur
  110.   oledDrawText("Setup");
  111.   delay(printPause);
  112.  
  113.   // Déclarer les broches comme "output" ou "input"
  114.   pinMode(pinLED, OUTPUT);
  115.   pinMode(pinRelayPump, OUTPUT);
  116.   pinMode(pinPressure, INPUT);
  117.  
  118.   // Initialisation des broches pour que les relais ne soient pas actifs au reset
  119.   digitalWrite(pinRelayPump, relayOff);
  120.  
  121.   Serial.println("Pins initialized.");
  122.   delay(printPause);
  123.  
  124.   // Initialisation de la connexion Ethernet avec l'adresse MAC, l'adresse IP et le masque
  125.   Ethernet.begin(netMac, netIp, netGateway, netMask);
  126.   Serial.print("Ethernet initialized on IP ");
  127.   for(int i = 0; i < 4; i++) {
  128.     if (i > 0) {
  129.       Serial.print(".");
  130.     }
  131.     Serial.print(netIp[i]);
  132.   }
  133.   udp.begin(udpPort);
  134.   Serial.println("");
  135.   delay(printPause);
  136.   // Initialisation des variables
  137.   for(int i=0; i<nbConsReads; i++) {
  138.     reads[i] = NULL;
  139.   }
  140.   //getNtpTime();
  141.   nextNtp = 0;
  142.  
  143.   // Initialisation du serveur interne, et commence à écouter les clients
  144.   httpServer.begin();  
  145.  
  146.   flushSerial();   // Pour vider le port série des ordres ventuellement arriv lors de l'initialisation
  147.   loopStatus = statusWaiting;
  148.   Serial.println("Setup finish.");
  149.   delay(printPause);
  150.  
  151. }
  152.  
  153.  
  154.  
  155. /**************************************************************************
  156. **
  157. **  loop()  ==>  Fonction obligatoire. Boucle infinie de fonctionnement
  158. **
  159. **************************************************************************/
  160.  
  161. void loop() {
  162.  
  163.   // Reset du watchDog : C'est reparti pour 8 secondes
  164.   wdt_reset();
  165.  
  166.   // Lire les éventuelles commandes reçues sur les ports série ou réseau et y répondre
  167.   serialCommandProcess();    
  168.   httpCommandProcess();
  169.  
  170.   // Gérer la mesure de pression
  171.   if (isOperating()) {
  172.     oledDrawText("Operate");
  173.     float p = pressureRead(pinPressure);
  174.     addPressure(p);
  175.     Serial.print(p, 2);
  176.     Serial.println(" kPa");
  177.     delay(printPause);
  178.     int e = pressureCheckEvolution();
  179.     if (e > 0) {
  180.       // La pile est pleine, la phase 1 a bien commencée
  181.       if ( (loopStatus == statusOperatingPhase1) && (e == 2) ) {
  182.         // Phase 1 ET que des diminutions
  183.         loopStatusChange(statusOperatingPhase2);
  184.       }
  185.       if ( (loopStatus == statusOperatingPhase2) && (e == 1) ) {
  186.         // Phase 2 ET une augmentation : On a trouvé une valeur
  187.         airPumpStop(statusWaiting);
  188.       }
  189.     }
  190.     cycles++;
  191.     if (cycles >= maxCycles) {
  192.       airPumpStop(statusDefault);
  193.     }
  194.   }
  195.  
  196.   // Gérer le temps - Toutes les 10 minutes environ, récupérer l'heure
  197.   if ( (millis() > nextNtp) && !isOperating() ) {
  198.     getNtpTime();
  199.     nextNtp = calcNextNtp(nextNtp);
  200.     if (checkTime()) {
  201.       airPumpStart();
  202.     }
  203.   }
  204.    
  205.   // Faire clignoter la LED, en fonction de loopStatus
  206.   toggleLedStatus();
  207.   // Si pas en cours de lecture, faire une longue pause
  208.   if (!isOperating()) {
  209.     // Si en défaut, quelques éclats supplémentaires
  210.     if (loopStatus == statusDefault) {
  211.       oledDrawText("Default");
  212.       for (int i=0; i<7; i++) {
  213.         toggleLedStatus();
  214.         delay(100);
  215.       }
  216.       delay(800);
  217.     }
  218.     else {
  219.       delay(1500);
  220.     }
  221.   }
  222. }
  223.  
  224.  
  225. /**************************************************************************
  226. **
  227. **  Fonctions d'affichage oLed et calcul du pourcentage
  228. **
  229. **************************************************************************/
  230.  
  231. // A partir du niveau d'eau en mm, calcul le pourcentage de remplissage de la cuve
  232. float perCentFill() {
  233.   // 1600 mm ou plus => 100% (plein)
  234.   // 200 mm ou moins => 0%  (vide)
  235.   const float fillMax = 1600;
  236.   const float fillMin = 200;
  237.   return( (waterLevel - fillMin) / (fillMax - fillMin) * 100 );
  238. }
  239.  
  240. // Affiche le texte passé en paramètre
  241. void oledDrawText(String label) {
  242.   u8g.firstPage();  
  243.   do {
  244.     u8g.setFont(smallFont);
  245.     u8g.setFontPosTop();
  246.     u8g.setScale2x2();
  247.     u8g.drawStr( 0, 10, label.c_str());
  248.     u8g.undoScale();
  249.   } while( u8g.nextPage() );
  250. }
  251.  
  252. // Affiche un pourcentage en gros
  253. void oledDrawPCentLevel(float pc, float level) {
  254.   int top = 0;
  255.   char str[10];
  256.   u8g.firstPage();  
  257.   do {
  258.    
  259.     u8g.setColorIndex(1);
  260.     u8g.drawBox(0, 0, 127, 63);
  261.     u8g.setColorIndex(0);
  262.  
  263.     u8g.setFont(hugeFont); // OK
  264.     u8g.setFontPosTop();
  265.     // Cas du chiffre 100
  266.     if (pc > 99.5) {
  267.       u8g.drawStr(-7, top, "1");
  268.       u8g.drawStr(24, top, "00");
  269.     }
  270.     else {
  271.       if (pc > 9.5) {
  272.         u8g.drawStr(20, top, dtostrf(pc, 2, 0, str));
  273.       }
  274.       else {
  275.         if (pc < 0) {
  276.           if (pc <= -9.5) {
  277.             u8g.drawStr(0, top, dtostrf(pc, 3, 0, str));
  278.           }
  279.           else {
  280.             u8g.drawStr(37, top, dtostrf(pc, 3, 0, str));
  281.           }
  282.         }
  283.         else {
  284.           u8g.drawStr(52, top, dtostrf(pc, 2, 0, str));
  285.         }
  286.       }
  287.     }
  288.     // Le pourcentage
  289.     int xe = 100;
  290.     u8g.drawStr(xe, top, "/");
  291.     u8g.drawFilledEllipse(xe+8, top+6, 6, 8, U8G_DRAW_ALL);
  292.     u8g.drawFilledEllipse(xe+20, top+45, 6, 8, U8G_DRAW_ALL);
  293.      
  294.     u8g.setFont(smallFont);
  295.     u8g.setFontPosTop();
  296.     //u8g.drawStr(104, top, "0");
  297.     //u8g.drawStr(119, top + 42, "0");
  298.  
  299.     // le niveau
  300.     int top2 = 52;
  301.     u8g.drawStr(50, top2, dtostrf((int)level, 4, 0, str));
  302.     u8g.drawStr(90, top2, "mm");
  303.  
  304.     u8g.setColorIndex(1);
  305.     u8g.drawFilledEllipse(xe+8, top+6, 2, 3, U8G_DRAW_ALL);
  306.     u8g.drawFilledEllipse(xe+20, top+45, 2, 3, U8G_DRAW_ALL);
  307.    
  308.   } while( u8g.nextPage() );
  309. }
  310.  
  311.  
  312.  
  313. /**************************************************************************
  314. **
  315. **  Fonctions diverses
  316. **
  317. **************************************************************************/
  318.  
  319. // Vérifie si c'est l'heure de lire la hauteur d'eau
  320. boolean checkTime() {
  321.   // Entre respectivement 11h00 et 23h00
  322.   if ( (ntpHour == 11) || (ntpHour == 23) ) {
  323.     // et 11h10 et 23h10
  324.     if (ntpMinute < ntpEveryMinutes) {
  325.       return(true);
  326.     }
  327.   }
  328.   return(false);
  329. }
  330.  
  331. // Change l'état de la LED
  332. void toggleLedStatus(void) {
  333.   if (ledStatus == LOW) {
  334.     ledStatus = HIGH;
  335.   }
  336.   else {
  337.     ledStatus = LOW;
  338.   }
  339.   digitalWrite(pinLED, ledStatus);
  340. }
  341.  
  342. // Vérifie si c'est une phase d'activité
  343. boolean isOperating() {
  344.   return ( (loopStatus == statusOperatingPhase1) || (loopStatus == statusOperatingPhase2) );
  345. }
  346.  
  347. // Changer le status courant
  348. void loopStatusChange(int newStatus) {
  349.   loopLastStatus = loopStatus;
  350.   loopStatus = newStatus;
  351.   Serial.print("loopStatus=");
  352.   Serial.println(textStatus(loopStatus));
  353. }
  354.  
  355.  
  356. // Envoi des données vers le server de dataLogging
  357. void ethernetSendData() {
  358.   // Envoyer le rapport vers le server
  359.   if (sqlClient.connect(sqlServer, sqlPort)) {
  360.     sqlClient.print("GET /data/");
  361.     sqlClient.print("WaterTank");
  362.     sqlClient.print("/status=");
  363.     sqlClient.print(textStatus(loopStatus));
  364.     sqlClient.print("&level=");
  365.     sqlClient.print(waterLevel, 0);
  366.     sqlClient.println(" HTTP/1.0");
  367.     sqlClient.println();
  368.     delay(2);
  369.     sqlClient.stop();
  370.   }
  371. }
  372.  
  373. // Envoie des données de lecture sur le port série
  374. void serialSendData(void) {
  375.   Serial.print("{\"status\":\"");
  376.   Serial.print(textStatus(loopStatus));
  377.   Serial.print("\"");
  378.   jsonPrint("level", waterLevel, 0);
  379.   Serial.println("}");
  380. }
  381.  
  382. // Chaine du statut
  383. String textStatus(int currentStatus) {
  384.   if (currentStatus == statusWaiting) {
  385.     return("Waiting");
  386.   }
  387.   else if (currentStatus == statusOperatingPhase1) {
  388.     return("OperatingPhase1");
  389.   }
  390.   else if (currentStatus == statusOperatingPhase2) {
  391.     return("OperatingPhase2");
  392.   }
  393.   else if (currentStatus == statusDefault) {
  394.     return("Default");
  395.   }
  396.   return("Unknown");
  397. }
  398.  
  399. //  Ajout d'une valeur au document jSon, après vérification
  400. void jsonPrint(String label, float val, int precis) {
  401.   if ( !isinf(val) && !isnan(val) && (val <= 4294967040.0) && (val >= -4294967040.0) ) {
  402.     Serial.print(",\"");
  403.     Serial.print(label);
  404.     Serial.print("\":");
  405.     Serial.print(val, precis);
  406.   }
  407. }
  408.  
  409.  
  410. /**************************************************************************
  411. **
  412. **  Gestion des commandes reçues
  413. **
  414. **************************************************************************/
  415.  
  416. // Lire les éventuelles commandes reçues sur le port série et y répondre
  417. void serialCommandProcess(void) {
  418.   String commandText = String("");
  419.   char byteInSerial;
  420.   while (Serial.available() > 0) {
  421.     byteInSerial = Serial.read();
  422.     if ( (byteInSerial == 10) || (byteInSerial == 13) ) {
  423.       //Serial.println("*");
  424.       commandProcess(commandText);
  425.     }
  426.     else {
  427.       //Serial.println("+");
  428.       commandText += String(byteInSerial);
  429.     }
  430.   }
  431.   if (commandText.length() > 0) {
  432.     //Serial.println("-");
  433.     commandProcess(commandText);
  434.   }
  435. }  
  436.  
  437. void commandProcess(String commandTxt) {
  438.   Serial.println("commandProcess");
  439.   // Test les différentes commandes possibles
  440.   if (commandTxt.startsWith("Reset")) {
  441.     commandReset();
  442.   }
  443.   if (commandTxt.startsWith("ReadWaterLevel")) {
  444.     commandInitOperate();
  445.   }
  446.   if (commandTxt.startsWith("AbortProcess")) {
  447.     commandAbortProcess();
  448.   }
  449.   else {
  450.     commandUnknown(commandTxt);
  451.   }
  452. }
  453.  
  454. // Commande de reset
  455. void commandReset() {
  456.   Serial.println("commandReset");
  457.   delay(printPause);
  458.   // Demander au watchdog de ne pas attendre plus que 15 millisecondes avant d'agir
  459.   wdt_enable(WDTO_15MS);
  460.   // Demander une longue attente pour déclencher le watchdog
  461.   delay(10000);
  462. }
  463.  
  464. // Commande d'initialisation de la lecture du niveau d'eau dans la cuve
  465. void commandInitOperate(void) {
  466.   Serial.println("commandInitOperate");
  467.   delay(printPause);
  468.   airPumpStart();
  469. }
  470.  
  471. // Commande AbortProcess
  472. void commandAbortProcess(void) {
  473.   Serial.println("commandAbortProcess");
  474.   delay(printPause);
  475.   digitalWrite(pinRelayPump, relayOff);
  476.   loopStatusChange(statusWaiting);
  477. }
  478.  
  479. // Commande inconnue !
  480. void commandUnknown(String commandTxt) {
  481.   Serial.print("commandUnknown:");
  482.   Serial.println(commandTxt);
  483.   delay(printPause);
  484. }
  485.  
  486. // Démarrage de la pompe à air - Démarrage du processus de lecture du niveau d'eau
  487. void airPumpStart(void) {
  488.   digitalWrite(pinRelayPump, relayOn);
  489.   loopStatusChange(statusOperatingPhase1);
  490.   cycles = 0;
  491. }
  492.  
  493. // Arret de la pompe à air - Fin du processus de lecture du niveau d'eau
  494. // Envoi des données de lecture
  495. void airPumpStop(int newStatus) {
  496.   waterLevel = pressureKpaToMm(reads[nbConsReads-1]);
  497.   digitalWrite(pinRelayPump, relayOff);
  498.   loopStatusChange(newStatus);
  499.   serialSendData();
  500.   oledDrawPCentLevel(perCentFill(), waterLevel);
  501.   ethernetSendData();
  502. }
  503.  
  504. // Pour vider le port série
  505. void flushSerial(void) {
  506.   while(1) {
  507.     if (Serial.available() == 0)  {
  508.       break;
  509.     }
  510.     else {
  511.        Serial.read();
  512.     }
  513.   }
  514. }
  515.  
  516.  
  517. /**************************************************************************
  518. **
  519. **  Gestion du réseau
  520. **
  521. **************************************************************************/
  522.  
  523. // Lire les éventuelles commandes reçues sur le port ethernet et y répondre
  524. void httpCommandProcess(void) {
  525.   // Exemple d'envoi de commande : http://192.168.1.2/ReadWaterLevel
  526.   // Déclaration des variables
  527.   String chaineRecue = ""; // Pour y mettre la chaine reçue
  528.   int comptChar = 0;       // Pour compter les caractères reçus
  529.   // crée un objet client basé sur le client connecté au serveur
  530.   EthernetClient httpClient = httpServer.available();
  531.   if (httpClient) { // si l'objet client n'est pas vide
  532.     // Initialisation des variables utilisées pour l'échange serveur/client
  533.     chaineRecue = "";        // Vide le String de reception
  534.     comptChar = 0;           // Compteur de caractères en réception à 0  
  535.     if (httpClient.connected()) {       // Si que le client est connecté
  536.       while (httpClient.available()) {  // Tant que des octets sont disponibles en lecture
  537.         char c = httpClient.read();     // Lit l'octet suivant reçu du client (pour vider le buffer au fur à mesure !)
  538.         comptChar++;                    // Incrémente le compteur de caractère reçus
  539.         if (comptChar <= httpMaxChar) { // Les 25 premiers caractères sont suffisants pour analyses la requête
  540.           chaineRecue += c;             // Ajoute le caractère reçu au String pour les N premiers caractères
  541.         }
  542.       }
  543.       // Si la chaine recue fait au moins 25 caractères ET qu'elle commence par "GET"
  544.       if ( (chaineRecue.length() >= httpMaxChar) && (chaineRecue.startsWith("GET")) ) {
  545.         // Extrait à partir du 6ème caractère
  546.         httpSend(httpClient,"OK");
  547.         commandProcess(chaineRecue.substring(5));
  548.       }
  549.     } // Fin while client connected
  550.     delay(10);           // On donne au navigateur le temps de recevoir les données
  551.     httpClient.stop();   // Fermeture de la connexion avec le client
  552.   }
  553. }  
  554.  
  555. void httpSend(EthernetClient httpClient, String txt) {
  556.   // Envoi d'une entete standard de réponse http
  557.   httpClient.print("HTTP/1.1 20");
  558.   if (txt.length() == 0) {
  559.     //httpClient.println("HTTP/1.1 204 OK");
  560.     httpClient.print("4");
  561.   }
  562.   else {
  563.     //httpClient.println("HTTP/1.1 200 OK");
  564.     httpClient.print("0");
  565.   }
  566.   httpClient.println(" OK");
  567.   //httpClient.println("HTTP/1.1 200 OK");
  568.   httpClient.println("Content-Type: text/html");
  569.   httpClient.println("Connection: close");
  570.   httpClient.println();
  571.   // Affiche chaines caractères simples
  572.   if (txt.length() > 0) {
  573.     httpClient.println(txt);
  574.   }
  575.   delay(200);
  576. }
  577.  
  578. // Envoi une demande au server NTP dont l'adresse est en paramètre
  579. unsigned long sendNtpPacket(char* address) {
  580.   // Initialise le buffer à 0 par défaut
  581.   memset(packetBuffer, 0, ntpPacketSize);
  582.   // Initialisée la demande NTP
  583.   packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  584.   packetBuffer[1] = 0;            // Stratum, or type of clock
  585.   packetBuffer[2] = 6;            // Polling Interval
  586.   packetBuffer[3] = 0xEC;         // Peer Clock Precision
  587.   // 8 bytes of zero for Root Delay & Root Dispersion
  588.   packetBuffer[12]  = 49;
  589.   packetBuffer[13]  = 0x4E;
  590.   packetBuffer[14]  = 49;
  591.   packetBuffer[15]  = 52;
  592.  
  593.   // Envoi du paquet IP de demande NTP
  594.   udp.beginPacket(address, 123);  // Les requetes NTP se font sur le port 123
  595.   udp.write(packetBuffer, ntpPacketSize);
  596.   udp.endPacket();
  597. }
  598.  
  599. void getNtpTime(void) {
  600.   // Envoyer un packet Ntp au server
  601.   sendNtpPacket(timeServer);
  602.   // attendre la réponse du server NTP. 100 ms pour un server local est suffisant. Prévoir plus pour un server Internet
  603.   delay(100);
  604.   if (udp.parsePacket()) {
  605.     // Le server NTP a répondu.
  606.     // Lire lire données reçues dans le buffer
  607.     udp.read(packetBuffer, ntpPacketSize);
  608.     // l(heure se trouve codée sur 4 octets à partir de l'octet 40
  609.     // soit 2 'mots' à extraire
  610.     unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
  611.     unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
  612.     // Combiner les 4 octets (2 'mots') dans un entier long pour avoir l'heure NTP (le nombre de secondes depuis le 1er janvier 1900)
  613.     unsigned long secsSince1900 = highWord << 16 | lowWord;
  614.     // Convertir l'heure NTP en temps usuel. Soustraire 70 ans pour avoir l'"epoch" (le temps UNIX)
  615.     unsigned long epoch = secsSince1900 - seventyYears;
  616.  
  617.     // Récupérer Heures, Minutes et Secondes
  618.     ntpHour = (epoch  % 86400L) / 3600;    // Il y a 86400 secondes par jour
  619.     ntpMinute = (epoch  % 3600) / 60;      // il y a 3600 secondes par minute
  620.     ntpSecond = epoch % 60;
  621.    
  622.     // Envoi de l'heure au terminal série
  623.     Serial.print("l'heure GMT est ");      // GMT (Greenwich Meridian Time) ou heure TU, ou encore UTC
  624.     Serial.print(ntpHour); // print the hour (86400 equals secs per day)
  625.     Serial.print(':');
  626.     if (ntpMinute < 10) {
  627.       // Pour les 10 premières minutes, il faut plcer d'abord un "0"
  628.       Serial.print('0');
  629.     }
  630.     Serial.print(ntpMinute);
  631.     Serial.print(':');
  632.     if (ntpSecond < 10) {
  633.       // Pour les 10 premières secondes, il faut plcer d'abord un "0"
  634.       Serial.print('0');
  635.     }
  636.     Serial.println(ntpSecond);
  637.   }
  638. }
  639.  
  640. // Prochaine lecture de l'heure
  641. unsigned long calcNextNtp(unsigned long currentMillis) {
  642.   return(currentMillis + (ntpEveryMinutes * 60L * 1000L));
  643. }
  644.  
  645.  
  646.  
  647. /**************************************************************************
  648. **
  649. **  Lecture de la pression
  650. **
  651. **************************************************************************/
  652.  
  653.  
  654.  
  655. /**************************************************************************
  656. **
  657. **  Lecture de la pression du composant Freescale MPX5050DP
  658. **    Valeur d'isopression :  32  => O kPa
  659. **    Valeur max :            1016 => 50 kPa
  660. **    Valeur min :            11  => dépression
  661. **
  662. **    Max 3 mètres d'eau, soit 30 kPa
  663. **
  664. **    1 bar = 1OO kPa
  665. **
  666. **************************************************************************/
  667. const float pressureBitMin = 32.0;      // Valeur brute en isopression
  668. const float pressureBitMax = 1016.0;    // Valeur brute maxi
  669. const float pressureKPaMin = 0.0;       // kPa en isopression
  670. const float pressureKPaMax = 50.0;      // kPa maxi
  671. const float pressureMmPerKpa = 100.0;   // Coefficient à appliquer aux kPa pour avoir des millimètres d'eau
  672.  
  673. // Vérifie l'évolution des mesures
  674. // Renvoi:
  675. //    0 -> pas assez de mesure
  676. //    1 -> il y a au moins une augmentation dans la série
  677. //    2 -> toutes les mesures montrent une diminution
  678. int pressureCheckEvolution(void) {
  679.   if (reads[nbConsReads-1] == NULL) {
  680.     //Serial.println(reads[nbConsReads-1]);
  681.     return(0);  
  682.   }
  683.   for (int i=1; i<nbConsReads-1; i++) {
  684.     if (reads[i-1] < reads[i]) {
  685.       //Serial.println(i);
  686.       //Serial.print(reads[i-1]);
  687.       //Serial.print(" < ");
  688.       //Serial.println(reads[i]);
  689.       return(1);
  690.     }
  691.   }
  692.   //Serial.println("OK");
  693.   return(2);
  694. }
  695.  
  696.  
  697. // Ajoute une mesure, faite sur la broche passée en paramètre, dans la pile des mesures
  698. void addPressure(float p) {
  699.   // Dépiler si la pile est pleine
  700.   if (reads[nbConsReads-1] != NULL) {
  701.     for (int i=1; i<nbConsReads; i++) {
  702.       reads[i-1] = reads[i];
  703.     }
  704.     reads[nbConsReads-1] = NULL;
  705.   }
  706.   // Placer la valeur dans la première case vide
  707.   for (int i=0; i<nbConsReads; i++) {
  708.     if (reads[i] == NULL) {
  709.       reads[i] = p;
  710.       break;
  711.     }
  712.   }
  713. }
  714.  
  715. // Effectue une mesure sur la broche passée en paramètre et renvoi une valeur moyenne exprimée en kPa
  716. float pressureRead(int pin) {
  717.   long sumReads = 0;
  718.   float avgReads = 0;
  719.   for (int i=0; i<nLoop; i++) {
  720.      sumReads += pressureReadAnalogPin(pin);
  721.      delay(5);
  722.   }
  723.   avgReads = (float)sumReads / (float)nLoop;
  724.   //Serial.println(avgReads, 2);
  725.   return (pressureRawToKpa(avgReads));
  726. }
  727.  
  728. // Lit la broche analogique passée en paramètre et renvoi une valeur brute
  729. int pressureReadAnalogPin(int pin) {
  730.   int r = analogRead(pin);
  731.   //float p = (r - pressureBitMin) / (pressureBitMax - pressureBitMin) * (pressureKPaMax - pressureKPaMin);
  732.   //return p;
  733.   return (r);
  734. }
  735.  
  736. // Convertir une valeur brute lue sur une broche en kPa
  737. float pressureRawToKpa(float avgR) {
  738.   return (avgR - pressureBitMin) / (pressureBitMax - pressureBitMin) * (pressureKPaMax - pressureKPaMin);
  739. }
  740.  
  741. //Transforme les kPa en millimètres d'eau
  742. float pressureKpaToMm(float kpa) {
  743.   return kpa * pressureMmPerKpa;
  744. }
  745.  
  746. /**************************************************************************
  747. **
  748. **  MPX5050DP DataSheet
  749. **    Pressure range      : From 0 to 50 kPa
  750. **    Supply voltage (Vs) : 5 V
  751. **    Min pressure offset : 0.2 V
  752. **    Full scale ouput    : 4.7 V
  753. **    Full scale span     : 4.5 V
  754. **    Sensitivity         : 90 mV / kPa
  755. **    Transfert function  : Vout = Vs * (P * 0.0018 + 0.04)
  756. **    Valeur isopression  : 32
  757. **
  758. **************************************************************************/
  759.  
  760.  
  761. /**************************************************************************
  762. **
  763. **  MPX5500DP DataSheet
  764. **    Pressure range      : From 0 to 500 kPa
  765. **    Supply voltage (Vs) : 5 V
  766. **    Supply current (Io) : 10 mA max
  767. **    Min pressure offset : 0.2 V
  768. **    Full scale ouput    : 4.7 V
  769. **    Full scale span     : 4.5 V
  770. **    Sensitivity         : 9 mV / kPa
  771. **    Transfert function  : Vout = Vs * (P * 0.0018 + 0.04)  ?
  772. **    Valeur isopression  : 37
  773. **
  774. **************************************************************************/

 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.

JPEG - 75.6 ko

Commentaires

Logo de Finizi
mercredi 23 août 2017 à 14h29 - par  Finizi

Merci Jo
Cela va faire 2 ans que c’est installé, et comme ça fonctionne, je n’y touche plus :).
Donc, je n’ai pas été voir si j’avais de l’eau du coté du capteur.
J’ai déjà utilisé un capteur identique sur un circuit de chauffage, donc directement sur l’eau, ce pour quoi il n’est pas prévu. Il a tenu quand même 9 mois...
Si il y a de l’eau, il serait intéressant de mettre une purge.
A priori, un très petit trou devrait suffire, tête d’épingle, ou peut-être mieux, une buse de goutte à goutte, placé en bas d’un U (siphon). Si le débit d’air passant par cette purge est suffisamment faible, cela ne devrait pas affecter le résultat de la mesure.
Il faudra vérifier qu’il ne s’encrasse pas...

Logo de Jo
mercredi 16 août 2017 à 22h21 - par  Jo

Bonjour

Super article. J’ai réalisé la même et ça fonctionne. Sauf que j’ai un peu d’eau qui revient vers le capteur. Serait-ce de la condensation ?

As tu rencontré ce problème ? Le T se trouve comme toi dans la citerne tout en haut.

Merci pour ton aide.

Jo

Navigation

Articles de la rubrique

  • Niveau d’eau dans la citerne