jeudi 26 mars 2020

Serveur ESP32 : tests automatisés (2ème partie)


 Serveur ESP32 : tests automatisés

(2ème partie)


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

Le but de cette dernière évolution est :
  • ajouter la délivrance d'un service aux abonnés
  • améliorer le serveur 
  • tester certains cas nominaux ou non nominaux
  • tests particuliers
  • tests interactifs
Rappel : comme vu dans la 4ème partie seul l'ESP32 pourra offrir une possibilité de partitionnement entre SPIFFS et FAT.
Pour un ESP8266 il faudra forcément utiliser une SD.

1. Evolutions

1.1. Service aux abonnés

Ce service va être simple, afin de rajouter aussi peu de matériel que possible :
  • un bouton-poussoir
  • une LED (la LED de la carte ESP32)
L'abonné présente sa carte et appuie sur le bouton poussoir.
Si l'abonné n'est pas enregistré la LED clignote 1 fois.
Si l'abonné est désactivé la LED clignote 3 fois.
Si l'abonné a un crédit nul la LED clignote 6 fois.
S'il a du crédit la LED s'allume 10 secondes et son crédit est décompté.

On pourrait imaginer beaucoup de solutions hardware :
  • écran LCD alphanumérique
  • écran tactile
  • gâche électrique
  • vanne de distribution de boisson
  • distributeur de barres vitaminées
  • imprimante de tickets
  • etc.
Le but final de cet exercice sera d'ouvrir une porte. Le bouton-poussoir disparaîtra à terme. La notion de crédit ne sera pas nécessaire non plus, mais cela peut servir pour d'autres applications.

1.2. Historique

L'historique fonctionne maintenant à l'envers. Il est plus pratique d'avoir les dernières opérations affichées en premier.
L'affichage de l'historique est maintenant limité à 20 lignes et un lien "More history ..." permet de visualiser le fichier complet.

Une nouvelle ligne a été ajoutée : la date d'enregistrement de l'abonné.

1.3. Les détails

Le changement d'heure
Comme le changement d'heure est intervenu cette nuit (29 mars), le serveur est en retard d'une heure.

L'heure était configurée comme ceci :

  configTime(3600, 0, ntpServer);

Aujourd'hui il faudrait la configurer avec une heure de plus :

  configTime(7200, 0, ntpServer);

Mais il y a moyen de faire cela automatiquement, avec une "timezone string" :

  configTzTime("CET-1CEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00", ntpServer);

Le changement d'heure sera donc automatique le dernier dimanche de mars et octobre.
Les explications ici : https://www.di-mgt.com.au/wclock/tz.html

Les image et logos
Les deux pages HTML affichent maintenant un petit logo (un cadenas) et l'icône favori (favicon.ico) a également été ajouté :

Ceci pour démontrer qu'il ne suffit pas d'écrire <img src="/lock.jpg"> dans une page pour que l'image s'affiche.
Cela n'est pas forcément une évidence lorsque l'on a l'habitude de travailler sur un serveur Apache, mais avec un ESP32 et la librairie AsyncWebServer, il faut servir cette image.

Mémoire libre
Une URL supplémentaire a été ajoutée afin de pouvoir afficher la mémoire dynamique libre :
/freeram.html

La quantité de mémoire RAM libre est également affichée dans la boîte de dialogue "About ...".

2. Tests

2.1. Cas nominaux et non nominaux

Quelques tests nominaux ont été ajoutés :
  • recherche par nom
  • recherche par UID
Ces tests ont permis de corriger un bug.

Dans tout développement sérieux il ne suffit pas de tester quelques cas nominaux. Il faut aussi tester les cas d'erreurs :
  • carte non lue
  • abonné inexistant
  • abonné désactivé
  • crédit nul
Pour tous ces cas, du code a été écrit. Il serait assez peu judicieux de faire confiance au code écrit sans le tester, car en dehors d'assurer une fonctionnalité, tout cas d'utilisation, nominal ou non, est susceptible de produire un crash du logiciel.
Tant que l'on n'a pas testé le plus de cas possible rien ne permet d'affirmer que le logiciel fonctionne correctement.

Ces tests paraissent simples. Vérifier que le serveur affiche bien "Subscriber NOT FOUND !!!" si l'abonné n'existe pas ne devrait poser aucune difficulté.

Pour rappel cette suite de test s'exécute comme ceci :

$ python3 test.py AutomatedTest

2.2. Cas particuliers

Partition pleine
Un test en particulier est absolument indispensable : le test de remplissage de la partition FAT.Pour tester ce cas, il va être difficile de créer un abonné ou plusieurs et de réaliser un grand nombre d'actions (ajout de crédit par exemple) jusqu'à remplir la partition de 1.44 méga-octets. Chaque ligne d'historique faisant environ 30 octets. Cela nous obligerait à réaliser 48059 actions.

Faire ce test en automatique sera long également. Même si chaque opération ne dure que 500ms, il nous faudrait tout de même 24000 secondes soit plus de 6 heures !

Une astuce consiste à remplir artificiellement la partition de fichiers de taille assez importante.
La serveur se voit donc ajouter une nouvelle URL /create.html permettant de créer un fichier X de taille donnée. La durée du test sera nettement raccourcie.
Une deuxième URL /freebytes.html est ajoutée afin que le logiciel de test puisse récupérer l'espace disponible.

Le logiciel de test va donc demander au serveur de créer N fichiers de 100Ko jusqu'à ce que l'espace soit presque plein.On ne peut pas simplement diviser l'espace disponible au départ par 100Ko et créer N fichiers. L'allocation des secteurs par le système de fichiers n'est pas prédictible.

La taille restante sera d'environ 4096 octets, la taille d'un secteur de la FLASH.
En dessous de cette valeur la méthode FFat.freeBytes() utilisée pour déterminer l'espace disponible retourne ZÉRO, ce qui ne nous arrange pas car on ne saurait quand s'arrêter.

Il ne nous restera plus qu'à effectuer une centaine d'opérations pour remplir le dernier secteur.

Test intensif
Ce test enchaîne 5000 ajouts de crédit.
Cela m'a permis de m'apercevoir qu'après l'ajout de 5000 lignes dans le fichier historique, lorsque l'on utilise la fonction explorer,  la lecture du fichier historique échoue. L'ESP32 n'a plus assez de mémoire.
Donc les fichiers sont affichés par page de 200 lignes avec un lien "More ...".
Les lignes sont maintenant numérotées.

Cette suite de test s'exécute comme ceci :

$ python3 test.py SpecialTest

2.3. Tests interactifs

Deux tests interactifs ont été également ajoutés :
  • ajout d'un abonné
  • débit
Le test d'ajout d'un abonné demande au testeur de passer par la page "Nouvel abonné" d'entrer un nom et de présenter une carte.

Le test de débit nécessite la présentation de la carte et l'appui sur le bouton-poussoir.

Il est à noter que chacun de ces test, s'il est effectué un peu avant le passage à la minute suivante, échouerait, d'où une petite attente, afin de laisser au testeur le temps de réaliser les opérations manuellement :

    def wait_minute_00(self) :
        dt = datetime.now()
        if dt.second < 30:
            return
        print("wait %s seconds please" % (60 - dt.second))
        time.sleep(60 - dt.second)


Cette suite de test interactifs s'exécute comme ceci :

$ python3 test.py InteractiveTest

3. La suite de test

Celle-ci est disponible ici :
https://bitbucket.org/henri_bachetti/webserver-form/src/v2.3/esp32-subscriber/test/test.py

On voit donc de nouveaux tests apparaître :
  • remise à ZÉRO du crédit
  • désactivation de l'abonné
  • réactivation de l'abonné
  • test de remplissage de la partition
Une nouvelle classe de base apparaît : BaseTest. Elle contient les méthodes de base qui seront utilisées par les classes qui en hériteront :
  • setUp() et tearDown() : voir article précédent
  • nettoyage du serveur
  • remplissage de la partition
  • récuparation de l'heure
La récupération de l'heure comporte une petite astuce particulière : elle fait une pause de 2 secondes au moment où les secondes arrivent à 59 afin d'éviter les problèmes de comparaison d'heure dans les tests.
Le PC peut en effet demander au serveur d'effectuer une opération qu'il effectuera à 10:44:59, et il est probable que le logiciel de test récupère ensuite une heure légèrement différente : 10:45:00. La comparaison échouerait.

L'heure du PC et de l'ESP32 ne sont pas parfaitement synchronisées à la seconde près. C'est pour cette raison que l'on évite généralement de contrôler des heures à la seconde près.

