Aller au contenu

TP04 : Station météo MQTT

Dans le TP précédent, nous avons réalisé une station météo connectée qui transmet des mesures en effectuant du broadcasting 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ù.

Pour ce TP, nous utiliserons une seule cible qui devra directement transmettre les données environnementales 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 sur Teams.

Les livrables sont :

  • un release avec le tag “tp04” doit être créé sur votre dépôt.
  • un journal de travail déposé sur Teams.

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

Délai

Les dates de rendu des TPs sont indiquées sur le plan de cours.

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 placer sur le slot 1 ou sur le slot 2.

Schéma bloc

Configuration de Mbed OS et de PlatformIO

Pour utiliser le module WiFi et la librairie MQTT, 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
lib_deps =
    ...
    https://github.com/SergeAyer/embsys-esp32-driver#main
    https://github.com/heia-fr/embsys-mbed-mqtt#main
    ...

Dossier mbedignore

Pour ce TP, nous devons également activer la stack IP et le WiFi de Mbed OS:

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

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

Les lignes suivantes devraient toujours être présentes :

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 :

1
2
3
4
5
6
7
8
    "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 utiliserez 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 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, vous devez utiliser 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. La ligne de commande à utiliser devrait être similaire à :

mosquitto_pub -d -q 1 -h "demo.thingsboard.io" -p "1883" -t "v1/devices/me/telemetry" -u "YOUR_CLIENT_USERNAME" -P "YOUR_CLIENT_PASSWORD" -m {"temperature":25}
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 publishMeasurement() 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();
      }
    }
    
    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;
    
      // 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 ThingsBoard relatives au topic et au payload du message et selon les tests effectués lors de la mise en œuvre du dashboard. Pour la réalisation, vous devez utiliser l’instance de MQTTClient définie dans la classe MQTTPublisher et la méthode publish() de la classe MQTTClient.

  • 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 publication par minute. Votre application doit se comporter correctement lorsque la publication au broker échoue : elle doit afficher un message et continuer à fonctionner. Vous pouvez limiter la réalisation à une seule tentative de connexion.

Points d’accès dans la salle de classe

Afin d’accéder au point d’accès mis à disposition dans la salle de classe, vous pouvez utiliser les informations suivantes dans votre fichier “mbed_app.json”:

  • “nsapi.default-wifi-ssid” : “"Notyour2.4G"“,
  • “nsapi.default-wifi-password” : “"Fribourg"“,

Note

Pour prendre en main la partie WiFi, vous pouvez commencer par afficher la liste des points d’accès visibles par votre cible. Pour ce faire, 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"

#include "ESP32Interface.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()
{  
  printf("Starting wifi scan\n");
  static ESP32Interface esp32;
  WiFiInterface* wifi = &esp32;  
  MBED_ASSERT(wifi != nullptr);

  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.

  • Vous devez publier des données environnementales vers le broker Thingsboard pendant au moins 6 heures et ajoutez le lien public à votre dashboard dans le rapport. Le lien vers le dashboard public doit fonctionner sans s’identifier sur la plateforme Thingsboard.

Sécurité (Bonus)

Votre projet a besoin de plusieurs données sensibles telles que le mot de passe du WiFi ou le mot de passe pour le broker MQTT. Ce n’est pas une bonne idée de mettre ces données sensibles dans le code ni dans tout autre fichier géré par git.

Décrivez et implémentez une technique qui permet de compiler le code sans divulguer d’information sensible dans git.

Voici quelques liens utiles:

Tests et validations

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

À ne pas oublier

  • 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.
  • Utilisez des assertions dans votre code pour le documenter et le rendre plus robuste.
  • Validez tous les codes de retour des méthodes/fonctions utilisées.

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 : Module Acquisition et traitement de données : Internet des Objets
    • le titre de votre document : Travail Pratique 4 : Station météo MQTT
    • 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 le devoir Teams correspondant avec le nom report04.pdf.