IoT-Tutorials

Qt MQTT Client in C++: HMIs für Desktop und Embedded bauen

Nutzen Sie das Modul Qt MQTT, um eine C++-HMI zu bauen, die sich über TLS mit einem Cloud-Broker verbindet, Maschinen-Topics abonniert, Live-Werte mit Erkennung veralteter Daten anzeigt und Bedienerbefehle sendet.

Qt MQTT Client als HMI auf einem industriellen Touchscreen mit Live-Temperatur, Druck und Zustand einer Maschine von einem Cloud-Broker
Probieren Sie es auf Ihrem eigenen MQTT-Broker ausHolen Sie sich im Free-Tarif einen privaten, verwalteten MQTT-Broker und starten Sie mit fertigen Codebeispielen.
Kostenlos testen Codebeispiele Keine Kreditkarte erforderlich

Ein Qt MQTT Client passt hervorragend zu Bedienpanels und Desktop-Dashboards: Qt liefert eine schnelle native Oberfläche unter Windows, Linux und auf Embedded-Boards, und MQTT bringt Live-Maschinendaten hinein. In diesem Tutorial nutzen Sie das Modul Qt MQTT, um eine kleine C++-HMI zu bauen, die sich über TLS verbindet, Live-Werte anzeigt, veraltete Daten markiert und Befehle an eine Maschine zurücksendet.

Was Sie bauen

Die HMI überwacht eine Maschine, eine Hydraulikpresse an Linie 1, und zeigt drei Werte sowie einen Befehlsbutton:

Tag Topic Payload
Öltemperatur <prefix>/line1/press1/temperature 48.2
Druck <prefix>/line1/press1/pressure 182.5
Zustand <prefix>/line1/press1/state RUNNING, IDLE, FAULT
Befehl <prefix>/line1/press1/cmd START oder STOP

Die Topic-Struktur folgt der Hierarchie Standort/Linie/Maschine, wie sie in einem Unified Namespace (UNS) nach ISA-95 üblich ist, sodass sich die HMI später leicht um weitere Maschinen erweitern lässt. Im vollständigen UNS bildet die ISA-95-Anlagenhierarchie (Enterprise → Site → Area → Line → Cell) die Topic-Struktur, etwa acme/werk-stuttgart/presswerk/linie-1/presse-1/temperature; das Präfix der HMI deckt dann die oberen Ebenen ab. Der Leitfaden zu MQTT Sparkplug B und Unified Namespace erklärt diese Struktur im Detail.

Das Modul Qt MQTT beschaffen

Qt MQTT ist in Qt 6 ein Zusatzmodul. Installieren Sie es mit dem Qt Online Installer oder dem Maintenance Tool: Wählen Sie Ihre Qt-6-Version, öffnen Sie "Additional Libraries" und aktivieren Sie "Qt MQTT". Nutzen Sie die Qt-Pakete einer Distribution oder ein Yocto-Image, suchen Sie nach einem Paket qtmqtt oder bauen Sie das Modul aus dem Quell-Repository qtmqtt gegen Ihre Qt-Installation.

Prüfen Sie vor der Auslieferung die Lizenz: Qt MQTT wird unter der GPLv3 oder einer kommerziellen Qt-Lizenz angeboten, nicht unter der LGPL, die einen Großteil von Qt abdeckt. Für eine kommerzielle Closed-Source-HMI bedeutet das in der Regel eine kommerzielle Lizenz. Die offizielle Qt-MQTT-Dokumentation listet die unten verwendeten Klassen und die Lizenzbedingungen auf.

Das Modul unterstützt MQTT 3.1, 3.1.1 und 5.0, TLS über das SSL-Backend von Qt sowie eigene Transporte wie WebSocket.

Projekt-Setup mit 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)

Die Qt MQTT Client HMI schreiben

Die gesamte Anwendung passt in eine Datei. Ein Widget PressPanel besitzt einen QMqttClient, baut ein einfaches Raster aus Labels auf, abonniert nach dem Verbinden, verbindet sich bei Verbindungsabbruch mit Backoff neu und graut Werte aus, die sich in letzter Zeit nicht aktualisiert haben.

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 kann nach einem Reconnect dasselbe Subscription-Objekt
            // zurückgeben, und wir dürfen keine Nachricht doppelt verarbeiten.
            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;                         // fehlerhafte Payloads ignorieren
            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;");   // grau = veraltet
        }
    }

    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();
}

Bauen und ausführen:

