Techniques "avancées" de serveur web sur ESP8266

Bonsoir,

je démarre une nouvelle série de tuto pour faire un petit serveur web sur ESP qui fonctionne bien et rapidement, mais il me faudra un peu de temps pour la compléter - soyez patients car on commence par le commencement....


L'objectif de ce tuto est de dépasser les exemples simples (et lents) que vous pouvez trouver fournis avec votre ESP et pousser l'étude un peu plus loin afin de bâtir un mini site pour votre ESP sous environnement Arduino qui permette d'afficher dynamiquement l'état de votre programme et aussi de modifier certaines variables.

Si vous voulez un truc tout fait sans vous prendre la tête, je vous conseille d'aller voir FSBrowserNG

c'est un serveur web asynchrone utilisant SPIFFs avec plein de fonctions sympa...

Mais si vous voulez apprendre ou vous challenger les neurones, continuez à lire :slight_smile:


Le monde des ESP étant changeant, pour que ce qui suit fonctionne pour vous il faut avoir installé la dernière version du code ESP8266 pour Arduino --> ça ne se fait pas automatiquement il faut cloner ce GitHub à la main dans hardware/esp8266com/esp8266 (comme documenté sur le Github). Si vous utilisez la fonction de l'IDE pour charger le code, vous aurez une ancienne version et ce qui suit ne compilera pas.


On va y aller pas à pas car il y a de nombreux concepts et techniques à maîtriser, mais je ne rentrerai pas dans les détail du HTML et CSS et JavaScript, il y a des cours très bien chez openClassroom pour cela.

Introduction au HTML

Apprenez à créer votre site web avec HTML5 et CSS3

Dynamisez vos sites web avec JavaScript !

Apprenez à coder avec JavaScript

Nous allons aussi utiliser AJAX (pour Asynchronous JavaScript And XML) et pour ceux qui veulent aller plus loin il y a ce cours éventuellement à suivre

AJAX et l'échange de données en JavaScript


1 Like

1. Utilisation du SPIFFS avec un ESP8266

les processeurs ESP8266 embarquent un plus ou moins grosse mémoire flash accessible en SPI. C'est dans cette mémoire que vous chargez votre code mais il est aussi possible de stocker des fichiers directement dans cet espace. Stocker des fichiers veut dire qu'il faut un système de gestion de fichiers, et SPIFFS c'est cela: SPI Flash Filing System

On verra dans ce premier tuto comment configurer l'IDE pour charger des fichiers dans cet espace. Pour ce tuto j'utilise un WemosD1 mini R2 qui dispose de 4Mo de mémoire flash et qui est architecturé sur un ESP-8266EX

L'approche est similaire pour d'autres type d'ESP.

SPIFFS a été conçu pour être peu gourmand en resources processeur (ram) et offre des fonctions simples de haut niveau comme ouvrir, fermer, lire, écrire ou se positionner à un endroit précis d'un fichier. Utilisant la mémoire flash qui a tendance à s'user vite si on écrit toujours au même endroit, le SPIFFS offre une approche simple (statique) mais relativement efficace d'allocation des blocs pour éviter une usure prématurée de votre ESP.

Cependant n'y voyez pas l'équivalent dun file system moderne. il n'y a pas de correction d'erreur et il n'y a pas d'arborescence de fichier (répertoires et sous répertoires), tout est au même niveau.

Cependant le caractère / est accepté dans un nom de fichier ce qui fait que vous pouvez stocker un fichier du nom de "/web/index.htm" si vous voulez avoir quelque chose de structuré conceptuellement. Attention cependant les noms de fichiers sont limités à 32 caractères, y compris le '\0' de fin de c-string donc 31 caractères utiles.

ESP8266FS

Pour charger des fichiers directement depuis l'IDE il faut installer un plugin qui va copier les fichiers au bon endroit. Cela n'écrasera pas votre sketch que vous avez éventuellement déjà chargé, les zones mémoires étant bien définies (mais si vous changez la taille de la partition faut tout recharger)

Pour cela rendez vous sur Arduino plugin for uploading files to ESP8266 file system et suivez les instructions d'installation du gitHub "releases page"

1. Créez un répertoire tools dans votre dossier Arduino standard

2. Copiez dedans le fichier ESP8266FS-0.3.0.zip que vous avez téléchargé (à l'heure où j'écris ces lignes)

3. Dé-zippez le ficher, ça va créer dans .../Arduino/tools l'arborescence suivante: ESP8266FS/tool/esp8266fs.jar

4. Quittez et relancez l'IDE

5. Créez et sauvez à l'endroit qui vous convient un nouveau sketch vide sous le nom de testSPIFFS.ino

6. Dans l'explorateur de fichier de votre ordinateur, allez dans le dossier de ce nouveau sketch et créez un répertoire sous le nom de data (donc à côté de votre .ino) -> Ce sera la racine de ce qui sera transféré dans la mémoire flash gérée par SPIFFS

7. Prenez un éditeur de texte et créez un fichier contenant le code HTML suivant

<!DOCTYPE html>
<html>
<head>
 <meta charset="utf-8" />
 <title>Démo SPIFFS</title>
</head>
<h1>Bonjour à tous!</h1>
</body>
</html>

