Python Modbus Communication

Modbus is a communication standard to transfer values between computers. It is commonly used with Programmable Logic Controllers (PLCs), Human Machine Interfaces (HMIs), and other networking applications. It has several limitations that motivated the creation of newer standards such as OPC UA. Although it is an old standard, several legacy systems still support this protocol. There are several serial or network connection possibilities including RS232, RS422, RS485 (serial) or TCP/IP (network).

A Modbus server has coils, discrete inputs, input registers, and holding registers. The coils and discrete inputs are a 1 or 0 (on or off) while the registers are 16-bit values (0-65,535 for unsigned integer). A floating point 32-bit number uses two registers, and a double precision 64-bit number uses four registers. The coil numbers start with 0 and span from 00001 to 09999, discrete input numbers start with 1 and span from 10001 to 19999, and holding register numbers start with 4 and span from 40001 to 49999.

Object type Access Size Address Space
Coil Read-write 1 bit 00001 – 09999
Discrete input Read-only 1 bit 10001 – 19999
Input register Read-only 16 bits 30001 – 39999
Holding register Read-write 16 bits 40001 – 49999

The coils and discrete inputs hold 1 bit each as a 0 or 1 (off/on). The input registers and holding registers have 16 bits each. A 16-bit unsigned integer can store integer values between 0 and 65,535 while a signed integer uses an extra bit to store the positive or negative sign and can store values between -32,768 to 32,767. A 32-bit (7 digits of accuracy) or 64-bit (14 digits of accuracy) number can also be sent, and the values are sent across multiple 16-bit registers with 2 registers for 32-bit numbers and 4 registers for 64-bit numbers.

Install pymodbus

The pymodbus Python package is a full-featured set of methods that includes a Modbus server and client. Install the pymodbus package with pip from the PyPI repository. The code in these examples use version 3.0.2.

pip install pymodbus==3.0.2

There are several example applications in the pymodbus documentation to help build an application. Modbus includes a server (formerly called slave) that contains the registers and a client (formerly called master) that reads and writes to the server. There may be multiple clients that read and write values to the server. The client and server may also be on the same computer.

Modbus Server (Run First)

This example code is a Modbus server and client that communicate using pymodbus. The server starts with the following actions:

  • Import required packages
  • Start debug or error logging
  • Start server
    • 200 coils
    • 200 digital inputs
    • 200 input registers
    • 200 holding registers
  • Identify server
  • Continuously run server

The code runs the server on localhost (IP Address: 127.0.0.1) with port 502. Other common ports over TCP/IP are ftp (port 21), ssh (port 22), http (port 80), and https (port 443). Run the Modbus server before running the client because the client needs a Modbus server to store or retrieve values.

# Modbus server (TCP)
from pymodbus.server import StartTcpServer
from pymodbus.device import ModbusDeviceIdentification
from pymodbus.datastore import ModbusSequentialDataBlock
from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext

def run_async_server():
    nreg = 200
    # initialize data store
    store = ModbusSlaveContext(
        di=ModbusSequentialDataBlock(0, [15]*nreg),
        co=ModbusSequentialDataBlock(0, [16]*nreg),
        hr=ModbusSequentialDataBlock(0, [17]*nreg),
        ir=ModbusSequentialDataBlock(0, [18]*nreg))
    context = ModbusServerContext(slaves=store, single=True)

    # initialize the server information
    identity = ModbusDeviceIdentification()
    identity.VendorName = 'APMonitor'
    identity.ProductCode = 'APM'
    identity.VendorUrl = 'https://apmonitor.com'
    identity.ProductName = 'Modbus Server'
    identity.ModelName = 'Modbus Server'
    identity.MajorMinorRevision = '3.0.2'

    # TCP Server
    StartTcpServer(context=context, host='localhost',\
                   identity=identity, address=("127.0.0.1", 502))

if __name__ == "__main__":
    print('Modbus server started on localhost port 502')
    run_async_server()

Modbus Client (Coil Write/Read)

With the Modbus server running, test a simple client that reads and writes to a coil (bit).

from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient('127.0.0.1')
client.write_coil(1, True)
result = client.read_coils(1,1)
print(result.bits[0])
client.close()

Modbus Client (Holding Registers Write/Read)

