Aller au contenu

TP04 : Station météo sur Internet

Dans le TP précédent, nous avons réalisé une station météo connectée qui transmet des mesures à un écran par BLE. Le but de ce TP est de publier ces mêmes mesures à un site Internet et de permettre ainsi un accès aux données depuis n’importe où.

Vous utiliserez encore une fois deux cibles :

  • La première cible fait exactement la même chose que pour le TP précédent. Elle se charge de prendre des mesures environnementales et de les diffuser (broadcast) avec le BLE;
  • La deuxième cible reçoit les mesures diffusées par BLE, les affiche sur l’écran, et les envoie sur un serveur par MQTT.

Schéma bloc

Objectifs du TP

Ce TP a pour but de mettre en pratique une communication MQTT par WiFi

À la fin de ce TP, les étudiants :

  • auront activé la stack IP de Mbed OS et configuré le WiFi;
  • auront implémenté un client MQTT sur Mbed OS;
  • auront publié des données sur un serveur Internet;
  • auront réalisé des analyses statiques de code et corrigé les erreurs rapportées ;
  • auront rédigé un journal de travail et déposé le PDF dans le dépôt git.

Les livrables sont :

  • un projet git (tp03) dans votre groupe sur gitlab.forge.hefr.ch avec le code du TP ;
  • un journal de travail déposé sur gitlab.

Temps accordé : 4 périodes de 45 minutes en classe + travail personnel à la maison

Délai

Le TP doit être rendu au plus tard 6 jours après la séance en classe à 23:59.

Matériel

Pour ce TP, vous avez besoin du WiFi BLE click :

WiFi BLE click

Ce click-board utilise l’interface UART et vous pouvez le mettre sur le slot 1 ou sur le slot 2.

Schéma bloc

Configuration de Mbed OS et de PlatformIO

Pour utiliser le module BLE, vous devez configurer votre projet de la manière suivante :

Fichier platformio.ini

Dans le fichier platformio.ini, ajoutez la librairie pour le module EPS32 et pour le MQTT aux dépendances de votre projet (ligne 5 et 6) :

1
2
3
4
5
6
7
lib_deps =
    https://github.com/heia-fr/embsys-target-disco-heiafr.git#v0.2.0
    https://github.com/ARMmbed/mbed-os-ble-utils.git
    https://github.com/heia-fr/embsys-disco-gfx.git#main
    https://github.com/heia-fr/embsys-esp32-driver#main
    https://github.com/heia-fr/embsys-mbed-mqtt#main
    mbed-star297/BME280@0.0.0+sha.eaf30b268430

Dossier mbedignore

En plus de BLE, nous devons ré-activer la stack IP et le WiFi de Mbed OS pour ce TP :

Éditez le fichier mbedignore/connectivity/.mbedignore et supprimez les lignes suivantes :

