Skip to content
Start free evaluation

Use the SDK from Qt and MFC

The Modbus SDK creates no threads, and a client request blocks the calling thread until the response or timeout_ms. In a GUI application, follow these rules:

  • Call an endpoint only from a worker thread. Calling connect or request on the GUI thread freezes the window meanwhile.
  • A server or gateway answers requests only while a worker thread repeats {"action":"poll"} at a short interval, such as every 5 ms.
  • Hand results to the GUI thread through queued signals or posted messages.
  • The library serializes calls on one endpoint, but one worker thread per endpoint keeps the design simple.

QtEndpoint takes a QJsonObject command and returns the reply’s result object as a QJsonObject; failures throw std::runtime_error. The header is bindings/qt/PdcommModbusQt.hpp in the SDK. It uses QtCore only and creates no signals or threads of its own.

This example puts a TCP server and a client on their own worker threads. The server worker sends poll every 5 ms; the client worker runs the blocking connect and request. The main thread plays the GUI thread: it only queues commands and receives results as queued calls.

examples/qt/modbus.cpp
// Modbus TCP server and client on Qt worker threads (QtCore only).
//
// Each endpoint is used only from its own worker thread. The server worker
// sends {"action":"poll"} every 5 ms, because the SDK has no background
// thread; the client worker runs the blocking connect and request commands.
// The main thread, which stands in for a GUI thread, only queues commands to
// the workers and receives their results as queued calls, so it never blocks.
//
// With --tls <directory> both endpoints use Modbus/TCP Security with the files
// of the Modbus/TCP Security tutorial in that directory: ca.pem,
// server-chain.pem and server.key, operator-chain.pem and operator.key.
#include <PdcommModbusQt.hpp>
#include <QCoreApplication>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QStringList>
#include <QThread>
#include <QTimer>
#include <cstdio>
#include <functional>
#include <memory>
#include <stdexcept>
namespace {
using Endpoint = pdcomm::modbus::QtEndpoint;
using Done = std::function<void(const QJsonObject &result, const QString &error)>;
// A worker thread that owns one endpoint.
class Worker {
public:
Worker() {
context_->moveToThread(&thread_);
// The context and its poll timer are deleted on the worker thread.
QObject::connect(&thread_, &QThread::finished, context_, &QObject::deleteLater);
thread_.start();
}
~Worker() {
thread_.quit();
thread_.wait();
}
// Runs one command on the worker thread and hands the result to `done` on
// the main thread.
void call(const QJsonObject &command, Done done) {
QMetaObject::invokeMethod(context_, [this, command, done] {
QJsonObject result;
QString error;
try {
result = endpoint_.call(command);
} catch (const std::exception &failure) {
error = QString::fromUtf8(failure.what());
}
QMetaObject::invokeMethod(qApp, [done, result, error] { done(result, error); },
Qt::QueuedConnection);
}, Qt::QueuedConnection);
}
// Serves requests: polls the endpoint every 5 ms on the worker thread.
void startPolling() {
QMetaObject::invokeMethod(context_, [this] {
auto *timer = new QTimer(context_);
QObject::connect(timer, &QTimer::timeout, context_, [this, timer] {
try {
endpoint_.call(QJsonObject{{"action", "poll"}});
} catch (const std::exception &failure) {
timer->stop();
std::fprintf(stderr, "Poll failed: %s\n", failure.what());
}
});
timer->start(5);
}, Qt::QueuedConnection);
}
private:
QThread thread_;
QObject *context_ = new QObject;
Endpoint endpoint_;
};
// Returns null (plain Modbus TCP) without a certificate directory.
QJsonValue security(const QString &directory, const QString &identity, bool server) {
if (directory.isEmpty())
return QJsonValue();
QJsonObject value{{"mode", "encrypted"},
{"trust", directory + "/ca.pem"},
{"chain", directory + "/" + identity + "-chain.pem"},
{"private_key", directory + "/" + identity + ".key"}};
if (server) {
// Clients with the "operator" role may use functions 03 and 06 only.
value.insert("roles", QJsonObject{{"operator", QJsonArray{3, 6}}});
value.insert("null_role", QJsonArray{});
}
return value;
}
QJsonObject tcp(const char *role, const QString &address, const QJsonValue &security) {
return {{"transport", "tcp"}, {"role", role}, {"address", address},
{"port", ""}, {"unit", 1}, {"baud", 19200},
{"parity", "even"}, {"stop_bits", 1}, {"timeout_ms", 1000},
{"security", security}};
}
void print(const char *label, const QJsonObject &result) {
std::printf("%s %s\n", label,
QJsonDocument(result).toJson(QJsonDocument::Compact).constData());
}
} // namespace
int main(int argc, char **argv) {
QCoreApplication application(argc, argv);
const QStringList arguments = application.arguments();
const bool tls = arguments.size() == 3 && arguments.at(1) == "--tls";
if (arguments.size() != 1 && !tls) {
std::fprintf(stderr, "Usage: %s [--tls <certificate directory>]\n", argv[0]);
return 2;
}
const QString certificates = tls ? arguments.at(2) : QString();
Worker server;
Worker client;
int exitCode = 1;
auto fail = [&exitCode](const QString &error) {
std::fprintf(stderr, "Command error: %s\n", qPrintable(error));
exitCode = 1;
QCoreApplication::quit();
};
// Each step runs after the previous result arrives on the main thread.
auto readBack = [&] {
client.call({{"action", "request"}, {"pdu", "0300000001"}},
[&](const QJsonObject &result, const QString &error) {
if (!error.isEmpty())
return fail(error);
print("read", result);
exitCode = result.value("response").toArray() ==
QJsonArray{3, 2, 0, 42}
? 0
: 1;
QCoreApplication::quit();
});
};
auto write = [&] {
client.call({{"action", "request"}, {"pdu", "060000002A"}},
[&](const QJsonObject &result, const QString &error) {
if (!error.isEmpty())
return fail(error);
print("write", result);
readBack();
});
};
auto connectClient = [&](const QString &address) {
client.call({{"action", "configure"},
{"config", tcp("client", address, security(certificates, "operator", false))}},
[&](const QJsonObject &, const QString &error) {
if (!error.isEmpty())
return fail(error);
client.call({{"action", "connect"}},
[&](const QJsonObject &, const QString &error) {
if (!error.isEmpty())
return fail(error);
write();
});
});
};
// Port 0 lets the system choose a free port; connect reports the address.
server.call({{"action", "configure"},
{"config", tcp("server", "127.0.0.1:0", security(certificates, "server", true))}},
[&](const QJsonObject &, const QString &error) {
if (!error.isEmpty())
return fail(error);
server.call({{"action", "connect"}},
[&](const QJsonObject &state, const QString &error) {
if (!error.isEmpty())
return fail(error);
const auto address = state.value("config")
.toObject()
.value("address")
.toString();
std::printf("server listening on %s\n",
qPrintable(address));
server.startPolling();
connectClient(address);
});
});
QTimer::singleShot(10000, &application, [&] { fail("No result within 10 s"); });
application.exec();
return exitCode;
}

