MQTT et IoT#

MQTT (Message Queuing Telemetry Transport) est un protocole de messagerie publish/subscribe conçu par Andy Stanford-Clark et Arlen Nipper en 1999 pour IBM. Initialement développé pour la télémétrie sur des liaisons satellite à faible bande passante, MQTT s’est imposé comme le protocole de référence de l’Internet des Objets (IoT) grâce à ses propriétés uniques : faible empreinte mémoire, faible consommation réseau, gestion robuste des connexions instables, et modèle publish/subscribe découplant producteurs et consommateurs de données.

Ce chapitre couvre l’architecture pub/sub, le protocole MQTT en détail, les niveaux de QoS, MQTT 5.0, et les cas d’usage IoT. Le code paho-mqtt est présenté en mode illustratif (broker requis) ; des simulations matplotlib illustrent le protocole.

Hide code cell source

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.lines as mlines
import seaborn as sns
import pandas as pd
import struct
import json
import time
import random
from datetime import datetime, timedelta

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
np.random.seed(42)

Architecture publish/subscribe#

Modèle pub/sub vs requête-réponse#

Le modèle publish/subscribe (pub/sub) est fondamentalement différent du modèle requête-réponse de HTTP :

Requête-réponse (HTTP/REST) :

  • Couplage direct : le client connaît l’adresse du serveur

  • Synchrone : le client attend la réponse

  • 1-à-1 : une requête produit une réponse

Publish/Subscribe (MQTT) :

  • Découplage total : producteurs et consommateurs ne se connaissent pas

  • Asynchrone : le producteur publie et continue ; les consommateurs reçoivent quand ils sont prêts

  • 1-à-N ou N-à-N : un message peut atteindre zéro, un ou plusieurs consommateurs

Les trois acteurs du modèle MQTT :

  • Broker : serveur central qui reçoit tous les messages et les redistribue aux abonnés concernés (Mosquitto, HiveMQ, EMQX, AWS IoT Core)

  • Publisher (éditeur) : client qui envoie des messages sur un topic

  • Subscriber (abonné) : client qui s’inscrit à un ou plusieurs topics et reçoit les messages correspondants

Un client peut être à la fois publisher et subscriber

Dans MQTT, un même client peut publier sur certains topics et s’abonner à d’autres simultanément. Par exemple, un capteur intelligent publie ses mesures mais s’abonne à un topic de commandes pour recevoir des instructions de configuration.

Hide code cell source

# Architecture broker/pub/sub
fig, ax = plt.subplots(figsize=(13, 8))
ax.set_xlim(0, 14)
ax.set_ylim(0, 9)
ax.axis("off")
ax.set_title("Architecture MQTT : Broker, Publishers et Subscribers", fontsize=13, fontweight="bold")

# Broker central
broker_rect = mpatches.FancyBboxPatch((5.5, 3.5), 3, 2,
                                       boxstyle="round,pad=0.15",
                                       facecolor="#FF9800", edgecolor="#E65100",
                                       alpha=0.9, linewidth=3, zorder=5)
ax.add_patch(broker_rect)
ax.text(7, 4.7, "BROKER MQTT", ha="center", va="center",
        fontsize=12, fontweight="bold", color="white", zorder=6)
ax.text(7, 4.2, "Mosquitto / EMQX / HiveMQ", ha="center", va="center",
        fontsize=8.5, color="#FFF8E1", zorder=6)
ax.text(7, 3.8, "Port 1883 (MQTT) / 8883 (TLS)", ha="center", va="center",
        fontsize=8, color="#FFF8E1", zorder=6)

# Publishers (gauche)
publishers = [
    (1.2, 7.5, "Capteur\nTempérature", "#2196F3", "home/salon/temp → 23.4°C"),
    (1.2, 5.5, "Capteur\nHumidité", "#2196F3", "home/salon/hum → 55%"),
    (1.2, 3.5, "Capteur\nCO₂", "#2196F3", "home/air/co2 → 412ppm"),
    (1.2, 1.5, "Compteur\nÉlectrique", "#2196F3", "home/energie/kwh → 3.4"),
]

for x, y, label, color, topic in publishers:
    rect = mpatches.FancyBboxPatch((x - 1.1, y - 0.55), 2.2, 1.1,
                                    boxstyle="round,pad=0.1",
                                    facecolor=color, edgecolor="white",
                                    alpha=0.85, linewidth=2, zorder=5)
    ax.add_patch(rect)
    ax.text(x, y, label, ha="center", va="center",
            fontsize=8.5, color="white", fontweight="bold", zorder=6)
    # Flèche vers broker
    ax.annotate("", xy=(5.5, 4.5), xytext=(x + 1.1, y),
                arrowprops=dict(arrowstyle="->", color=color, lw=1.8,
                               connectionstyle="arc3,rad=0.1"))
    ax.text((x + 1.1 + 5.5) / 2 - 0.5, (y + 4.5) / 2 + 0.15,
            topic.split("→")[0].strip(), fontsize=6.5, color=color,
            style="italic", ha="center")

# Subscribers (droite)
subscribers = [
    (12.8, 7.5, "Application\nDomotique", "#4CAF50", "home/#"),
    (12.8, 5.5, "Dashboard\nMonitoring", "#4CAF50", "home/+/temp"),
    (12.8, 3.5, "Alerte\nCO₂", "#F44336", "home/air/#"),
    (12.8, 1.5, "Facture\nÉnergie", "#9C27B0", "home/energie/#"),
]

for x, y, label, color, abonnement in subscribers:
    rect = mpatches.FancyBboxPatch((x - 1.1, y - 0.55), 2.2, 1.1,
                                    boxstyle="round,pad=0.1",
                                    facecolor=color, edgecolor="white",
                                    alpha=0.85, linewidth=2, zorder=5)
    ax.add_patch(rect)
    ax.text(x, y, label, ha="center", va="center",
            fontsize=8.5, color="white", fontweight="bold", zorder=6)
    # Flèche depuis broker
    ax.annotate("", xy=(x - 1.1, y), xytext=(8.5, 4.5),
                arrowprops=dict(arrowstyle="->", color=color, lw=1.8,
                               connectionstyle="arc3,rad=-0.1"))
    ax.text((8.5 + x - 1.1) / 2 + 0.3, (y + 4.5) / 2 + 0.15,
            f"sub: {abonnement}", fontsize=6.5, color=color,
            style="italic", ha="center")

# Légende
ax.text(7, 0.5, "Le broker route les messages selon le topic des abonnements",
        ha="center", fontsize=9, color="#37474F", style="italic")

plt.tight_layout()
plt.show()
_images/41d89e86a1eb62829ff1b53daae56dbf0e360bd9a8fe90d59992d2f35ffbb163.png

Le protocole MQTT en détail#

Structure des paquets MQTT#

Chaque paquet MQTT commence par un en-tête fixe de 1 à 5 octets, suivi d’un en-tête variable et d’une payload.

En-tête fixe :

Octet 1 : [Type (4 bits)] [Flags (4 bits)]
Octets 2-5 : Remaining Length (1-4 octets, encodé varint MQTT)