Le test de remplissage de la partition est situé dans une classe à part SpecialTest afin de permettre son exécution séparément des autre tests, car son exécution prend pas mal de temps : 2 minutes.

$ python3 test.py SpecialTest
.
----------------------------------------------------------------------
Ran 1 test in 122.269s

OK


L'exécution des tests nominaux est plus courte : 10 secondes.

$ python3 test.py AutomatedTest
......
----------------------------------------------------------------------
Ran 7 tests in 10.686s

OK


4. Téléchargement

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

5. Liens utiles

La suite :
Serveur ESP32 : implémentation (6eme-partie)


6. Conclusion

Une fois de plus le test automatisé permet de fiabiliser le logiciel. Sans test automatisé il m'aurait été impossible de m'apercevoir de ce que j'ai observé en test intensif (voir 2.2).

On voit également que souvent le code à tester évolue au fur et à mesure que les tests sont écrits, ce qui est parfaitement normal dans une démarche de test automatisé.

Il existe même une méthode dite "Test-driven development" (Développement piloté par les tests) qui préconise l'écriture des tests avant l'écriture du code.

Bien entendu la suite de tests n'est pas totalement terminée, mais cela ne justifie pas un article de plus.Les suppléments seront ajoutés dans celui-ci.

Pour conclure, je dirais que le test automatisé nécessite une investissement non négligeable, certes, mais cet investissement est largement compensé par le temps gagné par rapport à des tests effectués manuellement, surtout si on les répète souvent.
En fait on s'aperçoit très vite que l'on a tendance à lancer les tests après chaque journée ou semaine de travail, avant le commit (partage de son travail). Lorsque l'on travaille en équipe cela permet de ne pas perturber le travail des autres membres de l'équipe.
J'ai tellement pris goût à ces méthodes que même quand je travaillais seul, je continuais à faire de même.

J'espère que cette suite d'articles vous aura éclairé quant à la manière de concevoir un serveur ESP32, et surtout de le tester.


Cordialement
Henri

7. Mises à jour

27/03/2020 : ajout de tests
                     corrections
29/03/2020 : 1.3. Les détails / le changement d'heure

mardi 24 mars 2020

Serveur ESP32 : tests automatisés

Serveur ESP32 : tests automatisés


Ceci est la suite des cinq 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)

Maintenant que notre serveur est bien avancé il serait temps de démarrer une séance de test. Nous allons pour cela utiliser PYTHON et le framework UNITTEST.

Ce serveur est l'occasion de démontrer l'intérêt des tests automatisés. Il sera facile d'envoyer des requêtes à celui-ci et de vérifier que tout se passe comme prévu.

1. Tests automatisés

Pourquoi des tests automatisés ?
J'ai l'habitude de travailler comme cela depuis 20 ans et ces tests comportent tellement d'avantages que je ne pourrais plus travailler autrement.

1.1. Gain de temps

Une fois que les tests sont écrits, le temps d'exécution est ridicule par rapport à celui que prendrait l'exécution des mêmes tests faits à la main.

1.2. Répétabilité

Les tests logiciels sont répétables à volonté. On peut les exécuter autant de fois que nécessaire.

1.3. Régressions

A chaque modification de code une régression est possible. Une évolution peut introduire un bug, y compris dans du code déjà existant, par effet de bord par exemple.
L'exécution des tests automatisés permet de s'assurer que le code fonctionne comme avant la modification.

Il est beaucoup plus facile de modifier du code en profondeur (refactoring) quand on dispose de tests déjà écrits. Une fois le code modifié, le déroulement des tests permet de s'assurer que le code fonctionne toujours.
Un refactoring permet de réécrire des portions de code mal conçues, et le fait de redevoir refaire les tests à la main est souvent dissuasif, donc le refactoring n'est pas effectué, ce qui est une erreur.

1.4. Tests finaux

Le déroulement des tests automatisés ne dispense pas de réaliser des tests finaux (intégration, validation).
Ces tests finaux permettront aussi de constater des bugs. Souvent un bug est dû à l'absence d'un test unitaire.
Idéalement, chaque bug doit être l'occasion d'écrire un nouveau test, et ce test doit être OK après correction du bug.

1.5. Sérénité

Lorsqu'un projet est terminé et que l'on doit installer une version terrain, le déroulement des tests automatisés permet de livrer son travail avec beaucoup plus d'assurance et moins d'appréhension.

2. Logiciels nécessaires

Pour réaliser nos tests nous allons avoir besoin de quelques logiciels sur PC.

Il existe plusieurs solutions très élaborées, dont SELENIUM, qui nécessitent une mise en œuvre plus lourde.

SELENIUM procède par pilotage en direct du navigateur à l'aide d'un WebDriver approprié (Firefox, Safari, Edge, Chrome, Internet Explorer, etc.).
Le navigateur est donc actif et on peut voir en direct la navigation opérer pendant le test, comme une vidéo :
  • clicks sur les boutons, liens
  • entrée de caractères au clavier
  • changements de page HTML
  • etc.
La technique que je propose permet d'utiliser des composants simples, à la portée de tout un chacun. Elle est assez facile à comprendre par toute personne, même peu habituée au développement WEB :
  • envoi d'une requête à un serveur WEB
  • réception de la réponse
  • examen de la réponse (parsing)
  • comparaison du résultat par rapport à un résultat attendu
  • affichage des éventuels problèmes
SELENIUM est un sujet à part entière, plus complexe, qui fera sans doute l'objet d'un autre article.

2.1 PYTHON

PYTHON est un langage interprété très moderne dont j'ai déjà parlé sur ce blog.

2.1.1. PYTHON 2
La version 2.7 commence à dater, mais elle est toujours maintenue. La dernière release date d'octobre 2019.
PYTHON 2.7 a été très utilisé par le passé, d'où un support encore très actif.

2.1.2. PYTHON 3
PYTHON 3 est sorti en 2008, et comme il s'agit ici d'un nouveau projet, nous allons plutôt opter pour ce choix.

La reprise d'un ancien projet PYTHON 2.7 pour le porter en PYTHON 3 nécessiterait pas mal de travail, en fonction de la taille du projet.

Pour installer PYTHON3 sous Ubuntu :

sudo apt-get install python3

Pour installer PYTHON3 sous Windows :

https://www.python.org/downloads/

Choisir la dernière version (3.7.7 actuellement) :

https://www.python.org/downloads/release/python-377/

En bas de page choisir "Windows x86 executable installer" ou "Windows x86-64 executable installer", en fonction de l'architecture 32bits ou 64bits du PC.

2.2. UNITTEST

UNITTEST est un framework de tests unitaires PYTHON.
Il permet d'exécuter des tests automatisés. Les tests peuvent être organisés :
  • module (un programme PYTHON)
    • suite de test (une classe)
      • test (une méthode de la classe)
# test.py
import unittest

class TestStringMethods(unittest.TestCase):

    def test_upper(self):
        self.assertEqual('foo'.upper(), 'FOO')

    def test_isupper(self):
        self.assertTrue('FOO'.isupper())
        self.assertFalse('Foo'.isupper())

    def test_split(self):
        s = 'hello world'
        self.assertEqual(s.split(), ['hello', 'world'])
        # check that s.split fails when the separator is not a string
        with self.assertRaises(TypeError):
            s.split(2)

if __name__ == '__main__':
    unittest.main()


Il est possible d'exécuter tous les tests d'un même module, une seule classe ou un test en particulier :

python3 test.py
python3 test.py TestStringMethods
python3 test.py TestStringMethods.test_upper

2.3. URLLIB et HTMLParser

URLLIB est une librairie permettant d'envoyer une requête à un serveur et d'attendre sa réponse :

from urllib import request, parse
 

# envoi d'une requête GET
html = request.urlopen('http://192.168.1.18').read()
 

# envoi d'une requête POST
data = parse.urlencode({'action': 'add1'}).encode("ascii")
html = request.urlopen('http://192.168.1.18/', data=data).read()


URLLIB fait partie des librairies de base de PYTHON.

HTMLParser va nous permettre de décortiquer la réponse du serveur :

from html.parser import HTMLParser
 

class MyHTMLParser(HTMLParser):

  def __init__(self):
        HTMLParser.__init__(self)

        self.state = 'idle'
        self.title = ''

    def handle_starttag(self, tag, attrs):

        print("Encountered a start tag:", tag, attrs)
        if (tag == 'title') :
            self.state = 'title'

    def handle_endtag(self, tag):
        print("Encountered an end tag :", tag)
        self.state = 'idle'

    def handle_data(self, data):
        print("Encountered some data  :", data)

        if self.state == 'title':
            self.title = data


