Éclairer le réseau (3)

Over The Air

. Par : Jean-Luc. URL : https://www.locoduino.org/spip.php?article299

Nous avons maintenant un ESP32 qui se connecte au WiFi de manière aussi fiable que possible compte tenu de l’application et qui répond au doux nom d’Eclairage.local sur le réseau local. Occupons nous maintenant de l’OTA.

L’OTA

OTA est l’acronyme de Over The Air. Il s’agit d’un service permettant de téléverser un sketch via le réseau WiFi et donc de ne pas avoir à ramper ou à grimper jusqu’à l’ESP32, une fois que celui-ci sera installé à son emplacement final, pour y brancher un câble USB. C’est évidemment extrêmement pratique et, en réalité, quasiment indispensable.

Il est possible de faire une mise en place minimum avec très peu de code et une mise en place un peu plus élaborée qui sera pratique pour débugger. Commençons par la mise en place minimum.

Il faut d’abord inclure la bibliothèque comme ceci :

#include <ArduinoOTA.h>

Il faut ensuite initialiser l’OTA en :

  1. Spécifiant un nom pour la machine. À ma connaissance l’OTA repose sur mDNS. Ce nom est celui sous lequel l’ESP apparaîtra dans le menu de l’IDE, nous y reviendrons. Pour cela, on utilise la fonction ArduinoOTA.setHostname(...) ;
  2. Démarrer le service OTA avec la fonction ArduinoOTA.begin()

Pour cela nous allons ajouter une méthode de classe (c’est à dire qualifiée de static) privée à la classe Connection, méthode que nous appellerons initOTA() et dont l’implémentation est la suivante :

void Connection::initOTA()
{
  DP("Demarrage OTA sous le nom ");
  DPLN(sName);
  ArduinoOTA.setHostname(sName);
  ArduinoOTA.begin();
}

Afin d’appeler initOTA() au bon moment, c’est à dire après avoir effectué la connection au WiFi et lancé le service mDNS [1], nous allons ajouter un état OTA_OK à notre automate et le remanier en conséquence.

Figure 1 : La machine à état de connexion intégrant l'OTA.
Figure 1 : La machine à état de connexion intégrant l’OTA.

Deux case sont modifiés en conséquence :

    case MDNS_OK:
      if (WiFi.status() == WL_CONNECTED) {
        initOTA();
        sState = OTA_OK;
      } else {
        DPLN("Perte de connexion");
        sState = OFFLINE;
      }
      break;

    case OTA_OK:
      if (WiFi.status() != WL_CONNECTED) {
        DPLN("Perte de connexion");
        ArduinoOTA.end();
        sState = OFFLINE;
      }
      break;

Et isOnline() l’est également :

bool Connection::isOnline()
{ 
  return (sState == OTA_OK);
}

Enfin, il faut appeler le plus souvent possible la fonction ArduinoOTA.handle() pour que le service puisse répondre aux sollicitations de téléversement que l’IDE lui envoie. Nous modifions donc Connection::loop() à cette fin.

void Connection::loop()
{
  const uint32_t currentDate = millis();
  if ((currentDate - sLastDate) >= sPeriod) {
    update();
    sLastDate = currentDate;
  }
  ArduinoOTA.handle();
}

Le code est chargé sur l’ESP32 de manière classique, via l’USB, mais désormais un nouveau port devrait apparaître dans le menu Outils, sous menu Ports de l’IDE, dans la section Ports réseau, comme montré à la figure 2.

Figure 2 : Après chargement du sketch comprenant le service OTA, un nouveau port réseau apparaît, « Eclairage ».
Figure 2 : Après chargement du sketch comprenant le service OTA, un nouveau port réseau apparaît, « Eclairage ».

Si ce n’est pas le cas, quitter et relancer l’IDE règlera le problème. Nous voilà prêts à tester le téléversement via le WiFi. Le port réseau est sélectionné puis on presse le bouton Téléverser. l’IDE compile le sketch et le téléverse comme montré à la figure 3. L’OTA est fonctionnel.

