Serveur WEB pilotage relais

Bonjour,

J’ai réalisé un petit projet serveur WEB permettant de piloter des relais, pour manipuler et comprendre le langage de programmation arduino.

J’ai pris comme base de départ un projet de J-M-L trouvé sur ce forum : Exemple d'usage du shield Ethernet pour faire un serveur interactif
Je le remercie car ça m’a permis de mettre un pied à l’étrier. J’ai essayé de modifier tout ça à ma sauce. Hormis la partie gestion serveur NTP que j’ai récupéré tel quel d’un exemple librairie, j’ai programmé les modifications et ajouts par moi-même pour comprendre tout ça.

Le code fonctionne bien.
Je vous le présente car j’aimerai avoir des avis et des retours, est-il lisible, y-a-t-il des coquilles, des choses qui peuvent le rendre instable, des erreurs par rapport aux conventions de programmation, peut-on l’écrire autrement, l'optimiser, peut-on le rendre plus léger, moins gourmand en mémoire vive… Bref, tout avis qui pourrait me faire progresser.

Merci à vous tous sur ce forum car tous vos topics, échanges et partages sont extrêmement utiles pour des novices comme moi.

Merci pour votre avis, bonne journée à tous.

/******************************************************************************************
   Programme de communication Serveur Arduino / Client Navigateur WEB
   Celui-ci permet d'afficher :
   L'heure actuelle via un serveur NTP
   3 bouton de changement d'état pour 3 pins sorties, avec affichage de l'état actuel de chaque sortie
   1 formulaire horaire de mise à 1 et 1 formulaire horaire de mise à 0 pour chaque pin de sortie, avec affichage des horaires appliqués. Mise à 0 des sorties programmée en hard chaque 24h à minuit.
   1 bouton pour mettre toutes les sorties à 1
   1 bouton pour mettre toutes les sorties à 0
   1 bouton pour effectuer un RESET du arduino en hard (câblage sortie pin A0 sur la commande d'un transistor alimentant la pin RESET)

   Un enregistrement des états de pins et des valeurs de programmation horaire est également effectué dans l'EEPROM
   afin de conserver les données et revenir aux états présents lors d'une coupure d'alimentation du arduino.

   Synoptique de fonctionnement :
   setup()
   loop()
      horloge NTP()
      progHoraireAction()
      priseEnChargeClient()
   priseEnChargeClient()
      priseEnChargeCommande()
          analyseCommande()
              enregistrementEEPROM()
          envoieReponse()

 *******************************************************************************************/

#include <SPI.h>
#include <Ethernet.h>
#include <EEPROM.h>
#include <TimeLib.h>
#include <EthernetUdp.h>

// déclaration du serveur arduino
byte mac[] = {0x00, 0xAA, 0xBB, 0xCC, 0xDE, 0x02};
IPAddress ip(192, 168, 0, 200);
EthernetServer webServer(80);

// déclaration du tableau enregistrant la requête url pour l'analyse (+1 pour le trailing '\0'
const byte MaxCommand = 30;
char urlCommand[MaxCommand + 1];

// déclaration du serveur NTP (On se connecte à un serveur NTP pour récupérer la date et l'heure : envoie d'une requête UDP au serveur sur le port 123
// et récupération de la réponse UDP du serveur sur le port local 8888 défini en dessous
IPAddress timeServer(192, 168, 0, 6); // addresse du serveur NTP (ici l'ordinateur du réseau interne configuré en serveur NTP)
const int timeZone = 2; // faisceau horaire Europe centrale
EthernetUDP Udp;
unsigned int localPort = 8888;

// Tableaux des éléments à identifier dans le tableau urlCommand durant l'analyse de la requête
const char * labelsOfInterest[] = {"ECL", "SON", "PLA", "TAL", "TEX", "RES"};
const unsigned int maxLabelsOfInterest = sizeof(labelsOfInterest) / sizeof(char*);
const char * labelsHoraire[] = {"EON", "EOF", "SON", "SOF", "PON", "POF"};
const unsigned int maxLabelsHoraire = sizeof(labelsHoraire) / sizeof(char*);