parser = MyHTMLParser()
parser.feed(str(html))

Les méthodes de la classe MyHTMLParser doivent être complétées pour récupérer les informations qui nous intéressent :
  • textes de label, de bouton
  • contenu et couleur d'un champ de saisie
Les lignes print() peuvent être laissées dans un premier temps pour y voir plus clair, et retirées ensuite.

L'exemple fourni est complet : voir 8. Téléchargement
Il s'agit d'un petit automate à états finis très simple à comprendre.

HTMLParser fait également partie des librairies de base de PYTHON.

Avec UNITTEST + URLLIB et HTMLParser nous avons donc tout le matériel nécessaire pour élaborer nos tests.

5. Exemple

    def test_first_launch(self):
        html = request.urlopen('http://192.168.1.18').read()
        self.parser.feed(str(html))
        self.assertEqual(self.parser.title, 'Subscribers')
        self.assertEqual(self.parser.page_title, 'SUBSCRIBERS')
        self.assertEqual(self.parser.subscriber_name, 'Unknown')
        self.assertEqual(self.parser.subscriber_id, '0')
        self.assertEqual(self.parser.subscriber_credit, '0 credits')
        self.assertEqual(self.parser.subscriber_history, '\\n')
        self.assertEqual(self.parser.status, 'No Subscribers registered')
        self.assertEqual(self.parser.status_color, 'background-color:red;color:white;')


Ici nous testons le premier lancement de notre serveur :
  • la requête est envoyée et la réponse reçue
  • les informations dont vérifiées :
    • le titre de la page : "Subscribers"
    • un titre en gras : "SUBSCRIBERS"
    • le nom de l'abonné "Unknown"
    • l'ID de l'abonné "0"
    • le solde de l'abonné "0 credits"
    • l'historique : vide
    • le status : "No Subscribers registered"
    • la couleur du status : blanc sur fond vert
Exécution :

$ python3 test.py Test.test_first_launch
.
----------------------------------------------------------------------
Ran 1 test in 0.662s

OK


L'exécution a réussi.

5. Quelques règles :

UNITTEST considère qu'une méthode est une méthode de test si son nom commence par "test". Les autres méthodes sont ignorées. Bien entendu les méthodes de test peuvent faire appel à toute méthode classique, ne sesait-ce que pour des raisons de factorisation.

Deux méthodes setUp() et tearDown() peuvent être appelées automatiquement par UNITTEST, si elles existent, en début et fin de chaque test :
  • setUp() : initialisations communes à tous les tests de la classe
  • tearDown() : terminaisons communes à tous les tests de la classe
La méthode spéciale setUp() est exécutée avant chaque test :

    def setUp(self):
        self.parser = MyHTMLParser()


Elle nous sert simplement à créer notre objet MyHTMLParser.

L'autre méthode spéciale tearDown() est exécutée après chaque test :

    def clear_all(self):
        request.urlopen('http://192.168.1.18/wipe.html').read()

   def tearDown(self):
        self.clear_all()


Elle est utilisée pour nettoyer le serveur après chaque test.

Pourquoi nettoyer après chaque test ? parce que UNITTEST lance les tests les uns à la suite des autres sans ordre précis.

Un test ne doit pas dépendre des conditions finales du test qui a été exécuté précédemment.

Par exemple le test de l'exemple du paragraphe 4 échouerait s'il était exécuté après un autre test ayant créé un abonné.
Le status en particulier ne serait pas ègal à "No Subscribers registered", forcément.

6. La testabilité

Le serveur ESP32 a été développé dès le départ pour être logiciellement testable.

Par exemple les champs d'un formulaire ont un nom explicite qui permet de les retrouver facilement dans la réponse à une requête.

Quelques requêtes supplémentaires ont été ajoutées :
  • visialisation des fichier sur le serveur
  • destruction des fichier sur le serveur
Dans cette version j'ai ajouté une requête permettent l'ajout d'un abonné en fournissant son nom et son ID. Cette méthode permet de se passer de la présentation de la carte RFID.

S'il fallait présenter une carte pour exécuter certains tests, cela ne s'appellerait plus du test automatisé.

Mais cela n'empêche pas d'écrire une suite de test spéciale dite "suite interactive" avec une interaction homme machine.

Exemple :
  • le test demande la présentation d'une carte
  • le testeur présente la carte et appuie sur <RETURN>
  • le test continue
Cette suite interactive sera déroulée suivant les besoins, probablement moins souvent que la suite de tests automatisés.

7. La suite de test

Celle-ci est disponible ici :
https://bitbucket.org/henri_bachetti/webserver-form/src/v2.2/esp32-subscriber/test/test.py

Elle comporte 3 tests :
  • test de premier lancement du serveur
  • test d'ajout d'un abonné
  • test d'ajout de 1 crédit + 5 crédits
Exécution :

$ python3 test.py
...
----------------------------------------------------------------------
Ran 3 tests in 2.003s

OK


L'exécution prend 2 secondes. Ce temps est bien inférieur à celui qu'il aurait fallu pour faire la même chose manuellement avec la souris et le clavier.

8. Téléchargement

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

9. Liens utiles

La suite :
Serveur ESP32 : tests automatisés (2ème partie)
Serveur ESP32 : implémentation (6eme-partie)


10. Conclusion

Dans le cadre d'un développement d'un serveur ESP32 cette méthode de test est facile à mettre en place, car le canal de communication permettant de dérouler les tests existe déjà : HTTP.

Faire la même chose avec un ARDUINO par la ligne série n'est pas impossible. On peut s'inspirer de cet article :
https://riton-duino.blogspot.com/2019/12/commander-un-arduino-par-la-ligne-serie_21.html

Cette suite de tests m'a déjà permis de trouver un bug dans gestion de la requête de nettoyage. La fiabilité ne peut qu'augmenter en continuant.

Bien sûr les tests sont loin d'avoir été tous écrits. Par exemple le contenu des fichiers n'est pas vérifié. Le travail continue donc.

J'espère vous avoir éveillé votre curiosité avec cette démarche de test.


Cordialement
Henri

lundi 23 mars 2020

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


Serveur ESP32 : implémentation

(5è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)

Dans cette 5ème partie nous allons nous occuper de la gestion des données des abonnés.
Comme vu dans l'article précédent ces données seront gérées sous forme de fichiers dans une partition FAT.

Rappel : seul l'ESP32 pourra offrir une possibilité de partitionnement entre SPIFFS et FAT.
Pour un ESP8266 il faudra forcément utiliser une SD.

1. Le fichier abonnés principal

Ce fichier contient les données principales :
  • nom de l'abonné
  • ID de l'abonné (N° de carte)
  • son état (actif, désactivé)
  • son solde
Ce fichier comporte une ligne par abonné :

Unknown:0,0,0
Henri Bachetti:1158992794,1,15
Martin Hirsch:1436301657,1,10
Michelle Tournier:1436648937,1,5
Jean Lain:1437378937,1,10
Arthur Leroy:1437071625,1,10
Sarah Humbert:-1011506900,1,5


Il est lu au démarrage et les données sont placées en mémoire RAM.

A chaque opération (ajout de crédit, désactivation, réactivation, etc.) le fichier est mis à jour.

2. Les fichiers historique

Pour chaque abonné il y un fichier historique à son nom.
Ce fichier retrace toutes les opérations effectuées sur son compte, avec les dates :

23/03/2020 15:16 : +10 credits
23/03/2020 15:19 : deactivated
23/03/2020 15:19 : activated
23/03/2020 15:19 : 0 credits
23/03/2020 15:19 : +5 credits


3. Mode opératoire

3.1. Nouvel abonné

Pour ajouter un nouvel abonné, un nouveau bouton "Add a new subsciber" est ajouté :

Cliquer sur ce bouton provoque l'affichage d'une nouvelle page :

Il suffit d'entrer le nom, d'approcher la carte du lecteur et cliquer sur le bouton "Read media".

Un message d'erreur sur fond rouge est affiché dans le champ status si :
  • l'utilisateur existe déjà
  • la carte n'est pas présentée ou illisible
  • il ne reste plus de place dans la partition FAT
  • une erreur d'écriture se produit
Sinon, on revient au formulaire principal et les données de l'abonné sont affichées avec un message "Subscriber is OK" sur fond vert.

3.2. Opérations abonné