To build it against the SDK, put this CMakeLists.txt next to the example file:

CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(modbus_qt_example LANGUAGES CXX)
set(PDCOMM_MODBUS_SDK "" CACHE PATH "Installed Modbus SDK root")
find_package(Qt6 REQUIRED COMPONENTS Core)
find_library(MODBUS_LIBRARY NAMES pdcomm_modbus_ffi
PATHS "${PDCOMM_MODBUS_SDK}/lib" NO_DEFAULT_PATH REQUIRED)
add_executable(modbus-qt modbus.cpp)
target_compile_features(modbus-qt PRIVATE cxx_std_17)
target_include_directories(modbus-qt PRIVATE
"${PDCOMM_MODBUS_SDK}/include" "${PDCOMM_MODBUS_SDK}/bindings/qt")
target_link_libraries(modbus-qt PRIVATE "${MODBUS_LIBRARY}" Qt6::Core)
set_target_properties(modbus-qt PROPERTIES BUILD_RPATH "${PDCOMM_MODBUS_SDK}/lib")
터미널 창
cmake -S qt-example -B qt-build -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=/path/to/Qt -DPDCOMM_MODBUS_SDK="$MODBUS_SDK"
cmake --build qt-build
./qt-build/modbus-qt
server listening on 127.0.0.1:54095
write {"response":[6,0,0,0,42],"source":"wire","status":"success"}
read {"response":[3,2,0,42],"source":"wire","status":"success"}
  • qt-example is the folder with modbus.cpp and CMakeLists.txt; CMAKE_PREFIX_PATH points at your Qt 6 installation.
  • The server opens port 0, so the port in the first line changes on every run.
  • The program exits with code 0 after reading the expected value. If it isn’t done within 10 seconds, it prints No result within 10 s and exits with 1.

In a widgets application, update widgets in the callbacks that run through QMetaObject::invokeMethod(qApp, ...). QtEndpoint isn’t a QObject, so make sure only the worker object that owns it calls it, on its own thread.

MfcEndpoint takes a UTF-16 CString JSON command and returns the whole reply ({"ok":true,"result":...}) as a CString; the adapter converts to and from UTF-8. The header is bindings/mfc/PdcommModbus.hpp in the SDK.

  • Build the application as Unicode (UNICODE and _UNICODE) so that CString is CStringW.
  • Failures throw std::runtime_error, whose what() is the UTF-8 error JSON.
  • Call request and connect from a worker thread, for example one started with AfxBeginThread, and send results to the window with PostMessage. Never call them from a window’s message handler.
  • For a server, repeat poll on the worker thread.

This console program initializes MFC, runs a write against the local model and checks the reply:

examples/modbus-mfc/main.cpp
#include "PdcommModbus.hpp"
#include <afxwin.h>
#include <cstdio>
CWinApp application;
int wmain() {
if (!AfxWinInit(GetModuleHandle(nullptr), nullptr, GetCommandLineW(), 0))
return 2;
try {
pdcomm::modbus::MfcEndpoint endpoint;
const auto reply =
endpoint.Call(L"{\"action\":\"execute\",\"pdu\":[6,0,0,0,9]}");
if (reply.Find(L"\"response\":[6,0,0,0,9]") < 0)
return 1;
std::puts("Modbus MFC consumer passed");
return 0;
} catch (const std::exception &) {
return 1;
}
}

Build it with the Visual Studio C++ tools including MFC, and put the SDK’s pdcomm_modbus_ffi.dll and the dependency DLLs from its lib folder next to the program or on PATH.

Call on Endpoint blocks too. In WinForms or WPF, call it from the thread pool (for example with Task.Run) or a dedicated worker thread, and marshal results back to the UI thread. Endpoint serializes calls with an internal lock; Dispose it when you’re done.