I. Avant-propos

La rédaction de cet article assume que vous avez connaissance des matériels utilisés et des mécanismes détaillés dans la première et dans la deuxième partie. Il serait donc préférable que vous les ayez lues avant d’entamer la lecture de cette troisième partie, car je serai amené à y faire référence à plusieurs reprises.

Nous allons donc « aborder le JavaScript, avec un petit tour du côté d’AJAX et de JSON ». Quand je dis « aborder », je devrais plutôt dire « effleurer » tant notre incursion dans ces domaines sera succincte. Cependant, il faut un début à tout, et la réalisation des deux premières parties nous offre un support matériel et logiciel suffisamment simple et complet pour tenter cette incursion, tout en la justifiant.

II. Introduction

Je ne vais bien entendu pas vous faire un cours sur JavaScript, ni d’ailleurs sur les autres points abordés dans cette troisième partie. D’autres l’ont déjà fait, qui sont bien plus compétents que moi sur le sujet. Vous trouverez sur developpez.comCours JavaScript et sur w3schools.comRéférence JavaScript (voire ailleurs) tout ce que vous pourriez avoir besoin ou envie de connaître sur la question.

Je ne vais vous détailler, à l’instar de ce que j’ai fait pour le HTML et le CSS, que les éléments de ce langage qui seront nécessaires pour manipuler les « objets » suivants :

  • le DOM (pour Document Object Model) : il s’agit d’une interface de programmation qui va permettre à JavaScript d’agir directement au niveau du document pour en modifier le contenu ou la présentation. Comme cela se passe en local, c’est-à-dire au sein même de votre navigateur, cette fonctionnalité permet de modifier une page web sans avoir à la recharger ;
  • AJAX (pour Asynchronous JavaScript And XML) : il s’agit d’une technique qui utilise l’objet XMLHttpRequest intégré au navigateur pour effectuer des échanges de données entre le client et le serveur. Habituellement, le client envoie une requête au serveur, lequel traite la requête en question, avec un programme PHP par exemple, et renvoie la page HTML mise à jour grâce à ce traitement. Si, par exemple, vous vous connectez sur la page d’accueil d’un site en envoyant votre identifiant et votre mot de passe, la plupart du temps le site rend compte de votre connexion en affichant un message personnalisé dans un coin de la page.
    Est-il nécessaire, au niveau du serveur, de générer et d’envoyer au client une nouvelle page dont la seule modification par rapport à l’ancienne est d’incorporer ce message de bienvenue ? Assurément non, si on peut faire autrement. Une page web peut facilement peser plusieurs mégaoctets, notamment si elle contient des images, alors que la requête et la réponse ne totalisent que quelques dizaines d’octets. À l’échelle de la planète, ce serait un vrai gâchis énergétique. Heureusement, avec AJAX et le DOM (et JavaScript bien sûr), on peut. Bien entendu, pour notre petite réalisation, ce critère n’est pas forcément très pertinent.
    Le « Asynchronous » signifie simplement que le programme JavaScript peut continuer à travailler après l’envoi de la requête, sans attendre la réponse du serveur. La page n’est donc pas figée. À la réception de cette réponse, JavaScript la traite et met la page à jour, si nécessaire, par l’intermédiaire du DOM ;
  • JSON (pour JavaScript Object Notation) : les échanges de données entre le serveur et le client se font sous forme textuelle. On le savait pour ce qui concernait la requête, incorporée à l’URL par la méthode GET. Il en est de même quant au retour. Pour faciliter le décryptage de la réponse, il convient de structurer l’information contenue dans la chaîne renvoyée par le serveur. Plutôt que de laisser le développeur « imaginer » une structure plus ou moins efficace, et écrire les routines en permettant le décryptage, l’objet XMLHttpRequest, comme son nom le laisse deviner, utilise classiquement la structuration XML. Mais ce n’est pas obligatoire. De plus en plus, un autre formatage des données lui est préféré, le JSON, en raison de son écriture plus cohérente avec les langages de programmation que le XML, et surtout de ses liens très étroits avec le JavaScript qui dispose nativement des méthodes pour le traiter.

III. Le DOM

Comme stipulé plus haut, dans sa brève description, le DOM (Document Object Model) est une interface de programmation. Le « Petit Larousse » nous dit d’une interface, en informatique, qu’elle est l’ « Ensemble des règles et des conventions qui permettent l’échange d’informations entre deux systèmes de données » (©Le Petit Larousse Illustré 2018). Cette définition très générale nous indique déjà que nous aurons affaire à deux entités. Quelles sont-elles ? Puisqu’il s’agit d’une interface de programmation, il nous faut de quoi programmer, à savoir un langage. Quiz : lequel ? Réponse : JavaScript. Bravo, vous avez gagné. Bon ! Ce n’était pas trop difficile. Quelle est l’autre entité ? Le moteur de rendu du navigateur, dont le travail consiste à dessiner sur votre écran la page décrite dans cette interface.

Au chargement de la page web, le navigateur crée, à partir du code HTML qui la décrit, une instance d’un objet appelé DOM qui est la traduction sous une forme exploitable, à la fois par JavaScript et par son moteur de rendu, de ce code. Cette instance, unique pour la page, est appelée « document », et donc sera connue de JavaScript sous ce nom. On peut schématiser ceci de la façon suivante :

Image non disponible

Comme tout objet, celui-ci possède des propriétés et fournit des méthodes permettant de les manipuler. Vous trouverez dans la rubrique w3schools HTML DOM Reference la liste exhaustive de ces méthodes et propriétés.

Nous allons prendre un exemple pour voir plus clairement la façon dont ça se passe. Je vous propose de reprendre le code HTML vu dans ce chapitre de la première partie, modifié comme suit :

test-dom.html
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
<!DOCTYPE html>

<html lang="fr">

  <meta charset="utf-8" />
  <title>Test du DOM</title>
   
  <body>
    <div>
      <h3 id="titre">Informations sur la page</h3>
      <p>Cette page HTML ne contient qu’un peu de texte, avec un minimum de mise en forme pour l’exemple. Elle fait appel aux fonctionnalités de base d’un navigateur, à savoir afficher du texte et établir un hyperlien. Sa syntaxe satisfait aux exigences du W3C.</p>
      <p id="modifParagraphe">Le <a href="https://fr.wikipedia.org/wiki/World_Wide_Web_Consortium">W3C</a> (<i>World Wide Web Consortium</i>), est l’organisme ayant en charge la promotion et la standardisation du Web.</p>
    </div>
    <script src="test-dom.js"></script>
          
  </body>

</html>

Créez un dossier nommé test-dom. Copiez le code ci-dessus dans un fichier texte vide que vous sauvegarderez dans le dossier que vous venez de créer sous le nom test-dom.html.

Les modifications sont très succinctes : nous avons appelé titre la balise <h3> et modifParagraphe l’une des balises <p> grâce à l’attribut id. Je rappelle que, contrairement à l’attribut class, chaque id doit être unique dans le document (voir le chapitre sur le CSS dans la seconde partie).

Nous avons également ajouté la ligne <script src="test-dom.js"></script> après la balise </div>. Cette ligne demande que soit chargé, à l’endroit où elle se trouve, le contenu du fichier test-dom.js censé se trouver dans le même dossier que le fichier HTML.

Ouvrez ce fichier dans votre navigateur :

Image non disponible

vous pouvez constater qu’il n’y a aucun changement dans l’affichage par rapport à la première version. C’est tout à fait normal dans la mesure où, pour le moment, les identités ne sont utilisées nulle part et le fichier test-dom.js demandé n’existe pas dans le dossier où il est recherché. Comme d’habitude, le navigateur néglige ce qu’il ne peut pas utiliser.

Copiez maintenant le code suivant dans un autre fichier texte vide que vous nommerez test-dom.js et que vous sauvegarderez dans le même dossier :

test-dom.js
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
document.getElementById("titre").style.backgroundColor = "yellow";
document.getElementById("modifParagraphe").innerHTML = "Le paragraphe a été modifié !";
var tableau = document.getElementsByTagName("p"); 
var i;
for (i = 0; i < tableau.length; i++) {
  tableau[i].style.color = "#0000ff";
}

Ouvrez à nouveau le fichier HTML dans un nouvel onglet de votre navigateur et comparez les deux affichages :

Image non disponible

(pas de commentaires désobligeants quant à mon sens aigu de l’esthétique ;-)

Avant d’aller plus loin, demandez à votre navigateur d’afficher le code source de ces deux pages. Avec « Mozilla Firefox », par exemple, on obtient cela en faisant un clic droit sur la fenêtre concernée puis en cliquant sur l’item Code source de la page. Comparez les codes source de ces deux pages manifestement différentes :

Image non disponible

ils sont identiques.

Nous allons essayer d’analyser tout ça, en commençant par le fichier test-dom.html.

Nous avons vu que l’attribut id est utilisé pour identifier une balise précise de votre code HTML dans le but de modifier son comportement par défaut en utilisant le CSS. Au chargement de votre code HTML, l’objet document a intégré cet identifiant comme tout le reste de la page, et votre balise est donc identifiée dans le DOM de la même manière que vous l’avez identifiée dans votre code HTML. Cet identifiant va donc vous servir également pour agir sur cette balise avec JavaScript.

La ligne <script src="test-dom.js"></script> est destinée à importer le code JavaScript, un peu comme on l’a fait avec la balise <link> pour le fichier CSS, à cette différence près qu’ici, le code JavaScript sera incorporé à l’endroit où se trouve cette ligne dans le code HTML. Il faut bien comprendre ce mécanisme, mais il est assez logique. Lors de l’interprétation de votre code HTML, celui-ci est lu ligne par ligne pour générer le DOM. Quand l’interpréteur arrive sur du code JavaScript invoquant un élément particulier, il est indispensable que cet élément existe déjà dans le DOM pour que le code soit exécuté, à défaut de quoi, le code JavaScript ne sachant pas sur quoi agir ne fera rien. Si vous placez ce code dans l’entête de votre code HTML par exemple, donc avant la balise <body>, il est facile de comprendre qu’il ne pourra agir sur aucun élément de votre page puisqu’aucun n’a encore été créé.

Cela ne veut pas dire qu’on ne peut pas mettre de JavaScript dans l’entête, mais procédons par étapes.

Au tour du fichier test-dom.js.

