Différences entre versions de « Arduino W5100 web server »

De The Linux Craftsman
Aller à la navigation Aller à la recherche
 
(93 versions intermédiaires par le même utilisateur non affichées)
Ligne 1 : Ligne 1 :
= Partie électronique =
+
=Introduction=
== Le composant ==
+
{|style="width:650px" align="center"
Le Shiled Ethernet embarque un contrôleur Wiznet 5100 qui est plus fiable que son homologue le contrôleur ENC28J60. Il existe principalement en shield mais peut également se trouver en platine.
+
|
 +
[[Fichier:Warning manual.jpg|centré|300px]]
 +
|valign="top"|
 +
Il faut prendre connaissance du [[Arduino_W5100_intro | module W5100]] et s'assurer que le module possède une configuration de [[Arduino_W5100_OSI3 |niveau 3 OSI]].
  
{| align=center
+
Soyez sûr de comprendre la section sur [[Arduino_sketch_writing| comment écrire un sketch]] avant de poursuivre. Le code ci-dessous fait référence à des parties bien spécifiques, détaillées et expliquées dans la section suscitée.
|-
 
|[[Fichier:W5100_shield_uno_mega.jpg|centré|250px]]
 
|[[Fichier:W5100_shield_nano.jpg|centré|350px]]
 
|[[Fichier:W5100_platine.jpg|centré|200px]]
 
|-
 
|align=center| ''Wiznet 5100'' version shield pour Mega/Uno
 
|align=center| ''Wiznet 5100'' version shield pour Nano
 
|align=center| ''Wiznet 5100'' version platine
 
 
|}
 
|}
Les versions shield sont intéressantes car elles embarquent également un lecteur de carte micro SD qui, dans notre cas de figure, va nous servir à héberger les pages du site Web.
 
  
== Le montage ==
+
= Récupération de la requête =
Le montage suivant requiert le shield, un câble Ethernet qui le relie à un switch et un ordinateur relié au même switch.
+
Il est possible de démarrer un serveur qui écoutera sur un port précis qui, dans notre cas de figure, utilisera le port ''TCP 80''. Dans un premier temps, nous allons récupérer la requête qui vient du client pour voir comment elle est formatée puis, dans un deuxième temps, on verra comment on pourra traiter et formuler une réponse.
{| align=center
+
==Imports==
|-
+
<source lang="c">
|[[Fichier:W5100_shield_on_mega.jpg|centré|350px]]
+
#include "SPI.h"
|[[Fichier:W5100_shield_on_uno.jpg|centré|300px]]
+
#include "Ethernet.h"
|[[Fichier:W5100_shield_on_nano.jpg|centré|300px]]
+
</source>
|-
+
==Variables globales==
|align=center| Shield ''Wiznet 5100'' sur un Mega
+
<source lang="c">
|align=center| Shield ''Wiznet 5100'' sur un Uno
+
// Serveur écoutant sur le port 80
|align=center| Shield ''Wiznet 5100'' sur un Nano
+
EthernetServer server(80);
|}
+
</source>
 +
==setup()==
 +
<source lang="c">
 +
// Démarrage du serveur
 +
server.begin();
 +
</source>
 +
==loop()==
 +
<source lang="c">
 +
// On écoute les connections entrantes
 +
EthernetClient client = server.available();
 +
// Si la connection est établie (SYN / SYN+ACK / ACK)...
 +
if (client) {
 +
  Serial.println(F("---- new request ----"));
 +
  // ...pendant que le client maintient la session TCP...
 +
  while (client.connected()) {
 +
    // ...et que la requête contient des caractères...
 +
    if (client.available()) {
 +
      // ...on récupére les caractères...
 +
      char c = client.read();
 +
      // ... et on les affiche sur le terminal série
 +
      Serial.print(c);
 +
    }
 +
  }
 +
  // Fin de la requête
 +
  Serial.println(F(""));
 +
  Serial.println(F("---- end request ----"));
 +
}
 +
</source>
 +
Lorsque l'on entre dans la barre de recherche du navigateur l'adresse IP du module Ethernet on  a, après le ''timeout'' TCP, le résultat suivant :
 +
<pre>
 +
---- new request ----
 +
GET /index.htm&param1=value1 HTTP/1.1
 +
Host: 192.168.1.26
 +
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:50.0) Gecko/20100101 Firefox/50.0
 +
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
 +
Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3
 +
Accept-Encoding: gzip, deflate
 +
Connection: keep-alive
 +
Upgrade-Insecure-Requests: 1
 +
 
  
N'oubliez pas que la communication entre l'Arduino et le shield se fait a travers certaines broches qu'il ne faut donc pas utiliser !
+
---- end request ----
[[Fichier:W5100_shield_port_use.jpg|centré|600px]]
+
</pre>
 +
On constate que :
 +
* la requête démarre par le verbe HTTP (ici ''GET'');
 +
* il y a un espace est un premier slash ;
 +
* suivit de l'URL (ici ''index.htm'') ;
 +
* les paramètres suivent le '?' ;
 +
* la requête contient toutes les entêtes envoyés par le navigateur ;
 +
* la requête se termine par deux sauts de ligne.
  
Comme vous pouvez le constater, le protocole SPI ([https://fr.wikipedia.org/wiki/Serial_Peripheral_Interface Serial Port Interface]) est utilisé ici pour communiquer avec la carte SD et le contrôleur Ethernet (SPI = pas en simultané).
+
Pour terminer proprement la requête ''HTTP'', il suffit de mettre fin à la session TCP à réception des deux sauts de ligne ou, mieux, d'attendre que le client n'envoie plus de caractères.
  
= Partie logicielle =
+
= Traitement de fin de requête =
== Les librairies ==
+
==loop()==
Les librairies utilisées sont déjà inclues de base et sont :
 
* SPI.h
 
* Ethernet.h
 
* SD.h
 
== Ethernet : attribution d'une adresse IP ==
 
=== Adressage IP dynamique ===
 
Le plus simple, si votre réseau possède un serveur DHCP (normalement votre box est dotée de cette fonctionnalité...), est de demander la configuration OSI de niveau 3. Les seuls paramètres qui nous intéresserons vraiment sont l'adresse IP et le masque. La passerelle ne sera pas utile dans notre cas...
 
 
<source lang="c">
 
<source lang="c">
#include <SPI.h>
+
// On écoute les connections entrantes
#include <Ethernet.h>
+
EthernetClient client = server.available();
/**
+
// Si la connection est établie (SYN / SYN+ACK / ACK)...
  Adresse MAC du module, doit être unique sur le réseau !
+
if (client) {
  Ici 00:01:02:03:04:05
+
  while (client.connected()) {
*/
+
    if (client.available()) {
byte mac[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 };
+
      char c = client.read();
 
+
      // Réception des caractères
void setup() {
+
    } else {
  // Démarrage du port série
+
      // Envoie du code status HTTP, ici '200 OK'
  Serial.begin(9600);
+
      client.println("HTTP/1.1 200 OK");
  Serial.println(F("Requesting ip..."));
+
      // Entête spécifiant le contenu du corps
  /**
+
      client.println("Content-Type: text/html");
    Démarrage du shield Ethernet sans spécifier d'adresse IP
+
      /**
    Cela oblige le contrôleur Ethernet à demander une configuration OSI 3
+
        On prévient le client qu'à la fin de
  */
+
        la requête, on coupe la session TCP
  if (Ethernet.begin(mac) == 0) {
+
      */
    // Si c'est un échec, pas la peine de pousuivre...
+
      client.println("Connection: close");
    Serial.println(F("DHCP failure !"));
+
      /**
    while (true);
+
        Spération entre les entêtes HTTP et le corps du message
 +
        !! TRES IMPORTANT, SANS LE SAUT DE LIGNE LE NAVIGATEUR
 +
        NE FAIT PAS LA SEPARTATION ENTRE HEADER ET CORPS !!
 +
      */
 +
      client.println();
 +
      // Corps du message HTTP
 +
      client.println("<!DOCTYPE HTML>");
 +
      client.println("<html>");
 +
      client.println("It Works !");
 +
      client.println("</html>");
 +
      // On donne le temps au navigateur de traiter le message
 +
      delay(1);
 +
      // Fermeture de la session TCP
 +
      client.stop();
 +
      break;
 +
    }
 
   }
 
   }
   // Affichage des informations obtenues
+
   // Fin de la requête
  Serial.print(F("IP : "));
+
   Serial.println(F(""));
  Serial.println(Ethernet.localIP());
+
   Serial.println(F("---- end request ----"));
  Serial.print(F("Mask : "));
 
   Serial.println(Ethernet.subnetMask());
 
  Serial.print(F("Gateway : "));
 
   Serial.println(Ethernet.gatewayIP());
 
  Serial.print(F("DNS : "));
 
  Serial.println(Ethernet.dnsServerIP());
 
}
 
void loop() {
 
 
}
 
}
 
