Tutoriais de IoT

Cliente MQTT com Qt em C++: Construindo IHMs Desktop e Embarcadas

Use o módulo Qt MQTT para construir uma IHM em C++ que se conecta a um broker na nuvem via TLS, assina topics de máquinas, mostra valores ao vivo com detecção de dados obsoletos e envia comandos do operador.

IHM com cliente MQTT em Qt em uma tela touch industrial mostrando temperatura, pressão e estado de uma máquina ao vivo a partir de um broker na nuvem
Rode isso no seu próprio broker MQTTTenha um broker MQTT privado e gerenciado no plano Free e comece com exemplos de código prontos.
Teste grátis Exemplos de código Sem cartão de crédito

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:

  1. Crie um broker no portal do ConnectMQ e copie o usuário, a senha e o client ID gerados.
  2. Defina MQTT_USERNAME, MQTT_PASSWORD, MQTT_CLIENT_ID e MQTT_TOPIC_PREFIX no ambiente do painel (por exemplo, na unit do systemd).
  3. Mantenha TLS na 8883 e dê a cada painel as suas próprias credenciais, porque o ConnectMQ vincula os client IDs às credenciais emitidas.
  4. 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.

Artigos relacionados

Melhores Ferramentas CLI MQTT para Testes, Scripts e Depuração

Uma comparação prática entre mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, a CLI do MQTT.js, curl e mqttui, com o mesmo teste rodando no broker.connectmq.com, para você escolher a ferramenta certa para scripts e depuração.

9 min de leitura

Todos os artigos →

Seu broker MQTT privado está a poucos cliques de distância

Crie uma conta, receba as credenciais do seu broker e conecte seu primeiro dispositivo — sem servidores para instalar, atualizar ou escalar.