Types de paquets MQTT principaux :

Code

Nom

Direction

Description

1

CONNECT

C→S

Établissement de connexion

2

CONNACK

S→C

Accusé de connexion

3

PUBLISH

Bidirectionnel

Publication d’un message

4

PUBACK

C↔S

Accusé QoS 1

5

PUBREC

C↔S

Reçu QoS 2 (étape 1)

6

PUBREL

C↔S

Relâcher QoS 2 (étape 2)

7

PUBCOMP

C↔S

Complété QoS 2 (étape 3)

8

SUBSCRIBE

C→S

Abonnement à des topics

9

SUBACK

S→C

Accusé d’abonnement

12

PINGREQ

C→S

Keepalive ping

13

PINGRESP

S→C

Keepalive pong

14

DISCONNECT

C→S

Déconnexion propre

Connexion CONNECT/CONNACK#

Hide code cell source

# Simulation d'un paquet CONNECT MQTT
def encoder_mqtt_string(s: str) -> bytes:
    """Encode une chaîne UTF-8 au format MQTT (2 octets longueur + données)."""
    encoded = s.encode("utf-8")
    return struct.pack(">H", len(encoded)) + encoded

def simuler_paquet_connect(client_id: str, keepalive: int = 60,
                            username: str = None, password: str = None,
                            will_topic: str = None, will_message: str = None) -> bytes:
    """
    Construit un paquet CONNECT MQTT 3.1.1 simplifié.
    """
    # En-tête variable CONNECT
    payload_parts = []

    # Nom du protocole
    payload_parts.append(encoder_mqtt_string("MQTT"))
    # Niveau du protocole (4 = MQTT 3.1.1, 5 = MQTT 5.0)
    payload_parts.append(bytes([4]))

    # Flags de connexion
    connect_flags = 0b00000010  # Clean Session
    if username:
        connect_flags |= 0b10000000
    if password:
        connect_flags |= 0b01000000
    if will_topic and will_message:
        connect_flags |= 0b00000100  # Will Flag
        connect_flags |= 0b00001000  # Will QoS = 1
    payload_parts.append(bytes([connect_flags]))

    # Keep Alive (2 octets, big-endian)
    payload_parts.append(struct.pack(">H", keepalive))

    # Payload : Client ID
    payload_parts.append(encoder_mqtt_string(client_id))

    # Will Topic et Message (si spécifiés)
    if will_topic and will_message:
        payload_parts.append(encoder_mqtt_string(will_topic))
        payload_parts.append(encoder_mqtt_string(will_message))

    # Username/Password
    if username:
        payload_parts.append(encoder_mqtt_string(username))
    if password:
        payload_parts.append(encoder_mqtt_string(password))

    variable_header_and_payload = b"".join(payload_parts)
    remaining_length = len(variable_header_and_payload)

    # Encodage de la Remaining Length (varint MQTT)
    rl_bytes = bytearray()
    x = remaining_length
    while True:
        encoded_byte = x % 128
        x = x // 128
        if x > 0:
            encoded_byte |= 128
        rl_bytes.append(encoded_byte)
        if x == 0:
            break

    # En-tête fixe : type CONNECT (0x10) + flags (0x00)
    fixed_header = bytes([0x10]) + bytes(rl_bytes)

    return fixed_header + variable_header_and_payload

# Construire un exemple de paquet CONNECT
paquet = simuler_paquet_connect(
    client_id="capteur-salon-001",
    keepalive=60,
    username="admin",
    password="secret",
    will_topic="home/capteurs/status",
    will_message='{"statut": "offline", "capteur": "capteur-salon-001"}'
)

print("Paquet CONNECT MQTT 3.1.1 :")
print("=" * 60)
print(f"Taille totale : {len(paquet)} octets")
print(f"En-tête fixe  : {' '.join(f'{b:02X}' for b in paquet[:2])}")
print(f"  → Type : CONNECT (0x10)")
print(f"  → Remaining Length : {paquet[1]} octets")
print()

# Décomposition visuelle
offset = 2  # Après l'en-tête fixe
print("En-tête variable et payload :")
print(f"  Nom protocole : {paquet[offset:offset+6]}")
offset += 6
print(f"  Version       : {paquet[offset]:02X} (MQTT 3.1.1 = 0x04)")
offset += 1
print(f"  Flags         : {paquet[offset]:08b}")
print(f"    Clean Session: {bool(paquet[offset] & 0x02)}")
print(f"    Will Flag    : {bool(paquet[offset] & 0x04)}")
print(f"    Username     : {bool(paquet[offset] & 0x80)}")
print(f"    Password     : {bool(paquet[offset] & 0x40)}")
offset += 1
keepalive_val = struct.unpack(">H", paquet[offset:offset+2])[0]
print(f"  Keep Alive    : {keepalive_val}s")
Paquet CONNECT MQTT 3.1.1 :
============================================================
Taille totale : 123 octets
En-tête fixe  : 10 79
  → Type : CONNECT (0x10)
  → Remaining Length : 121 octets

En-tête variable et payload :
  Nom protocole : b'\x00\x04MQTT'
  Version       : 04 (MQTT 3.1.1 = 0x04)
  Flags         : 11001110
    Clean Session: True
    Will Flag    : True
    Username     : True
    Password     : True
  Keep Alive    : 60s

Hide code cell source

# Diagramme séquentiel MQTT complet
fig, ax = plt.subplots(figsize=(12, 10))
ax.set_xlim(0, 12)
ax.set_ylim(0, 12)
ax.axis("off")
ax.set_title("Séquence complète MQTT : connexion, pub/sub, déconnexion", fontsize=12, fontweight="bold")

# Acteurs
ax.axvline(x=1.5, color="#2196F3", linewidth=2.5, ymin=0.05, ymax=0.95)
ax.axvline(x=6, color="#FF9800", linewidth=2.5, ymin=0.05, ymax=0.95)
ax.axvline(x=10.5, color="#4CAF50", linewidth=2.5, ymin=0.05, ymax=0.95)
ax.text(1.5, 11.7, "Client A\n(Capteur)", ha="center", fontsize=10,
        fontweight="bold", color="#2196F3")
ax.text(6, 11.7, "Broker\n(Mosquitto)", ha="center", fontsize=10,
        fontweight="bold", color="#FF9800")
ax.text(10.5, 11.7, "Client B\n(Dashboard)", ha="center", fontsize=10,
        fontweight="bold", color="#4CAF50")