</source>
 
</source>
Si vous avez une réponse de votre serveur DHCP, vous devriez obtenir, dans le terminal série, le résultat suivant:
+
A partir de maintenant, côté navigateur, la requête se termine proprement et on attend plus le ''timeout'' TCP. Il faudrait maintenant, ''digérer'' l'URL pour pouvoir répondre en fonction de la requête !
 +
 
 +
= Digestion de l'URL =
 +
Le code devient trop complexe pour résider dans la fonction principale. Il faut donc le fragmenter en plusieurs fonctions qui seront appelées dans ''loop()''.
 +
==Algorithmique==
 +
Prenons comme exemple la requête suivante :  
 
<pre>
 
<pre>
Requesting ip...
+
GET /part1/part2/index.htm?paramA=valueA&...&paramX=valueX HTTP1.1
IP : 192.168.1.26
 
Mask : 255.255.255.0
 
Gateway : 192.168.1.254
 
DNS : 192.168.1.254
 
 
</pre>
 
</pre>
 +
Nous allons écrire la fonction ''bool digestURL(char c)'' qui va :
 +
* vérifier que la ligne commence par ''GET'' ;
 +
* lire chaque partie du chemin (part1, part2, index.htm) ;
 +
* détecter le '?' pour déterminer si la requête contient des arguments ;
 +
* détecter les '=' pour faire la séparation entre nom de paramètre et valeur ;
 +
* détecter les '&' pour séparer les paramètres.
 +
 +
On part du principe que la fonction ''loop()'' appelle ''digestURL(char c)'' à la réception d'un caractère et renvoie ''vrai'' lorsque l'URL est lue.
  
=== Adressage IP statique ===
+
==Variables globales==
On peut très bien spécifier une configuration OSI de niveau 3 de manière statique. Deux avantages : pas d'adresse IP qui change et c'est plus rapide !
 
 
<source lang="c">
 
<source lang="c">
#include <SPI.h>
+
// Méthode démarrant le début de l'URL
#include <Ethernet.h>
+
static const char METHOD[] = "GET";
// Adresse IP
+
/**
IPAddress ip = { 192, 168, 1, 26 };
+
  Arbitrairement on décide que le chemin
// Masque de sous-réseau
+
  contiendra 5 partie de 10 caractères
IPAddress mask = { 255, 255, 255, 0 };
+
*/
// Passerelle
+
static const uint8_t URL_MAX_PART = 5;
IPAddress gateway = { 192, 168, 1, 254 };
+
static const uint8_t URL_PART_SIZE = 10;
// DNS
 
IPAddress server_dns = { 192, 168, 1, 254 };
 
 
/**
 
/**
  Adresse MAC du module, doit être unique sur le réseau !
+
  Arbitrairement on décide qu'il y aura
  Ici 00:01:02:03:04:05
+
  maximum 5 paramètres de 10 caractères
 
*/
 
*/
byte mac[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 };
+
static const uint8_t PARAM_MAX_NUMBER = 5;
 +
static const uint8_t PARAM_SIZE = 10;
 +
// Tableau permettant de stocker les parties du chemin
 +
char url_parts[URL_MAX_PART][URL_PART_SIZE];
 +
// Tableau permettant de stocker les paramètres
 +
char param_names[PARAM_MAX_NUMBER][PARAM_SIZE];
 +
char param_values[PARAM_MAX_NUMBER][PARAM_SIZE];
 +
 
 +
// Variables utilisée à l'exécution
 +
uint16_t http_status_code = 0;
 +
// Permet de savoir si l'URL est trouvée
 +
bool isUrl = false;
 +
// Nombre de parties du chemin
 +
uint8_t nbUrlPart = 0;
 +
// Nombre de paramètres de l'URL
 +
uint8_t nbParam = 0;
 +
// URL complète pour accéder à un fichier
 +
char url[URL_MAX_PART*URL_PART_SIZE+URL_MAX_PART];
 +
// Statut HTTP de la requête
 +
uint16_t http_error_code;
 +
</source>
  