cmake -B build -DCMAKE_PREFIX_PATH=$HOME/Qt/6.8.0/gcc_64
cmake --build build
./build/press_hmi

Designentscheidungen, die in einer HMI zählen

Im Handler connected abonnieren. Mit einer Clean Session verwirft der Broker Abonnements beim Trennen, daher stellt ein Abonnement an dieser Stelle sie nach jedem Reconnect wieder her. Qt MQTT verbindet sich nicht von selbst neu, weshalb das Panel einen Single-Shot-Timer mit exponentiellem Backoff nutzt, der bei 30 Sekunden gedeckelt ist.

Veraltete Werte anzeigen, nicht nur Werte. Ein Bediener, der "48.2 °C" sieht, kann nicht erkennen, ob die Zahl von jetzt stammt oder von vor einer Stunde, als das Netzwerk ausfiel. Einen Wert nach 10 Sekunden ohne Aktualisierung auszugrauen, kostet wenig und verhindert Fehlentscheidungen. Kombinieren Sie das mit Geräten, die eine Last-Will-Nachricht publizieren, damit die HMI auch anzeigen kann, wann die Maschine selbst offline gegangen ist.

Retained Messages für Zustände nutzen. Publiziert die Maschine ihren Zustand mit Retain-Flag, zeigt eine frisch gestartete HMI den aktuellen Zustand sofort an, statt bis zur nächsten Änderung "--".

Alles validieren. Payloads, die keine Zahlen sind, werden ignoriert, und String-Werte werden gekürzt. Lassen Sie nie zu, dass eine fehlerhafte Nachricht ein Panel in der Fertigungshalle zum Absturz bringt.

Den UI-Thread frei halten. In diesem Beispiel lebt der Client im GUI-Thread, was für einige Dutzend Nachrichten pro Sekunde ausreicht. Abonniert ein Panel Hunderte sich schnell ändernder Tags, speichern Sie im Message-Handler den jeweils letzten Wert pro Tag und aktualisieren Sie die Labels etwa alle 200 ms über einen Timer statt bei jeder Nachricht. Schneller können Bediener ohnehin nicht lesen, und die Event-Loop reagiert weiter auf Berührungen. Bei sehr hohem Datenaufkommen verschieben Sie den Client in einen Worker-QThread und übergeben Werte über Queued Signals an die UI.

Befehle mit QoS 1. Verwenden Sie QoS 1 für Bedienerbefehle, damit ein kurzer Netzwerkaussetzer kein STOP verschluckt. Die Maschinenseite sollte Befehle trotzdem als Anfragen behandeln und den resultierenden Zustand melden, damit die HMI zeigt, was tatsächlich passiert ist, und nicht, was angefordert wurde.

Ohne Maschine testen

Starten Sie die HMI und simulieren Sie die Presse aus einem Terminal gegen den kostenlosen öffentlichen MQTT-Broker, der für Tests und Prototypen gedacht ist. Der öffentliche Bereich ist für alle lesbar, verwenden Sie also nur das zufällige Präfix und synthetische Werte:

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

Beobachten Sie in einem dritten Terminal die Befehle, die die HMI sendet:

mosquitto_sub -h broker.connectmq.com -p 1883 -i qt-sim-cmd -t "$P/cmd" -v

Stoppen Sie die Schleife, und Temperatur und Druck werden nach 10 Sekunden grau. Wünschen Sie eine umfangreichere Simulation mit mehreren Maschinen, publiziert die 3D-Digital-Twin-Demo von ConnectMQ eine ganze virtuelle Fabrik über sicheren WebSocket und ist ein guter Belastungstest für eine HMI mit mehreren Maschinen.

Embedded-Zielsysteme und WebSocket

Auf Embedded Linux laufen Qt-HMIs oft im Vollbild mit dem Plattform-Plugin eglfs oder linuxfb. Der MQTT-Code ändert sich nicht, prüfen Sie aber, ob das Image ein CA-Zertifikats-Bundle enthält und die Uhr vor dem Verbinden gestellt ist; ein Board, das im Jahr 1970 bootet, lehnt jedes Zertifikat als noch nicht gültig ab.

Erlaubt das Werksnetz ausgehend nur HTTPS, kann Qt MQTT über WebSocket laufen, indem Sie einen eigenen QIODevice-Transport an setTransport() übergeben; die Qt-MQTT-Beispiele enthalten einen WebSocket-Transport, den Sie mit wss://broker.connectmq.com:8084/mqtt wiederverwenden können. Die Abwägungen beim Transport erläutert MQTT über WebSocket.

