Skip to content
Start free evaluation

A closer look at the first TCP exchange

In the quickstart, peer.py configured the server for you. In this tutorial you configure the server yourself, tell success, exception and error results apart on the client, and learn the SDK’s API layers and ownership rules.

  • Understand the SDK folder and how the example projects find it.
  • Configure a headless server by hand, then read and change its model from the server side.
  • Distinguish normal responses, exception responses and service errors on the client.
  • See what happens when the server goes away, and reconnect.
  • The prerequisites and the MODBUS_SDK and MODBUS_EXAMPLES variables from the quickstart
  • The C++ or C# console you built in step 3 of the quickstart
  • Two terminals
pdcomm-modbus/
├── bin/ pdcomm-modbus-headless, pdcomm-modbus-mcp (Windows: .exe files and the GUI)
├── lib/ libpdcomm_modbus_ffi.dylib / .so; on Windows pdcomm_modbus_ffi.dll and .dll.lib
├── include/pdcomm/modbus/
│ ├── pdcomm_modbus.h C ABI
│ ├── pdcomm_modbus.hpp C++ wrapper
│ └── pdcomm_modbus_logging.hpp
├── bindings/ dotnet/PDComm.Modbus, qt/PdcommModbusQt.hpp, mfc/PdcommModbus.hpp
├── licenses/
└── manifest.json file inventory, feature bits, ABI version

The macOS SDK also has PDComm Modbus Simulator.app at the top. The library and the programs use dependencies from the same SDK’s lib/, so don’t move single files out of the folder.

The examples take the SDK path when you build them:

PDCOMM_MODBUS_SDK locates the headers and the library, and the program gets the SDK’s lib/ as its run path (rpath).

examples/modbus-tutorial/CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(pdcomm_modbus_tutorial LANGUAGES CXX)
set(PDCOMM_MODBUS_SDK "" CACHE PATH "Installed Modbus SDK root")
if(NOT EXISTS "${PDCOMM_MODBUS_SDK}/include/pdcomm/modbus/pdcomm_modbus.hpp")
message(FATAL_ERROR "Set PDCOMM_MODBUS_SDK to the extracted SDK root")
endif()
find_library(MODBUS_LIBRARY NAMES pdcomm_modbus_ffi pdcomm_modbus_ffi.dll
PATHS "${PDCOMM_MODBUS_SDK}/lib" NO_DEFAULT_PATH REQUIRED)
add_executable(pdcomm-modbus-tutorial main.cpp)
target_compile_features(pdcomm-modbus-tutorial PRIVATE cxx_std_17)
target_include_directories(pdcomm-modbus-tutorial PRIVATE "${PDCOMM_MODBUS_SDK}/include")
target_link_libraries(pdcomm-modbus-tutorial PRIVATE "${MODBUS_LIBRARY}")
set_target_properties(pdcomm-modbus-tutorial PROPERTIES
BUILD_RPATH "${PDCOMM_MODBUS_SDK}/lib")
Layer Function that sends a command Result Failure
C pdcomm_modbus_call Result buffer handle Status code; a service error also returns an error buffer.
C++ Endpoint::call The string {"ok":true,"result":...} std::runtime_error whose what() is the whole error JSON
.NET Endpoint.Call result as a JsonElement ModbusCommandException (derives from InvalidOperationException) whose Message is the error string; license errors also set Code
Qt QtEndpoint::call result as a QJsonObject std::runtime_error
MFC MfcEndpoint::Call The whole reply as a CString std::runtime_error

When you use the C ABI directly, follow this sequence:

  1. Create an endpoint with pdcomm_modbus_endpoint_create. Handles are integers, not pointers, and 0 is never valid.
  2. Pass UTF-8 JSON and its byte length to pdcomm_modbus_call. PDCOMM_MODBUS_OK and PDCOMM_MODBUS_SERVICE_ERROR return a buffer; other statuses return none.
  3. Call pdcomm_modbus_buffer_copy once without an output to learn the size, allocate, then call it again to copy.
  4. Release the buffer and the endpoint once each with pdcomm_modbus_release.

The C++, .NET, Qt and MFC wrappers release for you. The pdcomm_modbus_call entry of the API reference has a complete C example.

The threading rules are the same for every layer:

  • The library serializes calls on one endpoint; different endpoints run in parallel.
  • A client’s connect and request block the calling thread until the response or timeout_ms. Don’t call them on a GUI thread.
  • The library creates no background thread. A server or gateway answers requests only while your application sends {"action":"poll"} from a worker thread. The headless simulator and the GUI do this for you.

In terminal A, start the headless simulator and enter two lines:

터미널 창
"$MODBUS_SDK/bin/pdcomm-modbus-headless"
{"action":"configure","config":{"transport":"tcp","role":"server","address":"127.0.0.1:1502","port":"","unit":1,"baud":19200,"parity":"even","stop_bits":1,"timeout_ms":1000}}
{"action":"connect"}

Check for "connected":true in the connect result. The headless simulator keeps polling the server while it waits for input. Closing its input (EOF) stops the server too, so leave terminal A as it is.

The config of configure must contain all of transport, role, address, port, unit, baud, parity, stop_bits and timeout_ms; fill the serial members with their defaults even for TCP. A missing member fails with an error such as missing field `port` .

In terminal B, start the client console:

