Basic Concepts
Modbus protocol fundamentals for PyModbus. Data model, function codes, addressing, frame formats, exception codes, and byte order.
Modbus is a serial communication protocol from 1979. Still widely used because it's simple and reliable.
Master/Slave Architecture
[Master/Client] ---request---> [Slave/Server]
<--response---- Master (Client): Initiates requests
- Slave (Server): Responds to requests
- One master can talk to 247 slaves
- Slaves have unique IDs (1-247)
Data Model
Modbus has 4 data types:
| Type | Access | Size | Address Range | Common Use |
|---|---|---|---|---|
| Coils | Read/Write | 1 bit | 00001-09999 | Outputs, relays |
| Discrete Inputs | Read only | 1 bit | 10001-19999 | Switches, sensors |
| Input Registers | Read only | 16 bit | 30001-39999 | Measurements |
| Holding Registers | Read/Write | 16 bit | 40001-49999 | Configuration |
PyModbus uses 0-based addressing. Coil 00001 = address 0. Register 40001 = address 0. Register 40100 = address 99.
Function Codes
| Code | Name | PyModbus Method |
|---|---|---|
| 0x01 | Read Coils | read_coils() |
| 0x02 | Read Discrete Inputs | read_discrete_inputs() |
| 0x03 | Read Holding Registers | read_holding_registers() |
| 0x04 | Read Input Registers | read_input_registers() |
| 0x05 | Write Single Coil | write_coil() |
| 0x06 | Write Single Register | write_register() |
| 0x0F | Write Multiple Coils | write_coils() |
| 0x10 | Write Multiple Registers | write_registers() |
from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient('192.168.1.100')
client.connect()
# Read holding registers (FC 03)
result = client.read_holding_registers(address=0, count=10, slave=1)
# Read coils (FC 01)
result = client.read_coils(address=0, count=8, slave=1)
# Write single register (FC 06)
client.write_register(address=0, value=123, slave=1)
# Write single coil (FC 05)
client.write_coil(address=0, value=True, slave=1)
client.close()Log Modbus data automatically
TofuPilot records test results from your PyModbus scripts, tracks pass/fail rates, and generates compliance reports. Free to start.
Frame Formats
Modbus TCP
[Transaction ID][Protocol ID][Length][Unit ID][Function Code][Data]
2 bytes 2 bytes 2 bytes 1 byte 1 byte n bytesModbus RTU
[Slave Address][Function Code][Data][CRC]
1 byte 1 byte n bytes 2 bytesModbus ASCII
[:][Slave Address][Function Code][Data][LRC][CR][LF]
1 2 chars 2 chars n chars 2 1 1Exception Codes
When a device rejects a request, it returns an exception code:
| Code | Meaning | Fix |
|---|---|---|
| 01 | Illegal Function | Device doesn't support this function code |
| 02 | Illegal Data Address | Address doesn't exist on this device |
| 03 | Illegal Data Value | Value out of range |
| 04 | Slave Device Failure | Internal device error |
| 05 | Acknowledge | Request accepted, still processing |
| 06 | Slave Device Busy | Try again later |
result = client.read_holding_registers(9999, 10, slave=1)
if result.isError():
print(f"Exception code: {result.exception_code}")Address Mapping
Device manuals use 1-based addresses with a prefix. PyModbus uses 0-based addresses.
# Manual says "Temperature at register 40101"
# WRONG:
result = client.read_holding_registers(40101, 1, slave=1)
# RIGHT: subtract 40001
result = client.read_holding_registers(100, 1, slave=1) # 40101 - 40001 = 100Timing Limits
TCP
- Default timeout: 3 seconds
- Max registers per read: 125
- Max coils per read: 2000
RTU
- Inter-frame delay: 3.5 character times
- Max frame size: 256 bytes
- Response timeout depends on baud rate
Endianness (Byte Order)
Different devices use different byte orders for multi-register values (32-bit floats, 32-bit integers):
from pymodbus.payload import BinaryPayloadDecoder
from pymodbus.constants import Endian
# Read 2 registers for a 32-bit float
result = client.read_holding_registers(0, 2, slave=1)
# Try different byte/word order if the value looks wrong
decoder = BinaryPayloadDecoder.fromRegisters(
result.registers,
byteorder=Endian.BIG, # or Endian.LITTLE
wordorder=Endian.BIG # or Endian.LITTLE
)
value = decoder.decode_32bit_float()Practical Tips
-
Always check the connection before reading:
if not client.connect(): print("Cannot connect") exit(1) -
Always check for errors on every read:
result = client.read_holding_registers(0, 10, slave=1) if result.isError(): print(f"Error: {result}") exit(1) -
Use context managers to guarantee cleanup:
with ModbusTcpClient('192.168.1.100') as client: result = client.read_holding_registers(0, 10, slave=1)