Projet MQTT

Le but de ce projet est de programmer un client et un serveur MQTT, conformément à un sous-ensemble (de taille raisonnable) de la spécification 3.1.

Présentation

MQTT (Message Queuing Telemetry Transport) est un protocole de publication de donnéees très utilisé notamment dans le domaine de l'IoT (Internet of Things) : typiquement 1 ou plusieurs capteurs (appelés publishers) publient régulièrement des mesures sur un serveur (appelé broker) qui se charge ensuite de faire suivre ces mesures à différents clients (appelés subscribers).

Vidéo d'introduction rapide à MQTT (François Riotte)

On trouve sur le web de nombreuses implémentations complètes et open source de ce protocole. Mosquitto est une de ces implémentations. Elle est déjà disponible sur les machines du CREMI.

Démo

Commençons par un exemple d'utilisation de Mosquitto.

demo

Voici les trames échangées lors de la démo ci-dessus, à analyser avec Wireshark : mqtt.pcap.

Dans cette démo, le subscriber, le publisher et le broker s'exécutent tous les trois sur la machine locale (127.0.0.1), respectivement avec les ports 35634, 35636, et 1883.

  • Les trames 4, 6, 8, 10 contiennent les paquets MQTT échangés entre le subscriber et le broker au moment du démarrage du subscriber.
  • Les trames 15, 17, 19, 23 contiennent les paquets MQTT échangés entre le publisher et le broker au moment de la publication de la première témpérature "24".
  • La trame 21 contient le paquet MQTT que le broker envoie au subscriber lors de la diffusion de la température "24".

Vous trouverez ici une description suffisamment précise sans être trop longue du format utilisé pour les paquets MQTT.

Objectif

L'objectif pédagogique de ce projet est double :

  • vous amenez à lire et comprendre une spécification,
  • vous amenez à produire une implémentation qui respecte un protocole.

Pour cela nous vous demandons d'implémenter en Python 3 un publisher, un subscriber et un broker offrant un sous-ensemble des fonctionnalités de MQTT tout en respectant strictement la spécification 3.1.1 du protocole. Le code que vous produirez devra être ""lisible, bien structuré, un minimum commenté et un minimum testé"". En passant un peu de temps à vous appliquer sur la qualité de votre code, vous en gagner beaucoup en temps de débugage.

Fonctionnalités

MQTT offre 3 niveaux de Qualité de Service (QoS en anglais) : de 0 la plus simple à 2 la plus sophistitquée. On ne considérera dans ce projet que la QoS 0.

Le protocole supporte de paquets de tailles relativement grandes. On supposera ici que tous les paquets ont une taille inférieure à 127 octets, et donc que la longueur des paquets sera codé sur 1 seul octet.

Par ailleurs, le protocole MQTT permet aussi de gérer l'authentification des clients. Là encore on considérera uniquement une version sans authentification. En revanche, dans sa version la plus aboutie, vos clients et serveurs doivent supporter l'option retain. Ainsi les seuls paquets que vous avez à considérer sont ceux de type : CONNECT, CONNACK, PUBLISH, SUBSCRIBE, SUBACK et DISCONNECT.

Voici les différents jalons à respecter :

1) Digérer la spécification du protocole pour en extraire les informations pertinantes pour votre projet. 2) Se faire la main en implémentant une fonction create_mqtt_publish_msg permettant de forger un paquet MQTT de type PUBLISH. Votre fonction doit renvoyer le paquet MQTT sous forme d'un byte array prêt à l'envoi avec la fonction socket.sendall(). (cf. activité VPL). Pour pouvoir être évaluée, votre fonction doit être testée avec l'outil doctest (cf. Annexe). 3) Implémenter les fonctionnalités du publisher (sans l'option retain). 4) Implémenter les fonctionnalités du subscriber. 5) Implémenter le broker en vous inspirant du serveur echo (en version select) vu en TP. 6) Modifier les implémentations du publisher et du broker pour gérer l'option retain.

Biblio

En complément, lisez attentivement les documents ci-dessous pour bien comprendre le fonctionnement du protocole MQTT :

Travail Demandé

Il s'agit d'un travail à réaliser en groupe de 2 étudiants.

Il est demandé de développer en langage Python 3 les programmes client (publisher+subscriber) et serveur (broker), conformément à la description du protocole MQTT fournies ci-dessus, en incluant les modifications demandées. On s'appuiera obligatoirement sur la bibliothèque socket et sur la couche de transport TCP pour réaliser ce projet.

