In this lesson

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 laptopSimulator (Modbus server)127.0.0.1:5020Clientmbpoll or pymodbusloopback, never leavesthe machineWeb interface of the simulator127.0.0.1:8081No RS485 cable, no gateway, no device needed
The whole exercise runs inside one laptop, so a mistyped register address cannot switch anything on in a real installation.

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.

BytesFieldValueWhat it says
00 01Transaction id1You pick it, the server echoes it back
00 00Protocol id0Always zero, this is Modbus
00 06Length61 unit id byte plus 5 PDU bytes
01Unit id1The device id this simulator answers on
03Function codeFC03Read holding registers
00 03Start address3PDU address 3, counted from zero
00 01Quantity1One register

Eleven bytes come back: 00 01 00 00 00 05 01 03 02 42 69.

BytesFieldValueWhat it says
00 01Transaction id1Copied from the request
00 00Protocol id0Copied from the request
00 05Length51 unit id byte plus 4 PDU bytes
01Unit id1Copied from the request
03Function codeFC03Unchanged, so no error
02Byte count2One register of two bytes
42 69Register value170010x4269 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.

BytesFieldValueWhat it says
00 02Transaction id2This is the second request
00 00Protocol id0Unchanged
00 03Length31 unit id byte plus 2 PDU bytes
01Unit id1Unchanged
83Exception function code131FC03 plus 0x80
02Exception code2Illegal 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

  1. Reading the deprecation warnings as failures. Version 3.15.0 logs warnings about ModbusSimulatorContext and ModbusServerContext at startup. People stop right there and begin troubleshooting a server that is working perfectly. The server is listening; carry on.

  2. Copying code from old blog posts. pymodbus stopped accepting unit= in client calls in 3.1.0, renamed the parameter to slave= in 3.3.0, and renamed that to device_id= in 3.10.0. The same 3.10.0 removed BinaryPayloadDecoder in favour of convert_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.

  3. Using ModbusSequentialDataBlock(0, ...). Since 3.13.0 pymodbus subtracts 1 from that first argument internally, so a zero becomes address -1 and raises TypeError: 0 <= address < 65535. Either start at 1, or better, use SimData and SimDevice as shown above.

  4. 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.json sets "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. 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. 2

    Start the simulator

    Run pymodbus.simulator --modbus_server server --modbus_device device and open http://127.0.0.1:8081/ in your browser to see what is in the registers.

  3. 3

    Read PDU address 3

    One register, with device_id=1. Expected: 17001, the same value on every attempt.

  4. 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. 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 with convert_from_registers.

  6. 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. 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 0x80 followed by one exception code byte, so a failed FC03 read comes back as 83 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
You read two registers and get [9, 27177]. What is that as a uint32 with the high 16 bits first?
You start the simulator on port 5020 and set the Wireshark display filter to mbtcp. Nothing shows up, while the TCP stream clearly carries traffic. Why?
You copy an example with client.read_holding_registers(0, 10, unit=1) and get an error about an unknown argument. What happened?
Which one of these can you not practise with a simulator?

Stuck on a project?

Describe what you see and where it goes wrong. We will look at it and tell you what we would do.

Further reading in the knowledge base