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

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:

icare:
Bonsoir Artouste,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:

OK
çà fait un moment que je n'ai pas joué avec les commandes AT sur esp8266
mais il y a un incontournable
Si tu n'a pas de documentation "vérifiée" concernant "le jeux de commandes AT" adossée à un fichier .bin (firmware AT)
Tu ne peux pas etre sur de grand chose
Si je devais flasher un firmware AT , je pense que je partirais de cette doc de 2017

Re,
Il y a tellement de différence entre la doc "ESP8266 AT Instruction Set" et les binaires que l'on peut trouver au niveau des adresses mémoires. Il ne me reste plus que la méthode des tâtonnements successifs. :wink:

icare:
Re,
Il y a tellement de différence entre la doc "ESP8266 AT Instruction Set" et les binaires que l'on peut trouver au niveau des adresses mémoires. Il ne me reste plus que la méthode des tâtonnements successifs. :wink:

Meme si l'approche "firmware AT " permet de mettre le "pied à l'etrier" , qu'est ce qui te contraint à devoir rester sur de l'AT ?

Re,

Artouste:
Meme si l'approche "firmware AT " permet de mettre le "pied à l'etrier" , qu'est ce qui te contraint à devoir rester sur de l'AT ?

Rien
Je me suis pris au jeu par envie de savoir, de curiosité et de montrer au "petit jeune" que les vieux ne sont pas morts. :grin:
Les premiers essais m'ont permis de mettre la version AT : 0.51.0.0, j'en suis maintenant à la version : 1.5.0 et je tente la 2.0

icare:
Re,Rien
Je me suis pris au jeu par envie de savoir, de curiosité et de montrer au "petit jeune" que les vieux ne sont pas encore morts. :grin:
Les premiers essais m'ont permis de mettre la version AT : 0.51.0.0, j'en suis maintenant à la version : 1.5.0 et je tente la 2.0

rappelle toi et rappelle à ton petit jeune , que la temporalité est une notion physique très importante ! ;D

Entre les notions de "naissance et disparition" :grin:

L'important c'est ce qui passe entre les 2 points de mesures , considérés absolu entre eux

1 Like

:slight_smile:

[edit]

Je bloque à la version AT : 1.5.0

Toujours mieux que la version 0.60 que j’avais plus haut... faudra que je prenne le temps de mettre à jour les miens! (La version stable suivante serait la 2.1 mais effectivement sans doute pas compatible sur 4Mo)