Modul master Level 3 VibeKoding: Antarmuka Mesin Industri (HMI) dengan Qt C++/Python.Modul master Level 3 VibeKoding: Antarmuka Mesin Industri (HMI) dengan Qt C++/Python.
In this tutorial, we will complete a full closed loop: build an industrial-grade pump monitoring HMI (Human-Machine Interface) system from scratch with Qt. It can read sensor data in real time, draw pressure trend charts, trigger automatic over-threshold alarms, and record fault logs. The whole process uses free simulation software on a PC instead of real industrial hardware.In this tutorial, we will complete a full closed loop: build an industrial-grade pump monitoring HMI (Human-Machine Interface) system from scratch with Qt. It can read sensor data in real time, draw pressure trend charts, trigger automatic over-threshold alarms, and record fault logs. The whole process uses free simulation software on a PC instead of real industrial hardware.
For this tutorial, you should at least have:For this tutorial, you should at least have:
> Zero hardware, zero cost: use free PC simulation software (Modbus Slave) as the lower-level device; no need to buy hardware. Use official Qt QModbusTcpClient + Qt Charts modules directly, no manual protocol parsing needed. After running, you will see real-time pressure trends, over-threshold alarm popups, and fault logs, matching real factory workflow.> Zero hardware, zero cost: use free PC simulation software (Modbus Slave) as the lower-level device; no need to buy hardware. Use official Qt QModbusTcpClient + Qt Charts modules directly, no manual protocol parsing needed. After running, you will see real-time pressure trends, over-threshold alarm popups, and fault logs, matching real factory workflow.
In industrial automation, there are two concepts you must understand: upper computer and lower computer.In industrial automation, there are two concepts you must understand: upper computer and lower computer.
Lower Computer: the "hands and feet" on-siteLower Computer: the "hands and feet" on-site
The lower computer is the controller that directly interacts with physical devices. In factories, it is usually a PLC (Programmable Logic Controller) or sensor, responsible for:The lower computer is the controller that directly interacts with physical devices. In factories, it is usually a PLC (Programmable Logic Controller) or sensor, responsible for:
You can think of the lower computer as a "worker" on the factory floor. It does not need complex thinking, but must execute tasks reliably.You can think of the lower computer as a "worker" on the factory floor. It does not need complex thinking, but must execute tasks reliably.
Upper Computer: the "eyes and brain" in the control roomUpper Computer: the "eyes and brain" in the control room
The upper computer is monitoring software running on PC or industrial computer, which is the HMI (Human-Machine Interface) we will build today. It is responsible for:The upper computer is monitoring software running on PC or industrial computer, which is the HMI (Human-Machine Interface) we will build today. It is responsible for:
You can think of the upper computer as the factory's "monitoring center." Operators can understand plant status from the screen.You can think of the upper computer as the factory's "monitoring center." Operators can understand plant status from the screen.
How do they communicate?How do they communicate?
Upper and lower computers exchange data through industrial communication protocols. The most common one is Modbus, a "veteran" protocol born in 1979. It is still widely used because it is simple, reliable, and supported by almost all industrial devices.Upper and lower computers exchange data through industrial communication protocols. The most common one is Modbus, a "veteran" protocol born in 1979. It is still widely used because it is simple, reliable, and supported by almost all industrial devices.
text Control room Factory site ββββββββββββ Modbus protocol ββββββββββββ β Upper β ββββββββββββββββββββΊ β Lower β β computer β "Tell me pressure" β computer β β (Qt HMI) β "Pressure is 1.20MPa"β (PLC/Sensor) β Display β β Read dataβ β Log data β β Control β β Alarms β β Protect β ββββββββββββ ββββββββββββ
πΌοΈ placeholder: Diagram of upper vs lower computer relationship: PC screen (upper computer) on the left, PLC and pump (lower computer) on the right, connected via Modbusplaceholder: Diagram of upper vs lower computer relationship: PC screen (upper computer) on the left, PLC and pump (lower computer) on the right, connected via Modbus
Modbus is the "common language" of industrial communication. It defines how upper and lower computers "talk."Modbus is the "common language" of industrial communication. It defines how upper and lower computers "talk."
Only two core concepts:Only two core concepts:
0, 1, 2, ...), storing a number. For example, address 0 stores pressure and address 1 stores temperature.Register: data "cells" in the lower computer. Each has an address (0, 1, 2, ...), storing a number. For example, address 0 stores pressure and address 1 stores temperature.Two common Modbus variants:Two common Modbus variants:
| Variant | Transport | Typical Scenario |
|---|---|---|
| Modbus RTU | Serial (RS-485/RS-232) | Short distance, direct device connection |
| Modbus TCP | Ethernet (TCP/IP) | Long distance, network communication |
This tutorial uses Modbus TCP. Since it is network-based, upper-computer app and lower-computer simulator can run on the same machine with no physical wiring.This tutorial uses Modbus TCP. Since it is network-based, upper-computer app and lower-computer simulator can run on the same machine with no physical wiring.
Qt is a top framework choice for industrial software. Many monitoring interfaces in factories, hospitals, and transportation systems are built with Qt. The reasons are simple:Qt is a top framework choice for industrial software. Many monitoring interfaces in factories, hospitals, and transportation systems are built with Qt. The reasons are simple:
| Advantage | Explanation |
|---|---|
| Cross-platform | One codebase compiles to Windows, Linux, and embedded devices |
| Built-in industrial protocol support | Qt Serial Bus supports Modbus natively, no third-party library required |
| Powerful charting | Qt Charts provides professional real-time charts |
| High performance | C++ foundation suitable for real-time data refresh |
| Mature and stable | 30-year history, proven in industrial domain |
We will build a Pump Monitoring HMI System simulating real factory pump pressure monitoring:We will build a Pump Monitoring HMI System simulating real factory pump pressure monitoring:
| Function | Description |
|---|---|
| Real-time data reading | Read pressure from lower computer every second |
| Pressure trend chart | Line chart for last 60 seconds of pressure |
| Over-threshold alarm | Popup warning and red UI when pressure exceeds threshold |
| Fault log | Record all alarm events in database for history queries |
| Manual control | One-click start/stop pump (write lower-computer register) |
πΌοΈ placeholder: Pump monitoring HMI preview showing real-time pressure number, trend chart, alarm indicator, start/stop button, and log listplaceholder: Pump monitoring HMI preview showing real-time pressure number, trend chart, alarm indicator, start/stop button, and log list
We will complete the flow in these steps:We will complete the flow in these steps:
Qt provides a free open-source version, enough for this tutorial.Qt provides a free open-source version, enough for this tutorial.
> Tip: if Qt is already installed but missing Serial Bus or Charts, rerun Qt Maintenance Tool and add components.> Tip: if Qt is already installed but missing Serial Bus or Charts, rerun Qt Maintenance Tool and add components.
πΌοΈ placeholder: Qt installer component selection screenshot highlighting Qt Serial Bus and Qt Chartsplaceholder: Qt installer component selection screenshot highlighting Qt Serial Bus and Qt Charts
Modbus Slave is a free Modbus slave simulator. It can simulate an industrial device (PLC/sensor) on your computer so your upper app has something to communicate with.Modbus Slave is a free Modbus slave simulator. It can simulate an industrial device (PLC/sensor) on your computer so your upper app has something to communicate with.
127.0.0.1 (localhost)IP address: 127.0.0.1 (localhost)502 (default Modbus TCP port)Port: 502 (default Modbus TCP port)0, 1, 2, ...)You will see a register table, each row is a register address (0, 1, 2, ...)1=running, 0=stopped)Double-click value at address 2, change to 1 (pump state: 1=running, 0=stopped)Now Modbus Slave is your "24/7 virtual pump." Keep the window open; it will continuously respond to read/write requests.Now Modbus Slave is your "24/7 virtual pump." Keep the window open; it will continuously respond to read/write requests.
πΌοΈ placeholder: Modbus Slave screenshot showing TCP config and simulated register valuesplaceholder: Modbus Slave screenshot showing TCP config and simulated register values
> Dynamic simulation tip: Modbus Slave supports auto increment/random changes. Right-click register value and choose "Auto increment" or "Random" to simulate realistic sensor fluctuations.> Dynamic simulation tip: Modbus Slave supports auto increment/random changes. Right-click register value and choose "Auto increment" or "Random" to simulate realistic sensor fluctuations.
Open Qt Creator and create a new project:Open Qt Creator and create a new project:
Open PumpHMI.pro (or CMakeLists.txt if using CMake), and add key modules:Open PumpHMI.pro (or CMakeLists.txt if using CMake), and add key modules:
pro QT += core gui widgets serialbus charts sql
| Module | Purpose |
|---|---|
serialbus | Provides QModbusTcpClient for Modbus TCP communication |
charts | Provides QChart, QLineSeries for real-time trend chart |
sql | Provides QSqlDatabase for SQLite fault logs |
If using CMake, equivalent config:If using CMake, equivalent config:
cmake find_package(Qt6 REQUIRED COMPONENTS Widgets SerialBus Charts Sql) target_link_libraries(PumpHMI PRIVATE Qt6::Widgets Qt6::SerialBus Qt6::Charts Qt6::Sql)
Ask AI to generate header file:Ask AI to generate header file:
text Please help me write mainwindow.h with core members for pump monitoring HMI: 1. QModbusTcpClient for Modbus TCP communication 2. QTimer for timed data reading 3. QChart + QLineSeries for real-time trend chart 4. QSqlDatabase for fault log storage 5. UI elements: pressure label, status indicator, start/stop button, log table
Core header:Core header:
cpp // mainwindow.h #ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QMainWindow> #include <QModbusTcpClient> #include <QModbusDataUnit> #include <QTimer> #include <QtCharts> #include <QSqlDatabase> #include <QLabel> #include <QPushButton> #include <QTableWidget> class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr); ~MainWindow(); private slots: void connectModbus(); // connect lower computer void readPressure(); // timed pressure read void onReadReady(); // read callback void triggerAlarm(float v); // trigger alarm void togglePump(); // start/stop pump private: // Modbus communication QModbusTcpClient *m_modbusClient = nullptr; QTimer *m_pollTimer = nullptr; // Real-time chart QChart *m_chart = nullptr; QLineSeries *m_series = nullptr; QDateTimeAxis *m_axisX = nullptr; QValueAxis *m_axisY = nullptr; // Database QSqlDatabase m_db; // UI elements QLabel *m_pressureLabel = nullptr; // pressure display QLabel *m_statusLight = nullptr; // status indicator QPushButton *m_pumpButton = nullptr; // start/stop button QTableWidget *m_logTable = nullptr; // log table // Alarm threshold float m_alarmThreshold = 1.50f; // alarm above 1.50 MPa bool m_pumpRunning = false; void setupUI(); void setupDatabase(); void logAlarm(float pressure, const QString &message); }; #endif // MAINWINDOW_H
πΌοΈ placeholder: Screenshot of mainwindow.h in Qt Creatorplaceholder: Screenshot of mainwindow.h in Qt Creator
Implement connection logic in mainwindow.cpp:Implement connection logic in mainwindow.cpp:
cpp // mainwindow.cpp - connection section void MainWindow::connectModbus() { m_modbusClient = new QModbusTcpClient(this); // Connect to Modbus Slave simulator m_modbusClient->setConnectionParameter( QModbusDevice::NetworkPortParameter, 502); m_modbusClient->setConnectionParameter( QModbusDevice::NetworkAddressParameter, "127.0.0.1"); m_modbusClient->setTimeout(1000); // 1s timeout m_modbusClient->setNumberOfRetries(3); // retry 3 times if (!m_modbusClient->connectDevice()) { statusBar()->showMessage("Failed to connect lower computer!", 3000); return; } statusBar()->showMessage("Connected to lower computer (127.0.0.1:502)", 3000); // Start timer, read once per second m_pollTimer = new QTimer(this); connect(m_pollTimer, &QTimer::timeout, this, &MainWindow::readPressure); m_pollTimer->start(1000); // 1000ms = 1s }
Code notes:Code notes:
| Code | Meaning |
|---|---|
QModbusTcpClient | Built-in Qt Modbus TCP client, communicates with lower computer |
NetworkPortParameter, 502 | Connect to port 502 (same as Modbus Slave config) |
NetworkAddressParameter, "127.0.0.1" | Connect localhost (simulator runs locally) |
m_pollTimer->start(1000) | Call readPressure() every second |
cpp // mainwindow.cpp - reading section void MainWindow::readPressure() { if (!m_modbusClient || m_modbusClient->state() != QModbusDevice::ConnectedState) return; // Build read request: start at address 0, read 3 holding registers QModbusDataUnit readUnit( QModbusDataUnit::HoldingRegisters, // register type 0, // start address 3 // quantity ); // Send async read request if (auto *reply = m_modbusClient->sendReadRequest(readUnit, 1)) { if (!reply->isFinished()) { connect(reply, &QModbusReply::finished, this, &MainWindow::onReadReady); } else { delete reply; // broadcast request, delete directly } } } void MainWindow::onReadReady() { auto *reply = qobject_cast<QModbusReply *>(sender()); if (!reply) return; if (reply->error() == QModbusDevice::NoError) { const QModbusDataUnit unit = reply->result(); // Parse values (divide register value for real units) float pressure = unit.value(0) / 100.0f; // addr 0: pressure (MPa) float temperature = unit.value(1) / 10.0f; // addr 1: temperature (Β°C) int pumpStatus = unit.value(2); // addr 2: pump state // Update UI m_pressureLabel->setText( QString("%1 MPa").arg(pressure, 0, 'f', 2)); // Check alarm if (pressure > m_alarmThreshold) { triggerAlarm(pressure); } // Update trend chart (implemented next chapter) // updateChart(pressure); } else { statusBar()->showMessage( QString("Read failed: %1").arg(reply->errorString()), 2000); } reply->deleteLater(); }
Modbus reading flow:Modbus reading flow:
text readPressure() triggered by timer -> Build QModbusDataUnit ("read addresses 0-2") -> sendReadRequest() async send (UI not blocked) -> lower computer returns data -> onReadReady() triggered -> parse register values and update UI
πΌοΈ placeholder: Running app screenshot showing real-time pressure updates and status bar "connected to lower computer"placeholder: Running app screenshot showing real-time pressure updates and status bar "connected to lower computer"
Qt Charts provides professional chart components. Ask AI to initialize in constructor:Qt Charts provides professional chart components. Ask AI to initialize in constructor:
text Please help me initialize Qt Charts real-time line chart in MainWindow constructor: 1. Create QChart and QLineSeries 2. X axis uses QDateTimeAxis, showing latest 60 seconds 3. Y axis uses QValueAxis, range 0-3.0 MPa 4. Line color blue, width 2px 5. Place chart into QChartView and add to layout
Core code:Core code:
cpp // mainwindow.cpp - chart initialization void MainWindow::setupChart() { m_series = new QLineSeries(); m_series->setName("Pressure (MPa)"); m_series->setPen(QPen(QColor("#2196F3"), 2)); m_chart = new QChart(); m_chart->addSeries(m_series); m_chart->setTitle("Real-time Pressure Trend"); m_chart->setAnimationOptions(QChart::NoAnimation); // no animation for real-time data // X axis: time m_axisX = new QDateTimeAxis(); m_axisX->setFormat("HH:mm:ss"); m_axisX->setTitleText("Time"); m_chart->addAxis(m_axisX, Qt::AlignBottom); m_series->attachAxis(m_axisX); // Y axis: pressure m_axisY = new QValueAxis(); m_axisY->setRange(0, 3.0); m_axisY->setTitleText("Pressure (MPa)"); m_axisY->setLabelFormat("%.1f"); m_chart->addAxis(m_axisY, Qt::AlignLeft); m_series->attachAxis(m_axisY); // Create chart view QChartView *chartView = new QChartView(m_chart); chartView->setRenderHint(QPainter::Antialiasing); // Add to layout (assuming existing centralLayout) centralLayout->addWidget(chartView); }
Whenever a new pressure value is read, append one point and keep only latest 60 seconds:Whenever a new pressure value is read, append one point and keep only latest 60 seconds:
cpp // mainwindow.cpp - chart updates void MainWindow::updateChart(float pressure) { QDateTime now = QDateTime::currentDateTime(); // Append new point m_series->append(now.toMSecsSinceEpoch(), pressure); // Keep only latest 60s data QDateTime cutoff = now.addSecs(-60); while (m_series->count() > 0 && m_series->at(0).x() < cutoff.toMSecsSinceEpoch()) { m_series->remove(0); } // Update X axis range: always show latest 60s m_axisX->setRange(cutoff, now); }
Then call it in onReadReady():Then call it in onReadReady():
cpp // Add after pressure parsing in onReadReady(): updateChart(pressure);
Now run the program. You will see a blue line updating in real time, one point per second, always showing latest 60 seconds. If you modify register values in Modbus Slave manually, the line reflects changes immediately.Now run the program. You will see a blue line updating in real time, one point per second, always showing latest 60 seconds. If you modify register values in Modbus Slave manually, the line reflects changes immediately.
πΌοΈ placeholder: Real-time pressure trend screenshot showing scrolling blue line, time X-axis, pressure Y-axisplaceholder: Real-time pressure trend screenshot showing scrolling blue line, time X-axis, pressure Y-axis
> Performance tip: QChart::NoAnimation is important. Real-time data refresh every second; animations can cause UI lag. This is a common industrial HMI practice.> Performance tip: QChart::NoAnimation is important. Real-time data refresh every second; animations can cause UI lag. This is a common industrial HMI practice.
When pressure exceeds threshold, we need: red UI warning + popup alert + log record.When pressure exceeds threshold, we need: red UI warning + popup alert + log record.
cpp // mainwindow.cpp - alarm logic void MainWindow::triggerAlarm(float pressure) { // Turn UI red m_pressureLabel->setStyleSheet( "color: white; background-color: #F44336;" "font-size: 32px; padding: 10px; border-radius: 8px;"); // Status indicator red m_statusLight->setStyleSheet( "background-color: #F44336; border-radius: 12px;" "min-width: 24px; min-height: 24px;"); // Popup alarm (only first time crossing threshold to avoid repeated popups) static bool alarmActive = false; if (!alarmActive) { alarmActive = true; QMessageBox::warning(this, "Pressure Alarm", QString("Current pressure %1 MPa exceeds threshold %2 MPa!\nPlease check pump status immediately.") .arg(pressure, 0, 'f', 2) .arg(m_alarmThreshold, 0, 'f', 2)); } // Record to DB logAlarm(pressure, QString("Pressure over threshold: %1 MPa > %2 MPa") .arg(pressure, 0, 'f', 2) .arg(m_alarmThreshold, 0, 'f', 2)); // Reset when pressure returns to normal if (pressure <= m_alarmThreshold) { alarmActive = false; m_pressureLabel->setStyleSheet( "color: #2196F3; font-size: 32px; padding: 10px;"); m_statusLight->setStyleSheet( "background-color: #4CAF50; border-radius: 12px;" "min-width: 24px; min-height: 24px;"); } }
πΌοΈ placeholder: Over-threshold alarm screenshot showing red pressure background, red indicator, and alarm popupplaceholder: Over-threshold alarm screenshot showing red pressure background, red indicator, and alarm popup
Industrial systems must log all alarm events for traceability. We use SQLite:Industrial systems must log all alarm events for traceability. We use SQLite:
cpp // mainwindow.cpp - database initialization void MainWindow::setupDatabase() { m_db = QSqlDatabase::addDatabase("QSQLITE"); m_db.setDatabaseName("pump_alarm_log.db"); if (!m_db.open()) { qWarning() << "Cannot open database:" << m_db.lastError().text(); return; } // Create alarm table QSqlQuery query; query.exec( "CREATE TABLE IF NOT EXISTS alarm_log (" " id INTEGER PRIMARY KEY AUTOINCREMENT," " timestamp DATETIME DEFAULT CURRENT_TIMESTAMP," " pressure REAL," " message TEXT" ")" ); }
cpp // mainwindow.cpp - write logs void MainWindow::logAlarm(float pressure, const QString &message) { // Write to DB QSqlQuery query; query.prepare( "INSERT INTO alarm_log (pressure, message) VALUES (?, ?)"); query.addBindValue(pressure); query.addBindValue(message); query.exec(); // Update on-screen table int row = m_logTable->rowCount(); m_logTable->insertRow(row); m_logTable->setItem(row, 0, new QTableWidgetItem( QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss"))); m_logTable->setItem(row, 1, new QTableWidgetItem(QString::number(pressure, 'f', 2))); m_logTable->setItem(row, 2, new QTableWidgetItem(message)); // Auto-scroll to latest row m_logTable->scrollToBottom(); }
Log table has three columns: time, pressure value, and alarm message. Each alarm appends one row and is persisted to SQLite.Log table has three columns: time, pressure value, and alarm message. Each alarm appends one row and is persisted to SQLite.
πΌοΈ placeholder: Fault log table screenshot with multiple records including timestamp, pressure, and alarm messageplaceholder: Fault log table screenshot with multiple records including timestamp, pressure, and alarm message
Besides reading data, upper computer should control lower computer too. We do this by writing register values:Besides reading data, upper computer should control lower computer too. We do this by writing register values:
cpp // mainwindow.cpp - pump control void MainWindow::togglePump() { if (!m_modbusClient || m_modbusClient->state() != QModbusDevice::ConnectedState) return; m_pumpRunning = !m_pumpRunning; // Build write request: write 1 (start) or 0 (stop) to address 2 QModbusDataUnit writeUnit( QModbusDataUnit::HoldingRegisters, 2, 1); writeUnit.setValue(0, m_pumpRunning ? 1 : 0); if (auto *reply = m_modbusClient->sendWriteRequest(writeUnit, 1)) { connect(reply, &QModbusReply::finished, this, [this, reply]() { if (reply->error() == QModbusDevice::NoError) { m_pumpButton->setText(m_pumpRunning ? "Stop Pump" : "Start Pump"); m_pumpButton->setStyleSheet(m_pumpRunning ? "background-color: #F44336; color: white; padding: 12px;" : "background-color: #4CAF50; color: white; padding: 12px;"); statusBar()->showMessage( m_pumpRunning ? "Pump started" : "Pump stopped", 2000); } reply->deleteLater(); }); } }
In Modbus Slave, you will see address 2 switching between 0 and 1 as you click the button. This is the upper-computer "control" process.In Modbus Slave, you will see address 2 switching between 0 and 1 as you click the button. This is the upper-computer "control" process.
πΌοΈ placeholder: Pump start/stop button screenshot showing green "Start Pump" and red "Stop Pump" statesplaceholder: Pump start/stop button screenshot showing green "Start Pump" and red "Stop Pump" states
Qt provides official deployment tools to collect required dynamic libraries automatically.Qt provides official deployment tools to collect required dynamic libraries automatically.
Windows:Windows:
bash # Build Release first, then run in build directory: windeployqt PumpHMI.exe
windeployqt copies Qt DLLs, plugins, translation files, etc. next to the executable. That packaged folder can be sent directly.windeployqt copies Qt DLLs, plugins, translation files, etc. next to the executable. That packaged folder can be sent directly.
macOS:macOS:
bash macdeployqt PumpHMI.app -dmg
This generates a .dmg installer image.This generates a .dmg installer image.
If you want a professional setup wizard ("Next -> Next -> Finish"), use Qt Installer Framework:If you want a professional setup wizard ("Next -> Next -> Finish"), use Qt Installer Framework:
text Please help me create an installer for PumpHMI with Qt Installer Framework: 1. Create installer directory structure (config, packages) 2. Configure config.xml (installer name, version, target directory) 3. Put windeployqt output files into packages/com.example.pumphmi/data/ 4. Run binarycreator to generate installer
πΌοΈ placeholder: PumpHMI setup wizard screenshot showing install path and progressplaceholder: PumpHMI setup wizard screenshot showing install path and progress
Congratulations! You have built an industrial-grade pump monitoring HMI system from scratch. Recap:Congratulations! You have built an industrial-grade pump monitoring HMI system from scratch. Recap:
QModbusTcpClientBuilt upper-lower communication using Qt QModbusTcpClientThe whole process used no real industrial hardware, but the architecture and functions match real factory HMI systems. If you replace Modbus Slave with a real PLC, this app can be used in production scenarios directly.The whole process used no real industrial hardware, but the architecture and functions match real factory HMI systems. If you replace Modbus Slave with a real PLC, this app can be used in production scenarios directly.
Advanced directions:Advanced directions:
Use code to protect every device in industrial operations.Use code to protect every device in industrial operations.