Wiki / Software

pymodbus

Python library for Modbus TCP and RTU: script reads and writes against PLC and RTU registers and coils from your own machine.

Modbus data model and function codes
unit / slave id
Coils (RW bit)
FC01 read
FC05 write
Discrete inputs (RO bit)
FC02 read
Holding regs (RW 16b)
FC03 read
FC06 write
Input regs (RO 16b)
FC04 read
Modbus exposes four separate address spaces per unit id. Coils and holding registers are writable and are the actuation surface; discrete inputs and input registers are read-only sensor data. Each space is addressed from 0 and overlaps only by convention.

pymodbus is a pure-Python library that implements the Modbus protocol over TCP and RTU. It lets you script reads and writes against PLCs, RTUs, and other industrial devices from your own machine, without a vendor tool or an engineering workstation. Where modpoll gives you a fixed CLI, pymodbus gives you the full protocol as a programmable object you can loop, fuzz, and automate.

What it is

Modbus is a 1979 industrial fieldbus protocol that never gained authentication or encryption and is still everywhere in OT. It models a device as four flat address spaces per unit id: read/write coils (single bits, e.g. a relay), read-only discrete inputs (single bits, e.g. a limit switch), read/write holding registers (16-bit words, e.g. a setpoint), and read-only input registers (16-bit sensor values). pymodbus wraps that model in a client (and a server, for building targets): you open a connection and call methods that map one-to-one onto the Modbus function codes.

Why it matters

Because Modbus has no authentication, reachability equals control. Once you can open a TCP connection to port 502, you can read every register the device exposes and, critically, write to coils and holding registers to flip outputs, change setpoints, or force a state. That is the difference between reading a sensor and actuating a process. Scripting it in Python means you can enumerate the whole map, diff it over time, and build a precise, repeatable manipulation instead of clicking through a GUI.

How to use it

pip install pymodbus
from pymodbus.client import ModbusTcpClient
c = ModbusTcpClient('192.168.1.100', port=502)
c.connect()

rr = c.read_holding_registers(0, count=10, slave=1)   # FC03
print(rr.registers)                                    # [230, 0, 1500, ...]

c.write_register(1, 1234, slave=1)                     # FC06 write one holding reg
c.write_coil(0, True, slave=1)                         # FC05 energise a coil
c.close()

The method names map straight onto function codes: read_holding_registers is FC03, read_input_registers is FC04, write_register is FC06, write_coil is FC05, and the multi-write variants are write_registers (FC16) and write_coils (FC15). Always pass the correct slave= (unit id); the wrong one silently reads a different device on the same gateway. To map an unknown target, sweep the register space in blocks and watch which reads return data versus an exception.

Pitfalls

  • A device returns a Modbus exception (illegal address, illegal function) rather than raising a Python error for many failures. Check rr.isError() before touching rr.registers, or you read a stale value.
  • The API changed across major versions. Older code uses unit=; recent pymodbus uses slave=. Match your examples to your installed version.
  • Register numbering is a trap. Documentation often lists 1-based "4xxxx" reference numbers while the wire protocol is 0-based; holding register "40001" is address 0. Off-by-one here writes the wrong register.
  • Writes are live and immediate on real plant. On an actual process, a coil write can move a physical output. Rehearse against a simulator or a lab PLC first.

What it pairs with

Find the device with nmap -p 502 first, then use pymodbus to enumerate and manipulate it. Capture the exchange in Wireshark (which has a strong Modbus/TCP dissector) to confirm exactly which function codes and addresses hit the wire. For a quick manual poke without writing code, modpoll covers the basics; reach for pymodbus when you need loops, logic, or a custom server.

Further reading