L’ESP32 peut désormais être déconnecté de l’ordinateur que vous utilisez pour programmer. Pourvu qu’il soit alimenté et connecté au même réseau WiFi que votre ordinateur, vous pourrez le programmer à distance grâce à l’OTA.

Figure 3 : Téléversement du sketch en OTA. L'IDE indique l'adresse IP puis affiche une barre de progression.
Figure 3 : Téléversement du sketch en OTA. L’IDE indique l’adresse IP puis affiche une barre de progression.
Bien évidemment, si vous chargez de cette manière un sketch dépourvu du service OTA ou une nouvelle version d’un sketch qui ne fonctionne pas correctement, ou qui plante, vous perdrez le service OTA et devrez donc aller chercher l’ESP là où vous l’avez installé pour le reprogrammer via l’USB. Il faut donc être prudent et tester systématiquement toute nouvelle version sur un ESP de développement avant de la déployer en OTA.

Sécurisons un peu

Il ne s’agit pas ici de blinder le téléversement en OTA contre des cyberattaques mais plutôt d’éviter des méprises. En effet, quand le nombre d’ESP32 dédiés à des tâches diverses commencent à pulluler sur le réseau du domicile et que la liste des ports réseau s’allonge dans l’IDE, il est aisé de se tromper de port et de flasher le mauvais ESP. En associant un mot de passe différent à chaque logiciel, le risque d’erreur est plus limité.

Une donnée de classe est ajoutée à Connection pour garder un pointeur sur le mot de passe OTA :

  static char *sOTAPass;

Il suffit d’ajouter un appel à ArduinoOTA.setPassword(...) dans la méthode initOTA et le tour est joué.

  if (sOTAPass != NULL) {
    ArduinoOTA.setPassword(sOTAPass);
  }

Enfin, un argument supplémentaire est ajouté à Connection::setup(...) :

void Connection::setup(const char *inSsid,
                       const char *inPass,
                       const char *inName,
                       const char *inOTAPass,
                       const uint32_t inPeriod)

L’appel à setup est fait avec le mot de passe désiré, dans l’exemple suivant locoduino :

  Connection::setup(ssid, pass, "Eclairage", "locoduino");

Lors du téléversement, le mot de passe est désormais demandé comme montré à la figure 4.

Figure 4 : Fenêtre d'authentification pour autoriser un téléversement via OTA.
Figure 4 : Fenêtre d’authentification pour autoriser un téléversement via OTA.

On peut également ne pas vouloir mettre ce mot de passe en clair dans le sketch et lui substituer un mot de passe chiffré. Dans ce cas, il faut chiffrer le mot de passe désiré avec MD5. On appelle le résultat un hash MD5. Avec un Mac, on ouvre un terminal et on tape :

jlb@Arrakis ~ % md5 -s locoduino
MD5 ("locoduino") = 35789343f9ea974b8742c8367e43f8a1
jlb@Arrakis ~ % 

Avec Linux, c’est un peu différent :

pi@raspberrypi:~ $ echo -n 'locoduino' | md5sum
35789343f9ea974b8742c8367e43f8a1  -
pi@raspberrypi:~ $ 

Il est également possible d’utiliser un générateur de hash MD5 en ligne.

Il suffit ensuite de copier-coller le hash MD5 dans votre sketch et d’utiliser la fonction ArduinoOTA.setPasswordHash(...) en lieu et place de ArduinoOTA.setPassword(...). Il va de soi qu’en cas d’oubli du mot de passe vous êtes fichu et que vous devrez ramper ou grimper pour brancher un câble USB sur votre ESP pour téléverser un nouveau sketch.

Debugger l’OTA

Il est possible d’accrocher des fonctions à soi aux endroits critiques du service OTA pour être notifié de la progression des opérations. L’intérêt est limité lorsque l’ESP est placé à son endroit définitif car, en l’absence de connexion série, les affichages que l’on effectueraient partiraient dans le vide.