int horaire[6];   //tableau d'enregistrement des horaires

// déclaration des PINS
const byte ECLPin = 5;
const byte SONPin = 6;
const byte PLAPin = 7;
const byte RESPin = A0;

void setup()
{
  //  Pour utiliser le bouton RESET ARDUINO du programme, il faut connecter une pin (ici A0) à la commande d'un transistor alimentant en 5v la pin Reset du arduino.
  //  La pin Reset fait un reboot du arduino lorsqu'elle est à 0. Il faut donc mettre la pin de commande transistor à 1 tant qu'on ne souhaite pas rebooter.
  //  Attention, si il y a un problème avec ce cablage et que la pin RESET se trouve à 0, le arduino va rebooter en continu dès sa mise sous tension.
  
  pinMode(RESPin, OUTPUT);
  digitalWrite(RESPin, HIGH);

  //  Déclaration des pins en sortie et initialisation en fonction de leur état enregistré dans l'EEPROM avant la dernière coupure d'alimentation du arduino.
  pinMode(ECLPin, OUTPUT);
  digitalWrite(ECLPin, EEPROM.read(0));
  pinMode(SONPin, OUTPUT);
  digitalWrite(SONPin, EEPROM.read(1));
  pinMode(PLAPin, OUTPUT);
  digitalWrite(PLAPin, EEPROM.read(2));

  Serial.begin(9600);

  Ethernet.begin(mac, ip);          // Initialise la connexion ethernet
  webServer.begin();                // démarre l'écoute du serveur sur le réseau
  Serial.print(F("*\n-> Le serveur est démarré et a comme adresse : "));
  Serial.println(Ethernet.localIP());

  Udp.begin(localPort);
  Serial.println("waiting for sync UDP");
  setSyncProvider(getNtpTime);

  Serial.println(F("état EEPROM"));
  for (int i = 0; i < 3; i++) {
    Serial.println((EEPROM.read(i)));
  }
}

/**** GESTION DE L'HORLOGE ****/

time_t prevDisplay = 0;               // instant où l'horloge est initialisé

void horlogeNTP()
{
  if (timeStatus() != timeNotSet) {
    if (now() != prevDisplay) { //update the display only if time has changed
      prevDisplay = now();
    }
  }
}

/**** COMMUNICATION AVEC LE SERVEUR NTP ****/

const int NTP_PACKET_SIZE = 48; // NTP time is in the first 48 bytes of message
byte packetBuffer[NTP_PACKET_SIZE]; //buffer to hold incoming & outgoing packets

time_t getNtpTime()
{
  while (Udp.parsePacket() > 0) ; // discard any previously received packets
  Serial.println("Transmit NTP Request");
  sendNTPpacket(timeServer);
  uint32_t beginWait = millis();
  while (millis() - beginWait < 1500) {
    int size = Udp.parsePacket();
    if (size >= NTP_PACKET_SIZE) {
      Serial.println("Receive NTP Response");
      Udp.read(packetBuffer, NTP_PACKET_SIZE);  // read packet into the buffer
      unsigned long secsSince1900;
      // convert four bytes starting at location 40 to a long integer
      secsSince1900 =  (unsigned long)packetBuffer[40] << 24;
      secsSince1900 |= (unsigned long)packetBuffer[41] << 16;
      secsSince1900 |= (unsigned long)packetBuffer[42] << 8;
      secsSince1900 |= (unsigned long)packetBuffer[43];
      return secsSince1900 - 2208988800UL + timeZone * SECS_PER_HOUR;
    }
  }
  Serial.println("No NTP Response :-(");
  return 0; // return 0 if unable to get the time
}

