Site : tvaira.free.fr

MQTT

Introduction

MQTT (Message Queuing Telemetry Transport) est un protocole de messagerie publish-subscribe basé sur le protocole TCP/IP. [cf. fr.wikipedia.org/wiki/MQTT]

C’est maintenent un protocole utilisé dans le cadre de l’Internet des objets (IoT) et peut être considéré comme un protocole M2M (Machine-to-Machine).

Liens :

MQTT offre une solution adaptée à une transmission de messages (proche d’un système de messagerie ou d’un bus logiciel comme D-BUS). Tous les acteurs se connectent à un élément central (le broker) et communiquent entre eux en envoyant (publish) des messages libellés avec un sujet et en souscrivant (subscribe) aux sujets qui les intéressent.

Les messages sont envoyés par des « publishers » sur un « topic » (canal de communication) à un « broker ». Ces messages peuvent être lus par des « subscribers » (abonnés). Les « topics » peuvent avoir une hiérarchie qui permettra de sélectionner les informations. Les « publishers » et « subscribers » sont considérés comme des « clients » MQTT : Liste des clients et bibliothèques.

Chaque message doit donc avoir un sujet associé (« topic »). Les sujets (« topic ») sont divisés en niveaux. Il y a des « wild-cards » (jokers) + et # :

capteurs/temperature/salon et capteurs/temperature/chambre capteurs/humidite/salon et capteurs/humidite/chambre

  • + : sujet unique. Par exemple : a/+/c sera abonné à a/b/c et a/x/c.

  • # : multi-sujets. Par exemple : a/# sera abonné à a/‹tout›

Le publisher définira l’option QoS dans les messages qui transitent via MQTT :

  • QoS = 0 : le message sera délivré tout au plus une fois (« at most once ») → sans garantie de réception et le message peut donc être perdu.
  • QoS = 1 : le message sera délivré au moins une fois (« at least once ») → garantie de réception avec retransmission si besoin.
  • QoS = 2 : le message sera délivré une fois (« exactly once ») → garantie sans perte (il ne sera pas perdu car il est sauvegardé) donc plus lent mais plus sûr.

Protocole

Le protocole MQTT fonctionne sur TCP en utilisant le port 1883.

On capture un échange publish entre un client (localhost) et un broker (localhost) avec wireshark :

Les trames 1, 2 et 3 correspondent à l’établissement classique (SYN-SYN/ACK-ACK) du mode connecté de TCP ici du client publisher vers le port 1883 du broker et les trames 13 et 14 correspondent à sa libération.

Les trames 4, 6, 8, 9 et 10 transportent le protocle MQTT. On filtre maintenant seulement l’échange MQTT :

L’échange peut se représenter de la manière suivante :

  • [trame 4] Client1 → Connect Command → Broker

  • [trame 6] Client1 ← Connect Ack ← Broker

  • [trame 8] Client1 → Publish Message → Broker

  • [trame 9] Client1 → Disconnect Req → Broker

  • [trame 10] Client2 ← Publish Message ← Broker

Capture wireshark : mqtt-publish.pcap

Remarque : le Client2 est ici un « subscriber » déjà connecté au broker.

Capture wireshark : mqtt-subscribe.pcap

Tests

Pour réaliser quelques essais en MQTT, il faut tout d’abord un broker. On utilisera Mosquitto.

Installation du broker MQTT Mosquitto :

$ sudo apt-get install mosquitto

$ mosquitto -v
mosquitto version 1.4.15 (build date Sat, 07 Apr 2018 11:16:43 +0100) starting
Using default config.
Opening ipv4 listen socket on port 1883.
...

Les ports d’écoute du broker Moquito sont :

  • 1883 : MQTT, unencrypted
  • 8883 : MQTT, encrypted
  • 8884 : MQTT, encrypted, client certificate required
  • 8080 : MQTT over WebSockets, unencrypted
  • 8081 : MQTT over WebSockets, encrypted

Le service mosquitto est contrôlé par systemd si celui-ci est installé :

$ systemctl status mosquitto
mosquitto.service - LSB: mosquitto MQTT v3.1 message broker
   Loaded: loaded (/etc/init.d/mosquitto; generated)
   Active: active (running) since Sun 2018-11-18 09:28:52 CET; 12s ago
     Docs: man:systemd-sysv-generator(8)
    Tasks: 1 (limit: 4915)
   CGroup: /system.slice/mosquitto.service
           └─25349 /usr/sbin/mosquitto -c /etc/mosquitto/mosquitto.conf

