콘텐츠로 이동
무료 평가 시작

첫 TCP 통신 자세히 보기

빠른 시작은 peer.py가 서버 구성을 대신했습니다. 이 튜토리얼은 서버를 직접 구성하고, 클라이언트가 받는 성공·예외·오류를 구분하며, SDK의 API 계층과 소유권 규칙을 익힙니다.

  • SDK 폴더 구성과 예제 프로젝트가 SDK를 찾는 방법을 이해합니다.
  • 헤드리스 서버를 직접 구성하고, 서버 쪽에서 모델 값을 확인하고 바꿉니다.
  • 클라이언트에서 정상 응답, 예외 응답, 서비스 오류를 구분합니다.
  • 서버가 사라졌을 때의 동작과 재연결을 확인합니다.
  • 빠른 시작의 준비물과 환경 변수 MODBUS_SDK, MODBUS_EXAMPLES
  • 빠른 시작 3단계에서 빌드한 C++ 또는 C# 콘솔
  • 터미널 두 개
pdcomm-modbus/
├── bin/ pdcomm-modbus-headless, pdcomm-modbus-mcp (Windows: .exe와 GUI)
├── lib/ libpdcomm_modbus_ffi.dylib / .so, Windows는 pdcomm_modbus_ffi.dll과 .dll.lib
├── include/pdcomm/modbus/
│ ├── pdcomm_modbus.h C ABI
│ ├── pdcomm_modbus.hpp C++ 래퍼
│ └── pdcomm_modbus_logging.hpp
├── bindings/ dotnet/PDComm.Modbus, qt/PdcommModbusQt.hpp, mfc/PdcommModbus.hpp
├── licenses/
└── manifest.json 제공 파일 목록, 기능 비트, ABI 버전

macOS SDK는 최상위에 PDComm Modbus Simulator.app이 있습니다. 라이브러리와 실행 파일은 같은 SDK의 lib/에 있는 의존 파일을 함께 씁니다. 파일 하나만 다른 곳으로 옮기지 않습니다.

예제는 SDK 경로를 받아 빌드합니다.

PDCOMM_MODBUS_SDK로 헤더와 라이브러리를 찾고, 실행 파일에 SDK lib/의 실행 경로(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")
계층 명령을 보내는 함수 결과 실패
C pdcomm_modbus_call 결과 버퍼 핸들 상태 코드. 서비스 오류도 오류 버퍼를 돌려줍니다.
C++ Endpoint::call {"ok":true,"result":...} 문자열 std::runtime_error. what()은 오류 JSON 전체입니다.
.NET Endpoint.Call result의 JsonElement ModbusCommandException(InvalidOperationException에서 파생). Message는 오류 문자열이고, 라이선스 오류에서는 Code에 오류 코드가 들어 있습니다.
Qt QtEndpoint::call result의 QJsonObject std::runtime_error
MFC MfcEndpoint::Call 응답 전체의 CString std::runtime_error

C ABI를 직접 쓰면 다음 순서를 지킵니다.

  1. pdcomm_modbus_endpoint_create로 엔드포인트를 만듭니다. 핸들은 포인터가 아닌 정수이며 0은 유효하지 않습니다.
  2. pdcomm_modbus_call에 UTF-8 JSON과 바이트 길이를 넘깁니다. PDCOMM_MODBUS_OK와 PDCOMM_MODBUS_SERVICE_ERROR는 결과 버퍼를 돌려주고, 그 밖의 상태는 버퍼가 없습니다.
  3. pdcomm_modbus_buffer_copy를 출력 없이 한 번 호출해 크기를 알아낸 뒤, 메모리를 잡고 다시 호출해 복사합니다.
  4. 버퍼와 엔드포인트를 각각 pdcomm_modbus_release로 한 번씩 해제합니다.

C++, .NET, Qt, MFC 래퍼는 이 해제를 대신합니다. C API 레퍼런스의 pdcomm_modbus_call 항목에 전체 C 예제가 있습니다.

스레드 규칙은 모든 계층에 같습니다.

  • 같은 엔드포인트의 호출은 라이브러리가 직렬화합니다. 다른 엔드포인트는 동시에 실행됩니다.
  • 클라이언트의 connect와 request는 응답이나 timeout_ms까지 호출한 스레드를 막습니다. GUI 스레드에서 부르지 않습니다.
  • 라이브러리는 백그라운드 스레드를 만들지 않습니다. 서버와 게이트웨이는 애플리케이션이 작업 스레드에서 {"action":"poll"}을 반복해야 요청에 응답합니다. 헤드리스 시뮬레이터와 GUI는 이 반복을 직접 합니다.

터미널 A에서 헤드리스 시뮬레이터를 시작하고 두 줄을 입력합니다.

터미널 창
"$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"}