Nous avons ici essentiellement trois lignes à expliciter. Pour tous ceux qui ont déjà manipulé des objets, la structure de ces lignes ne posera pas de problème. Pour les autres, voici comment ça se passe :

  • comme vu plus haut, la première chose que fait le navigateur quand il reçoit la page HTML est de la traduire en « quelque chose » d’utilisable par un langage de programmation, ici JavaScript. Ce quelque chose est un « objet », au sens informatique du terme, appelé document. Cette appellation n’est pas négociable. Contrairement à ce qui se passe habituellement lorsqu’on instancie un objet, on ne peut pas ici le nommer comme on veut. Est-ce embêtant ? Pas vraiment dans le sens où le programme ne travaillera jamais que sur une seule page, celle qui l’a chargé, et donc sur un seul objet. Ainsi, cet objet a été nommé une fois pour toutes document ;
  • cet objet, qui représente la page HTML, est composé, entre autres, de différents « champs » dont chacun représente une partie distincte de cette page. Chacun de ces champs est associé à une balise HTML liée à l’affichage d’un élément de la page. Par exemple, la balise <p></p> qui détermine l’affichage d’un paragraphe dans la page, est associée à un champ de l’objet document. La balise <h3></h3> dont le rôle est d’afficher un titre de niveau 3 est également associée à un champ. Nous avons vu que nous pouvions nommer univoquement une balise pour y faire référence avec CSS, en utilisant son attribut id. C’est ce même attribut qui va également nous permettre de faire référence à ce champ, en utilisant la construction suivante : document.getElementById(). Il y a plusieurs façons d’invoquer un élément de la page, que ce soit par son type (la balise ou tag), la classe à laquelle il appartient (class) s’il y en a une ou son nom (id). Pour cela, l’objet document nous fournit des « méthodes » permettant de faire notre choix. Ici, la méthode getElementById() , ce que l’on pourrait traduire par « sélectionner l’élément par son identité », va nous permettre de sélectionner un élément précis de la page, nommé id, pour n’agir que sur lui. Les méthodes et les propriétés d’un objet sont (presque) toujours préfixées par le nom de l’objet, ici document, suivi d’un point et du nom de la méthode ou de la propriété. Par exemple, pour invoquer notre balise <h3 id="titre">, nous écrirons document.getElementById("titre") ;
  • gardons le cas de notre balise <h3>. Comme tous les éléments, la balise <h3> possède des propriétés, la plus importante étant son libellé, le texte qu’elle affiche. Celui-ci lui a été attribué lors de l’écriture du fichier HTML (mais nous pourrions le modifier en cas de besoin). Dans notre exemple, le but est de faire s’afficher le titre sur fond jaune. La couleur du fond fait partie des propriétés gérées habituellement par les feuilles de style, et on aurait évidemment pu attribuer cette couleur depuis le début. Nous allons pour ce faire modifier la propriété backgroundColor appartenant au sous-ensemble style des propriétés de <h3>. Ce sous-ensemble comprend la majeure partie des propriétés CSS. On invoquera cette propriété en la préfixant du nom du groupe de propriétés auquel elle appartient de la manière suivante : style.backgroundColor. Notez la différence de nommage par rapport au CSS où elle est écrite background-color ;
  • enfin, l’affectation de la valeur qui se fait traditionnellement en utilisant le signe égal (=) et en utilisant, puisque dans notre cas il s’agit de couleurs, l’un des codages autorisés en CSS. Vous trouverez ici les nommages reconnus par tous les navigateurs.

Les deux autres lignes s’analysent de la même manière avec bien sûr de petites différences sans lesquelles elles n’auraient pas d’intérêt :

  • dans la deuxième ligne, la propriété innerHTML représente le texte qui sera affiché par la balise <p id="modifParagraphe"></p>. Ce texte remplacera le texte précédent ;
  • la troisième ligne utilise une autre méthode du DOM nommée getElementsByTagName(). Elle se différencie de la précédente par le fait que, au lieu de sélectionner un élément unique, elle sélectionne toutes les balises (tags) de la page correspondant à la balise donnée en argument, et les place dans un tableau en suivant l’ordre dans lequel elles apparaissent dans le code. Elle sera donc utilisée pour effectuer un même traitement sur un groupe de balises identiques. Ce traitement se fera item par item en parcourant le tableau avec par exemple une boucle for, comme ici pour modifier la couleur du texte de toutes les balises <p></p> se trouvant dans la page.

IV. AJAX

AJAX est une technique permettant l’échange de données textuelles entre le client et le serveur. Vous allez peut-être me dire que ça n’a rien de transcendant puisque c’est ce qu’on a fait dans la première partie, et vous auriez en partie raison. Il va tout de même y avoir un petit plus, vous devez vous en douter.

Dans la première partie, le client envoyait au serveur une requête, sous forme textuelle, incorporée à l’URL : souvenez-vous de « http://192.168.1.200/?on=ON ». Le serveur traitait cette requête, fabriquait une nouvelle page intégrant le résultat de son traitement, puis l’envoyait au client, sous forme textuelle également, pourrait-on dire, puisque le fichier HTML est un fichier texte. Mais peut-on parler d’échange ? Alors oui dans une certaine mesure, mais à sens unique. Le serveur a bien traité la requête, et donc travaillé sur une donnée envoyée par le client, mais le client n’a fait qu’afficher le produit fini. Aucun traitement n’a été effectué à son niveau sur une éventuelle donnée fournie par le serveur. AJAX va permettre de changer la donne, notamment en entraînant les réorganisations suivantes :

  • transfert d’une partie de la charge de travail du serveur vers le client. Ce n’est plus le serveur qui construira la page mais le client, en exploitant les données issues du traitement de la requête par le serveur ;
  • compensation de ce surcroît de travail du client par l’exonération de l’interprétation d’une nouvelle page et de son affichage ;
  • optimisation du trafic sur le réseau : la baisse de charge due à l’envoi d’une donnée à la place d’une page entière minimise les temps de transfert et améliore la fluidité des échanges.

IV-A. Envoi d’une requête

La technique AJAX réside dans la manipulation à l’aide de JavaScript d’un objet fourni par le navigateur : XMLHttpRequest. Cet objet possède les méthodes et les propriétés nécessaires à l’envoi et à la réception de chaînes de caractères ainsi qu’au traitement des éventuelles erreurs survenues pendant l’opération.

Créez les fichiers test-ajax.html et test-ajax.js contenant respectivement les codes suivants :

test-ajax.html
Sélectionnez
<!DOCTYPE html>

<html lang="fr">

  <head> <!-- La balise <head> peut être omise en HTML5 -->
    <meta charset="utf-8" />
    <script src="test-ajax.js"></script>
    <link rel="icon" type="image/png" href="icone.png">
    <title>Télécommande</title>
  </head>
  
  <body>
    <p class="titre">Cliquez sur le bouton</p>
    <button type="button" id="bouton" onClick="envoyer()">Envoyer</button>
  </body>
    
</html>
test-ajax.js
Sélectionnez
const chaine = "Donnée envoyée";

function envoyer() 
{
  var xhr = new XMLHttpRequest();
  xhr.open("GET", chaine , true);
  xhr.send(null);
}

Voyons ces deux codes dans l’ordre de leur apparition à l’écran.

Pour le code HTML, vous avez déjà tout vu, à l’exception de la balise <button>. Comme son nom l’indique, cette balise génère un bouton dans votre page. Trois types de boutons existent :

  • button : bouton généraliste auquel vous devrez associer une fonction personnalisée. C’est le type dont nous avons besoin ici ;
  • submit : bouton dont la fonction est d’envoyer le formulaire dans lequel il est défini ;
  • reset : bouton commandant la restauration des valeurs par défaut des champs du formulaire dans lequel il est défini.

Comme le type de bouton par défaut n’est pas le même pour tous les navigateurs, il est préférable de l’indiquer, par précaution.

Nous avons donné une identité à ce bouton : id="bouton". Ce n’est peut-être pas très original, mais ça fera l’affaire. Nous n’utiliserons pas cette identité dans ce chapitre, mais elle servira plus tard.

Le libellé du bouton, Envoyer, se trouve entre la balise ouvrante et la balise fermante.

Comme on l’a vu ci-dessus, le type button que nous avons choisi pour notre bouton nous impose de lui associer une fonction. C’est ce qu’on fait avec l’attribut onClick. Cet attribut permet d’associer ce qu’on appelle un « gestionnaire d’évènement », la fonction en question, à l’évènement onClick qui se produit lorsque l’on clique sur ce bouton avec la souris. Cette fonction doit se trouver dans le code JavaScript chargé avec votre page, et porter le même nom que celui fourni à l’attribut. Avec le paramétrage onClick="envoyer()", nous créons cette association.

Dans le code JavaScript, nous définissons donc la fonction envoyer() qui sera appelée à chaque clic de la souris sur le bouton.

Pour manipuler un objet, il faut avant tout en créer une instance. Le document du DOM est un cas particulier. La ligne var xhr = new XMLHttpRequest(); va s’en charger. La variable xhr est une nouvelle instance de l’objet XMLHttpRequest(). Les mots réservés var et new parlent d’eux-mêmes.

« xhr », diminutif couramment employé pour désigner l’objet XMLHttpRequest() est également souvent utilisé comme identifiant pour instancier cet objet : c’est pourquoi je l’utilise. Il n’est bien entendu pas obligatoire et peut être remplacé par n’importe quel identifiant valide.

Nous utilisons ensuite la méthode open() de l’objet xhr. Cette méthode attend trois arguments :

  • le mode d’acheminement de la requête. Les deux modes possibles sont "GET" et "POST". Nous utiliserons "GET" qui est réputé plus simple d’emploi, mais les deux modes sont possibles ;
  • la requête proprement dite. Il s’agit normalement d’une URL ou d’un fichier texte. Dans notre cas, puisqu’on traite « à la main » les données sur le serveur, notre requête sera simplement une chaîne de caractères ;
  • Le commutateur indiquant le mode de traitement des données : "true" pour « asynchrone » et "false" pour « synchrone ». Comme le mode synchrone bloque le déroulement du programme dans l’attente de la réponse, il est préférable d’utiliser le mode asynchrone. De toute manière, le w3c déconseille d’utiliser le mode synchrone et une procédure de suppression de ce mode est en cours.

Enfin la méthode send() de l’objet xhr est appelée avec l’argument null (ou sans argument) si le mode "GET" est utilisé, ou une chaîne de caractères s’il s’agit du mode "POST".

Il ne nous reste plus qu’à écrire le sketch du serveur pour faire vivre tout ça, ou du moins adapter le sketch serveur vu au chapitre III-D Récapitulation de la seconde partie. L’adaptation va se cantonner à la prise en charge des nouveaux fichiers, test-ajax.html et test-ajax.js, ainsi que du type des données traitées.

La ligne :

tuto.html
Sélectionnez
44.
envoiFichier(client, "tuto.html");

devient :

test-ajax.ino
Sélectionnez
44.
envoiFichier(client,"test-ajax.html");

et les lignes :

tuto.html
Sélectionnez
46.
47.
48.
49.
50.
if (reception.startsWith("GET /tuto-1.css HTTP/1.1"))
{
  arHtml(client, "text/css");
  envoiFichier(client, "tuto.css");
}

deviennent :

test-ajax.ino
Sélectionnez
46.
47.
48.
49.
50.
if (reception.startsWith("GET /test-ajax.js HTTP/1.1"))
{
  arHtml(client, "application/javascript");
  envoiFichier(client, "test-ajax.js");
}

Ces mécanismes ayant été expliqués dans la seconde partie, je ne m’y étendrai pas. Il s’agit d’une simple adaptation au nouveau contexte. Un seul petit détail : vous trouverez souvent dans la littérature le paramètre text/javascript. Il fonctionne toujours, mais est à présent déprécié au profit de application/javascript. Donc, autant être à jour. Attention à ne pas faire d’extrapolation : text/html et text/css sont toujours d’actualité.

Copiez ces deux fichiers sur votre carte microSD, mettez-la en place sur votre prototype, téléversez le sketch ci-dessus puis connectez-vous.

Au niveau du navigateur, vous devez avoir quelque chose qui ressemble à ça :

Image non disponible

ce qui est en tout point conforme (heureusement) à notre code HTML.

Jetons maintenant un petit coup d’œil au moniteur de l’EDI :

Image non disponible

Il est lui aussi en tout point conforme à ce qu’on devait obtenir. Bien sûr, s’il s’agit de votre première connexion à la télécommande depuis que vous avez lancé votre navigateur, vous aurez également la requête concernant le fichier de l’icône.

Cliquez à présent sur « Effacer la sortie » en bas à droite de la fenêtre du moniteur, puis cliquez sur le bouton « Envoyer » de la télécommande. Vous allez probablement obtenir ceci :

