Un cliente MQTT en Qt encaja de forma natural en paneles de operario y dashboards de escritorio: Qt te da una interfaz nativa rápida en Windows, Linux y placas embebidas, y MQTT le aporta datos de máquina en vivo. En este tutorial usarás el módulo Qt MQTT para construir una pequeña HMI en C++ que se conecta por TLS, muestra valores en vivo, marca los datos obsoletos y envía comandos de vuelta a una máquina.
Qué vas a construir
La HMI vigila una máquina, una prensa hidráulica de la línea 1, y muestra tres valores más un botón de comando:
| Variable | Topic | Payload |
|---|---|---|
| Temperatura del aceite | <prefix>/line1/press1/temperature |
48.2 |
| Presión | <prefix>/line1/press1/pressure |
182.5 |
| Estado | <prefix>/line1/press1/state |
RUNNING, IDLE, FAULT |
| Comando | <prefix>/line1/press1/cmd |
START o STOP |
La estructura de topics sigue la jerarquía planta/línea/máquina habitual en los diseños de Unified Namespace de estilo ISA-95, lo que hace que la HMI sea fácil de ampliar a más máquinas más adelante. La guía de MQTT Sparkplug B y Unified Namespace explica esa estructura en profundidad.
Obtener el módulo Qt MQTT
Qt MQTT es un módulo adicional en Qt 6. Instálalo con el Qt Online Installer o la Maintenance Tool: selecciona tu versión de Qt 6, abre "Additional Libraries" y marca "Qt MQTT". Si usas los paquetes de Qt de una distribución o una imagen Yocto, busca un paquete qtmqtt, o compila el módulo desde el repositorio fuente qtmqtt contra tu instalación de Qt.
Revisa la licencia antes de distribuir: Qt MQTT se ofrece bajo GPLv3 o una licencia comercial de Qt, no bajo la LGPL que cubre buena parte de Qt. Para una HMI comercial de código cerrado eso suele implicar una licencia comercial. La documentación oficial de Qt MQTT enumera las clases que se usan a continuación y las condiciones de licencia.
El módulo es compatible con MQTT 3.1, 3.1.1 y 5.0, TLS mediante el backend SSL de Qt y transportes personalizados como WebSocket.
Configuración del proyecto con 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)
Escribir la HMI con el cliente MQTT de Qt
Toda la aplicación cabe en un solo archivo. Un widget PressPanel es dueño de un QMqttClient, construye una sencilla cuadrícula de etiquetas, se suscribe al conectarse, reconecta con backoff cuando cae el enlace y atenúa los valores que no se han actualizado recientemente.
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 puede devolver el mismo objeto de suscripción
// tras una reconexión, y no debemos procesar cada mensaje dos veces.
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; // ignorar payloads mal formados
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;"); // gris = 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();
}
Compila y ejecuta:
cmake -B build -DCMAKE_PREFIX_PATH=$HOME/Qt/6.8.0/gcc_64
cmake --build build
./build/press_hmi
Decisiones de diseño que importan en una HMI
Suscríbete en el manejador de connected. Con una sesión limpia, el broker descarta las suscripciones al desconectarse, así que suscribirse ahí las restaura tras cada reconexión. Qt MQTT no reconecta por sí solo, y por eso el panel usa un temporizador de un solo disparo con backoff exponencial limitado a 30 segundos.
Muestra la antigüedad, no solo los valores. Un operario que ve "48.2 °C" no puede saber si ese número es de ahora o de hace una hora, cuando se cayó la red. Atenuar un valor tras 10 segundos sin actualizaciones es barato y evita malas decisiones. Combínalo con dispositivos que publiquen un mensaje de last will para que la HMI también pueda mostrar cuándo se desconectó la propia máquina.
Usa retained messages para el estado. Si la máquina publica su estado con el flag retain, una HMI recién iniciada muestra el estado actual de inmediato en lugar de "--" hasta el siguiente cambio.
Valídalo todo. Los payloads que no son números se ignoran y los valores de texto se truncan. Nunca dejes que un mensaje mal formado bloquee un panel en la planta de producción.
Mantén libre el hilo de la interfaz. En este ejemplo el cliente vive en el hilo de la GUI, lo cual está bien para decenas de mensajes por segundo. Si un panel se suscribe a cientos de variables que cambian rápido, guarda el último valor de cada variable en el manejador de mensajes y refresca las etiquetas desde un temporizador cada 200 ms aproximadamente, en lugar de con cada mensaje. De todos modos, los operarios no pueden leer más rápido, y el bucle de eventos sigue respondiendo a los toques. Para flujos muy intensos, mueve el cliente a un QThread de trabajo y pasa los valores a la interfaz mediante señales en cola.
Comandos con QoS 1. Usa QoS 1 para los comandos del operario, para que un breve corte de red no haga perder un STOP. Aun así, el lado de la máquina debe tratar los comandos como peticiones e informar del estado resultante, para que la HMI muestre lo que realmente ocurrió, no lo que se pidió.
Pruebas sin máquina
Ejecuta la HMI y simula la prensa desde una terminal contra el servidor MQTT público gratuito. El área pública es legible por todo el mundo y sirve para pruebas y prototipos, no para datos de producción, así que usa el prefijo aleatorio y solo 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
En una tercera terminal, observa los comandos que envía la HMI:
mosquitto_sub -h broker.connectmq.com -p 1883 -i qt-sim-cmd -t "$P/cmd" -v
Detén el bucle y los valores de temperatura y presión se volverán grises al cabo de 10 segundos. Si quieres una simulación más rica con varias máquinas, la demo del gemelo digital 3D de ConnectMQ publica toda una fábrica virtual por WebSocket seguro y es una buena prueba de carga para una HMI multimáquina.
Dispositivos embebidos y WebSocket
En Linux embebido, las HMI de Qt suelen ejecutarse a pantalla completa con el plugin de plataforma eglfs o linuxfb. El código MQTT no cambia, pero comprueba que la imagen contiene un paquete de certificados de CA y que el reloj está en hora antes de conectar; una placa que arranca en 1970 rechaza todos los certificados por no ser aún válidos.
Si la red de la planta solo permite HTTPS saliente, Qt MQTT puede funcionar sobre WebSocket pasando un transporte QIODevice personalizado a setTransport(); los ejemplos de Qt MQTT incluyen un transporte WebSocket que puedes reutilizar con wss://broker.connectmq.com:8084/mqtt. Para las ventajas y desventajas de cada transporte, lee MQTT sobre WebSocket.
Del prototipo a la planta
En producción, sea una línea de prensas en Querétaro o en Bilbao, ejecuta la HMI contra un broker privado en un broker MQTT en la nube como ConnectMQ, un broker MQTT gestionado:
- Crea un broker en el portal de ConnectMQ y copia el usuario, la contraseña y el client ID generados.
- Define
MQTT_USERNAME,MQTT_PASSWORD,MQTT_CLIENT_IDyMQTT_TOPIC_PREFIXen el entorno del panel (por ejemplo, en su unidad systemd). - Mantén TLS en el 8883 y da a cada panel sus propias credenciales, porque ConnectMQ vincula los client ID a las credenciales emitidas.
- Usa la vista de dispositivos del portal, que agrupa los topics por niveles ISA-95, y las reglas de alerta por umbral para vigilar las mismas variables de forma centralizada.
Si tu pasarela está escrita en C, el tutorial de Paho MQTT C combina bien con esta HMI, la guía de pruebas de clientes MQTT muestra cómo poner bajo CI el análisis de variables y la lógica de reconexión, y la guía de bibliotecas cliente MQTT compara Qt MQTT con otras opciones.
Solución de problemas
| Síntoma | Causa | Solución |
|---|---|---|
find_package no encuentra Qt6Mqtt |
Módulo no instalado | Añade "Qt MQTT" en la Maintenance Tool o compila qtmqtt |
| Se desconecta de inmediato con un error de transporte | Falta el backend TLS o no hay paquete de CA | Comprueba el plugin OpenSSL de Qt y los certificados del sistema |
| Conecta, pero no muestra valores | El prefijo de topic no coincide | Compara MQTT_TOPIC_PREFIX con el del simulador |
| Dos paneles se expulsan mutuamente una y otra vez | Mismo client ID | Un client ID único por panel |
| La interfaz se congela con mucho tráfico | Demasiadas actualizaciones de etiquetas | Limita las actualizaciones con un temporizador y pinta solo el último valor |
Empieza con ConnectMQ
Pon tu HMI de Qt en un broker privado y seguro en minutos. Crea un broker Serverless gratuito en app.connectmq.com, sin tarjeta de crédito, define las credenciales generadas en el entorno del panel y observa las mismas variables en el explorador de topics en vivo del portal mientras los operarios usan la HMI.
Preguntas frecuentes
¿El módulo Qt MQTT es gratuito?
Está disponible bajo GPLv3 o una licencia comercial de Qt. Los productos comerciales de código cerrado suelen necesitar la licencia comercial, así que revisa las condiciones antes de distribuir.
¿QMqttClient reconecta automáticamente?
No. Escucha la señal disconnected y vuelve a llamar a connectToHostEncrypted() desde un temporizador, y después vuelve a suscribirte en el manejador de connected.
¿Qt MQTT es compatible con MQTT 5?
Sí. Llama a setProtocolVersion(QMqttClient::MQTT_5_0) antes de conectar; ConnectMQ es compatible tanto con MQTT 5.0 como con 3.1.1.
¿Puedo usar Qt MQTT desde QML?
Sí. Expón a QML un objeto C++ que envuelva QMqttClient con propiedades y métodos invocables, o adapta los ejemplos QML que incluye el módulo Qt MQTT.