drivers/ble/*
drivers/wifi/*
FEATURE_BLE/*
libraries/*
lwipstack/*
mbedtls/*
netsocket/*

Il doit vous rester les lignes suivantes :

cellular/*
drivers/802.15.4_RF/*
drivers/cellular/*
drivers/emac/*
drivers/lora/*
drivers/mbedtls/*
drivers/nfc/*
lorawan/*
nanostack/*
nfc/*

Effacez aussi la ligne du fichier mbedignore/platform/.mbedignore (et conservez le fichier vide)

Fichier mbed_app.json

Pour modifier les valeurs par défaut, éditez le fichier mbed_app.json de votre projet et ajoutez les lignes suivantes :

Modifiez le fichier mbed_app.json pour l’utilisation du Wifi et configurez les “secrets” de votre connexion :

    "DISCO_HEIAFR": {
        ...
        "target.network-default-interface-type" : "WIFI",
        "nsapi.default-wifi-ssid" : "\"MySSID\"",
        "nsapi.default-wifi-password" : "\"MyPWD\"",
        "nsapi.default-wifi-security": "WPA_WPA2"
    }

Publication vers un broker MQTT public

Afin de faciliter l’intégration des données dans une infrastructure existante, nous utiliserons un broker public pour publier les données de votre station météo. Selon les instructions que vous recevrez en classe, vous pourrez utiliser le broker Adafruit.io ou le broker ThingsBoard.io. Votre code client sera identique, au-delà des informations permettant de vous authentifier. Cela démontre l’avantage d’utiliser un standard de communication bien établi.

Le Dashboard Adafruit

Afin de simplifier la réalisation, nous utilisons le dashboard offert par Adafruit. Rendez-vous sur io.adafruit.com et cliquez sur “Get Started for Free”1 en haut à droite.

Créez un nouveau dashboard en cliquant sur “+ New Dashboard”

Dans votre nouveau dashboard, ajoutez un bloc, par exemple une gauge, pour représenter l’humidité :

Ajout d’un bloc

Configurez le bloc pour l’humidité :

Configuration du bloc

Créez ensuite un nouveau feed pour l’humidité :

Ajout d’un feed

Et connectez ce feed à votre bloc

Connexion d’un feed

Vous pouvez obtenir les détails d’un feed en cliquant sur “feed info”

Détails d’un feed

Le feed peut recevoir des données par une connextion Web ou MQTT. Cette page vous indique le topic MQTT correspondant à votre feed.

Obtenez encode une clé en cliquant sur “My Key”

Testez maintenant l’envoi d’une valeur sur le feed que vous avez créé à l’aide de MQTT Explorer ou en ligne de commande :

mosquitto_pub \
  -h io.adafruit.com \
  -p 1883 \
  -u <VOTRE USERNAME (par ex. supcik)> \
  -P <VOTRE CLE> \
  -t <VOTRE FEED (par ex. supcik/feeds/humidity)> \
  -m 42

La gauge de votre dashboard devrait maintenant indiquer 42 :

Gauge après mise à jour

Complétez le dashboard avec un bloc pour la température et un bloc pour la pression atmosphérique.

Notez que vous pouvez très bien configurer deux blocs avec le même feed. Expérimentez avec une mesure donnée (par exemple la température sur une gauge et sur un graph).

Le Dashboard ThingsBoard

Afin de mettre en place le dashboard ThingsBoard, vous devez lire attentivement les instructions données sur Getting Started with ThingsBoard. Ce guide décrit avec détails toutes les étapes vous permettant de mettre en place un dashboard, permettant d’afficher les données reçues de votre station météo en utilisant un des nombreux widgets mis à disposition. Vous devez préparer un dashboard permettant de visualiser (sous une forme de votre choix) les données de température, d’humidité et de pression atmosphérique provenant de votre station.

Pour l’authentification, nous utiliserons l’authentification basée sur les Username et Password, comme expliqué sur Authentication based on Username and Password. Afin de créer un nom d’utilisateur et un mot de passe pour un device, vous devez créer les credentials pour le device configuré, comme illustré dans l’image ci-dessous (cliquez sur le bouton Manage Credentials, choisissez MQTT Basic comme type de credentials et choisissez un Username et Password).

Device credentials Gestion des accès à un device

A la fin de cette étape, vous devez être à même de publier des données vers le broker MQTT à l’aide d’un client comme mosquitto_pub et comme décrit sous MQTT Linux ou MacOS , MQTT Windows ou une autre méthode de votre choix. Les données publiées ainsi doivent être visibles dans votre dashboard. Il est important que la publication vers votre dashboard soit validée avant de commencer la réalisation du client MQTT pour votre cible.

Réalisation

La réalisation du client MQTT doit se faire en respectant l’architecture des classes affichées dans le diagramme de classe ci-dessous :

Diagramme de classes Diagramme de classes

Dans le diagramme ci-dessus, les classes CoAPPublisher et HTTPPublisher ne sont pas détaillées car elles ne sont pas réalisées dans ce travail. Comme vous pouvez le voir dans le diagramme, la réalisation se base sur une interface IPublisher et une classe de base PublisherBase. Afin de débuter la réalisation, vous devez tout d’abord créer le squelette des classes de la manière suivante :

  • Vous devez définir l’interface IPublisher dans un fichier ipublisher.hpp. En C++, le mot clé interface n’existe pas et une interface est en fait une abstract class dont toutes les méthodes sont abstraites (pure virtual) et qui ne contient en principe pas d’attribut. L’interface IPublisher contient les trois méthodes présentées dans le diagramme de classe. La méthode connect() reçoit les paramètres nécessaires à la connexion et la méthode publishMeasurements reçoit les mesures des capteurs.
  • La classe PublisherBase est la réalisation de base de toutes les classes Publisher. La classe hérite de IPublisher et est également une classe abstraite. Cette classe fournit la fonctionnalité de connexion au réseau pour les classes Publisher, à l’aide de la méthode connectNetworkInterface(). La réalisation de la classe PublisherBase à insérer dans votre fichier publisher_base.cpp est donnée ci-dessous :
    publisher_base.cpp
    // Copyright 2022 Haute école d'ingénierie et d'architecture de Fribourg
    //
    // Licensed under the Apache License, Version 2.0 (the "License");
    // you may not use this file except in compliance with the License.
    // You may obtain a copy of the License at
    //
    //     http://www.apache.org/licenses/LICENSE-2.0
    //
    // Unless required by applicable law or agreed to in writing, software
    // distributed under the License is distributed on an "AS IS" BASIS,
    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    // See the License for the specific language governing permissions and
    // limitations under the License.
    
    /****************************************************************************
     * @file publisher_base.cpp
     * @author Serge Ayer <serge.ayer@hefr.ch>
     *
     * @brief Base class for MQTT and CoAP publishers
     *
     * @date 2022-01-01
     * @version 0.1.0
     ***************************************************************************/
    
    #include "publisher_base.hpp"
    
    #include "ESP32Interface.h"
    #include "mbed_trace.h"
    #if defined(MBED_CONF_MBED_TRACE_ENABLE)
    #define TRACE_GROUP "PublisherBase"
    #endif  // MBED_CONF_MBED_TRACE_ENABLE
    
    PublisherBase::PublisherBase()
    {
      static ESP32Interface esp32;
      networkInterface_ = &esp32;
      MBED_ASSERT(networkInterface_ != nullptr);
    }
    
    PublisherBase::~PublisherBase()
    {
      if (networkInterface_ != nullptr &&
          networkInterface_->get_connection_status() != NSAPI_STATUS_DISCONNECTED) {
        networkInterface_->disconnect();
      }
      delete networkInterface_;
      networkInterface_ = nullptr;
    }
    
    nsapi_error_t PublisherBase::connectNetworkInterface()
    {
      tr_debug("Connecting to the network...");
    
      // check if we're already connected
      if (networkInterface_->get_connection_status() == NSAPI_STATUS_GLOBAL_UP) {
        tr_debug("Already connected");
        return NSAPI_ERROR_OK;
      }
    
      // set the default parameters (such as wifi pwd or sim pin) before connecting
      networkInterface_->set_default_parameters();
    
      nsapi_error_t rc = NSAPI_ERROR_OK;
      for (uint8_t retry = 0; retry <= MAX_CONNECTION_RETRY_COUNT; retry++) {
        rc = networkInterface_->connect();
    
        if (rc == NSAPI_ERROR_OK) {
          tr_info("Connection Established.");
          break;
        } else if (rc == NSAPI_ERROR_AUTH_FAILURE) {
          tr_info("Authentication Failure.");
          return rc;
        } else {
          tr_info("Couldn't connect: %d, will retry.", rc);
        }
      }
    
      printNetworkInfo();
    
      return rc;
    }
    
    void PublisherBase::printNetworkInfo() const
    {
      // print the network info
      SocketAddress a;
      networkInterface_->get_ip_address(&a);
      tr_info("IP address: %s", a.get_ip_address() ? a.get_ip_address() : "None");
      networkInterface_->get_netmask(&a);
      tr_info("Netmask: %s", a.get_ip_address() ? a.get_ip_address() : "None");
      networkInterface_->get_gateway(&a);
      tr_info("Gateway: %s", a.get_ip_address() ? a.get_ip_address() : "None");
    }
    
  • La classe MQTTPublisher représente la classe concrète que l’application pourra utiliser afin de publier des données vers le broker. La classe réalise toutes les méthodes abstraites de l’interface IPublisher. Après avoir créé une instance de cette classe, le client pourra ainsi se connecter (une seule fois) au broker MQTT puis publier les mesures à intervalle régulier. La classe réalise également une méthode permettant de vérifier que le client MQTT est bien connecté.

  • La méthode connect() de la classe MQTTPublisher est donnée ci-dessous :

    mqtt_publisher.cpp
    nsapi_error_t MQTTPublisher::connect(const char* serverHostname,
                                         const char* userName,
                                         const char* keyOrPwd)
    {
      // call the base class for connecting to the network
      nsapi_error_t retCode = PublisherBase::connectNetworkInterface();
      if (retCode != NSAPI_ERROR_OK) {
        tr_error("Could not connect to network: %d", retCode);
        return retCode;
      }
    
      static const char* SOCKET_STRING = "TCPSocket";
      tr_info("Opening %s", SOCKET_STRING);
      retCode = tcpSocket_.open(networkInterface_);
      if (retCode != NSAPI_ERROR_OK) {
        tr_error("%s::open() failed, code: %d", SOCKET_STRING, retCode);
        return retCode;
      }
    
      static constexpr int SOCKET_TIMEOUT = 15000;
      tcpSocket_.set_timeout(SOCKET_TIMEOUT);
      SocketAddress socketAddress;
      tr_debug("Resolving hostname %s", serverHostname);
      retCode = networkInterface_->gethostbyname(serverHostname, &socketAddress);
      if (retCode != NSAPI_ERROR_OK) {
        tr_error("gethostbyname couldn't resolve remote host: %s, code: %d",
                 serverHostname,
                 retCode);
        return retCode;
      }
      tr_debug("Hostname %s resolved to %s",
               serverHostname,
               socketAddress.get_ip_address());
      static constexpr int serverPort = 1883;
      socketAddress.set_port(serverPort);
      tr_debug("%s: trying to connect socket with address: %s on port %d",
               SOCKET_STRING,
               socketAddress.get_ip_address(),
               socketAddress.get_port());
      retCode = tcpSocket_.connect(socketAddress);
      if (retCode < 0) {
        tr_error("%s: connect() failed with code: %d", SOCKET_STRING, retCode);
        return retCode;
      } else {
        tr_debug("%s: connected with %s server", SOCKET_STRING, serverHostname);
      }
    
      // options for MQTT
      MQTTPacket_connectData options = MQTTPacket_connectData_initializer;
      // do not initialize the clientID
      options.username.cstring = (char*)userName;
      options.password.cstring = (char*)keyOrPwd;
      // options.MQTTVersion = 3;
    
      // store user name and key
      userName_ = userName;
      key_      = keyOrPwd;
    
      // connect to the broker
      tr_debug("Trying to connect to MQTT broker %s with user name %s",
               serverHostname,
               userName);
      retCode = mqttClient_.connect(options);
      if (retCode != NSAPI_ERROR_OK) {
        tr_error("connect : Failed to connect MQTT client, code : %x", retCode);
        return retCode;
      }
    
      return retCode;
    }
    

  • Vous devez réaliser la méthode publishMeasurements() en suivant les spécifications du broker Adafruit ou ThingsBoard relatives au topic et au payload du message et selon les tests effectués lors de la mise en œuvre du dashboard.
  • Vous devez également réaliser la méthode isConnected() qui valide que le client MQTT est bien connecté au broker.

