Fading Coder

An Old Coder’s Final Dance

Home > Tech > Content

Building a Modbus RTU Master and Slave with Qt 5 and libmodbus on Windows

Tech 1

This guide shows how to integrate libmodbus with a Qt 5 application on Windows to implement both a Modbus RTU master (client) and a Modbus RTU slave (server). The examples use a simple Widgets-based UI, periodically enumerate serial ports, and demonstrate reading holding registers from multiple slave IDs.

Environment: Windows 10, Qt 5.12

Project setupp

  1. Create a new Qt Widgets Application.

  2. Add libmodbus to the project (either as prebuilt library or compile the sources in to your project). If building from source, include the headers and C files in your project and ensure they are compiled with your toolchain.

  3. Update the .pro file:

QT += widgets serialport
CONFIG += c++11

# If using libmodbus sources directly:
INCLUDEPATH += $$PWD/third_party/libmodbus
SOURCES += \
    $$PWD/third_party/libmodbus/modbus.c \
    $$PWD/third_party/libmodbus/modbus-rtu.c \
    $$PWD/third_party/libmodbus/modbus-data.c \
    $$PWD/third_party/libmodbus/modbus-errno.c \
    $$PWD/third_party/libmodbus/modbus-private.c

# Or link against a prebuilt libmodbus library as appropriate for your toolchain

The UI in the snippets assumes these widgets exist: comboBox_name (serial port), comboBox_baud (baud), lineEdit (number of slaves), pushButton (open/close), pushButton_2 (clear log), textEdit (log view).

Modbus RTU master implementation

Header (mainwindow.h):

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QSerialPortInfo>
#include <QTimer>
#include <QStringList>
#include "libmodbus/modbus.h"

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void on_pushButton_clicked(bool checked);      // Open/close master
    void on_pushButton_2_clicked();                // Clear log
    void refreshPorts();
    void pollSlaves();

private:
    Ui::MainWindow *ui;

    QTimer portScanTimer;                          // Enumerate ports periodically
    QTimer pollTimer;                              // Periodic Modbus polling

    modbus_t *ctx = nullptr;                       // libmodbus context

    int slaveCount = 0;                            // Total slaves to poll
    int currentSlave = 1;                          // Round-robin slave address

    uint16_t readBuf[16] = {0};                    // Temporary read buffer
};

#endif // MAINWINDOW_H

Source (mainwindow.cpp):

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QTextCursor>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // Scan serial ports once per second
    connect(&portScanTimer, &QTimer::timeout, this, &MainWindow::refreshPorts);
    portScanTimer.start(1000);

    // Polling timer for master
    connect(&pollTimer, &QTimer::timeout, this, &MainWindow::pollSlaves);
}

MainWindow::~MainWindow()
{
    pollTimer.stop();
    if (ctx) {
        modbus_close(ctx);
        modbus_free(ctx);
        ctx = nullptr;
    }
    delete ui;
}

void MainWindow::on_pushButton_clicked(bool checked)
{
    if (checked) {
        if (ui->comboBox_name->currentText().isEmpty()) {
            ui->textEdit->append("Select a serial port");
            ui->pushButton->setChecked(false);
            return;
        }

        slaveCount = ui->lineEdit->text().toInt();
        if (slaveCount <= 0) {
            ui->textEdit->append("Set a positive number of slave IDs");
            ui->pushButton->setChecked(false);
            return;
        }

        const QByteArray portName = ui->comboBox_name->currentText().toLatin1();
        const int baud = ui->comboBox_baud->currentText().toInt();

        ctx = modbus_new_rtu(portName.constData(), baud, 'N', 8, 1);
        if (!ctx) {
            ui->textEdit->append("modbus_new_rtu failed");
            ui->pushButton->setChecked(false);
            return;
        }

        // Default to slave 1; will change in pollSlaves
        modbus_set_slave(ctx, 1);

        // Timeout: 1000 ms
        struct timeval tv;
        tv.tv_sec = 1;
        tv.tv_usec = 0;
        modbus_set_response_timeout(ctx, tv.tv_sec, tv.tv_usec);
        modbus_set_error_recovery(ctx, MODBUS_ERROR_RECOVERY_LINK);

        if (modbus_connect(ctx) == -1) {
            ui->textEdit->append(QString("modbus_connect failed: %1").arg(modbus_strerror(errno)));
            modbus_free(ctx);
            ctx = nullptr;
            ui->pushButton->setChecked(false);
            return;
        }

        currentSlave = 1;
        pollTimer.start(300); // Poll every 300 ms
        ui->pushButton->setText("close");
    } else {
        pollTimer.stop();
        if (ctx) {
            modbus_close(ctx);
            modbus_free(ctx);
            ctx = nullptr;
        }
        ui->pushButton->setText("open");
    }
}

void MainWindow::pollSlaves()
{
    if (!ctx || slaveCount <= 0)
        return;

    // Set current slave and read 5 holding registers starting at 0
    modbus_set_slave(ctx, currentSlave);

    int rc = modbus_read_registers(ctx, 0, 5, readBuf);
    if (rc == 5) {
        ui->textEdit->append(QString("ID %1: %2 %3 %4 %5 %6")
            .arg(currentSlave)
            .arg(readBuf[0])
            .arg(readBuf[1])
            .arg(readBuf[2])
            .arg(readBuf[3])
            .arg(readBuf[4]));
    } else {
        ui->textEdit->append(QString("ID %1: read error (%2)")
            .arg(currentSlave)
            .arg(modbus_strerror(errno)));
    }

    ui->textEdit->moveCursor(QTextCursor::End);

    // Move to next slave in [1..slaveCount]
    currentSlave = (currentSlave % slaveCount) + 1;
}