Pour effectuer une opération pour un abonné il est possible de procéder de différentes manières :
  • approcher la carte du lecteur et cliquer sur le bouton "Read media". Le formulaire présentera les données de l'abonné.
  • recherche l'abonné par son nom ou son N° de carte
  • utiliser le bouton "abonnés.
Un message d'erreur sur fond rouge est affiché dans le champ status si :
  • l'utilisateur n'existe pas
  • la carte n'est pas présentée ou illisible
Ensuite il faut choisir l'opération à effectuer et cliquer sur OK. Le formulaire affrichera les nouvelles données de l'abonné.
Un message d'erreur sur fond rouge est affiché dans le champ status si :
  • il ne reste plus de place dans la partition FAT
  • une erreur d'écriture se produit
Sinon un message est affiché sur fond vert :
  • 1 credit added
  • 5 credit added
  • 10 credit added
  • Credits = 0
  • Subscriber is OK
En cas de désactivation de l'abonnement, un message "Subscriber is DEACTIVATED !!!" est affiché sur fond rouge.

4. Améliorations et petits trucs

4.1. Libellés cliquables

Les libellée des boutons radios sont maintenant cliquables :

<input type="radio" name="action" id=add1 value="add1" checked>
<label for="add1">Add 1 credit</label>


4.2. Explorateur

Un explorateur de fichier est accessible au moyen d'une URL cachée.
Elle n'est pas accessible depuis le formulaire d'accueil (pas de bouton, pas de lien hypertexte, il faut la taper manuellement dans la barre du navigateur) :
http://xxx.xxx.xxx.xxx/explore.html :

Cette page permet de visualiser les différents fichiers présents, et même de les détruire.
Elle est très utile pour contrôler le contenu des fichiers en cours de développement.

4.3. Nettoyage

Une autre URL cachée permet d'effacer tous les fichiers et de recréer un fichier abonnés vide :
http://xxx.xxx.xxx.xxx/wipe.html :

Cette URL est utilisée pour nettoyer le serveur en cours de développement, si l'on a écrit de mauvais contenus dans un plusieurs fichiers, donc en cas de bug, et que l'on veut repartir de ZÉRO.

5. BITBUCKET

Comme on l'a vu tout au long de ces quelques articles j'ai utilisé les services du serveur BITBUCKET pour partager mon travail.
BITBUCKET est un serveur git, au même titre que GITHUB.

A chaque nouvel article une nouvelle version est née (v1.0, v1.1, v2.0, v2.1).
Toutes ces versions sont encore diponibles, alors que si j'avais travaillé en local sur son PC, faire la même chose aurait été bien plus fastidieux.
Avec git le partage est grandement facilité.

git offre des facilités qui rendent la vie du développeur bien plus agréable. Par exemple comparer deux versions d'un fichier pour examiner les différences est un jeu d'enfant.

git nécessite un petit apprentissage, mais cela en vaut vraiment la peine. Il peut s'interfacer avec certains IDE, comme VisualStudioCode ou Eclipse.

6. Téléchargement

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

7. Liens utiles

La suite :
Serveur ESP32 : tests automatisés
Serveur ESP32 : tests automatisés (2ème partie)
Serveur ESP32 : implémentation (6eme-partie)

8. Conclusion


Le développement d'un petit serveur de ce genre sur ESP32 est très amusant et il est possible de lui associer très facilement une partie matérielle, ici un lecteur RFID.
Comme on le voit au paragraphe 4, il ne faut pas hésiter à doter le serveur de quelques petits outils qui facilitent la mise au point et le contrôle du résultat, et rendent donc les tests bien plus faciles.


Cordialement
Henri

dimanche 22 mars 2020

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


Serveur ESP32 : implémentation

(4ème partie)



Ceci est la suite des trois articles précédents :
Serveur ESP32 : implémentation
Serveur ESP32 : implémentation (2ème partie)
Serveur ESP32 : implementation (3eme-partie)

Dans cette 4ème partie nous allons parler des données.

Pour l'instant nous avons deux types de données :
  • les fichier HTML dans la partition SPIFFS
  • les données utilisateurs en RAM
Pour pouvoir enregistrer les données utilisateur en mémoire permanente il nous faudra un support de données non volatile, c'est à dire une mémoire qui ne sera pas vidée en cas de redémarrage du serveur ou de coupure secteur.

1. Une SD

L'espace mémoire d'une SD est énorme. A moins d'avoir à gérer un très grand nombre d'abonnés, ce n'est pas très judicieux. Cette solution nécessiterait un module SD. De plus mon expérience de la SD sur ARDUINO me laisse assez perplexe. La fiabilité n'est pas exceptionnelle, et dépend fortement de la SD.

2. EEPROM

L'EEPROM existant sur ARDUINO est émulée sur ESP32 dans un espace en mémoire FLASH appelé NVS (Non Volatile Storage).

Voici la table des partitions par défaut d'un ESP32 :

# Name,   Type, SubType, Offset,  Size, Flags
nvs,      data, nvs,     0x9000,  0x5000,
otadata,  data, ota,     0xe000,  0x2000,
app0,     app,  ota_0,   0x10000, 0x140000,
app1,     app,  ota_1,   0x150000,0x140000,
spiffs,   data, spiffs,  0x290000,0x170000,


On voit que cet espace a une taille de 0x5000 octets soit 20480 octets. C'est luxueux par rapport à un ARDUINO.

C'est un espace adressable directement, par adresse, sans notion de fichiers.
Cela convient-il ?
Oui, si le nombre d'abonnés est fixe.
Cela va nous obliger à partager l'espace disponible entre les différents abonnés. S'il y en a cinq au total cela permet d'avoir 4000 octets par abonné.
Un historique des opérations est envisagé, dans lequel chaque opération sera enregistrée avec une date et un libellé :

22/03/2020 16:33 : +10 credits

Une ligne ferait 30 caractères en moyenne.
Dans 4000 octets on pourrait enregistrer 133 lignes de 30 octets.
Cela pourrait convenir si les nombre d'opérations et d'abonnés est limité. Si l'on envisage 1 opération par mois et 10 abonnés, une année s'écoulera avant que notre espace soit saturé. C'est peu.

Mais le système que j'imagine a un nombre d'abonnés indéterminé. On peut en ajouter, en enlever. Cela ne semble pas optimal.

3. Fichiers SPIFFS

Cela semble une bonne solution. Nous disposons déjà d'un espace SPIFFS conséquent (voir la ligne spiffs dans la table des partitions).
Cet espace a une taille de 0x170000 octets soit 1507328 octets. C'est déjà beaucoup plus spacieux, et les données pourront être organisées :
  • un fichier global contenant une ligne par abonné
    • nom
    • ID
    • validité de la carte
    • solde
  • un fichier par abonné pour l'historique
Cet espace contient déjà les fichiers HTML, mais est-il utilisable pour nos données ?
Oui et non. On peut effectivement créer des fichiers mais ceux-ci disparaîtront lorsque nous devrons recharger les ressources HTML.
Pourquoi ? parce que le logiciel de chargement des données SPIFFS, le fameux "ESP32 Sketch Data Upload" vu dans la première partie, construit une image de partition contenant les fichiers HTML (ou autres) et la charge dans la FLASH de l'ESP32. Cette nouvelle partition écrase la première et nos précieux fichiers abonnés disparaissent.

4. Fichiers FAT

Il existe une solution pourtant, créer deux partitions :
  • une partition dédiée aux ressources
  • une partition dédiée aux données
Comment faire ? il faut créer une nouvelle table des partitions.
Et pendant que nous y sommes pourquoi ne pas adopter une partition FAT qui nous apportera plus de performances ?

Cette solution ne sera adaptée qu'à l'ESP32.

5. Plus d'espace

L'ESP32 n'est pas limité à 4Mo. Il existe des versions 16Mo pour des applications plus exigeantes.
Il est possible aussi d'utiliser un espace de stockage déporté, par exemple une RASPBERRY PI pourrait offrir les services d'un serveur HTTP permettant d'enregistrer et relire nos données grâce à une base MYSQL ou SQLITE.

Dans ce cas, on se demande s'il ne serait pas plus intéressant de brancher le lecteur RC522 directement sur la RASPBERRY PI. Après tout, le SPI fait partie de ses possibilités et le RC522 est supporté :
https://raspberry-lab.fr/Composants/Module-RFID-RC522-Raspberry-Francais/

Le seul intérêt de l'ESP32 dans ce cas est sa taille miniature.

6. Partitionnement

Nous allons maintenant examiner en détail la solution 4. Fichiers FAT.
Rappel : seul l'ESP32 pourra offrir cette possibilité.