// send an NTP request to the time server at the given address
void sendNTPpacket(IPAddress &address)
{
  // set all bytes in the buffer to 0
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  // Initialize values needed to form NTP request
  // (see URL above for details on the packets)
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  // 8 bytes of zero for Root Delay & Root Dispersion
  packetBuffer[12]  = 49;
  packetBuffer[13]  = 0x4E;
  packetBuffer[14]  = 49;
  packetBuffer[15]  = 52;
  // all NTP fields have been given values, now
  // you can send a packet requesting a timestamp:
  Udp.beginPacket(address, 123); //NTP requests are to port 123
  Udp.write(packetBuffer, NTP_PACKET_SIZE);
  Udp.endPacket();
}

/**** GESTION EEPROM ****/

void enregistrementEEPROM()
{
  EEPROM.update (0, (digitalRead(ECLPin)));
  EEPROM.update (1, (digitalRead(SONPin)));
  EEPROM.update (2, (digitalRead(PLAPin)));

  Serial.println(F("état EEPROM"));
  for (int i = 0; i < 3; i++) {
    Serial.println(EEPROM.read(i));
  }
}

/**** GESTION DES PROGRAMMATIONS HORAIRES ****/

void progHoraireAction()
{
  if (((100 * hour()) + minute()) != 0) {       //Allumage et extinction des sorties en fonction de l'heure
    //ECLAIRAGE
    if (horaire[0] == ((100 * hour()) + minute())) digitalWrite(ECLPin, HIGH);
    if (horaire[1] == ((100 * hour()) + minute())) digitalWrite(ECLPin, LOW);
    //SONORISATION
    if (horaire[2] == ((100 * hour()) + minute())) digitalWrite(SONPin, HIGH);
    if (horaire[3] == ((100 * hour()) + minute())) digitalWrite(SONPin, LOW);
    //PLAYER
    if (horaire[4] == ((100 * hour()) + minute())) digitalWrite(PLAPin, HIGH);
    if (horaire[5] == ((100 * hour()) + minute())) digitalWrite(PLAPin, LOW);
  }
  else {   // Extinction totale à minuit
    digitalWrite(ECLPin, LOW);
    digitalWrite(SONPin, LOW);
    digitalWrite(PLAPin, LOW);
  }
}

/**** INTERPRETATION DE L'URL ENVOYE PAR LE CLIENT NAVIGATEUR WEB AU SERVEUR ARDUINO ****/

void analyseCommande()
{
  char * item, * label, * action, * heure, * minutes;
  int labelIndex;

  Serial.print("urlCommand ");
  for (int i = 0; i < MaxCommand + 1; i++) {
    Serial.print(urlCommand[i]);
  }

  item = strchr(urlCommand, '%');     // On cherche un % dans urlCommand si il y a

  if (item == NULL) {                 // Il n'y a pas de % dans urlCommand, on cherche une demande d'action
    label = strtok(urlCommand, "=");
    action = strtok(0, "H");
    for (int i = 0; i < maxLabelsOfInterest; i++) { // On compare le label présent avant le = dans urlCommand avec ceux entrés dans le tableau global
      if (!strcmp(labelsOfInterest[i], label)) {
        labelIndex = i;
        break;
      }
    }
    switch (labelIndex) {
      case 0: // ECLAIRAGE
        if (!strcmp(action, "ON")) digitalWrite(ECLPin, HIGH);  // On compare action à ON, si les deux sont égals la fonction strcmp renvoie un 0, on met la pin à 1
        else if (!strcmp(action, "OFF")) digitalWrite(ECLPin, LOW);
        break;
      case 1: // SONORISATION
        if (!strcmp(action, "ON")) digitalWrite(SONPin, HIGH);
        else if (!strcmp(action, "OFF")) digitalWrite(SONPin, LOW);
        break;
      case 2: // PLAYER
        if (!strcmp(action, "ON")) digitalWrite(PLAPin, HIGH);
        else if (!strcmp(action, "OFF")) digitalWrite(PLAPin, LOW);
        break;
      case 3 : //ALLUMAGE TOTAL
        if (!strcmp(action, "TOTAL+ON")) {
          digitalWrite(ECLPin, HIGH);
          digitalWrite(SONPin, HIGH);
          digitalWrite(PLAPin, HIGH);
        }
        break;
      case 4 :
        if (!strcmp(action, "TOTAL+OFF")) {
          digitalWrite(ECLPin, LOW);
          digitalWrite(SONPin, LOW);
          digitalWrite(PLAPin, LOW);
        }
        break;
      case 5 : //RESET arduino
        if (!strcmp(action, "RESET+ARDUINO")) digitalWrite(RESPin, LOW); //met la pin A0 à 0 donc la pin reset à 0
        delay(2);
        digitalWrite(RESPin, HIGH); //remet la pin A0 à 1 donc la pin reset à 1
        break;
    }
  }
  else {        // Il y a un % dans urlCommand, on cherche un horaire
    label = strtok(urlCommand, "=");
    heure = strtok(0, "%");
    minutes = strtok(0, "H");
    for (int i = 0; i < maxLabelsHoraire; i++) {
      if (!strcmp(labelsHoraire[i], label)) {
        horaire[i] = (100 * atoi(heure)) + atoi(minutes + 2);
        break;
      }
    }
  }

  Serial.println("tableau horaires");
  for (int i = 0; i < 6; i++) {
    Serial.println(horaire[i]);
  }
  Serial.print(F("état ECL "));
  Serial.println(digitalRead(ECLPin));
  Serial.print(F("état SON "));
  Serial.println(digitalRead(SONPin));
  Serial.print(F("état PLA "));
  Serial.println(digitalRead(PLAPin));

  enregistrementEEPROM();
}

