Skip to content

Commit 2a7ab6d

Browse files
committed
SimRuntime generator.
1 parent 5cc128d commit 2a7ab6d

File tree

10 files changed

+397
-180
lines changed

10 files changed

+397
-180
lines changed

doc/source/simulator/datamodel.rst

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,7 @@ Class definitions
4040
:show-inheritance:
4141
:member-order: bysource
4242

43-
44-
Action data class examples
45-
^^^^^^^^^^^^^^^^^^^^^^^^^^
46-
47-
48-
.. autoclass:: pymodbus.simulator.SimDataMinMax
49-
:members:
50-
:undoc-members:
51-
:show-inheritance:
52-
:member-order: bysource
53-
54-
.. autoclass:: pymodbus.simulator.SimDataIncrement
43+
.. autoclass:: pymodbus.simulator.SimDevices
5544
:members:
5645
:undoc-members:
5746
:show-inheritance:

examples/server_datamodel.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def define_datamodel():
4545
#block4 = SimData(17, count=5, values=123, datatype=DataType.INT64)
4646
block5 = SimData(27, 1, "Hello ", datatype=DataType.STRING)
4747

48-
block_def = SimData(0, count=1000, datatype=DataType.REGISTERS, default=True)
48+
block_def = SimData(0, count=1000, datatype=DataType.REGISTERS)
4949

5050
# SimDevice can be instantiated with positional or optional parameters:
5151
assert SimDevice(

pymodbus/simulator/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"SimCore",
66
"SimData",
77
"SimDevice",
8+
"SimDevices",
89
"SimValueType",
910
]
1011

@@ -14,4 +15,4 @@
1415
SimData,
1516
SimValueType,
1617
)
17-
from .simdevice import SimDevice
18+
from .simdevice import SimDevice, SimDevices

pymodbus/simulator/simdata.py

Lines changed: 29 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ class SimData:
2525
SimData(
2626
address=100,
2727
count=5,
28-
value=12345678
28+
values=12345678
2929
datatype=DataType.INT32
3030
)
3131
SimData(
3232
address=100,
33-
value=[1, 2, 3, 4, 5]
33+
values=[1, 2, 3, 4, 5]
3434
datatype=DataType.INT32
3535
)
3636
@@ -40,29 +40,38 @@ class SimData:
4040
4141
SimData(
4242
address=100,
43-
count=17,
44-
value=True
43+
count=16,
44+
values=True
4545
datatype=DataType.BITS
4646
)
4747
SimData(
4848
address=100,
49-
value=[0xffff, 1]
49+
values=[True] * 16
50+
datatype=DataType.BITS
51+
)
52+
SimData(
53+
address=100,
54+
values=0xffff
55+
datatype=DataType.BITS
56+
)
57+
SimData(
58+
address=100,
59+
values=[0xffff]
5060
datatype=DataType.BITS
5161
)
5262
53-
Each SimData defines 17 BITS (coils), with value True.
63+
Each SimData defines 16 BITS (coils), with value True.
5464
55-
In block mode (CO and DI) addresses are 100-116 (each 1 bit)
65+
Value are stored in registers (16bit is 1 register), the address refer to the register.
5666
57-
In shared mode BITS are stored in registers (16bit is 1 register), the address refer to the register,
58-
addresses are 100-101 (with register 101 being padded with 15 bits set to False)
67+
**Remark** when using offsets, only bit 0 of each register is used!
5968
6069
.. code-block:: python
6170
6271
SimData(
6372
address=0,
6473
count=1000,
65-
value=0x1234
74+
values=0x1234
6675
datatype=DataType.REGISTERS
6776
)
6877
@@ -76,9 +85,10 @@ class SimData:
7685
#:
7786
#: - count=3 datatype=DataType.REGISTERS is 3 registers.
7887
#: - count=3 datatype=DataType.INT32 is 6 registers.
79-
#: - count=1 (default), value="ABCD" is 2 registers
88+
#: - count=1 datatype=DataType.STRING, values="ABCD" is 2 registers
89+
#: - count=2 datatype=DataType.STRING, values="ABCD" is 4 registers
8090
#:
81-
#: Cannot be used if value is a list or datatype is DataType.STRING
91+
#: Count cannot be used if values= is a list
8292
count: int = 1
8393

8494
#: Value/Values of datatype,
@@ -111,25 +121,6 @@ class SimData:
111121
#: **remark** only to be used with address= and count=
112122
invalid: bool = False
113123

