Tutoriels IoT

Tutoriel du client Paho MQTT C pour Linux embarqué

Un tutoriel Paho MQTT C pratique pour Linux embarqué : compilez la bibliothèque avec OpenSSL, écrivez un publisher et un subscriber TLS, faites une compilation croisée pour ARM et exécutez le tout de façon fiable sous systemd.

Client Paho MQTT C fonctionnant sur une carte Linux embarquée et publiant des données de capteurs en TLS vers un broker MQTT cloud
Essayez-le sur votre propre broker MQTTObtenez un broker MQTT privé et managé avec l’offre Free et partez d’exemples de code prêts à l’emploi.
Essayer gratuitement Exemples de code Aucune carte bancaire requise

La bibliothèque Paho MQTT C est le choix de référence lorsque vous avez besoin d'un client MQTT compact et peu dépendant sous Linux embarqué. Dans ce tutoriel, vous allez compiler Paho MQTT C avec OpenSSL, écrire un publisher et un subscriber TLS, faire une compilation croisée pour une carte ARM et exécuter le résultat sous forme de service systemd résilient connecté à un broker cloud.

Pourquoi Paho MQTT C pour Linux embarqué

Eclipse Paho C est écrit en C99 pur avec une seule dépendance optionnelle, OpenSSL, pour le TLS. Il tourne sur tout, d'un Raspberry Pi à une passerelle industrielle disposant de quelques mégaoctets de RAM, prend en charge MQTT 3.1, 3.1.1 et 5, et est packagé par la plupart des distributions et des couches Yocto. Si vous hésitez entre plusieurs langages et bibliothèques, la présentation des bibliothèques clientes MQTT le replace dans son contexte ; si vous préférez le C++, Paho propose aussi un wrapper C++ au-dessus de cette bibliothèque.

La bibliothèque est livrée en quatre variantes. Choisissez la bonne au moment de l'édition de liens :

Bibliothèque Style d'API TLS Usage typique
paho-mqtt3c Synchrone MQTTClient_* Non Outils simples sur des réseaux de confiance
paho-mqtt3cs Synchrone MQTTClient_* Oui La plupart des publishers et subscribers embarqués
paho-mqtt3a Asynchrone MQTTAsync_* Non Applications événementielles
paho-mqtt3as Asynchrone MQTTAsync_* Oui Applications événementielles avec TLS

Ce tutoriel utilise paho-mqtt3cs : l'API synchrone avec TLS. « Synchrone » signifie que les appels de connexion et de publication bloquent jusqu'à leur achèvement ; les messages entrants arrivent tout de même sur un thread d'arrière-plan via des callbacks.

Installer ou compiler la bibliothèque

Sous Debian, Ubuntu ou Raspberry Pi OS, la version packagée suffit généralement :

sudo apt update
sudo apt install -y build-essential libpaho-mqtt-dev libssl-dev ca-certificates

Pour obtenir la dernière version, ou pour faire une compilation croisée, compilez depuis les sources :

git clone https://github.com/eclipse-paho/paho.mqtt.c.git
cd paho.mqtt.c
cmake -B build -DPAHO_WITH_SSL=TRUE -DPAHO_BUILD_SAMPLES=FALSE -DPAHO_BUILD_STATIC=FALSE -DCMAKE_BUILD_TYPE=Release
cmake --build build -j"$(nproc)"
sudo cmake --install build
sudo ldconfig

La documentation officielle de Paho C décrit chaque structure et fonction utilisée ci-dessous.

Un publisher Paho MQTT C en TLS

Ce programme se connecte à broker.connectmq.com sur le port 8883, vérifie le certificat du serveur à l'aide du bundle de CA du système et publie un relevé toutes les cinq secondes. Il fonctionne de manière anonyme avec le serveur MQTT public gratuit, réservé aux tests, et lit les identifiants dans des variables d'environnement lorsque vous passez à un broker privé.

publisher.c :

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>
#include "MQTTClient.h"

#define ADDRESS   "ssl://broker.connectmq.com:8883"
#define CA_BUNDLE "/etc/ssl/certs/ca-certificates.crt"
#define QOS       1
#define TIMEOUT   10000L

static volatile sig_atomic_t running = 1;
static void on_signal(int sig) { (void)sig; running = 0; }

static void connection_lost(void *ctx, char *cause) {
    (void)ctx;
    fprintf(stderr, "connection lost: %s\n", cause ? cause : "unknown");
}

static int message_arrived(void *ctx, char *topic, int len, MQTTClient_message *msg) {
    (void)ctx; (void)len;
    MQTTClient_freeMessage(&msg);
    MQTTClient_free(topic);
    return 1;
}

