Um cliente MQTT com Qt combina naturalmente com painéis de operador e dashboards desktop: o Qt oferece uma UI nativa rápida no Windows, no Linux e em placas embarcadas, e o MQTT leva até ela os dados das máquinas ao vivo. Neste tutorial você vai usar o módulo Qt MQTT para construir uma pequena IHM em C++ que se conecta via TLS, exibe valores ao vivo, sinaliza dados obsoletos e envia comandos de volta a uma máquina.
O que você vai construir
A IHM monitora uma máquina, uma prensa hidráulica na linha 1 de uma planta de autopeças, e mostra três valores mais um botão de comando:
| Tag | Topic | Payload |
|---|---|---|
| Temperatura do óleo | <prefix>/line1/press1/temperature |
48.2 |
| Pressão | <prefix>/line1/press1/pressure |
182.5 |
| Estado | <prefix>/line1/press1/state |
RUNNING, IDLE, FAULT |
| Comando | <prefix>/line1/press1/cmd |
START ou STOP |
A estrutura de topics segue a hierarquia site/linha/máquina comum em projetos de Unified Namespace no estilo ISA-95, o que facilita estender a IHM para mais máquinas depois. O guia de MQTT Sparkplug B e Unified Namespace explica essa estrutura em profundidade.
Obtendo o módulo Qt MQTT
O Qt MQTT é um módulo adicional do Qt 6. Instale-o com o Qt Online Installer ou com a Maintenance Tool: selecione a sua versão do Qt 6, abra "Additional Libraries" e marque "Qt MQTT". Se você usa os pacotes Qt de uma distribuição ou uma imagem Yocto, procure um pacote qtmqtt, ou compile o módulo a partir do repositório de código-fonte qtmqtt contra a sua instalação do Qt.
Confira a licença antes de distribuir: o Qt MQTT é oferecido sob GPLv3 ou sob uma licença comercial do Qt, e não sob a LGPL que cobre grande parte do Qt. Para uma IHM comercial de código fechado, isso geralmente significa uma licença comercial. A documentação oficial do Qt MQTT lista as classes usadas abaixo e os termos de licenciamento.
O módulo suporta MQTT 3.1, 3.1.1 e 5.0, TLS por meio do backend SSL do Qt e transportes personalizados como WebSocket.
Configuração do projeto com 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)
Escrevendo a IHM com o cliente MQTT do Qt
A aplicação inteira cabe em um arquivo. Um widget PressPanel é dono de um QMqttClient, monta uma grade simples de labels, assina ao conectar, reconecta com backoff quando o link cai e deixa em cinza os valores que não foram atualizados recentemente.
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: o Qt pode devolver o mesmo objeto de assinatura
// após uma reconexão, e não podemos tratar cada mensagem duas vezes.
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; // ignora payloads malformados
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;"); // cinza = obsoleto
}
}
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();
}
Compile e execute:
cmake -B build -DCMAKE_PREFIX_PATH=$HOME/Qt/6.8.0/gcc_64
cmake --build build
./build/press_hmi
Decisões de projeto que importam em uma IHM
Assine no handler connected. Com clean session, o broker descarta as assinaturas ao desconectar, então assinar ali as restaura após cada reconexão. O Qt MQTT não reconecta sozinho, e é por isso que o painel usa um timer de disparo único com backoff exponencial limitado a 30 segundos.
Mostre a idade dos dados, não só os valores. Um operador que vê "48.2 °C" não consegue saber se o número é de agora ou de uma hora atrás, quando a rede caiu. Deixar um valor em cinza após 10 segundos sem atualização é barato e evita decisões erradas. Combine isso com dispositivos que publicam uma mensagem de last will, para que a IHM também mostre quando a própria máquina ficou offline.
Use retained messages para o estado. Se a máquina publica seu estado com a flag retain, uma IHM recém-iniciada mostra o estado atual imediatamente, em vez de "--" até a próxima mudança.
Valide tudo. Payloads que não são números são ignorados, e valores de texto são truncados. Nunca deixe uma mensagem malformada derrubar um painel no chão de fábrica.
Mantenha a thread da UI livre. Neste exemplo o cliente vive na thread da GUI, o que funciona bem para dezenas de mensagens por segundo. Se um painel assina centenas de tags que mudam rapidamente, guarde o último valor de cada tag no handler de mensagens e atualize os labels a partir de um timer a cada 200 ms, mais ou menos, em vez de a cada mensagem. Os operadores não conseguem ler mais rápido do que isso de qualquer forma, e o event loop continua respondendo aos toques. Para fluxos muito pesados, mova o cliente para uma QThread de trabalho e passe os valores para a UI por sinais enfileirados.
Comandos com QoS 1. Use QoS 1 para comandos do operador, para que uma breve oscilação da rede não perca um STOP. O lado da máquina ainda deve tratar os comandos como solicitações e informar o estado resultante, para que a IHM mostre o que realmente aconteceu, e não o que foi pedido.
Testando sem uma máquina
Rode a IHM e simule a prensa a partir de um terminal contra o servidor MQTT público (gratuito). A área pública pode ser lida por qualquer pessoa e serve para testes, não para dados de produção, então use apenas o prefixo aleatório e valores sintéticos:
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
Em um terceiro terminal, acompanhe os comandos que a IHM envia:
mosquitto_sub -h broker.connectmq.com -p 1883 -i qt-sim-cmd -t "$P/cmd" -v
Pare o loop e os valores de temperatura e pressão ficam cinza após 10 segundos. Se quiser uma simulação mais rica, com várias máquinas, a demo de gêmeo digital 3D do ConnectMQ publica uma fábrica virtual inteira via WebSocket seguro e é um bom teste de estresse para uma IHM com várias máquinas.
Alvos embarcados e WebSocket
No Linux embarcado, as IHMs Qt costumam rodar em tela cheia com o plugin de plataforma eglfs ou linuxfb. O código MQTT não muda, mas verifique se a imagem contém um bundle de certificados de CA e se o relógio está acertado antes de conectar; uma placa que inicializa em 1970 rejeita todo certificado como ainda não válido.
Se a rede da planta só permite HTTPS de saída, o Qt MQTT pode rodar sobre WebSocket passando um transporte QIODevice personalizado para setTransport(); os exemplos do Qt MQTT incluem um transporte WebSocket que você pode reutilizar com wss://broker.connectmq.com:8084/mqtt. Para os prós e contras dos transportes, leia MQTT sobre WebSocket.
Do protótipo à planta
Em produção, rode a IHM contra um broker privado em um broker MQTT na nuvem como o ConnectMQ, um broker MQTT gerenciado:
- Crie um broker no portal do ConnectMQ e copie o usuário, a senha e o client ID gerados.
- Defina
MQTT_USERNAME,MQTT_PASSWORD,MQTT_CLIENT_IDeMQTT_TOPIC_PREFIXno ambiente do painel (por exemplo, na unit do systemd). - Mantenha TLS na 8883 e dê a cada painel as suas próprias credenciais, porque o ConnectMQ vincula os client IDs às credenciais emitidas.
- Use a visão de dispositivos do portal, que agrupa os topics pelos níveis ISA-95, e as regras de alerta por limite para acompanhar as mesmas tags de forma centralizada.
Se o seu gateway é escrito em C, o tutorial de Paho MQTT C combina bem com esta IHM, o guia sobre como testar clientes MQTT mostra como colocar o parse de tags e a lógica de reconexão sob CI, e o guia de bibliotecas cliente MQTT compara o Qt MQTT com outras opções.
Solução de problemas
| Sintoma | Causa | Correção |
|---|---|---|
find_package não encontra Qt6Mqtt |
Módulo não instalado | Adicione "Qt MQTT" na Maintenance Tool ou compile o qtmqtt |
| Desconecta imediatamente com erro de transporte | Backend TLS ausente ou sem bundle de CAs | Verifique o plugin OpenSSL do Qt e os certificados do sistema |
| Conecta, mas sem valores | Prefixo de topic divergente | Compare MQTT_TOPIC_PREFIX com o do simulador |
| Dois painéis ficam derrubando um ao outro | Mesmo client ID | Client ID único por painel |
| A UI trava com tráfego intenso | Atualizações de labels demais | Limite as atualizações com um timer e desenhe só o último valor |
Comece a usar o ConnectMQ
Coloque a sua IHM Qt em um broker privado e seguro em minutos. Crie um broker Serverless gratuito em app.connectmq.com, sem cartão de crédito, defina as credenciais geradas no ambiente do painel e acompanhe as mesmas tags no explorador de topics ao vivo do portal enquanto os operadores usam a IHM.
Perguntas frequentes
O módulo Qt MQTT é gratuito?
Ele está disponível sob GPLv3 ou sob uma licença comercial do Qt. Produtos comerciais de código fechado normalmente precisam da licença comercial, então confira os termos antes de distribuir.
O QMqttClient reconecta automaticamente?
Não. Escute o sinal disconnected e chame connectToHostEncrypted() novamente a partir de um timer, depois refaça as assinaturas no handler connected.
O Qt MQTT suporta MQTT 5?
Sim. Chame setProtocolVersion(QMqttClient::MQTT_5_0) antes de conectar; o ConnectMQ suporta tanto MQTT 5.0 quanto 3.1.1.
Posso usar o Qt MQTT a partir de QML?
Sim. Exponha ao QML um objeto C++ que encapsula o QMqttClient com propriedades e métodos invocáveis, ou adapte os exemplos QML que acompanham o módulo Qt MQTT.



