Skip to content

Commit 3e2fcc4

Browse files
authored
SimRuntime generator, first take. (#2843)
1 parent 97c3bbf commit 3e2fcc4

10 files changed

Lines changed: 415 additions & 164 deletions

File tree

examples/server_datamodel.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
This file shows examples of how to configure the datamodel for the server/simulator.
55
66
There are different examples showing the flexibility of the datamodel.
7+
8+
**REMARK** This code is experimental and not integrated into production.
9+
710
"""
811

912
from pymodbus.constants import DataType
@@ -45,7 +48,7 @@ def define_datamodel():
4548
#block4 = SimData(17, count=5, values=123, datatype=DataType.INT64)
4649
block5 = SimData(27, 1, "Hello ", datatype=DataType.STRING)
4750

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

5053
# SimDevice can be instantiated with positional or optional parameters:
5154
assert SimDevice(

pymodbus/simulator/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
"""Simulator."""
1+
"""Simulator.
2+
3+
**REMARK** This code is experimental and not integrated into production.
4+
"""
25

36
__all__ = [
47
"SimAction",
58
"SimCore",
69
"SimData",
710
"SimDevice",
11+
"SimDevices",
812
"SimValueType",
913
]
1014

@@ -14,4 +18,4 @@
1418
SimData,
1519
SimValueType,
1620
)
17-
from .simdevice import SimDevice
21+
from .simdevice import SimDevice, SimDevices

pymodbus/simulator/simcore.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
"""Simulator data model implementation."""
1+
"""Simulator data model implementation.
2+
3+
**REMARK** This code is experimental and not integrated into production.
4+
"""
25
from __future__ import annotations
36

47
from .simdata import SimData

pymodbus/simulator/simdata.py

Lines changed: 29 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
"""Simulator data model classes."""
1+
"""Simulator data model classes.
2+
3+
**REMARK** This code is experimental and not integrated into production.
4+
"""
25
from __future__ import annotations
36

47
import inspect
@@ -25,12 +28,12 @@ class SimData:
2528
SimData(
2629
address=100,
2730
count=5,
28-
value=12345678
31+
values=12345678
2932
datatype=DataType.INT32
3033
)
3134
SimData(
3235
address=100,
33-
value=[1, 2, 3, 4, 5]
36+
values=[1, 2, 3, 4, 5]
3437
datatype=DataType.INT32
3538
)
3639
@@ -40,29 +43,38 @@ class SimData:
4043
4144
SimData(
4245
address=100,
43-
count=17,
44-
value=True
46+
count=16,
47+
values=True
48+
datatype=DataType.BITS
49+
)
50+
SimData(
51+
address=100,
52+
values=[True] * 16
4553
datatype=DataType.BITS
4654
)
4755
SimData(
4856
address=100,
49-
value=[0xffff, 1]
57+
values=0xffff
58+
datatype=DataType.BITS
59+
)
60+
SimData(
61+
address=100,
62+
values=[0xffff]
5063
datatype=DataType.BITS
5164
)
5265
53-
Each SimData defines 17 BITS (coils), with value True.
66+
Each SimData defines 16 BITS (coils), with value True.
5467
55-
In block mode (CO and DI) addresses are 100-116 (each 1 bit)
68+
Value are stored in registers (16bit is 1 register), the address refer to the register.
5669
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)
70+
**Remark** when using offsets, only bit 0 of each register is used!
5971
6072
.. code-block:: python
6173
6274
SimData(
6375
address=0,
6476
count=1000,
65-
value=0x1234
77+
values=0x1234
6678
datatype=DataType.REGISTERS
6779
)
6880
@@ -76,9 +88,10 @@ class SimData:
7688
#:
7789
#: - count=3 datatype=DataType.REGISTERS is 3 registers.
7890
#: - count=3 datatype=DataType.INT32 is 6 registers.
79-
#: - count=1 (default), value="ABCD" is 2 registers
91+
#: - count=1 datatype=DataType.STRING, values="ABCD" is 2 registers
92+
#: - count=2 datatype=DataType.STRING, values="ABCD" is 4 registers
8093
#:
81-
#: Cannot be used if value is a list or datatype is DataType.STRING
94+
#: Count cannot be used if values= is a list
8295
count: int = 1
8396

8497
#: Value/Values of datatype,
@@ -111,25 +124,6 @@ class SimData:
111124
#: **remark** only to be used with address= and count=
112125
invalid: bool = False
113126

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-
133127
def __check_simple(self):
134128
"""Check simple parameters."""
135129
if not isinstance(self.address, int) or not 0 <= self.address <= 65535:
@@ -142,23 +136,18 @@ def __check_simple(self):
142136
raise TypeError("datatype= must by an DataType")
143137
if self.action and not (callable(self.action) and inspect.iscoroutinefunction(self.action)):
144138
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")
149139

150140
def __post_init__(self):
151141
"""Define a group of registers."""
152142
self.__check_simple()
153-
if self.default:
154-
self.__check_default()
155-
x_datatype: type | tuple[type, type]
156143
if self.datatype == DataType.STRING:
157144
if not isinstance(self.values, str):
158145
raise TypeError("datatype=DataType.STRING only allows values=\"string\"")
159146
x_datatype, x_len = str, int((len(self.values) +1) / 2)
160147
else:
161-
x_datatype, x_len = DATATYPE_STRUCT[self.datatype]
148+
x = DATATYPE_STRUCT[self.datatype]
149+
x_len = x[1]
150+
x_datatype = cast(type[str], x[0])
162151
if not isinstance(self.values, list):
163152
super().__setattr__("values", [self.values])
164153
for x_value in cast(list, self.values):

pymodbus/simulator/simdevice.py

Lines changed: 113 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
"""Simulator device model classes."""
1+
"""Simulator device model classes.
2+
3+
**REMARK** This code is experimental and not integrated into production.
4+
"""
25
from __future__ import annotations
36

47
from dataclasses import dataclass
8+
from typing import cast
59

6-
from .simdata import SimData
10+
from pymodbus.constants import DATATYPE_STRUCT, DataType
711

12+
from .simdata import SimData
813

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

1115
@dataclass(order=True, frozen=True)
1216
class SimDevice:
@@ -62,70 +66,140 @@ class SimDevice:
6266
#:
6367
registers: list[SimData]
6468

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

7077
#: Enforce type checking, if True access are controlled to be conform with datatypes.
7178
#:
7279
#: Type violations like e.g. reading INT32 as INT16 are returned as ExceptionResponses,
7380
#: as well as being logged.
7481
type_check: bool = False
7582

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

7799
def __check_block(self, block: list[SimData]) -> list[SimData]:
78100
"""Check block content."""
101+
if not block:
102+
return block
79103
for inx, entry in enumerate(block):
80104
if not isinstance(entry, SimData):
81105
raise TypeError(f"registers[{inx}]= is a SimData entry")
82106
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."""
87107
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
93108
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?")
109+
last_address = self.__check_block_entries(last_address, entry)
110+
if self.default and block:
111+
first_address = block[0].address
112+
if self.default.address > first_address:
113+
raise TypeError("Default address is {self.default.address} but {first_address} is defined?")
114+
def_last_address = self.default.address + self.default.count -1
115+
if last_address > def_last_address:
116+
raise TypeError("Default address+count is {def_last_address} but {last_address} is defined?")
111117
return block
112118

113-
def __post_init__(self):
114-
"""Define a device."""
119+
def __check_block_entries(self, last_address: int, entry: SimData) -> int:
120+
"""Check block entries."""
121+
values = entry.values if isinstance(entry.values, list) else [entry.values]
122+
if entry.address <= last_address:
123+
raise TypeError("SimData address {entry.address} is overlapping!")
124+
if entry.datatype == DataType.BITS:
125+
if isinstance(values[0], bool):
126+
reg_count = int((len(values) + 15) / 16)
127+
else:
128+
reg_count = len(values)
129+
return entry.address + reg_count * entry.count -1
130+
if entry.datatype == DataType.STRING:
131+
return entry.address + len(cast(str, entry.values)) * entry.count -1
132+
register_count = DATATYPE_STRUCT[entry.datatype][1]
133+
return entry.address + register_count * entry.count -1
134+
135+
def __check_simple(self):
136+
"""Check simple parameters."""
115137
if not isinstance(self.id, int) or not 0 <= self.id <= 255:
116138
raise TypeError("0 <= id < 255")
117-
if not isinstance(self.registers, list) or not self.registers:
139+
if not isinstance(self.registers, list):
118140
raise TypeError("registers= not a list")
141+
if not self.default and not self.registers:
142+
raise TypeError("Either registers= or default= must contain SimData")
119143
if not isinstance(self.type_check, bool):
120144
raise TypeError("type_check= not a bool")
145+
if (not self.endian
146+
or not isinstance(self.endian, tuple)
147+
or len(self.endian) != 2
148+
or not isinstance(self.endian[0], bool)
149+
or not isinstance(self.endian[1], bool)
150+
):
151+
raise TypeError("endian= must be a tuple with 2 bool")
152+
if not isinstance(self.identity, str):
153+
raise TypeError("identity= must be a string")
154+
if not self.default:
155+
return
156+
if not isinstance(self.default, SimData):
157+
raise TypeError("default= must be a SimData object")
158+
if not self.default.datatype == DataType.REGISTERS:
159+
raise TypeError("default= only allow datatype=DataType.REGISTERS")
160+
161+
def __post_init__(self):
162+
"""Define a device."""
163+
self.__check_simple()
121164
super().__setattr__("registers", self.__check_block(self.registers))
122-
if self.offset_address != OFFSET_NONE:
165+
if self.offset_address is not None:
166+
if not isinstance(self.offset_address, tuple):
167+
raise TypeError("offset_address= must be a tuple")
123168
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
169+
raise TypeError("offset_address= must be a tuple with 4 addresses")
170+
if self.default:
171+
reg_start = self.default.address
172+
reg_end = self.default.address + self.default.count -1
173+
else:
174+
reg_start = self.registers[0].address
175+
reg_end = self.registers[-1].address
127176
for i in range(4):
128177
if not (reg_start < self.offset_address[i] < reg_end):
129178
raise TypeError(f"offset_address[{i}] outside defined range")
130179
if i and self.offset_address[i-1] >= self.offset_address[i]:
131180
raise TypeError("offset_address= must be ascending addresses")
181+
182+
@dataclass(order=True, frozen=True)
183+
class SimDevices:
184+
"""Define a group of devices.
185+
186+
If wanting to use multiple devices in a single server,
187+
each SimDevice must be grouped with SimDevices.
188+
"""
189+
190+
#: Add a list of SimDevice
191+
devices: list[SimDevice]
192+
193+
def __post_init__(self):
194+
"""Define a group of devices."""
195+
if not isinstance(self.devices, list):
196+
raise TypeError("devices= must be a list of SimDevice")
197+
if not self.devices:
198+
raise TypeError("devices= must contain at least 1 SimDevice")
199+
list_id = []
200+
for device in self.devices:
201+
if not isinstance(device, SimDevice):
202+
raise TypeError("devices= contains non SimDevice entries")
203+
if device.id in list_id:
204+
raise TypeError(f"device_id={device.id} is duplicated")
205+
list_id.append(device.id)

0 commit comments

Comments
 (0)