mardi 15 août 2023

ESP32 : Micro-irrigation Connectée (3ème partie)

 


ESP32 : Micro-irrigation Connectée (3ème partie)


Cet article fait suite à ceux-ci :

ESP32 : Micro-irrigation Connectée (1ère partie)

ESP32 : Micro-irrigation Connectée (2ème partie)

Pour rappel, le projet se trouve ici :

https://bitbucket.org/henri_bachetti/esp32-sprinkle-timer

1. Le hardware

Du côté matériel, le capteur d'humidité et le capteur de débit sont présents : 

  • capteur d'humidité : GPIO36 (broche SVP)
  • capteur de débit : GPIO39 (broche SVN)

Contrairement à ce que je pensais, l'ESP32 est tout à fait capable de mesurer une tension analogique de 3.3V, ou plutôt la librairie ARDUINO fait ce qu'il faut pour que cela soit possible. Le pont diviseur R3 R5 disparaît du schéma (paragraphe 5).

En ce qui concerne le capteur de débit, la mesure renvoie des valeurs non nulles pour un débit de zéro. Il faut que je fasse des essais afin de voir s'il ne s'agirait pas d'un problème d'alimentation. Il est alimenté par la broche 5V de l'ESP32 et celui-ci est alimenté par l'USB. Comme il ne reste que 4.7V, ce n'est peut-être pas suffisant.

L'écran OLED est connecté, ainsi que le bouton fonction et arrosage manuel: 

  • SDA : GPIO21
  • SCL : GPIO22
  • bouton fonction : GPIO35
  • bouton arrosage manuel : GPIO34

Comme il n'y a pas de résistance de PULLUP interne sur les GPIOS 35 et 34, il faut en ajouter une sur le schéma entre GPIO35, GPIO34 et 3.3V : 10KΩ.

2. La configuration

2.1. config.ini

Le paramétrage des capteurs a été ajouté :

[moisture]
sensor=36
max=50

[flow]
sensor=39
max=10

2.2 Schedule.ini

Ce fichier reste inchangé, mis à part que deux périodes d'arrosage ont été ajoutées :

[courges.voie1]
schedule1=08:00,15

[massif.voie1]
schedule1=08:30,15

3. L'interface HTML

La page d'accueil comporte maintenant des boutons permettant de modifier les périodes d'arrosage et même d'en ajouter : 

Cliquer sur l'un des boutons CONFIGURE permet d'accéder à un formulaire :

Il suffit de modifier les valeurs et de cliquer sur OK pour enregistrer les changements. Les changements sont stockés dans le fichier schedule.ini.

En cliquant sur le bouton AJOUTER on accède à un formulaire à peine différent :

Le formulaire propose une liste des zones existantes.

Attention il n'y a pas pour l'instant de contrôle de validité sur ces deux formulaires. Il faut bien respecter le format de saisie HH:MM.

Les durées sont exprimées en minutes.

Le bouton maintenance permet d'accéder au menu suivant :

Les actions suivantes sont possibles :

  • tester les relais
  • afficher le fichier config.ini
  • afficher le fichier schedule.ini

Il est donc possible de sauvegarder ces deux fichiers avant une mise à jour des fichiers HTML. Les fichiers HTML et les fichiers de configuration (.ini) sont situés dans le même répertoire data du projet. Les fichiers de configuration seraient donc écrasés à la prochaine mise à jour si l'on ne prend pas de précautions.

4. L'écran OLED

L'afficheur montre la date et l'heure, ainsi que l'humidité et le débit :

Un appui sur le bouton fonction permet l'affichage de l'adresse IP de l'ESP32 pendant 4 secondes, puis de l'heure du prochain arrosage pendant 4 secondes, puis l'affichage revient à la normale.

Un appui sur le bouton d'arrosage manuel provoque l'affichage du premier arrosage manuel possible et de sa durée :