Image non disponible

Ce qui est rassurant, c’est que les mêmes causes produisent les mêmes effets. Comme nous l’avons déjà vu, quand une requête n’est pas traitée, le navigateur n’obtenant pas d’accusé de réception réitère sa demande jusqu’à un maximum de dix fois puis abandonne. Mais alors, pourquoi, contrairement à ce qui s’était passé précédemment, la page reste-t-elle affichée ? Eh bien tout simplement parce qu’en utilisant l’objet XMLHttpRequest, la page n’est pas rechargée. C’est l’intérêt même de l’utilisation de cette technique.

Vous avez bien entendu noté les séquences de caractères commençant par « % » remplaçant les « é » et l’espace. C’est dû au fait que la chaîne envoyée ne respecte pas le jeu de caractères autorisés nativement dans une URL (un sous-ensemble des caractères ASCII). Il nous faudra donc veiller, lors de nos futures requêtes, à n’employer que les bons caractères, et notamment à faire l’impasse sur les caractères accentués et sur l’espace.

IV-B. Réception de la réponse du serveur

Cette réception va nécessiter trois étapes :

  • élaboration et envoi d’une requête cohérente par le client ;
  • élaboration et envoi de la réponse par le serveur à l’aide du sketch Arduino ;
  • réception de la réponse et mise à jour de la page par le client à l’aide du programme JavaScript.

Je vous propose pour rentrer dans le concret (enfin) de réaliser le petit montage suivant :

Image non disponible

Il s’agit du même montage que celui réalisé dans la première partie : anode reliée à la broche 2, cathode reliée à GND à travers une résistance de 220 Ω.

IV-B-1. Requête du client

Nous allons faire les quelques modifications suivantes dans nos codes :

  • dans le code HTML, modifiez la ligne :
test-ajax.html
Sélectionnez
<button type="button" id="bouton" onClick="envoyer()">Envoyer</button>

comme suit :

test-ajax.html
Sélectionnez
<button type="button" id="bouton" value="inverser" onClick="envoyer()">Inverser</button>

Nous avons ajouté l’attribut value="inverser", qui va nous permettre avec JavaScript de récupérer (ou de modifier) une valeur associée au bouton, et nous avons changé le libellé du bouton ;

  • modifiez le code JavaScript comme suit :
test-ajax.js
Sélectionnez
const typeReq = "ajax?";

function envoyer() 
{
  var requete = typeReq + document.getElementById("bouton").getAttribute("value");
  var xhr = new XMLHttpRequest();
  xhr.open("GET", requete , true);
  xhr.send(null);
}

Ici, nous avons remplacé la constante chaine par une nouvelle constante appelée typeReq, dont la valeur est fixée à "ajax?", et par la variable requete qui inclura entre autres cette constante. Celle-ci va nous servir de préfixe à la requête pour permettre au serveur de déterminer à quel type de requête (d’où le nom typeReq) il a affaire. Nos requêtes AJAX commenceront donc toujours par ajax?. Il est évident que ce préfixe n’est pas obligatoire ou qu’il peut être différent. Il y a généralement plusieurs approches possibles pour traiter un même problème, je ne vous livre ici qu’une approche personnelle.

La construction de la chaîne requete commence donc par la constante que l’on vient de voir, à laquelle on ajoute le contenu de l’attribut value. Regardez bien la structure de la ligne qui réalise cela. On déclare la variable chaîne. On lui affecte la valeur de la constante typeReq puis on ajoute à l’aide de l’opérateur de concaténation « + » la chaîne représentant le contenu de l’attribut value. Pour récupérer cette chaîne, on part du document, on sélectionne l’élément bouton avec getElementById("bouton") puis on récupère le contenu de value avec getAttribute("value") ;

  • dans le sketch Arduino, commencez par remplacer les lignes 40 à 55 par :
test-ajax.ino
Sélectionnez
if (reception.indexOf("ajax?") != -1) // Si requête ajax
{
  arHtml(client, "application/json");
  reception.replace("GET /ajax?", "");
  reception.replace("HTTP/1.1", "");
  reception.trim();
  Serial.println(reception);
  if (reception == "inverser")
  {
    digitalWrite(led, !digitalRead(led));
  }
}
else
{
  Serial.println(reception);
  if (reception.startsWith("GET / HTTP/1.1"))
  {
    arHtml(client, "text/html");
    envoiFichier(client, "test-ajax.html");
  }
  else if (reception.startsWith("GET /test-ajax.js HTTP/1.1"))
  {
    arHtml(client, "application/javascript");
    envoiFichier(client, "test-ajax.js");
  }
  else if (reception.startsWith("GET /icone.png HTTP/1.1"))
  {
    arHtml(client, "image/png");
    envoiFichier(client, "icone.png");
  }
}

puis déclarez comme d’habitude la constante led(const led = 2) et configurez-la en sortie (pinMode (led, OUTPUT)).

L’analyse de ce code montre qu’il y a deux blocs distincts dont l’exécution est liée à la présence ou à l’absence du préfixe ajax? dans la requête :

  • s’il est absent, on passe directement au deuxième bloc qui est le même que celui vu précédemment et dont l’objet est d’envoyer le fichier HTML et les fichiers requis par ce dernier. Ces mécanismes ont été vus plus haut ;
  • si le préfixe est présent, il s’agit d’une requête AJAX et on va la traiter. On commence par envoyer l’accusé de réception. De cette manière, si la requête est longue à traiter, le navigateur n’aura aucune raison de s’inquiéter. Ensuite, on isole la requête proprement dite en supprimant le texte superflu maintenant qu’il a fait son travail. Ce texte, qui encadre la requête, sera toujours le même et il est donc très simple de le supprimer grâce à une des méthodes de l’objet String : replace(). Cette méthode a pour fonction de remplacer une sous-chaîne par une autre. On remplace donc la sous-chaîne que l’on veut supprimer par une chaîne vide et le tour est joué. La syntaxe est évidente dans le code et ne nécessite pas d’autres explications. Enfin, un petit passage par la méthode trim() qui permet de s’assurer qu’aucun caractère indésirable ne traîne en début ou en fin de chaîne ;
  • on affiche, pour contrôle, la requête sur le moniteur ;
  • on évalue la requête : si elle est égale à « inverser », on inverse l’état de la LED. Bien entendu, puisqu’ici il n’y a qu’une commande possible, ce test est inutile et on peut inverser directement l’état de la LED.

Mettez à jour les fichiers sur la carte microSD, insérez-la dans son logement et téléversez le sketch puis connectez-vous.

Au niveau du client, l’interface n’a bien entendu pas changé, à part le libellé du bouton. Au niveau du moniteur, vous obtiendrez quelque chose comme :

Image non disponible

Nous avons les trois (ou deux en fonction du contexte) lignes déjà vues générées lors de la connexion, suivies d’une requête générée à chaque clic sur le bouton de la télécommande, lequel entraîne un changement d’état de la LED.

Il y a bien entendu une multitude de façons de « rédiger » une requête et de la traiter. Dans les conditions présentes, étant donné la simplicité du cas, on aurait pu écrire tout simplement :

test-ajax.ino
Sélectionnez
if (reception.indexOf("ajax?") != -1) // Si requête ajax
{
  arHtml(client, "application/json");
  digitalWrite(led, !digitalRead(led));
}

Traiter la requête ne sert à rien puisqu’elle sera toujours la même : il suffit de savoir que c’est bien une requête AJAX et donc de vérifier la présence de ajax? dans la chaîne reçue. Ensuite on envoie l’accusé de réception pour éviter que le navigateur n’émette dix fois la requête, puis on inverse l’état de la LED. L’attribut value de l’élément bouton ne sert à rien, pas plus que l’attribut id. Ils peuvent donc être supprimés du paramétrage de la balise <button>, ce qui donne :

test-ajax.html
Sélectionnez
<button type="button" onClick="envoyer()">Inverser</button>

On pourrait même supprimer le libellé du bouton, dans la mesure où il n’y a aucune ambiguïté sur son utilisation, mais bon !

Enfin, le code JavaScript pourrait se résumer à ceci :

test-ajax.js
Sélectionnez
function envoyer() 
{
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "ajax?", true);
  xhr.send(null);
}

Cependant, en procédant comme on l’a fait, on a préparé le terrain pour la suite.

IV-B-2. Réponse du serveur

Ce qu’on vient de voir pourrait suffire dans de nombreux cas. On dispose d’une télécommande avec un nombre quelconque de boutons, chacun étant destiné à inverser l’état d’un éclairage : un « multitélérupteur télécommandé » en quelque sorte. Une simple extrapolation de nos codes permet de réaliser cela.

Tant que vous vous trouvez dans la pièce concernée par les éclairages (ou quoi que ce soit d’autre), vous avez le contrôle visuel de l’opportunité de modifier l’état d’un des éclairages (restons sur cet exemple) d’une part, et d’autre part de vérifier le bon déroulement de cette modification. Mais, comme on l’a vu dans la première partie, ce n’est pas toujours le cas.

Pour ce qui nous concerne ici, il s’agit de savoir si la LED est allumée ou éteinte. Il faut donc que le serveur envoie un message au client et que celui-ci l’affiche d’une manière ou d’une autre.

Pour l’affichage, on va faire simple : on va jouer sur le libellé du bouton. Quand la LED sera éteinte, le bouton affichera « Allumer », et dans le cas contraire, il affichera « Éteindre ». C’est clair et pratique : on a en même temps le renseignement sur l’état de la LED et sur la fonction du bouton.

On va reprendre les codes établis ci-dessus et les modifier pour les adapter à nos désirs.

Pour le code HTML, peu de changements. La ligne :

test-ajax.html
Sélectionnez
<button type="button" id="bouton" value="inverser" onClick="envoyer()">Inverser</button>

devient :

test-ajax.html
Sélectionnez
<button type="button" id="bouton" value="ON" onClick="onClickBouton()">Allumer</button>

Nous avons modifié le paramètre de value qui devient ON et le libellé du bouton qui devient « Allumer ». Nous avons, de plus, renommé la fonction envoyer(), car les modifications que va subir cette fonction rendent ce nom peu pertinent.

Voyons le sketch à présent. Nous allons modifier le fragment de code :

test-ajax.ino
Sélectionnez
if (reception.indexOf("ajax?") != -1) // Si requête ajax
{
  arHtml(client, "application/json");
  reception.replace("GET /ajax?", "");
  reception.replace("HTTP/1.1", "");
  reception.trim();
  Serial.println(reception);
  if (reception == "inverser")
  {
    digitalWrite(led, !digitalRead(led));         
  }
}

comme suit :

test-ajax.ino
Sélectionnez
if (reception.indexOf("ajax?") != -1) // Si requête ajax
{
  arHtml(client, "application/json");
  reception.replace("GET /ajax?", "");
  reception.replace("HTTP/1.1", "");
  reception.trim();
  Serial.println(reception);
  if (reception == "ON")
  {
    digitalWrite(led, HIGH);
    client.write("ledOn");
  }
  else if (reception == "OFF")
  {
    digitalWrite(led, LOW);
    client.write("ledOff");
  }
}

Si le serveur reçoit ON, il allume la LED et renvoie ledOn au client, sinon, s’il reçoit OFF, il éteint la LED et envoie ledOff au client. Ce code ne présente pas de difficulté particulière. Comme dit au chapitre précédent, étant donné la simplicité du dispositif, on peut faire bien plus simple, mais je préfère rester dans le cas général où on aurait plusieurs boutons à gérer.