Les différents schémas de partitionnement sont décrits dans des fichiers CSV situés dans le répertoire suivant :

Sous Linux :
/home/username/.arduino15/packages/esp32/hardware/esp32/1.0.4/tools/partitions

Sous OS X :
/Users/username/Library/Arduino15/packages/esp32/hardware/esp32/1.0.4/tools/partitions

Sous Windows :
C:\Users\username\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.4\tools\partitions

Voici le fichier default.csv :

# Name,   Type, SubType, Offset,  Size, Flags
nvs,      data, nvs,     0x9000,  0x5000,
otadata,  data, ota,     0xe000,  0x2000,
app0,     app,  ota_0,   0x10000, 0x140000,
app1,     app,  ota_1,   0x150000,0x140000,
spiffs,   data, spiffs,  0x290000,0x170000,


La partition SPIFFS démarre en 0x290000 et a une taille de 0x170000 octets.

Nous allons d'abord copier ce fichier en le nommant spiffs_fat.csv et puis nous allons le modifier :

# Name,   Type, SubType, Offset,  Size, Flags
nvs,      data, nvs,     0x9000,  0x5000,
otadata,  data, ota,     0xe000,  0x2000,
app0,     app,  ota_0,   0x10000, 0x140000,
app1,     app,  ota_1,   0x150000,0x140000,
spiffs,   data, spiffs,  0x290000,0x10000,
ffat,     data, fat,     0x2A0000,0x160000,


La partition SPIFFS a maintenant une taille de 0x10000 (64Ko), la partition FAT démarre donc en 0x290000 + 0x10000 = 0x2A0000. Elle a une taille de 0x170000 - 0x10000 soit 0x160000 octets.

64Ko seront plus que suffisants pour stocker nos fichiers HTML. L'espace FAT a quant à lui une taille de 0x160000 = 1441792 octets.
Dans cet espace on pourrait enregistrer 48059 lignes de 30 octets.
Si l'on envisage 1 opération par semaine et 10 abonnés, 92 années s'écouleront avant que notre espace soit saturé.

Nous sommes parfaitement à l'aise cette fois-ci.

Notre fichier spiffs_fat.csv est prêt. Il faut l'ajouter au fichier boards.txt dans le répertoire suivant :

Sous Linux :
/home/username/.arduino15/packages/esp32/hardware/esp32/1.0.4

Sous Windows :
C:\Users\username\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.4

Voici, en gras, ce qu'il faut ajouter :

esp32.menu.PartitionScheme.default=Default 4MB with spiffs (1.2MB APP/1.5MB SPIFFS)
esp32.menu.PartitionScheme.default.build.partitions=default
esp32.menu.PartitionScheme.defaultffat=Default 4MB with ffat (1.2MB APP/1.5MB FATFS)
esp32.menu.PartitionScheme.defaultffat.build.partitions=default_ffat
esp32.menu.PartitionScheme.defaultspiffs_fat=Default 4MB with spiffs+ffat (1.2MB APP/1.5MB SPIFFS64K+FAT)
esp32.menu.PartitionScheme.defaultspiffs_fat.build.partitions=spiffs_fat


Ensuite il faut recharger nos resssources HTML, car la partition SPIFFS n'est plus au même endroit en mémoire :

Menu "Outils / ESP32 Sketch Data Upload"

L'ESP32 redémarre et nous avons donc maintenant deux espaces SPIFFS et FAT.

REMARQUE : en cas de réinstallation de l'IDE, ces fichiers ne sont pas censés dispararaître, mais je conseille tout de même d'en conserver une copie dans le projet, au cas ou l'on serait contraint de faire une réinstallation totale.

7. Liens utiles

La suite :
Serveur ESP32 : implémentation (5eme-partie)
Serveur ESP32 : tests automatisés
Serveur ESP32 : tests automatisés (2ème partie)
Serveur ESP32 : implémentation (6eme-partie)

8. Conclusion

Nous sommes prêts à utiliser cette nouvelle partition FAT pour le stockage de nos données.
On peut essayer de jouer avec :
https://github.com/espressif/arduino-esp32/blob/master/libraries/FFat/examples/FFat_Test/FFat_Test.ino


Cordialement
Henri

vendredi 20 mars 2020

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



Serveur ESP32 : implémentation

(3ème partie)



Ceci est la suite des deux articles précédents :
Serveur ESP32 : implémentation
Serveur ESP32 : implémentation (2ème partie)

Dans cette 3ème partie nous ajoutons un lecteur de cartes et badges RFID MIFARE RC522.

1. Le RC522

Le RC522 est un lecteur capable de lire et écrire les cartes sans contact MIFARE. Ces cartes sont très utilisées pour le contrôle des accès à des locaux d'entreprises.

En général on utilise assez peu la mémoire de stockage interne. On se contente de lire ne N° de carte (UID), et les accès sont gérés par un serveur, ce qui permet de vous radier facilement lorsque vous quittez l'entreprise, et également de palier facilement aux pertes de cartes.

Ici nous allons procéder de la même façon. Les données utilisateur seront stockées sur le serveur.
Le lecteur est branché comme suit sur l'ESP32 :
  • 3.3V : 3.3V
  • RST : pin D5
  • GND : GND
  • MISO : D19
  • MOSI : D23
  • SCK : D18
  • SS (SDA) : D4
La librairie utilisée est la suivante :
https://github.com/miguelbalboa/rfid.git

Il est recommandé de charger tout d'abord l'exemple readNUID afin de vérifier que tout fonctionne correctement.

Dans cet exemple il faut changer quelques constantes :

#define SS_PIN 4
#define RST_PIN 5



2. L'implémentation

2.1 Le module abonnés

Le code implémenté dans le fichier subscriber.cpp ne change pas fondamentalement.
La méthode readMedia() reprend en grande partie le code de l'exemple readNUID.
Lorsque l'UID de la carte est lu readMedia() effectue une recherche dans la liste des abonnés en comparant l'UID de la carte avec l'ID de chaque abonné.

En cas d'erreur elle retourne une valeur nulle ou négative :
  • ZERO : l'abonné n'existe pas
  • ERR_NOCARD : pas de carte
  • ERR_READCARD : erreur de lecture
  • ERR_CARDTYPE : la carte n'est pas une MIFARE
Le code d'erreur ERR_NOTFOUND est utilisé lors d'une recherche par ID ou par nom dans le formulaire, afin de pouvoir afficher un message d'erreur différent.

Un méthode d'ininitalisation begin() est ajoutée. Elle est appelée par la fonction setup().

Je me suis ajouté dans la liste des abonnés, et la carte RFID associée à mon profil est ma carte d'accès à mon ex-entreprise.

Voilà, c'est tout.

2.1. L'application

Les modifications de l'application (le sketch) concernent principalment la gestion des nouvelles erreurs.

3. Fonctionnement du lecteur

Apparemment le lecteur n'apprécie pas une trop grande proximité avec la carte. Si celle-ci est posée à même le lecteur, il a du mal à la lire plusieurs fois de suite.

Je ne pense pas qu'il s'agisse d'un problème d'alimentation.
Dans un projet fini il conviendrait de couvrir le lecteur d'une cible en matière plastique. De toutes manières il serait assez déconseillé de laisser le lecteur nu en façade d'un boîtier, surtout en extérieur. Et d'un point de vue esthétique le résultat serait déplorable.

Il peut être tentant de placer des LEDs gérées en PMM sur la cible. Mais c'est plutôt déconseillé. Le PWM génèrerait un champ magnétique qui perturberait le lecteur.

Certains constructeurs placent un écran TFT sur le lecteur. Vous avez certainement rencontré ce genre de terminal dans les transports en commun :


Le lecteur n'est pas un lecteur ARDUINO, il s'agit de matériel beaucoup plus élaboré. Le lecteur est certainement derrière l'écran, mais l'antenne est probablement située autour de celui-ci. Désolé, vous ne pourrez pas faire la même chose avec le lecteur RC522, vu que son antenne est imprimée sur le PCB.

4. Téléchargement

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

Comme c'est un ajout de fonctionnalité majeur, l'indice de version passe à 2.0.

Les autres version sont encore disponibles bien entendu : voir les articles précédents.

5. Liens utiles

La suite :
Serveur ESP32 : implémentation (4eme-partie)
Serveur ESP32 : implémentation (5eme-partie)
Serveur ESP32 : tests automatisés
Serveur ESP32 : tests automatisés (2ème partie)
Serveur ESP32 : implémentation (6eme-partie)

6. Conclusion