En appuyant plusieurs fois sur ce bouton on peut faire défiler les noms des voies. Un appui sur le bouton fonction déclenche l'arrosage manuel sur la voie choisie.

5. Rappels

Ce logiciel a besoin d'être compilé en utilisant les librairies suivantes :

ESPAsyncWebServer : https://github.com/me-no-dev/ESPAsyncWebServer

SPIFFSIniFile : https://github.com/yurilopes/SPIFFSIniFile

SSD1306 : https://github.com/adafruit/Adafruit_SSD1306

GFX : https://github.com/adafruit/Adafruit-GFX-Library

ezButton : https://github.com/ArduinoGetStarted/button

Quelques constantes peuvent être modifiées dans le code :

// max line length in config file
#define MAX_LINE 250
// max definition length for object names (relays, ways, zones, etc.)
#define MAX_DEF 50
#define MAX_BUF 30

// max ways and relays number
#define MAX_WAY 128
// max zones number
#define MAX_ZONE 12
// max waterings number in a day for each way
#define MAX_SCHEDULE 4
// max waterings number in a day for all ways
#define MAX_WATERING 256

On voit par exemple que le nombre d'arrosages par voie est limité à 4 par jour, ce qui me semble amplement suffisant. On peut bien entendu ajuster certaines valeurs, mais attention, celles-ci ne sont pas modifiables ultérieurement par l'interface HTML.

Pour le reste, relire l'article précédent : 5. Essayer

6. Le code

Le code, comme on peut s'en douter, n'est pas constitué d'un seul sketch, il serait énorme et illisible.

Voici une liste des fichiers :

esp32-sprinkle-timer.ino : le sketch principal
config.cpp, config.h : lecture du fichier de configuration config.ini
constants.h : constantes
flow.cpp, flow.h : capteur de débit
hmi.cpp, hmi.h : interface homme/machine sur l'écran OLED
html.cpp, html.h : interface homme/machine HTML
humidity.cpp, humidity.h : capteur d'humidité
module.cpp, module.h : gestion des modules (GPIOs, MCP23017, etc.)
oled.cpp, oled.h : écran OLED
relay.cpp, relay.h : gestion des relais
schedule.cpp, schedule.h : lecture du fichier de configuration schedule.ini
valve.cpp, valve.h : gestion des vannes
watering.cpp, watering.h : gestion des arrosages
way.cpp, way.h : gestion des voies
zone.cpp, zone.h : gestion des zones

Je veux bien admettre que ce code commence à devenir très conséquent, mais la complexité ne se gère pas avec simplicité. En opérant un découpage en modules simples, on peut toutefois réduire cette complexité et améliorer la lisibilité.

7. Conclusion

Les difficultés sont immenses car ce logiciel est hyper-paramétrable, et le temps manque. En effet, on peut difficilement s'occuper d'un jardin de 45m², récolter, cuisiner, faire des conserves, gérer un poulailler, aménager une nouvelle habitation, modifier son circuit électrique, et faire du développement logiciel en parallèle.

La période hivernale sera certainement plus productive en lignes de code. Patience ...


Cordialement

Henri

8. Mises à jour

18/08/2023 : ajout de l'écran OLED et du bouton fonction
22/08/2023 : ajout du bouton d'arrosage manuel