Plus précisément, on demande de programmer les commandes ci-dessous en respectant la syntaxe suivante :

# server
$ ./mqtt-server.py -h
usage: mqtt-server [-h] [-p PORT] [-l LOG] [--debug]

Run the broker

optional arguments:
  -h, --help            show this help message and exit
  -p PORT, --port PORT  port used by the broker (default value: 1883)
  -l LOG, --log LOG     Filename to store log informations
  --debug               log debug informations


# client
$./mqtt-client -h
usage: mqtt-client [-h] [-H HOST] [-p PORT] [-i ID] [-l LOG] [--debug] [-r] -t TOPIC {pub,sub}

Run a publisher or a subsriber. Publisher read values (as strings) from the standard input and Subscriber
displays the values on the standard output.

positional arguments:
  {pub,sub}             indicate if the client is a publisher (pub) or a subscriber (sub)

optional arguments:
  -h, --help            show this help message and exit
  -H HOST, --host HOST  address of the broker (default value: localhost)
  -p PORT, --port PORT  port used by the broker (default value: 1883)
  -i ID, --id ID        client ID (default value: anonymous)
  -l LOG, --log LOG     Filename to store log informations
  --debug               log debug informations
  -r, --retain          Only for publisher: indicates that the values published must be retained by the
                        broker
  -t TOPIC, --topic TOPIC

Voici une démo où votre publisher lit les valeurs saisies au clavier. Remarquez dans la deuxième partie que le comportement du broker a changé lorsqu'on utilise l'option -r

demo

Voici une 2ième démo où votre publisher lit des valeurs à partir d'un fichier. Notez que le programme se termine correctement et sans erreur une fois toutes les valeurs lues.

demo

La commande cliente permet de lancer un publisher (option pub) ou un subscriber (option sub).

Pour vous aider à démarrer ce projet, nous vous fournissons la coquille du projet, qui se compose de trois fichiers :

Cette coquille réalise juste l'analyse des options et des arguments passées à ces commandes à l'aide du module argparse. Vous ne devez pas modifier les fichiers mqtt-client.py et mqtt-server.py.

N'essayez pas de réaliser tout le projet d'un coup, mais concentrez-vous sur les fonctionnalités principales, sans gérer les options ou les erreurs.

👉 Rendez votre travail sur Moodle. Votre code sera évalué automatiquement avec VPL : il est donc impératif de respecter scrupuleusement les consignes ! Votre code sera également relu par votre enseignant : il est donc impératif de soigner la qualité de son code (nommage des variables et fonctions cohérent, code commenté, éviter les fonctions trop longs ou trop complexes, éviter la duplication de code et les constantes magigues,...)

Annexes

Quelques astuces en Python pour vous aider.

Décoder une trame binaire

Voici un extrait de code illustrant comment décoder une requête utilisée dans un autre protocole (TFTP).

frame = b'\x00\x02test.txt\x00octet\x00'          # sample of write request as byte array
frame1 = frame[0:2]                               # frame1 = b'\x00\x02'
frame2 = frame[2:]                                # frame2 = b'test.txt\x00octet\x00'
opcode = int.from_bytes(frame1, byteorder='big')  # opcode = 2
args = frame2.split(b'\x00')                      # args = [b'test.txt', b'octet', b'']
filename = args[0].decode('ascii')                # filename = 'test.txt'
(20).to_bytes(2, byteorder='big')                 # encode the number 20 on 2 bytes
mode = args[1].decode('ascii')                    # mode = 'octet'

Documentation Python

Un peu d'aide pour Python 3 :

Tester une fonction avec doctest

La manière la plus rapide et la plus simple pour tester une fonction Python est d'utiliser l'outil doctest : https://docs.python.org/3/library/doctest.html.

Considérons un fichier my_example.py, qui contient une fonction que l'on souhaite tester. Cette fonction convertit en hexadécimal une chaîne de caractères UTF-8. Ainsi, il est possible d'écrire un petit test dans la documentation de cette fonction comme ceci.

def my_function(msg):
    """ Convert string into bytes
    >>> my_function("coucou").hex()
    '636f75636f75'
    """
    return bytes(msg, "utf-8")

Nota Bene : A partir de la version 3.8 de Python, il est possible d'ajouter un argument option à la fonction .hex(" ") pour rendre la sortie en hexadécimal plus lisible.

Pour lancer ce test, il suffit alors de faire :

$ python3 -m doctest -v my_example.py
Trying:
    my_function("coucou").hex()
Expecting:
    '636f75636f75'
ok
...
1 passed and 0 failed.
Test passed.