Modbus TCP / UDP 드라이버
개요
Modbus 산업 표준 (Modbus-IDA / IEC 61158-6-12) 기반 PLC 통신.
| 항목 | 값 |
|---|---|
opc_type | MODBUS (TCP) / MODBUSUDP (UDP) |
| 구현 클래스 | plantpulse.driver.protocol.modbus.ModbusTCPDriver / ModbusUDPDriver |
| 라이브러리 | 자체 네이티브 (plantpulse-plc-protocol 의 ModbusTcp / ModbusUdp) |
| read | ✅ |
| write | ✅ (coil / holding-register) |
| 보안 | 없음 (Modbus 표준상) |
Modbus TCP/UDP 모두 옛 PLC4j 위임을 걷어내고 plantpulse-plc-protocol 의 네이티브 ModbusTcp / ModbusUdp(공통 베이스 AbstractNativeModbusDriver)로 재작성됐습니다 — FINS / MELSEC / AB-ETH 와 동일한 패턴. UDP 는 MBAP-over-UDP(단일 데이터그램 = 단일 ADU)이며 주소·포맷·write 레이어는 TCP 와 완전 동일. 주소 문법(holding-register:N[:TYPE])은 PLC4X modbus4x 표기를 그대로 보존합니다.
OPC 등록 폼
| 필드 | 의미 | 예시 |
|---|---|---|
opc_agent_ip | Modbus 슬레이브 IP | 192.168.0.50 |
opc_agent_port | Modbus 포트 | 502 (TCP 기본) |
timecycle | 폴링 주기 (ms) | 1000 |
options.request-timeout | 요청 타임아웃 (ms) | 5000 |
Modbus TCP 는 보통 IP+포트 가 슬레이브를 구분하므로 unit-identifier(별칭 unit-id) 는 기본 1.
시리얼-TCP 게이트웨이 뒤에 다중 슬레이브가 있을 때 1~247 로 지정합니다.
태그 plc_address 형식
주소 표기 (<area>:<address> + :<datatype> + [<count>]) — PLC4X modbus4x 문법을 그대로 보존.
주소는 1-based(wire 로 0-based 변환, holding-register:5 → wire 레지스터 4).
| 표기 | 의미 |
|---|---|
holding-register:1 | Holding Register 1 (16-bit) |
holding-register:1:UINT | 16-bit unsigned |
holding-register:1:DINT | 32-bit signed (2 register) |
holding-register:1:REAL | 32-bit float (2 register) |
holding-register:100:STRING[10] | 100~109 의 ASCII 문자열 |
coil:1 | 코일 1 비트 |
discrete-input:1 | discrete input 1 비트 |
input-register:1 | input register 16-bit |
상세 표기 규칙은 PLC4j Modbus 문서 참고.
data_type / format 매핑
Edge 의 format 이 PLC4j 의 type suffix 로 그대로 매핑됩니다.
edge data_type | edge format | PLC4j addr 매핑 | 비고 |
|---|---|---|---|
| Boolean | (empty) / BOOL | coil:N 또는 :BOOL | 1 bit |
| Integer / Short | (empty) | :INT | 16-bit signed |
| UInteger | UI / UWORD | :UINT | 16-bit unsigned |
| Integer (32) | DW | :DINT | 32-bit signed (2 reg) |
| UInteger (32) | UDW | :UDINT | 32-bit unsigned |
| Float | REAL | :REAL | 32-bit IEEE 754 |
| Double | LREAL | :LREAL | 64-bit IEEE 754 |
| String | STR[N] | :STRING[N] | N 글자 |
format 이 STR[N], REAL[N], UDINT[N] 으로 시작하면 배열 처리됩니다 (readWordValues).
Write (네이티브 — TCP/UDP 공통)
ModbusTCPDriver / ModbusUDPDriver 는 write 를 지원합니다(isWriteSupported()=true):
coil:N→ single coil write (Boolean).holding-register:N[:TYPE]→ datatype 별 register write.REAL/DINT은 MSWord-first 2 레지스터, 그 외는 단일 16-bit. 값은 호출자가ProtocolAddress.value로 넘깁니다.discrete-input/input-register는 프로토콜상 read-only — write 불가.
흔한 에러 + 해결
| 메시지 / 증상 | 원인 | 해결 |
|---|---|---|
PLC_READ_TIMEOUT_EXCEPTION | 슬레이브 응답 지연 또는 단절 | 케이블/방화벽/포트(502) 확인. request-timeout 늘리기 |
PLC_READ_RUNTIME_EXCEPTION: Invalid PLC4j address | 주소 표기 오타 | holding-register:N 형식 검증, : 누락 주의 |
| 값이 0 만 반환 | 등록 종류 mismatch (input vs holding) | 슬레이브 매뉴얼에서 fc 03/04/01/02 중 어디인지 확인 |
| 32-bit 값 swap 이상 | byte/word order | 슬레이브가 little-endian 이면 format 을 :UDINT_LSWORD_FIRST 등으로 변경 (PLC4j 옵션) |
curl 등록 예시 (Modbus TCP)
curl -X POST http://<edge-host>/api/v1/opc \
-H "Content-Type: application/json" \
-d '{
"opc_id": "OPC_MB_HVAC",
"opc_type": "MODBUS",
"opc_name": "HVAC Slave",
"opc_agent_ip": "192.168.0.50",
"opc_agent_port": "502",
"site_id": "SITE_00001",
"auto_collect": true,
"timecycle": 1000,
"options": { "request-timeout": "5000" },
"tag_list": [
{
"tag_id": "OPC_MB_HVAC_TAG_00001",
"tag_name": "Temperature",
"plc_address": "holding-register:1:REAL",
"data_type": "Float",
"format": "REAL"
},
{
"tag_id": "OPC_MB_HVAC_TAG_00002",
"tag_name": "Status",
"plc_address": "coil:1",
"data_type": "Boolean"
}
]
}'
UDP 의 경우 opc_type: "MODBUSUDP" 로 변경, 포트는 보통 502 또는 슬레이브 설정에 따름.
값 read:
curl -s http://<edge-host>/api/v1/tag/OPC_MB_HVAC_TAG_00001/value | jq
예제 모음 (데이터타입별)
코일 / 디스크리트 (Boolean)
data_type | format | plc_address 예 | 비고 |
|---|---|---|---|
Boolean | (empty) | coil:1 | FC 01 (Read Coils) |
Boolean | (empty) | discrete-input:1 | FC 02 (Read Discrete Inputs) |
Boolean | BIN[3] | holding-register:1 | 워드 read 후 bit 3 추출 |
16-bit 정수
data_type | format | plc_address 예 | 비고 |
|---|---|---|---|
Integer | (empty) | holding-register:1 | signed INT 16 |
Integer | (empty) | holding-register:1:INT | 동일 (PLC4j 표기) |
Integer | UI | holding-register:1:UINT | unsigned 16 |
Integer | (empty) | input-register:1 | input register (FC 04) |
32-bit 정수
data_type | format | plc_address 예 | 비고 |
|---|---|---|---|
Integer | DW | holding-register:1:DINT | signed 32 (2 register) |
Integer | UDW | holding-register:1:UDINT | unsigned 32 |
Integer | UDINT | holding-register:1 | format 명시로 32-bit |
실수
data_type | format | plc_address 예 | 비고 |
|---|---|---|---|
Float | REAL | holding-register:1 | 32-bit float (2 register) |
Float | REAL | holding-register:1:REAL | 동일 표기 |
Double | LREAL | holding-register:1:LREAL | 64-bit double (4 register) |
문자열
data_type | format | plc_address 예 | 비고 |
|---|---|---|---|
String | STR[5] | holding-register:10 | 5 word = 10 byte ASCII |
String | STR[16] | holding-register:100:STRING[16] | PLC4j 표기 |
Formula 활용
| 용도 | data_type | fomula | 비고 |
|---|---|---|---|
| 정수 raw → 소수 | Float | ${VALUE}*0.1 | 압력/온도 raw 스케일 |
| Word swap 보정 | Float | ${VALUE}*1.0 | 32-bit float byte order 가 다른 슬레이브용. 대안: :REAL_LSWORD_FIRST |
| RH/Temp combo (한 워드 두 값) | Float | ${VALUE}/256 | 상위 8 bit 만 |
curl 종합 예제
curl -X POST http://<edge-host>/api/v1/opc \
-H "Content-Type: application/json" \
-d '{
"opc_id": "OPC_MB_FULL",
"opc_type": "MODBUS",
"opc_name": "Modbus Full",
"opc_agent_ip": "192.168.0.50",
"opc_agent_port": "502",
"site_id": "SITE_00001",
"auto_collect": true,
"timecycle": 1000,
"options": { "request-timeout": "5000" },
"tag_list": [
{"tag_id":"OPC_MB_FULL_T01", "tag_name":"Coil1", "plc_address":"coil:1", "data_type":"Boolean"},
{"tag_id":"OPC_MB_FULL_T02", "tag_name":"DI1", "plc_address":"discrete-input:1", "data_type":"Boolean"},
{"tag_id":"OPC_MB_FULL_T03", "tag_name":"HRSigned", "plc_address":"holding-register:1", "data_type":"Integer"},
{"tag_id":"OPC_MB_FULL_T04", "tag_name":"HRUnsigned", "plc_address":"holding-register:2:UINT", "data_type":"Integer", "format":"UI"},
{"tag_id":"OPC_MB_FULL_T05", "tag_name":"DInt", "plc_address":"holding-register:3:DINT", "data_type":"Integer", "format":"DW"},
{"tag_id":"OPC_MB_FULL_T06", "tag_name":"Temp", "plc_address":"holding-register:5:REAL", "data_type":"Float", "format":"REAL"},
{"tag_id":"OPC_MB_FULL_T07", "tag_name":"BatchName", "plc_address":"holding-register:10:STRING[10]", "data_type":"String", "format":"STR[10]"},
{"tag_id":"OPC_MB_FULL_T08", "tag_name":"PressScale", "plc_address":"holding-register:7:REAL", "data_type":"Float", "format":"REAL", "fomula":"${VALUE}*0.1"}
]
}'