터미널 창
./build/modbus-tutorial/pdcomm-modbus-tutorial

Send a request before configuring anything:

{"action":"request","pdu":"0300000001"}

Nothing appears on standard output; standard error shows:

Command error: {"error":"Connect a client before sending a request","ok":false}

The C++ console writes only successful replies to standard output and writes failures to standard error as Command error: followed by the error JSON. The C# console writes the message only, as in Command error: Connect a client before sending a request.

{"action":"configure","config":{"transport":"tcp","role":"client","address":"127.0.0.1:1502","port":"","unit":1,"baud":19200,"parity":"even","stop_bits":1,"timeout_ms":1000}}
{"action":"connect"}
{"action":"request","pdu":"060000002A"}
{"action":"request","pdu":"0304000001"}
{"ok":true,"result":{"response":[6,0,0,0,42],"source":"wire","status":"success"}}
{"ok":true,"result":{"response":[131,2],"source":"wire","status":"exception"}}

The second request reads address 1024. The server model covers 0–1023, so it returns exception 02. An exception response is still a normal result with ok set to true; check status to handle it.

4. Read and change the model on the server

Section titled “4. Read and change the model on the server”

In terminal A, read the server’s own model and set holding register 1 to 7:

{"action":"execute","pdu":"0300000001"}
{"action":"data_set","table":"holding_registers","address":1,"values":[7]}
{"ok":true,"result":{"response":[3,2,0,42],"source":"local","status":"success"}}
{"ok":true,"result":{"written":1}}

execute on a connected server runs against the live server model, so you see the 42 the client wrote. Read two registers in terminal B to see the server’s change as well:

{"action":"request","pdu":"0300000002"}
{"ok":true,"result":{"response":[3,4,0,42,0,7],"source":"wire","status":"success"}}

Enter {"action":"shutdown"} in terminal A to stop the server. Then enter these lines in terminal B:

{"action":"request","pdu":"0300000001"}
{"action":"state"}
{"action":"connect"}
  • request fails with Command error: {"error":"Closed","ok":false} because the server closed the connection.
  • state reports connected as false.
  • connect fails with Io(ConnectionRefused, "Connection refused (os error 61)"). The OS error number and text in parentheses differ between operating systems.

When a response doesn’t arrive within timeout_ms, the client also fails the request with Timeout and closes the connection; connect again to continue. If a write timed out, the device may already have applied it, so read the value first. PDComm never resends writes automatically.

Enter quit in terminal B. Because this session had failed commands, the C++ console exits with code 1. The server in terminal A has already stopped.

The C++ console calls call for every input line and records failures it catches. On exit it sends disconnect, and the endpoint’s destructor releases the handle.

examples/modbus-tutorial/main.cpp
#include <pdcomm/modbus/pdcomm_modbus.hpp>
#include <iostream>
#include <string>
// A client/local-model console: the separate simulator polls the server.
// Enter one service JSON command per line, then "quit" or EOF to release it.
int main() {
try {
pdcomm::modbus::Endpoint endpoint;
bool failed = false;
std::string line;
std::cerr << "Modbus C++ tutorial: JSON lines; quit or EOF to finish.\n";
while (std::getline(std::cin, line)) {
if (line == "quit")
break;
if (line.find_first_not_of(" \t\r") == std::string::npos)
continue;
try {
std::cout << endpoint.call(line) << std::endl;
} catch (const std::exception &error) {
failed = true;
std::cerr << "Command error: " << error.what() << std::endl;
}
}
endpoint.call("{\"action\":\"disconnect\"}");
return failed ? 1 : 0;
} catch (const std::exception &error) {
std::cerr << "Fatal error: " << error.what() << std::endl;
return 1;
}
}

The C# console parses each line with JsonDocument, passes it to Call, and wraps the returned result as {"ok":true,"result":...} so its output matches the C++ console. using var endpoint releases the handle at the end.

examples/modbus-tutorial/dotnet/Program.cs
using System.Runtime.InteropServices;
using System.Text.Json;
using PDComm.Modbus;
if (args.Length != 2 || args[0] != "--native" || !Path.IsPathFullyQualified(args[1]))
{
Console.Error.WriteLine("Usage: ModbusTutorial --native <absolute native library path>");
return 2;
}
try
{
string nativePath = Path.GetFullPath(args[1]);
NativeLibrary.SetDllImportResolver(typeof(Endpoint).Assembly,
(name, assembly, search) => name == "pdcomm_modbus_ffi" ? NativeLibrary.Load(nativePath) : IntPtr.Zero);
using var endpoint = new Endpoint();
bool failed = false;
Console.Error.WriteLine("Modbus C# tutorial: JSON lines; quit or EOF to finish.");
while (Console.ReadLine() is string line)
{
if (line == "quit") break;
if (string.IsNullOrWhiteSpace(line)) continue;
try
{
using var command = JsonDocument.Parse(line);
var result = endpoint.Call(command.RootElement);
Console.WriteLine(JsonSerializer.Serialize(new { ok = true, result }));
}
catch (Exception error)
{
failed = true;
Console.Error.WriteLine($"Command error: {error.Message}");
}
}
endpoint.Call(new { action = "disconnect" });
return failed ? 1 : 0;
}
catch (Exception error)
{
Console.Error.WriteLine($"Fatal error: {error.Message}");
return 1;
}