View all lessons
Part 1
Fundamentals
Part 2
Modbus RTU
- Lesson 5RS485 explained: A, B, common and termination20 min
- Lesson 6Baud rate and parity: why 8E1 is the default16 min
- Lesson 7The Modbus RTU frame byte by byte, plus CRC18 min
- Lesson 8Multiple Modbus devices on one RS485 bus16 min
- Lesson 9Read your first Modbus device with mbpoll22 min
- Lesson 10Modbus RTU troubleshooting: symptom to cause20 min
Part 3
Modbus TCP
Part 4
Advanced
- Lesson 16Writing to a Modbus device without breaking it18 min
- Lesson 17Word order and floats: same bytes, other value22 min
- Lesson 18Calculate your poll interval and bus load18 min
- Lesson 19Modbus security: the protocol will not help16 min
- Lesson 20Modbus integration: PLC, Home Assistant, cloud20 min
- Lesson 21Modbus commissioning checklist and cheat sheet18 min
Practise Modbus TCP with a free simulator
A free Modbus simulator lets you practise without hardware. Start a server on port 5020, read its registers and trigger a real exception on your own laptop.
What this lesson covers
- A server on port 5020 and your first read against 127.0.0.1
- Triggering an exception on purpose and assembling a 32-bit value
- Your own server in fifteen lines of Python, and what it cannot show
Read first: How to read a Modbus datasheet, step by step, Port 502, the MBAP header and the unit id
A Modbus simulator is a Modbus server that runs on your own laptop, so you can practise reads, exceptions and 32-bit values without owning a single device. The one in this lesson is free, installs with one command and listens on port 5020. After this lesson you have a running server, a register you read yourself, an exception you triggered on purpose, and a dataset of your own making.
Why you get further without hardware than you think
Most of what goes wrong in a Modbus TCP project is an addressing or a data type problem, and none of that needs a physical bus. A simulator gives you a real server speaking the real protocol: the same MBAP header, the same function codes, the same exception responses, byte for byte. So you can practise frame layout, the gap between a PDU address and a datasheet number, block reads, exceptions and word order, and get your client settings wrong as often as you like without touching an installation.
What it cannot give you is the physical layer: cabling, baud rate, parity, termination, reflections and bus load all stay out of reach. Hold on to that split, because it tells you which half of this course a laptop can replace. Once real devices are on the network, scanning a Modbus network is the reference for finding them and their addresses.
Installing and starting the simulator
The simulator ships with pymodbus, which is BSD-3 licensed and free to use, including commercially. It needs Python 3.10 or newer.
python -m pip install "pymodbus[serial,simulator]==3.15.0"
On Linux and macOS, create a virtual environment first, otherwise pip refuses with "externally managed environment":
python3 -m venv ~/modbus-course
source ~/modbus-course/bin/activate
Then start it:
pymodbus.simulator --modbus_server server --modbus_device device
You now have a Modbus TCP server on 0.0.0.0:5020 and a web interface on http://127.0.0.1:8081/. Port 5020 instead of the registered port 502 is deliberate: on most systems a port below 1024 needs administrator rights, and nothing in the protocol changes when you move it. Your client has to be told about that port, though, and so does Wireshark.
Your first read against 127.0.0.1
Ask the simulator for one holding register at PDU address 3. Twelve bytes go out: 00 01 00 00 00 06 01 03 00 03 00 01.
| Bytes | Field | Value | What it says |
|---|---|---|---|
00 01 | Transaction id | 1 | You pick it, the server echoes it back |
00 00 | Protocol id | 0 | Always zero, this is Modbus |
00 06 | Length | 6 | 1 unit id byte plus 5 PDU bytes |
01 | Unit id | 1 | The device id this simulator answers on |
03 | Function code | FC03 | Read holding registers |
00 03 | Start address | 3 | PDU address 3, counted from zero |
00 01 | Quantity | 1 | One register |
Eleven bytes come back: 00 01 00 00 00 05 01 03 02 42 69.
| Bytes | Field | Value | What it says |
|---|---|---|---|
00 01 | Transaction id | 1 | Copied from the request |
00 00 | Protocol id | 0 | Copied from the request |
00 05 | Length | 5 | 1 unit id byte plus 4 PDU bytes |
01 | Unit id | 1 | Copied from the request |
03 | Function code | FC03 | Unchanged, so no error |
02 | Byte count | 2 | One register of two bytes |
42 69 | Register value | 17001 | 0x4269 written in decimal |
That is the rule in one frame: a normal response repeats the function code, then a byte count, then two bytes per register.
from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient("127.0.0.1", port=5020)
client.connect()
rr = client.read_holding_registers(address=3, count=1, device_id=1)
print(rr.registers)
Note device_id=1. That is the unit id from the MBAP header, and this simulator answers on device 1. For a device wired straight to the network the standard recommends 0xFF and also accepts 0x00; behind a gateway the field carries the real server address between 1 and 247, which is the subject of the MBAP header and the unit id. The value 17001 at PDU address 3 comes from the setup.json shipped with the simulator, with its action explicitly set to none, so it does not move between reads.
From the command line, mbpoll 1.5.4 does the same read. It is GPLv3 and also free to use commercially, and -0 is what tells it that -r 3 is a PDU address and not a datasheet number.
mbpoll -m tcp -p 5020 -a 1 -t 4 -r 3 -c 1 -0 -1 127.0.0.1
Triggering an exception on purpose
Change exactly one thing in that request, the start address, and read PDU address 1 instead. This dataset has no readable register there, so the server refuses. Nine bytes come back: 00 02 00 00 00 03 01 83 02.
| Bytes | Field | Value | What it says |
|---|---|---|---|
00 02 | Transaction id | 2 | This is the second request |
00 00 | Protocol id | 0 | Unchanged |
00 03 | Length | 3 | 1 unit id byte plus 2 PDU bytes |
01 | Unit id | 1 | Unchanged |
83 | Exception function code | 131 | FC03 plus 0x80 |
02 | Exception code | 2 | Illegal data address |
An exception response PDU is always exactly two bytes: the original function code with its top bit set, then one exception code. 0x03 plus 0x80 is 0x83, which pymodbus prints in decimal as function_code=131 with exception_code=2. Exception 02 is the one you meet most often in the field, and it nearly always means the off-by-one between a datasheet number and a PDU address, or a read that runs past the end of a block. The other eight codes are in function codes and exception codes.
Assembling a 32-bit value
PDU address 4 holds a value that does not fit in one register, so you ask for two. The response carries four data bytes: 00 03 00 00 00 07 01 03 04 00 09 6A 29. Length is 7 this time, because the PDU grew to six bytes, and the byte count field says 04.
Those four bytes arrive as the register pair [9, 27177]. The first register carries the high 16 bits, so 9 times 65536 is 589824, plus 27177 gives 617001. Read 00 09 6A 29 as one number and you land on that same 617001. Do not do the arithmetic by hand every time:
rr = client.read_holding_registers(address=4, count=2, device_id=1)
value = client.convert_from_registers(rr.registers, data_type=client.DATATYPE.UINT32)
Name the data type, never guess it. convert_from_registers also takes word_order, which defaults to big. Flip it and the same four bytes turn into a completely different number, which is the subject of data types and word order. One warning about this dataset: PDU address 6 holds a float32 that starts at 404.17 and climbs on every read, so do not write it down as a fixed value. The register at address 3 and the pair at address 4 stay put.
Your own server with your own registers
The shipped dataset is useful for a first hour. After that you want registers that match the device you are actually working on, and fifteen lines is enough for that.
from pymodbus.server import StartTcpServer
from pymodbus.simulator import DataType, SimData, SimDevice
block = SimData(0, values=[1234, 5678, 42], datatype=DataType.REGISTERS)
rest = SimData(3, count=97, datatype=DataType.REGISTERS)
device = SimDevice(1, simdata=[block, rest])
StartTcpServer(context=device, address=("0.0.0.0", 5020))
Read PDU address 0 with a count of 3 and [1234, 5678, 42] comes back. This is how you build a practice copy of a device you are still waiting for: take three rows from the datasheet, put plausible values in SimData, and write your client or your Home Assistant Modbus configuration before the meter is on site. When the hardware arrives, only the IP address and the unit id change.
What the simulator cannot teach you
Everything electrical. There is no differential pair, so there is no termination to place, no biasing, no reflections and no cable length limit. There is no baud rate, no parity and no stop bit to get wrong, and no bus load to calculate, because loopback has no bus.
A second gap catches people out later. Real devices have limits a simulator does not: a maximum number of registers per block, a response timeout, a gateway with exceptions of its own, and a unit id that matters again once a serial line sits behind it. Modbus TCP explained covers that ground.
Common mistakes
-
Reading the deprecation warnings as failures. Version 3.15.0 logs warnings about
ModbusSimulatorContextandModbusServerContextat startup. People stop right there and begin troubleshooting a server that is working perfectly. The server is listening; carry on. -
Copying code from old blog posts. pymodbus stopped accepting
unit=in client calls in 3.1.0, renamed the parameter toslave=in 3.3.0, and renamed that todevice_id=in 3.10.0. The same 3.10.0 removedBinaryPayloadDecoderin favour ofconvert_from_registers. An older example fails with an unknown argument or a missing class, which looks exactly like a broken installation. Check the version an example was written for before you debug your own setup. -
Using
ModbusSequentialDataBlock(0, ...). Since 3.13.0 pymodbus subtracts 1 from that first argument internally, so a zero becomes address -1 and raisesTypeError: 0 <= address < 65535. Either start at 1, or better, useSimDataandSimDeviceas shown above. -
Taking simulator behaviour for protocol behaviour. Reading two registers from PDU address 3 crosses a type boundary in this dataset and returns exception 02, because the shipped
setup.jsonsets"type exception": true. That is a choice this simulator made, not a rule of Modbus. Keep noting which observations are about the protocol and which are about the tool.
Get hands-on
Work through this once and you finish with four observations and a server of your own. Everything runs on one machine.
- 1
Install pymodbus
Run
python -m pip install "pymodbus[serial,simulator]==3.15.0". On Linux and macOS, create and activate a virtual environment first. - 2
Start the simulator
Run
pymodbus.simulator --modbus_server server --modbus_device deviceand openhttp://127.0.0.1:8081/in your browser to see what is in the registers. - 3
Read PDU address 3
One register, with
device_id=1. Expected: 17001, the same value on every attempt. - 4
Trigger an exception
Read PDU address 1. Expected: function code 131 (
0x83) with exception 02, illegal data address. Write down which two bytes that is on the wire. - 5
Turn two registers into a uint32
Read PDU address 4, two registers. Expected:
[9, 27177]. Check the sum yourself, 9 times 65536 plus 27177 is 617001, then confirm it withconvert_from_registers. - 6
Read the value that moves
Read PDU address 6, two registers, as a float32. Read twice in a row and watch it climb from 404.17, so do not record it as fixed.
- 7
Build your own server
Stop the simulator, run the fifteen lines above with three values you pick yourself, then read them back from PDU address 0 with a count of 3.
With hardware. Repeat steps 3 to 6 against a device of your own and compare. The frames have the same shape, only the values and the unit id differ. To watch those same frames travel the wire, the lesson on troubleshooting with Wireshark puts a capture next to them.
Expected result: a running server, a register you read, an exception you provoked, a uint32 you checked by hand, and a server that hands back three values of your choosing.
Summary
- A Modbus simulator is a real Modbus TCP server on your own laptop, so frames, addressing, data types and exceptions can all be practised without hardware.
- The pymodbus simulator listens on port 5020 with a web interface on port 8081, and both pymodbus and mbpoll are free to use, including commercially.
- An exception response is the original function code plus
0x80followed by one exception code byte, so a failed FC03 read comes back as83 02, which pymodbus prints as function code 131 with exception code 2. - Two registers holding 9 and 27177 make 617001 as a uint32, because the first register carries the high 16 bits: 9 times 65536 plus 27177.
- A simulator cannot show you cabling, baud rate, termination or bus load, so treat it as a substitute for the protocol chapters and never for the RS485 chapter.
Check yourself
Four questions about this lesson. Every answer comes with an explanation.
Question 1 of 4
Want to see how it works?
The ModbusCloud Gateway reads the devices from this course without you programming a single register.