/**** REPONSE DU SERVEUR ARDUINO AU CLIENT NAVIGATEUR WEB ****/

void envoieReponse(EthernetClient & client)           // La fonction prend un client en argument
{
  //entête de réponse standard, infos pour le navigateur
  client.println(F("HTTP/1.1 200 OK"));                 // type du HTML + réponse 200=réussite
  client.println(F("Content-Type: text/html"));         //type de fichier et encodage des caractères
  client.println(F("Connection: close"));               // fermeture de la connexion quand toute la réponse sera envoyée
  client.println(F("Refresh: 30"));                     // rafraichit la page toutes les 30s
  client.println();
  client.println(F("<!DOCTYPE HTML>"));

  // Construction de la page HTML
  client.println(F("<html><head><link rel='icon' href='data:,'><title>PLANETE BLEUE</title><style>body {text-align:center;}</style></head>")); // <link rel='icon' href='data:,'> est une commande pour supprimer la requête favicon du navigateur
  client.println(F("<body bgcolor='#00979C'>")); // page backgroud color
  client.println(F("<h1>COMMANDES PLANETE BLEUE</h1><hr>"));
  client.println(F("<br />"));

  //Horloge
  byte h = hour();
  byte m = minute();
  byte s = second();
  byte d = day();
  byte ms = month();
  if (h < 10) client.print('0');
  client.print(hour());
  client.print(F(":"));
  if (m < 10) client.print('0');
  client.print(minute());
  client.print(F(":"));
  if (s < 10) client.print('0');
  client.print(second());
  client.print(F("<br />"));
  if (d < 10) client.print('0');
  client.print(day());
  client.print(F("/"));
  if (ms < 10) client.print('0');
  client.print(month());
  client.print(F("/"));
  client.print(year());
  client.print(F("<br /><br />"));

  // bouton ECL
  client.print(F("<h2>ECLAIRAGE</h2>"));
  if (digitalRead(ECLPin) == LOW) {
    client.print(F("<body><p style='color:red;'>ETAT ACTUEL : OFF</p><form action='?' method='get'><input type='submit' name='ECL' value='ON'></form></body><br />"));
  }
  else {
    client.print(F("<body><p style='color:green;'>ETAT ACTUEL : ON</p><form action='?' method='get'><input type='submit' name='ECL' value='OFF'></form></body><br />"));
  }
  client.print(F("Horaires de fonctionnement<br />"));
  if ((horaire[0] / 100) < 10) client.print('0');
  client.print((horaire[0] / 100));
  client.print(F(":"));
  if ((horaire[0] % 100) < 10) client.print('0');
  client.print((horaire[0] % 100));
  client.print(F("<form><label for='ECL'>Allumage  </label><input type='time' id='ECL' name='EON' min='00:00' max='24:00' required><input type='submit' value='validation'></form>"));
  if ((horaire[1] / 100) < 10) client.print('0');
  client.print((horaire[1] / 100));
  client.print(F(":"));
  if ((horaire[1] % 100) < 10) client.print('0');
  client.print((horaire[1] % 100));
  client.print(F("<form><label for='ECL'>Extinction</label><input type='time' id='ECL' name='EOF' min='00:00' max='24:00' required><input type='submit' value='validation'></form><br />"));

  // bouton SON
  client.print(F("<h2>SONORISATION</h2>"));
  if (digitalRead(SONPin) == LOW) {
    client.print(F("<body><p style='color:red;'>ETAT ACTUEL : OFF</p></body>"));
    client.print(F("<form action='?' method='get'><input type='submit' name='SON' value='ON'></form><br />"));
  }
  else {
    client.print(F("<body><p style='color:green;'>ETAT ACTUEL : ON</p></body>"));
    client.print(F("<form action='?' method='get'><input type='submit' name='SON' value='OFF'></form><br />"));
  }
  client.print(F("Horaires de fonctionnement<br />"));
  if ((horaire[2] / 100) < 10) client.print('0');
  client.print((horaire[2] / 100));
  client.print(F(":"));
  if ((horaire[2] % 100) < 10) client.print('0');
  client.print((horaire[2] % 100));
  client.print(F("<form><label for='SON'>Allumage  </label><input type='time' id='SON' name='SON' min='00:00' max='24:00' required><input type='submit' value='validation'></form>"));
  if ((horaire[3] / 100) < 10) client.print('0');
  client.print((horaire[3] / 100));
  client.print(F(":"));
  if ((horaire[3] % 100) < 10) client.print('0');
  client.print((horaire[3] % 100));
  client.print(F("<form><label for='SON'>Extinction</label><input type='time' id='SON' name='SOF' min='00:00' max='24:00' required><input type='submit' value='validation'></form><br />"));

  // bouton PLAY
  client.print(F("<h2>PLAYER</h2>"));
  if (digitalRead(PLAPin) == LOW) {
    client.print(F("<body><p style='color:red;'>ETAT ACTUEL : OFF</p></body>"));
    client.print(F("<form action='?' method='get'><input type='submit' name='PLA' value='ON'></form><br />"));
  }
  else {
    client.print(F("<body><p style='color:green;'>ETAT ACTUEL : ON</p></body>"));
    client.print(F("<form action='?' method='get'><input type='submit' name='PLA' value='OFF'></form><br />"));
  }
  client.print(F("Horaires de fonctionnement<br />"));
  if ((horaire[4] / 100) < 10) client.print('0');
  client.print((horaire[4] / 100));
  client.print(F(":"));
  if ((horaire[4] % 100) < 10) client.print('0');
  client.print((horaire[4] % 100));
  client.print(F("<form><label for='PLA'>Allumage  </label><input type='time' id='PLA' name='PON' min='00:00' max='24:00' required><input type='submit' value='validation'></form>"));
  if ((horaire[5] / 100) < 10) client.print('0');
  client.print((horaire[5] / 100));
  client.print(F(":"));
  if ((horaire[5] % 100) < 10) client.print('0');
  client.print((horaire[5] % 100));
  client.print(F("<form><label for='PLA'>Extinction</label><input type='time' id='PLA' name='POF' min='00:00' max='24:00' required><input type='submit' value='validation'></form><br /><hr><br />"));

  // bouton allumage totale
  client.print(F("<form action='?' method='get'><input type='submit' name='TAL' value='TOTAL ON'></form><br />"));

  // bouton extinction totale
  client.print(F("<form action='?' method='get'><input type='submit' name='TEX' value='TOTAL OFF'></form><br />"));

  // bouton Reset arduino
  client.print(F("<form action='?' method='get'><input type='submit' name='RES' value='RESET ARDUINO'></form><br /><br />"));
  client.print(F("APRES UN RESET RECHARGER LA PAGE DU NAVIGATEUR AVEC L'ADDRESSE IP ARDUINO"));

  client.println(F("</center></body></html>"));
}