void setup() {
+
==loop()==
  // Démarrage du port série
+
<source lang="c">
  Serial.begin(9600);
 
  Serial.println(F("Setting ip..."));
 
  /**
 
    Démarrage du shield Ethernet en spécifiant la configuration OSI 3
 
  */
 
  Ethernet.begin(mac, ip, server_dns, gateway, mask);
 
  // Affichage des informations
 
  Serial.print(F("IP : "));
 
  Serial.println(Ethernet.localIP());
 
  Serial.print(F("Mask : "));
 
  Serial.println(Ethernet.subnetMask());
 
  Serial.print(F("Gateway : "));
 
  Serial.println(Ethernet.gatewayIP());
 
  Serial.print(F("DNS : "));
 
  Serial.println(Ethernet.dnsServerIP());
 
}
 
 
void loop() {
 
void loop() {
 +
  // On écoute les connections entrantes
 +
  EthernetClient client = server.available();
 +
  // Si la connexion est établie (SYN / SYN+ACK / ACK)...
 +
  if (client) {
 +
    while (client.connected()) {
 +
      if (client.available()) {
 +
        char c = client.read();
 +
        if (!isUrl) {
 +
          if (digestURL(c)) {
 +
            isUrl = true;
 +
            // Optionnel, permet de voir ce qui à été récupéré
 +
            //displayUrlVariables();
 +
          }
 +
        }
 +
      }else{
 +
        // Réponse au client
 +
        sendHeader(client);
 +
        // Fin de la requête
 +
        break;
 +
      }
 +
    }
 +
    // On coupe la connexion
 +
    client.stop();
 +
    resetVariables();
 +
  }
 +
 +
</source>
 +
 +
==Fonctions annexes==
 +
{|
 +
|valign="top"|
 +
<source lang="c">
 +
/**
 +
  Digère l'URL en séparant le chemin et les paramètres
 +
*/
 +
bool digestURL(char c) {
 +
  static int8_t algoPart = 0, readBytes = 0, urlSize = 0;
 +
  if (algoPart == 0) {
 +
    // On vérifie que le caractère lue correspond à un caractère de la méthode
 +
    if (c == METHOD[readBytes++]) {
 +
      if (readBytes == strlen(METHOD)) {
 +
        // On vient de lire 'GET', on passe à la suite
 +
        algoPart++;
 +
        readBytes = 0;
 +
      }
 +
    } else {
 +
      //On remet le compteur à zéro
 +
      readBytes = 0;
 +
    }
 +
  } else if (algoPart == 1) {
 +
    // On doit ignorer les 2 caractères qui suivent ' /'
 +
    if (readBytes < 1) {
 +
      readBytes++;
 +
    } else {
 +
      // Les deux caractères sont passés, on passe à la suite
 +
      algoPart++;
 +
      readBytes = 0;
 +
    }
 +
  } else if (c == ' ' || c == '\n') {
 +
    // On à terminé la lecture de l'URL, RAZ des variables
 +
    if (algoPart == 2) {
 +
      // On termine la chaîne
 +
      url_parts[nbUrlPart][readBytes] = '\0';
 +
      url[urlSize] = '\0';
 +
      nbUrlPart++;
 +
    } else if (algoPart == 4) {
 +
      // On termine la chaîne
 +
      param_values[nbParam][readBytes] = '\0';
 +
      nbParam++;
 +
    }
 +
    algoPart = -1;
 +
  } else if (algoPart == 2) {
 +
    // Lecture des parties du chemin
 +
    if (c == '/') {
 +
      // On termine la chaîne
 +
      url_parts[nbUrlPart][readBytes] = '\0';
 +
      url[urlSize++] = '/';
 +
      /**
 +
        On passe à la lecture de la partie suivante
 +
        si le nombre max de parties n'est pas atteint
 +
      */
 +
      if (nbUrlPart == URL_MAX_PART - 1) {
 +
        // Erreur 414 (Request-URI Too Long)
 +
        http_status_code = 414;
 +
        algoPart = -1;
 +
      } else {
 +
        nbUrlPart++;
 +
        readBytes = 0;
 +
      }
 +
    } else if (c == '?') {
 +
      // On termine la chaîne
 +
      url_parts[nbUrlPart][readBytes] = '\0';
 +
      url[urlSize] = '\0';
 +
      // On a terminé la lecture du chemin et il y a des paramètres
 +
      nbUrlPart++;
 +
      algoPart++;
 +
      readBytes = 0;
 +
    } else {
 +
      /**
 +
        On ajoute le caractère à la partie si on
 +
        a pas atteint le nombre max de caractères
 +
      */
 +
      if (readBytes ==  URL_PART_SIZE - 1) {
 +
        // Erreur 413 (Request Entity Too Large)
 +
        http_status_code = 413;
 +
        algoPart = -1;
 +
      } else {
 +
        url_parts[nbUrlPart][readBytes++] = c;
 +
        url[urlSize++] = c;
 +
      }
 +
    }
 +
  } else if (algoPart == 3) {
 +
    // Lecture des noms de paramètres
 +
    if (c == '=') {
 +
      // On termine la chaîne
 +
      param_names[nbParam][readBytes] = '\0';
 +
      // On passe à la lecture de la valeur
 +
      algoPart++;
 +
      readBytes = 0;
 +
    } else {
 +
      /**
 +
        On ajoute le caractère au nom si on
 +
        a pas atteint le nombre max de caractères
 +
      */
 +
      if (readBytes == PARAM_SIZE - 1) {
 +
        // Erreur 413 (Request Entity Too Large)
 +
        http_status_code = 413;
 +
        algoPart = -1;
 +
      } else {
 +
        param_names[nbParam][readBytes++] = c;
 +
      }
 +
    }
 +
  } else if (algoPart == 4) {
 +
    // Lecture des valeurs de paramètres
 +
    if (c == '&') {
 +
      // On termine la chaîne
 +
      param_values[nbParam][readBytes] = '\0';
 +
      /**
 +
        On passe à la lecture du nom du paramètre suivant
 +
        si le nombre max de paramètres n'est pas atteint
 +
      */
 +
      if (nbParam == PARAM_MAX_NUMBER - 1) {
 +
        // Erreur 414 (Request-URI Too Long)
 +
        http_status_code = 414;
 +
        algoPart = -1;
 +
      } else {
 +
        nbParam++;
 +
        algoPart--;
 +
        readBytes = 0;
 +
      }
 +
    } else {
 +
      /**
 +
        On ajoute le caractère à la valeur si on
 +
        a pas atteint le nombre max de caractères
 +
      */
 +
      if (readBytes == PARAM_SIZE - 1) {
 +
        // Erreur 413 (Request Entity Too Large)
 +
        http_status_code = 413;
 +
        algoPart = -1;
 +
      } else {
 +
        param_values[nbParam][readBytes++] = c;
 +
      }
 +
    }
 +
  }
 +
  if (algoPart == -1) {
 +
    // RAZ des variables et fin !
 +
    algoPart = 0;
 +
    readBytes = 0;
 +
    urlSize = 0;
 +
    return true;
 +
  }
 +
  return false;
 
}
 
}
 
</source>
 
</source>
Le même résultat que précédement devrait s'afficher dans le terminal série.
+
|valign="top"|
 
 
=== Adressage au choix ! ===
 
Le '''must''', c'est de pouvoir choisir en fonction du réseau ou on va placer notre montage ! Le code ci-dessous utilise la compilation conditionnelle pour faire cohabiter les deux codes précédents. Le choix se fera en fonction de la variable ''DHCP'' positionner au tout début du sketch.
 
 
<source lang="c">
 
<source lang="c">
#include <SPI.h>
 
#include <Ethernet.h>
 
 
/**
 
/**
   Variable permettant de choisir entre une assignation
+
   Remet à zéro les variables pour la requête suivante
  fixe ou dynamique du niveau 3 OSI
 
  '0' --> configuration statique
 
  '1' --> configuration dynamique
 
 
*/
 
*/
#define DHCP 0
+
void resetVariables() {
 +
  isUrl = false;
 +
  http_status_code = 200;
 +
  nbUrlPart = 0;
 +
  nbParam = 0;
 +
}
 
/**
 
/**
   Adresse MAC du module, doit être unique sur le réseau !
+
   Envoie l'entête HTTP
  Ici 00:01:02:03:04:05
 
 
*/
 
*/
byte mac[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 };
+
void sendHeader(EthernetClient client) {
#if DHCP == 0
+
  // Envoie de l'entête HTTP
// Adresse IP
+
  client.print(F("HTTP/1.1 "));
IPAddress ip = { 192, 168, 1, 26 };
+
  client.println(http_status_code);
// Masque de sous-réseau
+
  // Ajouter ici les codes HTTP
IPAddress mask = { 255, 255, 255, 0 };
+
  if (http_status_code == 200) {
// Passerelle
+
    client.println(F(" OK"));
IPAddress gateway = { 192, 168, 1, 254 };
+
  } else if (http_status_code == 201) {
// DNS
+
    client.println(F(" Created"));
IPAddress server_dns = { 192, 168, 1, 254 };
+
  } else if (http_status_code == 204) {
#endif
+
    client.println(F(" No Content"));
 
+
   } else if (http_status_code == 400) {
void setup() {
+
    client.println(F(" Bad Request"));
  // Démarrage du port série
+
   } else if (http_status_code == 404) {
  Serial.begin(9600);
+
    client.println(F(" Not Found"));
   /**
+
  } else if (http_status_code == 414) {
    Démarrage du shield Ethernet sans spécifier d'adresse IP
+
    client.println(F(" Request-URI Too Long"));
    Cela oblige le contrôleur Ethernet à demander une configuration OSI 3
+
   } else if (http_status_code == 415) {
  */
+
     client.println(F(" Request Entity Too Large"));
#if DHCP == 0
+
  } else if (http_status_code == 500) {
  Serial.println(F("Setting ip..."));
+
     client.println(F(" Internal Server Error"));
   Ethernet.begin(mac, ip, server_dns, gateway, mask);
 
#else
 
  Serial.println(F("Requesting ip..."));
 
   if (Ethernet.begin(mac) == 0) {
 
     // Si c'est un échec, pas la peine de pousuivre...
 
    Serial.println(F("DHCP failure !"));
 
     while (true);
 
 
   }
 
   }
#endif
+
   // Format de la réponse
   // Affichage des informations obtenues
+
   client.println(F("Content-Type: text/html"));
   Serial.print(F("IP : "));
+
   // On prévient le client que la connexion est fermée
   Serial.println(Ethernet.localIP());
+
   client.println(F("Connection: close"));
   Serial.print(F("Mask : "));
+
   // Saut de ligne qui sépare le header du body
   Serial.println(Ethernet.subnetMask());
+
   client.println();
  Serial.print(F("Gateway : "));
+
   // On attend que le client reçoive la réponse
   Serial.println(Ethernet.gatewayIP());
+
   delay(1);
   Serial.print(F("DNS : "));
 
   Serial.println(Ethernet.dnsServerIP());
 
 
}
 
}
void loop() {
+
/**
 +
  Affiche ce qui à été récupéré
 +
*/
 +
void displayUrlVariables() {
 +
  if (http_error_code < 200 || http_error_code >= 300) {
 +
    Serial.print(F("Error : "));
 +
    Serial.println(http_error_code);
 +
  } else {
 +
    for (uint8_t i = 0; i < nbUrlPart; i++) {
 +
      Serial.print(url_parts[i]);
 +
      if (i < nbUrlPart - 1) {
 +
        Serial.print(F(" / "));
 +
      }
 +
    }
 +
    Serial.println();
 +
    for (uint8_t i = 0; i < nbParam; i++) {
 +
      Serial.print(param_names[i]);
 +
      Serial.print(F("="));
 +
      Serial.print(param_values[i]);
 +
      if (i < nbParam - 1) {
 +
        Serial.print(F(" & "));
 +
      }
 +
    }
 +
  }
 
}
 
}
 
</source>
 
</source>
Lorsque vous avez plusieurs possibilités n'hésitez pas à user de la compilation conditionnelle car, la mémoire réduite de l'ATMega ne permet pas d'embarquer beaucoup de code...
+
|}
  
== Ethernet : utilisation d'un serveur ==
+
Et voila, on peut maintenant récupérer la requête. Ci dessous un exemple de ce qui s'affiche dans la console avec l'URL suivante :
Il est possible de démarrer un serveur qui écoutera sur un port précis. Dans notre cas de figure, le serveur utilisera le port ''TCP 80''. Dans un premier temps, nous allons récupérer la requête qui vient du client pour voir comment elle est formatée. Dans un deuxième temps, on verra comment on pourra traiter et formuler une réponse.
+
<pre>
=== Récupération de la requête ===
+
http://192.168.1.26/part1/part2/part3/part4/index.htm?param1=value1&param2=value2&param3=value3&param4=value4&param5=value5
 +
</pre>
 +
 
 +
<pre>
 +
part1 / part2 / part3 / part4 / index.htm
 +
param1=value1 & param2=value2 & param3=value3 & param4=value4 & param5=value5
 +
</pre>
 +
 
 +
= Servir une page Web =
 +
Pour que notre serveur soit achevé, il faut qu'il serve un fichier ''HTML / CCS / JavaScript'' présent sur la carte SD.
 +
==Imports==
 +
<source lang="c">
 +
#include "SD.h"
 +
</source>
 +
==setup()==
 +
Sous la ligne :
 +
<source lang="c">
 +
Serial.begin(9600);
 +
</source>
 +
Ajoutez :
 
<source lang="c">
 
<source lang="c">
// !!!! Idem sketch précédent !!!! //
+
// Accès au module SD
// Serveur écoutant sur le port 80
+
if(!SD.begin(4)){
EthernetServer server(80);
+
   Serial.println(F("SD module problem..."));
void setup() {
+
   while(true);
   // !!!! Idem sketch précédent !!!! //
 
   // Démarrage du serveur
 
  server.begin();
 
 
}
 
}
void loop() {
+
</source>
  // On écoute les connections entrantes
+
==loop()==
  EthernetClient client = server.available();
+
Remplacez la ligne :
  // Si la connection est établie (SYN / SYN+ACK / ACK)...
+
<source lang="c">
  if (client) {
+
sendHeader(client);
    Serial.println(F("---- new request ----"));
+
</source>
     // ...pendant que le client maintient la session TCP...
+
par :
     while (client.connected()) {
+
<source lang="c">
      // ...et que la requête contient des caractères...
+
sendContent(client);
      if (client.available()) {
+
</source>
        // ...on récupére les caractères...
+
==Fonctions annexes==
        char c = client.read();
+
<source lang="c">
        // ... et on les affiche sur le terminal série
+
/**
         Serial.print(c);
+
  Envoie la page Web demandé par le client
 +
*/
 +
void sendContent(EthernetClient client) {
 +
  if (!SD.exists(url)) {
 +
     // Le fichier n'existe pas, erreur 404
 +
    http_status_code = 404;
 +
     sendHeader(client);
 +
  } else {
 +
    // Le fichier existe on l'ouvre en lecture
 +
    File f = SD.open(url, FILE_READ);
 +
    if (!f) {
 +
      // Echec de l'ouverture, erreur 500
 +
      http_status_code = 500;
 +
      sendHeader(client);
 +
    } else {
 +
      sendHeader(client);
 +
      // Lecture du fichier et envoie au client
 +
      while (f.available()) {
 +
         client.print((char)f.read());
 
       }
 
       }
 
     }
 
     }
     // Fin de la requête
+
     f.close();
    Serial.println(F(""));
 
    Serial.println(F("---- end request ----"));
 
 
   }
 
   }
 +
  // On attend que le client reçoive la réponse
 +
  delay(1);
 
}
 
}
 
</source>
 
</source>
Lorsque l'on entre dans la barre de recherche du navigateur l'adresse IP du module Ethernet on a, après le ''timeout'' TCP, le résultat suivant :
+
==Test==
<pre>
+
Sur la carte SD, on place le fichier ''index.htm'' (convention 8.3) qui contient le code suivant :
---- new request ----
+
<source lang="html5" style="font-size:120%">
GET / HTTP/1.1
+
<!DOCTYPE HTML>
Host: 192.168.1.26
+
<html>
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:50.0) Gecko/20100101 Firefox/50.0
+
<head>
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+
<meta charset="utf-8">
Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3
+
<title>W5100 on Arduino Mega 2560</title>
Accept-Encoding: gzip, deflate
+
</head>
Connection: keep-alive
+
<body>
Upgrade-Insecure-Requests: 1
+
It Works !
 +