msgs = [
    # Client B s'abonne d'abord
    (10.8, 10.5, 6, "SUBSCRIBE (topic: home/salon/+, QoS=1)", "#4CAF50"),
    (10.0, 6, 10.5, "SUBACK (QoS accordé: 1)", "#FF9800"),
    # Client A se connecte
    (9.0, 1.5, 6, "CONNECT (clientId, keepalive=60, will)", "#2196F3"),
    (8.0, 6, 1.5, "CONNACK (returnCode=0, sessionPresent=0)", "#FF9800"),
    # Client A publie
    (6.8, 1.5, 6, "PUBLISH (topic: home/salon/temp, payload: 23.4, QoS=1, msgId=1)", "#2196F3"),
    (6.0, 6, 1.5, "PUBACK (msgId=1)", "#FF9800"),
    # Broker distribue au subscriber
    (5.0, 6, 10.5, "PUBLISH (topic: home/salon/temp, payload: 23.4, QoS=1, msgId=42)", "#FF9800"),
    (4.2, 10.5, 6, "PUBACK (msgId=42)", "#4CAF50"),
    # Keepalive
    (3.0, 1.5, 6, "PINGREQ", "#9E9E9E"),
    (2.4, 6, 1.5, "PINGRESP", "#9E9E9E"),
    # Déconnexion
    (1.2, 1.5, 6, "DISCONNECT", "#F44336"),
    # Broker envoie le Will si déconnexion anormale
]

for y, x1, x2, label, color in msgs:
    ax.annotate("", xy=(x2, y), xytext=(x1, y),
                arrowprops=dict(arrowstyle="->", color=color, lw=1.8))
    mid = (x1 + x2) / 2
    ax.text(mid, y + 0.15, label, ha="center", fontsize=6.8, color=color)

# Zones colorées
ax.axhspan(7.5, 9.7, alpha=0.04, color="#2196F3")
ax.text(0.1, 8.6, "Connexion", fontsize=7.5, color="#2196F3", rotation=90, va="center")

ax.axhspan(5.5, 7.4, alpha=0.04, color="#4CAF50")
ax.text(0.1, 6.4, "Publication\nQoS 1", fontsize=7.5, color="#4CAF50", rotation=90, va="center")

ax.axhspan(2.5, 3.5, alpha=0.04, color="#9E9E9E")
ax.text(0.1, 3.0, "Keepalive", fontsize=7.5, color="#9E9E9E", rotation=90, va="center")

plt.tight_layout()
plt.show()
_images/a1a8c132f0bb4bc042dccc95658e9821d30bb52a347f2cbb0e76c5d25c8cf745.png

Topics MQTT : hiérarchie et wildcards#

Structure des topics#

Un topic MQTT est une chaîne UTF-8 hiérarchique utilisant / comme séparateur de niveaux. Il n’y a pas de limite de longueur formelle (max 65535 caractères en pratique).

Exemples de hiérarchies de topics :

home/salon/temperature
home/salon/humidite
home/cuisine/temperature
home/exterieur/pluie
usine/ligne1/machine3/temperature
usine/ligne1/machine3/vibration
flotte/vehicule/V001/position
flotte/vehicule/V001/carburant
$SYS/broker/clients/connected
$SYS/broker/messages/received

Wildcards#

MQTT définit deux caractères wildcard pour les abonnements :

+ (Single-Level Wildcard) : remplace exactement un niveau de topic.

  • home/+/temperature correspond à home/salon/temperature, home/cuisine/temperature, mais pas à home/salon/sous-sol/temperature

# (Multi-Level Wildcard) : remplace zéro ou plusieurs niveaux. Doit être le dernier caractère.

  • home/# correspond à home/salon/temperature, home/cuisine/humidite, home/exterieur/pluie, etc.

  • # seul correspond à tous les topics (sauf $SYS)

Topics système $SYS : topics réservés publiés par le broker pour son monitoring. Ils ne sont pas accessibles via # mais nécessitent un abonnement explicite à $SYS/#.

Hide code cell source

# Visualisation : arbre de topics et matching des wildcards
fig, ax = plt.subplots(figsize=(13, 8))
ax.set_xlim(0, 14)
ax.set_ylim(0, 9)
ax.axis("off")
ax.set_title("Hiérarchie de topics MQTT et wildcards", fontsize=13, fontweight="bold")

# Nœuds de l'arbre
noeuds = {
    "home": (7, 8.3),
    "salon": (3.5, 7),
    "cuisine": (7, 7),
    "exterieur": (10.5, 7),
    "temp_salon": (2, 5.7),
    "hum_salon": (4.5, 5.7),
    "temp_cuisine": (6, 5.7),
    "hum_cuisine": (8, 5.7),
    "pluie": (9.5, 5.7),
    "vent": (11.5, 5.7),
}

etiquettes = {
    "home": "home",
    "salon": "salon",
    "cuisine": "cuisine",
    "exterieur": "exterieur",
    "temp_salon": "temperature\n(23.4°C)",
    "hum_salon": "humidite\n(55%)",
    "temp_cuisine": "temperature\n(19.2°C)",
    "hum_cuisine": "humidite\n(62%)",
    "pluie": "pluie\n(true)",
    "vent": "vent\n(15km/h)",
}

# Topics complets
topics_complets = {
    "temp_salon": "home/salon/temperature",
    "hum_salon": "home/salon/humidite",
    "temp_cuisine": "home/cuisine/temperature",
    "hum_cuisine": "home/cuisine/humidite",
    "pluie": "home/exterieur/pluie",
    "vent": "home/exterieur/vent",
}

# Abonnements à visualiser
abonnements = {
    "home/salon/+": {"matches": ["temp_salon", "hum_salon"], "color": "#2196F3"},
    "home/+/temperature": {"matches": ["temp_salon", "temp_cuisine"], "color": "#4CAF50"},
    "home/#": {"matches": ["temp_salon", "hum_salon", "temp_cuisine", "hum_cuisine",
                            "pluie", "vent"], "color": "#FF9800"},
}

# Dessin des nœuds
couleurs_base = {
    "home": "#37474F",
    "salon": "#607D8B",
    "cuisine": "#607D8B",
    "exterieur": "#607D8B",
    "temp_salon": "#90A4AE",
    "hum_salon": "#90A4AE",
    "temp_cuisine": "#90A4AE",
    "hum_cuisine": "#90A4AE",
    "pluie": "#90A4AE",
    "vent": "#90A4AE",
}

# Arêtes
aretes = [
    ("home", "salon"), ("home", "cuisine"), ("home", "exterieur"),
    ("salon", "temp_salon"), ("salon", "hum_salon"),
    ("cuisine", "temp_cuisine"), ("cuisine", "hum_cuisine"),
    ("exterieur", "pluie"), ("exterieur", "vent"),
]

for p, e in aretes:
    x1, y1 = noeuds[p]
    x2, y2 = noeuds[e]
    ax.plot([x1, x2], [y1, y2], "-", color="#90A4AE", linewidth=1.5, zorder=1)

# Nœuds
for nom, (x, y) in noeuds.items():
    niveau = 1 if nom == "home" else (2 if nom in ("salon", "cuisine", "exterieur") else 3)
    radius = 0.6 if niveau == 1 else (0.55 if niveau == 2 else 0.5)
    cercle = plt.Circle((x, y), radius, color=couleurs_base[nom], alpha=0.9, zorder=4)
    ax.add_patch(cercle)
    ax.text(x, y, etiquettes[nom], ha="center", va="center",
            fontsize=6.5, color="white", fontweight="bold", zorder=5,
            multialignment="center")

