Les Bases d'un Serveur Web sur ESP-01 en commande AT

EDIT: SI VOUS CHERCHEZ LE CODE IL EST DANS LE POST #10

Un petit tuto d'explication sur l'écriture d'un mini serveur web en utilisant un ESP01 connecté en mode série sur un Arduino et en utilisant le langage de commandes AT

L'objet de ce post n'est pas de documenter l'ESP-01 et comment s'assurer d'avoir le bon firmware dessus. Ceci est traité ailleurs sur le web, je vous laisse explorer :slight_smile:

(perso je suis sur Mac et donc n'ai pas accès aux nombreux logiciels sous Windows permettant de flasher le module genre ESP8266 Flash Downloader ou encore Esp Flash Download Tool mais même si j'étais sous PC je n'installerai pas un binaire venant de n'importe où, trop de risque de virus... donc j'utilise un script python esptool.py et la ligne de commande - ça fait le job très bien, par exemple des infos ici - ÉDIT Nouveau lien car l’ancien ne fonctionne plus - merci Henri)

ESP-01.png

Dans ce test, pour me simplifier la vie, j'utilise un adaptateur de tension avec les bons pull-up sur les GPIO de manière à pouvoir jouer rapidement

ESP-adapter.png

Ça s'assemble très simplement sur une breadboard

ESP-Breadboard.png

De plus afin de voir ce qu'il se passe avec affichage sur la console, je vais avoir besoin de deux ports séries. Un pour la console de mon Mac et un pour parler avec l'ESP-01. On peut bien entendu utiliser une émulation de port série par la librairie SoftwareSerial qui parfois présente quelques soucis de stabilité, surtout si vous communiquez à 115200 bauds et envoyez ou recevez beaucoup d'infos.

Comme l'objet de ce post n'est pas de debugger la liaison SoftwareSerial, je vais me simplifier la vie en prenant un Arduino MEGA et en utilisant un port Série matériel.

Enfin, l'ESP-01 peut consommer pas mal en émission et réception, jusqu'aux environ de 300mA et donc au delà de ce que votre Arduino peut fournir simplement (et certainement pas sur la pin 3.3V si vous n'avez pas pris l'adaptateur 5V). Pour ne pas avoir à gérer cette partie là j'installe une alimentation séparée sur ma breadboard qui aura pour but uniquement d'alimenter l'ESP-01.

Le câblage pour nos test sera donc le suivant

ESP-01 monté sur Adaptateur, branché à cheval sur le milieu de la breadboard
Alimentation 5V connectée sur les rails des bords de la breadboard (rail rouge = 5V, rail bleu = GND)

Pin - (masse) de l'adaptateur ESP-01 connectée au rail bleu de la breadboard (GND) (fil noir)
Pin - (masse) de l'arduino MEGA connectée au rail bleu de la breadboard (GND) (fil noir) car il faut joindre les masses bien sûr !

Pin + (alimentation) de l'adaptateur ESP-01 connectée au rail rouge de la breadboard (5V) (fil rouge)
Pin Enable (EN) de l'adaptateur ESP-01 connectée à 5V de la breadboard (fil rouge)

Pin RESET (RST) de l'adaptateur ESP-01 connectée à la pin 7 de l'arduino Mega (fil jaune)
Pin Rx (réception) de l'adaptateur ESP-01 connectée à la pin Tx1 (18) de l'arduino Mega (fil blanc)
Pin Tx (émission) de l'adaptateur ESP-01 connectée à la pin Rx1 (19) de l'arduino Mega (fil bleu)

les 2 GPIO de l'adaptateur ESP-01 restent "en l'air".

Partie 1: Jouer avec les commandes AT

Votre ESP-01 est donc configuré par défaut avec un firmware qui embarque un langage de commandes interactif que l'on utilise sur la voie série. Ce langage de commande est spécifique à votre module et est connu sous le nom de Commandes Hayes ou commandes AT.

Les Commandes Hayes, parfois appelées Commandes AT, constituent un langage de commandes développé à l'origine pour le modem Hayes Smartmodem 300. Ce jeu de commandes s'est ensuite retrouvé dans quasiment tous les modems produits
(source = wikipedia)

Chaque fabricant, en fonction de ses besoins, s'est permis d'ajouter des extensions, des commandes spécifiques etc...

Pour les ESP la société Espressif Systems documente son jeu de commandes AT qui est dans son firmware ESP8266_NONOS_SDK que vous pouvez flasher sur votre carte.

Attention ce que vous trouverez dans ce document correspond à l'état de l'art du jeu de commande et suivant la version de votre firmware et les capacités de votre ESP, vous n'aurez peut-être toutes le commandes disponibles et éventuellement il faudra trouver une vieille version de cette doc.

Certaines commandes sont maintenant considérées comme obsolètes (on dit dépréciées, deprecated en anglais) comme par exemple AT+CWJAP pour se connecter à un point d'accès wifi est maintenant remplacé par AT+CWJAP_CUR si on ne veut pas sauver les paramètres en mémoire flash ou AT+CWJAP_DEF si on veut sauvegarder la config en flash pour le prochain reboot mais la majorité des ESP-01 que j'ai vu trainer ont peu de mémoire et on ne peut pas installer les derniers firmware et donc on n'a pas accès à ce jeu d'instruction "moderne"....

Note: il faut donc s'attendre à ce que votre ESP-01 ne réponde pas exactement comme le mien suivant votre firmware.

Mise en place du code

Pour jouer avec les commandes AT, il faut pouvoir envoyer ces commandes au module et lire la réponse (et bien sûr potasser toute la doc des commandes possibles).

Sur la base du montage décrit dans le post précédent, voici un petit code à charger pour permettre de taper directement dans la console série (réglée à 115200 bauds et envoyant New Line et Carriage Return (NL et CR))

le code est très simple, je l'ai commenté et il devrait se lire sans soucis:

// Controle du reset de la carte ESP-01 
const byte hardResetPIN = 7;

void setup() {
  Serial.begin(115200);
  Serial1.begin(115200); // suivant votre config, essayez 9600 19200 38400 57600 74880 115200
  pinMode(hardResetPIN, OUTPUT); // merci icare - j’avais oublié cette ligne 

  // on reset notre esp-01 (RST pin à ground pour 100ms)
  digitalWrite(hardResetPIN, LOW);
  delay(100);
  digitalWrite(hardResetPIN, HIGH);
  
  // on attend un peu le reboot
  delay(500);
}

void loop()
{
  int r;

  // on lit ce que l'utilisateur tape sur la console série et on l'envoie à l'ESP
  while (Serial.available()) {
    r = Serial.read();
    if (r != -1) Serial1.write((char) r);
  }
  
  // on lit ce que l'ESP nous dit et on l'affiche pour l'utilisateur sur la console série
  while (Serial1.available()) {
    r = Serial1.read();
    if (r != -1) Serial.write((char) r);
  }
  
}

Une fois que vous avez chargé ce code on va vérifier que tout fonctionne avant d'aller plus loin. Pour ce faire le plus simple est d'envoyer la commande

AT

à notre ESP qui doit nous répondre

OK

à noter leSerial1.begin([color=red]115200[/color]);. ici il est important d'utiliser le bon débit, c'est celui qui est configuré sur votre ESP-01. si ce qui suit ne fonctionne pas, essayez avec des valeurs classiques 9600 19200 38400 57600 74880 115200

Tant que vous ne voyez pas le [color=purple]OK[/color], pas la peine de continuer, c'est que vous n'arrivez pas à parler correctement à votre module - assurez vous qu'il soit bien configuré avec une version firmware de commande AT, et bien sûr que l'alimentation et les broches soient bien connectées.

Une fois que ça fonctionne, la seconde commande que l'on peut envoyer c'est pour connaître les informations de versions de votre carte. Pour cela on envoie

[color=blue]AT+GMR[/color]

Voici ce que me répond le mien à cette commande:

[color=purple]AT+GMR

AT version:0.60.0.0(Feb  2 2016 18:43:31)
SDK version:1.5.2(80914727)
compile time:Feb  2 2016 19:00:35
OK
[/color]

j'ai donc quelques trains de retards vu que ma version du langage AT est la 0.60.0.0 et date de 2016... mais bon ça fera le job. (si ça vous tente Vous avez la doc Espressif ici et le github pour le ESP8266_NONOS_SDK)

Attention il ne faut pas d'espace à la fin de la commande, assurez vous de tapez juste le texte

Partie 2: configurer son ESP-01 sur réseau Wi-Fi

pour que l'ESP-01 soit utilisable en réseau Wi-Fi il faut ... du réseau.

Votre ESP dispose de 3 modes possibles pour le réseau:

  • 1 : station mode
  • 2 : softAP mode
  • 3 : softAP + station mode

le mode station, ça veut dire que votre ESP rejoint votre réseau Wi-Fi
le mode softAP, ça veut dire que votre ESP crée un point d'accès Wi-Fi
le dernier mode fait les 2 à la fois.

Dans notre exemple on va se concentrer sur le premier mode: on va rejoindre le réseau Wi-Fi de la maison ce qui établit un pont sans fil entre votre ESP-01 et votre réseau local, et si votre réseau local à connecté à internet alors votre ESP aura aussi accès à internet.

Il serait trop long et hors sujet de parler de réseau ici. Je simplifierai donc à l'extrême en disant que lorsque vous rejoignez un réseau TCP-IP vous devez avoir une adresse IP.

Cette adresse peut-être statique (vous la définissez) ou dynamique (vous l'obtenez par DHCP depuis votre routeur).

Si vous voulez avoir une adresse IP statique, jetez un oeil sur

AT+CIPSTA

– définir l'adresse IP en mode station [dépréciée]

AT+CIPSTA_CUR

– définir l'adresse IP en mode station

AT+CIPSTA_DEF

– définir l'adresse IP en mode station et sauvegarde en mémoire flash

Dans notre cas on va rester simple, la majorité des routeurs à la maison supportent DHCP et on va donc en profiter.

Voici une liste de commande à envoyer par le petit programme précédent

On remet tout à Zéro

[color=blue]AT+RESTORE[/color]

On vérifie que tout va bien, ça doit répondre OK

AT

On passe en mode station, client du réseau Wi-Fi local

[color=blue]AT+CWMODE=1[/color]

On établit la connection au réseau en DHCP

[color=blue]AT+CWJAP="[color=green]Nom du Réseau[/color]","[color=green]Mot de Passe du Réseau[/color]"[/color]

Dans cette dernière commande bien sûr il faut mettre entre les guillemets le SSID (le nom de votre réseau Wi-Fi) et votre mot de passe. Si votre nom réseau ou mot de passe a des caractère bizarres genre une virgule, un anti-slash () ou un slash (/) alors il faut mettre un slash (/) devant. par exemple si votre nom réseau c'est Ma/Maison et le mot de passe 123,ABC alors il faudra envoyer la commande

[color=blue]AT+CWJAP="[color=green]Ma[color=red]/[/color]/Maison[/color]","[color=green]123[color=red]/[/color],ABC[/color]"[/color]

Voici ce que ça donne sur mon modèle:

[color=purple]
[color=blue]AT+RESTORE[/color]


OK
WIFI DISCONNECT

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

load 0x40100000, len 25968, room 16 
tail 0
chksum 0x2a
load 0x3ffe8000, len 2092, room 8 
tail 4
chksum 0x57
load 0x3ffe8830, len 9344, room 4 
tail 12
chksum 0xde
csum 0xde
don't use rtc mem data
sl⸮⸮sl⸮⸮⸮bl⸮cs|⸮bll⸮⸮⸮lc⸮⸮o⸮no⸮⸮l⸮|⸮l⸮l⸮⸮⸮⸮⸮⸮l`⸮n⸮�⸮⸮⸮c⸮⸮b⸮⸮⸮cl⸮b⸮lsl
ready
[color=blue]AT+CWMODE=1[/color]


OK
[color=blue]AT+CWJAP="[s]monSSID[/s]","[s]monSecret[/s]"[/color]

WIFI CONNECTED
WIFI GOT IP

OK
[/color]

et voilà je suis connecté au Wi-Fi de ma maison

si je veux connaître mon adresse IP obtenue via DHCP, il y a bien sûr une commande pour cela. Si j'envoie

[color=blue]AT+CIFSR[/color]

j'ai comme réponse

[color=purple]AT+CIFSR

+CIFSR:STAIP,"192.168.1.28"
+CIFSR:STAMAC,"18:fe:34:e6:27:8f"

OK
[/color]

et donc mon adresse est 192.168.1.28 - je vais l'utiliser dans les posts suivant pour accéder mon ESP-01. La votre sera sans nul doute différente, il faudra donc utiliser la votre dans le navigateur web pour les tests.

Partie 3: Configurer son ESP-01 en serveur

Nous en sommes donc au point où on va pouvoir vraiment commencer à jouer, l'ESP est sur le réseau de la maison.

Il faut maintenant dire à votre module d'accepter des connexion entrantes et donc de lancer un processus qui écoute des requêtes sur le réseau à son intention.

Pour ce faire il y a deux choses à configurer. d'une part dire à l'ESP s'il accepte les connexion multiple ou pas et une fois que c'est fait, demander à l'ESP-01 de créer un serveur TCP sur un port d'écoute particulier.

C'est ici que suivant la sécurité de votre réseau à la maison les choses peuvent se gâter. Si on veut discuter, il va falloir pouvoir envoyer, depuis un ordinateur connecté au réseau local, une requête TCP vers le port sur lequel l'ESP écoute. Mais si vous avez configuré votre routeur pour bloquer des ports ou des communications entre appareils en dehors des ports standards, alors ça ne fonctionnera pas, le routeur va intercepter la requête partant de votre ordinateur et ne la publiera pas vers le réseau et donc l'ESP ne verra rien passer...

Comme la majorité (tous?) des routeurs autorisent la communication sur le port utilisé par défaut en mode web (HTTP, port 80), mon exemple utilise le port 80 au lieu du port par défaut de l'ESP qui est le 333. Il y a 99% de chances que cela fonctionne aussi chez vous

Comme nous souhaitons répondre à plusieurs solicitations et jouer le rôle de serveur, notre ESP doit accepter plusieurs communications entrantes simultanées. Nous allons donc lui envoyer la commande suivante pour autoriser les connexions TCP multiples

[color=blue]AT+CIPMUX=1[/color]

l'ESP n'est pas bavard dans sa réponse

[color=purple]AT+CIPMUX=1


OK
[/color]

et ensuite nous allons créer un serveur TCP qui écoute sur le port 80 (créer un serveur n'est possible uniquement quand les connexions TCP multiples sont activées). On envoie donc la commande

[color=blue]AT+CIPSERVER=1,80[/color]

et on obtient la réponse tout aussi laconique

[color=purple]AT+CIPSERVER=1,80


OK
[/color]

mais n'en doutez pas, derrière ce petit [color=purple]OK[/color] sibyllin s'est cachée une débauche technologique qui nécessite beaucoup de puissance et implémentation de standards - votre ESP écoute maintenant sur le réseau ce qui peut lui être adressé !

Pour s'en convaincre on va initier une connexion vers notre ESP-01 depuis un navigateur web. Je lance donc Chrome sur mon ordinateur connecté sur ce même réseau local, et je tape dans la barre d'adresse

http://192.168.1.28[color=blue]:80[/color]

(le [color=blue]:80[/color] n'est pas absolument nécessaire car c'est le port web par défaut donc celui que le navigateur utilise si vous ne dites rien, c'est juste pour vous monter où mettre l'info si vous aviez choisi un autre port)

et lorsque je valide, si je regarde dans la console Arduino je vois qu'il se passe des choses ! mon ESP-01 a détecté une requête entrante et reçu tout un tas d'information sur cette requête. Voici ce que je vois dans la console série:

[color=purple]
0,CONNECT

[color=red][b]+IPD,0,408:[/b][/color]GET / HTTP/1.1
Host: 192.168.1.28
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: fr-FR,fr;q=0.8,en;q=0.6,en-US;q=0.4
[/color]

le [color=red][b]+IPD,0,408:[/b][/color] est très important. [color=purple]+IPD[/color] est envoyé par votre ESP lorsque vous recevez une requête réseau - cf la documentation

Ici nous sommes en mode "connexions TCP multiples" et donc on voit que après le [color=purple]+IPD,[/color] on va avoir un paramètre important qui est le numéro de connexion (ici 0) et le nombre d'octets reçus dans la requête (ici 408) - puis il y a des infos optionnelles et enfin après le ':' les données brutes émises par Chrome pour fabriquer la requête; On voit une requête HTTP GET bien formée, avec tout plein d'information comme le nom du navigateur, l'adresse IP appelante, les compétences du navigateur etc...

si on ne fait rien au bout d'un moment Chrome s'ennuie et considère que le site web ne fonctionne pas et affichera

Donc il faut répondre si on veut voir quelque chose s'afficher.

Pour bâtir cette réponse, on se fiche d'à peu près tout ce que l'on a reçu dans cette requête sauf de 2 choses:

1/ le fait que l'on ait reçu une requête (donc réception de [color=purple]+IPD,[/color])
2/ le numéro de la connexion.

(si vous êtes tatillon, vous pouvez vérifier que la requête est de type [color=purple]HTTP GET[/color] et éventuellement valider que l'adresse IP appelante est un demandeur autorisé par exemple si vous voulez limiter qui a le droit de vous appeler).

Partie 4: répondre à une requête

On a donc une requête de type HTTP GET qui vient d'arriver sur notre ESP-01, il nous faut répondre sans tarder sinon la connexion expire.

si vous êtes un peu lent, vous pouvez configurer le time out avec la commande [color=blue]AT+CIPSTO[/color] qui est bien sûr documentée


Vous passez un temps en secondes, jusqu'à 7200 secondes (deux heures !).

la valeur par défaut de votre firmware est interrogeable par la commande [color=blue]AT+CIPSTO?[/color] qui me répond

[color=purple]AT+CIPSTO?

+CIPSTO:180

OK
[/color]

je vois donc que j'ai 180 secondes (soit 3 minutes) pour envoyer une réponse par défaut.

Si vous mettez 0 il n'y aura pas de timeout, ce n'est pas recommandé, le mieux c'est de ne pas y toucher, 3 minutes devraient suffire au commun des mortels.

La question qui se pose donc maintenant c'est comment répondre et que répondre ?

On a eu une requête en HTTP qui est un protocole de la couche application. Il peut fonctionner sur n'importe quelle connexion fiable, dans les faits on utilise le protocole TCP comme couche de transport. Dans le protocole HTTP, une méthode est une commande qui spécifie un type d'action que le serveur doit effectuer. En général l'action concerne une ressource identifiée par une URL qui suit le nom de la méthode. Vous vous souvenez de la requête que nous a envoyé notre ESP-01 listée plus haut:

[color=purple][color=red]+IPD,0,408:[/color][color=green]GET[/color] [color=blue]/[/color] HTTP/1.1[/color]

La méthode est GET et la resource demandée est / (c'est à dire la racine du site web, la page de garde). Il existe de nombreuses méthodes, les plus courantes étant GET, HEAD et POST

Pour se convaincre que tout cela fonctionne, entrez dans Chrome l'URL suivante

http://192.168.1.28:80[color=blue]/arduino[/color]

si vous regardez maintenant dans la console série vous voyez que l'ESP-01 a reçu une requête

[color=purple]0,CONNECT

[color=red]+IPD,0,415:[/color][color=green]GET[/color] [color=blue]/arduino[/color] HTTP/1.1
Host: 192.168.1.28
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: fr-FR,fr;q=0.8,en;q=0.6,en-US;q=0.4

[/color]

Vous êtes rodé, on reconnait donc qu'on a reçu une requête TCP puisque l'on a +IPD, que le numéro de connexion réseau sur l'ESP-01 est 0, que la requête comporte 415 caractères et que la méthode est GET et que la resource demandée est /arduino

wow, ça marche !

Bon, merci pour l'entracte, mais ça ne nous dit toujours pas comment on répond et ce que l'on doit répondre.

Pour savoir ce que l'on doit répondre, il faut lire la specification du protocole HTTP... c'est long et pénible et ça évolue sans cesse - par exemple en février 2014, la spécification de HTTP 1.1 a été re-publiée et éclatée en plusieurs RFC et corrigée pour toutes ses imprécisions, RFC 7230 à RFC 7237)

Le protocole HTTP 1.0, décrit dans la RFC 1945, prévoit l'utilisation d'en-têtes spécifiés dans la RFC 822. La gestion de la connexion est simple: le client établit la connexion, envoie une requête, le serveur répond et ferme immédiatement la connexion pour signifier la fin de transmission.

Voilà à quoi ça ressemble:

Une requête HTTP présente le format suivant :

Ligne de commande (Commande, URL, Version de protocole)
En-tête de la requête
[Ligne vide]
Corps de la requête

Les réponses HTTP présentent le format suivant :

Ligne de statut (Version, Code-réponse, Texte-réponse)
En-tête de la réponse
[Ligne vide]
Corps de la réponse

donc il suffit de savoir qu'une réponse HTTP c'est "en gros" juste de renvoyer des données en vrac et terminer la connexion. Si vous organisez un peu le "vrac" alors vous pourrez avoir des jolies pages HTML mais sinon le navigateur web va essayer de comprendre ce que vous renvoyez et l'afficher.

Bon mais comment répondre à une requête

par une ou plusieurs commandes AT pardi !

Donc comme précisé par la spec HTPP, on va balancer des octets sur la ligne. il nous faut donc une commande qui envoie un certain nombre d'octets. Cette commande c'est [color=blue]AT+CIPSEND[/color]

On voit que l'on a besoin de 2 choses pour exécuter correctement cette commande, d'une part le numéro de la connexion (vous vous souvenez, c'est le chiffre qui était juste après le [color=red]+IPD,[/color] et le nombre d'octets que l'on veut transmettre

Une fois cette commande envoyée votre ESP-01 n'interprète plus ce qu'il reçoit comme des commandes AT, mais récupère le tout comme octets à transférer, jusqu'à ce qu'il ait reçu le nombre d'octets spécifiés.

Bien sûr on n'est pas complètement coincé si on change d'avis: La doc dit que si vous voulez sortir de ce mode sans tout taper, vous envoyez la séquence magique "+++" dans un paquet unique (et attendre 1 seconde avant de passer à la commande AT suivante)

Enfin et c'est important la doc dit que le nombre d'octets dans un "paquet" doit être inférieur à 2048 (2 kilo-octets) et qu'il faut 20ms entre l'envoi de deux paquets.

OK, c'est bien beau tout cela mais je fais quoi alors ?

On va aller au plus simple. on va ignorer la Ligne de statut et les en-tête de la réponse et juste balancer du texte.

Donc on se prépare

  • ATTENTION: si vous avez fait l'essai des +++ pensez à remettre la console avec la fin de ligne sous forme de CR+NL car sinon vos commandes AT ne sont pas prises en compte.

  • on tape dans le navigateur web

[color=blue]http://192.168.1.28:80[/color]

On vérifie que l'on reçoit bien la requête dans la console Série et on note le numéro de connexion

[color=purple]0,CONNECT

+IPD,[color=red][b]0[/b][/color],408:GET / HTTP/1.1
Host: 192.168.1.28
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: fr-FR,fr;q=0.8,en;q=0.6,en-US;q=0.4

[/color]

On note le 0 et on tape donc avant le timeout dans la console Série que l'on veut envoyer une réponse de 13 octets. Pourquoi 13? parce que je vais envoyer le texte "HELLO WORLD" suivi de CR et de NL qui sont rajoutés par la console série et que si vous comptez ça fait 11 octets pour le texte et 2 octets rajoutés (CR et NL) = 13

[color=blue]AT+CIPSEND=[color=red][b]0[/b][/color],13[/color]

la réponse:

[color=purple]AT+CIPSEND=0,13


OK
> [/color]

On voit le prompt d'attente > et on tape notre message

[color=blue]HELLO WORLD[/color]

on valide, comme ça fait 13 octets, l'ESP vous dit qu'il a tout reçu

[color=purple]Recv 13 bytes

SEND OK[/color]

il ne reste plus qu'à clore la connexion pour dire au navigateur que la conversation HTTP est terminée. Pour cela on envoie simplement une commande AT supplémentaire

[color=blue]AT+CIPCLOSE=0[/color] (le 0 c'est le numéro de connexion, le même que vous avez utilisé dans la commande CIPSEND )

et là, pour le plus grand bonheur des petits et grands, devant nos yeux ébahis, le navigateur web affiche...

ça fonctionne donc !!!

pour les curieux, si vous regardez la console Série Arduino, vous voyez que votre ESP-01 se met encore à vous parler après avoir reçu la fermeture de la connexion... Diantre, que diable me veut-il encore ?

étudions donc la nouvelle requête entrante:

[color=purple][color=green]+IPD,0,374:[/color][color=blue]GET /favicon.ico[/color] HTTP/1.1
Host: 192.168.1.28
Connection: keep-alive
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36
Accept: image/webp,image/apng,image/*,*/*;q=0.8
Referer: http://192.168.1.28/
Accept-Encoding: gzip, deflate
Accept-Language: fr-FR,fr;q=0.8,en;q=0.6,en-US;q=0.4

[color=red]0,CLOSED[/color][/color]

on voit que c'est une nouvelle requête TCP sur la connexion 0 avec 374 octets dans la requête puisqu'on a le +IPD,0,374: et la requête en question c'est un GET HTTP pour l'élément suivant /favicon.ico

Une favicon (icône de favori) c'est un petit dessin informatique symbolisant un site web. Elles est utilisée par le concepteur du site internet pour que les navigateurs puisse l'afficher dans la barre d'adresse, les signets, les onglets ou les raccourcis. Ca ne fait pas partie du protocole HTTP, c'est le navigateur web qui prend l'initiative de nous dire "eh si par hasard tu as une icône à me filer, je suis preneur"... Mais vous remarquez aussi le 0,CLOSED qui s'affiche à la fin... ça veut dire que votre commande [color=blue]AT+CIPCLOSE=0[/color] a bien été prise en compte et la communication est rompue... Donc vous devez ignorer superbement la demande du favicon.


Et si je veux retourner une "jolie" page web ?

C'est à la fois simple et compliqué : il faut balancer une réponse HTML bien structurée avec les en têtes et le contenu. C'est là qu'il faut prendre un peu de temps si vous voulez faire quelque chose de joli et aller lire un cours HTML (et CSS) pour tout comprendre de la mise en forme.

Pour l'exemple essayons de faire une page plus sympa. Je conserve quelque chose de très simple, par exemple

qui correspond au code HTML suivant

<!DOCTYPE HTML>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>TEST DE MISE EN PAGE</title>
  <style type="text/css">
    body {background-color: #00979c}
  </style>
</head>
<body>
Temp&eacute;rature = 10&deg;C

<button onclick="location.href='/tup'" type='button'>  +  </button>
<button onclick="location.href='/tdown'" type='button'>  -  </button>
</body>
</html>

et bien il va falloir envoyer ce texte HTML avec des commandes [color=blue]AT+CIPSEND=0,yyyy[/color] où yyyy est le nombre d'octets mais avant d'envoyer ce texte on va aussi faire propre et envoyer notre en tête HTTP et donc en tout ce qu'il faut envoyer c'est cela - avec une ligne vide pour séparer les en-têtes du protocole du contenu du message

[color=blue]HTTP/1.1 200 OK
Content-Type: text/html
Connection: close  
[color=red][LIGNE VIDE ICI][/color]
<!DOCTYPE HTML>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>TEST DE MISE EN PAGE</title>
  <style type="text/css">
    body {background-color: #00979c}
  </style>
</head>
<body>
Temp&eacute;rature = 10&deg;C

<button onclick="location.href='/tup'" type='button'>  +  </button>
<button onclick="location.href='/tdown'" type='button'>  -  </button>
</body>
</html>[/color]

on se prépare:

on tape dans le navigateur web [color=blue]http://192.168.1.28:80[/color]

On vérifie que l'on reçoit bien la requête dans la console Série et on note le numéro de connexion

[color=purple]0,CONNECT

+IPD,0,408:GET / HTTP/1.1
Host: 192.168.1.28
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: fr-FR,fr;q=0.8,en;q=0.6,en-US;q=0.4[/color]

et là on balance toutes les lignes (en regroupant un peu)

[color=blue]
AT+CIPSEND=0,17
HTTP/1.1 200 OK
AT+CIPSEND=0,25
Content-Type: text/html
AT+CIPSEND=0,21
Connection: close  
AT+CIPSEND=0,2

AT+CIPSEND=0,29
<!DOCTYPE HTML><html><head>
AT+CIPSEND=0,69
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
AT+CIPSEND=0,37
<title>TEST DE MISE EN PAGE</title>
AT+CIPSEND=0,78
<style type="text/css">body {background-color: #00979c}</style></head><body>
AT+CIPSEND=0,35
Temp&eacute;rature = 10&deg;C

AT+CIPSEND=0,70
<button onclick="location.href='/tup'" type='button'>   +  </button>
AT+CIPSEND=0,71
<button onclick="location.href='/tdown'" type='button'>  -  </button>
AT+CIPSEND=0,16
</body></html>
AT+CIPCLOSE=0
[/color]

et si vous arrivez à tout taper correctement tout cela (copier coller, attention au timeout et attention aux blancs en fin de ligne) vous verrez que dès que vous commencez à envoyer le corps HTML votre page web se construit dans le navigateur, éléments par élément et une fois que vous avez fini vous verrez

bon ça fonctionne mais c'est quand même drôlement laborieux... vous allez me demander comment j'ai fait pour compter le bon nombre de caractère par ligne... et bien à la main... enfin, j'ai mis les lignes dans excel et j'ai écrit une petite formule qui me génére la commande AT+CIPSEND qui va bien et le texte à taper...

Notez que les 2 boutons sont fonctionnels, si vous cliquez dessus vous verrez dans la console Série

[color=purple]0,CONNECT

+IPD,0,442:[color=red]GET /tup[/color] HTTP/1.1
Host: 192.168.1.28
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://192.168.1.28/
Accept-Encoding: gzip, deflate
Accept-Language: fr-FR,fr;q=0.8,en;q=0.6,en-US;q=0.4

0,CLOSED
0,CONNECT

+IPD,0,444:[color=red]GET /tdown[/color] HTTP/1.1
Host: 192.168.1.28
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://192.168.1.28/
Accept-Encoding: gzip, deflate
Accept-Language: fr-FR,fr;q=0.8,en;q=0.6,en-US;q=0.4

[/color]

On voit donc bien les requêtes GET pour /tup (température UP) et /tdown (temperature down) que j'avais affectés aux 2 boutons dans le code HTML (hyper simplifié, sans formulaire)

A vous de détecter donc en lisant la requête GET et l'URL associée ce que le navigateur demande et veut faire et de rebâtir ensuite la liste des commandes à envoyer pour afficher la nouvelle page réponse

à noter ici que comme notre réponse HTTP était bien formée, la demande du favicon n'est pas interrompue et vous devriez donc répondre à une requête pour favicon par un [color=blue]AT+CIPCLOSE=0[/color]

Voilà vous avez toute la théorie de base nécessaire pour faire ce que vous voulez (enfin la base) par les commandes AT !

Donc pour résumer:

On configure

AT+RESTORE
AT+CWMODE=1
AT+CWJAP="SSID","MotDePasse"
AT+CIPMUX=1
AT+CIPSERVER=1,80

puis on doit attendre de recevoir une commande TCP

Pour cela on analyse les lignes reçues de l'ESP et on cherche +IPD,[color=red]x[/color],yyy:[color=blue]GET[/color] [color=purple]/[/color] HTTP/1.1 qui est c'est une requête HTTP de type GET avec comme numéro de connection (linkID) x (souvent 0 si vous êtes tout seul à vous connecter) pour la resource /.

Si vous avez ces 3 conditions alors vous pouvez bâtir une réponse HTTP correcte. pour cela on fait autant de cycle de

AT+CIPSEND=[color=red]linkID[/color],<[i]length[/i]>
[color=purple]ici on met[/color] <[i]length[/i]> [color=purple]octets[/color]

que l'on veut en s'assurant de ne pas avoir plus de 2048 octets pour et de faire une pause de 20ms entre deux envois consécutifs.

l'information que l'on envoie doit respecter le protocole HTTP, idéalement vous mettez un header conforme, puis vous envoyez une ligne vide, puis le corps en HTML de votre page

Enfin on termine en fermant la connexion en envoyant un

AT+CIPCLOSE=[color=red]linkID[/color]

Oui mais c'est super laborieux ton truc....

Ben oui.. mais heureusement vous avez un Arduino sous la main... il peut donc automatiser énormément de choses pour vous si d'aventure vous programmez correctement la bête...

la partie configuration initiale peut être dans le setup(), puis dans la boucle on va écouter la voie série, en récupérant ligne par ligne ce que nous dit le module ESP (cf mon tuto pour écouter une entrée asynchrone). et on attend la ligne qui commence par +IPD,. On extrait les valeurs importantes de cette commande pour décider si on doit répondre ou ignorer (renvoyer un CIPCLOSE) et si on répond alors on alterne les CIPSEND et le contenu pour bâtir la réponse et on termine la connexion.

Si on veut pousser un peu la résistance du code, il est bon de tester la réponse de l'ESP-01 à toute commande, au moins en s'assurant qu'il dit 'ready' ou 'OK' ou que l'on a le prompt '>' pour envoyer les data par exemple avant d'envoyer la nouvelle commande.

ça semble jouable, non?

plus difficile, le fait que deux navigateurs peuvent envoyer des commandes en même temps et donc sur le port série, en réponse à des commandes vous allez peut-être voir arriver en plus d'autre ordres... donc faudra un analyseur un peu robuste si vous voulez gérer cela bien (quand votre ESP est en mode envoi de données il met ce qu'il reçoit de côté donc ça ne vient pas vous polluer)

Mettre tout cela en musique avec du code

Une bonne pratique quand on veut faire du code c'est de bien comprendre ce dont on a besoin. Tous les tests ci dessus peuvent sembler laborieux, mais à la main, nous avons réussi à envoyer l'ensemble des commandes AT nécessaire à

  • La configuration de l'ESP-01 pour qu'il rejoigne le réseau Wi-Fi et écoute des requêtes TCP entrantes
  • On a compris comment détecter une requête web pertinente - pas de code on ouvre les yeux :slight_smile:
  • La structuration d'une réponse respectant les contraintes techniques de l'ESP
  • La clôture d'une session

On a aussi en tête maintenant la structure générale de notre programme

  • Le setup() doit établir la configuration initiale et rejoindre le réseau. Pour ce faire on doit établir une communication série avec l'ESP et envoyer un certain nombre de commandes AT.

  • La loop() doit détecter une requête pertinente et la traiter. Pour ce faire on doit écouter un flux de données structuré sur le port Série de l'ESP-01, détecter une ligne spéciale qui commence par "+IPD,", extraire de l'information de cette ligne (le linkID et l'URL) en confirmant que c'est une requête pertinente (le GET, le HTTP) et si c'est le cas bâtir et envoyer la réponse et clore la connexion.

J'ai déjà rédigé un tuto sur les bases de l'écoute sur le port série, on va s'inspirer de ce qui est fait et je vous engage à le lire pour comprendre le principe.

Enfin, toute bonne pratique de code doit se faire en décomposant les problèmes et ensuite en agrégeant les solutions, on va donc explorer diverses fonctions qui nous sont nécessaires

1. envoyer des commandes AT
Dit comme cela, ça paraît simple, mais pour envoyer correctement une commande AT il faut réaliser un certain nombre de tâches. écrire bien sûr sur le port série, mais surtout écouter si la réponse contient des éléments qui nous intéressent.

Comme vous le savez si vous avez lu mon tuto référencé plus haut, il vaut mieux éviter la classe String sur notre petit micro contrôleur et utiliser des fonctions de plus bas niveau standard en C (
stdlib.h et string.h par exemple)

Certaines fonctions d'analyse sont performantes et générales comme sscanf() mais peuvent être couteuses en mémoire programme.

imaginons(par hasard) que vous ayez une chaîne de caractère dans un buffer qui contient ceci "+IPD,5,200:GET /tup HTTP/1.1" et que vous vouliez extraire le 5 et le /tup.

Un premier code pourrait être celui ci, utilisant la fonction sscanf()

char ligne[] = "+IPD,5,200:GET /tup HTTP/1.1";
int linkID;
int reqSize;

const byte maxURLLength = 20;
char urlBuffer[maxURLLength];

void setup() {
  Serial.begin(115200);
  if (sscanf(ligne, "+IPD,%d,%d:GET %s HTTP/1.1", &linkID, &reqSize, urlBuffer) == 3) {
    Serial.println(linkID);
    Serial.println(urlBuffer);
  } else {
    Serial.println(F("Erreur"));
  }
}

void loop() {}

(si vous l'exécutez vous verrez qu'il affiche bien le 5 et le /tup)

et si vous regardez les infos de compilation

Le croquis utilise 3940 octets (1%) de l'espace de stockage de programmes. Le maximum est de 253952 octets.
Les variables globales utilisent 268 octets (3%) de mémoire dynamique, ce qui laisse 7924 octets pour les variables locales. Le maximum est de 8192 octets.

Si vous comparez maintenant avec le programme suivant qui effectue la même chose en gérant des pointeurs et de la recherche dans des chaînes

char ligne[] = "+IPD,5,200:GET /tup HTTP/1.1";
int linkID;
int reqSize;
const byte maxURLLength = 20;
char urlBuffer[maxURLLength];

boolean isHTTPRequest(char * s)
{
  boolean correct = false;
  char * ptr1, *ptr2, *ptr3;

  if (ptr1 = strstr(s, "+IPD,")) {
    if (ptr2 = strstr(s, ":GET /")) {
      if (ptr3 = strstr(s, " HTTP")) {
        linkID = atoi(ptr1 + 5);
        byte b = *ptr3;
        *ptr3 = '\0';
        strncpy(urlBuffer, ptr2 + 5, maxURLLength));
        urlBuffer[maxURLLength - 1] = '\0'; // strncpy might not put the trailing null
        *ptr3 = b;   // restore string as it was
        correct = true;
      }
    }
  }
  return correct;
}

void setup() {
  Serial.begin(115200);
  if (isHTTPRequest(ligne)) {
    Serial.println(linkID);
    Serial.println(urlBuffer);
  } else {
    Serial.println(F("Erreur"));
  }
}

void loop() {}

on obtient le même résultat, les valeurs sont bien lues même si je conviens que c'est moins intuitif - en gros voilà comment elle fonctionne:

On vérifie la présence de mots clés dans la ligne en cherchant avec strstr() et on positionne des pointeurs (ptr1, ptr2, ptr3) sur ces mots clés. Si ces 3 pointeurs sont non nuls on considère que l'on a trouvé une phrase correcte et comme on sait où sont les éléments pertinents on utilise des fonctions standard atoi() qui convertit du texte en entier et strncpy() qui copie un certain nombre de caractères pour extraire les 2 éléments pertinents.

Cela dit côté mémoire le travail que l'on a fait à la main est payant. Parce que l'on sait ce que l'on cherche notre fonction peut être plus efficace en mémoire.

Le croquis utilise 2408 octets (0%) de l'espace de stockage de programmes. Le maximum est de 253952 octets.
Les variables globales utilisent 258 octets (3%) de mémoire dynamique, ce qui laisse 7934 octets pour les variables locales. Le maximum est de 8192 octets.

On gagne plus de 1500 octets de mémoire programme.

sscanf() est une fonction générique hyper puissante mais elle a un coût en empreinte mémoire. De plus en travaillant à la main je m'assure de ne pas dépasser la taille du buffer mémoire d'URL, chose que je ne contrôle pas bien avec sscanf() sans rajouter encore plus de code. Certes sur un MEGA on en a 253952 à disposition, on peut donc hésiter entre l'un est l'autre, mais ces 1500 octets vont potentiellement vous manquer à un moment surtout si vous voulez mettre en mémoire flash par exemple le code de votre site web.

Pour cette raison je laisse tomber la facilité de sscanf() et je pars sur la manipulation de pointeurs. vous voyez qu'avec un petit dessin ce n'est pas compliqué.

Mine de rien on progresse ! On a déjà une fonction utile qui nous dit si un buffer contient une requête bien formée et dans ce cas extrait les 2 éléments importants pour la génération de pages web en commande AT.


Attaquons nous au problème de lire une ligne dans un buffer de manière non bloquante. Voici une fonction gotLine() qui va lire sur un port série (que je définis modulaire pour plus tard), ignore les '\r' et répond vrai quand on a trouvé la fin de ligne '\n'. Dans ce cas le buffer est correctement constitué et stocké dans ESP_MessageLine.

#define ESPSEPRIAL Serial

const byte maxMessageSize = 100;
char ESP_MessageLine[maxMessageSize + 1]; // +1 as we want to be able to store the trailing '\0'

// --------------------------------------
// read a line from ESPSEPRIAL, ignore '\r' and returns true if '\n' is found
// --------------------------------------

boolean gotLine()
{
  static byte indexMessage = 0;
  boolean incomingMessage = true;

  while (ESPSEPRIAL.available() && incomingMessage) {
    int c = ESPSEPRIAL.read();
    if (c != -1) {
      switch (c) {
        case '\n':
          ESP_MessageLine[indexMessage] = '\0'; // trailing NULL char for a correct c-string
          indexMessage = 0; // get ready for next time
          incomingMessage = false;
          break;
        case '\r': // don't care about this one
          break;
        default:
          if (indexMessage <= maxMessageSize - 1) ESP_MessageLine[indexMessage++] = (char) c; // else ignore it..
          break;
      }
    }
  }
  return !incomingMessage;
}


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

void loop() {
  if (gotLine()) {
    Serial.print(ESP_MessageLine);
  }

  // here you can do something else!
}

La boucle tourne en allant voir et enregistrant tout ce qui arrive et quand on est prêt on affiche ce qu'on a lu et on recommence. Tout simple.

Seconde fonction utile dans la poche qui, combinée avec la première, va nous permettre d'écouter le port Série dans la loop() et une fois qu'on a reçu une ligne, tester simplement si c'est une requête GET que l'on doit gérer.

void loop() {
  if (gotLine()) {
    if (isHTTPRequest(ESP_MessageLine)) {
      if (!strcmp(urlBuffer, "/")) { // on regarde si l'URL est pour le site racine
        // si oui générer le web site
      }
      // fermer la connexion
    }
  }

  // ici on peut faire autre chose du moment que ce n'est pas trop long
  // ....
}

la structure prend forme!

On voit que dans la boucle je dois générer le site web.. Grande question de savoir comment coder cela. Scindons les problèmes. Il va d'abord décider où on stocke le code HTML. S'il n'est pas trop conséquent, on peut le mettre en mémoire programme et pour cela on utilise PROGMEM sinon il faut du stockage externe, par exemple une carte SD. Dans cet exemple vu que le MEGA a beaucoup de mémoire on va partir sur le stockage en mémoire flash.

Quand on veut stocker du texte au kilomètre dans la mémoire flash, on déclare un tableau et on écrit le contenu. Si c'est long et qu'on a plusieurs lignes et qu'on veut pas coder les '\n' à la main, c'est pénible.

Il existe une astuce de représentation en C++ qu'on appelle Raw string literal. C'est une notation où vous mettez en début et fin de chaîne des marqueurs et tout ce qui est au milieu se trouvera dans la chaîne. il suffit que le marqueur ne se trouve bien sûr pas dans le texte. Perso j'utilise souvent [color=red]--8<--8<--[/color] (ce sont des ciseaux sur une ligne pour montrer qu'on découpe là :slight_smile: )

l'écriture est simple

const char blabla[] PROGMEM = [color=red]R"--8<--8<--([/color][color=blue]ici tout
le texte que vous tapez
y compris les passages à la ligne
se trouvera dans la chaîne de blabla
c'est super non ?
[/color][color=red])--8<--8<--"[/color];

pour simplifier encore je mets cela généralement dans un onglet séparé du programme principal que j'appelle par exemple contenu.h et il ne reste plus qu'à mettre en début de son onglet principal #include "contenu.h" pour que le tableau blabla soit connu. (une autre raison de le mettre dans un autre onglet c'est que l'appui sur ctrl-T our cmd-T sur mac pour indenter correctement son code ne fonctionne plus si vous avez ce genre de déclaration)

OK vous me croyez sur parole, c'est sympa, mais comment on peut lire la mémoire pour l'envoyer sur le port Série par exemple ? faisons un petit programme pour cela (je mets tout dans le même fichier par simplicité pour poster dans le forum).

// C++ raw string literals cf http://en.cppreference.com/w/cpp/language/string_literal
// USE PROGMEM with Program Space Utilities http://www.nongnu.org/avr-libc/user-manual/group__avr__pgmspace.html

const char webRoot[] PROGMEM = R"--8<--8<--(HTTP/1.1 200 OK
Content-Type: text/html
Connection: close

<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>TEST</title>
<style type="text/css">
body {background-color: #00979c}
</style></head><body>HELLO WORLD
</body></html>
)--8<--8<--";

uint16_t linkID = 1;

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

  const byte maxLine = 100;
  char lineToSend[maxLine + 1];
  uint8_t c, index;

  uint16_t nbBytes = strlen_P(webRoot);
  index = 0;

  for (uint16_t pos = 0; pos < nbBytes; pos++) {
    c =  pgm_read_byte(webRoot + pos);
    lineToSend[index++] = (char) c;

    if ((c == '\n') || (index >= maxLine - 1)) {
      lineToSend[index] = '\0';
      if (!strcmp(lineToSend, "\n"))
        strcpy(lineToSend, "\r\n");        // respect the HTTP spec for empty line and send \r\n instead
      Serial.print(F("AT+CIPSEND="));
      Serial.print(linkID);
      Serial.print(F(","));
      Serial.println(strlen(lineToSend));
      Serial.print(lineToSend);
      index = 0;
    }
  }
}

void loop() {}

si vous exécutez ce code vous aurez en sortie quelque chose qui doit vous rappeler ce que l'on a vu plus haut: en lisant les lignes on va pouvoir générer les commandes AT+CIPSEND pour l'envoi des données !

[color=red]AT+CIPSEND=1,16[/color]
[color=blue]HTTP/1.1 200 OK[/color]
[color=red]AT+CIPSEND=1,24[/color]
[color=blue]Content-Type: text/html[/color]
[color=red]AT+CIPSEND=1,18[/color]
[color=blue]Connection: close[/color]
[color=red]AT+CIPSEND=1,2[/color]

[color=red]AT+CIPSEND=1,16[/color]
[color=blue]<!DOCTYPE HTML>[/color]
[color=red]AT+CIPSEND=1,7[/color]
[color=blue]<html>[/color]
[color=red]AT+CIPSEND=1,7[/color]
[color=blue]<head>[/color]
[color=red]AT+CIPSEND=1,68[/color]
[color=blue]<meta http-equiv="Content-Type" content="text/html; charset=utf-8">[/color]
[color=red]AT+CIPSEND=1,20[/color]
[color=blue]<title>TEST</title>[/color]
[color=red]AT+CIPSEND=1,24[/color]
[color=blue]<style type="text/css">[/color]
[color=red]AT+CIPSEND=1,33[/color]
[color=blue]body {background-color: #00979c}[/color]
[color=red]AT+CIPSEND=1,51[/color]
[color=blue]</style></head><body>HELLO WORLD
</body></html>[/color]

Comme la communication AT se fait en mode ligne, peut lire ligne par ligne ce qui arrive et ensuite analyser comme on l'a vu plus haut.

Il se peut aussi que parfois on se fiche du contenu des lignes et que l'on attende un morceau de chaîne de texte (comme le "OK" final d'une réponse à une commande AT). Cela nous sera utile pour après avoir envoyé une commande pour attendre la réponse et s'assurer qu'elle a bien été exécutée.

L'écriture de cette fonction est relativement simple mais pas simpliste. On va prendre en paramètre la chaîne attendue et pour bien sûr ne pas se retrouvé coincé on va limiter la lecture du port série à un certain temps. Si au bout de ce certain temps on n'a pas reçu la réponse, on abandonne et on dit au code appelant que ça n'a pas marché.

Voici un bout de code qui vous donne 5 secondes pour lui dire "HELLO" dans la console Série. il vous salue en retour si vous écrivez HELLO sinon... je vous laisse essayer

#define ESPSEPRIAL Serial



// --------------------------------------
// waitForString wait max for duration ms whilst checking if endMarker string is received
// on the ESP Serial port returns a boolean stating if the marker was found
// --------------------------------------

boolean waitForString(const char * endMarker, unsigned long duration)
{
  int localBufferSize = strlen(endMarker); // we won't need an \0 at the end
  char localBuffer[localBufferSize];
  int index = 0;
  boolean endMarkerFound = false;
  unsigned long currentTime;

  memset(localBuffer, '\0', localBufferSize); // clear buffer

  currentTime = millis();
  while (millis() - currentTime <= duration) {
    if (ESPSEPRIAL.available() > 0) {
      if (index == localBufferSize) index = 0;
      localBuffer[index] = (uint8_t) ESPSEPRIAL.read();
      endMarkerFound = true;
      for (int i = 0; i < localBufferSize; i++) {
        if (localBuffer[(index + 1 + i) % localBufferSize] != endMarker[i]) {
          endMarkerFound = false;
          break;
        }
      }
      index++;
    }
    if (endMarkerFound) break;
  }
  return endMarkerFound;
}


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

void loop() {
  if (waitForString("HELLO", 5000ul)) {
    Serial.println("Bonjour!");
  } else {
    Serial.println("Malpoli!");
  }
}

la fonction crée un buffer circulaire qui conserve suffisamment de caractères par rapport au mot clé recherché et écoute le port Série pendant un certain temps et compare à chaque fois que l'on a reçu un nouveau caractère si on a enfin le mot clé.

Cette fonction nous sera bien utile, après avoir envoyé une commande à l'ESP pour s'assurer qu'il nous répond bien "Ready" ou "OK".

Se pose donc la question de l'envoi des données à l'ESP. Certes, c'est simplement écrire sur le port série, mais comme on va le faire souvent, autant se dotter de petits outils qui vont nous simplifier la vie.

Il nous faut une fonction qui envoie une commande, et attends pendant un certain temps le code de réponse et retourne VRAI si ça s'est bien passé et faux sinon.

Mais ce n'est pas toujours aussi simple, parfois il faut passer des variables au coeur des commandes, par exemple le SSID et le mot de passe quand on envoie

AT+CWJAP="[color=blue]SSID[/color]","[color=blue]MotDePasse[/color]"

Soit il faut bâtir une chaîne avec tout le contenu avant de l'envoyer - mais ça mange de la mémoire pour rien, soit il faut pouvoir construire petit à petit la requête avec des print ou write.

On s'équipe donc pour simplification des 3 outils suivants

// --------------------------------------
// espPrintlnATCommand executes an AT commmand by adding at the end a CR LF
// then it checks if endMarker string is receivedon the ESP Serial port
// for max duration ms returns a boolean stating if the marker was found
// --------------------------------------

boolean espPrintlnATCommand(const char * command, const char * endMarker, unsigned long duration)
{
  ESPSEPRIAL.println(command);
  return waitForString(endMarker, duration);
}

// --------------------------------------
// espPrintATCommand or espWriteATCommand is used when you don't want to send the CR LF
// at the end of the commmand line; use it to build up a multi part command
// same syntax as print as they are Variadic Macros using print or write
// --------------------------------------
#define espPrintATCommand(...) ESPSEPRIAL.print(__VA_ARGS__)
#define espWriteATCommand(...) ESPSEPRIAL.write(__VA_ARGS__)

la fonction espPrintlnATCommand() envoie la commande et attends une réponse pendant un certain temps et retourne vrai (ça s'est bien passé) ou faux (mal passé). et j'ai deux marcros qui ne font qu'appeler print ou write, juste par convenance.

et tant qu'on est dans les utilitaires, dotons nous de

  • 2 fonctions d'affichage sélectif pour être en mode debug ou pas. Pour cela on utilise de la compilation conditionnelle, si le drapeau debugFlag est défini alors la macro debugMessage() effectue l'impression sur le port Serial sinon la macro ne fait rien. idem pour l'autre. (attention au remplacement de texte comme c'est une macro et pas une fonction)

  • On définit aussi des éléments globaux dont on aura besoin souvent, par exemple la chaîne "OK" avec son passage à la ligne ou des pauses de durée variable.

// comment this line to remove Debug information
#define debugFlag

// comment this line to remove the detailed Debug information
#define deepDebugFlag


#ifdef debugFlag
#define debugMessage(...) Serial.print(__VA_ARGS__)
#else
#define debugMessage(...) {}
#endif


#ifdef deepDebugFlag
#define deepDebugMessage(...) Serial.print(__VA_ARGS__)
#else
#define deepDebugMessage(...) {}
#endif

#define SHORT_PAUSE (1000ul)
#define LONG_PAUSE  (10000ul)
const char * OK_STR = "OK\r\n";
const char * DOT_STR = ".";

Essayons de mettre un peu tout cela ensemble par exemple pour envoyer la commande qui demande notre adresse IP et ensuite la récupérer. Voici à quoi ça pourrait ressembler

byte myIPAddress[4];  // une variable globale pour lire l'adresse

...

// --------------------------------------
// extract from the flow of data our IP address
// --------------------------------------
boolean getMyIPAddress()
{
  unsigned long currentTime;

  const char * searchForString = "+CIFSR:STAIP,";
  const byte searchForStringLength = strlen(searchForString);

  char * ptr;
  boolean foundMyIPAddress = false;

  espPrintATCommand(F("AT+CIFSR\r\n")); // ask for our IP address

  // this returns something like
  //  AT+CIFSR
  //
  //  +CIFSR:STAIP,"192.168.1.28"
  //  +CIFSR:STAMAC,"18:fe:34:e6:27:8f"
  //
  //  OK

  currentTime = millis();
  while (millis() - currentTime <= LONG_PAUSE) {
    if (gotLine()) {
      if (ptr = strstr(ESP_MessageLine, searchForString)) {
        ptr += searchForStringLength + 1; // +1 to skip the "
        if (!(ptr = strtok (ptr, DOT_STR))) break;
        myIPAddress[0] = atoi(ptr);
        if (!(ptr = strtok (NULL, DOT_STR))) break;
        myIPAddress[1] = atoi(ptr);
        if (!(ptr = strtok (NULL, DOT_STR))) break;
        myIPAddress[2] = atoi(ptr);
        if (!(ptr = strtok (NULL, DOT_STR))) break;
        myIPAddress[3] = atoi(ptr);
        if (foundMyIPAddress = waitForString(OK_STR, SHORT_PAUSE)) { // wait for the final OK
          debugMessage(F("\nmy IP address is: "));
          debugMessage(myIPAddress[0]); debugMessage(DOT_STR);
          debugMessage(myIPAddress[1]); debugMessage(DOT_STR);
          debugMessage(myIPAddress[2]); debugMessage(DOT_STR);
          debugMessage(myIPAddress[3]);
          debugMessage(F("\n"));
          break;
        }
      }
    }
  }
  return foundMyIPAddress ;
}

Vous voyez qu'on balance la commande à l'ESP, puis on lit ligne par ligne la réponse et on regarde si la réponse contient "+CIFSR:STAIP," qui est l'indication que l'adresse IP suit et là j'utilise strtok() pour passer de bout en bout d'adresse IP et extraire le nombre. Enfin on vide le reste du buffer de la commande en attendant le OK final et si le debug est activé alors on imprime l'adresse IP.

De même le setup() est maintenant simplifié, on envoie la suite de commandes

AT+RESTORE
AT+CWMODE=1
AT+CWQAP
AT+CWJAP="ssid","pwd"
AT+CIPMUX=1
AT+CIPSERVER=1,80

en vérifiant à chaque fois que ça s'est bien passé.

Si ça se passe mal - je ne le fais pas ici - vous pourriez couper l'alimentation de l'ESP par exemple si elle est pilotée pour le rebooter complètement et éventuellement balancer un reset de l'arduino.. c'est la gestion d'erreur simple, on reboot... :). Il faudrait injecter un peu d'intelligence pour ne pas que ça reboot sans arrêt, utiliser l'EEPROM par exemple pour mémoriser le reboot et ne pas rebooter plus de 3 fois et avoir dans ce cas un mode de boot piloté par un switch qui bosserait dans un mode de configuration etc... bref un monde de possibilités !


On peut rendre notre code un peu plus configurable. Vous avez vu que je n'ai pas codé en dur partout que mon ESP était sur Serial1 mais j'utilise une macro qui remplace ESPSEPRIAL par le nom du port Série à utiliser. C'est une bonne pratique à retenir si vous voulez du code adaptable; En procédant ainsi, grace à la compilation conditionnelle, on peut même gérer un port Série SoftwareSerial ou Hardware Serial. Voici comment je fais:

// Don't go faster than 38400 with sowftare serial for reliability
#define ESPSERIALBAUD 115200 // Set to whatever is used by default by your ESP after a RESTORE

// uncomment this line if you want to use sowftare SERIAL and define the pins you use below
// #define USESOFTWARESERIAL

#ifdef USESOFTWARESERIAL
#include <SoftwareSerial.h>
// DEFINE THE PINS YOU USE FOR SOFTWARE SERIAL
#define ARDUINO_RX_PIN 5    // set here the pin number you use for software Serial Rx
#define ARDUINO_TX_PIN 6    // set here the pin number you use for software Serial Tx
SoftwareSerial esp01(ARDUINO_RX_PIN, ARDUINO_TX_PIN); // rxPin (the pin on which to receive serial data), txPin (the pin on which to transmit serial data)
#define ESPSEPRIAL esp01
#else
// DEFINE WHICH HARDWARE SERIAL YOU USE
#define ESPSEPRIAL Serial1  // could be Serial1, Serial2, Serial3 etc depending on your hardware.
#endif

Si vous définissez USESOFTWARESERIAL alors le compilateur va importer la classe SoftwareSerial définir 2 pins pour le Rx et Tx, créer une instance de la classe et définir ESPSEPRIAL à cette instance. Sinon vous affectez ESPSEPRIAL au port série matériel que vous voulez. Comme cela c'est super simple de passer sur Serial2 si c'est celui là qui vous plait.

EDIT: VOIR POST #41 SUR LA DISCUSSION DU AT+RESTORE, LE PLUS SIMPLE EST ALORS DE S'EN PASSER

On aurait pu faire pareil avec la fonction de debug et définir quel port Série utiliser pour afficher les messages de debug, j'ai eu la flemme, mais comme les fonction de debug sont encapsulées et qu'il n'y a pas des print qui traineront partout dans le code, ce serait facile à faire.


Voilà en mettant tout cela bout à bout et en injectant un peu de code on arrive à un projet qui fonctionne. On n'a pas traité tous les cas d'erreur, donc parfois faudra rebooter votre machine si d'aventure le réseau Wi-Fi était perdu etc... mais vous avez le principe.

Je vous joins un projet complet qui affiche cette page web. (les quelques commentaires sont en anglais, question d'habitude... désolé pour les non anglophones)

j'ai hacké un petit goodie supplémentaire dans la fonction qui lit le HTML stocké en mémoire flash: Je regarde avant d'envoyer le code si la ligne contient (un seul) "$$$x$$$". si c'est le cas la génération de code est modifié et on remplace le "$$$x$$$" par le contenu d'une variable stockée à la position x dans un tableau global. ça permet de rendre le code HTML statique en mémoire flash, mais de générer un truc un peu plus dynamique. par exemple la ligne HTML qui affiche la température est

Temp&eacute;rature = [color=red]$$0$$[/color]&deg;C

et le $$$0$$$ est replacé par [nobbc]variables[0][/nobbc] lors de l'envoi à l'ESP.

Ce n'est pas une implémentation robuste, il ne faut pas que la ligne soit trop longue pour ne pas être tronquée au milieu des $$ par exemple ou que je déborde de mon buffer... je vous laisse améliorer le concept :slight_smile:

Mais ça vous donne une idée de comment rendre tout cela un peu plus dynamique et donc les boutons sont fonctionnels et pour le montrer si vous augmentez ou baissez la température, la LED intégrée de la carte arduino clignotera plus ou moins vite (demi période = 10 fois la valeur de la température).

En PJ le projet complet.

En espérant que cela vous aide à

  • Analyser un problème avant de commencer à coder
  • comprendre mieux comment on peut parler à son ESP-01 par commande AT
  • architecturer un projet (il y a 2h de codage donc c'est pas super génial)
  • éviter si on peut ce qui mange beaucoup de mémoire
  • créer des trucs funs...

Soit dit en passant, vous voyez aussi pourquoi c'est galère et que la majorité de ceux qui utilisent un ESP pour une communication Wi-Fi ne le font pas au niveau de la ligne de commande mais prennent une librairie toute faite :slight_smile:

Have fun!

ESP01_WebServer.zip (5.62 KB)

Bonjour J-M-L,

Je suis avec intérêt vos publications et les réponses que vous apportez à certains Arduinoteurs.

Bravo pour ce tuto, clair et plein d'exemples.

Ayant suivi il y a quelques temps une voie similaire avec les mêmes technos pour les mêmes raisons, (pas de String, routines minimalistes et machines à états pour gérer les cas d'erreur, les reprises et encapsuler le code), je suis arrivé à un fonctionnement parfois erratique dans l'envoi de messages par l'ESP01.

Schématiquement, avec le terminal, tout allait bien, mais les choses se gâtaient quand c'était l'AVR qui envoyait le texte, mais ce n'était pas systématique.
De temps en temps, l'ESP01 s'emmêlait les pinceaux et balançait un peu n'importe quoi au navigateur. Un espion sur la ligne série m'avait confirmé que l'AVR envoyait les bonnes commandes à l'ESP01. Grace aux machines à état, l'AVR ne plantait pas, et il suffisait de renouveler la requête et cela finissait par aboutir. Le bidule fonctionnait quand même et il est resté près d'un an en service, mais c'était limité (signalement d'état et mesure de température).

Pour des fonctions plus ambitieuses (commande), j'ai donc abandonné cette voie (AVR + ESP01) pour programmer directement l'ESP8266 (nodeMCU ou similaire, qui se programme comme un Arduino et avec l'IDE Arduino). C'est plus simple, cela revient moins cher, est beaucoup plus rapide et fonctionne absolument sans problème (avec les précautions déjà mentionnées).

Je voulais savoir si vous aviez rencontré le même problème ? Dans un passage de votre tuto, vous mentionnez le reset ?

Voila, encore bravo et merci pour vos infos.

MicroQuettas

PS: j'avais développé une petite carte (simple face) AVR328 + ESP01 avec régulateur 3V3 etc, qui se programme comme un Uno avec un adaptateur USB Série. Si cela intéresse du monde, je la partagerai volontiers.

Bonsoir

L’ESP-01, surtout avec de vieux firmware n’est pas un foudre de stabilité surtout en cas de connexions multiples. l’alimentation est aussi super importante pour la stabilité

Pour la programmation directe je suis passé aux wemos D1 et le firmware embarqué - effectivement ça simplifie le tout

Bonsoir,

Heureux de voir que nous sommes arrivés à la même conclusion :slight_smile:

Je soupçonne le récepteur série de l'ESP8266 de causer les problèmes. La doc dit que le buffer interne est limité à 128 octets. Même avec des messages plus courts, j'ai dû faire quelques bidouilles pour arriver à le faire marcher de manière fiable, mais c'est peu de chose comparé aux possibilités du Wifi.

La carte Wemos D1 mini est vraiment intéressante par sa taille et son prix... Il faudra que je l'essaie. Si elle tient ses promesses, inutile de s'embêter à faire une mini carte pour ESP12 !

J'ai aussi remarqué que vous ne mettiez pas de champ "Content-Length" dans l'entête http et ça a l'air de marcher quand même. A l'occasion, je serais preneur d'explications sur la nécessité ou non de ce champ et de la raison de sa présence dans la norme ?

A+

MicroQuettas

MicroQuettas:
J'ai aussi remarqué que vous ne mettiez pas de champ "Content-Length" dans l'entête http et ça a l'air de marcher quand même. A l'occasion, je serais preneur d'explications sur la nécessité ou non de ce champ et de la raison de sa présence dans la norme ?

L'en-tête (header) Content-Length indique effectivement la taille en octets du corps d'une réponse envoyée.

La spec (section 7.2.2 pour les curieux) décrit comment déterminer la longueur de la réponse et on peut soit effectivement remplir cet attribut (mais dans notre cas on ne le connait pas au moment d'envoyer l'en tête généralement sauf à pré-générer toute la page ce qui va prendre plein de mémoire et donc n'est pas souhaitable) soit simplement clore la connexion quand on a fini de transmettre si on ne met pas d'Entity-Body --> c'est ce que fait le code (et du HTML bien écrit suffit au navigateur à afficher la page ensuite)

(dans une requête POST généralement elle serait fournie par exemple)

Merci.
Je voulais savoir si vous saviez à quoi sert ce champ content-length puisque cela marche apparemment aussi bien sans.
Pour ma part, je fais de la chunked transmission qui permet d'envoyer la longueur sans bloquer trop de mémoire.
A+

Il sert plus pour du client vers le serveur par exemple dans un POST

Bonjour,
J'ai utilisé ce tuto pour l'initiation d'un jeune aux serveur web, on c'est bien éclaté.
Pour la suite, je souhaite mettre à jour le firmware AT pour exploiter d'autres possibilités.
Pour cette mise à jour, de doit avouer que je me retrouve au milieu d'une jungle et impossible d'avancer avec ma machette :frowning:

Où peut-on trouvé de manière simple les firmware en question ?

icare:
Bonjour,
J'ai utilisé ce tuto pour l'initiation d'un jeune aux serveur web, on c'est bien éclaté.
Pour la suite, je souhaite mettre à jour le firmware AT pour exploiter d'autres possibilités.
Pour cette mise à jour, de doit avouer que je me retrouve au milieu d'une jungle et impossible d'avancer avec ma machette :frowning:

Où peut-on trouvé de manière simple les firmware en question ?

Bonsoir Icare
qu'est ce que tu entend là exactement par "firmware" ?
sur esp8266 il existe différends "firmware possible"

à l'origine les esp8266 était disponibles avec un "firmware AT"
plusieurs versions ont circulées(commandes AT differentes selon les versions)
un peu plus tard , il y a eu des implémentations differentes pour pouvoir exploiter les esp8266 differements
en gros vrac

  • sous IDE "arduino"(perso c'est ce que j'utilise actuellement)
  • sous LUA
  • micropython
  • qq autres trucs exotiques

Bonsoir Artouste,

Artouste:
qu'est ce que tu entend là exactement par "firmware" ?

Je cherche tout simplement un firmware AT (si possible la dernière version utilisable sur un 4MO).
Mais dans ce cadre, il y a pléthore de versions et en plus je n'arrive pas à tous les faire fonctionner :frowning:
Bref, ce n'est pas encore clair pour moi :wink: