pymodbus
Python library for Modbus TCP and RTU: script reads and writes against PLC and RTU registers and coils from your own machine.
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 touchingrr.registers, or you read a stale value. - The API changed across major versions. Older code uses
unit=; recent pymodbus usesslave=. 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.