nov. 18 09:28:52 sedatech systemd[1]: Starting LSB: mosquitto MQTT v3.1 message broker...
nov. 18 09:28:52 sedatech mosquitto[25308]:  * Starting network daemon: mosquitto
nov. 18 09:28:52 sedatech mosquitto[25308]:    ...done.
nov. 18 09:28:52 sedatech systemd[1]: Started LSB: mosquitto MQTT v3.1 message broker.

Le fichier de configuration par défaut est /etc/mosquitto/mosquitto.conf et les messages de journalisation (log) seront stockés dans /var/log/mosquitto/mosquitto.log.

Installation des clients pour publier et souscrire à des sujets :

$ sudo apt-get install mosquitto-clients

On dispose maintenant de deux « clients » en mode CLI :

Les options de bases sont :

  • -h : hostname (par défaut localhost)
  • -t : le « topic »
  • -m : le message

Pour créer un échange de messages MQTT, on va ouvrir (au moins) deux terminaux, un pour le publisher et l’autre pour le subscriber.

  • terminal subscriber :
$ mosquitto_sub -t greeting
  • terminal publisher :
$ mosquitto_pub -t greeting -m "Hello World !"

Exemple simulant des capteurs de température :

$ mosquitto_sub -h 192.168.52.12 -t "capteurs/#" -v

On simule l’envoi de températures :

$ mosquitto_pub -h 192.168.52.12 -t capteurs/temperature/salon -m 23.0
$ mosquitto_pub -h 192.168.52.12 -t capteurs/temperature/chambre -m 21.0

Les valeurs peuvent être de type numérique ou du texte (au format JSON par exemple).

Programmation C

Il existe une bibliothèque C libmosquitto pour écrire un client MQTT :

$ sudo apt-get install libmosquitto-dev

Quelques fonctions de la bibliothèque libmosquitto :

  • mosquitto_lib_version : la version de la bibliothèque
  • mosquitto_lib_init : initialise la bibliothèque
  • mosquitto_lib_cleanup : libère l’utilisation de la bibliothèque
  • mosquitto_new : créer un client
  • mosquitto_destroy : détruire un client
  • mosquitto_reinitialize : détruit un client et en crée un nouveau
  • mosquitto_username_pw_set : le userid et le password pour l’authentification
  • mosquitto_connect : connecter un client au broker
  • mosquitto_connect_bind : idem mais contraint l’interface à faire un bind
  • mosquitto_connect_async : connexion asynchrone
  • mosquitto_reconnect : reconnexion après une connexion perdue
  • mosquitto_reconnect_async : idem en asynchrone
  • mosquitto_disconnect : déconnexion d’un client
  • mosquitto_publish : publish un message avec un sujet
  • mosquitto_subscribe : subscribe aux messages d’un sujet
  • mosquitto_unsubscribe : unsubscribe aux messages d’un sujet
  • mosquitto_loop : boucle de traitement des messages entrants en provenance du broker
  • mosquitto_loop_forever : idem mais continue le traitement jusqu’à ce que le client soit déconnecté
  • mosquitto_loop_read
  • mosquitto_loop_write
  • mosquitto_socket : retourne la socket TCP/IP bas niveau
  • mosquitto_want_write : retourne vrai si on est en attente d’envoi de données au broker
  • mosquitto_loop_start : démarre un thread pour triater la boucle mosquitto_loop en arrière-plan
  • mosquitto_loop_end : arrête un thread

Exemple :

#include <stdio.h>
#include <string.h>
#include <mosquitto.h>

/*
 * gcc publisher.c -o publisher -lmosquitto
 */

int main(int argc, char *argv[]) 
{
    char *broker = "192.168.52.12";
    int port = 1883;
    char *message = "hello world !";
    char *sujet = "greeting";
    struct mosquitto *client;
    int ret = 0;
    
    // initialise la bibliothèque
    mosquitto_lib_init();
    
    // crée un client    
    // parametres : generate an id, create a clean session, no callback param 
    client = mosquitto_new(NULL, true, NULL);
    
    ret = mosquitto_connect(client, broker, port, false); // pas de connexion persistante
    if (ret != MOSQ_ERR_SUCCESS) 
    {
        perror("mosquitto_connect");
        return(ret);        
    }
    
    // publie un message dans le sujet
    // parametres : client handle, message id, topic, length of message, message to be sent, QoS, retained
    mosquitto_publish(client, NULL, sujet, strlen(message)+1, (const void *)message, 0, false);

    // détruit le client
    mosquitto_destroy(client);
    
    // libère les ressources MQTT
    mosquitto_lib_cleanup();
    
    return 0;
}

