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

주소와 프레임 관찰

이 튜토리얼은 네트워크나 직렬 포트 없이 진행합니다. 헤드리스 시뮬레이터 하나를 열고, 그 안의 데이터 모델에 PDU를 실행하고(execute), 원시 바이트를 프레임 디코더에 넣어 봅니다(fault). 모든 결과는 source가 local 또는 synthetic이며 실제 통신이 아닙니다.

  • 장비 매뉴얼의 주소(예: 40108)를 PDU 주소로 바꿔 읽고 씁니다.
  • 비트 테이블의 바이트 묶음과 예외 응답을 확인합니다.
  • 같은 요청의 TCP, RTU, ASCII 프레임을 해석하고, CRC·LRC 오류와 RTU 문자 간격 오류를 재현합니다.
  • PDComm Modbus SDK. 빠른 시작처럼 MODBUS_SDK가 SDK 폴더를 가리키게 합니다.
  • 터미널 하나

1. 헤드리스 시뮬레이터를 시작합니다

섹션 제목: “1. 헤드리스 시뮬레이터를 시작합니다”
터미널 창
"$MODBUS_SDK/bin/pdcomm-modbus-headless"

헤드리스 시뮬레이터는 표준 입력에서 JSON 명령을 한 줄씩 읽고, 줄마다 응답 한 줄을 출력합니다. 아무것도 출력하지 않고 입력을 기다리면 정상입니다. 새 세션은 연결되지 않은 TCP 서버 구성이며, 네 테이블에 각각 0으로 채운 1024개 항목이 있습니다.

먼저 빌드가 지원하는 기능을 확인합니다.

{"action":"capabilities"}
{"ok":true,"result":{"ascii":true,"canopen":false,"diagnostic_logging":true,"diagnostics":[0,1,2,3,4,10,11,12,13,14,15,16,17,18,20],"functions":[1,2,3,4,5,6,7,8,11,12,15,16,17,20,21,22,23,24,43],"logging_schema_version":1,"mei13":"opaque_extension","project_version":1,"protocol":"modbus","rtu":true,"security":true,"tcp":true}}

rtu, ascii가 false인 SDK라면 6단계와 7단계의 해당 전송은 Transport is excluded 오류가 납니다.

2. 매뉴얼 주소를 PDU 주소로 바꿉니다

섹션 제목: “2. 매뉴얼 주소를 PDU 주소로 바꿉니다”

장비 매뉴얼에 “40108: 설정 온도”가 있다고 가정합니다. 접두 숫자 4는 보유 레지스터이고, 108에서 1을 빼면 PDU 주소 107(0x006B)입니다. 1234(0x04D2)를 쓰고 다시 읽습니다.

{"action":"execute","pdu":"06006B04D2"}
{"action":"execute","pdu":"03006B0001"}
{"ok":true,"result":{"response":[6,0,107,4,210],"source":"local","status":"success"}}
{"ok":true,"result":{"response":[3,2,4,210],"source":"local","status":"success"}}

응답은 10진수 바이트입니다. [3,2,4,210]은 기능 코드 3, 바이트 수 2, 값 0x04D2(1234)입니다.

코일 0~7에 0xA5를 한 번에 쓰고 8개를 읽습니다.

{"action":"execute","pdu":"0F0000000801A5"}
{"action":"execute","pdu":"0100000008"}
{"ok":true,"result":{"response":[15,0,0,0,8],"source":"local","status":"success"}}
{"ok":true,"result":{"response":[1,1,165],"source":"local","status":"success"}}

읽은 바이트 165(0xA5)의 최하위 비트가 코일 0입니다. 따라서 코일 0, 2, 5, 7이 켜져 있습니다.

이산 입력은 클라이언트가 쓸 수 없는 테이블입니다. 시뮬레이터에서는 data_set으로 장비 입력이 바뀐 상황을 만듭니다.

{"action":"data_set","table":"discrete_inputs","address":0,"values":[1,0,1]}
{"action":"execute","pdu":"0200000003"}
{"ok":true,"result":{"written":3}}
{"ok":true,"result":{"response":[2,1,5],"source":"local","status":"success"}}

모델에 없는 주소 65535를 읽습니다.

{"action":"execute","pdu":"03FFFF0001"}
{"ok":true,"result":{"response":[131,2],"source":"local","status":"exception"}}

131은 0x83(기능 코드 3 + 0x80)이고 2는 Illegal Data Address입니다. 명령 자체는 성공했으므로 ok는 true입니다.

fault 명령은 원시 바이트를 프레임 디코더에 넣고 결과를 보여 줍니다. 입출력은 하지 않습니다.

