vendredi 16 juillet 2021

ESP32 & ESP8266 : les Fichiers de Paramètres

 

 

ESP32 & ESP8266 : les Fichiers de Paramètres

 

Nous allons examiner dans cet article une technique de paramétrage d'un logiciel sur ESP32 ou ESP8266.

Tout d'abord qu'est ce que le paramétrage ?

C'est la possibilité de pouvoir modifier certaines variables, et donc le comportement du logiciel, sans être obligé modifier, recompiler, et recharger le code :

  • à l'aide d'un fichier de paramètres
  • à l'aide d'une page WEB, avec sauvegarde

Dans cet article nous allons aborder différents sujets :

  • chargement d'un fichier dans la FLASH
  • lecture des paramètres
  • la fonction magique strtok(), ou mieux,  strtok_r()
  • un peu de C++ : classes, héritage, variables et méthodes de classe, etc.

1. Les possibilités

1.1. EEPROM

La première solution qui vient à l'esprit est : l'EEPROM. Pour rappel, sur un ESP8266 ou un ESP32 l'espace de stockage EEPROM est émulé en mémoire FLASH.

J'avais déjà présente un projet ESP8266 où certains paramètres étaient modifiables par une page WEB :

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

Les paramètres sont stockés en FLASH à l'aide de la librairie EEPROM. Les valeurs par défaut, utilisées au premier démarrage, sont stockés sous forme de constantes en dur dans le code.

Stocker des paramètre à l'aide d'un espace en EEPROM n'est pas très souple. Cette méthode impose de créer une structure figée. Les paramètres ne sont pas nommés, et occupent chacun un emplacement précis en EEPROM.

1.2. Preferences

La librairie EEPROM est actuellement laissée de côté au profit de la librairie Preferences. Cette librairie permet de créer plusieurs espaces de noms, et pour chaque espace un ensemble de paramètres nommés.

L'exemple StartCounter est suffisamment parlant :

Un espace de noms my-app est ouvert :

preferences.begin("my-app", false);

On récupère la valeur d'un compteur :

unsigned int counter = preferences.getUInt("counter", 0);

On change la valeur du compteur :

preferences.putUInt("counter", counter);

On ferme l'espace de noms :

preferences.end();

Comme précédemment, les valeurs par défaut, utilisées au premier démarrage, sont stockés sous forme de constantes en dur dans le code.

Cette librairie peut convenir pour des cas de paramétrage simple. Pour l'usage que j'envisage, je préférerais opter pour un paramétrage du type fichier .INI, avec des sections :

[section1]
param1=valeur1
param2=valeur2

[section2]
param1=valeur1
param2=valeur2

Avec la librairie Preferences, cela imposerait de créer autant d'espaces de noms que de sections, ce qui serait assez vite fastidieux.

1.3. Fichier

Il est possible également de stocker les paramètres dans un fichier. Au départ, un fichier avec des valeurs par défaut est enregistré dans le système de fichiers SPIFFS. Ces valeurs peuvent être ensuite modifiées à l'aide d'une page WEB et réécrites dans le fichier, ou dans l'EEPROM.

Il devient donc possible, avec un code identique, de paramétrer plusieurs cartes avec des fichiers différents.

Partons d'un exemple simple :

Lorsqu'on travaille avec un ESP32 ou un ESP8266 la méthode que l'on utilise pour se connecter à un point d'accès est la suivante :

On déclare deux variables :

// remplacer par les vrais ssid et password
const char *ssid = "SSID";
const char *password = "PASSWORD"; 

Ensuite on appelle WiFi.begin() dans la fonction setup() :

  WiFi.begin(ssid, password);
  Serial.println("");

  // Wait for connection
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

Et si l'on essayait de paramétrer ces variables ssid et password à l'aide d'un fichier ?

Ce fichier pourrait ressembler à ceci :

[WIFI]
ssid=
SSID
pwd=
PASSWORD

Ou ceci :

[WIFI]
access-point=SSID:
PASSWORD

Vu comme cela l'intérêt peut paraître faible, mais ce fichier pourrait contenir également d'autre paramètres de fonctionnement, plus complexes :

[server]
url=user:password@http://my-server.com:8080

Cette ligne pourrait servir à paramétrer l'accès à un serveur sur le port 8080, avec identifiant utilisateur et mot de passe.

Et cela peut aller beaucoup plus loin.