Voilà donc une partie importante du projet finalisée en peu de temps. Merci aux
concepteurs de librairies et en particulier à André Balboa, pour sa librairie RFID.

Ce n'est pas fini. Il reste la partie gestion des données utilisateurs.


Cordialement
Henri

jeudi 19 mars 2020

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



Serveur ESP32 : implémentation

(2ème partie)



Ceci est la suite de l'article précédent :
Serveur ESP32 : implémentation

Nous allons ajouter quelques petites facilités à notre serveur :
  • un lien permettant d'afficher les informations du serveur
    • titre
    • auteur
    • site WEB de l'auteur
    • version du serveur
  • une possibilité de rechercher un abonné :
    • par son identifiant
    • par son nom
  • un dictionnaire pour les variables de la template
    Cette évolution ne concerne que la version SPIFFS avec templates. Il me serait en effet difficile de maintenir 3 versions simultanément.
Il est parfaitement possible d'avoir une notion de template en utilisant des fichiers HTML codés en dur (voir versions USE_SPRINTF et USE_STRING de l'article précédent), mais il faudra ajouter la fonctionnalité soi-même.
Je l'ai déjà réalisé ici : un-web-server-sur-ethernet

1. Informations du serveur

Ces information sont affichées par la fonction JAVASCRIPT alert() :

function about() {
  var message = document.getElementById("title").value;
  message += "\r\n\r\nAuthor: ";
  message += document.getElementById("author").value;
  message += "\r\nWebSite: ";
  message += document.getElementById("website").value;
  message += "\r\nVersion: ";
  message += document.getElementById("version").value;
  alert(message);
};


Que sont ces nouveaux objets "title", "author", "website" et "version" ?
Ce sont des objets cachés dans la page HTML :

<input id="title" name="title" type="hidden" value="%PLACEHOLDER TITLE%">
<input id="author" name="author" type="hidden" value="%PLACEHOLDER AUTHOR%">
<input id="website" name="website" type="hidden" value="%PLACEHOLDER WEBSITE%">
<input id="version" name="version" type="hidden" value="%PLACEHOLDER VERSION%">


Ces objets cachés ont l'attribut hidden et ne sont pas visibles dans le navigateur.
Elles sont renseignées par le même mécanisme de variables templates décrit dans l'article précédent.

Les informations sont affichées lorsque l'on clique sur le lien "About ..." en bas de page :

<a href="#null" onclick="javascript:about();">About ...</a>


Comme on le voit la version est 1.1. En effet il aurait été malvenu de choisir 1.0 étant donné que la version 1.0 était la précédente.
La gestion de version est une notion qu'il ne faut pas négliger.

2. Rechercher un abonné

Deux liens hypertexte ... ont été ajoutés en face des champs "identifiant de l'abonné" et "nom de l'abonné" :


Il suffit maintenant d'entrer l'identifiant ou le nom de l'abonné et de cliquer sur les 3 points pour que la page affiche les informations de l'abonné.
Si l'abonné n'existe pas le status affiché est : Subscriber is UNKNOWN !!!

Comme pour le bouton N° d'abonné cette nouvelle fonctionnalité est réalisée en JAVASCRIPT :

<a href=javascript:void(0); onclick=idChanged()>...</a>
<a href=javascript:void(0); onclick=nameChanged()>...</a>

function idChanged(value) {
  var id = document.getElementById("subscriber_id").value;
  document.location ="/?id=" + id;
};
function nameChanged(value) {
  var name = document.getElementById("subscriber_name").value;
  document.location ="/?name=" + name;
};


Les deux arguments id et name seront interprétés par la fonction handleRoot de l'ESP32 :

    if (request->hasParam("id", false)) {
      const char *s = request->arg("id").c_str();
      uint32_t id;
      sscanf(s, "%lx", &id);
      subscriberIndex = Subscriber::fromId(id);
    }
    if (request->hasParam("name", false)) {
      subscriberIndex = Subscriber::fromName(request->arg("name").c_str());
    }


fromId() et fromName() sont deux méthodes de la classe Subscriber permettant de faire une recherche dans la liste des abonnés.

Il est à noter que l'identifiant de l'abonné est affiché maintenant en hexadécimal.
En effet c'est souvent avec cette notation que l'on affiche un N° de carte. S'il s'agissait d'un autre média, comme une carte à code barre, on pourrait adopter un autre format.

3. Le dictionnaire

Auparavant nous avions 9 variables à afficher dans la page. On passe à 13 (dont 4 cachées).

On ne peut décemment pas passer 13 variables à une fonction. cela devient très vite lourd, et il est rare que cela n'augmente pas.

J'ai donc ajouté une liste de variables (un dictionnaire composé de couples de chaînes nom + valeur).
Voici l'implémentation :
template.h
template.cpp

Elle s'appuie sur une liste standard de la librairie C++ STL (Standard Template Library). Cette librairie est très utilisée en développement C++.

Chaque entrée du dictionnaire est une instance de la classe DictionaryEntry, contenant un nom et une valeur.

Avant d'appeler la fonction qui va envoyer la réponse, l'application enregistre chaque variables à afficher :

  clearDictionary();
  addToDictionary("DATE", timeString);

  addToDictionary("SUBSCRIBER", subscriberIndex);
  // etc 

  addToDictionary("VERSION", version);
  SendSubscribersForm(request);
 

Le dictionnaire est vidé à chaque page par clearDictionary().

Ensuite la fonction responsable de la traduction des variables (PLACEHOLDERS) pour la template, lorsqu'elle est appelée, recherche le nom de la variable dans le dictionnaire :

String templateProcessor(const String& var)
{
  return getDictionaryValue(var.substring(strlen("PLACEHOLDER ")).c_str());
}


Le mot PLACEHOLDER ne fait pas partie du nom enregistré dans le dictionnaire. Si dans le fichier HTML on a :

%PLACEHOLDER DATE%

Dans le dictionnaire seule la variable nommée "DATE" est enregistrée. Il serait inutile d'enregistrer le nom complet "PLACEHOLDER DATE" puisque le premier mot est constant. Donc il suffit de rechercher le mot suivant, "DATE".

Si l'on revient à la version précédente de la fonction templateProcessor() :

String templateProcessor(const String& var)
{
  if (var == "PLACEHOLDER DATE") {
    return var_timeString;
  }
  else if (var == "PLACEHOLDER SUBSCRIBER") {
    return String(var_subscriberIndex);
  }
  else if (var == "PLACEHOLDER NSUBSCRIBERS") {
    return String(var_totalSubscribers);
  }
  else if (var == "PLACEHOLDER SUBSCRIBER_ID") {
    return String(var_subscriberId);
  }
  else if (var == "PLACEHOLDER SUBSCRIBER_NAME") {
    return var_subscriberName;
  }
  else if (var == "PLACEHOLDER SUBSCRIBER_CREDIT") {
    return String(var_credit);
  }
  else if (var == "PLACEHOLDER SUBSCRIBER_HISTORY") {
    return var_history;
  }
  else if (var == "PLACEHOLDER STATUS") {
    return var_status;
  }
  else if (var == "PLACEHOLDER STATUS_COLOR") {
    return var_statusColor;
  }
  return "?????";
}


Cela n'a plus grand chose à voir.

4. Téléchargement

Cette version 1.1 est disponible ici :
https://bitbucket.org/henri_bachetti/webserver-form/src/v1.1/esp32-subscriber/

5. Liens utiles

La suite :
Serveur ESP32 : implémentation (3eme-partie)
Serveur ESP32 : implémentation (4eme-partie)
Serveur ESP32 : implémentation (5eme-partie)
Serveur ESP32 : tests automatisés
Serveur ESP32 : tests automatisés (2ème partie)
Serveur ESP32 : implémentation (6eme-partie)

6. Conclusion

Pour cette évolution j'ai cherché avant tout à ne pas trop compliquer les choses avec trop de JAVASCRIPT. Le CSS est d'ailleurs absent jusqu'à présent.
Le fichier HTML reste assez simple :
https://bitbucket.org/henri_bachetti/webserver-form/src/master/esp32-subscriber/data/index.html

La version précédente :
https://bitbucket.org/henri_bachetti/webserver-form/src/v1.0/esp32-subscriber/data/index.html


Cordialement
Henri

mercredi 18 mars 2020

Serveur ESP32 : implémentation



Serveur ESP32 : implémentation


Cet article a pour but de présenter plusieurs méthodes d'implémentation pour un serveur HTTP sur ESP32 ou ESP8266.

