Affichage des articles dont le libellé est Communication. Afficher tous les articles
Affichage des articles dont le libellé est Communication. Afficher tous les articles

mardi 20 juillet 2021

Les Expressions Régulières



Les Expressions Régulières

 

Aujourd'hui nous allons aborder un sujet inconnu de la plupart des amateurs : les expressions régulières.

Qu'est ce qu'une expression régulière ? C'est un motif, ou modèle, décrivant une chaîne de caractères.

Le but des expressions régulières est :

  • reconnaître la syntaxe d'une phrase
  • extraire des mots d'une phrase
  • remplacer des mots dans une phrase

1. Exemple

Que peut-on espérer des expressions régulières dans le monde ARDUINO, attendu que l'on a rarement l'occasion d'examiner des phrases ? En fait, une phrase peut très bien provenir d'un capteur, d'un dispositif intelligent, et dans ce cas on appelle cette phrase une trame.

Partons d'un exemple concret. Imaginons que nous ayons à implémenter l'analyse lexicale d'une chaîne de caractères provenant d'un système de mesure de tension et courant qui envoie périodiquement sur une ligne série des trames du genre de celle-ci :

<20/07/2021 17:55:04> 10.31V, 1.32A

Notre intention est d'extraire les valeurs de cette chaîne :

  • date
  • heure
  • tension
  • courant

La première idée qui vient à l'esprit est de rechercher les séparateurs :

  • < et >
  • espace et virgule
Il faut également examiner les caractères pour déterminer leur nature :
  • chiffre
  • point décimal
  • lettre minuscule, majuscule

1.1. strtok()

Voici la manière habituelle de découper une phrase en mots avec strtok() :

    char str[] = "- Voici une phrase avec quelques séparateurs ! -";
    const char * separators = " ,.-!";

    char * strToken = strtok(str, separators);
    while (strToken != NULL) {
        Serial.println(strToken);
        strToken = strtok(NULL, separators);
    }

On peut rapidement constater que la méthode classique qui consiste à utiliser la fonction strtok() n'est pas tout à fait adaptée à notre cas. Son prototype est le suivant :

char *strtok(char *str, const char *delim);

strtok() a besoin de délimiteurs, or au sein des mots 10.31V et 1.32A il n'y a pas de séparateur entre la valeur et l'unité.

Il faudrait donc considérer que 10.31V et 1.32A sont deux valeurs à extraire. Il ne restera plus qu'à utiliser atof() pour récupérer les valeurs 10.31 et 1.32 :

char phrase[] = "<20/07/2021 17:55:04> 10.31V, 1.32A";

void setup() {
  Serial.begin(115200);
  int day, month, year, hour, minute, second;
  char voltage[10], current[10];
  float volt, amp;
 
  char *p = strtok(phrase, "</");
  if (p == NULL) {
    Serial.println("manque </");
    return;
  }
  day = atoi(p);
  p = strtok(NULL, "/");
  if (p == NULL) {
    Serial.println("manque /");
    return;
  }
  month = atoi(p);
  p = strtok(NULL, "/ ");
  if (p == NULL) {
    Serial.println("manque / et espace");
    return;
  }
  year = atoi(p);
  p = strtok(NULL, ":");
  if (p == NULL) {
    Serial.println("manque :");
    return;
  }
  hour = atoi(p);
  p = strtok(NULL, ":");
  if (p == NULL) {
    Serial.println("manque :");
    return;
  }
  minute = atoi(p);
  p = strtok(NULL, ":>");
  if (p == NULL) {
    Serial.println("manque :>");
    return;
  }
  second = atoi(p);
  p = strtok(NULL, " ,");
  if (p == NULL) {
    Serial.println("manque virgule");
    return;
  }
  strcpy(voltage, p);
  voltage[strlen(voltage)-1] = '\0';
  volt = atof(voltage);
  p = strtok(NULL, " ");
  if (p == NULL) {
    Serial.println("manque espace");
    return;
  }
  strcpy(current, p);
  current[strlen(voltage)-1] = '\0';
  amp = atof(current);

  Serial.println(day);
  Serial.println(month);
  Serial.println(year);
  Serial.println(hour);
  Serial.println(minute);
  Serial.println(second);
  Serial.println(volt);
  Serial.println(amp);
}

void loop() {
}

Voici le résultat :

20
07
2021
17
55
04
10.31
1.32 

On remarque que la syntaxe de la phrase est vérifiée. Si elle n'est pas exactement celle prévue, rien ne sera affiché.

1.2. sscanf()

Il serait possible d'utiliser sscanf() :

char phrase[] = "<20/07/2021 17:55:04> 10.31V, 1.32A";

void setup() {
  Serial.begin(115200);
  int day, month, year, hour, minute, second;
  char voltage[10], current[10];
  float volt, amp;
  int n = sscanf(phrase, "<%d/%d/%d %d:%d:%d> %s %s>", &day, &month, &year, &hour, &minute, &second, voltage, current);
  if (n == 8) {
    volt = atof(voltage);
    amp = atof(current);
    Serial.println(day);
    Serial.println(month);
    Serial.println(year);
    Serial.println(hour);
    Serial.println(minute);
    Serial.println(second);
    Serial.println(volt);
    Serial.println(amp);
  }
}

void loop() {
}

Voici le résultat :

20
7
2021
17
55
4
10.31
1.32

sscanf() est incapable d'interpréter de manière simple certains séparateurs, par exemple la virgule. Par contre le code est un peu plus concis.

sscanf() est également capable de travailler sur des jeux de caractères : %[set].

char phrase[] = "<20/07/2021 17:55:04> 10.31V, 1.32A";

void setup() {
  Serial.begin(115200);
  char day[3], month[3], year[5], hour[3], minute[3], second[3];
  char voltage[10], current[10];
  float volt, amp;
  int n = sscanf(phrase, "<%[0-9]/%[0-9]/%[0-9] %[0-9]:%[0-9]:%[0-9]> %[0-9.]V, %[0-9.]A>", day, month, year, hour, minute, second, voltage, current);
  Serial.println(n);
  if (n == 8) {
    volt = atof(voltage);
    amp = atof(current);
    Serial.println(day);
    Serial.println(month);
    Serial.println(year);
    Serial.println(hour);
    Serial.println(minute);
    Serial.println(second);
    Serial.println(volt);
    Serial.println(amp);
  }
}

void loop() {
}

Dans cet exemple :

  • %[0-9] représente un nombre entier composé de N chiffres de 0 à 9
  • %[0-9.] représente un nombre flottant composé de N chiffres de 0 à 9 avec éventuellement un point

Il y a un inconvénient : les données extraites (day, month, year, hour, minute, second) sont rangées dans des chaînes de caractères. Si l'on a besoin de leur valeur numérique il faudra les convertir avec atoi().

Dans les deux cas, la valeur retournée de sscanf() est comparée à 8, le nombre de mots à extraire. Si la syntaxe n'est pas celle prévue, rien ne sera affiché.

1.3. Analyseur sur mesure

Pour analyser cette chaîne, classiquement, nous ferions les opérations suivantes en déplaçant un pointeur ou un index dans la chaîne :

char phrase[] = "<20/07/2021 17:55:04> 10.31V, 1.32A";

