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이 있습니다. 폴더 전체를 함께 보관합니다.
따라 하기
섹션 제목: “따라 하기”-
두 터미널에서 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이 붙습니다. -
터미널 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처럼 같은 빈 포트로 바꿉니다. -
터미널 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-tutorialWindows의 Visual Studio 생성기는 실행 파일을
build\modbus-tutorial\Release\pdcomm-modbus-tutorial.exe에 만들고, 실행 전에lib\pdcomm_modbus_ffi.dll을 찾을 수 있게PATH에 SDK의lib을 추가해야 합니다.터미널 창 dotnet build "$MODBUS_EXAMPLES/modbus-tutorial/dotnet/ModbusTutorial.csproj" -c Release -p:PdcommModbusSdk="$MODBUS_SDK" -o build/modbus-dotnetdotnet build/modbus-dotnet/ModbusTutorial.dll --native "$MODBUS_SDK/lib/libpdcomm_modbus_ffi.dylib"--native에는 네이티브 라이브러리의 절대 경로를 줍니다. Linux는libpdcomm_modbus_ffi.so, Windows는pdcomm_modbus_ffi.dll입니다.콘솔은 표준 오류에
Modbus C++ tutorial: JSON lines; quit or EOF to finish.(C#은Modbus C# tutorial: ...)을 출력하고 입력을 기다립니다. -
터미널 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입니다.
-
터미널 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}]}} -
정리합니다. 터미널 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을 호출합니다.
#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을 호출합니다.
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;}다음 단계
섹션 제목: “다음 단계”- 튜토리얼: 첫 TCP 통신 자세히 보기: SDK 구성, API 소유권, 오류 처리를 설명합니다.
- 튜토리얼: 19개 기능 코드와 진단: 모든 기능 코드를 실행해 봅니다.
- 데이터 모델과 주소: 장비 매뉴얼의 40001 같은 주소를 PDU 주소로 바꾸는 방법을 익힙니다.