TCP · RTU · ASCII
Modbus
Build Modbus TCP, RTU and ASCII clients, servers and TCP-to-RTU/ASCII gateways, driven by one JSON command interface.
What it does
- Modbus TCP, RTU and ASCII transports.
- Client, server and TCP-to-RTU/ASCII gateway roles.
- 19 function codes and 15 diagnostics sub-functions.
- Server data model: the four tables, file records, FIFO queues and device identification objects.
- Trace of the last 1000 requests and frames, frame decoding and diagnostics counters.
- Save and restore the configuration and data model as a JSON project file.
Example
Each binding shows code from a real example file in the examples folder of the PDComm SDK. The source file and line range appear above the code.
Modbus client in C
snprintf(configure, sizeof(configure),
"{\"action\":\"configure\",\"config\":{\"transport\":\"tcp\",\"role\":\"client\","
"\"address\":%s,\"port\":\"\",\"unit\":1,\"baud\":19200,\"parity\":\"even\","
"\"stop_bits\":1,\"timeout_ms\":1000,\"security\":%s}}",
address, security);
pdcomm_modbus_handle endpoint = 0;
if (pdcomm_modbus_endpoint_create(&endpoint) != PDCOMM_MODBUS_OK) {
fputs("Cannot create the Modbus endpoint\n", stderr);
return 1;
}
/* Configure, connect, write 42 to holding register 0 (function 06), then
* read that register back (function 03). Each step needs the previous one. */
const char *steps[] = {configure, "{\"action\":\"connect\"}",
"{\"action\":\"request\",\"pdu\":\"060000002A\"}",
"{\"action\":\"request\",\"pdu\":\"0300000001\"}"};
char *reply = NULL;
int passed = 1;
for (size_t step = 0; passed && step < sizeof(steps) / sizeof(steps[0]); ++step) {
free(reply);
reply = call(endpoint, steps[step]);
passed = reply != NULL;
if (passed && step >= 2)
printf("%s -> %s\n", steps[step], reply);
}
/* {"ok":true,"result":{"response":[3,2,0,42],"source":"wire","status":"success"}} */
passed = passed && strstr(reply, "\"response\":[3,2,0,42]") != NULL;
free(reply);
free(call(endpoint, "{\"action\":\"disconnect\"}"));
pdcomm_modbus_release(endpoint);From the C example: pass JSON commands to pdcomm_modbus_call() to configure and connect a TCP client, write 42 to holding register 0 (function 06) and read it back (function 03). Run it against the Modbus simulator as the server.
Modbus client in C++
try {
pdcomm::modbus::Endpoint endpoint;
endpoint.call("{\"action\":\"configure\",\"config\":{\"transport\":\"tcp\",\"role\":\"client\","
"\"address\":" + quote(argv[1]) + ",\"port\":\"\",\"unit\":1,\"baud\":19200,"
"\"parity\":\"even\",\"stop_bits\":1,\"timeout_ms\":1000,\"security\":" + security + "}}");
endpoint.call("{\"action\":\"connect\"}");
// Function 06: write 42 (0x002A) to holding register 0.
std::cout << "write " << endpoint.call("{\"action\":\"request\",\"pdu\":\"060000002A\"}") << "\n";
// Function 03: read one holding register from address 0.
const std::string read = endpoint.call("{\"action\":\"request\",\"pdu\":\"0300000001\"}");
std::cout << "read " << read << "\n";
endpoint.call("{\"action\":\"disconnect\"}");
// {"ok":true,"result":{"response":[3,2,0,42],"source":"wire","status":"success"}}
if (read.find("\"response\":[3,2,0,42]") == std::string::npos)
return 1;
std::cout << "PASS Modbus client (C++" << (tls ? ", TLS" : "") << ")" << std::endl;
return 0;From the C++ example: pdcomm::modbus::Endpoint::call() runs the same JSON commands and throws std::runtime_error with the error reply when one fails.
Modbus client in .NET
try
{
using var endpoint = new Endpoint();
endpoint.Call(new
{
action = "configure",
config = new
{
transport = "tcp", role = "client", address = args[2], port = "", unit = 1,
baud = 19200, parity = "even", stop_bits = 1, timeout_ms = 1000, security,
},
});
endpoint.Call(new { action = "connect" });
// Function 06: write 42 (0x002A) to holding register 0.
JsonElement write = endpoint.Call(new { action = "request", pdu = "060000002A" });
Console.WriteLine($"write {write}");
// Function 03: read one holding register from address 0.
JsonElement read = endpoint.Call(new { action = "request", pdu = "0300000001" });
Console.WriteLine($"read {read}");
endpoint.Call(new { action = "disconnect" });From the .NET example: Endpoint.Call takes anonymous objects as commands and returns the result as a JsonElement. A refused command raises ModbusCommandException with a Code.
Qt Modbus worker
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);
}From the Qt Modbus example: a worker thread owns a QtEndpoint, runs QJsonObject commands and hands results back to the main thread, so the GUI never blocks.
Modbus MFC endpoint
#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;
}
}From the MFC example: MfcEndpoint takes UTF-16 CString JSON commands and returns the whole reply as a CString. Build it on Windows with MSVC and MFC.