IoT Tutorials

Qt MQTT Client in C++: Building Desktop and Embedded HMIs

Use the Qt MQTT module to build a C++ HMI that connects to a cloud broker over TLS, subscribes to machine topics, shows live values with staleness detection and sends operator commands.

Qt MQTT client HMI on an industrial touchscreen showing live machine temperature, pressure and state from a cloud broker
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

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:

  1. Create a broker in the ConnectMQ portal and copy the generated username, password and client ID.
  2. Set MQTT_USERNAME, MQTT_PASSWORD, MQTT_CLIENT_ID and MQTT_TOPIC_PREFIX in the panel's environment (for example in its systemd unit).
  3. Keep TLS on 8883 and give every panel its own credentials, because ConnectMQ binds client IDs to issued credentials.
  4. 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.

Related articles

Best MQTT CLI Tools for Testing, Scripting and Debugging

A hands-on comparison of mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, the MQTT.js CLI, curl and mqttui, with the same test run against broker.connectmq.com so you can choose the right tool for scripting and debugging.

8 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.