Modbus TCP / UDP Driver
Overview
PLC communication based on the Modbus industrial standard (Modbus-IDA / IEC 61158-6-12).
| Item | Value |
|---|---|
opc_type | MODBUS (TCP) / MODBUSUDP (UDP) |
| Implementation class | plantpulse.driver.protocol.modbus.ModbusTCPDriver / ModbusUDPDriver |
| Library | In-house native (ModbusTcp / ModbusUdp in plantpulse-plc-protocol) |
| read | ✅ |
| write | ✅ (coil / holding-register) |
| Security | None (per the Modbus standard) |
Both Modbus TCP/UDP have dropped the legacy PLC4j delegation and been rewritten as the native ModbusTcp / ModbusUdp in plantpulse-plc-protocol (with the shared base AbstractNativeModbusDriver) — the same pattern as FINS / MELSEC / AB-ETH. UDP uses MBAP-over-UDP (a single datagram = a single ADU), and the address, format, and write layers are identical to TCP. The address syntax (holding-register:N[:TYPE]) preserves the PLC4X modbus4x notation as is.
OPC Registration Form
| Field | Meaning | Example |
|---|---|---|
opc_agent_ip | Modbus slave IP | 192.168.0.50 |
opc_agent_port | Modbus port | 502 (TCP default) |
timecycle | Polling interval (ms) | 1000 |
options.request-timeout | Request timeout (ms) | 5000 |
In Modbus TCP the slave is normally identified by IP + port, so unit-identifier (alias unit-id) defaults to 1.
Set it to 1–247 when multiple slaves sit behind a serial-to-TCP gateway.
Tag plc_address Format
Address notation (<area>:<address> + :<datatype> + [<count>]) — the PLC4X modbus4x syntax is preserved as is.
Addresses are 1-based (converted to 0-based on the wire; holding-register:5 → wire register 4).
| Notation | Meaning |
|---|---|
holding-register:1 | Holding Register 1 (16-bit) |
holding-register:1:UINT | 16-bit unsigned |
holding-register:1:DINT | 32-bit signed (2 registers) |
holding-register:1:REAL | 32-bit float (2 registers) |
holding-register:100:STRING[10] | ASCII string from 100 to 109 |
coil:1 | Coil, 1 bit |
discrete-input:1 | Discrete input, 1 bit |
input-register:1 | Input register, 16-bit |
For detailed notation rules, see the PLC4j Modbus documentation.
data_type / format Mapping
The Edge format maps directly to the PLC4j type suffix.
edge data_type | edge format | PLC4j addr mapping | Notes |
|---|---|---|---|
| Boolean | (empty) / BOOL | coil:N or :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 characters |
If format starts with STR[N], REAL[N], or UDINT[N], it is handled as an array (readWordValues).
Write (Native — common to TCP/UDP)
ModbusTCPDriver / ModbusUDPDriver support write (isWriteSupported()=true):
coil:N→ single coil write (Boolean).holding-register:N[:TYPE]→ register write per datatype.REAL/DINTuse MSWord-first 2 registers; everything else is a single 16-bit register. The caller passes the value asProtocolAddress.value.discrete-input/input-registerare read-only by protocol — write is not possible.
Common Errors and Fixes
| Message / Symptom | Cause | Fix |
|---|---|---|
PLC_READ_TIMEOUT_EXCEPTION | Slave response delayed or disconnected | Check cabling/firewall/port (502). Increase request-timeout |
PLC_READ_RUNTIME_EXCEPTION: Invalid PLC4j address | Typo in the address notation | Verify the holding-register:N format; watch for a missing : |
| Only 0 is returned | Register type mismatch (input vs holding) | Check the slave manual for whether it is fc 03/04/01/02 |
| 32-bit value swap issue | byte/word order | If the slave is little-endian, change format to :UDINT_LSWORD_FIRST or similar (PLC4j option) |
curl Registration Example (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"
}
]
}'
For UDP, change to opc_type: "MODBUSUDP"; the port is usually 502 or as configured on the slave.
Value read:
curl -s http://<edge-host>/api/v1/tag/OPC_MB_HVAC_TAG_00001/value | jq
Example Collection (by Data Type)
Coil / Discrete (Boolean)
data_type | format | plc_address example | Notes |
|---|---|---|---|
Boolean | (empty) | coil:1 | FC 01 (Read Coils) |
Boolean | (empty) | discrete-input:1 | FC 02 (Read Discrete Inputs) |
Boolean | BIN[3] | holding-register:1 | Reads the word, then extracts bit 3 |
16-bit Integer
data_type | format | plc_address example | Notes |
|---|---|---|---|
Integer | (empty) | holding-register:1 | signed INT 16 |
Integer | (empty) | holding-register:1:INT | Same (PLC4j notation) |
Integer | UI | holding-register:1:UINT | unsigned 16 |
Integer | (empty) | input-register:1 | input register (FC 04) |
32-bit Integer
data_type | format | plc_address example | Notes |
|---|---|---|---|
Integer | DW | holding-register:1:DINT | signed 32 (2 registers) |
Integer | UDW | holding-register:1:UDINT | unsigned 32 |
Integer | UDINT | holding-register:1 | 32-bit via explicit format |
Floating Point
data_type | format | plc_address example | Notes |
|---|---|---|---|
Float | REAL | holding-register:1 | 32-bit float (2 registers) |
Float | REAL | holding-register:1:REAL | Same notation |
Double | LREAL | holding-register:1:LREAL | 64-bit double (4 registers) |
String
data_type | format | plc_address example | Notes |
|---|---|---|---|
String | STR[5] | holding-register:10 | 5 words = 10 bytes ASCII |
String | STR[16] | holding-register:100:STRING[16] | PLC4j notation |
Using Formulas
| Purpose | data_type | fomula | Notes |
|---|---|---|---|
| Integer raw → decimal | Float | ${VALUE}*0.1 | Raw scaling for pressure/temperature |
| Word swap correction | Float | ${VALUE}*1.0 | For slaves with a different 32-bit float byte order. Alternative: :REAL_LSWORD_FIRST |
| RH/Temp combo (two values in one word) | Float | ${VALUE}/256 | Upper 8 bits only |
Comprehensive curl Example
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"}
]
}'