114-
#: Use as default for undefined registers
115-
#: Define legal register range as:
116-
#:
117-
#: address= <= legal addresses <= address= + count=
118-
#:
119-
#: **remark** only to be used with address= and count=
120-
default: bool = False
121-
122-
#: The following are internal variables
123-
register_count: int = -1
124-
type_size: int = -1
125-
126-
def __check_default(self):
127-
"""Check use of default=."""
128-
if self.datatype != DataType.REGISTERS:
129-
raise TypeError("default=True only works with datatype=DataType.REGISTERS")
130-
if isinstance(self.values, list):
131-
raise TypeError("default=True only works with values=<integer>")
132-
133124
def __check_simple(self):
134125
"""Check simple parameters."""
135126
if not isinstance(self.address, int) or not 0 <= self.address <= 65535:
@@ -142,30 +133,20 @@ def __check_simple(self):
142133
raise TypeError("datatype= must by an DataType")
143134
if self.action and not (callable(self.action) and asyncio.iscoroutinefunction(self.action)):
144135
raise TypeError("action= not a async function")
145-
if self.register_count != -1:
146-
raise TypeError("register_count= is illegal")
147-
if self.type_size != -1:
148-
raise TypeError("type_size= is illegal")
149136

150137
def __post_init__(self):
151138
"""Define a group of registers."""
152139
self.__check_simple()
153-
if self.default:
154-
self.__check_default()
155-
x_datatype: type | tuple[type, type]
156140
if self.datatype == DataType.STRING:
157141
if not isinstance(self.values, str):
158142
raise TypeError("datatype=DataType.STRING only allows values=\"string\"")
159-
x_datatype, x_len = str, int((len(self.values) +1) / 2)
160-
else:
161-
x_datatype, x_len = DATATYPE_STRUCT[self.datatype]
162-
if not isinstance(self.values, list):
163-
super().__setattr__("values", [self.values])
164-
for x_value in cast(list, self.values):
165-
if not isinstance(x_value, x_datatype):
166-
raise TypeError(f"value= can only contain {x_datatype!s}")
167-
super().__setattr__("register_count", self.count * x_len)
168-
super().__setattr__("type_size", x_len)
143+
return
144+
x_datatype = DATATYPE_STRUCT[self.datatype][0]
145+
if not isinstance(self.values, list):
146+
super().__setattr__("values", [self.values])
147+
for x_value in cast(list, self.values):
148+
if not isinstance(x_value, x_datatype):
149+
raise TypeError(f"value= can only contain {x_datatype!s}")
169150

170151

171152

pymodbus/simulator/simdevice.py

Lines changed: 109 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
from __future__ import annotations
33

44
from dataclasses import dataclass
5+
from typing import cast
56

6-
from .simdata import SimData
7+
from pymodbus.constants import DATATYPE_STRUCT, DataType
78

9+
from .simdata import SimData
810

9-
OFFSET_NONE = (-1, -1, -1, -1)
1011

1112
@dataclass(order=True, frozen=True)
1213
class SimDevice:
@@ -62,70 +63,140 @@ class SimDevice:
6263
#:
6364
registers: list[SimData]
6465

65-
#: Use this for old devices with 4 blocks.
66+
#: Default SimData to be used for registers not defined.
67+
default: SimData | None = None
68+
69+
#: Define starting address for each of the 4 blocks.
6670
#:
67-
#: .. tip:: content is (coil, direct, holding, input)
68-
offset_address: tuple[int, int, int, int] = OFFSET_NONE
71+
#: .. tip:: Content (coil, direct, holding, input) in growing order.
72+
offset_address: tuple[int, int, int, int] | None = None
6973

7074
#: Enforce type checking, if True access are controlled to be conform with datatypes.
7175
#:
7276
#: Type violations like e.g. reading INT32 as INT16 are returned as ExceptionResponses,
7377
#: as well as being logged.
7478
type_check: bool = False
7579

80+
#: Change endianness.
81+
#:
82+
#: Word order is not defined in the modbus standard and thus a device that
83+
#: uses little-endian is still within the modbus standard.
84+
#:
85+
#: Byte order is defined in the modbus standard to be big-endian,
86+
#: however it is definable to test non-standard modbus devices
87+
#:
88+
#: ..tip:: Content (word_order, byte_order)
89+
endian: tuple[bool, bool] = (True, True)
90+
91+
#: Set device identity
92+
#:
93+
identity: str = "pymodbus simulator/server"
94+
7695

7796
def __check_block(self, block: list[SimData]) -> list[SimData]:
7897
"""Check block content."""
98+
if not block:
99+
return block
79100
for inx, entry in enumerate(block):
80101
if not isinstance(entry, SimData):
81102
raise TypeError(f"registers[{inx}]= is a SimData entry")
82103
block.sort(key=lambda x: x.address)
83-
return self.__check_block_entries(block)
84-
85-
def __check_block_entries(self, block: list[SimData]) -> list[SimData]:
86-
"""Check block entries."""
87104
last_address = -1
88-
if len(block) > 1 and block[1].default:
89-
temp = block[0]
90-
block[0] = block[1]
91-
block[1] = temp
92-
first = True
93105
for entry in block:
94-
if entry.default:
95-
if first:
96-
first = False
97-
continue
98-
raise TypeError("Multiple default SimData, not allowed")
99-
first = False
100-
if entry.address <= last_address:
101-
raise TypeError("SimData address {entry.address} is overlapping!")
102-
last_address = entry.address + entry.register_count -1
103-
if not block[0].default:
104-
default = SimData(address=block[0].address, count=last_address - block[0].address +1, default=True)
105-
block.insert(0, default)
106-
max_address = block[0].address + block[0].register_count -1
107-
if last_address > max_address:
108-
raise TypeError("Default set max address {max_address} but {last_address} is defined?")
109-
if len(block) > 1 and block[0].address > block[1].address:
110-
raise TypeError("Default set lowest address to {block[0].address} but {block[1].address} is defined?")
106+
last_address = self.__check_block_entries(last_address, entry)
107+
if self.default and block:
108+
first_address = block[0].address
109+
if self.default.address > first_address:
110+
raise TypeError("Default address is {self.default.address} but {first_address} is defined?")
111+
def_last_address = self.default.address + self.default.count -1
112+
if last_address > def_last_address:
113+
raise TypeError("Default address+count is {def_last_address} but {last_address} is defined?")
111114
return block
112115