Code source : publisher.c

Programmation Qt

Qt For Automation fournit des modules supplémentaires pour la communication M2M (machine à machine) dont Qt MQTT.

Qt MQTT fournit une implémentation conforme à la norme MQTT. Il permet aux applications d’agir en tant qu’IHM de télémétrie et aux appareils de publier des données de télémétrie.

Remarque : Qt MQTT fait parti de Qt For Automation et pas directement de Qt.

$ sudo git clone  git://code.qt.io/qt/qtmqtt.git

$ cd qtmqtt/

$ sudo qmake

$ sudo make

$ sudo make install

En cas d’erreurs, il est possible que cela soit du à la version de Qt. Dans ce cas, il faut sélectionner la branche qui correspond à la version de Qt utilisée.

Par exemple :

$ qmake -v
QMake version 3.1
Using Qt version 5.11.3 in /usr/lib/arm-linux-gnueabihf

$ sudo git clone  git://code.qt.io/qt/qtmqtt.git

$ cd qtmqtt/

$ sudo git checkout 5.11.2

$ sudo qmake

$ sudo make

$ sudo make install

Liens :

Pour accèder aux classes du module Qt MQTT, il faut ajouter dans son fichier de projet .pro:

QT += mqtt

Les classes du module Qt MQTT sont :

Créer un client MQTT :

#include <QtMqtt/QtMqtt>
#include <QtMqtt/QMqttClient>

...
QMqttClient *client;
...

...
client = new QMqttClient(this);
client->setHostname("192.168.52.12");
client->setPort(1883);
...

Ensuite, il est possible de se connecter :

client->connectToHost();

Et au final de se déconnecter :

client->disconnectFromHost();

Une fois connecté, on s’abonnera à un sujet :

QMqttSubscription *subscription;
...

subscription = client->subscribe(QLatin1String("greeting"));
if (!subscription) 
{
    QMessageBox::critical(this, "Erreur", "Impossible de s'abonner !");
    return;
}

On peut publier un message :

QByteArray message = "hello world Qt !";
quint8 qos = 1;
client->publish(QLatin1String("greeting"), message, qos);

Et on pourra recevoir les messages du broker : il faut connecter le signal messageReceived() à un slot qui recevra en paramètres le message (un QByteArray) et le topic (un QMqttTopicName)

// connexion signal/slot
connect(client, SIGNAL(messageReceived(const QByteArray &, const QMqttTopicName &)), this, SLOT(messageRecu(const QByteArray &, const QMqttTopicName &)));

// le slot
void XXX::messageRecu(const QByteArray &message, const QMqttTopicName &topic)
{
    qDebug() << QDateTime::currentDateTime().toString() << topic.name() << message;
}

Pour gérer la connexion au niveau de l’application, les signaux stateChanged(), connected() et disconnected() peuvent être utilisés. On a vu dans la capture wireshark ci-dessus, un échange de Ping Request et Ping Response : pour faire une requête, on appellera requestPing() et la réponse sera signalée par pingResponseReceived().

Exemple (sans IHM) :

// Fichier .h

#ifndef DIALOG_H
#define DIALOG_H

#include <QDialog>
#include <QtMqtt/QtMqtt>

class Dialog : public QDialog
{
    Q_OBJECT
private:
    QMqttClient *client;
    QMqttSubscription *subscription;

public:
    Dialog(QWidget *parent = 0);
    ~Dialog();

public slots:
    void connecte();
    void deconnecte();
    void messageRecu(const QByteArray &message, const QMqttTopicName &topic);
};

#endif // DIALOG_H

// Fichier .cpp
#include "dialog.h"
#include <QDebug>
#include <QMessageBox>

Dialog::Dialog(QWidget *parent) : QDialog(parent)
{
    client = new QMqttClient(this);
    client->setHostname("192.168.52.12");
    client->setPort(1883);

    connect(client, SIGNAL(connected()), this, SLOT(connecte()));
    connect(client, SIGNAL(disconnected()), this, SLOT(deconnecte()));
    connect(client, SIGNAL(messageReceived(const QByteArray &, const QMqttTopicName &)), this, SLOT(messageRecu(const QByteArray &, const QMqttTopicName &)));

    client->connectToHost();
}