La dernière étape de la réalisation consiste à modifier le programme principal pour créer une instance de MQTTPublisher, connecter cette instance au broker et publier les mesures à intervalle régulier. Vous devez limiter la publication des mesures à une fréquence maximale de 1 minute. Votre application doit se comporter correctement lorsque la publication au broker échoue : elle doit afficher un message et continue à fonctionner. Vous pouvez limiter la réalisation à une seule tentative de connexion.

Note

Pour prendre en main la partie WiFi, vous pouvez commencer par afficher la liste des points d’accès visibles par votre cible. Vous pouvez vous inspirer du code suivant:

// Copyright 2022 Haute école d'ingénierie et d'architecture de Fribourg
// SPDX-License-Identifier: Apache-2.0

/****************************************************************************
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief Network related utility functions
 *
 * @date 2022-01-01
 * @version 0.1.0
 ***************************************************************************/

#include "nsapi.h"

static const char* get_security_string(nsapi_security_t sec)
{
  switch (sec) {
    case NSAPI_SECURITY_NONE:
      return "None";
    case NSAPI_SECURITY_WEP:
      return "WEP";
    case NSAPI_SECURITY_WPA:
      return "WPA";
    case NSAPI_SECURITY_WPA2:
      return "WPA2";
    case NSAPI_SECURITY_WPA_WPA2:
      return "WPA/WPA2";
    case NSAPI_SECURITY_UNKNOWN:
    default:
      return "Unknown";
  }
}

