Un client MQTT Qt est tout indiqué pour les pupitres opérateur et les tableaux de bord desktop : Qt vous offre une interface native rapide sous Windows, Linux et sur cartes embarquées, et MQTT y apporte les données machine en direct. Dans ce tutoriel, vous allez utiliser le module Qt MQTT pour créer une petite IHM en C++ qui se connecte en TLS, affiche des valeurs en direct, signale les données périmées et renvoie des commandes à une machine.
Ce que vous allez construire
L'IHM surveille une machine, une presse hydraulique sur la ligne 1, et affiche trois valeurs ainsi qu'un bouton de commande :
| Tag | Topic | Payload |
|---|---|---|
| Température d'huile | <prefix>/line1/press1/temperature |
48.2 |
| Pression | <prefix>/line1/press1/pressure |
182.5 |
| État | <prefix>/line1/press1/state |
RUNNING, IDLE, FAULT |
| Commande | <prefix>/line1/press1/cmd |
START ou STOP |
L'organisation des topics suit la hiérarchie site/ligne/machine courante dans les architectures Unified Namespace de type ISA-95, ce qui permet d'étendre facilement l'IHM à d'autres machines par la suite. Le guide MQTT Sparkplug B et Unified Namespace explique cette structure en profondeur.
Obtenir le module Qt MQTT
Qt MQTT est un module complémentaire de Qt 6. Installez-le avec le Qt Online Installer ou le Maintenance Tool : sélectionnez votre version de Qt 6, ouvrez « Additional Libraries » et cochez « Qt MQTT ». Si vous utilisez les paquets Qt d'une distribution ou une image Yocto, cherchez un paquet qtmqtt, ou compilez le module à partir du dépôt source qtmqtt avec votre installation Qt.
Vérifiez la licence avant de livrer : Qt MQTT est proposé sous GPLv3 ou sous licence commerciale Qt, et non sous la LGPL qui couvre une grande partie de Qt. Pour une IHM commerciale à code fermé, cela implique généralement une licence commerciale. La documentation officielle de Qt MQTT liste les classes utilisées ci-dessous et les conditions de licence.
Le module prend en charge MQTT 3.1, 3.1.1 et 5.0, TLS via le backend SSL de Qt, ainsi que des transports personnalisés comme WebSocket.
Configuration du projet avec CMake
CMakeLists.txt :
cmake_minimum_required(VERSION 3.21)
project(PressHmi LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
find_package(Qt6 REQUIRED COMPONENTS Widgets Network Mqtt)
qt_add_executable(press_hmi main.cpp)
target_link_libraries(press_hmi PRIVATE Qt6::Widgets Qt6::Network Qt6::Mqtt)
Écrire l'IHM du client MQTT Qt
Toute l'application tient dans un seul fichier. Un widget PressPanel possède un QMqttClient, construit une grille simple de labels, s'abonne une fois connecté, se reconnecte avec un backoff lorsque la liaison tombe et grise les valeurs qui n'ont pas été mises à jour récemment.
main.cpp :
#include <QApplication>
#include <QElapsedTimer>
#include <QGridLayout>
#include <QLabel>
#include <QMap>
#include <QPushButton>
#include <QRandomGenerator>
#include <QSslConfiguration>
#include <QTimer>
#include <QWidget>
#include <QtMqtt/QMqttClient>
#include <QtMqtt/QMqttSubscription>
class PressPanel : public QWidget {
public:
explicit PressPanel(QWidget *parent = nullptr) : QWidget(parent) {
m_prefix = qEnvironmentVariable("MQTT_TOPIC_PREFIX",
"connectmq/demo/qt-hmi-3f81") + "/line1/press1";
buildUi();
setupClient();
m_staleTimer.setInterval(1000);
connect(&m_staleTimer, &QTimer::timeout, this, &PressPanel::checkStale);
m_staleTimer.start();
m_reconnectTimer.setSingleShot(true);
connect(&m_reconnectTimer, &QTimer::timeout, this, &PressPanel::connectToBroker);
connectToBroker();
}
private:
struct Tag { QLabel *value; QElapsedTimer age; QString unit; };
void buildUi() {
auto *grid = new QGridLayout(this);
QFont big = font();
big.setPointSize(28);
big.setBold(true);
const QList<QPair<QString, QString>> tags = {
{"temperature", " °C"}, {"pressure", " bar"}, {"state", ""}};
int row = 0;
for (const auto &[name, unit] : tags) {
auto *caption = new QLabel(name.toUpper(), this);
auto *value = new QLabel("--", this);
value->setFont(big);
grid->addWidget(caption, row, 0);
grid->addWidget(value, row, 1);
m_tags.insert(name, Tag{value, QElapsedTimer(), unit});
++row;
}
m_startBtn = new QPushButton("START", this);
m_stopBtn = new QPushButton("STOP", this);
grid->addWidget(m_startBtn, row, 0);
grid->addWidget(m_stopBtn, row, 1);
connect(m_startBtn, &QPushButton::clicked, this, [this] { sendCommand("START"); });
connect(m_stopBtn, &QPushButton::clicked, this, [this] { sendCommand("STOP"); });
m_status = new QLabel(this);
grid->addWidget(m_status, row + 1, 0, 1, 2);
setCommandsEnabled(false);
}
void setupClient() {
const QString clientId = qEnvironmentVariable(
"MQTT_CLIENT_ID",
QString("qt-hmi-%1").arg(QRandomGenerator::global()->generate(), 8, 16, QChar('0')));
m_client.setHostname("broker.connectmq.com");
m_client.setPort(8883);
m_client.setClientId(clientId);
m_client.setUsername(qEnvironmentVariable("MQTT_USERNAME"));
m_client.setPassword(qEnvironmentVariable("MQTT_PASSWORD"));
m_client.setProtocolVersion(QMqttClient::MQTT_5_0);
m_client.setKeepAlive(30);
m_client.setCleanSession(true);
connect(&m_client, &QMqttClient::connected, this, [this] {
m_backoffMs = 1000;
m_status->setText("Connected to " + m_client.hostname());
setCommandsEnabled(true);
auto *sub = m_client.subscribe(QMqttTopicFilter(m_prefix + "/+"), 1);
if (!sub) {
m_status->setText("Subscribe failed");
return;
}
// UniqueConnection : Qt peut renvoyer le même objet d'abonnement
// après une reconnexion, et chaque message ne doit pas être traité deux fois.
connect(sub, &QMqttSubscription::messageReceived, this,
&PressPanel::onMessage, Qt::UniqueConnection);
});
connect(&m_client, &QMqttClient::disconnected, this, [this] {
setCommandsEnabled(false);
m_status->setText(QString("Disconnected (error %1), retrying in %2 s")
.arg(int(m_client.error()))
.arg(m_backoffMs / 1000));
m_reconnectTimer.start(m_backoffMs);
m_backoffMs = qMin(m_backoffMs * 2, 30000);
});
}
void connectToBroker() {
m_status->setText("Connecting...");
m_client.connectToHostEncrypted(QSslConfiguration::defaultConfiguration());
}
void onMessage(const QMqttMessage &msg) {
const QString name = msg.topic().levels().last();
auto it = m_tags.find(name);
if (it == m_tags.end())
return;
const QString text = QString::fromUtf8(msg.payload()).trimmed();
if (name != "state") {
bool ok = false;
const double v = text.toDouble(&ok);
if (!ok) return; // ignorer les payloads malformés
it->value->setText(QString::number(v, 'f', 1) + it->unit);
} else {
it->value->setText(text.left(16));
it->value->setStyleSheet(text == "FAULT" ? "color: #c62828;" : "");
}
it->age.restart();
if (name != "state") it->value->setStyleSheet("");
}
void checkStale() {
for (auto &tag : m_tags) {
if (tag.age.isValid() && tag.age.elapsed() > 10000)
tag.value->setStyleSheet("color: #9e9e9e;"); // gris = périmé
}
}
void sendCommand(const QString &cmd) {
const qint32 id = m_client.publish(QMqttTopicName(m_prefix + "/cmd"), cmd.toUtf8(), 1, false);
if (id == -1)
m_status->setText("Publish failed");
}
void setCommandsEnabled(bool on) {
m_startBtn->setEnabled(on);
m_stopBtn->setEnabled(on);
}
QMqttClient m_client;
QMap<QString, Tag> m_tags;
QString m_prefix;
QLabel *m_status = nullptr;
QPushButton *m_startBtn = nullptr;
QPushButton *m_stopBtn = nullptr;
QTimer m_staleTimer;
QTimer m_reconnectTimer;
int m_backoffMs = 1000;
};
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
PressPanel panel;
panel.setWindowTitle("Press 1 HMI");
panel.resize(420, 300);
panel.show();
return app.exec();
}
Compilez et exécutez :
cmake -B build -DCMAKE_PREFIX_PATH=$HOME/Qt/6.8.0/gcc_64
cmake --build build
./build/press_hmi
Les choix de conception qui comptent dans une IHM
S'abonner dans le gestionnaire connected. Avec une clean session, le broker supprime les abonnements à la déconnexion ; s'abonner à cet endroit les rétablit donc après chaque reconnexion. Qt MQTT ne se reconnecte pas tout seul, c'est pourquoi le pupitre utilise un timer à déclenchement unique avec un backoff exponentiel plafonné à 30 secondes.
Afficher l'ancienneté, pas seulement les valeurs. Un opérateur qui lit « 48.2 °C » ne peut pas savoir si ce chiffre date de maintenant ou d'il y a une heure, au moment où le réseau est tombé. Griser une valeur après 10 secondes sans mise à jour ne coûte rien et évite les mauvaises décisions. Associez cela à des appareils qui publient un message last will pour que l'IHM puisse aussi indiquer quand la machine elle-même est passée hors ligne.
Utiliser les retained messages pour l'état. Si la machine publie son état avec le flag retain, une IHM fraîchement démarrée affiche immédiatement l'état actuel au lieu de « -- » jusqu'au prochain changement.
Tout valider. Les payloads qui ne sont pas des nombres sont ignorés, et les valeurs texte sont tronquées. Ne laissez jamais un message malformé faire planter un pupitre en atelier.
Garder le thread d'interface libre. Dans cet exemple, le client vit dans le thread graphique, ce qui convient pour quelques dizaines de messages par seconde. Si un pupitre s'abonne à des centaines de tags qui évoluent rapidement, stockez la dernière valeur de chaque tag dans le gestionnaire de messages et rafraîchissez les labels via un timer toutes les 200 ms environ plutôt qu'à chaque message. Les opérateurs ne peuvent de toute façon pas lire plus vite, et la boucle d'événements reste réactive aux appuis tactiles. Pour des flux très lourds, déplacez le client dans un QThread de travail et transmettez les valeurs à l'interface via des signaux en file.
Les commandes en QoS 1. Utilisez le QoS 1 pour les commandes opérateur afin qu'une brève coupure réseau ne fasse pas perdre un STOP. Côté machine, les commandes doivent tout de même être traitées comme des demandes, et l'état résultant doit être publié, pour que l'IHM affiche ce qui s'est réellement passé et non ce qui a été demandé.
Tester sans machine
Lancez l'IHM et simulez la presse depuis un terminal avec le serveur MQTT public gratuit. Ce serveur de test est lisible par tout le monde : utilisez uniquement le préfixe aléatoire et des valeurs synthétiques :
P="connectmq/demo/qt-hmi-3f81/line1/press1"
mosquitto_pub -h broker.connectmq.com -p 1883 -i qt-sim-state -r -t "$P/state" -m "RUNNING"
while true; do
mosquitto_pub -h broker.connectmq.com -p 1883 -i qt-sim-temp -t "$P/temperature" -m "4$((RANDOM % 10)).$((RANDOM % 10))"
mosquitto_pub -h broker.connectmq.com -p 1883 -i qt-sim-pres -t "$P/pressure" -m "18$((RANDOM % 10)).0"
sleep 1
done
Dans un troisième terminal, observez les commandes envoyées par l'IHM :
mosquitto_sub -h broker.connectmq.com -p 1883 -i qt-sim-cmd -t "$P/cmd" -v
Arrêtez la boucle : les valeurs de température et de pression deviennent grises au bout de 10 secondes. Si vous voulez une simulation plus riche avec plusieurs machines, la démo de jumeau numérique 3D de ConnectMQ publie une usine virtuelle entière via WebSocket sécurisé et constitue un bon test de charge pour une IHM multi-machines.
Cibles embarquées et WebSocket
Sous Linux embarqué, les IHM Qt tournent souvent en plein écran avec le plugin de plateforme eglfs ou linuxfb. Le code MQTT ne change pas, mais vérifiez que l'image contient un bundle de certificats de CA et que l'horloge est réglée avant la connexion ; une carte qui démarre en 1970 rejette chaque certificat comme pas encore valide.
Si le réseau de l'usine n'autorise que le HTTPS sortant, Qt MQTT peut fonctionner via WebSocket en passant un transport QIODevice personnalisé à setTransport() ; les exemples Qt MQTT incluent un transport WebSocket que vous pouvez réutiliser avec wss://broker.connectmq.com:8084/mqtt. Pour les compromis entre transports, lisez MQTT over WebSocket.
Du prototype à l'usine
En production, faites tourner l'IHM sur un broker privé, hébergé sur un broker MQTT cloud ; un broker MQTT managé comme ConnectMQ vous évite d'exploiter ce serveur :
- Créez un broker dans le portail ConnectMQ et copiez le username, le mot de passe et le client ID générés.
- Définissez
MQTT_USERNAME,MQTT_PASSWORD,MQTT_CLIENT_IDetMQTT_TOPIC_PREFIXdans l'environnement du pupitre (par exemple dans son unité systemd). - Conservez TLS sur 8883 et donnez à chaque pupitre ses propres identifiants, car ConnectMQ lie les client IDs aux identifiants émis.
- Utilisez la vue des appareils du portail, qui regroupe les topics par niveaux ISA-95, ainsi que des règles d'alerte à seuil pour surveiller les mêmes tags de manière centralisée.
Si votre passerelle est écrite en C, le tutoriel Paho MQTT C se marie bien avec cette IHM, le guide pour tester les clients MQTT montre comment placer l'analyse des tags et la logique de reconnexion sous CI, et le guide des bibliothèques clientes MQTT compare Qt MQTT aux autres options.
Dépannage
| Symptôme | Cause | Solution |
|---|---|---|
find_package ne trouve pas Qt6Mqtt |
Module non installé | Ajoutez « Qt MQTT » dans le Maintenance Tool ou compilez qtmqtt |
| Déconnexion immédiate avec une erreur de transport | Backend TLS manquant ou absence de bundle de CA | Vérifiez le plugin OpenSSL de Qt et les certificats système |
| Connecté mais aucune valeur | Préfixe de topic différent | Comparez MQTT_TOPIC_PREFIX avec celui du simulateur |
| Deux pupitres s'éjectent mutuellement | Même client ID | Un client ID unique par pupitre |
| L'interface se fige sous un trafic intense | Trop de mises à jour de labels | Limitez les mises à jour avec un timer et n'affichez que la dernière valeur |
Démarrer avec ConnectMQ
Placez votre IHM Qt sur un broker privé et sécurisé en quelques minutes. Créez un broker Serverless gratuit sur app.connectmq.com, sans carte bancaire, définissez les identifiants générés dans l'environnement du pupitre et suivez les mêmes tags dans l'explorateur de topics en direct du portail pendant que les opérateurs utilisent l'IHM.
Questions fréquentes
Le module Qt MQTT est-il gratuit ?
Il est disponible sous GPLv3 ou sous licence commerciale Qt. Les produits commerciaux à code fermé nécessitent généralement la licence commerciale : vérifiez les conditions avant de livrer.
QMqttClient se reconnecte-t-il automatiquement ?
Non. Écoutez le signal disconnected et rappelez connectToHostEncrypted() depuis un timer, puis réabonnez-vous dans le gestionnaire connected.
Qt MQTT prend-il en charge MQTT 5 ?
Oui. Appelez setProtocolVersion(QMqttClient::MQTT_5_0) avant la connexion ; ConnectMQ prend en charge MQTT 5.0 comme 3.1.1.
Puis-je utiliser Qt MQTT depuis QML ?
Oui. Exposez à QML un objet C++ qui encapsule QMqttClient avec des propriétés et des méthodes invocables, ou adaptez les exemples QML fournis avec le module Qt MQTT.



