첫 TCP 통신 자세히 보기
빠른 시작은 peer.py가 서버 구성을 대신했습니다. 이 튜토리얼은 서버를 직접 구성하고, 클라이언트가 받는 성공·예외·오류를 구분하며, SDK의 API 계층과 소유권 규칙을 익힙니다.
- SDK 폴더 구성과 예제 프로젝트가 SDK를 찾는 방법을 이해합니다.
- 헤드리스 서버를 직접 구성하고, 서버 쪽에서 모델 값을 확인하고 바꿉니다.
- 클라이언트에서 정상 응답, 예외 응답, 서비스 오류를 구분합니다.
- 서버가 사라졌을 때의 동작과 재연결을 확인합니다.
준비물
섹션 제목: “준비물”- 빠른 시작의 준비물과 환경 변수
MODBUS_SDK,MODBUS_EXAMPLES - 빠른 시작 3단계에서 빌드한 C++ 또는 C# 콘솔
- 터미널 두 개
SDK 구성
섹션 제목: “SDK 구성”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)를 넣습니다.
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")SDK의 Endpoint.cs를 소스로 포함합니다. NuGet 패키지가 필요 없습니다.
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net8.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> </PropertyGroup> <ItemGroup> <Compile Include="$(PdcommModbusSdk)/bindings/dotnet/PDComm.Modbus/Endpoint.cs" Link="Endpoint.cs" /> </ItemGroup> <Target Name="ValidateSdk" BeforeTargets="BeforeBuild"> <Error Condition="!Exists('$(PdcommModbusSdk)/bindings/dotnet/PDComm.Modbus/Endpoint.cs')" Text="Set PdcommModbusSdk to the extracted SDK root." /> </Target></Project>API 계층과 소유권
섹션 제목: “API 계층과 소유권”| 계층 | 명령을 보내는 함수 | 결과 | 실패 |
|---|---|---|---|
| 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를 직접 쓰면 다음 순서를 지킵니다.
pdcomm_modbus_endpoint_create로 엔드포인트를 만듭니다. 핸들은 포인터가 아닌 정수이며 0은 유효하지 않습니다.pdcomm_modbus_call에 UTF-8 JSON과 바이트 길이를 넘깁니다.PDCOMM_MODBUS_OK와PDCOMM_MODBUS_SERVICE_ERROR는 결과 버퍼를 돌려주고, 그 밖의 상태는 버퍼가 없습니다.pdcomm_modbus_buffer_copy를 출력 없이 한 번 호출해 크기를 알아낸 뒤, 메모리를 잡고 다시 호출해 복사합니다.- 버퍼와 엔드포인트를 각각
pdcomm_modbus_release로 한 번씩 해제합니다.
C++, .NET, Qt, MFC 래퍼는 이 해제를 대신합니다. C API 레퍼런스의 pdcomm_modbus_call 항목에 전체 C 예제가 있습니다.
스레드 규칙은 모든 계층에 같습니다.
- 같은 엔드포인트의 호출은 라이브러리가 직렬화합니다. 다른 엔드포인트는 동시에 실행됩니다.
- 클라이언트의
connect와request는 응답이나timeout_ms까지 호출한 스레드를 막습니다. GUI 스레드에서 부르지 않습니다. - 라이브러리는 백그라운드 스레드를 만들지 않습니다. 서버와 게이트웨이는 애플리케이션이 작업 스레드에서
{"action":"poll"}을 반복해야 요청에 응답합니다. 헤드리스 시뮬레이터와 GUI는 이 반복을 직접 합니다.
1. 서버를 직접 구성합니다
섹션 제목: “1. 서버를 직접 구성합니다”터미널 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` 처럼 실패합니다.
2. 연결 전에 요청해 봅니다
섹션 제목: “2. 연결 전에 요청해 봅니다”터미널 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은 쓰기를 자동으로 다시 보내지 않습니다.
6. 정리합니다
섹션 제목: “6. 정리합니다”터미널 B에 quit를 입력합니다. 이번 세션에는 실패한 명령이 있었으므로 C++ 콘솔은 종료 코드 1로 끝납니다. 서버 터미널 A는 이미 끝났습니다.
예제 코드 읽기
섹션 제목: “예제 코드 읽기”C++ 콘솔은 입력 줄마다 call을 부르고, 예외를 잡아 failed를 표시합니다. 끝낼 때 disconnect를 보내고 엔드포인트 소멸자가 핸들을 해제합니다.
#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가 끝날 때 핸들을 해제합니다.
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;}다음 단계
섹션 제목: “다음 단계”- 19개 기능 코드와 진단
- GUI 애플리케이션의 스레드 구성은 가이드: Qt와 MFC에서 사용하기를 참고합니다.