PyModbus
PyModbusDocs

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:

TypeAccessSizeAddress RangeCommon Use
CoilsRead/Write1 bit00001-09999Outputs, relays
Discrete InputsRead only1 bit10001-19999Switches, sensors
Input RegistersRead only16 bit30001-39999Measurements
Holding RegistersRead/Write16 bit40001-49999Configuration

PyModbus uses 0-based addressing. Coil 00001 = address 0. Register 40001 = address 0. Register 40100 = address 99.

Function Codes

CodeNamePyModbus Method
0x01Read Coilsread_coils()
0x02Read Discrete Inputsread_discrete_inputs()
0x03Read Holding Registersread_holding_registers()
0x04Read Input Registersread_input_registers()
0x05Write Single Coilwrite_coil()
0x06Write Single Registerwrite_register()
0x0FWrite Multiple Coilswrite_coils()
0x10Write Multiple Registerswrite_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 bytes

Modbus RTU

[Slave Address][Function Code][Data][CRC]
    1 byte         1 byte     n bytes 2 bytes

Modbus ASCII

[:][Slave Address][Function Code][Data][LRC][CR][LF]
 1      2 chars       2 chars    n chars 2   1   1

Exception Codes

When a device rejects a request, it returns an exception code:

CodeMeaningFix
01Illegal FunctionDevice doesn't support this function code
02Illegal Data AddressAddress doesn't exist on this device
03Illegal Data ValueValue out of range
04Slave Device FailureInternal device error
05AcknowledgeRequest accepted, still processing
06Slave Device BusyTry 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 = 100

Timing 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

  1. Always check the connection before reading:

    if not client.connect():
        print("Cannot connect")
        exit(1)
  2. 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)
  3. Use context managers to guarantee cleanup:

    with ModbusTcpClient('192.168.1.100') as client:
        result = client.read_holding_registers(0, 10, slave=1)