IV-B-3. Actualisation de la page

Après avoir utilisé les fonctionnalités de l’objet XMLHttpRequest concernant l’envoi de la requête du client, nous allons mettre en œuvre celles concernant la réception de la réponse du serveur.

Pour envoyer notre requête, nous avions créé la fonction envoyer(). Cette fonction était appelée par l’évènement onClick du bouton. Cet évènement ne pouvant appeler qu’une fonction, le traitement de la réponse doit impérativement être implémenté par cette fonction, ou par une autre fonction qu’elle va elle-même appeler.

La fonction envoyer() va donc être scindée en deux fonctions onClickBouton() et requeteAjax(), la première appelant la seconde. Voici le déroulement des opérations :

  1. La fonction onClickBouton() va reprendre la même construction de la requête que la fonction envoyer() puis va appeler la fonction requeteAjax(). La variable requete doit quant à elle être déclarée globale puisqu’elle sera utilisée en dehors de la fonction ;
  2. La fonction requeteAjax() va dans un premier temps envoyer la requête générée par la fonction onClickBouton() en reprenant exactement le même code. Nous avons donc « reconstitué » en quelque sorte notre fonction envoyer(), et ce n’est déjà pas si mal ;
  3. La fonction requeteAjax() va dans un second temps, et c’est là que réside la nouveauté, se charger de la réception et de l’exploitation de la réponse à la requête qu’elle vient d’envoyer, en utilisant les fonctionnalités suivantes de l’objet XMLHttpRequest :

    • onreadystatechange,
    • readystate,
    • responseText.

L’évènement onreadystatechange est déclenché à chaque changement de readyState. Comme tout évènement, on l’a vu précédemment pour l’évènement onClick du bouton, il doit être associé à un gestionnaire d’évènement. Ce gestionnaire d’évènement aura pour fonction de récupérer la réponse fournie par le serveur, et mise à la disposition de JavaScript sous forme d’une simple chaîne de caractères, par l’intermédiaire du champ responseText de l’objet XMLHttpRequest, et d’exploiter cette réponse pour mettre à jour l’affichage de la page.

Lors de l’envoi de la requête, plusieurs procédures sont déclenchées pour mener à bien le processus de récupération de la réponse envoyée par le serveur. La propriété readyState renvoie le compte rendu de chacune de ces étapes sous forme d’un code numérique pour permettre un éventuel traitement par le programme. Les valeurs possibles sont :

  • 0 – Requête non initialisée ;
  • 1 – Connexion établie avec le serveur ;
  • 2 – Requête reçue par le serveur ;
  • 3 – Requête en cours de traitement par le serveur ;
  • 4 – Requête traitée, réponse envoyée et prête.

Seule la valeur « 4 » va nous intéresser. En effectuant le test if (xhr.readyState == 4), elle nous indiquera le moment auquel le gestionnaire d’évènement pourra récupérer la réponse.

Lors de toute requête émise par le client, le serveur renvoie un « message d’état HTTP » destiné à renseigner celui-ci sur un éventuel problème de transaction à fin de traitement. Tout le monde (?) a eu l’occasion au moins une fois de tomber sur le message « 404 Page non trouvée », ou un libellé approchant, le « 404 » représentant le code de l’erreur détectée quand la page demandée n’existe pas.

La réception de ce code par le navigateur permet par exemple à celui-ci d’ afficher, à l’aide du JavaScript, une page d’information plus explicite que le simple message « 404 ».

Donc, en réalité, le test standard lié à la réception de la requête devrait être :

 
Sélectionnez
if (xhr.readyState == 4 && xhr.status == 200) {}

Avec xhr.status == 200 indiquant le bon déroulement des opérations. Comme, dans notre cas, ce renseignement est peu pertinent et que nous ne le traiterons pas, je ne l’incorpore pas au test, mais en toute rigueur, il devrait en faire partie.

Pour des raisons de clarté, nous allons laisser le gestionnaire d’évènement dans la fonction requeteAjax(), mais nous allons déplacer le code traitant la réponse dans une fonction dédiée, que je vous propose d’appeler actualisation(), puisque son travail sera justement d’actualiser l’affichage de la page, fonction qui sera appelée par le gestionnaire d’évènement. C’est cette fonction qui sera chargée de récupérer xhr.responseText pour en effectuer le traitement.

La fonction actualisation() nous fait découvrir une nouvelle procédure et une nouvelle propriété du DOM :

  • la procédure setAttribute("attribut", "valeur") sert à modifier la valeur d’un attribut. Ici, nous travaillons sur l’attribut value de bouton que nous fixons à ON ou à OFF selon que son action sera d’allumer ou d’éteindre la LED ;
  • la propriété innerHTML représente le texte se trouvant entre la balise ouvrante et la balise fermante. Dans le cas de la balise <button>texte</button>, texte représente le libellé du bouton, qui passe ici de « Allumer » à « Éteindre », selon qu’il s’agira d’allumer ou d’éteindre la LED.

Voici le nouveau code JavaScript :

test-ajax.js
Sélectionnez
const typeReq = "ajax?";
var requete = "";

function onClickBouton() 
{
 requete = typeReq + document.getElementById("bouton").getAttribute("value");
 requeteAjax();
} 

function requeteAjax() 
{
 var xhr = new XMLHttpRequest();
  xhr.open("GET", requete , true);
  xhr.send(null);
  xhr.onreadystatechange = function()
  {
    if (xhr.readyState == 4)
    {
    actualisation(xhr.responseText);
    }
  }
}

function actualisation(reponse)
{
  if ( reponse == "ledOn")
  {
    document.getElementById("bouton").setAttribute("value","OFF");
    document.getElementById("bouton").innerHTML = "Éteindre";
  }
  else if ( reponse == "ledOff") 
  {
    document.getElementById("bouton").setAttribute("value","ON");
    document.getElementById("bouton").innerHTML = "Allumer";
  }   
}

Mettez à jour votre carte microSD avec les nouveaux codes, replacez-la dans son slot, téléversez le nouveau sketch et connectez-vous. Cliquez sur le bouton et observez les changements.

À force de vous répéter ce qui est écrit ci-dessus, vous allez penser que je radote. Donc, une dernière fois, pour le plaisir :

  • à chaque modification d’un fichier HTML, JavaScript ou CSS, il faut mettre la carte microSD à jour et ne pas oublier de la réinsérer dans son logement sur l’Arduino. Ne riez pas, ça arrive ;
  • à chaque modification du sketch, n’oubliez pas de le téléverser, et ce de préférence après la réinsertion de la carte microSD si celle-ci a été manipulée, sinon vous devrez réinitialiser la carte Arduino pour que la carte microSD soit de nouveau reconnue ;
  • pour la même raison, si la carte microSD a été mise à jour, mais que le sketch n’a pas été modifié, vous devrez réinitialiser la carte Arduino.

À partir du moment où vous pouvez modifier le libellé du bouton sans recharger la page, vous pouvez de la même manière modifier n’importe quelle partie de cette page sans la recharger, et c’était le but de la manœuvre. Vous connaissez l’outil pour le faire, AJAX, et la façon (ou du moins l’une des façons) de vous en servir.

Pourtant, nous n’avons pas encore fini.

IV-B-4. Actualisation au chargement

Mettons-nous dans le cas de figure suivant : vous vous êtes connecté et vous avez allumé votre LED. N’oublions pas que la LED n’est qu’un ersatz d’expérimentateur. Le but est de piloter, à terme, quelque chose d’utile, et puisque nous étions partis sur un éclairage, restons-y. Une fois l’éclairage allumé, vous n’avez pas besoin de rester connecté. Vous pouvez vous déconnecter ou même vous absenter, votre éclairage sera toujours allumé.

Revenons à notre LED alimentée et reprenons notre expérimentation. Sans même avoir besoin de vous déconnecter, mais vous pouvez le faire, connectez-vous à nouveau. Vous chargez une nouvelle page. Cependant, cette page ne « sait » pas que votre LED est allumée, et elle affiche le libellé que vous avez saisi dans votre code HTML, à savoir « Allumer », et le paramétrage de value est ON. Si vous cliquez sur le bouton, la requête que vous allez envoyer sera donc d’allumer une LED qui l’est déjà. Bien entendu, à partir de là, tout reprend son cours normal, mais d’une part ça ne fait pas très professionnel, et d’autre part il peut y avoir des circonstances dans lesquelles cela poserait un problème.

Y a-t-il un moyen pour que l’affichage de la télécommande au chargement reflète l’état du système télécommandé ? Probablement oui, sinon je ne poserais pas la question. Ce moyen réside dans l’utilisation de l’attribut onload applicable, entre autres, à la balise <body>. Cet attribut correspond à un évènement et doit donc être associé à un gestionnaire d’évènement : cet évènement se produira au chargement de la page.

Commencez par modifier, dans le fichier test-ajax.html, la balise <body> comme suit :

test-ajax.html
Sélectionnez
<body onload="maj()">

De cette manière, quand l’interpréteur HTML de votre navigateur incorporera la balise <body> au DOM, il appellera la fonction maj() de votre code JavaScript. Il faut donc écrire cette fonction. Dans le fichier test-ajax.js, déclarez la constante globale reqMaj et la fonction maj() comme suit :

test-ajax.js
Sélectionnez
const reqMaj = "maj?";/function maj()
{
  requete = typeReq + reqMaj;
  requeteAjax();
}

La fonction maj() construit la requête "ajax?maj?" puis appelle la fonction requeteAjax(); qui va se charger de l’envoyer au serveur.

Il nous faut donc modifier le sketch pour qu’il réponde convenablement à cette requête. Nous allons en profiter pour mettre dans une fonction, que l’on va appeler maj() également, car c’est cohérent, ce qui ne concerne que la mise à jour. Ça nous sera utile plus tard.

Voici le nouveau sketch. Je le donne dans son intégralité pour avoir une vue d’ensemble de tout ce que nous venons de voir :

test-ajax.ino
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
#include <SdFat.h>
#include <Ethernet.h>

SdFat SD;

const int carteSD = 4;
const int led = 2;

byte macSerre[] = {0x90, 0xA2, 0xDA, 0x10, 0x4F, 0x25};
IPAddress IPSerre(192, 168, 1, 200);
EthernetServer serveurHTTP(80);

void setup()
{
  SD.begin(carteSD);
  Ethernet.begin(macSerre, IPSerre);
  serveurHTTP.begin();
  pinMode(led, OUTPUT);
}

void loop()
{
  EthernetClient client = serveurHTTP.available();
  if (client)
  {
    if (client.connected())
    {
      String reception;
      while (client.available())
      {
        char carLu = client.read();
        if (carLu != 10)
        {
          reception += carLu;
        }
        else
        {
          break;
        }
      }
      if (reception.indexOf("ajax?") != -1) // Si requête ajax
      {
        if (reception.indexOf("maj?") == -1) // Si pas maj
        {
          reception.replace("GET /ajax?", "");
          reception.replace("HTTP/1.1", "");
          reception.trim();
          if (reception == "ON")
          {
            digitalWrite(led, HIGH);
          }
          else if (reception == "OFF")
          {
            digitalWrite(led, LOW);
          }
        }
        arHtml(client, "application/json");
        maj(client);
      }
      else
      {
        if (reception.startsWith("GET / HTTP/1.1"))
        {
          arHtml(client, "text/html");
          envoiFichier(client, "test-ajax.html");
        }
        else if (reception.startsWith("GET /test-ajax.js HTTP/1.1"))
        {
          arHtml(client, "text/javascript");
          envoiFichier(client, "test-ajax.js");
        }
        else if (reception.startsWith("GET /icone.png HTTP/1.1"))
        {
          arHtml(client, "image/png");
          envoiFichier(client, "icone.png");
        }
        delay(1);
      }
      client.stop();
    }
  }
}