Dans ce tutoriel nous allons aborder :
  • formulaire
  • objets divers :
    • bouton, bouton-radio
    • champ numérique avec boutons + -
    • champ texte, simple ou multiligne
  • un peu de JAVASCRIPT 
  • récupération de l'heure avec NTP
  • requêtes GET ou POST
  • envoi d'une réponse avec snprintf() 
  • envoi d'une réponse avec String
  • envoi d'une réponse à partir du SPIFFS avec une template

1. La classe WebServer

Cette classe fait partie de la librairie WebServer du package standard ESP32. Elle a déjà été utilisée dans l'article précédent :
https://riton-duino.blogspot.com/2020/03/arduino-ethernet-esp32-comparons.html

Elle permet d'implémenter un serveur très simplement. Nous n'allons pas revenir dessus.

2. La classe AsyncWebServer

Cette classe fait partie de la librairie ESPAsyncWebServer qui peut être trouvée ici :
https://github.com/me-no-dev/ESPAsyncWebServer
Comme dit sur la page la librairie AsyncTCP est nécessaire :
https://github.com/me-no-dev/AsyncTCP
Il existe une version ESP8266 :
https://github.com/me-no-dev/ESPAsyncTCP

Elle a l'énorme avantage de proposer, en autres, la gestion de pages HTML templates, ce qui va bien nous arranger.
Ceci sera expliqué plus loin.

3. Méthode GET ou POST

Comme je l'ai déjà expliqué deux méthode d'envoi de requête existent : GET ou POST.

En méthode GET les arguments suivent l'URL et on les voit dans la barre d'adresse du navigateur lorsque l'on valide le formulaire.
Les arguments font partie de l'URL, qui elle-même fait partie de l'entête HTTP.

En méthode POST les arguments ne sont pas visibles dans la barre d'adresse du navigateur lorsque l'on valide le formulaire.
Les arguments sont envoyés après l'entête HTTP. Un double caractère '\n' signale la fin de l'entête et donc le début des arguments.

On utilise en général la requête POST dans un formulaire. La requête GET est employée principalement dans la gestion de boutons ou de liens hypertexte.

4. JAVASCRIPT

Certains s'étonneront de voir du code JAVASCRIPT dans l'exemple qui va suivre. Comment se fait-il que l'on puisse exécuter du code JAVASCIPT dans une application ESP32 ?

Tout simplement l'ESP32 n'exécute pas le JAVASCRIPT. Les lignes JAVASCRIPT sont envoyées par l'ESP32 au sein du code HTML (ou dans des fichiers .js) et elles sont exécutées par le navigateur.

Pour l'ESP32 le code HTML et JAVASCRIPT ne sont que du texte. Il pourrait de la même façon envoyer un bout de code écrit en C++ ou une liste de courses, simplement le navigateur ne saurait pas quoi en faire, à part afficher le texte.

5. Affichage de pages à contenu variable

Il est assez rare, dans un logiciel serveur d'afficher une page statique, à moins qu'il ne s'agisse d'une page d'aide ou d'une page destinée à afficher la version du serveur, la liste des modifications, etc.
Dans la plupart des cas, nous aurons un mélange de code HTML fixe et de code HTML variable.

6. Les différentes méthodes d'envoi de réponse

Nous allons maintenant aborder la partie qui nous intéresse : quand le navigateur envoie une requête HTTP, comment faire pour lui répondre de manière correcte et efficace.

6.1. Utiliser un objet String

En général la solution utilisée est celle-ci :

  String msg = "<!DOCTYPE html>\n";
  msg += "<html>\n";
  msg += "<head>\n";
  msg += "<meta charset=\"UTF-8\">\n";
  msg += "<title>My Server</title>\n";
  msg += "</head>\n";
  msg += "<body>\n";
  msg += timeString;

  msg += "<br><br><h1>TITLE</h1>\n";
  // etc.
  request->send(200, "text/html", msg);


