Skip to content

Commit 5cc128d

Browse files
authored
Complete test for SimData / SimDevice. (#2798)
1 parent a1773ac commit 5cc128d

File tree

11 files changed

+386
-212
lines changed

11 files changed

+386
-212
lines changed

examples/server_datamodel.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,40 +32,41 @@ def define_datamodel():
3232
assert SimData(
3333
5, 10, 17, DataType.REGISTERS
3434
) == SimData(
35-
address=5, value=17, count=10, datatype=DataType.REGISTERS
35+
address=5, values=17, count=10, datatype=DataType.REGISTERS
3636
)
3737

3838
# Define a group of coils/direct inputs non-shared (address=15..31 each 1 bit)
39-
block1 = SimData(address=15, count=16, value=True, datatype=DataType.BITS)
39+
#block1 = SimData(address=15, count=16, values=True, datatype=DataType.BITS)
4040
# Define a group of coils/direct inputs shared (address=15..31 each 16 bit)
41-
block2 = SimData(address=15, count=16, value=0xFFFF, datatype=DataType.BITS)
41+
#block2 = SimData(address=15, count=16, values=0xFFFF, datatype=DataType.BITS)
4242

4343
# Define a group of holding/input registers (remark NO difference between shared and non-shared)
44-
block3 = SimData(10, 1, 123.4, datatype=DataType.FLOAT32)
45-
block4 = SimData(17, count=5, value=123, datatype=DataType.INT64)
44+
#block3 = SimData(10, 1, 123.4, datatype=DataType.FLOAT32)
45+
#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)
48+
block_def = SimData(0, count=1000, datatype=DataType.REGISTERS, default=True)
4949

5050
# SimDevice can be instantiated with positional or optional parameters:
5151
assert SimDevice(
52-
5,False, [block_def, block5]
52+
5,
53+
[block_def, block5],
5354
) == SimDevice(
54-
id=5, type_check=False, block_shared=[block_def, block5]
55+
id=5, type_check=False, registers=[block_def, block5]
5556
)
5657

5758
# SimDevice can define either a shared or a non-shared register model
58-
SimDevice(1, False, block_shared=[block_def, block5])
59-
SimDevice(2, False,
60-
block_coil=[block1],
61-
block_direct=[block1],
62-
block_holding=[block2],
63-
block_input=[block3, block4])
59+
SimDevice(id=1, type_check=False, registers=[block_def, block5])
60+
#SimDevice(2, False,
61+
# block_coil=[block1],
62+
# block_direct=[block1],
63+
# block_holding=[block2],
64+
# block_input=[block3, block4])
6465
# Remark: it is legal to reuse SimData, the object is only used for configuration,
6566
# not for runtime.
6667

6768
# id=0 in a SimDevice act as a "catch all". Requests to an unknown id is executed in this SimDevice.
68-
SimDevice(0, block_shared=[block2])
69+
#SimDevice(0, block_shared=[block2])
6970

7071

7172
def main():

pymodbus/constants.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
This is the single location for storing default
44
values for the servers and clients.
55
"""
6+
from __future__ import annotations
7+
68
import enum
7-
from typing import Union
89

910

1011
INTERNAL_ERROR = "Pymodbus internal error"
@@ -161,7 +162,7 @@ class DataType(enum.IntEnum):
161162
#: Registers == 2 bytes (identical to UINT16)
162163
REGISTERS = enum.auto()
163164

164-
DATATYPE_STRUCT: dict[DataType, tuple[Union[type, tuple[type, ...]], int]] = { # pylint: disable=consider-using-namedtuple-or-dataclass
165+
DATATYPE_STRUCT: dict[DataType, tuple[type | tuple[type, type], int]] = { # pylint: disable=consider-using-namedtuple-or-dataclass
165166
DataType.INT16: (int, 1),
166167
DataType.UINT16: (int, 1),
167168
DataType.INT32: (int, 2),
@@ -171,6 +172,6 @@ class DataType(enum.IntEnum):
171172
DataType.FLOAT32: (float, 2),
172173
DataType.FLOAT64: (float, 4),
173174
DataType.STRING: (str, -1),
174-
DataType.BITS: ((list, int, bool), -2),
175+
DataType.BITS: ((list, int), -2),
175176
DataType.REGISTERS: (int, 1),
176177
}

pymodbus/simulator/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Simulator."""
22

33
__all__ = [
4+
"SimAction",
45
"SimCore",
56
"SimData",
67
"SimDevice",
@@ -9,6 +10,7 @@
910

1011
from .simcore import SimCore
1112
from .simdata import (
13+
SimAction,
1214
SimData,
1315
SimValueType,
1416
)

pymodbus/simulator/simcore.py

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
from .simdevice import SimDevice
66

77

8-
class SimCore:
9-
"""Datastore for the simulator/server."""
8+
class SimCore: # pylint: disable=too-few-public-methods
9+
"""Handler for the simulator/server."""
1010

1111
def __init__(self) -> None:
1212
"""Build datastore."""
@@ -16,30 +16,3 @@ def __init__(self) -> None:
1616
def build_block(cls, _block: list[SimData]) -> tuple[int, int, int] | None:
1717
"""Build registers for device."""
1818
return None
19-
20-
@classmethod
21-
def build_config(cls, devices: list[SimDevice]) -> SimCore:
22-
"""Build devices/registers ready for server/simulator."""
23-
core = SimCore()
24-
for dev in devices:
25-
if dev.id in core.devices:
26-
raise TypeError(f"device id {dev.id} defined multiple times.")
27-
block_coil = block_direct = block_holding = block_input = block_shared = None
28-
for cfg_block, _block in (
29-
(dev.block_coil, block_coil),
30-
(dev.block_direct, block_direct),
31-
(dev.block_holding, block_holding),
32-
(dev.block_input, block_input),
33-
(dev.block_shared, block_shared)
34-
):
35-
if cfg_block:
36-
cls.build_block(cfg_block)
37-
38-
core.devices[dev.id] = dev
39-
# block_coil,
40-
# block_direct,
41-
# block_holding,
42-
# block_input,
43-
# block_shared
44-
#)
45-
return core

pymodbus/simulator/simdata.py

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@
44
import asyncio
55
from collections.abc import Awaitable, Callable
66
from dataclasses import dataclass
7-
from typing import TypeAlias
7+
from typing import TypeAlias, cast
88

99
from pymodbus.constants import DATATYPE_STRUCT, DataType
1010
from pymodbus.pdu import ExceptionResponse
1111

1212

13-
SimValueTypeSimple: TypeAlias = int | float | str | bool | bytes
14-
SimValueType: TypeAlias = SimValueTypeSimple | list[SimValueTypeSimple]
13+
SimValueTypeSimple: TypeAlias = int | float | str | bytes
14+
SimValueType: TypeAlias = SimValueTypeSimple | list[SimValueTypeSimple | bool]
1515
SimAction: TypeAlias = Callable[[int, int, list[int]], Awaitable[list[int] | ExceptionResponse]]
1616

17-
@dataclass(frozen=True)
17+
@dataclass(order=True, frozen=True)
1818
class SimData:
1919
"""Configure a group of continuous identical values/registers.
2020
@@ -83,7 +83,7 @@ class SimData:
8383

8484
#: Value/Values of datatype,
8585
#: will automatically be converted to registers, according to datatype.
86-
value: SimValueType = 0
86+
values: SimValueType = 0
8787

8888
#: Used to check access and convert value to/from registers.
8989
datatype: DataType = DataType.REGISTERS
@@ -104,22 +104,68 @@ class SimData:
104104
#: .. tip:: use functools.partial to add extra parameters if needed.
105105
action: SimAction | None = None
106106

107+
#: Mark register(s) as readonly.
108+
readonly: bool = False
107109

108-
def __post_init__(self):
109-
"""Define a group of registers."""
110-
if not isinstance(self.address, int) or not 0 <= self.address < 65535:
110+
#: Mark register(s) as invalid.
111+
#: **remark** only to be used with address= and count=
112+
invalid: bool = False
113+
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+
133+
def __check_simple(self):
134+
"""Check simple parameters."""
135+
if not isinstance(self.address, int) or not 0 <= self.address <= 65535:
111136
raise TypeError("0 <= address < 65535")
112-
if not isinstance(self.count, int) or not 0 <= self.count < 65535:
113-
raise TypeError("0 <= count < 65535")
137+
if not isinstance(self.count, int) or not 1 <= self.count <= 65536:
138+
raise TypeError("1 <= count < 65536")
139+
if not 1 <= self.address + self.count <= 65536:
140+
raise TypeError("1 <= address + count < 65536")
114141
if not isinstance(self.datatype, DataType):
115-
raise TypeError("datatype must by an DataType")
116-
if isinstance(self.value, list):
117-
if self.count > 1 or self.datatype == DataType.STRING:
118-
raise TypeError("count > 1 cannot be combined with given values=")
119-
for entry in self.value:
120-
if not isinstance(entry, DATATYPE_STRUCT[self.datatype][0]) or isinstance(entry, str):
121-
raise TypeError(f"elements in values must be {self.datatype!s} and not string")
122-
elif not isinstance(self.value, DATATYPE_STRUCT[self.datatype][0]):
123-
raise TypeError(f"value must be {self.datatype!s}")
142+
raise TypeError("datatype= must by an DataType")
124143
if self.action and not (callable(self.action) and asyncio.iscoroutinefunction(self.action)):
125-
raise TypeError("action not a async function")
144+
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")
149+
150+
def __post_init__(self):
151+
"""Define a group of registers."""
152+
self.__check_simple()
153+
if self.default:
154+
self.__check_default()
155+
x_datatype: type | tuple[type, type]
156+
if self.datatype == DataType.STRING:
157+
if not isinstance(self.values, str):
158+
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)
169+
170+
171+

0 commit comments

Comments
 (0)