113-
def __post_init__(self):
114-
"""Define a device."""
116+
def __check_block_entries(self, last_address: int, entry: SimData) -> int:
117+
"""Check block entries."""
118+
values = entry.values if isinstance(entry.values, list) else [entry.values]
119+
if entry.address <= last_address:
120+
raise TypeError("SimData address {entry.address} is overlapping!")
121+
if entry.datatype == DataType.BITS:
122+
if isinstance(values[0], bool):
123+
reg_count = int((len(values) + 15) / 16)
124+
else:
125+
reg_count = len(values)
126+
return entry.address + reg_count * entry.count -1
127+
if entry.datatype == DataType.STRING:
128+
return entry.address + len(cast(str, entry.values)) * entry.count -1
129+
register_count = DATATYPE_STRUCT[entry.datatype][1]
130+
return entry.address + register_count * entry.count -1
131+
132+
def __check_simple(self):
133+
"""Check simple parameters."""
115134
if not isinstance(self.id, int) or not 0 <= self.id <= 255:
116135
raise TypeError("0 <= id < 255")
117-
if not isinstance(self.registers, list) or not self.registers:
136+
if not isinstance(self.registers, list):
118137
raise TypeError("registers= not a list")
138+
if not self.default and not self.registers:
139+
raise TypeError("Either registers= or default= must contain SimData")
119140
if not isinstance(self.type_check, bool):
120141
raise TypeError("type_check= not a bool")
142+
if (not self.endian
143+
or not isinstance(self.endian, tuple)
144+
or len(self.endian) != 2
145+
or not isinstance(self.endian[0], bool)
146+
or not isinstance(self.endian[1], bool)
147+
):
148+
raise TypeError("endian= must be a tuple with 2 bool")
149+
if not isinstance(self.identity, str):
150+
raise TypeError("identity= must be a string")
151+
if not self.default:
152+
return
153+
if not isinstance(self.default, SimData):
154+
raise TypeError("default= must be a SimData object")
155+
if not self.default.datatype == DataType.REGISTERS:
156+
raise TypeError("default= only allow datatype=DataType.REGISTERS")
157+
158+
def __post_init__(self):
159+
"""Define a device."""
160+
self.__check_simple()
121161
super().__setattr__("registers", self.__check_block(self.registers))
122-
if self.offset_address != OFFSET_NONE:
162+
if self.offset_address is not None:
163+
if not isinstance(self.offset_address, tuple):
164+
raise TypeError("offset_address= must be a tuple")
123165
if len(self.offset_address) != 4:
124-
raise TypeError("offset_address= must have 4 addresses")
125-
reg_start = self.registers[0].address
126-
reg_end = self.registers[0].address + self.registers[0].register_count
166+
raise TypeError("offset_address= must be a tuple with 4 addresses")
167+
if self.default:
168+
reg_start = self.default.address
169+
reg_end = self.default.address + self.default.count -1
170+
else:
171+
reg_start = self.registers[0].address
172+
reg_end = self.registers[-1].address
127173
for i in range(4):
128174
if not (reg_start < self.offset_address[i] < reg_end):
129175
raise TypeError(f"offset_address[{i}] outside defined range")
130176
if i and self.offset_address[i-1] >= self.offset_address[i]:
131177
raise TypeError("offset_address= must be ascending addresses")
178+
179+
@dataclass(order=True, frozen=True)
180+
class SimDevices:
181+
"""Define a group of devices.
182+
183+
If wanting to use multiple devices in a single server,
184+
each SimDevice must be grouped with SimDevices.
185+
"""
186+
187+
#: Add a list of SimDevice
188+
devices: list[SimDevice]
189+
190+
def __post_init__(self):
191+
"""Define a group of devices."""
192+
if not isinstance(self.devices, list):
193+
raise TypeError("devices= must be a list of SimDevice")
194+
if not self.devices:
195+
raise TypeError("devices= must contain at least 1 SimDevice")
196+
list_id = []
197+
for device in self.devices:
198+
if not isinstance(device, SimDevice):
199+
raise TypeError("devices= contains non SimDevice entries")
200+
if device.id in list_id:
201+
raise TypeError(f"device_id={device.id} is duplicated")
202+
list_id.append(device.id)

0 commit comments

Comments
 (0)