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 += widgets and LIBS += -lmodbus
  • Qt6: the same code works; install the Qt6 base dev package instead of qtbase5-dev and adjust the QT += 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() before modbus_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 0 here. 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.

Related Guides

Similar Posts