The client connects to the Modbus server and writes 5 values to integer holding registers 40001 to 40005. The client runs for 10 cycles to write and read values. Note that integer values are stored in the registers, so the numbers lose the additional 0.1 when the floating-point number is converted to an integer.

# Modbus client
from pymodbus.client import ModbusTcpClient as ModbusClient
from pymodbus.constants import Endian
from pymodbus.payload import BinaryPayloadBuilder
from pymodbus.payload import BinaryPayloadDecoder
import time

print('Start Modbus Client')
client = ModbusClient(host='127.0.0.1', port=502)
reg=0; address=0

# initialize data
data = [0.1,1.1,2.1,3.1,4.1]

for i in range(10):
   print('-'*5,'Cycle ',i,'-'*30)
   time.sleep(1.0)

   # increment data by one
   for i,d in enumerate(data):
      data[i] = d + 1

   # write holding registers (40001 to 40005)
   print('Write',data)
   builder = BinaryPayloadBuilder(byteorder=Endian.Big,\
                                  wordorder=Endian.Little)
   for d in data:
      builder.add_16bit_int(int(d))
   payload = builder.build()
   result  = client.write_registers(int(reg), payload,\
              skip_encode=True, unit=int(address))

   # read holding registers
   rd = client.read_holding_registers(reg,len(data)).registers
   print('Read',rd)

client.close()

Write Holding Registers

The pymodbus package includes several potential variable types are added to a builder object.

builder.add_string('abcdefgh')
builder.add_bits([0, 1, 0, 1, 1, 0, 1, 0])
builder.add_8bit_int(-0x12)
builder.add_8bit_uint(0x12)
builder.add_16bit_int(-0x5678)
builder.add_16bit_uint(0x1234)
builder.add_32bit_int(-0x1234)
builder.add_32bit_uint(0x12345678)
builder.add_16bit_float(12.34)
builder.add_16bit_float(-12.34)
builder.add_32bit_float(22.34)
builder.add_32bit_float(-22.34)
builder.add_64bit_int(-0xDEADBEEF)
builder.add_64bit_uint(0x12345678DEADBEEF)
builder.add_64bit_uint(0x12345678DEADBEEF)
builder.add_64bit_float(123.45)
builder.add_64bit_float(-123.45)

Read 32-bit or 64-bit Holding Registers

Reading registers that occupy 2 or 4 integer 16-bit registers requires decoding. Below is an example of how to create a 32-bit floating point number from two registers r1 and r2 using the struct package.

import struct
value = struct.unpack('f',struct.pack('<HH',int(r1),int(r2)))[0]

Batch Read and Write

Modbus read or write can transfer up to 2008 coils / discrete inputs (1 bit each) or 130 registers (16 bits each) with each transfer. For transfers larger than this, use multiple reads and writes. Although Modbus has several limitations, it is a reliable communication protocol that is actively used for supervisory control and data acquisition (SCADA) and control systems (PLC and DCS).


✅ Knowledge Check

1. Which of the following statements regarding Modbus communication is true?

A. Modbus has only one type of register, the holding register.
Incorrect. Modbus has multiple types of registers such as coils, discrete inputs, input registers, and holding registers.
B. Coils in a Modbus server store 16-bit values.
Incorrect. Coils in a Modbus server store only 1-bit values (either 0 or 1).
C. Modbus servers cannot communicate over TCP/IP.
Incorrect. Modbus can communicate over several serial or network connections including TCP/IP.
D. A 32-bit floating-point number in Modbus uses two 16-bit registers.
Correct. A 32-bit floating point number indeed uses two 16-bit registers in Modbus.

2. When using the pymodbus Python package, how can you create a 32-bit floating point number from two registers r1 and r2?

A. value = (r1 << 16) + r2
Incorrect. This method would shift the bits of r1 and combine them with r2, but this doesn't convert them to a 32-bit floating point number.
B. value = r1 + r2 * 0.0001
Incorrect. This method tries to scale r2 and add it to r1, but this is not how floating point representation works in Modbus.
C. value = struct.unpack('f',struct.pack('<HH',int(r1),int(r2)))[0]
Correct. This method uses the struct package to correctly create a 32-bit floating point number from two 16-bit registers.
D. value = struct.pack('f',int(r1),int(r2))[0]
Incorrect. This method incorrectly attempts to pack r1 and r2 directly into a float, which is not the correct approach.