static int connect_broker(MQTTClient client) {
    MQTTClient_connectOptions opts = MQTTClient_connectOptions_initializer;
    MQTTClient_SSLOptions ssl = MQTTClient_SSLOptions_initializer;

    ssl.trustStore = CA_BUNDLE;
    ssl.enableServerCertAuth = 1;
    ssl.verify = 1;                       /* vérifier aussi le nom d'hôte */
    ssl.sslVersion = MQTT_SSL_VERSION_TLS_1_2;

    opts.keepAliveInterval = 30;
    opts.cleansession = 1;
    opts.ssl = &ssl;
    opts.username = getenv("MQTT_USERNAME");   /* NULL = anonyme */
    opts.password = getenv("MQTT_PASSWORD");

    int rc = MQTTClient_connect(client, &opts);
    if (rc != MQTTCLIENT_SUCCESS)
        fprintf(stderr, "connect failed, rc=%d\n", rc);
    return rc;
}

int main(void) {
    const char *client_id = getenv("MQTT_CLIENT_ID");
    const char *prefix = getenv("MQTT_TOPIC_PREFIX");
    char id_buf[64], topic[256], payload[128];

    if (!client_id) {
        snprintf(id_buf, sizeof id_buf, "paho-c-%d", (int)getpid());
        client_id = id_buf;
    }
    if (!prefix) prefix = "connectmq/demo/paho-c-4821";
    snprintf(topic, sizeof topic, "%s/gateway1/temperature", prefix);

    signal(SIGINT, on_signal);
    signal(SIGTERM, on_signal);

    MQTTClient client;
    int rc = MQTTClient_create(&client, ADDRESS, client_id,
                               MQTTCLIENT_PERSISTENCE_NONE, NULL);
    if (rc != MQTTCLIENT_SUCCESS) {
        fprintf(stderr, "create failed, rc=%d\n", rc);
        return EXIT_FAILURE;
    }
    MQTTClient_setCallbacks(client, NULL, connection_lost, message_arrived, NULL);

    int backoff = 1;
    while (running && connect_broker(client) != MQTTCLIENT_SUCCESS) {
        sleep(backoff);
        if (backoff < 60) backoff *= 2;
    }
    printf("connected as %s, publishing to %s\n", client_id, topic);

    srand((unsigned)time(NULL));
    while (running) {
        if (!MQTTClient_isConnected(client)) {
            if (connect_broker(client) != MQTTCLIENT_SUCCESS) { sleep(5); continue; }
        }

        double temp = 20.0 + (rand() % 100) / 10.0;
        int n = snprintf(payload, sizeof payload,
                         "{\"ts\":%ld,\"temperature\":%.1f}", (long)time(NULL), temp);

        MQTTClient_message msg = MQTTClient_message_initializer;
        MQTTClient_deliveryToken token;
        msg.payload = payload;
        msg.payloadlen = n;
        msg.qos = QOS;
        msg.retained = 0;

        rc = MQTTClient_publishMessage(client, topic, &msg, &token);
        if (rc == MQTTCLIENT_SUCCESS)
            rc = MQTTClient_waitForCompletion(client, token, TIMEOUT);
        if (rc != MQTTCLIENT_SUCCESS)
            fprintf(stderr, "publish failed, rc=%d\n", rc);
        else
            printf("sent %s\n", payload);

        sleep(5);
    }

    MQTTClient_disconnect(client, TIMEOUT);
    MQTTClient_destroy(&client);
    return EXIT_SUCCESS;
}

Compilez et exécutez :

gcc -Wall -O2 -o publisher publisher.c -lpaho-mqtt3cs
./publisher

Quelques lignes méritent une explication. ssl.verify = 1 active la vérification du nom d'hôte, désactivée par défaut ; sans elle, n'importe quel certificat valide pour n'importe quel hôte serait accepté. Appeler MQTTClient_setCallbacks avant la connexion place le client en mode multithread : la bibliothèque gère le keep-alive et la réception sur son propre thread, et connection_lost vous signale la perte de la liaison. La boucle de publication vérifie MQTTClient_isConnected et se reconnecte avec sa propre logique de nouvelle tentative, car l'API synchrone ne se reconnecte pas automatiquement.

Un subscriber avec callbacks

Le subscriber réutilise la même logique de connexion et fait son travail dans message_arrived. Remplacez main par ceci (en conservant les fonctions utilitaires ci-dessus) :

static int on_message(void *ctx, char *topic, int len, MQTTClient_message *msg) {
    (void)ctx; (void)len;
    printf("%s: %.*s\n", topic, msg->payloadlen, (char *)msg->payload);
    MQTTClient_freeMessage(&msg);
    MQTTClient_free(topic);
    return 1;   /* 1 = message traité ; 0 demande à la bibliothèque de le redistribuer */
}

