GDT + HMI Integration: Real-Time Weight Display with Modbus Communication
Introduction
In industrial weighing systems, real-time weight display and recording are critical requirements. Our GDT Digital Transmitter communicates with HMI Industrial Display Solutions via Modbus TCP or Modbus RTU protocol to meet these needs.
In this article, we examine in detail how to read, display, and record weight data from GDT on an HMI panel.
System Architecture
┌──────────────────────────────────────────────────────┐
│ Loadcell Platform (4-channel) │
│ │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │LC #1 │ │LC #2 │ │LC #3 │ │LC #4 │ │
│ │2mV/V │ │2mV/V │ │2mV/V │ │2mV/V │ │
│ └───┬────┘ └───┬────┘ └───┬────┘ └───┬────┘ │
│ │ │ │ │ │
└──────┼───────────┼───────────┼───────────┼────────┘
│ │ │ │
└───────────┴───────────┴───────────┘
│
┌──────▼──────┐
│ GDT-410M │ ← 24-bit ADC, Modbus TCP
│ Transmitter│
└──────┬──────┘
│ Ethernet (Modbus TCP)
│ Port 502
┌──────▼──────┐
│ HMI Panel │ ← 10.1" Touchscreen
│ Qt/QML UI │ Embedded Linux
└─────────────┘
GDT Modbus Register Map
| Register | Address | Type | Unit | Description |
|---|---|---|---|---|
| Gross Weight | 40001-40002 | Float32 | kg | Total weight (4 channels) |
| Net Weight | 40003-40004 | Float32 | kg | Gross - Tare |
| Tare Value | 40005-40006 | Float32 | kg | Container weight |
| Channel 1 | 40011-40012 | Float32 | mV/V | Loadcell 1 raw data |
| Channel 2 | 40013-40014 | Float32 | mV/V | Loadcell 2 raw data |
| Channel 3 | 40015-40016 | Float32 | mV/V | Loadcell 3 raw data |
| Channel 4 | 40017-40018 | Float32 | mV/V | Loadcell 4 raw data |
| Status | 40101 | Uint16 | - | Device status |
| Zero Command | 40201 | Uint16 | - | Write 1 = Zero |
| Tare Command | 40202 | Uint16 | - | Write 1 = Tare |
HMI Application: Qt + QML Interface
C++ Modbus Backend
ModbusReader.h
#ifndef MODBUSREADER_H
#define MODBUSREADER_H
#include <QObject>
#include <QModbusTcpClient>
#include <QTimer>
class ModbusReader : public QObject
{
Q_OBJECT
Q_PROPERTY(float grossWeight READ grossWeight NOTIFY weightChanged)
Q_PROPERTY(float netWeight READ netWeight NOTIFY weightChanged)
Q_PROPERTY(float tare READ tare NOTIFY weightChanged)
Q_PROPERTY(bool connected READ connected NOTIFY connectionChanged)
public:
explicit ModbusReader(QObject *parent = nullptr);
~ModbusReader();
float grossWeight() const { return m_grossWeight; }
float netWeight() const { return m_netWeight; }
float tare() const { return m_tare; }
bool connected() const { return m_connected; }
Q_INVOKABLE void connectToDevice(const QString &ip, int port = 502);
Q_INVOKABLE void zero();
Q_INVOKABLE void setTare();
signals:
void weightChanged();
void connectionChanged();
void errorOccurred(const QString &error);
private slots:
void onStateChanged(QModbusDevice::State state);
void onReadReady();
void readRegisters();
private:
QModbusTcpClient *m_modbusClient;
QTimer *m_readTimer;
float m_grossWeight = 0.0f;
float m_netWeight = 0.0f;
float m_tare = 0.0f;
bool m_connected = false;
float registersToFloat(quint16 reg1, quint16 reg2);
};
#endif
ModbusReader.cpp
#include "ModbusReader.h"
#include <QModbusDataUnit>
#include <QDebug>
ModbusReader::ModbusReader(QObject *parent)
: QObject(parent)
{
m_modbusClient = new QModbusTcpClient(this);
connect(m_modbusClient, &QModbusClient::stateChanged,
this, &ModbusReader::onStateChanged);
// Read every 200ms
m_readTimer = new QTimer(this);
connect(m_readTimer, &QTimer::timeout, this, &ModbusReader::readRegisters);
}
void ModbusReader::connectToDevice(const QString &ip, int port)
{
m_modbusClient->setConnectionParameter(QModbusDevice::NetworkAddressParameter, ip);
m_modbusClient->setConnectionParameter(QModbusDevice::NetworkPortParameter, port);
m_modbusClient->setTimeout(1000);
m_modbusClient->setNumberOfRetries(3);
m_modbusClient->connectDevice();
}
void ModbusReader::onStateChanged(QModbusDevice::State state)
{
m_connected = (state == QModbusDevice::ConnectedState);
emit connectionChanged();
if (m_connected) {
qDebug() << "✓ Connected to GDT";
m_readTimer->start(200); // 5 Hz update rate
} else {
m_readTimer->stop();
}
}
void ModbusReader::readRegisters()
{
if (!m_modbusClient || !m_connected) return;
// Read holding registers 40001-40006 (6 registers = 3 floats)
QModbusDataUnit readUnit(QModbusDataUnit::HoldingRegisters, 0, 6);
auto *reply = m_modbusClient->sendReadRequest(readUnit, 1); // Unit ID = 1
if (reply) {
if (!reply->isFinished()) {
connect(reply, &QModbusReply::finished, this, &ModbusReader::onReadReady);
} else {
delete reply;
}
}
}
void ModbusReader::onReadReady()
{
auto *reply = qobject_cast<QModbusReply *>(sender());
if (!reply) return;
if (reply->error() == QModbusDevice::NoError) {
const QModbusDataUnit unit = reply->result();
// Parse Float32 values (2 registers each)
m_grossWeight = registersToFloat(unit.value(0), unit.value(1));
m_netWeight = registersToFloat(unit.value(2), unit.value(3));
m_tare = registersToFloat(unit.value(4), unit.value(5));
emit weightChanged();
} else {
emit errorOccurred(reply->errorString());
}
reply->deleteLater();
}
float ModbusReader::registersToFloat(quint16 reg1, quint16 reg2)
{
// IEEE 754 big-endian conversion
quint32 raw = (static_cast<quint32>(reg1) << 16) | reg2;
float value;
memcpy(&value, &raw, sizeof(float));
return value;
}
void ModbusReader::zero()
{
// Write 1 to register 40201
QModbusDataUnit writeUnit(QModbusDataUnit::HoldingRegisters, 200, 1);
writeUnit.setValue(0, 1);
m_modbusClient->sendWriteRequest(writeUnit, 1);
qDebug() << "Zero command sent";
}
void ModbusReader::setTare()
{
// Write 1 to register 40202
QModbusDataUnit writeUnit(QModbusDataUnit::HoldingRegisters, 201, 1);
writeUnit.setValue(0, 1);
m_modbusClient->sendWriteRequest(writeUnit, 1);
qDebug() << "Tare command sent";
}
QML User Interface
main.qml
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtCharts 2.15
import com.amazeng 1.0 // ModbusReader
ApplicationWindow {
visible: true
width: 1024
height: 600
title: "GDT Weighing System"
ModbusReader {
id: modbusReader
Component.onCompleted: {
connectToDevice("192.168.1.100", 502)
}
}
Rectangle {
anchors.fill: parent
color: "#1E1E1E"
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 20
// Header
Rectangle {
Layout.fillWidth: true
height: 60
color: "#2D2D2D"
radius: 8
RowLayout {
anchors.fill: parent
anchors.margins: 10
Text {
text: "GDT-410M Weighing Station"
font.pixelSize: 28
font.bold: true
color: "#FFFFFF"
}
Item { Layout.fillWidth: true }
Rectangle {
width: 20
height: 20
radius: 10
color: modbusReader.connected ? "#2ECC71" : "#E74C3C"
}
Text {
text: modbusReader.connected ? "Connected" : "Disconnected"
font.pixelSize: 16
color: "#FFFFFF"
}
}
}
// Main weight display
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 250
color: "#27AE60"
radius: 12
ColumnLayout {
anchors.centerIn: parent
spacing: 10
Text {
text: "NET WEIGHT"
font.pixelSize: 24
color: "white"
Layout.alignment: Qt.AlignHCenter
}
Text {
text: modbusReader.netWeight.toFixed(2)
font.pixelSize: 96
font.bold: true
color: "white"
Layout.alignment: Qt.AlignHCenter
}
Text {
text: "kg"
font.pixelSize: 36
color: "white"
Layout.alignment: Qt.AlignHCenter
}
}
}
// Gross and Tare
RowLayout {
Layout.fillWidth: true
spacing: 20
Rectangle {
Layout.fillWidth: true
height: 120
color: "#34495E"
radius: 8
ColumnLayout {
anchors.centerIn: parent
spacing: 5
Text {
text: "Gross"
font.pixelSize: 18
color: "#BDC3C7"
Layout.alignment: Qt.AlignHCenter
}
Text {
text: modbusReader.grossWeight.toFixed(2) + " kg"
font.pixelSize: 32
color: "white"
Layout.alignment: Qt.AlignHCenter
}
}
}
Rectangle {
Layout.fillWidth: true
height: 120
color: "#34495E"
radius: 8
ColumnLayout {
anchors.centerIn: parent
spacing: 5
Text {
text: "Tare"
font.pixelSize: 18
color: "#BDC3C7"
Layout.alignment: Qt.AlignHCenter
}
Text {
text: modbusReader.tare.toFixed(2) + " kg"
font.pixelSize: 32
color: "white"
Layout.alignment: Qt.AlignHCenter
}
}
}
}
// Control buttons
RowLayout {
Layout.fillWidth: true
spacing: 20
Button {
text: "ZERO"
Layout.fillWidth: true
Layout.preferredHeight: 80
font.pixelSize: 24
onClicked: modbusReader.zero()
background: Rectangle {
color: parent.pressed ? "#E67E22" : "#F39C12"
radius: 8
}
contentItem: Text {
text: parent.text
font: parent.font
color: "white"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
Button {
text: "TARE"
Layout.fillWidth: true
Layout.preferredHeight: 80
font.pixelSize: 24
onClicked: modbusReader.setTare()
background: Rectangle {
color: parent.pressed ? "#2980B9" : "#3498DB"
radius: 8
}
contentItem: Text {
text: parent.text
font: parent.font
color: "white"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
}
}
}
}
Real-World Application: Automatic Packaging Line
Scenario
Products are weighed and automatically recorded in a packaging line.
Requirements:
- Target Weight: 25 kg ± 0.1 kg
- Automatic Recording: Product over threshold → Save to database
- Tare: Automatic tare after every 10 weighing operations
Implementation
Data Recording (SQLite):
// DatabaseManager.cpp
void DatabaseManager::saveWeighing(float weight, const QString &batchCode)
{
QSqlQuery query(m_db);
query.prepare("INSERT INTO weighings (timestamp, weight, batch_code) "
"VALUES (datetime('now'), :weight, :batch)");
query.bindValue(":weight", weight);
query.bindValue(":batch", batchCode);
query.exec();
}
GDT + HMI + AWS Integration
Our GDT + HMI combination can also send data to the cloud. Details in our Cloud & IoT Data Collection solution:
GDT (Modbus) → HMI (Gateway) → MQTT → AWS IoT Core
HMI panel:
- Displays weight locally (Qt/QML interface)
- Publishes to AWS IoT Core (MQTT client)
- Stores in database (SQLite/PostgreSQL)
Conclusion
Our GDT Digital Transmitter and HMI Industrial Display Solutions integration provides:
✅ Real-time weight monitoring with 200ms update rate
✅ Modern touchscreen interface with Qt/QML
✅ Flexible communication with Modbus TCP/RTU
✅ Cloud integration ready (AWS, Azure)