void setup() {
  Serial.begin(115200);
  int day, month, year, hour, minute, second;
  float voltage, current;
  char *p = phrase;
  if (*p == '<') {
    p++;
    day = atoi(p);
    while (isdigit(*p)) p++;
    if (*p == '/') {
      p++;
      month = atoi(p);
      while (isdigit(*p)) p++;
      if (*p == '/') {
        p++;
        year = atoi(p);
        while (isdigit(*p)) p++;
        if (*p == ' ') {
          p++;
          hour = atoi(p);
          while (isdigit(*p)) p++;
          if (*p == ':') {
            p++;
            minute = atoi(p);
            while (isdigit(*p)) p++;
            if (*p == ':') {
              p++;
              second = atoi(p);
              while (isdigit(*p)) p++;
              if (*p == '>') {
                p++;
                if (*p == ' ') {
                  p++;
                  voltage = atof(p);
                  while (isdigit(*p) || *p == '.') p++;
                  if (*p == 'V') {
                    p++;
                    if (*p == ',') {
                      p++;
                      if (*p == ' ') {
                        p++;
                        current = atof(p);
                        while (isdigit(*p)) p++;

                        Serial.println(day);
                        Serial.println(month);
                        Serial.println(year);
                        Serial.println(hour);
                        Serial.println(minute);
                        Serial.println(second);
                        Serial.println(voltage);
                        Serial.println(current);
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

void loop() {
}

Voici le résultat :

20
07
2021
17
55
04
10.31
1.32 

Cette fois-ci nous obtenons un contrôle parfait sur chaque caractère, mais quelle longueur de code !

On remarque aussi que la syntaxe de la phrase est vérifiée. Si elle n'est pas exactement celle prévue, rien ne sera affiché.

1.4. Expression régulière

N' y a t'il pas plus simple ?

Si, bien sûr. En utilisant une analyse lexicale on pourrait faire cette opération en une seule fois.

Une description de cette chaîne pourrait être celle-ci :

<chiffres,/,chiffres,/,chiffres,espace,chiffres,:,chiffres,:,chiffres>

chiffres&point,V,virgule,espace,chiffres&point,A

Ensuite il faudrait donner à manger cette description et la phrase à analyser à un bout de logiciel qui nous dirait si la phrase correspond à la description, et si oui, extraire les données.

2. La librairie

La librairie de Nick Gammon (un personnage connu), installable depuis le gestionnaire de bibliothèques de l'IDE ARDUINO, est celle-ci :

https://github.com/nickgammon/Regexp

C'est une librairie qui se rapproche fortement de celles que l'on a l'habitude d'utiliser sur PC, en C, C++, PYTHON, ou d'autres langages.

2.1. Les exemples

Les exemples de la librairie sont assez pauvres. A part extraire tous les mots d'une phrase comme "The quick brown fox jumps over the lazy wolf", ils ne sont pas d'une aide suffisante. Cela justifie cet article.

2.2. Allons un peu plus loin

2.2.1. Présentation

Ce qui suit est une explication de cette page :

http://www.gammon.com.au/scripts/doc.php?lua=string.find 

Tout d'abord présentons les caractères spéciaux utilisés, ceux qui ont une signification particulière dans la description de la grammaire :

  • ^ : le début de la phrase
  • $ : la fin de la phrase
  • ( ) : encadre une capture (une extraction de données)
  •  % : introduit une classe de caractères
    • %a : des lettres
    • %d : des chiffres
  • . : représente n'importe quel caractère
  • [ ] : encadre un jeu de caractères
    • [a-z] : une minuscule
    • [A-Z] : une majuscule
    • [0-9] : un chiffre
  • * : exprime une répétition
    • [a-z]* : plusieurs minuscules (y compris aucune)
    • [A-Z]* : plusieurs majuscules (y compris aucune) 
    • [0-9]* : plusieurs chiffres (y compris aucun)
  • + : exprime une répétition
    • [a-z]+ : plusieurs minuscules (au moins une)
    • [A-Z]+ : plusieurs majuscules (au moins une)
    • [0-9]+ : plusieurs chiffres (au moins un) 
  • - : exprime une répétition, comme + mais en extrayant le moins possible de caractères
  • ? : exprime une répétition (zéro ou une)

Le caractère % est utilisé également pour échapper un caractère spécial

  • . : représente n'importe quel caractère
  • %. : représente un point
  • %( : représente une parenthèse ouvrante

Comment pourrions-nous décrire notre phrase précédente ?

<20/07/2021 17:55:04> 10.31V, 1.32A

Comme ceci :

<([0-9]+/[0-9]+/[0-9]+) ([0-9]+:[0-9]+:[0-9]+)> ([0-9%.]+)V, ([0-9%.]+)A

Examinons chaque élément de cette description :

  • < : nous attendons un caractère <
  • ([0-9]+/[0-9]+/[0-9]+) : nous voulons extraire un mot composé de 3 groupes de plusieurs nombres séparés par un slash (/)
  • ([0-9]+:[0-9]+:[0-9]+) : nous voulons extraire un mot composé de 3 groupes de plusieurs nombres séparés par deux points (:)
  • > : nous attendons un caractère >
  • nous attendons un espace
  • ([0-9%.]+) : nous voulons extraire un mot composé de plusieurs chiffres et éventuellement d'un point, donc un nombre flottant
  • V : nous attendons une lettre V
  • , : nous attendons une virgule suivie d'un espace
  • ([0-9%.]+) : nous voulons extraire un mot composé de plusieurs chiffres et éventuellement d'un point, donc un nombre flottant
  • A : nous attendons une lettre A

2.2.2. Le code exemple

Le code présenté est inspiré de celui-ci :

https://github.com/nickgammon/Regexp/blob/master/examples/GlobalMatch/GlobalMatch.pde

Le voici :

#include <Regexp.h>

char phrase[] = "<20/07/2021 17:55:04> 10.31V, 1.32A";
char regexp[] = "<([0-9]+/[0-9]+/[0-9]+) ([0-9]+:[0-9]+:[0-9]+)> ([0-9%.]+)V, ([0-9%.]+)A";

void match_callback  (const char * match, const unsigned int length, const MatchState & ms)
{
  char cap [10];   // must be large enough to hold captures

  Serial.print ("Matched: ");
  Serial.write ((byte *) match, length);
  Serial.println ();
  for (byte i = 0; i < ms.level; i++) {
    Serial.print ("Capture");
    Serial.print (i);
    Serial.print ("=");
    ms.GetCapture (cap, i);
    Serial.println (cap);
  }
}

void setup() {
  MatchState ms (phrase);
  unsigned long count;

  Serial.begin(115200);
  count = ms.GlobalMatch(regexp, match_callback);
  Serial.print("Found ");
  Serial.print(count);
  Serial.println(" matches.");
}

void loop() {
}

La variable phrase représente la phrase à analyser. La variable regexp représente l'expression régulière.

Exécutons ce code :

Matched: <20/07/2021 17:55:04> 10.31V, 1.32A
Capture0=20/07/2021
Capture1=17:55:04
Capture2=10.31
Capture3=1.32
Found 1 matches.

1 correspondance a été trouvée (Found 1 matches), et 4 captures ont été réalisées :

  • la date
  • l'heure
  • la tension
  • le courant

La date est l'heure sont extraites en deux mots. Si l'on a besoin d'extraire le jour, le mois, l'année, l'heure, les minutes et les secondes séparément, il suffit d'encadrer chaque élément [0-9]+ par des parenthèses :

char regexp[] = "<([0-9]+)/([0-9]+)/([0-9]+) ([0-9]+):([0-9]+):([0-9]+)> ([0-9%.]+)V, ([0-9%.]+)A";

Le résultat sera :

Matched: <20/07/2021 17:55:04> 10.31V, 1.32A
Capture0=20
Capture1=07
Capture2=2021
Capture3=17
Capture4=55
Capture5=04
Capture6=10.31
Capture7=1.32
Found 1 matches. 

On pourrait corser la chose. Si le système de mesure est capable de travailler avec des unités autres que des Volts ou des Ampères, des millivolts ou de milliampères par exemple :

char phrase[] = "<20/07/2021 17:55:04> 10.31mV, 1.32mA";
char regexp[] = "<([0-9]+/[0-9]+/[0-9]+) ([0-9]+:[0-9]+:[0-9]+)> ([0-9%.]+)([a-zA-Z]+), ([0-9%.]+)([a-zA-Z]+)";

A la place de A ou V nous attendons plusieurs lettres, minuscule ou majuscule, et les parenthèses permettent de les extraire (capture) :

Matched: <20/07/2021 17:55:04> 10.31mV, 1.32mA
Capture0=20/07/2021
Capture1=17:55:04
Capture2=10.31
Capture3=mV
Capture4=1.32
Capture5=mA
Found 1 matches.

Notre résultat pourra facilement être exploité car nous avons à disposition toutes les informations nécessaires, mesures et unités.

La grammaire peut être rendue plus tolérante. dans l'exemple qui suit, les caractères < et > peuvent être remplacés par [ et ] :

char phrase[] = "[20/07/2021 17:55:04] 10.31mV, 1.32mA";
char regexp[] = "[<%[]([0-9]+/[0-9]+/[0-9]+) ([0-9]+:[0-9]+:[0-9]+)[>%]] ([0-9%.]+)([a-zA-Z]+), ([0-9%.]+)([a-zA-Z]+)";

explication de [<%[] et [>%]] :

On voit que les caractères [ et ] sont échapés à l'aide du caractère %, sinon ils seraient pris pour des caractères spéciaux.

Avec cette description, la date et l'heure peuvent être encadrées soit par < et > soit par [ et ].

Donc ces deux phrases donneront le même résultat :

char phrase[] = "<20/07/2021 17:55:04> 10.31mV, 1.32mA";
char phrase[] = "[20/07/2021 17:55:04] 10.31mV, 1.32mA";

Matched: [20/07/2021 17:55:04] 10.31mV, 1.32mA
Capture0=20/07/2021
Capture1=17:55:04
Capture2=10.31
Capture3=mV
Capture4=1.32
Capture5=mA
Found 1 matches.

Si l'information <> ou [] a une signification, et qu'elle doit être prise en compte il suffit d'encadrer par des parenthèses :

char regexp[] = "([<%[])([0-9]+/[0-9]+/[0-9]+) ([0-9]+:[0-9]+:[0-9]+)([>%]]) ([0-9%.]+)([a-zA-Z]+), ([0-9%.]+)([a-zA-Z]+)";

Matched: [20/07/2021 17:55:04] 10.31mV, 1.32mA
Capture0=[
Capture1=20/07/2021
Capture2=17:55:04
Capture3=]
Capture4=10.31
Capture5=mV
Capture6=1.32
Capture7=mA
Found 1 matches.

Un dernier mot à propos des caractères spéciaux :

  • %a : représente une lettre, minuscule ou majuscule (équivalent à [a-zA-Z])
  • %d : représente un chiffre (équivalent à [0-9])
  • %l : représente une lettre minuscule (équivalent à [a-z])
  • %u : représente une lettre majuscule (équivalent à [A-Z])
  • %w : représente une lettre ou un chiffre (équivalent à [a-zA-Z0-9])
  • etc.

La liste complète ici : http://www.gammon.com.au/scripts/doc.php?lua=string.find 

Notre dernière expression peut donc être réduite :

char regexp[] = "([<%[])([0-9]+/[0-9]+/[0-9]+) ([0-9]+:[0-9]+:[0-9]+)([>%]]) ([0-9%.]+)([a-zA-Z]+), ([0-9%.]+)([a-zA-Z]+)";

Est équivalent à :

char regexp[] = "([<%[])([%d]+\/[%d]+\/[%d]+) ([%d]+:[%d]+:[%d]+)([>%]]) ([%d%.]+)([%a]+), ([%d%.]+)([%a]+)";

Elle donnera exactement le même résultat.

2.2.3. Mise au point

Si la phrase à analyser est longue et complexe, il est difficile d'écrire l'expression régulière d'une seule traite sans se tromper. On peut le faire étape par étape. On peut commencer par la date seule :

char phrase[] = "<20/07/2021 17:55:04> 10.31mV, 1.32mA";

char regexp[] = "([<%[])([0-9]+/[0-9]+/[0-9]+)";

Matched: [20/07/2021
Capture0=[
Capture1=20/07/2021
Found 1 matches.

Ensuite, si tout va bien, ajoutons l'heure :

char regexp[] = "([<%[])([0-9]+/[0-9]+/[0-9]+) ([0-9]+:[0-9]+:[0-9]+)([>%]])";

Matched: [20/07/2021 17:55:04]
Capture0=[
Capture1=20/07/2021
Capture2=17:55:04
Capture3=]
Found 1 matches.

Et ainsi de suite ...

2.2.4. Mise au point en ligne

Des sites WEB permettent de tester une expression régulière, par exemple regex101.com.

Mais il faut savoir que les expressions régulières traditionnelles sont légèrement différentes des expressions régulières de Nick Gammon :

  • les caractères spéciaux sont plus nombreux (slash / en est un)
  • le caractère spécial % est remplacé par backslash : \

Pour notre exemple j'ai simplement remplacé :

  • %[ par \[ et %] par \]
  • %. par \.
  • / par \/

La version de test sur regex101 devient :

([<\[])([0-9]+\/[0-9]+\/[0-9]+) ([0-9]+:[0-9]+:[0-9]+)([>\]]) ([0-9\.]+)([a-zA-Z]+), ([0-9\.]+)([a-zA-Z]+)

Dans la fenêtre en bas à droite, on constate que la phrase est comprise et que les différents éléments sont extraits.

Après avoir testé la version WEB, Dans notre skecth il faut copier / coller l'expression, et faire les remplacements inverses :

char regexp[] = "([<%[])([0-9]+\/[0-9]+\/[0-9]+) ([0-9]+:[0-9]+:[0-9]+)([>%]]) ([0-9%.]+)([a-zA-Z]+), ([0-9%.]+)([a-zA-Z]+)";

Ce n'est pas une opération énorme, et avec un peu d'habitude on y parvient facilement.

2.2.5. Test avec un terminal

Si l'on désire faire des tests sur ARDUINO avec différentes phrases entrées dans un terminal série ou le moniteur serie, il suffit d'adapter un peu. Voici le premier exemple modifié :

#include <Regexp.h>

// enter a string like : <20/07/2021 17:55:04> 10.31V, 1.32A
char phrase[40];
char regexp[] = "<([0-9]+)/([0-9]+)/([0-9]+) ([0-9]+):([0-9]+):([0-9]+)> ([0-9%.]+)V, ([0-9%.]+)A";

void match_callback  (const char * match, const unsigned int length, const MatchState & ms)
{
  char cap [10];   // must be large enough to hold captures

  Serial.print ("Matched: ");
  Serial.write ((byte *) match, length);
  Serial.println ();
  for (byte i = 0; i < ms.level; i++) {
    Serial.print ("Capture");
    Serial.print (i);
    Serial.print ("=");
    ms.GetCapture (cap, i);
    Serial.println (cap);
  }
}

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

void loop() {
  if (Serial.available()) {
    Serial.readBytesUntil('\n', phrase, 40);
    MatchState ms (phrase);
    unsigned long count;

    count = ms.GlobalMatch(regexp, match_callback);
    Serial.print("Found ");
    Serial.print(count);
    Serial.println(" matches.");
  }
}

3. Conclusion

En matière de développement informatique on arrive rarement à réaliser de grandes chose en restant cantonné dans son petit code personnel et ses petites habitudes, sans apprendre des autres.

L'utilisation des librairies ARDUINO simplifient grandement la tâche d'écriture de pilotes matériels lorsque l'on doit exploiter des composants et des modules.

Il en va de même pour le code pur. Plutôt que de s'enfermer dans une solution personnelle, ce qui revient souvent à s'engager dans une voie sans issue lorsque la complexité est importante, il est souvent préférable d'utiliser le travail des autres, surtout lorsque ce travail existe depuis des dizaines d'années.

C'est le cas pour les expressions régulières : elles sont nées en 1940.

Les expressions régulières peuvent sembler rébarbatives, mais le gain qu'elles apportent en terme de temps de développement est inestimable par rapport au temps passé en apprentissage.

En deux mots : la curiosité est un excellent défaut. Soyez curieux !


Cordialement

Henri



dimanche 23 août 2020

ARDUINO + ESP8266 : est-ce bien raisonnable ?

 

ARDUINO + ESP8266 :

est-ce bien raisonnable ?

 

En 2014 le premier ESP8266 dit ESP01 sortait. Ce microcontrôleur disposait de seulement 2 GPIOs. Il était utilisé principalement couplé à une carte ARDUINO, afin de bénéficier des nombreuses entrées / sorties de celle-ci.

Pour ce faire la majorité des cartes ESP01 étaient équipées d'un logiciel dit "FIRMWARE AT", permettant le dialogue avec l'ARDUINO.

Le FIRMWARE AT reçoit des commandes de la part de l'ARDUINO par la ligne série et lui renvoie un résultat :

  • AT+GMR : demande de la version
  • AT+CWMODE : fixe le mode (station, point d'accès)
  • AT+CWJAP : connexion à un point d'accès
  • etc.

Cette technologie est-elle encore viable à l'heure où de nombreuses cartes ESP8266 et ESP32, beaucoup plus riches qu'un ESP01, existent sur le marché ?

Que choisir ? un ARDUINO + ESP01, ou un ESP8266 / ESP32 tout seul ?

Nous allons essayer de mettre en œuvre les deux solutions, et nous aider de deux petits exemples de serveur WEB sur ESP32 afin de montrer les différences.

1. L'ESP01

1.1. ESP01 + convertisseur USB

Nous allons dans un premier temps utiliser l'ESP01 seul, en le reliant au PC  à l'aide d'un convertisseur USB / série.

Rappelons le brochage d'un ESP01 :

Il faut impérativement utiliser un convertisseur USB / série 3.3V ou possédant un switch ou un cavalier 5V / 3.3V. Je conseille plutôt un FT232R :


Seulement 4 fils sont nécessaires :

FT232R ESP01
VCC VCC
GND GND
TX RX
RX TX

Le premier point à vérifier est la présence du FIRMWARE AT.

On peut pour cela utiliser le moniteur série de l'IDE ARDUINO, avec les réglages suivants :

  • 115200 baud
  • fin de ligne NL + CR

Entrer la commande suivante :

AT

L'ESP01 doit répondre :

OK

Entrer la commande suivante :

AT+GMR

L'ESP01 doit répondre :

AT version:1.2.0.0(Jul  1 2016 20:04:45)
SDK version:1.5.4.1(39cb9a32)
v1.0.0
Mar 11 2018 18:27:31
OK

La version peut bien entendu être différente, en fonction de l'origine de la carte.

Si l'ESP01 ne répond pas, il est peut-être configuré avec un autre baudrate. Il faut essayer 9600 ou d'autres vitesses.

1.1.1. Recharger le FIRMWARE AT avec ESPTOOL

Si l'ESP01 ne répond toujours pas, il se peut que le FIRMWARE AT soit absent, soit il a été effacé par son propriétaire, soit remplacé par un autre sketch, soit il a été livré tel quel (ce qui est extrêmement rare).
Dans ce cas il faut le recharger. Il existe différents tutoriels sur le WEB dont la documentation "AT Instruction Set" d'ESPRESSIF :

https://www.espressif.com/sites/default/files/documentation/4a-esp8266_at_instruction_set_en.pdf

Voir : 1.2. Downloading AT Firmware into the Flash

Avant de commencer il est impératif de connaître le type de mémoire FLASH implanté sur la carte, le petit circuit à 8 pattes. Il vaut mieux disposer d'une bonne loupe :

Par exemple : t25s80 ou pn25F08 = 8Mbits

Ici un tutoriel utilisant ESPTOOL, fonctionnant sur toutes plateformes :

https://robertoostenveld.nl/esp8266-at-firmware/ 

ATTENTION : l'auteur utilise un ESP01 équipé de 512K de mémoire FLASH (FLASH 4Mbits).

Pour un ESP01 équipé d'une FLASH 8Mbits les commandes sont légèrement différentes, en particulier l'emplacement des données "esp_init_data_default" : voir la doc ESPRESSIF : 1.2.4. 8 Mbit Flash

Voici les commandes à entrer pour recharger  le FIRMWARE AT sur une carte 8Mbits (les cartes les plus courantes actuellement) :

esptool.py --port /dev/ttyUSB1 erase_flash
esptool.py --port /dev/ttyUSB1 --baud 115200 write_flash --flash_mode dio 0x00000 boot_v1.2.bin
esptool.py --port /dev/ttyUSB1 --baud 115200 write_flash --flash_mode dio 0x01000 at/512+512/user1.1024.new.2.bin
esptool.py --port /dev/ttyUSB1 --baud 115200 write_flash --flash_mode dio 0xFE000 blank.bin
esptool.py --port /dev/ttyUSB1 --baud 115200 write_flash --flash_mode dio 0x7E000 blank.bin
esptool.py --port /dev/ttyUSB1 --baud 115200 write_flash --flash_mode dio 0xFC000 esp_init_data_default_v05.bin

J'ai essayé pour vous : cela fonctionne parfaitement.

Où se trouve ESPTOOL ?

ESPTOOL est installé par défaut dans le répertoire suivant :

arduino15/packages/esp8266/hardware/esp8266/2.5.2/tools/esptool/esptool.py

Le répertoire arduino15 se trouve ici :

Sous Linux : /home/username/.arduino15

Sous Windows : C:\Users\username\AppData\Local\Arduino15

Sur Mac : /Users/username/Library/Arduino15

Il y a deux solutions : modifier la variable d'environnement PATH pour lui ajouter le chemin où est installé ESPTOOL, ou utiliser le chemin complet.

1.1.2. Recharger le FIRMWARE AT avec FLASH DOWNLOAD TOOL

D'autre tutoriels utilisent FLASH DOWNLOAD TOOL, une application graphique (Windows uniquement) :

https://www.instructables.com/id/Flash-or-Upgrade-Firmware-on-ESP8266-ESP-01-Module/

Travaillant sous Linux, je n'ai pas essayé ce logiciel.

1.2. ESP01 + ARDUINO

Les ressources WEB (tutoriels, forums) sont en général plutôt simplistes et se contentent de présenter la manière de connecter un ARDUINO et un ESP01, et d'envoyer une page HTML "Hello World", rarement plus, d'où une certaine frustration des débutants, parfaitement compréhensible.

Ce tutoriel par contre, beaucoup plus complet, vous enseignera l'art et la manière de développer un serveur WEB à l'aide de commandes AT :

Tutoriel de J-M-L du forum ARDUINO

On constatera assez facilement que le code est assez conséquent (400 lignes) :

https://forum.arduino.cc/index.php?action=dlattach;topic=501923.0;attach=227323

La page WEB développée est simple, mais elle comporte néanmoins des boutons, ce qui n'est pas le cas de la majorité des tutoriels rencontrés sur le WEB :

Sincèrement je ne pense pas que ce genre de développement soit accessible à un débutant. A chacun donc de se faire sa propre idée en fonction de ses compétences et de sa compréhension de ce tutoriel.

1.3.1. Le matériel

L'ESP01 doit être alimenté en 3.3V / 500mA minimum. Il convient donc de se procurer un régulateur adéquat.

D'autre par l'entrée RX de l'ESP01 n'est pas censée supporter les 5V de la sortie TX d'un ARDUINO. Un pont diviseur sera nécessaire :

L'auteur du tutoriel ci-dessus utilise un adaptateur permettant d'assurer les fonctions alimentation et adaptation de niveau :

Certains tutoriels présentent l'ESP01 alimenté par la broche 3.3V d'une carte UNO.

Or le 3.3V d'une carte UNO est délivré par un régulateur LP2985 capable de fournir 150mA au maximum, ce qui sera insuffisant pour établir une connexion WIFI qui réclame plus de 400mA.

Avec une carte NANO, dont le 3.3V est fourni par le chip CH340, le courant sera également insuffisant.

Il faudra également alimenter l'ARDUINO en 5V par sa broche VCC ou en 7.5V ou plus par sa broche VIN ou JACK, à moins d'opter pour une carte ARDUINO 3.3V, une PRO MINI 8MHz par exemple.

On voit bien que cette solution est déjà complexe rien que pour la partie hardware.

1.3.2. La ligne série

La ligne série utilisée pour le dialogue entre l'ESP01 et l'ARDUINO est généralement la ligne série hardware de l'ARDUINO (broches RX TX) .

Cela supprime donc toute possibilité d'utiliser la sortie standard de l'ARDUINO pour afficher des informations sur le terminal, et complique te chargement du sketch (il faut libérer RX TX).

Il est possible d'utiliser deux GPIOs et un objet du type SoftwareSerial afin de libérer la ligne série matérielle.

Avec une ligne SoftwareSerial on utilisera de préférence un baudrate plus faible (9600 ou 19200 baud, peut-être plus ?). Le baudrate de l'ESP01 peut être configuré à l'aide du moniteur série ARDUINO et de la commande AT+UART_DEF :

AT+UART_DEF=9600,8,1,0,3

Enfin, les utilisateurs de cartes MEGA2560 pourront utiliser une autre liaison série hardware Serial1, Serial2 ou Serial3, afin de réserver la ligne Serial au chargement et au terminal.

1.3.3. Les ressources HTML

Il faut bien comprendre que le code HTML est envoyé à l'ESP01 par l'ARDUINO, en plusieurs morceaux si nécessaire, et que la mémoire RAM de l'ARDUINO est limitée : 2Ko (8Ko pour une MEGA). C'est peu.

Si l'on envisage de stocker des fichier ressources (HTML, JS, images), l'ARDUINO n'aura d'autre choix que d'embarquer une mémoire externe (µSD, FLASH SPI, etc.).

Le code devra assurer l'intégralité de la gestion de ces ressources et la taille mémoire de l'ARDUINO sera vite un handicap.

On comprend vite qu'un serveur WEB développé avec cette technique sera très limité.

1.3. ESP01 seul

L'ESP01 seul a des ressources limitées :

  • 2 GPIOs
  • I2C possible sur ces 2 GPIOs

Il sera difficile de raccorder plus d'un ou deux modules :

  • relais
  • LEDs
  • boutons
  • afficheur I2C, expander

Les possibilités matérielles sont assez limitées mais par contre l'ESP01 dispose de toutes les ressources d'un ESP8266 classique : capacité de calcul, mémoire RAM, FLASH, qu'un ARDUINO aura du mal à concurrencer.

Pour résumer si les deux GPIOs suffisent autant utiliser l'ESP01 seul, sans ARDUINO.

1.4. Carte UNO WIFI

Depuis quelques années des cartes toutes intégrées sont disponibles sur le marché :

Ces cartes UNO sont équipées d'un ESP01 et le connecteur USB permet de communiquer soit avec l'ATMEGA328 soit avec l'ESP01, en fonction de la position de DIP-SWITCHES. Les broches de l'ESP01 sont disponibles : voir en haut à droite.

Il est fortement conseillé de lire la documentation :

https://robotdyn.com/uno-wifi-r3-atmega328p-esp8266-32mb-flash-usb-ttl-ch340g-micro-usb.html

Cette carte a toutefois un inconvénient : il faut sans arrêt changer de configuration :

  • CH340 sur l'ATMEGA328 : chargement du sketch
  • ESP01 sur l'ATMEGA328 : exécution

Il faut donc basculer 4 switches à chaque fois. Galère ... 

On peut opter également pour une liaison SoftwareSerial en ajoutant deux fils, ce qui aura aussi un inconvénient : la vitesse réduite.

1.5. Firmware maison

Qu'il s'agisse d'un couple ARDUINO + ESP01 ou d'une carte ARDUINO WIFI rien n'empêche quiconque de développer pour ses propres besoins un firmware spécialisé afin de communiquer avec d'autres moyens qu'HTTP : MQTT par exemple, ou UDP.

Il est parfaitement possible également d'imaginer un FIRMWARE intelligent qui stockerait les pages HTML dans la mémoire FLASH de l'ESP01, et proposerait une interface permettant de les afficher en passant le nom de la page, avec une liste d'arguments variables, un peu comme on le ferait avec un écran NEXTION.

Mais dans ce cas on se demande quelle est la valeur ajoutée de l'ARDUINO ?
Un ESP8266 ou ESP32 ferait tout cela certainement mieux.

2. L'ESP8266 et l'ESP32

2.1. ESP8266

 
Une carte ESP8266 (D1 MINI par exemple) possède un certain nombre d'entrées sorties :
  • GPIOs (leur nombre dépend de la carte)
  • I2C
  • SPI
  • 1 entrée analogique

On peut examiner cette page afin de déterminer quelle carte sera capable de couvrir les besoins :

https://randomnerdtutorials.com/esp8266-pinout-reference-gpios/ 

2.2. ESP32

 

Une carte ESP32 possède un plus grand nombre d'entrées sorties (16 sorties PWM), ainsi que 18 entrées analogiques, et deux sorties DAC !

De plus, 10 GPIOS peuvent être utilisées avec des touches capacitives.

Voir ici :

https://randomnerdtutorials.com/esp32-pinout-reference-gpios/ 

2.3. Des avantages incontestables

On voit rapidement que le manque d'entrées / sorties d'un ESP8266 ou d'un ESP32 par rapport à une carte ARDUINO est un argument qui ne tient pas.

Dans le pire des cas on pourra ajouter un module expander MCP23008 ou MCP23017 :

https://riton-duino.blogspot.com/2019/02/les-extensions-de-gpios-de-larduino.html

On constatera également que d'un point de vue prix de revient le coût d'une carte ESP32 est sensiblement équivalent à celui d'un couple ARDUINO UNO + ESP01.

L'encombrement du couple ARDUINO UNO + ESP01 est également en défaveur de cette solution, ainsi que les liaisons par fils DUPONT, sources de problèmes (faux contacts).

Une dernière chose : un serveur WEB développé à l'aide d'un ESP8266 ou ESP32 disposera de ressources largement supérieures à celles d'un ARDUINO :
  • puissance de calcul
  • espace mémoire FLASH, RAM
  • espace de stockage des pages WEB

Il faut aussi noter que pour les personnes qui désireraient une intégration maximale, un module seul, soudé sur PCB, peut apporter une solution ultra-compacte :

Module ESP32


2.1. Implémenter un serveur WEB

Il existe différentes possibilités de développer un serveur WEB avec un ESP8266 ou un ESP32.

Les ressources WEB (tutoriels, cours, forums) sont également très nombreuses, beaucoup plus nombreuses que celles liées aux solutions ARDUINO + ESP01, et d'un tout autre niveau.

Sur ce blog certains projets WEB utilisent un ESP8266 ou un ESP32 :

https://riton-duino.blogspot.com/2020/03/serveur-esp32-implementation-et-test.html

https://riton-duino.blogspot.com/2020/04/un-afficheur-tft-pour-domoticz-ou.html 

https://riton-duino.blogspot.com/2020/03/arduino-ethernet-esp32-comparons.html 

https://riton-duino.blogspot.com/2020/01/un-logger-analogique-et-digital-version.html

D'autres articles plus généralistes en parlent également :

https://riton-duino.blogspot.com/2019/02/esp8266-sur-batterie.html

Dans les paragraphes suivants j'évoque les différentes possibilités d'implémentation, sans entrer dans les détails, mis à part que je fournis deux petits exemples. Il est aussi du ressort de chacun d'apprendre à se documenter sans que le travail soit prémâché. Pourquoi réécrire ce qui l'est déjà ?

Je pense que l'effort de documentation et de formation aux techniques de développement ESP8266 ou ESP32 sera moins important que celui qui sera nécessaire pour mener à bien un développement complexe à base d'ARDUINO + ESP01, et que le résultat sera autrement moins tributaire des limitations, aussi bien en matière de performances qu'en matière de vitesse de développement.

2.1.1. SPIFFS ou LittleFS

Lorsque l'on développe un serveur WEB on en vient rapidement à ne plus générer de contenu HTML avec des lignes de code, mais à l'aide de fichiers HTML stockés dans un espace dit "système de fichiers", organisé comme un disque dur.

Un ESP8266 ou un ESP32 offrira un système de fichiers en FLASH permettant le stockage de ces fichiers, ainsi que des images ou des fichiers JavaScript, tout cela sans avoir à ajouter d'espace de stockage supplémentaire (carte µSD ou FLASH SPI).

L'IDE ARDUINO permet le chargement de ces ressources à l'aide de deux plugins :

https://github.com/me-no-dev/arduino-esp32fs-plugin
https://github.com/esp8266/arduino-esp8266fs-plugin

Ces deux plugins créeront deux choix disponibles dans le menu outils :
  • ESP32 Sketch Data Upload
  • ESP8266 Sketch Data Upload

Il sera donc facile de charger les ressources à partir de l'IDE. Les fichiers à charger seront simplement placés dans un sous-répertoire data du projet.

Ensuite dans le code il suffira de lire ces fichiers et de les envoyer sur la sortie standard du serveur.

Les exemples sont nombreux sur le WEB :

https://byfeel.info/utilisation-de-la-memoire-spiffs-sur-esp8266/

Il est à noter que la librairie EspAsyncWebServer propose une méthode qui permet d'éviter de faire cela manuellement :

class AsyncWebServerRequest {
    void send(FS &fs, const String& path, const String& contentType=String(), bool download=false, AwsTemplateProcessor callback=nullptr);

Exemple :

  request->send(SPIFFS, "/index.html", "text/html", false);

Le fichier index.html sera chargé à partir de la FLASH de l'ESP8266 / ESP32, sans que l'on aie à s'en préoccuper. Je parle plus loin de cette librairie.

2.1.2. WifiServer

Les librairies ESP8266WifiServer et WifiServer (ESP32) sont assez bas niveau :

https://randomnerdtutorials.com/esp32-web-server-arduino-ide/

Je déconseille leur usage sauf si c'est dans un but de formation, si l'on désire traiter une requête HTTP soi-même "from scratch" par exemple. C'est assez barbare et peu de gens l'utilisent.

2.1.3. WebServer

Les librairies ESP8266WebServer ou WebServer (ESP32) disposent d'APIs de plus haut niveau permettant de réaliser un développement en peu de temps. Elles permettent d'associer des fonctions à chaque URL à traiter :

https://tttapa.github.io/ESP8266/Chap10%20-%20Simple%20Web%20Server.html

Exemple :

Dans la fonction Setup :

  server.on("/", handleRoot);

Le handler :

void handleRoot(void)
{
  server.send(200, "text/html", message);     // message = texte HTML à afficher
}

Simplissime, n'est ce pas ?

2.1.4. AsyncWebServer

Enfin la librairie EspAsyncWebServer est capable de gérer des pages HTML à l'aide de templates. Les templates sont des fichiers HTML contenant des tags nommés permettant la substitution de variables (PLACEHOLDER).

Cette librairie est très largement documentée sur le WEB.

Ce projet l'utilise :

https://riton-duino.blogspot.com/2020/03/serveur-esp32-implementation-et-test.html

2.1.5. Petits exemples

ESP32 Web server

Ici se trouve un petit exemple ESP32 reproduisant la même page HTML que le tutoriel vu au paragraphe 1.3. ESP01 + ARDUINO :

esp32-simple-webserver

La librairie WebServer est utilisée. Sur un ESP8266 on utiliserait la librairie ESP8266WebServer, le code serait assez peu différent.

On peut remarquer :

  • 105 lignes de code seulement au lieu de 400
  • simplicité : je l'ai écrit en une vingtaine de minutes (la fonction Setup est en grande partie reprise d'un autre projet)
  • le fichier root.h contient la page HTML avec deux tags %TEMP% et %PRESSURE%, qui sont remplacées dans le code par leurs valeurs à l'aide de simples appels à String::replace()

Cet exemple montre que le code est beaucoup plus simple que la version ARDUINO+ESP01, et montre également que l'on peut bénéficier de ressources logicielles inutilisables sur un ARDUINO, en particulier la classe String, qui provoquerait une fragmentation de la mémoire de celui-ci :

https://riton-duino.blogspot.com/2020/02/arduino-la-fragmentation-memoire.html

Bien entendu, cette solution à base d'ESP8266 ou d'ESP32 sera incomparablement plus évolutive qu'une solution à base d'ARDUINO+ESP01, et permettra de développer un serveur beaucoup plus riche, tout en réduisant fortement la charge de travail.

ESP32 Access point

Ici on peut trouver également un petit exemple de serveur WEB fonctionnant en point d'accès WIFI :

esp32-simple-accesspoint

L'adresse IP de ce point d'accès est affiché sur le terminal au démarrage (normalement 192.168.4.1).

3. Conclusion

Est-il raisonnable à l'heure actuelle de s'orienter vers une solution à base d'ARDUINO + ESP01 ?

A moins d'avoir envie de se convaincre que c'est une piètre solution, j'ai bien peur que non.

En tous cas, cette technique ne permettra pas d'aller bien loin de par la pauvreté des ressources matérielles (CPU, mémoire) d'un ARDUINO.

Qu'en est-il du HTML, du JAVASCRIPT ?
Que ce soit avec une carte ARDUINO, un ESP8266 ou un ESP32, dans tous les cas la connaissance de ces deux langages est indispensable. Envisager un développement WEB sans connaissances dans ces domaines est tout bonnement impossible.

On trouve de nombreux tutoriels sur le WEB et on peut facilement tester une page HTML avec un simple navigateur, en chargeant simplement le fichier.


Cordialement
Henri

4. Mises à jour

24/08/2020 : 2.1.5. Petit exemple
                     1.1.2. Recharger le FIRMWARE AT avec FLASH DOWNLOAD TOOL


jeudi 7 mai 2020

Le SC16IS750 : UART avec 8 GPIOs



Le SC16IS750 : UART avec 8 GPIOs


Aujourd'hui je partage une petite découverte récente : le SC16IS750.

Il s'agit d'un circuit permettent d'ajouter une ligne série et 8 GPIOs à un microcontrôleur, un ARDUINO par exemple.

On ne trouve pas énormément d'informations à propose de ce circuit sur le WEB. C'est une bonne raison pour l'essayer et faire un tutoriel.

Il a les caractéristiques suivantes :
  • alimentation 3.3V
  • consommation 30µA en standby
  • entrées tolérantes au 5V
  • pilotage en I2C
    • I2C 400 kbit/s
    • 2 broches d'adresse I2C
  • pilotage en SPI
    • 4 Mbit/s
  • UART : RX / TX avec lignes CTS RTS
    • baud rate jusqu'à 5 Mbit/s
    • contrôle de flux RTS/CTS
    • possibilité de configuration de 4 pins en RI, CD, DTR, DSR
    • RS485
  • 8 GPIOs bidirectionnelles
    • courant de sortie 10 mA maxi
    • sans résistances de pull-up ou pull-down
Comme on le voit son baud rate maximal sera bien supérieur à ce que donnera un objet SoftwareSerial.

Il existe des modules sur AliExpress, coûtant environ 3€.

Ces modules comportent un régulateur 3.3V. C'est un MIC5219 (marquage LG33) qui accepte au maximum 20V sur son entrée.
Il peuvent donc être alimentés en 5V ou 12V sans problème.

1. Le fonctionnement en I2C



Sur ce schéma les deux broches d'adresse sont reliées au +5V. Le circuit possède 16 adresses possibles (voir paragraphe 10.3 Addressing de la datasheet).

Pour le test l'entrée RX est rebouclée sur TX, le SC16IS750 reçoit donc ce qu'il émet.

Le sketch suivant permet de tester le montage :

#include <SC16IS750.h>

SC16IS750 i2cuart = SC16IS750(SC16IS750_PROTOCOL_I2C, SC16IS750_ADDRESS_AA);

#define SIZE    64
#define LED     1

char xmit[SIZE + 1];
char rcv[SIZE + 1];

void setup()
{
  Serial.begin(115200);
  i2cuart.begin(230400);
  i2cuart.pinMode(LED, OUTPUT);
  i2cuart.digitalWrite(LED, LOW);
  if (i2cuart.ping() != 1) {
    Serial.println("device not found");
    while (1);
  } else {
    Serial.println("device found");
  }
  Serial.println("start serial communication");
  memset(xmit, 'A', SIZE);
  xmit[SIZE] = 0;
}

void loop()
{
  static int count;

  i2cuart.print(xmit);
  while (i2cuart.available() == 0);
  memset(rcv, 0, SIZE);
  i2cuart.readBytes(rcv, SIZE);
  rcv[SIZE] = 0;
  Serial.print(rcv);
  if (memcmp(rcv, xmit, SIZE)) {
    Serial.print("\nError");
  }
  Serial.print("\nInputs: ");
  Serial.println(i2cuart.GPIOGetPortState(), HEX);
  if (++count == 10) {
    i2cuart.digitalWrite(LED, HIGH);
    delay(100);
    i2cuart.digitalWrite(LED, LOW);
    count = 0;
  }
}


Le logiciel compare ce qu'il a reçu avec ce qu'il a émis. Je n'ai pas vu d'erreur à l'affichage.

La LED clignote à chaque fois que 10 messages ont été émis et reçus. L'état des entrées est affiché en continu.

2. Le fonctionnement en SPI


Attention la broche N°1 (I2C-SPI) passe à GND au lieu de +5V. A part cela le câblage SPI est classique.

La broche chip select (CS) peut bien sûr être reliée à une autre broche de l'ARDUINO (ici : la 10).

Voici le sketch :

#include <SC16IS750.h>

SC16IS750 i2cuart = SC16IS750(SC16IS750_PROTOCOL_SPI, 10);

#define SIZE    64
#define LED     1

char xmit[SIZE + 1];
char rcv[SIZE + 1];

void setup()
{
  Serial.begin(115200);
  i2cuart.begin(230400);
  i2cuart.pinMode(LED, OUTPUT);
  i2cuart.digitalWrite(LED, LOW);
  if (i2cuart.ping() != 1) {
    Serial.println("device not found");
    while (1);
  } else {
    Serial.println("device found");
  }
  Serial.println("start serial communication");
  memset(xmit, 'A', SIZE);
  xmit[SIZE] = 0;
}

void loop()
{
  static int count;

  i2cuart.print(xmit);
  while (i2cuart.available() == 0);
  memset(rcv, 0, SIZE);
  i2cuart.readBytes(rcv, SIZE);
  rcv[SIZE] = 0;
  Serial.print(rcv);
  if (memcmp(rcv, xmit, SIZE)) {
    Serial.print("\nError");
  }
  Serial.print("\nInputs: ");
  Serial.println(i2cuart.GPIOGetPortState(), HEX);
  if (++count == 10) {
    i2cuart.digitalWrite(LED, HIGH);
    delay(100);
    i2cuart.digitalWrite(LED, LOW);
    count = 0;
  }
}

Que dire de plus ? le sketch fonctionne comme le précédent, avec une vitesse bien supérieure, grâce au bus SPI comme on peut s'en douter.

3. La librairie

On peut télécharger la librairie ici :


4. Conclusion

Voici un petit module facile à mettre en œuvre qui pourra offrir une solution de choix aux personnes ne désirant pas changer leur ARDUINO UNO, NANO ou PRO MINI pour une MEGA, pour des raisons diverses et variées : encombrement, consommation, etc.
Ce test a été réalisé avec une PRO MINI 8MHz, alimentée en 3.3V et 5V. Les deux fonctionnent.

Quelques remarques :
  • ces tests ont été effectués à 230400 baud, mais rien ne s'oppose à ce que l'on adopte une vitesse supérieure (j'ai testé avec succès à 921600 baud).
  • les lignes SDA et SCL possèdent des résistances de pull-up internes
  • la méthode digitalRead() ne fonctionne pas (probablement un bug de la librairie)
    • la méthode GPIOGetPortState() permet de lire les 8 entrées à la fois dans un byte, et donc de s'en sortir.
  • il est possible de lire une GPIO si elle est configurée en sortie (comme sur un ARDUINO)
  • la limite de 64 octets des buffers du sketch est une limite de la librairie ARDUINO. Si l'on veut émettre ou recevoir plus d'octets, il faudra le faire en plusieurs fois.

Cordialement
Henri

lundi 6 avril 2020

Serveur ESP32 : implémentation (6ème partie)




Serveur ESP32 : implémentation

(6ème partie)


Ceci est la suite des quatre articles précédents :
Serveur ESP32 : implémentation
Serveur ESP32 : implémentation (2ème partie)
Serveur ESP32 : implementation (3eme-partie)
Serveur ESP32 : implementation (4eme-partie)
Serveur ESP32 : implementation (5eme-partie)
Serveur ESP32 : tests automatisés
Serveur ESP32 : tests automatisés (2ème partie)

Je pensais en avoir terminé mais j'ai décidé d'aller un peu plus loin.

Dans cette 6ème partie nous allons ajouter un afficheur LCD 4 lignes ou un afficheur OLED SSD1306.

1. Les afficheurs

J'ai choisi un afficheur LCD I2C et un OLED SSD1306 I2C.

Je me suis assez vite aperçu que l'affichage sur un écran LCD I2C ne fonctionnait pas lorsqu'il était effectué dans une callback appelée par la classe AsyncWebServer, en tous cas cela fonctionne très mal.
Le LCD affiche des caractères étranges.
L'afficheur OLED fonctionne un peu mieux, il affiche un ou deux pixels en plus des messages, de manière aléatoire.

Explication : une fonction callback est une fonction appelée par le code de la classe AsyncWebServer lorsqu'une requête HTTP donnée est reçue :

  server.on("/", handleRoot);

Lorsque la requête http://xxx.xxx.xxx.xxx/ est reçue la fonction handleRoot() est appelée.

Le problème est que cette fonction est exécutée dans la tâche WIFI, alors que la fonction loop() est exécutée dans une autre tâche.
Est-ce la cause du problème ?

J'ai donc fait un essai d'affichage depuis la fonction loop() en fonction des besoins des callbacks, et cela fonctionne autrement mieux.

1.1. Les messages

Le message à afficher dépend du contexte :

L'usager est invité à présenter sa carte et à appuyer sur un bouton :

"Put your Card &"
"Press The button"


La carte n'est pas détectée :

"No Card Detected"

La carte est inconnue :

"Unknown Card"

Le crédit est à ZÉRO :

"No Credit"

La carte est désactivés :

"Card is Deactivated"

La carte est débitée :

"Please Enter"
"The door is Open"
"Credit : XX"


Un nouvel usager doit être créé :

"Put your Card &"
"Press Read Media"


Les premiers messages sont affichés dans la fonction loop() et ne posent donc aucun problème. Il n'y a aucune interaction avec les requêtes HTML.

Le dernier message est affiché dans une callback "/new.html" et le premier message sera à nouveau affiché lorsque l'usager aura été créé.
Ces messages devront être affichés tout de même dans la fonction loop().

La technique utilisée est simple :
L'affichage est géré par une classe display dont deux classes sont dérivées :
  • lcdDisplay
  • oledDisplay
La classe display possède une méthode begin() qui enregistre l'ID de la tâche courante, donc celle de main(), setup() et loop().
Chaque classe lcdDisplay oledDisplay possède un certain nombre de méthodes d'affichage de messages :
  • void displayWelcome(void);
  • void displayNoCard(void);
  • void displayUnknownCard(void);
  • void displayNoCredit(void);
  • void displayInactiveCard(void);
  • void displayEnter(int credit);
  • void displayAskForCard(void);
Exemple :
void lcdDisplay::displayWelcome(void)
{
  Serial.printf("lcdDisplay::displayWelcome %x", xTaskGetCurrentTaskHandle());
  if (m_mainTask == xTaskGetCurrentTaskHandle()) {
    Serial.printf("OK\n");
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Put your card &");
    lcd.setCursor(0, 1);
    lcd.print("Press The button");
    messageDisplayed = WELCOME;
  }
  else {
    Serial.printf("NO\n");
  }
  messageToDisplay = WELCOME;
}

 
Si l'identifiant de la tâche courante est celui de la tâche main(), l'affichage est réalisé directement, sinon une variable est messageToDisplay est positionnée.

La méthode display::process() est appelée par la fonction loop() :
Si le message désiré n'est pas encore affiché, la méthode effectue le travail :

void display::process(void)
{

  // laisse le temps aux méthodes d'affichage de positionner la variable messageToDisplay
  delay(10);
  if (messageToDisplay != messageDisplayed) {
    switch (messageToDisplay) {
      case NONE:
        clear();
        break;
      case ASK_FOR_CARD:
        displayAskForCard();
        break;
      case WELCOME:
        displayWelcome();
        break;
    }
  }
}


Seuls les messages de bienvenue et de présentation carte lors de l'ajout d'un nouvel abonné sont concernés.

2. Pages HTML

Voici les copies d'écran de la dernière version :


Hitorique

3. Téléchargement

Cette version 2.4 est disponible ici :
https://bitbucket.org/henri_bachetti/webserver-form/src/v2.4/esp32-subscriber/

Le choix de l'écran à utiliser se fait par une option de compilation :

#define NODISPLAY     0
#define USE_LCD       1
#define USE_SSD1306   2

#define SCREEN USE_LCD


Pour utiliser un SSD1306 :

#define SCREEN USE_SSD1306

4. Conclusion

Communiquer en I2C dans une callback n'est pas aussi simple qu'il n'y paraît. Quelle en est la raison ? Mystère pour l'instant.
La communication SPI a l'air de ne poser aucun problème par contre, puisqu'elle est utilisée pour la lecture des cartes RFID.

Il y a fort à parier que d'autres composants I2C devront être gérés de la même manière, des capteurs de température ou de lumière par exemple.


Cordialement
Henri

vendredi 3 avril 2020

ARDUINO : NTP sur Ethernet



ARDUINO : NTP sur Ethernet


Le problème se pose souvent lorsque l'on travaille avec un ARDUINO et une carte Ethernet W5100 ou W5500 : comment récupérer l'heure sans horloge RTC (Real Time Clock) ?

Cette petite librairie va être très utile. Elle permet de récupérer l'heure UTC (Coordinated Universal Time) ou locale à partir d'un serveur (pool.ntp.org par défaut).

Elle possède un mécanisme permettant d'appliquer une correction en fonction du fuseau horaire et des règles de changement d'heure été / hiver (Timezones).

Sur ESP8266 ou ESP32 on utilisera une autre solution. Voir en fin d'article.


1. Le besoin

Cette librairie a été conçue dans le but de pouvoir utiliser les 3 systèmes de gestion de l'heure les plus courants sur ARDUINO :
  • librairie standard C :
    • time()
    • gmtime()
    • localtime()
    • ctime()
    • strftime()
    • etc.
  • librairie TimeLib de Paul Stoffregen (optionnelle) :
    • year()
    • month()
    • day()
    • hour()
    • minute()
    • second()
    • etc.
  • librairie AdaFruit RtcLib (optionnelle)
La possibilité d'adopter une librairie connue permettra d'une part de satisfaire les besoins et habitudes spécifiques de chacun et d'autre part d'intégrer au besoin une horloge RTC DS3231.

On peut se poser la question de l'utilité d'une horloge RTC lorsque l'on dispose d'une connexion Ethernet.
Cette possibilité peut être utile lorsque cette connexion n'est pas permanente. Bien sûr dans ce cas on imagine assez bien que l'ARDUINO ne sera pas utilisé en tant que serveur WEB.
Pour cette raison j'ai choisi d'offrir la possibilité d'intégrer uniquement un DS3231, bien plus précis qu'un DS1307.

Un ordinateur du type PC dispose également d'une horloge RTC, et cette horloge est mise à jour périodiquement lorsque le PC est relié au réseau. J'ai repris le même principe ici.

    1.1. Heure UTC et locale

    L'heure UTC est l'heure au méridien de Greenwich (Greenwich Mean Time, abrégé en GMT). Elle correspond au fuseau horaire UTC+0.

    L'heure locale est l'heure correspondant à un fuseau horaire donné. Cette heure locale, suivant les pays, peut être corrigée en fonction d'horaires été / hiver.

    Afin de ne pas avoir à remettre à l'heure une horloge à chaque changement d'heure on utilise des règles de changement d'heure. En France elles sont les suivantes :
    • dernier dimanche de mars à 2:00 : GMT+2
    • dernier dimanche d'octobre à 3:00 : GMT+1
    Comme beaucoup de systèmes UNIX existants l'heure interne est stockée sous la forme UTC. Elle est récupérable directement à l'aide de la fonction time() de la Librairie standard C.
    L'heure est ensuite corrigée par la fonction localtime() en fonction de la zone et des changements d'heure été / hiver.

    1.2. Librairie standard C

    On peut récupérer l'heure UTC ou locale à l'aide des fonctions de la librairie standard C :

        time_t t;
        // heure UTC en nombre de secondes depuis le 01/01/1970
        time(&t);
     
        // heure UTC (année, mois, jour, heure, minutes, secondes, etc.)
        struct tm *current = gmtime(&t);
        const char *format = "%A %d/%m/%Y, %H:%M:%S";

        // formatage : jour jj/mm/aaaa, hh:mm:ss
        strftime(buf, sizeof(buf), format, current);

         // heure locale (année, mois, jour, heure, minutes, secondes, etc.) 
        current = localtime(&t); 
        // formatage : jour jj/mm/aaaa, hh:mm:ss 
        strftime(buf, sizeof(buf), format, current);

    La librairie standard C possède bien d'autres fonctions.

    1.3. Librairie optionnelles

    Les librairies TimeLib et RtcLib sont optionnelles. Il suffit de les activer par une option de compilation.
    Si ces librairies ne sont activée elles n'auront aucun besoin d'être installées.

    Il est possible d'ajouter une horloge RTC DS3231 comme source de temps supplémentaire. A chaque fois que le serveur NTP pourra être joint, la RTC sera mise à jour. Ensuite si le serveur NTP ne peut plus être joint, l'heure sera récupérée à partir de la RTC.

    Attention : la RTC est mise à jour à l'aide de l'heure UTC. Si l'on cherche à récupérer l'heure et la date à partir des méthodes de la RtcLib dans l'application, elle ne sera pas corrigée en fonction de l'heure été / hiver.

    Une méthode rtcNow() permet de récupérer l'heure de la RTC corrigée. Voir l'exemple plus bas.

    1.1. Stockage de l'heure

    Dans la librairie C une simple variable est utilisée pour le stockage de l'heure :

    volatile time_t __system_time;

    Sa valeur est égale au nombre de secondes écoulées depuis le 1er janvier 1970. C'est la norme en vigueur en langage C.

    La fonction time() retourne simplement la valeur de cette variable.

    Contrairement à une librairie C classique, dans la librairie C AVR la variable __system_time n'est pas incrémentée automatiquement, il convient de le faire toutes les secondes dans la fonction loop(). Classiquement on le fait comme ceci :

      static unsigned long lastTick;
      unsigned long tick = millis();
      if (tick - lastTick > 1000) {
        system_tick();
        lastTick = tick;
      }


    La librairie proposée ici met à disposition une méthode tick() permettant également la mise à jour de l'horloge RTC, si elle est utilisée. L'horloge RTC sera mise à jour avec une période exprimée en secondes, passée en paramètre à la méthode begin() :

    void EthernetNtp::begin(IPAddress &addr, time_t syncInterval);

    Nous appellerons donc cette méthode tick() en lieu et place de system_tick() :

      static unsigned long lastTick;
      unsigned long tick = millis();
      if (tick - lastTick > 1000) {
        EthernetNtp::getInstance()->tick();
        lastTick = tick;
      }


    Il est déconseillé d'utiliser la fonction delay() dans le sketch. Lorsque l'on développe un serveur WEB, c'est d'ailleurs peu recommandé.

    Si l'on est obligé d'utiliser delay() ou des fonctions de traitement longues il faudra plutôt faire ceci :

      static unsigned long lastTick;
      unsigned long tick = millis();
      while (
    tick - lastTick >= 1000) {
         EthernetNtp::getInstance()->tick();
         lastTick += 1000;
      }


    Le fonctionnement de la librairie TimeLib est différent. La variable interne sysTime est mise à jour à chaque appel de la méthode now() :
    La méthode now() est appelée par toutes les autres méthodes year(), month(), day(), hour(), minute(), second() de la librairie TimeLib.

    2. Mise en œuvre

    Dans la fonction setup() d'un sketch ARDUINO il suffit de peu de choses pour utiliser cette librairie. Voici un exemple typique pour une gestion de l'heure en TimeZone Europe/Paris. La majeure partie du code est associée au démarrage du serveur Ethernet et à la recherche du serveur NTP par DNS :

    void setup()
    {
      EthernetNtp *ntp = EthernetNtp::getInstance();
      Serial.begin(115200);
      Serial.println(F("Network Time Protocol Example"));
      Ethernet.begin(mac, ip, dnsIp);
      if (Ethernet.hardwareStatus() == EthernetNoHardware) {
        Serial.println(F("Ethernet hardware was not found.  Sorry :("));
        while (true) {
          delay(1);
        }
      }
      if (Ethernet.linkStatus() == LinkOFF) {
        Serial.println(F("Ethernet cable is not connected."));
      }
      // Europe/Paris : last sunday of october at 3:00 : 1H offset
      ntp->std("CET", Last, Sun, Oct, 3, 60);
      // Europe/Paris : last sunday of march at 2:00 : 2H offset
      ntp->dst("CEST", Last, Sun, Mar, 2, 120);
      dns.begin(dnsIp);
      // get NTP server using DNS
      Serial.print(F("DNS (")); Serial.print(ntpUrl); Serial.print(F("): "));
      Serial.println(dns.getHostByName(ntpUrl, ntpIp) == true ? "SUCCESS" : "FAILED");
      // update time every 60 seconds
      ntp->begin(ntpIp, 60);
    #ifdef USE_RTCLIB
      ntp->addDS3231();

    #endif
      Serial.print(F("server is at "));
      Serial.println(Ethernet.localIP());
    }


    Ensuite il suffira de récupérer l'heure et la date à l'aide des fonction habituelles de la librairie standard C ou TimeLib. Voici un bout de code affichant l'heure et la date UTC ou locale sur une page WEB :

        char buf[30];
        client.println(F("HTTP/1.1 200 OK"));
        client.println(F("Content-Type: text/html"));
        client.println(F("Connection: close"));
        client.println();
        client.println(F("<!DOCTYPE HTML>"));
        client.println(F("<html>"));
        client.print("Using standard C library:");
        client.println(F("<br />"));
        client.print("UTC time & date: ");
        time_t t;
        time(&t);
        struct tm *current = gmtime(&t);
        const char *format = "%A %d/%m/%Y, %H:%M:%S";
        strftime(buf, sizeof(buf), format, current);
     

        client.print(buf);
        client.println(F("<br />"));
        client.print("Local time & date: ");
        current = localtime(&t);
        strftime(buf, sizeof(buf), format, current);
     

        client.print(buf);
        client.println(F("<br /><br />"));
        client.print("Using formattedTime:");
        client.println(F("<br />"));
        EthernetNtp *ntp = EthernetNtp::getInstance();
        ntp->formattedTime(buf, sizeof(buf), format);
     

        client.print("Local time & date: ");
        client.print(buf);
    #ifdef USE_RTCLIB
        client.println(F("<br /><br />"));
        client.print("Using RTC:");
        client.println(F("<br />"));
        DateTime now(ntp->rtcNow());
        strcpy(buf, "DDD DD/MM/YYYY hh:mm:ss");
        now.toString(buf);
        client.print("Local time & date: ");
        client.print(buf);
    #endif
        client.println(F("<br />"));
        client.println(F("</html>"));


    Le fait de récupérer l'heure de la RTC avec la méthode rtcNow() offre peu d'intérêt, étant donné que l'heure système est forcément à jour par rapport au serveur NTP.
    Le seul intérêt réside dans l'utilisation de la classe DateTime qui offre certaines facilités.
    Personnellement je trouve que les fonctions standards de la librairie C sont plus nombreuses et bien plus puissantes.

    2.1. Tick

    Si l'on utilise les fonctions standards de la librairie C, ou la TimeLib, il ne faudra pas oublier de mettre à jour l'heure interne toutes les secondes dans la fonction loop() :

    void loop()
    {
      static char request[REQUEST_MAX + 1];
      static unsigned long lastTick;
      int reqIndex = 0;
      char c;
      EthernetClient client = server.available();

      unsigned long tick = millis();
      if (tick - lastTick > 1000) {
        EthernetNtp::getInstance()->tick();
        lastTick = tick;
      }


    2.2. Exemple

    L'exemple de la librairie utilise 3 méthodes de lecture et de formatage de l'heure :
    • librairie standard C : time(), gmtime(), localtime(), strftime()
    • méthode formattedTime() de la classe EthernetNtp
    • méthode rtcNow() de la classe EthernetNtp et méthode toString() de la classe DateTime (librairie RtcLib).
    L'URL suivante est utilisée : http://xxxx.xxx.xxx.xxx/time
    L'adresse IP est affichée sur la console au démarrage.

    Le résultat :

    Using standard C library:
    UTC time & date: Saturday 04/04/2020, 08:39:35
    Local time & date: Saturday 04/04/2020, 10:39:35

    Using formattedTime:
    Local time & date: Saturday 04/04/2020, 10:39:35

    Using RTC:
    Local time & date: Sat 04/04/2020 10:39:35


    Dans l'exemple l'heure NTP est demandée toutes les 60 secondes :

      ntp->begin(ntpIp, 60);

    On pourra augmenter cette période en fonction de la précision de l'oscillateur de la carte.

    2. La librairie

    La librairie est disponible ici :
    https://bitbucket.org/henri_bachetti/w5100-ntp

    On pourra activer les options TimeLib et RtcLib dans w5100-ntp.h comme ceci :

    // uncomment to use TimeLib
    #define USE_TIMELIB

    // uncomment to use RtcLib
    //#define USE_RTCLIB


    La librairie Ethernet :
    https://github.com/arduino-libraries/Ethernet

    On peut également installer les librairies TimeLib et RtcLib si l'on a choisi l'une ou l'autre de ces options, ou les deux :
    https://github.com/PaulStoffregen/Time
    https://github.com/adafruit/RTClib

    l'exemple complet :
    https://bitbucket.org/henri_bachetti/w5100-ntp/src/master/examples/ntp/ntp.ino

    3. ESP32 & ESP8266

    Avec un ESP32 ou un ESP8266 récupérer l'heure à partir d'un serveur NTP et configurer la TimeZone sont beaucoup plus simples :

    #include <Arduino.h>
    #ifdef ESP32
    #include <WiFi.h>
    #else
    #include <ESP8266WiFi.h>
    #endif

    #define MAX_SIZE 80

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

    const char* ntpServer = "pool.ntp.org";

    void setup() {
      Serial.begin(115200);
      WiFi.begin(ssid, password);

      while (WiFi.status() != WL_CONNECTED) {
        Serial.print('.');
        delay (500);
      }
      configTzTime("CET-1CEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00", ntpServer);
    }

    void loop() {
      time_t timestamp = time( NULL );
      char buffer[MAX_SIZE];
      struct tm *pTime = localtime(&timestamp );
      strftime(buffer, MAX_SIZE, "%d/%m/%Y %H:%M:%S", pTime);
      Serial.println(buffer);
      delay(1000);
    }

    Avec ce code l'heure est demandée au serveur toutes les heures par défaut :
    #define SNTP_UPDATE_DELAY 3600000
    Cette constante est dans les options (hardware/esp8266/3.0.0/tools/sdk/lwip2/include/lwip/apps/sntp_opts.h)

    La chaîne de caractères "CET-1CEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00" est une "timezone string".

    Avec cette configuration le changement d'heure sera donc automatique le dernier dimanche de mars à 2:00 et octobre à 3:00.

    Les explications ici : https://www.di-mgt.com.au/wclock/tz.html

    Une documentation à lire : https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/system_time.html



    Cordialement
    Henri BACHETTI

    4. Mises à jour

    05/04/2020 : correction d'un bug de la mise à l'heure système.