</body>
 +
</html>
 +
</source>
 +
On peut y accéder avec un navigateur web.  
 +
 
 +
Attention, certaines fonctionnalité HTML ne fonctionnent pas ou sont dégradées :  
 +
* les attributs ''src'' vont amorcer une deuxième session TCP qui va générer de la latence car seulement une session à la fois est gérée par l'Arduino.
 +
[[Fichier:Http latency multi tcp session.jpg|centré|900px]]
 +
* les fichiers binaires doivent être pris en compte en fonction de leurs extension, notamment les images.
 +
 
 +
=Web Service pour manipuler les broches=
 +
==Principe==
 +
Le principe est simple, grâce à une URL on va manipuler des broches de l'Arduino. Il faut prendre en compte les appareils mobile et la tendance qu'ils ont de recharger les pages lorsque l'on bascule entre applications (économie de mémoire). L'appel du Web Service se fera de manière asynchrone en JavaScript grâce à l'utilisation d'[https://fr.wikipedia.org/wiki/Ajax_(informatique) AJAX].
 +
===API : url===
 +
Le Web Service utilisera l'URL de base ''ws'' et les fonctionnalités suivantes seront accessibles avec :
 +
* lecture digitale : ''dr''
 +
* écriture digitale : ''dw''
 +
* impulsion digitale (contacteurs secs) : ''di''
 +
* lecture analogique : ''ar''
 +
* écriture analogique (PWM) : ''aw''
 +
* changement d'état de la broche (pinMode) : ''pm''
 +
Il faudra également utiliser le paramètre ''s'' pour spécifier l'état dans le cas de l'écriture digitale et analogique et du changement d'état d'une broche.
 +
 
 +
===API : utilisation===
 +
<div align="center">
 +
{|class="wikitable" width="65%"
 +
! URI !! Sémantique !! Code réponse !! Corps
 +
|-
 +
|| GET /ws/dw/1?s=1
 +
|| Écrit un état haut sur la broche 1
 +
|align="center"| 204 No Content
 +
|| vide
 +
|-
 +
|| GET /ws/di/1
 +
|| Envoie une impulsion sur la broche 1
 +
|align="center"| 204 No Content
 +
|| vide
 +
|-
 +
|| GET /ws/dr/1
 +
|| Lit l'état de la broche 1
 +
|align="center"| 200 OK
 +
|| 1 ou 0
 +
|-
 +
|| GET /ws/aw/3?s=200
 +
|| Écrit sur la broche 3
 +
|align="center"| 204 No Content
 +
|| vide
 +
|-
 +
|| GET /ws/ar/1
 +
|| Lit l'état de la broche A1
 +
|align="center"| 200 OK
 +
|| Entre 0 et 255
 +
|-
 +
|| GET /ws/pm/3?s=0<br>GET /ws/pm/3?s=1<br>GET /ws/pm/3?s=2
 +
|| Passe la broche 1 en OUTPUT<br>Passe la broche 1 en INPUT<br>Passe la broche 1 en INPUT_PULLUP
 +
|align="center"| 204 No Content
 +
|| vide
 +
|}
 +
</div>
  
 +
Il faut bien penser à préserver les broches utilisées pour la communication avec le shield Ethernet (broches 4, 10, 11, 12, 13).
  
---- end request ----
+
==Implémentation==
</pre>
+
===Variables globales===
On constate que :
+
<source lang="c">
* la requête démarre par le verbe HTTP (ici ''GET'') suivit de l'URL (ici ''/'') et du protocole (ici ''HTTP/1.1'') ;
+
// Broches à ne pas modifier, utilisées pour le shield
* la requête contient toutes les entêtes envoyés par le navigateur ;
+
int8_t unmodifiablePins[] = { 4, 10 , 11, 12, 13 };
* la requête se termine par deux sauts de ligne.
+
// URL utilisées
 +
static const char WEB_SERVICE_BASE_URL[] = "ws";
 +
static const char WEB_SERVICE_DR_URL[] = "dr";
 +
static const char WEB_SERVICE_DW_URL[] = "dw";
 +
static const char WEB_SERVICE_DWT_URL[] = "dwt";
 +
static const char WEB_SERVICE_AR_URL[] = "ar";
 +
static const char WEB_SERVICE_AW_URL[] = "aw";
 +
static const char WEB_SERVICE_PM_URL[] = "pm";
 +
// Paramètres du Web Service
 +
static const char WEB_SERVICE_PARAM_STATE[] = "s";
 +
</source>
  
Pour terminer proprement la requête ''HTTP'', il suffit de mettre fin à la session TCP à réception des deux sauts de ligne.
+
===loop()===
 +
Modifiez la ligne :
 +
<source lang="c">
 +
sendContent(client);
 +
</source>
 +
Par :
 +
<source lang="c">
 +
if (strcmp(url_part[0], WEB_SERVICE_BASE_URL) == 0) {
 +
  // Appel d'un Web Service
 +
  processWS(client);
 +
} else {
 +
  // Servir une page web
 +
  sendContent(client);
 +
}
 +
</source>
  
=== Traitement de fin de requête ===
+
===Fonctions annexes===
On va écrire un programme qui ''attend'' le saut de ligne (''\n'') à la fin d'une ligne vide.
+
{|
 +
|valign="top"|
 
<source lang="c">
 
<source lang="c">
// !!!! idem sketch précédent !!!! //
+
/**
void loop() {
+
  Fonction traitant l'appel d'un Web Service
   // On écoute les connections entrantes
+
*/
  EthernetClient client = server.available();
+
void processWS(EthernetClient client) {
   // Si la connection est établie (SYN / SYN+ACK / ACK)...
+
   uint16_t pinValue = 0;
  if (client) {
+
  if (nbUrlPart < 2) {
    // Nos deux compteurs
+
    // Nombre insuffisant de parties dans l'URL
    uint8_t nbNewLine = 0, bytesOnLine = 0;
+
    http_status_code = 400;
     while (client.connected()) {
+
   } else {
       if (client.available()) {
+
    // On test si la broche fait partie de celles utilisées par le shield
         char c = client.read();
+
    uint8_t pin = atoi(url_parts[2]);
        if (c != '\r') {
+
    if (!isPinModifiable(pin)) {
          if (c == '\n') {
+
      // Broche non modifiable !
            // Fin de ligne
+
      http_status_code = 400;
            if (bytesOnLine == 0) {
+
     } else {
              // Ligne vide on incrémente le conteur
+
      // Lecture de l'action demandée
              nbNewLine++;
+
       if (strcmp(url_parts[1], WEB_SERVICE_AR_URL) == 0) {
              if (nbNewLine == 1) {
+
         // Lecture analogique
                // Deuxième ligne vide = fin requête
+
        pinValue = analogRead(pin);
                // Envoie du status HTTP, ici '200 OK'
+
      } else if (strcmp(url_parts[1], WEB_SERVICE_DR_URL) == 0) {
                client.println("HTTP/1.1 200 OK");
+
        // Lecture numérique
                // Entête spécifiant le contenu du corps
+
        pinValue = digitalRead(pin);
                client.println("Content-Type: text/html");
+
      } else if (strcmp(url_parts[1], WEB_SERVICE_DI_URL) == 0) {
                /**
+
        // Impulsion numérique
                  On prévient le client qu'à la fin de
+
        http_status_code = 204;
                  la requête, on coupe la session TCP
+
        pinMode(pin, OUTPUT);
                */
+
        digitalWrite(pin, HIGH);
                client.println("Connection: close");
+
        delay(DIGITAL_IMPULSE_TIME);
                // Spération entre les entêtes HTTP et le corps du message
+
        digitalWrite(pin, LOW);
                client.println();
+
      } else {
                // contenu HTML
+
        http_status_code = 204;
                client.println("<!DOCTYPE HTML>");
+
        // Récupération de l'état
                client.println("<html>");
+
        int16_t state = getIntParameter(WEB_SERVICE_PARAM_STATE);
                client.println("It Works !");
+
        if (state == -1) {
                client.println("</html>");
+
          // Abscence du paramètre obligatoire ''state''
                // On donne le temps au navigateur de traiter le message
+
          http_status_code = 400;
                delay(1);
+
        } else if (strcmp(url_parts[1], WEB_SERVICE_DW_URL) == 0) {
                // Fermeture de la session TCP
+
          // Ecriture numérique
                client.stop();
+
          if (state == 0 || state == 1) {
                break;
+
            pinMode(pin, OUTPUT);
              }
+
            digitalWrite(pin, state);
            } else {
+
          } else {
              // Ligne contenant des caractères, on remet le conteur à zéro
+
            // Valeurs hors limite !
              bytesOnLine = 0;
+
            http_status_code = 400;
             }
+
          }
 +
        } else if (strcmp(url_parts[1], WEB_SERVICE_PM_URL) == 0) {
 +
          // Changement d'état de broche
 +
          if (state == 0) {
 +
            pinMode(pin, OUTPUT);
 +
          } else if (state == 1) {
 +
            pinMode(pin, INPUT);
 +
          } else if (state == 2) {
 +
            pinMode(pin, INPUT_PULLUP);
 +
          } else {
 +
            // Valeurs hors limite !
 +
            http_status_code = 400;
 +
          }
 +
        } else if (strcmp(url_parts[1], WEB_SERVICE_AW_URL) == 0) {
 +
          // Ecriture analogique (PWM)
 +
          if (state > 0 && state <= 255) {
 +
            pinMode(pin, OUTPUT);
 +
             analogWrite(pin, state);
 
           } else {
 
           } else {
             // On incrémente le conteur de caractères
+
             // Valeurs hors limite !
             bytesOnLine++;
+
             http_status_code = 400;
 
           }
 
           }
 +
        } else {
 +
          // L'action demandée n'existe pas !
 +
          http_status_code = 400;
 
         }
 
         }
 
       }
 
       }
 
     }
 
     }
     // Fin de la requête
+
  }
     Serial.println(F(""));
+
  if (http_status_code == 200) {
     Serial.println(F("---- end request ----"));
+
     // On envoie le header et la valeur
 +
     sendHeader(client);
 +
     client.print(pinValue);
 +
  } else {
 +
    // On envoie simplement le header
 +
    sendHeader(client);
 
   }
 
   }
 
}
 
}
 
</source>
 
</source>
A partir de maintenant, côté navigateur, la requête se termine proprement et on attend plus le ''timeout'' TCP. Il faudrait maintenant, ''digérer'' l'URL pour pouvoir répondre en fonction de la requête !
+
|valign="top"|
 
 
=== Digestion de l'URL ===
 
 
<source lang="c">
 
<source lang="c">
 +
/**
 +
  Vérifie si la broche fait partie de celles utilisées par le shield
 +
*/
 +
bool isPinModifiable(uint8_t pin) {
 +
  for (uint8_t i = 0; i < (sizeof(unmodifiablePins) / sizeof(uint8_t)); i++) {
 +
    if (unmodifiablePins[i] == pin) {
 +
      return false;
 +
    }
 +
  }
 +
  return true;
 +
}
 +
/**
 +
  Récupére la valeur du paramètre et la convertie en entier
 +
  Retourne -1 si le paramètre n'existe pas
 +
*/
 +
int16_t getIntParameter(char paramName[]) {
 +
  char* value = getParameter(paramName);
 +
  if (strlen(value) == 0) {
 +
    return -1;
 +
  }
 +
  return atoi(value);
 +
}
 +
/**
 +
  Récupére la valeur du paramètre
 +
  Retourne une chaîne vide si le paramètre n'existe pas
 +
*/
 +
int16_t getParameter(char paramName[]) {
 +
  for (uint8_t i = 0; i < nbParam; i++) {
 +
    if (strcmp(param_names[i], paramName) == 0) {
 +
      // On récupére la valeur
 +
      return param_values[i];
 +
    }
 +
  }
 +
  return "";
 +
}
 
</source>
 
</source>
 +
|}
 +