# Légende des abonnements
y_leg = 4.3
for abonnement, info in abonnements.items():
    # Surligner les nœuds correspondants
    for noeud_id in info["matches"]:
        x, y = noeuds[noeud_id]
        cercle_hl = plt.Circle((x, y), 0.7, color=info["color"], alpha=0.25, zorder=3)
        ax.add_patch(cercle_hl)

    # Légende
    rect = mpatches.FancyBboxPatch((0.5, y_leg - 0.3), 3, 0.6,
                                    boxstyle="round,pad=0.1",
                                    facecolor=info["color"], alpha=0.2,
                                    edgecolor=info["color"], linewidth=1.5)
    ax.add_patch(rect)
    ax.text(2, y_leg, f"sub: {abonnement}", ha="center", va="center",
            fontsize=8, color=info["color"], fontweight="bold", family="monospace")
    ax.text(2, y_leg - 0.7, f"→ {len(info['matches'])} topic(s)",
            ha="center", fontsize=7.5, color=info["color"])
    y_leg -= 1.6

# Topics système
ax.text(7, 0.8, "$SYS/broker/clients/connected — Topics système (monitoring du broker)",
        ha="center", fontsize=8.5, color="#9C27B0", style="italic",
        bbox=dict(boxstyle="round", facecolor="#F3E5F5", alpha=0.7))

plt.tight_layout()
plt.show()
_images/6c4364e8583755ed501779d53ebf166c7cb654446b7478eada1f0bdf45c14c80.png

Niveaux de QoS#

MQTT définit trois niveaux de Qualité de Service (QoS) qui régissent la garantie de livraison des messages :

Hide code cell source

# Diagrammes des 3 niveaux de QoS
fig, axes = plt.subplots(1, 3, figsize=(15, 8))
fig.suptitle("Niveaux de QoS MQTT : garanties de livraison", fontsize=14, fontweight="bold")

def dessiner_qos(ax, titre, sous_titre, messages, broker_pos=5.5):
    ax.set_xlim(0, 11)
    ax.set_ylim(-0.5, len(messages) + 1)
    ax.set_title(titre, fontsize=11, fontweight="bold")
    ax.axis("off")
    ax.text(5.5, len(messages) + 0.7, sous_titre, ha="center", fontsize=8.5,
            color="#607D8B", style="italic")

    # Lignes des acteurs
    ax.axvline(x=1.5, color="#2196F3", linewidth=2, ymin=0.03, ymax=0.95)
    ax.axvline(x=broker_pos, color="#FF9800", linewidth=2, ymin=0.03, ymax=0.95)
    ax.axvline(x=9.5, color="#4CAF50", linewidth=2, ymin=0.03, ymax=0.95)

    ax.text(1.5, len(messages) + 0.3, "Publisher", ha="center", fontsize=9,
            fontweight="bold", color="#2196F3")
    ax.text(broker_pos, len(messages) + 0.3, "Broker", ha="center", fontsize=9,
            fontweight="bold", color="#FF9800")
    ax.text(9.5, len(messages) + 0.3, "Subscriber", ha="center", fontsize=9,
            fontweight="bold", color="#4CAF50")

    for i, (direction, x1, x2, label, color, style) in enumerate(messages):
        y = len(messages) - 1 - i
        ax.annotate("", xy=(x2, y), xytext=(x1, y),
                    arrowprops=dict(arrowstyle="->", color=color, lw=1.8,
                                   linestyle=style if style else "-"))
        mid = (x1 + x2) / 2
        ax.text(mid, y + 0.18, label, ha="center", fontsize=7, color=color)

# QoS 0 : At Most Once
qos0 = [
    ("→", 1.5, 5.5, "PUBLISH (QoS=0, fire and forget)", "#2196F3", None),
    ("→", 5.5, 9.5, "PUBLISH (QoS=0, livraison non garantie)", "#FF9800", None),
    ("X", 1.5, 5.5, "(perte possible — aucun accusé)", "#9E9E9E", "dashed"),
    ("X", 5.5, 9.5, "(perte possible — aucun accusé)", "#9E9E9E", "dashed"),
]
msgs_qos0 = [
    ("→", 1.5, 5.5, "PUBLISH (QoS=0)", "#2196F3", None),
    ("→", 5.5, 9.5, "PUBLISH (QoS=0)", "#FF9800", None),
]
dessiner_qos(axes[0], "QoS 0\nAt Most Once", "0 ou 1 livraison, aucun accusé\n→ Capteurs haute fréquence, pertes acceptables", msgs_qos0)
axes[0].text(5.5, 0.3, "Pas d'accusé de réception\n→ Perte possible sans retransmission",
             ha="center", fontsize=7.5, color="#F44336", style="italic")

# QoS 1 : At Least Once
msgs_qos1 = [
    ("→", 1.5, 5.5, "PUBLISH (QoS=1, msgId=1)", "#2196F3", None),
    ("←", 5.5, 1.5, "PUBACK (msgId=1)", "#FF9800", None),
    ("→", 5.5, 9.5, "PUBLISH (QoS=1, msgId=42)", "#FF9800", None),
    ("←", 9.5, 5.5, "PUBACK (msgId=42)", "#4CAF50", None),
    ("→", 1.5, 5.5, "PUBLISH (ré-émission si timeout)", "#9E9E9E", "dashed"),
]
dessiner_qos(axes[1], "QoS 1\nAt Least Once", "Au moins 1 livraison, accusé PUBACK\n→ Alertes, commandes critiques", msgs_qos1)
axes[1].text(5.5, 0.3, "Doublons possibles si PUBACK perdu\n→ Les abonnés doivent dédupliquer",
             ha="center", fontsize=7.5, color="#FF9800", style="italic")

# QoS 2 : Exactly Once
msgs_qos2 = [
    ("→", 1.5, 5.5, "PUBLISH (QoS=2, msgId=1)", "#2196F3", None),
    ("←", 5.5, 1.5, "PUBREC (msgId=1)", "#FF9800", None),
    ("→", 1.5, 5.5, "PUBREL (msgId=1)", "#2196F3", None),
    ("←", 5.5, 1.5, "PUBCOMP (msgId=1)", "#FF9800", None),
    ("→", 5.5, 9.5, "PUBLISH (QoS=2, msgId=42)", "#FF9800", None),
    ("←", 9.5, 5.5, "PUBREC (msgId=42)", "#4CAF50", None),
    ("→", 5.5, 9.5, "PUBREL (msgId=42)", "#FF9800", None),
    ("←", 9.5, 5.5, "PUBCOMP (msgId=42)", "#4CAF50", None),
]
dessiner_qos(axes[2], "QoS 2\nExactly Once", "Exactement 1 livraison, 4 échanges\n→ Transactions, commandes de paiement", msgs_qos2)
axes[2].text(5.5, 0.1, "Garantie stricte mais coût de 2 RTT\n→ À utiliser avec parcimonie",
             ha="center", fontsize=7.5, color="#9C27B0", style="italic")