// FONCTIONS

void arHtml(EthernetClient nomClient, String type)
{
  nomClient.println(F("HTTP/1.1 200 OK"));
  nomClient.println("Content-Type: " + type);
  nomClient.println(F("Connection: close"));
  nomClient.println();
}

void envoiFichier(EthernetClient nomClient, String fichierEnCours)
{
  char tableau[fichierEnCours.length() + 1];
  fichierEnCours.toCharArray(tableau, fichierEnCours.length() + 1);
  if (SD.exists(tableau))
  {
    File fichier = SD.open(fichierEnCours, FILE_READ);
    while (fichier.available())
    {
      nomClient.write(fichier.read());
    }
    fichier.close();
  }
  else
  {
    nomClient.println("Page '" + fichierEnCours + "' introuvable...");
  }
}

void maj(EthernetClient nomClient)
{
  if (digitalRead(led) == HIGH)
  {
    nomClient.write("ledOn");
  }
  else if (digitalRead(led) == LOW)
  {
    nomClient.write("ledOff");
  }
}

Commençons par la fonction maj() puisqu’elle est juste au-dessus. Cette fonction reçoit en argument une instance EthernetClient dont elle a besoin pour pouvoir envoyer la réponse. Pour générer cette réponse, elle teste simplement l’état de la LED, puis elle envoie ledOn ou ledOff au client selon que la LED est allumée ou éteinte.

En ce qui concerne la réception de la requête, nous avons toujours le test :

test-ajax.ino
Sélectionnez
if (reception.indexOf("ajax?") != -1)
{

}

qui sépare comme précédemment les requêtes AJAX des autres requêtes, mais il est suivi d’un autre test qui va séparer les requêtes AJAX standard, c’est-à-dire les commandes, des demandes de mises à jour. De ce fait, tout ce qui se trouvera dans le bloc :

test-ajax.ino
Sélectionnez
if (reception.indexOf("maj?") == -1)
{

}

ne concernera que l’extraction de la requête et l’action associée au niveau de l’Arduino, la partie mise à jour de l’affichage chez le client rendant compte de cette action étant dévolue à la fonction maj(). Remarquez les deux lignes :

test-ajax.ino
Sélectionnez
arHtml(client, "application/json");
maj(client);

qui se trouvent en dehors de ce bloc, car elles doivent toujours être exécutées, qu’il s’agisse d’une requête AJAX standard ou d’une simple demande de mise à jour.

Vous avez peut-être noté l’ajout suivant :

 
Sélectionnez
77.
delay(1);

Cette ligne de code est censée donner au navigateur web le temps de recevoir les données. Je n’ai jamais constaté de problème lié à l’absence de cette temporisation ce qui fait que je ne l’utilise habituellement pas. Comme il s’agit d’un tutoriel, je l’inclus dans notre code pour rester en conformité avec les exemples de sketchs « officiels ». Vous ferez vos propres choix. De toute manière, nous ne sommes pas à une milliseconde près.

Connectez-vous et allumez votre LED. Sans fermer la page, reconnectez-vous : le libellé du bouton sur la nouvelle page est bien « Éteindre ». C’est ce que nous voulions obtenir.

Pourtant, tout n’est pas réglé. Cliquez sur le bouton. La LED s’éteint et le libellé du bouton devient « Allumer ». Ça fonctionne. Cliquez maintenant sur l’onglet de la première page. Le libellé du bouton est toujours « Éteindre ». Eh oui ! Aucun mécanisme n’a été prévu pour le mettre à jour automatiquement.

Ça n’en finit pas : on règle un problème, un autre surgit. Zen… Mais à chaque jour suffit sa peine, comme on dit : on réglera ça plus tard. Le chapitre AJAX ayant atteint son objectif, du moins je l’espère, il est temps de passer à un autre chapitre.

IV-C. Affichage de la température

Le principe en a été décrit dans la première partie (voir VI. Télémétrie), je ne reviendrai pas dessus. Il va suffire de l’adapter à notre nouvelle approche, ce qui va être, somme toute, assez simple.

Commençons par réaliser le montage suivant :

Image non disponible

Il s’agit de la réunion des deux montages réalisés dans la première partie. Le câblage de la LED étant le même que celui effectué auparavant dans ce tutoriel, vous n’aurez à ce niveau rien à modifier, et si vous vous connectez dès maintenant, vous constaterez que votre télécommande fonctionne comme précédemment.

Nous allons d’abord prévoir une zone dans notre page HTML pour afficher cette température :

test-ajax.html
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
<!DOCTYPE html>

<html lang="fr">

  <head>
    <meta charset="utf-8" />
    <link rel="icon" type="image/png" href="icone.png">
    <script src="test-ajax.js"></script>
    <title>Télécommande</title>
  </head>
  
  <body onload="maj()">
    <p class="titre">Température de la serre</p>
    <p id="temperature"></p>
    <p class="titre">Cliquez sur le bouton</p>
    <button type="button" id="bouton" value="ON" onClick="onClickBouton()">Allumer</button>
  </body>
    
</html>

Nous avons ajouté deux paragraphes :

  • le premier annonce ce qui va être affiché. On le fait appartenir à la même classe que celui qui demande d’appuyer sur le bouton, ce qui permettra de le « styler » de la même manière si on le désire ;
  • le second est vide, pour l’instant, mais avec l’identité qu’on lui a donnée, il est référencé dans le DOM, et on pourra donc lui donner un texte à afficher, comme on l’a fait plus haut avec le libellé du bouton.

Les modifications à effectuer sur le sketch Arduino sont les suivantes :

  • déclaration, en début de sketch, de la constante LM35 qui représente la broche analogique à laquelle on a connecté notre capteur :

     
    Sélectionnez
    const byte LM35 = 0;
  • ajout en fin de sketch de la fonction relative au calcul de la température d’après les données fournies par le capteur :
 
Sélectionnez
float temperature(byte broche, word iteration)
{
  long lecture = 0;
  for (word i = 0; i < iteration; i++)
  {
    lecture += analogRead(broche);
  }   
  return(float(lecture) *500 /1024 /iteration);
}

Ces deux ajouts sont directement issus de la première partie.

Il va falloir à présent envoyer cette nouvelle donnée, c’est-à-dire modifier la fonction maj() dont c’est le travail, mais comment va-t-on s’y prendre ? On pourrait essayer quelque chose comme ceci pour le sketch Arduino :

Image non disponible

et comme cela pour le code JavaScript, dans lequel il n’y a à priori que la fonction actualisation() à modifier :

Image non disponible

Le serveur nous envoie la chaîne "temperature=valeur". On extrait la donnée exactement de la même manière qu’on extrayait la requête avec le sketch Arduino.

Si vous voulez faire le test et que vous vous connectez, vous constaterez que ça ne fonctionne pas :

  • la page HTML étant correcte, la nouvelle page présente bien la zone où la température doit s’afficher, mais celle-ci ne s’affiche pas ;
  • si vous cliquez sur le bouton, votre LED va bien s’allumer, signe que la télécommande en elle-même fonctionne, mais vous n’aurez pas de réponse du serveur et donc pas d’actualisation du bouton. Son libellé et le paramétrage de son attribut value ne changeront pas, ce qui fait que vous ne pourrez pas éteindre la LED puisqu’il sera toujours en configuration « allumage ».

Le problème vient de ce que la réponse générée par la fonction maj() du serveur est constituée de deux chaînes distinctes, mise à jour du bouton et mise à jour de la température, alors que la variable xhr.responseText, de type string, ne peut contenir qu’une seule chaîne. Il faut donc regrouper les différentes mises à jour, deux ici, mais il y en aura généralement plusieurs, dans une seule chaîne. On pourrait, pour la mise à jour de seulement deux évènements, bricoler quelque chose. Cependant, l’Arduino UNO possédant dix-neuf broches pilotables par votre télécommande, vous pourriez théoriquement être amené à gérer jusqu’à dix-neuf mises à jour. Là, le bricolage devient de la haute voltige et il vous faudra trouver une autre solution.

Cela ne constitue pas un problème insurmontable, mais va malgré tout vous obliger à :

  • concevoir côté serveur un formatage adapté aux types de données que vous êtes susceptibles d’envoyer ;
  • écrire côté client une routine destinée à décoder la chaîne formatée pour en extraire les informations utiles destinées à actualiser votre page HTML.

Bien qu’intéressant en soi, ce travail supplémentaire n’est pas forcément nécessaire : en informatique, comme pratiquement partout d’ailleurs, on sait qu’il ne sert à rien de réinventer la roue. D’autres solutions existent déjà : on va s’intéresser à l’une d’entre elles.

V. JSON

Depuis son origine, l’objet XMLHttpRequest est, comme son nom l’indique, destiné à réceptionner des chaînes au format XML, et de ce fait dispose d’un analyseur XML intégré. Grâce à cela, la chaîne récupérée dans la propriété xhr.responseXML est directement exploitable en tant qu’objet du DOM. Malgré cet avantage et malgré ses qualités, le XML nécessite une certaine pratique et n’est pas toujours le format le mieux adapté.

Dans notre cas, un autre format, très populaire également quoique plus récent, me semble plus approprié : JSON (JavaScript Object Notation). Il est parfaitement intégré dans JavaScript, puisque faisant partie de ce langage en tant qu’objet, et sa prise en main, comme vous allez le voir, est rapide. En outre, pour quelqu’un qui n’est familier ni de l’un ni de l’autre, ce qui était mon cas lorsque j’ai eu à faire le choix entre les deux, il est plus intuitif à lire et à écrire que le XML. Il est également plus compact dans la mesure où les balises utilisées en XML sont remplacées par des signes de ponctuation, à la fois plus rapides à saisir et prenant moins de place. Pour récupérer la chaîne JSON, il faudra simplement continuer à utiliser comme on l’a déjà fait la propriété xhr.responseText qui accepte tout type de texte, brut ou formaté.

Comme d’habitude, je ne vais détailler que ce que nous allons utiliser. Pour ceux qui voudront aller plus loin, ils trouveront sur w3schools.com de quoi se satisfaire.

Une chaîne JSON est composée de couples du type clé/valeur, comme on en trouve par exemple dans un fichier .ini, à ceci près que la syntaxe en est légèrement différente. D’une manière générale :

  • le couple clé/valeur s’écrira "clé":"valeur", c’est-à-dire deux chaînes de caractères guillemetées séparées par « : » (deux-points) quand « valeur » représente un texte, et "clé":valeur (sans guillemet pour valeur) quand « valeur » est numérique ;
  • chaque couple sera séparé du suivant par « , » (virgule) ;
  • la chaîne sera encadrée par { et } (accolades) ;
  • « clé » doit impérativement respecter les mêmes règles de nommage que les identifiants JavaScript, à savoir notamment ne pas contenir de caractères accentués. En effet, après analyse de la chaîne par la méthode JSON.parse(), « clé » va être utilisé comme identifiant en tant que propriété d’un objet JSON. Pour « valeur », les règles à appliquer dépendront de son utilisation.

Pour prendre un exemple concret directement en liaison avec cet article, la chaîne :

 
Sélectionnez
{"bouton" :"ledOn","temperature":20.45}