8. sauvez ce fichier sous le nom de hello.html dans le répertoire data de votre sketch, en forçant le format UTF-8 (pour qu'on ait les accents comme il faut).

9. Une fois que le fichier est sauvé en UTF-8 dans le répertoire data du sketch, retournez dans l'IDE et assurez vous que la console Série soit fermée, que le bon port Série et la bonne "board" arduino soit sélectionnée. Dans le menu tools vous verrez une entrée ESP8266 Sketch Data Upload.

dataUpload.png

Notez que suivant votre carte, vous pouvez choisir une certaine répartition de l'espace flash: une partie pour le programme et une partie pour les données. Ici pour mon Wemos je peux avoir soit 1 Megas soit 3 Megas pour la partie SPIFSS

Plus vous prenez un espace de stockage important, plus le téléchargement des fichiers du répertoire data va durer longtemps car il initialise toute l'image mémoire. prenez 1M pour le SPIFFS si vous n'avez pas besoin de plus. (notez que si vous allez choisir d'autres types d'ESP vous aurez plus de choix de configuration de votre espace mémoire total, assurez vous d'avoir toute la place voulue en flash pour votre sketch).

10. Choisissez le menu "ESP8266 Sketch Data Upload", ça déclenche le téléchargement de votre répertoire data dans la mémoire des données flash de votre ESP. (si vous n'avez pas un ESP qui se fonctionne tout seul en téléchargement, il vous faudra le mettre en mode flash)

Regardez en dessous de votre sketch dans la console de compilation, il y a des informations qui défilent pour montrer la taille de vos données et ensuite la progression du chargement. (j'ai choisis un SPIFFS de 1 Mega)

Voici ce qui est affiché (on voit que je charge 1 Mega de données pris dans le répertoire data de mon sketch, transmis à 115200 bauds, donc ça prends assez longtemps)

[sub][SPIFFS] data   : ~/Desktop/[color=red]testSPIFFS/data[/color]
[SPIFFS] [color=red]size   : 1004[/color]
[SPIFFS] page   : 256
[SPIFFS] block  : 8192
/hello.html
skipping .DS_Store
[SPIFFS] upload : /var/folders/xk/mwl4yt3j5rsgdmgmgjlqsk7w0000gn/T/arduino_build_803691/testSPIFFS.spiffs.bin
[SPIFFS] address: 0x300000
[SPIFFS] reset  : nodemcu
[SPIFFS] port   : /dev/cu.Repleo-CH341-00002314
[SPIFFS] speed  : [color=red]115200[/color]

[color=red]Uploading 1028096 bytes[/color] from /var/folders/xk/mwl4yt3j5rsgdmgmgjlqsk7w0000gn/T/arduino_build_803691/testSPIFFS.spiffs.bin to flash at 0x00300000
................................................................................ [  7% ]
................................................................................ [ 15% ]
<snip>
................................................................................ [ 95% ]
............................................                                     [ 100% ][/sub]

OK donc maintenant on a dans le système de fichier de la mémoire flash un petit fichier HTML.

c'était pas si dur que cela, n'est-ce pas? en gros on met dans le répertoire data du sketch ce que l'on veut envoyer côté SPIFFS et on utilisera le menu pour le charger... simple !


Ecrivons un bout de code qui va nous permettre de le voir dans un navigateur web

Pour cela il nous faudra utiliser les fonctions de la libraire SPIFFS et donc notre code doit contenir un

#include "FS.h"

Vous pouvez lire sur cette page les commandes (comme begin() open(), rename() etc ) proposées par la librairie

Mettez le code suivant dans votre sketch en adaptant le SSID et le mot de passe à votre réseau Wi-Fi personnel

#include "FS.h" // pour le SPIFFS
const char * nomDeFichier = "/hello.html";

// provient de https://github.com/esp8266/Arduino 
// télécharger et installer à la main la dernière version
#include <ESP8266WiFi.h>                


const char* ssid = "*****"; // <<--- METTRE ICI VOTRE NOM RESEAU WIFI
const char* password = "*******"; // <<--- METTRE ICI VOTRE MOT DE PASSE WIFI

const uint16_t HTTPPort = 80;
WiFiServer serveurWeb(HTTPPort); // crée un serveur sur le port HTTP standard

void printHTTPServerInfo()
{
  Serial.print(F("Site web http://")); Serial.print(WiFi.localIP());
  if (HTTPPort != 80) {
    Serial.print(F(":"));
    Serial.print(HTTPPort);
  }
  Serial.println();
}

void testRequeteWeb()
{
  boolean currentLineIsBlank = true;

  WiFiClient client = serveurWeb.available();
  if (!client) return; // pas de client connecté

  while (client.connected()) {
    if (client.available()) {
      // on lit toute la trame HTPP, ici sans se soucier de la reqête
      char c = client.read();

      if (c == '\n' && currentLineIsBlank) { // une requête HTTP se termine par une ligne vide
        // ON GENERE LA PAGE WEB
        // On envoie un en tête de réponse HTTP standard
        client.println(F("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n"));
        
        if (SPIFFS.exists(nomDeFichier)) {
          File pageWeb = SPIFFS.open(nomDeFichier, "r");
          client.write(pageWeb);
          pageWeb.close();
        } else {
          Serial.println(F("Erreur de fichier"));
        }
        // on sort du while et termine la requête
        break;
      }
      if (c == '\n') currentLineIsBlank = true;
      else if (c != '\r') currentLineIsBlank = false;
    } // end if available
  } // end while
  delay(1);
  client.stop(); // termine la connexion
}


void setup() {
  Serial.begin(74880); // parce que mon Wemos et par défaut à peu près à cette vitesse, évite les caractères bizarre au boot
  Serial.println("\n\nTest SPIFFS\n");

  // on démarre le SPIFSS
  if (!SPIFFS.begin()) {
    Serial.println("erreur SPIFFS");
    while (true); // on ne va pas plus loin
  }

  WiFi.begin(ssid, password);

  Serial.println();
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.write('.');
  }
  Serial.println();

  // on démarre le serveur
  serveurWeb.begin();
  printHTTPServerInfo();

}

void loop() {
  testRequeteWeb();
}

La fonction testRequeteWeb() se met en attente d'un client web, quand elle en a un elle se met en attente de la requête HTPP qui se termine par une ligne vide (dans ce code on ne fait rien d'autre qu'attendre cette ligne vide) et une fois qu'on a eu la ligne vide on a ce bout de code tout simple à comprendre:

      // On envoie un en tête de réponse HTTP standard
        client.println(F("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n"));
        
        if (SPIFFS.exists(nomDeFichier)) {
          File pageWeb = SPIFFS.open(nomDeFichier, "r");
          client.write(pageWeb);
          pageWeb.close();

dans un premier temps on envoie un petit en-tête HTTP correct, puis on ouvre le fichier en lecture et si ça a fonctionné alors on l'envoie directement au client et on referme le fichier, puis on sort du while et on termine la connexion.

Compilez et chargez ce code dans votre ESP. La console Série, que vous avez pris soin de régler à 74880 bauds va vous afficher

```
ets Jan  8 2013,rst cause:2, boot mode:(3,6)

load 0x4010f000, len 1384, room 16
tail 8
chksum 0x2d
csum 0x2d
v303a71de
~ld

Test SPIFFS

...
Site web http://192.168.1.46
```

Prenez un navigateur web, tapez l'URL du site web http://192.168.1.46 que vous pouvez lire dans la console... et magie, notre page web est envoyée.

Voilà une première bonne chose de faite ! vous pouvez essayer avec des pages web plus importantes, vous verrez que ça va assez vite et c'est beaucoup mieux que d'avoir des client.print() comme on les trouve dans les sketches d'exemple

2. gestion dynamique du contenu

Les concepts:

Nous allons utiliser AJAX (Asynchronous JavaScript and XML) qui pour simplifier est l'usage de Javascript pour extraire de l'information d'un serveur web et l'injecter directement dans une page web sans rafraichir l'ensemble de la page.

Javascript est un langage de script, s'exécutant dans le navigateur web (si l'utilisateur ne l'a pas désactivé). il se charge en même temps que votre page web quand vous visitez un site et ensuite peut exécuter côté client un certain nombre de tâches pour vous.

Dans l'usage que nous allons en faire, nous utiliserons Javascript pour déclencher une requête GET particulière sur notre serveur web (notre WemosD1) à laquelle nous répondrons avec l'information requise. Pour cela il nous faut donc commencer par modifier la code précédent pour écouter exactement ce que nous demande la requête HTTP que nous recevons

une requête GET HTTP va se présenter sous forme d'un flux de données arrivant sur une instance de la classe WiFiClient retournée par votre instance du serveur web. ce flux de texte se présente sous la forme suivante:

——————————————

GET /une_URL_de_requete HTTP/1.1
Host: 192.168.1.46
Accept-Encoding: gzip, deflate
.. du bla-bla plus ou moins long
Accept-Language: fr-fr
Referer: http://192.168.1.46/
<<--- ici il y a la ligne vide

——————————————

Le code que l'on va écrire récupère la requête HTPP, regarde si la ligne commence par GET, auquel cas on sait que l'on a affaire à une requête HTPP et va extraire l'URL qui se trouve dans cette ligne. le code doit ensuite écouter tout le reste de la requête mais en gros ne va rien en faire, jusqu'à ce qu'on l'on reçoive la ligne vide qui marque la fin de la requête HTTP. (si vous avez lu d'autres de mes tutos c'est un code similaire que l'on écrit)

Une fois la requête HTTP entièrement reçue on va analyser le contenu de l'URL pour décider quoi renvoyer. Si on nous demande la page de garde du site "/" alors on renvoie le code de la page que l'on va lire dans la mémoire flash en SPIFFS comme vu précédemment, par contre si la requête (l'URL) est différente alors on va envoyer "quelque chose d'autre" et c'est là que la magie Javascript va rentrer en ligne de compte.

Si vous lisez quelque peu d'info sur le Document Object Model (DOM), vous verrez que cela vous permet d'accéder de manière programmatique aux blocs constitutifs de votre page web sous forme d'un jeu d'objets reliés selon une structure en arbre et à l'aide de DOM un script peut modifier le document présent dans le navigateur en ajoutant ou en supprimant des noeuds de l'arbre.

Donc pour mettre à jour une partie d'une page web sans recharger toute la page, on va écrire un petit script (en Javascript) qui va envoyer une requête GET à notre Arduino lui demandant de lui retourner un peu d'information. Le script va alors traiter cette information pour mettre à jour un des noeuds de l'arbre. Pour faire simple, si la réponse est un petit bout the HTML, le script peut purement est simplement remplacer un noeud (un bout de code HTML) par le nouveau code HTML reçu.

Pour cela il faut pouvoir bien sûr savoir quel noeud modifier. Un noeud en HTML peut être reconnu par un identifiant (id). Le script que l'on pourrait écrire se résumerait donc à

1/ envoyer une requête HTTP de type GET pour obtenir un bout de code HTML correspondant à l'identifiant d'un noeud connu
2/ attendre et recevoir la réponse du serveur
3/ chercher dans l'arborescence DOM de la page existante le noeud qui a cet identifiant
4/ replacer le contenu de ce noeud par le nouveau code reçu

c'est tout simple mais il fallait y penser :slight_smile:

Il existe plusieurs approches pour donner un nom à un noeud de l'arbre. Soit ce noeud correspond à une balise capable d'être nommée (la majorité des balises - comme par exemple un bouton, un paragraphe, etc) et dans ce cas il suffit d'ajouter id = "toto" dans la balise pour lui donner le nom toto, soit si on veut rentre variable une partie un peu arbitraire de la page, on va nommer une zone dans la page et cela peut se faire avec la balise <div> (qui va faire un saut de ligne et qui sert de structuration de code) ou la balise <span> qui elle vraiment est invisible et peut servir à désigner sous un certain nom une sous partie de la page.

Vite un exemple !! imaginons que nous voulions afficher sur la page web si un bouton est appuyé ou pas et écrire cela en français "le bouton est actif en ce moment" ou "le bouton est inactif en ce moment". La partie dynamique de cette page c'est simplement le mot actif ou inactif qui doit changer en fonction de l'état du bouton côté Arduino

On pourrait donc avoir une page web qui affiche le texte sous cette formele bouton est <span id="idBouton">...</span> en ce momentLorsque l'on charge la page, on verra les ... car on n'a pas d'information. Ces ... représentent le contenu HTML du noeud défini par le <span> sous le nom idBouton.

Si on rajoute un bouton HTML sur cette page qui déclenche un javascript qui va lancer une requête HTTP puis demander à replacer le noeud qui a pour id "idBouton" par le résultat d'un requête à notre arduino du genre


GET /reqEtatBouton HTTP/1.1
Host: 192.168.1.46
Accept-Encoding: gzip, deflate
...


notre code Arduino comprendrait que l'on veut l'URL /reqEtatBouton et n'aurait plus qu'à retourner le mot "actif" ou "inactif" tout simplement et le Javascript fera le reste du travail. Comme on retourne peu d'information, ça va être rapide et grace à la modification direct du DOM la page ne se re-dessinera pas entièrement.

Une dernière subtilité à connaître avant de passer à la pratique: le cache.

Les navigateurs peuvent essayer d'optimiser la demande de resources et lorsqu'ils voient passer une requête GET pour une URL qu'ils ont déjà vu ils n'envoient pas la requête au serveur et redonnent la réponse qu'ils avaient mis en cache; C'est pratique pour ne pas recharger des images, du CSS etc mais ce n'est pas pratique pour nous car on veut vraiment que le serveur reçoive la requête pour aller lire l'état du bouton et nous dire s'il est actif ou inactif.

Pour palier ce problème, il existe plusieurs stratégies (désactiver le cache ne fonctionne pas toujours suivant le navigateur pour une XMLHttpRequest même si c'est faisable) le plus simple reste de générer une requête différente à chaque fois que l'on appuie sur le bouton (ou alors faire un POST au lieu d'un GET mais on va rester simple) --> Donc Au lieu d'envoyer

GET /reqEtatBouton HTTP/1.1

on va envoyer un

GET /reqEtatBouton[color=green]ABCXYZ[/color] HTTP/1.1

ou ABCXYZ va varier à chaque appel. Comme la requête est construite en javascript, on peut fabriquer cela à l'exécution, vous verrez souvent des techniques comme insérer l'unix time (la date) ou un nombre aléatoire avec Math.random() juste après l'URL de requête.

Allez - on passe à la pratique:

Pour le Javascript il faudra lire un peu de théorie sur XMLHttpRequest()

le code de la fonction appelée par le bouton sera le suivant

function lireBouton() {
 var uniqueURL = "reqEtatBouton" + "&aleatoire=" + Math.trunc(Math.random() * 1000000);
 var request = new XMLHttpRequest(); // http://www.toutjavascript.com/reference/ref-xmlhttprequest.php

 // la fonction à appeler lors d'un changement d'avancement de la requête AJAX
 request.onreadystatechange = function() {

 if (this.readyState == 4) { 
 // Indicateur de l'avancement de l'appel AJAX == 4 => Données complètement accessibles 
 if (this.status == 200) { 
 // Code retour du serveur après l'appel AJAX == 200 => OK, tout s'est bien passé
 if (this.responseText != null) { 
 // si on a bien obtenu une réponse non nulle
 // alors on remplace le noeud par la réponse
 document.getElementById("idBouton").innerHTML = this.responseText; 
 }
 }
 }
 }
 request.open("GET", uniqueURL , true); // ici on envoie la requête GET sur l'URL /reqEtatBouton
 request.send(null);
 }

On dit qu'on définit une fonction de nom lireBouton(). Dans cette fonction on crée une chaîne de caractère qui est la concaténation de notre requête HTTP "reqEtatBouton" suivie d'un texte "&aleatoire=" suivi d'un nombre aléatoire entre 0 et 1000000 --> comme cela notre requête changera en permanence.

Ensuite on crée une requête HTTP (XMLHttpRequest) du nom de request et on définit la fonction à appeler lors d'un changement d'avancement sur cette requête AJAX (onreadystatechange) comme étant une fonction qui fait la chose suivante:

  • on vérifie que la requête a bien été entièrement exécutée (les différents if)
  • si oui on va chercher dans le DOM le noeud qui s'appelle "idBouton" (document.getElementById("idBouton")) et on substitue son code HTML (.innerHTML =) par le contenu de la réponse que l'on vient d'obtenir (this.responseText).

Une fois défini le callback, on configure l'appel AJAX (request.open("GET", uniqueURL , true);) en lui disant qu'on veut faire un GET sur l'URL que l'on a construite puis on déclenche l'appel AJAX vers le serveur (request.send(null);)

La requête est exécutée et quand notre Arduino aura répondu la fonction JavaScript définie pour le callBack avec le onreadystatechange est exécutée, elle remplace le noeud et la page se met à jour tout simplement

Le code Javascript se met dans l'en-tête (<head>  </head>), entre des balises <script> et </script> et dans le body on met simplement ce que l'on veut afficher.

La page va donc ressembler à cela:

<!DOCTYPE html>
<html>
<head>
 <meta charset="utf-8" />
 <title>AJAX Web Page</title>
 <script>
 function lireBouton() {
 var uniqueURL = "reqEtatBouton" + "&aleatoire=" + Math.trunc(Math.random() * 1000000);
 var request = new XMLHttpRequest(); // http://www.toutjavascript.com/reference/ref-xmlhttprequest.php

 // la fonction à appeler lors d'un changement d'avancement de la requête AJAX
 request.onreadystatechange = function() {

 if (this.readyState == 4) { 
 // Indicateur de l'avancement de l'appel AJAX == 4 => Données complètement accessibles 
 if (this.status == 200) { 
 // Code retour du serveur après l'appel AJAX == 200 => OK, tout s'est bien passé
 if (this.responseText != null) { 
 // si on a bien obtenu une réponse non nulle
 // alors on remplace le noeud par la réponse
 document.getElementById("idBouton").innerHTML = this.responseText; 
 }
 }
 }
 }
 request.open("GET", uniqueURL , true); // ici on envoie la requête GET sur l'URL /reqEtatBouton
 request.send(null);
 }
 </script>
</head>
<body>
 le bouton est <span id="idBouton">...</span> en ce moment

 <button type="button" onclick="lireBouton()">Lire l'état du bouton</button>
</body>
</html>

Sauvez ce fichier html (attention en UTF-8) sous le nom de basic.html dans le répertoire data de votre sketch SPIFFS, assurez vous que la console est fermée est chargez les data dans la partie SPIFFS de votre ESP en utilisant le menu.

Pour la partie Arduino maintenant

Rajoutez un bouton connecté de la façon suivante

D3 <--> bouton <---> GND

On mettra D3 en INPUT_PULLUP dans le setup() donc pas besoin de résistance, LOW voudra dire bouton appuyé et HIGH bouton au repos.

le code reprendra ce que l'on a vu ci dessus, mais

  • on déclare deux tableaux de caractères (c-strings) httpLine et urlRequest. Le premier nous sert à lire ligne par ligne ce que l'on reçoit dans une requête HTTP comme expliqué plus haut et le second nous servira à sauvegarder l'URL reçue lors de la détection d'un GET

  • on a étendu la fonction testRequeteWeb() pour lire ce qui nous arrive depuis le client sous forme de requête HTTP, si on trouve GET on extrait l'URL, sinon on passe à la suite tant qu'on n'a pas reçu la ligne vide.

  • Une fois reçu la ligne vide, on sait que la requête HTTP est entièrement arrivée et on teste l'URL reçue.

Si c'est "/favicon.ico" on ne fait rien, on n'a pas d'icône à retourner.

Sinon on génère un petit en-tête de réponse HTTP correct

client.println("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: keep-alive\r\n");

puis on teste si l'URL contient [color=green]/reqEtatBouton[/color]. Si oui on appelle la fonction envoyerEtatBouton(), si non on fait comme dans l'exemple précédent, on retourne toute la page en la lisant dans la partie SPIFFS

la fonction envoyerEtatBouton() est toute simple. On regarde le bouton, et s'il est appuyé on envoie le texte "actif" et sinon "inactif". on n'a pas mis de balises HTML ou quoi que ce soit, c'est sans fioriture, mais par la magie noire du Javascript, ce bout de texte qui formera notre réponse à la requête HTTP remplacera le HTML du noeud de l'arborescence DOM et s'affichera.

Le code une fois que l'on comprend cela n'est pas sorcier:(le modifier pour avoir votre SSID et mot de passe)

#include "FS.h" // pour le SPIFFS
const char * nomDeFichier = "/basic.html";

const byte pinBouton = D3;

// provient de https://github.com/esp8266/Arduino
// télécharger et installer à la main la dernière version
#include <ESP8266WiFi.h>


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

const uint16_t HTTPPort = 80;
WiFiServer serveurWeb(HTTPPort); // crée un serveur sur le port HTTP standard

const byte maxHTTPLine = 100;
char httpLine[maxHTTPLine + 1]; // +1 pour avoir la place du '\0'

const byte maxURL = 50;
char urlRequest[maxURL + 1]; // +1 pour avoir la place du '\0'


void printHTTPServerInfo()
{
  Serial.print(F("Site web http://")); Serial.print(WiFi.localIP());
  if (HTTPPort != 80) {
    Serial.print(F(":"));
    Serial.print(HTTPPort);
  }
  Serial.println();
}

void envoyerEtatBouton(WiFiClient &cl)
{
  if (digitalRead(pinBouton) == LOW) {
    cl.println(F("actif"));
  }
  else {
    cl.println(F("inactif"));
  }
}

boolean testRequeteWeb()
{
  boolean requeteHTTPRecue = false;
  byte indexMessage = 0;
  char * ptrGET, *ptrEspace;

  WiFiClient client = serveurWeb.available();
  if (!client) return requeteHTTPRecue; // pas de client connecté
  boolean currentLineIsBlank = true;
  while (client.connected()) {
    if (client.available()) {
      char c = client.read();
      Serial.print(c);
      if (c == '\n' && currentLineIsBlank) { // une requête HTTP se termine par une ligne vide

        // ON GENERE LA PAGE WEB
        if (strcmp(urlRequest, "/favicon.ico")) { // si ce n'est pas pour le favicon
          requeteHTTPRecue = true;

          // On envoie un en tête de réponse HTTP standard
          client.println("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: keep-alive\r\n");

          // on regarde si on a une requete qui continet /reqEtatBouton ( sinon on renvoie toute la page)
          if (strstr(urlRequest, "/reqEtatBouton")) { // http://www.cplusplus.com/reference/cstring/strstr/?kw=strstr
            // on va lire l'état du bouton et on renvoie l'information correctement
            envoyerEtatBouton(client);
          } else { // on envoie la page web par défaut
            if (SPIFFS.exists(nomDeFichier)) {
              File pageWeb = SPIFFS.open(nomDeFichier, "r");
              client.write(pageWeb);
              pageWeb.close();
            } else {
              Serial.println(F("Erreur de fichier"));
            }
          }
        }
        break;           // on sort du while et termine la requête
      } // fin de génération de la réponse HTTP

      if (c == '\n') {
        currentLineIsBlank = true;
        httpLine[indexMessage] = '\0'; // on termine la ligne correctement (c-string)
        indexMessage = 0; // on se reprépre pour la prochaine ligne
        if (ptrGET = strstr(httpLine, "GET")) {
          // c'est la requête GET, la ligne continent "GET /URL HTTP/1.1", on extrait l'URL
          ptrEspace = strstr(ptrGET + 4, " ");
          *ptrEspace = '\0';
          strncpy(urlRequest, ptrGET + 4, maxURL);
          urlRequest[maxURL] = '\0'; // par précaution si URL trop longue
        }
      } else if (c != '\r') {
        currentLineIsBlank = false;
        if (indexMessage <= maxHTTPLine - 1) {
          httpLine[indexMessage++] =  c; // sinon on ignore le reste de la ligne
        }
      }
    } // end if available
  } // end while
  delay(1);
  client.stop(); // termine la connexion
  return requeteHTTPRecue;
}


void setup() {

  pinMode(pinBouton, INPUT_PULLUP);

  Serial.begin(74880); // parce que mon Wemos et par défaut à peu près à cette vitesse, évite les caractères bizarre au boot
  Serial.println("\n\nTest SPIFFS\n");

  // on démarre le SPIFSS
  if (!SPIFFS.begin()) {
    Serial.println("erreur SPIFFS");
    while (true); // on ne va pas plus loin
  }

  WiFi.begin(ssid, password);

  Serial.println();
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.write('.');
  }
  Serial.println();

  // on démarre le serveur
  serveurWeb.begin();
  printHTTPServerInfo();

}

void loop() {
  testRequeteWeb();
}

Si vous chargez ce code dans votre ESP, puis que vous ouvrez la console Série vous allez voir

—————————————————————

```
ets Jan 8 2013,rst cause:2, boot mode:(3,6)

load 0x4010f000, len 1384, room 16
tail 8
chksum 0x2d
csum 0x2d
v303a71de
~ld

Test SPIFFS

.
Site web http://192.168.1.46
```

—————————————————————

ouvrez un navigateur web et tapez votre URL (ici pour moi

http://192.168.1.46

)

vous verrez défiler dans la console Série la requête HTTP (car j'imprime chaque caractère reçu)

—————————————————————

</sub> <sub>GET / HTTP/1.1 <<--- on voit que la requête HTTP est pour l'URL "/" Host: 192.168.1.46 Connection: keep-alive Upgrade-Insecure-Requests: 1 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/604.3.5 (KHTML, like Gecko) Version/11.0.1 Safari/604.3.5 Accept-Language: fr-fr DNT: 1 Accept-Encoding: gzip, deflate</sub> <sub>
<<--- ici il y a la ligne vide
—————————————————————

Comme on a demandé la racine du site web (URL est "/") on retourne le fichier basic.html et c'est ce que l'on voit dans le navigateur

step1.png

clickez maintenant sur le bouton dans la page web. Vous verrez défiler dans le console Série le text suivant

—————————————————————

</sub> <sub>GET [color=green]/reqEtatBouton&aleatoire=420484[/color] HTTP/1.1 Host: 192.168.1.46 Accept-Encoding: gzip, deflate Connection: keep-alive Accept: */* User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/604.3.5 (KHTML, like Gecko) Version/11.0.1 Safari/604.3.5 Accept-Language: fr-fr Referer: http://192.168.1.46/ DNT: 1</sub> <sub>
<<--- ici il y a la ligne vide
—————————————————————

On voit bien qu'on a reçu une requête HTTP en provenance de notre javaScript pour l'URL "[color=green]/reqEtatBouton&aleatoire=420484[/color]", qui est donc bien composée de "/reqEtatBouton" suivi de la partie qu'on construit aléatoirement pour éviter le problème de cache.

votre navigateur affichera sans tout recharger que le bouton est inactif

step2.png

Tenez maintenant appuyé le bouton sur votre breadboard et appuyez à nouveau sur le bouton dans la page web

Vous verrez défiler dans le console Série le text suivant

—————————————————————

GET /reqEtatBouton&aleatoire=326319 HTTP/1.1
Host: 192.168.1.46
Accept-Encoding: gzip, deflate
Connection: keep-alive
Accept: */*
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/604.3.5 (KHTML, like Gecko) Version/11.0.1 Safari/604.3.5
Accept-Language: fr-fr
Referer: http://192.168.1.46/
DNT: 1


—————————————————————

On voit bien qu'on a reçu une requête HTTP en provenance de notre javaScript pour l'URL "[color=green]/reqEtatBouton&aleatoire=326319[/color]", qui est donc bien composée de "/reqEtatBouton" suivi de la partie aléatoire qui a bien changé par rapport à la précédente

votre navigateur affichera sans tout recharger que le bouton est actif

step3.png

Automatiser la mise à jour:

C'est sympa tout cela, mais c'est quand même un peu pénible de clicker sur le bouton pour avoir le contenu qui se met à jour. Ne peut on pas automatiser tout cela ?

of course - un ordinateur ça sert à cela...

En Javascript on peut déclencher une minuterie pour lancer un script périodiquement (en lui donnant une période en millisecondes). cela se fait en appelant la fonction [url=http://www.toutjavascript.com/reference/ref-window.settimeout.php]setTimeout()[/url]

setTimeout("etatBouton()", 1000); // Déclenche une minuterie et appelle etatBouton() dans 1000 millisecondes = 1s

et on peut aussi demander à la page web d'exécuter automatiquement un script une fois qu'elle est chargée, cela se met dans la balise <body>.

Dans notre exemple si on écrit

<body onload="etatBouton()">

notre page va se charger puis la fonction javaScript etatBouton() va être appelée. Celle ci va déclencher la requête HTTP pour lire l'état du bouton et donc juste après le chargement de la page on remplacera les ... par l'état correct du bouton.

Si maintenant à la fin de la fonction lireBouton() on activait la minuterie, alors la page va envoyer automatiquement x millisecondes plus tard une nouvelle requête pour relire l'état du bouton puis re-déclencher la minuterie et donc ça met à jour la page toutes les x ms.

Bien sûr le bouton de mise à jour dans la page web dans ce cas ne sert plus à rien on peut l'enlever

Notre HTML devient alors (à sauver en UTF-8 sous le nom de basic[color=red]1[/color].html)

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>AJAX Web Page</title>
<script>
function lireBouton() {
var uniqueURL = "reqEtatBouton" + "&aleatoire=" + Math.trunc(Math.random() * 1000000);
var request = new XMLHttpRequest(); // http://www.toutjavascript.com/reference/ref-xmlhttprequest.php

// la fonction à appeler lors d'un changement d'avancement de la requête AJAX
request.onreadystatechange = function() {

if (this.readyState == 4) { 
// Indicateur de l'avancement de l'appel AJAX == 4 => Données complètement accessibles 
if (this.status == 200) { 
// Code retour du serveur après l'appel AJAX == 200 => OK, tout s'est bien passé
if (this.responseText != null) { 
// si on a bien obtenu une réponse non nulle
// alors on remplace le noeud par la réponse
document.getElementById("idBouton").innerHTML = this.responseText; 
}
}
}
}
request.open("GET", uniqueURL , true); // ici on envoie la requête GET sur l'URL /reqEtatBouton
request.send(null);
setTimeout("etatBouton()", 1000); // prochaine demande dans 1 seconde, ne pas descendre sous 500
}
</script>
</head>
<body onload="lireBouton()">
le bouton est <span id="idBouton">...</span> en ce moment

<button type="button" onclick="lireBouton()">Lire l'état du bouton</button>
</body>
</html>

sauvez ce fichier dans la partie data du sketch, chargez le dans le SPIFFS de votre ESP et modifiez juste le code Arduino pour aller lire ce fichier en changeant au début du sketch

const char * nomDeFichier = "/basic.html";

en

const char * nomDeFichier = "/basic1.html";

(vous pouvez aussi enlever la ligne

Serial.print(c);

dans la fonction testRequeteWeb() car elle balance beaucoup de texte sur la console série.

#include "FS.h" // pour le SPIFFS
const char * nomDeFichier = "/basic1.html";

const byte pinBouton = D3;


// provient de https://github.com/esp8266/Arduino
// télécharger et installer à la main la dernière version
#include <ESP8266WiFi.h>


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

const uint16_t HTTPPort = 80;
WiFiServer serveurWeb(HTTPPort); // crée un serveur sur le port HTTP standard


const byte maxHTTPLine = 100;
char httpLine[maxHTTPLine + 1]; // +1 pour avoir la place du '\0'

const byte maxURL = 50;
char urlRequest[maxURL + 1]; // +1 pour avoir la place du '\0'


void printHTTPServerInfo()
{
  Serial.print(F("Site web http://")); Serial.print(WiFi.localIP());
  if (HTTPPort != 80) {
    Serial.print(F(":"));
    Serial.print(HTTPPort);
  }
  Serial.println();
}

void envoyerEtatBouton(WiFiClient &cl)
{
  if (digitalRead(pinBouton) == LOW) {
    cl.println(F("actif"));
  }
  else {
    cl.println(F("inactif"));
  }
}

boolean testRequeteWeb()
{
  boolean requeteHTTPRecue = false;
  byte indexMessage = 0;
  char * ptrGET, *ptrEspace;

  WiFiClient client = serveurWeb.available();
  if (!client) return requeteHTTPRecue; // pas de client connecté
  boolean currentLineIsBlank = true;
  while (client.connected()) {
    if (client.available()) {
      char c = client.read();
      if (c == '\n' && currentLineIsBlank) { // une requête HTTP se termine par une ligne vide

        // ON GENERE LA PAGE WEB
        if (strcmp(urlRequest, "/favicon.ico")) { // si ce n'est pas pour le favicon
          requeteHTTPRecue = true;

          // On envoie un en tête de réponse HTTP standard
          client.println("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: keep-alive\r\n");

          // on regarde si on a une requete qui continet /reqEtatBouton ( sinon on renvoie toute la page)
          if (strstr(urlRequest, "/reqEtatBouton")) { // http://www.cplusplus.com/reference/cstring/strstr/?kw=strstr
            // on va lire l'état du bouton et on renvoie l'information correctement
            envoyerEtatBouton(client);
          } else { // on envoie la page web par défaut
            if (SPIFFS.exists(nomDeFichier)) {
              File pageWeb = SPIFFS.open(nomDeFichier, "r");
              client.write(pageWeb);
              pageWeb.close();
            } else {
              Serial.println(F("Erreur de fichier"));
            }
          }
        }
        break;           // on sort du while et termine la requête
      } // fin de génération de la réponse HTTP

      if (c == '\n') {
        currentLineIsBlank = true;
        httpLine[indexMessage] = '\0'; // on termine la ligne correctement (c-string)
        indexMessage = 0; // on se reprépre pour la prochaine ligne
        if (ptrGET = strstr(httpLine, "GET")) {
          // c'est la requête GET, la ligne continent "GET /URL HTTP/1.1", on extrait l'URL
          ptrEspace = strstr(ptrGET + 4, " ");
          *ptrEspace = '\0';
          strncpy(urlRequest, ptrGET + 4, maxURL);
          urlRequest[maxURL] = '\0'; // par précaution si URL trop longue
        }
      } else if (c != '\r') {
        currentLineIsBlank = false;
        if (indexMessage <= maxHTTPLine - 1) {
          httpLine[indexMessage++] =  c; // sinon on ignore le reste de la ligne
        }
      }
    } // end if available
  } // end while
  delay(1);
  client.stop(); // termine la connexion
  return requeteHTTPRecue;
}


void setup() {

  pinMode(pinBouton, INPUT_PULLUP);

  Serial.begin(74880); // parce que mon Wemos et par défaut à peu près à cette vitesse, évite les caractères bizarre au boot
  Serial.println("\n\nTest SPIFFS\n");

  // on démarre le SPIFSS
  if (!SPIFFS.begin()) {
    Serial.println("erreur SPIFFS");
    while (true); // on ne va pas plus loin
  }

  WiFi.begin(ssid, password);

  Serial.println();
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.write('.');
  }
  Serial.println();

  // on démarre le serveur
  serveurWeb.begin();
  printHTTPServerInfo();

}

void loop() {
  testRequeteWeb();
}

Chargez le code, connectez vous avez un navigateur web, appuyez ou relâchez le bouton sur la breadboard et vous devriez vous l'interface web se mettre à jour automatiquement toutes les secondes; si vous voulez que ce soit plus rapide, vous pouvez modifier le setTimeout() pour faire la requête 500ms après par exemple, bien sûr si votre ESP doit faire aussi autre chose en même temps, le temps passé à répondre à la requête c'est du temps indisponible pour le reste de votre programme.

![|500x311](Techniques "avancées" de serveur web sur ESP8266 - Tutoriels et cours - Arduino Forum 233862)

3. Aller plus loin: AJAX et XML

imaginons maintenant que vous ayez un certain nombre de valeurs (des éléments mesurés de votre programme arduino par exemple comme la température, l'humidité, l'état d'un potentiomètre etc...) que vous voulez afficher de temps en temps

On pourrait prendre la même approche que ci dessus et générer un gros bout de code HTML pour la mise en page du résultat mais on retombe alors dans les problèmes de vitesse d'affichage, plus la réponse à la requête HTTP est grande et moins on aura l'impression que l'interface web est réactive. de plus ça oblige à coder en dur du HTML dans le programme Arduino et donc on n'est plus libre de choisir la mise en page HTML / CSS que l'on souhaite. si on veut changer la page web, on doit changer et re-compiler le code Arduino...

Si on se souvient que JavaScript est un langage de programmation ayant accès comme on l'a vu à l'arborescence objet représentant la page web (DOM) on pourrait se dire que si le code HTML pouvait simplement dire "ici afficher la valeur de la variable x" et si on pouvait depuis une réponse dire simplement dire "x vaut maintenant 18" alors on obtiendrait cette indépendance entre le code Arduino et le code HTML.

c'est exactement ce que l'on va faire :slight_smile:

Un peu d'info sur XML (eXtensible Markup Language)

il y a beaucoup d'info sur XML en ligne, par exemple XML expliqué aux débutants et vous pouvez aussi lire le document officiel de La spécification XML du 26 November 2008 (c'est pas récent comme vous pouvez le constater)

On retiendra surtout que

  • XML est un langage de balisage utilisant les symboles <> par exemple

  • Il doit toujours y avoir une balise ouvrante et une balise fermante <toto>...</toto>

  • Si on n'a rien entre deux balises, il existe une forme restreinte avec un / en fin de balise <toto/>

  • On peut affecter des attributs délimités obligatoirement par des guillemets ou des apostrophe aux éléments (valeurs des entités) par exemple <toto age="12" comportement="blagueur"/>

  • les balises sont organisées en arbre (aucun chevauchement n'est autorisé) il y a donc une balise qui englobe toutes les autres et qui est la racine de l'arbre

<racine>
   <enfants>
      <fred age="12" comportement="blagueur"/>
      <paul age="14" comportement="douteux"/>
   </enfants>
</racine>
  • un "prologue" XML sert à définir la version de XML qu'on utilise ou l'encodage (attribut encoding -> par défaut UTF-8) par exemple et se représente ainsi. (vous utilisez quasiment toujours la version 1.0
<?xml version="1.0" ?>

comme la plupart des éditeurs de texte sauvent le fichier par défaut en ISO-8859-1, vous verrez le plus souvent plutôt

<?xml version="1.0" encoding="ISO-8859-1"?>

Comment l'utiliser ?

On pourrait envisager un fichier XML (sauvé en UTF-8) structuré ainsi:

<?xml version = "1.0" ?>
<variables>
   <bouton>actif</bouton>
   <digital1>1</digital1>
   <analog1>432</analog1>
</variables>

ou alors comme cela

<?xml version = "1.0" ?>
<variables>
   <bouton>actif</bouton>
   <bouton>1</bouton>
   <analog>432</analog>
</variables>

On dit qu'on a un arborescence qui s'appelle variables et qui contient 3 éléments. La différence entre les 2 (on verra que c'est utile plus tard) c'est que dans le premier cas chaque élément a un nom unique alors que dans le second cas on dit qu'on a deux types d'éléments, deux boutons (car même nom) et une valeur analogique


Bon c'est bien toute cette théorie mais comment en Javascript vais-je aller extraire des valeurs et les affecter dans la page?

un peu comme avant...

Si au lieu de retourner donc du code HTML qui servirait comme précédemment à remplacer un noeud de l'arborescence DOM on retourne du XML, on peut écrire un script JavaScript qui va aller comprendre le contenu du XML pour trouver quels noeuds DOM modifier et injecter la bonne valeur.

Si vous regardez un peu plus haut quand on retournait du contenu HTML, dans le header HTTP on disait

HTTP/1.1 200 OK
Content-Type: text/html
Connection: keep-alive

mais maintenant comme on va retourner du XML, dans le header de la réponse il faudra préciser que c'est du XML pour que notre script sache quoi en faire et donc on mettra avant le XML l'en-tête suivant:

HTTP/1.1 200 OK
Content-Type: text/xml
Connection: keep-alive

Commençons par travailler avec la première forme XML pour le moment.

<?xml version = "1.0" ?>
<variables>
   <bouton>actif</bouton>
   <digital1>1</digital1>
   <analog1>432</analog1>
</variables>

c'est ce que doit nous retourner l'Arduino pour répondre à une requête AJAX. On se souvient que la réponse du serveur web traitée par le Javascript était dans this.responseText.

Ici comme on va renvoyer du XML on ira regarder la réponse dans [b]this.responseXML[/b] qui représente notre arbre XML reçu en réponse; JavaScript comprend tout seul la structure d'un objet XML et donc on peut par programme demander à extraire des éléments de l'arbre par leur nom. Par exemple pour trouver le bout de XML correspondant à analog1 on va faire

this.responseXML.getElementsByTagName('analog1')

il se pourrait (comme on l'a vu ci dessus) que notre XML contienne plusieurs éléments portant un même nom. donc le getElementsByTagName() retourne en fait un tableau de tous ces éléments. ici on veut le premier, donc à l'index 0 du tableau et on écrira donc

this.responseXML.getElementsByTagName('analog1')[ 0 ]

Cet élément peut avoir plusieurs sous branches (enfants, child en anglais) et on s'intéresse (vu notre XML ci dessus) uniquement à la première entrée. Pour cela on peut soit utiliser childNodes[ 0 ] soit firstChild qui est le premier enfant de la liste.

Enfin on veut extraire la valeur 432 et c'est la valeur de notre noeud, que l'on trouver avec [b].nodeValue[/b]. Donc au final, pour aller extraire la valeur stockée dans notre XML pur le nom analog1 on va demander donc pour lire le 432

this.responseXML.getElementsByTagName('analog1')[0].childNodes[ 0 ].nodeValue

Ensuite on va utiliser la même technique que précédemment: on va remplacer dans l'arbre DOM le HTML associé à un noeud de nom particulier par cette nouvelle valeur et donc on va écrire:

document.getElementById("analog1ID").innerHTML = this.responseXML.getElementsByTagName('analog1')[ 0 ].childNodes[0].nodeValue;

il nous faudra bien sûr avoir défini dans le corps du HTML une zone portant le nom [color=blue]analog1ID[/color], par exemple en utilisant <span>

la pin A0 vaut <span id="analog1ID"]>...</span>

On fait de même pour les autres et voilà à quoi pourrait resembler notre script et le code HTML

<!DOCTYPE html>
<html>
<head>
   <meta charset="utf-8" />
   <title>AJAX et XML</title>
   <script>
   function obtenirVariables()
   {
      var uniqueURL = "reqEtatVariables" + "&aleatoire=" + Math.trunc(Math.random() * 1000000);
      var request = new XMLHttpRequest(); // http://www.toutjavascript.com/reference/ref-xmlhttprequest.php

      // la fonction à appeler lors d'un changement d'avancement de la requête AJAX
      request.onreadystatechange = function()
      {
         if (this.readyState == 4) {
            // Indicateur de l'avancement de l'appel AJAX == 4 => Données complètement accessibles 
            if (this.status == 200) {
               // Code retour du serveur après l'appel AJAX == 200 => OK, tout s'est bien passé
               if (this.responseXML != null) {
                  // si on a bien obtenu une réponse non nulle
                  // alors on va extraire du XML les éléments qui nous intéressent
                  document.getElementById("boutonID").innerHTML =
                  this.responseXML.getElementsByTagName('bouton')[0].childNodes[0].nodeValue;
                  document.getElementById("digital1ID").innerHTML =
                  this.responseXML.getElementsByTagName('digital1')[0].childNodes[0].nodeValue;
                  document.getElementById("analog1ID").innerHTML =
                  this.responseXML.getElementsByTagName('analog1')[0].childNodes[0].nodeValue;
               }
            }
         }
      }
      request.open("GET", uniqueURL , true); // ici on envoie la requête GET sur l'URL /reqEtatVariables
      request.send(null);
      setTimeout("obtenirVariables()", 1000); // on rappelle obtenirVariables() dans 1s
   }
   </script>
</head>
<body onload="obtenirVariables()">
   <p>le bouton est <span id="boutonID">...</span> maintenant</p>
   <p>la pin D4 vaut <span id="digital1ID">...</span></p>
   <p>la pin A0 vaut <span id="analog1ID">...</span></p>
</body>
</html>

copier ce fichier, sauvegardez le en UTF-8 sous le nom de dynamique.html dans la zone data de votre sketch et utiliser le menu pour le télécharger dans la zone SPIFFS

Il va nous falloir modifier un peu notre code Arduino précédent maintenant pour tenir compte des changements; ils ont peu nombreux:

  • on doit retourner le fichier "dynamique.html" de la zone SPIFFS, à modifier en début de sketch
  • l'en tête HTTP n'est plus le même pour la page principale que pour la réponse AJAX donc on déplace la génération de cet en tête
  • la requête AJAX du HTML ci dessus va appeler l'URL /reqEtatVariables, donc il faut détecter cela
  • on doit retourner le bon en tête XML et le code XML dans la fonction appelée dynamiquement quand on détecte la requête AJAX (on change de nom, au lieu de envoyerEtatBouton() on va appeler cela envoyerVariables())
  • dans cette fonction il faudra lire l'état du bouton mais aussi de A0 et A1

voilà donc à quoi ressemble notre nouveau code pour notre ESP

#include "FS.h" // pour le SPIFFS
const char * nomDeFichier = "/dynamique.html";

const byte pinBouton = D3;


// provient de https://github.com/esp8266/Arduino
// télécharger et installer à la main la dernière version
#include <ESP8266WiFi.h>

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

const uint16_t HTTPPort = 80;
WiFiServer serveurWeb(HTTPPort); // crée un serveur sur le port HTTP standard

const byte maxHTTPLine = 100;
char httpLine[maxHTTPLine + 1]; // +1 pour avoir la place du '\0'

const byte maxURL = 50;
char urlRequest[maxURL + 1]; // +1 pour avoir la place du '\0'


void printHTTPServerInfo()
{
  Serial.print(F("Site web http://")); Serial.print(WiFi.localIP());
  if (HTTPPort != 80) {
    Serial.print(F(":"));
    Serial.print(HTTPPort);
  }
  Serial.println();
}

void envoyerVariables(WiFiClient &cl)
{
  // On envoie un en tête de réponse HTTP de type XML et le XML

  // l'en-tête doit être comme cela

  // ----------------------------------
  //  HTTP/1.1 200 OK
  //  Content-Type: text/xml
  //  Connection: keep-alive
  //                    <-- une ligne vide ici
  // ----------------------------------

  //  et le XML doit ressembler à cela avec bien sûr les bonnes valeurs insérées dans les balises

  // ----------------------------------
  //  <?xml version = "1.0" ?>
  //  <variables>
  //    <bouton>xxx</bouton>
  //    <digital1>yyy</digital1>
  //    <analog1>zzz</analog1>
  //  </variables>
  // ----------------------------------

  cl.print(F("HTTP/1.1 200 OK\r\n"
             "Content-Type: text/xml\r\n"
             "Connection: keep-alive\r\n\r\n"
             "<?xml version = \"1.0\" ?>"
             "<variables><bouton>"));

  if (digitalRead(pinBouton) == LOW) cl.print(F("actif"));
  else cl.print(F("inactif"));

  cl.print(F("</bouton><digital1>"));
  cl.print(digitalRead(D4));
  cl.print(F("</digital1><analog1>"));
  cl.print(analogRead(A0));
  cl.print(F("</analog1></variables>"));
}

boolean testRequeteWeb()
{
  boolean requeteHTTPRecue = false;
  byte indexMessage = 0;
  char * ptrGET, *ptrEspace;

  WiFiClient client = serveurWeb.available();
  if (!client) return requeteHTTPRecue; // pas de client connecté
  boolean currentLineIsBlank = true;
  while (client.connected()) {
    if (client.available()) {
      char c = client.read();
      if (c == '\n' && currentLineIsBlank) { // une requête HTTP se termine par une ligne vide

        // ON GENERE LA PAGE WEB
        if (strcmp(urlRequest, "/favicon.ico")) { // si ce n'est pas pour le favicon
          requeteHTTPRecue = true;

          // on regarde si on a une requete qui continet /reqEtatBouton ( sinon on renvoie toute la page)
          if (strstr(urlRequest, "/reqEtatVariables")) { // http://www.cplusplus.com/reference/cstring/strstr/?kw=strstr
            // on va lire l'état du bouton et on renvoie l'information correctement
            envoyerVariables(client);
          } else { // on envoie la page web par défaut
            if (SPIFFS.exists(nomDeFichier)) {
              // On envoie un en tête de réponse HTTP standard de type HTML
              client.println("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: keep-alive\r\n");
              File pageWeb = SPIFFS.open(nomDeFichier, "r");
              client.write(pageWeb);
              pageWeb.close();
            } else {
              Serial.println(F("Erreur de fichier"));
            }
          }
        }
        break; // on sort du while et termine la requête
      } // fin de génération de la réponse HTTP

      if (c == '\n') {
        currentLineIsBlank = true;
        httpLine[indexMessage] = '\0'; // on termine la ligne correctement (c-string)
        indexMessage = 0; // on se reprépre pour la prochaine ligne
        if (ptrGET = strstr(httpLine, "GET")) {
          // c'est la requête GET, la ligne continent "GET /URL HTTP/1.1", on extrait l'URL
          ptrEspace = strstr(ptrGET + 4, " ");
          *ptrEspace = '\0';
          strncpy(urlRequest, ptrGET + 4, maxURL);
          urlRequest[maxURL] = '\0'; // par précaution si URL trop longue
        }
      } else if (c != '\r') {
        currentLineIsBlank = false;
        if (indexMessage <= maxHTTPLine - 1) {
          httpLine[indexMessage++] =  c; // sinon on ignore le reste de la ligne
        }
      }
    } // end if available
  } // end while
  delay(1);
  client.stop(); // termine la connexion
  return requeteHTTPRecue;
}


void setup() {

  pinMode(pinBouton, INPUT_PULLUP);
  pinMode(D4, INPUT);
  pinMode(A0, INPUT);
  
  Serial.begin(74880); // parce que mon Wemos et par défaut à peu près à cette vitesse, évite les caractères bizarre au boot
  Serial.println("\n\nTest SPIFFS\n");

  // on démarre le SPIFSS
  if (!SPIFFS.begin()) {
    Serial.println("erreur SPIFFS");
    while (true); // on ne va pas plus loin
  }

  WiFi.begin(ssid, password);

  Serial.println();
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.write('.');
  }
  Serial.println();

  // on démarre le serveur
  serveurWeb.begin();
  printHTTPServerInfo();

}

void loop() {
  testRequeteWeb();
}

compilez le, chargez le dans votre ESP, ouvrez la console Série à 74880 bauds. Comme auparavant il vous donne l'URL pour vous connecter sur votre ESP, ouvrez une fenêtre de navigateur web, tapez l'URL et simplement en jouant maintenant sur le bouton ou en branchant des fils à la masse ou sur AU MAX 3.2V - attention un ESP ne supporte pas le 5V et l'entrée analogique pas plus de 3.3 vous verrez l'interface qui change

dyn.png

L'avantage donc de cette approche est que notre code Arduino ne fait qu'émettre les variables en XML et le dessin de la page est maintenant complètement laissé au choix du designer web, il suffira d'utiliser un et les bons ID aux bons endroits, le script se chargeant de faire l'affichage en fonction des valeurs reçues.

Par exemple si vous remplacez avec ce code HTML à la place maintenant - c'est tout de suite plus joli :slight_smile:

<!DOCTYPE html>
<html>
<head>
   <meta charset="utf-8" />
   <title>AJAX et XML</title>
   <style type="text/css">
   table
   {
      margin-top:2px;
      margin-left:2px;
      margin-right:2px;
      margin-bottom:2px;
   }
   fieldset
   {
      color:gray;
      border-style: dotted;
      border-radius: 5px;
   }
   fieldset:hover
   {
      color:blue;
      border-color: blue;
      border-style: double;   
   }
   </style>
   <script>
   function obtenirVariables()
   {
      var uniqueURL = "reqEtatVariables" + "&aleatoire=" + Math.trunc(Math.random() * 1000000);
      var request = new XMLHttpRequest(); // http://www.toutjavascript.com/reference/ref-xmlhttprequest.php

      // la fonction à appeler lors d'un changement d'avancement de la requête AJAX
      request.onreadystatechange = function()
      {
         if (this.readyState == 4) {
            // Indicateur de l'avancement de l'appel AJAX == 4 => Données complètement accessibles 
            if (this.status == 200) {
               // Code retour du serveur après l'appel AJAX == 200 => OK, tout s'est bien passé
               if (this.responseXML != null) {
                  // si on a bien obtenu une réponse non nulle
                  // alors on va extraire du XML les éléments qui nous intéressent
                  document.getElementById("boutonID").innerHTML =
                  this.responseXML.getElementsByTagName('bouton')[0].childNodes[0].nodeValue;
                  document.getElementById("digital1ID").innerHTML =
                  this.responseXML.getElementsByTagName('digital1')[0].childNodes[0].nodeValue;
                  document.getElementById("analog1ID").innerHTML =
                  this.responseXML.getElementsByTagName('analog1')[0].childNodes[0].nodeValue;
               }
            }
         }
      }
      request.open("GET", uniqueURL , true); // ici on envoie la requête GET sur l'URL /reqEtatVariables
      request.send(null);
      setTimeout("obtenirVariables()", 1000); // on rappelle obtenirVariables() dans 1s
   }
   </script>
</head>
<body onload="obtenirVariables()">
   <fieldset style="width:30%">
      <table>
         <legend>Les Paramètres</legend>
         <tr>
            <td>le bouton est <span id="boutonID">...</span> maintenant</td>
         </tr>
         <tr>
            <td>la pin D4 vaut <span id="digital1ID">...</span></td>
         </tr>
         <tr>
            <td>la pin A0 vaut <span id="analog1ID">...</span></td>
         </tr>
      </table>
   </fieldset>
</body>
</html>

dyn1.png
quand la souris n'est pas dans le cadre

dyn2.png
et quand la souris est dans le cadre

Bien sûr pour améliorer les performances de votre code, ne mettez pas le commentaires dans le fichier HTML... (pour le script). Ils seront balancés sur le réseau et ça encombre pour rien

Maintenant si vous êtes doué en Javascript, canvas, HTML5 etc - ou si vous savez réutiliser des composants dispo sur le web, on peut aller beaucoup plus loin...

par exemple en utilisant cette superbe bibliothèque de cadrans en HTML5 et JavaScript, si vous chargez ce code HTML (et que votre ordinateur qui consulte a accès à internet car je vais chercher la définition des cadrans sur internet)

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8" />
	<title>AJAX et XML</title>
	<style type="text/css">
	table
	{
		margin-top:2px;
		margin-left:2px;
		margin-right:2px;
		margin-bottom:2px;
	}
	fieldset
	{
		color:gray;
		border-style: dotted;
		border-radius: 5px;
	}
	fieldset:hover
	{
		color:blue;
		border-color: blue;
		border-style: double;   
	}
	</style>

	<script src="http://cdn.rawgit.com/Mikhus/canvas-gauges/gh-pages/download/2.1.4/all/gauge.min.js"></script>

	<script>
	function initCadran() {
		cadran = new RadialGauge({	// cadran devient une variable globale car pas var devant
			renderTo: "analog1ID", 
			width: 300,
			height: 300,
			units: "A0",
			minValue: 0,
			maxValue: 1200,
			startAngle: 90,
			ticksAngle: 180,
			valueBox: false,
			majorTicks: [0,200,400,600,800,1000,1200],
			minorTicks: 2,
			strokeTicks: true,
			highlights: [
			{"from":    0, "to":  300, "color": "rgba(  0,   0, 255, .15)" },
			{"from":  300, "to":  800, "color": "rgba(  0, 255,   0, .15)"},
			{"from":  800, "to": 1000, "color": "rgba(200,  10,  10, .15)"},
			{"from": 1000, "to": 1200, "color": "rgba(255,   0,   0, .5)"}
			],
			colorPlate: "#fff",
			borderShadowWidth: 0,
			borders: false,
			needleType: "arrow",
			needleWidth: 2,
			needleCircleSize: 7,
			needleCircleOuter: true,
			needleCircleInner: false,
			value: 0
		});
		cadran.draw();
	}

	function obtenirVariables()
	{
		var uniqueURL = "reqEtatVariables" + "&aleatoire=" + Math.trunc(Math.random() * 1000000);
		var request = new XMLHttpRequest(); // http://www.toutjavascript.com/reference/ref-xmlhttprequest.php

		// la fonction à appeler lors d'un changement d'avancement de la requête AJAX
		request.onreadystatechange = function()
		{
			if (this.readyState == 4) {
				// Indicateur de l'avancement de l'appel AJAX == 4 => Données complètement accessibles 
				if (this.status == 200) {
					// Code retour du serveur après l'appel AJAX == 200 => OK, tout s'est bien passé
					if (this.responseXML != null) {
						// si on a bien obtenu une réponse non nulle
						// alors on va extraire du XML les éléments qui nous intéressent
						document.getElementById("boutonID").innerHTML =
						this.responseXML.getElementsByTagName('bouton')[0].childNodes[0].nodeValue;
						document.getElementById("digital1ID").innerHTML =
						this.responseXML.getElementsByTagName('digital1')[0].childNodes[0].nodeValue;
						cadran.value = this.responseXML.getElementsByTagName('analog1')[0].childNodes[0].nodeValue;
						cadran.draw();
					}
				}
			}
		}
		request.open("GET", uniqueURL , true); // ici on envoie la requête GET sur l'URL /reqEtatVariables
		request.send(null);
		setTimeout("obtenirVariables()", 500);
	}

	function initialisation()
	{
		initCadran();
		obtenirVariables();
	}
	</script>

</head>
<body onload="initialisation()">
	<fieldset>
		<table>
			<legend>Les Paramètres</legend>
			<tr>
				<td>le bouton est <span id="boutonID">...</span> maintenant</td>
			</tr>
			<tr>
				<td>la pin D4 vaut <span id="digital1ID">...</span></td>
			</tr>
			<tr>
				<td>
					<canvas id="analog1ID"></canvas>
				</td>
			</tr>
		</table>
	</fieldset>
</body>
</html>

alors vous verrez un joli affichage sous cette forme

super non ? et l'aiguille va bouger avec la lecture Analogique sur A0 automatiquement !!!

reservé

reservé

reservé

reservé

reservé

MERCI DE NE PAS POSTER POUR LE MOMENT DANS CETTE FILE HISTOIRE QUE JE PUISSE FINIR LE TUTO SANS MELANGER AVEC DES COMMENTAIRES

MERCI DE NE PAS POSTER POUR LE MOMENT DANS CETTE FILE HISTOIRE QUE JE PUISSE FINIR LE TUTO SANS MELANGER AVEC DES COMMENTAIRES

MERCI DE NE PAS POSTER POUR LE MOMENT DANS CETTE FILE HISTOIRE QUE JE PUISSE FINIR LE TUTO SANS MELANGER AVEC DES COMMENTAIRES