plt.tight_layout()
plt.show()
_images/3e5ade3bdf046455bf7609268a8b7c648f8f281f529f87278a7c0001a83b64a7.png

Retained messages et Last Will Testament#

Retained message (message retenu) : quand un publisher envoie un message avec le flag RETAIN=1, le broker conserve ce message. Tout nouvel abonné reçoit immédiatement le dernier message retenu dès son abonnement, sans attendre le prochain publish. Utile pour l’état actuel des capteurs.

# Publier avec rétention
publisher.publish("home/salon/temp", "23.4", retain=True)

# Tout nouvel abonné reçoit immédiatement "23.4"
# sans attendre la prochaine publication

Last Will Testament (Testament) : lors de la connexion, le client peut spécifier un message qui sera publié automatiquement par le broker si la connexion est perdue anormalement (perte réseau, crash) sans DISCONNECT propre.

# Configuration du testament lors de la connexion
client.will_set(
    topic="home/capteurs/status",
    payload='{"capteur": "salon-001", "statut": "offline"}',
    qos=1,
    retain=True,
)

Will vs DISCONNECT

Le testament n’est déclenché que si la connexion se perd de façon anormale (pas de DISCONNECT reçu par le broker). Une déconnexion propre via DISCONNECT supprime le testament. Cela permet de distinguer un arrêt prévu (maintenance) d’une panne.

MQTT 5.0 : nouvelles fonctionnalités#

MQTT 5.0 (OASIS, 2019) apporte des améliorations majeures par rapport à MQTT 3.1.1 :

Propriétés : chaque paquet peut transporter des propriétés supplémentaires (paires clé-valeur typées), permettant d’ajouter des métadonnées sans modifier le payload :

  • Message-Expiry-Interval : durée de validité d’un message retenu

  • Content-Type : type MIME du payload (application/json, etc.)

  • Correlation-Data : identifiant pour le pattern requête-réponse

  • User-Properties : propriétés personnalisées applicatives

Shared Subscriptions : plusieurs subscribers partagent la charge d’un topic sous la forme $share/{groupe}/{topic}. Le broker distribue les messages en round-robin ou load-balanced entre les membres du groupe.

# Subscribers dans le groupe "dashboard"
$share/dashboard/home/capteurs/+/temperature

Request/Response Pattern : MQTT 5.0 formalise le pattern requête-réponse avec les propriétés Response-Topic et Correlation-Data, permettant aux clients de recevoir des réponses sur un topic dédié.

Hide code cell source

# Comparaison MQTT 3.1.1 vs MQTT 5.0
fig, ax = plt.subplots(figsize=(13, 5))
ax.axis("off")

colonnes = ["Fonctionnalité", "MQTT 3.1.1", "MQTT 5.0"]
lignes = [
    ["Propriétés des paquets", "Non", "Oui (types, métadonnées riches)"],
    ["Codes de raison", "Basiques (0/1)", "127+ codes précis (CONNACK, PUBACK...)"],
    ["Shared subscriptions", "Non (hack propriétaire)", "Oui ($share/groupe/topic natif)"],
    ["Request/Response", "Non (implémentation manuelle)", "Oui (Response-Topic, Correlation-Data)"],
    ["Topic Alias", "Non", "Oui (compresser les topics longs)"],
    ["Message Expiry", "Non", "Oui (Message-Expiry-Interval)"],
    ["Subscription ID", "Non", "Oui (identifier l'abonnement à l'origine)"],
    ["Session Expiry", "Session clean ou persistante", "Intervalle configurable en secondes"],
    ["Auth ENHANCED", "Username/password uniquement", "ENHANCED_AUTH (OAuth, SCRAM...)"],
    ["Payload Format Indicator", "Non", "Oui (UTF-8 vs binaire)"],
    ["Content-Type", "Non", "Oui (application/json, etc.)"],
    ["Will Delay Interval", "Non", "Oui (délai avant publication du testament)"],
]

couleurs = []
for i, ligne in enumerate(lignes):
    if i % 2 == 0:
        couleurs.append(["#ECEFF1", "#FFF8E1", "#E8F5E9"])
    else:
        couleurs.append(["#F5F5F5", "#FFFDE7", "#F1F8E9"])

table = ax.table(
    cellText=lignes,
    colLabels=colonnes,
    cellLoc="center",
    loc="center",
    cellColours=couleurs,
)
table.auto_set_font_size(False)
table.set_fontsize(8.5)
table.scale(1, 1.55)

for j, (label, color) in enumerate(zip(colonnes, ["#37474F", "#FF6F00", "#2E7D32"])):
    table[0, j].set_facecolor(color)
    table[0, j].set_text_props(color="white", fontweight="bold")

ax.set_title("Comparaison MQTT 3.1.1 vs MQTT 5.0", fontsize=12, fontweight="bold", pad=15)
plt.tight_layout()
plt.show()
_images/e966a4a4f24edae10eb8fc98786cb53baf79dded75cac37ca56b203f23bee455.png

Mosquitto : installation et configuration#

Eclipse Mosquitto est l’implémentation open-source de référence du broker MQTT.

Installation#

# Ubuntu/Debian
sudo apt install mosquitto mosquitto-clients

# macOS (Homebrew)
brew install mosquitto

# Docker
docker run -it -p 1883:1883 -p 9001:9001 eclipse-mosquitto

Configuration de base#

# /etc/mosquitto/mosquitto.conf

# Port d'écoute
listener 1883

# Port TLS
listener 8883
certfile /etc/mosquitto/certs/server.crt
keyfile /etc/mosquitto/certs/server.key
cafile /etc/mosquitto/certs/ca.crt

# Authentification par fichier de mots de passe
password_file /etc/mosquitto/passwd
allow_anonymous false

# ACL (contrôle d'accès aux topics)
acl_file /etc/mosquitto/acl

# Logs
log_dest file /var/log/mosquitto/mosquitto.log
log_type all

# Persistance des messages QoS 1/2 et retained
persistence true
persistence_location /var/lib/mosquitto/

Fichier ACL#

# /etc/mosquitto/acl

# L'utilisateur "capteurs" peut publier sur home/capteurs/# et lire $SYS
user capteurs
topic write home/capteurs/#
topic read $SYS/broker/uptime

# L'utilisateur "dashboard" peut lire home/#
user dashboard
topic read home/#
topic read $SYS/#

# Un utilisateur pattern (utilise %u pour le nom d'utilisateur)
pattern write maison/%u/#

Code Python avec paho-mqtt#

Les blocs suivants nécessitent un broker MQTT actif (Mosquitto local ou cloud MQTT).

Publisher avec paho-mqtt#

import paho.mqtt.client as mqtt
import json
import time
import random
import ssl

BROKER = "localhost"
PORT = 1883
# Pour TLS : PORT = 8883