Dans la réponse à la requête du navigateur on vient insérer une variable timeString (la date et l'heure courante).

On voit des caratères quotes " échapés à l'aide de backslash : \"
Cet échappement est obligatoire en C si l'on veut insérer une quote dans une chaîne de caractères.
Résultat : fastidieux et assez illisible.

6.2. Utiliser snprintf

On peut également utiliser la vénérable fonction snprintf() (elle devrait bientôt fêter son demi-siècle d'existence) :

#define RESPONSE_SIZE     1000
char msg[RESPONSE_SIZE];
 

const char *page = "<!DOCTYPE html>\n"
                              "<html>\n"
                              "<head>\n"
                              "<meta charset=\"UTF-8\">\n"
                              "<title>Subscribers</title>\n"
                              "</head>\n"
                              "<body>\n"
                              "%s<br><br><h1>SUBSCRIBERS</h1>\n"

                              // etc
  snprintf(msg, RESPONSE_SIZE, page, timeString, etc.
  request->send(200, "text/html", msg);

Nous remarquons dans l'utilisation de la fonction snprintf() qu'un large buffer est utilisé est que nous passons son adresse et sa taille à la fonction, ceci afin d'éviter les débordements qui pourraient arriver si l'on utilisait simplement sprintf().
snprintf() apporte une sécurité supplémentaire, et c'est important.

La variable timeString est également passée en argument et sera formatée selon une chaîne de format %s (c'est à dire une chaîne de caractère).

snprintf() est une fonction à nombre d'arguments variables. Il est donc possible de passer plusieurs arguments à formater si la page HTML comporte plusieurs champs variables, donc plusieurs chaînes de format :
  • %s : chaîne de caractère
  • %d : variable entière (int)
  • %x : variable entière (int) à formater en hexadécimal
  • %l : variable entière (long)
  • %f : variable flotante (float)
  • la liste est longue.
Le nombre d'arguments variables doit être le même que le nombre de chaînes de format. Le format doit être cohérent avec le type de variable. On ne formate pas une variable entière avec une chaîne de format convenant à une chaîne de caractères, et vice-versa.

Par rapport à la solution précédente, il est assez difficile de maintenir une cohérence lorsqu'un nombre important de chaînes de format sont noyées au sein d'une longue page HTML. Il faut être rigoureux et contrôler que le bon nombre d'arguments est passé à snprintf() et que le format convient.

L'utilisation des quotes avec échappement est identique à la méthode précédente.
Résultat : fastidieux et à peine plus lisible.

Par contre cette méthode offre une rapidité d'exécution sans équivalent.

6.3. Utiliser une template

La troisième méthode passe par un fichier HTML stocké dans la partition SPIFFS de l'ESP32 :

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Subscribers</title>
</head>
<body>
%PLACEHOLDER DATE%<br><br><h1>SUBSCRIBERS</h1>

etc.

On voit dans ce fichier un identifiant %PLACEHOLDER DATE% qui sera remplacé par la valeur d'une variable lors de l'envoi.

Le fichier HTML doit être créé dans un répertoire data du projet et sera chargé dans la partition SPIFFS de l'ESP32 grâce au chargeur :

Menu "Outils / ESP32 Sketch Data Upload"
Menu "Outils / ESP8266 Sketch Data Upload"

Il est nécessaire d'installer un plugin dans l'IDE ARDUINO :
https://github.com/me-no-dev/arduino-esp32fs-plugin
https://github.com/esp8266/arduino-esp8266fs-plugin

Voici le template processor :

const char *var_timeString;
const char *var_statusColor;

String templateProcessor(const String& var)
{
  if (var == "PLACEHOLDER DATE") {
    return var_timeString;
  }

  // etc.
  else if (var == "PLACEHOLDER STATUS_COLOR") {
    return var_statusColor;
  }
  return "?????";

}

Ensuite dans le code du handler on passe le nom du fichier HTML et l'adresse d'une fonction de traitement des PLACEHOLDERs :

  server.on("/index.html", HTTP_GET, [](AsyncWebServerRequest * request) {
    request->send(SPIFFS, "/index.html", "text/html", false, templateProcessor);
  });

A chaque fois qu'un identifiant encadré par deux caractères % sera rencontré la fonction templateProcessor sera appelée. Elle renvoie la valeur à insérer dans la page HTML envoyée.
Bien entendu si la variable est un nombre entier ou flottant il faudra la convertir en String :

int var_subscriberId;
    return String(var_subscriberId);


Cette méthode est largement utilisée dans le monde du développement WEB (Cheetah par exemple en PYTHON).

Résultat : le fichier HTML est clair, et l'on n'est même pas perturbé par la présence des identifiants PLACEHOLDER.

Même si cette solution est coûteuse en temps d'exécution, elle soulage grandement le développeur.

7. Exemple

Un serveur exemple est récupérable ici :
https://bitbucket.org/henri_bachetti/webserver-form/src/v1.0/esp32-subscriber/

Une directive de compilation permet de tester l'une des trois méthodes :

#define USE_SPRINTF       1
#define USE_STRING        2
#define USE_SPIFFS        3
#define METHOD            USE_SPIFFS


Il suffit de changer :

#define METHOD            USE_SPRINTF
#define METHOD            USE_STRING

Il s'agit d'un petit serveur de gestion d'abonnés :


Le formulaire comporte un certain nombre d'objets :
  • date et heure courante (obtenue par NTP)
  • bouton "Read media"
  • champ champ N° de l'abonné
  • champ identifiant de l'abonné
  • champ nom de l'abonné
  • champ crédits de l'abonné
  • 6 boutons radio
    • ajouter 1, 5 ou 10 crédits
    • mettre les crédits à zéro
    • interdire l'abonné
    • réautoriser l'abonné
  • champ historique des actions
  • champ status 
  • bouton SUBMIT
Le bouton "Read media" permet de lire le média de l'abonné. Celui-ci peut de divers types :
  • carte magnétique
  • carte à puce
  • RFID
  • code barre
  • carte SD
  • etc.
Le champ "Subscriber Number" permet de parcourir la liste des abonnés.

esp32-subscriber.ino
Le skectch est assez classique. Il gère une seule URL : /
La callback handleRoot fait tout le travail, qu'il s'agisse de requêtes GET ou POST.

subscriber.cpp
Comme on peut le voir dans ce source, tout est virtuel bien entendu.
Les données sont en RAM, et le bouton "Read media" se contente d'appeler un fonction qui tire aléatoirement un numéro.
Aucun besoin de matériel. Un ESP32 suffit.

Dans une vraie application il faudrait ajouter un lecteur de média et enregistrer les données historiques dans un fichier.
Ceci sera probablement le sujet d'un prochain tutoriel.

index.html
Le fichier HTML comporte deux petites fonction JAVASCRIPT qui permettent de gérer les boutons "Read media" et "N° de l'abonné" :
<body>
<script type="text/javascript">
function subscriberChanged(value){
document.location ="/?subscriber=" + value;
};
function readMedia(){
document.location ="/?action=read_media"
};
</script>

La fonction subscriberChanged() est associée au champ "N° de l'abonné" :
<input type="number" id="subscriber" name="subscriber" value="%PLACEHOLDER SUBSCRIBER%"
min="1" max="%PLACEHOLDER NSUBSCRIBERS%" onchange="subscriberChanged(this.value)">
A chaque changement de valeur la page est réaffichée :

  document.location ="/?subscriber=" + value;

L'URL / est donc demandée avec l'argument subscriber=numéro. Cet argument est traité par la callback handleRoot du sketch.

La fonction readMedia() est associée au bouton "ReadMedia" :

<button onclick="readMedia()">Read media</button>

A chaque click la page est réaffichée :

document.location ="/?action=read_media"

L'URL / est donc demandée avec l'argument action=read_media. Cet argument est traité par la callback handleRoot du sketch.

8. Le petit plus : NTP

L'application demande l'heure au démarrage au serveur NTP (Network Time Protocol) pool.ntp.org :

#include <time.h>

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

  configTime(3600, 0, ntpServer);

Ensuite il suffit de lire l'heure et de la formater selon le besoin :

  char timeString[50];
  struct tm timeinfo;
 

  if (!getLocalTime(&timeinfo)) {
    Serial.println("Failed to obtain time");
    return;
  }
  else {
    strftime(timeString, sizeof(timeString), "%d/%m/%Y %H:%M", &timeinfo);
  }


C'est tout ? Oui
Pas la moindre librairie ? Non.

Pour une application sérieuse il serait souhaitable de demander l'heure périodiquement, ceci afin de mettre à jour l'heure de l'ESP32, car son oscillateur n'est pas d'une précision extraordinaire.

Une période d'une heure par exemple, ou même une journée, est suffisante. On peut utiliser un timer.

Vous trouvez cela manque de sérieux d'équiper un ESP32 d'un oscillateur qui dérive tellement qu'il faille réajuster l'heure aussi souvent ?
Votre montre est plus précise ?

Sachez-le : un ARDUINO dérive encore plus.

Avouez que grâce au NTP un circuit RTC (Real Time Clock) paraît absolument superflu.

9. Petits défauts

9.1. snprintf

L'utilisation de snprintf() impose quelques précautions quand à la taille du buffer d'émission. Dans cet exemple elle est de 1950 octets.
Si la taille était insuffisante, la réponse serait tronquée. Dans le code la taille est testée :

#define RESPONSE_SIZE     1950
char msg[RESPONSE_SIZE];
 

  size_t sz = strlen(msg);
  if (sz > RESPONSE_SIZE - 50) {
    Serial.println("WARNING");
    Serial.print("response size: "); Serial.println(sz);
    Serial.print("maximal size: "); Serial.println(RESPONSE_SIZE);
  }


Si le buffer est presque plein, ou tout à fait plein, un message est affiché sur le terminal :

WARNING
response size: 1912
maximal size: 1950


Les risques de plantage dû à un débordement du buffer sont donc inexistants.

9.2. String

L'utilisation de String est lente. Pourquoi ?
A chaque fois que l'on ajoute une chaîne de caractère à un objet String, cela provoque une ré-allocation de mémoire. Cela prend du temps.

9.3. AsyncWebServer

La classe AsyncWebServer n'est pas tout à fait aussi pratique que la classe WebServer dans la manipulation des arguments de la requête.
Dans cette petite application j'ai choisi de traiter les requêtes GET et POST dans la même fonction callback handleRoot (j'ai fait ce choix car cela m'arrange d'un point de vue factorisation du code) :

  int subscriberIndex = 1;
  if (request->hasArg("subscriber")) {                      // GET
    subscriberIndex = request->arg("subscriber").toInt();
  }
  if (request->hasParam("subscriber", true)) {         // POST
    subscriberIndex = request->getParam("subscriber", true)->value().toInt();
  }


Dans cet exemple, suivant qu'il s'agisse d'une requête GET ou POST, l'accès aux arguments diffère. Pourquoi les dévloppeurs de AsyncWebServer ont-ils fait ce choix ? mystère.

L'utilisation de la classe WebServer est plus simple :

  int subscriberIndex = 1;
  if (server->hasArg("subscriber")) {                      // GET ou POST
    subscriberIndex = server->arg("subscriber").toInt();
  }


Mais ce petit défaut de la classe AsyncWebServer est minime. Il suffit de connaître le problème.

10. Performances

Le temps d'envoi de la réponse est le suivant :
  • version snprintf() : 2ms
  • version String : 4ms
  • version SPIFFS : 5ms
La version SPIFFS n'est pas beaucoup plus pénalisante que la version String. Autant utiliser le SPIFFS et les templates.
La version snprintf() est la plus rapide.

11. Empreinte mémoire

En utilisant la classe WebServer la quantité de mémoire utilisée par l'exemple de l'article cité plus haut et la suivante :

Code: 754574 octets (57%) 
Variables: 45264 octets (13%)

La classe AsyncWebServer est un peu plus gourmande en code :

Code: 794582 octets (60%)
Variables: 43544 octets (13%)

Les deux codes sont assez comparables. On peut donc estimer que la différence est négligeable.

La version SPIFFS utilisant les templates est à peine moins économe en FLASH. Elle est même plus économe en mémoire RAM :

Code : 800790 octets (61%)
Variables: 41648 octets (12%)

11. Téléchargement

Cette version 1.0 est disponible ici :
https://bitbucket.org/henri_bachetti/webserver-form/src/v1.0/esp32-subscriber/

12. Liens utiles

La suite :
Serveur ESP32 : implémentation (2ème partie)
Serveur ESP32 : implémentation (3eme-partie)
Serveur ESP32 : implémentation (4eme-partie)
Serveur ESP32 : implémentation (5eme-partie)
Serveur ESP32 : tests automatisés
Serveur ESP32 : tests automatisés (2ème partie)
Serveur ESP32 : implémentation (6eme-partie)

13. Conclusion

La classe AsyncWebServer ouvre de belles perspectives pour l'écriture de pages formulaires évoluées.
Les templates apportent un gain de temps appréciable. J'irais même jusqu'à dire que sans elles cette classe AsyncWebServer a peu d'intérêt.
J'espère que ce petit divertissement vous aura plu.


Cordialement
Henri