Bir Qt MQTT client, operatör panelleri ve masaüstü panoları için doğal bir seçimdir: Qt size Windows, Linux ve gömülü kartlarda hızlı, yerel bir arayüz sunar; MQTT ise canlı makine verisini bu arayüze taşır. Bu eğitimde Qt MQTT modülünü kullanarak TLS üzerinden bağlanan, canlı değerleri gösteren, eskimiş verileri işaretleyen ve bir makineye komut gönderen küçük bir C++ HMI geliştireceksiniz.
Ne Geliştireceksiniz?
HMI tek bir makineyi, 1. hattaki bir hidrolik presi izler ve üç değerle bir komut düğmesi gösterir:
| Etiket | Topic | Payload |
|---|---|---|
| Yağ sıcaklığı | <prefix>/line1/press1/temperature |
48.2 |
| Basınç | <prefix>/line1/press1/pressure |
182.5 |
| Durum | <prefix>/line1/press1/state |
RUNNING, IDLE, FAULT |
| Komut | <prefix>/line1/press1/cmd |
START veya STOP |
Topic düzeni, ISA-95 tarzı Unified Namespace tasarımlarında yaygın olan saha/hat/makine hiyerarşisini izler; bu da HMI'ı ileride daha fazla makineye genişletmeyi kolaylaştırır. MQTT Sparkplug B ve Unified Namespace rehberi bu yapıyı derinlemesine açıklar.
Qt MQTT Modülünü Edinme
Qt MQTT, Qt 6'da bir eklenti modülüdür. Qt Online Installer veya Maintenance Tool ile kurun: Qt 6 sürümünüzü seçin, "Additional Libraries" bölümünü açın ve "Qt MQTT" seçeneğini işaretleyin. Bir dağıtımın Qt paketlerini veya bir Yocto imajını kullanıyorsanız qtmqtt paketini arayın ya da modülü qtmqtt kaynak deposundan kendi Qt kurulumunuza karşı derleyin.
Yayınlamadan önce lisansı kontrol edin: Qt MQTT, Qt'nin büyük kısmını kapsayan LGPL altında değil, GPLv3 veya ticari Qt lisansı altında sunulur. Kapalı kaynak ticari bir HMI için bu genellikle ticari lisans anlamına gelir. Resmi Qt MQTT dokümantasyonu aşağıda kullanılan sınıfları ve lisans koşullarını listeler.
Modül MQTT 3.1, 3.1.1 ve 5.0'ı, Qt'nin SSL backend'i üzerinden TLS'i ve WebSocket gibi özel taşıma katmanlarını destekler.
CMake ile Proje Kurulumu
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)
Qt MQTT Client HMI'ını Yazma
Uygulamanın tamamı tek bir dosyaya sığar. Bir PressPanel widget'ı bir QMqttClient'a sahip olur, basit bir etiket ızgarası oluşturur, bağlandığında subscribe olur, bağlantı koptuğunda backoff ile yeniden bağlanır ve yakın zamanda güncellenmeyen değerleri griye çevirir.
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 yeniden bağlanmadan sonra aynı subscription
// nesnesini döndürebilir; her mesajı iki kez işlememeliyiz.
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; // bozuk payload'ları yok say
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;"); // gri = eskimiş
}
}
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();
}
Derleyin ve çalıştırın:
cmake -B build -DCMAKE_PREFIX_PATH=$HOME/Qt/6.8.0/gcc_64
cmake --build build
./build/press_hmi
Bir HMI'da Önemli Olan Tasarım Kararları
connected işleyicisinde subscribe olun. Clean session ile broker, bağlantı kesildiğinde subscription'ları siler; bu yüzden orada subscribe olmak, her yeniden bağlanmadan sonra onları geri yükler. Qt MQTT kendi başına yeniden bağlanmaz; panelin 30 saniyeyle sınırlanmış üstel backoff'a sahip tek atımlık bir zamanlayıcı kullanmasının nedeni budur.
Yalnızca değerleri değil, bayatlığı da gösterin. "48.2 °C" değerine bakan bir operatör, bu sayının şimdiye mi yoksa ağın koptuğu bir saat öncesine mi ait olduğunu anlayamaz. 10 saniye boyunca güncellenmeyen bir değeri griye çevirmek ucuzdur ve hatalı kararları önler. Bunu bir last will mesajı publish eden cihazlarla birleştirin; böylece HMI makinenin kendisinin çevrimdışı olduğunu da gösterebilir.
Durum için retained mesajlar kullanın. Makine durumunu retain bayrağıyla publish ederse, yeni başlatılan bir HMI bir sonraki değişikliğe kadar "--" göstermek yerine güncel durumu hemen gösterir.
Her şeyi doğrulayın. Sayı olmayan payload'lar yok sayılır, string değerler kısaltılır. Bozuk bir mesajın fabrika sahasındaki bir paneli çökertmesine asla izin vermeyin.
UI thread'ini serbest tutun. Bu örnekte client GUI thread'inde yaşar; bu, saniyede onlarca mesaj için sorun değildir. Bir panel hızla değişen yüzlerce etikete subscribe oluyorsa, mesaj işleyicide her etiketin son değerini saklayın ve etiketleri her mesajda değil, yaklaşık 200 ms'de bir bir zamanlayıcıdan yenileyin. Operatörler zaten bundan daha hızlı okuyamaz ve olay döngüsü dokunuşlara yanıt vermeye devam eder. Çok yoğun akışlar için client'ı bir worker QThread'e taşıyın ve değerleri arayüze kuyruğa alınmış sinyallerle iletin.
Komutlar QoS 1 ile. Kısa bir ağ kesintisinin bir STOP komutunu kaybettirmemesi için operatör komutlarında QoS 1 kullanın. Makine tarafı yine de komutları birer istek olarak ele almalı ve ortaya çıkan durumu raporlamalıdır; böylece HMI istenen şeyi değil, gerçekte olanı gösterir.
Makine Olmadan Test Etme
HMI'ı çalıştırın ve presi bir terminalden ücretsiz public MQTT broker üzerinde simüle edin. Public alan herkes tarafından okunabilir; bu yüzden yalnızca rastgele öneki ve yapay değerleri kullanın:
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
Üçüncü bir terminalde HMI'ın gönderdiği komutları izleyin:
mosquitto_sub -h broker.connectmq.com -p 1883 -i qt-sim-cmd -t "$P/cmd" -v
Döngüyü durdurduğunuzda sıcaklık ve basınç değerleri 10 saniye sonra griye döner. Birkaç makineli daha zengin bir simülasyon isterseniz, ConnectMQ 3D dijital ikiz demosu güvenli WebSocket üzerinden koca bir sanal fabrikayı publish eder ve çok makineli bir HMI için iyi bir stres testidir.
Gömülü Hedefler ve WebSocket
Gömülü Linux'ta Qt HMI'ları genellikle eglfs veya linuxfb platform eklentisiyle tam ekran çalışır. MQTT kodu değişmez, ancak bağlanmadan önce imajın bir CA sertifika paketi içerdiğini ve saatin ayarlandığını kontrol edin; 1970 yılında açılan bir kart her sertifikayı henüz geçerli değil diye reddeder.
Tesis ağı dışarıya yalnızca HTTPS'e izin veriyorsa, Qt MQTT setTransport() metoduna özel bir QIODevice taşıma katmanı verilerek WebSocket üzerinden çalışabilir; Qt MQTT örnekleri, wss://broker.connectmq.com:8084/mqtt ile yeniden kullanabileceğiniz bir WebSocket taşıma katmanı içerir. Taşıma katmanı ödünleşimleri için WebSocket üzerinden MQTT yazısını okuyun.
Prototipten Tesise
Üretim için HMI'ı ConnectMQ gibi bir bulut MQTT broker üzerindeki özel bir broker'a karşı çalıştırın:
- ConnectMQ portalında bir broker oluşturun ve üretilen kullanıcı adını, parolayı ve client ID'yi kopyalayın.
- Panelin ortamında (örneğin systemd unit dosyasında)
MQTT_USERNAME,MQTT_PASSWORD,MQTT_CLIENT_IDveMQTT_TOPIC_PREFIXdeğerlerini ayarlayın. - 8883'te TLS'i koruyun ve her panele kendi kimlik bilgilerini verin, çünkü ConnectMQ client ID'leri verilen kimlik bilgilerine bağlar.
- Aynı etiketleri merkezi olarak izlemek için portalın topic'leri ISA-95 seviyelerine göre gruplayan cihazlar görünümünü ve eşik tabanlı uyarı kurallarını kullanın.
Gateway'iniz C ile yazılmışsa Paho MQTT C eğitimi bu HMI ile iyi bir ikili oluşturur; MQTT client'larını test etme rehberi etiket ayrıştırma ve yeniden bağlanma mantığını CI altına nasıl alacağınızı gösterir; MQTT client kütüphaneleri rehberi de Qt MQTT'yi diğer seçeneklerle karşılaştırır.
Sorun Giderme
| Belirti | Neden | Çözüm |
|---|---|---|
find_package, Qt6Mqtt paketini bulamıyor |
Modül kurulu değil | Maintenance Tool'da "Qt MQTT" ekleyin veya qtmqtt derleyin |
| Bir taşıma hatasıyla hemen bağlantı kopuyor | TLS backend'i eksik veya CA paketi yok | Qt'nin OpenSSL eklentisini ve sistem sertifikalarını kontrol edin |
| Bağlanıyor ama değer gelmiyor | Topic öneki uyuşmazlığı | MQTT_TOPIC_PREFIX değerini simülatörle karşılaştırın |
| İki panel birbirini sürekli düşürüyor | Aynı client ID | Her panel için benzersiz client ID |
| Yoğun trafikte arayüz donuyor | Çok fazla etiket güncellemesi | Güncellemeleri bir zamanlayıcıyla kısın ve yalnızca son değeri çizin |
ConnectMQ ile Başlayın
Qt HMI'ınızı dakikalar içinde özel ve güvenli bir broker'a taşıyın. app.connectmq.com üzerinde kredi kartı gerekmeden ücretsiz bir Serverless broker oluşturun, üretilen kimlik bilgilerini panelin ortamına ayarlayın ve operatörler HMI'ı kullanırken aynı etiketleri portalın canlı topic explorer'ında izleyin.
Sık Sorulan Sorular
Qt MQTT modülü ücretsiz mi?
GPLv3 veya ticari Qt lisansı altında sunulur. Kapalı kaynak ticari ürünler genellikle ticari lisansa ihtiyaç duyar; bu yüzden yayınlamadan önce koşulları kontrol edin.
QMqttClient otomatik olarak yeniden bağlanır mı?
Hayır. disconnected sinyalini dinleyin ve bir zamanlayıcıdan connectToHostEncrypted() metodunu yeniden çağırın, ardından connected işleyicisinde yeniden subscribe olun.
Qt MQTT, MQTT 5'i destekliyor mu?
Evet. Bağlanmadan önce setProtocolVersion(QMqttClient::MQTT_5_0) çağırın; ConnectMQ hem MQTT 5.0'ı hem de 3.1.1'i destekler.
Qt MQTT'yi QML'den kullanabilir miyim?
Evet. QMqttClient'ı özellikler ve çağrılabilir metotlarla saran bir C++ nesnesini QML'e açın ya da Qt MQTT modülüyle gelen QML örneklerini uyarlayın.