def creer_client_mqtt(client_id: str, username: str = None,
                       password: str = None) -> mqtt.Client:
    """Crée et configure un client MQTT paho."""

    client = mqtt.Client(
        client_id=client_id,
        protocol=mqtt.MQTTv5,      # MQTT 5.0
        callback_api_version=mqtt.CallbackAPIVersion.VERSION2,
    )

    # Authentification
    if username and password:
        client.username_pw_set(username, password)

    # TLS (décommenter pour production)
    # client.tls_set(
    #     ca_certs="/etc/ssl/certs/ca-bundle.crt",
    #     certfile=None,
    #     keyfile=None,
    #     tls_version=ssl.PROTOCOL_TLSv1_2,
    # )

    # Last Will Testament
    client.will_set(
        topic=f"maison/capteurs/{client_id}/status",
        payload=json.dumps({"statut": "offline", "id": client_id}),
        qos=1,
        retain=True,
    )

    return client


def on_connect(client, userdata, flags, reason_code, properties):
    if reason_code == 0:
        print(f"Connecté au broker MQTT")
        # Publier le statut en ligne
        client.publish(
            f"maison/capteurs/{client.client_id}/status",
            json.dumps({"statut": "online"}),
            qos=1,
            retain=True,
        )
    else:
        print(f"Connexion échouée : code {reason_code}")


def publisher_capteur(client_id: str = "capteur-salon-001"):
    """Simule un capteur IoT publiant des mesures périodiques."""
    client = creer_client_mqtt(client_id)
    client.on_connect = on_connect

    client.connect(BROKER, PORT, keepalive=60)
    client.loop_start()

    try:
        sequence = 0
        while True:
            sequence += 1
            mesure = {
                "id": client_id,
                "seq": sequence,
                "ts": time.time(),
                "temperature": round(22 + random.gauss(0, 1.5), 2),
                "humidite": round(55 + random.gauss(0, 5), 1),
                "co2_ppm": int(400 + random.gauss(0, 30)),
            }

            # QoS 0 pour les mesures fréquentes (peut se perdre)
            client.publish(
                f"maison/capteurs/{client_id}/mesures",
                json.dumps(mesure),
                qos=0,
            )

            # QoS 1 pour les alertes (ne doit pas se perdre)
            if mesure["co2_ppm"] > 1000:
                alerte = {"capteur": client_id, "type": "co2_eleve",
                          "valeur": mesure["co2_ppm"]}
                client.publish(
                    "maison/alertes",
                    json.dumps(alerte),
                    qos=1,
                )

            print(f"[{sequence}] T={mesure['temperature']}°C H={mesure['humidite']}% CO₂={mesure['co2_ppm']}ppm")
            time.sleep(5)

    except KeyboardInterrupt:
        pass
    finally:
        client.publish(
            f"maison/capteurs/{client_id}/status",
            json.dumps({"statut": "offline_propre"}),
            qos=1,
            retain=True,
        )
        client.loop_stop()
        client.disconnect()


publisher_capteur()

Subscriber avec gestion des QoS#

import paho.mqtt.client as mqtt
import json
import time

BROKER = "localhost"
PORT = 1883

def on_connect(client, userdata, flags, reason_code, properties):
    print(f"Connecté, code={reason_code}")
    # Abonnements avec différents QoS
    abonnements = [
        ("maison/capteurs/#", 0),           # Mesures : QoS 0
        ("maison/alertes", 1),              # Alertes : QoS 1
        ("maison/commandes/#", 2),          # Commandes : QoS 2 (exactement une fois)
        ("$SYS/broker/clients/connected", 0),  # Monitoring broker
    ]
    client.subscribe(abonnements)

def on_message(client, userdata, msg):
    topic = msg.topic
    try:
        payload = json.loads(msg.payload.decode("utf-8"))
    except (json.JSONDecodeError, UnicodeDecodeError):
        payload = msg.payload.decode("utf-8", errors="replace")

    print(f"[{topic}] QoS={msg.qos} retain={msg.retain}")
    print(f"  Payload : {json.dumps(payload, indent=2, ensure_ascii=False)[:200]}")

def on_subscribe(client, userdata, mid, reason_codes, properties):
    for i, rc in enumerate(reason_codes):
        print(f"Abonnement #{i} accordé avec QoS {rc}")

def on_disconnect(client, userdata, flags, reason_code, properties):
    print(f"Déconnecté (code={reason_code}), reconnexion dans 5s...")
    time.sleep(5)
    client.reconnect()

client = mqtt.Client(
    client_id="dashboard-principal",
    protocol=mqtt.MQTTv5,
    callback_api_version=mqtt.CallbackAPIVersion.VERSION2,
)
client.on_connect = on_connect
client.on_message = on_message
client.on_subscribe = on_subscribe
client.on_disconnect = on_disconnect

client.connect(BROKER, PORT, keepalive=30)

# Loop bloquant (utiliser loop_start() + loop_stop() pour non-bloquant)
client.loop_forever()

QoS 2 : publication avec garantie exacte#

import paho.mqtt.client as mqtt
import json

def on_publish(client, userdata, mid, reason_code, properties):
    """Appelé quand le publish QoS 2 est complètement acquitté (PUBCOMP reçu)."""
    print(f"Message QoS 2 livré (mid={mid})")

client = mqtt.Client(
    client_id="capteur-critique",
    protocol=mqtt.MQTTv5,
    callback_api_version=mqtt.CallbackAPIVersion.VERSION2,
)
client.on_publish = on_publish
client.connect("localhost", 1883)
client.loop_start()

# Publication QoS 2 : exactement une livraison garantie
# Utiliser pour les commandes critiques (ouvrir une porte, déclencher une alarme)
result = client.publish(
    topic="maison/commandes/alarme",
    payload=json.dumps({"action": "activer", "zone": "exterieur"}),
    qos=2,
    retain=False,
)
result.wait_for_publish(timeout=10.0)
print(f"Publish QoS 2 : rc={result.rc}, mid={result.mid}")

client.loop_stop()
client.disconnect()

Simulation MQTT sans broker#

Hide code cell source

# Simulation complète d'un réseau MQTT avec plusieurs capteurs
np.random.seed(42)

class SimulateurMQTT:
    """Simule un broker MQTT et des clients pub/sub."""

    def __init__(self):
        self.abonnements = {}  # topic_pattern → [subscriber_ids]
        self.messages = []     # Historique des messages
        self.retenus = {}      # topic → dernier message retenu
        self.clients = {}      # client_id → {"connecte": bool, "topics": [...]}

    def connecter(self, client_id: str, will_topic: str = None, will_payload=None):
        self.clients[client_id] = {
            "connecte": True,
            "will_topic": will_topic,
            "will_payload": will_payload,
            "connect_ts": time.time(),
        }

    def souscrire(self, client_id: str, topic_pattern: str, qos: int = 0):
        if topic_pattern not in self.abonnements:
            self.abonnements[topic_pattern] = []
        self.abonnements[topic_pattern].append((client_id, qos))

    def _topic_match(self, pattern: str, topic: str) -> bool:
        """Vérifie si un topic correspond à un pattern avec wildcards."""
        if pattern == "#":
            return not topic.startswith("$")
        pattern_parts = pattern.split("/")
        topic_parts = topic.split("/")
        for i, pp in enumerate(pattern_parts):
            if pp == "#":
                return True
            if i >= len(topic_parts):
                return False
            if pp == "+":
                continue
            if pp != topic_parts[i]:
                return False
        return len(pattern_parts) == len(topic_parts)

    def publier(self, client_id: str, topic: str, payload, qos: int = 0,
                retain: bool = False, ts=None):
        ts = ts or time.time()
        message = {
            "publisher": client_id, "topic": topic,
            "payload": payload, "qos": qos, "retain": retain, "ts": ts,
            "deliveries": [],
        }
        if retain:
            self.retenus[topic] = message

        # Distribution aux abonnés
        for pattern, subscribers in self.abonnements.items():
            if self._topic_match(pattern, topic):
                for sub_id, sub_qos in subscribers:
                    if sub_id != client_id:
                        qos_effective = min(qos, sub_qos)
                        message["deliveries"].append((sub_id, qos_effective))

        self.messages.append(message)
        return message