38 commentaires:

  1. Oui, le pont diviseur peut être modifié comme suit :
    R3 à remplacer par 0Ω, un bout de fil.
    R5 à laisser vide
    C'est vrai qu'un SSD1327 offre plus de surface d'affichage qu'un SSD1306.

    RépondreSupprimer
  2. Merci pour le retour.
    J'ai testé une autre solution qui fonctionne bien même si effectivement elle n'a pas réellement d'intérêt...
    Les valeurs du pont diviseur mettait l'entrée GPIO toujours à la masse car un courant trop fort circulait et par conséquent aucune lecture sur la broche... En employant des valeurs de 39K et 100K ça fonctionne parfaitement. J'ai en outre modifié l'atténuation de l'ADC sur 0db comme cela on une mesure plus précise (4096 points sur 1,1V env).
    Avez-vous pu résoudre/comprendre le problème sur l'entrée du capteur de débit qui n'affichait pas 0 ? Peut -être également des valeurs du pont diviseur trop faibles ?
    Bonne journée

    RépondreSupprimer
  3. Tout dépend certainement du modèle de capteur.
    Je n'ai pas encore eu le temps de voir côté capteur de débit.

    RépondreSupprimer
  4. Bonjour Henri, je souhaiterai tout comme vous avez prévu d'ajouter une irrigation à une voie, pouvoir également en "supprimer" une par un bouton.
    Ma question est donc comment supprimer une des 4 irrigations d'une voie. Faut-il tout simplement mettre la valeur m_duration à 0 ou y a t il d'autres variables à réinitialiser ?
    Autre question :
    J'ai par exemple 3 irrigations pour la voie Jardin.Potager :
    Irrigation N°1 - Index[0] Jardin.Potager 06:00 15min
    Irrigation N°2 - Index[1] Jardin.Potager 20:00 15min
    Irrigation N°3 - Index[2] Jardin.Potager 22:00 15min
    Je supprime l'irrigation N°2. Il me reste donc :
    Irrigation N°1 - Index[0] Jardin.Potager 06:00 15min
    Irrigation N°3 - Index[2] Jardin.Potager 22:00 15min
    Comment faire pour "copier" ou "déplacer" l'irrigation N°3 avec l'index[2] en Irrigation N°2 - Index[1] Jardin.potager 22:00 15min.
    Je pose cette question car contrairement à vous mon interface WEB ouvre et affiche toutes les irrigations d'une voie lorsque je "configure" une voie. Et surtout, mon code lit les irrigations dans l'ordre. D'où mon souhait de ne pas laisser de trous entre les index des irrigations.
    Je ne sais pas si je suis bien clair...
    En fait j'ai reproduit l'application Rain Bird : https://play.google.com/store/apps/details?id=com.rainbird&hl=en_US
    Merci beaucoup.

    RépondreSupprimer
    Réponses
    1. Classiquement, dans un tableau, si l'on veut déplacer un élément on utilise memcpy(destination, source, taille), ensuite memset(source, 0, taille) pour effacer l'élément source.

      Supprimer
    2. Bon je pense comprendre le principe, mais je n'arrive pas à mettre en oeuvre concrètement vos explications... Je bute avec des erreurs sur les fonctions memcpy et memset...
      Si je reprends mon exemple avec 3 irrigations pour la voie Jardin.Potager :
      Irrigation N°1 - Index[0] Jardin.Potager 06:00 15min
      Irrigation N°2 - Index[1] Jardin.Potager 20:00 15min
      Irrigation N°3 - Index[2] Jardin.Potager 22:00 15min
      Irrigation N°4 - Index[3] Jardin.Potager 00:00 0min
      Si je veux supprimer l'irrigation N°2, comment est ce que je fais :
      1- pour supprimer l'irrigation N°2 Index[1]
      2- pour décaler les irrigations N°3 Index[2] et N°4 Index[3] vers leur nouvelle position Index[1] et Index[2]
      3- "recréer" une irrigation libre pour l'index[3]
      Auriez-vous un peut bout de code pour me mettre sur la piste (au moins comment supprimer et copier une irrigation d'une voie).
      Même si j'avais un peu programmé avant, je n'avais jamais fait de C auparavant et du coup c'est compliqué pour moi de tout comprendre... J'arrive à adapter votre code, mais pas toujours à créer des nouvelles fonctions...
      Avec mes remerciements et toutes mes excuses.

      Supprimer
    3. Il faudrait avoir au moins la structure d'une irrigation. Est-ce une structure, une classe ?

      Supprimer
    4. Bonjour, je n'ai pas modifié votre code de ce côté là. J'ai juste adapté quelques fonctions pour mon affichage et mes gestions de dates.
      Donc si j'ai bien compris "mes" irrigations sont "vos" waterings, avec pour chaque way -> 4 irrigations (MAX_SCHEDULE). Donc à priori ce sont des classes avec un tableau[4 ou MAX_SCHEDULE] pour chaque way ?

      Supprimer
    5. Il vaudrait mieux donc implémenter un opérateur de recopie.
      Watering::Watering(const Watering& w) :
      m_way(w.m_way),
      m_hour(w.m_hour), m_minute(w.m_minute),
      m_duration(w.m_duration),
      m_always(w.m_always)
      {
      }
      Mais c'est un peu compliquer les choses. Dans mon cas, à partir du moment où m_duration vaut zéro, je n'affiche pas le watering en question, donc je considère qu'il est libre.

      Supprimer
    6. Bonjour Henri, merci pour votre aide. Grâce à vous j’ai réussi à faire ce que je souhaiterais... Je continue à suivre votre projet avec grand intérêt...

      Supprimer
  5. Bonjour Henri, pour info mes boutons ne marchaient pas sur la carte que j'ai conçue (d'après votre plan). Il faut faire une petite correction. En effet après avoir consulté la datasheet les entrées GPIO 34 et 35 ne possèdent pas de résistances PULLUP intégrées. Il faut donc les prévoir en externe. J'ai mis des 10K et ça fonctionne maintenant parfaitement.

    RépondreSupprimer
  6. Bonjour Henri, j’ai repris la suite de mon système d’arrosage après avoir installé les tuyaux et autres arroseurs.
    J’ai repris votre programme que j’ai essayé d’adapter avec des jours pairs impairs et personnalisés...
    J’ai profité de cet hiver pour m’améliorer en C (car j’étais complètement débutant) et je commence à avoir un résultat sympa. Je vais encore essayer d’implémenter une fonction pour rendre une voie inactive temporairement (pendant un barbecue par exemple) et une autre pour ajuster la durée d’arrosage en fonction de la saison (25% à 200%). Si cela vous intéresse je pourrai vous transmettre mon code (que j’ai pu compléter grâce à votre travail initial dont je vous remercie au passage). J’ai juste utilisé un écran SSD1327 au lieu du SSD1306...
    Par contre je rencontre le même problème que vous pour le capteur de débit... À l’arrêt il n’indique pas forcément 0. Avez-vous pu trouver la cause ?
    Enfin dans votre code sauf erreur de ma part, je pense que la fonction watering:run à un plusieurs bugs. Elle ne démarre pas quant il faut et lorsqu’elle le fait, toute les minutes, le(s) relais concernés s’éteignent et se rallument. En tout cas sur ma carte électronique c’est ce qu’il se passe... J’ai modifié la fonction et maintenant ça fonctionne, mais je pense que vous devriez y jeter un coup d’œil pour confirmer ou infirmer mes dires.
    Avez-vous pu avancer de votre côté cet hiver ?
    Encore merci et au plaisir de vous lire.

    RépondreSupprimer
    Réponses
    1. Bonjour.
      Oui j'ai pu avancer un peu. J'ai aussi ajouté une possibilité d'arroser tous les X jours.

      Supprimer
    2. Bonjour Rodolphe,
      J'ai le même problème pour l'arrosage auto. Vous pouvez me donner vos modifications svp ?
      Cdlt

      Supprimer
    3. Bonjour, mon code est très largement modifié car j'ai intégré de nombreuses fonctions supplémentaires ainsi qu'une interface WEB entièrement nouvelle. Du coup je veux bien vous le transmettre mais il vous faudra tout reprendre à zéro. D'après mon analyse (de petit programmeur) le problème vient de startTime. J'ai utilisé une autre approche pour le déterminer à savoir endTime - wateringTime ce qui évite au code d'Henry de passer à J+1 dès que l'heure de démarrage est atteinte.

      Supprimer
    4. Je veux bien regarder votre solution. (messenger : m.me/bertrand.muller.96).

      Supprimer
    5. https://github.com/rgodin974/ESP32_sprinkler_timer

      Supprimer
    6. Je ne trouve pas le programme principal "".ino

      Supprimer
    7. C'est normal c'est codé avec VS Code en C/C++ classique... Donc c'est dans le fichier main.cpp

      Supprimer
    8. J'ai trouvé la solution au pb d'arrosage auto... voir plus loin.

      Supprimer
  7. Y a-t-il un moyen de vous envoyer des photos de mon interface WEB que je me suis amusé à bien finaliser ?

    RépondreSupprimer
    Réponses
    1. Il y a différents moyens :
      - google photos, puis partager avec moi
      - facebook (chercher Henri Bachetti) mais il faut avoir un compte.
      - etc.

      Supprimer
    2. Bonjour je vous ai envoyé les photos par Messenger...

      Supprimer
  8. Bonjour,
    J'utilise votre projet que je trouve super ! J'ai rajouté la gestion de remplissage d'une cuve via une pompe déclenchée en 433 MHZ et le déclenchement de l'arrosage via une deuxième pompe.
    J'ai créé un point d'accès pour l'ESP32 car j'ai pas internet au jardin, j'aimerai utiliser un module RTC DS1302 en cas de coupure de courant. Comment faire en sorte que votre projet fonctionne sans internet et donc sans serveur sntp, et comme vous avez tout codé avec time()... Avez-vous possibilité de m'aider svp.
    Merci d'avance.

    RépondreSupprimer
    Réponses
    1. Tout d'abord j'utiliserais plutôt un DS3231, plus stable et compensé en température.
      Pour la mise à l'heure du RTC, quelques lignes suffisent, en utilisant RTClib :
      #include "RTClib.h"
      RTC_DS3231 rtc;
      void setup()
      {
      if (!m_rtc.begin()) {
      log_println(F("Couldn't find RTC"));
      }
      m_rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
      }
      void loop() {}
      Ensuite dans mon code il suffit de remplacer time() par rtc.now().unixtime().

      Supprimer
    2. ok merci !
      Par ailleurs l'arrosage automatique ne fonctionne pas, à l'heure programmée d'arrosage auto, on passe à J+1, et la vanne ne s'ouvre pas.
      Des pistes ? Merci.

      Supprimer
    3. Pas vraiment. Je vais essayer de voir pour intégrer la possibilité de se passer de NTP, à l'aide d'une option.

      Supprimer
    4. j'ai le même problème avec ton programme non modifié.

      Supprimer
    5. 12:34:31.583 -> aromatique.aromatique: next start 12/07/2024 12:33:00
      12:34:31.583 -> aromatique.aromatique next stop 11/07/2024 12:35:00
      12:34:31.583 -> t watering = 1720701268
      12:34:31.583 -> startTime = 1720787580
      12:34:31.583 -> stopTime = 1720701300
      12:34:31.583 -> aromatique.aromatique: before time, schedule in 23:58:32

      Supprimer
    6. Y aurait-il un pb d'arrosage auto dans ton programme original ?

      Supprimer
    7. j'ai fait quelques modif dans watering.cpp et l'arrosage auto se met bien en route puis s'arrête à l'heure de fin, et on peut mettre plusieurs plages par voie "way" :
      // run the watering
      bool Watering::run(time_t t)
      {
      struct tm *pTime;

      Serial.println();
      pTime = localtime(&t);
      int toDay = pTime->tm_mday;
      for (int i = 0 ; i < MAX_WATERING ; i++) {
      Watering *w = &m_watering[i];
      if (w->getDuration() != 0) {
      char buffer[MAX_BUF];
      time_t startTime = w->getStartTime(t);
      pTime = localtime(&startTime);
      strftime(buffer, MAX_BUF, "%d/%m/%Y %H:%M:%S", pTime);
      Serial.printf("%s: next start %s\n", w->getWayName(), buffer);
      time_t stopTime = w->getStopTime(t);
      pTime = localtime(&stopTime);
      strftime(buffer, MAX_BUF, "%d/%m/%Y %H:%M:%S", pTime);
      Serial.printf("%s next stop %s\n", w->getWayName(), buffer);
      if (t < startTime) {
      time_t d = startTime - t;
      int h = d / 3600;
      d %= 3600;
      int m = d / 60;
      d %= 60;
      int s = d;
      if (toDay != pTime->tm_mday) {
      Serial.printf("%s: after time, schedule in %02d:%02d:%02d\n", w->getWayName(), h, m, s);
      // w->autoStop();
      }
      else {
      Serial.printf("%s: before time, schedule in %02d:%02d:%02d\n", w->getWayName(), h, m, s);
      }
      }
      if (toDay == pTime->tm_mday && t >= startTime && t <= stopTime)
      {
      Serial.printf("In time, open relay %s\n", w->m_way->getRelay()->getName());
      w->autoStart();
      }
      if (toDay == pTime->tm_mday && t > stopTime)
      { w->autoStop();
      }
      }
      }
      if (isAnyWateringRunning() == false)
      {
      Valve::getMainValve()->close();
      }
      else
      {
      Valve::getMainValve()->open();
      }
      return true;
      }

      et ceci :

      // Exemple de fonction autoStart avec des journaux de débogage
      void Watering::autoStart() {
      int moisture;

      Serial.printf("Watering::autoStart %s: %ld\n\n", getWayName(), m_duration);
      m_autoStarted = now();
      if (m_moisture == 0) {
      m_moisture = getSoilMoisture(&moisture);
      if (m_moisture == HUMIDITY_DRY) {
      Serial.printf("Watering::autoStart: moisture %x (DRY)\n", moisture);
      m_way->open();
      } else {
      Serial.printf("Watering::autoStart: moisture %x (WET)\n", moisture);
      }
      }
      }



      void Watering::autoStop() {
      Serial.printf("Watering::autoStop %s\n\n", getWayName());
      m_autoStarted = 0;
      m_moisture = 0;
      if (!m_way->manualStarted(NULL)) {
      Serial.printf("close relay %s\n", m_way->getRelay()->getName());
      m_way->close();
      } else {
      Serial.printf("Relay %s remains open (manual start detected)\n", m_way->getRelay()->getName());
      }
      }

      Supprimer
    8. // return the watering's start time
      time_t Watering::getStartTime(time_t now) {
      struct tm *pTime;

      pTime = localtime(&now);
      // Serial.printf("Current hour: %d, Current minute: %d\n", pTime->tm_hour, pTime->tm_min);
      pTime->tm_hour = m_hour;
      pTime->tm_min = m_minute;
      pTime->tm_sec = 0;
      time_t at = mktime(pTime);
      if (at+ (m_duration*60) < now) {
      at += DAY_DURATION;
      }
      // Serial.printf("Start time: %ld\n", at);
      return at;
      }

      // return the watering's stop time
      time_t Watering::getStopTime(time_t now) {
      struct tm *pTime;

      pTime = localtime(&now);
      pTime->tm_hour = m_hour + (m_duration / 60);
      pTime->tm_min = m_minute + (m_duration % 60);
      pTime->tm_sec = 0;
      time_t at = mktime(pTime);
      if (at+10 < now) {
      at += DAY_DURATION;
      }
      // Serial.printf("Stop time: %ld\n", at);
      return at;
      }

      Supprimer
    9. Salut.
      Il m'est assez difficile de comparer mon code avec le tien, étant donné que le mien a également évolué de son côté.
      J'ai en effet ajouté une notion d'intervalle d'arrosage en jours, ce qui permet des arrosages tous les 2 jours par exemple.

      Supprimer
    10. Salut,

      J'ai uniquement changé 3, 4 lignes de ton code.
      Peux tu actualiser ton code sur le site stp.
      Merci et a+

      Supprimer