void wifiScan()
{
  NetworkInterface* pNetworkInterface =
      NetworkInterface::get_default_instance();
  MBED_ASSERT(pNetworkInterface != nullptr);
  MBED_ASSERT(pNetworkInterface->wifiInterface() != nullptr);

  WiFiInterface* wifi = pNetworkInterface->wifiInterface();

  static constexpr size_t MAX_NUMBER_OF_ACCESS_POINTS = 10;
  WiFiAccessPoint ap[MAX_NUMBER_OF_ACCESS_POINTS];

  // scan call returns number of access points found
  int result = wifi->scan(ap, MAX_NUMBER_OF_ACCESS_POINTS);

  if (result <= 0) {
    printf("WiFiInterface::scan() failed with return value: %d\r\n", result);
    return;
  }

  printf("%d networks available:\r\n", result);

  for (int i = 0; i < result; i++) {
    printf(
        "Network: %s secured: %s BSSID: %hhX:%hhX:%hhX:%hhx:%hhx:%hhx RSSI: "
        "%hhd Ch: %hhd\r\n",
        ap[i].get_ssid(),
        get_security_string(ap[i].get_security()),
        ap[i].get_bssid()[0],
        ap[i].get_bssid()[1],
        ap[i].get_bssid()[2],
        ap[i].get_bssid()[3],
        ap[i].get_bssid()[4],
        ap[i].get_bssid()[5],
        ap[i].get_rssi(),
        ap[i].get_channel());
  }
  printf("\r\n");
}