broker = SimulateurMQTT()

# Connexion des clients
capteurs = ["capteur-salon", "capteur-cuisine", "capteur-exterieur"]
for c in capteurs:
    broker.connecter(c, will_topic=f"maison/{c}/status",
                     will_payload={"statut": "offline"})
broker.connecter("dashboard")
broker.connecter("alerte-service")

# Abonnements
broker.souscrire("dashboard", "maison/#", qos=1)
broker.souscrire("alerte-service", "maison/+/temperature", qos=1)
broker.souscrire("alerte-service", "maison/alertes", qos=2)

# Simulation de 120 secondes de mesures
debut = time.time()
temperatures = {"capteur-salon": 22.0, "capteur-cuisine": 19.0, "capteur-exterieur": 8.0}
historique_temp = {c: [] for c in capteurs}
timestamps = []

for t in range(120):
    ts = debut + t
    if t == 0:
        timestamps.append(ts)

    for capteur in capteurs:
        # Simulation de la température (marche aléatoire)
        temperatures[capteur] += np.random.normal(0, 0.3)
        temperatures[capteur] = np.clip(temperatures[capteur], -5, 35)
        historique_temp[capteur].append(temperatures[capteur])

        # Publier toutes les 10 secondes
        if t % 10 == 0:
            broker.publier(
                capteur,
                f"maison/{capteur}/temperature",
                round(temperatures[capteur], 1),
                qos=0, retain=True, ts=ts
            )
            # Alerte si température trop haute ou basse
            if temperatures[capteur] > 30 or temperatures[capteur] < 5:
                broker.publier(
                    capteur,
                    "maison/alertes",
                    {"capteur": capteur, "type": "temperature", "valeur": round(temperatures[capteur], 1)},
                    qos=2, ts=ts
                )

# Visualisation
fig, axes = plt.subplots(2, 2, figsize=(14, 8))
fig.suptitle("Simulation MQTT : 3 capteurs, 1 broker, 120 secondes", fontsize=13, fontweight="bold")

t_axis = list(range(120))
couleurs_capteurs = {"capteur-salon": "#F44336", "capteur-cuisine": "#2196F3",
                     "capteur-exterieur": "#4CAF50"}

# Graphique 1 : températures en temps réel
ax1 = axes[0][0]
for capteur, temps in historique_temp.items():
    ax1.plot(t_axis, temps, "-", color=couleurs_capteurs[capteur],
             linewidth=2, label=capteur.replace("capteur-", "").capitalize())
ax1.axhline(y=30, color="red", linestyle="--", linewidth=1.5, alpha=0.7, label="Seuil chaud (30°C)")
ax1.axhline(y=5, color="blue", linestyle="--", linewidth=1.5, alpha=0.7, label="Seuil froid (5°C)")
ax1.fill_between(t_axis, 5, 30, alpha=0.05, color="green")
ax1.set_xlabel("Temps (s)", fontsize=10)
ax1.set_ylabel("Température (°C)", fontsize=10)
ax1.set_title("Températures des 3 capteurs", fontsize=11)
ax1.legend(fontsize=8)
ax1.grid(True, alpha=0.3)

# Graphique 2 : activité du broker (messages par topic)
ax2 = axes[0][1]
topics_comptes = {}
for msg in broker.messages:
    top = msg["topic"].split("/")[1] if "/" in msg["topic"] else msg["topic"]
    topics_comptes[top] = topics_comptes.get(top, 0) + 1

tops = list(topics_comptes.keys())
counts = list(topics_comptes.values())
colors = [couleurs_capteurs.get("capteur-" + t.replace("capteur-", ""), "#9C27B0") for t in tops]
ax2.barh(tops, counts, color=["#F44336", "#2196F3", "#4CAF50", "#9C27B0"][:len(tops)], alpha=0.85)
ax2.set_xlabel("Nombre de messages", fontsize=10)
ax2.set_title(f"Messages par source\n({len(broker.messages)} total)", fontsize=11)
ax2.grid(True, alpha=0.3, axis="x")

# Graphique 3 : QoS utilisés
ax3 = axes[1][0]
qos_counts = {0: 0, 1: 0, 2: 0}
for msg in broker.messages:
    qos_counts[msg["qos"]] += 1
labels = [f"QoS {q}" for q in qos_counts.keys()]
sizes = list(qos_counts.values())
explode = (0.05, 0.05, 0.1)
couleurs_qos = ["#4CAF50", "#FF9800", "#F44336"]
ax3.pie(sizes, labels=labels, autopct="%1.0f%%", explode=explode,
        colors=couleurs_qos, startangle=90, textprops={"fontsize": 10})
ax3.set_title("Répartition des niveaux de QoS\nutilisés dans la simulation", fontsize=11)

# Graphique 4 : messages retenus (retained)
ax4 = axes[1][1]
topics_retenus = list(broker.retenus.keys())
valeurs_retenues = [broker.retenus[t]["payload"] for t in topics_retenus]
noms_courts = [t.replace("maison/", "") for t in topics_retenus]

ax4.barh(range(len(noms_courts)), [float(v) if isinstance(v, (int, float)) else 0
                                     for v in valeurs_retenues],
         color=["#F44336", "#2196F3", "#4CAF50"][:len(noms_courts)], alpha=0.85)
ax4.set_yticks(range(len(noms_courts)))
ax4.set_yticklabels(noms_courts, fontsize=8)
ax4.set_xlabel("Valeur retenue (°C)", fontsize=10)
ax4.set_title(f"Messages retenus (retained)\n{len(broker.retenus)} topics avec état actuel", fontsize=11)
for i, v in enumerate(valeurs_retenues):
    if isinstance(v, (int, float)):
        ax4.text(float(v) + 0.1, i, f"{v}°C", va="center", fontsize=9)
ax4.grid(True, alpha=0.3, axis="x")

plt.tight_layout()
plt.show()