int main(void) {
    const char *prefix = getenv("MQTT_TOPIC_PREFIX");
    char filter[256];
    if (!prefix) prefix = "connectmq/demo/paho-c-4821";
    snprintf(filter, sizeof filter, "%s/+/temperature", prefix);

    signal(SIGINT, on_signal);
    signal(SIGTERM, on_signal);

    MQTTClient client;
    MQTTClient_create(&client, ADDRESS, "paho-c-sub-4821",
                      MQTTCLIENT_PERSISTENCE_NONE, NULL);
    MQTTClient_setCallbacks(client, NULL, connection_lost, on_message, NULL);

    while (running) {
        if (!MQTTClient_isConnected(client)) {
            if (connect_broker(client) == MQTTCLIENT_SUCCESS) {
                MQTTClient_subscribe(client, filter, QOS);
                printf("subscribed to %s\n", filter);
            } else {
                sleep(5);
                continue;
            }
        }
        sleep(1);
    }
    MQTTClient_disconnect(client, TIMEOUT);
    MQTTClient_destroy(&client);
    return 0;
}

Libérer à la fois le message et la chaîne du topic est obligatoire ; oublier l'un ou l'autre est la fuite mémoire la plus courante dans les programmes Paho C. Gardez le callback court. Si le traitement est lent (écriture en flash, appel d'une API HTTP), copiez le payload dans une file et retournez immédiatement, car le callback s'exécute sur le thread de réception de la bibliothèque.

Vous pouvez aussi observer le publisher sans écrire une seule ligne de code :

mosquitto_sub -h broker.connectmq.com -p 1883 -i watcher-4821 -t "connectmq/demo/paho-c-4821/#" -v

Compilation croisée pour une carte ARM

Pour les images Yocto ou Buildroot, Paho C dispose de recettes prêtes à l'emploi. Pour une compilation croisée manuelle rapide, indiquez à CMake votre toolchain et un OpenSSL compilé pour la cible :

cmake -B build-arm \
  -DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc \
  -DCMAKE_SYSTEM_NAME=Linux \
  -DPAHO_WITH_SSL=TRUE \
  -DOPENSSL_ROOT_DIR=/opt/sysroot/usr \
  -DPAHO_BUILD_SHARED=TRUE -DPAHO_BUILD_SAMPLES=FALSE \
  -DCMAKE_INSTALL_PREFIX=/opt/paho-arm -DCMAKE_INSTALL_LIBDIR=lib
cmake --build build-arm -j"$(nproc)"
cmake --install build-arm

aarch64-linux-gnu-gcc -O2 -o publisher publisher.c \
  -I/opt/paho-arm/include -L/opt/paho-arm/lib -lpaho-mqtt3cs -lssl -lcrypto -lpthread

# Copier le binaire et la bibliothèque partagée sur la carte
scp publisher /opt/paho-arm/lib/libpaho-mqtt3cs.so* root@gateway:/usr/local/lib/

Sur la carte, déplacez publisher dans /usr/local/bin et exécutez ldconfig. Paho peut aussi être compilé en bibliothèque statique (-DPAHO_BUILD_STATIC=TRUE) pour éviter de livrer un .so séparé, mais vous devenez alors responsable des mises à jour d'OpenSSL pour ce binaire. Vérifiez que l'image cible contient un bundle de CA ; les images minimales n'en ont souvent pas, et le TLS échoue avec une erreur de vérification de certificat alors que le code est correct. Si la même passerelle pilote aussi un écran tactile local, le tutoriel du client MQTT Qt montre comment construire la partie IHM en C++, et tester les clients MQTT explique comment intégrer ce code à votre CI.

QoS, persistance et mise en tampon hors ligne

Les passerelles perdent la connectivité : décidez donc dès le départ de ce qu'il advient des données pendant une coupure. Le client ci-dessus utilise MQTTCLIENT_PERSISTENCE_NONE, qui conserve les messages QoS 1 en cours d'envoi uniquement en mémoire ; si le processus redémarre en pleine livraison, ces messages sont perdus. Paho peut à la place persister les messages en cours sur disque : passez MQTTCLIENT_PERSISTENCE_DEFAULT et un chemin de répertoire comme deux derniers arguments de MQTTClient_create, connectez-vous avec cleansession = 0, et la bibliothèque renvoie après reconnexion tout ce qui n'a pas encore été acquitté. Sur une mémoire flash, mettez cela en balance avec l'usure en écriture, car persister chaque message sur une carte SD bon marché raccourcit sa durée de vie.

La persistance ne couvre que les messages déjà remis à la bibliothèque. Dans l'exemple, les relevés effectués pendant que la liaison est coupée n'atteignent jamais MQTTClient_publishMessage, car la boucle saute la publication jusqu'à la reconnexion. Si vous avez besoin de chaque relevé, écrivez-les dans un tampon circulaire local (un fichier ou une base SQLite) et videz-le après reconnexion, du plus ancien au plus récent, avec l'horodatage d'origine dans le payload pour que le back-end puisse placer chaque valeur correctement.

Le choix du QoS dépend des données. Une télémétrie périodique remplacée toutes les quelques secondes convient en QoS 0 ou 1 ; les commandes et les alarmes méritent le QoS 1. Le QoS 2 ajoute un aller-retour supplémentaire et est rarement nécessaire sur les passerelles. Le plan ConnectMQ Serverless gratuit prend en charge le QoS jusqu'à 1, et les plans payants ajoutent le QoS 2. Le guide des niveaux de QoS MQTT détaille les garanties de livraison.

Exécution en tant que service systemd

/etc/systemd/system/mqtt-publisher.service :

[Unit]
Description=Paho MQTT C publisher
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/local/bin/publisher
Environment=MQTT_CLIENT_ID=YOUR_CLIENT_ID
Environment=MQTT_USERNAME=YOUR_USERNAME
Environment=MQTT_PASSWORD=YOUR_PASSWORD
Environment=MQTT_TOPIC_PREFIX=acme/plant1/gateway1
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now mqtt-publisher
journalctl -u mqtt-publisher -f

Les identifiants ci-dessus proviennent de votre broker privé. Avec un broker MQTT managé comme ConnectMQ, vous créez le broker dans le portail et recevez un username, un mot de passe et un client ID générés ; les client IDs sont liés à ces identifiants, donnez donc à chaque passerelle son propre jeu. Au-delà de quelques appareils, placez les secrets dans un EnvironmentFile lisible uniquement par root.

Codes de retour et dépannage

Code de retour / symptôme Signification Que vérifier
-1 (MQTTCLIENT_FAILURE) à la connexion TLS Échec de la négociation ou du certificat Chemin du bundle de CA, horloge système (le TLS échoue si la date est fausse), nom d'hôte
4 Username ou mot de passe incorrect Identifiants, saut de ligne final dans le fichier d'environnement
5 Non autorisé Le client ID ne correspond pas aux identifiants émis
2 Identifiant rejeté Client ID vide ou en double
La connexion bloque sur 8883 Un pare-feu bloque le port Testez avec openssl s_client -connect broker.connectmq.com:8883
La mémoire augmente sur plusieurs jours Messages ou topics non libérés MQTTClient_freeMessage et MQTTClient_free dans chaque branche du callback

Le problème d'horloge mérite d'être souligné pour Linux embarqué : les cartes sans horloge temps réel démarrent en 1970, et chaque certificat paraît « pas encore valide » jusqu'à la synchronisation NTP. Ordonnez votre service après time-sync.target si vous êtes concerné. D'autres causes d'échec TLS sont traitées dans MQTT TLS handshake failed.

Démarrer avec ConnectMQ

Votre passerelle C est prête pour un vrai broker. Créez un broker Serverless gratuit sur app.connectmq.com, sans carte bancaire, placez les identifiants générés dans l'unité systemd et regardez les messages arriver dans l'explorateur de topics en direct du portail. Lorsque la flotte grandit, le même binaire se connecte aux brokers payants, Dedicated ou BYOC sans modification du code.

Questions fréquentes

Quelle bibliothèque Paho C lier pour le TLS ?

Liez paho-mqtt3cs pour l'API synchrone avec TLS, ou paho-mqtt3as pour l'API asynchrone avec TLS. Les variantes sans le s final ne prennent pas en charge SSL.

Le client Paho MQTT C se reconnecte-t-il automatiquement ?

L'API synchrone MQTTClient ne le fait pas : vérifiez MQTTClient_isConnected et reconnectez-vous vous-même. L'API asynchrone MQTTAsync propose une option automaticReconnect.

Paho C prend-il en charge MQTT 5 ?

Oui. Créez le client avec MQTTClient_createWithOptions et MQTTVERSION_5, puis utilisez la famille de fonctions MQTTClient_connect5 et MQTTClient_publish5 ; ConnectMQ accepte MQTT 3.1.1 comme 5.0.

De combien de mémoire Paho MQTT C a-t-il besoin ?

Il est suffisamment compact pour les passerelles Linux embarqué courantes, mais l'empreinte exacte dépend de vos options de compilation et d'OpenSSL. Mesurez-la sur votre cible avec la variante que vous liez réellement.

Articles liés

Les meilleurs outils CLI MQTT pour tester, scripter et déboguer

Un comparatif pratique de mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, la CLI MQTT.js, curl et mqttui, avec le même test exécuté sur le serveur MQTT public gratuit broker.connectmq.com pour choisir le bon outil pour le scripting et le débogage.

9 min de lecture

Tous les articles →

Votre broker MQTT privé, à quelques clics

Créez un compte, récupérez les identifiants de votre broker et connectez votre premier appareil — aucun serveur à installer, mettre à jour ou dimensionner.