que notre sketch serveur va devoir générer et envoyer au client va être utilisée par JavaScript pour créer un objet JSON grâce au code suivant :

 
Sélectionnez
var objetJSON = JSON.parse({"bouton" :"ledOn","temperature":20.45}’);

en ajoutant à cet objet les propriétés objetJSON.bouton et objetJSON.temperature dont les valeurs sont respectivement "ledOn" et 20.45.

Nous allons donc écrire deux codes corrects, cette fois, en mettant en pratique ce que nous venons de voir :

  • la fonction maj() du sketch devient :
test-ajax.ino
Sélectionnez
void maj(EthernetClient nomClient)
{
  String reponseJSON = "{";
  reponseJSON.concat("\"bouton\":");
  if (digitalRead(led) == HIGH) reponseJSON.concat("\"ledOn\","); 
  else reponseJSON.concat("\"ledOff\",");
  reponseJSON.concat("\"temperature\":");
  reponseJSON.concat(temperature(LM35, 100));
  reponseJSON.concat("}");
  nomClient.println(reponseJSON);
}

Ce code ne présente aucune difficulté : il ne fait que construire étape par étape la chaîne vue ci-dessus. N’oubliez pas d’échapper les guillemets (\")qui font partie de la chaîne à envoyer ;

  • la fonction JavaScript actualisation() devient, quant à elle :
test-ajax.js
Sélectionnez
function actualisation(reponse)
{
  var retour = JSON.parse(reponse);
  if (retour.bouton == "ledOn")
  {
    document.getElementById("bouton").setAttribute("value","OFF");
    document.getElementById("bouton").innerHTML = "Éteindre";
  }
  else if (retour.bouton == "ledOff")
  {
    document.getElementById("bouton").setAttribute("value","ON");
    document.getElementById("bouton").innerHTML = "Allumer";
  } 
  document.getElementById("temperature").innerHTML = retour.temperature;
  requete = typeReq + reqMaj;
}

Ici, comme vu plus haut, on crée un objet JSON appelé retour à l’aide de la fonction JSON.parse(). Cet objet inclut les deux champs bouton et temperature extraits de la chaîne reponse et permet, en analysant leur valeur respective, de mettre à jour le bouton et la température.

Je vous dirais bien maintenant d’essayer ces nouveaux codes, mais on va faire l’économie d’une manipulation superflue, car il manque encore quelque chose. À la connexion, tout se passera bien, la température va bien s’afficher et le bouton aura toutes ses fonctionnalités, mais par la suite vous constaterez que la température ne change que lorsque vous cliquez sur le bouton. C’est en fait assez normal, et c’est d’ailleurs le contraire qui serait surprenant. La mise à jour de la température est réalisée par la fonction actualisation(). Cette fonction ne se lance pas toute seule : il faut qu’elle soit appelée. Dans l’état actuel des choses, il n’y a que deux appels possibles :

  • le premier est généré à la connexion lorsque se produit l’évènement onload(), attribut de la balise <body>, qui invoque la fonction maj(), laquelle appelle la fonction requeteAjax() qui elle-même appelle la fonction actualisation() ;
  • le second se produit à chaque clic sur le bouton qui génère l’évènement onClick(), lequel est associé au gestionnaire d’évènement onClickButton(), lequel lance la fonction requeteAjax() qui elle-même invoque la fonction actualisation(). Ouf !

Voilà pourquoi la température s’affiche bien à la connexion et est ensuite mise à jour à chaque clic sur le bouton, mais seulement à ces moments-là.

Mais ça, c’est l’explication : ce n’est pas la solution.

Peut-être vous souvenez-vous du petit problème que nous avons (ou plutôt que j’ai) laissé en suspens à la fin du chapitre IV-B-4. Actualisation au chargementActualisation au chargement concernant la mise à jour du bouton se trouvant sur un autre client connecté au serveur. La cause en est la même, et je ne l’avais pas traitée alors, mais maintenant c’est le moment.

Dans la première partie, ce problème avait été traité à l’aide de la ligne <meta http-equiv="refresh" content="1" /> placée dans l’entête du fichier HTML, et dont le but était de recharger entièrement la page. Ici, le BOM (Browser Object Model) va venir à notre secours avec son objet window.

Sans entrer dans le détail de ce qu’est le BOM, il peut être utile de savoir que l’objet window, qui représente la fenêtre du navigateur, est hiérarchiquement l’objet racine dont tous les autres éléments globaux, objets, fonctions et variables de JavaScript liés au navigateur, dont l’objet document du DOM, descendent. Cet objet, window donc, va nous fournir la méthode adéquate : setInterval(fonction, délais).

En toute rigueur, on devrait écrire :

 
Sélectionnez
window.setInterval(fonction, délais)

comme on aurait dû écrire par exemple :

 
Sélectionnez
window.document.getElementById()

Toutefois, quand il n’y a pas d’ambiguïté, il est admis de considérer le préfixe window comme implicite et donc de ne pas l’indiquer.

Cette méthode prend comme argument une fonction et une durée exprimée en millisecondes. Quand cette méthode est invoquée, la fonction passée en argument est exécutée périodiquement, la période correspondant à la durée passée en argument. Cette méthode doit être appelée dès le chargement de la page, donc on va la mettre dans la fonction maj(), et on va évidemment lui donner la fonction requeteAjax() comme premier argument, puisqu’il faut toujours envoyer une requête pour obtenir une réponse. Le serveur ne prend pas d’initiative.

La fonction maj() va être modifiée comme suit :

 
Sélectionnez
function maj()
{
  requete = typeReq + reqMaj;
  setInterval(requeteAjax, 1000);
}

Un dernier petit problème va se poser. Tant que vous n’avez qu’une télécommande connectée, ou une seule page ouverte ce qui revient au même, tout fonctionne à la perfection. Quand deux (ou plus) télécommandes sont connectées, chacune envoie, à la suite du clic sur son bouton, sa propre requête AJAX. Cette requête est générée par la ligne :

 
Sélectionnez
requete = typeReq + document.getElementById("bouton").getAttribute("value");

Or, chacune des deux télécommandes envoie à intervalle régulier cette requête, à cause de l’utilisation de la méthode setInterval() rendue nécessaire pour les mises à jour automatiques. En fonction de l’état initial des télécommandes, l’une des requêtes peut être "ajax?ON" alors que l’autre est "ajax?OFF". Le serveur reçoit alors, avec un petit décalage dans le temps, deux ordres contradictoires, et la LED clignote. Je signale au passage, pour ceux dont le seul but serait de faire clignoter la LED, qu’on peut faire plus simple (Blink ;-).

La solution consiste simplement à modifier la requête de telle manière que les simples mises à jour et les commandes effectives suivent un chemin différent dans le sketch Arduino. Complétez pour cela la fonction actualisation() en lui ajoutant la ligne :

 
Sélectionnez
requete = typeReq + reqMaj;

Quand vous cliquez sur un bouton, la requête contient typeReq mais pas reqMaj et donc le sketch exécute la modification de l’état de la LED. Le retour de l’information est traité par la fonction actualisation() qui, grâce à la ligne qu’on vient d’ajouter, en profite pour modifier la requête en ajoutant reqMaj à typeReq, ce qui fait qu’aux passages suivants, le code influant sur l’état de la LED est shunté et seule la mise à jour est effectuée.

Notre cheminement de problème en problème arrive à son terme. Il est temps de regrouper tout ce qu’on a vu en un ensemble cohérent et utilisable.

VI. Récapitulation

VI-A. Le code

Notre télécommande utilise, dans sa version actuelle, trois fichiers nécessaires :

  • test-ajax.ino : le sketch serveur, dont le travail consiste à répondre aux requêtes du client, qui sont :

    • demande de fichiers,
    • exécution des commandes,
    • retour d’informations ;
  • text-ajax.html : la page web qui s’occupe de l’interface de la télécommande, en ce qui concerne le contenu ;
  • text.ajax.js : le code client destiné à générer les requêtes, automatiques ou manuelles, et à gérer leur envoi ainsi que le retour d’informations par l’intermédiaire d’AJAX

et un fichier (très) secondaire : icône.png, dont l’unique utilité est d’orner l’onglet de la page affichant votre télécommande dans le navigateur.

Nous allons ajouter un fichier non nécessaire, mais bien utile pour améliorer, sinon l’esthétique, du moins la lisibilité sur smartphone, et dans une moindre mesure sur tablette : test‑ajax.css. Je vous en laisse examiner le contenu et modifier les réglages à votre convenance : c’est la meilleure façon d’en comprendre le fonctionnement. Pour vous faciliter la tâche, toutes les propriétés utilisées sont commentées. Ce fichier n’offre qu’un très faible aperçu de ce que l’on peut réaliser en CSS, cette remarque étant d’ailleurs également d’actualité pour les autres fichiers dans leur domaine respectif.

L’utilisation de ce fichier va entraîner deux ajouts dans l’entête du fichier HTML, l’un relatif à la requête concernant ce fichier, l’autre permettant de récupérer la largeur de l’affichage dont on a besoin dans le code CSS (viewport). De plus, la classe du paragraphe affichant la température a été modifiée. Le sketch serveur nécessite également l’ajout de quelques lignes, destinées à l’envoi de ce fichier. Ces mécanismes ont été détaillés plus haut et je n’y reviens donc pas.

Enfin, comme nous ne sommes plus en phase de test, j’ai renommé les fichiers « telecommande ». Désolé pour l’orthographe, mais les lettres accentuées ne sont pas appréciées par le HTML. J’ai également modifié quelques libellés, tant dans le fichier HTML que dans le fichier JavaScript, pour que l’affichage soit cohérent avec le scénario de départ, à savoir l’ouverture d’une trappe dans une serre en fonction de la température ambiante.

Voici donc le fruit de notre travail dans son intégralité :

telecommande.ino
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
#include <SdFat.h>
#include <Ethernet.h>

SdFat SD;

const int carteSD = 4;
const int led = 2;
const byte LM35 = 0;

byte macSerre[] = {0x90, 0xA2, 0xDA, 0x10, 0x4F, 0x25};
IPAddress IPSerre(192, 168, 1, 200);
EthernetServer serveurHTTP(80);

void setup()
{
  SD.begin(carteSD);
  Ethernet.begin(macSerre, IPSerre);
  serveurHTTP.begin();
  pinMode(led, OUTPUT);
}

void loop()
{
  EthernetClient client = serveurHTTP.available();
  if (client)
  {
    if (client.connected())
    {
      String reception;
      while (client.available())
      {
        char carLu = client.read();
        if (carLu != 10)
        {
          reception += carLu;
        }
        else
        {
          break;
        }
      }
      if (reception.indexOf("ajax?") != -1) // Si c’est une requête AJAX
      {
        if (reception.indexOf("maj?") == -1) // Si ce n’est pas une mise à jour
        {
          reception.replace("GET /ajax?", "");
          reception.replace("HTTP/1.1", "");
          reception.trim();
          if (reception == "ON")
          {
            digitalWrite(led, HIGH);
          }
          else if (reception == "OFF")
          {
            digitalWrite(led, LOW);
          }
        }
        arHtml(client, "application/json"); // application/json
        maj(client);
      }
      else
      {
        if (reception.startsWith("GET / HTTP/1.1"))
        {
          arHtml(client, "text/html");
          envoiFichier(client, "telecommande.html");
        }
        else if (reception.startsWith("GET /telecommande.js HTTP/1.1"))
        {
          arHtml(client, "application/javascript");
          envoiFichier(client, "telecommande.js");
        }
        else if (reception.startsWith("GET /telecommande.css HTTP/1.1"))
        {
          arHtml(client, "text/css");
          envoiFichier(client, "telecommande.css");
        }
        else if (reception.startsWith("GET /telecommande.png HTTP/1.1"))
        {
          arHtml(client, "image/png");
          envoiFichier(client, "telecommande.png");
        }
        delay(1);
      }
      client.stop();
    }
  }
}

// FONCTIONS

void arHtml(EthernetClient nomClient, String type)
{
  nomClient.println(F("HTTP/1.1 200 OK"));
  nomClient.println("Content-Type: " + type);
  nomClient.println(F("Connection: close"));
  nomClient.println();
}

void envoiFichier(EthernetClient nomClient, String fichierEnCours)
{
  char tableau[fichierEnCours.length() + 1];
  fichierEnCours.toCharArray(tableau, fichierEnCours.length() + 1);
  if (SD.exists(tableau))
  {
    File fichier = SD.open(fichierEnCours, FILE_READ);
    while (fichier.available())
    {
      nomClient.write(fichier.read());
    }
    fichier.close();
  }
}

void maj(EthernetClient nomClient)
{
  String reponseJSON = "{";
  reponseJSON.concat("\"bouton\":");
  if (digitalRead(led) == HIGH) reponseJSON.concat("\"ledOn\","); 
  else reponseJSON.concat("\"ledOff\",");
  reponseJSON.concat("\"temperature\":");
  reponseJSON.concat(temperature(LM35, 100));
  reponseJSON.concat("}");
  nomClient.println(reponseJSON);
}

float temperature(byte broche, word iteration)
{
  long lecture = 0;
  for (word i = 0; i < iteration; i++)
  {
    lecture += analogRead(broche);
  }   
  return(float(lecture) *500 /1024 /iteration);
}
telecommande.html
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
<!DOCTYPE html>

<html lang="fr">

  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1">    
    <script src="telecommande.js"></script>
    <link rel="stylesheet" href="telecommande.css">
    <link rel="icon" type="image/png" href="telecommande.png"> 
    <title>Télécommande</title>
  </head>
  
  <body onload="maj()">
    <p class="titre">Température de la serre</p>
    <p class="temp bleu" id="temperature"></p>
    <p class="titre">Ouverture de la trappe</p>
    <button type="button" class="bouton" id="bouton" value="ON" onClick="onClickBouton()">Ouvrir</button>
    <p id="infoLed" class="retour"></p>
  </body>
    
</html>
telecommande.js
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
const typeReq = "ajax?";
const reqMaj = "maj?";
var requete = "";

function onClickBouton()
{
  requete = typeReq + document.getElementById("bouton").getAttribute("value");
  requeteAjax();
}

function maj()
{
  requete = typeReq + reqMaj;
  setInterval(requeteAjax, 1000);
}
  
function requeteAjax()
{
  var xhr = new XMLHttpRequest();
  xhr.open("GET", requete , true);
  xhr.send(null);  
    xhr.onreadystatechange = function()
    {
        if (xhr.readyState == 4)
        {
            actualisation(xhr.responseText);
        }
    }
}

function actualisation(reponse)
{
    var retour = JSON.parse(reponse);  
  if (retour.bouton == "ledOn")
  {
    document.getElementById("bouton").setAttribute("value","OFF");
    document.getElementById("bouton").innerHTML = "Fermer";
    document.getElementById("infoLed").innerHTML = "Trappe ouverte";
    document.getElementById("infoLed").style.color = "rgb(0,200,0)";
  }
  else if (retour.bouton == "ledOff")
  {
    document.getElementById("bouton").setAttribute("value","ON");
    document.getElementById("bouton").innerHTML = "Ouvrir";
    document.getElementById("infoLed").innerHTML = "Trappe fermée";
    document.getElementById("infoLed").style.color = "rgb(100,100,100)";
  } 
  document.getElementById("temperature").innerHTML = retour.temperature.toFixed(2).concat(" °C");
  requete = typeReq + reqMaj;  
}
telecommande.css
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
/* Seule la première occurrence d'une propriété sera commentée */

/* Paramétrages indépendants de la taille de l'écran */

.retour /* Couleur de fond de l'indicateur d'état de la LED */
{
  background-color: rgb(200,255,200); /* Couleur de fond */
}

.bleu /* Couleur de la police affichant la température */
{
  color: rgb(0,0,255);                /* Couleur de la police */
}



button   /* Paramétrage commun à tous les boutons */
{
  text-align: center;                 /* Alignement horizontal du libellé du bouton */
  cursor: pointer;                    /* Curseur main avec index tendu */
}

.bouton  /* Paramétrage commun à toutes les classes "bouton" */
{
  background-color: white;             
  color: black;                
  border: 2px solid rgb(80,180,80);   /* Bordure : épaisseur, tracé continu, couleur vert moyen */
  border-radius: 20px;                /* Rayon de l'arrondi des angles */
}

/* Paramétrages dépendant de la taille de l'écran */

/*PC*/
@media screen and (min-width: 1000px) /* Écrans de plus de 1000 pixels de large */
{ 
  .bouton
  {
    font-size: 16px;                  /* Taille de la fonte en pixels */
    padding: 8px 16px;                /* Marge intérieure : haut et bas, gauche et droite */
    margin-left: 10px;                /* Marge extérieure gauche en pixels */
    transition-duration: 0.5s;        /* Modification progressive des paramètres modifiés par le survol */ 
  }

  .bouton:hover  /* Modification de l'apparence du bouton survolé par la souris */ 
  {
    background-color: rgb(80,180,80); 
    color: white;
  }
  
  .temp
  {
    font-size: 2em;                   /* 1em est la taille par défaut de la fonte. Le coefficient est multiplicateur */
  }
  
  .retour
  {
    font-size: 2em;
  }
}

/* Tablettes /
@media screen and (max-width: 1000px) /* Écrans de moins de 1000 pixels de large */
{ 
  button
  {
    font-size: 6vw;                   /* Taille de la fonte en % de la largeur de la fenêtre (viewport) */
    width: 40%;                       /* Largeur en % par rapport au conteneur */
    margin-left: 5%;                  /* Marge extérieure en % par rapport au conteneur */
    padding: 1% 2%;                   /* Marge intérieure en % par rapport au conteneur */
  }

  .titre
  {
    font-size: 4vw;
  }

  .temp
  {
    font-size: 6vw;
    font-weight: bold;                /* Épaisseur du tracé des caractères */
  }

  .retour
  {
    font-size: 6vw;
  }
}

/* Smartphones /
@media screen and (max-width: 600px)  /* Écrans de moins de 600 pixels de large */  
{
  button
  {
    font-size: 8vw;
    width: 90%;
    margin-left: 5%;
    padding: 1% 2%;
  }

  .titre
  {
    font-size: 5vw;
  }
  
  .temp
  {
    font-size: 7vw;
    font-weight: bold;
  }

  .retour
  {
    font-size: 7vw;
  }
}

Voici en image ce que donne l’affichage de votre télécommande dans trois dimensions d’écran différentes correspondant respectivement à un smartphone, une tablette et un écran PC :

Image non disponible
Image non disponible
Image non disponible

Vous obtiendrez ces différents affichages en faisant varier la taille de la fenêtre de votre navigateur pour simuler des écrans de différentes tailles. Ce processus est géré dans le fichier CSS grâce aux MediaQueries qui se basent sur la largeur de l’affichage courant fourni par viewport dans le fichier HTML.

À l’évidence, au niveau esthétique, on peut faire beaucoup mieux, mais ce n’est pas l’objet de ce tutoriel. À vous de jouer !

VI-B. Aperçu des transactions HTTP

Voici une représentation simplifiée de la façon dont se déroule une session :

Transactions

Action utilisateur

Source

Destination

Taille

Requête/Retour

1

Connexion

192.168.1.25

192.168.1.200

387

GET / HTTP/1.1

2

 

192.168.1.200

192.168.1.25

60

Paquet fichier HTML

3..18

 

192.168.1.200

192.168.1.25

60

Paquets fichier HTML

19

 

192.168.1.200

192.168.1.25

60

Paquet fichier HTML

20

 

192.168.1.25

192.168.1.200

344

GET /telecommande.js HTTP/1.1

21

 

192.168.1.200

192.168.1.25

60

Paquet fichier JS

22..88

 

192.168.1.200

192.168.1.25

60

Paquets fichier JS

89

 

192.168.1.200

192.168.1.25

60

Paquet fichier JS

90

 

192.168.1.25

192.168.1.200

360

GET /telecommande.css /HTTP/1.1

91

 

192.168.1.200

192.168.1.25

60

Paquet fichier CSS

92..206

 

192.168.1.200

192.168.1.25

60

Paquets fichier CSS

207

 

192.168.1.200

192.168.1.25

60

Paquet fichier CSS

208

 

192.168.1.25

192.168.1.200

324

GET /telecommande.png /HTTP/1.1

209..216

 

192.168.1.25

192.168.1.200

324

GET /telecommande.png /HTTP/1.1

217

 

192.168.1.25

192.168.1.200

324

GET /telecommande.png /HTTP/1.1

218

 

192.168.1.25

192.168.1.200

338

GET /ajax?maj?/HTTP/1.1

219

 

192.168.1.200

192.168.1.25

60

Paquet JSON

220

 

192.168.1.25

192.168.1.200

338

GET /ajax?maj?/HTTP/1.1

221

 

192.168.1.200

192.168.1.25

60

Paquet JSON

222

 

192.168.1.25

192.168.1.200

338

GET /ajax?maj?/HTTP/1.1

223

 

192.168.1.200

192.168.1.25

60

Paquet JSON

224

Clic sur le bouton On

192.168.1.25

192.168.1.200

336

GET /ajax?On/HTTP/1.1

225

 

192.168.1.200

192.168.1.25

60

Paquet JSON

226

 

192.168.1.25

192.168.1.200

338

GET /ajax?maj?/HTTP/1.1

227

 

192.168.1.200

192.168.1.25

 

Paquet JSON

Repères :

  • la colonne « Transactions » représente l’ordre chronologique dans lequel s’effectuent les échanges entre le client (ici 192.168.1.25 étant l’adresse hypothétique du PC sur le réseau local) et le serveur (ici 192.168.1.200 qui est l’adresse attribuée au serveur sur le même réseau pour ce tutoriel), depuis la connexion jusqu’à la fin de la session (non représentée sur le tableau). La zone orange concerne les transactions liées à la connexion, à savoir le téléchargement des différents fichiers : ces transactions n’ont lieu qu’une fois par session. La zone bleue correspond aux échanges qui auront lieu pendant tout le reste de la session (limitée à quelques échanges pour l’exemple, évidemment) ;
  • la colonne « Taille » affiche la taille en octets des paquets ;
  • les lignes vertes correspondent aux requêtes, et sont donc émises par le client vers le serveur. Elles concernent :

    • pour la zone orange, l’envoi des fichiers nécessaires à la session : mis à part le fichier HTML qui est automatiquement demandé lors de la connexion, les autres fichiers sont demandés au fur et à mesure de l’interprétation du fichier HTML par le client, dans l’ordre dans lequel ils apparaissent dans ce fichier. Ce processus est géré par le client et ne nécessite aucune intervention de notre part,
    • pour la zone bleue, les demandes de mise à jour, automatiques, ou les commandes, nécessitant une action de la part de l’utilisateur : cette partie-ci est gérée par notre code JavaScript ;
  • les lignes roses correspondent à l’envoi par le serveur au client des données demandées par celui-ci. Elles concernent :

    • pour la zone orange, les fichiers demandés scindés sous la forme de « paquets » de soixante octets, chaque paquet contenant un entête et une fraction du fichier demandé. La bibliothèque Ethernet, se charge de gérer la découpe du fichier et la constitution du paquet. Le client, quant à lui, se charge de la reconstruction. Le rôle du sketch consiste ici à fournir à la bibliothèque Ethernet le fichier réclamé par le client, sous la forme d’un flux de caractères,
    • pour la zone bleue, la chaîne JSON contenant les éléments permettant la mise à jour de l’affichage de la télécommande. Cette chaîne est construite dans notre sketch puis fournie à la bibliothèque Ethernet en tant que chaîne de caractères ;
  • les lignes grisées représentent les envois successifs des paquets concernant une même requête. Le contenu des paquets est différent, mais la ligne est la même.

Le protocole utilisé ici est HTTP version 1.1, lequel s’appuie sur le protocole TCP. Chaque paquet de soixante octets contient, outre son formatage, un nombre variable d’octets de données utiles, voire aucune. Leur nombre n’est donc pas forcément représentatif de la taille du fichier envoyé. Si une requête est émise par le client avant que la requête précédente ait été traitée entièrement, les paquets continueront à être envoyés dans l’ordre chronologique des demandes.

Les chaînes JSON retournées par notre application lors des requêtes ajax? sont de la forme {"bouton":"ledOff","temperature":20.70} et tiennent dans un seul paquet : Paquet JSON.

Nous sommes ici dans une situation exemple où l’icône est inaccessible, et donc, à défaut d’accusé de réception, la requête la concernant est envoyée dix fois. Comme à ce moment toutes les autres données demandées ont été reçues, les requêtes ajax? prennent le relais.

Ce tableau est réalisé d’après les données fournies par l’analyseur de protocole réseau Wireshark et est représentatif du début d’une session standard de notre télécommande.

Pour aller plus loin :

VII. Optimisations

Les codes fournis ci-dessus fonctionnent parfaitement et ont le mérite d’être simples. Ils ne sont toutefois pas vraiment représentatifs d’une application réelle, justement en raison de cette simplicité. Une application réelle fera appel à plusieurs capteurs dont il faudra traiter les données et commandera probablement plusieurs actuateurs : en bref, le train-train d’un microcontrôleur. Si la partie « client », à savoir les fichiers HTML, JavaScript et CSS, ne pose pas de problème d’extension, son codage n’étant limité que par la capacité de la carte microSD, il n’en est pas de même de la partie « serveur », c’est-à-dire le sketch. Les mémoires statique (flash) et dynamique (RAM) relativement modestes de la carte Arduino UNO peuvent limiter assez rapidement l’étendue des possibilités.

Pour des raisons de simplicité, j’ai utilisé la classe « String » qui fournit des méthodes pratiques pour manipuler les chaînes de caractères, mais qui possède l’inconvénient d’avoir un impact mémoire non négligeable. Afin d’économiser un peu de cette mémoire si précieuse (environ 2 kio), il peut être préférable d’utiliser des tableaux de caractères.

De plus, comme l’a fait très justement remarquer Jay M dans son commentaire de la seconde partie de ce tutoriel, il est préférable, dans les fonctions concernées, de passer le paramètre nomClient par référence. En effet, je le passe par valeur ce qui, entre autres choses, a l’inconvénient de créer une instance supplémentaire et inutile de l’objet EthernetClient, et donc également d’impacter mémoire. Dans la mesure où la fonction ne modifie pas la valeur de ce paramètre, passer celui-ci par référence ne pose aucun problème. L’impact mémoire étant ici également de l’ordre de 2 kio, cela nous fait globalement environ 4 kio d’économisés, ce qui n’est pas du tout négligeable.

Je vous propose donc, après l’avoir testée, la version suivante du sketch serveur, écrite par Jay M, qui tient compte de ces deux remarques et qui respecte mieux que je ne saurais le faire les standards du langage C :

telecommande.ino
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
//  *** LA GESTION DE LA CARTE SD ***
#include <SdFat.h>
SdFat carteSd;
const byte csPinSD = 4;
 
// *** LA GESTION ETHERNET ***
#include <Ethernet.h>
byte mac[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED};  // notre adresse MAC
IPAddress ip(192, 168, 1, 200); // notre adresse IP, à adapter à votre réseau local
EthernetServer serveurHTTP(80); // 80 est le port standard du protocole HTTP
 
const byte tailleMaxGETBuffer = 60; // taille max pour la commande GET reçue, à adapter aux besoins
 
//  *** LES PINS UTILISÉES ***
const byte ledPin = 2;    // une LED sur la broche numérique 2
const byte LM35Pin = A0;  // le capteur de température sur la broche analogique 0
 
// *** LES FONCTIONS UTILITAIRES ***
 
void arHtml(EthernetClient& nomClient, const __FlashStringHelper* type) // on passe le type en utilisant la macro F("...") pour économiser la mémoire SRAM
{
  nomClient.println(F("HTTP/1.1 200 OK"));
  nomClient.print(F("Content-Type: "));
  nomClient.println(type);
  nomClient.println(F("Connection: close"));
  nomClient.println();
}
 
void envoiFichier(EthernetClient& nomClient, const char* fichierEnCours)
{
  if (carteSd.exists(fichierEnCours))
  {
    File fichier = carteSd.open(fichierEnCours, FILE_READ);
    if (fichier) { // si l'accès s'est bien passé
      while (fichier.available()) nomClient.write(fichier.read()); // on envoie le contenu du fichier
      fichier.close();  // puis on ferme le fichier
    }
  }
}
 
float temperature(byte broche, word iteration)
{
  long lecture = 0;
  for (word i = 0; i < iteration; i++)
  {
    lecture += analogRead(broche);
  }
  return (float(lecture) * 500.0 / 1024.0 / iteration);
}
 
 
void maj(EthernetClient& nomClient)
{
  float temperatureMoyenne = temperature(LM35Pin, 10);  // on calcule la moyenne de la température sur quelques échantillons
 
  // on va renvoyer un JSON
  arHtml(nomClient, F("application/json"));
 
  // on construit et émet notre JSON
  nomClient.print(F("{\"bouton\":\""));
  if (digitalRead(ledPin) == HIGH)
    nomClient.print(F("ledOn"));
  else
    nomClient.print(F("ledOff"));
  nomClient.print(F("\",\"temperature\":"));
  nomClient.print(temperatureMoyenne, 2); // on émet la température avec 2 chiffres après la virgule
  nomClient.println(F("}"));
}
 
// *** LE PROGRAMME PRINCIPAL ***
 
void setup()
{
  pinMode(ledPin, OUTPUT);
  Serial.begin(115200);
  carteSd.begin(csPinSD);
  Ethernet.begin(mac, ip);
}
 
void loop()
{
  EthernetClient client = serveurHTTP.available();
  if (client)
  {
    if (client.connected())
    {
      char reception[tailleMaxGETBuffer + 1]; // +1 car une chaîne bien formée se termine par un '\0' invisible
 
      byte nbCar = 0;
      while (client.available())       // tant que des données sont disponibles
      {
        char carLu = client.read();     // on lit le prochain octet
        if (carLu != '\n')              // si ce n'est pas la marque de fin de ligne
        {
          if (nbCar < tailleMaxGETBuffer) reception[nbCar++] = carLu; // et si on a la place alors on conserve le caractère sinon on l'ignore
        }
        else break;                     // si on a reçu la fin de ligne on termine
      }
      reception[nbCar] = '\0';          // on met la marque de fin de notre chaîne correctement
 
      // on analyse maintenant ce que l'on a reçu en cherchant des mots clés en utilisant strstr() (cf http://www.cplusplus.com/reference/cstring/strstr/)
 
      // est-ce une demande de type ajax ?
      char *ajaxPtr = strstr(reception, "ajax?");
      if (ajaxPtr != NULL)
      {
        if (strstr(ajaxPtr, "ON") != NULL)  digitalWrite(ledPin, HIGH);       // si la requête demande d'allumer, on le fait
        else if (strstr(ajaxPtr, "OFF") != NULL) digitalWrite(ledPin, LOW);   // si la requête demande l'extinction, on le fait
        // puis on envoie une réponse JSON avec mise à jour
        maj(client);
      }
 
      // est-ce une demande pour telecommande.css
      else if (strstr(reception, "telecommande.css") != NULL)
      {
        arHtml(client, F("text/css"));
        envoiFichier(client, "telecommande.css");
      }
 
      // est-ce une demande pour telecommande.js
      else if (strstr(reception, "telecommande.js") != NULL)
      {
        arHtml(client, F("application/javascript"));
        envoiFichier(client, "telecommande.js");
      }
 
      // est-ce une demande pour icone.png
      else if (strstr(reception, "icone.png") != NULL)
      {
        arHtml(client, F("image/png"));
        envoiFichier(client, "icone.png");
      }
 
      // sinon on envoie la page standard
      else
      {
        arHtml(client, F("text/html"));
        envoiFichier(client, "telecommande.html");
      }
 
      delay(1);
    }
    client.stop();  // on se déconnecte du serveur. le buffer non lu est conservé jusqu'à disparition de l'instance client, ce qui se fait en fin de loop
  }

VIII. Conclusion

Votre télécommande est à présent techniquement fonctionnelle, et même si son esthétique laisse à désirer, j’espère qu’elle pourra vous servir de base de départ pour vos réalisations personnelles.

Malgré l’optimisation du code, en fonction de ce que vous envisagez de réaliser, il va vous falloir tenir compte d’un éventuel manque de place. J’ai supprimé dans le fichier telecommande.ino les lignes relatives à l’affichage des messages sur le moniteur série, ainsi que celles relatives au message d’erreur sur le navigateur. En utilisation standard « in situ », ces fonctionnalités ne servent à rien et prennent de la place inutilement. Avec le code actuel, il vous reste environ 10 kio de mémoire flash et 700 octets de mémoire dynamique.

En ce qui concerne la mémoire dynamique, vous pouvez opter pour des chaînes de caractères plus courtes que celles utilisées dans ce tutoriel. Par exemple, "bouton" peut être remplacé par "b1", ce qui donnera "b2" pour le suivant et ainsi de suite. Les noms des fichiers comme "telecommande.html" seront avantageusement remplacés par "t.html" par exemple. L’important à ce niveau n’est plus tant l’intelligibilité des noms utilisés que l’espace économisé. De la même manière, la réservation mémoire pour le tableau reponseJSON[127] peut être revue à la baisse en fonction de la taille maximum que peut atteindre votre chaîne JSON.

Pour la mémoire statique (flash), il n’y aura pas grand-chose à faire, le compilateur s’occupant d’optimiser au mieux le code. On peut mettre tous les commentaires que l’on veut dans le code source, ils ne prennent aucune place dans le code compilé.

Mais avant d’en arriver là, il vous reste encore de quoi travailler sans prendre de précaution particulière, à part de temps en temps un petit clic sur le bouton « Vérifier » de votre EDI pour surveiller votre bilan mémoire.

Si vous le désirez, vous pouvez télécharger cette archive. Elle contient tous les fichiers nécessaires ainsi qu’un bref mode d’emploi.

Merci de l’intérêt que vous avez porté à ce tutoriel. Que ce soit par simple curiosité ou dans le but de l’utiliser pour une réalisation concrète, je souhaite que le temps que vous y avez consacré ne sera pas inutile.

IX. Remerciements

Un grand merci à f-leb pour sa relecture technique.

Mes remerciements vont également à Claude Leloup pour sa relecture orthographique.