===Test===
 +
On peut tester notre Web Service avec le plugin [https://addons.mozilla.org/fr/firefox/addon/restclient/ RESTClient] sur Firefox ou avec un peu d'AJAX !
  
== Utilisation de la carte SD ==
+
La page suivante permet d'allumer et d'éteindre une LED sur la broche 8.
=== Affichage des paramètres de la carte ===
+
 
=== Lecture ===
+
Contenu de la carte SD :
=== Écriture ===
+
{|
=== Ajout ===
+
|valign="top"|
=== Effacement ===
+
* Le fichier index.htm à la racine :
 +
<source lang="html5" style="font-size:120%">
 +
<!DOCTYPE HTML>
 +
<html>
 +
  <head>
 +
    <meta charset="utf-8">
 +
    <title>Web Services with W5100 and Arduino</title>
 +
    <script type="text/javascript" src="js/script.js"></script>
 +
  </head>
 +
  <body>
 +
    <table>
 +
      <tr>
 +
        <td>
 +
          <button onClick="digitalWrite(8, 1)">ON</button>
 +
        </td>
 +
        <td>
 +
          <button onClick="digitalWrite(8, 0)">OFF</button>
 +
        </td>
 +
      </tr>
 +
    </table>
 +
  </body>
 +
</html>
 +
</source>
 +
|valign="top"|
 +
* Le fichier script.js dans le répertoire js :
 +
<source lang="javascript">
 +
function getXhr() {
 +
  var xhr;
 +
  if (window.XMLHttpRequest) {
 +
    // code for IE7+, Firefox, Chrome, Opera, Safari
 +
    xhr = new XMLHttpRequest();
 +
  } else {
 +
    // code for IE6, IE5
 +
    xhr = new ActiveXObject("Microsoft.XMLHTTP");
 +
  }
 +
  return xhr;
 +
}
 +
function digitalWrite(pin, state) {
 +
  // Création de l'objet XHR
 +
  var xhr = getXhr();
 +
  // Ouverture de l'URL en GET avec l'identifiant
 +
  xhr.open("GET", "/ws/dw/"+pin+"?s="+state, false);
 +
  // Envoie de la requête
 +
  xhr.send();
 +
}
 +
</source>
 +
|}

Version actuelle datée du 5 octobre 2020 à 10:16

Introduction

Warning manual.jpg

Il faut prendre connaissance du module W5100 et s'assurer que le module possède une configuration de niveau 3 OSI.

Soyez sûr de comprendre la section sur comment écrire un sketch avant de poursuivre. Le code ci-dessous fait référence à des parties bien spécifiques, détaillées et expliquées dans la section suscitée.

Récupération de la requête

Il est possible de démarrer un serveur qui écoutera sur un port précis qui, dans notre cas de figure, utilisera le port TCP 80. Dans un premier temps, nous allons récupérer la requête qui vient du client pour voir comment elle est formatée puis, dans un deuxième temps, on verra comment on pourra traiter et formuler une réponse.

Imports

#include "SPI.h"
#include "Ethernet.h"

Variables globales

// Serveur écoutant sur le port 80
EthernetServer server(80);

setup()

// Démarrage du serveur
server.begin();

loop()

// On écoute les connections entrantes
EthernetClient client = server.available();
// Si la connection est établie (SYN / SYN+ACK / ACK)...
if (client) {
  Serial.println(F("---- new request ----"));
  // ...pendant que le client maintient la session TCP...
  while (client.connected()) {
    // ...et que la requête contient des caractères...
    if (client.available()) {
      // ...on récupére les caractères...
      char c = client.read();
      // ... et on les affiche sur le terminal série
      Serial.print(c);
    }
  }
  // Fin de la requête
  Serial.println(F(""));
  Serial.println(F("---- end request ----"));
}

Lorsque l'on entre dans la barre de recherche du navigateur l'adresse IP du module Ethernet on a, après le timeout TCP, le résultat suivant :

---- new request ----
GET /index.htm&param1=value1 HTTP/1.1
Host: 192.168.1.26
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:50.0) Gecko/20100101 Firefox/50.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1