Questions / Requis

  • Expliquez le fonctionnement de la méthode connect() de la classe MQTTPublisher en détail. En particulier, détaillez les erreurs qui peuvent survenir lors de la connexion et les composants logiciels utilisés afin de réaliser une connexion vers un broker MQTT.

  • Publiez des données environnementales vers le broker pendant au moins 6 heures et documentez l’accès à votre dashboard dans le rapport. Le lecteur du rapport doit pouvoir accéder au dashboard et vérifier les données publiées.

Tests et validations

Comme pour le TP précédent, configurez l’analyse statique de code de manière plus stricte.

À ne pas oublier

Gardez toujours en têtes les bonnes pratiques ainsi que les dix commandements du bon programmeur.

  • Choisissez de bons noms pour les classes, les méthodes et les variables.
  • Implémentez les bibliothèques avec un haut niveau d’abstraction pour pouvoir réutiliser les méthodes dans d’autres projets.
  • Faites des “git commit” régulièrement avec de bons commentaires.
  • Configurez le CI/CD de gitlab et testez automatiquement le plus de choses possibles.
  • Implémentez des tests unitaires.
  • Utilisez des assertions dans votre code pour le documenter et le rendre plus robuste.

Journal de travail

  • Rédigez un rapport (journal de travail) avec les indications suivantes :
  • Une page de titre avec au minimum :
    • le nom et le logo officiel de l’école
    • le nom du cours : Informatique Embarquée
    • le titre de votre document : Travail Pratique 3 : Station météo connectée
    • le numéro de votre groupe
    • les noms des auteurs (vous) avec la classe dans laquelle vous êtes
    • la date à laquelle vous avez terminé le rapport
    • éventuellement la version du rapport
  • Une introduction pour poser le contexte
  • Un résumé des notions que vous avez apprises pendant ce TP en précisant si c’est
    • non acquis
    • acquis, mais encore à exercer
    • parfaitement acquis
  • Un résumé des points qui vous semblent importants et que vous devez retenir
  • Les réponses aux questions.
  • Le code source bien formaté et avec du “syntax highlighting” de votre code source.
  • Une conclusion par laquelle vous donnez vos impressions sur le TP, ce que vous avez aimé, ce que vous avez moins aimé, et éventuellement des suggestions pour des changements. Indiquez également le nombre d’heures que vous avez passées, par personne, en dehors des heures de TP en classe.

Important

Déposez votre rapport dans un dossier /docs de votre dépôt git (tp04) avec le nom report04.pdf (le chemin complet vers votre rapport est donc /docs/report04.pdf)


  1. Cliquez sur “Sign In” si vous avez déjà un compte.