Data model configuration
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 are used to mirror the physical device.
SimDevice defines device parameters and a list of SimData. The
list of SimData can be added as shared registers or as 4 separate blocks as defined in modbus.
SimDevice are used to simulate a single device, while a list of
SimDevice simulates a multipoint line (rs485 line) or a serial forwarder.
A server consist of communication parameters and a list of SimDevice
Usage examples
#!/usr/bin/env python3
"""Pymodbus server datamodel examples.
This file shows examples of how to configure the datamodel for the server/simulator.
There are different examples showing the flexibility of the datamodel.
"""
from pymodbus.constants import DataType
from pymodbus.simulator import SimData, SimDevice
def define_datamodel():
"""Define register groups.
Coils and direct inputs are modeled as bits representing a relay in the device.
There are no real difference between coils and direct inputs, but historically
they have been divided. Please be aware the coils and direct inputs are addressed differently
in shared vs non-shared models.
- In a non-shared model the address is the bit directly.
It can be thought of as if 1 register == 1 bit.
- In a shared model the address is the register containing the bits.
1 register == 16bit, so a single bit CANNOT be addressed directly.
Holding registers and input registers are modeled as int/float/string representing a sensor in the device.
There are no real difference between holding registers and input registers, but historically they have
been divided.
Please be aware that 1 sensor might be modeled as several register because it needs more than
16 bit for accuracy (e.g. a INT32).
"""
# SimData can be instantiated with positional or optional parameters:
assert SimData(
5, 10, 17, DataType.REGISTERS
) == SimData(
address=5, values=17, count=10, datatype=DataType.REGISTERS
)
# Define a group of coils/direct inputs non-shared (address=15..31 each 1 bit)
#block1 = SimData(address=15, count=16, values=True, datatype=DataType.BITS)
# Define a group of coils/direct inputs shared (address=15..31 each 16 bit)
#block2 = SimData(address=15, count=16, values=0xFFFF, datatype=DataType.BITS)
# Define a group of holding/input registers (remark NO difference between shared and non-shared)
#block3 = SimData(10, 1, 123.4, datatype=DataType.FLOAT32)
#block4 = SimData(17, count=5, values=123, datatype=DataType.INT64)
block5 = SimData(27, 1, "Hello ", datatype=DataType.STRING)
block_def = SimData(0, count=1000, datatype=DataType.REGISTERS, default=True)
# SimDevice can be instantiated with positional or optional parameters:
assert SimDevice(
5,
[block_def, block5],
) == SimDevice(
id=5, type_check=False, registers=[block_def, block5]
)
# SimDevice can define either a shared or a non-shared register model
SimDevice(id=1, type_check=False, registers=[block_def, block5])
#SimDevice(2, False,
# block_coil=[block1],
# block_direct=[block1],
# block_holding=[block2],
# block_input=[block3, block4])
# Remark: it is legal to reuse SimData, the object is only used for configuration,
# not for runtime.
# id=0 in a SimDevice act as a "catch all". Requests to an unknown id is executed in this SimDevice.
#SimDevice(0, block_shared=[block2])
def main():
"""Combine setup and run."""
define_datamodel()
if __name__ == "__main__":
main()
Class definitions
- class pymodbus.constants.DataType(value)
Register types, used to define of a group of registers.
This is the types pymodbus recognizes, actually the modbus standard do NOT define e.g. INT32, but since nearly every device contain e.g. values of type INT32, it is available in pymodbus, with automatic conversions to/from registers.
- INT16 = 1
1 integer == 1 register
- UINT16 = 2
1 positive integer == 1 register
- INT32 = 3
1 integer == 2 registers
- UINT32 = 4
1 positive integer == 2 registers
- INT64 = 5
1 integer == 4 registers
- UINT64 = 6
1 positive integer == 4 register
- FLOAT32 = 7
1 float == 2 registers
- FLOAT64 = 8
1 float == 4 registers
- STRING = 9
1 string == (len(string) / 2) registers
- BITS = 10
16 bits == 1 register
- REGISTERS = 11
Registers == 2 bytes (identical to UINT16)
- class pymodbus.simulator.SimData(address: int, count: int = 1, values: int | float | str | bytes | list[int | float | str | bytes | bool] = 0, datatype: DataType = DataType.REGISTERS, action: Callable[[int, int, list[int]], Awaitable[list[int] | ExceptionResponse]] | None = None, readonly: bool = False, invalid: bool = False, default: bool = False, register_count: int = -1, type_size: int = -1)
Bases:
objectConfigure a group of continuous identical values/registers.
Examples:
SimData( address=100, count=5, value=12345678 datatype=DataType.INT32 ) SimData( address=100, value=[1, 2, 3, 4, 5] datatype=DataType.INT32 )
Each SimData defines 5 INT32 in total 10 registers (address 100-109)
SimData( address=100, count=17, value=True datatype=DataType.BITS ) SimData( address=100, value=[0xffff, 1] datatype=DataType.BITS )
Each SimData defines 17 BITS (coils), with value True.
In block mode (CO and DI) addresses are 100-116 (each 1 bit)
In shared mode BITS are stored in registers (16bit is 1 register), the address refer to the register, addresses are 100-101 (with register 101 being padded with 15 bits set to False)
SimData( address=0, count=1000, value=0x1234 datatype=DataType.REGISTERS )
Defines a range of addresses 0..999 each with the value 0x1234.
- address: int
Address of first register, starting with 0 (identical to the requests)
- count: int = 1
Count of datatype e.g.
count=3 datatype=DataType.REGISTERS is 3 registers.
count=3 datatype=DataType.INT32 is 6 registers.
count=1 (default), value=”ABCD” is 2 registers
Cannot be used if value is a list or datatype is DataType.STRING
- values: int | float | str | bytes | list[int | float | str | bytes | bool] = 0
Value/Values of datatype, will automatically be converted to registers, according to datatype.
- action: Callable[[int, int, list[int]], Awaitable[list[int] | ExceptionResponse]] | None = None
Tip
use functools.partial to add extra parameters if needed.
- readonly: bool = False
Mark register(s) as readonly.
- invalid: bool = False
Mark register(s) as invalid. remark only to be used with address= and count=
- default: bool = False
Use as default for undefined registers Define legal register range as:
address= <= legal addresses <= address= + count=
remark only to be used with address= and count=
- register_count: int = -1
The following are internal variables
- type_size: int = -1
- class pymodbus.simulator.SimDevice(id: int, registers: list[SimData], offset_address: tuple[int, int, int, int] = (-1, -1, -1, -1), type_check: bool = False)
Bases:
objectConfigure a device with parameters and registers.
Registers are always defined as one block.
Some old devices uses 4 distinct blocks instead of 1 block, to support these devices, define 1 large block consisting of the 4 blocks and use the offset_*= parameters.
When using distinct blocks, coils and direct inputs are addressed differently, each register represent 1 coil/relay.
Device with shared registers:
SimDevice( id=1, registers=[SimData(...)] )
Device with non-shared registers:
SimDevice( id=1, registers=[SimData(...)], non_shared_mode=True, offset_coil=0, offset_direct=10, offset_holding=20, offset_input=30, )
Meaning registers:
0-9 are coils
10-19 are relays
20-29 are holding registers
30-.. are input registers
A server can contain either a single
SimDeviceor list ofSimDeviceto simulate a multipoint line.Warning
each block is sorted by address !!
- id: int
Address/id of device
Default 0 means accept all devices, except those specifically defined.
- offset_address: tuple[int, int, int, int] = (-1, -1, -1, -1)
Use this for old devices with 4 blocks.
Tip
content is (coil, direct, holding, input)
- type_check: bool = False
Enforce type checking, if True access are controlled to be conform with datatypes.
Type violations like e.g. reading INT32 as INT16 are returned as ExceptionResponses, as well as being logged.