Vom Prototyp ins Werk

Für die Produktion betreiben Sie die HMI gegen einen privaten Broker auf einem Cloud-MQTT-Broker wie ConnectMQ:

  1. Erstellen Sie im ConnectMQ-Portal einen Broker und kopieren Sie den generierten Benutzernamen, das Passwort und die Client ID.
  2. Setzen Sie MQTT_USERNAME, MQTT_PASSWORD, MQTT_CLIENT_ID und MQTT_TOPIC_PREFIX in der Umgebung des Panels (zum Beispiel in seiner systemd-Unit). Für das Präfix bietet sich der ISA-95-Pfad im UNS an, etwa acme/werk-stuttgart/presswerk.
  3. Bleiben Sie bei TLS auf 8883 und geben Sie jedem Panel eigene Zugangsdaten, denn ConnectMQ bindet Client IDs an die ausgegebenen Zugangsdaten.
  4. Nutzen Sie die Geräteansicht des Portals, die Topics nach ISA-95-Ebenen gruppiert, sowie Schwellwert-Alarmregeln, um dieselben Tags zentral zu überwachen.

Ist Ihr Gateway in C geschrieben, passt das Paho MQTT C Tutorial gut zu dieser HMI, der Leitfaden MQTT-Clients testen zeigt, wie Sie Tag-Parsing und Reconnect-Logik in die CI bringen, und der Leitfaden zu MQTT-Client-Bibliotheken vergleicht Qt MQTT mit anderen Optionen.

Fehlerbehebung

Symptom Ursache Lösung
find_package findet Qt6Mqtt nicht Modul nicht installiert "Qt MQTT" im Maintenance Tool hinzufügen oder qtmqtt bauen
Trennt sofort mit einem Transportfehler TLS-Backend fehlt oder kein CA-Bundle OpenSSL-Plugin von Qt und Systemzertifikate prüfen
Verbindet, aber keine Werte Topic-Präfix stimmt nicht überein MQTT_TOPIC_PREFIX mit dem Simulator vergleichen
Zwei Panels werfen sich gegenseitig hinaus Gleiche Client ID Eindeutige Client ID pro Panel
UI friert bei hohem Verkehr ein Zu viele Label-Aktualisierungen Aktualisierungen per Timer drosseln und nur den letzten Wert darstellen

Starten Sie mit ConnectMQ

Bringen Sie Ihre Qt-HMI in wenigen Minuten auf einen privaten, sicheren Broker. Erstellen Sie unter app.connectmq.com einen kostenlosen Serverless Broker, ganz ohne Kreditkarte, setzen Sie die generierten Zugangsdaten in der Umgebung des Panels und beobachten Sie dieselben Tags im Live-Topic-Explorer des Portals, während Bediener die HMI nutzen.

Häufig gestellte Fragen

Ist das Modul Qt MQTT kostenlos nutzbar?

Es ist unter der GPLv3 oder einer kommerziellen Qt-Lizenz verfügbar. Kommerzielle Closed-Source-Produkte benötigen in der Regel die kommerzielle Lizenz, prüfen Sie die Bedingungen also vor der Auslieferung.

Verbindet sich QMqttClient automatisch neu?

Nein. Hören Sie auf das Signal disconnected, rufen Sie connectToHostEncrypted() per Timer erneut auf und abonnieren Sie im Handler connected neu.

Unterstützt Qt MQTT MQTT 5?

Ja. Rufen Sie vor dem Verbinden setProtocolVersion(QMqttClient::MQTT_5_0) auf; ConnectMQ unterstützt sowohl MQTT 5.0 als auch 3.1.1.

Kann ich Qt MQTT aus QML verwenden?

Ja. Stellen Sie QML ein C++-Objekt bereit, das QMqttClient mit Properties und aufrufbaren Methoden kapselt, oder passen Sie die QML-Beispiele an, die mit dem Modul Qt MQTT ausgeliefert werden.

Verwandte Artikel

Die besten MQTT-CLI-Tools zum Testen, Skripten und Debuggen

Ein praxisnaher Vergleich von mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, der MQTT.js-CLI, curl und mqttui – mit demselben Test gegen broker.connectmq.com, damit Sie das richtige Tool zum Skripten und Debuggen wählen.

8 Min. Lesezeit

Alle Artikel →

Ihr privater MQTT-Broker ist nur wenige Klicks entfernt

Konto anlegen, Broker-Zugangsdaten erhalten und das erste Gerät verbinden — ohne Server, die installiert, gepatcht oder skaliert werden müssen.