---- end request ----

On constate que :

  • la requête démarre par le verbe HTTP (ici GET);
  • il y a un espace est un premier slash ;
  • suivit de l'URL (ici index.htm) ;
  • les paramètres suivent le '?' ;
  • la requête contient toutes les entêtes envoyés par le navigateur ;
  • la requête se termine par deux sauts de ligne.

Pour terminer proprement la requête HTTP, il suffit de mettre fin à la session TCP à réception des deux sauts de ligne ou, mieux, d'attendre que le client n'envoie plus de caractères.

Traitement de fin de requête

loop()

// On écoute les connections entrantes
EthernetClient client = server.available();
// Si la connection est établie (SYN / SYN+ACK / ACK)...
if (client) {
  while (client.connected()) {
    if (client.available()) {
      char c = client.read();
      // Réception des caractères
    } else {
      // Envoie du code status HTTP, ici '200 OK'
      client.println("HTTP/1.1 200 OK");
      // Entête spécifiant le contenu du corps
      client.println("Content-Type: text/html");
      /**
         On prévient le client qu'à la fin de
         la requête, on coupe la session TCP
      */
      client.println("Connection: close");
      /**
         Spération entre les entêtes HTTP et le corps du message
         !! TRES IMPORTANT, SANS LE SAUT DE LIGNE LE NAVIGATEUR
         NE FAIT PAS LA SEPARTATION ENTRE HEADER ET CORPS !!
      */
      client.println();
      // Corps du message HTTP
      client.println("<!DOCTYPE HTML>");
      client.println("<html>");
      client.println("It Works !");
      client.println("</html>");
      // On donne le temps au navigateur de traiter le message
      delay(1);
      // Fermeture de la session TCP
      client.stop();
      break;
    }
  }
  // Fin de la requête
  Serial.println(F(""));
  Serial.println(F("---- end request ----"));
}

A partir de maintenant, côté navigateur, la requête se termine proprement et on attend plus le timeout TCP. Il faudrait maintenant, digérer l'URL pour pouvoir répondre en fonction de la requête !

Digestion de l'URL

Le code devient trop complexe pour résider dans la fonction principale. Il faut donc le fragmenter en plusieurs fonctions qui seront appelées dans loop().

Algorithmique

Prenons comme exemple la requête suivante :

GET /part1/part2/index.htm?paramA=valueA&...&paramX=valueX HTTP1.1

Nous allons écrire la fonction bool digestURL(char c) qui va :

  • vérifier que la ligne commence par GET ;
  • lire chaque partie du chemin (part1, part2, index.htm) ;
  • détecter le '?' pour déterminer si la requête contient des arguments ;
  • détecter les '=' pour faire la séparation entre nom de paramètre et valeur ;
  • détecter les '&' pour séparer les paramètres.

On part du principe que la fonction loop() appelle digestURL(char c) à la réception d'un caractère et renvoie vrai lorsque l'URL est lue.

Variables globales

// Méthode démarrant le début de l'URL
static const char METHOD[] = "GET";
/**
  Arbitrairement on décide que le chemin
  contiendra 5 partie de 10 caractères
*/
static const uint8_t URL_MAX_PART = 5;
static const uint8_t URL_PART_SIZE = 10;
/**
  Arbitrairement on décide qu'il y aura
  maximum 5 paramètres de 10 caractères
*/
static const uint8_t PARAM_MAX_NUMBER = 5;
static const uint8_t PARAM_SIZE = 10;
// Tableau permettant de stocker les parties du chemin
char url_parts[URL_MAX_PART][URL_PART_SIZE];
// Tableau permettant de stocker les paramètres
char param_names[PARAM_MAX_NUMBER][PARAM_SIZE];
char param_values[PARAM_MAX_NUMBER][PARAM_SIZE];

// Variables utilisée à l'exécution
uint16_t http_status_code = 0;
// Permet de savoir si l'URL est trouvée
bool isUrl = false;
// Nombre de parties du chemin
uint8_t nbUrlPart = 0;
// Nombre de paramètres de l'URL
uint8_t nbParam = 0;
// URL complète pour accéder à un fichier
char url[URL_MAX_PART*URL_PART_SIZE+URL_MAX_PART];
// Statut HTTP de la requête
uint16_t http_error_code;

loop()

void loop() {
  // On écoute les connections entrantes
  EthernetClient client = server.available();
  // Si la connexion est établie (SYN / SYN+ACK / ACK)...
  if (client) {
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();
        if (!isUrl) {
          if (digestURL(c)) {
            isUrl = true;
            // Optionnel, permet de voir ce qui à été récupéré
            //displayUrlVariables();
          }
        }
      }else{
        // Réponse au client
        sendHeader(client);
        // Fin de la requête
        break;
      }
    }
    // On coupe la connexion
    client.stop();
    resetVariables();
  }
}