void MainWindow::refreshPorts()
{
    QStringList ports;
    const auto infos = QSerialPortInfo::availablePorts();
    for (const QSerialPortInfo &info : infos)
        ports << info.portName();

    // Refresh combobox if changed
    const QString current = ui->comboBox_name->currentText();
    if (ui->comboBox_name->count() != ports.size() || !ports.contains(current)) {
        ui->comboBox_name->clear();
        ui->comboBox_name->addItems(ports);
    }
}

void MainWindow::on_pushButton_2_clicked()
{
    ui->textEdit->clear();
}

Note: Any blocking I/O in the GUI thread can cause visible freezes. If you observe UI stalls, move the polling loop to a worker thread and communicate results via signals.

Modbus RTU slave implementation

Header (mainwindow.h):

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QSerialPortInfo>
#include <QTimer>
#include <QStringList>
#include "libmodbus/modbus.h"

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void on_pushButton_clicked(bool checked);      // Start/stop slave
    void on_pushButton_2_clicked();                // Clear log
    void refreshPorts();
    void serviceRequests();                        // Process incoming RTU frames

private:
    Ui::MainWindow *ui;

    QTimer portScanTimer;
    QTimer serviceTimer;

    modbus_t *ctx = nullptr;                       // libmodbus RTU context
    modbus_mapping_t *map = nullptr;               // Register map
};

#endif // MAINWINDOW_H

Source (mainwindow.cpp):

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QTextCursor>
#include <errno.h>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    connect(&portScanTimer, &QTimer::timeout, this, &MainWindow::refreshPorts);
    portScanTimer.start(1000);

    connect(&serviceTimer, &QTimer::timeout, this, &MainWindow::serviceRequests);
}

MainWindow::~MainWindow()
{
    serviceTimer.stop();
    if (ctx) {
        modbus_close(ctx);
        modbus_free(ctx);
        ctx = nullptr;
    }
    if (map) {
        modbus_mapping_free(map);
        map = nullptr;
    }
    delete ui;
}

void MainWindow::on_pushButton_clicked(bool checked)
{
    if (checked) {
        if (ui->comboBox_name->currentText().isEmpty()) {
            ui->textEdit->append("Select a serial port");
            ui->pushButton->setChecked(false);
            return;
        }

        const QByteArray portName = ui->comboBox_name->currentText().toLatin1();
        const int baud = ui->comboBox_baud->currentText().toInt();

        ctx = modbus_new_rtu(portName.constData(), baud, 'N', 8, 1);
        if (!ctx) {
            ui->textEdit->append("modbus_new_rtu failed");
            ui->pushButton->setChecked(false);
            return;
        }

        // Configure this device as slave ID 1
        modbus_set_slave(ctx, 1);

        if (modbus_connect(ctx) == -1) {
            ui->textEdit->append(QString("modbus_connect failed: %1").arg(modbus_strerror(errno)));
            modbus_free(ctx);
            ctx = nullptr;
            ui->pushButton->setChecked(false);
            return;
        }

        // Allocate full map (bits/regs). Adjust sizes if desired.
        map = modbus_mapping_new(MODBUS_MAX_READ_BITS, MODBUS_MAX_READ_BITS,
                                 MODBUS_MAX_READ_REGISTERS, MODBUS_MAX_READ_REGISTERS);
        if (!map) {
            ui->textEdit->append(QString("modbus_mapping_new failed: %1").arg(modbus_strerror(errno)));
            modbus_close(ctx);
            modbus_free(ctx);
            ctx = nullptr;
            ui->pushButton->setChecked(false);
            return;
        }

        // Initialize first five holding registers (address 0..4)
        map->tab_registers[0] = 1;
        map->tab_registers[1] = 2;
        map->tab_registers[2] = 3;
        map->tab_registers[3] = 4;
        map->tab_registers[4] = 5;

        serviceTimer.start(100); // Service loop period
        ui->pushButton->setText("close");
    } else {
        serviceTimer.stop();
        if (ctx) {
            modbus_close(ctx);
            modbus_free(ctx);
            ctx = nullptr;
        }
        if (map) {
            modbus_mapping_free(map);
            map = nullptr;
        }
        ui->pushButton->setText("open");
    }
}

void MainWindow::serviceRequests()
{
    if (!ctx || !map)
        return;

    uint8_t adu[MODBUS_RTU_MAX_ADU_LENGTH];

    // modbus_receive blocks until a frame is available or timeout occurs
    int rc = modbus_receive(ctx, adu);
    if (rc > 0) {
        modbus_reply(ctx, adu, rc, map);
    }
}

void MainWindow::refreshPorts()
{
    QStringList ports;
    const auto infos = QSerialPortInfo::availablePorts();
    for (const QSerialPortInfo &info : infos)
        ports << info.portName();

    const QString current = ui->comboBox_name->currentText();
    if (ui->comboBox_name->count() != ports.size() || !ports.contains(current)) {
        ui->comboBox_name->clear();
        ui->comboBox_name->addItems(ports);
    }
}

void MainWindow::on_pushButton_2_clicked()
{
    ui->textEdit->clear();
}

When testing, configure the master to read five holding registers starting at adress 0 from slave ID 1. The provided slave initializes thoce registers to 1, 2, 3, 4, 5. If you need to simulate multiple slaves, run multiple slave instances with different IDs or use an external Modbus slave simulator.

For production, consider moving modbus_receive into a dedicated worker thread to avoid blocking the GUI thread during serial I/O and timeouts.

Tags: qt5libmodbus

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.