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

Modbus 빠른 시작

이 페이지는 같은 PC 안에서 Modbus TCP 서버와 클라이언트를 실행해 첫 통신을 확인합니다. 클라이언트는 보유 레지스터(holding register) 0에 42를 쓰고 다시 읽습니다. 실제 TCP 소켓으로 통신하므로 결과의 source가 wire입니다.

터미널 A: 헤드리스 서버 127.0.0.1:1502 <---- Modbus TCP ----> 터미널 B: C++ 또는 C# 클라이언트
  • 사용하는 OS의 PDComm Modbus SDK와 PDComm 예제(examples/)
  • C++: CMake 3.20 이상, C++17 컴파일러
  • C#: .NET 8 런타임과 net8.0을 빌드할 수 있는 .NET SDK
  • Python 3: 예제 서버 스크립트 peer.py를 실행합니다.
  • 평가판이나 개발 라이선스 파일. 클라이언트가 SDK를 쓰므로 필요합니다. 서버로 쓰는 헤드리스 시뮬레이터에는 필요 없습니다. 라이선스 설치와 확인을 참고합니다.

SDK 폴더에는 bin/, lib/, include/, bindings/, licenses/, manifest.json이 있습니다. 폴더 전체를 함께 보관합니다.

  1. 두 터미널에서 SDK와 예제 경로를 지정합니다. 경로는 실제로 압축을 푼 위치로 바꿉니다.

    터미널 창
    export MODBUS_SDK="$HOME/pdcomm/pdcomm-modbus"
    export MODBUS_EXAMPLES="$HOME/pdcomm/examples"
    export PDCOMM_LICENSE_FILE="$HOME/pdcomm/pdcomm.license"

    이 페이지의 명령은 macOS와 Linux 셸 기준입니다. Windows PowerShell에서는 같은 값을 환경 변수 $env:MODBUS_SDK, $env:MODBUS_EXAMPLES, $env:PDCOMM_LICENSE_FILE로 지정하고, 실행 파일과 라이브러리 이름에 .exe, .dll이 붙습니다.

  2. 터미널 A에서 서버를 시작합니다. peer.py는 SDK의 pdcomm-modbus-headless를 실행하고 TCP 서버로 구성한 뒤 연결을 엽니다.

    터미널 창
    python3 "$MODBUS_EXAMPLES/modbus-tutorial/peer.py" --sdk "$MODBUS_SDK" --address 127.0.0.1:1502

    두 줄의 JSON이 출력되고 두 번째 줄이 "connected":true이면 서버가 기다리는 중입니다. 터미널 A는 끝까지 열어 둡니다.

    {"ok":true,"result":{"config":{"address":"127.0.0.1:1502",...,"role":"server",...},"connected":true,...}}

    Io(AddrInUse, ...)가 나오면 1502 포트를 다른 프로그램이 쓰고 있습니다. 양쪽 터미널의 주소를 127.0.0.1:1503처럼 같은 빈 포트로 바꿉니다.

  3. 터미널 B에서 클라이언트 콘솔을 빌드하고 실행합니다.

    터미널 창
    cmake -S "$MODBUS_EXAMPLES/modbus-tutorial" -B build/modbus-tutorial -DCMAKE_BUILD_TYPE=Release -DPDCOMM_MODBUS_SDK="$MODBUS_SDK"
    cmake --build build/modbus-tutorial --config Release
    ./build/modbus-tutorial/pdcomm-modbus-tutorial

    Windows의 Visual Studio 생성기는 실행 파일을 build\modbus-tutorial\Release\pdcomm-modbus-tutorial.exe에 만들고, 실행 전에 lib\pdcomm_modbus_ffi.dll을 찾을 수 있게 PATH에 SDK의 lib을 추가해야 합니다.

    콘솔은 표준 오류에 Modbus C++ tutorial: JSON lines; quit or EOF to finish.(C#은 Modbus C# tutorial: ...)을 출력하고 입력을 기다립니다.

  4. 터미널 B에 다음 명령을 한 줄씩 입력합니다. 각 줄은 JSON 객체 하나입니다.

    {"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":"0300000001"}

    마지막 두 명령의 결과는 다음과 같습니다.

    {"ok":true,"result":{"response":[6,0,0,0,42],"source":"wire","status":"success"}}
    {"ok":true,"result":{"response":[3,2,0,42],"source":"wire","status":"success"}}
    • 060000002A는 보유 레지스터 0(0000)에 42(002A)를 쓰는 기능 코드 06 요청입니다. 서버는 같은 바이트를 돌려줍니다.
    • 0300000001은 보유 레지스터 0부터 1개를 읽는 기능 코드 03 요청입니다. 응답 [3,2,0,42]는 기능 코드 3, 바이트 수 2, 값 0x002A입니다.
  5. 터미널 A에 {"action":"trace"}를 입력해 서버가 받은 요청을 확인합니다.

    {"ok":true,"result":{"entries":[{"request":[6,0,0,0,42],"response":[6,0,0,0,42],"sequence":1,"source":"wire","status":"success","unit":1},{"request":[3,0,0,0,1],"response":[3,2,0,42],"sequence":2,"source":"wire","status":"success","unit":1}]}}
  6. 정리합니다. 터미널 B에 quit를 입력해 콘솔을 끝내고, 터미널 A에도 quit를 입력해 서버를 끝냅니다. 두 프로세스 모두 종료 코드 0으로 끝납니다.

증상 원인과 해결
Command error: {"error":"Io(ConnectionRefused, ...)","ok":false} 서버가 실행 중이 아니거나 주소가 다릅니다. 터미널 A의 서버와 같은 주소로 다시 configure, connect합니다.
Command error: {"error":"Timeout","ok":false} 서버가 응답하지 않았습니다. 서버 프로세스가 살아 있는지 확인하고 connect를 다시 실행합니다.
CMake가 Set PDCOMM_MODBUS_SDK to the extracted SDK root로 멈춤 MODBUS_SDK가 SDK 최상위 폴더(안에 include/가 있는 폴더)를 가리키게 합니다.
C#이 DllNotFoundException 또는 BadImageFormatException --native 경로가 절대 경로인지, OS와 CPU에 맞는 SDK인지 확인합니다.

C++ 콘솔은 실패한 명령의 오류 JSON을 그대로 출력하고, 오류가 한 번이라도 있었으면 종료 코드 1로 끝납니다. 더 많은 사례는 문제 해결에 있습니다.

C++ 콘솔은 Endpoint 하나를 만들고 입력 줄마다 Endpoint::call을 호출합니다.

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# 콘솔은 NativeLibrary.SetDllImportResolver로 네이티브 라이브러리 경로를 정한 뒤 Endpoint.Call을 호출합니다.

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