Dialog::~Dialog()
{
    client->disconnectFromHost();
}

void Dialog::connecte()
{
    subscription = client->subscribe(QLatin1String("greeting"));
    if (!subscription)
    {
        QMessageBox::critical(this, "Erreur", "Impossible de s'abonner au broker!");
        return;
    }
}

void Dialog::deconnecte()
{
    QMessageBox::critical(this, "Information", "Déconnexion du broker");
}

void Dialog::messageRecu(const QByteArray &message, const QMqttTopicName &topic)
{
    qDebug() << QDateTime::currentDateTime().toString() << topic.name() << message;
}

Code source : qt-mqtt-client.zip

The Things Network est un réseau LoRaWAN open source qui est disponible dans plusieurs pays et basé sur une communauté de plus de 17000 membres. Il peut être utilisé sans contrainte commerciale ou privée. Things Network utilise MQTT pour publier les activations et les messages des appareils référencés, mais il permet également de publier un message pour un appareil spécifique en réponse.

Lire : TTN MQTT API

Cela change la connexion :

  • Host : ‹region›.thethings.network, where ‹region› is last part of the handler you registered your application to, e.g. eu
  • Port : 1883 or 8883 for TLS
  • Username : Application ID
  • Password : Application Access Key

Soit :

client = new QMqttClient(this);
client->setHostname("eu.thethings.network");
client->setPort(1883);
client->setUsername(Application_ID);
client->setPassword(Application_Access_Key);

On distinguera les deux types messages :

  • Uplink Messages

→ Topic : <AppID>/devices/<DevID>/up

$ mosquitto_sub -h eu.thethings.network:1883 -d -t 'my-app-id/devices/my-dev-id/up'

Uplink Fields : my-app-id/devices/my-dev-id/up/<field>. Les données seront une chaîne avec un encodage de style JSON.

  • Downlink Messages

→ Topic : <AppID>/devices/<DevID>/down

$ mosquitto_pub -h <Region>.thethings.network:1883 -d -t 'my-app-id/devices/my-dev-id/down' -m '{"port":1,"payload_raw":"Base64 message"}'

La classe QByteArray possède une méthode toBase64() pour encoder la partie “Base64 message”.

Downlink Fields : Au lieu de payload_raw, on peut également utiliser payload_fields.

$ mosquitto_pub -h <Region>.thethings.network:1883 -d -t 'my-app-id/devices/my-dev-id/down' -m '{"port":1,"payload_fields":{"led":true}}'

Qt pour Android

Qt MQTT fait parti de Qt For Automation et pas directement de Qt. Qt For Automation fournit des modules supplémentaires pour la communication M2M (machine à machine) dont Qt MQTT.

La version de Qt utilisée ici est la 5.10.1. La fabrication de QtMqtt nécessite une installation de Qt5 pour Android fonctionnelle.

On utilisera le script build_qt_mqtt.sh qu’il faudra paramétrer en fonction de son installation :

#!/bin/bash

export NDK_ROOT="/home/tv/Android/Sdk/android-ndk-r10e"
export ANDROID_NDK_ROOT=$NDK_ROOT
export QT_ROOT="/opt/Qt5.10.1"
export SR="$NDK_ROOT/platforms/android-19/arch-arm"
export BR="$NDK_ROOT/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86/bin/arm-linux-androideabi-"
platform=android-armv7
platform_sort=arm
qmake="$QT_ROOT/5.10.1/android_armv7/bin/qmake"

dir=qtmqtt
rm -rf $dir
git clone git://code.qt.io/qt/qtmqtt.git

[ ! -f "$qmake" ] && { echo "qmake introuvable"; exit 1; }
[ ! -x "$qmake" ] && { echo "qmake permission execution manquante"; exit 1; }

pushd $dir
    make clean
    $qmake "QMAKE_CXX=$BR"g++ "QMAKE_LINK=$BR"g++ -o Makefile qtmqtt.pro
    make || exit 1
    make install || exit 1
popd

echo
echo "fini"
echo
rm -rf $dir

Le site de Qt fournit l’exemple Quick MQTT Example pour tester.

Code source : qt-mqtt-client.zip

Exemple Android/Java

En Java sous Android, il faudra utiliser le client Paho MQTT et le service Android fournis par Eclipse.

Lien : MQTT

Liens

Retour au sommaire