Building a Modbus RTU Master and Slave with Qt 5 and libmodbus on Windows
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
-
Create a new Qt Widgets Application.
-
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.
-
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.