{"action":"fault","transport":"tcp","bytes":"000100000006010300000001"}
{"ok":true,"result":{"errors":[],"frames":[{"pdu":[3,0,0,0,1],"transaction":1,"unit":1}],"source":"synthetic"}}

MBAP 헤더(0001 0000 0006 01)를 떼고 트랜잭션 1, 유닛 1, PDU 03 0000 0001을 얻었습니다. PDU 마지막 바이트를 빼면 프레임이 끝나지 않습니다.

{"action":"fault","transport":"tcp","bytes":"0001000000060103000000"}
{"ok":true,"result":{"errors":["Incomplete"],"frames":[],"source":"synthetic"}}

6. RTU와 ASCII 프레임을 해석합니다

섹션 제목: “6. RTU와 ASCII 프레임을 해석합니다”

같은 PDU 03 0000 000A를 RTU로 보냅니다. CRC C5 CD가 맞으면 프레임이 나오고, 한 비트만 바꿔도 Checksum 오류가 납니다.

{"action":"fault","transport":"rtu","bytes":"01030000000AC5CD"}
{"action":"fault","transport":"rtu","bytes":"01030000000AC5CC"}
{"ok":true,"result":{"errors":[],"frames":[{"pdu":[3,0,0,0,10],"unit":1}],"source":"synthetic"}}
{"ok":true,"result":{"errors":["Checksum"],"frames":[],"source":"synthetic"}}

ASCII는 화면에 보이는 문자 :01030000000AF2 + CR LF의 바이트 값을 16진수로 적어 넣습니다.

{"action":"fault","transport":"ascii","bytes":"3A30313033303030303030304146320D0A"}
{"action":"fault","transport":"ascii","bytes":"3A30313033303030303030304146330D0A"}
{"action":"fault","transport":"ascii","bytes":"3A30313033303030303030306166320D0A"}
{"ok":true,"result":{"errors":[],"frames":[{"pdu":[3,0,0,0,10],"unit":1}],"source":"synthetic"}}
{"ok":true,"result":{"errors":["Checksum"],"frames":[],"source":"synthetic"}}
{"ok":true,"result":{"errors":["Encoding"],"frames":[],"source":"synthetic"}}
  • 두 번째는 LRC를 F2에서 F3로 바꿔 Checksum 오류가 났습니다.
  • 세 번째는 A를 소문자 a(0x61)로 바꿔 Encoding 오류가 났습니다. ASCII 모드는 대문자 16진 문자만 받습니다.

7. RTU 문자 간격 오류를 재현합니다

섹션 제목: “7. RTU 문자 간격 오류를 재현합니다”

gap_us는 RTU 디코더에 넣는 바이트 사이의 가상 간격입니다. 기본값은 100µs입니다. 구성의 기본 속도 19200bps, 8E1에서 t1.5는 860µs, t3.5는 2006µs입니다.

{"action":"fault","transport":"rtu","bytes":"01030000000AC5CD","gap_us":1000}
{"action":"fault","transport":"rtu","bytes":"01030000000AC5CD","gap_us":3000}
{"ok":true,"result":{"errors":["Timing"],"frames":[],"source":"synthetic"}}
{"ok":true,"result":{"errors":["Length","Length","Length","Length","Length","Length","Length","Length"],"frames":[],"source":"synthetic"}}
  • 1000µs는 t1.5보다 길어 프레임 안의 간격 오류(Timing)로 버려집니다.
  • 3000µs는 t3.5보다 길어 바이트마다 프레임이 끝납니다. 1바이트 프레임은 너무 짧아 Length 오류가 8번 납니다.
{"action":"trace"}
{"action":"trace_clear"}

trace는 execute 기록을 source가 local인 항목으로 보여 줍니다. fault는 기록하지 않습니다. 끝내려면 {"action":"shutdown"}을 입력하거나 입력을 닫습니다(macOS·Linux는 Ctrl+D, Windows는 Ctrl+Z 후 Enter).

출력 원인과 해결
{"error":"Invalid hex length","ok":false} 16진 문자열의 글자 수가 홀수입니다. 바이트마다 두 글자로 씁니다.
{"error":"Invalid hex","ok":false} 16진수가 아닌 문자가 있습니다.
{"error":"key must be a string at line 1 column 2","ok":false} JSON 문법 오류입니다. 한 줄에 객체 하나를 완전한 JSON으로 씁니다. 여러 줄로 나눈 JSON은 줄마다 따로 해석됩니다.
{"error":"Unknown service action","ok":false} action 이름이 틀렸습니다. JSON 명령 레퍼런스를 확인합니다.

헤드리스 시뮬레이터는 오류가 한 번이라도 나면 끝날 때 종료 코드 1을 돌려줍니다. 오류 뒤의 명령은 계속 처리합니다.