En revanche, ça peut être pratique pour régler des problèmes pendants les tests alors que l’ESP est au bout du câble USB. Pour accrocher ces fonctions, l’objet ArduinoOTA propose les méthodes suivantes dont l’unique argument est la fonction à appeler :

  • ArduinoOTA.onStart(...) permet d’accrocher une fonction sans argument qui sera appelée une fois au début du processus de téléversement ;
  • ArduinoOTA.onProgress(...) permet d’accrocher une fonction avec deux arguments. Le premier est un entier non signé donnant la progression courante, progress, et le second est un entier non signé sonnant la progression totale, total. L’expression 100 * progress / total donne la progression en pourcentage ;
  • ArduinoOTA.onEnd(...) permet d’accrocher une fonction sans argument qui sera appelée une fois à la fin du processus de téléversement, et donc juste avant le redémarrage de l’ESP ;
  • ArduinoOTA.onError(...) permet d’accrocher une fonction avec un unique argument de type ota_error_t qui sera appelée si une erreur survient pendant les opérations.

Ajoutons l’affichage de ces informations de debug dans le sketch. Tout d’abord la déclaration des 4 fonctions que nous allons accrocher :

#ifdef DEBUG
  static void startingOTA();
  static void OTAInProgress(unsigned int inProgress, unsigned int inTotal);
  static void endingOTA();
  static void OTAError(ota_error_t error);
#endif

Ensuite leur implémentation :

#ifdef DEBUG
void Connection::startingOTA()
{
  const int command = ArduinoOTA.getCommand();
  if (command == U_FLASH) {
    DPLN("Téléversement du firmware");
  } else {
    DP("Commande inattendue ");
    DPLN(command);
  }
}

void Connection::OTAInProgress(unsigned int inProgress, unsigned int inTotal)
{
  static uint32_t previousNumberOfDots = 0;
  const uint32_t numberOfDots = 50 * inProgress / inTotal;
  for (uint32_t dot = previousNumberOfDots; dot < numberOfDots; dot++) {
    DP('.');
  }
  if (inTotal == inProgress) {
    DPLN();
  }
  previousNumberOfDots = numberOfDots;
}

void Connection::endingOTA()
{
  DPLN("Téléversement terminé");
}

void Connection::OTAError(ota_error_t error)
{
  DP("Erreur[");
  DP(error);
  DP("] : ");
  switch (error) {
    case OTA_AUTH_ERROR:    DPLN("L'authentification a échoué"); break;
    case OTA_BEGIN_ERROR:   DPLN("Échec au début");              break;
    case OTA_CONNECT_ERROR: DPLN("Échec à la connexion");        break;
    case OTA_RECEIVE_ERROR: DPLN("Échec à la réception");        break;
    case OTA_END_ERROR:     DPLN("Échec à la fermeture");        break;
  }
}
#endif

Et enfin, la mise en place de ces fonctions dans initOTA :

void Connection::initOTA()
{
  DP("Demarrage OTA sous le nom ");
  DPLN(sName);
  ArduinoOTA.setHostname(sName);
  if (sOTAPass != NULL) {
    ArduinoOTA.setPassword(sOTAPass);
  }
  
#ifdef DEBUG
  ArduinoOTA.onStart(startingOTA);
  ArduinoOTA.onProgress(OTAInProgress);
  ArduinoOTA.onEnd(endingOTA);
  ArduinoOTA.onError(OTAError);
#endif

  ArduinoOTA.begin();
}

Lors du téléchargement, le moniteur série affiche les informations sur le téléversement en cours.

Figure 5 : Affichage de la progression de l'OTA dans le moniteur série.
Figure 5 : Affichage de la progression de l’OTA dans le moniteur série.
Les numéros de ligne indiqués correspondent aux lignes du sketch final que l’on peut télécharger à la fin de l’article.

Voici le sketch, correspondant à cette étape, à télécharger.

Sketch comprenant la connexion au WiFi, mDNS et l’OTA

Le prochain article traitera de la mise en œuvre de MQTT

[1L’ESP32 plantera vigoureusement si l’OTA est initialisé alors que la connexion WiFi ne l’est pas.