Fonctions annexes

/**
   Digère l'URL en séparant le chemin et les paramètres
*/
bool digestURL(char c) {
  static int8_t algoPart = 0, readBytes = 0, urlSize = 0;
  if (algoPart == 0) {
    // On vérifie que le caractère lue correspond à un caractère de la méthode
    if (c == METHOD[readBytes++]) {
      if (readBytes == strlen(METHOD)) {
        // On vient de lire 'GET', on passe à la suite
        algoPart++;
        readBytes = 0;
      }
    } else {
      //On remet le compteur à zéro
      readBytes = 0;
    }
  } else if (algoPart == 1) {
    // On doit ignorer les 2 caractères qui suivent ' /'
    if (readBytes < 1) {
      readBytes++;
    } else {
      // Les deux caractères sont passés, on passe à la suite
      algoPart++;
      readBytes = 0;
    }
  } else if (c == ' ' || c == '\n') {
    // On à terminé la lecture de l'URL, RAZ des variables
    if (algoPart == 2) {
      // On termine la chaîne
      url_parts[nbUrlPart][readBytes] = '\0';
      url[urlSize] = '\0';
      nbUrlPart++;
    } else if (algoPart == 4) {
      // On termine la chaîne
      param_values[nbParam][readBytes] = '\0';
      nbParam++;
    }
    algoPart = -1;
  } else if (algoPart == 2) {
    // Lecture des parties du chemin
    if (c == '/') {
      // On termine la chaîne
      url_parts[nbUrlPart][readBytes] = '\0';
      url[urlSize++] = '/';
      /**
         On passe à la lecture de la partie suivante
         si le nombre max de parties n'est pas atteint
      */
      if (nbUrlPart == URL_MAX_PART - 1) {
        // Erreur 414 (Request-URI Too Long)
        http_status_code = 414;
        algoPart = -1;
      } else {
        nbUrlPart++;
        readBytes = 0;
      }
    } else if (c == '?') {
      // On termine la chaîne
      url_parts[nbUrlPart][readBytes] = '\0';
      url[urlSize] = '\0';
      // On a terminé la lecture du chemin et il y a des paramètres
      nbUrlPart++;
      algoPart++;
      readBytes = 0;
    } else {
      /**
         On ajoute le caractère à la partie si on
         a pas atteint le nombre max de caractères
      */
      if (readBytes ==  URL_PART_SIZE - 1) {
        // Erreur 413 (Request Entity Too Large)
        http_status_code = 413;
        algoPart = -1;
      } else {
        url_parts[nbUrlPart][readBytes++] = c;
        url[urlSize++] = c;
      }
    }
  } else if (algoPart == 3) {
    // Lecture des noms de paramètres
    if (c == '=') {
      // On termine la chaîne
      param_names[nbParam][readBytes] = '\0';
      // On passe à la lecture de la valeur
      algoPart++;
      readBytes = 0;
    } else {
      /**
         On ajoute le caractère au nom si on
         a pas atteint le nombre max de caractères
      */
      if (readBytes == PARAM_SIZE - 1) {
        // Erreur 413 (Request Entity Too Large)
        http_status_code = 413;
        algoPart = -1;
      } else {
        param_names[nbParam][readBytes++] = c;
      }
    }
  } else if (algoPart == 4) {
    // Lecture des valeurs de paramètres
    if (c == '&') {
      // On termine la chaîne
      param_values[nbParam][readBytes] = '\0';
      /**
         On passe à la lecture du nom du paramètre suivant
         si le nombre max de paramètres n'est pas atteint
      */
      if (nbParam == PARAM_MAX_NUMBER - 1) {
        // Erreur 414 (Request-URI Too Long)
        http_status_code = 414;
        algoPart = -1;
      } else {
        nbParam++;
        algoPart--;
        readBytes = 0;
      }
    } else {
      /**
         On ajoute le caractère à la valeur si on
         a pas atteint le nombre max de caractères
      */
      if (readBytes == PARAM_SIZE - 1) {
        // Erreur 413 (Request Entity Too Large)
        http_status_code = 413;
        algoPart = -1;
      } else {
        param_values[nbParam][readBytes++] = c;
      }
    }
  }
  if (algoPart == -1) {
    // RAZ des variables et fin !
    algoPart = 0;
    readBytes = 0;
    urlSize = 0;
    return true;
  }
  return false;
}
/**
   Remet à zéro les variables pour la requête suivante
*/
void resetVariables() {
  isUrl = false;
  http_status_code = 200;
  nbUrlPart = 0;
  nbParam = 0;
}
/**
   Envoie l'entête HTTP
*/
void sendHeader(EthernetClient client) {
  // Envoie de l'entête HTTP
  client.print(F("HTTP/1.1 "));
  client.println(http_status_code);
  // Ajouter ici les codes HTTP
  if (http_status_code == 200) {
    client.println(F(" OK"));
  } else if (http_status_code == 201) {
    client.println(F(" Created"));
  } else if (http_status_code == 204) {
    client.println(F(" No Content"));
  } else if (http_status_code == 400) {
    client.println(F(" Bad Request"));
  } else if (http_status_code == 404) {
    client.println(F(" Not Found"));
  } else if (http_status_code == 414) {
    client.println(F(" Request-URI Too Long"));
  } else if (http_status_code == 415) {
    client.println(F(" Request Entity Too Large"));
  } else if (http_status_code == 500) {
    client.println(F(" Internal Server Error"));
  }
  // Format de la réponse
  client.println(F("Content-Type: text/html"));
  // On prévient le client que la connexion est fermée
  client.println(F("Connection: close"));
  // Saut de ligne qui sépare le header du body
  client.println();
  // On attend que le client reçoive la réponse
  delay(1);
}
/**
   Affiche ce qui à été récupéré
*/
void displayUrlVariables() {
  if (http_error_code < 200 || http_error_code >= 300) {
    Serial.print(F("Error : "));
    Serial.println(http_error_code);
  } else {
    for (uint8_t i = 0; i < nbUrlPart; i++) {
      Serial.print(url_parts[i]);
      if (i < nbUrlPart - 1) {
        Serial.print(F(" / "));
      }
    }
    Serial.println();
    for (uint8_t i = 0; i < nbParam; i++) {
      Serial.print(param_names[i]);
      Serial.print(F("="));
      Serial.print(param_values[i]);
      if (i < nbParam - 1) {
        Serial.print(F(" & "));
      }
    }
  }
}

Et voila, on peut maintenant récupérer la requête. Ci dessous un exemple de ce qui s'affiche dans la console avec l'URL suivante :

http://192.168.1.26/part1/part2/part3/part4/index.htm?param1=value1&param2=value2&param3=value3&param4=value4&param5=value5
part1 / part2 / part3 / part4 / index.htm
param1=value1 & param2=value2 & param3=value3 & param4=value4 & param5=value5

Servir une page Web

Pour que notre serveur soit achevé, il faut qu'il serve un fichier HTML / CCS / JavaScript présent sur la carte SD.

Imports

#include "SD.h"

setup()

Sous la ligne :

Serial.begin(9600);

Ajoutez :

// Accès au module SD
if(!SD.begin(4)){
  Serial.println(F("SD module problem..."));
  while(true);
}

loop()

Remplacez la ligne :

sendHeader(client);

par :

sendContent(client);

Fonctions annexes

/**
   Envoie la page Web demandé par le client
*/
void sendContent(EthernetClient client) {
  if (!SD.exists(url)) {
    // Le fichier n'existe pas, erreur 404
    http_status_code = 404;
    sendHeader(client);
  } else {
    // Le fichier existe on l'ouvre en lecture
    File f = SD.open(url, FILE_READ);
    if (!f) {
      // Echec de l'ouverture, erreur 500
      http_status_code = 500;
      sendHeader(client);
    } else {
      sendHeader(client);
      // Lecture du fichier et envoie au client
      while (f.available()) {
        client.print((char)f.read());
      }
    }
    f.close();
  }
  // On attend que le client reçoive la réponse
  delay(1);
}

Test

Sur la carte SD, on place le fichier index.htm (convention 8.3) qui contient le code suivant :

<!DOCTYPE HTML>
<html>
	<head>
		<meta charset="utf-8">
		<title>W5100 on Arduino Mega 2560</title>
	</head>
	<body>
		It Works !
	</body>
</html>

On peut y accéder avec un navigateur web.

