Simulator

WORK IN PROGRESS, do NOT use

The simulator is a full fledged modbus server/simulator.

The purpose of the simulator is to provide support for client application test harnesses with end-to-end testing simulating real life modbus devices.

The simulator allows the user to (all automated):

  • simulate a modbus device by adding a simple configuration,

  • simulate a multipoint line, but adding multiple device configurations,

  • simulate devices that are not conforming to the protocol,

  • simulate communication problems (data loss etc),

  • test how a client handles modbus response and exceptions,

  • test a client apps correct use of the simulated device.

The web interface (activated optionally) allows the user to:

  • introduce modbus errors (like e.g. wrong length),

  • introduce communication errors (like splitting a message),

  • monitor requests/responses,

  • see/Change values online.

  • inject modbus errors like malicious a response,

  • run your test server in the cloud,

The REST API allow the test process to be automated

  • spin up a test server in your test harness,

  • set expected responses with a simple REST API command,

  • check the result with a simple REST API command,

  • test your client app in a true end-to-end fashion.

The web server uses the REST API internally, which helps to ensure that it actually works.

Data model configuration

Warning

from v3.9.0 this is available as a “normal” datastore model.

The simulator data model represent the registers and parameters of the simulated devices. The data model is defined using SimData and SimDevice before starting the server and cannot be changed without restarting the server.

SimData defines a group of continuous identical registers. This is the basis of the model, multiple SimData should be used to mirror the physical device.

SimDevice defines device parameters and a list of SimData. The list of SimData can added as shared registers or as the 4 blocks, defined in modbus. SimDevice can be used to simulate a single device, while a list of SimDevice simulates a multipoint line (simulating a rs485 line or a tcp based serial forwarder).

A server consist of communication parameters and a device or a list of devices

SimDataType is a helper class that defines legal datatypes.

SimActions is a helper class that defines built in actions.

examples/simulator_datamodel.py contains usage examples.

SimData

class pymodbus.simulator.SimData(start_register: int, value: int | float | str | bool | bytes = 0, count: int = 1, datatype: SimDataType = SimDataType.REGISTERS, action: Callable[[int, int | float | str | bool | bytes], int | float | str | bool | bytes] | None = None)

Bases: object

Configure a group of continuous identical registers.

Example:

SimData(
    start_register=100,
    count=5,
    value=-123456
    datatype=SimDataType.INT32
)

The above code defines 5 INT32, each with the value -123456, in total 20 registers.

Tip

use SimDatatype.DEFAULT to define register limits:

SimData(
    start_register=0, # First legal registers
    count=1000,       # last legal register is start_register+count-1
    value=0x1234      # Default register value
    datatype=SimDataType.DEFAULT
)

The above code sets the range of legal registers to 0..9999 all with the value 0x1234. Accessing non-defined registers will cause an exception response.

Attention

Using SimDataType.DEFAULT is a LOT more efficient to define all registers, than the other datatypes. This is because default registers are not created unless written to, whereas the registers of other datatypes are each created as objects.

action: Callable[[int, int | float | str | bool | bytes], int | float | str | bool | bytes] | None = None

Optional function to call when registers are being read/written.

Example function:

 def my_action(
    addr: int,
    value: int | float | str | bool | bytes
) -> int | float | str | bool | bytes:
     return value + 1

Tip

use functools.partial to add extra parameters if needed.

count: int = 1

Count of datatype e.g. count=3 datatype=SimdataType.INT32 is 6 registers.

SimdataType.STR is special:

  • count=1, value=”ABCD” is 2 registers

  • count=3, value=”ABCD” is 6 registers, with “ABCD” repeated 3 times.

datatype: SimDataType = 11

Datatype, used to check access and calculate register count.

Note

Default is SimDataType.REGISTERS

start_register: int

Address of first register, starting with 0.

Caution

No default, must be defined.

value: int | float | str | bool | bytes = 0

Value of datatype, to initialize the registers (repeated with count, apart from string).

Depending on in which block the object is used some value types are not legal e.g. float cannot be used to define coils.