/**** PRISE EN CHARGE DE LA REQUETE URL DU CLIENT NAVIGATEUR WEB ****/

void priseEnChargeCommande(EthernetClient & client)
{
  // Si une commande est bien enregistrée dans urlCommand alors on l'analyse et, dans tout les cas on envoie une réponse
  if (strlen(urlCommand) != 0) {
    analyseCommande();
  }
  envoieReponse(client);
}

/**** COMMUNICATION ENTRE CLIENT NAVIGATEUR WEB ET SERVEUR ARDUINO ****/

void priseEnChargeClient()
{
  boolean urlCommandFound = false;
  char httpHeader[MaxCommand + 1];
  byte httpHeaderIndex = 0;
  httpHeader[0] = '\0';
  urlCommand[0] = '\0';

  EthernetClient client = webServer.available();            // Regarde si un client est connecté et attend une réponse de la part du serveur arduino

  if (client) {                                             // Si un client est là...
    boolean currentLineIsBlank = true;
    while (client.connected()) {                            // Tant que le client est connecté
      if (client.available()) {                             // A-t-il des choses à dire ?
        char c = client.read();                             // on lit l'url qu'il envoie pour savoir ce qu'il veut
        //Serial.print(c);
        if (!urlCommandFound) {
          httpHeader[httpHeaderIndex++] = c;
          httpHeader[httpHeaderIndex] = '\0';
          if (httpHeaderIndex > MaxCommand - 1) httpHeaderIndex = MaxCommand - 1;
        }
        if (c == '\n' && currentLineIsBlank) {                // Une requête HTTP se termine par une ligne vide
          priseEnChargeCommande(client);
          delay(5);                                           // Donne le temps au client de prendre les données
          client.stop();                                      // Ferme la connexion avec le client
          break;
        }
        if (c == '\n') {
          currentLineIsBlank = true;                          // démarre une nouvelle ligne
          if (!strncmp("GET /? ", httpHeader, 6)) {
            strcpy(urlCommand, (httpHeader + 6));             // permet de se débarrasser du "GET /? " de début de requête url
            char * firstSpacePtr = strchr(urlCommand, ' ');
            if (firstSpacePtr) *firstSpacePtr = '\0';         // supprime l'espace entre le corps de la requête et le http...
            urlCommandFound = true;
          } else {
            httpHeaderIndex = 0;
            httpHeader[0] = '\0';
          }
        } else if (c != '\r') {
          currentLineIsBlank = false;                 // un nouveau caractère est présent sur la ligne actuelle, on ignore '\r'
        }
      }
    }
  }
}

void loop()
{
  horlogeNTP();
  progHoraireAction();
  priseEnChargeClient();
}

Il tourne sur quel Arduino ?

Dans la fonction progHoraireAction, tu peux appeler hour() et minute() une seule fois et stocker les valeurs dans deux variables pour les utiliser dans tes tests, plutôt qu'appeler les fonctions à chaque fois, ce qui fait des calculs redondants et peut même entraîner des erreurs dans le cas d'appels autour de l'heure pleine (xx:00:00)
Même remarque pour envoieReponse

Il tourne sur Arduino UNO.
Merci pour ton retour sur les appels de fonctions, je vais modifier et surtout le garder en tête, effectivement c'est beaucoup mieux de travailler avec des variables!