주소와 프레임 관찰
이 튜토리얼은 네트워크나 직렬 포트 없이 진행합니다. 헤드리스 시뮬레이터 하나를 열고, 그 안의 데이터 모델에 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)입니다.
3. 비트 테이블을 확인합니다
섹션 제목: “3. 비트 테이블을 확인합니다”코일 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"}}4. 예외 응답을 확인합니다
섹션 제목: “4. 예외 응답을 확인합니다”모델에 없는 주소 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입니다.
5. TCP 프레임을 해석합니다
섹션 제목: “5. TCP 프레임을 해석합니다”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번 납니다.
8. 기록을 확인하고 정리합니다
섹션 제목: “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을 돌려줍니다. 오류 뒤의 명령은 계속 처리합니다.
다음 단계
섹션 제목: “다음 단계”- 첫 TCP 통신 자세히 보기에서 실제 소켓으로 같은 PDU를 주고받습니다.
- 19개 기능 코드와 진단에서 나머지 기능 코드를 실행합니다.