2. La librairie

Il existe une librairie permettant de lire des fichiers de paramètres, directement installable depuis l'IDE ARDUINO :

https://github.com/yurilopes/SPIFFSIniFile

L'auteur a commis une petite erreur : la compilation se passe mal en version ESP8266. Dans le fichier SPIFFSIniFile.h il faut remplacer :

#include <SPIFFS.h>

Par :

#ifdef ESP32
#include <SPIFFS.h>
#endif

3. Le chargement dans SPIFFS

Après avoir remplacé les valeurs de ssid et pasword, il faut placer le fichier dans un sous répertoire du projet, nommé data, et le charger à l'aide du menu "Outils / ESP8266 Sketch Data Upload" ou "Outils / ESP32 Sketch Data Upload".

Bien entendu on choisira auparavant la carte adéquate à l'aide du menu "Outils / Type de carte". Pour ma part j'ai testé ce qui suit avec "ESP32 dev module" et "Wemos D1 R1".

Avant cela, si ce n'est déjà fait, il faut installer un plugin.

3.1. ESP32

Installer le plugin :

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

Il faut choisir le partitionnement de la FLASH à l'aide du menu "Outils / Flash Size". Le choix par défaut "Outils / Partition scheme" "Default 4MB with SPIFFS" suffit pour un essai.

Lancer le chargement à l'aide du menu "Outils / ESP32 Sketch Data Upload".

3.2. ESP8266

Installer le plugin :

https://github.com/esp8266/arduino-esp8266fs-plugin 

Il faut aussi choisir le partitionnement de la FLASH à l'aide du menu "Outils / Flash Size" : 4M (1M SPIFFS).

Lancer le chargement à l'aide du menu "Outils / ESP8266 Sketch Data Upload".

Sur ESP8266 j'ai eu des difficultés à utiliser le plugin. Le chargement ne se fait pas. L'IDE affiche simplement :

[SPIFFS] data    : /home/user/projects/esp-config/esp8266-config/data
[SPIFFS] size    : 1004
[SPIFFS] page    : 256
[SPIFFS] block   : 8192
/config.ini
[SPIFFS] upload  : /tmp/arduino_build_111554/esp8266-config.spiffs.bin
[SPIFFS] address  : 0x300000
[SPIFFS] reset    : nodemcu
[SPIFFS] port     : /dev/ttyUSB1
[SPIFFS] speed    : 921600
[SPIFFS] python   : python3
[SPIFFS] uploader : /home/
user/.arduino15/packages/esp8266/hardware/esp8266/2.5.2/tools/upload.py