SimDevice

class pymodbus.simulator.SimDevice(id: int = 0, type_check: bool = False, block_shared: list[SimData] | None = None, block_coil: list[SimData] | None = None, block_direct: list[SimData] | None = None, block_holding: list[SimData] | None = None, block_input: list[SimData] | None = None)

Bases: object

Configure a device with parameters and registers.

Registers can be defined as shared or as 4 separate blocks.

shared_block means all requests access the same registers, allowing e.g. coils to be read as a holding register (except if type_checking is True).

Warning

Shared mode cannot be mixed with non-shared mode !

In shared mode, individual coils/direct input cannot be addressed directly ! Instead the register address is used with count. In non-shared mode coils/direct input can be addressed directly.

Device with shared registers:

SimDevice(
    id=0,
    block_shared=[SimData(...)]
)

Device with non-shared registers:

SimDevice(
    id=0,
    block_coil=[SimData(...)],
    block_direct=[SimData(...)],
    block_holding=[SimData(...)],
    block_input=[SimData(...)],
)

A server can contain either a single SimDevice or list of SimDevice to simulate a multipoint line.

block_coil: list[SimData] | None = None

Use this block for non-shared registers (very old devices).

In this block an address is a single coil, there are no registers.

Request of type read/write_coil accesses this block.

Tip

block_coil/direct/holding/input must all be defined

block_direct: list[SimData] | None = None

Use this block for non-shared registers (very old devices).

In this block an address is a single direct relay, there are no registers.

Request of type read/write_direct_input accesses this block.

Tip

block_coil/direct/holding/input must all be defined

block_holding: list[SimData] | None = None

Use this block for non-shared registers (very old devices).

In this block an address is a register.

Request of type read/write_holding accesses this block.

Tip

block_coil/direct/holding/input must all be defined

block_input: list[SimData] | None = None

Use this block for non-shared registers (very old devices).

In this block an address is a register.

Request of type read/write_input accesses this block.

Tip

block_coil/direct/holding/input must all be defined

block_shared: list[SimData] | None = None

Use this block for shared registers (Modern devices).

Requests accesses all registers in this block.

Warning

cannot be used together with other block_* parameters!

id: int = 0

Address of device

Default 0 means accept all devices, except those defined in the same server.

Warning

A server with a single device id=0 accept all requests.

type_check: bool = False

Enforce type checking, if True access are controlled to be conform with datatypes.

Used to control that read_coils do not access a register defined as holding and visaversa

SimDataType

class pymodbus.simulator.SimDataType(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)

Bases: Enum

Register types, used to define group of registers.

This is the types pymodbus recognizes, actually the modbus standard do NOT define e.g. INT32, but since nearly every device have e.g. INT32 as part of its register map, it was decided to include it in pymodbus, with automatic conversions to/from registers.

BITS = 10

Shared mode: 16 bits == 1 register else 1 bit == 1 “register” (address)

DEFAULT = 12

Raw registers, but also sets register address limits.

Tip

It a single but special register, and therefore improves speed and memory usage compared to REGISTERS.

FLOAT32 = 7

1 float == 2 registers

FLOAT64 = 8

1 float == 4 registers

INT16 = 1

1 integer == 1 register

INT32 = 3

1 integer == 2 registers

INT64 = 5

1 integer == 4 registers

REGISTERS = 11

Raw registers

Warning

Do not use as default, since it fills the memory and block other registrations.

STRING = 9

1 string == len(string) / 2 registers

Tip

String length must be a multiple of 2 (corresponding to registers).

UINT16 = 2

1 positive integer == 1 register

UINT32 = 4

1 positive integer == 2 register2

UINT64 = 6

1 positive integer == 4 register

Simulator server

Note

This is a v4.0.0 functionality currently not available, please see the 3x simulator server.

Web frontend

Note

This is a v4.0.0 functionality currently not available, please see the 3x simulator server.

REST API

Note

This is a v4.0.0 functionality currently not available, please see the 3x simulator server.