print(f"\nRésumé de la simulation :")
print(f"  Messages publiés : {len(broker.messages)}")
print(f"  Messages retenus : {len(broker.retenus)}")
print(f"  Alertes déclenchées : {sum(1 for m in broker.messages if 'alertes' in m['topic'])}")
print(f"  Abonnements actifs : {sum(len(v) for v in broker.abonnements.values())}")
_images/0119d678b2e7f382a6e4df46a37b4c9edf300f2ca75a5c02daa1b32b3987ad9e.png
Résumé de la simulation :
  Messages publiés : 36
  Messages retenus : 3
  Alertes déclenchées : 0
  Abonnements actifs : 3

Use cases IoT#

Capteurs et domotique#

MQTT est le protocole standard de la domotique moderne. Des plateformes comme Home Assistant utilisent MQTT pour intégrer des milliers de types d’appareils. Le protocole Zigbee2MQTT traduit les trames Zigbee en messages MQTT, permettant à n’importe quel logiciel de domotique de contrôler des appareils Zigbee sans gateway propriétaire.

Flotte de véhicules connectés#

Hide code cell source

# Simulation de telemetrie de véhicules
fig, axes = plt.subplots(1, 2, figsize=(13, 5))

np.random.seed(123)
n_vehicules = 5
n_points = 60  # 60 secondes

# Simulation de vitesses et consommations
temps = np.arange(n_points)
couleurs_v = plt.cm.tab10(np.linspace(0, 1, n_vehicules))

ax1 = axes[0]
ax2 = axes[1]

for i in range(n_vehicules):
    vitesse = np.abs(50 + np.cumsum(np.random.normal(0, 5, n_points)))
    vitesse = np.clip(vitesse, 0, 130)
    consommation = 5 + vitesse * 0.05 + np.random.normal(0, 0.5, n_points)
    consommation = np.clip(consommation, 0, 15)

    label = f"V{i+1:03d}"
    ax1.plot(temps, vitesse, "-", color=couleurs_v[i], linewidth=1.5,
             label=label, alpha=0.8)
    ax2.scatter(vitesse, consommation, c=[couleurs_v[i]], alpha=0.4,
                s=10, label=label if i == 0 else None)

# Lignes de référence
ax1.axhline(y=50, color="blue", linestyle=":", alpha=0.5, label="50 km/h (ville)")
ax1.axhline(y=90, color="orange", linestyle=":", alpha=0.5, label="90 km/h (route)")
ax1.axhline(y=130, color="red", linestyle=":", alpha=0.5, label="130 km/h (autoroute)")

ax1.set_xlabel("Temps (s)", fontsize=10)
ax1.set_ylabel("Vitesse (km/h)", fontsize=10)
ax1.set_title(f"Télémétrie MQTT : {n_vehicules} véhicules\n"
               f"topic: flotte/vehicules/V*/vitesse",
              fontsize=11)
ax1.legend(fontsize=8, loc="upper right")
ax1.grid(True, alpha=0.3)

ax2.set_xlabel("Vitesse (km/h)", fontsize=10)
ax2.set_ylabel("Consommation (L/100km)", fontsize=10)
ax2.set_title("Corrélation vitesse / consommation\n(publication sur MQTT QoS 0)", fontsize=11)
ax2.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()
_images/8d3860613f2c975c8b3ce0c9f7e0965fa10f2033f2bb38502b0a1aa38498fcdf.png

Comparaison MQTT vs autres protocoles IoT#

Hide code cell source

# Tableau comparatif protocoles IoT
fig, ax = plt.subplots(figsize=(14, 5))
ax.axis("off")

colonnes = ["Critère", "MQTT", "CoAP", "AMQP", "HTTP/REST"]
lignes = [
    ["Transport", "TCP (TLS optionnel)", "UDP (DTLS)", "TCP (TLS)", "TCP (TLS)"],
    ["Modèle", "Pub/Sub via broker", "Requête-Réponse (REST-like)", "Pub/Sub + Queue", "Requête-Réponse"],
    ["Overhead en-tête", "2 octets min.", "4 octets min.", "Élevé", "Très élevé"],
    ["QoS", "0, 1, 2 natifs", "Confirmé/Non-confirmé", "Transactions ACID", "Aucun natif"],
    ["Broadcast/Multicast", "Oui (topics)", "Multicast UDP natif", "Échanges, routage", "Non"],
    ["Connexion persistante", "Oui (keepalive)", "Non (stateless)", "Oui", "Non (sauf WS)"],
    ["Consommation batterie", "Très faible", "Très faible", "Élevée", "Élevée"],
    ["Bande passante", "Très faible", "Très faible", "Moyenne", "Élevée"],
    ["Complexité client", "Faible", "Faible-Moyenne", "Élevée", "Faible"],
    ["Cas d'usage IoT", "Domotique, capteurs, véhicules", "Appareils contraints, M2M", "Finance, EAI, enterprise", "APIs web, mobile"],
]

couleurs = []
for i, ligne in enumerate(lignes):
    if i % 2 == 0:
        couleurs.append(["#ECEFF1", "#E8F5E9", "#E3F2FD", "#FFF3E0", "#FCE4EC"])
    else:
        couleurs.append(["#F5F5F5", "#F1F8E9", "#E1F5FE", "#FFF8E1", "#FFF0F4"])

table = ax.table(
    cellText=lignes,
    colLabels=colonnes,
    cellLoc="center",
    loc="center",
    cellColours=couleurs,
)
table.auto_set_font_size(False)
table.set_fontsize(8)
table.scale(1, 1.65)

col_colors = ["#37474F", "#FF6F00", "#1565C0", "#4A148C", "#B71C1C"]
for j, color in enumerate(col_colors):
    table[0, j].set_facecolor(color)
    table[0, j].set_text_props(color="white", fontweight="bold")

ax.set_title("Comparaison des protocoles IoT", fontsize=13, fontweight="bold", pad=20)
plt.tight_layout()
plt.show()
_images/10f05a3069061f58c346e0d48ba686c913b7d627136b9c6626df0441ab2415e1.png

Résumé#

MQTT est le protocole de référence de l’IoT pour de bonnes raisons : son overhead minimal (2 octets d’en-tête), ses niveaux de QoS adaptés aux réseaux instables, et son modèle pub/sub découplant producteurs et consommateurs sont parfaitement adaptés aux contraintes des objets connectés.

Points clés à retenir :

  • QoS 0 (fire and forget) : capteurs haute fréquence, pertes tolérées

  • QoS 1 (at least once) : alertes, notifications, garantie de réception mais doublons possibles

  • QoS 2 (exactly once) : commandes critiques, transactions, overhead le plus élevé

  • Retained messages : état actuel des capteurs immédiatement disponible pour les nouveaux abonnés

  • Last Will Testament : détection automatique des déconnexions anormales

  • MQTT 5.0 : propriétés riches, shared subscriptions, request/response natif

  • Mosquitto : broker de référence, configuration simple, ACL puissantes

L’écosystème MQTT s’étend bien au-delà de la domotique : véhicules connectés, industrie 4.0 (IIoT), agriculture de précision, smart cities, et toute application nécessitant une communication légère et robuste entre de nombreux appareils hétérogènes.