Mais le sketch exemple (que l'on verra ensuite) ne trouve pas le fichier.

Si l'on rencontre le même problème on peut charger le fichier en ligne de commande avec esptool.

Le nom du fichier à charger est indiqué plus haut en gras (upload).

Le chemin du logiciel de chargement est indiqué à la fin (uploader). il suffira de remplacer upload.py par esptool/esptool.py ou esptool\esptool.py sous Windows.

Sous Linux il suffit de construire une ligne de commande comme ceci :

python /home/user/.arduino15/packages/esp8266/hardware/esp8266/2.5.2/tools/esptool/esptool.py  --baud 115200 --port /dev/ttyUSB1 write_flash 0x300000 /tmp/arduino_build_111554/esp8266-config.spiffs.bin

Sous Windows ou Mac la ligne sera assez peu différente :

--port /dev/ttyUSB1 est à remplacer par le port de communication USB à utiliser.

Voici ce que cela donne :

config.spiffs.bin
esptool.py v2.6
Serial port /dev/ttyUSB1
Connecting....
Detecting chip type... ESP8266
Chip is ESP8266EX
Features: WiFi
MAC: 5c:cf:7f:14:3a:da
Uploading stub...
Running stub...
Stub running...
Configuring flash size...
Auto-detected Flash size: 4MB
Compressed 1028096 bytes to 1417...
Wrote 1028096 bytes (1417 compressed) at 0x00300000 in 0.1 seconds (effective 63914.4 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting via RTS pin...

4. Exemples 

La repository https://bitbucket.org/henri_bachetti/esp-config.git contient 3 exemples :

  • esp8266-config
  • esp32-config
  • esp32-relay-config

Les deux exemples esp8266-config et esp32-config sont équivalents et permettent simplement de connecter un ESP8266 ou un ESP32 au réseau WIFI, ssid et password étant stockés dans un fichier config.ini.

L'exemple esp32-relay-config permet de récupérer les paramètres d'un certain nombre de relais connectés à un ESP32.

4.1. Exemple simple

Nous allons utiliser ce fichier de paramètres (config.ini):

[WIFI]
access-point=SSID:
ABCDEFGHIJ

Bien entendu les identifiants SSID et ABCDEFGHIJ sont des exemples, à remplacer par les identifiants véritables du réseau WIFI dans le fichier config.ini

Le ssid et le password sont séparés par le caractère ':'. Pourquoi compliquer les choses alors que deux paramètres seraient plus simples à lire ?

Parce que dans un fichier de paramètres plus complexe une écriture de plusieurs paramètres sur une seule ligne aura l'avantage d'être plus concise et plus claire. Dans l'exemple suivant cela paraîtra plus évident.

4.1.1. La classe config

Pour lire notre ssid et notre password j'ai écrit une petite classe nommée config :

https://bitbucket.org/henri_bachetti/esp-config/src/master/esp8266-config/config.h 

https://bitbucket.org/henri_bachetti/esp-config/src/master/esp8266-config/config.cpp

#define MAX_LINE        100

class config : public SPIFFSIniFile
{
  public:
    config(const char *fileName);
    bool read(void);
    void print(void);
    char *getSsid(void) {return m_ssid;}
    char *getPassword(void)  {return m_password;}
  private:
    const char *m_fileName;
    char m_ssid[MAX_LINE/2];
    char m_password[MAX_LINE/2];
};

Cette classe hérite de SPIFFSIniFile. Elle pourra donc appeler directement les méthodes publiques de SPIFFSIniFile (open(), getValue(), etc.).

Elle possède un constructeur qui se contente d'appeler celui de SPIFFSIniFile :

config::config(const char *fileName) :
  SPIFFSIniFile(fileName)
{
  m_fileName = fileName;
}

Elle possède également une méthode read() permettant de lire et stocker les paramètres :

bool config::read(void)
{
  char buffer[MAX_LINE];
  const char *p;
  char *s;

  if (!open()) {
    Serial.printf("%s: not found\n", m_fileName);
    return false;
  }
  if (getValue("WIFI", "access-point", buffer, sizeof(buffer)) != true) {
    Serial.printf("%s:WIFI:access-point not found (error %d)\n", m_fileName, getError());
    return false;
  }
  p = strtok_r(buffer, ":", &s);
  if (p == NULL) {
    Serial.printf("%s: bad format, missing ':'\n", buffer);
    return false;
  }
  strcpy(m_ssid, p);
  p = strtok_r(NULL, ":", &s);
  if (p == NULL) {
    Serial.printf("%s: missing password\n", buffer);
    return false;
  }
  strcpy(m_password, p);
  return true;
}

Tout d'abord le fichier est ouvert, puis le paramètre [WIFI] access-point est lu dans un buffer de taille suffisante (MAX_LINE, qui vaut ici 100) .

Comme on le voit les erreurs sont gérées et des messages sont affichés si le fichier ou le paramètre est introuvable, ou que le format est incorrect. Les erreurs possibles sont listées dans SPIFFSIniFile.h.

Nous voyons ici une utilisation de la fonction strtok_r(), qui permet de découper une chaîne de caractères en mots, appelés jetons (token).

Il existe deux fonctions : strtok(), et strtok_r(), qui est récursive.

L'utilisation de strtok_r() est préférable car dans notre code on n'est jamais certain qu'une fonction utilisant strtok() n'appellera pas une autre fonction qui appelle également strtok(). Dans ce cas, le contexte de travail de la première fonction sera détruit par la seconde.

On voit également que les fonctions C de manipulation de C strings sont utilisées (strcpy(), et strchr() et strcmp() dans l'exemple suivant). Pourquoi ne pas utiliser les objets String du C++ ? Premièrement parce que la librairie SPIFFSIniFile ne les utilise pas, deuxièmement parce qu'il n'existe pas d'équivalent de strtok_r() en C++. Il serait dommage de se passer de cette fonction si pratique.

Examinons la suite du code.

Cette ligne demande à strtok_r() de retourner le premier jeton de la chaîne buffer, en tenant compte du séparateur ':', situé entre ssid et password :

  p = strtok_r(buffer, ":", &s);

Dans le deuxième appel à strtok_r() la chaîne à traiter est remplacée par NULL. Cela indique à strtok_r() qu'elle va devoir chercher le jeton suivant :

  p = strtok_r(NULL, ":", &s);

Au bout du compte deux variables m_ssid et m_password se voient affecter les deux jetons :

  strcpy(m_ssid, p);
  ...
  strcpy(m_password, p);

Ces deux valeurs pourront être demandées par l'appelant quand il le désirera, à l'ide de deux méthodes getSsid() et getPassword().

4.1.2. Le sketch

Voyons maintenant ce qui se passe au niveau du sketch :

https://bitbucket.org/henri_bachetti/esp-config/src/master/esp8266-config/esp8266-config.ino

On déclare deux variables vide :

const char *ssid;
const char *password;

On objet de la classe config est instancié :

config config("/config.ini");

Ensuite dans la fonction setup() le système de fichiers SPIFFS est démarré, puis la configuration est lue et affichée :

  if (!SPIFFS.begin()) {
    Serial.println("SPIFFS.begin() failed");
  }
  config.read();
  config.print();

Il n'y a plus qu'à se connecter :

  WiFi.mode(WIFI_STA);
  ssid = config.getSsid();
  password = config.getPassword();
  WiFi.begin(ssid, password);


Et le moniteur série affiche la connexion :

ssid: SSID
password:
ABCDEFGHIJ
..
Connected to SSID

IP address: 192.168.1.42
MDNS responder started
HTTP server started

4.2. Exemple plus complexe

Tout d'abord pour essayer cet exemple il faudra installer la librairie MCP23017 :

https://github.com/adafruit/Adafruit-MCP23017-Arduino-Library

On peut l'installer à partir du gestionnaire de bibliothèques de l'IDE ARDUINO.

Le fichier de paramètres de l'exemple esp32-relay-config est celui-ci :

[WIFI]
access-point=SSID:
ABCDEFGHIJ

[relays]
main=GPIO-H(4, 5)
secondary=I2C-L(0),I2C-L(1),I2C-L(2),I2C-L(3),I2C-H(4),I2C-H(5),I2C-H(6),I2C-H(7)

On retrouve les paramètres de connexion au réseau WIFI.

Une section relays a été ajoutée. Elle permet de paramétrer l'utilisation d'un certain nombre de relais :

Un relais principal bistable à deux bobines est relié aux GPIOS 4 et 5. Son niveau de commande est HIGH (GPIO-H) :

main=GPIO-H(4, 5)

8 relais secondaires sont reliés à un expander du type MCP23017 :

  • les 4 premiers sont des relais activables par un niveau bas (I2C-L).
  • les 4 autres sont des relais activables par un niveau haut (I2C-H).

On peut imaginer que deux modules à 4 relais sont branchés sur la sortie du MCP23017, le premier activable par un niveau bas, l'autre par un niveau haut.

Et on peut aussi imaginer que par la suite, deux autres modules pourront être branchés et configurés, pour arriver à un total de 16 relais.

secondary=I2C-L(0),I2C-L(1),I2C-L(2),I2C-L(3),I2C-H(4),I2C-H(5),I2C-H(6),I2C-H(7)

Ici on voit bien l'avantage d'un paramétrage sur une seule ligne, que l'on aurait pu écrire comme suit, ce qui est nettement moins concis :

secondary1=I2C-L(0)
secondary2=I2C-L(1)
secondary3=I2C-L(2)
etc.

On a donc affaire à une grammaire permettant de configurer différents type de relais :
  • commande directe : GPIO-H ou GPIO-L
  • commande par I2C : I2C-H ou I2C-L
  • relais classique : 1 seule broche de commande
  • relais bistable : 2 broches de commande

Le but de la manoeuvre est d'écrire deux classes :

  • une classe config comme précédemment
  • une classe relay intelligente

4.2.1. La classe config

Parlons d'abord de la classe config

https://bitbucket.org/henri_bachetti/esp-config/src/master/esp32-relay-config/config.h

https://bitbucket.org/henri_bachetti/esp-config/src/master/esp32-relay-config/config.cpp

Elle a exactement la même interface que la précédente (config.h). par contre la méthode config::read() est plus complexe :

bool config::read(void)
{
  char buffer[MAX_LINE];
  const char *p;
  char *s;
  int n;
  relay *relay;

  if (!open()) {
    Serial.printf("%s: not found\n", m_fileName);
    return false;
  }
  if (getValue("WIFI", "access-point", buffer, sizeof(buffer)) != true) {
    Serial.printf("%s:WIFI:access-point not found (error %d)\n", m_fileName, getError());
    return false;
  }
  p = strtok_r(buffer, ":", &s);
  if (p == NULL) {
    Serial.printf("%s: bad format, missing ':'\n", buffer);
    return false;
  }
  strcpy(m_ssid, p);
  p = strtok_r(NULL, ":", &s);
  if (p == NULL) {
    Serial.printf("%s: missing password\n", buffer);
    return false;
  }
  strcpy(m_password, p);
  if (getValue("relays", "main", buffer, sizeof(buffer)) != true) {
    Serial.printf("%s:relays:main not found (error %d)\n", m_fileName, getError());
    return false;
  }
  relay = relay::getRelay(0);
  if (relay->create(buffer) != true) {
    return false;
  }
  if (getValue("relays", "secondary", buffer, sizeof(buffer)) != true) {
    Serial.printf("%s:relays:secondary not found (error %d)\n", m_fileName, getError());
    return false;
  }
  n = 1;
  p = strtok_r(buffer, ", ", &s);
  while (p != NULL) {
    relay = relay::getRelay(n);
    if (relay == 0) {
      Serial.printf("relay %d: not found\n", n);
      return false;
    }
    if (relay->create(p) != true) {
      return false;
    }
    p = strtok_r(NULL, ", ", &s);
    n++;
  }
  return true;
}

Le début (la récupération de ssid et de password) est identique à la version précédente.

Ensuite le paramètre [relays] main est lu, et un premier relais principal est créé :

  if (getValue("relays", "main", buffer, sizeof(buffer)) != true) {
    Serial.printf("%s:relays:main not found (error %d)\n", m_fileName, getError());
    return false;
  }
  relay = relay::getRelay(0);
  if (relay->create(buffer) != true) {
    return false;
  }

Ensuite le paramètre [relays] secondary est lu, et un certain nombre de relais secondaires sont créés :

  if (getValue("relays", "secondary", buffer, sizeof(buffer)) != true) {
    Serial.printf("%s:relays:secondary not found (error %d)\n", m_fileName, getError());
    return false;
  }
  n = 1;
  p = strtok_r(buffer, ", ", &s);
  while (p != NULL) {
    relay = relay::getRelay(n);
    if (relay == 0) {
      Serial.printf("relay %d: not found\n", n);
      return false;
    }
    if (relay->create(p) != true) {
      return false;
    }
    p = strtok_r(NULL, ", ", &s);
    n++;
  }

On voit ici un découpage à l'aide de strtok_r() du paramètre [relays] secondary en un certain nombre de jetons séparés par des virgules :

secondary=I2C-L(0),I2C-L(1),I2C-L(2),I2C-L(3),I2C-H(4),I2C-H(5),I2C-H(6),I2C-H(7)

Mais le format de ces différents jetons est le même que pour le relais principal. 

On remarque que si l'on a configuré trop de relais ( > MAX_RELAY), la fonction getRelay() retourne ZERO et la configuration s'arrête (la partie en vert).

La méthode relay::create() reçoit donc en paramètre une chaine représentant une description de relais :

  • "GPIO-H(4, 5)" pour le relais 0
  • "I2C-L(0)" pour le relais 1
  • "I2C-L(1)" pour le relais 2
  • ...
  • "I2C-H(7)" pour le relais 8 

A noter : la méthode relay::create() utilise strtok_r(), et elle est appelée par config::read() qui utilise aussi strtok_r(). Si l'on utilisait strtok() cela ne fonctionnerait pas.

4.2.2. La classe relay

Ensuite, voici la classe relay :

https://bitbucket.org/henri_bachetti/esp-config/src/master/esp32-relay-config/relay.h 

https://bitbucket.org/henri_bachetti/esp-config/src/master/esp32-relay-config/relay.cpp

#define MAX_DEF         20
#define MAX_RELAY       32

enum accessType {IO, I2C};

class relay
{
  public:
    static bool begin(Adafruit_MCP23017 *mcp, uint8_t address=0);
    static int getCount();
    static relay *getRelay(int n);
    bool create(const char *def);
    relay() {m_onPin = m_offPin = -1; m_level = HIGH;}
    bool isPresent(void);
    bool isLatch(void);
    void print(void);
    void on(void);
    void off(void);
  private:
    static relay m_relay[MAX_RELAY];
    static Adafruit_MCP23017 *m_mcp;
    static bool m_mcpFound;
    accessType m_access;
    int m_onPin;
    int m_offPin;
    int m_level;
};

On voit tout d'abord deux constantes et une énumération :

  • MAX_DEF : la longueur maximale d'une description de relais (GPIO-H(4, 5) par exemple)
  • MAX_RELAY : le nombre maximal de relais configurables
  • accessType : définit la méthode d'accès au relais (GPIO ou I2C)

Les variables importantes sont les suivantes :

  • m_relay : le tableau de relais (32 au maximum)
  • m_mcp : l'adresse de l'objet MCP23017
  • m_access : la méthode d'accès du relais (GPIO ou I2C)
  • m_onPin : la pin d'activation du relais (ON)
  • m_offPin : la pin de désactivation du relais (OFF)
  • m_level : le niveau d'activation du relais (LOW ou HIGH)

m_relay, m_mcp et m_mcpFound sont des variables membres de la classe (static). Cela veut dire qu'elle n'appartient pas à une instance particulière, mais à la classe elle-même. Elles existent donc en exemplaire unique. Elle doivent être déclarées dans le code (relay.cpp).

La classe possède de nombreuses méthodes :

  • begin : initialiser la classe et surtout le composant MCP23017
  • getCount : retourne le nombre de relais configurés
  • getRelay : retourne l'adresse de l'objet relay N°n
  • create : crée un relais
  • relay : le constructeur
  • isPresent : indique que le relais est présent et configuré
  • isLatch : indique qu'il s'agit un relais bistable
  • print : affiche les informations du relais
  • on : active le relais
  • off : désactive le relais

Tout d'abord, voici la méthode begin()

bool relay::begin(Adafruit_MCP23017 *mcp, uint8_t address)
{
  m_mcp = mcp;
  mcp->begin(address);
  Wire.beginTransmission(MCP23017_ADDRESS+address);
  byte error = Wire.endTransmission();
  if (error == 0)  {
    Serial.printf("I2C device found at %02x\n", address);
    m_mcpFound = true;
    return true;
  }
  else if (error == 4) {
    Serial.printf("I2C error at %02x\n", address);
  }
  return false;
}
 

Elle appelle la méthode begin() de l'objet Adafruit_MCP23017. Ensuite elle vérifie que le MCP23017 est présent, car malheureusement la librairie AdaFruit ne le fait pas.

La méthode begin() est une méthode de classe (static). Cela veut dire qu'elle n'appartient pas à une instance particulière, mais à la classe elle-même. Elle devra être appelée ainsi :

Adafruit_MCP23017 mcp;
relay::begin(&mcp);

Nous verrons cela plus tard au niveau du sketch.

Le constructeur est simple :

    relay() {m_onPin = m_offPin = -1; m_level = HIGH;}

Il se contente d'initialiser quelques variables. 

La méthode getRelay() retourne l'adresse d'une instance du tableau m_relay :

relay *relay::getRelay(int n)
{
  if (n < MAX_RELAY) {
    return &m_relay[n];
  }
  return 0;
}

Attention: elle retourne 0 si le N° du relais est supérieur ou égal à MAX_RELAY.

Voyons la méthode create() :

bool relay::create(const char *def)
{
  char tmp[MAX_DEF];
  const char *p;
  char *s;

  strcpy(tmp, def);
  p = strtok_r(tmp, "(", &s);
  if (p == NULL) {
    Serial.printf("%s: bad format, missing parenthesis\n", def);
    return false;
  }
  if (!strcmp(p, "GPIO-H")) {
    m_access = IO;
    m_level = HIGH;
  }
  else if (!strcmp(p, "GPIO-L")) {
    m_access = IO;
    m_level = LOW;
  }
  else if (!strcmp(p, "I2C-H")) {
    m_access = I2C;
    m_level = HIGH;
  }
  else if (!strcmp(p, "I2C-L")) {
    m_access = I2C;
    m_level = LOW;
  }
  else {
    Serial.printf("%s: bad value\n", def);
    return false;
  }
  p = strtok_r(NULL, ")", &s);
  if (p == NULL) {
    Serial.printf("%s: bad format, missing parenthesis\n", def);
  }
  if (strchr(p, ',')) {
    strcpy(tmp, p);
    p = strtok_r(tmp, ", ", &s);
    m_onPin = atoi(p);
    p = strtok_r(NULL, ", ", &s);
    m_offPin = atoi(p);
  }
  else {
    m_onPin = atoi(p);
  }
  return true;
}

Rappel : cette méthode est appelée par la méthode read() de la classe config. Elle reçoit en paramètre une chaine représentant une description de relais :

  • GPIO-H(4, 5) pour le relais 0
  • I2C-L(0) pour le relais 1
  • I2C-L(1) pour le relais 2
  • ...
  • I2C-H(7) pour le relais 8

Le premier jeton est un mot suivi d'une parenthèse. Il est évalué pour savoir si c'est un relais sur GPIO (GPIO-H ou GPIO-L) ou sur bus I2C (I2C-L ou I2C-H).

L'indicateur L ou H permet de savoir si c'est un relais activable sur niveau bas (souvent le cas) ou haut (plus rarement).

Le résultat est stocké dans deux variables :

  • m_access : qui vaudra IO ou I2C
  • m_level qui vaudra LOW ou HIGH

Le deuxième jeton est un entier (ou deux) suivi d'une parenthèse fermante. Il est évalué pour savoir si c'est un relais classique ou bistable. On cherche d'abors si le jeton contient une virgule avec strchr(). Si oui, et que deux valeurs sont présentes, séparées par une virgule, c'est un relais bistable (à deux broches de commande), sinon, c'est un relais classique.

Le résultat est stocké dans deux variables :

  • m_onPin : la pin de commande ON
  • m_offPin : la pin de commande OFF

Quand la méthode relay::on() ou relay::off() sera appelée, l'objet aura donc toutes les informations nécessaires pour savoir comment commander le relais :

  • à l'aide du bus I2C ou non
  • quel niveau de commande appliquer (LOW ou HIGH)
  • sur quel N° de pin il doit envoyer la commande

Voici la méthode relay::on() :

void relay::on(void)
{
  if (!isPresent()) {
    return;
  }
  if (m_access == I2C) {
    // RELAIS sur bus I2C
    if (!m_mcpFound) {
      Serial.printf("MCP not found !!!\n");
      //return;
    }
    if (isLatch()) {
      // RELAIS bistable
      Serial.printf("MCP%d: %d\n", m_onPin, m_level);
      m_mcp->digitalWrite(m_onPin, m_level);
      delay(50);
      Serial.printf("MCP%d: %d\n", m_onPin, !m_level);
      m_mcp->digitalWrite(m_onPin, !m_level);
    }
    else {
      // RELAIS classique
      Serial.printf("I2C%d: %d\n", m_onPin, m_level);
      m_mcp->digitalWrite(m_onPin, m_level);
    }
  }
  else {
    // RELAIS sur GPIO
    if (isLatch()) {
      // RELAIS bistable
      Serial.printf("
GPIO%d: %d\n", m_onPin, m_level);
      digitalWrite(m_onPin, m_level);
      delay(50);
      Serial.printf("GPIO%d: %d\n", m_onPin, !m_level);
      digitalWrite(m_onPin, !m_level);
    }
    else {
      // RELAIS classique
      Serial.printf("GPIO%d: %d\n", m_onPin, m_level);
      digitalWrite(m_onPin, m_level);
    }
  }
}

Un relais bistable se commande avec une simple impulsion de 50ms sur sa broche SET ou RESET.

Si la fonction doit piloter un relais I2C, et que le MCP23017 est absent, elle envoie tout de même les commandes et affiche les informations correspondantes.

Si l'on désire qu'elle ne fasse rien, il suffit de décommenter la ligne en vert.

4.2.3. Le sketch

Voyons maintenant ce qui se passe au niveau du sketch :

https://bitbucket.org/henri_bachetti/esp-config/src/master/esp32-relay-config/esp32-relay-config.ino

Il est identique au précédent, mis à part quelques ajouts :

Un objet Adafruit_MCP23017 est instancié :

Adafruit_MCP23017 mcp;

La classe relay est initialisée dans la fonction setup() :

  if (relay::begin(&mcp) == true) {
    Serial.println("\nMCP23017 found");
  }
  else {
    Serial.println("\nMCP23017 not found !!!");
  }
  config.read();
  config.print();

Les relais sont désactivés à la fin de la fonction setup() :

  Serial.println("RESET all relays");
  for (int n = 0 ; n < relay::getCount() ; n++) {
    relay *relay = relay::getRelay(n);
    relay->off();
  }

Voici le résultat sur le moniteur série :

MCP23017 not found !!!
ssid: SSID
password: ABCDEFGHIJ
LATCH, GPIO, HIGH-LEVEL(4,5)
NORMAL, I2C, LOW-LEVEL(0)
NORMAL, I2C, LOW-LEVEL(1)
NORMAL, I2C, LOW-LEVEL(2)
NORMAL, I2C, LOW-LEVEL(3)
NORMAL, I2C, HIGH-LEVEL(4)
NORMAL, I2C, HIGH-LEVEL(5)
NORMAL, I2C, HIGH-LEVEL(6)
NORMAL, I2C, HIGH-LEVEL(7)
 
Connecting to
SSID
HTTP server started
RESET all relays
GPIO5: 1
GPIO5: 0
MCP not found !!!
MCP0: 1
MCP not found !!!
MCP1: 1
MCP not found !!!
MCP2: 1
MCP not found !!!
MCP3: 1
MCP not found !!!
MCP4: 0
MCP not found !!!
MCP5: 0
MCP not found !!!
MCP6: 0
MCP not found !!!
MCP7: 0
Connected to AP successfully!
WiFi connected to
SSID
IP address:  
192.168.1.37

Le MCP23017 n'est pas branché, ce qui est normal, je ne l'ai pas fait.

Les 9 relais sont configurés (la partie en vert).

Les bonnes commandes sont envoyées aux relais (la partie en orange), en particulier :

  • une commande sur le GPIO5 du MCP23017 à HIGH puis LOW (c'est un relais bistable)
  • les 4 premiers relais secondaires sont désactivés grâce à une commande sur leur GPIO à HIGH (ce sont des relais actifs au niveau LOW)
  • les 4 derniers relais secondaires sont désactivés grâce à une commande sur leur GPIO à LOW (ce sont des relais actifs au niveau HIGH)

Cela se passe très bien. C'est une bonne raison pour continuer dans cette voie.

Pourquoi un tel bazar pour configurer quelques relais ?

Il sera possible ensuite d'utiliser ces informations pour bâtir une interface homme machine avec des pages WEB dynamiques, qui permettront en quelques clicks :

  • d'ajouter ou de retirer des relais
  • d'assigner un rôle à chaque relais

Et tout cela sans avoir à recompiler le code !

5. Modification des paramètres en live

Ce sujet fera l'objet d'un autre article. Mais dans la pratique c'est relativement simple si l'on maîtrise déjà les formulaires HTML.

Une fois le formulaire validé, il suffit, dans la fonction de traitement de l'URL, d'ouvrir le fichier en écriture, de réécrire les nouveaux paramètres, et de le fermer.

Dans ce sketch, examiner la fonction handleFormData :

https://bitbucket.org/henri_bachetti/domoticz-esp8266-tft-display/src/master/arduino/esp8266-tft-display/esp8266-tft-display.ino

Remarque : dans le cadre de l'écriture d'un WebServer, si l'on a utilisé le système de fichiers SPIFFS pour stocker les ressources (pages HTML, images, etc.), à chaque fois que l'on rechargera les ressources le fichier de paramètre sera écrasé.

On peut conrourner ce problème en écrivant les nouveaux paramètres en EEPROM. Ainsi, à chaque démarrage, si les paramètres existent en EEPROM, le logiciel les lit et les utilise, sinon, il récupère ceux du fichier de paramètres.

On peut également imaginer une solution plus simple, sans page WEB, en transférant directement (upload) un nouveau fichier de paramètres, à l'aide du protocole HTTP, ou même FTP.

6. Téléchargement

Le projet se trouve ici :

https://bitbucket.org/henri_bachetti/esp-config.git

7. Conclusion

Cette pré-étude a été réalisée dans le but de créer un projet d'irrigation automatisée à base d'ESP32 (à venir), pour lequel j'envisage un paramètrege très souple, afin de laisser à l'utilisateur final le choix de ses interfaces de sortie (des électrovannes), à l'aide de simples pages WEB.


Cordialement

Henri


Aucun commentaire:

Enregistrer un commentaire