Build a Qt Modbus Master with libmodbus (QModMaster-style)
This is a real scenario from qtcentre.org (thread 61749, “QT and Libmodbus”): a developer read QModMaster’s source code and wanted to build their own Qt GUI Modbus master application on top of libmodbus — targeting a Raspberry Pi. This page sketches the same architecture QModMaster uses, with a minimal but complete example: connect, read holding registers, and deliver the values to the GUI thread.
The architecture: GUI thread + blocking worker
libmodbus calls such as modbus_read_registers() are synchronous — they block until the reply arrives or the timeout fires. QModMaster-style applications therefore do all Modbus I/O in a worker thread and push results to the GUI through signals and slots. The one rule that matters: never call libmodbus from the GUI thread, or your interface will freeze on every poll.
Minimal example: connect → read → emit
One file, Qt5, C++17. The poller lives in a QThread; a QTimer in that same thread triggers each poll; results travel back via signals:
// Build with: QT += widgets and LIBS += -lmodbus
// Debian / Raspberry Pi OS: sudo apt install qtbase5-dev libmodbus-dev
#include <QCoreApplication>
#include <QThread>
#include <QTimer>
#include <QVector>
#include <QDebug>
#include <cerrno>
#include <modbus/modbus.h>
class ModbusPoller : public QObject {
Q_OBJECT
public:
explicit ModbusPoller(QObject *parent = nullptr) : QObject(parent) {}
~ModbusPoller() override {
if (ctx) { modbus_close(ctx); modbus_free(ctx); }
}
bool openRtu(const char *device, int slaveId) {
ctx = modbus_new_rtu(device, 9600, 'N', 8, 1);
if (!ctx) return false;
modbus_set_slave(ctx, slaveId);
struct timeval timeout{2, 0}; // 2 s response timeout
modbus_set_response_timeout(ctx, &timeout);
if (modbus_connect(ctx) == -1) {
qWarning() << "connect failed:" << modbus_strerror(errno);
modbus_free(ctx);
ctx = nullptr;
return false;
}
return true;
}
public slots:
void poll() { // runs in the worker thread
uint16_t regs[10] = {};
int rc = modbus_read_registers(ctx, 0, 10, regs); // 0-based PDU address
if (rc == -1) {
emit failed(QString::fromUtf8(modbus_strerror(errno)));
return;
}
QVector<quint16> out;
out.reserve(rc);
for (int i = 0; i < rc; ++i) out.append(regs[i]);
emit valuesReady(out);
}
signals:
void valuesReady(const QVector<quint16> &values);
void failed(const QString &message);
private:
modbus_t *ctx = nullptr;
};
int main(int argc, char *argv[]) {
QCoreApplication app(argc, argv);
auto *thread = new QThread;
auto *poller = new ModbusPoller;
poller->moveToThread(thread);
auto *timer = new QTimer; // no parent yet: move it before wiring
timer->setInterval(1000);
timer->moveToThread(thread);
QObject::connect(thread, &QThread::started, [=]() {
if (!poller->openRtu("/dev/ttyUSB0", 1)) {
qWarning() << "could not open Modbus link";
app.quit();
}
});
QObject::connect(timer, &QTimer::timeout, poller, &ModbusPoller::poll);
QObject::connect(thread, &QThread::started,
timer, static_cast<void (QTimer::*)()>(&QTimer::start));
QObject::connect(poller, &ModbusPoller::valuesReady,
[](const QVector<quint16> &v) { qDebug() << "registers:" << v; });
QObject::connect(poller, &ModbusPoller::failed,
[](const QString &m) { qWarning() << "poll failed:" << m; });
QObject::connect(&app, &QCoreApplication::aboutToQuit, thread, &QThread::quit);
QObject::connect(thread, &QThread::finished, poller, &QObject::deleteLater);
QObject::connect(thread, &QThread::finished, timer, &QObject::deleteLater);
QObject::connect(thread, &QThread::finished, thread, &QObject::deleteLater);
thread->start();
return app.exec();
}
// If everything lives in one .cpp file, add at the end:
// #include "main.moc"
For a TCP device instead of serial, swap modbus_new_rtu() for modbus_new_tcp("192.168.1.100", 502) — everything else stays identical.
Build notes
- Debian / Raspberry Pi OS / Ubuntu:
sudo apt install qtbase5-dev libmodbus-dev - .pro file: add
QT += widgetsandLIBS += -lmodbus - Qt6: the same code works; install the Qt6 base dev package instead of
qtbase5-devand adjust theQT +=line for your Qt6 setup - Windows: build libmodbus first (see the libmodbus documentation), then link your Qt project against it the same way
Gotchas QModMaster already solved for you
- Timeouts: always set a response timeout (
modbus_set_response_timeout) — without one, a dead slave hangs your worker thread. - Slave ID: call
modbus_set_slave()beforemodbus_connect(); the default unit ID is not always what your device expects. - Address base: libmodbus uses 0-based PDU addresses — the “40001” in a device manual is address
0here. See our register address offset guide for the full story. - Error messages:
modbus_strerror(errno)turns a failed call into a human-readable reason — log it, don’t swallow it.