Attention, certaines fonctionnalité HTML ne fonctionnent pas ou sont dégradées :

  • les attributs src vont amorcer une deuxième session TCP qui va générer de la latence car seulement une session à la fois est gérée par l'Arduino.
Http latency multi tcp session.jpg
  • les fichiers binaires doivent être pris en compte en fonction de leurs extension, notamment les images.

Web Service pour manipuler les broches

Principe

Le principe est simple, grâce à une URL on va manipuler des broches de l'Arduino. Il faut prendre en compte les appareils mobile et la tendance qu'ils ont de recharger les pages lorsque l'on bascule entre applications (économie de mémoire). L'appel du Web Service se fera de manière asynchrone en JavaScript grâce à l'utilisation d'AJAX.

API : url

Le Web Service utilisera l'URL de base ws et les fonctionnalités suivantes seront accessibles avec :

  • lecture digitale : dr
  • écriture digitale : dw
  • impulsion digitale (contacteurs secs) : di
  • lecture analogique : ar
  • écriture analogique (PWM) : aw
  • changement d'état de la broche (pinMode) : pm

Il faudra également utiliser le paramètre s pour spécifier l'état dans le cas de l'écriture digitale et analogique et du changement d'état d'une broche.

API : utilisation

URI Sémantique Code réponse Corps
GET /ws/dw/1?s=1 Écrit un état haut sur la broche 1 204 No Content vide
GET /ws/di/1 Envoie une impulsion sur la broche 1 204 No Content vide
GET /ws/dr/1 Lit l'état de la broche 1 200 OK 1 ou 0
GET /ws/aw/3?s=200 Écrit sur la broche 3 204 No Content vide
GET /ws/ar/1 Lit l'état de la broche A1 200 OK Entre 0 et 255
GET /ws/pm/3?s=0
GET /ws/pm/3?s=1
GET /ws/pm/3?s=2
Passe la broche 1 en OUTPUT
Passe la broche 1 en INPUT
Passe la broche 1 en INPUT_PULLUP
204 No Content vide

Il faut bien penser à préserver les broches utilisées pour la communication avec le shield Ethernet (broches 4, 10, 11, 12, 13).

Implémentation

Variables globales

// Broches à ne pas modifier, utilisées pour le shield
int8_t unmodifiablePins[] = { 4, 10 , 11, 12, 13 };
// URL utilisées
static const char WEB_SERVICE_BASE_URL[] = "ws";
static const char WEB_SERVICE_DR_URL[] = "dr";
static const char WEB_SERVICE_DW_URL[] = "dw";
static const char WEB_SERVICE_DWT_URL[] = "dwt";
static const char WEB_SERVICE_AR_URL[] = "ar";
static const char WEB_SERVICE_AW_URL[] = "aw";
static const char WEB_SERVICE_PM_URL[] = "pm";
// Paramètres du Web Service
static const char WEB_SERVICE_PARAM_STATE[] = "s";

loop()

Modifiez la ligne :

sendContent(client);

Par :

if (strcmp(url_part[0], WEB_SERVICE_BASE_URL) == 0) {
  // Appel d'un Web Service
  processWS(client);
} else {
  // Servir une page web
  sendContent(client);
}

Fonctions annexes

/**
   Fonction traitant l'appel d'un Web Service
*/
void processWS(EthernetClient client) {
  uint16_t pinValue = 0;
  if (nbUrlPart < 2) {
    // Nombre insuffisant de parties dans l'URL
    http_status_code = 400;
  } else {
    // On test si la broche fait partie de celles utilisées par le shield
    uint8_t pin = atoi(url_parts[2]);
    if (!isPinModifiable(pin)) {
      // Broche non modifiable !
      http_status_code = 400;
    } else {
      // Lecture de l'action demandée
      if (strcmp(url_parts[1], WEB_SERVICE_AR_URL) == 0) {
        // Lecture analogique
        pinValue = analogRead(pin);
      } else if (strcmp(url_parts[1], WEB_SERVICE_DR_URL) == 0) {
        // Lecture numérique
        pinValue = digitalRead(pin);
      } else if (strcmp(url_parts[1], WEB_SERVICE_DI_URL) == 0) {
        // Impulsion numérique
        http_status_code = 204;
        pinMode(pin, OUTPUT);
        digitalWrite(pin, HIGH);
        delay(DIGITAL_IMPULSE_TIME);
        digitalWrite(pin, LOW);
      } else {
        http_status_code = 204;
        // Récupération de l'état
        int16_t state = getIntParameter(WEB_SERVICE_PARAM_STATE);
        if (state == -1) {
          // Abscence du paramètre obligatoire ''state''
          http_status_code = 400;
        } else if (strcmp(url_parts[1], WEB_SERVICE_DW_URL) == 0) {
          // Ecriture numérique
          if (state == 0 || state == 1) {
            pinMode(pin, OUTPUT);
            digitalWrite(pin, state);
          } else {
            // Valeurs hors limite !
            http_status_code = 400;
          }
        } else if (strcmp(url_parts[1], WEB_SERVICE_PM_URL) == 0) {
          // Changement d'état de broche
          if (state == 0) {
            pinMode(pin, OUTPUT);
          } else if (state == 1) {
            pinMode(pin, INPUT);
          } else if (state == 2) {
            pinMode(pin, INPUT_PULLUP);
          } else {
            // Valeurs hors limite !
            http_status_code = 400;
          }
        } else if (strcmp(url_parts[1], WEB_SERVICE_AW_URL) == 0) {
          // Ecriture analogique (PWM)
          if (state > 0 && state <= 255) {
            pinMode(pin, OUTPUT);
            analogWrite(pin, state);
          } else {
            // Valeurs hors limite !
            http_status_code = 400;
          }
        } else {
          // L'action demandée n'existe pas !
          http_status_code = 400;
        }
      }
    }
  }
  if (http_status_code == 200) {
    // On envoie le header et la valeur
    sendHeader(client);
    client.print(pinValue);
  } else {
    // On envoie simplement le header
    sendHeader(client);
  }
}
/**
   Vérifie si la broche fait partie de celles utilisées par le shield
*/
bool isPinModifiable(uint8_t pin) {
  for (uint8_t i = 0; i < (sizeof(unmodifiablePins) / sizeof(uint8_t)); i++) {
    if (unmodifiablePins[i] == pin) {
      return false;
    }
  }
  return true;
}
/**
   Récupére la valeur du paramètre et la convertie en entier
   Retourne -1 si le paramètre n'existe pas
*/
int16_t getIntParameter(char paramName[]) {
  char* value = getParameter(paramName);
  if (strlen(value) == 0) {
    return -1;
  }
  return atoi(value);
}
/**
   Récupére la valeur du paramètre
   Retourne une chaîne vide si le paramètre n'existe pas
*/
int16_t getParameter(char paramName[]) {
  for (uint8_t i = 0; i < nbParam; i++) {
    if (strcmp(param_names[i], paramName) == 0) {
      // On récupére la valeur
      return param_values[i];
    }
  }
  return "";
}

Test

On peut tester notre Web Service avec le plugin RESTClient sur Firefox ou avec un peu d'AJAX !

La page suivante permet d'allumer et d'éteindre une LED sur la broche 8.

Contenu de la carte SD :

  • Le fichier index.htm à la racine :
<!DOCTYPE HTML>
<html>
  <head>
    <meta charset="utf-8">
    <title>Web Services with W5100 and Arduino</title>
    <script type="text/javascript" src="js/script.js"></script>
  </head>
  <body>
    <table>
      <tr>
        <td>
          <button onClick="digitalWrite(8, 1)">ON</button>
        </td>
        <td>
          <button onClick="digitalWrite(8, 0)">OFF</button>
        </td>
      </tr>
    </table>
  </body>
</html>
  • Le fichier script.js dans le répertoire js :
function getXhr() {
  var xhr;
  if (window.XMLHttpRequest) {
    // code for IE7+, Firefox, Chrome, Opera, Safari
    xhr = new XMLHttpRequest();
  } else {
    // code for IE6, IE5
    xhr = new ActiveXObject("Microsoft.XMLHTTP");
  }
  return xhr;
}
function digitalWrite(pin, state) {
  // Création de l'objet XHR
  var xhr = getXhr();
  // Ouverture de l'URL en GET avec l'identifiant
  xhr.open("GET", "/ws/dw/"+pin+"?s="+state, false);
  // Envoie de la requête
  xhr.send();
}