Skip to main content

Modbus TCP / UDP Driver

Overview

PLC communication based on the Modbus industrial standard (Modbus-IDA / IEC 61158-6-12).

ItemValue
opc_typeMODBUS (TCP) / MODBUSUDP (UDP)
Implementation classplantpulse.driver.protocol.modbus.ModbusTCPDriver / ModbusUDPDriver
LibraryIn-house native (ModbusTcp / ModbusUdp in plantpulse-plc-protocol)
read
write✅ (coil / holding-register)
SecurityNone (per the Modbus standard)
2026-07 Native Migration (TCP + UDP)

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

FieldMeaningExample
opc_agent_ipModbus slave IP192.168.0.50
opc_agent_portModbus port502 (TCP default)
timecyclePolling interval (ms)1000
options.request-timeoutRequest timeout (ms)5000
unit-identifier

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).

NotationMeaning
holding-register:1Holding Register 1 (16-bit)
holding-register:1:UINT16-bit unsigned
holding-register:1:DINT32-bit signed (2 registers)
holding-register:1:REAL32-bit float (2 registers)
holding-register:100:STRING[10]ASCII string from 100 to 109
coil:1Coil, 1 bit
discrete-input:1Discrete input, 1 bit
input-register:1Input 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_typeedge formatPLC4j addr mappingNotes
Boolean(empty) / BOOLcoil:N or :BOOL1 bit
Integer / Short(empty):INT16-bit signed
UIntegerUI / UWORD:UINT16-bit unsigned
Integer (32)DW:DINT32-bit signed (2 reg)
UInteger (32)UDW:UDINT32-bit unsigned
FloatREAL:REAL32-bit IEEE 754
DoubleLREAL:LREAL64-bit IEEE 754
StringSTR[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 / DINT use MSWord-first 2 registers; everything else is a single 16-bit register. The caller passes the value as ProtocolAddress.value.
  • discrete-input / input-register are read-only by protocol — write is not possible.

Common Errors and Fixes

Message / SymptomCauseFix
PLC_READ_TIMEOUT_EXCEPTIONSlave response delayed or disconnectedCheck cabling/firewall/port (502). Increase request-timeout
PLC_READ_RUNTIME_EXCEPTION: Invalid PLC4j addressTypo in the address notationVerify the holding-register:N format; watch for a missing :
Only 0 is returnedRegister type mismatch (input vs holding)Check the slave manual for whether it is fc 03/04/01/02
32-bit value swap issuebyte/word orderIf 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_typeformatplc_address exampleNotes
Boolean(empty)coil:1FC 01 (Read Coils)
Boolean(empty)discrete-input:1FC 02 (Read Discrete Inputs)
BooleanBIN[3]holding-register:1Reads the word, then extracts bit 3

16-bit Integer

data_typeformatplc_address exampleNotes
Integer(empty)holding-register:1signed INT 16
Integer(empty)holding-register:1:INTSame (PLC4j notation)
IntegerUIholding-register:1:UINTunsigned 16
Integer(empty)input-register:1input register (FC 04)

32-bit Integer

data_typeformatplc_address exampleNotes
IntegerDWholding-register:1:DINTsigned 32 (2 registers)
IntegerUDWholding-register:1:UDINTunsigned 32
IntegerUDINTholding-register:132-bit via explicit format

Floating Point

data_typeformatplc_address exampleNotes
FloatREALholding-register:132-bit float (2 registers)
FloatREALholding-register:1:REALSame notation
DoubleLREALholding-register:1:LREAL64-bit double (4 registers)

String

data_typeformatplc_address exampleNotes
StringSTR[5]holding-register:105 words = 10 bytes ASCII
StringSTR[16]holding-register:100:STRING[16]PLC4j notation

Using Formulas

Purposedata_typefomulaNotes
Integer raw → decimalFloat${VALUE}*0.1Raw scaling for pressure/temperature
Word swap correctionFloat${VALUE}*1.0For slaves with a different 32-bit float byte order. Alternative: :REAL_LSWORD_FIRST
RH/Temp combo (two values in one word)Float${VALUE}/256Upper 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"}
]
}'