A Qt MQTT client is a natural fit for operator panels and desktop dashboards: Qt gives you a fast native UI on Windows, Linux and embedded boards, and MQTT brings live machine data into it. In this tutorial you will use the Qt MQTT module to build a small C++ HMI that connects over TLS, displays live values, flags stale data and sends commands back to a machine.
What You Will Build
The HMI watches one machine, a hydraulic press on line 1, and shows three values plus a command button:
| Tag | Topic | Payload |
|---|---|---|
| Oil temperature | <prefix>/line1/press1/temperature |
48.2 |
| Pressure | <prefix>/line1/press1/pressure |
182.5 |
| State | <prefix>/line1/press1/state |
RUNNING, IDLE, FAULT |
| Command | <prefix>/line1/press1/cmd |
START or STOP |
The topic layout follows the site/line/machine hierarchy common in ISA-95 style Unified Namespace designs, which keeps the HMI easy to extend to more machines later. The MQTT Sparkplug B and Unified Namespace guide explains that structure in depth.
Getting the Qt MQTT Module
Qt MQTT is an add-on module in Qt 6. Install it with the Qt Online Installer or Maintenance Tool: select your Qt 6 version, open "Additional Libraries" and tick "Qt MQTT". If you use a distribution's Qt packages or a Yocto image, look for a qtmqtt package, or build the module from the qtmqtt source repository against your Qt installation.
Check the licence before you ship: Qt MQTT is offered under the GPLv3 or a commercial Qt licence, not under the LGPL that covers much of Qt. For a closed-source commercial HMI that usually means a commercial licence. The official Qt MQTT documentation lists the classes used below and the licensing terms.
The module supports MQTT 3.1, 3.1.1 and 5.0, TLS through Qt's SSL backend, and custom transports such as WebSocket.
Project Setup With 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)
Writing the Qt MQTT Client HMI
The whole application fits in one file. A PressPanel widget owns a QMqttClient, builds a simple grid of labels, subscribes when connected, reconnects with backoff when the link drops, and greys out values that have not updated recently.
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 may hand back the same subscription object
// after a reconnect, and we must not handle each message twice.
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; // ignore malformed payloads
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;"); // grey = stale
}
}
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();
}
Build and run:
cmake -B build -DCMAKE_PREFIX_PATH=$HOME/Qt/6.8.0/gcc_64
cmake --build build
./build/press_hmi
Design Decisions That Matter in an HMI
Subscribe in the connected handler. With a clean session the broker drops subscriptions on disconnect, so subscribing there restores them after every reconnect. Qt MQTT does not reconnect on its own, which is why the panel uses a single-shot timer with exponential backoff capped at 30 seconds.
Show staleness, not just values. An operator looking at "48.2 °C" cannot tell whether that number is from now or from an hour ago when the network dropped. Greying a value after 10 seconds without updates is cheap and prevents bad decisions. Pair it with devices that publish a last will message so the HMI can also show when the machine itself went offline.
Use retained messages for state. If the machine publishes its state with the retain flag, a freshly started HMI shows the current state immediately instead of "--" until the next change.
Validate everything. Payloads that are not numbers are ignored, and string values are truncated. Never let a malformed message crash a panel on the factory floor.
Keep the UI thread free. In this example the client lives in the GUI thread, which is fine for tens of messages per second. If a panel subscribes to hundreds of fast-changing tags, store the latest value per tag in the message handler and refresh the labels from a timer every 200 ms or so instead of on every message. Operators cannot read faster than that anyway, and the event loop stays responsive to touches. For very heavy feeds, move the client into a worker QThread and pass values to the UI through queued signals.
Commands at QoS 1. Use QoS 1 for operator commands so a brief network blip does not lose a STOP. The machine side should still treat commands as requests and report the resulting state, so the HMI shows what actually happened, not what was asked for.
Testing Without a Machine
Run the HMI and simulate the press from a terminal against the free public MQTT broker. The public area is readable by everyone, so use the random prefix and synthetic values only:
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
In a third terminal, watch the commands the HMI sends:
mosquitto_sub -h broker.connectmq.com -p 1883 -i qt-sim-cmd -t "$P/cmd" -v
Stop the loop and the temperature and pressure values turn grey after 10 seconds. If you want a richer simulation with several machines, the ConnectMQ 3D digital twin demo publishes a whole virtual factory over secure WebSocket and is a good stress test for a multi-machine HMI.
Embedded Targets and WebSocket
On embedded Linux, Qt HMIs often run full-screen with the eglfs or linuxfb platform plugin. The MQTT code does not change, but check that the image contains a CA certificate bundle and that the clock is set before connecting; a board that boots in 1970 rejects every certificate as not yet valid.
If the plant network only allows HTTPS outbound, Qt MQTT can run over WebSocket by passing a custom QIODevice transport to setTransport(); the Qt MQTT examples include a WebSocket transport you can reuse with wss://broker.connectmq.com:8084/mqtt. For the transport trade-offs, read MQTT over WebSocket.
From Prototype to Plant
For production, run the HMI against a private broker on a cloud MQTT broker such as ConnectMQ:
- Create a broker in the ConnectMQ portal and copy the generated username, password and client ID.
- Set
MQTT_USERNAME,MQTT_PASSWORD,MQTT_CLIENT_IDandMQTT_TOPIC_PREFIXin the panel's environment (for example in its systemd unit). - Keep TLS on 8883 and give every panel its own credentials, because ConnectMQ binds client IDs to issued credentials.
- Use the portal's devices view, which groups topics by ISA-95 levels, and threshold alert rules to watch the same tags centrally.
If your gateway is written in C, the Paho MQTT C tutorial pairs well with this HMI, the guide to testing MQTT clients shows how to put tag parsing and reconnect logic under CI, and the MQTT client libraries guide compares Qt MQTT with other options.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
find_package cannot find Qt6Mqtt |
Module not installed | Add "Qt MQTT" in the Maintenance Tool or build qtmqtt |
| Disconnects immediately with a transport error | TLS backend missing or no CA bundle | Check Qt's OpenSSL plugin and system certificates |
| Connects but no values | Topic prefix mismatch | Compare MQTT_TOPIC_PREFIX with the simulator |
| Two panels keep kicking each other off | Same client ID | Unique client ID per panel |
| UI freezes under heavy traffic | Too many label updates | Throttle updates with a timer and render the latest value only |
Get Started with ConnectMQ
Put your Qt HMI on a private, secure broker in minutes. Create a free Serverless broker at app.connectmq.com, no credit card required, set the generated credentials in the panel's environment, and watch the same tags in the portal's live topic explorer while operators use the HMI.
Frequently Asked Questions
Is the Qt MQTT module free to use?
It is available under the GPLv3 or a commercial Qt licence. Closed-source commercial products typically need the commercial licence, so check the terms before shipping.
Does QMqttClient reconnect automatically?
No. Listen for the disconnected signal and call connectToHostEncrypted() again from a timer, then resubscribe in the connected handler.
Does Qt MQTT support MQTT 5?
Yes. Call setProtocolVersion(QMqttClient::MQTT_5_0) before connecting; ConnectMQ supports both MQTT 5.0 and 3.1.1.
Can I use Qt MQTT from QML?
Yes. Expose a C++ object that wraps QMqttClient with properties and invokable methods to QML, or adapt the QML examples that ship with the Qt MQTT module.