connect 결과의 "connected":true를 확인합니다. 헤드리스 시뮬레이터는 입력을 기다리는 동안에도 서버를 계속 poll합니다. 입력을 닫으면(EOF) 서버도 끝나므로 터미널 A를 그대로 둡니다.

configure의 config에는 transport, role, address, port, unit, baud, parity, stop_bits, timeout_ms가 모두 있어야 합니다. TCP에서도 직렬 항목을 기본값으로 채웁니다. 필드가 빠지면 missing field `port` 처럼 실패합니다.

터미널 B에서 클라이언트 콘솔을 시작합니다.

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

구성하기 전에 요청을 보냅니다.

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

표준 출력에는 아무것도 나오지 않고, 표준 오류에 다음이 나옵니다.

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

C++ 콘솔은 성공한 응답만 표준 출력에 쓰고, 실패는 Command error: 뒤에 오류 JSON을 붙여 표준 오류에 씁니다. C# 콘솔은 Command error: Connect a client before sending a request처럼 메시지만 씁니다.

3. 연결하고 쓰기와 예외를 확인합니다

섹션 제목: “3. 연결하고 쓰기와 예외를 확인합니다”
{"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"}}

두 번째 요청은 주소 1024를 읽습니다. 서버 모델은 0~1023이므로 예외 02가 돌아옵니다. 예외 응답도 ok가 true인 정상 결과입니다. 애플리케이션은 status를 확인해 예외를 처리합니다.

4. 서버 쪽에서 값을 확인하고 바꿉니다

섹션 제목: “4. 서버 쪽에서 값을 확인하고 바꿉니다”

터미널 A에서 서버 자신의 모델을 읽고, 보유 레지스터 1을 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는 연결 중인 서버 모델에서 실행되므로 클라이언트가 쓴 42가 보입니다. 터미널 B에서 두 레지스터를 읽으면 서버가 바꾼 값도 보입니다.

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

5. 서버를 끝내고 클라이언트 동작을 확인합니다

섹션 제목: “5. 서버를 끝내고 클라이언트 동작을 확인합니다”

터미널 A에 {"action":"shutdown"}을 입력해 서버를 끝냅니다. 그다음 터미널 B에서 다음을 차례로 입력합니다.

{"action":"request","pdu":"0300000001"}
{"action":"state"}
{"action":"connect"}
  • request는 Command error: {"error":"Closed","ok":false}로 실패합니다. 서버가 연결을 닫았기 때문입니다.
  • state 결과의 connected는 false입니다.
  • connect는 Io(ConnectionRefused, "Connection refused (os error 61)")로 실패합니다. 괄호 안의 OS 오류 번호와 문구는 운영체제마다 다릅니다.

응답이 timeout_ms 안에 오지 않을 때도 클라이언트는 Timeout 오류를 돌려주고 연결을 닫습니다. 다시 connect한 뒤 계속합니다. 쓰기 요청이 시간 초과됐다면 장비가 이미 처리했을 수 있으므로 먼저 값을 읽어 확인합니다. PDComm은 쓰기를 자동으로 다시 보내지 않습니다.

터미널 B에 quit를 입력합니다. 이번 세션에는 실패한 명령이 있었으므로 C++ 콘솔은 종료 코드 1로 끝납니다. 서버 터미널 A는 이미 끝났습니다.

C++ 콘솔은 입력 줄마다 call을 부르고, 예외를 잡아 failed를 표시합니다. 끝낼 때 disconnect를 보내고 엔드포인트 소멸자가 핸들을 해제합니다.

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;
}
}

C# 콘솔은 입력 줄을 JsonDocument로 파싱해 그대로 Call에 넘기고, 받은 result를 다시 {"ok":true,"result":...}로 감싸 C++ 콘솔과 같은 형식으로 출력합니다. using var endpoint가 끝날 때 핸들을 해제합니다.

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;
}